fix: 修复后台文章计数为0 + 分类编辑补全描述字段 + CSP放行外部图片 + 更新关于页描述
- 文章管理:计数请求补 page=1 参数,命中分页接口返回正确的 total - 分类管理:编辑模式新增描述输入框,保存时一并提交 description - CSP:img-src 加入 https: 允许加载外部图片 - 关于页:数据存储描述从 JSON 文件更正为 SQLite 数据库 - Footer:添加 ICP 备案号
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
"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 Placeholder from "@tiptap/extension-placeholder";
|
||||
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
|
||||
import { common, createLowlight } from "lowlight";
|
||||
import { Toggle } from "@/components/ui/toggle";
|
||||
import {
|
||||
Bold,
|
||||
Italic,
|
||||
Strikethrough,
|
||||
Code,
|
||||
Heading1,
|
||||
Heading2,
|
||||
Heading3,
|
||||
Quote,
|
||||
List,
|
||||
ListOrdered,
|
||||
Link as LinkIcon,
|
||||
ImageIcon,
|
||||
CodeSquare,
|
||||
Minus,
|
||||
Undo2,
|
||||
Redo2,
|
||||
} from "lucide-react";
|
||||
|
||||
const lowlight = createLowlight(common);
|
||||
|
||||
interface RichEditorProps {
|
||||
value: string;
|
||||
onChange: (html: string) => void;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export default function RichEditor({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = "开始写文章...",
|
||||
}: RichEditorProps) {
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
codeBlock: false,
|
||||
}),
|
||||
Image.configure({
|
||||
HTMLAttributes: { class: "rounded-lg my-4" },
|
||||
}),
|
||||
Link.configure({
|
||||
openOnClick: false,
|
||||
HTMLAttributes: {
|
||||
class: "text-primary underline underline-offset-2",
|
||||
},
|
||||
}),
|
||||
Placeholder.configure({ placeholder }),
|
||||
CodeBlockLowlight.configure({ lowlight }),
|
||||
],
|
||||
content: value,
|
||||
onUpdate: ({ editor }) => {
|
||||
onChange(editor.getHTML());
|
||||
},
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class:
|
||||
"prose-literary min-h-[300px] px-4 py-3 focus:outline-none",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!editor) return null;
|
||||
|
||||
function addLink() {
|
||||
const url = window.prompt("输入链接地址:");
|
||||
if (url) {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.extendMarkRange("link")
|
||||
.setLink({ href: url })
|
||||
.run();
|
||||
}
|
||||
}
|
||||
|
||||
function addImage() {
|
||||
const url = window.prompt("输入图片地址:");
|
||||
if (url) {
|
||||
editor.chain().focus().setImage({ src: url }).run();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-border bg-card overflow-hidden">
|
||||
{/* 工具栏 */}
|
||||
<div className="flex flex-wrap items-center gap-0.5 px-2 py-1.5 border-b border-border bg-muted/30">
|
||||
{/* 格式 */}
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={editor.isActive("heading", { level: 1 })}
|
||||
onPressedChange={() =>
|
||||
editor.chain().focus().toggleHeading({ level: 1 }).run()
|
||||
}
|
||||
aria-label="标题 1"
|
||||
>
|
||||
<Heading1 className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={editor.isActive("heading", { level: 2 })}
|
||||
onPressedChange={() =>
|
||||
editor.chain().focus().toggleHeading({ level: 2 }).run()
|
||||
}
|
||||
aria-label="标题 2"
|
||||
>
|
||||
<Heading2 className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={editor.isActive("heading", { level: 3 })}
|
||||
onPressedChange={() =>
|
||||
editor.chain().focus().toggleHeading({ level: 3 }).run()
|
||||
}
|
||||
aria-label="标题 3"
|
||||
>
|
||||
<Heading3 className="h-4 w-4" />
|
||||
</Toggle>
|
||||
|
||||
<div className="w-px h-5 bg-border mx-1" />
|
||||
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={editor.isActive("bold")}
|
||||
onPressedChange={() => editor.chain().focus().toggleBold().run()}
|
||||
aria-label="粗体"
|
||||
>
|
||||
<Bold className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={editor.isActive("italic")}
|
||||
onPressedChange={() => editor.chain().focus().toggleItalic().run()}
|
||||
aria-label="斜体"
|
||||
>
|
||||
<Italic className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={editor.isActive("strike")}
|
||||
onPressedChange={() => editor.chain().focus().toggleStrike().run()}
|
||||
aria-label="删除线"
|
||||
>
|
||||
<Strikethrough className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={editor.isActive("code")}
|
||||
onPressedChange={() => editor.chain().focus().toggleCode().run()}
|
||||
aria-label="行内代码"
|
||||
>
|
||||
<Code className="h-4 w-4" />
|
||||
</Toggle>
|
||||
|
||||
<div className="w-px h-5 bg-border mx-1" />
|
||||
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={editor.isActive("blockquote")}
|
||||
onPressedChange={() =>
|
||||
editor.chain().focus().toggleBlockquote().run()
|
||||
}
|
||||
aria-label="引用"
|
||||
>
|
||||
<Quote className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={editor.isActive("bulletList")}
|
||||
onPressedChange={() =>
|
||||
editor.chain().focus().toggleBulletList().run()
|
||||
}
|
||||
aria-label="无序列表"
|
||||
>
|
||||
<List className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={editor.isActive("orderedList")}
|
||||
onPressedChange={() =>
|
||||
editor.chain().focus().toggleOrderedList().run()
|
||||
}
|
||||
aria-label="有序列表"
|
||||
>
|
||||
<ListOrdered className="h-4 w-4" />
|
||||
</Toggle>
|
||||
|
||||
<div className="w-px h-5 bg-border mx-1" />
|
||||
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={editor.isActive("codeBlock")}
|
||||
onPressedChange={() =>
|
||||
editor.chain().focus().toggleCodeBlock().run()
|
||||
}
|
||||
aria-label="代码块"
|
||||
>
|
||||
<CodeSquare className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={editor.isActive("link")}
|
||||
onPressedChange={addLink}
|
||||
aria-label="链接"
|
||||
>
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle size="sm" onPressedChange={addImage} aria-label="图片">
|
||||
<ImageIcon className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle
|
||||
size="sm"
|
||||
onPressedChange={() =>
|
||||
editor.chain().focus().setHorizontalRule().run()
|
||||
}
|
||||
aria-label="分割线"
|
||||
>
|
||||
<Minus className="h-4 w-4" />
|
||||
</Toggle>
|
||||
|
||||
<div className="w-px h-5 bg-border mx-1" />
|
||||
|
||||
{/* 撤销/重做 */}
|
||||
<Toggle
|
||||
size="sm"
|
||||
onPressedChange={() => editor.chain().focus().undo().run()}
|
||||
disabled={!editor.can().undo()}
|
||||
aria-label="撤销"
|
||||
>
|
||||
<Undo2 className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle
|
||||
size="sm"
|
||||
onPressedChange={() => editor.chain().focus().redo().run()}
|
||||
disabled={!editor.can().redo()}
|
||||
aria-label="重做"
|
||||
>
|
||||
<Redo2 className="h-4 w-4" />
|
||||
</Toggle>
|
||||
</div>
|
||||
|
||||
{/* 编辑区域 */}
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user