import React, { useEffect, useState, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import Sidebar from '../components/Sidebar'; import { Plus, Edit, Trash2, Save, Printer, Undo, Redo, Bold, Italic, Underline, AlignLeft, AlignCenter, AlignRight, Table, Image as ImageIcon, Bot, Check, Download, Upload } from 'lucide-react'; import { User, Template, FormField, FieldType, DEFAULT_FORM_FIELDS } from '../types'; import { defaultReportContent } from '../utils/defaultContent'; import { printDocument } from '../utils/print'; import { storage } from '../utils/storage'; export default function TemplateManage() { const navigate = useNavigate(); const [currentUser, setCurrentUser] = useState(null); const [templates, setTemplates] = useState([]); const [currentTemplateId, setCurrentTemplateId] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); const [exportModalOpen, setExportModalOpen] = useState(false); const [isEditing, setIsEditing] = useState(false); const [formData, setFormData] = useState({ name: '', desc: '' }); const [importedContent, setImportedContent] = useState<{content: string; fields: FormField[]} | null>(null); const fileInputRef = useRef(null); const [isSaved, setIsSaved] = useState(false); const editorRef = useRef(null); const savedRangeRef = useRef(null); const undoStack = useRef([]); const redoStack = useRef([]); const [fieldLibTab, setFieldLibTab] = useState<'insert' | 'manage'>('insert'); const [formFields, setFormFields] = useState([]); const [newFieldForm, setNewFieldForm] = useState({ label: '', category: '填空', type: 'text' as FieldType }); const [newFieldOptions, setNewFieldOptions] = useState(''); const [expandedCategories, setExpandedCategories] = useState(['填空', '单选', '多选', '时间']); const [imagePickerOpen, setImagePickerOpen] = useState(false); const [imagePickerTarget, setImagePickerTarget] = useState(null); const [activeFieldKey, setActiveFieldKey] = useState(null); const [editingFieldKey, setEditingFieldKey] = useState(null); const [editFieldLabel, setEditFieldLabel] = useState(''); const [editFieldOptions, setEditFieldOptions] = useState(''); const [editFieldTimeFormat, setEditFieldTimeFormat] = useState(''); const [editFieldTimeDefault, setEditFieldTimeDefault] = useState<'current' | 'specific'>('specific'); const [editFieldFixedTimeValue, setEditFieldFixedTimeValue] = useState(''); const [editFieldHasUnderline, setEditFieldHasUnderline] = useState(false); const [newFieldTimeFormat, setNewFieldTimeFormat] = useState('YYYY年MM月DD日'); const [newFieldTimeDefault, setNewFieldTimeDefault] = useState<'current' | 'specific'>('specific'); const [newFieldFixedTimeValue, setNewFieldFixedTimeValue] = useState(''); const [newFieldHasUnderline, setNewFieldHasUnderline] = useState(false); const [customTimeFormats, setCustomTimeFormats] = useState([]); const [formatDropdownOpen, setFormatDropdownOpen] = useState(false); const [newFormatDropdownOpen, setNewFormatDropdownOpen] = useState(false); const [placeholderModal, setPlaceholderModal] = useState({ isOpen: false, width: '200', height: '200', mode: 'frame' as 'frame' | 'manual' }); const [tableModal, setTableModal] = useState({ isOpen: false, rows: '2', cols: '3' }); const [imageAssets, setImageAssets] = useState<{ id: string; name: string; dataUrl: string }[]>([]); const [selectedIds, setSelectedIds] = useState([]); const updatePageHeight = () => { if (!editorRef.current) return; const contentHeight = editorRef.current.scrollHeight; const pageHeightMm = 297; const mmToPx = 3.7795275591; const pages = Math.max(2, Math.ceil(contentHeight / (pageHeightMm * mmToPx))); editorRef.current.style.minHeight = `${pages * pageHeightMm}mm`; }; useEffect(() => { const user = storage.get('currentUser', null); if (!user || user.role === 'user') { navigate('/dashboard'); return; } setCurrentUser(user); const savedFields = storage.get('formFieldsConfig', []); if (savedFields.length > 0) { setFormFields(savedFields); } else { setFormFields(DEFAULT_FORM_FIELDS); storage.set('formFieldsConfig', DEFAULT_FORM_FIELDS); } const savedAssets = storage.get<{ id: string; name: string; dataUrl: string }[]>('imageAssets', []); if (savedAssets.length > 0) { setImageAssets(savedAssets); } else { fetch('/logo_square.png') .then((res) => res.blob()) .then((blob) => { const reader = new FileReader(); reader.onloadend = () => { const dataUrl = reader.result as string; const initialAssets = [{ id: 'asset_logo', name: '医院Logo', dataUrl }]; setImageAssets(initialAssets); storage.set('imageAssets', initialAssets); }; reader.readAsDataURL(blob); }) .catch(() => setImageAssets([])); } const savedFormats = storage.get('customTimeFormats', []); const defaultFormats = ['YYYY-MM-DD', 'YYYY年MM月DD日', 'MM-DD', 'MM月DD日', 'HH:mm', 'hh:mm A']; const cleanedSaved = savedFormats.filter(f => f !== '24h' && f !== '12h'); setCustomTimeFormats(Array.from(new Set([...defaultFormats, ...cleanedSaved]))); const savedTemplates = storage.get('templates', []); if (savedTemplates.length === 0) { const initial: Template = { id: 'surgery', name: '腹腔镜胆囊切除术报告', desc: '标准手术记录模板', content: defaultReportContent, createdAt: new Date().toISOString(), author: 'admin' }; setTemplates([initial]); storage.set('templates', [initial]); setCurrentTemplateId(initial.id); } else { const manageable = user.role === 'super' ? savedTemplates.map(t => t.id) : (Array.isArray(user.manageableTemplates) ? user.manageableTemplates : savedTemplates.map(t => t.id)); const filtered = savedTemplates.filter(t => manageable.includes(t.id)); setTemplates(filtered); setCurrentTemplateId(filtered[0]?.id || null); } }, [navigate]); useEffect(() => { if (currentTemplateId && editorRef.current) { const template = templates.find(t => t.id === currentTemplateId); if (template) { editorRef.current.innerHTML = template.content; if (template.fields && template.fields.length > 0) { setFormFields(template.fields); storage.set('formFieldsConfig', template.fields); } } setTimeout(() => updatePageHeight(), 0); } }, [currentTemplateId, templates]); useEffect(() => { if (!editorRef.current) return; const observer = new MutationObserver(() => { updatePageHeight(); }); observer.observe(editorRef.current, { childList: true, subtree: true, attributes: true, characterData: true }); return () => observer.disconnect(); }, [currentUser]); const fillPlaceholderSrc = (placeholder: HTMLElement, src: string) => { const mw = placeholder.style.maxWidth || placeholder.style.width || '200px'; const mh = placeholder.style.maxHeight || placeholder.style.height || '200px'; placeholder.innerHTML = ` × `; placeholder.classList.add('has-image'); placeholder.style.border = 'none'; placeholder.style.background = 'transparent'; placeholder.style.width = 'auto'; placeholder.style.height = 'auto'; placeholder.style.lineHeight = 'normal'; placeholder.style.maxWidth = mw; placeholder.style.maxHeight = mh; placeholder.style.textAlign = 'left'; placeholder.style.verticalAlign = 'top'; placeholder.style.justifyContent = 'flex-start'; placeholder.style.alignItems = 'flex-start'; saveTemplateContent(); }; // Handle image placeholder and smart field delete interactions via click capture useEffect(() => { const handleEditorClick = (e: MouseEvent) => { let node: Node | null = e.target as Node; if (node.nodeType === Node.TEXT_NODE) node = node.parentElement; const targetEl = node as HTMLElement | null; if (!targetEl) return; const smartField = targetEl.closest('.smart-field-wrapper') as HTMLElement | null; if (smartField && targetEl.closest('.delete-btn')) { e.stopPropagation(); e.preventDefault(); pushHistory(); const sel = window.getSelection(); const range = document.createRange(); range.selectNode(smartField); sel?.removeAllRanges(); sel?.addRange(range); document.execCommand('delete'); saveTemplateContent(); return; } if (smartField) { const valueSpan = smartField.querySelector('.field-value'); const fieldKey = valueSpan?.getAttribute('data-bind') || smartField.getAttribute('data-bind'); if (fieldKey) { setActiveFieldKey(fieldKey); const field = formFields.find(f => f.key === fieldKey); if (field) { setExpandedCategories(prev => prev.includes(field.category) ? prev : [...prev, field.category]); setTimeout(() => { const el = document.getElementById(`sidebar-field-${fieldKey}`); el?.scrollIntoView({ behavior: 'smooth', block: 'center' }); }, 50); } } return; } const placeholder = targetEl.closest('.image-placeholder') as HTMLElement | null; if (!placeholder) return; if (targetEl.closest('.delete-btn')) { e.stopPropagation(); e.preventDefault(); pushHistory(); if (placeholder.classList.contains('has-image')) { placeholder.classList.remove('has-image'); const w = parseInt(placeholder.style.maxWidth || placeholder.style.width || '0'); const text = w > 0 && w < 80 ? '插图' : '插入/点击放置图片'; placeholder.innerHTML = ` × ${text} `; placeholder.style.border = '1px dashed #cbd5e1'; placeholder.style.background = '#f8fafc'; const mw = placeholder.style.maxWidth; const mh = placeholder.style.maxHeight; if (mw) placeholder.style.width = mw; if (mh) { placeholder.style.height = mh; placeholder.style.lineHeight = mh; } placeholder.style.textAlign = 'center'; placeholder.style.verticalAlign = 'middle'; placeholder.style.justifyContent = 'center'; placeholder.style.alignItems = 'center'; } else { const range = document.createRange(); range.selectNode(placeholder); const sel = window.getSelection(); sel?.removeAllRanges(); sel?.addRange(range); document.execCommand('delete'); } return; } if (!placeholder.classList.contains('has-image')) { e.preventDefault(); e.stopPropagation(); setImagePickerTarget(placeholder); setImagePickerOpen(true); } }; const editor = editorRef.current; if (editor) { editor.addEventListener('click', handleEditorClick, true); } return () => { if (editor) { editor.removeEventListener('click', handleEditorClick, true); } }; }, [currentTemplateId, currentUser, formFields]); // Intercept Backspace/Delete next to smart fields to avoid whole-line deletion useEffect(() => { const editor = editorRef.current; if (!editor) return; const handleKeyDown = (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'z') { e.preventDefault(); if (e.shiftKey) { handleRedo(); } else { handleUndo(); } return; } if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'y') { e.preventDefault(); handleRedo(); return; } if (e.key !== 'Backspace' && e.key !== 'Delete') return; const sel = window.getSelection(); if (!sel || !sel.isCollapsed || sel.rangeCount === 0) return; const range = sel.getRangeAt(0); const node = range.startContainer; const offset = range.startOffset; let target: Element | null = null; if (node.nodeType === Node.TEXT_NODE) { if (e.key === 'Backspace' && offset === 0) { const prev = node.previousSibling; if (prev && prev.nodeType === Node.ELEMENT_NODE && (prev as Element).classList?.contains('smart-field-wrapper')) { target = prev as Element; } } else if (e.key === 'Delete' && offset === (node.textContent?.length || 0)) { const next = node.nextSibling; if (next && next.nodeType === Node.ELEMENT_NODE && (next as Element).classList?.contains('smart-field-wrapper')) { target = next as Element; } } } else if (node.nodeType === Node.ELEMENT_NODE) { const el = node as Element; if (e.key === 'Backspace' && offset > 0) { const prev = el.childNodes[offset - 1]; if (prev && prev.nodeType === Node.ELEMENT_NODE && (prev as Element).classList?.contains('smart-field-wrapper')) { target = prev as Element; } } else if (e.key === 'Delete' && offset < el.childNodes.length) { const next = el.childNodes[offset]; if (next && next.nodeType === Node.ELEMENT_NODE && (next as Element).classList?.contains('smart-field-wrapper')) { target = next as Element; } } } if (target) { e.preventDefault(); pushHistory(); const sel = window.getSelection(); const range = document.createRange(); range.selectNode(target); sel?.removeAllRanges(); sel?.addRange(range); document.execCommand('delete'); saveTemplateContent(); } }; editor.addEventListener('keydown', handleKeyDown, true); return () => { editor.removeEventListener('keydown', handleKeyDown, true); }; }, [currentTemplateId]); const saveSelection = () => { const sel = window.getSelection(); if (sel && sel.rangeCount > 0) { savedRangeRef.current = sel.getRangeAt(0); } }; const restoreSelection = () => { if (!savedRangeRef.current) return; const sel = window.getSelection(); sel?.removeAllRanges(); sel?.addRange(savedRangeRef.current); }; const pushHistory = () => { if (!editorRef.current) return; undoStack.current.push(editorRef.current.innerHTML); redoStack.current = []; }; const handleUndo = () => { if (undoStack.current.length === 0 || !editorRef.current) return; redoStack.current.push(editorRef.current.innerHTML); const prev = undoStack.current.pop(); if (prev !== undefined) { editorRef.current.innerHTML = prev; saveTemplateContent(); } }; const handleRedo = () => { if (redoStack.current.length === 0 || !editorRef.current) return; undoStack.current.push(editorRef.current.innerHTML); const next = redoStack.current.pop(); if (next !== undefined) { editorRef.current.innerHTML = next; saveTemplateContent(); } }; const execCmd = (command: string, value: string | undefined = undefined) => { if (command !== 'undo' && command !== 'redo') { pushHistory(); } editorRef.current?.focus(); document.execCommand(command, false, value); editorRef.current?.focus(); }; const changeLineHeight = (height: string) => { const sel = window.getSelection(); if (!sel || !sel.rangeCount) return; let node = sel.getRangeAt(0).commonAncestorContainer; if (node.nodeType === Node.TEXT_NODE) node = node.parentNode as Node; const block = (node as HTMLElement).closest('p, div, td, h1, h2, h3, li'); if (block) { (block as HTMLElement).style.lineHeight = height; saveTemplateContent(); } }; const changeAlignment = (align: 'left' | 'center' | 'right' | 'justify') => { const sel = window.getSelection(); if (!sel || !sel.rangeCount) return; let node = sel.getRangeAt(0).commonAncestorContainer; if (node.nodeType === Node.TEXT_NODE) node = node.parentNode as Node; const block = (node as HTMLElement).closest('p, div, td, h1, h2, h3, li'); if (block) { (block as HTMLElement).style.textAlign = align; saveTemplateContent(); } }; const saveTemplateContent = () => { if (!currentTemplateId || !editorRef.current) return; let cleanContent = editorRef.current.innerHTML; cleanContent = cleanContent.replace(/

\s*\s*<\/p>/gi, ''); cleanContent = cleanContent.replace(/

<\/p>/gi, ''); cleanContent = cleanContent.replace(/>(\s+)<'); if (cleanContent !== editorRef.current.innerHTML) { editorRef.current.innerHTML = cleanContent; } const allTemplates = storage.get('templates', []); const updated = allTemplates.map(t => t.id === currentTemplateId ? { ...t, content: cleanContent, updatedAt: new Date().toISOString() } : t ); setTemplates(prevTemplates => prevTemplates.map(t => updated.find(u => u.id === t.id) || t)); storage.set('templates', updated); }; const insertSmartField = (field: FormField) => { editorRef.current?.focus(); restoreSelection(); if (editorRef.current?.querySelector(`[data-bind="${field.key}"]`)) { alert(`字段 "${field.label}" 已存在,请勿重复插入。`); return; } pushHistory(); const underlineClass = field.hasUnderline !== true ? ' no-underline' : ''; const html = ` ×​`; const sel = window.getSelection(); if (sel && sel.rangeCount > 0) { const range = sel.getRangeAt(0); range.deleteContents(); const wrapper = document.createElement('div'); wrapper.innerHTML = html; const fragment = document.createDocumentFragment(); while (wrapper.firstChild) { fragment.appendChild(wrapper.firstChild); } range.insertNode(fragment); const lastNode = fragment.lastChild; if (lastNode) { const newRange = document.createRange(); newRange.setStartAfter(lastNode); newRange.collapse(true); sel.removeAllRanges(); sel.addRange(newRange); } } editorRef.current?.focus(); saveTemplateContent(); }; const highlightField = (key: string, active: boolean) => { if (!editorRef.current) return; const el = editorRef.current.querySelector(`[data-bind="${key}"]`) as HTMLElement | null; if (!el) return; if (active) { el.style.transition = 'all 0.2s'; el.style.boxShadow = '0 0 0 2px #3b82f6'; el.style.backgroundColor = '#e0f2fe'; } else { el.style.boxShadow = ''; el.style.backgroundColor = ''; } }; const toggleFieldVisible = (key: string) => { const updated = formFields.map(f => f.key === key ? { ...f, visibleInForm: !f.visibleInForm } : f); setFormFields(updated); storage.set('formFieldsConfig', updated); }; const deleteField = (key: string) => { const updated = formFields.filter(f => f.key !== key); setFormFields(updated); storage.set('formFieldsConfig', updated); }; const saveFieldEdit = (key: string) => { const updated = formFields.map(f => { if (f.key !== key) return f; const next: FormField = { ...f }; if (!f.isSystemLocked) { next.label = editFieldLabel.trim() || f.label; } if (['单选', '多选', '图片'].includes(f.category)) { next.options = editFieldOptions.split(/[,,]/).map(s => s.trim()).filter(Boolean); } if (f.category === '时间') { next.timeFormat = editFieldTimeFormat; next.timeDefault = editFieldTimeDefault; next.fixedTimeValue = editFieldFixedTimeValue; } next.hasUnderline = editFieldHasUnderline; return next; }); setFormFields(updated); storage.set('formFieldsConfig', updated); setEditingFieldKey(null); // 同步更新编辑器中已插入字段的 classList if (editorRef.current) { const els = editorRef.current.querySelectorAll(`.field-value[data-bind="${key}"]`); els.forEach(el => { if (editFieldHasUnderline) { el.classList.remove('no-underline'); } else { el.classList.add('no-underline'); } }); saveTemplateContent(); } }; const addField = () => { if (!newFieldForm.label.trim()) return; const key = 'custom_' + Date.now(); const newField: FormField = { key, label: newFieldForm.label.trim(), category: newFieldForm.category, type: newFieldForm.type, visibleInForm: true, isSystemLocked: false, hasUnderline: newFieldHasUnderline, options: ['单选', '多选'].includes(newFieldForm.category) && newFieldOptions.trim() ? newFieldOptions.split(/[,,]/).map(s => s.trim()).filter(Boolean) : undefined }; if (newFieldForm.category === '时间') { newField.timeFormat = newFieldTimeFormat; newField.timeDefault = newFieldTimeDefault; newField.fixedTimeValue = newFieldFixedTimeValue; } const updated = [...formFields, newField]; setFormFields(updated); storage.set('formFieldsConfig', updated); setNewFieldForm({ label: '', category: '填空', type: 'text' }); setNewFieldOptions(''); setNewFieldTimeFormat('YYYY年MM月DD日'); setNewFieldTimeDefault('specific'); setNewFieldFixedTimeValue(''); setNewFieldHasUnderline(true); }; const handleAssetUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { const dataUrl = event.target?.result as string; const asset = { id: 'asset_' + Date.now(), name: file.name, dataUrl }; const updated = [...imageAssets, asset]; setImageAssets(updated); storage.set('imageAssets', updated); }; reader.readAsDataURL(file); e.target.value = ''; }; const insertTable = () => { editorRef.current?.focus(); restoreSelection(); pushHistory(); setTableModal({ isOpen: true, rows: '2', cols: '3' }); }; const insertImage = () => { editorRef.current?.focus(); restoreSelection(); pushHistory(); setPlaceholderModal({ isOpen: true, width: '200', height: '200', mode: 'frame' }); }; const insertAiRegion = () => { const name = window.prompt('请输入 AI 可编辑区域的名称(如:手术步骤、病灶描述):'); if (!name || !name.trim()) return; if (editorRef.current?.querySelector(`[data-ai-id="${name}"]`)) { window.alert('该区域名称已存在,请使用其他名称以保证 AI 定位准确。'); return; } editorRef.current?.focus(); // Insert ai-region HTML const html = `

${name}-AI可编辑区域


`; document.execCommand('insertHTML', false, html); saveTemplateContent(); }; const saveCurrentTemplate = () => { if (!currentTemplateId || !editorRef.current) return; let cleanContent = editorRef.current.innerHTML; cleanContent = cleanContent.replace(/

\s*\s*<\/p>/gi, ''); cleanContent = cleanContent.replace(/

<\/p>/gi, ''); cleanContent = cleanContent.replace(/>(\s+)<'); if (cleanContent !== editorRef.current.innerHTML) { editorRef.current.innerHTML = cleanContent; } const allTemplates = storage.get('templates', []); const updated = allTemplates.map(t => { if (t.id === currentTemplateId) { return { ...t, content: cleanContent, updatedAt: new Date().toISOString() }; } return t; }); setTemplates(updated.filter(t => templates.some(x => x.id === t.id))); storage.set('templates', updated); setIsSaved(true); setTimeout(() => setIsSaved(false), 3000); }; const handleAddTemplate = () => { setIsEditing(false); setFormData({ name: '', desc: '' }); setIsModalOpen(true); }; const handleEditInfo = (template: Template) => { setIsEditing(true); setFormData({ name: template.name, desc: template.desc || '' }); setIsModalOpen(true); }; const handleDeleteTemplate = (id: string) => { if (window.confirm('确定要删除此模板吗?')) { const allTemplates = storage.get('templates', []); const updated = allTemplates.filter(t => t.id !== id); setTemplates(updated); storage.set('templates', updated); if (currentTemplateId === id) { setCurrentTemplateId(updated[0]?.id || null); } setSelectedIds(prev => prev.filter(sid => sid !== id)); } }; const handleBatchDelete = () => { if (selectedIds.length === 0) return; if (!window.confirm(`确定要删除选中的 ${selectedIds.length} 个模板吗?`)) return; const allTemplates = storage.get('templates', []); const updated = allTemplates.filter(t => !selectedIds.includes(t.id)); setTemplates(updated); storage.set('templates', updated); if (currentTemplateId && selectedIds.includes(currentTemplateId)) { setCurrentTemplateId(updated[0]?.id || null); } setSelectedIds([]); }; const handleBatchExport = () => { if (selectedIds.length === 0) return; const targets = templates.filter(t => selectedIds.includes(t.id)); const ts = new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString().replace(/[:.]/g, '-').slice(0, 16); const exportData = { version: '1.0', type: 'surclaw_template_package_batch', templates: targets }; const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `模板批量导出-${ts}.json`; a.click(); URL.revokeObjectURL(url); }; const handleImportFile = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { try { const json = JSON.parse(event.target?.result as string); if (json.type !== 'surclaw_template_package') { alert('无效的模板包文件'); return; } setFormData({ name: json.title || '', desc: json.description || '' }); setImportedContent({ content: json.content || '', fields: Array.isArray(json.fields) ? json.fields : [] }); } catch { alert('文件解析失败,请检查 JSON 格式'); } }; reader.readAsText(file); if (e.target) e.target.value = ''; }; const handleExportTemplate = (template: Template) => { const exportData = { version: '1.0', type: 'surclaw_template_package', title: template.name, description: template.desc || '', content: template.content, fields: template.fields || formFields }; const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; const ts = new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString().replace(/[:.]/g, '-').slice(0, 16); a.download = `模板导出-${template.name}-${ts}.json`; a.click(); URL.revokeObjectURL(url); }; const handleModalSubmit = (e: React.FormEvent) => { e.preventDefault(); const allTemplates = storage.get('templates', []); if (isEditing) { const updated = allTemplates.map(t => { if (t.id === currentTemplateId) { return { ...t, name: formData.name, desc: formData.desc }; } return t; }); setTemplates(updated.filter(t => templates.some(x => x.id === t.id))); storage.set('templates', updated); } else { const newTpl: Template = { id: 'tpl_' + Date.now(), name: formData.name, desc: formData.desc, content: importedContent?.content || defaultReportContent, createdAt: new Date().toISOString(), author: currentUser?.username || 'admin', fields: importedContent?.fields || formFields }; const updated = [...allTemplates, newTpl]; setTemplates([...templates, newTpl]); storage.set('templates', updated); setCurrentTemplateId(newTpl.id); if (importedContent?.fields && importedContent.fields.length > 0) { setFormFields(importedContent.fields); storage.set('formFieldsConfig', importedContent.fields); } const savedUsers = storage.get('users', []); let updatedUsers = savedUsers; if (currentUser?.role === 'super') { updatedUsers = savedUsers.map(u => { if (u.role === 'super') { const mt = [...(u.manageableTemplates || [])]; const vt = [...(u.visibleTemplates || [])]; if (!mt.includes(newTpl.id)) mt.push(newTpl.id); if (!vt.includes(newTpl.id)) vt.push(newTpl.id); return { ...u, manageableTemplates: mt, visibleTemplates: vt }; } return u; }); } else if (currentUser?.role === 'admin') { const dept = currentUser.department || ''; updatedUsers = savedUsers.map(u => { if (u.username === currentUser.username) { const mt = [...(u.manageableTemplates || [])]; const vt = [...(u.visibleTemplates || [])]; if (!mt.includes(newTpl.id)) mt.push(newTpl.id); if (!vt.includes(newTpl.id)) vt.push(newTpl.id); return { ...u, manageableTemplates: mt, visibleTemplates: vt }; } if (u.role === 'user' && u.department === dept) { const vt = [...(u.visibleTemplates || [])]; if (!vt.includes(newTpl.id)) vt.push(newTpl.id); return { ...u, visibleTemplates: vt }; } return u; }); } storage.set('users', updatedUsers); const currentCached = updatedUsers.find(u => u.username === currentUser?.username); if (currentCached) { storage.set('currentUser', currentCached); setCurrentUser(currentCached); } } setIsModalOpen(false); setImportedContent(null); }; if (!currentUser) return null; const currentTemplate = templates.find(t => t.id === currentTemplateId); return (

{/* Template List Sidebar */} {/* Main Editor */}

模板管理

{currentTemplate ? currentTemplate.name : '请选择模板'}

{isSaved && ( 已保存 )}
{/* Editor Main */}
{/* Toolbar */}
e.preventDefault()} onChange={(e) => execCmd('foreColor', e.target.value)} className="w-9 h-9 p-1.5 bg-transparent border-none cursor-pointer rounded-lg hover:bg-white transition-colors" title="文字颜色" />