Files
yanpeng200212 a8890c81e9 新增质量模块(工艺执行书 / 样茧试缫)与报表中心
质量模块:
- 11 张工艺执行书表:封面、试产、选剥茧(含混茧明细)、试缫与大样、煮茧(含分车间配温、副煮车间)、自动缫(含大样)、后缫
- 10 张样茧试缫表:原料小样/综合样主表 + 选茧、解舒、茧幅、切剖、清洁洁净、茧丝百回纤度
- 服务与接口、主控指针回写(列表 √ 标记)、封面编号自增、报表数据包
- 前端 8 张报表打印版式、7 类工艺单据与 6 类调查录入弹窗、字段驱动表单/表格组件
- 菜单种子新增「质量」目录,样茧子表增加 YjssType 判别列,避免原料小样与综合样自增 Id 重号串数据

报表中心:
- 报表模板管理、锐浪 grhtml5 加载与预览、全屏设计面板、数据源 Provider(落丝记录、职工花名册)

其他:同步工作进度 1-11 文档、开发数据库快照、.gitignore 忽略运行日志目录
2026-10-22 10:30:37 +08:00

338 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 工作进度-1:报表中心模块(Grid++Report 6 集成)
> 时间:2026-10-19
> 环境:后端 .NET 8 + FreeSql + MediatR(Windows),前端 Vue3 + Element Plus + Vite
> 本机 Grid++Report 版本:**6.8.23.0618**(设计器 `grdesigner6.exe`)
---
## 一、目标
在系统(智慧缫丝 / F9WebAiTest)中嵌入报表系统:
1. 报表中心作为菜单页面,支持报表的**新增 / 删除 / 修改**
2. 每张报表有独立权限,管理员需在「角色管理 → 权限分配」中给角色授权后,用户才能看到
3. 报表支持 F9 的 `ReportConfig` 约定:动态查询控件 + 按 `DsName` 解析后端查询
---
## 二、Grid++Report 6 Web 嵌入方案调研(结论)
本机安装目录 `C:\Grid++Report 6` 关键发现:
| 项 | 结论 |
|---|---|
| 浏览器插件方案(NPAPI/ActiveX) | Chrome / Edge 已不支持,**弃用** |
| `grhtml5-6.8-min.js` | HTML5 浏览器内渲染,免插件、跨浏览器 ✅ |
| `grwebapp.js` + `grwebapp6.exe` | 本机 WEB 报表客户端,支持 preview/print/pdf/xls/csv/img/grd 等 |
| `.grf` 文件格式 | **UTF-8 BOM + JSON 文本**(可程序化生成) |
| `.grf` 版本 | 官方设计器保存为 `6.8.5.0`;`WebSamples` 里的样本是 `6.6.8.1` |
| `.grf` 文件关联 | `GR.Template` → `"C:\Grid++Report 6\grdesigner6.exe" "%1"` |
| 已有 URL 协议 | `grwebapp://` → `grwebapp6.exe` |
采用方案:**HTML5 网页内预览为主 + 本机客户端打印导出为辅 + f9designer 协议唤起设计器编辑**。
---
## 三、后端实现
### 1. 领域实体
| 文件 | 说明 |
|---|---|
| `Domain/Entities/SYS/Reports/Report.cs` | 报表实体(表 `sys_reports`),继承 `AggregateRoot` 自动建表 |
| `Domain/Events/SYS/Reports/Report_ChangedDomainEvent.cs` | 报表变更领域事件 |
`Report` 字段:`Code / Name / Category / Description / DataSql / DataParams / Config / TemplateData(byte[]) / TemplateName / TemplateSize / SortOrder / IsActive`
### 2. 权限模型(与现有 RBAC 完全契合)
现有 `Resource_Type_Enum` 已内置 `报表 = 3`,直接复用:
- 新建报表 → 自动创建 `Permission`(`ResourceType=报表`, `ResourceId=Report.Id`, `Code=report:{code}:visble`)
- 改编码 → 同步权限 Code;删除报表 → 级联删除权限单元 + `RolePermission` 关联
- 授权入口:角色管理 → 权限分配 → 「报表权限」tab(复用现有 `resourceType=3` 接口)
- 用户可见性:`AuthService.GetUserPermissionsAsync` 原生返回所有类型权限码 → 前端按 `report:{code}:visble` 过滤
### 3. 数据查询架构(F9 同构)
```
报表 Config.DsName
→ IReportDataProvider 注册表(静态 ReportName 解析)
→ MediatR CQRS Query → QueryHandler(注入业务读仓储)
→ ReportDto_DataSource(Report_Data: 数据集名 → ReportDataDto_Data)
→ ToGridReportJson() 转成 Grid++ JSON 数据
```
| 文件 | 说明 |
|---|---|
| `Application/Dtos/SYS/Reports/ReportDataSourceDto.cs` | `ReportDto_DataSource` + `ReportDataDto_Data`(`Field_Data` / `Parameter_Data`) |
| `Application/Interfaces/Reports/IReportDataProvider.cs` | `ReportName` + `GetData(pairs, mediator)` |
| `Application/CQRS/SYS/Reports/Queries/GetEmployeeDirectoryRepQuery.cs` | 职工花名册查询(注入 `IEmployeeReadRepository`) |
| `Application/Services/Reports/Providers/RepDt_Sys_Users.cs` | 示例 Provider(DsName = `RepDt_Sys_Users`) |
| `Application/Services/Reports/ReportService.cs` | CRUD + 权限联动 + 数据执行 + JSON 转换 |
| `Application/Services/Reports/IReportService.cs` | 服务接口 |
| `Application/Dtos/SYS/Reports/ReportDtos.cs` | 列表/详情/视图/增删改 DTO |
| `Api/Controllers/SysReportController.cs` | REST 控制器(详见下) |
### 4. 控制器端点
| 端点 | 说明 |
|---|---|
| `GET api/sys/report` | 分页(管理) |
| `GET api/sys/report/all` | 全部启用报表(角色授权树用) |
| `GET api/sys/report/{id}` | 详情(含 DataSql / Config) |
| `POST api/sys/report` | 新增(自动创建权限单元) |
| `PUT api/sys/report/{id}` | 修改(同步权限 Code) |
| `DELETE api/sys/report/{id}` | 删除(级联清理权限) |
| `POST api/sys/report/{id}/template` | 上传 .grf(multipart,限 .grf,50MB) |
| `GET api/sys/report/{id}/template/download` | 下载 .grf |
| `GET api/sys/report/{id}/template[.json\|.grf]` | 模板下载(供 HTML5 / 客户端,支持 `?access_token=`) |
| `GET/POST api/sys/report/{id}/data[.json]` | 报表数据(Grid++ JSON 格式) |
| `GET api/sys/report/visible` | 当前用户可见报表(按权限过滤) |
| `GET api/sys/report/{id}/preview` | 预览(模板 URL + 数据,包 `ApiResult`) |
### 5. 种子
`Infrastructure/Data/DbSeeder.cs`:
- `SeedReportMenus()`:`/report` 目录 + `/report/center`(报表中心)+ `/report/manage`(报表管理),自动授权 admin
- `SeedSampleReport()`:示例报表「职工花名册」(code `employee_directory`)
- `LoadEmbeddedTemplate()`:从嵌入资源读取 `.grf`
### 6. 其他后端改动
| 文件 | 改动 |
|---|---|
| `Api/Program.cs` | JwtBearer `OnMessageReceived` 支持 `/api/sys/report` 路径的 query token |
| `Application/DependencyInjection.cs` | 注册 `IReportService` + 自动扫描注册所有 `IReportDataProvider` |
| `Infrastructure/F9WebAiTest.Infrastructure.csproj` | `.grf` 模板作为嵌入资源 |
| `Application/F9WebAiTest.Application.csproj` | 新增 `System.Data.SQLite.Core`(与 FreeSql 驱动一致) |
| `Infrastructure/Assets/ReportTemplates/employee_directory.grf` | 基于官方 6.8.5.0 结构派生的示例模板 |
---
## 四、前端实现
| 文件 | 说明 |
|---|---|
| `web/public/grlib/grhtml5-6.8-min.js` | 官方 HTML5 渲染库(246KB) |
| `web/src/utils/gridreport/grwebapp.js` | 官方 WEB 客户端通信库 |
| `web/src/utils/gridreport/loadGrhtml5.js` | 动态加载 HTML5 库脚本 |
| `web/src/utils/gridreport/reportConfig.js` | F9 ReportConfig 解析 + 动态控件渲染映射 |
| `web/src/views/report/center/index.vue` | 报表中心(左侧分类树 + 动态查询面板,右侧多 Tab 报表区) |
| `web/src/views/report/manage/index.vue` | 报表管理(列表 + 打开全屏编辑器) |
| `web/src/views/report/manage/editor.vue` | 全屏报表编辑器(配置 / 设计 / 源码 / 预览) |
| `web/src/router/index.js` | componentMap 增加 `report/center/index`、`report/manage/index` |
| `web/src/views/sys/role/index.vue` | 权限分配对话框增加「报表权限」tab(`RESOURCE_TYPE.report = 3`) |
### 报表编辑器(editor.vue)页面结构
```
┌─────────────┬──────────────────────────────────────────┐
│ [配置][参数] │ [设计][源码 JSON][预览视图] │
│ │ 本机设计器 | 上传.grf | 下载.grf | 保存 │
│ 分组 │ 设计视图:报表标题 + 明细字段表格 │
│ 编号 │ 源码视图:完整 .grf JSON │
│ 名称 │ 预览视图:grhtml5 实时渲染(切换即渲染) │
│ 顺序 │ │
│ 数据源DsName │ │
│ 数据SQL │ │
│ 备注 │ │
│ Config JSON │ │
└─────────────┴──────────────────────────────────────────┘
```
---
## 五、本机设计器直连(f9designer:// 协议)
| 文件 | 说明 |
|---|---|
| `tools/report-designer/F9ReportDesignerHandler.ps1` | 协议处理器:下载模板 → 打开设计器 → 关闭后自动回传 |
| `tools/report-designer/install-f9designer-protocol.reg` | 注册协议(**HKCU 用户级,无需管理员**,已在本机注册) |
流程:
```
浏览器 --f9designer://open?base=..&rid=..&token=..&code=..--> Windows Shell
--> .ps1:curl 下载 .grf 到 %TEMP%\F9ReportDesigner\
--> 启动 grdesigner6.exe(等待关闭)
--> 关闭后 curl POST 回传模板 + 弹窗提示
```
---
## 六、踩坑与解决方案(重要)
| # | 问题 | 根因 | 解决 |
|---|---|---|---|
| 1 | 点击报表报「报表加载失败 请求失败」 | `/preview` 返回裸对象,而 `request.js` 拦截器要求 `data.code === 200` | 后端 Preview 包 `ApiResult` |
| 2 | 报表中心页面白屏 | `buildTree` 中根节点未初始化 `reports`,`node.reports.forEach` 抛 TypeError | 根/目录节点补 `reports: []` + 防御性兜底;目录 id 改为基于路径稳定值 |
| 3 | HTML5 取模板报 400「请选择要上传的 .grf 模板文件」 | grhtml5 库约定:**URL 不含 `.json` 就用 POST**(`/\.json/.test(url) ? "GET" : "POST"`),POST 撞上上传接口 | 模板增加 `.json` 路由别名,前端 URL 以 `.json` 结尾 |
| 4 | 客户端打印导出报「报表模板已经被破坏」 | grwebapp.js 同样约定(`.grf/.txt/.xml/.json` 才用 GET),且传给客户端的是相对 URL | 增加 `template.grf` / `data.json` 别名;前端传绝对 URL(`location.origin`) |
| 5 | 下载的 .grf 设计器打不开 | 手写模板标 `Version: 6.6.8.1` 且结构不完整,设计器 6.8 无法解析 | 改为基于官方 6.8.5.0 模板派生,作为嵌入资源 |
| 6 | 报表参数不生效 | 键名写成小写 `parameter`;且 HTML5 不解析文本内 `[#Parameter(xx)#]` 表达式 | 数据键名改 **`Parameter`**(大写);模板改用 `StaticBox` 的 `"Parameter":"关键字"` 属性绑定,并在模板顶层声明 `Parameter` 节点 |
| 7 | 字段值渲染为空 | ASP.NET 默认 camelCase 序列化把 `EmployeeName` 输出成 `employeeName`,与模板字段不符 | 数据行改用 `Dictionary` 承载(字典键不受命名策略影响),反射 + PropertyInfo 缓存 |
| 8 | 切到预览视图空白 | `watch(activeView)` 未触发渲染 | 切换到 preview 时自动调用 `renderPreview()` |
| 9 | 协议唤起脚本不执行 | `.ps1` 为 UTF-8 **无 BOM**,PowerShell 5.1 按 GBK 读取致中文乱码、语法破坏 | 转为 **UTF-8 BOM** |
| 10 | 注册表写入失败 | `HKEY_CLASSES_ROOT` 需管理员 | 改注册到 `HKEY_CURRENT_USER\Software\Classes`(免管理员) |
| 11 | 第一次进编辑报表页报「请求失败」,明细字段/模板参数暂无数据 | 编辑器 `GET /sys/report/{id}/template` 返回**裸 .grf JSON**(无 ApiResult 包装),`request.js` 拦截器要求 `data.code === 200`,判定失败 reject → 前端兜底为空模板(字段/参数全空) | 拦截器增加放行:响应无 `code` 字段(非 ApiResult 包装,如模板原文/文件流)直接原样返回 |
### Grid++ 数据 JSON 格式约定(已验证)
```jsonc
// 单数据集
{ "recordset": [ {...}, {...} ] }
// 多数据集(顶层键 = 数据集名)
{ "Master": [ {...} ], "Detail": [ {...} ] }
// 报表参数(键名必须与模板 Parameter 节点一致,渲染器取数组第一项)
{ "recordset": [...], "Parameter": [ { "账期": "2026年10月", "车间": "前缫" } ] }
```
---
## 七、新增报表的标准流程
1. 建 CQRS:`XXXRepQuery` + Handler,Handler 内注入业务**读仓储**查询,组装 `ReportDto_DataSource`
- `Field_Data`:每行匿名对象的属性名 = 模板字段名
- `Parameter_Data`:账期 / 车间 / 查询时间等报表参数
2. 建 Provider:`RepDt_XXX : IReportDataProvider`,`ReportName` 常量 + 解析 `pairs` → `mediator.Send(query)`(DI 自动发现,无需注册)
3. 报表管理 → 新增报表,填 `Config`:
```json
{
"DsName": "RepDt_XXX",
"ViewerType": 0,
"Parms": [
{ "Name": "q_yearmonth", "Label": "账期", "Prompt": "", "IsRequired": false,
"ControlType": "AntdUI.DatePickerYearMonth", "Ds_Static": null, "Ds_Service": null, "DefaultValue": "默认值" }
]
}
```
4. 用设计器做 `.grf`(记录集字段对齐 `Field_Data`;显示查询条件的控件设 `"Parameter":"账期"`,模板顶层声明 `Parameter` 节点)
5. 上传模板 → 角色管理分配报表权限 → 打开即可
**ControlType 支持**:`AntdUI.Input` / `AntdUI.Select` / `AntdUI.SelectMultiple` / `AntdUI.DatePicker` / `AntdUI.DatePickerYearMonth` / `AntdUI.DatePickerYear`;`F9-*` 业务控件见下方「自定义控件模块」。
---
## 七之二、Web 自定义控件模块(对应 F9 `Page_ReportQueryTool.GetParamControl`)
WinForms 端按 `ControlType` 字符串 if/else 分发创建控件(`GetParamControlOfF9` / `GetParamControlOfAntdUI`),
Web 端同构为**注册中心 + 统一渲染组件**:新增控件只注册一次,查询面板与编辑器下拉同时生效。
| 文件 | 说明 | 对应 WinForms |
|---|---|---|
| `web/src/utils/gridreport/controls/registry.js` | 注册中心:`registerControl / getControlDef / groupedControlOptions / resolveControlDef / fallbackDef` + 取值工具(`toOptions / apiOptions / registryOptions / pickRows`) | `_paramGetValudFuncs` 注册表 |
| `web/src/utils/gridreport/controls/antdControls.js` | AntdUI 基础控件(一个 type 一个函数 + `ANTD_BUILDERS` 分发) | `GetParamControlOfAntdUI` |
| `web/src/utils/gridreport/controls/f9Controls.js` | F9 业务控件(一个 type 一个函数 + `F9_BUILDERS` 分发) | `GetParamControlOfF9` |
| `web/src/utils/gridreport/controls/index.js` | 装配入口:注册 AntdUI + F9 两组控件;`parseStaticDs / fetchServiceDs / loadParamOptions` | `GetParamControl` |
| `web/src/components/report/F9ParamControl.vue` | 统一渲染:input / select / multiselect / date / yearmonth / year / picker(选择页 / 远程搜索 / 降级输入) | 控件实例 |
### 选择页(对应 F9.Common.Report.SelectPages)
| 文件 | 说明 |
|---|---|
| `web/src/components/select/F9SelectPage.vue` | 通用选择页弹窗:过滤条件 + 关键字 + 分页表格 + 单选/多选 + 校验(对应 `F9ModalPageUserControl_Medium` + `f9AntdUITable` 的 `QueryData` / `ValidCustom`) |
| `web/src/components/select/CraftZhuangKouSelect.vue` | 工艺庄口选择页(对应 `Page_CraftZhuangKou_SelectV2`:庄口编号/名称/产地/排序/备注/年度,年度过滤,数据 `GET /api/craftzhuangkou`) |
| `web/src/components/select/index.js` | 选择页注册中心:`registerSelectPage / getSelectPage` |
控件绑定选择页:控件定义里写 `page: 'CraftZhuangKou'`,`F9ParamControl` 即打开该页(优先级:选择页 > 内建远程搜索 > 降级文本输入)。
业务页面直接复用:`<CraftZhuangKouSelect v-model="visible" @confirm="onPicked" />`(**落丝记录 lsjl 的庄口已改为打开选择页**)。
控件定义字段:`{ type, kind, label, group, hint, useDs, options, loader(prm), search(kw), columns, valueField, labelField, multiple }`
### kind 与 F9 的映射
| Web kind | 渲染 | F9 对应 | 取值 |
|---|---|---|---|
| `input` | el-input | `AntdUI.Input` | 文本 |
| `select` | el-select | `AntdUI.Select` / 各 `F9-*-SELECT` | key(部门为 Id) |
| `multiselect` | el-select multiple | `AntdUI.SelectMultiple` / `F9-WMS-SS-PICI-KCSTATUS` | `"a,b,c"`(对齐 `string.Join(",")`) |
| `date` / `yearmonth` / `year` | el-date-picker | `AntdUI.DatePicker*` | `YYYY-MM-DD` / `YYYY-MM` / `YYYY` |
| `picker` | 只读框 + 选择按钮 → 弹窗表格远程搜索 | `ShowModalPage(Page_XXX_Select)` | 记录 **Id**(与 F9 一致) |
| `picker-multiple` | 同上多选 | `F9-GYZK-SELECT-MUTIPLE` | `"1,2,3"` |
选项优先级:`Ds_Static` → `Ds_Service` → 控件 `loader` → 静态 `options`;picker 无 `search` 实现时自动降级为文本输入。
### 部门控件:Web 版 F9GlobalUIHelper
WinForms 端部门下拉不是一把全量,而是**按口径取数 + ParentId 建树**(`F9GlobalUIHelper` + 各 `GetDepartment*Query`)。
Web 端同构实现:`web/src/utils/departments.js`
| F9 方法 / 查询 | Web 实现 |
|---|---|
| `Department_GetAllCacheAsync` | `getAllDeps()`(缓存,`GET /api/sys/department`) |
| `GetDepartmentByDutyTypeWithoutRightQuery(types)` | `depsByDuty(deps, types)`(只保留自身 DutyType 命中) |
| `GetDepartmentOfSiyeQuery(includeSiye)` | `depsOfSiye(deps, true)`(丝业中心 + 全部子孙) |
| `GetDepartmentForEmployee/Kaoqin/XinziQuery` | `depsOfLoginUser()`(本部门 + 子孙;管理员=全部)+ `removeCompany()` |
| `Dep_GenerateSelectItems` | `buildDepTree()`(SortOrder → Code → Name,父不在集合内即为根) |
| `Dep_Select_SelectDep(sel, depId)` | `defaultFromUser: true` → 默认选中登录用户部门 |
| 控件 | 口径 | 实测结果 |
|---|---|---|
| `F9-DEP-QS-SELECT` | DutyType=前缫(25) | 前缫车间#26 → 前缫一~四车间#31-34 |
| `F9-DEPARTMENT_SIYE` | 丝业 | 丝业生产中心#2 + 全部子孙 |
| `F9-DEP-EMPLOYEE-SELECT` / `F9-DEP-XZ-SELECT` / `F9-DEPARTMENT_KAOQIN` | 职工 / 薪资 / 考勤口径(默认登录部门) | 管理员=全部,否则本部门+子孙 |
| `F9-DEP-ALL-SELECT`(新增) | 全部 | 完整部门树 |
渲染:控件定义 `tree: true` → `F9ParamControl` 用 `el-tree-select`(层级展示,取值仍为部门 Id)。
落丝记录(lsjl)的车间下拉也改为 `depOptions({ duty: [DUTY.前缫] }, { flat: true })`,替换掉原来的 `name.includes('前缫')`。
### 已内置的 F9 控件
- 庄口:`F9-GYZK-SELECT`(**打开工艺庄口选择页**)、`F9-GYZK-SELECT-MUTIPLE`(多选,取值 `"1,2,3"`)
- 部门/职工:`F9-DEP-QS-SELECT`、`F9-DEPARTMENT_SIYE`、`F9-DEP-EMPLOYEE-SELECT`、`F9-DEP-XZ-SELECT`、`F9-DEPARTMENT_KAOQIN`、`F9-EMPLOYEE-SELECT`(picker)、`F9-EMPLOYEE-WORKSTATUS`、`F9-EMPLOYEE-CHANGE_TYPE_SELECT`、`F9-Employee-isXuegong`
- 物资/原料:`F9-WZ-SS/FP/ZJP/WJ-SELECT`(picker)、`F9-YLZK-SELECT`(picker)、`F9-WMS-YL-CD/JIBIE/CANPINZHONG`(注册表)
- 生产/质量:`F9-YEXIAN-SOURCE-SELECT`、`F9-GUIGE-SELECT`、`F9-PROD-QS-LSCD-YICHANG`、`F9-PROD-QS-ZHJJ-COUNT`、`F9-QC-YJSS-YLXY/ZHY-SELECT`(降级输入)
- 仓储:`F9-WMS-CRKTYPE`、`F9-WMS-SS-PICI-KCSTATUS`(多选)、`F9-WMS-SS-PICI-SSTYPE`、`F9-WMS-YL-RK/CK-TYPE-SELECT`
Web 端无对应枚举/接口的走注册表或 `Ds_Static`,并在编辑器/查询面板显示降级提示。
### 扩展新控件(两步,无需改页面)
```js
import { registerControl, KIND } from '@/utils/gridreport/controls';
registerControl({
type: 'F9-MY-SELECT', kind: KIND.SELECT, group: 'F9 自定义', label: '我的选择',
loader: async () => [{ key: '1', value: '选项一' }],
});
// 弹窗选择类:再给 search / columns / valueField / labelField
```
报表管理 → 参数 → 「控件类型」下拉自动按 group 分组列出所有已注册控件。
---
## 八、当前状态
| 项 | 状态 |
|---|---|
| 后端编译 | 0 错误 0 警告 ✅ |
| 后端服务 | 运行中 `http://localhost:3000` ✅ |
| 前端服务 | 运行中 `http://localhost:5173` ✅ |
| 报表中心页面 | 正常(分类树 + 查询面板 + 多 Tab 预览)✅ |
| 报表管理 + 全屏编辑器 | 正常(配置/设计/源码/预览)✅ |
| 职工花名册示例 | 942 条职工数据 + 报表参数正常渲染 ✅ |
| 本机设计器协议 | 已注册,下载→编辑→自动回传全链路验证通过 ✅ |
| 角色授权(报表权限 tab) | 已实现 ✅ |
### 启动方式
```powershell
# 后端
cd backend/F9WebAiTest.Api
dotnet run
# 前端
cd web
npm run dev
```
默认账号:`admin / 123456`
### 备注
- 日志目录 `.run-logs/`(backend.log、frontend.log、start-web.bat)
- 协议脚本日志:`%TEMP%\F9ReportDesigner\handler.log`