2026-04-18-20-03-44 - 模板导入导出迁移、Logo替换为可交互占位符

This commit is contained in:
Administrator
2026-04-18 20:08:43 +08:00
parent 9c09e6cccc
commit c5648077e8
6 changed files with 324 additions and 6 deletions

View File

@@ -1,7 +1,7 @@
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, Check, Download } from 'lucide-react';
import { Plus, Edit, Trash2, Save, Printer, Undo, Redo, Bold, Italic, Underline, AlignLeft, AlignCenter, AlignRight, Table, Image as ImageIcon, 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';
@@ -16,6 +16,8 @@ export default function TemplateManage() {
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<HTMLInputElement>(null);
const [isSaved, setIsSaved] = useState(false);
const editorRef = useRef<HTMLDivElement>(null);
const savedRangeRef = useRef<Range | null>(null);
@@ -128,6 +130,10 @@ export default function TemplateManage() {
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);
}
@@ -598,6 +604,48 @@ export default function TemplateManage() {
}
};
const handleImportFile = (e: React.ChangeEvent<HTMLInputElement>) => {
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;
a.download = `模板导出-${template.name}.json`;
a.click();
URL.revokeObjectURL(url);
};
const handleModalSubmit = (e: React.FormEvent) => {
e.preventDefault();
const allTemplates = storage.get<Template[]>('templates', []);
@@ -615,14 +663,19 @@ export default function TemplateManage() {
id: 'tpl_' + Date.now(),
name: formData.name,
desc: formData.desc,
content: defaultReportContent,
content: importedContent?.content || defaultReportContent,
createdAt: new Date().toISOString(),
author: currentUser?.username || 'admin'
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<User[]>('users', []);
let updatedUsers = savedUsers;
@@ -663,6 +716,7 @@ export default function TemplateManage() {
}
}
setIsModalOpen(false);
setImportedContent(null);
};
if (!currentUser) return null;
@@ -708,6 +762,12 @@ export default function TemplateManage() {
>
</button>
<button
onClick={(e) => { e.stopPropagation(); handleExportTemplate(tpl); }}
className="px-2 py-1 rounded-md bg-blue-50 text-blue-600 text-[10px] font-bold hover:bg-blue-100 transition-colors"
>
</button>
{templates.length > 1 && (
<button
onClick={(e) => { e.stopPropagation(); handleDeleteTemplate(tpl.id); }}
@@ -1291,6 +1351,19 @@ export default function TemplateManage() {
<div className="bg-white rounded-2xl p-10 w-full max-w-[500px] shadow-2xl border border-border">
<h3 className="text-xl font-bold text-text-main mb-2">{isEditing ? '编辑模板信息' : '新增模板'}</h3>
<p className="text-sm text-text-muted mb-8"></p>
{!isEditing && (
<div className="flex items-center gap-3 mb-6 p-3 bg-slate-50 rounded-xl border border-dashed border-slate-200">
<div className="text-xs text-text-muted flex-1"></div>
<button
type="button"
onClick={() => fileInputRef.current?.click()}
className="w-8 h-8 bg-accent text-white rounded-lg flex items-center justify-center hover:bg-blue-700 transition-colors shadow-sm"
>
<Upload size={16} />
</button>
<input ref={fileInputRef} type="file" accept=".json" className="hidden" onChange={handleImportFile} />
</div>
)}
<form onSubmit={handleModalSubmit} className="space-y-6">
<div className="space-y-1.5">
<label className="block text-xs font-bold text-text-main uppercase tracking-wider"> *</label>
@@ -1315,7 +1388,7 @@ export default function TemplateManage() {
<div className="flex justify-end gap-3 pt-4 border-t border-border">
<button
type="button"
onClick={() => setIsModalOpen(false)}
onClick={() => { setIsModalOpen(false); setImportedContent(null); }}
className="px-6 py-2.5 bg-slate-100 text-text-muted rounded-lg text-sm font-semibold hover:bg-slate-200 transition-colors"
>