feat: 修复测试套件问题并添加Woodpecker CI配置
- 修复API测试认证问题:创建全局认证设置,更新Playwright配置 - 优化回归测试稳定性:增加超时时间到15秒,修复定位器 - 创建Woodpecker CI工作流:CI、部署和质量门禁配置 - 添加Jest配置和测试脚本 - 移除登录页面的默认账号密码显示(安全问题修复)
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
'use client';
|
||||
|
||||
import { useEditor, EditorContent } from '@tiptap/react';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import Image from '@tiptap/extension-image';
|
||||
import Link from '@tiptap/extension-link';
|
||||
import {
|
||||
Bold,
|
||||
Italic,
|
||||
Strikethrough,
|
||||
Code,
|
||||
Heading1,
|
||||
Heading2,
|
||||
Heading3,
|
||||
List,
|
||||
ListOrdered,
|
||||
Quote,
|
||||
Undo,
|
||||
Redo,
|
||||
Link as LinkIcon,
|
||||
Image as ImageIcon
|
||||
} from 'lucide-react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
interface RichTextEditorProps {
|
||||
content: string;
|
||||
onChange: (content: string) => void;
|
||||
}
|
||||
|
||||
export default function RichTextEditor({ content, onChange }: RichTextEditorProps) {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Image.configure({
|
||||
HTMLAttributes: {
|
||||
class: 'max-w-full h-auto rounded-lg',
|
||||
},
|
||||
}),
|
||||
Link.configure({
|
||||
openOnClick: false,
|
||||
HTMLAttributes: {
|
||||
class: 'text-[#C41E3A] underline',
|
||||
},
|
||||
}),
|
||||
],
|
||||
content,
|
||||
onUpdate: ({ editor }) => {
|
||||
onChange(editor.getHTML());
|
||||
},
|
||||
});
|
||||
|
||||
const handleImageUpload = useCallback(async () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/*';
|
||||
|
||||
input.onchange = async (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
setUploading(true);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('type', 'image');
|
||||
|
||||
const res = await fetch('/api/admin/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (res.ok && editor) {
|
||||
editor.chain().focus().setImage({ src: data.file.url }).run();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传图片失败:', error);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
input.click();
|
||||
}, [editor]);
|
||||
|
||||
const addLink = useCallback(() => {
|
||||
if (!editor) return;
|
||||
|
||||
const url = window.prompt('输入链接地址:');
|
||||
if (url) {
|
||||
editor.chain().focus().setLink({ href: url }).run();
|
||||
}
|
||||
}, [editor]);
|
||||
|
||||
if (!editor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border border-gray-300 rounded-lg overflow-hidden">
|
||||
<div className="bg-gray-50 border-b border-gray-300 p-2 flex flex-wrap gap-1">
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
className={`p-2 rounded hover:bg-gray-200 ${editor.isActive('bold') ? 'bg-gray-200' : ''}`}
|
||||
title="粗体"
|
||||
>
|
||||
<Bold className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
className={`p-2 rounded hover:bg-gray-200 ${editor.isActive('italic') ? 'bg-gray-200' : ''}`}
|
||||
title="斜体"
|
||||
>
|
||||
<Italic className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||
className={`p-2 rounded hover:bg-gray-200 ${editor.isActive('strike') ? 'bg-gray-200' : ''}`}
|
||||
title="删除线"
|
||||
>
|
||||
<Strikethrough className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleCode().run()}
|
||||
className={`p-2 rounded hover:bg-gray-200 ${editor.isActive('code') ? 'bg-gray-200' : ''}`}
|
||||
title="代码"
|
||||
>
|
||||
<Code className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<div className="w-px h-6 bg-gray-300 mx-1 self-center" />
|
||||
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
||||
className={`p-2 rounded hover:bg-gray-200 ${editor.isActive('heading', { level: 1 }) ? 'bg-gray-200' : ''}`}
|
||||
title="标题 1"
|
||||
>
|
||||
<Heading1 className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||
className={`p-2 rounded hover:bg-gray-200 ${editor.isActive('heading', { level: 2 }) ? 'bg-gray-200' : ''}`}
|
||||
title="标题 2"
|
||||
>
|
||||
<Heading2 className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
|
||||
className={`p-2 rounded hover:bg-gray-200 ${editor.isActive('heading', { level: 3 }) ? 'bg-gray-200' : ''}`}
|
||||
title="标题 3"
|
||||
>
|
||||
<Heading3 className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<div className="w-px h-6 bg-gray-300 mx-1 self-center" />
|
||||
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||
className={`p-2 rounded hover:bg-gray-200 ${editor.isActive('bulletList') ? 'bg-gray-200' : ''}`}
|
||||
title="无序列表"
|
||||
>
|
||||
<List className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||
className={`p-2 rounded hover:bg-gray-200 ${editor.isActive('orderedList') ? 'bg-gray-200' : ''}`}
|
||||
title="有序列表"
|
||||
>
|
||||
<ListOrdered className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
||||
className={`p-2 rounded hover:bg-gray-200 ${editor.isActive('blockquote') ? 'bg-gray-200' : ''}`}
|
||||
title="引用"
|
||||
>
|
||||
<Quote className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<div className="w-px h-6 bg-gray-300 mx-1 self-center" />
|
||||
|
||||
<button
|
||||
onClick={addLink}
|
||||
className={`p-2 rounded hover:bg-gray-200 ${editor.isActive('link') ? 'bg-gray-200' : ''}`}
|
||||
title="链接"
|
||||
>
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleImageUpload}
|
||||
disabled={uploading}
|
||||
className="p-2 rounded hover:bg-gray-200 disabled:opacity-50"
|
||||
title="上传图片"
|
||||
>
|
||||
{uploading ? (
|
||||
<div className="h-4 w-4 border-2 border-gray-400 border-t-transparent rounded-full animate-spin" />
|
||||
) : (
|
||||
<ImageIcon className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="w-px h-6 bg-gray-300 mx-1 self-center" />
|
||||
|
||||
<button
|
||||
onClick={() => editor.chain().focus().undo().run()}
|
||||
disabled={!editor.can().undo()}
|
||||
className="p-2 rounded hover:bg-gray-200 disabled:opacity-50"
|
||||
title="撤销"
|
||||
>
|
||||
<Undo className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().redo().run()}
|
||||
disabled={!editor.can().redo()}
|
||||
className="p-2 rounded hover:bg-gray-200 disabled:opacity-50"
|
||||
title="重做"
|
||||
>
|
||||
<Redo className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<EditorContent
|
||||
editor={editor}
|
||||
className="prose prose-sm max-w-none p-4 min-h-[300px] focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user