+
+
+
runCommand("undo")}>
+
runCommand("redo")}>
+
+
} onClick={insertImage} />
+
+
+
+
+
+ fileInputRef.current?.click()}>附件
+ 链接
+ 图片链接
+ runCommand("insertHorizontalRule")}>分隔线
+
+
+
} onClick={() => fileInputRef.current?.click()} />
+
} onClick={() => runCommand("insertText", "\n[日程]\n")} />
+
} onClick={() => runCommand("insertText", "🙂")} />
+
} active={formatOpen} onClick={() => setFormatOpen((value) => !value)} />
+
+ } onClick={insertSignature} disabled={!signatureText.trim()} />
+
+ {formatOpen && (
+
+
runCommand("removeFormat")}>
+
+
+
+
+
+
+ {["Arial", "Georgia", "Times New Roman", "Courier New"].map((font) => (
+ runCommand("fontName", font)}>{font}
+ ))}
+
+
+
+
+
+
+
+ {[["2", "小号"], ["3", "正文"], ["4", "中号"], ["5", "大号"]].map(([size, label]) => (
+ runCommand("fontSize", size)}>{label}
+ ))}
+
+
+
+
runCommand("bold")}>
+
runCommand("italic")}>
+
runCommand("underline")}>
+
runCommand("strikeThrough")}>
+
+
+
+
+
+ {[["#111827", "默认"], ["#dc2626", "红色"], ["#2563eb", "蓝色"], ["#16a34a", "绿色"]].map(([color, label]) => (
+ runCommand("foreColor", color)}>
+ {label}
+
+ ))}
+
+
+
+
+
+
+
+ {[["transparent", "无高亮"], ["#fef3c7", "黄色"], ["#dcfce7", "绿色"], ["#dbeafe", "蓝色"]].map(([color, label]) => (
+ runCommand("backColor", color)}>
+ {label}
+
+ ))}
+
+
+
+
runCommand("insertUnorderedList")}>
+
runCommand("insertOrderedList")}>
+
runCommand("outdent")}>
+
runCommand("indent")}>
+
+
runCommand("justifyLeft")}>
+
runCommand("justifyCenter")}>
+
runCommand("justifyRight")}>
+
runCommand("formatBlock", "blockquote")}>
+
runCommand("formatBlock", "pre")}>
+
+ )}
+
+ {empty &&
输入正文
}
+
{ dirtyRef.current = true; syncEditor() }}
+ onBlur={syncEditor}
+ dangerouslySetInnerHTML={{ __html: initialHtmlRef.current }}
+ />
+
+ {files.length > 0 && (
+
+
+ {files.map((file, index) => (
+
+
+ {file.name}
+ {formatBytes(file.size)}
+
+
+ ))}
+
+
+ )}
)
}
+function ToolbarTextButton({ label, icon, active, disabled, onClick }: { label: string; icon: React.ReactNode; active?: boolean; disabled?: boolean; onClick?: () => void }) {
+ return (
+
+ )
+}
+
function ToolbarButton({ label, children, onClick, disabled }: { label: string; children: React.ReactNode; onClick?: () => void; disabled?: boolean }) {
return
}
function splitEmails(s: string) { return s.split(/[;,,\s]+/).map((v) => v.trim()).filter(Boolean) }
-function markdownToHtml(value: string) { return DOMPurify.sanitize(marked.parse(value, { async: false, breaks: true })) }
+function plainTextComposerValue(value: string): ComposerValue { return { text: value, html: plainTextToHtml(value) } }
+function composerValueFromElement(element: HTMLElement | null): ComposerValue {
+ const text = (element?.innerText || "").replace(/\u00a0/g, " ").trimEnd()
+ if (!text.trim()) return { text: "", html: "" }
+ const html = DOMPurify.sanitize(element?.innerHTML || "")
+ return { text, html: html || plainTextToHtml(text) }
+}
+function plainTextToHtml(value: string) {
+ const normalized = value.replace(/\r\n/g, "\n")
+ if (!normalized.trim()) return ""
+ return DOMPurify.sanitize(normalized.split(/\n{2,}/).map((paragraph) => `
${plainTextToHtmlFragment(paragraph) || "
"}
`).join(""))
+}
+function plainTextToHtmlFragment(value: string) { return value.split("\n").map((line) => escapeHtml(line)).join("
") }
+function escapeHtml(value: string) {
+ return value.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'")
+}
function playIncomingMailSound(ref: React.MutableRefObject
) {
const AudioContextCtor = window.AudioContext || (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext
if (!AudioContextCtor) return