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

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

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

20 KiB
Raw Permalink Blame History

工作进度-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 格式约定(已验证)

// 单数据集
{ "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:
    {
      "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,并在编辑器/查询面板显示降级提示。

扩展新控件(两步,无需改页面)

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) 已实现 ✅

启动方式

# 后端
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