website-01/components/admin/RichEditor.tsx

93 lines
2.3 KiB
TypeScript
Raw Normal View History

2026-06-22 14:43:46 +08:00
'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: () => (
<div className="flex h-64 items-center justify-center rounded bg-gray-50 text-xs text-gray-400">
</div>
),
});
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 (
<div
className={cn(
'overflow-hidden rounded-md border border-gray-300 bg-white',
className,
)}
>
<ReactQuill
theme="snow"
value={value ?? ''}
onChange={onChange}
modules={modules}
placeholder={placeholder}
/>
</div>
);
}