# 工作进度-4:Grid++Report 6 → 7 替换 + 模板实例化(ReportFile)+ 页面内嵌报表设计工作台 > 时间:2026-10-20(承接工作进度-3) > 环境:后端 .NET 8 + FreeSql + MediatR(Windows/SQLite),前端 Vue3 + Element Plus + Vite > 本机 Grid++Report:**7.0**(`D:\GridReport7`,设计器 `gr7designer64.exe`,WEB 客户端 `gr7webapp64.exe`) > 参考:锐浪 GR7 官方 `Samples/Web/vue`、`Samples/Web/html/html5`;F9 实体 `Report.cs` / `ReportFile.cs` / `Report_FavoriteGroup.cs` --- ## 一、本轮目标 1. 把报表渲染链路从 **Grid++Report 6** 整体换成 **Grid++Report 7**(GR6 的浏览器插件方案已被 Chrome/Edge 移除) 2. 模板不再"改完先保存再上传 .grf",而是**作为报表的一个属性实例**,与配置/参数**一次保存** 3. 参考 F9 实体设计,把模板从报表主表拆成独立实体 **`ReportFile`**(`Report.ReportFileId` 关联) 4. 按 WinForms「编辑报表」的嵌入形态,做**页面内嵌的报表设计工作台**,不再唤起外部设计器程序 --- ## 二、GR7 替换(技术结论先行) | 项 | GR6 | GR7 | |---|---|---| | 网页内渲染库 | `grhtml5-6.8-min.js`(6.8.24.0119) | **`grhtml5-7-min.js`(7.0.26.0930)** | | 渲染入口 | `rubylong.grhtml5.insertReportViewer(id, tpl, data)` | **API 完全一致**(可直接替换) | | Web 报表客户端 | `grwebapp6.exe`,协议 `grwebapp://`,WS 端口 **22333** | `gr7webapp64.exe`,协议 **`gr7webapp://`**,WS 端口 **22337** | | 设计器 | `grdesigner6.exe` | `gr7designer64.exe` | | `.grf` 格式 | UTF-8 BOM + JSON | **新增 `_WR7_` 压缩格式**(`EF BB BF` + `_WR7_w...`),也兼容旧 JSON;库内 `gr.wr.decodeWR()` 解码 | **关键判断**:`.grf` 里的 `Version` 保持 `6.8.5.0` 不动——GR7 自带样例全是 6.6.8.1,说明向下兼容确定;`grhtml5-7-min.js` 中 `Version` 只是 `grconst.VERSION` 常量,不校验文件版本。 ### 改动清单 | 文件 | 改动 | |---|---| | `web/public/grlib/grhtml5-7-min.js` | 新增(官方 270KB),删除 `grhtml5-6.8-min.js` | | `web/src/utils/gridreport/loadGrhtml5.js` | 加载 `/grlib/grhtml5-7-min.js` | | `web/src/utils/gridreport/grwebapp.js` | 整份换 GR7 官方版(`gr7webapp://` / 22337 / v7 客户端下载 URL),启用 ESM 导出 | | `tools/report-designer/F9ReportDesignerHandler.ps1` | 设计器优先 `D:\GridReport7\gr7designer64.exe`,其次 .grf 关联(只认 gr7designer),最后回退 GR6 | | `tools/report-designer/install-gr7-grf-assoc.reg` | 新增:`.grf` → `GR7.Template` → `gr7designer64.exe`(HKCU 用户级,已导入生效) | ### 顺带修掉的两个真缺陷 | # | 问题 | 说明 | |---|---|---| | 1 | `viewer.Stop()` 一直是空操作 | 官方 API 是**小写 `stop()`**(GR6/GR7 都是),原来被 `try/catch` 吞掉 → 切 Tab / 关预览从未真正销毁渲染实例。改为 `(viewer.stop \|\| viewer.Stop).call(viewer)` | | 2 | `urlAddRandomNo()` 污染 token | URL 已带 `?access_token=xxx` 时再拼 `?id=`,浏览器把 `xxx?id=123` 整体当 token → 客户端打印/导出 401。改为已有查询串时用 `&` | --- ## 三、模板保存方式:随报表保存 + 上传/下载 .grf(两种并存) > 中途做过"只随报表保存、去掉上传/下载"的版本,后按需求**恢复上传/下载 .grf 的方式**,并保留"随报表保存"用于页面内画布编辑落库。 ### 3.1 后端 `ReportCreateDto` / `ReportUpdateDto` 增加 `TemplateJson`(.grf 的 JSON 文本);`CreateAsync/UpdateAsync` 内一并落库,**不传则保留原模板**(启停等只改配置的请求不会清模板)。 模板文本规范化 `NormalizeTemplateJson()`:去 BOM → trim → 校验必须是 JSON 对象 → 否则 400「报表模板(.grf)不是合法 JSON」。 独立上传/下载接口保持不变: - `POST /api/sys/report/{id}/template`(multipart,`IFormFile`)→ 解码 UTF-8、去 BOM → 写 `ReportFile.DesignContent` - `GET /api/sys/report/{id}/template/download` → 输出带 UTF-8 BOM 的 `.grf`,文件名 `{编码}.grf` ### 3.2 前端(两条路径) | 路径 | 入口 | 提交方式 | |---|---|---| | A. 页面内编辑 | 中栏「页面设计」画布 / 「源码 JSON」 | `save()` 时随配置一起 `templateJson` 提交 | | B. 锐浪设计器设计好后上传 | 工作台顶栏「上传 .grf / 下载 .grf」;列表卡片「上传模板 / 下载模板」 | 走独立接口,**不走保存**;上传成功后画布自动重新载入 | - 工作台:`uploadGrf()` 选本地 `.grf` → `POST /{id}/template` → `reloadTemplateFromServer()` 重载画布;`downloadGrf()` 用 `access_token` 直开下载地址 - 列表页:隐藏 `input[type=file][accept=".grf"]` + `pickUpload(row)` / `downloadTemplate(row)`;若该报表正在右侧编辑,上传后 `designerKey++` 强制工作台重新载入 --- ## 四、实体重构:Report + ReportFile(对齐 F9) ### 4.1 新实体 `Domain/Entities/SYS/Reports/ReportFile.cs`(表 `sys_report_files`,基类 `BusinessEntity`): ```csharp public class ReportFile : BusinessEntity { public long ReportId { get; private set; } public string DesignContent { get; private set; } = ""; // .grf 的 JSON 文本 public void UpdateContent(string designContent) { ... } } ``` `Report` 增加 `ReportFileId`(对齐 F9 `Report.ReportFileId`)+ `BindReportFile(long)`,原 `TemplateData/TemplateName/TemplateSize` 标记**废弃**(仅供启动迁移读取,不再写入),新增 `ClearLegacyTemplate()`。 ### 4.2 服务层 | 方法 | 说明 | |---|---| | `GetTemplateContentAsync(Report)` | 读 `ReportFile.DesignContent`;无模板返回 null | | `SaveTemplateAsync(Report, json)` | 有实例则 `UpdateContent`,否则新建实例并 `BindReportFile`;**只改内存**,由调用方统一保存 | | `FindTemplateFileAsync(Report)` | 优先按 `ReportFileId` 查,兜底按 `ReportId` 查 | | `DeleteAsync` | 级联删除模板实例 | | `HasTemplate(r)` | `r.ReportFileId != 0` | Controller 侧模板输出统一补 **UTF-8 BOM**(`ToGrfBytes()`),保证下载给设计器/浏览器渲染都符合 .grf 约定;**数据库里存纯文本**(不带 BOM),避免"套娃 BOM"。 ### 4.3 旧数据自愈 `DbSeeder.MigrateReportTemplateToFile()` 在启动时把 `sys_reports.template_data`(旧内嵌列)迁移成 `ReportFile` 并回填 `ReportFileId`,随后清空废弃字段。同时 `EnsureColumn(fsql, "sys_reports", "report_file_id", ...)` 兜底旧库加列(`SyncStructure` 不会为已存在表加列)。 实测启动日志: ``` [DbSeeder] 报表「职工花名册」模板已迁移到 ReportFile#139204832664752128 [DbSeeder] 报表「落丝记录」模板已迁移到 ReportFile#139204832731860992 ``` --- ## 五、报表编辑页(配置 + 参数 + 模板预览) > **重要变更**:先用"自研网页可视化设计器"做过一版(画布 + 对象树 + 属性面板),因达不到锐浪设计器的排版能力,**已整体移除**,模板改回"上传/下载维护"。 ### 5.1 页面结构 `/report/manage` 为 **左列表 + 右编辑面板**: ``` ┌──────────────┬──────────────────────────────────────────────────────┐ │ 报表列表 │ 顶栏:编辑报表·名称 [模板已配置] [删除][上传 .grf][下载 .grf] │ ┌──────────┐ ├─────────────┬────────────────────────────────────────┤ │ │落丝记录 │ │ 配置 | 参数 │ [预览视图][模板源码] │ │ │PROD_QS_1 │ │ │ │ │ │模板已配置 │ │ 分组/编号 │ ┌──── grhtml5 网页内渲染 ────┐ │ │ │上传 下载 删除│ │ 名称/顺序 │ │ 日期 车间 机组 庄口 回数 ... │ │ │ └──────────┘ │ 数据源/SQL │ └─────────────────────────────┘ │ │ ┌──────────┐ │ 备注 │ │ │ │职工花名册 │ │ Config JSON │ 模板源码:只读查看 .grf JSON │ │ └──────────┘ │ │ │ └──────────────┴─────────────┴────────────────────────────────────────┘ ``` ### 5.2 功能边界 | 区域 | 内容 | |---|---| | 左栏·配置 | 分组 / 编号 / 名称 / 顺序 / 数据源 `DsName` / 数据 SQL / 备注 | | 左栏·参数 | 查询参数列表(参数名、标签、控件类型、默认值、`Ds_Service`、`Ds_Static`、必填) | | 左栏·Config JSON | 自动模式随表单一同生成,可切「源码」直接写 | | 右栏·预览视图 | grhtml5 渲染真实数据;未上传模板时显示占位提示 + 上传按钮 | | 右栏·模板源码 | 只读查看 `.grf` JSON,用于核对字段名 | | 顶栏 | 删除 / 上传 .grf / 下载 .grf / 刷新预览 / 保存 | 模板本身**不在网页里编辑**——用 Grid++Report 7 设计器排版 `.grf`,再上传。 ### 5.3 文件 | 文件 | 说明 | |---|---| | `web/src/components/report/ReportEditor.vue` | 新增:配置 + 参数 + 预览 + 模板源码(只读) | | `web/src/components/report/ReportDesigner.vue` | **已删除**(自研画布/对象树/属性面板) | | `web/src/views/report/manage/index.vue` | 重写:左列表卡片(名称/编码/分组/模板状态/上传/下载/删除)+ 右侧编辑面板 | | `web/src/views/report/manage/editor.vue` | **已删除**(旧的全屏抽屉版) | 保存语义:编辑已有报表时**不提交模板**,避免覆盖刚上传的 `.grf`;仅新建时写入一份最小兜底模板。 --- ## 六、踩坑记录(本轮) | # | 问题 | 根因 | 解决 | |---|---|---|---| | 1 | 后端改完编译报 `MSB3027/MSB3021` 文件被锁 | `dotnet run` 进程占用 `bin` 下 dll | 先 `Stop-Process` 停后端再 build(编译本身 0 错误,只是拷贝失败) | | 2 | 模板读回来变空、字段列表丢失 | 服务端存 .grf 带 UTF-8 BOM,axios 的 `JSON.parse` 失败后**静默返回字符串**,前端再 `JSON.parse` 遇 BOM 抛错 → 走 `catch` 得到空模板 | 存储改纯文本,输出时补 BOM;前端 `parseTemplatePayload()` 统一去 BOM | | 3 | 旧库加了字段但服务端读不到 | FreeSql `CodeFirst.SyncStructure` **不会给已存在表加列** | `EnsureColumn(fsql, "sys_reports", "report_file_id", "INTEGER NOT NULL DEFAULT 0")` | | 4 | 迁移后再也读不到旧模板 | 实体属性删了就读不到 `template_data` 列 | 保留 `TemplateData` 属性标废弃,仅供迁移读取,迁移后 `ClearLegacyTemplate()` | | 5 | 保存后模板体积从 4300B 变 2911B | 前端 `JSON.stringify(tpl)` 无缩进,缩进空白被去掉 | 非缺陷,属预期(`JSON.stringify(obj, null, '\t')` 才带缩进) | | 6 | 画布拖动无效 | `startDrag(e, path, mode, event)` 定义 4 参,模板只传 3 参 | 签名改为 `startDrag(path, mode, event)` | | 7 | 对象树 `default-expanded-keys` 不生效 | 节点没有 `node-key` | 每个节点补 `pathKey = JSON.stringify(path)` | | 8 | **上传 .grf 报「不是合法 JSON」** | **GR7 设计器保存的 .grf 已不是 JSON**:`EF BB BF` + `_WR7_w...` 压缩文本(抽样 D:\GridReport7\Samples 1102 个 .grf:970 个 `_WR7_` 格式,130 个旧 JSON 格式) | 后端不再做 JSON 校验,只去 BOM/空白原样存储;`DecodeTemplateBytes()` 按 BOM 识别 UTF-8/UTF-16LE/UTF-16BE | | 9 | 上传后预览报「获取报表模板失败 URL: 乱码」 | GR7 库用 `F(e)` 判断模板来源:只有 `_WR_`(4 字符,下划线)开头才当"内容",而设计器存的是 `_WR7_` → 被判为 URL 去请求 | 预览改传**模板 URL**(`/template.json?access_token=`),走库的 ajax → `Report.load()` 分支,内部 `"_WR"==substr(0,3)` 命中后 `decodeWR()` 正常解码 | | 10 | `/template.json` 返回带 BOM 会破坏前缀判断 | `\uFEFF` 开头使 `"_WR"==substr(0,3)` 与 `"{"===charAt(0)` 双双失效 | `template.json` **不输出 BOM**;仅 `/template/download`(给设计器的文件)带 BOM | --- ## 七、验证记录 ### 7.1 接口层(node 脚本直连 `localhost:3000`) ``` CREATE_HAS_TPL=true NAME=tmp_tpl_351543.grf SIZE=626 TPL_BOM=efbbbf TPL_TITLE=TMP 随报表保存 FIELDS=A,B NO_TPL_UPDATE_HAS_TPL=true SIZE=626 ← 不带模板的修改不会清模板 WITH_TPL_UPDATE_TITLE=改过的标题 ← 带模板一起改 BAD_TPL_CODE=400 MSG=报表模板(.grf)不是合法 JSON ``` 迁移 + 模板随报表保存(重启后): ``` LIST=PROD_QS_1(file=139204832731860992,has=true) | employee_directory(file=139204832664752128,has=true) TPL_BOM=efbbbf LEN=4300 UPDATE_CODE=200 AFTER_AUTHOR=F9 内嵌设计器 KEEP_TPL_LEN=2911 ← 不带模板的 PUT 后模板仍在 ``` ### 7.2 报表中心(GR7 渲染) ``` GR_SCRIPTS=["/grlib/grhtml5-7-min.js"] HOLDER_HTML_LEN=240770 HOLDER_CHILDREN=3 职工花名册 部门:(全部) 关键字:(全部) 记录数:942 工号 职工姓名 性别 部门 工种 电话 入职日期 / 付华芳 女 丝业生产中心 ... ALERT=NONE ``` ### 7.3 内嵌设计工作台 ``` LIST_ITEMS=2 TOPBAR=编辑报表 · 落丝记录 VIEW_TABS=["页面设计","明细字段","源码 JSON","预览视图"] LEFT_TABS=["配置","参数 (0)"] PAPER_EXISTS=true W=793.8px ← A4 21cm SECTIONS=["报表头","页眉","明细网格","页脚"] CONTROLS=4 DETAIL_CELLS=36 PROPS_TITLE=属性 · StaticBox#0 PROPS=["Type","Name","Center","Left","Top","Width","Height","Text"] CONTROLS_AFTER_INSERT=5 → CONTROLS_AFTER_DELETE=4 PREVIEW_TEXT=日期 车间 机组 庄口 回数 颜色 半车 落丝时间 缫丝桶数 ``` ### 7.4 编译 - 后端 `dotnet build`:**0 错误**(仅有既存 CS8632 nullable 注释警告) - 前端 `vite build`:通过(仅 `@microsoft/signalr` 的既有 Rollup 注释警告) --- ## 八、当前状态 | 项 | 状态 | |---|---| | 报表渲染库 | GR7(`grhtml5-7-min.js` 7.0.26.0930)✅ | | WEB 客户端打印/PDF/Excel | GR7(`gr7webapp://` / 22337)✅ | | 本机设计器协议 | 指向 `gr7designer64.exe`,`.grf` 关联已切 GR7 ✅ | | 模板存储 | 独立实体 `ReportFile.DesignContent`,`Report.ReportFileId` 关联 ✅ | | 新增/编辑报表 | 配置 + 参数 + 模板一次保存;**同时保留 .grf 上传/下载** ✅ | | 旧数据 | 启动自动迁移到 `ReportFile`,日志可查 ✅ | | 报表管理页 | 左列表 + 右编辑面板(配置/参数/预览/模板源码只读),无自研设计器 ✅ | | 报表中心 | 多 Tab 预览正常(942 条职工)✅ | | 后端服务 | 运行中 `http://localhost:3000`(本轮重启后可正常工作)✅ | | 前端服务 | 运行中 `http://localhost:5173` ✅ | 启动方式不变: ```powershell cd backend/F9WebAiTest.Api && dotnet run cd web && npm run dev ``` 默认账号:`admin / 123456` --- ## 九、遗留与后续建议 1. **`Report_FavoriteGroup` 未实现**:用户提供的 F9 实体(`User_Id` / `Parent_Id` / `Name` / `Sort`)对应「报表收藏分组」,本轮聚焦模板实例化与内嵌设计器,未落地。 2. **模板编辑能力边界**:锐浪设计器是 COM 组件,浏览器无法嵌入;自研的网页画布在排版精度上达不到要求,已移除。模板排版统一在 Grid++Report 7 设计器完成 → 上传。 3. **无网页端模板编辑**:如需在系统内改模板,需下载 `.grf` → 本机设计器修改 → 上传覆盖;或走 `f9designer://` 本机设计器协议(自动回传)。 4. **历史废弃列**:`sys_reports.template_data / template_name / template_size` 仍在表中(值已清空),确认无回滚需求后可 `DROP COLUMN`。 5. **编辑器与报表中心分叉**:报表中心的参数控件配置(左栏「参数」tab)目前是纯文本填 `ControlType`,未接 `utils/gridreport/controls` 注册中心的分组下拉(原 `editor.vue` 有,随删除一起丢失,可补回)。 6. **工作进度-3 遗留仍在**:`Number()` 精度隐患、admin `RolePermission` 重复、部门权限无消费方、`system/*` 死代码。