质量模块: - 11 张工艺执行书表:封面、试产、选剥茧(含混茧明细)、试缫与大样、煮茧(含分车间配温、副煮车间)、自动缫(含大样)、后缫 - 10 张样茧试缫表:原料小样/综合样主表 + 选茧、解舒、茧幅、切剖、清洁洁净、茧丝百回纤度 - 服务与接口、主控指针回写(列表 √ 标记)、封面编号自增、报表数据包 - 前端 8 张报表打印版式、7 类工艺单据与 6 类调查录入弹窗、字段驱动表单/表格组件 - 菜单种子新增「质量」目录,样茧子表增加 YjssType 判别列,避免原料小样与综合样自增 Id 重号串数据 报表中心: - 报表模板管理、锐浪 grhtml5 加载与预览、全屏设计面板、数据源 Provider(落丝记录、职工花名册) 其他:同步工作进度 1-11 文档、开发数据库快照、.gitignore 忽略运行日志目录
20 KiB
工作进度-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)中嵌入报表系统:
- 报表中心作为菜单页面,支持报表的新增 / 删除 / 修改
- 每张报表有独立权限,管理员需在「角色管理 → 权限分配」中给角色授权后,用户才能看到
- 报表支持 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(报表管理),自动授权 adminSeedSampleReport():示例报表「职工花名册」(codeemployee_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月", "车间": "前缫" } ] }
七、新增报表的标准流程
- 建 CQRS:
XXXRepQuery+ Handler,Handler 内注入业务读仓储查询,组装ReportDto_DataSourceField_Data:每行匿名对象的属性名 = 模板字段名Parameter_Data:账期 / 车间 / 查询时间等报表参数
- 建 Provider:
RepDt_XXX : IReportDataProvider,ReportName常量 + 解析pairs→mediator.Send(query)(DI 自动发现,无需注册) - 报表管理 → 新增报表,填
Config:{ "DsName": "RepDt_XXX", "ViewerType": 0, "Parms": [ { "Name": "q_yearmonth", "Label": "账期", "Prompt": "", "IsRequired": false, "ControlType": "AntdUI.DatePickerYearMonth", "Ds_Static": null, "Ds_Service": null, "DefaultValue": "默认值" } ] } - 用设计器做
.grf(记录集字段对齐Field_Data;显示查询条件的控件设"Parameter":"账期",模板顶层声明Parameter节点) - 上传模板 → 角色管理分配报表权限 → 打开即可
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