Include field library metadata in template exports
- Add fieldLibrary metadata to HTML template packages, including form fields, custom time formats, multi-select options, and anesthesia options. - Restore imported template field metadata into local compatibility caches and the backend field library API when available. - Preserve legacy JSON template import compatibility while keeping user-facing exports on HTML packages. - Prevent template field saves from overwriting stored multi-select and anesthesia options with empty values. - Update README, AGENTS, feature, requirement, design, module, progress, component, and testing docs for complete template export behavior. - Extend template export tests to cover field library metadata round-tripping.
This commit is contained in:
@@ -350,7 +350,7 @@ PostgreSQL 数据模型。当前覆盖 `Tenant`、`Department`、`User`、`UserS
|
||||
- 后端报告 schema 区分草稿和完成状态,草稿可暂缺患者姓名/住院号,完成报告必须填写
|
||||
- 后端模板 DTO 和权限资源映射
|
||||
- 模板列表合并工具,防止新增模板被旧 `localStorage.templates` 覆盖
|
||||
- 模板导入导出工具,覆盖 HTML 模板包生成、回导和旧 JSON 导入兼容
|
||||
- 模板导入导出工具,覆盖 HTML 模板包生成、字段库元数据回导和旧 JSON 导入兼容
|
||||
- 后端用户 DTO 和部门模板授权映射
|
||||
- 后端系统设置 schema 校验
|
||||
- 后端 AI 入参和讯飞语音代理帧处理
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
- AI 辅助撰写:通过后端 `/api/ai/chat` 代理 OpenAI 兼容接口,对报告内容进行对话或指定区域改写。
|
||||
- 语音输入:通过后端 `/api/speech/iat` WebSocket 代理讯飞 IAT 听写,把识别文本写入 AI 输入框。
|
||||
- 报告管理:后端权限过滤的列表/详情/保存/删除,支持搜索、筛选、查看、编辑、历史恢复和打印/PDF 导出。
|
||||
- 模板管理:后端权限过滤的模板新增/编辑/删除,字段库和模板图片资源优先走后端 API,支持 AI 可编辑区域、导入导出。
|
||||
- 模板管理:后端权限过滤的模板新增/编辑/删除,字段库和模板图片资源优先走后端 API,支持 AI 可编辑区域、可回导 HTML 模板包导入导出;HTML 包内嵌模板字段和字段管理设置。
|
||||
- 用户管理:后端用户/部门权限 API,支持用户、角色、部门、模板授权、账号状态和电子签名文件。
|
||||
- 系统设置:后端 Settings API,支持抽帧策略、默认模板、AI Provider、讯飞语音配置。
|
||||
|
||||
@@ -200,6 +200,7 @@ cp .env.example .env.local
|
||||
- 报告管理按角色过滤报告。
|
||||
- 本地存储封装和系统设置兼容。
|
||||
- 默认报告模板结构和字段配置。
|
||||
- 模板 HTML 导出包字段库元数据。
|
||||
- 打印导出入口。
|
||||
- 后端权限策略、AI 入参和语音代理帧处理。
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ main.tsx
|
||||
- 报告正文编辑区:`contentEditable` HTML、智能字段和图片占位符。
|
||||
- 视频分析区:视频上传、预览、自动抽帧、手动截帧。
|
||||
- 关键帧列表:点击插入、拖拽插入、后端文件上传状态。
|
||||
- AI 撰写面板:对话、区域改写、差异确认和 AI 日志导出。
|
||||
- AI 撰写面板:对话、区域改写和差异确认。
|
||||
- 语音输入:浏览器麦克风采集,连接 `/api/speech/iat`。
|
||||
- 保存逻辑:优先调用 Reports API,并同步 `ReportMedia` 所需的视频/关键帧引用。
|
||||
|
||||
@@ -199,7 +199,7 @@ main.tsx
|
||||
- 富文本命令、表格、字段、图片占位符、AI 区域插入。
|
||||
- 字段库维护。
|
||||
- 模板图片资源上传和删除。
|
||||
- 模板 HTML 模板包导出和 HTML/历史 JSON 模板包导入。
|
||||
- 模板 HTML 模板包导出和 HTML/历史 JSON 模板包导入;HTML 包内嵌模板字段和字段管理设置。
|
||||
|
||||
后续拆分优先级:
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ AI 可编辑区域通过 `.ai-region[data-ai-id]` 和内部 `.ai-content` 定位
|
||||
|
||||
`src/utils/print.ts` 通过隐藏 iframe 写入报告 HTML 和打印样式,然后调用浏览器打印。PDF 导出依赖浏览器“另存为 PDF”,不是服务端生成 PDF。
|
||||
|
||||
前端用户可见 JSON 导出入口已移除。报告导出保留浏览器打印/PDF;模板导出保留可回导 HTML 模板包和 PDF 打印预览,历史 JSON 模板包只作为导入兼容格式保留。
|
||||
前端用户可见 JSON 导出入口已移除。报告导出保留浏览器打印/PDF;模板导出保留可回导 HTML 模板包和 PDF 打印预览,历史 JSON 模板包只作为导入兼容格式保留。HTML 模板包会在内嵌元数据中保存模板正文、模板对应字段和字段管理设置,导入时同步恢复字段库兼容缓存并尝试写回 `/api/library/fields`。
|
||||
|
||||
## 外部服务设计
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
| 报告查看 | 真实集成 | `ReportView` 优先调用 `GET /api/reports/:id`,后端校验查看权限;页面渲染报告 HTML;只有开发/显式回退模式下 API 不可用才回退本地权限检查。 |
|
||||
| PDF 导出 | 真实集成 | 通过隐藏 iframe 或 `window.print()` 调用浏览器打印,用户手动保存为 PDF;不是后端 PDF 生成。 |
|
||||
| 前端 JSON 导出 | 已移除 | 用户可见的报告、模板和 AI 日志 JSON 导出入口已移除;模板导入仍兼容旧 JSON 模板包。 |
|
||||
| 模板管理 | 真实集成 | `TemplateManage` 优先调用 `/api/templates?access=manage`,新增/编辑/删除会写后端并清洗 HTML;新增后保存内容以当前页面模板列表为准并同步兼容缓存,避免旧本地缓存覆盖新模板;字段库优先调用 `/api/library/fields`,模板图片资源优先调用 `/api/files`。导出保留 HTML 模板包和 PDF 打印预览,导入兼容 HTML 模板包和旧 JSON 模板包,仍主要在前端处理。 |
|
||||
| 模板管理 | 真实集成 | `TemplateManage` 优先调用 `/api/templates?access=manage`,新增/编辑/删除会写后端并清洗 HTML;新增后保存内容以当前页面模板列表为准并同步兼容缓存,避免旧本地缓存覆盖新模板;字段库优先调用 `/api/library/fields`,模板图片资源优先调用 `/api/files`。导出保留 HTML 模板包和 PDF 打印预览,HTML 模板包内嵌模板正文、模板字段和字段管理设置;导入兼容 HTML 模板包和旧 JSON 模板包,仍主要在前端处理。 |
|
||||
| 模板权限 | 真实集成 | 后端按部门模板、部门授权和个人模板过滤 `access=use/manage`;迁移期仍同步 `localStorage.templates`,仅在开发/显式回退模式下作为回退。 |
|
||||
| 我的个人模板 | 真实集成 | 医生在报告编辑器中保存个人模板时优先调用 `POST /api/templates`,后端把模板归属当前用户;只有开发/显式回退模式下 API 不可用才回退本地模板。 |
|
||||
| 用户管理 | 真实集成 | `UserManage` 优先调用 `/api/users` 增删改查,后端校验超级管理员/管理员范围、管理员唯一性和医生创建约束;只有开发/显式回退模式下 API 不可用才保留本地回退。 |
|
||||
@@ -67,7 +67,7 @@
|
||||
| `src/pages/ReportManage.test.tsx` | 医生/管理员报告可见范围。 |
|
||||
| `src/utils/permissions.test.ts` | 报告权限、管理员本部门范围、医生个人模板和部门模板范围。 |
|
||||
| `src/utils/templateList.test.ts` | 模板列表合并,覆盖新增模板不被旧缓存覆盖。 |
|
||||
| `src/utils/templateExport.test.ts` | HTML 模板包生成、HTML 回导、旧 JSON 导入兼容和文件名清理。 |
|
||||
| `src/utils/templateExport.test.ts` | HTML 模板包生成、字段库元数据回导、旧 JSON 导入兼容和文件名清理。 |
|
||||
| `src/utils/storage.test.ts` | 本地存储、系统设置混淆兼容、会话恢复键、默认 Provider 不携带内置 Key 的契约。 |
|
||||
| `src/utils/defaultContent.test.ts` | 默认模板结构、智能字段、图片占位符、AI 区域、字段和 Provider 配置。 |
|
||||
| `src/utils/print.test.ts` | 浏览器打印导出入口。 |
|
||||
|
||||
@@ -76,15 +76,21 @@
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"version": "1.1",
|
||||
"type": "surclaw_template_package",
|
||||
"title": "模板名称",
|
||||
"description": "模板描述",
|
||||
"content": "模板 HTML",
|
||||
"fields": []
|
||||
"fields": [],
|
||||
"fieldLibrary": {
|
||||
"formFields": [],
|
||||
"customTimeFormats": [],
|
||||
"multiSelectOptions": {},
|
||||
"anesthesiaOptions": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
HTML 模板包是一个可直接用浏览器打开的完整 HTML 文件,包含 A4 页面样式、打印样式和内嵌的 `surclaw_template_package` 元数据。它比 JSON 更适合保留“报告整体观感”,也是当前推荐的可回导模板交换格式。
|
||||
HTML 模板包是一个可直接用浏览器打开的完整 HTML 文件,包含 A4 页面样式、打印样式和内嵌的 `surclaw_template_package` 元数据。元数据会随包保存模板正文、模板对应字段,以及字段管理相关设置:字段库、时间格式、多选字段选项和麻醉方式选项。它比单独 JSON 文件更适合保留“报告整体观感”,也是当前推荐的可回导模板交换格式。
|
||||
|
||||
PDF 导出走浏览器打印,适合归档和人工查看,不适合再次导入编辑。
|
||||
|
||||
@@ -88,3 +88,4 @@
|
||||
| 2026-05-02 | 模板管理新增 HTML 模板包导出/导入。 |
|
||||
| 2026-05-02 | 修复报告编辑器新增 AI 可编辑区域后 AI 撰写下拉栏不立即更新的问题,并补充 E2E。 |
|
||||
| 2026-05-02 | 移除前端用户可见 JSON 导出入口,保留模板历史 JSON 导入兼容。 |
|
||||
| 2026-05-02 | 模板 HTML 导出包补充模板字段和字段管理设置,导入时恢复字段库元数据。 |
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
- 支持模板新增、编辑、删除、批量删除。
|
||||
- 支持模板内容富文本编辑、智能字段插入、图片占位符、AI 可编辑区域。
|
||||
- 支持模板导出可回导 HTML 模板包;导入兼容 HTML 模板包和历史 JSON 模板包。
|
||||
- 支持模板导出可回导 HTML 模板包;HTML 包需包含模板正文、模板对应字段和字段管理设置,导入兼容 HTML 模板包和历史 JSON 模板包。
|
||||
- 支持表单字段库维护,包括字段显示、选项、时间格式、默认值和下划线样式。
|
||||
- 新增模板后会同步当前用户或部门用户的模板权限。
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ AI 第三方接口、讯飞语音上游 WebSocket、麦克风权限和真实视
|
||||
| 默认字段和 AI Provider | 已覆盖 | `defaultContent.test.ts` |
|
||||
| 打印导出入口 | 已覆盖 | `print.test.ts` |
|
||||
| 模板列表合并工具 | 已覆盖 | `templateList.test.ts`,防止新增模板被旧本地缓存覆盖。 |
|
||||
| 模板导入导出工具 | 已覆盖 | `templateExport.test.ts`,覆盖 HTML 模板包、HTML 回导、旧 JSON 导入兼容和文件名清理。 |
|
||||
| 模板导入导出工具 | 已覆盖 | `templateExport.test.ts`,覆盖 HTML 模板包、字段库元数据回导、旧 JSON 导入兼容和文件名清理。 |
|
||||
| 默认快捷登录 | 已覆盖 | `e2e/login.spec.ts` |
|
||||
| 报告权限 E2E | 已覆盖 | `e2e/report-permissions.spec.ts` |
|
||||
| 报告修订版本 E2E | 已覆盖 | `e2e/report-revision.spec.ts` |
|
||||
|
||||
@@ -18,8 +18,12 @@ import {
|
||||
getExportTimestamp,
|
||||
parseTemplatePackageFile,
|
||||
safeFileName,
|
||||
type TemplateFieldLibrary,
|
||||
type TemplatePackage,
|
||||
} from '../utils/templateExport';
|
||||
|
||||
const DEFAULT_TIME_FORMATS = ['YYYY-MM-DD', 'YYYY年MM月DD日', 'MM-DD', 'MM月DD日', 'HH:mm', 'hh:mm A'];
|
||||
|
||||
export default function TemplateManage() {
|
||||
const navigate = useNavigate();
|
||||
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
||||
@@ -29,7 +33,7 @@ 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 [importedContent, setImportedContent] = useState<TemplatePackage | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [isSaved, setIsSaved] = useState(false);
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
@@ -76,6 +80,17 @@ export default function TemplateManage() {
|
||||
editorRef.current.style.minHeight = `${pages * pageHeightMm}mm`;
|
||||
};
|
||||
|
||||
const getStoredFieldLibraryOptions = () => ({
|
||||
multiSelectOptions: storage.get<Record<string, string[]>>('multiSelectOptions', {}),
|
||||
anesthesiaOptions: storage.get<string[]>('anesthesiaOptions', []),
|
||||
});
|
||||
|
||||
const buildFieldLibrarySnapshot = (fields: FormField[] = formFields): TemplateFieldLibrary => ({
|
||||
formFields: fields,
|
||||
customTimeFormats,
|
||||
...getStoredFieldLibraryOptions(),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const user = storage.get<User | null>('currentUser', null);
|
||||
if (!user || user.role === 'user') {
|
||||
@@ -112,16 +127,18 @@ export default function TemplateManage() {
|
||||
}
|
||||
|
||||
const savedFormats = storage.get<string[]>('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])));
|
||||
setCustomTimeFormats(Array.from(new Set([...DEFAULT_TIME_FORMATS, ...cleanedSaved])));
|
||||
|
||||
void getFieldLibrary().then((library) => {
|
||||
if (library.formFields.length > 0) {
|
||||
setFormFields(library.formFields);
|
||||
storage.set('formFieldsConfig', library.formFields);
|
||||
}
|
||||
setCustomTimeFormats(Array.from(new Set([...defaultFormats, ...library.customTimeFormats])));
|
||||
setCustomTimeFormats(Array.from(new Set([...DEFAULT_TIME_FORMATS, ...library.customTimeFormats])));
|
||||
storage.set('customTimeFormats', library.customTimeFormats);
|
||||
storage.set('multiSelectOptions', library.multiSelectOptions);
|
||||
storage.set('anesthesiaOptions', library.anesthesiaOptions);
|
||||
}).catch(() => {});
|
||||
|
||||
void listFiles('TEMPLATE_ASSET').then((files) => {
|
||||
@@ -173,16 +190,55 @@ export default function TemplateManage() {
|
||||
const persistFieldLibrary = (next: Partial<{
|
||||
formFields: FormField[];
|
||||
customTimeFormats: string[];
|
||||
multiSelectOptions: Record<string, string[]>;
|
||||
anesthesiaOptions: string[];
|
||||
}>) => {
|
||||
const storedOptions = getStoredFieldLibraryOptions();
|
||||
void updateFieldLibrary({
|
||||
formFields,
|
||||
customTimeFormats,
|
||||
multiSelectOptions: {},
|
||||
anesthesiaOptions: [],
|
||||
...storedOptions,
|
||||
...next,
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
const applyImportedFieldLibrary = (templatePackage: TemplatePackage) => {
|
||||
const library = templatePackage.fieldLibrary;
|
||||
const importedFields = templatePackage.fields.length > 0
|
||||
? templatePackage.fields
|
||||
: library?.formFields || [];
|
||||
const nextFormats = library?.customTimeFormats
|
||||
? Array.from(new Set([...DEFAULT_TIME_FORMATS, ...library.customTimeFormats]))
|
||||
: customTimeFormats;
|
||||
|
||||
if (importedFields.length > 0) {
|
||||
setFormFields(importedFields);
|
||||
storage.set('formFieldsConfig', importedFields);
|
||||
}
|
||||
|
||||
if (library?.customTimeFormats) {
|
||||
setCustomTimeFormats(nextFormats);
|
||||
storage.set('customTimeFormats', nextFormats);
|
||||
}
|
||||
|
||||
if (library?.multiSelectOptions) {
|
||||
storage.set('multiSelectOptions', library.multiSelectOptions);
|
||||
}
|
||||
|
||||
if (library?.anesthesiaOptions) {
|
||||
storage.set('anesthesiaOptions', library.anesthesiaOptions);
|
||||
}
|
||||
|
||||
if (importedFields.length > 0 || library) {
|
||||
persistFieldLibrary({
|
||||
...(importedFields.length > 0 ? { formFields: importedFields } : {}),
|
||||
...(library?.customTimeFormats ? { customTimeFormats: nextFormats } : {}),
|
||||
...(library?.multiSelectOptions ? { multiSelectOptions: library.multiSelectOptions } : {}),
|
||||
...(library?.anesthesiaOptions ? { anesthesiaOptions: library.anesthesiaOptions } : {}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (currentTemplateId && editorRef.current) {
|
||||
const template = templates.find(t => t.id === currentTemplateId);
|
||||
@@ -771,10 +827,7 @@ export default function TemplateManage() {
|
||||
try {
|
||||
const templatePackage = parseTemplatePackageFile(file.name, event.target?.result as string);
|
||||
setFormData({ name: templatePackage.title || '', desc: templatePackage.description || '' });
|
||||
setImportedContent({
|
||||
content: templatePackage.content || '',
|
||||
fields: templatePackage.fields
|
||||
});
|
||||
setImportedContent(templatePackage);
|
||||
} catch (error) {
|
||||
alert(error instanceof Error ? error.message : '文件解析失败,请检查 JSON/HTML 格式');
|
||||
}
|
||||
@@ -784,7 +837,8 @@ export default function TemplateManage() {
|
||||
};
|
||||
|
||||
const handleExportTemplate = (template: Template) => {
|
||||
const exportData = createTemplatePackage(template, template.content, template.fields || formFields);
|
||||
const templateFields = template.fields || formFields;
|
||||
const exportData = createTemplatePackage(template, template.content, templateFields, buildFieldLibrarySnapshot(templateFields));
|
||||
const html = createTemplateHtmlDocument(exportData);
|
||||
const blob = new Blob([html], { type: 'text/html' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
@@ -799,7 +853,8 @@ export default function TemplateManage() {
|
||||
const downloadCurrentTemplateHtmlPackage = () => {
|
||||
const content = editorRef.current?.innerHTML || currentTemplate?.content || '';
|
||||
const name = currentTemplate?.name || '模板';
|
||||
const templatePackage = createTemplatePackage(currentTemplate, content, currentTemplate?.fields || formFields);
|
||||
const templateFields = currentTemplate?.fields || formFields;
|
||||
const templatePackage = createTemplatePackage(currentTemplate, content, templateFields, buildFieldLibrarySnapshot(templateFields));
|
||||
const ts = getExportTimestamp();
|
||||
const safeName = safeFileName(name);
|
||||
const body = createTemplateHtmlDocument(templatePackage);
|
||||
@@ -845,7 +900,7 @@ export default function TemplateManage() {
|
||||
content: importedContent?.content || defaultReportContent,
|
||||
createdAt: new Date().toISOString(),
|
||||
author: currentUser?.username || 'admin',
|
||||
fields: importedContent?.fields || formFields,
|
||||
fields: importedContent?.fields.length ? importedContent.fields : formFields,
|
||||
scope: 'department',
|
||||
department: currentUser?.role === 'super' ? '' : (currentUser?.department || '')
|
||||
};
|
||||
@@ -861,9 +916,8 @@ export default function TemplateManage() {
|
||||
setTemplates(updatedTemplates);
|
||||
storage.set('templates', mergeTemplatesById(cachedTemplates, updatedTemplates));
|
||||
setCurrentTemplateId(newTpl.id);
|
||||
if (importedContent?.fields && importedContent.fields.length > 0) {
|
||||
setFormFields(importedContent.fields);
|
||||
storage.set('formFieldsConfig', importedContent.fields);
|
||||
if (importedContent) {
|
||||
applyImportedFieldLibrary(importedContent);
|
||||
}
|
||||
|
||||
const savedUsers = storage.get<User[]>('users', []);
|
||||
|
||||
@@ -28,13 +28,43 @@ describe('template export utilities', () => {
|
||||
});
|
||||
|
||||
it('round-trips standalone HTML template packages', () => {
|
||||
const pkg = createTemplatePackage({ name: '整体版式', desc: '' }, '<h1>报告</h1>', []);
|
||||
const pkg = createTemplatePackage(
|
||||
{ name: '整体版式', desc: '' },
|
||||
'<h1>报告</h1>',
|
||||
[{
|
||||
key: 'startTime',
|
||||
label: '开始时间',
|
||||
category: '时间',
|
||||
type: 'time',
|
||||
visibleInForm: true,
|
||||
isSystemLocked: true,
|
||||
timeFormat: 'HH:mm',
|
||||
}],
|
||||
{
|
||||
formFields: [{
|
||||
key: 'startTime',
|
||||
label: '开始时间',
|
||||
category: '时间',
|
||||
type: 'time',
|
||||
visibleInForm: true,
|
||||
isSystemLocked: true,
|
||||
timeFormat: 'HH:mm',
|
||||
}],
|
||||
customTimeFormats: ['HH:mm:ss'],
|
||||
multiSelectOptions: { surgeon: ['张医生'] },
|
||||
anesthesiaOptions: ['全麻'],
|
||||
},
|
||||
);
|
||||
const html = createTemplateHtmlDocument(pkg);
|
||||
const parsed = parseTemplatePackageFile('整体版式.html', html);
|
||||
|
||||
expect(html).toContain(`id="${TEMPLATE_HTML_META_ID}"`);
|
||||
expect(parsed.title).toBe('整体版式');
|
||||
expect(parsed.content).toContain('<h1>报告</h1>');
|
||||
expect(parsed.fields[0].key).toBe('startTime');
|
||||
expect(parsed.fieldLibrary?.customTimeFormats).toEqual(['HH:mm:ss']);
|
||||
expect(parsed.fieldLibrary?.multiSelectOptions.surgeon).toEqual(['张医生']);
|
||||
expect(parsed.fieldLibrary?.anesthesiaOptions).toEqual(['全麻']);
|
||||
});
|
||||
|
||||
it('accepts existing raw template JSON for compatibility', () => {
|
||||
|
||||
@@ -10,6 +10,14 @@ export interface TemplatePackage {
|
||||
description: string;
|
||||
content: string;
|
||||
fields: FormField[];
|
||||
fieldLibrary?: TemplateFieldLibrary;
|
||||
}
|
||||
|
||||
export interface TemplateFieldLibrary {
|
||||
formFields: FormField[];
|
||||
customTimeFormats: string[];
|
||||
multiSelectOptions: Record<string, string[]>;
|
||||
anesthesiaOptions: string[];
|
||||
}
|
||||
|
||||
export const getExportTimestamp = () =>
|
||||
@@ -22,14 +30,44 @@ export const createTemplatePackage = (
|
||||
template: Pick<Template, 'name' | 'desc'> | null | undefined,
|
||||
content: string,
|
||||
fields: FormField[],
|
||||
): TemplatePackage => ({
|
||||
version: '1.0',
|
||||
type: TEMPLATE_PACKAGE_TYPE,
|
||||
title: template?.name || '模板',
|
||||
description: template?.desc || '',
|
||||
content,
|
||||
fields,
|
||||
});
|
||||
fieldLibrary?: TemplateFieldLibrary,
|
||||
): TemplatePackage => {
|
||||
const templatePackage: TemplatePackage = {
|
||||
version: '1.1',
|
||||
type: TEMPLATE_PACKAGE_TYPE,
|
||||
title: template?.name || '模板',
|
||||
description: template?.desc || '',
|
||||
content,
|
||||
fields,
|
||||
};
|
||||
|
||||
if (fieldLibrary) {
|
||||
templatePackage.fieldLibrary = fieldLibrary;
|
||||
}
|
||||
|
||||
return templatePackage;
|
||||
};
|
||||
|
||||
const normalizeStringArray = (value: unknown): string[] =>
|
||||
Array.isArray(value) ? value.map(String).filter(Boolean) : [];
|
||||
|
||||
const normalizeMultiSelectOptions = (value: unknown): Record<string, string[]> => {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
||||
return Object.fromEntries(
|
||||
Object.entries(value as Record<string, unknown>).map(([key, values]) => [key, normalizeStringArray(values)]),
|
||||
);
|
||||
};
|
||||
|
||||
const parseFieldLibrary = (value: unknown): TemplateFieldLibrary | undefined => {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
||||
const raw = value as Record<string, unknown>;
|
||||
return {
|
||||
formFields: Array.isArray(raw.formFields) ? raw.formFields as FormField[] : [],
|
||||
customTimeFormats: normalizeStringArray(raw.customTimeFormats),
|
||||
multiSelectOptions: normalizeMultiSelectOptions(raw.multiSelectOptions),
|
||||
anesthesiaOptions: normalizeStringArray(raw.anesthesiaOptions),
|
||||
};
|
||||
};
|
||||
|
||||
const escapeHtml = (value: string) =>
|
||||
value
|
||||
@@ -80,6 +118,7 @@ export const createTemplateHtmlDocument = (templatePackage: TemplatePackage) =>
|
||||
export const parseTemplatePackageJson = (text: string): TemplatePackage => {
|
||||
const json = JSON.parse(text);
|
||||
if (json.type === TEMPLATE_PACKAGE_TYPE) {
|
||||
const fieldLibrary = parseFieldLibrary(json.fieldLibrary);
|
||||
return {
|
||||
version: String(json.version || '1.0'),
|
||||
type: TEMPLATE_PACKAGE_TYPE,
|
||||
@@ -87,10 +126,12 @@ export const parseTemplatePackageJson = (text: string): TemplatePackage => {
|
||||
description: String(json.description || json.desc || ''),
|
||||
content: String(json.content || ''),
|
||||
fields: Array.isArray(json.fields) ? json.fields : [],
|
||||
...(fieldLibrary ? { fieldLibrary } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
if (json.content) {
|
||||
const fieldLibrary = parseFieldLibrary(json.fieldLibrary);
|
||||
return {
|
||||
version: '1.0',
|
||||
type: TEMPLATE_PACKAGE_TYPE,
|
||||
@@ -98,6 +139,7 @@ export const parseTemplatePackageJson = (text: string): TemplatePackage => {
|
||||
description: String(json.desc || json.description || ''),
|
||||
content: String(json.content || ''),
|
||||
fields: Array.isArray(json.fields) ? json.fields : [],
|
||||
...(fieldLibrary ? { fieldLibrary } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user