93 lines
2.3 KiB
TypeScript
93 lines
2.3 KiB
TypeScript
|
|
'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>
|
|||
|
|
);
|
|||
|
|
}
|