# 工作进度-8:模板源码在线编辑(免下载 / 上传) > 时间:2026-10-20(承接工作进度-7) > 触发:用户提出「能不能直接编辑对应报表的模板文件然后保存,而不是下载 → 改 → 上传 → 保存」 --- ## 一、结论 可以。模板本来就以文本存在库里(`sys_report_files.design_content`,TEXT), 之前只是没有「写回文本」的入口,只能整文件上传。本次新增: - 后端一个接口:`PUT /api/sys/report/{id}/template/content`,请求体 `{ "content": "<模板全文>" }` - 前端「模板源码」由**只读**改为**可编辑**,配「保存模板 / 格式化 / 撤销修改 / 放大编辑」 原来的「上传 .grf / 下载 .grf」保留不动(用于本机设计器排版、外部模板导入)。 --- ## 二、方案取舍 | 方案 | 说明 | 结论 | |---|---|---| | 现状:下载 → 本地改 → 上传 | 需要本地装锐浪设计器或文本编辑器,来回一次文件 | 保留,不再作为主路径 | | **在线编辑模板源码 + 保存** | 文本表单直接改 `.grf` 内容,保存即入库,刷新预览立即生效 | ✅ 本次实现 | | 内嵌可视化设计器(拖控件、改属性) | 需要对 `.grf` 全量解析 / 回写;GR7 设计器格式是 `_WR` 压缩文本,无解码器 | 不做(见工作进度-5 遗留) | --- ## 三、后端 ### 3.1 DTO(`ReportDtos.cs`) ```csharp public class ReportTemplateContentDto { public string? Content { get; set; } } ``` ### 3.2 Service(`IReportService` / `ReportService.SaveTemplateContentAsync`) ```csharp public async Task SaveTemplateContentAsync(long id, string content) { var entity = ... ?? throw new F9NotFoundException("报表不存在"); var text = NormalizeTemplateContent(content); // 去 BOM / trim,空内容拦截 if (head.StartsWith('{') || head.StartsWith('[')) JsonDocument.Parse(text); // JSON 语法校验,失败抛 F9OperationException await SaveTemplateAsync(entity, text); // 复用「有则更新 ReportFile,无则新建并绑定」 ... return text.Length; } ``` - 与上传接口走同一套落库逻辑(`SaveTemplateAsync`),因此「新建 / 更新 / 绑定 `report_file_id`」行为一致 - 校验规则: | 内容形态 | 服务端行为 | |---|---| | `{...}` / `[...]`(GR6 JSON 模板) | 语法校验,不合法直接 400「模板 JSON 不合法:…」 | | `_WR...`(GR7 设计器压缩文本) | 原样保存(无法在服务端校验) | | 空 / 仅空白 | 400「报表模板内容为空」 | ### 3.3 接口(`SysReportController`) ``` PUT /api/sys/report/{id}/template/content Body: { "content": "..." } 200 → { code: 200, data: { id, size, hasTemplate: true }, message: "模板已保存" } 404 → 报表不存在 400 → 内容为空 / JSON 不合法 ``` 与已有接口的关系: | 方法 | 路由 | 用途 | |---|---|---| | GET | `/{id}/template` | 渲染库 / 前端读模板 | | POST | `/{id}/template` | 上传 .grf 文件(设计器回传) | | **PUT** | **`/{id}/template/content`** | **在线编辑后保存文本(本次新增)** | | GET | `/{id}/template/download` | 下载 .grf 给本机设计器 | --- ## 四、前端 ### 4.1 `ReportEditor.vue` —— 「模板源码」页 - 去掉 `readonly`;`v-model="templateSource"` 直接编辑 - 新增状态:`templateSaved`(已入库内容,用于判定「未保存」)、`templateSaving`、`wrUnlocked`、`sourceMaximized` - 工具条: | 按钮 / 标签 | 行为 | |---|---| | `JSON 模板` / `_WR 压缩模板` | 当前模板形态提示 | | `未保存` | `templateSource !== templateSaved` 时显示 | | 格式化 | `JSON.parse` + `JSON.stringify(…, '\t')`;非 JSON 禁用 | | 撤销修改 | 确认后恢复为上次保存内容 | | 放大编辑 | 整屏弹窗编辑同一份文本(同一 v-model) | | 保存模板 | 前端先校验 JSON,再 `PUT /sys/report/{id}/template/content` | - 保存成功后:重新拉取模板 → `hasTemplate = true` → 重置预览(提示点「刷新预览」)→ `emit('saved')` - 「保存报表」时若模板源码有未保存修改,会先弹确认:`先保存模板` / `忽略模板修改`,避免手改内容被静默丢弃 - `_WR` 压缩模板默认**只读**,需勾「允许编辑」才可改,保存前再二次确认(无本地校验能力) - 报表未保存时提示「请先保存报表,再编辑模板源码」;已保存但无模板时也可以直接粘贴 JSON 保存(相当于无文件新建模板) ### 4.2 预览缓存 `grhtml5` 拉模板是普通 GET,容易被浏览器缓存。预览 URL 追加 `_t=Date.now()`(编辑器与报表中心都加),保证改完模板点「刷新预览」能看到新内容。 ### 4.3 `views/report/manage/index.vue` —— 列表页入口 - 每行新增「编辑模板」:选中该报表并让右侧工作台**直接打开「模板源码」页**(`ReportEditor` 新增 `openSource` prop,配合 `designerKey` 强制重载) - 「上传模板 / 下载模板」保留 --- ## 五、验证 | 项 | 结果 | |---|---| | 后端 `dotnet build`(输出到独立目录避开运行中进程锁定) | ✅ 0 错误 | | `IReportService` / `ReportService` / DTO / Controller 改动 | ✅ 编译通过 | | 前端 IDE 诊断(`ReportEditor.vue`、`manage/index.vue`、`center/index.vue`) | ✅ 0 诊断 | | 前端 `npm run build` | ⚠ 当前环境无 node/npm,未执行;需在本机 `cd web && npm run build` 复核 | > 运行中的 `F9WebAiTest.Api` 仍在用旧程序集,**需重启后端**后新接口才生效。 --- ## 六、遗留 1. **无语法高亮 / 行号**:目前是 `el-input textarea`,后续可换 CodeMirror / Monaco(需新增前端依赖)。 2. **无模板历史版本**:手工改坏后无法一键回滚,只能重新「生成模板」或重新上传;建议后续给 `sys_report_files` 加版本表。 3. **`_WR` 仍不能结构化编辑**:需要锐浪的解码 / 编码算法,暂以「允许编辑 + 风险提示」兜底。 4. **保存模板不刷新明细字段 / 报表参数页签**:手改 `DetailGrid.Recordset.Field` 后,左栏字段列表仍是旧值(点「识别明细列与报表参数」可同步)。