2026-04-18-20-03-44 - 模板导入导出迁移、Logo替换为可交互占位符
This commit is contained in:
@@ -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"
|
||||
>
|
||||
取消
|
||||
|
||||
@@ -67,6 +67,7 @@ export interface Template {
|
||||
createdAt: string;
|
||||
updatedAt?: string;
|
||||
author: string;
|
||||
fields?: FormField[];
|
||||
}
|
||||
|
||||
export interface SystemSettings {
|
||||
|
||||
@@ -6,9 +6,9 @@ const smartField = (key: string) => {
|
||||
|
||||
export const defaultReportContent = `
|
||||
<div style="display: flex; justify-content: center; align-items: center; gap: 12px; margin-bottom: 4px;">
|
||||
<span class="image-placeholder" data-placeholder="true" contenteditable="false" data-mode="manual" style="position:relative;display:inline-flex;align-items:center;justify-content:center;width:65px;height:65px;border:1px dashed #cbd5e1;background:#f8fafc;vertical-align:middle;cursor:pointer;">
|
||||
<span class="image-placeholder" data-placeholder="true" contenteditable="false" data-mode="manual" style="display:inline-block;text-align:center;width:65px;height:65px;line-height:65px;border:1px dashed #cbd5e1;background:#f8fafc;vertical-align:middle;margin:0 4px;cursor:pointer;position:relative;">
|
||||
<span class="delete-btn" contenteditable="false">×</span>
|
||||
<span class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);display:block;width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">插入图片</span>
|
||||
<span class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);display:block;width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">LOGO</span>
|
||||
</span>
|
||||
<div style="text-align: center;">
|
||||
<div style="font-size: 14pt; font-family: SimSun; border-bottom: 1px solid #000; padding-bottom: 0; margin-bottom: 8px; display: inline-block; line-height: 1;">西 安 交 通 大 学 第 一 附 属 医 院</div>
|
||||
|
||||
130
工程分析/实现方案-2026-04-18-20-03-44.md
Normal file
130
工程分析/实现方案-2026-04-18-20-03-44.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# 实现方案 —— 2026-04-18-20-03-44
|
||||
|
||||
## 方案目标
|
||||
实现模板的导入/导出迁移能力,统一默认模板 Logo 的交互行为。
|
||||
|
||||
## 需求 1:模板导出功能
|
||||
|
||||
### 修改文件
|
||||
`src/pages/TemplateManage.tsx`
|
||||
|
||||
### 修改内容
|
||||
在模板列表的每个模板行操作列中增加「导出」按钮(使用 Download 图标)。点击时:
|
||||
```ts
|
||||
const handleExportTemplate = (template: Template) => {
|
||||
const exportData = {
|
||||
version: '1.0',
|
||||
type: 'surclaw_template_package',
|
||||
title: template.title,
|
||||
description: template.description,
|
||||
content: template.content,
|
||||
fields: template.fields || []
|
||||
};
|
||||
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.title}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
```
|
||||
|
||||
## 需求 2:模板导入功能
|
||||
|
||||
### 修改文件
|
||||
`src/pages/TemplateManage.tsx`
|
||||
|
||||
### 修改内容
|
||||
1. **新增状态**:
|
||||
```ts
|
||||
const [importedContent, setImportedContent] = useState<{content: string, fields: FormField[]} | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
```
|
||||
|
||||
2. **新增导入处理函数**:
|
||||
```ts
|
||||
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;
|
||||
}
|
||||
setNewTemplateTitle(json.title || '');
|
||||
setNewTemplateDescription(json.description || '');
|
||||
setImportedContent({
|
||||
content: json.content || '',
|
||||
fields: Array.isArray(json.fields) ? json.fields : []
|
||||
});
|
||||
} catch {
|
||||
alert('文件解析失败,请检查 JSON 格式');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
```
|
||||
|
||||
3. **修改创建逻辑**:在 `handleCreateTemplate` 中,如果有 `importedContent`,优先使用导入的内容和字段:
|
||||
```ts
|
||||
const newTemplate: Template = {
|
||||
id: 'tmpl_' + Date.now(),
|
||||
title: newTemplateTitle,
|
||||
description: newTemplateDescription,
|
||||
content: importedContent?.content || `<div style="font-size:12pt;line-height:1.5;"><p>请输入模板内容...</p></div>`,
|
||||
fields: importedContent?.fields || [],
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
```
|
||||
|
||||
4. **UI 调整**:在新增模板 Modal 中标题下方加入导入区域:
|
||||
```tsx
|
||||
<div className="flex items-center gap-3 mb-4 p-3 bg-slate-50 rounded-xl border border-dashed border-slate-200">
|
||||
<div className="text-xs text-text-muted flex-1">已有模板文件?点击右侧图标导入</div>
|
||||
<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>
|
||||
```
|
||||
|
||||
5. **关闭 Modal 时重置**:`setImportedContent(null)`
|
||||
|
||||
## 需求 3:Logo 替换为可交互占位符
|
||||
|
||||
### 修改文件
|
||||
`src/utils/defaultContent.ts`
|
||||
|
||||
### 修改内容
|
||||
将默认模板顶部的 Logo HTML 替换为标准 `image-placeholder`:
|
||||
```html
|
||||
<span class="image-placeholder" data-placeholder="true" contenteditable="false" data-mode="manual" style="display:inline-block;text-align:center;width:65px;height:65px;line-height:65px;border:1px dashed #cbd5e1;background:#f8fafc;vertical-align:middle;margin:0 4px;cursor:pointer;position:relative;">
|
||||
<span class="delete-btn" contenteditable="false">×</span>
|
||||
<span class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);display:block;width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">LOGO</span>
|
||||
</span>
|
||||
```
|
||||
|
||||
关键点:
|
||||
- `class="image-placeholder"`:触发编辑器中的占位符交互逻辑
|
||||
- `data-mode="manual"`:标记为静态图片占位,不支持自动帧插入
|
||||
- `position:relative` + `position:absolute` 居中:确保提示文字绝对居中
|
||||
- `delete-btn`:支持点击右上方的「×」删除
|
||||
|
||||
## 涉及文件及修改点
|
||||
| 文件 | 修改点 |
|
||||
|------|--------|
|
||||
| `src/pages/TemplateManage.tsx` | 新增 `handleExportTemplate`;新增 `importedContent` 状态和 `handleImportFile`;修改 `handleCreateTemplate` 使用导入数据;新增模板 Modal 中增加导入 UI;模板列表操作列增加导出按钮 |
|
||||
| `src/utils/defaultContent.ts` | 顶部 Logo 替换为标准 `image-placeholder` |
|
||||
|
||||
## 风险与注意事项
|
||||
1. 导入的 JSON 中 `fields` 数组需要与 `FormField` 类型结构兼容。由于 JSON 导入的是纯数据,直接赋值给 `template.fields` 即可(TypeScript 编译时类型校验通过)。
|
||||
2. 导出文件名中包含模板标题,需注意标题中的特殊字符可能影响文件名(但浏览器通常会自动处理)。
|
||||
3. Logo 占位符替换后,原有「西安交通大学第一附属医院」的样式应保持不变,仅替换 Logo 部分。
|
||||
4. 新增模板弹窗关闭时,需同步重置 `importedContent` 为 `null`,避免影响下一次创建。
|
||||
62
工程分析/测试方案-2026-04-18-20-03-44.md
Normal file
62
工程分析/测试方案-2026-04-18-20-03-44.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# 测试方案 —— 2026-04-18-20-03-44
|
||||
|
||||
## 测试目标
|
||||
验证模板导入/导出功能和默认模板 Logo 替换的正确性。
|
||||
|
||||
## 测试用例
|
||||
|
||||
### TC-1:模板导出
|
||||
**前置条件**:模板列表中已有至少一个模板,且该模板有内容和字段配置。
|
||||
**步骤**:
|
||||
1. 在模板列表中找到目标模板。
|
||||
2. 点击操作列的「导出」按钮。
|
||||
**预期结果**:
|
||||
- 浏览器下载一个 JSON 文件,文件名为 `模板导出-{模板名称}.json`。
|
||||
- JSON 内容包含 `version`、`type`、`title`、`description`、`content`、`fields` 字段。
|
||||
- `fields` 数组与模板原有的字段配置一致。
|
||||
|
||||
### TC-2:模板导入(自动填充名称和描述)
|
||||
**前置条件**:已有一个有效的模板导出 JSON 文件。
|
||||
**步骤**:
|
||||
1. 点击「新增模板」按钮。
|
||||
2. 在弹窗中点击导入图标,选择 JSON 文件。
|
||||
**预期结果**:
|
||||
- 模板名称输入框自动填充为 JSON 中的 `title`。
|
||||
- 模板描述输入框自动填充为 JSON 中的 `description`。
|
||||
- 无控制台报错。
|
||||
|
||||
### TC-3:模板导入后创建
|
||||
**前置条件**:已完成 TC-2 的导入操作。
|
||||
**步骤**:
|
||||
1. 点击「创建」按钮。
|
||||
2. 在新创建的模板中点击「编辑模板」。
|
||||
**预期结果**:
|
||||
- 编辑器中显示的内容与导入 JSON 中的 `content` 一致。
|
||||
- 字段管理中的配置与导入 JSON 中的 `fields` 一致。
|
||||
|
||||
### TC-4:导入无效文件
|
||||
**前置条件**:准备一个非 JSON 文件或格式错误的 JSON。
|
||||
**步骤**:
|
||||
1. 在新增模板弹窗中选择无效文件。
|
||||
**预期结果**:
|
||||
- 弹出提示「文件解析失败,请检查 JSON 格式」或「无效的模板包文件」。
|
||||
- 表单保持空白,不填充任何数据。
|
||||
|
||||
### TC-5:Logo 占位符交互
|
||||
**前置条件**:新建报告,默认模板已加载。
|
||||
**步骤**:
|
||||
1. 查看顶部 Logo 区域。
|
||||
2. 点击 Logo 占位符右上方的「×」。
|
||||
3. 再次点击 Logo 区域。
|
||||
**预期结果**:
|
||||
- Logo 区域显示为虚线框,提示文字「LOGO」居中显示。
|
||||
- 点击「×」后 Logo 占位符被删除。
|
||||
- 再次点击可打开图片选择器插入图片。
|
||||
|
||||
## 回归测试
|
||||
- 确保模板列表的加载、编辑、删除功能正常。
|
||||
- 确保默认模板的其他部分(基础信息、手术步骤、图片表格等)不受影响。
|
||||
- 确保打印样式正常。
|
||||
|
||||
## 测试通过标准
|
||||
所有用例均通过,无控制台报错,导入/导出数据完整准确。
|
||||
52
工程分析/需求分析-2026-04-18-20-03-44.md
Normal file
52
工程分析/需求分析-2026-04-18-20-03-44.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# 需求分析 —— 2026-04-18-20-03-44
|
||||
|
||||
## 需求来源
|
||||
用户希望增强模板管理模块的数据迁移能力和默认模板的交互一致性。
|
||||
|
||||
## 需求概述
|
||||
|
||||
### 需求 1:模板导出功能
|
||||
在 `TemplateManage` 的模板列表中,新增「导出」按钮。导出内容需包含:
|
||||
- 模板名称(`title`)
|
||||
- 模板描述(`description`)
|
||||
- 模板内容(`content`)
|
||||
- 字段管理配置(`fields`)
|
||||
|
||||
导出格式为 JSON,结构如下:
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"type": "surclaw_template_package",
|
||||
"title": "...",
|
||||
"description": "...",
|
||||
"content": "...",
|
||||
"fields": []
|
||||
}
|
||||
```
|
||||
|
||||
### 需求 2:模板导入功能
|
||||
在「新增模板」弹窗中,新增「导入本地模板」选项。用户选择 JSON 文件后:
|
||||
- 自动解析并填充模板名称和描述到表单
|
||||
- 暂存模板内容和字段配置
|
||||
- 点击「创建」时,将暂存的内容和字段一并写入新模板
|
||||
|
||||
导入 UI 使用指定的样式类名:`w-8 h-8 bg-accent text-white rounded-lg flex items-center justify-center hover:bg-blue-700 transition-colors shadow-sm`
|
||||
|
||||
### 需求 3:Logo 替换为可交互占位符
|
||||
默认模板 `defaultContent.ts` 中顶部医院 Logo 当前为硬编码的 `<span>` 结构(非标准 `image-placeholder`),导致:
|
||||
- 无法点击右上方的「×」删除
|
||||
- 无法触发图片上传/选择逻辑
|
||||
- 与编辑器中其他图片占位符的交互不一致
|
||||
|
||||
需将其替换为标准的 65×65 `image-placeholder`(`data-mode="manual"`),使其支持删除、点击插入等完整交互。
|
||||
|
||||
## 涉及文件
|
||||
- `src/pages/TemplateManage.tsx`(需求 1、2)
|
||||
- `src/utils/defaultContent.ts`(需求 3)
|
||||
- `src/types.ts`(确认 Template 类型结构)
|
||||
|
||||
## 需求影响范围
|
||||
- 模板列表操作列新增导出按钮
|
||||
- 新增模板弹窗新增导入 UI 和逻辑
|
||||
- 默认模板头部 Logo 的 HTML 结构
|
||||
- 模板创建流程需支持字段配置写入
|
||||
Reference in New Issue
Block a user