'use client'; import dynamic from 'next/dynamic'; import { useMemo } from 'react'; import { cn } from '@/lib/utils'; // quill 主题样式(client 组件可直接 import) import 'react-quill/dist/quill.snow.css'; // react-quill 仅支持客户端渲染 const ReactQuill = dynamic(() => import('react-quill'), { ssr: false, loading: () => (
编辑器加载中…
), }); export interface RichEditorProps { value?: string; onChange: (html: string) => void; placeholder?: string; className?: string; } const TOOLBAR = [ [{ header: [1, 2, 3, 4, 5, 6, false] }], ['bold', 'italic', 'underline', 'strike'], [{ color: [] }, { background: [] }], [{ align: [] }, { list: 'ordered' }, { list: 'bullet' }], ['blockquote', 'code-block'], ['link', 'image'], ['clean'], ]; // 图片插入:转 base64 内联(如需对接后端上传,可在此处替换实现) // quill 的 toolbar handler this 指向 toolbar 实例,需要 any // eslint-disable-next-line @typescript-eslint/no-explicit-any function imageHandler(this: any): void { const quill = this?.quill; if (!quill) return; const input = document.createElement('input'); input.setAttribute('type', 'file'); input.setAttribute('accept', 'image/*'); input.addEventListener('change', () => { const file = input.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => { const range = quill.getSelection(); const idx = range ? range.index : 0; quill.insertEmbed(idx, 'image', String(reader.result)); }; reader.readAsDataURL(file); }); input.click(); } export function RichEditor({ value, onChange, placeholder = '请输入内容…', className, }: RichEditorProps) { const modules = useMemo( () => ({ toolbar: { container: TOOLBAR, handlers: { image: imageHandler }, }, }), [], ); return (
); }