Remove frontend JSON export actions
- Remove user-visible JSON export options from report editor, report management, template management, bulk template export, and AI debug logs. - Keep HTML template package and PDF/browser print exports as the supported frontend export formats. - Change per-template export to generate reusable HTML template packages. - Preserve legacy JSON template import compatibility without exposing new JSON export buttons. - Update README, AGENTS, feature, requirement, design, module, API contract, progress, and testing docs for the export policy change.
This commit is contained in:
@@ -83,7 +83,7 @@ npm run test:e2e
|
||||
8. AI 面板把消息和图片上下文发送到 `/api/ai/chat`,后端读取系统设置中的共用 OpenAI 兼容 Provider 并代理调用 `/chat/completions`,返回内容可注入 AI 可编辑区域。
|
||||
9. 语音听写把麦克风音频发送到 `/api/speech/iat` WebSocket,后端读取系统设置中的讯飞配置、生成鉴权 URL 并转发讯飞 IAT 结果。
|
||||
10. 报告保存优先调用 `POST/PATCH /api/reports`,后端保存到 PostgreSQL;仅开发/显式回退模式下 API 不可用时回退 `localStorage.reports`。
|
||||
11. 报告管理页优先调用 `GET /api/reports`,后端按角色过滤报告;前端支持搜索、筛选、历史恢复、打印和 JSON 导出。
|
||||
11. 报告管理页优先调用 `GET /api/reports`,后端按角色过滤报告;前端支持搜索、筛选、历史恢复和浏览器打印/PDF 导出。
|
||||
12. 模板管理页优先调用 `GET/POST/PATCH/DELETE /api/templates` 管理模板 HTML,调用 `/api/library/fields` 维护字段库,并通过 `/api/files` 上传模板图片资源。
|
||||
13. 用户管理页优先调用 `GET/PATCH /api/users` 等接口维护用户、角色、部门和模板授权;签名图片通过 `/api/users/:id/signature` 上传为后端文件资源。
|
||||
14. 系统设置页优先调用 `GET/PATCH /api/settings/system` 维护抽帧策略、默认模板、AI Provider 和讯飞语音配置。
|
||||
@@ -92,7 +92,7 @@ npm run test:e2e
|
||||
|
||||
详细清单见 `docs/features.md`。处理需求时应区分:
|
||||
|
||||
- 真实可用:本地初始化、字段绑定、JSON 导出等。
|
||||
- 真实可用:本地初始化、字段绑定、浏览器打印/PDF 导出等。
|
||||
- 真实集成:后端 Session 登录、Dashboard API、报告 API、模板 API、用户/部门 API、设置 API、签名文件 API、审计日志 API、AI 代理、讯飞语音代理、浏览器打印/PDF、视频抽帧。它们有真实代码路径,但依赖后端服务、浏览器能力、权限、有效密钥、网络或人工保存。
|
||||
- 前端体验控制:页面级角色守卫和菜单隐藏已接入 Auth Context,但不能替代后端 API 权限。
|
||||
|
||||
@@ -350,7 +350,7 @@ PostgreSQL 数据模型。当前覆盖 `Tenant`、`Department`、`User`、`UserS
|
||||
- 后端报告 schema 区分草稿和完成状态,草稿可暂缺患者姓名/住院号,完成报告必须填写
|
||||
- 后端模板 DTO 和权限资源映射
|
||||
- 模板列表合并工具,防止新增模板被旧 `localStorage.templates` 覆盖
|
||||
- 模板导入导出工具,覆盖 JSON/HTML 模板包生成、回导和旧 JSON 兼容
|
||||
- 模板导入导出工具,覆盖 HTML 模板包生成、回导和旧 JSON 导入兼容
|
||||
- 后端用户 DTO 和部门模板授权映射
|
||||
- 后端系统设置 schema 校验
|
||||
- 后端 AI 入参和讯飞语音代理帧处理
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
- 视频关键帧:本地预览视频、按百分比自动抽帧、手动截帧,视频和关键帧优先上传为后端文件资源后插入报告。
|
||||
- AI 辅助撰写:通过后端 `/api/ai/chat` 代理 OpenAI 兼容接口,对报告内容进行对话或指定区域改写。
|
||||
- 语音输入:通过后端 `/api/speech/iat` WebSocket 代理讯飞 IAT 听写,把识别文本写入 AI 输入框。
|
||||
- 报告管理:后端权限过滤的列表/详情/保存/删除,支持搜索、筛选、查看、编辑、历史恢复、打印、JSON 导出。
|
||||
- 报告管理:后端权限过滤的列表/详情/保存/删除,支持搜索、筛选、查看、编辑、历史恢复和打印/PDF 导出。
|
||||
- 模板管理:后端权限过滤的模板新增/编辑/删除,字段库和模板图片资源优先走后端 API,支持 AI 可编辑区域、导入导出。
|
||||
- 用户管理:后端用户/部门权限 API,支持用户、角色、部门、模板授权、账号状态和电子签名文件。
|
||||
- 系统设置:后端 Settings API,支持抽帧策略、默认模板、AI Provider、讯飞语音配置。
|
||||
@@ -262,4 +262,4 @@ docker-compose down
|
||||
|
||||
- 继续增强报告媒体模型,例如断点续传、转码、后端抽帧和独立媒体审计。
|
||||
- 查看日志、第三方代理调用摘要、错误追踪和更细粒度操作审计。报告导出按权限控制,不要求专门导出审计。
|
||||
- 后端 PDF/JSON 导出、对象存储、限流和备份恢复。
|
||||
- 后端 PDF 导出、对象存储、限流和备份恢复。
|
||||
|
||||
@@ -322,7 +322,7 @@ pageSize?: number
|
||||
|
||||
### `GET /api/reports/:id/export.json`
|
||||
|
||||
返回结构化报告 JSON。权限与查看报告一致。当前确定不需要水印、导出原因、审批或专门导出审计;现阶段仍主要由前端 Blob 导出。
|
||||
返回结构化报告 JSON。权限与查看报告一致。当前确定不需要水印、导出原因、审批或专门导出审计;用户可见前端 Blob JSON 导出入口已移除,如后续需要结构化交换应由后端接口统一提供。
|
||||
|
||||
## Templates API
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@ main.tsx
|
||||
|
||||
- 调用 Reports API 获取权限过滤后的报告。
|
||||
- 前端搜索、状态筛选和日期筛选。
|
||||
- 删除报告、历史恢复、JSON 导出和批量操作。
|
||||
- 删除报告、历史恢复、打印/PDF 导出和批量操作。
|
||||
|
||||
`ReportView` 负责只读展示:
|
||||
|
||||
@@ -199,7 +199,7 @@ main.tsx
|
||||
- 富文本命令、表格、字段、图片占位符、AI 区域插入。
|
||||
- 字段库维护。
|
||||
- 模板图片资源上传和删除。
|
||||
- 模板 JSON 导入导出。
|
||||
- 模板 HTML 模板包导出和 HTML/历史 JSON 模板包导入。
|
||||
|
||||
后续拆分优先级:
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ AI 可编辑区域通过 `.ai-region[data-ai-id]` 和内部 `.ai-content` 定位
|
||||
|
||||
`src/utils/print.ts` 通过隐藏 iframe 写入报告 HTML 和打印样式,然后调用浏览器打印。PDF 导出依赖浏览器“另存为 PDF”,不是服务端生成 PDF。
|
||||
|
||||
JSON 导出使用 Blob 下载,报告导出主要包含元信息和 `DEFAULT_FORM_FIELDS` 中定义的字段。
|
||||
前端用户可见 JSON 导出入口已移除。报告导出保留浏览器打印/PDF;模板导出保留可回导 HTML 模板包和 PDF 打印预览,历史 JSON 模板包只作为导入兼容格式保留。
|
||||
|
||||
## 外部服务设计
|
||||
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
| 报告管理筛选 | 真实集成 | `ReportManage` 优先调用 `GET /api/reports`,后端按超级管理员/管理员/医生过滤;前端继续支持搜索、状态和时间筛选;只有开发/显式回退模式下 API 不可用才回退本地报告。 |
|
||||
| 报告查看 | 真实集成 | `ReportView` 优先调用 `GET /api/reports/:id`,后端校验查看权限;页面渲染报告 HTML;只有开发/显式回退模式下 API 不可用才回退本地权限检查。 |
|
||||
| PDF 导出 | 真实集成 | 通过隐藏 iframe 或 `window.print()` 调用浏览器打印,用户手动保存为 PDF;不是后端 PDF 生成。 |
|
||||
| JSON 导出 | 真实可用 | Blob 下载结构化报告字段或模板包;模板 JSON 包适合系统内迁移。 |
|
||||
| 模板管理 | 真实集成 | `TemplateManage` 优先调用 `/api/templates?access=manage`,新增/编辑/删除会写后端并清洗 HTML;新增后保存内容以当前页面模板列表为准并同步兼容缓存,避免旧本地缓存覆盖新模板;字段库优先调用 `/api/library/fields`,模板图片资源优先调用 `/api/files`。导入导出支持 JSON 模板包、可再导入的 HTML 模板包和 PDF 打印预览,仍主要在前端处理。 |
|
||||
| 前端 JSON 导出 | 已移除 | 用户可见的报告、模板和 AI 日志 JSON 导出入口已移除;模板导入仍兼容旧 JSON 模板包。 |
|
||||
| 模板管理 | 真实集成 | `TemplateManage` 优先调用 `/api/templates?access=manage`,新增/编辑/删除会写后端并清洗 HTML;新增后保存内容以当前页面模板列表为准并同步兼容缓存,避免旧本地缓存覆盖新模板;字段库优先调用 `/api/library/fields`,模板图片资源优先调用 `/api/files`。导出保留 HTML 模板包和 PDF 打印预览,导入兼容 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` | JSON/HTML 模板包生成、HTML 回导、旧 JSON 兼容和文件名清理。 |
|
||||
| `src/utils/templateExport.test.ts` | HTML 模板包生成、HTML 回导、旧 JSON 导入兼容和文件名清理。 |
|
||||
| `src/utils/storage.test.ts` | 本地存储、系统设置混淆兼容、会话恢复键、默认 Provider 不携带内置 Key 的契约。 |
|
||||
| `src/utils/defaultContent.test.ts` | 默认模板结构、智能字段、图片占位符、AI 区域、字段和 Provider 配置。 |
|
||||
| `src/utils/print.test.ts` | 浏览器打印导出入口。 |
|
||||
|
||||
@@ -26,8 +26,7 @@
|
||||
- 删除:优先调用 `DELETE /api/reports/:id` 做后端软删除,再同步本地兼容缓存;只有本地回退开启时,API 不可用才会从本地 `reports` 中移除。
|
||||
- 历史版本:查看 `Report.history`,可恢复某个历史内容到编辑器。
|
||||
- 导出 PDF:调用浏览器打印。
|
||||
- 导出 JSON:下载结构化字段数据。
|
||||
- 批量导出和批量删除:基于表格选中项操作。
|
||||
- 批量导出 PDF 和批量删除:基于表格选中项操作。
|
||||
|
||||
医生只能编辑或删除自己的报告。
|
||||
|
||||
@@ -43,11 +42,6 @@
|
||||
|
||||
后端 `Report` 表保存标题、患者姓名、住院号、HTML 正文、作者、部门、状态和修订版本。`Report.metadata` 暂存患者扩展字段;视频和关键帧文件内容写入 `FileResource`,报告中的媒体引用、排序和抽帧信息写入 `ReportMedia` 关系表。Reports API 返回时仍组装成前端兼容的 `videos` 和 `capturedFrames` 字段。
|
||||
|
||||
## 导出 JSON 内容
|
||||
## JSON 导出
|
||||
|
||||
当前报告 JSON 导出主要包含:
|
||||
|
||||
- `meta`:报告 ID、标题、创建/更新时间、作者和状态。
|
||||
- `fields`:按 `DEFAULT_FORM_FIELDS` 提取的报告字段。
|
||||
|
||||
报告正文 HTML 和视频帧数据不在这个结构化字段导出范围内。
|
||||
前端用户可见的报告 JSON 导出入口已移除。当前报告导出只保留浏览器打印/PDF 路径,结构化数据交换后续如需要应走后端 API 或数据库层能力。
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
- 删除或批量删除模板。
|
||||
- 保存当前模板内容。
|
||||
- 打印模板预览。
|
||||
- 单个导出 JSON 模板包、HTML 模板包和 PDF 打印预览,批量导出 JSON。
|
||||
- 从 JSON 模板包或 HTML 模板包导入。
|
||||
- 单个导出 HTML 模板包和 PDF 打印预览。
|
||||
- 从 HTML 模板包或历史 JSON 模板包导入。
|
||||
|
||||
新增、编辑、保存内容和删除模板会优先调用后端 `POST/PATCH/DELETE /api/templates`,后端会对模板 HTML 做白名单清洗;成功后同步 `localStorage.templates` 作为兼容缓存。只有本地回退开启时,API 失败才允许写本地模板。
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
|
||||
## 导入导出格式
|
||||
|
||||
单模板 JSON 模板包大致结构:
|
||||
前端用户可见导出只保留 HTML 模板包和 PDF 打印预览。历史 JSON 模板包仍可导入,兼容结构大致如下:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -85,8 +85,6 @@
|
||||
}
|
||||
```
|
||||
|
||||
HTML 模板包是一个可直接用浏览器打开的完整 HTML 文件,包含 A4 页面样式、打印样式和内嵌的 `surclaw_template_package` 元数据。它比 JSON 更适合保留“报告整体观感”,也可以重新导入系统恢复模板 HTML 和字段定义。
|
||||
HTML 模板包是一个可直接用浏览器打开的完整 HTML 文件,包含 A4 页面样式、打印样式和内嵌的 `surclaw_template_package` 元数据。它比 JSON 更适合保留“报告整体观感”,也是当前推荐的可回导模板交换格式。
|
||||
|
||||
PDF 导出走浏览器打印,适合归档和人工查看,不适合再次导入编辑。
|
||||
|
||||
批量导出使用 `type: "surclaw_template_package_batch"`,包含 `templates` 数组。当前导入逻辑只接受单模板 JSON/HTML 包。
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
- 工作台统计已接入 `GET /api/dashboard/stats`。
|
||||
- 报告编辑、模板选择、字段绑定、富文本编辑已实现。
|
||||
- 视频上传、自动抽帧、手动截帧、关键帧插入已实现;视频和关键帧优先上传到后端 Files API。
|
||||
- AI 对话、AI 区域改写、差异确认和调用日志已实现;AI 对话已改为后端 `/api/ai/chat` 代理。
|
||||
- AI 对话、AI 区域改写和差异确认已实现;AI 对话已改为后端 `/api/ai/chat` 代理。
|
||||
- 报告编辑器会监听正文 AI 区域变化,新插入的 AI 可编辑区域会立即同步到 AI 撰写目标下拉栏。
|
||||
- 讯飞语音听写入口已实现,并已改为后端 `/api/speech/iat` WebSocket 代理;前端启动前会检查麦克风采集能力和安全上下文。
|
||||
- 报告管理、查看、历史恢复、打印、JSON/PDF 导出已实现。
|
||||
- 报告管理、查看、历史恢复和打印/PDF 导出已实现;用户可见前端 JSON 导出入口已移除。
|
||||
- 报告 API 已实现列表、详情、创建、保存、完成修订、历史记录和软删除;`ReportManage`、`ReportView`、`ReportEditor` 已优先调用后端,只有开发/显式回退模式下才保留本地回退。
|
||||
- 后端报告校验已区分草稿和完成状态:草稿允许患者姓名/住院号暂空,完成报告仍强制要求。
|
||||
- 模板管理、字段库、模板导入导出已实现;模板 API 已支持可用/可管理列表、详情、创建、更新、删除和个人模板;字段库已优先接入 `/api/library/fields`。
|
||||
@@ -85,5 +85,6 @@
|
||||
| 2026-05-02 | 增加 Nginx 和 NestJS 请求体上限配置,修复大图文报告保存 `request entity too large`。 |
|
||||
| 2026-05-02 | 新增 Docker HTTPS 演示入口和麦克风访问说明,解决非安全上下文下语音听写不可启动的问题。 |
|
||||
| 2026-05-02 | 修复模板管理中新建模板后点击保存内容导致模板从列表消失的问题,并补充单元测试和 E2E。 |
|
||||
| 2026-05-02 | 模板管理新增 HTML 模板包导出/导入,修正右上角 JSON 导出为标准模板包。 |
|
||||
| 2026-05-02 | 模板管理新增 HTML 模板包导出/导入。 |
|
||||
| 2026-05-02 | 修复报告编辑器新增 AI 可编辑区域后 AI 撰写下拉栏不立即更新的问题,并补充 E2E。 |
|
||||
| 2026-05-02 | 移除前端用户可见 JSON 导出入口,保留模板历史 JSON 导入兼容。 |
|
||||
|
||||
@@ -40,13 +40,13 @@
|
||||
- 支持按状态和时间范围筛选。
|
||||
- 医生只能看到本人报告;管理员和超级管理员可看到更多报告。
|
||||
- 支持查看、编辑、删除、历史版本恢复。
|
||||
- 支持单份或批量导出 PDF/JSON。
|
||||
- 支持单份或批量通过浏览器打印导出 PDF。
|
||||
|
||||
### 模板管理
|
||||
|
||||
- 支持模板新增、编辑、删除、批量删除。
|
||||
- 支持模板内容富文本编辑、智能字段插入、图片占位符、AI 可编辑区域。
|
||||
- 支持模板导入导出 JSON 包。
|
||||
- 支持模板导出可回导 HTML 模板包;导入兼容 HTML 模板包和历史 JSON 模板包。
|
||||
- 支持表单字段库维护,包括字段显示、选项、时间格式、默认值和下划线样式。
|
||||
- 新增模板后会同步当前用户或部门用户的模板权限。
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ AI 第三方接口、讯飞语音上游 WebSocket、麦克风权限和真实视
|
||||
| 默认字段和 AI Provider | 已覆盖 | `defaultContent.test.ts` |
|
||||
| 打印导出入口 | 已覆盖 | `print.test.ts` |
|
||||
| 模板列表合并工具 | 已覆盖 | `templateList.test.ts`,防止新增模板被旧本地缓存覆盖。 |
|
||||
| 模板导入导出工具 | 已覆盖 | `templateExport.test.ts`,覆盖 JSON/HTML 模板包、HTML 回导、旧 JSON 兼容和文件名清理。 |
|
||||
| 模板导入导出工具 | 已覆盖 | `templateExport.test.ts`,覆盖 HTML 模板包、HTML 回导、旧 JSON 导入兼容和文件名清理。 |
|
||||
| 默认快捷登录 | 已覆盖 | `e2e/login.spec.ts` |
|
||||
| 报告权限 E2E | 已覆盖 | `e2e/report-permissions.spec.ts` |
|
||||
| 报告修订版本 E2E | 已覆盖 | `e2e/report-revision.spec.ts` |
|
||||
|
||||
@@ -125,14 +125,6 @@ export default function ReportEditor() {
|
||||
]);
|
||||
const [isEditingPrompts, setIsEditingPrompts] = useState(false);
|
||||
const [diffModal, setDiffModal] = useState<{isOpen: boolean, originalHtml: string, newHtml: string, targetId: string} | null>(null);
|
||||
const [lastExchangeLog, setLastExchangeLog] = useState<{
|
||||
startTime: string;
|
||||
modelConfig: { provider: string; endpoint: string; modelName: string };
|
||||
requestPayload: any;
|
||||
responsePayload: any | null;
|
||||
errorDetail: { status: number; statusText: string; responseText: string; message: string } | null;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
stateRef.current.chatMessages = chatMessages;
|
||||
}, [chatMessages]);
|
||||
@@ -1327,16 +1319,7 @@ export default function ReportEditor() {
|
||||
delete payload.presence_penalty;
|
||||
delete payload.frequency_penalty;
|
||||
}
|
||||
const logEntry = {
|
||||
startTime: new Date().toISOString(),
|
||||
modelConfig: { provider: settings.activeAiProvider || 'kimi', endpoint: '/api/ai/chat', modelName },
|
||||
requestPayload: JSON.parse(JSON.stringify(payload)),
|
||||
responsePayload: null as any | null,
|
||||
errorDetail: null as { status: number; statusText: string; responseText: string; message: string } | null
|
||||
};
|
||||
const data = await createAiChatCompletion(payload);
|
||||
logEntry.responsePayload = data;
|
||||
setLastExchangeLog(logEntry);
|
||||
const responseText = data.choices[0].message.content.trim();
|
||||
const cleanedText = responseText.replace(/```json\n?|```/g, '');
|
||||
let responseJson: any = {};
|
||||
@@ -2861,32 +2844,6 @@ export default function ReportEditor() {
|
||||
{isEditingPrompts ? '+ 添加' : '⚙️'}
|
||||
</button>
|
||||
{isEditingPrompts && <button onClick={() => setIsEditingPrompts(false)} className="px-2 py-1 bg-blue-100 text-blue-600 text-[11px] rounded-full">完成</button>}
|
||||
<button onClick={() => {
|
||||
const data = {
|
||||
exportAt: new Date().toISOString(),
|
||||
url: window.location.href,
|
||||
messages: chatMessages,
|
||||
lastExchange: lastExchangeLog,
|
||||
metadata: {
|
||||
user: currentUser?.username || 'anonymous',
|
||||
activeProvider: (() => { const s = storage.get<SystemSettings>('systemSettings', {} as SystemSettings); return s.activeAiProvider || 'kimi'; })(),
|
||||
targetRegion: aiTargetRegion,
|
||||
modifyEnabled: aiModifyEnabled,
|
||||
chatInput,
|
||||
uploadedImagesCount: aiUploadedImages.length,
|
||||
selectedFramesCount: aiSelectedEditorImages.length
|
||||
}
|
||||
};
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `ai-logs-${Date.now()}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}} className="px-2 py-1 bg-slate-100 text-slate-500 text-[11px] rounded-full hover:bg-slate-200 ml-auto" title="导出 AI 日志(调试用)">
|
||||
导出 AI 日志
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 沉浸式输入框 */}
|
||||
@@ -3058,23 +3015,6 @@ export default function ReportEditor() {
|
||||
}}
|
||||
className="w-full py-2.5 bg-accent text-white rounded text-sm font-semibold hover:opacity-90 transition-colors"
|
||||
>导出 PDF</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
const ts = new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString().replace(/[:.]/g, '-').slice(0, 16);
|
||||
const title = reportData.title || '无标题';
|
||||
const patient = reportData.patientName || '未知';
|
||||
const hid = reportData.hospitalId || '无号';
|
||||
const blob = new Blob([JSON.stringify(reportData, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `图文报告-${title}-${patient}-${hid}-${ts}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
setExportModalOpen(false);
|
||||
}}
|
||||
className="w-full py-2.5 bg-slate-100 text-slate-700 rounded text-sm font-semibold hover:bg-slate-200 transition-colors"
|
||||
>导出 JSON</button>
|
||||
<button
|
||||
onClick={() => setExportModalOpen(false)}
|
||||
className="w-full py-2.5 border border-border text-text-main rounded text-sm font-semibold hover:bg-slate-50 transition-colors"
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Sidebar from '../components/Sidebar';
|
||||
import { Search, Eye, Edit, Trash2, FileText, History, X, Download, Printer } from 'lucide-react';
|
||||
import { User, Report, DEFAULT_FORM_FIELDS } from '../types';
|
||||
import { User, Report } from '../types';
|
||||
import { storage } from '../utils/storage';
|
||||
import { printDocument } from '../utils/print';
|
||||
import { canDeleteReport, canEditReport, getAccessibleReports } from '../utils/permissions';
|
||||
@@ -161,45 +161,10 @@ export default function ReportManage() {
|
||||
setSelectedIds([]);
|
||||
};
|
||||
|
||||
const buildExportData = (report: Report) => {
|
||||
const fields: Record<string, any> = {};
|
||||
DEFAULT_FORM_FIELDS.forEach(f => {
|
||||
fields[f.key] = (report as any)[f.key];
|
||||
});
|
||||
return {
|
||||
meta: {
|
||||
id: report.id,
|
||||
title: report.title,
|
||||
createdAt: report.createdAt,
|
||||
updatedAt: report.updatedAt,
|
||||
author: report.author,
|
||||
authorName: report.authorName,
|
||||
status: report.status,
|
||||
revision: report.revision || 1
|
||||
},
|
||||
fields
|
||||
};
|
||||
};
|
||||
|
||||
const downloadJSON = (data: any, filename: string) => {
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const exportSinglePDF = (report: Report) => {
|
||||
printDocument(report.content);
|
||||
};
|
||||
|
||||
const exportSingleJSON = (report: Report) => {
|
||||
const data = buildExportData(report);
|
||||
downloadJSON(data, `报告_${report.patientName || '未命名'}_${report.id}.json`);
|
||||
};
|
||||
|
||||
const exportBulkPDF = () => {
|
||||
const users = storage.get<User[]>('users', []);
|
||||
const selectedReports = getAccessibleReports(currentUser!, reports, users).filter(r => selectedIds.includes(r.id));
|
||||
@@ -207,14 +172,6 @@ export default function ReportManage() {
|
||||
printDocument(mergedHTML);
|
||||
};
|
||||
|
||||
const exportBulkJSON = () => {
|
||||
const users = storage.get<User[]>('users', []);
|
||||
const selectedReports = getAccessibleReports(currentUser!, reports, users).filter(r => selectedIds.includes(r.id));
|
||||
const data = selectedReports.map(r => buildExportData(r));
|
||||
const timestamp = new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString().replace(/[:.]/g, '-').slice(0, 16);
|
||||
downloadJSON(data, `reports_export_${timestamp}.json`);
|
||||
};
|
||||
|
||||
const openExportModal = (report: Report) => {
|
||||
setExportTarget(report);
|
||||
setExportModalOpen(true);
|
||||
@@ -280,12 +237,6 @@ export default function ReportManage() {
|
||||
>
|
||||
<Printer size={14} /> 批量导出 PDF
|
||||
</button>
|
||||
<button
|
||||
onClick={exportBulkJSON}
|
||||
className="px-3 py-1.5 text-sm font-medium rounded-lg bg-white border border-border hover:bg-slate-100 transition-colors flex items-center gap-1"
|
||||
>
|
||||
<Download size={14} /> 批量导出 JSON
|
||||
</button>
|
||||
<button
|
||||
onClick={handleBulkDelete}
|
||||
className="px-3 py-1.5 text-sm font-medium rounded-lg bg-red-50 text-red-600 hover:bg-red-100 transition-colors"
|
||||
@@ -480,7 +431,7 @@ export default function ReportManage() {
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-text-muted mb-4">选择导出格式:</p>
|
||||
<p className="text-sm text-text-muted mb-4">将通过浏览器打印导出,可保存为 PDF。</p>
|
||||
<div className="flex flex-col gap-3">
|
||||
<button
|
||||
onClick={() => { exportSinglePDF(exportTarget); setExportModalOpen(false); }}
|
||||
@@ -492,16 +443,6 @@ export default function ReportManage() {
|
||||
<div className="text-xs text-text-muted">调用浏览器打印并保存为 PDF</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { exportSingleJSON(exportTarget); setExportModalOpen(false); }}
|
||||
className="w-full px-4 py-3 rounded-lg bg-slate-50 hover:bg-slate-100 transition-colors flex items-center gap-3"
|
||||
>
|
||||
<Download size={18} className="text-text-muted" />
|
||||
<div className="text-left">
|
||||
<div className="text-sm font-semibold text-text-main">导出 JSON</div>
|
||||
<div className="text-xs text-text-muted">下载结构化字段数据</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -763,24 +763,6 @@ export default function TemplateManage() {
|
||||
setSelectedIds([]);
|
||||
};
|
||||
|
||||
const handleBatchExport = () => {
|
||||
if (selectedIds.length === 0) return;
|
||||
const targets = templates.filter(t => selectedIds.includes(t.id));
|
||||
const ts = getExportTimestamp();
|
||||
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<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
@@ -803,30 +785,29 @@ export default function TemplateManage() {
|
||||
|
||||
const handleExportTemplate = (template: Template) => {
|
||||
const exportData = createTemplatePackage(template, template.content, template.fields || formFields);
|
||||
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
|
||||
const html = createTemplateHtmlDocument(exportData);
|
||||
const blob = new Blob([html], { type: 'text/html' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
const ts = getExportTimestamp();
|
||||
a.download = `模板导出-${safeFileName(template.name)}-${ts}.json`;
|
||||
a.download = `模板导出-${safeFileName(template.name)}-${ts}.html`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const downloadCurrentTemplatePackage = (format: 'json' | 'html') => {
|
||||
const downloadCurrentTemplateHtmlPackage = () => {
|
||||
const content = editorRef.current?.innerHTML || currentTemplate?.content || '';
|
||||
const name = currentTemplate?.name || '模板';
|
||||
const templatePackage = createTemplatePackage(currentTemplate, content, currentTemplate?.fields || formFields);
|
||||
const ts = getExportTimestamp();
|
||||
const safeName = safeFileName(name);
|
||||
const body = format === 'html'
|
||||
? createTemplateHtmlDocument(templatePackage)
|
||||
: JSON.stringify(templatePackage, null, 2);
|
||||
const blob = new Blob([body], { type: format === 'html' ? 'text/html' : 'application/json' });
|
||||
const body = createTemplateHtmlDocument(templatePackage);
|
||||
const blob = new Blob([body], { type: 'text/html' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${safeName}-${ts}.${format}`;
|
||||
a.download = `${safeName}-${ts}.html`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
@@ -952,12 +933,6 @@ export default function TemplateManage() {
|
||||
<div className="px-4 pt-3 pb-1 flex items-center justify-between bg-slate-50 border-b border-border">
|
||||
<span className="text-xs text-text-muted font-bold">已选中 {selectedIds.length} 项</span>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleBatchExport}
|
||||
className="px-2 py-1 rounded-md bg-blue-50 text-blue-600 text-[10px] font-bold hover:bg-blue-100 transition-colors"
|
||||
>
|
||||
批量导出
|
||||
</button>
|
||||
<button
|
||||
onClick={handleBatchDelete}
|
||||
className="px-2 py-1 rounded-md bg-red-50 text-red-600 text-[10px] font-bold hover:bg-red-100 transition-colors"
|
||||
@@ -1007,7 +982,7 @@ export default function TemplateManage() {
|
||||
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"
|
||||
>
|
||||
导出
|
||||
导出HTML
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDeleteTemplate(tpl.id); }}
|
||||
@@ -1566,18 +1541,11 @@ export default function TemplateManage() {
|
||||
>导出 PDF</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
downloadCurrentTemplatePackage('html');
|
||||
downloadCurrentTemplateHtmlPackage();
|
||||
setExportModalOpen(false);
|
||||
}}
|
||||
className="w-full py-2.5 bg-slate-900 text-white rounded text-sm font-semibold hover:bg-slate-800 transition-colors"
|
||||
>导出 HTML 模板包</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
downloadCurrentTemplatePackage('json');
|
||||
setExportModalOpen(false);
|
||||
}}
|
||||
className="w-full py-2.5 bg-slate-100 text-slate-700 rounded text-sm font-semibold hover:bg-slate-200 transition-colors"
|
||||
>导出 JSON 模板包</button>
|
||||
<button
|
||||
onClick={() => setExportModalOpen(false)}
|
||||
className="w-full py-2.5 border border-border text-text-main rounded text-sm font-semibold hover:bg-slate-50 transition-colors"
|
||||
|
||||
Reference in New Issue
Block a user