zzw
3 天以前 7858c25faa53dd2f4c0faae58618565ac5bb50ae
feat: add engineering order print and externalize app config

- Add engineering order print (and print-2) that renders selected faults
into a Word template with four copies per logical page and no single
record split across pages; register the menu and button permissions
- Move frontend port and API target/base URL into .env, read them in
vue.config.js and the axios client
- Abstract approval roles in menus and UI copy to the first through
fourth approver so the sensitive-word replacer can substitute the real
role names
- Move the fault-code prefix into config/business-config.yml and import
it optionally from application.yml
- Update the sensitive-word dictionary and requirement docs
3个文件已添加
1 文件已重命名
23个文件已修改
2246 ■■■■■ 已修改文件
HANDOFF.md 297 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/pom.xml 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DataInitializer.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/business/controller/FaultController.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/business/service/BusinessService.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/business/service/EngineeringOrderPrintService.java 1400 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/system/service/ButtonPermissionCatalog.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/resources/application.yml 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/.env 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/public/dict.js 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/public/sensitive-words.json 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/api/index.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/router/index.js 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/utils/dict.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/Dashboard.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/fault/DispatchPage.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/fault/EngineeringPrint.vue 272 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/fault/SimpleApproval.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/fault/XcbReview.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/vue.config.js 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
requirement/交接文件_2026.10.3.md 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
requirement/决策表_待确认项.md 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
requirement/前44页需求梳理.md 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
requirement/工程状态机设计.md 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
requirement/改进2026.10.txt 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
requirement/敏感词字典.md 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
requirement/界面还原.html 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HANDOFF.md
@@ -404,12 +404,12 @@
- æœ¬ä¼šè¯è¿›å±•:用户给出 **AXU1 æµç¨‹æ€»è§ˆçš„口径变更**——第一版**不按原始需求**,改为 **① ç®€åŒ–业务流程 + â‘¡ åªæä¾›ç½‘络版、不提供离线单机版**。
- **第一版简化流程(10 æ­¥ï¼Œå·²ç¡®è®¤ï¼›2026-09-20 ç»†åŒ–验收链)**:T员故障上报 â†’ XCB助理审核(**分配承修单位、拆单、生成工程单号**)→ CMB领导审批 â†’ JCC助理审批 â†’ JCC处长审批 â†’ XCB助理派单(**显示未派单故障,勾选后一键派单,按承修单位生成派工单**)→ åŽ‚å®¶çº¿ä¸‹ä¿®ç† â†’ åŽ‚å®¶ä¸ŠæŠ¥å®Œå·¥ä¿¡æ¯ â†’ å®Œå·¥éªŒæ”¶æµç¨‹ï¼ˆéªŒæ”¶ç”³è¯· â†’ è´¨æ£€å‘˜éªŒæ”¶ â†’ è´¨æ£€è´Ÿè´£äººéªŒæ”¶ â†’ å®Œå·¥éªŒæ”¶æ‰“印输出 â†’ ä¿®åŽè´¨é‡è¯„级)→ éªŒæ”¶å®Œæˆã€‚
- è¿žå¸¦å£å¾„变更:① å–消艇长审批环节(原待确认清单第 9 æ¡ â†’ å·²è§£å†³ï¼šä¸çº³å…¥ç¬¬ä¸€ç‰ˆï¼‰ï¼›â‘¡ å–消全部离线能力(单机版/离线填写、数据包导入导出);③ å–消“XCB助理完工处理/导出完工项目”环节;④ æŠ¥ä»·å¤„理移出主流程;⑤ JCC助理审批确认为必经环节。
- è¿žå¸¦å£å¾„变更:① å–消T长审批环节(原待确认清单第 9 æ¡ â†’ å·²è§£å†³ï¼šä¸çº³å…¥ç¬¬ä¸€ç‰ˆï¼‰ï¼›â‘¡ å–消全部离线能力(单机版/离线填写、数据包导入导出);③ å–消“XCB助理完工处理/导出完工项目”环节;④ æŠ¥ä»·å¤„理移出主流程;⑤ JCC助理审批确认为必经环节。
- å·²åŒæ­¥è½ç›˜ï¼š`requirement\界面还原.html`(新增全局口径条 + AXU1 å¡ç‰‡é‡å†™ + AXU2/AXU13 å—影响标注)、`requirement\前44页需求梳理.md`(第 2 èŠ‚ä¸šåŠ¡æ€»è§ˆã€3.3 æµç¨‹æ€»è§ˆã€4.1/6.2/6.3/第 7 èŠ‚æ ‡æ³¨ã€ç¬¬ 9 èŠ‚æ–°å¢ž 14–17 æ¡ã€æœ¯è¯­è¡¨ï¼‰ã€`requirement\工程状态机设计.md`(审批顺序、任务状态机、流转表/矩阵、新增待确认 11–12)、`requirement\决策表_待确认项.md`(全局口径、AXU1 è¡Œå·²æ ¸å¯¹ã€C-8 å·²è§£å†³ã€æ–°å¢ž C-12–C-15)。
- å½“时待办的后续处理:C-12/C-13/C-14/C-15 å‡å·²è§£å†³ï¼›AXU3/AXU4 å·²äºŽ 2026-09-16 æ ¸å¯¹å®Œæˆï¼›A ç»„下一步为 AXU5,B ç»„工程状态机 10 é¡¹ä»å¾…答。
**2026-09-14 ç”¨æˆ·ç­”复(已回填全部文档)**:
- â‘  è‰‡é•¿çŽ¯èŠ‚ä¸çº³å…¥ç¬¬ä¸€ç‰ˆï¼ˆç¡®è®¤ï¼‰ï¼›â‘¡ æŠ¥ä»·å¤„理移出主流程,“报价管理”菜单与模块整体不做(C-14 å·²è§£å†³ï¼‰ï¼›â‘¢ JCC åŠ©ç†å®¡æ‰¹ä¸ºå¿…ç»çŽ¯èŠ‚ï¼ˆç¡®è®¤ï¼‰ï¼›â‘£ C-12 ç¦»çº¿/数据包相关功能**整体取消,对应菜单项一并删除**(数据包导入、完工信息导入、导出待完工数据包、报价导出/导入);⑤ AXU2 æ•°æ®åŒ…导入:只做网络版、无导入功能,**无需核对**(页面与菜单项删除)。
- â‘  T长环节不纳入第一版(确认);② æŠ¥ä»·å¤„理移出主流程,“报价管理”菜单与模块整体不做(C-14 å·²è§£å†³ï¼‰ï¼›â‘¢ JCC åŠ©ç†å®¡æ‰¹ä¸ºå¿…ç»çŽ¯èŠ‚ï¼ˆç¡®è®¤ï¼‰ï¼›â‘£ C-12 ç¦»çº¿/数据包相关功能**整体取消,对应菜单项一并删除**(数据包导入、完工信息导入、导出待完工数据包、报价导出/导入);⑤ AXU2 æ•°æ®åŒ…导入:只做网络版、无导入功能,**无需核对**(页面与菜单项删除)。
- å·²è§£å†³ï¼š**C-13 æ´¾å•口径**(2026-09-16 ç¡®è®¤ï¼‰â€”—XCB助理审核仍负责分配承修单位、拆单、生成工程单号;后续派单页按故障选择、按承修单位生成派工单,不再重新分配承修单位或修改厂家。剩余:B ç»„工程状态机 10 é¡¹ï¼›A ç»„下一步 AXU5 CMB领导审批。(C-15 æœ¯è¯­å·²äºŽ 2026-09-14 ç­”复:统一写“T员验收”)
- åŽç»­è¿›å±•:A ç»„已于 2026-09-16 å®Œæˆ AXU3/AXU4,下一步为 AXU5 CMB领导审批(AXU0/AXU1/AXU15 å·²å®Œæˆï¼ŒAXU2 å…æ ¸å¯¹ï¼‰ã€‚
@@ -469,7 +469,7 @@
当前纳入模块:XCB助理派单(AXU10)→ å®Œå·¥æƒ…况处理(AXU14)→ å®Œå·¥ä¿¡æ¯ä¸ŠæŠ¥ï¼ˆAXU15)→ å®Œå·¥éªŒæ”¶ï¼ˆéªŒæ”¶ç”³è¯· â†’ è´¨æ£€å‘˜éªŒæ”¶ â†’ è´¨æ£€è´Ÿè´£äººéªŒæ”¶ â†’ å®Œå·¥éªŒæ”¶æ‰“印输出 â†’ ä¿®åŽè´¨é‡è¯„级)→ ç³»ç»Ÿç®¡ç† â†’ ç»Ÿè®¡æŠ¥è¡¨ã€‚原临抢修工程管理中的 CMB领导已阅(AXU9)、已派工程(AXU11)、已派工程单(AXU12)、工程单打印输出(AXU13)以及技术文件管理(AXU16)暂不处理;监测信息、查询、基础信息和技术文件管理不再主动展开,除非用户后续补充需求。
原始需求流程(存档,已不采用):故障上报(数据包导入)→ è‰‡é•¿ï¼ˆæ‰¹é‡/单项)→ å¤§é˜ŸåŠ©ç†ï¼ˆXCB)审批 â†’ CMB领导审批 â†’ JCC助理审批 â†’ JCC处长审批 â†’ â€¦â€¦ â†’ å®Œå·¥ä¿¡æ¯ä¸ŠæŠ¥ â†’ éªŒæ”¶ï¼›å«å•机版/离线数据包与报价导出导入环节。
原始需求流程(存档,已不采用):故障上报(数据包导入)→ T长(批量/单项)→ å¤§é˜ŸåŠ©ç†ï¼ˆXCB)审批 â†’ CMB领导审批 â†’ JCC助理审批 â†’ JCC处长审批 â†’ â€¦â€¦ â†’ å®Œå·¥ä¿¡æ¯ä¸ŠæŠ¥ â†’ éªŒæ”¶ï¼›å«å•机版/离线数据包与报价导出导入环节。
关键规则(来自截图备注):
- æ´¾å•规则(2026-09-20 ç¡®è®¤ï¼‰ï¼šJCC处长审批完成后进入XCB助理派单页面,显示“未派单”的故障一览;勾选一个或多个故障后点击“一键派单”,系统按每个故障的承修单位生成派工单。一个故障有多个承修单位时,每个承修单位各生成一张派工单。
@@ -517,7 +517,7 @@
6. ã€å·²è§£å†³ 2026-09-20】“键派单”确认应为“一键派单”;修改、生成临修工程单、删除亚复(疑为清除重复)等原截图业务按钮暂不需要。
7. æ— æˆªå›¾æ¨¡å—页面设计:监测信息、查询(工程单/工程项目/故障信息/销项)、报价管理(导出待报价/修船办初审/JCC审核)、基础信息(JC信息/用户数据范围);系统管理菜单已确认包含承修单位管理、产品结构树管理、设备基础库管理;完工验收5菜单已确认,字段与页面细节仍待设计。
8. ã€å·²è§£å†³ã€‘首页统计卡片完整清单(2026-09-10):实为“按X号统计表”表头 11 åˆ—(X号/当前状态/停靠泊位/TD/工程总数/销项总数/重大故障/未完工/完工待试验/已完工/待处理),非散列卡片;“JC信息”=舰船信息(2026-08-26)、TD=艇队(2026-09-10)均已确认。
9. ã€å·²è§£å†³ 2026-09-11】艇长环节不纳入第一版(简化流程从 T员故障上报直接进入 XCB助理审核)。
9. ã€å·²è§£å†³ 2026-09-11】T长环节不纳入第一版(简化流程从 T员故障上报直接进入 XCB助理审核)。
10. å­—典数据:故障影响(示例“供气”)、故障性质(示例“重大”)已确认按可配置字典字段处理,其他取值待补;X号“当前状态”其他取值待补。
11. æœ¯è¯­å‹˜è¯¯ï¼ˆOCR):白修=自修、T员队验收=T员验收(2026-09-14 ç¡®è®¤ç»Ÿä¸€å†™â€œT员验收”);H ä¸ºæ•æ„Ÿè¯ä»£ç§°ï¼Œéžå¯†ææ–™ç»Ÿä¸€å†™â€œæ˜¯å¦æ¶‰H”(2026-09-16)。
12. ã€å¾…办·提醒用户】器材系统对接文档:用户已有文档,需要时向用户索取(器材目录表结构、器材ID、接口方式等)。
@@ -593,7 +593,7 @@
## 11. æ”¶å°¾å¿«ç…§ï¼ˆ2026-09-14 å‘¨ä¸€æ™šï¼Œç”¨æˆ·å…³æœºå‰ï¼‰
- **本会话(2026-09-11 æ™š / 2026-09-14)结论已全部落盘**:`requirement\界面还原.html`、`requirement\前44页需求梳理.md`、`requirement\工程状态机设计.md`、`requirement\决策表_待确认项.md`、`requirement\敏感词字典.md` + æœ¬æ–‡ä»¶ï¼›æ— ä¸´æ—¶æ–‡ä»¶æ®‹ç•™ï¼ˆå·²æ ¸éªŒï¼‰ï¼Œæ— è¿›è¡Œä¸­çš„æœªä¿å­˜å·¥ä½œã€‚
- **本次已解决的待确认项**:C-8(艇长环节不纳入第一版)、C-12(离线/数据包功能整体取消 + å¯¹åº”菜单项删除)、C-14(报价管理菜单与模块整体不做)、C-15(术语统一写“T员验收”);AXU2 æ•°æ®åŒ…导入免核对(页面取消)。
- **本次已解决的待确认项**:C-8(T长环节不纳入第一版)、C-12(离线/数据包功能整体取消 + å¯¹åº”菜单项删除)、C-14(报价管理菜单与模块整体不做)、C-15(术语统一写“T员验收”);AXU2 æ•°æ®åŒ…导入免核对(页面取消)。
- **仍未答复(下次开工从这里接)**:
  1. C-13 æ´¾å•口径已于 2026-09-16 è§£å†³ï¼šXCB助理审核负责分配承修单位、拆单、生成工程单号;派单页按故障选择、按承修单位生成派工单。
  2. AXU3/AXU4 å·²äºŽ 2026-09-16 å®Œæˆï¼›A ç»„下一步为 AXU5 CMB领导审批。
@@ -830,7 +830,7 @@
## 27. æ”¶å°¾å¿«ç…§ï¼ˆ2026-10-07 å‘¨ä¸‰ï¼ŒCMB å®¡æ‰¹åŽ JCC åŠ©ç†åˆ—è¡¨æœªåˆ·æ–°ï¼‰
**问题与根因**
- `rk_CMB领导审批` é€šè¿‡åŽè¿›å…¥ `rk_JCC助理审批`,页面可能不显示已进入 `CMB_APPROVED` çŠ¶æ€çš„æ•°æ®ã€‚
- `rk_第二审批人审批` é€šè¿‡åŽè¿›å…¥ `rk_第三审批人审批`,页面可能不显示已进入 `CMB_APPROVED` çŠ¶æ€çš„æ•°æ®ã€‚
- CMB、JCC åŠ©ç†ã€JCC å¤„长三个审批路由共用 `SimpleApproval.vue`,并共用同一个 keep-alive ç¼“存名 `SimpleApproval`。路由切换时组件实例被复用,但原代码只在 `created()` æŸ¥è¯¢ä¸€æ¬¡ï¼Œæœªåœ¨ `stage` å˜åŒ–时重新加载。
**修复内容**
@@ -1040,3 +1040,288 @@
- å¼€å‘环境敏感词配置会把 `rk_T` æ˜¾ç¤ºä¸ºâ€œæ¢¯â€ï¼Œå› æ­¤é¡µé¢æˆªå›¾ä¸­å¯èƒ½æ˜¾ç¤ºâ€œè´Ÿè´£æ¢¯â€ï¼›ä»£ç å’ŒåŽç«¯å­—段仍是 `rk_T`。
- ä¸è¦æäº¤ `.codex-run`、Office é”æ–‡ä»¶æˆ– `emergency-repair-web/public/新建 æ–‡æœ¬æ–‡æ¡£.txt`。
- æœ¬è½®ä»£ç ã€éœ€æ±‚文件和交接记录一并提交并推送公司 Git ä»“库 `main` åˆ†æ”¯ã€‚
## 27. æ”¶å°¾å¿«ç…§ï¼ˆ2026-10-09 å‘¨äº”,需求 32 å·¥ç¨‹å•打印修正与验证)
**本轮修正内容**
- å·¥ç¨‹å• Word æ¨¡æ¿çš„æœ€å³åˆ—独立为 `UnitNo` æ ï¼Œåˆ—宽 `400` twips;该列跨联头、表头和数据行纵向合并,单元格文字方向为 `btLr`(从下到上竖排)。
- ç§»é™¤è”头中原来的“第{unitNo}联”,改为右侧 `UnitNo:{unitNo}`,四联依次替换为 `1/2/3/4`。
- è”头补回 `ZB型号:{zbModel}`。渲染时按故障 `xNo` å®šä½äº§å“ç»“构树 `side/SIDE` èŠ‚ç‚¹ï¼Œå–å…¶çˆ¶èŠ‚ç‚¹åç§°ä½œä¸º `zbModel`。
- å¢žåŠ å››è”åç§°é…ç½®ï¼šæ¯è”å¯åœ¨åŽå°ä¸šåŠ¡é…ç½®ä¸­å•ç‹¬è®¾ç½®åç§°ï¼ŒWord é¡¶éƒ¨æŒ‰ `第1联    ä¸ŠæŠ¥äºº` çš„æ ¼å¼è¾“出。
- æ–°å»ºç‹¬ç«‹ä¸šåŠ¡é…ç½®æ–‡ä»¶ `emergency-repair-server/config/business-config.yml`,把 `fault-code.prefix`、工程单模板路径和四联名称集中维护,避免每次改业务参数都动主 `application.yml`。
- é»˜è®¤æ¨¡æ¿ç‰ˆæœ¬å‡çº§ä¸º `engineering-order-template-v3`;启动时若模板缺少版本标记、`{zbModel}` æˆ– `{unitDisplay}`,会自动生成新版模板。运营人员仍可在 `emergency-repair-server/config/engineering-order-template.docx` ä¸Šç”¨ Word è°ƒæ•´ç‰ˆå¼ã€‚
- ä¸» `application.yml` é€šè¿‡ `spring.config.import` å¼•入独立业务配置;配置文件缺失时不影响程序启动。
**主要文件**
- `emergency-repair-server/config/business-config.yml`
- `emergency-repair-server/src/main/java/com/emergencyrepair/business/service/EngineeringOrderPrintService.java`
- `emergency-repair-server/src/main/java/com/emergencyrepair/business/service/BusinessService.java`
- `emergency-repair-server/src/main/resources/application.yml`
- `emergency-repair-server/config/engineering-order-template.docx`
- `emergency-repair-web/src/views/fault/EngineeringPrint.vue`
**验证结果**
- å°†å››è”名称临时改为 `1=上报人`、`2=审核人` åŽé‡å¯ï¼Œç”Ÿæˆ Word å®žé™…输出 `第1联    ä¸ŠæŠ¥äºº | ç¬¬2联    å®¡æ ¸äºº | ç¬¬3联 | ç¬¬4联`,确认外部业务配置文件已被读取;恢复 `2` ä¸ºç©ºåŽå†æ¬¡éªŒè¯è¾“出为 `第1联    ä¸ŠæŠ¥äºº | ç¬¬2联 | ç¬¬3联 | ç¬¬4联`。
- ä½¿ç”¨ `admin / admin123` ç™»å½•成功,查询到 6 æ¡æ•…障并调用 `POST /api/faults/engineering-print`,实际下载生成 Word。
- ç”Ÿæˆæ–‡æ¡£è§£æžç»“果:4 ä¸ªè¡¨æ ¼ï¼ˆå››è”)、3 ä¸ªåˆ†é¡µç¬¦ï¼›æ¯è” 8 è¡Œï¼Œå…¶ä¸­ 6 è¡Œæ•°æ®ã€‚
- å››è”右侧 `UnitNo` åˆ†åˆ«ä¸º `1/2/3/4`;最右单元格具备 `vMerge`、`textDirection=btLr`、`tcW=400`。
- æ–‡æ¡£ä¸­ `{zbModel}`、`{unitNo}`、`{unitName}`、`{unitDisplay}` æ®‹ç•™æ•°é‡å‡ä¸º 0。
- å½“前产品树 `201` çš„ `side` èŠ‚ç‚¹çˆ¶èŠ‚ç‚¹ä¸º `型号s44444`;生成联头实际为 `X号:201 ZB型号:型号s44444 专业:动力专业…`。
- å››è”顶部标题实际为 `第1联    ä¸ŠæŠ¥äºº`、`第2联`、`第3联`、`第4联`。
- æœ¬è½®åŽç«¯é‡æ–°æ‰“包并启动成功;前端构建此前已通过,仅有既有 webpack åŒ…体积告警。
**当前运行状态(2026-10-09)**
- å‰ç«¯ `http://localhost:8082`,PID `144352`;后端 `http://127.0.0.1:8091`,PID `218764`。
- DM8 `127.0.0.1:5236`;登录 `admin / admin123`;业务 schema `LX`。
- åŽç«¯æ—¥å¿—:`.codex-run/backend-20261009-business-config-v3.out.log`、`.codex-run/backend-20261009-business-config-v3.err.log`。
- æœ¬è½®é‡æ–°æ‰“包并重启后,磁盘模板已确认升级为 `engineering-order-template-v3`;独立业务配置已生效。
**接手注意**
- ä¸è¦æäº¤ `.codex-run`、`engineering-order-verify.docx`、Office é”æ–‡ä»¶ `config/~$gineering-order-template.docx` æˆ– `emergency-repair-web/public/新建 æ–‡æœ¬æ–‡æ¡£.txt`。
- `engineering-order-template.docx` å·²æ˜¯ v3;如 WPS æ­£åœ¨å ç”¨è¯¥æ–‡ä»¶ï¼ŒåŽç«¯å¯åŠ¨æ—¶ä¸ä¼šè¦†ç›–ï¼Œæ‰“å°æœåŠ¡ä¼šä½¿ç”¨å†…å­˜ä¸­çš„ v3 é»˜è®¤æ¨¡æ¿ï¼Œå…³é—­ WPS åŽé‡å¯å³å¯æ¢å¤ç£ç›˜æ¨¡æ¿è‡ªåŠ¨å‡çº§ã€‚
- è‹¥åŽç»­ä¿®æ”¹è”头或表格列,模板版本标记仍需保留;需要强制升级模板时再提升 `TEMPLATE_VERSION`。
## 29. ä¿®æ­£å¿«ç…§ï¼ˆ2026-10-09 å‘¨äº”,工程单四联名称位置修正)
**用户澄清**
- å››è”名称不能放在 Word é¡¶éƒ¨ä½œä¸ºæ ‡é¢˜ï¼Œå¿…须写在原来 `UnitNo` çš„æœ€å³åˆ—竖排位置,并用它替换原来的 `UnitNo` æ–‡æœ¬ã€‚
**本轮修正**
- é»˜è®¤æ¨¡æ¿ç‰ˆæœ¬ç”± `engineering-order-template-v3` å‡çº§ä¸º `engineering-order-template-v4`。
- æœ€å³åˆ—单元格占位符由 `UnitNo:{unitNo}` æ”¹ä¸º `{unitDisplay}`,继续保留 `vMerge`、`btLr` ç«–排和 `400` twips åˆ—宽。
- ç§»é™¤è¡¨æ ¼ä¸Šæ–¹çš„ `{unitDisplay}` æ ‡é¢˜æ®µè½ï¼›è¾“出后四联右侧依此显示 `第1联    ä¸ŠæŠ¥äºº`、`第2联`、`第3联`、`第4联`。
- `config/business-config.yml` çš„ `unit-names` é…ç½®æ–¹å¼ä¸å˜ï¼ŒåŽç»­åªéœ€è°ƒæ•´å„联名称。
**验证结果**
- åŽç«¯ `mvn -B -DskipTests compile` ä¸Ž `mvn -B -DskipTests package` é€šè¿‡ã€‚
- é‡å¯åŽç£ç›˜æ¨¡æ¿å·²å‡çº§ä¸º `engineering-order-template-v4`,模板正文中已无 `UnitNo`,`{unitDisplay}` ä½äºŽæœ€å³ç«–排单元格。
- å®žé™…调用 `POST /api/faults/engineering-print` ç”Ÿæˆ Word å¹¶è§£æžï¼š4 ä¸ªè¡¨æ ¼ã€3 ä¸ªåˆ†é¡µç¬¦ï¼Œ`UnitNo` æ®‹ç•™ `0`,`第1联    ä¸ŠæŠ¥äºº` ä½äºŽæœ€å³ç«–排单元格,其他三联分别为 `第2联`、`第3联`、`第4联`。
- éªŒè¯æ–‡ä»¶ï¼š`.codex-run/engineering-order-unit-display-right.docx`。
**当前运行状态(2026-10-09)**
- å‰ç«¯ `http://localhost:8082`,PID `144352`;后端 `http://127.0.0.1:8091`,PID `216492`。
- åŽç«¯æ—¥å¿—:`.codex-run/backend-20261009-unit-display-right.out.log`、`.codex-run/backend-20261009-unit-display-right.err.log`。
## 30. ä¿®æ­£å¿«ç…§ï¼ˆ2026-10-09 å‘¨äº”,工程单模板原位填充并保留 Word æ ·å¼ï¼‰
**用户澄清**
- å·¥ç¨‹å•打印不应重新构造一份空白 Word,而应打开现有 Word æ¨¡æ¿ï¼ŒæŒ‰æ¨¡æ¿ä¸­çš„ `{}` æ ‡è¯†å¡«å†™æ•°æ®ï¼Œå¹¶ä¿ç•™æ¨¡æ¿åŽŸæœ‰æ ·å¼ã€‚
**本轮修正**
- `EngineeringOrderPrintService` æ”¹ä¸ºå…ˆè¯»å– `config/engineering-order-template.docx`,在模板文档对象上复制正文并填充标记,不再通过 `new XWPFDocument()` åˆ›å»ºç©ºç™½ç›®æ ‡æ–‡æ¡£ã€‚
- ä¿ç•™æ¨¡æ¿åŒ…内的 `styles.xml`、页眉、页脚、主题、图片等资源;正文段落和表格的段落样式、字体、字号、加粗、边框、列宽等随模板副本保留。
- æ ‡è®°æ›¿æ¢åŒæ—¶æ”¯æŒ `{field}` å’Œ `{{ field }}`,并支持标记跨多个 Word run çš„æƒ…况;替换值优先写入标记起始 run,以保留该处原有字体格式。
- `ensureTemplate()` ä»…在模板文件不存在时生成默认模板;已有模板不再因版本标记或占位符检查被自动覆盖,便于运营人员直接用 Word ç»´æŠ¤ç‰ˆå¼ã€‚
- `openTemplate()` å§‹ç»ˆä¼˜å…ˆä½¿ç”¨ç£ç›˜æ¨¡æ¿ï¼Œç£ç›˜æ¨¡æ¿ç¼ºå¤±æ—¶æ‰å›žé€€åˆ°å†…存默认模板。
**主要文件**
- `emergency-repair-server/src/main/java/com/emergencyrepair/business/service/EngineeringOrderPrintService.java`
- `emergency-repair-server/config/engineering-order-template.docx`
**验证结果**
- åŽç«¯ `mvn -B -DskipTests package` é€šè¿‡ã€‚
- ä½¿ç”¨å½“前默认模板调用 `POST /api/faults/engineering-print`,生成 `.codex-run/engineering-order-template-style-smoke.docx`:占位符残留 `0`、四联头 `4` ä¸ªã€åˆ†é¡µç¬¦ `3` ä¸ªã€‚
- å¦ç”¨å¸¦ `styles.xml`、3 ç»„页眉页脚、`theme1.xml`、`fontTable.xml` çš„ `临修工程完工验收单.docx` ä½œä¸ºæ¨¡æ¿ï¼Œåœ¨ç‹¬ç«‹ `8092` ç«¯å£å¯åŠ¨åŽç«¯ç”Ÿæˆ `.codex-run/engineering-order-styled-template-output.docx`。
- è¾“出中 `styles.xml`、页眉页脚、主题、字体表均存在;经 XML è§„范化对比,样式、页眉、页脚内容与源模板一致。输出中没有 `{}` æˆ– `{{}}` æ®‹ç•™ï¼Œå››è”å’Œ 3 ä¸ªåˆ†é¡µç¬¦æ­£å¸¸ã€‚
- æ ·å¼éªŒè¯æœŸé—´ä¸´æ—¶ `8092` è¿›ç¨‹å·²åœæ­¢ï¼Œä¸»åŽç«¯å’Œå‰ç«¯ä¿æŒè¿è¡Œã€‚
**当前运行状态(2026-10-09)**
- å‰ç«¯ `http://localhost:8082`,PID `144352`;后端 `http://127.0.0.1:8091`,PID `224748`。
- åŽç«¯æ—¥å¿—:`.codex-run/backend-20261009-template-style.out.log`、`.codex-run/backend-20261009-template-style.err.log`。
- ç™»å½• `admin / admin123`;数据库 `SYSDBA/SYSDBA`,业务 schema `LX`。
**接手注意**
- å·²æœ‰ `engineering-order-template.docx` ä¼šæŒ‰åŽŸæ ·ä½¿ç”¨ï¼Œä¸å†è‡ªåŠ¨å‡çº§è¦†ç›–ï¼›å¦‚éœ€é‡å»ºé»˜è®¤æ¨¡æ¿ï¼Œå…ˆå¤‡ä»½å¹¶åˆ é™¤è¯¥æ–‡ä»¶ï¼Œæˆ–åœ¨ä»£ç ä¸­æ˜¾å¼ç”Ÿæˆã€‚
- ä¸è¦æäº¤ `.codex-run`、Office é”æ–‡ä»¶æˆ– `emergency-repair-web/public/新建 æ–‡æœ¬æ–‡æ¡£.txt`。
## 31. ä¿®æ­£å¿«ç…§ï¼ˆ2026-10-09 å‘¨äº”,工程单非明细表重复末行修复)
**问题现象**
- å·¥ç¨‹å•模板包含上方 X å·/型号信息表、中间故障明细表和下方签字表。打印后上方信息表的最后一行被复制成多条,下方签字日期行也被重复增加。
**原因与修正**
- åŽŸé€»è¾‘åªè¦æ¨¡æ¿ä¸­å‡ºçŽ°ä»»æ„ `{}` å ä½ç¬¦ï¼Œå°±æŠŠæ¯ä¸€å¼ è¡¨çš„æœ€åŽä¸€è¡Œéƒ½æŒ‰è®°å½•数展开。
- çŽ°æ”¹ä¸ºæŒ‰è¡¨æ ¼å†…å®¹è¯†åˆ«ï¼šåªæœ‰åŒ…å« `faultCode`、`equipmentName`、`repairRequirement` ç­‰æ˜Žç»†å­—段的表格才展开最后一行;信息表和签字表只替换字段,不复制行。
**验证结果**
- åŽç«¯ `mvn -B -DskipTests package` é€šè¿‡ã€‚
- ä½¿ç”¨å½“前实际模板和 6 æ¡æ•…障数据生成 `.codex-run/engineering-order-template-row-fix.docx`。
- ç¬¬ 1 å¼ ä¿¡æ¯è¡¨å›ºå®š `2` è¡Œï¼Œç¬¬ 2 å¼ æ˜Žç»†è¡¨ä¸º `7` è¡Œï¼ˆè¡¨å¤´ 1 è¡Œ + æ•°æ® 6 è¡Œï¼‰ï¼Œç¬¬ 3 å¼ ç­¾å­—表固定 `3` è¡Œï¼›å››è”均一致。
- è¾“出占位符残留 `0`,分页符 `3` ä¸ªã€‚
**当前运行状态(2026-10-09)**
- å‰ç«¯ `http://localhost:8082`,PID `144352`;后端 `http://127.0.0.1:8091`,PID `225740`。
- åŽç«¯æ—¥å¿—:`.codex-run/backend-20261009-template-table-rows.out.log`、`.codex-run/backend-20261009-template-table-rows.err.log`。
## 32. ä¿®æ­£å¿«ç…§ï¼ˆ2026-10-09 å‘¨äº”,工程单签字表固定一页底部)
**问题现象**
- æ¨¡æ¿æŠŠç­¾å­—表移到页面最下方,中间用 11 ä¸ªç©ºæ®µè½æ’‘高。6 æ¡æ˜Žç»†æ‰“印后,签字表被挤到下一页;四联实际输出为 7 é¡µï¼Œä¸­é—´è¿˜æœ‰ç©ºç™½é¡µã€‚
**原因与修正**
- ç©ºæ®µè½æ’‘高会随明细行数变化,6 è¡Œæ˜Žç»†æ—¶å¿…然溢出;原来四联间使用独立分页段落,在上一页排满时也可能先落到下一页,再产生空白页。
- æ¸²æŸ“时删除明细表与底部签字表之间的纯空段落,以及签字表后的纯空段落。
- å°†æ¨¡æ¿æœ€åŽä¸€å¼ è¡¨è®¾ä¸º `vertAnchor=margin`、`tblpYSpec=bottom`,直接固定到文本区域底部,不再依赖空段落撑高。
- å››è”分页改为在下一联首个正文元素上设置 `w:pageBreakBefore`,不再额外插入独立分页段落,避免空白页。
**验证结果**
- åŽç«¯ `mvn -B -DskipTests package` é€šè¿‡ã€‚
- 6 æ¡è®°å½•生成 `.codex-run/engineering-order-template-one-page.docx`,Word å®žé™…统计 `4` é¡µï¼Œå››è”分别在第 `1/2/3/4` é¡µã€‚
- æ¯è”表格固定为:信息表 `2` è¡Œã€æ˜Žç»†è¡¨ `7` è¡Œã€ç­¾å­—表 `3` è¡Œï¼›ç­¾å­—表纵向位置约 `427.9pt`,位于文本区域底部,未与明细表重叠。
- 2 æ¡è®°å½•生成 `.codex-run/engineering-order-template-two-records.docx`,仍为 `4` é¡µï¼Œç­¾å­—表同样固定在底部。
- è¾“出中 `pageBreakBefore` ä¸º `3` ä¸ªï¼Œå ä½ç¬¦æ®‹ç•™ `0`。
**当前运行状态(2026-10-09)**
- å‰ç«¯ `http://localhost:8082`,PID `144352`;后端 `http://127.0.0.1:8091`,PID `223880`。
- åŽç«¯æ—¥å¿—:`.codex-run/backend-20261009-template-one-page.out.log`、`.codex-run/backend-20261009-template-one-page.err.log`。
## 33. ä¿®æ­£å¿«ç…§ï¼ˆ2026-10-10 æ˜ŸæœŸå…­ï¼Œå·¥ç¨‹å•打印单条记录禁止跨页)
**用户澄清**
- å·¥ç¨‹å•打印不限制每页记录条数,可以多页输出;关键是**一条故障记录不能跨页**,且记录高度随文本多少变化,必须动态计算,不能写死行高或条数。
- ç¬¬ 1 ä¸ªè¡¨æ ¼ï¼ˆX å·/型号信息)与第 3 ä¸ªè¡¨æ ¼ï¼ˆç­¾å­—表)分别固定在每页顶部和底部,由程序动态生成,模板不需要再改位置。
**本轮修正**
- `EngineeringOrderPrintService.configureDetailTablePagination` ç»§ç»­å¯¹æ¯ä¸€è¡Œè®¾ç½® `w:cantSplit`(行不可拆)。
- æ–°å¢ž `configureCellParagraphPagination(row)`:对行内所有单元格段落设置 `w:keepLines`(段落不可拆到两页)。
- ä¸¤è€…配合后,行高仍由 Word æŒ‰å®žé™…文本动态排版,没有固定行高、固定每页条数或字符数阈值。
**主要文件**
- `emergency-repair-server/src/main/java/com/emergencyrepair/business/service/EngineeringOrderPrintService.java`(第 488-514 è¡Œé™„近)
**验证结果**
- åŽç«¯ `mvn -B -DskipTests clean package` é€šè¿‡ï¼ˆJDK `C:\Program Files\Java\jdk1.8.0_172`,Maven `D:\solidDir\apache-maven-3.6.1`)。
- å®žé™…调用 `POST /api/faults/engineering-print`(6 æ¡æ•…障)生成 `.codex-run/engineering-order-dynamic-pagination-2.docx`:`cantSplit` 28 ä¸ªã€`keepLines` 224 ä¸ªã€`tblHeader` 4 ä¸ªã€å››è”分页 `pageBreakBefore` 3 ä¸ªã€å ä½ç¬¦æ®‹ç•™ 0。
- æ­£å¸¸æ•°æ®ç» Word å¯¼å‡º `.codex-run/engineering-order-normal-fresh.pdf` ä¸º 4 é¡µï¼Œä¸€è”一页。
- é•¿æ–‡æœ¬è¾¹ç•Œæµ‹è¯•:把第 1 æ¡æ˜Žç»†çš„单元格文本放大到约 900 å­—符(`.codex-run/engineering-order-long-row-fresh2.docx`),Word é‡æ–°åˆ†é¡µåŽä¸º 5 é¡µï¼›`.codex-run/engineering-order-long-row-fresh2.pdf` ä¸­è¯¥è®°å½•的全部文本只出现在第 1 é¡µï¼Œå…¶ä½™ 5 æ¡è®°å½•与签字表顺延到第 2 é¡µï¼Œè®°å½•未跨页。
- æ¸²æŸ“截图:`.codex-run/word-export-long-row-fresh/page-1.png` è‡³ `page-5.png`;逐页字符统计脚本 `.codex-run/count_marker_chars.py`。
- æ³¨æ„ï¼šç”¨ .NET `ZipArchive` æ”¹å†™ docx ä¼šè®© Word æŠ¥â€œæ–‡ä»¶å¯èƒ½å·²ç»æŸåâ€ï¼Œå¿…须用 Python `zipfile` é‡å†™æµ‹è¯•文档。
**当前运行状态(2026-10-10)**
- åŽç«¯ `http://127.0.0.1:8091`,PID `180416`,标准 fat jar å¯åŠ¨ï¼š`java -cp target\emergency-repair-server-1.0.0-SNAPSHOT.jar org.springframework.boot.loader.JarLauncher`。
- åŽç«¯æ—¥å¿—:`.codex-run/backend-20261010-dynamic-pagination-8.out.log`、`.codex-run/backend-20261010-dynamic-pagination-8.err.log`。
- å‰ç«¯ `http://localhost:8082`,进程 PID `232852`,日志:`.codex-run/frontend-20261010-dynamic-pagination.out.log`、`.codex-run/frontend-20261010-dynamic-pagination.err.log`。
- ç™»å½• `admin / admin123`,打印接口测试使用 `Authorization: Bearer demo-admin-token`。
**接手注意**
- æžç«¯æƒ…况下若单条记录的实际排版高度超过一整页,任何 Word è¡Œçº§çº¦æŸéƒ½æ— æ³•让它完整放在一页;当前业务数据不存在该情况。
- ä¸è¦æäº¤ `.codex-run`、Office é”æ–‡ä»¶æˆ– `emergency-repair-web/public/新建 æ–‡æœ¬æ–‡æ¡£.txt`。
## 34. ä¿®æ­£å¿«ç…§ï¼ˆ2026-10-10 æ˜ŸæœŸå…­ï¼Œå·¥ç¨‹å•按明细分组连续打印四联)
**用户澄清**
- åŒä¸€ä¸ªæ˜Žç»†æ•°æ®çš„ 4 è”必须连续打印,再打印下一个明细数据的 4 è”,不能先打印所有明细的第 1 è”再打印第 2 è”。
**本轮修正**
- `EngineeringOrderPrintService.renderUnits` è°ƒæ•´ä¸ºå¤–层遍历明细记录、内层遍历 1-4 è”。
- æ¯å¼ æ˜Žç»†è¡¨åªæ¸²æŸ“当前一条记录,并在下一张联表前设置分页;保留单条记录 `cantSplit + keepLines`,避免记录跨页。
- ä¸»è¦æ–‡ä»¶ï¼š`emergency-repair-server/src/main/java/com/emergencyrepair/business/service/EngineeringOrderPrintService.java`。
**验证结果**
- ä½¿ç”¨ 6 æ¡æ•…障数据调用 `POST /api/faults/engineering-print`,生成 `.codex-run/engineering-order-per-record-units.docx` å’Œå¯¹åº” PDF。
- DOCX é¡¶å±‚元素顺序确认:明细 1 çš„第1/2/3/4联连续,随后是明细 2、3、4、5、6 çš„各自四联。
- å…± 48 å¼ è”表、24 é¡µï¼›æ¯è”标签各 6 æ¬¡ï¼Œå ä½ç¬¦æ®‹ç•™ 0,`pageBreakBefore` 23 ä¸ªã€‚
- PDF é¡µåˆ†ç»„确认:第 1-4 é¡µä¸ºæ˜Žç»† 1,第 5-8 é¡µä¸ºæ˜Žç»† 2,依次到第 21-24 é¡µä¸ºæ˜Žç»† 6。
- åŽç«¯å½“前运行 PID `243948`,监听 `http://127.0.0.1:8091`;前端 PID `232852`,监听 `http://localhost:8082`。
- åŽç«¯æ—¥å¿—:`.codex-run/backend-20261010-per-record-units.out.log`、`.codex-run/backend-20261010-per-record-units.err.log`。
## 35. ä¿®æ­£å¿«ç…§ï¼ˆ2026-10-10 æ˜ŸæœŸå…­ï¼Œå·¥ç¨‹å•打印分页问题定位)
**用户反馈**
- ç”¨æˆ·åé¦ˆâ€œç»“果不对,每页明细数据都只有一条”。
**定位结论**
- æœ€åˆæ€€ç–‘用户打开的是 `2026-10-10 12:45:28` ç”Ÿæˆçš„æ—§ç‰ˆæ–‡ä»¶ï¼Œä½†åŽç»­ç¡®è®¤è¯¥ç»“论不成立:真正的根因是当前代码在展开明细表时,只替换了数据行,模板中作为联头的第 1 è¡Œç”¨äº†ç©ºå€¼æ›¿æ¢ï¼Œå¯¼è‡´å³ä¾§ç«–排单元格的联号/联名被写成空字符串。详见第 36 èŠ‚ã€‚
- å½“前代码已改为单张明细表内按“记录 1 çš„第 1-4 è”、记录 2 çš„第 1-4 è”……”顺序渲染,并保留 `cantSplit + keepLines`;不再把同一故障的四联整体 `keepNext` ç»‘定,避免长记录把整组推到下一页。
**当前代码验证**
- ç”¨åŒä¸€æ‰¹ 6 æ¡æ•…障重新调用 `POST /api/faults/engineering-print`,生成 `.codex-run/engineering-order-current.docx`。
- å½“前 DOCX åªæœ‰ `1` å¼ æ˜Žç»†è¡¨ï¼ŒçŸ­è®°å½•可在同页排多条,四联顺序连续;联号/联名的最终验证见第 36 èŠ‚ã€‚
- å½“前文件另存验证副本:`C:\Users\Lenovo\Downloads\工程单-当前后台验证-20261010-135834.docx`。
**边界说明**
- å…¨é‡ 22 æ¡æ•°æ®æµ‹è¯•时,部分测试记录的“工程名称及损坏情况”超过 300 ä¸ªå­—符,单联高度已接近或超过一页可用明细区域;这种情况下 WPS æ— æ³•把四联或同页多条记录强行压在页内,即使去掉 `keepNext` ä¹Ÿä»ä¼šä¸€è”一页。
- è¿™ä¸æ˜¯å½“前分组代码造成的;当前代码已消除了旧版“一联一张顶层表格”的硬分页。
**运行状态(2026-10-10 13:58)**
- å‰ç«¯ `http://localhost:8082`,PID `232852`,端口已监听。
- åŽç«¯ `http://127.0.0.1:8091`,PID `247224`,启动时间 `2026-10-10 13:46:23`,端口已监听。
## 36. ä¿®æ­£å¿«ç…§ï¼ˆ2026-10-10 æ˜ŸæœŸå…­ï¼Œå·¥ç¨‹å•联号/联名未显示修复)
**用户反馈**
- ç”¨æˆ·åé¦ˆâ€œè”号和联名页没显示”,即 Word å·¥ç¨‹å•最右侧竖排单元格里的“第N联 ä¸ŠæŠ¥äººâ€ä¸ºç©ºã€‚
**根因**
- æ¨¡æ¿æ˜Žç»†è¡¨ç¬¬ 1 è¡Œæ˜¯è”头行(含最右侧 `{unitDisplay}` ç«–排单元格),最后一行是数据行。
- æ—§é€»è¾‘只展开并写入数据行,联头行用空值替换,导致联号/联名被写成空字符串,输出里只剩数据、没有联头标识。
**代码修改**
- `EngineeringOrderPrintService.renderUnits` æ”¹ä¸ºè°ƒç”¨ `renderDetailTable(detailTable, records, unitNames)`。
- æ–°å¢ž `renderDetailTable`:按“故障1的第1至4联,故障2的第1至4联……”逐组复制模板联头行和数据行,并对每联正确写入 `unitNo`、`unitName`、`unitDisplay`。
- `unitDisplay` æ ¼å¼ä¸º `第N联`,配置了名称时追加四个空格和名称,例如 `第1联    ä¸ŠæŠ¥äºº`。
- `configureDetailTablePagination` ä¿ç•™æ¯è¡Œ `cantSplit` å’Œæ®µè½ `keepLines`,显式清除旧 `keepNext`,只让联头行与自己的数据行保持 `keepNext`;不再把同一故障的四联绑成整体。
**配置**
- è”名仍取自 `emergency-repair-server/config/business-config.yml` çš„ `app.engineering-print.unit-names`。
- å½“前配置:`1: ä¸ŠæŠ¥äºº`,`2/3/4` ä¸ºç©ºï¼Œå› æ­¤ç¬¬ 2/3/4 è”只显示“第2联/第3联/第4联”。
**验证结果**
- åŽç«¯ `mvn -B -DskipTests package` é€šè¿‡ï¼ˆJDK `C:\Program Files\Java\jdk1.8.0_172`,Maven `D:\solidDir\apache-maven-3.6.1`)。
- æ–°åŽç«¯ PID `248156`,监听 `http://127.0.0.1:8091`;前端 PID `232852`,监听 `http://localhost:8082`。
- ä½¿ç”¨å‰ 17 æ¡æ•…障调用 `POST /api/faults/engineering-print`,Header ä¸º `Authorization: Bearer demo-admin-token`,生成 `.codex-run/engineering-order-labels-verified.docx` ä¸Ž `.pdf`。
- XML æ£€æŸ¥ï¼š1 å¼ æ˜Žç»†è¡¨ã€136 è¡Œï¼Œå³ 17 æ¡æ•…éšœ Ã— 4 è” Ã— 2 è¡Œã€‚
- `第1联    ä¸ŠæŠ¥äºº`、`第2联`、`第3联`、`第4联` å„出现 17 æ¬¡ï¼›`{unitNo}`、`{unitName}`、`{unitDisplay}` æ®‹ç•™å‡ä¸º 0。
- PDF å…± 29 é¡µï¼Œå·²æ¸²æŸ“到 `.codex-run/engineering-order-labels-pages/`;第 1、4 é¡µå¯è§æœ€å³ä¾§ç«–排联号,第 1 é¡µæœ‰æ˜Žç»†ï¼Œä¸å†åªæœ‰é¡¶éƒ¨å’Œç­¾å­—表。
**运行状态(2026-10-10 14:21)**
- å‰ç«¯ `http://localhost:8082`,PID `232852`。
- åŽç«¯ `http://127.0.0.1:8091`,PID `248156`,启动时间 `2026-10-10 14:20:00`。
- åŽç«¯æ—¥å¿—:`.codex-run/backend-20261010-unit-labels.out.log`、`.codex-run/backend-20261010-unit-labels.err.log`。
- éªŒè¯æ–‡ä»¶ï¼š`.codex-run/engineering-order-labels-verified.docx`、`.codex-run/engineering-order-labels-verified.pdf`。
**注意事项**
- ä¸è¦é‡æ–°æŠŠåŒä¸€æ•…障的四联用 `keepNext` ç»‘成整块,否则长记录会把整组推到下一页。
- è‹¥ç”¨æˆ·å†çœ‹æ—§æ–‡ä»¶ä¼šè¯¯ä»¥ä¸ºæœªä¿®å¤ï¼Œéœ€é‡æ–°åœ¨é¡µé¢è§¦å‘一次打印;旧 `.docx` ä¸ä¼šè¢«åŽå°å›žå†™ã€‚
- ä¸è¦æäº¤ `.codex-run`、Office é”æ–‡ä»¶æˆ– `emergency-repair-web/public/新建 æ–‡æœ¬æ–‡æ¡£.txt`。
## 37. ä¿®æ­£å¿«ç…§ï¼ˆ2026-10-10 æ˜ŸæœŸå…­ï¼ŒåŽŸæ‰“å°å·¥ç¨‹å•æŒ‰é€»è¾‘é¡µè¾“å‡ºå››è”ï¼‰
**用户澄清**
- åŽŸâ€œæ‰“å°å·¥ç¨‹å•â€çš„é‡å¤å•ä½æ˜¯é€»è¾‘é¡µï¼Œä¸æ˜¯å•æ¡è®°å½•ï¼šå…ˆæŒ‰è®°å½•åŠ¨æ€åˆ†é¡µï¼Œå†æŠŠæ¯ä¸ªé€»è¾‘é¡µçš„æ•°æ®åˆ†åˆ«è¾“å‡ºä¸ºç¬¬ 1、2、3、4 è”。
- â€œæ‰“印工程单2”已经稳定,本轮不改它的 `renderLegacyGroupedUnits()` å’Œæœ€åŽä¸€åˆ—纵向合并逻辑。
**代码修改**
- `EngineeringOrderPrintService.renderPagedUnits()` ä¿ç•™é¡µçº§å››è”输出:每个逻辑页生成 1-4 è”,每联单独分页。
- `paginateRecords()` çš„可用高度系数由 `0.75` è°ƒæ•´ä¸º `0.50`,防止长文本明细把同一联表挤到续页。
- æ˜Žç»†è¡Œç»§ç»­ä¿ç•™ `cantSplit + keepLines`,一条记录不能跨页;模板页眉和签字页脚继续通过 Word header/footer åœ¨æ¯é¡µé‡å¤ã€‚
- ä¸»è¦æ–‡ä»¶ï¼š`emergency-repair-server/src/main/java/com/emergencyrepair/business/service/EngineeringOrderPrintService.java`。
**验证结果**
- åŽç«¯ `mvn -q -DskipTests package` é€šè¿‡ã€‚
- ä½¿ç”¨ `.codex-run/engineering-print-all-request.json` çš„ 22 æ¡æ•…障调用 `POST /api/faults/engineering-print`,生成 `.codex-run/engineering-order-page-repeat-v3.docx`。
- XML ç»“构:28 å¼ è”表、28 ä¸ªè”号标记;Word å®žé™… 28 é¡µï¼Œç­‰äºŽ 7 ä¸ªé€»è¾‘页乘 4 è”,没有联表续页。
- Word æ£€æŸ¥ç¡®è®¤æ¯é¡µéƒ½æœ‰é¡µçœ‰å’Œç­¾å­—页脚;第 1 è”到第 4 è”按逻辑页连续输出。
**运行状态(2026-10-10 16:48)**
- å‰ç«¯ `http://localhost:8082`,PID `232852`,端口已监听。
- åŽç«¯ `http://127.0.0.1:8091`,PID `261356`,端口已监听;由当前 Codex ç»ˆç«¯å‰å°æ‰˜ç®¡ã€‚
- ç™»å½•账号 `admin / admin123`。
## 38. ä¿®æ­£å¿«ç…§ï¼ˆ2026-10-10 æ˜ŸæœŸå…­ï¼Œå·¥ç¨‹å•打印页面去掉表格内部滚动条)
**用户反馈**
- è°ƒæ•´å¸ƒå±€åŽ»æŽ‰æœ€å³ä¾§å·¥ä½œåŒºæ»šåŠ¨æ¡åŽï¼Œè¡¨æ ¼å†…éƒ¨åˆå‡ºçŽ°äº†æ»šåŠ¨æ¡ï¼›ç”¨æˆ·ä¸å¸Œæœ›è¡¨æ ¼æœ¬èº«æ˜¾ç¤ºæ»šåŠ¨æ¡ã€‚
**本轮修正**
- `EngineeringPrint.vue` ä¿æŒé¡µé¢çºµå‘ flex å¸ƒå±€ï¼Œå·¥ä½œåŒºä¸å†äº§ç”Ÿå¤–滚动条。
- å¯¹ Element UI çš„ `.el-table__body-wrapper` å’Œ `.el-table__fixed-body-wrapper` åŒæ—¶éšè—æ»šåŠ¨æ¡ï¼Œæ»šåŠ¨èƒ½åŠ›ä¿ç•™ï¼Œé¿å… 20 æ¡æ•°æ®æ—¶åº•部内容无法访问。
- ä¸»è¦æ–‡ä»¶ï¼š`emergency-repair-web/src/views/fault/EngineeringPrint.vue`。
**验证结果**
- CDP æ¡Œé¢è§†å£ `1440x900`:工作区 `clientHeight=792`、`scrollHeight=792`,外滚动条不存在。
- `.el-table__body-wrapper` ä»å¯æ»šåŠ¨ï¼Œè®¡ç®—æ ·å¼ `scrollbar-width: none`,滚动条不可见。
- é¡µé¢æˆªå›¾ï¼š`.codex-run/engineering-print-layout-current.png`。
**当前运行状态(2026-10-10)**
- å‰ç«¯ `http://localhost:8082`,PID `232852`;后端 `http://127.0.0.1:8091`,PID `261356`。
emergency-repair-server/pom.xml
@@ -29,6 +29,22 @@
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>4.1.0</version>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.poi</groupId>
                    <artifactId>poi-ooxml-schemas</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>ooxml-schemas</artifactId>
            <version>1.4</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-crypto</artifactId>
        </dependency>
emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DataInitializer.java
@@ -89,6 +89,7 @@
        seedMenusAndRoles();
        ensureSystemMenus();
        ensureCompletionMenus();
        ensureEngineeringPrintMenu();
        normalizeSensitiveTerms();
        normalizeEquipmentTerms();
        seedButtonMenus();
@@ -116,14 +117,15 @@
            createMenu("dashboard", null, "首页门户", "/dashboard", "el-icon-s-home", 1, 10);
            Long fault = createMenu("fault", null, "故障管理", null, "el-icon-warning-outline", 0, 20);
            createMenu("fault-report", fault, "故障上报", "/fault/report", null, 1, 10);
            createMenu("xcb-review", fault, "rk_XCB助理审核", "/fault/review", null, 1, 20);
            createMenu("cmb-approval", fault, "rk_CMB领导审批", "/fault/approval/cmb", null, 1, 30);
            createMenu("jcc-assistant", fault, "rk_JCC助理审批", "/fault/approval/jcc-assistant", null, 1, 40);
            createMenu("jcc-director", fault, "rk_JCC处长审批", "/fault/approval/jcc-director", null, 1, 50);
            createMenu("dispatch", fault, "rk_XCB助理派单", "/fault/dispatch", null, 1, 60);
            createMenu("xcb-review", fault, "rk_第一审批人审核", "/fault/review", null, 1, 20);
            createMenu("cmb-approval", fault, "rk_第二审批人审批", "/fault/approval/cmb", null, 1, 30);
            createMenu("jcc-assistant", fault, "rk_第三审批人审批", "/fault/approval/jcc-assistant", null, 1, 40);
            createMenu("jcc-director", fault, "rk_第四审批人审批", "/fault/approval/jcc-director", null, 1, 50);
            createMenu("dispatch", fault, "rk_第一审批人派单", "/fault/dispatch", null, 1, 60);
            createMenu("completion-list", fault, "完工情况处理", "/fault/completion", null, 1, 70);
            createMenu("completion-report", fault, "完工信息上报", "/completion/report", null, 1, 80);
            createMenu("completion-query", fault, "完工信息查阅", "/completion/query", null, 1, 90);
            createMenu("engineering-print", fault, "工程单打印", "/fault/engineering-print", null, 1, 100);
            Long acceptance = createMenu("acceptance", null, "完工验收", null, "el-icon-circle-check", 0, 30);
            createMenu("accept-application", acceptance, "验收申请", "/acceptance/application", null, 1, 10);
@@ -281,6 +283,16 @@
        grantAdminMenus("/completion/query");
    }
    private void ensureEngineeringPrintMenu() {
        SysMenu fault = menuMapper.selectOne(Wrappers.<SysMenu>lambdaQuery()
                .eq(SysMenu::getName, "故障管理")
                .eq(SysMenu::getMenuType, 0)
                .last("LIMIT 1"));
        Long pid = fault == null ? 0L : fault.getId();
        ensureMenu(pid, "工程单打印", "/fault/engineering-print", 100);
        grantAdminMenus("/fault/engineering-print");
    }
    private void ensureSystemMenus() {
        SysMenu system = menuMapper.selectOne(Wrappers.<SysMenu>lambdaQuery()
                .eq(SysMenu::getName, "\u7cfb\u7edf\u7ba1\u7406").last("LIMIT 1"));
emergency-repair-server/src/main/java/com/emergencyrepair/business/controller/FaultController.java
@@ -5,8 +5,13 @@
import com.emergencyrepair.business.entity.Fault;
import com.emergencyrepair.business.entity.WorkOrder;
import com.emergencyrepair.business.service.BusinessService;
import com.emergencyrepair.business.service.EngineeringOrderPrintService;
import com.emergencyrepair.common.Result;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -19,6 +24,9 @@
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
@RestController
@@ -26,6 +34,7 @@
@RequiredArgsConstructor
public class FaultController {
    private final BusinessService businessService;
    private final EngineeringOrderPrintService engineeringOrderPrintService;
    @GetMapping("/page")
    public Result<Page<Fault>> page(@RequestParam(defaultValue = "1") long current,
@@ -112,4 +121,41 @@
    public Result<List<WorkOrder>> dispatch(@RequestBody Requests.DispatchRequest request) {
        return Result.ok(businessService.dispatch(request));
    }
    /**
     * å·¥ç¨‹å•打印:把选中的故障记录套用 Word æ¨¡æ¿ç”Ÿæˆå››è”工程单。
     */
    @PostMapping("/engineering-print")
    public ResponseEntity<byte[]> engineeringPrint(@RequestBody Requests.IdList request) {
        return documentResponse(engineeringOrderPrintService.render(request.getIds()),
                "工程单.docx", "engineering-order.docx");
    }
    /**
     * æ‰“印工程单2:第1联全部明细后换页,再输出第2联,以此类推。
     */
    @PostMapping("/engineering-print-v2")
    public ResponseEntity<byte[]> engineeringPrintV2(@RequestBody Requests.IdList request) {
        return documentResponse(engineeringOrderPrintService.renderVersion2(request.getIds()),
                "工程单2.docx", "engineering-order-2.docx");
    }
    private ResponseEntity<byte[]> documentResponse(byte[] document, String displayName, String fallbackName) {
        String filename = encodeFilename(displayName, fallbackName);
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.parseMediaType(
                "application/vnd.openxmlformats-officedocument.wordprocessingml.document"));
        headers.set(HttpHeaders.CONTENT_DISPOSITION,
                "attachment; filename=\"" + fallbackName + "\"; filename*=UTF-8''" + filename);
        headers.setContentLength(document.length);
        return new ResponseEntity<>(document, headers, HttpStatus.OK);
    }
    private String encodeFilename(String name, String fallbackName) {
        try {
            return URLEncoder.encode(name, StandardCharsets.UTF_8.name()).replace("+", "%20");
        } catch (UnsupportedEncodingException exception) {
            return fallbackName;
        }
    }
}
emergency-repair-server/src/main/java/com/emergencyrepair/business/service/BusinessService.java
@@ -47,7 +47,7 @@
public class BusinessService {
    private static final DateTimeFormatter FAULT_DATE_FORMAT = DateTimeFormatter.ofPattern("yyMMdd");
    private static final Object FAULT_CODE_LOCK = new Object();
    @Value("${fault-code.prefix:\u6f5c1\u57fa-}")
    @Value("${fault-code.prefix:固定前缀-}")
    private String faultCodePrefix;
    private final FaultMapper faultMapper;
    private final JdbcTemplate jdbcTemplate;
@@ -253,7 +253,7 @@
            throw new BusinessException("请填写销项理由");
        }
        String cancelledBy = StringUtils.hasText(request.getCancelledBy())
                ? request.getCancelledBy().trim() : "rk_XCB助理";
                ? request.getCancelledBy().trim() : "rk_第一审批人";
        for (Long id : request.getIds()) {
            Fault fault = requireFault(id);
            if (!StatusConstants.Task.REPORTED_XCB.equals(fault.getTaskStatus())
@@ -307,7 +307,7 @@
            Fault fault = requireFault(id);
            if (!StatusConstants.Task.REPORTED_XCB.equals(fault.getTaskStatus())
                    && !StatusConstants.Task.RETURNED.equals(fault.getTaskStatus())) {
                throw new BusinessException("故障 " + fault.getFaultCode() + " å½“前状态不允许rk_XCB助理审核");
                throw new BusinessException("故障 " + fault.getFaultCode() + " å½“前状态不允许rk_第一审批人审核");
            }
            index++;
            fault.setContractorIds(contractorIds);
@@ -334,21 +334,21 @@
            LocalDateTime now = LocalDateTime.now();
            switch (request.getStage()) {
                case "CMB":
                    requireStatus(fault, StatusConstants.Task.XCB_REVIEWED, "rk_CMB领导审批");
                    requireStatus(fault, StatusConstants.Task.XCB_REVIEWED, "rk_第二审批人审批");
                    fault.setCmbOpinion(request.getOpinion());
                    fault.setCmbResult(pass ? "通过" : "不通过");
                    fault.setCmbTime(now);
                    fault.setTaskStatus(pass ? StatusConstants.Task.CMB_APPROVED : StatusConstants.Task.RETURNED);
                    break;
                case "JCC_ASSISTANT":
                    requireStatus(fault, StatusConstants.Task.CMB_APPROVED, "rk_JCC助理审批");
                    requireStatus(fault, StatusConstants.Task.CMB_APPROVED, "rk_第三审批人审批");
                    fault.setJccAssistantOpinion(request.getOpinion());
                    fault.setJccAssistantResult(pass ? "通过" : "不通过");
                    fault.setJccAssistantTime(now);
                    fault.setTaskStatus(pass ? StatusConstants.Task.JCC_ASSISTANT_APPROVED : StatusConstants.Task.RETURNED);
                    break;
                case "JCC_DIRECTOR":
                    requireStatus(fault, StatusConstants.Task.JCC_ASSISTANT_APPROVED, "rk_JCC处长审批");
                    requireStatus(fault, StatusConstants.Task.JCC_ASSISTANT_APPROVED, "rk_第四审批人审批");
                    fault.setJccDirectorOpinion(request.getOpinion());
                    fault.setJccDirectorResult(pass ? "通过" : "不通过");
                    fault.setJccDirectorTime(now);
emergency-repair-server/src/main/java/com/emergencyrepair/business/service/EngineeringOrderPrintService.java
New file
@@ -0,0 +1,1400 @@
package com.emergencyrepair.business.service;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.emergencyrepair.business.entity.CompletionInfo;
import com.emergencyrepair.business.entity.Fault;
import com.emergencyrepair.business.entity.WorkOrder;
import com.emergencyrepair.business.mapper.CompletionInfoMapper;
import com.emergencyrepair.business.mapper.FaultMapper;
import com.emergencyrepair.business.mapper.WorkOrderMapper;
import com.emergencyrepair.business.entity.ProductNode;
import com.emergencyrepair.business.mapper.ProductNodeMapper;
import com.emergencyrepair.common.BusinessException;
import lombok.RequiredArgsConstructor;
import org.apache.poi.ooxml.POIXMLProperties;
import org.apache.poi.wp.usermodel.HeaderFooterType;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFFooter;
import org.apache.poi.xwpf.usermodel.XWPFHeader;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
import org.apache.xmlbeans.XmlObject;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBody;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTHdrFtr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageMar;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageSz;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRow;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSpacing;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTrPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSectPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTbl;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblBorders;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblGrid;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblPPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblWidth;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblCellMar;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTc;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcMar;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STYAlign;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STBorder;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STPageOrientation;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STLineSpacingRule;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STTblWidth;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STTextDirection;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STVerticalJc;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STVAnchor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
 * å·¥ç¨‹å•打印(需求 32)。
 *
 * <p>把选中的故障记录按 Word æ¨¡æ¿è¾“出成 A4 æ¨ªå‘的四联工程单,明细可自动分页。
 * æ¨¡æ¿é‡Œç”¨ {@code {字段名}} å ä½ï¼Œæ¸²æŸ“时替换成真实数据。</p>
 *
 * <p>模板文件默认放在 {@code config/engineering-order-template.docx},首次启动若不存在会自动生成一份,
 * è¿ç»´/业务人员可以直接用 Word æ‰“开修改标题、列宽、联头文字,不需要改代码。</p>
 *
 * <p>业务配置放在 {@code config/business-config.yml},可按联号维护第 1~4 è”名称。</p>
 *
 * <p>模板约定(包含明细字段的表格):</p>
 * <ul>
 *   <li>明细表最后一行是数据行模板,会按选中记录数复制展开,记录行不允许跨页拆分;</li>
 *   <li>联级字段可以用 {@code {xNo}}、{@code {teamMajor}}、{@code {maxFinishDate}}、{@code {unitNo}}、{@code {unitName}}、{@code {unitDisplay}};</li>
 *   <li>整份模板会被复制 4 æ¬¡è¾“出为四联(每联一页),{@code {unitNo}} ä¾æ¬¡ä¸º 1/2/3/4。</li>
 * </ul>
 */
@Service
@RequiredArgsConstructor
public class EngineeringOrderPrintService {
    private static final Logger log = LoggerFactory.getLogger(EngineeringOrderPrintService.class);
    /** ä¸€ä¸ª Word æ–‡ä»¶é‡Œçš„联数。 */
    private static final int UNIT_COUNT = 4;
    private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final Pattern PLACEHOLDER = Pattern.compile("\\{([^{}]*)\\}");
    private static final String TEMPLATE_VERSION = "engineering-order-template-v4";
    private static final Pattern TEMPLATE_PLACEHOLDER =
            Pattern.compile("\\{\\{\\s*([^{}]+?)\\s*\\}\\}|\\{\\s*([^{}]+?)\\s*\\}");
    /** åªæœ‰åŒ…含明细字段的表格才按记录数展开最后一行。 */
    private static final Set<String> DETAIL_PLACEHOLDERS = new HashSet<>(Arrays.asList(
            "faultCode", "engineeringNo", "workOrderNo", "equipmentName", "quantity", "equipmentCount",
            "parentSystem", "engineeringMajor", "faultImpact", "faultNature", "majorFaultDesc",
            "repairRequirement", "engineeringNameDesc", "isSelfRepair", "isHRelated", "isMonitorItem",
            "monitorItem", "monitorValue", "requestedFinishDate", "remark", "contractorName",
            "contractorNames", "startDate", "finishDate", "owner", "reportUnit", "reportDate"
    ));
    private static final BigInteger A4_LANDSCAPE_WIDTH = BigInteger.valueOf(16838);
    private static final BigInteger A4_LANDSCAPE_HEIGHT = BigInteger.valueOf(11906);
    private static final BigInteger PAGE_MARGIN = BigInteger.valueOf(567);
    /** é»˜è®¤æ¨¡æ¿çš„联头占位文字。 */
    private static final String DEFAULT_UNIT_HEADER =
            "X号:{xNo} ZB型号:{zbModel} 专业:{teamMajor} 最大要求完工时间:{maxFinishDate}";
    /** é»˜è®¤æ¨¡æ¿çš„列定义:标题 + å­—段占位符。 */
    private static final String[][] DEFAULT_COLUMNS = {
            {"故障编号", "{faultCode}"},
            {"设备名称", "{equipmentName}"},
            {"工程名称及损坏情况", "{engineeringNameDesc}"},
            {"修理技术要求", "{repairRequirement}"},
            {"承修单位", "{contractorName}"},
            {"数量", "{quantity}"},
            {"备注", "{remark}"}
    };
    /** é»˜è®¤æ¨¡æ¿çš„列宽(twips),合计约等于 A4 æ¨ªå‘可用宽度。 */
    private static final int UNIT_COLUMN_WIDTH = 400;
    private static final int[] TABLE_COLUMN_WIDTHS = {1450, 2750, 3300, 3300, 2000, 800, 1700, UNIT_COLUMN_WIDTH};
    private final FaultMapper faultMapper;
    private final WorkOrderMapper workOrderMapper;
    private final CompletionInfoMapper completionInfoMapper;
    private final ProductNodeMapper productNodeMapper;
    @Value("${app.engineering-print.template-path:${app.engineering-print.template:config/engineering-order-template.docx}}")
    private String templateLocation;
    @Value("${app.engineering-print.unit-names.1:上报人}")
    private String unitName1;
    @Value("${app.engineering-print.unit-names.2:}")
    private String unitName2;
    @Value("${app.engineering-print.unit-names.3:}")
    private String unitName3;
    @Value("${app.engineering-print.unit-names.4:}")
    private String unitName4;
    @PostConstruct
    public void ensureTemplate() {
        Path path = templatePath();
        if (path == null) {
            return;
        }
        try {
            Files.createDirectories(path.getParent());
            if (Files.exists(path)) {
                return;
            }
            try (XWPFDocument document = buildDefaultTemplate();
                 ByteArrayOutputStream out = new ByteArrayOutputStream()) {
                document.write(out);
                Files.write(path, out.toByteArray());
            }
            log.info("工程单打印模板已生成或升级:{}", path.toAbsolutePath());
        } catch (Exception exception) {
            log.warn("生成或升级工程单打印模板失败:{}", exception.getMessage());
        }
    }
    private Map<Integer, String> loadUnitNames() {
        Map<Integer, String> names = new LinkedHashMap<>();
        names.put(1, text(unitName1).trim());
        names.put(2, text(unitName2).trim());
        names.put(3, text(unitName3).trim());
        names.put(4, text(unitName4).trim());
        return names;
    }
    private boolean templateNeedsUpgrade(Path path) {
        try (InputStream input = Files.newInputStream(path);
             XWPFDocument document = new XWPFDocument(input)) {
            POIXMLProperties.CoreProperties properties = document.getProperties().getCoreProperties();
            return !TEMPLATE_VERSION.equals(properties.getKeywords())
                    || collectText(document).indexOf("{zbModel}") < 0
                    || collectText(document).indexOf("{unitDisplay}") < 0;
        } catch (Exception exception) {
            log.warn("工程单打印模板无法读取,将重新生成:{}", exception.getMessage());
            return true;
        }
    }
    /**
     * æŒ‰é€‰ä¸­çš„æ•…障记录渲染工程单 Word æ–‡æ¡£ã€‚
     *
     * @param faultIds æ•…障主键
     */
    public byte[] render(List<Long> faultIds) {
        return renderInternal(faultIds, false);
    }
    /**
     * æ‰“印工程单2:按联分组输出,第1联全部明细后换页,再输出第2联,以此类推。
     */
    public byte[] renderVersion2(List<Long> faultIds) {
        return renderInternal(faultIds, true);
    }
    private byte[] renderInternal(List<Long> faultIds, boolean legacyGroupedUnits) {
        List<Long> ids = normalizeIds(faultIds);
        if (ids.isEmpty()) {
            throw new BusinessException("请至少选择一条记录后再打印");
        }
        List<Map<String, String>> records = loadRecords(ids);
        if (records.isEmpty()) {
            throw new BusinessException("选中的记录已不存在,请刷新后重试");
        }
        Map<Integer, String> unitNames = loadUnitNames();
        try (InputStream template = openTemplate();
             XWPFDocument document = new XWPFDocument(template);
             ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {
            if (legacyGroupedUnits) {
                renderLegacyGroupedUnits(document, records, unitNames);
            } else {
                renderUnits(document, records, unitNames);
            }
            document.write(buffer);
            return buffer.toByteArray();
        } catch (IOException exception) {
            throw new BusinessException("生成工程单失败:" + exception.getMessage());
        }
    }
    private List<Long> normalizeIds(List<Long> faultIds) {
        if (faultIds == null) {
            return new ArrayList<>();
        }
        List<Long> ids = new ArrayList<>();
        for (Long id : faultIds) {
            if (id != null && !ids.contains(id)) {
                ids.add(id);
            }
        }
        return ids;
    }
    private List<Map<String, String>> loadRecords(List<Long> ids) {
        List<Fault> faults = faultMapper.selectBatchIds(ids);
        Map<Long, Fault> faultById = new LinkedHashMap<>();
        for (Fault fault : faults) {
            faultById.put(fault.getId(), fault);
        }
        Map<String, String> zbModelByXNo = loadZbModels(faults);
        List<WorkOrder> workOrders = workOrderMapper.selectList(Wrappers.<WorkOrder>lambdaQuery()
                .in(WorkOrder::getFaultId, ids));
        Map<Long, WorkOrder> workOrderByFault = new LinkedHashMap<>();
        List<Long> workOrderIds = new ArrayList<>();
        for (WorkOrder workOrder : workOrders) {
            workOrderByFault.putIfAbsent(workOrder.getFaultId(), workOrder);
            if (workOrder.getId() != null) {
                workOrderIds.add(workOrder.getId());
            }
        }
        Map<Long, CompletionInfo> completionByWorkOrder = new LinkedHashMap<>();
        if (!workOrderIds.isEmpty()) {
            List<CompletionInfo> completions = completionInfoMapper.selectList(Wrappers.<CompletionInfo>lambdaQuery()
                    .in(CompletionInfo::getWorkOrderId, workOrderIds));
            for (CompletionInfo completion : completions) {
                completionByWorkOrder.putIfAbsent(completion.getWorkOrderId(), completion);
            }
        }
        LocalDate maxFinishDate = null;
        for (CompletionInfo completion : completionByWorkOrder.values()) {
            if (completion.getFinishDate() != null
                    && (maxFinishDate == null || completion.getFinishDate().isAfter(maxFinishDate))) {
                maxFinishDate = completion.getFinishDate();
            }
        }
        if (maxFinishDate == null) {
            for (Long id : ids) {
                Fault fault = faultById.get(id);
                if (fault != null && fault.getRequestedFinishDate() != null
                        && (maxFinishDate == null || fault.getRequestedFinishDate().isAfter(maxFinishDate))) {
                    maxFinishDate = fault.getRequestedFinishDate();
                }
            }
        }
        List<Map<String, String>> records = new ArrayList<>();
        for (Long id : ids) {
            Fault fault = faultById.get(id);
            if (fault == null) {
                continue;
            }
            WorkOrder workOrder = workOrderByFault.get(id);
            CompletionInfo completion = workOrder == null ? null : completionByWorkOrder.get(workOrder.getId());
            String zbModel = zbModelByXNo.get(normalizeXNo(fault.getXNo()));
            records.add(buildRecord(fault, workOrder, completion, maxFinishDate, zbModel));
        }
        return records;
    }
    private Map<String, String> loadZbModels(List<Fault> faults) {
        Map<String, String> result = new LinkedHashMap<>();
        List<String> xNos = new ArrayList<>();
        for (Fault fault : faults) {
            String xNo = normalizeXNo(fault.getXNo());
            if (StringUtils.hasText(xNo) && !xNos.contains(xNo)) {
                xNos.add(xNo);
            }
        }
        if (xNos.isEmpty()) {
            return result;
        }
        List<ProductNode> sideNodes = productNodeMapper.selectList(Wrappers.<ProductNode>lambdaQuery()
                .in(ProductNode::getLayer, java.util.Arrays.asList("side", "SIDE"))
                .in(ProductNode::getName, xNos)
                .ne(ProductNode::getIsDelete, "1")
                .orderByAsc(ProductNode::getSortNo)
                .orderByAsc(ProductNode::getId));
        Map<String, Long> parentIdsByXNo = new LinkedHashMap<>();
        for (ProductNode side : sideNodes) {
            String xNo = normalizeXNo(side.getName());
            if (StringUtils.hasText(xNo) && side.getParentId() != null && side.getParentId() != 0L) {
                parentIdsByXNo.putIfAbsent(xNo, side.getParentId());
            }
        }
        if (parentIdsByXNo.isEmpty()) {
            return result;
        }
        List<Long> parentIds = new ArrayList<>(new java.util.LinkedHashSet<>(parentIdsByXNo.values()));
        List<ProductNode> parents = productNodeMapper.selectBatchIds(parentIds);
        Map<Long, String> parentNameById = new LinkedHashMap<>();
        for (ProductNode parent : parents) {
            if (!"1".equals(parent.getIsDelete())) {
                parentNameById.put(parent.getId(), text(parent.getName()));
            }
        }
        for (Map.Entry<String, Long> entry : parentIdsByXNo.entrySet()) {
            result.put(entry.getKey(), text(parentNameById.get(entry.getValue())));
        }
        return result;
    }
    private String normalizeXNo(String value) {
        return StringUtils.hasText(value) ? value.trim() : "";
    }
    private Map<String, String> buildRecord(Fault fault, WorkOrder workOrder, CompletionInfo completion,
                                            LocalDate maxFinishDate, String zbModel) {
        Map<String, String> values = new LinkedHashMap<>();
        String contractor = firstText(
                workOrder == null ? null : workOrder.getContractorName(),
                completion == null ? null : completion.getContractorName(),
                fault.getContractorNames());
        String engineeringNo = firstText(
                completion == null ? null : completion.getEngineeringNo(),
                fault.getEngineeringNo());
        values.put("faultCode", text(fault.getFaultCode()));
        values.put("engineeringNo", text(engineeringNo));
        values.put("workOrderNo", text(workOrder == null ? null : workOrder.getWorkOrderNo()));
        values.put("equipmentName", text(fault.getEquipmentName()));
        values.put("quantity", String.valueOf(equipmentQuantity(fault)));
        values.put("equipmentCount", values.get("quantity"));
        values.put("parentSystem", text(fault.getParentSystem()));
        values.put("engineeringMajor", text(fault.getEngineeringMajor()));
        values.put("teamMajor", text(fault.getTeamMajor()));
        values.put("xNo", text(fault.getXNo()));
        values.put("zbModel", text(zbModel));
        values.put("owner", text(fault.getOwner()));
        values.put("reportUnit", text(fault.getReportUnit()));
        values.put("reportDate", date(fault.getReportDate()));
        values.put("faultImpact", text(fault.getFaultImpact()));
        values.put("faultNature", text(fault.getFaultNature()));
        values.put("majorFaultDesc", text(fault.getMajorFaultDesc()));
        values.put("repairRequirement", text(fault.getRepairRequirement()));
        values.put("engineeringNameDesc", text(fault.getEngineeringNameDesc()));
        values.put("isSelfRepair", yesNo(fault.getIsSelfRepair()));
        values.put("isHRelated", yesNo(fault.getIsHRelated()));
        values.put("isMonitorItem", yesNo(fault.getIsMonitorItem()));
        values.put("monitorItem", text(fault.getMonitorItem()));
        values.put("monitorValue", text(fault.getMonitorValue()));
        values.put("requestedFinishDate", date(fault.getRequestedFinishDate()));
        values.put("remark", text(fault.getRemark()));
        values.put("contractorName", text(contractor));
        values.put("contractorNames", text(fault.getContractorNames()));
        values.put("startDate", date(completion == null ? null : completion.getStartDate()));
        values.put("finishDate", date(completion == null ? null : completion.getFinishDate()));
        values.put("maxFinishDate", date(maxFinishDate));
        return values;
    }
    private int equipmentQuantity(Fault fault) {
        String equipmentIds = fault.getEquipmentIds();
        int count = 0;
        if (StringUtils.hasText(equipmentIds)) {
            for (String item : equipmentIds.split(",")) {
                if (StringUtils.hasText(item)) {
                    count++;
                }
            }
        }
        if (count == 0 && fault.getEquipmentId() != null) {
            count = 1;
        }
        return count;
    }
    private void renderUnits(XWPFDocument document,
                             List<Map<String, String>> records, Map<Integer, String> unitNames) {
        CTBody body = document.getDocument().getBody();
        List<XmlObject> templateElements = snapshotBodyElements(document);
        if (templateElements.isEmpty()) {
            throw new BusinessException("Engineering order template is empty: " + templateLocation);
        }
        int detailTableIndex = findDetailTableIndex(templateElements);
        if (detailTableIndex < 0) {
            throw new BusinessException("Engineering order template has no detail table: " + templateLocation);
        }
        int bottomTableIndex = findBottomTableIndex(templateElements, detailTableIndex);
        Map<String, String> shared = new LinkedHashMap<>(records.get(0));
        CTSectPr section = body.isSetSectPr() ? (CTSectPr) body.getSectPr().copy() : null;
        CTBody cleanBody = CTBody.Factory.newInstance();
        if (section != null) {
            cleanBody.addNewSectPr().set(section);
        }
        body.set(cleanBody);
        // Render each dynamically paginated detail page as four consecutive units.
        renderPagedUnits(body, (CTTbl) templateElements.get(detailTableIndex), records, unitNames, section);
        moveSectionPropertiesToEnd(body);
        copyTemplatePrefixToHeader(document, templateElements, detailTableIndex, shared, records);
        if (bottomTableIndex >= 0) {
            copyTemplateTableToFooter(document, (CTTbl) templateElements.get(bottomTableIndex), shared, records);
        }
    }
    private void renderPagedUnits(CTBody body, CTTbl templateTable,
                                  List<Map<String, String>> records,
                                  Map<Integer, String> unitNames,
                                  CTSectPr section) {
        List<List<Map<String, String>>> pages = paginateRecords(templateTable, records, section);
        int renderedPage = 0;
        for (List<Map<String, String>> page : pages) {
            for (int unit = 1; unit <= UNIT_COUNT; unit++) {
                String unitName = text(unitNames.get(unit)).trim();
                Map<String, String> unitValues = new LinkedHashMap<>();
                unitValues.put("unitNo", String.valueOf(unit));
                unitValues.put("unitName", unitName);
                unitValues.put("unitDisplay", "\u7b2c" + unit + "\u8054"
                        + (StringUtils.hasText(unitName) ? "    " + unitName : ""));
                List<Map<String, String>> unitRecords = new ArrayList<>();
                for (Map<String, String> record : page) {
                    Map<String, String> unitRecord = new LinkedHashMap<>(record);
                    unitRecord.putAll(unitValues);
                    unitRecords.add(unitRecord);
                }
                CTTbl table = body.addNewTbl();
                table.set(templateTable);
                clearTablePosition(table);
                renderTable(table, unitValues, unitRecords);
                int headerRowCount = table.sizeOfTrArray() - unitRecords.size();
                configureUnitTablePagination(table, renderedPage > 0, headerRowCount);
                configureUnitColumnVerticalMerge(table);
                renderedPage++;
            }
        }
    }
    private List<List<Map<String, String>>> paginateRecords(CTTbl templateTable,
                                                             List<Map<String, String>> records,
                                                             CTSectPr section) {
        List<List<Map<String, String>>> pages = new ArrayList<>();
        if (records.isEmpty() || templateTable.sizeOfTrArray() == 0) {
            return pages;
        }
        List<Integer> columnWidths = tableColumnWidths(templateTable);
        int dataRowIndex = templateTable.sizeOfTrArray() - 1;
        int headerHeight = 0;
        for (int i = 0; i < dataRowIndex; i++) {
            headerHeight += estimateRowHeight(templateTable.getTrArray(i), columnWidths, records.get(0));
        }
        int availableHeight = Math.max(1200,
                (int) ((pageContentHeight(section) - headerHeight) * 0.50));
        List<Map<String, String>> current = new ArrayList<>();
        int usedHeight = 0;
        for (Map<String, String> record : records) {
            int rowHeight = estimateRowHeight(templateTable.getTrArray(dataRowIndex), columnWidths, record);
            if (!current.isEmpty() && usedHeight + rowHeight > availableHeight) {
                pages.add(current);
                current = new ArrayList<>();
                usedHeight = 0;
            }
            current.add(record);
            usedHeight += rowHeight;
        }
        if (!current.isEmpty()) {
            pages.add(current);
        }
        return pages;
    }
    private List<Integer> tableColumnWidths(CTTbl table) {
        List<Integer> widths = new ArrayList<>();
        CTTblGrid grid = table.getTblGrid();
        if (grid != null) {
            for (int i = 0; i < grid.sizeOfGridColArray(); i++) {
                Object value = grid.getGridColArray(i).getW();
                widths.add(value == null ? 0 : new BigInteger(String.valueOf(value)).intValue());
            }
        }
        if (widths.isEmpty()) {
            for (int width : TABLE_COLUMN_WIDTHS) {
                widths.add(width);
            }
        }
        return widths;
    }
    private int pageContentHeight(CTSectPr section) {
        int height = A4_LANDSCAPE_HEIGHT.intValue();
        int top = PAGE_MARGIN.intValue();
        int bottom = PAGE_MARGIN.intValue();
        if (section != null) {
            if (section.isSetPgSz() && section.getPgSz().getH() != null) {
                height = new BigInteger(String.valueOf(section.getPgSz().getH())).intValue();
            }
            if (section.isSetPgMar()) {
                if (section.getPgMar().getTop() != null) {
                    top = new BigInteger(String.valueOf(section.getPgMar().getTop())).intValue();
                }
                if (section.getPgMar().getBottom() != null) {
                    bottom = new BigInteger(String.valueOf(section.getPgMar().getBottom())).intValue();
                }
            }
        }
        return Math.max(1200, height - top - bottom);
    }
    private int estimateRowHeight(CTRow row, List<Integer> columnWidths, Map<String, String> values) {
        int maxHeight = 0;
        int gridIndex = 0;
        for (CTTc cell : row.getTcList()) {
            int gridSpan = 1;
            if (cell.isSetTcPr() && cell.getTcPr().isSetGridSpan()
                    && cell.getTcPr().getGridSpan().getVal() != null) {
                gridSpan = new BigInteger(String.valueOf(cell.getTcPr().getGridSpan().getVal())).intValue();
            }
            int cellWidth = cellWidthTwips(cell, columnWidths, gridIndex, gridSpan);
            int cellHeight = 0;
            for (CTP paragraph : cell.getPList()) {
                String paragraphText = substitute(paragraphText(paragraph), values);
                double fontSize = paragraphFontSize(paragraph, 9.0);
                double usableWidth = Math.max(8.0, cellWidth / 20.0 - 2.0);
                int lines = Math.max(1, (int) Math.ceil(textUnits(paragraphText) * fontSize / usableWidth));
                cellHeight += (int) Math.ceil(lines * fontSize * 1.28 * 20.0) + 20;
            }
            maxHeight = Math.max(maxHeight, cellHeight);
            gridIndex += gridSpan;
        }
        return Math.max(240, maxHeight);
    }
    private int cellWidthTwips(CTTc cell, List<Integer> columnWidths, int gridIndex, int gridSpan) {
        if (cell.isSetTcPr() && cell.getTcPr().isSetTcW()) {
            Object width = cell.getTcPr().getTcW().getW();
            if (width != null && (!cell.getTcPr().getTcW().isSetType()
                    || cell.getTcPr().getTcW().getType() == STTblWidth.DXA)) {
                return new BigInteger(String.valueOf(width)).intValue();
            }
        }
        int width = 0;
        for (int i = gridIndex; i < gridIndex + gridSpan && i < columnWidths.size(); i++) {
            width += columnWidths.get(i);
        }
        return width > 0 ? width : 1000;
    }
    private String paragraphText(CTP paragraph) {
        StringBuilder builder = new StringBuilder();
        for (CTR run : paragraph.getRList()) {
            for (CTText text : run.getTList()) {
                builder.append(text.getStringValue());
            }
        }
        return builder.toString();
    }
    private double paragraphFontSize(CTP paragraph, double fallback) {
        for (CTR run : paragraph.getRList()) {
            if (run.isSetRPr() && run.getRPr().isSetSz() && run.getRPr().getSz().getVal() != null) {
                return new BigInteger(String.valueOf(run.getRPr().getSz().getVal())).intValue() / 2.0;
            }
        }
        return fallback;
    }
    private double textUnits(String value) {
        double units = 0;
        for (int i = 0; i < value.length(); i++) {
            char character = value.charAt(i);
            if (character == '\r' || character == '\n') {
                continue;
            }
            if (character == ' ') {
                units += 0.5;
            } else if (character >= 0x2e80 || Character.isIdeographic(character)) {
                units += 1.0;
            } else {
                units += 0.58;
            }
        }
        return units;
    }
    private void renderInterleavedUnitTable(CTBody body, CTTbl templateTable,
                                              List<Map<String, String>> records,
                                              Map<Integer, String> unitNames) {
        List<Map<String, String>> repeatedRecords = repeatRecordsByUnit(records, unitNames);
        CTTbl table = body.addNewTbl();
        table.set(templateTable);
        clearTablePosition(table);
        Map<String, String> headerValues = new LinkedHashMap<>(records.get(0));
        headerValues.put("unitDisplay", "联号/联名");
        renderTable(table, headerValues, repeatedRecords);
        configureRecordRows(table, table.sizeOfTrArray() - repeatedRecords.size());
    }
    private List<Map<String, String>> repeatRecordsByUnit(List<Map<String, String>> records,
                                                        Map<Integer, String> unitNames) {
        List<Map<String, String>> repeatedRecords = new ArrayList<>();
        for (Map<String, String> record : records) {
            for (int unit = 1; unit <= UNIT_COUNT; unit++) {
                Map<String, String> values = new LinkedHashMap<>(record);
                String unitName = text(unitNames.get(unit)).trim();
                values.put("unitNo", String.valueOf(unit));
                values.put("unitName", unitName);
                values.put("unitDisplay", "第" + unit + "联"
                        + (StringUtils.hasText(unitName) ? "    " + unitName : ""));
                repeatedRecords.add(values);
            }
        }
        return repeatedRecords;
    }
    private void renderLegacyGroupedUnits(XWPFDocument document,
                                           List<Map<String, String>> records,
                                           Map<Integer, String> unitNames) {
        CTBody body = document.getDocument().getBody();
        List<XmlObject> templateElements = snapshotBodyElements(document);
        if (templateElements.isEmpty()) {
            throw new BusinessException("Engineering order template is empty: " + templateLocation);
        }
        int detailTableIndex = findDetailTableIndex(templateElements);
        if (detailTableIndex < 0) {
            throw new BusinessException("Engineering order template has no detail table: " + templateLocation);
        }
        int bottomTableIndex = findBottomTableIndex(templateElements, detailTableIndex);
        Map<String, String> shared = new LinkedHashMap<>(records.get(0));
        // The first and last template tables are page chrome. Put them into the
        // Word header/footer so Word repeats them on every generated page.
        copyTemplatePrefixToHeader(document, templateElements, detailTableIndex, shared, records);
        if (bottomTableIndex >= 0) {
            copyTemplateTableToFooter(document, (CTTbl) templateElements.get(bottomTableIndex), shared, records);
        }
        CTSectPr section = body.isSetSectPr() ? (CTSectPr) body.getSectPr().copy() : null;
        CTBody cleanBody = CTBody.Factory.newInstance();
        if (section != null) {
            cleanBody.addNewSectPr().set(section);
        }
        body.set(cleanBody);
        for (int unit = 1; unit <= UNIT_COUNT; unit++) {
            if (unit > 1) {
                appendPageBreakParagraph(body);
            }
            String unitName = text(unitNames.get(unit)).trim();
            Map<String, String> unitValues = new LinkedHashMap<>();
            unitValues.put("unitNo", String.valueOf(unit));
            unitValues.put("unitName", unitName);
            unitValues.put("unitDisplay", "第" + unit + "联"
                    + (StringUtils.hasText(unitName) ? "    " + unitName : ""));
            Map<String, String> values = new LinkedHashMap<>(shared);
            values.putAll(unitValues);
            List<Map<String, String>> unitRecords = new ArrayList<>();
            for (Map<String, String> record : records) {
                Map<String, String> unitRecord = new LinkedHashMap<>(record);
                unitRecord.putAll(unitValues);
                unitRecords.add(unitRecord);
            }
            CTTbl table = body.addNewTbl();
            table.set(templateElements.get(detailTableIndex));
            clearTablePosition(table);
            renderTable(table, values, unitRecords);
            configureRecordRows(table, table.sizeOfTrArray() - unitRecords.size());
            configureUnitColumnVerticalMerge(table);
        }
        moveSectionPropertiesToEnd(body);
    }
    private List<XmlObject> snapshotBodyElements(XWPFDocument document) {
        List<XmlObject> elements = new ArrayList<>();
        for (Object element : document.getBodyElements()) {
            if (element instanceof XWPFParagraph) {
                elements.add(((XWPFParagraph) element).getCTP().copy());
            } else if (element instanceof XWPFTable) {
                elements.add(((XWPFTable) element).getCTTbl().copy());
            }
        }
        return elements;
    }
    private int findDetailTableIndex(List<XmlObject> elements) {
        for (int i = 0; i < elements.size(); i++) {
            XmlObject element = elements.get(i);
            if (element instanceof CTTbl && containsDetailPlaceholder((CTTbl) element)) {
                return i;
            }
        }
        return -1;
    }
    private int findBottomTableIndex(List<XmlObject> elements, int detailTableIndex) {
        int bottomTableIndex = -1;
        for (int i = detailTableIndex + 1; i < elements.size(); i++) {
            if (elements.get(i) instanceof CTTbl) {
                bottomTableIndex = i;
            }
        }
        return bottomTableIndex;
    }
    private void renderDetailTables(CTBody body, CTTbl table,
                                   List<Map<String, String>> records,
                                   Map<Integer, String> unitNames) {
        int rowCount = table.sizeOfTrArray();
        if (rowCount == 0) {
            return;
        }
        int headerRowCount = rowCount - 1;
        List<CTRow> headerRows = new ArrayList<>();
        for (int i = 0; i < headerRowCount; i++) {
            headerRows.add((CTRow) table.getTrArray(i).copy());
        }
        CTRow dataRow = (CTRow) table.getTrArray(rowCount - 1).copy();
        CTTbl rowTemplate = (CTTbl) table.copy();
        while (rowTemplate.sizeOfTrArray() > 0) {
            rowTemplate.removeTr(0);
        }
        // Each unit is one continuously paginated table. Word fits as many detail
        // rows per page as their actual rendered heights allow; cantSplit keeps each
        // individual detail row intact across page boundaries.
        for (int unit = 1; unit <= UNIT_COUNT; unit++) {
            String unitName = text(unitNames.get(unit)).trim();
            Map<String, String> unitValues = new LinkedHashMap<>();
            unitValues.put("unitNo", String.valueOf(unit));
            unitValues.put("unitName", unitName);
            unitValues.put("unitDisplay", "\u7b2c" + unit + "\u8054"
                    + (StringUtils.hasText(unitName) ? "    " + unitName : ""));
            CTTbl unitTable = (CTTbl) rowTemplate.copy();
            for (CTRow headerRow : headerRows) {
                CTRow renderedHeader = unitTable.addNewTr();
                renderedHeader.set(headerRow);
                replaceInRow(renderedHeader, unitValues);
            }
            for (Map<String, String> record : records) {
                Map<String, String> values = new LinkedHashMap<>(record);
                values.putAll(unitValues);
                CTRow renderedData = unitTable.addNewTr();
                renderedData.set(dataRow);
                replaceInRow(renderedData, values);
            }
            configureUnitTablePagination(unitTable, unit > 1, headerRows.size());
            body.addNewTbl().set(unitTable);
        }
    }
    private void configureUnitTablePagination(CTTbl table, boolean pageBreakBefore, int headerRowCount) {
        configureRecordRows(table, headerRowCount);
        if (!pageBreakBefore || table.sizeOfTrArray() == 0) {
            return;
        }
        CTRow firstRow = table.getTrArray(0);
        if (firstRow.sizeOfTcArray() == 0) {
            return;
        }
        CTTc firstCell = firstRow.getTcArray(0);
        if (firstCell.sizeOfPArray() == 0) {
            firstCell.addNewP();
        }
        setPageBreakBefore(firstCell.getPArray(0));
    }
    /** Merge the final unit-number column across every row in one unit table. */
    private void configureUnitColumnVerticalMerge(CTTbl table) {
        int rowCount = table.sizeOfTrArray();
        for (int i = 0; i < rowCount; i++) {
            CTRow row = table.getTrArray(i);
            if (row.sizeOfTcArray() == 0) {
                continue;
            }
            CTTc unitCell = row.getTcArray(row.sizeOfTcArray() - 1);
            CTTcPr properties = unitCell.isSetTcPr() ? unitCell.getTcPr() : unitCell.addNewTcPr();
            if (properties.isSetVMerge()) {
                properties.unsetVMerge();
            }
            properties.addNewVMerge().setVal(i == 0 ? STMerge.RESTART : STMerge.CONTINUE);
            if (!properties.isSetTextDirection()) {
                properties.addNewTextDirection();
            }
            properties.getTextDirection().setVal(STTextDirection.BT_LR);
            if (!properties.isSetVAlign()) {
                properties.addNewVAlign();
            }
            properties.getVAlign().setVal(STVerticalJc.CENTER);
        }
    }
    private void configureRecordRows(CTTbl table, int headerRowCount) {
        clearTablePosition(table);
        for (int i = 0; i < table.sizeOfTrArray(); i++) {
            CTRow row = table.getTrArray(i);
            CTTrPr properties = row.isSetTrPr() ? row.getTrPr() : row.addNewTrPr();
            if (properties.sizeOfCantSplitArray() == 0) {
                properties.addNewCantSplit();
            }
            while (properties.sizeOfTblHeaderArray() > 0) {
                properties.removeTblHeader(0);
            }
            if (i < headerRowCount) {
                properties.addNewTblHeader();
            }
            configureCellParagraphPagination(row, false);
        }
    }
    private int tableWidth(CTTbl table) {
        CTTblGrid grid = table.getTblGrid();
        if (grid != null) {
            int width = 0;
            for (int i = 0; i < grid.sizeOfGridColArray(); i++) {
                Object value = grid.getGridColArray(i).getW();
                if (value != null) {
                    width += new BigInteger(String.valueOf(value)).intValue();
                }
            }
            if (width > 0) {
                return width;
            }
        }
        return totalWidth(TABLE_COLUMN_WIDTHS);
    }
    private void noBorder(org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBorder border) {
        border.setVal(STBorder.NIL);
        border.setSz(BigInteger.ZERO);
        border.setSpace(BigInteger.ZERO);
    }
    private void zeroMargin(CTTblWidth margin) {
        margin.setType(STTblWidth.DXA);
        margin.setW(BigInteger.ZERO);
    }
    private void configureCellParagraphPagination(CTRow row, boolean keepWithNext) {
        // Keep the complete row together. Word measures the final rendered text height dynamically,
        // so no fixed row height or per-page record count is needed.
        for (CTTc cell : row.getTcList()) {
            for (CTP paragraph : cell.getPList()) {
                CTPPr properties = paragraph.isSetPPr() ? paragraph.getPPr() : paragraph.addNewPPr();
                if (!properties.isSetKeepLines()) {
                    properties.addNewKeepLines();
                }
                if (properties.isSetKeepNext()) {
                    properties.unsetKeepNext();
                }
                if (keepWithNext) {
                    properties.addNewKeepNext();
                }
            }
        }
    }
    private void copyTemplatePrefixToHeader(XWPFDocument document, List<XmlObject> elements,
                                             int detailTableIndex, Map<String, String> values,
                                             List<Map<String, String>> records) {
        XWPFHeader header = document.createHeader(HeaderFooterType.DEFAULT);
        CTHdrFtr headerContent = header._getHdrFtr();
        for (int i = 0; i < detailTableIndex; i++) {
            XmlObject element = elements.get(i);
            if (element instanceof CTP) {
                CTP paragraph = headerContent.addNewP();
                paragraph.set(element);
                replaceInParagraph(paragraph, values);
            } else if (element instanceof CTTbl) {
                appendHeaderFooterTable(headerContent, (CTTbl) element, values, records);
            }
        }
    }
    private void copyTemplateTableToFooter(XWPFDocument document, CTTbl source,
                                           Map<String, String> values, List<Map<String, String>> records) {
        XWPFFooter footer = document.createFooter(HeaderFooterType.DEFAULT);
        appendHeaderFooterTable(footer._getHdrFtr(), source, values, records);
    }
    private void appendHeaderFooterTable(CTHdrFtr content, CTTbl source,
                                        Map<String, String> values,
                                        List<Map<String, String>> records) {
        CTTbl table = content.addNewTbl();
        table.set(source);
        clearTablePosition(table);
        renderTable(table, values, records);
    }
    private void clearTablePosition(CTTbl table) {
        CTTblPr properties = table.getTblPr() != null ? table.getTblPr() : table.addNewTblPr();
        if (properties.isSetTblpPr()) {
            properties.unsetTblpPr();
        }
    }
    private void renderTable(CTTbl table, Map<String, String> unitValues,
                             List<Map<String, String>> records) {
        boolean expand = containsDetailPlaceholder(table);
        int rowCount = table.sizeOfTrArray();
        if (rowCount == 0) {
            return;
        }
        if (expand) {
            CTRow dataRow = table.getTrArray(rowCount - 1);
            for (CTTc cell : dataRow.getTcList()) {
                CTTcPr properties = cell.isSetTcPr() ? cell.getTcPr() : cell.addNewTcPr();
                if (properties.isSetVMerge()) {
                    properties.unsetVMerge();
                }
            }
        }
        int dataRowIndex = rowCount - 1;
        if (expand) {
            CTRow dataRow = table.getTrArray(dataRowIndex);
            for (int i = 0; i < records.size(); i++) {
                table.addNewTr().set(dataRow);
            }
            table.removeTr(dataRowIndex);
        }
        for (int i = 0; i < table.sizeOfTrArray(); i++) {
            boolean dataRow = expand && i >= table.sizeOfTrArray() - records.size();
            Map<String, String> values = dataRow
                    ? records.get(i - (table.sizeOfTrArray() - records.size()))
                    : unitValues;
            replaceInRow(table.getTrArray(i), values);
        }
    }
    private boolean containsDetailPlaceholder(CTTbl table) {
        StringBuilder builder = new StringBuilder();
        for (CTRow row : table.getTrList()) {
            for (CTTc cell : row.getTcList()) {
                for (CTP paragraph : cell.getPList()) {
                    for (CTR run : paragraph.getRList()) {
                        for (CTText text : run.getTList()) {
                            builder.append(text.getStringValue());
                        }
                    }
                }
            }
        }
        Matcher matcher = TEMPLATE_PLACEHOLDER.matcher(builder.toString());
        while (matcher.find()) {
            String key = matcher.group(1) != null ? matcher.group(1) : matcher.group(2);
            if (DETAIL_PLACEHOLDERS.contains(key.trim())) {
                return true;
            }
        }
        return false;
    }
    private void replaceInRow(CTRow row, Map<String, String> values) {
        for (CTTc cell : row.getTcList()) {
            for (CTP paragraph : cell.getPList()) {
                replaceInParagraph(paragraph, values);
            }
        }
    }
    private void replaceInParagraph(CTP paragraph, Map<String, String> values) {
        List<CTR> runs = paragraph.getRList();
        if (runs.isEmpty()) {
            return;
        }
        List<TextChunk> chunks = new ArrayList<>();
        StringBuilder builder = new StringBuilder();
        for (CTR run : runs) {
            for (CTText text : run.getTList()) {
                String value = text.getStringValue();
                if (value == null) {
                    value = "";
                }
                chunks.add(new TextChunk(text, builder.length(), builder.length() + value.length()));
                builder.append(value);
            }
        }
        String original = builder.toString();
        if (original.indexOf('{') < 0) {
            return;
        }
        Map<CTText, List<TextEdit>> edits = new LinkedHashMap<>();
        Matcher matcher = TEMPLATE_PLACEHOLDER.matcher(original);
        while (matcher.find()) {
            String key = matcher.group(1) != null ? matcher.group(1) : matcher.group(2);
            String replacement = values.get(key.trim());
            if (replacement == null) {
                replacement = "";
            }
            for (TextChunk chunk : chunks) {
                int overlapStart = Math.max(matcher.start(), chunk.start);
                int overlapEnd = Math.min(matcher.end(), chunk.end);
                if (overlapStart >= overlapEnd) {
                    continue;
                }
                String inserted = overlapStart == matcher.start() ? replacement : "";
                edits.computeIfAbsent(chunk.text, item -> new ArrayList<>())
                        .add(new TextEdit(overlapStart - chunk.start, overlapEnd - chunk.start, inserted));
            }
        }
        for (Map.Entry<CTText, List<TextEdit>> entry : edits.entrySet()) {
            String value = text(entry.getKey().getStringValue());
            entry.getValue().sort((left, right) -> Integer.compare(right.start, left.start));
            for (TextEdit edit : entry.getValue()) {
                value = value.substring(0, edit.start) + edit.replacement + value.substring(edit.end);
            }
            entry.getKey().setStringValue(value);
        }
    }
    private static final class TextChunk {
        private final CTText text;
        private final int start;
        private final int end;
        private TextChunk(CTText text, int start, int end) {
            this.text = text;
            this.start = start;
            this.end = end;
        }
    }
    private static final class TextEdit {
        private final int start;
        private final int end;
        private final String replacement;
        private TextEdit(int start, int end, String replacement) {
            this.start = start;
            this.end = end;
            this.replacement = replacement;
        }
    }
    private void replaceInParagraphLegacy(CTP paragraph, Map<String, String> values) {
        List<CTR> runs = paragraph.getRList();
        if (runs.isEmpty()) {
            return;
        }
        StringBuilder builder = new StringBuilder();
        for (CTR run : runs) {
            for (CTText text : run.getTList()) {
                builder.append(text.getStringValue());
            }
        }
        String original = builder.toString();
        if (original.indexOf('{') < 0) {
            return;
        }
        String replaced = substitute(original, values);
        if (replaced.equals(original)) {
            return;
        }
        if (runs.size() == 1) {
            CTR run = runs.get(0);
            if (run.sizeOfTArray() > 0) {
                run.getTArray(0).setStringValue(replaced);
                for (int i = run.sizeOfTArray() - 1; i >= 1; i--) {
                    run.removeT(i);
                }
                return;
            }
        }
        for (CTR run : runs) {
            for (int i = run.sizeOfTArray() - 1; i >= 0; i--) {
                run.removeT(i);
            }
            for (int i = run.sizeOfTabArray() - 1; i >= 0; i--) {
                run.removeTab(i);
            }
            for (int i = run.sizeOfBrArray() - 1; i >= 0; i--) {
                run.removeBr(i);
            }
        }
        runs.get(0).addNewT().setStringValue(replaced);
    }
    private String substitute(String text, Map<String, String> values) {
        Matcher matcher = PLACEHOLDER.matcher(text);
        StringBuffer buffer = new StringBuffer();
        while (matcher.find()) {
            String key = matcher.group(1).trim();
            String value = values.get(key);
            matcher.appendReplacement(buffer, Matcher.quoteReplacement(value == null ? "" : value));
        }
        matcher.appendTail(buffer);
        return buffer.toString();
    }
    private void appendPageBreakParagraph(CTBody body) {
        CTP paragraph = body.addNewP();
        CTPPr properties = paragraph.addNewPPr();
        properties.addNewPageBreakBefore();
        properties.addNewSpacing().setBefore(BigInteger.ZERO);
        properties.getSpacing().setAfter(BigInteger.ZERO);
        properties.addNewRPr().addNewSz().setVal(BigInteger.valueOf(2));
    }
    private void setPageBreakBefore(CTP paragraph) {
        CTPPr properties = paragraph.isSetPPr() ? paragraph.getPPr() : paragraph.addNewPPr();
        if (!properties.isSetPageBreakBefore()) {
            properties.addNewPageBreakBefore();
        }
    }
    private void moveSectionPropertiesToEnd(CTBody body) {
        if (!body.isSetSectPr()) {
            return;
        }
        CTSectPr section = (CTSectPr) body.getSectPr().copy();
        body.unsetSectPr();
        body.setSectPr(section);
    }
    private String collectText(XWPFDocument document) {
        StringBuilder builder = new StringBuilder();
        for (XWPFParagraph paragraph : document.getParagraphs()) {
            builder.append(paragraph.getText()).append('\n');
        }
        for (XWPFTable table : document.getTables()) {
            for (XWPFTableRow row : table.getRows()) {
                for (XWPFTableCell cell : row.getTableCells()) {
                    builder.append(cell.getText()).append('\n');
                }
            }
        }
        return builder.toString();
    }
    private InputStream openTemplate() throws IOException {
        Path path = templatePath();
        if (path != null && Files.exists(path)) {
            return Files.newInputStream(path);
        }
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        try (XWPFDocument document = buildDefaultTemplate()) {
            document.write(buffer);
        }
        return new ByteArrayInputStream(buffer.toByteArray());
    }
    private Path templatePath() {
        if (!StringUtils.hasText(templateLocation)) {
            return null;
        }
        return Paths.get(templateLocation).toAbsolutePath().normalize();
    }
    /** ç”Ÿæˆä¸€ä»½å¸¦å ä½ç¬¦çš„默认工程单模板。 */
    XWPFDocument buildDefaultTemplate() throws IOException {
        XWPFDocument document = new XWPFDocument();
        CTBody body = document.getDocument().getBody();
        CTSectPr section = body.isSetSectPr() ? body.getSectPr() : body.addNewSectPr();
        CTPageSz pageSize = section.isSetPgSz() ? section.getPgSz() : section.addNewPgSz();
        pageSize.setW(A4_LANDSCAPE_WIDTH);
        pageSize.setH(A4_LANDSCAPE_HEIGHT);
        pageSize.setOrient(STPageOrientation.LANDSCAPE);
        CTPageMar margin = section.isSetPgMar() ? section.getPgMar() : section.addNewPgMar();
        margin.setTop(PAGE_MARGIN);
        margin.setBottom(PAGE_MARGIN);
        margin.setLeft(PAGE_MARGIN);
        margin.setRight(PAGE_MARGIN);
        int mainColumns = DEFAULT_COLUMNS.length;
        int columns = mainColumns + 1;
        XWPFTable table = document.createTable(3, columns);
        applyBorders(table.getCTTbl());
        applyGrid(table.getCTTbl(), TABLE_COLUMN_WIDTHS);
        XWPFTableRow headerRow = table.getRow(0);
        mergeMainColumns(headerRow, mainColumns, document);
        setCellText(headerRow.getCell(0), DEFAULT_UNIT_HEADER, true);
        XWPFTableRow titleRow = table.getRow(1);
        XWPFTableRow fieldRow = table.getRow(2);
        for (int i = 0; i < mainColumns; i++) {
            setWidth(titleRow.getCell(i), TABLE_COLUMN_WIDTHS[i]);
            setWidth(fieldRow.getCell(i), TABLE_COLUMN_WIDTHS[i]);
            setCellText(titleRow.getCell(i), DEFAULT_COLUMNS[i][0], true);
            setCellText(fieldRow.getCell(i), DEFAULT_COLUMNS[i][1], false);
        }
        configureUnitColumn(titleRow, mainColumns, false);
        configureUnitColumn(fieldRow, mainColumns, false);
        moveSectionPropertiesToEnd(body);
        document.getProperties().getCoreProperties().setKeywords(TEMPLATE_VERSION);
        return document;
    }
    private void applyBorders(CTTbl table) {
        CTTblPr properties = table.getTblPr() != null ? table.getTblPr() : table.addNewTblPr();
        CTTblBorders borders = properties.isSetTblBorders() ? properties.getTblBorders() : properties.addNewTblBorders();
        border(borders.isSetTop() ? borders.getTop() : borders.addNewTop());
        border(borders.isSetLeft() ? borders.getLeft() : borders.addNewLeft());
        border(borders.isSetBottom() ? borders.getBottom() : borders.addNewBottom());
        border(borders.isSetRight() ? borders.getRight() : borders.addNewRight());
        border(borders.isSetInsideH() ? borders.getInsideH() : borders.addNewInsideH());
        border(borders.isSetInsideV() ? borders.getInsideV() : borders.addNewInsideV());
    }
    private void border(org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBorder border) {
        border.setVal(STBorder.SINGLE);
        border.setSz(BigInteger.valueOf(4));
        border.setColor("000000");
        border.setSpace(BigInteger.ZERO);
    }
    private void applyGrid(CTTbl table, int[] widths) {
        CTTblGrid grid = table.getTblGrid() != null ? table.getTblGrid() : table.addNewTblGrid();
        while (grid.sizeOfGridColArray() > widths.length) {
            grid.removeGridCol(grid.sizeOfGridColArray() - 1);
        }
        while (grid.sizeOfGridColArray() < widths.length) {
            grid.addNewGridCol();
        }
        for (int i = 0; i < widths.length; i++) {
            grid.getGridColArray(i).setW(BigInteger.valueOf(widths[i]));
        }
        CTTblPr properties = table.getTblPr() != null ? table.getTblPr() : table.addNewTblPr();
        CTTblWidth width = properties.isSetTblW() ? properties.getTblW() : properties.addNewTblW();
        width.setType(STTblWidth.DXA);
        width.setW(BigInteger.valueOf(totalWidth(widths)));
    }
    private int totalWidth(int[] widths) {
        int total = 0;
        for (int value : widths) {
            total += value;
        }
        return total;
    }
    private void mergeMainColumns(XWPFTableRow row, int mainColumns, XWPFDocument document) {
        CTRow ctRow = row.getCtRow();
        while (ctRow.sizeOfTcArray() > 2) {
            ctRow.removeTc(ctRow.sizeOfTcArray() - 2);
        }
        CTTc cell = ctRow.getTcArray(0);
        CTTcPr properties = cell.isSetTcPr() ? cell.getTcPr() : cell.addNewTcPr();
        if (!properties.isSetGridSpan()) {
            properties.addNewGridSpan().setVal(BigInteger.valueOf(mainColumns));
        }
        CTTblWidth cellWidth = properties.isSetTcW() ? properties.getTcW() : properties.addNewTcW();
        cellWidth.setType(STTblWidth.DXA);
        cellWidth.setW(BigInteger.valueOf(totalWidth(TABLE_COLUMN_WIDTHS) - UNIT_COLUMN_WIDTH));
        CTTc unitCell = ctRow.getTcArray(1);
        XWPFTableCell unitWrapper = new XWPFTableCell(unitCell, row, document);
        setWidth(unitWrapper, UNIT_COLUMN_WIDTH);
        configureUnitCell(unitCell, true);
        setCellText(unitWrapper, "{unitDisplay}", true);
    }
    private void configureUnitColumn(XWPFTableRow row, int mainColumns, boolean restart) {
        int index = mainColumns;
        XWPFTableCell cell = row.getCell(index);
        setWidth(cell, UNIT_COLUMN_WIDTH);
        configureUnitCell(cell.getCTTc(), restart);
    }
    private void configureUnitCell(CTTc cell, boolean restart) {
        CTTcPr properties = cell.isSetTcPr() ? cell.getTcPr() : cell.addNewTcPr();
        if (!properties.isSetVMerge()) {
            properties.addNewVMerge();
        }
        properties.getVMerge().setVal(restart ? STMerge.RESTART : STMerge.CONTINUE);
        if (!properties.isSetTextDirection()) {
            properties.addNewTextDirection();
        }
        properties.getTextDirection().setVal(STTextDirection.BT_LR);
        if (!properties.isSetVAlign()) {
            properties.addNewVAlign();
        }
        properties.getVAlign().setVal(STVerticalJc.CENTER);
    }
    private void setWidth(XWPFTableCell cell, int width) {
        CTTcPr properties = cell.getCTTc().isSetTcPr() ? cell.getCTTc().getTcPr() : cell.getCTTc().addNewTcPr();
        CTTblWidth cellWidth = properties.isSetTcW() ? properties.getTcW() : properties.addNewTcW();
        cellWidth.setType(STTblWidth.DXA);
        cellWidth.setW(BigInteger.valueOf(width));
    }
    private void setCellText(XWPFTableCell cell, String text, boolean centered) {
        XWPFParagraph paragraph = cell.getParagraphs().isEmpty() ? cell.addParagraph() : cell.getParagraphs().get(0);
        if (centered) {
            paragraph.setAlignment(org.apache.poi.xwpf.usermodel.ParagraphAlignment.CENTER);
        }
        org.apache.poi.xwpf.usermodel.XWPFRun run = paragraph.createRun();
        run.setFontFamily("SimSun");
        run.setFontSize(9);
        run.setText(text);
        for (XWPFParagraph extra : new ArrayList<>(cell.getParagraphs())) {
            if (extra != paragraph) {
                cell.removeParagraph(cell.getParagraphs().indexOf(extra));
            }
        }
    }
    private static String text(String value) {
        return value == null ? "" : value;
    }
    private static String date(LocalDate value) {
        return value == null ? "" : DATE_FORMAT.format(value);
    }
    private static String yesNo(Integer value) {
        return value != null && value == 1 ? "是" : "否";
    }
    private static String firstText(String... candidates) {
        for (String candidate : candidates) {
            if (StringUtils.hasText(candidate)) {
                return candidate;
            }
        }
        return "";
    }
}
emergency-repair-server/src/main/java/com/emergencyrepair/system/service/ButtonPermissionCatalog.java
@@ -56,6 +56,10 @@
                def("fault:completion:query", "\u67e5\u8be2"),
                def("fault:completion:status", "\u5b8c\u5de5\u72b6\u6001\u8bbe\u7f6e"),
                def("fault:completion:score", "rk_T\u5458\u8bc4\u5206"));
        put("/fault/engineering-print",
                def("fault:engineeringPrint:query", "\u67e5\u8be2"),
                def("fault:engineeringPrint:print", "\u6253\u5370"),
                def("fault:engineeringPrint:print2", "\u6253\u5370\u5de5\u7a0b\u53552"));
        put("/completion/report",
                def("completion:report:save", "\u4fdd\u5b58"),
                def("completion:report:submit", "\u63d0\u4ea4"));
emergency-repair-server/src/main/resources/application.yml
@@ -1,5 +1,3 @@
fault-code:
  prefix: "固定前缀-"
app:
  seed-demo-data: false
server:
@@ -11,6 +9,8 @@
      force: true
spring:
  config:
    import: optional:file:./config/business-config.yml
  application:
    name: emergency-repair-server
  datasource:
emergency-repair-web/.env
New file
@@ -0,0 +1,11 @@
# å‰ç«¯é…ç½®ï¼ˆä¿®æ”¹åŽéœ€é‡å¯ npm run serve / é‡æ–° npm run build ç”Ÿæ•ˆï¼‰
# å‰ç«¯å¼€å‘服务器端口
PORT=8082
# å‰ç«¯è®¿é—®çš„后端地址(开发环境 /api ä»£ç†ç›®æ ‡ï¼Œæ ¼å¼ http://ip:端口)
VUE_APP_API_TARGET=http://127.0.0.1:8091
# å‰ç«¯è¯·æ±‚后端的基础路径;默认 /api(走开发代理或部署时的反向代理),
# å¦‚需前端直连后端可改为 http://ip:端口/api
VUE_APP_API_BASE_URL=/api
emergency-repair-web/public/dict.js
@@ -5,14 +5,14 @@
window.__APP_DICT__ = {
  taskStatuses: {
    UNREPORTED: { text: '未上报', type: 'info' },
    REPORTED_XCB: { text: '已上报rk_XCB', type: 'warning' },
    REPORTED_XCB: { text: '故障已上报', type: 'warning' },
    RETURNED: { text: '上级打回', type: 'danger' },
    XCB_REVIEWED: { text: 'rk_XCB助理审核完成', type: 'primary' },
    CMB_APPROVED: { text: 'rk_CMB领导审批完成', type: 'success' },
    JCC_ASSISTANT_APPROVED: { text: 'rk_JCC助理审批完成', type: 'success' },
    JCC_DIRECTOR_APPROVED: { text: 'rk_JCC处长已阅', type: 'success' },
    XCB_REVIEWED: { text: '第一审批人审核完成审核完成', type: 'primary' },
    CMB_APPROVED: { text: '第二审批人审批完成', type: 'success' },
    JCC_ASSISTANT_APPROVED: { text: '第三审批人审批完成', type: 'success' },
    JCC_DIRECTOR_APPROVED: { text: '第四审批人已阅', type: 'success' },
    DISPATCHED: { text: '已派单', type: 'primary' },
    CANCELLED: { text: '已销项', type: 'info' }
    CANCELLED: { text: '销项', type: 'info' }
  },
  completionStatuses: {
    UNFINISHED: { text: '未完工', type: 'info' },
emergency-repair-web/public/sensitive-words.json
@@ -2,7 +2,7 @@
  "sensitive_words": [
    {
      "codename": "rk_SystemName",
      "real_name": "临抢修管理系统",
      "real_name": "维修管理系统",
      "scope": "ui",
      "note": "System title shown in the login page and application header."
    },
@@ -14,56 +14,55 @@
    },
    {
      "codename": "rk_X号",
      "real_name": "县号",
      "real_name": "线号",
      "scope": "ui",
      "note": "Development mode displays the codename. Replace real_name only in the deployed production dictionary."
    },
    {
      "codename": "rk_JC",
      "real_name": "JC",
    {
      "codename": "rk_第一审批人",
      "real_name": "第一审批人",
      "scope": "ui",
      "note": "JC信息 = JC information."
      "note": "XCB assistant review and dispatch."
    },
    {
      "codename": "rk_JCC",
      "real_name": "JCC",
      "codename": "rk_第二审批人",
      "real_name": "第二审批人",
      "scope": "ui",
      "note": "CMB leader approval role."
    },
    {
      "codename": "rk_第三审批人",
      "real_name": "第三审批人",
      "scope": "ui",
      "note": "JCC assistant, director, and acceptance roles."
    },
    {
      "codename": "rk_CMB",
      "real_name": "CMB",
      "codename": "rk_第四审批人",
      "real_name": "第四审批人",
      "scope": "ui",
      "note": "CMB leader approval role."
      "note": "JCC assistant, director, and acceptance roles."
    },
    {
      "codename": "rk_TD",
      "real_name": "TD",
      "real_name": "梯队",
      "scope": "ui",
      "note": "Home page statistic column."
    },
    {
      "codename": "rk_T",
      "real_name": "T",
      "real_name": "梯",
      "scope": "ui",
      "note": "Word root. T队专业, T内施工, and T员 all use the codename in development."
    },
    {
      "codename": "rk_XCB",
      "real_name": "修船版",
      "scope": "ui",
      "note": "XCB assistant review and dispatch."
    },
    {
      "codename": "rk_H",
      "real_name": "H",
      "real_name": "黑",
      "scope": "ui",
      "note": "Development mode displays H only. Replace real_name after production deployment."
    }
  ],
  "config": {
    "environment": "development",
    "rule": "Build and deploy the normal development package. After deployment, manually replace the real_name values in the deployed dist/sensitive-words.json file in the production environment. Do not rebuild the frontend after that replacement.",
    "code_pre": "ABC"
    "rule": "Build and deploy the normal development package. After deployment, manually replace the real_name values in the deployed dist/sensitive-words.json file in the production environment. Do not rebuild the frontend after that replacement."
  }
}
emergency-repair-web/src/api/index.js
@@ -2,7 +2,7 @@
import { Message } from 'element-ui'
const api = axios.create({
  baseURL: '/api',
  baseURL: process.env.VUE_APP_API_BASE_URL,
  timeout: 20000
})
emergency-repair-web/src/router/index.js
@@ -13,12 +13,13 @@
    children: [
      { path: 'dashboard', component: () => import('@/views/Dashboard.vue'), meta: { title: '首页门户', cacheName: 'Dashboard' } },
      { path: 'fault/report', component: () => import('@/views/fault/FaultReport.vue'), meta: { title: '故障上报', cacheName: 'FaultReport' } },
      { path: 'fault/review', component: () => import('@/views/fault/XcbReview.vue'), meta: { title: 'rk_XCB助理审核', cacheName: 'XcbReview' } },
      { path: 'fault/approval/cmb', component: () => import('@/views/fault/SimpleApproval.vue'), props: { stage: 'CMB' }, meta: { title: 'rk_CMB领导审批', stage: 'CMB', cacheName: 'SimpleApproval' } },
      { path: 'fault/approval/jcc-assistant', component: () => import('@/views/fault/SimpleApproval.vue'), props: { stage: 'JCC_ASSISTANT' }, meta: { title: 'rk_JCC助理审批', stage: 'JCC_ASSISTANT', cacheName: 'SimpleApproval' } },
      { path: 'fault/approval/jcc-director', component: () => import('@/views/fault/SimpleApproval.vue'), props: { stage: 'JCC_DIRECTOR' }, meta: { title: 'rk_JCC处长审批', stage: 'JCC_DIRECTOR', cacheName: 'SimpleApproval' } },
      { path: 'fault/dispatch', component: () => import('@/views/fault/DispatchPage.vue'), meta: { title: 'rk_XCB助理派单', cacheName: 'DispatchPage' } },
      { path: 'fault/review', component: () => import('@/views/fault/XcbReview.vue'), meta: { title: 'rk_第一审批人审核', cacheName: 'XcbReview' } },
      { path: 'fault/approval/cmb', component: () => import('@/views/fault/SimpleApproval.vue'), props: { stage: 'CMB' }, meta: { title: 'rk_第二审批人审批', stage: 'CMB', cacheName: 'SimpleApproval' } },
      { path: 'fault/approval/jcc-assistant', component: () => import('@/views/fault/SimpleApproval.vue'), props: { stage: 'JCC_ASSISTANT' }, meta: { title: 'rk_第三审批人审批', stage: 'JCC_ASSISTANT', cacheName: 'SimpleApproval' } },
      { path: 'fault/approval/jcc-director', component: () => import('@/views/fault/SimpleApproval.vue'), props: { stage: 'JCC_DIRECTOR' }, meta: { title: 'rk_第四审批人审批', stage: 'JCC_DIRECTOR', cacheName: 'SimpleApproval' } },
      { path: 'fault/dispatch', component: () => import('@/views/fault/DispatchPage.vue'), meta: { title: 'rk_第一审批人派单', cacheName: 'DispatchPage' } },
      { path: 'fault/completion', component: () => import('@/views/completion/CompletionList.vue'), meta: { title: '完工情况处理', cacheName: 'CompletionList' } },
      { path: 'fault/engineering-print', component: () => import('@/views/fault/EngineeringPrint.vue'), meta: { title: '工程单打印', cacheName: 'EngineeringPrint' } },
      { path: 'completion/report', component: () => import('@/views/completion/CompletionReport.vue'), props: route => ({ workOrderId: route.query.workOrderId || null }), meta: { title: '完工信息上报', cacheName: 'CompletionReport' } },
      { path: 'completion/query', component: () => import('@/views/completion/CompletionQuery.vue'), meta: { title: '完工信息查阅', cacheName: 'CompletionQuery' } },
      { path: 'acceptance/application', component: () => import('@/views/acceptance/AcceptanceList.vue'), props: { stage: 'APPLICATION' }, meta: { title: '验收申请', stage: 'APPLICATION', cacheName: 'AcceptanceList' } },
emergency-repair-web/src/utils/dict.js
@@ -2,10 +2,10 @@
  UNREPORTED: { text: '未上报', type: 'info' },
  REPORTED_XCB: { text: '已上报rk_XCB', type: 'warning' },
  RETURNED: { text: '上级打回', type: 'danger' },
  XCB_REVIEWED: { text: 'rk_XCB助理审核完成', type: 'primary' },
  CMB_APPROVED: { text: 'rk_CMB领导审批完成', type: 'success' },
  JCC_ASSISTANT_APPROVED: { text: 'rk_JCC助理审批完成', type: 'success' },
  JCC_DIRECTOR_APPROVED: { text: 'rk_JCC处长已阅', type: 'success' },
  XCB_REVIEWED: { text: 'rk_第一审批人审核完成', type: 'primary' },
  CMB_APPROVED: { text: 'rk_第二审批人审批完成', type: 'success' },
  JCC_ASSISTANT_APPROVED: { text: 'rk_第三审批人审批完成', type: 'success' },
  JCC_DIRECTOR_APPROVED: { text: 'rk_第四审批人已阅', type: 'success' },
  DISPATCHED: { text: '已派单', type: 'primary' },
  CANCELLED: { text: '已销项', type: 'info' }
}
emergency-repair-web/src/views/Dashboard.vue
@@ -28,7 +28,7 @@
        <div class="panel-head"><h3>常用功能</h3></div>
        <div class="quick-list">
          <button @click="$router.push('/fault/report')"><i class="el-icon-edit-outline"></i><span>故障上报</span></button>
          <button @click="$router.push('/fault/review')"><i class="el-icon-s-check"></i><span>rk_XCB助理审核</span></button>
          <button @click="$router.push('/fault/review')"><i class="el-icon-s-check"></i><span>rk_第一审批人审核</span></button>
          <button @click="$router.push('/fault/dispatch')"><i class="el-icon-s-promotion"></i><span>一键派单</span></button>
          <button @click="$router.push('/fault/completion')"><i class="el-icon-finished"></i><span>完工处理</span></button>
          <button @click="$router.push('/acceptance/application')"><i class="el-icon-document-checked"></i><span>验收申请</span></button>
emergency-repair-web/src/views/fault/DispatchPage.vue
@@ -2,8 +2,8 @@
  <div class="page">
    <div class="page-title">
      <div>
        <h2>rk_XCB助理派单</h2>
        <p>显示rk_JCC处长审批完成且未派单的故障,按承修单位一键生成派工单</p>
        <h2>rk_第一审批人派单</h2>
        <p>显示rk_第四审批人审批完成且未派单的故障,按承修单位一键生成派工单</p>
      </div>
      <el-button type="primary" icon="el-icon-s-promotion" :disabled="selection.length === 0" @click="dispatch">一键派单</el-button>
    </div>
emergency-repair-web/src/views/fault/EngineeringPrint.vue
New file
@@ -0,0 +1,272 @@
<template>
  <div class="page engineering-print-page">
    <div class="page-title">
      <div>
        <h2>工程单打印</h2>
        <p>按查询条件筛选故障记录,勾选后套用 Word æ¨¡æ¿ç”Ÿæˆå››è”工程单,明细可自动多页</p>
      </div>
    </div>
    <div class="toolbar query-toolbar">
      <span class="filter-label">rk_X号</span>
      <el-select v-model="query.xNo" placeholder="请选择rk_X号" style="width:110px">
        <el-option v-for="item in xNumbers" :key="item" :label="item" :value="item"></el-option>
      </el-select>
      <span class="filter-label">rk_T队专业</span>
      <el-select v-model="query.teamMajor" placeholder="请选择rk_T队专业" style="width:130px">
        <el-option v-for="item in teamMajors" :key="item.id" :label="item.name" :value="item.name"></el-option>
      </el-select>
      <span class="filter-label">工程专业</span>
      <el-select v-model="query.engineeringMajor" placeholder="全部" clearable style="width:130px">
        <el-option v-for="item in engineeringMajors" :key="item.id" :label="item.name" :value="item.name"></el-option>
      </el-select>
      <span class="filter-label">上报单位</span>
      <el-input v-model="query.reportUnit" placeholder="单位名称" clearable style="width:150px" @keyup.enter.native="search"></el-input>
      <span class="filter-label">负责人</span>
      <el-input v-model="query.owner" placeholder="负责人" clearable style="width:110px" @keyup.enter.native="search"></el-input>
      <span class="filter-label">上报日期</span>
      <el-date-picker
        v-model="query.reportDateRange"
        type="daterange"
        value-format="yyyy-MM-dd"
        range-separator="至"
        start-placeholder="开始日期"
        end-placeholder="结束日期"
        style="width:300px"
      ></el-date-picker>
      <div class="filter-break"></div>
      <span class="filter-label">任务状态</span>
      <el-select v-model="query.status" placeholder="全部" clearable style="width:160px">
        <el-option v-for="(meta, key) in taskStatuses" :key="key" :label="meta.text" :value="key"></el-option>
      </el-select>
      <el-input v-model="query.keyword" placeholder="故障编号/设备名称/工程名称及损坏情况" clearable style="width:320px" @keyup.enter.native="search"></el-input>
      <el-button type="primary" icon="el-icon-search" @click="search">查询</el-button>
      <el-button icon="el-icon-refresh-left" @click="resetQuery">重置</el-button>
      <el-button icon="el-icon-refresh" @click="load">刷新</el-button>
    </div>
    <div class="toolbar action-toolbar">
      <span class="selection-tip">已选择 {{ selection.length }} æ¡</span>
      <div class="spacer"></div>
      <el-button type="primary" icon="el-icon-printer" :loading="printing" :disabled="selection.length === 0" @click="print">打印工程单</el-button>
      <el-button type="success" icon="el-icon-printer" :loading="printingV2" :disabled="selection.length === 0" @click="printV2">打印工程单2</el-button>
    </div>
    <div class="table-panel">
      <el-table
        ref="table"
        :data="rows"
        border
        stripe
        height="100%"
        :header-cell-style="{ textAlign: 'center' }"
        @selection-change="onSelectionChange"
      >
        <el-table-column type="selection" width="46"></el-table-column>
        <el-table-column prop="taskStatus" label="任务状态" width="130" fixed>
          <template slot-scope="{ row }"><el-tag size="mini" :type="taskMeta(row.taskStatus).type">{{ taskMeta(row.taskStatus).text }}</el-tag></template>
        </el-table-column>
        <el-table-column prop="faultCode" label="故障编号" width="180" fixed></el-table-column>
        <el-table-column prop="xNo" label="rk_X号" width="70"></el-table-column>
        <el-table-column prop="equipmentName" label="设备名称" min-width="210" show-overflow-tooltip></el-table-column>
        <el-table-column prop="engineeringNameDesc" label="工程名称及损坏情况" min-width="220" show-overflow-tooltip></el-table-column>
        <el-table-column prop="repairRequirement" label="修理技术要求" min-width="200" show-overflow-tooltip></el-table-column>
        <el-table-column prop="engineeringMajor" label="工程专业" width="100"></el-table-column>
        <el-table-column prop="teamMajor" label="rk_T队专业" width="100"></el-table-column>
        <el-table-column prop="owner" label="负责人" width="90"></el-table-column>
        <el-table-column prop="reportUnit" label="上报单位" min-width="130"></el-table-column>
        <el-table-column prop="reportDate" label="上报日期" width="110" align="center"></el-table-column>
        <el-table-column prop="requestedFinishDate" label="要求完工日期" width="120" align="center"></el-table-column>
      </el-table>
    </div>
    <div class="pagination-bar">
      <el-pagination
        background
        :current-page="pagination.current"
        :page-size="pagination.size"
        :page-sizes="[20, 50, 100]"
        :total="pagination.total"
        layout="total, sizes, prev, pager, next, jumper"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
      ></el-pagination>
    </div>
  </div>
</template>
<script>
import api from '@/api'
import { taskStatuses, statusMeta } from '@/utils/dict'
export default {
  name: 'EngineeringPrint',
  data() {
    return {
      taskStatuses,
      query: { keyword: '', status: '', xNo: '', teamMajor: '', engineeringMajor: '', reportUnit: '', owner: '', reportDateRange: [] },
      rows: [],
      selection: [],
      pagination: { current: 1, size: 20, total: 0 },
      printing: false,
      printingV2: false,
      teamMajors: [],
      engineeringMajors: [],
      xNumbers: []
    }
  },
  created() {
    this.init()
  },
  methods: {
    taskMeta(value) { return statusMeta(taskStatuses, value) },
    async init() {
      try {
        await Promise.all([this.loadXNumbers(), this.loadMajors()])
      } catch (e) {}
      this.applyDefaultFilters()
      this.load()
    },
    loadXNumbers() {
      return api.get('/system/tree/product/x-numbers').then(res => { this.xNumbers = res.data || [] })
    },
    loadMajors() {
      const team = api.get('/system/team-majors').then(res => { this.teamMajors = res.data || [] })
      const engineering = api.get('/system/engineering-majors').then(res => { this.engineeringMajors = res.data || [] })
      return Promise.all([team, engineering])
    },
    applyDefaultFilters() {
      if (this.xNumbers.length && !this.xNumbers.includes(this.query.xNo)) {
        this.query.xNo = this.xNumbers[0]
      }
      const teamNames = this.teamMajors.map(item => item.name)
      if (teamNames.length && !teamNames.includes(this.query.teamMajor)) {
        this.query.teamMajor = teamNames[0]
      }
    },
    queryParams() {
      const range = this.query.reportDateRange || []
      return {
        keyword: this.query.keyword,
        status: this.query.status,
        xNo: this.query.xNo,
        teamMajor: this.query.teamMajor,
        engineeringMajor: this.query.engineeringMajor,
        reportUnit: this.query.reportUnit,
        owner: this.query.owner,
        reportDateStart: range[0] || '',
        reportDateEnd: range[1] || ''
      }
    },
    load() {
      const params = {
        ...this.queryParams(),
        current: this.pagination.current,
        size: this.pagination.size
      }
      return api.get('/faults/page', { params }).then(res => {
        const page = res.data || {}
        this.rows = page.records || []
        this.pagination.total = Number(page.total || 0)
        if (!this.rows.length && this.pagination.current > 1 && this.pagination.total > 0) {
          this.pagination.current -= 1
          this.load()
        }
      })
    },
    search() {
      this.pagination.current = 1
      this.load()
    },
    resetQuery() {
      this.query = { keyword: '', status: '', xNo: '', teamMajor: '', engineeringMajor: '', reportUnit: '', owner: '', reportDateRange: [] }
      this.applyDefaultFilters()
      this.search()
    },
    handleCurrentChange(current) {
      this.pagination.current = current
      this.load()
    },
    handleSizeChange(size) {
      this.pagination.size = size
      this.pagination.current = 1
      this.load()
    },
    onSelectionChange(rows) {
      this.selection = rows
    },
    print() {
      this.printDocument('/faults/engineering-print', '工程单.docx', 'printing')
    },
    printV2() {
      this.printDocument('/faults/engineering-print-v2', '工程单2.docx', 'printingV2')
    },
    printDocument(url, filename, loadingKey) {
      const ids = this.selection.map(item => item.id)
      if (!ids.length) return
      this[loadingKey] = true
      return api.post(url, { ids }, { responseType: 'blob' })
        .then(res => this.saveDocument(res, filename))
        .catch(() => {})
        .finally(() => { this[loadingKey] = false })
    },
    async saveDocument(res, filename) {
      const blob = res instanceof Blob ? res : new Blob([res])
      if (blob.type && blob.type.indexOf('json') >= 0) {
        const text = await blob.text()
        let message = '生成工程单失败'
        try {
          message = JSON.parse(text).message || message
        } catch (e) {}
        this.$message.error(message)
        return
      }
      const link = document.createElement('a')
      link.href = URL.createObjectURL(blob)
      link.download = filename || '工程单.docx'
      link.click()
      URL.revokeObjectURL(link.href)
      this.$message.success((filename || '工程单').replace('.docx', '') + '已生成')
    }
  }
}
</script>
<style scoped>
.engineering-print-page {
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 0;
  overflow: hidden;
}
.engineering-print-page .page-title,
.engineering-print-page .toolbar,
.engineering-print-page .pagination-bar {
  flex: 0 0 auto;
}
.engineering-print-page .table-panel {
  flex: 1 1 auto;
  min-height: 0;
}
.engineering-print-page ::v-deep .el-table__body-wrapper,
.engineering-print-page ::v-deep .el-table__fixed-body-wrapper {
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.engineering-print-page ::v-deep .el-table__body-wrapper::-webkit-scrollbar,
.engineering-print-page ::v-deep .el-table__fixed-body-wrapper::-webkit-scrollbar {
  display: none;
  width: 0;
  height: 0;
}
.query-toolbar { margin-bottom: 8px; }
.filter-break { flex-basis: 100%; height: 0; }
.query-toolbar ::v-deep .el-range-separator { flex: 0 0 18px; min-width: 18px; }
.action-toolbar { min-height: 50px; }
.filter-label { margin-left: 4px; color: #4d6073; font-size: 13px; white-space: nowrap; }
.selection-tip { color: #6d7f91; font-size: 13px; }
.selection-hint { margin-left: 12px; color: #9aa7b4; font-size: 12px; }
.pagination-bar { display: flex; justify-content: flex-end; padding: 12px 0 4px; }
@media (max-width: 900px) {
  .query-toolbar .el-date-editor { width: 100% !important; }
}
</style>
emergency-repair-web/src/views/fault/SimpleApproval.vue
@@ -66,7 +66,7 @@
        <el-form-item label="审批意见">
          <el-input v-model="form.opinion" type="textarea" :rows="3" placeholder="请填写审批意见"></el-input>
        </el-form-item>
        <el-alert v-if="form.result === 'REJECT'" :closable="false" type="warning" show-icon title="审批不通过时直接退回rk_XCB助理,任务状态变为“上级打回”。"></el-alert>
        <el-alert v-if="form.result === 'REJECT'" :closable="false" type="warning" show-icon title="审批不通过时直接退回rk_第一审批人,任务状态变为“上级打回”。"></el-alert>
      </el-form>
      <template slot="footer">
        <el-button @click="dialogVisible = false">取消</el-button>
@@ -89,13 +89,13 @@
  },
  computed: {
    pageTitle() {
      return { CMB: 'rk_CMB领导审批', JCC_ASSISTANT: 'rk_JCC助理审批', JCC_DIRECTOR: 'rk_JCC处长审批' }[this.stage]
      return { CMB: 'rk_第二审批人审批', JCC_ASSISTANT: 'rk_第三审批人审批', JCC_DIRECTOR: 'rk_第四审批人审批' }[this.stage]
    },
    subtitle() {
      return {
        CMB: '对rk_XCB助理审核完成的故障进行批量通过或退回',
        JCC_ASSISTANT: '对rk_CMB领导审批完成的故障进行批量通过或退回',
        JCC_DIRECTOR: '对rk_JCC助理审批完成的故障进行最终审批,通过后进入派单'
        CMB: '对rk_第一审批人审核完成的故障进行批量通过或退回',
        JCC_ASSISTANT: '对rk_第二审批人审批完成的故障进行批量通过或退回',
        JCC_DIRECTOR: '对rk_第三审批人审批完成的故障进行最终审批,通过后进入派单'
      }[this.stage]
    },
    status() {
emergency-repair-web/src/views/fault/XcbReview.vue
@@ -2,8 +2,8 @@
  <div class="page">
    <div class="page-title">
      <div>
        <h2>rk_XCB助理审核</h2>
        <p>分配承修单位、跟踪技术队和要求完工日期,审核后进入rk_CMB领导审批</p>
        <h2>rk_第一审批人审核</h2>
        <p>分配承修单位、跟踪技术队和要求完工日期,审核后进入rk_第二审批人审批</p>
      </div>
    </div>
@@ -97,7 +97,7 @@
    <fault-edit-dialog :visible.sync="editVisible" :fault="editingFault" @saved="onFaultSaved"></fault-edit-dialog>
    <el-dialog title="rk_XCB助理审核" :visible.sync="dialogVisible" width="820px" append-to-body>
    <el-dialog title="rk_第一审批人审核" :visible.sync="dialogVisible" width="820px" append-to-body>
      <el-alert :closable="false" type="info" show-icon title="承修单位可多选。排序前7个作为常用承修单位优先显示,失信单位标记提醒。" style="margin-bottom:14px"></el-alert>
      <el-form :model="form" label-width="105px">
        <el-form-item label="已选故障">
@@ -191,7 +191,7 @@
      cancelReason: '',
      cancelling: false,
      saving: false,
      trackingTeams: ['T01队', 'T02队', 'T03队'],
      trackingTeams: ['技术队1', '技术队2', '技术队3'],
      form: { contractorIds: [], trackingTeam: 'T01队', requestedFinishDate: '', opinion: '' }
    }
  },
@@ -292,7 +292,7 @@
      api.post('/faults/cancel', {
        ids: this.selection.map(item => item.id),
        reason: this.cancelReason.trim(),
        cancelledBy: sessionStorage.getItem('realName') || 'rk_XCB助理'
        cancelledBy: sessionStorage.getItem('realName') || 'rk_第一审批人'
      }).then(() => {
        this.$message.success('销项完成')
        this.cancelVisible = false
emergency-repair-web/vue.config.js
@@ -1,11 +1,26 @@
// å‰ç«¯ç«¯å£ã€åŽç«¯åœ°å€ç»Ÿä¸€åœ¨é¡¹ç›®æ ¹ç›®å½• .env ä¸­é…ç½®
const devPort = Number(process.env.PORT)
const apiTarget = process.env.VUE_APP_API_TARGET
const apiBase = process.env.VUE_APP_API_BASE_URL
if (!devPort) {
  throw new Error('缺少前端端口配置:请在 emergency-repair-web/.env ä¸­è®¾ç½® PORT')
}
if (!apiTarget) {
  throw new Error('缺少后端地址配置:请在 emergency-repair-web/.env ä¸­è®¾ç½® VUE_APP_API_TARGET')
}
if (!apiBase) {
  throw new Error('缺少接口基础路径配置:请在 emergency-repair-web/.env ä¸­è®¾ç½® VUE_APP_API_BASE_URL')
}
module.exports = {
  parallel: false,
  devServer: {
    port: 8082,
    port: devPort,
    headers: { 'Cache-Control': 'no-store' },
    proxy: {
      '/api': {
        target: 'http://127.0.0.1:8091',
        target: apiTarget,
        changeOrigin: true
      }
    }
requirement/½»½ÓÎļþ_2026.10.3.md
@@ -796,7 +796,7 @@
| é—®é¢˜ | çŠ¶æ€ | æ ¹å› ä¸Žä¿®å¤ |
| --- | --- | --- |
| `rk_CMB领导审批` é€šè¿‡åŽè¿›å…¥ `rk_JCC助理审批`,列表没有数据 | å·²ä¿®å¤ | CMB、JCC åŠ©ç†ã€JCC å¤„长共用 `SimpleApproval.vue` å’ŒåŒä¸€ keep-alive ç¼“存名;原组件仅在 `created()` æŸ¥è¯¢ä¸€æ¬¡ï¼Œåˆ‡æ¢ `stage` æ—¶æ²¡æœ‰é‡æ–°è¯·æ±‚。现监听 `stage`,切换时清空旧列表并立即按新状态查询;缓存页签再次激活时也会刷新 |
| `rk_第二审批人审批` é€šè¿‡åŽè¿›å…¥ `rk_第三审批人审批`,列表没有数据 | å·²ä¿®å¤ | CMB、JCC åŠ©ç†ã€JCC å¤„长共用 `SimpleApproval.vue` å’ŒåŒä¸€ keep-alive ç¼“存名;原组件仅在 `created()` æŸ¥è¯¢ä¸€æ¬¡ï¼Œåˆ‡æ¢ `stage` æ—¶æ²¡æœ‰é‡æ–°è¯·æ±‚。现监听 `stage`,切换时清空旧列表并立即按新状态查询;缓存页签再次激活时也会刷新 |
### 21.1 ä¸»è¦å˜æ›´æ–‡ä»¶
requirement/¾ö²ß±í_´ýÈ·ÈÏÏî.md
@@ -23,7 +23,7 @@
>
> **2026-09-16 ä¸Šçº§æ‰“回口径**:不逐级打回;CMB领导、JCC助理、JCC处长审批不通过时均直接退回XCB助理,任务状态为“上级打回”;该状态与“已上报XCB”业务语义等价,但作为不同状态值保存,以区分流转轨迹。XCB助理本环节审核不通过时不转“上级打回”:内容不合格用“修改”处理,不作为故障处理时点“销项”。
>
> **2026-09-14 ç”¨æˆ·è¡¥å……确认**:① è‰‡é•¿çŽ¯èŠ‚ä¸çº³å…¥ç¬¬ä¸€ç‰ˆï¼›â‘¡ æŠ¥ä»·å¤„理移出主流程、“报价管理”菜单与模块不做;③ JCC åŠ©ç†å®¡æ‰¹ä¸ºå¿…ç»çŽ¯èŠ‚ï¼›â‘£ ç¦»çº¿/数据包相关功能整体取消、对应菜单项一并删除(C-12 å·²è§£å†³ï¼‰ï¼›â‘¤ AXU2 æ•°æ®åŒ…导入无需核对;⑥ C-15 åŽŸå£å¾„ä¸ºç»Ÿä¸€å†™â€œT员验收”;2026-09-20 éªŒæ”¶æµç¨‹æ”¹ä¸º5菜单后,该名称不再作为菜单名,“T员”角色代称仍保留。
> **2026-09-14 ç”¨æˆ·è¡¥å……确认**:① T长环节不纳入第一版;② æŠ¥ä»·å¤„理移出主流程、“报价管理”菜单与模块不做;③ JCC åŠ©ç†å®¡æ‰¹ä¸ºå¿…ç»çŽ¯èŠ‚ï¼›â‘£ ç¦»çº¿/数据包相关功能整体取消、对应菜单项一并删除(C-12 å·²è§£å†³ï¼‰ï¼›â‘¤ AXU2 æ•°æ®åŒ…导入无需核对;⑥ C-15 åŽŸå£å¾„ä¸ºç»Ÿä¸€å†™â€œT员验收”;2026-09-20 éªŒæ”¶æµç¨‹æ”¹ä¸º5菜单后,该名称不再作为菜单名,“T员”角色代称仍保留。
## A. ç•Œé¢è¿˜åŽŸé€å¡æ ¸å¯¹ï¼ˆå¯¹ç…§ requirement\界面还原.html;AXU15 å·²äºŽ 2026-08-26 æ ¸å¯¹å®Œæˆï¼‰
@@ -67,7 +67,7 @@
5. ã€å·²è§£å†³ 2026-09-20】“键派单”确认应为“一键派单”;修改、生成临修工程单、删除亚复(疑为清除重复)等原截图业务按钮暂不需要。
6. æ— æˆªå›¾æ¨¡å—页面结构:监测信息、查询(工程单/工程项目/故障信息/销项)、报价管理(导出待报价/修船办初审/JCC审核)、基础信息(JC信息/用户数据范围)——是否有参考原型/纸质稿,或授权自行设计;系统管理菜单已确认包含承修单位管理、产品结构树管理、设备基础库管理;统计报表菜单已确认6项功能;完工验收5个菜单已确认,但字段与页面细节待设计。
7. ã€å·²è§£å†³ã€‘“TD”的含义(2026-09-09):TD=艇队(敏感词代称,已录入敏感词字典);首页“TD”为统计表表头列(“JC信息”按钮=舰船信息已确认 2026-08-26)
8. ã€å·²è§£å†³ 2026-09-11】艇长环节不纳入第一版:简化流程从 T员故障上报 ç›´æŽ¥è¿›å…¥ XCB助理审核
8. ã€å·²è§£å†³ 2026-09-11】T长环节不纳入第一版:简化流程从 T员故障上报 ç›´æŽ¥è¿›å…¥ XCB助理审核
9. å­—典数据取值:X号“当前状态”(示例“小修”,其余待确认);故障影响(示例“供气”)、故障性质(示例“重大”)已确认按可配置字典字段处理,其他取值仍待补(2026-09-16)。
10. ã€AXU15 è¡¥å……】字段来源与可改性:X号/工程编号/开工时间/完工时间/承修单位等,哪些系统带出、哪些手填、可否修改
11. ã€èµ„料待索取】器材系统对接文档(器材目录表结构、器材ID、接口方式等)——用户已有文档,推进对接时索取
requirement/ǰ44Ò³ÐèÇóÊáÀí.md
@@ -68,7 +68,7 @@
与原始需求的差异:
- ä¸å†åŒ…含“艇长批量审批 / è‰‡é•¿å®¡æ‰¹â€çŽ¯èŠ‚ï¼Œæµç¨‹ä»Ž T员故障上报直接进入 XCB助理审核。
- ä¸å†åŒ…含“T长批量审批 / T长审批”环节,流程从 T员故障上报直接进入 XCB助理审核。
- å–消一切离线能力:单机版/离线填写数据包、内网导入导出数据包(2026-09-14 ç¡®è®¤æ•´ä½“取消并删除对应菜单项:数据包导入、完工信息导入、导出待完工数据包、报价管理)。
- å–消“XCB åŠ©ç†å®Œå·¥å¤„ç†/导出完工项目”环节,改为厂家直接上报完工信息。
- æŠ¥ä»·å¤„理不再作为主流程环节(2026-09-14 ç¡®è®¤ï¼šâ€œæŠ¥ä»·ç®¡ç†â€èœå•与模块整体不做)。
@@ -106,8 +106,8 @@
- ã€å¾…确认】验收流程各步骤的操作角色、完工验收打印输出的实际打印行为,以及“修后质量评级”与 AXU14“T员评分”字段的关系。
- åŽŸå§‹æˆªå›¾æµç¨‹ï¼ˆå­˜æ¡£ï¼Œå·²ä¸é‡‡ç”¨ï¼‰ï¼šæ•…éšœä¸ŠæŠ¥æ•°æ®åŒ… â†’ XCB助理审核(可直接销项)→ CMB领导审批(可打回上一步)→ JCC处长审批 â†’ XCB助理派单(打印派工单)→ åŽ‚å®¶çº¿ä¸‹ä¿®ç† â†’ XCB助理完工处理(导出完工项目/单机版填写/厂家离线填写/内网上报)→ æ‰“印完工验收单;T员验收 â†’ æŠ¥ä»·å¤„理(导出待报价项目 â†’ å•机版填写/导入 â†’ ä¿®èˆ¹åŠžåŠ©ç†å®¡ä»· â†’ JCC审价)。
- æµç¨‹å›¾èŠ‚ç‚¹å¸¦è¶…é“¾æŽ¥ï¼ˆæ–‡æ¡£å†…å«äº¤äº’è¡¨ï¼šç‚¹å‡»èŠ‚ç‚¹åœ¨æ–°çª—å£æ‰“å¼€å¯¹åº”çŠ¶æ€é¡µï¼‰
- ä¸´æŠ¢ä¿®æµç¨‹çŠ¶æ€ï¼ˆæ¥è‡ªæ–‡æ¡£ç›®å½•ï¼Œå¯¹åº”åŽæ–‡è¯¦ç»†é¡µï¼Œå‰ 44 é¡µæœªå±•开,且用户已确认暂不处理 AXU19–31):0 æµç¨‹çŠ¶æ€ â†’ 1 æ•…障上报 â†’ 2.1 è‰‡é•¿æ‰¹é‡å®¡æ‰¹ / 2.2 è‰‡é•¿å®¡æ‰¹ â†’ 3.1 åŠ©ç†å®¡æ‰¹-同意维修 / 3.2 åŠ©ç†å®¡æ‰¹-提交会议研究 / 3.3 åŠ©ç†å®¡æ‰¹-办结 â†’ 4.1 å¤„长批量审批 / 4.2 å¤„长审批 â†’ 5.1 ç»´ä¿® / 5.2 ç»´ä¿®ä¿¡æ¯
- ã€å·²è§£å†³ 2026-09-11】艇长环节不纳入第一版(简化流程不含该环节)
- ä¸´æŠ¢ä¿®æµç¨‹çŠ¶æ€ï¼ˆæ¥è‡ªæ–‡æ¡£ç›®å½•ï¼Œå¯¹åº”åŽæ–‡è¯¦ç»†é¡µï¼Œå‰ 44 é¡µæœªå±•开,且用户已确认暂不处理 AXU19–31):0 æµç¨‹çŠ¶æ€ â†’ 1 æ•…障上报 â†’ 2.1 T长批量审批 / 2.2 T长审批 â†’ 3.1 åŠ©ç†å®¡æ‰¹-同意维修 / 3.2 åŠ©ç†å®¡æ‰¹-提交会议研究 / 3.3 åŠ©ç†å®¡æ‰¹-办结 â†’ 4.1 å¤„长批量审批 / 4.2 å¤„长审批 â†’ 5.1 ç»´ä¿® / 5.2 ç»´ä¿®ä¿¡æ¯
- ã€å·²è§£å†³ 2026-09-11】T长环节不纳入第一版(简化流程不含该环节)
## 4. ä¸ŠæŠ¥å®¡æ‰¹é“¾ï¼ˆAXU2–AXU7)
@@ -292,7 +292,7 @@
8. ã€æš‚不处理 2026-09-20】AXU16 ä¸çº³å…¥å½“前需求
9. ç›‘测信息/查询/报价/基础信息(无截图模块)的页面结构;完工验收流程的菜单与操作已确认,页面字段仍待设计
10. é¦–页统计卡片完整清单(JC信息=舰船信息已确认 2026-08-26;TD å«ä¹‰å¾…确认)
11. ã€å·²è§£å†³ 2026-09-11】“艇长批量审批/艇长审批”环节:第一版简化流程不含艇长环节,从 T员故障上报直接进入 XCB助理审核
11. ã€å·²è§£å†³ 2026-09-11】“T长批量审批/T长审批”环节:第一版简化流程不含T长环节,从 T员故障上报直接进入 XCB助理审核
12. å­—典数据取值:故障影响(示例“供气”)、故障性质(示例“重大”)均为可配置字典字段,其他值待补;X号“当前状态”等其他字典值也待补
13. ã€AXU15 è¡¥å…… 2026-09-08】字段来源与可改性:X号/工程编号/开工时间/完工时间/承修单位等,哪些系统带出、哪些手填、可否修改(原 HANDOFF ç¬¬ 6 èŠ‚ç¬¬ 13 æ¡ï¼Œæœ¬æ¬¡åŒæ­¥å…¥å†Œï¼‰
14. ã€å·²è§£å†³ 2026-09-14】离线/数据包相关功能整体取消,对应菜单项一并删除:AXU2 æ•°æ®åŒ…导入(页面与菜单项删除,无需核对)、完工信息导入、工程单打印输出的“导出待完工数据包”、报价管理的导出/导入。
@@ -316,4 +316,4 @@
## 10. å…³äºŽæ–‡æ¡£ 45 é¡µä¹‹åŽçš„说明
- 45 é¡µä¹‹åŽå¤§éƒ¨åˆ†ä¸ºâ€œè®¡åˆ’修理”(AXU32–AXU66)与“改换装”(AXU67–AXU76)的原型页面,与临抢修项目无关。
- æ–‡æ¡£ç¬¬ 55–67 é¡µçš„“临抢修”详细流程页面(AXU19–AXU31:0流程状态/0流程表格/0.1流程图/1故障上报/2.1艇长批量审批/2.2艇长审批/3.1–3.3助理审批/4.1–4.2处长审批/5.1ç»´ä¿®/5.2维修信息)经确认【暂不处理】。
- æ–‡æ¡£ç¬¬ 55–67 é¡µçš„“临抢修”详细流程页面(AXU19–AXU31:0流程状态/0流程表格/0.1流程图/1故障上报/2.1T长批量审批/2.2T长审批/3.1–3.3助理审批/4.1–4.2处长审批/5.1ç»´ä¿®/5.2维修信息)经确认【暂不处理】。
requirement/¹¤³Ì״̬»úÉè¼Æ.md
@@ -1,8 +1,8 @@
# ä¸´æŠ¢ä¿®è½¯ä»¶Â·å·¥ç¨‹çŠ¶æ€æœºè®¾è®¡ï¼ˆè‰æ¡ˆï¼‰
> æ—¥æœŸï¼š2026-08-25 ï½œ å½“前依据:AXU0/AXU3/AXU4/AXU10/AXU14/AXU15 åŠç”¨æˆ·å·²ç¡®è®¤çš„æ´¾å•、完工状态和验收流程;工程状态统一设计为状态机
> 2026-09-11 æ›´æ–°ï¼šæŒ‰ç”¨æˆ·ç¡®è®¤çš„“简化流程 + åªåšç½‘络版”口径修订(无艇长环节、无离线数据包;2026-09-16 ä¿®æ­£ä¸ºï¼šXCB助理审核负责分配承修单位、拆单、生成工程单号;2026-09-20 è¿›ä¸€æ­¥ä¿®æ­£ä¸ºæ´¾å•页按故障选择、按承修单位生成派工单)。
> 2026-09-14 æ›´æ–°ï¼šç¦»çº¿/数据包相关功能整体取消并删除菜单项(数据包导入、完工信息导入、导出待完工数据包、报价管理模块);艇长环节确认不纳入第一版;JCC åŠ©ç†å®¡æ‰¹ä¸ºå¿…ç»çŽ¯èŠ‚ã€‚
> 2026-09-11 æ›´æ–°ï¼šæŒ‰ç”¨æˆ·ç¡®è®¤çš„“简化流程 + åªåšç½‘络版”口径修订(无T长环节、无离线数据包;2026-09-16 ä¿®æ­£ä¸ºï¼šXCB助理审核负责分配承修单位、拆单、生成工程单号;2026-09-20 è¿›ä¸€æ­¥ä¿®æ­£ä¸ºæ´¾å•页按故障选择、按承修单位生成派工单)。
> 2026-09-14 æ›´æ–°ï¼šç¦»çº¿/数据包相关功能整体取消并删除菜单项(数据包导入、完工信息导入、导出待完工数据包、报价管理模块);T长环节确认不纳入第一版;JCC åŠ©ç†å®¡æ‰¹ä¸ºå¿…ç»çŽ¯èŠ‚ã€‚
> 2026-09-16 æ›´æ–°ï¼šä»»åŠ¡çŠ¶æ€ç»Ÿä¸€ä¸ºä¸ƒä¸ªå€¼ï¼šâ€œæœªä¸ŠæŠ¥ / å·²ä¸ŠæŠ¥XCB / ä¸Šçº§æ‰“回 / XCB助理审核完成 / CMB领导审批完成 / JCC助理审批完成 / JCC处长已阅”;AXU4 æ‰¿ä¿®å•位分配、拆单、工程单号生成均在 XCB助理审核环节完成。
> 2026-09-17 æ›´æ–°ï¼šAXU5/6/7 ä¸‰å¼ å®¡æ‰¹é¡µç»Ÿä¸€ä¿ç•™åˆ—表、刷新、批量审批,去掉修改、CMB已阅、销项、添加进度备注;统一“审批”用语和“JCC处长审批”名称;故障信息增加三组审批字段。
> 2026-09-20 èŒƒå›´æ”¶å£ï¼šAXU9、AXU11–AXU13、AXU16 æš‚不处理,不纳入当前需求;AXU8 ç»§ç»­æš‚缓。
@@ -17,7 +17,7 @@
- çŠ¶æ€å˜åŒ–è®°å½•æ“ä½œäººã€æ“ä½œæ—¶é—´ã€æ“ä½œæ„è§ï¼Œå½¢æˆå¯è¿½æº¯çš„æµè½¬æ—¥å¿—ï¼›AXU4、AXU5/6/7 åŠ AXU14 å·²ç¡®è®¤ä¸æä¾›â€œæ·»åŠ è¿›åº¦å¤‡æ³¨â€æŒ‰é’®ã€‚
- AXU5/6/7 ä¸æä¾›é€šç”¨â€œä¿®æ”¹/销项”操作;审批人只执行批量审批,审批意见、审批结果和审批时间随故障信息保存。
- æ•…障上报阶段的销项动作统一由 XCB助理执行;T队和其他审批角色不提供销项操作。销项理由、销项人、销项时间随故障信息保存;销项人自动取当前登录 XCB助理,销项时间自动取系统当前时间。
- å®¡æ‰¹çŽ¯èŠ‚é¡ºåºå›ºå®šï¼ˆT员故障上报→XCB助理审核→CMB领导审批→JCC助理审批→JCC处长审批),不通过流程引擎实例/任务表驱动。(2026-09-11 ç”¨æˆ·ç¡®è®¤ç®€åŒ–流程,不含艇长环节)
- å®¡æ‰¹çŽ¯èŠ‚é¡ºåºå›ºå®šï¼ˆT员故障上报→XCB助理审核→CMB领导审批→JCC助理审批→JCC处长审批),不通过流程引擎实例/任务表驱动。(2026-09-11 ç”¨æˆ·ç¡®è®¤ç®€åŒ–流程,不含T长环节)
## 1. è®¾è®¡æ€è·¯ï¼šä¸¤å±‚状态
requirement/¸Ä½ø2026.10.txt
File was renamed from requirement/¸Ä½ø20261003.txt
@@ -43,6 +43,17 @@
28,组织机构(部门树表),增加扩展属性设置,目前主要包括负责rk_T
29,用户管理,增加扩展属性设置,目前主要有工程专业、rk_TD专业,负责rk_T;用户列表展开显示工程专业、rk_T队专业、负责rk_T三列,最后操作列提供“编辑属性”。
30、新增故障,默认值,故障上报单位默认填写当前登录人的单位,责任默认填写当前登录人,rk_X号默认填写当前登录人的单位的扩张属性负责rk_T,工程专业、rk_TD专业分别为当前用户的扩展属性工程专业、rk_TD专业
31、添加完工评分,采用5星评分及修后质量意见栏
32、增加工程单打印,新开一个菜单功能
    é¡µé¢ç±»åŒæ•…障上报,那些查询条件都要,但rk_X号、rk_TD专业没有全部选项,默认暂时选择第一个
    è¦æ±‚按模板输出到word,做成活的,{}号里是字段名, ç›®å‰å¦‚图所示,在一个word里打印,4联(UnitNo),最多选6条记录打印一页,编号字段暂时不管
    4联(UnitNo)那个放在最右边文字转向显示, é‚£ä¸ªZB型号:{rk_X号的父节点(在产品数里)名称} æ¼äº†
    åšä¸ªé…ç½®æ¯1联有个名称, è¾“出到word后显示 å¦‚“第1联   ä¸ŠæŠ¥äººâ€  è¿™æ ·çš„æ ¼å¼  ï¼ˆä¸ŠæŠ¥äººæ˜¯ä¸ºç¬¬1联配置的)可能以后还有别的地方需要,但每联配置和这个配置是不一样
    ä¸é™åˆ¶6条记录,可以多页输出,每页显示页不限制条数,要求记录不能跨页显示,第一个表格和第33个表个分别固定在每页的顶部和底部,
33、前端端口、前端访问后端ip及端口做成配置
34、
requirement/Ãô¸Ð´Ê×Öµä.md
@@ -29,6 +29,6 @@
- æ–‡æ¡£ä¸­æ‰€æœ‰æ¶‰åŠä»¥ä¸Šå«ä¹‰å¤„一律使用**代称列**名称(词根代称,组合词直接以代称书写)。
- æœ¯è¯­ç»Ÿä¸€ï¼ˆ2026-09-09):界面/文档一律写代称(如 X号=舷号),不使用近似真实表述(如“舰号”);TD=艇队 ä½œä¸ºç‹¬ç«‹è¯æ¡ç»´æŠ¤ã€‚
- æœ¯è¯­æ›´æ–°ï¼ˆ2026-09-20):2026-09-14 æ›¾ç»Ÿä¸€å†™â€œT员验收”;现完工验收流程固定为5个菜单(验收申请/质检员验收/质检负责人验收/完工验收打印输出/修后质量评级),不再使用“T员验收”作为菜单名。T员=艇员,由 T è¯æ ¹æ´¾ç”Ÿï¼›è¯„分与角色名仍统一写“T员”。
- æœ¯è¯­æ›´æ–°ï¼ˆ2026-09-20):2026-09-14 æ›¾ç»Ÿä¸€å†™â€œT员验收”;现完工验收流程固定为5个菜单(验收申请/质检员验收/质检负责人验收/完工验收打印输出/修后质量评级),不再使用“T员验收”作为菜单名。T员=T员,由 T è¯æ ¹æ´¾ç”Ÿï¼›è¯„分与角色名仍统一写“T员”。
- æ–°å¢žæ•æ„Ÿè¯ç”±ç”¨æˆ·æä¾›åŽè¿½åŠ åˆ°ç¬¬ 2 èŠ‚è¡¨æ ¼åŠ JSON é…ç½®ã€‚
- ç”Ÿäº§çŽ¯å¢ƒæ›¿æ¢é€»è¾‘ï¼šä»…æ›¿æ¢ `real_name` ä¸Ž `codename` ä¸åŒä¸”未使用占位符的词条;字典不可用时继续输出开发代称。
requirement/½çÃæ»¹Ô­.html
@@ -18,7 +18,7 @@
</style></head><body>
<h1>临抢修软件 Â· ç•Œé¢è¯†åˆ«è¿˜åŽŸï¼ˆåŽŸå§‹éœ€æ±‚.docx å‰ 44 é¡µï¼‰</h1>
<p style="font-size:13px;color:#555">用途:与原始文档逐张核对 OCR/识别结果。黄色高亮 = å¾…确认/OCR疑误。表格列为横向排列,不代表原图实际列顺序。</p>
<div class="note">全局口径(2026-09-11 ç”¨æˆ·ç¡®è®¤ï¼‰ï¼šç¬¬ä¸€ç‰ˆ<mark>不按原始需求</mark>实现,改为 â‘  <b>简化业务流程</b> + â‘¡ <b>只提供网络版、不提供离线单机版</b>;简化流程见 AXU1 å¡ç‰‡ã€‚所有“单机版填写/离线填写/数据包导入导出”相关功能均不进入第一版。<mark>2026-09-20 ç”¨æˆ·ç¡®è®¤ï¼šJCC处长审批完成后直接进入XCB助理派单页面;页面显示未派单故障,勾选后点击“一键派单”;按承修单位生成派工单,编号为故障编号 + `/01`、`/02` ç­‰ã€‚除“一键派单”外,原 AXU10 å…¶ä»–业务按钮暂不需要。故障编号由系统按 `固定前缀-X号/分类-YYMMDD-nnn` è‡ªåŠ¨ç”Ÿæˆï¼Œæ•…éšœä¿¡æ¯ä¿ç•™â€œæ•…éšœç¼–å·â€å­—æ®µï¼ˆ2026-09-28 æ›´æ–°ï¼‰ã€‚</mark><mark>2026-09-14 ç”¨æˆ·ç¡®è®¤ï¼šç¦»çº¿/数据包相关功能整体取消、对应菜单项一并删除(C-12 å·²è§£å†³ï¼‰ï¼›â€œæŠ¥ä»·ç®¡ç†â€èœå•与模块整体不做(报价处理移出主流程);艇长环节不纳入第一版;JCC åŠ©ç†å®¡æ‰¹ä¸ºå¿…ç»çŽ¯èŠ‚ï¼›AXU2 æ•°æ®åŒ…导入无需核对。</mark></div>
<div class="note">全局口径(2026-09-11 ç”¨æˆ·ç¡®è®¤ï¼‰ï¼šç¬¬ä¸€ç‰ˆ<mark>不按原始需求</mark>实现,改为 â‘  <b>简化业务流程</b> + â‘¡ <b>只提供网络版、不提供离线单机版</b>;简化流程见 AXU1 å¡ç‰‡ã€‚所有“单机版填写/离线填写/数据包导入导出”相关功能均不进入第一版。<mark>2026-09-20 ç”¨æˆ·ç¡®è®¤ï¼šJCC处长审批完成后直接进入XCB助理派单页面;页面显示未派单故障,勾选后点击“一键派单”;按承修单位生成派工单,编号为故障编号 + `/01`、`/02` ç­‰ã€‚除“一键派单”外,原 AXU10 å…¶ä»–业务按钮暂不需要。故障编号由系统按 `固定前缀-X号/分类-YYMMDD-nnn` è‡ªåŠ¨ç”Ÿæˆï¼Œæ•…éšœä¿¡æ¯ä¿ç•™â€œæ•…éšœç¼–å·â€å­—æ®µï¼ˆ2026-09-28 æ›´æ–°ï¼‰ã€‚</mark><mark>2026-09-14 ç”¨æˆ·ç¡®è®¤ï¼šç¦»çº¿/数据包相关功能整体取消、对应菜单项一并删除(C-12 å·²è§£å†³ï¼‰ï¼›â€œæŠ¥ä»·ç®¡ç†â€èœå•与模块整体不做(报价处理移出主流程);T长环节不纳入第一版;JCC åŠ©ç†å®¡æ‰¹ä¸ºå¿…ç»çŽ¯èŠ‚ï¼›AXU2 æ•°æ®åŒ…导入无需核对。</mark></div>
<div class="note"><b>范围收口(2026-09-20):</b><mark>AXU9、AXU11、AXU12、AXU13、AXU16 æš‚不处理,不纳入当前需求与后续核对;AXU8 ç»§ç»­æš‚缓。</mark> åŽŸå§‹æˆªå›¾æ–‡å­—ä»ä¿ç•™ï¼Œä¾›èµ„æ–™è¿½æº¯ã€‚</div>
<h2>AXUnn â†” èœå•名 â†” æ–‡æ¡£é¡µ å¯¹ç…§è¡¨</h2>
<table><tr><th>截图编号</th><th>菜单名/页面名</th><th>文档页</th></tr><tr><td><b>AXU0</b></td><td>首页门户</td><td>文档页 5</td></tr>
@@ -56,9 +56,9 @@
<div class="label">第一版简化流程(10 æ­¥ï¼Œå·²ç¡®è®¤ 2026-09-11)</div>
<ol class="flow"><li>1. T员故障上报</li><li>2. XCB助理审核</li><li>3. CMB领导审批</li><li>4. JCC助理审批</li><li>5. JCC处长审批</li><li>6. XCB助理派单 â€”— æ˜¾ç¤ºæœªæ´¾å•故障,勾选后点击“一键派单”;按承修单位生成派工单,不再分配或修改承修单位</li><li>7. åŽ‚å®¶çº¿ä¸‹ä¿®ç†ï¼ˆç³»ç»Ÿå¤–çº¿ä¸‹çŽ¯èŠ‚ï¼‰</li><li>8. åŽ‚å®¶ä¸ŠæŠ¥å®Œå·¥ä¿¡æ¯</li><li>9. å®Œå·¥éªŒæ”¶æµç¨‹ï¼šéªŒæ”¶ç”³è¯· â†’ è´¨æ£€å‘˜éªŒæ”¶ â†’ è´¨æ£€è´Ÿè´£äººéªŒæ”¶ â†’ å®Œå·¥éªŒæ”¶æ‰“印输出 â†’ ä¿®åŽè´¨é‡è¯„级</li><li>10. éªŒæ”¶å®Œæˆ</li></ol><div class="note2">退回规则(2026-09-16 ç¡®è®¤ï¼‰ï¼šCMB领导、JCC助理、JCC处长审批不通过时,均直接退回XCB助理,任务状态为“上级打回”;不逐级打回。“上级打回”与“已上报XCB”业务语义等价,但作为不同状态值保存,以区分流转轨迹。</div>
<div class="label">相对原始需求的差异(2026-09-11 ç¡®è®¤ï¼‰</div>
<div class="note2">① ä¸å†åŒ…含“艇长批量审批/艇长审批”,直接从 T员故障上报开始;② å–消“单机版填写/离线填写数据包 + å†…网导入/导出数据包”;③ å–消“XCB助理完工处理(导出完工项目/单机版填写/厂家离线填写)”环节,改为厂家直接上报完工信息;④ æŠ¥ä»·å¤„理不再作为主流程环节(“报价管理”菜单是否保留待确认);⑤ JCC助理审批确认为流程必经环节(原截图流程图未单列)。</div>
<div class="note2">① ä¸å†åŒ…含“T长批量审批/T长审批”,直接从 T员故障上报开始;② å–消“单机版填写/离线填写数据包 + å†…网导入/导出数据包”;③ å–消“XCB助理完工处理(导出完工项目/单机版填写/厂家离线填写)”环节,改为厂家直接上报完工信息;④ æŠ¥ä»·å¤„理不再作为主流程环节(“报价管理”菜单是否保留待确认);⑤ JCC助理审批确认为流程必经环节(原截图流程图未单列)。</div>
<div class="note2">原始截图流程(存档参考,已不采用):故障上报数据包 â†’ XCB助理审核(可直接销项)→ CMB领导审批(可打回上一步)→ JCC处长审批 â†’ XCB助理派单(打印派工单:同X号+同上报日期+同承修单位合成一个派工单)→ åŽ‚å®¶çº¿ä¸‹ä¿®ç† â†’ XCB助理完工处理(导出完工项目/单机版填写/厂家离线填写/内网上报)→ æ‰“印完工验收单;T员验收 â†’ æŠ¥ä»·å¤„理(导出待报价项目 â†’ å•机版填写/导入 â†’ ä¿®èˆ¹åŠžåŠ©ç†å®¡ä»· â†’ JCC审价)。</div>
<div class="note2">术语(2026-09-14 ç”¨æˆ·ç¡®è®¤ï¼‰ï¼šç»Ÿä¸€å†™â€œT员”(T=艇代称,T员=艇员);原文档菜单的旧写法已作废,评分与角色名一并统一为“T员”。</div>
<div class="note2">术语(2026-09-14 ç”¨æˆ·ç¡®è®¤ï¼‰ï¼šç»Ÿä¸€å†™â€œT员”(T=艇代称,T员=T员);原文档菜单的旧写法已作废,评分与角色名一并统一为“T员”。</div>
</section><section class="card"><h3>AXU2 Â· æ•°æ®åŒ…导入(文档页 8)</h3><div class="note">导入故障上报数据包文件</div><div class="label">按钮/操作</div><div class="chips"><span class="td">选择文件</span><span class="td">导入数据包</span></div><div class="note2">导入结果统计</div><div class="label">列表列(表头)</div><div class="chips"><span class="td">统计分类</span><span class="td">数量</span><span class="td">明细</span></div><div class="note2">统计分类明细:项目数量 / æ¶‰åŠå·¥ç¨‹ä¸“业 / æ¶‰åŠT队专业 / æ¶‰åŠè£…备</div><div class="note2">【已确认 2026-09-14】只做网络版、无导入功能 â†’ æœ¬é¡µæ•´ä½“取消,菜单项“数据包导入”删除,无需核对。</div></section><section class="card"><h3>AXU3 Â· æ•…障上报(文档页 9)</h3><div class="note">页面标题:列表</div><div class="label">查询条件</div><div class="chips"><span class="td">X号</span><span class="td">T队专业</span><span class="td">工程专业</span><span class="td">上报单位</span><span class="td">负责人</span><span class="td">上报日期(起)—(至)</span></div><div class="label">按钮/操作</div><div class="chips"><span class="td">查询</span><span class="td">刷新</span><span class="td">新增</span><span class="td">上报</span><span class="td">修改</span><span class="td">删除</span><span class="td">导出</span></div><div class="note2">【已确认 2026-09-16】字段与按钮通读无误;“填写故障分析报告”第一版暂不实现,业务逻辑待后续明确。</div><div class="label">列表列(表头)</div><div class="chips"><span class="td">序号</span><span class="td">故障编号</span><span class="td">任务状态</span><span class="td">上报日期</span><span class="td">上报单位</span><span class="td">装备名称</span><span class="td">所属上级系统</span><span class="td">工程名称及损坏情况</span><span class="td">修理技术要求</span><span class="td">工程专业</span><span class="td">T队专业</span><span class="td">跟踪技术队</span><span class="td">负责人</span><span class="td">故障影响</span><span class="td">故障性质</span><span class="td">重大故障描述</span><span class="td">是否自修</span><span class="td">是否涉H</span><span class="td">监测项目</span><span class="td">监测数值</span><span class="td">要求完工日期</span><span class="td">备注</span></div><div class="note2">任务状态(2026-09-16 ç¡®è®¤ï¼Œå…¨å±€ä¸ƒä¸ªå€¼ï¼‰ï¼šæœªä¸ŠæŠ¥ / å·²ä¸ŠæŠ¥XCB / ä¸Šçº§æ‰“回 / XCB助理审核完成 / CMB领导审批完成 / JCC助理审批完成 / JCC处长已阅。</div><div class="note2">列表表头增加“上报日期”“上报单位”(2026-09-16 ç¡®è®¤ï¼‰ã€‚</div><div class="note2">删除限制(2026-09-16 ç¡®è®¤ï¼‰ï¼šæœªä¸ŠæŠ¥çš„æ•…障可以删除;已上报的故障不能删除;任务状态为“上级打回”时仍允许删除。</div><div class="note2">审批字段组(2026-09-17 ç¡®è®¤ï¼‰ï¼šCMB领导审批意见、CMB领导审批结果(通过/不通过)、CMB领导审批时间;JCC助理审批意见、JCC助理审批结果(通过/不通过)、JCC助理审批时间;JCC处长审批意见、JCC处长审批结果(通过/不通过)、JCC处长审批时间。</div><div class="note2">销项字段组(2026-09-17 ç¡®è®¤ï¼‰ï¼šé”€é¡¹ç†ç”±ã€é”€é¡¹äººã€é”€é¡¹æ—¶é—´ï¼›æ•…障上报阶段的销项统一由XCB助理执行,T队和其他审批角色不提供销项操作。销项人自动取当前登录XCB助理,销项时间自动取系统当前时间。</div><div class="label">新增/编辑表单字段(2026-09-16 ç¡®è®¤ï¼šæ–°å¢žæ”¹ä¸ºè¡¨å•式,原需求表格式已调整):</div><div class="chips"><span class="td">故障编号(只读,自动生成)</span><span class="td">X号</span><span class="td">上报单位</span><span class="td">T队专业</span><span class="td">跟踪技术队</span><span class="td">负责人</span><span class="td">上报日期</span><span class="td">工程专业</span><span class="td">装备名称</span><span class="td">分机/部位</span><span class="td">所属上级系统</span><span class="td">工程名称</span><span class="td">修理技术要求</span><span class="td">故障影响<mark>可配置字典;示例值:供气;其他值待确认</mark></span><span class="td">故障性质<mark>可配置字典;示例值:重大;其他值待确认</mark></span><span class="td">重大故障描述</span><span class="td">是否自修</span><span class="td">是否涉H</span><span class="td">监测项目</span><span class="td">监测数值</span><span class="td">要求完工日期</span><span class="td">备注</span></div><div class="note2">字段调整(2026-09-16 ç¡®è®¤ï¼‰ï¼šåœæ³Šåœ°å­—段不保留。</div><div class="note2">故障编号(2026-09-28 ç¡®è®¤ï¼Œæ›¿ä»£ 2026-09-20 å£å¾„):由系统自动生成,规则 <b>固定前缀-X号/分类-YYMMDD-nnn</b>;X号取3位数字,分类取“T队专业配置”的 A/B,YYMMDD ä¸ºä¸ŠæŠ¥æ—¥æœŸï¼Œnnn ä¸ºåŒä¸€ X号+同一T队专业+同一上报日期的序号(从 001 èµ·ï¼‰ã€‚表单中该字段只读,显示“保存后自动生成”。</div><div class="label">新增表单交互(2026-09-16 ç¡®è®¤ï¼‰ï¼š</div><div class="note2">点击“新增”弹出表单,不使用原需求的表格式录入。</div><div class="note2">默认值:上报单位=登录用户所属单位;T队专业=登录用户所属专业;负责人=登录用户;上报日期=当前日期。</div><div class="note2">下拉字段:T队专业、工程专业、故障影响、故障性质。</div><div class="note2">点击“装备名称”打开产品结构树;选择装备(设备)后,自动填入“装备名称”和“所属上级系统”。</div><div class="note2">H ä¸ºæ•æ„Ÿè¯å­—典代称,字段统一写“是否涉H”。</div></section><section class="card"><h3>AXU4 Â· XCB助理审核(文档页 10)</h3><div class="note">页面定位:简化流程第 2 æ­¥â€œXCB助理审核”。</div><div class="note">命名口径(2026-09-16 ç¡®è®¤ï¼‰ï¼šé¡µé¢åŠèœå•名称统一写“XCB助理审核”,页面内原“审批”文案统一改为“审核”。</div><div class="note2">【已确认 2026-09-16】本环节完成:分配承修单位、按承修单位拆单、生成工程单号;XCB助理审核完成后即可领器材。JCC处长审批完成后直接进入XCB助理派单页面;页面显示未派单故障,勾选后一键派单并按承修单位生成派工单,不再分配或修改承修单位。</div><div class="label">业务规则</div><div class="note2">一个故障单可以分配多个承修单位;审核完成后按承修单位拆成多个工程单走后续流程。</div><div class="note2">工程单号在本环节生成,规则:固定-X号/班组类别-YYMMDD-厂家代号/序号,其中 YYMMDD ä¸ºä¸ŠæŠ¥æ—¥æœŸã€‚</div><div class="note2">本步确定是否监测项目、监测数值等。</div><div class="label">查询条件</div><div class="chips"><span class="td">工程通用条件</span><span class="td">承修单位</span><span class="td">故障性质</span><span class="td">任务状态</span></div><div class="label">按钮/操作</div><div class="chips"><span class="td">查询</span><span class="td">刷新</span><span class="td">修改</span><span class="td">销项(仅XCB助理)</span><span class="td">审核通过/不通过</span><span class="td"><mark>已去掉:添加进度备注、CMB已阅</mark></span></div><div class="note2">任务状态统一采用全局七个值:未上报 / å·²ä¸ŠæŠ¥XCB / ä¸Šçº§æ‰“回 / XCB助理审核完成 / CMB领导审批完成 / JCC助理审批完成 / JCC处长已阅。</div><div class="label">列表列(表头)</div><div class="chips"><span class="td">序号</span><span class="td">操作</span><span class="td">任务状态</span><span class="td">所属上级系统</span><span class="td">装备名称</span><span class="td">工程名称及损坏情况</span><span class="td">修理技术要求</span><span class="td">工程专业</span><span class="td">T队专业</span><span class="td">跟踪技术队</span><span class="td">负责人</span><span class="td">故障影响</span><span class="td">故障性质</span><span class="td">重大故障描述</span><span class="td">是否自修</span><span class="td">是否涉H</span><span class="td">监测项目</span><span class="td">监测数值</span><span class="td">要求完工日期</span><span class="td">备注</span></div><div class="label">审核表单字段(2026-09-16 ç¡®è®¤ï¼‰ï¼š</div><div class="chips"><span class="td">T队专业</span><span class="td">上报单位</span><span class="td">负责人</span><span class="td">装备名称</span><span class="td">上报日期</span><span class="td">工程名称</span><span class="td">承修单位(可分配多个;候选来自“系统管理 > æ‰¿ä¿®å•位管理”,按“排序”取前7个常用承修单位显示)</span><span class="td">跟踪技术队(从故障信息带出,单选:一队/二队/三队)</span><span class="td">要求完工日期</span><span class="td">故障性质</span><span class="td">是否自修</span><span class="td">是否涉H</span><span class="td">是否监测项目</span><span class="td">重大故障描述</span><span class="td">监测数据值</span><span class="td">备注</span></div><div class="note2">审核表单包含通过、不通过、关闭。XCB助理审核不通过时不转“上级打回”:内容不合格由XCB助理通过“修改”按钮自行修正;不作为故障处理时点“销项”。</div><div class="note2">销项权限与字段(2026-09-17 ç¡®è®¤ï¼‰ï¼šæ•…障上报阶段的销项统一由XCB助理执行;记录销项理由、销项人、销项时间。销项人自动取当前登录XCB助理,销项时间自动取系统当前时间。</div></section><section class="card"><h3>AXU5 Â· CMB领导审批(文档页 11)</h3><div class="note">列表 + æ‰¹é‡å®¡æ‰¹</div><div class="note2">AXU5/6/7 å…±é€šï¼ˆ2026-09-17 ç¡®è®¤ï¼‰ï¼šä¿ç•™åˆ—表、刷新、批量审批;去掉修改、CMB已阅、销项、添加进度备注;三页统一使用“审批”用语。</div><div class="label">按钮/操作</div><div class="chips"><span class="td">列表</span><span class="td">刷新</span><span class="td">批量审批</span><span class="td"><mark>已去掉:修改、CMB已阅、销项、添加进度备注</mark></span></div><div class="label">列表列(表头)</div><div class="chips"><span class="td">序号</span><span class="td">操作</span><span class="td">任务状态</span><span class="td">所属上级系统</span><span class="td">装备名称</span><span class="td">工程名称及损坏情况</span><span class="td">修理技术要求</span><span class="td">工程专业</span><span class="td">T队专业</span><span class="td">跟踪技术队</span><span class="td">负责人</span><span class="td">故障影响</span><span class="td">故障性质</span><span class="td">重大故障描述</span><span class="td">是否自修</span><span class="td">是否涉H</span><span class="td">监测项目</span><span class="td">监测数值</span><span class="td">要求完工日期</span><span class="td">备注</span></div><div class="note2">任务状态:统一采用全局七个值;本页审批不通过时直接转为“上级打回”并退回XCB助理。</div><div class="note2">故障信息审批字段组(2026-09-17 ç¡®è®¤ï¼‰ï¼šCMB领导审批意见、CMB领导审批结果(通过/不通过)、CMB领导审批时间;JCC助理审批意见、JCC助理审批结果(通过/不通过)、JCC助理审批时间;JCC处长审批意见、JCC处长审批结果(通过/不通过)、JCC处长审批时间。</div><div class="label">审批表单:</div><div class="chips"><span class="td">审批意见</span><span class="td">关闭 / é€šè¿‡ / ä¸é€šè¿‡</span></div></section><section class="card"><h3>AXU6 Â· JCC助理审批(文档页 12)</h3><div class="note">与 AXU5 å…±ç”¨åŒä¸€å¥—界面和功能(2026-09-17 ç¡®è®¤ï¼‰</div><div class="note2">上级打回规则:JCC助理审批不通过时直接退回XCB助理,任务状态转为“上级打回”,不逐级打回。</div><div class="label">按钮/操作</div><div class="chips"><span class="td">列表</span><span class="td">刷新</span><span class="td">批量审批</span><span class="td"><mark>已去掉:修改、CMB已阅、销项、添加进度备注</mark></span></div><div class="label">列表列(表头)</div><div class="chips"><span class="td">序号</span><span class="td">操作</span><span class="td">任务状态</span><span class="td">所属上级系统</span><span class="td">装备名称</span><span class="td">工程名称及损坏情况</span><span class="td">修理技术要求</span><span class="td">工程专业</span><span class="td">T队专业</span><span class="td">跟踪技术队</span><span class="td">负责人</span><span class="td">故障影响</span><span class="td">故障性质</span><span class="td">重大故障描述</span><span class="td">是否自修</span><span class="td">是否涉H</span><span class="td">监测项目</span><span class="td">监测数值</span><span class="td">要求完工日期</span><span class="td">备注</span></div></section><section class="card"><h3>AXU7 Â· JCC处长审批(文档页 13)</h3><div class="note">与 AXU5/6 å…±ç”¨åŒä¸€å¥—界面和功能(2026-09-17 ç¡®è®¤ï¼‰ï¼›é¡µé¢åŠèœå•名称统一为“JCC处长审批”</div><div class="note2">上级打回规则:JCC处长审批不通过时直接退回XCB助理,任务状态转为“上级打回”,不逐级打回。</div><div class="label">按钮/操作</div><div class="chips"><span class="td">列表</span><span class="td">刷新</span><span class="td">批量审批</span><span class="td"><mark>已去掉:修改、CMB已阅、销项、添加进度备注</mark></span></div><div class="label">列表列(表头)</div><div class="chips"><span class="td">序号</span><span class="td">操作</span><span class="td">任务状态</span><span class="td">所属上级系统</span><span class="td">装备名称</span><span class="td">工程名称及损坏情况</span><span class="td">修理技术要求</span><span class="td">工程专业</span><span class="td">T队专业</span><span class="td">跟踪技术队</span><span class="td">负责人</span><span class="td">故障影响</span><span class="td">故障性质</span><span class="td">重大故障描述</span><span class="td">是否自修</span><span class="td">是否涉H</span><span class="td">监测项目</span><span class="td">监测数值</span><span class="td">要求完工日期</span><span class="td">备注</span></div></section><section class="card"><h3>AXU8 Â· ä¸´æŠ¢ä¿®å·¥ç¨‹ç®¡ç†ï¼ˆæ€»è§ˆï¼‰ï¼ˆæ–‡æ¡£é¡µ 14)</h3><div class="note">页面说明:“类似已派工程单,角色不一样,手工操作”</div><div class="label">状态入口 / ç»Ÿè®¡</div><div class="chips"><span class="td">完工</span><span class="td">未完工</span><span class="td">待试验</span><span class="td">待备件</span><span class="td"><mark>已确认为工程状态筛选入口(按钮/页签),形式待定</mark></span></div><div class="note2">下方示意性表格:表头仅 Column1/Column2/Column3 + 2 è¡Œç©ºè¡Œï¼ˆä½œè€…未填字段)——按状态筛选展示工程列表,字段后续自行设计</div></section><section class="card"><h3>AXU9 Â· CMB领导已阅(文档页 15)</h3><div class="note"><mark>暂不处理 2026-09-20:不纳入当前需求与后续核对。</mark></div><div class="note">页面说明:“查看CMB领导已批阅的”工程</div><div class="label">列表列(表头)</div><div class="chips"><span class="td">序号</span><span class="td">任务状态</span><span class="td">所属上级系统</span><span class="td">装备名称</span><span class="td">工程名称及损坏情况</span><span class="td">修理技术要求</span><span class="td">工程专业</span><span class="td">T队专业</span><span class="td">跟踪技术队</span><span class="td">负责人</span><span class="td">故障影响</span><span class="td">故障性质</span><span class="td">重大故障描述</span><span class="td">是否自修</span><span class="td">是否涉H</span><span class="td">监测项目</span><span class="td">监测数值</span><span class="td">要求完工日期</span><span class="td">备注</span><span class="td">JCC助理审批(否)</span><span class="td">JCC处长审批</span><span class="td">审批完成</span></div><div class="label">按钮/操作</div><div class="chips"><span class="td">销项</span></div><div class="note2"><mark>JCC助理审批/JCC处长审批/审批完成 ä¸‰åˆ—含义待确认</mark></div><div class="note2">流程位置(2026-09-20 ç¡®è®¤ï¼‰ï¼šJCC处长审批完成后直接进入XCB助理派单;AXU9不作为必经流程节点。</div></section><section class="card"><h3>AXU10 Â· æ´¾å•管理(文档页 16;2026-09-20 æŒ‰ç”¨æˆ·å£å¾„重定义)</h3>
<div class="note2">页面入口:JCC处长审批完成后,流程直接进入 XCB助理派单页面。</div>
<div class="note2">页面内容:显示“未派单”的故障一览。</div>