Files
labelimage/components/label/components/EditorContainer.tsx
2026-04-01 16:24:43 +08:00

180 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { Box } from "@mantine/core"
import { RichTextEditor } from "@mantine/tiptap"
import "@mantine/tiptap/styles.css"
import { IconPhoto } from "@tabler/icons-react"
import Image from "@tiptap/extension-image"
import Link from "@tiptap/extension-link"
import Placeholder from "@tiptap/extension-placeholder"
import Underline from "@tiptap/extension-underline"
import { useEditor } from "@tiptap/react"
import StarterKit from "@tiptap/starter-kit"
import { useEffect, useRef, type ChangeEvent } from "react"
import { useKeyEventStore } from "../store"
import { useTopToolsStore } from "../useTopToolsStore"
export const splitWord = "@1024-"
const getEditorHtml = (rawValue?: string) => {
if (!rawValue) return ""
const [html] = rawValue.split(splitWord)
return html ?? rawValue
}
const editorLabels = {
boldControlLabel: "加粗",
italicControlLabel: "斜体",
underlineControlLabel: "下划线",
strikeControlLabel: "删除线",
clearFormattingControlLabel: "清除格式",
codeControlLabel: "行内代码",
h1ControlLabel: "一级标题",
h2ControlLabel: "二级标题",
h3ControlLabel: "三级标题",
h4ControlLabel: "四级标题",
bulletListControlLabel: "无序列表",
orderedListControlLabel: "有序列表",
blockquoteControlLabel: "引用",
hrControlLabel: "分割线",
linkControlLabel: "插入链接",
unlinkControlLabel: "移除链接",
linkEditorInputLabel: "输入链接地址",
linkEditorInputPlaceholder: "请输入链接例如https://example.com",
linkEditorExternalLink: "在新标签页打开",
linkEditorInternalLink: "在当前标签页打开",
linkEditorSave: "保存",
}
interface EditorContainerProps {
value: string
onChange: (value: string) => void
color?: string
disabled?: boolean
}
const EditorContainer = ({
value,
onChange,
color,
disabled,
}: EditorContainerProps) => {
const { isView } = useTopToolsStore()
const fileInputRef = useRef<HTMLInputElement>(null)
const editor = useEditor({
immediatelyRender: false,
extensions: [
StarterKit,
Underline,
Link,
Image,
Placeholder.configure({ placeholder: "请输入内容..." }),
],
content: getEditorHtml(value),
editable: !disabled && !isView,
onUpdate: ({ editor }) => {
const html = getEditorHtml(editor.getHTML())
onChange(`${html}${splitWord}${editor.getText()}`)
},
onFocus: () => {
useKeyEventStore.getState().setFocusInput(true)
},
onBlur: () => {
useKeyEventStore.getState().setFocusInput(false)
},
})
useEffect(() => {
if (editor) {
editor.setEditable(!disabled && !isView)
}
}, [disabled, isView, editor])
useEffect(() => {
if (!editor) return
const incomingHtml = getEditorHtml(value) || "<p></p>"
if (editor.getHTML() !== incomingHtml) {
// Keep editor content in sync with external state, including empty value switch.
editor.commands.setContent(incomingHtml, { emitUpdate: false })
}
}, [value, editor])
const handleImageUpload = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (file && editor) {
const reader = new FileReader()
reader.onload = (e) => {
const result = e.target?.result as string
editor.chain().focus().setImage({ src: result }).run()
}
reader.readAsDataURL(file)
}
}
return (
<Box
className={color}
style={{
border: "1px solid var(--mantine-color-gray-3)",
borderRadius: "8px",
padding: "4px",
}}>
<RichTextEditor
editor={editor}
labels={editorLabels}
style={{ border: 0 }}>
<RichTextEditor.Toolbar>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Bold />
<RichTextEditor.Italic />
<RichTextEditor.Underline />
<RichTextEditor.Strikethrough />
<RichTextEditor.ClearFormatting />
<RichTextEditor.Code />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.H1 />
<RichTextEditor.H2 />
<RichTextEditor.H3 />
<RichTextEditor.H4 />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.BulletList />
<RichTextEditor.OrderedList />
<RichTextEditor.Blockquote />
<RichTextEditor.Hr />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Link />
<RichTextEditor.Unlink />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Control
onClick={() => fileInputRef.current?.click()}
aria-label="上传图片"
title="上传图片">
<IconPhoto stroke={1.5} size="1rem" />
</RichTextEditor.Control>
</RichTextEditor.ControlsGroup>
</RichTextEditor.Toolbar>
<RichTextEditor.Content h={144} style={{ overflowY: "auto" }} />
</RichTextEditor>
<input
type="file"
ref={fileInputRef}
style={{ display: "none" }}
accept="image/*"
onChange={handleImageUpload}
/>
</Box>
)
}
export default EditorContainer