文章
nextjs使用wangEditor
1、富文本编辑器
RichEditor.tsx
"use client";
import "@wangeditor-next/editor/dist/css/style.css";
import { Editor, Toolbar } from "@wangeditor-next/editor-for-react";
import { IDomEditor, IEditorConfig, IToolbarConfig } from "@wangeditor-next/editor";
import { useEffect, useState } from "react";
interface RichEditorProps {
content: string;
onChange: (html: string) => void;
}
export default function RichEditor({ content, onChange }: RichEditorProps) {
const [editor, setEditor] = useState<IDomEditor | null>(null);
const toolbarConfig: Partial<IToolbarConfig> = {};
const editorConfig: Partial<IEditorConfig> = {
placeholder: "请输入内容...",
autoFocus: false,
};
const handleCreated = (editorInstance: IDomEditor) => {
setEditor(editorInstance);
editorInstance.setHtml(content || "<p></p>");
};
const handleChange = (editorInstance: IDomEditor) => {
const html = editorInstance.getHtml();
onChange(html);
};
useEffect(() => {
return () => {
if (editor) {
editor.destroy();
setEditor(null);
}
};
}, [editor]);
return (
<div style={{ border: "1px solid #ccc", zIndex: 100 }}>
<Toolbar
editor={editor}
defaultConfig={toolbarConfig}
mode="default"
style={{ borderBottom: "1px solid #ccc" }}
/>
<Editor
value={content}
defaultConfig={editorConfig}
onCreated={handleCreated}
onChange={handleChange}
mode="default"
style={{ height: "500px", overflowY: "hidden" }}
/>
</div>
);
}2、封装进表单组件
"use client";
import { FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
createDocumentDefaultValues,
createDocumentSchema,
CreateDocumentSchema,
} from "@/app/(protected)/dashboard/documents/_types/form-schema";
import { ControlledInput } from "@/components/controlled/controlled-input";
import dynamic from "next/dynamic";
import { Button } from "@/components/ui/button";
import { useEffect, useState } from "react";
const RichEditor = dynamic(() => import("@/components/rich-editor"), {
ssr: false,
loading: () => <p className="text-sm text-muted-foreground">加载编辑器...</p>,
});
interface DocumentFormProps {
mode: "create" | "edit";
initialData?: {
title: string;
content: string;
};
isPending?: boolean;
onSubmit: (data: { title: string; content: string }) => void;
}
export function DocumentForm({
mode,
initialData,
isPending,
onSubmit,
}: DocumentFormProps) {
const form = useForm<CreateDocumentSchema>({
defaultValues: createDocumentDefaultValues,
resolver: zodResolver(createDocumentSchema),
});
const [content, setContent] = useState("");
/** 编辑模式:回填 */
useEffect(() => {
if (mode === "edit" && initialData) {
form.reset({ title: initialData.title });
setTimeout(() => {
setContent(initialData.content)
}, 50);
}
}, [mode, initialData, form]);
const handleSubmit = (data: CreateDocumentSchema) => {
onSubmit({
title: data.title,
content,
});
};
return (
<FormProvider {...form}>
<form
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-6 max-w-4xl mx-auto"
>
{/* 标题 */}
<ControlledInput<CreateDocumentSchema>
name="title"
label="标题"
placeholder="请输入文档标题"
/>
{/* 内容 */}
<div className="space-y-2">
<label className="text-sm font-medium">内容</label>
<RichEditor content={content} onChange={setContent} />
</div>
<div className="flex justify-end">
<Button type="submit" disabled={isPending}>
{mode === "edit" ? "更新文档" : "创建文档"}
</Button>
</div>
</form>
</FormProvider>
);
}
这里的form-schema.ts
import { requiredStringSchema } from "@/lib/zod-schemas";
import z from "zod";
// 创建文档表单
export const createDocumentSchema = z.object({
title: requiredStringSchema,
});
export type CreateDocumentSchema = z.infer<typeof createDocumentSchema>;
export const createDocumentDefaultValues: CreateDocumentSchema = {
title: "",
};
// 更新文档表单
export const updateDocumentSchema = z.object({
id: z.string(),
title: requiredStringSchema,
});
export type UpdateDocumentSchema = z.infer<typeof updateDocumentSchema>;
export const updateDocumentDefaultValues: UpdateDocumentSchema = {
id: "",
title: "",
};新建文档
"use client";
import { DocumentForm } from "@/app/(protected)/dashboard/documents/_compoents/document-form";
import { useCreateDocument } from "@/app/(protected)/dashboard/documents/_services/use-document-mutations";
import { useRouter } from "next/navigation";
import { DocumentLayout } from "@/app/(protected)/dashboard/documents/_compoents/document-layout";
export default function NewDocumentPage() {
const router = useRouter();
const createMutation = useCreateDocument();
return (
<DocumentLayout variant="edit">
<div className="p-8">
<h1 className="text-2xl font-semibold mb-6">创建文档</h1>
<DocumentForm
mode="create"
isPending={createMutation.isPending}
onSubmit={(data) => {
createMutation.mutate(data, {
onSuccess: () => {
router.push("/dashboard/documents");
},
});
}}
/>
</div>
</DocumentLayout>
);
}
编辑文档
"use client";
import { useDocument } from "@/app/(protected)/dashboard/documents/_services/use-document-queries";
import { useUpdateDocument } from "@/app/(protected)/dashboard/documents/_services/use-document-mutations";
import { useParams, useRouter } from "next/navigation";
import { DocumentForm } from "@/app/(protected)/dashboard/documents/_compoents/document-form";
import { DocumentLayout } from "@/app/(protected)/dashboard/documents/_compoents/document-layout";
export default function EditDocumentPage() {
const { id } = useParams<{ id: string }>();
const router = useRouter();
const documentQuery = useDocument(id);
const updateMutation = useUpdateDocument();
if (documentQuery.isLoading) {
return <p className="p-8">加载中...</p>;
}
if (!documentQuery.data) {
return <p className="p-8">文档不存在</p>;
}
const { title, content } = documentQuery.data;
return (
<DocumentLayout variant="edit">
<div className="p-8">
<h1 className="text-2xl font-semibold mb-6">编辑文档</h1>
<DocumentForm
mode="edit"
initialData={{ title, content }}
isPending={updateMutation.isPending}
onSubmit={(data) => {
updateMutation.mutate(
{
id,
...data,
},
{
onSuccess: () => {
router.push("/dashboard/documents");
},
}
);
}}
/>
</div>
</DocumentLayout>
);
}
文档详情
"use client";
import { useParams, useRouter } from "next/navigation";
import { useDocument } from "@/app/(protected)/dashboard/documents/_services/use-document-queries";
import { Button } from "@/components/ui/button";
import { Edit, ArrowLeft } from "lucide-react";
import { DocumentLayout } from "@/app/(protected)/dashboard/documents/_compoents/document-layout";
export default function DocumentDetailPage() {
const { id } = useParams<{ id: string }>();
const router = useRouter();
const documentQuery = useDocument(id);
if (documentQuery.isLoading) {
return <div className="p-8">加载中...</div>;
}
if (!documentQuery.data) {
return <div className="p-8">文档不存在</div>;
}
const { title, content } = documentQuery.data;
return (
<DocumentLayout variant="read">
<div className="max-w-4xl mx-auto py-8 px-4 space-y-6">
{/* 顶部操作栏 */}
<div className="flex items-center justify-between">
<Button
variant="ghost"
onClick={() => router.push("/dashboard/documents")}
>
<ArrowLeft className="mr-2 h-4 w-4" />
返回列表
</Button>
<Button
onClick={() =>
router.push(`/dashboard/documents/${id}/edit`)
}
>
<Edit className="mr-2 h-4 w-4" />
编辑
</Button>
</div>
{/* 标题 */}
<h1 className="text-3xl font-bold">{title}</h1>
{/* 正文 */}
<div
className="prose prose-neutral max-w-none editor-content"
dangerouslySetInnerHTML={{ __html: content }}
/>
</div>
</DocumentLayout>
);
}
修复表格缺少边框,global.css添加
.editor-content table {
border-collapse: collapse;
border: 1px solid #ccc;
}
.editor-content table th,
.editor-content table td {
border: 1px solid #ccc;
padding: 8px;
text-align: left;
}
/* 表头浅灰色背景 */
.editor-content table th {
background-color: #f5f5f5; /* 或者 #f0f0f0, #eee 等你喜欢的浅灰 */
}