From d71ebc572712ee8e8aaae7f64f2fa28fdb82f3bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=86=95?= Date: Tue, 30 Jun 2026 16:36:38 +0800 Subject: [PATCH] Add ONE-OS Web legacy prototypes and sidebar menu folder. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Import desktop web originals into dedicated prototypes with a shared legacy shell, sync script, and a new「ONE-OS Web 端」sidebar section for review alongside existing refactored pages. Co-authored-by: Cursor --- .axhub/make/sidebar-tree.json | 99 +- package.json | 1 + scripts/sync-oneos-web-prototypes.mjs | 270 + .../oneosWebLegacy/OneosWebLegacyShell.tsx | 86 + src/common/oneosWebLegacy/legacyGlobals.ts | 18 + src/prototypes/oneos-web-business/index.tsx | 55 + .../oneos-web-business/pages/01-保险采购.jsx | 8698 +++++++++++++ .../pages/02-编辑交车任务.jsx | 344 + .../pages/03-查看交车任务.jsx | 382 + .../pages/04-车辆成本维护.jsx | 187 + .../pages/05-供应商管理-编辑.jsx | 279 + .../pages/06-供应商管理-查看.jsx | 191 + .../pages/07-供应商管理-新增.jsx | 256 + .../pages/08-供应商管理.jsx | 326 + .../oneos-web-business/pages/09-交车任务.jsx | 1698 +++ .../pages/10-客户管理-编辑.jsx | 274 + .../pages/11-客户管理-查看.jsx | 167 + .../pages/12-客户管理-新增.jsx | 251 + .../oneos-web-business/pages/13-客户管理.jsx | 323 + .../pages/14-新增交车任务.jsx | 345 + .../pages/15-租赁账单-查看.jsx | 347 + .../pages/16-租赁账单-收费明细.jsx | 451 + .../oneos-web-business/pages/17-租赁账单.jsx | 478 + .../oneos-web-business/pages/18-ETC管理.jsx | 460 + .../oneos-web-contract-template/index.tsx | 21 + .../pages/01-合同模板管理.jsx | 2362 ++++ .../oneos-web-data-analysis/index.tsx | 37 + .../pages/01-客户服务部业务统计报表.jsx | 754 ++ .../pages/02-物流业务台账.jsx | 2549 ++++ .../pages/03-物流业务月度统计.jsx | 845 ++ .../pages/04-业务部汇总台账.jsx | 561 + .../pages/05-业务部台账.jsx | 2470 ++++ .../pages/06-业务部业绩明细.jsx | 814 ++ .../pages/07-业务台账.jsx | 841 ++ .../pages/08-租赁车辆收入明细.jsx | 803 ++ .../pages/09-租赁客户氢费台账.jsx | 696 ++ src/prototypes/oneos-web-finance/index.tsx | 39 + .../pages/01-还车应结款-查看.jsx | 644 + .../pages/02-还车应结款-费用明细.jsx | 1526 +++ .../oneos-web-finance/pages/03-还车应结款.jsx | 2071 ++++ .../pages/04-提车收款单-编辑.jsx | 538 + .../pages/05-提车应收款-查看.jsx | 297 + .../pages/06-提车应收款-开票信息.jsx | 592 + .../pages/07-提车应收款-审核.jsx | 399 + .../pages/08-提车应收款-提车收款单.jsx | 556 + .../oneos-web-finance/pages/09-提车应收款.jsx | 454 + .../oneos-web-finance/pages/10-租赁账单.jsx | 536 + src/prototypes/oneos-web-h2-station/index.tsx | 25 + .../pages/01-加氢订单.jsx | 182 + .../pages/02-加氢记录.jsx | 1809 +++ .../pages/03-站点信息.jsx | 10044 ++++++++++++++++ .../oneos-web-help-center/index.tsx | 21 + .../pages/01-功能说明书.jsx | 234 + .../oneos-web-lease-contract/index.tsx | 35 + .../pages/01-车辆租赁合同-变更为三方合同.jsx | 461 + .../pages/02-车辆租赁合同-查看.jsx | 562 + .../pages/03-车辆租赁合同-附加费用.jsx | 358 + .../pages/04-车辆租赁合同-新增.jsx | 815 ++ .../pages/05-车辆租赁合同-新增车辆.jsx | 635 + .../pages/06-车辆租赁合同-续签合同.jsx | 806 ++ .../pages/07-车辆租赁合同-转正式合同.jsx | 878 ++ .../pages/08-车辆租赁合同.jsx | 1430 +++ .../oneos-web-ledger-data/index.tsx | 29 + .../pages/01-保险分摊明细.jsx | 517 + .../pages/02-车辆氢费明细.jsx | 4541 +++++++ .../pages/03-车辆维修明细.jsx | 1825 +++ .../pages/04-氢费采购端汇总报表.jsx | 2331 ++++ .../pages/05-租赁业务台账.jsx | 3178 +++++ src/prototypes/oneos-web-login/index.tsx | 21 + .../oneos-web-login/pages/01-登录.jsx | 457 + src/prototypes/oneos-web-ops/index.tsx | 117 + .../oneos-web-ops/pages/01-备件库存.jsx | 1220 ++ .../oneos-web-ops/pages/02-仓库管理.jsx | 499 + .../oneos-web-ops/pages/03-备车-编辑.jsx | 625 + .../oneos-web-ops/pages/04-备车-查看.jsx | 315 + .../oneos-web-ops/pages/05-备车-新增.jsx | 660 + .../oneos-web-ops/pages/06-备车管理.jsx | 729 ++ .../oneos-web-ops/pages/07-查看故障.jsx | 228 + .../oneos-web-ops/pages/08-调拨管理-查看.jsx | 297 + .../pages/09-调拨管理-调拨信息.jsx | 648 + .../pages/10-调拨管理-接收信息-第三方运输.jsx | 460 + .../pages/11-调拨管理-接收信息-司机运输.jsx | 520 + .../oneos-web-ops/pages/12-调拨管理-新增.jsx | 492 + .../pages/13-调拨管理-业务提交-编辑.jsx | 466 + .../pages/14-调拨管理-运维(调拨方)-编辑.jsx | 634 + .../pages/15-调拨管理-运维(接收)-编辑.jsx | 479 + .../oneos-web-ops/pages/16-调拨管理.jsx | 1005 ++ .../oneos-web-ops/pages/17-故障管理-编辑.jsx | 269 + .../oneos-web-ops/pages/18-故障管理.jsx | 505 + .../oneos-web-ops/pages/19-还车管理-查看.jsx | 667 + .../oneos-web-ops/pages/20-还车管理-还车.jsx | 1007 ++ .../oneos-web-ops/pages/21-还车管理.jsx | 481 + .../oneos-web-ops/pages/22-后装设备.jsx | 483 + .../pages/23-交车管理-编辑抽屉.jsx | 505 + .../pages/24-交车管理-交车单-编辑.jsx | 855 ++ .../pages/25-交车管理-交车单-查看.jsx | 495 + .../pages/26-交车管理-交车单.jsx | 245 + .../oneos-web-ops/pages/27-交车管理.jsx | 3362 ++++++ .../oneos-web-ops/pages/28-年审管理-办理.jsx | 1115 ++ .../oneos-web-ops/pages/29-年审管理-查看.jsx | 367 + .../oneos-web-ops/pages/30-年审管理.jsx | 1728 +++ .../oneos-web-ops/pages/31-上牌管理.jsx | 940 ++ .../pages/32-替换车管理-编辑.jsx | 853 ++ .../pages/33-替换车管理-查看.jsx | 368 + .../pages/34-替换车管理-新增.jsx | 813 ++ .../oneos-web-ops/pages/35-替换车管理.jsx | 948 ++ .../oneos-web-ops/pages/36-新增故障.jsx | 419 + .../oneos-web-ops/pages/37-新增后装设备.jsx | 145 + .../oneos-web-ops/pages/38-异动管理-编辑.jsx | 629 + .../oneos-web-ops/pages/39-异动管理-查看.jsx | 405 + .../pages/40-异动管理-结束异动.jsx | 473 + .../oneos-web-ops/pages/41-异动管理-新增.jsx | 605 + .../oneos-web-ops/pages/42-异动管理.jsx | 926 ++ .../oneos-web-ops/pages/43-证照管理-编辑.jsx | 2830 +++++ .../oneos-web-ops/pages/44-证照管理.jsx | 3093 +++++ .../oneos-web-ops/pages/45-停车场管理.jsx | 1321 ++ .../oneos-web-ops/pages/46-型号参数-编辑.jsx | 344 + .../oneos-web-ops/pages/47-型号参数-查看.jsx | 222 + .../oneos-web-ops/pages/48-型号参数-新增.jsx | 317 + .../oneos-web-ops/pages/49-型号参数.jsx | 268 + .../oneos-web-procurement/index.tsx | 23 + .../pages/01-三方退租车管理.jsx | 1136 ++ .../pages/02-三方退租申请-新增.jsx | 1652 +++ .../oneos-web-requirements/index.tsx | 58 + .../oneos-web-safety-training/index.tsx | 21 + .../pages/01-安全培训扫码.jsx | 613 + .../oneos-web-vehicle-asset/index.tsx | 23 + .../pages/01-车辆管理.jsx | 3331 +++++ .../pages/02-车辆管理-查看.jsx | 1583 +++ src/prototypes/oneos-web-workbench/index.tsx | 21 + .../oneos-web-workbench/pages/01-工作台.jsx | 2571 ++++ .../oneos-web-legacy/styles/README.md | 72 + .../styles/ant-table-global-fix.css | 37 + .../styles/contract-template.css | 386 + .../oneos-web-legacy/styles/index.css | 19 + .../styles/oneos-ant-table-global-fix.js | 14 + .../oneos-web-legacy/styles/oneos-app.css | 792 ++ .../styles/oneos-arco-theme-README.md | 7 + .../styles/oneos-arco-theme.css | 509 + .../oneos-web-legacy/styles/oneos-tokens.css | 185 + .../styles/vehicle-management.css | 803 ++ .../oneos-web-legacy/styles/vm-tokens.css | 5 + .../oneos-web-legacy/业务管理/ETC管理.jsx | 458 + .../export-tools/export-ai-package.mjs | 555 + .../业务管理/export-tools/package.json | 8 + .../export-tools/preview-bootstrap.html | 47 + .../业务管理/export-tools/source/保险采购.jsx | 8698 +++++++++++++ .../oneos-web-legacy/业务管理/交车任务.jsx | 1696 +++ .../业务管理/供应商管理-新增.jsx | 254 + .../业务管理/供应商管理-查看.jsx | 189 + .../业务管理/供应商管理-编辑.jsx | 277 + .../oneos-web-legacy/业务管理/供应商管理.jsx | 324 + .../oneos-web-legacy/业务管理/保险采购.jsx | 8698 +++++++++++++ .../业务管理/客户管理-新增.jsx | 249 + .../业务管理/客户管理-查看.jsx | 165 + .../业务管理/客户管理-编辑.jsx | 272 + .../oneos-web-legacy/业务管理/客户管理.jsx | 321 + .../业务管理/文档/_build_租赁账单手册.py | 187 + .../业务管理/新增交车任务.jsx | 343 + .../业务管理/查看交车任务.jsx | 380 + .../业务管理/租赁账单-收费明细.jsx | 449 + .../业务管理/租赁账单-查看.jsx | 345 + .../oneos-web-legacy/业务管理/租赁账单.jsx | 476 + .../业务管理/编辑交车任务.jsx | 342 + .../业务管理/车辆成本维护.jsx | 185 + .../docs/h2-station-manual-create-page.png | Bin 0 -> 1097239 bytes .../docs/h2-station-manual-list-page.png | Bin 0 -> 1046016 bytes .../docs/h2-station-manual-statement-flow.png | Bin 0 -> 1261829 bytes .../export-tools/export-ai-package.mjs | 667 + .../加氢站管理/export-tools/package.json | 8 + .../export-tools/preview-bootstrap.html | 45 + .../export-tools/source/站点信息.jsx | 9501 +++++++++++++++ .../oneos-web-legacy/加氢站管理/加氢订单.jsx | 181 + .../oneos-web-legacy/加氢站管理/加氢记录.jsx | 1807 +++ .../加氢站管理/站点信息-产品需求说明.md | 487 + .../oneos-web-legacy/加氢站管理/站点信息.jsx | 10042 +++++++++++++++ .../加氢站管理/站点管理使用说明书.md | 474 + .../docs/ONE-OS-租赁业务台账操作说明.pdf | Bin 0 -> 1530768 bytes .../docs/generate-lease-manual-pdf.mjs | 42 + .../docs/lease-bill-preview-screenshot.png | Bin 0 -> 89398 bytes .../台账数据/docs/租赁业务台账操作说明.html | 394 + .../台账数据/保险分摊明细.jsx | 515 + .../台账数据/氢费采购端汇总报表-需求文档.md | 199 + .../台账数据/氢费采购端汇总报表.jsx | 2329 ++++ .../台账数据/租赁业务台账.jsx | 3176 +++++ .../台账数据/车辆氢费明细-需求内容.js | 410 + .../台账数据/车辆氢费明细-需求文档.md | 408 + .../台账数据/车辆氢费明细-需求渲染.js | 336 + .../台账数据/车辆氢费明细.jsx | 4539 +++++++ .../台账数据/车辆维修明细.jsx | 1823 +++ .../oneos-web-legacy/合同模板管理.jsx | 2362 ++++ .../oneos-web-legacy/安全培训扫码.jsx | 611 + src/resources/oneos-web-legacy/工作台.jsx | 2569 ++++ .../oneos-web-legacy/帮助中心/功能说明书.jsx | 232 + .../oneos-web-legacy/数据分析/业务台账.jsx | 839 ++ .../数据分析/业务部业绩明细.jsx | 812 ++ .../oneos-web-legacy/数据分析/业务部台账.jsx | 2468 ++++ .../数据分析/业务部汇总台账.jsx | 559 + .../数据分析/客户服务部业务统计报表.jsx | 752 ++ .../数据分析/物流业务台账-产品需求说明.md | 212 + .../数据分析/物流业务台账-需求内容.js | 215 + .../数据分析/物流业务台账.jsx | 2547 ++++ .../数据分析/物流业务月度统计.jsx | 843 ++ .../数据分析/租赁客户氢费台账.jsx | 694 ++ .../数据分析/租赁车辆收入明细.jsx | 801 ++ src/resources/oneos-web-legacy/登录.jsx | 455 + .../财务管理/提车应收款-审核.jsx | 397 + .../财务管理/提车应收款-开票信息.jsx | 590 + .../财务管理/提车应收款-提车收款单.jsx | 554 + .../财务管理/提车应收款-查看.jsx | 295 + .../oneos-web-legacy/财务管理/提车应收款.jsx | 452 + .../财务管理/提车收款单-编辑.jsx | 537 + .../财务管理/文档/_build_还车应结款手册.py | 271 + .../文档/还车应结款-用户操作说明.docx | Bin 0 -> 108742 bytes .../文档/配图-还车应结款-查看页-原型结构.png | Bin 0 -> 5316 bytes .../文档/配图-还车应结款-费用明细-原型结构.png | Bin 0 -> 5931 bytes .../oneos-web-legacy/财务管理/租赁账单.jsx | 536 + .../财务管理/还车应结款-查看.jsx | 643 + .../财务管理/还车应结款-费用明细.jsx | 1525 +++ .../oneos-web-legacy/财务管理/还车应结款.jsx | 2070 ++++ .../车辆租赁合同-变更为三方合同.jsx | 459 + .../车辆租赁合同/车辆租赁合同-新增.jsx | 813 ++ .../车辆租赁合同/车辆租赁合同-新增车辆.jsx | 633 + .../车辆租赁合同/车辆租赁合同-查看.jsx | 560 + .../车辆租赁合同/车辆租赁合同-续签合同.jsx | 804 ++ .../车辆租赁合同/车辆租赁合同-转正式合同.jsx | 876 ++ .../车辆租赁合同/车辆租赁合同-附加费用.jsx | 356 + .../车辆租赁合同/车辆租赁合同.jsx | 1428 +++ .../oneos-web-legacy/车辆管理-查看.jsx | 1581 +++ src/resources/oneos-web-legacy/车辆管理.jsx | 3329 +++++ .../运维管理/基本数据维护/停车场管理.jsx | 1319 ++ .../运维管理/基本数据维护/型号参数-新增.jsx | 315 + .../运维管理/基本数据维护/型号参数-查看.jsx | 220 + .../运维管理/基本数据维护/型号参数-编辑.jsx | 342 + .../运维管理/基本数据维护/型号参数.jsx | 266 + .../运维管理/备件管理/仓库管理.jsx | 498 + .../运维管理/备件管理/备件库存.jsx | 1218 ++ .../运维管理/车辆业务/上牌管理.jsx | 938 ++ .../车辆业务/交车管理-交车单-查看.jsx | 494 + .../车辆业务/交车管理-交车单-编辑.jsx | 854 ++ .../运维管理/车辆业务/交车管理-交车单.jsx | 243 + .../运维管理/车辆业务/交车管理-编辑抽屉.jsx | 503 + .../运维管理/车辆业务/交车管理.jsx | 3360 ++++++ .../运维管理/车辆业务/后装设备.jsx | 481 + .../运维管理/车辆业务/备车-新增.jsx | 658 + .../运维管理/车辆业务/备车-查看.jsx | 313 + .../运维管理/车辆业务/备车-编辑.jsx | 623 + .../运维管理/车辆业务/备车管理.jsx | 727 ++ .../运维管理/车辆业务/年审管理-办理.jsx | 1115 ++ .../运维管理/车辆业务/年审管理-查看.jsx | 367 + .../运维管理/车辆业务/年审管理.jsx | 1728 +++ .../运维管理/车辆业务/异动管理-新增.jsx | 604 + .../运维管理/车辆业务/异动管理-查看.jsx | 403 + .../运维管理/车辆业务/异动管理-结束异动.jsx | 471 + .../运维管理/车辆业务/异动管理-编辑.jsx | 628 + .../运维管理/车辆业务/异动管理.jsx | 924 ++ .../运维管理/车辆业务/故障管理-编辑.jsx | 267 + .../运维管理/车辆业务/故障管理.jsx | 503 + .../运维管理/车辆业务/新增后装设备.jsx | 143 + .../运维管理/车辆业务/新增故障.jsx | 417 + .../运维管理/车辆业务/替换车管理-新增.jsx | 811 ++ .../运维管理/车辆业务/替换车管理-查看.jsx | 366 + .../运维管理/车辆业务/替换车管理-编辑.jsx | 851 ++ .../运维管理/车辆业务/替换车管理.jsx | 946 ++ .../运维管理/车辆业务/查看故障.jsx | 226 + .../运维管理/车辆业务/证照管理-编辑.jsx | 2830 +++++ .../运维管理/车辆业务/证照管理.jsx | 3093 +++++ .../车辆业务/调拨管理-业务提交-编辑.jsx | 464 + .../车辆业务/调拨管理-接收信息-司机运输.jsx | 519 + .../车辆业务/调拨管理-接收信息-第三方运输.jsx | 458 + .../运维管理/车辆业务/调拨管理-新增.jsx | 490 + .../运维管理/车辆业务/调拨管理-查看.jsx | 295 + .../运维管理/车辆业务/调拨管理-调拨信息.jsx | 646 + .../车辆业务/调拨管理-运维(接收)-编辑.jsx | 478 + .../车辆业务/调拨管理-运维(调拨方)-编辑.jsx | 633 + .../运维管理/车辆业务/调拨管理.jsx | 1004 ++ .../运维管理/车辆业务/还车管理-查看.jsx | 666 + .../运维管理/车辆业务/还车管理-还车.jsx | 1006 ++ .../运维管理/车辆业务/还车管理.jsx | 479 + .../采购管理/三方退租申请-新增.jsx | 1650 +++ .../采购管理/三方退租车管理.jsx | 1134 ++ .../需求说明/Antd设计规范(v5).md | 200 + .../oneos-web-legacy/需求说明/README.md | 3 + .../需求说明/业务管理/供应商管理 | 35 + .../需求说明/业务管理/客户管理 | 35 + .../需求说明/业务管理/租赁账单 | 48 + .../需求说明/业务管理/租赁账单-收费明细 | 51 + .../需求说明/业务管理/租赁账单-查看 | 50 + .../需求说明/业务管理/车辆成本维护 | 15 + .../oneos-web-legacy/需求说明/交车任务 | 45 + .../oneos-web-legacy/需求说明/交车任务-编辑 | 28 + .../oneos-web-legacy/需求说明/交车单-查看 | 34 + .../oneos-web-legacy/需求说明/交车管理 | 58 + .../oneos-web-legacy/需求说明/后装设备管理 | 24 + .../oneos-web-legacy/需求说明/后装设备管理2 | 33 + .../oneos-web-legacy/需求说明/工作台 | 52 + .../oneos-web-legacy/需求说明/新增交车任务 | 28 + .../oneos-web-legacy/需求说明/新增备车 | 26 + .../需求说明/租赁合同/租赁合同管理 | 71 + .../租赁合同/车辆租赁合同-变更为三方合同 | 125 + .../需求说明/租赁合同/车辆租赁合同-新增 | 108 + .../需求说明/租赁合同/车辆租赁合同-新增车辆 | 169 + .../需求说明/租赁合同/车辆租赁合同-查看 | 166 + .../需求说明/租赁合同/车辆租赁合同-续签合同 | 108 + .../需求说明/租赁合同/车辆租赁合同-转正式合同 | 108 + .../需求说明/财务管理/提车应收款 | 49 + .../需求说明/财务管理/提车应收款-审批 | 69 + .../需求说明/财务管理/提车应收款-开票信息 | 82 + .../需求说明/财务管理/提车应收款-提车收款单 | 70 + .../财务管理/提车应收款-提车收款单-编辑 | 68 + .../需求说明/财务管理/提车应收款-查看 | 83 + .../需求说明/财务管理/还车应结款 | 35 + .../需求说明/财务管理/还车应结款-费用明细 | 109 + .../需求说明/运维管理-仓库管理/交车任务 | 19 + .../需求说明/运维管理-仓库管理/备件库存 | 79 + .../需求说明/运维管理-车务管理/交车单-查看 | 34 + .../需求说明/运维管理-车务管理/交车明细-查看 | 31 + .../运维管理-车务管理/交车管理-交车单 | 35 + .../运维管理-车务管理/交车管理-交车单-编辑 | 42 + .../需求说明/运维管理-车务管理/备车管理 | 55 + .../需求说明/运维管理-车务管理/异动-新增 | 25 + .../需求说明/运维管理-车务管理/异动管理 | 51 + .../运维管理-车务管理/异动管理-结束异动 | 30 + .../运维管理-车务管理/提车首付款-收费 | 63 + .../需求说明/运维管理-车务管理/替换车管理 | 64 + .../运维管理-车务管理/替换车管理-新增 | 39 + .../运维管理-车务管理/替换车管理-查看 | 37 + .../运维管理-车务管理/替换车管理-编辑 | 39 + .../需求说明/运维管理-车务管理/调拨管理 | 54 + .../运维管理-车务管理/调拨管理-接收信息 | 35 + .../运维管理-车务管理/调拨管理-接收信息-司机运输 | 35 + .../调拨管理-接收信息-第三方运输 | 28 + .../运维管理-车务管理/调拨管理-接收记录 | 28 + .../需求说明/运维管理-车务管理/调拨管理-新增 | 20 + .../运维管理-车务管理/调拨管理-调拨信息 | 29 + .../需求说明/运维管理-车务管理/还车管理 | 51 + .../需求说明/运维管理-车务管理/还车管理-查看 | 46 + tsconfig.typecheck.json | 3 + 338 files changed, 259967 insertions(+), 1 deletion(-) create mode 100644 scripts/sync-oneos-web-prototypes.mjs create mode 100644 src/common/oneosWebLegacy/OneosWebLegacyShell.tsx create mode 100644 src/common/oneosWebLegacy/legacyGlobals.ts create mode 100644 src/prototypes/oneos-web-business/index.tsx create mode 100644 src/prototypes/oneos-web-business/pages/01-保险采购.jsx create mode 100644 src/prototypes/oneos-web-business/pages/02-编辑交车任务.jsx create mode 100644 src/prototypes/oneos-web-business/pages/03-查看交车任务.jsx create mode 100644 src/prototypes/oneos-web-business/pages/04-车辆成本维护.jsx create mode 100644 src/prototypes/oneos-web-business/pages/05-供应商管理-编辑.jsx create mode 100644 src/prototypes/oneos-web-business/pages/06-供应商管理-查看.jsx create mode 100644 src/prototypes/oneos-web-business/pages/07-供应商管理-新增.jsx create mode 100644 src/prototypes/oneos-web-business/pages/08-供应商管理.jsx create mode 100644 src/prototypes/oneos-web-business/pages/09-交车任务.jsx create mode 100644 src/prototypes/oneos-web-business/pages/10-客户管理-编辑.jsx create mode 100644 src/prototypes/oneos-web-business/pages/11-客户管理-查看.jsx create mode 100644 src/prototypes/oneos-web-business/pages/12-客户管理-新增.jsx create mode 100644 src/prototypes/oneos-web-business/pages/13-客户管理.jsx create mode 100644 src/prototypes/oneos-web-business/pages/14-新增交车任务.jsx create mode 100644 src/prototypes/oneos-web-business/pages/15-租赁账单-查看.jsx create mode 100644 src/prototypes/oneos-web-business/pages/16-租赁账单-收费明细.jsx create mode 100644 src/prototypes/oneos-web-business/pages/17-租赁账单.jsx create mode 100644 src/prototypes/oneos-web-business/pages/18-ETC管理.jsx create mode 100644 src/prototypes/oneos-web-contract-template/index.tsx create mode 100644 src/prototypes/oneos-web-contract-template/pages/01-合同模板管理.jsx create mode 100644 src/prototypes/oneos-web-data-analysis/index.tsx create mode 100644 src/prototypes/oneos-web-data-analysis/pages/01-客户服务部业务统计报表.jsx create mode 100644 src/prototypes/oneos-web-data-analysis/pages/02-物流业务台账.jsx create mode 100644 src/prototypes/oneos-web-data-analysis/pages/03-物流业务月度统计.jsx create mode 100644 src/prototypes/oneos-web-data-analysis/pages/04-业务部汇总台账.jsx create mode 100644 src/prototypes/oneos-web-data-analysis/pages/05-业务部台账.jsx create mode 100644 src/prototypes/oneos-web-data-analysis/pages/06-业务部业绩明细.jsx create mode 100644 src/prototypes/oneos-web-data-analysis/pages/07-业务台账.jsx create mode 100644 src/prototypes/oneos-web-data-analysis/pages/08-租赁车辆收入明细.jsx create mode 100644 src/prototypes/oneos-web-data-analysis/pages/09-租赁客户氢费台账.jsx create mode 100644 src/prototypes/oneos-web-finance/index.tsx create mode 100644 src/prototypes/oneos-web-finance/pages/01-还车应结款-查看.jsx create mode 100644 src/prototypes/oneos-web-finance/pages/02-还车应结款-费用明细.jsx create mode 100644 src/prototypes/oneos-web-finance/pages/03-还车应结款.jsx create mode 100644 src/prototypes/oneos-web-finance/pages/04-提车收款单-编辑.jsx create mode 100644 src/prototypes/oneos-web-finance/pages/05-提车应收款-查看.jsx create mode 100644 src/prototypes/oneos-web-finance/pages/06-提车应收款-开票信息.jsx create mode 100644 src/prototypes/oneos-web-finance/pages/07-提车应收款-审核.jsx create mode 100644 src/prototypes/oneos-web-finance/pages/08-提车应收款-提车收款单.jsx create mode 100644 src/prototypes/oneos-web-finance/pages/09-提车应收款.jsx create mode 100644 src/prototypes/oneos-web-finance/pages/10-租赁账单.jsx create mode 100644 src/prototypes/oneos-web-h2-station/index.tsx create mode 100644 src/prototypes/oneos-web-h2-station/pages/01-加氢订单.jsx create mode 100644 src/prototypes/oneos-web-h2-station/pages/02-加氢记录.jsx create mode 100644 src/prototypes/oneos-web-h2-station/pages/03-站点信息.jsx create mode 100644 src/prototypes/oneos-web-help-center/index.tsx create mode 100644 src/prototypes/oneos-web-help-center/pages/01-功能说明书.jsx create mode 100644 src/prototypes/oneos-web-lease-contract/index.tsx create mode 100644 src/prototypes/oneos-web-lease-contract/pages/01-车辆租赁合同-变更为三方合同.jsx create mode 100644 src/prototypes/oneos-web-lease-contract/pages/02-车辆租赁合同-查看.jsx create mode 100644 src/prototypes/oneos-web-lease-contract/pages/03-车辆租赁合同-附加费用.jsx create mode 100644 src/prototypes/oneos-web-lease-contract/pages/04-车辆租赁合同-新增.jsx create mode 100644 src/prototypes/oneos-web-lease-contract/pages/05-车辆租赁合同-新增车辆.jsx create mode 100644 src/prototypes/oneos-web-lease-contract/pages/06-车辆租赁合同-续签合同.jsx create mode 100644 src/prototypes/oneos-web-lease-contract/pages/07-车辆租赁合同-转正式合同.jsx create mode 100644 src/prototypes/oneos-web-lease-contract/pages/08-车辆租赁合同.jsx create mode 100644 src/prototypes/oneos-web-ledger-data/index.tsx create mode 100644 src/prototypes/oneos-web-ledger-data/pages/01-保险分摊明细.jsx create mode 100644 src/prototypes/oneos-web-ledger-data/pages/02-车辆氢费明细.jsx create mode 100644 src/prototypes/oneos-web-ledger-data/pages/03-车辆维修明细.jsx create mode 100644 src/prototypes/oneos-web-ledger-data/pages/04-氢费采购端汇总报表.jsx create mode 100644 src/prototypes/oneos-web-ledger-data/pages/05-租赁业务台账.jsx create mode 100644 src/prototypes/oneos-web-login/index.tsx create mode 100644 src/prototypes/oneos-web-login/pages/01-登录.jsx create mode 100644 src/prototypes/oneos-web-ops/index.tsx create mode 100644 src/prototypes/oneos-web-ops/pages/01-备件库存.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/02-仓库管理.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/03-备车-编辑.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/04-备车-查看.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/05-备车-新增.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/06-备车管理.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/07-查看故障.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/08-调拨管理-查看.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/09-调拨管理-调拨信息.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/10-调拨管理-接收信息-第三方运输.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/11-调拨管理-接收信息-司机运输.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/12-调拨管理-新增.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/13-调拨管理-业务提交-编辑.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/14-调拨管理-运维(调拨方)-编辑.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/15-调拨管理-运维(接收)-编辑.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/16-调拨管理.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/17-故障管理-编辑.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/18-故障管理.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/19-还车管理-查看.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/20-还车管理-还车.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/21-还车管理.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/22-后装设备.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/23-交车管理-编辑抽屉.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/24-交车管理-交车单-编辑.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/25-交车管理-交车单-查看.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/26-交车管理-交车单.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/27-交车管理.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/28-年审管理-办理.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/29-年审管理-查看.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/30-年审管理.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/31-上牌管理.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/32-替换车管理-编辑.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/33-替换车管理-查看.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/34-替换车管理-新增.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/35-替换车管理.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/36-新增故障.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/37-新增后装设备.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/38-异动管理-编辑.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/39-异动管理-查看.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/40-异动管理-结束异动.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/41-异动管理-新增.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/42-异动管理.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/43-证照管理-编辑.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/44-证照管理.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/45-停车场管理.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/46-型号参数-编辑.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/47-型号参数-查看.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/48-型号参数-新增.jsx create mode 100644 src/prototypes/oneos-web-ops/pages/49-型号参数.jsx create mode 100644 src/prototypes/oneos-web-procurement/index.tsx create mode 100644 src/prototypes/oneos-web-procurement/pages/01-三方退租车管理.jsx create mode 100644 src/prototypes/oneos-web-procurement/pages/02-三方退租申请-新增.jsx create mode 100644 src/prototypes/oneos-web-requirements/index.tsx create mode 100644 src/prototypes/oneos-web-safety-training/index.tsx create mode 100644 src/prototypes/oneos-web-safety-training/pages/01-安全培训扫码.jsx create mode 100644 src/prototypes/oneos-web-vehicle-asset/index.tsx create mode 100644 src/prototypes/oneos-web-vehicle-asset/pages/01-车辆管理.jsx create mode 100644 src/prototypes/oneos-web-vehicle-asset/pages/02-车辆管理-查看.jsx create mode 100644 src/prototypes/oneos-web-workbench/index.tsx create mode 100644 src/prototypes/oneos-web-workbench/pages/01-工作台.jsx create mode 100644 src/resources/oneos-web-legacy/styles/README.md create mode 100644 src/resources/oneos-web-legacy/styles/ant-table-global-fix.css create mode 100644 src/resources/oneos-web-legacy/styles/contract-template.css create mode 100644 src/resources/oneos-web-legacy/styles/index.css create mode 100644 src/resources/oneos-web-legacy/styles/oneos-ant-table-global-fix.js create mode 100644 src/resources/oneos-web-legacy/styles/oneos-app.css create mode 100644 src/resources/oneos-web-legacy/styles/oneos-arco-theme-README.md create mode 100644 src/resources/oneos-web-legacy/styles/oneos-arco-theme.css create mode 100644 src/resources/oneos-web-legacy/styles/oneos-tokens.css create mode 100644 src/resources/oneos-web-legacy/styles/vehicle-management.css create mode 100644 src/resources/oneos-web-legacy/styles/vm-tokens.css create mode 100644 src/resources/oneos-web-legacy/业务管理/ETC管理.jsx create mode 100644 src/resources/oneos-web-legacy/业务管理/export-tools/export-ai-package.mjs create mode 100644 src/resources/oneos-web-legacy/业务管理/export-tools/package.json create mode 100644 src/resources/oneos-web-legacy/业务管理/export-tools/preview-bootstrap.html create mode 100644 src/resources/oneos-web-legacy/业务管理/export-tools/source/保险采购.jsx create mode 100644 src/resources/oneos-web-legacy/业务管理/交车任务.jsx create mode 100644 src/resources/oneos-web-legacy/业务管理/供应商管理-新增.jsx create mode 100644 src/resources/oneos-web-legacy/业务管理/供应商管理-查看.jsx create mode 100644 src/resources/oneos-web-legacy/业务管理/供应商管理-编辑.jsx create mode 100644 src/resources/oneos-web-legacy/业务管理/供应商管理.jsx create mode 100644 src/resources/oneos-web-legacy/业务管理/保险采购.jsx create mode 100644 src/resources/oneos-web-legacy/业务管理/客户管理-新增.jsx create mode 100644 src/resources/oneos-web-legacy/业务管理/客户管理-查看.jsx create mode 100644 src/resources/oneos-web-legacy/业务管理/客户管理-编辑.jsx create mode 100644 src/resources/oneos-web-legacy/业务管理/客户管理.jsx create mode 100644 src/resources/oneos-web-legacy/业务管理/文档/_build_租赁账单手册.py create mode 100644 src/resources/oneos-web-legacy/业务管理/新增交车任务.jsx create mode 100644 src/resources/oneos-web-legacy/业务管理/查看交车任务.jsx create mode 100644 src/resources/oneos-web-legacy/业务管理/租赁账单-收费明细.jsx create mode 100644 src/resources/oneos-web-legacy/业务管理/租赁账单-查看.jsx create mode 100644 src/resources/oneos-web-legacy/业务管理/租赁账单.jsx create mode 100644 src/resources/oneos-web-legacy/业务管理/编辑交车任务.jsx create mode 100644 src/resources/oneos-web-legacy/业务管理/车辆成本维护.jsx create mode 100644 src/resources/oneos-web-legacy/加氢站管理/docs/h2-station-manual-create-page.png create mode 100644 src/resources/oneos-web-legacy/加氢站管理/docs/h2-station-manual-list-page.png create mode 100644 src/resources/oneos-web-legacy/加氢站管理/docs/h2-station-manual-statement-flow.png create mode 100644 src/resources/oneos-web-legacy/加氢站管理/export-tools/export-ai-package.mjs create mode 100644 src/resources/oneos-web-legacy/加氢站管理/export-tools/package.json create mode 100644 src/resources/oneos-web-legacy/加氢站管理/export-tools/preview-bootstrap.html create mode 100644 src/resources/oneos-web-legacy/加氢站管理/export-tools/source/站点信息.jsx create mode 100644 src/resources/oneos-web-legacy/加氢站管理/加氢订单.jsx create mode 100644 src/resources/oneos-web-legacy/加氢站管理/加氢记录.jsx create mode 100644 src/resources/oneos-web-legacy/加氢站管理/站点信息-产品需求说明.md create mode 100644 src/resources/oneos-web-legacy/加氢站管理/站点信息.jsx create mode 100644 src/resources/oneos-web-legacy/加氢站管理/站点管理使用说明书.md create mode 100644 src/resources/oneos-web-legacy/台账数据/docs/ONE-OS-租赁业务台账操作说明.pdf create mode 100644 src/resources/oneos-web-legacy/台账数据/docs/generate-lease-manual-pdf.mjs create mode 100644 src/resources/oneos-web-legacy/台账数据/docs/lease-bill-preview-screenshot.png create mode 100644 src/resources/oneos-web-legacy/台账数据/docs/租赁业务台账操作说明.html create mode 100644 src/resources/oneos-web-legacy/台账数据/保险分摊明细.jsx create mode 100644 src/resources/oneos-web-legacy/台账数据/氢费采购端汇总报表-需求文档.md create mode 100644 src/resources/oneos-web-legacy/台账数据/氢费采购端汇总报表.jsx create mode 100644 src/resources/oneos-web-legacy/台账数据/租赁业务台账.jsx create mode 100644 src/resources/oneos-web-legacy/台账数据/车辆氢费明细-需求内容.js create mode 100644 src/resources/oneos-web-legacy/台账数据/车辆氢费明细-需求文档.md create mode 100644 src/resources/oneos-web-legacy/台账数据/车辆氢费明细-需求渲染.js create mode 100644 src/resources/oneos-web-legacy/台账数据/车辆氢费明细.jsx create mode 100644 src/resources/oneos-web-legacy/台账数据/车辆维修明细.jsx create mode 100644 src/resources/oneos-web-legacy/合同模板管理.jsx create mode 100644 src/resources/oneos-web-legacy/安全培训扫码.jsx create mode 100644 src/resources/oneos-web-legacy/工作台.jsx create mode 100644 src/resources/oneos-web-legacy/帮助中心/功能说明书.jsx create mode 100644 src/resources/oneos-web-legacy/数据分析/业务台账.jsx create mode 100644 src/resources/oneos-web-legacy/数据分析/业务部业绩明细.jsx create mode 100644 src/resources/oneos-web-legacy/数据分析/业务部台账.jsx create mode 100644 src/resources/oneos-web-legacy/数据分析/业务部汇总台账.jsx create mode 100644 src/resources/oneos-web-legacy/数据分析/客户服务部业务统计报表.jsx create mode 100644 src/resources/oneos-web-legacy/数据分析/物流业务台账-产品需求说明.md create mode 100644 src/resources/oneos-web-legacy/数据分析/物流业务台账-需求内容.js create mode 100644 src/resources/oneos-web-legacy/数据分析/物流业务台账.jsx create mode 100644 src/resources/oneos-web-legacy/数据分析/物流业务月度统计.jsx create mode 100644 src/resources/oneos-web-legacy/数据分析/租赁客户氢费台账.jsx create mode 100644 src/resources/oneos-web-legacy/数据分析/租赁车辆收入明细.jsx create mode 100644 src/resources/oneos-web-legacy/登录.jsx create mode 100644 src/resources/oneos-web-legacy/财务管理/提车应收款-审核.jsx create mode 100644 src/resources/oneos-web-legacy/财务管理/提车应收款-开票信息.jsx create mode 100644 src/resources/oneos-web-legacy/财务管理/提车应收款-提车收款单.jsx create mode 100644 src/resources/oneos-web-legacy/财务管理/提车应收款-查看.jsx create mode 100644 src/resources/oneos-web-legacy/财务管理/提车应收款.jsx create mode 100644 src/resources/oneos-web-legacy/财务管理/提车收款单-编辑.jsx create mode 100644 src/resources/oneos-web-legacy/财务管理/文档/_build_还车应结款手册.py create mode 100644 src/resources/oneos-web-legacy/财务管理/文档/还车应结款-用户操作说明.docx create mode 100644 src/resources/oneos-web-legacy/财务管理/文档/配图-还车应结款-查看页-原型结构.png create mode 100644 src/resources/oneos-web-legacy/财务管理/文档/配图-还车应结款-费用明细-原型结构.png create mode 100644 src/resources/oneos-web-legacy/财务管理/租赁账单.jsx create mode 100644 src/resources/oneos-web-legacy/财务管理/还车应结款-查看.jsx create mode 100644 src/resources/oneos-web-legacy/财务管理/还车应结款-费用明细.jsx create mode 100644 src/resources/oneos-web-legacy/财务管理/还车应结款.jsx create mode 100644 src/resources/oneos-web-legacy/车辆租赁合同/车辆租赁合同-变更为三方合同.jsx create mode 100644 src/resources/oneos-web-legacy/车辆租赁合同/车辆租赁合同-新增.jsx create mode 100644 src/resources/oneos-web-legacy/车辆租赁合同/车辆租赁合同-新增车辆.jsx create mode 100644 src/resources/oneos-web-legacy/车辆租赁合同/车辆租赁合同-查看.jsx create mode 100644 src/resources/oneos-web-legacy/车辆租赁合同/车辆租赁合同-续签合同.jsx create mode 100644 src/resources/oneos-web-legacy/车辆租赁合同/车辆租赁合同-转正式合同.jsx create mode 100644 src/resources/oneos-web-legacy/车辆租赁合同/车辆租赁合同-附加费用.jsx create mode 100644 src/resources/oneos-web-legacy/车辆租赁合同/车辆租赁合同.jsx create mode 100644 src/resources/oneos-web-legacy/车辆管理-查看.jsx create mode 100644 src/resources/oneos-web-legacy/车辆管理.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/基本数据维护/停车场管理.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/基本数据维护/型号参数-新增.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/基本数据维护/型号参数-查看.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/基本数据维护/型号参数-编辑.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/基本数据维护/型号参数.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/备件管理/仓库管理.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/备件管理/备件库存.jsx create mode 100755 src/resources/oneos-web-legacy/运维管理/车辆业务/上牌管理.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/交车管理-交车单-查看.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/交车管理-交车单-编辑.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/交车管理-交车单.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/交车管理-编辑抽屉.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/交车管理.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/后装设备.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/备车-新增.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/备车-查看.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/备车-编辑.jsx create mode 100755 src/resources/oneos-web-legacy/运维管理/车辆业务/备车管理.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/年审管理-办理.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/年审管理-查看.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/年审管理.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/异动管理-新增.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/异动管理-查看.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/异动管理-结束异动.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/异动管理-编辑.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/异动管理.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/故障管理-编辑.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/故障管理.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/新增后装设备.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/新增故障.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/替换车管理-新增.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/替换车管理-查看.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/替换车管理-编辑.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/替换车管理.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/查看故障.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/证照管理-编辑.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/证照管理.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/调拨管理-业务提交-编辑.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/调拨管理-接收信息-司机运输.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/调拨管理-接收信息-第三方运输.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/调拨管理-新增.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/调拨管理-查看.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/调拨管理-调拨信息.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/调拨管理-运维(接收)-编辑.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/调拨管理-运维(调拨方)-编辑.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/调拨管理.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/还车管理-查看.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/还车管理-还车.jsx create mode 100644 src/resources/oneos-web-legacy/运维管理/车辆业务/还车管理.jsx create mode 100644 src/resources/oneos-web-legacy/采购管理/三方退租申请-新增.jsx create mode 100644 src/resources/oneos-web-legacy/采购管理/三方退租车管理.jsx create mode 100644 src/resources/oneos-web-legacy/需求说明/Antd设计规范(v5).md create mode 100644 src/resources/oneos-web-legacy/需求说明/README.md create mode 100644 src/resources/oneos-web-legacy/需求说明/业务管理/供应商管理 create mode 100644 src/resources/oneos-web-legacy/需求说明/业务管理/客户管理 create mode 100644 src/resources/oneos-web-legacy/需求说明/业务管理/租赁账单 create mode 100644 src/resources/oneos-web-legacy/需求说明/业务管理/租赁账单-收费明细 create mode 100644 src/resources/oneos-web-legacy/需求说明/业务管理/租赁账单-查看 create mode 100644 src/resources/oneos-web-legacy/需求说明/业务管理/车辆成本维护 create mode 100644 src/resources/oneos-web-legacy/需求说明/交车任务 create mode 100644 src/resources/oneos-web-legacy/需求说明/交车任务-编辑 create mode 100644 src/resources/oneos-web-legacy/需求说明/交车单-查看 create mode 100644 src/resources/oneos-web-legacy/需求说明/交车管理 create mode 100644 src/resources/oneos-web-legacy/需求说明/后装设备管理 create mode 100644 src/resources/oneos-web-legacy/需求说明/后装设备管理2 create mode 100644 src/resources/oneos-web-legacy/需求说明/工作台 create mode 100644 src/resources/oneos-web-legacy/需求说明/新增交车任务 create mode 100644 src/resources/oneos-web-legacy/需求说明/新增备车 create mode 100644 src/resources/oneos-web-legacy/需求说明/租赁合同/租赁合同管理 create mode 100644 src/resources/oneos-web-legacy/需求说明/租赁合同/车辆租赁合同-变更为三方合同 create mode 100644 src/resources/oneos-web-legacy/需求说明/租赁合同/车辆租赁合同-新增 create mode 100644 src/resources/oneos-web-legacy/需求说明/租赁合同/车辆租赁合同-新增车辆 create mode 100644 src/resources/oneos-web-legacy/需求说明/租赁合同/车辆租赁合同-查看 create mode 100644 src/resources/oneos-web-legacy/需求说明/租赁合同/车辆租赁合同-续签合同 create mode 100644 src/resources/oneos-web-legacy/需求说明/租赁合同/车辆租赁合同-转正式合同 create mode 100644 src/resources/oneos-web-legacy/需求说明/财务管理/提车应收款 create mode 100644 src/resources/oneos-web-legacy/需求说明/财务管理/提车应收款-审批 create mode 100644 src/resources/oneos-web-legacy/需求说明/财务管理/提车应收款-开票信息 create mode 100644 src/resources/oneos-web-legacy/需求说明/财务管理/提车应收款-提车收款单 create mode 100644 src/resources/oneos-web-legacy/需求说明/财务管理/提车应收款-提车收款单-编辑 create mode 100644 src/resources/oneos-web-legacy/需求说明/财务管理/提车应收款-查看 create mode 100644 src/resources/oneos-web-legacy/需求说明/财务管理/还车应结款 create mode 100644 src/resources/oneos-web-legacy/需求说明/财务管理/还车应结款-费用明细 create mode 100644 src/resources/oneos-web-legacy/需求说明/运维管理-仓库管理/交车任务 create mode 100644 src/resources/oneos-web-legacy/需求说明/运维管理-仓库管理/备件库存 create mode 100644 src/resources/oneos-web-legacy/需求说明/运维管理-车务管理/交车单-查看 create mode 100644 src/resources/oneos-web-legacy/需求说明/运维管理-车务管理/交车明细-查看 create mode 100644 src/resources/oneos-web-legacy/需求说明/运维管理-车务管理/交车管理-交车单 create mode 100644 src/resources/oneos-web-legacy/需求说明/运维管理-车务管理/交车管理-交车单-编辑 create mode 100644 src/resources/oneos-web-legacy/需求说明/运维管理-车务管理/备车管理 create mode 100644 src/resources/oneos-web-legacy/需求说明/运维管理-车务管理/异动-新增 create mode 100644 src/resources/oneos-web-legacy/需求说明/运维管理-车务管理/异动管理 create mode 100644 src/resources/oneos-web-legacy/需求说明/运维管理-车务管理/异动管理-结束异动 create mode 100644 src/resources/oneos-web-legacy/需求说明/运维管理-车务管理/提车首付款-收费 create mode 100644 src/resources/oneos-web-legacy/需求说明/运维管理-车务管理/替换车管理 create mode 100644 src/resources/oneos-web-legacy/需求说明/运维管理-车务管理/替换车管理-新增 create mode 100644 src/resources/oneos-web-legacy/需求说明/运维管理-车务管理/替换车管理-查看 create mode 100644 src/resources/oneos-web-legacy/需求说明/运维管理-车务管理/替换车管理-编辑 create mode 100644 src/resources/oneos-web-legacy/需求说明/运维管理-车务管理/调拨管理 create mode 100644 src/resources/oneos-web-legacy/需求说明/运维管理-车务管理/调拨管理-接收信息 create mode 100644 src/resources/oneos-web-legacy/需求说明/运维管理-车务管理/调拨管理-接收信息-司机运输 create mode 100644 src/resources/oneos-web-legacy/需求说明/运维管理-车务管理/调拨管理-接收信息-第三方运输 create mode 100644 src/resources/oneos-web-legacy/需求说明/运维管理-车务管理/调拨管理-接收记录 create mode 100644 src/resources/oneos-web-legacy/需求说明/运维管理-车务管理/调拨管理-新增 create mode 100644 src/resources/oneos-web-legacy/需求说明/运维管理-车务管理/调拨管理-调拨信息 create mode 100644 src/resources/oneos-web-legacy/需求说明/运维管理-车务管理/还车管理 create mode 100644 src/resources/oneos-web-legacy/需求说明/运维管理-车务管理/还车管理-查看 diff --git a/.axhub/make/sidebar-tree.json b/.axhub/make/sidebar-tree.json index 1356447..4e622f8 100644 --- a/.axhub/make/sidebar-tree.json +++ b/.axhub/make/sidebar-tree.json @@ -1,6 +1,6 @@ { "version": 1, - "updatedAt": "2026-06-30T07:13:33.620Z", + "updatedAt": "2026-06-30T08:18:11.135Z", "prototypes": [ { "id": "item:prototypes:insurance-procurement", @@ -55,6 +55,103 @@ "kind": "item", "title": "自营业务台账", "itemKey": "prototypes/self-operated-business-ledger" + }, + { + "id": "folder-prototypes-oneos-web", + "kind": "folder", + "title": "ONE-OS Web 端", + "children": [ + { + "id": "item:prototypes:oneos-web-workbench", + "kind": "item", + "title": "工作台", + "itemKey": "prototypes/oneos-web-workbench" + }, + { + "id": "item:prototypes:oneos-web-login", + "kind": "item", + "title": "登录", + "itemKey": "prototypes/oneos-web-login" + }, + { + "id": "item:prototypes:oneos-web-safety-training", + "kind": "item", + "title": "安全培训扫码", + "itemKey": "prototypes/oneos-web-safety-training" + }, + { + "id": "item:prototypes:oneos-web-vehicle-asset", + "kind": "item", + "title": "车辆管理", + "itemKey": "prototypes/oneos-web-vehicle-asset" + }, + { + "id": "item:prototypes:oneos-web-contract-template", + "kind": "item", + "title": "合同模板管理", + "itemKey": "prototypes/oneos-web-contract-template" + }, + { + "id": "item:prototypes:oneos-web-help-center", + "kind": "item", + "title": "帮助中心", + "itemKey": "prototypes/oneos-web-help-center" + }, + { + "id": "item:prototypes:oneos-web-finance", + "kind": "item", + "title": "财务管理", + "itemKey": "prototypes/oneos-web-finance" + }, + { + "id": "item:prototypes:oneos-web-procurement", + "kind": "item", + "title": "采购管理", + "itemKey": "prototypes/oneos-web-procurement" + }, + { + "id": "item:prototypes:oneos-web-lease-contract", + "kind": "item", + "title": "车辆租赁合同", + "itemKey": "prototypes/oneos-web-lease-contract" + }, + { + "id": "item:prototypes:oneos-web-h2-station", + "kind": "item", + "title": "加氢站管理", + "itemKey": "prototypes/oneos-web-h2-station" + }, + { + "id": "item:prototypes:oneos-web-data-analysis", + "kind": "item", + "title": "数据分析", + "itemKey": "prototypes/oneos-web-data-analysis" + }, + { + "id": "item:prototypes:oneos-web-ledger-data", + "kind": "item", + "title": "台账数据", + "itemKey": "prototypes/oneos-web-ledger-data" + }, + { + "id": "item:prototypes:oneos-web-business", + "kind": "item", + "title": "业务管理", + "itemKey": "prototypes/oneos-web-business" + }, + { + "id": "item:prototypes:oneos-web-ops", + "kind": "item", + "title": "运维管理", + "itemKey": "prototypes/oneos-web-ops" + }, + { + "id": "item:prototypes:oneos-web-requirements", + "kind": "item", + "title": "需求说明", + "itemKey": "prototypes/oneos-web-requirements" + } + ] } ], "docs": [], diff --git a/package.json b/package.json index 9bef51b..347cb13 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "dev": "node scripts/sync-vendor-if-present.mjs && vite", "start": "npm run dev", "metadata:sync": "node scripts/sync-project-metadata.mjs", + "oneos-web:sync": "node scripts/sync-oneos-web-prototypes.mjs && node scripts/scan-entries.js && npm run metadata:sync", "build": "node scripts/sync-vendor-if-present.mjs && node scripts/scan-entries.js && node scripts/build-all.js", "preview": "vite preview", "typecheck": "node scripts/sync-vendor-if-present.mjs && tsc --noEmit -p tsconfig.typecheck.json", diff --git a/scripts/sync-oneos-web-prototypes.mjs b/scripts/sync-oneos-web-prototypes.mjs new file mode 100644 index 0000000..846320b --- /dev/null +++ b/scripts/sync-oneos-web-prototypes.mjs @@ -0,0 +1,270 @@ +#!/usr/bin/env node +/** + * 从 src/resources/oneos-web-legacy 生成 ONE-OS Web 端原型与侧边栏菜单项。 + * + * 用法:node scripts/sync-oneos-web-prototypes.mjs + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const projectRoot = path.resolve(__dirname, '..'); +const legacyRoot = path.join(projectRoot, 'src/resources/oneos-web-legacy'); +const prototypesRoot = path.join(projectRoot, 'src/prototypes'); +const sidebarPath = path.join(projectRoot, '.axhub/make/sidebar-tree.json'); + +const PAGE_ID_RE = /^[a-z0-9-]+$/u; + +const MODULES = [ + { slug: 'oneos-web-workbench', title: '工作台', files: ['工作台.jsx'] }, + { slug: 'oneos-web-login', title: '登录', files: ['登录.jsx'] }, + { slug: 'oneos-web-safety-training', title: '安全培训扫码', files: ['安全培训扫码.jsx'] }, + { slug: 'oneos-web-vehicle-asset', title: '车辆管理', files: ['车辆管理.jsx', '车辆管理-查看.jsx'] }, + { slug: 'oneos-web-contract-template', title: '合同模板管理', files: ['合同模板管理.jsx'] }, + { slug: 'oneos-web-help-center', title: '帮助中心', dir: '帮助中心' }, + { slug: 'oneos-web-finance', title: '财务管理', dir: '财务管理', excludeDirs: ['文档'] }, + { slug: 'oneos-web-procurement', title: '采购管理', dir: '采购管理' }, + { slug: 'oneos-web-lease-contract', title: '车辆租赁合同', dir: '车辆租赁合同' }, + { slug: 'oneos-web-h2-station', title: '加氢站管理', dir: '加氢站管理', excludeDirs: ['export-tools', 'AI-加氢站站点信息-complete'] }, + { slug: 'oneos-web-data-analysis', title: '数据分析', dir: '数据分析' }, + { slug: 'oneos-web-ledger-data', title: '台账数据', dir: '台账数据', excludeDirs: ['docs'] }, + { slug: 'oneos-web-business', title: '业务管理', dir: '业务管理', excludeDirs: ['文档', 'export-tools', 'AI-保险采购-complete'] }, + { slug: 'oneos-web-ops', title: '运维管理', dir: '运维管理' }, + { slug: 'oneos-web-requirements', title: '需求说明', dir: '需求说明', docsOnly: true }, +]; + +function slugifyPageId(relPath) { + const stem = relPath.replace(/\.jsx$/u, '').replace(/[\\/]/g, '-'); + const ascii = stem.replace(/[^\x00-\x7F]/gu, (ch) => `u${ch.charCodeAt(0).toString(16)}`); + let id = ascii.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, ''); + if (!id || !PAGE_ID_RE.test(id)) { + id = `p${Math.abs(hashCode(relPath)).toString(36)}`; + } + return id.slice(0, 48); +} + +function hashCode(value) { + let hash = 0; + for (let i = 0; i < value.length; i += 1) { + hash = ((hash << 5) - hash) + value.charCodeAt(i); + hash |= 0; + } + return hash; +} + +function collectJsxFiles(module) { + if (module.docsOnly) { + return []; + } + if (module.files) { + return module.files.map((file) => ({ + rel: file, + abs: path.join(legacyRoot, file), + title: file.replace(/\.jsx$/u, ''), + })); + } + const baseDir = path.join(legacyRoot, module.dir); + const results = []; + const walk = (dir, prefix = '') => { + if (!fs.existsSync(dir)) return; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (entry.name.startsWith('.')) continue; + const full = path.join(dir, entry.name); + const rel = path.posix.join(module.dir, prefix, entry.name).replace(/\\/g, '/'); + if (entry.isDirectory()) { + if (module.excludeDirs?.includes(entry.name)) continue; + walk(full, prefix ? `${prefix}/${entry.name}` : entry.name); + continue; + } + if (!entry.name.endsWith('.jsx')) continue; + results.push({ + rel, + abs: full, + title: entry.name.replace(/\.jsx$/u, ''), + }); + } + }; + walk(baseDir); + return results.sort((a, b) => a.rel.localeCompare(b.rel, 'zh-CN')); +} + +function prepareLegacyJsx(sourcePath, targetPath) { + const raw = fs.readFileSync(sourcePath, 'utf8'); + let content = raw; + if (!/export\s+default\s+Component/u.test(content)) { + content = `${content.trim()}\n\nexport default Component;\n`; + } + fs.mkdirSync(path.dirname(targetPath), { recursive: true }); + fs.writeFileSync(targetPath, content, 'utf8'); +} + +function writeRequirementsPrototype(module) { + const protoDir = path.join(prototypesRoot, module.slug); + fs.mkdirSync(protoDir, { recursive: true }); + const indexTsx = `/** + * @name ${module.title} + * 自 ONE-OS web端/需求说明 归档 + */ +import React, { useMemo, useState } from 'react'; +import { Card, Col, Input, List, Row, Typography } from 'antd'; + +const DOC_MAP = import.meta.glob('../../resources/oneos-web-legacy/需求说明/**/*', { + query: '?raw', + import: 'default', + eager: true, +}); + +const FILES = Object.keys(DOC_MAP) + .map((key) => key.replace('../../resources/oneos-web-legacy/需求说明/', '')) + .filter(Boolean) + .sort((a, b) => a.localeCompare(b, 'zh-CN')) + .map((rel) => ({ rel, title: rel })); + +export default function OneosWebRequirementsPage() { + const [query, setQuery] = useState(''); + const [active, setActive] = useState(FILES[0]?.rel ?? ''); + + const filtered = useMemo(() => { + const q = query.trim(); + if (!q) return FILES; + return FILES.filter((item) => item.title.includes(q)); + }, [query]); + + const content = useMemo(() => { + if (!active) return ''; + const key = \`../../resources/oneos-web-legacy/需求说明/\${active}\`; + return String(DOC_MAP[key] ?? '无法加载文档'); + }, [active]); + + return ( + + + setQuery(e.target.value)} style={{ width: 160 }} />}> + ( + setActive(item.rel)}> + {item.title} + + )} + /> + + + + + {content} + + + + ); +} +`; + fs.writeFileSync(path.join(protoDir, 'index.tsx'), indexTsx, 'utf8'); +} + +function writePrototype(module, pages) { + const protoDir = path.join(prototypesRoot, module.slug); + const pagesDir = path.join(protoDir, 'pages'); + fs.mkdirSync(pagesDir, { recursive: true }); + + const pageEntries = []; + const importLines = []; + const usedIds = new Set(); + + pages.forEach((page, index) => { + const fileName = `${String(index + 1).padStart(2, '0')}-${path.basename(page.rel)}`; + const target = path.join(pagesDir, fileName); + prepareLegacyJsx(page.abs, target); + const importName = `Page${index + 1}`; + importLines.push(`import ${importName} from './pages/${fileName}';`); + let id = slugifyPageId(page.rel); + while (usedIds.has(id)) id = `${id}-${index + 1}`; + usedIds.add(id); + pageEntries.push({ id, title: page.title, importName }); + }); + + const pagesArray = pageEntries.map((p) => ` { id: '${p.id}', title: '${p.title.replace(/'/g, "\\'")}', component: ${p.importName} },`).join('\n'); + + const indexTsx = `/** + * @name ${module.title} + * 自 ONE-OS web端 原稿复刻(${pages.length} 个页面) + */ +import React from 'react'; +${importLines.join('\n')} +import { OneosWebLegacyShell } from '../../common/oneosWebLegacy/OneosWebLegacyShell'; + +const pages = [ +${pagesArray} +]; + +export default function ${toComponentName(module.slug)}() { + return ( + + ); +} +`; + fs.writeFileSync(path.join(protoDir, 'index.tsx'), indexTsx, 'utf8'); +} + +function toComponentName(slug) { + return slug + .split('-') + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(''); +} + +function updateSidebar(modules) { + const sidebar = JSON.parse(fs.readFileSync(sidebarPath, 'utf8')); + const prefix = 'item:prototypes:oneos-web-'; + const existing = (sidebar.prototypes ?? []).filter((item) => !String(item.id).startsWith(prefix) && !String(item.itemKey).startsWith('prototypes/oneos-web-')); + + const oneosFolder = { + id: 'folder-prototypes-oneos-web', + kind: 'folder', + title: 'ONE-OS Web 端', + defaultExpanded: true, + children: modules.map((module) => ({ + id: `item:prototypes:${module.slug}`, + kind: 'item', + title: module.title, + itemKey: `prototypes/${module.slug}`, + })), + }; + + sidebar.prototypes = [...existing, oneosFolder]; + sidebar.updatedAt = new Date().toISOString(); + fs.writeFileSync(sidebarPath, `${JSON.stringify(sidebar, null, 2)}\n`, 'utf8'); +} + +function main() { + if (!fs.existsSync(legacyRoot)) { + console.error('缺少 src/resources/oneos-web-legacy,请先复制 ONE-OS web端 资料。'); + process.exit(1); + } + + for (const module of MODULES) { + if (module.docsOnly) { + writeRequirementsPrototype(module); + console.log(`[ok] ${module.slug} (需求说明文档浏览)`); + continue; + } + const pages = collectJsxFiles(module); + if (!pages.length) { + console.warn(`[skip] ${module.slug}: 未找到 jsx 页面`); + continue; + } + writePrototype(module, pages); + console.log(`[ok] ${module.slug} (${pages.length} pages)`); + } + + updateSidebar(MODULES); + console.log('已更新 .axhub/make/sidebar-tree.json'); +} + +main(); diff --git a/src/common/oneosWebLegacy/OneosWebLegacyShell.tsx b/src/common/oneosWebLegacy/OneosWebLegacyShell.tsx new file mode 100644 index 0000000..7195802 --- /dev/null +++ b/src/common/oneosWebLegacy/OneosWebLegacyShell.tsx @@ -0,0 +1,86 @@ +import React, { useMemo } from 'react'; +import { ConfigProvider, Layout, Menu, Typography } from 'antd'; +import { defineHashPageRoute, useHashPage } from '../useHashPage'; +import { ensureOneosWebLegacyGlobals } from './legacyGlobals'; + +import '../../resources/oneos-web-legacy/styles/oneos-app.css'; +import '../../resources/oneos-web-legacy/styles/oneos-tokens.css'; +import '../../resources/oneos-web-legacy/styles/index.css'; + +export interface OneosWebLegacyPage { + id: string; + title: string; + component: React.ComponentType; +} + +export interface OneosWebLegacyShellProps { + moduleTitle: string; + pages: OneosWebLegacyPage[]; + defaultPageId?: string; +} + +const oneosTheme = { + token: { + colorPrimary: '#165dff', + borderRadius: 8, + fontFamily: + 'Inter, -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif', + }, +}; + +export function OneosWebLegacyShell({ + moduleTitle, + pages, + defaultPageId, +}: OneosWebLegacyShellProps) { + ensureOneosWebLegacyGlobals(); + + const route = useMemo( + () => defineHashPageRoute( + pages.map((page) => ({ id: page.id, title: page.title })), + { defaultPageId: defaultPageId ?? pages[0]?.id }, + ), + [pages, defaultPageId], + ); + const { page, setPage } = useHashPage(route); + const active = pages.find((item) => item.id === page) ?? pages[0]; + + if (!active) { + return ( + + 未配置可预览页面 + + ); + } + + const ActivePage = active.component; + const showMenu = pages.length > 1; + + return ( + + + {showMenu ? ( + +
{moduleTitle}
+ ({ + key: item.id, + label: item.title, + onClick: () => setPage(item.id), + }))} + /> + + ) : null} + + + + + + ); +} diff --git a/src/common/oneosWebLegacy/legacyGlobals.ts b/src/common/oneosWebLegacy/legacyGlobals.ts new file mode 100644 index 0000000..1898f97 --- /dev/null +++ b/src/common/oneosWebLegacy/legacyGlobals.ts @@ -0,0 +1,18 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import * as antd from 'antd'; + +declare global { + interface Window { + React: typeof React; + ReactDOM: typeof ReactDOM; + antd: typeof antd; + } +} + +export function ensureOneosWebLegacyGlobals(): void { + if (typeof window === 'undefined') return; + window.React = React; + window.ReactDOM = ReactDOM; + window.antd = antd; +} diff --git a/src/prototypes/oneos-web-business/index.tsx b/src/prototypes/oneos-web-business/index.tsx new file mode 100644 index 0000000..2203a98 --- /dev/null +++ b/src/prototypes/oneos-web-business/index.tsx @@ -0,0 +1,55 @@ +/** + * @name 业务管理 + * 自 ONE-OS web端 原稿复刻(18 个页面) + */ +import React from 'react'; +import Page1 from './pages/01-保险采购.jsx'; +import Page2 from './pages/02-编辑交车任务.jsx'; +import Page3 from './pages/03-查看交车任务.jsx'; +import Page4 from './pages/04-车辆成本维护.jsx'; +import Page5 from './pages/05-供应商管理-编辑.jsx'; +import Page6 from './pages/06-供应商管理-查看.jsx'; +import Page7 from './pages/07-供应商管理-新增.jsx'; +import Page8 from './pages/08-供应商管理.jsx'; +import Page9 from './pages/09-交车任务.jsx'; +import Page10 from './pages/10-客户管理-编辑.jsx'; +import Page11 from './pages/11-客户管理-查看.jsx'; +import Page12 from './pages/12-客户管理-新增.jsx'; +import Page13 from './pages/13-客户管理.jsx'; +import Page14 from './pages/14-新增交车任务.jsx'; +import Page15 from './pages/15-租赁账单-查看.jsx'; +import Page16 from './pages/16-租赁账单-收费明细.jsx'; +import Page17 from './pages/17-租赁账单.jsx'; +import Page18 from './pages/18-ETC管理.jsx'; +import { OneosWebLegacyShell } from '../../common/oneosWebLegacy/OneosWebLegacyShell'; + +const pages = [ + { id: 'u4e1au52a1u7ba1u7406-u4fddu9669u91c7u8d2d', title: '保险采购', component: Page1 }, + { id: 'u4e1au52a1u7ba1u7406-u7f16u8f91u4ea4u8f66u4efbu5', title: '编辑交车任务', component: Page2 }, + { id: 'u4e1au52a1u7ba1u7406-u67e5u770bu4ea4u8f66u4efbu5', title: '查看交车任务', component: Page3 }, + { id: 'u4e1au52a1u7ba1u7406-u8f66u8f86u6210u672cu7ef4u6', title: '车辆成本维护', component: Page4 }, + { id: 'u4e1au52a1u7ba1u7406-u4f9bu5e94u5546u7ba1u7406-u', title: '供应商管理-编辑', component: Page5 }, + { id: 'u4e1au52a1u7ba1u7406-u4f9bu5e94u5546u7ba1u7406-u-6', title: '供应商管理-查看', component: Page6 }, + { id: 'u4e1au52a1u7ba1u7406-u4f9bu5e94u5546u7ba1u7406-u-7', title: '供应商管理-新增', component: Page7 }, + { id: 'u4e1au52a1u7ba1u7406-u4f9bu5e94u5546u7ba1u7406', title: '供应商管理', component: Page8 }, + { id: 'u4e1au52a1u7ba1u7406-u4ea4u8f66u4efbu52a1', title: '交车任务', component: Page9 }, + { id: 'u4e1au52a1u7ba1u7406-u5ba2u6237u7ba1u7406-u7f16u', title: '客户管理-编辑', component: Page10 }, + { id: 'u4e1au52a1u7ba1u7406-u5ba2u6237u7ba1u7406-u67e5u', title: '客户管理-查看', component: Page11 }, + { id: 'u4e1au52a1u7ba1u7406-u5ba2u6237u7ba1u7406-u65b0u', title: '客户管理-新增', component: Page12 }, + { id: 'u4e1au52a1u7ba1u7406-u5ba2u6237u7ba1u7406', title: '客户管理', component: Page13 }, + { id: 'u4e1au52a1u7ba1u7406-u65b0u589eu4ea4u8f66u4efbu5', title: '新增交车任务', component: Page14 }, + { id: 'u4e1au52a1u7ba1u7406-u79dfu8d41u8d26u5355-u67e5u', title: '租赁账单-查看', component: Page15 }, + { id: 'u4e1au52a1u7ba1u7406-u79dfu8d41u8d26u5355-u6536u', title: '租赁账单-收费明细', component: Page16 }, + { id: 'u4e1au52a1u7ba1u7406-u79dfu8d41u8d26u5355', title: '租赁账单', component: Page17 }, + { id: 'u4e1au52a1u7ba1u7406-etcu7ba1u7406', title: 'ETC管理', component: Page18 }, +]; + +export default function OneosWebBusiness() { + return ( + + ); +} diff --git a/src/prototypes/oneos-web-business/pages/01-保险采购.jsx b/src/prototypes/oneos-web-business/pages/01-保险采购.jsx new file mode 100644 index 0000000..dc34610 --- /dev/null +++ b/src/prototypes/oneos-web-business/pages/01-保险采购.jsx @@ -0,0 +1,8698 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 业务管理 - 保险采购 +// 模块:① 比价单(选车报价 → 保存 → 按最晚付费日临期/超期提醒 → 勾选提交采购审批) +// ② 保单管理(一车一档台账,OCR/导入/逐条录入,与比价单不关联) +// 与车辆管理「保险状态」联动:交强险 + 商业险均存在且在有效期内为正常,否则异常(禁止交车) + +const { useState, useMemo, useCallback, useRef } = React; +const moment = window.moment || window.dayjs; +const antd = window.antd; +const { + Input, + Select, + Button, + Card, + Table, + Badge, + Tooltip, + Modal, + Tag, + message, + Popover, + Alert, + Checkbox, + DatePicker, + InputNumber, + Space, + Radio, + Upload, + Progress, + Dropdown, + Tabs, +} = antd; + +const ANCHOR_TODAY = '2026-06-01'; +const IPC_STORAGE_KEY = 'oneos_ipc_insurance_v1'; +const IPC_COMPARE_SHEETS_KEY = 'oneos_ipc_compare_sheets_v1'; +const IPC_POLICY_RECOGN_TASKS_KEY = 'oneos_ipc_policy_recogn_tasks_v2'; +const IPC_INSURANCE_HISTORY_EDITS_KEY = 'oneos_ipc_insurance_history_edits_v1'; +const IPC_EDIT_PLATE_KEY = 'oneos_ipc_edit_plate'; +const NO_PLATE_LABEL = '暂无车牌'; +const PROTO_COMPARE_CREATOR = '张明辉'; + +const INSURANCE_TYPE_ITEMS = [ + { key: 'compulsory', label: '交强', fullLabel: '交强险' }, + { key: 'commercial', label: '商业', fullLabel: '商业险' }, + { key: 'excess', label: '超赔', fullLabel: '超赔险' }, + { key: 'cargo', label: '货物', fullLabel: '货物险' }, + { key: 'driverAccident', label: '驾意', fullLabel: '驾意险' }, +]; + +const CORE_INSURANCE_KEYS = ['compulsory', 'commercial']; +/** 比价单采购流程中视为「已占用比价单」的状态(审批中、审批通过;不含撤回、审批驳回) */ +const ACTIVE_COMPARE_PROCUREMENT_STATUSES = ['submitted', 'approved']; + +const normalizeCompareProcurementStatus = (status) => ( + status === 'completed' ? 'approved' : (status || 'none') +); + +const isCompareProcurementSelectionDisabled = (status) => { + const st = normalizeCompareProcurementStatus(status); + return st === 'submitted' || st === 'approved'; +}; + +/** 采购状态展示(撤回、审批驳回、审批通过由工作流回写,本页只读展示) */ +const COMPARE_PROCUREMENT_STATUS_META = { + none: { label: '未提交', color: 'default' }, + submitted: { label: '审批中', color: 'processing' }, + approved: { label: '审批通过', color: 'success' }, + withdrawn: { label: '撤回', color: 'default' }, + rejected: { label: '审批驳回', color: 'error' }, +}; + +const renderCompareProcurementStatusTag = (status) => { + const st = normalizeCompareProcurementStatus(status); + const meta = COMPARE_PROCUREMENT_STATUS_META[st] || COMPARE_PROCUREMENT_STATUS_META.none; + return {meta.label}; +}; + +/** KPI 临期/逾期弹窗:比价采购状态标签(与列表采购状态文案略有差异) */ +const ALERT_COMPARE_PROCUREMENT_STATUS_META = { + submitted: { label: '审批中', color: 'processing' }, + approved: { label: '审批完成', color: 'success' }, + withdrawn: { label: '撤回', color: 'default' }, + rejected: { label: '驳回', color: 'error' }, +}; + +const COMPARE_PROCUREMENT_STATUS_PRIORITY = { + submitted: 4, + approved: 3, + rejected: 2, + withdrawn: 1, +}; + +const renderAlertCompareProcurementTag = (status) => { + const st = normalizeCompareProcurementStatus(status); + const meta = ALERT_COMPARE_PROCUREMENT_STATUS_META[st]; + if (!meta) return null; + return {meta.label}; +}; + +const buildCompareProcurementStatusByVehicleType = (compareSheets) => { + const map = new Map(); + (compareSheets || []).forEach((sheet) => { + (sheet.rows || []).forEach((row) => { + const key = buildCompareSubmissionKey(row, row.insuranceType); + if (!key) return; + const st = normalizeCompareProcurementStatus(row.procurementStatus); + if (st === 'none') return; + const prev = map.get(key); + const prevP = prev ? (COMPARE_PROCUREMENT_STATUS_PRIORITY[prev] ?? 0) : 0; + const nextP = COMPARE_PROCUREMENT_STATUS_PRIORITY[st] ?? 0; + if (nextP >= prevP) map.set(key, st); + }); + }); + return map; +}; + +const getCompareProcurementStatusForVehicleType = (vehicle, insuranceTypeLabel, statusMap) => { + const key = buildCompareSubmissionKey(vehicle, insuranceTypeLabel); + if (!key || !statusMap) return null; + return statusMap.get(key) || null; +}; + +/** 工作流当前审批人样例(正式环境由审批流接口回写,非必填) */ +const MOCK_WORKFLOW_CURRENT_APPROVERS = ['李专员', '王专员', '张明辉', '陈高伟', '赵六']; + +const pickMockWorkflowCurrentApprover = (rowId) => { + const hash = String(rowId || '').split('').reduce((sum, ch) => sum + ch.charCodeAt(0), 0); + return MOCK_WORKFLOW_CURRENT_APPROVERS[hash % MOCK_WORKFLOW_CURRENT_APPROVERS.length]; +}; +const INSURANCE_WARN_DAYS = 30; +/** 比价单:最晚付费日期 ≤ 该天数视为临期 */ +const LATEST_PAY_WARN_DAYS = 3; + +const INSURANCE_LABEL_TO_KEY = { + 交强险: 'compulsory', + 商业险: 'commercial', + 超赔险: 'excess', + 货物险: 'cargo', + 驾意险: 'driverAccident', +}; + +const INSURANCE_KEY_TO_LABEL = { + compulsory: '交强险', + commercial: '商业险', + excess: '超赔险', + cargo: '货物险', + driverAccident: '驾意险', +}; + +const EXPIRING_WARN_TYPE_KEYS = INSURANCE_TYPE_ITEMS.map((item) => item.key); + +const getVehicleInsuranceEndDate = (ledgerKey, typeKey, allInsurance) => { + const item = allInsurance[ledgerKey]?.[typeKey]; + if (!item?.endDate || !item?.policyNo) return ''; + return item.endDate; +}; + +const compareInsuranceEndDate = (dateA, dateB, order) => { + const av = dateA || ''; + const bv = dateB || ''; + if (!av && !bv) return 0; + if (!av) return 1; + if (!bv) return -1; + const cmp = String(av).localeCompare(String(bv)); + return order === 'ascend' ? cmp : -cmp; +}; + +const POLICY_OCR_MODES = [ + { key: 'policy', label: '保单录入', desc: '选择险种后上传附件,自动识别保单要素并匹配台账' }, + { key: 'suspend', label: '停保', desc: '上传停保/停驶批单,识别保单号与车牌后停保' }, + { key: 'resume', label: '复驶', desc: '上传复驶批单,识别保单号、恢复时间与新到期日期' }, + { key: 'cancel', label: '退保', desc: '上传退保批单,识别保单号、退保时间与退保金额' }, +]; + +const POLICY_BIZ_TYPE_OPTIONS = [ + { value: 'policy', label: '保单录入' }, + { value: 'suspend', label: '停租' }, + { value: 'resume', label: '复驶' }, + { value: 'cancel', label: '退保' }, +]; + +const EMPTY_POLICY_DETAIL = { + plateNo: '', + vin: '', + insuranceType: '交强险', + bizType: 'policy', + company: '', + policyNo: '', + endorsementNo: '', + payTime: '', + startDate: '', + endDate: '', + reinstateDate: '', + premium: '', + coverageItems: [], + applicant: '', + insured: '', + vehicleOwner: '', + signDate: '', + suspendTime: '', + resumeTime: '', + newEndDate: '', + cancelTime: '', + attachments: [], +}; + +const EMPTY_COVERAGE_ITEM = { + coverageName: '', + coverageAmount: '', + deductible: '', + itemPremium: '', +}; + +/** 保单项目/责任限额:表单为结构化列表;导入/OCR/台账可为分号拼接文本 */ +const parseCoverageItemsInput = (raw) => { + if (Array.isArray(raw)) { + return raw.map((s) => String(s ?? '').trim()).filter(Boolean); + } + const text = String(raw ?? '').trim(); + if (!text) return []; + const segments = text.split(/[;;\n]+/).map((s) => s.trim()).filter(Boolean); + const expanded = []; + segments.forEach((part) => { + const sub = part.split(/、/).map((s) => s.trim()).filter(Boolean); + if (sub.length > 1 && part.length <= 120) { + expanded.push(...sub); + } else { + expanded.push(part); + } + }); + return expanded; +}; + +const normalizeCoverageItem = (raw) => { + if (raw && typeof raw === 'object' && !Array.isArray(raw)) { + return { + coverageName: String(raw.coverageName ?? raw.name ?? '').trim(), + coverageAmount: String(raw.coverageAmount ?? '').trim(), + deductible: String(raw.deductible ?? '').trim(), + itemPremium: String(raw.itemPremium ?? raw.premium ?? '').trim(), + }; + } + const text = String(raw ?? '').trim(); + return text ? { ...EMPTY_COVERAGE_ITEM, coverageName: text } : { ...EMPTY_COVERAGE_ITEM }; +}; + +const normalizeCoverageItems = (raw) => { + if (!raw) return []; + if (Array.isArray(raw)) { + return raw + .map(normalizeCoverageItem) + .filter((item) => item.coverageName || item.coverageAmount || item.deductible || item.itemPremium); + } + return parseCoverageItemsInput(raw).map((name) => ({ ...EMPTY_COVERAGE_ITEM, coverageName: name })); +}; + +const serializeCoverageItems = (items) => ( + normalizeCoverageItems(items).map((row) => { + const parts = [ + row.coverageName, + row.coverageAmount && `保额${row.coverageAmount}`, + row.deductible && `免额${row.deductible}`, + row.itemPremium && `保费${row.itemPremium}元`, + ].filter(Boolean); + return parts.join(' '); + }).join(';') +); + +const getCoverageItemsFormRows = (items) => { + const list = normalizeCoverageItems(items); + return list.length ? list.map((item) => ({ ...EMPTY_COVERAGE_ITEM, ...item })) : [{ ...EMPTY_COVERAGE_ITEM }]; +}; + +const buildSampleCoverageItemsForRecognEdit = (insuranceType, premiumTotal) => { + const total = (premiumTotal || '').trim(); + const samples = { + 交强险: [ + { coverageName: '死亡伤残赔偿', coverageAmount: '180000元', deductible: '—', itemPremium: '850.00' }, + { coverageName: '医疗费用赔偿', coverageAmount: '18000元', deductible: '—', itemPremium: '283.00' }, + { coverageName: '财产损失赔偿', coverageAmount: '2000元', deductible: '—', itemPremium: '110.00' }, + ], + 商业险: [ + { coverageName: '机动车损失险', coverageAmount: '350000元', deductible: '绝对免赔额500元', itemPremium: '5200.00' }, + { coverageName: '第三者责任险', coverageAmount: '2000000元', deductible: '—', itemPremium: '6100.00' }, + { coverageName: '车上人员责任险(司机)', coverageAmount: '20000元', deductible: '—', itemPremium: '850.00' }, + { coverageName: '车上人员责任险(乘客)', coverageAmount: '20000元/座', deductible: '—', itemPremium: '650.50' }, + ], + 超赔险: [ + { coverageName: '超赔责任险', coverageAmount: '10000000元', deductible: '—', itemPremium: '1200.00' }, + { coverageName: '附加超额第三者责任', coverageAmount: '5000000元', deductible: '—', itemPremium: '300.00' }, + ], + 货物险: [ + { coverageName: '公路货物运输定额保险', coverageAmount: '500000元', deductible: '每次事故免赔1000元', itemPremium: '1800.00' }, + { coverageName: '集装箱货物及其箱体', coverageAmount: '200000元', deductible: '—', itemPremium: '420.00' }, + ], + 驾意险: [ + { coverageName: '驾乘意外身故伤残', coverageAmount: '500000元/座', deductible: '—', itemPremium: '220.00' }, + { coverageName: '驾乘意外医疗', coverageAmount: '50000元/座', deductible: '免赔额100元', itemPremium: '160.00' }, + ], + }; + const rows = (samples[insuranceType] || samples.交强险).map((row) => ({ ...EMPTY_COVERAGE_ITEM, ...row })); + if (total && rows.length) { + const sum = rows.reduce((acc, row) => acc + (parseFloat(row.itemPremium) || 0), 0); + const target = parseFloat(total); + if (!Number.isNaN(target) && sum > 0 && Math.abs(sum - target) > 0.01) { + const last = rows[rows.length - 1]; + const adjust = (target - sum + (parseFloat(last.itemPremium) || 0)).toFixed(2); + last.itemPremium = adjust; + } + } + return rows; +}; + +const enrichPolicyDetailCoverageForEdit = (detail) => { + const normalized = enrichSuspendPolicyDetail(detail); + if (normalized.bizType === 'suspend') return normalized; + const items = normalizeCoverageItems(normalized.coverageItems); + const needsSample = !items.length || items.every((i) => !i.coverageAmount && !i.deductible && !i.itemPremium); + if (needsSample) { + return { + ...normalized, + coverageItems: buildSampleCoverageItemsForRecognEdit(normalized.insuranceType, normalized.premium), + }; + } + return { ...normalized, coverageItems: items }; +}; + +/** 保单 OCR 识别不反写承保险种明细(保额/免赔/分项保费) */ +const stripPolicyRecognCoverageFields = (detail) => { + const d = normalizePolicyDetail(detail); + return { ...d, coverageItems: [] }; +}; + +/** 基于用户提供的真实保单/批单样本(PDF 解析 + 文件名) */ +const REFERENCE_POLICY_OCR_MOCKS = [ + { + test: (n) => /沪BDB9161.*交强险/i.test(n), + detail: { + plateNo: '沪BDB9161', vin: 'LC0DF4CD8S0303140', insuranceType: '交强险', bizType: 'policy', + policyNo: 'ASHZ001CTP26B187065J', endorsementNo: 'DZQA26480000279515', + company: '中国太平洋财产保险股份有限公司', + payTime: '2026-06-01 17:42:10', signDate: '2026-05-27', startDate: '2026-06-05', endDate: '2027-06-04', + premium: '1243.00', + coverageItems: [ + { coverageName: '死亡伤残赔偿', coverageAmount: '180000元', deductible: '—', itemPremium: '850.00' }, + { coverageName: '医疗费用赔偿', coverageAmount: '18000元', deductible: '—', itemPremium: '283.00' }, + { coverageName: '财产损失赔偿', coverageAmount: '2000元', deductible: '—', itemPremium: '110.00' }, + ], + applicant: '上海羚牛氢运物联网科技有限公司', insured: '上海羚牛氢运物联网科技有限公司', + vehicleOwner: '上海羚牛氢运物联网科技有限公司', + }, + }, + { + test: (n) => /粤AGP9827.*商业险/i.test(n), + detail: { + plateNo: '粤AGP9827', insuranceType: '商业险', bizType: 'policy', + policyNo: '2050AA330400260000GV', company: '紫金财产保险股份有限公司', + payTime: '2026-05-28 10:00', startDate: '2026-06-06', endDate: '2027-05-27', premium: '12800', + applicant: '羚牛运营(广东)', insured: '羚牛运营(广东)', vehicleOwner: '羚牛运营(广东)', + coverageItems: [ + { coverageName: '机动车损失险', coverageAmount: '350000元', deductible: '绝对免赔额500元', itemPremium: '5200.00' }, + { coverageName: '第三者责任险', coverageAmount: '2000000元', deductible: '—', itemPremium: '6100.00' }, + { coverageName: '车上人员责任险', coverageAmount: '20000元/座', deductible: '—', itemPremium: '1500.00' }, + ], + }, + }, + { + test: (n) => /粤AGP3071.*驾意/i.test(n), + detail: { + plateNo: '粤AGP3071', insuranceType: '驾意险', bizType: 'policy', + policyNo: 'JY2026AGP3071001', company: '中国平安财产保险股份有限公司', + startDate: '2026-06-06', endDate: '2027-05-27', premium: '380.00', + coverageItems: '驾乘意外险,每座身故伤残/医疗限额', + }, + }, + { + test: (n) => /粤AGR9766.*超赔/i.test(n), + detail: { + plateNo: '粤AGR9766', vin: 'LB9A32A21R0LS1478', insuranceType: '超赔险', bizType: 'policy', + policyNo: '6260828000909X006408', company: '国任财产保险股份有限公司广州市番禺支公司', + payTime: '2026-04-16 14:37:37', signDate: '2026-04-16', startDate: '2026-04-17', endDate: '2027-04-16', + premium: '1500.00', + coverageItems: '公路货物运输定额保险;累计赔偿限额10001000元;主险货物保险金额1000元', + applicant: '羚牛氢能科技(广东)有限公司', insured: '羚牛氢能科技(广东)有限公司', + vehicleOwner: '羚牛氢能科技(广东)有限公司', + }, + }, + { + test: (n) => /货物险|20208A330400240001QX/i.test(n), + detail: { + plateNo: '浙F05178F', insuranceType: '货物险', bizType: 'policy', + policyNo: '20208A330400240001QX', company: '紫金财产保险股份有限公司', + payTime: '2024-10-17 15:58:05', startDate: '2024-10-18', endDate: '2025-10-17', premium: '1500.00', + coverageItems: '公路货物运输定额保险 CNY500000;集装箱货物及其箱体', + applicant: '嘉兴羚牛汽车服务有限公司', insured: '嘉兴羚牛汽车服务有限公司', + vehicleOwner: '嘉兴羚牛汽车服务有限公司', + }, + }, + { + test: (n) => /粤A03423F.*停驶/i.test(n), + detail: { + plateNo: '粤A03423F', insuranceType: '商业险', bizType: 'suspend', + policyNo: '2050AA3304002600002EM', endorsementNo: '3050AA3304002600002EM01', + company: '紫金财产保险股份有限公司', payTime: '2026-04-16 16:13:53', + startDate: '2026-04-17', endDate: '2027-03-31', reinstateDate: '2027-03-31', + suspendTime: '2026-04-17', resumeTime: '2027-03-31', newEndDate: '2027-03-31', + coverageItems: '停驶批单:保险车辆停驶,停驶期间保险责任中止', + }, + }, + { + test: (n) => /粤A06290F.*复驶/i.test(n), + detail: { + plateNo: '粤A06290F', insuranceType: '商业险', bizType: 'resume', + policyNo: '2050AA33040026000226', endorsementNo: '3050AA3304002600022602', + company: '紫金财产保险股份有限公司', payTime: '2026-04-30 14:25:43', + resumeTime: '2026-05-06', newEndDate: '2027-03-27', + startDate: '2026-05-06', endDate: '2027-03-27', reinstateDate: '2026-05-06', + coverageItems: '复驶批单:停驶车辆恢复行驶,保险责任自复驶日起恢复', + }, + }, + { + test: (n) => /浙F03220F.*复驶|BSHZ001S2024B005477B/i.test(n), + detail: { + plateNo: '浙F03220F', insuranceType: '商业险', bizType: 'resume', + policyNo: 'BSHZ001S2024B005477B', endorsementNo: 'BSHZ001S2024B005477E', + company: '中国太平洋财产保险股份有限公司', + resumeTime: '2026-05-01', newEndDate: '2027-04-30', + startDate: '2026-05-01', endDate: '2027-04-30', + coverageItems: '复驶批单', + }, + }, + { + test: (n) => /沪A06192F.*停保|BSHZ001S2024B005054V/i.test(n), + detail: { + plateNo: '沪A06192F', insuranceType: '商业险', bizType: 'suspend', + policyNo: 'BSHZ001S2024B005054V', endorsementNo: 'BSHZ001S2024B005054E', + company: '中国太平洋财产保险股份有限公司', startDate: '2025-12-05', endDate: '2026-03-05', + reinstateDate: '2026-06-01', suspendTime: '2025-12-05', resumeTime: '2026-06-01', newEndDate: '2026-03-05', + coverageItems: '停保批单', + }, + }, + { + test: (n) => /粤A03331F.*退保/i.test(n), + detail: { + plateNo: '粤A03331F', insuranceType: '商业险', bizType: 'cancel', + policyNo: '2050AA330400260000GV', endorsementNo: '3050AA330400260000GV02', + company: '紫金财产保险股份有限公司', payTime: '2026-05-27 17:51:20', + cancelTime: '2026-05-28', startDate: '2026-05-28', endDate: '2026-05-28', premium: '7853.27', + coverageItems: '商业险退保批单,退还保费', + }, + }, + { + test: (n) => /粤AGR0772.*停保/i.test(n), + detail: { + plateNo: '粤AGR0772', insuranceType: '商业险', bizType: 'suspend', + policyNo: 'PAIC-SY-AGR0772-2025', company: '中国平安财产保险股份有限公司', + startDate: '2025-12-05', endDate: '2026-03-05', reinstateDate: '2026-06-01', + suspendTime: '2025-12-05', resumeTime: '2026-06-01', newEndDate: '2026-03-05', + coverageItems: '商业险停保', + }, + }, +]; + +const normalizePolicyDetail = (raw = {}) => ({ + ...EMPTY_POLICY_DETAIL, + ...raw, + plateNo: (raw.plateNo || '').trim(), + vin: (raw.vin || '').trim(), + insuranceType: raw.insuranceType || '交强险', + bizType: raw.bizType || 'policy', + coverageItems: normalizeCoverageItems(raw.coverageItems), +}); + +const enrichSuspendPolicyDetail = (raw = {}) => { + const d = normalizePolicyDetail(raw); + if (d.bizType !== 'suspend') return d; + const suspendTime = (d.suspendTime || d.startDate || '').trim(); + const resumeTime = (d.resumeTime || d.reinstateDate || '').trim(); + const newEndDate = (d.newEndDate || d.endDate || '').trim(); + return { + ...d, + suspendTime, + resumeTime, + newEndDate, + startDate: suspendTime || d.startDate, + reinstateDate: resumeTime || d.reinstateDate, + endDate: newEndDate || d.endDate, + }; +}; + +const enrichResumePolicyDetail = (raw = {}) => { + const d = normalizePolicyDetail(raw); + if (d.bizType !== 'resume') return d; + const resumeTime = (d.resumeTime || d.reinstateDate || d.startDate || '').trim(); + const newEndDate = (d.newEndDate || d.endDate || '').trim(); + return { + ...d, + resumeTime, + newEndDate, + reinstateDate: resumeTime || d.reinstateDate, + endDate: newEndDate || d.endDate, + startDate: resumeTime || d.startDate, + }; +}; + +const enrichCancelPolicyDetail = (raw = {}) => { + const d = normalizePolicyDetail(raw); + if (d.bizType !== 'cancel') return d; + const cancelTime = (d.cancelTime || d.startDate || d.endDate || '').trim(); + const refundAmount = String(d.premium || '').trim(); + return { + ...d, + cancelTime, + startDate: cancelTime || d.startDate, + endDate: cancelTime || d.endDate, + premium: refundAmount || d.premium || '', + }; +}; + +const SUSPEND_RECOGN_FORM_REQUIRED_KEYS = [ + 'policyNo', + 'suspendTime', + 'newEndDate', +]; + +const RESUME_RECOGN_FORM_REQUIRED_KEYS = [ + 'policyNo', + 'resumeTime', + 'newEndDate', +]; + +const CANCEL_RECOGN_FORM_REQUIRED_KEYS = [ + 'policyNo', + 'cancelTime', + 'premium', +]; + +const renderSuspendTooltipTitle = (recordOrItem) => { + if (!recordOrItem) return null; + const detail = recordOrItem.policyDetail || {}; + const suspendTime = recordOrItem.suspendTime || detail.suspendTime || detail.startDate || recordOrItem.startDate || ''; + const resumeTime = recordOrItem.resumeTime || detail.resumeTime || detail.reinstateDate || recordOrItem.reinstateDate || ''; + const newEndDate = recordOrItem.newEndDate || detail.newEndDate || recordOrItem.endDate || detail.endDate || ''; + return ( +
+
中止时间:{suspendTime || '—'}
+
恢复时间:{resumeTime || '—'}
+
新到期日期:{newEndDate || '—'}
+
+ ); +}; + +const inferPolicyDetailFromFileName = (fileName) => { + const name = fileName || ''; + const plateMatch = name.match(/[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼][A-Z][A-Z0-9]{4,6}[A-Z0-9挂学警港澳]?/i); + const plateNo = plateMatch ? plateMatch[0].toUpperCase() : ''; + let insuranceType = '交强险'; + if (/商业险|商业/.test(name)) insuranceType = '商业险'; + else if (/超赔/.test(name)) insuranceType = '超赔险'; + else if (/驾意/.test(name)) insuranceType = '驾意险'; + else if (/货物/.test(name)) insuranceType = '货物险'; + else if (/交强/.test(name)) insuranceType = '交强险'; + let bizType = 'policy'; + if (/停驶|停保/.test(name)) bizType = 'suspend'; + else if (/复驶/.test(name)) bizType = 'resume'; + else if (/退保/.test(name)) bizType = 'cancel'; + const policyNoMatch = name.match(/BSHZ\d+[A-Z0-9]+|ASHZ\d+[A-Z0-9]+|202\d{2}A\d+QX|2050AA\d+[A-Z0-9]+|6260\d+X\d+/i); + const rangeMatch = name.match(/(\d{4})[.\-](\d{1,2})[.\-](\d{1,2})/); + let startDate = ''; + let endDate = ''; + if (rangeMatch) { + const y = rangeMatch[1]; + const m = String(rangeMatch[2]).padStart(2, '0'); + const d = String(rangeMatch[3]).padStart(2, '0'); + startDate = `${y}-${m}-${d}`; + const parts = name.match(/(\d{4})[.\-](\d{1,2})[.\-](\d{1,2}).*?(\d{4})[.\-](\d{1,2})[.\-](\d{1,2})/); + if (parts) { + endDate = `${parts[4]}-${String(parts[5]).padStart(2, '0')}-${String(parts[6]).padStart(2, '0')}`; + } + } + return normalizePolicyDetail({ + plateNo, + insuranceType, + bizType, + policyNo: policyNoMatch ? policyNoMatch[0] : '', + startDate, + endDate, + }); +}; + +const resolvePolicyDetailFromFileName = (fileName) => { + const ref = REFERENCE_POLICY_OCR_MOCKS.find((m) => m.test(fileName)); + if (ref) { + if (ref.detail.bizType === 'suspend') return enrichSuspendPolicyDetail(ref.detail); + if (ref.detail.bizType === 'resume') return enrichResumePolicyDetail(ref.detail); + if (ref.detail.bizType === 'cancel') return enrichCancelPolicyDetail(ref.detail); + return normalizePolicyDetail(ref.detail); + } + return inferPolicyDetailFromFileName(fileName); +}; + +const bizTypeToRecognMode = (bizType) => ( + bizType === 'suspend' ? 'suspend' : bizType === 'resume' ? 'resume' : bizType === 'cancel' ? 'cancel' : 'policy' +); + +/** 任务级 mode 与单条 result.recognMode 合并,停保/复驶/退保任务优先于 result 内嵌字段 */ +const resolvePolicyRecognEffectiveMode = (taskMode, result) => { + if (result?.recognMode && result.recognMode !== 'policy') return result.recognMode; + if (taskMode && taskMode !== 'policy') return taskMode; + return bizTypeToRecognMode(result?.ocrBizType || result?.policyDetail?.bizType); +}; + +const POLICY_RECOGN_CONFIRM_HINT = { + policy: '左侧预览保单原件,右侧核对识别反写的车牌、车主、投保人、被保险人、保险公司、保单号、收费确认时间、生效/到期日期及保险费合计;带 * 为必填项。', + suspend: '左侧预览停保批单原件,右侧核对「保单号」「中止时间」「恢复时间」「新到期日期」;带 * 为必填项。确认后新到期日期将写入台账该保单的到期日期。', + resume: '左侧预览复驶批单原件,右侧核对「保单号」「恢复时间」「新到期日期」;带 * 为必填项。确认后新到期日期将写入台账该保单的到期日期。', + cancel: '左侧预览退保批单原件,右侧核对「保单号」「退保时间」「退保金额」;带 * 为必填项。', +}; + +const applyPolicyDetailToInsuranceItem = (item, detail, mode) => { + const d = normalizePolicyDetail(detail); + const next = { ...item }; + next.company = d.company || next.company; + next.policyNo = d.policyNo || next.policyNo; + next.endorsementNo = d.endorsementNo || next.endorsementNo || ''; + next.startDate = d.startDate || next.startDate; + next.endDate = d.endDate || next.endDate; + next.premium = d.premium || next.premium; + next.payTime = d.payTime || next.payTime || ''; + next.signDate = d.signDate || next.signDate || ''; + next.coverageItems = serializeCoverageItems(d.coverageItems) || next.coverageItems || ''; + next.applicant = d.applicant || next.applicant || ''; + next.insured = d.insured || next.insured || ''; + next.vehicleOwner = d.vehicleOwner || next.vehicleOwner || ''; + if (Array.isArray(d.attachments)) { + next.attachments = d.attachments; + } + if (mode === 'policy') { + next.policyTag = ''; + next.reinstateDate = ''; + } else if (mode === 'suspend') { + const suspendTime = (d.suspendTime || d.startDate || '').trim(); + const resumeTime = (d.resumeTime || d.reinstateDate || '').trim(); + const newEndDate = (d.newEndDate || d.endDate || '').trim(); + next.policyTag = 'suspended'; + next.suspendTime = suspendTime; + next.resumeTime = resumeTime; + next.reinstateDate = resumeTime || next.reinstateDate || ''; + if (newEndDate) next.endDate = newEndDate; + } else if (mode === 'resume') { + const resumeTime = (d.resumeTime || d.reinstateDate || '').trim(); + const newEndDate = (d.newEndDate || d.endDate || '').trim(); + next.policyTag = ''; + next.resumeTime = resumeTime; + next.reinstateDate = resumeTime || next.reinstateDate || ''; + if (newEndDate) next.endDate = newEndDate; + } else if (mode === 'cancel') { + const cancelTime = (d.cancelTime || d.startDate || d.endDate || '').trim(); + next.policyTag = 'cancelled'; + next.reinstateDate = ''; + next.cancelTime = cancelTime; + if (d.premium) next.premium = normalizeRecognPremiumAmount(d.premium) || d.premium; + } + return next; +}; + +const createPolicyRecognTaskId = () => `TASK-${Date.now().toString().slice(-8)}`; + +const isPolicyRecognImageOrPdf = (file) => { + const name = (file?.name || '').toLowerCase(); + const type = (file?.type || '').toLowerCase(); + return type.includes('pdf') || type.startsWith('image/') || /\.(pdf|png|jpe?g|webp|bmp|gif|tiff?)$/i.test(name); +}; + +const isPolicyImportExcelFile = (file) => { + const name = (file?.name || '').toLowerCase(); + const type = (file?.type || '').toLowerCase(); + return /\.(csv|xlsx|xls)$/i.test(name) + || type.includes('csv') + || type.includes('spreadsheet') + || type.includes('excel'); +}; + +/** 新增/续保录入必填项(车牌与 VIN 至少填一项;承保险种明细非必填) */ +const POLICY_ENTRY_REQUIRED_FIELDS = [ + { key: 'insuranceType', label: '险种' }, + { key: 'company', label: '保险公司' }, + { key: 'policyNo', label: '保单号' }, + { key: 'startDate', label: '生效日期' }, + { key: 'endDate', label: '到期日期' }, + { key: 'premium', label: '保险费合计' }, +]; + +const POLICY_ENTRY_FORM_REQUIRED_KEYS = [ + 'plateNo', + ...POLICY_ENTRY_REQUIRED_FIELDS.map((item) => item.key), +]; + +const validatePolicyEntryDetail = (detail, options = {}) => { + const d = normalizePolicyDetail(detail); + const { silent = false, rowIndex } = options; + const rowPrefix = rowIndex != null ? `第 ${rowIndex} 行:` : ''; + if (!d.plateNo && !d.vin) { + if (!silent) message.warning(`${rowPrefix}车牌号与 VIN 至少填一项`); + return { ok: false, label: '车牌号或 VIN' }; + } + const missing = POLICY_ENTRY_REQUIRED_FIELDS.find(({ key }) => !String(d[key] || '').trim()); + if (missing) { + if (!silent) message.warning(`${rowPrefix}请填写${missing.label}`); + return { ok: false, label: missing.label }; + } + return { ok: true }; +}; + +/** 批量导入模板列:与「新增保单」表单一致,仅用于新增/续保;带 * 为必填 */ +const POLICY_IMPORT_TEMPLATE_COLUMNS = [ + { header: '车牌号', key: 'plateNo', required: true, sample: '沪BDB9161' }, + { header: 'VIN码', key: 'vin', required: false, sample: 'LC0DF4CD8S0303140' }, + { header: '险种', key: 'insuranceType', required: true, sample: '交强险' }, + { header: '保险公司', key: 'company', required: true, sample: '中国太平洋财产保险股份有限公司' }, + { header: '保单号', key: 'policyNo', required: true, sample: 'ASHZ001CTP26B187065J' }, + { header: '批单号', key: 'endorsementNo', required: false, sample: '' }, + { header: '付款时间', key: 'payTime', required: false, sample: '2026-06-01 17:42:10' }, + { header: '签单日期', key: 'signDate', required: false, sample: '2026-05-27' }, + { header: '生效日期', key: 'startDate', required: true, sample: '2026-06-05' }, + { header: '到期日期', key: 'endDate', required: true, sample: '2027-06-04' }, + { header: '保险费合计', key: 'premium', required: true, sample: '1243.00' }, + { header: '投保人', key: 'applicant', required: false, sample: '上海羚牛氢运物联网科技有限公司' }, + { header: '被保险人', key: 'insured', required: false, sample: '上海羚牛氢运物联网科技有限公司' }, + { header: '承保险种', key: 'coverageName', required: false, sample: '机动车第三者责任险' }, + { header: '保险金额', key: 'coverageAmount', required: false, sample: '2000000元' }, + { header: '保险金额/责任免额', key: 'coverageDeductible', required: false, sample: '绝对免赔额500元' }, + { header: '保险费', key: 'coveragePremium', required: false, sample: '1280.00' }, +]; + +const POLICY_IMPORT_TEMPLATE_HEADERS = POLICY_IMPORT_TEMPLATE_COLUMNS.map((col) => ( + col.required ? `${col.header}*` : col.header +)); +const POLICY_IMPORT_TEMPLATE_SAMPLE_ROW = POLICY_IMPORT_TEMPLATE_COLUMNS.map((col) => col.sample); + +const POLICY_IMPORT_HEADER_ALIASES = { + 车牌: 'plateNo', + VIN: 'vin', + 车辆识别代码: 'vin', + 保险类型: 'insuranceType', + 生效日: 'startDate', + 生效时间: 'startDate', + 起保日期: 'startDate', + 到期日: 'endDate', + 到期时间: 'endDate', + '保费(元)': 'premium', + 保费: 'premium', + 保单项目: 'coverageItems', + 责任免额: 'coverageDeductible', +}; + +const finalizePolicyImportRow = (row) => { + const coverageName = (row.coverageName || '').trim(); + const coverageAmount = (row.coverageAmount || '').trim(); + const coverageDeductible = (row.coverageDeductible || '').trim(); + const coveragePremium = (row.coveragePremium || '').trim(); + let coverageItems = row.coverageItems; + if (coverageName || coverageAmount || coverageDeductible || coveragePremium) { + coverageItems = [{ + coverageName, + coverageAmount, + deductible: coverageDeductible, + itemPremium: coveragePremium, + }]; + } + const { + coverageName: _coverageName, + coverageAmount: _coverageAmount, + coverageDeductible: _coverageDeductible, + coveragePremium: _coveragePremium, + ...rest + } = row; + return normalizePolicyDetail({ ...rest, coverageItems }); +}; + +const downloadPolicyImportTemplate = () => { + const escapeCsvCell = (val) => { + const s = String(val ?? ''); + return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; + }; + const csv = `\uFEFF${[ + POLICY_IMPORT_TEMPLATE_HEADERS.map(escapeCsvCell).join(','), + POLICY_IMPORT_TEMPLATE_SAMPLE_ROW.map(escapeCsvCell).join(','), + ].join('\n')}`; + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = '保单新增续保导入模板.csv'; + a.click(); + URL.revokeObjectURL(url); +}; + +const parseCsvLine = (line) => { + const result = []; + let cur = ''; + let inQuote = false; + for (let i = 0; i < line.length; i += 1) { + const c = line[i]; + if (c === '"') { + inQuote = !inQuote; + continue; + } + if (c === ',' && !inQuote) { + result.push(cur.trim()); + cur = ''; + continue; + } + cur += c; + } + result.push(cur.trim()); + return result; +}; + +const normalizeImportHeaderKey = (header) => { + const h = String(header || '').trim().replace(/^\uFEFF/, '').replace(/\*+$/, ''); + const fromColumns = POLICY_IMPORT_TEMPLATE_COLUMNS.find((col) => col.header === h)?.key; + if (fromColumns) return fromColumns; + if (h === '业务类型') return 'bizTypeLabel'; + return POLICY_IMPORT_HEADER_ALIASES[h] || null; +}; + +const POLICY_IMPORT_SKIP_BIZ_LABELS = new Set(['停保', '停租', '复驶', '退保']); + +const parsePolicyImportFileText = (text) => { + const lines = String(text || '').split(/\r?\n/).map((l) => l.trim()).filter(Boolean); + if (!lines.length) return []; + const headerCells = parseCsvLine(lines[0]); + const colIndex = {}; + headerCells.forEach((cell, idx) => { + const key = normalizeImportHeaderKey(cell); + if (key) colIndex[key] = idx; + }); + const hasHeader = Object.keys(colIndex).length >= 3; + const dataLines = hasHeader ? lines.slice(1) : lines; + const fallbackIndexByKey = Object.fromEntries( + POLICY_IMPORT_TEMPLATE_COLUMNS.map((col, idx) => [col.key, idx]) + ); + const pick = (cells, key) => { + if (hasHeader && colIndex[key] != null) return (cells[colIndex[key]] || '').trim(); + const idx = fallbackIndexByKey[key]; + return idx != null ? (cells[idx] || '').trim() : ''; + }; + return dataLines.map((line) => { + const cells = parseCsvLine(line); + if (!cells.some((c) => c)) return null; + const bizLabel = pick(cells, 'bizTypeLabel'); + if (bizLabel && POLICY_IMPORT_SKIP_BIZ_LABELS.has(bizLabel)) return null; + const row = { bizType: 'policy' }; + POLICY_IMPORT_TEMPLATE_COLUMNS.forEach((col) => { + row[col.key] = pick(cells, col.key); + }); + if (!row.coverageItems) { + row.coverageItems = pick(cells, 'coverageItems'); + } + return finalizePolicyImportRow(row); + }).filter(Boolean); +}; + +const validatePolicyImportRows = (rows) => { + for (let i = 0; i < (rows || []).length; i += 1) { + const result = validatePolicyEntryDetail(rows[i], { silent: true, rowIndex: i + 1 }); + if (!result.ok) { + message.error(`第 ${i + 1} 行缺少必填项:${result.label}`); + return false; + } + } + return true; +}; + +const resolveImportRowLedgerKey = (row) => { + const plate = (row?.plateNo || '').trim(); + const vin = (row?.vin || '').trim(); + if (plate) { + const v = findVehicleByPlate(plate); + if (v) return getVehicleLedgerKey(v); + } + if (vin) { + const v = findVehicleByVin(vin); + if (v) return getVehicleLedgerKey(v); + } + return null; +}; + +const isImportRowLedgerMatched = (ledgerKey) => ( + !!ledgerKey && MOCK_VEHICLES.some((v) => getVehicleLedgerKey(v) === ledgerKey) +); + +const buildRecognResultFromDetail = (fileMeta, detail, allInsurance, forcedMode) => { + const mode = forcedMode || bizTypeToRecognMode(detail.bizType); + const raw = mode === 'suspend' + ? enrichSuspendPolicyDetail(detail) + : mode === 'resume' + ? enrichResumePolicyDetail(detail) + : mode === 'cancel' + ? enrichCancelPolicyDetail(detail) + : normalizePolicyDetail(detail); + const d = mode === 'policy' ? stripPolicyRecognCoverageFields(raw) : raw; + const plate = d.plateNo; + const vin = d.vin; + const vehicle = (plate && findVehicleByPlate(plate)) + || (vin && findVehicleByVin(vin)) + || { plateNo: plate, vin }; + let ledgerKey = resolveImportRowLedgerKey({ plateNo: plate, vin }) + || (plate ? getVehicleLedgerKey(vehicle) : '') + || (vin ? getVehicleLedgerKey(vehicle) : ''); + let typeKey = INSURANCE_LABEL_TO_KEY[d.insuranceType]; + const record = ledgerKey ? allInsurance[ledgerKey] : null; + let existing = typeKey && record ? record[typeKey] : null; + if (mode !== 'policy' && d.policyNo) { + const policyMatch = findPolicyMatchAcrossLedger(allInsurance, d.policyNo) + || (ledgerKey ? findPolicyMatchInLedger(allInsurance, ledgerKey, d.policyNo) : null); + if (policyMatch) { + if (policyMatch.ledgerKey) ledgerKey = policyMatch.ledgerKey; + typeKey = policyMatch.typeKey; + existing = policyMatch.item; + } + } + const typeLabel = INSURANCE_TYPE_ITEMS.find((it) => it.key === typeKey)?.fullLabel || d.insuranceType || '—'; + const ocrPolicyNo = d.policyNo || existing?.policyNo || ''; + let ocrEndDate = ( + mode === 'suspend' || mode === 'resume' + ? (d.newEndDate || d.endDate) + : d.endDate + ) || existing?.endDate || ''; + let reinstateDate = d.resumeTime || d.reinstateDate || ''; + if (!ocrEndDate) { + if (mode === 'suspend' || mode === 'cancel') ocrEndDate = ANCHOR_TODAY; + else if (mode === 'resume') ocrEndDate = '2027-06-30'; + else ocrEndDate = '2027-12-31'; + } + const matched = (mode === 'suspend' || mode === 'resume' || mode === 'cancel') + ? !!ledgerKey && !!typeKey && !!ocrPolicyNo + : isImportRowLedgerMatched(ledgerKey) && !!typeKey && !!(ocrPolicyNo || ocrEndDate); + const matchTip = matched + ? ((mode === 'suspend' || mode === 'resume' || mode === 'cancel') ? '已匹配台账保单,可核对后确认' : '已与台账车辆、险种匹配,可核对后确认') + : (mode === 'suspend' || mode === 'resume' || mode === 'cancel') + ? (!ocrPolicyNo ? '请填写保单号' : '未匹配到台账保单,请检查保单号') + : !ledgerKey + ? '未匹配到台账车辆,请检查车牌或 VIN' + : !typeKey + ? '险种填写有误' + : '请填写保单号或到期日期'; + const bizLabel = POLICY_BIZ_TYPE_OPTIONS.find((o) => o.value === d.bizType)?.label || '保单录入'; + const companyResolve = mode === 'policy' + ? resolveInsuranceCompanyFromOcr(d.company) + : { company: d.company || existing?.company || '', candidates: [] }; + const ocrRecognizedPlate = String(plate || '').trim().toUpperCase(); + return { + id: fileMeta.id || `ocr-r-${Date.now()}`, + fileUid: fileMeta.fileUid || fileMeta.uid || `f-${Date.now()}`, + fileName: fileMeta.fileName || fileMeta.name || '导入记录', + fileType: fileMeta.fileType || '', + policyDetail: d, + ocrPlateNo: (vehicle.plateNo || plate || '').trim(), + ocrRecognizedPlate, + ocrVin: vehicle.vin || vin || '', + displayPlate: formatVehiclePlateDisplay(vehicle.plateNo || plate), + ocrPolicyNo, + ocrEndDate, + ocrStartDate: d.startDate || '', + ocrPremium: d.premium || '', + ocrPayTime: d.payTime || '', + ocrEndorsementNo: d.endorsementNo || '', + ocrCoverageItems: mode === 'policy' ? '' : serializeCoverageItems(d.coverageItems), + ocrBizType: d.bizType, + ocrBizTypeLabel: bizLabel, + ocrCompany: mode === 'policy' ? companyResolve.company : (d.company || existing?.company || ''), + ocrCompanyRaw: d.company || '', + companyCandidates: companyResolve.candidates, + ocrVehicleOwner: d.vehicleOwner || '', + ocrApplicant: d.applicant || '', + ocrInsured: d.insured || '', + reinstateDate, + suspendTime: d.suspendTime || d.startDate || '', + resumeTime: d.resumeTime || d.reinstateDate || '', + newEndDate: d.newEndDate || d.endDate || '', + cancelTime: d.cancelTime || d.startDate || d.endDate || '', + ledgerKey: ledgerKey || '', + typeKey: typeKey || '', + insuranceTypeLabel: typeLabel, + matched, + matchTip, + recognSuccess: true, + confirmed: false, + recognMode: mode, + }; +}; + +const buildImportResultsFromRows = (rows, allInsurance) => ( + (rows || []).map((row, idx) => buildRecognResultFromDetail( + { id: `import-r-${idx}-${Date.now()}`, fileUid: `import-row-${idx}`, fileName: `导入_${row.plateNo || row.vin || `第${idx + 1}行`}.csv`, fileType: 'text/csv' }, + normalizePolicyDetail(row), + allInsurance + )) +); + +const readPolicyImportFileAsText = (file) => new Promise((resolve, reject) => { + const name = (file?.name || '').toLowerCase(); + if (/\.(xlsx|xls)$/i.test(name)) { + message.warning('当前原型请使用 CSV 模板(在 Excel 中打开模板后另存为 CSV UTF-8 再上传)'); + resolve(''); + return; + } + const reader = new FileReader(); + reader.onload = (e) => resolve(String(e?.target?.result || '')); + reader.onerror = () => reject(new Error('read failed')); + reader.readAsText(file, 'UTF-8'); +}); + +const findPolicyMatchInLedger = (allInsurance, ledgerKey, policyNo) => { + const record = allInsurance?.[ledgerKey]; + if (!record || !policyNo) return null; + for (let i = 0; i < INSURANCE_TYPE_ITEMS.length; i += 1) { + const item = INSURANCE_TYPE_ITEMS[i]; + if (record[item.key]?.policyNo === policyNo) { + return { typeKey: item.key, item: record[item.key], label: item.fullLabel }; + } + } + return null; +}; + +const findPolicyMatchAcrossLedger = (allInsurance, policyNo) => { + if (!policyNo) return null; + const keys = Object.keys(allInsurance || {}); + for (let i = 0; i < keys.length; i += 1) { + const ledgerKey = keys[i]; + const match = findPolicyMatchInLedger(allInsurance, ledgerKey, policyNo); + if (match) return { ledgerKey, ...match }; + } + return null; +}; + +const buildMockOcrResults = (files, mode, insuranceTypeLabel, allInsurance) => ( + (files || []).filter((f) => f.status === 'done').map((file, idx) => { + const fromFile = resolvePolicyDetailFromFileName(file.name); + const detail = normalizePolicyDetail({ + ...fromFile, + insuranceType: mode === 'policy' ? (insuranceTypeLabel || fromFile.insuranceType) : fromFile.insuranceType, + bizType: mode !== 'policy' ? mode : (fromFile.bizType || 'policy'), + }); + if (!detail.plateNo && !detail.vin) { + const vehicle = MOCK_VEHICLES[idx % MOCK_VEHICLES.length] || {}; + detail.plateNo = vehicle.plateNo || ''; + detail.vin = vehicle.vin || ''; + } + if (!detail.policyNo) { + detail.policyNo = `PDZA${String(20260000 + idx)}`; + } + if (mode === 'policy') { + Object.assign(detail, stripPolicyRecognCoverageFields(detail)); + if (!detail.premium) { + detail.premium = detail.insuranceType === '交强险' ? '1243.00' : '12800.00'; + } + } + if (mode === 'suspend') { + Object.assign(detail, enrichSuspendPolicyDetail(detail)); + } + if (mode === 'resume') { + Object.assign(detail, enrichResumePolicyDetail(detail)); + } + if (mode === 'cancel') { + Object.assign(detail, enrichCancelPolicyDetail(detail)); + } + const result = buildRecognResultFromDetail( + { id: `ocr-r-${file.uid}`, fileUid: file.uid, fileName: file.name, fileType: file.type || '' }, + detail, + allInsurance, + mode + ); + if (files.length >= 2 && idx === files.length - 1) { + return { + ...result, + recognSuccess: false, + matched: false, + matchTip: 'OCR 识别失败,请检查文件清晰度或重新上传', + }; + } + return result; + }) +); + +const recognResultToPolicyDetail = (result, taskMode = 'policy') => { + const mode = resolvePolicyRecognEffectiveMode(taskMode, result); + const pd = result.policyDetail || {}; + const merged = { + plateNo: (result.ocrPlateNo || pd.plateNo || '').trim(), + vin: (result.ocrVin || pd.vin || '').trim(), + insuranceType: result.insuranceTypeLabel || pd.insuranceType || '交强险', + bizType: mode !== 'policy' ? mode : (result.ocrBizType || pd.bizType || 'policy'), + company: result.ocrCompany || pd.company || '', + policyNo: result.ocrPolicyNo || pd.policyNo || '', + endorsementNo: result.ocrEndorsementNo || pd.endorsementNo || '', + payTime: result.ocrPayTime || pd.payTime || '', + startDate: result.ocrStartDate || pd.startDate || '', + endDate: result.ocrEndDate || pd.endDate || '', + reinstateDate: result.reinstateDate || pd.reinstateDate || '', + suspendTime: result.suspendTime || pd.suspendTime || '', + resumeTime: result.resumeTime || pd.resumeTime || '', + newEndDate: result.newEndDate || pd.newEndDate || '', + cancelTime: result.cancelTime || pd.cancelTime || '', + premium: result.ocrPremium || pd.premium || '', + coverageItems: mode === 'policy' + ? [] + : normalizeCoverageItems(pd.coverageItems ?? result.ocrCoverageItems), + applicant: result.ocrApplicant || pd.applicant || '', + insured: result.ocrInsured || pd.insured || '', + vehicleOwner: result.ocrVehicleOwner || pd.vehicleOwner || '', + signDate: pd.signDate || '', + attachments: pd.attachments || [], + }; + if (mode === 'suspend') return enrichSuspendPolicyDetail(merged); + if (mode === 'resume') return enrichResumePolicyDetail(merged); + if (mode === 'cancel') return enrichCancelPolicyDetail(merged); + return normalizePolicyDetail(merged); +}; + +const buildPolicyRecognConfirmDraft = (result, taskMode = 'policy') => { + const mode = resolvePolicyRecognEffectiveMode(taskMode, result); + const detail = recognResultToPolicyDetail(result, taskMode); + if (mode === 'suspend') { + return enrichSuspendPolicyDetail({ ...detail, bizType: 'suspend' }); + } + if (mode === 'resume') { + return enrichResumePolicyDetail({ ...detail, bizType: 'resume' }); + } + if (mode === 'cancel') { + return enrichCancelPolicyDetail({ ...detail, bizType: 'cancel' }); + } + if (mode === 'policy') { + const enriched = enrichPolicyRecognOcrDetail( + { + ...detail, + vehicleOwner: result.ocrVehicleOwner || detail.vehicleOwner, + applicant: result.ocrApplicant || detail.applicant, + insured: result.ocrInsured || detail.insured, + company: result.ocrCompanyRaw || detail.company || result.ocrCompany, + }, + { + ocrRecognizedPlate: result.ocrRecognizedPlate || result.ocrPlateNo, + companyCandidates: result.companyCandidates, + } + ); + return stripPolicyRecognCoverageFields(enriched); + } + return normalizePolicyDetail({ ...detail, bizType: mode }); +}; + +const applyPolicyOcrResultToLedger = (result, mode) => { + const { ledgerKey, typeKey } = result; + if (!ledgerKey || !typeKey) return false; + const effectiveMode = result.recognMode || mode; + const rawDetail = result.policyDetail + ? stripPolicyRecognDraftMeta(result.policyDetail) + : null; + const detail = rawDetail + ? (effectiveMode === 'suspend' + ? enrichSuspendPolicyDetail(rawDetail) + : effectiveMode === 'resume' + ? enrichResumePolicyDetail(rawDetail) + : effectiveMode === 'cancel' + ? enrichCancelPolicyDetail(rawDetail) + : normalizePolicyDetail(rawDetail)) + : recognResultToPolicyDetail(result); + const nowStr = formatCompareSheetNow(); + return (prev) => { + const record = ensureInsuranceRecordShape(prev[ledgerKey] || createEmptyInsuranceRecord()); + const item = applyPolicyDetailToInsuranceItem({ ...record[typeKey] }, detail, effectiveMode); + item.updateTime = nowStr; + item.updateUser = PROTO_COMPARE_CREATOR; + return { ...prev, [ledgerKey]: { ...record, [typeKey]: item } }; + }; +}; + +/** 保险公司管理模块 — 保险公司名称枚举(原型 mock) */ +const INSURANCE_MGMT_COMPANIES = [ + '中国人民财产保险股份有限公司', + '中国平安财产保险股份有限公司', + '中国太平洋财产保险股份有限公司', + '中国人寿财产保险股份有限公司', + '阳光财产保险股份有限公司', + '中华联合财产保险股份有限公司', + '太平财产保险有限公司', + '大地财产保险股份有限公司', + '紫金财产保险股份有限公司', + '国任财产保险股份有限公司广州市番禺支公司', + '上海某某保险公司', +]; + +const QUOTE_INSURANCE_TYPES = ['交强险', '商业险', '超赔险', '货物险', '驾意险']; + +const sanitizePremiumInput = (raw) => { + let s = String(raw || '').replace(/[^\d.]/g, ''); + const dotIdx = s.indexOf('.'); + if (dotIdx >= 0) { + s = s.slice(0, dotIdx + 1) + s.slice(dotIdx + 1).replace(/\./g, '').slice(0, 2); + } + return s; +}; + +const isValidPremium = (s) => { + const v = (s || '').trim(); + if (!v) return false; + if (!/^\d+(\.\d{1,2})?$/.test(v)) return false; + return parseFloat(v) > 0; +}; + +const formatPremiumDisplay = (s) => { + if (!isValidPremium(s)) return s || ''; + return parseFloat(s).toFixed(2); +}; + +const createEmptyQuoteDraft = () => ({ company: undefined, premium: '' }); + +const shortInsuranceCompanyName = (name) => ( + (name || '').replace(/股份有限公司/g, '').replace(/有限公司/g, '').trim() +); + +/** 比价单:汇总各行已确认报价金额 */ +const calcCompareSheetConfirmedTotal = (rows) => { + let total = 0; + let count = 0; + (rows || []).forEach((row) => { + if (!row.confirmedQuoteId) return; + const quote = (row.quotes || []).find((q) => q.id === row.confirmedQuoteId); + if (!quote?.premium) return; + const amount = parseFloat(quote.premium); + if (!Number.isNaN(amount) && amount > 0) { + total += amount; + count += 1; + } + }); + return { total, count }; +}; + +const VEHICLE_PROFILES = { + '沪A03561F': { customer: '上海迅杰物流有限公司', ownerCompany: '浙江羚牛氢能科技有限公司', color: '白色', regDate: '2024-06-05', inspectExpire: '2026-06-30' }, + '粤B58888F': { customer: '深圳冷链运输有限公司', ownerCompany: '羚牛运营(广东)', color: '蓝色', regDate: '2024-07-20', inspectExpire: '2026-07-20' }, + '苏E33333': { customer: '苏州港务集团', ownerCompany: '浙江羚牛氢能科技有限公司', color: '红色', regDate: '2024-05-16', inspectExpire: '2026-05-15' }, + '京A12345': { customer: '—', ownerCompany: '某某科技有限公司', color: '灰色', regDate: '2020-10-01', inspectExpire: '2024-10-01' }, + '浙A88888': { customer: '—', ownerCompany: '浙江羚牛氢能科技有限公司', color: '绿色', regDate: '2025-01-01', inspectExpire: '2027-12-31' }, + '沪D66666': { customer: '客户C', ownerCompany: '羚牛运营(上海)', color: '白色', regDate: '2021-06-15', inspectExpire: '2025-01-31' }, + '粤A12345': { customer: '客户A', ownerCompany: '羚牛运营(广东)', color: '白色', regDate: '2023-07-01', inspectExpire: '2026-02-28' }, + '苏A55678': { customer: '—', ownerCompany: '羚牛运营(嘉兴)', color: '黄色', regDate: '2025-05-01', inspectExpire: '2026-04-30' }, + LZYTBACR2M9999001: { customer: '嘉兴某某物流有限公司', ownerCompany: '浙江羚牛氢能科技有限公司', color: '白色', regDate: '2025-11-01', inspectExpire: '2026-10-31' }, + '浙F08888F': { customer: '嘉兴港务物流有限公司', ownerCompany: '羚牛运营(嘉兴)', color: '白色', regDate: '2024-08-10', inspectExpire: '2026-08-10' }, + '浙F07777F': { customer: '平湖冷链运输有限公司', ownerCompany: '羚牛运营(嘉兴)', color: '蓝色', regDate: '2024-09-15', inspectExpire: '2026-09-15' }, + '粤AGP9001': { customer: '广州氢能示范运营公司', ownerCompany: '羚牛运营(广东)', color: '银色', regDate: '2025-02-01', inspectExpire: '2026-07-15' }, + '粤AGP9002': { customer: '深圳城配物流有限公司', ownerCompany: '羚牛运营(广东)', color: '白色', regDate: '2024-11-20', inspectExpire: '2026-06-20' }, + '沪A09999F': { customer: '上海综合物流有限公司', ownerCompany: '羚牛运营(上海)', color: '绿色', regDate: '2025-04-01', inspectExpire: '2026-10-01' }, +}; + +const createCompareRowId = () => `cr-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; +const createCompareSheetId = () => `cs-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; +const createCompareAttachmentId = () => `att-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; +const createQuoteId = () => `qt-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; + +const formatFileSize = (bytes) => { + if (bytes == null || Number.isNaN(bytes)) return ''; + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; +}; + +const attachmentsToUploadFileList = (attachments) => (attachments || []).map((a) => ({ + uid: a.uid || a.id || createCompareAttachmentId(), + name: a.name, + size: a.size, + type: a.type, + status: 'done', + uploadedAt: a.uploadedAt, +})); + +const uploadFileListToAttachments = (fileList) => (fileList || []).map((f) => ({ + id: f.uid || createCompareAttachmentId(), + uid: f.uid, + name: f.name, + size: f.size, + type: f.type || '', + uploadedAt: f.uploadedAt || formatCompareSheetNow(), +})); + +const formatCompareSheetNow = () => { + if (moment) return moment(ANCHOR_TODAY).hour(10).minute(30).second(0).format('YYYY-MM-DD HH:mm:ss'); + return `${ANCHOR_TODAY} 10:30:00`; +}; + +const calcCompareSheetStats = (rows) => { + const vehicleKeys = new Set(); + (rows || []).forEach((row) => { + const plate = (row.plateNo || '').trim(); + const vin = (row.vin || '').trim(); + if (plate) vehicleKeys.add(`plate:${plate.toUpperCase()}`); + else if (vin) vehicleKeys.add(`vin:${vin.toUpperCase()}`); + }); + return { + totalVehicles: vehicleKeys.size, + insuranceCount: (rows || []).length, + }; +}; + +const countCompareRowsWithConfirmedQuote = (rows) => ( + (rows || []).filter((r) => r.confirmedQuoteId).length +); + +const getLatestPayDateDiffDays = (dateStr) => { + if (!dateStr || !moment) return null; + const today = moment(ANCHOR_TODAY).startOf('day'); + const pay = moment(dateStr).startOf('day'); + if (!pay.isValid()) return null; + return pay.diff(today, 'days'); +}; + +const getLatestPayDateStatus = (dateStr) => { + const diff = getLatestPayDateDiffDays(dateStr); + if (diff === null) return { type: 'none', text: '未填写最晚付费日期' }; + if (diff < 0) return { type: 'overdue', text: `最晚付费已超期 ${Math.abs(diff)} 天`, diffDays: diff }; + if (diff <= LATEST_PAY_WARN_DAYS) return { type: 'warning', text: `最晚付费临期,剩余 ${diff} 天`, diffDays: diff }; + return { type: 'normal', text: `距离最晚付费 ${diff} 天`, diffDays: diff }; +}; + +const calcCompareSheetPayAlerts = (sheet) => { + let warning = 0; + let overdue = 0; + (sheet?.rows || []).forEach((row) => { + const st = getLatestPayDateStatus(row.latestPayDate); + if (st.type === 'warning') warning += 1; + if (st.type === 'overdue') overdue += 1; + }); + return { warning, overdue }; +}; + +const syncCompareSheetProcurementCounts = (sheet) => { + const rows = sheet?.rows || []; + const submittedProcurementCount = rows.filter((r) => { + const st = normalizeCompareProcurementStatus(r.procurementStatus); + return st === 'submitted' || st === 'approved'; + }).length; + const approvedCount = rows.filter((r) => ( + normalizeCompareProcurementStatus(r.procurementStatus) === 'approved' + )).length; + return { submittedProcurementCount, approvedCount, completedCount: approvedCount }; +}; + +const normalizeCompareRows = (rows) => (rows || []).map((row) => ({ + ...row, + procurementStatus: normalizeCompareProcurementStatus(row.procurementStatus), + procurementCurrentApprover: row.procurementCurrentApprover || '', +})); + +const normalizeCompareSheet = (sheet) => { + const rows = normalizeCompareRows(sheet.rows); + const attachments = Array.isArray(sheet.attachments) ? sheet.attachments : []; + return { + ...sheet, + rows, + attachments, + ...calcCompareSheetStats(rows), + ...syncCompareSheetProcurementCounts({ rows }), + }; +}; + +const createEmptyInsuranceItem = () => ({ + company: '', + policyNo: '', + endorsementNo: '', + startDate: '', + endDate: '', + premium: '', + payTime: '', + signDate: '', + coverageItems: '', + applicant: '', + insured: '', + updateTime: '', + updateUser: '', + policyTag: '', + reinstateDate: '', + suspendTime: '', + resumeTime: '', + cancelTime: '', + refundPremium: '', + attachments: [], + operationLogs: [], + archivedPolicies: [], +}); + +const INSURANCE_OPERATION_TYPE_LABEL = { + add: '新增', + suspend: '停保', + resume: '复驶', + cancel: '退保', +}; + +const EMPTY_POLICY_BIZ_FORM = { + suspendTime: '', + resumeTime: '', + newEndDate: '', + cancelTime: '', + refundPremium: '', +}; + +const appendInsuranceOperationLog = (logs, payload) => [ + { + id: `iop-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, + time: formatCompareSheetNow(), + operator: payload.operator || PROTO_COMPARE_CREATOR, + type: payload.type, + remark: payload.remark || '', + attachments: Array.isArray(payload.attachments) ? payload.attachments : [], + }, + ...(logs || []), +]; + +/** 保单业务状态:正常(含复驶后)、已停保、已退保 */ +const INSURANCE_POLICY_STATUS_META = { + normal: { label: '正常', color: 'success' }, + suspended: { label: '已停保', color: 'warning' }, + cancelled: { label: '已退保', color: 'default' }, +}; + +const getInsurancePolicyStatusKey = (record) => { + if (record?.policyTag === 'cancelled' || record?.purchaseType === 'cancel') return 'cancelled'; + if (record?.policyTag === 'suspended' || record?.purchaseType === 'rentStop') return 'suspended'; + return 'normal'; +}; + +const getInsurancePolicyStatusMeta = (record) => ( + INSURANCE_POLICY_STATUS_META[getInsurancePolicyStatusKey(record)] || INSURANCE_POLICY_STATUS_META.normal +); + +const buildOperationChangeRemark = (changes) => ( + (changes || []) + .filter((c) => c.before !== c.after) + .map((c) => `${c.label}:${c.before || '—'} → ${c.after || '—'}`) + .join(';') +); + +const deriveLedgerMgmtPurchaseType = (item) => { + if (item?.policyTag === 'cancelled') return 'cancel'; + if (item?.policyTag === 'suspended') return 'rentStop'; + return 'new'; +}; + +const createLedgerMgmtHistoryRecord = (vehicle, ledgerKey, typeKey, typeLabel, item, options = {}) => { + const purchaseTime = item.startDate || item.updateTime || item.endDate || item.suspendTime || ''; + const derivedEventType = item.policyTag === 'cancelled' + ? 'cancel' + : item.policyTag === 'suspended' + ? 'suspend' + : 'purchase'; + const record = createInsuranceHistoryRecord({ + id: options.id || `ih-${ledgerKey}-${typeKey}-ledger-current`, + typeKey, + typeLabel, + eventType: options.eventType || derivedEventType, + purchaseType: options.purchaseType || deriveLedgerMgmtPurchaseType(item), + time: purchaseTime, + payTime: item.payTime || '', + policyNo: item.policyNo, + company: item.company, + premium: item.premium, + startDate: item.startDate, + endDate: item.endDate, + policyTag: item.policyTag || '', + reinstateDate: item.reinstateDate || item.resumeTime || '', + suspendTime: item.suspendTime || '', + resumeTime: item.resumeTime || item.reinstateDate || '', + policyDetail: buildPolicyDetailFromLedgerItem( + vehicle, + typeLabel, + item, + item.policyTag === 'suspended' ? 'suspend' : item.policyTag === 'cancelled' ? 'cancel' : 'policy' + ), + source: 'ledger', + sourceLabel: options.sourceLabel || '台账当前保单', + fileName: item.attachments?.[0]?.name || `${item.policyNo}_${typeLabel}.pdf`, + }); + return { + ...record, + purchaseTime, + operationLogs: item.operationLogs || [], + isArchived: !!options.isArchived, + isLedgerCurrent: !options.isArchived, + attachments: item.attachments || [], + summary: getInsuranceEventSummary(record), + }; +}; + +const createEmptyCompareRow = () => ({ + id: createCompareRowId(), + plateNo: '', + vin: '', + customer: '', + ownerCompany: '', + brand: '', + model: '', + bodyColor: '', + regDate: '', + inspectExpire: '', + insureMode: '续保', + insuranceType: '交强险', + jqValidUntil: '', + syValidUntil: '', + latestPayDate: '', + quotes: [], + confirmedQuoteId: '', + procurementStatus: 'none', + procurementSubmittedAt: '', + procurementCurrentApprover: '', +}); + +const buildCompareRowFromVehicle = (v, insuranceData) => ({ + id: createCompareRowId(), + ...buildVehicleComparePatch(v, insuranceData), + latestPayDate: '', + quotes: [], + confirmedQuoteId: '', + procurementStatus: 'none', + procurementSubmittedAt: '', + procurementCurrentApprover: '', +}); + +const cloneCompareRow = (row) => ({ + ...JSON.parse(JSON.stringify(row)), + id: createCompareRowId(), + quotes: (row.quotes || []).map((q) => ({ ...q, id: createQuoteId() })), + confirmedQuoteId: '', +}); + +const MOCK_VEHICLES = [ + { plateNo: '沪A03561F', brand: '宇通', model: '49吨牵引车头', vin: 'LMRKH9AC0R1004086', status: '自营' }, + { plateNo: '粤B58888F', brand: '福田', model: '奥铃4.5吨冷藏车', vin: 'LGHXCAE28M6789012', status: '租赁' }, + { plateNo: '苏E33333', brand: '陕汽', model: '德龙X3000混动牵引车', vin: 'LSXCH9AE8M1094857', status: '自营' }, + { plateNo: '京A12345', brand: '东风', model: 'DFH1180厢式货车', vin: 'LKLG7C4E4NA774759', status: '退出运营' }, + { plateNo: '浙A88888', brand: '宇通', model: '氢燃料电池大巴', vin: 'LMRKH9AE2P9876543', status: '库存' }, + { plateNo: '沪D66666', brand: '比亚迪', model: 'T5轻卡', vin: 'LSVAU2BR3NS567890', status: '租赁' }, + { plateNo: '粤A12345', brand: '比亚迪', model: '汉EV', vin: 'LGWEF4A59NS123456', status: '租赁' }, + { plateNo: '苏A55678', brand: '福田', model: '欧马可4.2米', vin: 'LVBV3JBB8NY123456', status: '库存' }, + { plateNo: '', brand: '东风', model: '氢燃料电池牵引车(待上牌)', vin: 'LZYTBACR2M9999001', status: '库存' }, + { plateNo: '沪BDB9161', brand: '腾势', model: 'QCJ6520MBEV1纯电动', vin: 'LC0DF4CD8S0303140', status: '自营' }, + { plateNo: '粤AGR9766', brand: '帕力安', model: '燃料电池翼开启厢式车', vin: 'LB9A32A21R0LS1478', status: '自营' }, + { plateNo: '粤AGP9827', brand: '比亚迪', model: '轻卡', vin: 'LGXAGP98270000001', status: '租赁' }, + { plateNo: '粤AGP3071', brand: '福田', model: '欧马可', vin: 'LGXAGP30710000001', status: '租赁' }, + { plateNo: '粤A03423F', brand: '宇通', model: '49吨牵引', vin: 'LZYTBACR2A03423F01', status: '自营' }, + { plateNo: '粤A06290F', brand: '陕汽', model: '牵引车', vin: 'LZYTBACR2A06290F01', status: '自营' }, + { plateNo: '粤A03331F', brand: '东风', model: '厢式货车', vin: 'LZYTBACR2A03331F01', status: '自营' }, + { plateNo: '浙F03220F', brand: '福田', model: '冷藏车', vin: 'LZYTBACR2F03220F01', status: '租赁' }, + { plateNo: '沪A06192F', brand: '比亚迪', model: 'T5', vin: 'LZYTBACR2A06192F01', status: '自营' }, + { plateNo: '浙F05178F', brand: '福田', model: '牵引车', vin: 'LZYTBACR2F05178F01', status: '自营' }, + /* 样例:多险种同时临期/到期 */ + { plateNo: '浙F08888F', brand: '宇通', model: '49吨氢能牵引车', vin: 'LMRKH9AC0R1004991', status: '自营' }, + { plateNo: '浙F07777F', brand: '福田', model: '4.5吨氢能冷藏车', vin: 'LGHXCAE28M6784992', status: '租赁' }, + { plateNo: '粤AGP9001', brand: '帕力安', model: '燃料电池厢式车', vin: 'LB9A32A21R0LS4993', status: '自营' }, + { plateNo: '粤AGP9002', brand: '陕汽', model: '德龙氢能牵引车', vin: 'LSXCH9AE8M1094994', status: '租赁' }, + { plateNo: '沪A09999F', brand: '比亚迪', model: 'T5氢能轻卡', vin: 'LSVAU2BR3NS5674995', status: '自营' }, +]; + +const hasVehiclePlate = (vehicle) => !!(vehicle?.plateNo || '').trim(); + +const getVehicleLedgerKey = (vehicleOrKey) => { + if (!vehicleOrKey) return ''; + if (typeof vehicleOrKey === 'object') { + const plate = (vehicleOrKey.plateNo || '').trim(); + if (plate) return plate; + return (vehicleOrKey.vin || '').trim(); + } + return String(vehicleOrKey).trim(); +}; + +const formatVehiclePlateDisplay = (plateNo) => { + const p = (plateNo || '').trim(); + return p || NO_PLATE_LABEL; +}; + +const isCompareRowVehicleLinked = (row) => !!(row?.plateNo || '').trim() || !!(row?.vin || '').trim(); + +const getVehicleProfile = (vehicle) => { + if (!vehicle) return {}; + const plate = (vehicle.plateNo || '').trim(); + if (plate && VEHICLE_PROFILES[plate]) return VEHICLE_PROFILES[plate]; + const vin = (vehicle.vin || '').trim(); + return VEHICLE_PROFILES[vin] || {}; +}; + +const getInitialInsuranceSeed = (vehicle) => { + const plate = (vehicle.plateNo || '').trim(); + if (plate && INITIAL_INSURANCE_DATA[plate]) return INITIAL_INSURANCE_DATA[plate]; + const vin = (vehicle.vin || '').trim(); + return INITIAL_INSURANCE_DATA[vin] || null; +}; + +const findVehicleByPlate = (plate) => { + const key = (plate || '').trim().toUpperCase(); + if (!key) return null; + return MOCK_VEHICLES.find((v) => (v.plateNo || '').trim().toUpperCase() === key) || null; +}; + +const findVehicleByVin = (vin) => { + const key = (vin || '').trim().toUpperCase(); + return MOCK_VEHICLES.find((v) => v.vin.toUpperCase() === key) || null; +}; + +const PLATE_SELECT_OPTIONS = MOCK_VEHICLES + .filter((v) => hasVehiclePlate(v)) + .map((v) => ({ label: v.plateNo, value: v.plateNo })); +const VIN_SELECT_OPTIONS = MOCK_VEHICLES.map((v) => ({ label: v.vin, value: v.vin })); + +const getVehicleRegistrationOwner = (plateNo) => { + const vehicle = findVehicleByPlate(plateNo); + if (!vehicle) return ''; + const profile = getVehicleProfile(vehicle); + return profile.ownerCompany || profile.customer || ''; +}; + +const normalizeRecognPayTime = (raw) => { + const s = String(raw || '').trim(); + if (!s) return ''; + if (/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}$/.test(s)) return s; + const m = s.match(/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{1,2})(?::(\d{1,2}))?(?::(\d{1,2}))?)?$/); + if (m) { + const [, y, mo, d, h = '0', mi = '0', se = '0'] = m; + return `${y}-${mo}-${d} ${String(h).padStart(2, '0')}:${String(mi).padStart(2, '0')}:${String(se).padStart(2, '0')}`; + } + return s; +}; + +const normalizeRecognDate = (raw) => { + const s = String(raw || '').trim(); + if (!s) return ''; + const m = s.match(/^(\d{4})-(\d{2})-(\d{2})/); + if (m) return `${m[1]}-${m[2]}-${m[3]}`; + return s; +}; + +const normalizeRecognPremiumAmount = (raw) => { + const cleaned = sanitizePremiumInput(raw); + if (!cleaned) return ''; + const num = parseFloat(cleaned); + if (Number.isNaN(num)) return cleaned; + return num.toFixed(2); +}; + +const matchInsuranceCompanyCandidates = (ocrText, catalog = INSURANCE_MGMT_COMPANIES) => { + const text = String(ocrText || '').trim(); + if (!text) return []; + const exact = catalog.filter((c) => c === text); + if (exact.length) return exact; + const norm = text.replace(/\s/g, ''); + return catalog.filter((c) => { + const cn = c.replace(/\s/g, ''); + return cn.includes(norm) || norm.includes(cn); + }); +}; + +const resolveInsuranceCompanyFromOcr = (ocrText, catalog = INSURANCE_MGMT_COMPANIES) => { + const candidates = matchInsuranceCompanyCandidates(ocrText, catalog); + if (candidates.length === 1) return { company: candidates[0], candidates }; + if (candidates.length > 1) return { company: '', candidates }; + return { company: '', candidates: catalog }; +}; + +const stripPolicyRecognDraftMeta = (draft) => { + if (!draft || typeof draft !== 'object') return draft; + const { + _companyCandidates, + _ocrRecognizedPlate, + _plateLedgerMatched, + ...rest + } = draft; + return rest; +}; + +const enrichPolicyRecognOcrDetail = (detail, context = {}) => { + const d = normalizePolicyDetail(detail); + const ocrPlate = String(context.ocrRecognizedPlate || d.plateNo || '').trim().toUpperCase(); + const ledgerPlate = ocrPlate && findVehicleByPlate(ocrPlate) ? ocrPlate : ''; + const plateNo = ledgerPlate || String(d.plateNo || '').trim().toUpperCase(); + const vehicle = findVehicleByPlate(plateNo); + const companyMatches = matchInsuranceCompanyCandidates(d.company); + const companyResolve = resolveInsuranceCompanyFromOcr(d.company); + const resolvedCompany = companyMatches.length === 1 ? companyMatches[0] : companyResolve.company; + const candidates = companyMatches.length > 1 + ? companyMatches + : (context.companyCandidates || companyResolve.candidates); + return { + ...d, + plateNo: ledgerPlate || plateNo, + vin: vehicle?.vin || d.vin || '', + vehicleOwner: (d.vehicleOwner || '').trim() || getVehicleRegistrationOwner(ledgerPlate || plateNo), + payTime: normalizeRecognPayTime(d.payTime), + startDate: normalizeRecognDate(d.startDate), + endDate: normalizeRecognDate(d.endDate), + premium: normalizeRecognPremiumAmount(d.premium), + company: resolvedCompany, + applicant: (d.applicant || '').trim(), + insured: (d.insured || '').trim(), + _companyCandidates: candidates, + _ocrRecognizedPlate: ocrPlate, + _plateLedgerMatched: !!ledgerPlate, + }; +}; + +const validatePolicyRecognPlateForConfirm = (draft, result, options = {}) => { + const { silent = false } = options; + const ocrPlate = String(result?.ocrRecognizedPlate || result?.ocrPlateNo || '').trim().toUpperCase(); + const selected = String(draft?.plateNo || '').trim().toUpperCase(); + if (!selected) { + if (!silent) message.warning('请选择车牌号'); + return false; + } + if (!findVehicleByPlate(selected)) { + if (!silent) message.warning('所选车牌号不在台账车辆中,请重新选择'); + return false; + } + if (ocrPlate && selected !== ocrPlate) { + if (!silent) message.error(`识别车牌号为 ${ocrPlate},与当前所选 ${selected} 不一致,请核对`); + return false; + } + return true; +}; + +const buildVehicleComparePatch = (vehicle, insuranceData) => { + if (!vehicle) return {}; + const profile = getVehicleProfile(vehicle); + const ins = insuranceData[getVehicleLedgerKey(vehicle)] || {}; + const jqEnd = ins.compulsory?.endDate || ''; + const syEnd = ins.commercial?.endDate || ''; + return { + plateNo: vehicle.plateNo, + vin: vehicle.vin, + customer: profile.customer || '', + ownerCompany: profile.ownerCompany || '', + brand: vehicle.brand, + model: vehicle.model, + bodyColor: profile.color || '', + regDate: profile.regDate || '', + inspectExpire: profile.inspectExpire || '', + insureMode: jqEnd || syEnd ? '续保' : '新保', + insuranceType: '交强险', + jqValidUntil: jqEnd, + syValidUntil: syEnd, + }; +}; + +const clearVehicleComparePatch = () => ({ + plateNo: '', + vin: '', + customer: '', + ownerCompany: '', + brand: '', + model: '', + bodyColor: '', + regDate: '', + inspectExpire: '', + insureMode: '续保', + insuranceType: '交强险', + jqValidUntil: '', + syValidUntil: '', +}); + +const createEmptyInsuranceRecord = () => ({ + compulsory: createEmptyInsuranceItem(), + commercial: createEmptyInsuranceItem(), + excess: createEmptyInsuranceItem(), + cargo: createEmptyInsuranceItem(), + driverAccident: createEmptyInsuranceItem(), +}); + +const ensureInsuranceRecordShape = (record) => { + const base = createEmptyInsuranceRecord(); + const next = { ...base }; + INSURANCE_TYPE_ITEMS.forEach(({ key }) => { + next[key] = { ...base[key], ...(record?.[key] || {}) }; + }); + return next; +}; + +const VEHICLE_INSURANCE_MGMT_TABS = [ + { key: 'timeline', label: '保险采购全周期记录' }, + { key: 'compulsory', label: '交强险' }, + { key: 'commercial', label: '商业险' }, + { key: 'excess', label: '超赔险' }, + { key: 'driverAccident', label: '驾意险' }, + { key: 'cargo', label: '货物险' }, +]; + +/** 管理页记录类型:新保 / 续保 / 停租 / 复驶 / 退保 */ +const POLICY_PURCHASE_TYPE_META = { + new: { label: '新保', color: 'success', timelineColor: 'green', chipClass: 'lc-purchase-type--new' }, + renew: { label: '续保', color: 'processing', timelineColor: 'blue', chipClass: 'lc-purchase-type--renew' }, + rentStop: { label: '停保', color: 'warning', timelineColor: 'orange', chipClass: 'lc-purchase-type--rent-stop' }, + resume: { label: '复驶', color: 'cyan', timelineColor: 'cyan', chipClass: 'lc-purchase-type--resume' }, + cancel: { label: '退保', color: 'default', timelineColor: 'gray', chipClass: 'lc-purchase-type--cancel' }, +}; + +const POLICY_LIKE_EVENT_TYPES = new Set(['purchase', 'renew', 'procurement', 'recognize']); + +const isPolicyLikeInsuranceRecord = (record) => ( + POLICY_LIKE_EVENT_TYPES.has(record?.eventType) + || record?.purchaseType === 'new' + || record?.purchaseType === 'renew' +); + +const eventTypeToDefaultPurchaseType = (eventType) => { + if (eventType === 'suspend') return 'rentStop'; + if (eventType === 'resume') return 'resume'; + if (eventType === 'cancel') return 'cancel'; + if (eventType === 'renew') return 'renew'; + return 'new'; +}; + +const assignPurchaseTypesToRecords = (records) => { + const counters = {}; + const chronological = [...records].sort((a, b) => String(a.time).localeCompare(String(b.time))); + chronological.forEach((rec) => { + const typeKey = rec.typeKey || '_'; + if (!counters[typeKey]) counters[typeKey] = 0; + let purchaseType = eventTypeToDefaultPurchaseType(rec.eventType); + if (POLICY_LIKE_EVENT_TYPES.has(rec.eventType)) { + purchaseType = counters[typeKey] > 0 ? 'renew' : 'new'; + counters[typeKey] += 1; + } + rec.purchaseType = purchaseType; + }); + return records; +}; + +const subtractInsuranceYears = (dateStr, years) => { + if (!moment || !dateStr) return ''; + const d = moment(dateStr, 'YYYY-MM-DD', true); + if (!d.isValid()) return ''; + return d.subtract(years, 'year').format('YYYY-MM-DD'); +}; + +const vehicleMatchesCompareRow = (vehicle, row) => { + if (!vehicle || !row) return false; + const ledgerKey = getVehicleLedgerKey(vehicle); + const rowKey = getVehicleLedgerKey({ plateNo: row.plateNo, vin: row.vin }); + return !!ledgerKey && ledgerKey === rowKey; +}; + +const isActiveCompareProcurementStatus = (status) => ( + ACTIVE_COMPARE_PROCUREMENT_STATUSES.includes(status) +); + +const buildCompareSubmissionKey = (vehicleOrRow, insuranceTypeLabel) => { + const key = getVehicleLedgerKey(vehicleOrRow); + const type = insuranceTypeLabel || '交强险'; + if (!key) return ''; + return `${key}::${type}`; +}; + +const buildActiveCompareSubmissionSet = (compareSheets) => { + const set = new Set(); + (compareSheets || []).forEach((sheet) => { + (sheet.rows || []).forEach((row) => { + if (!isActiveCompareProcurementStatus(row.procurementStatus)) return; + const submissionKey = buildCompareSubmissionKey(row, row.insuranceType); + if (submissionKey) set.add(submissionKey); + }); + }); + return set; +}; + +const isVehicleTypeSubmittedToCompare = (vehicle, insuranceTypeLabel, submissionSet) => ( + submissionSet.has(buildCompareSubmissionKey(vehicle, insuranceTypeLabel)) +); + +const createInsuranceHistoryRecord = (payload) => ({ + id: payload.id || `ih-${payload.typeKey}-${payload.eventType}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, + typeKey: payload.typeKey, + typeLabel: payload.typeLabel, + eventType: payload.eventType, + purchaseType: payload.purchaseType || eventTypeToDefaultPurchaseType(payload.eventType), + time: payload.time || '', + payTime: payload.payTime || '', + purchaseTime: payload.time || '', + policyNo: payload.policyNo || '', + company: payload.company || '', + premium: payload.premium || '', + startDate: payload.startDate || '', + endDate: payload.endDate || '', + source: payload.source || 'ledger', + sourceLabel: payload.sourceLabel || '', + policyTag: payload.policyTag || '', + reinstateDate: payload.reinstateDate || '', + suspendTime: payload.suspendTime || '', + resumeTime: payload.resumeTime || '', + policyDetail: payload.policyDetail || null, + fileName: payload.fileName || (payload.policyNo ? `${payload.policyNo}_${payload.typeLabel}.pdf` : '保单附件.pdf'), +}); + +const purchaseTypeToBizType = (purchaseType, eventType) => { + if (purchaseType === 'rentStop' || eventType === 'suspend') return 'suspend'; + if (purchaseType === 'resume' || eventType === 'resume') return 'resume'; + if (purchaseType === 'cancel' || eventType === 'cancel') return 'cancel'; + return 'policy'; +}; + +const historyRecordToPolicyDetail = (record, vehicle) => { + if (record?.policyDetail) { + return normalizePolicyDetail({ + ...record.policyDetail, + plateNo: vehicle?.plateNo || record.policyDetail.plateNo, + vin: vehicle?.vin || record.policyDetail.vin, + }); + } + return normalizePolicyDetail({ + plateNo: vehicle?.plateNo || '', + vin: vehicle?.vin || '', + insuranceType: record?.typeLabel || '交强险', + bizType: purchaseTypeToBizType(record?.purchaseType, record?.eventType), + company: record?.company || '', + policyNo: record?.policyNo || '', + payTime: record?.payTime || '', + startDate: record?.startDate || '', + endDate: record?.endDate || '', + reinstateDate: record?.reinstateDate || '', + premium: record?.premium || '', + coverageItems: normalizeCoverageItems(record?.policyDetail?.coverageItems), + applicant: '', + insured: '', + signDate: '', + }); +}; + +const applyPolicyDetailToHistoryRecord = (record, detail) => { + const d = enrichSuspendPolicyDetail(detail); + const time = d.bizType === 'suspend' + ? (d.suspendTime || d.startDate || record.time) + : (d.startDate || record.time); + const typeLabel = d.insuranceType || record.typeLabel; + const next = { + ...record, + policyDetail: d, + typeLabel, + policyNo: d.policyNo || record.policyNo, + company: d.company, + payTime: d.payTime, + startDate: d.startDate, + endDate: d.bizType === 'suspend' ? (d.newEndDate || d.endDate) : d.endDate, + premium: d.premium, + reinstateDate: d.reinstateDate, + suspendTime: d.suspendTime || d.startDate || record.suspendTime || '', + resumeTime: d.resumeTime || d.reinstateDate || record.resumeTime || '', + time: d.bizType === 'suspend' ? (d.suspendTime || d.startDate || time) : time, + purchaseTime: d.bizType === 'suspend' ? (d.suspendTime || d.startDate || time) : time, + fileName: d.policyNo ? `${d.policyNo}_${typeLabel}.pdf` : record.fileName, + }; + next.summary = getInsuranceEventSummary(next); + return next; +}; + +const applyHistoryEditsToVehicleHistory = (history, edits) => { + if (!history || !edits || typeof edits !== 'object') return history; + const patch = (r) => { + const detail = edits[r.id]; + if (!detail) return r; + return applyPolicyDetailToHistoryRecord(r, detail); + }; + const byType = {}; + Object.keys(history.byType || {}).forEach((k) => { + byType[k] = (history.byType[k] || []).map(patch); + }); + return { + ...history, + timeline: (history.timeline || []).map(patch), + byType, + }; +}; + +const buildPolicyDetailFromLedgerItem = (vehicle, typeLabel, item, bizType = 'policy') => normalizePolicyDetail({ + plateNo: vehicle?.plateNo || '', + vin: vehicle?.vin || '', + insuranceType: typeLabel, + bizType, + company: item?.company || '', + policyNo: item?.policyNo || '', + endorsementNo: item?.endorsementNo || '', + payTime: item?.payTime || '', + signDate: item?.signDate || '', + startDate: item?.startDate || '', + endDate: item?.endDate || '', + reinstateDate: item?.reinstateDate || '', + suspendTime: item?.suspendTime || '', + resumeTime: item?.resumeTime || item?.reinstateDate || '', + newEndDate: item?.endDate || '', + premium: item?.premium || '', + coverageItems: parseCoverageItemsInput(item?.coverageItems), + applicant: item?.applicant || '', + insured: item?.insured || '', +}); + +const getInsuranceEventSummary = (record) => { + const premiumText = record.premium ? `,金额 ¥${record.premium}` : ''; + const typeLabel = POLICY_PURCHASE_TYPE_META[record.purchaseType]?.label || '记录'; + const period = record.startDate && record.endDate + ? `,期间 ${record.startDate} 至 ${record.endDate}` + : (record.endDate ? `,到期日期 ${record.endDate}` : ''); + switch (record.purchaseType) { + case 'new': + return `${typeLabel} ${record.typeLabel},保单号 ${record.policyNo || '—'}${period}${premiumText}`; + case 'renew': + return `${typeLabel} ${record.typeLabel},保单号 ${record.policyNo || '—'}${period}${premiumText}`; + case 'rentStop': { + const suspendTime = record.suspendTime || record.policyDetail?.suspendTime || record.startDate || ''; + const resumeTime = record.resumeTime || record.reinstateDate || record.policyDetail?.resumeTime || ''; + const newEndDate = record.endDate || record.policyDetail?.newEndDate || ''; + const parts = [`${typeLabel} · ${record.typeLabel},保单号 ${record.policyNo || '—'}`]; + if (suspendTime) parts.push(`中止 ${suspendTime}`); + if (resumeTime) parts.push(`恢复 ${resumeTime}`); + if (newEndDate) parts.push(`新到期 ${newEndDate}`); + return parts.join(','); + } + case 'resume': + return `${typeLabel} · ${record.typeLabel},保单号 ${record.policyNo || '—'}${period}`; + case 'cancel': + return `${typeLabel} · ${record.typeLabel},保单号 ${record.policyNo || '—'}${premiumText}`; + default: + return `${record.typeLabel} · ${record.policyNo || '—'}`; + } +}; + +const isTimelineBizRecord = (item) => ( + ['rentStop', 'resume', 'cancel'].includes(item?.purchaseType) + || ['suspend', 'resume', 'cancel'].includes(item?.eventType) +); + +const buildOperationLogTimelineEntries = (insRecord) => { + const entries = []; + const seen = new Set(); + INSURANCE_TYPE_ITEMS.forEach(({ key: typeKey, fullLabel: typeLabel }) => { + const pushLogs = (item) => { + if (!item) return; + (item.operationLogs || []).forEach((log) => { + if (!log?.id || seen.has(log.id)) return; + seen.add(log.id); + const purchaseType = log.type === 'suspend' + ? 'rentStop' + : log.type === 'resume' + ? 'resume' + : log.type === 'cancel' + ? 'cancel' + : 'new'; + const record = createInsuranceHistoryRecord({ + id: `tl-op-${log.id}`, + typeKey, + typeLabel, + eventType: log.type === 'add' ? 'purchase' : log.type, + purchaseType, + time: log.time, + policyNo: item.policyNo || '', + company: item.company || '', + source: 'operation', + sourceLabel: '操作记录', + }); + entries.push({ + ...record, + summary: log.remark || `${INSURANCE_OPERATION_TYPE_LABEL[log.type] || log.type} · ${typeLabel}`, + operator: log.operator, + fromOperationLog: true, + }); + }); + }; + const item = insRecord[typeKey]; + pushLogs(item); + (item?.archivedPolicies || []).forEach(pushLogs); + }); + return entries; +}; + +const splitVehicleInsuranceTimeline = (timeline, insRecord) => { + const opEntries = buildOperationLogTimelineEntries(insRecord); + const seenIds = new Set(); + const all = []; + [...(timeline || []), ...opEntries].forEach((item) => { + if (!item?.id || seenIds.has(item.id)) return; + seenIds.add(item.id); + all.push({ + ...item, + summary: item.summary || getInsuranceEventSummary(item), + }); + }); + const timelinePolicy = []; + const timelineBiz = []; + all.forEach((item) => { + if (isTimelineBizRecord(item)) timelineBiz.push(item); + else timelinePolicy.push(item); + }); + const sorter = (a, b) => String(b.time || '').localeCompare(String(a.time || '')); + timelinePolicy.sort(sorter); + timelineBiz.sort(sorter); + return { timelinePolicy, timelineBiz }; +}; + +const buildVehicleInsuranceHistory = (vehicle, allInsurance, compareSheets, policyRecognTasks) => { + const ledgerKey = getVehicleLedgerKey(vehicle); + const record = ensureInsuranceRecordShape(allInsurance[ledgerKey] || createEmptyInsuranceRecord()); + const records = []; + + INSURANCE_TYPE_ITEMS.forEach(({ key: typeKey, fullLabel: typeLabel }) => { + const item = record[typeKey]; + if (!item?.policyNo) return; + records.push(createLedgerMgmtHistoryRecord(vehicle, ledgerKey, typeKey, typeLabel, item)); + }); + + (compareSheets || []).forEach((sheet) => { + (sheet.rows || []).forEach((row) => { + if (!vehicleMatchesCompareRow(vehicle, row)) return; + const typeKey = INSURANCE_LABEL_TO_KEY[row.insuranceType] || 'compulsory'; + const typeLabel = row.insuranceType || INSURANCE_TYPE_ITEMS.find((it) => it.key === typeKey)?.fullLabel || '—'; + const confirmed = (row.quotes || []).find((q) => q.id === row.confirmedQuoteId); + if (!confirmed) return; + const eventType = normalizeCompareProcurementStatus(row.procurementStatus) === 'approved' ? 'procurement' : 'procurement'; + records.push(createInsuranceHistoryRecord({ + id: `ih-compare-${sheet.id}-${row.id}`, + typeKey, + typeLabel, + eventType, + time: row.procurementSubmittedAt || sheet.createdAt, + policyNo: confirmed.policyNo || `CG-${String(row.id).slice(-8)}`, + company: confirmed.company, + premium: confirmed.premium, + source: 'compare', + sourceLabel: sheet.periodLabel ? `比价单 · ${sheet.periodLabel}` : '比价单采购', + fileName: `${sheet.id || 'sheet'}_${typeLabel}_采购单.pdf`, + })); + }); + }); + + (policyRecognTasks || []).forEach((task) => { + (task.results || []).forEach((r) => { + if (!r.confirmed || r.ledgerKey !== ledgerKey) return; + const typeKey = r.typeKey || INSURANCE_LABEL_TO_KEY[r.insuranceTypeLabel]; + if (!typeKey) return; + let eventType = 'recognize'; + if (task.mode === 'suspend') eventType = 'suspend'; + else if (task.mode === 'cancel') eventType = 'cancel'; + else if (task.mode === 'resume') eventType = 'resume'; + records.push(createInsuranceHistoryRecord({ + id: `ih-recognize-${task.id}-${r.id}`, + typeKey, + typeLabel: r.insuranceTypeLabel || INSURANCE_TYPE_ITEMS.find((it) => it.key === typeKey)?.fullLabel, + eventType, + time: task.completedAt || task.createdAt, + policyNo: r.ocrPolicyNo, + company: r.ocrCompany, + premium: r.ocrPremium || '', + payTime: r.ocrPayTime || '', + startDate: r.ocrStartDate || '', + endDate: r.ocrEndDate, + reinstateDate: r.reinstateDate, + policyTag: eventType === 'suspend' ? 'suspended' : eventType === 'cancel' ? 'cancelled' : '', + policyDetail: r.policyDetail ? normalizePolicyDetail(r.policyDetail) : recognResultToPolicyDetail(r), + source: 'recognize', + sourceLabel: task.entryLabel || '批量识别', + recognizeTaskId: task.id, + recognizeResultId: r.id, + fileName: r.fileName || `${r.ocrPolicyNo}_${r.insuranceTypeLabel}.pdf`, + })); + }); + }); + + assignPurchaseTypesToRecords(records); + records.sort((a, b) => String(b.time).localeCompare(String(a.time))); + + const timeline = records.map((r) => ({ + ...r, + summary: getInsuranceEventSummary(r), + })); + + const byType = {}; + INSURANCE_TYPE_ITEMS.forEach(({ key: typeKey, fullLabel: typeLabel }) => { + const item = record[typeKey]; + const typeRows = []; + if (item?.policyNo) { + typeRows.push(createLedgerMgmtHistoryRecord(vehicle, ledgerKey, typeKey, typeLabel, item)); + } + (item?.archivedPolicies || []).forEach((archived, archivedIndex) => { + typeRows.push(createLedgerMgmtHistoryRecord(vehicle, ledgerKey, typeKey, typeLabel, archived, { + id: `ih-${ledgerKey}-${typeKey}-archived-${archivedIndex}`, + isArchived: true, + sourceLabel: '历史保单', + purchaseType: archived.policyTag === 'cancelled' ? 'cancel' : 'renew', + })); + }); + byType[typeKey] = typeRows; + }); + + const { timelinePolicy, timelineBiz } = splitVehicleInsuranceTimeline(timeline, record); + + return { timeline, timelinePolicy, timelineBiz, byType, ledgerKey }; +}; + +const INITIAL_INSURANCE_DATA = { + '沪A03561F': { + compulsory: { company: '中国人民财产保险', policyNo: 'PDZA202533048200000123', startDate: '2025-01-01', endDate: '2026-12-31', premium: '950.00', updateTime: '2025-01-05 10:00', updateUser: '张明辉' }, + commercial: { company: '中国人民财产保险', policyNo: 'PDAA202533048200000456', startDate: '2025-01-01', endDate: '2026-12-31', premium: '12800.50', updateTime: '2025-01-05 10:00', updateUser: '张明辉' }, + excess: { company: '中国平安财产保险', policyNo: 'PAIC-CP-2025-8899', startDate: '2025-07-01', endDate: '2026-06-30', premium: '3200.00', updateTime: '2025-06-28 14:20', updateUser: '李专员' }, + cargo: { company: '中国太平洋财产保险', policyNo: 'CPIC-HW-2025-1122', startDate: '2025-03-15', endDate: '2026-03-14', premium: '1800.00', updateTime: '2025-03-10 09:15', updateUser: '李专员' }, + driverAccident: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + }, + '粤B58888F': { + compulsory: { company: '阳光财产保险', policyNo: 'YGCI-JQ-2025-3301', startDate: '2025-09-01', endDate: '2026-08-31', premium: '950.00', updateTime: '2025-08-28 11:00', updateUser: '王专员' }, + commercial: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + excess: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + cargo: { company: '中国人寿财产保险', policyNo: 'GPIC-HW-2025-7788', startDate: '2025-04-01', endDate: '2026-03-31', premium: '1600.00', updateTime: '2025-03-28 16:40', updateUser: '王专员' }, + driverAccident: { company: '中华联合财产保险', policyNo: 'CIC-JY-2025-001', startDate: '2025-05-01', endDate: '2026-04-30', premium: '560.00', updateTime: '2025-04-28 10:00', updateUser: '王专员' }, + }, + '苏E33333': { + compulsory: { company: '中国太平洋财产保险', policyNo: 'CPIC-JQ-2024-7788', startDate: '2024-06-01', endDate: '2025-05-31', premium: '880.00', updateTime: '2024-05-28 09:00', updateUser: '陈高伟' }, + commercial: { company: '中国人寿财产保险', policyNo: 'GPIC-SY-2025-1122', startDate: '2025-07-01', endDate: '2026-06-30', premium: '9850.00', updateTime: '2025-06-25 15:30', updateUser: '陈高伟' }, + excess: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + cargo: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + driverAccident: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + }, + '京A12345': { + compulsory: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + commercial: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + excess: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + cargo: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + driverAccident: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + }, + '浙A88888': { + compulsory: { company: '中国人民财产保险', policyNo: 'PDZA202533048200000789', startDate: '2025-07-01', endDate: '2026-06-30', premium: '950.00', updateTime: '2025-06-28 10:00', updateUser: '张小凡' }, + commercial: { company: '中国人民财产保险', policyNo: 'PDAA202533048200000790', startDate: '2025-01-01', endDate: '2027-12-31', premium: '15600.00', updateTime: '2025-01-05 10:00', updateUser: '张小凡' }, + excess: { company: '中国平安财产保险', policyNo: 'PAIC-CP-2025-9900', startDate: '2025-01-01', endDate: '2026-12-31', premium: '2800.00', updateTime: '2025-01-03 11:00', updateUser: '张小凡' }, + cargo: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + driverAccident: { company: '阳光财产保险', policyNo: 'YGCI-JY-2025-2200', startDate: '2025-09-01', endDate: '2026-08-31', premium: '480.00', updateTime: '2025-08-30 09:00', updateUser: '张小凡' }, + }, + '沪D66666': { + compulsory: { company: '中国人民财产保险', policyNo: 'PDZA202533048200000321', startDate: '2025-02-01', endDate: '2026-01-31', premium: '950.00', updateTime: '2025-01-28 14:00', updateUser: '赵六' }, + commercial: { company: '中国人民财产保险', policyNo: 'PDAA202533048200000322', startDate: '2025-02-01', endDate: '2026-01-31', premium: '11200.00', updateTime: '2025-01-28 14:00', updateUser: '赵六' }, + excess: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + cargo: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + driverAccident: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + }, + '粤A12345': { + compulsory: { company: '中国平安财产保险', policyNo: 'PAIC-JQ-2025-4455', startDate: '2025-03-01', endDate: '2026-02-28', premium: '950.00', updateTime: '2025-02-25 10:00', updateUser: '张三' }, + commercial: { + company: '中国平安财产保险', + policyNo: 'PAIC-SY-2025-4456', + startDate: '2025-03-01', + endDate: '2026-08-31', + premium: '10500.00', + updateTime: '2026-05-15 14:20', + updateUser: '张三', + policyTag: 'suspended', + suspendTime: '2026-05-10', + resumeTime: '2026-09-01', + reinstateDate: '2026-09-01', + attachments: [{ id: 'att-demo-suspend-1', name: 'PAIC-SY-2025-4456_停保批单.pdf', size: 245760, type: 'application/pdf', uploadedAt: '2026-05-15 14:20:00' }], + operationLogs: [ + { + id: 'iop-demo-suspend-1', + time: '2026-05-15 14:20:00', + operator: '张三', + type: 'suspend', + remark: '保单状态:正常 → 已停保;到期日期:2026-02-28 → 2026-08-31;中止时间:— → 2026-05-10;恢复时间:— → 2026-09-01', + attachments: [{ id: 'att-demo-suspend-1', name: 'PAIC-SY-2025-4456_停保批单.pdf', size: 245760, type: 'application/pdf', uploadedAt: '2026-05-15 14:20:00' }], + }, + ], + }, + excess: { company: '中国太平洋财产保险', policyNo: 'CPIC-CP-2025-5566', startDate: '2025-03-01', endDate: '2026-02-28', premium: '2400.00', updateTime: '2025-02-25 10:00', updateUser: '张三' }, + cargo: { company: '中华联合财产保险', policyNo: 'CIC-HW-2025-7788', startDate: '2025-03-01', endDate: '2026-02-28', premium: '1500.00', updateTime: '2025-02-25 10:00', updateUser: '张三' }, + driverAccident: { company: '阳光财产保险', policyNo: 'YGCI-JY-2025-9900', startDate: '2025-03-01', endDate: '2026-02-28', premium: '520.00', updateTime: '2025-02-25 10:00', updateUser: '张三' }, + }, + '苏A55678': { + compulsory: { company: '中国人寿财产保险', policyNo: 'GPIC-JQ-2025-0011', startDate: '2025-05-01', endDate: '2026-04-30', premium: '880.00', updateTime: '2025-04-28 09:00', updateUser: '孙七' }, + commercial: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + excess: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + cargo: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + driverAccident: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + }, + LZYTBACR2M9999001: { + compulsory: { company: '中国人民财产保险', policyNo: 'PDZA202533048200000901', startDate: '2025-10-01', endDate: '2026-09-30', premium: '950.00', updateTime: '2025-09-28 10:00', updateUser: '李专员' }, + commercial: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + excess: { company: '中国平安财产保险', policyNo: 'PAIC-CP-2025-7701', startDate: '2025-10-01', endDate: '2026-09-30', premium: '2600.00', updateTime: '2025-09-28 10:00', updateUser: '李专员' }, + cargo: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + driverAccident: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + }, + /* 样例1:五类险种均为临期(基准日 2026-06-01 起 30 天内) */ + '浙F08888F': { + compulsory: { company: '中国人民财产保险', policyNo: 'PDZA-DEMO-08888-JQ', startDate: '2025-06-08', endDate: '2026-06-08', premium: '950.00', updateTime: '2026-05-28 10:00', updateUser: '样例数据' }, + commercial: { company: '中国人民财产保险', policyNo: 'PDAA-DEMO-08888-SY', startDate: '2025-06-12', endDate: '2026-06-12', premium: '11800.00', updateTime: '2026-05-28 10:00', updateUser: '样例数据' }, + excess: { company: '中国平安财产保险', policyNo: 'PAIC-DEMO-08888-CP', startDate: '2025-06-16', endDate: '2026-06-16', premium: '2800.00', updateTime: '2026-05-28 10:00', updateUser: '样例数据' }, + cargo: { company: '中国太平洋财产保险', policyNo: 'CPIC-DEMO-08888-HW', startDate: '2025-06-20', endDate: '2026-06-20', premium: '1500.00', updateTime: '2026-05-28 10:00', updateUser: '样例数据' }, + driverAccident: { company: '阳光财产保险', policyNo: 'YGCI-DEMO-08888-JY', startDate: '2025-06-25', endDate: '2026-06-25', premium: '520.00', updateTime: '2026-05-28 10:00', updateUser: '样例数据' }, + }, + /* 样例2:临期 + 到期混合 */ + '浙F07777F': { + compulsory: { company: '中国人寿财产保险', policyNo: 'GPIC-DEMO-07777-JQ', startDate: '2025-05-20', endDate: '2026-05-20', premium: '880.00', updateTime: '2026-05-28 10:00', updateUser: '样例数据' }, + commercial: { company: '中国平安财产保险', policyNo: 'PAIC-DEMO-07777-SY', startDate: '2025-06-10', endDate: '2026-06-10', premium: '10200.00', updateTime: '2026-05-28 10:00', updateUser: '样例数据' }, + excess: { company: '中华联合财产保险', policyNo: 'CIC-DEMO-07777-CP', startDate: '2025-05-31', endDate: '2026-05-31', premium: '2400.00', updateTime: '2026-05-28 10:00', updateUser: '样例数据' }, + cargo: { company: '中国太平洋财产保险', policyNo: 'CPIC-DEMO-07777-HW', startDate: '2025-06-28', endDate: '2026-06-28', premium: '1600.00', updateTime: '2026-05-28 10:00', updateUser: '样例数据' }, + driverAccident: { company: '阳光财产保险', policyNo: 'YGCI-DEMO-07777-JY', startDate: '2025-05-15', endDate: '2026-05-15', premium: '480.00', updateTime: '2026-05-28 10:00', updateUser: '样例数据' }, + }, + /* 样例3:四类临期 + 一类到期 */ + '粤AGP9001': { + compulsory: { company: '中国人民财产保险', policyNo: 'PDZA-DEMO-9001-JQ', startDate: '2025-06-05', endDate: '2026-06-05', premium: '950.00', updateTime: '2026-05-28 10:00', updateUser: '样例数据' }, + commercial: { company: '中国人民财产保险', policyNo: 'PDAA-DEMO-9001-SY', startDate: '2025-06-30', endDate: '2026-06-30', premium: '13200.00', updateTime: '2026-05-28 10:00', updateUser: '样例数据' }, + excess: { company: '中国平安财产保险', policyNo: 'PAIC-DEMO-9001-CP', startDate: '2025-06-18', endDate: '2026-06-18', premium: '3000.00', updateTime: '2026-05-28 10:00', updateUser: '样例数据' }, + cargo: { company: '中华联合财产保险', policyNo: 'CIC-DEMO-9001-HW', startDate: '2025-05-28', endDate: '2026-05-28', premium: '1400.00', updateTime: '2026-05-28 10:00', updateUser: '样例数据' }, + driverAccident: { company: '阳光财产保险', policyNo: 'YGCI-DEMO-9001-JY', startDate: '2025-06-22', endDate: '2026-06-22', premium: '560.00', updateTime: '2026-05-28 10:00', updateUser: '样例数据' }, + }, + /* 样例4:三类临期 + 两类到期(交强/商业同日落临期) */ + '粤AGP9002': { + compulsory: { company: '中国人寿财产保险', policyNo: 'GPIC-DEMO-9002-JQ', startDate: '2025-06-01', endDate: '2026-06-01', premium: '900.00', updateTime: '2026-05-28 10:00', updateUser: '样例数据' }, + commercial: { company: '中国平安财产保险', policyNo: 'PAIC-DEMO-9002-SY', startDate: '2025-06-15', endDate: '2026-06-15', premium: '10800.00', updateTime: '2026-05-28 10:00', updateUser: '样例数据' }, + excess: { company: '中国太平洋财产保险', policyNo: 'CPIC-DEMO-9002-CP', startDate: '2025-06-15', endDate: '2026-06-15', premium: '2600.00', updateTime: '2026-05-28 10:00', updateUser: '样例数据' }, + cargo: { company: '中华联合财产保险', policyNo: 'CIC-DEMO-9002-HW', startDate: '2025-05-10', endDate: '2026-05-10', premium: '1200.00', updateTime: '2026-05-28 10:00', updateUser: '样例数据' }, + driverAccident: { company: '阳光财产保险', policyNo: 'YGCI-DEMO-9002-JY', startDate: '2025-06-30', endDate: '2026-06-30', premium: '500.00', updateTime: '2026-05-28 10:00', updateUser: '样例数据' }, + }, + /* 样例5:五类险种均为临期(均落在 30 天临界内) */ + '沪A09999F': { + compulsory: { company: '中国人民财产保险', policyNo: 'PDZA-DEMO-09999-JQ', startDate: '2025-07-01', endDate: '2026-07-01', premium: '950.00', updateTime: '2026-05-28 10:00', updateUser: '样例数据' }, + commercial: { company: '中国人民财产保险', policyNo: 'PDAA-DEMO-09999-SY', startDate: '2025-07-01', endDate: '2026-07-01', premium: '12500.00', updateTime: '2026-05-28 10:00', updateUser: '样例数据' }, + excess: { company: '中国平安财产保险', policyNo: 'PAIC-DEMO-09999-CP', startDate: '2025-07-01', endDate: '2026-07-01', premium: '2900.00', updateTime: '2026-05-28 10:00', updateUser: '样例数据' }, + cargo: { company: '中国太平洋财产保险', policyNo: 'CPIC-DEMO-09999-HW', startDate: '2025-07-01', endDate: '2026-07-01', premium: '1550.00', updateTime: '2026-05-28 10:00', updateUser: '样例数据' }, + driverAccident: { company: '阳光财产保险', policyNo: 'YGCI-DEMO-09999-JY', startDate: '2025-07-01', endDate: '2026-07-01', premium: '530.00', updateTime: '2026-05-28 10:00', updateUser: '样例数据' }, + }, +}; + +const loadInsuranceFromStorage = () => { + try { + const raw = localStorage.getItem(IPC_STORAGE_KEY); + if (!raw) return null; + const parsed = JSON.parse(raw); + return parsed && typeof parsed === 'object' ? parsed : null; + } catch { + return null; + } +}; + +const persistInsuranceToStorage = (data) => { + try { + localStorage.setItem(IPC_STORAGE_KEY, JSON.stringify(data)); + } catch { + /* ignore */ + } +}; + +const loadCompareSheetsFromStorage = () => { + try { + const raw = localStorage.getItem(IPC_COMPARE_SHEETS_KEY); + if (!raw) return null; + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed : null; + } catch { + return null; + } +}; + +const persistCompareSheetsToStorage = (sheets) => { + try { + localStorage.setItem(IPC_COMPARE_SHEETS_KEY, JSON.stringify(sheets)); + } catch { + /* ignore */ + } +}; + +const POLICY_RECOGN_ENTRY_LABEL = { ocr: '保单批量识别', import: '批量导入' }; +const POLICY_RECOGN_MODE_LABEL = Object.fromEntries(POLICY_OCR_MODES.map((m) => [m.key, m.label])); +const POLICY_RECOGN_STATUS_META = { + pending_confirm: { label: '待确认', color: 'warning' }, + partial: { label: '部分确认', color: 'processing' }, + completed: { label: '已完成', color: 'success' }, +}; + +const validatePolicyRecognDetailForConfirm = (detail, options = {}) => { + const { silent = false, mode: modeOverride, recognResult } = options; + const mode = modeOverride || bizTypeToRecognMode(detail?.bizType); + if (mode === 'suspend') { + const d = enrichSuspendPolicyDetail(detail); + if (!d.policyNo) { + if (!silent) message.warning('请填写保单号'); + return false; + } + if (!d.suspendTime) { + if (!silent) message.warning('请填写中止时间'); + return false; + } + if (!d.newEndDate) { + if (!silent) message.warning('请填写新到期日期'); + return false; + } + return true; + } + if (mode === 'resume') { + const d = enrichResumePolicyDetail(detail); + if (!d.policyNo) { + if (!silent) message.warning('请填写保单号'); + return false; + } + if (!d.resumeTime) { + if (!silent) message.warning('请填写恢复时间'); + return false; + } + if (!d.newEndDate) { + if (!silent) message.warning('请填写新到期日期'); + return false; + } + return true; + } + if (mode === 'cancel') { + const d = enrichCancelPolicyDetail(detail); + if (!d.policyNo) { + if (!silent) message.warning('请填写保单号'); + return false; + } + if (!d.cancelTime) { + if (!silent) message.warning('请填写退保时间'); + return false; + } + if (!isValidPremium(d.premium)) { + if (!silent) message.warning('请填写退保金额'); + return false; + } + return true; + } + const stripped = stripPolicyRecognDraftMeta(detail); + if (!validatePolicyEntryDetail(stripped, { silent }).ok) return false; + if (recognResult && !validatePolicyRecognPlateForConfirm(detail, recognResult, { silent })) return false; + return true; +}; + +const getPolicyRecognSuccessResults = (results) => ( + (results || []).filter((r) => r.recognSuccess !== false) +); + +const derivePolicyRecognTaskStatus = (results) => { + const list = getPolicyRecognSuccessResults(results); + const matched = list.filter((r) => r.matched); + const confirmedMatched = matched.filter((r) => r.confirmed); + if (matched.length > 0 && confirmedMatched.length >= matched.length) return 'completed'; + if (list.some((r) => r.confirmed)) return 'partial'; + return 'pending_confirm'; +}; + +const summarizePolicyRecognTask = (results, extras = {}) => { + const list = results || []; + const successList = getPolicyRecognSuccessResults(list); + const failList = list.filter((r) => r.recognSuccess === false); + const total = extras.totalFileCount ?? list.length; + const done = extras.recognDoneCount ?? (extras.phase === 'recognizing' ? 0 : total); + return { + fileCount: list.length, + totalFileCount: total, + recognDoneCount: done, + recognSuccessCount: successList.length, + recognFailCount: failList.length, + matchedCount: successList.filter((r) => r.matched).length, + confirmedCount: successList.filter((r) => r.confirmed).length, + }; +}; + +const isPolicyRecognTaskRecognizing = (task) => ( + task?.phase === 'recognizing' + || ((task?.totalFileCount || 0) > 0 && (task?.recognDoneCount || 0) < task.totalFileCount) +); + +const buildPolicyRecognTaskRecord = ({ + id, + entry, + mode, + insuranceType, + results, + createdAt, + creator, + status, + completedAt, + phase, + totalFileCount, + recognDoneCount, +}) => { + const taskPhase = phase || 'results'; + const stats = summarizePolicyRecognTask(results, { + totalFileCount, + recognDoneCount, + phase: taskPhase, + }); + return { + id, + createdAt: createdAt || formatCompareSheetNow(), + completedAt: completedAt || '', + entry, + entryLabel: POLICY_RECOGN_ENTRY_LABEL[entry] || entry, + mode, + modeLabel: POLICY_RECOGN_MODE_LABEL[mode] || mode, + insuranceType: insuranceType || '', + creator: creator || PROTO_COMPARE_CREATOR, + status: status || derivePolicyRecognTaskStatus(results), + phase: taskPhase, + ...stats, + results: (results || []).map((r) => ({ ...r })), + fileNames: (results || []).map((r) => r.fileName).filter(Boolean), + }; +}; + +const loadPolicyRecognTasksFromStorage = () => { + try { + const raw = localStorage.getItem(IPC_POLICY_RECOGN_TASKS_KEY); + if (!raw) return null; + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed : null; + } catch { + return null; + } +}; + +const persistPolicyRecognTasksToStorage = (tasks) => { + try { + localStorage.setItem(IPC_POLICY_RECOGN_TASKS_KEY, JSON.stringify(tasks)); + } catch { + /* ignore */ + } +}; + +const loadInsuranceHistoryEditsFromStorage = () => { + try { + const raw = localStorage.getItem(IPC_INSURANCE_HISTORY_EDITS_KEY); + if (!raw) return {}; + const parsed = JSON.parse(raw); + return parsed && typeof parsed === 'object' ? parsed : {}; + } catch { + return {}; + } +}; + +const persistInsuranceHistoryEditsToStorage = (edits) => { + try { + localStorage.setItem(IPC_INSURANCE_HISTORY_EDITS_KEY, JSON.stringify(edits)); + } catch { + /* ignore */ + } +}; + +const createMockPolicyRecognTasks = () => { + const insMap = buildMockInsuranceMap(); + + const filesCompleted1 = [ + { uid: 'demo-c1', name: '粤BDG9701_交强险.pdf', status: 'done' }, + { uid: 'demo-c2', name: '粤AGR9766_商业险.pdf', status: 'done' }, + { uid: 'demo-c3', name: '沪A03561F_交强险.pdf', status: 'done' }, + ]; + const resultsCompleted1 = buildMockOcrResults(filesCompleted1, 'policy', '交强险', insMap); + if (resultsCompleted1[0]) resultsCompleted1[0].confirmed = true; + + const filesCompleted2 = [ + { uid: 'demo-c4', name: '粤B88888_复驶批单.pdf', status: 'done' }, + { uid: 'demo-c5', name: '京ADH1653_复驶批单.pdf', status: 'done' }, + { uid: 'demo-c6', name: '粤BDG9701_复驶批单.pdf', status: 'done' }, + { uid: 'demo-c7', name: '模糊扫描件_复驶.pdf', status: 'done' }, + ]; + const resultsCompleted2 = buildMockOcrResults(filesCompleted2, 'resume', '', insMap); + resultsCompleted2.forEach((r) => { + if (r.recognSuccess !== false) r.confirmed = true; + }); + + const filesSuspend = [ + { uid: 'demo-s1', name: '沪A06192F_商业险_停保批单.pdf', status: 'done' }, + { uid: 'demo-s2', name: '粤AGR0772_商业险停保.pdf', status: 'done' }, + ]; + const resultsSuspend = buildMockOcrResults(filesSuspend, 'suspend', '', insMap); + + return [ + buildPolicyRecognTaskRecord({ + id: 'TASK-83892906', + entry: 'ocr', + mode: 'policy', + insuranceType: '交强险', + results: resultsCompleted1, + createdAt: '2026-05-28 15:20:10', + completedAt: '2026-05-28 15:32:00', + phase: 'results', + totalFileCount: filesCompleted1.length, + recognDoneCount: filesCompleted1.length, + }), + buildPolicyRecognTaskRecord({ + id: 'TASK-84120155', + entry: 'ocr', + mode: 'resume', + insuranceType: '', + results: resultsCompleted2, + createdAt: '2026-05-30 09:15:00', + completedAt: '2026-05-30 09:18:40', + phase: 'results', + totalFileCount: filesCompleted2.length, + recognDoneCount: filesCompleted2.length, + }), + buildPolicyRecognTaskRecord({ + id: 'TASK-84330812', + entry: 'ocr', + mode: 'suspend', + insuranceType: '', + results: resultsSuspend, + createdAt: '2026-06-02 14:22:00', + completedAt: '2026-06-02 14:25:18', + phase: 'results', + totalFileCount: filesSuspend.length, + recognDoneCount: filesSuspend.length, + }), + buildPolicyRecognTaskRecord({ + id: 'TASK-84210588', + entry: 'ocr', + mode: 'policy', + insuranceType: '商业险', + results: [], + createdAt: '2026-06-01 10:08:22', + phase: 'recognizing', + totalFileCount: 5, + recognDoneCount: 2, + }), + ]; +}; + +const buildMockInsuranceMap = () => { + const map = {}; + MOCK_VEHICLES.forEach((v) => { + const key = getVehicleLedgerKey(v); + const seed = getInitialInsuranceSeed(v); + map[key] = seed ? JSON.parse(JSON.stringify(seed)) : createEmptyInsuranceRecord(); + }); + return map; +}; + +const createMockCompareRowWithQuote = (vehicle, insMap, insuranceType, premium, extra = {}) => { + const row = buildCompareRowFromVehicle(vehicle, insMap); + const quoteId = createQuoteId(); + row.insuranceType = insuranceType; + row.quotes = [{ id: quoteId, company: INSURANCE_MGMT_COMPANIES[0], premium }]; + row.confirmedQuoteId = quoteId; + row.latestPayDate = extra.latestPayDate || '2026-06-15'; + row.procurementStatus = normalizeCompareProcurementStatus(extra.procurementStatus || 'none'); + row.procurementSubmittedAt = extra.procurementSubmittedAt || ''; + row.procurementCurrentApprover = extra.procurementCurrentApprover || ''; + return row; +}; + +const createMockCompareSheets = () => { + const insMap = buildMockInsuranceMap(); + const sheet1Rows = normalizeCompareRows([ + createMockCompareRowWithQuote(MOCK_VEHICLES[0], insMap, '交强险', '950.00', { latestPayDate: '2026-06-03', procurementStatus: 'approved' }), + createMockCompareRowWithQuote(MOCK_VEHICLES[0], insMap, '商业险', '12800.50', { + latestPayDate: '2026-06-04', + procurementStatus: 'submitted', + procurementCurrentApprover: MOCK_WORKFLOW_CURRENT_APPROVERS[0], + procurementSubmittedAt: '2026-05-29 10:00:00', + }), + createMockCompareRowWithQuote(MOCK_VEHICLES[1], insMap, '交强险', '950.00', { + latestPayDate: '2026-05-28', + procurementStatus: 'submitted', + procurementSubmittedAt: '2026-05-31 09:15:00', + procurementCurrentApprover: '', + }), + ]); + const sheet2Rows = normalizeCompareRows([ + createMockCompareRowWithQuote(MOCK_VEHICLES[2], insMap, '交强险', '880.00', { latestPayDate: '2026-06-02', procurementStatus: 'approved' }), + createMockCompareRowWithQuote(MOCK_VEHICLES[2], insMap, '商业险', '9850.00', { latestPayDate: '2026-06-02', procurementStatus: 'approved' }), + createMockCompareRowWithQuote(MOCK_VEHICLES[7], insMap, '交强险', '950.00', { + latestPayDate: '2026-06-01', + procurementStatus: 'submitted', + procurementCurrentApprover: MOCK_WORKFLOW_CURRENT_APPROVERS[1], + procurementSubmittedAt: '2026-05-30 14:00:00', + }), + createMockCompareRowWithQuote(MOCK_VEHICLES[7], insMap, '超赔险', '2600.00', { + latestPayDate: '2026-07-01', + procurementStatus: 'submitted', + procurementCurrentApprover: MOCK_WORKFLOW_CURRENT_APPROVERS[2], + procurementSubmittedAt: '2026-05-30 14:00:00', + }), + createMockCompareRowWithQuote(MOCK_VEHICLES[3], insMap, '交强险', '950.00', { + latestPayDate: '2026-06-10', + procurementStatus: 'submitted', + procurementCurrentApprover: MOCK_WORKFLOW_CURRENT_APPROVERS[3], + procurementSubmittedAt: '2026-05-27 16:40:00', + }), + createMockCompareRowWithQuote(MOCK_VEHICLES[1], insMap, '商业险', '11200.00', { + latestPayDate: '2026-05-20', + procurementStatus: 'withdrawn', + procurementSubmittedAt: '2026-05-18 11:20:00', + }), + createMockCompareRowWithQuote(MOCK_VEHICLES[1], insMap, '货物险', '1600.00', { + latestPayDate: '2026-05-22', + procurementStatus: 'rejected', + procurementSubmittedAt: '2026-05-19 09:30:00', + }), + ]); + const sheet3Rows = normalizeCompareRows([ + createMockCompareRowWithQuote(MOCK_VEHICLES[4], insMap, '商业险', '15600.00', { latestPayDate: '2026-05-20' }), + ]); + return [ + normalizeCompareSheet({ + id: 'cs-mock-20260528', + createdAt: '2026-05-28 14:20:00', + createdBy: '张明辉', + periodLabel: '2026年5-6月', + remark: '华东区二季度集中采购', + attachments: [ + { id: 'att-demo-1', uid: 'att-demo-1', name: '6月比价询价单.pdf', size: 245760, type: 'application/pdf', uploadedAt: '2026-05-28 14:18:00' }, + { id: 'att-demo-2', uid: 'att-demo-2', name: '保险公司报价截图.zip', size: 1048576, type: 'application/zip', uploadedAt: '2026-05-28 14:19:00' }, + ], + rows: sheet1Rows, + }), + normalizeCompareSheet({ + id: 'cs-mock-20260520', + createdAt: '2026-05-20 09:15:00', + createdBy: '李专员', + periodLabel: '2026年5月', + remark: '苏粤车辆续保比价', + attachments: [ + { id: 'att-demo-3', uid: 'att-demo-3', name: '5月比价汇总表.xlsx', size: 186240, type: 'application/vnd.ms-excel', uploadedAt: '2026-05-20 09:10:00' }, + ], + rows: sheet2Rows, + }), + normalizeCompareSheet({ + id: 'cs-mock-20260510', + createdAt: '2026-05-10 16:40:00', + createdBy: '王专员', + periodLabel: '2026年5月', + remark: '浙A88888 商业险新保询价', + attachments: [ + { id: 'att-demo-4', uid: 'att-demo-4', name: '询价邮件截图.png', size: 98304, type: 'image/png', uploadedAt: '2026-05-10 16:35:00' }, + ], + rows: sheet3Rows, + }), + ]; +}; + +const compareSheetMatchesPlateFilter = (sheet, plateKey) => { + if (!plateKey) return true; + const key = plateKey.trim().toLowerCase(); + return (sheet.rows || []).some((row) => { + const plate = (row.plateNo || '').trim().toLowerCase(); + const vin = (row.vin || '').trim().toLowerCase(); + if (plate && plate.includes(key)) return true; + if (!plate && (NO_PLATE_LABEL.toLowerCase().includes(key) || key.includes('暂无'))) return true; + if (vin && vin.includes(key)) return true; + return false; + }); +}; + +const compareSheetMatchesCreatedRange = (createdAt, range) => { + if (!range || !range[0] || !range[1]) return true; + if (!createdAt || !moment) return true; + const day = moment(String(createdAt).slice(0, 10), 'YYYY-MM-DD', true); + if (!day.isValid()) return true; + const start = range[0].clone().startOf('day'); + const end = range[1].clone().endOf('day'); + return day.isSameOrAfter(start) && day.isSameOrBefore(end); +}; + +const insuranceEndDateMatchesRange = (endDate, range) => { + if (!range?.[0] || !range?.[1]) return true; + if (!endDate || !moment) return false; + const day = moment(endDate, 'YYYY-MM-DD', true); + if (!day.isValid()) return false; + const start = range[0].clone().startOf('day'); + const end = range[1].clone().endOf('day'); + return day.isSameOrAfter(start) && day.isSameOrBefore(end); +}; + +const vehicleMatchesListInsuranceTypeFilter = (ledgerKey, insuranceTypeLabel, endDateRange, insuranceData) => { + if (!insuranceTypeLabel) return true; + const typeKey = INSURANCE_LABEL_TO_KEY[insuranceTypeLabel]; + if (!typeKey) return true; + const item = insuranceData?.[ledgerKey]?.[typeKey]; + const endDate = item?.endDate; + if (!endDate && !item?.policyNo) return false; + if (endDateRange?.[0] && endDateRange?.[1]) { + return insuranceEndDateMatchesRange(endDate, endDateRange); + } + return true; +}; + +const compareSheetMatchesPayAlertFilter = (sheet, filter) => { + if (!filter || filter === 'all') return true; + const alerts = calcCompareSheetPayAlerts(sheet); + if (filter === 'warning') return alerts.warning > 0; + if (filter === 'overdue') return alerts.overdue > 0; + return true; +}; + +const DEFAULT_COMPARE_MGMT_FILTERS = { plateNo: '', createdRange: null, payAlertFilter: 'all' }; +const DEFAULT_COMPARE_EDITOR_FILTERS = { vehicles: '', latestPayWithin3Days: false, insuranceType: '' }; +const DEFAULT_POLICY_RECOGN_TASK_FILTERS = { mode: '全部', createdRange: null }; + +const compareRowMatchesVehicleFilter = (row, vehicleText) => { + const tokens = parseMultiPlates(vehicleText); + if (!tokens.length) return true; + const plate = (row.plateNo || '').trim().toUpperCase(); + const vin = (row.vin || '').trim().toUpperCase(); + return tokens.some((token) => { + if (plate && (plate === token || plate.includes(token))) return true; + if (vin && (vin === token || vin.includes(token))) return true; + return false; + }); +}; + +const isCompareRowLatestPayWithinDays = (row, days = LATEST_PAY_WARN_DAYS) => { + const diff = getLatestPayDateDiffDays(row.latestPayDate); + if (diff === null) return false; + return diff <= days; +}; + +const filterCompareEditorRows = (rows, filters) => { + const type = filters.insuranceType || ''; + return (rows || []).filter((row) => { + if (!compareRowMatchesVehicleFilter(row, filters.vehicles)) return false; + if (filters.latestPayWithin3Days && !isCompareRowLatestPayWithinDays(row)) return false; + if (type && (row.insuranceType || '交强险') !== type) return false; + return true; + }); +}; + +const countCompareRowsByInsuranceType = (rows) => { + const counts = {}; + QUOTE_INSURANCE_TYPES.forEach((t) => { counts[t] = 0; }); + (rows || []).forEach((row) => { + const t = row.insuranceType || '交强险'; + if (counts[t] !== undefined) counts[t] += 1; + }); + return counts; +}; + +const tableTitleMultiline = (...lines) => ( +
+ {lines.map((line, idx) => ( + {line} + ))} +
+); + +const listColumnHeaderCell = () => ({ className: 'lc-th-wrap' }); + +const mapInsuranceStatusToBadge = (type) => { + if (type === 'success') return 'success'; + if (type === 'warning') return 'warning'; + if (type === 'expired') return 'error'; + return 'default'; +}; + +/** 到期日期列:剩余 / 过期天数文案 */ +const getInsuranceRemainShortText = (status) => { + const { type, diffDays } = status || {}; + if (type === 'unuploaded') return '未购买'; + if (type === 'expired') return diffDays != null ? `过期${Math.abs(diffDays)}天` : '已过期'; + if (diffDays != null) return `剩余${diffDays}天`; + return '—'; +}; + +const sortVehiclesRetiredLast = (vehicles) => { + const active = []; + const retired = []; + vehicles.forEach((v) => { + if (v.status === '退出运营') retired.push(v); + else active.push(v); + }); + return [...active, ...retired]; +}; + +const parseMultiPlates = (text) => { + const raw = (text || '').trim(); + if (!raw) return []; + const lines = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean); + const expanded = lines.flatMap((line) => { + if (/[,,、;;]/.test(line)) { + return line.split(/[,,、;;]+/).map((s) => s.trim()).filter(Boolean); + } + return [line]; + }); + return [...new Set(expanded.map((s) => s.toUpperCase()))]; +}; + +/** 批量新增比价行:每行一条车牌或 VIN,不去重 */ +const parseBatchVehicleLines = (text) => ( + (text || '').trim().split(/\r?\n/).map((line) => line.trim()).filter(Boolean) +); + +const findVehicleByPlateOrVin = (token) => { + const key = (token || '').trim(); + if (!key) return null; + return findVehicleByPlate(key) || findVehicleByVin(key); +}; + +const ICONS = { + vehicle: , + success: , + warning: , + shield: , + policy: , + more: , +}; + +const PAGE_STYLE = ` +.lc-edit-page { font-family: system-ui, -apple-system, sans-serif; color: #1e293b; } +.lc-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; } +.lc-filter-card.ant-card { border-radius: 16px !important; border: 1px solid #e2e8f0 !important; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.03) !important; margin-bottom: 16px; } +.lc-filter-card > .ant-card-head { border-bottom: 1px solid #f1f5f9 !important; min-height: auto; padding: 12px 20px !important; } +.lc-filter-card > .ant-card-head .ant-card-head-title { font-size: 15px !important; font-weight: 700 !important; color: #0f172a !important; padding: 0 !important; } +.lc-filter-card > .ant-card-body { padding: 16px 20px 20px !important; } +.lc-filter-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px 24px; } +@media (max-width: 1100px) { .lc-filter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } +@media (max-width: 720px) { .lc-filter-grid { grid-template-columns: 1fr; } } +.lc-filter-field { display: flex; align-items: center; gap: 12px; min-width: 0; } +.lc-filter-field-label { flex: 0 0 72px; text-align: right; font-size: 13px; font-weight: 500; color: #475569; line-height: 1.4; white-space: nowrap; } +.lc-filter-field-control { flex: 1; min-width: 0; } +.lc-filter-field-control .ant-input, .lc-filter-field-control .ant-select { width: 100%; } +.lc-multi-plate-pop { width: 320px; padding: 4px 2px; } +.lc-multi-plate-pop-hint { font-size: 12px; color: #64748b; margin-bottom: 8px; line-height: 1.5; } +.lc-multi-plate-pop-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 10px; } +.lc-multi-plate-trigger { cursor: pointer; } +.lc-multi-plate-trigger .ant-input { cursor: pointer; } +.lc-filter-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; padding-top: 16px; border-top: 1px solid #f1f5f9; } +.lc-alert-stats-row { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; } +@media (max-width: 1200px) { .lc-alert-stats-row { grid-template-columns: repeat(3, minmax(0, 1fr)); } } +@media (max-width: 768px) { .lc-alert-stats-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } } +.lc-alert-card { display: flex; align-items: flex-start; gap: 12px; padding: 14px 30px 14px 16px; border-radius: 12px; border: 1px solid #e2e8f0; background: #fff; position: relative; overflow: hidden; min-width: 0; } +.lc-alert-card-main { flex: 1; min-width: 0; } +.lc-alert-card-icon { flex-shrink: 0; width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; } +.lc-alert-card-val { font-size: 26px; font-weight: 800; line-height: 1.1; color: #0f172a; font-variant-numeric: tabular-nums; } +.lc-alert-card-title { font-size: 13px; font-weight: 600; color: #334155; margin-top: 2px; } +.lc-alert-card-tip-anchor { position: absolute; top: 8px; right: 8px; z-index: 2; line-height: 0; } +.lc-alert-card-tip { width: 18px; height: 18px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; color: #94a3b8; background: rgba(255,255,255,.92); border: 1px solid #e2e8f0; cursor: help; line-height: 0; } +.lc-alert-card-tip:hover { color: #64748b; border-color: #cbd5e1; background: #fff; } +.lc-alert-card--total { background: linear-gradient(135deg, #f8fafc 0%, #fff 100%); } +.lc-alert-card--total .lc-alert-card-icon { background: #e2e8f0; color: #475569; } +.lc-alert-card--normal { background: linear-gradient(135deg, #ecfdf5 0%, #fff 55%); border-color: #bbf7d0; } +.lc-alert-card--normal .lc-alert-card-icon { background: #d1fae5; color: #059669; } +.lc-alert-card--normal .lc-alert-card-val { color: #047857; } +.lc-alert-card--warning { background: linear-gradient(135deg, #fff7ed 0%, #fff 55%); border-color: #fed7aa; } +.lc-alert-card--warning .lc-alert-card-icon { background: #ffedd5; color: #ea580c; } +.lc-alert-card--warning .lc-alert-card-val { color: #c2410c; } +.lc-alert-card--expired { background: linear-gradient(135deg, #fef2f2 0%, #fff 55%); border-color: #fecaca; } +.lc-alert-card--expired .lc-alert-card-icon { background: #fee2e2; color: #dc2626; } +.lc-alert-card--expired .lc-alert-card-val { color: #b91c1c; } +.lc-alert-card--unuploaded { background: linear-gradient(135deg, #f8fafc 0%, #fff 55%); } +.lc-alert-card--unuploaded .lc-alert-card-icon { background: #f1f5f9; color: #64748b; } +.lc-alert-card--unuploaded .lc-alert-card-val { color: #64748b; } +.lc-alert-card-clickable { cursor: pointer; transition: box-shadow .2s ease, border-color .2s ease; } +.lc-alert-card-clickable:hover { box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08); } +.lc-alert-card-active { box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.2) !important; border-color: #165dff !important; } +.lc-table-section { margin-bottom: 0; } +.lc-table-toolbar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px 16px; margin-bottom: 8px; min-height: 32px; } +.lc-table-card { background: #fff; border-radius: 16px; border: 1px solid #e2e8f0; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.03); overflow: hidden; } +.lc-table-card .ant-table-thead > tr > th { background: #f8fafc !important; color: #475569 !important; font-weight: 700 !important; font-size: 13px !important; border-bottom: 1px solid #e2e8f0 !important; padding: 12px 16px !important; vertical-align: middle; } +.lc-table-card .ant-table-thead > tr > th.lc-th-wrap { padding: 10px 8px !important; text-align: center; vertical-align: middle; white-space: nowrap; } +.lc-table-th-multiline { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; line-height: 1.3; white-space: normal; } +.lc-table-th-line { display: block; font-size: 12px; font-weight: 700; color: #475569; } +.lc-list-expire-cell { display: flex; flex-direction: column; gap: 4px; min-width: 0; } +.lc-list-expire-date { font-size: 12px; font-weight: 600; line-height: 1.35; } +.lc-list-expire-meta { line-height: 1.2; } +.lc-list-status-badge-wrap { display: inline-flex; max-width: 100%; } +.lc-list-status-badge-wrap .ant-badge { display: inline-flex; align-items: center; max-width: 100%; } +.lc-list-status-badge-text { font-size: 11px; font-weight: 600; white-space: nowrap; } +.lc-list-table .ant-table-wrapper, .lc-list-table .ant-table { width: 100% !important; } +.lc-list-table .ant-table-content table { table-layout: fixed; width: 100% !important; } +.lc-cell-ellipsis { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.lc-table-card .ant-table-tbody > tr:not(.ant-table-measure-row) > td { padding: 10px 8px !important; border-bottom: 1px solid #f1f5f9 !important; } +.lc-table-card .ant-table-tbody > tr:not(.ant-table-measure-row):hover > td { background: #f8fafc !important; } +.lc-table-card .ant-table-tbody > tr.lc-row-retired:not(.ant-table-measure-row) > td { background: #f8fafc !important; color: #94a3b8; } +.lc-list-plate-cell { display: flex; flex-direction: column; gap: 2px; min-width: 0; } +.lc-list-plate-sub { display: block; font-size: 11px; font-weight: 500; line-height: 1.35; } +.lc-list-plate-empty { color: #94a3b8 !important; font-style: italic; font-weight: 500 !important; } +.lc-table-toolbar-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-left: auto; } +.lc-compare-modal .ant-modal-content { border-radius: 16px; overflow: hidden; } +.lc-compare-modal .ant-modal-header { background: linear-gradient(135deg, #f8fafc 0%, #fff 100%); border-bottom: 1px solid #e2e8f0; padding: 16px 20px; } +.lc-compare-modal .ant-modal-title { font-size: 16px; font-weight: 700; color: #0f172a; } +.lc-compare-modal .ant-modal-body { padding: 16px 20px 12px; max-height: calc(100vh - 180px); overflow: auto; } +.lc-compare-modal .ant-modal-footer { border-top: 1px solid #f1f5f9; padding: 12px 20px 16px; } +.lc-compare-toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; flex-wrap: wrap; gap: 8px; } +.lc-compare-table-wrap { border-radius: 12px; border: 1px solid #e2e8f0; overflow: hidden; background: #fff; box-shadow: 0 4px 16px -4px rgba(15, 23, 42, 0.06); } +.lc-compare-table .ant-table-thead > tr > th { background: #f1f5f9 !important; color: #334155 !important; font-size: 12px !important; font-weight: 700 !important; padding: 10px 8px !important; white-space: nowrap; border-bottom: 1px solid #e2e8f0 !important; } +.lc-compare-table .ant-table-thead > tr > th.lc-compare-th-key { background: #ecfdf5 !important; color: #065f46 !important; } +.lc-compare-table .ant-table-thead > tr > th.lc-compare-th-auto { background: #f8fafc !important; } +.lc-compare-table .ant-table-thead > tr > th.lc-compare-th-edit { background: #fffbeb !important; color: #92400e !important; } +.lc-compare-th-batch-trigger { display: inline-flex; align-items: center; gap: 4px; cursor: pointer; user-select: none; white-space: nowrap; } +.lc-compare-th-batch-trigger:hover { color: #059669 !important; } +.lc-compare-th-batch-tag { font-size: 10px; font-weight: 700; color: #059669; background: #ecfdf5; border: 1px solid #bbf7d0; padding: 0 4px; border-radius: 4px; line-height: 16px; } +.lc-compare-table .ant-table-thead > tr > th.ant-table-cell-fix-right.lc-compare-th-edit { background: #fffbeb !important; z-index: 3; } +.lc-compare-table .ant-table-thead > tr > th.ant-table-cell-fix-right.lc-compare-th-auto { background: #f8fafc !important; z-index: 3; } +.lc-compare-table .ant-table-tbody > tr > td.ant-table-cell-fix-right { z-index: 2; } +.lc-compare-table .ant-table-tbody > tr > td { padding: 8px 6px !important; vertical-align: middle !important; font-size: 12px; border-bottom: 1px solid #f1f5f9 !important; } +.lc-compare-table .ant-table-tbody > tr:nth-child(even) > td { background: #fafbfc; } +.lc-compare-table .ant-table-tbody > tr:hover > td { background: #f0fdf4 !important; } +.lc-compare-table .ant-table-tbody > tr.ant-table-row-selected > td { background: #ecfdf5 !important; } +.lc-compare-table .ant-table-thead > tr > th.ant-table-cell-fix-left, +.lc-compare-table .ant-table-thead > tr > th.ant-table-cell-fix-right { background: #f1f5f9 !important; } +.lc-compare-table .ant-table-thead > tr > th.ant-table-cell-fix-left.lc-compare-th-key { background: #ecfdf5 !important; } +.lc-compare-table .ant-table-tbody > tr > td.ant-table-cell-fix-left, +.lc-compare-table .ant-table-tbody > tr > td.ant-table-cell-fix-right { background: #fff !important; } +.lc-compare-table .ant-table-tbody > tr:nth-child(even) > td.ant-table-cell-fix-left, +.lc-compare-table .ant-table-tbody > tr:nth-child(even) > td.ant-table-cell-fix-right { background: #fafbfc !important; } +.lc-compare-table .ant-table-tbody > tr:hover > td.ant-table-cell-fix-left, +.lc-compare-table .ant-table-tbody > tr:hover > td.ant-table-cell-fix-right { background: #f0fdf4 !important; } +.lc-compare-table .ant-table-tbody > tr.ant-table-row-selected > td.ant-table-cell-fix-left, +.lc-compare-table .ant-table-tbody > tr.ant-table-row-selected > td.ant-table-cell-fix-right { background: #ecfdf5 !important; } +.lc-compare-table .ant-table-cell-fix-left-last::after, +.lc-compare-table .ant-table-cell-fix-right-first::after { box-shadow: inset 10px 0 8px -8px rgba(15, 23, 42, 0.12); } +.lc-compare-cell-select { width: 100%; min-width: 100px; } +.lc-compare-cell-select .ant-select-selector { border-radius: 6px !important; font-size: 12px !important; min-height: 28px !important; } +.lc-compare-cell-input { width: 100%; min-width: 88px; border-radius: 6px; } +.lc-compare-cell-input.ant-input-sm, .lc-compare-cell-input.ant-picker-small { font-size: 12px; } +.lc-compare-readonly { display: block; padding: 4px 8px; min-height: 28px; line-height: 20px; border-radius: 6px; background: #f8fafc; border: 1px solid #f1f5f9; color: #334155; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.lc-compare-readonly--wrap { white-space: normal; word-break: break-all; line-height: 1.4; } +.lc-compare-readonly.is-empty { color: #94a3b8; } +.lc-compare-readonly.is-linked { background: #f0fdf4; border-color: #bbf7d0; color: #065f46; font-weight: 500; } +.lc-compare-action-cell { display: flex; align-items: center; gap: 2px; flex-wrap: wrap; } +.lc-compare-action-cell .ant-btn-link { font-size: 12px; font-weight: 600; padding: 0 4px; height: auto; } +.lc-compare-quote-btn { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 6px; background: #ecfdf5; border: 1px solid #a7f3d0; color: #059669 !important; font-weight: 600 !important; font-size: 12px !important; } +.lc-compare-quote-btn:hover { background: #d1fae5 !important; } +.lc-quote-popover-overlay .ant-popover-inner { padding: 0 !important; border-radius: 14px !important; overflow: hidden; box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.22) !important; border: 1px solid #e2e8f0; } +.lc-quote-popover-overlay .ant-popover-arrow { display: none; } +.lc-quote-card { width: 400px; max-width: 92vw; } +.lc-quote-card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 14px 16px; background: linear-gradient(135deg, #ecfdf5 0%, #f8fafc 55%, #fff 100%); border-bottom: 1px solid #e2e8f0; } +.lc-quote-card-title { font-size: 15px; font-weight: 700; color: #0f172a; } +.lc-quote-card-plate { font-size: 11px; font-weight: 600; color: #059669; background: #d1fae5; border: 1px solid #a7f3d0; padding: 2px 8px; border-radius: 999px; white-space: nowrap; } +.lc-quote-card-body { padding: 14px 16px 16px; max-height: 420px; overflow-y: auto; } +.lc-quote-card-body::-webkit-scrollbar { width: 4px; } +.lc-quote-card-body::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 2px; } +.lc-quote-list-label { font-size: 11px; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 8px; } +.lc-quote-empty { text-align: center; padding: 20px 12px; border-radius: 10px; background: #f8fafc; border: 1px dashed #e2e8f0; color: #94a3b8; font-size: 12px; margin-bottom: 14px; } +.lc-quote-item-card { display: flex; align-items: flex-start; gap: 10px; padding: 10px 12px; margin-bottom: 8px; border-radius: 10px; border: 1px solid #e2e8f0; background: #fff; transition: border-color .2s, box-shadow .2s; } +.lc-quote-item-card:hover { border-color: #cbd5e1; box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04); } +.lc-quote-item-card.is-selected { border-color: #10b981; background: linear-gradient(135deg, #f0fdf4 0%, #fff 100%); box-shadow: 0 0 0 1px rgba(16, 185, 129, 0.15); } +.lc-quote-item-card .ant-radio { margin-top: 2px; } +.lc-quote-item-main { flex: 1; min-width: 0; } +.lc-quote-item-company { font-size: 13px; font-weight: 600; color: #0f172a; line-height: 1.4; margin-bottom: 4px; } +.lc-quote-item-row { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; font-size: 12px; color: #64748b; } +.lc-quote-type-tag { display: inline-flex; align-items: center; padding: 1px 7px; border-radius: 4px; background: #eff6ff; border: 1px solid #bfdbfe; color: #1d4ed8; font-size: 11px; font-weight: 600; } +.lc-quote-price { font-size: 14px; font-weight: 700; color: #059669; font-variant-numeric: tabular-nums; } +.lc-quote-price-unit { font-size: 11px; font-weight: 500; color: #64748b; margin-left: 2px; } +.lc-quote-item-del { flex-shrink: 0; padding: 0 4px !important; height: auto !important; font-size: 12px !important; } +.lc-quote-form-wrap { margin-top: 14px; padding-top: 14px; border-top: 1px solid #f1f5f9; } +.lc-quote-form-title { font-size: 13px; font-weight: 700; color: #0f172a; margin-bottom: 12px; display: flex; align-items: center; gap: 6px; } +.lc-quote-form-title::before { content: ''; width: 3px; height: 14px; border-radius: 2px; background: #10b981; } +.lc-quote-form-field { margin-bottom: 10px; } +.lc-quote-form-label { display: block; font-size: 12px; font-weight: 600; color: #475569; margin-bottom: 5px; } +.lc-quote-form-label-required::after { content: ' *'; color: #ef4444; } +.lc-quote-form-actions { display: flex; justify-content: flex-end; margin-top: 4px; } +.lc-quote-form-actions .ant-btn-primary { border-radius: 8px; font-weight: 600; background: linear-gradient(135deg, #10b981 0%, #059669 100%); border: none; box-shadow: 0 2px 8px rgba(16, 185, 129, 0.25); } +.lc-compare-quote-cell { display: flex; flex-direction: column; gap: 6px; min-width: 0; } +.lc-compare-quote-inline-list { display: flex; flex-direction: column; gap: 4px; width: 100%; } +.lc-compare-quote-inline-item { display: flex; align-items: center; gap: 4px; padding: 4px 6px; border-radius: 6px; border: 1px solid #e2e8f0; background: #fff; cursor: pointer; transition: border-color .15s, background .15s; margin: 0 !important; } +.lc-compare-quote-inline-item:hover { border-color: #cbd5e1; background: #f8fafc; } +.lc-compare-quote-inline-item.is-selected { border-color: #10b981; background: #f0fdf4; box-shadow: 0 0 0 1px rgba(16, 185, 129, 0.12); } +.lc-compare-quote-inline-item .ant-radio { margin-right: 0; top: 0; } +.lc-compare-quote-inline-company { flex: 1; min-width: 0; font-size: 11px; font-weight: 600; color: #334155; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.lc-compare-quote-inline-price { flex-shrink: 0; font-size: 12px; font-weight: 700; color: #059669; font-variant-numeric: tabular-nums; } +.lc-compare-quote-inline-del { flex-shrink: 0; padding: 0 2px !important; height: 18px !important; min-width: 18px !important; font-size: 14px !important; line-height: 1 !important; color: #94a3b8 !important; } +.lc-compare-quote-inline-del:hover { color: #ef4444 !important; } +.lc-compare-quote-add { padding: 0 !important; height: auto !important; font-size: 12px !important; font-weight: 600 !important; color: #059669 !important; align-self: flex-start; } +.lc-compare-quote-empty-hint { font-size: 11px; color: #94a3b8; line-height: 1.4; } +.lc-quote-card-type-badge { font-size: 11px; font-weight: 600; color: #1d4ed8; background: #eff6ff; border: 1px solid #bfdbfe; padding: 2px 8px; border-radius: 999px; white-space: nowrap; } +.lc-compare-footer { margin-top: 14px; padding-top: 14px; border-top: 1px solid #e2e8f0; display: flex; flex-direction: column; gap: 12px; } +.lc-compare-remark-field { display: flex; flex-direction: column; gap: 6px; } +.lc-compare-remark-label { font-size: 13px; font-weight: 600; color: #334155; } +.lc-compare-field-label-required::after { content: '*'; color: #ef4444; margin-left: 2px; } +.lc-compare-total-row { display: flex; gap: 12px; align-items: stretch; } +@media (max-width: 720px) { .lc-compare-total-row { flex-direction: column; } } +.lc-compare-total-bar { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px 16px; padding: 12px 16px; border-radius: 10px; background: linear-gradient(135deg, #ecfdf5 0%, #f8fafc 100%); border: 1px solid #bbf7d0; } +.lc-compare-total-row .lc-compare-total-bar { flex: 1; min-width: 0; flex-direction: column; align-items: flex-start; gap: 6px; } +.lc-compare-total-label { font-size: 13px; font-weight: 600; color: #334155; } +.lc-compare-total-amount { font-size: 22px; font-weight: 800; color: #059669; font-variant-numeric: tabular-nums; line-height: 1.2; } +.lc-compare-total-unit { font-size: 13px; font-weight: 600; color: #059669; margin-left: 2px; } +.lc-compare-total-hint { font-size: 12px; color: #64748b; margin-left: auto; } +.lc-compare-total-row .lc-compare-total-hint { margin-left: 0; } +.lc-copy-pop { width: 200px; } +.lc-copy-pop-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 10px; } +.lc-compare-mgmt-modal .ant-modal-body { padding: 16px 20px 20px; } +.lc-compare-mgmt-filter { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px 24px; margin-bottom: 14px; padding-bottom: 14px; border-bottom: 1px solid #f1f5f9; } +@media (max-width: 720px) { .lc-compare-mgmt-filter { grid-template-columns: 1fr; } } +.lc-compare-mgmt-toolbar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; } +.lc-compare-mgmt-table .ant-table-thead > tr > th { background: #f8fafc !important; font-weight: 700 !important; font-size: 13px !important; } +.lc-compare-mgmt-table .ant-table-tbody > tr > td { font-size: 13px; } +.lc-compare-mgmt-count { font-variant-numeric: tabular-nums; font-weight: 700; color: #0f172a; } +.lc-expiring-warn-modal .ant-modal-body { padding: 16px 20px 20px; } +.lc-expiring-warn-filter { margin-bottom: 14px; padding: 12px 14px; border-radius: 10px; background: #f8fafc; border: 1px solid #e2e8f0; } +.lc-expiring-warn-filter-label { font-size: 12px; font-weight: 700; color: #64748b; margin-bottom: 8px; } +.lc-expiring-warn-filter .ant-checkbox-group { display: flex; flex-wrap: wrap; gap: 8px 16px; } +.lc-expiring-warn-toolbar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; } +.lc-expiring-warn-table .ant-table-thead > tr > th { background: #f8fafc !important; font-weight: 700 !important; font-size: 13px !important; cursor: pointer; } +.lc-expiring-warn-table .ant-table-tbody > tr > td { font-size: 13px; } +.lc-compare-pay-alert-row { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin-bottom: 14px; } +.lc-compare-pay-alert { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 14px; border-radius: 10px; border: 1px solid #e2e8f0; background: #fff; cursor: pointer; transition: box-shadow .2s ease, border-color .2s ease; } +.lc-compare-pay-alert:hover { box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08); } +.lc-compare-pay-alert--active { box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.2) !important; border-color: #165dff !important; } +.lc-compare-pay-alert--all { border-color: #cbd5e1; background: linear-gradient(135deg, #f8fafc 0%, #fff 80%); } +.lc-compare-pay-alert--warning { border-color: #fed7aa; background: linear-gradient(135deg, #fff7ed 0%, #fff 80%); } +.lc-compare-pay-alert--overdue { border-color: #fecaca; background: linear-gradient(135deg, #fef2f2 0%, #fff 80%); } +.lc-compare-pay-alert-val { font-size: 22px; font-weight: 800; font-variant-numeric: tabular-nums; line-height: 1; } +.lc-compare-pay-alert--all .lc-compare-pay-alert-val { color: #0f172a; } +.lc-compare-pay-alert--warning .lc-compare-pay-alert-val { color: #c2410c; } +.lc-compare-pay-alert--overdue .lc-compare-pay-alert-val { color: #b91c1c; } +.lc-compare-editor-filter { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px 20px; margin-bottom: 12px; padding: 12px 14px; border-radius: 10px; background: #f8fafc; border: 1px solid #e2e8f0; align-items: end; } +@media (max-width: 900px) { .lc-compare-editor-filter { grid-template-columns: 1fr; } } +.lc-compare-editor-filter-check { display: flex; align-items: center; min-height: 32px; padding: 4px 0; } +.lc-compare-type-stats-row { margin-bottom: 12px; grid-template-columns: repeat(6, minmax(0, 1fr)); } +@media (max-width: 1200px) { .lc-compare-type-stats-row { grid-template-columns: repeat(3, minmax(0, 1fr)); } } +@media (max-width: 768px) { .lc-compare-type-stats-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } } +.lc-compare-type-card { display: flex; flex-direction: column; gap: 4px; padding: 10px 14px; border-radius: 10px; border: 1px solid #e2e8f0; background: #fff; cursor: pointer; transition: box-shadow .2s ease, border-color .2s ease; min-width: 0; } +.lc-compare-type-card:hover { box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08); } +.lc-compare-type-card.is-active { box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.2); border-color: #165dff; background: linear-gradient(135deg, #eff6ff 0%, #fff 80%); } +.lc-compare-type-card-val { font-size: 22px; font-weight: 800; color: #0f172a; font-variant-numeric: tabular-nums; line-height: 1.1; } +.lc-compare-type-card-title { font-size: 12px; font-weight: 600; color: #64748b; } +.lc-compare-editor-meta { display: flex; flex-wrap: wrap; gap: 12px 20px; margin-bottom: 12px; padding: 10px 14px; border-radius: 10px; background: #f8fafc; border: 1px solid #e2e8f0; } +.lc-compare-editor-meta-field { display: flex; align-items: center; gap: 8px; min-width: 200px; flex: 1; } +.lc-compare-editor-meta-label { font-size: 12px; font-weight: 600; color: #64748b; white-space: nowrap; } +.lc-compare-procurement-hint { font-size: 12px; color: #64748b; } +.lc-compare-total-bar--procurement { border-color: #bfdbfe; background: linear-gradient(135deg, #eff6ff 0%, #f8fafc 100%); } +.lc-compare-total-bar--procurement .lc-compare-total-amount { color: #1d4ed8; } +.lc-module-tabs.ant-tabs > .ant-tabs-nav { margin-bottom: 12px; } +.lc-module-tabs .ant-tabs-tab { font-weight: 600; font-size: 14px; } +.lc-policy-toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; } +.lc-policy-ocr-upload { margin: 12px 0; } +.lc-policy-import-template-bar { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 12px 14px; margin-bottom: 12px; border-radius: 10px; background: linear-gradient(135deg, #ecfdf5 0%, #f8fafc 100%); border: 1px solid #a7f3d0; } +.lc-policy-import-template-bar-text { font-size: 12px; color: #047857; line-height: 1.55; flex: 1; min-width: 200px; } +.lc-policy-import-excel-upload { margin: 0; } +.lc-policy-recogn-modal .ant-modal-body { padding: 16px 20px 20px; max-height: calc(100vh - 160px); overflow-y: auto; } +.lc-policy-recogn-file-list { margin-top: 10px; max-height: 140px; overflow-y: auto; } +.lc-policy-recogn-file-item { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 0; border-bottom: 1px solid #f1f5f9; font-size: 12px; } +.lc-policy-recogn-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; flex-wrap: wrap; } +.lc-policy-recogn-tasks-filter { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px 16px; margin-bottom: 12px; } +@media (max-width: 900px) { .lc-policy-recogn-tasks-filter { grid-template-columns: repeat(2, minmax(0, 1fr)); } } +.lc-policy-recogn-tasks-toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; flex-wrap: wrap; gap: 8px; } +.lc-policy-recogn-task-progress { min-width: 108px; } +.lc-policy-recogn-task-progress .ant-progress { margin-bottom: 2px; line-height: 1; } +.lc-policy-recogn-task-progress-text { display: block; font-size: 11px; color: #64748b; font-variant-numeric: tabular-nums; text-align: center; } +.lc-policy-recogn-task-id { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: #334155; } +.lc-policy-recogn-preview { width: 100%; min-height: 360px; border: 1px solid #e2e8f0; border-radius: 8px; background: #f8fafc; display: flex; align-items: center; justify-content: center; } +.lc-policy-recogn-preview img { max-width: 100%; max-height: 100%; object-fit: contain; } +.lc-policy-recogn-preview iframe { width: 100%; height: 100%; border: none; border-radius: 8px; } +.lc-policy-recogn-confirm-split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; min-height: 480px; margin-top: 12px; } +@media (max-width: 1100px) { .lc-policy-recogn-confirm-split { grid-template-columns: 1fr; } } +.lc-policy-recogn-confirm-preview { border: 1px solid #e2e8f0; border-radius: 12px; background: #f8fafc; display: flex; flex-direction: column; min-height: 480px; overflow: hidden; } +.lc-policy-recogn-confirm-preview-head { padding: 10px 14px; border-bottom: 1px solid #e2e8f0; font-size: 12px; font-weight: 600; color: #64748b; background: #fff; } +.lc-policy-recogn-confirm-preview-body { flex: 1; display: flex; align-items: center; justify-content: center; padding: 12px; min-height: 0; } +.lc-policy-recogn-confirm-preview-body .lc-policy-recogn-preview { min-height: 420px; height: 100%; border: none; background: transparent; } +.lc-policy-recogn-confirm-form { border: 1px solid #e2e8f0; border-radius: 12px; background: #fff; padding: 14px 16px; max-height: 520px; overflow-y: auto; } +.lc-policy-recogn-confirm-form-title { font-size: 13px; font-weight: 700; color: #0f172a; margin-bottom: 12px; } +.lc-policy-recogn-picker { margin-bottom: 4px; } +.lc-policy-recogn-picker .ant-table-tbody > tr { cursor: pointer; } +.lc-policy-recogn-picker .ant-table-tbody > tr.lc-policy-recogn-picker-row--active > td { background: #eff6ff !important; } +.lc-policy-recogn-picker .ant-table-tbody > tr.lc-policy-recogn-picker-row--confirmed > td { opacity: 0.72; } +.lc-policy-field-label-required::after { content: '*'; color: #ef4444; margin-left: 2px; } +.lc-vehicle-ins-mgmt-modal .ant-modal-content { border-radius: 16px; overflow: hidden; box-shadow: 0 24px 64px -16px rgba(15, 23, 42, 0.28); } +.lc-vehicle-ins-mgmt-modal .ant-modal-header { display: none; } +.lc-vehicle-ins-mgmt-modal .ant-modal-body { padding: 0; max-height: calc(100vh - 96px); overflow: hidden; display: flex; flex-direction: column; } +.lc-vehicle-ins-mgmt-modal .ant-modal-footer { border-top: 1px solid #e2e8f0; padding: 12px 20px; background: #f8fafc; } +.lc-vehicle-ins-mgmt-shell { display: flex; flex-direction: column; min-height: 0; flex: 1; } +.lc-vehicle-ins-mgmt-hero { padding: 20px 24px 18px; background: linear-gradient(135deg, #ecfdf5 0%, #f8fafc 42%, #fff 100%); border-bottom: 1px solid #e2e8f0; } +.lc-vehicle-ins-mgmt-hero-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin-bottom: 16px; } +.lc-vehicle-ins-mgmt-hero-title { font-size: 11px; font-weight: 700; color: #059669; letter-spacing: 0.06em; text-transform: uppercase; margin-bottom: 6px; } +.lc-vehicle-ins-mgmt-plate { font-size: 26px; font-weight: 800; color: #0f172a; letter-spacing: 0.02em; line-height: 1.2; } +.lc-vehicle-ins-mgmt-subtitle { font-size: 13px; color: #64748b; margin-top: 6px; } +.lc-vehicle-ins-mgmt-status-pill { display: inline-flex; align-items: center; gap: 6px; padding: 6px 14px; border-radius: 999px; font-size: 13px; font-weight: 700; border: 1px solid transparent; } +.lc-vehicle-ins-mgmt-status-pill--success { background: #ecfdf5; border-color: #a7f3d0; color: #047857; } +.lc-vehicle-ins-mgmt-status-pill--warning { background: #fffbeb; border-color: #fde68a; color: #b45309; } +.lc-vehicle-ins-mgmt-status-pill--error { background: #fef2f2; border-color: #fecaca; color: #b91c1c; } +.lc-vehicle-ins-mgmt-meta-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px 14px; } +@media (max-width: 768px) { .lc-vehicle-ins-mgmt-meta-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } +.lc-vehicle-ins-mgmt-meta-card { padding: 10px 12px; border-radius: 10px; background: rgba(255,255,255,.85); border: 1px solid #e2e8f0; min-width: 0; } +.lc-vehicle-ins-mgmt-meta-label { font-size: 11px; color: #94a3b8; font-weight: 600; margin-bottom: 4px; } +.lc-vehicle-ins-mgmt-meta-val { font-size: 13px; font-weight: 600; color: #0f172a; word-break: break-all; line-height: 1.35; } +.lc-vehicle-ins-mgmt-body { padding: 16px 20px 20px; overflow-y: auto; flex: 1; min-height: 0; } +.lc-vehicle-ins-mgmt-filter { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; padding: 12px 14px; border-radius: 10px; background: #f8fafc; border: 1px solid #e2e8f0; } +.lc-vehicle-ins-mgmt-filter .lc-filter-field { flex: 1; min-width: 0; margin: 0; } +.lc-vehicle-ins-mgmt-filter .lc-filter-field-label { flex: 0 0 64px; } +.lc-vehicle-ins-policy-more-btn { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 6px; color: #64748b; cursor: pointer; transition: background 0.15s, color 0.15s; } +.lc-vehicle-ins-policy-more-btn:hover { background: #f1f5f9; color: #334155; } +.lc-policy-biz-summary { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px 16px; padding: 14px 16px; margin-bottom: 16px; border-radius: 12px; background: #f8fafc; border: 1px solid #e2e8f0; } +.lc-policy-biz-summary-item-label { font-size: 11px; color: #94a3b8; font-weight: 600; margin-bottom: 4px; } +.lc-policy-biz-summary-item-val { font-size: 13px; color: #0f172a; font-weight: 600; word-break: break-all; } +.lc-policy-biz-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 16px; } +.lc-policy-biz-form-full { grid-column: 1 / -1; } +@media (max-width: 640px) { .lc-policy-biz-summary, .lc-policy-biz-form { grid-template-columns: 1fr; } } +.lc-purchase-type-chip { display: inline-flex; align-items: center; padding: 2px 10px; border-radius: 999px; font-size: 11px; font-weight: 700; border: 1px solid transparent; line-height: 1.5; } +.lc-purchase-type--new { background: #ecfdf5; border-color: #a7f3d0; color: #047857; } +.lc-purchase-type--renew { background: #eff6ff; border-color: #bfdbfe; color: #1d4ed8; } +.lc-purchase-type--rent-stop { background: #fff7ed; border-color: #fed7aa; color: #c2410c; } +.lc-purchase-type--resume { background: #ecfeff; border-color: #a5f3fc; color: #0e7490; } +.lc-purchase-type--cancel { background: #f1f5f9; border-color: #cbd5e1; color: #475569; } +.lc-vehicle-ins-mgmt-tabs .ant-tabs-nav { margin-bottom: 0 !important; padding: 0 4px; } +.lc-vehicle-ins-mgmt-tabs .ant-tabs-tab { font-weight: 600; font-size: 13px; padding: 10px 14px !important; transition: color 0.15s; } +.lc-vehicle-ins-mgmt-tabs .ant-tabs-tab-active .ant-tabs-tab-btn { color: #059669 !important; } +.lc-vehicle-ins-mgmt-tabs .ant-tabs-ink-bar { background: #10b981 !important; height: 3px !important; border-radius: 3px 3px 0 0; } +.lc-vehicle-ins-mgmt-tabs .ant-tabs-content-holder { padding-top: 14px; } +.lc-ins-tab-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 18px; height: 18px; padding: 0 5px; margin-left: 6px; border-radius: 999px; background: #e2e8f0; color: #475569; font-size: 11px; font-weight: 700; } +.lc-vehicle-ins-mgmt-tabs .ant-tabs-tab-active .lc-ins-tab-badge { background: #d1fae5; color: #047857; } +.lc-vehicle-ins-timeline-center { max-height: 480px; overflow-y: auto; padding: 4px 8px 12px; } +.lc-vehicle-ins-timeline-center-head { display: grid; grid-template-columns: minmax(0, 1fr) 28px minmax(0, 1fr); gap: 12px; align-items: center; margin-bottom: 12px; padding: 0 4px; } +.lc-vehicle-ins-timeline-center-head-side { font-size: 13px; font-weight: 700; color: #334155; } +.lc-vehicle-ins-timeline-center-head-side--left { text-align: right; } +.lc-vehicle-ins-timeline-center-head-side--right { text-align: left; } +.lc-vehicle-ins-timeline-center-head-axis { width: 2px; height: 18px; margin: 0 auto; border-radius: 2px; background: linear-gradient(180deg, #10b981, #94a3b8); } +.lc-vehicle-ins-timeline-center-body { position: relative; } +.lc-vehicle-ins-timeline-center-row { display: grid; grid-template-columns: minmax(0, 1fr) 28px minmax(0, 1fr); gap: 12px; align-items: stretch; min-height: 72px; } +.lc-vehicle-ins-timeline-center-col { display: flex; min-width: 0; } +.lc-vehicle-ins-timeline-center-col--left { justify-content: flex-end; } +.lc-vehicle-ins-timeline-center-col--right { justify-content: flex-start; } +.lc-vehicle-ins-timeline-center-axis { position: relative; display: flex; justify-content: center; padding-top: 18px; } +.lc-vehicle-ins-timeline-center-axis::before { content: ''; position: absolute; top: 0; bottom: 0; left: 50%; width: 2px; margin-left: -1px; background: #e2e8f0; } +.lc-vehicle-ins-timeline-center-row:first-child .lc-vehicle-ins-timeline-center-axis::before { top: 22px; } +.lc-vehicle-ins-timeline-center-row:last-child .lc-vehicle-ins-timeline-center-axis::before { bottom: auto; height: 22px; } +.lc-vehicle-ins-timeline-center-dot { position: relative; z-index: 1; width: 12px; height: 12px; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 0 0 2px currentColor; flex-shrink: 0; } +.lc-vehicle-ins-timeline-center-dot--policy { color: #10b981; background: #10b981; } +.lc-vehicle-ins-timeline-center-dot--biz { color: #f59e0b; background: #f59e0b; } +.lc-vehicle-ins-timeline-item--left { text-align: right; } +.lc-vehicle-ins-timeline-item--left .lc-vehicle-ins-timeline-title { justify-content: flex-end; } +.lc-vehicle-ins-timeline-item--left .lc-vehicle-ins-timeline-meta { text-align: right; } +@media (max-width: 720px) { + .lc-vehicle-ins-timeline-center-head { grid-template-columns: 1fr; gap: 4px; text-align: center !important; } + .lc-vehicle-ins-timeline-center-head-axis { display: none; } + .lc-vehicle-ins-timeline-center-row { grid-template-columns: 1fr; gap: 8px; min-height: auto; padding-left: 20px; border-left: 2px solid #e2e8f0; margin-left: 8px; } + .lc-vehicle-ins-timeline-center-axis { display: none; } + .lc-vehicle-ins-timeline-center-col--left { justify-content: flex-start; } + .lc-vehicle-ins-timeline-item--left { text-align: left; } + .lc-vehicle-ins-timeline-item--left .lc-vehicle-ins-timeline-title { justify-content: flex-start; } + .lc-vehicle-ins-timeline-item--left .lc-vehicle-ins-timeline-meta { text-align: left; } +} +.lc-vehicle-ins-timeline-center-col .lc-vehicle-ins-timeline-item { width: 100%; max-width: 360px; margin: 0 0 14px; } +.lc-vehicle-ins-timeline-item { cursor: pointer; padding: 12px 14px; margin: 0 0 10px; border-radius: 12px; border: 1px solid #e2e8f0; background: #fff; transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s; } +.lc-vehicle-ins-timeline-item:hover { border-color: #a7f3d0; box-shadow: 0 4px 14px -6px rgba(16, 185, 129, 0.35); transform: translateY(-1px); } +.lc-vehicle-ins-timeline-item:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; } +.lc-vehicle-ins-timeline-title { font-size: 13px; font-weight: 700; color: #0f172a; margin-bottom: 6px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } +.lc-vehicle-ins-timeline-desc { font-size: 12px; color: #475569; line-height: 1.55; } +.lc-vehicle-ins-timeline-meta { font-size: 11px; color: #94a3b8; margin-top: 6px; } +.lc-vehicle-ins-mgmt-table-card { border-radius: 12px; border: 1px solid #e2e8f0; overflow: hidden; background: #fff; } +.lc-vehicle-ins-mgmt-table-card .ant-table-thead > tr > th { background: #f8fafc !important; font-weight: 700 !important; font-size: 12px !important; color: #475569 !important; white-space: nowrap; } +.lc-vehicle-ins-mgmt-table .ant-table-content table { table-layout: fixed; width: 100% !important; } +.lc-vehicle-ins-mgmt-table .ant-table-tbody > tr.ant-table-measure-row { visibility: collapse !important; height: 0 !important; font-size: 0 !important; line-height: 0 !important; } +.lc-vehicle-ins-mgmt-table .ant-table-tbody > tr.ant-table-measure-row > td { padding: 0 !important; border: none !important; height: 0 !important; } +.lc-vehicle-ins-mgmt-table .ant-table-tbody > tr:not(.ant-table-measure-row) > td { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; vertical-align: middle; padding-top: 8px !important; padding-bottom: 8px !important; } +.lc-vehicle-ins-mgmt-table .ant-table-tbody > tr > td .ant-table-cell-content { overflow: hidden; text-overflow: ellipsis; } +.lc-vehicle-ins-mgmt-table .lc-vehicle-ins-mgmt-actions { display: inline-flex; align-items: center; flex-wrap: nowrap; white-space: nowrap; gap: 2px; max-width: 100%; } +.lc-vehicle-ins-mgmt-table .lc-vehicle-ins-mgmt-actions .ant-btn-link { flex-shrink: 0; white-space: nowrap; } +.lc-vehicle-ins-mgmt-table .lc-vehicle-ins-mgmt-cell-ellipsis { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; } +.lc-vehicle-ins-mgmt-empty { padding: 48px 24px; text-align: center; border-radius: 12px; border: 1px dashed #cbd5e1; background: #f8fafc; } +.lc-ins-history-row--active > td { background: #ecfdf5 !important; } +.lc-ins-history-row--active > td:first-child { box-shadow: inset 3px 0 0 #10b981; } +.lc-policy-detail-form { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 16px; } +.lc-policy-detail-form--suspend-confirm { grid-template-columns: 1fr; max-width: 400px; } +@media (max-width: 720px) { .lc-policy-detail-form { grid-template-columns: 1fr; } } +.lc-policy-detail-form-full { grid-column: 1 / -1; } +.lc-policy-detail-section-title { font-size: 13px; font-weight: 700; color: #334155; margin: 4px 0 8px; grid-column: 1 / -1; } +.lc-coverage-items-table-section { grid-column: 1 / -1; } +.lc-coverage-items-table-section--confirm { margin-top: 4px; } +.lc-coverage-items-table-wrap { border-radius: 10px; border: 1px solid #e2e8f0; overflow: hidden; background: #fff; } +.lc-coverage-items-table .ant-table-thead > tr > th { background: #f8fafc !important; font-size: 12px !important; font-weight: 700 !important; padding: 8px 10px !important; } +.lc-coverage-items-table .ant-table-tbody > tr > td { padding: 6px 8px !important; vertical-align: middle !important; } +.lc-coverage-items-add { margin-top: 8px; border-radius: 8px !important; font-weight: 600; color: #059669 !important; border-color: #a7f3d0 !important; background: #f0fdf4 !important; } +.lc-coverage-items-add:hover { border-color: #6ee7b7 !important; color: #047857 !important; } +.lc-list-policy-tag { margin-top: 4px; } +.lc-compare-attach-field { display: flex; flex-direction: column; gap: 8px; } +.lc-compare-attach-label { font-size: 13px; font-weight: 600; color: #334155; } +.lc-compare-attach-hint { font-size: 12px; color: #94a3b8; line-height: 1.5; } +.lc-compare-attach-upload .ant-upload-list { max-height: 160px; overflow-y: auto; margin-top: 8px; } +.lc-compare-attach-upload .ant-upload-list-item { border-radius: 8px; } +.lc-compare-attach-upload .ant-upload-select { display: block; } +`; + +const goInsuranceEditPage = (ledgerKey, master, vehicle) => { + const label = vehicle + ? (hasVehiclePlate(vehicle) ? vehicle.plateNo : `${NO_PLATE_LABEL}(${vehicle.vin})`) + : ledgerKey; + try { + sessionStorage.setItem(IPC_EDIT_PLATE_KEY, ledgerKey); + persistInsuranceToStorage(master); + } catch { + /* ignore */ + } + if (typeof window.__axhubNavigate === 'function') { + window.__axhubNavigate('保险采购-编辑'); + message.success(`已进入 [${label}] 保险维护`); + return; + } + message.info(`已带入 [${label}] 车辆信息,请打开「保险采购-编辑」页面继续维护`); +}; + +const Component = function () { + const [allInsurance, setAllInsurance] = useState(() => { + const stored = loadInsuranceFromStorage(); + if (stored) { + const normalized = {}; + Object.keys(stored).forEach((k) => { + normalized[k] = ensureInsuranceRecordShape(stored[k]); + }); + MOCK_VEHICLES.forEach((v) => { + const key = getVehicleLedgerKey(v); + if (normalized[key]) return; + const seed = getInitialInsuranceSeed(v); + if (seed) { + normalized[key] = ensureInsuranceRecordShape(JSON.parse(JSON.stringify(seed))); + } + }); + return normalized; + } + const merged = {}; + MOCK_VEHICLES.forEach((v) => { + const key = getVehicleLedgerKey(v); + const seed = getInitialInsuranceSeed(v); + merged[key] = ensureInsuranceRecordShape( + seed ? JSON.parse(JSON.stringify(seed)) : createEmptyInsuranceRecord() + ); + }); + return merged; + }); + + const updateAllInsurance = useCallback((updater) => { + setAllInsurance((prev) => { + const next = typeof updater === 'function' ? updater(prev) : updater; + persistInsuranceToStorage(next); + return next; + }); + }, []); + + const DEFAULT_LIST_FILTERS = { + plateNo: '', + plateNos: '', + vin: '', + brand: '', + model: '', + operateStatus: '全部', + insuranceStatus: '全部', + insuranceType: '', + endDateRange: null, + }; + const [listFilters, setListFilters] = useState(() => ({ ...DEFAULT_LIST_FILTERS })); + const [appliedFilters, setAppliedFilters] = useState(() => ({ ...DEFAULT_LIST_FILTERS })); + const [multiPlateOpen, setMultiPlateOpen] = useState(false); + const [multiPlateDraft, setMultiPlateDraft] = useState(''); + const [kpiFilter, setKpiFilter] = useState('total'); + const [prdOpen, setPrdOpen] = useState(false); + const [compareMgmtOpen, setCompareMgmtOpen] = useState(false); + const [compareSheets, setCompareSheets] = useState(() => { + const stored = loadCompareSheetsFromStorage(); + const list = stored && stored.length ? stored : createMockCompareSheets(); + return list.map(normalizeCompareSheet); + }); + const [compareMgmtFilters, setCompareMgmtFilters] = useState(() => ({ ...DEFAULT_COMPARE_MGMT_FILTERS })); + const [appliedCompareMgmtFilters, setAppliedCompareMgmtFilters] = useState(() => ({ ...DEFAULT_COMPARE_MGMT_FILTERS })); + const [compareModalOpen, setCompareModalOpen] = useState(false); + const [editingCompareSheetId, setEditingCompareSheetId] = useState(null); + const [compareRows, setCompareRows] = useState([]); + const [selectedCompareKeys, setSelectedCompareKeys] = useState([]); + const [copyPopoverRowId, setCopyPopoverRowId] = useState(null); + const [copyCountDraft, setCopyCountDraft] = useState(1); + const [quoteDraft, setQuoteDraft] = useState(createEmptyQuoteDraft); + const [quoteEditRowId, setQuoteEditRowId] = useState(null); + const [compareRemark, setCompareRemark] = useState(''); + const [compareEditorFilters, setCompareEditorFilters] = useState(() => ({ ...DEFAULT_COMPARE_EDITOR_FILTERS })); + const [compareAttachmentFileList, setCompareAttachmentFileList] = useState([]); + const [policyRecognOpen, setPolicyRecognOpen] = useState(false); + const [policyRecognEntry, setPolicyRecognEntry] = useState('ocr'); + const [policyRecognMode, setPolicyRecognMode] = useState('policy'); + const [policyRecognInsuranceType, setPolicyRecognInsuranceType] = useState('交强险'); + const [policyRecognPhase, setPolicyRecognPhase] = useState('upload'); + const [policyRecognFiles, setPolicyRecognFiles] = useState([]); + const [policyRecognTaskId, setPolicyRecognTaskId] = useState(''); + const policyRecognTimerRef = useRef(null); + const policyRecognProgressTimerRef = useRef(null); + const [policyRecognResults, setPolicyRecognResults] = useState([]); + const [policyRecognViewOnly, setPolicyRecognViewOnly] = useState(false); + const [policyRecognActiveResultId, setPolicyRecognActiveResultId] = useState(''); + const [policyRecognConfirmDraft, setPolicyRecognConfirmDraft] = useState(() => ({ ...EMPTY_POLICY_DETAIL })); + const [policyPreview, setPolicyPreview] = useState(null); + const [policyRecognTasks, setPolicyRecognTasks] = useState(() => { + const stored = loadPolicyRecognTasksFromStorage(); + return stored && stored.length ? stored : createMockPolicyRecognTasks(); + }); + const [policyRecognTasksOpen, setPolicyRecognTasksOpen] = useState(false); + const [policyRecognTasksFilters, setPolicyRecognTasksFilters] = useState(() => ({ ...DEFAULT_POLICY_RECOGN_TASK_FILTERS })); + const [appliedPolicyRecognTasksFilters, setAppliedPolicyRecognTasksFilters] = useState(() => ({ ...DEFAULT_POLICY_RECOGN_TASK_FILTERS })); + const [policyAddOpen, setPolicyAddOpen] = useState(false); + const [vehicleInsMgmtOpen, setVehicleInsMgmtOpen] = useState(false); + const [vehicleInsMgmtVehicle, setVehicleInsMgmtVehicle] = useState(null); + const [vehicleInsMgmtActiveTab, setVehicleInsMgmtActiveTab] = useState('timeline'); + const [vehicleInsMgmtHighlightId, setVehicleInsMgmtHighlightId] = useState(''); + const [vehicleInsMgmtPolicyNoFilter, setVehicleInsMgmtPolicyNoFilter] = useState(''); + const [vehicleInsMgmtTabPage, setVehicleInsMgmtTabPage] = useState({}); + const [policyBizModalOpen, setPolicyBizModalOpen] = useState(false); + const [policyBizModalMode, setPolicyBizModalMode] = useState('suspend'); + const [policyBizModalRecord, setPolicyBizModalRecord] = useState(null); + const [policyBizForm, setPolicyBizForm] = useState(() => ({ ...EMPTY_POLICY_BIZ_FORM })); + const [policyBizAttachmentFileList, setPolicyBizAttachmentFileList] = useState([]); + const [policyOpHistoryOpen, setPolicyOpHistoryOpen] = useState(false); + const [policyOpHistoryRecord, setPolicyOpHistoryRecord] = useState(null); + const [policyAddDraft, setPolicyAddDraft] = useState(() => ({ ...EMPTY_POLICY_DETAIL })); + const [policyAddAttachmentFileList, setPolicyAddAttachmentFileList] = useState([]); + const [insuranceHistoryEdits, setInsuranceHistoryEdits] = useState(() => loadInsuranceHistoryEditsFromStorage()); + const [vehicleInsHistoryEditOpen, setVehicleInsHistoryEditOpen] = useState(false); + const [vehicleInsHistoryEditRecord, setVehicleInsHistoryEditRecord] = useState(null); + const [vehicleInsHistoryEditDraft, setVehicleInsHistoryEditDraft] = useState(() => ({ ...EMPTY_POLICY_DETAIL })); + const [insuranceAlertOpen, setInsuranceAlertOpen] = useState(false); + const [insuranceAlertMode, setInsuranceAlertMode] = useState('expiring'); + const [insuranceAlertTypeFilter, setInsuranceAlertTypeFilter] = useState(() => [...EXPIRING_WARN_TYPE_KEYS]); + const [insuranceAlertSort, setInsuranceAlertSort] = useState({ key: 'commercial', order: 'descend' }); + const [batchCompareTypesOpen, setBatchCompareTypesOpen] = useState(false); + const [batchCompareTypesDraft, setBatchCompareTypesDraft] = useState(() => [...QUOTE_INSURANCE_TYPES]); + const [compareVehicleFilterOpen, setCompareVehicleFilterOpen] = useState(false); + const [compareVehicleFilterDraft, setCompareVehicleFilterDraft] = useState(''); + const [compareBatchAddOpen, setCompareBatchAddOpen] = useState(false); + const [compareBatchAddDraft, setCompareBatchAddDraft] = useState(''); + + const compareSheetSummary = useMemo( + () => calcCompareSheetConfirmedTotal(compareRows), + [compareRows] + ); + + const selectedProcurementSummary = useMemo(() => { + const selected = compareRows.filter((r) => selectedCompareKeys.includes(r.id)); + return calcCompareSheetConfirmedTotal(selected); + }, [compareRows, selectedCompareKeys]); + + const compareEditorTypeCounts = useMemo( + () => countCompareRowsByInsuranceType(compareRows), + [compareRows] + ); + + const displayCompareRows = useMemo( + () => filterCompareEditorRows(compareRows, compareEditorFilters), + [compareRows, compareEditorFilters] + ); + + const appliedCompareVehicles = useMemo( + () => parseMultiPlates(compareEditorFilters.vehicles), + [compareEditorFilters.vehicles] + ); + + const compareVehicleTriggerText = appliedCompareVehicles.length + ? `已选 ${appliedCompareVehicles.length} 辆车` + : ''; + + const isCompareEditorFiltered = useMemo(() => { + const f = compareEditorFilters; + return !!(f.vehicles || '').trim() || f.latestPayWithin3Days || f.insuranceType; + }, [compareEditorFilters]); + + const compareMgmtBaseFilteredSheets = useMemo(() => { + const plateKey = (appliedCompareMgmtFilters.plateNo || '').trim(); + const range = appliedCompareMgmtFilters.createdRange; + return compareSheets + .filter((sheet) => compareSheetMatchesCreatedRange(sheet.createdAt, range)) + .filter((sheet) => compareSheetMatchesPlateFilter(sheet, plateKey)); + }, [compareSheets, appliedCompareMgmtFilters.plateNo, appliedCompareMgmtFilters.createdRange]); + + const compareMgmtPayAlerts = useMemo(() => { + let warningSheets = 0; + let overdueSheets = 0; + compareMgmtBaseFilteredSheets.forEach((sheet) => { + const alert = calcCompareSheetPayAlerts(sheet); + if (alert.warning > 0) warningSheets += 1; + if (alert.overdue > 0) overdueSheets += 1; + }); + return { + total: compareMgmtBaseFilteredSheets.length, + warning: warningSheets, + overdue: overdueSheets, + }; + }, [compareMgmtBaseFilteredSheets]); + + const saveCompareSheets = useCallback((nextSheets) => { + setCompareSheets(nextSheets); + persistCompareSheetsToStorage(nextSheets); + }, []); + + const filteredCompareSheets = useMemo(() => ( + compareMgmtBaseFilteredSheets + .filter((sheet) => compareSheetMatchesPayAlertFilter(sheet, appliedCompareMgmtFilters.payAlertFilter)) + .sort((a, b) => String(b.createdAt || '').localeCompare(String(a.createdAt || ''))) + ), [compareMgmtBaseFilteredSheets, appliedCompareMgmtFilters.payAlertFilter]); + + const handleCompareMgmtPayAlertFilter = (payAlertFilter) => { + setCompareMgmtFilters((prev) => ({ ...prev, payAlertFilter })); + setAppliedCompareMgmtFilters((prev) => ({ ...prev, payAlertFilter })); + }; + + const updateCompareRow = useCallback((rowId, patch) => { + setCompareRows((prev) => prev.map((r) => (r.id === rowId ? { ...r, ...patch } : r))); + }, []); + + const fillCompareRowFromVehicle = useCallback((rowId, vehicle) => { + if (!vehicle) return; + updateCompareRow(rowId, buildVehicleComparePatch(vehicle, allInsurance)); + }, [allInsurance, updateCompareRow]); + + const fillCompareRowFromPlate = useCallback((rowId, plateNo) => { + const vehicle = findVehicleByPlate(plateNo); + if (!vehicle) return; + fillCompareRowFromVehicle(rowId, vehicle); + }, [fillCompareRowFromVehicle]); + + const handleComparePlateChange = useCallback((rowId, plateNo) => { + if (!plateNo) { + updateCompareRow(rowId, clearVehicleComparePatch()); + return; + } + fillCompareRowFromPlate(rowId, plateNo); + }, [fillCompareRowFromPlate, updateCompareRow]); + + const handleCompareVinChange = useCallback((rowId, vin) => { + if (!vin) { + updateCompareRow(rowId, clearVehicleComparePatch()); + return; + } + const vehicle = findVehicleByVin(vin); + if (!vehicle) { + message.warning('未找到该 VIN 对应车辆'); + return; + } + fillCompareRowFromVehicle(rowId, vehicle); + }, [fillCompareRowFromVehicle, updateCompareRow]); + + const renderReadonlyField = (val, linked) => ( + + {val || '—'} + + ); + + const renderReadonlyDate = (val, linked) => ( + + {val || '—'} + + ); + + const openCompareMgmtModal = () => { + setCompareMgmtFilters({ ...DEFAULT_COMPARE_MGMT_FILTERS }); + setAppliedCompareMgmtFilters({ ...DEFAULT_COMPARE_MGMT_FILTERS }); + setCompareMgmtOpen(true); + }; + + const openCompareEditor = (sheet) => { + if (sheet) { + setEditingCompareSheetId(sheet.id); + setCompareRows(normalizeCompareRows(JSON.parse(JSON.stringify(sheet.rows || [])))); + setCompareRemark(sheet.remark || ''); + setCompareAttachmentFileList(attachmentsToUploadFileList(sheet.attachments)); + } else { + setEditingCompareSheetId(null); + setCompareRows([createEmptyCompareRow()]); + setCompareRemark(''); + setCompareAttachmentFileList([]); + } + setCompareEditorFilters({ ...DEFAULT_COMPARE_EDITOR_FILTERS }); + setCompareVehicleFilterOpen(false); + setCompareVehicleFilterDraft(''); + setCompareBatchAddOpen(false); + setCompareBatchAddDraft(''); + setSelectedCompareKeys([]); + setQuoteDraft(createEmptyQuoteDraft()); + setQuoteEditRowId(null); + setCompareModalOpen(true); + }; + + const handleCompareVehicleFilterOpenChange = (open) => { + setCompareVehicleFilterOpen(open); + if (open) setCompareVehicleFilterDraft(compareEditorFilters.vehicles || ''); + }; + + const handleCompareVehicleFilterClear = () => { + setCompareVehicleFilterDraft(''); + setCompareEditorFilters((prev) => ({ ...prev, vehicles: '' })); + setCompareVehicleFilterOpen(false); + }; + + const handleCompareVehicleFilterApply = () => { + const trimmed = compareVehicleFilterDraft.trim(); + setCompareEditorFilters((prev) => ({ ...prev, vehicles: trimmed })); + setCompareVehicleFilterOpen(false); + const tokens = parseMultiPlates(trimmed); + if (tokens.length) { + const hitCount = filterCompareEditorRows(compareRows, { + ...compareEditorFilters, + vehicles: trimmed, + }).length; + message.success(`已按 ${tokens.length} 辆车筛选,命中 ${hitCount} 条购买记录`); + } + }; + + const buildSheetPayloadFromEditor = (rowsSnapshot) => normalizeCompareSheet({ + id: editingCompareSheetId || createCompareSheetId(), + createdAt: editingCompareSheetId + ? (compareSheets.find((s) => s.id === editingCompareSheetId)?.createdAt || formatCompareSheetNow()) + : formatCompareSheetNow(), + createdBy: editingCompareSheetId + ? (compareSheets.find((s) => s.id === editingCompareSheetId)?.createdBy || PROTO_COMPARE_CREATOR) + : PROTO_COMPARE_CREATOR, + periodLabel: editingCompareSheetId + ? (compareSheets.find((s) => s.id === editingCompareSheetId)?.periodLabel || '') + : '', + remark: compareRemark, + attachments: uploadFileListToAttachments(compareAttachmentFileList), + rows: rowsSnapshot, + }); + + const handleCompareAttachmentChange = ({ fileList }) => { + setCompareAttachmentFileList( + fileList.map((f) => ({ + ...f, + status: 'done', + uploadedAt: f.uploadedAt || formatCompareSheetNow(), + })) + ); + }; + + const handleCompareMgmtQuery = () => { + setAppliedCompareMgmtFilters({ ...compareMgmtFilters }); + message.success('查询完成'); + }; + + const handleCompareMgmtReset = () => { + setCompareMgmtFilters({ ...DEFAULT_COMPARE_MGMT_FILTERS }); + setAppliedCompareMgmtFilters({ ...DEFAULT_COMPARE_MGMT_FILTERS }); + message.info('筛选条件已重置'); + }; + + const handleDeleteCompareSheet = (sheet) => { + Modal.confirm({ + title: '删除比价单', + content: `确定删除 ${sheet.createdAt || ''} 由 ${sheet.createdBy || '—'} 创建的比价单吗?删除后不可恢复。`, + okText: '删除', + okType: 'danger', + cancelText: '取消', + centered: true, + onOk: () => { + const next = compareSheets.filter((s) => s.id !== sheet.id); + saveCompareSheets(next); + message.success('比价单已删除'); + }, + }); + }; + + const handleAddCompareRow = () => { + setCompareRows((prev) => [...prev, createEmptyCompareRow()]); + }; + + const handleOpenBatchAddCompareRows = () => { + setCompareBatchAddDraft(''); + setCompareBatchAddOpen(true); + }; + + const handleConfirmBatchAddCompareRows = () => { + const lines = parseBatchVehicleLines(compareBatchAddDraft); + if (!lines.length) { + message.warning('请输入至少一条车牌号或车辆识别代码'); + return; + } + const notFound = []; + const newRows = lines.map((token) => { + const vehicle = findVehicleByPlateOrVin(token); + if (!vehicle) { + notFound.push(token); + return null; + } + const row = buildCompareRowFromVehicle(vehicle, allInsurance); + if (compareEditorFilters.insuranceType) { + row.insuranceType = compareEditorFilters.insuranceType; + } + return row; + }).filter(Boolean); + if (!newRows.length) { + message.warning( + notFound.length + ? `未找到匹配车辆:${notFound.slice(0, 5).join('、')}${notFound.length > 5 ? ` 等 ${notFound.length} 条` : ''}` + : '没有可新增的记录' + ); + return; + } + setCompareRows((prev) => [...prev, ...newRows]); + setCompareBatchAddOpen(false); + setCompareBatchAddDraft(''); + const skipHint = notFound.length ? `,${notFound.length} 条未匹配已跳过` : ''; + message.success(`已批量新增 ${newRows.length} 条购买记录${skipHint}`); + }; + + const handleBatchSetCompareInsuranceType = (insuranceType) => { + const targetIdSet = new Set( + selectedCompareKeys.length + ? selectedCompareKeys + : compareRows.map((r) => r.id) + ); + if (!targetIdSet.size) { + message.warning('没有可设置的购买记录'); + return; + } + let updated = 0; + let skippedProcurement = 0; + setCompareRows((prev) => prev.map((r) => { + if (!targetIdSet.has(r.id)) return r; + if (isCompareProcurementSelectionDisabled(r.procurementStatus)) { + skippedProcurement += 1; + return r; + } + if (r.insuranceType === insuranceType) return r; + updated += 1; + return { + ...r, + insuranceType, + quotes: [], + confirmedQuoteId: '', + }; + })); + if (!updated) { + message.info( + skippedProcurement + ? '所选记录均为审批中/审批通过,或险种已是目标值' + : '没有需要变更的记录' + ); + return; + } + const scopeLabel = selectedCompareKeys.length ? '所选' : '全部'; + const skipHint = skippedProcurement ? `,${skippedProcurement} 条审批中/通过记录已跳过` : ''; + message.success(`已将${scopeLabel} ${updated} 条记录设为${insuranceType}${skipHint}`); + }; + + const handleDeleteCompareRow = (rowId) => { + setCompareRows((prev) => prev.filter((r) => r.id !== rowId)); + setSelectedCompareKeys((prev) => prev.filter((k) => k !== rowId)); + }; + + const handleCopyCompareRow = (row, count) => { + const n = Math.max(1, Math.min(50, Number(count) || 1)); + const clones = Array.from({ length: n }, () => cloneCompareRow(row)); + setCompareRows((prev) => { + const idx = prev.findIndex((r) => r.id === row.id); + if (idx < 0) return [...prev, ...clones]; + const next = [...prev]; + next.splice(idx + 1, 0, ...clones); + return next; + }); + setCopyPopoverRowId(null); + setCopyCountDraft(1); + message.success(`已复制 ${n} 条记录`); + }; + + const handleAddQuote = (rowId, rowInsuranceType) => { + if (!quoteDraft.company) { + message.warning('请选择保险公司'); + return; + } + if (!isValidPremium(quoteDraft.premium)) { + message.warning('请输入大于 0 的报价,最多两位小数'); + return; + } + const premium = formatPremiumDisplay(quoteDraft.premium); + const newQuote = { + id: createQuoteId(), + company: quoteDraft.company, + premium, + }; + let added = false; + setCompareRows((prev) => prev.map((r) => { + if (r.id !== rowId) return r; + const exists = (r.quotes || []).some((q) => q.company === quoteDraft.company); + if (exists) { + message.warning('该保险公司报价已存在'); + return r; + } + added = true; + return { ...r, quotes: [...(r.quotes || []), newQuote] }; + })); + if (added) { + setQuoteDraft(createEmptyQuoteDraft()); + message.success(`已添加${rowInsuranceType || ''}报价`); + } + }; + + const handleRemoveQuote = (rowId, quoteId) => { + setCompareRows((prev) => prev.map((r) => { + if (r.id !== rowId) return r; + const quotes = (r.quotes || []).filter((q) => q.id !== quoteId); + const confirmedQuoteId = r.confirmedQuoteId === quoteId ? '' : r.confirmedQuoteId; + return { ...r, quotes, confirmedQuoteId }; + })); + }; + + const validateCompareSheetRequiredMeta = () => { + if (!(compareRemark || '').trim()) { + message.warning('请填写备注'); + return false; + } + const hasAttachment = (compareAttachmentFileList || []).some((f) => f.status !== 'removed'); + if (!hasAttachment) { + message.warning('请上传附件'); + return false; + } + return true; + }; + + const handleSubmitCompareSheet = (options = {}) => { + const { closeModal = true } = options; + if (!compareRows.length) { + message.warning('请至少添加一条购买记录'); + return null; + } + const missingVehicle = compareRows.find((r) => !(r.plateNo || '').trim() && !(r.vin || '').trim()); + if (missingVehicle) { + message.warning('存在未选择车辆的记录,请填写车牌或 VIN'); + return null; + } + if (!validateCompareSheetRequiredMeta()) return null; + const rowsSnapshot = normalizeCompareRows(JSON.parse(JSON.stringify(compareRows))); + const payload = buildSheetPayloadFromEditor(rowsSnapshot); + let nextSheets; + let savedId = payload.id; + if (editingCompareSheetId) { + nextSheets = compareSheets.map((s) => (s.id === editingCompareSheetId ? payload : s)); + message.success(`比价单已保存,共 ${rowsSnapshot.length} 条购买记录`); + } else { + nextSheets = [payload, ...compareSheets]; + message.success(`比价单已创建,共 ${rowsSnapshot.length} 条购买记录`); + savedId = payload.id; + } + saveCompareSheets(nextSheets); + setEditingCompareSheetId(savedId); + if (closeModal) { + setCompareModalOpen(false); + } + return savedId; + }; + + const submitProcurementApplication = (keys = selectedCompareKeys) => { + if (!keys.length) return; + const submittedAt = formatCompareSheetNow(); + const rowsSnapshot = compareRows.map((r) => ( + keys.includes(r.id) + ? { + ...r, + procurementStatus: 'submitted', + procurementSubmittedAt: submittedAt, + procurementCurrentApprover: pickMockWorkflowCurrentApprover(r.id), + } + : r + )); + const payload = buildSheetPayloadFromEditor(rowsSnapshot); + const nextSheets = compareSheets.map((s) => (s.id === payload.id ? payload : s)); + saveCompareSheets(nextSheets); + setCompareRows(rowsSnapshot); + setSelectedCompareKeys([]); + message.success('比价单审批流程提交成功'); + }; + + const handleSubmitProcurement = () => { + if (!selectedCompareKeys.length) { + message.warning('请勾选需要提交采购的购买记录'); + return; + } + const selectedRows = compareRows.filter((r) => selectedCompareKeys.includes(r.id)); + const noQuotes = selectedRows.find((r) => !(r.quotes || []).length); + if (noQuotes) { + message.warning('勾选记录须新增保险报价(报价情况为必填)'); + return; + } + const noConfirmed = selectedRows.find((r) => !r.confirmedQuoteId); + if (noConfirmed) { + message.warning('勾选记录须将报价设为最终比价结果后方可提交'); + return; + } + const noPayDate = selectedRows.find((r) => !r.latestPayDate); + if (noPayDate) { + message.warning('勾选记录须填写最晚付费日期'); + return; + } + const alreadySubmitted = selectedRows.find((r) => isCompareProcurementSelectionDisabled(r.procurementStatus)); + if (alreadySubmitted) { + message.warning('勾选记录中包含审批中或审批通过项,请重新选择'); + return; + } + if (!validateCompareSheetRequiredMeta()) return; + if (!editingCompareSheetId) { + Modal.confirm({ + title: '保存并提交采购', + content: '提交采购前将先保存当前比价单,是否继续?', + okText: '继续', + cancelText: '取消', + centered: true, + onOk: () => { + const savedId = handleSubmitCompareSheet({ closeModal: false }); + if (savedId) submitProcurementApplication(); + }, + }); + return; + } + submitProcurementApplication(); + }; + + const resolvePolicyVehicleKey = (plateOrVin) => { + const key = (plateOrVin || '').trim(); + if (!key) return null; + const byPlate = findVehicleByPlate(key); + if (byPlate) return getVehicleLedgerKey(byPlate); + const byVin = findVehicleByVin(key); + if (byVin) return getVehicleLedgerKey(byVin); + return null; + }; + + const upsertPolicyRecognTask = useCallback((snapshot) => { + const { + taskId, + entry, + mode, + insuranceType, + results, + phase, + completedAt, + totalFileCount, + recognDoneCount, + } = snapshot; + if (!taskId) return; + setPolicyRecognTasks((prev) => { + const existing = prev.find((t) => t.id === taskId); + let mergedResults = results; + if (mergedResults !== undefined && existing?.results?.length) { + const existingFailed = existing.results.filter((r) => r.recognSuccess === false); + const newHasFailed = mergedResults.some((r) => r.recognSuccess === false); + if (existingFailed.length && !newHasFailed) { + mergedResults = [...mergedResults, ...existingFailed]; + } + } + const finalResults = mergedResults !== undefined ? mergedResults : (existing?.results || []); + const record = buildPolicyRecognTaskRecord({ + id: taskId, + entry: entry ?? existing?.entry ?? 'ocr', + mode: mode ?? existing?.mode ?? 'policy', + insuranceType: insuranceType ?? existing?.insuranceType ?? '', + results: finalResults, + createdAt: existing?.createdAt, + creator: existing?.creator, + completedAt: completedAt ?? existing?.completedAt ?? '', + phase: phase ?? existing?.phase ?? 'results', + totalFileCount: totalFileCount ?? existing?.totalFileCount, + recognDoneCount: recognDoneCount ?? existing?.recognDoneCount, + }); + const idx = prev.findIndex((t) => t.id === taskId); + const next = idx >= 0 + ? prev.map((t, i) => (i === idx ? record : t)) + : [record, ...prev]; + persistPolicyRecognTasksToStorage(next); + return next; + }); + }, []); + + const vehicleInsuranceHistory = useMemo(() => { + if (!vehicleInsMgmtVehicle) { + return { timeline: [], timelinePolicy: [], timelineBiz: [], byType: {}, ledgerKey: '' }; + } + const built = buildVehicleInsuranceHistory( + vehicleInsMgmtVehicle, + allInsurance, + compareSheets, + policyRecognTasks + ); + const patched = applyHistoryEditsToVehicleHistory(built, insuranceHistoryEdits); + const insRecord = ensureInsuranceRecordShape(allInsurance[patched.ledgerKey] || createEmptyInsuranceRecord()); + const { timelinePolicy, timelineBiz } = splitVehicleInsuranceTimeline(patched.timeline, insRecord); + return { ...patched, timelinePolicy, timelineBiz }; + }, [vehicleInsMgmtVehicle, allInsurance, compareSheets, policyRecognTasks, insuranceHistoryEdits]); + + const openVehicleInsuranceMgmt = (vehicle) => { + setVehicleInsMgmtVehicle(vehicle); + setVehicleInsMgmtActiveTab('timeline'); + setVehicleInsMgmtHighlightId(''); + setVehicleInsMgmtPolicyNoFilter(''); + setVehicleInsMgmtTabPage({}); + setVehicleInsMgmtOpen(true); + }; + + const openPolicyBizModal = (record, mode) => { + if (!vehicleInsMgmtVehicle || !record?.typeKey) return; + const ledgerKey = getVehicleLedgerKey(vehicleInsMgmtVehicle); + const item = allInsurance[ledgerKey]?.[record.typeKey] || {}; + setPolicyBizModalRecord(record); + setPolicyBizModalMode(mode); + setPolicyBizForm({ + suspendTime: item.suspendTime || ANCHOR_TODAY, + resumeTime: item.resumeTime || item.reinstateDate || '', + newEndDate: item.endDate || '', + cancelTime: item.cancelTime || ANCHOR_TODAY, + refundPremium: item.refundPremium || item.premium || '', + }); + setPolicyBizAttachmentFileList(attachmentsToUploadFileList(item.attachments || [])); + setPolicyBizModalOpen(true); + }; + + const openPolicyOpHistoryModal = (record) => { + if (!vehicleInsMgmtVehicle || !record) return; + const ledgerKey = getVehicleLedgerKey(vehicleInsMgmtVehicle); + const ledgerItem = allInsurance[ledgerKey]?.[record.typeKey]; + let logs = record.operationLogs || []; + if (record.isArchived && ledgerItem?.archivedPolicies?.length) { + const archivedIndex = Number(String(record.id || '').split('-archived-')[1]); + if (!Number.isNaN(archivedIndex)) { + logs = ledgerItem.archivedPolicies[archivedIndex]?.operationLogs || logs; + } + } else if (record.isLedgerCurrent && ledgerItem) { + logs = ledgerItem.operationLogs || logs; + } + setPolicyOpHistoryRecord({ ...record, operationLogs: logs }); + setPolicyOpHistoryOpen(true); + }; + + const handlePolicyBizAttachmentChange = ({ fileList }) => { + const incoming = fileList.filter((f) => f.status !== 'removed'); + const valid = incoming.filter((f) => isPolicyRecognImageOrPdf(f)); + if (valid.length < incoming.length) { + message.warning('已忽略不支持格式,附件仅支持 PDF / 图片'); + } + setPolicyBizAttachmentFileList( + valid.map((f) => ({ + ...f, + status: 'done', + uploadedAt: f.uploadedAt || formatCompareSheetNow(), + })) + ); + }; + + const submitPolicyBizModal = () => { + if (!vehicleInsMgmtVehicle || !policyBizModalRecord?.typeKey) return; + const ledgerKey = getVehicleLedgerKey(vehicleInsMgmtVehicle); + const typeKey = policyBizModalRecord.typeKey; + const mode = policyBizModalMode; + const attachments = uploadFileListToAttachments(policyBizAttachmentFileList); + + if (mode === 'suspend' && (!policyBizForm.suspendTime || !policyBizForm.newEndDate)) { + message.warning('请填写中止时间与「新到期日期」'); + return; + } + if (mode === 'resume' && (!policyBizForm.resumeTime || !policyBizForm.newEndDate)) { + message.warning('请填写恢复时间与「新到期日期」'); + return; + } + if (mode === 'cancel' && !policyBizForm.cancelTime) { + message.warning('请填写退保时间'); + return; + } + + updateAllInsurance((prev) => { + const rec = ensureInsuranceRecordShape(prev[ledgerKey] || createEmptyInsuranceRecord()); + const before = { ...rec[typeKey] }; + const nextItem = { ...before }; + const changes = []; + const beforeStatus = before.policyTag === 'cancelled' + ? '已退保' + : before.policyTag === 'suspended' + ? '已停保' + : '正常'; + + if (mode === 'suspend') { + changes.push( + { label: '保单状态', before: beforeStatus, after: '已停保' }, + { label: '到期日期', before: before.endDate, after: policyBizForm.newEndDate }, + { label: '中止时间', before: before.suspendTime, after: policyBizForm.suspendTime }, + { label: '恢复时间', before: before.resumeTime || before.reinstateDate, after: policyBizForm.resumeTime }, + ); + nextItem.policyTag = 'suspended'; + nextItem.endDate = policyBizForm.newEndDate; + nextItem.suspendTime = policyBizForm.suspendTime; + nextItem.resumeTime = policyBizForm.resumeTime; + nextItem.reinstateDate = policyBizForm.resumeTime; + if (attachments.length) nextItem.attachments = attachments; + } else if (mode === 'resume') { + const resumeBeforeStatus = before.policyTag === 'cancelled' + ? '已退保' + : before.policyTag === 'suspended' + ? '已停保' + : '正常'; + changes.push( + { label: '保单状态', before: resumeBeforeStatus, after: '正常' }, + { label: '到期日期', before: before.endDate, after: policyBizForm.newEndDate }, + { label: '恢复时间', before: before.resumeTime || before.reinstateDate, after: policyBizForm.resumeTime }, + ); + if (before.policyTag === 'cancelled') { + changes.push({ label: '退保时间', before: before.cancelTime, after: '' }); + nextItem.cancelTime = ''; + } + nextItem.policyTag = ''; + nextItem.endDate = policyBizForm.newEndDate; + nextItem.resumeTime = policyBizForm.resumeTime; + nextItem.reinstateDate = policyBizForm.resumeTime; + if (attachments.length) nextItem.attachments = attachments; + } else if (mode === 'cancel') { + changes.push( + { label: '保单状态', before: beforeStatus, after: '已退保' }, + { label: '到期日期', before: before.endDate, after: '' }, + { label: '退保时间', before: before.cancelTime, after: policyBizForm.cancelTime }, + { label: '退还保费', before: before.refundPremium, after: policyBizForm.refundPremium }, + ); + nextItem.policyTag = 'cancelled'; + nextItem.endDate = ''; + nextItem.cancelTime = policyBizForm.cancelTime; + nextItem.refundPremium = policyBizForm.refundPremium; + if (attachments.length) nextItem.attachments = attachments; + } + nextItem.updateTime = formatCompareSheetNow(); + nextItem.updateUser = PROTO_COMPARE_CREATOR; + nextItem.operationLogs = appendInsuranceOperationLog(before.operationLogs, { + type: mode, + remark: buildOperationChangeRemark(changes), + attachments: ['suspend', 'resume', 'cancel'].includes(mode) ? attachments : [], + }); + return { ...prev, [ledgerKey]: { ...rec, [typeKey]: nextItem } }; + }); + + const okText = mode === 'suspend' ? '停保已提交' : mode === 'resume' ? '复驶已提交' : '退保已提交'; + message.success(okText); + setPolicyBizModalOpen(false); + setPolicyBizModalRecord(null); + setPolicyBizForm({ ...EMPTY_POLICY_BIZ_FORM }); + setPolicyBizAttachmentFileList([]); + }; + + const getPolicyMoreMenuItems = (record) => { + const items = []; + if (record?.isLedgerCurrent && !record?.isArchived) { + const pt = record.purchaseType; + if (pt === 'cancel') { + items.push({ key: 'resume', label: '复驶' }); + } else if (pt === 'new' || pt === 'renew') { + items.push({ key: 'suspend', label: '停保' }); + items.push({ key: 'cancel', label: '退保' }); + } else if (pt === 'rentStop') { + items.push({ key: 'resume', label: '复驶' }); + items.push({ key: 'cancel', label: '退保' }); + } + } + items.push({ key: 'history', label: '操作历史' }); + return items; + }; + + const handlePolicyMoreMenuClick = (record, key) => { + if (key === 'history') { + openPolicyOpHistoryModal(record); + return; + } + openPolicyBizModal(record, key); + }; + + const jumpToVehicleInsuranceRecord = (typeKey, recordId, rowsSource) => { + const rows = rowsSource || vehicleInsuranceHistory.byType[typeKey] || []; + const idx = rows.findIndex((r) => r.id === recordId); + if (idx >= 0 && rows.length > 8) { + const pageSize = 8; + setVehicleInsMgmtTabPage((prev) => ({ + ...prev, + [typeKey]: Math.floor(idx / pageSize) + 1, + })); + } + setVehicleInsMgmtActiveTab(typeKey); + setVehicleInsMgmtHighlightId(recordId); + window.setTimeout(() => setVehicleInsMgmtHighlightId(''), 3200); + }; + + const handleVehicleInsMgmtPolicyNoSearch = () => { + const key = (vehicleInsMgmtPolicyNoFilter || '').trim().toUpperCase(); + if (!key) { + message.warning('请输入保单号'); + return; + } + const matched = vehicleInsuranceHistory.timeline.filter((record) => ( + String(record.policyNo || '').toUpperCase().includes(key) + )); + if (!matched.length) { + message.warning('未找到匹配的保单记录'); + return; + } + if (matched.length > 1) { + message.info(`找到 ${matched.length} 条匹配记录,已定位至第一条`); + } + const target = matched[0]; + jumpToVehicleInsuranceRecord(target.typeKey, target.id); + }; + + const handleInsuranceRecordPreview = (record) => { + Modal.info({ + title: `预览 · ${record.fileName}`, + width: 520, + centered: true, + content: ( +
+
险种:{record.typeLabel}
+
保单号:{record.policyNo || '—'}
+
保险公司:{record.company || '—'}
+
正式环境将内嵌 PDF / 图片预览;原型仅展示附件名称。
+
+ ), + okText: '关闭', + }); + }; + + const handleInsuranceRecordDownload = (record) => { + message.success(`已开始下载:${record.fileName || '保单附件'}(原型)`); + }; + + const handleOperationLogAttachmentPreview = (attachment) => { + if (!attachment?.name) { + message.info('该操作未上传附件'); + return; + } + Modal.info({ + title: `预览 · ${attachment.name}`, + width: 520, + centered: true, + content: ( +
+
文件名:{attachment.name}
+ {attachment.size ? ( +
大小:{formatAttachmentSize(attachment.size)}
+ ) : null} +
正式环境将内嵌 PDF / 图片预览;原型仅展示附件名称。
+
+ ), + okText: '关闭', + }); + }; + + const handleOperationLogAttachmentDownload = (attachment) => { + if (!attachment?.name) { + message.info('该操作未上传附件'); + return; + } + message.success(`已开始下载:${attachment.name}(原型)`); + }; + + const renderInsurancePolicyStatusTag = (record) => { + const meta = getInsurancePolicyStatusMeta(record); + const tag = ( + + {meta.label} + + ); + if (meta.label === '已停保') { + return ( + + {tag} + + ); + } + return tag; + }; + + const syncVehicleInsHistoryEditToLedger = (record, detail) => { + if (!vehicleInsMgmtVehicle || record.source !== 'ledger') return; + const ledgerKey = getVehicleLedgerKey(vehicleInsMgmtVehicle); + if (!ledgerKey) return; + const ledgerEvents = new Set(['purchase', 'suspend', 'cancel']); + if (!ledgerEvents.has(record.eventType)) return; + const mode = bizTypeToRecognMode(detail.bizType); + updateAllInsurance((prev) => { + const rec = ensureInsuranceRecordShape(prev[ledgerKey] || createEmptyInsuranceRecord()); + const item = rec[record.typeKey]; + if (!item?.policyNo) return prev; + if (record.eventType === 'purchase' && item.policyNo !== record.policyNo) return prev; + const nextItem = applyPolicyDetailToInsuranceItem({ ...item }, detail, mode); + nextItem.updateTime = formatCompareSheetNow(); + nextItem.updateUser = PROTO_COMPARE_CREATOR; + return { ...prev, [ledgerKey]: { ...rec, [record.typeKey]: nextItem } }; + }); + }; + + const syncVehicleInsHistoryEditToRecognTask = (record, detail) => { + if (record.source !== 'recognize' || !record.recognizeTaskId || !record.recognizeResultId) return; + setPolicyRecognTasks((prev) => { + const next = prev.map((task) => { + if (task.id !== record.recognizeTaskId) return task; + const results = (task.results || []).map((r) => ( + r.id === record.recognizeResultId + ? mergeRecognResultWithDetail(r, detail) + : r + )); + return { ...task, results }; + }); + persistPolicyRecognTasksToStorage(next); + return next; + }); + }; + + const openVehicleInsHistoryEdit = (record) => { + if (!vehicleInsMgmtVehicle) return; + setVehicleInsHistoryEditRecord(record); + setVehicleInsHistoryEditDraft(historyRecordToPolicyDetail(record, vehicleInsMgmtVehicle)); + setVehicleInsHistoryEditOpen(true); + }; + + const saveVehicleInsHistoryEdit = () => { + if (!vehicleInsHistoryEditRecord) return; + const detail = normalizePolicyDetail(vehicleInsHistoryEditDraft); + if (!detail.policyNo && !detail.endDate) { + message.warning('请至少填写保单号或到期日期'); + return; + } + const record = vehicleInsHistoryEditRecord; + setInsuranceHistoryEdits((prev) => { + const next = { ...prev, [record.id]: detail }; + persistInsuranceHistoryEditsToStorage(next); + return next; + }); + syncVehicleInsHistoryEditToLedger(record, detail); + syncVehicleInsHistoryEditToRecognTask(record, detail); + setVehicleInsHistoryEditOpen(false); + setVehicleInsHistoryEditRecord(null); + message.success('已保存保单要素'); + }; + + const renderPurchaseTypeChip = (purchaseType, record) => { + const meta = POLICY_PURCHASE_TYPE_META[purchaseType] || { label: purchaseType, chipClass: '' }; + const chip = ( + {meta.label} + ); + if (purchaseType === 'rentStop' && record) { + return ( + + {chip} + + ); + } + return chip; + }; + + const renderVehicleInsuranceTimelineCard = (item, side) => ( +
jumpToVehicleInsuranceRecord(item.typeKey, item.id)} + onKeyDown={(e) => { + if (e.key === 'Enter') jumpToVehicleInsuranceRecord(item.typeKey, item.id); + }} + > +
+ {item.time || '—'} +
+
+ {renderPurchaseTypeChip(item.purchaseType, item)} + {item.typeLabel} + {item.policyNo ? ( + {item.policyNo} + ) : null} +
+
{item.summary}
+
+ {item.sourceLabel ? `${item.sourceLabel} · ` : ''} + 点击查看 {item.typeLabel} 明细 → +
+
+ ); + + const renderVehicleInsuranceCenterTimeline = () => { + const policyItems = vehicleInsuranceHistory.timelinePolicy || []; + const bizItems = vehicleInsuranceHistory.timelineBiz || []; + const merged = [ + ...policyItems.map((item) => ({ ...item, timelineSide: 'policy' })), + ...bizItems.map((item) => ({ ...item, timelineSide: 'biz' })), + ].sort((a, b) => String(b.time || '').localeCompare(String(a.time || ''))); + + return ( +
+
+
+ 保单新增 / 续保 +
+
+
+ 停保 / 复驶 / 退保 +
+
+
+ {merged.map((item) => ( +
+
+ {item.timelineSide === 'policy' ? renderVehicleInsuranceTimelineCard(item, 'left') : null} +
+
+ +
+
+ {item.timelineSide === 'biz' ? renderVehicleInsuranceTimelineCard(item, 'right') : null} +
+
+ ))} +
+
+ ); + }; + + const vehicleInsMgmtTabCounts = useMemo(() => { + const counts = { + timeline: (vehicleInsuranceHistory.timelinePolicy?.length || 0) + + (vehicleInsuranceHistory.timelineBiz?.length || 0), + }; + VEHICLE_INSURANCE_MGMT_TABS.filter((t) => t.key !== 'timeline').forEach((tab) => { + counts[tab.key] = (vehicleInsuranceHistory.byType[tab.key] || []).length; + }); + return counts; + }, [vehicleInsuranceHistory]); + + const renderMgmtTableEllipsis = (val) => { + const text = val || '—'; + return ( + + {text} + + ); + }; + + const vehicleInsuranceHistoryColumns = [ + { + title: '保险导入时间', + dataIndex: 'purchaseTime', + width: 148, + ellipsis: true, + render: (val) => renderMgmtTableEllipsis(val), + }, + { + title: '类型', + dataIndex: 'purchaseType', + width: 72, + render: (val, record) => renderPurchaseTypeChip(val, record), + }, + { + title: '保单号', + dataIndex: 'policyNo', + width: 148, + ellipsis: true, + render: (val) => renderMgmtTableEllipsis(val), + }, + { + title: '保险状态', + key: 'policyStatus', + width: 88, + render: (_, record) => renderInsurancePolicyStatusTag(record), + }, + { + title: '保险公司', + dataIndex: 'company', + width: 160, + ellipsis: true, + render: (val) => renderMgmtTableEllipsis(val), + }, + { + title: '付款时间', + dataIndex: 'payTime', + width: 148, + ellipsis: true, + render: (val) => renderMgmtTableEllipsis(val), + }, + { + title: '生效日期', + dataIndex: 'startDate', + width: 100, + ellipsis: true, + render: (val) => renderMgmtTableEllipsis(val), + }, + { + title: '到期日期', + dataIndex: 'endDate', + width: 100, + ellipsis: true, + render: (val) => renderMgmtTableEllipsis(val), + }, + { + title: '金额', + dataIndex: 'premium', + width: 96, + align: 'right', + ellipsis: true, + render: (val, record) => { + const text = val + ? `${record.purchaseType === 'cancel' ? '-' : ''}¥${val}` + : '—'; + return ( + + + {text} + + + ); + }, + }, + { + title: '操作', + key: 'action', + width: 188, + fixed: 'right', + render: (_, record) => ( +
+ + + + { + domEvent.stopPropagation(); + handlePolicyMoreMenuClick(record, key); + }, + }} + > + + e.stopPropagation()} + > + {ICONS.more} + + + +
+ ), + }, + ]; + + const renderVehicleInsuranceTypeTab = (typeKey) => { + const rows = vehicleInsuranceHistory.byType[typeKey] || []; + const tabLabel = VEHICLE_INSURANCE_MGMT_TABS.find((t) => t.key === typeKey)?.label || ''; + if (!rows.length) { + return ( +
+
{tabLabel}暂无记录
+
该险种首次购买为「新保」,此前已购后再投保记为「续保」
+
+ ); + } + return ( +
+ 8 ? { + current: vehicleInsMgmtTabPage[typeKey] || 1, + pageSize: 8, + showSizeChanger: false, + size: 'small', + onChange: (page) => setVehicleInsMgmtTabPage((prev) => ({ ...prev, [typeKey]: page })), + } : false} + scroll={{ x: 1248 }} + rowClassName={(record) => (record.id === vehicleInsMgmtHighlightId ? 'lc-ins-history-row--active' : '')} + /> + + ); + }; + + const renderVehicleInsMgmtTabLabel = (tab) => { + const count = vehicleInsMgmtTabCounts[tab.key] || 0; + return ( + + {tab.label} + {count > 0 ? {count} : null} + + ); + }; + + const filteredPolicyRecognTasks = useMemo(() => { + const mode = appliedPolicyRecognTasksFilters.mode; + const range = appliedPolicyRecognTasksFilters.createdRange; + return [...policyRecognTasks] + .filter((task) => { + if (mode && mode !== '全部' && task.modeLabel !== mode) return false; + return compareSheetMatchesCreatedRange(task.createdAt, range); + }) + .sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt))); + }, [policyRecognTasks, appliedPolicyRecognTasksFilters]); + + const openPolicyRecognTasksModal = () => { + setPolicyRecognTasksFilters({ ...DEFAULT_POLICY_RECOGN_TASK_FILTERS }); + setAppliedPolicyRecognTasksFilters({ ...DEFAULT_POLICY_RECOGN_TASK_FILTERS }); + setPolicyRecognTasksOpen(true); + }; + + const handlePolicyRecognTasksQuery = () => { + setAppliedPolicyRecognTasksFilters({ ...policyRecognTasksFilters }); + message.success('已刷新任务列表'); + }; + + const handlePolicyRecognTasksReset = () => { + setPolicyRecognTasksFilters({ ...DEFAULT_POLICY_RECOGN_TASK_FILTERS }); + setAppliedPolicyRecognTasksFilters({ ...DEFAULT_POLICY_RECOGN_TASK_FILTERS }); + }; + + const openPolicyRecogn = (entry, initialMode = 'policy') => { + const canResumeOcr = entry === 'ocr' + && policyRecognEntry === 'ocr' + && policyRecognTaskId + && (policyRecognPhase === 'recognizing' + || (policyRecognPhase === 'results' && policyRecognResults.length)); + if (canResumeOcr) { + if (policyRecognPhase === 'results' && policyRecognResults.length && !policyRecognActiveResultId) { + const preferred = policyRecognResults.find((r) => r.matched && !r.confirmed) + || policyRecognResults.find((r) => !r.confirmed) + || policyRecognResults[0]; + if (preferred) { + setPolicyRecognActiveResultId(preferred.id); + setPolicyRecognConfirmDraft(buildPolicyRecognConfirmDraft(preferred, policyRecognMode)); + showPolicyRecognResultPreview(preferred); + } + } + setPolicyRecognOpen(true); + return; + } + if (policyRecognTimerRef.current) { + window.clearTimeout(policyRecognTimerRef.current); + policyRecognTimerRef.current = null; + } + if (policyRecognProgressTimerRef.current) { + window.clearInterval(policyRecognProgressTimerRef.current); + policyRecognProgressTimerRef.current = null; + } + setPolicyRecognEntry(entry); + setPolicyRecognMode(entry === 'import' ? 'policy' : initialMode); + setPolicyRecognInsuranceType('交强险'); + setPolicyRecognPhase('upload'); + setPolicyRecognFiles([]); + setPolicyRecognTaskId(''); + setPolicyRecognResults([]); + setPolicyRecognViewOnly(false); + setPolicyRecognActiveResultId(''); + setPolicyRecognConfirmDraft({ ...EMPTY_POLICY_DETAIL }); + setPolicyPreview(null); + setPolicyRecognOpen(true); + }; + + const openPolicyRecognTaskRecord = (task) => { + if (!task?.id) { + message.warning('任务记录无效'); + return; + } + if (isPolicyRecognTaskRecognizing(task)) { + message.info('请等待识别完成后操作'); + return; + } + const successResults = getPolicyRecognSuccessResults(task.results).map((r) => ({ ...r })); + if (!successResults.length) { + message.warning('暂无识别成功的结果可确认'); + return; + } + setPolicyRecognEntry(task.entry || 'ocr'); + setPolicyRecognMode(task.mode || 'policy'); + setPolicyRecognInsuranceType(task.insuranceType || '交强险'); + setPolicyRecognTaskId(task.id); + setPolicyRecognResults(successResults); + setPolicyRecognFiles([]); + setPolicyRecognViewOnly(task.status === 'completed'); + setPolicyRecognPhase('results'); + setPolicyPreview(null); + setPolicyRecognOpen(true); + setPolicyRecognTasksOpen(false); + const preferred = successResults.find((r) => r.matched && !r.confirmed) + || successResults.find((r) => !r.confirmed) + || successResults[0]; + if (preferred) { + setPolicyRecognActiveResultId(preferred.id); + setPolicyRecognConfirmDraft(buildPolicyRecognConfirmDraft(preferred, task.mode || 'policy')); + showPolicyRecognResultPreview(preferred); + } + }; + + const closePolicyRecogn = () => { + const syncedResults = policyRecognPhase === 'results' + ? persistActiveRecognDraft() + : policyRecognResults; + if (syncedResults !== policyRecognResults) { + setPolicyRecognResults(syncedResults); + } + if (policyRecognTaskId && syncedResults.length) { + const status = derivePolicyRecognTaskStatus(syncedResults); + upsertPolicyRecognTask({ + taskId: policyRecognTaskId, + entry: policyRecognEntry, + mode: policyRecognMode, + insuranceType: policyRecognInsuranceType, + results: syncedResults, + phase: policyRecognPhase, + completedAt: status === 'completed' ? formatCompareSheetNow() : undefined, + }); + } + if (policyPreview?.url) URL.revokeObjectURL(policyPreview.url); + setPolicyPreview(null); + setPolicyRecognOpen(false); + }; + + const policyRecognAllUploaded = policyRecognFiles.length > 0 + && policyRecognFiles.every((f) => f.status === 'done'); + + const handlePolicyRecognUploadChange = ({ fileList }) => { + const incoming = fileList.filter((f) => f.status !== 'removed'); + const valid = incoming.filter((f) => isPolicyRecognImageOrPdf(f)); + if (valid.length < incoming.length) { + message.warning('已忽略不支持格式,仅支持 PDF / 图片'); + } + const next = valid.map((f) => { + if (f.status === 'done') return f; + return { ...f, status: 'uploading', percent: f.percent || 0 }; + }); + setPolicyRecognFiles(next); + next.forEach((f, i) => { + if (f.status === 'uploading') { + window.setTimeout(() => { + setPolicyRecognFiles((prev) => prev.map((p) => ( + p.uid === f.uid ? { ...p, status: 'done', percent: 100 } : p + ))); + }, 500 + i * 280); + } + }); + }; + + const handlePolicyImportUploadChange = ({ fileList }) => { + const incoming = fileList.filter((f) => f.status !== 'removed').slice(-1); + const valid = incoming.filter((f) => isPolicyImportExcelFile(f)); + if (incoming.length && !valid.length) { + message.warning('请上传 Excel 模板文件(.csv、.xlsx、.xls)'); + return; + } + const next = valid.map((f) => ( + f.status === 'done' ? f : { ...f, status: 'done', percent: 100 } + )); + setPolicyRecognFiles(next); + }; + + const mergeRecognResultWithDetail = (result, detail) => { + const mode = resolvePolicyRecognEffectiveMode(policyRecognMode, result); + const stripped = stripPolicyRecognDraftMeta(detail); + const normalizedDetail = mode === 'suspend' + ? enrichSuspendPolicyDetail({ ...stripped, bizType: 'suspend' }) + : mode === 'resume' + ? enrichResumePolicyDetail({ ...stripped, bizType: 'resume' }) + : mode === 'cancel' + ? enrichCancelPolicyDetail({ ...stripped, bizType: 'cancel' }) + : normalizePolicyDetail({ ...stripped, bizType: mode !== 'policy' ? mode : (stripped.bizType || 'policy') }); + const rebuilt = buildRecognResultFromDetail( + { + id: result.id, + fileUid: result.fileUid, + fileName: result.fileName, + fileType: result.fileType, + }, + normalizedDetail, + allInsurance, + mode + ); + return { ...result, ...rebuilt, id: result.id, confirmed: result.confirmed }; + }; + + const showPolicyRecognResultPreview = (result) => { + if (!result) return; + const file = policyRecognFiles.find((f) => f.uid === result.fileUid); + if (policyPreview?.url) URL.revokeObjectURL(policyPreview.url); + if (file?.originFileObj && (file.type || '').startsWith('image/')) { + const url = URL.createObjectURL(file.originFileObj); + setPolicyPreview({ url, fileName: result.fileName, isImage: true }); + return; + } + setPolicyPreview({ + url: null, + fileName: result.fileName, + isImage: false, + hint: policyRecognEntry === 'import' + ? '导入记录无原件预览,请核对右侧识别字段' + : (policyRecognFiles.length + ? 'PDF 预览(原型):正式环境将内嵌预览识别原件' + : '任务记录未保存原件,正式环境可从附件库查看'), + }); + }; + + const persistActiveRecognDraft = (resultsList = policyRecognResults) => { + if (!policyRecognActiveResultId) return resultsList; + const result = resultsList.find((r) => r.id === policyRecognActiveResultId); + if (!result) return resultsList; + const merged = mergeRecognResultWithDetail(result, policyRecognConfirmDraft); + return resultsList.map((r) => (r.id === policyRecognActiveResultId ? merged : r)); + }; + + const selectPolicyRecognResult = (resultId, resultsList = policyRecognResults) => { + const nextResults = policyRecognActiveResultId && policyRecognActiveResultId !== resultId + ? persistActiveRecognDraft(resultsList) + : resultsList; + if (nextResults !== resultsList) { + setPolicyRecognResults(nextResults); + } + const result = nextResults.find((r) => r.id === resultId); + if (!result) return nextResults; + setPolicyRecognActiveResultId(resultId); + setPolicyRecognConfirmDraft(buildPolicyRecognConfirmDraft(result, policyRecognMode)); + showPolicyRecognResultPreview(result); + return nextResults; + }; + + const enterPolicyRecognConfirmPhase = (results, snapshot = {}) => { + const displayResults = getPolicyRecognSuccessResults(results); + if (!displayResults.length) return; + if (snapshot.mode) setPolicyRecognMode(snapshot.mode); + if (snapshot.entry) setPolicyRecognEntry(snapshot.entry); + if (snapshot.insuranceType !== undefined) setPolicyRecognInsuranceType(snapshot.insuranceType); + setPolicyRecognResults(displayResults); + setPolicyRecognPhase('results'); + const preferred = displayResults.find((r) => r.matched && !r.confirmed) + || displayResults.find((r) => !r.confirmed) + || displayResults[0]; + if (preferred) { + setPolicyRecognActiveResultId(preferred.id); + setPolicyRecognConfirmDraft(buildPolicyRecognConfirmDraft(preferred, snapshot.mode ?? policyRecognMode)); + showPolicyRecognResultPreview(preferred); + } + if (snapshot.taskId) { + upsertPolicyRecognTask({ + taskId: snapshot.taskId, + entry: snapshot.entry ?? policyRecognEntry, + mode: snapshot.mode ?? policyRecognMode, + insuranceType: snapshot.insuranceType ?? policyRecognInsuranceType, + results, + phase: 'results', + totalFileCount: snapshot.totalFileCount, + recognDoneCount: snapshot.recognDoneCount ?? snapshot.totalFileCount, + }); + } + }; + + const handleRecognConfirmPlateChange = (plateNo) => { + const vehicle = plateNo ? findVehicleByPlate(plateNo) : null; + const result = policyRecognResults.find((r) => r.id === policyRecognActiveResultId); + const ocrPlate = String(result?.ocrRecognizedPlate || result?.ocrPlateNo || '').trim().toUpperCase(); + const selected = String(plateNo || '').trim().toUpperCase(); + if (ocrPlate && selected && ocrPlate !== selected) { + message.error(`识别车牌号为 ${ocrPlate},与所选 ${selected} 不一致,请核对`); + } + setPolicyRecognConfirmDraft((prev) => ({ + ...prev, + plateNo: plateNo || '', + vin: vehicle?.vin || (plateNo ? prev.vin : ''), + vehicleOwner: vehicle?.vin + ? (prev.vehicleOwner || getVehicleRegistrationOwner(plateNo)) + : prev.vehicleOwner, + _ocrRecognizedPlate: ocrPlate || prev._ocrRecognizedPlate, + _plateLedgerMatched: !!vehicle, + })); + }; + + const startPolicyExcelImportTask = async () => { + const fileItem = policyRecognFiles.find((f) => f.status === 'done'); + const file = fileItem?.originFileObj; + if (!file) { + message.warning('请先上传 Excel 导入文件'); + return; + } + const taskId = createPolicyRecognTaskId(); + setPolicyRecognPhase('recognizing'); + setPolicyRecognTaskId(taskId); + try { + const text = await readPolicyImportFileAsText(file); + if (!String(text).trim()) { + setPolicyRecognPhase('upload'); + return; + } + const rows = parsePolicyImportFileText(text); + if (!rows.length) { + message.error('未解析到有效数据,请按模板填写带 * 的必填项'); + setPolicyRecognPhase('upload'); + return; + } + if (!validatePolicyImportRows(rows)) { + setPolicyRecognPhase('upload'); + return; + } + const results = buildImportResultsFromRows(rows, allInsurance); + enterPolicyRecognConfirmPhase(results, { + taskId, + entry: 'import', + mode: 'policy', + insuranceType: '', + }); + const matchedN = results.filter((r) => r.matched).length; + message.success(`已解析 ${results.length} 条,${matchedN} 条已匹配台账,请核对识别内容后确认`); + } catch { + message.error('导入文件读取失败,请重试'); + setPolicyRecognPhase('upload'); + } + }; + + const startPolicyRecognTask = () => { + if (!policyRecognAllUploaded) { + message.warning(policyRecognEntry === 'import' ? '请先上传 Excel 文件' : '请等待全部文件上传完成'); + return; + } + if (policyRecognEntry === 'import') { + startPolicyExcelImportTask(); + return; + } + if (policyRecognMode === 'policy' && !policyRecognInsuranceType) { + message.warning('请选择保险类型'); + return; + } + const taskId = createPolicyRecognTaskId(); + const entrySnap = policyRecognEntry; + const modeSnap = policyRecognMode; + const insuranceSnap = policyRecognInsuranceType; + const filesSnap = policyRecognFiles; + const fileCount = filesSnap.filter((f) => f.status === 'done').length; + if (policyRecognTimerRef.current) { + window.clearTimeout(policyRecognTimerRef.current); + policyRecognTimerRef.current = null; + } + if (policyRecognProgressTimerRef.current) { + window.clearInterval(policyRecognProgressTimerRef.current); + policyRecognProgressTimerRef.current = null; + } + setPolicyRecognPhase('recognizing'); + setPolicyRecognTaskId(taskId); + upsertPolicyRecognTask({ + taskId, + entry: entrySnap, + mode: modeSnap, + insuranceType: insuranceSnap, + results: [], + phase: 'recognizing', + totalFileCount: fileCount, + recognDoneCount: 0, + }); + setPolicyRecognOpen(false); + message.info('正在识别,请稍后点击「保单批量识别」确认识别结果'); + policyRecognProgressTimerRef.current = window.setInterval(() => { + setPolicyRecognTasks((prev) => { + const task = prev.find((t) => t.id === taskId); + if (!task || !isPolicyRecognTaskRecognizing(task)) { + if (policyRecognProgressTimerRef.current) { + window.clearInterval(policyRecognProgressTimerRef.current); + policyRecognProgressTimerRef.current = null; + } + return prev; + } + const nextDone = Math.min(task.totalFileCount, (task.recognDoneCount || 0) + 1); + if (nextDone >= task.totalFileCount) { + if (policyRecognProgressTimerRef.current) { + window.clearInterval(policyRecognProgressTimerRef.current); + policyRecognProgressTimerRef.current = null; + } + return prev; + } + const next = prev.map((t) => ( + t.id === taskId ? { ...t, recognDoneCount: nextDone } : t + )); + persistPolicyRecognTasksToStorage(next); + return next; + }); + }, 480); + policyRecognTimerRef.current = window.setTimeout(() => { + policyRecognTimerRef.current = null; + if (policyRecognProgressTimerRef.current) { + window.clearInterval(policyRecognProgressTimerRef.current); + policyRecognProgressTimerRef.current = null; + } + const results = buildMockOcrResults( + filesSnap, + modeSnap, + insuranceSnap, + allInsurance + ); + enterPolicyRecognConfirmPhase(results, { + taskId, + entry: entrySnap, + mode: modeSnap, + insuranceType: insuranceSnap, + totalFileCount: fileCount, + recognDoneCount: fileCount, + }); + }, 2400); + }; + + const openPolicyRecognResults = () => { + if (!policyRecognResults.length) { + message.warning('暂无识别结果'); + return; + } + enterPolicyRecognConfirmPhase(policyRecognResults, { taskId: policyRecognTaskId }); + }; + + const renderPolicyDetailForm = (draft, setDraft, options = {}) => { + const { + showBizType = true, + recognConfirmMode = false, + policyEntryMode = false, + bizRecognMode, + onPlateChange, + recognResult, + } = options; + const isSuspendRecogn = recognConfirmMode && ( + bizRecognMode === 'suspend' || draft.bizType === 'suspend' + ); + const isResumeRecogn = recognConfirmMode && ( + bizRecognMode === 'resume' || draft.bizType === 'resume' + ); + const isCancelRecogn = recognConfirmMode && ( + bizRecognMode === 'cancel' || draft.bizType === 'cancel' + ); + const isEndorsementRecogn = isSuspendRecogn || isResumeRecogn || isCancelRecogn; + const isPolicyRecogn = recognConfirmMode && !isEndorsementRecogn; + const companySelectList = (() => { + if (!isPolicyRecogn) return INSURANCE_MGMT_COMPANIES; + const raw = recognResult?.ocrCompanyRaw || ''; + const partial = matchInsuranceCompanyCandidates(raw); + if (partial.length > 1) return partial; + return INSURANCE_MGMT_COMPANIES; + })(); + const companySelectOptions = companySelectList.map((c) => ({ label: c, value: c })); + const requiredKeys = isSuspendRecogn + ? SUSPEND_RECOGN_FORM_REQUIRED_KEYS + : isResumeRecogn + ? RESUME_RECOGN_FORM_REQUIRED_KEYS + : isCancelRecogn + ? CANCEL_RECOGN_FORM_REQUIRED_KEYS + : (recognConfirmMode || policyEntryMode ? POLICY_ENTRY_FORM_REQUIRED_KEYS : []); + const fieldLabel = (text, key) => ( + requiredKeys.includes(key) ? {text} : text + ); + const coverageRows = getCoverageItemsFormRows(draft.coverageItems); + const updateCoverageField = (idx, field, value) => { + const next = coverageRows.map((row, i) => (i === idx ? { ...row, [field]: value } : row)); + setDraft((p) => ({ ...p, coverageItems: next })); + }; + const addCoverageRow = () => { + setDraft((p) => ({ + ...p, + coverageItems: [...getCoverageItemsFormRows(p.coverageItems), { ...EMPTY_COVERAGE_ITEM }], + })); + }; + const removeCoverageRow = (idx) => { + const next = coverageRows.filter((_, i) => i !== idx); + setDraft((p) => ({ ...p, coverageItems: next.length ? next : [{ ...EMPTY_COVERAGE_ITEM }] })); + }; + return ( +
+ {isPolicyRecogn && draft._ocrRecognizedPlate && !draft._plateLedgerMatched ? ( +
+ +
+ ) : null} + {isPolicyRecogn ? ( +
车辆与主体
+ ) : (!recognConfirmMode ?
车辆与险种
: null)} + {isPolicyRecogn ? renderFilterField('车主', ( + setDraft((p) => ({ ...p, vehicleOwner: e.target.value }))} + placeholder="行驶证车主名称" + /> + )) : null} + {!isEndorsementRecogn ? renderFilterField(fieldLabel('车牌号', 'plateNo'), ( + recognConfirmMode ? ( + setDraft((p) => ({ ...p, plateNo: e.target.value }))} + placeholder="与 VIN 至少填一项" + /> + ) + )) : null} + {!isEndorsementRecogn ? renderFilterField('车辆识别代码', ( + setDraft((p) => ({ ...p, vin: e.target.value }))} + style={recognConfirmMode ? { background: '#f8fafc', color: '#475569' } : undefined} + /> + )) : null} + {isPolicyRecogn ? ( + <> + {renderFilterField('投保人', ( + setDraft((p) => ({ ...p, applicant: e.target.value }))} + placeholder="投保人名称" + /> + ))} + {renderFilterField('被保险人', ( + setDraft((p) => ({ ...p, insured: e.target.value }))} + placeholder="被保险人名称" + /> + ))} + + ) : null} + {showBizType ? renderFilterField('业务类型', ( + setDraft((p) => ({ ...p, insuranceType: v }))} + style={{ width: '100%' }} + options={QUOTE_INSURANCE_TYPES.map((t) => ({ label: t, value: t }))} + /> + )) : null} + {!recognConfirmMode ?
保单要素
: null} + {isPolicyRecogn ?
保单要素
: null} + {isEndorsementRecogn ?
批单要素
: null} + {!isEndorsementRecogn ? renderFilterField(fieldLabel('保险公司', 'company'), ( + setDraft((p) => ({ ...p, policyNo: e.target.value }))} /> + ))} + {!recognConfirmMode ? renderFilterField('批单号', ( + setDraft((p) => ({ ...p, endorsementNo: e.target.value }))} placeholder="停保/复驶/退保批单号" /> + )) : null} + {!isEndorsementRecogn ? renderFilterField( + isPolicyRecogn ? fieldLabel('收费确认时间', 'payTime') : fieldLabel('付款时间', 'payTime'), + ( + setDraft((p) => ({ ...p, payTime: e.target.value }))} + onBlur={(e) => setDraft((p) => ({ ...p, payTime: normalizeRecognPayTime(e.target.value) }))} + placeholder="YYYY-MM-DD HH:MM:SS" + /> + ) + ) : null} + {!recognConfirmMode ? renderFilterField('签单日期', ( + setDraft((p) => ({ ...p, signDate: ds || '' }))} + /> + )) : null} + {isSuspendRecogn ? ( + <> + {renderFilterField(fieldLabel('中止时间', 'suspendTime'), ( + setDraft((p) => ({ + ...p, + suspendTime: ds || '', + startDate: ds || p.startDate, + }))} + /> + ))} + {renderFilterField('恢复时间', ( + setDraft((p) => ({ + ...p, + resumeTime: ds || '', + reinstateDate: ds || p.reinstateDate, + }))} + /> + ))} + {renderFilterField(fieldLabel('新到期日期', 'newEndDate'), ( + setDraft((p) => ({ + ...p, + newEndDate: ds || '', + endDate: ds || p.endDate, + }))} + /> + ))} + + ) : isResumeRecogn ? ( + <> + {renderFilterField(fieldLabel('恢复时间', 'resumeTime'), ( + setDraft((p) => ({ + ...p, + resumeTime: ds || '', + reinstateDate: ds || p.reinstateDate, + startDate: ds || p.startDate, + }))} + /> + ))} + {renderFilterField(fieldLabel('新到期日期', 'newEndDate'), ( + setDraft((p) => ({ + ...p, + newEndDate: ds || '', + endDate: ds || p.endDate, + }))} + /> + ))} + + ) : isCancelRecogn ? ( + <> + {renderFilterField(fieldLabel('退保时间', 'cancelTime'), ( + setDraft((p) => ({ + ...p, + cancelTime: ds || '', + startDate: ds || p.startDate, + endDate: ds || p.endDate, + }))} + /> + ))} + {renderFilterField(fieldLabel('退保金额', 'premium'), ( + setDraft((p) => ({ ...p, premium: sanitizePremiumInput(e.target.value) }))} + onBlur={(e) => setDraft((p) => ({ ...p, premium: normalizeRecognPremiumAmount(e.target.value) }))} + placeholder="0.00" + /> + ))} + + ) : ( + <> + {renderFilterField(fieldLabel('生效日期', 'startDate'), ( + setDraft((p) => ({ ...p, startDate: ds || '' }))} + /> + ))} + {renderFilterField(fieldLabel('到期日期', 'endDate'), ( + setDraft((p) => ({ ...p, endDate: ds || '' }))} + /> + ))} + + )} + {!recognConfirmMode && (draft.bizType === 'suspend' || draft.bizType === 'resume') ? renderFilterField('复驶日期', ( + setDraft((p) => ({ ...p, reinstateDate: ds || '' }))} + /> + )) : null} + {!isEndorsementRecogn ? renderFilterField( + recognConfirmMode || policyEntryMode + ? fieldLabel('保险费合计', 'premium') + : (draft.bizType === 'cancel' ? '退费金额(元)' : '保险费合计'), + ( + setDraft((p) => ({ ...p, premium: sanitizePremiumInput(e.target.value) }))} + onBlur={(e) => setDraft((p) => ({ ...p, premium: normalizeRecognPremiumAmount(e.target.value) }))} + placeholder="0.00" + /> + ) + ) : null} + {!recognConfirmMode && !isPolicyRecogn ? renderFilterField('投保人', ( + setDraft((p) => ({ ...p, applicant: e.target.value }))} /> + )) : null} + {!recognConfirmMode && !isPolicyRecogn ? renderFilterField('被保险人', ( + setDraft((p) => ({ ...p, insured: e.target.value }))} /> + )) : null} + {!isEndorsementRecogn && !isPolicyRecogn ? ( +
+ {!recognConfirmMode ? ( +
保单项目/责任限额
+ ) : null} +
+
`cov-row-${idx}`} + dataSource={coverageRows} + scroll={{ x: recognConfirmMode ? 600 : 720 }} + locale={{ emptyText: '暂无承保险种数据' }} + columns={[ + { + title: '承保险种', + dataIndex: 'coverageName', + width: recognConfirmMode ? 140 : 160, + render: (val, _row, idx) => ( + updateCoverageField(idx, 'coverageName', e.target.value)} + placeholder="如:机动车损失险" + /> + ), + }, + { + title: '保险金额', + dataIndex: 'coverageAmount', + width: recognConfirmMode ? 120 : 140, + render: (val, _row, idx) => ( + updateCoverageField(idx, 'coverageAmount', e.target.value)} + placeholder="如:2000000元" + /> + ), + }, + { + title: '保险金额/责任免额', + dataIndex: 'deductible', + width: recognConfirmMode ? 140 : 160, + render: (val, _row, idx) => ( + updateCoverageField(idx, 'deductible', e.target.value)} + placeholder="如:绝对免赔额500元" + /> + ), + }, + { + title: '保险费', + dataIndex: 'itemPremium', + width: recognConfirmMode ? 96 : 110, + render: (val, _row, idx) => ( + updateCoverageField(idx, 'itemPremium', e.target.value)} + placeholder="0.00" + /> + ), + }, + { + title: '操作', + key: 'action', + width: 64, + fixed: 'right', + render: (_val, _row, idx) => ( + + ), + }, + ]} + /> + + + {!recognConfirmMode ? ( +
+ 按承保险种分行维护保额、免赔额与分项保险费;上方「保险费合计」为整单合计金额 +
+ ) : ( +
+ 识别结果已自动反写,可按需修改;上方「保险费合计」为整单合计金额 +
+ )} + + ) : isSuspendRecogn ? ( +
+ 停保批单核对保单号、中止时间、恢复时间与新到期日期;台账匹配依据保单号,「新到期日期」确认后将写入该保单到期日期 +
+ ) : isResumeRecogn ? ( +
+ 复驶批单核对保单号、恢复时间与新到期日期;台账匹配依据保单号,「新到期日期」确认后将写入该保单到期日期 +
+ ) : isCancelRecogn ? ( +
+ 退保批单核对保单号、退保时间与退保金额;台账匹配依据保单号,确认后将标记该保单为已退保 +
+ ) : null} + + ); + }; + + const confirmPolicyRecognResult = (resultId, detailOverride) => { + const baseResults = persistActiveRecognDraft(); + const result = baseResults.find((r) => r.id === resultId); + if (!result) return; + const detail = detailOverride || ( + resultId === policyRecognActiveResultId + ? policyRecognConfirmDraft + : buildPolicyRecognConfirmDraft(result, policyRecognMode) + ); + const confirmMode = resolvePolicyRecognEffectiveMode(policyRecognMode, result); + if (!validatePolicyRecognDetailForConfirm(detail, { mode: confirmMode, recognResult: result })) return; + const merged = mergeRecognResultWithDetail(result, detail); + if (!merged.matched) { + const warnMode = resolvePolicyRecognEffectiveMode(policyRecognMode, merged); + message.warning( + warnMode === 'suspend' || warnMode === 'resume' || warnMode === 'cancel' + ? '该条未匹配台账,请检查保单号是否正确' + : '该条未匹配台账,请检查车牌号是否正确' + ); + return; + } + if (merged.confirmed) { + message.info('该条已确认'); + return; + } + const mode = resolvePolicyRecognEffectiveMode(policyRecognMode, merged); + updateAllInsurance(applyPolicyOcrResultToLedger(merged, mode)); + const nextResults = baseResults.map((r) => ( + r.id === resultId ? { ...merged, confirmed: true } : r + )); + setPolicyRecognResults(nextResults); + if (derivePolicyRecognTaskStatus(nextResults) === 'completed') { + setPolicyRecognViewOnly(true); + } + if (policyRecognTaskId) { + upsertPolicyRecognTask({ + taskId: policyRecognTaskId, + entry: policyRecognEntry, + mode: policyRecognMode, + insuranceType: policyRecognInsuranceType, + results: nextResults, + phase: 'results', + completedAt: derivePolicyRecognTaskStatus(nextResults) === 'completed' ? formatCompareSheetNow() : undefined, + }); + } + message.success(`已确认 ${merged.displayPlate || merged.ocrVin},台账已更新`); + }; + + const confirmCurrentPolicyRecognResult = () => { + if (!policyRecognActiveResultId) { + message.warning('请先选择要确认的识别记录'); + return; + } + confirmPolicyRecognResult(policyRecognActiveResultId, policyRecognConfirmDraft); + }; + + const confirmAllPolicyRecognResults = () => { + const synced = persistActiveRecognDraft(); + setPolicyRecognResults(synced); + const pending = synced.filter((r) => r.matched && !r.confirmed); + if (!pending.length) { + message.info('没有可批量确认的记录'); + return; + } + let nextInsurance = { ...allInsurance }; + const invalid = pending.find((result) => { + const detail = result.id === policyRecognActiveResultId + ? policyRecognConfirmDraft + : buildPolicyRecognConfirmDraft(result, policyRecognMode); + const mode = resolvePolicyRecognEffectiveMode(policyRecognMode, result); + return !validatePolicyRecognDetailForConfirm(detail, { mode, silent: true, recognResult: result }); + }); + if (invalid) { + message.warning('批量确认前请确保每条记录必填项均已填写,可先逐条确认'); + return; + } + pending.forEach((result) => { + const detail = result.id === policyRecognActiveResultId + ? policyRecognConfirmDraft + : buildPolicyRecognConfirmDraft(result, policyRecognMode); + const merged = mergeRecognResultWithDetail(result, detail); + const mode = resolvePolicyRecognEffectiveMode(policyRecognMode, merged); + nextInsurance = applyPolicyOcrResultToLedger(merged, mode)(nextInsurance); + }); + updateAllInsurance(() => nextInsurance); + const nextResults = policyRecognResults.map((r) => ( + r.matched ? { ...r, confirmed: true } : r + )); + setPolicyRecognResults(nextResults); + const allDone = derivePolicyRecognTaskStatus(nextResults) === 'completed'; + if (allDone) setPolicyRecognViewOnly(true); + if (policyRecognTaskId) { + upsertPolicyRecognTask({ + taskId: policyRecognTaskId, + entry: policyRecognEntry, + mode: policyRecognMode, + insuranceType: policyRecognInsuranceType, + results: nextResults, + phase: 'results', + completedAt: allDone ? formatCompareSheetNow() : undefined, + }); + } + message.success(`已批量确认 ${pending.length} 条,台账到期日期已更新`); + }; + + const policyRecognActiveResult = useMemo( + () => policyRecognResults.find((r) => r.id === policyRecognActiveResultId) || null, + [policyRecognResults, policyRecognActiveResultId] + ); + + const activePolicyRecognMode = useMemo( + () => resolvePolicyRecognEffectiveMode(policyRecognMode, policyRecognActiveResult), + [policyRecognMode, policyRecognActiveResult] + ); + + const policyRecognPickerColumns = useMemo(() => ([ + { + title: '文件/记录', + dataIndex: 'fileName', + width: 160, + ellipsis: true, + }, + { + title: '车牌号', + key: 'plate', + width: 100, + render: (_, r) => r.displayPlate || r.ocrPlateNo || '—', + }, + { + title: '险种', + dataIndex: 'insuranceTypeLabel', + width: 72, + }, + { + title: '匹配', + key: 'matched', + width: 80, + render: (_, r) => ( + + {r.matched ? '已匹配' : '未匹配'} + + ), + }, + { + title: '状态', + key: 'confirmed', + width: 80, + render: (_, r) => ( + r.confirmed ? 已确认 : 待确认 + ), + }, + ]), []); + + const handlePolicyAddAttachmentChange = ({ fileList }) => { + const incoming = fileList.filter((f) => f.status !== 'removed'); + const valid = incoming.filter((f) => isPolicyRecognImageOrPdf(f)); + if (valid.length < incoming.length) { + message.warning('已忽略不支持格式,保单附件仅支持 PDF / 图片'); + } + setPolicyAddAttachmentFileList( + valid.map((f) => ({ + ...f, + status: 'done', + uploadedAt: f.uploadedAt || formatCompareSheetNow(), + })) + ); + }; + + const handlePolicyAddSubmit = () => { + const attachments = uploadFileListToAttachments(policyAddAttachmentFileList); + const detail = normalizePolicyDetail({ + ...policyAddDraft, + bizType: 'policy', + attachments, + }); + if (!validatePolicyEntryDetail(detail).ok) return; + const ledgerKey = resolvePolicyVehicleKey(detail.plateNo || detail.vin); + if (!ledgerKey) { + message.warning('请填写台账中存在的车牌或 VIN'); + return; + } + const typeKey = INSURANCE_LABEL_TO_KEY[detail.insuranceType]; + if (!typeKey) { + message.warning('险种填写有误'); + return; + } + updateAllInsurance((prev) => { + const record = ensureInsuranceRecordShape(prev[ledgerKey] || createEmptyInsuranceRecord()); + const existing = record[typeKey] || createEmptyInsuranceItem(); + let baseItem = { ...existing }; + let logs = existing.operationLogs || []; + if (existing.policyTag === 'cancelled' && existing.policyNo) { + baseItem = { + ...createEmptyInsuranceItem(), + archivedPolicies: [...(existing.archivedPolicies || []), { ...existing }], + }; + logs = []; + } + const item = applyPolicyDetailToInsuranceItem( + baseItem, + { ...detail, policyNo: detail.policyNo || `MAN-${Date.now().toString().slice(-6)}` }, + 'policy' + ); + item.updateTime = formatCompareSheetNow(); + item.updateUser = PROTO_COMPARE_CREATOR; + item.operationLogs = appendInsuranceOperationLog(logs, { + type: 'add', + remark: buildOperationChangeRemark([ + { label: '保单号', before: existing.policyNo, after: item.policyNo }, + { label: '保险公司', before: existing.company, after: item.company }, + { label: '到期日期', before: existing.endDate, after: item.endDate }, + ]), + }); + return { ...prev, [ledgerKey]: { ...record, [typeKey]: item } }; + }); + message.success('保单已录入台账'); + setPolicyAddOpen(false); + setPolicyAddDraft({ ...EMPTY_POLICY_DETAIL }); + setPolicyAddAttachmentFileList([]); + }; + + const renderPolicyStatusTag = (item) => { + if (!item?.policyTag) return null; + if (item.policyTag === 'suspended') { + return ( + + 已停保 + + ); + } + if (item.policyTag === 'cancelled') { + return ( + 已退保 + ); + } + return null; + }; + + const renderDateCell = (rowId, field, value) => ( + updateCompareRow(rowId, { [field]: ds || '' })} + placeholder="选择日期" + style={{ width: '100%' }} + /> + ); + + const renderQuoteAddPopover = (row) => ( + { + setQuoteEditRowId(open ? row.id : null); + if (!open) setQuoteDraft(createEmptyQuoteDraft()); + }} + content={( +
+
+ 添加报价 + {row.insuranceType || '交强险'} +
+
+
+ + setQuoteDraft((d) => ({ ...d, premium: sanitizePremiumInput(e.target.value) }))} + onBlur={() => { + if (quoteDraft.premium && isValidPremium(quoteDraft.premium)) { + setQuoteDraft((d) => ({ ...d, premium: formatPremiumDisplay(d.premium) })); + } + }} + /> +
+
+ +
+
+
+ )} + > + +
+ ); + + const renderQuoteCell = (row) => { + const quotes = row.quotes || []; + return ( +
+ {quotes.length > 0 ? ( + updateCompareRow(row.id, { confirmedQuoteId: e.target.value })} + className="lc-compare-quote-inline-list" + > + {quotes.map((q) => { + const selected = row.confirmedQuoteId === q.id; + return ( + + ); + })} + + ) : ( + 暂无报价(必填) + )} + {renderQuoteAddPopover(row)} +
+ ); + }; + + const compareColumns = [ + { + title: '车牌号', + dataIndex: 'plateNo', + width: 118, + fixed: 'left', + onHeaderCell: () => ({ className: 'lc-compare-th-key' }), + render: (val, row) => ( + (option?.label || '').toLowerCase().includes(input.toLowerCase())} + onChange={(v) => handleCompareVinChange(row.id, v)} + /> + + ), + }, + { + title: '客户', + dataIndex: 'customer', + width: 128, + onHeaderCell: () => ({ className: 'lc-compare-th-auto' }), + render: (val, row) => renderReadonlyField(val, isCompareRowVehicleLinked(row)), + }, + { + title: '归属公司', + dataIndex: 'ownerCompany', + width: 148, + onHeaderCell: () => ({ className: 'lc-compare-th-auto' }), + render: (val, row) => renderReadonlyField(val, isCompareRowVehicleLinked(row)), + }, + { + title: '品牌', + dataIndex: 'brand', + width: 80, + onHeaderCell: () => ({ className: 'lc-compare-th-auto' }), + render: (val, row) => renderReadonlyField(val, isCompareRowVehicleLinked(row)), + }, + { + title: '型号', + dataIndex: 'model', + width: 120, + onHeaderCell: () => ({ className: 'lc-compare-th-auto' }), + render: (val, row) => renderReadonlyField(val, isCompareRowVehicleLinked(row)), + }, + { + title: '车身颜色', + dataIndex: 'bodyColor', + width: 80, + onHeaderCell: () => ({ className: 'lc-compare-th-auto' }), + render: (val, row) => renderReadonlyField(val, isCompareRowVehicleLinked(row)), + }, + { + title: '行驶证注册日期', + dataIndex: 'regDate', + width: 118, + onHeaderCell: () => ({ className: 'lc-compare-th-auto' }), + render: (val, row) => renderReadonlyDate(val, isCompareRowVehicleLinked(row)), + }, + { + title: '年检有效期', + dataIndex: 'inspectExpire', + width: 108, + onHeaderCell: () => ({ className: 'lc-compare-th-auto' }), + render: (val, row) => renderReadonlyDate(val, isCompareRowVehicleLinked(row)), + }, + { + title: '投保方式', + dataIndex: 'insureMode', + width: 96, + onHeaderCell: () => ({ className: 'lc-compare-th-edit' }), + render: (val, row) => ( + updateCompareRow(row.id, { + insuranceType: v, + quotes: [], + confirmedQuoteId: '', + })} + options={QUOTE_INSURANCE_TYPES.map((t) => ({ label: t, value: t }))} + /> + ), + }, + { + title: '交强险到期日期', + dataIndex: 'jqValidUntil', + width: 108, + onHeaderCell: () => ({ className: 'lc-compare-th-auto' }), + render: (val, row) => renderReadonlyDate(val, isCompareRowVehicleLinked(row)), + }, + { + title: '商业险到期日期', + dataIndex: 'syValidUntil', + width: 108, + onHeaderCell: () => ({ className: 'lc-compare-th-auto' }), + render: (val, row) => renderReadonlyDate(val, isCompareRowVehicleLinked(row)), + }, + { + title: '最晚付费日期', + dataIndex: 'latestPayDate', + width: 128, + onHeaderCell: () => ({ className: 'lc-compare-th-edit' }), + render: (val, row) => { + const paySt = getLatestPayDateStatus(val); + return ( +
+ {renderDateCell(row.id, 'latestPayDate', val)} + {val ? ( +
+ + {paySt.type === 'overdue' ? `超期${Math.abs(paySt.diffDays)}天` : paySt.type === 'warning' ? `临期${paySt.diffDays}天` : `剩余${paySt.diffDays}天`} + +
+ ) : null} +
+ ); + }, + }, + { + title: '采购状态', + dataIndex: 'procurementStatus', + width: 92, + fixed: 'right', + onHeaderCell: () => ({ className: 'lc-compare-th-auto' }), + render: (st) => renderCompareProcurementStatusTag(st), + }, + { + title: '当前审批人', + dataIndex: 'procurementCurrentApprover', + width: 96, + fixed: 'right', + onHeaderCell: () => ({ className: 'lc-compare-th-auto' }), + render: (val, row) => { + if (normalizeCompareProcurementStatus(row.procurementStatus) !== 'submitted') { + return ; + } + if (!val) { + return ( + + + + ); + } + return {val}; + }, + }, + { + title: ( + + 报价情况 + * + + ), + key: 'quotes', + width: 240, + fixed: 'right', + onHeaderCell: () => ({ className: 'lc-compare-th-edit' }), + render: (_, row) => renderQuoteCell(row), + }, + { + title: '操作', + key: 'action', + width: 96, + fixed: 'right', + onHeaderCell: () => ({ className: 'lc-compare-th-edit' }), + render: (_, row) => ( +
+ { + setCopyPopoverRowId(open ? row.id : null); + if (!open) setCopyCountDraft(1); + }} + content={( +
+
复制条数
+ setCopyCountDraft(v || 1)} style={{ width: '100%' }} /> +
+ + +
+
+ )} + > + +
+ +
+ ), + }, + ]; + + const getInsuranceItemStatus = (ledgerKey, typeKey) => { + const item = allInsurance[ledgerKey]?.[typeKey]; + if (item?.policyTag === 'cancelled') { + return { type: 'unuploaded', text: '已退保', diffDays: null }; + } + if (!item || !item.policyNo) { + return { type: 'unuploaded', text: '未购买', diffDays: null }; + } + if (!item.endDate) { + if (item.policyTag === 'suspended') { + return { type: 'warning', text: '已停保', diffDays: null }; + } + return { type: 'unuploaded', text: '未购买', diffDays: null }; + } + const today = new Date(ANCHOR_TODAY); + today.setHours(0, 0, 0, 0); + const expDate = new Date(item.endDate); + expDate.setHours(0, 0, 0, 0); + const diffDays = Math.ceil((expDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)); + if (diffDays <= 0) { + return { type: 'expired', text: `已到期 (逾期 ${Math.abs(diffDays)} 天)`, diffDays }; + } + if (diffDays <= INSURANCE_WARN_DAYS) { + return { type: 'warning', text: `临期 (${diffDays} 天后)`, diffDays }; + } + return { type: 'success', text: '正常', diffDays }; + }; + + /** 与车辆管理「保险状态」一致:交强险 + 商业险均存在且在有效期内(临期仍算有效) */ + const getVehicleInsuranceStatus = (ledgerKey) => { + const jq = getInsuranceItemStatus(ledgerKey, 'compulsory'); + const sy = getInsuranceItemStatus(ledgerKey, 'commercial'); + const isValid = (st) => st.type === 'success' || st.type === 'warning'; + const isNormal = isValid(jq) && isValid(sy); + if (isNormal) { + const hasWarning = jq.type === 'warning' || sy.type === 'warning'; + return { + label: hasWarning ? '临期' : '正常', + color: hasWarning ? 'warning' : 'success', + abnormal: false, + tip: hasWarning + ? '交强险或商业险临期,仍在有效期内,可交车但需尽快续保' + : '交强险、商业险均在有效期内', + }; + } + const reasons = []; + if (jq.type === 'unuploaded') reasons.push('交强险未购买'); + else if (jq.type === 'expired') reasons.push('交强险已到期'); + if (sy.type === 'unuploaded') reasons.push('商业险未购买'); + else if (sy.type === 'expired') reasons.push('商业险已到期'); + return { + label: '异常', + color: 'error', + abnormal: true, + tip: `${reasons.join(';')}。保险状态异常的车辆禁止交车`, + }; + }; + + const isCoreInsuranceNormal = (ledgerKey) => !getVehicleInsuranceStatus(ledgerKey).abnormal; + + const isAnyInsuranceWarning = (ledgerKey) => ( + INSURANCE_TYPE_ITEMS.some((item) => getInsuranceItemStatus(ledgerKey, item.key).type === 'warning') + ); + + const isCoreInsuranceExpired = (ledgerKey) => ( + CORE_INSURANCE_KEYS.some((key) => getInsuranceItemStatus(ledgerKey, key).type === 'expired') + ); + + const isCoreInsuranceMissing = (ledgerKey) => ( + CORE_INSURANCE_KEYS.some((key) => getInsuranceItemStatus(ledgerKey, key).type === 'unuploaded') + ); + + const matchKpiFilter = (ledgerKey, filterKey) => { + if (filterKey === 'total') return true; + if (filterKey === 'normal') return isCoreInsuranceNormal(ledgerKey); + if (filterKey === 'warning') return isAnyInsuranceWarning(ledgerKey); + if (filterKey === 'expired') return isCoreInsuranceExpired(ledgerKey); + if (filterKey === 'unuploaded') return isCoreInsuranceMissing(ledgerKey); + return true; + }; + + const stats = useMemo(() => { + let normal = 0; + let warning = 0; + let expired = 0; + let unuploaded = 0; + MOCK_VEHICLES.forEach((v) => { + const ledgerKey = getVehicleLedgerKey(v); + if (isCoreInsuranceNormal(ledgerKey)) normal += 1; + if (isAnyInsuranceWarning(ledgerKey)) warning += 1; + if (isCoreInsuranceExpired(ledgerKey)) expired += 1; + if (isCoreInsuranceMissing(ledgerKey)) unuploaded += 1; + }); + return { total: MOCK_VEHICLES.length, normal, warning, expired, unuploaded }; + }, [allInsurance]); + + const activeCompareSubmissionSet = useMemo( + () => buildActiveCompareSubmissionSet(compareSheets), + [compareSheets] + ); + + const compareProcurementStatusByVehicleType = useMemo( + () => buildCompareProcurementStatusByVehicleType(compareSheets), + [compareSheets] + ); + + const openInsuranceAlertModal = (mode) => { + setInsuranceAlertMode(mode); + setInsuranceAlertTypeFilter( + mode === 'coreExpired' ? [...CORE_INSURANCE_KEYS] : [...EXPIRING_WARN_TYPE_KEYS] + ); + setInsuranceAlertSort({ key: 'commercial', order: 'descend' }); + setInsuranceAlertOpen(true); + }; + + const insuranceAlertBaseList = useMemo(() => { + const selectedKeys = insuranceAlertTypeFilter || []; + if (!selectedKeys.length) return []; + return MOCK_VEHICLES.filter((vehicle) => { + const ledgerKey = getVehicleLedgerKey(vehicle); + if (insuranceAlertMode === 'coreExpired') { + if (!isCoreInsuranceExpired(ledgerKey)) return false; + return selectedKeys.some((typeKey) => getInsuranceItemStatus(ledgerKey, typeKey).type === 'expired'); + } + return selectedKeys.some((typeKey) => { + const st = getInsuranceItemStatus(ledgerKey, typeKey).type; + return st === 'warning' || st === 'expired'; + }); + }); + }, [allInsurance, insuranceAlertTypeFilter, insuranceAlertMode]); + + const insuranceAlertSortedList = useMemo(() => { + const list = [...insuranceAlertBaseList]; + const sortKey = insuranceAlertSort.key || 'commercial'; + const sortOrder = insuranceAlertSort.order || 'descend'; + list.sort((a, b) => { + const av = getVehicleInsuranceEndDate(getVehicleLedgerKey(a), sortKey, allInsurance); + const bv = getVehicleInsuranceEndDate(getVehicleLedgerKey(b), sortKey, allInsurance); + return compareInsuranceEndDate(av, bv, sortOrder); + }); + return list; + }, [insuranceAlertBaseList, insuranceAlertSort, allInsurance]); + + const handleInsuranceAlertTableChange = (_pagination, _filters, sorter) => { + const nextSorter = Array.isArray(sorter) ? sorter[0] : sorter; + if (!nextSorter || !nextSorter.columnKey) return; + setInsuranceAlertSort({ + key: nextSorter.columnKey, + order: nextSorter.order || 'descend', + }); + }; + + const renderInsuranceAlertDateCell = (record, typeKey) => { + const ledgerKey = getVehicleLedgerKey(record); + const dateVal = getVehicleInsuranceEndDate(ledgerKey, typeKey, allInsurance); + const status = getInsuranceItemStatus(ledgerKey, typeKey); + const isWarn = status.type === 'warning'; + const isExpired = status.type === 'expired'; + const typeLabel = INSURANCE_KEY_TO_LABEL[typeKey]; + const procurementSt = getCompareProcurementStatusForVehicleType( + record, + typeLabel, + compareProcurementStatusByVehicleType + ); + return ( +
+ + {dateVal || '—'} + + {procurementSt && (isWarn || isExpired) ? ( +
+ {renderAlertCompareProcurementTag(procurementSt)} +
+ ) : null} +
+ ); + }; + + const insuranceAlertColumns = useMemo(() => { + const sortOrderFor = (typeKey) => (insuranceAlertSort.key === typeKey ? insuranceAlertSort.order : null); + const dateCol = (typeKey, title) => ({ + title, + key: typeKey, + dataIndex: typeKey, + width: 136, + sortOrder: sortOrderFor(typeKey), + sorter: () => 0, + sortDirections: ['descend', 'ascend'], + showSorterTooltip: false, + render: (_val, record) => renderInsuranceAlertDateCell(record, typeKey), + }); + return [ + { + title: '车牌号', + key: 'plateNo', + width: 108, + fixed: 'left', + render: (_val, record) => ( + {formatVehiclePlateDisplay(record.plateNo)} + ), + }, + dateCol('compulsory', '交强险到期日期'), + dateCol('commercial', '商业险到期日期'), + dateCol('excess', '超赔险到期日期'), + dateCol('cargo', '货物险到期日期'), + dateCol('driverAccident', '驾意险到期日期'), + ]; + }, [allInsurance, insuranceAlertSort, compareProcurementStatusByVehicleType]); + + const openBatchCompareTypesModal = () => { + if (!insuranceAlertSortedList.length) { + message.warning( + insuranceAlertMode === 'coreExpired' + ? '当前筛选条件下暂无核心险种逾期记录' + : '当前筛选条件下暂无临期记录' + ); + return; + } + const defaultLabels = insuranceAlertTypeFilter.map((key) => INSURANCE_KEY_TO_LABEL[key]).filter(Boolean); + const fallback = insuranceAlertMode === 'coreExpired' + ? CORE_INSURANCE_KEYS.map((k) => INSURANCE_KEY_TO_LABEL[k]) + : [...QUOTE_INSURANCE_TYPES]; + setBatchCompareTypesDraft(defaultLabels.length ? defaultLabels : fallback); + setBatchCompareTypesOpen(true); + }; + + const handleConfirmBatchCompareSheets = () => { + const allowedTypes = insuranceAlertMode === 'coreExpired' + ? CORE_INSURANCE_KEYS.map((k) => INSURANCE_KEY_TO_LABEL[k]) + : QUOTE_INSURANCE_TYPES; + const selectedLabels = (batchCompareTypesDraft || []).filter((label) => allowedTypes.includes(label)); + if (!selectedLabels.length) { + message.warning('请至少选择一种保险类型'); + return; + } + + const targetStatus = insuranceAlertMode === 'coreExpired' ? 'expired' : 'warning'; + const generatedRows = []; + let skippedSubmitted = 0; + selectedLabels.forEach((typeLabel) => { + const typeKey = INSURANCE_LABEL_TO_KEY[typeLabel]; + insuranceAlertSortedList + .filter((vehicle) => getInsuranceItemStatus(getVehicleLedgerKey(vehicle), typeKey).type === targetStatus) + .forEach((vehicle) => { + if (isVehicleTypeSubmittedToCompare(vehicle, typeLabel, activeCompareSubmissionSet)) { + skippedSubmitted += 1; + return; + } + const row = buildCompareRowFromVehicle(vehicle, allInsurance); + row.insuranceType = typeLabel; + generatedRows.push(row); + }); + }); + + if (!generatedRows.length) { + message.warning( + skippedSubmitted + ? '所选险种均已提交比价单或无可生成记录,未生成购买记录' + : (insuranceAlertMode === 'coreExpired' + ? '所选险种在当前列表中无逾期车辆,未生成购买记录' + : '所选险种在当前列表中无临期车辆,未生成购买记录') + ); + return; + } + + setEditingCompareSheetId(null); + setCompareRows(normalizeCompareRows(generatedRows)); + setCompareRemark(`临期预警一键生成 · ${selectedLabels.join('、')}`); + setCompareEditorFilters({ ...DEFAULT_COMPARE_EDITOR_FILTERS }); + setCompareVehicleFilterOpen(false); + setCompareVehicleFilterDraft(''); + setCompareAttachmentFileList([]); + setSelectedCompareKeys([]); + setQuoteDraft(createEmptyQuoteDraft()); + setQuoteEditRowId(null); + setBatchCompareTypesOpen(false); + setInsuranceAlertOpen(false); + setCompareModalOpen(true); + const skipHint = skippedSubmitted ? `,已跳过 ${skippedSubmitted} 条已提交比价单记录` : ''; + message.success(`已带入 ${generatedRows.length} 条购买记录至新建比价单${skipHint},请继续维护报价后保存`); + }; + + const brandOptions = useMemo( + () => [...new Set(MOCK_VEHICLES.map((v) => v.brand))].map((b) => ({ label: b, value: b })), + [] + ); + const modelOptions = useMemo( + () => [...new Set(MOCK_VEHICLES.map((v) => v.model))].map((m) => ({ label: m, value: m })), + [] + ); + + const appliedMultiPlates = useMemo(() => parseMultiPlates(appliedFilters.plateNos), [appliedFilters.plateNos]); + + const filterVehiclesByFilters = (vehicles, f, kpi) => { + const plateKey = (f.plateNo || '').trim().toLowerCase(); + const multiPlates = parseMultiPlates(f.plateNos); + const vinKey = (f.vin || '').trim().toLowerCase(); + const brandKey = (f.brand || '').trim().toLowerCase(); + const modelKey = (f.model || '').trim().toLowerCase(); + + return vehicles.filter((v) => { + const ledgerKey = getVehicleLedgerKey(v); + const plateText = (v.plateNo || '').trim(); + if (multiPlates.length) { + if (!plateText) return false; + if (!multiPlates.includes(plateText.toUpperCase())) return false; + } else if (plateKey) { + if (!plateText) { + if (!NO_PLATE_LABEL.toLowerCase().includes(plateKey) && !plateKey.includes('暂无')) return false; + } else if (!plateText.toLowerCase().includes(plateKey)) return false; + } + if (vinKey && !v.vin.toLowerCase().includes(vinKey)) return false; + if (brandKey && !v.brand.toLowerCase().includes(brandKey)) return false; + if (modelKey && !v.model.toLowerCase().includes(modelKey)) return false; + if (f.operateStatus !== '全部' && v.status !== f.operateStatus) return false; + if (f.insuranceStatus === '正常' && getVehicleInsuranceStatus(ledgerKey).abnormal) return false; + if (f.insuranceStatus === '异常' && !getVehicleInsuranceStatus(ledgerKey).abnormal) return false; + if (!vehicleMatchesListInsuranceTypeFilter(ledgerKey, f.insuranceType, f.endDateRange, allInsurance)) return false; + if (!matchKpiFilter(ledgerKey, kpi)) return false; + return true; + }); + }; + + const filteredVehicles = useMemo( + () => sortVehiclesRetiredLast(filterVehiclesByFilters(MOCK_VEHICLES, appliedFilters, kpiFilter)), + [appliedFilters, allInsurance, kpiFilter] + ); + + const handleListFilterQuery = () => { + const hasEndStart = listFilters.endDateRange?.[0]; + const hasEndEnd = listFilters.endDateRange?.[1]; + if ((hasEndStart || hasEndEnd) && !listFilters.insuranceType) { + message.warning('请先选择保险类型,再按到期时间筛选'); + return; + } + if ((hasEndStart && !hasEndEnd) || (!hasEndStart && hasEndEnd)) { + message.warning('请完整选择到期时间的开始与结束日期'); + return; + } + const plates = parseMultiPlates(multiPlateDraft); + const next = { + ...listFilters, + plateNos: multiPlateDraft.trim(), + plateNo: plates.length ? '' : listFilters.plateNo, + }; + setListFilters(next); + setAppliedFilters(next); + setMultiPlateOpen(false); + const hitCount = filterVehiclesByFilters(MOCK_VEHICLES, next, kpiFilter).length; + if (plates.length) { + message.success(`已按 ${plates.length} 个车牌筛选,命中 ${hitCount} 条记录`); + } else { + message.success(`查询完成,命中 ${hitCount} 条记录`); + } + }; + + const handleListFilterReset = () => { + setListFilters({ ...DEFAULT_LIST_FILTERS }); + setAppliedFilters({ ...DEFAULT_LIST_FILTERS }); + setMultiPlateDraft(''); + setKpiFilter('total'); + message.info('筛选条件已重置'); + }; + + const handleMultiPlateOpenChange = (open) => { + setMultiPlateOpen(open); + if (open) setMultiPlateDraft(listFilters.plateNos || ''); + }; + + const renderFilterField = (label, control) => ( +
+ {label} +
{control}
+
+ ); + + const isRetiredVehicle = (record) => record?.status === '退出运营'; + + const renderInsuranceDateCell = (record, typeKey) => { + const ledgerKey = getVehicleLedgerKey(record); + const item = allInsurance[ledgerKey]?.[typeKey]; + const status = getInsuranceItemStatus(ledgerKey, typeKey); + const dateVal = item?.endDate; + const muted = isRetiredVehicle(record); + const policyTagEl = renderPolicyStatusTag(item); + return ( +
+
+ {dateVal || '—'} +
+ {!muted ? ( +
+ {policyTagEl || ( + + + + {getInsuranceRemainShortText(status)} + + )} + /> + + + )} +
+ ) : null} +
+ ); + }; + + const listColumns = [ + { + title: '车牌号', + dataIndex: 'plateNo', + key: 'plateNo', + width: 148, + onHeaderCell: listColumnHeaderCell, + align: 'left', + render: (plate, record) => { + const muted = isRetiredVehicle(record); + const noPlate = !hasVehiclePlate(record); + const displayPlate = formatVehiclePlateDisplay(plate); + const sub = [record.brand, record.model].filter(Boolean).join(' · '); + return ( +
+ + {displayPlate} + + {sub ? ( + + + {sub} + + + ) : null} +
+ ); + }, + }, + { + title: 'VIN码', + dataIndex: 'vin', + key: 'vin', + width: 112, + onHeaderCell: listColumnHeaderCell, + render: (vin, record) => ( + + + {vin} + + + ), + }, + { + title: '运营状态', + dataIndex: 'status', + key: 'status', + width: 80, + onHeaderCell: listColumnHeaderCell, + render: (status) => ( + + {status} + + ), + }, + { + title: '保险状态', + key: 'insuranceStatus', + width: 80, + onHeaderCell: listColumnHeaderCell, + render: (record) => { + const st = getVehicleInsuranceStatus(getVehicleLedgerKey(record)); + return ( + + + {st.label} + + + ); + }, + }, + { + title: '交强险到期日期', + key: 'compulsory', + width: 118, + onHeaderCell: listColumnHeaderCell, + render: (record) => renderInsuranceDateCell(record, 'compulsory'), + }, + { + title: '商业险到期日期', + key: 'commercial', + width: 118, + onHeaderCell: listColumnHeaderCell, + render: (record) => renderInsuranceDateCell(record, 'commercial'), + }, + { + title: '超赔险到期日期', + key: 'excess', + width: 118, + onHeaderCell: listColumnHeaderCell, + render: (record) => renderInsuranceDateCell(record, 'excess'), + }, + { + title: '货物险到期日期', + key: 'cargo', + width: 118, + onHeaderCell: listColumnHeaderCell, + render: (record) => renderInsuranceDateCell(record, 'cargo'), + }, + { + title: '驾意险到期日期', + key: 'driverAccident', + width: 118, + onHeaderCell: listColumnHeaderCell, + render: (record) => renderInsuranceDateCell(record, 'driverAccident'), + }, + { + title: '操作', + key: 'action', + width: 64, + onHeaderCell: listColumnHeaderCell, + render: (record) => ( + + ), + }, + ]; + + return ( +
+ + +
+
+
+
保险采购
+
保单管理(一车一档)· 比价单独立管理,互不关联
+
+ +
+ + +
+ {renderFilterField('车牌号', ( + 0} + value={listFilters.plateNo} + onChange={(e) => setListFilters((prev) => ({ ...prev, plateNo: e.target.value }))} + onPressEnter={handleListFilterQuery} + style={{ borderRadius: 8 }} + /> + ))} + {renderFilterField('多车牌', ( + +
每行一个车牌,或同一行内用逗号分隔
+ setMultiPlateDraft(e.target.value)} + placeholder={'沪A03561F\n粤B58888F'} + style={{ borderRadius: 8 }} + /> +
+ + +
+
+ )} + > + + + ))} + {renderFilterField('VIN码', ( + setListFilters((prev) => ({ ...prev, vin: e.target.value }))} + onPressEnter={handleListFilterQuery} + style={{ borderRadius: 8 }} + /> + ))} + {renderFilterField('品牌', ( + setListFilters((prev) => ({ ...prev, model: val || '' }))} + options={modelOptions} + filterOption={(input, option) => (option?.label || '').toLowerCase().includes(input.toLowerCase())} + style={{ width: '100%' }} + /> + ))} + {renderFilterField('运营状态', ( + + ))} + {renderFilterField('保险状态', ( + + ))} + {renderFilterField('保险类型', ( +
getVehicleLedgerKey(record)} + rowClassName={(record) => (record.status === '退出运营' ? 'lc-row-retired' : '')} + pagination={false} + scroll={{ x: 1040 }} + locale={{ + emptyText: ( +
+ +
暂无符合检索条件的保险台账车辆
+
+ ), + }} + /> + + + + + setVehicleInsMgmtOpen(false)}> + 关闭 + + )} + onCancel={() => setVehicleInsMgmtOpen(false)} + > + {vehicleInsMgmtVehicle ? (() => { + const profile = getVehicleProfile(vehicleInsMgmtVehicle); + const ledgerKey = getVehicleLedgerKey(vehicleInsMgmtVehicle); + const insStatus = getVehicleInsuranceStatus(ledgerKey); + const statusPillClass = insStatus.color === 'success' || insStatus.color === 'warning' + ? `lc-vehicle-ins-mgmt-status-pill--${insStatus.color}` + : 'lc-vehicle-ins-mgmt-status-pill--error'; + return ( +
+
+
+
+
车辆保险档案
+
{formatVehiclePlateDisplay(vehicleInsMgmtVehicle.plateNo)}
+
+ {vehicleInsMgmtVehicle.brand} {vehicleInsMgmtVehicle.model} +
+
+ + + 保险状态 · {insStatus.label} + + +
+
+
+
VIN码
+
{vehicleInsMgmtVehicle.vin || '—'}
+
+
+
运营状态
+
{vehicleInsMgmtVehicle.status || '—'}
+
+
+
客户
+
{profile.customer || '—'}
+
+
+
产权方
+
{profile.ownerCompany || '—'}
+
+
+
注册日期
+
{profile.regDate || '—'}
+
+
+
年审到期
+
{profile.inspectExpire || '—'}
+
+
+
+
+
+ {renderFilterField('保单号', ( + setVehicleInsMgmtPolicyNoFilter(e.target.value)} + onPressEnter={handleVehicleInsMgmtPolicyNoSearch} + style={{ borderRadius: 8 }} + /> + ))} + +
+ { + setVehicleInsMgmtActiveTab(key); + setVehicleInsMgmtHighlightId(''); + }} + type="card" + size="small" + > + + {!vehicleInsuranceHistory.timelinePolicy?.length + && !vehicleInsuranceHistory.timelineBiz?.length ? ( +
+
暂无全周期记录
+
可通过新增、续保或停保 / 复驶 / 退保操作产生记录
+
+ ) : ( + renderVehicleInsuranceCenterTimeline() + )} +
+ {VEHICLE_INSURANCE_MGMT_TABS.filter((t) => t.key !== 'timeline').map((tab) => ( + + {renderVehicleInsuranceTypeTab(tab.key)} + + ))} +
+
+
+ ); + })() : null} +
+ + { + setPolicyBizModalOpen(false); + setPolicyBizModalRecord(null); + setPolicyBizForm({ ...EMPTY_POLICY_BIZ_FORM }); + setPolicyBizAttachmentFileList([]); + }} + onOk={submitPolicyBizModal} + > + {policyBizModalRecord && vehicleInsMgmtVehicle ? ( + <> +
+
+
车牌号
+
{formatVehiclePlateDisplay(vehicleInsMgmtVehicle.plateNo)}
+
+
+
保单号
+
{policyBizModalRecord.policyNo || '—'}
+
+
+
保险公司
+
{policyBizModalRecord.company || '—'}
+
+
+
生效日期
+
{policyBizModalRecord.startDate || '—'}
+
+
+
到期日期
+
{policyBizModalRecord.endDate || '—'}
+
+
+
+ {policyBizModalMode === 'suspend' ? ( + <> + {renderFilterField('中止时间', ( + setPolicyBizForm((p) => ({ ...p, suspendTime: ds || '' }))} + /> + ))} + {renderFilterField('恢复时间', ( + setPolicyBizForm((p) => ({ ...p, resumeTime: ds || '' }))} + /> + ))} + {renderFilterField('新到期日期', ( + setPolicyBizForm((p) => ({ ...p, newEndDate: ds || '' }))} + /> + ))} +
+ 停保单附件 + 支持 PDF、图片,可上传多份 + false} + onChange={handlePolicyBizAttachmentChange} + > + + +
+ + ) : null} + {policyBizModalMode === 'resume' ? ( + <> + {renderFilterField('恢复时间', ( + setPolicyBizForm((p) => ({ ...p, resumeTime: ds || '' }))} + /> + ))} + {renderFilterField('新到期日期', ( + setPolicyBizForm((p) => ({ ...p, newEndDate: ds || '' }))} + /> + ))} +
+ 复驶单附件 + 支持 PDF、图片,可上传多份 + false} + onChange={handlePolicyBizAttachmentChange} + > + + +
+ + ) : null} + {policyBizModalMode === 'cancel' ? ( + <> + {renderFilterField('退保时间', ( + setPolicyBizForm((p) => ({ ...p, cancelTime: ds || '' }))} + /> + ))} + {renderFilterField('退还保费', ( + setPolicyBizForm((p) => ({ ...p, refundPremium: e.target.value }))} + placeholder="元" + /> + ))} +
+ 退保单附件 + 支持 PDF、图片,可上传多份 + false} + onChange={handlePolicyBizAttachmentChange} + > + + +
+ + ) : null} +
+ + ) : null} +
+ + { + setPolicyOpHistoryOpen(false); + setPolicyOpHistoryRecord(null); + }} + > + 关闭 + + )} + onCancel={() => { + setPolicyOpHistoryOpen(false); + setPolicyOpHistoryRecord(null); + }} + > +
{val || '—'}, + }, + { + title: '操作人', + dataIndex: 'operator', + width: 96, + ellipsis: true, + }, + { + title: '操作类型', + dataIndex: 'type', + width: 88, + render: (val) => INSURANCE_OPERATION_TYPE_LABEL[val] || val || '—', + }, + { + title: '备注', + dataIndex: 'remark', + width: 220, + ellipsis: true, + render: (val) => ( + + {val || '—'} + + ), + }, + { + title: '附件', + key: 'attachments', + width: 148, + render: (_, log) => { + const bizTypes = ['suspend', 'resume', 'cancel']; + if (!bizTypes.includes(log.type)) return ; + const attachments = log.attachments || []; + if (!attachments.length) { + return 无附件; + } + return ( +
+ {attachments.map((att) => ( + + + + + + + ))} +
+ ); + }, + }, + ]} + /> + + + setPrdOpen(false)} + > + + + +

一、需求背景与目标

+

+ 车队保险采购分散在 Excel、邮件与线下批单中,台账不全、续保临期易漏、停保退保无留痕、采购比价难追溯、交车合规无法实时校验。 + 本模块需在同一页面承载两条相互独立的业务线,技术实现上须避免强耦合与自动双向同步。 +

+
    +
  • 保单管理:建立一车一档台账,支撑 OCR/导入/手工录入及停保/复驶/退保变更,并与车辆管理交车合规联动。
  • +
  • 比价单:支撑采购前多方报价、批次管理、最晚付费预警与采购审批发起;审批结果回写购买记录,不自动写入保单台账
  • +
+ +

二、模块边界与外部依赖

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
依赖模块交互方式产品要求
车辆管理读取车辆档案;输出保险状态交车合规判定口径须与车辆管理「保险状态」字段一致,异常车辆禁止交车
审批中心发起采购审批;回写采购状态本页只读展示状态与当前审批人,不提供审批办理能力
OCR 服务保单/批单识别识别结果须人工确认后落库;不同业务类型识别字段不同(见第六节)
+

+ 关键约束:比价单引用车辆档案与台账到期日作只读参考;审批通过后运营人员须在保单管理侧另行录入正式保单。 +

+ +

三、核心数据实体(需持久化)

+
    +
  • 车辆保险台账(一车一档):以车牌或 VIN 为键;下设五类险种(交强、商业、超赔、货物、驾意),每险种独立存储保单要素、policyTag、操作日志、附件、历史归档保单。
  • +
  • 险种记录关键字段:保单号、保险公司、生效/到期日、保费、保险状态(正常/已停保/已退保)、停保中止/恢复时间、退保时间与退还保费、附件列表、operationLogs[]
  • +
  • 比价单:批次头(创建时间、创建人、备注、附件)+ 购买记录行数组。
  • +
  • 购买记录行:车辆标识、险种、投保方式、报价列表、最终比价结果 ID、最晚付费日、采购状态、提交时间、当前审批人。
  • +
  • 识别任务:任务级 mode(保单/停保/复驶/退保/导入)+ 多条待确认结果;确认后写入台账并标记任务状态。
  • +
+ +

四、保单管理 · 功能需求

+

4.1 列表与筛选

+
    +
  • 支持车牌、多车牌、VIN、品牌、型号、运营状态、保险状态(正常/异常)、险种、到期时间范围筛选。
  • +
  • 首页 KPI:台账车辆总数、保险状态正常、险种临期预警(30 天)、核心险种逾期、核心险种待购;点击 KPI 可筛选列表或打开预警弹窗。
  • +
  • 列表展示五类险种到期日;到期日单元格副文案展示临期/逾期天数或停保/退保标签。
  • +
+

4.2 车辆保险档案(管理弹窗)

+
    +
  • Tab1「全周期记录」:时间轴分左右——左侧新保/续保,右侧停保/复驶/退保。
  • +
  • Tab2~Tab6 按险种展示历史表;列顺序须包含:导入时间、类型、保单号、保险状态(保单号右侧)、保险公司、付款/生效/到期、金额、操作。
  • +
  • 保险状态枚举:正常(复驶后亦算正常)、已停保、已退保;已停保悬停展示中止时间、恢复时间、新到期日期。
  • +
  • 当前有效记录支持停保/复驶/退保;历史归档记录只读,不可办理业务变更。
  • +
+

4.3 录入通道

+
    +
  • 逐条新增、保单批量识别(OCR)、Excel 批量导入(仅新保/续保)。
  • +
  • 导入须自动跳过停保/复驶/退保类业务行。
  • +
  • 保单 OCR 确认页不展示承保险种明细、保险金额、免赔额、分项保费及操作列;上述字段仅在手工新增/台账编辑保留。
  • +
+ +

五、险种状态计算规则(须服务端统一实现)

+

单险种展示状态(列表副标签),按优先级自上而下命中即停止:

+
    +
  1. 已退保 → 已退保
  2. +
  3. 无保单号 → 未购买
  4. +
  5. 有保单号 + 停保标记 → 已停保
  6. +
  7. 有保单号无到期日 → 未购买
  8. +
  9. 到期日 ≤ 今天 → 已到期
  10. +
  11. 到期日 ≤ 今天+30 天 → 临期
  12. +
  13. 其余 → 正常
  14. +
+

车辆级保险状态(交车联动)

+
    +
  • 仅由交强险 + 商业险决定;二者均为正常或临期 → 车辆正常/临期,允许交车。
  • +
  • 任一为未购买或已到期 → 车辆异常,禁止交车;已退保视同无有效保障。
  • +
  • 超赔/货物/驾意仅参与「险种临期预警」统计,不参与交车判定。
  • +
+ +

六、OCR / 批单识别 · 字段与落库规则

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
业务类型识别/确认字段确认后写入匹配规则
保单录入车牌、车主、投保人、被保险人、保司、保单号、收费确认时间、生效/到期日、保费合计对应险种台账;清空停保/退保标记车牌/VIN 匹配车辆
停保保单号、中止时间、恢复时间、新到期日期到期日写新到期日期;标记已停保按保单号全库匹配
复驶保单号、恢复时间、新到期日期清除停保/退保标记;到期日更新;状态恢复为正常按保单号全库匹配
退保保单号、退保时间、退保金额标记已退保;清空到期日按保单号全库匹配
+ +

七、停保 / 复驶 / 退保 · 业务规则

+ + + + + + + + + + + + + +
当前记录类型允许操作
新保 / 续保(正常)停保、退保
已停保复驶、退保
已退保仅复驶
历史归档只读,不可操作
+
    +
  • 每次办理须写入 operationLogs:操作时间、操作人、类型、变更备注;停保/复驶/退保须支持上传附件。
  • +
  • 操作历史弹窗:停保/复驶/退保类日志须展示附件预览与下载入口;无附件时展示「无附件」。
  • +
  • 新保录入时若当前险种为已退保且有保单号,须将旧记录归档至 archivedPolicies 后写入新保单。
  • +
+ +

八、比价单 · 功能需求

+

8.1 比价单管理列表

+
    +
  • 支持按创建时间、车牌筛选。
  • +
  • 看板三项须可点击筛选下方列表:全部比价单最晚付费临期(批次内含距最晚付费日 ≤ 3 天的购买记录)、最晚付费超期(批次内含已过期购买记录)。数字统计为比价单批次数量,可与时间/车牌筛选叠加。
  • +
  • 列表字段:创建日期、创建人、总车辆(去重)、保险数量(行数)、附件数、已提交采购数量、审批通过数量。
  • +
+

8.2 编辑器 · 购买记录

+
    +
  • 每行关联车辆(车牌或 VIN 至少一项);选车后客户、品牌、车型、年检/交强/商业到期日只读带出。
  • +
  • 可编辑:投保方式、保险类型、最晚付费日期。有交强或商业到期日默认续保,否则新保。
  • +
  • 修改保险类型须清空该行全部报价与最终比价结果。
  • +
  • 每行须录入至少一条报价(保司+金额),提交前须确定唯一「最终比价结果」。
  • +
  • 最晚付费日展示标签:剩余天数 / 临期(≤3天)/ 超期。
  • +
+

8.3 保存与提交校验

+ + + + + + + + + + + + + + + + + +
动作校验条件
保存比价单 + ≥1 条购买记录;每行已选车辆;整单备注非空;整单 ≥1 个附件 +
提交采购申请 + ≥1 条勾选行;勾选行均有最终比价结果与最晚付费日;采购状态为未提交/撤回/审批驳回;满足保存条件;未保存须先提示保存 +
+ +

九、采购状态机(购买记录行级)

+ + + + + + + + + + + + + + + +
状态可否再次提交写入方UI 约束
未提交可以默认
审批中不可以本页提交多选框禁用
审批通过不可以审批中心回写多选框禁用
撤回 / 审批驳回可以审批中心回写
+

+ 当前审批人由审批流回写,允许为空;本页不提供审批/撤回/驳回操作。 +

+ +

十、预警与一键生成

+
    +
  • 台账 KPI:险种临期 = 任一类险种 30 天内到期;核心逾期 = 交强或商业已过期;临期弹窗展示比价采购状态标签。
  • +
  • 一键生成比价单:从临期/逾期预警勾选险种带入购买记录;自动填备注;附件须用户补传;跳过已有审批中/审批通过的同车同险种记录。
  • +
  • 比价看板:临期阈值 3 天;点击看板卡片即时筛选列表,无需二次点查询。
  • +
+ +

十一、验收标准(研发自测 + 产品验收)

+
    +
  1. 五类险种表格在保单号右侧正确展示保险状态;复驶后显示「正常」。
  2. +
  3. 停保/复驶/退保办理后,操作历史可预览与下载对应附件。
  4. +
  5. 停保/复驶/退保 OCR 仅展示约定字段,确认后按保单号匹配台账并正确更新到期日与状态。
  6. +
  7. 保单 OCR 确认页不展示险种明细表;手工编辑路径仍保留明细能力。
  8. +
  9. 交强+商业均有效时车辆保险状态正常/临期,与车辆管理交车规则一致。
  10. +
  11. 比价单保存/提交校验按第八节执行;审批中/通过行不可重复勾选提交。
  12. +
  13. 比价单管理看板「全部/临期/超期」点击后列表筛选结果正确,可与创建时间/车牌叠加。
  14. +
  15. 审批通过后比价单数据不自动写入保单台账;须通过保单管理独立录入。
  16. +
+
+ ), + }, + { + key: 'manual', + label: '操作手册', + children: ( +
+ + +

提交前自查(勾选行须全部满足)

+
    +
  • 已设最终比价结果
  • +
  • 已填最晚付费日期
  • +
  • 采购状态为「未提交 / 撤回 / 审批驳回」(审批中、审批通过不可选)
  • +
  • 比价单已填备注并上传附件
  • +
+

金额栏

+
    +
  • 左:当前保单总金额 — 全单已确认报价合计
  • +
  • 右:已选保单总金额 — 勾选待提交行合计
  • +
+

常见情况

+
    +
  • 一键生成后须补报价、附件,再保存提交。
  • +
  • 撤回/驳回后:重新勾选该行 → 再次提交采购申请。
  • +
  • 比价单管理列表可筛创建时间/车牌,查看临期/超期看板。
  • +
+ + ), + }, + ]} + /> + + + setCompareMgmtOpen(false)} + > +
+ {renderFilterField('创建时间', ( + setCompareMgmtFilters((prev) => ({ ...prev, createdRange: range }))} + placeholder={['开始日期', '结束日期']} + allowClear + /> + ))} + {renderFilterField('车牌号', ( + setCompareMgmtFilters((prev) => ({ ...prev, plateNo: e.target.value }))} + onPressEnter={handleCompareMgmtQuery} + style={{ borderRadius: 8 }} + /> + ))} +
+
+ + +
+
+
handleCompareMgmtPayAlertFilter('all')} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleCompareMgmtPayAlertFilter('all'); + } + }} + > +
+
全部比价单
+
显示符合筛选条件的全部批次
+
+ {compareMgmtPayAlerts.total} +
+
handleCompareMgmtPayAlertFilter('warning')} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleCompareMgmtPayAlertFilter('warning'); + } + }} + > +
+
最晚付费临期
+
含临期购买记录的比价单(≤ {LATEST_PAY_WARN_DAYS} 天)
+
+ {compareMgmtPayAlerts.warning} +
+
handleCompareMgmtPayAlertFilter('overdue')} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleCompareMgmtPayAlertFilter('overdue'); + } + }} + > +
+
最晚付费超期
+
含超期购买记录的比价单
+
+ {compareMgmtPayAlerts.overdue} +
+
+
+ + 共 {filteredCompareSheets.length} 条比价单 + + +
+
`共 ${t} 条` }} + scroll={{ x: 1040 }} + locale={{ emptyText: '暂无比价单,请点击「新建比价单」' }} + columns={[ + { + title: '创建日期', + dataIndex: 'createdAt', + key: 'createdAt', + width: 168, + render: (val) => {val || '—'}, + }, + { + title: '创建人', + dataIndex: 'createdBy', + key: 'createdBy', + width: 96, + render: (val) => val || '—', + }, + { + title: '总车辆', + dataIndex: 'totalVehicles', + key: 'totalVehicles', + width: 88, + align: 'center', + render: (val) => {val ?? 0}, + }, + { + title: '保险数量', + dataIndex: 'insuranceCount', + key: 'insuranceCount', + width: 96, + align: 'center', + render: (val) => {val ?? 0}, + }, + { + title: '附件', + key: 'attachments', + width: 72, + align: 'center', + render: (_, record) => { + const n = record.attachments?.length || 0; + return n > 0 ? ( + a.name).join('、')}> + {n} + + ) : ( + + ); + }, + }, + { + title: '已提交采购数量', + dataIndex: 'submittedProcurementCount', + key: 'submittedProcurementCount', + width: 128, + align: 'center', + render: (val) => ( + 0 ? 'processing' : 'default'} style={{ margin: 0, fontWeight: 600 }}> + {val ?? 0} + + ), + }, + { + title: '审批通过数量', + dataIndex: 'approvedCount', + key: 'approvedCount', + width: 120, + align: 'center', + render: (val) => ( + 0 ? 'success' : 'default'} style={{ margin: 0, fontWeight: 600 }}> + {val ?? 0} + + ), + }, + { + title: '操作', + key: 'action', + width: 120, + fixed: 'right', + render: (_, record) => ( + + + + + ), + }, + ]} + /> + + + { + setCompareModalOpen(false); + setEditingCompareSheetId(null); + }} + footer={( +
+ + 勾选购买记录后可提交采购申请;须新增报价、设为最终比价结果并填写最晚付费日期 + +
+ + + +
+
+ )} + > +
+ {renderFilterField('车辆', ( + +
+ 支持多辆车车牌号、车辆识别代码,每行一条;可从 Excel 等批量复制粘贴,点击「收起」后列表展示全部命中记录。 +
+ setCompareVehicleFilterDraft(e.target.value)} + placeholder={'沪A03561F\n粤B58888F\nLB9A32A21R0LS1478'} + autoSize={{ minRows: 5, maxRows: 10 }} + style={{ borderRadius: 8, fontFamily: 'monospace', fontSize: 13 }} + /> +
+ + +
+
+ )} + > + setCompareVehicleFilterOpen(true)} + onClear={(e) => { + e.stopPropagation(); + handleCompareVehicleFilterClear(); + }} + style={{ borderRadius: 8 }} + suffix={( + + + + )} + /> + + ))} +
+ setCompareEditorFilters((prev) => ({ ...prev, latestPayWithin3Days: e.target.checked }))} + > + + 仅显示最晚付费日期 {LATEST_PAY_WARN_DAYS} 天内的记录 + + +
+
+ +
+ +
+
setCompareEditorFilters((prev) => ({ ...prev, insuranceType: '' }))} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + setCompareEditorFilters((prev) => ({ ...prev, insuranceType: '' })); + } + }} + > + {compareRows.length} + 全部 +
+ {QUOTE_INSURANCE_TYPES.map((typeLabel) => ( +
setCompareEditorFilters((prev) => ({ + ...prev, + insuranceType: typeLabel, + }))} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + setCompareEditorFilters((prev) => ({ ...prev, insuranceType: typeLabel })); + } + }} + > + {compareEditorTypeCounts[typeLabel] ?? 0} + {typeLabel} +
+ ))} +
+ +
+ + + + {selectedCompareKeys.length ? ( + + ) : null} + + + {isCompareEditorFiltered ? ( + <>筛选显示 {displayCompareRows.length} / {compareRows.length} 条购买记录 + ) : ( + <>共 {compareRows.length} 条购买记录 + )} + +
+
+
({ + disabled: isCompareProcurementSelectionDisabled(record.procurementStatus), + }), + }} + locale={{ emptyText: '暂无购买记录,请点击「新增一行」或「批量新增」' }} + /> + +
+
+ + setCompareRemark(e.target.value)} + placeholder="请填写比价说明、采购要求等" + maxLength={500} + showCount + style={{ borderRadius: 8 }} + /> +
+
+ 附件 + 必填,不限制文件格式与上传数量;保存比价单时一并存储附件信息(原型仅存元数据) + false} + onChange={handleCompareAttachmentChange} + itemRender={(originNode, file) => ( + + {originNode} + + )} + > + + +
+
+
+ 当前保单总金额 + + {compareSheetSummary.total.toFixed(2)} + + + + 全单已确认 {compareSheetSummary.count} 项 + +
+
+ 已选保单总金额 + + {selectedProcurementSummary.total.toFixed(2)} + + + + 已勾选 {selectedCompareKeys.length} 条 · 可提交 {selectedProcurementSummary.count} 项确认报价 + +
+
+
+ + + { + setCompareBatchAddOpen(false); + setCompareBatchAddDraft(''); + }} + onOk={handleConfirmBatchAddCompareRows} + > +
+ 每行输入一个车牌号或车辆识别代码(VIN),确认后按行数生成购买记录并自动带出车辆信息。 + {compareEditorFilters.insuranceType ? ( + + 当前已选险种筛选「{compareEditorFilters.insuranceType}」,新增记录将默认使用该险种。 + + ) : null} +
+ setCompareBatchAddDraft(e.target.value)} + placeholder={'沪A03561F\n粤B58888F\nLMRKH9AC0R1004086'} + style={{ borderRadius: 8, fontFamily: 'ui-monospace, monospace', fontSize: 13 }} + /> +
+ 已输入 {parseBatchVehicleLines(compareBatchAddDraft).length} 条 +
+
+ + setPolicyRecognTasksOpen(false)} + > +
+ {renderFilterField('创建时间', ( + setPolicyRecognTasksFilters((prev) => ({ ...prev, createdRange: range }))} + placeholder={['开始日期', '结束日期']} + allowClear + /> + ))} + {renderFilterField('业务类型', ( +
`共 ${t} 条` }} + scroll={{ x: 1080 }} + locale={{ emptyText: '暂无识别任务,请发起「保单批量识别」' }} + columns={[ + { + title: '创建时间', + dataIndex: 'createdAt', + width: 168, + render: (val) => {val || '—'}, + }, + { + title: '操作人', + dataIndex: 'creator', + width: 96, + ellipsis: true, + }, + { + title: '业务类型', + dataIndex: 'modeLabel', + width: 96, + render: (val) => val || '—', + }, + { + title: '识别成功数', + dataIndex: 'recognSuccessCount', + width: 96, + align: 'center', + render: (val) => ( + + {val ?? 0} + + ), + }, + { + title: '识别失败数', + dataIndex: 'recognFailCount', + width: 96, + align: 'center', + render: (val) => ( + 0 ? '#dc2626' : '#64748b', fontWeight: 600 }}> + {val ?? 0} + + ), + }, + { + title: '识别进度', + key: 'recognProgress', + width: 132, + render: (_, record) => { + const total = record.totalFileCount || record.fileCount || 0; + const done = record.recognDoneCount ?? (isPolicyRecognTaskRecognizing(record) ? 0 : total); + const recognizing = isPolicyRecognTaskRecognizing(record); + const percent = total > 0 ? Math.round((done / total) * 100) : 0; + return ( +
+ + + {total > 0 ? `${done}/${total}` : '—'} + +
+ ); + }, + }, + { + title: '确认进度', + key: 'confirmProgress', + width: 96, + align: 'center', + render: (_, record) => ( + + {record.confirmedCount}/{record.recognSuccessCount || 0} + + ), + }, + { + title: '操作', + key: 'action', + width: 120, + fixed: 'right', + render: (_, record) => { + const recognizing = isPolicyRecognTaskRecognizing(record); + const noSuccess = !(record.recognSuccessCount > 0); + const disabled = recognizing || noSuccess; + const btn = ( + + ); + if (recognizing) { + return ( + + {btn} + + ); + } + return btn; + }, + }, + ]} + /> + + + + {policyRecognPhase === 'upload' ? ( + <> + {policyRecognEntry === 'ocr' ? ( + <> + +
+
业务类型
+ setPolicyRecognMode(e.target.value)} + style={{ display: 'flex', flexDirection: 'column', gap: 8 }} + > + {POLICY_OCR_MODES.map((m) => ( + + {m.label} + {m.desc} + + ))} + +
+ {policyRecognMode === 'policy' ? ( +
+ {renderFilterField('保险类型', ( +
5 ? { pageSize: 5, showSizeChanger: false } : false} + scroll={{ x: 560 }} + columns={policyRecognPickerColumns} + onRow={(record) => ({ + onClick: () => selectPolicyRecognResult(record.id), + className: [ + record.id === policyRecognActiveResultId ? 'lc-policy-recogn-picker-row--active' : '', + record.confirmed ? 'lc-policy-recogn-picker-row--confirmed' : '', + ].filter(Boolean).join(' '), + })} + /> +
+
+
+ 保单预览 · {policyPreview?.fileName || '—'} +
+
+
+ {policyPreview?.isImage && policyPreview.url ? ( + {policyPreview.fileName} + ) : ( +
+
PDF
+
{policyPreview?.fileName || '暂无预览'}
+
{policyPreview?.hint}
+
+ )} +
+
+
+
+
+ {activePolicyRecognMode === 'suspend' ? '确认识别内容(停保)' + : activePolicyRecognMode === 'resume' ? '确认识别内容(复驶)' + : activePolicyRecognMode === 'cancel' ? '确认识别内容(退保)' + : '确认识别内容'} +
+ {policyRecognActiveResultId ? ( + renderPolicyDetailForm(policyRecognConfirmDraft, setPolicyRecognConfirmDraft, { + showBizType: false, + recognConfirmMode: true, + bizRecognMode: activePolicyRecognMode, + onPlateChange: handleRecognConfirmPlateChange, + recognResult: policyRecognActiveResult, + }) + ) : ( +
+ 请从上方列表选择一条识别记录 +
+ )} +
+
+
+ {!policyRecognViewOnly ? ( + + ) : null} + {!policyRecognViewOnly ? ( + + ) : null} + {!policyRecognViewOnly ? ( + + ) : null} + +
+ + ) : null} + + + { + setVehicleInsHistoryEditOpen(false); + setVehicleInsHistoryEditRecord(null); + }} + onOk={saveVehicleInsHistoryEdit} + okText="保存" + cancelText="取消" + > + + {renderPolicyDetailForm(vehicleInsHistoryEditDraft, setVehicleInsHistoryEditDraft)} + + + { + setPolicyAddOpen(false); + setPolicyAddAttachmentFileList([]); + }} + onOk={handlePolicyAddSubmit} + okText="保存" + cancelText="取消" + > + + {renderPolicyDetailForm(policyAddDraft, setPolicyAddDraft, { showBizType: false, policyEntryMode: true })} +
+ 保单附件 + 支持 PDF、图片格式,可上传多份;保存时一并存入保单档案(原型仅存元数据) + false} + onChange={handlePolicyAddAttachmentChange} + itemRender={(originNode, file) => ( + + {originNode} + + )} + > + + +
+
+ + + + 默认按商业险到期日期降序;点击表头可切换各险种升序/降序 + + + + + + + )} + onCancel={() => setInsuranceAlertOpen(false)} + > +
+
+ {insuranceAlertMode === 'coreExpired' + ? '逾期险种筛选(交强险、商业险)' + : '临期险种筛选(默认全选 5 类)'} +
+ CORE_INSURANCE_KEYS.includes(item.key)) + : INSURANCE_TYPE_ITEMS + ).map((item) => ({ label: item.fullLabel, value: item.key }))} + value={insuranceAlertTypeFilter} + onChange={(vals) => setInsuranceAlertTypeFilter(vals)} + /> +
+
+ + {insuranceAlertMode === 'coreExpired' ? ( + <> + 共 {insuranceAlertSortedList.length} 辆核心险种逾期 + (交强险或商业险已到期,禁止交车) + + ) : ( + <> + 共 {insuranceAlertSortedList.length} 辆存在临期记录 + (含临期 ≤ {INSURANCE_WARN_DAYS} 天及已到期) + + )} + +
+ +
getVehicleLedgerKey(record)} + size="small" + bordered + pagination={{ pageSize: 10, showSizeChanger: true, pageSizeOptions: ['10', '20', '50'] }} + scroll={{ x: 940 }} + dataSource={insuranceAlertSortedList} + columns={insuranceAlertColumns} + onChange={handleInsuranceAlertTableChange} + locale={{ + emptyText: insuranceAlertMode === 'coreExpired' + ? '当前筛选条件下暂无核心险种逾期记录' + : '当前筛选条件下暂无临期记录', + }} + /> + + + setBatchCompareTypesOpen(false)} + onOk={handleConfirmBatchCompareSheets} + > + +
选择要生成的保险类型
+ INSURANCE_KEY_TO_LABEL[k]) + : QUOTE_INSURANCE_TYPES + ).map((t) => ({ label: t, value: t }))} + value={batchCompareTypesDraft} + onChange={(vals) => setBatchCompareTypesDraft(vals)} + /> +
+ + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-business/pages/02-编辑交车任务.jsx b/src/prototypes/oneos-web-business/pages/02-编辑交车任务.jsx new file mode 100644 index 0000000..eb51dbe --- /dev/null +++ b/src/prototypes/oneos-web-business/pages/02-编辑交车任务.jsx @@ -0,0 +1,344 @@ +// 【重要】必须使用 const Component 作为组件变量名 - Axhub 产品原型 +// 数字化资产ONEOS运管平台 - 编辑交车任务模块(布局同新增交车任务,项目名称禁用不可改) + +const Component = function() { + var antd = window.antd; + var Input = antd.Input; + var Select = antd.Select; + var Button = antd.Button; + var DatePicker = antd.DatePicker; + var Modal = antd.Modal; + var message = antd.message; + var Option = Select.Option; + var Checkbox = antd.Checkbox; + var Tooltip = antd.Tooltip; + var RangePicker = DatePicker.RangePicker; + + var projectId = React.useState('p1'); + var selectedProjectId = projectId[0]; + var setSelectedProjectId = projectId[1]; + + var expectedDelivery = React.useState(function() { + if (typeof window !== 'undefined' && window.dayjs) { + return [window.dayjs('2026-03-01'), window.dayjs('2026-03-05')]; + } + return null; + }); + var expectedDeliveryValue = expectedDelivery[0]; + var setExpectedDelivery = expectedDelivery[1]; + + var billingDate = React.useState(function() { + if (typeof window !== 'undefined' && window.dayjs) { + return window.dayjs('2026-03-06'); + } + return null; + }); + var billingDateValue = billingDate[0]; + var setBillingDate = billingDate[1]; + + var noReturnCarCheckedState = React.useState(false); + var noReturnCarChecked = noReturnCarCheckedState[0]; + var setNoReturnCarChecked = noReturnCarCheckedState[1]; + + var selectedRowKeys = React.useState(['v3', 'v4']); + var checkedRowKeys = selectedRowKeys[0]; + var setCheckedRowKeys = selectedRowKeys[1]; + + var formErrors = React.useState({}); + var errors = formErrors[0]; + var setErrors = formErrors[1]; + + var reqModalOpen = React.useState(false); + + // Mock:项目列表及车辆(与新增交车任务一致,含续签项目) + var projectList = [ + { id: 'p1', name: '嘉兴某某物流氢能运输项目', contractCode: 'JXZL20260216YW101235A', customerName: '嘉兴某某物流有限公司', deliveryRegion: '浙江省 / 嘉兴市', deliveryLocation: '浙江省嘉兴市南湖区科技大道1号', vehicles: [ + { key: 'v1', seq: 1, brand: '东风', model: '氢燃料电池重卡 H31', plateNo: '浙A10001', vin: 'LFV2BJCH8K3123456', monthRent: '12800', serviceFee: '800', deposit: '30000', remark: '首车', deliveryStatus: 'submitted' }, + { key: 'v2', seq: 2, brand: '东风', model: '氢燃料电池重卡 H31', plateNo: '浙A10002', vin: 'LFV2BJCH8K3123457', monthRent: '12800', serviceFee: '800', deposit: '30000', remark: '', deliveryStatus: 'submitted' }, + { key: 'v3', seq: 3, brand: '福田', model: '智蓝氢能轻卡', plateNo: '', vin: 'LZYTBACR2M1234567', monthRent: '8500', serviceFee: '500', deposit: '20000', remark: '待上牌', deliveryStatus: 'none' }, + { key: 'v4', seq: 4, brand: '重汽', model: '豪沃氢能牵引车', plateNo: '浙F20001', vin: 'ZZ4257N386FZ12345', monthRent: '15000', serviceFee: '1000', deposit: '35000', remark: '', deliveryStatus: 'none' }, + { key: 'v5', seq: 5, brand: '陕汽', model: '德龙氢能自卸', plateNo: '浙F20002', vin: 'SX1313GR456123456', monthRent: '13200', serviceFee: '880', deposit: '32000', remark: '固定线路', deliveryStatus: 'none' } + ]}, + { id: 'p1_r', name: '嘉兴某某物流氢能运输项目(续签合同)', isRenewal: true, contractCode: 'JXZL20270116YW101235B', customerName: '嘉兴某某物流有限公司', deliveryRegion: '浙江省 / 嘉兴市', deliveryLocation: '浙江省嘉兴市南湖区科技大道1号', vehicles: [ + { key: 'v1r_1', seq: 1, brand: '东风', model: '氢燃料电池重卡 H31', plateNo: '浙A10001', vin: 'LFV2BJCH8K3123456', monthRent: '12800', serviceFee: '800', deposit: '30000', remark: '续签合同车辆', deliveryStatus: 'none' }, + { key: 'v1r_0', seq: 2, brand: '东风', model: '氢燃料电池重卡 H31', plateNo: '浙A09999', vin: 'LFV2BJCH8K3000000', monthRent: '12800', serviceFee: '800', deposit: '30000', remark: '续签:已交车车辆样例', deliveryStatus: 'completed' }, + { key: 'v1r_0b', seq: 3, brand: '福田', model: '智蓝氢能轻卡', plateNo: '浙A08888', vin: 'LZYTBACR2M1000001', monthRent: '8500', serviceFee: '500', deposit: '20000', remark: '续签:已交车车辆样例', deliveryStatus: 'completed' }, + { key: 'v1r_0c', seq: 4, brand: '重汽', model: '豪沃氢能牵引车', plateNo: '浙A07777', vin: 'ZZ4257N386FZ00002', monthRent: '15000', serviceFee: '1000', deposit: '35000', remark: '续签:已交车车辆样例', deliveryStatus: 'completed' }, + { key: 'v1r_2', seq: 5, brand: '福田', model: '智蓝氢能轻卡', plateNo: '', vin: 'LZYTBACR2M1234567', monthRent: '8500', serviceFee: '500', deposit: '20000', remark: '续签待上牌', deliveryStatus: 'none' } + ]}, + { id: 'p2', name: '上海某某运输氢能租赁项目', contractCode: 'SHZL20260201YW200123A', customerName: '上海某某运输公司', deliveryRegion: '上海市 / 上海市', deliveryLocation: '上海市浦东新区张江高科技园区', vehicles: [ + { key: 'v6', seq: 1, brand: '上汽红岩', model: '杰狮氢能牵引', plateNo: '沪A30003', vin: 'SH1313HY789012345', monthRent: '14500', serviceFee: '950', deposit: '34000', remark: '', deliveryStatus: 'submitted' }, + { key: 'v7', seq: 2, brand: '宇通', model: '氢能公交 ZK6126', plateNo: '沪B40001', vin: 'LZYTAGCF8K4567890', monthRent: '22000', serviceFee: '1200', deposit: '50000', remark: '示范线路', deliveryStatus: 'none' }, + { key: 'v8', seq: 3, brand: '福田', model: '欧辉氢能大巴', plateNo: '', vin: 'LZYTBACR2M2345678', monthRent: '19800', serviceFee: '1100', deposit: '45000', remark: '', deliveryStatus: 'none' } + ]}, + { id: 'p3', name: '杭州某某租赁氢能项目', contractCode: 'HZZL20260115YW100089A', customerName: '杭州某某租赁有限公司', deliveryRegion: '浙江省 / 杭州市', deliveryLocation: '浙江省杭州市余杭区未来科技城', vehicles: [ + { key: 'v9', seq: 1, brand: '品牌C', model: '型号C1', plateNo: '浙A40004', vin: 'L4234567890ABCDEF', monthRent: '8200', serviceFee: '450', deposit: '20000', remark: '重点客户', deliveryStatus: 'completed' }, + { key: 'v10', seq: 2, brand: '品牌C', model: '型号C2', plateNo: '', vin: 'L5234567890ABCDEF', monthRent: '7800', serviceFee: '420', deposit: '19000', remark: '', deliveryStatus: 'submitted' }, + { key: 'v11', seq: 3, brand: '东风', model: '氢燃料电池厢货', plateNo: '浙A50001', vin: 'LFV2BJCH8K5678901', monthRent: '9200', serviceFee: '520', deposit: '22000', remark: '城配', deliveryStatus: 'none' }, + { key: 'v12', seq: 4, brand: '开沃', model: '创源氢能轻卡', plateNo: '浙A50002', vin: 'LJXTBACR9N6789012', monthRent: '8800', serviceFee: '480', deposit: '21000', remark: '', deliveryStatus: 'none' } + ]} + ]; + + var selectedProject = projectList.find(function(p) { return p.id === selectedProjectId; }); + var isRenewalProject = !!(selectedProject && selectedProject.isRenewal); + var vehicleListRaw = selectedProject ? selectedProject.vehicles : []; + var vehicleList = vehicleListRaw.filter(function(v) { + if (v.deliveryStatus === 'completed') { if (!isRenewalProject) return false; return true; } + if (v.deliveryStatus === 'submitted') return true; + if (isRenewalProject && !!noReturnCarChecked) return false; + return true; + }); + var selectableVehicles = vehicleList.filter(function(v) { + if (v.deliveryStatus === 'submitted') return false; + if (v.deliveryStatus === 'completed') return isRenewalProject && !!noReturnCarChecked; + return true; + }); + + var handleProjectChange = function(id) { + setSelectedProjectId(id || ''); + setCheckedRowKeys([]); + setNoReturnCarChecked(false); + }; + + var todayStr = (function() { + var d = new Date(); + return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); + })(); + + var validateExpectedDelivery = function() { + if (!expectedDeliveryValue || !expectedDeliveryValue.length) return '请选择预计交车日期'; + var start = expectedDeliveryValue[0] && expectedDeliveryValue[0].format ? expectedDeliveryValue[0].format('YYYY-MM-DD') : null; + var end = expectedDeliveryValue[1] && expectedDeliveryValue[1].format ? expectedDeliveryValue[1].format('YYYY-MM-DD') : null; + if (!start) return '请选择预计交车日期'; + if (end && end < start) return '结束日期不能早于开始日期'; + if (end && end < todayStr) return '结束日期不能早于当前日期'; + return null; + }; + + var needExpectedDelivery = !(isRenewalProject && noReturnCarChecked); + var expectedDeliveryError = needExpectedDelivery ? validateExpectedDelivery() : null; + + var billingDateError = !billingDateValue ? '请选择开始计费日期' : null; + + var handleSubmit = function() { + var err = {}; + if (!selectedProjectId) err.projectName = '请选择项目名称'; + if (needExpectedDelivery && expectedDeliveryError) err.expectedDelivery = expectedDeliveryError; + if (billingDateError) err.billingDate = billingDateError; + if (checkedRowKeys.length === 0 && selectableVehicles.length > 0) err.vehicles = '请至少选择一辆车'; + setErrors(err); + if (Object.keys(err).length) return; + message.success('交车任务已保存。'); + }; + + var onToggleNoReturnCar = function(e) { + var next = !!(e && e.target ? e.target.checked : e); + setNoReturnCarChecked(next); + if (!next) { + setCheckedRowKeys(function(prev) { + var completedKeys = vehicleList.filter(function(v) { return v && v.deliveryStatus === 'completed'; }).map(function(v) { return v.key; }); + return prev.filter(function(k) { return completedKeys.indexOf(k) === -1; }); + }); + } + }; + + var handleCancel = function() { + message.info('取消'); + }; + + var onSelectAll = function(checked) { + if (checked) setCheckedRowKeys(selectableVehicles.map(function(v) { return v.key; })); + else setCheckedRowKeys([]); + }; + + var onSelectRow = function(record, checked) { + if (checked) setCheckedRowKeys(function(prev) { return prev.indexOf(record.key) === -1 ? prev.concat(record.key) : prev; }); + else setCheckedRowKeys(function(prev) { return prev.filter(function(k) { return k !== record.key; }); }); + }; + + var styles = { + page: { padding: '16px 24px 48px', backgroundColor: '#f5f5f5', minHeight: '100vh', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', fontSize: 14 }, + breadcrumb: { marginBottom: 16, color: '#666' }, + breadcrumbSep: { margin: '0 8px', color: '#999' }, + card: { backgroundColor: '#fff', borderRadius: 8, marginBottom: 16, boxShadow: '0 1px 2px rgba(0,0,0,0.05)', overflow: 'hidden' }, + cardHeader: { padding: '16px 20px', borderBottom: '1px solid #f0f0f0' }, + cardTitle: { fontSize: 16, fontWeight: 600, color: '#333' }, + cardBody: { padding: '20px 24px' }, + formRow: { display: 'flex', flexWrap: 'wrap', marginBottom: 16 }, + formCol: { flex: '0 0 33.33%', minWidth: 200, paddingRight: 16, marginBottom: 8 }, + label: { display: 'block', marginBottom: 6, color: '#333' }, + labelRequired: { color: '#ff4d4f', marginRight: 4 }, + errMsg: { color: '#ff4d4f', fontSize: 12, marginTop: 4 }, + footer: { position: 'fixed', bottom: 0, left: 0, right: 0, padding: '12px 24px', backgroundColor: '#fff', borderTop: '1px solid #e8e8e8', display: 'flex', gap: 12, justifyContent: 'flex-start', zIndex: 99 }, + tableWrap: { marginTop: 16, overflowX: 'auto' }, + table: { width: '100%', borderCollapse: 'collapse', fontSize: 13 }, + th: { padding: '10px 8px', textAlign: 'left', borderBottom: '1px solid #e8e8e8', backgroundColor: '#fafafa', fontWeight: 600 }, + td: { padding: '8px', borderBottom: '1px solid #f0f0f0', verticalAlign: 'middle' }, + inputDisabled: { width: '100%', padding: '6px 10px', border: '1px solid #d9d9d9', borderRadius: 4, backgroundColor: '#f5f5f5', color: '#666', fontSize: 13 }, + modalMask: { position: 'fixed', left: 0, top: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.45)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center' }, + modalBox: { backgroundColor: '#fff', borderRadius: 8, width: '90%', maxWidth: 640, maxHeight: '85vh', overflow: 'hidden', display: 'flex', flexDirection: 'column', boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }, + modalHeader: { padding: '16px 20px', borderBottom: '1px solid #f0f0f0', fontSize: 16, fontWeight: 600 }, + modalBody: { padding: 20, overflow: 'auto', flex: 1 } + }; + + var reqSpecText = '编辑交车任务\n一个「数字化资产ONEOS运管平台」中的「交车任务」「编辑」模块\n1.面包屑:\n1.1.业务管理-交车任务-编辑\n\n2.表单:\n2.1.选择项目名称:必选项,反写新增时写入的数据,选择器,默认提示文本:请选择或输入项目名称,支持从输入框内输入内容进行模糊搜索,对应自营合同、租赁合同-「项目名称」字段;;\n2.2.合同编码:输入框(禁用状态),根据所选项目名称自动反写合同编码;\n2.3.客户名称:输入框(禁用状态),根据所选项目名称自动反写客户名称;\n2.4.交车区域:输入框(禁用状态),根据所选项目名称自动反写交车区域。提交时根据交车区域,为对应区域运维人员分配对应交车任务;\n2.5.交车地点:输入框(禁用状态),根据所选项目名称自动反写交车地点;\n2.6.预计交车日期:必填项,反写新增时写入的数据,日期选择器,支持某天或某个时间段两种模式,格式为YYYY-MM-DD或YYYY-MM-DD至YYYY-MM-DD,结束日期不能早于开始日期,并且结束日期不能早于当前日期;\n 2.6.1.如勾选不需要还车,则预计交车日期隐藏,不需要设置;\n2.7.开始计费日期:必填项,反写新增时写入的数据,日期选择器,支持单日选择,格式为YYYY-MM-DD;\n2.8.已交车辆是否需要还车:勾选框,反写新增时写入的数据,内容为不需要还车,默认不勾选,不勾选时,无法在车辆列表勾选已交未还的车辆,勾选后,已交未还的车辆也可以勾选,但不会生成新交车任务,只是按照开始计费日期生成账单;\n 2.8.1.勾选时,车辆列表只显示已交未还的车辆清单,不能添加新车(避免已交未还部分车辆开始计费日期计算不精确);\n 2.8.2.不勾选时,车辆列表显示已交未还的车辆清单及续签合同新增车辆,但是已交未还部分的车辆不可勾选;\n2.9.下方为列表,列表拉取该车辆租赁合同对应所有车辆信息(该合同下已提交过交车任务的车辆不可选,已完成交车的车辆不显示在列表中),列表字段为:全选/多选、序号、品牌、型号、车牌号、车辆识别代码、车辆月租金、服务费、保证金、备注;\n 2.9.1.序号:与租赁合同车辆序号对应;\n 2.9.2.全选/多选:支持全选、多选模式,反写新增时写入的数据,选择对应车辆后,点击提交自动生成被选中车辆交车任务,需要至少选择1辆,才能进行提交,该合同下已提交过交车任务和已交车的车辆不可选,多选框为禁用状态,悬浮时提示:该车辆已完成交车/该车辆已创建交车任务;\n 2.9.3.品牌:输入框(禁用状态),根据所选项目名称自动反写品牌;\n 2.9.4.型号:输入框(禁用状态),根据所选项目名称自动反写型号;\n 2.9.5.车牌号:输入框(禁用状态),根据所选项目名称自动反写车牌号,车牌号可能为空,为空时显示为-;\n 2.9.6.车辆识别代码:输入框(禁用状态),根据所选项目名称自动反写车辆识别代码;\n 2.9.7.车辆月租金:输入框(禁用状态),根据所选项目名称自动反写车辆月租金,后缀为元;\n 2.9.8.服务费:输入框(禁用状态),根据所选项目名称自动反写服务费,后缀为元;\n 2.9.9.保证金:输入框(禁用状态),根据所选项目名称自动反写保证金,后缀为元;\n 2.9.10.备注:输入框(禁用状态),根据所选项目名称自动反写备注信息,备注为空时显示为-;\n2.10.页面底部为提交、取消;'; + + var FormItem = function(props) { + return React.createElement('div', { style: styles.formCol }, + React.createElement('label', { style: styles.label }, props.required ? React.createElement('span', { style: styles.labelRequired }, '*') : null, props.label), + props.children, + props.error ? React.createElement('div', { style: styles.errMsg }, props.error) : null + ); + }; + + var projectOptions = projectList.map(function(p) { return React.createElement(Option, { key: p.id, value: p.id }, p.name); }); + + var formRow1 = React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '选择项目名称', required: true, error: errors.projectName }, + React.createElement(Select, { + placeholder: '请选择或输入项目名称(对应自营合同、租赁合同-项目名称)', + style: { width: '100%' }, + value: selectedProjectId || undefined, + disabled: true, + showSearch: true, + filterOption: function(input, opt) { return opt.children && String(opt.children).toLowerCase().indexOf((input || '').toLowerCase()) >= 0; }, + status: errors.projectName ? 'error' : undefined + }, projectOptions)), + React.createElement(FormItem, { label: '合同编码' }, React.createElement(Input, { value: selectedProject ? selectedProject.contractCode : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '客户名称' }, React.createElement(Input, { value: selectedProject ? selectedProject.customerName : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '交车区域' }, React.createElement(Input, { value: selectedProject ? selectedProject.deliveryRegion : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '交车地点' }, React.createElement(Input, { value: selectedProject ? selectedProject.deliveryLocation : '', disabled: true, style: { width: '100%' } })) + ); + + var formRow2 = React.createElement('div', { style: styles.formRow }, + needExpectedDelivery ? React.createElement(FormItem, { label: '预计交车日期', required: true, error: errors.expectedDelivery }, + React.createElement(RangePicker, { + style: { width: '100%' }, + format: 'YYYY-MM-DD', + placeholder: ['请选择开始日期', '请选择结束日期(单日请选同一天)'], + value: expectedDeliveryValue, + onChange: function(dates) { setExpectedDelivery(dates && dates.length === 2 ? dates : null); }, + status: errors.expectedDelivery ? 'error' : undefined + })) : null, + React.createElement(FormItem, { label: '开始计费日期', required: true, error: errors.billingDate }, + React.createElement(DatePicker, { + style: { width: '100%' }, + format: 'YYYY-MM-DD', + placeholder: '请选择日期', + value: billingDateValue, + onChange: function(d, dateStr) { setBillingDate(d); }, + status: errors.billingDate ? 'error' : undefined + })), + isRenewalProject ? React.createElement(FormItem, { label: '已交车辆是否需要还车' }, + React.createElement('div', { style: { display: 'flex', alignItems: 'flex-start', gap: 8, lineHeight: 1.6 } }, + React.createElement(Checkbox, { checked: !!noReturnCarChecked, onChange: onToggleNoReturnCar }, '不需要还车'), + React.createElement('span', { style: { color: '#8c8c8c', fontSize: 12, marginTop: 2 } }, + '勾选不需要还车,则车辆无需交车,', + React.createElement('br', null), + '只设置开始计费日期即可重新生成账单' + ) + ) + ) : null + ); + + var allSelectableChecked = selectableVehicles.length > 0 && selectableVehicles.every(function(v) { return checkedRowKeys.indexOf(v.key) !== -1; }); + var someSelectableChecked = selectableVehicles.some(function(v) { return checkedRowKeys.indexOf(v.key) !== -1; }); + var tableHeader = React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: Object.assign({}, styles.th, { width: 48 }) }, + React.createElement(Checkbox, { + checked: allSelectableChecked, + indeterminate: someSelectableChecked && !allSelectableChecked, + onChange: function(e) { onSelectAll(e.target.checked); } + })), + React.createElement('th', { style: Object.assign({}, styles.th, { width: 56 }) }, '序号'), + React.createElement('th', { style: styles.th }, '品牌'), + React.createElement('th', { style: styles.th }, '型号'), + React.createElement('th', { style: styles.th }, '车牌号'), + React.createElement('th', { style: styles.th }, '车辆识别代码'), + React.createElement('th', { style: styles.th }, '车辆月租金'), + React.createElement('th', { style: styles.th }, '服务费'), + React.createElement('th', { style: styles.th }, '保证金'), + React.createElement('th', { style: styles.th }, '备注') + ) + ); + + var tableBody = React.createElement('tbody', null, + vehicleList.length === 0 + ? React.createElement('tr', null, React.createElement('td', { colSpan: 10, style: Object.assign({}, styles.td, { textAlign: 'center', color: '#999' }) }, '请先选择项目名称,将自动带出该合同下车辆信息')) + : vehicleList.map(function(row) { + var isSubmitted = row.deliveryStatus === 'submitted'; + var isCompleted = row.deliveryStatus === 'completed'; + var completedSelectable = isRenewalProject && !!noReturnCarChecked; + return React.createElement('tr', { key: row.key }, + React.createElement('td', { style: styles.td }, + isSubmitted + ? React.createElement(Tooltip, { title: '该车辆已创建交车任务' }, + React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', cursor: 'not-allowed' } }, + React.createElement(Checkbox, { disabled: true, checked: false }))) + : isCompleted + ? React.createElement(Tooltip, { title: '该车辆已完成交车' }, + React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center' } }, + React.createElement(Checkbox, { + disabled: !completedSelectable, + checked: completedSelectable ? (checkedRowKeys.indexOf(row.key) !== -1) : false, + onChange: function(e) { onSelectRow(row, e.target.checked); } + }) + )) + : React.createElement(Checkbox, { checked: checkedRowKeys.indexOf(row.key) !== -1, onChange: function(e) { onSelectRow(row, e.target.checked); } })), + React.createElement('td', { style: styles.td }, row.seq != null ? row.seq : '—'), + React.createElement('td', { style: styles.td }, React.createElement(Input, { value: row.brand, disabled: true, style: styles.inputDisabled })), + React.createElement('td', { style: styles.td }, React.createElement(Input, { value: row.model, disabled: true, style: styles.inputDisabled })), + React.createElement('td', { style: styles.td }, React.createElement(Input, { value: row.plateNo || '-', disabled: true, style: styles.inputDisabled })), + React.createElement('td', { style: styles.td }, React.createElement(Input, { value: row.vin, disabled: true, style: styles.inputDisabled })), + React.createElement('td', { style: styles.td }, React.createElement(Input, { value: row.monthRent + '元', disabled: true, style: styles.inputDisabled })), + React.createElement('td', { style: styles.td }, React.createElement(Input, { value: row.serviceFee + '元', disabled: true, style: styles.inputDisabled })), + React.createElement('td', { style: styles.td }, React.createElement(Input, { value: row.deposit + '元', disabled: true, style: styles.inputDisabled })), + React.createElement('td', { style: styles.td }, React.createElement(Input, { value: row.remark || '-', disabled: true, style: styles.inputDisabled })) + ); + }) + ); + + var tableEl = React.createElement('div', { style: styles.tableWrap }, + React.createElement('table', { style: styles.table }, tableHeader, tableBody) + ); + + if (errors.vehicles) { + tableEl = React.createElement('div', null, + React.createElement('div', { style: Object.assign({}, styles.errMsg, { marginBottom: 8 }) }, errors.vehicles), + tableEl + ); + } + + return React.createElement('div', { style: styles.page }, + React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 } }, + React.createElement('div', { style: styles.breadcrumb }, + React.createElement('span', null, '业务管理'), + React.createElement('span', { style: styles.breadcrumbSep }, ' / '), + React.createElement('span', null, '交车任务'), + React.createElement('span', { style: styles.breadcrumbSep }, ' / '), + React.createElement('span', { style: { color: '#1890ff' } }, '编辑交车任务')), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function() { reqModalOpen[1](true); } }, '查看需求说明')), + React.createElement('div', { style: styles.card }, + React.createElement('div', { style: styles.cardHeader }, React.createElement('span', { style: styles.cardTitle }, '交车任务')), + React.createElement('div', { style: styles.cardBody }, + formRow1, + formRow2, + tableEl)), + React.createElement('div', { style: { height: 60 } }), + React.createElement(Modal, { + title: '需求说明', + open: reqModalOpen[0], + onCancel: function() { reqModalOpen[1](false); }, + width: 720, + footer: React.createElement(Button, { onClick: function() { reqModalOpen[1](false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, React.createElement('div', { style: { padding: '8px 0' } }, + React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6 } }, reqSpecText))), + React.createElement('div', { style: styles.footer }, + React.createElement(Button, { type: 'primary', onClick: handleSubmit }, '提交'), + React.createElement(Button, { onClick: handleCancel }, '取消')) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-business/pages/03-查看交车任务.jsx b/src/prototypes/oneos-web-business/pages/03-查看交车任务.jsx new file mode 100644 index 0000000..86e06a5 --- /dev/null +++ b/src/prototypes/oneos-web-business/pages/03-查看交车任务.jsx @@ -0,0 +1,382 @@ +// 【重要】必须使用 const Component 作为组件变量名 - Axhub 产品原型 +// 数字化资产ONEOS运管平台 - 查看交车任务模块(只读表单 + 车辆列表还车操作) + +const Component = function () { + var useState = React.useState; + var useCallback = React.useCallback; + + var antd = window.antd; + var Input = antd.Input; + var Button = antd.Button; + var Modal = antd.Modal; + var Drawer = antd.Drawer; + var DatePicker = antd.DatePicker; + var RangePicker = DatePicker.RangePicker; + var Cascader = antd.Cascader; + var Space = antd.Space; + var Checkbox = antd.Checkbox; + var message = antd.message; + + var reqModalOpen = useState(false); + /** 交车单是否已点击「车辆到达」(到达后不可撤销还车) */ + var vehicleArrivedPair = useState(false); + var vehicleArrived = vehicleArrivedPair[0]; + var setVehicleArrived = vehicleArrivedPair[1]; + + var returnRegionOptions = [ + { value: '浙江省', label: '浙江省', children: [{ value: '杭州市', label: '杭州市' }, { value: '嘉兴市', label: '嘉兴市' }, { value: '宁波市', label: '宁波市' }] }, + { value: '上海市', label: '上海市', children: [{ value: '上海市', label: '上海市' }] }, + { value: '江苏省', label: '江苏省', children: [{ value: '南京市', label: '南京市' }, { value: '苏州市', label: '苏州市' }] }, + { value: '广东省', label: '广东省', children: [{ value: '广州市', label: '广州市' }, { value: '深圳市', label: '深圳市' }] }, + { value: '北京市', label: '北京市', children: [{ value: '北京市', label: '北京市' }] } + ]; + + var initialVehicles = [ + { seq: 1, brand: '东风', model: '氢燃料电池重卡 H31', plateNo: '浙A10001', vin: 'LFV2BJCH8K3123456', monthRent: '12800', serviceFee: '800', deposit: '30000', remark: '首车', deliverySuccess: true, returnTaskCreated: false, returnTimeStart: null, returnTimeEnd: null, returnProvince: null, returnCity: null }, + { seq: 2, brand: '福田', model: '智蓝氢能轻卡', plateNo: '', vin: 'LZYTBACR2M1234567', monthRent: '8500', serviceFee: '500', deposit: '20000', remark: '待上牌', deliverySuccess: false, returnTaskCreated: false, returnTimeStart: null, returnTimeEnd: null, returnProvince: null, returnCity: null }, + { seq: 3, brand: '重汽', model: '豪沃氢能牵引车', plateNo: '浙F20001', vin: 'ZZ4257N386FZ12345', monthRent: '15000', serviceFee: '1000', deposit: '35000', remark: '', deliverySuccess: true, returnTaskCreated: true, returnTimeStart: '2026-04-10', returnTimeEnd: '2026-04-12', returnProvince: '浙江省', returnCity: '嘉兴市' } + ]; + + var vehiclesPair = useState(initialVehicles); + var vehicles = vehiclesPair[0]; + var setVehicles = vehiclesPair[1]; + + var drawerOpenPair = useState(false); + var drawerOpen = drawerOpenPair[0]; + var setDrawerOpen = drawerOpenPair[1]; + var drawerRowIndexPair = useState(-1); + var drawerRowIndex = drawerRowIndexPair[0]; + var setDrawerRowIndex = drawerRowIndexPair[1]; + var drawerRangePair = useState(null); + var drawerRange = drawerRangePair[0]; + var setDrawerRange = drawerRangePair[1]; + var drawerRegionPair = useState([]); + var drawerRegion = drawerRegionPair[0]; + var setDrawerRegion = drawerRegionPair[1]; + + var task = { + projectName: '嘉兴某某物流氢能运输项目', + contractCode: 'JXZL20260216YW101235A', + customerName: '嘉兴某某物流有限公司', + deliveryRegion: '浙江省 / 嘉兴市', + deliveryLocation: '浙江省嘉兴市南湖区科技大道1号', + planDeliveryDisplay: '2026-03-01至2026-03-05', + billingStartDate: '2026-03-06' + }; + + function toYmd(d) { + if (d == null) return ''; + if (typeof d === 'string') return d.slice(0, 10); + if (typeof d.format === 'function') return d.format('YYYY-MM-DD'); + return ''; + } + + function formatReturnTimeDisplay(row) { + if (!row.returnTaskCreated || !row.returnTimeStart) return '-'; + var s = row.returnTimeStart; + var e = row.returnTimeEnd || row.returnTimeStart; + if (s === e) return s; + return s + '至' + e; + } + + function formatReturnAreaDisplay(row) { + if (!row.returnProvince || !row.returnCity) return '-'; + return row.returnProvince + '-' + row.returnCity; + } + + function returnStatusText(row) { + return row.returnTaskCreated ? '已还车' : '未还车'; + } + + var openReturnDrawer = useCallback(function (index) { + setDrawerRowIndex(index); + setDrawerRange(null); + setDrawerRegion([]); + setDrawerOpen(true); + }, [setDrawerOpen, setDrawerRowIndex, setDrawerRange, setDrawerRegion]); + + var closeReturnDrawer = useCallback(function () { + setDrawerOpen(false); + setDrawerRowIndex(-1); + setDrawerRange(null); + setDrawerRegion([]); + }, [setDrawerOpen, setDrawerRowIndex, setDrawerRange, setDrawerRegion]); + + var submitReturnDrawer = useCallback(function () { + if (drawerRowIndex < 0) return; + var dates = drawerRange; + if (!dates || !dates[0] || !dates[1]) { + message.warning('请选择还车时间'); + return; + } + if (!drawerRegion || drawerRegion.length < 2) { + message.warning('请选择还车区域(省-市)'); + return; + } + var startStr = toYmd(dates[0]); + var endStr = toYmd(dates[1]); + var prov = drawerRegion[0]; + var city = drawerRegion[1]; + setVehicles(function (prev) { + var next = prev.slice(); + var r = Object.assign({}, next[drawerRowIndex]); + r.returnTaskCreated = true; + r.returnTimeStart = startStr; + r.returnTimeEnd = endStr; + r.returnProvince = prov; + r.returnCity = city; + next[drawerRowIndex] = r; + return next; + }); + message.success('还车任务已创建'); + closeReturnDrawer(); + }, [drawerRowIndex, drawerRange, drawerRegion, setVehicles, closeReturnDrawer]); + + var revokeReturn = useCallback(function (index) { + if (vehicleArrived) { + message.warning('交车单已确认车辆到达,无法撤销还车'); + return; + } + Modal.confirm({ + title: '确认撤销还车?', + content: '撤销后将清除已填写的还车时间与还车区域,需重新发起还车。', + okText: '确定', + cancelText: '取消', + onOk: function () { + setVehicles(function (prev) { + var next = prev.slice(); + var r = Object.assign({}, next[index]); + r.returnTaskCreated = false; + r.returnTimeStart = null; + r.returnTimeEnd = null; + r.returnProvince = null; + r.returnCity = null; + next[index] = r; + return next; + }); + message.success('已撤销还车'); + } + }); + }, [vehicleArrived, setVehicles]); + + var styles = { + page: { padding: '16px 24px 48px', backgroundColor: '#f5f5f5', minHeight: '100vh', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', fontSize: 14 }, + breadcrumb: { marginBottom: 16, color: '#666' }, + breadcrumbSep: { margin: '0 8px', color: '#999' }, + card: { backgroundColor: '#fff', borderRadius: 8, marginBottom: 16, boxShadow: '0 1px 2px rgba(0,0,0,0.05)', overflow: 'visible' }, + cardHeader: { padding: '16px 20px', borderBottom: '1px solid #f0f0f0' }, + cardTitle: { fontSize: 16, fontWeight: 600, color: '#333' }, + cardBody: { padding: '20px 24px' }, + formRow: { display: 'flex', flexWrap: 'wrap', marginBottom: 16 }, + formCol: { flex: '0 0 33.33%', minWidth: 200, paddingRight: 16, marginBottom: 8 }, + label: { display: 'block', marginBottom: 6, color: '#333' }, + inputDisabled: { width: '100%', padding: '6px 10px', border: '1px solid #d9d9d9', borderRadius: 4, backgroundColor: '#f5f5f5', color: '#666', fontSize: 13 }, + footer: { position: 'fixed', bottom: 0, left: 0, right: 0, padding: '12px 24px', backgroundColor: '#fff', borderTop: '1px solid #e8e8e8', display: 'flex', gap: 12, justifyContent: 'flex-start', alignItems: 'center', zIndex: 99 }, + tableWrap: { marginTop: 16, width: '100%', maxWidth: '100%', overflowX: 'auto', WebkitOverflowScrolling: 'touch' }, + /** 随列内容撑开总宽,窄屏由外层横向滚动 */ + table: { width: 'max-content', maxWidth: 'none', borderCollapse: 'collapse', fontSize: 13, tableLayout: 'auto' }, + th: { padding: '10px 8px', textAlign: 'left', borderBottom: '1px solid #e8e8e8', backgroundColor: '#fafafa', fontWeight: 600, whiteSpace: 'nowrap' }, + td: { padding: '8px', borderBottom: '1px solid #f0f0f0', verticalAlign: 'middle' }, + /** 表体单元格内输入框:按内容宽度,避免被 100% 拉满整表 */ + inputInTable: { boxSizing: 'border-box', minWidth: 72, maxWidth: 280, width: 'auto', padding: '6px 10px', border: '1px solid #d9d9d9', borderRadius: 4, backgroundColor: '#f5f5f5', color: '#666', fontSize: 13 }, + thStickyRight: { + position: 'sticky', + right: 0, + zIndex: 4, + backgroundColor: '#fafafa', + boxShadow: '-6px 0 8px -4px rgba(0,0,0,0.12)', + borderLeft: '1px solid #e8e8e8' + }, + tdStickyRight: { + position: 'sticky', + right: 0, + zIndex: 3, + backgroundColor: '#fff', + boxShadow: '-6px 0 8px -4px rgba(0,0,0,0.1)', + borderLeft: '1px solid #f0f0f0' + }, + drawerFieldLabel: { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }, + protoHint: { fontSize: 12, color: '#999', marginTop: 8 } + }; + + var FormItemReadOnly = function (props) { + return React.createElement('div', { style: styles.formCol }, + React.createElement('label', { style: styles.label }, props.label), + React.createElement(Input, { value: props.value != null ? props.value : '', disabled: true, style: styles.inputDisabled }) + ); + }; + + var formRow1 = React.createElement('div', { style: styles.formRow }, + React.createElement(FormItemReadOnly, { label: '选择项目名称', value: task.projectName }), + React.createElement(FormItemReadOnly, { label: '合同编码', value: task.contractCode }), + React.createElement(FormItemReadOnly, { label: '客户名称', value: task.customerName }), + React.createElement(FormItemReadOnly, { label: '交车区域', value: task.deliveryRegion }), + React.createElement(FormItemReadOnly, { label: '交车地点', value: task.deliveryLocation }) + ); + + var formRow2 = React.createElement('div', { style: styles.formRow }, + React.createElement(FormItemReadOnly, { label: '预计交车日期', value: task.planDeliveryDisplay }), + React.createElement(FormItemReadOnly, { label: '开始计费日期', value: task.billingStartDate }) + ); + + var tableHeader = React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: Object.assign({}, styles.th, { width: 56 }) }, '序号'), + React.createElement('th', { style: styles.th }, '品牌'), + React.createElement('th', { style: styles.th }, '型号'), + React.createElement('th', { style: styles.th }, '车牌号'), + React.createElement('th', { style: styles.th }, '车辆识别代码'), + React.createElement('th', { style: styles.th }, '车辆月租金'), + React.createElement('th', { style: styles.th }, '服务费'), + React.createElement('th', { style: styles.th }, '保证金'), + React.createElement('th', { style: styles.th }, '备注'), + React.createElement('th', { style: styles.th }, '还车时间'), + React.createElement('th', { style: styles.th }, '还车区域'), + React.createElement('th', { style: styles.th }, '还车状态'), + React.createElement('th', { style: Object.assign({}, styles.th, styles.thStickyRight, { width: 140, minWidth: 140 }) }, '操作') + ) + ); + + var renderOperationCell = function (row, i) { + if (!row.deliverySuccess) { + return React.createElement('span', { style: { color: '#999' } }, '—'); + } + if (!row.returnTaskCreated) { + return React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0 }, onClick: function () { openReturnDrawer(i); } }, '还车'); + } + return React.createElement(Button, { + type: 'link', + size: 'small', + style: { padding: 0 }, + disabled: vehicleArrived, + onClick: function () { revokeReturn(i); } + }, '撤销还车'); + }; + + var tableBody = React.createElement('tbody', null, + vehicles.length === 0 + ? React.createElement('tr', null, React.createElement('td', { colSpan: 13, style: Object.assign({}, styles.td, { textAlign: 'center', color: '#999' }) }, '暂无车辆')) + : vehicles.map(function (row, i) { + return React.createElement('tr', { key: row.vin || i }, + React.createElement('td', { style: styles.td }, row.seq != null ? row.seq : '—'), + React.createElement('td', { style: styles.td }, React.createElement(Input, { value: row.brand, disabled: true, style: styles.inputInTable })), + React.createElement('td', { style: styles.td }, React.createElement(Input, { value: row.model, disabled: true, style: styles.inputInTable })), + React.createElement('td', { style: styles.td }, React.createElement(Input, { value: row.plateNo || '-', disabled: true, style: styles.inputInTable })), + React.createElement('td', { style: styles.td }, React.createElement(Input, { value: row.vin, disabled: true, style: styles.inputInTable })), + React.createElement('td', { style: styles.td }, React.createElement(Input, { value: row.monthRent + '元', disabled: true, style: styles.inputInTable })), + React.createElement('td', { style: styles.td }, React.createElement(Input, { value: row.serviceFee + '元', disabled: true, style: styles.inputInTable })), + React.createElement('td', { style: styles.td }, React.createElement(Input, { value: row.deposit + '元', disabled: true, style: styles.inputInTable })), + React.createElement('td', { style: styles.td }, React.createElement(Input, { value: row.remark || '-', disabled: true, style: styles.inputInTable })), + React.createElement('td', { style: styles.td }, React.createElement(Input, { value: formatReturnTimeDisplay(row), disabled: true, style: styles.inputInTable })), + React.createElement('td', { style: styles.td }, React.createElement(Input, { value: formatReturnAreaDisplay(row), disabled: true, style: styles.inputInTable })), + React.createElement('td', { style: styles.td }, React.createElement(Input, { value: returnStatusText(row), disabled: true, style: styles.inputInTable })), + React.createElement('td', { style: Object.assign({}, styles.td, styles.tdStickyRight, { minWidth: 140 }) }, renderOperationCell(row, i)) + ); + }) + ); + + var tableEl = React.createElement('div', { style: styles.tableWrap }, + React.createElement('table', { style: styles.table }, tableHeader, tableBody) + ); + + var handleBack = function () { + if (window.history && window.history.back) window.history.back(); + else message.info('返回'); + }; + + var drawerTitle = '还车'; + if (drawerRowIndex >= 0 && vehicles[drawerRowIndex]) { + var dr = vehicles[drawerRowIndex]; + drawerTitle = '还车(序号 ' + dr.seq + (dr.plateNo ? ' · ' + dr.plateNo : '') + ')'; + } + + var returnDrawer = React.createElement(Drawer, { + title: drawerTitle, + placement: 'right', + width: 420, + open: drawerOpen, + onClose: closeReturnDrawer, + destroyOnClose: true, + footer: React.createElement('div', { style: { textAlign: 'right' } }, + React.createElement(Space, null, + React.createElement(Button, { onClick: closeReturnDrawer }, '取消'), + React.createElement(Button, { type: 'primary', onClick: submitReturnDrawer }, '提交') + ) + ) + }, + React.createElement('div', { style: { paddingBottom: 8 } }, + React.createElement('div', { style: styles.drawerFieldLabel }, '还车时间'), + React.createElement(RangePicker, { + style: { width: '100%' }, + format: 'YYYY-MM-DD', + placeholder: ['开始日期', '结束日期(单日请选同一天)'], + value: drawerRange, + onChange: function (dates) { setDrawerRange(dates && dates.length === 2 ? dates : null); } + }), + React.createElement('div', { style: { marginTop: 4, fontSize: 12, color: '#999' } }, '单日还车请将开始、结束选为同一天;跨天则为开始至结束时间段。') + ), + React.createElement('div', { style: { marginTop: 20 } }, + React.createElement('div', { style: styles.drawerFieldLabel }, '还车区域'), + React.createElement(Cascader, { + style: { width: '100%' }, + options: returnRegionOptions, + value: drawerRegion && drawerRegion.length ? drawerRegion : undefined, + onChange: function (v) { setDrawerRegion(v || []); }, + placeholder: '请选择省 / 市', + showSearch: true + }) + ) + ); + + var reqSpecText = + '查看交车任务\n' + + '一个「数字化资产ONEOS运管平台」中的「交车任务」「查看」模块。\n\n' + + '1.面包屑:业务管理-交车任务-查看交车任务\n\n' + + '2.表单(只读):项目名称、合同编码、客户名称、交车区域、交车地点、预计交车日期、开始计费日期等,根据交车任务数据反显。\n\n' + + '3.车辆列表:\n' + + '3.1.序号、品牌、型号、车牌号、车辆识别代码、车辆月租金、服务费、保证金、备注(只读);\n' + + '3.2.还车时间:展示操作「还车」提交时填写的还车时间;单日为 YYYY-MM-DD,时间段为 YYYY-MM-DD至YYYY-MM-DD;未提交还车前显示为「-」;\n' + + '3.3.还车区域:展示提交还车时选择的省-市(格式:省-市);未提交前显示为「-」;\n' + + '3.4.还车状态:已还车(已创建还车任务)/ 未还车;\n' + + '3.5.操作列:\n' + + ' · 仅「交车成功」的车辆显示操作;未交车成功的行操作列为「—」;\n' + + ' · 未创建还车任务时显示「还车」:点击后右侧抽屉打开,含「还车时间」(日期区间选择器,单输入框双日历,单日将起止选同一天)、「还车区域」(省-市二级级联),底部「提交」「取消」;提交校验通过后创建还车任务并关闭抽屉;\n' + + ' · 已创建还车任务后隐藏「还车」,显示「撤销还车」;点击「撤销还车」须二次确认(确认弹窗说明将清除还车时间与区域);确认后撤销还车任务;交车单已点击「车辆到达」后「撤销还车」按钮置灰不可点;\n' + + '3.6.表格布局:表格总宽度随列内容撑开;容器内超出宽度时支持横向滚动;「操作」列固定在可视区域右侧(横向滚动时保持可见)。\n\n' + + '4.原型演示:列表下方提供「模拟交车单已点击车辆到达」勾选,用于联调前验证「撤销还车」置灰逻辑;正式对接后由交车单状态接口驱动,可移除该勾选。\n\n' + + '5.页面底部为「返回」按钮。'; + + return React.createElement('div', { style: styles.page }, + React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 } }, + React.createElement('div', { style: styles.breadcrumb }, + React.createElement('span', null, '业务管理'), + React.createElement('span', { style: styles.breadcrumbSep }, ' / '), + React.createElement('span', null, '交车任务'), + React.createElement('span', { style: styles.breadcrumbSep }, ' / '), + React.createElement('span', { style: { color: '#1890ff' } }, '查看交车任务')), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { reqModalOpen[1](true); } }, '查看需求说明')), + React.createElement('div', { style: styles.card }, + React.createElement('div', { style: styles.cardHeader }, React.createElement('span', { style: styles.cardTitle }, '交车任务')), + React.createElement('div', { style: styles.cardBody }, + formRow1, + formRow2, + tableEl, + React.createElement('div', { style: styles.protoHint }, + React.createElement(Checkbox, { checked: vehicleArrived, onChange: function (e) { setVehicleArrived(e.target.checked); } }), + ' 原型演示:模拟交车单已点击「车辆到达」(勾选后「撤销还车」置灰,对接接口后由交车单状态驱动)'))), + React.createElement('div', { style: { height: 60 } }), + React.createElement('div', { style: styles.footer }, + React.createElement(Button, { onClick: handleBack }, '返回')), + returnDrawer, + React.createElement(Modal, { + title: '需求说明', + open: reqModalOpen[0], + onCancel: function () { reqModalOpen[1](false); }, + width: 560, + footer: React.createElement(Button, { onClick: function () { reqModalOpen[1](false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, React.createElement('div', { style: { padding: '8px 0', whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6 } }, reqSpecText)) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-business/pages/04-车辆成本维护.jsx b/src/prototypes/oneos-web-business/pages/04-车辆成本维护.jsx new file mode 100644 index 0000000..551036e --- /dev/null +++ b/src/prototypes/oneos-web-business/pages/04-车辆成本维护.jsx @@ -0,0 +1,187 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 业务管理 - 车辆成本维护(2026年3月12日版本) + +const Component = function () { + var useState = React.useState; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Table = antd.Table; + var Button = antd.Button; + var Input = antd.Input; + var Space = antd.Space; + var Modal = antd.Modal; + var message = antd.message; + + var requirementModalVisible = useState(false); + var listDataState = useState([ + { id: 1, brand: '东风', model: '18吨双飞翼', rentalCostPerDay: 260.00, selfOperatedCostPerDay: 250.00 }, + { id: 2, brand: '福田', model: '18吨厢式', rentalCostPerDay: 255.00, selfOperatedCostPerDay: 245.00 }, + { id: 3, brand: '重汽', model: '18吨栏板', rentalCostPerDay: 270.00, selfOperatedCostPerDay: 260.00 }, + { id: 4, brand: '陕汽', model: '25吨牵引', rentalCostPerDay: 268.00, selfOperatedCostPerDay: 258.00 }, + { id: 5, brand: '解放', model: '18吨冷藏', rentalCostPerDay: 262.00, selfOperatedCostPerDay: 252.00 }, + { id: 6, brand: '江淮', model: '9吨厢式', rentalCostPerDay: 248.00, selfOperatedCostPerDay: 238.00 }, + { id: 7, brand: '东风', model: '25吨栏板', rentalCostPerDay: 275.00, selfOperatedCostPerDay: 265.00 }, + { id: 8, brand: '福田', model: '25吨飞翼', rentalCostPerDay: 272.00, selfOperatedCostPerDay: 262.00 }, + { id: 9, brand: '重汽', model: '25吨冷藏', rentalCostPerDay: 278.00, selfOperatedCostPerDay: 268.00 }, + { id: 10, brand: '陕汽', model: '18吨自卸', rentalCostPerDay: 265.00, selfOperatedCostPerDay: 255.00 } + ]); + var listData = listDataState[0]; + var setListData = listDataState[1]; + var editingKeyState = useState(null); + var editingKey = editingKeyState[0]; + var setEditingKey = editingKeyState[1]; + var editRentalState = useState(''); + var editRental = editRentalState[0]; + var setEditRental = editRentalState[1]; + var editSelfOperatedState = useState(''); + var editSelfOperated = editSelfOperatedState[0]; + var setEditSelfOperated = editSelfOperatedState[1]; + + + function fmtCost(v) { + var n = typeof v === 'number' ? v : parseFloat(v); + return (isNaN(n) ? '0.00' : n.toFixed(2)) + '元'; + } + + function saveRow(id) { + var r = parseFloat(editRental); + var s = parseFloat(editSelfOperated); + if (isNaN(r) || r < 0 || isNaN(s) || s < 0) { + message.warning('请输入有效的日成本(元),支持两位小数'); + return; + } + setListData(function (prev) { + return prev.map(function (row) { + if (row.id !== id) return row; + var o = {}; for (var k in row) o[k] = row[k]; + o.rentalCostPerDay = Math.round(r * 100) / 100; + o.selfOperatedCostPerDay = Math.round(s * 100) / 100; + return o; + }); + }); + setEditingKey(null); + setEditRental(''); + setEditSelfOperated(''); + message.success('保存成功'); + } + + function cancelEdit() { + setEditingKey(null); + setEditRental(''); + setEditSelfOperated(''); + } + + var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; + + var columns = [ + { title: '序号', key: 'idx', width: 72, align: 'center', render: function (_, __, index) { return index + 1; } }, + { title: '品牌', dataIndex: 'brand', key: 'brand', width: 120, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '型号', dataIndex: 'model', key: 'model', width: 160, ellipsis: true, render: function (v) { return v || '—'; } }, + { + title: '租赁车辆日成本', + dataIndex: 'rentalCostPerDay', + key: 'rentalCostPerDay', + width: 160, + align: 'right', + render: function (v, record) { + var id = record.id; + var isEditing = editingKey === id; + if (isEditing) { + return React.createElement(Input, { + type: 'number', + step: 0.01, + min: 0, + value: editRental, + onChange: function (e) { setEditRental(e.target.value); }, + style: { width: 120, textAlign: 'right' }, + addonAfter: '元' + }); + } + return fmtCost(v); + } + }, + { + title: '自营车辆日成本', + dataIndex: 'selfOperatedCostPerDay', + key: 'selfOperatedCostPerDay', + width: 160, + align: 'right', + render: function (v, record) { + var id = record.id; + var isEditing = editingKey === id; + if (isEditing) { + return React.createElement(Input, { + type: 'number', + step: 0.01, + min: 0, + value: editSelfOperated, + onChange: function (e) { setEditSelfOperated(e.target.value); }, + style: { width: 120, textAlign: 'right' }, + addonAfter: '元' + }); + } + return fmtCost(v); + } + }, + { + title: '操作', + key: 'action', + width: 140, + render: function (_, record) { + var isEditing = editingKey === record.id; + if (isEditing) { + return React.createElement(Space, { size: 'small' }, + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { saveRow(record.id); } }, '保存'), + React.createElement(Button, { type: 'link', size: 'small', onClick: cancelEdit }, '取消') + ); + } + return React.createElement(Button, { + type: 'link', + size: 'small', + onClick: function () { + setEditingKey(record.id); + setEditRental(record.rentalCostPerDay != null ? String(record.rentalCostPerDay) : ''); + setEditSelfOperated(record.selfOperatedCostPerDay != null ? String(record.selfOperatedCostPerDay) : ''); + } + }, '编辑'); + } + } + ]; + + var requirementContent = '车辆成本维护(2026年3月12日版本)\n一个「数字化资产ONEOS运管平台」中的「车辆成本维护」模块\n#面包屑:业务管理-车辆成本维护;\n\n页面为1个卡片:车辆成本列表。列表默认显示当前系统中所有品牌和型号记录,如型号参数表有新增,则该处同步新增一条数据;\n1.品牌:显示车辆品牌;\n2.型号:显示车辆型号(如18吨双飞翼,而不是公告型号);\n3.租赁车辆日成本:显示格式为:xx.xx元,支持两位小数;\n4.自营车辆日成本:显示格式为:xx.xx元,支持两位小数;\n5.操作:编辑;'; + + return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + React.createElement(Breadcrumb, { + items: [ + { title: '业务管理' }, + { title: '车辆成本维护' } + ] + }), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { requirementModalVisible[1](true); } }, '查看需求说明') + ), + React.createElement(Card, { title: '车辆成本列表', style: cardStyle }, + React.createElement(Table, { + rowKey: 'id', + columns: columns, + dataSource: listData, + pagination: { pageSize: 10, showSizeChanger: true, showTotal: function (t) { return '共 ' + t + ' 条'; } }, + size: 'middle', + bordered: true + }) + ), + React.createElement(Modal, { + title: '需求说明', + open: requirementModalVisible[0], + onCancel: function () { requirementModalVisible[1](false); }, + width: 560, + footer: React.createElement(Button, { onClick: function () { requirementModalVisible[1](false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, React.createElement('div', { style: { padding: '8px 0', whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6 } }, requirementContent)) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-business/pages/05-供应商管理-编辑.jsx b/src/prototypes/oneos-web-business/pages/05-供应商管理-编辑.jsx new file mode 100644 index 0000000..80cc4fc --- /dev/null +++ b/src/prototypes/oneos-web-business/pages/05-供应商管理-编辑.jsx @@ -0,0 +1,279 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 业务管理 - 供应商管理-编辑(与新增相同,仅供应商编码不可修改) + +const Component = function () { + var useState = React.useState; + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Input = antd.Input; + var Select = antd.Select; + var Button = antd.Button; + var Cascader = antd.Cascader; + var message = antd.message; + + // 模拟编辑回显数据(实际由路由/列表传入) + var initialDetail = { + supplierCode: 'GYS-2025-001', + coopStatus: '已合作', + supplierName: '嘉兴某某加氢站', + type: '加氢站', + cityCascader: ['浙江省', '嘉兴市'], + address: '浙江省嘉兴市南湖区科技大道1号', + region: '华东', + contact: '张三', + contactMobile: '13800138001', + contactPhone: '0571-88888888', + email: 'zhangsan@example.com', + creditCodeOrId: '91330400MA2XXXXX1', + remark: '', + taxId: '91330400MA2XXXXX1', + invoiceAddress: '浙江省嘉兴市南湖区科技大道1号', + invoicePhone: '0571-88888888', + account: '6222021234567890123', + openingBank: '中国工商银行嘉兴分行', + mailingAddress: '浙江省嘉兴市南湖区科技大道1号' + }; + + var supplierCode = useState(initialDetail.supplierCode); + var coopStatus = useState(initialDetail.coopStatus); + var supplierName = useState(initialDetail.supplierName); + var type = useState(initialDetail.type); + var cityCascader = useState(initialDetail.cityCascader); + var address = useState(initialDetail.address); + var region = useState(initialDetail.region); + var contact = useState(initialDetail.contact); + var contactMobile = useState(initialDetail.contactMobile); + var contactPhone = useState(initialDetail.contactPhone); + var email = useState(initialDetail.email); + var creditCodeOrId = useState(initialDetail.creditCodeOrId); + var remark = useState(initialDetail.remark); + var taxId = useState(initialDetail.taxId); + var invoiceAddress = useState(initialDetail.invoiceAddress); + var invoicePhone = useState(initialDetail.invoicePhone); + var account = useState(initialDetail.account); + var openingBank = useState(initialDetail.openingBank); + var mailingAddress = useState(initialDetail.mailingAddress); + var formErrors = useState({}); + + var coopStatusOptions = [ + { value: '已合作', label: '已合作' }, + { value: '终止合作', label: '终止合作' }, + { value: '洽谈中', label: '洽谈中' }, + { value: '合约过期', label: '合约过期' } + ]; + var typeOptions = [ + { value: '备件供应商', label: '备件供应商' }, + { value: '保险公司', label: '保险公司' }, + { value: '加氢站', label: '加氢站' }, + { value: '充电站', label: '充电站' }, + { value: '维修站', label: '维修站' }, + { value: '救援车队', label: '救援车队' }, + { value: '整车厂', label: '整车厂' }, + { value: '其他', label: '其他' } + ]; + var cityCascaderOptions = [ + { value: '浙江省', label: '浙江省', children: [{ value: '杭州市', label: '杭州市' }, { value: '嘉兴市', label: '嘉兴市' }, { value: '宁波市', label: '宁波市' }] }, + { value: '上海市', label: '上海市', children: [{ value: '上海市', label: '上海市' }] }, + { value: '江苏省', label: '江苏省', children: [{ value: '南京市', label: '南京市' }, { value: '苏州市', label: '苏州市' }] }, + { value: '广东省', label: '广东省', children: [{ value: '广州市', label: '广州市' }, { value: '深圳市', label: '深圳市' }] }, + { value: '北京市', label: '北京市', children: [{ value: '北京市', label: '北京市' }] } + ]; + + // 根据省/直辖市名称匹配区域(城市级联第一级) + function getRegionByProvince(area) { + if (!area) return ''; + var a = String(area).trim(); + if (a === '北京市' || a === '天津市' || a === '河北省' || a === '山西省' || a === '内蒙古') return '华北'; + if (a === '上海市' || a === '江苏省' || a === '浙江省' || a === '安徽省' || a === '福建省' || a === '江西省' || a === '山东省' || a === '台湾省') return '华东'; + if (a === '广东省' || a === '广西省' || a === '海南省' || a === '澳门' || a === '香港') return '华南'; + if (a === '河南省' || a === '湖北省' || a === '湖南省') return '华中'; + if (a === '辽宁省' || a === '吉林省' || a === '黑龙江省') return '东北'; + if (a === '四川省' || a === '云南省' || a === '贵州省' || a === '重庆市' || a === '西藏') return '西南'; + if (a === '陕西省' || a === '甘肃省' || a === '青海省' || a === '宁夏' || a === '新疆') return '西北'; + return ''; + } + + function onCityCascaderChange(value) { + cityCascader[1](value); + region[1](value && value[0] ? getRegionByProvince(value[0]) : ''); + } + + function validate() { + var err = {}; + if (!(coopStatus[0] != null && coopStatus[0] !== '')) err.coopStatus = '请选择合作状态'; + if (!(supplierName[0] && String(supplierName[0]).trim())) err.supplierName = '请输入供应商名称'; + if (!(type[0] != null && type[0] !== '')) err.type = '请选择供应商类型'; + if (!(cityCascader[0] && Array.isArray(cityCascader[0]) && cityCascader[0].length >= 2)) err.city = '请选择城市(省-市)'; + if (!(address[0] && String(address[0]).trim())) err.address = '请输入供应商地址'; + if (!(contact[0] && String(contact[0]).trim())) err.contact = '请输入联系人'; + if (!(contactMobile[0] && String(contactMobile[0]).trim())) err.contactMobile = '请输入联系人手机'; + if (!(creditCodeOrId[0] && String(creditCodeOrId[0]).trim())) err.creditCodeOrId = '请输入信用代码/身份证'; + formErrors[1](err); + return Object.keys(err).length === 0; + } + + function handleSubmit() { + if (!validate()) { + message.warning('请完善必填项'); + return; + } + message.success('保存成功(原型)'); + } + + function handleCancel() { + if (window.__supplierEditBack) window.__supplierEditBack(); + else message.info('取消'); + } + + var layoutStyle = { padding: '16px 24px 48px', backgroundColor: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; + var formRowStyle = { display: 'flex', flexWrap: 'wrap', marginBottom: 16 }; + var formColStyle = { flex: '0 0 33.33%', minWidth: 200, paddingRight: 16, marginBottom: 8, boxSizing: 'border-box' }; + var formColFullStyle = { flex: '0 0 100%', marginBottom: 8 }; + var labelStyle = { display: 'block', marginBottom: 6, color: 'rgba(0,0,0,0.85)' }; + var labelRequiredStyle = { color: '#ff4d4f', marginRight: 4 }; + var errMsgStyle = { color: '#ff4d4f', fontSize: 12, marginTop: 4 }; + var inputStyle = { width: '100%' }; + var footerStyle = { position: 'fixed', bottom: 0, left: 0, right: 0, padding: '12px 24px', backgroundColor: '#fff', borderTop: '1px solid #e8e8e8', display: 'flex', gap: 12, zIndex: 99 }; + + function FormItem(props) { + var colStyle = props.fullWidth ? formColFullStyle : formColStyle; + return React.createElement('div', { style: colStyle }, + React.createElement('label', { style: labelStyle }, + props.required ? React.createElement('span', { style: labelRequiredStyle }, '*') : null, + props.label + ), + props.children, + props.error ? React.createElement('div', { style: errMsgStyle }, props.error) : null + ); + } + + var basicFields = React.createElement('div', null, + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '供应商编码' }, + React.createElement(Input, { value: supplierCode[0], style: inputStyle, disabled: true }) + ), + React.createElement(FormItem, { label: '合作状态', required: true, error: formErrors[0].coopStatus }, + React.createElement(Select, { + placeholder: '请选择合作状态', + style: inputStyle, + value: coopStatus[0], + onChange: function (v) { coopStatus[1](v); }, + options: coopStatusOptions, + status: formErrors[0].coopStatus ? 'error' : undefined + }) + ), + React.createElement(FormItem, { label: '供应商名称', required: true, error: formErrors[0].supplierName }, + React.createElement(Input, { placeholder: '请输入供应商名称', value: supplierName[0], onChange: function (e) { supplierName[1](e.target.value); }, style: inputStyle, status: formErrors[0].supplierName ? 'error' : undefined }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '类型', required: true, error: formErrors[0].type }, + React.createElement(Select, { + placeholder: '请选择供应商类型', + style: inputStyle, + value: type[0], + onChange: function (v) { type[1](v); }, + options: typeOptions, + status: formErrors[0].type ? 'error' : undefined + }) + ), + React.createElement(FormItem, { label: '城市', required: true, error: formErrors[0].city }, + React.createElement(Cascader, { + options: cityCascaderOptions, + value: cityCascader[0], + onChange: onCityCascaderChange, + placeholder: '请选择供应商所在城市', + allowClear: true, + style: inputStyle + }) + ), + React.createElement(FormItem, { label: '供应商地址', required: true, error: formErrors[0].address }, + React.createElement(Input, { placeholder: '请输入供应商地址', value: address[0], onChange: function (e) { address[1](e.target.value); }, style: inputStyle, status: formErrors[0].address ? 'error' : undefined }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '区域' }, + React.createElement(Input, { placeholder: '根据城市自动匹配', value: region[0], readOnly: true, style: Object.assign({}, inputStyle, { backgroundColor: '#f5f5f5', cursor: 'default' }) }) + ), + React.createElement(FormItem, { label: '联系人', required: true, error: formErrors[0].contact }, + React.createElement(Input, { placeholder: '请输入联系人', value: contact[0], onChange: function (e) { contact[1](e.target.value); }, style: inputStyle, status: formErrors[0].contact ? 'error' : undefined }) + ), + React.createElement(FormItem, { label: '联系人手机', required: true, error: formErrors[0].contactMobile }, + React.createElement(Input, { placeholder: '请输入联系人手机', value: contactMobile[0], onChange: function (e) { contactMobile[1](e.target.value); }, style: inputStyle, status: formErrors[0].contactMobile ? 'error' : undefined }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '联系人座机' }, + React.createElement(Input, { placeholder: '请输入联系人座机', value: contactPhone[0], onChange: function (e) { contactPhone[1](e.target.value); }, style: inputStyle }) + ), + React.createElement(FormItem, { label: '电子邮箱' }, + React.createElement(Input, { placeholder: '请输入邮箱地址', value: email[0], onChange: function (e) { email[1](e.target.value); }, style: inputStyle }) + ), + React.createElement(FormItem, { label: '信用代码/身份证', required: true, error: formErrors[0].creditCodeOrId }, + React.createElement(Input, { placeholder: '请输入信用代码编号/身份证号', value: creditCodeOrId[0], onChange: function (e) { creditCodeOrId[1](e.target.value); }, style: inputStyle, status: formErrors[0].creditCodeOrId ? 'error' : undefined }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '备注', fullWidth: true }, + React.createElement(Input.TextArea, { placeholder: '请输入备注信息', value: remark[0], onChange: function (e) { remark[1](e.target.value); }, rows: 3, style: inputStyle }) + ) + ) + ); + + var invoiceFields = React.createElement('div', null, + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '纳税人识别号' }, + React.createElement(Input, { placeholder: '请输入纳税人识别号', value: taxId[0], onChange: function (e) { taxId[1](e.target.value); }, style: inputStyle }) + ), + React.createElement(FormItem, { label: '地址' }, + React.createElement(Input, { placeholder: '请输入地址信息', value: invoiceAddress[0], onChange: function (e) { invoiceAddress[1](e.target.value); }, style: inputStyle }) + ), + React.createElement(FormItem, { label: '电话' }, + React.createElement(Input, { placeholder: '请输入电话', value: invoicePhone[0], onChange: function (e) { invoicePhone[1](e.target.value); }, style: inputStyle }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '账户' }, + React.createElement(Input, { placeholder: '请输入账户信息', value: account[0], onChange: function (e) { account[1](e.target.value); }, style: inputStyle }) + ), + React.createElement(FormItem, { label: '开户行' }, + React.createElement(Input, { placeholder: '请输入开户行信息', value: openingBank[0], onChange: function (e) { openingBank[1](e.target.value); }, style: inputStyle }) + ), + React.createElement(FormItem, { label: '邮寄地址' }, + React.createElement(Input, { placeholder: '请输入邮寄地址', value: mailingAddress[0], onChange: function (e) { mailingAddress[1](e.target.value); }, style: inputStyle }) + ) + ) + ); + + var cardTitleStyle = { display: 'flex', alignItems: 'center', gap: 8 }; + var cardTitleBarStyle = { width: 4, height: 16, backgroundColor: '#1890ff', borderRadius: 2 }; + function CardTitleWithBar(text) { + return React.createElement('span', { style: cardTitleStyle }, + React.createElement('span', { style: cardTitleBarStyle }), + text + ); + } + + return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { marginBottom: 16 } }, + React.createElement(Breadcrumb, { + items: [ + { title: '业务管理' }, + { title: '供应商管理' }, + { title: '编辑', className: 'breadcrumb-last' } + ] + }) + ), + React.createElement(Card, { title: CardTitleWithBar('供应商基本信息'), style: cardStyle }, basicFields), + React.createElement(Card, { title: CardTitleWithBar('供应商开票信息'), style: cardStyle }, invoiceFields), + React.createElement('div', { style: { height: 60 } }), + React.createElement('div', { style: footerStyle }, + React.createElement(Button, { type: 'primary', onClick: handleSubmit }, '提交'), + React.createElement(Button, { onClick: handleCancel }, '取消') + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-business/pages/06-供应商管理-查看.jsx b/src/prototypes/oneos-web-business/pages/06-供应商管理-查看.jsx new file mode 100644 index 0000000..c4eb638 --- /dev/null +++ b/src/prototypes/oneos-web-business/pages/06-供应商管理-查看.jsx @@ -0,0 +1,191 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 业务管理 - 供应商管理-查看(布局同供应商管理-新增,所有字段禁用) + +const Component = function () { + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Input = antd.Input; + var Select = antd.Select; + var Button = antd.Button; + var Cascader = antd.Cascader; + + // 模拟查看数据(实际由路由/列表传入) + var detail = { + supplierCode: 'GYS-2025-001', + coopStatus: '已合作', + supplierName: '嘉兴某某加氢站', + type: '加氢站', + cityCascader: ['浙江省', '嘉兴市'], + address: '浙江省嘉兴市南湖区科技大道1号', + region: '华东', + contact: '张三', + contactMobile: '13800138001', + contactPhone: '0571-88888888', + email: 'zhangsan@example.com', + creditCodeOrId: '91330400MA2XXXXX1', + remark: '', + taxId: '91330400MA2XXXXX1', + invoiceAddress: '浙江省嘉兴市南湖区科技大道1号', + invoicePhone: '0571-88888888', + account: '6222021234567890123', + openingBank: '中国工商银行嘉兴分行', + mailingAddress: '浙江省嘉兴市南湖区科技大道1号' + }; + + var inputStyle = { width: '100%' }; + var layoutStyle = { padding: '16px 24px 48px', backgroundColor: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; + var formRowStyle = { display: 'flex', flexWrap: 'wrap', marginBottom: 16 }; + var formColStyle = { flex: '0 0 33.33%', minWidth: 200, paddingRight: 16, marginBottom: 8, boxSizing: 'border-box' }; + var formColFullStyle = { flex: '0 0 100%', marginBottom: 8 }; + var labelStyle = { display: 'block', marginBottom: 6, color: 'rgba(0,0,0,0.85)' }; + var labelRequiredStyle = { color: '#ff4d4f', marginRight: 4 }; + var footerStyle = { position: 'fixed', bottom: 0, left: 0, right: 0, padding: '12px 24px', backgroundColor: '#fff', borderTop: '1px solid #e8e8e8', display: 'flex', gap: 12, zIndex: 99 }; + + var cityCascaderOptions = [ + { value: '浙江省', label: '浙江省', children: [{ value: '杭州市', label: '杭州市' }, { value: '嘉兴市', label: '嘉兴市' }, { value: '宁波市', label: '宁波市' }] }, + { value: '上海市', label: '上海市', children: [{ value: '上海市', label: '上海市' }] }, + { value: '江苏省', label: '江苏省', children: [{ value: '南京市', label: '南京市' }, { value: '苏州市', label: '苏州市' }] }, + { value: '广东省', label: '广东省', children: [{ value: '广州市', label: '广州市' }, { value: '深圳市', label: '深圳市' }] }, + { value: '北京市', label: '北京市', children: [{ value: '北京市', label: '北京市' }] } + ]; + var coopStatusOptions = [ + { value: '已合作', label: '已合作' }, + { value: '终止合作', label: '终止合作' }, + { value: '洽谈中', label: '洽谈中' }, + { value: '合约过期', label: '合约过期' } + ]; + var typeOptions = [ + { value: '备件供应商', label: '备件供应商' }, + { value: '保险公司', label: '保险公司' }, + { value: '加氢站', label: '加氢站' }, + { value: '充电站', label: '充电站' }, + { value: '维修站', label: '维修站' }, + { value: '救援车队', label: '救援车队' }, + { value: '整车厂', label: '整车厂' }, + { value: '其他', label: '其他' } + ]; + + function FormItem(props) { + var colStyle = props.fullWidth ? formColFullStyle : formColStyle; + return React.createElement('div', { style: colStyle }, + React.createElement('label', { style: labelStyle }, + props.required ? React.createElement('span', { style: labelRequiredStyle }, '*') : null, + props.label + ), + props.children + ); + } + + var basicFields = React.createElement('div', null, + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '供应商编码' }, + React.createElement(Input, { value: detail.supplierCode, style: inputStyle, disabled: true }) + ), + React.createElement(FormItem, { label: '合作状态', required: true }, + React.createElement(Select, { value: detail.coopStatus, style: inputStyle, disabled: true, options: coopStatusOptions }) + ), + React.createElement(FormItem, { label: '供应商名称', required: true }, + React.createElement(Input, { value: detail.supplierName, style: inputStyle, disabled: true }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '类型', required: true }, + React.createElement(Select, { value: detail.type, style: inputStyle, disabled: true, options: typeOptions }) + ), + React.createElement(FormItem, { label: '城市', required: true }, + React.createElement(Cascader, { value: detail.cityCascader, style: inputStyle, disabled: true, options: cityCascaderOptions }) + ), + React.createElement(FormItem, { label: '供应商地址', required: true }, + React.createElement(Input, { value: detail.address, style: inputStyle, disabled: true }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '区域' }, + React.createElement(Input, { value: detail.region, style: inputStyle, disabled: true }) + ), + React.createElement(FormItem, { label: '联系人', required: true }, + React.createElement(Input, { value: detail.contact, style: inputStyle, disabled: true }) + ), + React.createElement(FormItem, { label: '联系人手机', required: true }, + React.createElement(Input, { value: detail.contactMobile, style: inputStyle, disabled: true }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '联系人座机' }, + React.createElement(Input, { value: detail.contactPhone || '-', style: inputStyle, disabled: true }) + ), + React.createElement(FormItem, { label: '电子邮箱' }, + React.createElement(Input, { value: detail.email, style: inputStyle, disabled: true }) + ), + React.createElement(FormItem, { label: '信用代码/身份证', required: true }, + React.createElement(Input, { value: detail.creditCodeOrId, style: inputStyle, disabled: true }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '备注', fullWidth: true }, + React.createElement(Input.TextArea, { value: detail.remark || '', rows: 3, style: inputStyle, disabled: true }) + ) + ) + ); + + var invoiceFields = React.createElement('div', null, + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '纳税人识别号' }, + React.createElement(Input, { value: detail.taxId, style: inputStyle, disabled: true }) + ), + React.createElement(FormItem, { label: '地址' }, + React.createElement(Input, { value: detail.invoiceAddress, style: inputStyle, disabled: true }) + ), + React.createElement(FormItem, { label: '电话' }, + React.createElement(Input, { value: detail.invoicePhone, style: inputStyle, disabled: true }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '账户' }, + React.createElement(Input, { value: detail.account, style: inputStyle, disabled: true }) + ), + React.createElement(FormItem, { label: '开户行' }, + React.createElement(Input, { value: detail.openingBank, style: inputStyle, disabled: true }) + ), + React.createElement(FormItem, { label: '邮寄地址' }, + React.createElement(Input, { value: detail.mailingAddress, style: inputStyle, disabled: true }) + ) + ) + ); + + var cardTitleStyle = { display: 'flex', alignItems: 'center', gap: 8 }; + var cardTitleBarStyle = { width: 4, height: 16, backgroundColor: '#1890ff', borderRadius: 2 }; + function CardTitleWithBar(text) { + return React.createElement('span', { style: cardTitleStyle }, + React.createElement('span', { style: cardTitleBarStyle }), + text + ); + } + + function handleBack() { + if (window.__supplierViewBack) window.__supplierViewBack(); + else if (window.history && window.history.back) window.history.back(); + } + + return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { marginBottom: 16 } }, + React.createElement(Breadcrumb, { + items: [ + { title: '业务管理' }, + { title: '供应商管理' }, + { title: '查看', className: 'breadcrumb-last' } + ] + }) + ), + React.createElement(Card, { title: CardTitleWithBar('供应商基本信息'), style: cardStyle }, basicFields), + React.createElement(Card, { title: CardTitleWithBar('供应商开票信息'), style: cardStyle }, invoiceFields), + React.createElement('div', { style: { height: 60 } }), + React.createElement('div', { style: footerStyle }, + React.createElement(Button, { type: 'primary', onClick: handleBack }, '返回') + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-business/pages/07-供应商管理-新增.jsx b/src/prototypes/oneos-web-business/pages/07-供应商管理-新增.jsx new file mode 100644 index 0000000..b992d4c --- /dev/null +++ b/src/prototypes/oneos-web-business/pages/07-供应商管理-新增.jsx @@ -0,0 +1,256 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 业务管理 - 供应商管理-新增(布局参照客户管理-新增,两卡片:供应商基本信息、供应商开票信息) + +const Component = function () { + var useState = React.useState; + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Input = antd.Input; + var Select = antd.Select; + var Button = antd.Button; + var Cascader = antd.Cascader; + var message = antd.message; + + var supplierCode = useState(''); + var coopStatus = useState(undefined); + var supplierName = useState(''); + var type = useState(undefined); + var cityCascader = useState(undefined); + var address = useState(''); + var region = useState(''); + var contact = useState(''); + var contactMobile = useState(''); + var contactPhone = useState(''); + var email = useState(''); + var creditCodeOrId = useState(''); + var remark = useState(''); + var taxId = useState(''); + var invoiceAddress = useState(''); + var invoicePhone = useState(''); + var account = useState(''); + var openingBank = useState(''); + var mailingAddress = useState(''); + var formErrors = useState({}); + + var coopStatusOptions = [ + { value: '已合作', label: '已合作' }, + { value: '终止合作', label: '终止合作' }, + { value: '洽谈中', label: '洽谈中' }, + { value: '合约过期', label: '合约过期' } + ]; + var typeOptions = [ + { value: '备件供应商', label: '备件供应商' }, + { value: '保险公司', label: '保险公司' }, + { value: '加氢站', label: '加氢站' }, + { value: '充电站', label: '充电站' }, + { value: '维修站', label: '维修站' }, + { value: '救援车队', label: '救援车队' }, + { value: '整车厂', label: '整车厂' }, + { value: '其他', label: '其他' } + ]; + var cityCascaderOptions = [ + { value: '浙江省', label: '浙江省', children: [{ value: '杭州市', label: '杭州市' }, { value: '嘉兴市', label: '嘉兴市' }, { value: '宁波市', label: '宁波市' }] }, + { value: '上海市', label: '上海市', children: [{ value: '上海市', label: '上海市' }] }, + { value: '江苏省', label: '江苏省', children: [{ value: '南京市', label: '南京市' }, { value: '苏州市', label: '苏州市' }] }, + { value: '广东省', label: '广东省', children: [{ value: '广州市', label: '广州市' }, { value: '深圳市', label: '深圳市' }] }, + { value: '北京市', label: '北京市', children: [{ value: '北京市', label: '北京市' }] } + ]; + + // 根据省/直辖市名称匹配区域(城市级联第一级) + function getRegionByProvince(area) { + if (!area) return ''; + var a = String(area).trim(); + if (a === '北京市' || a === '天津市' || a === '河北省' || a === '山西省' || a === '内蒙古') return '华北'; + if (a === '上海市' || a === '江苏省' || a === '浙江省' || a === '安徽省' || a === '福建省' || a === '江西省' || a === '山东省' || a === '台湾省') return '华东'; + if (a === '广东省' || a === '广西省' || a === '海南省' || a === '澳门' || a === '香港') return '华南'; + if (a === '河南省' || a === '湖北省' || a === '湖南省') return '华中'; + if (a === '辽宁省' || a === '吉林省' || a === '黑龙江省') return '东北'; + if (a === '四川省' || a === '云南省' || a === '贵州省' || a === '重庆市' || a === '西藏') return '西南'; + if (a === '陕西省' || a === '甘肃省' || a === '青海省' || a === '宁夏' || a === '新疆') return '西北'; + return ''; + } + + function onCityCascaderChange(value) { + cityCascader[1](value); + region[1](value && value[0] ? getRegionByProvince(value[0]) : ''); + } + + function validate() { + var err = {}; + if (!(coopStatus[0] != null && coopStatus[0] !== '')) err.coopStatus = '请选择合作状态'; + if (!(supplierName[0] && String(supplierName[0]).trim())) err.supplierName = '请输入供应商名称'; + if (!(type[0] != null && type[0] !== '')) err.type = '请选择供应商类型'; + if (!(cityCascader[0] && Array.isArray(cityCascader[0]) && cityCascader[0].length >= 2)) err.city = '请选择城市(省-市)'; + if (!(address[0] && String(address[0]).trim())) err.address = '请输入供应商地址'; + if (!(contact[0] && String(contact[0]).trim())) err.contact = '请输入联系人'; + if (!(contactMobile[0] && String(contactMobile[0]).trim())) err.contactMobile = '请输入联系人手机'; + if (!(creditCodeOrId[0] && String(creditCodeOrId[0]).trim())) err.creditCodeOrId = '请输入信用代码/身份证'; + formErrors[1](err); + return Object.keys(err).length === 0; + } + + function handleSubmit() { + if (!validate()) { + message.warning('请完善必填项'); + return; + } + message.success('提交成功(原型)'); + } + + function handleCancel() { + if (window.__supplierAddBack) window.__supplierAddBack(); + else message.info('取消'); + } + + var layoutStyle = { padding: '16px 24px 48px', backgroundColor: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; + var formRowStyle = { display: 'flex', flexWrap: 'wrap', marginBottom: 16 }; + var formColStyle = { flex: '0 0 33.33%', minWidth: 200, paddingRight: 16, marginBottom: 8, boxSizing: 'border-box' }; + var formColFullStyle = { flex: '0 0 100%', marginBottom: 8 }; + var labelStyle = { display: 'block', marginBottom: 6, color: 'rgba(0,0,0,0.85)' }; + var labelRequiredStyle = { color: '#ff4d4f', marginRight: 4 }; + var errMsgStyle = { color: '#ff4d4f', fontSize: 12, marginTop: 4 }; + var inputStyle = { width: '100%' }; + var footerStyle = { position: 'fixed', bottom: 0, left: 0, right: 0, padding: '12px 24px', backgroundColor: '#fff', borderTop: '1px solid #e8e8e8', display: 'flex', gap: 12, zIndex: 99 }; + + function FormItem(props) { + var colStyle = props.fullWidth ? formColFullStyle : formColStyle; + return React.createElement('div', { style: colStyle }, + React.createElement('label', { style: labelStyle }, + props.required ? React.createElement('span', { style: labelRequiredStyle }, '*') : null, + props.label + ), + props.children, + props.error ? React.createElement('div', { style: errMsgStyle }, props.error) : null + ); + } + + var basicFields = React.createElement('div', null, + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '供应商编码' }, + React.createElement(Input, { placeholder: '请输入供应商编码', value: supplierCode[0], onChange: function (e) { supplierCode[1](e.target.value); }, style: inputStyle }) + ), + React.createElement(FormItem, { label: '合作状态', required: true, error: formErrors[0].coopStatus }, + React.createElement(Select, { + placeholder: '请选择合作状态', + style: inputStyle, + value: coopStatus[0], + onChange: function (v) { coopStatus[1](v); }, + options: coopStatusOptions, + status: formErrors[0].coopStatus ? 'error' : undefined + }) + ), + React.createElement(FormItem, { label: '供应商名称', required: true, error: formErrors[0].supplierName }, + React.createElement(Input, { placeholder: '请输入供应商名称', value: supplierName[0], onChange: function (e) { supplierName[1](e.target.value); }, style: inputStyle, status: formErrors[0].supplierName ? 'error' : undefined }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '类型', required: true, error: formErrors[0].type }, + React.createElement(Select, { + placeholder: '请选择供应商类型', + style: inputStyle, + value: type[0], + onChange: function (v) { type[1](v); }, + options: typeOptions, + status: formErrors[0].type ? 'error' : undefined + }) + ), + React.createElement(FormItem, { label: '城市', required: true, error: formErrors[0].city }, + React.createElement(Cascader, { + options: cityCascaderOptions, + value: cityCascader[0], + onChange: onCityCascaderChange, + placeholder: '请选择供应商所在城市', + allowClear: true, + style: inputStyle + }) + ), + React.createElement(FormItem, { label: '供应商地址', required: true, error: formErrors[0].address }, + React.createElement(Input, { placeholder: '请输入供应商地址', value: address[0], onChange: function (e) { address[1](e.target.value); }, style: inputStyle, status: formErrors[0].address ? 'error' : undefined }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '区域' }, + React.createElement(Input, { placeholder: '根据城市自动匹配', value: region[0], readOnly: true, style: Object.assign({}, inputStyle, { backgroundColor: '#f5f5f5', cursor: 'default' }) }) + ), + React.createElement(FormItem, { label: '联系人', required: true, error: formErrors[0].contact }, + React.createElement(Input, { placeholder: '请输入联系人', value: contact[0], onChange: function (e) { contact[1](e.target.value); }, style: inputStyle, status: formErrors[0].contact ? 'error' : undefined }) + ), + React.createElement(FormItem, { label: '联系人手机', required: true, error: formErrors[0].contactMobile }, + React.createElement(Input, { placeholder: '请输入联系人手机', value: contactMobile[0], onChange: function (e) { contactMobile[1](e.target.value); }, style: inputStyle, status: formErrors[0].contactMobile ? 'error' : undefined }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '联系人座机' }, + React.createElement(Input, { placeholder: '请输入联系人座机', value: contactPhone[0], onChange: function (e) { contactPhone[1](e.target.value); }, style: inputStyle }) + ), + React.createElement(FormItem, { label: '电子邮箱' }, + React.createElement(Input, { placeholder: '请输入邮箱地址', value: email[0], onChange: function (e) { email[1](e.target.value); }, style: inputStyle }) + ), + React.createElement(FormItem, { label: '信用代码/身份证', required: true, error: formErrors[0].creditCodeOrId }, + React.createElement(Input, { placeholder: '请输入信用代码编号/身份证号', value: creditCodeOrId[0], onChange: function (e) { creditCodeOrId[1](e.target.value); }, style: inputStyle, status: formErrors[0].creditCodeOrId ? 'error' : undefined }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '备注', fullWidth: true }, + React.createElement(Input.TextArea, { placeholder: '请输入备注信息', value: remark[0], onChange: function (e) { remark[1](e.target.value); }, rows: 3, style: inputStyle }) + ) + ) + ); + + var invoiceFields = React.createElement('div', null, + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '纳税人识别号' }, + React.createElement(Input, { placeholder: '请输入纳税人识别号', value: taxId[0], onChange: function (e) { taxId[1](e.target.value); }, style: inputStyle }) + ), + React.createElement(FormItem, { label: '地址' }, + React.createElement(Input, { placeholder: '请输入地址信息', value: invoiceAddress[0], onChange: function (e) { invoiceAddress[1](e.target.value); }, style: inputStyle }) + ), + React.createElement(FormItem, { label: '电话' }, + React.createElement(Input, { placeholder: '请输入电话', value: invoicePhone[0], onChange: function (e) { invoicePhone[1](e.target.value); }, style: inputStyle }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '账户' }, + React.createElement(Input, { placeholder: '请输入账户信息', value: account[0], onChange: function (e) { account[1](e.target.value); }, style: inputStyle }) + ), + React.createElement(FormItem, { label: '开户行' }, + React.createElement(Input, { placeholder: '请输入开户行信息', value: openingBank[0], onChange: function (e) { openingBank[1](e.target.value); }, style: inputStyle }) + ), + React.createElement(FormItem, { label: '邮寄地址' }, + React.createElement(Input, { placeholder: '请输入邮寄地址', value: mailingAddress[0], onChange: function (e) { mailingAddress[1](e.target.value); }, style: inputStyle }) + ) + ) + ); + + var cardTitleStyle = { display: 'flex', alignItems: 'center', gap: 8 }; + var cardTitleBarStyle = { width: 4, height: 16, backgroundColor: '#1890ff', borderRadius: 2 }; + function CardTitleWithBar(text) { + return React.createElement('span', { style: cardTitleStyle }, + React.createElement('span', { style: cardTitleBarStyle }), + text + ); + } + + return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { marginBottom: 16 } }, + React.createElement(Breadcrumb, { + items: [ + { title: '业务管理' }, + { title: '供应商管理' }, + { title: '新增供应商', className: 'breadcrumb-last' } + ] + }) + ), + React.createElement(Card, { title: CardTitleWithBar('供应商基本信息'), style: cardStyle }, basicFields), + React.createElement(Card, { title: CardTitleWithBar('供应商开票信息'), style: cardStyle }, invoiceFields), + React.createElement('div', { style: { height: 60 } }), + React.createElement('div', { style: footerStyle }, + React.createElement(Button, { type: 'primary', onClick: handleSubmit }, '提交'), + React.createElement(Button, { onClick: handleCancel }, '取消') + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-business/pages/08-供应商管理.jsx b/src/prototypes/oneos-web-business/pages/08-供应商管理.jsx new file mode 100644 index 0000000..fd6bba8 --- /dev/null +++ b/src/prototypes/oneos-web-business/pages/08-供应商管理.jsx @@ -0,0 +1,326 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 业务管理 - 供应商管理(2026年3月12日版本) + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Table = antd.Table; + var Button = antd.Button; + var Select = antd.Select; + var Input = antd.Input; + var Space = antd.Space; + var Modal = antd.Modal; + var Cascader = antd.Cascader; + var message = antd.message; + + var requirementModalVisible = useState(false); + var filterExpanded = useState(false); + var filterSupplierCode = useState(undefined); + var filterCoopStatus = useState([]); + var filterType = useState([]); + var filterRegion = useState([]); + var filterCity = useState(undefined); + var filterCreator = useState(undefined); + var supplierNameKeyword = useState(''); + var deleteModalOpen = useState(false); + var deleteTargetRecord = useState(null); + + var regionOptions = [ + { value: '华北', label: '华北' }, + { value: '华东', label: '华东' }, + { value: '华南', label: '华南' }, + { value: '华中', label: '华中' }, + { value: '东北', label: '东北' }, + { value: '西南', label: '西南' }, + { value: '西北', label: '西北' } + ]; + var coopStatusOptions = [ + { value: '已合作', label: '已合作' }, + { value: '终止合作', label: '终止合作' }, + { value: '洽谈中', label: '洽谈中' }, + { value: '合约过期', label: '合约过期' } + ]; + var typeOptions = [ + { value: '备件供应商', label: '备件供应商' }, + { value: '保险公司', label: '保险公司' }, + { value: '加氢站', label: '加氢站' }, + { value: '充电站', label: '充电站' }, + { value: '维修站', label: '维修站' }, + { value: '救援车队', label: '救援车队' }, + { value: '整车厂', label: '整车厂' }, + { value: '其他', label: '其他' } + ]; + var cityCascaderOptions = [ + { value: '浙江省', label: '浙江省', children: [{ value: '杭州市', label: '杭州市' }, { value: '嘉兴市', label: '嘉兴市' }, { value: '宁波市', label: '宁波市' }] }, + { value: '上海市', label: '上海市', children: [{ value: '上海市', label: '上海市' }] }, + { value: '江苏省', label: '江苏省', children: [{ value: '南京市', label: '南京市' }, { value: '苏州市', label: '苏州市' }] }, + { value: '广东省', label: '广东省', children: [{ value: '广州市', label: '广州市' }, { value: '深圳市', label: '深圳市' }] }, + { value: '北京市', label: '北京市', children: [{ value: '北京市', label: '北京市' }] } + ]; + + var listDataState = useState([ + { id: 1, supplierCode: 'GYS-2025-001', coopStatus: '已合作', supplierName: '嘉兴某某加氢站', type: '加氢站', region: '华东', city: '浙江省-嘉兴市', creditCodeOrId: '91330400MA2XXXXX1', contact: '张三', contactMobile: '13800138001', contactPhone: '0571-88888888', address: '浙江省嘉兴市南湖区科技大道1号', email: 'zhangsan@example.com', remark: '', creator: '李四', createTime: '2025-01-10 09:30' }, + { id: 2, supplierCode: 'GYS-2025-002', coopStatus: '洽谈中', supplierName: '上海某某保险公司', type: '保险公司', region: '华东', city: '上海市-上海市', creditCodeOrId: '91310000MA2YYYYY2', contact: '王五', contactMobile: '13900139002', contactPhone: '-', address: '上海市浦东新区张江镇', email: 'wangwu@example.com', remark: '意向供应商', creator: '李专员', createTime: '2025-02-15 14:20' }, + { id: 3, supplierCode: 'GYS-2025-003', coopStatus: '已合作', supplierName: '杭州某某维修站', type: '维修站', region: '华东', city: '浙江省-杭州市', creditCodeOrId: '91330100MA2ZZZZZ3', contact: '赵六', contactMobile: '13700137003', contactPhone: '0571-99999999', address: '浙江省杭州市余杭区未来科技城', email: 'zhaoliu@example.com', remark: '', creator: '张经理', createTime: '2025-01-20 11:00' }, + { id: 4, supplierCode: 'GYS-2024-012', coopStatus: '终止合作', supplierName: '某某备件供应商', type: '备件供应商', region: '华南', city: '广东省-广州市', creditCodeOrId: '91440100MA2AAAAA4', contact: '孙七', contactMobile: '13600136004', contactPhone: '-', address: '广东省广州市天河区', email: 'sunqi@example.com', remark: '已终止', creator: '李四', createTime: '2024-06-01 10:00' }, + { id: 5, supplierCode: 'GYS-2025-004', coopStatus: '合约过期', supplierName: '南京某某整车厂', type: '整车厂', region: '华东', city: '江苏省-南京市', creditCodeOrId: '91320100MA2BBBBB5', contact: '周八', contactMobile: '13500135005', contactPhone: '025-66666666', address: '江苏省南京市江宁区', email: 'zhouba@example.com', remark: '待续约', creator: '王专员', createTime: '2025-03-01 08:45' } + ]); + var listData = listDataState[0]; + var setListData = listDataState[1]; + + var supplierCodeOptions = useMemo(function () { + var arr = []; + listData.forEach(function (r) { + if (r.supplierCode && arr.every(function (o) { return o.value !== r.supplierCode; })) arr.push({ value: r.supplierCode, label: r.supplierCode }); + }); + return arr; + }, [listData]); + var creatorOptions = useMemo(function () { + var arr = []; + listData.forEach(function (r) { + if (r.creator && arr.every(function (o) { return o.value !== r.creator; })) arr.push({ value: r.creator, label: r.creator }); + }); + return arr; + }, [listData]); + + var filteredList = useMemo(function () { + var list = listData; + var code = filterSupplierCode[0]; + var statuses = filterCoopStatus[0] || []; + var types = filterType[0] || []; + var regions = filterRegion[0] || []; + var cityVal = filterCity[0]; + var creator = filterCreator[0]; + var nameKw = (supplierNameKeyword[0] || '').trim().toLowerCase(); + if (code) list = list.filter(function (r) { return r.supplierCode === code; }); + if (statuses.length) list = list.filter(function (r) { return statuses.indexOf(r.coopStatus) !== -1; }); + if (types.length) list = list.filter(function (r) { return types.indexOf(r.type) !== -1; }); + if (regions.length) list = list.filter(function (r) { return regions.indexOf(r.region) !== -1; }); + if (cityVal && Array.isArray(cityVal) && cityVal.length >= 2) list = list.filter(function (r) { return r.city === cityVal[0] + '-' + cityVal[1]; }); + if (creator) list = list.filter(function (r) { return r.creator === creator; }); + if (nameKw) list = list.filter(function (r) { return (r.supplierName || '').toLowerCase().indexOf(nameKw) !== -1; }); + return list; + }, [listData, filterSupplierCode[0], filterCoopStatus[0], filterType[0], filterRegion[0], filterCity[0], filterCreator[0], supplierNameKeyword[0]]); + + function handleReset() { + filterSupplierCode[1](undefined); + filterCoopStatus[1]([]); + filterType[1]([]); + filterRegion[1]([]); + filterCity[1](undefined); + filterCreator[1](undefined); + } + + function goView(record) { + if (window.__supplierView) window.__supplierView(record); + else message.info('跳转供应商管理-查看(原型)'); + } + function goEdit(record) { + if (window.__supplierEdit) window.__supplierEdit(record); + else message.info('跳转供应商管理-编辑(原型)'); + } + function openDeleteConfirm(record) { + deleteTargetRecord[1](record); + deleteModalOpen[1](true); + } + function confirmDelete() { + var record = deleteTargetRecord[0]; + if (record) { + setListData(function (prev) { return prev.filter(function (r) { return r.id !== record.id; }); }); + message.success('已删除'); + } + deleteModalOpen[1](false); + deleteTargetRecord[1](null); + } + + var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; + var filterLabelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var filterItemStyle = { marginBottom: 12 }; + var filterControlStyle = { width: '100%' }; + + var columns = [ + { title: '供应商编码', dataIndex: 'supplierCode', key: 'supplierCode', width: 120, ellipsis: true, fixed: 'left', render: function (v) { return v || '—'; } }, + { title: '合作状态', dataIndex: 'coopStatus', key: 'coopStatus', width: 100, render: function (v) { return v || '—'; } }, + { title: '供应商名称', dataIndex: 'supplierName', key: 'supplierName', width: 180, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '类型', dataIndex: 'type', key: 'type', width: 110, render: function (v) { return v || '—'; } }, + { title: '区域', dataIndex: 'region', key: 'region', width: 80, render: function (v) { return v || '—'; } }, + { title: '城市', dataIndex: 'city', key: 'city', width: 140, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '信用代码/身份证', dataIndex: 'creditCodeOrId', key: 'creditCodeOrId', width: 180, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '联系人', dataIndex: 'contact', key: 'contact', width: 100, render: function (v) { return v || '—'; } }, + { title: '联系人手机', dataIndex: 'contactMobile', key: 'contactMobile', width: 120, render: function (v) { return v || '—'; } }, + { title: '联系人座机', dataIndex: 'contactPhone', key: 'contactPhone', width: 120, render: function (v) { return v === '-' || !v ? '—' : v; } }, + { title: '供应商地址', dataIndex: 'address', key: 'address', width: 200, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '电子邮箱', dataIndex: 'email', key: 'email', width: 180, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '备注', dataIndex: 'remark', key: 'remark', width: 120, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '创建人', dataIndex: 'creator', key: 'creator', width: 100, render: function (v) { return v || '—'; } }, + { title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 160, render: function (v) { return v || '—'; } }, + { + title: '操作', + key: 'action', + width: 180, + fixed: 'right', + render: function (_, record) { + return React.createElement(Space, { size: 'small' }, + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { goView(record); } }, '查看'), + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { goEdit(record); } }, '编辑'), + React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function () { openDeleteConfirm(record); } }, '删除') + ); + } + } + ]; + + var filterItems = [ + React.createElement('div', { key: 'code', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '供应商编码'), + React.createElement(Select, { + placeholder: '请选择或输入供应商编码', + allowClear: true, + showSearch: true, + style: filterControlStyle, + value: filterSupplierCode[0], + onChange: function (v) { filterSupplierCode[1](v); }, + options: supplierCodeOptions, + filterOption: function (input, opt) { return (opt.label || '').toString().toLowerCase().indexOf((input || '').toLowerCase()) !== -1; } + }) + ), + React.createElement('div', { key: 'status', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '合作状态'), + React.createElement(Select, { + placeholder: '请选择合作状态', + allowClear: true, + mode: 'multiple', + style: filterControlStyle, + value: filterCoopStatus[0], + onChange: function (v) { filterCoopStatus[1](v || []); }, + options: coopStatusOptions + }) + ), + React.createElement('div', { key: 'type', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '类型'), + React.createElement(Select, { + placeholder: '请选择类型', + allowClear: true, + mode: 'multiple', + style: filterControlStyle, + value: filterType[0], + onChange: function (v) { filterType[1](v || []); }, + options: typeOptions + }) + ), + React.createElement('div', { key: 'region', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '区域'), + React.createElement(Select, { + placeholder: '请选择区域', + allowClear: true, + mode: 'multiple', + style: filterControlStyle, + value: filterRegion[0], + onChange: function (v) { filterRegion[1](v || []); }, + options: regionOptions + }) + ), + React.createElement('div', { key: 'city', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '城市'), + React.createElement(Cascader, { + options: cityCascaderOptions, + value: filterCity[0], + onChange: function (v) { filterCity[1](v); }, + placeholder: '请选择省-市', + allowClear: true, + style: filterControlStyle + }) + ), + React.createElement('div', { key: 'creator', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '创建人'), + React.createElement(Select, { + placeholder: '请选择创建人', + allowClear: true, + style: filterControlStyle, + value: filterCreator[0], + onChange: function (v) { filterCreator[1](v); }, + options: creatorOptions + }) + ) + ]; + var filterCount = filterExpanded[0] ? 6 : 3; + var filterNodes = filterItems.slice(0, filterCount); + + var requirementContent = '供应商管理(2026年3月12日版本)\n一个「数字化资产ONEOS运管平台」中的「供应商管理」模块\n#面包屑:业务管理-供应商管理;\n\n页面分为2个卡片;\n1.筛选:默认显示首行,可通过展开、收起功能进行展开/收起的切换,右侧为重置、查询按钮;\n#支持供应商编码、合作状态、类型、区域、城市、创建人等筛选方式;\n1.1.供应商编码:选择器,支持输入框内输入内容模糊搜索,下拉显示对应选项;\n1.2.合作状态:选择器,支持多选,选项为已合作、终止合作、洽谈中、合约过期;\n1.3.类型:选择器,支持多选,选项为:备件供应商、保险公司、加氢站、充电站、维修站、救援车队、整车厂、其他;\n1.4.区域:选择器,支持多选,选项为:华北、华东、华南、华中、东北、西南、西北;\n1.4.城市:地区级联选择器,支持2级选择,格式为省-市;\n1.5.创建人:选择器,选项为所有创建用户姓名;\n\n2.供应商列表:\n#左上方为供应商名称输入框(输入供应商名称直接联动),右上角为新增、导出、导入;\n2.1.供应商编码:显示供应商编码;\n2.2.合作状态:显示合作状态,选项为已合作、终止合作、洽谈中、合约过期;\n2.3.供应商名称:显示供应商名称;\n2.4.类型:备件供应商、保险公司、加氢站、充电站、维修站、救援车队、整车厂、其他;\n2.4.区域:显示供应商区域,由选择城市后自动匹配;\n2.5.城市:显示供应商城市,格式为:省-市;\n2.6.信用代码/身份证:显示供应商信用代码或身份证,取决于新增时输入的是什么;\n2.7.联系人:显示供应商联系人;\n2.8.联系人手机:显示供应商联系人手机;\n2.9.联系人座机:显示供应商联系人座机,无则显示为-;\n2.10.供应商地址:显示供应商地址信息;\n2.11.电子邮箱:显示供应商电子邮箱;\n2.12.备注:显示供应商备注信息;\n2.13.创建人:显示供应商创建人用户姓名;\n2.14.创建时间:显示供应商创建时间;\n2.15.操作:查看、编辑、删除;\n 2.16.1.查看:跳转供应商管理-查看页;\n 2.16.2.编辑:跳转供应商管理-编辑页;\n 2.16.3.删除:二次确认,点击确认后删除,已删除供应商无法在各类合同或账单选择时选择,但历史数据中该供应商信息依然显示;'; + + return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + React.createElement(Breadcrumb, { + items: [ + { title: '业务管理' }, + { title: '供应商管理' } + ] + }), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { requirementModalVisible[1](true); } }, '查看需求说明') + ), + React.createElement(Card, { title: '筛选', style: cardStyle }, + React.createElement('div', { + style: { + display: 'grid', + gridTemplateColumns: '1fr 1fr 1fr', + gap: '16px 24px', + alignItems: 'start', + flex: 1, + minWidth: 0 + } + }, filterNodes), + React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 } }, + React.createElement(Button, { onClick: handleReset }, '重置'), + React.createElement(Button, { type: 'primary' }, '查询'), + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { filterExpanded[1](!filterExpanded[0]); } }, filterExpanded[0] ? '收起' : '展开') + ) + ), + React.createElement(Card, { title: '供应商列表', style: cardStyle }, + React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 } }, + React.createElement(Input.Search, { + placeholder: '请输入供应商名称', + allowClear: true, + value: supplierNameKeyword[0], + onChange: function (e) { supplierNameKeyword[1](e.target.value); }, + style: { width: 280 } + }), + React.createElement(Space, null, + React.createElement(Button, { type: 'primary' }, '新增'), + React.createElement(Button, null, '导出'), + React.createElement(Button, null, '导入') + ) + ), + React.createElement(Table, { + rowKey: 'id', + columns: columns, + dataSource: filteredList, + pagination: { pageSize: 10, showSizeChanger: true, showTotal: function (t) { return '共 ' + t + ' 条'; } }, + size: 'middle', + bordered: true, + scroll: { x: 2400 } + }) + ), + React.createElement(Modal, { + title: '确认删除', + open: deleteModalOpen[0], + onCancel: function () { deleteModalOpen[1](false); deleteTargetRecord[1](null); }, + onOk: confirmDelete, + okText: '确认', + cancelText: '取消' + }, '确定要删除该供应商吗?删除后该供应商将无法在各类合同或账单中选择,历史数据中仍会保留该供应商信息。'), + React.createElement(Modal, { + title: '需求说明', + open: requirementModalVisible[0], + onCancel: function () { requirementModalVisible[1](false); }, + width: 640, + footer: React.createElement(Button, { onClick: function () { requirementModalVisible[1](false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, React.createElement('div', { style: { padding: '8px 0', whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6 } }, requirementContent)) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-business/pages/09-交车任务.jsx b/src/prototypes/oneos-web-business/pages/09-交车任务.jsx new file mode 100644 index 0000000..190771d --- /dev/null +++ b/src/prototypes/oneos-web-business/pages/09-交车任务.jsx @@ -0,0 +1,1698 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 数字化资产ONEOS运管平台 - 业务管理 - 交车任务 + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Table = antd.Table; + var Select = antd.Select; + var Input = antd.Input; + var Button = antd.Button; + var Space = antd.Space; + var Popover = antd.Popover; + var Modal = antd.Modal; + var DatePicker = antd.DatePicker; + var message = antd.message; + var RangePicker = DatePicker.RangePicker; + + // 筛选条件 + var filterDeliveryTaskCode = useState(undefined); + var filterContractCode = useState(undefined); + var filterProjectName = useState(undefined); + var filterCustomerName = useState(undefined); + + // 分页(主表) + var pageState = useState(1); + var pageSizeState = useState(10); + var currentPage = pageState[0]; + var setCurrentPage = pageState[1]; + var pageSize = pageSizeState[0]; + var setPageSize = pageSizeState[1]; + + // 主表展开:只展开一行 + var expandedRowKeysState = useState([]); + var expandedRowKeys = expandedRowKeysState[0]; + var setExpandedRowKeys = expandedRowKeysState[1]; + + // 子表交车数量气泡(只开一个) + var deliveryPopoverOpen = useState(null); + + // 筛选展开(默认显示一行) + var filterExpanded = useState(false); + + // 需求说明弹窗 + var reqSpecOpen = useState(false); + // 激活弹窗:重新设置预计交车日期、开始计费日期 + var activateModalOpen = useState(false); + var activateRecord = useState(null); + var activatePlanRange = useState(null); + var activateBillingDate = useState(null); + + function buildDeliveryTaskCode(contractCode, seq) { + var suffix = 'JC' + ('0000' + String(seq != null ? seq : '')).slice(-4); + return String(contractCode || '') + suffix; + } + + function getDeliveryStatus(detailRecord) { + var vehicles = (detailRecord && detailRecord.vehicles) || []; + if (!vehicles.length) return '未交车'; + var allDelivered = vehicles.every(function (v) { return v && v.actualDeliveryDate; }); + return allDelivered ? '已交车' : '未交车'; + } + + // 模拟:主表按合同聚合,子表为交车任务 + var initialMainList = useMemo(function () { + return [ + { + contractCode: 'JXZL20260216YW101235A', + projectName: '嘉兴氢能运输项目', + customerName: '嘉兴某某物流有限公司', + businessDept: '业务1部', + businessPerson: '张经理', + contractEffectiveDate: '2026-02-16', + contractEndDate: '2027-02-15', + children: [ + { + seq: 1, + planDeliveryDisplay: '2026-03-01至2026-03-05', + deliveryCount: 2, + vehicles: [ + { brand: '东风', model: 'DFH1180', plateNo: '浙A12345', actualDeliveryDate: '2026-03-02', deliverer: '运维李' }, + { brand: '福田', model: 'BJ1180', plateNo: '浙A23456', actualDeliveryDate: '2026-03-03', deliverer: '运维王' } + ], + billingStartDate: '2026-03-01', + needReturnCar: '需要', + creator: '张经理', + createdAt: '2026-02-20', + enabled: true + }, + { + seq: 2, + planDeliveryDisplay: '2026-03-08', + deliveryCount: 1, + vehicles: [{ brand: '重汽', model: 'ZZ1187', plateNo: '浙A34567', actualDeliveryDate: '', deliverer: '' }], + billingStartDate: '2026-03-08', + needReturnCar: '不需要', + creator: '张经理', + createdAt: '2026-02-22', + enabled: false + } + ] + }, + { + contractCode: 'SHZL20260210YW101200A', + projectName: '上海物流租赁项目', + customerName: '上海某某运输公司', + businessDept: '业务2部', + businessPerson: '李专员', + contractEffectiveDate: '2026-02-10', + contractEndDate: '2027-02-09', + children: [ + { + seq: 1, + planDeliveryDisplay: '2026-03-10', + deliveryCount: 1, + vehicles: [{ brand: '解放', model: 'JH6', plateNo: '沪A30003', actualDeliveryDate: '2026-03-10', deliverer: '运维李' }], + billingStartDate: '2026-03-10', + needReturnCar: '不需要', + creator: '李专员', + createdAt: '2026-02-21', + enabled: true + } + ] + }, + { + contractCode: 'JXZL20260215YW101234A', + projectName: '杭州城配租赁项目', + customerName: '杭州某某租赁有限公司', + businessDept: '业务3部', + businessPerson: '王专员', + contractEffectiveDate: '2026-02-15', + contractEndDate: '2027-02-14', + children: [ + { + seq: 1, + planDeliveryDisplay: '2026-02-28至2026-03-02', + deliveryCount: 3, + vehicles: [ + { brand: '品牌A', model: '型号A1', plateNo: '浙A10001', actualDeliveryDate: '2026-02-28', deliverer: '运维李' }, + { brand: '品牌B', model: '型号B1', plateNo: '浙B20002', actualDeliveryDate: '2026-03-01', deliverer: '运维王' }, + { brand: '品牌C', model: '型号C1', plateNo: '浙C50003', actualDeliveryDate: '2026-03-02', deliverer: '运维赵' } + ], + billingStartDate: '2026-02-28', + needReturnCar: '需要', + creator: '王专员', + createdAt: '2026-02-18', + enabled: true + } + ] + } + ]; + }, []); + + var mainListState = useState(initialMainList); + var mainList = mainListState[0]; + var setMainList = mainListState[1]; + + // 下拉 options(合同编码/项目/客户) + var filterOptions = useMemo(function () { + var codes = []; + var deliveryTaskCodes = []; + var projects = []; + var customers = []; + mainList.forEach(function (r) { + if (r.contractCode && codes.indexOf(r.contractCode) === -1) codes.push(r.contractCode); + if (r.projectName && projects.indexOf(r.projectName) === -1) projects.push(r.projectName); + if (r.customerName && customers.indexOf(r.customerName) === -1) customers.push(r.customerName); + (r.children || []).forEach(function (c) { + var full = buildDeliveryTaskCode(r.contractCode, c.seq); + if (full && deliveryTaskCodes.indexOf(full) === -1) deliveryTaskCodes.push(full); + }); + }); + return { + deliveryTaskCode: deliveryTaskCodes.map(function (v) { return { value: v, label: v }; }), + contractCode: codes.map(function (v) { return { value: v, label: v }; }), + projectName: projects.map(function (v) { return { value: v, label: v }; }), + customerName: customers.map(function (v) { return { value: v, label: v }; }) + }; + }, [mainList]); + + function selectFuzzy(input, opt) { + return (opt.label || '').toString().toLowerCase().indexOf((input || '').toLowerCase()) !== -1; + } + + function resetFilters() { + filterDeliveryTaskCode[1](undefined); + filterContractCode[1](undefined); + filterProjectName[1](undefined); + filterCustomerName[1](undefined); + setCurrentPage(1); + } + + function handleQuery() { + setCurrentPage(1); + message.info('已按筛选条件查询'); + } + + function goView(record) { + message.info('查看交车任务(原型)'); + } + + function goEdit(record) { + message.info('编辑交车任务(原型)'); + } + + function toggleEnable(record) { + if (record.enabled) { + Modal.confirm({ + title: '确认挂起', + content: '挂起交车任务会释放清空该交车任务车牌号以释放车辆,是否确定', + okText: '确定', + cancelText: '取消', + onOk: function () { + var parent = record._parentRecord; + setMainList(function (prev) { + return prev.map(function (p) { + if (!parent || p.contractCode !== parent.contractCode) return p; + var next = {}; + for (var k in p) next[k] = p[k]; + next.children = (p.children || []).map(function (c) { + if (c.seq !== record.seq) return c; + var vehicles = (c.vehicles || []).map(function (v) { + return Object.assign({}, v, { plateNo: '', actualDeliveryDate: '', deliverer: '' }); + }); + return Object.assign({}, c, { enabled: false, vehicles: vehicles }); + }); + return next; + }); + }); + message.success('已挂起'); + } + }); + } else { + activateRecord[1](record); + activatePlanRange[1](null); + activateBillingDate[1](null); + activateModalOpen[1](true); + } + } + + function handleActivateConfirm() { + var record = activateRecord[0]; + var planRange = activatePlanRange[0]; + var billingDate = activateBillingDate[0]; + if (!record) { activateModalOpen[1](false); return; } + var toDateStr = function (d) { return d && typeof d.format === 'function' ? d.format('YYYY-MM-DD') : (d && typeof d === 'string' ? d : ''); }; + var planStr = planRange && planRange.length >= 1 && planRange[0] + ? (planRange.length >= 2 && planRange[1] ? toDateStr(planRange[0]) + '至' + toDateStr(planRange[1]) : toDateStr(planRange[0])) + : ''; + var billingStr = billingDate ? toDateStr(billingDate) : ''; + if (!planStr || !billingStr) { message.warning('请填写预计交车日期和开始计费日期'); return; } + var parent = record._parentRecord; + setMainList(function (prev) { + return prev.map(function (p) { + if (!parent || p.contractCode !== parent.contractCode) return p; + var next = {}; + for (var k in p) next[k] = p[k]; + next.children = (p.children || []).map(function (c) { + if (c.seq !== record.seq) return c; + return Object.assign({}, c, { enabled: true, planDeliveryDisplay: planStr, billingStartDate: billingStr }); + }); + return next; + }); + }); + activateModalOpen[1](false); + activateRecord[1](null); + message.success('已激活'); + } + + // 筛选后的主表 + var filteredMainList = useMemo(function () { + var list = mainList.slice(); + var contractCode = filterContractCode[0]; + var projectName = filterProjectName[0]; + var customerName = filterCustomerName[0]; + var deliveryTaskCode = filterDeliveryTaskCode[0]; + + if (contractCode) list = list.filter(function (r) { return r.contractCode === contractCode; }); + if (projectName) list = list.filter(function (r) { return r.projectName === projectName; }); + if (customerName) list = list.filter(function (r) { return r.customerName === customerName; }); + + // 交车任务编码:选择器(同合同编码交互),筛选时命中任一子表编码即可 + if (deliveryTaskCode) { + list = list.filter(function (r) { + var children = r.children || []; + return children.some(function (c) { + return buildDeliveryTaskCode(r.contractCode, c.seq) === deliveryTaskCode; + }); + }); + } + + return list; + }, [mainList, filterContractCode[0], filterProjectName[0], filterCustomerName[0], filterDeliveryTaskCode[0]]); + + // 主表 dataSource 去掉 children,避免 Table 误用 treeData 产生空白行 + var mainTableDataSource = useMemo(function () { + return filteredMainList.map(function (r) { + var o = {}; + for (var k in r) if (k !== 'children') o[k] = r[k]; + o._detailList = r.children || []; + return o; + }); + }, [filteredMainList]); + + var paginatedMain = useMemo(function () { + var start = (currentPage - 1) * pageSize; + return mainTableDataSource.slice(start, start + pageSize); + }, [mainTableDataSource, currentPage, pageSize]); + + function renderDeliveryPopover(record) { + var vehicles = record.vehicles || []; + var parentCode = (record._parentRecord && record._parentRecord.contractCode) || ''; + var key = parentCode + '-' + (record.seq != null ? record.seq : ''); + + var listStyle = { width: '100%', borderCollapse: 'collapse', fontSize: 13 }; + var thStyle = { padding: '6px 10px', textAlign: 'left', borderBottom: '1px solid #f0f0f0', backgroundColor: '#fafafa', fontWeight: 600 }; + var tdStyle = { padding: '6px 10px', borderBottom: '1px solid #f0f0f0' }; + + var content = vehicles.length === 0 + ? React.createElement('div', { style: { padding: 8 } }, '—') + : React.createElement('div', { style: { padding: 0, minWidth: 520 } }, + React.createElement('table', { style: listStyle }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: thStyle }, '品牌'), + React.createElement('th', { style: thStyle }, '型号'), + React.createElement('th', { style: thStyle }, '车牌号'), + React.createElement('th', { style: thStyle }, '实际交车日期'), + React.createElement('th', { style: thStyle }, '交车人') + ) + ), + React.createElement('tbody', null, + vehicles.map(function (v, i) { + return React.createElement('tr', { key: i }, + React.createElement('td', { style: tdStyle }, v.brand || '—'), + React.createElement('td', { style: tdStyle }, v.model || '—'), + React.createElement('td', { style: tdStyle }, v.plateNo || '—'), + React.createElement('td', { style: tdStyle }, v.actualDeliveryDate ? v.actualDeliveryDate : '-'), + React.createElement('td', { style: tdStyle }, v.deliverer ? v.deliverer : '-') + ); + }) + ) + ) + ); + + var count = record.deliveryCount != null ? Number(record.deliveryCount) : 0; + return React.createElement(Popover, { + content: content, + title: '车辆明细', + trigger: 'click', + open: deliveryPopoverOpen[0] === key, + onOpenChange: function (open) { deliveryPopoverOpen[1](open ? key : null); } + }, React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0 } }, (isNaN(count) ? 0 : count) + '辆')); + } + + var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; + var filterLabelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var filterItemStyle = { marginBottom: 12 }; + var filterControlStyle = { width: '100%' }; + + var expandColumnWidth = 48; + + var mainColumns = [ + { title: '合同编码', dataIndex: 'contractCode', key: 'contractCode', width: 200, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '项目名称', dataIndex: 'projectName', key: 'projectName', width: 160, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 180, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '业务部门', dataIndex: 'businessDept', key: 'businessDept', width: 110, render: function (v) { return v || '—'; } }, + { title: '业务负责人', dataIndex: 'businessPerson', key: 'businessPerson', width: 110, render: function (v) { return v || '—'; } }, + { title: '合同生效日期', dataIndex: 'contractEffectiveDate', key: 'contractEffectiveDate', width: 120, render: function (v) { return v || '—'; } }, + { title: '合同结束日期', dataIndex: 'contractEndDate', key: 'contractEndDate', width: 120, render: function (v) { return v || '—'; } } + ]; + + var subColumns = [ + { + title: '交车任务编码', + key: 'deliveryTaskCode', + width: 260, + ellipsis: true, + render: function (_, record) { + var p = record._parentRecord; + return buildDeliveryTaskCode(p && p.contractCode, record.seq) || '—'; + } + }, + { title: '任务状态', key: 'taskStatus', width: 88, render: function (_, record) { return record.enabled !== false ? '激活' : '挂起'; } }, + { title: '交车状态', key: 'deliveryStatus', width: 100, render: function (_, record) { return getDeliveryStatus(record); } }, + { title: '预计交车日期', dataIndex: 'planDeliveryDisplay', key: 'planDeliveryDisplay', width: 160, render: function (v) { return v || '—'; } }, + { title: '交车数量', key: 'deliveryCount', width: 100, render: function (_, record) { return renderDeliveryPopover(record); } }, + { title: '开始计费日期', dataIndex: 'billingStartDate', key: 'billingStartDate', width: 120, render: function (v) { return v || '—'; } }, + { title: '创建人', dataIndex: 'creator', key: 'creator', width: 100, render: function (v) { return v || '—'; } }, + { title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', width: 110, render: function (v) { return v || '—'; } }, + { + title: '操作', + key: 'action', + width: 200, + fixed: 'right', + render: function (_, record) { + return React.createElement(Space, { size: 'small' }, + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { goView(record); } }, '查看'), + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { goEdit(record); } }, '编辑'), + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { toggleEnable(record); } }, record.enabled ? '挂起' : '激活') + ); + } + } + ]; + + function expandRowRender(record) { + var rows = (record._detailList || []).map(function (r) { + var o = {}; + for (var k in r) o[k] = r[k]; + o._parentRecord = record; + return o; + }); + + return React.createElement('div', { style: { marginBottom: 0, paddingLeft: expandColumnWidth, boxSizing: 'border-box' } }, + React.createElement(Table, { + rowKey: function (r) { return (record.contractCode || '') + '-' + (r.seq != null ? r.seq : Math.random()); }, + columns: subColumns, + dataSource: rows, + pagination: false, + size: 'small', + bordered: true, + scroll: { x: 1380 } + }) + ); + } + + var reqSpecText = + "交车任务\n" + + "一个「数字化资产ONEOS运管平台」中的「交车任务」模块\n" + + "1.面包屑:\n" + + "1.1.业务管理-交车任务\n\n" + + "2.筛选:\n" + + "支持通过合同编码、项目名称、客户名称进行筛选,右侧为查询、重置按钮;\n" + + "2.1.交车任务编码:选择器,默认所有交车任务编码,提示信息为:请输入或选择交车任务编码,支持从输入框输入内容进行模糊搜索,下拉显示结果;\n" + + " 编码规则为:[合同编码][交车任务编码],主要用于后期与用友YS系统打通时获取财务收款及发票相关数据;\n" + + " 前缀为合同编码,后缀为交车任务编码,规则为:JC+4位编号,为该合同下第x份交车任务,例如:JC0001为该合同下第1份交车任务,依次类推;\n" + + " 例如:JXZL20260216YW101235AJC0001即为JXZL20260216YW101235A合同下第1份账单;\n" + + "2.2.合同编码:选择器,默认为所有合同;提示信息为:请输入或选择合同编码,支持从输入框输入内容进行模糊搜索,下拉显示结果;\n" + + "2.3.项目名称:选择器,默认为所有项目;提示信息为:请输入或选择项目名称,支持从输入框输入内容进行模糊搜索,下拉显示结果;\n" + + "2.4.客户名称:选择器,默认为所有客户;提示信息为:请输入或选择客户名称,支持从输入框输入内容进行模糊搜索,下拉显示结果;\n" + + "2.5.查询:点击查询,根据单个或多个筛选条件(且)联动表格进行查询;\n" + + "2.6.重置:点击清空查询条件至默认;\n\n" + + "3.列表:\n" + + "#嵌套子表格,分为主表和子表,右侧为新增按钮;\n" + + "3.1.主表展示以下内容:合同编码、项目名称、客户名称、业务部门、业务负责人、合同生效日期、合同结束日期;\n" + + "3.1.1合同编码:显示对应合同编码;\n" + + "3.1.2.项目名称:显示对应项目名称;\n" + + "3.1.3.客户名称:显示对应客户名称;\n" + + "3.1.4.业务部门:显示业务部门名称;\n" + + "3.1.5.业务负责人:显示业务负责人名称;\n" + + "3.1.6.合同生效日期:显示合同生效日期,格式为:YYYY-MM-DD;\n" + + "3.1.7.合同结束日期:显示合同结束日期,格式为:YYYY-MM-DD;\n\n" + + "3.2.子表展示以下内容:交车任务编码、预计交车日期、交车数量、开始计费日期、是否需要退还车、创建人、创建时间;\n" + + "3.2.1.交车任务编码:编码规则为:[合同编码][交车任务编码],主要用于后期与用友YS系统打通时获取财务收款及发票相关数据;\n" + + " 前缀为合同编码,后缀为交车任务编码,规则为:JC+4位编号,为该合同下第x份交车任务,例如:JC0001为该合同下第1份交车任务,依次类推;\n" + + " 例如:JXZL20260216YW101235AJC0001即为JXZL20260216YW101235A合同下第1份账单;\n" + + "3.2.2.任务状态:显示挂起/激活,挂起的数据操作列对应激活、激活的数据操作列对应挂起;\n" + + "3.2.3.交车状态:显示交车任务状态,分为:已交车、未交车;\n" + + "3.2.4.预计交车日期:显示该交车任务预计交车日期,可能是某天,也可能是开始-结束时间段,格式分别为:YYYY-MM-DD、YYYY-MM-DD至YYYY-MM-DD/;\n" + + "3.2.5.交车数量:显示实际交车数量,格式为:xx辆,点击数量弹出气泡卡片,列表显示:品牌、型号、车牌号、实际交车日期、交车人,其中实际交车日期和交车人没交车时显示为-;\n" + + "3.2.6.开始计费日期:显示该交车任务开始计费日期,格式为:YYYY-MM-DD;\n" + + "3.2.7.创建人:显示交车任务的创建人用户姓名;\n" + + "3.2.8.创建时间:显示交车任务的创建时间,格式为YYYY-MM-DD;\n" + + "3.2.9.操作:支持查看、编辑、挂起/激活;\n" + + " 3.2.9.1.查看:跳转交车任务-查看页;\n" + + " 3.2.9.2.编辑:跳转交车任务-编辑页,已交车的任务不支持编辑;\n" + + " 3.2.9.3.挂起/激活:点击挂起,进行二次确认提示,提示文案:挂起交车任务会释放清空该交车任务车牌号以释放车辆,是否确定?;点击激活,弹框中重新设置预计交车日期、开始计费日期,设置成功后提示交车任务激活成功;\n\n" + + "4.列表右下方为分页功能,支持单页显示条目选择;"; + + return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + React.createElement(Breadcrumb, { items: [{ title: '业务管理' }, { title: '交车任务' }] }), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { reqSpecOpen[1](true); } }, '查看需求说明') + ), + React.createElement(Card, { title: '筛选', style: cardStyle }, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px 24px', alignItems: 'start' } }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '交车任务编码'), + React.createElement(Select, { + placeholder: '请输入交车任务编码', + allowClear: true, + showSearch: true, + style: filterControlStyle, + value: filterDeliveryTaskCode[0], + onChange: function (v) { filterDeliveryTaskCode[1](v); }, + options: filterOptions.deliveryTaskCode, + filterOption: selectFuzzy + }) + ), + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '合同编码'), + React.createElement(Select, { + placeholder: '请输入合同编码', + allowClear: true, + showSearch: true, + style: filterControlStyle, + value: filterContractCode[0], + onChange: function (v) { filterContractCode[1](v); }, + options: filterOptions.contractCode, + filterOption: selectFuzzy + }) + ), + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '项目名称'), + React.createElement(Select, { + placeholder: '请输入项目名称', + allowClear: true, + showSearch: true, + style: filterControlStyle, + value: filterProjectName[0], + onChange: function (v) { filterProjectName[1](v); }, + options: filterOptions.projectName, + filterOption: selectFuzzy + }) + ), + filterExpanded[0] ? React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '客户名称'), + React.createElement(Select, { + placeholder: '请输入客户名称', + allowClear: true, + showSearch: true, + style: filterControlStyle, + value: filterCustomerName[0], + onChange: function (v) { filterCustomerName[1](v); }, + options: filterOptions.customerName, + filterOption: selectFuzzy + }) + ) : null + ), + React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 } }, + React.createElement(Button, { type: 'link', onClick: function () { filterExpanded[1](!filterExpanded[0]); } }, filterExpanded[0] ? '收起' : '展开'), + React.createElement(Button, { type: 'primary', onClick: handleQuery }, '查询'), + React.createElement(Button, { onClick: resetFilters }, '重置') + ) + ), + React.createElement(Card, { + title: '交车任务', + style: cardStyle, + extra: React.createElement(Button, { + type: 'primary', + onClick: function () { message.info('请参照原型:业务管理-交车任务-新增交车任务'); } + }, '新增') + }, + React.createElement(Table, { + rowKey: 'contractCode', + columns: mainColumns, + dataSource: paginatedMain, + expandable: { + expandedRowKeys: expandedRowKeys, + onExpandedRowsChange: function (keys) { setExpandedRowKeys(keys && keys.length ? [keys[keys.length - 1]] : []); }, + expandedRowRender: expandRowRender, + rowExpandable: function (record) { return record._detailList && record._detailList.length > 0; }, + columnWidth: expandColumnWidth + }, + pagination: { + current: currentPage, + pageSize: pageSize, + total: mainTableDataSource.length, + showSizeChanger: true, + showTotal: function (t) { return '共 ' + t + ' 条'; }, + pageSizeOptions: ['10', '20', '50', '100'], + onChange: function (p, ps) { setCurrentPage(p); setPageSize(ps); }, + onShowSizeChange: function () { setCurrentPage(1); } + }, + size: 'middle', + bordered: true, + scroll: { x: 1110 } + }) + ) + , + React.createElement(Modal, { + title: '激活交车任务', + open: activateModalOpen[0], + onCancel: function () { activateModalOpen[1](false); activateRecord[1](null); }, + width: 520, + okText: '确定', + cancelText: '取消', + onOk: handleActivateConfirm, + destroyOnClose: true + }, React.createElement('div', { style: { padding: '8px 0' } }, + React.createElement('div', { style: { marginBottom: 16 } }, + React.createElement('div', { style: { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' } }, '预计交车日期'), + React.createElement(RangePicker, { + style: { width: '100%' }, + format: 'YYYY-MM-DD', + placeholder: ['请选择开始日期', '请选择结束日期(单日请选同一天)'], + value: activatePlanRange[0], + onChange: function (dates) { activatePlanRange[1](dates && dates.length === 2 ? dates : null); } + }), + React.createElement('div', { style: { marginTop: 4, fontSize: 12, color: '#999' } }, '支持单日或开始-结束时间段,单日时开始与结束选同一天即可') + ), + React.createElement('div', { style: { marginBottom: 8 } }, + React.createElement('div', { style: { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' } }, '开始计费日期'), + React.createElement(DatePicker, { + style: { width: '100%' }, + format: 'YYYY-MM-DD', + placeholder: '请选择日期', + value: (function () { + var v = activateBillingDate[0]; + if (v == null) return null; + if (typeof v.isValid === 'function') return v; + if (typeof v === 'string' && window.dayjs) return window.dayjs(v); + return null; + })(), + onChange: function (date) { activateBillingDate[1](date); } + }) + ) + )), + React.createElement(Modal, { + title: '需求说明', + open: reqSpecOpen[0], + onCancel: function () { reqSpecOpen[1](false); }, + width: 820, + footer: React.createElement(Button, { onClick: function () { reqSpecOpen[1](false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, React.createElement('div', { style: { whiteSpace: 'pre-wrap', lineHeight: 1.7, padding: '4px 2px' } }, reqSpecText)) + ); +}; + +/* +// 【重要】必须使用 const Component 作为组件变量名 +// 数字化资产ONEOS运管平台 - 业务管理 - 交车任务 + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Table = antd.Table; + var Select = antd.Select; + var Input = antd.Input; + var Button = antd.Button; + var Space = antd.Space; + var Popover = antd.Popover; + var Modal = antd.Modal; + var message = antd.message; + + // 筛选条件 + var filterDeliveryTaskCode = useState(''); + var filterContractCode = useState(undefined); + var filterProjectName = useState(undefined); + var filterCustomerName = useState(undefined); + + // 分页(主表) + var pageState = useState(1); + var pageSizeState = useState(10); + var currentPage = pageState[0]; + var setCurrentPage = pageState[1]; + var pageSize = pageSizeState[0]; + var setPageSize = pageSizeState[1]; + + // 主表展开:只展开一行 + var expandedRowKeysState = useState([]); + var expandedRowKeys = expandedRowKeysState[0]; + var setExpandedRowKeys = expandedRowKeysState[1]; + + // 子表交车数量气泡(只开一个) + var deliveryPopoverOpen = useState(null); + + function buildDeliveryTaskCode(contractCode, seq) { + var suffix = 'JC' + ('0000' + String(seq != null ? seq : '')).slice(-4); + return String(contractCode || '') + suffix; + } + + // 模拟:主表按合同聚合,子表为交车任务 + var initialMainList = useMemo(function () { + return [ + { + contractCode: 'JXZL20260216YW101235A', + projectName: '嘉兴氢能运输项目', + customerName: '嘉兴某某物流有限公司', + businessDept: '业务1部', + businessPerson: '张经理', + contractEffectiveDate: '2026-02-16', + contractEndDate: '2027-02-15', + children: [ + { + seq: 1, + planDeliveryDisplay: '2026-03-01至2026-03-05', + deliveryCount: 2, + vehicles: [ + { brand: '东风', model: 'DFH1180', plateNo: '浙A12345', actualDeliveryDate: '2026-03-02', deliverer: '运维李' }, + { brand: '福田', model: 'BJ1180', plateNo: '浙A23456', actualDeliveryDate: '2026-03-03', deliverer: '运维王' } + ], + billingStartDate: '2026-03-01', + needReturnCar: '需要', + creator: '张经理', + createdAt: '2026-02-20', + enabled: true + }, + { + seq: 2, + planDeliveryDisplay: '2026-03-08', + deliveryCount: 1, + vehicles: [{ brand: '重汽', model: 'ZZ1187', plateNo: '浙A34567', actualDeliveryDate: '2026-03-08', deliverer: '运维赵' }], + billingStartDate: '2026-03-08', + needReturnCar: '不需要', + creator: '张经理', + createdAt: '2026-02-22', + enabled: false + } + ] + }, + { + contractCode: 'SHZL20260210YW101200A', + projectName: '上海物流租赁项目', + customerName: '上海某某运输公司', + businessDept: '业务2部', + businessPerson: '李专员', + contractEffectiveDate: '2026-02-10', + contractEndDate: '2027-02-09', + children: [ + { + seq: 1, + planDeliveryDisplay: '2026-03-10', + deliveryCount: 1, + vehicles: [{ brand: '解放', model: 'JH6', plateNo: '沪A30003', actualDeliveryDate: '2026-03-10', deliverer: '运维李' }], + billingStartDate: '2026-03-10', + needReturnCar: '不需要', + creator: '李专员', + createdAt: '2026-02-21', + enabled: true + } + ] + }, + { + contractCode: 'JXZL20260215YW101234A', + projectName: '杭州城配租赁项目', + customerName: '杭州某某租赁有限公司', + businessDept: '业务3部', + businessPerson: '王专员', + contractEffectiveDate: '2026-02-15', + contractEndDate: '2027-02-14', + children: [ + { + seq: 1, + planDeliveryDisplay: '2026-02-28至2026-03-02', + deliveryCount: 3, + vehicles: [ + { brand: '品牌A', model: '型号A1', plateNo: '浙A10001', actualDeliveryDate: '2026-02-28', deliverer: '运维李' }, + { brand: '品牌B', model: '型号B1', plateNo: '浙B20002', actualDeliveryDate: '2026-03-01', deliverer: '运维王' }, + { brand: '品牌C', model: '型号C1', plateNo: '浙C50003', actualDeliveryDate: '2026-03-02', deliverer: '运维赵' } + ], + billingStartDate: '2026-02-28', + needReturnCar: '需要', + creator: '王专员', + createdAt: '2026-02-18', + enabled: true + } + ] + } + ]; + }, []); + + var mainListState = useState(initialMainList); + var mainList = mainListState[0]; + var setMainList = mainListState[1]; + + // 下拉 options(合同编码/项目/客户) + var filterOptions = useMemo(function () { + var codes = []; + var projects = []; + var customers = []; + mainList.forEach(function (r) { + if (r.contractCode && codes.indexOf(r.contractCode) === -1) codes.push(r.contractCode); + if (r.projectName && projects.indexOf(r.projectName) === -1) projects.push(r.projectName); + if (r.customerName && customers.indexOf(r.customerName) === -1) customers.push(r.customerName); + }); + return { + contractCode: codes.map(function (v) { return { value: v, label: v }; }), + projectName: projects.map(function (v) { return { value: v, label: v }; }), + customerName: customers.map(function (v) { return { value: v, label: v }; }) + }; + }, [mainList]); + + function selectFuzzy(input, opt) { + return (opt.label || '').toString().toLowerCase().indexOf((input || '').toLowerCase()) !== -1; + } + + function resetFilters() { + filterDeliveryTaskCode[1](''); + filterContractCode[1](undefined); + filterProjectName[1](undefined); + filterCustomerName[1](undefined); + setCurrentPage(1); + } + + function handleQuery() { + setCurrentPage(1); + message.info('已按筛选条件查询'); + } + + function goView(record) { + message.info('查看交车任务(原型)'); + } + + function goEdit(record) { + message.info('编辑交车任务(原型)'); + } + + function toggleEnable(record) { + if (record.enabled) { + Modal.confirm({ + title: '确认挂起', + content: '挂起交车任务会释放清空该交车任务车牌号以释放车辆,是否确定', + okText: '确定', + cancelText: '取消', + onOk: function () { + var parent = record._parentRecord; + setMainList(function (prev) { + return prev.map(function (p) { + if (!parent || p.contractCode !== parent.contractCode) return p; + var next = {}; + for (var k in p) next[k] = p[k]; + next.children = (p.children || []).map(function (c) { + if (c.seq !== record.seq) return c; + var vehicles = (c.vehicles || []).map(function (v) { + return Object.assign({}, v, { plateNo: '', actualDeliveryDate: '', deliverer: '' }); + }); + return Object.assign({}, c, { enabled: false, vehicles: vehicles }); + }); + return next; + }); + }); + message.success('已挂起'); + } + }); + } else { + activateRecord[1](record); + activatePlanRange[1](null); + activateBillingDate[1](null); + activateModalOpen[1](true); + } + } + + function handleActivateConfirm() { + var record = activateRecord[0]; + var planRange = activatePlanRange[0]; + var billingDate = activateBillingDate[0]; + if (!record) { activateModalOpen[1](false); return; } + var toDateStr = function (d) { return d && typeof d.format === 'function' ? d.format('YYYY-MM-DD') : (d && typeof d === 'string' ? d : ''); }; + var planStr = planRange && planRange.length >= 1 && planRange[0] + ? (planRange.length >= 2 && planRange[1] ? toDateStr(planRange[0]) + '至' + toDateStr(planRange[1]) : toDateStr(planRange[0])) + : ''; + var billingStr = billingDate ? toDateStr(billingDate) : ''; + if (!planStr || !billingStr) { message.warning('请填写预计交车日期和开始计费日期'); return; } + var parent = record._parentRecord; + setMainList(function (prev) { + return prev.map(function (p) { + if (!parent || p.contractCode !== parent.contractCode) return p; + var next = {}; + for (var k in p) next[k] = p[k]; + next.children = (p.children || []).map(function (c) { + if (c.seq !== record.seq) return c; + return Object.assign({}, c, { enabled: true, planDeliveryDisplay: planStr, billingStartDate: billingStr }); + }); + return next; + }); + }); + activateModalOpen[1](false); + activateRecord[1](null); + message.success('已激活'); + } + + // 筛选后的主表 + var filteredMainList = useMemo(function () { + var list = mainList.slice(); + var contractCode = filterContractCode[0]; + var projectName = filterProjectName[0]; + var customerName = filterCustomerName[0]; + var taskCodeKw = (filterDeliveryTaskCode[0] || '').trim().toLowerCase(); + + if (contractCode) list = list.filter(function (r) { return r.contractCode === contractCode; }); + if (projectName) list = list.filter(function (r) { return r.projectName === projectName; }); + if (customerName) list = list.filter(function (r) { return r.customerName === customerName; }); + + // 交车任务编码:支持模糊搜索(匹配完整编码:合同编码 + JCxxxx) + if (taskCodeKw) { + list = list.filter(function (r) { + var children = r.children || []; + return children.some(function (c) { + var full = buildDeliveryTaskCode(r.contractCode, c.seq); + return full.toLowerCase().indexOf(taskCodeKw) !== -1; + }); + }); + } + return list; + }, [mainList, filterContractCode[0], filterProjectName[0], filterCustomerName[0], filterDeliveryTaskCode[0]]); + + // 主表 dataSource 去掉 children,避免 Table 误用 treeData 产生空白行 + var mainTableDataSource = useMemo(function () { + return filteredMainList.map(function (r) { + var o = {}; + for (var k in r) if (k !== 'children') o[k] = r[k]; + o._detailList = r.children || []; + return o; + }); + }, [filteredMainList]); + + var paginatedMain = useMemo(function () { + var start = (currentPage - 1) * pageSize; + return mainTableDataSource.slice(start, start + pageSize); + }, [mainTableDataSource, currentPage, pageSize]); + + function renderDeliveryPopover(record) { + var vehicles = record.vehicles || []; + var parentCode = (record._parentRecord && record._parentRecord.contractCode) || ''; + var key = parentCode + '-' + (record.seq != null ? record.seq : ''); + + var listStyle = { width: '100%', borderCollapse: 'collapse', fontSize: 13 }; + var thStyle = { padding: '6px 10px', textAlign: 'left', borderBottom: '1px solid #f0f0f0', backgroundColor: '#fafafa', fontWeight: 600 }; + var tdStyle = { padding: '6px 10px', borderBottom: '1px solid #f0f0f0' }; + + var content = vehicles.length === 0 + ? React.createElement('div', { style: { padding: 8 } }, '—') + : React.createElement('div', { style: { padding: 0, minWidth: 520 } }, + React.createElement('table', { style: listStyle }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: thStyle }, '品牌'), + React.createElement('th', { style: thStyle }, '型号'), + React.createElement('th', { style: thStyle }, '车牌号'), + React.createElement('th', { style: thStyle }, '实际交车日期'), + React.createElement('th', { style: thStyle }, '交车人') + ) + ), + React.createElement('tbody', null, + vehicles.map(function (v, i) { + return React.createElement('tr', { key: i }, + React.createElement('td', { style: tdStyle }, v.brand || '—'), + React.createElement('td', { style: tdStyle }, v.model || '—'), + React.createElement('td', { style: tdStyle }, v.plateNo || '—'), + React.createElement('td', { style: tdStyle }, v.actualDeliveryDate || '—'), + React.createElement('td', { style: tdStyle }, v.deliverer || '—') + ); + }) + ) + ) + ); + + var count = record.deliveryCount != null ? Number(record.deliveryCount) : 0; + return React.createElement(Popover, { + content: content, + title: '车辆明细', + trigger: 'click', + open: deliveryPopoverOpen[0] === key, + onOpenChange: function (open) { deliveryPopoverOpen[1](open ? key : null); } + }, React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0 } }, (isNaN(count) ? 0 : count) + '辆')); + } + + var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; + var filterLabelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var filterItemStyle = { marginBottom: 12 }; + var filterControlStyle = { width: '100%' }; + + var expandColumnWidth = 48; + + var mainColumns = [ + { title: '合同编码', dataIndex: 'contractCode', key: 'contractCode', width: 200, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '项目名称', dataIndex: 'projectName', key: 'projectName', width: 160, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 180, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '业务部门', dataIndex: 'businessDept', key: 'businessDept', width: 110, render: function (v) { return v || '—'; } }, + { title: '业务负责人', dataIndex: 'businessPerson', key: 'businessPerson', width: 110, render: function (v) { return v || '—'; } }, + { title: '合同生效日期', dataIndex: 'contractEffectiveDate', key: 'contractEffectiveDate', width: 120, render: function (v) { return v || '—'; } }, + { title: '合同结束日期', dataIndex: 'contractEndDate', key: 'contractEndDate', width: 120, render: function (v) { return v || '—'; } } + ]; + + var subColumns = [ + { + title: '交车任务编码', + key: 'deliveryTaskCode', + width: 260, + ellipsis: true, + render: function (_, record) { + var p = record._parentRecord; + return buildDeliveryTaskCode(p && p.contractCode, record.seq) || '—'; + } + }, + { title: '预计交车日期', dataIndex: 'planDeliveryDisplay', key: 'planDeliveryDisplay', width: 160, render: function (v) { return v || '—'; } }, + { title: '交车数量', key: 'deliveryCount', width: 100, render: function (_, record) { return renderDeliveryPopover(record); } }, + { title: '开始计费日期', dataIndex: 'billingStartDate', key: 'billingStartDate', width: 120, render: function (v) { return v || '—'; } }, + { title: '是否需要退还车', dataIndex: 'needReturnCar', key: 'needReturnCar', width: 120, render: function (v) { return v || '—'; } }, + { title: '创建人', dataIndex: 'creator', key: 'creator', width: 100, render: function (v) { return v || '—'; } }, + { title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', width: 110, render: function (v) { return v || '—'; } }, + { + title: '操作', + key: 'action', + width: 200, + fixed: 'right', + render: function (_, record) { + return React.createElement(Space, { size: 'small' }, + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { goView(record); } }, '查看'), + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { goEdit(record); } }, '编辑'), + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { toggleEnable(record); } }, record.enabled ? '挂起' : '激活') + ); + } + } + ]; + + function expandRowRender(record) { + var rows = (record._detailList || []).map(function (r) { + var o = {}; + for (var k in r) o[k] = r[k]; + o._parentRecord = record; + return o; + }); + + return React.createElement('div', { style: { marginBottom: 0, paddingLeft: expandColumnWidth, boxSizing: 'border-box' } }, + React.createElement(Table, { + rowKey: function (r) { return (record.contractCode || '') + '-' + (r.seq != null ? r.seq : Math.random()); }, + columns: subColumns, + dataSource: rows, + pagination: false, + size: 'small', + bordered: true, + scroll: { x: 1400 } + }) + ); + } + + return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { marginBottom: 16 } }, + React.createElement(Breadcrumb, { items: [{ title: '业务管理' }, { title: '交车任务' }] }) + ), + React.createElement(Card, { title: '筛选', style: cardStyle }, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px 24px', alignItems: 'start' } }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '交车任务编码'), + React.createElement(Input, { + placeholder: '请输入交车任务编码,支持模糊搜索', + allowClear: true, + style: filterControlStyle, + value: filterDeliveryTaskCode[0], + onChange: function (e) { filterDeliveryTaskCode[1](e.target.value); } + }) + ), + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '合同编码'), + React.createElement(Select, { + placeholder: '请输入或选择合同编码,支持从输入框输入内容进行模糊搜索', + allowClear: true, + showSearch: true, + style: filterControlStyle, + value: filterContractCode[0], + onChange: function (v) { filterContractCode[1](v); }, + options: filterOptions.contractCode, + filterOption: selectFuzzy + }) + ), + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '项目名称'), + React.createElement(Select, { + placeholder: '请输入或选择项目名称,支持从输入框输入内容进行模糊搜索', + allowClear: true, + showSearch: true, + style: filterControlStyle, + value: filterProjectName[0], + onChange: function (v) { filterProjectName[1](v); }, + options: filterOptions.projectName, + filterOption: selectFuzzy + }) + ), + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '客户名称'), + React.createElement(Select, { + placeholder: '请输入或选择客户名称,支持从输入框输入内容进行模糊搜索', + allowClear: true, + showSearch: true, + style: filterControlStyle, + value: filterCustomerName[0], + onChange: function (v) { filterCustomerName[1](v); }, + options: filterOptions.customerName, + filterOption: selectFuzzy + }) + ) + ), + React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 } }, + React.createElement(Button, { type: 'primary', onClick: handleQuery }, '查询'), + React.createElement(Button, { onClick: resetFilters }, '重置') + ) + ), + React.createElement(Card, { title: '列表', style: cardStyle }, + React.createElement(Table, { + rowKey: 'contractCode', + columns: mainColumns, + dataSource: paginatedMain, + expandable: { + expandedRowKeys: expandedRowKeys, + onExpandedRowsChange: function (keys) { setExpandedRowKeys(keys && keys.length ? [keys[keys.length - 1]] : []); }, + expandedRowRender: expandRowRender, + rowExpandable: function (record) { return record._detailList && record._detailList.length > 0; }, + columnWidth: expandColumnWidth + }, + pagination: { + current: currentPage, + pageSize: pageSize, + total: mainTableDataSource.length, + showSizeChanger: true, + showTotal: function (t) { return '共 ' + t + ' 条'; }, + pageSizeOptions: ['10', '20', '50', '100'], + onChange: function (p, ps) { setCurrentPage(p); setPageSize(ps); }, + onShowSizeChange: function () { setCurrentPage(1); } + }, + size: 'middle', + bordered: true, + scroll: { x: 1110 } + }) + ) + ); +}; + +// 【重要】必须使用 const Component 作为组件变量名 +// 数字化资产ONEOS运管平台 - 业务管理 - 交车任务 + +const Component = function() { + var useState = React.useState; + var useMemo = React.useMemo; + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Select = antd.Select; + var Button = antd.Button; + var Table = antd.Table; + var Card = antd.Card; + var Tabs = antd.Tabs; + var DatePicker = antd.DatePicker; + var Popover = antd.Popover; + var Modal = antd.Modal; + var message = antd.message; + var App = antd.App; + var RangePicker = DatePicker.RangePicker; + + // 筛选条件(表单输入) + var _contractCode = useState(undefined); + var _projectName = useState(undefined); + var _customerName = useState(undefined); + var _planDateRange = useState(null); + var _creator = useState(undefined); + // 已应用的筛选条件(点击查询后生效) + var _applied = useState({ + contractCode: undefined, + projectName: undefined, + customerName: undefined, + planDateRange: null, + creator: undefined + }); + + // Tab:进行中 / 已完成 + var _activeTab = useState('ongoing'); + + // 筛选展开(默认显示前 3 项,与车辆租赁合同一致) + var _filterExpanded = useState(false); + + // 需求说明弹窗 + var _reqSpecOpen = useState(false); + + // 停用/启用:重新选择预计交车日期弹窗 + var _rescheduleModalVisible = useState(false); + var _rescheduleTask = useState(null); + var _rescheduleDateRange = useState(null); + + // 模拟选项(可模糊搜索) + var contractOptions = [ + { value: 'JXZL20260216YW101235A', label: 'JXZL20260216YW101235A' }, + { value: 'JXZL20260216YW101236A', label: 'JXZL20260216YW101236A' }, + { value: 'JXZL20260215YW101234A', label: 'JXZL20260215YW101234A' }, + { value: 'SHZL20260210YW101200A', label: 'SHZL20260210YW101200A' } + ]; + var projectOptions = [ + { value: 'p1', label: '嘉兴氢能运输项目' }, + { value: 'p2', label: '上海物流租赁项目' }, + { value: 'p3', label: '杭州城配租赁项目' } + ]; + var customerOptions = [ + { value: 'c1', label: '嘉兴某某物流有限公司' }, + { value: 'c2', label: '上海某某运输公司' }, + { value: 'c3', label: '杭州某某租赁有限公司' } + ]; + var creatorOptions = [ + { value: 'u1', label: '张经理' }, + { value: 'u2', label: '李专员' }, + { value: 'u3', label: '王专员' } + ]; + + // 进行中列表数据(未完成交车单提交),用 state 以便停用/启用后刷新 + var _ongoingList = useState([ + { id: 'o1', contractCode: 'JXZL20260216YW101235A', projectName: '嘉兴氢能运输项目', customerName: '嘉兴某某物流有限公司', deliveryCount: 2, deliveryRegion: '浙江省-嘉兴市', deliveryLocation: '嘉兴市南湖区科技大道1号', planDeliveryDisplay: '2026-03-01至2026-03-05', planDeliveryEnd: '2026-03-05', billingStartDate: '2026-03-01', creator: '张经理', createdAt: '2026-02-20', lastUpdater: '张经理', lastUpdatedAt: '2026-02-24', enabled: true }, + { id: 'o2', contractCode: 'JXZL20260216YW101236A', projectName: '上海物流租赁项目', customerName: '上海某某运输公司', deliveryCount: 1, deliveryRegion: '上海市-上海市', deliveryLocation: '上海市浦东新区张江高科技园区', planDeliveryDisplay: '2026-03-10', planDeliveryEnd: '2026-03-10', billingStartDate: '-', creator: '李专员', createdAt: '2026-02-21', lastUpdater: '李专员', lastUpdatedAt: '2026-02-22', enabled: false }, + { id: 'o3', contractCode: 'JXZL20260215YW101234A', projectName: '杭州城配租赁项目', customerName: '杭州某某租赁有限公司', deliveryCount: 3, deliveryRegion: '浙江省-杭州市', deliveryLocation: '杭州市余杭区未来科技城', planDeliveryDisplay: '2026-02-28至2026-03-02', planDeliveryEnd: '2026-03-02', billingStartDate: '2026-02-28', creator: '王专员', createdAt: '2026-02-18', lastUpdater: '王专员', lastUpdatedAt: '2026-02-25', enabled: true }, + { id: 'o4', contractCode: 'JXZL20260101YW101200A', projectName: '嘉兴氢能运输项目', customerName: '嘉兴某某物流有限公司', deliveryCount: 1, deliveryRegion: '浙江省-嘉兴市', deliveryLocation: '嘉兴市秀洲区洪兴西路288号', planDeliveryDisplay: '2026-01-05至2026-01-10', planDeliveryEnd: '2026-01-10', billingStartDate: '-', creator: '张经理', createdAt: '2026-01-02', lastUpdater: '张经理', lastUpdatedAt: '2026-01-15', enabled: false }, + { id: 'o5', contractCode: 'JXZL20260218YW101237A', projectName: '嘉兴氢能运输项目', customerName: '嘉兴某某物流有限公司', deliveryCount: 2, deliveryRegion: '浙江省-嘉兴市', deliveryLocation: '嘉兴市南湖区广益路与亚中路交叉口', planDeliveryDisplay: '2026-03-08至2026-03-12', planDeliveryEnd: '2026-03-12', billingStartDate: '-', creator: '张经理', createdAt: '2026-02-22', lastUpdater: '张经理', lastUpdatedAt: '2026-02-26', enabled: true }, + { id: 'o6', contractCode: 'SHZL20260210YW101201A', projectName: '上海物流租赁项目', customerName: '上海某某运输公司', deliveryCount: 1, deliveryRegion: '上海市-上海市', deliveryLocation: '上海市闵行区申滨路1051号', planDeliveryDisplay: '2026-03-15', planDeliveryEnd: '2026-03-15', billingStartDate: '-', creator: '李专员', createdAt: '2026-02-19', lastUpdater: '李专员', lastUpdatedAt: '2026-02-23', enabled: true }, + { id: 'o7', contractCode: 'JXZL20260220YW101238A', projectName: '杭州城配租赁项目', customerName: '杭州某某租赁有限公司', deliveryCount: 4, deliveryRegion: '浙江省-杭州市', deliveryLocation: '杭州市萧山区市心北路与建设一路交叉口', planDeliveryDisplay: '2026-03-20至2026-03-25', planDeliveryEnd: '2026-03-25', billingStartDate: '-', creator: '王专员', createdAt: '2026-02-23', lastUpdater: '王专员', lastUpdatedAt: '2026-02-27', enabled: true }, + { id: 'o8', contractCode: 'JXZL20260214YW101233A', projectName: '嘉兴氢能运输项目', customerName: '嘉兴某某物流有限公司', deliveryCount: 1, deliveryRegion: '浙江省-嘉兴市', deliveryLocation: '嘉兴市经开区昌盛路与文昌路交叉口', planDeliveryDisplay: '2026-03-05', planDeliveryEnd: '2026-03-05', billingStartDate: '-', creator: '张经理', createdAt: '2026-02-17', lastUpdater: '李专员', lastUpdatedAt: '2026-02-24', enabled: true }, + { id: 'o9', contractCode: 'SHZL20260212YW101202A', projectName: '上海物流租赁项目', customerName: '上海某某运输公司', deliveryCount: 2, deliveryRegion: '上海市-上海市', deliveryLocation: '上海市嘉定区安亭镇墨玉南路与博园路交叉口', planDeliveryDisplay: '2026-03-18至2026-03-22', planDeliveryEnd: '2026-03-22', billingStartDate: '-', creator: '李专员', createdAt: '2026-02-24', lastUpdater: '李专员', lastUpdatedAt: '2026-02-28', enabled: true }, + { id: 'o10', contractCode: 'JXZL20260222YW101239A', projectName: '杭州城配租赁项目', customerName: '杭州某某租赁有限公司', deliveryCount: 3, deliveryRegion: '浙江省-杭州市', deliveryLocation: '杭州市西湖区文三路与古翠路交叉口', planDeliveryDisplay: '2026-03-25至2026-03-30', planDeliveryEnd: '2026-03-30', billingStartDate: '-', creator: '王专员', createdAt: '2026-02-25', lastUpdater: '王专员', lastUpdatedAt: '2026-02-28', enabled: true } + ]); + var ongoingList = _ongoingList[0]; + var setOngoingList = _ongoingList[1]; + + // 已完成列表数据(已提交交车单),交车数量可点开气泡 + var completedList = [ + { id: 'c1', contractCode: 'JXZL20260215YW101234A', projectName: '杭州城配租赁项目', customerName: '杭州某某租赁有限公司', deliveryCount: 2, deliveryRegion: '浙江省-杭州市', deliveryLocation: '杭州市余杭区未来科技城', vehicles: [{ vehicleType: '4.5吨冷链车-轻型厢式货车', brand: '品牌A', model: '型号A1', plateNo: '浙A10001', actualDeliveryDate: '2026-02-28', deliverer: '运维李' }, { vehicleType: '18吨厢式货车-重型厢式货车', brand: '品牌B', model: '型号B1', plateNo: '浙B20002', actualDeliveryDate: '2026-03-01', deliverer: '运维王' }], planDeliveryDisplay: '2026-02-28至2026-03-01', billingStartDate: '2026-02-28', creator: '王专员', createdAt: '2026-02-18', lastUpdater: '王专员', lastUpdatedAt: '2026-03-01' }, + { id: 'c2', contractCode: 'SHZL20260210YW101200A', projectName: '上海物流租赁项目', customerName: '上海某某运输公司', deliveryCount: 1, deliveryRegion: '上海市-上海市', deliveryLocation: '上海市浦东新区张江高科技园区', vehicles: [{ vehicleType: '18吨厢式货车-重型厢式货车', brand: '品牌B', model: '型号B1', plateNo: '沪A30003', actualDeliveryDate: '2026-02-25', deliverer: '运维赵' }], planDeliveryDisplay: '2026-02-25', billingStartDate: '2026-02-25', creator: '李专员', createdAt: '2026-02-15', lastUpdater: '李专员', lastUpdatedAt: '2026-02-25' }, + { id: 'c3', contractCode: 'JXZL20260208YW101231A', projectName: '嘉兴氢能运输项目', customerName: '嘉兴某某物流有限公司', deliveryCount: 2, deliveryRegion: '浙江省-嘉兴市', deliveryLocation: '嘉兴市南湖区科技大道1号', vehicles: [{ vehicleType: '4.5吨冷链车-轻型厢式货车', brand: '品牌A', model: '型号A1', plateNo: '浙A10002', actualDeliveryDate: '2026-02-20', deliverer: '运维李' }, { vehicleType: '18吨厢式货车-重型厢式货车', brand: '品牌B', model: '型号B1', plateNo: '浙A10003', actualDeliveryDate: '2026-02-21', deliverer: '运维王' }], planDeliveryDisplay: '2026-02-20至2026-02-21', billingStartDate: '2026-02-20', creator: '张经理', createdAt: '2026-02-10', lastUpdater: '张经理', lastUpdatedAt: '2026-02-21' }, + { id: 'c4', contractCode: 'SHZL20260205YW101198A', projectName: '上海物流租赁项目', customerName: '上海某某运输公司', deliveryCount: 1, deliveryRegion: '上海市-上海市', deliveryLocation: '上海市闵行区申滨路1051号', vehicles: [{ vehicleType: '18吨厢式货车-重型厢式货车', brand: '品牌B', model: '型号B2', plateNo: '沪B40004', actualDeliveryDate: '2026-02-18', deliverer: '运维赵' }], planDeliveryDisplay: '2026-02-18', billingStartDate: '2026-02-18', creator: '李专员', createdAt: '2026-02-08', lastUpdater: '李专员', lastUpdatedAt: '2026-02-18' }, + { id: 'c5', contractCode: 'JXZL20260212YW101232A', projectName: '杭州城配租赁项目', customerName: '杭州某某租赁有限公司', deliveryCount: 3, deliveryRegion: '浙江省-杭州市', deliveryLocation: '杭州市萧山区市心北路与建设一路交叉口', vehicles: [{ vehicleType: '4.5吨冷链车-轻型厢式货车', brand: '品牌A', model: '型号A2', plateNo: '浙C50001', actualDeliveryDate: '2026-02-22', deliverer: '运维李' }, { vehicleType: '18吨厢式货车-重型厢式货车', brand: '品牌B', model: '型号B1', plateNo: '浙C50002', actualDeliveryDate: '2026-02-23', deliverer: '运维王' }, { vehicleType: '4.5吨货车-轻型厢式货车', brand: '品牌C', model: '型号C1', plateNo: '浙C50003', actualDeliveryDate: '2026-02-24', deliverer: '运维赵' }], planDeliveryDisplay: '2026-02-22至2026-02-24', billingStartDate: '2026-02-22', creator: '王专员', createdAt: '2026-02-14', lastUpdater: '王专员', lastUpdatedAt: '2026-02-24' }, + { id: 'c6', contractCode: 'JXZL20260201YW101228A', projectName: '嘉兴氢能运输项目', customerName: '嘉兴某某物流有限公司', deliveryCount: 1, deliveryRegion: '浙江省-嘉兴市', deliveryLocation: '嘉兴市秀洲区洪兴西路288号', vehicles: [{ vehicleType: '4.5吨冷链车-轻型厢式货车', brand: '品牌A', model: '型号A1', plateNo: '浙A10004', actualDeliveryDate: '2026-02-15', deliverer: '运维李' }], planDeliveryDisplay: '2026-02-15', billingStartDate: '2026-02-15', creator: '张经理', createdAt: '2026-02-05', lastUpdater: '张经理', lastUpdatedAt: '2026-02-15' }, + { id: 'c7', contractCode: 'SHZL20260203YW101199A', projectName: '上海物流租赁项目', customerName: '上海某某运输公司', deliveryCount: 2, deliveryRegion: '上海市-上海市', deliveryLocation: '上海市嘉定区安亭镇墨玉南路与博园路交叉口', vehicles: [{ vehicleType: '18吨厢式货车-重型厢式货车', brand: '品牌B', model: '型号B1', plateNo: '沪A30005', actualDeliveryDate: '2026-02-16', deliverer: '运维赵' }, { vehicleType: '4.5吨冷链车-轻型厢式货车', brand: '品牌A', model: '型号A1', plateNo: '沪A30006', actualDeliveryDate: '2026-02-17', deliverer: '运维李' }], planDeliveryDisplay: '2026-02-16至2026-02-17', billingStartDate: '2026-02-16', creator: '李专员', createdAt: '2026-02-06', lastUpdater: '李专员', lastUpdatedAt: '2026-02-17' }, + { id: 'c8', contractCode: 'JXZL20260128YW101227A', projectName: '杭州城配租赁项目', customerName: '杭州某某租赁有限公司', deliveryCount: 2, deliveryRegion: '浙江省-杭州市', deliveryLocation: '杭州市西湖区文三路与古翠路交叉口', vehicles: [{ vehicleType: '18吨厢式货车-重型厢式货车', brand: '品牌B', model: '型号B2', plateNo: '浙B20004', actualDeliveryDate: '2026-02-10', deliverer: '运维王' }, { vehicleType: '4.5吨货车-轻型厢式货车', brand: '品牌C', model: '型号C1', plateNo: '浙B20005', actualDeliveryDate: '2026-02-11', deliverer: '运维赵' }], planDeliveryDisplay: '2026-02-10至2026-02-11', billingStartDate: '2026-02-10', creator: '王专员', createdAt: '2026-01-30', lastUpdater: '王专员', lastUpdatedAt: '2026-02-11' }, + { id: 'c9', contractCode: 'JXZL20260206YW101230A', projectName: '嘉兴氢能运输项目', customerName: '嘉兴某某物流有限公司', deliveryCount: 1, deliveryRegion: '浙江省-嘉兴市', deliveryLocation: '嘉兴市经开区昌盛路与文昌路交叉口', vehicles: [{ vehicleType: '4.5吨冷链车-轻型厢式货车', brand: '品牌A', model: '型号A2', plateNo: '浙A10005', actualDeliveryDate: '2026-02-19', deliverer: '运维李' }], planDeliveryDisplay: '2026-02-19', billingStartDate: '2026-02-19', creator: '张经理', createdAt: '2026-02-09', lastUpdater: '张经理', lastUpdatedAt: '2026-02-19' }, + { id: 'c10', contractCode: 'SHZL20260215YW101203A', projectName: '上海物流租赁项目', customerName: '上海某某运输公司', deliveryCount: 2, deliveryRegion: '上海市-上海市', deliveryLocation: '上海市浦东新区金桥镇金海路1000号', vehicles: [{ vehicleType: '18吨厢式货车-重型厢式货车', brand: '品牌B', model: '型号B1', plateNo: '沪C60001', actualDeliveryDate: '2026-02-26', deliverer: '运维赵' }, { vehicleType: '4.5吨冷链车-轻型厢式货车', brand: '品牌A', model: '型号A1', plateNo: '沪C60002', actualDeliveryDate: '2026-02-27', deliverer: '运维李' }], planDeliveryDisplay: '2026-02-26至2026-02-27', billingStartDate: '2026-02-26', creator: '李专员', createdAt: '2026-02-18', lastUpdater: '李专员', lastUpdatedAt: '2026-02-27' } + ]; + + var applied = _applied[0]; + var setApplied = _applied[1]; + + var getProjectLabel = function(v) { var o = projectOptions.find(function(x) { return x.value === v; }); return o ? o.label : v; }; + var getCustomerLabel = function(v) { var o = customerOptions.find(function(x) { return x.value === v; }); return o ? o.label : v; }; + var getCreatorLabel = function(v) { var o = creatorOptions.find(function(x) { return x.value === v; }); return o ? o.label : v; }; + + var filterOngoing = function(list) { + var a = applied; + if (a.contractCode) list = list.filter(function(r) { return r.contractCode === a.contractCode; }); + if (a.projectName) { var pl = getProjectLabel(a.projectName); if (pl) list = list.filter(function(r) { return r.projectName === pl; }); } + if (a.customerName) { var cl = getCustomerLabel(a.customerName); if (cl) list = list.filter(function(r) { return r.customerName === cl; }); } + if (a.creator) { var ul = getCreatorLabel(a.creator); if (ul) list = list.filter(function(r) { return r.creator === ul; }); } + if (a.planDateRange && a.planDateRange.length === 2 && window.dayjs) { + var start = window.dayjs(a.planDateRange[0]).startOf('day'); + var end = window.dayjs(a.planDateRange[1]).endOf('day'); + list = list.filter(function(r) { + var d = r.planDeliveryEnd ? window.dayjs(r.planDeliveryEnd) : null; + return d && !d.isBefore(start) && !d.isAfter(end); + }); + } + return list; + }; + + var filteredOngoing = useMemo(function() { + return filterOngoing(ongoingList.slice()); + }, [ongoingList, applied.contractCode, applied.projectName, applied.customerName, applied.creator, applied.planDateRange]); + + var filteredCompleted = useMemo(function() { + var list = completedList.slice(); + if (applied.contractCode) list = list.filter(function(r) { return r.contractCode === applied.contractCode; }); + if (applied.projectName) { var pl = getProjectLabel(applied.projectName); if (pl) list = list.filter(function(r) { return r.projectName === pl; }); } + if (applied.customerName) { var cl = getCustomerLabel(applied.customerName); if (cl) list = list.filter(function(r) { return r.customerName === cl; }); } + if (applied.creator) { var ul = getCreatorLabel(applied.creator); if (ul) list = list.filter(function(r) { return r.creator === ul; }); } + if (applied.planDateRange && applied.planDateRange.length === 2 && window.dayjs) { + var start = window.dayjs(applied.planDateRange[0]).startOf('day'); + var end = window.dayjs(applied.planDateRange[1]).endOf('day'); + list = list.filter(function(r) { + var planEnd = r.planDeliveryDisplay && r.planDeliveryDisplay.indexOf('至') >= 0 + ? r.planDeliveryDisplay.split('至')[1] + : r.planDeliveryDisplay; + var d = planEnd ? window.dayjs(planEnd) : null; + return d && !d.isBefore(start) && !d.isAfter(end); + }); + } + return list; + }, [applied.contractCode, applied.projectName, applied.customerName, applied.creator, applied.planDateRange]); + + var handleQuery = function() { + setApplied({ + contractCode: _contractCode[0], + projectName: _projectName[0], + customerName: _customerName[0], + planDateRange: _planDateRange[0] && _planDateRange[0].length === 2 ? _planDateRange[0] : null, + creator: _creator[0] + }); + message.info('已按筛选条件查询'); + }; + + var handleReset = function() { + _contractCode[1](undefined); + _projectName[1](undefined); + _customerName[1](undefined); + _planDateRange[1](null); + _creator[1](undefined); + setApplied({ + contractCode: undefined, + projectName: undefined, + customerName: undefined, + planDateRange: null, + creator: undefined + }); + }; + + var handleDisableEnable = function(record) { + if (record.enabled) { + Modal.confirm({ + title: '是否确认停用该交车任务?', + okText: '确定', + cancelText: '取消', + onOk: function() { + setOngoingList(function(prev) { + return prev.map(function(r) { return r.id === record.id ? Object.assign({}, r, { enabled: false }) : r; }); + }); + message.success('已停用'); + } + }); + return; + } + // 启用:判断预计交车结束日期是否已过期 + var dayjs = window.dayjs; + var endDate = record.planDeliveryEnd ? (dayjs ? dayjs(record.planDeliveryEnd) : null) : null; + var now = dayjs ? dayjs() : null; + if (endDate && now && endDate.isBefore(now, 'day')) { + _rescheduleTask[1](record); + _rescheduleDateRange[1](null); + _rescheduleModalVisible[1](true); + return; + } + Modal.confirm({ + title: '是否确认启用该交车任务?', + okText: '确定', + cancelText: '取消', + onOk: function() { + setOngoingList(function(prev) { + return prev.map(function(r) { return r.id === record.id ? Object.assign({}, r, { enabled: true }) : r; }); + }); + message.success('已启用'); + } + }); + }; + + var handleRescheduleConfirm = function() { + var task = _rescheduleTask[0]; + var range = _rescheduleDateRange[0]; + if (!task || !range || range.length !== 2) { + message.warning('请选择预计交车日期(单日请选择同一天为开始和结束)'); + return; + } + var dayjs = window.dayjs; + var startStr = dayjs ? dayjs(range[0]).format('YYYY-MM-DD') : String(range[0]); + var endStr = dayjs ? dayjs(range[1]).format('YYYY-MM-DD') : String(range[1]); + var planDisplay = startStr === endStr ? startStr : startStr + '至' + endStr; + setOngoingList(function(prev) { + return prev.map(function(r) { + if (r.id !== task.id) return r; + return Object.assign({}, r, { planDeliveryDisplay: planDisplay, planDeliveryEnd: endStr, enabled: true }); + }); + }); + message.success('已重新选择预计交车日期并启用'); + _rescheduleModalVisible[1](false); + _rescheduleTask[1](null); + _rescheduleDateRange[1](null); + }; + + // 进行中表格列 + var ongoingColumns = [ + { title: '合同编码', dataIndex: 'contractCode', key: 'contractCode', width: 180, ellipsis: true, fixed: 'left' }, + { title: '项目名称', dataIndex: 'projectName', key: 'projectName', width: 140, ellipsis: true, fixed: 'left' }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 160, ellipsis: true, fixed: 'left' }, + { title: '交车数量', dataIndex: 'deliveryCount', key: 'deliveryCount', width: 100 }, + { title: '交车区域', dataIndex: 'deliveryRegion', key: 'deliveryRegion', width: 140 }, + { title: '交车地点', dataIndex: 'deliveryLocation', key: 'deliveryLocation', width: 200, ellipsis: true }, + { title: '预计交车日期', dataIndex: 'planDeliveryDisplay', key: 'planDeliveryDisplay', width: 180 }, + { title: '开始计费日期', dataIndex: 'billingStartDate', key: 'billingStartDate', width: 120 }, + { title: '创建人', dataIndex: 'creator', key: 'creator', width: 100 }, + { title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', width: 120 }, + { title: '最后更新人', dataIndex: 'lastUpdater', key: 'lastUpdater', width: 100 }, + { title: '最后更新时间', dataIndex: 'lastUpdatedAt', key: 'lastUpdatedAt', width: 120 }, + { + title: '操作', + key: 'action', + width: 180, + fixed: 'right', + render: function(_, record) { + return React.createElement('span', null, + React.createElement(Button, { type: 'link', size: 'small', onClick: function() { message.info('请参照原型业务管理-交车任务页面'); } }, '查看'), + React.createElement(Button, { type: 'link', size: 'small', onClick: function() { message.info('请参照原型业务管理-交车任务-编辑交车任务页面'); } }, '编辑'), + React.createElement(Button, { + type: 'link', + size: 'small', + onClick: function() { handleDisableEnable(record); } + }, record.enabled ? '停用' : '启用') + ); + } + } + ]; + + // 已完成表格列(交车数量重点色+气泡) + var vehiclePopoverContent = function(vehicles) { + if (!vehicles || !vehicles.length) return React.createElement('span', null, '-'); + var headers = ['车辆类型', '品牌', '型号', '车牌号', '实际交车日期', '交车人']; + return React.createElement('div', { style: { padding: 4, minWidth: 420 } }, + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 12 } }, + React.createElement('thead', null, + React.createElement('tr', null, + headers.map(function(h) { return React.createElement('th', { key: h, style: { padding: '6px 8px', textAlign: 'left', borderBottom: '1px solid #eee', fontWeight: 600 } }, h); }) + ) + ), + React.createElement('tbody', null, + vehicles.map(function(v, i) { + return React.createElement('tr', { key: i }, + React.createElement('td', { style: { padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, v.vehicleType || '-'), + React.createElement('td', { style: { padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, v.brand || '-'), + React.createElement('td', { style: { padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, v.model || '-'), + React.createElement('td', { style: { padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, v.plateNo || '-'), + React.createElement('td', { style: { padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, v.actualDeliveryDate || '-'), + React.createElement('td', { style: { padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, v.deliverer || '-') + ); + }) + ) + ) + ); + }; + + var completedColumns = [ + { title: '合同编码', dataIndex: 'contractCode', key: 'contractCode', width: 180, ellipsis: true, fixed: 'left' }, + { title: '项目名称', dataIndex: 'projectName', key: 'projectName', width: 140, ellipsis: true, fixed: 'left' }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 160, ellipsis: true, fixed: 'left' }, + { + title: '交车数量', + dataIndex: 'deliveryCount', + key: 'deliveryCount', + width: 100, + render: function(count, record) { + var content = vehiclePopoverContent(record.vehicles); + return React.createElement(Popover, { content: content, title: '交车车辆明细', trigger: 'click' }, + React.createElement('span', { style: { color: '#1890ff', cursor: 'pointer', fontWeight: 500 } }, count) + ); + } + }, + { title: '交车区域', dataIndex: 'deliveryRegion', key: 'deliveryRegion', width: 140 }, + { title: '交车地点', dataIndex: 'deliveryLocation', key: 'deliveryLocation', width: 200, ellipsis: true }, + { title: '预计交车日期', dataIndex: 'planDeliveryDisplay', key: 'planDeliveryDisplay', width: 180 }, + { title: '开始计费日期', dataIndex: 'billingStartDate', key: 'billingStartDate', width: 120 }, + { title: '创建人', dataIndex: 'creator', key: 'creator', width: 100 }, + { title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', width: 120 }, + { title: '最后更新人', dataIndex: 'lastUpdater', key: 'lastUpdater', width: 100 }, + { title: '最后更新时间', dataIndex: 'lastUpdatedAt', key: 'lastUpdatedAt', width: 120 }, + { + title: '操作', + key: 'action', + width: 80, + fixed: 'right', + render: function(_, record) { + return React.createElement(Button, { type: 'link', size: 'small', onClick: function() { message.info('请参照原型业务管理-交车任务页面'); } }, '查看'); + } + } + ]; + + var filterOption = function(input, option) { + var label = (option && option.label) ? String(option.label) : ''; + return label.toLowerCase().indexOf((input || '').toLowerCase()) >= 0; + }; + + var filterLabelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var filterItemStyle = { marginBottom: 12 }; + var filterControlStyle = { width: '100%' }; + + var filterItems = [ + React.createElement('div', { key: 'contractCode', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '合同编码'), + React.createElement(Select, { + placeholder: '请输入或选择合同编码', + style: filterControlStyle, + value: _contractCode[0], + onChange: function(v) { _contractCode[1](v); }, + options: contractOptions, + showSearch: true, + allowClear: true, + filterOption: filterOption + })), + React.createElement('div', { key: 'projectName', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '项目名称'), + React.createElement(Select, { + placeholder: '请输入或选择项目名称', + style: filterControlStyle, + value: _projectName[0], + onChange: function(v) { _projectName[1](v); }, + options: projectOptions, + showSearch: true, + allowClear: true, + filterOption: filterOption + })), + React.createElement('div', { key: 'customerName', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '客户名称'), + React.createElement(Select, { + placeholder: '请输入或选择客户名称', + style: filterControlStyle, + value: _customerName[0], + onChange: function(v) { _customerName[1](v); }, + options: customerOptions, + showSearch: true, + allowClear: true, + filterOption: filterOption + })), + React.createElement('div', { key: 'planDate', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '预计交车日期'), + React.createElement(RangePicker, { + style: filterControlStyle, + placeholder: ['请选择预计交车开始时间', '请选择预计交车结束时间'], + value: _planDateRange[0] && _planDateRange[0].length === 2 && window.dayjs ? [_planDateRange[0][0], _planDateRange[0][1]] : null, + onChange: function(dates) { _planDateRange[1](dates && dates.length === 2 ? dates : null); } + })), + React.createElement('div', { key: 'creator', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '创建人'), + React.createElement(Select, { + placeholder: '请输入或选择创建人', + style: filterControlStyle, + value: _creator[0], + onChange: function(v) { _creator[1](v); }, + options: creatorOptions, + showSearch: true, + allowClear: true, + filterOption: filterOption + })) + ]; + + var filterCount = _filterExpanded[0] ? 5 : 3; + var filterNodes = []; + for (var i = 0; i < filterCount && i < filterItems.length; i++) { + filterNodes.push(filterItems[i]); + } + + // 表格单元格一行显示、根据内容适应宽度 + var cellNoWrap = function() { return { style: { whiteSpace: 'nowrap' } }; }; + var ongoingColumnsWithNowrap = ongoingColumns.map(function(col) { return Object.assign({}, col, { onCell: cellNoWrap }); }); + var completedColumnsWithNowrap = completedColumns.map(function(col) { return Object.assign({}, col, { onCell: cellNoWrap }); }); + + var layoutStyle = { padding: '16px 24px 48px', backgroundColor: '#f5f5f5', minHeight: '100vh' }; + + var reqTitleStyle = { fontSize: 18, fontWeight: 600, marginBottom: 16, color: 'rgba(0,0,0,0.85)' }; + var reqSectionStyle = { fontSize: 15, fontWeight: 600, marginTop: 16, marginBottom: 8, color: 'rgba(0,0,0,0.85)' }; + var reqSubStyle = { fontSize: 14, fontWeight: 500, marginLeft: 16, marginTop: 8, marginBottom: 4, color: 'rgba(0,0,0,0.85)' }; + var reqItemStyle = { fontSize: 13, marginLeft: 32, marginTop: 4, marginBottom: 2, lineHeight: 1.6, color: 'rgba(0,0,0,0.75)' }; + var reqSubItemStyle = { fontSize: 13, marginLeft: 48, marginTop: 2, marginBottom: 2, lineHeight: 1.6, color: 'rgba(0,0,0,0.7)' }; + var reqBlockStyle = { marginBottom: 8 }; + + var reqSpecContent = React.createElement('div', { style: { padding: '0 4px' } }, + React.createElement('div', { style: reqTitleStyle }, '交车任务'), + React.createElement('div', { style: reqBlockStyle }, + React.createElement('div', { style: reqSectionStyle }, '1.面包屑:'), + React.createElement('div', { style: reqSubStyle }, '1.1.业务管理-交车任务') + ), + React.createElement('div', { style: reqBlockStyle }, + React.createElement('div', { style: reqSectionStyle }, '2.筛选:'), + React.createElement('div', { style: reqItemStyle }, '支持通过合同编码、项目名称、客户名称、预计交车日期、创建人进行筛选,右侧为查询、重置按钮;'), + React.createElement('div', { style: reqItemStyle }, '2.1.合同编码:选择器,默认为所有合同;提示信息为:请输入或选择合同编码,支持从输入框输入内容进行模糊搜索,下拉显示结果;'), + React.createElement('div', { style: reqItemStyle }, '2.2.项目名称:选择器,默认为所有项目;提示信息为:请输入或选择项目名称,支持从输入框输入内容进行模糊搜索,下拉显示结果;'), + React.createElement('div', { style: reqItemStyle }, '2.3.客户名称:选择器,默认为所有客户;提示信息为:请输入或选择客户名称,支持从输入框输入内容进行模糊搜索,下拉显示结果;'), + React.createElement('div', { style: reqItemStyle }, '2.4.预计交车日期:日期选择器,默认提示信息为:请选择预计交车开始时间 请选择预计交车结束时间,单输入框,双日历,支持时间段选择,精确至天,格式为:YYYY-MM-DD - YYYY-MM-DD;'), + React.createElement('div', { style: reqItemStyle }, '2.5.创建人:选择器,默认为所有创建人;提示信息为:请输入或选择创建人,支持从输入框输入内容进行模糊搜索,下拉显示结果;'), + React.createElement('div', { style: reqItemStyle }, '2.6.查询:点击查询,根据单个或多个筛选条件(且)联动表格进行查询;'), + React.createElement('div', { style: reqItemStyle }, '2.7.重置:点击清空查询条件至默认') + ), + React.createElement('div', { style: reqBlockStyle }, + React.createElement('div', { style: reqSectionStyle }, '3.列表:分为2个tab:进行中、已完成,右侧为新增;'), + React.createElement('div', { style: reqSubStyle }, '3.1进行中:显示所有已新增成功,但未完成交车单提交的任务;列表展示以下内容:合同编码、项目名称、客户名称、交车数量、预计交车日期、开始计费日期、创建人、创建时间、最后更新人、最后更新时间、操作;'), + React.createElement('div', { style: reqItemStyle }, '3.1.1合同编码:显示对应合同编码;'), + React.createElement('div', { style: reqItemStyle }, '3.1.2.项目名称:显示对应项目名称;'), + React.createElement('div', { style: reqItemStyle }, '3.1.3.客户名称:显示对应客户名称;'), + React.createElement('div', { style: reqItemStyle }, '3.1.4.交车数量:显示交车数;'), + React.createElement('div', { style: reqItemStyle }, '3.1.5.交车区域:显示交车省-市;'), + React.createElement('div', { style: reqItemStyle }, '3.1.6.交车地点:显示交车详细地址;'), + React.createElement('div', { style: reqItemStyle }, '3.1.7.预计交车日期:显示预计交车日期,支持某天或某个时间段,格式为YYYY-MM-DD或YYYY-MM-DD至YYYY-MM-DD;'), + React.createElement('div', { style: reqItemStyle }, '3.1.8.开始计费日期:显示开始计费日期,格式为YYYY-MM-DD,在交车单完成电子签章后开始生效;'), + React.createElement('div', { style: reqItemStyle }, '3.1.9.创建人:显示交车任务的创建人用户姓名;'), + React.createElement('div', { style: reqItemStyle }, '3.1.10.创建时间:显示交车任务的创建时间,格式为YYYY-MM-DD;'), + React.createElement('div', { style: reqItemStyle }, '3.1.11.最后更新人:显示交车任务的最后一次更新人用户姓名;'), + React.createElement('div', { style: reqItemStyle }, '3.1.12.最后更新时间:显示交车任务的最后一次更新时间,格式为YYYY-MM-DD;'), + React.createElement('div', { style: reqItemStyle }, '3.1.13.操作:支持查看、编辑、停用/启用;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.13.1.查看:跳转查看交车任务;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.13.2.编辑:跳转编辑交车任务;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.13.3.停用/启用:任务创建后为启用状态,此处为停用,点击停用后,变为启用。停用状态的交车任务不会推送给对应区域运维人员,重新启用时判断预计交车结束日期是否已经过期,如已过期则启用时需要在弹出卡片中重新选择预计交车日期;'), + React.createElement('div', { style: reqSubStyle }, '3.2.已完成:显示所有已新增成功并完成交车单提交的任务,列表展示以下内容:合同编码、项目名称、客户名称、交车数量、预计交车日期、开始计费日期、创建人、创建时间、最后更新人、最后更新时间、操作;'), + React.createElement('div', { style: reqItemStyle }, '3.2.1.合同编码:显示对应合同编码;'), + React.createElement('div', { style: reqItemStyle }, '3.2.2.项目名称:显示对应项目名称;'), + React.createElement('div', { style: reqItemStyle }, '3.2.3.客户名称:显示对应客户名称;'), + React.createElement('div', { style: reqItemStyle }, '3.2.4.交车数量:显示交车数,重点色显示,点击弹出气泡卡片,列表显示:车辆类型、品牌、型号、车牌号、实际交车日期、交车人;'), + React.createElement('div', { style: reqSubItemStyle }, '3.2.4.1.车辆类型:显示车辆类型;'), + React.createElement('div', { style: reqSubItemStyle }, '3.2.4.2.品牌:显示车辆品牌;'), + React.createElement('div', { style: reqSubItemStyle }, '3.2.4.3.型号:显示车辆型号;'), + React.createElement('div', { style: reqSubItemStyle }, '3.2.4.4.车牌号:显示交车车辆车牌号,如果该车还未交车则显示为-;'), + React.createElement('div', { style: reqSubItemStyle }, '3.2.4.5.实际交车日期:显示实际交车日期,与列表中显示相同,格式为YYYY-MM-DD,如该车还未交车则显示为-;'), + React.createElement('div', { style: reqSubItemStyle }, '3.2.4.6.交车人:显示实际交车人用户姓名,如该车还未还车则显示为-;'), + React.createElement('div', { style: reqItemStyle }, '3.2.5.预计交车日期:显示预计交车日期,支持某天或某个时间段,格式为YYYY-MM-DD或YYYY-MM-DD至YYYY-MM-DD;'), + React.createElement('div', { style: reqItemStyle }, '3.2.6.开始计费日期:显示开始计费日期,格式为YYYY-MM-DD,在交车单完成电子签章后开始生效;'), + React.createElement('div', { style: reqItemStyle }, '3.2.7.创建人:显示交车任务的创建人用户姓名;'), + React.createElement('div', { style: reqItemStyle }, '3.2.8.创建时间:显示交车任务的创建时间,格式为YYYY-MM-DD;'), + React.createElement('div', { style: reqItemStyle }, '3.2.9.最后更新人:显示交车任务的最后一次更新人用户姓名;'), + React.createElement('div', { style: reqItemStyle }, '3.2.10.最后更新时间:显示交车任务的最后一次更新时间,格式为YYYY-MM-DD;'), + React.createElement('div', { style: reqItemStyle }, '3.2.11.操作:支持查看;') + ), + React.createElement('div', { style: reqBlockStyle }, + React.createElement('div', { style: reqSectionStyle }, '4.列表右下方为分页功能,支持单页显示条目选择;') + ) + ); + + return React.createElement(App, null, + React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + React.createElement(Breadcrumb, { + items: [ + { title: '业务管理' }, + { title: '交车任务' } + ] + }), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function() { _reqSpecOpen[1](true); } }, '查看需求说明') + ), + React.createElement(Card, { style: { marginBottom: 16 } }, + React.createElement('div', { + style: { + display: 'grid', + gridTemplateColumns: '1fr 1fr 1fr', + gap: '16px 24px', + alignItems: 'start', + flex: 1, + minWidth: 0 + } + }, filterNodes), + React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 } }, + React.createElement(Button, { onClick: handleReset }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleQuery }, '查询'), + React.createElement(Button, { type: 'link', size: 'small', onClick: function() { _filterExpanded[1](!_filterExpanded[0]); } }, _filterExpanded[0] ? '收起' : '展开') + ) + ), + React.createElement(Card, null, + React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 } }, + React.createElement(Tabs, { + activeKey: _activeTab[0], + onChange: function(k) { _activeTab[1](k); }, + items: [ + { key: 'ongoing', label: '进行中' }, + { key: 'completed', label: '已完成' } + ] + }), + React.createElement(Button, { type: 'primary', onClick: function() { message.info('请参照原型业务管理-交车任务-新增交车任务页面'); } }, '新增') + ), + _activeTab[0] === 'ongoing' + ? React.createElement(Table, { + rowKey: 'id', + columns: ongoingColumnsWithNowrap, + dataSource: filteredOngoing, + scroll: { x: 1860 }, + size: 'small', + pagination: { + showSizeChanger: true, + showTotal: function(t) { return '共 ' + t + ' 条'; }, + pageSizeOptions: ['10', '20', '50', '100'] + } + }) + : React.createElement(Table, { + rowKey: 'id', + columns: completedColumnsWithNowrap, + dataSource: filteredCompleted, + scroll: { x: 1860 }, + size: 'small', + pagination: { + showSizeChanger: true, + showTotal: function(t) { return '共 ' + t + ' 条'; }, + pageSizeOptions: ['10', '20', '50', '100'] + } + }) + ), + React.createElement(Modal, { + title: '需求说明', + open: _reqSpecOpen[0], + onCancel: function() { _reqSpecOpen[1](false); }, + width: 720, + footer: React.createElement(Button, { onClick: function() { _reqSpecOpen[1](false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, reqSpecContent), + React.createElement(Modal, { + title: '预计交车日期已过期,请重新选择预计交车日期', + open: _rescheduleModalVisible[0], + onCancel: function() { + _rescheduleModalVisible[1](false); + _rescheduleTask[1](null); + _rescheduleDateRange[1](null); + }, + onOk: handleRescheduleConfirm, + okText: '确定', + cancelText: '取消' + }, React.createElement('div', { style: { padding: '8px 0' } }, + React.createElement('div', { style: { marginBottom: 8, color: '#666', fontSize: 13 } }, '支持选择单日或开始-结束时间段,单日时开始与结束选同一天即可。'), + React.createElement('span', { style: { marginRight: 8 } }, '预计交车日期:'), + React.createElement(RangePicker, { + placeholder: ['请选择开始日期', '请选择结束日期(单日请选同一天)'], + value: _rescheduleDateRange[0] && _rescheduleDateRange[0].length === 2 && window.dayjs ? [_rescheduleDateRange[0][0], _rescheduleDateRange[0][1]] : null, + onChange: function(dates) { _rescheduleDateRange[1](dates && dates.length === 2 ? dates : null); } + }) + )) + ) + ); +}; +*/ + +export default Component; diff --git a/src/prototypes/oneos-web-business/pages/10-客户管理-编辑.jsx b/src/prototypes/oneos-web-business/pages/10-客户管理-编辑.jsx new file mode 100644 index 0000000..39cdbb2 --- /dev/null +++ b/src/prototypes/oneos-web-business/pages/10-客户管理-编辑.jsx @@ -0,0 +1,274 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 业务管理 - 客户管理-编辑(与新增相同,仅客户编码不可修改) + +const Component = function () { + var useState = React.useState; + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Input = antd.Input; + var Select = antd.Select; + var Button = antd.Button; + var Cascader = antd.Cascader; + var message = antd.message; + + // 模拟编辑回显数据(实际由路由/列表传入) + var initialDetail = { + customerCode: 'KH-2025-001', + coopStatus: '已合作', + customerName: '嘉兴某某物流有限公司', + cityCascader: ['浙江省', '嘉兴市'], + address: '浙江省嘉兴市南湖区科技大道1号', + region: '华东', + contact: '张三', + contactMobile: '13800138001', + contactPhone: '0571-88888888', + email: 'zhangsan@example.com', + creditCodeOrId: '91330400MA2XXXXX1', + businessManager: '张经理', + remark: '', + taxId: '91330400MA2XXXXX1', + invoiceAddress: '浙江省嘉兴市南湖区科技大道1号', + invoicePhone: '0571-88888888', + account: '6222021234567890123', + openingBank: '中国工商银行嘉兴分行', + mailingAddress: '浙江省嘉兴市南湖区科技大道1号' + }; + + var customerCode = useState(initialDetail.customerCode); + var coopStatus = useState(initialDetail.coopStatus); + var customerName = useState(initialDetail.customerName); + var cityCascader = useState(initialDetail.cityCascader); + var address = useState(initialDetail.address); + var region = useState(initialDetail.region); + var contact = useState(initialDetail.contact); + var contactMobile = useState(initialDetail.contactMobile); + var contactPhone = useState(initialDetail.contactPhone); + var email = useState(initialDetail.email); + var creditCodeOrId = useState(initialDetail.creditCodeOrId); + var businessManager = useState(initialDetail.businessManager); + var remark = useState(initialDetail.remark); + var taxId = useState(initialDetail.taxId); + var invoiceAddress = useState(initialDetail.invoiceAddress); + var invoicePhone = useState(initialDetail.invoicePhone); + var account = useState(initialDetail.account); + var openingBank = useState(initialDetail.openingBank); + var mailingAddress = useState(initialDetail.mailingAddress); + var formErrors = useState({}); + + var coopStatusOptions = [ + { value: '已合作', label: '已合作' }, + { value: '终止合作', label: '终止合作' }, + { value: '洽谈中', label: '洽谈中' }, + { value: '合约过期', label: '合约过期' } + ]; + var cityCascaderOptions = [ + { value: '浙江省', label: '浙江省', children: [{ value: '杭州市', label: '杭州市' }, { value: '嘉兴市', label: '嘉兴市' }, { value: '宁波市', label: '宁波市' }] }, + { value: '上海市', label: '上海市', children: [{ value: '上海市', label: '上海市' }] }, + { value: '江苏省', label: '江苏省', children: [{ value: '南京市', label: '南京市' }, { value: '苏州市', label: '苏州市' }] }, + { value: '广东省', label: '广东省', children: [{ value: '广州市', label: '广州市' }, { value: '深圳市', label: '深圳市' }] }, + { value: '北京市', label: '北京市', children: [{ value: '北京市', label: '北京市' }] } + ]; + var businessManagerOptions = [ + { value: '张经理', label: '张经理' }, + { value: '李专员', label: '李专员' }, + { value: '王专员', label: '王专员' } + ]; + + // 根据省/直辖市名称匹配区域(城市级联第一级) + function getRegionByProvince(area) { + if (!area) return ''; + var a = String(area).trim(); + if (a === '北京市' || a === '天津市' || a === '河北省' || a === '山西省' || a === '内蒙古') return '华北'; + if (a === '上海市' || a === '江苏省' || a === '浙江省' || a === '安徽省' || a === '福建省' || a === '江西省' || a === '山东省' || a === '台湾省') return '华东'; + if (a === '广东省' || a === '广西省' || a === '海南省' || a === '澳门' || a === '香港') return '华南'; + if (a === '河南省' || a === '湖北省' || a === '湖南省') return '华中'; + if (a === '辽宁省' || a === '吉林省' || a === '黑龙江省') return '东北'; + if (a === '四川省' || a === '云南省' || a === '贵州省' || a === '重庆市' || a === '西藏') return '西南'; + if (a === '陕西省' || a === '甘肃省' || a === '青海省' || a === '宁夏' || a === '新疆') return '西北'; + return ''; + } + + function onCityCascaderChange(value) { + cityCascader[1](value); + region[1](value && value[0] ? getRegionByProvince(value[0]) : ''); + } + + function validate() { + var err = {}; + if (!(coopStatus[0] != null && coopStatus[0] !== '')) err.coopStatus = '请选择合作状态'; + if (!(customerName[0] && String(customerName[0]).trim())) err.customerName = '请输入客户名称'; + if (!(cityCascader[0] && Array.isArray(cityCascader[0]) && cityCascader[0].length >= 2)) err.city = '请选择城市(省-市)'; + if (!(address[0] && String(address[0]).trim())) err.address = '请输入客户地址'; + if (!(contact[0] && String(contact[0]).trim())) err.contact = '请输入联系人'; + if (!(contactMobile[0] && String(contactMobile[0]).trim())) err.contactMobile = '请输入联系人手机'; + if (!(creditCodeOrId[0] && String(creditCodeOrId[0]).trim())) err.creditCodeOrId = '请输入信用代码/身份证'; + if (!(businessManager[0] != null && businessManager[0] !== '')) err.businessManager = '请选择业务经理'; + formErrors[1](err); + return Object.keys(err).length === 0; + } + + function handleSubmit() { + if (!validate()) { + message.warning('请完善必填项'); + return; + } + message.success('保存成功(原型)'); + } + + function handleCancel() { + if (window.__customerEditBack) window.__customerEditBack(); + else message.info('取消'); + } + + var layoutStyle = { padding: '16px 24px 48px', backgroundColor: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; + var formRowStyle = { display: 'flex', flexWrap: 'wrap', marginBottom: 16 }; + var formColStyle = { flex: '0 0 33.33%', minWidth: 200, paddingRight: 16, marginBottom: 8, boxSizing: 'border-box' }; + var formColFullStyle = { flex: '0 0 100%', marginBottom: 8 }; + var labelStyle = { display: 'block', marginBottom: 6, color: 'rgba(0,0,0,0.85)' }; + var labelRequiredStyle = { color: '#ff4d4f', marginRight: 4 }; + var errMsgStyle = { color: '#ff4d4f', fontSize: 12, marginTop: 4 }; + var inputStyle = { width: '100%' }; + var footerStyle = { position: 'fixed', bottom: 0, left: 0, right: 0, padding: '12px 24px', backgroundColor: '#fff', borderTop: '1px solid #e8e8e8', display: 'flex', gap: 12, zIndex: 99 }; + + function FormItem(props) { + var colStyle = props.fullWidth ? formColFullStyle : formColStyle; + return React.createElement('div', { style: colStyle }, + React.createElement('label', { style: labelStyle }, + props.required ? React.createElement('span', { style: labelRequiredStyle }, '*') : null, + props.label + ), + props.children, + props.error ? React.createElement('div', { style: errMsgStyle }, props.error) : null + ); + } + + var basicFields = React.createElement('div', null, + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '客户编码' }, + React.createElement(Input, { value: customerCode[0], style: inputStyle, disabled: true }) + ), + React.createElement(FormItem, { label: '合作状态', required: true, error: formErrors[0].coopStatus }, + React.createElement(Select, { + placeholder: '请选择合作状态', + style: inputStyle, + value: coopStatus[0], + onChange: function (v) { coopStatus[1](v); }, + options: coopStatusOptions, + status: formErrors[0].coopStatus ? 'error' : undefined + }) + ), + React.createElement(FormItem, { label: '客户名称', required: true, error: formErrors[0].customerName }, + React.createElement(Input, { placeholder: '请输入客户名称', value: customerName[0], onChange: function (e) { customerName[1](e.target.value); }, style: inputStyle, status: formErrors[0].customerName ? 'error' : undefined }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '城市', required: true, error: formErrors[0].city }, + React.createElement(Cascader, { + options: cityCascaderOptions, + value: cityCascader[0], + onChange: onCityCascaderChange, + placeholder: '级联选择器支持2级,省-市即可', + allowClear: true, + style: inputStyle + }) + ), + React.createElement(FormItem, { label: '客户地址', required: true, error: formErrors[0].address }, + React.createElement(Input, { placeholder: '请输入客户地址', value: address[0], onChange: function (e) { address[1](e.target.value); }, style: inputStyle, status: formErrors[0].address ? 'error' : undefined }) + ), + React.createElement(FormItem, { label: '区域' }, + React.createElement(Input, { placeholder: '根据城市自动匹配', value: region[0], readOnly: true, style: Object.assign({}, inputStyle, { backgroundColor: '#f5f5f5', cursor: 'default' }) }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '联系人', required: true, error: formErrors[0].contact }, + React.createElement(Input, { placeholder: '请输入联系人', value: contact[0], onChange: function (e) { contact[1](e.target.value); }, style: inputStyle, status: formErrors[0].contact ? 'error' : undefined }) + ), + React.createElement(FormItem, { label: '联系人手机', required: true, error: formErrors[0].contactMobile }, + React.createElement(Input, { placeholder: '请输入联系人手机', value: contactMobile[0], onChange: function (e) { contactMobile[1](e.target.value); }, style: inputStyle, status: formErrors[0].contactMobile ? 'error' : undefined }) + ), + React.createElement(FormItem, { label: '联系人座机' }, + React.createElement(Input, { placeholder: '请输入联系人座机', value: contactPhone[0], onChange: function (e) { contactPhone[1](e.target.value); }, style: inputStyle }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '电子邮箱' }, + React.createElement(Input, { placeholder: '请输入邮箱地址', value: email[0], onChange: function (e) { email[1](e.target.value); }, style: inputStyle }) + ), + React.createElement(FormItem, { label: '信用代码/身份证', required: true, error: formErrors[0].creditCodeOrId }, + React.createElement(Input, { placeholder: '请输入信用代码编号/身份证号', value: creditCodeOrId[0], onChange: function (e) { creditCodeOrId[1](e.target.value); }, style: inputStyle, status: formErrors[0].creditCodeOrId ? 'error' : undefined }) + ), + React.createElement(FormItem, { label: '业务经理', required: true, error: formErrors[0].businessManager }, + React.createElement(Select, { + placeholder: '请选择业务经理', + style: inputStyle, + value: businessManager[0], + onChange: function (v) { businessManager[1](v); }, + options: businessManagerOptions, + status: formErrors[0].businessManager ? 'error' : undefined + }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '备注', fullWidth: true }, + React.createElement(Input.TextArea, { placeholder: '请输入备注信息', value: remark[0], onChange: function (e) { remark[1](e.target.value); }, rows: 3, style: inputStyle }) + ) + ) + ); + + var invoiceFields = React.createElement('div', null, + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '纳税人识别号' }, + React.createElement(Input, { placeholder: '请输入纳税人识别号', value: taxId[0], onChange: function (e) { taxId[1](e.target.value); }, style: inputStyle }) + ), + React.createElement(FormItem, { label: '地址' }, + React.createElement(Input, { placeholder: '请输入地址信息', value: invoiceAddress[0], onChange: function (e) { invoiceAddress[1](e.target.value); }, style: inputStyle }) + ), + React.createElement(FormItem, { label: '电话' }, + React.createElement(Input, { placeholder: '请输入电话', value: invoicePhone[0], onChange: function (e) { invoicePhone[1](e.target.value); }, style: inputStyle }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '账户' }, + React.createElement(Input, { placeholder: '请输入账户信息', value: account[0], onChange: function (e) { account[1](e.target.value); }, style: inputStyle }) + ), + React.createElement(FormItem, { label: '开户行' }, + React.createElement(Input, { placeholder: '请输入开户行信息', value: openingBank[0], onChange: function (e) { openingBank[1](e.target.value); }, style: inputStyle }) + ), + React.createElement(FormItem, { label: '邮寄地址' }, + React.createElement(Input, { placeholder: '请输入邮寄地址', value: mailingAddress[0], onChange: function (e) { mailingAddress[1](e.target.value); }, style: inputStyle }) + ) + ) + ); + + var cardTitleStyle = { display: 'flex', alignItems: 'center', gap: 8 }; + var cardTitleBarStyle = { width: 4, height: 16, backgroundColor: '#1890ff', borderRadius: 2 }; + function CardTitleWithBar(text) { + return React.createElement('span', { style: cardTitleStyle }, + React.createElement('span', { style: cardTitleBarStyle }), + text + ); + } + + return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { marginBottom: 16 } }, + React.createElement(Breadcrumb, { + items: [ + { title: '业务管理' }, + { title: '客户管理' }, + { title: '编辑', className: 'breadcrumb-last' } + ] + }) + ), + React.createElement(Card, { title: CardTitleWithBar('客户基本信息'), style: cardStyle }, basicFields), + React.createElement(Card, { title: CardTitleWithBar('客户开票信息'), style: cardStyle }, invoiceFields), + React.createElement('div', { style: { height: 60 } }), + React.createElement('div', { style: footerStyle }, + React.createElement(Button, { type: 'primary', onClick: handleSubmit }, '提交'), + React.createElement(Button, { onClick: handleCancel }, '取消') + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-business/pages/11-客户管理-查看.jsx b/src/prototypes/oneos-web-business/pages/11-客户管理-查看.jsx new file mode 100644 index 0000000..d6ac3c3 --- /dev/null +++ b/src/prototypes/oneos-web-business/pages/11-客户管理-查看.jsx @@ -0,0 +1,167 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 业务管理 - 客户管理-查看(布局同客户管理-新增,所有字段禁用) + +const Component = function () { + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Input = antd.Input; + var Select = antd.Select; + var Button = antd.Button; + var Cascader = antd.Cascader; + + // 模拟查看数据(实际由路由/列表传入) + var detail = { + customerCode: 'KH-2025-001', + coopStatus: '已合作', + customerName: '嘉兴某某物流有限公司', + cityCascader: ['浙江省', '嘉兴市'], + address: '浙江省嘉兴市南湖区科技大道1号', + region: '华东', + contact: '张三', + contactMobile: '13800138001', + contactPhone: '0571-88888888', + email: 'zhangsan@example.com', + creditCodeOrId: '91330400MA2XXXXX1', + businessManager: '张经理', + remark: '', + taxId: '91330400MA2XXXXX1', + invoiceAddress: '浙江省嘉兴市南湖区科技大道1号', + invoicePhone: '0571-88888888', + account: '6222021234567890123', + openingBank: '中国工商银行嘉兴分行', + mailingAddress: '浙江省嘉兴市南湖区科技大道1号' + }; + + var inputStyle = { width: '100%' }; + var layoutStyle = { padding: '16px 24px 48px', backgroundColor: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; + var formRowStyle = { display: 'flex', flexWrap: 'wrap', marginBottom: 16 }; + var formColStyle = { flex: '0 0 33.33%', minWidth: 200, paddingRight: 16, marginBottom: 8, boxSizing: 'border-box' }; + var formColFullStyle = { flex: '0 0 100%', marginBottom: 8 }; + var labelStyle = { display: 'block', marginBottom: 6, color: 'rgba(0,0,0,0.85)' }; + var labelRequiredStyle = { color: '#ff4d4f', marginRight: 4 }; + var footerStyle = { position: 'fixed', bottom: 0, left: 0, right: 0, padding: '12px 24px', backgroundColor: '#fff', borderTop: '1px solid #e8e8e8', display: 'flex', gap: 12, zIndex: 99 }; + + function FormItem(props) { + var colStyle = props.fullWidth ? formColFullStyle : formColStyle; + return React.createElement('div', { style: colStyle }, + React.createElement('label', { style: labelStyle }, + props.required ? React.createElement('span', { style: labelRequiredStyle }, '*') : null, + props.label + ), + props.children + ); + } + + var basicFields = React.createElement('div', null, + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '客户编码' }, + React.createElement(Input, { value: detail.customerCode, style: inputStyle, disabled: true }) + ), + React.createElement(FormItem, { label: '合作状态', required: true }, + React.createElement(Select, { value: detail.coopStatus, style: inputStyle, disabled: true, options: [{ value: '已合作', label: '已合作' }, { value: '终止合作', label: '终止合作' }, { value: '洽谈中', label: '洽谈中' }, { value: '合约过期', label: '合约过期' }] }) + ), + React.createElement(FormItem, { label: '客户名称', required: true }, + React.createElement(Input, { value: detail.customerName, style: inputStyle, disabled: true }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '城市', required: true }, + React.createElement(Cascader, { value: detail.cityCascader, style: inputStyle, disabled: true, options: [{ value: '浙江省', label: '浙江省', children: [{ value: '杭州市', label: '杭州市' }, { value: '嘉兴市', label: '嘉兴市' }, { value: '宁波市', label: '宁波市' }] }, { value: '上海市', label: '上海市', children: [{ value: '上海市', label: '上海市' }] }, { value: '江苏省', label: '江苏省', children: [{ value: '南京市', label: '南京市' }, { value: '苏州市', label: '苏州市' }] }, { value: '广东省', label: '广东省', children: [{ value: '广州市', label: '广州市' }, { value: '深圳市', label: '深圳市' }] }, { value: '北京市', label: '北京市', children: [{ value: '北京市', label: '北京市' }] }] }) + ), + React.createElement(FormItem, { label: '客户地址', required: true }, + React.createElement(Input, { value: detail.address, style: inputStyle, disabled: true }) + ), + React.createElement(FormItem, { label: '区域' }, + React.createElement(Input, { value: detail.region, style: inputStyle, disabled: true }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '联系人', required: true }, + React.createElement(Input, { value: detail.contact, style: inputStyle, disabled: true }) + ), + React.createElement(FormItem, { label: '联系人手机', required: true }, + React.createElement(Input, { value: detail.contactMobile, style: inputStyle, disabled: true }) + ), + React.createElement(FormItem, { label: '联系人座机' }, + React.createElement(Input, { value: detail.contactPhone || '-', style: inputStyle, disabled: true }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '电子邮箱' }, + React.createElement(Input, { value: detail.email, style: inputStyle, disabled: true }) + ), + React.createElement(FormItem, { label: '信用代码/身份证', required: true }, + React.createElement(Input, { value: detail.creditCodeOrId, style: inputStyle, disabled: true }) + ), + React.createElement(FormItem, { label: '业务经理', required: true }, + React.createElement(Select, { value: detail.businessManager, style: inputStyle, disabled: true, options: [{ value: '张经理', label: '张经理' }, { value: '李专员', label: '李专员' }, { value: '王专员', label: '王专员' }] }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '备注', fullWidth: true }, + React.createElement(Input.TextArea, { value: detail.remark || '', rows: 3, style: inputStyle, disabled: true }) + ) + ) + ); + + var invoiceFields = React.createElement('div', null, + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '纳税人识别号' }, + React.createElement(Input, { value: detail.taxId, style: inputStyle, disabled: true }) + ), + React.createElement(FormItem, { label: '地址' }, + React.createElement(Input, { value: detail.invoiceAddress, style: inputStyle, disabled: true }) + ), + React.createElement(FormItem, { label: '电话' }, + React.createElement(Input, { value: detail.invoicePhone, style: inputStyle, disabled: true }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '账户' }, + React.createElement(Input, { value: detail.account, style: inputStyle, disabled: true }) + ), + React.createElement(FormItem, { label: '开户行' }, + React.createElement(Input, { value: detail.openingBank, style: inputStyle, disabled: true }) + ), + React.createElement(FormItem, { label: '邮寄地址' }, + React.createElement(Input, { value: detail.mailingAddress, style: inputStyle, disabled: true }) + ) + ) + ); + + var cardTitleStyle = { display: 'flex', alignItems: 'center', gap: 8 }; + var cardTitleBarStyle = { width: 4, height: 16, backgroundColor: '#1890ff', borderRadius: 2 }; + function CardTitleWithBar(text) { + return React.createElement('span', { style: cardTitleStyle }, + React.createElement('span', { style: cardTitleBarStyle }), + text + ); + } + + function handleBack() { + if (window.__customerViewBack) window.__customerViewBack(); + else window.history && window.history.back ? window.history.back() : void 0; + } + + return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { marginBottom: 16 } }, + React.createElement(Breadcrumb, { + items: [ + { title: '业务管理' }, + { title: '客户管理' }, + { title: '查看', className: 'breadcrumb-last' } + ] + }) + ), + React.createElement(Card, { title: CardTitleWithBar('客户基本信息'), style: cardStyle }, basicFields), + React.createElement(Card, { title: CardTitleWithBar('客户开票信息'), style: cardStyle }, invoiceFields), + React.createElement('div', { style: { height: 60 } }), + React.createElement('div', { style: footerStyle }, + React.createElement(Button, { type: 'primary', onClick: handleBack }, '返回') + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-business/pages/12-客户管理-新增.jsx b/src/prototypes/oneos-web-business/pages/12-客户管理-新增.jsx new file mode 100644 index 0000000..8a3748d --- /dev/null +++ b/src/prototypes/oneos-web-business/pages/12-客户管理-新增.jsx @@ -0,0 +1,251 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 业务管理 - 客户管理-新增(布局参照车辆租赁合同,两卡片:客户基本信息、客户开票信息) + +const Component = function () { + var useState = React.useState; + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Input = antd.Input; + var Select = antd.Select; + var Button = antd.Button; + var Cascader = antd.Cascader; + var message = antd.message; + + var customerCode = useState(''); + var coopStatus = useState(undefined); + var customerName = useState(''); + var cityCascader = useState(undefined); + var address = useState(''); + var region = useState(''); + var contact = useState(''); + var contactMobile = useState(''); + var contactPhone = useState(''); + var email = useState(''); + var creditCodeOrId = useState(''); + var businessManager = useState(undefined); + var remark = useState(''); + var taxId = useState(''); + var invoiceAddress = useState(''); + var invoicePhone = useState(''); + var account = useState(''); + var openingBank = useState(''); + var mailingAddress = useState(''); + var formErrors = useState({}); + + var coopStatusOptions = [ + { value: '已合作', label: '已合作' }, + { value: '终止合作', label: '终止合作' }, + { value: '洽谈中', label: '洽谈中' }, + { value: '合约过期', label: '合约过期' } + ]; + var cityCascaderOptions = [ + { value: '浙江省', label: '浙江省', children: [{ value: '杭州市', label: '杭州市' }, { value: '嘉兴市', label: '嘉兴市' }, { value: '宁波市', label: '宁波市' }] }, + { value: '上海市', label: '上海市', children: [{ value: '上海市', label: '上海市' }] }, + { value: '江苏省', label: '江苏省', children: [{ value: '南京市', label: '南京市' }, { value: '苏州市', label: '苏州市' }] }, + { value: '广东省', label: '广东省', children: [{ value: '广州市', label: '广州市' }, { value: '深圳市', label: '深圳市' }] }, + { value: '北京市', label: '北京市', children: [{ value: '北京市', label: '北京市' }] } + ]; + var businessManagerOptions = [ + { value: '张经理', label: '张经理' }, + { value: '李专员', label: '李专员' }, + { value: '王专员', label: '王专员' } + ]; + + // 根据省/直辖市名称匹配区域(城市级联第一级) + function getRegionByProvince(area) { + if (!area) return ''; + var a = String(area).trim(); + if (a === '北京市' || a === '天津市' || a === '河北省' || a === '山西省' || a === '内蒙古') return '华北'; + if (a === '上海市' || a === '江苏省' || a === '浙江省' || a === '安徽省' || a === '福建省' || a === '江西省' || a === '山东省' || a === '台湾省') return '华东'; + if (a === '广东省' || a === '广西省' || a === '海南省' || a === '澳门' || a === '香港') return '华南'; + if (a === '河南省' || a === '湖北省' || a === '湖南省') return '华中'; + if (a === '辽宁省' || a === '吉林省' || a === '黑龙江省') return '东北'; + if (a === '四川省' || a === '云南省' || a === '贵州省' || a === '重庆市' || a === '西藏') return '西南'; + if (a === '陕西省' || a === '甘肃省' || a === '青海省' || a === '宁夏' || a === '新疆') return '西北'; + return ''; + } + + function onCityCascaderChange(value) { + cityCascader[1](value); + region[1](value && value[0] ? getRegionByProvince(value[0]) : ''); + } + + function validate() { + var err = {}; + if (!(coopStatus[0] != null && coopStatus[0] !== '')) err.coopStatus = '请选择合作状态'; + if (!(customerName[0] && String(customerName[0]).trim())) err.customerName = '请输入客户名称'; + if (!(cityCascader[0] && Array.isArray(cityCascader[0]) && cityCascader[0].length >= 2)) err.city = '请选择城市(省-市)'; + if (!(address[0] && String(address[0]).trim())) err.address = '请输入客户地址'; + if (!(contact[0] && String(contact[0]).trim())) err.contact = '请输入联系人'; + if (!(contactMobile[0] && String(contactMobile[0]).trim())) err.contactMobile = '请输入联系人手机'; + if (!(creditCodeOrId[0] && String(creditCodeOrId[0]).trim())) err.creditCodeOrId = '请输入信用代码/身份证'; + if (!(businessManager[0] != null && businessManager[0] !== '')) err.businessManager = '请选择业务经理'; + formErrors[1](err); + return Object.keys(err).length === 0; + } + + function handleSubmit() { + if (!validate()) { + message.warning('请完善必填项'); + return; + } + message.success('提交成功(原型)'); + } + + function handleCancel() { + if (window.__customerAddBack) window.__customerAddBack(); + else message.info('取消'); + } + + var layoutStyle = { padding: '16px 24px 48px', backgroundColor: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; + var formRowStyle = { display: 'flex', flexWrap: 'wrap', marginBottom: 16 }; + var formColStyle = { flex: '0 0 33.33%', minWidth: 200, paddingRight: 16, marginBottom: 8, boxSizing: 'border-box' }; + var formColFullStyle = { flex: '0 0 100%', marginBottom: 8 }; + var labelStyle = { display: 'block', marginBottom: 6, color: 'rgba(0,0,0,0.85)' }; + var labelRequiredStyle = { color: '#ff4d4f', marginRight: 4 }; + var errMsgStyle = { color: '#ff4d4f', fontSize: 12, marginTop: 4 }; + var inputStyle = { width: '100%' }; + var footerStyle = { position: 'fixed', bottom: 0, left: 0, right: 0, padding: '12px 24px', backgroundColor: '#fff', borderTop: '1px solid #e8e8e8', display: 'flex', gap: 12, zIndex: 99 }; + + function FormItem(props) { + var colStyle = props.fullWidth ? formColFullStyle : formColStyle; + return React.createElement('div', { style: colStyle }, + React.createElement('label', { style: labelStyle }, + props.required ? React.createElement('span', { style: labelRequiredStyle }, '*') : null, + props.label + ), + props.children, + props.error ? React.createElement('div', { style: errMsgStyle }, props.error) : null + ); + } + + var basicFields = React.createElement('div', null, + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '客户编码' }, + React.createElement(Input, { placeholder: '请输入客户编码', value: customerCode[0], onChange: function (e) { customerCode[1](e.target.value); }, style: inputStyle }) + ), + React.createElement(FormItem, { label: '合作状态', required: true, error: formErrors[0].coopStatus }, + React.createElement(Select, { + placeholder: '请选择合作状态', + style: inputStyle, + value: coopStatus[0], + onChange: function (v) { coopStatus[1](v); }, + options: coopStatusOptions, + status: formErrors[0].coopStatus ? 'error' : undefined + }) + ), + React.createElement(FormItem, { label: '客户名称', required: true, error: formErrors[0].customerName }, + React.createElement(Input, { placeholder: '请输入客户名称', value: customerName[0], onChange: function (e) { customerName[1](e.target.value); }, style: inputStyle, status: formErrors[0].customerName ? 'error' : undefined }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '城市', required: true, error: formErrors[0].city }, + React.createElement(Cascader, { + options: cityCascaderOptions, + value: cityCascader[0], + onChange: onCityCascaderChange, + placeholder: '级联选择器支持2级,省-市即可', + allowClear: true, + style: inputStyle + }) + ), + React.createElement(FormItem, { label: '客户地址', required: true, error: formErrors[0].address }, + React.createElement(Input, { placeholder: '请输入客户地址', value: address[0], onChange: function (e) { address[1](e.target.value); }, style: inputStyle, status: formErrors[0].address ? 'error' : undefined }) + ), + React.createElement(FormItem, { label: '区域' }, + React.createElement(Input, { placeholder: '根据城市自动匹配', value: region[0], readOnly: true, style: Object.assign({}, inputStyle, { backgroundColor: '#f5f5f5', cursor: 'default' }) }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '联系人', required: true, error: formErrors[0].contact }, + React.createElement(Input, { placeholder: '请输入联系人', value: contact[0], onChange: function (e) { contact[1](e.target.value); }, style: inputStyle, status: formErrors[0].contact ? 'error' : undefined }) + ), + React.createElement(FormItem, { label: '联系人手机', required: true, error: formErrors[0].contactMobile }, + React.createElement(Input, { placeholder: '请输入联系人手机', value: contactMobile[0], onChange: function (e) { contactMobile[1](e.target.value); }, style: inputStyle, status: formErrors[0].contactMobile ? 'error' : undefined }) + ), + React.createElement(FormItem, { label: '联系人座机' }, + React.createElement(Input, { placeholder: '请输入联系人座机', value: contactPhone[0], onChange: function (e) { contactPhone[1](e.target.value); }, style: inputStyle }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '电子邮箱' }, + React.createElement(Input, { placeholder: '请输入邮箱地址', value: email[0], onChange: function (e) { email[1](e.target.value); }, style: inputStyle }) + ), + React.createElement(FormItem, { label: '信用代码/身份证', required: true, error: formErrors[0].creditCodeOrId }, + React.createElement(Input, { placeholder: '请输入信用代码编号/身份证号', value: creditCodeOrId[0], onChange: function (e) { creditCodeOrId[1](e.target.value); }, style: inputStyle, status: formErrors[0].creditCodeOrId ? 'error' : undefined }) + ), + React.createElement(FormItem, { label: '业务经理', required: true, error: formErrors[0].businessManager }, + React.createElement(Select, { + placeholder: '请选择业务经理', + style: inputStyle, + value: businessManager[0], + onChange: function (v) { businessManager[1](v); }, + options: businessManagerOptions, + status: formErrors[0].businessManager ? 'error' : undefined + }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '备注', fullWidth: true }, + React.createElement(Input.TextArea, { placeholder: '请输入备注信息', value: remark[0], onChange: function (e) { remark[1](e.target.value); }, rows: 3, style: inputStyle }) + ) + ) + ); + + var invoiceFields = React.createElement('div', null, + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '纳税人识别号' }, + React.createElement(Input, { placeholder: '请输入纳税人识别号', value: taxId[0], onChange: function (e) { taxId[1](e.target.value); }, style: inputStyle }) + ), + React.createElement(FormItem, { label: '地址' }, + React.createElement(Input, { placeholder: '请输入地址信息', value: invoiceAddress[0], onChange: function (e) { invoiceAddress[1](e.target.value); }, style: inputStyle }) + ), + React.createElement(FormItem, { label: '电话' }, + React.createElement(Input, { placeholder: '请输入电话', value: invoicePhone[0], onChange: function (e) { invoicePhone[1](e.target.value); }, style: inputStyle }) + ) + ), + React.createElement('div', { style: formRowStyle }, + React.createElement(FormItem, { label: '账户' }, + React.createElement(Input, { placeholder: '请输入账户信息', value: account[0], onChange: function (e) { account[1](e.target.value); }, style: inputStyle }) + ), + React.createElement(FormItem, { label: '开户行' }, + React.createElement(Input, { placeholder: '请输入开户行信息', value: openingBank[0], onChange: function (e) { openingBank[1](e.target.value); }, style: inputStyle }) + ), + React.createElement(FormItem, { label: '邮寄地址' }, + React.createElement(Input, { placeholder: '请输入邮寄地址', value: mailingAddress[0], onChange: function (e) { mailingAddress[1](e.target.value); }, style: inputStyle }) + ) + ) + ); + + var cardTitleStyle = { display: 'flex', alignItems: 'center', gap: 8 }; + var cardTitleBarStyle = { width: 4, height: 16, backgroundColor: '#1890ff', borderRadius: 2 }; + function CardTitleWithBar(text) { + return React.createElement('span', { style: cardTitleStyle }, + React.createElement('span', { style: cardTitleBarStyle }), + text + ); + } + + return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { marginBottom: 16 } }, + React.createElement(Breadcrumb, { + items: [ + { title: '业务管理' }, + { title: '客户管理' }, + { title: '新增客户', className: 'breadcrumb-last' } + ] + }) + ), + React.createElement(Card, { title: CardTitleWithBar('客户基本信息'), style: cardStyle }, basicFields), + React.createElement(Card, { title: CardTitleWithBar('客户开票信息'), style: cardStyle }, invoiceFields), + React.createElement('div', { style: { height: 60 } }), + React.createElement('div', { style: footerStyle }, + React.createElement(Button, { type: 'primary', onClick: handleSubmit }, '提交'), + React.createElement(Button, { onClick: handleCancel }, '取消') + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-business/pages/13-客户管理.jsx b/src/prototypes/oneos-web-business/pages/13-客户管理.jsx new file mode 100644 index 0000000..21e4780 --- /dev/null +++ b/src/prototypes/oneos-web-business/pages/13-客户管理.jsx @@ -0,0 +1,323 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 业务管理 - 客户管理(2026年3月12日版本) + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Table = antd.Table; + var Button = antd.Button; + var Select = antd.Select; + var Input = antd.Input; + var Space = antd.Space; + var Modal = antd.Modal; + var Cascader = antd.Cascader; + var message = antd.message; + + var requirementModalVisible = useState(false); + var filterExpanded = useState(false); + var filterCustomerCode = useState(undefined); + var filterCoopStatus = useState([]); + var filterRegion = useState([]); + var filterCity = useState(undefined); + var filterBusinessManager = useState(undefined); + var filterCreator = useState(undefined); + var customerNameKeyword = useState(''); + var deleteModalOpen = useState(false); + var deleteTargetRecord = useState(null); + + var regionOptions = [ + { value: '华北', label: '华北' }, + { value: '华东', label: '华东' }, + { value: '华南', label: '华南' }, + { value: '华中', label: '华中' }, + { value: '东北', label: '东北' }, + { value: '西南', label: '西南' }, + { value: '西北', label: '西北' } + ]; + var coopStatusOptions = [ + { value: '已合作', label: '已合作' }, + { value: '终止合作', label: '终止合作' }, + { value: '洽谈中', label: '洽谈中' }, + { value: '合约过期', label: '合约过期' } + ]; + var cityCascaderOptions = [ + { value: '浙江省', label: '浙江省', children: [{ value: '杭州市', label: '杭州市' }, { value: '嘉兴市', label: '嘉兴市' }, { value: '宁波市', label: '宁波市' }] }, + { value: '上海市', label: '上海市', children: [{ value: '上海市', label: '上海市' }] }, + { value: '江苏省', label: '江苏省', children: [{ value: '南京市', label: '南京市' }, { value: '苏州市', label: '苏州市' }] }, + { value: '广东省', label: '广东省', children: [{ value: '广州市', label: '广州市' }, { value: '深圳市', label: '深圳市' }] }, + { value: '北京市', label: '北京市', children: [{ value: '北京市', label: '北京市' }] } + ]; + + var listDataState = useState([ + { id: 1, customerCode: 'KH-2025-001', coopStatus: '已合作', customerName: '嘉兴某某物流有限公司', region: '华东', city: '浙江省-嘉兴市', creditCodeOrId: '91330400MA2XXXXX1', contact: '张三', contactMobile: '13800138001', contactPhone: '0571-88888888', businessManagers: '张经理', address: '浙江省嘉兴市南湖区科技大道1号', email: 'zhangsan@example.com', remark: '', creator: '李四', createTime: '2025-01-10 09:30' }, + { id: 2, customerCode: 'KH-2025-002', coopStatus: '洽谈中', customerName: '上海某某运输公司', region: '华东', city: '上海市-上海市', creditCodeOrId: '91310000MA2YYYYY2', contact: '王五', contactMobile: '13900139002', contactPhone: '-', businessManagers: '李专员', address: '上海市浦东新区张江镇', email: 'wangwu@example.com', remark: '意向客户', creator: '李专员', createTime: '2025-02-15 14:20' }, + { id: 3, customerCode: 'KH-2025-003', coopStatus: '已合作', customerName: '杭州某某租赁有限公司', region: '华东', city: '浙江省-杭州市', creditCodeOrId: '91330100MA2ZZZZZ3', contact: '赵六', contactMobile: '13700137003', contactPhone: '0571-99999999', businessManagers: '张经理、王专员', address: '浙江省杭州市余杭区未来科技城', email: 'zhaoliu@example.com', remark: '', creator: '张经理', createTime: '2025-01-20 11:00' }, + { id: 4, customerCode: 'KH-2024-012', coopStatus: '终止合作', customerName: '某某旧客户公司', region: '华南', city: '广东省-广州市', creditCodeOrId: '91440100MA2AAAAA4', contact: '孙七', contactMobile: '13600136004', contactPhone: '-', businessManagers: '李专员', address: '广东省广州市天河区', email: 'sunqi@example.com', remark: '已终止', creator: '李四', createTime: '2024-06-01 10:00' }, + { id: 5, customerCode: 'KH-2025-004', coopStatus: '合约过期', customerName: '南京某某供应链公司', region: '华东', city: '江苏省-南京市', creditCodeOrId: '91320100MA2BBBBB5', contact: '周八', contactMobile: '13500135005', contactPhone: '025-66666666', businessManagers: '王专员', address: '江苏省南京市江宁区', email: 'zhouba@example.com', remark: '待续约', creator: '王专员', createTime: '2025-03-01 08:45' } + ]); + var listData = listDataState[0]; + var setListData = listDataState[1]; + + var customerCodeOptions = useMemo(function () { + var arr = []; + listData.forEach(function (r) { + if (r.customerCode && arr.every(function (o) { return o.value !== r.customerCode; })) arr.push({ value: r.customerCode, label: r.customerCode }); + }); + return arr; + }, [listData]); + var businessManagerOptions = useMemo(function () { + var set = new Set(); + listData.forEach(function (r) { + var managers = (r.businessManagers || '').split(/[、,,]/).map(function (s) { return s.trim(); }).filter(Boolean); + managers.forEach(function (m) { set.add(m); }); + }); + return Array.from(set).map(function (v) { return { value: v, label: v }; }); + }, [listData]); + var creatorOptions = useMemo(function () { + var arr = []; + listData.forEach(function (r) { + if (r.creator && arr.every(function (o) { return o.value !== r.creator; })) arr.push({ value: r.creator, label: r.creator }); + }); + return arr; + }, [listData]); + + var filteredList = useMemo(function () { + var list = listData; + var code = filterCustomerCode[0]; + var statuses = filterCoopStatus[0] || []; + var regions = filterRegion[0] || []; + var cityVal = filterCity[0]; + var manager = filterBusinessManager[0]; + var creator = filterCreator[0]; + var nameKw = (customerNameKeyword[0] || '').trim().toLowerCase(); + if (code) list = list.filter(function (r) { return r.customerCode === code; }); + if (statuses.length) list = list.filter(function (r) { return statuses.indexOf(r.coopStatus) !== -1; }); + if (regions.length) list = list.filter(function (r) { return regions.indexOf(r.region) !== -1; }); + if (cityVal && Array.isArray(cityVal) && cityVal.length >= 2) list = list.filter(function (r) { return r.city === cityVal[0] + '-' + cityVal[1]; }); + if (manager) list = list.filter(function (r) { return (r.businessManagers || '').indexOf(manager) !== -1; }); + if (creator) list = list.filter(function (r) { return r.creator === creator; }); + if (nameKw) list = list.filter(function (r) { return (r.customerName || '').toLowerCase().indexOf(nameKw) !== -1; }); + return list; + }, [listData, filterCustomerCode[0], filterCoopStatus[0], filterRegion[0], filterCity[0], filterBusinessManager[0], filterCreator[0], customerNameKeyword[0]]); + + function handleReset() { + filterCustomerCode[1](undefined); + filterCoopStatus[1]([]); + filterRegion[1]([]); + filterCity[1](undefined); + filterBusinessManager[1](undefined); + filterCreator[1](undefined); + } + + function goView(record) { + if (window.__customerView) window.__customerView(record); + else message.info('跳转客户管理-查看(原型)'); + } + function goEdit(record) { + if (window.__customerEdit) window.__customerEdit(record); + else message.info('跳转客户管理-编辑(原型)'); + } + function openDeleteConfirm(record) { + deleteTargetRecord[1](record); + deleteModalOpen[1](true); + } + function confirmDelete() { + var record = deleteTargetRecord[0]; + if (record) { + setListData(function (prev) { return prev.filter(function (r) { return r.id !== record.id; }); }); + message.success('已删除'); + } + deleteModalOpen[1](false); + deleteTargetRecord[1](null); + } + + var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; + var filterLabelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var filterItemStyle = { marginBottom: 12 }; + var filterControlStyle = { width: '100%' }; + + var columns = [ + { title: '客户编码', dataIndex: 'customerCode', key: 'customerCode', width: 120, ellipsis: true, fixed: 'left', render: function (v) { return v || '—'; } }, + { title: '合作状态', dataIndex: 'coopStatus', key: 'coopStatus', width: 100, render: function (v) { return v || '—'; } }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 180, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '区域', dataIndex: 'region', key: 'region', width: 80, render: function (v) { return v || '—'; } }, + { title: '城市', dataIndex: 'city', key: 'city', width: 140, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '信用代码/身份证', dataIndex: 'creditCodeOrId', key: 'creditCodeOrId', width: 180, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '联系人', dataIndex: 'contact', key: 'contact', width: 100, render: function (v) { return v || '—'; } }, + { title: '联系人手机', dataIndex: 'contactMobile', key: 'contactMobile', width: 120, render: function (v) { return v || '—'; } }, + { title: '联系人座机', dataIndex: 'contactPhone', key: 'contactPhone', width: 120, render: function (v) { return v === '-' || !v ? '—' : v; } }, + { title: '业务经理', dataIndex: 'businessManagers', key: 'businessManagers', width: 140, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '客户地址', dataIndex: 'address', key: 'address', width: 200, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '电子邮箱', dataIndex: 'email', key: 'email', width: 180, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '备注', dataIndex: 'remark', key: 'remark', width: 120, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '创建人', dataIndex: 'creator', key: 'creator', width: 100, render: function (v) { return v || '—'; } }, + { title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 160, render: function (v) { return v || '—'; } }, + { + title: '操作', + key: 'action', + width: 180, + fixed: 'right', + render: function (_, record) { + return React.createElement(Space, { size: 'small' }, + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { goView(record); } }, '查看'), + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { goEdit(record); } }, '编辑'), + React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function () { openDeleteConfirm(record); } }, '删除') + ); + } + } + ]; + + var filterItems = [ + React.createElement('div', { key: 'code', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '客户编码'), + React.createElement(Select, { + placeholder: '请选择或输入客户编码', + allowClear: true, + showSearch: true, + style: filterControlStyle, + value: filterCustomerCode[0], + onChange: function (v) { filterCustomerCode[1](v); }, + options: customerCodeOptions, + filterOption: function (input, opt) { return (opt.label || '').toString().toLowerCase().indexOf((input || '').toLowerCase()) !== -1; } + }) + ), + React.createElement('div', { key: 'status', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '合作状态'), + React.createElement(Select, { + placeholder: '请选择合作状态', + allowClear: true, + mode: 'multiple', + style: filterControlStyle, + value: filterCoopStatus[0], + onChange: function (v) { filterCoopStatus[1](v || []); }, + options: coopStatusOptions + }) + ), + React.createElement('div', { key: 'region', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '区域'), + React.createElement(Select, { + placeholder: '请选择区域', + allowClear: true, + mode: 'multiple', + style: filterControlStyle, + value: filterRegion[0], + onChange: function (v) { filterRegion[1](v || []); }, + options: regionOptions + }) + ), + React.createElement('div', { key: 'city', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '城市'), + React.createElement(Cascader, { + options: cityCascaderOptions, + value: filterCity[0], + onChange: function (v) { filterCity[1](v); }, + placeholder: '请选择省-市', + allowClear: true, + style: filterControlStyle + }) + ), + React.createElement('div', { key: 'manager', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '业务经理'), + React.createElement(Select, { + placeholder: '请选择业务经理', + allowClear: true, + style: filterControlStyle, + value: filterBusinessManager[0], + onChange: function (v) { filterBusinessManager[1](v); }, + options: businessManagerOptions + }) + ), + React.createElement('div', { key: 'creator', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '创建人'), + React.createElement(Select, { + placeholder: '请选择创建人', + allowClear: true, + style: filterControlStyle, + value: filterCreator[0], + onChange: function (v) { filterCreator[1](v); }, + options: creatorOptions + }) + ) + ]; + var filterCount = filterExpanded[0] ? 6 : 3; + var filterNodes = filterItems.slice(0, filterCount); + + var requirementContent = '客户管理(2026年3月12日版本)\n一个「数字化资产ONEOS运管平台」中的「客户管理」模块\n#面包屑:业务管理-客户管理;\n\n页面分为2个卡片;\n1.筛选:默认显示首行,可通过展开、收起功能进行展开/收起的切换,右侧为重置、查询按钮;\n#支持客户编码、合作状态、区域、城市、业务经理、创建人等筛选方式;\n1.1.客户编码:选择器,支持输入框内输入内容模糊搜索,下拉显示对应选项;\n1.2.合作状态:选择器,支持多选,选项为已合作、终止合作、洽谈中、合约过期;\n1.3.区域:选择器,支持多选,选项为:华北、华东、华南、华中、东北、西南、西北;\n1.4.城市:地区级联选择器,支持2级选择,格式为省-市;\n1.5.业务经理:选择器,选项为客户表中所有业务经理;\n1.6.创建人:选择器,选项为所有创建用户姓名;\n\n2.客户列表:\n#左上方为客户名称输入框(输入客户名称直接联动),右上角为新增、导出、导入;\n2.1.客户编码:显示客户编码;\n2.2.合作状态:显示合作状态,选项为已合作、终止合作、洽谈中、合约过期;\n2.3.客户名称:显示客户名称;\n2.4.区域:显示客户区域,客户区域由选择城市后自动匹配;\n2.5.城市:显示客户城市,格式为:省-市;\n2.6.信用代码/身份证:显示客户信用代码或身份证,取决于新增时输入的是什么;\n2.7.联系人:显示客户联系人;\n2.8.联系人手机:显示客户联系人手机;\n2.9.联系人座机:显示客户联系人座机,无则显示为-;\n2.10.业务经理:显示客户对应业务经理姓名,可能会存在多个业务经理维护一个客户;\n2.11.客户地址:显示客户地址信息;\n2.12.电子邮箱:显示客户电子邮箱;\n2.13.备注:显示客户备注信息;\n2.14.创建人:显示客户创建人用户姓名;\n2.15.创建时间:显示客户创建时间;\n2.16.操作:查看、编辑、删除;\n 2.16.1.查看:跳转客户管理-查看页;\n 2.16.2.编辑:跳转客户管理-编辑页;\n 2.16.3.删除:二次确认,点击确认后删除,已删除客户无法再被新合同选择客户时选择,但历史数据中该客户信息依然显示;'; + + return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + React.createElement(Breadcrumb, { + items: [ + { title: '业务管理' }, + { title: '客户管理' } + ] + }), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { requirementModalVisible[1](true); } }, '查看需求说明') + ), + React.createElement(Card, { title: '筛选', style: cardStyle }, + React.createElement('div', { + style: { + display: 'grid', + gridTemplateColumns: '1fr 1fr 1fr', + gap: '16px 24px', + alignItems: 'start', + flex: 1, + minWidth: 0 + } + }, filterNodes), + React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 } }, + React.createElement(Button, { onClick: handleReset }, '重置'), + React.createElement(Button, { type: 'primary' }, '查询'), + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { filterExpanded[1](!filterExpanded[0]); } }, filterExpanded[0] ? '收起' : '展开') + ) + ), + React.createElement(Card, { title: '客户列表', style: cardStyle }, + React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 } }, + React.createElement(Input.Search, { + placeholder: '请输入客户名称', + allowClear: true, + value: customerNameKeyword[0], + onChange: function (e) { customerNameKeyword[1](e.target.value); } + , style: { width: 280 } + }), + React.createElement(Space, null, + React.createElement(Button, { type: 'primary' }, '新增'), + React.createElement(Button, null, '导出'), + React.createElement(Button, null, '导入') + ) + ), + React.createElement(Table, { + rowKey: 'id', + columns: columns, + dataSource: filteredList, + pagination: { pageSize: 10, showSizeChanger: true, showTotal: function (t) { return '共 ' + t + ' 条'; } }, + size: 'middle', + bordered: true, + scroll: { x: 2200 } + }) + ), + React.createElement(Modal, { + title: '确认删除', + open: deleteModalOpen[0], + onCancel: function () { deleteModalOpen[1](false); deleteTargetRecord[1](null); }, + onOk: confirmDelete, + okText: '确认', + cancelText: '取消' + }, '确定要删除该客户吗?删除后该客户将无法在新合同中选择,历史数据中仍会保留该客户信息。'), + React.createElement(Modal, { + title: '需求说明', + open: requirementModalVisible[0], + onCancel: function () { requirementModalVisible[1](false); }, + width: 640, + footer: React.createElement(Button, { onClick: function () { requirementModalVisible[1](false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, React.createElement('div', { style: { padding: '8px 0', whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6 } }, requirementContent)) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-business/pages/14-新增交车任务.jsx b/src/prototypes/oneos-web-business/pages/14-新增交车任务.jsx new file mode 100644 index 0000000..15dfde9 --- /dev/null +++ b/src/prototypes/oneos-web-business/pages/14-新增交车任务.jsx @@ -0,0 +1,345 @@ +// 【重要】必须使用 const Component 作为组件变量名 - Axhub 产品原型 +// 数字化资产ONEOS运管平台 - 新增交车任务模块(参照新增租赁合同布局) + +const Component = function() { + var antd = window.antd; + var Input = antd.Input; + var Select = antd.Select; + var Button = antd.Button; + var DatePicker = antd.DatePicker; + var Modal = antd.Modal; + var message = antd.message; + var Option = Select.Option; + var Checkbox = antd.Checkbox; + var Tooltip = antd.Tooltip; + var RangePicker = DatePicker.RangePicker; + + var projectId = React.useState(''); + var selectedProjectId = projectId[0]; + var setSelectedProjectId = projectId[1]; + + var expectedDelivery = React.useState(null); + var expectedDeliveryValue = expectedDelivery[0]; + var setExpectedDelivery = expectedDelivery[1]; + + var billingDate = React.useState(null); + var billingDateValue = billingDate[0]; + var setBillingDate = billingDate[1]; + + // 续签合同:是否需要还车(勾选“不需要还车”则可选已交车车辆),续签时默认不勾选 + var noReturnCarCheckedState = React.useState(false); + var noReturnCarChecked = noReturnCarCheckedState[0]; + var setNoReturnCarChecked = noReturnCarCheckedState[1]; + + var selectedRowKeys = React.useState([]); + var checkedRowKeys = selectedRowKeys[0]; + var setCheckedRowKeys = selectedRowKeys[1]; + + var formErrors = React.useState({}); + var errors = formErrors[0]; + var setErrors = formErrors[1]; + + var reqModalOpen = React.useState(false); + + // Mock:项目列表及车辆样例。deliveryStatus: 'none' 可选,'submitted' 已提交交车任务不可选,'completed' 已完成交车不显示 + var projectList = [ + { id: 'p1', name: '嘉兴某某物流氢能运输项目', contractCode: 'JXZL20260216YW101235A', customerName: '嘉兴某某物流有限公司', deliveryRegion: '浙江省 / 嘉兴市', deliveryLocation: '浙江省嘉兴市南湖区科技大道1号', vehicles: [ + { key: 'v1', seq: 1, brand: '东风', model: '氢燃料电池重卡 H31', plateNo: '浙A10001', vin: 'LFV2BJCH8K3123456', monthRent: '12800', serviceFee: '800', deposit: '30000', remark: '首车', deliveryStatus: 'submitted' }, + { key: 'v2', seq: 2, brand: '东风', model: '氢燃料电池重卡 H31', plateNo: '浙A10002', vin: 'LFV2BJCH8K3123457', monthRent: '12800', serviceFee: '800', deposit: '30000', remark: '', deliveryStatus: 'submitted' }, + { key: 'v3', seq: 3, brand: '福田', model: '智蓝氢能轻卡', plateNo: '', vin: 'LZYTBACR2M1234567', monthRent: '8500', serviceFee: '500', deposit: '20000', remark: '待上牌', deliveryStatus: 'none' }, + { key: 'v4', seq: 4, brand: '重汽', model: '豪沃氢能牵引车', plateNo: '浙F20001', vin: 'ZZ4257N386FZ12345', monthRent: '15000', serviceFee: '1000', deposit: '35000', remark: '', deliveryStatus: 'none' }, + { key: 'v5', seq: 5, brand: '陕汽', model: '德龙氢能自卸', plateNo: '浙F20002', vin: 'SX1313GR456123456', monthRent: '13200', serviceFee: '880', deposit: '32000', remark: '固定线路', deliveryStatus: 'none' } + ]}, + { id: 'p1_r', name: '嘉兴某某物流氢能运输项目(续签合同)', isRenewal: true, contractCode: 'JXZL20270116YW101235B', customerName: '嘉兴某某物流有限公司', deliveryRegion: '浙江省 / 嘉兴市', deliveryLocation: '浙江省嘉兴市南湖区科技大道1号', vehicles: [ + { key: 'v1r_1', seq: 1, brand: '东风', model: '氢燃料电池重卡 H31', plateNo: '浙A10001', vin: 'LFV2BJCH8K3123456', monthRent: '12800', serviceFee: '800', deposit: '30000', remark: '续签合同车辆', deliveryStatus: 'none' }, + { key: 'v1r_0', seq: 2, brand: '东风', model: '氢燃料电池重卡 H31', plateNo: '浙A09999', vin: 'LFV2BJCH8K3000000', monthRent: '12800', serviceFee: '800', deposit: '30000', remark: '续签:已交车车辆样例', deliveryStatus: 'completed' }, + { key: 'v1r_0b', seq: 3, brand: '福田', model: '智蓝氢能轻卡', plateNo: '浙A08888', vin: 'LZYTBACR2M1000001', monthRent: '8500', serviceFee: '500', deposit: '20000', remark: '续签:已交车车辆样例', deliveryStatus: 'completed' }, + { key: 'v1r_0c', seq: 4, brand: '重汽', model: '豪沃氢能牵引车', plateNo: '浙A07777', vin: 'ZZ4257N386FZ00002', monthRent: '15000', serviceFee: '1000', deposit: '35000', remark: '续签:已交车车辆样例', deliveryStatus: 'completed' }, + { key: 'v1r_2', seq: 5, brand: '福田', model: '智蓝氢能轻卡', plateNo: '', vin: 'LZYTBACR2M1234567', monthRent: '8500', serviceFee: '500', deposit: '20000', remark: '续签待上牌', deliveryStatus: 'none' } + ]}, + { id: 'p2', name: '上海某某运输氢能租赁项目', contractCode: 'SHZL20260201YW200123A', customerName: '上海某某运输公司', deliveryRegion: '上海市 / 上海市', deliveryLocation: '上海市浦东新区张江高科技园区', vehicles: [ + { key: 'v6', seq: 1, brand: '上汽红岩', model: '杰狮氢能牵引', plateNo: '沪A30003', vin: 'SH1313HY789012345', monthRent: '14500', serviceFee: '950', deposit: '34000', remark: '', deliveryStatus: 'submitted' }, + { key: 'v7', seq: 2, brand: '宇通', model: '氢能公交 ZK6126', plateNo: '沪B40001', vin: 'LZYTAGCF8K4567890', monthRent: '22000', serviceFee: '1200', deposit: '50000', remark: '示范线路', deliveryStatus: 'none' }, + { key: 'v8', seq: 3, brand: '福田', model: '欧辉氢能大巴', plateNo: '', vin: 'LZYTBACR2M2345678', monthRent: '19800', serviceFee: '1100', deposit: '45000', remark: '', deliveryStatus: 'none' } + ]}, + { id: 'p3', name: '杭州某某租赁氢能项目', contractCode: 'HZZL20260115YW100089A', customerName: '杭州某某租赁有限公司', deliveryRegion: '浙江省 / 杭州市', deliveryLocation: '浙江省杭州市余杭区未来科技城', vehicles: [ + { key: 'v9', seq: 1, brand: '品牌C', model: '型号C1', plateNo: '浙A40004', vin: 'L4234567890ABCDEF', monthRent: '8200', serviceFee: '450', deposit: '20000', remark: '重点客户', deliveryStatus: 'completed' }, + { key: 'v10', seq: 2, brand: '品牌C', model: '型号C2', plateNo: '', vin: 'L5234567890ABCDEF', monthRent: '7800', serviceFee: '420', deposit: '19000', remark: '', deliveryStatus: 'submitted' }, + { key: 'v11', seq: 3, brand: '东风', model: '氢燃料电池厢货', plateNo: '浙A50001', vin: 'LFV2BJCH8K5678901', monthRent: '9200', serviceFee: '520', deposit: '22000', remark: '城配', deliveryStatus: 'none' }, + { key: 'v12', seq: 4, brand: '开沃', model: '创源氢能轻卡', plateNo: '浙A50002', vin: 'LJXTBACR9N6789012', monthRent: '8800', serviceFee: '480', deposit: '21000', remark: '', deliveryStatus: 'none' } + ]} + ]; + + var selectedProject = projectList.find(function(p) { return p.id === selectedProjectId; }); + var isRenewalProject = !!(selectedProject && selectedProject.isRenewal); + var vehicleListRaw = selectedProject ? selectedProject.vehicles : []; + // 2.8.1 勾选「不需要还车」:只显示已交未还车辆;2.8.2 不勾选:显示已交未还+续签新增车辆,已交未还不可勾选。非续签:已完成交车不显示,已提交不可选。 + var vehicleList = vehicleListRaw.filter(function(v) { + if (v.deliveryStatus === 'completed') { + if (!isRenewalProject) return false; + return true; + } + if (v.deliveryStatus === 'submitted') return true; + if (isRenewalProject && !!noReturnCarChecked) return false; + return true; + }); + var selectableVehicles = vehicleList.filter(function(v) { + if (v.deliveryStatus === 'submitted') return false; + if (v.deliveryStatus === 'completed') return isRenewalProject && !!noReturnCarChecked; + return true; + }); + + var handleProjectChange = function(id) { + var nextId = id || ''; + setSelectedProjectId(nextId); + var nextProject = projectList.find(function(p) { return p.id === nextId; }); + var isRenewal = !!(nextProject && nextProject.isRenewal); + setCheckedRowKeys([]); + setNoReturnCarChecked(false); + }; + + var todayStr = (function() { + var d = new Date(); + return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); + })(); + + var validateExpectedDelivery = function() { + if (!expectedDeliveryValue || !expectedDeliveryValue.length) return '请选择预计交车日期'; + var start = expectedDeliveryValue[0] && expectedDeliveryValue[0].format ? expectedDeliveryValue[0].format('YYYY-MM-DD') : null; + var end = expectedDeliveryValue[1] && expectedDeliveryValue[1].format ? expectedDeliveryValue[1].format('YYYY-MM-DD') : null; + if (!start) return '请选择预计交车日期'; + if (end && end < start) return '结束日期不能早于开始日期'; // 结束可与开始相同,即单日 + if (end && end < todayStr) return '结束日期不能早于当前日期'; + return null; + }; + + var needExpectedDelivery = !(isRenewalProject && noReturnCarChecked); + var expectedDeliveryError = needExpectedDelivery ? validateExpectedDelivery() : null; + + var billingDateError = !billingDateValue ? '请选择开始计费日期' : null; + + var handleSubmit = function() { + var err = {}; + if (!selectedProjectId) err.projectName = '请选择项目名称'; + if (needExpectedDelivery && expectedDeliveryError) err.expectedDelivery = expectedDeliveryError; + if (billingDateError) err.billingDate = billingDateError; + if (checkedRowKeys.length === 0 && selectableVehicles.length > 0) err.vehicles = '请至少选择一辆车'; + setErrors(err); + if (Object.keys(err).length) return; + var count = checkedRowKeys.length; + message.success('已为 ' + count + ' 辆车生成交车任务,将按交车区域分配对应运维人员。'); + }; + + var handleCancel = function() { + message.info('取消'); + }; + + var onSelectAll = function(checked) { + if (checked) setCheckedRowKeys(selectableVehicles.map(function(v) { return v.key; })); + else setCheckedRowKeys([]); + }; + + var onSelectRow = function(record, checked) { + if (checked) setCheckedRowKeys(function(prev) { return prev.indexOf(record.key) === -1 ? prev.concat(record.key) : prev; }); + else setCheckedRowKeys(function(prev) { return prev.filter(function(k) { return k !== record.key; }); }); + }; + + var onToggleNoReturnCar = function(e) { + var next = !!(e && e.target ? e.target.checked : e); + setNoReturnCarChecked(next); + if (!next) { + setCheckedRowKeys(function(prev) { + var completedKeys = vehicleList.filter(function(v) { return v && v.deliveryStatus === 'completed'; }).map(function(v) { return v.key; }); + return prev.filter(function(k) { return completedKeys.indexOf(k) === -1; }); + }); + } + }; + + var styles = { + page: { padding: '16px 24px 48px', backgroundColor: '#f5f5f5', minHeight: '100vh', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', fontSize: 14 }, + breadcrumb: { marginBottom: 16, color: '#666' }, + breadcrumbSep: { margin: '0 8px', color: '#999' }, + card: { backgroundColor: '#fff', borderRadius: 8, marginBottom: 16, boxShadow: '0 1px 2px rgba(0,0,0,0.05)', overflow: 'hidden' }, + cardHeader: { padding: '16px 20px', borderBottom: '1px solid #f0f0f0' }, + cardTitle: { fontSize: 16, fontWeight: 600, color: '#333' }, + cardBody: { padding: '20px 24px' }, + formRow: { display: 'flex', flexWrap: 'wrap', marginBottom: 16 }, + formCol: { flex: '0 0 33.33%', minWidth: 200, paddingRight: 16, marginBottom: 8 }, + label: { display: 'block', marginBottom: 6, color: '#333' }, + labelRequired: { color: '#ff4d4f', marginRight: 4 }, + errMsg: { color: '#ff4d4f', fontSize: 12, marginTop: 4 }, + footer: { position: 'fixed', bottom: 0, left: 0, right: 0, padding: '12px 24px', backgroundColor: '#fff', borderTop: '1px solid #e8e8e8', display: 'flex', gap: 12, justifyContent: 'flex-start', zIndex: 99 }, + tableWrap: { marginTop: 16, overflowX: 'auto' }, + table: { width: '100%', borderCollapse: 'collapse', fontSize: 13 }, + th: { padding: '10px 8px', textAlign: 'left', borderBottom: '1px solid #e8e8e8', backgroundColor: '#fafafa', fontWeight: 600 }, + td: { padding: '8px', borderBottom: '1px solid #f0f0f0', verticalAlign: 'middle' }, + inputDisabled: { width: '100%', padding: '6px 10px', border: '1px solid #d9d9d9', borderRadius: 4, backgroundColor: '#f5f5f5', color: '#666', fontSize: 13 }, + modalMask: { position: 'fixed', left: 0, top: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.45)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center' }, + modalBox: { backgroundColor: '#fff', borderRadius: 8, width: '90%', maxWidth: 640, maxHeight: '85vh', overflow: 'hidden', display: 'flex', flexDirection: 'column', boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }, + modalHeader: { padding: '16px 20px', borderBottom: '1px solid #f0f0f0', fontSize: 16, fontWeight: 600 }, + modalBody: { padding: 20, overflow: 'auto', flex: 1 } + }; + + var FormItem = function(props) { + return React.createElement('div', { style: styles.formCol }, + React.createElement('label', { style: styles.label }, props.required ? React.createElement('span', { style: styles.labelRequired }, '*') : null, props.label), + props.children, + props.error ? React.createElement('div', { style: styles.errMsg }, props.error) : null + ); + }; + + var projectOptions = projectList.map(function(p) { return React.createElement(Option, { key: p.id, value: p.id }, p.name); }); + + var formRow1 = React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '选择项目名称', required: true, error: errors.projectName }, + React.createElement(Select, { + placeholder: '请选择或输入项目名称(对应自营合同、租赁合同-项目名称)', + style: { width: '100%' }, + value: selectedProjectId || undefined, + onChange: handleProjectChange, + showSearch: true, + allowClear: true, + filterOption: function(input, opt) { return opt.children && String(opt.children).toLowerCase().indexOf((input || '').toLowerCase()) >= 0; }, + status: errors.projectName ? 'error' : undefined + }, projectOptions)), + React.createElement(FormItem, { label: '合同编码' }, React.createElement(Input, { value: selectedProject ? selectedProject.contractCode : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '客户名称' }, React.createElement(Input, { value: selectedProject ? selectedProject.customerName : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '交车区域' }, React.createElement(Input, { value: selectedProject ? selectedProject.deliveryRegion : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '交车地点' }, React.createElement(Input, { value: selectedProject ? selectedProject.deliveryLocation : '', disabled: true, style: { width: '100%' } })) + ); + + var formRow2 = React.createElement('div', { style: styles.formRow }, + needExpectedDelivery ? React.createElement(FormItem, { label: '预计交车日期', required: true, error: errors.expectedDelivery }, + React.createElement(RangePicker, { + style: { width: '100%' }, + format: 'YYYY-MM-DD', + placeholder: ['请选择开始日期', '请选择结束日期(单日请选同一天)'], + value: expectedDeliveryValue, + onChange: function(dates) { setExpectedDelivery(dates && dates.length === 2 ? dates : null); }, + status: errors.expectedDelivery ? 'error' : undefined + })) : null, + React.createElement(FormItem, { label: '开始计费日期', required: true, error: errors.billingDate }, + React.createElement(DatePicker, { + style: { width: '100%' }, + format: 'YYYY-MM-DD', + placeholder: '请选择日期', + value: billingDateValue, + onChange: function(d, dateStr) { setBillingDate(d); }, + status: errors.billingDate ? 'error' : undefined + })), + isRenewalProject ? React.createElement(FormItem, { label: '已交车辆是否需要还车' }, + React.createElement('div', { style: { display: 'flex', alignItems: 'flex-start', gap: 8, lineHeight: 1.6 } }, + React.createElement(Checkbox, { checked: !!noReturnCarChecked, onChange: onToggleNoReturnCar }, '不需要还车'), + React.createElement('span', { style: { color: '#8c8c8c', fontSize: 12, marginTop: 2 } }, + '勾选不需要还车,则车辆无需交车,', + React.createElement('br', null), + '只设置开始计费日期即可重新生成账单' + ) + ) + ) : null + ); + + var allSelectableChecked = selectableVehicles.length > 0 && selectableVehicles.every(function(v) { return checkedRowKeys.indexOf(v.key) !== -1; }); + var someSelectableChecked = selectableVehicles.some(function(v) { return checkedRowKeys.indexOf(v.key) !== -1; }); + var tableHeader = React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: Object.assign({}, styles.th, { width: 48 }) }, + React.createElement(Checkbox, { + checked: allSelectableChecked, + indeterminate: someSelectableChecked && !allSelectableChecked, + onChange: function(e) { onSelectAll(e.target.checked); } + })), + React.createElement('th', { style: Object.assign({}, styles.th, { width: 56 }) }, '序号'), + React.createElement('th', { style: styles.th }, '品牌'), + React.createElement('th', { style: styles.th }, '型号'), + React.createElement('th', { style: styles.th }, '车牌号'), + React.createElement('th', { style: styles.th }, '车辆识别代码'), + React.createElement('th', { style: styles.th }, '车辆月租金'), + React.createElement('th', { style: styles.th }, '服务费'), + React.createElement('th', { style: styles.th }, '保证金'), + React.createElement('th', { style: styles.th }, '备注') + ) + ); + + var tableBody = React.createElement('tbody', null, + vehicleList.length === 0 + ? React.createElement('tr', null, React.createElement('td', { colSpan: 10, style: Object.assign({}, styles.td, { textAlign: 'center', color: '#999' }) }, '请先选择项目名称,将自动带出该合同下车辆信息')) + : vehicleList.map(function(row) { + var isSubmitted = row.deliveryStatus === 'submitted'; + var isCompleted = row.deliveryStatus === 'completed'; + var completedSelectable = isRenewalProject && !!noReturnCarChecked; + return React.createElement('tr', { key: row.key }, + React.createElement('td', { style: styles.td }, + isSubmitted + ? React.createElement(Tooltip, { title: '该车辆已创建交车任务' }, + React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 6, cursor: 'not-allowed' } }, + React.createElement(Checkbox, { disabled: true, checked: false }), + React.createElement('span', { style: { color: '#999', fontSize: 12 } }, '已提交'))) + : isCompleted + ? React.createElement(Tooltip, { title: '该车辆已完成交车' }, + React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center' } }, + React.createElement(Checkbox, { + disabled: !completedSelectable, + checked: completedSelectable ? (checkedRowKeys.indexOf(row.key) !== -1) : true, + onChange: function(e) { onSelectRow(row, e.target.checked); } + }) + )) + : React.createElement(Checkbox, { checked: checkedRowKeys.indexOf(row.key) !== -1, onChange: function(e) { onSelectRow(row, e.target.checked); } })), + React.createElement('td', { style: styles.td }, row.seq != null ? row.seq : '—'), + React.createElement('td', { style: styles.td }, React.createElement(Input, { value: row.brand, disabled: true, style: styles.inputDisabled })), + React.createElement('td', { style: styles.td }, React.createElement(Input, { value: row.model, disabled: true, style: styles.inputDisabled })), + React.createElement('td', { style: styles.td }, React.createElement(Input, { value: row.plateNo || '-', disabled: true, style: styles.inputDisabled })), + React.createElement('td', { style: styles.td }, React.createElement(Input, { value: row.vin, disabled: true, style: styles.inputDisabled })), + React.createElement('td', { style: styles.td }, React.createElement(Input, { value: row.monthRent + '元', disabled: true, style: styles.inputDisabled })), + React.createElement('td', { style: styles.td }, React.createElement(Input, { value: row.serviceFee + '元', disabled: true, style: styles.inputDisabled })), + React.createElement('td', { style: styles.td }, React.createElement(Input, { value: row.deposit + '元', disabled: true, style: styles.inputDisabled })), + React.createElement('td', { style: styles.td }, React.createElement(Input, { value: row.remark || '-', disabled: true, style: styles.inputDisabled })) + ); + }) + ); + + var tableEl = React.createElement('div', { style: styles.tableWrap }, + React.createElement('table', { style: styles.table }, tableHeader, tableBody) + ); + + if (errors.vehicles) { + tableEl = React.createElement('div', null, + React.createElement('div', { style: Object.assign({}, styles.errMsg, { marginBottom: 8 }) }, errors.vehicles), + tableEl + ); + } + + var reqSpecText = '新增交车任务\n一个「数字化资产ONEOS运管平台」中的「新增交车任务」模块\n1.面包屑:\n1.1.业务管理-交车任务-新增交车任务\n\n2.表单:\n2.1.选择项目名称:必选项,选择器,默认提示文本:请选择或输入项目名称,支持从输入框内输入内容进行模糊搜索,对应自营合同、租赁合同-「项目名称」字段;;\n2.2.合同编码:输入框(禁用状态),根据所选项目名称自动反写合同编码;\n2.3.客户名称:输入框(禁用状态),根据所选项目名称自动反写客户名称;\n2.4.交车区域:输入框(禁用状态),根据所选项目名称自动反写交车区域。提交时根据交车区域,为对应区域运维人员分配对应交车任务;\n2.5.交车地点:输入框(禁用状态),根据所选项目名称自动反写交车地点;\n2.6.预计交车日期:必填项,日期选择器,支持某天或某个时间段两种模式,格式为YYYY-MM-DD或YYYY-MM-DD至YYYY-MM-DD,结束日期不能早于开始日期,并且结束日期不能早于当前日期;\n 2.6.1.如勾选不需要还车,则预计交车日期隐藏,不需要设置;\n2.7.开始计费日期:必填项,日期选择器,支持单日选择,格式为YYYY-MM-DD;\n2.8.已交车辆是否需要还车:勾选框,内容为不需要还车,默认不勾选,不勾选时,无法在车辆列表勾选已交未还的车辆,勾选后,已交未还的车辆也可以勾选,但不会生成新交车任务,只是按照开始计费日期生成账单;\n 2.8.1.勾选时,车辆列表只显示已交未还的车辆清单,不能添加新车(避免已交未还部分车辆开始计费日期计算不精确);\n 2.8.2.不勾选时,车辆列表显示已交未还的车辆清单及续签合同新增车辆,但是已交未还部分的车辆不可勾选;\n2.9.下方为列表,列表拉取该车辆租赁合同对应所有车辆信息(该合同下已提交过交车任务的车辆不可选,已完成交车的车辆不显示在列表中),列表字段为:全选/多选、序号、品牌、型号、车牌号、车辆识别代码、车辆月租金、服务费、保证金、备注;\n 2.9.1.序号:与租赁合同车辆序号对应;\n 2.9.2.全选/多选:支持全选、多选模式,选择对应车辆后,点击提交自动生成被选中车辆交车任务,需要至少选择1辆,才能进行提交,该合同下已提交过交车任务和已交车的车辆不可选,多选框为禁用状态,悬浮时提示:该车辆已完成交车/该车辆已创建交车任务;\n 2.9.3.品牌:输入框(禁用状态),根据所选项目名称自动反写品牌;\n 2.9.4.型号:输入框(禁用状态),根据所选项目名称自动反写型号;\n 2.9.5.车牌号:输入框(禁用状态),根据所选项目名称自动反写车牌号,车牌号可能为空,为空时显示为-;\n 2.9.6.车辆识别代码:输入框(禁用状态),根据所选项目名称自动反写车辆识别代码;\n 2.9.7.车辆月租金:输入框(禁用状态),根据所选项目名称自动反写车辆月租金,后缀为元;\n 2.9.8.服务费:输入框(禁用状态),根据所选项目名称自动反写服务费,后缀为元;\n 2.9.9.保证金:输入框(禁用状态),根据所选项目名称自动反写保证金,后缀为元;\n 2.9.10.备注:输入框(禁用状态),根据所选项目名称自动反写备注信息,备注为空时显示为-;\n2.10.页面底部为提交、取消;'; + + return React.createElement('div', { style: styles.page }, + React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 } }, + React.createElement('div', { style: styles.breadcrumb }, + React.createElement('span', null, '业务管理'), + React.createElement('span', { style: styles.breadcrumbSep }, ' / '), + React.createElement('span', null, '交车任务'), + React.createElement('span', { style: styles.breadcrumbSep }, ' / '), + React.createElement('span', { style: { color: '#1890ff' } }, '新增交车任务')), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function() { reqModalOpen[1](true); } }, '查看需求说明')), + React.createElement('div', { style: styles.card }, + React.createElement('div', { style: styles.cardHeader }, React.createElement('span', { style: styles.cardTitle }, '交车任务')), + React.createElement('div', { style: styles.cardBody }, + formRow1, + formRow2, + tableEl)), + React.createElement('div', { style: { height: 60 } }), + React.createElement('div', { style: styles.footer }, + React.createElement(Button, { type: 'primary', onClick: handleSubmit }, '提交'), + React.createElement(Button, { onClick: handleCancel }, '取消')), + React.createElement(Modal, { + title: '需求说明', + open: reqModalOpen[0], + onCancel: function() { reqModalOpen[1](false); }, + width: 720, + footer: React.createElement(Button, { onClick: function() { reqModalOpen[1](false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, React.createElement('div', { style: { padding: '8px 0' } }, + React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6 } }, reqSpecText))) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-business/pages/15-租赁账单-查看.jsx b/src/prototypes/oneos-web-business/pages/15-租赁账单-查看.jsx new file mode 100644 index 0000000..c233cb6 --- /dev/null +++ b/src/prototypes/oneos-web-business/pages/15-租赁账单-查看.jsx @@ -0,0 +1,347 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 租赁账单-查看(2026年3月11日版本) + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Button = antd.Button; + var Popover = antd.Popover; + var Table = antd.Table; + var Modal = antd.Modal; + var message = antd.message; + + var requirementModalVisible = useState(false); + var servicePopoverRowIndex = useState(null); + + // 模拟:当前账单信息(由路由/接口传入) + var billInfo = useMemo(function () { + return { + contractCode: 'HT-ZL-2025-001', + contractType: '正式合同', + projectName: '嘉兴氢能示范项目', + customerName: '嘉兴某某物流有限公司', + deliveryTaskCode: 'JT-2025-001-A', + billCode: 'HT-ZL-2025-0010001', + period: 1, + billStartDate: '2025-01-01', + billEndDate: '2025-01-31' + }; + }, []); + + var isFirstPeriod = billInfo.period === 1; + + // 车辆明细(只读展示) + var vehicles = useMemo(function () { + return [ + { key: 'v1', index: 1, brand: '东风', model: 'DFH1180', plateNo: '浙A12345', receivableRent: 30000, actualRent: '29800.00', rentRemark: '首期六期一次性付清', discountAmount: '200.00', discountRemark: '首月优惠', discountProof: [{ name: '优惠审批单.pdf' }], receivableDeposit: 10000, serviceItems: [{ name: '代处理费用', receivable: 200, actual: '200.00', discount: '0.00', remark: '' }, { name: '保险上浮', receivable: 500, actual: '480.00', discount: '20.00', remark: '客户协商' }], receivableService: 700, actualService: '680.00' }, + { key: 'v2', index: 2, brand: '福田', model: 'BJ1180', plateNo: '浙A23456', receivableRent: 27000, actualRent: '27000.00', rentRemark: '', discountAmount: '0.00', discountRemark: '', discountProof: [], receivableDeposit: 8000, serviceItems: [{ name: '保养费用', receivable: 300, actual: '300.00', discount: '0.00', remark: '含首保' }], receivableService: 300, actualService: '300.00' }, + { key: 'v3', index: 3, brand: '重汽', model: 'ZZ1187', plateNo: '浙A34567', receivableRent: 31200, actualRent: '31200.00', rentRemark: '', discountAmount: '0.00', discountRemark: '', discountProof: [], receivableDeposit: 10000, serviceItems: [{ name: '代处理费用', receivable: 180, actual: '180.00', discount: '0.00', remark: '' }, { name: '上牌服务', receivable: 400, actual: '400.00', discount: '0.00', remark: '' }], receivableService: 580, actualService: '580.00' } + ]; + }, []); + + var hydrogen = useMemo(function () { + return { receivable: '3580.00', actual: '3500.00', discount: '80.00', discountRemark: '预付款批量减免' }; + }, []); + + var totals = useMemo(function () { + var receivableRent = 0, actualRent = 0, receivableDeposit = 0, receivableService = 0, actualService = 0, discountTotal = 0, serviceDiscountTotal = 0; + vehicles.forEach(function (v) { + receivableRent += Number(v.receivableRent) || 0; + actualRent += parseFloat(v.actualRent) || 0; + receivableDeposit += Number(v.receivableDeposit) || 0; + receivableService += Number(v.receivableService) || 0; + actualService += parseFloat(v.actualService) || 0; + discountTotal += parseFloat(v.discountAmount) || 0; + (v.serviceItems || []).forEach(function (s) { serviceDiscountTotal += parseFloat(s.discount) || 0; }); + }); + var hydrogenReceivable = parseFloat(hydrogen.receivable) || 0; + var hydrogenActual = parseFloat(hydrogen.actual) || 0; + var hydrogenDiscount = parseFloat(hydrogen.discount) || 0; + return { + receivableRent: receivableRent.toFixed(2), + actualRent: actualRent.toFixed(2), + receivableDeposit: receivableDeposit.toFixed(2), + receivableService: receivableService.toFixed(2), + actualService: actualService.toFixed(2), + discountTotal: discountTotal.toFixed(2), + serviceDiscountTotal: serviceDiscountTotal.toFixed(2), + hydrogenReceivable: hydrogenReceivable.toFixed(2), + hydrogenActual: hydrogenActual.toFixed(2), + hydrogenDiscount: hydrogenDiscount.toFixed(2) + }; + }, [vehicles, hydrogen]); + + var receivableTotal = useMemo(function () { + return (parseFloat(totals.receivableRent) + parseFloat(totals.receivableDeposit) + parseFloat(totals.receivableService) + (isFirstPeriod ? parseFloat(totals.hydrogenReceivable) : 0)).toFixed(2); + }, [totals, isFirstPeriod]); + + var actualTotal = useMemo(function () { + var base = parseFloat(totals.actualRent) + parseFloat(totals.receivableDeposit) + parseFloat(totals.actualService) - parseFloat(totals.discountTotal) - parseFloat(totals.serviceDiscountTotal); + if (isFirstPeriod) base += parseFloat(totals.hydrogenActual) - parseFloat(totals.hydrogenDiscount); + return base.toFixed(2); + }, [totals, isFirstPeriod]); + + var invoiceTotal = useMemo(function () { + // 开票总额:同“实收款总额”展示格式,但不包含应收车辆保证金 + var base = parseFloat(totals.actualRent) + parseFloat(totals.actualService) - parseFloat(totals.discountTotal) - parseFloat(totals.serviceDiscountTotal); + if (isFirstPeriod) base += parseFloat(totals.hydrogenActual) - parseFloat(totals.hydrogenDiscount); + return base.toFixed(2); + }, [totals, isFirstPeriod]); + + var receivablePopoverContent = useMemo(function () { + var rows = [ + ['总计应收车辆月租金', totals.receivableRent + ' 元'], + ['总计应收车辆保证金', totals.receivableDeposit + ' 元'], + ['总计应收服务费', totals.receivableService + ' 元'] + ]; + if (isFirstPeriod) rows.push(['氢费预付款应收金额', totals.hydrogenReceivable + ' 元']); + return React.createElement('div', { style: { padding: 8, minWidth: 220 } }, + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 13 } }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '项目'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '金额') + ) + ), + React.createElement('tbody', null, + rows.map(function (r, i) { return React.createElement('tr', { key: i }, React.createElement('td', { style: { padding: '6px 8px' } }, r[0]), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, r[1])); }) + ) + ) + ); + }, [totals, isFirstPeriod]); + + var actualPopoverContent = useMemo(function () { + var rows = [ + ['总计实收车辆月租金', totals.actualRent + ' 元'], + ['总计应收车辆保证金', totals.receivableDeposit + ' 元'], + ['总计实收服务费', totals.actualService + ' 元'], + ['总计减免金额', totals.discountTotal + ' 元'], + ['服务费各项减免费用总和', totals.serviceDiscountTotal + ' 元'] + ]; + if (isFirstPeriod) { + rows.push(['氢费预付款实收金额', totals.hydrogenActual + ' 元']); + rows.push(['氢费预付款减免金额', totals.hydrogenDiscount + ' 元']); + } + return React.createElement('div', { style: { padding: 8, minWidth: 240 } }, + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 13 } }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '项目'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '金额') + ) + ), + React.createElement('tbody', null, + rows.map(function (r, i) { return React.createElement('tr', { key: i }, React.createElement('td', { style: { padding: '6px 8px' } }, r[0]), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, r[1])); }) + ) + ) + ); + }, [totals, isFirstPeriod]); + + var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; + var labelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var formRowStyle = { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px 24px', marginBottom: 16 }; + var formItemStyle = { marginBottom: 12 }; + var valueStyle = { color: 'rgba(0,0,0,0.85)', fontSize: 14, lineHeight: '22px', minHeight: 22 }; + var thBase = { padding: '10px 12px', border: '1px solid #f0f0f0', whiteSpace: 'nowrap' }; + var highlightStyle = { color: '#1890ff', fontWeight: 600, cursor: 'pointer' }; + + var activeServiceIndex = servicePopoverRowIndex[0]; + var setServicePopoverRowIndex = servicePopoverRowIndex[1]; + + function handleBack() { + if (window.__billViewBack) window.__billViewBack(); + else message.info('返回租赁账单列表(原型)'); + } + + var requirementContent = '租赁账单-查看(2026年3月11日版本)\n一个「数字化资产ONEOS运管平台」中的「租赁账单」「查看」模块\n#面包屑:业务管理-租赁账单-收费明细;\n1.账单信息:\n#显示该账单对应合同及账单信息,显示以下字段:\n 1.1.合同编码:显示该账单对应合同编码;\n 1.2.合同类型:显示该账单对应合同类型,包括正式合同、试用合同;\n 1.3.项目名称:显示该账单对应合同中项目名称;\n 1.4.客户名称:显示该账单对应合同中客户名称;\n 1.5.交车任务编码:显示交车任务对应编码;\n 1.6.账单编码:显示该账单编码,账单编码规则为:[合同编码][账单编号]组成,主要用于后期与用友YS系统打通时获取财务收款及发票相关数据;\n 前缀为合同编码,后缀为账单编号,规则为:ZD+4位编号,为该合同下第x份账单,例如:ZD0001为该合同下第1份账单,依次类推;\n 例如:JXZL20260216YW101235AZD0001即为JXZL20260216YW101235A合同下第1份账单;\n 1.7.账单期数:显示该账单期数;\n 1.8.账单开始日期:显示账单开始日期,根据租赁合同及交车任务开始计费日期按规则生成,格式为:YYYY-MM-DD;\n 1.9.账单结束日期:显示账单结束日期,根据租赁合同及交车任务开始计费日期按规则生成,格式为:YYYY-MM-DD;\n2.账单明细:\n#顶部显示应收款总额、实收款总额;\n 2.1.应收款总额:「应收车辆月租金总和」+「应收车辆保证金总和」+「应收服务费总和」+「氢费预付款应收金额」\n 2.2.实收款总额:「实收车辆月租金总和」+「应收车辆保证金总和」+「实收服务费总和」+「氢费预付款实收金额」-「减免费用总和」「服务费各项减免费用总和」\n\n#显示该账单对应车辆列表及相关费用信息,显示以下字段;\n 2.3.序号:与租赁合同及交车任务中该车辆序号一致;\n 2.4.品牌:与租赁合同及交车任务中该车辆品牌一致;\n 2.5.型号:与租赁合同及交车任务中该车辆型号一致;\n 2.6.车牌号:与租赁合同及交车任务中该车辆车牌号一致;\n 2.7.应收车辆月租金:与租赁合同中该车辆车辆月租金一致,格式为:xx.xx元;\n 2.8.实收车辆月租金:与收费明细中实收车辆月租金一致,格式为:xx.xx元;\n 2.9.车辆租金备注:显示收费明细中已填写备注信息;\n 2.10.减免金额:显示收费明细中已填写减免金额;\n 2.11.减免金额备注:显示收费明细中减免金额备注信息;\n 2.12.减免证明:显示已上传减免证明文件,已上传文件一行显示一条,支持点击预览,格式支持各类图片、doc、docx、pdf等格式;\n 2.13.应收车辆保证金:首期与合同中车辆保证金一致,第二期开始为0.00,格式为:xx.xx元;\n 2.14.服务费项目:点击管理,弹出气泡卡片,气泡卡片标题为服务费项目,下方为列表,显示服务项目、应收费用、实收费用、备注;\n 2.14.1.服务项目:显示租赁合同中所有服务项目名称;\n 2.14.2.应收费用:显示租赁合同中所有服务项目对应费用,格式为:xx.xx元;\n 2.14.3.实收费用:显示收费明细中填写的实收费用金额,精确至2位小数,后缀为元,默认与应收费用一致;\n 2.14.4.减免费用:显示收费明细中填写的减免费用金额,精确至2位小数,后缀为元;\n 2.14.4.备注:显示收费明细中填写的备注信息;\n 2.15.应收服务费:显示当前车辆所有服务费应收费用总和,格式为:xx.xx元;\n 2.16.实收服务费:显示当前车辆所有服务费实收费用总和,格式为:xx.xx元;\n#列表下方为氢费预付款情况,首期账单显示,第二期账单开始无此区域内容显示;\n 2.17.氢费预付款应收金额:显示合同中氢费预付款金额;\n 2.18.氢费预付款实收金额:显示收费明细中填写的氢费预付款实收金额,格式为:xx.xx元;\n 2.19.减免金额:显示收费明细中填写的减免金额,格式为:xx.xx元;\n 2.20.减免金额备注:显示收费明细中填写的减免金额备注信息;\n\n3.下方为返回按钮\n 3.1.返回:点击返回按钮,返回租赁账单列表页面;\n'; + + return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + React.createElement(Breadcrumb, { + items: [ + { title: '业务管理' }, + { title: '租赁账单' }, + { title: '查看' } + ] + }), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { requirementModalVisible[1](true); } }, '查看需求说明') + ), + React.createElement(Card, { title: '账单信息', style: cardStyle }, + React.createElement('div', { style: formRowStyle }, + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '合同编码'), + React.createElement('div', { style: valueStyle }, billInfo.contractCode || '—') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '合同类型'), + React.createElement('div', { style: valueStyle }, billInfo.contractType || '—') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '项目名称'), + React.createElement('div', { style: valueStyle }, billInfo.projectName || '—') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '客户名称'), + React.createElement('div', { style: valueStyle }, billInfo.customerName || '—') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '交车任务编码'), + React.createElement('div', { style: valueStyle }, billInfo.deliveryTaskCode || '—') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '账单编码'), + React.createElement('div', { style: valueStyle }, billInfo.billCode || '—') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '账单期数'), + React.createElement('div', { style: valueStyle }, billInfo.period != null ? billInfo.period : '—') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '账单开始日期'), + React.createElement('div', { style: valueStyle }, billInfo.billStartDate || '—') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '账单结束日期'), + React.createElement('div', { style: valueStyle }, billInfo.billEndDate || '—') + ) + ) + ), + React.createElement(Card, { title: '账单明细', style: cardStyle }, + React.createElement('div', { style: { marginBottom: 16, display: 'flex', gap: 24, alignItems: 'center' } }, + React.createElement(Popover, { content: receivablePopoverContent, title: '应收款明细', trigger: 'click' }, + React.createElement('span', { style: { cursor: 'pointer' } }, '应收款总额:', React.createElement('span', { style: highlightStyle }, receivableTotal, ' 元')) + ), + React.createElement(Popover, { content: actualPopoverContent, title: '实收款明细', trigger: 'click' }, + React.createElement('span', { style: { cursor: 'pointer' } }, '实收款总额:', React.createElement('span', { style: highlightStyle }, actualTotal, ' 元')) + ), + React.createElement('span', { style: { cursor: 'default' } }, '开票总额:', React.createElement('span', { style: highlightStyle }, invoiceTotal, ' 元')) + ), + React.createElement('div', { style: { overflowX: 'auto', marginBottom: 16 } }, + React.createElement('table', { style: { width: '100%', minWidth: 1500, borderCollapse: 'collapse', fontSize: 13, tableLayout: 'fixed' } }, + React.createElement('thead', null, + React.createElement('tr', { style: { backgroundColor: '#fafafa' } }, + React.createElement('th', { style: Object.assign({}, thBase, { width: 50 }) }, '序号'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 80 }) }, '品牌'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 90 }) }, '型号'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 100 }) }, '车牌号'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 110 }) }, '应收车辆月租金'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 110 }) }, '实收车辆月租金'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 110 }) }, '车辆租金备注'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 90 }) }, '减免金额'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 100 }) }, '减免金额备注'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 120 }) }, '减免证明'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 110 }) }, '应收车辆保证金'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 90 }) }, '服务费项目'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 90 }) }, '应收服务费'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 90 }) }, '实收服务费') + ) + ), + React.createElement('tbody', null, + vehicles.map(function (row) { + var servicePopover = React.createElement('div', { style: { padding: 8, minWidth: 340 } }, + React.createElement('div', { style: { fontWeight: 600, marginBottom: 8 } }, '服务费项目'), + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 12 } }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '服务项目'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '应收费用'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '实收费用'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '减免费用'), + React.createElement('th', { style: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '备注') + ) + ), + React.createElement('tbody', null, + (row.serviceItems || []).map(function (s, si) { + return React.createElement('tr', { key: si }, + React.createElement('td', { style: { padding: '6px 8px' } }, s.name), + React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, (s.receivable != null ? s.receivable : '') + ' 元'), + React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, (s.actual != null ? s.actual : '') + ' 元'), + React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, (s.discount != null ? s.discount : '0.00') + ' 元'), + React.createElement('td', { style: { padding: '6px 8px' } }, s.remark || '—') + ); + }) + ) + ) + ); + var proofList = row.discountProof || []; + var depositDisplay = isFirstPeriod ? (row.receivableDeposit || 0) : '0.00'; + return React.createElement('tr', { key: row.key }, + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, row.index), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, row.brand), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, row.model), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, row.plateNo || '—'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, (row.receivableRent != null ? row.receivableRent : 0) + ' 元'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, (row.actualRent || '0.00') + ' 元'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, row.rentRemark || '—'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, (row.discountAmount || '0.00') + ' 元'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, row.discountRemark || '—'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, + proofList.length === 0 ? '—' : React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 2, fontSize: 12 } }, + proofList.map(function (p, pidx) { + return React.createElement(Button, { key: pidx, type: 'link', size: 'small', style: { padding: 0, height: 'auto', textAlign: 'left' }, onClick: function () { message.info('预览:' + (p.name || '附件')); } }, p.name || '附件'); + }) + ) + ), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, (depositDisplay !== undefined && depositDisplay !== null ? depositDisplay : '0.00') + ' 元'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, + React.createElement(Popover, { + content: servicePopover, + title: null, + trigger: 'click', + open: activeServiceIndex === row.key, + onOpenChange: function (open) { setServicePopoverRowIndex(open ? row.key : null); } + }, + React.createElement(Button, { type: 'link', size: 'small' }, '管理') + ) + ), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, (row.receivableService != null ? row.receivableService : '0.00') + ' 元'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, (row.actualService || '0.00') + ' 元') + ); + }) + ) + ) + ), + isFirstPeriod ? React.createElement('div', { style: { marginTop: 16 } }, + React.createElement('div', { style: { fontSize: 14, fontWeight: 500, marginBottom: 8 } }, '氢费预付款情况'), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr', gap: 16, maxWidth: 800 } }, + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '氢费预付款应收金额'), + React.createElement('div', { style: valueStyle }, hydrogen.receivable + ' 元') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '氢费预付款实收金额'), + React.createElement('div', { style: valueStyle }, hydrogen.actual + ' 元') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '减免金额'), + React.createElement('div', { style: valueStyle }, hydrogen.discount + ' 元') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '减免金额备注'), + React.createElement('div', { style: valueStyle }, hydrogen.discountRemark || '—') + ) + ) + ) : null + ), + React.createElement('div', { style: { marginTop: 24 } }, + React.createElement(Button, { onClick: handleBack }, '返回') + ), + React.createElement(Modal, { + title: '需求说明', + open: requirementModalVisible[0], + onCancel: function () { requirementModalVisible[1](false); }, + width: 720, + footer: React.createElement(Button, { onClick: function () { requirementModalVisible[1](false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, React.createElement('div', { style: { padding: '8px 0' } }, + React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6 } }, requirementContent)) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-business/pages/16-租赁账单-收费明细.jsx b/src/prototypes/oneos-web-business/pages/16-租赁账单-收费明细.jsx new file mode 100644 index 0000000..374bc27 --- /dev/null +++ b/src/prototypes/oneos-web-business/pages/16-租赁账单-收费明细.jsx @@ -0,0 +1,451 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 租赁账单-收费明细(2026年3月11日版本) + +const Component = function () { + var useState = React.useState; + var useCallback = React.useCallback; + var useMemo = React.useMemo; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Input = antd.Input; + var Button = antd.Button; + var Popover = antd.Popover; + var Modal = antd.Modal; + var message = antd.message; + + var servicePopoverRowIndex = useState(null); + var submitConfirmVisible = useState(false); + var cancelConfirmVisible = useState(false); + var requirementModalVisible = useState(false); + + // 模拟:当前账单信息(由路由/接口传入,此处为第1期账单) + var billInfo = useMemo(function () { + return { + contractCode: 'HT-ZL-2025-001', + contractType: '正式合同', + projectName: '嘉兴氢能示范项目', + customerName: '嘉兴某某物流有限公司', + deliveryTaskCode: 'JT-2025-001-A', + billCode: 'HT-ZL-2025-0010001', + period: 1, + billStartDate: '2025-01-01', + billEndDate: '2025-01-31' + }; + }, []); + + var isFirstPeriod = billInfo.period === 1; + + // 车辆明细(该账单对应车辆及费用) + var vehicleRows = useState([ + { key: 'v1', index: 1, brand: '东风', model: 'DFH1180', plateNo: '浙A12345', receivableRent: 30000, actualRent: '29800.00', rentRemark: '首期六期一次性付清', discountAmount: '200.00', discountRemark: '首月优惠', discountProof: [{ name: '优惠审批单.pdf' }], receivableDeposit: 10000, serviceItems: [{ name: '代处理费用', receivable: 200, actual: '200.00', discount: '0.00', remark: '' }, { name: '保险上浮', receivable: 500, actual: '480.00', discount: '20.00', remark: '客户协商' }], receivableService: 700, actualService: '680.00' }, + { key: 'v2', index: 2, brand: '福田', model: 'BJ1180', plateNo: '浙A23456', receivableRent: 27000, actualRent: '27000.00', rentRemark: '', discountAmount: '0.00', discountRemark: '', discountProof: [], receivableDeposit: 8000, serviceItems: [{ name: '保养费用', receivable: 300, actual: '300.00', discount: '0.00', remark: '含首保' }], receivableService: 300, actualService: '300.00' }, + { key: 'v3', index: 3, brand: '重汽', model: 'ZZ1187', plateNo: '浙A34567', receivableRent: 31200, actualRent: '31200.00', rentRemark: '', discountAmount: '0.00', discountRemark: '', discountProof: [], receivableDeposit: 10000, serviceItems: [{ name: '代处理费用', receivable: 180, actual: '180.00', discount: '0.00', remark: '' }, { name: '上牌服务', receivable: 400, actual: '400.00', discount: '0.00', remark: '' }], receivableService: 580, actualService: '580.00' } + ]); + var vehicles = vehicleRows[0]; + var setVehicles = vehicleRows[1]; + + var hydrogenRow = useState({ receivable: '3580.00', actual: '3500.00', discount: '80.00', discountRemark: '预付款批量减免' }); + var hydrogen = hydrogenRow[0]; + var setHydrogen = hydrogenRow[1]; + + var updateVehicle = useCallback(function (key, field, value) { + setVehicles(function (prev) { + return prev.map(function (r) { + if (r.key !== key) return r; + var next = Object.assign({}, r); + next[field] = value; + if (field === 'serviceItems') { + var total = 0; + (value || []).forEach(function (s) { total += parseFloat(s.actual) || 0; }); + next.actualService = total.toFixed(2); + } + return next; + }); + }); + }, []); + + var updateServiceItem = useCallback(function (vehicleKey, itemIndex, field, value) { + setVehicles(function (prev) { + return prev.map(function (r) { + if (r.key !== vehicleKey) return r; + var items = (r.serviceItems || []).slice(); + if (!items[itemIndex]) return r; + items[itemIndex] = Object.assign({}, items[itemIndex], { [field]: value }); + var total = 0; + items.forEach(function (s) { total += parseFloat(s.actual) || 0; }); + return Object.assign({}, r, { serviceItems: items, actualService: total.toFixed(2) }); + }); + }); + }, []); + + // 应收款总额 = 应收车辆月租金总和 + 应收车辆保证金总和 + 应收服务费总和 + 氢费预付款应收金额 + var totals = useMemo(function () { + var receivableRent = 0, actualRent = 0, receivableDeposit = 0, receivableService = 0, actualService = 0, discountTotal = 0, serviceDiscountTotal = 0; + vehicles.forEach(function (v) { + receivableRent += Number(v.receivableRent) || 0; + actualRent += parseFloat(v.actualRent) || 0; + receivableDeposit += Number(v.receivableDeposit) || 0; + receivableService += Number(v.receivableService) || 0; + actualService += parseFloat(v.actualService) || 0; + discountTotal += parseFloat(v.discountAmount) || 0; + (v.serviceItems || []).forEach(function (s) { serviceDiscountTotal += parseFloat(s.discount) || 0; }); + }); + var hydrogenReceivable = parseFloat(hydrogen.receivable) || 0; + var hydrogenActual = parseFloat(hydrogen.actual) || 0; + var hydrogenDiscount = parseFloat(hydrogen.discount) || 0; + return { + receivableRent: receivableRent.toFixed(2), + actualRent: actualRent.toFixed(2), + receivableDeposit: receivableDeposit.toFixed(2), + receivableService: receivableService.toFixed(2), + actualService: actualService.toFixed(2), + discountTotal: discountTotal.toFixed(2), + serviceDiscountTotal: serviceDiscountTotal.toFixed(2), + hydrogenReceivable: hydrogenReceivable.toFixed(2), + hydrogenActual: hydrogenActual.toFixed(2), + hydrogenDiscount: hydrogenDiscount.toFixed(2) + }; + }, [vehicles, hydrogen]); + + var receivableTotal = useMemo(function () { + return (parseFloat(totals.receivableRent) + parseFloat(totals.receivableDeposit) + parseFloat(totals.receivableService) + (isFirstPeriod ? parseFloat(totals.hydrogenReceivable) : 0)).toFixed(2); + }, [totals, isFirstPeriod]); + + var actualTotal = useMemo(function () { + var base = parseFloat(totals.actualRent) + parseFloat(totals.receivableDeposit) + parseFloat(totals.actualService) - parseFloat(totals.discountTotal) - parseFloat(totals.serviceDiscountTotal); + if (isFirstPeriod) base += parseFloat(totals.hydrogenActual) - parseFloat(totals.hydrogenDiscount); + return base.toFixed(2); + }, [totals, isFirstPeriod]); + + var invoiceTotal = useMemo(function () { + // 开票金额:同“实收款总额”展示格式,但不包含应收车辆保证金 + var base = parseFloat(totals.actualRent) + parseFloat(totals.actualService) - parseFloat(totals.discountTotal) - parseFloat(totals.serviceDiscountTotal); + if (isFirstPeriod) base += parseFloat(totals.hydrogenActual) - parseFloat(totals.hydrogenDiscount); + return base.toFixed(2); + }, [totals, isFirstPeriod]); + + var receivablePopoverContent = useMemo(function () { + var rows = [ + ['总计应收车辆月租金', totals.receivableRent + ' 元'], + ['总计应收车辆保证金', totals.receivableDeposit + ' 元'], + ['总计应收服务费', totals.receivableService + ' 元'] + ]; + if (isFirstPeriod) rows.push(['氢费预付款应收金额', totals.hydrogenReceivable + ' 元']); + return React.createElement('div', { style: { padding: 8, minWidth: 220 } }, + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 13 } }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '项目'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '金额') + ) + ), + React.createElement('tbody', null, + rows.map(function (r, i) { return React.createElement('tr', { key: i }, React.createElement('td', { style: { padding: '6px 8px' } }, r[0]), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, r[1])); }) + ) + ) + ); + }, [totals, isFirstPeriod]); + + var actualPopoverContent = useMemo(function () { + var rows = [ + ['总计实收车辆月租金', totals.actualRent + ' 元'], + ['总计应收车辆保证金', totals.receivableDeposit + ' 元'], + ['总计实收服务费', totals.actualService + ' 元'], + ['总计减免金额', totals.discountTotal + ' 元'], + ['服务费各项减免费用总和', totals.serviceDiscountTotal + ' 元'] + ]; + if (isFirstPeriod) { + rows.push(['氢费预付款实收金额', totals.hydrogenActual + ' 元']); + rows.push(['氢费预付款减免金额', totals.hydrogenDiscount + ' 元']); + } + return React.createElement('div', { style: { padding: 8, minWidth: 240 } }, + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 13 } }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '项目'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '金额') + ) + ), + React.createElement('tbody', null, + rows.map(function (r, i) { return React.createElement('tr', { key: i }, React.createElement('td', { style: { padding: '6px 8px' } }, r[0]), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, r[1])); }) + ) + ) + ); + }, [totals, isFirstPeriod]); + + var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; + var labelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var formRowStyle = { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px 24px', marginBottom: 16 }; + var formItemStyle = { marginBottom: 12 }; + var valueStyle = { color: 'rgba(0,0,0,0.85)', fontSize: 14, lineHeight: '22px', minHeight: 22 }; + var thBase = { padding: '10px 12px', border: '1px solid #f0f0f0', whiteSpace: 'nowrap' }; + var highlightStyle = { color: '#1890ff', fontWeight: 600, cursor: 'pointer' }; + var requiredStar = { color: '#ff4d4f', marginRight: 2 }; + + var activeServiceIndex = servicePopoverRowIndex[0]; + var setServicePopoverRowIndex = servicePopoverRowIndex[1]; + + function handleSubmit() { + submitConfirmVisible[1](true); + } + function handleSubmitConfirm() { + submitConfirmVisible[1](false); + message.success('提交成功'); + if (window.__billChargeDetailBack) window.__billChargeDetailBack(); + } + function handleSave() { + message.success('保存成功'); + if (window.__billChargeDetailBack) window.__billChargeDetailBack(); + } + function handleCancel() { + cancelConfirmVisible[1](true); + } + function handleCancelConfirm() { + cancelConfirmVisible[1](false); + if (window.__billChargeDetailBack) window.__billChargeDetailBack(); + else message.info('返回租赁账单列表(原型)'); + } + + var requirementContent = '租赁账单-收费明细(2026年3月11日版本)\n一个「数字化资产ONEOS运管平台」中的「租赁账单」「收费明细」模块\n#面包屑:业务管理-租赁账单-收费明细;\n1.账单信息:\n#显示该账单对应合同及账单信息,显示以下字段:\n 1.1.合同编码:显示该账单对应合同编码;\n 1.2.合同类型:显示该账单对应合同类型,包括正式合同、试用合同;\n 1.3.项目名称:显示该账单对应合同中项目名称;\n 1.4.客户名称:显示该账单对应合同中客户名称;\n 1.5.交车任务编码:显示交车任务对应编码;\n 1.6.账单编码:显示该账单编码,账单编码规则为:[合同编码][账单编号]组成,主要用于后期与用友YS系统打通时获取财务收款及发票相关数据;\n 前缀为合同编码,后缀为账单编号,规则为:ZD+4位编号,为该合同下第x份账单,例如:ZD0001为该合同下第1份账单,依次类推;\n 例如:JXZL20260216YW101235AZD0001即为JXZL20260216YW101235A合同下第1份账单;;\n 1.7.账单期数:显示该账单期数;\n 1.8.账单开始日期:显示账单开始日期,根据租赁合同及交车任务开始计费日期按规则生成,格式为:YYYY-MM-DD;\n 1.9.账单结束日期:显示账单结束日期,根据租赁合同及交车任务开始计费日期按规则生成,格式为:YYYY-MM-DD;\n2.账单明细:\n#顶部显示应收款总额、实收款总额;\n 2.1.应收款总额:「应收车辆月租金总和」+「应收车辆保证金总和」+「应收服务费总和」+「氢费预付款应收金额」\n 2.2.实收款总额:「实收车辆月租金总和」+「应收车辆保证金总和」+「实收服务费总和」+「氢费预付款实收金额」-「减免费用总和」「服务费各项减免费用总和」\n 2.3.开票金额:「实收车辆月租金总和」+「实收服务费总和」-「减免费用总和」「服务费各项减免费用总和」\n\n#显示该账单对应车辆列表及相关费用信息,显示以下字段;\n 2.3.序号:与租赁合同及交车任务中该车辆序号一致;\n 2.4.品牌:与租赁合同及交车任务中该车辆品牌一致;\n 2.5.型号:与租赁合同及交车任务中该车辆型号一致;\n 2.6.车牌号:与租赁合同及交车任务中该车辆车牌号一致;\n 2.7.应收车辆月租金:与租赁合同中该车辆车辆月租金一致,格式为:xx.xx元;\n 2.8.实收车辆月租金:输入框,2位小数,后缀为元,默认为与应收车辆月租金一致;\n 2.9.车辆租金备注:输入框,支持自定义输入;\n 2.10.减免金额:输入框,2位小数,后缀为元,默认为0.00元;\n 2.11.减免金额备注:输入框,支持自定义输入;\n 2.12.减免证明:附件上传按钮,点击支持多文件上传,已上传文件一行显示一条,支持点击预览,格式支持各类图片、doc、docx、pdf等格式;\n 2.13.应收车辆保证金:首期与合同中车辆保证金一致,第二期开始为0.00,格式为:xx.xx元;\n 2.14.服务费项目:点击管理,弹出气泡卡片,气泡卡片标题为服务费项目,下方为列表,显示服务项目、应收费用、实收费用、备注;\n 2.14.1.服务项目:显示租赁合同中所有服务项目名称;\n 2.14.2.应收费用:显示租赁合同中所有服务项目对应费用,格式为:xx.xx元;\n 2.14.3.实收费用:必填项,输入框,默认反写应收费用,支持修改,由业务员自行输入实收费用金额,精确至2位小数,后缀为元,默认与应收费用一致;\n 2.14.4.减免费用:选填项,输入框,默认为:0.00,精确至2位小数,后缀为元;\n 2.14.4.备注:选填项,输入框,由业务员自行输入备注信息;\n 2.15.应收服务费:显示当前车辆所有服务费应收费用总和,格式为:xx.xx元;\n 2.16.实收服务费:显示当前车辆所有服务费实收费用总和,格式为:xx.xx元;\n#列表下方为氢费预付款情况,首期账单显示,第二期账单开始无此区域内容显示;\n 2.17.氢费预付款应收金额:显示合同中氢费预付款金额;\n 2.18.氢费预付款实收金额:输入框,2位小数,后缀为元,默认与氢费预付款应收金额一致;\n 2.19.减免金额:输入框,2位小数,后缀为元;\n 2.20.减免金额备注:输入框,支持自定义输入;\n\n3.下方为提交审核、保存、取消按钮\n 3.1.提交审核:点击提交审核按钮,进入审核流程;\n 3.2.保存:点击保存按钮,保存当前表单已填数据,不做校验;\n'; + + return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { marginBottom: 16, display: 'flex', alignItems: 'center', justifyContent: 'space-between' } }, + React.createElement(Breadcrumb, { + items: [ + { title: '业务管理' }, + { title: '租赁账单' }, + { title: '收费明细' } + ] + }), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { requirementModalVisible[1](true); } }, '查看需求说明') + ), + React.createElement(Card, { title: '账单信息', style: cardStyle }, + React.createElement('div', { style: formRowStyle }, + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '合同编码'), + React.createElement('div', { style: valueStyle }, billInfo.contractCode || '—') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '合同类型'), + React.createElement('div', { style: valueStyle }, billInfo.contractType || '—') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '项目名称'), + React.createElement('div', { style: valueStyle }, billInfo.projectName || '—') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '客户名称'), + React.createElement('div', { style: valueStyle }, billInfo.customerName || '—') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '交车任务编码'), + React.createElement('div', { style: valueStyle }, billInfo.deliveryTaskCode || '—') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '账单编码'), + React.createElement('div', { style: valueStyle }, billInfo.billCode || '—') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '账单期数'), + React.createElement('div', { style: valueStyle }, billInfo.period != null ? billInfo.period : '—') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '账单开始日期'), + React.createElement('div', { style: valueStyle }, billInfo.billStartDate || '—') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '账单结束日期'), + React.createElement('div', { style: valueStyle }, billInfo.billEndDate || '—') + ) + ) + ), + React.createElement(Card, { title: '账单明细', style: cardStyle }, + React.createElement('div', { style: { marginBottom: 16, display: 'flex', gap: 24, alignItems: 'center' } }, + React.createElement(Popover, { content: receivablePopoverContent, title: '应收款明细', trigger: 'click' }, + React.createElement('span', { style: { cursor: 'pointer' } }, '应收款总额:', React.createElement('span', { style: highlightStyle }, receivableTotal, ' 元')) + ), + React.createElement(Popover, { content: actualPopoverContent, title: '实收款明细', trigger: 'click' }, + React.createElement('span', { style: { cursor: 'pointer' } }, '实收款总额:', React.createElement('span', { style: highlightStyle }, actualTotal, ' 元')) + ) + , + React.createElement('span', { style: { cursor: 'default' } }, '开票金额:', React.createElement('span', { style: highlightStyle }, invoiceTotal, ' 元')) + ), + React.createElement('div', { style: { overflowX: 'auto', marginBottom: 16 } }, + React.createElement('table', { style: { width: '100%', minWidth: 1600, borderCollapse: 'collapse', fontSize: 13, tableLayout: 'fixed' } }, + React.createElement('thead', null, + React.createElement('tr', { style: { backgroundColor: '#fafafa' } }, + React.createElement('th', { style: Object.assign({}, thBase, { width: 50 }) }, '序号'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 80 }) }, '品牌'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 90 }) }, '型号'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 100 }) }, '车牌号'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 120 }) }, '应收车辆月租金'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 130 }) }, '实收车辆月租金'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 120 }) }, '车辆租金备注'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 100 }) }, '减免金额'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 120 }) }, '减免金额备注'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 130 }) }, '减免证明'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 110 }) }, '应收车辆保证金'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 90 }) }, '服务费项目'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 90 }) }, '应收服务费'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 90 }) }, '实收服务费') + ) + ), + React.createElement('tbody', null, + vehicles.map(function (row) { + var servicePopover = React.createElement('div', { style: { padding: 8, minWidth: 360 } }, + React.createElement('div', { style: { fontWeight: 600, marginBottom: 8 } }, '服务费项目'), + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 12 } }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '服务项目'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '应收费用'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, React.createElement('span', null, React.createElement('span', { style: requiredStar }, '*'), '实收费用')), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '减免费用'), + React.createElement('th', { style: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '备注') + ) + ), + React.createElement('tbody', null, + (row.serviceItems || []).map(function (s, si) { + return React.createElement('tr', { key: si }, + React.createElement('td', { style: { padding: '6px 8px' } }, s.name), + React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, (s.receivable != null ? s.receivable : '') + ' 元'), + React.createElement('td', { style: { padding: '6px 8px' } }, + React.createElement(Input, { size: 'small', value: s.actual, suffix: '元', onChange: function (e) { updateServiceItem(row.key, si, 'actual', e.target.value); } }) + ), + React.createElement('td', { style: { padding: '6px 8px' } }, + React.createElement(Input, { size: 'small', value: s.discount, suffix: '元', onChange: function (e) { updateServiceItem(row.key, si, 'discount', e.target.value); } }) + ), + React.createElement('td', { style: { padding: '6px 8px' } }, + React.createElement(Input, { size: 'small', value: s.remark, placeholder: '备注', onChange: function (e) { updateServiceItem(row.key, si, 'remark', e.target.value); } }) + ) + ); + }) + ) + ) + ); + var proofList = row.discountProof || []; + var onProofUpload = function (e) { + var files = e.target.files; + if (!files || !files.length) return; + var next = proofList.slice(); + for (var i = 0; i < files.length; i++) { next.push({ name: files[i].name }); } + updateVehicle(row.key, 'discountProof', next); + e.target.value = ''; + }; + var removeProof = function (idx) { + var next = (row.discountProof || []).slice(); + next.splice(idx, 1); + updateVehicle(row.key, 'discountProof', next); + }; + var depositDisplay = isFirstPeriod ? (row.receivableDeposit || 0) : '0.00'; + return React.createElement('tr', { key: row.key }, + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, row.index), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, row.brand), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, row.model), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, row.plateNo || '—'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, (row.receivableRent != null ? row.receivableRent : 0) + ' 元'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, + React.createElement(Input, { size: 'small', value: row.actualRent, suffix: '元', onChange: function (e) { updateVehicle(row.key, 'actualRent', e.target.value); } }) + ), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, + React.createElement(Input, { size: 'small', value: row.rentRemark, onChange: function (e) { updateVehicle(row.key, 'rentRemark', e.target.value); } }) + ), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, + React.createElement(Input, { size: 'small', value: row.discountAmount, suffix: '元', onChange: function (e) { updateVehicle(row.key, 'discountAmount', e.target.value); } }) + ), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, + React.createElement(Input, { size: 'small', value: row.discountRemark, onChange: function (e) { updateVehicle(row.key, 'discountRemark', e.target.value); } }) + ), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, + React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, fontSize: 12 } }, + proofList.map(function (p, pidx) { + return React.createElement('div', { key: pidx, style: { display: 'flex', alignItems: 'center', gap: 4 } }, + React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0, height: 'auto' }, onClick: function () { message.info('预览:' + (p.name || '附件')); } }, p.name || '附件'), + React.createElement(Button, { type: 'link', size: 'small', danger: true, style: { padding: 0, minWidth: 'auto' }, onClick: function () { removeProof(pidx); } }, '删除') + ); + }), + React.createElement('span', null, + React.createElement('input', { type: 'file', multiple: true, accept: '.jpg,.jpeg,.png,.pdf,.doc,.docx', style: { display: 'none' }, id: 'proof-' + row.key, onChange: onProofUpload }), + React.createElement(Button, { type: 'default', size: 'small', onClick: function () { var el = document.getElementById('proof-' + row.key); if (el) el.click(); } }, '附件上传') + ) + ) + ), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, (depositDisplay !== undefined && depositDisplay !== null ? depositDisplay : '0.00') + ' 元'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, + React.createElement(Popover, { + content: servicePopover, + title: null, + trigger: 'click', + open: activeServiceIndex === row.key, + onOpenChange: function (open) { setServicePopoverRowIndex(open ? row.key : null); } + }, + React.createElement(Button, { type: 'link', size: 'small' }, '管理') + ) + ), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, (row.receivableService != null ? row.receivableService : '0.00') + ' 元'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, (row.actualService || '0.00') + ' 元') + ); + }) + ) + ) + ), + isFirstPeriod ? React.createElement('div', { style: { marginTop: 16 } }, + React.createElement('div', { style: { fontSize: 14, fontWeight: 500, marginBottom: 8 } }, '氢费预付款情况'), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr', gap: 16, maxWidth: 800 } }, + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '氢费预付款应收金额'), + React.createElement(Input, { value: hydrogen.receivable + ' 元', disabled: true }) + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '氢费预付款实收金额'), + React.createElement(Input, { value: hydrogen.actual, suffix: '元', onChange: function (e) { setHydrogen(function (p) { return Object.assign({}, p, { actual: e.target.value }); }); } }) + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '减免金额'), + React.createElement(Input, { value: hydrogen.discount, suffix: '元', onChange: function (e) { setHydrogen(function (p) { return Object.assign({}, p, { discount: e.target.value }); }); } }) + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '减免金额备注'), + React.createElement(Input, { value: hydrogen.discountRemark, onChange: function (e) { setHydrogen(function (p) { return Object.assign({}, p, { discountRemark: e.target.value }); }); } }) + ) + ) + ) : null + ), + React.createElement('div', { style: { display: 'flex', gap: 8, marginTop: 24 } }, + React.createElement(Button, { type: 'primary', onClick: handleSubmit }, '提交'), + React.createElement(Button, { onClick: handleSave }, '保存'), + React.createElement(Button, { onClick: handleCancel }, '取消') + ), + React.createElement(Modal, { + title: '确认', + open: submitConfirmVisible[0], + onCancel: function () { submitConfirmVisible[1](false); }, + onOk: handleSubmitConfirm, + okText: '确认', + cancelText: '取消' + }, '请确认账单金额无误'), + React.createElement(Modal, { + title: '确认取消', + open: cancelConfirmVisible[0], + onCancel: function () { cancelConfirmVisible[1](false); }, + onOk: handleCancelConfirm, + okText: '确认', + cancelText: '取消' + }, '取消将会丢失未保存的修改,是否确认?'), + React.createElement(Modal, { + title: '需求说明', + open: requirementModalVisible[0], + onCancel: function () { requirementModalVisible[1](false); }, + width: 720, + footer: React.createElement(Button, { onClick: function () { requirementModalVisible[1](false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, React.createElement('div', { style: { padding: '8px 0' } }, + React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6 } }, requirementContent) + )) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-business/pages/17-租赁账单.jsx b/src/prototypes/oneos-web-business/pages/17-租赁账单.jsx new file mode 100644 index 0000000..adf6b1c --- /dev/null +++ b/src/prototypes/oneos-web-business/pages/17-租赁账单.jsx @@ -0,0 +1,478 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 租赁账单(2026年3月10日版本) + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Table = antd.Table; + var Button = antd.Button; + var Select = antd.Select; + var Input = antd.Input; + var Space = antd.Space; + var Popover = antd.Popover; + var Modal = antd.Modal; + var message = antd.message; + + var requirementModalVisible = useState(false); + var filterContractCode = useState(undefined); + var filterProjectName = useState(undefined); + var filterCustomerName = useState(undefined); + var filterBusinessDept = useState(undefined); + var filterBusinessPerson = useState(undefined); + var filterDeliveryTaskCode = useState(''); + var filterExpanded = useState(false); + var expandedRowKeysState = useState([]); + var deliveryPopoverOpen = useState(null); + var costEditsState = useState({}); + var costEdits = costEditsState[0]; + var setCostEdits = costEditsState[1]; + var editingCostState = useState(null); // { key, field } + var editingCost = editingCostState[0]; + var setEditingCost = editingCostState[1]; + var editingCostValueState = useState(''); + var editingCostValue = editingCostValueState[0]; + var setEditingCostValue = editingCostValueState[1]; + var expandedRowKeys = expandedRowKeysState[0]; + var setExpandedRowKeys = expandedRowKeysState[1]; + + // 主表数据:按合同聚合,每条主表下有多条账单(子表) + var mainListData = [ + { + contractCode: 'HT-ZL-2025-001', + deliveryTaskCode: 'JT-2025-001-A', + contractType: '正式合同', + projectName: '嘉兴氢能示范项目', + customerName: '嘉兴某某物流有限公司', + contractEffectiveDate: '2025-01-15', + businessDept: '业务1部', + businessPerson: '张经理', + children: [ + { period: 1, status: '已提交', billStartDate: '2025-01-01', billEndDate: '2025-01-31', deliveryCount: 3, deliveryVehicles: [{ brand: '东风', model: 'DFH1180', plateNo: '浙A12345' }, { brand: '福田', model: 'BJ1180', plateNo: '浙A23456' }, { brand: '重汽', model: 'ZZ1187', plateNo: '浙A34567' }], receivableTotal: 45800.00, actualTotal: 45500.00, discountTotal: 300.00, hydrogenCost: 1200.00, otherCost: 300.00 }, + { period: 2, status: '待提交', billStartDate: '2025-02-01', billEndDate: '2025-02-28', deliveryCount: 2, deliveryVehicles: [{ brand: '陕汽', model: 'SX1313', plateNo: '浙A45678' }, { brand: '解放', model: 'J6P', plateNo: '浙A56789' }], receivableTotal: 45800.00, actualTotal: 45800.00, discountTotal: 0.00, hydrogenCost: 900.00, otherCost: 180.00 }, + { period: 3, status: '待提交', billStartDate: '2025-03-01', billEndDate: '2025-03-31', deliveryCount: 2, deliveryVehicles: [{ brand: '江淮', model: '格尔发K5', plateNo: '浙A67890' }, { brand: '东风', model: 'DFH1250', plateNo: '浙A11111' }], receivableTotal: 45800.00, actualTotal: 45000.00, discountTotal: 800.00, hydrogenCost: 650.00, otherCost: 220.00 } + ] + }, + { + contractCode: 'HT-ZL-2025-002', + deliveryTaskCode: 'JT-2025-002-B', + contractType: '试用合同', + projectName: '上海物流租赁项目', + customerName: '上海某某运输公司', + contractEffectiveDate: '2025-02-01', + businessDept: '业务2部', + businessPerson: '李专员', + children: [ + { period: 1, status: '已提交', billStartDate: '2025-02-01', billEndDate: '2025-02-28', deliveryCount: 2, deliveryVehicles: [{ brand: '江淮', model: '格尔发K5', plateNo: '沪B11111' }, { brand: '东风', model: 'DFH1250', plateNo: '沪B22222' }], receivableTotal: 33400.00, actualTotal: 33400.00, discountTotal: 0.00, hydrogenCost: 780.00, otherCost: 160.00 }, + { period: 2, status: '待提交', billStartDate: '2025-03-01', billEndDate: '2025-03-31', deliveryCount: 1, deliveryVehicles: [{ brand: '解放', model: 'JH6', plateNo: '沪B33333' }], receivableTotal: 33400.00, actualTotal: 33400.00, discountTotal: 0.00, hydrogenCost: 0.00, otherCost: 0.00 } + ] + }, + { + contractCode: 'HT-ZL-2025-003', + deliveryTaskCode: 'JT-2025-003-C', + contractType: '正式合同', + projectName: '杭州城配租赁项目', + customerName: '杭州某某租赁有限公司', + contractEffectiveDate: '2025-02-10', + businessDept: '业务3部', + businessPerson: '王专员', + children: [ + { period: 1, status: '已提交', billStartDate: '2025-02-10', billEndDate: '2025-03-09', deliveryCount: 1, deliveryVehicles: [{ brand: '福田', model: '欧曼EST', plateNo: '浙C33333' }], receivableTotal: 41200.00, actualTotal: 41200.00, discountTotal: 0.00, hydrogenCost: 500.00, otherCost: 120.00 } + ] + } + ]; + + var mainList = mainListData; + var filterOptions = useMemo(function () { + var codes = [], projects = [], customers = [], depts = [], persons = []; + mainList.forEach(function (r) { + if (r.contractCode && codes.indexOf(r.contractCode) === -1) codes.push(r.contractCode); + if (r.projectName && projects.indexOf(r.projectName) === -1) projects.push(r.projectName); + if (r.customerName && customers.indexOf(r.customerName) === -1) customers.push(r.customerName); + if (r.businessDept && depts.indexOf(r.businessDept) === -1) depts.push(r.businessDept); + if (r.businessPerson && persons.indexOf(r.businessPerson) === -1) persons.push(r.businessPerson); + }); + return { + contractCode: codes.map(function (v) { return { value: v, label: v }; }), + projectName: projects.map(function (v) { return { value: v, label: v }; }), + customerName: customers.map(function (v) { return { value: v, label: v }; }), + businessDept: depts.map(function (v) { return { value: v, label: v }; }), + businessPerson: persons.map(function (v) { return { value: v, label: v }; }) + }; + }, [mainList]); + + var filteredMainList = useMemo(function () { + var list = mainList; + var code = filterContractCode[0]; + var project = filterProjectName[0]; + var customer = filterCustomerName[0]; + var dept = filterBusinessDept[0]; + var person = filterBusinessPerson[0]; + var taskCode = (filterDeliveryTaskCode[0] || '').trim().toLowerCase(); + if (code) list = list.filter(function (r) { return r.contractCode === code; }); + if (project) list = list.filter(function (r) { return r.projectName === project; }); + if (customer) list = list.filter(function (r) { return r.customerName === customer; }); + if (dept) list = list.filter(function (r) { return r.businessDept === dept; }); + if (person) list = list.filter(function (r) { return r.businessPerson === person; }); + if (taskCode) list = list.filter(function (r) { return (r.deliveryTaskCode || '').toLowerCase().indexOf(taskCode) !== -1; }); + return list; + }, [mainList, filterContractCode[0], filterProjectName[0], filterCustomerName[0], filterBusinessDept[0], filterBusinessPerson[0], filterDeliveryTaskCode[0]]); + + var mainTableDataSource = useMemo(function () { + return filteredMainList.map(function (r) { + var o = {}; + for (var k in r) if (k !== 'children') o[k] = r[k]; + o._detailList = r.children || []; + return o; + }); + }, [filteredMainList]); + + function fmtMoney(v) { + if (v === null || v === undefined) return '0.00元'; + var n = typeof v === 'number' ? v : parseFloat(v); + return (isNaN(n) ? '0.00' : n.toFixed(2)) + '元'; + } + + function pad4(n) { + var s = String(n == null ? '' : n); + return ('0000' + s).slice(-4); + } + + function calcDays(startStr, endStr) { + if (!startStr || !endStr) return 0; + var start = new Date(startStr + 'T00:00:00'); + var end = new Date(endStr + 'T00:00:00'); + var ms = end.getTime() - start.getTime(); + if (!isFinite(ms)) return 0; + var days = Math.floor(ms / 86400000) + 1; + return days < 0 ? 0 : days; + } + + var modelCostPerDayMap = useMemo(function () { + return { + 'DFH1180': 120, + 'BJ1180': 95, + 'ZZ1187': 130, + 'SX1313': 110, + 'J6P': 125, + '格尔发K5': 90, + 'DFH1250': 140, + 'JH6': 118, + '欧曼EST': 150 + }; + }, []); + + function calcVehicleCost(record) { + var days = calcDays(record.billStartDate, record.billEndDate); + var vehicles = record.deliveryVehicles || []; + var sum = 0; + for (var i = 0; i < vehicles.length; i++) { + var m = vehicles[i] && vehicles[i].model; + var costPerDay = modelCostPerDayMap[m] || 0; + sum += costPerDay * days; + } + return sum; + } + + function getCostKey(record) { + var p = record._parentRecord; + var code = (p && p.contractCode) || ''; + var period = record.period != null ? record.period : ''; + return code + '-ZD' + pad4(period); + } + + function renderEditableCost(field, record) { + var key = getCostKey(record); + var map = costEdits[key] || {}; + var v = map[field]; + if (v === undefined) v = record[field]; + var isEditing = editingCost && editingCost.key === key && editingCost.field === field; + if (isEditing) { + return React.createElement(Input, { + autoFocus: true, + value: editingCostValue, + onChange: function (e) { setEditingCostValue(e.target.value); }, + onBlur: function () { + var raw = (editingCostValue || '').trim(); + var num = raw === '' ? 0 : parseFloat(raw); + var next = isNaN(num) ? 0 : Math.round(num * 100) / 100; + setCostEdits(function (prev) { + var p = Object.assign({}, prev || {}); + var row = Object.assign({}, p[key] || {}); + row[field] = next; + p[key] = row; + return p; + }); + setEditingCost(null); + setEditingCostValue(''); + }, + onPressEnter: function () { if (document && document.activeElement) document.activeElement.blur(); }, + suffix: '元' + }); + } + return React.createElement('span', { + style: { cursor: 'pointer' }, + onClick: function () { + setEditingCost({ key: key, field: field }); + setEditingCostValue((v == null || v === '') ? '' : String(Number(v).toFixed(2))); + } + }, fmtMoney(v)); + } + + function goView(record, parent) { + message.info('查看账单详情(原型)'); + } + function goChargeDetail(record, parent) { + message.info('收费明细(原型)'); + } + + function renderSubActions(record, parentRecord) { + var status = record && record.status; + var isSubmitted = status === '已提交'; + return React.createElement(Space, { size: 'small' }, + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { goView(record, parentRecord); } }, '查看'), + isSubmitted ? null : React.createElement(Button, { type: 'link', size: 'small', onClick: function () { goChargeDetail(record, parentRecord); } }, '收费明细') + ); + } + + // 子表提车数量气泡:列表显示 品牌、型号、车牌号 + function renderDeliveryPopover(record) { + var vehicles = record.deliveryVehicles || []; + var parentCode = (record._parentRecord && record._parentRecord.contractCode) || ''; + var popoverKey = parentCode + '-' + (record.period != null ? record.period : ''); + var listStyle = { width: '100%', borderCollapse: 'collapse', fontSize: 13 }; + var thStyle = { padding: '6px 10px', textAlign: 'left', borderBottom: '1px solid #f0f0f0', backgroundColor: '#fafafa', fontWeight: 600 }; + var tdStyle = { padding: '6px 10px', borderBottom: '1px solid #f0f0f0' }; + var content = vehicles.length === 0 ? React.createElement('div', { style: { padding: 8 } }, '—') : React.createElement('div', { style: { padding: 0, minWidth: 200 } }, + React.createElement('table', { style: listStyle }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: thStyle }, '品牌'), + React.createElement('th', { style: thStyle }, '型号'), + React.createElement('th', { style: thStyle }, '车牌号') + ) + ), + React.createElement('tbody', null, + vehicles.map(function (v, i) { + return React.createElement('tr', { key: i }, + React.createElement('td', { style: tdStyle }, v.brand || '—'), + React.createElement('td', { style: tdStyle }, v.model || '—'), + React.createElement('td', { style: tdStyle }, v.plateNo || '—') + ); + }) + ) + ) + ); + var count = record.deliveryCount != null && record.deliveryCount !== '' ? Number(record.deliveryCount) : 0; + return React.createElement(Popover, { + content: content, + title: '车辆详情', + trigger: 'click', + open: deliveryPopoverOpen[0] === popoverKey, + onOpenChange: function (open) { deliveryPopoverOpen[1](open ? popoverKey : null); } + }, React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0 } }, (isNaN(count) ? 0 : count) + '辆')); + } + + var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; + var filterLabelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var filterItemStyle = { marginBottom: 12 }; + var filterControlStyle = { width: '100%' }; + var expandColumnWidth = 48; + + var mainColumns = [ + { title: '合同编码', dataIndex: 'contractCode', key: 'contractCode', width: 130, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '合同类型', dataIndex: 'contractType', key: 'contractType', width: 100, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '项目名称', dataIndex: 'projectName', key: 'projectName', width: 140, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 140, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '合同生效日期', dataIndex: 'contractEffectiveDate', key: 'contractEffectiveDate', width: 118, render: function (v) { return v || '—'; } }, + { title: '交车任务编码', dataIndex: 'deliveryTaskCode', key: 'deliveryTaskCode', width: 130, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '业务部门', dataIndex: 'businessDept', key: 'businessDept', width: 100, render: function (v) { return v || '—'; } }, + { title: '业务负责人', dataIndex: 'businessPerson', key: 'businessPerson', width: 100, render: function (v) { return v || '—'; } } + ]; + + var subColumns = [ + { title: '账单编号', dataIndex: 'billNo', key: 'billNo', width: 220, ellipsis: true, render: function (v, record) { var p = record._parentRecord; var code = (p && p.contractCode) || ''; var period = record.period != null ? record.period : ''; return code ? (code + 'ZD' + pad4(period)) : '—'; } }, + { title: '账单期数', dataIndex: 'period', key: 'period', width: 90, align: 'center', render: function (v) { return v != null ? v : '—'; } }, + { title: '状态', dataIndex: 'status', key: 'status', width: 90, render: function (v) { return v || '—'; } }, + { title: '账单开始日期', dataIndex: 'billStartDate', key: 'billStartDate', width: 120, render: function (v) { return v || '—'; } }, + { title: '账单结束日期', dataIndex: 'billEndDate', key: 'billEndDate', width: 120, render: function (v) { return v || '—'; } }, + { title: '提车数量', key: 'deliveryCount', width: 88, align: 'center', render: function (_, record) { return renderDeliveryPopover(record); } }, + { title: '应收款总额', dataIndex: 'receivableTotal', key: 'receivableTotal', width: 110, align: 'right', render: function (v) { return fmtMoney(v); } }, + { title: '实收款总额', dataIndex: 'actualTotal', key: 'actualTotal', width: 110, align: 'right', render: function (v) { return fmtMoney(v); } }, + { title: '减免总金额', dataIndex: 'discountTotal', key: 'discountTotal', width: 100, align: 'right', render: function (v) { return fmtMoney(v); } }, + { title: '车辆成本', key: 'vehicleCost', width: 110, align: 'right', render: function (_, record) { return fmtMoney(calcVehicleCost(record)); } }, + { title: '氢费成本', dataIndex: 'hydrogenCost', key: 'hydrogenCost', width: 110, align: 'right', render: function (_, record) { return renderEditableCost('hydrogenCost', record); } }, + { title: '其他成本', dataIndex: 'otherCost', key: 'otherCost', width: 110, align: 'right', render: function (_, record) { return renderEditableCost('otherCost', record); } }, + { + title: '操作', + key: 'action', + width: 160, + fixed: 'right', + render: function (_, record, rowIndex, extra) { + var parentRecord = (extra && extra._parentRecord) || record._parentRecord; + return renderSubActions(record, parentRecord); + } + } + ]; + + // 子表 rowKey 唯一:contractCode + period + var _expandRowRender = function (record) { + var rows = (record._detailList || []).map(function (r, idx) { + var o = {}; for (var k in r) o[k] = r[k]; o._parentRecord = record; o._rowIndex = idx; return o; + }); + return React.createElement('div', { + style: { marginBottom: 0, paddingLeft: expandColumnWidth, boxSizing: 'border-box' }, + draggable: false, + onDragStart: function (e) { e.preventDefault(); } + }, + React.createElement(Table, { + rowKey: function (r) { return (record.contractCode || '') + '-' + (r.period != null ? r.period : r._rowIndex); }, + columns: subColumns.map(function (col) { + if (col.key !== 'action') return col; + return { + title: col.title, + key: col.key, + width: col.width, + fixed: col.fixed, + render: function (val, row) { + return renderSubActions(row, record); + } + }; + }), + dataSource: rows, + pagination: false, + size: 'small', + bordered: true, + scroll: { x: 1450 } + }) + ); + }; + + var requirementContent = '租赁账单(2026年3月10日版本)\n一个「数字化资产ONEOS运管平台」中的「租赁账单」模块\n#面包屑:业务管理-租赁账单;\n1.筛选:\n#支持合同编码/项目名称/客户名称/业务部门/业务负责人等筛选方式;\n1.1.合同编码:选择器,支持输入框内手动输入下拉模糊匹配对应项;\n1.2.项目名称:选择器,支持输入框内手动输入下拉模糊匹配对应项;\n1.3.客户名称:选择器,支持输入框内手动输入下拉模糊匹配对应项;\n1.4.业务部门:选择器,支持选择所有业务部门;\n1.5.业务负责人:选择器,支持选择所有业务负责人;\n1.6.交车任务编码:输入框,支持模糊搜索;\n1.7.右侧为重置、查询按钮;\n\n2.租赁账单列表:\n列表展示方式为:嵌套子表格,分为主表和子表\n2.1.主表:显示以下字段:合同编码、合同类型、项目名称、客户名称、合同生效日期、业务部门、业务负责人;\n 2.1.1.合同编码:显示车辆租赁合同编码;\n 2.1.2.合同类型:显示该租赁合同对应合同类型,显示该合同为试用合同还是正式合同;\n 2.1.3.项目名称:显示该租赁合同对应项目名称;\n 2.1.4.客户名称:显示该租赁合同对应客户名称;\n 2.1.5.合同生效日期:显示该租赁合同生效日期,格式为:YYYY-MM-DD;\n 2.1.6.交车任务编码:显示交车任务编码;\n 2.1.7.业务部门:显示该租赁合同对应业务部门名称;\n 2.1.8.业务负责人:显示该租赁合同对应业务负责人;\n 2.1.9.主表右下方为分页符,支持分页和选择单页显示数据条数;\n\n2.2.子表:显示以下字段:账单编号、账单期数、账单开始日期、账单结束日期、应收款总额、实收款总额、减免总金额、实际到账金额、是否已开票、开票金额、操作;\n 2.2.1.账单编号:[合同编码][账单编号]组成,主要用于后期与用友YS系统打通时获取财务收款及发票相关数据;\n 前缀为合同编码,后缀为账单编号,规则为:ZD+4位编号,为该合同下第x份账单,例如:ZD0001为该合同下第1份账单,依次类推;\n 例如:JXZL20260216YW101235AZD0001即为JXZL20260216YW101235A合同下第1份账单;\n 2.2.2.账单期数:显示该笔账单对应期数;\n 2.2.3.处理状态:已提交、待提交、已结清;\n 2.2.3.1.已提交:业务人员已通过点击收费明细,对收费项进行过维护并经过二次确认后提交;\n 2.2.3.2.待提交:业务人员未完成收费明细填报;\n 2.2.3.3.已结清:(等对接财务系统后,到账金额等于实收金额总额后算作已结清);\n 2.2.3.账单开始日期:显示该笔账单开始日期,格式为:YYYY-MM-DD;\n 2.2.4.账单结束日期:显示该笔账单结束日期,格式为:YYYY-MM-DD;\n 2.2.5.提车数量:显示提车数量,格式为:xx辆,点击弹出气泡卡片,卡片内为列表,显示:品牌、型号、车牌号;\n 2.2.6.应收款总额:显示该笔账单应收款总额,格式为:xx.xx元,计算方式:「所有车辆月租金总和」+「所有车辆服务费总和」;\n 2.2.7.实收款总额:显示该笔账单实收款总额,格式为:xx.xx元,计算方式:「所有车辆月租金总和」+「所有车辆服务费总和」-「减免总金额」;\n 2.2.8.减免总金额:显示该笔账单减免总金额,格式为:xx.xx元,计算方式:「所有减免金额总和」;\n 2.2.9.车辆成本:根据型号成本表中对应型号车辆成本,租赁订单包含车辆型号*实际账单天数计算得出;\n 2.2.10.氢费成本:显示格式为:xx.xx元,点击变为输入框,后缀为元,支持2位小数,失焦后保存;\n 2.2.11.其他成本:显示格式为:xx.xx元,点击变为输入框,后缀为元,支持2位小数,失焦后保存;\n\n 2.2.12.操作:查看、收费明细;\n 2.2.12.1.查看:点击跳转租赁账单-查看页;\n 2.2.12.2.收费明细:提交后收费明细将不可修改;\n\n'; + + return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + React.createElement(Breadcrumb, { + items: [ + { title: '业务管理' }, + { title: '租赁账单' } + ] + }), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { requirementModalVisible[1](true); } }, '查看需求说明') + ), + React.createElement(Card, { title: '筛选', style: cardStyle }, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px 24px', alignItems: 'start' } }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '合同编码'), + React.createElement(Select, { + placeholder: '请选择或输入合同编码', + allowClear: true, + showSearch: true, + style: filterControlStyle, + value: filterContractCode[0], + onChange: function (v) { filterContractCode[1](v); }, + options: filterOptions.contractCode, + filterOption: function (input, opt) { return (opt.label || '').toString().toLowerCase().indexOf((input || '').toLowerCase()) !== -1; } + }) + ), + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '项目名称'), + React.createElement(Select, { + placeholder: '请选择或输入项目名称', + allowClear: true, + showSearch: true, + style: filterControlStyle, + value: filterProjectName[0], + onChange: function (v) { filterProjectName[1](v); }, + options: filterOptions.projectName, + filterOption: function (input, opt) { return (opt.label || '').toString().toLowerCase().indexOf((input || '').toLowerCase()) !== -1; } + }) + ), + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '客户名称'), + React.createElement(Select, { + placeholder: '请选择或输入客户名称', + allowClear: true, + showSearch: true, + style: filterControlStyle, + value: filterCustomerName[0], + onChange: function (v) { filterCustomerName[1](v); }, + options: filterOptions.customerName, + filterOption: function (input, opt) { return (opt.label || '').toString().toLowerCase().indexOf((input || '').toLowerCase()) !== -1; } + }) + ), + filterExpanded[0] ? React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '业务部门'), + React.createElement(Select, { + placeholder: '请选择业务部门', + allowClear: true, + style: filterControlStyle, + value: filterBusinessDept[0], + onChange: function (v) { filterBusinessDept[1](v); }, + options: filterOptions.businessDept + }) + ) : null, + filterExpanded[0] ? React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '业务负责人'), + React.createElement(Select, { + placeholder: '请选择业务负责人', + allowClear: true, + style: filterControlStyle, + value: filterBusinessPerson[0], + onChange: function (v) { filterBusinessPerson[1](v); }, + options: filterOptions.businessPerson + }) + ) : null, + filterExpanded[0] ? React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '交车任务编码'), + React.createElement(Input, { + placeholder: '请输入交车任务编码,支持模糊搜索', + allowClear: true, + style: filterControlStyle, + value: filterDeliveryTaskCode[0], + onChange: function (e) { filterDeliveryTaskCode[1](e.target.value); } + }) + ) : null + ), + React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 } }, + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { filterExpanded[1](!filterExpanded[0]); } }, filterExpanded[0] ? '收起' : '展开'), + React.createElement(Button, { onClick: function () { filterContractCode[1](undefined); filterProjectName[1](undefined); filterCustomerName[1](undefined); filterBusinessDept[1](undefined); filterBusinessPerson[1](undefined); filterDeliveryTaskCode[1](''); } }, '重置'), + React.createElement(Button, { type: 'primary' }, '查询') + ) + ), + React.createElement(Card, { title: '租赁账单列表', style: cardStyle }, + React.createElement(Table, { + rowKey: 'contractCode', + columns: mainColumns, + dataSource: mainTableDataSource, + expandable: { + expandedRowKeys: expandedRowKeys, + onExpandedRowsChange: function (keys) { setExpandedRowKeys(keys || []); }, + expandedRowRender: _expandRowRender, + rowExpandable: function (record) { return (record._detailList && record._detailList.length > 0); }, + columnWidth: expandColumnWidth + }, + pagination: { pageSize: 10, showSizeChanger: true, showTotal: function (t) { return '共 ' + t + ' 条'; } }, + size: 'middle', + bordered: true, + scroll: { x: 958 } + }) + ), + React.createElement(Modal, { + title: '需求说明', + open: requirementModalVisible[0], + onCancel: function () { requirementModalVisible[1](false); }, + width: 720, + footer: React.createElement(Button, { onClick: function () { requirementModalVisible[1](false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, React.createElement('div', { style: { padding: '8px 0' } }, + React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6 } }, requirementContent)) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-business/pages/18-ETC管理.jsx b/src/prototypes/oneos-web-business/pages/18-ETC管理.jsx new file mode 100644 index 0000000..f4a53c1 --- /dev/null +++ b/src/prototypes/oneos-web-business/pages/18-ETC管理.jsx @@ -0,0 +1,460 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 业务管理 - ETC管理(2026年3月10日版本) +// 模块:充值明细 / ETC账单明细 / ETC余额管理 + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Table = antd.Table; + var Tabs = antd.Tabs; + var Button = antd.Button; + var Select = antd.Select; + var Input = antd.Input; + var DatePicker = antd.DatePicker; + var Modal = antd.Modal; + var message = antd.message; + var Tag = antd.Tag; + var Upload = antd.Upload; + + var activeTab = useState('recharge'); + var setActiveTab = activeTab[1]; + + // ========== 充值明细 Tab ========== + var filterCustomer = useState(undefined); + var filterRemitStart = useState(null); + var filterRemitEnd = useState(null); + var filterReceiveCompany = useState(undefined); + var filterEntryType = useState(undefined); + var filterRechargeNo = useState(''); + var filterAuditStatus = useState(undefined); + var addRechargeVisible = useState(false); + var deleteConfirmVisible = useState(false); + var deleteConfirmRecord = useState(null); + var rechargeList = useState([ + { id: 'r1', auditStatus: '已入账', rechargeNo: 'ETC202603010001', customerName: '嘉兴某某物流有限公司', etcBalance: 12580.00, remitAccount: '嘉兴某某物流有限公司', remitDate: '2026-03-01', receiveCompany: '浙江羚牛氢能科技有限公司', entryType: '合同预付', receiveBank: '工商银行杭州分行', amount: 10000.00, creator: '张经理', createTime: '2026-03-01 10:00', attachment: '凭证.pdf', updateTime: '2026-03-01 10:00' }, + { id: 'r2', auditStatus: '待审核', rechargeNo: 'ETC202603050002', customerName: '上海某某运输公司', etcBalance: 8600.00, remitAccount: '上海某某运输公司', remitDate: '2026-03-05', receiveCompany: '羚牛新能源科技上海有限公司', entryType: '线下充值', receiveBank: '建设银行上海分行', amount: 5000.00, creator: '李专员', createTime: '2026-03-05 14:30', attachment: '', updateTime: '2026-03-05 14:30' }, + { id: 'r3', auditStatus: '已驳回', rechargeNo: 'ETC202603080003', customerName: '杭州某某租赁有限公司', etcBalance: 3200.00, remitAccount: '杭州某某租赁有限公司', remitDate: '2026-03-08', receiveCompany: '浙江羚牛氢能科技有限公司', entryType: '资金冲红', receiveBank: '农业银行杭州分行', amount: -500.00, creator: '王专员', createTime: '2026-03-08 09:15', attachment: '冲红说明.pdf', updateTime: '2026-03-08 11:00' } + ]); + var rechargeData = rechargeList[0]; + var setRechargeList = rechargeList[1]; + + // 新增充值表单 + var addFormCustomer = useState(undefined); + var addFormRemitAccount = useState(''); + var addFormEtcBalance = useState(''); + var addFormRemitDate = useState(null); + var addFormReceiveCompany = useState(undefined); + var addFormOpType = useState(undefined); + var addFormAmount = useState(''); + var addFormReceiveBank = useState(undefined); + var addFormFiles = useState([]); + + var receiveCompanyOptions = [ + { label: '羚牛新能源科技上海有限公司', value: '羚牛新能源科技上海有限公司' }, + { label: '浙江羚牛氢能科技有限公司', value: '浙江羚牛氢能科技有限公司' } + ]; + var entryTypeOptions = [ + { label: '合同预付', value: '合同预付' }, + { label: '线下充值', value: '线下充值' }, + { label: '资金冲红', value: '资金冲红' } + ]; + var opTypeOptions = [ + { label: '普通充值', value: '普通充值' }, + { label: '资金冲红', value: '资金冲红' } + ]; + + var filteredRecharge = useMemo(function () { + var list = rechargeData; + if (filterCustomer[0]) list = list.filter(function (r) { return r.customerName === filterCustomer[0]; }); + if (filterReceiveCompany[0]) list = list.filter(function (r) { return r.receiveCompany === filterReceiveCompany[0]; }); + if (filterEntryType[0]) list = list.filter(function (r) { return r.entryType === filterEntryType[0]; }); + if (filterAuditStatus[0]) list = list.filter(function (r) { return r.auditStatus === filterAuditStatus[0]; }); + if ((filterRechargeNo[0] || '').trim()) { + var kw = (filterRechargeNo[0] || '').trim().toLowerCase(); + list = list.filter(function (r) { return (r.rechargeNo || '').toLowerCase().indexOf(kw) !== -1; }); + } + return list; + }, [rechargeData, filterCustomer[0], filterReceiveCompany[0], filterEntryType[0], filterAuditStatus[0], filterRechargeNo[0]]); + + var rechargeColumns = [ + { title: '审核状态', dataIndex: 'auditStatus', key: 'auditStatus', width: 88, render: function (v) { var color = v === '已入账' ? 'success' : v === '待审核' ? 'processing' : 'error'; return React.createElement(Tag, { color: color }, v); } }, + { title: '充值编号', dataIndex: 'rechargeNo', key: 'rechargeNo', width: 160, ellipsis: true }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 160, ellipsis: true }, + { title: 'ETC账户余额', dataIndex: 'etcBalance', key: 'etcBalance', width: 120, align: 'right', render: function (v) { return (v != null ? Number(v).toFixed(2) : '0.00') + ' 元'; } }, + { title: '汇款账户名', dataIndex: 'remitAccount', key: 'remitAccount', width: 140, ellipsis: true }, + { title: '汇款日期', dataIndex: 'remitDate', key: 'remitDate', width: 110 }, + { title: '收款公司', dataIndex: 'receiveCompany', key: 'receiveCompany', width: 200, ellipsis: true }, + { title: '入账类型', dataIndex: 'entryType', key: 'entryType', width: 100 }, + { title: '收款银行', dataIndex: 'receiveBank', key: 'receiveBank', width: 140, ellipsis: true }, + { title: '充值金额', dataIndex: 'amount', key: 'amount', width: 100, align: 'right', render: function (v) { return (v != null ? Number(v).toFixed(2) : '0.00') + ' 元'; } }, + { title: '创建人', dataIndex: 'creator', key: 'creator', width: 90 }, + { title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 160 }, + { title: '附件', dataIndex: 'attachment', key: 'attachment', width: 90, render: function (v) { return v ? React.createElement(Button, { type: 'link', size: 'small', onClick: function () { message.info('预览:' + v); } }, '预览') : '—'; } }, + { title: '更新时间', dataIndex: 'updateTime', key: 'updateTime', width: 160 }, + { title: '操作', key: 'action', width: 160, fixed: 'right', render: function (_, record) { + return React.createElement(React.Fragment, null, + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { message.info('查看 ' + record.rechargeNo); } }, '查看'), + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { message.info('编辑 ' + record.rechargeNo); } }, '编辑'), + React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function () { deleteConfirmRecord[1](record); deleteConfirmVisible[1](true); } }, '删除') + ); + } } + ]; + + var handleDeleteRecharge = function () { + var r = deleteConfirmRecord[0]; + if (r) setRechargeList(function (prev) { return prev.filter(function (x) { return x.id !== r.id; }); }); + deleteConfirmVisible[1](false); + deleteConfirmRecord[1](null); + message.success('已删除'); + }; + + var handleAddRechargeSubmit = function () { + message.success('充值信息已提交'); + addRechargeVisible[1](false); + }; + + // ========== ETC账单明细 Tab ========== + var billFilterYear = useState(2026); + var billFilterDateRange = useState(null); + var billFilterBillDateRange = useState(null); + var billFilterPassDate = useState(null); + var billFilterPlate = useState(''); + var billFilterVehicleNature = useState(undefined); + var billFilterCustomer = useState(''); + var billFilterReconciled = useState(undefined); + var invoiceConfirmVisible = useState(false); + var billList = useState([ + { id: 'b1', year: 2026, month: 3, week: 2, salesman: '张经理', billDate: '2026/3/10', plateNo: '浙A12345', passTime: '2026-03-10 08:56:48', entry: '杭州南', exit: '嘉兴东', fee: 45.50, vehicleType: '18T', nature: '租赁', customerName: '嘉兴某某物流有限公司', isInvoiced: '否', invoiceAmount: 0, remark: '' }, + { id: 'b2', year: 2026, month: 3, week: 2, salesman: '张经理', billDate: '2026/3/10', plateNo: '浙A23456', passTime: '2026-03-10 09:12:00', entry: '嘉兴东', exit: '上海枫泾', fee: 128.00, vehicleType: '18T', nature: '租赁', customerName: '嘉兴某某物流有限公司', isInvoiced: '是', invoiceAmount: 128.00, remark: '' }, + { id: 'b3', year: 2026, month: 3, week: 2, salesman: '李专员', billDate: '2026/3/9', plateNo: '沪B11111', passTime: '2026-03-09 14:30:22', entry: '上海徐泾', exit: '苏州城区', fee: 86.00, vehicleType: '4.5T', nature: '自营', customerName: '上海某某运输公司', isInvoiced: '否', invoiceAmount: 0, remark: '' } + ]); + var billData = billList[0]; + var totalEtcFee = useMemo(function () { return billData.reduce(function (acc, r) { return acc + (Number(r.fee) || 0); }, 0); }, [billData]); + + var billColumns = [ + { title: '年份', dataIndex: 'year', key: 'year', width: 72 }, + { title: '月份', dataIndex: 'month', key: 'month', width: 64 }, + { title: '周', dataIndex: 'week', key: 'week', width: 56 }, + { title: '业务员', dataIndex: 'salesman', key: 'salesman', width: 90 }, + { title: '账单日期', dataIndex: 'billDate', key: 'billDate', width: 100 }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 100 }, + { title: '通行时间', dataIndex: 'passTime', key: 'passTime', width: 172 }, + { title: '入口', dataIndex: 'entry', key: 'entry', width: 100 }, + { title: '出口', dataIndex: 'exit', key: 'exit', width: 100 }, + { title: '通行费用', dataIndex: 'fee', key: 'fee', width: 96, align: 'right', render: function (v) { return (v != null ? Number(v).toFixed(2) : '0.00') + ' 元'; } }, + { title: '车型', dataIndex: 'vehicleType', key: 'vehicleType', width: 72 }, + { title: '性质', dataIndex: 'nature', key: 'nature', width: 72 }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 160, ellipsis: true }, + { title: '是否已开票', dataIndex: 'isInvoiced', key: 'isInvoiced', width: 96 }, + { title: '开票金额', dataIndex: 'invoiceAmount', key: 'invoiceAmount', width: 96, align: 'right', render: function (v) { return (v != null ? Number(v).toFixed(2) : '0.00') + ' 元'; } }, + { title: '备注', dataIndex: 'remark', key: 'remark', width: 100, ellipsis: true }, + { title: '操作', key: 'action', width: 160, fixed: 'right', render: function (_, record) { + return React.createElement(React.Fragment, null, + React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function () { message.info('删除 ' + record.id); } }, '删除'), + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { message.info('编辑 ' + record.id); } }, '编辑'), + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { message.info('查看 ' + record.id); } }, '查看') + ); + } } + ]; + + // ========== ETC余额管理 Tab ========== + var balanceFilterCustomer = useState(undefined); + var balanceFilterStatus = useState(undefined); + var balanceFilterProject = useState(undefined); + var balanceFilterMin = useState(''); + var balanceFilterMax = useState(''); + var thresholdModalVisible = useState(false); + var thresholdModalRecord = useState(null); + var balanceList = useState([ + { id: 'c1', type: 'customer', customerName: '嘉兴某某物流有限公司', projectCount: 2, status: '正常', totalBalance: 12580.00, rechargeTotal: 20000.00, consumeTotal: 7420.00, lastRemitDate: '2026-03-01', threshold: 1000, projects: [ + { id: 'c1-p1', type: 'project', projectName: '嘉兴氢能示范项目' }, + { id: 'c1-p2', type: 'project', projectName: '嘉兴城配项目' } + ]}, + { id: 'c2', type: 'customer', customerName: '上海某某运输公司', projectCount: 1, status: '预警', totalBalance: 860.00, rechargeTotal: 5000.00, consumeTotal: 4140.00, lastRemitDate: '2026-03-05', threshold: 1000, projects: [ + { id: 'c2-p1', type: 'project', projectName: '上海物流租赁项目' } + ]}, + { id: 'c3', type: 'customer', customerName: '杭州某某租赁有限公司', projectCount: 1, status: '欠费', totalBalance: -200.00, rechargeTotal: 3000.00, consumeTotal: 3200.00, lastRemitDate: '2026-02-20', threshold: 500, projects: [ + { id: 'c3-p1', type: 'project', projectName: '杭州城配租赁项目' } + ]} + ]); + var balanceData = balanceList[0]; + + var balanceColumns = [ + { title: '客户名称/项目名称', key: 'name', width: 220, render: function (_, record) { + if (record.type === 'customer') return React.createElement('span', { style: { fontWeight: 600 } }, record.customerName); + return React.createElement('span', { style: { paddingLeft: 24 } }, record.projectName); + } }, + { title: '项目数量', dataIndex: 'projectCount', key: 'projectCount', width: 92 }, + { title: '状态值', dataIndex: 'status', key: 'status', width: 88, render: function (v) { + if (!v) return '—'; + var color = v === '正常' ? 'success' : v === '预警' ? 'warning' : 'error'; + return React.createElement(Tag, { color: color }, v); + } }, + { title: '总账户余额', dataIndex: 'totalBalance', key: 'totalBalance', width: 120, align: 'right', render: function (v) { return v != null ? (Number(v).toFixed(2) + ' 元') : '—'; } }, + { title: '客户充值余额', dataIndex: 'rechargeTotal', key: 'rechargeTotal', width: 120, align: 'right', render: function (v) { return v != null ? (Number(v).toFixed(2) + ' 元') : '—'; } }, + { title: '客户消耗ETC费用', dataIndex: 'consumeTotal', key: 'consumeTotal', width: 130, align: 'right', render: function (v) { return v != null ? (Number(v).toFixed(2) + ' 元') : '—'; } }, + { title: '最近汇款日期', dataIndex: 'lastRemitDate', key: 'lastRemitDate', width: 118 }, + { title: '操作', key: 'action', width: 180, fixed: 'right', render: function (_, record) { + if (record.type === 'customer') { + return React.createElement(React.Fragment, null, + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { thresholdModalRecord[1](record); thresholdModalVisible[1](true); } }, '阈值设置'), + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { message.info('跳转账单明细:' + record.customerName); } }, '账单明细') + ); + } + return React.createElement(Button, { type: 'link', size: 'small', onClick: function () { message.info('项目账单明细:' + record.projectName); } }, '账单明细'); + } } + ]; + + var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; + var labelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var formRowStyle = { display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px 24px', marginBottom: 16 }; + var formItemStyle = { marginBottom: 12 }; + + return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + React.createElement(Breadcrumb, { + items: [ + { title: '业务管理' }, + { title: 'ETC管理' } + ] + }) + ), + React.createElement(Card, { style: cardStyle }, + React.createElement(Tabs, { + activeKey: activeTab[0], + onChange: setActiveTab, + items: [ + { + key: 'recharge', + label: '充值明细', + children: React.createElement(React.Fragment, null, + React.createElement('div', { style: formRowStyle }, + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '客户名称'), + React.createElement(Select, { placeholder: '请选择客户', allowClear: true, style: { width: '100%' }, value: filterCustomer[0], onChange: filterCustomer[1], options: [{ label: '嘉兴某某物流有限公司', value: '嘉兴某某物流有限公司' }, { label: '上海某某运输公司', value: '上海某某运输公司' }, { label: '杭州某某租赁有限公司', value: '杭州某某租赁有限公司' }] }) + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '汇款日期'), + React.createElement(DatePicker.RangePicker, { style: { width: '100%' }, value: [filterRemitStart[0], filterRemitEnd[0]], onChange: function (dates) { filterRemitStart[1](dates && dates[0] || null); filterRemitEnd[1](dates && dates[1] || null); } }) + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '收款公司'), + React.createElement(Select, { placeholder: '请选择', allowClear: true, style: { width: '100%' }, value: filterReceiveCompany[0], onChange: filterReceiveCompany[1], options: receiveCompanyOptions }) + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '入账类型'), + React.createElement(Select, { placeholder: '请选择', allowClear: true, style: { width: '100%' }, value: filterEntryType[0], onChange: filterEntryType[1], options: entryTypeOptions }) + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '充值编号'), + React.createElement(Input, { placeholder: '请输入充值编号', value: filterRechargeNo[0], onChange: function (e) { filterRechargeNo[1](e.target.value); } }) + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '审核状态'), + React.createElement(Select, { placeholder: '请选择', allowClear: true, style: { width: '100%' }, value: filterAuditStatus[0], onChange: filterAuditStatus[1], options: [{ label: '待审核', value: '待审核' }, { label: '已驳回', value: '已驳回' }, { label: '已入账', value: '已入账' }] }) + ) + ), + React.createElement('div', { style: { marginBottom: 16, display: 'flex', gap: 8 } }, + React.createElement(Button, { onClick: function () { filterCustomer[1](undefined); filterRemitStart[1](null); filterRemitEnd[1](null); filterReceiveCompany[1](undefined); filterEntryType[1](undefined); filterRechargeNo[1](''); filterAuditStatus[1](undefined); } }, '重置'), + React.createElement(Button, { type: 'primary', onClick: function () { message.info('查询'); } }, '查询'), + React.createElement(Button, { type: 'primary', onClick: function () { addRechargeVisible[1](true); } }, '新增') + ), + React.createElement(Table, { rowKey: 'id', columns: rechargeColumns, dataSource: filteredRecharge, scroll: { x: 1800 }, pagination: { showSizeChanger: true, showTotal: function (t) { return '共 ' + t + ' 条'; } } }) + ) + }, + { + key: 'bill', + label: 'ETC账单明细', + children: React.createElement(React.Fragment, null, + React.createElement('div', { style: { marginBottom: 16, padding: 16, background: '#e6f7ff', borderRadius: 8, border: '1px solid #91d5ff' } }, + React.createElement('span', { style: { fontSize: 14, color: 'rgba(0,0,0,0.65)' } }, '总ETC通行金额:'), + React.createElement('span', { style: { fontSize: 20, fontWeight: 600, color: '#1890ff', marginLeft: 8 } }, totalEtcFee.toFixed(2), ' 元') + ), + React.createElement('div', { style: formRowStyle }, + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '年份'), + React.createElement(Select, { style: { width: '100%' }, value: billFilterYear[0], onChange: billFilterYear[1], options: [{ label: '2026', value: 2026 }, { label: '2027', value: 2027 }] }) + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '时间区间'), + React.createElement(DatePicker.RangePicker, { style: { width: '100%' }, value: billFilterDateRange[0], onChange: billFilterDateRange[1] }) + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '账单日期'), + React.createElement(DatePicker.RangePicker, { style: { width: '100%' }, value: billFilterBillDateRange[0], onChange: billFilterBillDateRange[1] }) + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '通行日期'), + React.createElement(DatePicker, { style: { width: '100%' }, value: billFilterPassDate[0], onChange: billFilterPassDate[1] }) + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '车牌号'), + React.createElement(Input, { placeholder: '请输入车牌号', value: billFilterPlate[0], onChange: function (e) { billFilterPlate[1](e.target.value); } }) + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '车辆性质'), + React.createElement(Select, { placeholder: '请选择', allowClear: true, style: { width: '100%' }, value: billFilterVehicleNature[0], onChange: billFilterVehicleNature[1], options: [{ label: '租赁', value: '租赁' }, { label: '自营', value: '自营' }, { label: '异动', value: '异动' }, { label: '维修', value: '维修' }, { label: '库存', value: '库存' }] }) + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '客户名称'), + React.createElement(Input, { placeholder: '请输入客户名称', value: billFilterCustomer[0], onChange: function (e) { billFilterCustomer[1](e.target.value); } }) + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '是否对账'), + React.createElement(Select, { placeholder: '请选择', allowClear: true, style: { width: '100%' }, value: billFilterReconciled[0], onChange: billFilterReconciled[1], options: [{ label: '是', value: '是' }, { label: '否', value: '否' }] }) + ) + ), + React.createElement('div', { style: { marginBottom: 16 } }, + React.createElement(Button, { type: 'primary', onClick: function () { invoiceConfirmVisible[1](true); } }, '提交开票') + ), + React.createElement(Table, { rowKey: 'id', columns: billColumns, dataSource: billData, scroll: { x: 1900 }, pagination: { showSizeChanger: true, showTotal: function (t) { return '共 ' + t + ' 条'; } } }) + ) + }, + { + key: 'balance', + label: 'ETC余额管理', + children: React.createElement(React.Fragment, null, + React.createElement('div', { style: formRowStyle }, + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '客户名称'), + React.createElement(Input, { placeholder: '请输入客户名称', value: balanceFilterCustomer[0], onChange: function (e) { balanceFilterCustomer[1](e.target.value); } }) + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '状态值'), + React.createElement(Select, { placeholder: '请选择', allowClear: true, style: { width: '100%' }, value: balanceFilterStatus[0], onChange: balanceFilterStatus[1], options: [{ label: '正常', value: '正常' }, { label: '预警', value: '预警' }, { label: '欠费', value: '欠费' }] }) + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '项目名称'), + React.createElement(Input, { placeholder: '请输入项目名称', value: balanceFilterProject[0], onChange: function (e) { balanceFilterProject[1](e.target.value); } }) + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '余额范围'), + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } }, + React.createElement(Input, { placeholder: '最小值', value: balanceFilterMin[0], onChange: function (e) { balanceFilterMin[1](e.target.value); } }), + React.createElement('span', null, '-'), + React.createElement(Input, { placeholder: '最大值', value: balanceFilterMax[0], onChange: function (e) { balanceFilterMax[1](e.target.value); } }) + ) + ) + ), + React.createElement('div', { style: { marginBottom: 16 } }, + React.createElement(Button, { onClick: function () { message.info('导出'); } }, '导出'), + React.createElement(Button, { style: { marginLeft: 8 } }, '批量阈值设置') + ), + React.createElement(Table, { + rowKey: 'id', + columns: balanceColumns, + dataSource: balanceData, + scroll: { x: 1200 }, + pagination: false, + expandable: { + expandedRowRender: function (record) { + if (!record.projects || !record.projects.length) return null; + return React.createElement('div', { style: { padding: '8px 24px', background: '#fafafa' } }, + record.projects.map(function (p) { return React.createElement('div', { key: p.id, style: { padding: '4px 0' } }, p.projectName); }) + ); + }, + rowExpandable: function (record) { return record.projects && record.projects.length > 0; } + } + }) + ) + } + ] + }) + ), + // 新增充值信息弹窗 + React.createElement(Modal, { + title: '新增充值信息', + open: addRechargeVisible[0], + onCancel: function () { addRechargeVisible[1](false); }, + width: 640, + footer: [ + React.createElement(Button, { key: 'cancel', onClick: function () { addRechargeVisible[1](false); } }, '取消'), + React.createElement(Button, { key: 'submit', type: 'primary', onClick: handleAddRechargeSubmit }, '提交') + ] + }, React.createElement(React.Fragment, null, + React.createElement('div', { style: { fontSize: 15, fontWeight: 600, marginBottom: 12, paddingBottom: 8, borderBottom: '1px solid #f0f0f0' } }, '客户信息'), + React.createElement('div', { style: formRowStyle }, + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, React.createElement('span', { style: { color: '#ff4d4f' } }, '*'), '客户名称'), + React.createElement(Select, { placeholder: '请选择/录入客户', allowClear: true, style: { width: '100%' }, value: addFormCustomer[0], onChange: function (v) { addFormCustomer[1](v); addFormRemitAccount[1](v ? v + '(自动带出)' : ''); addFormEtcBalance[1](v ? '12580.00' : ''); }, options: [{ label: '嘉兴某某物流有限公司', value: '嘉兴某某物流有限公司' }, { label: '上海某某运输公司', value: '上海某某运输公司' }] }) + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '汇款账户名'), + React.createElement(Input, { value: addFormRemitAccount[0], disabled: true, placeholder: '选择客户后自动带出' }) + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, 'ETC账户余额'), + React.createElement(Input, { value: addFormEtcBalance[0], disabled: true, placeholder: '选择客户后同步展示' }) + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, React.createElement('span', { style: { color: '#ff4d4f' } }, '*'), '汇款日期'), + React.createElement(DatePicker, { style: { width: '100%' }, value: addFormRemitDate[0], onChange: addFormRemitDate[1] }) + ) + ), + React.createElement('div', { style: { fontSize: 15, fontWeight: 600, marginBottom: 12, marginTop: 20, paddingBottom: 8, borderBottom: '1px solid #f0f0f0' } }, '我司信息'), + React.createElement('div', { style: formRowStyle }, + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, React.createElement('span', { style: { color: '#ff4d4f' } }, '*'), '收款公司'), + React.createElement(Select, { placeholder: '请选择', style: { width: '100%' }, value: addFormReceiveCompany[0], onChange: addFormReceiveCompany[1], options: receiveCompanyOptions }) + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, React.createElement('span', { style: { color: '#ff4d4f' } }, '*'), '操作类型'), + React.createElement(Select, { placeholder: '请选择', style: { width: '100%' }, value: addFormOpType[0], onChange: addFormOpType[1], options: opTypeOptions }) + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, React.createElement('span', { style: { color: '#ff4d4f' } }, '*'), '充值金额(¥)'), + React.createElement(Input, { type: 'number', placeholder: '请输入', value: addFormAmount[0], onChange: function (e) { addFormAmount[1](e.target.value); } }), + React.createElement('div', { style: { fontSize: 12, color: '#666', marginTop: 4 } }, '当前账户余额 + 本次变动金额 = 预计账户余额(选择客户后展示)') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, React.createElement('span', { style: { color: '#ff4d4f' } }, '*'), '收款银行'), + React.createElement(Select, { placeholder: '请选择', style: { width: '100%' }, value: addFormReceiveBank[0], onChange: addFormReceiveBank[1], options: [{ label: '工商银行杭州分行', value: '工商银行杭州分行' }, { label: '建设银行上海分行', value: '建设银行上海分行' }, { label: '农业银行杭州分行', value: '农业银行杭州分行' }] }) + ) + ), + React.createElement('div', { style: { fontSize: 15, fontWeight: 600, marginBottom: 12, marginTop: 20, paddingBottom: 8, borderBottom: '1px solid #f0f0f0' } }, '凭证上传'), + React.createElement('div', { style: formItemStyle }, + React.createElement(Upload, { multiple: true, fileList: addFormFiles[0], beforeUpload: function () { return false; }, onChange: function (info) { addFormFiles[1](info.fileList.slice(-5)); } }, + React.createElement(Button, null, '点击上传或拖拽文件') + ) + ) + )), + // 删除二次确认 + React.createElement(Modal, { + title: '确认删除', + open: deleteConfirmVisible[0], + onOk: handleDeleteRecharge, + onCancel: function () { deleteConfirmVisible[1](false); deleteConfirmRecord[1](null); }, + okText: '确认', + cancelText: '取消' + }, '确认删除该条充值记录?'), + // 提交开票二次确认 + React.createElement(Modal, { + title: '确认开票', + open: invoiceConfirmVisible[0], + onOk: function () { invoiceConfirmVisible[1](false); message.success('开票操作已提交'); }, + onCancel: function () { invoiceConfirmVisible[1](false); }, + okText: '确认', + cancelText: '取消' + }, '确认无误后执行开票操作?'), + // 阈值设置弹窗 + React.createElement(Modal, { + title: '阈值设置', + open: thresholdModalVisible[0], + onOk: function () { thresholdModalVisible[1](false); thresholdModalRecord[1](null); message.success('阈值已保存'); }, + onCancel: function () { thresholdModalVisible[1](false); thresholdModalRecord[1](null); }, + okText: '确定', + cancelText: '取消' + }, thresholdModalRecord[0] ? React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '客户:' + (thresholdModalRecord[0].customerName || '')), + React.createElement('div', { style: labelStyle }, '余额阈值(需大于0)'), + React.createElement(Input, { type: 'number', placeholder: '请输入阈值', defaultValue: thresholdModalRecord[0].threshold }) + ) : null) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-contract-template/index.tsx b/src/prototypes/oneos-web-contract-template/index.tsx new file mode 100644 index 0000000..e027400 --- /dev/null +++ b/src/prototypes/oneos-web-contract-template/index.tsx @@ -0,0 +1,21 @@ +/** + * @name 合同模板管理 + * 自 ONE-OS web端 原稿复刻(1 个页面) + */ +import React from 'react'; +import Page1 from './pages/01-合同模板管理.jsx'; +import { OneosWebLegacyShell } from '../../common/oneosWebLegacy/OneosWebLegacyShell'; + +const pages = [ + { id: 'u5408u540cu6a21u677fu7ba1u7406', title: '合同模板管理', component: Page1 }, +]; + +export default function OneosWebContractTemplate() { + return ( + + ); +} diff --git a/src/prototypes/oneos-web-contract-template/pages/01-合同模板管理.jsx b/src/prototypes/oneos-web-contract-template/pages/01-合同模板管理.jsx new file mode 100644 index 0000000..5a0dff8 --- /dev/null +++ b/src/prototypes/oneos-web-contract-template/pages/01-合同模板管理.jsx @@ -0,0 +1,2362 @@ +// AUTO-GENERATED V26.5 Word HTML +var V265_LEASE_DOCUMENT_HTML = `

商用车租赁合同

合同编号:【LNZLHT {{contractCode}} 】

甲方(出租方):{{lessorName}} (盖章)

乙方(承租方):{{customerName}}          (盖章)

签订日期: 年 月 日

签订日期: 年 月


(正文部分)

本合同经双方平等自愿协商,根据《中华人民共和国民法典》及相关法规,就甲方向乙方提供氢燃料电池车租赁服务之事宜,达成如下一致条款,以资共同遵守。

第一条 租赁标的、服务内容、租期、里程

租赁标的:租赁车辆信息及租金价格、保证金标准详见《租赁订单》(附件1)。

1.2 服务内容:甲方根据乙方的需求,为乙方提供符合约定的商用车供乙方合法使用,并提供有偿非车辆质量问题导致的车辆故障、事故救援服务。

1.3 租期:以双方签订的《租赁订单》(附件1)为准。约定的租期届满,若双方对租赁订单的内容没有变更,且任何一方未提出终止合同的,租赁期限顺延,顺延的期限与本次租赁订单的期限相同。若租赁订单内容有变更,双方签署新的《租赁订单》,租金价格、租期等相关内容,以新的《租赁订单》为准,《租赁订单》未约定的内容,仍以本合同的约定为准。

1.4 里程:乙方承诺按如下约定完成公里数(请在方框里用“√”进行选择。)

1.4.1 Ρ每月完成【 】公里;

1.4.2 ≤每季度完成【 】公里。

若乙方未完成本合同中约定的里程,甲方有权将车辆强制收回,甲方不承担违约责任。

第二条 租金、费用及支付

2.1 租金的结算及支付

2.1.1 租金、服务费先付后用,每【 】个自然月为一个付款周期,每个付款周期末月的25日向甲方支付下个周期的租金和服务费,如25日为节假日的,付款日顺延至节假日后的第一个工作日。

2.1.2 保证金和首期租金及服务费支付:①提车日为当月15日前(含15日)的,于提车前2日内支付保证金和当月剩余天数租金及服务费,当月25日支付下个付款周期的租金及服务费;②提车日为当月15日后的,于提车前2日内支付保证金和当月实际剩余天数和下个付款周期的租金及服务费。

2.1.3 提车或还车当月的租金及服务费,以当月实际租赁天数按比例计算,计算公式如下:【(月租金+月服务费)/30】*当月实际租赁天数支付(金额取数按四舍五入取整到单位“元”)。

2.1.4 租赁期间非因车辆自身质量原因造成的租赁车辆无法上路、无法使用等情况的,乙方仍应按本合同约定支付租金及服务费。包括本条2.3.1中约定的其他费用。

2.1.5租金减免条件:

2.1.5.1基础减免:当月行驶里程达到6000公里及以上的,次月可享受2000元/辆的租金减免,当月行驶里程未达6000公里的,次月不享受租金减免。

2.1.5.2里程补足与累计:次月行驶里程可用于补足上月(截至2026年5月31日之前的月份)未达6000公里的差额部分,补足后可补享上月对应的租金减免,当月行驶里程超过6000公里的部分,可累计计入次月里程。

2.1.5.3连续周期减免:若连续两个月累计行驶里程达到12000公里,该两个月均能享受租金减免。

2.1.5.4持续达标减免:若租赁期间每月行驶里程均满6000公里,可每月享受次月2000元/辆的租金减免(若租期为1年,第12个月里程达标对应的减免,在第13个月租金结算时执行)。

本租金减免政策有效期至2026年12月31日止。

2.2 氢气费(含加氢相关服务费)

2.2.1 氢气原则上由乙方自行解决。如需甲方安排加氢的,先款后货、全额预付款支付及结算。乙方提前预估当月用气总量并计算氢气费用后将氢气预付款【 】元提前【 】个工作日内转账给甲方,甲方在收到乙方预付款后安排加氢。乙方预付款不足【 】元时,乙方接到甲方通知后2个工作日内支付预付款;否则,甲方有权不予安排车辆加氢气,由此造成的一切损失由乙方承担,给甲方造成损失的,乙方还应承担赔偿责任。

2.2.3每月25日核对上月25日至本月24日的用气对账单,有异议2个工作日内提出,双方重新核对账单,账单核对无误后,对核对无误的账单书面确认。

2.2.4 氢气价格按市场价随行就市调整。

2.2.5 乙方还车时车里剩余氢气量若低于交车时的氢气量,则按照【 】元/公斤向甲方支付氢气费,若高于交车时的氢气量,乙方同意甲方不向乙方返还氢气费。

2.2.6 租赁车辆充电产生的电费及与充电相关的费用由乙方承担。租赁车辆原则上优先选择甲方经营的充电桩充电,若选择甲方经营的充电桩充电,费用结算方式为现结。

2.3 其他费用

2.3.1 租赁期间车辆产生的其他费用,包括但不限于非质量问题产生的车辆维修费、保养费、违章费用、施救费、证照补办费用、改装费、违约金、能耗费、路桥费及乙方要求增加购买其他保险的费用等,均由乙方全额另行承担。

2.4 垫付费用:本合同中若甲方垫付费用的,乙方应在甲方通知后【3】个工作日内一次性足额支付给甲方。

2.5 甲方收款账户

2.5.1乙方应将上述租金、服务费、保证金、氢气费及其他费用统一汇入甲方如下指定账户,否则视为未支付:

户 名:【{{lessorAccountName}}】

开户行:【{{lessorBankName}} 】

账 号:【{{lessorBankAccount}} 】

2.5.2 本合同所涉及的所有费用均通过银行转账的方式支付至甲方上述指定账户。甲方不会以任何方式允许甲方人员通过个人账户收取本合同所涉及的各项费用,若甲方人员要求将本合同项下产生的各项费用汇至其个人账户或其他账户,乙方可拨打甲方24小时服务热线:400-021-1773。

2.5.3 乙方将本合同所涉及的费用汇入甲方指定上述收款账户之外的任何账户导致的损失,由乙方自行承担,同时乙方仍须按本合同约定向甲方支付各项费用。

2.6 发票

2.6.1 乙方开票信息如下:

企业名称:

开户银行:

银行账号:

纳税人识别号:

企业地址:

企业电话:

2.6.2甲方收到乙方支付的租金和服务费后分别开具等额的增值税发票。若乙方变更开票信息,乙方需在付款周期15日前书面告知甲方,否则所产生的损失由乙方承担。若国家发布新的税收政策导致税率变化,甲方按最新政策开具发票,乙方支付的总价款不变。

2.6.3 甲方除开具租金和服务费发票外,本合同中的其他费用,甲方不向乙方开具对应的服务费发票。

2.7 乙方款项抵扣

乙方应及时足额地支付租赁费、服务费、保证金以及其他应当由乙方承担的各项费用;乙方存在任何应付未付费用时,甲方有权从保证金、应返还乙方的保险理赔款以及乙方在甲方处的其他款项中先行抵扣,不足部分,乙方应及时补足。

2.8 碳资产

本合同约定仅限于车辆使用,该车辆所产生的碳资产数据权益和碳资产收益归甲方所有,本合同报价不含减碳、碳资产管理费用等。

第三条 交车与还车

3.1 本合同签署前,乙方需提供如下有效证据资料:包括但不限于营业执照、法定代表人身份证复印件、驾驶员驾驶证、驾驶员从业资格证等。复印件留存甲方,原件查验后退回。

3.2 租赁车辆交付时,乙方应对租赁车辆状况及附属设施、有效证件标志进行现场检验,确认无误后应在《交接车验收清单》上签字/盖章确认。提车后,非车辆质量问题产生的一切风险由乙方承担。

3.3 还车时,乙方负责将租赁车辆归还至甲方指定的还车地点。 乙方还车时,应携带提车时签订的《交接车验收清单》,经甲方检查车辆确认无误后,双方签署《交接车验收清单》,确认还车。

3.4 返还车辆除正常使用耗损外,车辆信息与《交接车验收清单》保持一致,无任何毁损,证照齐全,无未处理交通违章、交通事故等不存在未决纠纷争议可能影响甲方利益情形。否则,甲方有权拒绝接收车辆,乙方应在限期内整改,整改期内日租金按两倍日租金标准继续计算,直至车辆符合交还标准;如乙方拒绝或超过十五天仍未完成整改的,甲方有权强制收回车辆并送至修理厂进行修复或调整,因此产生的运输费、修理费等费用以及处理交通违章、事故等发生的一切费用,均由乙方全额承担。

第四条 车辆的风险与管理

4.1 车辆自交付乙方之时起,车辆发生损毁、灭失、被盗、遗失等风险的,由乙方承担。乙方应赔偿甲方全部损失,但该损失是因车辆自身质量问题导致的除外。租赁期间,因租赁车辆引发的甲方、乙方、乙方聘用人员或第三者损失的,乙方应承担全额赔偿责任。

4.2 车辆自交付乙方之时起,乙方负责车辆相关安全保管义务和日常管理,乙方应在本合同约定的使用区域内使用车辆,使用车辆时应符合国家、地方的相关法律法规、规章制度,遵守交通规则,禁止超速、超载,禁止装载易燃易爆、易腐蚀等危险物品及在车中留下刺激性气味的物品。

4.3 租赁期间,乙方装载4.2条中禁止装载的物品,或车辆发生交通事故或其他安全、意外事故,造成甲方、乙方、第三方财产损失或人身损害、车辆损失、设备损失,或导致包括但不限于有关部门的罚金或罚分、行政处罚等一切责任的,均由乙方承担全部责任。给甲方造成的损失或者导致甲方承担责任的,乙方除承担本条前述的全部责任外,还须承担甲方的间接损失、可得利益等。如导致甲方赔偿的,甲方有权向乙方追偿。

4.4 租赁车辆的车身外观、车身广告等权益由甲方享有,未经甲方书面同意,乙方不得擅自更改车身外观,增加车身广告等。否则必须恢复车辆原状,若未恢复的,根据实际发生的费用向甲方支付修复费用,此费用甲方有权从保证金中直接扣除,不足部分,另行向甲方支付。

4.5 无论乙方自身运营或聘用他人运营标的车辆,乙方承诺自身或聘用第三人具备运营车辆的全部证照,乙方对聘用人员证照负有审核义务,证照包括但不限于营业执照、组织机构代码、车辆驾驶人员驾驶证、身份证、道路运输许可证(如进行营运的)、运输人员从业资格证等,乙方对证照的真实性、有效性以及完整性负责,并就该第三人驾驶或使用标的车辆的行为所产生的一切法律责任承担无限连带责任,如导致甲方赔偿的,甲方有权向乙方追偿。在签订合同时,乙方应向甲方出示原件并提供全部证照复印件备案,如驾驶人员发生变更的,乙方应在变更前将全部证照提供给甲方备案。

4.6 未经甲方书面同意,乙方不得将租赁车辆以转让、出借、转租、合租等任何方式许可任何第三方使用,不得设立任何形式抵押、留置、质押等权利负担,不得擅自改装(安装)车辆或拆除车辆设施、设备、标记、广告等,不得拆卸、更换车辆零部件,不得将车辆的行驶证件及相关手续、车辆号牌以任何方式许可第三方使用。

第五条 车辆的维修、保养

5.1 租赁期间,乙方应按照驾驶技术规范及该车使用说明书等规定的要求使用、保养车辆,坚持“三检三查”制,确保车辆技术状况符合规范和要求。甲方有权对车辆的技术状况进行定期或不定期的检查,并根据检查情况要求乙方修复。

5.2 乙方应每天对刹车油、防冻液、刹车片、轮胎、轮胎气压、灯光等做常规检查,如发现问题,应及时到甲方指定的维修站进行维修,并及时向甲方报备维修情况。

5.3 租赁期间,租赁车辆损坏的,乙方应在甲方指定维修站对车辆进行维修、保养,将车辆修复至符合有关技术规范标准的状态,费用由乙方全额承担,并在3日内通知甲方,将维修、保养相关单据的原件交回甲方留存。如需在指定维修站以外维修、保养车辆的,应征得甲方书面同意,否则属于乙方违约,乙方应按2000元/次的标准向甲方支付违约金。

5.4 租赁期间,乙方应负责保管车辆及GPS等设施设备,进行定期或不定期的检修、检测,确保其性能可靠、使用正常。如造成车辆及设施设备损坏、过度磨损或遗失、损毁、失效的,由乙方负责照价赔偿甲方。

5.5 乙方另行购买甲方维保服务的,甲方所提供维保服务仅限车辆日常的技术指导、使用培训、易损件的维修更换服务,不含车辆维修等其他服务。

5.6 非因车辆自身质量问题产生的车辆维修费用及拖车费由乙方全额承担。费用以维修站的价格为准。

5.7 因发生车辆故障、交通事故等情形,甲乙双方对是否属于车辆质量缺陷导致、车损价值等无法达成一致意见的,双方可委托共同认可的第三方机构进行评估、鉴定。鉴定、评估前费用由双方共同平均分担,评估后鉴定结果属于甲方全部或部分责任的,鉴定、评估费由甲方全部或部分承担,甲方应向乙方支付乙方承担的全部或部分鉴定、评估费;不属于甲方责任或属于甲方部分责任的,鉴定、评估费由乙方全部或部分承担,乙方应向甲方支付甲方先行承担的全部或部分鉴定费、评估费。乙方未响应甲方办理评估、鉴定委托事宜通知的,甲方可单方委托第三方机构进行评估鉴定。

第六条 保险、事故、动态违规及违章处理

6.1 甲方负责的保险

6.1.1 若租金中已含租赁车辆的交强险及商业险(包括三者险、车损险)保险费用,乙方对甲方所投保险险种及保额没有异议。如乙方要求增加险种或提高保险额度的,乙方应在租金标准外另行向甲方支付保险费用,由甲方负责向甲方合作的保险服务商购买保险。

6.1.2 若租金中不包含租赁车辆的交强险或商业险保险费用,乙方需按照甲方要求的保险险种委托甲方代为购买,乙方应另行向甲方支付保险费用。若标的车辆保险费用已经由甲方先行购买的,乙方应将相应保险费用支付给甲方。如遇租期内续保,乙方应在甲方通知后【3】个工作日内将相应保险费用支付给甲方。

6.2 乙方负责的保险

6.2.1 汽车车上人员责任保险(驾驶员、车上其他人员)由乙方购买:乙方应为使用该车辆的驾驶员及车上其他人员购买责任保险,租赁期内所发生的与乙方驾驶员及车上其他人员相关的一切财产、人身损失等由乙方承担,若给甲方造成损失的,乙方需向甲方承担赔偿责任。

6.2.2 货物保险:乙方应自行购买货物运输相关保险,并自行承担所有管货责任。租赁期内,无论何种原因导致乙方货物毁损、灭失、污染、变质、损耗、短少等异常情况,甲方均免责。乙方投保时,亦应向保险人要求豁免甲方责任,即使货物异常是由于甲方车辆问题所致,甲方亦不承担任何赔偿责任。

6.3 事故报告

6.3.1在租赁期内,车辆发生安全事故的,乙方应在1小时内如实报告公安交通管理机关、甲方和甲方安全部及保险公司,并配合甲方处理,且到甲方指定的维修站进行维修,若乙方隐瞒事故真实情况、虚构事故情况等或未在甲方指定维修站进行维修的,需向甲方承担3000元/次/辆违约金。当需要办理保险理赔事项时,乙方应协助甲方办理保险理赔手续,因协助办理所产生的费用由乙方承担,若不配合甲方处理安全事故或者不协助办理保险理赔事项的,须向甲方承担500元/次/辆违约金。本条项下全部应付违约金,统一并入下一租金付款周期同步结算。

6.3.2 如乙方怠于履行本合同约定义务导致无法办理保险理赔或保险公司拒赔、免赔的,全部损失和法律责任由乙方承担,造成甲方或第三方损失的,乙方应予以赔偿。

6.4 出险

6.4.1 租赁期间,如车辆发生安全事故造成损失超出保险公司赔付金额的,超出部分的损失由乙方全额承担。

6.4.2 租赁期间,租赁车辆出险并办理保险理赔的,乙方应每次向甲方支付保险上浮费,18吨车型主责及以上责任为3000元/次/辆,其他责任为2000元/次/辆(驾驶员无责除外)。如乙方自车辆出险之日起 5个工作日内未向甲方支付上述保险上浮费,则甲方有权直接从车损险理赔款项中扣除对应的保险上浮费,若车损险理赔款项不足以覆盖乙方应向甲方支付的保险上浮费,差额部分乙方须于下一租金支付周期内,连同当期租金及其他应付费用一并结清。

6.4.3 乙方出险进保,应征得甲方书面同意。如事故损失总金额不满足甲方进保理赔标准的(2000元里面不允许进保,),由乙方向甲方报备后自行到甲方指定维修站维修并由乙方自行承担相应费用,无需办理保险理赔手续。如乙方在未征得甲方书面同意前提下向保险公司报案赔付的,须向甲方承担2000元/次/辆违约金,同时,甲方不向乙方支付任何保险理赔款,若保险理赔款直接打入乙方账户的,乙方需将保险理赔款返还给甲方。

6.4.4 保险上浮费和超出保险公司赔偿的部分,甲方有权从保险理赔款中先行抵扣,如保险理赔款有剩余且应支付给乙方的,在乙方不存在到期应付未付款项时,甲方在收到理赔款后 15个工作日内无息支付给乙方,如保险理赔款直接赔付给第三方的,则乙方应在甲方通知后3个工作日内支付给甲方,且甲方有权从保证金中直接抵扣。

6.4.5 单车事故进保审批​

6.4.5.1 租赁期间,因乙方违反限高、限速、限重等交通管理规定或操作规范导致的单车事故,原则上不予办理保险理赔,相关车辆维修、施救等全部费用由乙方自行承担。​

6.4.5.2 若乙方因特殊情况申请对本条 6.4.5.1 约定的单车事故进行保险理赔(以下简称 “进保申请”),需向甲方提交书面申请,并附事故现场照片、交警部门出具的事故认定书(如有)、车辆损失评估报告等相关证明材料。​

6.4.5.3 甲方收到乙方进保申请后,由甲方在收到材料后【5】个工作日内完成审批并出具意见。

6.4.5.4 仅甲方审批通过,甲方才协助办理理赔,未经审批擅自进保的,乙方支付【2000】元/次辆违约金,且承担全部保险费用及理赔损失。​

6.4.5.5 经甲方审批通过的进保,乙方仍需按 6.4.2 条支付保险上浮费,甲方可根据情况要求乙方额外承担评估费、手续费等

6.5 安全教育

6.5.1 乙方发生交通事故或者安全事故的,甲方有权对乙方及乙方驾驶员进行教育培训。

6.5.2 在租赁期内,若乙方发生一般有责事故及以上事故,乙方必须做到:①查清事故原因;②严肃处理事故责任者;③对车辆事故的驾驶员进行教育培训;④落实防范措施。若甲方做过安全教育培训的,还需向甲方提供安全教育会议内容、安全教育签到表、现场至少三张照片。若乙方未做到前述事项的,须向甲方承担1000元违约金。

6.5.3若乙方发生三次或者三次以上(含三次)以及一个星期连续发生两次及两次以上(含两次)安全事故或者交通事故的,甲方有权对乙方及乙方驾驶员进行安全教育培训,乙方必须参加甲方组织的教育培训活动,乙方接到甲方教育培训未按时参加的,需向甲方承担1000元违约金。如乙方经甲方约谈教育后仍发生事故,视为乙方严重违约,甲方有权单方面无条件强制收回租赁车辆,无需承担任何违约责任。收车过程中产生的拖车费、保管费、运输费、场地占用费等一切相关费用,均由乙方承担;同时,因乙方前述违约行为造成甲方全部损失(含但不限于行政罚款、赔付款项、律师费、诉讼费、保全费、差旅费等),均由乙方全额承担,甲方有权从保证金中直接抵扣,保证金不足以清偿的,甲方有权向乙方继续追偿剩余款项。

6.5.4动态监控违规管理:

6.5.4.1一般违规:租赁期限内,因驾驶员违规行为产生属实动态监控预警风险的,乙方按以下标准承担违约金:首次50元/次,第二次100元/次,第三次及以上违规500元/次,乙方应当在甲方通知违规之日起1个自然日内支付给甲方,逾期未支付的,乙方须在次月车辆租金结算时一并向甲方支付。乙方更换新驾驶员须提前向甲方书面报备,人员更换后违规次数将重新计算。

6.5.4.2 严重违规处置及车辆收回:租赁期限内,车辆因违规导致动态监控风险属实,连续2个月被交管部门通报的,甲方有权要求乙方在第2个自然月底起将车辆停运1个月,停运期间正常计收租金。为避免车辆营运证连续3个月通报导致被注销,乙方须严格遵照甲方停运要求执行,乙方拒不配合停运的,视为严重违约,甲方有权单方无条件强制收回租赁车辆,无需承担任何违约及赔偿责任。若因乙方拒不配合收车、违规持续营运造成车辆营运证被注销的,乙方须全额承担营运证注销后6个月内全部车辆租金。车辆强制收回过程中产生的拖车费、停放保管费、转运费、场地使用费等全部费用均由乙方承担。因乙方前述违约行为给甲方造成的全部损失(含但不限于行政罚款、赔偿金、律师费、诉讼费、保全费、差旅费等),均由乙方全额承担,甲方有权从车辆保证金中直接抵扣,保证金不足以清偿的,甲方有权就剩余差额向乙方另行追偿。

6.6 违章处理

6.6.1车辆发生违法违规的,乙方应及时处理车辆违章并承担处理费用(违章,最晚应在甲方通知后15个自然日内处理完毕)。乙方超过15天未处理的,甲方有权代办处理,并向乙方发送强办告知函,明确强办时间、强办费用,强办处理的收费标准以附件2的约定为准。同时,甲方有权按一条违章信息1200元的标准(广东区域内为2000元/条),向乙方收取违约金;违约金金额不足以覆盖甲方损失时,乙方还应承担赔偿责任。

6.6.2在处理违章过程中,乙方必须遵循如下驾驶证使用限制:车辆租赁期限6个月以下的(含6个月),仅限于一本驾驶证处理违章,租赁期限超过6个月的,仅限于两本驾驶证处理违章,乙方处理违章超过前述限制的,须向甲方承担 5000元/ 次/辆的违约金。租赁满一个记分周期可使用三本驾驶证。

6.7 年检年审配合

6.7.1甲方负责办理车辆年检年审等相关证照手续,乙方应积极配合,在甲方通知的时间内将车辆开到指定地点。乙方完全明白且理解,车辆的年检年审等检验检测工作,是车辆依法、安全运营的基本保障,如乙方未能在甲方通知时间内将车辆开到甲方指定地点,属于乙方严重违约行为,不论最终是否顺利完成相关检验检测手续,乙方均应向甲方支付2000元/次的违约金。

6.7.2车辆年审过程中发现有未处理违章罚分、罚款等,经甲方以口头、电话、短信、微信及其他方式等前述任一方式告知乙方后,乙方在【3】个工作日内仍未履行的,甲方有偿代办处理,代办处理费用同6.6.1条规定。

6.7.3 乙方应按有关规定使用车辆,不得私自改装车辆,如需改装应经甲方同意,保证车辆在年检时符合要求,如年检不合格,乙方应自费维修整改,经维修整改后仍不合格的,乙方应赔偿甲方重新购买同款同型车辆及设施设备的全部费用。如乙方未经甲方同意,私自拆卸、改装车辆核心零部件,视为乙方严重违约,由此产生的一切责任和后果均由乙方承担;如因此造成车辆严重安全隐患,乙方须根据甲方要求,按照该车辆购买价(以发票金额为准)的两倍购买该改装车辆

6.7.4 租赁期间,车辆相关证照遗失、被盗、毁损的,甲方可以协助补办,乙方应承担补办证照发生的费用(包括但不限于差旅费、工本费、官费等);具体费用标准以附件2约定的为准。在证照办理期间,租赁费用不停止计费。

第七条 调换车辆和备用车辆服务标准

7.1 甲方有权根据实际运营情况,对已经出租给乙方的车辆进行调换,但调换车辆应是同类型车辆,并提前15天通知乙方,若乙方不同意,甲方有权强制收车,终止与乙方的合作,本合同即终止,双方均不承担违约责任。

7.2 在租赁期内,如非因车辆自身质量问题导致维修的,维修期间,乙方可向甲方申请备用车辆,在征得甲方同意后,交付备用车辆供乙方使用,乙方应按照交付车辆类型对应的租金标准另行向甲方支付租金、保险费等租赁费用。

7.3 调换车辆和备用车辆交还车流程参照本合同相应条款的约定。

第八条 违约责任

8.1如乙方违反本合同项下任何约定(含保证人违约),视为乙方违约,乙方应当按照现行法律法规及本合同约定向甲方承担违约责任,且甲方有权采取如下一种或者多种救济措施以维护其权利:

8.1.1要求乙方在5日内补正或采取补救措施,未在5日内整改补正的,甲方有权书面通知乙方单方终止本合同或终止所涉车辆租赁关系。

8.1.2要求乙方赔偿甲方的经济损失。乙方致使甲方承担任何费用、责任或者遭受任何损失的,乙方应就前述任何费用、责任或者损失向甲方进行赔偿,这些费用、损失包括甲方向第三方支付的或导致第三方遭受的赔偿金、违约金等费用及损失。

8.1.3要求乙方承担因乙方行为导致甲方进行各类诉讼或者仲裁而产生的费用,包括但不限于诉讼费、律师费、保全费、公证费、鉴定费、人员工资、差旅费等,且这些费用不应包含在本合同约定的违约金中。

8.2 乙方同意并确认,如乙方违反本合同项下任何一项约定、任何承诺和保证,除本合同另有特别约定外,乙方应按5000元/次的标准向甲方支付违约金。致使甲方承担任何费用、责任或者遭受损失的,乙方应就上述任何费用、责任或者损失(包括但不限于因违约而支付或损失的利息以及律师费)向甲方另行赔偿。

8.3 本合同履行期内,乙方逾期支付租赁费、保证金以及应支付的其他任何费用的,每逾期一日,应向甲方支付应付款项千分之三的违约金,且甲方有权锁车再进行协商处理;超过当期结算日期3天仍未支付相关费用,甲方有权提前终止本合同并强制收回车辆。

8.4 乙方根据法律及本合同约定应向甲方支付费用、赔偿损失或承担其他法律责任,在乙方未能全面履行时,甲方可从保证金中直接进行等额抵扣,不足部分仍由乙方承担,且乙方应在甲方书面通知后3个工作日内补足保证金至本合同约定数额,否则,每逾期1日,乙方每日按照应付未付保证金金额的千分之三向甲方支付违约金。

8.5 在租赁期内,乙方使用标的车辆超载、超速或有其他违反交通规范行为但未造成损失的,法律责任由乙方承担,甲方给予乙方警告处理,后续若拒不纠正再次发生的,乙方应向甲方支付2000元/次违约金,达到提前终止合同条件的,甲方有权提前终止合同并强制收回车辆。

8.6 若因乙方未按1.4条完成里程,造成甲方或车辆厂商不能或未能足额申领国家、地方补贴的,差额部分由乙方负责赔偿给甲方或车辆厂商,导致甲方赔偿的,甲方有权向乙方追偿。

8.7 乙方按照本合同约定向甲方支付违约金的,如同一违约情形下违约金约定不一致的,以约定较高标准的违约金为准,如同一违约情形下设定不同类型违约金的,违约金应累计计算,如违约金不足以弥补甲方损失的,乙方另行向甲方补足。

第九条 合同终止、解除

9.1 因不可抗力或行业主管部门的要求,致使本合同无法继续履行的,甲乙双方均有权提前终止本合同,双方均不承担违约责任,在乙方向甲方结清全部费用后,双方无任何争议,甲方将剩余保证金及租金无息退还乙方。

9.2 因甲方经营状况发生重大变化,致使甲方不能继续从事车辆租赁服务的,甲乙双方均有权提前终止本合同,且双方均不承担违约责任,在乙方归还甲方车辆,向甲方结清全部费用后,甲方应将剩余保证金及租金无息退还乙方。

9.3 经甲乙双方协商一致,本合同可以解除或提前终止。

9.4 在租赁期限内,因乙方行为导致甲方解除或提前终止本合同的,或乙方出现下列情形之一的,甲方有权立即解除或提前终止本合同并强制收回车辆,且乙方应向甲方支付违约金,违约金标准为租赁当年月平均租金总价标准的2倍:

9.4.1利用标的车辆从事违法犯罪活动的,或因涉嫌违法犯罪被采取强制措施或受到行政处罚的;

9.4.2 未按期支付本合同项下的相应费用超过3个工作日的,经甲方催告后仍不支付的;

9.4.3涉酒涉毒驾车、交通事故逃逸、不及时抢救伤员保护现场、谎报或瞒报交通事故的;

9.4.4 擅自将标的车辆转让、转租、转包,或设置抵押、留置、质押等负担,或恶意更换车辆零部件或拆撤、损毁、涂抹车辆设施、设备、标记、广告等的;

9.4.5 标的车辆技术状况经甲方累计3次检查不合格,或经甲方检查不合格要求乙方修复,乙方在3日内拒不修复的;

9.4.6 向甲方提供虚假证件或资料的;

9.4.7 发生3次有责交通事故或单次有重大、特大交通事故的;

9.4.8甲方通知乙方协助办理标的车辆年检手续,乙方不按甲方的要求履行协助义务,导致标的车辆无法年检或年检不合格的;

9.4.9利用标的车辆进行相关法律法规明令禁止的行为,或其他严重影响甲方权益或可能导致甲方损失的行为;

9.4.10 乙方有违反本合同约定导致甲方解除或提前终止本合同的其他情形的。

9.5 租赁期限届满前,乙方提前退租的,则属于乙方严重违约,乙方应按租赁当年月平均租金总价标准的2倍向甲方支付租金补贴,租金补贴甲方有权从乙方保证金中直接抵扣。

9.6 甲方提前解除本合同的,应提前15天书面通知乙方,乙方如有异议的,乙方应在通知送达之日起15日内书面回复,但不影响甲方解除合同的效力。

9.7 本合同中的合同解除、业务终止之标准:甲乙双方在《交接车验收清单》上签字,退车手续办理完毕,乙方处理完所有违章,扣除各种乙方应承担的费用,甲方无息退还保证金。

9.8 乙方若需要办理ETC,由乙方自行办理。合同解除、业务终止后,乙方须先自行注销ETC,再办理退车手续,否则向甲方支付2000元/卡退卡费。

第十条 其他

10.1 关于送达:任何与本合同有关的由一方发送给其他方的通知或其他通讯往来(“通知”)应采用书面形式,按照下列通讯地址或通讯号码或电子邮箱送达被通知人均构成有效通知,包括但不限于法院、仲裁机构的法律文书及双方的来往函件等。

甲方:{{lessorName}}

通讯地址:{{lessorAddress}}

联系人姓名及电话:{{lessorContact}} {{lessorPhone}}

邮箱:{{lessorEmail}}

乙 方 :

通讯地址:

联系人姓名及电话:

邮箱:

10.2 任何一方上述的通讯地址或通知方式发生变化(“变动方”),变动方应当在该变更发生后的七(7)日内通知其他方。变动方因上述地址不明确或未按约定及时通知,导致法院、仲裁机构的法律文书或任一方文件无法送达的,文件退回之日即视为送达之日。甲乙双方确认,前述送达方式适用于各个司法阶段,包括但不限于一审、二审、再审、执行以及督促程序。

10.3 本合同中约定的违约责任、赔偿责任条款以及乙方义务,甲方于订立合同时已提请乙方注意并按乙方要求对该条款予以说明,乙方已充分了解并自愿订立且不再提出违约金、赔偿金过高和义务过多的请求

10.4 本合同未尽事宜可另行签订补充合同,补充合同与本合同具有同等效力。

10.5 甲、乙双方因履行本合同发生争议,可协商解决,协商不成提起诉讼的,甲乙双方一致同意由合同签订地(浙江省嘉兴市平湖市乍浦镇)人民法院管辖。

10.6 本合同一式两份,甲方持一份,乙方持一份,在甲方盖章,乙方签字(自然人)或盖章后生效,若有保证人,保证人签署本合同及附件《保证人的保证责任》后,对保证人生效。

10.7 本合同所有附件、附表、车辆交接单均是本合同的有效组成部分,与本合同具有同等的法律效力。本合同包括但不限于以下附件:

附件1《租赁订单》、附件2《车辆产生的费用》、附件3《安全责任告知书》、附件4《重型普通货运车辆租赁动态监管责任告知与确认书》、附件5《安全责任承诺书》、附件6《车辆使用规范和驾驶员要求》《授权委托书》《安全责任协议》及其他。

(以下无正文)
















附件1:租赁订单 订单编号(LNZLHT )

甲方(出租方): {{lessorName}}

乙方(承租方):

本订单租赁车辆总计辆,合同租金及服务费含税总价为:¥ 元,保证金总计需缴纳¥ 元(若开具保证金收据的,退车时应提交保证金收据原件方可退还保证金),首期租金及服务费含税需支付¥ 元。租赁车辆含保险总计辆,每辆保险险种包含 三者责任险【】万元、车损 ,含通行证总计张。


一、租赁车辆信息


品牌

车型


车牌号

租金及服务费

小计

(元/ 月/辆,含税)

价格及服务内容


保证金

(元/辆)


车辆租赁期限

车辆租金

(元/月/辆,含税)服务费

(含税)服务内容

车牌号在签约时无法确定的,以实际交付的车牌号为准,若后续需要更换车辆的,均以《交接车验收清单》为准。

二、其他约定:

1.车辆交付(交还)地点: 。车辆交付时间:车辆交付时间:     年 月 日(以实际交付的时间为准)。

2.租赁车辆仅限 行政区域范围内使用,超出前述约定区域范围使用租赁车辆应事先征得甲方书面同意并办理相关手续。

3.甲乙双方协商一致增加租赁车辆的,双方另行制作《租赁订单》,经甲方盖章,乙方签字/盖章后生效。

4 、甲乙双方均认可并接受双方签订的《商用车租赁合同》(合同编号 )全部内容,本租赁订单列明的条款的,按本租赁订单执行,本租赁订单未列明的条款,均按照《商用车租赁合同》执行。

5.甲方有权对租赁车辆进行调换,但应提前15天通知乙方,乙方必须配合。还车、调换车辆时车辆外观、驾驶室、车厢必须保持干净整洁,否则收取清洗费,车身原车广告未经甲方同意不得撕毁、损坏、涂抹,否则承担车辆的修复费。本条中的清洗费,500元/辆,车身修复费,5000元/辆,甲方有权在保证金直接扣除。

6.租赁期内车辆产生的能耗费、停车费等用车费用由乙方承担。

7.本《租赁订单》一式两份,甲方执一份,乙方执一份,在甲方盖章,乙方签字(自然人)或盖章后生效。保证人签字(自然人)或盖章后,对保证人生效。


甲方(签章


乙方签字/盖章

授权代表:


授权代表:

签订日期:

年 月 日

签订日期: 年 月


附件2:车辆产生的费用

、车辆维保、易损件、车辆轮胎、蓄电池及送车上门和上门收车费用:

维保服务

易损件

轮胎及蓄电池

自行承担车辆维保所发生的所有费用

自费承担

型号:11.00R20 18PR ,自费承担,还车时轮胎每磨损1mm,乙方需向甲方支付【110 】元/毫米的折旧费(计费公式:(交车时轮胎厚度-还车时轮胎厚度)*【mm 】,如乙方在租赁期内更换过轮胎,计算费用为负值的,乙方无需支付费用,甲方也不做任何补偿。)蓄电池,自费承担,每块电池【1200 】元/2年,按照实际租赁时间折算费用。

特别说明:1、轮胎、门把手刹车片等易损件正常磨损达到需更换时客户负责更换,配件、维修工时费乙方自理;2、乙方充分知晓和同意,如果承租的甲方车辆并非全新车辆,或面临车辆租赁后标的车辆配件即过保的情形;3、如乙方未根据下表维护保养标准执行车辆维护工作,造成车辆脱保的,由此给甲方产生的不利影响、额外义务负担、费用损失等均由乙方自行承担。

送车上门,上门收车:从出发往返1公里以内免费;往返1公里以上,按8元/公里收费;人工费以实际维修项目算。拖车费:起步价【400】元+【 5】元/公里/空车,起步价【400】元+【15】元/公里/重车;基础服务费为【100 】元/辆/次。材料费以维修站的价格为准,最终以甲方确定的价格为准。


二、保养和修复:不论乙方租赁车辆期限长短,乙方确认并同意均按照下述规则维护保养和修复标准执行, 产生的费用按维修站的价格计算,若维修站未确定价格的,参考下表价格执行,最终以甲方确定的价格为准:1、底盘保养

序号区分保养项目周期/万KM型号数量单价次数总价公里要求(万公里)1配件驱动电机、电池冷却液24个月/20万公里重负荷有机乙二醇型冷却液36L1432286202工时501503配件转向器6个月/10万公里8 号液力传动液)及滤芯1.7L251.742.5104工时501505配件新能源法士特、绿控12个月/5万公里GL-5\\85W7L25717556工时501507配件油润滑的中

后桥12个月/10万公里GL-5\\80W-909L29.79267.3108工时501509配件脂润滑的轮边2个月/1.5万公里3#锂脂3KG203601.510工时7517511配件驾驶室举升器6个月/5万公里10 号航空液压油1L30130512工时5015013配件空气干燥器12个月/10万公里科诺尔、威伯科1个18411841014工时3013015配件空气滤清器12个月/4万公里空气滤芯1个1501150416工时3013017配件各类球头2个月/1万公里2号锂基脂751751合计



2.燃料电池保养项目

序号保养配件周期/万KM次数量单价(元,未税)工时【90】元/时公里要求(万公里)1空滤27/1099 4522离子过滤器首次1万KM,之后每次间隔3万KM4/969.5790首次1万,后每次间隔3万3冷却液8万KM140L1664.845 1808三、证照及违章处理等收费标准:如甲方代为处理,乙方应向甲方支付的代处理费计费结构为:(基础费用)+(服务费收费标准),具体如下表:

项目

基础费用

服务费收费基础

服务费/元

补办行驶证

以实际收费为准

每台车每次

200

补办牌照

以实际收费为准

每台车每次

300

补办营运证

以实际收费为准

每台车每次

300

补办加氢证

以实际收费为准

每台车每次

300

处理违章(罚款)

实际罚款为准

每台车每条

100

处理违章(罚分)

实际罚分为准

每台车每条

100

借用备用钥匙

送上门,20公里内100元,每超1公里3元每公里,过桥过路费额外收取

每台车每次

100

补配钥匙

实际补配价格

每台车每次

400


附件3:安全责任告知书

一、各车辆使用人责任主体:

1.各租赁客户合作伙伴、挂靠我司车辆的用户,当您使用我司车辆期间必须承担安全责任,责任主体是:车辆使用人(公司或个人),挂靠车辆使用人(公司或个人)。

2.根据《中华人民共和国民法典》第一千二百零九条,因租赁、借用等情形机动车所有人、管理人与使用人不是同一人时,发生交通事故等造成损害,属于该机动车一方责任的,由机动车承租人和使用人承担该赔偿责任。

3.租赁车辆、挂靠车辆使用人,平时要做好出车检查,制订车辆日常检查记录表,对于:氢系统、电池、灯光、轮胎、车辆外观、随车配置物品等做好日常检查并记录,如有异常必须及时处理,车辆合格满足出车条件以后才能出车,并且必须保持车辆定位系统在线。

4.车辆、挂靠车辆使用人,要做好我司车辆的安全、事故台账,出事故必须登记在台账中,并且开展四不放过教育:①事故原因没有查清不放过,②事故责任者没有严肃处理不放过,③全体司机及车队管理人员没受到教育不放过,④防范措施没有落实不放过。

开展教育必须有现场多张照片,所有司机及车队管理人员在教育签到表上签字,照片及签到表原件留档,我司安全部备查。

二、车辆使用安全注意事项:

1.所有驾驶员要有从业资格证,9.6米厢车与4.2米冷藏车,必须出示本人资格证,登记后才可以提车。

2.注意车辆高度,苏龙、柳汽乘龙等9.6米车高4米。跃进、现代冷链4.2米车高3.38米。现代普货车高3.28米。集卡飞驰车头3.9米,挂车装载平箱4.2米左右。驾驶员一定要注意车厢高度,当心限高杆、桥梁、树枝等物。

3.因为是氢用车,车内不得吸烟,车周边10米以内不得吸烟,车辆远离烟火(周边10米以上),不得将车停放在高压电线下。

4.进入加氢站,必须无烟火,关闭总电源后加氢。服从加氢站管理,如:静电接地,不超过35Mpa压力等等。

5.车辆遇到氢气泄漏,不严重移至空旷处,关闭总电源!车辆旁边不得有闲杂人等,远离明火,联系运维人员:江浙沪沈帅15021428914;新疆赵波18639103977;广东王河北13928468652;集卡车李泽琦13383480807;北京及其他地方赵小峰18868302409。

6.系统漏气并着火,火焰未烧至其他管路和车身;处理方式:立即熄火,关闭电源,马上灭火,立即打上述专业人员电话,在他们指导下灭火,以防电池等爆炸。如果火势大、控制难、立即拨打 119,人员撤离至安全区域。

7.供氢系统漏气着火,或电池着火,火焰烧至其他管路或车身;处理方式:立即拨打119,人员撤离至安全区域。

8.车辆每日检查:轮胎完好、气压标准、灯光设施齐全、车辆不带病上路。

9.雨雾天气,降低速度。注意能见度,与前车保持足够安全距离。

10.行驶中电池高温冒烟,马上靠边,断开高压系统,马上联系运维。

11.车辆发生火情立即停车,用灭火器降温灭火,关闭电源,火势大要保证人身安全,离开车辆打119报警。

12.氢能车辆禁止电焊,周围不许任何焊接,此类维修通知专业人员来修理。

13.出交通事故,第一时间打安全经理王屹青电话:18116198188,告知实际情况,加此电话微信,拍现场照片:整体事故场景、我方车牌、对方车牌、我方及对方车辆受损部位。并发定位,安全经理会指导处理事故。

贵方上述安全告知已收到并熟悉,我方(被告知人)郑重承诺:无条件遵守上述安全操作事项。若因我方(被告知人)原因导致发生安全事故,我方(被告知人)自行承担一切法律责任。






我方已清楚地了解附件 1《租赁订单》、附件2《车辆产生的费用》、附件3《安全责任告知书》和所有内容及车辆租用相关要求,在此向出租人郑重承诺:保证遵守《商用车租赁合同》及附件文件所有约定。





确认方(签字/盖章):

确认时间: 年 月 日


附件4:重型普通货运车辆租赁动态监管责任告知与确认书

甲方(出租方):{{lessorName}}

乙方(承租方):{{customerName}}


为严格落实《中华人民共和国道路交通安全法》《道路货物运输及站场管理规定》等相关法律法规及行业监管要求,规范重型普通货运车辆租赁期间的安全运营行为,强化驾驶员安全管控,防范道路交通安全风险,保障租赁车辆、人员及公共安全,现就甲方租赁给乙方车辆的动态监管违规行为及处置事宜,正式告知如下:

甲方将对租赁车辆实施24小时动态监管,重点监测驾驶员存在的超速行驶、超时驾驶(持续时间≥4小时)、接打手持电话、操作手机、抽烟、生理疲劳驾驶、未按规定系安全带、偏离驾驶位、佩戴红外阻断型墨镜遮挡视线、动态监控设备被遮挡或失效、双手脱离方向盘、不目视前方行驶、实线变道、前向碰撞、车道偏离、前向车距过近等各类违反道路交通安全法律法规及双方签订的《商用车租赁合同》约定的违规行为。本确认书所载违规行为的认定,以甲方提供的、由车辆合法装载的动态监控设备所记录的数据、影像为准。若乙方对相关数据、影像记录的违规事实有异议,应在收到甲方通知后48小时内书面提出,并提出理由。乙方驾驶员的上述违规行为,不仅会给乙方自身运营带来重大安全隐患与法律责任风险,还将对甲方造成合规、运营等多方面的不利影响及实际损失,具体如下:

一、合规经营风险:

甲方作为重型普通货运车辆出租主体,依法承担租赁车辆的动态监管主体责任,若乙方驾驶员的违规行为被交通运输、公安交管等监管部门查处,甲方将面临行政处罚、行业资质核查、信用评级下调等合规风险,情节严重的,将直接影响甲方道路运输相关业务的正常开展。若租赁车辆因乙方原因(包含但不限于乙方驾驶员屡次、严重违规或乙方安全管理缺失)导致租赁车辆被交通主管部门依法做出撤销/吊销/注销其道路运输证,导致车辆无法运营,相关损失由乙方承担。

二、运营资源消耗:

针对乙方驾驶员的违规行为,甲方需额外投入大量人力、物力开展违规核实、沟通对接、整改监督等工作,占用甲方动态监管、车辆维护、客户服务等核心运营资源,大幅降低整体租赁业务运营效率,严重影响甲方对其他合规客户的服务质量,造成甲方运营成本增加。

针对监测发现的驾驶员违规行为,结合上述违规行为对甲方造成的各类影响与损失,甲方将采取分级处置措施,具体如下:

(1)一般违规:租赁期限内,因驾驶员违规行为产生属实动态监控预警风险的,乙方按以下标准承担违约金:首次50元/次,第二次100元/次,第三次及以上违规500元/次,乙方应当在甲方通知违规之日起1个自然日内支付给甲方,逾期未支付的,乙方须在次月车辆租金结算时一并向甲方支付。乙方更换新驾驶员须提前向甲方书面报备,人员更换后违规次数将重新计算。

(2)严重违规处置及车辆收回:租赁期限内,车辆因违规导致动态监控风险属实,连续2个月被交管部门通报的,甲方有权要求乙方在第2个自然月底起将车辆停运1个月,停运期间正常计收租金。为避免车辆营运证连续3个月通报导致被注销,乙方须严格遵照甲方停运要求执行,乙方拒不配合停运的,视为严重违约,甲方有权单方无条件强制收回租赁车辆,无需承担任何违约及赔偿责任。若因乙方拒不配合收车、违规持续营运造成车辆营运证被注销的,乙方须全额承担营运证注销后6个月内全部车辆租金。车辆强制收回过程中产生的拖车费、停放保管费、转运费、场地使用费等全部费用均由乙方承担。因乙方前述违约行为给甲方造成的全部损失(含但不限于行政罚款、赔偿金、律师费、诉讼费、保全费、差旅费等),均由乙方全额承担,甲方有权从车辆保证金中直接抵扣,保证金不足以清偿的,甲方有权就剩余差额向乙方另行追偿。

乙方需高度重视租赁车辆驾驶员的安全管控工作,切实履行承租方安全管理主体责任,加强对驾驶员的日常安全教育、培训及常态化管理,严格落实本确认书约定的分级处置措施,积极配合甲方的动态监管工作,及时整改各类违规行为。甲方将定期向乙方反馈车辆动态监管情况,对乙方未按本确认书要求落实处置措施、整改不到位或者拒不配合监管的行为,甲方将严格按照法律法规及双方签订的《租赁合同》约定处理。

本确认书自甲乙双方签字或盖章后生效,本确认书作为《商用车租赁合同》的附件,与《商用车租赁合同》具有同等法律效力。本确认书未尽事宜,按照双方签订的《商用车租赁合同》相关约定执行;若本确认书内容与相关法律法规冲突,按法律法规规定执行。

乙方确认:已认真阅读并充分全面理解本确认书全部内容,自愿严格遵照本确认书约定及相关法律法规要求执行,并承担因违反确认书约定及相关规定产生的全部责任。

(以下无正文)



甲方(盖章): 乙方(签字/盖章):


授权代表(签字): 授权代表(签字):


日期: 年 月 日 日期: 年 月 日











附件5:

安全责任承诺书

为严格落实《道路运输车辆动态监督管理办法》《广东省道路运输条例》及粤标动态监控管理要求,防范因超速、疲劳驾驶、接打电话、抽烟、遮挡摄像头、屏蔽设备、离线等违规行为被交通运输主管部门连续通报,避免因此导致车辆被责令停运、营运证被暂扣或注销等严重后果,并就车辆粤标动态监控相关安全责任,作出如下郑重承诺:

1.严格遵守《道路运输车辆动态监督管理办法》《广东省道路运输车辆智能监管系统应用管理办法》等国家及地方性法规,并已阅读、理解且承诺遵守贵公司提供的《粤标动态监控专项安全管理方案》(下称“管理方案”)相关规定,服从贵公司依据该方案及相关法规实施的动态监控管理。

2.负责督促我司承租司机规范驾驶,杜绝疲劳驾驶、超速、分心驾驶等违规行为,严禁破坏、屏蔽、断电、拆机等影响粤标监控设备正常运行的行为。

3.及时接收贵公司反馈的车辆违规信息,严格按照要求落实整改,配合贵公司开展司机安全教育、车辆停运等处置措施。

4.若因我司及我司承租司机违规行为,导致车辆被交管、运政部门通报、处罚或贵公司遭受其他损失的,我方同意承担由此产生的全部直接经济损失,包括但不限于政府部门处以的罚款、贵公司为处理此事支出的合理费用(如差旅费、律师费)以及车辆被责令停运期间的租金损失(按《商用车租赁合同》(下称“主合同”)约定标准计算)。

5.我司知悉,若因连续违规被主管部门通报3次及以上,导致贵公司营运证被暂扣或注销等严重后果的,自愿承担全部赔偿责任(包括但不限于车辆价值贬损、营运损失等)。同时,贵公司有权单方解除《商用车租赁合同》,且有权在5年内拒绝与我司及关联方开展任何商业合作。

6.本承诺书自签订之日起生效,有效期至《商用车租赁合同》约定租赁期满之日止。

7.本承诺书作为《商用车租赁合同》的有效附件,与主合同具有同等法律效力。如本承诺书内容与主合同内容约定不一致,则以更严格约束我司义务的条款为准。

(以下无正文)


承诺客户(盖章):__________


法定代表人/授权代表人(签字):__________


签订日期: 年 月 日


附件6:车辆使用规范和驾驶员要求

一、车辆使用规范




启动前

的操作

1.确认电源总开关处于开启状态。

2.检查储氢瓶的气密性,是否变形。

3.确认驻车制动操纵手柄处于驻车状态,确认变速操纵杆处于空挡位置。

4.车辆起步前,观察仪表有无异常显示和报警:剩余氢气量(SOC)不低于10%或者氢气压力不低于3.5兆帕,且动力电池电压显示不低于520V,低压蓄电池显示于22V以上,原地转动方向盘检查转向泵工作是否正常若仪表有指示灯亮起或声音报警,应停车检查。

5.系好安全带,检查安全措施。




车辆起步

1.先将钥匙转动至“ACC”位置,踩住刹车再继续转动至“START”,停留3秒。

2.仪表盘会显示READY等字样。

3.在观察电机运转正常,各仪表、指示灯、无警报灯正常的情况下方可起步。

4.仪表显示“ready”,踩下制动踏板,挂前进挡时,换挡杆切换至“D”,挂倒挡时,换挡杆切换至“R”。

5.放下驻车制动操作手柄,缓慢踩踏加速踏板,电机启动,车辆开始行驶。


停车动作


1.应先放松加速踏板,降低车速。

2.必须在车辆完全停稳后踩住刹车才能换入倒档,否则传动系统将受到损坏。

3.怠速运转夏天5分钟、冬天10分钟时间,关闭电源,停车时长超过2小时需要关闭总电源。

4.关闭用氢系统开关。


起步

注意事项

1.观察左右反光镜,注意车辆行人。

2.两次起动车辆的间隔时间不少于30秒。

3.驾驶室倾翻起来的情况下,禁止启动电机。(一体式车头除外)

4.严禁不松手刹的情况下强行行驶。

5.起步和行车途中手刹处于施加状态电机功率将会降低50%。





汽车行驶

注意事项

1.汽车行驶中,注意使电机冷却液温度保持在正常状态。

2.严禁行驶途中关闭氢能源开关。到加氢站加氢时,提前5分钟关闭用氢开关。

3.行驶中注意观察电池电量(SOC),如果低于设定值50%以下,停车检查是否打开用氢开关,再报售后维修。

4.遇水后请注意特别驾驶,制动器受潮会暂时降低制动力。并缓慢通过。

5.车辆倒车速度不超过15公里/时。

6.紧急刹车制动距离较长且急速刹车时车辆会向左倾斜,应注意车速(最佳车速在40—50),且必要时预见性减速。

7.长时间或下雨天停车要关闭整车总闸,避免损耗低压电或线路故障。

8.行车途中若听见不正常的声音或嗅到不正常的味道应及时停车找出原因。

9.所有高压电源均由点火开关控制,驾驶员离开车辆时必须关掉点火开关,取走钥匙。

10驾驶过程中严禁吸烟(包括但不限于驾驶室、密封空间、距离车辆【10】米范围内)。

11.注意观察冷机的温度是否在正常范围之内。

加氢流程

提前5分钟关闭用氢开关—车辆停在指定位置--人员离开到安全位置等待--加氢进行--加氢完毕关闭加氢口--纯电启动驶离加氢站--打开用氢开关。

加氢注意事项

1.氢的剩余量不能低于SOC10%。气压不能低于3.5兆帕。

2.加氢时严禁在加氢站点燃明火,或者吸烟。

3.要在听从加氢站工作人员的指导下开到指定位置,关闭电源,停车,由工作人员代驾。



4.禁止个人加氢,避免储氢瓶氢气泄漏,加氢需到加氢站。

5.不要携带手机、打火机、MP3等易引起爆炸、自燃的物品进入氢化工作区域。

6.在加氢站附近,拿工具、零件时应轻拿轻放,不得发生刚性撞击产生火花。








特别注意

1.不能急加速,匀速行驶。

2.车辆停车后,要等空压机吹扫结束再关闭电源(建议停车前10分钟关闭EV开关)。

3.电池电量不可低于25%。低于25%时,整车报警,低于15%时,整车无法上高压,将无法启动,应提前外接充电或开启混动模式充电。

4.氢气余量不可低于10%。

5.当车辆soc电量低于50%且未充电,则要确认是否在用氢状态,或靠边确认故障。

6.仪表台中部“EV”按钮为运行模式切换,按下后车辆由混合动力转换为纯动,再按则转换回混合动力,正常无需对此按钮进行操作。

7.加氢时关闭EV开关,使用电动进站加氢,离开加氢站再启动氢(EV开关)。

8.车辆为自动换挡,正常行驶中无需换挡操作,且车辆为电驱动系统,滑行时不须切换为空挡,切换空挡会导致车辆失去油门响应;如需换挡时,应踩刹车进行换挡,切换挡位时须先切换为空挡,并在空挡停留0.5s~1s。

9.开车前检查水箱防冻液是否缺失,过久未使用车辆需更换防冻液。

10.不要随意开启氢罐两侧的门,更不能随意关闭氢管路的阀门。

11.不要频繁关闭开启氢开关,停车20分钟左右的不需关闭。

12.非紧急情况下禁止碰高压急停开关,紧急情况下使用(如事故,涉水,起火)。

13.不能用水枪冲洗橙色线路,避免进水报绝缘和漏电。

14.车辆为电动车辆,因此涉水高度最高为300mm,如积水超过500mm严禁通行,且通过积水路段时应注意减速慢行。

15.停机注意:首先关闭所有用电设备(如空调)钥匙关一档(待燃料电池完全停止工作—再可关二档—最后方可关闭总电源开关)包括重新启动按以上要求操作。


二、车辆操作提醒

(一)安全/充电事项

1.高压安全:高压橙色线束及其连接件,不能随意触碰,有高压触电风险。

2.氢气安全:若有泄漏,立即现场隔离。 严禁人、车入内。供氢系统发生火灾:若条件允许,车辆移动空旷区域,人员撤离;若条件允许,可用淋水对氢瓶降温。

3.充电事项:充电时,需选择电压大于500V及辅助电压为24V的充电桩。尽量避免人为使用EV模式驾驶,减少充电次数。

3.加氢事项:行车前,合理规划路线,关注氢剩余量,及时补充。

4.氢堆功率待机条件:混动模式下氢堆功率输出条件

SOC在85%以上时氢堆自动进入待机状态,直到SOC低于65%时自动开始启动;然后电池开始充电直到SOC在85%以上又进入待机状态,依次循环。

注意:氢堆待机时,如果驾驶员要求功率大于电池放电功率限制值,氢堆也会自动启动输出功率。

5.功率限制

限制条件

限制措施

限制现象

纯EV模式

SOC≤30%

输出功率限制

空调停止使用,电池电量报警灯点亮/SOC进度条显示黄色

SOC≤20%

车辆限速≤52km/h/电池低电量警告灯+闪烁/SOC进度条显示红色

SOC≤10%

车辆限速≤17km/h/电池低电量警告灯+闪烁/整车故障灯点亮/SOC进度条显示红色,车辆限输出扭矩

(二)主要故障灯处理

1.氢燃料电池冷却液报警指示灯:停车检查,检查燃料电池冷却液液面状态,联系羚牛运维服务人员检查冷却液是否泄漏或其他原因。

2.氢燃料电池堆报警灯:氢燃料电池控制系统某个运行数据异常,切换E V模式、联系羚牛运维服务人员处理。

3.氢气泄漏报警灯:立即切换为EV模式,立即寻找远离火源空旷通风区域停车,关闭低压电源总开关,联系羚牛运维服务人员处理。

4.供氢系统报警灯:车辆可能会出现限制功率输出;切换EV模式、联系羚牛运维服务人员处理。

5.按指示灯颜色判断:绿灯正常运行-黄灯检查警告-红灯停车检查。

三)驾驶模式切换-混动/EV模式

EV模式:按下EV开关键,仪表指示灯亮,车辆执行EV纯电模式运行。

混动模式:EV开关键关闭下,车辆启动,系统默认为混动模式。

注意:为延长动力电池寿命,纯EV模式仅允许在紧急情况下使用,严禁长期使用; EV模式可以在任何条件下切换到混动模式;混动模式只能在停车后才可切换为EV模式。

(四)能量回收功能

1.开启方法:拨杆在最上位置时为1档,拨动开关到最下位置时为2档。 (车辆默认挡位为1档)

2.启用条件:SOC<95%、车速>15Km/h、未踩加速踏板。

3.解除条件:踩下加速踏板或 SOC>90%或车速<15Km/h。

4.当道路平坦需长距离滑行或坡度较大需要较大制动力时,下拨能量回收开关到2档。

注意:合理使用能量回收,能有效减少制动系统磨损和降低氢耗;能量回收不能代替行车制动。

(五)ECO模式

开启方法:按下ECO模式开关,仪表ECO指示灯点亮,车辆进入节能模式运行;否则为正常运行模式。

注意:重载时使用正常模式(ECO未开启);道路平坦,空车或者轻载时,打开ECO开关,使用

节能模式;合理使用能量回收和ECO功能,可以提高续航里程。

(六)车辆停放

车辆停车后,将钥匙置于OFF档,等待3分钟,待氢堆完成自洁后夏天5分钟和冬天10分钟,再关闭低压电源总开关;停车区尽量选择空旷非封闭空间且应远离火源;为保护低压蓄电池性能,长时间停车(超过2个小时),强烈建议关闭低压电源总开关。

(七)提升续航驾驶

1.不要急加、减速:急加、减速能量损耗大,严重降低续航。

2.经济车速40-70KM/H:车速保持在此阶段匀速行驶 ,能有效地提高车辆续航里程。

3.能量回收和ECO模式使用:能量回收模式可回收多余动能提高续航,合理使用ECO模式,降低动力输出提高续航。

注意:合理使用空调,避免夏季过冷,冬季过热,导致能耗增加。

(八)AVAS-低速报警

1.开启条件:根据车速自动激活;D挡时:车速在1-20km/h时,发出“嗡嗡”报警声,车速高于20km/h时,报警自动取消;R档时,发出倒车音提示音。

2.作用:车辆启动行驶时,车辆噪声较小,为确保行车安全,强烈建议开启低速报警功能,提醒行人注意安全。

(九)定速巡航

1. 开启方法:按下定速巡航开关按键,通过方向盘设定速度。

2.启用条件:车辆无故障、40km/h≤车速≤90km/h。

3.解除条件:关闭巡航开关、踩下制动踏板、车速>90km/h、车速<40km/h。

注意:用定速巡航开关以外的方式解除定速巡航,巡航控制系统仍处于待命状态。 按下“SET/-”时,车辆会自动恢复到最近设定的巡航速度。

(十)故障处理流程

1.若车辆出现故障时,请及时联系羚牛运维服务人员。

2.若车辆故障时,为快速及有效地判断故障原因,请驾驶员记录以下信息,反馈给现代商用车服务人员。需提供车辆信息如下:车辆铭牌、故障码内容照片、仪表整体显示照片。

3.故障现象描述:车辆在行驶过程中或启动时,仪表显示故障灯,车辆有什么故障现象;车辆在使用过程中,车辆什么功能无法使用;尽可能拍摄故障时照片或视频,便于快速故障原因判断;

(十一)参数/故障查询方法:

1.车辆运行参数查询:按下方向盘上“菜单键”,进入“设置”界面,按“选择键”选择“信息栏目”项后,再按“确认键”,可读取相关系统信息。

2.车辆故障内容查询:按下方向盘“菜单键”,进入“故障诊断”界面,按“选择键”选择“故障内容”项后,再按“确认键”,可读取故障码内容。


三、驾驶员安全管理要求:

1.乙方驾驶员必须遵守《中华人民共和国道路交通安全法》《中华人民共和国道路运输条例》及有关交通安全管理的规章规则,安全驾车,并应遵守其他相关的规章制度。

2. 乙方与驾驶员须签订聘书,聘用条件为:

⑴须持有合法的驾驶执照,对于租赁车辆的同车型实际驾龄必须超过2年;

⑵年龄不超过55周岁;

⑶取得道路运输管理部门颁发的从业资格证;

⑷经乙方组织对其进行面试、理论和机械常识考核、道路驾驶技术测试合格。

3.驾驶员的管理、宣传、教育。

(1)驾驶员应爱惜甲方车辆,平时要注意车辆的保养,经常检查车辆的主要机件。每月至少用半天时间对自己所开车辆进行检修,确保车辆,以保持车辆的清洁。驾驶员发现车辆有故障时要立即检修。不会检修的,应立即报告管理人员,并提出具体的维修意见(包括维修项目和大致需要的经费等)。未经批准,不许私自将车辆送厂维修;

(2)驾驶员对自己所开车辆的各种证件的有效性应经常检查,出车时一定保证证件齐全。连续行车里程超过四小时的,须配备两名驾驶员,每位驾驶员连续驾驶时间不得超过四小时;

(3)乙方应对本单位每位驾驶员进行岗前安全教育培训、考核,考核不合格的,不得上车驾驶。并保存驾驶员安全培训记录;

(4)对违章次数多、出险率高、发生交通事故的驾驶员进行再教育培训或依据公司制度进行惩罚。

4.承租人在收到违章信息通知后,从通知日期后15天内处理完毕,防止积累。(北京要求10个工作日内处理)。

5.承租人如果发生违章,将会收到短信,微信或者书面形式通知,并会有出租人或售后人员致电确认。

6.如若自身不便处理违章,请在发生违章之日起 10日内 联系出租人或售后人员并说明情况,由出租人代为处理,产生的一切费用由客户自行承担。在收到以下违章信息的情况下,必须在得到通知后的10天内必须处理完毕:

(1)单辆车累计扣分6分以上或罚款800元以上;

(2)单辆车未处理违章3条以上;

(3)因年审要求,在注册日期当月最后截止日期前10个工作日处理掉所有违章。不处理的,出租方将安排代为加急处理,产生的费用由承租方承担。

7.如收到违章信息超出规定时间拒不处理的,出租人将预警发函或进行远程控制(如锁车)措施处理。

8.对半年内单辆车出现违章超过10次(北京在季度内单辆车不能超过两次违章,并且不能出现一次扣6分的违章)、发生交通事故负有责任4次以上或者重大交通事故一次的情况下,出租人将会做出以下应对方案:

(1)承租人必须更换司机;

(2)拒不整改配合,则视承租人为违约做收车处理。

9.欢迎您对我们的服务进行监督并提出宝贵意见。我公司的监督投诉电话:【 400-021-1773】。




接收方(签字/盖章):

时间: 年 月 日





授权委托书

致:{{lessorName}}

兹委托受托人1 姓名【】联系方式【】证件号码【】代表委托人处理委托人与办理Ρ签署《商用车租赁合同》(合同编号:)及前述合同相关包括但不限于《租赁订单》等合同附件、Ρ提车、Ρ退车、Ρ更换车、≤ 事宜,授权期限自本授权委托书签署之日起至《商用车租赁合同》终止之日止。

兹委托受托人2 姓名【】联系方式【】证件号码【 】代表委托人处理委托人与办理≤签署《商用车租赁合同》(合同编号:)及前述合同相关包括但不限于《租赁订单》等合同附件、≤提车、≤退车、≤更换车、≤ 合同抬头变更事宜,授权期限自本授权委托书签署之日起至《商用车租赁合同》终止之日止。

上列受托人的代理权限为特别授权,即受托人有权代委托人签署《商用车租赁合同》及前述合同相关包括但不限于《租赁订单》等合同附件、有权代为履行前述合同及附件内容、有权代为签收、确认前述合同及其附件相关通知、函件、诉讼文件等。委托人对上列受托人在授权范围内的行为承担全部法律责任。

本授权委托书手填部分有效,委托人特此声明及确认。




委托人(签字/盖章):


年 月 日




安全责任协议


甲方(出租方):

经办人:

联系电话:


乙方(承租方):

统一社会信用代码/身份证号:

通信地址:

经办人:

联系电话:


为明确双方车辆租赁期间安全管理责任,保障运输作业安全,依据相关法律法规,在已签订《车辆租赁合同》同时,签订本协议,约定租赁车辆安全运营及管理责任。

一、协议目的

1.1本协议明确双方合作期间安全责任,确保企业主体责任履行,保障人员、车辆和货物安全,避免事故发生。

1.2本协议有效期与《车辆租赁合同》合同期限保持一致,租赁期间内乙方承担全部车辆相关的安全责任。

二、安全责任划分

2.1车辆交接时,乙方确认车辆状态,在交接单签字即视为认可车辆状况,甲方完成合格交付。自交付之日起,车辆运营安全责任及维修保养责任转移至乙方。

三、乙方安全承诺

乙方结合车辆安全和加氢安全实际,就安全生产做出承诺并制定规程、制度、预案等。

3.1安全管理制度

3.1.1乙方建立健全本单位安全生产责任制,成立安全生产管理机构、依据规定配备专职或兼职安全管理人员,明确各部门、各岗位安全职责。

3.1.2乙方制定本单位生产安全事故隐患排查治理制度,明确各层级隐患排查职责,及时消除安全隐患。

3.1.3乙方制定本单位交通事故应急救援预案,明确应急组织机构及职责,确定应急处置流程、储备相应应急物资。

3.1.4乙方制定本单位驾驶员安全培训制度,落实驾驶员岗前安全培训、考核工作,并保存驾驶员培训记录。

3.1.5乙方制定并落实安全生产、交通运输等法律规定的其他安全管理制度。

3.1.6乙方负责按照甲方的要求提供企业营业执照及必要资质、运营证件及个人驾驶证、身份证供甲方审查核验。

3.1.7甲方有权对每一位租赁车辆的驾驶员证件信息、有效期资质进行审核。

3.2 事故隐患排查治理

3.2.1乙方每月至少进行一次全面安全检查,重点检查安全管理制度是否完善,是否定期组织召开安全会议,是否对每位驾驶员开展岗前安全培训,是否定期开展隐患排查及隐患限期整改落实,是否至少每半年开展一次应急演练等安全事项。发现存在安全隐患立即通知整改,并立即抓好落实,及时消除。

3.2.2乙方做好出车前、停车后的准备、检查工作,确保行车安全,发现隐患要及时修复后方可出车。

3.2.3乙方装货时严查超载和擅自装载危险品,确认无误后方可出车。

3.2.4乙方不定期检查车辆的安全装置、灯光信号、证件。

3.2.5乙方检查驾驶员是否带病、疲劳、酒后驾驶,是否违反安全驾驶操作规程。

3.2.6乙方建立驾驶员安全考核奖惩制度,针对违章次数多、出险率较高的司机进行再教育或依制度进行处罚。

3.3 驾驶员管理

3.3.1乙方驾驶员必须遵守《中华人民共和国道路交通法》《中华人民共和国道路运输条例》及有关交通安全管理的规章规则,安全驾车,并应遵守其他相关的规章制度。

3.3.2 乙方与驾驶员签订聘书,聘用条件为:

(1)驾驶员须持有合法驾照;

(2)年龄不超过60周岁;

(3)驾驶4.5T以上车型须有从业资格证,且经乙方面试、理论和机械常识考核、道路驾驶技术测试合格。

3.3.3乙方应对驾驶员进行岗前及定期安全教育培训、考核,考核不及格的,不得上车驾驶,保存培训材料,对违章次数多、出险率高或发生交通事故的驾驶员进行再教育或依据公司制度进行处罚。

3.3.4 乙方应当对驾驶员进行安全生产教育和培训,保证驾驶员具备必要的安全生产知识,熟悉道路交通安全规章规则,掌握行车安全操作技能,了解事故应急处理措施,知悉自身在交通安全方面的权利和义务。未经安全教育和培训合格的驾驶员,乙方不得让其上岗作业。

3.3.5 乙方必须依法参加工伤保险,为驾驶员缴纳保险费。

3.4车辆安全管理要求

3.4.1车辆发生故障必须及时修复,特别是危及行车安全的重大隐患,如制动、转向、轮胎、灯光装置等。严禁车辆带病出场。

3.4.2严禁超员、超载行驶,严禁客货混装,严禁超速行驶。

3.4.3车辆在运行途中,驾驶员应主动做好车辆的自检自查,发现问题及时解决。

3.4.4车辆周围10米内严禁抽烟,严禁明火。进入加氢站加氢时按操作流程执行。

3.4.5车辆必须按规定位置停放,严禁停放在密封场所、低洼积水区域。车辆停放不得占据消防通道或妨碍消防通道畅通,不得将车辆停放在有人员居住的建筑物内,不得将车辆停放在可能造成人员伤亡的坡道路段,应确保车辆与爆破、吊装、起重、动火、高温等危险作业场所保持足够的安全距离。乙方违反本条事项要求所造成的人员伤亡、财产损失,均由乙方自行承担。

3.5车辆技术管理要求

3.5.1乙方应根据甲方保养要求及时对车辆组织保养维修,并及时将保养维修、车辆肇事情况通知甲方。

3.5.2车辆重要部位改动或加装部件,乙方应征得甲方同意并报甲方登记备案。

3.5.3甲方运维部管理人员检查车辆上路行驶情况时,在告知其身份后,司机必须停车配合,如发现车辆状况不佳或违反甲方安全管理规定的,甲方管理人员有对车辆进行停驶、回场检查的权利。

3.5.4 乙方要配合甲方对车辆进行年审、二级维护、检测、事故违章处理等工作。

3.6突发事件应急处置

为减少、避免事故的发生,把事故的损失降到最低程度,乙方及乙方驾驶员应做到:

3.6.1交通事故

驾驶员在车辆运行中发生交通事故时,停车后马上下车关闭车辆总电源,查看事故情况及是否有人员受伤,并将事故碰撞现场进行拍照留存,在车辆后方100米处放置警示标志。

发生交通事故,当事人应立即进行自救,并报警。电话: 122(交警)、119(消防)、120(急救),应简明讲清事故地点、伤亡、损失等情况,以及事故对周围环境的危害程度,保护现场,抢救伤员,保护货物财产并上报公司领导及甲方管理部门。

事故如果导致氢气压力异常和氢气管路有“嗤嗤声”等异常现象,发现车辆有漏气情况时,驾驶员要第一时间通知公司领导或甲方相关人员告知事故情况,甲方安排专业售后人员进行应急处理。

远离人群,事故车的处理地点应尽量避免在人口密集区,如只能在原地进行处理,应在周围设置提示牌。

安全撤离,当车辆发生明火现象时,驾驶人员应停车并关闭车辆总电源,并采取紧急灭火措施,灭火器应使用二氧化碳灭火器,对着火点进行喷射。如遇到无法扑灭时,立即疏散周围人员及车辆并撤离到安全地带拨打119报警,等待救援。

3.6.2车辆氢气泄漏

(1)驾驶员在车辆运行中发现氢气泄漏报警情况时,应就近安全区域停车。

(2)停车后马上关闭车辆总电源、在车辆后方100米处放置警示标志,并疏散周围人员车辆。

(3)驾驶员将情况通知公司领导或甲方相关人员告知,甲方安排专业售后人员进行应急处理。

3.6.3车辆运行中发生整车故障

驾驶员驾驶车辆行驶中发生整车故障报警情况时,应在安全地带停车。

停车后关闭车辆总电源,并电话联系甲方,甲方收到信息后安排售后人员电话指导驾驶员进行处理。如电话无法处理时,安排厂家前往现场进行处理或拖至服务站处理。

3.6.4火灾事故

(1)发生车辆火灾事故或出现火险苗头时,驾驶员应立即熄火有条件地迅速将车转移至安全地点,切断电源,关闭气阀。

(2)使用二氧化碳灭火器对着火点进行喷射。

(3)火灾现场在进行初期扑救的同时或发现火势较大,估计自身无法扑灭的,并撤离到安全地带,疏散周围人员及车辆。迅速拨打火警电话报警,并通知公司领导及甲方管理部门。

(4)如有受伤人员,必须首先将伤员抢救出来,及时送往附近医院进行救治。

(5)公安消防人员到达现场后,应急救援人员应服从专业消防人员的指挥,在消防人员的统一组织下,积极参加火灾事故的救援工作。

3.7 禁止第三方使用

3.7.1 严格禁止乙方擅自将甲方车辆转租、分租、出借或以联营、合作、借用等形式变相交由第三方使用。如有违反,甲方有权立即解除合同、收回车辆,并要求乙方支付相当于三个月租金的违约金,且不影响其他损失赔偿权利。


四、甲方安全管理责任和权利

4.1 甲方有权审查乙方拟租赁车型是否在乙方经营许可范围之内,审查所交车型是否与乙方提车人驾驶证准驾车型匹配,驾驶证、从业资格证是否在有效期内。

4.2甲方负责按照《车辆可交付标准》对乙方拟租赁车辆进行整备,确保车辆性能完好、安全附件齐备。

4.3甲方负责指引乙方按照“交车单”内容项进行实物及车况确认,无误后甲乙双方在交车单上签字。

4.4甲方负责通知乙方所承租车辆的保养、年审、违法处理等信息。

4.5乙方在履行安全管理责任时,若需要甲方协助的,甲方在自身能力范围内且在不影响自身正常运营的情况下,对乙方提供必要的技术指导和信息支持等协助。

4.6乙方承诺自愿按照“乙方安全承诺”的规范内容进行操作,对于违反“乙方安全承诺”内容的操作行为,乙方应向甲方支付相当于三个月租金的违约金;若该违约金不足以弥补甲方损失的,乙方还应赔偿甲方因此遭受的全部损失。

4.7 尽管有上述规定,甲方是否行使甲方的安全管理责任和权利以及如何行使甲方的安全管理责任和权利,无论如何均不影响乙方根据本协议及“乙方安全承诺”履行其安全管理责任,也不减免或减轻其任何安全管理责任。

五、其他

5.1 本协议的任何变更或补充需经双方书面协商一致,并签订相关协议变更或补充文件。

5.2 本协议为《车辆租赁合同》的附件,本协议未约定的,以《车辆租赁合同》约定为准;若本协议约定与《车辆租赁合同》约定不一致的,以本协议约定为准。

5.3 本协议经双方签字(盖章)之日起生效,一式两份,甲乙双方各执一份,具有同等法律效力。



甲方(签章):


乙方(签字/盖章):

授权代表:


授权代表:

签订日期:

年 月 日

签订日期: 年 月 日













附:承租方(乙方)营业执照、法定代表人身份证复印件(若有授权书,需附被授权人身份证复印件)、道路运输经营许可证等。


V26.5





PAGE \\* MERGEFORMAT 1




`; +// END AUTO-GENERATED V26.5 + +var DEFAULT_LESSOR_COMPANIES = [ + { + id: 'jx', shortName: '嘉兴羚牛', enabled: true, + legalName: '浙江羚牛氢能科技有限公司', + creditCode: '91330481MA2JXXXXX', + address: '浙江省嘉兴市平湖市乍浦镇杭州湾大道1号', + contact: '法务部', phone: '0573-88888888', email: 'legal@lingniu-jx.com', + accountName: '浙江羚牛氢能科技有限公司', + bankName: '招商银行嘉兴分行', + bankAccount: '120924165110301', + hotline: '400-021-1773' + }, + { + id: 'sh', shortName: '上海羚牛', enabled: true, + legalName: '羚牛氢能科技(上海)有限公司', + creditCode: '91310115MA1HXXXXX', + address: '上海市浦东新区张江高科技园区科苑路88号', + contact: '法务部', phone: '021-88888888', email: 'legal@lingniu-sh.com', + accountName: '羚牛氢能科技(上海)有限公司', + bankName: '招商银行上海张江支行', + bankAccount: '120924165110401', + hotline: '400-021-1773' + }, + { + id: 'gd', shortName: '广东羚牛', enabled: true, + legalName: '羚牛氢能科技(广东)有限公司', + creditCode: '91440101MA5CXXXXX', + address: '广东省广州市黄埔区科学城开源大道11号', + contact: '法务部', phone: '020-88888888', email: 'legal@lingniu-gd.com', + accountName: '羚牛氢能科技(广东)有限公司', + bankName: '招商银行广州萝岗支行', + bankAccount: '120924165110201', + hotline: '400-021-1773' + } +]; +var LESSOR_TEMPLATE_VARS = [ + { key: 'lessorName', label: '甲方全称' }, + { key: 'lessorAddress', label: '甲方地址' }, + { key: 'lessorContact', label: '甲方联系人' }, + { key: 'lessorPhone', label: '甲方电话' }, + { key: 'lessorEmail', label: '甲方邮箱' }, + { key: 'lessorAccountName', label: '收款户名' }, + { key: 'lessorBankName', label: '开户行' }, + { key: 'lessorBankAccount', label: '银行账号' } +]; +var CONTRACT_TEMPLATE_VAR_GROUPS = [ + { + title: '甲方签约主体', + desc: '创建合同时按所选签约公司自动替换', + vars: LESSOR_TEMPLATE_VARS + }, + { + title: '合同通用', + desc: '创建合同时由系统或客服填写', + vars: [ + { key: 'contractCode', label: '合同编号' }, + { key: 'customerName', label: '乙方公司全称' }, + { key: 'creditCode', label: '乙方统一社会信用代码' }, + { key: 'customerAddress', label: '乙方地址' }, + { key: 'customerContact', label: '乙方联系人' }, + { key: 'customerPhone', label: '乙方联系电话' }, + { key: 'signDateYear', label: '签订年份' }, + { key: 'signDateMonth', label: '签订月份' }, + { key: 'signDateDay', label: '签订日期' } + ] + } +]; +var DEFAULT_LESSOR_HOTLINE = '400-021-1773'; +if (typeof window !== 'undefined') { + if (!window.ONEOS_LESSOR_COMPANIES || !window.ONEOS_LESSOR_COMPANIES.length) { + window.ONEOS_LESSOR_COMPANIES = DEFAULT_LESSOR_COMPANIES; + } + window.ONEOS_LESSOR_TEMPLATE_VARS = LESSOR_TEMPLATE_VARS; + window.ONEOS_CONTRACT_TEMPLATE_VAR_GROUPS = CONTRACT_TEMPLATE_VAR_GROUPS; + window.ONEOS_CONTRACT_CLAUSE_MARKERS = { + lockSelector: '.ct-inline-lock[data-inline-lock="1"]', + riskRedlineSelector: '.ct-risk-redline[data-risk-redline="1"]', + nonstdTriggerAttr: 'data-nonstd-trigger', + nonstdApprovalType: '非标准合同审批' + }; +} + +// 【重要】必须使用 const Component 作为组件变量名 +// ONE-OS 合同模板管理(法务人员维护 Word 版合同基础模板) +// 设计规范:docs/ONE-OS-DESIGN-SPEC.md · 样式:web端/styles/contract-template.css + +const Component = function () { + var ONEOS_ANT_TABLE_GLOBAL_FIX = [ + '.ant-table-container .ant-table-header { margin-bottom: 0 !important; }', + '.ant-table-container .ant-table-body { margin-top: 0 !important; }', + '.ant-table-container .ant-table-body > table, .ant-table-content table { margin-top: 0 !important; }', + '.ant-table-tbody > tr.ant-table-measure-row, .ant-table-tbody > tr.ant-table-measure-row > td, .ant-table-tbody > tr.ant-table-measure-row > th { display: none !important; height: 0 !important; max-height: 0 !important; min-height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; line-height: 0 !important; font-size: 0 !important; overflow: hidden !important; visibility: hidden !important; pointer-events: none !important; }' + ]; + + var useState = React.useState; + var useMemo = React.useMemo; + var useRef = React.useRef; + var useEffect = React.useEffect; + var antd = window.antd; + var App = antd.App; + var Card = antd.Card; + var Table = antd.Table; + var Button = antd.Button; + var Tabs = antd.Tabs; + var Select = antd.Select; + var Input = antd.Input; + var Space = antd.Space; + var Tag = antd.Tag; + var Modal = antd.Modal; + var Row = antd.Row; + var Col = antd.Col; + var Upload = antd.Upload; + var Tooltip = antd.Tooltip; + var Popover = antd.Popover; + var Divider = antd.Divider; + var Dropdown = antd.Dropdown; + var message = antd.message; + + var FONT_SIZE_OPTIONS = [ + { value: '12px', label: '12px' }, + { value: '14px', label: '14px' }, + { value: '16px', label: '16px' }, + { value: '18px', label: '18px' }, + { value: '20px', label: '20px' }, + { value: '24px', label: '24px' }, + { value: '28px', label: '28px' }, + { value: '32px', label: '32px' } + ]; + + var FONT_FAMILY_OPTIONS = [ + { value: 'Microsoft YaHei', label: '微软雅黑' }, + { value: 'SimSun', label: '宋体' }, + { value: 'SimHei', label: '黑体' }, + { value: 'KaiTi', label: '楷体' }, + { value: 'FangSong', label: '仿宋' }, + { value: 'Arial', label: 'Arial' }, + { value: 'Times New Roman', label: 'Times' } + ]; + + var COLOR_OPTIONS = [ + { value: '#000000', label: '■ 黑' }, + { value: '#dc2626', label: '■ 红' }, + { value: '#2563eb', label: '■ 蓝' }, + { value: '#16a34a', label: '■ 绿' }, + { value: '#ca8a04', label: '■ 金' }, + { value: '#64748b', label: '■ 灰' } + ]; + + var BG_COLOR_OPTIONS = [ + { value: '#fef08a', label: '■ 黄' }, + { value: '#bbf7d0', label: '■ 绿' }, + { value: '#bfdbfe', label: '■ 蓝' }, + { value: '#fecaca', label: '■ 红' }, + { value: '#f1f5f9', label: '■ 灰' }, + { value: '#ffffff', label: '■ 无' } + ]; + + var LINE_HEIGHT_OPTIONS = [ + { value: '1', label: '1.0 倍' }, + { value: '1.25', label: '1.25 倍' }, + { value: '1.5', label: '1.5 倍' }, + { value: '1.75', label: '1.75 倍' }, + { value: '2', label: '2.0 倍' }, + { value: '2.5', label: '2.5 倍' } + ]; + + var PARAGRAPH_STYLE_OPTIONS = [ + { value: 'p', label: '正文' }, + { value: 'h1', label: '标题 1' }, + { value: 'h2', label: '标题 2' }, + { value: 'h3', label: '标题 3' }, + { value: 'h4', label: '标题 4' }, + { value: 'blockquote', label: '引用块' }, + { value: 'pre', label: '预格式/代码' } + ]; + + var SPECIAL_CHAR_ITEMS = [ + { key: 'sq', label: '方框 □', char: '□' }, + { key: 'ck', label: '对勾 √', char: '√' }, + { key: 'cc', label: '版权 ©', char: '©' }, + { key: 'reg', label: '注册 ®', char: '®' }, + { key: 'yen', label: '人民币 ¥', char: '¥' }, + { key: 'deg', label: '度数 °', char: '°' }, + { key: 'mul', label: '乘号 ×', char: '×' }, + { key: 'div', label: '除号 ÷', char: '÷' }, + { key: 'ell', label: '省略号 …', char: '…' }, + { key: 'dash', label: '破折号 —', char: '—' }, + { key: 'middot', label: '间隔号 ·', char: '·' } + ]; + + var TABLE_PRESET_ITEMS = [ + { key: '2x2', label: '2 × 2', rows: 2, cols: 2 }, + { key: '3x3', label: '3 × 3', rows: 3, cols: 3 }, + { key: '4x4', label: '4 × 4', rows: 4, cols: 4 }, + { key: '5x4', label: '5 × 4', rows: 5, cols: 4 }, + { key: '6x5', label: '6 × 5', rows: 6, cols: 5 } + ]; + + var LETTER_SPACING_OPTIONS = [ + { value: 'normal', label: '默认间距' }, + { value: '1px', label: '1px' }, + { value: '2px', label: '2px' }, + { value: '4px', label: '4px' } + ]; + + function rteSvg(children, extraClass) { + return React.createElement('svg', { + className: 'ct-rte-svg' + (extraClass ? ' ' + extraClass : ''), + width: 16, height: 16, viewBox: '0 0 24 24', + fill: 'none', stroke: 'currentColor', strokeWidth: 1.75, + strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true + }, children); + } + + function rteLetterIcon(letter, barColor, extraClass) { + return React.createElement('span', { className: 'ct-rte-letter' + (extraClass ? ' ' + extraClass : '') }, + letter, + barColor !== false ? React.createElement('span', { + className: 'ct-rte-letter__bar', + style: barColor ? { background: barColor } : null + }) : null + ); + } + + function rteIcon(name) { + var map = { + bold: rteLetterIcon('B', false), + italic: rteLetterIcon('I', false, 'is-italic'), + underline: rteLetterIcon('U', '#ef4444'), + fontColor: rteLetterIcon('A', '#ef4444'), + highlight: React.createElement('span', { className: 'ct-rte-letter ct-rte-letter--highlight' }, 'A'), + eraser: rteSvg([ + React.createElement('path', { key: 'p', d: 'M20 20H8L4 16l8-8 8 8-4 4z' }), + React.createElement('path', { key: 'p2', d: 'M12 6l6 6' }) + ]), + image: rteSvg([ + React.createElement('rect', { key: 'r', x: '3', y: '5', width: '18', height: '14', rx: '2' }), + React.createElement('circle', { key: 'c', cx: '9', cy: '10', r: '1.5', fill: 'currentColor', stroke: 'none' }), + React.createElement('path', { key: 'p', d: 'M21 16l-5-5L5 19' }) + ]), + link: rteSvg([ + React.createElement('path', { key: 'p', d: 'M10 14a4 4 0 0 1 0-5.5L12 6a4 4 0 0 1 5.5 0' }), + React.createElement('path', { key: 'p2', d: 'M14 10a4 4 0 0 1 0 5.5L12 18a4 4 0 0 1-5.5 0' }) + ]), + table: rteSvg([ + React.createElement('rect', { key: 'r', x: '3', y: '5', width: '18', height: '14', rx: '1' }), + React.createElement('path', { key: 'p', d: 'M3 10h18M3 14h18M9 5v14M15 5v14' }) + ]), + bulb: rteSvg([ + React.createElement('path', { key: 'p', d: 'M9 18h6M10 22h4' }), + React.createElement('path', { key: 'p2', d: 'M12 2a6 6 0 0 0-3 11v1h6v-1a6 6 0 0 0-3-11z' }) + ]), + undo: rteSvg(React.createElement('path', { d: 'M9 7H5v4M5 11a7 7 0 1 0 1.5 4.5' })), + redo: rteSvg(React.createElement('path', { d: 'M15 7h4v4M19 11a7 7 0 1 1-1.5 4.5' })), + more: rteSvg([ + React.createElement('circle', { key: 'c1', cx: '6', cy: '12', r: '1.25', fill: 'currentColor', stroke: 'none' }), + React.createElement('circle', { key: 'c2', cx: '12', cy: '12', r: '1.25', fill: 'currentColor', stroke: 'none' }), + React.createElement('circle', { key: 'c3', cx: '18', cy: '12', r: '1.25', fill: 'currentColor', stroke: 'none' }) + ]), + alignLeft: rteSvg([ + React.createElement('path', { key: 'p', d: 'M4 6h16M4 10h10M4 14h16M4 18h8' }) + ]), + alignCenter: rteSvg([ + React.createElement('path', { key: 'p', d: 'M4 6h16M7 10h10M5 14h14M8 18h8' }) + ]), + alignRight: rteSvg([ + React.createElement('path', { key: 'p', d: 'M4 6h16M10 10h10M4 14h16M12 18h8' }) + ]), + alignJustify: rteSvg([ + React.createElement('path', { key: 'p', d: 'M4 6h16M4 10h16M4 14h16M4 18h16' }) + ]), + lineHeight: rteSvg([ + React.createElement('path', { key: 'p', d: 'M4 7h12M4 12h12M4 17h12' }), + React.createElement('path', { key: 'p2', d: 'M20 7v10M18 9l2-2 2 2M18 15l2 2 2-2' }) + ]), + letterSpacing: rteSvg([ + React.createElement('path', { key: 'p', d: 'M6 8v8M18 8v8M9 12h6' }), + React.createElement('path', { key: 'p2', d: 'M4 12h2M18 12h2' }) + ]), + outdent: rteSvg([ + React.createElement('path', { key: 'p', d: 'M4 6h16M4 10h10M4 14h16M4 18h8' }), + React.createElement('path', { key: 'p2', d: 'M20 8l-3 4 3 4' }) + ]), + indent: rteSvg([ + React.createElement('path', { key: 'p', d: 'M4 6h16M10 10h10M4 14h16M8 18h12' }), + React.createElement('path', { key: 'p2', d: 'M4 8l3 4-3 4' }) + ]), + listBullet: rteSvg([ + React.createElement('circle', { key: 'c1', cx: '5', cy: '7', r: '1', fill: 'currentColor', stroke: 'none' }), + React.createElement('circle', { key: 'c2', cx: '5', cy: '12', r: '1', fill: 'currentColor', stroke: 'none' }), + React.createElement('circle', { key: 'c3', cx: '5', cy: '17', r: '1', fill: 'currentColor', stroke: 'none' }), + React.createElement('path', { key: 'p', d: 'M9 7h11M9 12h11M9 17h11' }) + ]), + listOrdered: rteSvg([ + React.createElement('path', { key: 'p', d: 'M9 7h11M9 12h11M9 17h11' }), + React.createElement('path', { key: 'n1', d: 'M4 7h1v3H4V7z', fill: 'currentColor', stroke: 'none' }), + React.createElement('path', { key: 'n2', d: 'M4 11h2v1H4v2h2', fill: 'none' }), + React.createElement('path', { key: 'n3', d: 'M4 16h2c0 1-1 1.5-2 2', fill: 'none' }) + ]), + script: rteSvg([ + React.createElement('path', { key: 'p', d: 'M8 6h8M6 18l4-8h4l-2 8' }), + React.createElement('path', { key: 'p2', d: 'M16 6v4' }) + ]), + chevron: rteSvg(React.createElement('path', { d: 'M8 10l4 4 4-4' }), 'ct-rte-svg--chevron') + }; + return map[name] || null; + } + + function isHtmlContent(text) { + return /<[a-z][\s\S]*>/i.test(String(text || '')); + } + + function toEditorHtml(content) { + if (!content) return ''; + if (isHtmlContent(content)) return String(content); + return String(content) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/\n/g, '
'); + } + + function htmlToPlainText(html) { + if (!html) return ''; + if (!isHtmlContent(html)) return String(html); + var div = document.createElement('div'); + div.innerHTML = html; + return (div.textContent || div.innerText || '').trim(); + } + + function mergeBlocksToDocument(blocks) { + var list = blocks || []; + if (!list.length) return ''; + if (list.length === 1) { + var single = String(list[0].content || ''); + return isHtmlContent(single) ? single : toEditorHtml(single); + } + return list.map(function (b) { return toEditorHtml(b.content); }).join(''); + } + + function documentToBlocks(html) { + if (!html) return []; + var div = document.createElement('div'); + div.innerHTML = html; + var sections = div.querySelectorAll('.ct-section'); + if (!sections.length) { + return [{ id: 'doc', title: '合同全文', content: html, locked: false }]; + } + var blocks = []; + sections.forEach(function (sec, idx) { + var clone = sec.cloneNode(true); + var labelClone = clone.querySelector('.ct-section-label'); + if (labelClone) labelClone.parentNode.removeChild(labelClone); + blocks.push({ + id: sec.getAttribute('data-section-id') || ('s' + idx), + title: sec.getAttribute('data-section-title') || ('模块' + (idx + 1)), + locked: sec.getAttribute('data-section-locked') === '1', + content: clone.innerHTML + }); + }); + return blocks; + } + + function countLockRegions(source) { + var html = typeof source === 'string' ? source : mergeBlocksToDocument(source); + var div = document.createElement('div'); + div.innerHTML = html; + return div.querySelectorAll('.ct-inline-lock[data-inline-lock="1"]').length; + } + + function countRiskRedlineRegions(source) { + var html = typeof source === 'string' ? source : mergeBlocksToDocument(source); + var div = document.createElement('div'); + div.innerHTML = html; + return div.querySelectorAll('.ct-risk-redline[data-risk-redline="1"]').length; + } + + function stripRiskRedlinePreviewFooters(html) { + if (!html || typeof document === 'undefined') return String(html || ''); + var div = document.createElement('div'); + div.innerHTML = html; + div.querySelectorAll('.ct-risk-redline__footer').forEach(function (el) { + el.parentNode.removeChild(el); + }); + return div.innerHTML; + } + + function preparePreviewHtml(html) { + var content = String(html || ''); + if (isWordFormattedDoc(content)) content = fixWordPreviewHtml(content); + return stripRiskRedlinePreviewFooters(content); + } + + function getDocumentTitle(html) { + var div = document.createElement('div'); + div.innerHTML = html || ''; + var h1 = div.querySelector('h1'); + if (h1) return htmlToPlainText(h1.innerHTML) || '合同预览'; + var wordTitle = div.querySelector('.ct-word-title'); + if (wordTitle) return htmlToPlainText(wordTitle.innerHTML) || '合同预览'; + return htmlToPlainText(div.innerHTML).split('\n')[0].trim() || '合同预览'; + } + + function buildTableHtml(rows, cols) { + var r = rows || 3; + var c = cols || 3; + var html = ''; + for (var i = 0; i < r; i++) { + html += ''; + for (var j = 0; j < c; j++) html += ''; + html += ''; + } + return html + '
 


'; + } + + var CATEGORY_OPTIONS = [ + { value: 'lease', label: '商用车租赁合同', desc: '商用车租赁标准合同' }, + { value: 'purchase', label: '采购合同', desc: '车辆及备件采购标准合同' }, + { value: 'service', label: '服务合同', desc: '运维、加氢等服务类标准合同' } + ]; + + function v265LeaseBlocks() { + if (typeof V265_LEASE_DOCUMENT_HTML !== 'undefined' && V265_LEASE_DOCUMENT_HTML) { + return [{ id: 'doc', title: '合同全文', locked: false, content: V265_LEASE_DOCUMENT_HTML }]; + } + return [ + { id: 'b1', title: '合同标题', locked: true, content: "商用车租赁合同" }, + { id: 'b2', title: '合同编号', locked: true, content: "合同编号:【LNZLHT {{contractCode}} 】" }, + { id: 'b3', title: '甲方(出租方)', locked: true, content: "甲方(出租方):{{lessorName}} (盖章)\n地址:{{lessorAddress}}\n联系人:{{lessorContact}}\n联系电话:{{lessorPhone}}" }, + { id: 'b4', title: '乙方(承租方)', locked: false, content: "乙方(承租方):{{customerName}} (盖章)\n统一社会信用代码:{{creditCode}}\n地址:{{customerAddress}}\n联系人:{{customerContact}}\n联系电话:{{customerPhone}}" }, + { id: 'b5', title: '正文部分', locked: false, content: "

(正文部分)

本合同经双方平等自愿协商,根据《中华人民共和国民法典》及相关法规,就甲方向乙方提供氢燃料电池车租赁服务之事宜,达成如下一致条款,以资共同遵守。

第一条 租赁标的、服务内容、租期、里程

租赁标的:租赁车辆信息及租金价格、保证金标准详见《租赁订单》(附件1)。

1.2 服务内容:甲方根据乙方的需求,为乙方提供符合约定的商用车供乙方合法使用,并提供有偿非车辆质量问题导致的车辆故障、事故救援服务。

1.3 租期:以双方签订的《租赁订单》(附件1)为准。约定的租期届满,若双方对租赁订单的内容没有变更,且任何一方未提出终止合同的,租赁期限顺延,顺延的期限与本次租赁订单的期限相同。若租赁订单内容有变更,双方签署新的《租赁订单》,租金价格、租期等相关内容,以新的《租赁订单》为准,《租赁订单》未约定的内容,仍以本合同的约定为准。

1.4 里程:乙方承诺按如下约定完成公里数(请在方框里用“√”进行选择。)

1.4.1 Ρ每月完成【 】公里;

1.4.2 ≤每季度完成【 】公里。

若乙方未完成本合同中约定的里程,甲方有权将车辆强制收回,甲方不承担违约责任。

" }, + { id: 'b6', title: '租金、费用及支付', locked: false, content: "

第二条 租金、费用及支付

2.1 租金的结算及支付

2.1.1 租金、服务费先付后用,每【 】个自然月为一个付款周期,每个付款周期末月的25日向甲方支付下个周期的租金和服务费,如25日为节假日的,付款日顺延至节假日后的第一个工作日。

2.1.2 保证金和首期租金及服务费支付:①提车日为当月15日前(含15日)的,于提车前2日内支付保证金和当月剩余天数租金及服务费,当月25日支付下个付款周期的租金及服务费;②提车日为当月15日后的,于提车前2日内支付保证金和当月实际剩余天数和下个付款周期的租金及服务费。

2.1.3 提车或还车当月的租金及服务费,以当月实际租赁天数按比例计算,计算公式如下:【(月租金+月服务费)/30】*当月实际租赁天数支付(金额取数按四舍五入取整到单位“元”)。

2.1.4 租赁期间非因车辆自身质量原因造成的租赁车辆无法上路、无法使用等情况的,乙方仍应按本合同约定支付租金及服务费。包括本条2.3.1中约定的其他费用。

2.1.5租金减免条件:

2.1.5.1基础减免:当月行驶里程达到6000公里及以上的,次月可享受2000元/辆的租金减免,当月行驶里程未达6000公里的,次月不享受租金减免。

2.1.5.2里程补足与累计:次月行驶里程可用于补足上月(截至2026年5月31日之前的月份)未达6000公里的差额部分,补足后可补享上月对应的租金减免,当月行驶里程超过6000公里的部分,可累计计入次月里程。

2.1.5.3连续周期减免:若连续两个月累计行驶里程达到12000公里,该两个月均能享受租金减免。

2.1.5.4持续达标减免:若租赁期间每月行驶里程均满6000公里,可每月享受次月2000元/辆的租金减免(若租期为1年,第12个月里程达标对应的减免,在第13个月租金结算时执行)。

本租金减免政策有效期至2026年12月31日止。

2.2 氢气费(含加氢相关服务费)

2.2.1 氢气原则上由乙方自行解决。如需甲方安排加氢的,先款后货、全额预付款支付及结算。乙方提前预估当月用气总量并计算氢气费用后将氢气预付款【 】元提前【 】个工作日内转账给甲方,甲方在收到乙方预付款后安排加氢。乙方预付款不足【 】元时,乙方接到甲方通知后2个工作日内支付预付款;否则,甲方有权不予安排车辆加氢气,由此造成的一切损失由乙方承担,给甲方造成损失的,乙方还应承担赔偿责任。

2.2.3每月25日核对上月25日至本月24日的用气对账单,有异议2个工作日内提出,双方重新核对账单,账单核对无误后,对核对无误的账单书面确认。

2.2.4 氢气价格按市场价随行就市调整。

2.2.5 乙方还车时车里剩余氢气量若低于交车时的氢气量,则按照【 】元/公斤向甲方支付氢气费,若高于交车时的氢气量,乙方同意甲方不向乙方返还氢气费。

2.2.6 租赁车辆充电产生的电费及与充电相关的费用由乙方承担。租赁车辆原则上优先选择甲方经营的充电桩充电,若选择甲方经营的充电桩充电,费用结算方式为现结。

2.3 其他费用

2.3.1 租赁期间车辆产生的其他费用,包括但不限于非质量问题产生的车辆维修费、保养费、违章费用、施救费、证照补办费用、改装费、违约金、能耗费、路桥费及乙方要求增加购买其他保险的费用等,均由乙方全额另行承担。

2.4 垫付费用:本合同中若甲方垫付费用的,乙方应在甲方通知后【3】个工作日内一次性足额支付给甲方。

2.5 甲方收款账户

2.5.1乙方应将上述租金、服务费、保证金、氢气费及其他费用统一汇入甲方如下指定账户,否则视为未支付:

户 名:【羚牛氢能科技(广东)有限公司 】

开户行:【招商银行广州萝岗支行 】

账 号:【120924165110201 】

2.5.2 本合同所涉及的所有费用均通过银行转账的方式支付至甲方上述指定账户。甲方不会以任何方式允许甲方人员通过个人账户收取本合同所涉及的各项费用,若甲方人员要求将本合同项下产生的各项费用汇至其个人账户或其他账户,乙方可拨打甲方24小时服务热线:400-021-1773。

2.5.3 乙方将本合同所涉及的费用汇入甲方指定上述收款账户之外的任何账户导致的损失,由乙方自行承担,同时乙方仍须按本合同约定向甲方支付各项费用。

2.6 发票

2.6.1 乙方开票信息如下:

企业名称:

开户银行:

银行账号:

纳税人识别号:

企业地址:

企业电话:

2.6.2甲方收到乙方支付的租金和服务费后分别开具等额的增值税发票。若乙方变更开票信息,乙方需在付款周期15日前书面告知甲方,否则所产生的损失由乙方承担。若国家发布新的税收政策导致税率变化,甲方按最新政策开具发票,乙方支付的总价款不变。

2.6.3 甲方除开具租金和服务费发票外,本合同中的其他费用,甲方不向乙方开具对应的服务费发票。

2.7 乙方款项抵扣

乙方应及时足额地支付租赁费、服务费、保证金以及其他应当由乙方承担的各项费用;乙方存在任何应付未付费用时,甲方有权从保证金、应返还乙方的保险理赔款以及乙方在甲方处的其他款项中先行抵扣,不足部分,乙方应及时补足。

2.8 碳资产

本合同约定仅限于车辆使用,该车辆所产生的碳资产数据权益和碳资产收益归甲方所有,本合同报价不含减碳、碳资产管理费用等。

" }, + { id: 'b7', title: '交车与车辆管理', locked: false, content: "

第三条 交车与还车

3.1 本合同签署前,乙方需提供如下有效证据资料:包括但不限于营业执照、法定代表人身份证复印件、驾驶员驾驶证、驾驶员从业资格证等。复印件留存甲方,原件查验后退回。

3.2 租赁车辆交付时,乙方应对租赁车辆状况及附属设施、有效证件标志进行现场检验,确认无误后应在《交接车验收清单》上签字/盖章确认。提车后,非车辆质量问题产生的一切风险由乙方承担。

3.3 还车时,乙方负责将租赁车辆归还至甲方指定的还车地点。 乙方还车时,应携带提车时签订的《交接车验收清单》,经甲方检查车辆确认无误后,双方签署《交接车验收清单》,确认还车。

3.4 返还车辆除正常使用耗损外,车辆信息与《交接车验收清单》保持一致,无任何毁损,证照齐全,无未处理交通违章、交通事故等不存在未决纠纷争议可能影响甲方利益情形。否则,甲方有权拒绝接收车辆,乙方应在限期内整改,整改期内日租金按两倍日租金标准继续计算,直至车辆符合交还标准;如乙方拒绝或超过十五天仍未完成整改的,甲方有权强制收回车辆并送至修理厂进行修复或调整,因此产生的运输费、修理费等费用以及处理交通违章、事故等发生的一切费用,均由乙方全额承担。

第四条 车辆的风险与管理

4.1 车辆自交付乙方之时起,车辆发生损毁、灭失、被盗、遗失等风险的,由乙方承担。乙方应赔偿甲方全部损失,但该损失是因车辆自身质量问题导致的除外。租赁期间,因租赁车辆引发的甲方、乙方、乙方聘用人员或第三者损失的,乙方应承担全额赔偿责任。

4.2 车辆自交付乙方之时起,乙方负责车辆相关安全保管义务和日常管理,乙方应在本合同约定的使用区域内使用车辆,使用车辆时应符合国家、地方的相关法律法规、规章制度,遵守交通规则,禁止超速、超载,禁止装载易燃易爆、易腐蚀等危险物品及在车中留下刺激性气味的物品。

4.3 租赁期间,乙方装载4.2条中禁止装载的物品,或车辆发生交通事故或其他安全、意外事故,造成甲方、乙方、第三方财产损失或人身损害、车辆损失、设备损失,或导致包括但不限于有关部门的罚金或罚分、行政处罚等一切责任的,均由乙方承担全部责任。给甲方造成的损失或者导致甲方承担责任的,乙方除承担本条前述的全部责任外,还须承担甲方的间接损失、可得利益等。如导致甲方赔偿的,甲方有权向乙方追偿。

4.4 租赁车辆的车身外观、车身广告等权益由甲方享有,未经甲方书面同意,乙方不得擅自更改车身外观,增加车身广告等。否则必须恢复车辆原状,若未恢复的,根据实际发生的费用向甲方支付修复费用,此费用甲方有权从保证金中直接扣除,不足部分,另行向甲方支付。

4.5 无论乙方自身运营或聘用他人运营标的车辆,乙方承诺自身或聘用第三人具备运营车辆的全部证照,乙方对聘用人员证照负有审核义务,证照包括但不限于营业执照、组织机构代码、车辆驾驶人员驾驶证、身份证、道路运输许可证(如进行营运的)、运输人员从业资格证等,乙方对证照的真实性、有效性以及完整性负责,并就该第三人驾驶或使用标的车辆的行为所产生的一切法律责任承担无限连带责任,如导致甲方赔偿的,甲方有权向乙方追偿。在签订合同时,乙方应向甲方出示原件并提供全部证照复印件备案,如驾驶人员发生变更的,乙方应在变更前将全部证照提供给甲方备案。

4.6 未经甲方书面同意,乙方不得将租赁车辆以转让、出借、转租、合租等任何方式许可任何第三方使用,不得设立任何形式抵押、留置、质押等权利负担,不得擅自改装(安装)车辆或拆除车辆设施、设备、标记、广告等,不得拆卸、更换车辆零部件,不得将车辆的行驶证件及相关手续、车辆号牌以任何方式许可第三方使用。

第五条 车辆的维修、保养

5.1 租赁期间,乙方应按照驾驶技术规范及该车使用说明书等规定的要求使用、保养车辆,坚持“三检三查”制,确保车辆技术状况符合规范和要求。甲方有权对车辆的技术状况进行定期或不定期的检查,并根据检查情况要求乙方修复。

5.2 乙方应每天对刹车油、防冻液、刹车片、轮胎、轮胎气压、灯光等做常规检查,如发现问题,应及时到甲方指定的维修站进行维修,并及时向甲方报备维修情况。

5.3 租赁期间,租赁车辆损坏的,乙方应在甲方指定维修站对车辆进行维修、保养,将车辆修复至符合有关技术规范标准的状态,费用由乙方全额承担,并在3日内通知甲方,将维修、保养相关单据的原件交回甲方留存。如需在指定维修站以外维修、保养车辆的,应征得甲方书面同意,否则属于乙方违约,乙方应按2000元/次的标准向甲方支付违约金。

5.4 租赁期间,乙方应负责保管车辆及GPS等设施设备,进行定期或不定期的检修、检测,确保其性能可靠、使用正常。如造成车辆及设施设备损坏、过度磨损或遗失、损毁、失效的,由乙方负责照价赔偿甲方。

5.5 乙方另行购买甲方维保服务的,甲方所提供维保服务仅限车辆日常的技术指导、使用培训、易损件的维修更换服务,不含车辆维修等其他服务。

5.6 非因车辆自身质量问题产生的车辆维修费用及拖车费由乙方全额承担。费用以维修站的价格为准。

5.7 因发生车辆故障、交通事故等情形,甲乙双方对是否属于车辆质量缺陷导致、车损价值等无法达成一致意见的,双方可委托共同认可的第三方机构进行评估、鉴定。鉴定、评估前费用由双方共同平均分担,评估后鉴定结果属于甲方全部或部分责任的,鉴定、评估费由甲方全部或部分承担,甲方应向乙方支付乙方承担的全部或部分鉴定、评估费;不属于甲方责任或属于甲方部分责任的,鉴定、评估费由乙方全部或部分承担,乙方应向甲方支付甲方先行承担的全部或部分鉴定费、评估费。乙方未响应甲方办理评估、鉴定委托事宜通知的,甲方可单方委托第三方机构进行评估鉴定。

第六条 保险、事故、动态违规及违章处理

6.1 甲方负责的保险

6.1.1 若租金中已含租赁车辆的交强险及商业险(包括三者险、车损险)保险费用,乙方对甲方所投保险险种及保额没有异议。如乙方要求增加险种或提高保险额度的,乙方应在租金标准外另行向甲方支付保险费用,由甲方负责向甲方合作的保险服务商购买保险。

6.1.2 若租金中不包含租赁车辆的交强险或商业险保险费用,乙方需按照甲方要求的保险险种委托甲方代为购买,乙方应另行向甲方支付保险费用。若标的车辆保险费用已经由甲方先行购买的,乙方应将相应保险费用支付给甲方。如遇租期内续保,乙方应在甲方通知后【3】个工作日内将相应保险费用支付给甲方。

6.2 乙方负责的保险

6.2.1 汽车车上人员责任保险(驾驶员、车上其他人员)由乙方购买:乙方应为使用该车辆的驾驶员及车上其他人员购买责任保险,租赁期内所发生的与乙方驾驶员及车上其他人员相关的一切财产、人身损失等由乙方承担,若给甲方造成损失的,乙方需向甲方承担赔偿责任。

6.2.2 货物保险:乙方应自行购买货物运输相关保险,并自行承担所有管货责任。租赁期内,无论何种原因导致乙方货物毁损、灭失、污染、变质、损耗、短少等异常情况,甲方均免责。乙方投保时,亦应向保险人要求豁免甲方责任,即使货物异常是由于甲方车辆问题所致,甲方亦不承担任何赔偿责任。

6.3 事故报告

6.3.1在租赁期内,车辆发生安全事故的,乙方应在1小时内如实报告公安交通管理机关、甲方和甲方安全部及保险公司,并配合甲方处理,且到甲方指定的维修站进行维修,若乙方隐瞒事故真实情况、虚构事故情况等或未在甲方指定维修站进行维修的,需向甲方承担3000元/次/辆违约金。当需要办理保险理赔事项时,乙方应协助甲方办理保险理赔手续,因协助办理所产生的费用由乙方承担,若不配合甲方处理安全事故或者不协助办理保险理赔事项的,须向甲方承担500元/次/辆违约金。本条项下全部应付违约金,统一并入下一租金付款周期同步结算。

6.3.2 如乙方怠于履行本合同约定义务导致无法办理保险理赔或保险公司拒赔、免赔的,全部损失和法律责任由乙方承担,造成甲方或第三方损失的,乙方应予以赔偿。

6.4 出险

6.4.1 租赁期间,如车辆发生安全事故造成损失超出保险公司赔付金额的,超出部分的损失由乙方全额承担。

6.4.2 租赁期间,租赁车辆出险并办理保险理赔的,乙方应每次向甲方支付保险上浮费,18吨车型主责及以上责任为3000元/次/辆,其他责任为2000元/次/辆(驾驶员无责除外)。如乙方自车辆出险之日起 5个工作日内未向甲方支付上述保险上浮费,则甲方有权直接从车损险理赔款项中扣除对应的保险上浮费,若车损险理赔款项不足以覆盖乙方应向甲方支付的保险上浮费,差额部分乙方须于下一租金支付周期内,连同当期租金及其他应付费用一并结清。

6.4.3 乙方出险进保,应征得甲方书面同意。如事故损失总金额不满足甲方进保理赔标准的(2000元里面不允许进保,),由乙方向甲方报备后自行到甲方指定维修站维修并由乙方自行承担相应费用,无需办理保险理赔手续。如乙方在未征得甲方书面同意前提下向保险公司报案赔付的,须向甲方承担2000元/次/辆违约金,同时,甲方不向乙方支付任何保险理赔款,若保险理赔款直接打入乙方账户的,乙方需将保险理赔款返还给甲方。

6.4.4 保险上浮费和超出保险公司赔偿的部分,甲方有权从保险理赔款中先行抵扣,如保险理赔款有剩余且应支付给乙方的,在乙方不存在到期应付未付款项时,甲方在收到理赔款后 15个工作日内无息支付给乙方,如保险理赔款直接赔付给第三方的,则乙方应在甲方通知后3个工作日内支付给甲方,且甲方有权从保证金中直接抵扣。

6.4.5 单车事故进保审批​

6.4.5.1 租赁期间,因乙方违反限高、限速、限重等交通管理规定或操作规范导致的单车事故,原则上不予办理保险理赔,相关车辆维修、施救等全部费用由乙方自行承担。​

6.4.5.2 若乙方因特殊情况申请对本条 6.4.5.1 约定的单车事故进行保险理赔(以下简称 “进保申请”),需向甲方提交书面申请,并附事故现场照片、交警部门出具的事故认定书(如有)、车辆损失评估报告等相关证明材料。​

6.4.5.3 甲方收到乙方进保申请后,由甲方在收到材料后【5】个工作日内完成审批并出具意见。

6.4.5.4 仅甲方审批通过,甲方才协助办理理赔,未经审批擅自进保的,乙方支付【2000】元/次辆违约金,且承担全部保险费用及理赔损失。​

6.4.5.5 经甲方审批通过的进保,乙方仍需按 6.4.2 条支付保险上浮费,甲方可根据情况要求乙方额外承担评估费、手续费等。

6.5 安全教育

6.5.1 乙方发生交通事故或者安全事故的,甲方有权对乙方及乙方驾驶员进行教育培训。

6.5.2 在租赁期内,若乙方发生一般有责事故及以上事故,乙方必须做到:①查清事故原因;②严肃处理事故责任者;③对车辆事故的驾驶员进行教育培训;④落实防范措施。若甲方做过安全教育培训的,还需向甲方提供安全教育会议内容、安全教育签到表、现场至少三张照片。若乙方未做到前述事项的,须向甲方承担1000元违约金。

6.5.3若乙方发生三次或者三次以上(含三次)以及一个星期连续发生两次及两次以上(含两次)安全事故或者交通事故的,甲方有权对乙方及乙方驾驶员进行安全教育培训,乙方必须参加甲方组织的教育培训活动,乙方接到甲方教育培训未按时参加的,需向甲方承担1000元违约金。如乙方经甲方约谈教育后仍发生事故,视为乙方严重违约,甲方有权单方面无条件强制收回租赁车辆,无需承担任何违约责任。收车过程中产生的拖车费、保管费、运输费、场地占用费等一切相关费用,均由乙方承担;同时,因乙方前述违约行为造成甲方全部损失(含但不限于行政罚款、赔付款项、律师费、诉讼费、保全费、差旅费等),均由乙方全额承担,甲方有权从保证金中直接抵扣,保证金不足以清偿的,甲方有权向乙方继续追偿剩余款项。

6.5.4动态监控违规管理:

6.5.4.1一般违规:租赁期限内,因驾驶员违规行为产生属实动态监控预警风险的,乙方按以下标准承担违约金:首次50元/次,第二次100元/次,第三次及以上违规500元/次,乙方应当在甲方通知违规之日起1个自然日内支付给甲方,逾期未支付的,乙方须在次月车辆租金结算时一并向甲方支付。乙方更换新驾驶员须提前向甲方书面报备,人员更换后违规次数将重新计算。

6.5.4.2 严重违规处置及车辆收回:租赁期限内,车辆因违规导致动态监控风险属实,连续2个月被交管部门通报的,甲方有权要求乙方在第2个自然月底起将车辆停运1个月,停运期间正常计收租金。为避免车辆营运证连续3个月通报导致被注销,乙方须严格遵照甲方停运要求执行,乙方拒不配合停运的,视为严重违约,甲方有权单方无条件强制收回租赁车辆,无需承担任何违约及赔偿责任。若因乙方拒不配合收车、违规持续营运造成车辆营运证被注销的,乙方须全额承担营运证注销后6个月内全部车辆租金。车辆强制收回过程中产生的拖车费、停放保管费、转运费、场地使用费等全部费用均由乙方承担。因乙方前述违约行为给甲方造成的全部损失(含但不限于行政罚款、赔偿金、律师费、诉讼费、保全费、差旅费等),均由乙方全额承担,甲方有权从车辆保证金中直接抵扣,保证金不足以清偿的,甲方有权就剩余差额向乙方另行追偿。

6.6 违章处理

6.6.1车辆发生违法违规的,乙方应及时处理车辆违章并承担处理费用(违章,最晚应在甲方通知后15个自然日内处理完毕)。乙方超过15天未处理的,甲方有权代办处理,并向乙方发送强办告知函,明确强办时间、强办费用,强办处理的收费标准以附件2的约定为准。同时,甲方有权按一条违章信息1200元的标准(广东区域内为2000元/条),向乙方收取违约金;违约金金额不足以覆盖甲方损失时,乙方还应承担赔偿责任。

6.6.2在处理违章过程中,乙方必须遵循如下驾驶证使用限制:车辆租赁期限6个月以下的(含6个月),仅限于一本驾驶证处理违章,租赁期限超过6个月的,仅限于两本驾驶证处理违章,乙方处理违章超过前述限制的,须向甲方承担 5000元/ 次/辆的违约金。租赁满一个记分周期可使用三本驾驶证。

6.7 年检年审配合

6.7.1甲方负责办理车辆年检年审等相关证照手续,乙方应积极配合,在甲方通知的时间内将车辆开到指定地点。乙方完全明白且理解,车辆的年检年审等检验检测工作,是车辆依法、安全运营的基本保障,如乙方未能在甲方通知时间内将车辆开到甲方指定地点,属于乙方严重违约行为,不论最终是否顺利完成相关检验检测手续,乙方均应向甲方支付2000元/次的违约金。

6.7.2车辆年审过程中发现有未处理违章罚分、罚款等,经甲方以口头、电话、短信、微信及其他方式等前述任一方式告知乙方后,乙方在【3】个工作日内仍未履行的,甲方有偿代办处理,代办处理费用同6.6.1条规定。

6.7.3 乙方应按有关规定使用车辆,不得私自改装车辆,如需改装应经甲方同意,保证车辆在年检时符合要求,如年检不合格,乙方应自费维修整改,经维修整改后仍不合格的,乙方应赔偿甲方重新购买同款同型车辆及设施设备的全部费用。如乙方未经甲方同意,私自拆卸、改装车辆核心零部件,视为乙方严重违约,由此产生的一切责任和后果均由乙方承担;如因此造成车辆严重安全隐患,乙方须根据甲方要求,按照该车辆购买价(以发票金额为准)的两倍购买该改装车辆。

6.7.4 租赁期间,车辆相关证照遗失、被盗、毁损的,甲方可以协助补办,乙方应承担补办证照发生的费用(包括但不限于差旅费、工本费、官费等);具体费用标准以附件2约定的为准。在证照办理期间,租赁费用不停止计费。

第七条 调换车辆和备用车辆服务标准

7.1 甲方有权根据实际运营情况,对已经出租给乙方的车辆进行调换,但调换车辆应是同类型车辆,并提前15天通知乙方,若乙方不同意,甲方有权强制收车,终止与乙方的合作,本合同即终止,双方均不承担违约责任。

7.2 在租赁期内,如非因车辆自身质量问题导致维修的,维修期间,乙方可向甲方申请备用车辆,在征得甲方同意后,交付备用车辆供乙方使用,乙方应按照交付车辆类型对应的租金标准另行向甲方支付租金、保险费等租赁费用。

7.3 调换车辆和备用车辆交还车流程参照本合同相应条款的约定。

" }, + { id: 'b8', title: '违约责任', locked: true, content: "

第八条 违约责任

8.1如乙方违反本合同项下任何约定(含保证人违约),视为乙方违约,乙方应当按照现行法律法规及本合同约定向甲方承担违约责任,且甲方有权采取如下一种或者多种救济措施以维护其权利:

8.1.1要求乙方在5日内补正或采取补救措施,未在5日内整改补正的,甲方有权书面通知乙方单方终止本合同或终止所涉车辆租赁关系。

8.1.2要求乙方赔偿甲方的经济损失。乙方致使甲方承担任何费用、责任或者遭受任何损失的,乙方应就前述任何费用、责任或者损失向甲方进行赔偿,这些费用、损失包括甲方向第三方支付的或导致第三方遭受的赔偿金、违约金等费用及损失。

8.1.3要求乙方承担因乙方行为导致甲方进行各类诉讼或者仲裁而产生的费用,包括但不限于诉讼费、律师费、保全费、公证费、鉴定费、人员工资、差旅费等,且这些费用不应包含在本合同约定的违约金中。

8.2 乙方同意并确认,如乙方违反本合同项下任何一项约定、任何承诺和保证,除本合同另有特别约定外,乙方应按5000元/次的标准向甲方支付违约金。致使甲方承担任何费用、责任或者遭受损失的,乙方应就上述任何费用、责任或者损失(包括但不限于因违约而支付或损失的利息以及律师费)向甲方另行赔偿。

8.3 本合同履行期内,乙方逾期支付租赁费、保证金以及应支付的其他任何费用的,每逾期一日,应向甲方支付应付款项千分之三的违约金,且甲方有权锁车再进行协商处理;超过当期结算日期3天仍未支付相关费用,甲方有权提前终止本合同并强制收回车辆。

8.4 乙方根据法律及本合同约定应向甲方支付费用、赔偿损失或承担其他法律责任,在乙方未能全面履行时,甲方可从保证金中直接进行等额抵扣,不足部分仍由乙方承担,且乙方应在甲方书面通知后3个工作日内补足保证金至本合同约定数额,否则,每逾期1日,乙方每日按照应付未付保证金金额的千分之三向甲方支付违约金。

8.5 在租赁期内,乙方使用标的车辆超载、超速或有其他违反交通规范行为但未造成损失的,法律责任由乙方承担,甲方给予乙方警告处理,后续若拒不纠正再次发生的,乙方应向甲方支付2000元/次违约金,达到提前终止合同条件的,甲方有权提前终止合同并强制收回车辆。

8.6 若因乙方未按1.4条完成里程,造成甲方或车辆厂商不能或未能足额申领国家、地方补贴的,差额部分由乙方负责赔偿给甲方或车辆厂商,导致甲方赔偿的,甲方有权向乙方追偿。

8.7 乙方按照本合同约定向甲方支付违约金的,如同一违约情形下违约金约定不一致的,以约定较高标准的违约金为准,如同一违约情形下设定不同类型违约金的,违约金应累计计算,如违约金不足以弥补甲方损失的,乙方另行向甲方补足。

" }, + { id: 'b9', title: '合同终止及其他', locked: true, content: "

第九条 合同终止、解除

9.1 因不可抗力或行业主管部门的要求,致使本合同无法继续履行的,甲乙双方均有权提前终止本合同,双方均不承担违约责任,在乙方向甲方结清全部费用后,双方无任何争议,甲方将剩余保证金及租金无息退还乙方。

9.2 因甲方经营状况发生重大变化,致使甲方不能继续从事车辆租赁服务的,甲乙双方均有权提前终止本合同,且双方均不承担违约责任,在乙方归还甲方车辆,向甲方结清全部费用后,甲方应将剩余保证金及租金无息退还乙方。

9.3 经甲乙双方协商一致,本合同可以解除或提前终止。

9.4 在租赁期限内,因乙方行为导致甲方解除或提前终止本合同的,或乙方出现下列情形之一的,甲方有权立即解除或提前终止本合同并强制收回车辆,且乙方应向甲方支付违约金,违约金标准为租赁当年月平均租金总价标准的2倍:

9.4.1利用标的车辆从事违法犯罪活动的,或因涉嫌违法犯罪被采取强制措施或受到行政处罚的;

9.4.2 未按期支付本合同项下的相应费用超过3个工作日的,经甲方催告后仍不支付的;

9.4.3涉酒涉毒驾车、交通事故逃逸、不及时抢救伤员保护现场、谎报或瞒报交通事故的;

9.4.4 擅自将标的车辆转让、转租、转包,或设置抵押、留置、质押等负担,或恶意更换车辆零部件或拆撤、损毁、涂抹车辆设施、设备、标记、广告等的;

9.4.5 标的车辆技术状况经甲方累计3次检查不合格,或经甲方检查不合格要求乙方修复,乙方在3日内拒不修复的;

9.4.6 向甲方提供虚假证件或资料的;

9.4.7 发生3次有责交通事故或单次有重大、特大交通事故的;

9.4.8甲方通知乙方协助办理标的车辆年检手续,乙方不按甲方的要求履行协助义务,导致标的车辆无法年检或年检不合格的;

9.4.9利用标的车辆进行相关法律法规明令禁止的行为,或其他严重影响甲方权益或可能导致甲方损失的行为;

9.4.10 乙方有违反本合同约定导致甲方解除或提前终止本合同的其他情形的。

9.5 租赁期限届满前,乙方提前退租的,则属于乙方严重违约,乙方应按租赁当年月平均租金总价标准的2倍向甲方支付租金补贴,租金补贴甲方有权从乙方保证金中直接抵扣。

9.6 甲方提前解除本合同的,应提前15天书面通知乙方,乙方如有异议的,乙方应在通知送达之日起15日内书面回复,但不影响甲方解除合同的效力。

9.7 本合同中的合同解除、业务终止之标准:甲乙双方在《交接车验收清单》上签字,退车手续办理完毕,乙方处理完所有违章,扣除各种乙方应承担的费用,甲方无息退还保证金。

9.8 乙方若需要办理ETC,由乙方自行办理。合同解除、业务终止后,乙方须先自行注销ETC,再办理退车手续,否则向甲方支付2000元/卡退卡费。

第十条 其他

10.1 关于送达:任何与本合同有关的由一方发送给其他方的通知或其他通讯往来(“通知”)应采用书面形式,按照下列通讯地址或通讯号码或电子邮箱送达被通知人均构成有效通知,包括但不限于法院、仲裁机构的法律文书及双方的来往函件等。

甲方:羚牛氢能科技(广东)有限公司

通讯地址:

联系人姓名及电话:

邮箱:

乙 方 :

通讯地址:

联系人姓名及电话:

邮箱:

10.2 任何一方上述的通讯地址或通知方式发生变化(“变动方”),变动方应当在该变更发生后的七(7)日内通知其他方。变动方因上述地址不明确或未按约定及时通知,导致法院、仲裁机构的法律文书或任一方文件无法送达的,文件退回之日即视为送达之日。甲乙双方确认,前述送达方式适用于各个司法阶段,包括但不限于一审、二审、再审、执行以及督促程序。

10.3 本合同中约定的违约责任、赔偿责任条款以及乙方义务,甲方于订立合同时已提请乙方注意并按乙方要求对该条款予以说明,乙方已充分了解并自愿订立且不再提出违约金、赔偿金过高和义务过多的请求。

10.4 本合同未尽事宜可另行签订补充合同,补充合同与本合同具有同等效力。

10.5 甲、乙双方因履行本合同发生争议,可协商解决,协商不成提起诉讼的,甲乙双方一致同意由合同签订地(浙江省嘉兴市平湖市乍浦镇)人民法院管辖。

10.6 本合同一式两份,甲方持一份,乙方持一份,在甲方盖章,乙方签字(自然人)或盖章后生效,若有保证人,保证人签署本合同及附件《保证人的保证责任》后,对保证人生效。

10.7 本合同所有附件、附表、车辆交接单均是本合同的有效组成部分,与本合同具有同等的法律效力。本合同包括但不限于以下附件:

附件1《租赁订单》、附件2《车辆产生的费用》、附件3《安全责任告知书》、附件4《重型普通货运车辆租赁动态监管责任告知与确认书》、附件5《安全责任承诺书》、附件6《车辆使用规范和驾驶员要求》《授权委托书》《安全责任协议》及其他。

(以下无正文)

" }, + { id: 'b10', title: '签章页', locked: true, content: "甲方(盖章):____________ 乙方(盖章):____________\n签订日期:{{signDateYear}} 年 {{signDateMonth}} 月 {{signDateDay}} 日\n\n(以下无正文)\n\n本合同所有附件、附表、车辆交接单均是本合同的有效组成部分,与本合同具有同等的法律效力。本合同包括但不限于以下附件:\n附件1《租赁订单》、附件2《车辆产生的费用》、附件3《安全责任告知书》、附件4《重型普通货运车辆租赁动态监管责任告知与确认书》、附件5《安全责任承诺书》、附件6《车辆使用规范和驾驶员要求》《授权委托书》《安全责任协议》及其他。" }, + { id: 'b11', title: '附件1 租赁订单', locked: false, content: "

附件 1:租赁订单 订单编号(LNZLHT )

甲方(出租方): 羚牛氢能科技(广东)有限公司

乙方(承租方):

本订单租赁车辆总计 辆,合同租金及服务费含税总价为:¥ 元,保证金总计需缴纳¥ 元(若开具保证金收据的,退车时应提交保证金收据原件方可退还保证金),首期租金及服务费含税需支付¥ 元。租赁车辆含保险总计 辆,每辆保险险种包含 三者责任险【 】万元、车损 ,含通行证总计 张。

一、租赁车辆信息

品牌

车型

车牌号

租金及服务费

小计

(元/ 月/辆,含税)

价格及服务内容

保证金

(元/辆)

车辆租赁期限

\u0007\u0007\u0007车辆租金

(元/月/辆,含税)\u0007服务费

(含税)\u0007服务内容\u0007\u0007\u0007\u0007 \u0007 \u0007 \u0007 \u0007 \u0007 \u0007 \u0007 \u0007\u0007 \u0007 \u0007 \u0007 \u0007\u0007 \u0007\u0007 \u0007\u0007

车牌号在签约时无法确定的,以实际交付的车牌号为准,若后续需要更换车辆的,均以《交接车验收清单》为准。

二、其他约定:

1.车辆交付(交还)地点: 。车辆交付时间:车辆交付时间:     年 月 日(以实际交付的时间为准)。

2.租赁车辆仅限 行政区域范围内使用,超出前述约定区域范围使用租赁车辆应事先征得甲方书面同意并办理相关手续。

3.甲乙双方协商一致增加租赁车辆的,双方另行制作《租赁订单》,经甲方盖章,乙方签字/盖章后生效。

4 、甲乙双方均认可并接受双方签订的《商用车租赁合同》(合同编号 )全部内容,本租赁订单列明的条款的,按本租赁订单执行,本租赁订单未列明的条款,均按照《商用车租赁合同》执行。

5.甲方有权对租赁车辆进行调换,但应提前15天通知乙方,乙方必须配合。还车、调换车辆时车辆外观、驾驶室、车厢必须保持干净整洁,否则收取清洗费,车身原车广告未经甲方同意不得撕毁、损坏、涂抹,否则承担车辆的修复费。本条中的清洗费,500元/辆,车身修复费,5000元/辆,甲方有权在保证金直接扣除。

6.租赁期内车辆产生的能耗费、停车费等用车费用由乙方承担。

7.本《租赁订单》一式两份,甲方执一份,乙方执一份,在甲方盖章,乙方签字(自然人)或盖章后生效。保证人签字(自然人)或盖章后,对保证人生效。

甲方(签章):

乙方(签字/盖章):

授权代表:

授权代表:

签订日期:

年 月 日

签订日期: 年 月 日

" }, + { id: 'b12', title: '附件2 车辆产生的费用', locked: false, content: "

附件2:车辆产生的费用

一、车辆维保、易损件、车辆轮胎、蓄电池及送车上门和上门收车费用:

维保服务

易损件

轮胎及蓄电池

自行承担车辆维保所发生的所有费用

自费承担

型号:11.00R20 18PR ,自费承担,还车时轮胎每磨损1mm,乙方需向甲方支付【110 】元/毫米的折旧费(计费公式:(交车时轮胎厚度-还车时轮胎厚度)*【mm 】,如乙方在租赁期内更换过轮胎,计算费用为负值的,乙方无需支付费用,甲方也不做任何补偿。)蓄电池,自费承担,每块电池【1200 】元/2年,按照实际租赁时间折算费用。

特别说明:1、轮胎、门把手、刹车片等易损件正常磨损达到需更换时客户负责更换,配件、维修工时费乙方自理;2、乙方充分知晓和同意,如果承租的甲方车辆并非全新车辆,或面临车辆租赁后标的车辆配件即过保的情形;3、如乙方未根据下表维护保养标准执行车辆维护工作,造成车辆脱保的,由此给甲方产生的不利影响、额外义务负担、费用损失等均由乙方自行承担。

送车上门,上门收车:从出发往返1公里以内免费;往返1公里以上,按8元/公里收费;人工费以实际维修项目算。拖车费:起步价【400】元+【 5】元/公里/空车,起步价【400】元+【15】元/公里/重车;基础服务费为【100 】元/辆/次。材料费以维修站的价格为准,最终以甲方确定的价格为准。

二、保养和修复:不论乙方租赁车辆期限长短,乙方确认并同意均按照下述规则维护保养和修复标准执行, 产生的费用按维修站的价格计算,若维修站未确定价格的,参考下表价格执行,最终以甲方确定的价格为准:1、底盘保养

序号\u0007区分\u0007保养项目\u0007周期/万KM\u0007型号\u0007数量\u0007单价\u0007次数\u0007总价\u0007公里要求(万公里)\u0007\u00071\u0007配件\u0007驱动电机、电池冷却液\u000724个月/20万公里\u0007重负荷有机乙二醇型冷却液\u000736L\u0007143\u00072\u0007286\u000720\u0007\u00072\u0007工时\u0007\u0007\u0007\u0007\u000750\u00071\u000750\u0007\u0007\u00073\u0007配件\u0007转向器\u00076个月/10万公里\u00078 号液力传动液)及滤芯\u00071.7L\u000725\u00071.7\u000742.5\u000710\u0007\u00074\u0007工时\u0007\u0007\u0007\u0007\u000750\u00071\u000750\u0007\u0007\u00075\u0007配件\u0007新能源法士特、绿控\u000712个月/5万公里\u0007GL-5\\\\85W\u00077L\u000725\u00077\u0007175\u00075\u0007\u00076\u0007工时\u0007\u0007\u0007\u0007\u000750\u00071\u000750\u0007\u0007\u00077\u0007配件\u0007油润滑的中

后桥\u000712个月/10万公里\u0007GL-5\\\\80W-90\u00079L\u000729.7\u00079\u0007267.3\u000710\u0007\u00078\u0007工时\u0007\u0007\u0007\u0007\u000750\u00071\u000750\u0007\u0007\u00079\u0007配件\u0007脂润滑的轮边\u00072个月/1.5万公里\u00073#锂脂\u00073KG\u000720\u00073\u000760\u00071.5\u0007\u000710\u0007工时\u0007\u0007\u0007\u0007\u000775\u00071\u000775\u0007\u0007\u000711\u0007配件\u0007驾驶室举升器\u00076个月/5万公里\u000710 号航空液压油\u00071L\u000730\u00071\u000730\u00075\u0007\u000712\u0007工时\u0007\u0007\u0007\u0007\u000750\u00071\u000750\u0007\u0007\u000713\u0007配件\u0007空气干燥器\u000712个月/10万公里\u0007科诺尔、威伯科\u00071个\u0007184\u00071\u0007184\u000710\u0007\u000714\u0007工时\u0007\u0007\u0007\u0007\u000730\u00071\u000730\u0007\u0007\u000715\u0007配件\u0007空气滤清器\u000712个月/4万公里\u0007空气滤芯\u00071个\u0007150\u00071\u0007150\u00074\u0007\u000716\u0007工时\u0007\u0007\u0007\u0007\u000730\u00071\u000730\u0007\u0007\u000717\u0007配件\u0007各类球头\u00072个月/1万公里\u00072号锂基脂\u0007\u000775\u00071\u000775\u00071\u0007\u0007合计

2.燃料电池保养项目

序号\u0007保养配件\u0007周期/万KM\u0007次数\u0007量\u0007单价(元,未税)\u0007工时【90】元/时\u0007公里要求(万公里)\u0007\u00071\u0007空滤\u00072\u00077\u0007/\u00071099 \u000745\u00072\u0007\u00072\u0007离子过滤器\u0007首次1万KM,之后每次间隔3万KM\u00074\u0007/\u0007969.57\u000790\u0007首次1万,后每次间隔3万\u0007\u00073\u0007冷却液\u00078万KM\u00071\u000740L\u00071664.845 \u0007 180\u00078\u0007\u0007三、证照及违章处理等收费标准:如甲方代为处理,乙方应向甲方支付的代处理费计费结构为:(基础费用)+(服务费收费标准),具体如下表:

项目

基础费用

服务费收费基础

服务费/元

补办行驶证

以实际收费为准

每台车每次

200

补办牌照

以实际收费为准

每台车每次

300

补办营运证

以实际收费为准

每台车每次

300

补办加氢证

以实际收费为准

每台车每次

300

处理违章(罚款)

实际罚款为准

每台车每条

100

处理违章(罚分)

实际罚分为准

每台车每条

100

借用备用钥匙

送上门,20公里内100元,每超1公里3元每公里,过桥过路费额外收取

每台车每次

100

补配钥匙

实际补配价格

每台车每次

400

" }, + { id: 'b13', title: '附件3至6 安全与规范', locked: false, content: "

附件3:安全责任告知书

一、各车辆使用人责任主体:

1.各租赁客户合作伙伴、挂靠我司车辆的用户,当您使用我司车辆期间必须承担安全责任,责任主体是:车辆使用人(公司或个人),挂靠车辆使用人(公司或个人)。

2.根据《中华人民共和国民法典》第一千二百零九条,因租赁、借用等情形机动车所有人、管理人与使用人不是同一人时,发生交通事故等造成损害,属于该机动车一方责任的,由机动车承租人和使用人承担该赔偿责任。

3.租赁车辆、挂靠车辆使用人,平时要做好出车检查,制订车辆日常检查记录表,对于:氢系统、电池、灯光、轮胎、车辆外观、随车配置物品等做好日常检查并记录,如有异常必须及时处理,车辆合格满足出车条件以后才能出车,并且必须保持车辆定位系统在线。

4.车辆、挂靠车辆使用人,要做好我司车辆的安全、事故台账,出事故必须登记在台账中,并且开展四不放过教育:①事故原因没有查清不放过,②事故责任者没有严肃处理不放过,③全体司机及车队管理人员没受到教育不放过,④防范措施没有落实不放过。

开展教育必须有现场多张照片,所有司机及车队管理人员在教育签到表上签字,照片及签到表原件留档,我司安全部备查。

二、车辆使用安全注意事项:

1.所有驾驶员要有从业资格证,9.6米厢车与4.2米冷藏车,必须出示本人资格证,登记后才可以提车。

2.注意车辆高度,苏龙、柳汽乘龙等9.6米车高4米。跃进、现代冷链4.2米车高3.38米。现代普货车高3.28米。集卡飞驰车头3.9米,挂车装载平箱4.2米左右。驾驶员一定要注意车厢高度,当心限高杆、桥梁、树枝等物。

3.因为是氢用车,车内不得吸烟,车周边10米以内不得吸烟,车辆远离烟火(周边10米以上),不得将车停放在高压电线下。

4.进入加氢站,必须无烟火,关闭总电源后加氢。服从加氢站管理,如:静电接地,不超过35Mpa压力等等。

5.车辆遇到氢气泄漏,不严重移至空旷处,关闭总电源!车辆旁边不得有闲杂人等,远离明火,联系运维人员:江浙沪沈帅15021428914;新疆赵波18639103977;广东王河北13928468652;集卡车李泽琦13383480807;北京及其他地方赵小峰18868302409。

6.系统漏气并着火,火焰未烧至其他管路和车身;处理方式:立即熄火,关闭电源,马上灭火,立即打上述专业人员电话,在他们指导下灭火,以防电池等爆炸。如果火势大、控制难、立即拨打 119,人员撤离至安全区域。

7.供氢系统漏气着火,或电池着火,火焰烧至其他管路或车身;处理方式:立即拨打119,人员撤离至安全区域。

8.车辆每日检查:轮胎完好、气压标准、灯光设施齐全、车辆不带病上路。

9.雨雾天气,降低速度。注意能见度,与前车保持足够安全距离。

10.行驶中电池高温冒烟,马上靠边,断开高压系统,马上联系运维。

11.车辆发生火情立即停车,用灭火器降温灭火,关闭电源,火势大要保证人身安全,离开车辆打119报警。

12.氢能车辆禁止电焊,周围不许任何焊接,此类维修通知专业人员来修理。

13.出交通事故,第一时间打安全经理王屹青电话:18116198188,告知实际情况,加此电话微信,拍现场照片:整体事故场景、我方车牌、对方车牌、我方及对方车辆受损部位。并发定位,安全经理会指导处理事故。

贵方上述安全告知已收到并熟悉,我方(被告知人)郑重承诺:无条件遵守上述安全操作事项。若因我方(被告知人)原因导致发生安全事故,我方(被告知人)自行承担一切法律责任。

我方已清楚地了解附件 1《租赁订单》、附件2《车辆产生的费用》、附件3《安全责任告知书》和所有内容及车辆租用相关要求,在此向出租人郑重承诺:保证遵守《商用车租赁合同》及附件文件所有约定。

确认方(签字/盖章):

确认时间: 年 月 日

附件4:重型普通货运车辆租赁动态监管责任告知与确认书

甲方(出租方):羚牛氢能科技(广东)有限公司

乙方(承租方):

为严格落实《中华人民共和国道路交通安全法》《道路货物运输及站场管理规定》等相关法律法规及行业监管要求,规范重型普通货运车辆租赁期间的安全运营行为,强化驾驶员安全管控,防范道路交通安全风险,保障租赁车辆、人员及公共安全,现就甲方租赁给乙方车辆的动态监管违规行为及处置事宜,正式告知如下:

甲方将对租赁车辆实施24小时动态监管,重点监测驾驶员存在的超速行驶、超时驾驶(持续时间≥4小时)、接打手持电话、操作手机、抽烟、生理疲劳驾驶、未按规定系安全带、偏离驾驶位、佩戴红外阻断型墨镜遮挡视线、动态监控设备被遮挡或失效、双手脱离方向盘、不目视前方行驶、实线变道、前向碰撞、车道偏离、前向车距过近等各类违反道路交通安全法律法规及双方签订的《商用车租赁合同》约定的违规行为。本确认书所载违规行为的认定,以甲方提供的、由车辆合法装载的动态监控设备所记录的数据、影像为准。若乙方对相关数据、影像记录的违规事实有异议,应在收到甲方通知后48小时内书面提出,并提出理由。乙方驾驶员的上述违规行为,不仅会给乙方自身运营带来重大安全隐患与法律责任风险,还将对甲方造成合规、运营等多方面的不利影响及实际损失,具体如下:

一、合规经营风险:

甲方作为重型普通货运车辆出租主体,依法承担租赁车辆的动态监管主体责任,若乙方驾驶员的违规行为被交通运输、公安交管等监管部门查处,甲方将面临行政处罚、行业资质核查、信用评级下调等合规风险,情节严重的,将直接影响甲方道路运输相关业务的正常开展。若租赁车辆因乙方原因(包含但不限于乙方驾驶员屡次、严重违规或乙方安全管理缺失)导致租赁车辆被交通主管部门依法做出撤销/吊销/注销其道路运输证,导致车辆无法运营,相关损失由乙方承担。

二、运营资源消耗:

针对乙方驾驶员的违规行为,甲方需额外投入大量人力、物力开展违规核实、沟通对接、整改监督等工作,占用甲方动态监管、车辆维护、客户服务等核心运营资源,大幅降低整体租赁业务运营效率,严重影响甲方对其他合规客户的服务质量,造成甲方运营成本增加。

针对监测发现的驾驶员违规行为,结合上述违规行为对甲方造成的各类影响与损失,甲方将采取分级处置措施,具体如下:

(1)一般违规:租赁期限内,因驾驶员违规行为产生属实动态监控预警风险的,乙方按以下标准承担违约金:首次50元/次,第二次100元/次,第三次及以上违规500元/次,乙方应当在甲方通知违规之日起1个自然日内支付给甲方,逾期未支付的,乙方须在次月车辆租金结算时一并向甲方支付。乙方更换新驾驶员须提前向甲方书面报备,人员更换后违规次数将重新计算。

(2)严重违规处置及车辆收回:租赁期限内,车辆因违规导致动态监控风险属实,连续2个月被交管部门通报的,甲方有权要求乙方在第2个自然月底起将车辆停运1个月,停运期间正常计收租金。为避免车辆营运证连续3个月通报导致被注销,乙方须严格遵照甲方停运要求执行,乙方拒不配合停运的,视为严重违约,甲方有权单方无条件强制收回租赁车辆,无需承担任何违约及赔偿责任。若因乙方拒不配合收车、违规持续营运造成车辆营运证被注销的,乙方须全额承担营运证注销后6个月内全部车辆租金。车辆强制收回过程中产生的拖车费、停放保管费、转运费、场地使用费等全部费用均由乙方承担。因乙方前述违约行为给甲方造成的全部损失(含但不限于行政罚款、赔偿金、律师费、诉讼费、保全费、差旅费等),均由乙方全额承担,甲方有权从车辆保证金中直接抵扣,保证金不足以清偿的,甲方有权就剩余差额向乙方另行追偿。

乙方需高度重视租赁车辆驾驶员的安全管控工作,切实履行承租方安全管理主体责任,加强对驾驶员的日常安全教育、培训及常态化管理,严格落实本确认书约定的分级处置措施,积极配合甲方的动态监管工作,及时整改各类违规行为。甲方将定期向乙方反馈车辆动态监管情况,对乙方未按本确认书要求落实处置措施、整改不到位或者拒不配合监管的行为,甲方将严格按照法律法规及双方签订的《租赁合同》约定处理。

本确认书自甲乙双方签字或盖章后生效,本确认书作为《商用车租赁合同》的附件,与《商用车租赁合同》具有同等法律效力。本确认书未尽事宜,按照双方签订的《商用车租赁合同》相关约定执行;若本确认书内容与相关法律法规冲突,按法律法规规定执行。

乙方确认:已认真阅读并充分全面理解本确认书全部内容,自愿严格遵照本确认书约定及相关法律法规要求执行,并承担因违反确认书约定及相关规定产生的全部责任。

(以下无正文)

甲方(盖章): 乙方(签字/盖章):

授权代表(签字): 授权代表(签字):

日期: 年 月 日 日期: 年 月 日

附件5:

安全责任承诺书

为严格落实《道路运输车辆动态监督管理办法》《广东省道路运输条例》及粤标动态监控管理要求,防范因超速、疲劳驾驶、接打电话、抽烟、遮挡摄像头、屏蔽设备、离线等违规行为被交通运输主管部门连续通报,避免因此导致车辆被责令停运、营运证被暂扣或注销等严重后果,并就车辆粤标动态监控相关安全责任,作出如下郑重承诺:

1.严格遵守《道路运输车辆动态监督管理办法》《广东省道路运输车辆智能监管系统应用管理办法》等国家及地方性法规,并已阅读、理解且承诺遵守贵公司提供的《粤标动态监控专项安全管理方案》(下称“管理方案”)相关规定,服从贵公司依据该方案及相关法规实施的动态监控管理。

2.负责督促我司承租司机规范驾驶,杜绝疲劳驾驶、超速、分心驾驶等违规行为,严禁破坏、屏蔽、断电、拆机等影响粤标监控设备正常运行的行为。

3.及时接收贵公司反馈的车辆违规信息,严格按照要求落实整改,配合贵公司开展司机安全教育、车辆停运等处置措施。

4.若因我司及我司承租司机违规行为,导致车辆被交管、运政部门通报、处罚或贵公司遭受其他损失的,我方同意承担由此产生的全部直接经济损失,包括但不限于政府部门处以的罚款、贵公司为处理此事支出的合理费用(如差旅费、律师费)以及车辆被责令停运期间的租金损失(按《商用车租赁合同》(下称“主合同”)约定标准计算)。

5.我司知悉,若因连续违规被主管部门通报3次及以上,导致贵公司营运证被暂扣或注销等严重后果的,自愿承担全部赔偿责任(包括但不限于车辆价值贬损、营运损失等)。同时,贵公司有权单方解除《商用车租赁合同》,且有权在5年内拒绝与我司及关联方开展任何商业合作。

6.本承诺书自签订之日起生效,有效期至《商用车租赁合同》约定租赁期满之日止。

7.本承诺书作为《商用车租赁合同》的有效附件,与主合同具有同等法律效力。如本承诺书内容与主合同内容约定不一致,则以更严格约束我司义务的条款为准。

(以下无正文)

承诺客户(盖章):__________

法定代表人/授权代表人(签字):__________

签订日期: 年 月 日

附件6:车辆使用规范和驾驶员要求

一、车辆使用规范

启动前

的操作

1.确认电源总开关处于开启状态。

2.检查储氢瓶的气密性,是否变形。

3.确认驻车制动操纵手柄处于驻车状态,确认变速操纵杆处于空挡位置。

4.车辆起步前,观察仪表有无异常显示和报警:剩余氢气量(SOC)不低于10%或者氢气压力不低于3.5兆帕,且动力电池电压显示不低于520V,低压蓄电池显示于22V以上,原地转动方向盘检查转向泵工作是否正常若仪表有指示灯亮起或声音报警,应停车检查。

5.系好安全带,检查安全措施。

车辆起步

1.先将钥匙转动至“ACC”位置,踩住刹车再继续转动至“START”,停留3秒。

2.仪表盘会显示READY等字样。

3.在观察电机运转正常,各仪表、指示灯、无警报灯正常的情况下方可起步。

4.仪表显示“ready”,踩下制动踏板,挂前进挡时,换挡杆切换至“D”,挂倒挡时,换挡杆切换至“R”。

5.放下驻车制动操作手柄,缓慢踩踏加速踏板,电机启动,车辆开始行驶。

停车动作

1.应先放松加速踏板,降低车速。

2.必须在车辆完全停稳后踩住刹车才能换入倒档,否则传动系统将受到损坏。

3.怠速运转夏天5分钟、冬天10分钟时间,关闭电源,停车时长超过2小时需要关闭总电源。

4.关闭用氢系统开关。

起步

注意事项

1.观察左右反光镜,注意车辆行人。

2.两次起动车辆的间隔时间不少于30秒。

3.驾驶室倾翻起来的情况下,禁止启动电机。(一体式车头除外)

4.严禁不松手刹的情况下强行行驶。

5.起步和行车途中手刹处于施加状态电机功率将会降低50%。

汽车行驶

注意事项

1.汽车行驶中,注意使电机冷却液温度保持在正常状态。

2.严禁行驶途中关闭氢能源开关。到加氢站加氢时,提前5分钟关闭用氢开关。

3.行驶中注意观察电池电量(SOC),如果低于设定值50%以下,停车检查是否打开用氢开关,再报售后维修。

4.遇水后请注意特别驾驶,制动器受潮会暂时降低制动力。并缓慢通过。

5.车辆倒车速度不超过15公里/时。

6.紧急刹车制动距离较长且急速刹车时车辆会向左倾斜,应注意车速(最佳车速在40—50),且必要时预见性减速。

7.长时间或下雨天停车要关闭整车总闸,避免损耗低压电或线路故障。

8.行车途中若听见不正常的声音或嗅到不正常的味道应及时停车找出原因。

9.所有高压电源均由点火开关控制,驾驶员离开车辆时必须关掉点火开关,取走钥匙。

10驾驶过程中严禁吸烟(包括但不限于驾驶室、密封空间、距离车辆【10】米范围内)。

11.注意观察冷机的温度是否在正常范围之内。

加氢流程

提前5分钟关闭用氢开关—车辆停在指定位置--人员离开到安全位置等待--加氢进行--加氢完毕关闭加氢口--纯电启动驶离加氢站--打开用氢开关。

加氢注意事项

1.氢的剩余量不能低于SOC10%。气压不能低于3.5兆帕。

2.加氢时严禁在加氢站点燃明火,或者吸烟。

3.要在听从加氢站工作人员的指导下开到指定位置,关闭电源,停车,由工作人员代驾。

4.禁止个人加氢,避免储氢瓶氢气泄漏,加氢需到加氢站。

5.不要携带手机、打火机、MP3等易引起爆炸、自燃的物品进入氢化工作区域。

6.在加氢站附近,拿工具、零件时应轻拿轻放,不得发生刚性撞击产生火花。

特别注意

1.不能急加速,匀速行驶。

2.车辆停车后,要等空压机吹扫结束再关闭电源(建议停车前10分钟关闭EV开关)。

3.电池电量不可低于25%。低于25%时,整车报警,低于15%时,整车无法上高压,将无法启动,应提前外接充电或开启混动模式充电。

4.氢气余量不可低于10%。

5.当车辆soc电量低于50%且未充电,则要确认是否在用氢状态,或靠边确认故障。

6.仪表台中部“EV”按钮为运行模式切换,按下后车辆由混合动力转换为纯动,再按则转换回混合动力,正常无需对此按钮进行操作。

7.加氢时关闭EV开关,使用电动进站加氢,离开加氢站再启动氢(EV开关)。

8.车辆为自动换挡,正常行驶中无需换挡操作,且车辆为电驱动系统,滑行时不须切换为空挡,切换空挡会导致车辆失去油门响应;如需换挡时,应踩刹车进行换挡,切换挡位时须先切换为空挡,并在空挡停留0.5s~1s。

9.开车前检查水箱防冻液是否缺失,过久未使用车辆需更换防冻液。

10.不要随意开启氢罐两侧的门,更不能随意关闭氢管路的阀门。

11.不要频繁关闭开启氢开关,停车20分钟左右的不需关闭。

12.非紧急情况下禁止碰高压急停开关,紧急情况下使用(如事故,涉水,起火)。

13.不能用水枪冲洗橙色线路,避免进水报绝缘和漏电。

14.车辆为电动车辆,因此涉水高度最高为300mm,如积水超过500mm严禁通行,且通过积水路段时应注意减速慢行。

15.停机注意:首先关闭所有用电设备(如空调)钥匙关一档(待燃料电池完全停止工作—再可关二档—最后方可关闭总电源开关)包括重新启动按以上要求操作。

二、车辆操作提醒

(一)安全/充电事项

1.高压安全:高压橙色线束及其连接件,不能随意触碰,有高压触电风险。

2.氢气安全:若有泄漏,立即现场隔离。 严禁人、车入内。供氢系统发生火灾:若条件允许,车辆移动空旷区域,人员撤离;若条件允许,可用淋水对氢瓶降温。

3.充电事项:充电时,需选择电压大于500V及辅助电压为24V的充电桩。尽量避免人为使用EV模式驾驶,减少充电次数。

3.加氢事项:行车前,合理规划路线,关注氢剩余量,及时补充。

4.氢堆功率待机条件:混动模式下氢堆功率输出条件

SOC在85%以上时氢堆自动进入待机状态,直到SOC低于65%时自动开始启动;然后电池开始充电直到SOC在85%以上又进入待机状态,依次循环。

注意:氢堆待机时,如果驾驶员要求功率大于电池放电功率限制值,氢堆也会自动启动输出功率。

5.功率限制

限制条件

限制措施

限制现象

纯EV模式

SOC≤30%

输出功率限制

空调停止使用,电池电量报警灯点亮/SOC进度条显示黄色

SOC≤20%

车辆限速≤52km/h/电池低电量警告灯+闪烁/SOC进度条显示红色

SOC≤10%

车辆限速≤17km/h/电池低电量警告灯+闪烁/整车故障灯点亮/SOC进度条显示红色,车辆限输出扭矩

(二)主要故障灯处理

1.氢燃料电池冷却液报警指示灯:停车检查,检查燃料电池冷却液液面状态,联系羚牛运维服务人员检查冷却液是否泄漏或其他原因。

2.氢燃料电池堆报警灯:氢燃料电池控制系统某个运行数据异常,切换E V模式、联系羚牛运维服务人员处理。

3.氢气泄漏报警灯:立即切换为EV模式,立即寻找远离火源空旷通风区域停车,关闭低压电源总开关,联系羚牛运维服务人员处理。

4.供氢系统报警灯:车辆可能会出现限制功率输出;切换EV模式、联系羚牛运维服务人员处理。

5.按指示灯颜色判断:绿灯正常运行-黄灯检查警告-红灯停车检查。

(三)驾驶模式切换-混动/EV模式

EV模式:按下EV开关键,仪表指示灯亮,车辆执行EV纯电模式运行。

混动模式:EV开关键关闭下,车辆启动,系统默认为混动模式。

注意:为延长动力电池寿命,纯EV模式仅允许在紧急情况下使用,严禁长期使用; EV模式可以在任何条件下切换到混动模式;混动模式只能在停车后才可切换为EV模式。

(四)能量回收功能

1.开启方法:拨杆在最上位置时为1档,拨动开关到最下位置时为2档。 (车辆默认挡位为1档)

2.启用条件:SOC<95%、车速>15Km/h、未踩加速踏板。

3.解除条件:踩下加速踏板或 SOC>90%或车速<15Km/h。

4.当道路平坦需长距离滑行或坡度较大需要较大制动力时,下拨能量回收开关到2档。

注意:合理使用能量回收,能有效减少制动系统磨损和降低氢耗;能量回收不能代替行车制动。

(五)ECO模式

开启方法:按下ECO模式开关,仪表ECO指示灯点亮,车辆进入节能模式运行;否则为正常运行模式。

注意:重载时使用正常模式(ECO未开启);道路平坦,空车或者轻载时,打开ECO开关,使用

节能模式;合理使用能量回收和ECO功能,可以提高续航里程。

(六)车辆停放

车辆停车后,将钥匙置于OFF档,等待3分钟,待氢堆完成自洁后夏天5分钟和冬天10分钟,再关闭低压电源总开关;停车区尽量选择空旷非封闭空间且应远离火源;为保护低压蓄电池性能,长时间停车(超过2个小时),强烈建议关闭低压电源总开关。

(七)提升续航驾驶

1.不要急加、减速:急加、减速能量损耗大,严重降低续航。

2.经济车速40-70KM/H:车速保持在此阶段匀速行驶 ,能有效地提高车辆续航里程。

3.能量回收和ECO模式使用:能量回收模式可回收多余动能提高续航,合理使用ECO模式,降低动力输出提高续航。

注意:合理使用空调,避免夏季过冷,冬季过热,导致能耗增加。

(八)AVAS-低速报警

1.开启条件:根据车速自动激活;D挡时:车速在1-20km/h时,发出“嗡嗡”报警声,车速高于20km/h时,报警自动取消;R档时,发出倒车音提示音。

2.作用:车辆启动行驶时,车辆噪声较小,为确保行车安全,强烈建议开启低速报警功能,提醒行人注意安全。

(九)定速巡航

1. 开启方法:按下定速巡航开关按键,通过方向盘设定速度。

2.启用条件:车辆无故障、40km/h≤车速≤90km/h。

3.解除条件:关闭巡航开关、踩下制动踏板、车速>90km/h、车速<40km/h。

注意:用定速巡航开关以外的方式解除定速巡航,巡航控制系统仍处于待命状态。 按下“SET/-”时,车辆会自动恢复到最近设定的巡航速度。

(十)故障处理流程

1.若车辆出现故障时,请及时联系羚牛运维服务人员。

2.若车辆故障时,为快速及有效地判断故障原因,请驾驶员记录以下信息,反馈给现代商用车服务人员。需提供车辆信息如下:车辆铭牌、故障码内容照片、仪表整体显示照片。

3.故障现象描述:车辆在行驶过程中或启动时,仪表显示故障灯,车辆有什么故障现象;车辆在使用过程中,车辆什么功能无法使用;尽可能拍摄故障时照片或视频,便于快速故障原因判断;

(十一)参数/故障查询方法:

1.车辆运行参数查询:按下方向盘上“菜单键”,进入“设置”界面,按“选择键”选择“信息栏目”项后,再按“确认键”,可读取相关系统信息。

2.车辆故障内容查询:按下方向盘“菜单键”,进入“故障诊断”界面,按“选择键”选择“故障内容”项后,再按“确认键”,可读取故障码内容。

三、驾驶员安全管理要求:

1.乙方驾驶员必须遵守《中华人民共和国道路交通安全法》《中华人民共和国道路运输条例》及有关交通安全管理的规章规则,安全驾车,并应遵守其他相关的规章制度。

2. 乙方与驾驶员须签订聘书,聘用条件为:

⑴须持有合法的驾驶执照,对于租赁车辆的同车型实际驾龄必须超过2年;

⑵年龄不超过55周岁;

⑶取得道路运输管理部门颁发的从业资格证;

⑷经乙方组织对其进行面试、理论和机械常识考核、道路驾驶技术测试合格。

3.驾驶员的管理、宣传、教育。

(1)驾驶员应爱惜甲方车辆,平时要注意车辆的保养,经常检查车辆的主要机件。每月至少用半天时间对自己所开车辆进行检修,确保车辆,以保持车辆的清洁。驾驶员发现车辆有故障时要立即检修。不会检修的,应立即报告管理人员,并提出具体的维修意见(包括维修项目和大致需要的经费等)。未经批准,不许私自将车辆送厂维修;

(2)驾驶员对自己所开车辆的各种证件的有效性应经常检查,出车时一定保证证件齐全。连续行车里程超过四小时的,须配备两名驾驶员,每位驾驶员连续驾驶时间不得超过四小时;

(3)乙方应对本单位每位驾驶员进行岗前安全教育培训、考核,考核不合格的,不得上车驾驶。并保存驾驶员安全培训记录;

(4)对违章次数多、出险率高、发生交通事故的驾驶员进行再教育培训或依据公司制度进行惩罚。

4.承租人在收到违章信息通知后,从通知日期后15天内处理完毕,防止积累。(北京要求10个工作日内处理)。

5.承租人如果发生违章,将会收到短信,微信或者书面形式通知,并会有出租人或售后人员致电确认。

6.如若自身不便处理违章,请在发生违章之日起 10日内 联系出租人或售后人员并说明情况,由出租人代为处理,产生的一切费用由客户自行承担。在收到以下违章信息的情况下,必须在得到通知后的10天内必须处理完毕:

(1)单辆车累计扣分6分以上或罚款800元以上;

(2)单辆车未处理违章3条以上;

(3)因年审要求,在注册日期当月最后截止日期前10个工作日处理掉所有违章。不处理的,出租方将安排代为加急处理,产生的费用由承租方承担。

7.如收到违章信息超出规定时间拒不处理的,出租人将预警发函或进行远程控制(如锁车)措施处理。

8.对半年内单辆车出现违章超过10次(北京在季度内单辆车不能超过两次违章,并且不能出现一次扣6分的违章)、发生交通事故负有责任4次以上或者重大交通事故一次的情况下,出租人将会做出以下应对方案:

(1)承租人必须更换司机;

(2)拒不整改配合,则视承租人为违约做收车处理。

9.欢迎您对我们的服务进行监督并提出宝贵意见。我公司的监督投诉电话:【 400-021-1773】。

接收方(签字/盖章):

时间: 年 月 日

授权委托书

致:羚牛氢能科技(广东)有限公司

兹委托受托人1 姓名【FORMTEXT     】联系方式【FORMTEXT     】证件号码【FORMTEXT       】代表委托人处理委托人与FORMTEXT      办理Ρ签署《商用车租赁合同》(合同编号:FORMTEXT      )及前述合同相关包括但不限于《租赁订单》等合同附件、Ρ提车、Ρ退车、Ρ更换车、≤ FORMTEXT      事宜,授权期限自本授权委托书签署之日起至《商用车租赁合同》终止之日止。

兹委托受托人2 姓名【FORMTEXT     】联系方式【FORMTEXT      】证件号码【 FORMTEXT       】代表委托人处理委托人与FORMTEXT      办理≤签署《商用车租赁合同》(合同编号:FORMTEXT      )及前述合同相关包括但不限于《租赁订单》等合同附件、≤提车、≤退车、≤更换车、≤ 合同抬头变更事宜,授权期限自本授权委托书签署之日起至《商用车租赁合同》终止之日止。

上列受托人的代理权限为特别授权,即受托人有权代委托人签署《商用车租赁合同》及前述合同相关包括但不限于《租赁订单》等合同附件、有权代为履行前述合同及附件内容、有权代为签收、确认前述合同及其附件相关通知、函件、诉讼文件等。委托人对上列受托人在授权范围内的行为承担全部法律责任。

本授权委托书手填部分有效,委托人特此声明及确认。

委托人(签字/盖章):

年 月 日

安全责任协议

甲方(出租方):

经办人:

联系电话:

乙方(承租方):

统一社会信用代码/身份证号:

通信地址:

经办人:

联系电话:

为明确双方车辆租赁期间安全管理责任,保障运输作业安全,依据相关法律法规,在已签订《车辆租赁合同》同时,签订本协议,约定租赁车辆安全运营及管理责任。

一、协议目的

1.1本协议明确双方合作期间安全责任,确保企业主体责任履行,保障人员、车辆和货物安全,避免事故发生。

1.2本协议有效期与《车辆租赁合同》合同期限保持一致,租赁期间内乙方承担全部车辆相关的安全责任。

二、安全责任划分

2.1车辆交接时,乙方确认车辆状态,在交接单签字即视为认可车辆状况,甲方完成合格交付。自交付之日起,车辆运营安全责任及维修保养责任转移至乙方。

三、乙方安全承诺

乙方结合车辆安全和加氢安全实际,就安全生产做出承诺并制定规程、制度、预案等。

3.1安全管理制度

3.1.1乙方建立健全本单位安全生产责任制,成立安全生产管理机构、依据规定配备专职或兼职安全管理人员,明确各部门、各岗位安全职责。

3.1.2乙方制定本单位生产安全事故隐患排查治理制度,明确各层级隐患排查职责,及时消除安全隐患。

3.1.3乙方制定本单位交通事故应急救援预案,明确应急组织机构及职责,确定应急处置流程、储备相应应急物资。

3.1.4乙方制定本单位驾驶员安全培训制度,落实驾驶员岗前安全培训、考核工作,并保存驾驶员培训记录。

3.1.5乙方制定并落实安全生产、交通运输等法律规定的其他安全管理制度。

3.1.6乙方负责按照甲方的要求提供企业营业执照及必要资质、运营证件及个人驾驶证、身份证供甲方审查核验。

3.1.7甲方有权对每一位租赁车辆的驾驶员证件信息、有效期资质进行审核。

3.2 事故隐患排查治理

3.2.1乙方每月至少进行一次全面安全检查,重点检查安全管理制度是否完善,是否定期组织召开安全会议,是否对每位驾驶员开展岗前安全培训,是否定期开展隐患排查及隐患限期整改落实,是否至少每半年开展一次应急演练等安全事项。发现存在安全隐患立即通知整改,并立即抓好落实,及时消除。

3.2.2乙方做好出车前、停车后的准备、检查工作,确保行车安全,发现隐患要及时修复后方可出车。

3.2.3乙方装货时严查超载和擅自装载危险品,确认无误后方可出车。

3.2.4乙方不定期检查车辆的安全装置、灯光信号、证件。

3.2.5乙方检查驾驶员是否带病、疲劳、酒后驾驶,是否违反安全驾驶操作规程。

3.2.6乙方建立驾驶员安全考核奖惩制度,针对违章次数多、出险率较高的司机进行再教育或依制度进行处罚。

3.3 驾驶员管理

3.3.1乙方驾驶员必须遵守《中华人民共和国道路交通法》《中华人民共和国道路运输条例》及有关交通安全管理的规章规则,安全驾车,并应遵守其他相关的规章制度。

3.3.2 乙方与驾驶员签订聘书,聘用条件为:

(1)驾驶员须持有合法驾照;

(2)年龄不超过60周岁;

(3)驾驶4.5T以上车型须有从业资格证,且经乙方面试、理论和机械常识考核、道路驾驶技术测试合格。

3.3.3乙方应对驾驶员进行岗前及定期安全教育培训、考核,考核不及格的,不得上车驾驶,保存培训材料,对违章次数多、出险率高或发生交通事故的驾驶员进行再教育或依据公司制度进行处罚。

3.3.4 乙方应当对驾驶员进行安全生产教育和培训,保证驾驶员具备必要的安全生产知识,熟悉道路交通安全规章规则,掌握行车安全操作技能,了解事故应急处理措施,知悉自身在交通安全方面的权利和义务。未经安全教育和培训合格的驾驶员,乙方不得让其上岗作业。

3.3.5 乙方必须依法参加工伤保险,为驾驶员缴纳保险费。

3.4车辆安全管理要求

3.4.1车辆发生故障必须及时修复,特别是危及行车安全的重大隐患,如制动、转向、轮胎、灯光装置等。严禁车辆带病出场。

3.4.2严禁超员、超载行驶,严禁客货混装,严禁超速行驶。

3.4.3车辆在运行途中,驾驶员应主动做好车辆的自检自查,发现问题及时解决。

3.4.4车辆周围10米内严禁抽烟,严禁明火。进入加氢站加氢时按操作流程执行。

3.4.5车辆必须按规定位置停放,严禁停放在密封场所、低洼积水区域。车辆停放不得占据消防通道或妨碍消防通道畅通,不得将车辆停放在有人员居住的建筑物内,不得将车辆停放在可能造成人员伤亡的坡道路段,应确保车辆与爆破、吊装、起重、动火、高温等危险作业场所保持足够的安全距离。乙方违反本条事项要求所造成的人员伤亡、财产损失,均由乙方自行承担。

3.5车辆技术管理要求

3.5.1乙方应根据甲方保养要求及时对车辆组织保养维修,并及时将保养维修、车辆肇事情况通知甲方。

3.5.2车辆重要部位改动或加装部件,乙方应征得甲方同意并报甲方登记备案。

3.5.3甲方运维部管理人员检查车辆上路行驶情况时,在告知其身份后,司机必须停车配合,如发现车辆状况不佳或违反甲方安全管理规定的,甲方管理人员有对车辆进行停驶、回场检查的权利。

3.5.4 乙方要配合甲方对车辆进行年审、二级维护、检测、事故违章处理等工作。

3.6突发事件应急处置

为减少、避免事故的发生,把事故的损失降到最低程度,乙方及乙方驾驶员应做到:

3.6.1交通事故

驾驶员在车辆运行中发生交通事故时,停车后马上下车关闭车辆总电源,查看事故情况及是否有人员受伤,并将事故碰撞现场进行拍照留存,在车辆后方100米处放置警示标志。

发生交通事故,当事人应立即进行自救,并报警。电话: 122(交警)、119(消防)、120(急救),应简明讲清事故地点、伤亡、损失等情况,以及事故对周围环境的危害程度,保护现场,抢救伤员,保护货物财产并上报公司领导及甲方管理部门。

事故如果导致氢气压力异常和氢气管路有“嗤嗤声”等异常现象,发现车辆有漏气情况时,驾驶员要第一时间通知公司领导或甲方相关人员告知事故情况,甲方安排专业售后人员进行应急处理。

远离人群,事故车的处理地点应尽量避免在人口密集区,如只能在原地进行处理,应在周围设置提示牌。

安全撤离,当车辆发生明火现象时,驾驶人员应停车并关闭车辆总电源,并采取紧急灭火措施,灭火器应使用二氧化碳灭火器,对着火点进行喷射。如遇到无法扑灭时,立即疏散周围人员及车辆并撤离到安全地带拨打119报警,等待救援。

3.6.2车辆氢气泄漏

(1)驾驶员在车辆运行中发现氢气泄漏报警情况时,应就近安全区域停车。

(2)停车后马上关闭车辆总电源、在车辆后方100米处放置警示标志,并疏散周围人员车辆。

(3)驾驶员将情况通知公司领导或甲方相关人员告知,甲方安排专业售后人员进行应急处理。

3.6.3车辆运行中发生整车故障

驾驶员驾驶车辆行驶中发生整车故障报警情况时,应在安全地带停车。

停车后关闭车辆总电源,并电话联系甲方,甲方收到信息后安排售后人员电话指导驾驶员进行处理。如电话无法处理时,安排厂家前往现场进行处理或拖至服务站处理。

3.6.4火灾事故

(1)发生车辆火灾事故或出现火险苗头时,驾驶员应立即熄火有条件地迅速将车转移至安全地点,切断电源,关闭气阀。

(2)使用二氧化碳灭火器对着火点进行喷射。

(3)火灾现场在进行初期扑救的同时或发现火势较大,估计自身无法扑灭的,并撤离到安全地带,疏散周围人员及车辆。迅速拨打火警电话报警,并通知公司领导及甲方管理部门。

(4)如有受伤人员,必须首先将伤员抢救出来,及时送往附近医院进行救治。

(5)公安消防人员到达现场后,应急救援人员应服从专业消防人员的指挥,在消防人员的统一组织下,积极参加火灾事故的救援工作。

3.7 禁止第三方使用

3.7.1 严格禁止乙方擅自将甲方车辆转租、分租、出借或以联营、合作、借用等形式变相交由第三方使用。如有违反,甲方有权立即解除合同、收回车辆,并要求乙方支付相当于三个月租金的违约金,且不影响其他损失赔偿权利。

四、甲方安全管理责任和权利

4.1 甲方有权审查乙方拟租赁车型是否在乙方经营许可范围之内,审查所交车型是否与乙方提车人驾驶证准驾车型匹配,驾驶证、从业资格证是否在有效期内。

4.2甲方负责按照《车辆可交付标准》对乙方拟租赁车辆进行整备,确保车辆性能完好、安全附件齐备。

4.3甲方负责指引乙方按照“交车单”内容项进行实物及车况确认,无误后甲乙双方在交车单上签字。

4.4甲方负责通知乙方所承租车辆的保养、年审、违法处理等信息。

4.5乙方在履行安全管理责任时,若需要甲方协助的,甲方在自身能力范围内且在不影响自身正常运营的情况下,对乙方提供必要的技术指导和信息支持等协助。

4.6乙方承诺自愿按照“乙方安全承诺”的规范内容进行操作,对于违反“乙方安全承诺”内容的操作行为,乙方应向甲方支付相当于三个月租金的违约金;若该违约金不足以弥补甲方损失的,乙方还应赔偿甲方因此遭受的全部损失。

4.7 尽管有上述规定,甲方是否行使甲方的安全管理责任和权利以及如何行使甲方的安全管理责任和权利,无论如何均不影响乙方根据本协议及“乙方安全承诺”履行其安全管理责任,也不减免或减轻其任何安全管理责任。

五、其他

5.1 本协议的任何变更或补充需经双方书面协商一致,并签订相关协议变更或补充文件。

5.2 本协议为《车辆租赁合同》的附件,本协议未约定的,以《车辆租赁合同》约定为准;若本协议约定与《车辆租赁合同》约定不一致的,以本协议约定为准。

5.3 本协议经双方签字(盖章)之日起生效,一式两份,甲乙双方各执一份,具有同等法律效力。

甲方(签章):

乙方(签字/盖章):

授权代表:

授权代表:

签订日期:

年 月 日

签订日期: 年 月 日

附:承租方(乙方)营业执照、法定代表人身份证复印件(若有授权书,需附被授权人身份证复印件)、道路运输经营许可证等。

" }, + ]; + } + + function defaultLeaseBlocks() { + return v265LeaseBlocks(); + } + function defaultPurchaseBlocks() { + return [ + { id: 'p1', title: '合同标题', content: '车辆采购合同', locked: true }, + { id: 'p2', title: '合同编号', content: '合同编号:{{contractCode}}', locked: true }, + { id: 'p3', title: '买方信息', content: '买方:浙江羚牛氢能科技有限公司\n地址:浙江省嘉兴市南湖区科技大道 1 号', locked: true }, + { id: 'p4', title: '卖方信息', content: '卖方:{{supplierName}}\n统一社会信用代码:{{supplierCreditCode}}', locked: false }, + { id: 'p5', title: '采购标的', content: '一、采购标的\n车辆型号:{{vehicleModel}}\n数量:{{quantity}} 台\n单价:¥{{unitPrice}}', locked: false }, + { id: 'p6', title: '交付条款', content: '二、交付\n交付地点:{{deliveryPlace}}\n交付日期:{{deliveryDate}}', locked: false }, + { id: 'p7', title: '付款方式', content: '三、付款方式\n{{paymentTerms}}', locked: true }, + { id: 'p8', title: '签章页', content: '买方(盖章):____________ 卖方(盖章):____________', locked: true } + ]; + } + + function defaultServiceBlocks() { + return [ + { id: 's1', title: '合同标题', content: '氢能车辆运维服务合同', locked: true }, + { id: 's2', title: '合同编号', content: '合同编号:{{contractCode}}', locked: true }, + { id: 's3', title: '委托方', content: '委托方(甲方):{{customerName}}', locked: false }, + { id: 's4', title: '服务方', content: '服务方(乙方):浙江羚牛氢能科技有限公司', locked: true }, + { id: 's5', title: '服务内容', content: '一、服务内容\n包括但不限于车辆日常运维、故障响应、定期巡检等。', locked: false }, + { id: 's6', title: '服务期限', content: '二、服务期限\n自 {{serviceStart}} 至 {{serviceEnd}}', locked: false }, + { id: 's7', title: '服务费用', content: '三、服务费用\n服务费合计:¥{{serviceFee}}', locked: false }, + { id: 's8', title: '违约责任', content: '四、违约责任\n(标准条款,锁定不可修改)', locked: true } + ]; + } + + function getDefaultBlocksForCategory(category) { + if (category === 'purchase') return defaultPurchaseBlocks(); + if (category === 'service') return defaultServiceBlocks(); + return defaultLeaseBlocks(); + } + + function mockWordBlocks(fileName, category) { + var cat = category || 'lease'; + var name = fileName || 'Word'; + if (cat === 'lease' && (/V26\.5|现代18吨|数智中心版/i.test(name) || /商用车租赁合同/.test(name))) { + return JSON.parse(JSON.stringify(v265LeaseBlocks())); + } + var base = getDefaultBlocksForCategory(cat); + base[0].content = base[0].content + '(导入自 ' + name + ')'; + return base; + } + + var templatesState = useState([ + { + id: 't8', + category: 'lease', + versionNo: 'v1.00', + versionName: '现代18吨车型-数智中心版', + status: 'published', + isDefault: true, + blocks: v265LeaseBlocks(), + creator: '法务-王静', + createTime: '2026-05-25 09:13', + updater: '法务-王静', + updateTime: '2026-05-25 09:13', + remark: '参照 Word 原版导入;含现代18吨车型专项条款、租金减免政策及全套附件' + }, + { + id: 't1', + category: 'lease', + versionNo: 'v1.01', + versionName: '2026年标准商用车租赁合同', + status: 'published', + isDefault: false, + blocks: defaultLeaseBlocks(), + creator: '法务-王静', + createTime: '2026-03-15 14:30', + updater: '法务-王静', + updateTime: '2026-03-15 14:30', + remark: '客服草拟合同时默认使用;含氢能车辆专项条款' + }, + { + id: 't2', + category: 'lease', + versionNo: 'v1.02', + versionName: '2025年修订版', + status: 'published', + isDefault: false, + blocks: defaultLeaseBlocks(), + creator: '法务-王静', + createTime: '2025-11-08 10:20', + updater: '法务-李明', + updateTime: '2025-12-01 16:45', + remark: '历史发布版本' + }, + { + id: 't3', + category: 'lease', + versionNo: null, + versionName: '2026年二季度草案', + status: 'draft', + isDefault: false, + blocks: defaultLeaseBlocks(), + creator: '法务-李明', + createTime: '2026-06-01 09:15', + updater: '法务-李明', + updateTime: '2026-06-01 09:15', + remark: '待法务审核发布' + }, + { + id: 't4', + category: 'purchase', + versionNo: 'v1.00', + versionName: '2026年车辆采购标准版', + status: 'published', + isDefault: true, + blocks: defaultPurchaseBlocks(), + creator: '法务-王静', + createTime: '2026-04-10 11:00', + updater: '法务-王静', + updateTime: '2026-04-10 11:00', + remark: '采购类标准模板' + }, + { + id: 't5', + category: 'purchase', + versionNo: 'v1.01', + versionName: '2025年采购版', + status: 'published', + isDefault: false, + blocks: defaultPurchaseBlocks(), + creator: '法务-李明', + createTime: '2025-09-20 16:40', + updater: '法务-李明', + updateTime: '2025-09-20 16:40', + remark: '' + }, + { + id: 't6', + category: 'service', + versionNo: 'v1.00', + versionName: '运维服务标准合同', + status: 'published', + isDefault: true, + blocks: defaultServiceBlocks(), + creator: '法务-王静', + createTime: '2026-02-28 09:30', + updater: '法务-王静', + updateTime: '2026-02-28 09:30', + remark: '服务类标准模板' + }, + { + id: 't7', + category: 'service', + versionNo: null, + versionName: '加氢服务草案', + status: 'draft', + isDefault: false, + blocks: defaultServiceBlocks(), + creator: '法务-李明', + createTime: '2026-05-18 15:20', + updater: '法务-李明', + updateTime: '2026-05-18 15:20', + remark: '加氢站服务专项条款待补充' + } + ]); + var templates = templatesState[0]; + var setTemplates = templatesState[1]; + + var categoryFilterState = useState('lease'); + var categoryFilter = categoryFilterState[0]; + var setCategoryFilter = categoryFilterState[1]; + + var pageViewState = useState('list'); + var pageView = pageViewState[0]; + var setPageView = pageViewState[1]; + + var editorModeState = useState('create'); + var editorMode = editorModeState[0]; + var setEditorMode = editorModeState[1]; + + var editingRecordState = useState(null); + var editingRecord = editingRecordState[0]; + var setEditingRecord = editingRecordState[1]; + + var lastImportFileNameState = useState(''); + var importChangedInSessionState = useState(false); + var formBlocksState = useState([]); + var formDocumentHtmlState = useState(''); + + var deleteModalState = useState(false); + var deleteTargetState = useState(null); + var defaultConfirmModalState = useState(false); + var defaultConfirmTargetState = useState(null); + var editorSyncKeyState = useState(0); + var editorInsertHandlerRef = useRef(null); + var registerEditorInsertRef = useRef(function (fn) { editorInsertHandlerRef.current = fn; }); + var placeholderPopoverOpenState = useState(false); + var previewLessorIdState = useState('gd'); + var previewLessorId = previewLessorIdState[0]; + var setPreviewLessorId = previewLessorIdState[1]; + + + var CURRENT_USER = '法务-当前用户'; + + var pageStyle = ONEOS_ANT_TABLE_GLOBAL_FIX.concat([ + '.oneos-page.ct-page,.ct-page{font-family:Inter,"PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;color:#0f172a}' + + '.ct-page *{box-sizing:border-box}' + + '.ct-category-tabs .ant-tabs-nav::before{border-color:#E2E8F0}' + + '.ct-category-tabs .ant-tabs-tab{border-radius:8px 8px 0 0!important;transition:color .2s ease}' + + '.ct-category-tabs .ant-tabs-tab-active .ant-tabs-tab-btn{color:#10b981!important;font-weight:600}' + + '.ct-category-tabs .ant-tabs-ink-bar{background:#10b981;height:3px;border-radius:3px 3px 0 0}' + + '.ct-data-table .ant-table-thead>tr>th{background:#F8FAFC!important;font-weight:600;font-size:13px;color:#475569}' + + '.ct-data-table .ant-table-tbody>tr.ct-row-hover:hover>td{background:#F8FAFC!important}' + + '.ct-data-table .ant-table-tbody>tr>td{font-size:13px;transition:background .15s ease}' + + '.ct-panel-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid #E2E8F0}' + + '.ct-panel-head__title{font-size:13px;font-weight:600;color:#334155;display:flex;align-items:center;gap:8px}' + + '.ct-panel-head__badge{font-size:11px;font-weight:500;color:#64748B;background:#F1F5F9;padding:2px 8px;border-radius:999px}' + + '.ct-modal-form-label{display:block;margin-bottom:6px;font-size:12px;font-weight:500;color:#64748B}' + + '.ct-rich-toolbar{background:#fff;border:1px solid #E8E8E8;border-bottom:none;border-radius:8px 8px 0 0;padding:4px 8px 2px}' + + '.ct-rich-toolbar__row{display:flex;align-items:center;flex-wrap:nowrap;gap:1px;min-height:34px;padding:2px 0;overflow-x:auto}' + + '.ct-rich-toolbar__row+.ct-rich-toolbar__row{border-top:1px solid #F0F0F0;margin-top:2px;padding-top:4px}' + + '.ct-rte-sep{width:1px;height:18px;background:#E8E8E8;margin:0 5px;flex-shrink:0}' + + '.ct-rte-btn{display:inline-flex;align-items:center;justify-content:center;min-width:28px;height:28px;border:none;background:transparent;border-radius:4px;color:#595959;cursor:pointer;padding:0 4px;transition:background .15s,color .15s;flex-shrink:0;font:inherit}' + + '.ct-rte-btn:hover{background:#F5F5F5;color:#262626}' + + '.ct-rte-btn.is-active{background:#ecfdf5;color:#10b981}' + + '.ct-rte-btn:disabled{opacity:.35;cursor:not-allowed}' + + '.ct-rte-btn:focus-visible{outline:2px solid #10b981;outline-offset:1px}' + + '.ct-rte-btn--dropdown{gap:2px;padding:0 4px 0 6px}' + + '.ct-rte-letter{display:inline-flex;flex-direction:column;align-items:center;justify-content:center;font-size:13px;font-weight:600;line-height:1;min-width:14px;font-family:"Inter, "PingFang SC", "Microsoft YaHei"",sans-serif}' + + '.ct-rte-letter.is-italic{font-style:italic;font-weight:500}' + + '.ct-rte-letter__bar{display:block;width:14px;height:2px;margin-top:1px;border-radius:1px;background:#595959}' + + '.ct-rte-letter--highlight{background:#FEF08A;border-radius:2px;padding:0 2px}' + + '.ct-rte-svg{display:block;flex-shrink:0}' + + '.ct-rte-svg--chevron{width:10px;height:10px;opacity:.45;margin-left:-2px}' + + '.ct-rte-align-group{display:inline-flex;align-items:center;background:#FAFAFA;border-radius:4px;padding:2px;gap:1px;flex-shrink:0}' + + '.ct-rte-align-group .ct-rte-btn{min-width:26px;height:26px}' + + '.ct-rte-select.ant-select{font-size:13px;color:#262626;flex-shrink:0}' + + '.ct-rte-select .ant-select-selector{background:transparent!important;border:none!important;box-shadow:none!important;padding:0 18px 0 6px!important;height:28px!important;min-height:28px!important}' + + '.ct-rte-select .ant-select-selection-item,.ct-rte-select .ant-select-selection-placeholder{line-height:28px!important;font-size:13px;color:#262626}' + + '.ct-rte-select .ant-select-arrow{color:#8C8C8C;right:4px}' + + '.ct-rte-select--size{width:68px}' + + '.ct-rte-select--font{width:108px}' + + '.ct-rte-dropdown-wrap{display:inline-flex;align-items:center}' + + '.ct-rich-toolbar .ant-btn:focus-visible,.ct-page .ant-btn:focus-visible{outline:2px solid #10b981;outline-offset:2px}' + + '@media(max-width:992px){.ct-editor-panel,.ct-preview-panel{max-height:none}.ct-rich-editor{min-height:320px;max-height:420px}}' + + '@media(prefers-reduced-motion:reduce){.ct-data-table .ant-table-tbody>tr>td,.ct-rich-toolbar .ant-btn{transition:none}}' + + '.ct-preview-doc{background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:32px 40px;min-height:520px;box-shadow:0 2px 12px rgba(15,23,42,0.06);font-size:14px;line-height:1.85;color:#1e293b}' + + '.ct-preview-doc h1{text-align:center;font-size:20px;font-weight:700;margin:0 0 24px;color:#0f172a}' + + '.ct-preview-section{margin-bottom:18px;word-break:break-word}' + + '.ct-preview-section__body{line-height:1.85}' + + '.ct-preview-section__body p{margin:0 0 10px}' + + '.ct-preview-section__body strong{font-weight:700;color:#0f172a}' + + '.ct-preview-section__body em{font-style:italic}' + + '.ct-preview-section__body img{max-width:100%;height:auto;margin:10px 0;border-radius:4px;display:block}' + + '.ct-preview-doc table.ct-doc-table,.ct-rich-editor table.ct-doc-table{width:100%;border-collapse:collapse;margin:12px 0;font-size:13px}' + + '.ct-preview-doc table.ct-doc-table td,.ct-preview-doc table.ct-doc-table th,.ct-rich-editor table.ct-doc-table td,.ct-rich-editor table.ct-doc-table th{border:1px solid #cbd5e1;padding:6px 10px;min-width:48px;vertical-align:top}' + + '.ct-preview-doc table.ct-doc-table th,.ct-rich-editor table.ct-doc-table th{background:#f1f5f9;font-weight:600}' + + '.ct-preview-doc h2,.ct-rich-editor h2{font-size:18px;font-weight:700;margin:16px 0 10px}' + + '.ct-preview-doc h3,.ct-rich-editor h3{font-size:16px;font-weight:600;margin:14px 0 8px}' + + '.ct-preview-doc ul,.ct-preview-doc ol,.ct-rich-editor ul,.ct-rich-editor ol{margin:8px 0;padding-left:24px}' + + '.ct-preview-doc blockquote,.ct-rich-editor blockquote{margin:10px 0;padding:8px 14px;border-left:3px solid #cbd5e1;background:#f8fafc;color:#475569}' + + '.ct-preview-doc a,.ct-rich-editor a{color:#2563eb;text-decoration:underline}' + + '.ct-inline-lock{background:#fef3c7;border-bottom:1px dashed #f59e0b;padding:0 2px;border-radius:2px}' + + '.ct-preview-doc .ct-inline-lock{background:#f8fafc;border-left:2px solid #94a3b8;padding:2px 4px}' + + '.ct-rich-editor .ct-risk-redline,.ct-preview-doc .ct-risk-redline,.ct-preview-page__body .ct-risk-redline,.ct-word-doc .ct-risk-redline{background:rgba(254,226,226,0.4);border:1px dashed #f87171;border-radius:3px;padding:0 2px;box-decoration-break:clone;-webkit-box-decoration-break:clone}' + + '.ct-rte-btn--text{width:auto;padding:0 10px;font-size:12px;font-weight:600;color:#dc2626;border:1px solid #fecaca;background:#fef2f2;border-radius:4px;min-width:auto;height:28px;gap:4px}' + + '.ct-rte-btn--text:hover{background:#fee2e2;border-color:#fca5a5;color:#b91c1c}' + + '.ct-rte-btn--text.is-muted{color:#64748B;border-color:#E2E8F0;background:#F8FAFC;font-weight:500}' + + '.ct-rte-btn--text.is-muted:hover{background:#F1F5F9;color:#475569}' + + '.ct-editor-panel{border:1px solid #e2e8f0;border-radius:12px;background:#fafbfc;max-height:calc(100vh - 280px);overflow:hidden;padding:0}' + + '.ct-preview-panel{border:1px solid #e2e8f0;border-radius:12px;background:#f1f5f9;max-height:calc(100vh - 280px);overflow:auto;padding:16px}' + + '.ct-category-tabs .ant-tabs-nav{margin-bottom:0}' + + '.ct-category-tabs .ant-tabs-tab{font-size:14px;padding:10px 4px}' + + '.ct-tab-summary{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;padding:14px 0 16px;border-bottom:1px solid #E2E8F0;margin-bottom:14px}' + + '.ct-tab-summary__meta{font-size:13px;color:#64748B;line-height:1.5}' + + '.ct-tab-summary__default{display:flex;align-items:center;flex-wrap:wrap;gap:8px}' + + '.ct-tab-label{display:inline-flex;align-items:center;gap:6px}' + + '.ct-rich-wrap{margin-top:4px}' + + '.ct-rich-editor{flex:1;min-height:calc(100vh - 380px);max-height:calc(100vh - 320px);overflow-y:auto;padding:16px 20px;border:1px solid #e2e8f0;border-top:none;border-radius:0 0 8px 8px;background:#fff;font-size:14px;line-height:1.75;outline:none;word-break:break-word}' + + '.ct-rich-editor:focus{border-color:#10b981;box-shadow:0 0 0 2px rgba(37,99,235,0.15)}' + + '.ct-rich-editor.is-readonly{background:#f8fafc;color:#475569;cursor:default}' + + '.ct-rich-editor img{max-width:100%;height:auto;margin:8px 0;border-radius:4px;display:block}' + + '.ct-rich-wrap.is-fullscreen{position:fixed;inset:0;z-index:1100;background:#fff;padding:12px;display:flex;flex-direction:column}' + + '.ct-rich-wrap.is-fullscreen .ct-rich-editor{max-height:none;min-height:0;flex:1}' + + '.ct-rich-wrap.is-fullscreen .ct-rich-toolbar{border-radius:8px}' + + '.ct-rich-source{width:100%;min-height:calc(100vh - 380px);max-height:calc(100vh - 320px);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;line-height:1.6;padding:14px 16px;border:1px solid #E2E8F0;border-top:none;border-radius:0 0 8px 8px;resize:vertical;outline:none}' + + '.ct-rich-source:focus{border-color:#10b981;box-shadow:0 0 0 2px rgba(37,99,235,0.15)}' + + '.ct-rich-statusbar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:6px 12px;font-size:12px;color:#64748B;background:#F8FAFC;border:1px solid #E2E8F0;border-top:none;border-radius:0 0 8px 8px}' + + '.ct-rich-statusbar__stats{display:flex;gap:16px;flex-wrap:wrap}' + + '.ct-page-break{display:block;height:0;border:none;border-top:2px dashed #CBD5E1;margin:24px 0;page-break-after:always}' + + '.ct-preview-doc .ct-page-break{border-top-color:#94A3B8}' + + '.ct-preview-doc.ct-word-doc,.ct-preview-doc .ct-word-doc{font-family:FangSong,STFangsong,"仿宋",SimSun,"宋体",serif;font-size:10.5pt;line-height:1.5;color:#000;letter-spacing:0}' + + '.ct-preview-doc.ct-word-doc{padding:48px 56px 56px}' + + '.ct-word-doc p{margin:0 0 4px}' + + '.ct-word-title{text-align:center;font-size:18pt;font-weight:700;margin:0 0 10px!important;line-height:1.4}' + + '.ct-word-contract-no{text-align:right;margin:0 0 14px!important;padding-right:1em;font-size:10.5pt}' + + '.ct-word-party-line{margin:0!important;line-height:1.8;font-size:10.5pt}' + + '.ct-word-body{text-align:justify;text-justify:inter-ideograph;margin:0 0 4px!important}' + + '.ct-word-clause-indent,.ct-word-clause-indent2{text-align:justify;text-justify:inter-ideograph;text-indent:2em;margin:0 0 4px!important}' + + '.ct-word-clause-title{text-align:justify;text-justify:inter-ideograph;margin:6px 0 4px!important;font-weight:700}' + + '.ct-word-table{width:100%;border-collapse:collapse;margin:6px 0 12px;table-layout:fixed}' + + '.ct-word-table .ct-word-party-left,.ct-word-table .ct-word-party-right{width:50%;border:none!important;padding:2px 6px;vertical-align:top}' + + '.ct-word-table .ct-word-party-left{padding-right:12px}' + + '.ct-word-table .ct-word-party-right{padding-left:12px}' + + '.ct-word-table .ct-word-td{border:1px solid #bfbfbf;padding:4px 6px;vertical-align:top;font-size:10.5pt}' + + '.ct-word-table .ct-word-td.ct-word-td-full,.ct-word-table td[colspan]{width:100%}' + + '.ct-word-table .ct-word-td p{margin:0!important}' + + '.ct-preview-doc.ct-word-doc b,.ct-preview-doc.ct-word-doc strong{font-weight:700}' + + '.ct-preview-doc.ct-word-doc .ct-preview-section__body{line-height:1.5}' + + '.ct-preview-doc.ct-word-paginated{padding:0!important;background:transparent!important;border:none!important;box-shadow:none!important;min-height:0!important}' + + '.ct-preview-pager{display:flex;align-items:center;justify-content:center;gap:10px;margin-bottom:12px;flex-wrap:wrap}' + + '.ct-preview-pager__info{font-size:12px;color:#64748B;min-width:88px;text-align:center}' + + '.ct-preview-pages{display:flex;flex-direction:column;align-items:center;gap:20px;padding:4px 0 12px}' + + '.ct-preview-page{width:210mm;min-height:297mm;padding:25.4mm 31.8mm;box-sizing:border-box;background:#fff;border:1px solid #d9d9d9;box-shadow:0 2px 12px rgba(0,0,0,0.12);position:relative;flex-shrink:0}' + + '.ct-preview-page__body{width:100%;overflow:hidden;word-break:break-word}' + + '.ct-preview-page__footer{position:absolute;bottom:10mm;left:0;right:0;text-align:center;font-size:9pt;color:#999;font-family:FangSong,STFangsong,"仿宋",SimSun,"宋体",serif}' + + '.ct-preview-page.is-loading{min-height:120px;display:flex;align-items:center;justify-content:center;color:#94A3B8;font-size:13px;width:210mm}' + + '.ct-preview-panel--word{background:#b8b8b8;padding:16px 12px}' + + '.ct-rich-editor .ct-word-doc,.ct-rich-editor .ct-word-doc{font-family:FangSong,STFangsong,"仿宋",SimSun,"宋体",serif;font-size:10.5pt;line-height:1.5;color:#000}' + + '.ct-rich-editor .ct-word-title{text-align:center;font-size:18pt;font-weight:700}' + + '.ct-rich-editor .ct-word-contract-no{text-align:right;padding-right:1em}' + + '.ct-rich-editor .ct-word-clause-indent,.ct-rich-editor .ct-word-clause-indent2{text-indent:2em;text-align:justify}' + + '.ct-rich-editor .ct-word-table{width:100%;border-collapse:collapse;margin:6px 0}' + + '.ct-rich-editor .ct-word-table .ct-word-party-left,.ct-rich-editor .ct-word-table .ct-word-party-right{width:50%;border:none!important;vertical-align:top}' + + '.ct-rich-editor .ct-word-table .ct-word-td{border:1px solid #bfbfbf;padding:4px 6px;vertical-align:top}' + + '.ct-placeholder-card{width:320px;max-height:420px;overflow:auto}' + + '.ct-placeholder-card__head{margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid #E2E8F0}' + + '.ct-placeholder-card__title{font-size:13px;font-weight:600;color:#334155;margin:0 0 4px}' + + '.ct-placeholder-card__desc{font-size:12px;color:#64748B;margin:0;line-height:1.5}' + + '.ct-placeholder-card__group{margin-bottom:12px}' + + '.ct-placeholder-card__group:last-child{margin-bottom:0}' + + '.ct-placeholder-card__group-title{font-size:12px;font-weight:600;color:#475569;margin:0 0 6px}' + + '.ct-placeholder-card__group-desc{font-size:11px;color:#94A3B8;margin:0 0 8px;line-height:1.45}' + + '.ct-placeholder-card__item{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 10px;border:1px solid #E2E8F0;border-radius:8px;background:#fff;cursor:pointer;transition:background .15s,border-color .15s,box-shadow .15s;margin-bottom:6px}' + + '.ct-placeholder-card__item:last-child{margin-bottom:0}' + + '.ct-placeholder-card__item:hover{background:#F8FAFC;border-color:#CBD5E1;box-shadow:0 1px 4px rgba(15,23,42,.06)}' + + '.ct-placeholder-card__item-main{min-width:0;flex:1}' + + '.ct-placeholder-card__item-label{font-size:12px;color:#334155;font-weight:500}' + + '.ct-placeholder-card__item-token{font-size:11px;color:#64748B;font-family:ui-monospace,Menlo,monospace;margin-top:2px;word-break:break-all}' + + '.ct-placeholder-card__item-actions{display:flex;gap:4px;flex-shrink:0}' + + '.ct-placeholder-card__action{font-size:11px;color:#64748B;padding:2px 6px;border-radius:4px;border:1px solid #E2E8F0;background:#F8FAFC;cursor:pointer}' + + '.ct-placeholder-card__action:hover{color:#10b981;border-color:#bbf7d0;background:#ecfdf5}' + + '.ct-placeholder-trigger.ant-btn{display:inline-flex;align-items:center;gap:6px}' + + '.ct-preview-lessor-bar{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px;flex-wrap:wrap}' + + '.ct-preview-lessor-bar__label{font-size:12px;color:#64748B;white-space:nowrap}' + + '.ct-editor-page{display:flex;flex-direction:column;min-height:calc(100vh - 48px)}' + + '.ct-editor-page__topbar{margin-bottom:12px}' + + '.ct-editor-page__back-btn.ant-btn{padding:4px 12px;height:32px;font-size:13px;color:#475569;border-color:#E2E8F0;background:#fff}' + + '.ct-editor-page__back-btn.ant-btn:hover{color:#059669;border-color:#bbf7d0;background:#ecfdf5}' + + '.ct-editor-page__header{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:16px;padding:12px 16px;background:#fff;border:1px solid #E2E8F0;border-radius:12px;flex-wrap:wrap}' + + '.ct-panel-head__actions{display:flex;align-items:center;gap:8px;flex-shrink:0}' + + '.ct-editor-page__title{margin:0;font-size:18px;font-weight:700;color:#0F172A;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' + + '.ct-editor-page__footer{position:sticky;bottom:0;margin-top:16px;padding:12px 16px;background:#fff;border:1px solid #E2E8F0;border-radius:12px;display:flex;justify-content:flex-end;box-shadow:0 -4px 12px rgba(15,23,42,.04);z-index:2}' + + '.ct-editor-page .ct-editor-panel,.ct-editor-page .ct-preview-panel{max-height:calc(100vh - 220px)}' + + '.ct-editor-page .ct-rich-editor,.ct-editor-page .ct-rich-source{min-height:calc(100vh - 300px);max-height:calc(100vh - 260px)}' + ]).join('\n'); + + var categoryStats = useMemo(function () { + var map = {}; + CATEGORY_OPTIONS.forEach(function (c) { + var list = templates.filter(function (t) { return t.category === c.value; }); + var def = list.find(function (t) { return t.isDefault && t.status === 'published'; }); + map[c.value] = { + count: list.length, + defaultVersion: def ? def.versionNo : '—', + defaultName: def ? def.versionName : '未设置' + }; + }); + return map; + }, [templates]); + + function statusTag(status) { + if (status === 'published') return React.createElement(Tag, { color: 'success' }, '已发布'); + if (status === 'draft') return React.createElement(Tag, { color: 'processing' }, '草稿'); + return React.createElement(Tag, null, '已归档'); + } + + function loadDocumentFromBlocks(blocks) { + var html = mergeBlocksToDocument(blocks); + formDocumentHtmlState[1](html); + formBlocksState[1](blocks); + } + + function parseVersionNo(versionNo) { + var m = String(versionNo || '').match(/^[vV](\d+)\.(\d{1,2})$/); + if (!m) return null; + return { major: parseInt(m[1], 10), minor: parseInt(m[2], 10) }; + } + + function formatVersionNo(major, minor) { + return 'v' + major + '.' + String(minor).padStart(2, '0'); + } + + function formatNowTime() { + return '2026-06-09 ' + new Date().toTimeString().slice(0, 5); + } + + function generateNextVersionNo(category) { + var list = templates.filter(function (t) { + return t.category === category && t.status === 'published' && t.versionNo; + }); + var maxMajor = 0; + var maxMinor = -1; + list.forEach(function (t) { + var parsed = parseVersionNo(t.versionNo); + if (!parsed) return; + if (parsed.major > maxMajor || (parsed.major === maxMajor && parsed.minor > maxMinor)) { + maxMajor = parsed.major; + maxMinor = parsed.minor; + } + }); + if (maxMinor < 0) return 'v1.00'; + if (maxMinor >= 99) return formatVersionNo(maxMajor + 1, 0); + return formatVersionNo(maxMajor, maxMinor + 1); + } + + function stripFileExtension(name) { + return String(name || '').replace(/\.[^.]+$/, ''); + } + + function buildAutoVersionName(category, versionNo, importFileName) { + var imported = stripFileExtension(importFileName); + if (imported) return imported; + var catOpt = CATEGORY_OPTIONS.find(function (c) { return c.value === category; }); + if (!versionNo) return (catOpt ? catOpt.label : '合同模板') + ' 草稿'; + return (catOpt ? catOpt.label : '合同模板') + ' ' + versionNo; + } + + function openCreate() { + setEditorMode('create'); + setEditingRecord(null); + lastImportFileNameState[1](''); + importChangedInSessionState[1](false); + formDocumentHtmlState[1](''); + formBlocksState[1]([]); + editorSyncKeyState[1](function (v) { return v + 1; }); + setPageView('editor'); + } + + function openEdit(record) { + setEditorMode('edit'); + setEditingRecord(record); + lastImportFileNameState[1](''); + importChangedInSessionState[1](false); + loadDocumentFromBlocks(JSON.parse(JSON.stringify(record.blocks || getDefaultBlocksForCategory(record.category)))); + setPageView('editor'); + } + + function handleImportWord(file) { + var name = file && file.name ? file.name : 'Word'; + var cat = editingRecord ? editingRecord.category : categoryFilter; + lastImportFileNameState[1](name); + importChangedInSessionState[1](true); + loadDocumentFromBlocks(mockWordBlocks(name, cat)); + editorSyncKeyState[1](function (v) { return v + 1; }); + message.success('已导入 Word 并合并为单一文档(原型模拟)'); + return false; + } + + function syncDocumentHtml(html) { + formDocumentHtmlState[1](html); + formBlocksState[1](documentToBlocks(html)); + } + + function handleSaveTemplate(publish) { + var docHtml = (formDocumentHtmlState[0] || '').trim(); + if (!docHtml) { + message.warning('请先导入 Word 并解析到编辑器'); + return; + } + var blocks = documentToBlocks(docHtml); + var now = formatNowTime(); + var importName = lastImportFileNameState[0] || ''; + + if (editorMode === 'create') { + if (publish) { + var versionNo = generateNextVersionNo(categoryFilter); + var newItem = { + id: 't' + Date.now(), + category: categoryFilter, + versionNo: versionNo, + versionName: buildAutoVersionName(categoryFilter, versionNo, importName), + status: 'published', + isDefault: false, + blocks: JSON.parse(JSON.stringify(blocks)), + creator: CURRENT_USER, + createTime: now, + updater: CURRENT_USER, + updateTime: now, + remark: '' + }; + setTemplates(function (prev) { return prev.concat([newItem]); }); + message.success('已发布,版本号 ' + versionNo); + } else { + var draftItem = { + id: 't' + Date.now(), + category: categoryFilter, + versionNo: null, + versionName: buildAutoVersionName(categoryFilter, null, importName), + status: 'draft', + isDefault: false, + blocks: JSON.parse(JSON.stringify(blocks)), + creator: CURRENT_USER, + createTime: now, + updater: CURRENT_USER, + updateTime: now, + remark: '' + }; + setTemplates(function (prev) { return prev.concat([draftItem]); }); + message.success('草稿已保存(未生成版本号)'); + } + } else if (editingRecord) { + if (publish) { + if (editingRecord.status === 'published') { + var cat = editingRecord.category; + var nextVersionNo = generateNextVersionNo(cat); + var newVersion = { + id: 't' + Date.now(), + category: cat, + versionNo: nextVersionNo, + versionName: buildAutoVersionName(cat, nextVersionNo, importName || editingRecord.versionName), + status: 'published', + isDefault: false, + blocks: JSON.parse(JSON.stringify(blocks)), + creator: CURRENT_USER, + createTime: now, + updater: CURRENT_USER, + updateTime: now, + remark: editingRecord.remark || '' + }; + setTemplates(function (prev) { return prev.concat([newVersion]); }); + message.success('已发布新版本 ' + nextVersionNo + ',原版本保留可回滚'); + } else { + var publishVersionNo = generateNextVersionNo(editingRecord.category); + setTemplates(function (prev) { + return prev.map(function (t) { + if (t.id !== editingRecord.id) return t; + return Object.assign({}, t, { + versionNo: publishVersionNo, + versionName: buildAutoVersionName(t.category, publishVersionNo, importName || t.versionName), + status: 'published', + blocks: JSON.parse(JSON.stringify(blocks)), + updater: CURRENT_USER, + updateTime: now + }); + }); + }); + message.success('已发布,版本号 ' + publishVersionNo); + } + } else { + setTemplates(function (prev) { + return prev.map(function (t) { + if (t.id !== editingRecord.id) return t; + var patch = { + blocks: JSON.parse(JSON.stringify(blocks)), + updater: CURRENT_USER, + updateTime: now, + status: 'draft', + versionNo: null + }; + if (importName) { + patch.versionName = buildAutoVersionName(t.category, null, importName); + } + return Object.assign({}, t, patch); + }); + }); + message.success('草稿已保存(未生成版本号)'); + } + } + backToList(); + } + + function backToList() { + setPageView('list'); + placeholderPopoverOpenState[1](false); + editorInsertHandlerRef.current = null; + } + + function confirmSetDefault(record) { + if (record.status !== 'published') { + message.warning('仅已发布版本可设为标准模板'); + return; + } + defaultConfirmTargetState[1](record); + defaultConfirmModalState[1](true); + } + + function handleSetDefault() { + var record = defaultConfirmTargetState[0]; + if (!record) return; + setTemplates(function (prev) { + return prev.map(function (t) { + if (t.category !== record.category) return t; + return Object.assign({}, t, { isDefault: t.id === record.id }); + }); + }); + defaultConfirmModalState[1](false); + defaultConfirmTargetState[1](null); + message.success('已设为标准模板:' + record.versionNo); + } + + function confirmDelete(record) { + if (record.isDefault) { + message.warning('当前为标准模板,无法删除。请先切换其他版本为标准模板。'); + return; + } + deleteTargetState[1](record); + deleteModalState[1](true); + } + + function handleDelete() { + var target = deleteTargetState[0]; + if (!target) return; + if (target.isDefault) { + message.warning('标准模板无法删除'); + return; + } + setTemplates(function (prev) { return prev.filter(function (t) { return t.id !== target.id; }); }); + deleteModalState[1](false); + deleteTargetState[1](null); + message.success('已删除模板版本'); + } + + function isWordFormattedDoc(html) { + return /ct-word-doc|ct-word-title/.test(String(html || '')); + } + + function getLessorCompanies() { + if (typeof window !== 'undefined' && window.ONEOS_LESSOR_COMPANIES && window.ONEOS_LESSOR_COMPANIES.length) { + return window.ONEOS_LESSOR_COMPANIES; + } + return DEFAULT_LESSOR_COMPANIES; + } + + function getLessorCompanyById(id) { + var list = getLessorCompanies(); + return list.find(function (c) { return c.id === id; }) || list.find(function (c) { return c.enabled; }) || list[0] || null; + } + + function buildLessorVars(company) { + if (!company) return {}; + return { + lessorName: company.legalName || '', + lessorShortName: company.shortName || '', + lessorAddress: company.address || '', + lessorContact: company.contact || '', + lessorPhone: company.phone || '', + lessorEmail: company.email || '', + lessorAccountName: company.accountName || company.legalName || '', + lessorBankName: company.bankName || '', + lessorBankAccount: company.bankAccount || '', + lessorHotline: DEFAULT_LESSOR_HOTLINE, + lessorCreditCode: company.creditCode || '' + }; + } + + function applyTemplateVars(html, vars) { + var out = String(html || ''); + Object.keys(vars || {}).forEach(function (key) { + var val = vars[key] != null ? String(vars[key]) : ''; + out = out.split('{{' + key + '}}').join(val); + }); + return out; + } + + function fixWordPreviewHtml(html) { + if (!html || typeof document === 'undefined') return String(html || ''); + var div = document.createElement('div'); + var cleaned = String(html) + .replace(/<\/p>]*>[\s\S]*?<\/tr><\/tbody><\/table>/g, function (frag) { + return frag.indexOf('= 0 ? frag : '

'; + }); + div.innerHTML = cleaned; + div.querySelectorAll('table.ct-word-table').forEach(function (table) { + var maxCols = 0; + table.querySelectorAll('tr').forEach(function (tr) { + var cols = 0; + tr.querySelectorAll('td, th').forEach(function (td) { + cols += parseInt(td.getAttribute('colspan') || '1', 10); + }); + if (cols > maxCols) maxCols = cols; + }); + if (maxCols <= 1) return; + table.querySelectorAll('tr').forEach(function (tr) { + var tds = tr.querySelectorAll('td, th'); + var cols = 0; + tds.forEach(function (td) { cols += parseInt(td.getAttribute('colspan') || '1', 10); }); + if (tds.length === 1 && cols === 1) { + tds[0].setAttribute('colspan', String(maxCols)); + tds[0].classList.add('ct-word-td-full'); + } + }); + }); + return div.innerHTML; + } + + function paginateWordHtml(html) { + var MM = 96 / 25.4; + var contentH = Math.floor((297 - 25.4 * 2) * MM); + var contentW = Math.floor((210 - 31.8 * 2) * MM); + var fixed = preparePreviewHtml(html); + var sandbox = document.createElement('div'); + sandbox.style.cssText = 'position:fixed;left:-20000px;top:0;visibility:hidden;pointer-events:none;'; + document.body.appendChild(sandbox); + + var measure = document.createElement('div'); + measure.className = 'ct-word-doc ct-word-measure'; + measure.style.width = contentW + 'px'; + measure.style.fontFamily = 'FangSong, STFangsong, "仿宋", SimSun, "宋体", serif'; + measure.style.fontSize = '10.5pt'; + measure.style.lineHeight = '1.5'; + measure.innerHTML = fixed; + sandbox.appendChild(measure); + + var root = measure.querySelector('.ct-word-doc') || measure; + var nodes = []; + root.childNodes.forEach(function (n) { + if (n.nodeType === 1) nodes.push(n); + else if (n.nodeType === 3 && String(n.textContent || '').trim()) { + var p = document.createElement('p'); + p.className = 'ct-word-body'; + p.textContent = n.textContent; + nodes.push(p); + } + }); + + var pageMeasure = document.createElement('div'); + pageMeasure.className = 'ct-word-doc ct-word-measure'; + pageMeasure.style.width = contentW + 'px'; + pageMeasure.style.fontFamily = measure.style.fontFamily; + pageMeasure.style.fontSize = measure.style.fontSize; + pageMeasure.style.lineHeight = measure.style.lineHeight; + sandbox.appendChild(pageMeasure); + + var pages = []; + nodes.forEach(function (node) { + var clone = node.cloneNode(true); + pageMeasure.appendChild(clone); + if (pageMeasure.scrollHeight > contentH + 4) { + pageMeasure.removeChild(clone); + if (pageMeasure.innerHTML.trim()) pages.push(pageMeasure.innerHTML); + pageMeasure.innerHTML = ''; + pageMeasure.appendChild(clone); + if (pageMeasure.scrollHeight > contentH + 4) { + pages.push(pageMeasure.innerHTML); + pageMeasure.innerHTML = ''; + } + } + }); + if (pageMeasure.innerHTML.trim()) pages.push(pageMeasure.innerHTML); + if (!pages.length) pages.push(fixed); + + document.body.removeChild(sandbox); + return pages; + } + + function WordPaginatedPreview(props) { + var html = props.html || ''; + var viewModeState = useState('scroll'); + var pagesState = useState([]); + var pageIdxState = useState(0); + var viewMode = viewModeState[0]; + var setViewMode = viewModeState[1]; + var pages = pagesState[0]; + var pageIdx = pageIdxState[0]; + var setPageIdx = pageIdxState[1]; + + useEffect(function () { + if (!html) { + pagesState[1]([]); + return; + } + var timer = window.setTimeout(function () { + pagesState[1](paginateWordHtml(html)); + pageIdxState[1](0); + }, 0); + return function () { window.clearTimeout(timer); }; + }, [html]); + + if (!pages.length) { + return React.createElement('div', { className: 'ct-preview-pages' }, + React.createElement('div', { className: 'ct-preview-page is-loading' }, 'Word 分页排版计算中…') + ); + } + + var pager = React.createElement('div', { className: 'ct-preview-pager' }, + React.createElement(Button, { + size: 'small', + disabled: pageIdx <= 0, + onClick: function () { setPageIdx(Math.max(0, pageIdx - 1)); } + }, '上一页'), + React.createElement('span', { className: 'ct-preview-pager__info' }, '第 ', pageIdx + 1, ' / ', pages.length, ' 页'), + React.createElement(Button, { + size: 'small', + disabled: pageIdx >= pages.length - 1, + onClick: function () { setPageIdx(Math.min(pages.length - 1, pageIdx + 1)); } + }, '下一页'), + React.createElement(Button, { + size: 'small', + type: viewMode === 'scroll' ? 'primary' : 'default', + onClick: function () { setViewMode('scroll'); } + }, '连续预览'), + React.createElement(Button, { + size: 'small', + type: viewMode === 'single' ? 'primary' : 'default', + onClick: function () { setViewMode('single'); } + }, '单页预览') + ); + + if (viewMode === 'single') { + return React.createElement('div', null, + pager, + React.createElement('div', { className: 'ct-preview-pages' }, + React.createElement('div', { className: 'ct-preview-page', key: pageIdx }, + React.createElement('div', { + className: 'ct-preview-page__body ct-word-doc', + dangerouslySetInnerHTML: { __html: pages[pageIdx] } + }), + React.createElement('div', { className: 'ct-preview-page__footer' }, '— ', pageIdx + 1, ' —') + ) + ) + ); + } + + return React.createElement('div', null, + pager, + React.createElement('div', { className: 'ct-preview-pages' }, + pages.map(function (pageHtml, i) { + return React.createElement('div', { className: 'ct-preview-page', key: 'p-' + i }, + React.createElement('div', { + className: 'ct-preview-page__body ct-word-doc', + dangerouslySetInnerHTML: { __html: pageHtml } + }), + React.createElement('div', { className: 'ct-preview-page__footer' }, '— ', i + 1, ' —') + ); + }) + ) + ); + } + + function renderPreview(html, previewVars) { + var raw = html || formDocumentHtmlState[0] || ''; + var content = previewVars ? applyTemplateVars(raw, previewVars) : raw; + var displayHtml = preparePreviewHtml(content); + var wordDoc = isWordFormattedDoc(content); + if (wordDoc) { + return React.createElement('div', { className: 'ct-preview-doc ct-word-doc ct-word-paginated' }, + React.createElement(WordPaginatedPreview, { html: content }) + ); + } + var previewClass = 'ct-preview-doc'; + return React.createElement('div', { className: previewClass }, + React.createElement('h1', null, getDocumentTitle(content)), + React.createElement('div', { + className: 'ct-preview-section__body', + dangerouslySetInnerHTML: { __html: displayHtml } + }) + ); + } + + function UnifiedContractEditor(props) { + var html = props.html; + var onChange = props.onChange; + var syncKey = props.syncKey; + var onRegisterInsert = props.onRegisterInsert; + var editorRef = useRef(null); + var sourceTextareaRef = useRef(null); + var lockIdRef = useRef(1); + var riskIdRef = useRef(1); + var fullscreenState = useState(false); + var isFullscreen = fullscreenState[0]; + var setFullscreen = fullscreenState[1]; + var sourceModeState = useState(false); + var isSourceMode = sourceModeState[0]; + var setSourceMode = sourceModeState[1]; + var isSourceModeRef = useRef(isSourceMode); + isSourceModeRef.current = isSourceMode; + var sourceDraftState = useState(''); + var findModalState = useState(false); + var findTextState = useState(''); + var replaceTextState = useState(''); + var docStatsState = useState({ chars: 0, words: 0, locks: 0, riskRedlines: 0 }); + var fmtState = useState({ bold: false, italic: false, underline: false, align: 'left' }); + var fontSizePickState = useState('14px'); + + function updateDocStats(content) { + var plain = htmlToPlainText(content || ''); + docStatsState[1]({ + chars: plain.length, + words: plain.trim() ? plain.trim().split(/\s+/).length : 0, + locks: countLockRegions(content || ''), + riskRedlines: countRiskRedlineRegions(content || '') + }); + } + + useEffect(function () { + var el = editorRef.current; + if (!el) return; + el.innerHTML = html || ''; + updateDocStats(html || ''); + }, [syncKey]); + + function insertAtCursor(text) { + var token = String(text || ''); + if (!token) return; + if (isSourceModeRef.current) { + var ta = sourceTextareaRef.current; + if (!ta) return; + var start = ta.selectionStart != null ? ta.selectionStart : ta.value.length; + var end = ta.selectionEnd != null ? ta.selectionEnd : start; + var next = ta.value.slice(0, start) + token + ta.value.slice(end); + sourceDraftState[1](next); + onChange(next); + updateDocStats(next); + requestAnimationFrame(function () { + ta.focus(); + var pos = start + token.length; + ta.setSelectionRange(pos, pos); + }); + return; + } + var el = editorRef.current; + if (!el) return; + el.focus(); + try { document.execCommand('insertText', false, token); } catch (e) { + try { document.execCommand('insertHTML', false, token); } catch (e2) { /* noop */ } + } + syncHtml(); + } + + useEffect(function () { + if (!onRegisterInsert) return; + onRegisterInsert(insertAtCursor); + return function () { onRegisterInsert(null); }; + }, [onRegisterInsert]); + + useEffect(function () { + function refreshFmt() { + try { + var align = 'left'; + if (document.queryCommandState('justifyCenter')) align = 'center'; + else if (document.queryCommandState('justifyRight')) align = 'right'; + else if (document.queryCommandState('justifyFull')) align = 'justify'; + fmtState[1]({ + bold: document.queryCommandState('bold'), + italic: document.queryCommandState('italic'), + underline: document.queryCommandState('underline'), + align: align + }); + } catch (e) { /* noop */ } + } + document.addEventListener('selectionchange', refreshFmt); + return function () { document.removeEventListener('selectionchange', refreshFmt); }; + }, []); + + function syncHtml() { + var el = editorRef.current; + if (!el) return; + var inner = el.innerHTML; + onChange(inner); + updateDocStats(inner); + } + + function runCmd(cmd, val) { + var el = editorRef.current; + if (!el) return; + el.focus(); + try { document.execCommand(cmd, false, val != null ? val : null); } catch (e) { /* noop */ } + syncHtml(); + } + + function rteSep(key) { + return React.createElement('span', { key: key || 'sep', className: 'ct-rte-sep' }); + } + + function rteBtn(title, icon, action, opts) { + opts = opts || {}; + var btn = React.createElement('button', { + type: 'button', + title: opts.wrap ? title : undefined, + className: 'ct-rte-btn' + (opts.active ? ' is-active' : '') + (opts.dropdown ? ' ct-rte-btn--dropdown' : ''), + disabled: !!opts.disabled, + onMouseDown: function (e) { + if (!opts.dropdown) e.preventDefault(); + if (!opts.disabled && action) action(e); + } + }, icon, opts.dropdown ? rteIcon('chevron') : null); + if (opts.wrap) return React.createElement('span', { key: opts.key || title, className: 'ct-rte-dropdown-wrap' }, btn); + return React.createElement(Tooltip, { key: opts.key || title, title: title }, btn); + } + + function rteDropdown(menu, title, icon, key) { + return React.createElement(Dropdown, { key: key, menu: menu, trigger: ['click'] }, + rteBtn(title, icon, null, { key: key + '-btn', dropdown: true, wrap: true }) + ); + } + + function rteColorMenu(options, cmd) { + return { + items: options.map(function (opt) { + return { + key: opt.value, + label: React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 8 } }, + React.createElement('span', { + style: { + width: 12, height: 12, borderRadius: 2, + background: opt.value, border: '1px solid #E2E8F0', display: 'inline-block' + } + }), + opt.label + ), + onClick: function () { runCmd(cmd, opt.value); } + }; + }) + }; + } + + function applyFontSize(size) { + if (!size) return; + fontSizePickState[1](size); + var el = editorRef.current; + if (!el) return; + el.focus(); + var sel = window.getSelection(); + if (!sel || sel.rangeCount === 0 || sel.isCollapsed) { + runCmd('fontSize', '4'); + syncHtml(); + return; + } + document.execCommand('insertHTML', false, '' + sel.toString() + ''); + syncHtml(); + } + + function applyLetterSpacing(spacing) { + if (!spacing) return; + var el = editorRef.current; + if (!el) return; + el.focus(); + var sel = window.getSelection(); + if (!sel || sel.rangeCount === 0 || sel.isCollapsed) return; + document.execCommand('insertHTML', false, '' + sel.toString() + ''); + syncHtml(); + } + + function applyFontFamily(font) { + if (!font) return; + runCmd('fontName', font); + } + + function applyHeading(tag) { + if (!tag) return; + runCmd('formatBlock', '<' + tag + '>'); + } + + function insertTable(rows, cols) { + var el = editorRef.current; + if (!el) return; + el.focus(); + document.execCommand('insertHTML', false, buildTableHtml(rows, cols)); + syncHtml(); + } + + function insertLink() { + var url = window.prompt('请输入链接地址', 'https://'); + if (!url) return; + runCmd('createLink', url); + } + + function doClipboard(cmd) { + var el = editorRef.current; + if (!el) return; + el.focus(); + if (cmd === 'paste') { + if (navigator.clipboard && navigator.clipboard.readText) { + navigator.clipboard.readText().then(function (text) { + document.execCommand('insertText', false, text); + syncHtml(); + }).catch(function () { + message.info('请使用 Ctrl+V 粘贴,或在浏览器中授予剪贴板权限'); + }); + return; + } + message.info('请使用 Ctrl+V 粘贴'); + return; + } + try { document.execCommand(cmd); syncHtml(); } catch (e) { message.info('请使用快捷键 Ctrl+' + (cmd === 'copy' ? 'C' : 'X')); } + } + + function applyLineHeight(lh) { + if (!lh) return; + var el = editorRef.current; + if (!el) return; + el.focus(); + var sel = window.getSelection(); + if (!sel || sel.rangeCount === 0) return; + var block = sel.getRangeAt(0).commonAncestorContainer; + if (block.nodeType === 3) block = block.parentNode; + while (block && block !== el && !/^(P|DIV|H1|H2|H3|H4|BLOCKQUOTE|LI|TD|TH)$/.test(block.nodeName)) block = block.parentNode; + if (block && block !== el) block.style.lineHeight = lh; + else document.execCommand('insertHTML', false, '


'); + syncHtml(); + } + + function applyFirstLineIndent(indent) { + var el = editorRef.current; + if (!el) return; + el.focus(); + var sel = window.getSelection(); + if (!sel || sel.rangeCount === 0) return; + var block = sel.getRangeAt(0).commonAncestorContainer; + if (block.nodeType === 3) block = block.parentNode; + while (block && block !== el && !/^(P|DIV|H1|H2|H3|H4)$/.test(block.nodeName)) block = block.parentNode; + if (block && block !== el) { + block.style.textIndent = indent; + syncHtml(); + } + } + + function insertPageBreak() { + runCmd('insertHTML', false, '
'); + } + + function insertDate() { + var d = new Date(); + var s = d.getFullYear() + '年' + (d.getMonth() + 1) + '月' + d.getDate() + '日'; + runCmd('insertText', false, s); + } + + function insertSpecialChar(ch) { + runCmd('insertText', false, ch); + } + + function getActiveTableCell() { + var sel = window.getSelection(); + var node = sel && sel.anchorNode; + if (!node) return null; + if (node.nodeType === 3) node = node.parentNode; + return node && node.closest ? node.closest('td, th') : null; + } + + function getActiveTable() { + var cell = getActiveTableCell(); + if (!cell) return null; + var table = cell.parentNode; + while (table && table.nodeName !== 'TABLE') table = table.parentNode; + return table; + } + + function tableOp(op) { + var cell = getActiveTableCell(); + var table = getActiveTable(); + if (!cell || !table) { + message.info('请将光标置于表格单元格内'); + return; + } + var row = cell.parentNode; + var colIdx = cell.cellIndex; + if (op === 'addRowAbove' || op === 'addRowBelow') { + var newRow = row.cloneNode(true); + Array.prototype.forEach.call(newRow.cells, function (c) { c.innerHTML = ' '; }); + if (op === 'addRowAbove') row.parentNode.insertBefore(newRow, row); + else row.parentNode.insertBefore(newRow, row.nextSibling); + } else if (op === 'addColLeft' || op === 'addColRight') { + var insertIdx = op === 'addColLeft' ? colIdx : colIdx + 1; + Array.prototype.forEach.call(table.rows, function (r) { + var td = document.createElement(r.cells[0] && r.cells[0].nodeName === 'TH' ? 'th' : 'td'); + td.innerHTML = ' '; + if (insertIdx >= r.cells.length) r.appendChild(td); + else r.insertBefore(td, r.cells[insertIdx]); + }); + } else if (op === 'deleteTable') { + table.parentNode.removeChild(table); + } + syncHtml(); + } + + function findNext() { + var kw = (findTextState[0] || '').trim(); + if (!kw) { message.info('请输入查找内容'); return; } + var found = window.find(kw, false, false, true, false, true, false); + if (!found) message.info('未找到更多匹配项'); + } + + function replaceInDoc(all) { + var findStr = (findTextState[0] || '').trim(); + var replStr = replaceTextState[0] || ''; + if (!findStr) { message.info('请输入查找内容'); return; } + var el = editorRef.current; + if (!el) return; + var count = 0; + var walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null); + var nodes = []; + while (walker.nextNode()) nodes.push(walker.currentNode); + for (var ni = 0; ni < nodes.length; ni++) { + var textNode = nodes[ni]; + var val = textNode.nodeValue; + if (val.indexOf(findStr) < 0) continue; + if (all) { + var parts = val.split(findStr); + count += parts.length - 1; + textNode.nodeValue = parts.join(replStr); + } else { + textNode.nodeValue = val.replace(findStr, replStr); + count = 1; + break; + } + } + if (count > 0) { + syncHtml(); + message.success(all ? ('已替换 ' + count + ' 处') : '已替换 1 处'); + } else message.info('未找到匹配内容'); + } + + function toggleSourceMode() { + if (!isSourceMode) { + sourceDraftState[1](editorRef.current ? editorRef.current.innerHTML : html); + setSourceMode(true); + } else { + onChange(sourceDraftState[0]); + setSourceMode(false); + setTimeout(function () { + var el = editorRef.current; + if (el) el.innerHTML = sourceDraftState[0]; + updateDocStats(sourceDraftState[0]); + }, 0); + } + } + + function printDoc() { + var content = isSourceMode ? sourceDraftState[0] : (editorRef.current ? editorRef.current.innerHTML : html); + var w = window.open('', '_blank'); + if (!w) { message.warning('请允许弹出窗口后重试'); return; } + w.document.write('合同打印预览'); + w.document.write(content); + w.document.write(''); + w.document.close(); + w.focus(); + w.print(); + } + + function handleInsertImage(file) { + var el = editorRef.current; + if (!el) return false; + if (!file || !file.type || file.type.indexOf('image') !== 0) { + message.warning('请上传图片文件'); + return false; + } + var reader = new FileReader(); + reader.onload = function (ev) { + el.focus(); + document.execCommand('insertHTML', false, '

合同插图

'); + syncHtml(); + }; + reader.readAsDataURL(file); + return false; + } + + function wrapSelectionAsLock() { + var el = editorRef.current; + if (!el) return; + var sel = window.getSelection(); + if (!sel || sel.rangeCount === 0 || sel.isCollapsed) { + message.info('请先选中要锁定的内容'); + return; + } + var range = sel.getRangeAt(0); + if (!el.contains(range.commonAncestorContainer)) return; + var lockId = 'lk' + (lockIdRef.current++); + var span = document.createElement('span'); + span.className = 'ct-inline-lock'; + span.setAttribute('data-inline-lock', '1'); + span.setAttribute('data-lock-id', lockId); + span.setAttribute('title', '内容锁定区'); + try { + range.surroundContents(span); + } catch (err) { + document.execCommand('insertHTML', false, '' + sel.toString() + ''); + } + syncHtml(); + message.success('已设为内容锁定区'); + } + + function unwrapSelectionLock() { + var el = editorRef.current; + if (!el) return; + var sel = window.getSelection(); + var node = sel && sel.anchorNode; + var lockEl = null; + if (node) { + if (node.nodeType === 3) node = node.parentNode; + if (node && node.closest) lockEl = node.closest('.ct-inline-lock'); + } + if (!lockEl) { + message.info('请将光标置于锁定内容内再解锁'); + return; + } + var parent = lockEl.parentNode; + while (lockEl.firstChild) parent.insertBefore(lockEl.firstChild, lockEl); + parent.removeChild(lockEl); + syncHtml(); + message.success('已解除内容锁定'); + } + + function getRiskWrapBlock(el, sel) { + var node = sel && sel.anchorNode; + if (!node) return null; + if (node.nodeType === 3) node = node.parentNode; + while (node && node !== el) { + if (node.nodeType === 1 && /^(P|DIV|LI|H[1-6]|TD)$/i.test(node.tagName)) return node; + node = node.parentNode; + } + return null; + } + + function wrapBlockAsRiskRedline(block) { + if (!block || block.closest('.ct-risk-redline')) { + message.info('该段落已是风控红线条款'); + return; + } + var wrapper = document.createElement('div'); + wrapper.className = 'ct-risk-redline'; + wrapper.setAttribute('data-risk-redline', '1'); + wrapper.setAttribute('data-risk-id', 'rk' + (riskIdRef.current++)); + wrapper.setAttribute('data-nonstd-trigger', '1'); + wrapper.setAttribute('title', '风控红线条款:创建合同时修改将触发非标审批'); + block.parentNode.insertBefore(wrapper, block); + wrapper.appendChild(block); + syncHtml(); + message.success('已设为风控红线条款'); + } + + function wrapSelectionAsRiskRedline() { + var el = editorRef.current; + if (!el) return; + var sel = window.getSelection(); + if (!sel || sel.rangeCount === 0) { + message.info('请先选中段落或条款内容'); + return; + } + var range = sel.getRangeAt(0); + if (!el.contains(range.commonAncestorContainer)) return; + if (sel.isCollapsed || !String(sel.toString() || '').trim()) { + var block = getRiskWrapBlock(el, sel); + if (block) { + wrapBlockAsRiskRedline(block); + return; + } + message.info('请先选中段落或条款内容'); + return; + } + if (sel.anchorNode && sel.anchorNode.parentNode && sel.anchorNode.parentNode.closest && + sel.anchorNode.parentNode.closest('.ct-risk-redline')) { + message.info('选中内容已在风控红线条款内'); + return; + } + var riskId = 'rk' + (riskIdRef.current++); + var span = document.createElement('span'); + span.className = 'ct-risk-redline'; + span.setAttribute('data-risk-redline', '1'); + span.setAttribute('data-risk-id', riskId); + span.setAttribute('data-nonstd-trigger', '1'); + span.setAttribute('title', '风控红线条款:创建合同时修改将触发非标审批'); + try { + range.surroundContents(span); + } catch (err) { + document.execCommand('insertHTML', false, + '' + + sel.toString() + ''); + } + syncHtml(); + message.success('已设为风控红线条款'); + } + + function unwrapSelectionRiskRedline() { + var el = editorRef.current; + if (!el) return; + var sel = window.getSelection(); + var node = sel && sel.anchorNode; + var riskEl = null; + if (node) { + if (node.nodeType === 3) node = node.parentNode; + if (node && node.closest) riskEl = node.closest('.ct-risk-redline'); + } + if (!riskEl) { + message.info('请将光标置于风控红线条款内再取消'); + return; + } + var footer = riskEl.querySelector('.ct-risk-redline__footer'); + if (footer) footer.parentNode.removeChild(footer); + var parent = riskEl.parentNode; + while (riskEl.firstChild) parent.insertBefore(riskEl.firstChild, riskEl); + parent.removeChild(riskEl); + syncHtml(); + message.success('已取消风控红线条款标记'); + } + + function rteTextBtn(label, title, action, opts) { + opts = opts || {}; + return React.createElement(Tooltip, { key: opts.key || label, title: title }, + React.createElement('button', { + type: 'button', + className: 'ct-rte-btn ct-rte-btn--text' + (opts.muted ? ' is-muted' : ''), + onMouseDown: function (e) { + e.preventDefault(); + if (action) action(e); + } + }, label) + ); + } + + var fmt = fmtState[0]; + + var tableMenuItems = TABLE_PRESET_ITEMS.map(function (item) { + return { key: item.key, label: item.label, onClick: function () { insertTable(item.rows, item.cols); } }; + }); + + var specialCharMenuItems = SPECIAL_CHAR_ITEMS.map(function (item) { + return { key: item.key, label: item.label, onClick: function () { insertSpecialChar(item.char); } }; + }); + + var lineHeightMenuItems = LINE_HEIGHT_OPTIONS.map(function (item) { + return { key: item.value, label: item.label, onClick: function () { applyLineHeight(item.value); } }; + }); + + var letterSpacingMenuItems = LETTER_SPACING_OPTIONS.map(function (item) { + return { key: item.value, label: item.label, onClick: function () { applyLetterSpacing(item.value); } }; + }); + + var scriptMenuItems = [ + { key: 'sup', label: '上标', onClick: function () { runCmd('superscript'); } }, + { key: 'sub', label: '下标', onClick: function () { runCmd('subscript'); } } + ]; + + var moreMenuItems = [ + { type: 'group', label: '编辑', children: [ + { key: 'find', label: '查找替换', onClick: function () { findModalState[1](true); } }, + { key: 'cut', label: '剪切', onClick: function () { doClipboard('cut'); } }, + { key: 'copy', label: '复制', onClick: function () { doClipboard('copy'); } }, + { key: 'paste', label: '粘贴', onClick: function () { doClipboard('paste'); } }, + { key: 'selectAll', label: '全选', onClick: function () { runCmd('selectAll'); } } + ] }, + { type: 'group', label: '段落', children: [ + { key: 'heading', label: '段落样式', children: PARAGRAPH_STYLE_OPTIONS.map(function (p) { + return { key: 'h-' + p.value, label: p.label, onClick: function () { applyHeading(p.value); } }; + }) }, + { key: 'strike', label: '删除线', onClick: function () { runCmd('strikeThrough'); } }, + { key: 'unlink', label: '取消链接', onClick: function () { runCmd('unlink'); } }, + { key: 'indent2', label: '首行缩进 2 字符', onClick: function () { applyFirstLineIndent('2em'); } }, + { key: 'indent0', label: '取消首行缩进', onClick: function () { applyFirstLineIndent('0'); } }, + { key: 'hr', label: '分隔线', onClick: function () { runCmd('insertHorizontalRule'); } }, + { key: 'pagebreak', label: '分页符', onClick: insertPageBreak }, + { key: 'date', label: '插入日期', onClick: insertDate } + ] }, + { type: 'group', label: '表格', children: [ + { key: 'rowUp', label: '上方插入行', onClick: function () { tableOp('addRowAbove'); } }, + { key: 'rowDn', label: '下方插入行', onClick: function () { tableOp('addRowBelow'); } }, + { key: 'colL', label: '左侧插入列', onClick: function () { tableOp('addColLeft'); } }, + { key: 'colR', label: '右侧插入列', onClick: function () { tableOp('addColRight'); } }, + { key: 'delTbl', label: '删除表格', onClick: function () { tableOp('deleteTable'); } } + ] }, + { type: 'group', label: '合同', children: [ + { key: 'lock', label: '锁定选中内容', onClick: wrapSelectionAsLock }, + { key: 'unlock', label: '解除锁定', onClick: unwrapSelectionLock }, + { key: 'risk', label: '设为风控红线条款', onClick: wrapSelectionAsRiskRedline }, + { key: 'unrisk', label: '取消风控红线条款', onClick: unwrapSelectionRiskRedline } + ] }, + { type: 'group', label: '视图', children: [ + { key: 'fullscreen', label: isFullscreen ? '退出全屏' : '全屏编辑', onClick: function () { setFullscreen(!isFullscreen); } }, + { key: 'source', label: isSourceMode ? '返回可视化' : 'HTML 源码', onClick: toggleSourceMode }, + { key: 'print', label: '打印预览', onClick: printDoc } + ] } + ]; + + var row1 = React.createElement('div', { className: 'ct-rich-toolbar__row' }, + React.createElement(Select, { + key: 'fs', className: 'ct-rte-select ct-rte-select--size', size: 'small', + value: fontSizePickState[0], options: FONT_SIZE_OPTIONS, + onChange: applyFontSize + }), + rteSep('s1'), + rteBtn('粗体 Ctrl+B', rteIcon('bold'), function () { runCmd('bold'); }, { key: 'bold', active: fmt.bold }), + rteBtn('斜体 Ctrl+I', rteIcon('italic'), function () { runCmd('italic'); }, { key: 'italic', active: fmt.italic }), + rteBtn('下划线 Ctrl+U', rteIcon('underline'), function () { runCmd('underline'); }, { key: 'underline', active: fmt.underline }), + rteDropdown(rteColorMenu(COLOR_OPTIONS, 'foreColor'), '字体颜色', rteIcon('fontColor'), 'fg'), + rteDropdown(rteColorMenu(BG_COLOR_OPTIONS, 'hiliteColor'), '背景色', rteIcon('highlight'), 'bg'), + rteDropdown({ items: letterSpacingMenuItems }, '字符间距', rteIcon('letterSpacing'), 'lsp'), + rteSep('s2'), + React.createElement('div', { key: 'align', className: 'ct-rte-align-group' }, + rteBtn('左对齐', rteIcon('alignLeft'), function () { runCmd('justifyLeft'); }, { key: 'al', active: fmt.align === 'left' }), + rteBtn('居中', rteIcon('alignCenter'), function () { runCmd('justifyCenter'); }, { key: 'ac', active: fmt.align === 'center' }), + rteBtn('右对齐', rteIcon('alignRight'), function () { runCmd('justifyRight'); }, { key: 'ar', active: fmt.align === 'right' }), + rteBtn('两端对齐', rteIcon('alignJustify'), function () { runCmd('justifyFull'); }, { key: 'aj', active: fmt.align === 'justify' }) + ), + rteDropdown({ items: lineHeightMenuItems }, '行距', rteIcon('lineHeight'), 'lh'), + rteBtn('减少缩进', rteIcon('outdent'), function () { runCmd('outdent'); }, { key: 'outdent' }) + ); + + var row2 = React.createElement('div', { className: 'ct-rich-toolbar__row' }, + React.createElement(Select, { + key: 'font', className: 'ct-rte-select ct-rte-select--font', size: 'small', + placeholder: '默认字体', options: FONT_FAMILY_OPTIONS, + onChange: applyFontFamily, allowClear: true + }), + rteSep('s3'), + rteBtn('清除格式', rteIcon('eraser'), function () { runCmd('removeFormat'); }, { key: 'eraser' }), + React.createElement(Upload, { key: 'img', showUploadList: false, accept: 'image/*', beforeUpload: handleInsertImage }, + rteBtn('插入图片', rteIcon('image'), null, { key: 'imgBtn', wrap: true }) + ), + rteDropdown({ items: scriptMenuItems }, '上标/下标', rteIcon('script'), 'script'), + rteBtn('无序列表', rteIcon('listBullet'), function () { runCmd('insertUnorderedList'); }, { key: 'ul' }), + rteBtn('有序列表', rteIcon('listOrdered'), function () { runCmd('insertOrderedList'); }, { key: 'ol' }), + rteBtn('增加缩进', rteIcon('indent'), function () { runCmd('indent'); }, { key: 'indent' }), + rteDropdown({ items: tableMenuItems }, '插入表格', rteIcon('table'), 'tbl'), + rteDropdown({ items: specialCharMenuItems }, '特殊字符', rteIcon('bulb'), 'spec'), + rteBtn('插入链接', rteIcon('link'), insertLink, { key: 'link' }), + rteSep('s4'), + rteBtn('撤销 Ctrl+Z', rteIcon('undo'), function () { runCmd('undo'); }, { key: 'undo' }), + rteBtn('重做 Ctrl+Y', rteIcon('redo'), function () { runCmd('redo'); }, { key: 'redo' }), + rteSep('s5'), + rteTextBtn('风控红线', '设为风控红线条款(选中段落或文字后点击;创建合同时修改将触发非标审批)', wrapSelectionAsRiskRedline, { key: 'risk-redline' }), + rteTextBtn('取消红线', '取消风控红线条款标记', unwrapSelectionRiskRedline, { key: 'unrisk-redline', muted: true }), + rteDropdown({ items: moreMenuItems }, '更多功能', rteIcon('more'), 'more') + ); + + var stats = docStatsState[0]; + + return React.createElement('div', { className: 'ct-rich-wrap' + (isFullscreen ? ' is-fullscreen' : '') }, + React.createElement('div', { className: 'ct-rich-toolbar' }, row1, row2), + isSourceMode + ? React.createElement('textarea', { + ref: sourceTextareaRef, + className: 'ct-rich-source', + value: sourceDraftState[0], + onChange: function (e) { + sourceDraftState[1](e.target.value); + onChange(e.target.value); + updateDocStats(e.target.value); + } + }) + : React.createElement('div', { + ref: editorRef, + className: 'ct-rich-editor', + contentEditable: true, + suppressContentEditableWarning: true, + onInput: syncHtml, + onBlur: syncHtml + }), + React.createElement('div', { className: 'ct-rich-statusbar' }, + React.createElement('div', { className: 'ct-rich-statusbar__stats' }, + React.createElement('span', null, '字符:' + stats.chars), + React.createElement('span', null, '词数:' + stats.words), + React.createElement('span', null, '锁定区:' + stats.locks + ' 处'), + React.createElement('span', null, '风控红线:' + stats.riskRedlines + ' 处') + ), + React.createElement('span', null, isSourceMode ? 'HTML 源码模式' : '可视化编辑') + ), + React.createElement(Modal, { + title: '查找与替换', + open: findModalState[0], + onCancel: function () { findModalState[1](false); }, + footer: React.createElement(Space, null, + React.createElement(Button, { onClick: findNext }, '查找下一处'), + React.createElement(Button, { onClick: function () { replaceInDoc(false); } }, '替换'), + React.createElement(Button, { type: 'primary', onClick: function () { replaceInDoc(true); } }, '全部替换'), + React.createElement(Button, { onClick: function () { findModalState[1](false); } }, '关闭') + ) + }, + React.createElement('div', { style: { marginBottom: 12 } }, + React.createElement('label', { className: 'ct-modal-form-label' }, '查找内容'), + React.createElement(Input, { + value: findTextState[0], + onChange: function (e) { findTextState[1](e.target.value); }, + placeholder: '输入要查找的文字', + allowClear: true + }) + ), + React.createElement('div', null, + React.createElement('label', { className: 'ct-modal-form-label' }, '替换为'), + React.createElement(Input, { + value: replaceTextState[0], + onChange: function (e) { replaceTextState[1](e.target.value); }, + placeholder: '留空则删除匹配文字', + allowClear: true + }) + ) + ) + ); + } + + function renderUnifiedEditor() { + return React.createElement(UnifiedContractEditor, { + key: 'unified-editor-' + editorSyncKeyState[0], + html: formDocumentHtmlState[0], + syncKey: editorSyncKeyState[0], + onChange: syncDocumentHtml, + onRegisterInsert: registerEditorInsertRef.current + }); + } + + function copyTemplatePlaceholder(key) { + var token = '{{' + key + '}}'; + if (typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(token).then(function () { + message.success('已复制 ' + token); + }).catch(function () { + message.info(token); + }); + return; + } + message.info(token); + } + + function insertTemplatePlaceholder(key) { + var token = '{{' + key + '}}'; + var insertFn = editorInsertHandlerRef.current; + if (!insertFn) { + message.warning('请先在编辑区点击定位光标'); + return; + } + insertFn(token); + message.success('已插入 ' + token); + } + + function renderTemplatePlaceholderCard() { + return React.createElement('div', { className: 'ct-placeholder-card' }, + React.createElement('div', { className: 'ct-placeholder-card__head' }, + React.createElement('p', { className: 'ct-placeholder-card__title' }, '模板占位符'), + React.createElement('p', { className: 'ct-placeholder-card__desc' }, '点击条目插入至光标处;也可复制后在编辑区粘贴。创建合同时将按规则自动替换。') + ), + CONTRACT_TEMPLATE_VAR_GROUPS.map(function (group) { + return React.createElement('div', { key: group.title, className: 'ct-placeholder-card__group' }, + React.createElement('p', { className: 'ct-placeholder-card__group-title' }, group.title), + React.createElement('p', { className: 'ct-placeholder-card__group-desc' }, group.desc), + group.vars.map(function (v) { + var token = '{{' + v.key + '}}'; + return React.createElement('div', { + key: v.key, + className: 'ct-placeholder-card__item', + role: 'button', + tabIndex: 0, + onClick: function () { insertTemplatePlaceholder(v.key); }, + onKeyDown: function (e) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + insertTemplatePlaceholder(v.key); + } + } + }, + React.createElement('div', { className: 'ct-placeholder-card__item-main' }, + React.createElement('div', { className: 'ct-placeholder-card__item-label' }, v.label), + React.createElement('div', { className: 'ct-placeholder-card__item-token' }, token) + ), + React.createElement('div', { className: 'ct-placeholder-card__item-actions' }, + React.createElement('span', { + className: 'ct-placeholder-card__action', + role: 'button', + tabIndex: 0, + onClick: function (e) { e.stopPropagation(); copyTemplatePlaceholder(v.key); }, + onKeyDown: function (e) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + e.stopPropagation(); + copyTemplatePlaceholder(v.key); + } + } + }, '复制') + ) + ); + }) + ); + }) + ); + } + + var columns = [ + { title: '版本号', dataIndex: 'versionNo', key: 'versionNo', width: 110, render: function (v, r) { + return React.createElement(Space, { size: 6 }, + React.createElement('span', { style: { fontWeight: v ? 600 : 400, color: v ? '#1E293B' : '#94A3B8' } }, v || '—'), + r.isDefault ? React.createElement(Tag, { color: 'gold' }, '标准模板') : null + ); + } }, + { title: '版本名称', dataIndex: 'versionName', key: 'versionName', width: 200, ellipsis: true }, + { title: '状态', dataIndex: 'status', key: 'status', width: 88, align: 'center', render: statusTag }, + { title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 150 }, + { title: '创建人', dataIndex: 'creator', key: 'creator', width: 110 }, + { title: '最后更新时间', dataIndex: 'updateTime', key: 'updateTime', width: 150 }, + { title: '更新人', dataIndex: 'updater', key: 'updater', width: 110 }, + { title: '备注', dataIndex: 'remark', key: 'remark', ellipsis: true }, + { + title: '操作', key: 'action', width: 240, fixed: 'right', + render: function (_, record) { + return React.createElement(Space, { size: 4, wrap: true }, + React.createElement(Button, { type: 'link', size: 'small', className: 'oneos-action-btn ct-action-btn', onClick: function () { openEdit(record); } }, '编辑'), + record.status === 'published' && !record.isDefault + ? React.createElement(Button, { type: 'link', size: 'small', className: 'oneos-action-btn ct-action-btn', onClick: function () { confirmSetDefault(record); } }, '设为标准模板') + : null, + record.isDefault + ? React.createElement(Tooltip, { title: '客服草拟合同时使用此标准模板' }, + React.createElement(Button, { type: 'link', size: 'small', className: 'oneos-action-btn ct-action-btn', disabled: true }, '当前标准模板') + ) + : null, + record.isDefault + ? React.createElement(Tooltip, { title: '标准模板不可删除' }, + React.createElement(Button, { type: 'link', size: 'small', className: 'oneos-action-btn ct-action-btn', disabled: true, danger: true }, '删除') + ) + : React.createElement(Button, { type: 'link', size: 'small', className: 'oneos-action-btn ct-action-btn', danger: true, onClick: function () { confirmDelete(record); } }, '删除') + ); + } + } + ]; + + var lessorCompanies = getLessorCompanies(); + var previewLessorCompany = getLessorCompanyById(previewLessorId); + var previewTemplateVars = Object.assign( + { customerName: '【承租方公司名称】', contractCode: '20260001' }, + buildLessorVars(previewLessorCompany) + ); + + function getListByCategory(categoryKey) { + return templates.filter(function (r) { + return r.category === categoryKey; + }).sort(function (a, b) { + if (a.isDefault !== b.isDefault) return a.isDefault ? -1 : 1; + return String(b.updateTime).localeCompare(String(a.updateTime)); + }); + } + + function renderTabTable(categoryKey) { + var list = getListByCategory(categoryKey); + var stats = categoryStats[categoryKey] || { count: 0, defaultVersion: '—', defaultName: '未设置' }; + var catOpt = CATEGORY_OPTIONS.find(function (c) { return c.value === categoryKey; }); + return React.createElement('div', null, + React.createElement('div', { className: 'ct-tab-summary' }, + React.createElement('div', { className: 'ct-tab-summary__meta' }, + (catOpt ? catOpt.desc : '') + ' · 共 ' + stats.count + ' 个版本' + ), + React.createElement('div', { className: 'ct-tab-summary__default' }, + React.createElement(Tag, { color: 'success', style: { margin: 0, fontWeight: 600 } }, '标准模板 · ' + stats.defaultVersion), + React.createElement('span', { style: { fontSize: 13, color: '#475569' } }, stats.defaultName) + ) + ), + React.createElement('div', { className: 'oneos-table-card ct-table-card' }, + React.createElement(Table, { + className: 'oneos-list-table ct-data-table', + rowKey: 'id', + columns: columns, + dataSource: list, + size: 'middle', + bordered: false, + rowClassName: function () { return 'ct-row-hover'; }, + pagination: { pageSize: 10, showTotal: function (t) { return '共 ' + t + ' 条'; }, showSizeChanger: false }, + scroll: { x: 1280 }, + locale: { emptyText: '该分类暂无合同模板,点击「新增标准合同」创建' } + }) + ) + ); + } + + var tabItems = CATEGORY_OPTIONS.map(function (c) { + var stats = categoryStats[c.value] || { count: 0 }; + return { + key: c.value, + label: React.createElement('span', { className: 'ct-tab-label' }, + c.label, + React.createElement(Tag, { style: { margin: 0, lineHeight: '18px' } }, stats.count) + ), + children: renderTabTable(c.value) + }; + }); + + function renderEditorWorkspace() { + var hasContent = !!(formDocumentHtmlState[0] || '').trim(); + return React.createElement(Row, { gutter: 16 }, + React.createElement(Col, { xs: 24, lg: 12 }, + React.createElement('div', { className: 'ct-panel-head' }, + React.createElement('div', { className: 'ct-panel-head__title' }, '编辑区', + React.createElement('span', { className: 'ct-panel-head__badge' }, '富文本 · 图文混排') + ), + React.createElement('div', { className: 'ct-panel-head__actions' }, + React.createElement(Popover, { + content: renderTemplatePlaceholderCard(), + title: null, + trigger: 'click', + placement: 'bottomRight', + open: placeholderPopoverOpenState[0], + onOpenChange: placeholderPopoverOpenState[1], + overlayInnerStyle: { padding: '12px 14px' } + }, + React.createElement(Button, { + size: 'small', + className: 'ct-placeholder-trigger', + type: placeholderPopoverOpenState[0] ? 'primary' : 'default', + ghost: placeholderPopoverOpenState[0] + }, '{ } 模板占位符') + ), + React.createElement(Upload, { + accept: '.doc,.docx', + showUploadList: false, + beforeUpload: handleImportWord + }, React.createElement(Button, { + size: 'small', + type: !hasContent && editorMode === 'create' ? 'primary' : 'default', + className: !hasContent && editorMode === 'create' ? 'ct-btn-cta' : '' + }, '导入 Word 模板')) + ) + ), + React.createElement('div', { key: 'editor-panel-' + editorSyncKeyState[0], className: 'ct-editor-panel' }, renderUnifiedEditor()) + ), + React.createElement(Col, { xs: 24, lg: 12 }, + React.createElement('div', { className: 'ct-panel-head' }, + React.createElement('div', { className: 'ct-panel-head__title' }, '实时预览', + React.createElement('span', { className: 'ct-panel-head__badge' }, + '锁定 ' + countLockRegions(formDocumentHtmlState[0]) + ' 处 · 风控红线 ' + countRiskRedlineRegions(formDocumentHtmlState[0]) + ' 处' + ) + ) + ), + React.createElement('div', { className: 'ct-preview-lessor-bar' }, + React.createElement('span', { className: 'ct-preview-lessor-bar__label' }, '预览签约主体(模拟创建合同时选择)'), + React.createElement(Select, { + style: { minWidth: 200 }, + value: previewLessorId, + onChange: setPreviewLessorId, + options: lessorCompanies.filter(function (c) { return c.enabled !== false; }).map(function (c) { + return { value: c.id, label: c.legalName }; + }) + }) + ), + React.createElement('div', { className: 'ct-preview-panel ct-preview-panel--word' }, renderPreview(formDocumentHtmlState[0], previewTemplateVars)) + ) + ); + } + + function renderEditorPage() { + var editorTitle = editorMode === 'create' + ? '新增标准合同' + : ('编辑标准合同 · ' + (editingRecord + ? (editingRecord.versionNo || '草稿') + : '')); + var hasContent = !!(formDocumentHtmlState[0] || '').trim(); + return React.createElement('div', { className: 'ct-editor-page' }, + React.createElement('div', { className: 'ct-editor-page__topbar' }, + React.createElement(Button, { className: 'ct-editor-page__back-btn', onClick: backToList }, '← 返回列表') + ), + React.createElement('div', { className: 'ct-editor-page__header' }, + React.createElement('h2', { className: 'ct-editor-page__title' }, editorTitle), + !hasContent && editorMode === 'create' + ? React.createElement('span', { style: { fontSize: 13, color: '#64748B' } }, '请先导入 Word 模板') + : null + ), + React.createElement('div', { className: 'ct-editor-page__body' }, renderEditorWorkspace()), + React.createElement('div', { className: 'ct-editor-page__footer' }, + React.createElement(Space, { size: 12 }, + React.createElement(Button, { onClick: backToList }, '取消'), + !(editorMode === 'edit' && editingRecord && editingRecord.status === 'published') + ? React.createElement(Button, { onClick: function () { handleSaveTemplate(false); } }, '保存草稿') + : null, + React.createElement(Button, { type: 'primary', className: 'ct-btn-cta', onClick: function () { handleSaveTemplate(true); } }, '保存并发布') + ) + ) + ); + } + + function renderListPage() { + return React.createElement('div', { className: 'oneos-table-section ct-table-section' }, + React.createElement(Card, { + className: 'oneos-filter-card ct-list-card', + title: '合同模板', + extra: React.createElement(Button, { type: 'primary', className: 'ct-btn-cta', onClick: openCreate }, '新增标准合同') + }, + React.createElement(Tabs, { + className: 'ct-category-tabs', + activeKey: categoryFilter, + onChange: setCategoryFilter, + type: 'card', + items: tabItems + }) + ) + ); + } + + return React.createElement(App, null, + React.createElement('style', null, pageStyle), + React.createElement('div', { className: 'oneos-page ct-page' }, + pageView === 'editor' ? renderEditorPage() : renderListPage(), + + React.createElement(Modal, { + wrapClassName: 'oneos-modal-wrap ct-modal-wrap', + title: '确认设为标准模板', + open: defaultConfirmModalState[0], + onOk: handleSetDefault, + onCancel: function () { defaultConfirmModalState[1](false); defaultConfirmTargetState[1](null); }, + okText: '确认设置', + cancelText: '取消' + }, defaultConfirmTargetState[0] + ? React.createElement('div', { style: { lineHeight: 1.6 } }, + '同一类型合同同时只能有一个标准模板。设为标准模板后,客服草拟新合同时将默认使用此版本。', + React.createElement('div', { style: { marginTop: 12, fontSize: 13, color: '#64748B' } }, + '将「', defaultConfirmTargetState[0].versionNo, ' · ', defaultConfirmTargetState[0].versionName, '」设为本分类标准模板' + ) + ) + : null), + + React.createElement(Modal, { + wrapClassName: 'oneos-modal-wrap ct-modal-wrap', + title: '确认删除', + open: deleteModalState[0], + onOk: handleDelete, + onCancel: function () { deleteModalState[1](false); deleteTargetState[1](null); }, + okText: '删除', + okButtonProps: { danger: true } + }, deleteTargetState[0] + ? React.createElement('div', null, '确定删除版本「', deleteTargetState[0].versionNo, ' · ', deleteTargetState[0].versionName, '」?删除后不可恢复。') + : null + ), + + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-data-analysis/index.tsx b/src/prototypes/oneos-web-data-analysis/index.tsx new file mode 100644 index 0000000..df5c3ab --- /dev/null +++ b/src/prototypes/oneos-web-data-analysis/index.tsx @@ -0,0 +1,37 @@ +/** + * @name 数据分析 + * 自 ONE-OS web端 原稿复刻(9 个页面) + */ +import React from 'react'; +import Page1 from './pages/01-客户服务部业务统计报表.jsx'; +import Page2 from './pages/02-物流业务台账.jsx'; +import Page3 from './pages/03-物流业务月度统计.jsx'; +import Page4 from './pages/04-业务部汇总台账.jsx'; +import Page5 from './pages/05-业务部台账.jsx'; +import Page6 from './pages/06-业务部业绩明细.jsx'; +import Page7 from './pages/07-业务台账.jsx'; +import Page8 from './pages/08-租赁车辆收入明细.jsx'; +import Page9 from './pages/09-租赁客户氢费台账.jsx'; +import { OneosWebLegacyShell } from '../../common/oneosWebLegacy/OneosWebLegacyShell'; + +const pages = [ + { id: 'u6570u636eu5206u6790-u5ba2u6237u670du52a1u90e8u4', title: '客户服务部业务统计报表', component: Page1 }, + { id: 'u6570u636eu5206u6790-u7269u6d41u4e1au52a1u53f0u8', title: '物流业务台账', component: Page2 }, + { id: 'u6570u636eu5206u6790-u7269u6d41u4e1au52a1u6708u5', title: '物流业务月度统计', component: Page3 }, + { id: 'u6570u636eu5206u6790-u4e1au52a1u90e8u6c47u603bu5', title: '业务部汇总台账', component: Page4 }, + { id: 'u6570u636eu5206u6790-u4e1au52a1u90e8u53f0u8d26', title: '业务部台账', component: Page5 }, + { id: 'u6570u636eu5206u6790-u4e1au52a1u90e8u4e1au7ee9u6', title: '业务部业绩明细', component: Page6 }, + { id: 'u6570u636eu5206u6790-u4e1au52a1u53f0u8d26', title: '业务台账', component: Page7 }, + { id: 'u6570u636eu5206u6790-u79dfu8d41u8f66u8f86u6536u5', title: '租赁车辆收入明细', component: Page8 }, + { id: 'u6570u636eu5206u6790-u79dfu8d41u5ba2u6237u6c22u8', title: '租赁客户氢费台账', component: Page9 }, +]; + +export default function OneosWebDataAnalysis() { + return ( + + ); +} diff --git a/src/prototypes/oneos-web-data-analysis/pages/01-客户服务部业务统计报表.jsx b/src/prototypes/oneos-web-data-analysis/pages/01-客户服务部业务统计报表.jsx new file mode 100644 index 0000000..ef64252 --- /dev/null +++ b/src/prototypes/oneos-web-data-analysis/pages/01-客户服务部业务统计报表.jsx @@ -0,0 +1,754 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 数据分析 - 客户服务部业务统计报表(依据业务台账模版数据统计方案) +// 经营总览 KPI + 租赁/物流/能源/成本账户/未收预警;支持汇总→明细钻取(原型演示数据) + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + + var antd = window.antd; + var App = antd.App; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Button = antd.Button; + var Table = antd.Table; + var Select = antd.Select; + var DatePicker = antd.DatePicker; + var Row = antd.Row; + var Col = antd.Col; + var Tabs = antd.Tabs; + var Space = antd.Space; + var Modal = antd.Modal; + var Tag = antd.Tag; + var Statistic = antd.Statistic; + var message = antd.message; + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + function fmtMoney(n) { + if (n === null || n === undefined || n === '') return '—'; + var x = Number(n); + if (isNaN(x)) return '—'; + return x.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + } + + function fmtPct(n) { + if (n === null || n === undefined || n === '') return '—'; + var x = Number(n); + if (isNaN(x)) return '—'; + return (x * 100).toFixed(1) + '%'; + } + + function numOrZero(v) { + if (v === null || v === undefined || v === '') return 0; + var n = Number(v); + return isNaN(n) ? 0 : n; + } + + function escapeCsv(v) { + var s = v == null ? '' : String(v); + if (s.indexOf(',') !== -1 || s.indexOf('"') !== -1 || s.indexOf('\n') !== -1) { + return '"' + s.replace(/"/g, '""') + '"'; + } + return s; + } + + function downloadCsv(filename, lines) { + var csv = lines.map(function (row) { return row.map(escapeCsv).join(','); }).join('\n'); + var blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' }); + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); + } + + function initialYear() { + try { + if (window.dayjs) return window.dayjs('2026-01-01'); + } catch (e1) {} + return null; + } + + function getAppliedYearMonth(yearApplied, monthApplied) { + var y = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : '2026'; + var m = monthApplied != null && monthApplied !== '' ? Number(monthApplied) : null; + return { year: y, month: m }; + } + + var SALESPERSONS = ['谈云', '刘念念', '谯云', '董剑煜', '尚建华']; + var CUSTOMERS = [ + '嘉兴古道物流有限公司', '杭州绿道城配科技有限公司', '宁波港联氢运物流有限公司', + '上海虹钦物流有限公司', '嘉兴市乍浦港口经营有限公司', '荣达餐饮(广东)集团有限公司' + ]; + + function buildMockKpi(ym) { + var seed = Number(ym.year) * 100 + (ym.month || 5); + return { + totalRevenue: 4280000 + (seed % 17) * 12000, + totalProfit: 612000 + (seed % 11) * 8000, + uncollected: 386500 + (seed % 9) * 5000, + accountBalance: 1258000 + (seed % 13) * 3000, + activeVehicles: 186 + (seed % 7) + }; + } + + function buildOverviewRows(ym) { + var lines = [ + { key: 'lease', line: '租赁业务', receivable: 1280000, received: 1120000, cost: 720000, profit: 400000 }, + { key: 'logistics', line: '物流业务', receivable: 2100000, received: 1980000, cost: 1650000, profit: 330000 }, + { key: 'energy', line: '能源销售', receivable: 680000, received: 620000, cost: 480000, profit: 140000 }, + { key: 'etc', line: 'ETC及其他', receivable: 220000, received: 210000, cost: 95000, profit: 115000 } + ]; + return lines.map(function (r) { + var uncollected = r.receivable - r.received; + return Object.assign({}, r, { + uncollected: uncollected, + profitRate: r.receivable > 0 ? r.profit / r.receivable : 0, + year: ym.year, + monthLabel: ym.month ? ym.month + '月' : '全年累计' + }); + }); + } + + function buildLeaseSummary(ym, filters) { + var rows = []; + var i; + for (i = 0; i < 8; i++) { + var sp = SALESPERSONS[i % SALESPERSONS.length]; + var cu = CUSTOMERS[i % CUSTOMERS.length]; + if (filters.salesperson && sp !== filters.salesperson) continue; + if (filters.customer && cu !== filters.customer) continue; + var recv = 120000 + i * 18500; + var got = recv - (i % 3 === 0 ? 22000 : 0); + var cost = recv * 0.58; + rows.push({ + key: 'lease-' + i, + year: ym.year, + month: ym.month || 5, + salesperson: sp, + customerName: cu, + receivable: recv, + uncollected: recv - got, + cost: cost, + profit: got - cost, + profitRate: recv > 0 ? (got - cost) / recv : 0 + }); + } + return rows; + } + + function buildLogisticsSummary(ym, filters) { + var projects = ['沪浙干线', '嘉兴冷链城配', '宁波港区短驳', '盒马城配专线']; + var rows = []; + projects.forEach(function (p, i) { + var sp = SALESPERSONS[i % SALESPERSONS.length]; + var cu = CUSTOMERS[(i + 1) % CUSTOMERS.length]; + if (filters.salesperson && sp !== filters.salesperson) return; + if (filters.customer && cu !== filters.customer) return; + var orders = 120 + i * 35; + var recv = 280000 + i * 95000; + var got = recv - (i === 2 ? 45000 : 8000); + rows.push({ + key: 'log-' + i, + month: ym.month || 5, + salesperson: sp, + projectName: p, + customerName: cu, + orderCount: orders, + receivable: recv, + received: got, + uncollected: recv - got, + invoiceAmount: got * 0.95 + }); + }); + return rows; + } + + function buildEnergySummary(ym) { + return CUSTOMERS.slice(0, 5).map(function (c, i) { + var kg = 4200 + i * 680; + var cost = kg * 32; + var sales = kg * 38; + return { + key: 'eng-' + i, + year: ym.year, + month: ym.month || 5, + customerName: c, + salesperson: SALESPERSONS[i % SALESPERSONS.length], + h2Kg: kg, + costAmount: cost, + salesAmount: sales, + grossProfit: sales - cost, + collectStatus: i % 2 === 0 ? '已收款' : '待收款' + }; + }); + } + + function buildAccountRows() { + return CUSTOMERS.slice(0, 6).map(function (c, i) { + var open = 50000 + i * 12000; + var recharge = 200000 + i * 30000; + var usedH2 = 120000 + i * 22000; + var usedElec = 45000 + i * 8000; + return { + key: 'acc-' + i, + customerName: c, + salesperson: SALESPERSONS[i % SALESPERSONS.length], + openBalance: open, + recharge: recharge, + usedH2: usedH2, + usedElec: usedElec, + closeBalance: open + recharge - usedH2 - usedElec + }; + }); + } + + function buildUncollectedRows(ym) { + var rows = []; + var i; + for (i = 0; i < 10; i++) { + var recv = 80000 + i * 12000; + var uncol = 15000 + (i % 4) * 8000; + rows.push({ + key: 'unc-' + i, + line: i % 2 === 0 ? '租赁' : '物流', + salesperson: SALESPERSONS[i % SALESPERSONS.length], + customerName: CUSTOMERS[i % CUSTOMERS.length], + plateNo: '浙F0' + (3280 + i) + 'F', + dueDate: '2026-0' + ((i % 5) + 1) + '-15', + receivable: recv, + uncollected: uncol, + overdueDays: (i % 5) * 7 + 3 + }); + } + return rows; + } + + function mockLeaseDetail(row) { + return [ + { key: 'd1', plateNo: '浙F03298F', receivable: row.receivable * 0.4, received: row.receivable * 0.35, uncollected: row.receivable * 0.05 }, + { key: 'd2', plateNo: '粤AGP3649', receivable: row.receivable * 0.35, received: row.receivable * 0.32, uncollected: row.receivable * 0.03 }, + { key: 'd3', plateNo: '沪A62261F', receivable: row.receivable * 0.25, received: row.receivable * 0.2, uncollected: row.receivable * 0.05 } + ]; + } + + function mockLogisticsDetail(row) { + return [ + { key: 'd1', plateNo: '浙F05519F', tripDate: '2026-05-08', amount: row.receivable * 0.45, h2Fee: 3200, etcFee: 890 }, + { key: 'd2', plateNo: '浙F02698F', tripDate: '2026-05-10', amount: row.receivable * 0.35, h2Fee: 2800, etcFee: 720 }, + { key: 'd3', plateNo: '粤AGR5099', tripDate: '2026-05-12', amount: row.receivable * 0.2, h2Fee: 1500, etcFee: 410 } + ]; + } + + var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var filterLabelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var filterItemStyle = { marginBottom: 12 }; + var filterControlStyle = { width: '100%' }; + var profitNegStyle = { background: '#fff1f0', padding: '2px 8px', borderRadius: 4, display: 'inline-block' }; + var drillLinkStyle = { cursor: 'pointer', color: '#1677ff', border: 'none', background: 'none', padding: 0, font: 'inherit' }; + + var tableStyle = + '.cs-dept-stat-table .ant-table-thead>tr>th{background:#e6f4ff!important;font-weight:500;font-size:12px}' + + '.cs-dept-stat-table .ant-table-tbody>tr>td{white-space:nowrap}' + + '.cs-dept-kpi .ant-statistic-title{font-size:13px;color:rgba(0,0,0,0.55)}'; + + var yearDraftState = useState(initialYear); + var yearDraft = yearDraftState[0]; + var setYearDraft = yearDraftState[1]; + var yearAppliedState = useState(initialYear); + var yearApplied = yearAppliedState[0]; + var setYearApplied = yearAppliedState[1]; + + var monthDraftState = useState(5); + var monthDraft = monthDraftState[0]; + var setMonthDraft = monthDraftState[1]; + var monthAppliedState = useState(5); + var monthApplied = monthAppliedState[0]; + var setMonthApplied = monthAppliedState[1]; + + var spDraftState = useState(undefined); + var spDraft = spDraftState[0]; + var setSpDraft = spDraftState[1]; + var spAppliedState = useState(undefined); + var spApplied = spAppliedState[0]; + var setSpApplied = spAppliedState[1]; + + var cuDraftState = useState(undefined); + var cuDraft = cuDraftState[0]; + var setCuDraft = cuDraftState[1]; + var cuAppliedState = useState(undefined); + var cuApplied = cuAppliedState[0]; + var setCuApplied = cuAppliedState[1]; + + var activeTabState = useState('overview'); + var activeTab = activeTabState[0]; + var setActiveTab = activeTabState[1]; + + var drillState = useState({ open: false, type: '', title: '', rows: [] }); + var drill = drillState[0]; + var setDrill = drillState[1]; + + var ym = useMemo(function () { + return getAppliedYearMonth(yearApplied, monthApplied); + }, [yearApplied, monthApplied]); + + var filters = useMemo(function () { + return { salesperson: spApplied, customer: cuApplied }; + }, [spApplied, cuApplied]); + + var kpi = useMemo(function () { return buildMockKpi(ym); }, [ym]); + var overviewRows = useMemo(function () { return buildOverviewRows(ym); }, [ym]); + var leaseRows = useMemo(function () { return buildLeaseSummary(ym, filters); }, [ym, filters]); + var logisticsRows = useMemo(function () { return buildLogisticsSummary(ym, filters); }, [ym, filters]); + var energyRows = useMemo(function () { return buildEnergySummary(ym); }, [ym]); + var accountRows = useMemo(function () { return buildAccountRows(); }, []); + var uncollectedRows = useMemo(function () { return buildUncollectedRows(ym); }, [ym]); + + var monthOptions = useMemo(function () { + var opts = [{ value: '', label: '全年' }]; + var m; + for (m = 1; m <= 12; m++) opts.push({ value: m, label: m + '月' }); + return opts; + }, []); + + var spOptions = useMemo(function () { + return [{ value: '', label: '全部业务员' }].concat(SALESPERSONS.map(function (s) { return { value: s, label: s }; })); + }, []); + + var cuOptions = useMemo(function () { + return [{ value: '', label: '全部客户' }].concat(CUSTOMERS.map(function (c) { return { value: c, label: c }; })); + }, []); + + var handleQuery = useCallback(function () { + setYearApplied(yearDraft); + setMonthApplied(monthDraft === '' ? null : monthDraft); + setSpApplied(spDraft || undefined); + setCuApplied(cuDraft || undefined); + }, [yearDraft, monthDraft, spDraft, cuDraft]); + + var handleReset = useCallback(function () { + var y0 = initialYear(); + setYearDraft(y0); + setYearApplied(y0); + setMonthDraft(5); + setMonthApplied(5); + setSpDraft(undefined); + setSpApplied(undefined); + setCuDraft(undefined); + setCuApplied(undefined); + }, []); + + var openDrill = useCallback(function (type, row) { + var rows = []; + var title = ''; + if (type === 'lease') { + rows = mockLeaseDetail(row); + title = '租赁车辆明细 · ' + row.customerName; + } else if (type === 'logistics') { + rows = mockLogisticsDetail(row); + title = '物流运单明细 · ' + row.projectName; + } + setDrill({ open: true, type: type, title: title, rows: rows }); + }, []); + + var closeDrill = useCallback(function () { + setDrill({ open: false, type: '', title: '', rows: [] }); + }, []); + + var pageTitle = useMemo(function () { + var m = monthApplied ? monthApplied + '月' : '全年'; + return ym.year + '年' + m + ' · 客户服务部业务统计'; + }, [ym, monthApplied]); + + var overviewColumns = useMemo(function () { + return [ + { title: '业务条线', dataIndex: 'line', key: 'line', width: 100, fixed: 'left' }, + { title: '应收', dataIndex: 'receivable', key: 'receivable', align: 'right', render: fmtMoney }, + { title: '实收', dataIndex: 'received', key: 'received', align: 'right', render: fmtMoney }, + { title: '未收', dataIndex: 'uncollected', key: 'uncollected', align: 'right', render: function (v) { + var n = Number(v); + if (!isNaN(n) && n > 0) return React.createElement('span', { style: { color: '#f53f3f' } }, fmtMoney(v)); + return fmtMoney(v); + }}, + { title: '成本', dataIndex: 'cost', key: 'cost', align: 'right', render: fmtMoney }, + { title: '利润', dataIndex: 'profit', key: 'profit', align: 'right', render: function (v) { + var n = Number(v); + var neg = !isNaN(n) && n < 0; + return neg ? React.createElement('span', { style: profitNegStyle }, fmtMoney(v)) : fmtMoney(v); + }}, + { title: '利润率', dataIndex: 'profitRate', key: 'profitRate', align: 'right', width: 88, render: fmtPct } + ]; + }, []); + + var leaseColumns = useMemo(function () { + return [ + { title: '业务员', dataIndex: 'salesperson', key: 'salesperson', width: 88 }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 200, ellipsis: true }, + { title: '应收', dataIndex: 'receivable', key: 'receivable', align: 'right', render: fmtMoney }, + { title: '未收', dataIndex: 'uncollected', key: 'uncollected', align: 'right', render: fmtMoney }, + { title: '成本', dataIndex: 'cost', key: 'cost', align: 'right', render: fmtMoney }, + { title: '利润', dataIndex: 'profit', key: 'profit', align: 'right', render: function (v) { return fmtMoney(v); } }, + { title: '利润率', dataIndex: 'profitRate', key: 'profitRate', align: 'right', render: fmtPct }, + { + title: '操作', + key: 'action', + width: 88, + fixed: 'right', + render: function (_, r) { + return React.createElement('button', { type: 'button', style: drillLinkStyle, onClick: function () { openDrill('lease', r); } }, '明细'); + } + } + ]; + }, [openDrill]); + + var logisticsColumns = useMemo(function () { + return [ + { title: '业务员', dataIndex: 'salesperson', key: 'salesperson', width: 88 }, + { title: '项目名称', dataIndex: 'projectName', key: 'projectName', width: 140 }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 180, ellipsis: true }, + { title: '运单量', dataIndex: 'orderCount', key: 'orderCount', align: 'right', width: 80 }, + { title: '应收', dataIndex: 'receivable', key: 'receivable', align: 'right', render: fmtMoney }, + { title: '实收', dataIndex: 'received', key: 'received', align: 'right', render: fmtMoney }, + { title: '未收', dataIndex: 'uncollected', key: 'uncollected', align: 'right', render: fmtMoney }, + { + title: '操作', + key: 'action', + width: 88, + fixed: 'right', + render: function (_, r) { + return React.createElement('button', { type: 'button', style: drillLinkStyle, onClick: function () { openDrill('logistics', r); } }, '明细'); + } + } + ]; + }, [openDrill]); + + var energyColumns = useMemo(function () { + return [ + { title: '客户名', dataIndex: 'customerName', key: 'customerName', width: 200, ellipsis: true }, + { title: '业务员', dataIndex: 'salesperson', key: 'salesperson', width: 88 }, + { title: '加氢量(kg)', dataIndex: 'h2Kg', key: 'h2Kg', align: 'right', render: function (v) { return fmtMoney(v); } }, + { title: '成本金额', dataIndex: 'costAmount', key: 'costAmount', align: 'right', render: fmtMoney }, + { title: '销售金额', dataIndex: 'salesAmount', key: 'salesAmount', align: 'right', render: fmtMoney }, + { title: '毛利', dataIndex: 'grossProfit', key: 'grossProfit', align: 'right', render: fmtMoney }, + { + title: '收款状态', + dataIndex: 'collectStatus', + key: 'collectStatus', + width: 96, + render: function (v) { + return React.createElement(Tag, { color: v === '已收款' ? 'success' : 'warning' }, v); + } + } + ]; + }, []); + + var accountColumns = useMemo(function () { + return [ + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 200, ellipsis: true }, + { title: '业务员', dataIndex: 'salesperson', key: 'salesperson', width: 88 }, + { title: '上年结存', dataIndex: 'openBalance', key: 'openBalance', align: 'right', render: fmtMoney }, + { title: '本年充值', dataIndex: 'recharge', key: 'recharge', align: 'right', render: fmtMoney }, + { title: '已用氢费', dataIndex: 'usedH2', key: 'usedH2', align: 'right', render: fmtMoney }, + { title: '已用电费', dataIndex: 'usedElec', key: 'usedElec', align: 'right', render: fmtMoney }, + { title: '期末结余', dataIndex: 'closeBalance', key: 'closeBalance', align: 'right', render: fmtMoney } + ]; + }, []); + + var uncollectedColumns = useMemo(function () { + return [ + { title: '条线', dataIndex: 'line', key: 'line', width: 72 }, + { title: '业务员', dataIndex: 'salesperson', key: 'salesperson', width: 88 }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 180, ellipsis: true }, + { title: '车牌', dataIndex: 'plateNo', key: 'plateNo', width: 110 }, + { title: '应付款日', dataIndex: 'dueDate', key: 'dueDate', width: 110 }, + { title: '应收', dataIndex: 'receivable', key: 'receivable', align: 'right', render: fmtMoney }, + { title: '未收', dataIndex: 'uncollected', key: 'uncollected', align: 'right', render: fmtMoney }, + { + title: '超期天数', + dataIndex: 'overdueDays', + key: 'overdueDays', + align: 'right', + width: 96, + render: function (v) { + var n = Number(v); + if (n > 14) return React.createElement(Tag, { color: 'error' }, v + ' 天'); + if (n > 0) return React.createElement(Tag, { color: 'warning' }, v + ' 天'); + return v; + } + } + ]; + }, []); + + var drillColumns = useMemo(function () { + if (drill.type === 'lease') { + return [ + { title: '车牌号码', dataIndex: 'plateNo', key: 'plateNo', width: 110 }, + { title: '应收', dataIndex: 'receivable', key: 'receivable', align: 'right', render: fmtMoney }, + { title: '实收', dataIndex: 'received', key: 'received', align: 'right', render: fmtMoney }, + { title: '未收', dataIndex: 'uncollected', key: 'uncollected', align: 'right', render: fmtMoney } + ]; + } + return [ + { title: '车牌', dataIndex: 'plateNo', key: 'plateNo', width: 110 }, + { title: '出车日期', dataIndex: 'tripDate', key: 'tripDate', width: 110 }, + { title: '金额', dataIndex: 'amount', key: 'amount', align: 'right', render: fmtMoney }, + { title: '氢费', dataIndex: 'h2Fee', key: 'h2Fee', align: 'right', render: fmtMoney }, + { title: 'ETC', dataIndex: 'etcFee', key: 'etcFee', align: 'right', render: fmtMoney } + ]; + }, [drill.type]); + + var handleExport = useCallback(function () { + var headers = ['业务条线', '应收', '实收', '未收', '成本', '利润']; + var body = [headers].concat(overviewRows.map(function (r) { + return [r.line, r.receivable, r.received, r.uncollected, r.cost, r.profit]; + })); + downloadCsv('客户服务部业务统计_' + ym.year + '_' + new Date().getTime() + '.csv', body); + message.success('已导出经营总览'); + }, [overviewRows, ym.year]); + + var tabItems = useMemo(function () { + return [ + { + key: 'overview', + label: '经营总览', + children: React.createElement(Table, { + className: 'cs-dept-stat-table', + rowKey: 'key', + size: 'small', + bordered: true, + pagination: false, + scroll: { x: 900 }, + columns: overviewColumns, + dataSource: overviewRows + }) + }, + { + key: 'lease', + label: '租赁经营', + children: React.createElement(Table, { + className: 'cs-dept-stat-table', + rowKey: 'key', + size: 'small', + bordered: true, + pagination: { pageSize: 10, showSizeChanger: true }, + scroll: { x: 1100 }, + columns: leaseColumns, + dataSource: leaseRows + }) + }, + { + key: 'logistics', + label: '物流经营', + children: React.createElement(Table, { + className: 'cs-dept-stat-table', + rowKey: 'key', + size: 'small', + bordered: true, + pagination: { pageSize: 10, showSizeChanger: true }, + scroll: { x: 1100 }, + columns: logisticsColumns, + dataSource: logisticsRows + }) + }, + { + key: 'energy', + label: '能源销售', + children: React.createElement(Table, { + className: 'cs-dept-stat-table', + rowKey: 'key', + size: 'small', + bordered: true, + pagination: { pageSize: 10 }, + scroll: { x: 1000 }, + columns: energyColumns, + dataSource: energyRows + }) + }, + { + key: 'account', + label: '成本与账户', + children: React.createElement(Table, { + className: 'cs-dept-stat-table', + rowKey: 'key', + size: 'small', + bordered: true, + pagination: { pageSize: 10 }, + scroll: { x: 1000 }, + columns: accountColumns, + dataSource: accountRows + }) + }, + { + key: 'uncollected', + label: '未收预警', + children: React.createElement(Table, { + className: 'cs-dept-stat-table', + rowKey: 'key', + size: 'small', + bordered: true, + pagination: { pageSize: 10 }, + scroll: { x: 1100 }, + columns: uncollectedColumns, + dataSource: uncollectedRows + }) + } + ]; + }, [ + overviewColumns, overviewRows, leaseColumns, leaseRows, logisticsColumns, logisticsRows, + energyColumns, energyRows, accountColumns, accountRows, uncollectedColumns, uncollectedRows + ]); + + return React.createElement( + App, + null, + React.createElement('style', null, tableStyle), + React.createElement( + 'div', + { style: layoutStyle }, + React.createElement(Breadcrumb, { + style: { marginBottom: 12 }, + items: [ + { title: '数据分析' }, + { title: '客户服务部业务统计' } + ] + }), + React.createElement( + Card, + { bordered: false, style: { marginBottom: 16 } }, + React.createElement('div', { style: { fontSize: 18, fontWeight: 600, marginBottom: 16, color: 'rgba(0,0,0,0.88)' } }, pageTitle), + React.createElement( + Row, + { gutter: 16, align: 'bottom' }, + React.createElement(Col, { xs: 24, sm: 12, md: 6, lg: 4 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '统计年份'), + React.createElement(DatePicker, { + picker: 'year', + style: filterControlStyle, + value: yearDraft, + onChange: setYearDraft, + allowClear: false + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 6, lg: 4 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '统计月份'), + React.createElement(Select, { + style: filterControlStyle, + value: monthDraft, + onChange: setMonthDraft, + options: monthOptions, + allowClear: false + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 6, lg: 5 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '业务员'), + React.createElement(Select, { + style: filterControlStyle, + value: spDraft, + onChange: setSpDraft, + options: spOptions, + allowClear: true, + showSearch: true, + filterOption: filterOption, + placeholder: '全部业务员' + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 6, lg: 5 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '客户名称'), + React.createElement(Select, { + style: filterControlStyle, + value: cuDraft, + onChange: setCuDraft, + options: cuOptions, + allowClear: true, + showSearch: true, + filterOption: filterOption, + placeholder: '全部客户' + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 24, md: 24, lg: 6 }, + React.createElement(Space, { style: { marginBottom: 12 } }, + React.createElement(Button, { type: 'primary', onClick: handleQuery }, '查询'), + React.createElement(Button, { onClick: handleReset }, '重置'), + React.createElement(Button, { onClick: handleExport }, '导出总览') + ) + ) + ) + ), + React.createElement( + Row, + { gutter: 16, style: { marginBottom: 16 }, className: 'cs-dept-kpi' }, + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 4 }, + React.createElement(Card, { size: 'small', bordered: false }, + React.createElement(Statistic, { title: '总收入(应收口径)', value: kpi.totalRevenue, precision: 2, suffix: '元' }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 4 }, + React.createElement(Card, { size: 'small', bordered: false }, + React.createElement(Statistic, { title: '总利润', value: kpi.totalProfit, precision: 2, suffix: '元', valueStyle: { color: '#00b42a' } }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 4 }, + React.createElement(Card, { size: 'small', bordered: false }, + React.createElement(Statistic, { title: '未收余额', value: kpi.uncollected, precision: 2, suffix: '元', valueStyle: { color: '#f53f3f' } }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 4 }, + React.createElement(Card, { size: 'small', bordered: false }, + React.createElement(Statistic, { title: '氢电账户结余', value: kpi.accountBalance, precision: 2, suffix: '元' }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 4 }, + React.createElement(Card, { size: 'small', bordered: false }, + React.createElement(Statistic, { title: '在营车辆', value: kpi.activeVehicles, suffix: '台' }) + ) + ) + ), + React.createElement( + Card, + { bordered: false, bodyStyle: { paddingTop: 12 } }, + React.createElement(Tabs, { + activeKey: activeTab, + onChange: setActiveTab, + items: tabItems, + destroyInactiveTabPane: false + }), + React.createElement('div', { style: { marginTop: 12, fontSize: 12, color: 'rgba(0,0,0,0.45)' } }, + '说明:数据为原型演示;联调后由租赁/物流/能源/收支管控等台账子表汇总写入。租赁、物流汇总行可点击「明细」钻取至车辆/运单层级。' + ) + ), + React.createElement(Modal, { + title: drill.title || '明细', + open: drill.open, + onCancel: closeDrill, + footer: null, + width: 720, + destroyOnClose: true + }, + React.createElement(Table, { + rowKey: 'key', + size: 'small', + bordered: true, + pagination: false, + columns: drillColumns, + dataSource: drill.rows + }) + ) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-data-analysis/pages/02-物流业务台账.jsx b/src/prototypes/oneos-web-data-analysis/pages/02-物流业务台账.jsx new file mode 100644 index 0000000..16216e8 --- /dev/null +++ b/src/prototypes/oneos-web-data-analysis/pages/02-物流业务台账.jsx @@ -0,0 +1,2549 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 数据分析 - 物流业务台账(对齐《2026年业务二部运营台账总表》子表 1.2物流业务台账) +// 业务服务组操作人维护各自台账;主管可查看全部。支持批量导入、表格底部行内新增与删改查。 + +var LOGISTICS_PAGE_STYLE = [ + '.logistics-ledger-page { padding: 24px 24px 80px; min-height: 100vh; display: flex; flex-direction: column; background: linear-gradient(165deg, #f1f5f9 0%, #f8fafc 50%, #f1f5f9 100%); overflow-x: hidden; overflow-y: auto; box-sizing: border-box; }', + '.logistics-ledger-page .lc-filter-card.ant-card { border-radius: 16px !important; border: 1px solid #e2e8f0 !important; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.03) !important; margin-bottom: 16px; }', + '.logistics-ledger-page .lc-filter-card > .ant-card-head { border-bottom: 1px solid #f1f5f9 !important; min-height: auto; padding: 12px 20px !important; }', + '.logistics-ledger-page .lc-filter-card > .ant-card-head .ant-card-head-title { font-size: 15px !important; font-weight: 700 !important; color: #0f172a !important; padding: 0 !important; }', + '.logistics-ledger-page .lc-filter-card > .ant-card-body { padding: 16px 20px 20px !important; }', + '.logistics-ledger-page .lc-filter-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px 24px; align-items: center; }', + '@media (max-width: 1280px) { .logistics-ledger-page .lc-filter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }', + '@media (max-width: 640px) { .logistics-ledger-page .lc-filter-grid { grid-template-columns: 1fr; } }', + '.logistics-ledger-page .lc-filter-field { display: flex; align-items: center; gap: 12px; min-width: 0; min-height: 32px; }', + '.logistics-ledger-page .lc-filter-field-label { flex: 0 0 88px; text-align: right; font-size: 13px; font-weight: 500; color: #475569; line-height: 32px; white-space: nowrap; }', + '.logistics-ledger-page .lc-filter-field-control { flex: 1; min-width: 0; }', + '.logistics-ledger-page .lc-filter-field-control .ant-input, .logistics-ledger-page .lc-filter-field-control .ant-input-affix-wrapper, .logistics-ledger-page .lc-filter-field-control .ant-select .ant-select-selector, .logistics-ledger-page .lc-filter-field-control .ant-input-number, .logistics-ledger-page .lc-filter-field-control .ant-picker { width: 100%; height: 32px !important; min-height: 32px !important; border-radius: 8px !important; box-sizing: border-box; }', + '.logistics-ledger-page .lc-filter-actions { display: flex; align-items: center; justify-content: flex-end; gap: 8px; margin-top: 16px; padding-top: 16px; border-top: 1px solid #f1f5f9; }', + '.logistics-ledger-page .lc-alert-stats-row { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 8px; margin-bottom: 16px; }', + '@media (max-width: 1280px) { .logistics-ledger-page .lc-alert-stats-row { grid-template-columns: repeat(3, minmax(0, 1fr)); } }', + '@media (max-width: 768px) { .logistics-ledger-page .lc-alert-stats-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } }', + '@media (max-width: 480px) { .logistics-ledger-page .lc-alert-stats-row { grid-template-columns: 1fr; } }', + '.logistics-ledger-page .lc-alert-card { display: flex; align-items: center; gap: 8px; padding: 10px 22px 10px 10px; border-radius: 10px; border: 1px solid #e2e8f0; background: #fff; position: relative; overflow: hidden; min-width: 0; min-height: 62px; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.logistics-ledger-page .lc-alert-card-main { flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: flex-start; justify-content: center; gap: 4px; }', + '.logistics-ledger-page .lc-alert-card-icon { flex-shrink: 0; width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: linear-gradient(145deg, #0d7a63 0%, #065f4a 100%); color: #fff; box-shadow: 0 2px 8px rgba(6, 95, 74, 0.22); }', + '.logistics-ledger-page .lc-alert-card-icon svg { width: 18px !important; height: 18px !important; }', + '.logistics-ledger-page .lc-alert-card-val { font-size: 17px; font-weight: 800; line-height: 1.1; color: #0f172a; font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }', + '.logistics-ledger-page .lc-alert-card-title { width: 100%; font-size: 11px; font-weight: 600; color: #64748b; line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }', + '.logistics-ledger-page .lc-alert-card-tip-anchor { position: absolute; top: 6px; right: 6px; z-index: 2; line-height: 0; }', + '.logistics-ledger-page .lc-alert-card-tip { width: 16px; height: 16px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; color: #94a3b8; background: rgba(255,255,255,0.92); border: 1px solid #e2e8f0; cursor: help; line-height: 0; }', + '.logistics-ledger-page .lc-stat-val--profit { color: #047857 !important; }', + '.logistics-ledger-page .lc-stat-val--loss { color: #b91c1c !important; }', + '.logistics-ledger-page .lc-table-section { margin-bottom: 0; }', + '.logistics-ledger-page .lc-table-toolbar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px 16px; margin-bottom: 8px; min-height: 32px; }', + '.logistics-ledger-page .lc-table-toolbar-meta { font-size: 13px; color: #64748b; }', + '.logistics-ledger-page .lc-table-toolbar-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-left: auto; }', + '.logistics-ledger-page .lc-table-card { background: #fff; border-radius: 16px; border: 1px solid #e2e8f0; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.03); padding: 12px 16px 16px; box-sizing: border-box; }', + '.logistics-ledger-page .logistics-ledger-table-wrap { width: 100%; }', + '.logistics-ledger-page .lc-action-btn { font-weight: 600 !important; color: #10b981 !important; padding: 0 !important; }', + '.logistics-ledger-page .lc-action-btn-danger { color: #ef4444 !important; }', + '.logistics-ledger-page .ll-action-icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 8px; cursor: pointer; color: rgba(15, 23, 42, 0.55); transition: background 0.15s ease, color 0.15s ease; }', + '.logistics-ledger-page .ll-action-icon-btn:hover { background: #f0f9ff; color: #1677ff; }', + '.logistics-ledger-page .ll-action-icon-btn:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; }', + '.logistics-ledger-page .ll-action-icon-btn.is-disabled { color: rgba(15, 23, 42, 0.25); cursor: not-allowed; pointer-events: none; }', + '.logistics-ledger-page .ll-row-more-btn.ll-action-icon-btn:hover { background: #f5f5f5; color: rgba(15, 23, 42, 0.75); }', + '.logistics-ledger-page .ll-import-modal-wrap .ant-modal-content { border-radius: 16px; overflow: hidden; box-shadow: 0 24px 48px -12px rgba(15, 23, 42, 0.18); }', + '.logistics-ledger-page .ll-import-modal-wrap .ant-modal-header { padding: 18px 24px 14px; border-bottom: 1px solid #f1f5f9; margin-bottom: 0; }', + '.logistics-ledger-page .ll-import-modal-wrap .ant-modal-title { font-size: 17px; font-weight: 700; color: #0f172a; }', + '.logistics-ledger-page .ll-import-modal-wrap .ant-modal-body { padding: 20px 24px 24px; background: #f8fafc; }', + '.logistics-ledger-page .ll-import-modal-wrap .ant-modal-close { top: 14px; width: 40px; height: 40px; border-radius: 10px; transition: background 0.2s ease, color 0.2s ease; }', + '.logistics-ledger-page .ll-import-modal-wrap .ant-modal-close:hover { background: #f1f5f9; color: #0f172a; }', + '.logistics-ledger-page .ll-import-modal-wrap .ant-modal-close:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; }', + '.logistics-ledger-page .ll-import-content { display: flex; flex-direction: column; gap: 14px; }', + '.logistics-ledger-page .ll-import-steps-card { padding: 18px 20px; border: 1px solid #e2e8f0; border-radius: 12px; background: #fff; box-sizing: border-box; }', + '.logistics-ledger-page .ll-import-steps-card .ll-import-steps { margin: 0; }', + '.logistics-ledger-page .ll-import-steps .ant-steps-item-title { font-size: 14px !important; font-weight: 600 !important; color: #334155 !important; }', + '.logistics-ledger-page .ll-import-steps .ant-steps-item-process .ant-steps-item-title { color: #0f172a !important; }', + '.logistics-ledger-page .ll-import-steps .ant-steps-item-process .ant-steps-item-icon { background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important; border-color: #059669 !important; }', + '.logistics-ledger-page .ll-import-steps .ant-steps-item-finish .ant-steps-item-icon { background: #ecfdf5 !important; border-color: #10b981 !important; }', + '.logistics-ledger-page .ll-import-steps .ant-steps-item-finish .ant-steps-item-icon .ant-steps-icon { color: #059669 !important; }', + '.logistics-ledger-page .ll-import-action-card { padding: 16px 18px; border: 1px solid #e2e8f0; border-radius: 12px; background: #fff; box-sizing: border-box; transition: border-color 0.2s ease, box-shadow 0.2s ease; }', + '.logistics-ledger-page .ll-import-action-card:hover { border-color: #cbd5e1; box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04); }', + '.logistics-ledger-page .ll-import-action-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; }', + '.logistics-ledger-page .ll-import-action-meta { display: flex; align-items: center; gap: 12px; min-width: 0; }', + '.logistics-ledger-page .ll-import-action-icon { flex-shrink: 0; width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; background: linear-gradient(145deg, #ecfdf5 0%, #d1fae5 100%); color: #059669; }', + '.logistics-ledger-page .ll-import-action-icon svg { width: 20px; height: 20px; }', + '.logistics-ledger-page .ll-import-action-label { font-size: 15px; font-weight: 700; color: #0f172a; line-height: 1.35; }', + '.logistics-ledger-page .ll-import-download-btn.ant-btn { flex-shrink: 0; min-height: 44px; height: 44px; padding: 0 18px; border-radius: 10px !important; font-weight: 600; }', + '.logistics-ledger-page .ll-import-upload-card { padding: 0; overflow: hidden; border-style: dashed; background: #fff; }', + '.logistics-ledger-page .ll-import-upload-card:hover { border-color: #34d399; }', + '.logistics-ledger-page .ll-import-upload-card .ll-import-dragger.ant-upload-wrapper { display: block; width: 100%; }', + '.logistics-ledger-page .ll-import-upload-card .ant-upload.ant-upload-drag { display: block; width: 100%; border: none !important; background: transparent !important; padding: 0 !important; margin: 0 !important; }', + '.logistics-ledger-page .ll-import-upload-card .ant-upload-drag .ant-upload-btn { display: block; width: 100%; padding: 28px 16px !important; border: none !important; background: transparent !important; cursor: pointer; }', + '.logistics-ledger-page .ll-import-upload-card .ant-upload-drag:hover .ant-upload-btn { background: #f0fdf4 !important; }', + '.logistics-ledger-page .ll-import-upload-card .ant-upload-drag.ant-upload-drag-hover .ant-upload-btn { background: #ecfdf5 !important; }', + '.logistics-ledger-page .ll-import-upload-inner { display: flex; flex-direction: column; align-items: center; text-align: center; }', + '.logistics-ledger-page .ll-import-upload-icon { display: inline-flex; align-items: center; justify-content: center; width: 48px; height: 48px; margin-bottom: 10px; border-radius: 12px; background: #ecfdf5; color: #059669; }', + '.logistics-ledger-page .ll-import-upload-icon svg { width: 22px; height: 22px; }', + '.logistics-ledger-page .ll-import-upload-title { margin: 0; font-size: 15px; font-weight: 600; color: #0f172a; line-height: 1.4; }', + '.logistics-ledger-page .ll-import-upload-hint { margin: 6px 0 0; font-size: 13px; color: #64748b; line-height: 1.45; }', + '.logistics-ledger-page .ll-import-upload-card .ant-upload-list { padding: 0 16px 12px; margin: 0; text-align: left; }', + '.logistics-ledger-page .ll-import-fail { padding: 12px 14px; border-radius: 10px; background: #fef2f2; border: 1px solid #fecaca; }', + '.logistics-ledger-page .ll-import-fail-title { font-size: 13px; font-weight: 700; color: #b91c1c; margin-bottom: 8px; }', + '.logistics-ledger-page .ll-import-error-list { margin: 0; padding-left: 18px; font-size: 13px; line-height: 1.65; color: #7f1d1d; }', + '@media (prefers-reduced-motion: reduce) { .logistics-ledger-page .ll-import-action-card, .logistics-ledger-page .ll-import-modal-wrap .ant-modal-close { transition: none; } }', + '.logistics-ledger-page .ll-form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 16px; }', + '@media (max-width: 720px) { .logistics-ledger-page .ll-form-grid { grid-template-columns: 1fr; } }', + '.logistics-ledger-page .ll-form-readonly { padding: 8px 10px; border-radius: 8px; background: #f8fafc; border: 1px solid #e2e8f0; font-size: 13px; font-weight: 600; color: #0f766e; }', + '.logistics-ledger-page .ll-table-add-row-wrap { flex-shrink: 0; margin-top: 12px; }', + '.logistics-ledger-page .ll-add-row-btn { border-radius: 8px !important; border-style: dashed !important; border-color: #cbd5e1 !important; color: #475569 !important; font-weight: 600 !important; min-height: 44px !important; background: #fff !important; }', + '.logistics-ledger-page .ll-add-row-btn:hover { border-color: #10b981 !important; color: #059669 !important; }', + '.logistics-ledger-page .ll-req-doc-panel { max-width: 100%; }', + '.logistics-ledger-page .ll-req-doc-panel h1:first-child { margin-top: 0; }' +].join(''); + +var LOGISTICS_TABLE_STYLE = + '.logistics-ledger-table-wrap{border-radius:12px;overflow:hidden;box-shadow:0 4px 24px -6px rgba(15,23,42,0.05),0 0 0 1px rgba(226,232,240,0.9)}' + + '.logistics-ledger-table .ant-table-thead>tr>th,.logistics-ledger-table .ant-table-thead .ant-table-cell{white-space:nowrap;color:#475569!important;font-weight:700!important;font-size:13px!important;background:#f8fafc!important;border-bottom:1px solid #e2e8f0!important;padding:0 8px!important;height:40px!important;text-align:center!important;vertical-align:middle!important}' + + '.logistics-ledger-table .ll-th-required{color:#ef4444!important;margin-left:2px;font-weight:700!important;line-height:1}' + + '.logistics-ledger-table .ant-table-header .ant-table-thead>tr>th,.logistics-ledger-table .ant-table-header .ant-table-thead .ant-table-cell{height:40px!important;line-height:1.35!important}' + + '.logistics-ledger-table .ant-table-tbody>tr:not(.ant-table-measure-row)>td{padding:6px 8px!important;font-size:12px!important;white-space:nowrap;vertical-align:middle!important}' + + '.logistics-ledger-table .ant-table-tbody>tr:not(.ant-table-measure-row):not(.ll-row-draft):hover>td{background:#f0f9ff!important}' + + '.logistics-ledger-table .ant-table-tbody>tr.ll-row-draft>td{background:#f0fdf4!important}' + + '.logistics-ledger-table .ant-table-tbody>tr.ant-table-measure-row,.logistics-ledger-table .ant-table-tbody>tr.ant-table-measure-row>td{height:0!important;max-height:0!important;padding:0!important;border:none!important;line-height:0!important;font-size:0!important;overflow:hidden!important;visibility:hidden!important}' + + '.logistics-ledger-table .ant-table-cell-fix-left,.logistics-ledger-table .ant-table-cell-fix-right{z-index:2!important}' + + '.logistics-ledger-table .ant-table-container .ant-table-header{position:relative;z-index:3;background:#f8fafc;min-height:40px!important}' + + '.logistics-ledger-table .ll-profit-pos{color:#047857;font-weight:600}' + + '.logistics-ledger-table .ll-profit-neg{color:#b91c1c;font-weight:600}' + + '.logistics-ledger-table .ll-h2-warn-cell,.logistics-ledger-table .ll-fee-warn-cell{display:inline-flex;align-items:center;justify-content:flex-end;gap:4px;width:100%;max-width:100%}' + + '.logistics-ledger-table .ll-cell-plain{display:block;min-height:20px;line-height:20px;font-size:12px!important;color:#334155;box-sizing:border-box}' + + '.logistics-ledger-table .ll-cell-input,.logistics-ledger-table .ll-cell-input.ant-input-affix-wrapper,.logistics-ledger-table .ll-cell-input-number,.logistics-ledger-table .ll-cell-input-number .ant-input-number-input,.logistics-ledger-table .ll-cell-date,.logistics-ledger-table .ll-cell-date .ant-picker-input>input{font-size:12px!important;color:#334155!important}' + + '.logistics-ledger-table .ll-cell-input,.logistics-ledger-table .ll-cell-input.ant-input-affix-wrapper{height:28px!important;min-height:28px!important;padding:2px 8px!important;line-height:24px!important;border-radius:6px!important}' + + '.logistics-ledger-table .ll-cell-input-number{width:100%!important;height:28px!important;min-height:28px!important;border-radius:6px!important}' + + '.logistics-ledger-table .ll-cell-input-number .ant-input-number-input{height:26px!important;padding:0 8px!important;text-align:right}' + + '.logistics-ledger-table .ll-cell-date{width:100%!important;height:28px!important;min-height:28px!important;border-radius:6px!important;padding:2px 8px 2px!important}' + + '.logistics-ledger-table .ll-cell-date .ant-picker-input>input{height:24px!important}' + + '.logistics-ledger-table .ll-cell-select.ant-select{font-size:12px!important;width:100%!important;min-width:0}' + + '.logistics-ledger-table .ll-cell-select.ant-select-single{height:28px!important}' + + '.logistics-ledger-table .ll-cell-select.ant-select-single .ant-select-selector{height:28px!important;min-height:28px!important;padding:0 8px!important;font-size:12px!important;border-radius:6px!important}' + + '.logistics-ledger-table .ll-cell-select .ant-select-selection-item,.logistics-ledger-table .ll-cell-select .ant-select-selection-placeholder{line-height:26px!important;font-size:12px!important}' + + '.logistics-ledger-table .ll-cell-select.ll-cell-select--fit .ant-select-selection-item{overflow:visible;text-overflow:clip;white-space:nowrap}'; + +var LL_PRIMARY_BTN = { borderRadius: 8, fontWeight: 600, background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)', border: 'none' }; +var LL_OUTLINE_BTN = { borderRadius: 8, fontWeight: 600, borderColor: '#10b981', color: '#059669' }; +var LL_REQ_BTN_STYLE = { borderRadius: 8, border: '1px solid #cbd5e1', color: '#475569', fontWeight: 600 }; + +var LL_IMPORT_HEADERS = [ + '月份', '出车日期', '业务名称', '司机', '电话', '车辆承接方', '车牌号码', '品牌', '型号', '单价', '数量', '金额', + '氢费', 'ETC费用', '薪资', '电费', '人工报销费用', '日社保服务费', '日挂车费用', '日停车费用', '日轮胎费用', '车辆费用', + '是否多趟', '线路计价', '备注' +]; + +var LL_UNDERTAKER_OPTIONS = [ + { value: '我司', label: '我司' }, + { value: '第三方车队', label: '第三方车队' } +]; + +// 品牌 → 型号级联(与「型号参数」模块一致,并补充车辆档案常用品牌) +var LL_BRAND_MODEL_MAP = { + '苏龙': [ + { value: '海格牌KLQ5180XYKFCEV', label: '海格牌KLQ5180XYKFCEV' }, + { value: 'KLQ6129', label: 'KLQ6129' }, + { value: 'KLQ6106', label: 'KLQ6106' }, + { value: '海格牌KLQ5090', label: '海格牌KLQ5090' } + ], + '东风': [ + { value: 'DFH1180', label: 'DFH1180' }, + { value: 'DFH1250', label: 'DFH1250' }, + { value: 'DFH5180', label: 'DFH5180' } + ], + '福田': [ + { value: 'BJ1180', label: 'BJ1180' }, + { value: 'BJ5180', label: 'BJ5180' }, + { value: '欧曼EST', label: '欧曼EST' } + ], + '江淮': [ + { value: 'HFC1180', label: 'HFC1180' }, + { value: 'HFC5180', label: 'HFC5180' }, + { value: '帅铃Q6', label: '帅铃Q6' } + ], + '重汽': [ + { value: 'ZZ5180', label: 'ZZ5180' }, + { value: 'ZZ1250', label: 'ZZ1250' }, + { value: '豪沃T7H', label: '豪沃T7H' } + ], + '陕汽': [ + { value: 'SX1180', label: 'SX1180' }, + { value: 'SX5180', label: 'SX5180' }, + { value: '德龙X3000', label: '德龙X3000' } + ], + '现代': [ + { value: '帕力安牌4.5吨冷链车', label: '帕力安牌4.5吨冷链车' }, + { value: '帕力安牌18吨双飞翼货车', label: '帕力安牌18吨双飞翼货车' }, + { value: '4.5吨货车', label: '4.5吨货车' } + ], + '飞驰': [ + { value: '49吨牵引车头', label: '49吨牵引车头' } + ], + '宇通': [ + { value: '49吨牵引车头', label: '49吨牵引车头' } + ] +}; + +// 我司车辆档案:车牌 → 品牌/型号(原型数据,联调时对接车辆管理) +var LL_VEHICLE_PLATE_REGISTRY = { + '粤AGQ8393': { brand: '现代', model: '帕力安牌4.5吨冷链车' }, + '粤AGR3208': { brand: '现代', model: '帕力安牌4.5吨冷链车' }, + '粤AGP9835': { brand: '现代', model: '帕力安牌4.5吨冷链车' }, + '浙F02608F': { brand: '现代', model: '帕力安牌18吨双飞翼货车' }, + '浙F00688F': { brand: '飞驰', model: '49吨牵引车头' }, + '沪A66921F': { brand: '苏龙', model: '海格牌KLQ5180XYKFCEV' }, + '浙F07588F': { brand: '飞驰', model: '49吨牵引车头' }, + '粤AGP5156': { brand: '现代', model: '帕力安牌4.5吨冷链车' } +}; + +// 车辆氢费明细(原型):车牌 + 加氢日期 → 成本总价;联调后对接「车辆氢费明细」接口按日汇总 +var LL_H2_FEE_DETAIL_MOCK = [ + { plateNo: '粤AGQ8393', refuelDate: '2026-01-01', costTotal: 300.00 }, + { plateNo: '粤AGR3208', refuelDate: '2026-01-01', costTotal: 304.15 }, + { plateNo: '粤AGP9835', refuelDate: '2026-01-01', costTotal: 220.00 }, + { plateNo: '浙F02608F', refuelDate: '2026-02-03', costTotal: 420.00 } +]; + +// ETC 通行记录(原型):车牌 + 通行日期 → 通行费用;联调后对接「ETC记录」按日汇总 +var LL_ETC_RECORD_MOCK = [ + { plateNo: '粤AGQ8393', passDate: '2026-01-01', fee: 12.50 }, + { plateNo: '粤AGQ8393', passDate: '2026-01-01', fee: 8.00 }, + { plateNo: '粤AGP9835', passDate: '2026-01-01', fee: 22.00 }, + { plateNo: '粤AGP9835', passDate: '2026-01-01', fee: 15.04 }, + { plateNo: '浙F02608F', passDate: '2026-02-03', fee: 30.00 }, + { plateNo: '浙F02608F', passDate: '2026-02-03', fee: 32.00 } +]; + +var LL_KPI_CARDS = [ + { + key: 'count', + title: '台账条数', + desc: '统计当前筛选条件下的全部台账记录条数。例如筛选后共有记录 A、B、C 三条,则台账条数 = 3。', + format: 'count' + }, + { + key: 'amount', + title: '金额合计', + desc: '将当前筛选条件下每条台账记录的「金额」逐项相加:记录 A 金额 + 记录 B 金额 + 记录 C 金额 + …', + format: 'money' + }, + { + key: 'totalCost', + title: '总成本合计', + desc: '将当前筛选条件下每条台账记录的「总成本」逐项相加:记录 A 总成本 + 记录 B 总成本 + 记录 C 总成本 + …', + format: 'money' + }, + { + key: 'hydrogenFee', + title: '氢费总计', + desc: '将当前筛选条件下每条台账记录的「氢费」逐项相加:记录 A 氢费 + 记录 B 氢费 + 记录 C 氢费 + …', + format: 'money' + }, + { + key: 'etcFee', + title: 'ETC费用总计', + desc: '将当前筛选条件下每条台账记录的「ETC费用」逐项相加:记录 A ETC费用 + 记录 B ETC费用 + 记录 C ETC费用 + …', + format: 'money' + }, + { + key: 'profitLoss', + title: '盈亏合计', + desc: '将当前筛选条件下每条台账记录的「盈亏」逐项相加:记录 A 盈亏 + 记录 B 盈亏 + 记录 C 盈亏 + …', + format: 'money' + } +]; + +var LL_MULTI_TRIP_OPTIONS = [ + { value: '否', label: '否' }, + { value: '是', label: '是' } +]; + +var LL_OPERATORS = [ + { id: 'u_tan', name: '谈云', role: 'staff' }, + { id: 'u_liu', name: '刘念念', role: 'staff' }, + { id: 'u_dong', name: '董剑煜', role: 'staff' }, + { id: 'u_super', name: '王主管', role: 'supervisor' } +]; + +var LL_CHANGE_LOG_FIELD_LABELS = { + month: '月份', + dispatchDate: '出车日期', + businessName: '业务名称', + driver: '司机', + phone: '电话', + vehicleUndertaker: '车辆承接方', + plateNo: '车牌号码', + brand: '品牌', + model: '型号', + unitPrice: '单价', + quantity: '数量', + amount: '金额', + hydrogenFee: '氢费', + etcFee: 'ETC费用', + salary: '薪资', + electricityFee: '电费', + manualReimburse: '人工报销费用', + dailySocialSecurity: '日社保服务费', + dailyTrailer: '日挂车费用', + dailyParking: '日停车费用', + dailyTire: '日轮胎费用', + vehicleFee: '车辆费用', + multiTrip: '是否多趟', + routePricing: '线路计价', + remark: '备注', + _create: '新增记录', + _import: '批量导入' +}; + +var LL_LOG_TRACK_FIELDS = [ + 'month', 'dispatchDate', 'businessName', 'driver', 'phone', 'vehicleUndertaker', 'plateNo', 'brand', 'model', + 'unitPrice', 'quantity', 'amount', 'hydrogenFee', 'etcFee', 'salary', 'electricityFee', 'manualReimburse', + 'dailySocialSecurity', 'dailyTrailer', 'dailyParking', 'dailyTire', 'vehicleFee', 'multiTrip', 'routePricing', 'remark' +]; + +var LL_ZH_DATE_LOCALE = { + lang: { + locale: 'zh_CN', + placeholder: '请选择日期', + rangePlaceholder: ['开始日期', '结束日期'], + today: '今天', + now: '此刻', + backToToday: '返回今天', + ok: '确定', + clear: '清除', + month: '月', + year: '年', + monthSelect: '选择月份', + yearSelect: '选择年份', + decadeSelect: '选择年代', + previousMonth: '上个月', + nextMonth: '下个月', + previousYear: '上一年', + nextYear: '下一年', + shortWeekDays: ['日', '一', '二', '三', '四', '五', '六'], + shortMonths: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'] + }, + timePickerLocale: { placeholder: '请选择时间' } +}; + +function llBrandOptions() { + return Object.keys(LL_BRAND_MODEL_MAP).map(function (b) { return { value: b, label: b }; }); +} + +function llModelOptions(brand) { + if (!brand || !LL_BRAND_MODEL_MAP[brand]) return []; + return LL_BRAND_MODEL_MAP[brand]; +} + +function llIsOurUndertaker(v) { return v === '我司'; } + +function llLookupByPlate(plateNo) { + if (!plateNo) return null; + return LL_VEHICLE_PLATE_REGISTRY[String(plateNo).trim()] || null; +} + +function llApplyPlateLookup(row) { + if (!llIsOurUndertaker(row.vehicleUndertaker)) return row; + var hit = llLookupByPlate(row.plateNo); + if (!hit) return Object.assign({}, row, { brand: row.brand || undefined, model: row.model || undefined }); + return Object.assign({}, row, { brand: hit.brand, model: hit.model }); +} + +function llColTitle(label) { + return React.createElement('span', { className: 'll-th-title' }, + label, + React.createElement('span', { className: 'll-th-required', title: '必填' }, '*') + ); +} + +function llSvgIcon(paths, size) { + return React.createElement('svg', { + width: size || 18, height: size || 18, viewBox: '0 0 24 24', fill: 'none', + stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true + }, paths.map(function (p, i) { + if (p.tag === 'circle') return React.createElement('circle', { key: i, cx: p.cx, cy: p.cy, r: p.r }); + if (p.tag === 'line') return React.createElement('line', { key: i, x1: p.x1, y1: p.y1, x2: p.x2, y2: p.y2 }); + return React.createElement('path', { key: i, d: p.d }); + })); +} + +// PRD 正文同步:物流业务台账-产品需求说明.md、物流业务台账-需求内容.js +var LL_REQUIREMENT_DOC = `# 物流业务台账 — 产品需求说明(PRD) + +| 项目 | 内容 | +|------|------| +| 文档版本 | v1.0 | +| 产品模块 | 数据分析 → 物流业务台账 | +| 文档类型 | 产品需求说明 | +| 适用读者 | 产品、业务、研发、测试 | +| 对齐来源 | 《2026年业务二部运营台账总表》子表「1.2物流业务台账」 | + +--- + +## 一、为什么做这件事 + +### 1.1 业务背景 + +业务二部物流运营涉及出车记录、营收、氢费、ETC、人工及车辆日成本等多类费用,原先分散在 Excel 子表中维护,存在以下问题: + +- 各操作人各自维护,主管难以统一查看与核对; +- 氢费、ETC 等费用与系统台账(车辆氢费明细、ETC 记录)缺乏自动比对,易录入偏差; +- 缺少按筛选条件汇总营收与成本的能力,经营分析效率低。 + +### 1.2 产品目标 + +建设 Web 端「物流业务台账」页面,支撑业务服务组操作人**录入、查询、导入、导出、删改**各自台账;主管可**查看全部**操作人数据;系统自动计算总成本、盈亏,并对氢费/ETC 异常给出提示。 + +### 1.3 本期不做 + +- 与财务系统过账、审批流(后续迭代); +- 复杂组织架构数据权限(本期按「操作人 / 主管」两类角色); +- 菜单路由注册(由 Axhub / 系统导航统一配置)。 + +--- + +## 二、角色与权限 + +| 角色 | 数据范围 | 可执行操作 | +|------|----------|------------| +| **业务服务组操作人** | 仅本人维护的台账 | 筛选、新增、行内编辑、弹窗编辑、导入、导出、批量删除(本人数据)、查看变更日志 | +| **业务服务组主管** | 全部操作人台账 | 筛选、查看、编辑/删除任意记录、导出、批量删除、查看变更日志;**不可**导入、表格底部新增 | + +**原型说明:** 页面顶部提供「当前登录(原型切换)」下拉,用于演示不同角色;正式环境由系统登录态与权限接口驱动。 + +--- + +## 三、页面结构 + +用户进入「数据分析 → 物流业务台账」后,自上而下为: + +1. 右上角 **「查看需求说明」** +2. **原型用户切换**(仅原型) +3. **筛选条件区** +4. **KPI 统计卡片**(6 张) +5. **列表工具栏**(批量导入 / 批量删除 / 导出) +6. **数据列表** +7. **表格底部新增入口**(操作人可见) +8. **弹窗**:批量导入、编辑台账、变更日志、需求说明 + +--- + +## 四、筛选条件 + +**交互原则:** 修改条件后需点击 **「查询」** 生效;**「重置」** 清空条件。 + +**默认展示:** 第一行 4 项(月份、出车日期、业务名称、车牌号码);点击 **「展开更多筛选项」** 显示其余条件;可 **「收起」**。 + +| 筛选项 | 说明 | +|--------|------| +| 月份 | 月份选择器,中文展示(如 2026年01月);占位「全部月份」 | +| 出车日期 | 日期区间,占位「开始日期 / 结束日期」,中文选择器 | +| 业务名称 | 下拉选择器,支持输入模糊搜索匹配项;查询为精确匹配所选业务 | +| 车牌号码 | **多选**;未选任何车牌时视为「全部」;可勾选/取消单个车牌 | +| 车辆承接方 | 我司 / 第三方车队 | +| 品牌 | 下拉,与型号级联 | +| 型号 | 需先选品牌;选项来自「型号参数」模块品牌型号映射 | +| 维护人 | **仅主管可见**;按台账维护人筛选 | + +--- + +## 五、KPI 统计卡片 + +统计范围为 **当前筛选条件下的全部台账记录**,卡片右上角 ? 可查看计算说明。 + +| 卡片 | 计算规则(示例表述) | +|------|----------------------| +| 台账条数 | 筛选后记录 A、B、C… 的条数合计 | +| 金额合计 | 记录 A 金额 + 记录 B 金额 + 记录 C 金额 + … | +| 总成本合计 | 记录 A 总成本 + 记录 B 总成本 + … | +| 氢费总计 | 记录 A 氢费 + 记录 B 氢费 + … | +| ETC费用总计 | 记录 A ETC费用 + 记录 B ETC费用 + … | +| 盈亏合计 | 记录 A 盈亏 + 记录 B 盈亏 + … | + +卡片顺序:台账条数 → 金额合计 → 总成本合计 → 氢费总计 → ETC费用总计 → 盈亏合计。 + +--- + +## 六、列表字段与排序 + +### 6.1 主要列 + +序号、月份、出车日期、业务名称、司机、电话、车辆承接方、车牌号码、品牌、型号、单价、数量、金额、氢费、ETC费用、薪资、电费、人工报销费用、日社保服务费、日挂车费用、日停车费用、日轮胎费用、车辆费用、总成本、盈亏、是否多趟、线路计价、备注、**操作人**、**最后操作时间**、操作。 + +- **主管**额外在列表前部展示 **维护人** 列。 +- **出车日期** 默认 **降序**;点击表头可在降序/升序间切换。 +- **数量** 支持 2 位小数。 + +### 6.2 自动计算 + +- **金额** = 单价 × 数量(修改单价或数量时自动重算,亦可手填金额)。 +- **总成本** = 氢费 + ETC费用 + 薪资 + 电费 + 人工报销费用 + 日社保服务费 + 日挂车费用 + 日停车费用 + 日轮胎费用 + 车辆费用。 +- **盈亏** = 金额 − 总成本。 + +### 6.3 氢费 / ETC 异常提示 + +当台账填写的氢费或 ETC 与系统按 **车牌 + 出车日期** 汇总值不一致时,对应单元格显示橙色警告图标,悬停提示: + +- 氢费:\`成本总价(元):xxx\`(对接「车辆氢费明细」按日汇总) +- ETC:\`ETC记录总金额(元):xxx\`(对接 ETC 通行记录按日汇总) + +--- + +## 七、车辆承接方与车牌 + +| 承接方 | 车牌 | 品牌型号 | 校验 | +|--------|------|----------|------| +| **我司** | 下拉选择档案车牌 | 根据车牌自动带出,不可手改 | 保存时校验车牌存在于车辆档案 | +| **第三方车队** | 自由输入 | 品牌、型号手动级联选择 | 不校验车牌档案 | + +品牌型号级联与「型号参数」模块一致(现代、飞驰、宇通等)。 + +--- + +## 八、维护方式 + +### 8.1 表格底部新增(操作人) + +- 点击 **「新增一行」** 在表格底部追加 **草稿行**(绿色背景)。 +- 草稿行支持 **行内编辑**;操作列显示 **保存 / 取消**。 +- 保存前校验:出车日期、业务名称、车辆承接方、车牌号码、单价、数量,以及氢费/ETC/薪资/电费/人工报销/日社保/日挂车/日停车/日轮胎等费用项(允许填 0)。 + +### 8.2 弹窗编辑 + +- 已有记录:操作列 **更多(⋮)→ 编辑**,弹窗表单修改。 +- 保存后更新 **操作人、最后操作时间**,并写入变更日志。 + +### 8.3 批量导入(操作人) + +1. 下载 CSV 模板(字段对齐子表 1.2,月份格式 YYYY-MM)。 +2. 上传 CSV;导入记录归属当前登录操作人。 +3. 车辆承接方为「我司」时按车牌自动匹配品牌型号;总成本、盈亏系统计算。 + +### 8.4 导出 + +- 导出 **当前筛选结果全部记录**(含排序后顺序)。 +- 字段含导入模板全部列 + 总成本、盈亏;主管导出额外含维护人。 +- 文件名:\`物流业务台账_时间戳.csv\`。 + +### 8.5 批量删除 + +- 列表左侧多选;仅可勾选本人有权维护的记录。 +- 工具栏 **批量删除**,二次确认后删除所选记录。 + +### 8.6 单条删除 + +- 更多 → 删除,二次确认。 + +--- + +## 九、操作列 + +| 行状态 | 操作列展示 | +|--------|------------| +| 草稿行(操作人) | 保存、取消 | +| 已保存(有权维护) | 更多 ⋮:编辑、删除、变更日志 | +| 无权维护 | 无操作 | + +样式参照「车辆氢费明细」操作列图标按钮。 + +--- + +## 十、变更日志 + +入口:操作列 **更多 → 变更日志**。 + +记录以下场景的字段变更:行内新增保存、弹窗编辑、批量导入。展示:修改时间、修改人、修改字段、修改前、修改后。 + +--- + +## 十一、联调接口(研发) + +| 能力 | 对接说明 | +|------|----------| +| 车辆档案 | 「车辆管理」车牌 → 品牌/型号 | +| 氢费比对 | 「车辆氢费明细」按车牌+加氢日期汇总 costTotal | +| ETC 比对 | ETC 通行记录按车牌+通行日期汇总 fee | +| 品牌型号 | 「型号参数」品牌型号级联 | +| 用户权限 | 登录用户 ID、角色(操作人/主管) | + +--- + +## 十二、验收要点(测试) + +1. 操作人仅见本人数据;主管见全部。 +2. 筛选默认收起第一行;展开后条件查询正确;车牌多选空=全部。 +3. KPI 随筛选变化;? 提示文案正确。 +4. 出车日期默认降序,表头切换升序。 +5. 我司/第三方车队车牌与品牌型号联动及校验。 +6. 氢费/ETC 不一致时出现警告提示。 +7. 导入、导出、批量删除、变更日志流程可用。 +8. 操作人、最后操作时间在保存/编辑后更新。 + +**文档结束** +`; + +var LL_ICONS = { + upload: llSvgIcon([{ d: 'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4' }, { d: 'M17 8l-5-5-5 5' }, { tag: 'line', x1: 12, y1: 3, x2: 12, y2: 15 }], 14), + download: llSvgIcon([{ d: 'M12 3v12' }, { d: 'm7 10 5 5 5-5' }, { d: 'M5 21h14' }], 14), + plus: llSvgIcon([{ tag: 'line', x1: 12, y1: 5, x2: 12, y2: 19 }, { tag: 'line', x1: 5, y1: 12, x2: 19, y2: 12 }], 14), + truck: llSvgIcon([{ d: 'M10 17h4V5H2v12h3' }, { d: 'M20 17h2v-3.34a4 4 0 0 0-1.17-2.83L19 9h-5v8h1' }, { tag: 'circle', cx: 7.5, cy: 17.5, r: 2.5 }, { tag: 'circle', cx: 17.5, cy: 17.5, r: 2.5 }], 22), + warn: llSvgIcon([ + { d: 'M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z' }, + { tag: 'line', x1: 12, y1: 9, x2: 12, y2: 13 }, + { tag: 'line', x1: 12, y1: 17, x2: 12.01, y2: 17 } + ], 14), + doc: llSvgIcon([ + { d: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z' }, + { tag: 'polyline', points: '14 2 14 8 20 8' }, + { tag: 'line', x1: 16, y1: 13, x2: 8, y2: 13 }, + { tag: 'line', x1: 16, y1: 17, x2: 8, y2: 17 } + ], 14) +}; + +function llParsePrdInlineText(text) { + var parts = String(text || '').split(/(\*\*[^*]+\*\*)/g); + var nodes = []; + var i; + for (i = 0; i < parts.length; i++) { + var p = parts[i]; + if (!p) continue; + if (p.indexOf('**') === 0 && p.lastIndexOf('**') === p.length - 2) { + nodes.push(React.createElement('strong', { key: i }, p.slice(2, -2))); + } else { + nodes.push(p); + } + } + return nodes.length === 1 ? nodes[0] : nodes; +} + +function llIsPrdTableRow(line) { + return /^\|.+\|$/.test(String(line || '').trim()); +} + +function llIsPrdTableSep(line) { + return /^\|[\s\-:|]+\|$/.test(String(line || '').trim()); +} + +function llRenderPrdTableRow(line, rowKey, isHeader) { + var cells = String(line).trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map(function (c) { return c.trim(); }); + return React.createElement('tr', { key: rowKey }, + cells.map(function (cell, ci) { + var Tag = isHeader ? 'th' : 'td'; + return React.createElement(Tag, { + key: ci, + style: { + border: '1px solid #e5e7eb', + padding: '8px 10px', + textAlign: 'left', + verticalAlign: 'top', + fontWeight: isHeader ? 600 : 400, + background: isHeader ? '#f8fafc' : '#fff', + fontSize: 13, + lineHeight: 1.5 + } + }, llParsePrdInlineText(cell)); + }) + ); +} + +function llRenderPrdMarkdown(markdown) { + var lines = String(markdown || '').split(/\r?\n/); + var nodes = []; + var i = 0; + while (i < lines.length) { + var line = lines[i]; + var trimmed = String(line || '').trim(); + if (trimmed === '---') { + nodes.push(React.createElement('hr', { key: 'hr-' + i, style: { border: 'none', borderTop: '1px solid #e8ecf0', margin: '20px 0' } })); + i += 1; + continue; + } + if (llIsPrdTableRow(trimmed)) { + var tableLines = []; + while (i < lines.length && llIsPrdTableRow(String(lines[i]).trim())) { + tableLines.push(String(lines[i]).trim()); + i += 1; + } + var bodyRows = []; + var ti; + for (ti = 0; ti < tableLines.length; ti++) { + if (llIsPrdTableSep(tableLines[ti])) continue; + bodyRows.push(llRenderPrdTableRow(tableLines[ti], 'tr-' + i + '-' + ti, ti === 0)); + } + if (bodyRows.length) { + nodes.push(React.createElement('div', { key: 'tbl-' + i, style: { overflowX: 'auto', margin: '12px 0 16px' } }, + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 13 } }, + React.createElement('tbody', null, bodyRows) + ) + )); + } + continue; + } + if (!trimmed) { i += 1; continue; } + if (trimmed.indexOf('# ') === 0) { + nodes.push(React.createElement('h1', { key: 'h1-' + i, style: { fontSize: 20, fontWeight: 700, color: '#0f172a', margin: '0 0 16px', lineHeight: 1.35 } }, llParsePrdInlineText(trimmed.slice(2).trim()))); + i += 1; continue; + } + if (trimmed.indexOf('## ') === 0) { + nodes.push(React.createElement('h2', { key: 'h2-' + i, style: { fontSize: 16, fontWeight: 700, color: '#1e293b', margin: '24px 0 12px', paddingBottom: 6, borderBottom: '2px solid #e0f2fe', lineHeight: 1.4 } }, llParsePrdInlineText(trimmed.slice(3).trim()))); + i += 1; continue; + } + if (trimmed.indexOf('### ') === 0) { + nodes.push(React.createElement('h3', { key: 'h3-' + i, style: { fontSize: 14, fontWeight: 600, color: '#334155', margin: '16px 0 8px', lineHeight: 1.45 } }, llParsePrdInlineText(trimmed.slice(4).trim()))); + i += 1; continue; + } + if (trimmed === '**文档结束**') { + nodes.push(React.createElement('div', { key: 'end-' + i, style: { marginTop: 24, paddingTop: 16, borderTop: '1px dashed #e2e8f0', color: '#94a3b8', fontSize: 13, textAlign: 'center' } }, '— 文档结束 —')); + i += 1; continue; + } + if (/^\d+\.\s/.test(trimmed)) { + nodes.push(React.createElement('div', { key: 'ol-' + i, style: { fontSize: 13, color: '#475569', lineHeight: 1.75, margin: '6px 0 6px 4px', paddingLeft: 4 } }, llParsePrdInlineText(trimmed))); + i += 1; continue; + } + if (trimmed.indexOf('- ') === 0) { + nodes.push(React.createElement('div', { key: 'ul-' + i, style: { display: 'flex', gap: 8, fontSize: 13, color: '#475569', lineHeight: 1.75, margin: '4px 0 4px 2px' } }, + React.createElement('span', { style: { color: '#1677ff', flexShrink: 0 } }, '•'), + React.createElement('span', { style: { flex: 1 } }, llParsePrdInlineText(trimmed.slice(2).trim())) + )); + i += 1; continue; + } + nodes.push(React.createElement('p', { key: 'p-' + i, style: { fontSize: 13, color: '#475569', lineHeight: 1.75, margin: '6px 0' } }, llParsePrdInlineText(trimmed))); + i += 1; + } + return nodes; +} + +function renderLlRequirementDocPanel() { + return React.createElement('div', { className: 'll-req-doc-panel', style: { padding: '4px 4px 16px' } }, llRenderPrdMarkdown(LL_REQUIREMENT_DOC)); +} + +var _llRowSeq = 1; +function llNextKey() { return 'll-' + (_llRowSeq++); } + +function llNum(v) { + if (v === null || v === undefined || v === '') return 0; + var n = Number(v); + return isNaN(n) ? 0 : n; +} + +function llRound(n) { return Math.round(llNum(n) * 100) / 100; } + +function llFmtMoney(n) { + return llRound(n).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); +} + +function llFmtQty(n) { + if (n === null || n === undefined || n === '') return '-'; + return llRound(n).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); +} + +function llDispatchDayKey(v) { + var d = llParseDate(v); + if (d && typeof d.isValid === 'function' && d.isValid()) return d.format('YYYY-MM-DD'); + return ''; +} + +function llDispatchDateSortValue(v) { + var d = llParseDate(v); + if (d && typeof d.isValid === 'function' && d.isValid()) return d.valueOf(); + return 0; +} + +function llCompareDispatchDate(a, b) { + return llDispatchDateSortValue(a && a.dispatchDate) - llDispatchDateSortValue(b && b.dispatchDate); +} + +function llGetH2DetailCostTotal(plateNo, dispatchDate) { + if (!plateNo || !dispatchDate) return null; + var day = llDispatchDayKey(dispatchDate); + if (!day) return null; + var plate = String(plateNo).trim(); + var total = 0; + var found = false; + var i; + for (i = 0; i < LL_H2_FEE_DETAIL_MOCK.length; i++) { + var item = LL_H2_FEE_DETAIL_MOCK[i]; + if (item.plateNo === plate && item.refuelDate === day) { + total += llNum(item.costTotal); + found = true; + } + } + return found ? llRound(total) : null; +} + +function llIsHydrogenFeeMismatch(row) { + var detailTotal = llGetH2DetailCostTotal(row && row.plateNo, row && row.dispatchDate); + if (detailTotal == null) return false; + return Math.abs(llNum(row && row.hydrogenFee) - detailTotal) > 0.009; +} + +function llHydrogenFeeMismatchTip(row) { + var detailTotal = llGetH2DetailCostTotal(row && row.plateNo, row && row.dispatchDate); + if (detailTotal == null) return ''; + return '成本总价(元):' + llFmtMoney(detailTotal); +} + +function llGetEtcRecordTotal(plateNo, dispatchDate) { + if (!plateNo || !dispatchDate) return null; + var day = llDispatchDayKey(dispatchDate); + if (!day) return null; + var plate = String(plateNo).trim(); + var total = 0; + var found = false; + var i; + for (i = 0; i < LL_ETC_RECORD_MOCK.length; i++) { + var item = LL_ETC_RECORD_MOCK[i]; + if (item.plateNo === plate && item.passDate === day) { + total += llNum(item.fee); + found = true; + } + } + return found ? llRound(total) : null; +} + +function llIsEtcFeeMismatch(row) { + var recordTotal = llGetEtcRecordTotal(row && row.plateNo, row && row.dispatchDate); + if (recordTotal == null) return false; + return Math.abs(llNum(row && row.etcFee) - recordTotal) > 0.009; +} + +function llEtcFeeMismatchTip(row) { + var recordTotal = llGetEtcRecordTotal(row && row.plateNo, row && row.dispatchDate); + if (recordTotal == null) return ''; + return 'ETC记录总金额(元):' + llFmtMoney(recordTotal); +} + +function llIsSupervisor(user) { return user && user.role === 'supervisor'; } + +function llNow() { + return window.dayjs ? window.dayjs() : null; +} + +function llFmtDateTime(v) { + if (!v) return '-'; + if (typeof v.format === 'function' && typeof v.isValid === 'function' && v.isValid()) { + return v.format('YYYY-MM-DD HH:mm:ss'); + } + var d = llParseDate(v); + if (d && d.isValid && d.isValid()) return d.format('YYYY-MM-DD HH:mm:ss'); + return String(v); +} + +function llTouchOperateMeta(row, user) { + return Object.assign({}, row, { + operatorName: user && user.name ? user.name : row.operatorName, + lastOperateAt: llNow() + }); +} + +function llFormatLogValue(field, val) { + if (val == null || val === '') return '—'; + if (field === 'dispatchDate') return llFmtDate(val); + if (field === 'month') return llFmtMonth(val); + if (['unitPrice', 'quantity', 'amount', 'hydrogenFee', 'etcFee', 'salary', 'electricityFee', 'manualReimburse', 'dailySocialSecurity', 'dailyTrailer', 'dailyParking', 'dailyTire', 'vehicleFee', 'totalCost', 'profitLoss'].indexOf(field) >= 0) { + return String(val); + } + return String(val); +} + +function llParseDate(v) { + if (!v || v === '-') return null; + if (window.dayjs) { + var d = window.dayjs(v); + if (d.isValid()) return d; + var serial = Number(v); + if (!isNaN(serial) && serial > 30000) { + var base = window.dayjs('1899-12-30'); + return base.add(serial, 'day'); + } + } + return null; +} + +function llFmtDate(v) { + var d = llParseDate(v); + if (d && typeof d.isValid === 'function' && d.isValid()) return d.format('YYYY-MM-DD'); + return v ? String(v) : '-'; +} + +function llParseMonth(v, fallbackDate) { + if (v && typeof v.format === 'function' && typeof v.isValid === 'function' && v.isValid()) { + return v.format('YYYY-MM'); + } + if (v === null || v === undefined || v === '') { + var fd = llParseDate(fallbackDate); + if (fd && fd.isValid()) return fd.format('YYYY-MM'); + return ''; + } + var s = String(v).trim(); + if (window.dayjs) { + var dm = window.dayjs(s, 'YYYY-MM', true); + if (dm.isValid()) return dm.format('YYYY-MM'); + var n = Number(s); + if (!isNaN(n) && n >= 1 && n <= 12) { + var base = llParseDate(fallbackDate); + var y = (base && base.isValid()) ? base.year() : window.dayjs().year(); + return y + '-' + String(n).padStart(2, '0'); + } + } + return s; +} + +function llFmtMonth(v) { + var parsed = llParseMonth(v); + return parsed || '-'; +} + +function llMonthToDayjs(v, fallbackDate) { + if (!v && v !== 0) { + var fd = llParseDate(fallbackDate); + return fd && fd.isValid() ? fd.startOf('month') : null; + } + if (window.dayjs) { + if (v && typeof v.format === 'function' && typeof v.isValid === 'function' && v.isValid()) { + return v.startOf('month'); + } + var d = window.dayjs(String(v).trim(), 'YYYY-MM', true); + if (d.isValid()) return d; + var n = Number(v); + if (!isNaN(n) && n >= 1 && n <= 12) { + var base = llParseDate(fallbackDate); + var y = (base && base.isValid()) ? base.year() : window.dayjs().year(); + return window.dayjs(new Date(y, n - 1, 1)); + } + } + return null; +} + +function llRecalcRow(row) { + var amount = llRound(row.amount); + if (!amount && row.unitPrice != null && row.quantity != null) { + amount = llRound(llNum(row.unitPrice) * llNum(row.quantity)); + } + var totalCost = llRound( + llNum(row.hydrogenFee) + llNum(row.etcFee) + llNum(row.salary) + llNum(row.electricityFee) + + llNum(row.manualReimburse) + llNum(row.dailySocialSecurity) + llNum(row.dailyTrailer) + + llNum(row.dailyParking) + llNum(row.dailyTire) + llNum(row.vehicleFee) + ); + return Object.assign({}, row, { + amount: amount, + totalCost: totalCost, + profitLoss: llRound(amount - totalCost) + }); +} + +function llCanView(row, user, supervisor) { + if (supervisor) return true; + return row && row.createdBy === user.id; +} + +function llCanMutate(row, user, supervisor) { + if (supervisor) return true; + return row && row.createdBy === user.id; +} + +function llEscapeCsv(v) { + var s = v == null ? '' : String(v); + return /[",\n\r]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s; +} + +function llDownloadCsv(filename, headers, rows) { + var lines = [headers.map(llEscapeCsv).join(',')].concat( + (rows || []).map(function (r) { return r.map(llEscapeCsv).join(','); }) + ); + var blob = new Blob(['\ufeff' + lines.join('\n')], { type: 'text/csv;charset=utf-8' }); + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); +} + +function llBuildExportHeaders(supervisor) { + var headers = LL_IMPORT_HEADERS.concat(['总成本', '盈亏']); + if (supervisor) headers.push('维护人'); + return headers; +} + +function llBuildExportRow(r, supervisor) { + var row = [ + r.month || '', + r.dispatchDate || '', + r.businessName || '', + r.driver || '', + r.phone || '', + r.vehicleUndertaker || '', + r.plateNo || '', + r.brand || '', + r.model || '', + r.unitPrice != null && r.unitPrice !== '' ? r.unitPrice : '', + r.quantity != null && r.quantity !== '' ? r.quantity : '', + r.amount != null && r.amount !== '' ? r.amount : '', + r.hydrogenFee != null && r.hydrogenFee !== '' ? r.hydrogenFee : '', + r.etcFee != null && r.etcFee !== '' ? r.etcFee : '', + r.salary != null && r.salary !== '' ? r.salary : '', + r.electricityFee != null && r.electricityFee !== '' ? r.electricityFee : '', + r.manualReimburse != null && r.manualReimburse !== '' ? r.manualReimburse : '', + r.dailySocialSecurity != null && r.dailySocialSecurity !== '' ? r.dailySocialSecurity : '', + r.dailyTrailer != null && r.dailyTrailer !== '' ? r.dailyTrailer : '', + r.dailyParking != null && r.dailyParking !== '' ? r.dailyParking : '', + r.dailyTire != null && r.dailyTire !== '' ? r.dailyTire : '', + r.vehicleFee != null && r.vehicleFee !== '' ? r.vehicleFee : '', + r.multiTrip || '', + r.routePricing || '', + r.remark || '', + r.totalCost != null && r.totalCost !== '' ? r.totalCost : '', + r.profitLoss != null && r.profitLoss !== '' ? r.profitLoss : '' + ]; + if (supervisor) row.push(r.maintainerName || ''); + return row; +} + +function llParseCsvLine(line) { + var out = []; + var cur = ''; + var inQ = false; + var i; + for (i = 0; i < line.length; i++) { + var c = line[i]; + if (c === '"') { inQ = !inQ; continue; } + if (c === ',' && !inQ) { out.push(cur.trim()); cur = ''; continue; } + cur += c; + } + out.push(cur.trim()); + return out; +} + +function llIsDraftRow(row) { + return row && row.rowStatus === 'draft'; +} + +function llApplyRowFieldPatch(row, field, val) { + var next = Object.assign({}, row); + next[field] = val; + if (field === 'month' && val && typeof val.format === 'function' && val.isValid && val.isValid()) { + next.month = val.format('YYYY-MM'); + } + if (field === 'dispatchDate' && val && typeof val.isValid === 'function' && val.isValid()) { + next.month = val.format('YYYY-MM'); + } + if (field === 'vehicleUndertaker') { + if (val === '第三方车队') { + next.brand = undefined; + next.model = undefined; + } else if (llIsOurUndertaker(val) && next.plateNo) { + next = llApplyPlateLookup(next); + } + } + if (field === 'plateNo' && llIsOurUndertaker(next.vehicleUndertaker)) { + next = llApplyPlateLookup(Object.assign({}, next, { plateNo: val })); + } + if (field === 'brand') { + next.model = undefined; + } + if (field === 'unitPrice' || field === 'quantity') { + if (field === 'quantity' && val != null && val !== '') { + next.quantity = llRound(val); + } + next.amount = llRound(llNum(next.unitPrice) * llNum(next.quantity)); + } + return llRecalcRow(next); +} + +function llIsFilledNum(val) { + if (val === null || val === undefined || val === '') return false; + return !isNaN(Number(val)); +} + +function llValidateRow(row) { + if (!row.dispatchDate) return '请填写出车日期'; + if (!row.businessName || !String(row.businessName).trim()) return '请填写业务名称'; + if (!row.vehicleUndertaker) return '请选择车辆承接方'; + if (!row.plateNo || !String(row.plateNo).trim()) return '请填写车牌号码'; + if (llIsOurUndertaker(row.vehicleUndertaker)) { + if (!llLookupByPlate(row.plateNo)) return '未在车辆档案中匹配到该车牌,请确认车牌或选择「第三方车队」'; + } + if (!llIsFilledNum(row.unitPrice)) return '请填写单价'; + if (!llIsFilledNum(row.quantity)) return '请填写数量'; + var requiredNums = [ + ['hydrogenFee', '氢费'], + ['etcFee', 'ETC费用'], + ['salary', '薪资'], + ['electricityFee', '电费'], + ['manualReimburse', '人工报销费用'], + ['dailySocialSecurity', '日社保服务费'], + ['dailyTrailer', '日挂车费用'], + ['dailyParking', '日停车费用'], + ['dailyTire', '日轮胎费用'] + ]; + var i; + for (i = 0; i < requiredNums.length; i++) { + if (!llIsFilledNum(row[requiredNums[i][0]])) return '请填写' + requiredNums[i][1]; + } + return ''; +} + +function llIsImportRowEmpty(cols) { + if (!cols || !cols.length) return true; + var i; + for (i = 0; i < cols.length; i++) { + if (String(cols[i] || '').trim()) return false; + } + return true; +} + +function llValidateImportCsv(text, user) { + var lines = String(text || '').split(/\r?\n/).filter(function (l) { return l.trim(); }); + if (lines.length < 2) { + return { ok: false, errors: ['文件中未解析到有效数据,请确认已填写模板并保留表头行'] }; + } + var headerCols = llParseCsvLine(lines[0]); + if (headerCols.length < LL_IMPORT_HEADERS.length) { + return { ok: false, errors: ['表头列数与模板不一致(应为 ' + LL_IMPORT_HEADERS.length + ' 列),请重新下载模板填写'] }; + } + var errors = []; + var imported = []; + var i; + for (i = 1; i < lines.length; i++) { + var cols = llParseCsvLine(lines[i]); + if (llIsImportRowEmpty(cols)) continue; + var rowNo = i + 1; + var undertakerVal = cols[5]; + if (undertakerVal && undertakerVal !== '我司' && undertakerVal !== '第三方车队') { + errors.push('第 ' + rowNo + ' 行:车辆承接方只能填写「我司」或「第三方车队」'); + continue; + } + var isNewFmt = cols[5] === '我司' || cols[5] === '第三方车队'; + var multiTripIdx = (isNewFmt ? 9 : 7) + 13; + var multiTripVal = cols[multiTripIdx]; + if (multiTripVal && multiTripVal !== '是' && multiTripVal !== '否') { + errors.push('第 ' + rowNo + ' 行:是否多趟只能填写「是」或「否」'); + continue; + } + var row = llRowFromImportCols(cols, user); + var err = llValidateRow(row); + if (err) { + errors.push('第 ' + rowNo + ' 行:' + err); + continue; + } + imported.push(row); + } + if (!imported.length && !errors.length) { + return { ok: false, errors: ['未解析到有效数据行,请检查业务名称、车牌号码等必填项是否已填写'] }; + } + if (errors.length) { + var displayErrors = errors.length > 10 ? errors.slice(0, 10).concat(['… 还有 ' + (errors.length - 10) + ' 条错误未展示']) : errors; + return { ok: false, errors: displayErrors, partialCount: imported.length }; + } + return { ok: true, rows: imported }; +} + +function llBuildEmptyRow(user) { + var now = window.dayjs ? window.dayjs() : null; + return llRecalcRow({ + key: llNextKey(), + rowStatus: 'draft', + createdBy: user.id, + maintainerName: user.name, + operatorName: user.name, + lastOperateAt: llNow(), + month: now ? now.format('YYYY-MM') : '', + dispatchDate: now, + businessName: '', + driver: '', + phone: '', + vehicleUndertaker: '我司', + plateNo: '', + brand: undefined, + model: undefined, + unitPrice: undefined, + quantity: 1, + amount: undefined, + hydrogenFee: 0, + etcFee: 0, + salary: 0, + electricityFee: 0, + manualReimburse: 0, + dailySocialSecurity: 0, + dailyTrailer: 0, + dailyParking: 0, + dailyTire: 0, + vehicleFee: 0, + multiTrip: '否', + routePricing: '', + remark: '' + }); +} + +function llRowFromImportCols(cols, user) { + var d = llParseDate(cols[1]); + var isNewFormat = cols[5] === '我司' || cols[5] === '第三方车队'; + var undertaker; + var plateNo; + var brand; + var model; + var unitPriceIdx; + if (isNewFormat) { + undertaker = cols[5] || '我司'; + plateNo = cols[6] || ''; + brand = cols[7] || ''; + model = cols[8] || ''; + unitPriceIdx = 9; + } else { + undertaker = '我司'; + plateNo = cols[5] || ''; + brand = ''; + model = cols[6] || ''; + unitPriceIdx = 7; + } + var row = llRecalcRow({ + key: llNextKey(), + rowStatus: 'saved', + createdBy: user.id, + maintainerName: user.name, + operatorName: user.name, + lastOperateAt: llNow(), + month: llParseMonth(cols[0], cols[1] || d), + dispatchDate: d || cols[1] || null, + businessName: cols[2] || '', + driver: cols[3] || '', + phone: cols[4] || '', + vehicleUndertaker: undertaker, + plateNo: plateNo, + brand: brand, + model: model, + unitPrice: llNum(cols[unitPriceIdx]), + quantity: llNum(cols[unitPriceIdx + 1]), + amount: llNum(cols[unitPriceIdx + 2]), + hydrogenFee: llNum(cols[unitPriceIdx + 3]), + etcFee: llNum(cols[unitPriceIdx + 4]), + salary: llNum(cols[unitPriceIdx + 5]), + electricityFee: llNum(cols[unitPriceIdx + 6]), + manualReimburse: llNum(cols[unitPriceIdx + 7]), + dailySocialSecurity: llNum(cols[unitPriceIdx + 8]), + dailyTrailer: llNum(cols[unitPriceIdx + 9]), + dailyParking: llNum(cols[unitPriceIdx + 10]), + dailyTire: llNum(cols[unitPriceIdx + 11]), + vehicleFee: llNum(cols[unitPriceIdx + 12]), + multiTrip: cols[unitPriceIdx + 13] || '否', + routePricing: cols[unitPriceIdx + 14] || '', + remark: cols[unitPriceIdx + 15] || '' + }); + return llApplyPlateLookup(row); +} + +function llBuildMockRows() { + var samples = [ + ['2026-01', '2026-01-01', '嘉兴金小悦贸易有限公司', '王召善', '15112321670', '我司', '粤AGQ8393', '现代', '帕力安牌4.5吨冷链车', '590', '1', '590', '303.8', '0', '258.06', '0', '0', '40', '0', '0', '20', '80', '否', '', ''], + ['2026-01', '2026-01-01', '嘉兴金小悦贸易有限公司', '杨国平', '13720987869', '我司', '粤AGR3208', '现代', '帕力安牌4.5吨冷链车', '610', '1', '610', '304.15', '0', '258.06', '0', '0', '40', '0', '0', '20', '80', '否', '', ''], + ['2026-01', '2026-01-01', '嘉兴益顺冷链物流有限公司', '曹文想', '18321541863', '我司', '粤AGP9835', '现代', '帕力安牌4.5吨冷链车', '553.94', '1', '553.94', '215.6', '37.04', '290.32', '0', '5', '40', '0', '0', '20', '80', '否', '敏胜-梅山', ''], + ['2026-02', '2026-02-03', '杭州绿道城配科技有限公司', '李强', '13800138001', '我司', '浙F02608F', '现代', '帕力安牌18吨双飞翼货车', '680', '2', '1360', '420', '56', '300', '12', '0', '40', '0', '15', '20', '80', '是', '嘉兴-杭州', ''] + ]; + var tan = LL_OPERATORS[0]; + var liu = LL_OPERATORS[1]; + var rows = []; + var i; + for (i = 0; i < 3; i++) rows.push(llRowFromImportCols(samples[i], tan)); + rows.push(llRowFromImportCols(samples[3], liu)); + return rows; +} + +function llEmptyForm(user) { + var now = window.dayjs ? window.dayjs() : null; + return { + month: now ? now.startOf('month') : null, + dispatchDate: now, + businessName: '', + driver: '', + phone: '', + vehicleUndertaker: '我司', + plateNo: '', + brand: undefined, + model: undefined, + unitPrice: undefined, + quantity: 1, + amount: undefined, + hydrogenFee: 0, + etcFee: 0, + salary: 0, + electricityFee: 0, + manualReimburse: 0, + dailySocialSecurity: 0, + dailyTrailer: 0, + dailyParking: 0, + dailyTire: 0, + vehicleFee: 0, + multiTrip: '否', + routePricing: '', + remark: '', + createdBy: user.id, + maintainerName: user.name + }; +} + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + var useRef = React.useRef; + + var antd = window.antd; + var ConfigProvider = antd.ConfigProvider; + var App = antd.App; + var Card = antd.Card; + var Button = antd.Button; + var Table = antd.Table; + var Select = antd.Select; + var Input = antd.Input; + var InputNumber = antd.InputNumber; + var DatePicker = antd.DatePicker; + var Modal = antd.Modal; + var Form = antd.Form; + var Upload = antd.Upload; + var Steps = antd.Steps; + var Dropdown = antd.Dropdown; + var Tooltip = antd.Tooltip; + var message = antd.message; + + var zhCN = antd.locale && antd.locale.zhCN; + var datePickerLocale = (zhCN && zhCN.DatePicker) || LL_ZH_DATE_LOCALE; + + var currentUser = LL_OPERATORS[0]; + var isSupervisor = llIsSupervisor(currentUser); + + var rowsState = useState(llBuildMockRows); + var allRows = rowsState[0]; + var setAllRows = rowsState[1]; + + var monthDraftState = useState(undefined); + var monthDraft = monthDraftState[0]; + var setMonthDraft = monthDraftState[1]; + var businessDraftState = useState(undefined); + var businessDraft = businessDraftState[0]; + var setBusinessDraft = businessDraftState[1]; + var plateDraftState = useState([]); + var plateDraft = plateDraftState[0]; + var setPlateDraft = plateDraftState[1]; + var undertakerDraftState = useState(undefined); + var undertakerDraft = undertakerDraftState[0]; + var setUndertakerDraft = undertakerDraftState[1]; + var brandDraftState = useState(undefined); + var brandDraft = brandDraftState[0]; + var setBrandDraft = brandDraftState[1]; + var modelDraftState = useState(undefined); + var modelDraft = modelDraftState[0]; + var setModelDraft = modelDraftState[1]; + var operatorDraftState = useState(undefined); + var operatorDraft = operatorDraftState[0]; + var setOperatorDraft = operatorDraftState[1]; + var dateRangeDraftState = useState(null); + var dateRangeDraft = dateRangeDraftState[0]; + var setDateRangeDraft = dateRangeDraftState[1]; + + var filterExpandedState = useState(false); + var filterExpanded = filterExpandedState[0]; + var setFilterExpanded = filterExpandedState[1]; + + var requirementModalOpenState = useState(false); + var requirementModalOpen = requirementModalOpenState[0]; + var setRequirementModalOpen = requirementModalOpenState[1]; + + var monthAppliedState = useState(undefined); + var monthApplied = monthAppliedState[0]; + var setMonthApplied = monthAppliedState[1]; + var businessAppliedState = useState(undefined); + var businessApplied = businessAppliedState[0]; + var setBusinessApplied = businessAppliedState[1]; + var plateAppliedState = useState([]); + var plateApplied = plateAppliedState[0]; + var setPlateApplied = plateAppliedState[1]; + var undertakerAppliedState = useState(undefined); + var undertakerApplied = undertakerAppliedState[0]; + var setUndertakerApplied = undertakerAppliedState[1]; + var brandAppliedState = useState(undefined); + var brandApplied = brandAppliedState[0]; + var setBrandApplied = brandAppliedState[1]; + var modelAppliedState = useState(undefined); + var modelApplied = modelAppliedState[0]; + var setModelApplied = modelAppliedState[1]; + var operatorAppliedState = useState(undefined); + var operatorApplied = operatorAppliedState[0]; + var setOperatorApplied = operatorAppliedState[1]; + var dateRangeAppliedState = useState(null); + var dateRangeApplied = dateRangeAppliedState[0]; + var setDateRangeApplied = dateRangeAppliedState[1]; + + var dispatchSortState = useState('descend'); + var dispatchSortOrder = dispatchSortState[0]; + var setDispatchSortOrder = dispatchSortState[1]; + + var selectedRowKeysState = useState([]); + var selectedRowKeys = selectedRowKeysState[0]; + var setSelectedRowKeys = selectedRowKeysState[1]; + + var importOpenState = useState(false); + var importOpen = importOpenState[0]; + var setImportOpen = importOpenState[1]; + var importErrorsState = useState([]); + var importErrors = importErrorsState[0]; + var setImportErrors = importErrorsState[1]; + var importStepState = useState(0); + var importStep = importStepState[0]; + var setImportStep = importStepState[1]; + + var formOpenState = useState(false); + var formOpen = formOpenState[0]; + var setFormOpen = formOpenState[1]; + var editingKeyState = useState(null); + var editingKey = editingKeyState[0]; + var setEditingKey = editingKeyState[1]; + var formState = useState(llEmptyForm(currentUser)); + var formData = formState[0]; + var setFormData = formState[1]; + + var changeLogsByKeyState = useState({}); + var changeLogsByKey = changeLogsByKeyState[0]; + var setChangeLogsByKey = changeLogsByKeyState[1]; + var changeLogModalState = useState({ open: false, rowKey: null, title: '' }); + var changeLogModal = changeLogModalState[0]; + var setChangeLogModal = changeLogModalState[1]; + + React.useEffect(function () { + if (window.dayjs && typeof window.dayjs.locale === 'function') { + try { window.dayjs.locale('zh-cn'); } catch (eLocale) { /* ignore */ } + } + }, []); + + var changeLogsSeededRef = useRef(false); + React.useEffect(function () { + if (changeLogsSeededRef.current || !allRows.length) return; + changeLogsSeededRef.current = true; + var first = allRows[0]; + if (!first || !first.key) return; + var demoAt = llNow(); + var demoAt2 = demoAt && demoAt.subtract ? demoAt.subtract(1, 'day') : demoAt; + setChangeLogsByKey(function (prev) { + if (prev[first.key] && prev[first.key].length) return prev; + var next = Object.assign({}, prev); + next[first.key] = [ + { + id: 'll-clog-demo-1', + at: demoAt, + userId: first.createdBy, + userName: first.operatorName || first.maintainerName || '—', + field: 'hydrogenFee', + fieldLabel: '氢费', + before: '300.00', + after: '303.80' + }, + { + id: 'll-clog-demo-2', + at: demoAt2, + userId: first.createdBy, + userName: first.operatorName || first.maintainerName || '—', + field: '_import', + fieldLabel: '批量导入', + before: '—', + after: '导入创建' + } + ]; + return next; + }); + }, [allRows]); + + var appendChangeLog = useCallback(function (rowKey, field, before, after, rowCtx, meta) { + if (!rowKey) return; + meta = meta || {}; + var fieldLabel = meta.fieldLabel || LL_CHANGE_LOG_FIELD_LABELS[field] || field; + var beforeText = meta.beforeText != null ? meta.beforeText : llFormatLogValue(field, before, rowCtx); + var afterText = meta.afterText != null ? meta.afterText : llFormatLogValue(field, after, rowCtx); + if (!meta.force && beforeText === afterText) return; + setChangeLogsByKey(function (prev) { + var list = (prev[rowKey] || []).slice(); + list.unshift({ + id: 'll-clog-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8), + at: llNow(), + userId: currentUser.id, + userName: currentUser.name, + field: field, + fieldLabel: fieldLabel, + before: beforeText, + after: afterText + }); + var next = Object.assign({}, prev); + next[rowKey] = list; + return next; + }); + }, [currentUser]); + + var diffAndLogRow = useCallback(function (oldRow, newRow) { + if (!oldRow || !newRow) return; + var i; + for (i = 0; i < LL_LOG_TRACK_FIELDS.length; i++) { + var field = LL_LOG_TRACK_FIELDS[i]; + var beforeVal = oldRow[field]; + var afterVal = newRow[field]; + if (field === 'dispatchDate') { + beforeVal = llFmtDate(beforeVal); + afterVal = llFmtDate(afterVal); + } else if (field === 'month') { + beforeVal = llFmtMonth(beforeVal); + afterVal = llFmtMonth(afterVal); + } + if (String(beforeVal == null ? '' : beforeVal) !== String(afterVal == null ? '' : afterVal)) { + appendChangeLog(newRow.key, field, oldRow[field], newRow[field], newRow); + } + } + }, [appendChangeLog]); + + var openChangeLogModal = useCallback(function (record) { + if (!record) return; + var title = record.businessName || record.plateNo || record.key; + setChangeLogModal({ open: true, rowKey: record.key, title: title }); + }, []); + + var closeChangeLogModal = useCallback(function () { + setChangeLogModal({ open: false, rowKey: null, title: '' }); + }, []); + + function filterOption(input, option) { + return String(option && option.label || '').toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + function renderFilterField(label, control) { + return React.createElement('div', { className: 'lc-filter-field' }, + React.createElement('span', { className: 'lc-filter-field-label' }, label), + React.createElement('div', { className: 'lc-filter-field-control' }, control) + ); + } + + var visibleRows = useMemo(function () { + var list = allRows.filter(function (r) { return llCanView(r, currentUser, isSupervisor); }); + if (monthApplied) { + var appliedMonth = llParseMonth(monthApplied); + list = list.filter(function (r) { return llParseMonth(r.month) === appliedMonth; }); + } + if (businessApplied) { + list = list.filter(function (r) { return r.businessName === businessApplied; }); + } + if (plateApplied && plateApplied.length) { + var plateSet = {}; + plateApplied.forEach(function (p) { plateSet[p] = true; }); + list = list.filter(function (r) { return plateSet[r.plateNo]; }); + } + if (undertakerApplied) list = list.filter(function (r) { return r.vehicleUndertaker === undertakerApplied; }); + if (brandApplied) list = list.filter(function (r) { return r.brand === brandApplied; }); + if (modelApplied) list = list.filter(function (r) { return r.model === modelApplied; }); + if (operatorApplied) list = list.filter(function (r) { return r.maintainerName === operatorApplied; }); + if (dateRangeApplied && dateRangeApplied[0] && dateRangeApplied[1] && window.dayjs) { + var s = window.dayjs(dateRangeApplied[0]).startOf('day'); + var e = window.dayjs(dateRangeApplied[1]).endOf('day'); + list = list.filter(function (r) { + var d = llParseDate(r.dispatchDate); + return d && !d.isBefore(s) && !d.isAfter(e); + }); + } + return list; + }, [allRows, currentUser, isSupervisor, monthApplied, businessApplied, plateApplied, undertakerApplied, brandApplied, modelApplied, operatorApplied, dateRangeApplied]); + + var tableRows = useMemo(function () { + var list = visibleRows.slice(); + list.sort(function (a, b) { + var draftA = llIsDraftRow(a); + var draftB = llIsDraftRow(b); + if (draftA !== draftB) return draftA ? 1 : -1; + var cmp = llCompareDispatchDate(a, b); + return dispatchSortOrder === 'ascend' ? cmp : -cmp; + }); + return list.map(function (r, idx) { return Object.assign({}, r, { seq: idx + 1 }); }); + }, [visibleRows, dispatchSortOrder]); + + var statSummary = useMemo(function () { + return visibleRows.reduce(function (acc, r) { + acc.amount += llNum(r.amount); + acc.totalCost += llNum(r.totalCost); + acc.profitLoss += llNum(r.profitLoss); + acc.hydrogenFee += llNum(r.hydrogenFee); + acc.etcFee += llNum(r.etcFee); + return acc; + }, { count: visibleRows.length, amount: 0, totalCost: 0, profitLoss: 0, hydrogenFee: 0, etcFee: 0 }); + }, [visibleRows]); + + var plateOptions = useMemo(function () { + var map = {}; + allRows.forEach(function (r) { if (r.plateNo) map[r.plateNo] = true; }); + Object.keys(LL_VEHICLE_PLATE_REGISTRY).forEach(function (p) { map[p] = true; }); + return Object.keys(map).sort(function (a, b) { return a.localeCompare(b, 'zh-CN'); }).map(function (p) { + return { value: p, label: p }; + }); + }, [allRows]); + + var businessOptions = useMemo(function () { + var map = {}; + allRows.forEach(function (r) { if (r.businessName) map[r.businessName] = true; }); + return Object.keys(map).sort(function (a, b) { return a.localeCompare(b, 'zh-CN'); }).map(function (n) { + return { value: n, label: n }; + }); + }, [allRows]); + + var filterModelOptions = useMemo(function () { return llModelOptions(brandDraft); }, [brandDraft]); + + var registryPlateOptions = useMemo(function () { + return Object.keys(LL_VEHICLE_PLATE_REGISTRY).map(function (p) { return { value: p, label: p }; }); + }, []); + + var formModelOptions = useMemo(function () { return llModelOptions(formData.brand); }, [formData.brand]); + + var operatorOptions = useMemo(function () { + var map = {}; + LL_OPERATORS.forEach(function (o) { if (o.role === 'staff') map[o.name] = true; }); + allRows.forEach(function (r) { if (r.maintainerName) map[r.maintainerName] = true; }); + return Object.keys(map).map(function (n) { return { value: n, label: n }; }); + }, [allRows]); + + var handleQuery = useCallback(function () { + setMonthApplied(monthDraft); + setBusinessApplied(businessDraft); + setPlateApplied(plateDraft ? plateDraft.slice() : []); + setUndertakerApplied(undertakerDraft); + setBrandApplied(brandDraft); + setModelApplied(modelDraft); + setOperatorApplied(operatorDraft); + setDateRangeApplied(dateRangeDraft); + }, [monthDraft, businessDraft, plateDraft, undertakerDraft, brandDraft, modelDraft, operatorDraft, dateRangeDraft]); + + var handleReset = useCallback(function () { + setMonthDraft(undefined); + setBusinessDraft(undefined); + setPlateDraft([]); + setUndertakerDraft(undefined); + setBrandDraft(undefined); + setModelDraft(undefined); + setOperatorDraft(undefined); + setDateRangeDraft(null); + setMonthApplied(undefined); + setBusinessApplied(undefined); + setPlateApplied([]); + setUndertakerApplied(undefined); + setBrandApplied(undefined); + setModelApplied(undefined); + setOperatorApplied(undefined); + setDateRangeApplied(null); + }, []); + + var downloadTemplate = useCallback(function () { + var sample = ['2026-01', '2026-01-01', '嘉兴金小悦贸易有限公司', '王召善', '15112321670', '我司', '粤AGQ8393', '现代', '帕力安牌4.5吨冷链车', '590', '1', '590', '303.8', '0', '258.06', '0', '0', '40', '0', '0', '20', '80', '否', '', '']; + llDownloadCsv('物流业务台账导入模板.csv', LL_IMPORT_HEADERS, [sample]); + setImportStep(1); + message.success('已下载导入模板'); + }, []); + + var openImportModal = useCallback(function () { + setImportErrors([]); + setImportStep(0); + setImportOpen(true); + }, []); + + var closeImportModal = useCallback(function () { + setImportErrors([]); + setImportStep(0); + setImportOpen(false); + }, []); + + var rowSelection = useMemo(function () { + return { + selectedRowKeys: selectedRowKeys, + onChange: function (keys) { setSelectedRowKeys(keys); }, + getCheckboxProps: function (record) { + return { disabled: !llCanMutate(record, currentUser, isSupervisor) }; + }, + columnWidth: 40 + }; + }, [selectedRowKeys, currentUser, isSupervisor]); + + var handleBatchDelete = useCallback(function () { + if (!selectedRowKeys.length) { + message.warning('请先勾选要删除的记录'); + return; + } + var keySet = {}; + selectedRowKeys.forEach(function (k) { keySet[k] = true; }); + var deletable = allRows.filter(function (r) { + return keySet[r.key] && llCanMutate(r, currentUser, isSupervisor); + }); + if (!deletable.length) { + message.warning('所选记录均无权删除'); + return; + } + Modal.confirm({ + title: '确认删除所选 ' + deletable.length + ' 条台账记录?', + content: '删除后不可恢复,请谨慎操作。', + okText: '删除', + cancelText: '取消', + okButtonProps: { danger: true }, + onOk: function () { + var delSet = {}; + deletable.forEach(function (r) { delSet[r.key] = true; }); + setAllRows(function (prev) { return prev.filter(function (r) { return !delSet[r.key]; }); }); + setSelectedRowKeys(function (prev) { return prev.filter(function (k) { return !delSet[k]; }); }); + message.success('已删除 ' + deletable.length + ' 条记录'); + } + }); + }, [selectedRowKeys, allRows, currentUser, isSupervisor]); + + var handleExport = useCallback(function () { + if (!tableRows.length) { + message.warning('当前筛选条件下暂无数据可导出'); + return; + } + var headers = llBuildExportHeaders(isSupervisor); + var rows = tableRows.map(function (r) { return llBuildExportRow(r, isSupervisor); }); + var stamp = ''; + try { + stamp = window.dayjs ? window.dayjs().format('YYYYMMDD_HHmmss') : String(Date.now()); + } catch (eExp) { + stamp = String(Date.now()); + } + llDownloadCsv('物流业务台账_' + stamp + '.csv', headers, rows); + message.success('已导出 ' + rows.length + ' 条记录'); + }, [tableRows, isSupervisor]); + + var handleImportFile = useCallback(function (file) { + if (!file) return false; + setImportErrors([]); + var name = String(file.name || '').toLowerCase(); + if (!name.endsWith('.csv')) { + setImportErrors(['仅支持上传 .csv 格式文件,请使用下载的模板另存为 CSV 后上传']); + return false; + } + var reader = new FileReader(); + reader.onload = function (e) { + var text = String((e && e.target && e.target.result) || ''); + var result = llValidateImportCsv(text, currentUser); + if (!result.ok) { + setImportErrors(result.errors || ['导入校验失败,请检查文件内容']); + return; + } + var imported = result.rows || []; + imported.forEach(function (row) { + appendChangeLog(row.key, '_import', null, null, row, { + fieldLabel: '批量导入', + beforeText: '—', + afterText: '导入创建', + force: true + }); + }); + setAllRows(function (prev) { return prev.concat(imported); }); + setImportErrors([]); + setImportOpen(false); + message.success('已导入 ' + imported.length + ' 条记录'); + }; + reader.onerror = function () { + setImportErrors(['文件读取失败,请重试或更换文件']); + }; + reader.readAsText(file, 'UTF-8'); + return false; + }, [currentUser, appendChangeLog]); + + var handleTableChange = useCallback(function (_pagination, _filters, sorter) { + var s = Array.isArray(sorter) ? sorter[0] : sorter; + if (!s || (s.columnKey !== 'dispatchDate' && s.field !== 'dispatchDate')) return; + setDispatchSortOrder(s.order === 'ascend' ? 'ascend' : 'descend'); + }, []); + + var openCreate = useCallback(function () { + setAllRows(function (prev) { return prev.concat([llBuildEmptyRow(currentUser)]); }); + message.success('已在表格底部新增一行,请直接填写'); + }, [currentUser]); + + var patchRow = useCallback(function (key, field, val) { + setAllRows(function (prev) { + return prev.map(function (r) { + return r.key === key ? llApplyRowFieldPatch(r, field, val) : r; + }); + }); + }, []); + + var handleSaveDraft = useCallback(function (record) { + setAllRows(function (prev) { + var latest = prev.filter(function (r) { return r.key === record.key; })[0]; + if (!latest) return prev; + var err = llValidateRow(latest); + if (err) { + message.error(err); + return prev; + } + var saved = llTouchOperateMeta(llApplyPlateLookup(Object.assign({}, latest, { rowStatus: 'saved' })), currentUser); + appendChangeLog(saved.key, '_create', null, null, saved, { + fieldLabel: '新增记录', + beforeText: '—', + afterText: '表格行内新增保存', + force: true + }); + message.success('已保存'); + return prev.map(function (r) { return r.key === record.key ? saved : r; }); + }); + }, [currentUser, appendChangeLog]); + + var handleCancelDraft = useCallback(function (record) { + setAllRows(function (prev) { return prev.filter(function (r) { return r.key !== record.key; }); }); + }, []); + + var openEdit = useCallback(function (record) { + if (!llCanMutate(record, currentUser, isSupervisor)) { + message.warning('无权编辑该记录'); + return; + } + setEditingKey(record.key); + setFormData(Object.assign({}, record, { + dispatchDate: llParseDate(record.dispatchDate), + month: llMonthToDayjs(record.month, record.dispatchDate) + })); + setFormOpen(true); + }, [currentUser, isSupervisor]); + + var handleDelete = useCallback(function (record) { + if (!llCanMutate(record, currentUser, isSupervisor)) { + message.warning('无权删除该记录'); + return; + } + setAllRows(function (prev) { return prev.filter(function (r) { return r.key !== record.key; }); }); + setSelectedRowKeys(function (prev) { return prev.filter(function (k) { return k !== record.key; }); }); + message.success('已删除'); + }, [currentUser, isSupervisor]); + + var handleFormSave = useCallback(function () { + if (!editingKey) return; + var next = llTouchOperateMeta(llRecalcRow(llApplyPlateLookup(Object.assign({}, formData, { + month: llParseMonth(formData.month, formData.dispatchDate), + dispatchDate: formData.dispatchDate, + rowStatus: 'saved', + createdBy: formData.createdBy, + maintainerName: formData.maintainerName + }))), currentUser); + var err = llValidateRow(next); + if (err) { + message.error(err); + return; + } + setAllRows(function (prev) { + var oldRow = prev.filter(function (r) { return r.key === editingKey; })[0]; + if (oldRow) diffAndLogRow(oldRow, Object.assign({}, next, { key: editingKey })); + return prev.map(function (r) { return r.key === editingKey ? Object.assign({}, next, { key: editingKey }) : r; }); + }); + message.success('已更新'); + setFormOpen(false); + }, [formData, editingKey, currentUser, diffAndLogRow]); + + var patchForm = useCallback(function (field, val) { + setFormData(function (prev) { return llApplyRowFieldPatch(prev, field, val); }); + }, []); + + var previewCalc = useMemo(function () { return llRecalcRow(formData); }, [formData]); + + var formIsOurFleet = llIsOurUndertaker(formData.vehicleUndertaker); + + var columns = useMemo(function () { + function moneyCell(v, cls) { + return React.createElement('span', { className: 'll-cell-plain' + (cls ? ' ' + cls : '') }, llFmtMoney(v)); + } + function editable(r) { + return llIsDraftRow(r) && llCanMutate(r, currentUser, isSupervisor); + } + function plain(v) { + return React.createElement('span', { className: 'll-cell-plain' }, v == null || v === '' ? '-' : v); + } + function plainText(text) { + return React.createElement('span', { className: 'll-cell-plain' }, text); + } + function cellSelect(extra) { + var opts = extra || {}; + var fit = opts.fit; + var dropdownMin = opts.dropdownMin || 120; + delete opts.fit; + delete opts.dropdownMin; + return Object.assign({ + size: 'small', + className: 'll-cell-select' + (fit ? ' ll-cell-select--fit' : ''), + popupMatchSelectWidth: false, + dropdownStyle: { minWidth: dropdownMin }, + style: { width: '100%', minWidth: 0 } + }, opts); + } + function cellInput(field, r) { + return React.createElement(Input, { + size: 'small', + className: 'll-cell-input', + value: r[field] || '', + style: { width: '100%', minWidth: 0 }, + onChange: function (e) { patchRow(r.key, field, e.target.value); } + }); + } + function cellNum(field, r) { + return React.createElement(InputNumber, { + size: 'small', + className: 'll-cell-input-number', + controls: false, + min: 0, + precision: 2, + style: { width: '100%' }, + value: r[field] == null ? null : r[field], + onChange: function (v) { patchRow(r.key, field, v == null ? null : v); } + }); + } + function cellQty(r) { + return React.createElement(InputNumber, { + size: 'small', + className: 'll-cell-input-number', + controls: false, + min: 0, + precision: 2, + step: 0.01, + style: { width: '100%' }, + value: r.quantity == null || r.quantity === '' ? null : r.quantity, + onChange: function (v) { patchRow(r.key, 'quantity', v == null ? null : v); } + }); + } + function feeWarnWrap(inner, tip) { + if (!tip) return inner; + return React.createElement('div', { className: 'll-fee-warn-cell' }, + inner, + React.createElement(Tooltip, { title: tip, placement: 'top' }, + React.createElement('span', { + role: 'img', + 'aria-label': tip, + style: { flexShrink: 0, color: '#f59e0b', display: 'inline-flex', cursor: 'help', lineHeight: 0 } + }, LL_ICONS.warn) + ) + ); + } + function hydrogenFeeCell(r) { + var inner = editable(r) ? cellNum('hydrogenFee', r) : moneyCell(r.hydrogenFee); + if (!llIsHydrogenFeeMismatch(r)) return inner; + return feeWarnWrap(inner, llHydrogenFeeMismatchTip(r)); + } + function etcFeeCell(r) { + var inner = editable(r) ? cellNum('etcFee', r) : moneyCell(r.etcFee); + if (!llIsEtcFeeMismatch(r)) return inner; + return feeWarnWrap(inner, llEtcFeeMismatchTip(r)); + } + var cols = [ + { title: '序号', dataIndex: 'seq', key: 'seq', width: 56, fixed: 'left' }, + { title: '月份', dataIndex: 'month', key: 'month', width: 108, render: function (v, r) { + if (!editable(r)) return plainText(llFmtMonth(v)); + return React.createElement(DatePicker, { + size: 'small', + className: 'll-cell-date', + picker: 'month', + format: 'YYYY-MM', + locale: datePickerLocale, + style: { width: '100%' }, + value: llMonthToDayjs(r.month, r.dispatchDate), + onChange: function (d) { patchRow(r.key, 'month', d); } + }); + } }, + { title: llColTitle('出车日期'), dataIndex: 'dispatchDate', key: 'dispatchDate', width: 124, sorter: true, sortOrder: dispatchSortOrder, sortDirections: ['descend', 'ascend'], render: function (v, r) { + if (!editable(r)) return plainText(llFmtDate(v)); + return React.createElement(DatePicker, { + size: 'small', + className: 'll-cell-date', + format: 'YYYY-MM-DD', + locale: datePickerLocale, + style: { width: '100%' }, + value: llParseDate(r.dispatchDate), + onChange: function (d) { patchRow(r.key, 'dispatchDate', d); } + }); + } }, + { title: llColTitle('业务名称'), dataIndex: 'businessName', key: 'businessName', width: 220, render: function (v, r) { + return editable(r) ? cellInput('businessName', r) : plain(v); + } }, + { title: '司机', dataIndex: 'driver', key: 'driver', width: 88, render: function (v, r) { + return editable(r) ? cellInput('driver', r) : plain(v); + } }, + { title: '电话', dataIndex: 'phone', key: 'phone', width: 118, render: function (v, r) { + return editable(r) ? cellInput('phone', r) : plain(v); + } }, + { title: llColTitle('车辆承接方'), dataIndex: 'vehicleUndertaker', key: 'vehicleUndertaker', width: 120, render: function (v, r) { + if (!editable(r)) return plain(v); + return React.createElement(Select, cellSelect({ + fit: true, + dropdownMin: 140, + value: r.vehicleUndertaker, + options: LL_UNDERTAKER_OPTIONS, + onChange: function (val) { patchRow(r.key, 'vehicleUndertaker', val); } + })); + } }, + { title: llColTitle('车牌号码'), dataIndex: 'plateNo', key: 'plateNo', width: 132, render: function (v, r) { + if (!editable(r)) return plain(v); + if (llIsOurUndertaker(r.vehicleUndertaker)) { + return React.createElement(Select, cellSelect({ + fit: true, + dropdownMin: 132, + allowClear: true, + showSearch: true, + placeholder: '车牌', + value: r.plateNo || undefined, + options: registryPlateOptions, + filterOption: filterOption, + onChange: function (val) { patchRow(r.key, 'plateNo', val || ''); } + })); + } + return cellInput('plateNo', r); + } }, + { title: '品牌', dataIndex: 'brand', key: 'brand', width: 96, render: function (v, r) { + if (!editable(r)) return plain(v); + if (llIsOurUndertaker(r.vehicleUndertaker)) return plain(r.brand); + return React.createElement(Select, cellSelect({ + fit: true, + dropdownMin: 120, + allowClear: true, + showSearch: true, + placeholder: '品牌', + value: r.brand, + options: llBrandOptions(), + filterOption: filterOption, + onChange: function (val) { patchRow(r.key, 'brand', val); } + })); + } }, + { title: '型号', dataIndex: 'model', key: 'model', width: 200, render: function (v, r) { + if (!editable(r)) return plain(v); + if (llIsOurUndertaker(r.vehicleUndertaker)) return plain(r.model); + return React.createElement(Select, cellSelect({ + fit: true, + dropdownMin: 220, + allowClear: true, + showSearch: true, + placeholder: r.brand ? '型号' : '先选品牌', + value: r.model, + options: llModelOptions(r.brand), + disabled: !r.brand, + filterOption: filterOption, + onChange: function (val) { patchRow(r.key, 'model', val); } + })); + } }, + { title: llColTitle('单价'), dataIndex: 'unitPrice', key: 'unitPrice', width: 88, align: 'right', render: function (v, r) { + return editable(r) ? cellNum('unitPrice', r) : moneyCell(v); + } }, + { title: llColTitle('数量'), dataIndex: 'quantity', key: 'quantity', width: 80, align: 'right', render: function (v, r) { + return editable(r) ? cellQty(r) : plainText(llFmtQty(v)); + } }, + { title: '金额', dataIndex: 'amount', key: 'amount', width: 96, align: 'right', render: function (v) { return moneyCell(v); } }, + { title: llColTitle('氢费'), dataIndex: 'hydrogenFee', key: 'hydrogenFee', width: 108, align: 'right', render: function (_v, r) { + return hydrogenFeeCell(r); + } }, + { title: llColTitle('ETC费用'), dataIndex: 'etcFee', key: 'etcFee', width: 108, align: 'right', render: function (_v, r) { + return etcFeeCell(r); + } }, + { title: llColTitle('薪资'), dataIndex: 'salary', key: 'salary', width: 88, align: 'right', render: function (v, r) { + return editable(r) ? cellNum('salary', r) : moneyCell(v); + } }, + { title: llColTitle('电费'), dataIndex: 'electricityFee', key: 'electricityFee', width: 80, align: 'right', render: function (v, r) { + return editable(r) ? cellNum('electricityFee', r) : moneyCell(v); + } }, + { title: llColTitle('人工报销费用'), dataIndex: 'manualReimburse', key: 'manualReimburse', width: 104, align: 'right', render: function (v, r) { + return editable(r) ? cellNum('manualReimburse', r) : moneyCell(v); + } }, + { title: llColTitle('日社保服务费'), dataIndex: 'dailySocialSecurity', key: 'dailySocialSecurity', width: 104, align: 'right', render: function (v, r) { + return editable(r) ? cellNum('dailySocialSecurity', r) : moneyCell(v); + } }, + { title: llColTitle('日挂车费用'), dataIndex: 'dailyTrailer', key: 'dailyTrailer', width: 96, align: 'right', render: function (v, r) { + return editable(r) ? cellNum('dailyTrailer', r) : moneyCell(v); + } }, + { title: llColTitle('日停车费用'), dataIndex: 'dailyParking', key: 'dailyParking', width: 96, align: 'right', render: function (v, r) { + return editable(r) ? cellNum('dailyParking', r) : moneyCell(v); + } }, + { title: llColTitle('日轮胎费用'), dataIndex: 'dailyTire', key: 'dailyTire', width: 96, align: 'right', render: function (v, r) { + return editable(r) ? cellNum('dailyTire', r) : moneyCell(v); + } }, + { title: '车辆费用', dataIndex: 'vehicleFee', key: 'vehicleFee', width: 88, align: 'right', render: function (v, r) { + return editable(r) ? cellNum('vehicleFee', r) : moneyCell(v); + } }, + { title: '总成本', dataIndex: 'totalCost', key: 'totalCost', width: 96, align: 'right', render: function (v) { return moneyCell(v); } }, + { title: '盈亏', dataIndex: 'profitLoss', key: 'profitLoss', width: 96, align: 'right', render: function (v) { + return moneyCell(v, llNum(v) >= 0 ? 'll-profit-pos' : 'll-profit-neg'); + } }, + { title: '是否多趟', dataIndex: 'multiTrip', key: 'multiTrip', width: 96, render: function (v, r) { + if (!editable(r)) return plain(v); + return React.createElement(Select, cellSelect({ + fit: true, + dropdownMin: 96, + value: r.multiTrip, + options: LL_MULTI_TRIP_OPTIONS, + onChange: function (val) { patchRow(r.key, 'multiTrip', val); } + })); + } }, + { title: '线路计价', dataIndex: 'routePricing', key: 'routePricing', width: 120, render: function (v, r) { + return editable(r) ? cellInput('routePricing', r) : plain(v); + } }, + { title: '备注', dataIndex: 'remark', key: 'remark', width: 120, render: function (v, r) { + return editable(r) ? cellInput('remark', r) : plain(v); + } }, + { title: '操作人', dataIndex: 'operatorName', key: 'operatorName', width: 88, render: function (v, r) { + return plain(v || r.maintainerName); + } }, + { title: '最后操作时间', dataIndex: 'lastOperateAt', key: 'lastOperateAt', width: 156, render: function (v) { + return plain(llFmtDateTime(v)); + } } + ]; + if (isSupervisor) { + cols.splice(3, 0, { title: '维护人', dataIndex: 'maintainerName', key: 'maintainerName', width: 88, render: function (v) { return plain(v); } }); + } + cols.push({ + title: '操作', key: 'action', width: 56, align: 'center', fixed: 'right', + render: function (_, record) { + var canEdit = llCanMutate(record, currentUser, isSupervisor); + if (llIsDraftRow(record) && canEdit) { + return React.createElement('div', { style: { display: 'inline-flex', gap: 8 } }, + React.createElement(Button, { type: 'link', size: 'small', className: 'lc-action-btn', onClick: function () { handleSaveDraft(record); } }, '保存'), + React.createElement(Button, { type: 'link', size: 'small', className: 'lc-action-btn lc-action-btn-danger', onClick: function () { handleCancelDraft(record); } }, '取消') + ); + } + if (!canEdit) return null; + + function renderIconAction(title, iconNode, onClick, extraClass, disabled) { + var cls = 'll-action-icon-btn' + (extraClass ? ' ' + extraClass : '') + (disabled ? ' is-disabled' : ''); + return React.createElement(Tooltip, { title: title }, + React.createElement('span', { + className: cls, + role: 'button', + tabIndex: disabled ? -1 : 0, + 'aria-label': title, + onClick: disabled || !onClick ? undefined : function (e) { e.stopPropagation(); } + }, iconNode) + ); + } + + function renderMoreIcon() { + return React.createElement('svg', { viewBox: '0 0 16 16', width: 16, height: 16, fill: 'currentColor', 'aria-hidden': true }, + React.createElement('circle', { cx: 8, cy: 3, r: 1.5 }), + React.createElement('circle', { cx: 8, cy: 8, r: 1.5 }), + React.createElement('circle', { cx: 8, cy: 13, r: 1.5 }) + ); + } + + function buildMoreMenuItems() { + return [ + { key: 'edit', label: '编辑', onClick: function () { openEdit(record); } }, + { key: 'delete', label: '删除', danger: true, onClick: function () { + Modal.confirm({ + title: '确认删除该条台账?', + okText: '删除', + cancelText: '取消', + okButtonProps: { danger: true }, + onOk: function () { handleDelete(record); } + }); + } }, + { type: 'divider', key: 'divider-log' }, + { key: 'changelog', label: '变更日志', onClick: function () { openChangeLogModal(record); } } + ]; + } + + return React.createElement(Dropdown, { + trigger: ['click'], + placement: 'bottomRight', + menu: { items: buildMoreMenuItems() } + }, renderIconAction('更多', renderMoreIcon(), null, 'll-row-more-btn')); + } + }); + return cols; + }, [currentUser, isSupervisor, openEdit, handleDelete, handleSaveDraft, handleCancelDraft, patchRow, registryPlateOptions, dispatchSortOrder, openChangeLogModal, datePickerLocale]); + + var changeLogRows = useMemo(function () { + if (!changeLogModal.rowKey) return []; + return changeLogsByKey[changeLogModal.rowKey] || []; + }, [changeLogModal.rowKey, changeLogsByKey]); + + var changeLogColumns = useMemo(function () { + return [ + { title: '修改时间', dataIndex: 'at', key: 'at', width: 168, render: function (v) { return llFmtDateTime(v); } }, + { title: '修改人', dataIndex: 'userName', key: 'userName', width: 88, align: 'center' }, + { title: '修改字段', dataIndex: 'fieldLabel', key: 'fieldLabel', width: 120, align: 'center' }, + { title: '修改前', dataIndex: 'before', key: 'before', ellipsis: true, render: function (v) { + return React.createElement('span', { style: { color: '#cf1322' } }, v == null || v === '' ? '—' : v); + } }, + { title: '修改后', dataIndex: 'after', key: 'after', ellipsis: true, render: function (v) { + return React.createElement('span', { style: { color: '#389e0d' } }, v == null || v === '' ? '—' : v); + } } + ]; + }, []); + + function renderStatCard(card) { + var val = card.format === 'count' ? String(statSummary.count) : llFmtMoney(statSummary[card.key]); + var valueClass = card.key === 'profitLoss' ? (statSummary.profitLoss >= 0 ? 'lc-stat-val--profit' : 'lc-stat-val--loss') : ''; + return React.createElement('div', { key: card.key, className: 'lc-alert-card lc-alert-card--total' }, + React.createElement('div', { className: 'lc-alert-card-tip-anchor' }, + React.createElement(Tooltip, { title: card.desc, placement: 'topRight', overlayStyle: { maxWidth: 360 } }, + React.createElement('span', { className: 'lc-alert-card-tip', role: 'img', 'aria-label': card.title + '说明' }, + llSvgIcon([{ tag: 'circle', cx: 12, cy: 12, r: 10 }, { tag: 'line', x1: 12, y1: 16, x2: 12, y2: 12 }, { tag: 'line', x1: 12, y1: 8, x2: 12.01, y2: 8 }], 10) + ) + ) + ), + React.createElement('div', { className: 'lc-alert-card-icon', 'aria-hidden': true }, LL_ICONS.truck), + React.createElement('div', { className: 'lc-alert-card-main' }, + React.createElement('div', { className: 'lc-alert-card-title' }, card.title), + React.createElement('div', { className: 'lc-alert-card-val ' + valueClass }, val) + ) + ); + } + + function renderFormItem(label, node) { + return React.createElement(Form.Item, { label: label, style: { marginBottom: 12 } }, node); + } + + function buildFilterFields() { + var fields = [ + renderFilterField('月份', React.createElement(DatePicker, { + picker: 'month', + format: 'YYYY年MM月', + placeholder: '全部月份', + allowClear: true, + locale: datePickerLocale, + style: { width: '100%' }, + value: monthDraft, + onChange: setMonthDraft + })), + renderFilterField('出车日期', React.createElement(DatePicker.RangePicker, { + style: { width: '100%' }, + format: 'YYYY-MM-DD', + placeholder: ['开始日期', '结束日期'], + locale: datePickerLocale, + value: dateRangeDraft, + onChange: setDateRangeDraft + })), + renderFilterField('业务名称', React.createElement(Select, { + allowClear: true, + showSearch: true, + placeholder: '请选择或输入业务名称', + style: { width: '100%' }, + value: businessDraft, + onChange: setBusinessDraft, + options: businessOptions, + filterOption: filterOption + })), + renderFilterField('车牌号码', React.createElement(Select, { + mode: 'multiple', + allowClear: true, + showSearch: true, + placeholder: '全部', + maxTagCount: 'responsive', + style: { width: '100%' }, + value: plateDraft, + onChange: setPlateDraft, + options: plateOptions, + filterOption: filterOption + })), + renderFilterField('车辆承接方', React.createElement(Select, { allowClear: true, placeholder: '全部', style: { width: '100%' }, value: undertakerDraft, onChange: setUndertakerDraft, options: LL_UNDERTAKER_OPTIONS })), + renderFilterField('品牌', React.createElement(Select, { allowClear: true, showSearch: true, placeholder: '全部', style: { width: '100%' }, value: brandDraft, onChange: function (v) { setBrandDraft(v); setModelDraft(undefined); }, options: llBrandOptions(), filterOption: filterOption })), + renderFilterField('型号', React.createElement(Select, { allowClear: true, showSearch: true, placeholder: brandDraft ? '全部' : '请先选择品牌', style: { width: '100%' }, value: modelDraft, onChange: setModelDraft, options: filterModelOptions, disabled: !brandDraft, filterOption: filterOption })) + ]; + if (isSupervisor) { + fields.push(renderFilterField('维护人', React.createElement(Select, { allowClear: true, showSearch: true, placeholder: '全部', style: { width: '100%' }, value: operatorDraft, onChange: setOperatorDraft, options: operatorOptions, filterOption: filterOption }))); + } + return fields; + } + + return React.createElement(ConfigProvider, { locale: zhCN || undefined }, + React.createElement(App, null, + React.createElement('style', null, LOGISTICS_PAGE_STYLE + LOGISTICS_TABLE_STYLE), + React.createElement('div', { className: 'logistics-ledger-page' }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', marginBottom: 16, flexShrink: 0 } }, + React.createElement(Button, { + type: 'default', + icon: LL_ICONS.doc, + style: LL_REQ_BTN_STYLE, + onClick: function () { setRequirementModalOpen(true); }, + 'aria-label': '查看需求说明' + }, '查看需求说明') + ), + React.createElement('div', { style: { display: 'flex', flexDirection: 'column', flex: 1 } }, + React.createElement(Card, { className: 'lc-filter-card', title: '筛选条件', bordered: false }, + React.createElement('div', { className: 'lc-filter-grid' }, (function () { + var items = buildFilterFields(); + var limit = filterExpanded ? items.length : 4; + var out = []; + var i; + for (i = 0; i < limit && i < items.length; i++) out.push(items[i]); + return out; + })()), + React.createElement('div', { className: 'lc-filter-actions' }, + buildFilterFields().length > 4 ? React.createElement(Button, { + type: 'link', + size: 'small', + onClick: function () { setFilterExpanded(!filterExpanded); }, + style: { marginRight: 'auto', paddingLeft: 0, fontWeight: 600 } + }, filterExpanded ? '收起' : '展开更多筛选项') : null, + React.createElement(Button, { onClick: handleReset, style: { borderRadius: 8 } }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleQuery, style: LL_PRIMARY_BTN }, '查询') + ) + ), + + React.createElement('div', { className: 'lc-alert-stats-row' }, LL_KPI_CARDS.map(renderStatCard)), + + React.createElement('div', { className: 'lc-table-section' }, + React.createElement('div', { className: 'lc-table-toolbar' }, + React.createElement('div', { className: 'lc-table-toolbar-meta' }, '共 ', React.createElement('strong', null, String(visibleRows.length)), ' 条记录'), + React.createElement('div', { className: 'lc-table-toolbar-actions' }, + !isSupervisor ? React.createElement(Button, { icon: LL_ICONS.upload, style: LL_OUTLINE_BTN, onClick: openImportModal }, '批量导入') : null, + React.createElement(Button, { + danger: true, + disabled: !selectedRowKeys.length, + onClick: handleBatchDelete, + style: { borderRadius: 8, fontWeight: 600 } + }, '批量删除'), + React.createElement(Button, { + onClick: handleExport, + style: { borderRadius: 8, fontWeight: 600 } + }, '导出') + ) + ), + React.createElement('div', { className: 'lc-table-card' }, + React.createElement('div', { className: 'logistics-ledger-table-wrap' }, + React.createElement(Table, { + className: 'logistics-ledger-table', + size: 'small', + bordered: true, + rowKey: 'key', + rowSelection: rowSelection, + columns: columns, + dataSource: tableRows, + rowClassName: function (r) { return llIsDraftRow(r) ? 'll-row-draft' : ''; }, + onChange: handleTableChange, + pagination: false, + scroll: { x: 'max-content' }, + locale: { emptyText: React.createElement('div', { style: { padding: '32px 0', color: '#94a3b8' } }, '暂无符合筛选条件的台账记录') } + }) + ) + ), + !isSupervisor ? React.createElement('div', { className: 'll-table-add-row-wrap' }, + React.createElement(Button, { + type: 'dashed', + block: true, + className: 'll-add-row-btn', + icon: LL_ICONS.plus, + onClick: openCreate + }, '新增一行') + ) : null + ) + ), + + React.createElement(Modal, { + className: 'll-import-modal', + wrapClassName: 'll-import-modal-wrap', + title: React.createElement('span', { style: { fontWeight: 700 } }, '批量导入'), + open: importOpen, + onCancel: closeImportModal, + footer: null, + width: 560, + centered: true, + destroyOnClose: true + }, + React.createElement('div', { className: 'll-import-content' }, + React.createElement('section', { className: 'll-import-steps-card', 'aria-label': '导入步骤' }, + React.createElement(Steps, { + className: 'll-import-steps', + current: importStep, + size: 'small', + items: [ + { title: '下载模板' }, + { title: '上传文件' } + ] + }) + ), + React.createElement('section', { className: 'll-import-action-card', 'aria-label': '下载模板' }, + React.createElement('div', { className: 'll-import-action-row' }, + React.createElement('div', { className: 'll-import-action-meta' }, + React.createElement('span', { className: 'll-import-action-icon', 'aria-hidden': true }, LL_ICONS.download), + React.createElement('span', { className: 'll-import-action-label' }, '下载模板') + ), + React.createElement(Button, { + type: 'primary', + ghost: true, + className: 'll-import-download-btn', + icon: LL_ICONS.download, + style: LL_OUTLINE_BTN, + onClick: downloadTemplate + }, '下载模板') + ) + ), + React.createElement('section', { className: 'll-import-action-card ll-import-upload-card', 'aria-label': '上传文件' }, + React.createElement(Upload.Dragger, { + className: 'll-import-dragger', + accept: '.csv', + maxCount: 1, + showUploadList: true, + beforeUpload: handleImportFile + }, + React.createElement('div', { className: 'll-import-upload-inner' }, + React.createElement('span', { className: 'll-import-upload-icon', 'aria-hidden': true }, LL_ICONS.upload), + React.createElement('p', { className: 'll-import-upload-title' }, '点击或拖拽文件进行导入'), + React.createElement('p', { className: 'll-import-upload-hint' }, '仅支持 .csv 格式,单次上传一个文件') + ) + ) + ), + importErrors && importErrors.length ? React.createElement('div', { className: 'll-import-fail', role: 'alert' }, + React.createElement('div', { className: 'll-import-fail-title' }, '导入校验未通过'), + React.createElement('ul', { className: 'll-import-error-list' }, + importErrors.map(function (err, idx) { + return React.createElement('li', { key: 'import-err-' + idx }, err); + }) + ) + ) : null + ) + ), + + React.createElement(Modal, { + title: '编辑台账', + open: formOpen && !!editingKey, + onCancel: function () { setFormOpen(false); }, + onOk: handleFormSave, + okText: '保存', + cancelText: '取消', + width: 760, + centered: true, + destroyOnClose: true + }, + React.createElement('div', { className: 'll-form-grid' }, + renderFormItem('月份', React.createElement(DatePicker, { + picker: 'month', + format: 'YYYY年MM月', + placeholder: '请选择月份', + locale: datePickerLocale, + style: { width: '100%' }, + value: formData.month, + onChange: function (v) { patchForm('month', v); } + })), + renderFormItem('出车日期', React.createElement(DatePicker, { + style: { width: '100%' }, + format: 'YYYY-MM-DD', + placeholder: '请选择出车日期', + locale: datePickerLocale, + value: formData.dispatchDate, + onChange: function (v) { patchForm('dispatchDate', v); } + })), + renderFormItem('业务名称', React.createElement(Input, { value: formData.businessName, onChange: function (e) { patchForm('businessName', e.target.value); } })), + renderFormItem('司机', React.createElement(Input, { value: formData.driver, onChange: function (e) { patchForm('driver', e.target.value); } })), + renderFormItem('电话', React.createElement(Input, { value: formData.phone, onChange: function (e) { patchForm('phone', e.target.value); } })), + renderFormItem('车辆承接方', React.createElement(Select, { style: { width: '100%' }, value: formData.vehicleUndertaker, onChange: function (v) { patchForm('vehicleUndertaker', v); }, options: LL_UNDERTAKER_OPTIONS })), + renderFormItem('车牌号码', formIsOurFleet + ? React.createElement(Select, { + allowClear: true, + showSearch: true, + placeholder: '请选择我司车牌', + style: { width: '100%' }, + value: formData.plateNo || undefined, + onChange: function (v) { patchForm('plateNo', v || ''); }, + options: registryPlateOptions, + filterOption: filterOption + }) + : React.createElement(Input, { placeholder: '第三方车牌,不做档案校验', value: formData.plateNo, onChange: function (e) { patchForm('plateNo', e.target.value); } }) + ), + renderFormItem('品牌', formIsOurFleet + ? React.createElement(Select, { + disabled: true, + style: { width: '100%' }, + value: formData.brand, + placeholder: formData.plateNo ? '根据车牌自动匹配' : '请先选择车牌', + options: formData.brand ? [{ value: formData.brand, label: formData.brand }] : [] + }) + : React.createElement(Select, { + allowClear: true, + showSearch: true, + placeholder: '请选择品牌', + style: { width: '100%' }, + value: formData.brand, + onChange: function (v) { patchForm('brand', v); }, + options: llBrandOptions(), + filterOption: filterOption + }) + ), + renderFormItem('型号', formIsOurFleet + ? React.createElement(Select, { + disabled: true, + style: { width: '100%' }, + value: formData.model, + placeholder: formData.plateNo ? '根据车牌自动匹配' : '请先选择车牌', + options: formData.model ? [{ value: formData.model, label: formData.model }] : [] + }) + : React.createElement(Select, { + allowClear: true, + showSearch: true, + placeholder: formData.brand ? '请选择型号' : '请先选择品牌', + style: { width: '100%' }, + value: formData.model, + onChange: function (v) { patchForm('model', v); }, + options: formModelOptions, + disabled: !formData.brand, + filterOption: filterOption + }) + ), + renderFormItem('单价', React.createElement(InputNumber, { style: { width: '100%' }, min: 0, precision: 2, value: formData.unitPrice, onChange: function (v) { patchForm('unitPrice', v); } })), + renderFormItem('数量', React.createElement(InputNumber, { style: { width: '100%' }, min: 0, precision: 2, step: 0.01, value: formData.quantity, onChange: function (v) { patchForm('quantity', v); } })), + renderFormItem('金额', React.createElement(InputNumber, { style: { width: '100%' }, min: 0, precision: 2, value: formData.amount, onChange: function (v) { patchForm('amount', v); } })), + renderFormItem('氢费', React.createElement(InputNumber, { style: { width: '100%' }, min: 0, precision: 2, value: formData.hydrogenFee, onChange: function (v) { patchForm('hydrogenFee', v); } })), + renderFormItem('ETC费用', React.createElement(InputNumber, { style: { width: '100%' }, min: 0, precision: 2, value: formData.etcFee, onChange: function (v) { patchForm('etcFee', v); } })), + renderFormItem('薪资', React.createElement(InputNumber, { style: { width: '100%' }, min: 0, precision: 2, value: formData.salary, onChange: function (v) { patchForm('salary', v); } })), + renderFormItem('电费', React.createElement(InputNumber, { style: { width: '100%' }, min: 0, precision: 2, value: formData.electricityFee, onChange: function (v) { patchForm('electricityFee', v); } })), + renderFormItem('人工报销费用', React.createElement(InputNumber, { style: { width: '100%' }, min: 0, precision: 2, value: formData.manualReimburse, onChange: function (v) { patchForm('manualReimburse', v); } })), + renderFormItem('日社保服务费', React.createElement(InputNumber, { style: { width: '100%' }, min: 0, precision: 2, value: formData.dailySocialSecurity, onChange: function (v) { patchForm('dailySocialSecurity', v); } })), + renderFormItem('日挂车费用', React.createElement(InputNumber, { style: { width: '100%' }, min: 0, precision: 2, value: formData.dailyTrailer, onChange: function (v) { patchForm('dailyTrailer', v); } })), + renderFormItem('日停车费用', React.createElement(InputNumber, { style: { width: '100%' }, min: 0, precision: 2, value: formData.dailyParking, onChange: function (v) { patchForm('dailyParking', v); } })), + renderFormItem('日轮胎费用', React.createElement(InputNumber, { style: { width: '100%' }, min: 0, precision: 2, value: formData.dailyTire, onChange: function (v) { patchForm('dailyTire', v); } })), + renderFormItem('车辆费用', React.createElement(InputNumber, { style: { width: '100%' }, min: 0, precision: 2, value: formData.vehicleFee, onChange: function (v) { patchForm('vehicleFee', v); } })), + renderFormItem('是否多趟', React.createElement(Select, { style: { width: '100%' }, value: formData.multiTrip, onChange: function (v) { patchForm('multiTrip', v); }, options: LL_MULTI_TRIP_OPTIONS })), + renderFormItem('线路计价', React.createElement(Input, { value: formData.routePricing, onChange: function (e) { patchForm('routePricing', e.target.value); } })), + renderFormItem('备注', React.createElement(Input.TextArea, { rows: 2, value: formData.remark, onChange: function (e) { patchForm('remark', e.target.value); } })) + ), + React.createElement('div', { style: { marginTop: 12, display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 10 } }, + React.createElement('div', null, + React.createElement('div', { style: { fontSize: 12, color: '#64748b', marginBottom: 4 } }, '总成本(自动)'), + React.createElement('div', { className: 'll-form-readonly' }, llFmtMoney(previewCalc.totalCost)) + ), + React.createElement('div', null, + React.createElement('div', { style: { fontSize: 12, color: '#64748b', marginBottom: 4 } }, '盈亏(自动)'), + React.createElement('div', { className: 'll-form-readonly', style: { color: previewCalc.profitLoss >= 0 ? '#047857' : '#b91c1c' } }, llFmtMoney(previewCalc.profitLoss)) + ), + React.createElement('div', null, + React.createElement('div', { style: { fontSize: 12, color: '#64748b', marginBottom: 4 } }, '维护人'), + React.createElement('div', { className: 'll-form-readonly' }, editingKey ? formData.maintainerName : currentUser.name) + ) + ) + ), + + React.createElement(Modal, { + title: '变更日志' + (changeLogModal.title ? ' · ' + changeLogModal.title : ''), + open: changeLogModal.open, + onCancel: closeChangeLogModal, + footer: React.createElement(Button, { onClick: closeChangeLogModal }, '关闭'), + width: 880, + centered: true, + destroyOnClose: true + }, + React.createElement(Table, { + size: 'small', + bordered: true, + rowKey: 'id', + pagination: changeLogRows.length > 8 ? { pageSize: 8, showSizeChanger: false } : false, + columns: changeLogColumns, + dataSource: changeLogRows, + locale: { emptyText: '暂无变更记录' }, + scroll: { x: 'max-content', y: 360 } + }) + ), + + React.createElement(Modal, { + title: React.createElement('span', { style: { fontWeight: 700 } }, '需求说明 · 物流业务台账'), + open: requirementModalOpen, + onCancel: function () { setRequirementModalOpen(false); }, + width: 720, + centered: true, + destroyOnClose: true, + styles: { body: { maxHeight: '72vh', overflow: 'auto', paddingTop: 8, paddingBottom: 16 } }, + footer: React.createElement(Button, { + onClick: function () { setRequirementModalOpen(false); }, + style: { borderRadius: 8 } + }, '关闭') + }, renderLlRequirementDocPanel()) + ) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-data-analysis/pages/03-物流业务月度统计.jsx b/src/prototypes/oneos-web-data-analysis/pages/03-物流业务月度统计.jsx new file mode 100644 index 0000000..02e0212 --- /dev/null +++ b/src/prototypes/oneos-web-data-analysis/pages/03-物流业务月度统计.jsx @@ -0,0 +1,845 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 数据分析 - 物流业务月度统计:Tab「物流业务人员明细」+「物流盈亏月度汇总」(1–12 月汇总,交互同业务部业绩汇总) +// 人员明细:进入页面默认统计「上一自然月」 + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + + var antd = window.antd; + var App = antd.App; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Button = antd.Button; + var Table = antd.Table; + var Select = antd.Select; + var DatePicker = antd.DatePicker; + var Row = antd.Row; + var Col = antd.Col; + var Tabs = antd.Tabs; + var message = antd.message; + + var TableSummary = Table.Summary; + var SummaryRow = TableSummary.Row; + var SummaryCell = TableSummary.Cell; + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + function fmtCell(n) { + if (n === null || n === undefined || n === '') return '—'; + var x = Number(n); + if (isNaN(x)) return '—'; + if (x === 0) return '—'; + return x.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + } + + function fmtIntCell(n) { + if (n === null || n === undefined || n === '') return '—'; + var x = Number(n); + if (isNaN(x)) return '—'; + if (x === 0) return '—'; + return String(Math.round(x)); + } + + /** 盈亏:允许负数、零显示 0.00 */ + function fmtProfit(n) { + if (n === null || n === undefined || n === '') return '—'; + var x = Number(n); + if (isNaN(x)) return '—'; + return x.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + } + + function fmtSum(n) { + var x = Number(n); + if (isNaN(x)) return '—'; + return x.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 6 }); + } + + function escapeCsv(v) { + var s = v == null ? '' : String(v); + if (s.indexOf(',') !== -1 || s.indexOf('"') !== -1 || s.indexOf('\n') !== -1 || s.indexOf('\r') !== -1) { + return '"' + s.replace(/"/g, '""') + '"'; + } + return s; + } + + function downloadCsv(filename, lines) { + var csv = lines.map(function (row) { return row.map(escapeCsv).join(','); }).join('\n'); + var blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' }); + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); + } + + function getLastMonthStatKey() { + try { + if (window.dayjs) return window.dayjs().subtract(1, 'month').format('YYYY-MM'); + } catch (e1) {} + var d = new Date(); + d.setDate(1); + d.setMonth(d.getMonth() - 1); + var y = d.getFullYear(); + var mo = d.getMonth() + 1; + return y + '-' + (mo < 10 ? '0' + mo : '' + mo); + } + + function getLastMonthPickerValue() { + try { + if (window.dayjs) return window.dayjs().subtract(1, 'month').startOf('month'); + } catch (e1) {} + try { + if (window.moment) return window.moment().subtract(1, 'month').startOf('month'); + } catch (e2) {} + return null; + } + + var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var filterLabelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var filterItemStyle = { marginBottom: 12 }; + var filterControlStyle = { width: '100%' }; + var tableSingleLineStyle = + '.logistics-monthly-stat-table .ant-table-thead th,.logistics-monthly-stat-table .ant-table-tbody td,.logistics-monthly-stat-table .ant-table-summary td{white-space:nowrap;}'; + var tabsBarStyle = '.logistics-monthly-stat-tabs .ant-tabs-nav{margin-bottom:0;}'; + + var bizReportTableTitleStyle = { + textAlign: 'center', + marginBottom: 16, + fontSize: 16, + fontWeight: 600, + color: 'rgba(0,0,0,0.88)' + }; + + var profitNegStyle = { background: '#fff1f0', padding: '2px 8px', borderRadius: 4, display: 'inline-block' }; + + var numericSumKeys = [ + 'income', + 'hydrogenFee', + 'laborCost', + 'etcFee', + 'electricityFee', + 'salary', + 'vehicleCount', + 'tireCost', + 'deprecInsuranceFee', + 'socialSecurityFee', + 'trailerFee', + 'parkingFee', + 'vehicleTotalCost', + 'totalCost', + 'profitLoss' + ]; + + function sumRows(rows, keys) { + var sums = {}; + keys.forEach(function (k) { + sums[k] = (rows || []).reduce(function (acc, row) { + var v = row[k]; + var n = v === null || v === undefined || v === '' ? 0 : Number(v); + return acc + (isNaN(n) ? 0 : n); + }, 0); + }); + return sums; + } + + var lastMonthStatKey = useMemo(function () { + return getLastMonthStatKey(); + }, []); + + var rawRowsTemplate = useMemo(function () { + var sm = lastMonthStatKey; + return [ + { + key: 'lm-1', + statMonth: sm, + salesperson: '谈云', + businessName: '上海虹钦物流有限公司', + vehicleModel: '帕立安4.5T', + income: 1005557.94, + hydrogenFee: 185420.5, + laborCost: 128800, + etcFee: 45230.12, + electricityFee: 62300.8, + salary: 96000, + vehicleCount: 12, + tireCost: 8900, + deprecInsuranceFee: 156000, + socialSecurityFee: 28400, + trailerFee: 12000, + parkingFee: 6800, + vehicleTotalCost: 420000, + totalCost: 918851.42, + profitLoss: 86706.52, + remark: '' + }, + { + key: 'lm-2', + statMonth: sm, + salesperson: '刘念念', + businessName: '杭州绿道城配科技有限公司', + vehicleModel: '古道车队', + income: 680200, + hydrogenFee: 142000, + laborCost: 98000, + etcFee: 32100, + electricityFee: 41000, + salary: 72000, + vehicleCount: 8, + tireCost: 5600, + deprecInsuranceFee: 112000, + socialSecurityFee: 21000, + trailerFee: 8000, + parkingFee: 4200, + vehicleTotalCost: 298000, + totalCost: 832900, + profitLoss: -152700, + remark: '' + }, + { + key: 'lm-3', + statMonth: sm, + salesperson: '谈云', + businessName: '宁波港联氢运物流有限公司', + vehicleModel: '飞越49T', + income: 892300.5, + hydrogenFee: 210000, + laborCost: 145000, + etcFee: 38900, + electricityFee: 55000, + salary: 88000, + vehicleCount: 10, + tireCost: 7200, + deprecInsuranceFee: 198000, + socialSecurityFee: 25600, + trailerFee: 15000, + parkingFee: 5500, + vehicleTotalCost: 512000, + totalCost: 1284200.5, + profitLoss: -391900, + remark: '淡季线路调整' + }, + { + key: 'lm-4', + statMonth: sm, + salesperson: '谯云', + businessName: '嘉兴南湖氢能示范运营', + vehicleModel: '帕立安4.5T', + income: 325000, + hydrogenFee: 52000, + laborCost: 38000, + etcFee: 12000, + electricityFee: 18500, + salary: 28000, + vehicleCount: 4, + tireCost: 2100, + deprecInsuranceFee: 48000, + socialSecurityFee: 9200, + trailerFee: null, + parkingFee: 1800, + vehicleTotalCost: 125000, + totalCost: 334600, + profitLoss: -9600, + remark: '' + }, + { + key: 'lm-5', + statMonth: sm, + salesperson: '董剑煜', + businessName: '温州瓯江冷链专线', + vehicleModel: '宇通49T', + income: 456780, + hydrogenFee: 88000, + laborCost: 56000, + etcFee: 22000, + electricityFee: 31000, + salary: 42000, + vehicleCount: 5, + tireCost: 4500, + deprecInsuranceFee: 72000, + socialSecurityFee: 11800, + trailerFee: 6000, + parkingFee: 2400, + vehicleTotalCost: 198000, + totalCost: 583700, + profitLoss: -126920, + remark: '' + } + ]; + }, [lastMonthStatKey]); + + var salespersonOptions = useMemo(function () { + var set = {}; + (rawRowsTemplate || []).forEach(function (r) { + if (r.salesperson) set[r.salesperson] = true; + }); + return Object.keys(set).map(function (n) { return { value: n, label: n }; }); + }, [rawRowsTemplate]); + + var businessNameOptions = useMemo(function () { + var set = {}; + (rawRowsTemplate || []).forEach(function (r) { + if (r.businessName) set[r.businessName] = true; + }); + return Object.keys(set).map(function (n) { return { value: n, label: n }; }); + }, [rawRowsTemplate]); + + /** 物流盈亏月度汇总:按自然月 1–12 列示(原型仅 2026 年有数据) */ + var monthlyPlMetricKeys = numericSumKeys; + + var monthlyPlSummary2026 = useMemo(function () { + function mk(m, p) { + var r = Object.assign({ key: 'plm-' + m, month: m }, p); + if (r.profitLoss == null && r.income != null && r.totalCost != null) { + r.profitLoss = Number(r.income) - Number(r.totalCost); + } + return r; + } + function empty(m) { + return mk(m, {}); + } + return [ + mk(1, { + income: 3350838.44, + hydrogenFee: 537400, + laborCost: 418800, + etcFee: 148230.12, + electricityFee: 228800.8, + salary: 386000, + vehicleCount: 39, + tireCost: 28100, + deprecInsuranceFee: 586000, + socialSecurityFee: 96000, + trailerFee: 53000, + parkingFee: 19600, + vehicleTotalCost: 1553000, + totalCost: 4123731.44, + profitLoss: -772893 + }), + mk(2, { + income: 2680000, + hydrogenFee: 412000, + laborCost: 298000, + etcFee: 98500, + electricityFee: 156000, + salary: 265000, + vehicleCount: 26, + tireCost: 19200, + deprecInsuranceFee: 445000, + socialSecurityFee: 68400, + trailerFee: 41000, + parkingFee: 15800, + vehicleTotalCost: 1180000, + totalCost: 3550900, + profitLoss: -870900 + }), + mk(3, { + income: 3012000, + hydrogenFee: 468000, + laborCost: 352000, + etcFee: 108000, + electricityFee: 172500, + salary: 298000, + vehicleCount: 31, + tireCost: 22100, + deprecInsuranceFee: 502000, + socialSecurityFee: 75200, + trailerFee: 38500, + parkingFee: 16900, + vehicleTotalCost: 1285000, + totalCost: 3488200, + profitLoss: -476200 + }), + empty(4), + empty(5), + empty(6), + empty(7), + empty(8), + empty(9), + empty(10), + empty(11), + empty(12) + ]; + }, []); + + function initialYear2026() { + try { + if (window.dayjs) return window.dayjs('2026-01-01'); + } catch (e) {} + return null; + } + + var mainTabState = useState('personDetail'); + var mainTab = mainTabState[0]; + var setMainTab = mainTabState[1]; + + var summaryYearDraftState = useState(initialYear2026); + var summaryYearDraft = summaryYearDraftState[0]; + var setSummaryYearDraft = summaryYearDraftState[1]; + + var summaryYearAppliedState = useState(initialYear2026); + var summaryYearApplied = summaryYearAppliedState[0]; + var setSummaryYearApplied = summaryYearAppliedState[1]; + + var draftState = useState(function () { + return { + month: getLastMonthPickerValue(), + salesperson: undefined, + businessName: undefined + }; + }); + var draft = draftState[0]; + var setDraft = draftState[1]; + + var appliedState = useState(function () { + return { + month: getLastMonthPickerValue(), + salesperson: undefined, + businessName: undefined + }; + }); + var applied = appliedState[0]; + var setApplied = appliedState[1]; + + var filteredRows = useMemo(function () { + return (rawRowsTemplate || []).filter(function (r) { + if (applied.month && applied.month.format) { + var mk = applied.month.format('YYYY-MM'); + if (r.statMonth !== mk) return false; + } + if (applied.salesperson && r.salesperson !== applied.salesperson) return false; + if (applied.businessName && r.businessName !== applied.businessName) return false; + return true; + }); + }, [rawRowsTemplate, applied.month, applied.salesperson, applied.businessName]); + + var columnSums = useMemo(function () { + return sumRows(filteredRows, numericSumKeys); + }, [filteredRows]); + + var reportTitle = useMemo(function () { + if (applied.month && applied.month.format) { + var y = applied.month.format('YYYY'); + var m = applied.month.format('M'); + return y + '年' + m + '月浙江羚牛氢能自运营物流业务盈亏月度汇总表'; + } + return '浙江羚牛氢能自运营物流业务盈亏月度汇总表'; + }, [applied.month]); + + var plSumTableTitle = useMemo(function () { + if (summaryYearApplied && summaryYearApplied.format) { + return summaryYearApplied.format('YYYY') + '年物流盈亏月度汇总'; + } + return '物流盈亏月度汇总'; + }, [summaryYearApplied]); + + var monthlyPlDataSource = useCallback(function (rows) { + var y = summaryYearApplied && summaryYearApplied.format ? summaryYearApplied.format('YYYY') : null; + if (!y) return []; + if (y !== '2026') return []; + return rows; + }, [summaryYearApplied]); + + var plMonthlyRows = useMemo(function () { + return monthlyPlDataSource(monthlyPlSummary2026); + }, [monthlyPlDataSource, monthlyPlSummary2026]); + + var plMonthlySums = useMemo(function () { + return sumRows(plMonthlyRows, monthlyPlMetricKeys); + }, [plMonthlyRows]); + + var handleQuery = useCallback(function () { + setApplied(Object.assign({}, draft)); + }, [draft]); + + var handleReset = useCallback(function () { + var def = { month: getLastMonthPickerValue(), salesperson: undefined, businessName: undefined }; + setDraft(def); + setApplied(def); + }, []); + + var handleSummaryQuery = useCallback(function () { + setSummaryYearApplied(summaryYearDraft); + }, [summaryYearDraft]); + + var handleSummaryReset = useCallback(function () { + var y0 = initialYear2026(); + setSummaryYearDraft(y0); + setSummaryYearApplied(y0); + }, []); + + var handleExport = useCallback(function () { + var rows = filteredRows; + if (!rows || rows.length === 0) { + message.warning('当前无数据可导出'); + return; + } + var headers = [ + '业务员', + '业务名称', + '系统车型', + '收入', + '氢费', + '人工费用', + 'ETC', + '电费', + '薪资', + '投入车数', + '轮胎', + '(折旧、年审、保险)费用', + '社保服务费', + '挂车费用', + '停车费', + '车总费用', + '总成本', + '盈亏', + '备注' + ]; + var rowLine = function (r) { + return [ + r.salesperson, + r.businessName, + r.vehicleModel, + fmtCell(r.income), + fmtCell(r.hydrogenFee), + fmtCell(r.laborCost), + fmtCell(r.etcFee), + fmtCell(r.electricityFee), + fmtCell(r.salary), + fmtIntCell(r.vehicleCount), + fmtCell(r.tireCost), + fmtCell(r.deprecInsuranceFee), + fmtCell(r.socialSecurityFee), + fmtCell(r.trailerFee), + fmtCell(r.parkingFee), + fmtCell(r.vehicleTotalCost), + fmtCell(r.totalCost), + fmtProfit(r.profitLoss), + r.remark || '' + ]; + }; + var body = [headers].concat(rows.map(rowLine)); + var sumLine = ['总计', '', ''].concat( + numericSumKeys.map(function (k, idx) { + if (k === 'vehicleCount') return fmtIntCell(columnSums[k]); + if (k === 'profitLoss') return fmtSum(columnSums[k]); + return fmtSum(columnSums[k]); + }) + ); + body.push(sumLine); + downloadCsv('物流业务月度统计_' + new Date().getTime() + '.csv', body); + message.success('已导出 ' + rows.length + ' 条记录'); + }, [filteredRows, columnSums]); + + var plMonthlyExportHeaders = [ + '月份', + '收入', + '氢费', + '人工费用', + 'ETC', + '电费', + '薪资', + '投入车数', + '轮胎', + '(折旧、年审、保险)费用', + '社保服务费', + '挂车费用', + '停车费', + '车总费用', + '总成本', + '盈亏月度合计' + ]; + + var handleExportPlMonthly = useCallback(function () { + var rows = plMonthlyRows; + if (!rows || rows.length === 0) { + message.warning('当前无数据可导出,请先选择年份并查询'); + return; + } + var rowLine = function (r) { + return [ + String(r.month), + fmtCell(r.income), + fmtCell(r.hydrogenFee), + fmtCell(r.laborCost), + fmtCell(r.etcFee), + fmtCell(r.electricityFee), + fmtCell(r.salary), + fmtIntCell(r.vehicleCount), + fmtCell(r.tireCost), + fmtCell(r.deprecInsuranceFee), + fmtCell(r.socialSecurityFee), + fmtCell(r.trailerFee), + fmtCell(r.parkingFee), + fmtCell(r.vehicleTotalCost), + fmtCell(r.totalCost), + fmtProfit(r.profitLoss) + ]; + }; + var body = [plMonthlyExportHeaders].concat(rows.map(rowLine)); + body.push(['总计'].concat( + monthlyPlMetricKeys.map(function (k) { + if (k === 'vehicleCount') return fmtIntCell(plMonthlySums[k]); + return fmtSum(plMonthlySums[k] || 0); + }) + )); + downloadCsv('物流盈亏月度汇总_' + new Date().getTime() + '.csv', body); + message.success('已导出 ' + rows.length + ' 个月度数据'); + }, [plMonthlyRows, plMonthlySums]); + + var columns = useMemo(function () { + return [ + { title: '业务员', dataIndex: 'salesperson', key: 'salesperson', width: 88, align: 'center', fixed: 'left' }, + { title: '业务名称', dataIndex: 'businessName', key: 'businessName', width: 200, ellipsis: true, fixed: 'left' }, + { title: '系统车型', dataIndex: 'vehicleModel', key: 'vehicleModel', width: 110, align: 'center' }, + { title: '收入', dataIndex: 'income', key: 'income', width: 120, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '氢费', dataIndex: 'hydrogenFee', key: 'hydrogenFee', width: 100, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '人工费用', dataIndex: 'laborCost', key: 'laborCost', width: 100, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: 'ETC', dataIndex: 'etcFee', key: 'etcFee', width: 90, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '电费', dataIndex: 'electricityFee', key: 'electricityFee', width: 100, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '薪资', dataIndex: 'salary', key: 'salary', width: 90, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '投入车数', dataIndex: 'vehicleCount', key: 'vehicleCount', width: 92, align: 'right', render: function (v) { return fmtIntCell(v); } }, + { title: '轮胎', dataIndex: 'tireCost', key: 'tireCost', width: 88, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '(折旧、年审、保险)费用', dataIndex: 'deprecInsuranceFee', key: 'deprecInsuranceFee', width: 160, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '社保服务费', dataIndex: 'socialSecurityFee', key: 'socialSecurityFee', width: 110, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '挂车费用', dataIndex: 'trailerFee', key: 'trailerFee', width: 100, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '停车费', dataIndex: 'parkingFee', key: 'parkingFee', width: 90, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '车总费用', dataIndex: 'vehicleTotalCost', key: 'vehicleTotalCost', width: 110, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '总成本', dataIndex: 'totalCost', key: 'totalCost', width: 120, align: 'right', render: function (v) { return fmtCell(v); } }, + { + title: '盈亏', + dataIndex: 'profitLoss', + key: 'profitLoss', + width: 120, + align: 'right', + render: function (v) { + var s = fmtProfit(v); + var neg = v !== null && v !== undefined && v !== '' && !isNaN(Number(v)) && Number(v) < 0; + return neg ? React.createElement('span', { style: profitNegStyle }, s) : s; + } + }, + { title: '备注', dataIndex: 'remark', key: 'remark', width: 140, ellipsis: true } + ]; + }, []); + + var tableSummary = useCallback(function () { + return React.createElement( + TableSummary, + null, + React.createElement( + SummaryRow, + null, + React.createElement(SummaryCell, { index: 0, align: 'center', colSpan: 3 }, '总计'), + numericSumKeys.map(function (k, idx) { + var text; + if (k === 'vehicleCount') text = fmtIntCell(columnSums[k]); + else text = fmtSum(columnSums[k]); + var isNegProfit = k === 'profitLoss' && Number(columnSums[k]) < 0; + var child = isNegProfit ? React.createElement('span', { style: profitNegStyle }, text) : text; + return React.createElement(SummaryCell, { key: k, index: idx + 3, align: 'right' }, child); + }), + React.createElement(SummaryCell, { index: numericSumKeys.length + 3, align: 'left' }, '') + ) + ); + }, [columnSums]); + + var columnsPlMonthly = useMemo(function () { + return [ + { title: '月份', dataIndex: 'month', key: 'month', width: 64, align: 'center' }, + { title: '收入', dataIndex: 'income', key: 'income', width: 118, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '氢费', dataIndex: 'hydrogenFee', key: 'hydrogenFee', width: 100, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '人工费用', dataIndex: 'laborCost', key: 'laborCost', width: 100, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: 'ETC', dataIndex: 'etcFee', key: 'etcFee', width: 88, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '电费', dataIndex: 'electricityFee', key: 'electricityFee', width: 100, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '薪资', dataIndex: 'salary', key: 'salary', width: 90, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '投入车数', dataIndex: 'vehicleCount', key: 'vehicleCount', width: 92, align: 'right', render: function (v) { return fmtIntCell(v); } }, + { title: '轮胎', dataIndex: 'tireCost', key: 'tireCost', width: 84, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '(折旧、年审、保险)费用', dataIndex: 'deprecInsuranceFee', key: 'deprecInsuranceFee', width: 168, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '社保服务费', dataIndex: 'socialSecurityFee', key: 'socialSecurityFee', width: 110, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '挂车费用', dataIndex: 'trailerFee', key: 'trailerFee', width: 100, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '停车费', dataIndex: 'parkingFee', key: 'parkingFee', width: 88, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '车总费用', dataIndex: 'vehicleTotalCost', key: 'vehicleTotalCost', width: 110, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '总成本', dataIndex: 'totalCost', key: 'totalCost', width: 118, align: 'right', render: function (v) { return fmtCell(v); } }, + { + title: '盈亏月度合计', + dataIndex: 'profitLoss', + key: 'profitLoss', + width: 128, + align: 'right', + render: function (v) { + var s = fmtProfit(v); + var neg = v !== null && v !== undefined && v !== '' && !isNaN(Number(v)) && Number(v) < 0; + return neg ? React.createElement('span', { style: profitNegStyle }, s) : s; + } + } + ]; + }, []); + + var tableSummaryPlMonthly = useCallback(function () { + return React.createElement( + TableSummary, + null, + React.createElement( + SummaryRow, + null, + React.createElement(SummaryCell, { index: 0, align: 'center' }, '总计'), + monthlyPlMetricKeys.map(function (k, idx) { + var text = k === 'vehicleCount' ? fmtIntCell(plMonthlySums[k]) : fmtSum(plMonthlySums[k] || 0); + var isNeg = k === 'profitLoss' && Number(plMonthlySums[k]) < 0; + var child = isNeg ? React.createElement('span', { style: profitNegStyle }, text) : text; + return React.createElement(SummaryCell, { key: k, index: idx + 1, align: 'right' }, child); + }) + ) + ); + }, [plMonthlySums]); + + var renderYearFilterCard = useCallback(function () { + return React.createElement(Card, { style: { marginBottom: 16 } }, + React.createElement(Row, { gutter: [16, 16], align: 'bottom' }, + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '选择年份'), + React.createElement(DatePicker, { + picker: 'year', + style: filterControlStyle, + placeholder: '请选择统计年份', + format: 'YYYY', + value: summaryYearDraft, + onChange: function (v) { setSummaryYearDraft(v); } + }) + ) + ) + ), + React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 } }, + React.createElement(Button, { onClick: handleSummaryReset }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleSummaryQuery }, '查询') + ) + ); + }, [summaryYearDraft, handleSummaryReset, handleSummaryQuery]); + + var tabItems = [ + { + key: 'personDetail', + label: '物流业务人员明细', + children: React.createElement(React.Fragment, null, + React.createElement(Card, { style: { marginBottom: 16 } }, + React.createElement(Row, { gutter: [16, 16], align: 'bottom' }, + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '统计月份'), + React.createElement(DatePicker, { + picker: 'month', + style: filterControlStyle, + placeholder: '请选择年-月', + format: 'YYYY-MM', + value: draft.month, + onChange: function (v) { setDraft(function (p) { return Object.assign({}, p, { month: v }); }); } + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '业务员'), + React.createElement(Select, { + placeholder: '请选择业务员', + style: filterControlStyle, + allowClear: true, + options: salespersonOptions, + value: draft.salesperson, + onChange: function (v) { setDraft(function (p) { return Object.assign({}, p, { salesperson: v }); }); }, + showSearch: true, + filterOption: filterOption + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '业务名称'), + React.createElement(Select, { + placeholder: '请选择业务名称', + style: filterControlStyle, + allowClear: true, + options: businessNameOptions, + value: draft.businessName, + onChange: function (v) { setDraft(function (p) { return Object.assign({}, p, { businessName: v }); }); }, + showSearch: true, + filterOption: filterOption + }) + ) + ) + ), + React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 } }, + React.createElement(Button, { onClick: handleReset }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleQuery }, '查询') + ) + ), + React.createElement(Card, { + extra: React.createElement(Button, { onClick: handleExport }, '导出') + }, + React.createElement('div', { style: bizReportTableTitleStyle }, reportTitle), + React.createElement('style', null, tableSingleLineStyle), + React.createElement('div', { className: 'logistics-monthly-stat-table' }, + React.createElement(Table, { + rowKey: 'key', + columns: columns, + dataSource: filteredRows, + pagination: false, + size: 'small', + summary: tableSummary, + scroll: { x: 2600 } + }) + ) + ) + ) + }, + { + key: 'plMonthly', + label: '物流盈亏月度汇总', + children: React.createElement(React.Fragment, null, + renderYearFilterCard(), + React.createElement(Card, { + extra: React.createElement(Button, { onClick: handleExportPlMonthly }, '导出') + }, + React.createElement('div', { style: bizReportTableTitleStyle }, plSumTableTitle), + React.createElement('style', null, tableSingleLineStyle), + React.createElement('div', { className: 'logistics-monthly-stat-table' }, + React.createElement(Table, { + rowKey: 'key', + columns: columnsPlMonthly, + dataSource: plMonthlyRows, + pagination: false, + size: 'small', + summary: tableSummaryPlMonthly, + scroll: { x: 2480 } + }) + ) + ) + ) + } + ]; + + return React.createElement(App, null, + React.createElement('div', { style: layoutStyle }, + React.createElement(Breadcrumb, { + style: { marginBottom: 16 }, + items: [{ title: '数据分析' }, { title: '物流业务月度统计' }] + }), + React.createElement(Card, null, + React.createElement('style', null, tabsBarStyle), + React.createElement(Tabs, { + className: 'logistics-monthly-stat-tabs', + activeKey: mainTab, + onChange: function (k) { setMainTab(k); }, + items: tabItems + }) + ) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-data-analysis/pages/04-业务部汇总台账.jsx b/src/prototypes/oneos-web-data-analysis/pages/04-业务部汇总台账.jsx new file mode 100644 index 0000000..b4796d9 --- /dev/null +++ b/src/prototypes/oneos-web-data-analysis/pages/04-业务部汇总台账.jsx @@ -0,0 +1,561 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 数据分析 - 浙江羚牛氢能业务部汇总台账(月度 × 业务 × 业绩/成本/利润) +// 原型:年份 + 业务部筛选、导出;点击「业绩」金额钻取业务员 → 再钻取项目明细(联调可替换为接口) + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + + var antd = window.antd; + var App = antd.App; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Button = antd.Button; + var Table = antd.Table; + var Select = antd.Select; + var DatePicker = antd.DatePicker; + var Row = antd.Row; + var Col = antd.Col; + var Space = antd.Space; + var Modal = antd.Modal; + var message = antd.message; + + /** 业务板块(与示意图一致):自营 / 租赁 / 销售 / 审车 / 代办 / ETC / 其他 */ + var CATEGORY_DEFS = [ + { key: 'self', label: '自营业务' }, + { key: 'lease', label: '租赁业务' }, + { key: 'sales', label: '销售' }, + { key: 'inspection', label: '审车' }, + { key: 'agency', label: '代办' }, + { key: 'etc', label: 'ETC' }, + { key: 'other', label: '其他' } + ]; + + var CATEGORY_KEYS = CATEGORY_DEFS.map(function (c) { return c.key; }); + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + function fmtMoney(n) { + if (n === null || n === undefined || n === '') return '-'; + var x = Number(n); + if (isNaN(x)) return '-'; + if (x === 0) return '-'; + return x.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + } + + function escapeCsv(v) { + var s = v == null ? '' : String(v); + if (s.indexOf(',') !== -1 || s.indexOf('"') !== -1 || s.indexOf('\n') !== -1 || s.indexOf('\r') !== -1) { + return '"' + s.replace(/"/g, '""') + '"'; + } + return s; + } + + function downloadCsv(filename, lines) { + var csv = lines.map(function (row) { return row.map(escapeCsv).join(','); }).join('\n'); + var blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' }); + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); + } + + function initialYear() { + try { + if (window.dayjs) return window.dayjs('2026-01-01'); + } catch (e1) {} + return null; + } + + function numOrZero(v) { + if (v === null || v === undefined || v === '') return 0; + var n = Number(v); + return isNaN(n) ? 0 : n; + } + + /** 汇总行:对 1–12 月逐字段求和 */ + function sumLedgerRows(monthRows) { + var acc = { month: 13, monthLabel: '合计', rowType: 'total', key: 'total' }; + CATEGORY_KEYS.forEach(function (k) { + acc[k + 'Perf'] = 0; + acc[k + 'Cost'] = 0; + acc[k + 'Profit'] = 0; + }); + (monthRows || []).forEach(function (r) { + CATEGORY_KEYS.forEach(function (k) { + acc[k + 'Perf'] += numOrZero(r[k + 'Perf']); + acc[k + 'Cost'] += numOrZero(r[k + 'Cost']); + acc[k + 'Profit'] += numOrZero(r[k + 'Profit']); + }); + }); + CATEGORY_KEYS.forEach(function (k) { + if (acc[k + 'Perf'] === 0) acc[k + 'Perf'] = null; + if (acc[k + 'Cost'] === 0) acc[k + 'Cost'] = null; + if (acc[k + 'Profit'] === 0) acc[k + 'Profit'] = null; + }); + return acc; + } + + /** + * 原型数据:2026 年按月;1 月部分数值与示意图/样例一致(审车业绩、代办业绩、ETC业绩) + * 其余月份为演示占位,可联调替换 + */ + function buildMockYear2026() { + var rows = []; + var i; + for (i = 1; i <= 12; i++) { + var selfPerf = 250000 + Math.random() * 100000; + var selfCost = selfPerf * (0.8 + Math.random() * 0.1); + + var leasePerf = 180000 + Math.random() * 50000; + var leaseCost = leasePerf * (0.6 + Math.random() * 0.1); + + var salesPerf = 300000 + Math.random() * 200000; + var salesCost = salesPerf * (0.7 + Math.random() * 0.15); + + var inspectionPerf = 80000 + Math.random() * 60000; + var inspectionCost = inspectionPerf * (0.6 + Math.random() * 0.2); + + var agencyPerf = 3000 + Math.random() * 2000; + var agencyCost = agencyPerf * (0.3 + Math.random() * 0.1); + + var etcPerf = 70000 + Math.random() * 20000; + var etcCost = etcPerf * (0.5 + Math.random() * 0.1); + + var otherPerf = 8000 + Math.random() * 5000; + var otherCost = otherPerf * (0.4 + Math.random() * 0.2); + + var src = { + selfPerf: Math.round(selfPerf * 100) / 100, + selfCost: Math.round(selfCost * 100) / 100, + selfProfit: Math.round((selfPerf - selfCost) * 100) / 100, + + leasePerf: Math.round(leasePerf * 100) / 100, + leaseCost: Math.round(leaseCost * 100) / 100, + leaseProfit: Math.round((leasePerf - leaseCost) * 100) / 100, + + salesPerf: Math.round(salesPerf * 100) / 100, + salesCost: Math.round(salesCost * 100) / 100, + salesProfit: Math.round((salesPerf - salesCost) * 100) / 100, + + inspectionPerf: Math.round(inspectionPerf * 100) / 100, + inspectionCost: Math.round(inspectionCost * 100) / 100, + inspectionProfit: Math.round((inspectionPerf - inspectionCost) * 100) / 100, + + agencyPerf: Math.round(agencyPerf * 100) / 100, + agencyCost: Math.round(agencyCost * 100) / 100, + agencyProfit: Math.round((agencyPerf - agencyCost) * 100) / 100, + + etcPerf: Math.round(etcPerf * 100) / 100, + etcCost: Math.round(etcCost * 100) / 100, + etcProfit: Math.round((etcPerf - etcCost) * 100) / 100, + + otherPerf: Math.round(otherPerf * 100) / 100, + otherCost: Math.round(otherCost * 100) / 100, + otherProfit: Math.round((otherPerf - otherCost) * 100) / 100 + }; + + rows.push({ + key: 'm' + i, + month: i, + monthLabel: i + '月', + rowType: 'month', + selfPerf: src.selfPerf, selfCost: src.selfCost, selfProfit: src.selfProfit, + leasePerf: src.leasePerf, leaseCost: src.leaseCost, leaseProfit: src.leaseProfit, + salesPerf: src.salesPerf, salesCost: src.salesCost, salesProfit: src.salesProfit, + inspectionPerf: src.inspectionPerf, inspectionCost: src.inspectionCost, inspectionProfit: src.inspectionProfit, + agencyPerf: src.agencyPerf, agencyCost: src.agencyCost, agencyProfit: src.agencyProfit, + etcPerf: src.etcPerf, etcCost: src.etcCost, etcProfit: src.etcProfit, + otherPerf: src.otherPerf, otherCost: src.otherCost, otherProfit: src.otherProfit + }); + } + rows.push(sumLedgerRows(rows)); + return rows; + } + + /** 业务员钻取:按 月 + 业务 返回演示列表(金额拆分为比例,末行补齐差额) */ + function mockSalesmenDrill(month, catKey, cellPerf) { + var base = [ + { key: 's1', name: '尚建华', ratio: 0.42 }, + { key: 's2', name: '刘念念', ratio: 0.35 }, + { key: 's3', name: '谯云', ratio: 0.15 }, + { key: 's4', name: '董剑煜', ratio: 0.08 } + ]; + var total = numOrZero(cellPerf); + if (total <= 0) total = 100000; + var assigned = 0; + var parts = base.map(function (b, idx) { + if (idx === base.length - 1) { + var last = Math.round((total - assigned) * 100) / 100; + return { key: b.key, salesperson: b.name, amount: last }; + } + var amt = Math.round(total * b.ratio * 100) / 100; + assigned += amt; + return { key: b.key, salesperson: b.name, amount: amt }; + }); + return parts; + } + + /** 项目明细钻取 */ + function mockProjectRows(salesperson, catKey) { + var catLabel = (CATEGORY_DEFS.find(function (c) { return c.key === catKey; }) || {}).label || catKey; + return [ + { key: 'p1', projectCode: 'PRJ-2026-001', projectName: catLabel + ' · 嘉兴冷链城配项目', plateNo: '沪A62261F', amount: null, bizDate: '2026-01-08', remark: '演示' }, + { key: 'p2', projectCode: 'PRJ-2026-018', projectName: catLabel + ' · 沪浙干线运输', plateNo: '粤AGP3649', amount: null, bizDate: '2026-01-15', remark: '-' }, + { key: 'p3', projectCode: 'PRJ-2026-033', projectName: catLabel + ' · 园区短驳', plateNo: '苏E·D32891', amount: null, bizDate: '2026-01-22', remark: '-' } + ].map(function (r, i, arr) { + var share = i === arr.length - 1 ? 1 - arr.slice(0, -1).reduce(function (a) { return a + 0.31; }, 0) : 0.31; + return Object.assign({}, r, { amount: Math.round(88000 * share * 100) / 100 }); + }); + } + + var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var filterLabelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var filterItemStyle = { marginBottom: 12 }; + var filterControlStyle = { width: '100%' }; + var filterActionsColStyle = { flex: '0 0 auto', marginLeft: 'auto' }; + + var ledgerTableStyle = + '.biz-ledger-summary-table .ant-table-thead>tr>th{color:rgba(0,0,0,0.75)!important;font-weight:500!important;font-size:12px!important;background:#e6f4ff!important;border-color:rgba(0,0,0,0.06)!important}' + + '.biz-ledger-summary-table .ant-table-thead>tr>th.ant-table-cell{background:#e6f4ff!important}' + + '.biz-ledger-summary-table .ant-table-tbody>tr>td,.biz-ledger-summary-table .ant-table-tbody>tr.ant-table-row{white-space:nowrap}' + + '.biz-ledger-summary-table .ant-table-tbody>tr[data-row-key=\"total\"]>td{font-weight:600;background:#fafafa!important}' + + '.biz-ledger-perf-link{cursor:pointer;color:#1677ff;padding:0;border:none;background:none;font:inherit}' + + '.biz-ledger-perf-link:hover{text-decoration:underline;color:#4096ff}'; + + var deptOptions = useMemo(function () { + return [ + { value: '业务二部', label: '业务二部' }, + { value: '华东业务部', label: '华东业务部' }, + { value: '华南业务部', label: '华南业务部' }, + { value: '华北业务部', label: '华北业务部' }, + { value: '西南业务部', label: '西南业务部' } + ]; + }, []); + + var yearDraftState = useState(initialYear); + var yearDraft = yearDraftState[0]; + var setYearDraft = yearDraftState[1]; + + var yearAppliedState = useState(initialYear); + var yearApplied = yearAppliedState[0]; + var setYearApplied = yearAppliedState[1]; + + var deptDraftState = useState('业务二部'); + var deptDraft = deptDraftState[0]; + var setDeptDraft = deptDraftState[1]; + + var deptAppliedState = useState('业务二部'); + var deptApplied = deptAppliedState[0]; + var setDeptApplied = deptAppliedState[1]; + + var dataSource = useMemo(function () { + var y = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : ''; + if (y === '2026') return buildMockYear2026(); + return []; + }, [yearApplied]); + + var tableTitle = useMemo(function () { + var y = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : '—'; + return y + '年度浙江羚牛氢能业务部汇总台账'; + }, [yearApplied]); + + var handleQuery = useCallback(function () { + setYearApplied(yearDraft); + setDeptApplied(deptDraft); + }, [yearDraft, deptDraft]); + + var handleReset = useCallback(function () { + var y0 = initialYear(); + setYearDraft(y0); + setYearApplied(y0); + setDeptDraft('业务二部'); + setDeptApplied('业务二部'); + }, []); + + var salesModalState = useState({ open: false, month: null, monthLabel: '', catKey: '', catLabel: '', perf: null }); + var salesModal = salesModalState[0]; + var setSalesModal = salesModalState[1]; + + var projectModalState = useState({ open: false, salesperson: '', catKey: '', amount: null }); + var projectModal = projectModalState[0]; + var setProjectModal = projectModalState[1]; + + var salesModalRows = useMemo(function () { + if (!salesModal.open || salesModal.month == null || !salesModal.catKey) return []; + return mockSalesmenDrill(salesModal.month, salesModal.catKey, salesModal.perf); + }, [salesModal.open, salesModal.month, salesModal.catKey, salesModal.perf]); + + var projectModalRows = useMemo(function () { + if (!projectModal.open || !projectModal.salesperson) return []; + return mockProjectRows(projectModal.salesperson, projectModal.catKey || 'self'); + }, [projectModal.open, projectModal.salesperson, projectModal.catKey]); + + var openPerfDrill = useCallback(function (row, catKey) { + if (row.rowType === 'total') { + message.info('合计行演示不支持钻取,请从各月份「业绩」进入'); + return; + } + var v = row[catKey + 'Perf']; + if (v === null || v === undefined || v === '' || numOrZero(v) === 0) { + message.warning('该单元格无业绩数据'); + return; + } + var catLabel = (CATEGORY_DEFS.find(function (c) { return c.key === catKey; }) || {}).label || catKey; + setSalesModal({ + open: true, + month: row.month, + monthLabel: row.monthLabel, + catKey: catKey, + catLabel: catLabel, + perf: v + }); + }, []); + + var closeSalesModal = useCallback(function () { + setSalesModal(function (s) { return Object.assign({}, s, { open: false }); }); + }, []); + + var openProjectDrill = useCallback(function (r) { + setProjectModal({ + open: true, + salesperson: r.salesperson, + catKey: salesModal.catKey, + amount: r.amount + }); + }, [salesModal.catKey]); + + var closeProjectModal = useCallback(function () { + setProjectModal({ open: false, salesperson: '', catKey: '', amount: null }); + }, []); + + var salesModalColumns = useMemo(function () { + return [ + { title: '业务员', dataIndex: 'salesperson', key: 'salesperson', width: 120 }, + { + title: '业绩金额', + dataIndex: 'amount', + key: 'amount', + align: 'right', + render: function (v, r) { + return React.createElement('button', { + type: 'button', + className: 'biz-ledger-perf-link', + onClick: function () { openProjectDrill(r); } + }, fmtMoney(v)); + } + }, + { title: '说明', key: 'hint', width: 200, render: function () { return React.createElement('span', { style: { color: 'rgba(0,0,0,0.45)', fontSize: 12 } }, '点击金额查看项目明细'); } } + ]; + }, [openProjectDrill]); + + var projectModalColumns = useMemo(function () { + return [ + { title: '项目编号', dataIndex: 'projectCode', key: 'projectCode', width: 130 }, + { title: '项目名称', dataIndex: 'projectName', key: 'projectName', ellipsis: true }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 110 }, + { title: '业绩金额', dataIndex: 'amount', key: 'amount', align: 'right', render: function (v) { return fmtMoney(v); } }, + { title: '业务日期', dataIndex: 'bizDate', key: 'bizDate', width: 110 }, + { title: '备注', dataIndex: 'remark', key: 'remark', width: 80 } + ]; + }, []); + + var handleExport = useCallback(function () { + if (!dataSource || dataSource.length === 0) { + message.warning('当前无数据可导出,请先查询'); + return; + } + var y = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : 'ledger'; + var headers = ['月份']; + CATEGORY_DEFS.forEach(function (c) { + headers.push(c.label + '-业绩', c.label + '-成本', c.label + '-利润'); + }); + var body = [headers]; + dataSource.forEach(function (r) { + var line = [r.monthLabel]; + CATEGORY_KEYS.forEach(function (k) { + line.push(fmtMoney(r[k + 'Perf']), fmtMoney(r[k + 'Cost']), fmtMoney(r[k + 'Profit'])); + }); + body.push(line); + }); + body.push(['业务部', deptApplied]); + downloadCsv('业务部汇总台账_' + y + '_' + new Date().getTime() + '.csv', body); + message.success('已导出'); + }, [dataSource, yearApplied, deptApplied]); + + var ledgerColumns = useMemo(function () { + var cols = [ + { + title: '月份', + dataIndex: 'monthLabel', + key: 'monthLabel', + fixed: 'left', + width: 72, + align: 'center', + render: function (t, r) { + if (r.rowType === 'total') return React.createElement('span', { style: { fontWeight: 600 } }, t); + return t; + } + } + ]; + CATEGORY_DEFS.forEach(function (cat) { + var ck = cat.key; + cols.push({ + title: cat.label, + key: 'grp-' + ck, + children: [ + { + title: '业绩', + dataIndex: ck + 'Perf', + key: ck + 'Perf', + width: 108, + align: 'right', + render: function (v, row) { + if (row.rowType === 'total' || v === null || v === undefined || v === '' || numOrZero(v) === 0) { + return fmtMoney(v); + } + return React.createElement('button', { + type: 'button', + className: 'biz-ledger-perf-link', + onClick: function () { openPerfDrill(row, ck); } + }, fmtMoney(v)); + } + }, + { + title: '成本', + dataIndex: ck + 'Cost', + key: ck + 'Cost', + width: 108, + align: 'right', + render: function (v) { return fmtMoney(v); } + }, + { + title: '利润', + dataIndex: ck + 'Profit', + key: ck + 'Profit', + width: 108, + align: 'right', + render: function (v) { return fmtMoney(v); } + } + ] + }); + }); + return cols; + }, [openPerfDrill]); + + return React.createElement(App, null, + React.createElement('style', null, ledgerTableStyle), + React.createElement('div', { style: layoutStyle }, + React.createElement(Breadcrumb, { style: { marginBottom: 12 }, items: [ + { title: '数据分析' }, + { title: '业务部汇总台账' } + ] }), + React.createElement(Card, { style: { marginBottom: 16 } }, + React.createElement(Row, { gutter: [16, 16], align: 'bottom' }, + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '年份选择'), + React.createElement(DatePicker, { + picker: 'year', + style: filterControlStyle, + placeholder: '请选择年份', + format: 'YYYY', + value: yearDraft, + onChange: function (v) { setYearDraft(v); } + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '业务部'), + React.createElement(Select, { + placeholder: '请选择业务部', + style: filterControlStyle, + value: deptDraft, + onChange: function (v) { setDeptDraft(v); }, + options: deptOptions, + showSearch: true, + allowClear: false, + filterOption: filterOption + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6, style: filterActionsColStyle }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '\u00a0'), + React.createElement(Space, { wrap: true }, + React.createElement(Button, { onClick: handleReset }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleQuery }, '查询'), + React.createElement(Button, { onClick: handleExport }, '导出') + ) + ) + ) + ) + ), + React.createElement(Card, null, + React.createElement('div', { style: { textAlign: 'center', marginBottom: 16, fontSize: 16, fontWeight: 600, color: 'rgba(0,0,0,0.88)' } }, tableTitle), + React.createElement('div', { style: { textAlign: 'center', marginBottom: 8, fontSize: 12, color: 'rgba(0,0,0,0.45)' } }, + '业务部:', deptApplied, ' (原型:仅 2026 年有演示数据;筛选业务部不影响数值,联调后按接口过滤)' + ), + React.createElement(Table, { + className: 'biz-ledger-summary-table', + size: 'small', + bordered: true, + rowKey: 'key', + columns: ledgerColumns, + dataSource: dataSource, + pagination: false, + scroll: { x: 2600, y: 520 }, + sticky: true + }) + ), + React.createElement(Modal, { + title: '业务员业绩 — ' + (salesModal.monthLabel || '') + ' / ' + (salesModal.catLabel || ''), + open: salesModal.open, + width: 720, + onCancel: closeSalesModal, + footer: React.createElement(Button, { onClick: closeSalesModal }, '关闭'), + destroyOnClose: true + }, + React.createElement('p', { style: { marginBottom: 12, color: 'rgba(0,0,0,0.55)', fontSize: 12 } }, + '从总表钻取:本业务线下各业务员业绩构成。点击「业绩金额」继续查看项目明细。' + ), + React.createElement(Table, { + size: 'small', + rowKey: 'key', + columns: salesModalColumns, + dataSource: salesModalRows, + pagination: false + }) + ), + React.createElement(Modal, { + title: '项目明细 — ' + (projectModal.salesperson || '') + ' · ' + ((CATEGORY_DEFS.find(function (c) { return c.key === projectModal.catKey; }) || {}).label || ''), + open: projectModal.open, + width: 900, + onCancel: closeProjectModal, + footer: React.createElement(Button, { onClick: closeProjectModal }, '关闭'), + destroyOnClose: true + }, + React.createElement('p', { style: { marginBottom: 12, color: 'rgba(0,0,0,0.55)', fontSize: 12 } }, + '二级钻取:该项目业务员名下具体项目/车辆维度业绩(演示数据)。' + ), + React.createElement(Table, { + size: 'small', + rowKey: 'key', + columns: projectModalColumns, + dataSource: projectModalRows, + pagination: false, + scroll: { x: 800 } + }) + ) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-data-analysis/pages/05-业务部台账.jsx b/src/prototypes/oneos-web-data-analysis/pages/05-业务部台账.jsx new file mode 100644 index 0000000..c8ba5e2 --- /dev/null +++ b/src/prototypes/oneos-web-data-analysis/pages/05-业务部台账.jsx @@ -0,0 +1,2470 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 数据分析 - 业务部台账(多业务板块:业绩 / 成本 / 利润,按月汇总) + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + + var antd = window.antd; + var App = antd.App; + var Card = antd.Card; + var Button = antd.Button; + var Table = antd.Table; + var Select = antd.Select; + var DatePicker = antd.DatePicker; + var RangePicker = DatePicker.RangePicker; + var Radio = antd.Radio; + var Row = antd.Row; + var Col = antd.Col; + var Space = antd.Space; + var Form = antd.Form; + var Divider = antd.Divider; + var Typography = antd.Typography; + var message = antd.message; + var Input = antd.Input; + var InputNumber = antd.InputNumber; + var Popover = antd.Popover; + + var Title = Typography.Title; + + /** 业务板块列(每列下挂:业绩、成本、利润) */ + var SECTOR_COLUMNS = [ + { key: 'logistics', label: '物流业务' }, + { key: 'lease', label: '租赁业务' }, + { key: 'sales', label: '销售业务' }, + { key: 'hydrogen', label: '氢费业务' }, + { key: 'elec', label: '电费业务' }, + { key: 'etc', label: 'ETC业务' }, + { key: 'other', label: '其他' } + ]; + + var ALL_DATA_KEYS = SECTOR_COLUMNS.map(function (s) { return s.key; }); + + var SearchIcon = function () { return React.createElement('svg', { viewBox: '0 0 48 48', width: 14, height: 14, fill: 'none', stroke: 'currentColor', strokeWidth: 4 }, React.createElement('path', { d: 'M33.072 33.071c6.248-6.248 6.248-16.379 0-22.627-6.249-6.249-16.38-6.249-22.628 0-6.248 6.248-6.248 16.379 0 22.627 6.248 6.248 16.38 6.248 22.628 0Zm0 0 8.485 8.485' })); }; + var ResetIcon = function () { return React.createElement('svg', { viewBox: '0 0 48 48', width: 14, height: 14, fill: 'none', stroke: 'currentColor', strokeWidth: 4 }, React.createElement('path', { d: 'M38.837 18C36.463 12.136 30.715 8 24 8 15.163 8 8 15.163 8 24s7.163 16 16 16c7.455 0 13.72-5.1 15.496-12M40 8v10H30' })); }; + var DownloadIcon = function () { return React.createElement('svg', { viewBox: '0 0 48 48', width: 14, height: 14, fill: 'none', stroke: 'currentColor', strokeWidth: 4 }, React.createElement('path', { d: 'm33.072 22.071-9.07 9.071-9.072-9.07M24 5v26m16 4v6H8v-6' })); }; + var EditIcon = function () { return React.createElement('svg', { viewBox: '0 0 48 48', width: 14, height: 14, fill: 'none', stroke: 'currentColor', strokeWidth: 4, strokeLinecap: 'butt', strokeLinejoin: 'miter' }, React.createElement('path', { d: 'M29 10l9 9L17 40H8v-9L29 10z' })); }; + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + /** 主表金额:含 0 与负数,与设计稿一致 */ + function fmtLedgerCell(n) { + if (n === null || n === undefined || n === '') return ''; + var x = Number(n); + if (isNaN(x)) return ''; + return x.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + } + + function escapeCsv(v) { + var s = v == null ? '' : String(v); + if (s.indexOf(',') !== -1 || s.indexOf('"') !== -1 || s.indexOf('\n') !== -1 || s.indexOf('\r') !== -1) { + return '"' + s.replace(/"/g, '""') + '"'; + } + return s; + } + + function downloadCsv(filename, lines) { + var csv = lines.map(function (row) { return row.map(escapeCsv).join(','); }).join('\n'); + var blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' }); + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); + } + + function initialYear() { + try { + if (window.dayjs) return window.dayjs('2026-01-01'); + } catch (e1) {} + return null; + } + + function numOrZero(v) { + if (v === null || v === undefined || v === '') return 0; + var n = Number(v); + return isNaN(n) ? 0 : n; + } + + function pad2(m) { + var n = Number(m); + if (n < 10) return '0' + n; + return String(n); + } + + function mockHydrogenCustomerPerf(baseMonth, baseAmount, yearApplied, filters) { + var year = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : '2026'; + var data = []; + var customers = [ + '嘉兴古道物流有限公司', '杭州张建丽信息咨询有限公司', '嘉兴益顺冷链物流有限公司', + '嘉兴港区众通快递有限公司', '嘉兴港区韵达快递有限公司新仓分理部', '嘉兴港区韵达快递有限公司' + ]; + + var targetMonths = []; + if (filters && filters.month) { + targetMonths = [filters.month.month() + 1]; + } else if (baseMonth <= 12) { + targetMonths = [baseMonth]; + } else { + targetMonths = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; + } + + var targetCustomer = filters && filters.customerName; + + targetMonths.forEach(function(m) { + var monthLabel = year + '年' + m + '月'; + var monthCustomers = targetCustomer ? [targetCustomer] : customers; + + var monthData = []; + monthCustomers.forEach(function(c) { + var count = 50 + Math.floor(Math.random() * 50); + var amountKg = count * 1.5 + Math.random() * 10; + var totalAmount = amountKg * 30; + + monthData.push({ + month: m, + monthLabel: monthLabel, + customerName: c, + count: count, + amountKg: Math.round(amountKg * 100) / 100, + totalAmount: Math.round(totalAmount * 100) / 100 + }); + }); + + if (targetMonths.length === 1 && !targetCustomer && baseAmount) { + var currentSum = monthData.reduce(function(sum, r) { return sum + r.totalAmount; }, 0); + if (currentSum > 0) { + var ratio = numOrZero(baseAmount) / currentSum; + monthData.forEach(function(r) { + r.totalAmount = Math.round(r.totalAmount * ratio * 100) / 100; + r.amountKg = Math.round(r.amountKg * ratio * 100) / 100; + }); + } + } + + if (monthData.length > 0) { + monthData[0].monthSpan = monthData.length; + for (var i = 1; i < monthData.length; i++) { + monthData[i].monthSpan = 0; + } + } + + data = data.concat(monthData); + }); + + data.forEach(function(item, index) { + item.key = 'h2-perf-' + index; + }); + + return { + rows: data, + options: { + customers: customers.map(function(c) { return { label: c, value: c }; }) + } + }; + } + + var HYDROGEN_STATION_NAMES = [ + '嘉兴港区羚牛加氢站', + '杭州余杭加氢示范站', + '宁波北仑港加氢站', + '上海临港加氢站', + '绍兴滨海新区加氢站' + ]; + + function hashStr(s) { + var h = 0; + for (var hi = 0; hi < s.length; hi++) { + h = ((h << 5) - h) + s.charCodeAt(hi); + h |= 0; + } + return Math.abs(h); + } + + function splitCustomerRowToStations(stationNames, totalCount, totalKg, totalAmount, seedStr) { + var n = stationNames.length; + if (n === 0) return []; + var tc = Math.round(numOrZero(totalCount)); + var tkg = numOrZero(totalKg); + var tm = numOrZero(totalAmount); + if (tc === 0 && tkg === 0 && tm === 0) return []; + + var weights = []; + var sumW = 0; + for (var wi = 0; wi < n; wi++) { + var w = 0.32 + ((hashStr(seedStr + '|st|' + wi) % 680) / 1000); + weights.push(w); + sumW += w; + } + var counts = []; + var acc = 0; + for (var ci = 0; ci < n; ci++) { + if (ci === n - 1) { + counts.push(Math.max(0, tc - acc)); + } else if (tc > 0) { + var c0 = Math.max(0, Math.floor(tc * (weights[ci] / sumW))); + counts.push(c0); + acc += c0; + } else { + counts.push(0); + } + } + var rows = []; + var remK = tkg; + var remM = tm; + for (var ki = 0; ki < n; ki++) { + var ratio = tc > 0 ? (counts[ki] / tc) : (weights[ki] / sumW); + var isLast = ki === n - 1; + var kg = isLast ? remK : Math.round(tkg * ratio * 100) / 100; + var money = isLast ? remM : Math.round(tm * ratio * 100) / 100; + if (!isLast) { + remK = Math.round((remK - kg) * 100) / 100; + remM = Math.round((remM - money) * 100) / 100; + } + rows.push({ + stationName: stationNames[ki], + count: counts[ki], + amountKg: kg, + totalAmount: money + }); + } + return rows; + } + + function mockHydrogenStationBreakdown(customerRows, drillMode, drillCustomerName) { + var filtered = (customerRows || []).filter(function (r) { + if (drillMode === 'customer' && r.customerName !== drillCustomerName) return false; + return true; + }); + if (filtered.length === 0) { + return { + rows: [], + monthLabel: '—', + drillScopeLabel: drillMode === 'customer' ? (drillCustomerName || '—') : '全部客户' + }; + } + var stationOrder = {}; + HYDROGEN_STATION_NAMES.forEach(function (sn, ord) { + stationOrder[sn] = ord; + }); + var rawPairs = []; + filtered.forEach(function (r) { + var parts = splitCustomerRowToStations( + HYDROGEN_STATION_NAMES, + r.count, + r.amountKg, + r.totalAmount, + r.customerName + '|' + r.monthLabel + ); + parts.forEach(function (p) { + if (numOrZero(p.count) === 0 && numOrZero(p.amountKg) === 0 && numOrZero(p.totalAmount) === 0) return; + rawPairs.push({ + monthLabel: r.monthLabel, + customerName: r.customerName, + stationName: p.stationName, + count: p.count, + amountKg: Math.round(numOrZero(p.amountKg) * 100) / 100, + totalAmount: Math.round(numOrZero(p.totalAmount) * 100) / 100 + }); + }); + }); + rawPairs.sort(function (a, b) { + var m = String(a.monthLabel).localeCompare(String(b.monthLabel), 'zh'); + if (m !== 0) return m; + var c = String(a.customerName).localeCompare(String(b.customerName), 'zh'); + if (c !== 0) return c; + return (stationOrder[a.stationName] || 0) - (stationOrder[b.stationName] || 0); + }); + var n = rawPairs.length; + var i = 0; + while (i < n) { + var ml = rawPairs[i].monthLabel; + var j = i + 1; + while (j < n && rawPairs[j].monthLabel === ml) j++; + var mlen = j - i; + var p = i; + while (p < j) { + var cn = rawPairs[p].customerName; + var q = p + 1; + while (q < j && rawPairs[q].customerName === cn) q++; + var clen = q - p; + for (var t = p; t < q; t++) { + rawPairs[t].monthSpan = t === i ? mlen : 0; + rawPairs[t].customerSpan = t === p ? clen : 0; + } + p = q; + } + i = j; + } + var dataRows = rawPairs.map(function (row, idx) { + return { + key: 'h2-sta-' + idx + '-' + String(hashStr(row.customerName + '|' + row.stationName)), + monthLabel: row.monthLabel, + customerName: row.customerName, + stationName: row.stationName, + count: row.count, + amountKg: row.amountKg, + totalAmount: row.totalAmount, + monthSpan: row.monthSpan, + customerSpan: row.customerSpan + }; + }); + return { + rows: dataRows, + monthLabel: filtered[0].monthLabel, + drillScopeLabel: drillMode === 'customer' ? drillCustomerName : '全部客户' + }; + } + + function mockHydrogenRefuelDetailRows(ctx, yearApplied, hydroFilterApplied, salesModal) { + var customerName = ctx && ctx.customerName; + var stationFilter = ctx && ctx.stationName; + var year = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : '2026'; + var monthNum = 1; + if (hydroFilterApplied && hydroFilterApplied.month && hydroFilterApplied.month.format) { + year = hydroFilterApplied.month.format('YYYY'); + monthNum = hydroFilterApplied.month.month() + 1; + } else if (salesModal && salesModal.month != null && salesModal.month <= 12) { + monthNum = salesModal.month; + } + if (!customerName) { + return { rows: [] }; + } + var daysInMonth = new Date(Number(year), monthNum, 0).getDate(); + if (daysInMonth < 1) daysInMonth = 28; + var stationsPool = HYDROGEN_STATION_NAMES.concat(['嘉兴嘉锦亭桥北综合供能服务站']); + var projects = ['宁波港49T*5租赁', '嘉兴大森租赁49T*3', '市区配送项目', '干线运输项目']; + var plates = ['沪A68122F', '粤AGP3513', '浙A12345F', '沪A69864F', '苏E88888F']; + var salespeople = ['刘念念', '尚建华', '谯云', '董剑煜']; + var n = 10 + (hashStr(String(customerName) + '|' + String(stationFilter || '')) % 18); + var rows = []; + for (var i = 0; i < n; i++) { + var day = 1 + (hashStr(String(customerName) + '|d|' + i) % daysInMonth); + var stName = stationFilter + ? stationFilter + : stationsPool[(hashStr(String(customerName) + '|s|' + i) % stationsPool.length)]; + var amountKg = Math.round((8 + (hashStr(String(customerName) + '|kg|' + i) % 190) / 10) * 100) / 100; + var unitPrice = 30; + var fee = Math.round(amountKg * unitPrice * 100) / 100; + rows.push({ + key: 'h2-det-' + i + '-' + hashStr(String(customerName) + stName + i), + refuelDateLabel: year + '年' + monthNum + '月' + day + '日', + customerName: customerName, + projectName: projects[hashStr(String(customerName) + '|p|' + i) % projects.length], + plateNo: plates[hashStr(String(customerName) + '|pl|' + i) % plates.length], + stationName: stName, + amountKg: amountKg, + unitPrice: unitPrice, + customerFee: fee, + salesperson: salespeople[hashStr(String(customerName) + '|sp|' + i) % salespeople.length] + }); + } + rows.sort(function (a, b) { + var da = a.refuelDateLabel; + var db = b.refuelDateLabel; + if (da !== db) return String(da).localeCompare(String(db), 'zh'); + return String(a.stationName).localeCompare(String(b.stationName), 'zh'); + }); + return { rows: rows }; + } + + function mockCustomerSummary(month, amount, deptApplied, filterApplied, isCost, overrides) { + var data = [ + { dept: '业务二部', salesperson: '刘念念', customerName: '安徽驰远供应链管理有限公司', receivable: 10000, received: 8000, unreceived: 2000, vehicleCost: 5000, brokerageFee: 1000, totalCost: 6000 }, + { dept: '业务二部', salesperson: '刘念念', customerName: '嘉兴港区韵达快递有限公司', receivable: 5000, received: 5000, unreceived: 0, vehicleCost: 2500, brokerageFee: 500, totalCost: 3000 }, + { dept: '业务二部', salesperson: '谯云', customerName: '无锡双庙运输有限公司', receivable: 8000, received: 0, unreceived: 8000, vehicleCost: 4000, brokerageFee: 800, totalCost: 4800 }, + { dept: '业务二部', salesperson: '尚建华', customerName: '新疆佳淇信息科技有限公司', receivable: 12000, received: 10000, unreceived: 2000, vehicleCost: 6000, brokerageFee: 1200, totalCost: 7200 }, + { dept: '业务二部', salesperson: '尚建华', customerName: '嘉兴益顺冷链物流有限公司', receivable: 6000, received: 6000, unreceived: 0, vehicleCost: 3000, brokerageFee: 600, totalCost: 3600 }, + { dept: '业务二部', salesperson: '尚建华', customerName: '新疆中铁黑豹物流有限公司', receivable: 9000, received: 4000, unreceived: 5000, vehicleCost: 4500, brokerageFee: 900, totalCost: 5400 }, + { dept: '业务二部', salesperson: '董剑煜', customerName: '嘉兴必出彩供应链有限公司', receivable: 7000, received: 7000, unreceived: 0, vehicleCost: 3500, brokerageFee: 700, totalCost: 4200 } + ]; + + var currentSum = 0; + data.forEach(function(r) { currentSum += isCost ? r.totalCost : r.receivable; }); + var targetTotal = numOrZero(amount); + + if (targetTotal > 0 && currentSum > 0) { + var ratio = targetTotal / currentSum; + data.forEach(function(r) { + if (isCost) { + r.vehicleCost = Math.round(r.vehicleCost * ratio * 100) / 100; + r.baseBrokerageFee = Math.round(r.brokerageFee * ratio * 100) / 100; + r.brokerageFee = r.baseBrokerageFee; + r.totalCost = Math.round((r.vehicleCost + r.brokerageFee) * 100) / 100; + } else { + r.receivable = Math.round(r.receivable * ratio * 100) / 100; + r.received = Math.round(r.received * ratio * 100) / 100; + r.unreceived = Math.round((r.receivable - r.received) * 100) / 100; + } + }); + } else { + data.forEach(function(r) { + if (isCost) { + r.baseBrokerageFee = r.brokerageFee; + } + }); + } + + if (isCost && overrides) { + data.forEach(function(r) { + var p1BaseBF = Math.round(r.baseBrokerageFee * 0.4 * 100) / 100; + var p2BaseBF = r.baseBrokerageFee - p1BaseBF; + + var sum = 0; + var plates = ['沪A34641F', '沪A69864F', '沪A89546F', '沪A99357F']; + var projects = [ + { name: '市区配送项目', base: p1BaseBF }, + { name: '干线运输项目', base: p2BaseBF } + ]; + + projects.forEach(function(proj) { + var projHasOverride = false; + var projSum = 0; + for (var i = 0; i < plates.length; i++) { + var key = r.customerName + '|' + proj.name + '|' + plates[i]; + if (overrides[key] !== undefined) { + projHasOverride = true; + projSum += numOrZero(overrides[key]); + } else { + var count = 4; + var plateRatio = (i === count - 1) ? 1 - (Math.floor(100/count)/100)*(count-1) : Math.floor(100/count)/100; + projSum += Math.round(proj.base * plateRatio * 100) / 100; + } + } + sum += projHasOverride ? projSum : proj.base; + }); + + r.brokerageFee = sum; + r.totalCost = Math.round((r.vehicleCost + r.brokerageFee) * 100) / 100; + }); + } + + var uniqueCustomers = []; + data.forEach(function(r) { + if (uniqueCustomers.indexOf(r.customerName) === -1) uniqueCustomers.push(r.customerName); + }); + var options = { + customers: uniqueCustomers.map(function(v) { return { label: v, value: v }; }) + }; + + var filteredData = data.filter(function(r) { + if (filterApplied && filterApplied.customerName && r.customerName !== filterApplied.customerName) return false; + return true; + }); + + var deptSpan = {}; + var spSpan = {}; + + filteredData.forEach(function(item, index) { + if (isCost) { + var p1Key = item.customerName + '|市区配送项目'; + var p2Key = item.customerName + '|干线运输项目'; + var p1BF = Math.round(item.baseBrokerageFee * 0.4 * 100) / 100; + var p2BF = item.baseBrokerageFee - p1BF; + + if (overrides) { + if (overrides[p1Key] !== undefined) p1BF = overrides[p1Key]; + if (overrides[p2Key] !== undefined) p2BF = overrides[p2Key]; + } + + item.brokerageFee = p1BF + p2BF; + item.totalCost = item.vehicleCost + item.brokerageFee; + } + + if (index === 0 || filteredData[index - 1].dept !== item.dept) { + var count = 0; + for (var i = index; i < filteredData.length; i++) { + if (filteredData[i].dept === item.dept) count++; + else break; + } + deptSpan[index] = count; + } else { + deptSpan[index] = 0; + } + + if (index === 0 || filteredData[index - 1].salesperson !== item.salesperson) { + var count = 0; + for (var i = index; i < filteredData.length; i++) { + if (filteredData[i].salesperson === item.salesperson) count++; + else break; + } + spSpan[index] = count; + } else { + spSpan[index] = 0; + } + + item.deptSpan = deptSpan[index]; + item.spSpan = spSpan[index]; + item.key = 'cust-sum-' + index; + }); + + return { rows: filteredData, options: options }; + } + + function mockProjectSummary(customerRecord, isCost, overrides) { + function getProjectBrokerage(cName, pName, baseBF) { + if (!isCost || !overrides) return baseBF; + var plates = ['沪A34641F', '沪A69864F', '沪A89546F', '沪A99357F']; + var hasOverride = false; + var sum = 0; + for (var i = 0; i < plates.length; i++) { + var key = cName + '|' + pName + '|' + plates[i]; + if (overrides[key] !== undefined) { + hasOverride = true; + sum += numOrZero(overrides[key]); + } else { + var count = 4; + var ratio = (i === count - 1) ? 1 - (Math.floor(100/count)/100)*(count-1) : Math.floor(100/count)/100; + sum += Math.round(baseBF * ratio * 100) / 100; + } + } + return hasOverride ? sum : baseBF; + } + + function generateProjects(cRecord) { + var targetReceivable = numOrZero(cRecord.receivable); + var targetVehicleCost = numOrZero(cRecord.vehicleCost); + var baseBrokerageFee = numOrZero(cRecord.baseBrokerageFee || cRecord.brokerageFee); + + var p1VC = Math.round(targetVehicleCost * 0.4 * 100) / 100; + var p2VC = targetVehicleCost - p1VC; + + var p1BaseBF = Math.round(baseBrokerageFee * 0.4 * 100) / 100; + var p2BaseBF = baseBrokerageFee - p1BaseBF; + + var p1BF = getProjectBrokerage(cRecord.customerName, '市区配送项目', p1BaseBF); + var p2BF = getProjectBrokerage(cRecord.customerName, '干线运输项目', p2BaseBF); + + var p1Rec = Math.round(targetReceivable * 0.4 * 100) / 100; + var p2Rec = targetReceivable - p1Rec; + + return [ + { + customerName: cRecord.customerName, + salesperson: cRecord.salesperson, + projectName: '市区配送项目', + receivable: p1Rec, + received: Math.round(p1Rec * 0.8 * 100) / 100, + unreceived: Math.round(p1Rec * 0.2 * 100) / 100, + vehicleCost: p1VC, + brokerageFee: p1BF, + totalCost: Math.round((p1VC + p1BF) * 100) / 100 + }, + { + customerName: cRecord.customerName, + salesperson: cRecord.salesperson, + projectName: '干线运输项目', + receivable: p2Rec, + received: Math.round(p2Rec * 0.5 * 100) / 100, + unreceived: Math.round(p2Rec * 0.5 * 100) / 100, + vehicleCost: p2VC, + brokerageFee: p2BF, + totalCost: Math.round((p2VC + p2BF) * 100) / 100 + } + ]; + } + + if (customerRecord.isAll) { + var customerMap = {}; + customerRecord.sourceRows.forEach(function(r) { + if (!customerMap[r.customerName]) { + customerMap[r.customerName] = { + customerName: r.customerName, + salesperson: r.salesperson, + receivable: 0, received: 0, unreceived: 0, + vehicleCost: 0, brokerageFee: 0, baseBrokerageFee: 0, totalCost: 0 + }; + } + var c = customerMap[r.customerName]; + c.receivable += numOrZero(r.receivable); + c.received += numOrZero(r.received); + c.unreceived += numOrZero(r.unreceived); + c.vehicleCost += numOrZero(r.vehicleCost); + c.brokerageFee += numOrZero(r.brokerageFee); + c.baseBrokerageFee += numOrZero(r.baseBrokerageFee || r.brokerageFee); + c.totalCost += numOrZero(r.totalCost); + }); + + var allData = []; + Object.keys(customerMap).forEach(function(cName) { + allData = allData.concat(generateProjects(customerMap[cName])); + }); + + var currentCust = null; + var currentCustStartIndex = -1; + allData.forEach(function(r, idx) { + r.key = 'proj-sum-all-' + idx; + if (r.customerName !== currentCust) { + if (currentCustStartIndex !== -1) { + allData[currentCustStartIndex].custSpan = idx - currentCustStartIndex; + } + currentCust = r.customerName; + currentCustStartIndex = idx; + r.custSpan = 1; + } else { + r.custSpan = 0; + } + }); + if (currentCustStartIndex !== -1) { + allData[currentCustStartIndex].custSpan = allData.length - currentCustStartIndex; + } + return allData; + } else { + var data = generateProjects(customerRecord); + data.forEach(function(item, index) { + item.custSpan = index === 0 ? data.length : 0; + item.key = 'proj-sum-' + index; + }); + return data; + } + } + + function mockLeaseOrderDrill(month, amount, deptApplied, deptOptions, yearApplied, drillFilterApplied, detailProject, isCost, overrides) { + var year = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : '2026'; + var billCycle; + if (month === 13) { + billCycle = year + '.01.01-' + year + '.12.31'; + } else { + var mStr = pad2(month); + var daysInMonth = new Date(year, month, 0).getDate(); + billCycle = year + '.' + mStr + '.01-' + year + '.' + mStr + '.' + daysInMonth; + } + + var rows = []; + var plates = ['沪A34641F', '沪A69864F', '沪A89546F', '沪A99357F']; + var count = 4; + + function generateRowsForProject(proj, idxOffset) { + var targetTotal = proj ? numOrZero(isCost ? proj.totalCost : proj.receivable) : numOrZero(amount); + var targetVehicleCost = proj ? numOrZero(proj.vehicleCost) : targetTotal * 0.9; + var targetBrokerageFee = proj ? numOrZero(proj.baseBrokerageFee || proj.brokerageFee) : targetTotal * 0.1; + + for (var i = 0; i < count; i++) { + var ratio = (i === count - 1) ? 1 - (Math.floor(100/count)/100)*(count-1) : Math.floor(100/count)/100; + var rReceivable = Math.round(targetTotal * ratio * 100) / 100; + var rVehicleCost = Math.round(targetVehicleCost * ratio * 100) / 100; + var rBrokerageFee = Math.round(targetBrokerageFee * ratio * 100) / 100; + + var plateNo = plates[i] || ('沪A' + Math.floor(10000 + Math.random() * 90000) + 'F'); + var custName = proj ? proj.customerName : '测试客户' + (i + 1); + var projName = proj ? proj.projectName : '测试项目' + (i + 1); + var orderKey = custName + '|' + projName + '|' + plateNo; + if (isCost && overrides && overrides[orderKey] !== undefined) { + rBrokerageFee = numOrZero(overrides[orderKey]); + } + + rows.push({ + key: 'drill-' + idxOffset + '-' + i, + billCycle: billCycle, + billInstallment: '第' + (i+1) + '期/共12期', + plateNo: plateNo, + brand: '苏龙', + vehicleModel: '海格牌18吨双飞翼货车', + salesperson: proj ? proj.salesperson : '业务员' + (i + 1), + nature: i % 2 === 0 ? '租赁' : '试用', + customerName: custName, + projectName: projName, + contractDate: '2025.12.1-2026.11.30', + pickupDate: '2026.01.01', + deposit: '/', + receivable: rReceivable, + received: null, + unreceived: rReceivable, + naturalMonthIncome: rReceivable, + vehicleCost: rVehicleCost, + brokerageFee: rBrokerageFee, + totalCost: Math.round((rVehicleCost + rBrokerageFee) * 100) / 100, + paymentDate: '2026.01.01', + payMethod: '预付', + payCycle: '1个月' + }); + } + } + + if (detailProject && detailProject.isAll) { + detailProject.sourceRows.forEach(function(proj, idx) { + generateRowsForProject(proj, idx); + }); + } else { + generateRowsForProject(detailProject, 0); + } + + var uniqueSalespersons = []; + var uniqueCustomers = []; + var uniquePlateNos = []; + rows.forEach(function(r) { + if (uniqueSalespersons.indexOf(r.salesperson) === -1) uniqueSalespersons.push(r.salesperson); + if (uniqueCustomers.indexOf(r.customerName) === -1) uniqueCustomers.push(r.customerName); + if (uniquePlateNos.indexOf(r.plateNo) === -1) uniquePlateNos.push(r.plateNo); + }); + var options = { + salespersons: uniqueSalespersons.map(function(v) { return { label: v, value: v }; }), + customers: uniqueCustomers.map(function(v) { return { label: v, value: v }; }), + plateNos: uniquePlateNos.map(function(v) { return { label: v, value: v }; }) + }; + + var sumReceivable = 0, sumReceived = 0, sumUnreceived = 0, sumNatural = 0, sumVehicleCost = 0, sumBrokerageFee = 0, sumTotalCost = 0; + + // 应用筛选条件 + var filteredRows = rows.filter(function(r) { + if (drillFilterApplied.salesperson && r.salesperson !== drillFilterApplied.salesperson) return false; + if (drillFilterApplied.customerName && r.customerName !== drillFilterApplied.customerName) return false; + if (drillFilterApplied.nature && r.nature !== drillFilterApplied.nature) return false; + if (drillFilterApplied.plateNo && r.plateNo !== drillFilterApplied.plateNo) return false; + if (drillFilterApplied.paymentDate && drillFilterApplied.paymentDate.length === 2) { + var start = drillFilterApplied.paymentDate[0].format('YYYY.MM.DD'); + var end = drillFilterApplied.paymentDate[1].format('YYYY.MM.DD'); + if (r.paymentDate < start || r.paymentDate > end) return false; + } + return true; + }); + + filteredRows.forEach(function(r) { + sumReceivable += r.receivable; + sumReceived += numOrZero(r.received); + sumUnreceived += r.unreceived; + sumNatural += r.naturalMonthIncome; + sumVehicleCost += numOrZero(r.vehicleCost); + sumBrokerageFee += numOrZero(r.brokerageFee); + sumTotalCost += numOrZero(r.totalCost); + }); + + return { + rows: filteredRows, + options: options, + totals: { + receivable: sumReceivable, + received: sumReceived, + unreceived: sumUnreceived, + naturalMonthIncome: sumNatural, + vehicleCost: sumVehicleCost, + brokerageFee: sumBrokerageFee, + totalCost: sumTotalCost + } + }; + } + + function mockLeaseOrderDetail(ym, deptName, salesperson, amount) { + var totalAmount = numOrZero(amount); + var rows = []; + var isAll = deptName === '全部'; + var depts = ['业务二部', '华东业务部', '华南业务部']; + var names = ['尚建华', '刘念念', '谯云']; + var n = isAll ? 6 : 3; + var sumDeposit = 0, sumVehicle = 0, sumAdditional = 0, sumTotal = 0, sumReceived = 0, sumUnreceived = 0; + + for (var i = 0; i < n; i++) { + var ratio = (i === n - 1) ? 1 - (Math.floor(100/n)/100)*(n-1) : Math.floor(100/n)/100; + var rTotal = Math.round(totalAmount * ratio * 100) / 100; + var rDeposit = Math.round(rTotal * 0.1 * 100) / 100; + var rVehicle = Math.round(rTotal * 0.8 * 100) / 100; + var rAdditional = Math.round((rTotal - rDeposit - rVehicle) * 100) / 100; + var rReceived = Math.round(rTotal * 0.6 * 100) / 100; + var rUnreceived = Math.round((rTotal - rReceived) * 100) / 100; + + var curDept = isAll ? depts[Math.floor(i / 2) % depts.length] : deptName; + var curName = isAll ? names[Math.floor(i / 2) % names.length] : salesperson; + + // For "全部", we span 2 rows for each person. For specific person, we span n rows for the first one. + var rowSpan = 0; + if (isAll) { + rowSpan = (i % 2 === 0) ? 2 : 0; + } else { + rowSpan = (i === 0) ? n : 0; + } + + rows.push({ + key: 'detail-' + i, + ym: ym, + deptName: curDept, + salesperson: curName, + billDate: ym + '-15', + plateNo: '浙A' + (10000 + i), + vehicleModel: 'XL-100', + customerName: '测试客户' + (i + 1), + contractDate: '2026-01-01至2027-01-01', + pickupDate: '2026-01-05', + payMethod: i % 2 === 0 ? '预付' : '后付', + payCycle: '1个月', + billCalcMethod: '自然月', + actualDays: '30天', + depositAmount: rDeposit, + vehicleAmount: rVehicle, + additionalAmount: rAdditional, + totalAmount: rTotal, + receivedAmount: rReceived, + unreceivedAmount: rUnreceived, + groupRowSpan: rowSpan, + monthRowSpan: i === 0 ? n : 0 + }); + sumDeposit += rDeposit; + sumVehicle += rVehicle; + sumAdditional += rAdditional; + sumTotal += rTotal; + sumReceived += rReceived; + sumUnreceived += rUnreceived; + } + return { + rows: rows, + totals: { + deposit: sumDeposit, + vehicle: sumVehicle, + additional: sumAdditional, + total: sumTotal, + received: sumReceived, + unreceived: sumUnreceived + } + }; + } + + /** 计算单行横向合计:业绩合计 / 成本合计 / 利润合计 */ + function calcRowTotals(r) { + var metrics = ['Perf', 'Cost', 'Profit']; + metrics.forEach(function (m) { + var rowSum = ALL_DATA_KEYS.reduce(function (acc, k) { return acc + numOrZero(r[k + m]); }, 0); + r['rowTotal' + m] = rowSum === 0 ? null : rowSum; + }); + return r; + } + + /** 纵向(全年)合计行 */ + function sumLedgerRows(monthRows) { + var acc = { month: 13, monthLabel: '合计', rowType: 'total', key: 'total' }; + var metrics = ['Perf', 'Cost', 'Profit']; + var allKeys = ALL_DATA_KEYS.concat(['rowTotal']); + + allKeys.forEach(function (k) { + metrics.forEach(function (m) { + acc[k + m] = 0; + }); + }); + + (monthRows || []).forEach(function (r) { + allKeys.forEach(function (k) { + metrics.forEach(function (m) { + acc[k + m] += numOrZero(r[k + m]); + }); + }); + }); + + allKeys.forEach(function (k) { + metrics.forEach(function (m) { + if (acc[k + m] === 0) acc[k + m] = null; + }); + }); + return acc; + } + + function buildMockYear2026() { + var rows = []; + var i; + for (i = 1; i <= 12; i++) { + var logisticsPerf = 800000 + Math.random() * 400000; + var logisticsCost = logisticsPerf * (0.9 + Math.random() * 0.2); + + var leasePerf = 200000 + Math.random() * 100000; + var leaseCost = leasePerf * (0.8 + Math.random() * 0.15); + + var salesPerf = 40000 + Math.random() * 60000; + var salesCost = salesPerf * (0.7 + Math.random() * 0.2); + + var hydrogenPerf = 100000 + Math.random() * 50000; + var hydrogenCost = hydrogenPerf * (0.85 + Math.random() * 0.1); + + var elecPerf = 3000 + Math.random() * 2000; + var elecCost = elecPerf * (0.4 + Math.random() * 0.3); + + var etcPerf = 60000 + Math.random() * 30000; + var etcCost = etcPerf; + + var otherPerf = 5000 + Math.random() * 10000; + var otherCost = otherPerf * (0.6 + Math.random() * 0.3); + + var src = { + logisticsPerf: Math.round(logisticsPerf * 100) / 100, + logisticsCost: Math.round(logisticsCost * 100) / 100, + logisticsProfit: Math.round((logisticsPerf - logisticsCost) * 100) / 100, + + leasePerf: Math.round(leasePerf * 100) / 100, + leaseCost: Math.round(leaseCost * 100) / 100, + leaseProfit: Math.round((leasePerf - leaseCost) * 100) / 100, + + salesPerf: Math.round(salesPerf * 100) / 100, + salesCost: Math.round(salesCost * 100) / 100, + salesProfit: Math.round((salesPerf - salesCost) * 100) / 100, + + hydrogenPerf: Math.round(hydrogenPerf * 100) / 100, + hydrogenCost: Math.round(hydrogenCost * 100) / 100, + hydrogenProfit: Math.round((hydrogenPerf - hydrogenCost) * 100) / 100, + + elecPerf: Math.round(elecPerf * 100) / 100, + elecCost: Math.round(elecCost * 100) / 100, + elecProfit: Math.round((elecPerf - elecCost) * 100) / 100, + + etcPerf: Math.round(etcPerf * 100) / 100, + etcCost: Math.round(etcCost * 100) / 100, + etcProfit: 0, + + otherPerf: Math.round(otherPerf * 100) / 100, + otherCost: Math.round(otherCost * 100) / 100, + otherProfit: Math.round((otherPerf - otherCost) * 100) / 100 + }; + + var row = { key: 'm' + i, month: i, monthLabel: i, rowType: 'month' }; + ALL_DATA_KEYS.forEach(function (k) { + ['Perf', 'Cost', 'Profit'].forEach(function (m) { + var v = src[k + m]; + row[k + m] = v !== undefined && v !== null ? v : null; + }); + }); + calcRowTotals(row); + rows.push(row); + } + rows.push(sumLedgerRows(rows)); + return rows; + } + + var layoutStyle = { + padding: '16px 24px 24px', + minHeight: '100vh', + background: 'linear-gradient(165deg, #eef4ff 0%, #f5f7fa 42%, #f0f2f5 100%)' + }; + var filterLabelStyle = { marginBottom: 6, fontSize: 13, color: 'rgba(0,0,0,0.55)', fontWeight: 500 }; + var filterItemStyle = { marginBottom: 12 }; + var filterControlStyle = { width: '100%' }; + var filterActionsColStyle = { flex: '0 0 auto', marginLeft: 'auto' }; + + var filterCardStyle = { + marginBottom: 20, + borderRadius: 16, + boxShadow: '0 4px 20px -4px rgba(16,24,40,0.03), 0 0 0 1px rgba(16,24,40,0.06)', + border: 'none', + background: '#ffffff' + }; + + var tableCardStyle = { + borderRadius: 16, + boxShadow: '0 10px 32px -4px rgba(16,24,40,0.06), 0 0 0 1px rgba(16,24,40,0.04)', + border: 'none', + background: '#ffffff', + overflow: 'hidden' + }; + + var ledgerTableStyle = + '.biz-standbook-table-wrap{border-radius:12px;overflow:hidden;box-shadow:0 4px 24px -6px rgba(15,23,42,0.05),0 0 0 1px rgba(22,119,255,0.1)}' + + '.biz-standbook-table .ant-table-thead>tr>th{white-space:nowrap;color:#1e293b!important;font-weight:600!important;font-size:13px!important;' + + 'background:#f8fafc!important;border-bottom:1px solid #e2e8f0!important;border-inline-end:1px solid #f1f5f9!important;padding:0 8px!important;height:38px!important;transition:background 0.2s}' + + '.biz-standbook-table .ant-table-thead>tr:first-child>th{text-align:center;background:#f1f5f9!important;color:#0f172a!important;font-size:14px!important;border-bottom:2px solid #e2e8f0!important}' + + '.biz-standbook-table .ant-table-tbody>tr:not(.ant-table-measure-row)>td{white-space:nowrap;font-variant-numeric:tabular-nums;color:#334155;border-bottom:1px solid #f1f5f9!important;border-inline-end:1px solid #f8fafc!important;padding:0 8px!important;height:38px!important}' + + '.biz-standbook-table .ant-table-tbody>tr.biz-row-month:hover>td{background:#f0f9ff!important;color:#0f172a}' + + '.biz-standbook-table .ant-table-tbody>tr[data-row-key=\"total\"]>td{font-weight:700;background:#f8fafc!important;color:#0f172a!important;border-top:2px solid #cbd5e1!important;border-bottom:none!important}' + + '.biz-standbook-table .ant-table-summary>tr>td{font-weight:700;background:#f8fafc!important;color:#0f172a!important;border-top:2px solid #cbd5e1!important;border-bottom:none!important;border-inline-end:1px solid #f1f5f9!important;padding:0 8px!important;height:38px!important}' + + '.biz-standbook-perf-link{cursor:pointer;color:#0ea5e9;padding:4px 8px;margin:-4px -8px;border:none;background:transparent;font:inherit;border-radius:6px;transition:all 0.2s}' + + '.biz-standbook-perf-link:hover{color:#0284c7;background:#e0f2fe}' + + '.biz-standbook-perf-link:focus{outline:2px solid #38bdf8;outline-offset:2px}' + + '@media (prefers-reduced-motion:reduce){.biz-standbook-table .ant-table-tbody>tr,.biz-standbook-perf-link{transition:none}}'; + + var deptOptions = useMemo(function () { + return [ + { value: '业务二部', label: '业务二部' }, + { value: '华东业务部', label: '华东业务部' }, + { value: '华南业务部', label: '华南业务部' }, + { value: '华北业务部', label: '华北业务部' }, + { value: '西南业务部', label: '西南业务部' } + ]; + }, []); + + var yearDraftState = useState(initialYear); + var yearDraft = yearDraftState[0]; + var setYearDraft = yearDraftState[1]; + + var yearAppliedState = useState(initialYear); + var yearApplied = yearAppliedState[0]; + var setYearApplied = yearAppliedState[1]; + + /** 业务部多选:空数组表示「全部」 */ + var deptDraftState = useState([]); + var deptDraft = deptDraftState[0]; + var setDeptDraft = deptDraftState[1]; + + var deptAppliedState = useState([]); + var deptApplied = deptAppliedState[0]; + var setDeptApplied = deptAppliedState[1]; + + var metricState = useState('Perf'); + var metricType = metricState[0]; + var setMetricType = metricState[1]; + + var viewState = useState('main'); + var view = viewState[0]; + var setView = viewState[1]; + + var salesModalState = useState({ month: null, monthLabel: '', amount: null, cellType: 'Perf' }); + var salesModal = salesModalState[0]; + var setSalesModal = salesModalState[1]; + + var salesDetailModalState = useState({ ym: '', deptName: '', salesperson: '', amount: null }); + var salesDetailModal = salesDetailModalState[0]; + var setSalesDetailModal = salesDetailModalState[1]; + + var drillFilterDraftState = useState({ salesperson: undefined, customerName: undefined, nature: undefined, plateNo: undefined, paymentDate: null }); + var drillFilterDraft = drillFilterDraftState[0]; + var setDrillFilterDraft = drillFilterDraftState[1]; + + var drillFilterAppliedState = useState({ salesperson: undefined, customerName: undefined, nature: undefined, plateNo: undefined, paymentDate: null }); + var drillFilterApplied = drillFilterAppliedState[0]; + var setDrillFilterApplied = drillFilterAppliedState[1]; + + var hydroFilterDraftState = useState({ month: null, customerName: undefined }); + var hydroFilterDraft = hydroFilterDraftState[0]; + var setHydroFilterDraft = hydroFilterDraftState[1]; + + var hydroFilterAppliedState = useState({ month: null, customerName: undefined }); + var hydroFilterApplied = hydroFilterAppliedState[0]; + var setHydroFilterApplied = hydroFilterAppliedState[1]; + + var hydroStationDrillState = useState({ mode: 'all', customerName: null }); + var hydroStationDrill = hydroStationDrillState[0]; + var setHydroStationDrill = hydroStationDrillState[1]; + + var hydroRefuelDetailCtxState = useState({ customerName: '', stationName: null }); + var hydroRefuelDetailCtx = hydroRefuelDetailCtxState[0]; + var setHydroRefuelDetailCtx = hydroRefuelDetailCtxState[1]; + + var custSumFilterDraftState = useState({ customerName: undefined }); + var custSumFilterDraft = custSumFilterDraftState[0]; + var setCustSumFilterDraft = custSumFilterDraftState[1]; + + var custSumFilterAppliedState = useState({ customerName: undefined }); + var custSumFilterApplied = custSumFilterAppliedState[0]; + var setCustSumFilterApplied = custSumFilterAppliedState[1]; + + var detailCustomerState = useState(null); + var detailCustomer = detailCustomerState[0]; + var setDetailCustomer = detailCustomerState[1]; + + var detailProjectState = useState(null); + var detailProject = detailProjectState[0]; + var setDetailProject = detailProjectState[1]; + + var editingBrokerageState = useState(null); + var editingBrokerage = editingBrokerageState[0]; + var setEditingBrokerage = editingBrokerageState[1]; + + var brokerageFeeOverridesState = useState({}); + var brokerageFeeOverrides = brokerageFeeOverridesState[0]; + var setBrokerageFeeOverrides = brokerageFeeOverridesState[1]; + + var brokerageBatchPopoverOpenState = useState(false); + var brokerageBatchPopoverOpen = brokerageBatchPopoverOpenState[0]; + var setBrokerageBatchPopoverOpen = brokerageBatchPopoverOpenState[1]; + + var brokerageBatchInputState = useState(null); + var brokerageBatchInput = brokerageBatchInputState[0]; + var setBrokerageBatchInput = brokerageBatchInputState[1]; + + var dataSource = useMemo(function () { + var y = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : ''; + if (y === '2026') return buildMockYear2026(); + return []; + }, [yearApplied]); + + var tableTitle = useMemo(function () { + return '浙江羚牛氢能业务部汇总台账'; + }, []); + + var deptDisplayLabel = useMemo(function () { + if (!deptApplied || deptApplied.length === 0) return '全部'; + return deptApplied.map(function (v) { + var o = deptOptions.find(function (x) { return x.value === v; }); + return o ? o.label : v; + }).join('、'); + }, [deptApplied, deptOptions]); + + var timeDisplayLabel = useMemo(function () { + return yearApplied && yearApplied.format ? yearApplied.format('YYYY年') : '—'; + }, [yearApplied]); + + var handleQuery = useCallback(function () { + setYearApplied(yearDraft); + setDeptApplied(deptDraft); + }, [yearDraft, deptDraft]); + + var handleReset = useCallback(function () { + var y0 = initialYear(); + setYearDraft(y0); + setYearApplied(y0); + setDeptDraft([]); + setDeptApplied([]); + }, []); + + var handleDrillQuery = useCallback(function () { + setDrillFilterApplied(drillFilterDraft); + }, [drillFilterDraft]); + + var handleDrillReset = useCallback(function () { + var empty = { salesperson: undefined, customerName: undefined, nature: undefined, plateNo: undefined, paymentDate: null }; + setDrillFilterDraft(empty); + setDrillFilterApplied(empty); + }, []); + + var handleCustSumQuery = useCallback(function () { + setCustSumFilterApplied(custSumFilterDraft); + }, [custSumFilterDraft]); + + var handleCustSumReset = useCallback(function () { + var empty = { customerName: undefined }; + setCustSumFilterDraft(empty); + setCustSumFilterApplied(empty); + }, []); + + var openProjectSummary = useCallback(function (record) { + setDetailCustomer(record); + setView('projectSummary'); + }, []); + + var openProjectDetail = useCallback(function (record) { + setDetailProject(record); + setView('sales'); + }, []); + + var openHydrogenDrill = useCallback(function (row, amount, hydrogenAsCost) { + if (amount === null || amount === undefined || amount === '' || numOrZero(amount) === 0) { + message.warning('该单元格无数据'); + return; + } + setSalesModal({ + month: row.month, + monthLabel: row.monthLabel, + amount: amount, + cellType: hydrogenAsCost ? 'HydrogenCost' : 'HydrogenPerf' + }); + var initialMonth = row.month <= 12 && yearApplied && yearApplied.format ? dayjs(yearApplied.format('YYYY') + '-' + pad2(row.month) + '-01') : null; + var resetState = { month: initialMonth, customerName: undefined }; + setHydroFilterDraft(resetState); + setHydroFilterApplied(resetState); + setView('hydrogenCustomerPerf'); + }, [yearApplied]); + + var openHydroStationDrillByCustomer = useCallback(function (record) { + setHydroStationDrill({ mode: 'customer', customerName: record.customerName }); + setView('hydrogenStationDrill'); + }, []); + + var openHydroStationDrillAllStations = useCallback(function () { + setHydroStationDrill({ mode: 'all', customerName: null }); + setView('hydrogenStationDrill'); + }, []); + + var openHydroRefuelDetailByCustomer = useCallback(function (customerName) { + if (!customerName) return; + setHydroRefuelDetailCtx({ customerName: customerName, stationName: null }); + setView('hydrogenRefuelDetail'); + }, []); + + var openHydroRefuelDetailByStationRow = useCallback(function (record) { + if (!record || !record.customerName || !record.stationName) return; + setHydroRefuelDetailCtx({ customerName: record.customerName, stationName: record.stationName }); + setView('hydrogenRefuelDetail'); + }, []); + + var openLeaseDrill = useCallback(function (row, cellKey, amount) { + if (cellKey !== 'leasePerf' && cellKey !== 'leaseCost') return; + if (amount === null || amount === undefined || amount === '' || numOrZero(amount) === 0) { + message.warning('该单元格无数据'); + return; + } + setSalesModal({ + month: row.month, + monthLabel: row.monthLabel, + amount: amount, + cellType: cellKey === 'leasePerf' ? 'Perf' : 'Cost' + }); + setDetailCustomer(null); + setDetailProject(null); + setView('customerSummary'); + }, []); + + var openDetailDrill = useCallback(function (row, amount) { + setSalesDetailModal({ + ym: row.ym, + deptName: row.deptName, + salesperson: row.salesperson, + amount: amount + }); + setView('sales_detail'); + }, []); + + var drillPayload = useMemo(function () { + if (view !== 'sales' || salesModal.month == null) { + return { rows: [], options: { salespersons: [], customers: [], plateNos: [] }, totals: { receivable: 0, received: 0, unreceived: 0, naturalMonthIncome: 0, vehicleCost: 0, brokerageFee: 0, totalCost: 0 } }; + } + return mockLeaseOrderDrill(salesModal.month, salesModal.amount, deptApplied, deptOptions, yearApplied, drillFilterApplied, detailProject, salesModal.cellType === 'Cost', brokerageFeeOverrides); + }, [view, salesModal, deptApplied, deptOptions, yearApplied, drillFilterApplied, detailProject, brokerageFeeOverrides]); + + var applyBrokerageBatch = useCallback(function () { + if (brokerageBatchInput === null || brokerageBatchInput === undefined || brokerageBatchInput === '') { + message.warning('请填写居间费批量设置'); + return; + } + var num = Number(brokerageBatchInput); + if (isNaN(num)) { + message.warning('请输入有效数字'); + return; + } + var val = Math.round(num * 100) / 100; + if (view !== 'sales' || salesModal.cellType !== 'Cost') { + setBrokerageBatchPopoverOpen(false); + setBrokerageBatchInput(null); + return; + } + var rows = drillPayload.rows || []; + if (rows.length === 0) { + message.warning('当前无数据'); + return; + } + var next = Object.assign({}, brokerageFeeOverrides); + rows.forEach(function (r) { + var compositeKey = r.customerName + '|' + r.projectName + '|' + r.plateNo; + next[compositeKey] = val; + }); + setBrokerageFeeOverrides(next); + setEditingBrokerage(null); + setBrokerageBatchPopoverOpen(false); + setBrokerageBatchInput(null); + message.success('已批量设置居间费'); + }, [brokerageBatchInput, view, salesModal, drillPayload.rows, brokerageFeeOverrides]); + + var hydrogenCustomerPerfPayload = useMemo(function () { + if (view !== 'hydrogenCustomerPerf' || salesModal.month == null) { + return { rows: [], options: { customers: [] } }; + } + return mockHydrogenCustomerPerf(salesModal.month, salesModal.amount, yearApplied, hydroFilterApplied); + }, [view, salesModal, yearApplied, hydroFilterApplied]); + + var hydrogenStationDrillPayload = useMemo(function () { + if (view !== 'hydrogenStationDrill' || salesModal.month == null) { + return { rows: [], monthLabel: '—', drillScopeLabel: '全部客户' }; + } + var customerRows = mockHydrogenCustomerPerf(salesModal.month, salesModal.amount, yearApplied, hydroFilterApplied).rows; + return mockHydrogenStationBreakdown(customerRows, hydroStationDrill.mode, hydroStationDrill.customerName); + }, [view, salesModal, yearApplied, hydroFilterApplied, hydroStationDrill]); + + var hydrogenRefuelDetailPayload = useMemo(function () { + if (view !== 'hydrogenRefuelDetail' || salesModal.month == null) { + return { rows: [] }; + } + return mockHydrogenRefuelDetailRows(hydroRefuelDetailCtx, yearApplied, hydroFilterApplied, salesModal); + }, [view, salesModal, yearApplied, hydroFilterApplied, hydroRefuelDetailCtx]); + + var customerSummaryPayload = useMemo(function () { + if (view !== 'customerSummary' || salesModal.month == null) { + return { rows: [], options: { customers: [] } }; + } + return mockCustomerSummary(salesModal.month, salesModal.amount, deptApplied, custSumFilterApplied, salesModal.cellType === 'Cost', brokerageFeeOverrides); + }, [view, salesModal, deptApplied, custSumFilterApplied, brokerageFeeOverrides]); + + var projectSummaryPayload = useMemo(function () { + if (view !== 'projectSummary' || !detailCustomer) { + return []; + } + return mockProjectSummary(detailCustomer, salesModal.cellType === 'Cost', brokerageFeeOverrides); + }, [view, detailCustomer, salesModal, brokerageFeeOverrides]); + + var drillColumns = useMemo(function () { + var isCost = salesModal.cellType === 'Cost'; + var cols = [ + { title: '账单周期', dataIndex: 'billCycle', key: 'billCycle', width: 160, align: 'center' }, + { title: '账单期数', dataIndex: 'billInstallment', key: 'billInstallment', width: 120, align: 'center' }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 100, align: 'center' }, + { title: '品牌', dataIndex: 'brand', key: 'brand', width: 80, align: 'center' }, + { title: '型号', dataIndex: 'vehicleModel', key: 'vehicleModel', width: 180, align: 'center' }, + { title: '业务员', dataIndex: 'salesperson', key: 'salesperson', width: 80, align: 'center' }, + { title: '性质', dataIndex: 'nature', key: 'nature', width: 80, align: 'center' }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 220, align: 'center' }, + { title: '项目名称', dataIndex: 'projectName', key: 'projectName', width: 160, align: 'center' }, + { title: '合同有效期', dataIndex: 'contractDate', key: 'contractDate', width: 160, align: 'center' }, + { title: '提车日期', dataIndex: 'pickupDate', key: 'pickupDate', width: 100, align: 'center' } + ]; + + if (isCost) { + var handleBrokerageBatchPopoverChange = function (nextOpen) { + setBrokerageBatchPopoverOpen(nextOpen); + if (!nextOpen) setBrokerageBatchInput(null); + }; + var brokerageBatchPopoverContent = React.createElement('div', { style: { minWidth: 268 } }, + React.createElement('div', { style: { marginBottom: 12, display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 8 } }, + React.createElement('span', { style: { color: '#ff4d4f' } }, '*'), + React.createElement('span', { style: { fontSize: 14 } }, '居间费批量设置'), + React.createElement(InputNumber, { + min: 0, + precision: 2, + step: 0.01, + controls: true, + value: brokerageBatchInput, + onChange: function (val) { setBrokerageBatchInput(val); }, + placeholder: '请输入', + style: { width: 150 } + }) + ), + React.createElement(Space, { size: 'small' }, + React.createElement(Button, { type: 'primary', size: 'small', onClick: applyBrokerageBatch }, '确认'), + React.createElement(Button, { size: 'small', onClick: function () { setBrokerageBatchPopoverOpen(false); setBrokerageBatchInput(null); } }, '关闭') + ) + ); + var brokerageColumnTitle = React.createElement('div', { style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'flex-end', gap: 6 } }, + React.createElement('span', null, '居间费'), + React.createElement(Popover, { + trigger: 'click', + placement: 'bottomRight', + content: brokerageBatchPopoverContent, + open: brokerageBatchPopoverOpen, + visible: brokerageBatchPopoverOpen, + onOpenChange: handleBrokerageBatchPopoverChange, + onVisibleChange: handleBrokerageBatchPopoverChange + }, React.createElement('span', { + role: 'button', + tabIndex: 0, + style: { cursor: 'pointer', color: '#0ea5e9', display: 'inline-flex', alignItems: 'center' }, + 'aria-label': '批量设置居间费' + }, React.createElement(EditIcon, null))) + ); + cols.push( + { title: '车辆成本', dataIndex: 'vehicleCost', key: 'vehicleCost', width: 100, align: 'right', render: function(v){ return fmtLedgerCell(v); } }, + { + title: brokerageColumnTitle, + dataIndex: 'brokerageFee', + key: 'brokerageFee', + width: 120, + align: 'right', + render: function (v, record) { + if (record.rowType === 'total') return fmtLedgerCell(v); + var compositeKey = record.customerName + '|' + record.projectName + '|' + record.plateNo; + var isEditing = editingBrokerage && editingBrokerage.key === compositeKey; + if (isEditing) { + return React.createElement(InputNumber, { + autoFocus: true, + value: editingBrokerage.value, + onChange: function(val) { setEditingBrokerage({ key: compositeKey, value: val }); }, + onBlur: function() { + setBrokerageFeeOverrides(Object.assign({}, brokerageFeeOverrides, { [compositeKey]: editingBrokerage.value })); + setEditingBrokerage(null); + }, + onPressEnter: function() { + setBrokerageFeeOverrides(Object.assign({}, brokerageFeeOverrides, { [compositeKey]: editingBrokerage.value })); + setEditingBrokerage(null); + }, + style: { width: 100 } + }); + } + return React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 6 } }, + fmtLedgerCell(v), + React.createElement('span', { + style: { cursor: 'pointer', color: '#165dff', display: 'flex', alignItems: 'center' }, + onClick: function() { setEditingBrokerage({ key: compositeKey, value: v }); } + }, React.createElement(EditIcon, null)) + ); + } + }, + { title: '总成本', dataIndex: 'totalCost', key: 'totalCost', width: 100, align: 'right', render: function(v){ return fmtLedgerCell(v); } } + ); + } else { + cols.push( + { title: '应收', dataIndex: 'receivable', key: 'receivable', width: 100, align: 'right', render: function(v){ return fmtLedgerCell(v); } }, + { title: '实收', dataIndex: 'received', key: 'received', width: 100, align: 'right', render: function(v){ return fmtLedgerCell(v); } }, + { title: '未收', dataIndex: 'unreceived', key: 'unreceived', width: 100, align: 'right', render: function(v){ return React.createElement('span', { style: { color: '#f53f3f' } }, fmtLedgerCell(v)); } }, + { title: '自然月收入', dataIndex: 'naturalMonthIncome', key: 'naturalMonthIncome', width: 100, align: 'right', render: function(v){ return fmtLedgerCell(v); } }, + { title: '付款日期', dataIndex: 'paymentDate', key: 'paymentDate', width: 100, align: 'center' }, + { title: '付款方式', dataIndex: 'payMethod', key: 'payMethod', width: 80, align: 'center' }, + { title: '付款周期', dataIndex: 'payCycle', key: 'payCycle', width: 80, align: 'center' } + ); + } + return cols; + }, [salesModal.cellType, detailProject, editingBrokerage, brokerageFeeOverrides, brokerageBatchPopoverOpen, brokerageBatchInput, applyBrokerageBatch]); + + var detailPayload = useMemo(function () { + if (view !== 'sales_detail' || salesDetailModal.ym == null) { + return { rows: [], totals: { deposit: 0, vehicle: 0, additional: 0, total: 0 } }; + } + return mockLeaseOrderDetail(salesDetailModal.ym, salesDetailModal.deptName, salesDetailModal.salesperson, salesDetailModal.amount); + }, [view, salesDetailModal]); + + var detailColumns = useMemo(function () { + return [ + { title: '月份', dataIndex: 'ym', key: 'ym', width: 104, align: 'center', onCell: function(r){ return { rowSpan: r.monthRowSpan }; } }, + { title: '业务部门', dataIndex: 'deptName', key: 'deptName', width: 124, align: 'center', onCell: function(r){ return { rowSpan: r.groupRowSpan }; } }, + { title: '业务人员', dataIndex: 'salesperson', key: 'salesperson', width: 100, align: 'center', onCell: function(r){ return { rowSpan: r.groupRowSpan }; } }, + { title: '账单日期', dataIndex: 'billDate', key: 'billDate', width: 110, align: 'center', onCell: function(r){ return { rowSpan: r.groupRowSpan }; } }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 120, align: 'center' }, + { title: '车辆型号', dataIndex: 'vehicleModel', key: 'vehicleModel', width: 120, align: 'center' }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 180, align: 'center' }, + { title: '合同日期', dataIndex: 'contractDate', key: 'contractDate', width: 200, align: 'center' }, + { title: '提车日期', dataIndex: 'pickupDate', key: 'pickupDate', width: 110, align: 'center' }, + { title: '付款方式', dataIndex: 'payMethod', key: 'payMethod', width: 90, align: 'center' }, + { title: '付款周期', dataIndex: 'payCycle', key: 'payCycle', width: 90, align: 'center' }, + { title: '账单计算方式', dataIndex: 'billCalcMethod', key: 'billCalcMethod', width: 110, align: 'center' }, + { title: '实际计费天数', dataIndex: 'actualDays', key: 'actualDays', width: 110, align: 'center' }, + { title: '保证金应收', dataIndex: 'depositAmount', key: 'depositAmount', width: 120, align: 'right', render: function(v){ return fmtLedgerCell(v); } }, + { title: '车辆应收', dataIndex: 'vehicleAmount', key: 'vehicleAmount', width: 120, align: 'right', render: function(v){ return fmtLedgerCell(v); } }, + { title: '附加费用应收', dataIndex: 'additionalAmount', key: 'additionalAmount', width: 140, align: 'right', render: function(v){ return fmtLedgerCell(v); } }, + { title: '合计应收', dataIndex: 'totalAmount', key: 'totalAmount', width: 120, align: 'right', render: function(v){ return fmtLedgerCell(v); } } + ]; + }, []); + + var handleExport = useCallback(function () { + if (!dataSource || dataSource.length === 0) { + message.warning('当前无数据可导出,请先查询'); + return; + } + var y = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : 'ledger'; + + var header = ['月份']; + SECTOR_COLUMNS.forEach(function (s) { + header.push(s.label + '-业绩', s.label + '-成本', s.label + '-利润'); + }); + header.push('业绩合计', '成本合计', '利润合计'); + var body = [header]; + + dataSource.forEach(function (r) { + var line = [r.monthLabel]; + SECTOR_COLUMNS.forEach(function (s) { + line.push(fmtLedgerCell(r[s.key + 'Perf']), fmtLedgerCell(r[s.key + 'Cost']), fmtLedgerCell(r[s.key + 'Profit'])); + }); + line.push(fmtLedgerCell(r.rowTotalPerf), fmtLedgerCell(r.rowTotalCost), fmtLedgerCell(r.rowTotalProfit)); + body.push(line); + }); + + var deptCsv = (deptApplied && deptApplied.length) + ? deptApplied.map(function (v) { + var o = deptOptions.find(function (x) { return x.value === v; }); + return o ? o.label : v; + }).join('、') + : '全部'; + body.push(['业务部', deptCsv]); + + downloadCsv('业务部台账_' + y + '_' + new Date().getTime() + '.csv', body); + message.success('已导出'); + }, [dataSource, yearApplied, deptApplied, deptOptions]); + + var ledgerColumns = useMemo(function () { + var cols = [ + { + title: '月份', + dataIndex: 'monthLabel', + key: 'monthLabel', + fixed: 'left', + width: 56, + align: 'center', + render: function (t, r) { + if (r.rowType === 'total') return React.createElement('span', { style: { fontWeight: 700 } }, t); + return t; + } + } + ]; + + SECTOR_COLUMNS.forEach(function (sec) { + cols.push({ + title: sec.label, + key: 'grp-' + sec.key, + align: 'center', + children: [ + { + title: '业绩', + dataIndex: sec.key + 'Perf', + key: sec.key + 'Perf', + width: 108, + align: 'center', + render: function (v, r) { + if (sec.key === 'lease' && numOrZero(v) !== 0) { + return React.createElement('button', { + type: 'button', + className: 'biz-standbook-perf-link', + onClick: function () { openLeaseDrill(r, sec.key + 'Perf', v); } + }, fmtLedgerCell(v)); + } + if (sec.key === 'hydrogen' && numOrZero(v) !== 0) { + return React.createElement('button', { + type: 'button', + className: 'biz-standbook-perf-link', + onClick: function () { openHydrogenDrill(r, v, false); } + }, fmtLedgerCell(v)); + } + return fmtLedgerCell(v); + } + }, + { + title: '成本', + dataIndex: sec.key + 'Cost', + key: sec.key + 'Cost', + width: 108, + align: 'center', + render: function (v, r) { + if (sec.key === 'lease' && numOrZero(v) !== 0) { + return React.createElement('button', { + type: 'button', + className: 'biz-standbook-perf-link', + onClick: function () { openLeaseDrill(r, sec.key + 'Cost', v); } + }, fmtLedgerCell(v)); + } + if (sec.key === 'hydrogen' && numOrZero(v) !== 0) { + return React.createElement('button', { + type: 'button', + className: 'biz-standbook-perf-link', + onClick: function () { openHydrogenDrill(r, v, true); } + }, fmtLedgerCell(v)); + } + return fmtLedgerCell(v); + } + }, + { + title: '利润', + dataIndex: sec.key + 'Profit', + key: sec.key + 'Profit', + width: 108, + align: 'center', + render: function (v) { return fmtLedgerCell(v); } + } + ] + }); + }); + + cols.push( + { + title: '业绩合计', + dataIndex: 'rowTotalPerf', + key: 'rowTotalPerf', + width: 112, + align: 'center', + fixed: 'right', + className: 'col-summary', + render: function (v) { return fmtLedgerCell(v); } + }, + { + title: '成本合计', + dataIndex: 'rowTotalCost', + key: 'rowTotalCost', + width: 112, + align: 'center', + fixed: 'right', + className: 'col-summary', + render: function (v) { return fmtLedgerCell(v); } + }, + { + title: '利润合计', + dataIndex: 'rowTotalProfit', + key: 'rowTotalProfit', + width: 112, + align: 'center', + fixed: 'right', + className: 'col-summary', + render: function (v) { return fmtLedgerCell(v); } + } + ); + + return cols; + }, [openLeaseDrill, openHydrogenDrill]); + + var rowClassName = useCallback(function (record) { + if (record.rowType === 'total') return ''; + return 'biz-row-month'; + }, []); + + var renderMainView = function () { + return React.createElement(React.Fragment, null, + React.createElement(Card, { style: filterCardStyle, bodyStyle: { paddingBottom: 4 } }, + React.createElement(Row, { gutter: [16, 16], align: 'bottom' }, + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '年份选择'), + React.createElement(DatePicker, { + picker: 'year', + style: filterControlStyle, + placeholder: '请选择年份', + format: 'YYYY', + value: yearDraft, + onChange: function (v) { setYearDraft(v); } + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '业务部'), + React.createElement(Select, { + mode: 'multiple', + placeholder: '全部', + style: filterControlStyle, + value: deptDraft, + onChange: function (v) { setDeptDraft(v || []); }, + options: deptOptions, + showSearch: true, + allowClear: true, + maxTagCount: 2, + filterOption: filterOption + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6, style: filterActionsColStyle }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '\u00a0'), + React.createElement(Space, { wrap: true }, + React.createElement(Button, { onClick: handleReset }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleQuery }, '查询') + ) + ) + ) + ) + ), + React.createElement(Card, { style: tableCardStyle, bodyStyle: { padding: '20px 20px 24px' } }, + React.createElement('div', { style: { position: 'relative', marginBottom: 8, minHeight: 36 } }, + React.createElement('div', { style: { textAlign: 'center', fontSize: 18, fontWeight: 700, color: 'rgba(15,23,42,0.92)', letterSpacing: '0.02em', padding: '0 88px' } }, tableTitle), + React.createElement('div', { style: { position: 'absolute', right: 0, top: '50%', transform: 'translateY(-50%)' } }, + React.createElement(Button, { onClick: handleExport }, '导出') + ) + ), + React.createElement('div', { style: { textAlign: 'center', marginBottom: 16, fontSize: 13, color: 'rgba(15,23,42,0.55)', fontWeight: 500 } }, + '时间:', + timeDisplayLabel, + '\u00A0\u00A0\u00A0\u00A0业务部:', + deptDisplayLabel + ), + React.createElement('div', { className: 'biz-standbook-table-wrap' }, + React.createElement(Table, { + className: 'biz-standbook-table', + size: 'small', + bordered: true, + rowKey: 'key', + columns: ledgerColumns, + dataSource: dataSource, + pagination: false, + rowClassName: rowClassName, + scroll: { x: 'max-content', y: 'calc(100vh - 250px)' }, + sticky: true + }) + ) + ) + ); + }; + + var renderHydrogenCustomerPerfView = function () { + var hydrogenIsCost = salesModal.cellType === 'HydrogenCost'; + var hydroAmountColTitle = hydrogenIsCost ? '成本金额(元)' : '对客总额(元)'; + var hydroCustomerPageTitle = hydrogenIsCost ? '氢费业务客户成本汇总' : '氢费业务客户业绩汇总'; + var columns = [ + { + title: '月份', + dataIndex: 'monthLabel', + key: 'monthLabel', + align: 'center', + onCell: function (record) { return { rowSpan: record.monthSpan }; } + }, + { + title: '客户名称', + dataIndex: 'customerName', + key: 'customerName', + align: 'center', + render: function (v, record) { + return React.createElement('button', { + type: 'button', + className: 'biz-standbook-perf-link', + onClick: function () { openHydroStationDrillByCustomer(record); } + }, v); + } + }, + { + title: '加氢次数', + dataIndex: 'count', + key: 'count', + align: 'center' + }, + { + title: '加氢量(kg)', + dataIndex: 'amountKg', + key: 'amountKg', + align: 'right', + render: function(v) { return fmtLedgerCell(v); } + }, + { + title: hydroAmountColTitle, + dataIndex: 'totalAmount', + key: 'totalAmount', + align: 'right', + render: function(v) { return fmtLedgerCell(v); } + } + ]; + + var sumCount = 0, sumAmountKg = 0, sumTotalAmount = 0; + var uniqueCustomers = []; + hydrogenCustomerPerfPayload.rows.forEach(function(r) { + if (uniqueCustomers.indexOf(r.customerName) === -1) uniqueCustomers.push(r.customerName); + sumCount += numOrZero(r.count); + sumAmountKg += numOrZero(r.amountKg); + sumTotalAmount += numOrZero(r.totalAmount); + }); + + var handleQuery = function() { + setHydroFilterApplied(hydroFilterDraft); + }; + var handleReset = function() { + var resetState = { month: null, customerName: undefined }; + setHydroFilterDraft(resetState); + setHydroFilterApplied(resetState); + }; + + var hydroStatMonthStr = hydroFilterApplied.month && hydroFilterApplied.month.format ? hydroFilterApplied.month.format('YYYY-MM') : '—'; + var hydroPerfSubline = hydroFilterApplied.customerName + ? hydroFilterApplied.customerName + '\u00A0\u00A0\u00A0\u00A0统计时间:' + hydroStatMonthStr + : '统计时间:' + hydroStatMonthStr; + + return React.createElement(React.Fragment, null, + React.createElement(Card, { style: filterCardStyle, bodyStyle: { paddingBottom: 4 } }, + React.createElement(Row, { gutter: [16, 16], align: 'bottom' }, + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '月份'), + React.createElement(DatePicker, { + picker: 'month', + placeholder: '日期选择器,精确至月', + style: filterControlStyle, + value: hydroFilterDraft.month, + onChange: function (v) { setHydroFilterDraft(Object.assign({}, hydroFilterDraft, { month: v })); } + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '客户名称'), + React.createElement(Select, { + placeholder: '选择器,枚举所有客户', + style: filterControlStyle, + value: hydroFilterDraft.customerName, + onChange: function (v) { setHydroFilterDraft(Object.assign({}, hydroFilterDraft, { customerName: v })); }, + allowClear: true, + showSearch: true, + filterOption: filterOption, + options: hydrogenCustomerPerfPayload.options ? hydrogenCustomerPerfPayload.options.customers : [] + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6, style: filterActionsColStyle }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '\u00a0'), + React.createElement(Space, { wrap: true }, + React.createElement(Button, { onClick: handleReset }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleQuery }, '查询') + ) + ) + ) + ) + ), + React.createElement(Card, { style: tableCardStyle, bodyStyle: { padding: '16px 24px' } }, + React.createElement('div', { style: { position: 'relative', marginBottom: 16, minHeight: 52 } }, + React.createElement('div', { style: { position: 'absolute', left: 0, top: 0, zIndex: 1 } }, + React.createElement(Button, { onClick: function () { setView('main'); } }, '返回上一步') + ), + React.createElement('div', { style: { textAlign: 'center', padding: '0 128px' } }, + React.createElement('div', { style: { fontSize: 18, fontWeight: 700, color: 'rgba(15,23,42,0.92)', letterSpacing: '0.02em' } }, hydroCustomerPageTitle), + React.createElement('div', { style: { marginTop: 6, fontSize: 13, color: 'rgba(15,23,42,0.55)', fontWeight: 500 } }, hydroPerfSubline) + ) + ), + React.createElement('div', { className: 'biz-standbook-table-wrap' }, + React.createElement(Table, { + className: 'biz-standbook-table', bordered: true, rowKey: 'key', columns: columns, dataSource: hydrogenCustomerPerfPayload.rows, pagination: false, scroll: { x: 'max-content', y: 'calc(100vh - 250px)' }, sticky: true, + summary: function () { + return React.createElement(Table.Summary, { fixed: 'bottom' }, + React.createElement(Table.Summary.Row, { className: 'row-total' }, + React.createElement(Table.Summary.Cell, { index: 0, align: 'center' }, '合计'), + React.createElement(Table.Summary.Cell, { index: 1, align: 'center' }, + React.createElement('button', { + type: 'button', + className: 'biz-standbook-perf-link', + onClick: openHydroStationDrillAllStations + }, uniqueCustomers.length) + ), + React.createElement(Table.Summary.Cell, { index: 2, align: 'center' }, sumCount), + React.createElement(Table.Summary.Cell, { index: 3, align: 'right' }, fmtLedgerCell(sumAmountKg)), + React.createElement(Table.Summary.Cell, { index: 4, align: 'right' }, fmtLedgerCell(sumTotalAmount)) + ) + ); + } + }) + ) + ) + ); + }; + + var renderHydrogenStationDrillView = function () { + var hydrogenIsCost = salesModal.cellType === 'HydrogenCost'; + var stAmountColTitle = hydrogenIsCost ? '成本金额(元)' : '对客总额(元)'; + var stPageTitle = hydrogenIsCost ? '客户各加氢站加氢成本' : '客户各加氢站加氢业绩'; + var stRows = hydrogenStationDrillPayload.rows || []; + var stSumCount = 0; + var stSumKg = 0; + var stSumAmt = 0; + stRows.forEach(function (r) { + stSumCount += numOrZero(r.count); + stSumKg += numOrZero(r.amountKg); + stSumAmt += numOrZero(r.totalAmount); + }); + var hydroStatMonthStr2 = hydroFilterApplied.month && hydroFilterApplied.month.format ? hydroFilterApplied.month.format('YYYY-MM') : '—'; + var scopeLbl = hydrogenStationDrillPayload.drillScopeLabel; + var drillPrefix = scopeLbl && scopeLbl !== '全部客户' ? scopeLbl + '\u00A0\u00A0\u00A0\u00A0' : ''; + var drillSubLabel = drillPrefix + '统计时间:' + hydroStatMonthStr2; + + var stColumns = [ + { + title: '月份', + dataIndex: 'monthLabel', + key: 'monthLabel', + align: 'center', + onCell: function (record) { return { rowSpan: record.monthSpan }; } + }, + { + title: '客户名称', + dataIndex: 'customerName', + key: 'customerName', + align: 'center', + onCell: function (record) { return { rowSpan: record.customerSpan }; }, + render: function (v, record) { + return React.createElement('button', { + type: 'button', + className: 'biz-standbook-perf-link', + onClick: function () { openHydroRefuelDetailByCustomer(v); } + }, v); + } + }, + { + title: '加氢站名称', + dataIndex: 'stationName', + key: 'stationName', + align: 'center', + render: function (v, record) { + return React.createElement('button', { + type: 'button', + className: 'biz-standbook-perf-link', + onClick: function () { openHydroRefuelDetailByStationRow(record); } + }, v); + } + }, + { + title: '加氢次数', + dataIndex: 'count', + key: 'count', + align: 'center' + }, + { + title: '加氢量(kg)', + dataIndex: 'amountKg', + key: 'amountKg', + align: 'right', + render: function (v) { return fmtLedgerCell(v); } + }, + { + title: stAmountColTitle, + dataIndex: 'totalAmount', + key: 'totalAmount', + align: 'right', + render: function (v) { return fmtLedgerCell(v); } + } + ]; + + return React.createElement(Card, { style: tableCardStyle, bodyStyle: { padding: '16px 24px' } }, + React.createElement('div', { style: { position: 'relative', marginBottom: 16, minHeight: 52 } }, + React.createElement('div', { style: { position: 'absolute', left: 0, top: 0, zIndex: 1 } }, + React.createElement(Button, { onClick: function () { setView('hydrogenCustomerPerf'); } }, '返回上一步') + ), + React.createElement('div', { style: { textAlign: 'center', padding: '0 128px' } }, + React.createElement('div', { style: { fontSize: 18, fontWeight: 700, color: 'rgba(15,23,42,0.92)', letterSpacing: '0.02em' } }, stPageTitle), + React.createElement('div', { style: { marginTop: 6, fontSize: 13, color: 'rgba(15,23,42,0.55)', fontWeight: 500 } }, drillSubLabel) + ) + ), + React.createElement('div', { className: 'biz-standbook-table-wrap' }, + React.createElement(Table, { + className: 'biz-standbook-table', + bordered: true, + rowKey: 'key', + columns: stColumns, + dataSource: stRows, + pagination: false, + scroll: { x: 'max-content', y: 'calc(100vh - 250px)' }, + sticky: true, + locale: { emptyText: '暂无加氢站明细' }, + summary: function () { + return React.createElement(Table.Summary, { fixed: 'bottom' }, + React.createElement(Table.Summary.Row, { className: 'row-total' }, + React.createElement(Table.Summary.Cell, { index: 0, colSpan: 3, align: 'center' }, '合计'), + React.createElement(Table.Summary.Cell, { index: 3, align: 'center' }, stSumCount), + React.createElement(Table.Summary.Cell, { index: 4, align: 'right' }, fmtLedgerCell(stSumKg)), + React.createElement(Table.Summary.Cell, { index: 5, align: 'right' }, fmtLedgerCell(stSumAmt)) + ) + ); + } + }) + ) + ); + }; + + var renderHydrogenRefuelDetailView = function () { + var hydrogenIsCost = salesModal.cellType === 'HydrogenCost'; + var refuelUnitTitle = hydrogenIsCost ? '成本单价(元/kg)' : '对客单价(元/kg)'; + var refuelFeeTitle = hydrogenIsCost ? '成本费用(元)' : '对客费用(元)'; + var dRows = hydrogenRefuelDetailPayload.rows || []; + var sumKg = 0; + var sumFee = 0; + dRows.forEach(function (r) { + sumKg += numOrZero(r.amountKg); + sumFee += numOrZero(r.customerFee); + }); + var avgUnit = sumKg > 0 ? Math.round((sumFee / sumKg) * 100) / 100 : 0; + var hydroStatMonthStr3 = hydroFilterApplied.month && hydroFilterApplied.month.format ? hydroFilterApplied.month.format('YYYY-MM') : '—'; + var ctx = hydroRefuelDetailCtx; + var detailSubParts = ['客户:' + (ctx.customerName || '—')]; + if (ctx.stationName) { + detailSubParts.push('加氢站:' + ctx.stationName); + } else { + detailSubParts.push('加氢站:全部'); + } + detailSubParts.push('统计时间:' + hydroStatMonthStr3); + var detailSubline = detailSubParts.join('\u00A0\u00A0\u00A0\u00A0'); + + var refuelCols = [ + { title: '加氢日期', dataIndex: 'refuelDateLabel', key: 'refuelDateLabel', align: 'center' }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', align: 'center' }, + { title: '项目名称', dataIndex: 'projectName', key: 'projectName', align: 'center' }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', align: 'center' }, + { title: '加氢站名称', dataIndex: 'stationName', key: 'stationName', align: 'center' }, + { title: '加氢量(kg)', dataIndex: 'amountKg', key: 'amountKg', align: 'right', render: function (v) { return fmtLedgerCell(v); } }, + { title: refuelUnitTitle, dataIndex: 'unitPrice', key: 'unitPrice', align: 'right', render: function (v) { return fmtLedgerCell(v); } }, + { title: refuelFeeTitle, dataIndex: 'customerFee', key: 'customerFee', align: 'right', render: function (v) { return fmtLedgerCell(v); } }, + { title: '业务员', dataIndex: 'salesperson', key: 'salesperson', align: 'center' } + ]; + + return React.createElement(Card, { style: tableCardStyle, bodyStyle: { padding: '16px 24px' } }, + React.createElement('div', { style: { position: 'relative', marginBottom: 16, minHeight: 52 } }, + React.createElement('div', { style: { position: 'absolute', left: 0, top: 0, zIndex: 1 } }, + React.createElement(Button, { onClick: function () { setView('hydrogenStationDrill'); } }, '返回上一步') + ), + React.createElement('div', { style: { textAlign: 'center', padding: '0 128px' } }, + React.createElement('div', { style: { fontSize: 18, fontWeight: 700, color: 'rgba(15,23,42,0.92)', letterSpacing: '0.02em' } }, '加氢明细'), + React.createElement('div', { style: { marginTop: 6, fontSize: 13, color: 'rgba(15,23,42,0.55)', fontWeight: 500 } }, detailSubline) + ) + ), + React.createElement('div', { className: 'biz-standbook-table-wrap' }, + React.createElement(Table, { + className: 'biz-standbook-table', + bordered: true, + rowKey: 'key', + columns: refuelCols, + dataSource: dRows, + pagination: false, + scroll: { x: 'max-content', y: 'calc(100vh - 250px)' }, + sticky: true, + locale: { emptyText: '暂无加氢明细' }, + summary: function () { + return React.createElement(Table.Summary, { fixed: 'bottom' }, + React.createElement(Table.Summary.Row, { className: 'row-total' }, + React.createElement(Table.Summary.Cell, { index: 0, colSpan: 5, align: 'center' }, '合计'), + React.createElement(Table.Summary.Cell, { index: 5, align: 'right' }, fmtLedgerCell(sumKg)), + React.createElement(Table.Summary.Cell, { index: 6, align: 'right' }, sumKg > 0 ? fmtLedgerCell(avgUnit) : '—'), + React.createElement(Table.Summary.Cell, { index: 7, align: 'right' }, fmtLedgerCell(sumFee)), + React.createElement(Table.Summary.Cell, { index: 8, align: 'center' }, '\u00a0') + ) + ); + } + }) + ) + ); + }; + + var renderCustomerSummaryView = function () { + var isCost = salesModal.cellType === 'Cost'; + var leaseSummaryMainTitle = isCost ? '租赁业务业务员成本汇总' : '租赁业务业务员业绩汇总'; + var yearStr = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : 'YYYY'; + var leaseStatTimeStr; + if (salesModal.month != null && salesModal.month <= 12) { + leaseStatTimeStr = yearStr + '-' + pad2(salesModal.month); + } else if (salesModal.month === 13) { + leaseStatTimeStr = yearStr + '(全年)'; + } else { + leaseStatTimeStr = '—'; + } + var leaseSumSubline = custSumFilterApplied.customerName + ? custSumFilterApplied.customerName + '\u00A0\u00A0\u00A0\u00A0统计时间:' + leaseStatTimeStr + : '统计时间:' + leaseStatTimeStr; + + var columns = [ + { + title: '业务部门', + dataIndex: 'dept', + key: 'dept', + align: 'center', + onCell: function (record) { + return { rowSpan: record.deptSpan }; + } + }, + { + title: '业务员', + dataIndex: 'salesperson', + key: 'salesperson', + align: 'center', + onCell: function (record) { + return { rowSpan: record.spSpan }; + } + }, + { + title: '客户名称', + dataIndex: 'customerName', + key: 'customerName', + align: 'center', + render: function (v, record) { + return React.createElement('button', { + className: 'biz-standbook-perf-link', + onClick: function () { openProjectSummary(record); } + }, v); + } + } + ]; + + if (isCost) { + columns.push( + { title: '车辆成本', dataIndex: 'vehicleCost', key: 'vehicleCost', align: 'right', render: function (v) { return fmtLedgerCell(v); } }, + { title: '居间费', dataIndex: 'brokerageFee', key: 'brokerageFee', align: 'right', render: function (v) { return fmtLedgerCell(v); } }, + { title: '成本总计', dataIndex: 'totalCost', key: 'totalCost', align: 'right', render: function (v) { return fmtLedgerCell(v); } } + ); + } else { + columns.push( + { title: '应收', dataIndex: 'receivable', key: 'receivable', align: 'right', render: function (v) { return fmtLedgerCell(v); } }, + { title: '实收', dataIndex: 'received', key: 'received', align: 'right', render: function (v) { return fmtLedgerCell(v); } }, + { title: '未收', dataIndex: 'unreceived', key: 'unreceived', align: 'right', render: function (v) { return fmtLedgerCell(v); } } + ); + } + + var sum1 = 0, sum2 = 0, sum3 = 0; + var uniqueCustomers = []; + customerSummaryPayload.rows.forEach(function(r) { + if (uniqueCustomers.indexOf(r.customerName) === -1) { + uniqueCustomers.push(r.customerName); + } + if (isCost) { + sum1 += numOrZero(r.vehicleCost); + sum2 += numOrZero(r.brokerageFee); + sum3 += numOrZero(r.totalCost); + } else { + sum1 += numOrZero(r.receivable); + sum2 += numOrZero(r.received); + sum3 += numOrZero(r.unreceived); + } + }); + var customerCount = uniqueCustomers.length; + + return React.createElement(React.Fragment, null, + React.createElement(Card, { style: filterCardStyle, bodyStyle: { paddingBottom: 4 } }, + React.createElement(Row, { gutter: [16, 16], align: 'bottom' }, + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '客户名称'), + React.createElement(Select, { + placeholder: '请选择客户名称', + style: filterControlStyle, + value: custSumFilterDraft.customerName, + onChange: function (v) { setCustSumFilterDraft(Object.assign({}, custSumFilterDraft, { customerName: v })); }, + allowClear: true, + showSearch: true, + filterOption: filterOption, + options: customerSummaryPayload.options ? customerSummaryPayload.options.customers : [] + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6, style: filterActionsColStyle }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '\u00a0'), + React.createElement(Space, { wrap: true }, + React.createElement(Button, { onClick: handleCustSumReset }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleCustSumQuery }, '查询') + ) + ) + ) + ) + ), + React.createElement(Card, { style: tableCardStyle, bodyStyle: { padding: '16px 24px' } }, + React.createElement('div', { style: { position: 'relative', marginBottom: 16, minHeight: 52 } }, + React.createElement('div', { style: { position: 'absolute', left: 0, top: 0, zIndex: 1 } }, + React.createElement(Button, { onClick: function () { setView('main'); } }, '返回上一步') + ), + React.createElement('div', { style: { textAlign: 'center', padding: '0 128px' } }, + React.createElement('div', { style: { fontSize: 18, fontWeight: 700, color: 'rgba(15,23,42,0.92)', letterSpacing: '0.02em' } }, leaseSummaryMainTitle), + React.createElement('div', { style: { marginTop: 6, fontSize: 13, color: 'rgba(15,23,42,0.55)', fontWeight: 500 } }, leaseSumSubline) + ) + ), + React.createElement('div', { className: 'biz-standbook-table-wrap' }, + React.createElement(Table, { + className: 'biz-standbook-table', + size: 'small', + bordered: true, + rowKey: 'key', + columns: columns, + dataSource: customerSummaryPayload.rows, + pagination: false, + scroll: { x: 'max-content', y: 'calc(100vh - 250px)' }, + sticky: true, + summary: function () { + return React.createElement(Table.Summary, { fixed: 'bottom' }, + React.createElement(Table.Summary.Row, { className: 'row-total' }, + React.createElement(Table.Summary.Cell, { index: 0, colSpan: 2, align: 'center' }, '合计'), + React.createElement(Table.Summary.Cell, { index: 1, align: 'center' }, + React.createElement('button', { + className: 'biz-standbook-perf-link', + onClick: function () { + openProjectSummary({ isAll: true, sourceRows: customerSummaryPayload.rows }); + } + }, customerCount) + ), + React.createElement(Table.Summary.Cell, { index: 2, align: 'right' }, fmtLedgerCell(sum1)), + React.createElement(Table.Summary.Cell, { index: 3, align: 'right' }, fmtLedgerCell(sum2)), + React.createElement(Table.Summary.Cell, { index: 4, align: 'right' }, fmtLedgerCell(sum3)) + ) + ); + } + }) + ) + ) + ); + }; + + var renderProjectSummaryView = function () { + var isCost = salesModal.cellType === 'Cost'; + var projectPageMainTitle = isCost ? '租赁业务客户各项目成本汇总' : '租赁业务客户各项目业绩汇总'; + var yearStr = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : 'YYYY'; + var projStatTimeStr; + if (salesModal.month != null && salesModal.month <= 12) { + projStatTimeStr = yearStr + '-' + pad2(salesModal.month); + } else if (salesModal.month === 13) { + projStatTimeStr = yearStr + '(全年)'; + } else { + projStatTimeStr = '—'; + } + var projectCustomerScope = detailCustomer + ? (detailCustomer.isAll ? '全部客户' : (detailCustomer.customerName || '—')) + : '—'; + var projectSumSubline = projectCustomerScope + '\u00A0\u00A0\u00A0\u00A0统计时间:' + projStatTimeStr; + + var columns = [ + { + title: '客户名称', + dataIndex: 'customerName', + key: 'customerName', + align: 'center', + onCell: function (record) { return { rowSpan: record.custSpan }; } + }, + { + title: '项目名称', + dataIndex: 'projectName', + key: 'projectName', + align: 'center', + render: function (text, record) { + return React.createElement('button', { + className: 'biz-standbook-perf-link', + onClick: function () { return openProjectDetail(record); } + }, text); + } + } + ]; + + if (isCost) { + columns.push( + { title: '车辆成本', dataIndex: 'vehicleCost', key: 'vehicleCost', align: 'right', render: function (v) { return fmtLedgerCell(v); } }, + { + title: '居间费', + dataIndex: 'brokerageFee', + key: 'brokerageFee', + align: 'right', + render: function (v) { return fmtLedgerCell(v); } + }, + { title: '成本总计', dataIndex: 'totalCost', key: 'totalCost', align: 'right', render: function (v) { return fmtLedgerCell(v); } } + ); + } else { + columns.push( + { title: '应收', dataIndex: 'receivable', key: 'receivable', align: 'right', render: function (v) { return fmtLedgerCell(v); } }, + { title: '实收', dataIndex: 'received', key: 'received', align: 'right', render: function (v) { return fmtLedgerCell(v); } }, + { title: '未收', dataIndex: 'unreceived', key: 'unreceived', align: 'right', render: function (v) { return fmtLedgerCell(v); } } + ); + } + + var sum1 = 0, sum2 = 0, sum3 = 0; + var uniqueProjects = []; + projectSummaryPayload.forEach(function(r) { + var projKey = r.customerName + '|' + r.projectName; + if (uniqueProjects.indexOf(projKey) === -1) { + uniqueProjects.push(projKey); + } + if (isCost) { + sum1 += numOrZero(r.vehicleCost); + sum2 += numOrZero(r.brokerageFee); + sum3 += numOrZero(r.totalCost); + } else { + sum1 += numOrZero(r.receivable); + sum2 += numOrZero(r.received); + sum3 += numOrZero(r.unreceived); + } + }); + var projectCount = uniqueProjects.length; + + return React.createElement(Card, { style: tableCardStyle, bodyStyle: { padding: '16px 24px' } }, + React.createElement('div', { style: { position: 'relative', marginBottom: 16, minHeight: 52 } }, + React.createElement('div', { style: { position: 'absolute', left: 0, top: 0, zIndex: 1 } }, + React.createElement(Button, { onClick: function () { setView('customerSummary'); } }, '返回上一步') + ), + React.createElement('div', { style: { textAlign: 'center', padding: '0 128px' } }, + React.createElement('div', { style: { fontSize: 18, fontWeight: 700, color: 'rgba(15,23,42,0.92)', letterSpacing: '0.02em' } }, projectPageMainTitle), + React.createElement('div', { style: { marginTop: 6, fontSize: 13, color: 'rgba(15,23,42,0.55)', fontWeight: 500 } }, projectSumSubline) + ) + ), + React.createElement('div', { className: 'biz-standbook-table-wrap' }, + React.createElement(Table, { + className: 'biz-standbook-table', bordered: true, rowKey: 'key', columns: columns, dataSource: projectSummaryPayload, pagination: false, scroll: { x: 'max-content', y: 'calc(100vh - 250px)' }, sticky: true, + summary: function () { + return React.createElement(Table.Summary, { fixed: 'bottom' }, + React.createElement(Table.Summary.Row, { className: 'row-total' }, + React.createElement(Table.Summary.Cell, { index: 0, align: 'center' }, '合计'), + React.createElement(Table.Summary.Cell, { index: 1, align: 'center' }, + React.createElement('button', { + className: 'biz-standbook-perf-link', + onClick: function () { + openProjectDetail({ isAll: true, sourceRows: projectSummaryPayload }); + } + }, projectCount) + ), + React.createElement(Table.Summary.Cell, { index: 2, align: 'right' }, fmtLedgerCell(sum1)), + React.createElement(Table.Summary.Cell, { index: 3, align: 'right' }, fmtLedgerCell(sum2)), + React.createElement(Table.Summary.Cell, { index: 4, align: 'right' }, fmtLedgerCell(sum3)) + ) + ); + } + }) + ) + ); + }; + + var renderSalesView = function () { + var isCost = salesModal.cellType === 'Cost'; + var typeLabel = '业绩'; + if (isCost) typeLabel = '成本'; + else if (salesModal.cellType === 'Profit') typeLabel = '利润'; + var yearStr = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : 'YYYY'; + var monthStr = salesModal.month === 13 ? '全年' : '-' + pad2(salesModal.month); + var titleText = '租赁业务' + typeLabel + '钻取明细(' + yearStr + monthStr + ')'; + if (detailProject) { + if (detailProject.isAll) { + titleText += ' - 全部项目'; + } else { + titleText += ' - ' + detailProject.customerName + ' - ' + detailProject.projectName; + } + } + return React.createElement(React.Fragment, null, + React.createElement(Card, { style: filterCardStyle, bodyStyle: { paddingBottom: 4 } }, + React.createElement(Row, { gutter: [16, 16], align: 'bottom' }, + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '业务员'), + React.createElement(Select, { + placeholder: '请选择业务员', + style: filterControlStyle, + value: drillFilterDraft.salesperson, + onChange: function (v) { setDrillFilterDraft(Object.assign({}, drillFilterDraft, { salesperson: v })); }, + allowClear: true, + showSearch: true, + filterOption: filterOption, + options: drillPayload.options ? drillPayload.options.salespersons : [] + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '客户名称'), + React.createElement(Select, { + placeholder: '请选择客户名称', + style: filterControlStyle, + value: drillFilterDraft.customerName, + onChange: function (v) { setDrillFilterDraft(Object.assign({}, drillFilterDraft, { customerName: v })); }, + allowClear: true, + showSearch: true, + filterOption: filterOption, + options: drillPayload.options ? drillPayload.options.customers : [] + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '性质'), + React.createElement(Select, { + placeholder: '请选择性质', + style: filterControlStyle, + value: drillFilterDraft.nature, + onChange: function (v) { setDrillFilterDraft(Object.assign({}, drillFilterDraft, { nature: v })); }, + allowClear: true, + options: [ + { value: '租赁', label: '租赁' }, + { value: '试用', label: '试用' } + ] + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '车牌号'), + React.createElement(Select, { + placeholder: '请选择或输入车牌号', + style: filterControlStyle, + value: drillFilterDraft.plateNo, + onChange: function (v) { setDrillFilterDraft(Object.assign({}, drillFilterDraft, { plateNo: v })); }, + allowClear: true, + showSearch: true, + filterOption: filterOption, + options: drillPayload.options ? drillPayload.options.plateNos : [] + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '付款日期'), + React.createElement(RangePicker, { + placeholder: ['开始日期', '结束日期'], + style: filterControlStyle, + value: drillFilterDraft.paymentDate, + onChange: function (v) { setDrillFilterDraft(Object.assign({}, drillFilterDraft, { paymentDate: v })); } + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6, style: filterActionsColStyle }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '\u00a0'), + React.createElement(Space, { wrap: true }, + React.createElement(Button, { onClick: handleDrillReset }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleDrillQuery }, '查询') + ) + ) + ) + ) + ), + React.createElement(Card, { style: tableCardStyle, bodyStyle: { padding: '16px 24px' } }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', marginBottom: 20 } }, + React.createElement(Button, { + onClick: function () { setView('projectSummary'); }, + style: { marginRight: 16 } + }, '返回上一步'), + React.createElement('div', { style: { fontSize: 18, fontWeight: 700, color: '#0f172a' } }, titleText) + ), + React.createElement('div', { className: 'biz-standbook-table-wrap' }, + React.createElement(Table, { + className: 'biz-standbook-table', + size: 'small', + bordered: true, + rowKey: 'key', + columns: drillColumns, + dataSource: drillPayload.rows, + pagination: false, + scroll: { x: 'max-content', y: 'calc(100vh - 250px)' }, + sticky: true, + summary: function () { + var totals = drillPayload.totals; + var Sm = Table.Summary; + var Row = Sm.Row; + var Cell = Sm.Cell; + + return React.createElement(Sm, null, + React.createElement(Row, null, + React.createElement(Cell, { index: 0, colSpan: 11, align: 'center' }, '合计'), + isCost ? React.createElement( + React.Fragment, + null, + React.createElement(Cell, { index: 1, align: 'right' }, fmtLedgerCell(totals.vehicleCost)), + React.createElement(Cell, { index: 2, align: 'right' }, fmtLedgerCell(totals.brokerageFee)), + React.createElement(Cell, { index: 3, align: 'right' }, fmtLedgerCell(totals.totalCost)) + ) : React.createElement( + React.Fragment, + null, + React.createElement(Cell, { index: 1, align: 'right' }, fmtLedgerCell(totals.receivable)), + React.createElement(Cell, { index: 2, align: 'right' }, fmtLedgerCell(totals.received)), + React.createElement(Cell, { index: 3, align: 'right' }, React.createElement('span', { style: { color: '#f53f3f' } }, fmtLedgerCell(totals.unreceived))), + React.createElement(Cell, { index: 4, align: 'right' }, fmtLedgerCell(totals.naturalMonthIncome)), + React.createElement(Cell, { index: 5, colSpan: 3 }) + ) + ) + ); + } + }) + ) + ) + ); + }; + + var renderSalesDetailView = function () { + var deptLabel = salesDetailModal.deptName === '全部' ? '全部业务部门' : salesDetailModal.deptName; + var personLabel = salesDetailModal.salesperson === '全部' ? '全部业务人员' : salesDetailModal.salesperson; + var titleText = '应收明细(' + salesDetailModal.ym + ' ' + deptLabel + ' ' + personLabel + ')'; + return React.createElement(Card, { style: tableCardStyle, bodyStyle: { padding: '16px 24px' } }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', marginBottom: 20 } }, + React.createElement(Button, { + onClick: function () { setView('sales'); }, + style: { marginRight: 16 } + }, '返回上一步'), + React.createElement('div', { style: { fontSize: 18, fontWeight: 700, color: '#0f172a' } }, titleText) + ), + React.createElement('div', { className: 'biz-standbook-table-wrap' }, + React.createElement(Table, { + className: 'biz-standbook-table', + size: 'small', + bordered: true, + rowKey: 'key', + columns: detailColumns, + dataSource: detailPayload.rows, + pagination: false, + rowClassName: rowClassName, + scroll: { x: 'max-content', y: 'calc(100vh - 250px)' }, + summary: function () { + var totals = detailPayload.totals; + var Sm = Table.Summary; + var Row = Sm.Row; + var Cell = Sm.Cell; + return React.createElement(Sm, null, + React.createElement(Row, null, + React.createElement(Cell, { index: 0, colSpan: 13, align: 'center' }, '合计'), + React.createElement(Cell, { index: 13, align: 'right' }, fmtLedgerCell(totals.deposit)), + React.createElement(Cell, { index: 14, align: 'right' }, fmtLedgerCell(totals.vehicle)), + React.createElement(Cell, { index: 15, align: 'right' }, fmtLedgerCell(totals.additional)), + React.createElement(Cell, { index: 16, align: 'right' }, fmtLedgerCell(totals.total)) + ) + ); + } + }) + ) + ); + }; + + var renderContent = function () { + if (view === 'main') return renderMainView(); + if (view === 'hydrogenCustomerPerf') return renderHydrogenCustomerPerfView(); + if (view === 'hydrogenStationDrill') return renderHydrogenStationDrillView(); + if (view === 'hydrogenRefuelDetail') return renderHydrogenRefuelDetailView(); + if (view === 'customerSummary') return renderCustomerSummaryView(); + if (view === 'projectSummary') return renderProjectSummaryView(); + if (view === 'sales') return renderSalesView(); + if (view === 'sales_detail') return renderSalesDetailView(); + return null; + }; + + var bodyEl = renderContent(); + + return React.createElement(App, null, + React.createElement('style', null, ledgerTableStyle), + React.createElement('div', { style: layoutStyle }, + bodyEl + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-data-analysis/pages/06-业务部业绩明细.jsx b/src/prototypes/oneos-web-data-analysis/pages/06-业务部业绩明细.jsx new file mode 100644 index 0000000..9ae4170 --- /dev/null +++ b/src/prototypes/oneos-web-data-analysis/pages/06-业务部业绩明细.jsx @@ -0,0 +1,814 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 数据分析 - 业务部业绩(明细 + 业绩/成本/利润汇总 Tab) +// 明细:进入页面默认统计「上一自然月」;跨月后重新进入或刷新页面,默认月份随系统日期更新为上一个月。 + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + + var antd = window.antd; + var App = antd.App; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Button = antd.Button; + var Table = antd.Table; + var Select = antd.Select; + var DatePicker = antd.DatePicker; + var Row = antd.Row; + var Col = antd.Col; + var Tabs = antd.Tabs; + var Space = antd.Space; + var message = antd.message; + + var TableSummary = Table.Summary; + var SummaryRow = TableSummary.Row; + var SummaryCell = TableSummary.Cell; + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + /** 表格正文:千分位 + 两位小数;空或 0 显示 -(利润等可为负,仍正常显示) */ + function fmtCell(n) { + if (n === null || n === undefined || n === '') return '-'; + var x = Number(n); + if (isNaN(x)) return '-'; + if (x === 0) return '-'; + return x.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + } + + /** 总计行 */ + function fmtSum(n) { + var x = Number(n); + if (isNaN(x)) return '-'; + return x.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 6 }); + } + + function escapeCsv(v) { + var s = v == null ? '' : String(v); + if (s.indexOf(',') !== -1 || s.indexOf('"') !== -1 || s.indexOf('\n') !== -1 || s.indexOf('\r') !== -1) { + return '"' + s.replace(/"/g, '""') + '"'; + } + return s; + } + + function downloadCsv(filename, lines) { + var csv = lines.map(function (row) { return row.map(escapeCsv).join(','); }).join('\n'); + var blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' }); + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); + } + + /** 统计月份键 YYYY-MM(上一自然月;每月初「上月」随系统日期变化,刷新/重新进入页面即更新) */ + function getLastMonthStatKey() { + try { + if (window.dayjs) return window.dayjs().subtract(1, 'month').format('YYYY-MM'); + } catch (e1) {} + var d = new Date(); + d.setDate(1); + d.setMonth(d.getMonth() - 1); + var y = d.getFullYear(); + var mo = d.getMonth() + 1; + return y + '-' + (mo < 10 ? '0' + mo : '' + mo); + } + + /** 月份选择器默认值(与 getLastMonthStatKey 同一自然月) */ + function getLastMonthPickerValue() { + try { + if (window.dayjs) return window.dayjs().subtract(1, 'month').startOf('month'); + } catch (e1) {} + try { + if (window.moment) return window.moment().subtract(1, 'month').startOf('month'); + } catch (e2) {} + return null; + } + + var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var filterLabelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var filterItemStyle = { marginBottom: 12 }; + var filterControlStyle = { width: '100%' }; + /** 与筛选项同一行时,将操作列推到右侧 */ + var filterActionsColStyle = { flex: '0 0 auto', marginLeft: 'auto' }; + var tableSingleLineStyle = + '.biz-dept-perf-table .ant-table-thead th,.biz-dept-perf-table .ant-table-tbody td,.biz-dept-perf-table .ant-table-summary td{white-space:nowrap;}'; + var tabsBarStyle = '.biz-dept-perf-tabs .ant-tabs-nav{margin-bottom:0;}'; + + var monthlyMetricKeys = ['logistics', 'lease', 'sales', 'hydrogen', 'electricity', 'etc', 'other', 'total']; + + function sumMonthlyRows(rows, keys) { + var sums = {}; + keys.forEach(function (k) { + sums[k] = (rows || []).reduce(function (acc, row) { + var v = row[k]; + var n = v === null || v === undefined || v === '' ? 0 : Number(v); + return acc + (isNaN(n) ? 0 : n); + }, 0); + }); + return sums; + } + + function monthlyColumnsFor(suffix) { + if (suffix === '业绩') { + return [ + { title: '月份', dataIndex: 'month', key: 'month', width: 72, align: 'center' }, + { title: '物流业绩', dataIndex: 'logistics', key: 'logistics', width: 120, align: 'center', render: function (v) { return fmtCell(v); } }, + { title: '租赁业绩', dataIndex: 'lease', key: 'lease', width: 120, align: 'center', render: function (v) { return fmtCell(v); } }, + { title: '销售业绩', dataIndex: 'sales', key: 'sales', width: 120, align: 'center', render: function (v) { return fmtCell(v); } }, + { title: '氢费业绩', dataIndex: 'hydrogen', key: 'hydrogen', width: 120, align: 'center', render: function (v) { return fmtCell(v); } }, + { title: '电费业绩', dataIndex: 'electricity', key: 'electricity', width: 120, align: 'center', render: function (v) { return fmtCell(v); } }, + { title: 'ETC业绩', dataIndex: 'etc', key: 'etc', width: 110, align: 'center', render: function (v) { return fmtCell(v); } }, + { title: '其他', dataIndex: 'other', key: 'other', width: 110, align: 'center', render: function (v) { return fmtCell(v); } }, + { title: '合计', dataIndex: 'total', key: 'total', width: 120, align: 'center', render: function (v) { return fmtCell(v); } } + ]; + } + if (suffix === '成本') { + return [ + { title: '月份', dataIndex: 'month', key: 'month', width: 72, align: 'center' }, + { title: '物流成本', dataIndex: 'logistics', key: 'logistics', width: 120, align: 'center', render: function (v) { return fmtCell(v); } }, + { title: '租赁成本', dataIndex: 'lease', key: 'lease', width: 120, align: 'center', render: function (v) { return fmtCell(v); } }, + { title: '销售成本', dataIndex: 'sales', key: 'sales', width: 120, align: 'center', render: function (v) { return fmtCell(v); } }, + { title: '氢费成本', dataIndex: 'hydrogen', key: 'hydrogen', width: 120, align: 'center', render: function (v) { return fmtCell(v); } }, + { title: '电费成本', dataIndex: 'electricity', key: 'electricity', width: 120, align: 'center', render: function (v) { return fmtCell(v); } }, + { title: 'ETC成本', dataIndex: 'etc', key: 'etc', width: 110, align: 'center', render: function (v) { return fmtCell(v); } }, + { title: '其他', dataIndex: 'other', key: 'other', width: 110, align: 'center', render: function (v) { return fmtCell(v); } }, + { title: '合计', dataIndex: 'total', key: 'total', width: 120, align: 'center', render: function (v) { return fmtCell(v); } } + ]; + } + return [ + { title: '月份', dataIndex: 'month', key: 'month', width: 72, align: 'center' }, + { title: '物流利润', dataIndex: 'logistics', key: 'logistics', width: 120, align: 'center', render: function (v) { return fmtCell(v); } }, + { title: '租赁利润', dataIndex: 'lease', key: 'lease', width: 120, align: 'center', render: function (v) { return fmtCell(v); } }, + { title: '销售利润', dataIndex: 'sales', key: 'sales', width: 120, align: 'center', render: function (v) { return fmtCell(v); } }, + { title: '氢费利润', dataIndex: 'hydrogen', key: 'hydrogen', width: 120, align: 'center', render: function (v) { return fmtCell(v); } }, + { title: '电费利润', dataIndex: 'electricity', key: 'electricity', width: 120, align: 'center', render: function (v) { return fmtCell(v); } }, + { title: 'ETC利润', dataIndex: 'etc', key: 'etc', width: 110, align: 'center', render: function (v) { return fmtCell(v); } }, + { title: '其他', dataIndex: 'other', key: 'other', width: 110, align: 'center', render: function (v) { return fmtCell(v); } }, + { title: '合计', dataIndex: 'total', key: 'total', width: 120, align: 'center', render: function (v) { return fmtCell(v); } } + ]; + } + + function rowTotal(r) { + var keys = ['logistics', 'lease', 'sales', 'hydrogen', 'electricity', 'etc', 'other']; + var s = 0; + var any = false; + keys.forEach(function (k) { + var v = r[k]; + if (v === null || v === undefined || v === '') return; + var n = Number(v); + if (isNaN(n)) return; + any = true; + s += n; + }); + return any ? s : null; + } + + function finalizeMonthlyRow(r) { + var t = r.total != null && r.total !== '' ? r.total : rowTotal(r); + return Object.assign({}, r, { total: t }); + } + + /** 原型:2026 年按月业绩汇总 */ + var monthlyPerf2026 = useMemo(function () { + var raw = [ + { month: 1, logistics: 1005557.94, lease: 4004.73, sales: 1495355.18, hydrogen: null, electricity: 88200.5, etc: 12000, other: 5000, total: null }, + { month: 2, logistics: 250000, lease: 180000, sales: null, hydrogen: 3200, electricity: null, etc: null, other: null, total: null }, + { month: 3, logistics: 180000, lease: 95000, sales: 220000, hydrogen: null, electricity: 45000, etc: 8000, other: 1200, total: null }, + { month: 4, logistics: null, lease: null, sales: null, hydrogen: null, electricity: null, etc: null, other: null, total: null }, + { month: 5, logistics: null, lease: null, sales: null, hydrogen: null, electricity: null, etc: null, other: null, total: null }, + { month: 6, logistics: null, lease: null, sales: null, hydrogen: null, electricity: null, etc: null, other: null, total: null }, + { month: 7, logistics: null, lease: null, sales: null, hydrogen: null, electricity: null, etc: null, other: null, total: null }, + { month: 8, logistics: null, lease: null, sales: null, hydrogen: null, electricity: null, etc: null, other: null, total: null }, + { month: 9, logistics: null, lease: null, sales: null, hydrogen: null, electricity: null, etc: null, other: null, total: null }, + { month: 10, logistics: null, lease: null, sales: null, hydrogen: null, electricity: null, etc: null, other: null, total: null }, + { month: 11, logistics: null, lease: null, sales: null, hydrogen: null, electricity: null, etc: null, other: null, total: null }, + { month: 12, logistics: null, lease: null, sales: null, hydrogen: null, electricity: null, etc: null, other: null, total: null } + ]; + return raw.map(function (x) { + return finalizeMonthlyRow(Object.assign({ key: 'perf-' + x.month }, x)); + }); + }, []); + + /** 原型:2026 年按月成本汇总 */ + var monthlyCost2026 = useMemo(function () { + var raw = [ + { month: 1, logistics: 1167787.05, lease: 320000, sales: 980000, hydrogen: 15000, electricity: 72000, etc: 9800, other: 2400, total: null }, + { month: 2, logistics: 210000, lease: 165000, sales: null, hydrogen: 2800, electricity: null, etc: null, other: null, total: null }, + { month: 3, logistics: 155000, lease: 88000, sales: 195000, hydrogen: null, electricity: 38000, etc: 6500, other: 800, total: null }, + { month: 4, logistics: null, lease: null, sales: null, hydrogen: null, electricity: null, etc: null, other: null, total: null }, + { month: 5, logistics: null, lease: null, sales: null, hydrogen: null, electricity: null, etc: null, other: null, total: null }, + { month: 6, logistics: null, lease: null, sales: null, hydrogen: null, electricity: null, etc: null, other: null, total: null }, + { month: 7, logistics: null, lease: null, sales: null, hydrogen: null, electricity: null, etc: null, other: null, total: null }, + { month: 8, logistics: null, lease: null, sales: null, hydrogen: null, electricity: null, etc: null, other: null, total: null }, + { month: 9, logistics: null, lease: null, sales: null, hydrogen: null, electricity: null, etc: null, other: null, total: null }, + { month: 10, logistics: null, lease: null, sales: null, hydrogen: null, electricity: null, etc: null, other: null, total: null }, + { month: 11, logistics: null, lease: null, sales: null, hydrogen: null, electricity: null, etc: null, other: null, total: null }, + { month: 12, logistics: null, lease: null, sales: null, hydrogen: null, electricity: null, etc: null, other: null, total: null } + ]; + return raw.map(function (x) { + return finalizeMonthlyRow(Object.assign({ key: 'cost-' + x.month }, x)); + }); + }, []); + + /** 原型:2026 年按月利润汇总(可为负) */ + var monthlyProfit2026 = useMemo(function () { + var raw = [ + { month: 1, logistics: -162229.11, lease: 45000, sales: null, hydrogen: -1200.5, electricity: 6200, etc: null, other: 800, total: null }, + { month: 2, logistics: null, lease: 15000, sales: null, hydrogen: null, electricity: null, etc: null, other: null, total: null }, + { month: 3, logistics: null, lease: 7000, sales: null, hydrogen: null, electricity: null, etc: null, other: null, total: null }, + { month: 4, logistics: null, lease: null, sales: null, hydrogen: null, electricity: null, etc: null, other: null, total: null }, + { month: 5, logistics: null, lease: null, sales: null, hydrogen: null, electricity: null, etc: null, other: null, total: null }, + { month: 6, logistics: null, lease: null, sales: null, hydrogen: null, electricity: null, etc: null, other: null, total: null }, + { month: 7, logistics: null, lease: null, sales: null, hydrogen: null, electricity: null, etc: null, other: null, total: null }, + { month: 8, logistics: null, lease: null, sales: null, hydrogen: null, electricity: null, etc: null, other: null, total: null }, + { month: 9, logistics: null, lease: null, sales: null, hydrogen: null, electricity: null, etc: null, other: null, total: null }, + { month: 10, logistics: null, lease: null, sales: null, hydrogen: null, electricity: null, etc: null, other: null, total: null }, + { month: 11, logistics: null, lease: null, sales: null, hydrogen: null, electricity: null, etc: null, other: null, total: null }, + { month: 12, logistics: null, lease: null, sales: null, hydrogen: null, electricity: null, etc: null, other: null, total: null } + ]; + return raw.map(function (x) { + return finalizeMonthlyRow(Object.assign({ key: 'profit-' + x.month }, x)); + }); + }, []); + + var deptOptions = useMemo(function () { + return [ + { value: '华东业务部', label: '华东业务部' }, + { value: '华南业务部', label: '华南业务部' }, + { value: '华北业务部', label: '华北业务部' }, + { value: '西南业务部', label: '西南业务部' } + ]; + }, []); + + var salespersonOptions = useMemo(function () { + return [ + { value: '尚建华', label: '尚建华' }, + { value: '刘念念', label: '刘念念' }, + { value: '谯云', label: '谯云' }, + { value: '董剑煜', label: '董剑煜' } + ]; + }, []); + + var lastMonthStatKey = useMemo(function () { + return getLastMonthStatKey(); + }, []); + + var allRows = useMemo(function () { + var sm = lastMonthStatKey; + return [ + { + key: '1', + statMonth: sm, + salesperson: '尚建华', + dept: '华东业务部', + logistics: 208868.38, + lease: 36000, + sales: null, + hydrogen: null, + electricity: null, + etc: null, + other: 55950.23, + total: 300818.61 + }, + { + key: '2', + statMonth: sm, + salesperson: '刘念念', + dept: '华南业务部', + logistics: 397181.78, + lease: 223800, + sales: 120000, + hydrogen: 4500.12, + electricity: 25000, + etc: null, + other: 224.14, + total: 770706.04 + }, + { + key: '3', + statMonth: sm, + salesperson: '谯云', + dept: '华东业务部', + logistics: 391153.33, + lease: 15000, + sales: null, + hydrogen: null, + electricity: null, + etc: 8665.43, + other: null, + total: 414818.76 + }, + { + key: '4', + statMonth: sm, + salesperson: '董剑煜', + dept: '西南业务部', + logistics: 8354.46, + lease: null, + sales: null, + hydrogen: null, + electricity: null, + etc: null, + other: 657.3, + total: 9011.76 + } + ]; + }, [lastMonthStatKey]); + + var mainTabState = useState('detail'); + var mainTab = mainTabState[0]; + var setMainTab = mainTabState[1]; + + var draftState = useState(function () { + return { + month: getLastMonthPickerValue(), + dept: undefined, + salesperson: undefined + }; + }); + var draft = draftState[0]; + var setDraft = draftState[1]; + + var appliedState = useState(function () { + return { + month: getLastMonthPickerValue(), + dept: undefined, + salesperson: undefined + }; + }); + var applied = appliedState[0]; + var setApplied = appliedState[1]; + + function initialYear2026() { + try { + if (window.dayjs) return window.dayjs('2026-01-01'); + } catch (e1) {} + return null; + } + + var summaryYearDraftState = useState(initialYear2026); + var summaryYearDraft = summaryYearDraftState[0]; + var setSummaryYearDraft = summaryYearDraftState[1]; + + var summaryYearAppliedState = useState(initialYear2026); + var summaryYearApplied = summaryYearAppliedState[0]; + var setSummaryYearApplied = summaryYearAppliedState[1]; + + var filteredRows = useMemo(function () { + return (allRows || []).filter(function (r) { + if (applied.month && applied.month.format) { + var mk = applied.month.format('YYYY-MM'); + if (r.statMonth !== mk) return false; + } + if (applied.dept && r.dept !== applied.dept) return false; + if (applied.salesperson && r.salesperson !== applied.salesperson) return false; + return true; + }); + }, [allRows, applied.month, applied.dept, applied.salesperson]); + + var metricKeys = useMemo(function () { + return ['logistics', 'lease', 'sales', 'hydrogen', 'electricity', 'etc', 'other', 'total']; + }, []); + + var columnSums = useMemo(function () { + var sums = {}; + metricKeys.forEach(function (k) { + sums[k] = filteredRows.reduce(function (acc, row) { + var v = row[k]; + var n = v === null || v === undefined || v === '' ? 0 : Number(v); + return acc + (isNaN(n) ? 0 : n); + }, 0); + }); + return sums; + }, [filteredRows, metricKeys]); + + var bizReportTableTitleStyle = { + textAlign: 'center', + marginBottom: 16, + fontSize: 16, + fontWeight: 600, + color: 'rgba(0,0,0,0.88)' + }; + + /** 与「查询」后已选月份一致:x年x月业绩明细表 */ + var detailTableTitle = useMemo(function () { + if (applied.month && applied.month.format) { + var y = applied.month.format('YYYY'); + var m = applied.month.format('M'); + return y + '年' + m + '月业绩明细表'; + } + return '业绩明细表'; + }, [applied.month]); + + /** 与汇总 Tab「查询」后已选年份一致 */ + var perfSumTableTitle = useMemo(function () { + if (summaryYearApplied && summaryYearApplied.format) { + return summaryYearApplied.format('YYYY') + '年业绩汇总'; + } + return '业绩汇总'; + }, [summaryYearApplied]); + + var costSumTableTitle = useMemo(function () { + if (summaryYearApplied && summaryYearApplied.format) { + return summaryYearApplied.format('YYYY') + '年成本汇总'; + } + return '成本汇总'; + }, [summaryYearApplied]); + + var profitSumTableTitle = useMemo(function () { + if (summaryYearApplied && summaryYearApplied.format) { + return summaryYearApplied.format('YYYY') + '年利润汇总'; + } + return '利润汇总'; + }, [summaryYearApplied]); + + var monthlyDataSource = useCallback(function (rows) { + var y = summaryYearApplied && summaryYearApplied.format ? summaryYearApplied.format('YYYY') : null; + if (!y) return []; + if (y !== '2026') return []; + return rows; + }, [summaryYearApplied]); + + var perfMonthlyRows = useMemo(function () { return monthlyDataSource(monthlyPerf2026); }, [monthlyDataSource, monthlyPerf2026]); + var costMonthlyRows = useMemo(function () { return monthlyDataSource(monthlyCost2026); }, [monthlyDataSource, monthlyCost2026]); + var profitMonthlyRows = useMemo(function () { return monthlyDataSource(monthlyProfit2026); }, [monthlyDataSource, monthlyProfit2026]); + + var perfMonthlySums = useMemo(function () { return sumMonthlyRows(perfMonthlyRows, monthlyMetricKeys); }, [perfMonthlyRows]); + var costMonthlySums = useMemo(function () { return sumMonthlyRows(costMonthlyRows, monthlyMetricKeys); }, [costMonthlyRows]); + var profitMonthlySums = useMemo(function () { return sumMonthlyRows(profitMonthlyRows, monthlyMetricKeys); }, [profitMonthlyRows]); + + var handleQuery = useCallback(function () { + setApplied(Object.assign({}, draft)); + }, [draft]); + + var handleReset = useCallback(function () { + var def = { month: getLastMonthPickerValue(), dept: undefined, salesperson: undefined }; + setDraft(def); + setApplied(def); + }, []); + + var handleSummaryQuery = useCallback(function () { + setSummaryYearApplied(summaryYearDraft); + }, [summaryYearDraft]); + + var handleSummaryReset = useCallback(function () { + var y0 = initialYear2026(); + setSummaryYearDraft(y0); + setSummaryYearApplied(y0); + }, []); + + var handleExportDetail = useCallback(function () { + var rows = filteredRows; + if (!rows || rows.length === 0) { + message.warning('当前无数据可导出'); + return; + } + var headers = [ + '业务员', + '物流业绩', + '租赁业绩', + '销售业绩', + '氢费业绩', + '电费业绩', + 'ETC业绩', + '其他', + '合计' + ]; + var body = [headers].concat( + rows.map(function (r) { + return [ + r.salesperson, + fmtCell(r.logistics), + fmtCell(r.lease), + fmtCell(r.sales), + fmtCell(r.hydrogen), + fmtCell(r.electricity), + fmtCell(r.etc), + fmtCell(r.other), + fmtCell(r.total) + ]; + }) + ); + body.push(['总计'].concat(metricKeys.map(function (k) { return fmtSum(columnSums[k] || 0); }))); + downloadCsv('业绩明细_' + new Date().getTime() + '.csv', body); + message.success('已导出 ' + rows.length + ' 条记录'); + }, [filteredRows, metricKeys, columnSums]); + + var exportMonthly = useCallback(function (rows, sums, headerTitles, filePrefix) { + if (!rows || rows.length === 0) { + message.warning('当前无数据可导出,请先选择年份并查询'); + return; + } + var body = [headerTitles].concat( + rows.map(function (r) { + return [ + String(r.month), + fmtCell(r.logistics), + fmtCell(r.lease), + fmtCell(r.sales), + fmtCell(r.hydrogen), + fmtCell(r.electricity), + fmtCell(r.etc), + fmtCell(r.other), + fmtCell(r.total) + ]; + }) + ); + body.push(['总计'].concat(monthlyMetricKeys.map(function (k) { return fmtSum(sums[k] || 0); }))); + downloadCsv(filePrefix + '_' + new Date().getTime() + '.csv', body); + message.success('已导出 ' + rows.length + ' 个月度数据'); + }, []); + + var handleExportPerfMonthly = useCallback(function () { + exportMonthly( + perfMonthlyRows, + perfMonthlySums, + ['月份', '物流业绩', '租赁业绩', '销售业绩', '氢费业绩', '电费业绩', 'ETC业绩', '其他', '合计'], + '业绩汇总' + ); + }, [exportMonthly, perfMonthlyRows, perfMonthlySums]); + + var handleExportCostMonthly = useCallback(function () { + exportMonthly( + costMonthlyRows, + costMonthlySums, + ['月份', '物流成本', '租赁成本', '销售成本', '氢费成本', '电费成本', 'ETC成本', '其他', '合计'], + '成本汇总' + ); + }, [exportMonthly, costMonthlyRows, costMonthlySums]); + + var handleExportProfitMonthly = useCallback(function () { + exportMonthly( + profitMonthlyRows, + profitMonthlySums, + ['月份', '物流利润', '租赁利润', '销售利润', '氢费利润', '电费利润', 'ETC利润', '其他', '合计'], + '利润汇总' + ); + }, [exportMonthly, profitMonthlyRows, profitMonthlySums]); + + var columns = useMemo(function () { + return [ + { title: '业务员', dataIndex: 'salesperson', key: 'salesperson', width: 110, align: 'center' }, + { title: '物流业绩', dataIndex: 'logistics', key: 'logistics', width: 130, align: 'center', render: function (v) { return fmtCell(v); } }, + { title: '租赁业绩', dataIndex: 'lease', key: 'lease', width: 130, align: 'center', render: function (v) { return fmtCell(v); } }, + { title: '销售业绩', dataIndex: 'sales', key: 'sales', width: 130, align: 'center', render: function (v) { return fmtCell(v); } }, + { title: '氢费业绩', dataIndex: 'hydrogen', key: 'hydrogen', width: 130, align: 'center', render: function (v) { return fmtCell(v); } }, + { title: '电费业绩', dataIndex: 'electricity', key: 'electricity', width: 140, align: 'center', render: function (v) { return fmtCell(v); } }, + { title: 'ETC业绩', dataIndex: 'etc', key: 'etc', width: 120, align: 'center', render: function (v) { return fmtCell(v); } }, + { title: '其他', dataIndex: 'other', key: 'other', width: 120, align: 'center', render: function (v) { return fmtCell(v); } }, + { title: '合计', dataIndex: 'total', key: 'total', width: 130, align: 'center', render: function (v) { return fmtCell(v); } } + ]; + }, []); + + var tableSummary = useCallback(function () { + return React.createElement( + TableSummary, + null, + React.createElement( + SummaryRow, + null, + React.createElement(SummaryCell, { index: 0, align: 'center' }, '总计'), + metricKeys.map(function (k, idx) { + return React.createElement( + SummaryCell, + { key: k, index: idx + 1, align: 'center' }, + fmtSum(columnSums[k] || 0) + ); + }) + ) + ); + }, [metricKeys, columnSums]); + + function renderMonthlySummary(sums) { + return function () { + return React.createElement( + TableSummary, + null, + React.createElement( + SummaryRow, + null, + React.createElement(SummaryCell, { index: 0, align: 'center' }, '总计'), + monthlyMetricKeys.map(function (k, idx) { + return React.createElement( + SummaryCell, + { key: k, index: idx + 1, align: 'center' }, + fmtSum(sums[k] || 0) + ); + }) + ) + ); + }; + } + + var columnsPerfMonthly = useMemo(function () { return monthlyColumnsFor('业绩'); }, []); + var columnsCostMonthly = useMemo(function () { return monthlyColumnsFor('成本'); }, []); + var columnsProfitMonthly = useMemo(function () { return monthlyColumnsFor('利润'); }, []); + + var renderYearFilterCard = useCallback(function () { + return React.createElement(Card, { style: { marginBottom: 16 } }, + React.createElement(Row, { gutter: [16, 16], align: 'bottom' }, + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '选择年份'), + React.createElement(DatePicker, { + picker: 'year', + style: filterControlStyle, + placeholder: '请选择统计年份', + format: 'YYYY', + value: summaryYearDraft, + onChange: function (v) { setSummaryYearDraft(v); } + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6, style: filterActionsColStyle }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '\u00a0'), + React.createElement(Space, { size: 8, wrap: true }, + React.createElement(Button, { onClick: handleSummaryReset }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleSummaryQuery }, '查询') + ) + ) + ) + ) + ); + }, [summaryYearDraft, handleSummaryReset, handleSummaryQuery]); + + var tabItems = [ + { + key: 'detail', + label: '业绩明细', + children: React.createElement(React.Fragment, null, + React.createElement(Card, { style: { marginBottom: 16 } }, + React.createElement(Row, { gutter: [16, 16], align: 'bottom' }, + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '选择月份'), + React.createElement(DatePicker, { + picker: 'month', + style: filterControlStyle, + placeholder: '选择器选择月份', + format: 'YYYY-MM', + value: draft.month, + onChange: function (v) { setDraft(function (p) { return Object.assign({}, p, { month: v }); }); } + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '选择业务部门'), + React.createElement(Select, { + placeholder: '选择器选择业务部门', + style: filterControlStyle, + allowClear: true, + options: deptOptions, + value: draft.dept, + onChange: function (v) { setDraft(function (p) { return Object.assign({}, p, { dept: v }); }); }, + showSearch: true, + filterOption: filterOption + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '选择业务员'), + React.createElement(Select, { + placeholder: '选择器选择业务员', + style: filterControlStyle, + allowClear: true, + options: salespersonOptions, + value: draft.salesperson, + onChange: function (v) { setDraft(function (p) { return Object.assign({}, p, { salesperson: v }); }); }, + showSearch: true, + filterOption: filterOption + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6, style: filterActionsColStyle }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '\u00a0'), + React.createElement(Space, { size: 8, wrap: true }, + React.createElement(Button, { onClick: handleReset }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleQuery }, '查询') + ) + ) + ) + ) + ), + React.createElement(Card, { + extra: React.createElement(Button, { onClick: handleExportDetail }, '导出') + }, + React.createElement('div', { style: bizReportTableTitleStyle }, detailTableTitle), + React.createElement('style', null, tableSingleLineStyle), + React.createElement('div', { className: 'biz-dept-perf-table' }, + React.createElement(Table, { + rowKey: 'key', + columns: columns, + dataSource: filteredRows, + pagination: false, + size: 'small', + summary: tableSummary, + scroll: { x: 1200 } + }) + ) + ) + ) + }, + { + key: 'perfSum', + label: '业绩汇总', + children: React.createElement(React.Fragment, null, + renderYearFilterCard(), + React.createElement(Card, { + extra: React.createElement(Button, { onClick: handleExportPerfMonthly }, '导出') + }, + React.createElement('div', { style: bizReportTableTitleStyle }, perfSumTableTitle), + React.createElement('style', null, tableSingleLineStyle), + React.createElement('div', { className: 'biz-dept-perf-table' }, + React.createElement(Table, { + rowKey: 'key', + columns: columnsPerfMonthly, + dataSource: perfMonthlyRows, + pagination: false, + size: 'small', + summary: renderMonthlySummary(perfMonthlySums), + scroll: { x: 1180 } + }) + ) + ) + ) + }, + { + key: 'costSum', + label: '成本汇总', + children: React.createElement(React.Fragment, null, + renderYearFilterCard(), + React.createElement(Card, { + extra: React.createElement(Button, { onClick: handleExportCostMonthly }, '导出') + }, + React.createElement('div', { style: bizReportTableTitleStyle }, costSumTableTitle), + React.createElement('style', null, tableSingleLineStyle), + React.createElement('div', { className: 'biz-dept-perf-table' }, + React.createElement(Table, { + rowKey: 'key', + columns: columnsCostMonthly, + dataSource: costMonthlyRows, + pagination: false, + size: 'small', + summary: renderMonthlySummary(costMonthlySums), + scroll: { x: 1180 } + }) + ) + ) + ) + }, + { + key: 'profitSum', + label: '利润汇总', + children: React.createElement(React.Fragment, null, + renderYearFilterCard(), + React.createElement(Card, { + extra: React.createElement(Button, { onClick: handleExportProfitMonthly }, '导出') + }, + React.createElement('div', { style: bizReportTableTitleStyle }, profitSumTableTitle), + React.createElement('style', null, tableSingleLineStyle), + React.createElement('div', { className: 'biz-dept-perf-table' }, + React.createElement(Table, { + rowKey: 'key', + columns: columnsProfitMonthly, + dataSource: profitMonthlyRows, + pagination: false, + size: 'small', + summary: renderMonthlySummary(profitMonthlySums), + scroll: { x: 1180 } + }) + ) + ) + ) + } + ]; + + return React.createElement(App, null, + React.createElement('div', { style: layoutStyle }, + React.createElement(Breadcrumb, { + style: { marginBottom: 16 }, + items: [{ title: '数据分析' }, { title: '业务部业绩' }] + }), + React.createElement(Card, null, + React.createElement('style', null, tabsBarStyle), + React.createElement(Tabs, { + className: 'biz-dept-perf-tabs', + activeKey: mainTab, + onChange: function (k) { setMainTab(k); }, + items: tabItems + }) + ) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-data-analysis/pages/07-业务台账.jsx b/src/prototypes/oneos-web-data-analysis/pages/07-业务台账.jsx new file mode 100644 index 0000000..e51d1cb --- /dev/null +++ b/src/prototypes/oneos-web-data-analysis/pages/07-业务台账.jsx @@ -0,0 +1,841 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 数据分析 - 业务台账(多层级表头:自营/租赁/销售/氢费/电费/ETC 各业绩·成本·利润;其他单列) +// 原型:年份 + 业务部筛选、导出;业绩列可钻取业务员 → 项目明细(联调可替换接口) + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + + var antd = window.antd; + var App = antd.App; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Button = antd.Button; + var Table = antd.Table; + var Select = antd.Select; + var DatePicker = antd.DatePicker; + var Row = antd.Row; + var Col = antd.Col; + var Space = antd.Space; + var Modal = antd.Modal; + var message = antd.message; + + /** 与示意图一致:前 6 类为三列;其他为单列 */ + var TRIPLE_DEFS = [ + { key: 'self', groupTitle: '自营业务', short: '自营' }, + { key: 'lease', groupTitle: '租赁业务', short: '租赁' }, + { key: 'resale', groupTitle: '销售', short: '销售' }, + { key: 'h2', groupTitle: '氢费', short: '氢费' }, + { key: 'apply', groupTitle: '电费', short: '电费' }, + { key: 'etc', groupTitle: 'ETC', short: 'ETC' } + ]; + + var TRIPLE_KEYS = TRIPLE_DEFS.map(function (c) { return c.key; }); + + var ALL_CAT_FOR_DRILL = TRIPLE_DEFS.map(function (c) { + return { key: c.key, label: c.groupTitle }; + }).concat([{ key: 'other', label: '其他' }]); + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + function fmtMoney(n) { + if (n === null || n === undefined || n === '') return '-'; + var x = Number(n); + if (isNaN(x)) return '-'; + if (x === 0) return '-'; + return x.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + } + + function escapeCsv(v) { + var s = v == null ? '' : String(v); + if (s.indexOf(',') !== -1 || s.indexOf('"') !== -1 || s.indexOf('\n') !== -1 || s.indexOf('\r') !== -1) { + return '"' + s.replace(/"/g, '""') + '"'; + } + return s; + } + + function downloadCsv(filename, lines) { + var csv = lines.map(function (row) { return row.map(escapeCsv).join(','); }).join('\n'); + var blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' }); + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); + } + + function initialYear() { + try { + if (window.dayjs) return window.dayjs('2026-01-01'); + } catch (e1) {} + return null; + } + + function numOrZero(v) { + if (v === null || v === undefined || v === '') return 0; + var n = Number(v); + return isNaN(n) ? 0 : n; + } + + function pad2(m) { + var n = Number(m); + if (n < 10) return '0' + n; + return String(n); + } + + /** 将金额(元)均分到 n 行,保证合计精确 */ + function splitAcrossN(total, n) { + if (n <= 0) return []; + var cents = Math.round(numOrZero(total) * 100); + if (cents === 0) { + var z = []; + var j; + for (j = 0; j < n; j++) z.push(0); + return z; + } + var each = Math.floor(cents / n); + var rem = cents % n; + var out = []; + var i; + for (i = 0; i < n; i++) { + out.push((each + (i < rem ? 1 : 0)) / 100); + } + return out; + } + + function sumLedgerRows(monthRows) { + var acc = { month: 13, monthLabel: '合计', rowType: 'total', key: 'total' }; + TRIPLE_KEYS.forEach(function (k) { + acc[k + 'Perf'] = 0; + acc[k + 'Cost'] = 0; + acc[k + 'Profit'] = 0; + }); + acc.otherAmount = 0; + (monthRows || []).forEach(function (r) { + TRIPLE_KEYS.forEach(function (k) { + acc[k + 'Perf'] += numOrZero(r[k + 'Perf']); + acc[k + 'Cost'] += numOrZero(r[k + 'Cost']); + acc[k + 'Profit'] += numOrZero(r[k + 'Profit']); + }); + acc.otherAmount += numOrZero(r.otherAmount); + }); + TRIPLE_KEYS.forEach(function (k) { + if (acc[k + 'Perf'] === 0) acc[k + 'Perf'] = null; + if (acc[k + 'Cost'] === 0) acc[k + 'Cost'] = null; + if (acc[k + 'Profit'] === 0) acc[k + 'Profit'] = null; + }); + if (acc.otherAmount === 0) acc.otherAmount = null; + return acc; + } + + /** 演示数据:2026;1 月氢气/申办/ETC 等与业务部汇总台账样例同源占位 */ + function buildMockYear2026() { + var rows = []; + var i; + for (i = 1; i <= 12; i++) { + var selfPerf = 250000 + Math.random() * 100000; + var selfCost = selfPerf * (0.8 + Math.random() * 0.1); + + var leasePerf = 180000 + Math.random() * 50000; + var leaseCost = leasePerf * (0.6 + Math.random() * 0.1); + + var resalePerf = 300000 + Math.random() * 200000; + var resaleCost = resalePerf * (0.7 + Math.random() * 0.15); + + var h2Perf = 80000 + Math.random() * 60000; + var h2Cost = h2Perf * (0.6 + Math.random() * 0.2); + + var applyPerf = 3000 + Math.random() * 2000; + var applyCost = applyPerf * (0.3 + Math.random() * 0.1); + + var etcPerf = 70000 + Math.random() * 20000; + var etcCost = etcPerf * (0.5 + Math.random() * 0.1); + + var otherAmount = 8000 + Math.random() * 5000; + + var src = { + selfPerf: Math.round(selfPerf * 100) / 100, + selfCost: Math.round(selfCost * 100) / 100, + selfProfit: Math.round((selfPerf - selfCost) * 100) / 100, + + leasePerf: Math.round(leasePerf * 100) / 100, + leaseCost: Math.round(leaseCost * 100) / 100, + leaseProfit: Math.round((leasePerf - leaseCost) * 100) / 100, + + resalePerf: Math.round(resalePerf * 100) / 100, + resaleCost: Math.round(resaleCost * 100) / 100, + resaleProfit: Math.round((resalePerf - resaleCost) * 100) / 100, + + h2Perf: Math.round(h2Perf * 100) / 100, + h2Cost: Math.round(h2Cost * 100) / 100, + h2Profit: Math.round((h2Perf - h2Cost) * 100) / 100, + + applyPerf: Math.round(applyPerf * 100) / 100, + applyCost: Math.round(applyCost * 100) / 100, + applyProfit: Math.round((applyPerf - applyCost) * 100) / 100, + + etcPerf: Math.round(etcPerf * 100) / 100, + etcCost: Math.round(etcCost * 100) / 100, + etcProfit: Math.round((etcPerf - etcCost) * 100) / 100, + + otherAmount: Math.round(otherAmount * 100) / 100 + }; + + rows.push({ + key: 'm' + i, + month: i, + monthLabel: i + '月', + rowType: 'month', + selfPerf: src.selfPerf, selfCost: src.selfCost, selfProfit: src.selfProfit, + leasePerf: src.leasePerf, leaseCost: src.leaseCost, leaseProfit: src.leaseProfit, + resalePerf: src.resalePerf, resaleCost: src.resaleCost, resaleProfit: src.resaleProfit, + h2Perf: src.h2Perf, h2Cost: src.h2Cost, h2Profit: src.h2Profit, + applyPerf: src.applyPerf, applyCost: src.applyCost, applyProfit: src.applyProfit, + etcPerf: src.etcPerf, etcCost: src.etcCost, etcProfit: src.etcProfit, + otherAmount: src.otherAmount + }); + } + rows.push(sumLedgerRows(rows)); + return rows; + } + + function mockSalesmenDrill(month, catKey, cellPerf) { + var base = [ + { key: 's1', name: '尚建华', ratio: 0.42 }, + { key: 's2', name: '刘念念', ratio: 0.35 }, + { key: 's3', name: '谯云', ratio: 0.15 }, + { key: 's4', name: '董剑煜', ratio: 0.08 } + ]; + var total = numOrZero(cellPerf); + if (total <= 0) total = 100000; + var assigned = 0; + return base.map(function (b, idx) { + if (idx === base.length - 1) { + return { key: b.key, salesperson: b.name, amount: Math.round((total - assigned) * 100) / 100 }; + } + var amt = Math.round(total * b.ratio * 100) / 100; + assigned += amt; + return { key: b.key, salesperson: b.name, amount: amt }; + }); + } + + function mockProjectRows(salesperson, catKey) { + var catLabel = (ALL_CAT_FOR_DRILL.find(function (c) { return c.key === catKey; }) || {}).label || catKey; + return [ + { key: 'p1', projectCode: 'PRJ-2026-001', projectName: catLabel + ' · 嘉兴冷链城配项目', plateNo: '沪A62261F', amount: null, bizDate: '2026-01-08', remark: '演示' }, + { key: 'p2', projectCode: 'PRJ-2026-018', projectName: catLabel + ' · 沪浙干线运输', plateNo: '粤AGP3649', amount: null, bizDate: '2026-01-15', remark: '-' }, + { key: 'p3', projectCode: 'PRJ-2026-033', projectName: catLabel + ' · 园区短驳', plateNo: '苏E·D32891', amount: null, bizDate: '2026-01-22', remark: '-' } + ].map(function (r, i, arr) { + var share = i === arr.length - 1 + ? 1 - arr.slice(0, -1).reduce(function (acc) { return acc + 0.31; }, 0) + : 0.31; + return Object.assign({}, r, { amount: Math.round(88000 * share * 100) / 100 }); + }); + } + + /** + * 自营/租赁业绩钻取:按已选业务部列出业务员行;金额列由主表当月汇总均分(氢费=氢气业绩,电费=申办业绩,与演示口径一致) + */ + function buildSelfLeaseDrillRows(salesModal, deptApplied, deptOptions, yearApplied) { + var src = salesModal.sourceRow; + if (!src || salesModal.month == null) { + return { rows: [], totals: { main: 0, h2: 0, elec: 0, etc: 0, other: 0 } }; + } + var year = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : '2026'; + var month = salesModal.month; + var ym = year + '-' + pad2(month); + var deptValues = (deptApplied && deptApplied.length) + ? deptApplied.slice() + : deptOptions.map(function (o) { return o.value; }); + var namesPool = ['尚建华', '刘念念', '谯云', '董剑煜', '陈思', '周宁']; + var perDept = 2; + var n = deptValues.length * perDept; + if (n === 0) { + return { rows: [], totals: { main: 0, h2: 0, elec: 0, etc: 0, other: 0 } }; + } + var totalMain = numOrZero(salesModal.perf); + var totalH2 = numOrZero(src.h2Perf); + var totalElec = numOrZero(src.applyPerf); + var totalEtc = numOrZero(src.etcPerf); + var totalOther = numOrZero(src.otherAmount); + var mains = splitAcrossN(totalMain, n); + var h2s = splitAcrossN(totalH2, n); + var elecs = splitAcrossN(totalElec, n); + var etss = splitAcrossN(totalEtc, n); + var others = splitAcrossN(totalOther, n); + var rows = []; + var idx = 0; + var d; + for (d = 0; d < deptValues.length; d++) { + var dv = deptValues[d]; + var deptLabel = (deptOptions.find(function (o) { return o.value === dv; }) || {}).label || dv; + var j; + for (j = 0; j < perDept; j++) { + rows.push({ + key: 'drill-' + ym + '-' + String(dv) + '-' + j, + ym: ym, + monthRowSpan: idx === 0 ? n : 0, + deptName: deptLabel, + salesperson: namesPool[idx % namesPool.length], + mainPerf: mains[idx] || 0, + h2PerfCol: h2s[idx] || 0, + elecPerf: elecs[idx] || 0, + etcPerfCol: etss[idx] || 0, + otherAmt: others[idx] || 0 + }); + idx++; + } + } + return { + rows: rows, + totals: { + main: totalMain, + h2: totalH2, + elec: totalElec, + etc: totalEtc, + other: totalOther + } + }; + } + + var layoutStyle = { + padding: '20px 24px 32px', + minHeight: '100vh', + background: 'linear-gradient(165deg, #eef4ff 0%, #f5f7fa 42%, #f0f2f5 100%)' + }; + var filterLabelStyle = { marginBottom: 6, fontSize: 13, color: 'rgba(0,0,0,0.55)', fontWeight: 500 }; + var filterItemStyle = { marginBottom: 12 }; + var filterControlStyle = { width: '100%' }; + var filterActionsColStyle = { flex: '0 0 auto', marginLeft: 'auto' }; + + var filterCardStyle = { + marginBottom: 20, + borderRadius: 16, + boxShadow: '0 4px 20px -4px rgba(16,24,40,0.03), 0 0 0 1px rgba(16,24,40,0.06)', + border: 'none', + background: '#ffffff' + }; + + var tableCardStyle = { + borderRadius: 16, + boxShadow: '0 10px 32px -4px rgba(16,24,40,0.06), 0 0 0 1px rgba(16,24,40,0.04)', + border: 'none', + background: '#ffffff', + overflow: 'hidden' + }; + + var ledgerTableStyle = + '.biz-standbook-table-wrap{border-radius:12px;overflow:hidden;box-shadow:0 4px 24px -6px rgba(15,23,42,0.05),0 0 0 1px rgba(22,119,255,0.1)}' + + '.biz-standbook-table .ant-table-thead>tr>th{white-space:nowrap;color:#1e293b!important;font-weight:600!important;font-size:13px!important;' + + 'background:#f8fafc!important;border-bottom:1px solid #e2e8f0!important;border-inline-end:1px solid #f1f5f9!important;padding:12px 16px!important;transition:background 0.2s}' + + '.biz-standbook-table .ant-table-thead>tr:first-child>th{text-align:center;background:#f1f5f9!important;color:#0f172a!important;font-size:14px!important;border-bottom:2px solid #e2e8f0!important}' + + '.biz-standbook-table .ant-table-tbody>tr:not(.ant-table-measure-row)>td{white-space:nowrap;font-variant-numeric:tabular-nums;color:#334155;border-bottom:1px solid #f1f5f9!important;border-inline-end:1px solid #f8fafc!important;padding:12px 16px!important}' + + '.biz-standbook-table .ant-table-tbody>tr.biz-row-month:hover>td{background:#f0f9ff!important;color:#0f172a}' + + '.biz-standbook-table .ant-table-tbody>tr[data-row-key=\"total\"]>td{font-weight:700;background:#f8fafc!important;color:#0f172a!important;border-top:2px solid #cbd5e1!important;border-bottom:none!important}' + + '.biz-standbook-perf-link{cursor:pointer;color:#0ea5e9;padding:4px 8px;margin:-4px -8px;border:none;background:transparent;font:inherit;border-radius:6px;transition:all 0.2s}' + + '.biz-standbook-perf-link:hover{color:#0284c7;background:#e0f2fe}' + + '.biz-standbook-perf-link:focus{outline:2px solid #38bdf8;outline-offset:2px}' + + '@media (prefers-reduced-motion:reduce){.biz-standbook-table .ant-table-tbody>tr,.biz-standbook-perf-link{transition:none}}'; + + var deptOptions = useMemo(function () { + return [ + { value: '业务二部', label: '业务二部' }, + { value: '华东业务部', label: '华东业务部' }, + { value: '华南业务部', label: '华南业务部' }, + { value: '华北业务部', label: '华北业务部' }, + { value: '西南业务部', label: '西南业务部' } + ]; + }, []); + + var yearDraftState = useState(initialYear); + var yearDraft = yearDraftState[0]; + var setYearDraft = yearDraftState[1]; + + var yearAppliedState = useState(initialYear); + var yearApplied = yearAppliedState[0]; + var setYearApplied = yearAppliedState[1]; + + /** 业务部多选:空数组表示「全部」 */ + var deptDraftState = useState([]); + var deptDraft = deptDraftState[0]; + var setDeptDraft = deptDraftState[1]; + + var deptAppliedState = useState([]); + var deptApplied = deptAppliedState[0]; + var setDeptApplied = deptAppliedState[1]; + + var dataSource = useMemo(function () { + var y = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : ''; + if (y === '2026') return buildMockYear2026(); + return []; + }, [yearApplied]); + + var tableTitle = useMemo(function () { + var y = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : '—'; + return '浙江羚牛氢能业务部汇总台账(' + y + '年度)'; + }, [yearApplied]); + + /** 与已应用筛选一致:空为全部,多选为「、」连接 */ + var deptDisplayLabel = useMemo(function () { + if (!deptApplied || deptApplied.length === 0) return '全部'; + return deptApplied.map(function (v) { + var o = deptOptions.find(function (x) { return x.value === v; }); + return o ? o.label : v; + }).join('、'); + }, [deptApplied, deptOptions]); + + var handleQuery = useCallback(function () { + setYearApplied(yearDraft); + setDeptApplied(deptDraft); + }, [yearDraft, deptDraft]); + + var handleReset = useCallback(function () { + var y0 = initialYear(); + setYearDraft(y0); + setYearApplied(y0); + setDeptDraft([]); + setDeptApplied([]); + }, []); + + var viewState = useState('main'); + var view = viewState[0]; + var setView = viewState[1]; + + var salesModalState = useState({ month: null, monthLabel: '', catKey: '', catLabel: '', perf: null, sourceRow: null }); + var salesModal = salesModalState[0]; + var setSalesModal = salesModalState[1]; + + var projectModalState = useState({ salesperson: '', catKey: '', amount: null }); + var projectModal = projectModalState[0]; + var setProjectModal = projectModalState[1]; + + var showSelfLeaseDrill = !!((view === 'sales' || view === 'project') && (salesModal.catKey === 'self' || salesModal.catKey === 'lease') && salesModal.sourceRow); + + var salesModalRows = useMemo(function () { + if (view === 'main' || salesModal.month == null || !salesModal.catKey) return []; + if (salesModal.catKey === 'self' || salesModal.catKey === 'lease') return []; + return mockSalesmenDrill(salesModal.month, salesModal.catKey, salesModal.perf); + }, [view, salesModal.month, salesModal.catKey, salesModal.perf]); + + var selfLeaseDrillPayload = useMemo(function () { + if (view === 'main' || (salesModal.catKey !== 'self' && salesModal.catKey !== 'lease') || !salesModal.sourceRow) { + return { rows: [], totals: { main: 0, h2: 0, elec: 0, etc: 0, other: 0 } }; + } + return buildSelfLeaseDrillRows(salesModal, deptApplied, deptOptions, yearApplied); + }, [view, salesModal.catKey, salesModal.sourceRow, salesModal.perf, salesModal.month, deptApplied, deptOptions, yearApplied]); + + var selfLeaseDrillTitle = useMemo(function () { + if (view === 'main' || salesModal.month == null) return ''; + var y = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : 'YYYY'; + var ym = y + '-' + pad2(salesModal.month); + var kind = salesModal.catKey === 'lease' ? '租赁' : '自营'; + return '浙江羚牛氢能业务员' + kind + '业务汇总(' + ym + ')'; + }, [view, salesModal.month, salesModal.catKey, yearApplied]); + + var projectModalRows = useMemo(function () { + if (view !== 'project' || !projectModal.salesperson) return []; + return mockProjectRows(projectModal.salesperson, projectModal.catKey || 'self'); + }, [view, projectModal.salesperson, projectModal.catKey]); + + var openPerfDrill = useCallback(function (row, catKey) { + if (row.rowType === 'total') { + message.info('合计行不支持钻取,请从各月份对应的「业绩」进入'); + return; + } + var v = row[catKey + 'Perf']; + if (v === null || v === undefined || v === '' || numOrZero(v) === 0) { + message.warning('该单元格无业绩数据'); + return; + } + var catLabel = (TRIPLE_DEFS.find(function (c) { return c.key === catKey; }) || {}).groupTitle || catKey; + setSalesModal({ + month: row.month, + monthLabel: row.monthLabel, + catKey: catKey, + catLabel: catLabel, + perf: v, + sourceRow: (catKey === 'self' || catKey === 'lease') ? row : null + }); + setView('sales'); + }, []); + + var openProjectDrill = useCallback(function (r) { + setProjectModal({ + salesperson: r.salesperson, + catKey: salesModal.catKey, + amount: r.amount + }); + setView('project'); + }, [salesModal.catKey]); + + var openProjectDrillFromDetail = useCallback(function (r) { + setProjectModal({ + salesperson: r.salesperson, + catKey: salesModal.catKey, + amount: r.mainPerf + }); + setView('project'); + }, [salesModal.catKey]); + + var salesModalColumns = useMemo(function () { + return [ + { title: '业务员', dataIndex: 'salesperson', key: 'salesperson', width: 120 }, + { + title: '业绩金额', + dataIndex: 'amount', + key: 'amount', + align: 'right', + render: function (v, r) { + return React.createElement('button', { + type: 'button', + className: 'biz-standbook-perf-link', + onClick: function () { openProjectDrill(r); } + }, fmtMoney(v)); + } + }, + { title: '说明', key: 'hint', width: 200, render: function () { return React.createElement('span', { style: { color: 'rgba(0,0,0,0.45)', fontSize: 12 } }, '点击金额查看项目明细'); } } + ]; + }, [openProjectDrill]); + + var selfLeaseDrillColumns = useMemo(function () { + var mainTitle = salesModal.catKey === 'lease' ? '租赁业绩' : '自营业绩'; + return [ + { + title: '月份', + dataIndex: 'ym', + key: 'ym', + width: 104, + align: 'center', + onCell: function (record) { + return { rowSpan: record.monthRowSpan }; + } + }, + { title: '业务部门', dataIndex: 'deptName', key: 'deptName', width: 124 }, + { title: '业务人员', dataIndex: 'salesperson', key: 'salesperson', width: 100 }, + { + title: mainTitle, + dataIndex: 'mainPerf', + key: 'mainPerf', + align: 'right', + width: 120, + render: function (v, r) { + if (v === null || v === undefined || v === '' || numOrZero(v) === 0) { + return fmtMoney(v); + } + return React.createElement('button', { + type: 'button', + className: 'biz-standbook-perf-link', + onClick: function () { openProjectDrillFromDetail(r); } + }, fmtMoney(v)); + } + }, + { title: '氢费业绩', dataIndex: 'h2PerfCol', key: 'h2PerfCol', align: 'right', width: 118, render: function (v) { return fmtMoney(v); } }, + { title: '电费业绩', dataIndex: 'elecPerf', key: 'elecPerf', align: 'right', width: 118, render: function (v) { return fmtMoney(v); } }, + { title: 'ETC业绩', dataIndex: 'etcPerfCol', key: 'etcPerfCol', align: 'right', width: 112, render: function (v) { return fmtMoney(v); } }, + { title: '其他', dataIndex: 'otherAmt', key: 'otherAmt', align: 'right', width: 100, render: function (v) { return fmtMoney(v); } } + ]; + }, [salesModal.catKey, openProjectDrillFromDetail]); + + var projectModalColumns = useMemo(function () { + return [ + { title: '项目编号', dataIndex: 'projectCode', key: 'projectCode', width: 130 }, + { title: '项目名称', dataIndex: 'projectName', key: 'projectName', ellipsis: true }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 110 }, + { title: '业绩金额', dataIndex: 'amount', key: 'amount', align: 'right', render: function (v) { return fmtMoney(v); } }, + { title: '业务日期', dataIndex: 'bizDate', key: 'bizDate', width: 110 }, + { title: '备注', dataIndex: 'remark', key: 'remark', width: 80 } + ]; + }, []); + + var handleExport = useCallback(function () { + if (!dataSource || dataSource.length === 0) { + message.warning('当前无数据可导出,请先查询'); + return; + } + var y = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : 'ledger'; + var headers = ['月份']; + TRIPLE_DEFS.forEach(function (c) { + headers.push(c.short + '业绩', c.short + '成本', c.short + '利润'); + }); + headers.push('其他'); + var body = [headers]; + dataSource.forEach(function (r) { + var line = [r.monthLabel]; + TRIPLE_KEYS.forEach(function (k) { + line.push(fmtMoney(r[k + 'Perf']), fmtMoney(r[k + 'Cost']), fmtMoney(r[k + 'Profit'])); + }); + line.push(fmtMoney(r.otherAmount)); + body.push(line); + }); + var deptCsv = (deptApplied && deptApplied.length) + ? deptApplied.map(function (v) { + var o = deptOptions.find(function (x) { return x.value === v; }); + return o ? o.label : v; + }).join('、') + : '全部'; + body.push(['业务部', deptCsv]); + downloadCsv('业务台账_' + y + '_' + new Date().getTime() + '.csv', body); + message.success('已导出'); + }, [dataSource, yearApplied, deptApplied, deptOptions]); + + var ledgerColumns = useMemo(function () { + var cols = [ + { + title: '月份', + dataIndex: 'monthLabel', + key: 'monthLabel', + fixed: 'left', + width: 76, + align: 'center', + render: function (t, r) { + if (r.rowType === 'total') return React.createElement('span', { style: { fontWeight: 700 } }, t); + return t; + } + } + ]; + TRIPLE_DEFS.forEach(function (cat) { + var ck = cat.key; + var s = cat.short; + cols.push({ + title: cat.groupTitle, + key: 'grp-' + ck, + align: 'center', + children: [ + { + title: s + '业绩', + dataIndex: ck + 'Perf', + key: ck + 'Perf', + width: 112, + align: 'right', + render: function (v, row) { + if (row.rowType === 'total' || v === null || v === undefined || v === '' || numOrZero(v) === 0) { + return fmtMoney(v); + } + return React.createElement('button', { + type: 'button', + className: 'biz-standbook-perf-link', + onClick: function () { openPerfDrill(row, ck); } + }, fmtMoney(v)); + } + }, + { + title: s + '成本', + dataIndex: ck + 'Cost', + key: ck + 'Cost', + width: 112, + align: 'right', + render: function (vmt) { return fmtMoney(vmt); } + }, + { + title: s + '利润', + dataIndex: ck + 'Profit', + key: ck + 'Profit', + width: 112, + align: 'right', + render: function (vp) { return fmtMoney(vp); } + } + ] + }); + }); + cols.push({ + title: '其他', + dataIndex: 'otherAmount', + key: 'otherAmount', + width: 108, + align: 'right', + render: function (vo) { return fmtMoney(vo); } + }); + return cols; + }, [openPerfDrill]); + + var rowClassName = useCallback(function (record) { + if (record.rowType === 'total') return ''; + return 'biz-row-month'; + }, []); + + var renderMainView = function () { + return React.createElement(React.Fragment, null, + React.createElement(Card, { style: filterCardStyle, bodyStyle: { paddingBottom: 4 } }, + React.createElement(Row, { gutter: [16, 16], align: 'bottom' }, + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '年份选择'), + React.createElement(DatePicker, { + picker: 'year', + style: filterControlStyle, + placeholder: '请选择年份', + format: 'YYYY', + value: yearDraft, + onChange: function (v) { setYearDraft(v); } + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '业务部'), + React.createElement(Select, { + mode: 'multiple', + placeholder: '全部', + style: filterControlStyle, + value: deptDraft, + onChange: function (v) { setDeptDraft(v || []); }, + options: deptOptions, + showSearch: true, + allowClear: true, + maxTagCount: 2, + filterOption: filterOption + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6, style: filterActionsColStyle }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '\u00a0'), + React.createElement(Space, { wrap: true }, + React.createElement(Button, { onClick: handleReset }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleQuery }, '查询') + ) + ) + ) + ) + ), + React.createElement(Card, { style: tableCardStyle, bodyStyle: { padding: '20px 20px 24px' } }, + React.createElement('div', { style: { position: 'relative', marginBottom: 8, minHeight: 36 } }, + React.createElement('div', { style: { textAlign: 'center', fontSize: 18, fontWeight: 700, color: 'rgba(15,23,42,0.92)', letterSpacing: '0.02em', padding: '0 88px' } }, tableTitle), + React.createElement('div', { style: { position: 'absolute', right: 0, top: '50%', transform: 'translateY(-50%)' } }, + React.createElement(Button, { onClick: handleExport }, '导出') + ) + ), + React.createElement('div', { style: { textAlign: 'center', marginBottom: 16, fontSize: 13, color: 'rgba(15,23,42,0.55)', fontWeight: 500 } }, + '业务部:', + deptDisplayLabel + ), + React.createElement('div', { className: 'biz-standbook-table-wrap' }, + React.createElement(Table, { + className: 'biz-standbook-table', + size: 'small', + bordered: true, + rowKey: 'key', + columns: ledgerColumns, + dataSource: dataSource, + pagination: false, + rowClassName: rowClassName, + scroll: { x: 'max-content', y: 520 }, + sticky: true + }) + ) + ) + ); + }; + + var renderSalesView = function () { + var titleText = showSelfLeaseDrill ? selfLeaseDrillTitle : ('业务员业绩 — ' + (salesModal.monthLabel || '') + ' / ' + (salesModal.catLabel || '')); + return React.createElement(Card, { style: tableCardStyle, bodyStyle: { padding: '24px' } }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', marginBottom: 20 } }, + React.createElement(Button, { + onClick: function () { setView('main'); }, + style: { marginRight: 16 } + }, '返回上一步'), + React.createElement('div', { style: { fontSize: 18, fontWeight: 700, color: '#0f172a' } }, titleText) + ), + showSelfLeaseDrill + ? React.createElement('div', { className: 'biz-standbook-table-wrap' }, + React.createElement(Table, { + className: 'biz-standbook-table', + size: 'small', + bordered: true, + rowKey: 'key', + columns: selfLeaseDrillColumns, + dataSource: selfLeaseDrillPayload.rows, + pagination: false, + scroll: { x: 'max-content' }, + summary: function () { + var totals = selfLeaseDrillPayload.totals; + var Sm = Table.Summary; + var Row = Sm.Row; + var Cell = Sm.Cell; + return React.createElement(Sm, null, + React.createElement(Row, null, + React.createElement(Cell, { index: 0, colSpan: 3, align: 'center' }, '合计'), + React.createElement(Cell, { index: 3, align: 'right' }, fmtMoney(totals.main)), + React.createElement(Cell, { index: 4, align: 'right' }, fmtMoney(totals.h2)), + React.createElement(Cell, { index: 5, align: 'right' }, fmtMoney(totals.elec)), + React.createElement(Cell, { index: 6, align: 'right' }, fmtMoney(totals.etc)), + React.createElement(Cell, { index: 7, align: 'right' }, fmtMoney(totals.other)) + ) + ); + } + }) + ) + : React.createElement(React.Fragment, null, + React.createElement('div', { style: { marginBottom: 16, color: '#64748b', fontSize: 14 } }, + '从总表钻取:本业务线下各业务员业绩构成。点击「业绩金额」继续查看项目明细。' + ), + React.createElement('div', { className: 'biz-standbook-table-wrap' }, + React.createElement(Table, { + className: 'biz-standbook-table', + size: 'small', + bordered: true, + rowKey: 'key', + columns: salesModalColumns, + dataSource: salesModalRows, + pagination: false, + scroll: { x: 'max-content' } + }) + ) + ) + ); + }; + + var renderProjectView = function () { + var titleText = '项目明细 — ' + (projectModal.salesperson || '') + ' · ' + ((TRIPLE_DEFS.find(function (c) { return c.key === projectModal.catKey; }) || {}).groupTitle || ''); + return React.createElement(Card, { style: tableCardStyle, bodyStyle: { padding: '24px' } }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', marginBottom: 20 } }, + React.createElement(Button, { + onClick: function () { setView('sales'); }, + style: { marginRight: 16 } + }, '返回上一步'), + React.createElement('div', { style: { fontSize: 18, fontWeight: 700, color: '#0f172a' } }, titleText) + ), + React.createElement('div', { style: { marginBottom: 16, color: '#64748b', fontSize: 14 } }, + '二级钻取:该项目业务员名下具体项目/车辆维度业绩(演示数据)。' + ), + React.createElement('div', { className: 'biz-standbook-table-wrap' }, + React.createElement(Table, { + className: 'biz-standbook-table', + size: 'small', + bordered: true, + rowKey: 'key', + columns: projectModalColumns, + dataSource: projectModalRows, + pagination: false, + scroll: { x: 'max-content' } + }) + ) + ); + }; + + var breadcrumbItems = [{ title: '数据分析' }, { title: '业务台账' }]; + if (view === 'sales' || view === 'project') { + breadcrumbItems.push({ title: showSelfLeaseDrill ? '业务员汇总' : '业务员业绩' }); + } + if (view === 'project') { + breadcrumbItems.push({ title: '项目明细' }); + } + + return React.createElement(App, null, + React.createElement('style', null, ledgerTableStyle), + React.createElement('div', { style: layoutStyle }, + React.createElement(Breadcrumb, { style: { marginBottom: 14 }, items: breadcrumbItems }), + view === 'main' ? renderMainView() : null, + view === 'sales' ? renderSalesView() : null, + view === 'project' ? renderProjectView() : null + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-data-analysis/pages/08-租赁车辆收入明细.jsx b/src/prototypes/oneos-web-data-analysis/pages/08-租赁车辆收入明细.jsx new file mode 100644 index 0000000..ca8d68f --- /dev/null +++ b/src/prototypes/oneos-web-data-analysis/pages/08-租赁车辆收入明细.jsx @@ -0,0 +1,803 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 数据分析 - 租赁车辆收入明细(租赁业务明细 Tab + 租赁业务盈亏月度汇总 Tab,汇总交互参考「业务部业绩明细」) + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + + var antd = window.antd; + var App = antd.App; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Button = antd.Button; + var Table = antd.Table; + var Select = antd.Select; + var DatePicker = antd.DatePicker; + var Row = antd.Row; + var Col = antd.Col; + var Tabs = antd.Tabs; + var message = antd.message; + + var TableSummary = Table.Summary; + var SummaryRow = TableSummary.Row; + var SummaryCell = TableSummary.Cell; + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + function fmtCell(n) { + if (n === null || n === undefined || n === '') return '—'; + var x = Number(n); + if (isNaN(x)) return '—'; + if (x === 0) return '—'; + return x.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + } + + function fmtSum(n) { + var x = Number(n); + if (isNaN(x)) return '—'; + return x.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 6 }); + } + + function fmtTextSlash(s) { + if (s === null || s === undefined || String(s).trim() === '') return '/'; + return String(s); + } + + function escapeCsv(v) { + var s = v == null ? '' : String(v); + if (s.indexOf(',') !== -1 || s.indexOf('"') !== -1 || s.indexOf('\n') !== -1 || s.indexOf('\r') !== -1) { + return '"' + s.replace(/"/g, '""') + '"'; + } + return s; + } + + function downloadCsv(filename, lines) { + var csv = lines.map(function (row) { return row.map(escapeCsv).join(','); }).join('\n'); + var blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' }); + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); + } + + function getLastMonthStatKey() { + try { + if (window.dayjs) return window.dayjs().subtract(1, 'month').format('YYYY-MM'); + } catch (e1) {} + var d = new Date(); + d.setDate(1); + d.setMonth(d.getMonth() - 1); + var y = d.getFullYear(); + var mo = d.getMonth() + 1; + return y + '-' + (mo < 10 ? '0' + mo : '' + mo); + } + + function getLastMonthPickerValue() { + try { + if (window.dayjs) return window.dayjs().subtract(1, 'month').startOf('month'); + } catch (e1) {} + try { + if (window.moment) return window.moment().subtract(1, 'month').startOf('month'); + } catch (e2) {} + return null; + } + + var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var filterLabelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var filterItemStyle = { marginBottom: 12 }; + var filterControlStyle = { width: '100%' }; + var tableSingleLineStyle = + '.lease-income-detail-table .ant-table-thead th,.lease-income-detail-table .ant-table-tbody td,.lease-income-detail-table .ant-table-summary td{white-space:nowrap;}'; + var tableMonthlyLineStyle = + '.lease-pl-monthly-table .ant-table-thead th,.lease-pl-monthly-table .ant-table-tbody td,.lease-pl-monthly-table .ant-table-summary td{white-space:nowrap;}'; + var tabsBarStyle = '.lease-income-tabs .ant-tabs-nav{margin-bottom:0;}'; + + var bizReportTableTitleStyle = { + textAlign: 'center', + marginBottom: 16, + fontSize: 16, + fontWeight: 600, + color: 'rgba(0,0,0,0.88)' + }; + + var outstandingPositiveStyle = { color: '#cf1322' }; + + var numericSumKeys = [ + 'deposit', + 'receivable', + 'received', + 'outstanding', + 'naturalMonthIncome', + 'hydrogenPrepay', + 'baseCost', + 'brokerage', + 'hydrogenFee', + 'totalCost' + ]; + + /** 月度盈亏汇总表:各月金额列 + 盈亏(自然月收入 − 总成本,可负) */ + var monthlyPLKeys = numericSumKeys.concat(['profit']); + + function sumMonthlyRows(rows, keys) { + var sums = {}; + keys.forEach(function (k) { + sums[k] = (rows || []).reduce(function (acc, row) { + var v = row[k]; + var n = v === null || v === undefined || v === '' ? 0 : Number(v); + return acc + (isNaN(n) ? 0 : n); + }, 0); + }); + return sums; + } + + function finalizeLeaseMonthlyRow(r) { + var inc = r.naturalMonthIncome; + var tc = r.totalCost; + var profit = null; + if (inc != null && inc !== '' && tc != null && tc !== '') { + var a = Number(inc); + var b = Number(tc); + if (!isNaN(a) && !isNaN(b)) profit = a - b; + } + return Object.assign({}, r, { profit: profit }); + } + + function sumRows(rows, keys) { + var sums = {}; + keys.forEach(function (k) { + sums[k] = (rows || []).reduce(function (acc, row) { + var v = row[k]; + var n = v === null || v === undefined || v === '' ? 0 : Number(v); + return acc + (isNaN(n) ? 0 : n); + }, 0); + }); + return sums; + } + + var lastMonthStatKey = useMemo(function () { + return getLastMonthStatKey(); + }, []); + + var deptOptions = useMemo(function () { + return [ + { value: '业务二部', label: '业务二部' }, + { value: '业务一部', label: '业务一部' } + ]; + }, []); + + var rawRowsTemplate = useMemo(function () { + var sm = lastMonthStatKey; + return [ + { + key: 'lv-1', + statMonth: sm, + plateNo: '沪A52898F', + vehicleType: '18T', + salesperson: '刘念忠', + nature: '纯租赁', + systemModel: '飞驰18T', + customerName: '上海虹钦物流有限公司', + contractDate: '2025.12.1-2026.11.30', + pickupDate: '2025-12-08', + deposit: 20000, + receivable: 18500, + received: 18500, + outstanding: 0, + naturalMonthIncome: 18500, + paymentDate: '2026-02-05', + hydrogenPrepay: 5000, + paymentMethod: '月付指付', + invoiceApplyDate: '2026-02-06', + baseCost: 12000, + brokerage: 800, + hydrogenFee: 3200, + totalCost: 16000 + }, + { + key: 'lv-2', + statMonth: sm, + plateNo: '粤AGF4535', + vehicleType: '4.5T', + salesperson: '冉建华', + nature: '试用车', + systemModel: '现代4.5T', + customerName: '杭州绿道城配科技有限公司', + contractDate: '2026.1.1-2026.12.31', + pickupDate: '', + deposit: 4500, + receivable: 4200, + received: 3000, + outstanding: 1200, + naturalMonthIncome: 4200, + paymentDate: '', + hydrogenPrepay: 0, + paymentMethod: '季度指付', + invoiceApplyDate: '', + baseCost: 2800, + brokerage: 0, + hydrogenFee: 900, + totalCost: 3700 + }, + { + key: 'lv-3', + statMonth: sm, + plateNo: '浙A88888F', + vehicleType: '49T', + salesperson: '刘念忠', + nature: '纯租赁', + systemModel: '苏龙18T', + customerName: '宁波港联氢运物流有限公司', + contractDate: '2025.11.15-2026.11.14', + pickupDate: '2025-11-20', + deposit: 50000, + receivable: 26800, + received: 20000, + outstanding: 6800, + naturalMonthIncome: 26800, + paymentDate: '2026-01-28', + hydrogenPrepay: 12000, + paymentMethod: '月付指付', + invoiceApplyDate: '2026-01-30', + baseCost: 18500, + brokerage: 1500, + hydrogenFee: 6200, + totalCost: 26200 + }, + { + key: 'lv-4', + statMonth: sm, + plateNo: '苏E66666F', + vehicleType: '4.5T', + salesperson: '冉建华', + nature: '纯租赁', + systemModel: '现代4.5T', + customerName: '嘉兴南湖氢能示范运营', + contractDate: '2026.2.1-2027.1.31', + pickupDate: '2026-02-10', + deposit: 8000, + receivable: 3800, + received: 3800, + outstanding: 0, + naturalMonthIncome: 3800, + paymentDate: '2026-02-12', + hydrogenPrepay: 2000, + paymentMethod: '月付指付', + invoiceApplyDate: '2026-02-15', + baseCost: 2100, + brokerage: 200, + hydrogenFee: 650, + totalCost: 2950 + } + ]; + }, [lastMonthStatKey]); + + var salespersonOptions = useMemo(function () { + var set = {}; + (rawRowsTemplate || []).forEach(function (r) { + if (r.salesperson) set[r.salesperson] = true; + }); + return Object.keys(set).map(function (n) { return { value: n, label: n }; }); + }, [rawRowsTemplate]); + + var customerOptions = useMemo(function () { + var set = {}; + (rawRowsTemplate || []).forEach(function (r) { + if (r.customerName) set[r.customerName] = true; + }); + return Object.keys(set).map(function (n) { return { value: n, label: n }; }); + }, [rawRowsTemplate]); + + /** 原型:按年 1–12 月租赁业务金额汇总(联调后由接口按年返回) */ + var monthlyLeasePL2026 = useMemo(function () { + var raw = [ + { month: 1, deposit: 120000, receivable: 98500, received: 92000, outstanding: 6500, naturalMonthIncome: 92000, hydrogenPrepay: 18000, baseCost: 62000, brokerage: 4200, hydrogenFee: 15000, totalCost: 81200 }, + { month: 2, deposit: 45000, receivable: 53200, received: 48000, outstanding: 5200, naturalMonthIncome: 50500, hydrogenPrepay: 8000, baseCost: 31000, brokerage: 800, hydrogenFee: 9800, totalCost: 41600 }, + { month: 3, deposit: null, receivable: 66800, received: 66800, outstanding: 0, naturalMonthIncome: 66800, hydrogenPrepay: null, baseCost: 40200, brokerage: 1500, hydrogenFee: 12100, totalCost: 53800 }, + { month: 4, deposit: null, receivable: null, received: null, outstanding: null, naturalMonthIncome: null, hydrogenPrepay: null, baseCost: null, brokerage: null, hydrogenFee: null, totalCost: null }, + { month: 5, deposit: null, receivable: null, received: null, outstanding: null, naturalMonthIncome: null, hydrogenPrepay: null, baseCost: null, brokerage: null, hydrogenFee: null, totalCost: null }, + { month: 6, deposit: null, receivable: null, received: null, outstanding: null, naturalMonthIncome: null, hydrogenPrepay: null, baseCost: null, brokerage: null, hydrogenFee: null, totalCost: null }, + { month: 7, deposit: null, receivable: null, received: null, outstanding: null, naturalMonthIncome: null, hydrogenPrepay: null, baseCost: null, brokerage: null, hydrogenFee: null, totalCost: null }, + { month: 8, deposit: null, receivable: null, received: null, outstanding: null, naturalMonthIncome: null, hydrogenPrepay: null, baseCost: null, brokerage: null, hydrogenFee: null, totalCost: null }, + { month: 9, deposit: null, receivable: null, received: null, outstanding: null, naturalMonthIncome: null, hydrogenPrepay: null, baseCost: null, brokerage: null, hydrogenFee: null, totalCost: null }, + { month: 10, deposit: null, receivable: null, received: null, outstanding: null, naturalMonthIncome: null, hydrogenPrepay: null, baseCost: null, brokerage: null, hydrogenFee: null, totalCost: null }, + { month: 11, deposit: null, receivable: null, received: null, outstanding: null, naturalMonthIncome: null, hydrogenPrepay: null, baseCost: null, brokerage: null, hydrogenFee: null, totalCost: null }, + { month: 12, deposit: null, receivable: null, received: null, outstanding: null, naturalMonthIncome: null, hydrogenPrepay: null, baseCost: null, brokerage: null, hydrogenFee: null, totalCost: null } + ]; + return raw.map(function (x) { + return finalizeLeaseMonthlyRow(Object.assign({ key: 'lease-pl-' + x.month }, x)); + }); + }, []); + + var mainTabState = useState('detail'); + var mainTab = mainTabState[0]; + var setMainTab = mainTabState[1]; + + function initialYear2026() { + try { + if (window.dayjs) return window.dayjs('2026-01-01'); + } catch (e1) {} + return null; + } + + var summaryYearDraftState = useState(initialYear2026); + var summaryYearDraft = summaryYearDraftState[0]; + var setSummaryYearDraft = summaryYearDraftState[1]; + + var summaryYearAppliedState = useState(initialYear2026); + var summaryYearApplied = summaryYearAppliedState[0]; + var setSummaryYearApplied = summaryYearAppliedState[1]; + + var draftState = useState(function () { + return { + month: getLastMonthPickerValue(), + salesperson: undefined, + customerName: undefined, + dept: '业务二部' + }; + }); + var draft = draftState[0]; + var setDraft = draftState[1]; + + var appliedState = useState(function () { + return { + month: getLastMonthPickerValue(), + salesperson: undefined, + customerName: undefined, + dept: '业务二部' + }; + }); + var applied = appliedState[0]; + var setApplied = appliedState[1]; + + var filteredRows = useMemo(function () { + return (rawRowsTemplate || []).filter(function (r) { + if (applied.month && applied.month.format) { + var mk = applied.month.format('YYYY-MM'); + if (r.statMonth !== mk) return false; + } + if (applied.salesperson && r.salesperson !== applied.salesperson) return false; + if (applied.customerName && r.customerName !== applied.customerName) return false; + return true; + }); + }, [rawRowsTemplate, applied.month, applied.salesperson, applied.customerName]); + + var columnSums = useMemo(function () { + return sumRows(filteredRows, numericSumKeys); + }, [filteredRows]); + + var reportTitle = useMemo(function () { + var dept = applied.dept || '业务二部'; + if (applied.month && applied.month.format) { + var y = applied.month.format('YYYY'); + return y + '年浙江羚牛氢能租赁车辆收入明细表(' + dept + ')'; + } + return '浙江羚牛氢能租赁车辆收入明细表(' + dept + ')'; + }, [applied.month, applied.dept]); + + var plSumTableTitle = useMemo(function () { + if (summaryYearApplied && summaryYearApplied.format) { + return '租赁业务' + summaryYearApplied.format('YYYY') + '年盈亏月度汇总'; + } + return '租赁业务盈亏月度汇总'; + }, [summaryYearApplied]); + + var monthlyDataSource = useCallback(function (rows) { + var y = summaryYearApplied && summaryYearApplied.format ? summaryYearApplied.format('YYYY') : null; + if (!y) return []; + if (y !== '2026') return []; + return rows; + }, [summaryYearApplied]); + + var plMonthlyRows = useMemo(function () { return monthlyDataSource(monthlyLeasePL2026); }, [monthlyDataSource, monthlyLeasePL2026]); + var plMonthlySums = useMemo(function () { return sumMonthlyRows(plMonthlyRows, monthlyPLKeys); }, [plMonthlyRows]); + + var handleQuery = useCallback(function () { + setApplied(Object.assign({}, draft)); + }, [draft]); + + var handleReset = useCallback(function () { + var def = { + month: getLastMonthPickerValue(), + salesperson: undefined, + customerName: undefined, + dept: '业务二部' + }; + setDraft(def); + setApplied(def); + }, []); + + var handleSummaryQuery = useCallback(function () { + setSummaryYearApplied(summaryYearDraft); + }, [summaryYearDraft]); + + var handleSummaryReset = useCallback(function () { + var y0 = initialYear2026(); + setSummaryYearDraft(y0); + setSummaryYearApplied(y0); + }, []); + + var handleExport = useCallback(function () { + var rows = filteredRows; + if (!rows || rows.length === 0) { + message.warning('当前无数据可导出'); + return; + } + var headers = [ + '车牌号码', + '车型', + '业务员', + '性质', + '系统车型', + '客户名称', + '合同日期', + '提车日期', + '押金', + '应收', + '实收', + '未收', + '自然月收入', + '付款日期', + '氢费预充值', + '付款方式', + '申请开票日期', + '成本', + '居间费', + '氢费', + '总成本' + ]; + var line = function (r) { + return [ + r.plateNo, + r.vehicleType, + r.salesperson, + r.nature, + r.systemModel, + r.customerName, + fmtTextSlash(r.contractDate), + fmtTextSlash(r.pickupDate), + fmtCell(r.deposit), + fmtCell(r.receivable), + fmtCell(r.received), + fmtCell(r.outstanding), + fmtCell(r.naturalMonthIncome), + fmtTextSlash(r.paymentDate), + fmtCell(r.hydrogenPrepay), + fmtTextSlash(r.paymentMethod), + fmtTextSlash(r.invoiceApplyDate), + fmtCell(r.baseCost), + fmtCell(r.brokerage), + fmtCell(r.hydrogenFee), + fmtCell(r.totalCost) + ]; + }; + var body = [headers].concat(rows.map(line)); + body.push([ + '汇总', '', '', '', '', '', '', '', + fmtSum(columnSums.deposit), + fmtSum(columnSums.receivable), + fmtSum(columnSums.received), + fmtSum(columnSums.outstanding), + fmtSum(columnSums.naturalMonthIncome), + '', + fmtSum(columnSums.hydrogenPrepay), + '', '', + fmtSum(columnSums.baseCost), + fmtSum(columnSums.brokerage), + fmtSum(columnSums.hydrogenFee), + fmtSum(columnSums.totalCost) + ]); + downloadCsv('租赁车辆收入明细_' + new Date().getTime() + '.csv', body); + message.success('已导出 ' + rows.length + ' 条记录'); + }, [filteredRows, columnSums]); + + var handleExportPLMonthly = useCallback(function () { + var rows = plMonthlyRows; + if (!rows || rows.length === 0) { + message.warning('当前无数据可导出,请先选择年份并查询'); + return; + } + var headers = [ + '月份', + '押金', + '应收', + '实收', + '未收', + '自然月收入', + '氢费预充值', + '成本', + '居间费', + '氢费', + '总成本', + '盈亏' + ]; + var body = [headers].concat( + rows.map(function (r) { + return [ + String(r.month), + fmtCell(r.deposit), + fmtCell(r.receivable), + fmtCell(r.received), + fmtCell(r.outstanding), + fmtCell(r.naturalMonthIncome), + fmtCell(r.hydrogenPrepay), + fmtCell(r.baseCost), + fmtCell(r.brokerage), + fmtCell(r.hydrogenFee), + fmtCell(r.totalCost), + fmtCell(r.profit) + ]; + }) + ); + body.push(['总计'].concat(monthlyPLKeys.map(function (k) { return fmtSum(plMonthlySums[k] || 0); }))); + downloadCsv('租赁业务盈亏月度汇总_' + new Date().getTime() + '.csv', body); + message.success('已导出 ' + rows.length + ' 个月度数据'); + }, [plMonthlyRows, plMonthlySums]); + + function renderOutstanding(v) { + var s = fmtCell(v); + if (v !== null && v !== undefined && v !== '' && !isNaN(Number(v)) && Number(v) > 0) { + return React.createElement('span', { style: outstandingPositiveStyle }, s); + } + return s; + } + + var columns = useMemo(function () { + return [ + { title: '车牌号码', dataIndex: 'plateNo', key: 'plateNo', width: 110, fixed: 'left', align: 'center' }, + { title: '车型', dataIndex: 'vehicleType', key: 'vehicleType', width: 72, align: 'center' }, + { title: '业务员', dataIndex: 'salesperson', key: 'salesperson', width: 88, align: 'center' }, + { title: '性质', dataIndex: 'nature', key: 'nature', width: 88, align: 'center' }, + { title: '系统车型', dataIndex: 'systemModel', key: 'systemModel', width: 100, align: 'center' }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 200, ellipsis: true }, + { title: '合同日期', dataIndex: 'contractDate', key: 'contractDate', width: 168, render: function (v) { return fmtTextSlash(v); } }, + { title: '提车日期', dataIndex: 'pickupDate', key: 'pickupDate', width: 110, render: function (v) { return fmtTextSlash(v); } }, + { title: '押金', dataIndex: 'deposit', key: 'deposit', width: 100, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '应收', dataIndex: 'receivable', key: 'receivable', width: 100, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '实收', dataIndex: 'received', key: 'received', width: 100, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '未收', dataIndex: 'outstanding', key: 'outstanding', width: 100, align: 'right', render: function (v) { return renderOutstanding(v); } }, + { title: '自然月收入', dataIndex: 'naturalMonthIncome', key: 'naturalMonthIncome', width: 118, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '付款日期', dataIndex: 'paymentDate', key: 'paymentDate', width: 110, render: function (v) { return fmtTextSlash(v); } }, + { title: '氢费预充值', dataIndex: 'hydrogenPrepay', key: 'hydrogenPrepay', width: 110, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '付款方式', dataIndex: 'paymentMethod', key: 'paymentMethod', width: 100, render: function (v) { return fmtTextSlash(v); } }, + { title: '申请开票日期', dataIndex: 'invoiceApplyDate', key: 'invoiceApplyDate', width: 120, render: function (v) { return fmtTextSlash(v); } }, + { title: '成本', dataIndex: 'baseCost', key: 'baseCost', width: 100, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '居间费', dataIndex: 'brokerage', key: 'brokerage', width: 90, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '氢费', dataIndex: 'hydrogenFee', key: 'hydrogenFee', width: 90, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '总成本', dataIndex: 'totalCost', key: 'totalCost', width: 110, align: 'right', render: function (v) { return fmtCell(v); } } + ]; + }, []); + + var columnsPLMonthly = useMemo(function () { + return [ + { title: '月份', dataIndex: 'month', key: 'month', width: 72, align: 'center' }, + { title: '押金', dataIndex: 'deposit', key: 'deposit', width: 100, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '应收', dataIndex: 'receivable', key: 'receivable', width: 100, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '实收', dataIndex: 'received', key: 'received', width: 100, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '未收', dataIndex: 'outstanding', key: 'outstanding', width: 100, align: 'right', render: function (v) { return renderOutstanding(v); } }, + { title: '自然月收入', dataIndex: 'naturalMonthIncome', key: 'naturalMonthIncome', width: 118, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '氢费预充值', dataIndex: 'hydrogenPrepay', key: 'hydrogenPrepay', width: 110, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '成本', dataIndex: 'baseCost', key: 'baseCost', width: 100, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '居间费', dataIndex: 'brokerage', key: 'brokerage', width: 90, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '氢费', dataIndex: 'hydrogenFee', key: 'hydrogenFee', width: 90, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '总成本', dataIndex: 'totalCost', key: 'totalCost', width: 100, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '盈亏', dataIndex: 'profit', key: 'profit', width: 100, align: 'right', render: function (v) { return fmtCell(v); } } + ]; + }, []); + + function renderPLMonthlySummary(sums) { + return function () { + return React.createElement( + TableSummary, + null, + React.createElement( + SummaryRow, + null, + React.createElement(SummaryCell, { index: 0, align: 'center' }, '总计'), + monthlyPLKeys.map(function (k, idx) { + var cell = fmtSum(sums[k] || 0); + if (k === 'outstanding' && Number(sums.outstanding) > 0) { + cell = React.createElement('span', { style: outstandingPositiveStyle }, fmtSum(sums.outstanding || 0)); + } + return React.createElement( + SummaryCell, + { key: k, index: idx + 1, align: 'right' }, + cell + ); + }) + ) + ); + }; + } + + var renderYearFilterCard = useCallback(function () { + return React.createElement(Card, { style: { marginBottom: 16 } }, + React.createElement(Row, { gutter: [16, 16], align: 'bottom' }, + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '选择年份'), + React.createElement(DatePicker, { + picker: 'year', + style: filterControlStyle, + placeholder: '请选择统计年份', + format: 'YYYY', + value: summaryYearDraft, + onChange: function (v) { setSummaryYearDraft(v); } + }) + ) + ) + ), + React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 } }, + React.createElement(Button, { onClick: handleSummaryReset }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleSummaryQuery }, '查询') + ) + ); + }, [summaryYearDraft, handleSummaryReset, handleSummaryQuery]); + + var tableSummary = useCallback(function () { + return React.createElement( + TableSummary, + null, + React.createElement( + SummaryRow, + null, + React.createElement(SummaryCell, { index: 0, align: 'center', colSpan: 8 }, '汇总'), + React.createElement(SummaryCell, { index: 8, align: 'right' }, fmtSum(columnSums.deposit)), + React.createElement(SummaryCell, { index: 9, align: 'right' }, fmtSum(columnSums.receivable)), + React.createElement(SummaryCell, { index: 10, align: 'right' }, fmtSum(columnSums.received)), + React.createElement(SummaryCell, { index: 11, align: 'right' }, + Number(columnSums.outstanding) > 0 + ? React.createElement('span', { style: outstandingPositiveStyle }, fmtSum(columnSums.outstanding)) + : fmtSum(columnSums.outstanding) + ), + React.createElement(SummaryCell, { index: 12, align: 'right' }, fmtSum(columnSums.naturalMonthIncome)), + React.createElement(SummaryCell, { index: 13, align: 'center' }, '—'), + React.createElement(SummaryCell, { index: 14, align: 'right' }, fmtSum(columnSums.hydrogenPrepay)), + React.createElement(SummaryCell, { index: 15, align: 'center' }, '—'), + React.createElement(SummaryCell, { index: 16, align: 'center' }, '—'), + React.createElement(SummaryCell, { index: 17, align: 'right' }, fmtSum(columnSums.baseCost)), + React.createElement(SummaryCell, { index: 18, align: 'right' }, fmtSum(columnSums.brokerage)), + React.createElement(SummaryCell, { index: 19, align: 'right' }, fmtSum(columnSums.hydrogenFee)), + React.createElement(SummaryCell, { index: 20, align: 'right' }, fmtSum(columnSums.totalCost)) + ) + ); + }, [columnSums]); + + var tabItems = [ + { + key: 'detail', + label: '租赁业务明细', + children: React.createElement(React.Fragment, null, + React.createElement(Card, { style: { marginBottom: 16 } }, + React.createElement(Row, { gutter: [16, 16], align: 'bottom' }, + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '统计月份'), + React.createElement(DatePicker, { + picker: 'month', + style: filterControlStyle, + placeholder: '请选择年-月', + format: 'YYYY-MM', + value: draft.month, + onChange: function (v) { setDraft(function (p) { return Object.assign({}, p, { month: v }); }); } + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '业务部门'), + React.createElement(Select, { + placeholder: '请选择业务部门', + style: filterControlStyle, + options: deptOptions, + value: draft.dept, + onChange: function (v) { setDraft(function (p) { return Object.assign({}, p, { dept: v }); }); } + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '业务员'), + React.createElement(Select, { + placeholder: '请选择业务员', + style: filterControlStyle, + allowClear: true, + options: salespersonOptions, + value: draft.salesperson, + onChange: function (v) { setDraft(function (p) { return Object.assign({}, p, { salesperson: v }); }); }, + showSearch: true, + filterOption: filterOption + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '客户名称'), + React.createElement(Select, { + placeholder: '请选择客户名称', + style: filterControlStyle, + allowClear: true, + options: customerOptions, + value: draft.customerName, + onChange: function (v) { setDraft(function (p) { return Object.assign({}, p, { customerName: v }); }); }, + showSearch: true, + filterOption: filterOption + }) + ) + ) + ), + React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 } }, + React.createElement(Button, { onClick: handleReset }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleQuery }, '查询') + ) + ), + React.createElement(Card, { + extra: React.createElement(Button, { onClick: handleExport }, '导出') + }, + React.createElement('div', { style: bizReportTableTitleStyle }, reportTitle), + React.createElement('style', null, tableSingleLineStyle), + React.createElement('div', { className: 'lease-income-detail-table' }, + React.createElement(Table, { + rowKey: 'key', + columns: columns, + dataSource: filteredRows, + pagination: false, + size: 'small', + summary: tableSummary, + scroll: { x: 2680 } + }) + ) + ) + ) + }, + { + key: 'monthlyPL', + label: '租赁业务盈亏月度汇总', + children: React.createElement(React.Fragment, null, + renderYearFilterCard(), + React.createElement(Card, { + extra: React.createElement(Button, { onClick: handleExportPLMonthly }, '导出') + }, + React.createElement('div', { style: bizReportTableTitleStyle }, plSumTableTitle), + React.createElement('style', null, tableMonthlyLineStyle), + React.createElement('div', { className: 'lease-pl-monthly-table' }, + React.createElement(Table, { + rowKey: 'key', + columns: columnsPLMonthly, + dataSource: plMonthlyRows, + pagination: false, + size: 'small', + summary: renderPLMonthlySummary(plMonthlySums), + scroll: { x: 1320 } + }) + ) + ) + ) + } + ]; + + return React.createElement(App, null, + React.createElement('div', { style: layoutStyle }, + React.createElement(Breadcrumb, { + style: { marginBottom: 16 }, + items: [{ title: '数据分析' }, { title: '租赁车辆收入明细' }] + }), + React.createElement(Card, null, + React.createElement('style', null, tabsBarStyle), + React.createElement(Tabs, { + className: 'lease-income-tabs', + activeKey: mainTab, + onChange: function (k) { setMainTab(k); }, + items: tabItems + }) + ) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-data-analysis/pages/09-租赁客户氢费台账.jsx b/src/prototypes/oneos-web-data-analysis/pages/09-租赁客户氢费台账.jsx new file mode 100644 index 0000000..40ce883 --- /dev/null +++ b/src/prototypes/oneos-web-data-analysis/pages/09-租赁客户氢费台账.jsx @@ -0,0 +1,696 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 数据分析 - 租赁客户氢费台账:Tab「租赁客户氢费明细」+「租赁客户氢费总计」(按客户×月汇总金额) + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + + var antd = window.antd; + var App = antd.App; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Button = antd.Button; + var Table = antd.Table; + var Select = antd.Select; + var DatePicker = antd.DatePicker; + var Row = antd.Row; + var Col = antd.Col; + var Tabs = antd.Tabs; + var message = antd.message; + + var TableSummary = Table.Summary; + var SummaryRow = TableSummary.Row; + var SummaryCell = TableSummary.Cell; + var RangePicker = DatePicker.RangePicker; + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + function fmtCell(n) { + if (n === null || n === undefined || n === '') return '—'; + var x = Number(n); + if (isNaN(x)) return '—'; + if (x === 0) return '—'; + return x.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + } + + function fmtSum(n) { + var x = Number(n); + if (isNaN(x)) return '—'; + return x.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 6 }); + } + + function fmtDateDash(iso) { + if (!iso) return '—'; + try { + if (window.dayjs) { + var d = window.dayjs(iso); + if (!d.isValid()) return '—'; + return d.format('YYYY-MM-DD'); + } + } catch (e1) {} + try { + var p = String(iso).split(/[-/]/); + if (p.length >= 3) { + var y = parseInt(p[0], 10); + var m = parseInt(p[1], 10); + var day = parseInt(p[2], 10); + if (!isNaN(y) && !isNaN(m) && !isNaN(day)) { + return y + '-' + (m < 10 ? '0' + m : m) + '-' + (day < 10 ? '0' + day : day); + } + } + } catch (e2) {} + return String(iso); + } + + function escapeCsv(v) { + var s = v == null ? '' : String(v); + if (s.indexOf(',') !== -1 || s.indexOf('"') !== -1 || s.indexOf('\n') !== -1 || s.indexOf('\r') !== -1) { + return '"' + s.replace(/"/g, '""') + '"'; + } + return s; + } + + function downloadCsv(filename, lines) { + var csv = lines.map(function (row) { return row.map(escapeCsv).join(','); }).join('\n'); + var blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' }); + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); + } + + function getLastMonthPickerValue() { + try { + if (window.dayjs) return window.dayjs().subtract(1, 'month').startOf('month'); + } catch (e1) {} + try { + if (window.moment) return window.moment().subtract(1, 'month').startOf('month'); + } catch (e2) {} + return null; + } + + function getStatMonthKey(monthsAgo) { + var off = monthsAgo == null ? 1 : monthsAgo; + try { + if (window.dayjs) return window.dayjs().subtract(off, 'month').format('YYYY-MM'); + } catch (e1) {} + var d = new Date(); + d.setDate(1); + d.setMonth(d.getMonth() - off); + var y = d.getFullYear(); + var mo = d.getMonth() + 1; + return y + '-' + (mo < 10 ? '0' + mo : '' + mo); + } + + function getLastMonthRange() { + try { + if (window.dayjs) { + var s = window.dayjs().subtract(1, 'month').startOf('month'); + var e = window.dayjs().subtract(1, 'month').endOf('month'); + return [s, e]; + } + } catch (e1) {} + return [null, null]; + } + + function rowDayMs(iso) { + if (!iso) return NaN; + var t = Date.parse(String(iso).length <= 10 ? String(iso) + 'T12:00:00' : iso); + return isNaN(t) ? NaN : t; + } + + function inRange(iso, range) { + if (!range || !range[0] || !range[1]) return true; + var t = rowDayMs(iso); + if (isNaN(t)) return false; + var s = range[0].startOf ? range[0].startOf('day').valueOf() : NaN; + var e = range[1].endOf ? range[1].endOf('day').valueOf() : NaN; + if (isNaN(s) || isNaN(e)) return true; + return t >= s && t <= e; + } + + var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var filterLabelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var filterItemStyle = { marginBottom: 12 }; + var filterControlStyle = { width: '100%' }; + /** 与「车辆租赁合同」列表表一致:单行不换行;本页含合计行故增加 summary 单元格 */ + var tableSingleLineStyle = + '.contract-list-table .ant-table-thead th,.contract-list-table .ant-table-tbody td,.contract-list-table .ant-table-summary td{white-space:nowrap;}'; + var listToolbarStyle = { marginBottom: 16, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', flexWrap: 'wrap', gap: 8 }; + var tabsBarStyle = '.lease-h2-ledger-tabs .ant-tabs-nav{margin-bottom:0;}'; + + var bizReportTableTitleStyle = { + textAlign: 'center', + marginBottom: 16, + fontSize: 16, + fontWeight: 600, + color: 'rgba(0,0,0,0.88)' + }; + + var boldNumStyle = { fontWeight: 600 }; + + /** 原型明细(statMonth / bizDate 随「上一月、上上月」变化,与默认筛选区间一致) */ + var allDetailRows = useMemo(function () { + var m0 = getStatMonthKey(1); + var m1 = getStatMonthKey(2); + var p0 = m0.split('-'); + var p1 = m1.split('-'); + var y0 = parseInt(p0[0], 10); + var mo0 = parseInt(p0[1], 10); + var y1 = parseInt(p1[0], 10); + var mo1 = parseInt(p1[1], 10); + function iso(y, mo, day) { + return y + '-' + (mo < 10 ? '0' + mo : mo) + '-' + (day < 10 ? '0' + day : day); + } + return [ + { + key: 'h2-1', + statMonth: m0, + bizDate: iso(y0, mo0, 1), + salesperson: '刘念念', + customerName: '嘉兴古道物流有限公司', + plateNo: '沪A68122F', + quantityKg: 45.6, + location: '嘉兴嘉锦亭桥-北综合供能服务站', + purchasePrice: 28.5, + costAmount: 1299.6, + unitPrice: 32, + amount: 1459.2 + }, + { + key: 'h2-2', + statMonth: m0, + bizDate: iso(y0, mo0, 3), + salesperson: '尚建华', + customerName: '嘉兴古道物流有限公司', + plateNo: '粤AGP3513', + quantityKg: 38.2, + location: '嘉兴嘉锦亭桥-北综合供能服务站', + purchasePrice: 28.5, + costAmount: 1088.7, + unitPrice: 32, + amount: 1222.4 + }, + { + key: 'h2-3', + statMonth: m0, + bizDate: iso(y0, mo0, 5), + salesperson: '刘念念', + customerName: '杭州绿道城配科技有限公司', + plateNo: '浙A88888F', + quantityKg: 52, + location: '杭州钱塘综合能源站', + purchasePrice: 29, + costAmount: 1508, + unitPrice: 33.5, + amount: 1742 + }, + { + key: 'h2-4', + statMonth: m0, + bizDate: iso(y0, mo0, 12), + salesperson: '尚建华', + customerName: '宁波港联氢运物流有限公司', + plateNo: '浙B12345F', + quantityKg: 60.5, + location: '宁波北仑氢能示范站', + purchasePrice: 27.8, + costAmount: 1681.9, + unitPrice: 31.2, + amount: 1887.6 + }, + { + key: 'h2-5', + statMonth: m1, + bizDate: iso(y1, mo1, 2), + salesperson: '刘念念', + customerName: '嘉兴古道物流有限公司', + plateNo: '沪A68122F', + quantityKg: 41, + location: '嘉兴嘉锦亭桥-北综合供能服务站', + purchasePrice: 28.6, + costAmount: 1172.6, + unitPrice: 32, + amount: 1312 + }, + { + key: 'h2-6', + statMonth: m1, + bizDate: iso(y1, mo1, 18), + salesperson: '刘念念', + customerName: '杭州绿道城配科技有限公司', + plateNo: '浙A88888F', + quantityKg: 48.5, + location: '杭州钱塘综合能源站', + purchasePrice: 29.1, + costAmount: 1411.35, + unitPrice: 33.5, + amount: 1624.75 + } + ]; + }, []); + + var salespersonOptions = useMemo(function () { + var set = {}; + allDetailRows.forEach(function (r) { + if (r.salesperson) set[r.salesperson] = true; + }); + return Object.keys(set).map(function (n) { return { value: n, label: n }; }); + }, [allDetailRows]); + + var customerOptions = useMemo(function () { + var set = {}; + allDetailRows.forEach(function (r) { + if (r.customerName) set[r.customerName] = true; + }); + return Object.keys(set).map(function (n) { return { value: n, label: n }; }); + }, [allDetailRows]); + + var mainTabState = useState('detail'); + var mainTab = mainTabState[0]; + var setMainTab = mainTabState[1]; + + var rangeDefault = useMemo(function () { return getLastMonthRange(); }, []); + + var draftDetailState = useState(function () { + return { + dateRange: rangeDefault[0] && rangeDefault[1] ? [rangeDefault[0], rangeDefault[1]] : null, + salesperson: undefined, + customerName: undefined + }; + }); + var draftDetail = draftDetailState[0]; + var setDraftDetail = draftDetailState[1]; + + var appliedDetailState = useState(function () { + return { + dateRange: rangeDefault[0] && rangeDefault[1] ? [rangeDefault[0], rangeDefault[1]] : null, + salesperson: undefined, + customerName: undefined + }; + }); + var appliedDetail = appliedDetailState[0]; + var setAppliedDetail = appliedDetailState[1]; + + var draftTotalState = useState(function () { + return { + month: getLastMonthPickerValue(), + customerName: undefined + }; + }); + var draftTotal = draftTotalState[0]; + var setDraftTotal = draftTotalState[1]; + + var appliedTotalState = useState(function () { + return { + month: getLastMonthPickerValue(), + customerName: undefined + }; + }); + var appliedTotal = appliedTotalState[0]; + var setAppliedTotal = appliedTotalState[1]; + + var filteredDetailRows = useMemo(function () { + return (allDetailRows || []).filter(function (r) { + if (appliedDetail.dateRange && appliedDetail.dateRange[0] && appliedDetail.dateRange[1]) { + if (!inRange(r.bizDate, appliedDetail.dateRange)) return false; + } + if (appliedDetail.salesperson && r.salesperson !== appliedDetail.salesperson) return false; + if (appliedDetail.customerName && r.customerName !== appliedDetail.customerName) return false; + return true; + }); + }, [allDetailRows, appliedDetail.dateRange, appliedDetail.salesperson, appliedDetail.customerName]); + + var detailSums = useMemo(function () { + var q = 0; + var c = 0; + var a = 0; + filteredDetailRows.forEach(function (r) { + q += isNaN(Number(r.quantityKg)) ? 0 : Number(r.quantityKg); + c += isNaN(Number(r.costAmount)) ? 0 : Number(r.costAmount); + a += isNaN(Number(r.amount)) ? 0 : Number(r.amount); + }); + return { quantityKg: q, costAmount: c, amount: a }; + }, [filteredDetailRows]); + + var totalByCustomerMonthRows = useMemo(function () { + var mk = ''; + if (appliedTotal.month && appliedTotal.month.format) mk = appliedTotal.month.format('YYYY-MM'); + if (!mk) return []; + var map = {}; + allDetailRows.forEach(function (r) { + if (r.statMonth !== mk) return; + if (appliedTotal.customerName && r.customerName !== appliedTotal.customerName) return; + var k = r.customerName || ''; + if (!map[k]) { + map[k] = { key: 'sum-' + mk + '-' + k, customerName: k, statMonth: mk, quantityKg: 0, amount: 0 }; + } + map[k].quantityKg += isNaN(Number(r.quantityKg)) ? 0 : Number(r.quantityKg); + map[k].amount += isNaN(Number(r.amount)) ? 0 : Number(r.amount); + }); + return Object.keys(map).map(function (name) { return map[name]; }); + }, [allDetailRows, appliedTotal.month, appliedTotal.customerName]); + + var totalTabSums = useMemo(function () { + var q = 0; + var a = 0; + totalByCustomerMonthRows.forEach(function (r) { + q += r.quantityKg || 0; + a += r.amount || 0; + }); + return { quantityKg: q, amount: a }; + }, [totalByCustomerMonthRows]); + + var detailTitle = useMemo(function () { + return '租赁客户氢费明细表'; + }, []); + + var totalTitle = useMemo(function () { + if (appliedTotal.month && appliedTotal.month.format) { + return appliedTotal.month.format('YYYY年M月') + '租赁客户氢费总计'; + } + return '租赁客户氢费总计'; + }, [appliedTotal.month]); + + var handleQueryDetail = useCallback(function () { + setAppliedDetail(Object.assign({}, draftDetail)); + }, [draftDetail]); + + var handleResetDetail = useCallback(function () { + var def = { + dateRange: rangeDefault[0] && rangeDefault[1] ? [rangeDefault[0], rangeDefault[1]] : null, + salesperson: undefined, + customerName: undefined + }; + setDraftDetail(def); + setAppliedDetail(def); + }, [rangeDefault]); + + var handleQueryTotal = useCallback(function () { + setAppliedTotal(Object.assign({}, draftTotal)); + }, [draftTotal]); + + var handleResetTotal = useCallback(function () { + var def = { month: getLastMonthPickerValue(), customerName: undefined }; + setDraftTotal(def); + setAppliedTotal(def); + }, []); + + var handleExportDetail = useCallback(function () { + var rows = filteredDetailRows; + if (!rows || rows.length === 0) { + message.warning('当前无数据可导出'); + return; + } + var headers = [ + '业务员', + '客户名称', + '日期', + '车牌号', + '加氢数量', + '加氢地点', + '进价', + '成本金额', + '单价', + '金额' + ]; + var line = function (r) { + return [ + r.salesperson, + r.customerName, + fmtDateDash(r.bizDate), + r.plateNo, + fmtCell(r.quantityKg), + r.location, + fmtCell(r.purchasePrice), + fmtCell(r.costAmount), + fmtCell(r.unitPrice), + fmtCell(r.amount) + ]; + }; + var body = [headers].concat(rows.map(line)); + body.push([ + '汇总', + '', + '', + '', + fmtSum(detailSums.quantityKg), + '', + '', + fmtSum(detailSums.costAmount), + '', + fmtSum(detailSums.amount) + ]); + downloadCsv('租赁客户氢费明细_' + new Date().getTime() + '.csv', body); + message.success('已导出 ' + rows.length + ' 条记录'); + }, [filteredDetailRows, detailSums]); + + var handleExportTotal = useCallback(function () { + var rows = totalByCustomerMonthRows; + if (!rows || rows.length === 0) { + message.warning('当前无数据可导出,请选择统计月份并查询'); + return; + } + var headers = ['客户名称', '统计月份', '加氢数量合计', '金额合计']; + var body = [headers].concat( + rows.map(function (r) { + return [r.customerName, r.statMonth, fmtSum(r.quantityKg), fmtSum(r.amount)]; + }) + ); + body.push(['总计', '', fmtSum(totalTabSums.quantityKg), fmtSum(totalTabSums.amount)]); + downloadCsv('租赁客户氢费总计_' + new Date().getTime() + '.csv', body); + message.success('已导出 ' + rows.length + ' 条记录'); + }, [totalByCustomerMonthRows, totalTabSums]); + + function renderBoldMoney(v) { + var s = fmtCell(v); + return React.createElement('span', { style: boldNumStyle }, s); + } + + var columnsDetail = useMemo(function () { + return [ + { title: '业务员', dataIndex: 'salesperson', key: 'salesperson', width: 100, align: 'left' }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 200, ellipsis: true, align: 'left' }, + { title: '日期', dataIndex: 'bizDate', key: 'bizDate', width: 118, align: 'left', render: function (_v, r) { return fmtDateDash(r.bizDate); } }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 120, align: 'left' }, + { title: '加氢数量', dataIndex: 'quantityKg', key: 'quantityKg', width: 100, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '加氢地点', dataIndex: 'location', key: 'location', width: 260, ellipsis: true, align: 'left' }, + { title: '进价', dataIndex: 'purchasePrice', key: 'purchasePrice', width: 90, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '成本金额', dataIndex: 'costAmount', key: 'costAmount', width: 110, align: 'right', render: function (v) { return fmtCell(v); } }, + { title: '单价', dataIndex: 'unitPrice', key: 'unitPrice', width: 90, align: 'right', render: function (v) { return renderBoldMoney(v); } }, + { title: '金额', dataIndex: 'amount', key: 'amount', width: 110, align: 'right', render: function (v) { return renderBoldMoney(v); } } + ]; + }, []); + + var columnsTotal = useMemo(function () { + return [ + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 240, ellipsis: true, align: 'left' }, + { title: '统计月份', dataIndex: 'statMonth', key: 'statMonth', width: 110, align: 'center' }, + { title: '加氢数量合计', dataIndex: 'quantityKg', key: 'quantityKg', width: 130, align: 'right', render: function (v) { return fmtSum(v); } }, + { title: '金额合计', dataIndex: 'amount', key: 'amount', width: 130, align: 'right', render: function (v) { return React.createElement('span', { style: boldNumStyle }, fmtSum(v)); } } + ]; + }, []); + + var detailTableSummary = useCallback(function () { + return React.createElement( + TableSummary, + null, + React.createElement( + SummaryRow, + null, + React.createElement(SummaryCell, { index: 0, align: 'center', colSpan: 4 }, '汇总'), + React.createElement(SummaryCell, { index: 4, align: 'right' }, fmtSum(detailSums.quantityKg)), + React.createElement(SummaryCell, { index: 5, align: 'center' }, '—'), + React.createElement(SummaryCell, { index: 6, align: 'right' }, '—'), + React.createElement(SummaryCell, { index: 7, align: 'right' }, fmtSum(detailSums.costAmount)), + React.createElement(SummaryCell, { index: 8, align: 'center' }, '—'), + React.createElement(SummaryCell, { index: 9, align: 'right' }, + React.createElement('span', { style: boldNumStyle }, fmtSum(detailSums.amount)) + ) + ) + ); + }, [detailSums]); + + var totalTableSummary = useCallback(function () { + return React.createElement( + TableSummary, + null, + React.createElement( + SummaryRow, + null, + React.createElement(SummaryCell, { index: 0, align: 'center', colSpan: 2 }, '总计'), + React.createElement(SummaryCell, { index: 2, align: 'right' }, fmtSum(totalTabSums.quantityKg)), + React.createElement(SummaryCell, { index: 3, align: 'right' }, + React.createElement('span', { style: boldNumStyle }, fmtSum(totalTabSums.amount)) + ) + ) + ); + }, [totalTabSums]); + + var tabItems = [ + { + key: 'detail', + label: '租赁客户氢费明细', + children: React.createElement(React.Fragment, null, + React.createElement(Card, { style: { marginBottom: 16 } }, + React.createElement(Row, { gutter: [16, 16], align: 'bottom' }, + React.createElement(Col, { xs: 24, sm: 12, md: 10, lg: 8 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '日期范围'), + React.createElement(RangePicker, { + style: filterControlStyle, + placeholder: ['开始日期', '结束日期'], + format: 'YYYY-MM-DD', + value: draftDetail.dateRange, + onChange: function (v) { setDraftDetail(function (p) { return Object.assign({}, p, { dateRange: v }); }); } + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 7, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '业务员'), + React.createElement(Select, { + placeholder: '请选择业务员', + style: filterControlStyle, + allowClear: true, + options: salespersonOptions, + value: draftDetail.salesperson, + onChange: function (v) { setDraftDetail(function (p) { return Object.assign({}, p, { salesperson: v }); }); }, + showSearch: true, + filterOption: filterOption + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 7, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '客户名称'), + React.createElement(Select, { + placeholder: '请选择客户名称', + style: filterControlStyle, + allowClear: true, + options: customerOptions, + value: draftDetail.customerName, + onChange: function (v) { setDraftDetail(function (p) { return Object.assign({}, p, { customerName: v }); }); }, + showSearch: true, + filterOption: filterOption + }) + ) + ) + ), + React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 } }, + React.createElement(Button, { onClick: handleResetDetail }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleQueryDetail }, '查询') + ) + ), + React.createElement('div', { style: listToolbarStyle }, + React.createElement(Button, { onClick: handleExportDetail }, '导出') + ), + React.createElement(Card, null, + React.createElement('div', { style: bizReportTableTitleStyle }, detailTitle), + React.createElement(React.Fragment, null, + React.createElement('style', null, tableSingleLineStyle), + React.createElement('div', { className: 'contract-list-table' }, + React.createElement(Table, { + rowKey: 'key', + columns: columnsDetail, + dataSource: filteredDetailRows, + pagination: false, + size: 'small', + summary: detailTableSummary, + scroll: { x: 1320 } + }) + ) + ) + ) + ) + }, + { + key: 'total', + label: '租赁客户氢费总计', + children: React.createElement(React.Fragment, null, + React.createElement(Card, { style: { marginBottom: 16 } }, + React.createElement(Row, { gutter: [16, 16], align: 'bottom' }, + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '统计月份'), + React.createElement(DatePicker, { + picker: 'month', + style: filterControlStyle, + placeholder: '请选择年-月', + format: 'YYYY-MM', + value: draftTotal.month, + onChange: function (v) { setDraftTotal(function (p) { return Object.assign({}, p, { month: v }); }); } + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '客户名称'), + React.createElement(Select, { + placeholder: '全部客户', + style: filterControlStyle, + allowClear: true, + options: customerOptions, + value: draftTotal.customerName, + onChange: function (v) { setDraftTotal(function (p) { return Object.assign({}, p, { customerName: v }); }); }, + showSearch: true, + filterOption: filterOption + }) + ) + ) + ), + React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 } }, + React.createElement(Button, { onClick: handleResetTotal }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleQueryTotal }, '查询') + ) + ), + React.createElement('div', { style: listToolbarStyle }, + React.createElement(Button, { onClick: handleExportTotal }, '导出') + ), + React.createElement(Card, null, + React.createElement('div', { style: bizReportTableTitleStyle }, totalTitle), + React.createElement(React.Fragment, null, + React.createElement('style', null, tableSingleLineStyle), + React.createElement('div', { className: 'contract-list-table' }, + React.createElement(Table, { + rowKey: 'key', + columns: columnsTotal, + dataSource: totalByCustomerMonthRows, + pagination: false, + size: 'small', + summary: totalTableSummary, + scroll: { x: 720 } + }) + ) + ) + ) + ) + } + ]; + + return React.createElement(App, null, + React.createElement('div', { style: layoutStyle }, + React.createElement(Breadcrumb, { + style: { marginBottom: 16 }, + items: [{ title: '数据分析' }, { title: '租赁客户氢费台账' }] + }), + React.createElement(Card, null, + React.createElement('style', null, tabsBarStyle), + React.createElement(Tabs, { + className: 'lease-h2-ledger-tabs', + activeKey: mainTab, + onChange: function (k) { setMainTab(k); }, + items: tabItems + }) + ) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-finance/index.tsx b/src/prototypes/oneos-web-finance/index.tsx new file mode 100644 index 0000000..42aef57 --- /dev/null +++ b/src/prototypes/oneos-web-finance/index.tsx @@ -0,0 +1,39 @@ +/** + * @name 财务管理 + * 自 ONE-OS web端 原稿复刻(10 个页面) + */ +import React from 'react'; +import Page1 from './pages/01-还车应结款-查看.jsx'; +import Page2 from './pages/02-还车应结款-费用明细.jsx'; +import Page3 from './pages/03-还车应结款.jsx'; +import Page4 from './pages/04-提车收款单-编辑.jsx'; +import Page5 from './pages/05-提车应收款-查看.jsx'; +import Page6 from './pages/06-提车应收款-开票信息.jsx'; +import Page7 from './pages/07-提车应收款-审核.jsx'; +import Page8 from './pages/08-提车应收款-提车收款单.jsx'; +import Page9 from './pages/09-提车应收款.jsx'; +import Page10 from './pages/10-租赁账单.jsx'; +import { OneosWebLegacyShell } from '../../common/oneosWebLegacy/OneosWebLegacyShell'; + +const pages = [ + { id: 'u8d22u52a1u7ba1u7406-u8fd8u8f66u5e94u7ed3u6b3e-u', title: '还车应结款-查看', component: Page1 }, + { id: 'u8d22u52a1u7ba1u7406-u8fd8u8f66u5e94u7ed3u6b3e-u-2', title: '还车应结款-费用明细', component: Page2 }, + { id: 'u8d22u52a1u7ba1u7406-u8fd8u8f66u5e94u7ed3u6b3e', title: '还车应结款', component: Page3 }, + { id: 'u8d22u52a1u7ba1u7406-u63d0u8f66u6536u6b3eu5355-u', title: '提车收款单-编辑', component: Page4 }, + { id: 'u8d22u52a1u7ba1u7406-u63d0u8f66u5e94u6536u6b3e-u', title: '提车应收款-查看', component: Page5 }, + { id: 'u8d22u52a1u7ba1u7406-u63d0u8f66u5e94u6536u6b3e-u-6', title: '提车应收款-开票信息', component: Page6 }, + { id: 'u8d22u52a1u7ba1u7406-u63d0u8f66u5e94u6536u6b3e-u-7', title: '提车应收款-审核', component: Page7 }, + { id: 'u8d22u52a1u7ba1u7406-u63d0u8f66u5e94u6536u6b3e-u-8', title: '提车应收款-提车收款单', component: Page8 }, + { id: 'u8d22u52a1u7ba1u7406-u63d0u8f66u5e94u6536u6b3e', title: '提车应收款', component: Page9 }, + { id: 'u8d22u52a1u7ba1u7406-u79dfu8d41u8d26u5355', title: '租赁账单', component: Page10 }, +]; + +export default function OneosWebFinance() { + return ( + + ); +} diff --git a/src/prototypes/oneos-web-finance/pages/01-还车应结款-查看.jsx b/src/prototypes/oneos-web-finance/pages/01-还车应结款-查看.jsx new file mode 100644 index 0000000..db0edec --- /dev/null +++ b/src/prototypes/oneos-web-finance/pages/01-还车应结款-查看.jsx @@ -0,0 +1,644 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 财务管理 - 还车应结款 - 查看(只读) + +const Component = function () { + var useMemo = React.useMemo; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Table = antd.Table; + var Button = antd.Button; + var Input = antd.Input; + var Tooltip = antd.Tooltip; + var Popover = antd.Popover; + var message = antd.message; + + var TextArea = Input.TextArea; + + function toFixed2(v) { + if (v === null || v === undefined || v === '') return ''; + var n = typeof v === 'number' ? v : parseFloat(v); + return isNaN(n) ? '' : n.toFixed(2); + } + + function fmtMoney(v) { + var n = typeof v === 'number' ? v : parseFloat(v); + if (isNaN(n)) n = 0; + return n.toFixed(2); + } + + function fmtYMDHM(v) { + if (v === null || v === undefined) return '-'; + var s = String(v).trim(); + if (!s) return '-'; + if (/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}/.test(s)) return s.slice(0, 16); + if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s + ' 00:00'; + try { + var d = new Date(s.replace(/-/g, '/')); + if (isNaN(d.getTime())) return s; + var p2 = function (n) { return n < 10 ? '0' + n : '' + n; }; + return d.getFullYear() + '-' + p2(d.getMonth() + 1) + '-' + p2(d.getDate()) + ' ' + p2(d.getHours()) + ':' + p2(d.getMinutes()); + } catch (e) { + return s; + } + } + + // 页面样式 + var layoutStyle = { padding: '16px 24px 72px', background: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; + var footerStyle = { position: 'fixed', left: 0, right: 0, bottom: 0, background: '#fff', borderTop: '1px solid #f0f0f0', padding: '12px 24px', display: 'flex', justifyContent: 'flex-start', gap: 12, zIndex: 10 }; + + // 还车车辆明细(单车示例) + var vehicleDetail = useMemo(function () { + return [{ + key: 'v1', + plateNo: '粤AGP5621', + contractCode: 'LNZLHT20251106001', + projectName: '嘉兴腾4.5T租赁', + customerName: '嘉兴某某物流有限公司', + deliveryTime: '2026-02-01 09:30', + returnTime: '2026-02-27 16:20', + fragileInsurance: '是', + tireInsurance: '否', + maintenanceInsurance: '是' + }]; + }, []); + + // 业务服务组明细(只读样例) + var businessServiceRows = useMemo(function () { + var items = ['租金', 'ETC-客户未缴费用', 'ETC卡缺损费', 'ETC设备缺损费']; + return items.map(function (name, i) { + return { + key: 'bs-' + i, + seq: i + 1, + feeItem: name, + amount: i === 1 ? '100.00' : '0.00', + remark: '', + lastUpdateTime: '2026-02-27 10:20', + photos: [], + attachments: [] + }; + }); + }, []); + + var billInfo = useMemo(function () { + return { + receivedRent: '0.00', + actualRent: '0.00', + shouldRefundRent: '0.00' + }; + }, []); + + var energy = useMemo(function () { + return { + deliveryHydrogen: '85.00', + returnHydrogen: '72.00', + hydrogenUnitPrice: '35.00', + hydrogenSupplement: '455.00', + hydrogenFee: '0.00', + electricFee: '0.00', + prepayRefund: '0.00', + userBalance: '1200.00' + }; + }, []); + + // 运维部明细(只读样例) + var tireTreadList = useMemo(function () { + var unit = 25; // 元/mm + var rows = [ + { key: 't1', name: '左前轮', deliveryDepth: '6.80', returnDepth: '6.20' }, + { key: 't2', name: '左后轮(内)', deliveryDepth: '6.70', returnDepth: '6.00' }, + { key: 't3', name: '左后轮(外)', deliveryDepth: '6.60', returnDepth: '6.10' }, + { key: 't4', name: '右前轮', deliveryDepth: '6.90', returnDepth: '6.40' }, + { key: 't5', name: '右后轮(内)', deliveryDepth: '6.50', returnDepth: '6.00' }, + { key: 't6', name: '右后轮(外)', deliveryDepth: '6.80', returnDepth: '6.30' }, + { key: 't7', name: '左中轮', deliveryDepth: '6.70', returnDepth: '6.20' }, + { key: 't8', name: '右中轮', deliveryDepth: '6.60', returnDepth: '6.10' }, + { key: 't9', name: '左后备位轮', deliveryDepth: '6.90', returnDepth: '6.50' }, + { key: 't10', name: '右后备位轮', deliveryDepth: '6.40', returnDepth: '6.00' }, + { key: 'spare', name: '备胎', deliveryDepth: '7.00', returnDepth: '7.00' } + ]; + return rows.map(function (r) { + var d = parseFloat(r.deliveryDepth) || 0; + var rr = parseFloat(r.returnDepth) || 0; + var diff = Math.max(0, d - rr); + var total = diff * unit; + return { + key: r.key, + name: r.name, + deliveryDepth: d.toFixed(2), + returnDepth: rr.toFixed(2), + diff: diff.toFixed(2), + unitPrice: unit.toFixed(2), + totalAmount: total.toFixed(2) + }; + }); + }, []); + + var operationRows = useMemo(function () { + var fixed = [ + { name: '清洗费', amount: '0.00' }, + { name: '未结算保养', amount: '372.50' }, + { name: '未结算维修', amount: '0.00' }, + { name: '车损费用', amount: '0.00' }, + { name: '工具损坏丢失费用', amount: '0.00' }, + { name: '证件丢失费用', amount: '0.00' }, + { name: '广告损坏丢失费用', amount: '0.00' }, + { name: '送车服务费', amount: '0.00' }, + { name: '接车服务费', amount: '0.00' }, + { name: '轮胎磨损费用', amount: '0.00' } + ]; + return fixed.map(function (x, i) { + return { key: 'op-' + i, seq: i + 1, feeItem: x.name, amount: x.amount, worryFreeDiscount: '0.00', remark: '', lastUpdateTime: '2026-02-27 10:20' }; + }); + }, []); + + // 安全组费用项(只读样例) + var safetyFeeRows = useMemo(function () { + var items = ['违章处理违约金', '保险上浮', '其他违规费用']; + return items.map(function (name, i) { + return { + key: 'sf-' + i, + seq: i + 1, + feeItem: name, + amount: i === 0 ? '200.00' : '0.00', + remark: '', + lastUpdateTime: '2026-02-27 10:20', + photos: [], + attachments: [] + }; + }); + }, []); + + // 安全组(只读示例) + var violationList = useMemo(function () { + return [{ + key: 'w1', + code: 'WZ202602010001', + plateNo: '浙F03218F', + violationBehavior: '闯红灯', + violationTime: '2026-02-01', + penaltyAmount: '100.00', + paymentStatus: '未缴费', + score: '6', + handleStatus: '未处理', + violationCustomer: '上海馨想事成物流有限公司', + violationPhoto: '', + remark: '' + }]; + }, []); + var accidentList = useMemo(function () { + return [{ + key: 'a1', + accidentCode: 'SG202508250001', + plateNo: '京A29256F', + accidentTime: '2025-08-25', + accidentPlace: '北京市大兴区某路段', + accidentType: '撞固定物', + customerName: '北京海龙运输有限公司', + ourClaimAmount: '', + theirClaimAmount: '', + responsibility: '全责', + accidentStatus: '未结案', + closeTime: '', + otherFee: '', + remark: '' + }]; + }, []); + + // 合计/统计(只读) + var businessServiceTotal = useMemo(function () { + var sum = 0; + (businessServiceRows || []).forEach(function (r) { sum += (parseFloat(r.amount) || 0); }); + return sum.toFixed(2); + }, [businessServiceRows]); + var operationTotal = useMemo(function () { + var sum = 0; + (operationRows || []).forEach(function (r) { sum += (parseFloat(r.amount) || 0); }); + return sum.toFixed(2); + }, [operationRows]); + var safetyTotal = useMemo(function () { + var sum = 0; + (safetyFeeRows || []).forEach(function (r) { sum += (parseFloat(r.amount) || 0); }); + return sum.toFixed(2); + }, [safetyFeeRows]); + var energyTotal = useMemo(function () { + var a = parseFloat(energy.hydrogenSupplement) || 0; + var b = parseFloat(energy.hydrogenFee) || 0; + var c = parseFloat(energy.electricFee) || 0; + return (a + b + c).toFixed(2); + }, [energy.hydrogenSupplement, energy.hydrogenFee, energy.electricFee]); + + var pendingSettle = useMemo(function () { + var bs = parseFloat(businessServiceTotal) || 0; + var rentRefund = parseFloat(billInfo.shouldRefundRent) || 0; + var op = parseFloat(operationTotal) || 0; + var en = (parseFloat(energy.hydrogenSupplement) || 0) + (parseFloat(energy.hydrogenFee) || 0) + (parseFloat(energy.electricFee) || 0) - (parseFloat(energy.prepayRefund) || 0); + var sf = parseFloat(safetyTotal) || 0; + return (bs + rentRefund + en + op + sf).toFixed(2); + }, [businessServiceTotal, billInfo.shouldRefundRent, operationTotal, energy.hydrogenSupplement, energy.hydrogenFee, energy.electricFee, energy.prepayRefund, safetyTotal]); + + var depositAmount = '5000.00'; + var refundTotal = useMemo(function () { + var diff = (parseFloat(depositAmount) || 0) - (parseFloat(pendingSettle) || 0); + return diff > 0 ? diff.toFixed(2) : '0.00'; + }, [depositAmount, pendingSettle]); + var payTotal = useMemo(function () { + var diff = (parseFloat(depositAmount) || 0) - (parseFloat(pendingSettle) || 0); + return diff < 0 ? Math.abs(diff).toFixed(2) : '0.00'; + }, [depositAmount, pendingSettle]); + + function buildBreakdownContent(list) { + var headStyle = { padding: '6px 10px', textAlign: 'left', borderBottom: '1px solid #f0f0f0', background: '#fafafa', fontWeight: 600, fontSize: 12 }; + var tdStyle = { padding: '6px 10px', borderBottom: '1px solid #f0f0f0', fontSize: 12 }; + return React.createElement('div', { style: { padding: 0, minWidth: 320 } }, + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse' } }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: headStyle }, '费用项'), + React.createElement('th', { style: Object.assign({}, headStyle, { textAlign: 'right' }) }, '金额(元)') + ) + ), + React.createElement('tbody', null, + (list || []).map(function (r) { + return React.createElement('tr', { key: r.key }, + React.createElement('td', { style: tdStyle }, r.item), + React.createElement('td', { style: Object.assign({}, tdStyle, { fontWeight: r.strong ? 600 : 400, textAlign: 'right' }) }, r.amount) + ); + }) + ) + ) + ); + } + + var settleBreakdown = useMemo(function () { + return [ + { key: 'bs', item: '业务服务组费用项金额总和', amount: businessServiceTotal || '0.00' }, + { key: 'rent', item: '业务服务组-车辆应退租金', amount: (toFixed2(billInfo.shouldRefundRent) || '0.00') }, + { key: 'hDiff', item: '能源采购组-氢量差补缴金额', amount: (toFixed2(energy.hydrogenSupplement) || '0.00') }, + { key: 'hFee', item: '能源采购组-氢费补缴金额', amount: (toFixed2(energy.hydrogenFee) || '0.00') }, + { key: 'eFee', item: '能源采购组-电费补缴金额', amount: (toFixed2(energy.electricFee) || '0.00') }, + { key: 'prepay', item: '能源采购组-预付款退费金额(减)', amount: '-' + (toFixed2(energy.prepayRefund) || '0.00') }, + { key: 'op', item: '运维部费用项金额总额', amount: operationTotal || '0.00' }, + { key: 'sf', item: '安全组费用项金额总和', amount: safetyTotal || '0.00' }, + { key: 'total', item: '待结算总额', amount: pendingSettle || '0.00', strong: true } + ]; + }, [businessServiceTotal, billInfo.shouldRefundRent, energy.hydrogenSupplement, energy.hydrogenFee, energy.electricFee, energy.prepayRefund, operationTotal, safetyTotal, pendingSettle]); + + var refundBreakdown = useMemo(function () { + return [ + { key: 'd', item: '保证金总额', amount: (toFixed2(depositAmount) || '0.00') }, + { key: 's', item: '待结算总额', amount: (pendingSettle || '0.00') }, + { key: 'r', item: '应退还总额', amount: (refundTotal || '0.00'), strong: true } + ]; + }, [depositAmount, pendingSettle, refundTotal]); + + var payBreakdown = useMemo(function () { + return [ + { key: 'd', item: '保证金总额', amount: (toFixed2(depositAmount) || '0.00') }, + { key: 's', item: '待结算总额', amount: (pendingSettle || '0.00') }, + { key: 'p', item: '应补缴总额', amount: (payTotal || '0.00'), strong: true } + ]; + }, [depositAmount, pendingSettle, payTotal]); + + var vehicleColumns = useMemo(function () { + return [ + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 110 }, + { title: '合同编码', dataIndex: 'contractCode', key: 'contractCode', width: 160, ellipsis: true }, + { title: '项目名称', dataIndex: 'projectName', key: 'projectName', width: 140, ellipsis: true }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 140, ellipsis: true }, + { title: '交车时间', dataIndex: 'deliveryTime', key: 'deliveryTime', width: 160, render: function (v) { return fmtYMDHM(v); } }, + { title: '还车时间', dataIndex: 'returnTime', key: 'returnTime', width: 160, render: function (v) { return fmtYMDHM(v); } }, + { + title: '易损保', dataIndex: 'fragileInsurance', key: 'fragileInsurance', width: 110, + render: function (v) { + return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 6 } }, + (v || '-'), + React.createElement(Tooltip, { title: '提供刹车片、灯泡、蓄电池、雨刮等易损件租期内免费更换服务(不含轮胎,只限自行到服务站更换)' }, + React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 16, height: 16, borderRadius: 999, border: '1px solid #d9d9d9', color: '#999', fontSize: 12, lineHeight: '16px', cursor: 'help', userSelect: 'none' } }, 'i') + ) + ); + } + }, + { + title: '轮胎保', dataIndex: 'tireInsurance', key: 'tireInsurance', width: 110, + render: function (v) { + return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 6 } }, + (v || '-'), + React.createElement(Tooltip, { title: '每个租赁年度内提供1次车辆轮胎因自然磨损产生的替换服务' }, + React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 16, height: 16, borderRadius: 999, border: '1px solid #d9d9d9', color: '#999', fontSize: 12, lineHeight: '16px', cursor: 'help', userSelect: 'none' } }, 'i') + ) + ); + } + }, + { + title: '养护保', dataIndex: 'maintenanceInsurance', key: 'maintenanceInsurance', width: 110, + render: function (v) { + return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 6 } }, + (v || '-'), + React.createElement(Tooltip, { title: '按厂家保养要求提供定期保养服务' }, + React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 16, height: 16, borderRadius: 999, border: '1px solid #d9d9d9', color: '#999', fontSize: 12, lineHeight: '16px', cursor: 'help', userSelect: 'none' } }, 'i') + ) + ); + } + } + ]; + }, []); + + var businessServiceColumns = useMemo(function () { + return [ + { title: '序号', dataIndex: 'seq', key: 'seq', width: 60 }, + { title: '费用项', dataIndex: 'feeItem', key: 'feeItem', width: 200, ellipsis: true }, + { title: '金额', dataIndex: 'amount', key: 'amount', width: 140, render: function (v) { return React.createElement(Input, { value: v, addonAfter: '元', disabled: true }); } }, + { title: '备注', dataIndex: 'remark', key: 'remark', render: function (v) { return React.createElement(TextArea, { value: v, rows: 1, disabled: true }); } }, + { title: '最后更新时间', dataIndex: 'lastUpdateTime', key: 'lastUpdateTime', width: 150, render: function (v) { return React.createElement('span', { style: { fontSize: 12, color: '#666' } }, v || '-'); } }, + { title: '照片', key: 'photo', width: 100, render: function () { return React.createElement(Button, { size: 'small', disabled: true }, '上传'); } }, + { title: '附件', key: 'attachment', width: 110, render: function () { return React.createElement(Button, { size: 'small', disabled: true }, '上传附件'); } } + ]; + }, []); + + var safetyFeeColumns = useMemo(function () { + return [ + { title: '序号', dataIndex: 'seq', key: 'seq', width: 60 }, + { title: '费用项', dataIndex: 'feeItem', key: 'feeItem', width: 200, ellipsis: true }, + { title: '金额', dataIndex: 'amount', key: 'amount', width: 140, render: function (v) { return React.createElement(Input, { value: v, addonAfter: '元', disabled: true }); } }, + { title: '备注', dataIndex: 'remark', key: 'remark', render: function (v) { return React.createElement(TextArea, { value: v, rows: 1, disabled: true }); } }, + { title: '最后更新时间', dataIndex: 'lastUpdateTime', key: 'lastUpdateTime', width: 150, render: function (v) { return React.createElement('span', { style: { fontSize: 12, color: '#666' } }, v || '-'); } }, + { title: '照片', key: 'photo', width: 100, render: function () { return React.createElement(Button, { size: 'small', disabled: true }, '上传'); } }, + { title: '附件', key: 'attachment', width: 110, render: function () { return React.createElement(Button, { size: 'small', disabled: true }, '上传附件'); } } + ]; + }, []); + + var operationColumns = useMemo(function () { + return [ + { title: '序号', dataIndex: 'seq', key: 'seq', width: 60 }, + { + title: '费用项', dataIndex: 'feeItem', key: 'feeItem', width: 200, + render: function (v) { + if (v === '轮胎磨损费用') { + var popContent = React.createElement('div', { style: { width: 760, maxWidth: '80vw' } }, + React.createElement('div', { style: { fontWeight: 600, marginBottom: 8 } }, '轮胎磨损费用明细'), + React.createElement(Table, { + rowKey: 'key', + size: 'small', + bordered: true, + pagination: false, + dataSource: tireTreadList, + columns: [ + { title: '轮胎名称', dataIndex: 'name', key: 'name', width: 120, ellipsis: true }, + { title: '交车胎纹深度', dataIndex: 'deliveryDepth', key: 'deliveryDepth', width: 110, render: function (vv) { return (vv || '0.00') + 'mm'; } }, + { title: '还车胎纹深度', dataIndex: 'returnDepth', key: 'returnDepth', width: 110, render: function (vv) { return (vv || '0.00') + 'mm'; } }, + { title: '胎纹差', dataIndex: 'diff', key: 'diff', width: 80, render: function (vv) { return (vv || '0.00') + 'mm'; } }, + { title: '单价', dataIndex: 'unitPrice', key: 'unitPrice', width: 90, render: function (vv) { return (vv || '0.00') + '元/mm'; } }, + { title: '总金额', dataIndex: 'totalAmount', key: 'totalAmount', width: 90, render: function (vv) { return (vv || '0.00') + '元'; } } + ] + }) + ); + return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 6 } }, + (v || '-'), + React.createElement(Popover, { content: popContent, trigger: 'hover', placement: 'topLeft' }, + React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 16, height: 16, borderRadius: 999, border: '1px solid #d9d9d9', color: '#999', fontSize: 12, lineHeight: '16px', cursor: 'help', userSelect: 'none' } }, 'i') + ) + ); + } + return v || '-'; + } + }, + { title: '金额', dataIndex: 'amount', key: 'amount', width: 140, render: function (v) { return React.createElement(Input, { value: v, addonAfter: '元', disabled: true }); } }, + { + title: React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 6 } }, + '无忧包减免', + React.createElement(Tooltip, { title: '无忧包减免不会列入运维成本,而是计入业务成本' }, + React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 16, height: 16, borderRadius: 999, border: '1px solid #d9d9d9', color: '#999', fontSize: 12, lineHeight: '16px', cursor: 'help', userSelect: 'none' } }, 'i') + ) + ), + dataIndex: 'worryFreeDiscount', + key: 'worryFreeDiscount', + width: 140, + render: function (v) { return React.createElement(Input, { value: v, addonAfter: '元', disabled: true }); } + }, + { title: '备注', dataIndex: 'remark', key: 'remark', render: function (v) { return React.createElement(TextArea, { value: v, rows: 1, disabled: true }); } }, + { title: '最后更新时间', dataIndex: 'lastUpdateTime', key: 'lastUpdateTime', width: 150, render: function (v) { return React.createElement('span', { style: { fontSize: 12, color: '#666' } }, v || '-'); } }, + { title: '照片', key: 'photo', width: 100, render: function () { return React.createElement(Button, { size: 'small', disabled: true }, '上传'); } }, + { title: '附件', key: 'attachment', width: 110, render: function () { return React.createElement(Button, { size: 'small', disabled: true }, '上传附件'); } } + ]; + }, [tireTreadList]); + + function goBack() { + message.info('返回还车应结款列表页(原型)'); + try { if (window.history && window.history.length > 1) window.history.back(); } catch (e) {} + } + + return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + React.createElement(Breadcrumb, { items: [{ title: '财务管理' }, { title: '还车应结款' }, { title: '查看' }] }) + ), + + React.createElement(Card, { title: '还车车辆明细', style: cardStyle }, + React.createElement(Table, { rowKey: 'key', columns: vehicleColumns, dataSource: vehicleDetail, pagination: false, bordered: true, size: 'middle', scroll: { x: 980 } }) + ), + + React.createElement(Card, { title: '还车费用明细', style: cardStyle }, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 16, marginBottom: 16 } }, + React.createElement('div', { style: { background: '#fff', border: '1px solid #f0f0f0', borderRadius: 8, padding: '18px 16px', boxShadow: '0 1px 2px rgba(0,0,0,0.04)' } }, + React.createElement('div', { style: { fontSize: 13, color: '#666', marginBottom: 12 } }, '保证金总额'), + React.createElement('div', { style: { fontSize: 22, fontWeight: 600, color: '#333' } }, (depositAmount || '0.00'), React.createElement('span', { style: { fontSize: 14, fontWeight: 500, marginLeft: 4 } }, '元')) + ), + React.createElement('div', { style: { background: '#fff', border: '1px solid #f0f0f0', borderRadius: 8, padding: '18px 16px', boxShadow: '0 1px 2px rgba(0,0,0,0.04)' } }, + React.createElement('div', { style: { fontSize: 13, color: '#666', marginBottom: 12 } }, '待结算总额'), + React.createElement(Popover, { trigger: 'click', placement: 'bottomLeft', content: buildBreakdownContent(settleBreakdown) }, + React.createElement('div', { style: { fontSize: 22, fontWeight: 600, color: '#fa8c16', cursor: 'pointer' } }, + (pendingSettle || '0.00'), + React.createElement('span', { style: { fontSize: 14, fontWeight: 500, marginLeft: 4, color: '#333' } }, '元') + ) + ) + ), + React.createElement('div', { style: { background: '#fff', border: '1px solid #f0f0f0', borderRadius: 8, padding: '18px 16px', boxShadow: '0 1px 2px rgba(0,0,0,0.04)' } }, + React.createElement('div', { style: { fontSize: 13, color: '#666', marginBottom: 12 } }, '应退还总额'), + React.createElement(Popover, { trigger: 'click', placement: 'bottomLeft', content: buildBreakdownContent(refundBreakdown) }, + React.createElement('div', { style: { fontSize: 22, fontWeight: 600, color: '#52c41a', cursor: 'pointer' } }, + (refundTotal || '0.00'), + React.createElement('span', { style: { fontSize: 14, fontWeight: 500, marginLeft: 4, color: '#333' } }, '元') + ) + ) + ), + React.createElement('div', { style: { background: '#fff', border: '1px solid #f0f0f0', borderRadius: 8, padding: '18px 16px', boxShadow: '0 1px 2px rgba(0,0,0,0.04)' } }, + React.createElement('div', { style: { fontSize: 13, color: '#666', marginBottom: 12 } }, '应补缴总额'), + React.createElement(Popover, { trigger: 'click', placement: 'bottomLeft', content: buildBreakdownContent(payBreakdown) }, + React.createElement('div', { style: { fontSize: 22, fontWeight: 600, color: '#f5222d', cursor: 'pointer' } }, + (payTotal || '0.00'), + React.createElement('span', { style: { fontSize: 14, fontWeight: 500, marginLeft: 4, color: '#333' } }, '元') + ) + ) + ) + ), + + React.createElement('div', { style: { background: '#fff', border: '1px solid #f0f0f0', borderRadius: 8, marginBottom: 12 } }, + React.createElement('div', { style: { padding: '12px 16px', display: 'flex', alignItems: 'center', gap: 12 } }, + React.createElement('div', { style: { fontWeight: 600, fontSize: 14 } }, '业务服务组'), + React.createElement('div', { style: { flex: 1 } }), + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 12, color: '#666', fontSize: 13 } }, + React.createElement('span', null, '总金额:', businessServiceTotal, ' 元'), + React.createElement('span', null, '提交人:业务服务组-张三'), + React.createElement('span', null, '状态:已提交') + ) + ), + React.createElement('div', { style: { padding: '12px 16px', borderTop: '1px solid #f0f0f0' } }, + React.createElement(Table, { rowKey: 'key', columns: businessServiceColumns, dataSource: businessServiceRows, pagination: false, bordered: true, size: 'small' }), + React.createElement('div', { style: { marginTop: 12, paddingTop: 12, borderTop: '1px dashed #f0f0f0' } }, + React.createElement('div', { style: { fontWeight: 600, marginBottom: 10 } }, '车辆租金'), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 12 } }, + React.createElement('div', null, + React.createElement('div', { style: { fontSize: 12, color: '#666', marginBottom: 6 } }, '本期账单已收租金'), + React.createElement(Input, { value: billInfo.receivedRent, addonAfter: '元', disabled: true }) + ), + React.createElement('div', null, + React.createElement('div', { style: { fontSize: 12, color: '#666', marginBottom: 6 } }, '车辆实际租金'), + React.createElement(Input, { value: billInfo.actualRent, addonAfter: '元', disabled: true }) + ), + React.createElement('div', null, + React.createElement('div', { style: { fontSize: 12, color: '#666', marginBottom: 6 } }, '车辆应退租金'), + React.createElement(Input, { value: billInfo.shouldRefundRent, addonAfter: '元', disabled: true }) + ) + ) + ) + ) + ), + + React.createElement('div', { style: { background: '#fff', border: '1px solid #f0f0f0', borderRadius: 8, marginBottom: 12 } }, + React.createElement('div', { style: { padding: '12px 16px', display: 'flex', alignItems: 'center', gap: 12 } }, + React.createElement('div', { style: { fontWeight: 600, fontSize: 14 } }, '能源采购组'), + React.createElement('div', { style: { flex: 1 } }), + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 12, color: '#666', fontSize: 13 } }, + React.createElement('span', null, '总金额:', energyTotal, ' 元'), + React.createElement('span', null, '提交人:能源采购组-李四'), + React.createElement('span', null, '状态:已提交') + ) + ), + React.createElement('div', { style: { padding: '12px 16px', borderTop: '1px solid #f0f0f0' } }, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 12, marginBottom: 16 } }, + React.createElement('div', null, + React.createElement('div', { style: { marginBottom: 8, fontWeight: 500 } }, '氢量差补缴金额'), + React.createElement(Input, { value: energy.hydrogenSupplement, addonAfter: '元', disabled: true }) + ), + React.createElement('div', null, + React.createElement('div', { style: { marginBottom: 8, fontWeight: 500 } }, '交车氢量'), + React.createElement(Input, { value: (toFixed2(energy.deliveryHydrogen) || '0.00'), addonAfter: 'MPa', disabled: true }) + ), + React.createElement('div', null, + React.createElement('div', { style: { marginBottom: 8, fontWeight: 500 } }, '还车氢量'), + React.createElement(Input, { value: (toFixed2(energy.returnHydrogen) || '0.00'), addonAfter: 'MPa', disabled: true }) + ), + React.createElement('div', null, + React.createElement('div', { style: { marginBottom: 8, fontWeight: 500 } }, '退还车氢气单价'), + React.createElement(Input, { value: (toFixed2(energy.hydrogenUnitPrice) || '0.00'), addonAfter: '元', disabled: true }) + ) + ), + React.createElement('div', null, + React.createElement('div', { style: { marginBottom: 8, fontWeight: 500 } }, '能源费补缴金额'), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 12, marginBottom: 12 } }, + React.createElement('div', null, React.createElement(Input, { value: energy.hydrogenFee, placeholder: '氢费补缴金额', addonAfter: '元', style: { width: '100%' }, disabled: true })), + React.createElement('div', null, React.createElement(Input, { value: energy.electricFee, placeholder: '电费补缴金额', addonAfter: '元', style: { width: '100%' }, disabled: true })), + React.createElement('div', null), + React.createElement('div', null) + ) + ), + React.createElement('div', { style: { gridColumn: '1 / -1', borderTop: '1px solid #f0f0f0', paddingTop: 12, marginTop: 4 } }, + React.createElement('div', { style: { marginBottom: 8, fontWeight: 500 } }, '预付款退费金额'), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 12 } }, + React.createElement('div', null, + React.createElement(Input, { value: energy.prepayRefund, placeholder: '0.00', addonAfter: '元', style: { width: '100%' }, disabled: true }) + ), + React.createElement('div', null), + React.createElement('div', null), + React.createElement('div', null) + ), + React.createElement('div', { style: { marginTop: 8, fontSize: 12, color: '#666' } }, '项目预充值余额:', energy.userBalance, ' 元') + ) + ) + ), + + React.createElement('div', { style: { background: '#fff', border: '1px solid #f0f0f0', borderRadius: 8, marginBottom: 12 } }, + React.createElement('div', { style: { padding: '12px 16px', display: 'flex', alignItems: 'center', gap: 12 } }, + React.createElement('div', { style: { fontWeight: 600, fontSize: 14 } }, '运维部'), + React.createElement('div', { style: { flex: 1 } }), + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 12, color: '#666', fontSize: 13 } }, + React.createElement('span', null, '总金额:', operationTotal, ' 元'), + React.createElement('span', null, '提交人:运维部-王五'), + React.createElement('span', null, '状态:已提交') + ) + ), + React.createElement('div', { style: { padding: '12px 16px', borderTop: '1px solid #f0f0f0' } }, + React.createElement(Table, { rowKey: 'key', columns: operationColumns, dataSource: operationRows, pagination: false, bordered: true, size: 'small' }) + ) + ), + + React.createElement('div', { style: { background: '#fff', border: '1px solid #f0f0f0', borderRadius: 8, marginBottom: 12 } }, + React.createElement('div', { style: { padding: '12px 16px', display: 'flex', alignItems: 'center', gap: 12 } }, + React.createElement('div', { style: { fontWeight: 600, fontSize: 14 } }, '安全组'), + React.createElement('div', { style: { flex: 1 } }), + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 12, color: '#666', fontSize: 13 } }, + React.createElement('span', null, '总金额:', safetyTotal, ' 元'), + React.createElement('span', null, '提交人:安全组-赵六'), + React.createElement('span', null, '状态:已提交') + ) + ), + React.createElement('div', { style: { padding: '12px 16px', borderTop: '1px solid #f0f0f0' } }, + React.createElement(Table, { rowKey: 'key', columns: safetyFeeColumns, dataSource: safetyFeeRows, pagination: false, bordered: true, size: 'small' }), + React.createElement('div', { style: { margin: '16px 0 12px', fontWeight: 600 } }, '违章清单'), + React.createElement(Table, { + rowKey: 'key', + size: 'small', + bordered: true, + pagination: false, + dataSource: violationList, + columns: [ + { title: '违章编码', dataIndex: 'code', key: 'code', width: 130, ellipsis: true }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 110 }, + { title: '违法行为', dataIndex: 'violationBehavior', key: 'violationBehavior', width: 110, ellipsis: true }, + { title: '违法时间', dataIndex: 'violationTime', key: 'violationTime', width: 110 }, + { title: '罚款金额', dataIndex: 'penaltyAmount', key: 'penaltyAmount', width: 90, render: function (v) { return (v || '0.00'); } }, + { title: '缴费状态', dataIndex: 'paymentStatus', key: 'paymentStatus', width: 90 }, + { title: '计分值', dataIndex: 'score', key: 'score', width: 70 }, + { title: '是否处理', dataIndex: 'handleStatus', key: 'handleStatus', width: 90 }, + { title: '违章客户', dataIndex: 'violationCustomer', key: 'violationCustomer', width: 160, ellipsis: true }, + { title: '违章照片', dataIndex: 'violationPhoto', key: 'violationPhoto', width: 90, render: function (v) { return v ? v : ''; } }, + { title: '备注', dataIndex: 'remark', key: 'remark', width: 120, ellipsis: true } + ] + }), + React.createElement('div', { style: { margin: '16px 0 12px', fontWeight: 600 } }, '事故清单'), + React.createElement(Table, { + rowKey: 'key', + size: 'small', + bordered: true, + pagination: false, + dataSource: accidentList, + locale: { emptyText: '暂无事故记录' }, + columns: [ + { title: '事故编码', dataIndex: 'accidentCode', key: 'accidentCode', width: 130, ellipsis: true }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 110 }, + { title: '事故时间', dataIndex: 'accidentTime', key: 'accidentTime', width: 110 }, + { title: '事故地点', dataIndex: 'accidentPlace', key: 'accidentPlace', width: 140, ellipsis: true }, + { title: '事故类型', dataIndex: 'accidentType', key: 'accidentType', width: 110, ellipsis: true }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 140, ellipsis: true }, + { title: '我方定损金额', dataIndex: 'ourClaimAmount', key: 'ourClaimAmount', width: 110, render: function (v) { return v || ''; } }, + { title: '对方定损金额', dataIndex: 'theirClaimAmount', key: 'theirClaimAmount', width: 110, render: function (v) { return v || ''; } }, + { title: '责任划分', dataIndex: 'responsibility', key: 'responsibility', width: 90 }, + { title: '事故状态', dataIndex: 'accidentStatus', key: 'accidentStatus', width: 90 }, + { title: '结案时间', dataIndex: 'closeTime', key: 'closeTime', width: 110 }, + { title: '其他费用', dataIndex: 'otherFee', key: 'otherFee', width: 90, render: function (v) { return v || ''; } }, + { title: '备注', dataIndex: 'remark', key: 'remark', width: 120, ellipsis: true } + ] + }) + ) + ) + ), + + React.createElement('div', { style: footerStyle }, + React.createElement(Button, { onClick: goBack }, '返回') + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-finance/pages/02-还车应结款-费用明细.jsx b/src/prototypes/oneos-web-finance/pages/02-还车应结款-费用明细.jsx new file mode 100644 index 0000000..fda7ab1 --- /dev/null +++ b/src/prototypes/oneos-web-finance/pages/02-还车应结款-费用明细.jsx @@ -0,0 +1,1526 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 财务管理 - 还车应结款 + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + var useEffect = React.useEffect; + var useRef = React.useRef; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Table = antd.Table; + var Button = antd.Button; + var Input = antd.Input; + var Modal = antd.Modal; + var Tooltip = antd.Tooltip; + var Popover = antd.Popover; + var message = antd.message; + + var TextArea = Input.TextArea; + + function RequiredLabel(text) { + return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 4 } }, + React.createElement('span', { style: { color: '#f5222d', fontWeight: 600 } }, '*'), + React.createElement('span', null, text) + ); + } + + function formatDateTimeNow() { + var d = new Date(); + var pad = function (n) { return n < 10 ? '0' + n : '' + n; }; + return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()); + } + + function fmtYMDHM(v) { + if (v === null || v === undefined) return '-'; + var s = String(v).trim(); + if (!s) return '-'; + if (/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}/.test(s)) return s.slice(0, 16); + if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s + ' 00:00'; + try { + var d = new Date(s.replace(/-/g, '/')); + if (isNaN(d.getTime())) return s; + var p2 = function (n) { return n < 10 ? '0' + n : '' + n; }; + return d.getFullYear() + '-' + p2(d.getMonth() + 1) + '-' + p2(d.getDate()) + ' ' + p2(d.getHours()) + ':' + p2(d.getMinutes()); + } catch (e) { + return s; + } + } + + function toFixed2(v) { + if (v === null || v === undefined || v === '') return ''; + var n = typeof v === 'number' ? v : parseFloat(v); + return isNaN(n) ? '' : n.toFixed(2); + } + + function filterSignedMoneyInput(raw) { + var s = String(raw == null ? '' : raw); + var neg = s.charAt(0) === '-'; + s = s.replace(/[^\d.]/g, ''); + var parts = s.split('.'); + if (parts.length > 2) { + s = parts[0] + '.' + parts.slice(1).join(''); + parts = s.split('.'); + } + if (parts[1] && parts[1].length > 2) s = parts[0] + '.' + parts[1].slice(0, 2); + return (neg ? '-' : '') + s; + } + + function formatSignedMoney2(raw) { + if (raw === null || raw === undefined) return ''; + var v = String(raw).trim(); + if (!v || v === '-') return v === '-' ? '-' : ''; + var n = parseFloat(v); + return isNaN(n) ? '' : n.toFixed(2); + } + + function isValidSignedMoney2(s) { + if (s === null || s === undefined) return false; + var v = String(s).trim(); + if (!v || v === '-') return false; + if (!/^-?\d+(\.\d{1,2})?$/.test(v)) return false; + return !isNaN(parseFloat(v)); + } + + // 页面样式 + var layoutStyle = { padding: '16px 24px 88px', background: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; + var footerStyle = { position: 'fixed', left: 0, right: 0, bottom: 0, background: '#fff', borderTop: '1px solid #f0f0f0', padding: '12px 24px', display: 'flex', justifyContent: 'flex-start', gap: 12, zIndex: 10 }; + + // 还车车辆明细(单车示例) + var vehicleDetail = useMemo(function () { + return [{ + key: 'v1', + plateNo: '粤AGP5621', + contractCode: 'LNZLHT20251106001', + projectName: '嘉兴腾4.5T租赁', + customerName: '嘉兴某某物流有限公司', + deliveryTime: '2026-02-01 09:30', + returnTime: '2026-02-27 16:20', + fragileInsurance: '是', + tireInsurance: '否', + maintenanceInsurance: '是' + }]; + }, []); + + var insuranceFlags = useMemo(function () { + var v = (vehicleDetail && vehicleDetail[0]) || {}; + return { + hasFragileInsurance: String(v.fragileInsurance || '') === '是', + hasTireInsurance: String(v.tireInsurance || '') === '是', + hasMaintenanceInsurance: String(v.maintenanceInsurance || '') === '是' + }; + }, [vehicleDetail]); + + // 原型判断:当前合同仅展示 1 辆车,则视为“最后一辆车还车” + var isLastVehicleInContract = useMemo(function () { + return (vehicleDetail || []).length === 1; + }, [vehicleDetail]); + + // 费用统计 + var statsState = useState({ + depositAmount: '5000.00', + pendingSettleAmount: '0.00', + shouldRefundAmount: '800.00', + shouldPayAmount: '400.00' + }); + var stats = statsState[0]; + + // 通用折叠区块(展开/收起图标) + var CollapseSection = function (props) { + var collapsed = props.collapsed; + var setCollapsed = props.setCollapsed; + var title = props.title; + var extra = props.extra; + var headerActions = props.headerActions; + return React.createElement('div', { style: { background: '#fff', border: '1px solid #f0f0f0', borderRadius: 8, marginBottom: 12, overflow: 'hidden' } }, + React.createElement('div', { + style: { padding: '12px 16px', display: 'flex', alignItems: 'center', gap: 12, cursor: 'pointer', background: '#fff' }, + onClick: function () { setCollapsed(!collapsed); } + }, + React.createElement('div', { style: { fontWeight: 600, fontSize: 14 } }, title), + React.createElement('div', { style: { flex: 1 } }), + extra ? React.createElement('div', { style: { display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 12, color: '#666', fontSize: 13 } }, extra) : null, + headerActions ? React.createElement('div', { onClick: function (e) { e.stopPropagation(); }, style: { display: 'inline-flex', alignItems: 'center', gap: 8 } }, headerActions) : null, + collapsed + ? React.createElement('svg', { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, style: { display: 'block', color: '#666' } }, React.createElement('path', { d: 'M6 9l6 6 6-6' })) + : React.createElement('svg', { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, style: { display: 'block', color: '#666' } }, React.createElement('path', { d: 'M18 15l-6-6-6 6' })) + ), + collapsed ? null : React.createElement('div', { style: { padding: '12px 16px', borderTop: '1px solid #f0f0f0' } }, props.children) + ); + }; + + // 业务服务组 + var businessServiceFixedItems = ['租金', 'ETC-客户未缴费用', 'ETC卡缺损费', 'ETC设备缺损费']; + var businessServiceRowsState = useState( + businessServiceFixedItems.map(function (name, i) { + return { key: 'bs-' + i, seq: i + 1, feeItem: name, amount: '', remark: '', lastUpdateTime: '', photos: [], attachments: [], fixed: true }; + }) + ); + var businessServiceRows = businessServiceRowsState[0]; + var setBusinessServiceRows = businessServiceRowsState[1]; + var businessServiceMetaState = useState({ submitBy: '', status: '待提交' }); + var businessServiceMeta = businessServiceMetaState[0]; + var setBusinessServiceMeta = businessServiceMetaState[1]; + var businessServiceCollapsedState = useState(false); + + function recalcBusinessServiceTotal(list) { + var sum = 0; + (list || []).forEach(function (r) { sum += (parseFloat(r.amount) || 0); }); + return sum.toFixed(2); + } + + var businessServiceTotalComputed = useMemo(function () { + return recalcBusinessServiceTotal(businessServiceRows || []); + }, [businessServiceRows]); + + function calcVehicleRent(monthRent, billStartDateStr, returnDateStr) { + var m = parseFloat(monthRent) || 0; + if (!billStartDateStr || !returnDateStr) return '0.00'; + var s = new Date(String(billStartDateStr).replace(/-/g, '/')); + var e = new Date(String(returnDateStr).replace(/-/g, '/')); + if (isNaN(s.getTime()) || isNaN(e.getTime())) return '0.00'; + var msDay = 24 * 60 * 60 * 1000; + var days = Math.floor((e.getTime() - s.getTime()) / msDay); + if (days < 0) days = 0; + return ((m / 30) * days).toFixed(2); + } + + var billInfoState = useState({ + billStartDate: '2026-02-01', + vehicleMonthRent: '9000.00', + receivedRent: '0.00', + actualRent: '', + actualRentManual: false, + shouldRefundRent: '', + shouldRefundManual: false + }); + var billInfo = billInfoState[0]; + var setBillInfo = billInfoState[1]; + + useEffect(function () { + if (billInfo.actualRentManual && String(billInfo.actualRent || '').trim() !== '') return; + var returnDateStr = (vehicleDetail && vehicleDetail[0] && vehicleDetail[0].returnTime) || ''; + var calc = calcVehicleRent(billInfo.vehicleMonthRent, billInfo.billStartDate, returnDateStr); + if (billInfo.actualRent !== calc) { + setBillInfo(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.actualRent = calc; return n; }); + } + }, [billInfo.vehicleMonthRent, billInfo.billStartDate, billInfo.actualRentManual, vehicleDetail]); + + useEffect(function () { + if (billInfo.shouldRefundManual && String(billInfo.shouldRefundRent || '').trim() !== '') return; + var received = parseFloat(billInfo.receivedRent) || 0; + var actual = parseFloat(billInfo.actualRent) || 0; + var calc = (received - actual).toFixed(2); + if (billInfo.shouldRefundRent !== calc) { + setBillInfo(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.shouldRefundRent = calc; return n; }); + } + }, [billInfo.receivedRent, billInfo.actualRent, billInfo.shouldRefundManual]); + + var addBusinessServiceRow = useCallback(function () { + setBusinessServiceRows(function (p) { + var next = p.slice(); + var i = next.length + 1; + next.push({ key: 'bs-new-' + Date.now(), seq: i, feeItem: '', amount: '', remark: '', lastUpdateTime: '', photos: [], attachments: [], fixed: false }); + return next.map(function (r, idx) { var o = {}; for (var k in r) o[k] = r[k]; o.seq = idx + 1; return o; }); + }); + }, []); + var removeBusinessServiceRow = useCallback(function (key) { + setBusinessServiceRows(function (p) { + var next = p.filter(function (r) { return r.key !== key; }); + return next.map(function (r, idx) { var o = {}; for (var k in r) o[k] = r[k]; o.seq = idx + 1; return o; }); + }); + }, []); + + var updateBusinessServiceRow = useCallback(function (key, field, value) { + setBusinessServiceRows(function (p) { + var next = p.map(function (r) { + if (r.key !== key) return r; + var n = {}; for (var k in r) n[k] = r[k]; + n[field] = value; + return n; + }); + return next; + }); + }, []); + + var handleBusinessServiceSave = useCallback(function () { + setBusinessServiceMeta(function (m) { var nm = {}; for (var k in m) nm[k] = m[k]; nm.status = '待提交'; return nm; }); + message.success('已保存'); + }, []); + + var handleBusinessServiceSubmit = useCallback(function () { + Modal.confirm({ + title: '提交确认', + content: '请确认业务服务组金额填写无误,点击确认完成提交。', + okText: '确认', + cancelText: '取消', + onOk: function () { + setBusinessServiceRows(function (p) { + var now = formatDateTimeNow(); + return p.map(function (r) { var n = {}; for (var k in r) n[k] = r[k]; n.lastUpdateTime = now; return n; }); + }); + setBusinessServiceMeta(function (m) { var nm = {}; for (var k in m) nm[k] = m[k]; nm.status = '已提交'; nm.submitBy = '业务服务组-张三'; return nm; }); + message.success('已提交'); + } + }); + }, []); + + var handleBusinessServiceRevoke = useCallback(function () { + Modal.confirm({ + title: '撤回确认', + content: '是否确认撤回?', + okText: '确认', + cancelText: '取消', + onOk: function () { + setBusinessServiceMeta(function (m) { var nm = {}; for (var k in m) nm[k] = m[k]; nm.status = '待提交'; return nm; }); + message.success('已撤回,可重新编辑并保存/提交'); + } + }); + }, []); + + // 能源采购组 + var energyState = useState({ + deliveryHydrogen: '85.00', + returnHydrogen: '72.00', + hydrogenUnitPrice: '35.00', + hydrogenSupplement: '455.00', + hydrogenFee: '', + electricFee: '', + prepayRefund: '', + userBalance: '1200.00' + }); + var energy = energyState[0]; + var setEnergy = energyState[1]; + var energyMetaState = useState({ submitBy: '', status: '待提交' }); + var energyMeta = energyMetaState[0]; + var setEnergyMeta = energyMetaState[1]; + var energyCollapsedState = useState(false); + + function recalcEnergyTotal(e) { + var a = parseFloat((e && e.hydrogenSupplement) || '') || 0; + var b = parseFloat((e && e.hydrogenFee) || '') || 0; + var c = parseFloat((e && e.electricFee) || '') || 0; + return (a + b + c).toFixed(2); + } + + var energyTotalComputed = useMemo(function () { return recalcEnergyTotal(energy); }, [energy.hydrogenSupplement, energy.hydrogenFee, energy.electricFee]); + + useEffect(function () { + var d = parseFloat(energy.deliveryHydrogen) || 0; + var r = parseFloat(energy.returnHydrogen) || 0; + var u = parseFloat(energy.hydrogenUnitPrice) || 0; + if (d > r) { + var calc = ((d - r) * u).toFixed(2); + if (energy.hydrogenSupplement !== calc) { + setEnergy(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.hydrogenSupplement = calc; return n; }); + } + } + }, [energy.deliveryHydrogen, energy.returnHydrogen, energy.hydrogenUnitPrice]); + + // energy 总金额使用 energyTotalComputed 派生,避免输入时额外 setState 造成丢焦 + + var handleEnergySave = useCallback(function () { + setEnergyMeta(function (m) { var nm = {}; for (var k in m) nm[k] = m[k]; nm.status = '待提交'; return nm; }); + message.success('能源采购组已保存'); + }, [energy]); + var handleEnergySubmit = useCallback(function () { + Modal.confirm({ + title: '提交确认', + content: '请确认能源采购组金额填写无误,点击确认完成提交。', + okText: '确认', + cancelText: '取消', + onOk: function () { + setEnergyMeta(function (m) { var nm = {}; for (var k in m) nm[k] = m[k]; nm.submitBy = '能源采购组-李四'; nm.status = '已提交'; return nm; }); + message.success('能源采购组已提交'); + } + }); + }, [energy]); + var handleEnergyRevoke = useCallback(function () { + Modal.confirm({ + title: '撤回确认', + content: '是否确认撤回?', + okText: '确认', + cancelText: '取消', + onOk: function () { + setEnergyMeta(function (m) { var nm = {}; for (var k in m) nm[k] = m[k]; nm.status = '待提交'; return nm; }); + message.success('能源采购组已撤回,可重新编辑并保存/提交'); + } + }); + }, []); + + // 运维部 + var operationFixedItems = ['清洗费', '未结算保养', '未结算维修', '车损费用', '工具损坏丢失费用', '证件丢失费用', '广告损坏丢失费用', '送车服务费', '接车服务费', '轮胎磨损费用']; + var operationRowsState = useState( + operationFixedItems.map(function (name, i) { + return { key: 'op-' + i, seq: i + 1, feeItem: name, amount: '', worryFreeDiscount: '', remark: '', lastUpdateTime: '', photos: [], attachments: [], fixed: true }; + }) + ); + var operationRows = operationRowsState[0]; + var setOperationRows = operationRowsState[1]; + var operationMetaState = useState({ submitBy: '', status: '待提交' }); + var operationMeta = operationMetaState[0]; + var setOperationMeta = operationMetaState[1]; + var operationCollapsedState = useState(false); + + function recalcOperationTotal(list) { + var sum = 0; + (list || []).forEach(function (r) { sum += (parseFloat(r.amount) || 0); }); + return sum.toFixed(2); + } + + var operationTotalComputed = useMemo(function () { + return recalcOperationTotal(operationRows || []); + }, [operationRows]); + + // 轮胎胎纹明细(原型 mock:10个轮胎 + 1个备胎,共11条) + var tireTreadList = useMemo(function () { + var unit = 25; // 元/mm + var rows = [ + { key: 't1', name: '左前轮', deliveryDepth: '6.8', returnDepth: '6.2' }, + { key: 't2', name: '左后轮(内)', deliveryDepth: '6.7', returnDepth: '6.0' }, + { key: 't3', name: '左后轮(外)', deliveryDepth: '6.6', returnDepth: '6.1' }, + { key: 't4', name: '右前轮', deliveryDepth: '6.9', returnDepth: '6.4' }, + { key: 't5', name: '右后轮(内)', deliveryDepth: '6.5', returnDepth: '6.0' }, + { key: 't6', name: '右后轮(外)', deliveryDepth: '6.8', returnDepth: '6.3' }, + { key: 't7', name: '左中轮', deliveryDepth: '6.7', returnDepth: '6.2' }, + { key: 't8', name: '右中轮', deliveryDepth: '6.6', returnDepth: '6.1' }, + { key: 't9', name: '左后备位轮', deliveryDepth: '6.9', returnDepth: '6.5' }, + { key: 't10', name: '右后备位轮', deliveryDepth: '6.4', returnDepth: '6.0' }, + { key: 'spare', name: '备胎', deliveryDepth: '7.0', returnDepth: '7.0' } + ]; + var deliverySum = 0; + var returnSum = 0; + rows.forEach(function (r) { deliverySum += (parseFloat(r.deliveryDepth) || 0); returnSum += (parseFloat(r.returnDepth) || 0); }); + var shouldCalc = returnSum <= deliverySum; + return rows.map(function (r) { + var d = parseFloat(r.deliveryDepth) || 0; + var rr = parseFloat(r.returnDepth) || 0; + var diff = shouldCalc ? Math.max(0, d - rr) : 0; + var total = shouldCalc ? (diff * unit) : 0; + return { + key: r.key, + name: r.name, + deliveryDepth: d.toFixed(2), + returnDepth: rr.toFixed(2), + diff: diff.toFixed(2), + unitPrice: unit.toFixed(2), + totalAmount: total.toFixed(2) + }; + }); + }, []); + + var tireWearTotal = useMemo(function () { + var sum = 0; + (tireTreadList || []).forEach(function (r) { sum += (parseFloat(r.totalAmount) || 0); }); + return sum.toFixed(2); + }, [tireTreadList]); + + // 运维部自动反写/计算(原型 mock) + var operationAuto = useMemo(function () { + // 证件丢失费用:行驶证200、牌照300、营运证300、加氢证300 + var missingDocs = ['行驶证', '牌照']; // mock:交车有,还车无 + var map = { '行驶证': 200, '牌照': 300, '营运证': 300, '加氢证': 300 }; + var docFee = missingDocs.reduce(function (s, k) { return s + (map[k] || 0); }, 0); + // 送车/接车服务费 mock + var deliveryFee = 0; + var receiveFee = 0; + // 轮胎磨损费用:按胎纹明细汇总 + return { docLossFee: docFee.toFixed(2), deliveryServiceFee: deliveryFee.toFixed(2), receiveServiceFee: receiveFee.toFixed(2), tireWearFee: String(tireWearTotal || '0.00') }; + }, [tireWearTotal]); + + useEffect(function () { + setOperationRows(function (p) { + var next = p.map(function (r) { + var n = {}; for (var k in r) n[k] = r[k]; + if (n.feeItem === '证件丢失费用' && !String(n.amount || '').trim()) n.amount = operationAuto.docLossFee; + if (n.feeItem === '送车服务费') n.amount = operationAuto.deliveryServiceFee; + if (n.feeItem === '接车服务费') n.amount = operationAuto.receiveServiceFee; + if (n.feeItem === '轮胎磨损费用' && !String(n.amount || '').trim()) n.amount = operationAuto.tireWearFee; + return n; + }); + return next; + }); + }, [operationAuto.docLossFee, operationAuto.deliveryServiceFee, operationAuto.receiveServiceFee, operationAuto.tireWearFee]); + + var handleOperationSave = useCallback(function () { + setOperationMeta(function (m) { var nm = {}; for (var k2 in m) nm[k2] = m[k2]; nm.status = '待提交'; return nm; }); + message.success('运维部已保存'); + }, []); + var handleOperationSubmit = useCallback(function () { + Modal.confirm({ + title: '提交确认', + content: '请确认运维部金额填写无误,点击确认完成提交。', + okText: '确认', + cancelText: '取消', + onOk: function () { + setOperationRows(function (p) { + var now = formatDateTimeNow(); + return p.map(function (r) { var n = {}; for (var k in r) n[k] = r[k]; n.lastUpdateTime = now; return n; }); + }); + setOperationMeta(function (m) { var nm = {}; for (var k in m) nm[k] = m[k]; nm.submitBy = '运维部-王五'; nm.status = '已提交'; return nm; }); + message.success('运维部已提交'); + } + }); + }, [operationRows]); + var handleOperationRevoke = useCallback(function () { + Modal.confirm({ + title: '撤回确认', + content: '是否确认撤回?', + okText: '确认', + cancelText: '取消', + onOk: function () { + setOperationMeta(function (m) { var nm = {}; for (var k in m) nm[k] = m[k]; nm.status = '待提交'; return nm; }); + message.success('运维部已撤回,可重新编辑并保存/提交'); + } + }); + }, []); + var addOperationRow = useCallback(function () { + setOperationRows(function (p) { + var next = p.slice(); + next.push({ key: 'op-new-' + Date.now(), seq: next.length + 1, feeItem: '', amount: '', worryFreeDiscount: '', remark: '', lastUpdateTime: '', photos: [], attachments: [], fixed: false }); + return next.map(function (r, idx) { var o = {}; for (var k in r) o[k] = r[k]; o.seq = idx + 1; return o; }); + }); + }, []); + var removeOperationRow = useCallback(function (key) { + setOperationRows(function (p) { + var next = p.filter(function (r) { return r.key !== key; }); + return next.map(function (r, idx) { var o = {}; for (var k in r) o[k] = r[k]; o.seq = idx + 1; return o; }); + }); + }, []); + var updateOperationRow = useCallback(function (key, field, value) { + setOperationRows(function (p) { + var next = p.map(function (r) { + if (r.key !== key) return r; + var n = {}; for (var k in r) n[k] = r[k]; + n[field] = value; + return n; + }); + return next; + }); + }, []); + + // 安全组 + var safetyFixedItems = ['违章处理违约金', '保险上浮', '其他违规费用']; + var safetyFeeRowsState = useState( + safetyFixedItems.map(function (name, i) { + return { key: 'sf-' + i, seq: i + 1, feeItem: name, amount: '', remark: '', lastUpdateTime: '', photos: [], attachments: [], fixed: true }; + }) + ); + var safetyFeeRows = safetyFeeRowsState[0]; + var setSafetyFeeRows = safetyFeeRowsState[1]; + + function recalcSafetyTotal(list) { + var sum = 0; + (list || []).forEach(function (r) { sum += (parseFloat(r.amount) || 0); }); + return sum.toFixed(2); + } + + var safetyTotalComputed = useMemo(function () { + return recalcSafetyTotal(safetyFeeRows || []); + }, [safetyFeeRows]); + + var addSafetyFeeRow = useCallback(function () { + setSafetyFeeRows(function (p) { + var next = p.slice(); + next.push({ key: 'sf-new-' + Date.now(), seq: next.length + 1, feeItem: '', amount: '', remark: '', lastUpdateTime: '', photos: [], attachments: [], fixed: false }); + return next.map(function (r, idx) { var o = {}; for (var k in r) o[k] = r[k]; o.seq = idx + 1; return o; }); + }); + }, []); + var removeSafetyFeeRow = useCallback(function (key) { + setSafetyFeeRows(function (p) { + var next = p.filter(function (r) { return r.key !== key; }); + return next.map(function (r, idx) { var o = {}; for (var k in r) o[k] = r[k]; o.seq = idx + 1; return o; }); + }); + }, []); + var updateSafetyFeeRow = useCallback(function (key, field, value) { + setSafetyFeeRows(function (p) { + return p.map(function (r) { + if (r.key !== key) return r; + var n = {}; for (var k in r) n[k] = r[k]; + n[field] = value; + return n; + }); + }); + }, []); + + var violationList = useMemo(function () { + return [{ + key: 'w1', + code: 'WZ202602010001', + plateNo: '浙F03218F', + violationBehavior: '测试', + violationTime: '2026-02-01', + penaltyAmount: '100.00', + paymentStatus: '未缴费', + score: '6', + handleStatus: '未处理', + violationCustomer: '上海馨想事…', + violationPhoto: '', + remark: '' + }]; + }, []); + var accidentList = useMemo(function () { + return [{ + key: 'a1', + accidentCode: 'SG202508250001', + plateNo: '京A29256F', + accidentTime: '2025-08-25', + accidentPlace: '北京市大…', + accidentType: '撞固定物', + customerName: '北京海龙…', + ourClaimAmount: '', + theirClaimAmount: '', + responsibility: '全责', + accidentStatus: '未结案', + closeTime: '', + otherFee: '', + remark: '' + }]; + }, []); + var safetyCollapsedState = useState(false); + var safetyMetaState = useState({ submitBy: '', status: '待提交' }); + var safetyMeta = safetyMetaState[0]; + var setSafetyMeta = safetyMetaState[1]; + + var handleSafetySave = useCallback(function () { + setSafetyMeta(function (m) { var nm = {}; for (var k in m) nm[k] = m[k]; nm.submitBy = nm.submitBy || '安全组-赵六'; nm.status = '待提交'; return nm; }); + message.success('安全组已保存'); + }, []); + var handleSafetySubmit = useCallback(function () { + Modal.confirm({ + title: '提交确认', + content: '请确认安全组信息无误,点击确认完成提交。', + okText: '确认', + cancelText: '取消', + onOk: function () { + setSafetyFeeRows(function (p) { + var now = formatDateTimeNow(); + return p.map(function (r) { var n = {}; for (var k in r) n[k] = r[k]; n.lastUpdateTime = now; return n; }); + }); + setSafetyMeta(function (m) { var nm = {}; for (var k in m) nm[k] = m[k]; nm.submitBy = nm.submitBy || '安全组-赵六'; nm.status = '已提交'; return nm; }); + message.success('安全组已提交'); + } + }); + }, []); + var handleSafetyRevoke = useCallback(function () { + Modal.confirm({ + title: '撤回确认', + content: '是否确认撤回?', + okText: '确认', + cancelText: '取消', + onOk: function () { + setSafetyMeta(function (m) { var nm = {}; for (var k in m) nm[k] = m[k]; nm.status = '待提交'; return nm; }); + message.success('安全组已撤回,可重新编辑并保存/提交'); + } + }); + }, []); + + var pendingSettleComputed = useMemo(function () { + var bs = parseFloat(businessServiceTotalComputed) || 0; + var rentRefund = parseFloat((billInfo && billInfo.shouldRefundRent) || '') || 0; + var op = parseFloat(operationTotalComputed) || 0; + var en = (parseFloat(energy.hydrogenSupplement) || 0) + (parseFloat(energy.hydrogenFee) || 0) + (parseFloat(energy.electricFee) || 0) - (parseFloat(energy.prepayRefund) || 0); + var sf = parseFloat(safetyTotalComputed) || 0; + return (bs + rentRefund + en + op + sf).toFixed(2); + }, [businessServiceTotalComputed, billInfo.shouldRefundRent, operationTotalComputed, energy.hydrogenSupplement, energy.hydrogenFee, energy.electricFee, energy.prepayRefund, safetyTotalComputed]); + + var refundTotalComputed = useMemo(function () { + var deposit = parseFloat(stats.depositAmount) || 0; + var settle = parseFloat(pendingSettleComputed) || 0; + var diff = deposit - settle; + return diff > 0 ? diff.toFixed(2) : '0.00'; + }, [stats.depositAmount, pendingSettleComputed]); + + var payTotalComputed = useMemo(function () { + var deposit = parseFloat(stats.depositAmount) || 0; + var settle = parseFloat(pendingSettleComputed) || 0; + var diff = deposit - settle; + return diff < 0 ? Math.abs(diff).toFixed(2) : '0.00'; + }, [stats.depositAmount, pendingSettleComputed]); + + function buildBreakdownContent(list) { + var headStyle = { padding: '6px 10px', textAlign: 'left', borderBottom: '1px solid #f0f0f0', background: '#fafafa', fontWeight: 600, fontSize: 12 }; + var tdStyle = { padding: '6px 10px', borderBottom: '1px solid #f0f0f0', fontSize: 12 }; + return React.createElement('div', { style: { padding: 0, minWidth: 320 } }, + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse' } }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: headStyle }, '费用项'), + React.createElement('th', { style: Object.assign({}, headStyle, { textAlign: 'right' }) }, '金额(元)') + ) + ), + React.createElement('tbody', null, + (list || []).map(function (r) { + return React.createElement('tr', { key: r.key }, + React.createElement('td', { style: tdStyle }, r.item), + React.createElement('td', { style: Object.assign({}, tdStyle, { fontWeight: r.strong ? 600 : 400, textAlign: 'right' }) }, r.amount) + ); + }) + ) + ) + ); + } + + var settleBreakdown = useMemo(function () { + return [ + { key: 'bs', item: '业务服务组费用项金额总和', amount: businessServiceTotalComputed || '0.00' }, + { key: 'rent', item: '业务服务组-车辆应退租金', amount: (toFixed2(billInfo && billInfo.shouldRefundRent) || '0.00') }, + { key: 'hDiff', item: '能源采购组-氢量差补缴金额', amount: (toFixed2(energy.hydrogenSupplement) || '0.00') }, + { key: 'hFee', item: '能源采购组-氢费补缴金额', amount: (toFixed2(energy.hydrogenFee) || '0.00') }, + { key: 'eFee', item: '能源采购组-电费补缴金额', amount: (toFixed2(energy.electricFee) || '0.00') }, + { key: 'prepay', item: '能源采购组-预付款退费金额(减)', amount: '-' + (toFixed2(energy.prepayRefund) || '0.00') }, + { key: 'op', item: '运维部费用项金额总额', amount: operationTotalComputed || '0.00' }, + { key: 'sf', item: '安全组费用项金额总和', amount: safetyTotalComputed || '0.00' }, + { key: 'total', item: '待结算总额', amount: pendingSettleComputed || '0.00', strong: true } + ]; + }, [businessServiceTotalComputed, billInfo.shouldRefundRent, energy.hydrogenSupplement, energy.hydrogenFee, energy.electricFee, energy.prepayRefund, operationTotalComputed, safetyTotalComputed, pendingSettleComputed]); + + var refundBreakdown = useMemo(function () { + return [ + { key: 'd', item: '保证金总额', amount: (toFixed2(stats.depositAmount) || '0.00') }, + { key: 's', item: '待结算总额', amount: (pendingSettleComputed || '0.00') }, + { key: 'r', item: '应退还总额', amount: (refundTotalComputed || '0.00'), strong: true } + ]; + }, [stats.depositAmount, pendingSettleComputed, refundTotalComputed]); + +var payBreakdown = useMemo(function () { + return [ + { key: 'd', item: '保证金总额', amount: (toFixed2(stats.depositAmount) || '0.00') }, + { key: 's', item: '待结算总额', amount: (pendingSettleComputed || '0.00') }, + { key: 'p', item: '应补缴总额', amount: (payTotalComputed || '0.00'), strong: true } + ]; + }, [stats.depositAmount, pendingSettleComputed, payTotalComputed]); + +var requirementModalOpenState = useState(false); +var requirementDocContent = useMemo(function () { + return (`还车应结款 +一个「数字化资产ONEOS运管平台」中的「还车应结款」「费用明细」模块 +#面包屑: +1.财务管理-还车应结款-费用明细 +每个模块为一个单独卡片: + +2.还车车辆明细; +2.1.车牌号:显示还车车牌号; +2.2.合同编码:显示还车车辆合同编码; +2.3.项目名称:显示还车车辆对应项目名称; +2.4.客户名称:显示还车车辆对应客户名称; +2.5.交车时间:显示还车车辆完成交车时间; +2.6.还车时间:显示还车车辆完成还车时间; +2.7.易损保:显示还车车辆是否购买易损保,显示为是或否,后方为提示图标,文案为:提供刹车片、灯泡、蓄电池、雨刮等易损件租期内免费更换服务(不含轮胎,只限自行到服务站更换); +2.8.轮胎保:显示还车车辆是否购买轮胎保,显示为是或否,后方为提示图标,文案为:每个租赁年度内提供1次车辆轮胎因自然磨损产生的替换服务; +2.9.养护保:显示还车车辆是否购买养护保,显示为是或否,后方为提示图标,文案为:按厂家保养要求提供定期保养服务; + + +3.还车费用明细: +#上方为还车费用统计数据,包括保证金总额、待结算总额、应退还总额、应补缴总额等相关信息,该部分只有业务管理组能查看; +3.1.保证金总额:显示该车辆保证金金额,格式为xx.xx元; +3.2.待结算总额:显示该车辆待结算金额,格式为xx.xx元,点击以气泡卡片列表显示:费用项、金额。计算方式为:「业务服务部所有费用项-金额总和」+「业务服务部-车辆应退租金」+「能源采购组-氢量差补缴金额」+「能源采购组-氢费补缴金额」+「能源采购组-电费补缴金额」-「能源采购组-预付款退费金额」+「运维部所有费用项-金额总额」+「安全组费用项金额总和」; +3.3.应退还总额:显示该车辆应退还金额,格式为xx.xx元,点击以气泡卡片列表显示:费用项、金额。计算方式为:「保证金金额」-「待结算金额」如果是正数,则显示在应退还总额中; +3.4.应补缴总额:显示该车辆应补缴金额,格式为xx.xx元,点击以气泡卡片列表显示:费用项、金额。计算方式为:「保证金金额」-「待结算金额」如果是负数,则显示在应补缴总额中; + +#下方为业务服务组、能源采购组、运维部、安全组4部分,支持展开/收起功能; +4.业务服务组:仅由业务服务组人员进行填写;标题栏标题为业务服务组,后方为:总金额、提交人、状态(待提交、已提交),该部分只有业务管理组能查看; +4.1.总金额:显示所有费用总金额; +4.2.提交人:显示提交人; +4.3.状态:分为待提交(点击保存按钮)、已提交(点击提交按钮); +4.4.保存:点击保存已填写内容,如果已提交,则隐藏保存按钮; +4.5.提交:点击进行二次确认,提示信息:请确认业务服务组金额填写无误,点击确认完成提交。如果已提交,隐藏提交按钮; +4.6.撤回:已提交后,显示撤回按钮。点击进行二次确认,提示信息:是否确认撤回,点击确认,重新显示保存、提交按钮; + +下方为列表,列表字段为:序号、费用项、金额、备注、照片、附件、操作; +4.7.序号:1、2、3....依次类推; +4.8.费用项:固定显示租金、ETC-客户未缴费用、ETC卡缺损费、ETC设备缺损费,该部分不能删除;可通过点击下方新增一行,添加新的条目(该部分可删除); + 4.8.1.租金:金额输入框支持正数/负数,最多2位小数; + 4.8.2.ETC-客户未缴费用: + 4.8.3.ETC卡缺损费: + 4.8.4.ETC设备缺损费: +4.9.金额:输入框,支持2位小数,后缀为元; +4.10.备注:文本域,支持自定义输入; +4.11.最后更新时间:显示最后更新时间,格式为:YYYY-MM-DD HH:MM; +4.12.照片:照片上传按钮,支持照片上传,支持多个附件; +4.13.附件:附件上传按钮,文案为:上传附件,支持多个附件; +4.14.操作:除固定费用项外,其余操作中为删除; +列表下方为车辆租金:包含本期账单已收租金、车辆实际租金、车辆应退租金; +4.15.本期账单已收租金:输入框(可编辑),反写本期账单该车辆实收款租金金额(后期与用友YS打通后,显示财务到账金额) +4.16.车辆实际租金:输入框(可编辑),自动计算车辆实际租金,按照:(车辆月租金/30)*(账单开始日期-还车日期总天数,取整) +4.17.车辆应退租金:输入框(可编辑),根据:本期账单已收租金-车辆实际租金计算并反写; + +5.能源采购组:仅由能源采购组人员进行填写;标题栏标题为能源采购组,后方为总金额、提交人、状态(待提交、已提交),该部分只有能源采购组能查看; +5.1.总金额:显示所有费用总金额; +5.2.提交人:显示提交人; +5.3.状态:分为待提交(点击保存按钮)、已提交(点击提交按钮); +5.4.保存:点击保存已填写内容,如果已提交,则隐藏保存按钮; +5.5.提交:点击进行二次确认,提示信息:请确认业务服务组金额填写无误,点击确认完成提交。如果已提交,隐藏提交按钮; +5.6.撤回:已提交后,显示撤回按钮。点击进行二次确认,提示信息:是否确认撤回,点击确认,重新显示保存、提交按钮; +下方为填写表单: +5.7.氢量差补缴金额:输入框,支持2位小数,后缀为元,如果交车氢量>还车氢量时,根据“差额*退还车氢气单价”自动计算。 +5.8.交车氢量:格式为:xx.xxMPa,保留2位小数,从该车辆交车在该合同交车时间氢量获取; +5.9.还车氢量:格式为:xx.xxMPa,保留2位小数,从该车辆交车在该合同还车时间氢量获取; +5.10.退还车氢气单价:xx.xx元,从车辆租赁合同中获取; +5.11.能源费补缴金额:右侧为2个输入框,分别为氢费补缴金额、电费补缴金额,支持2位小数,后缀为元; +5.12.预付款退费金额:输入框,支持2位小数,后缀为元;输入框下方显示项目预充值余额,如果当前租赁合同已是最后一辆车还车,则输入框右侧加红提示:当前车辆为该合同最后一辆车; + +6.运维部:仅由运维部人员进行填写;标题栏标题为运维部,后方为总金额、提交人、状态(待提交、已提交),该部分只有运维部能查看; +6.1.总金额:显示运维部所有费用项费用金额总额; +6.2.提交人:显示提交人; +6.3.状态:分为待提交(点击保存按钮)、已提交(点击提交按钮); +6.4.保存:点击保存已填写内容,如果已提交,则隐藏保存按钮; +6.5.提交:点击进行二次确认,提示信息:请确认业务服务组金额填写无误,点击确认完成提交。如果已提交,隐藏提交按钮; +6.6.撤回:已提交后,显示撤回按钮。点击进行二次确认,提示信息:是否确认撤回,点击确认,重新显示保存、提交按钮; +下方为列表: +6.7.序号:1、2、3....依次类推; +6.8.费用项:固定显示项为: + 6.8.1.清洗费:输入框手动填写; + 6.8.2.未结算保养:输入框手动填写; + 6.8.3.未结算维修:输入框手动填写; + 6.8.4.车损费用:输入框手动填写; + 6.8.5.工具损坏丢失费用:输入框手动填写; + 6.8.6.证件丢失费用:输入框,自动计算金额,交车时做备车检查时有,还车时无得证件自动计算费用,计算标准为:行驶证:200元,牌照:300元,营运证:300元,加氢证:300元;如所有证件无丢失,或交车时就为无则不进行计算; + 6.8.7.广告损坏丢失费用:输入框手动填写; + 6.8.8.送车服务费:输入框(禁用),反写交车时送车服务费,如无则显示为0; + 6.8.9.接车服务费:输入框(禁用),反写还车时接车服务费,如无则显示为0; + 6.8.10.轮胎磨损费用:输入框(可编辑),轮胎磨损费用(元/mm)*(交车时所有轮胎及备胎胎纹深度-还车时所有轮胎及备胎胎纹深度)自动计算,此外如还车胎纹总额大于交车胎纹总额则不进行计算,此外轮胎磨损费用标题后增加说明图标,悬浮图标显示气泡卡片:卡片内为列表,标题为:轮胎名称、交车胎纹深度、还车胎纹深度、胎纹差、单价、总金额,每一行为一个单独的轮胎(包括备胎); + 该部分不能删除;可通过点击下方新增一行,添加新的条目(该部分可删除); + +6.9.金额:输入框,支持2位小数,后缀为元; +6.10.无忧包减免:输入框,支持2位小数,后缀为元。仅有三个费用项后有输入框,用户未购买易损保时,未结算维修费用后该输入框禁用,用户未购买轮胎保时间,轮胎磨损费用后该输入框为禁用,用户未购买养护保时,未结算保养后该输入框为禁用; +6.11.备注:文本域,支持自定义输入; +6.12.照片:照片上传按钮,支持照片上传; +6.13.附件:附件上传按钮,文案为:上传附件; +6.14.操作:除固定费用项外,其余操作中为删除; + +7.安全组:仅由安全组人员进行确认提交;标题栏标题为安全组,后方为总金额、提交人、状态(待提交、已提交),该部分只有安全组能查看; +7.1.总金额:显示所有费用项金额总和; +7.2.提交人:显示提交人; +7.3.状态:分为待提交(点击保存按钮)、已提交(点击提交按钮); +7.4.保存:点击保存已填写内容,如果已提交,则隐藏保存按钮; +7.5.提交:点击进行二次确认,提示信息:请确认安全组信息无误,点击确认完成提交。如果已提交,隐藏提交按钮; +7.6.撤回:已提交后,显示撤回按钮。点击进行二次确认,提示信息:是否确认撤回,点击确认,重新显示保存、提交按钮; +7.7.费用项列表(样式同业务服务组):固定显示违章处理违约金、保险上浮、其他违规费用,不可删除;可新增自定义费用项;字段含序号、费用项、金额、备注、最后更新时间、照片、附件、操作; +7.8.违章清单:违章编码、车牌号、违法行为、违法时间、罚款金额、缴费状态、计分值、是否处理、违章客户、违章照片、备注; +7.9.事故清单:列表显示:事故编码、车牌号、事故时间、事故地点、事故类型、客户名称、我方定损金额、对方定损金额、责任划分、事故状态、结案时间、其他费用、备注; + +8.底部为提交、取消按钮; + 8.1.提交审核:还车应结款生成后有15天时间倒计时,倒计时结束并且业务服务组、能源采购组、运维部、安全部均提交后才启用,平时禁用,倒计时显示在按钮上,格式为:x天x小时后可提交审核; + 8.2.取消:点击返回还车应结款列表页; +`); +}, []); + + // 车辆明细表列 + var vehicleColumns = useMemo(function () { + return [ + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 110 }, + { title: '合同编码', dataIndex: 'contractCode', key: 'contractCode', width: 160, ellipsis: true }, + { title: '项目名称', dataIndex: 'projectName', key: 'projectName', width: 140, ellipsis: true }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 140, ellipsis: true }, + { title: '交车时间', dataIndex: 'deliveryTime', key: 'deliveryTime', width: 160, render: function (v) { return fmtYMDHM(v); } }, + { title: '还车时间', dataIndex: 'returnTime', key: 'returnTime', width: 160, render: function (v) { return fmtYMDHM(v); } }, + { + title: '易损保', dataIndex: 'fragileInsurance', key: 'fragileInsurance', width: 110, + render: function (v) { + return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 6 } }, + (v || '-'), + React.createElement(Tooltip, { title: '提供刹车片、灯泡、蓄电池、雨刮等易损件租期内免费更换服务(不含轮胎,只限自行到服务站更换)' }, + React.createElement('span', { + style: { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + width: 16, + height: 16, + borderRadius: 999, + border: '1px solid #d9d9d9', + color: '#999', + fontSize: 12, + lineHeight: '16px', + cursor: 'help', + userSelect: 'none' + } + }, 'i') + ) + ); + } + }, + { + title: '轮胎保', dataIndex: 'tireInsurance', key: 'tireInsurance', width: 110, + render: function (v) { + return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 6 } }, + (v || '-'), + React.createElement(Tooltip, { title: '每个租赁年度内提供1次车辆轮胎因自然磨损产生的替换服务' }, + React.createElement('span', { + style: { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + width: 16, + height: 16, + borderRadius: 999, + border: '1px solid #d9d9d9', + color: '#999', + fontSize: 12, + lineHeight: '16px', + cursor: 'help', + userSelect: 'none' + } + }, 'i') + ) + ); + } + }, + { + title: '养护保', dataIndex: 'maintenanceInsurance', key: 'maintenanceInsurance', width: 110, + render: function (v) { + return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 6 } }, + (v || '-'), + React.createElement(Tooltip, { title: '按厂家保养要求提供定期保养服务' }, + React.createElement('span', { + style: { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + width: 16, + height: 16, + borderRadius: 999, + border: '1px solid #d9d9d9', + color: '#999', + fontSize: 12, + lineHeight: '16px', + cursor: 'help', + userSelect: 'none' + } + }, 'i') + ) + ); + } + } + ]; + }, []); + + var latestRefs = useRef({}); + latestRefs.current = { + businessServiceMeta: businessServiceMeta, + operationMeta: operationMeta, + insuranceFlags: insuranceFlags, + tireTreadList: tireTreadList, + updateBusinessServiceRow: updateBusinessServiceRow, + removeBusinessServiceRow: removeBusinessServiceRow, + safetyMeta: safetyMeta, + updateSafetyFeeRow: updateSafetyFeeRow, + removeSafetyFeeRow: removeSafetyFeeRow, + updateOperationRow: updateOperationRow, + removeOperationRow: removeOperationRow + }; + + var businessServiceColumns = useMemo(function () { + return [ + { title: '序号', dataIndex: 'seq', key: 'seq', width: 60 }, + { + title: '费用项', dataIndex: 'feeItem', key: 'feeItem', width: 200, + render: function (v, r) { + var ref = latestRefs.current || {}; + var meta = ref.businessServiceMeta || {}; + var readOnly = meta.status === '已提交'; + return r.fixed ? (v || '-') : React.createElement(Input, { value: v, onChange: function (e) { ref.updateBusinessServiceRow(r.key, 'feeItem', e.target.value); }, placeholder: '费用项', disabled: readOnly }); + } + }, + { + title: RequiredLabel('金额'), dataIndex: 'amount', key: 'amount', width: 140, + render: function (v, r) { + var ref = latestRefs.current || {}; + var meta = ref.businessServiceMeta || {}; + var readOnly = meta.status === '已提交'; + var isRent = r.feeItem === '租金'; + return React.createElement(Input, { + value: v, + onChange: function (e) { + var nv = isRent ? filterSignedMoneyInput(e.target.value) : e.target.value; + ref.updateBusinessServiceRow(r.key, 'amount', nv); + }, + onBlur: isRent ? function (e) { + var formatted = formatSignedMoney2(e.target.value); + if (formatted !== e.target.value) ref.updateBusinessServiceRow(r.key, 'amount', formatted); + } : undefined, + placeholder: '0.00', + addonAfter: '元', + disabled: readOnly + }); + } + }, + { + title: '备注', dataIndex: 'remark', key: 'remark', + render: function (v, r) { + var ref = latestRefs.current || {}; + var meta = ref.businessServiceMeta || {}; + var readOnly = meta.status === '已提交'; + return React.createElement(TextArea, { value: v, onChange: function (e) { ref.updateBusinessServiceRow(r.key, 'remark', e.target.value); }, placeholder: '备注', rows: 1, disabled: readOnly }); + } + }, + { title: '最后更新时间', dataIndex: 'lastUpdateTime', key: 'lastUpdateTime', width: 150, render: function (v) { return React.createElement('span', { style: { fontSize: 12, color: '#666' } }, v || '-'); } }, + { title: '照片', key: 'photo', width: 100, render: function () { var ref = latestRefs.current || {}; var meta = ref.businessServiceMeta || {}; var readOnly = meta.status === '已提交'; return React.createElement(Button, { size: 'small', disabled: readOnly, onClick: function () { message.info('照片上传(原型)'); } }, '上传'); } }, + { title: '附件', key: 'attachment', width: 110, render: function () { var ref = latestRefs.current || {}; var meta = ref.businessServiceMeta || {}; var readOnly = meta.status === '已提交'; return React.createElement(Button, { size: 'small', disabled: readOnly, onClick: function () { message.info('上传附件(原型)'); } }, '上传附件'); } }, + { + title: '操作', key: 'action', width: 80, + render: function (_, r) { + var ref = latestRefs.current || {}; + var meta = ref.businessServiceMeta || {}; + if (r.fixed || meta.status === '已提交') return null; + return React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function () { ref.removeBusinessServiceRow(r.key); } }, '删除'); + } + } + ]; + }, []); + + var safetyFeeColumns = useMemo(function () { + return [ + { title: '序号', dataIndex: 'seq', key: 'seq', width: 60 }, + { + title: '费用项', dataIndex: 'feeItem', key: 'feeItem', width: 200, + render: function (v, r) { + var ref = latestRefs.current || {}; + var meta = ref.safetyMeta || {}; + var readOnly = meta.status === '已提交'; + return r.fixed ? (v || '-') : React.createElement(Input, { value: v, onChange: function (e) { ref.updateSafetyFeeRow(r.key, 'feeItem', e.target.value); }, placeholder: '费用项', disabled: readOnly }); + } + }, + { + title: RequiredLabel('金额'), dataIndex: 'amount', key: 'amount', width: 140, + render: function (v, r) { + var ref = latestRefs.current || {}; + var meta = ref.safetyMeta || {}; + var readOnly = meta.status === '已提交'; + return React.createElement(Input, { value: v, onChange: function (e) { ref.updateSafetyFeeRow(r.key, 'amount', e.target.value); }, placeholder: '0.00', addonAfter: '元', disabled: readOnly }); + } + }, + { + title: '备注', dataIndex: 'remark', key: 'remark', + render: function (v, r) { + var ref = latestRefs.current || {}; + var meta = ref.safetyMeta || {}; + var readOnly = meta.status === '已提交'; + return React.createElement(TextArea, { value: v, onChange: function (e) { ref.updateSafetyFeeRow(r.key, 'remark', e.target.value); }, placeholder: '备注', rows: 1, disabled: readOnly }); + } + }, + { title: '最后更新时间', dataIndex: 'lastUpdateTime', key: 'lastUpdateTime', width: 150, render: function (v) { return React.createElement('span', { style: { fontSize: 12, color: '#666' } }, v || '-'); } }, + { title: '照片', key: 'photo', width: 100, render: function () { var ref = latestRefs.current || {}; var meta = ref.safetyMeta || {}; var readOnly = meta.status === '已提交'; return React.createElement(Button, { size: 'small', disabled: readOnly, onClick: function () { message.info('照片上传(原型)'); } }, '上传'); } }, + { title: '附件', key: 'attachment', width: 110, render: function () { var ref = latestRefs.current || {}; var meta = ref.safetyMeta || {}; var readOnly = meta.status === '已提交'; return React.createElement(Button, { size: 'small', disabled: readOnly, onClick: function () { message.info('上传附件(原型)'); } }, '上传附件'); } }, + { + title: '操作', key: 'action', width: 80, + render: function (_, r) { + var ref = latestRefs.current || {}; + var meta = ref.safetyMeta || {}; + if (r.fixed || meta.status === '已提交') return null; + return React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function () { ref.removeSafetyFeeRow(r.key); } }, '删除'); + } + } + ]; + }, []); + + var operationColumns = useMemo(function () { + return [ + { title: '序号', dataIndex: 'seq', key: 'seq', width: 60 }, + { + title: '费用项', dataIndex: 'feeItem', key: 'feeItem', width: 200, + render: function (v, r) { + var ref = latestRefs.current || {}; + var meta = ref.operationMeta || {}; + var readOnly = meta.status === '已提交'; + if (r.fixed) { + if (v === '轮胎磨损费用') { + var popContent = React.createElement('div', { style: { width: 760, maxWidth: '80vw' } }, + React.createElement('div', { style: { fontWeight: 600, marginBottom: 8 } }, '轮胎磨损费用明细'), + React.createElement(Table, { + rowKey: 'key', + size: 'small', + bordered: true, + pagination: false, + dataSource: ref.tireTreadList, + columns: [ + { title: '轮胎名称', dataIndex: 'name', key: 'name', width: 120, ellipsis: true }, + { title: '交车胎纹深度', dataIndex: 'deliveryDepth', key: 'deliveryDepth', width: 110, render: function (vv) { return (vv || '0.00') + 'mm'; } }, + { title: '还车胎纹深度', dataIndex: 'returnDepth', key: 'returnDepth', width: 110, render: function (vv) { return (vv || '0.00') + 'mm'; } }, + { title: '胎纹差', dataIndex: 'diff', key: 'diff', width: 80, render: function (vv) { return (vv || '0.00') + 'mm'; } }, + { title: '单价', dataIndex: 'unitPrice', key: 'unitPrice', width: 90, render: function (vv) { return (vv || '0.00') + '元/mm'; } }, + { title: '总金额', dataIndex: 'totalAmount', key: 'totalAmount', width: 90, render: function (vv) { return (vv || '0.00') + '元'; } } + ] + }) + ); + return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 6 } }, + (v || '-'), + React.createElement(Popover, { content: popContent, trigger: 'hover', placement: 'topLeft' }, + React.createElement('span', { + style: { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + width: 16, + height: 16, + borderRadius: 999, + border: '1px solid #d9d9d9', + color: '#999', + fontSize: 12, + lineHeight: '16px', + cursor: 'help', + userSelect: 'none' + } + }, 'i') + ) + ); + } + return (v || '-'); + } + return React.createElement(Input, { value: v, onChange: function (e) { ref.updateOperationRow(r.key, 'feeItem', e.target.value); }, placeholder: '费用项', disabled: readOnly }); + } + }, + { + title: RequiredLabel('金额'), dataIndex: 'amount', key: 'amount', width: 140, + render: function (v, r) { + var ref = latestRefs.current || {}; + var meta = ref.operationMeta || {}; + var readOnly = meta.status === '已提交'; + var fee = r && r.feeItem; + var disabled = readOnly; + if (fee === '送车服务费' || fee === '接车服务费') disabled = true; + return React.createElement(Input, { value: v, onChange: function (e) { ref.updateOperationRow(r.key, 'amount', e.target.value); }, placeholder: '0.00', addonAfter: '元', disabled: disabled }); + } + }, + { + title: React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 6 } }, + '无忧包减免', + React.createElement(Tooltip, { title: '无忧包减免不会列入运维成本,而是计入业务成本' }, + React.createElement('span', { + style: { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + width: 16, + height: 16, + borderRadius: 999, + border: '1px solid #d9d9d9', + color: '#999', + fontSize: 12, + lineHeight: '16px', + cursor: 'help', + userSelect: 'none' + } + }, 'i') + ) + ), + dataIndex: 'worryFreeDiscount', + key: 'worryFreeDiscount', + width: 140, + render: function (v, r) { + var ref = latestRefs.current || {}; + var meta = ref.operationMeta || {}; + var fee = r && r.feeItem; + var enableFor = (fee === '未结算维修' || fee === '轮胎磨损费用' || fee === '未结算保养'); + if (!enableFor) return React.createElement('span', { style: { color: '#999' } }, '-'); + var readOnly = meta.status === '已提交'; + var disabled = false; + var ins = ref.insuranceFlags || {}; + if (fee === '未结算维修' && !ins.hasFragileInsurance) disabled = true; + if (fee === '轮胎磨损费用' && !ins.hasTireInsurance) disabled = true; + if (fee === '未结算保养' && !ins.hasMaintenanceInsurance) disabled = true; + return React.createElement(Input, { value: v, onChange: function (e) { ref.updateOperationRow(r.key, 'worryFreeDiscount', e.target.value); }, placeholder: '0.00', addonAfter: '元', disabled: disabled || readOnly }); + } + }, + { + title: '备注', dataIndex: 'remark', key: 'remark', + render: function (v, r) { + var ref = latestRefs.current || {}; + var meta = ref.operationMeta || {}; + var readOnly = meta.status === '已提交'; + return React.createElement(TextArea, { value: v, onChange: function (e) { ref.updateOperationRow(r.key, 'remark', e.target.value); }, placeholder: '备注', rows: 1, disabled: readOnly }); + } + }, + { title: '最后更新时间', dataIndex: 'lastUpdateTime', key: 'lastUpdateTime', width: 150, render: function (v) { return React.createElement('span', { style: { fontSize: 12, color: '#666' } }, v || '-'); } }, + { title: '照片', key: 'photo', width: 100, render: function () { return React.createElement(Button, { size: 'small', onClick: function () { message.info('照片上传(原型)'); } }, '上传'); } }, + { title: '附件', key: 'attachment', width: 110, render: function () { return React.createElement(Button, { size: 'small', onClick: function () { message.info('上传附件(原型)'); } }, '上传附件'); } }, + { + title: '操作', key: 'action', width: 80, + render: function (_, r) { + var ref = latestRefs.current || {}; + var meta = ref.operationMeta || {}; + if (r.fixed || meta.status === '已提交') return null; + return React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function () { ref.removeOperationRow(r.key); } }, '删除'); + } + } + ]; + }, []); + + // 提交审核倒计时(原型:生成后 15 天) + var generatedAt = useMemo(function () { return new Date(Date.now() - 3 * 24 * 60 * 60 * 1000); }, []); + var nowTickState = useState(Date.now()); + useEffect(function () { + var t = setInterval(function () { nowTickState[1](Date.now()); }, 60 * 1000); + return function () { clearInterval(t); }; + }, []); + + function remainingText() { + var end = generatedAt.getTime() + 15 * 24 * 60 * 60 * 1000; + var left = end - nowTickState[0]; + if (left <= 0) return '0天0小时后可提交审核'; + var d = Math.floor(left / (24 * 60 * 60 * 1000)); + var h = Math.floor((left % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000)); + return d + '天' + h + '小时后可提交审核'; + } + + function canSubmitReview() { + var end = generatedAt.getTime() + 15 * 24 * 60 * 60 * 1000; + var timeOk = (nowTickState[0] >= end); + var groupOk = businessServiceMeta.status === '已提交' && energyMeta.status === '已提交' && operationMeta.status === '已提交' && safetyMeta.status === '已提交'; + return timeOk && groupOk; + } + + function handleSubmit() { + function isValidMoney(s) { + if (s === null || s === undefined) return false; + var v = String(s).trim(); + if (!v) return false; + return !isNaN(parseFloat(v)); + } + + // 业务服务组:每行“金额”必填;租金支持正负两位小数 + for (var i = 0; i < (businessServiceRows || []).length; i++) { + var r1 = businessServiceRows[i]; + var amountOk = r1.feeItem === '租金' ? isValidSignedMoney2(r1.amount) : isValidMoney(r1.amount); + if (!amountOk) { + message.error(r1.feeItem === '租金' + ? '请填写业务服务组租金金额(支持正负,最多2位小数)' + : '请填写业务服务组第' + (r1.seq || (i + 1)) + '行金额'); + return; + } + } + if (!isValidMoney(billInfo.actualRent)) { message.error('请填写车辆实际租金'); return; } + if (!isValidMoney(billInfo.shouldRefundRent)) { message.error('请填写车辆应退租金'); return; } + + // 能源采购组:氢量差/氢费/电费/预付款退费 必填 + if (!isValidMoney(energy.hydrogenSupplement)) { message.error('请填写氢量差补缴金额'); return; } + if (!isValidMoney(energy.hydrogenFee)) { message.error('请填写氢费补缴金额'); return; } + if (!isValidMoney(energy.electricFee)) { message.error('请填写电费补缴金额'); return; } + if (!isValidMoney(energy.prepayRefund)) { message.error('请填写预付款退费金额'); return; } + + // 运维部:每行“金额”必填;无忧包减免仅对 3 个费用项必填(即显示输入框的行) + for (var j = 0; j < (operationRows || []).length; j++) { + var r2 = operationRows[j]; + if (!isValidMoney(r2.amount)) { + message.error('请填写运维部第' + (r2.seq || (j + 1)) + '行金额'); + return; + } + var fee = r2 && r2.feeItem; + var hasDiscountInput = (fee === '未结算维修' || fee === '轮胎磨损费用' || fee === '未结算保养'); + if (hasDiscountInput && !isValidMoney(r2.worryFreeDiscount)) { + message.error('请填写运维部“' + fee + '”的无忧包减免金额'); + return; + } + } + + for (var k = 0; k < (safetyFeeRows || []).length; k++) { + var r3 = safetyFeeRows[k]; + if (!isValidMoney(r3.amount)) { + message.error('请填写安全组第' + (r3.seq || (k + 1)) + '行金额'); + return; + } + } + + if (!canSubmitReview()) { + message.error('未满足提交审核条件(倒计时结束且四组均已提交)'); + return; + } + message.success('提交审核成功(原型)'); + } + function handleCancel() { + message.info('返回还车应结款列表页(原型)'); + } + +return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + React.createElement(Breadcrumb, { items: [{ title: '财务管理' }, { title: '还车应结款' }] }), + React.createElement(Button, { type: 'link', onClick: function () { requirementModalOpenState[1](true); } }, '查看需求说明') + ), + + React.createElement(Card, { title: '还车车辆明细', style: cardStyle }, + React.createElement(Table, { rowKey: 'key', columns: vehicleColumns, dataSource: vehicleDetail, pagination: false, bordered: true, size: 'middle', scroll: { x: 980 } }) + ), + + React.createElement(Card, { title: '还车费用明细', style: cardStyle }, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 16, marginBottom: 16 } }, + React.createElement('div', { style: { background: '#fff', border: '1px solid #f0f0f0', borderRadius: 8, padding: '18px 16px', boxShadow: '0 1px 2px rgba(0,0,0,0.04)' } }, + React.createElement('div', { style: { fontSize: 13, color: '#666', marginBottom: 12 } }, '保证金总额'), + React.createElement('div', { style: { fontSize: 22, fontWeight: 600, color: '#333' } }, (stats.depositAmount || '0.00'), React.createElement('span', { style: { fontSize: 14, fontWeight: 500, marginLeft: 4 } }, '元')) + ), + React.createElement('div', { style: { background: '#fff', border: '1px solid #f0f0f0', borderRadius: 8, padding: '18px 16px', boxShadow: '0 1px 2px rgba(0,0,0,0.04)' } }, + React.createElement('div', { style: { fontSize: 13, color: '#666', marginBottom: 12 } }, '待结算总额'), + React.createElement(Popover, { trigger: 'click', placement: 'bottomLeft', content: buildBreakdownContent(settleBreakdown) }, + React.createElement('div', { style: { fontSize: 22, fontWeight: 600, color: '#fa8c16', cursor: 'pointer' } }, + (pendingSettleComputed || '0.00'), + React.createElement('span', { style: { fontSize: 14, fontWeight: 500, marginLeft: 4, color: '#333' } }, '元') + ) + ) + ), + React.createElement('div', { style: { background: '#fff', border: '1px solid #f0f0f0', borderRadius: 8, padding: '18px 16px', boxShadow: '0 1px 2px rgba(0,0,0,0.04)' } }, + React.createElement('div', { style: { fontSize: 13, color: '#666', marginBottom: 12 } }, '应退还总额'), + React.createElement(Popover, { trigger: 'click', placement: 'bottomLeft', content: buildBreakdownContent(refundBreakdown) }, + React.createElement('div', { style: { fontSize: 22, fontWeight: 600, color: '#52c41a', cursor: 'pointer' } }, + (refundTotalComputed || '0.00'), + React.createElement('span', { style: { fontSize: 14, fontWeight: 500, marginLeft: 4, color: '#333' } }, '元') + ) + ) + ), + React.createElement('div', { style: { background: '#fff', border: '1px solid #f0f0f0', borderRadius: 8, padding: '18px 16px', boxShadow: '0 1px 2px rgba(0,0,0,0.04)' } }, + React.createElement('div', { style: { fontSize: 13, color: '#666', marginBottom: 12 } }, '应补缴总额'), + React.createElement(Popover, { trigger: 'click', placement: 'bottomLeft', content: buildBreakdownContent(payBreakdown) }, + React.createElement('div', { style: { fontSize: 22, fontWeight: 600, color: '#f5222d', cursor: 'pointer' } }, + (payTotalComputed || '0.00'), + React.createElement('span', { style: { fontSize: 14, fontWeight: 500, marginLeft: 4, color: '#333' } }, '元') + ) + ) + ) + ), + + React.createElement(CollapseSection, { + collapsed: businessServiceCollapsedState[0], + setCollapsed: businessServiceCollapsedState[1], + title: '业务服务组', + extra: React.createElement(React.Fragment, null, + React.createElement('span', null, '总金额:', businessServiceTotalComputed, ' 元'), + React.createElement('span', null, '提交人:', businessServiceMeta.submitBy || '-'), + React.createElement('span', null, '状态:', businessServiceMeta.status) + ), + headerActions: businessServiceMeta.status === '已提交' + ? React.createElement(Button, { size: 'small', onClick: handleBusinessServiceRevoke }, '撤回') + : React.createElement(React.Fragment, null, + React.createElement(Button, { size: 'small', onClick: handleBusinessServiceSave }, '保存'), + React.createElement(Button, { size: 'small', type: 'primary', onClick: handleBusinessServiceSubmit }, '提交') + ) + }, + React.createElement(Table, { rowKey: 'key', columns: businessServiceColumns, dataSource: businessServiceRows, pagination: false, bordered: true, size: 'small' }), + React.createElement('div', { style: { marginTop: 12, paddingTop: 12, borderTop: '1px dashed #f0f0f0' } }, + React.createElement('div', { style: { fontWeight: 600, marginBottom: 10 } }, '车辆租金'), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 12 } }, + React.createElement('div', null, + React.createElement('div', { style: { fontSize: 12, color: '#666', marginBottom: 6 } }, '本期账单已收租金'), + React.createElement(Input, { + value: billInfo.receivedRent, + onChange: function (e) { var v = e.target.value; setBillInfo(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.receivedRent = v; return n; }); }, + placeholder: '0.00', + addonAfter: '元', + disabled: businessServiceMeta.status === '已提交' + }) + ), + React.createElement('div', null, + React.createElement('div', { style: { fontSize: 12, color: '#666', marginBottom: 6 } }, RequiredLabel('车辆实际租金')), + React.createElement(Input, { + value: billInfo.actualRent, + onChange: function (e) { var v = e.target.value; setBillInfo(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.actualRent = v; n.actualRentManual = true; return n; }); }, + placeholder: '0.00', + addonAfter: '元', + disabled: businessServiceMeta.status === '已提交' + }) + ), + React.createElement('div', null, + React.createElement('div', { style: { fontSize: 12, color: '#666', marginBottom: 6 } }, RequiredLabel('车辆应退租金')), + React.createElement(Input, { + value: billInfo.shouldRefundRent, + onChange: function (e) { var v = e.target.value; setBillInfo(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.shouldRefundRent = v; n.shouldRefundManual = true; return n; }); }, + placeholder: '0.00', + addonAfter: '元', + disabled: businessServiceMeta.status === '已提交' + }) + ) + ) + ), + businessServiceMeta.status === '待提交' + ? React.createElement('div', { style: { marginTop: 8, width: '100%' } }, + React.createElement(Button, { type: 'dashed', size: 'small', onClick: addBusinessServiceRow, block: true, style: { width: '100%' } }, '新增一行') + ) + : null + ), + + React.createElement(CollapseSection, { + collapsed: energyCollapsedState[0], + setCollapsed: energyCollapsedState[1], + title: '能源采购组', + extra: React.createElement(React.Fragment, null, + React.createElement('span', null, '总金额:', energyTotalComputed, ' 元'), + React.createElement('span', null, '提交人:', energyMeta.submitBy || '-'), + React.createElement('span', null, '状态:', energyMeta.status) + ), + headerActions: energyMeta.status === '已提交' + ? React.createElement(Button, { size: 'small', onClick: handleEnergyRevoke }, '撤回') + : React.createElement(React.Fragment, null, + React.createElement(Button, { size: 'small', onClick: handleEnergySave }, '保存'), + React.createElement(Button, { size: 'small', type: 'primary', onClick: handleEnergySubmit }, '提交') + ) + }, + React.createElement('div', null, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 12, marginBottom: 16 } }, + React.createElement('div', null, + React.createElement('div', { style: { marginBottom: 8, fontWeight: 500 } }, RequiredLabel('氢量差补缴金额')), + React.createElement(Input, { + value: energy.hydrogenSupplement, + onChange: function (e) { setEnergy(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.hydrogenSupplement = e.target.value; return n; }); }, + placeholder: '0.00', + addonAfter: '元', + style: { width: '100%' }, + readOnly: (parseFloat(energy.deliveryHydrogen) || 0) > (parseFloat(energy.returnHydrogen) || 0), + disabled: energyMeta.status === '已提交' + }) + ), + React.createElement('div', null, + React.createElement('div', { style: { marginBottom: 8, fontWeight: 500 } }, '交车氢量'), + React.createElement(Input, { value: (toFixed2(energy.deliveryHydrogen) || '0.00'), addonAfter: 'MPa', disabled: true }) + ), + React.createElement('div', null, + React.createElement('div', { style: { marginBottom: 8, fontWeight: 500 } }, '还车氢量'), + React.createElement(Input, { value: (toFixed2(energy.returnHydrogen) || '0.00'), addonAfter: 'MPa', disabled: true }) + ), + React.createElement('div', null, + React.createElement('div', { style: { marginBottom: 8, fontWeight: 500 } }, '退还车氢气单价'), + React.createElement(Input, { value: (toFixed2(energy.hydrogenUnitPrice) || '0.00'), addonAfter: '元', disabled: true }) + ) + ), + + React.createElement('div', null, + React.createElement('div', { style: { marginBottom: 8, fontWeight: 500 } }, RequiredLabel('能源费补缴金额')), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 12, marginBottom: 12 } }, + React.createElement('div', null, React.createElement(Input, { value: energy.hydrogenFee, onChange: function (e) { setEnergy(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.hydrogenFee = e.target.value; return n; }); }, placeholder: '氢费补缴金额', addonAfter: '元', style: { width: '100%' }, disabled: energyMeta.status === '已提交' })), + React.createElement('div', null, React.createElement(Input, { value: energy.electricFee, onChange: function (e) { setEnergy(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.electricFee = e.target.value; return n; }); }, placeholder: '电费补缴金额', addonAfter: '元', style: { width: '100%' }, disabled: energyMeta.status === '已提交' })), + React.createElement('div', null), + React.createElement('div', null) + ) + ), + React.createElement('div', { style: { gridColumn: '1 / -1', borderTop: '1px solid #f0f0f0', paddingTop: 12, marginTop: 4 } }, + React.createElement('div', { style: { marginBottom: 8, fontWeight: 500 } }, RequiredLabel('预付款退费金额')), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 12 } }, + React.createElement('div', null, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 12 } }, + React.createElement('div', { style: { flex: 1, minWidth: 0 } }, + React.createElement(Input, { value: energy.prepayRefund, onChange: function (e) { setEnergy(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.prepayRefund = e.target.value; return n; }); }, placeholder: '0.00', addonAfter: '元', style: { width: '100%' }, disabled: energyMeta.status === '已提交' }) + ), + isLastVehicleInContract ? React.createElement('span', { style: { color: '#f5222d', fontSize: 12, fontWeight: 600, whiteSpace: 'nowrap' } }, '当前车辆为该合同最后一辆车') : null + ) + ), + React.createElement('div', null), + React.createElement('div', null), + React.createElement('div', null) + ), + React.createElement('div', { style: { marginTop: 8, fontSize: 12, color: '#666' } }, '项目预充值余额:', energy.userBalance, ' 元') + ) + ), + ), + + React.createElement(CollapseSection, { + collapsed: operationCollapsedState[0], + setCollapsed: operationCollapsedState[1], + title: '运维部', + extra: React.createElement(React.Fragment, null, + React.createElement('span', null, '总金额:', operationTotalComputed, ' 元'), + React.createElement('span', null, '提交人:', operationMeta.submitBy || '-'), + React.createElement('span', null, '状态:', operationMeta.status) + ), + headerActions: operationMeta.status === '已提交' + ? React.createElement(Button, { size: 'small', onClick: handleOperationRevoke }, '撤回') + : React.createElement(React.Fragment, null, + React.createElement(Button, { size: 'small', onClick: handleOperationSave }, '保存'), + React.createElement(Button, { size: 'small', type: 'primary', onClick: handleOperationSubmit }, '提交') + ) + }, + React.createElement(Table, { rowKey: 'key', columns: operationColumns, dataSource: operationRows, pagination: false, bordered: true, size: 'small' }), + operationMeta.status === '待提交' + ? React.createElement('div', { style: { marginTop: 8, width: '100%' } }, + React.createElement(Button, { type: 'dashed', size: 'small', onClick: addOperationRow, block: true, style: { width: '100%' } }, '新增一行') + ) + : null + ), + + React.createElement(CollapseSection, { + collapsed: safetyCollapsedState[0], + setCollapsed: safetyCollapsedState[1], + title: '安全组', + extra: React.createElement(React.Fragment, null, + React.createElement('span', null, '总金额:', safetyTotalComputed, ' 元'), + React.createElement('span', null, '提交人:', safetyMeta.submitBy || '-'), + React.createElement('span', null, '状态:', safetyMeta.status) + ), + headerActions: safetyMeta.status === '已提交' + ? React.createElement(Button, { size: 'small', onClick: handleSafetyRevoke }, '撤回') + : React.createElement(React.Fragment, null, + React.createElement(Button, { size: 'small', onClick: handleSafetySave }, '保存'), + React.createElement(Button, { size: 'small', type: 'primary', onClick: handleSafetySubmit }, '提交') + ) + }, + React.createElement(Table, { rowKey: 'key', columns: safetyFeeColumns, dataSource: safetyFeeRows, pagination: false, bordered: true, size: 'small' }), + safetyMeta.status === '待提交' + ? React.createElement('div', { style: { marginTop: 8, width: '100%' } }, + React.createElement(Button, { type: 'dashed', size: 'small', onClick: addSafetyFeeRow, block: true, style: { width: '100%' } }, '新增一行') + ) + : null, + React.createElement('div', { style: { margin: '16px 0 12px', fontWeight: 600 } }, '违章清单'), + React.createElement(Table, { + rowKey: 'key', + size: 'small', + bordered: true, + pagination: false, + dataSource: violationList, + columns: [ + { title: '违章编码', dataIndex: 'code', key: 'code', width: 130, ellipsis: true }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 110 }, + { title: '违法行为', dataIndex: 'violationBehavior', key: 'violationBehavior', width: 110, ellipsis: true }, + { title: '违法时间', dataIndex: 'violationTime', key: 'violationTime', width: 110 }, + { title: '罚款金额', dataIndex: 'penaltyAmount', key: 'penaltyAmount', width: 90, render: function (v) { return (v || '0.00'); } }, + { title: '缴费状态', dataIndex: 'paymentStatus', key: 'paymentStatus', width: 90 }, + { title: '记分值', dataIndex: 'score', key: 'score', width: 70 }, + { title: '是否处理', dataIndex: 'handleStatus', key: 'handleStatus', width: 90 }, + { title: '违章客户', dataIndex: 'violationCustomer', key: 'violationCustomer', width: 120, ellipsis: true }, + { title: '违章照片', dataIndex: 'violationPhoto', key: 'violationPhoto', width: 90, render: function (v) { return v ? v : ''; } }, + { title: '备注', dataIndex: 'remark', key: 'remark', width: 120, ellipsis: true } + ] + }), + React.createElement('div', { style: { margin: '16px 0 12px', fontWeight: 600 } }, '事故清单'), + React.createElement(Table, { + rowKey: 'key', + size: 'small', + bordered: true, + pagination: false, + dataSource: accidentList, + locale: { emptyText: '暂无事故记录' }, + columns: [ + { title: '事故编码', dataIndex: 'accidentCode', key: 'accidentCode', width: 130, ellipsis: true }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 110 }, + { title: '事故时间', dataIndex: 'accidentTime', key: 'accidentTime', width: 110 }, + { title: '事故地点', dataIndex: 'accidentPlace', key: 'accidentPlace', width: 120, ellipsis: true }, + { title: '事故类型', dataIndex: 'accidentType', key: 'accidentType', width: 110, ellipsis: true }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 120, ellipsis: true }, + { title: '我方定损金额', dataIndex: 'ourClaimAmount', key: 'ourClaimAmount', width: 110, render: function (v) { return v || ''; } }, + { title: '对方定损金额', dataIndex: 'theirClaimAmount', key: 'theirClaimAmount', width: 110, render: function (v) { return v || ''; } }, + { title: '责任划分', dataIndex: 'responsibility', key: 'responsibility', width: 90 }, + { title: '事故状态', dataIndex: 'accidentStatus', key: 'accidentStatus', width: 90 }, + { title: '结案时间', dataIndex: 'closeTime', key: 'closeTime', width: 110 }, + { title: '其它费用', dataIndex: 'otherFee', key: 'otherFee', width: 90, render: function (v) { return v || ''; } }, + { title: '备注', dataIndex: 'remark', key: 'remark', width: 120, ellipsis: true } + ] + }) + ) + ), + + React.createElement('div', { style: footerStyle }, + React.createElement(Button, { type: 'primary', onClick: handleSubmit, disabled: !canSubmitReview() }, canSubmitReview() ? '提交审核' : remainingText()), + React.createElement(Button, { onClick: handleCancel }, '取消') + ), + React.createElement(Modal, { + open: requirementModalOpenState[0], + onCancel: function () { requirementModalOpenState[1](false); }, + onOk: function () { requirementModalOpenState[1](false); }, + title: '需求说明', + width: 860 + }, + React.createElement('div', { style: { maxHeight: '70vh', overflow: 'auto', whiteSpace: 'pre-wrap', lineHeight: 1.7, color: '#333' } }, requirementDocContent) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-finance/pages/03-还车应结款.jsx b/src/prototypes/oneos-web-finance/pages/03-还车应结款.jsx new file mode 100644 index 0000000..6120b85 --- /dev/null +++ b/src/prototypes/oneos-web-finance/pages/03-还车应结款.jsx @@ -0,0 +1,2071 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 财务管理 - 还车应结款(列表页) + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; +var Table = antd.Table; +var Button = antd.Button; +var Modal = antd.Modal; +var Tooltip = antd.Tooltip; +var Select = antd.Select; +var DatePicker = antd.DatePicker; +var message = antd.message; + + var RangePicker = DatePicker.RangePicker; + + var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; + + function fmtYMD(d) { + var dd = d instanceof Date ? d : new Date(); + var p2 = function (n) { return n < 10 ? '0' + n : '' + n; }; + return dd.getFullYear() + '-' + p2(dd.getMonth() + 1) + '-' + p2(dd.getDate()); + } + + function fmtYMDHM(v) { + if (v === null || v === undefined) return '-'; + var s = String(v).trim(); + if (!s) return '-'; + if (/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}/.test(s)) return s.slice(0, 16); + if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s + ' 00:00'; + try { + var d = new Date(s.replace(/-/g, '/')); + if (isNaN(d.getTime())) return s; + var p2 = function (n) { return n < 10 ? '0' + n : '' + n; }; + return d.getFullYear() + '-' + p2(d.getMonth() + 1) + '-' + p2(d.getDate()) + ' ' + p2(d.getHours()) + ':' + p2(d.getMinutes()); + } catch (e) { + return s; + } + } + + function fmtMoney(v) { + var n = typeof v === 'number' ? v : parseFloat(v); + if (isNaN(n)) n = 0; + return n.toFixed(2); + } + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + function IconSubmitted(ok) { + return React.createElement('span', { + style: { + display: 'inline-block', + width: 10, + height: 10, + borderRadius: 999, + background: ok ? '#52c41a' : '#d9d9d9', + border: ok ? '1px solid #52c41a' : '1px solid #d9d9d9' + } + }); + } + + function SubmitCell(ok, name) { + return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 6 } }, + IconSubmitted(ok), + React.createElement('span', { style: { color: ok ? '#333' : '#999' } }, name || (ok ? '-' : '-')) + ); + } + + var approvalStatusOptions = useMemo(function () { + return [ + { value: '待提交', label: '待提交' }, + { value: '待审批', label: '待审批' }, + { value: '审批中', label: '审批中' }, + { value: '审批完成', label: '审批完成' }, + { value: '审批驳回', label: '审批驳回' }, + { value: '撤回', label: '撤回' } + ]; + }, []); + + var tableDataState = useState([ + { + key: 'r1', + contractCode: 'LNZLHT20251106001', + submitSafety: { ok: true, name: '赵六' }, + submitBusiness: { ok: true, name: '张三' }, + submitOperation: { ok: false, name: '王五' }, + submitEnergy: { ok: true, name: '李四' }, + approvalStatus: '待提交', + customerName: '嘉兴某某物流有限公司', + projectName: '嘉兴腾4.5T租赁', + plateNo: '粤AGP5621', + businessDept: '业务一部', + businessOwner: '张经理', + deliveryTime: '2026-02-01 09:30', + returnTime: '2026-02-27 16:20', + returnPerson: '陈还车' + }, + { + key: 'r2', + contractCode: 'LNZLHT20251201002', + submitSafety: { ok: true, name: '赵六' }, + submitBusiness: { ok: true, name: '张三' }, + submitOperation: { ok: true, name: '王五' }, + submitEnergy: { ok: true, name: '李四' }, + approvalStatus: '待审批', + customerName: '上海馨想事成物流有限公司', + projectName: '上海干线运输项目', + plateNo: '浙F03218F', + businessDept: '业务二部', + businessOwner: '李主管', + deliveryTime: '2026-01-10 10:00', + returnTime: '2026-02-20 18:05', + returnPerson: '周还车' + }, + { + key: 'r3', + contractCode: 'ZYYGHT20260105003', + submitSafety: { ok: false, name: '' }, + submitBusiness: { ok: false, name: '' }, + submitOperation: { ok: false, name: '' }, + submitEnergy: { ok: false, name: '' }, + approvalStatus: '审批驳回', + customerName: '北京海龙运输有限公司', + projectName: '京津冀冷链项目', + plateNo: '京A29256F', + businessDept: '业务三部', + businessOwner: '王总', + deliveryTime: '2025-08-01 08:30', + returnTime: '2026-02-02 11:15', + returnPerson: '刘还车' + }, + { + key: 'r4', + contractCode: 'LNZLHT20251106004', + submitSafety: { ok: true, name: '赵六' }, + submitBusiness: { ok: false, name: '' }, + submitOperation: { ok: true, name: '王五' }, + submitEnergy: { ok: false, name: '' }, + approvalStatus: '审批中', + customerName: '苏州某某快运有限公司', + projectName: '苏州城配项目', + plateNo: '苏E8K2P1', + businessDept: '业务一部', + businessOwner: '张经理', + deliveryTime: '2026-02-15 13:40', + returnTime: '2026-02-26 09:10', + returnPerson: '孙还车' + }, + { + key: 'r5', + contractCode: 'LNZLHT20251106005', + submitSafety: { ok: true, name: '赵六' }, + submitBusiness: { ok: true, name: '张三' }, + submitOperation: { ok: true, name: '王五' }, + submitEnergy: { ok: false, name: '' }, + approvalStatus: '撤回', + customerName: '杭州某某物流有限公司', + projectName: '杭州港区项目', + plateNo: '浙A3M8Q7', + businessDept: '业务二部', + businessOwner: '李主管', + deliveryTime: '2026-02-05', + returnTime: '2026-02-25', + returnPerson: '吴还车' + }, + { + key: 'r6', + contractCode: 'LNZLHT20251215006', + submitSafety: { ok: true, name: '赵六' }, + submitBusiness: { ok: true, name: '张三' }, + submitOperation: { ok: true, name: '王五' }, + submitEnergy: { ok: true, name: '李四' }, + approvalStatus: '审批完成', + customerName: '宁波某某供应链有限公司', + projectName: '宁波港区支线项目', + plateNo: '浙B6H9Q2', + businessDept: '业务三部', + businessOwner: '周经理', + deliveryTime: '2026-01-05', + returnTime: '2026-02-18', + returnPerson: '郑还车' + }, + { + key: 'r7', + contractCode: 'LNZLHT20260108007', + submitSafety: { ok: true, name: '赵六' }, + submitBusiness: { ok: false, name: '' }, + submitOperation: { ok: false, name: '' }, + submitEnergy: { ok: false, name: '' }, + approvalStatus: '待提交', + customerName: '合肥某某物流有限公司', + projectName: '合肥城配项目', + plateNo: '皖A7K3P8', + businessDept: '业务一部', + businessOwner: '张经理', + deliveryTime: '2026-02-10', + returnTime: '2026-02-28', + returnPerson: '许还车' + }, + { + key: 'r8', + contractCode: 'ZYYGHT20260112008', + submitSafety: { ok: true, name: '赵六' }, + submitBusiness: { ok: true, name: '张三' }, + submitOperation: { ok: true, name: '王五' }, + submitEnergy: { ok: true, name: '李四' }, + approvalStatus: '待审批', + customerName: '深圳某某快运有限公司', + projectName: '深莞干线项目', + plateNo: '粤B2P6M9', + businessDept: '业务二部', + businessOwner: '李主管', + deliveryTime: '2026-01-20', + returnTime: '2026-02-22', + returnPerson: '彭还车' + }, + { + key: 'r9', + contractCode: 'LNZLHT20251130009', + submitSafety: { ok: true, name: '赵六' }, + submitBusiness: { ok: true, name: '张三' }, + submitOperation: { ok: true, name: '王五' }, + submitEnergy: { ok: true, name: '李四' }, + approvalStatus: '审批中', + customerName: '天津某某运输有限公司', + projectName: '津冀干线项目', + plateNo: '津A5R8L1', + businessDept: '业务三部', + businessOwner: '王总', + deliveryTime: '2026-02-02', + returnTime: '2026-02-27', + returnPerson: '韩还车' + }, + { + key: 'r10', + contractCode: 'LNZLHT20251118010', + submitSafety: { ok: true, name: '赵六' }, + submitBusiness: { ok: true, name: '张三' }, + submitOperation: { ok: true, name: '王五' }, + submitEnergy: { ok: false, name: '' }, + approvalStatus: '审批驳回', + customerName: '武汉某某物流有限公司', + projectName: '武汉园区项目', + plateNo: '鄂A8D1Q6', + businessDept: '业务一部', + businessOwner: '周经理', + deliveryTime: '2026-01-15', + returnTime: '2026-02-16', + returnPerson: '蒋还车' + } + ]); + var tableDataAll = tableDataState[0]; + var setTableDataAll = tableDataState[1]; + + var contractOptions = useMemo(function () { + var map = {}; + tableDataAll.forEach(function (r) { map[r.contractCode] = true; }); + return Object.keys(map).map(function (v) { return { value: v, label: v }; }); + }, [tableDataAll]); + var customerOptions = useMemo(function () { + var map = {}; + tableDataAll.forEach(function (r) { map[r.customerName] = true; }); + return Object.keys(map).map(function (v) { return { value: v, label: v }; }); + }, [tableDataAll]); + var projectOptions = useMemo(function () { + var map = {}; + tableDataAll.forEach(function (r) { map[r.projectName] = true; }); + return Object.keys(map).map(function (v) { return { value: v, label: v }; }); + }, [tableDataAll]); + var plateOptions = useMemo(function () { + var map = {}; + tableDataAll.forEach(function (r) { map[r.plateNo] = true; }); + return Object.keys(map).map(function (v) { return { value: v, label: v }; }); + }, [tableDataAll]); + + var filtersState = useState({ + contractCode: undefined, + customerName: undefined, + projectName: undefined, + plateNo: undefined, + returnDateRange: null, + approvalStatus: undefined + }); + var filters = filtersState[0]; + var setFilters = filtersState[1]; +var filterMoreOpenState = useState(false); + + function resetFilters() { + setFilters({ + contractCode: undefined, + customerName: undefined, + projectName: undefined, + plateNo: undefined, + returnDateRange: null, + approvalStatus: undefined + }); + } + + function inReturnRange(returnTimeStr, range) { + if (!range || !range[0] || !range[1]) return true; + var s = new Date(String(range[0]).replace(/-/g, '/')); + var e = new Date(String(range[1]).replace(/-/g, '/')); + var d = new Date(String(returnTimeStr || '').replace(/-/g, '/')); + if (isNaN(s.getTime()) || isNaN(e.getTime()) || isNaN(d.getTime())) return true; + var ms = d.getTime(); + return ms >= s.getTime() && ms <= (e.getTime() + 24 * 60 * 60 * 1000 - 1); + } + + var filteredData = useMemo(function () { + return (tableDataAll || []).filter(function (r) { + if (filters.contractCode && r.contractCode !== filters.contractCode) return false; + if (filters.customerName && r.customerName !== filters.customerName) return false; + if (filters.projectName && r.projectName !== filters.projectName) return false; + if (filters.plateNo && r.plateNo !== filters.plateNo) return false; + if (filters.approvalStatus && r.approvalStatus !== filters.approvalStatus) return false; + if (!inReturnRange(r.returnTime, filters.returnDateRange)) return false; + return true; + }); + }, [tableDataAll, filters]); + +var billModalOpenState = useState(false); +var billModalModeState = useState('export'); // generate | export +var billRowState = useState(null); + +var requirementModalOpenState = useState(false); +var requirementDocContent = useMemo(function () { + return (`一个「数字化资产ONEOS运管平台」中的「还车应结款」模块 +#面包屑:财务管理-还车应结款 + +1.筛选; +1.1.合同编号:选择器,支持输入框内输入合同编号模糊搜索下拉显示匹配项; +1.2.客户名称:选择器,支持输入框内输入客户名称模糊搜索下拉显示匹配项; +1.3.项目名称:选择器,支持输入框内输入项目名称模糊搜索下拉显示匹配项; +1.4.车牌号:选择器,支持输入框内输入车牌号模糊搜索下拉显示匹配项; +1.5.还车时间:日期选择器,支持单输入框双日历选择开始-结束日期,精确至日; +1.6.审批状态:选择器,分为待提交、待审批、审批中、审批完成、审批驳回、撤回; + +2.还车应结款列表:右侧为导出; +2.1.合同编号:显示合同编号; +2.2.提交情况(合并表头):分为安全组、业务服务组、运维组、能源组,内容为各组提交人姓名,前方为已提交/未提交图标; +2.3.审批状态:待提交、待审批、审批中、审批完成、审批驳回、撤回; + 2.3.1.待提交:未完成4部门提交也未提交审批; + 2.3.2.待审批:已提交审批,但没有任意节点进行审批; + 2.3.3.审批中:已提交审批,已有节点完成审批,但未完成最终节点审批; + 2.3.4.审批完成:已提交审批,并完成最终节点审批; + 2.3.5.审批驳回:已提交审批,但在任意节点被驳回; + 2.3.6.撤回:已提交审批,但在最终节点审批完成前主动撤回; +2.4.客户名称:显示合同对应客户名称; +2.5.项目名称:显示合同对应项目名称; +2.6.车牌号:显示合同对应车牌号; +2.7.业务部门:显示合同对应业务部门; +2.8.业务负责人:显示合同对应业务负责人; +2.9.交车时间:显示该车辆交车时间,格式为:YYYY-MM-DD HH:MM; +2.10.还车时间:显示该车辆还车时间,格式为:YYYY-MM-DD HH:MM; +2.11.还车人:显示该车辆还车人姓名; +2.12.操作:查看、生成账单、费用明细、撤回; + 2.12.1.查看:点击跳转还车应结款-查看页面; + 2.12.2.生成账单:点击生成账单,弹框显示账单界面,审批状态为待审批的记录才可生成账单,其他状态生成账单隐藏; + 2.12.3.费用明细:点击跳转还车应结款-费用明细页面,审批状态为待审批、审批中、审批完成时,不显示费用明细; + 2.12.4.撤回:审批状态为待审批、审批中时,不显示费用明细点击二次确认,提示:是否确认撤回,点击确定后,提示:撤回成功,同时审批状态修改为撤回; +2.13.右下角为分页符,支持单页查看数据条数; +`); +}, []); + + var billDetail = useMemo(function () { + var row = billRowState[0] || (filteredData && filteredData[0]) || (tableDataAll && tableDataAll[0]) || {}; + var customerName = row.customerName || '广州毅斌物流有限公司'; + var plateNo = row.plateNo || '粤AGP6579'; + var deliveryTime = fmtYMDHM(row.deliveryTime || '2025-07-21 00:00'); + var returnTime = fmtYMDHM(row.returnTime || '2025-10-01 00:00'); + var billDate = fmtYMD(new Date()); + + var feeRows = [ + { item: '违章处理违约金', amount: '0.00' }, + { item: '保险上浮', amount: '0.00' }, + { item: 'ETC-客', amount: '100.00' }, + { item: 'ETC卡缺损费', amount: '0.00' }, + { item: 'ETC设备缺损费', amount: '0.00' }, + { item: '租金', amount: '0.00' }, + { item: '电费—客', amount: '0.00' }, + { item: '氢气费—客', amount: '0.00' }, + { item: '退还车氢量差', amount: '284.54' }, + { item: '能源费补缴', amount: '0.00' }, + { item: '能源费退款', amount: '0.00' }, + { item: '清洗费', amount: '0.00' }, + { item: '未结算保养费', amount: '372.50' }, + { item: '未结算维修费', amount: '0.00' }, + { item: '车损费', amount: '0.00' }, + { item: '工具损坏或丢失费', amount: '0.00' }, + { item: '证件费', amount: '0.00' }, + { item: '广告损坏费', amount: '0.00' }, + { item: '轮胎磨损费', amount: '0.00' } + ]; + var total = feeRows.reduce(function (s, r) { return s + (parseFloat(r.amount) || 0); }, 0); + var depositPaid = 2000.00; + var refund = Math.max(0, depositPaid - total); + var pay = Math.max(0, total - depositPaid); + + return { + customerName: customerName, + billDate: billDate, + vehicle: { seq: 1, plateNo: plateNo, deliveryTime: deliveryTime, returnTime: returnTime }, + feeRows: feeRows, + total: fmtMoney(total), + depositPaid: fmtMoney(depositPaid), + pendingSettle: fmtMoney(total), + refund: fmtMoney(refund), + pay: fmtMoney(pay) + }; + }, [billRowState[0], filteredData, tableDataAll]); + + function openBillModal(mode, row) { + billModalModeState[1](mode); + billRowState[1](row || null); + billModalOpenState[1](true); + } + + function printBill() { + try { + var el = document.getElementById('bill-print-area'); + if (!el) { message.error('未找到可打印区域'); return; } + var w = window.open('', '_blank'); + if (!w) { message.error('浏览器拦截了新窗口,请允许弹窗后重试'); return; } + var style = [ + 'body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;color:#000;padding:24px;}', + 'table{width:100%;border-collapse:collapse;}', + 'th,td{border:1px solid #d9d9d9;padding:8px 10px;font-size:12px;vertical-align:top;}', + 'th{background:#0f6b2d;color:#fff;font-weight:600;}', + '.t-center{text-align:center;} .t-right{text-align:right;}', + '.muted{color:#666;}' + ].join(''); + w.document.open(); + w.document.write('账单明细' + el.innerHTML + ''); + w.document.close(); + w.focus(); + w.print(); + } catch (e) { + message.error('打印失败(原型):' + (e && e.message ? e.message : '未知错误')); + } + } + + var pageState = useState({ current: 1, pageSize: 20 }); + var page = pageState[0]; + var setPage = pageState[1]; + + useMemo(function () { + if (page.current !== 1) setPage(function (p) { return Object.assign({}, p, { current: 1 }); }); + return null; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filters.contractCode, filters.customerName, filters.projectName, filters.plateNo, filters.approvalStatus, filters.returnDateRange]); + + function handleView(row) { + message.info('跳转:还车应结款-查看(原型) 合同编号:' + (row && row.contractCode)); + } + function handleFeeDetail(row) { + message.info('跳转:还车应结款-费用明细(原型) 合同编号:' + (row && row.contractCode)); + } + function handleGenerateBill(row) { + openBillModal('generate', row); + } + function handleRevoke(row) { + Modal.confirm({ + title: '撤回确认', + content: '是否确认撤回?', + okText: '确定', + cancelText: '取消', + onOk: function () { + setTableDataAll(function (p) { + return (p || []).map(function (r) { + if (!row || r.key !== row.key) return r; + var n = {}; for (var k in r) n[k] = r[k]; + n.approvalStatus = '撤回'; + return n; + }); + }); + message.success('撤回成功'); + } + }); + } + + var columns = useMemo(function () { + return [ + { title: '合同编号', dataIndex: 'contractCode', key: 'contractCode', width: 170, fixed: 'left', ellipsis: true }, + { + title: '提交情况', + key: 'submitGroup', + children: [ + { + title: '安全组', + key: 'submitSafety', + width: 140, + render: function (_, r) { return SubmitCell(r.submitSafety && r.submitSafety.ok, r.submitSafety && r.submitSafety.name); } + }, + { + title: '业务服务组', + key: 'submitBusiness', + width: 140, + render: function (_, r) { return SubmitCell(r.submitBusiness && r.submitBusiness.ok, r.submitBusiness && r.submitBusiness.name); } + }, + { + title: '运维组', + key: 'submitOperation', + width: 140, + render: function (_, r) { return SubmitCell(r.submitOperation && r.submitOperation.ok, r.submitOperation && r.submitOperation.name); } + }, + { + title: '能源组', + key: 'submitEnergy', + width: 140, + render: function (_, r) { return SubmitCell(r.submitEnergy && r.submitEnergy.ok, r.submitEnergy && r.submitEnergy.name); } + } + ] + }, + { title: '审批状态', dataIndex: 'approvalStatus', key: 'approvalStatus', width: 110 }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 180, ellipsis: true }, + { title: '项目名称', dataIndex: 'projectName', key: 'projectName', width: 160, ellipsis: true }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 110 }, + { title: '业务部门', dataIndex: 'businessDept', key: 'businessDept', width: 110 }, + { title: '业务负责人', dataIndex: 'businessOwner', key: 'businessOwner', width: 110 }, + { title: '交车时间', dataIndex: 'deliveryTime', key: 'deliveryTime', width: 160, render: function (v) { return fmtYMDHM(v); } }, + { title: '还车时间', dataIndex: 'returnTime', key: 'returnTime', width: 160, render: function (v) { return fmtYMDHM(v); } }, + { title: '还车人', dataIndex: 'returnPerson', key: 'returnPerson', width: 110 }, + { + title: '操作', + key: 'action', + width: 220, + fixed: 'right', + render: function (_, r) { + var st = String(r && r.approvalStatus); + var showFeeDetail = !(st === '待审批' || st === '审批中' || st === '审批完成'); + var showRevoke = (st === '待审批' || st === '审批中'); + var showGenerateBill = String(r && r.approvalStatus) === '待审批'; + return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 8 } }, + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { handleView(r); } }, '查看'), + showGenerateBill ? React.createElement(Button, { type: 'link', size: 'small', onClick: function () { handleGenerateBill(r); } }, '生成账单') : null, + showFeeDetail ? React.createElement(Button, { type: 'link', size: 'small', onClick: function () { handleFeeDetail(r); } }, '费用明细') : null, + showRevoke ? React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function () { handleRevoke(r); } }, '撤回') : null + ); + } + } + ]; + }, []); + +return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + React.createElement(Breadcrumb, { items: [{ title: '财务管理' }, { title: '还车应结款' }] }), + React.createElement(Button, { type: 'link', onClick: function () { requirementModalOpenState[1](true); } }, '查看需求说明') + ), + + React.createElement(Card, { title: '筛选', style: cardStyle }, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 12 } }, + React.createElement('div', null, + React.createElement('div', { style: { fontSize: 12, color: '#666', marginBottom: 6 } }, '合同编号'), + React.createElement(Select, { + value: filters.contractCode, + options: contractOptions, + allowClear: true, + placeholder: '请输入或选择合同编号', + showSearch: true, + filterOption: filterOption, + style: { width: '100%' }, + onChange: function (v) { setFilters(function (p) { return Object.assign({}, p, { contractCode: v }); }); } + }) + ), + React.createElement('div', null, + React.createElement('div', { style: { fontSize: 12, color: '#666', marginBottom: 6 } }, '客户名称'), + React.createElement(Select, { + value: filters.customerName, + options: customerOptions, + allowClear: true, + placeholder: '请输入或选择客户名称', + showSearch: true, + filterOption: filterOption, + style: { width: '100%' }, + onChange: function (v) { setFilters(function (p) { return Object.assign({}, p, { customerName: v }); }); } + }) + ), + React.createElement('div', null, + React.createElement('div', { style: { fontSize: 12, color: '#666', marginBottom: 6 } }, '项目名称'), + React.createElement(Select, { + value: filters.projectName, + options: projectOptions, + allowClear: true, + placeholder: '请输入或选择项目名称', + showSearch: true, + filterOption: filterOption, + style: { width: '100%' }, + onChange: function (v) { setFilters(function (p) { return Object.assign({}, p, { projectName: v }); }); } + }) + ) + , + filterMoreOpenState[0] ? React.createElement(React.Fragment, null, + React.createElement('div', null, + React.createElement('div', { style: { fontSize: 12, color: '#666', marginBottom: 6 } }, '车牌号'), + React.createElement(Select, { + value: filters.plateNo, + options: plateOptions, + allowClear: true, + placeholder: '请输入或选择车牌号', + showSearch: true, + filterOption: filterOption, + style: { width: '100%' }, + onChange: function (v) { setFilters(function (p) { return Object.assign({}, p, { plateNo: v }); }); } + }) + ), + React.createElement('div', null, + React.createElement('div', { style: { fontSize: 12, color: '#666', marginBottom: 6 } }, '还车时间'), + React.createElement(RangePicker, { + value: filters.returnDateRange, + allowClear: true, + style: { width: '100%' }, + placeholder: ['请选择开始日期', '请选择结束日期'], + onChange: function (v) { setFilters(function (p) { return Object.assign({}, p, { returnDateRange: v }); }); } + }) + ), + React.createElement('div', null, + React.createElement('div', { style: { fontSize: 12, color: '#666', marginBottom: 6 } }, '审批状态'), + React.createElement(Select, { + value: filters.approvalStatus, + options: approvalStatusOptions, + allowClear: true, + placeholder: '请选择审批状态', + style: { width: '100%' }, + onChange: function (v) { setFilters(function (p) { return Object.assign({}, p, { approvalStatus: v }); }); } + }) + ) + ) : null + ), + React.createElement('div', { style: { marginTop: 12, display: 'flex', justifyContent: 'flex-end', gap: 8 } }, + React.createElement(Button, { type: 'link', onClick: function () { filterMoreOpenState[1](!filterMoreOpenState[0]); } }, filterMoreOpenState[0] ? '收起' : '展开'), + React.createElement(Button, { onClick: resetFilters }, '重置'), + React.createElement(Button, { type: 'primary', onClick: function () { message.success('已查询(原型)'); } }, '查询') + ) + ), + + React.createElement(Card, { + title: '还车应结款列表', + style: cardStyle, + extra: React.createElement('div', { style: { display: 'inline-flex', alignItems: 'center', gap: 8 } }, + React.createElement(Button, { onClick: function () { openBillModal('export', (filteredData && filteredData[0]) || null); } }, '导出') + ) + }, + React.createElement(Table, { + rowKey: 'key', + columns: columns, + dataSource: filteredData, + bordered: true, + size: 'middle', + scroll: { x: 1650 }, + pagination: { + current: page.current, + pageSize: page.pageSize, + showSizeChanger: true, + pageSizeOptions: ['20', '50', '100'], + showQuickJumper: true, + total: (filteredData || []).length, + showTotal: function (t) { return '共 ' + t + ' 条'; }, + onChange: function (c, s) { setPage({ current: c, pageSize: s }); } + } + }) + ), + + React.createElement(Modal, { + open: billModalOpenState[0], + onCancel: function () { billModalOpenState[1](false); }, + footer: React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8 } }, + React.createElement(Button, { type: 'primary', onClick: printBill }, '打印'), + React.createElement(Button, { onClick: function () { billModalOpenState[1](false); } }, '返回') + ), + title: (billModalModeState[0] === 'generate' ? '生成账单' : '导出账单'), + width: 980 + }, + React.createElement('div', { id: 'bill-print-area' }, + React.createElement('div', { style: { fontSize: 18, fontWeight: 700, textAlign: 'center', marginBottom: 18 } }, '车辆退车结算明细'), + React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', marginBottom: 12, fontSize: 13 } }, + React.createElement('div', null, React.createElement('span', { style: { fontWeight: 600 } }, '客户名称:'), billDetail.customerName), + React.createElement('div', null, React.createElement('span', { style: { fontWeight: 600 } }, '账单生成日期:'), billDetail.billDate) + ), + React.createElement('table', null, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { className: 't-center', style: { width: 60 } }, '序号'), + React.createElement('th', { className: 't-center', style: { width: 120 } }, '车牌'), + React.createElement('th', { className: 't-center', style: { width: 120 } }, '交车日期'), + React.createElement('th', { className: 't-center', style: { width: 120 } }, '退车日期'), + React.createElement('th', { className: 't-center' }, '费用科目'), + React.createElement('th', { className: 't-center', style: { width: 140 } }, '待结算费用') + ) + ), + React.createElement('tbody', null, + (billDetail.feeRows || []).map(function (r, idx) { + var rowspan = (billDetail.feeRows || []).length + 1; + return React.createElement('tr', { key: 'fee-' + idx }, + idx === 0 ? React.createElement('td', { rowSpan: rowspan, className: 't-center' }, String(billDetail.vehicle.seq || 1)) : null, + idx === 0 ? React.createElement('td', { rowSpan: rowspan, className: 't-center' }, billDetail.vehicle.plateNo) : null, + idx === 0 ? React.createElement('td', { rowSpan: rowspan, className: 't-center' }, fmtYMDHM(billDetail.vehicle.deliveryTime)) : null, + idx === 0 ? React.createElement('td', { rowSpan: rowspan, className: 't-center' }, fmtYMDHM(billDetail.vehicle.returnTime)) : null, + React.createElement('td', null, r.item), + React.createElement('td', { className: 't-right' }, fmtMoney(r.amount)) + ); + }), + React.createElement('tr', { key: 'fee-total', style: { background: '#f6f7f8' } }, + React.createElement('td', { style: { fontWeight: 700 } }, '合计'), + React.createElement('td', { className: 't-right', style: { fontWeight: 700 } }, billDetail.total) + ) + ) + ), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', rowGap: 10, marginTop: 18, fontSize: 13 } }, + React.createElement('div', null, React.createElement('span', { style: { fontWeight: 600 } }, '已支付保证金:'), billDetail.depositPaid), + React.createElement('div', null, React.createElement('span', { style: { fontWeight: 600 } }, '待结算费用:'), billDetail.pendingSettle), + React.createElement('div', null, React.createElement('span', { style: { fontWeight: 600 } }, '应退款:'), billDetail.refund), + React.createElement('div', null, React.createElement('span', { style: { fontWeight: 600 } }, '应缴纳:'), billDetail.pay) + ) + ) + ), + React.createElement(Modal, { + open: requirementModalOpenState[0], + onCancel: function () { requirementModalOpenState[1](false); }, + onOk: function () { requirementModalOpenState[1](false); }, + title: '需求说明', + width: 860 + }, + React.createElement('div', { style: { maxHeight: '70vh', overflow: 'auto', whiteSpace: 'pre-wrap', lineHeight: 1.7, color: '#333' } }, requirementDocContent) + ) + ); +}; + +// 【重要】必须使用 const Component 作为组件变量名 +// 财务管理 - 还车应结款 + +// (历史残留)费用明细页代码误追加到本文件,为避免重复声明 Component,改名保留但不再作为入口组件使用 +const ComponentFeeDetail = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + var useEffect = React.useEffect; + var useRef = React.useRef; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Table = antd.Table; + var Button = antd.Button; + var Input = antd.Input; + var Modal = antd.Modal; + var Tooltip = antd.Tooltip; + var Popover = antd.Popover; + var message = antd.message; + + var TextArea = Input.TextArea; + + function RequiredLabel(text) { + return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 4 } }, + React.createElement('span', { style: { color: '#f5222d', fontWeight: 600 } }, '*'), + React.createElement('span', null, text) + ); + } + + function formatDateTimeNow() { + var d = new Date(); + var pad = function (n) { return n < 10 ? '0' + n : '' + n; }; + return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()); + } + + function toFixed2(v) { + if (v === null || v === undefined || v === '') return ''; + var n = typeof v === 'number' ? v : parseFloat(v); + return isNaN(n) ? '' : n.toFixed(2); + } + + // 页面样式 + var layoutStyle = { padding: '16px 24px 88px', background: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; + var footerStyle = { position: 'fixed', left: 0, right: 0, bottom: 0, background: '#fff', borderTop: '1px solid #f0f0f0', padding: '12px 24px', display: 'flex', justifyContent: 'flex-start', gap: 12, zIndex: 10 }; + + // 还车车辆明细(单车示例) + var vehicleDetail = useMemo(function () { + return [{ + key: 'v1', + plateNo: '粤AGP5621', + contractCode: 'LNZLHT20251106001', + projectName: '嘉兴腾4.5T租赁', + customerName: '嘉兴某某物流有限公司', + deliveryTime: '2026-02-01', + returnTime: '2026-02-27', + fragileInsurance: '是', + tireInsurance: '否', + maintenanceInsurance: '是' + }]; + }, []); + + var insuranceFlags = useMemo(function () { + var v = (vehicleDetail && vehicleDetail[0]) || {}; + return { + hasFragileInsurance: String(v.fragileInsurance || '') === '是', + hasTireInsurance: String(v.tireInsurance || '') === '是', + hasMaintenanceInsurance: String(v.maintenanceInsurance || '') === '是' + }; + }, [vehicleDetail]); + + // 费用统计 + var statsState = useState({ + depositAmount: '5000.00', + pendingSettleAmount: '0.00', + shouldRefundAmount: '800.00', + shouldPayAmount: '400.00' + }); + var stats = statsState[0]; + + // 通用折叠区块(展开/收起图标) + var CollapseSection = function (props) { + var collapsed = props.collapsed; + var setCollapsed = props.setCollapsed; + var title = props.title; + var extra = props.extra; + var headerActions = props.headerActions; + return React.createElement('div', { style: { background: '#fff', border: '1px solid #f0f0f0', borderRadius: 8, marginBottom: 12, overflow: 'hidden' } }, + React.createElement('div', { + style: { padding: '12px 16px', display: 'flex', alignItems: 'center', gap: 12, cursor: 'pointer', background: '#fff' }, + onClick: function () { setCollapsed(!collapsed); } + }, + React.createElement('div', { style: { fontWeight: 600, fontSize: 14 } }, title), + React.createElement('div', { style: { flex: 1 } }), + extra ? React.createElement('div', { style: { display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 12, color: '#666', fontSize: 13 } }, extra) : null, + headerActions ? React.createElement('div', { onClick: function (e) { e.stopPropagation(); }, style: { display: 'inline-flex', alignItems: 'center', gap: 8 } }, headerActions) : null, + collapsed + ? React.createElement('svg', { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, style: { display: 'block', color: '#666' } }, React.createElement('path', { d: 'M6 9l6 6 6-6' })) + : React.createElement('svg', { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, style: { display: 'block', color: '#666' } }, React.createElement('path', { d: 'M18 15l-6-6-6 6' })) + ), + collapsed ? null : React.createElement('div', { style: { padding: '12px 16px', borderTop: '1px solid #f0f0f0' } }, props.children) + ); + }; + + // 业务服务组 + var businessServiceFixedItems = ['违章处理违约金', '保险上浮', 'ETC-客户未缴费用', 'ETC卡缺损费', 'ETC设备缺损费']; + var businessServiceRowsState = useState( + businessServiceFixedItems.map(function (name, i) { + return { key: 'bs-' + i, seq: i + 1, feeItem: name, amount: '', remark: '', lastUpdateTime: '', photos: [], attachments: [], fixed: true }; + }) + ); + var businessServiceRows = businessServiceRowsState[0]; + var setBusinessServiceRows = businessServiceRowsState[1]; + var businessServiceMetaState = useState({ submitBy: '', status: '待提交' }); + var businessServiceMeta = businessServiceMetaState[0]; + var setBusinessServiceMeta = businessServiceMetaState[1]; + var businessServiceCollapsedState = useState(false); + + function recalcBusinessServiceTotal(list) { + var sum = 0; + (list || []).forEach(function (r) { sum += (parseFloat(r.amount) || 0); }); + return sum.toFixed(2); + } + + var businessServiceTotalComputed = useMemo(function () { + return recalcBusinessServiceTotal(businessServiceRows || []); + }, [businessServiceRows]); + + function calcVehicleRent(monthRent, billStartDateStr, returnDateStr) { + var m = parseFloat(monthRent) || 0; + if (!billStartDateStr || !returnDateStr) return '0.00'; + var s = new Date(String(billStartDateStr).replace(/-/g, '/')); + var e = new Date(String(returnDateStr).replace(/-/g, '/')); + if (isNaN(s.getTime()) || isNaN(e.getTime())) return '0.00'; + var msDay = 24 * 60 * 60 * 1000; + var days = Math.floor((e.getTime() - s.getTime()) / msDay); + if (days < 0) days = 0; + return ((m / 30) * days).toFixed(2); + } + + var billInfoState = useState({ + billStartDate: '2026-02-01', + vehicleMonthRent: '9000.00', + receivedRent: '0.00', + actualRent: '', + actualRentManual: false, + shouldRefundRent: '', + shouldRefundManual: false + }); + var billInfo = billInfoState[0]; + var setBillInfo = billInfoState[1]; + + useEffect(function () { + if (billInfo.actualRentManual && String(billInfo.actualRent || '').trim() !== '') return; + var returnDateStr = (vehicleDetail && vehicleDetail[0] && vehicleDetail[0].returnTime) || ''; + var calc = calcVehicleRent(billInfo.vehicleMonthRent, billInfo.billStartDate, returnDateStr); + if (billInfo.actualRent !== calc) { + setBillInfo(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.actualRent = calc; return n; }); + } + }, [billInfo.vehicleMonthRent, billInfo.billStartDate, billInfo.actualRentManual, vehicleDetail]); + + useEffect(function () { + if (billInfo.shouldRefundManual && String(billInfo.shouldRefundRent || '').trim() !== '') return; + var received = parseFloat(billInfo.receivedRent) || 0; + var actual = parseFloat(billInfo.actualRent) || 0; + var calc = (received - actual).toFixed(2); + if (billInfo.shouldRefundRent !== calc) { + setBillInfo(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.shouldRefundRent = calc; return n; }); + } + }, [billInfo.receivedRent, billInfo.actualRent, billInfo.shouldRefundManual]); + + var addBusinessServiceRow = useCallback(function () { + setBusinessServiceRows(function (p) { + var next = p.slice(); + var i = next.length + 1; + next.push({ key: 'bs-new-' + Date.now(), seq: i, feeItem: '', amount: '', remark: '', lastUpdateTime: '', photos: [], attachments: [], fixed: false }); + return next.map(function (r, idx) { var o = {}; for (var k in r) o[k] = r[k]; o.seq = idx + 1; return o; }); + }); + }, []); + var removeBusinessServiceRow = useCallback(function (key) { + setBusinessServiceRows(function (p) { + var next = p.filter(function (r) { return r.key !== key; }); + return next.map(function (r, idx) { var o = {}; for (var k in r) o[k] = r[k]; o.seq = idx + 1; return o; }); + }); + }, []); + + var updateBusinessServiceRow = useCallback(function (key, field, value) { + setBusinessServiceRows(function (p) { + var next = p.map(function (r) { + if (r.key !== key) return r; + var n = {}; for (var k in r) n[k] = r[k]; + n[field] = value; + return n; + }); + return next; + }); + }, []); + + var handleBusinessServiceSave = useCallback(function () { + setBusinessServiceMeta(function (m) { var nm = {}; for (var k in m) nm[k] = m[k]; nm.status = '待提交'; return nm; }); + message.success('已保存'); + }, []); + + var handleBusinessServiceSubmit = useCallback(function () { + Modal.confirm({ + title: '提交确认', + content: '请确认业务服务组金额填写无误,点击确认完成提交。', + okText: '确认', + cancelText: '取消', + onOk: function () { + setBusinessServiceRows(function (p) { + var now = formatDateTimeNow(); + return p.map(function (r) { var n = {}; for (var k in r) n[k] = r[k]; n.lastUpdateTime = now; return n; }); + }); + setBusinessServiceMeta(function (m) { var nm = {}; for (var k in m) nm[k] = m[k]; nm.status = '已提交'; nm.submitBy = '业务服务组-张三'; return nm; }); + message.success('已提交'); + } + }); + }, []); + + var handleBusinessServiceRevoke = useCallback(function () { + Modal.confirm({ + title: '撤回确认', + content: '是否确认撤回?', + okText: '确认', + cancelText: '取消', + onOk: function () { + setBusinessServiceMeta(function (m) { var nm = {}; for (var k in m) nm[k] = m[k]; nm.status = '待提交'; return nm; }); + message.success('已撤回,可重新编辑并保存/提交'); + } + }); + }, []); + + // 能源采购组 + var energyState = useState({ + deliveryHydrogen: '85.00', + returnHydrogen: '72.00', + hydrogenUnitPrice: '35.00', + hydrogenSupplement: '455.00', + hydrogenFee: '', + electricFee: '', + prepayRefund: '', + userBalance: '1200.00' + }); + var energy = energyState[0]; + var setEnergy = energyState[1]; + var energyMetaState = useState({ submitBy: '', status: '待提交' }); + var energyMeta = energyMetaState[0]; + var setEnergyMeta = energyMetaState[1]; + var energyCollapsedState = useState(false); + + function recalcEnergyTotal(e) { + var a = parseFloat((e && e.hydrogenSupplement) || '') || 0; + var b = parseFloat((e && e.hydrogenFee) || '') || 0; + var c = parseFloat((e && e.electricFee) || '') || 0; + return (a + b + c).toFixed(2); + } + + var energyTotalComputed = useMemo(function () { return recalcEnergyTotal(energy); }, [energy.hydrogenSupplement, energy.hydrogenFee, energy.electricFee]); + + useEffect(function () { + var d = parseFloat(energy.deliveryHydrogen) || 0; + var r = parseFloat(energy.returnHydrogen) || 0; + var u = parseFloat(energy.hydrogenUnitPrice) || 0; + if (d > r) { + var calc = ((d - r) * u).toFixed(2); + if (energy.hydrogenSupplement !== calc) { + setEnergy(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.hydrogenSupplement = calc; return n; }); + } + } + }, [energy.deliveryHydrogen, energy.returnHydrogen, energy.hydrogenUnitPrice]); + + // energy 总金额使用 energyTotalComputed 派生,避免输入时额外 setState 造成丢焦 + + var handleEnergySave = useCallback(function () { + setEnergyMeta(function (m) { var nm = {}; for (var k in m) nm[k] = m[k]; nm.status = '待提交'; return nm; }); + message.success('能源采购组已保存'); + }, [energy]); + var handleEnergySubmit = useCallback(function () { + Modal.confirm({ + title: '提交确认', + content: '请确认能源采购组金额填写无误,点击确认完成提交。', + okText: '确认', + cancelText: '取消', + onOk: function () { + setEnergyMeta(function (m) { var nm = {}; for (var k in m) nm[k] = m[k]; nm.submitBy = '能源采购组-李四'; nm.status = '已提交'; return nm; }); + message.success('能源采购组已提交'); + } + }); + }, [energy]); + var handleEnergyRevoke = useCallback(function () { + Modal.confirm({ + title: '撤回确认', + content: '是否确认撤回?', + okText: '确认', + cancelText: '取消', + onOk: function () { + setEnergyMeta(function (m) { var nm = {}; for (var k in m) nm[k] = m[k]; nm.status = '待提交'; return nm; }); + message.success('能源采购组已撤回,可重新编辑并保存/提交'); + } + }); + }, []); + + // 运维部 + var operationFixedItems = ['清洗费', '未结算保养', '未结算维修', '车损费用', '工具损坏丢失费用', '证件丢失费用', '广告损坏丢失费用', '送车服务费', '接车服务费', '轮胎磨损费用']; + var operationRowsState = useState( + operationFixedItems.map(function (name, i) { + return { key: 'op-' + i, seq: i + 1, feeItem: name, amount: '', worryFreeDiscount: '', remark: '', lastUpdateTime: '', photos: [], attachments: [], fixed: true }; + }) + ); + var operationRows = operationRowsState[0]; + var setOperationRows = operationRowsState[1]; + var operationMetaState = useState({ submitBy: '', status: '待提交' }); + var operationMeta = operationMetaState[0]; + var setOperationMeta = operationMetaState[1]; + var operationCollapsedState = useState(false); + + function recalcOperationTotal(list) { + var sum = 0; + (list || []).forEach(function (r) { sum += (parseFloat(r.amount) || 0); }); + return sum.toFixed(2); + } + + var operationTotalComputed = useMemo(function () { + return recalcOperationTotal(operationRows || []); + }, [operationRows]); + + // 轮胎胎纹明细(原型 mock:10个轮胎 + 1个备胎,共11条) + var tireTreadList = useMemo(function () { + var unit = 25; // 元/mm + var rows = [ + { key: 't1', name: '左前轮', deliveryDepth: '6.8', returnDepth: '6.2' }, + { key: 't2', name: '左后轮(内)', deliveryDepth: '6.7', returnDepth: '6.0' }, + { key: 't3', name: '左后轮(外)', deliveryDepth: '6.6', returnDepth: '6.1' }, + { key: 't4', name: '右前轮', deliveryDepth: '6.9', returnDepth: '6.4' }, + { key: 't5', name: '右后轮(内)', deliveryDepth: '6.5', returnDepth: '6.0' }, + { key: 't6', name: '右后轮(外)', deliveryDepth: '6.8', returnDepth: '6.3' }, + { key: 't7', name: '左中轮', deliveryDepth: '6.7', returnDepth: '6.2' }, + { key: 't8', name: '右中轮', deliveryDepth: '6.6', returnDepth: '6.1' }, + { key: 't9', name: '左后备位轮', deliveryDepth: '6.9', returnDepth: '6.5' }, + { key: 't10', name: '右后备位轮', deliveryDepth: '6.4', returnDepth: '6.0' }, + { key: 'spare', name: '备胎', deliveryDepth: '7.0', returnDepth: '7.0' } + ]; + var deliverySum = 0; + var returnSum = 0; + rows.forEach(function (r) { deliverySum += (parseFloat(r.deliveryDepth) || 0); returnSum += (parseFloat(r.returnDepth) || 0); }); + var shouldCalc = returnSum <= deliverySum; + return rows.map(function (r) { + var d = parseFloat(r.deliveryDepth) || 0; + var rr = parseFloat(r.returnDepth) || 0; + var diff = shouldCalc ? Math.max(0, d - rr) : 0; + var total = shouldCalc ? (diff * unit) : 0; + return { + key: r.key, + name: r.name, + deliveryDepth: d.toFixed(2), + returnDepth: rr.toFixed(2), + diff: diff.toFixed(2), + unitPrice: unit.toFixed(2), + totalAmount: total.toFixed(2) + }; + }); + }, []); + + var tireWearTotal = useMemo(function () { + var sum = 0; + (tireTreadList || []).forEach(function (r) { sum += (parseFloat(r.totalAmount) || 0); }); + return sum.toFixed(2); + }, [tireTreadList]); + + // 运维部自动反写/计算(原型 mock) + var operationAuto = useMemo(function () { + // 证件丢失费用:行驶证200、牌照300、营运证300、加氢证300 + var missingDocs = ['行驶证', '牌照']; // mock:交车有,还车无 + var map = { '行驶证': 200, '牌照': 300, '营运证': 300, '加氢证': 300 }; + var docFee = missingDocs.reduce(function (s, k) { return s + (map[k] || 0); }, 0); + // 送车/接车服务费 mock + var deliveryFee = 0; + var receiveFee = 0; + // 轮胎磨损费用:按胎纹明细汇总 + return { docLossFee: docFee.toFixed(2), deliveryServiceFee: deliveryFee.toFixed(2), receiveServiceFee: receiveFee.toFixed(2), tireWearFee: String(tireWearTotal || '0.00') }; + }, [tireWearTotal]); + + useEffect(function () { + setOperationRows(function (p) { + var next = p.map(function (r) { + var n = {}; for (var k in r) n[k] = r[k]; + if (n.feeItem === '证件丢失费用') n.amount = operationAuto.docLossFee; + if (n.feeItem === '送车服务费') n.amount = operationAuto.deliveryServiceFee; + if (n.feeItem === '接车服务费') n.amount = operationAuto.receiveServiceFee; + if (n.feeItem === '轮胎磨损费用' && !String(n.amount || '').trim()) n.amount = operationAuto.tireWearFee; + return n; + }); + return next; + }); + }, [operationAuto.docLossFee, operationAuto.deliveryServiceFee, operationAuto.receiveServiceFee, operationAuto.tireWearFee]); + + var handleOperationSave = useCallback(function () { + setOperationMeta(function (m) { var nm = {}; for (var k2 in m) nm[k2] = m[k2]; nm.status = '待提交'; return nm; }); + message.success('运维部已保存'); + }, []); + var handleOperationSubmit = useCallback(function () { + Modal.confirm({ + title: '提交确认', + content: '请确认运维部金额填写无误,点击确认完成提交。', + okText: '确认', + cancelText: '取消', + onOk: function () { + setOperationRows(function (p) { + var now = formatDateTimeNow(); + return p.map(function (r) { var n = {}; for (var k in r) n[k] = r[k]; n.lastUpdateTime = now; return n; }); + }); + setOperationMeta(function (m) { var nm = {}; for (var k in m) nm[k] = m[k]; nm.submitBy = '运维部-王五'; nm.status = '已提交'; return nm; }); + message.success('运维部已提交'); + } + }); + }, [operationRows]); + var handleOperationRevoke = useCallback(function () { + Modal.confirm({ + title: '撤回确认', + content: '是否确认撤回?', + okText: '确认', + cancelText: '取消', + onOk: function () { + setOperationMeta(function (m) { var nm = {}; for (var k in m) nm[k] = m[k]; nm.status = '待提交'; return nm; }); + message.success('运维部已撤回,可重新编辑并保存/提交'); + } + }); + }, []); + var addOperationRow = useCallback(function () { + setOperationRows(function (p) { + var next = p.slice(); + next.push({ key: 'op-new-' + Date.now(), seq: next.length + 1, feeItem: '', amount: '', worryFreeDiscount: '', remark: '', lastUpdateTime: '', photos: [], attachments: [], fixed: false }); + return next.map(function (r, idx) { var o = {}; for (var k in r) o[k] = r[k]; o.seq = idx + 1; return o; }); + }); + }, []); + var removeOperationRow = useCallback(function (key) { + setOperationRows(function (p) { + var next = p.filter(function (r) { return r.key !== key; }); + return next.map(function (r, idx) { var o = {}; for (var k in r) o[k] = r[k]; o.seq = idx + 1; return o; }); + }); + }, []); + var updateOperationRow = useCallback(function (key, field, value) { + setOperationRows(function (p) { + var next = p.map(function (r) { + if (r.key !== key) return r; + var n = {}; for (var k in r) n[k] = r[k]; + n[field] = value; + return n; + }); + return next; + }); + }, []); + + // 安全组(示例数据) + var violationList = useMemo(function () { + return [{ + key: 'w1', + code: 'WZ202602010001', + plateNo: '浙F03218F', + violationBehavior: '测试', + violationTime: '2026-02-01', + penaltyAmount: '100.00', + paymentStatus: '未缴费', + score: '6', + handleStatus: '未处理', + violationCustomer: '上海馨想事…', + violationPhoto: '', + remark: '' + }]; + }, []); + var accidentList = useMemo(function () { + return [{ + key: 'a1', + accidentCode: 'SG202508250001', + plateNo: '京A29256F', + accidentTime: '2025-08-25', + accidentPlace: '北京市大…', + accidentType: '撞固定物', + customerName: '北京海龙…', + ourClaimAmount: '', + theirClaimAmount: '', + responsibility: '全责', + accidentStatus: '未结案', + closeTime: '', + otherFee: '', + remark: '' + }]; + }, []); + var safetyCollapsedState = useState(false); + var safetyMetaState = useState({ submitBy: '', status: '待提交' }); + var safetyMeta = safetyMetaState[0]; + var setSafetyMeta = safetyMetaState[1]; + + var handleSafetySave = useCallback(function () { + setSafetyMeta(function (m) { var nm = {}; for (var k in m) nm[k] = m[k]; nm.submitBy = nm.submitBy || '安全组-赵六'; nm.status = '待提交'; return nm; }); + message.success('安全组已保存'); + }, []); + var handleSafetySubmit = useCallback(function () { + Modal.confirm({ + title: '提交确认', + content: '请确认安全组信息无误,点击确认完成提交。', + okText: '确认', + cancelText: '取消', + onOk: function () { + setSafetyMeta(function (m) { var nm = {}; for (var k in m) nm[k] = m[k]; nm.submitBy = nm.submitBy || '安全组-赵六'; nm.status = '已提交'; return nm; }); + message.success('安全组已提交'); + } + }); + }, []); + var handleSafetyRevoke = useCallback(function () { + Modal.confirm({ + title: '撤回确认', + content: '是否确认撤回?', + okText: '确认', + cancelText: '取消', + onOk: function () { + setSafetyMeta(function (m) { var nm = {}; for (var k in m) nm[k] = m[k]; nm.status = '待提交'; return nm; }); + message.success('安全组已撤回,可重新编辑并保存/提交'); + } + }); + }, []); + + var pendingSettleComputed = useMemo(function () { + var bs = parseFloat(businessServiceTotalComputed) || 0; + var rentRefund = parseFloat((billInfo && billInfo.shouldRefundRent) || '') || 0; + var op = parseFloat(operationTotalComputed) || 0; + var en = (parseFloat(energy.hydrogenSupplement) || 0) + (parseFloat(energy.hydrogenFee) || 0) + (parseFloat(energy.electricFee) || 0) - (parseFloat(energy.prepayRefund) || 0); + return (bs + rentRefund + en + op).toFixed(2); + }, [businessServiceTotalComputed, billInfo.shouldRefundRent, operationTotalComputed, energy.hydrogenSupplement, energy.hydrogenFee, energy.electricFee, energy.prepayRefund]); + + var refundTotalComputed = useMemo(function () { + var deposit = parseFloat(stats.depositAmount) || 0; + var settle = parseFloat(pendingSettleComputed) || 0; + var diff = deposit - settle; + return diff > 0 ? diff.toFixed(2) : '0.00'; + }, [stats.depositAmount, pendingSettleComputed]); + + var payTotalComputed = useMemo(function () { + var deposit = parseFloat(stats.depositAmount) || 0; + var settle = parseFloat(pendingSettleComputed) || 0; + var diff = deposit - settle; + return diff < 0 ? Math.abs(diff).toFixed(2) : '0.00'; + }, [stats.depositAmount, pendingSettleComputed]); + + function buildBreakdownContent(list) { + var headStyle = { padding: '6px 10px', textAlign: 'left', borderBottom: '1px solid #f0f0f0', background: '#fafafa', fontWeight: 600, fontSize: 12 }; + var tdStyle = { padding: '6px 10px', borderBottom: '1px solid #f0f0f0', fontSize: 12 }; + return React.createElement('div', { style: { padding: 0, minWidth: 320 } }, + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse' } }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: headStyle }, '费用项'), + React.createElement('th', { style: Object.assign({}, headStyle, { textAlign: 'right' }) }, '金额(元)') + ) + ), + React.createElement('tbody', null, + (list || []).map(function (r) { + return React.createElement('tr', { key: r.key }, + React.createElement('td', { style: tdStyle }, r.item), + React.createElement('td', { style: Object.assign({}, tdStyle, { fontWeight: r.strong ? 600 : 400, textAlign: 'right' }) }, r.amount) + ); + }) + ) + ) + ); + } + + var settleBreakdown = useMemo(function () { + return [ + { key: 'bs', item: '业务服务组费用项金额总和', amount: businessServiceTotalComputed || '0.00' }, + { key: 'rent', item: '业务服务组-车辆应退租金', amount: (toFixed2(billInfo && billInfo.shouldRefundRent) || '0.00') }, + { key: 'hDiff', item: '能源采购组-氢量差补缴金额', amount: (toFixed2(energy.hydrogenSupplement) || '0.00') }, + { key: 'hFee', item: '能源采购组-氢费补缴金额', amount: (toFixed2(energy.hydrogenFee) || '0.00') }, + { key: 'eFee', item: '能源采购组-电费补缴金额', amount: (toFixed2(energy.electricFee) || '0.00') }, + { key: 'prepay', item: '能源采购组-预付款退费金额(减)', amount: '-' + (toFixed2(energy.prepayRefund) || '0.00') }, + { key: 'op', item: '运维部费用项金额总额', amount: operationTotalComputed || '0.00' }, + { key: 'total', item: '待结算总额', amount: pendingSettleComputed || '0.00', strong: true } + ]; + }, [businessServiceTotalComputed, billInfo.shouldRefundRent, energy.hydrogenSupplement, energy.hydrogenFee, energy.electricFee, energy.prepayRefund, operationTotalComputed, pendingSettleComputed]); + + var refundBreakdown = useMemo(function () { + return [ + { key: 'd', item: '保证金总额', amount: (toFixed2(stats.depositAmount) || '0.00') }, + { key: 's', item: '待结算总额', amount: (pendingSettleComputed || '0.00') }, + { key: 'r', item: '应退还总额', amount: (refundTotalComputed || '0.00'), strong: true } + ]; + }, [stats.depositAmount, pendingSettleComputed, refundTotalComputed]); + +var payBreakdown = useMemo(function () { + return [ + { key: 'd', item: '保证金总额', amount: (toFixed2(stats.depositAmount) || '0.00') }, + { key: 's', item: '待结算总额', amount: (pendingSettleComputed || '0.00') }, + { key: 'p', item: '应补缴总额', amount: (payTotalComputed || '0.00'), strong: true } + ]; + }, [stats.depositAmount, pendingSettleComputed, payTotalComputed]); + +var requirementModalOpenState = useState(false); +var requirementDocContent = useMemo(function () { + return (`还车应结款 +一个「数字化资产ONEOS运管平台」中的「还车应结款」「费用明细」模块 +#面包屑: +1.财务管理-还车应结款-费用明细 +每个模块为一个单独卡片: + +2.还车车辆明细; +2.1.车牌号:显示还车车牌号; +2.2.合同编码:显示还车车辆合同编码; +2.3.项目名称:显示还车车辆对应项目名称; +2.4.客户名称:显示还车车辆对应客户名称; +2.5.交车时间:显示还车车辆完成交车时间; +2.6.还车时间:显示还车车辆完成还车时间; +2.7.易损保:显示还车车辆是否购买易损保,显示为是或否,后方为提示图标,文案为:提供刹车片、灯泡、蓄电池、雨刮等易损件租期内免费更换服务(不含轮胎,只限自行到服务站更换); +2.8.轮胎保:显示还车车辆是否购买轮胎保,显示为是或否,后方为提示图标,文案为:每个租赁年度内提供1次车辆轮胎因自然磨损产生的替换服务; +2.9.养护保:显示还车车辆是否购买养护保,显示为是或否,后方为提示图标,文案为:按厂家保养要求提供定期保养服务; + + +3.还车费用明细: +#上方为还车费用统计数据,包括保证金总额、待结算总额、应退还总额、应补缴总额等相关信息,该部分只有业务管理组能查看; +3.1.保证金总额:显示该车辆保证金金额,格式为xx.xx元; +3.2.待结算总额:显示该车辆待结算金额,格式为xx.xx元,点击以气泡卡片列表显示:费用项、金额。计算方式为:「业务服务部所有费用项-金额总和」+「业务服务部-车辆应退租金」+「能源采购组-氢量差补缴金额」+「能源采购组-氢费补缴金额」+「能源采购组-电费补缴金额」-「能源采购组-预付款退费金额」+「运维部所有费用项-金额总额」; +3.3.应退还总额:显示该车辆应退还金额,格式为xx.xx元,点击以气泡卡片列表显示:费用项、金额。计算方式为:「保证金金额」-「待结算金额」如果是正数,则显示在应退还总额中; +3.4.应补缴总额:显示该车辆应补缴金额,格式为xx.xx元,点击以气泡卡片列表显示:费用项、金额。计算方式为:「保证金金额」-「待结算金额」如果是负数,则显示在应补缴总额中; + +#下方为业务服务组、能源采购组、运维部、安全组4部分,支持展开/收起功能; +4.业务服务组:仅由业务服务组人员进行填写;标题栏标题为业务服务组,后方为:总金额、提交人、状态(待提交、已提交),该部分只有业务管理组能查看; +4.1.总金额:显示所有费用总金额; +4.2.提交人:显示提交人; +4.3.状态:分为待提交(点击保存按钮)、已提交(点击提交按钮); +4.4.保存:点击保存已填写内容,如果已提交,则隐藏保存按钮; +4.5.提交:点击进行二次确认,提示信息:请确认业务服务组金额填写无误,点击确认完成提交。如果已提交,隐藏提交按钮; +4.6.撤回:已提交后,显示撤回按钮。点击进行二次确认,提示信息:是否确认撤回,点击确认,重新显示保存、提交按钮; + +下方为列表,列表字段为:序号、费用项、金额、备注、照片、附件、操作; +4.7.序号:1、2、3....依次类推; +4.8.费用项:固定显示违章处理违约金、保险上浮、ETC-客户未缴费用、ETC卡缺损费、ETC设备缺损费,该部分不能删除;可通过点击下方新增一行,添加新的条目(该部分可删除); + 4.8.1.违章处理违约金: + 4.8.2.保险上浮: + 4.8.3.ETC-客户未缴费用: + 4.8.4.ETC卡缺损费: + 4.8.5.ETC设备缺损费: +4.9.金额:输入框,支持2位小数,后缀为元; +4.10.备注:文本域,支持自定义输入; +4.11.最后更新时间:显示最后更新时间,格式为:YYYY-MM-DD HH:MM; +4.12.照片:照片上传按钮,支持照片上传,支持多个附件; +4.13.附件:附件上传按钮,文案为:上传附件,支持多个附件; +4.14.操作:除固定费用项外,其余操作中为删除; +列表下方为车辆租金:包含本期账单已收租金、车辆实际租金、车辆应退租金; +4.15.本期账单已收租金:输入框(禁用),反写本期账单实际到账租金(从租赁账单中,首期由于未和用友YS打通,先显示为0,对接完成后显示财务实际到账金额); +4.16.车辆实际租金:输入框(可编辑),自动计算车辆实际租金,按照:(车辆月租金/30)*(账单开始日期-还车日期总天数,取整) +4.17.车辆应退租金:输入框(可编辑),根据:本期账单已收租金-车辆实际租金计算并反写; + +5.能源采购组:仅由能源采购组人员进行填写;标题栏标题为能源采购组,后方为总金额、提交人、状态(待提交、已提交),该部分只有能源采购组能查看; +5.1.总金额:显示所有费用总金额; +5.2.提交人:显示提交人; +5.3.状态:分为待提交(点击保存按钮)、已提交(点击提交按钮); +5.4.保存:点击保存已填写内容,如果已提交,则隐藏保存按钮; +5.5.提交:点击进行二次确认,提示信息:请确认业务服务组金额填写无误,点击确认完成提交。如果已提交,隐藏提交按钮; +5.6.撤回:已提交后,显示撤回按钮。点击进行二次确认,提示信息:是否确认撤回,点击确认,重新显示保存、提交按钮; +下方为填写表单: +5.7.氢量差补缴金额:输入框,支持2位小数,后缀为元,如果交车氢量>还车氢量时,根据“差额*退还车氢气单价”自动计算。 +5.8.交车氢量:格式为:xx.xxMPa,保留2位小数,从该车辆交车在该合同交车时间氢量获取; +5.9.还车氢量:格式为:xx.xxMPa,保留2位小数,从该车辆交车在该合同还车时间氢量获取; +5.10.退还车氢气单价:xx.xx元,从车辆租赁合同中获取; +5.11.能源费补缴金额:右侧为2个输入框,分别为氢费补缴金额、电费补缴金额,支持2位小数,后缀为元; +5.12.预付款退费金额:输入框,支持2位小数,后缀为元;输入框下方显示项目预充值余额; + +6.运维部:仅由运维部人员进行填写;标题栏标题为运维部,后方为总金额、提交人、状态(待提交、已提交),该部分只有运维部能查看; +6.1.总金额:显示运维部所有费用项费用金额总额; +6.2.提交人:显示提交人; +6.3.状态:分为待提交(点击保存按钮)、已提交(点击提交按钮); +6.4.保存:点击保存已填写内容,如果已提交,则隐藏保存按钮; +6.5.提交:点击进行二次确认,提示信息:请确认业务服务组金额填写无误,点击确认完成提交。如果已提交,隐藏提交按钮; +6.6.撤回:已提交后,显示撤回按钮。点击进行二次确认,提示信息:是否确认撤回,点击确认,重新显示保存、提交按钮; +下方为列表: +6.7.序号:1、2、3....依次类推; +6.8.费用项:固定显示项为: + 6.8.1.清洗费:输入框手动填写; + 6.8.2.未结算保养:输入框手动填写; + 6.8.3.未结算维修:输入框手动填写; + 6.8.4.车损费用:输入框手动填写; + 6.8.5.工具损坏丢失费用:输入框手动填写; + 6.8.6.证件丢失费用:输入框,自动计算金额,交车时做备车检查时有,还车时无得证件自动计算费用,计算标准为:行驶证:200元,牌照:300元,营运证:300元,加氢证:300元;如所有证件无丢失,或交车时就为无则不进行计算; + 6.8.7.广告损坏丢失费用:输入框手动填写; + 6.8.8.送车服务费:输入框(禁用),反写交车时送车服务费,如无则显示为0; + 6.8.9.接车服务费:输入框(禁用),反写还车时接车服务费,如无则显示为0; + 6.8.10.轮胎磨损费用:输入框(可编辑),轮胎磨损费用(元/mm)*(交车时所有轮胎及备胎胎纹深度-还车时所有轮胎及备胎胎纹深度)自动计算,此外如还车胎纹总额大于交车胎纹总额则不进行计算,此外轮胎磨损费用标题后增加说明图标,悬浮图标显示气泡卡片:卡片内为列表,标题为:轮胎名称、交车胎纹深度、还车胎纹深度、胎纹差、单价、总金额,每一行为一个单独的轮胎(包括备胎); + 该部分不能删除;可通过点击下方新增一行,添加新的条目(该部分可删除); + +6.9.金额:输入框,支持2位小数,后缀为元; +6.10.无忧包减免:输入框,支持2位小数,后缀为元。仅有三个费用项后有输入框,用户未购买易损保时,未结算维修费用后该输入框禁用,用户未购买轮胎保时间,轮胎磨损费用后该输入框为禁用,用户未购买养护保时,未结算保养后该输入框为禁用; +6.11.备注:文本域,支持自定义输入; +6.12.照片:照片上传按钮,支持照片上传; +6.13.附件:附件上传按钮,文案为:上传附件; +6.14.操作:除固定费用项外,其余操作中为删除; + +7.安全组:仅由安全组人员进行确认提交;标题栏标题为安全组,后方为提交人、状态(待提交、已提交),该部分只有安全组能查看; +7.1.提交人:显示提交人; +7.2.状态:分为待提交(点击保存按钮)、已提交(点击提交按钮); +7.3.保存:点击保存已填写内容,如果已提交,则隐藏保存按钮; +7.4.提交:点击进行二次确认,提示信息:请确认业务服务组金额填写无误,点击确认完成提交。如果已提交,隐藏提交按钮; +7.5.撤回:已提交后,显示撤回按钮。点击进行二次确认,提示信息:是否确认撤回,点击确认,重新显示保存、提交按钮; +7.6.违章清单:违章编码、车牌号、违法行为、违法时间、罚款金额、缴费状态、计分值、是否处理、违章客户、违章照片、备注; +7.7.事故清单:列表显示:事故编码、车牌号、事故时间、事故地点、事故类型、客户名称、我方定损金额、对方定损金额、责任划分、事故状态、结案时间、其他费用、备注; + +8.底部为提交、取消按钮; + 8.1.提交审核:还车应结款生成后有15天时间倒计时,倒计时结束并且业务服务组、能源采购组、运维部、安全部均提交后才启用,平时禁用,倒计时显示在按钮上,格式为:x天x小时后可提交审核; + 8.2.取消:点击返回还车应结款列表页; +`); +}, []); + + // 车辆明细表列 + var vehicleColumns = useMemo(function () { + return [ + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 110 }, + { title: '合同编码', dataIndex: 'contractCode', key: 'contractCode', width: 160, ellipsis: true }, + { title: '项目名称', dataIndex: 'projectName', key: 'projectName', width: 140, ellipsis: true }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 140, ellipsis: true }, + { title: '交车时间', dataIndex: 'deliveryTime', key: 'deliveryTime', width: 160, render: function (v) { return fmtYMDHM(v); } }, + { title: '还车时间', dataIndex: 'returnTime', key: 'returnTime', width: 160, render: function (v) { return fmtYMDHM(v); } }, + { + title: '易损保', dataIndex: 'fragileInsurance', key: 'fragileInsurance', width: 110, + render: function (v) { + return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 6 } }, + (v || '-'), + React.createElement(Tooltip, { title: '提供刹车片、灯泡、蓄电池、雨刮等易损件租期内免费更换服务(不含轮胎,只限自行到服务站更换)' }, + React.createElement('span', { + style: { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + width: 16, + height: 16, + borderRadius: 999, + border: '1px solid #d9d9d9', + color: '#999', + fontSize: 12, + lineHeight: '16px', + cursor: 'help', + userSelect: 'none' + } + }, 'i') + ) + ); + } + }, + { + title: '轮胎保', dataIndex: 'tireInsurance', key: 'tireInsurance', width: 110, + render: function (v) { + return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 6 } }, + (v || '-'), + React.createElement(Tooltip, { title: '每个租赁年度内提供1次车辆轮胎因自然磨损产生的替换服务' }, + React.createElement('span', { + style: { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + width: 16, + height: 16, + borderRadius: 999, + border: '1px solid #d9d9d9', + color: '#999', + fontSize: 12, + lineHeight: '16px', + cursor: 'help', + userSelect: 'none' + } + }, 'i') + ) + ); + } + }, + { + title: '养护保', dataIndex: 'maintenanceInsurance', key: 'maintenanceInsurance', width: 110, + render: function (v) { + return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 6 } }, + (v || '-'), + React.createElement(Tooltip, { title: '按厂家保养要求提供定期保养服务' }, + React.createElement('span', { + style: { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + width: 16, + height: 16, + borderRadius: 999, + border: '1px solid #d9d9d9', + color: '#999', + fontSize: 12, + lineHeight: '16px', + cursor: 'help', + userSelect: 'none' + } + }, 'i') + ) + ); + } + } + ]; + }, []); + + var latestRefs = useRef({}); + latestRefs.current = { + businessServiceMeta: businessServiceMeta, + operationMeta: operationMeta, + insuranceFlags: insuranceFlags, + tireTreadList: tireTreadList, + updateBusinessServiceRow: updateBusinessServiceRow, + removeBusinessServiceRow: removeBusinessServiceRow, + updateOperationRow: updateOperationRow, + removeOperationRow: removeOperationRow + }; + + var businessServiceColumns = useMemo(function () { + return [ + { title: '序号', dataIndex: 'seq', key: 'seq', width: 60 }, + { + title: '费用项', dataIndex: 'feeItem', key: 'feeItem', width: 200, + render: function (v, r) { + var ref = latestRefs.current || {}; + var meta = ref.businessServiceMeta || {}; + var readOnly = meta.status === '已提交'; + return r.fixed ? (v || '-') : React.createElement(Input, { value: v, onChange: function (e) { ref.updateBusinessServiceRow(r.key, 'feeItem', e.target.value); }, placeholder: '费用项', disabled: readOnly }); + } + }, + { + title: RequiredLabel('金额'), dataIndex: 'amount', key: 'amount', width: 140, + render: function (v, r) { + var ref = latestRefs.current || {}; + var meta = ref.businessServiceMeta || {}; + var readOnly = meta.status === '已提交'; + return React.createElement(Input, { value: v, onChange: function (e) { ref.updateBusinessServiceRow(r.key, 'amount', e.target.value); }, placeholder: '0.00', addonAfter: '元', disabled: readOnly }); + } + }, + { + title: '备注', dataIndex: 'remark', key: 'remark', + render: function (v, r) { + var ref = latestRefs.current || {}; + var meta = ref.businessServiceMeta || {}; + var readOnly = meta.status === '已提交'; + return React.createElement(TextArea, { value: v, onChange: function (e) { ref.updateBusinessServiceRow(r.key, 'remark', e.target.value); }, placeholder: '备注', rows: 1, disabled: readOnly }); + } + }, + { title: '最后更新时间', dataIndex: 'lastUpdateTime', key: 'lastUpdateTime', width: 150, render: function (v) { return React.createElement('span', { style: { fontSize: 12, color: '#666' } }, v || '-'); } }, + { title: '照片', key: 'photo', width: 100, render: function () { var ref = latestRefs.current || {}; var meta = ref.businessServiceMeta || {}; var readOnly = meta.status === '已提交'; return React.createElement(Button, { size: 'small', disabled: readOnly, onClick: function () { message.info('照片上传(原型)'); } }, '上传'); } }, + { title: '附件', key: 'attachment', width: 110, render: function () { var ref = latestRefs.current || {}; var meta = ref.businessServiceMeta || {}; var readOnly = meta.status === '已提交'; return React.createElement(Button, { size: 'small', disabled: readOnly, onClick: function () { message.info('上传附件(原型)'); } }, '上传附件'); } }, + { + title: '操作', key: 'action', width: 80, + render: function (_, r) { + var ref = latestRefs.current || {}; + var meta = ref.businessServiceMeta || {}; + if (r.fixed || meta.status === '已提交') return null; + return React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function () { ref.removeBusinessServiceRow(r.key); } }, '删除'); + } + } + ]; + }, []); + + var operationColumns = useMemo(function () { + return [ + { title: '序号', dataIndex: 'seq', key: 'seq', width: 60 }, + { + title: '费用项', dataIndex: 'feeItem', key: 'feeItem', width: 200, + render: function (v, r) { + var ref = latestRefs.current || {}; + var meta = ref.operationMeta || {}; + var readOnly = meta.status === '已提交'; + if (r.fixed) { + if (v === '轮胎磨损费用') { + var popContent = React.createElement('div', { style: { width: 760, maxWidth: '80vw' } }, + React.createElement('div', { style: { fontWeight: 600, marginBottom: 8 } }, '轮胎磨损费用明细'), + React.createElement(Table, { + rowKey: 'key', + size: 'small', + bordered: true, + pagination: false, + dataSource: ref.tireTreadList, + columns: [ + { title: '轮胎名称', dataIndex: 'name', key: 'name', width: 120, ellipsis: true }, + { title: '交车胎纹深度', dataIndex: 'deliveryDepth', key: 'deliveryDepth', width: 110, render: function (vv) { return (vv || '0.00') + 'mm'; } }, + { title: '还车胎纹深度', dataIndex: 'returnDepth', key: 'returnDepth', width: 110, render: function (vv) { return (vv || '0.00') + 'mm'; } }, + { title: '胎纹差', dataIndex: 'diff', key: 'diff', width: 80, render: function (vv) { return (vv || '0.00') + 'mm'; } }, + { title: '单价', dataIndex: 'unitPrice', key: 'unitPrice', width: 90, render: function (vv) { return (vv || '0.00') + '元/mm'; } }, + { title: '总金额', dataIndex: 'totalAmount', key: 'totalAmount', width: 90, render: function (vv) { return (vv || '0.00') + '元'; } } + ] + }) + ); + return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 6 } }, + (v || '-'), + React.createElement(Popover, { content: popContent, trigger: 'hover', placement: 'topLeft' }, + React.createElement('span', { + style: { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + width: 16, + height: 16, + borderRadius: 999, + border: '1px solid #d9d9d9', + color: '#999', + fontSize: 12, + lineHeight: '16px', + cursor: 'help', + userSelect: 'none' + } + }, 'i') + ) + ); + } + return (v || '-'); + } + return React.createElement(Input, { value: v, onChange: function (e) { ref.updateOperationRow(r.key, 'feeItem', e.target.value); }, placeholder: '费用项', disabled: readOnly }); + } + }, + { + title: RequiredLabel('金额'), dataIndex: 'amount', key: 'amount', width: 140, + render: function (v, r) { + var ref = latestRefs.current || {}; + var meta = ref.operationMeta || {}; + var readOnly = meta.status === '已提交'; + var fee = r && r.feeItem; + var disabled = readOnly; + if (fee === '送车服务费' || fee === '接车服务费' || fee === '证件丢失费用') disabled = true; + return React.createElement(Input, { value: v, onChange: function (e) { ref.updateOperationRow(r.key, 'amount', e.target.value); }, placeholder: '0.00', addonAfter: '元', disabled: disabled }); + } + }, + { + title: React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 6 } }, + '无忧包减免', + React.createElement(Tooltip, { title: '无忧包减免不会列入运维成本,而是计入业务成本' }, + React.createElement('span', { + style: { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + width: 16, + height: 16, + borderRadius: 999, + border: '1px solid #d9d9d9', + color: '#999', + fontSize: 12, + lineHeight: '16px', + cursor: 'help', + userSelect: 'none' + } + }, 'i') + ) + ), + dataIndex: 'worryFreeDiscount', + key: 'worryFreeDiscount', + width: 140, + render: function (v, r) { + var ref = latestRefs.current || {}; + var meta = ref.operationMeta || {}; + var fee = r && r.feeItem; + var enableFor = (fee === '未结算维修' || fee === '轮胎磨损费用' || fee === '未结算保养'); + if (!enableFor) return React.createElement('span', { style: { color: '#999' } }, '-'); + var readOnly = meta.status === '已提交'; + var disabled = false; + var ins = ref.insuranceFlags || {}; + if (fee === '未结算维修' && !ins.hasFragileInsurance) disabled = true; + if (fee === '轮胎磨损费用' && !ins.hasTireInsurance) disabled = true; + if (fee === '未结算保养' && !ins.hasMaintenanceInsurance) disabled = true; + return React.createElement(Input, { value: v, onChange: function (e) { ref.updateOperationRow(r.key, 'worryFreeDiscount', e.target.value); }, placeholder: '0.00', addonAfter: '元', disabled: disabled || readOnly }); + } + }, + { + title: '备注', dataIndex: 'remark', key: 'remark', + render: function (v, r) { + var ref = latestRefs.current || {}; + var meta = ref.operationMeta || {}; + var readOnly = meta.status === '已提交'; + return React.createElement(TextArea, { value: v, onChange: function (e) { ref.updateOperationRow(r.key, 'remark', e.target.value); }, placeholder: '备注', rows: 1, disabled: readOnly }); + } + }, + { title: '最后更新时间', dataIndex: 'lastUpdateTime', key: 'lastUpdateTime', width: 150, render: function (v) { return React.createElement('span', { style: { fontSize: 12, color: '#666' } }, v || '-'); } }, + { title: '照片', key: 'photo', width: 100, render: function () { return React.createElement(Button, { size: 'small', onClick: function () { message.info('照片上传(原型)'); } }, '上传'); } }, + { title: '附件', key: 'attachment', width: 110, render: function () { return React.createElement(Button, { size: 'small', onClick: function () { message.info('上传附件(原型)'); } }, '上传附件'); } }, + { + title: '操作', key: 'action', width: 80, + render: function (_, r) { + var ref = latestRefs.current || {}; + var meta = ref.operationMeta || {}; + if (r.fixed || meta.status === '已提交') return null; + return React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function () { ref.removeOperationRow(r.key); } }, '删除'); + } + } + ]; + }, []); + + // 提交审核倒计时(原型:生成后 15 天) + var generatedAt = useMemo(function () { return new Date(Date.now() - 3 * 24 * 60 * 60 * 1000); }, []); + var nowTickState = useState(Date.now()); + useEffect(function () { + var t = setInterval(function () { nowTickState[1](Date.now()); }, 60 * 1000); + return function () { clearInterval(t); }; + }, []); + + function remainingText() { + var end = generatedAt.getTime() + 15 * 24 * 60 * 60 * 1000; + var left = end - nowTickState[0]; + if (left <= 0) return '0天0小时后可提交审核'; + var d = Math.floor(left / (24 * 60 * 60 * 1000)); + var h = Math.floor((left % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000)); + return d + '天' + h + '小时后可提交审核'; + } + + function canSubmitReview() { + var end = generatedAt.getTime() + 15 * 24 * 60 * 60 * 1000; + var timeOk = (nowTickState[0] >= end); + var groupOk = businessServiceMeta.status === '已提交' && energyMeta.status === '已提交' && operationMeta.status === '已提交' && safetyMeta.status === '已提交'; + return timeOk && groupOk; + } + + function handleSubmit() { + function isValidMoney(s) { + if (s === null || s === undefined) return false; + var v = String(s).trim(); + if (!v) return false; + return !isNaN(parseFloat(v)); + } + + // 业务服务组:每行“金额”必填 + for (var i = 0; i < (businessServiceRows || []).length; i++) { + var r1 = businessServiceRows[i]; + if (!isValidMoney(r1.amount)) { + message.error('请填写业务服务组第' + (r1.seq || (i + 1)) + '行金额'); + return; + } + } + if (!isValidMoney(billInfo.actualRent)) { message.error('请填写车辆实际租金'); return; } + if (!isValidMoney(billInfo.shouldRefundRent)) { message.error('请填写车辆应退租金'); return; } + + // 能源采购组:氢量差/氢费/电费/预付款退费 必填 + if (!isValidMoney(energy.hydrogenSupplement)) { message.error('请填写氢量差补缴金额'); return; } + if (!isValidMoney(energy.hydrogenFee)) { message.error('请填写氢费补缴金额'); return; } + if (!isValidMoney(energy.electricFee)) { message.error('请填写电费补缴金额'); return; } + if (!isValidMoney(energy.prepayRefund)) { message.error('请填写预付款退费金额'); return; } + + // 运维部:每行“金额”必填;无忧包减免仅对 3 个费用项必填(即显示输入框的行) + for (var j = 0; j < (operationRows || []).length; j++) { + var r2 = operationRows[j]; + if (!isValidMoney(r2.amount)) { + message.error('请填写运维部第' + (r2.seq || (j + 1)) + '行金额'); + return; + } + var fee = r2 && r2.feeItem; + var hasDiscountInput = (fee === '未结算维修' || fee === '轮胎磨损费用' || fee === '未结算保养'); + if (hasDiscountInput && !isValidMoney(r2.worryFreeDiscount)) { + message.error('请填写运维部“' + fee + '”的无忧包减免金额'); + return; + } + } + + if (!canSubmitReview()) { + message.error('未满足提交审核条件(倒计时结束且四组均已提交)'); + return; + } + message.success('提交审核成功(原型)'); + } + function handleCancel() { + message.info('返回还车应结款列表页(原型)'); + } + +return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + React.createElement(Breadcrumb, { items: [{ title: '财务管理' }, { title: '还车应结款' }] }), + React.createElement(Button, { type: 'link', onClick: function () { requirementModalOpenState[1](true); } }, '查看需求说明') + ), + + React.createElement(Card, { title: '还车车辆明细', style: cardStyle }, + React.createElement(Table, { rowKey: 'key', columns: vehicleColumns, dataSource: vehicleDetail, pagination: false, bordered: true, size: 'middle', scroll: { x: 980 } }) + ), + + React.createElement(Card, { title: '还车费用明细', style: cardStyle }, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 16, marginBottom: 16 } }, + React.createElement('div', { style: { background: '#fff', border: '1px solid #f0f0f0', borderRadius: 8, padding: '18px 16px', boxShadow: '0 1px 2px rgba(0,0,0,0.04)' } }, + React.createElement('div', { style: { fontSize: 13, color: '#666', marginBottom: 12 } }, '保证金总额'), + React.createElement('div', { style: { fontSize: 22, fontWeight: 600, color: '#333' } }, (stats.depositAmount || '0.00'), React.createElement('span', { style: { fontSize: 14, fontWeight: 500, marginLeft: 4 } }, '元')) + ), + React.createElement('div', { style: { background: '#fff', border: '1px solid #f0f0f0', borderRadius: 8, padding: '18px 16px', boxShadow: '0 1px 2px rgba(0,0,0,0.04)' } }, + React.createElement('div', { style: { fontSize: 13, color: '#666', marginBottom: 12 } }, '待结算总额'), + React.createElement(Popover, { trigger: 'click', placement: 'bottomLeft', content: buildBreakdownContent(settleBreakdown) }, + React.createElement('div', { style: { fontSize: 22, fontWeight: 600, color: '#fa8c16', cursor: 'pointer' } }, + (pendingSettleComputed || '0.00'), + React.createElement('span', { style: { fontSize: 14, fontWeight: 500, marginLeft: 4, color: '#333' } }, '元') + ) + ) + ), + React.createElement('div', { style: { background: '#fff', border: '1px solid #f0f0f0', borderRadius: 8, padding: '18px 16px', boxShadow: '0 1px 2px rgba(0,0,0,0.04)' } }, + React.createElement('div', { style: { fontSize: 13, color: '#666', marginBottom: 12 } }, '应退还总额'), + React.createElement(Popover, { trigger: 'click', placement: 'bottomLeft', content: buildBreakdownContent(refundBreakdown) }, + React.createElement('div', { style: { fontSize: 22, fontWeight: 600, color: '#52c41a', cursor: 'pointer' } }, + (refundTotalComputed || '0.00'), + React.createElement('span', { style: { fontSize: 14, fontWeight: 500, marginLeft: 4, color: '#333' } }, '元') + ) + ) + ), + React.createElement('div', { style: { background: '#fff', border: '1px solid #f0f0f0', borderRadius: 8, padding: '18px 16px', boxShadow: '0 1px 2px rgba(0,0,0,0.04)' } }, + React.createElement('div', { style: { fontSize: 13, color: '#666', marginBottom: 12 } }, '应补缴总额'), + React.createElement(Popover, { trigger: 'click', placement: 'bottomLeft', content: buildBreakdownContent(payBreakdown) }, + React.createElement('div', { style: { fontSize: 22, fontWeight: 600, color: '#f5222d', cursor: 'pointer' } }, + (payTotalComputed || '0.00'), + React.createElement('span', { style: { fontSize: 14, fontWeight: 500, marginLeft: 4, color: '#333' } }, '元') + ) + ) + ) + ), + + React.createElement(CollapseSection, { + collapsed: businessServiceCollapsedState[0], + setCollapsed: businessServiceCollapsedState[1], + title: '业务服务组', + extra: React.createElement(React.Fragment, null, + React.createElement('span', null, '总金额:', businessServiceTotalComputed, ' 元'), + React.createElement('span', null, '提交人:', businessServiceMeta.submitBy || '-'), + React.createElement('span', null, '状态:', businessServiceMeta.status) + ), + headerActions: businessServiceMeta.status === '已提交' + ? React.createElement(Button, { size: 'small', onClick: handleBusinessServiceRevoke }, '撤回') + : React.createElement(React.Fragment, null, + React.createElement(Button, { size: 'small', onClick: handleBusinessServiceSave }, '保存'), + React.createElement(Button, { size: 'small', type: 'primary', onClick: handleBusinessServiceSubmit }, '提交') + ) + }, + React.createElement(Table, { rowKey: 'key', columns: businessServiceColumns, dataSource: businessServiceRows, pagination: false, bordered: true, size: 'small' }), + React.createElement('div', { style: { marginTop: 12, paddingTop: 12, borderTop: '1px dashed #f0f0f0' } }, + React.createElement('div', { style: { fontWeight: 600, marginBottom: 10 } }, '车辆租金'), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 12 } }, + React.createElement('div', null, + React.createElement('div', { style: { fontSize: 12, color: '#666', marginBottom: 6 } }, '本期账单已收租金'), + React.createElement(Input, { value: billInfo.receivedRent, addonAfter: '元', disabled: true }) + ), + React.createElement('div', null, + React.createElement('div', { style: { fontSize: 12, color: '#666', marginBottom: 6 } }, RequiredLabel('车辆实际租金')), + React.createElement(Input, { + value: billInfo.actualRent, + onChange: function (e) { var v = e.target.value; setBillInfo(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.actualRent = v; n.actualRentManual = true; return n; }); }, + placeholder: '0.00', + addonAfter: '元', + disabled: businessServiceMeta.status === '已提交' + }) + ), + React.createElement('div', null, + React.createElement('div', { style: { fontSize: 12, color: '#666', marginBottom: 6 } }, RequiredLabel('车辆应退租金')), + React.createElement(Input, { + value: billInfo.shouldRefundRent, + onChange: function (e) { var v = e.target.value; setBillInfo(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.shouldRefundRent = v; n.shouldRefundManual = true; return n; }); }, + placeholder: '0.00', + addonAfter: '元', + disabled: businessServiceMeta.status === '已提交' + }) + ) + ) + ), + businessServiceMeta.status === '待提交' + ? React.createElement('div', { style: { marginTop: 8, width: '100%' } }, + React.createElement(Button, { type: 'dashed', size: 'small', onClick: addBusinessServiceRow, block: true, style: { width: '100%' } }, '新增一行') + ) + : null + ), + + React.createElement(CollapseSection, { + collapsed: energyCollapsedState[0], + setCollapsed: energyCollapsedState[1], + title: '能源采购组', + extra: React.createElement(React.Fragment, null, + React.createElement('span', null, '总金额:', energyTotalComputed, ' 元'), + React.createElement('span', null, '提交人:', energyMeta.submitBy || '-'), + React.createElement('span', null, '状态:', energyMeta.status) + ), + headerActions: energyMeta.status === '已提交' + ? React.createElement(Button, { size: 'small', onClick: handleEnergyRevoke }, '撤回') + : React.createElement(React.Fragment, null, + React.createElement(Button, { size: 'small', onClick: handleEnergySave }, '保存'), + React.createElement(Button, { size: 'small', type: 'primary', onClick: handleEnergySubmit }, '提交') + ) + }, + React.createElement('div', null, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 12, marginBottom: 16 } }, + React.createElement('div', null, + React.createElement('div', { style: { marginBottom: 8, fontWeight: 500 } }, RequiredLabel('氢量差补缴金额')), + React.createElement(Input, { + value: energy.hydrogenSupplement, + onChange: function (e) { setEnergy(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.hydrogenSupplement = e.target.value; return n; }); }, + placeholder: '0.00', + addonAfter: '元', + style: { width: '100%' }, + readOnly: (parseFloat(energy.deliveryHydrogen) || 0) > (parseFloat(energy.returnHydrogen) || 0), + disabled: energyMeta.status === '已提交' + }) + ), + React.createElement('div', null, + React.createElement('div', { style: { marginBottom: 8, fontWeight: 500 } }, '交车氢量'), + React.createElement(Input, { value: (toFixed2(energy.deliveryHydrogen) || '0.00'), addonAfter: 'MPa', disabled: true }) + ), + React.createElement('div', null, + React.createElement('div', { style: { marginBottom: 8, fontWeight: 500 } }, '还车氢量'), + React.createElement(Input, { value: (toFixed2(energy.returnHydrogen) || '0.00'), addonAfter: 'MPa', disabled: true }) + ), + React.createElement('div', null, + React.createElement('div', { style: { marginBottom: 8, fontWeight: 500 } }, '退还车氢气单价'), + React.createElement(Input, { value: (toFixed2(energy.hydrogenUnitPrice) || '0.00'), addonAfter: '元', disabled: true }) + ) + ), + + React.createElement('div', null, + React.createElement('div', { style: { marginBottom: 8, fontWeight: 500 } }, RequiredLabel('能源费补缴金额')), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 12, marginBottom: 12 } }, + React.createElement('div', null, React.createElement(Input, { value: energy.hydrogenFee, onChange: function (e) { setEnergy(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.hydrogenFee = e.target.value; return n; }); }, placeholder: '氢费补缴金额', addonAfter: '元', style: { width: '100%' }, disabled: energyMeta.status === '已提交' })), + React.createElement('div', null, React.createElement(Input, { value: energy.electricFee, onChange: function (e) { setEnergy(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.electricFee = e.target.value; return n; }); }, placeholder: '电费补缴金额', addonAfter: '元', style: { width: '100%' }, disabled: energyMeta.status === '已提交' })), + React.createElement('div', null), + React.createElement('div', null) + ) + ), + React.createElement('div', { style: { gridColumn: '1 / -1', borderTop: '1px solid #f0f0f0', paddingTop: 12, marginTop: 4 } }, + React.createElement('div', { style: { marginBottom: 8, fontWeight: 500 } }, RequiredLabel('预付款退费金额')), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 12 } }, + React.createElement('div', null, + React.createElement(Input, { value: energy.prepayRefund, onChange: function (e) { setEnergy(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.prepayRefund = e.target.value; return n; }); }, placeholder: '0.00', addonAfter: '元', style: { width: '100%' }, disabled: energyMeta.status === '已提交' }) + ), + React.createElement('div', null), + React.createElement('div', null), + React.createElement('div', null) + ), + React.createElement('div', { style: { marginTop: 8, fontSize: 12, color: '#666' } }, '项目预充值余额:', energy.userBalance, ' 元') + ) + ), + ), + + React.createElement(CollapseSection, { + collapsed: operationCollapsedState[0], + setCollapsed: operationCollapsedState[1], + title: '运维部', + extra: React.createElement(React.Fragment, null, + React.createElement('span', null, '总金额:', operationTotalComputed, ' 元'), + React.createElement('span', null, '提交人:', operationMeta.submitBy || '-'), + React.createElement('span', null, '状态:', operationMeta.status) + ), + headerActions: operationMeta.status === '已提交' + ? React.createElement(Button, { size: 'small', onClick: handleOperationRevoke }, '撤回') + : React.createElement(React.Fragment, null, + React.createElement(Button, { size: 'small', onClick: handleOperationSave }, '保存'), + React.createElement(Button, { size: 'small', type: 'primary', onClick: handleOperationSubmit }, '提交') + ) + }, + React.createElement(Table, { rowKey: 'key', columns: operationColumns, dataSource: operationRows, pagination: false, bordered: true, size: 'small' }), + operationMeta.status === '待提交' + ? React.createElement('div', { style: { marginTop: 8, width: '100%' } }, + React.createElement(Button, { type: 'dashed', size: 'small', onClick: addOperationRow, block: true, style: { width: '100%' } }, '新增一行') + ) + : null + ), + + React.createElement(CollapseSection, { + collapsed: safetyCollapsedState[0], + setCollapsed: safetyCollapsedState[1], + title: '安全组', + extra: React.createElement(React.Fragment, null, + React.createElement('span', null, '提交人:', safetyMeta.submitBy || '-'), + React.createElement('span', null, '状态:', safetyMeta.status) + ), + headerActions: safetyMeta.status === '已提交' + ? React.createElement(Button, { size: 'small', onClick: handleSafetyRevoke }, '撤回') + : React.createElement(React.Fragment, null, + React.createElement(Button, { size: 'small', onClick: handleSafetySave }, '保存'), + React.createElement(Button, { size: 'small', type: 'primary', onClick: handleSafetySubmit }, '提交') + ) + }, + React.createElement('div', { style: { marginBottom: 12, fontWeight: 600 } }, '违章清单'), + React.createElement(Table, { + rowKey: 'key', + size: 'small', + bordered: true, + pagination: false, + dataSource: violationList, + columns: [ + { title: '违章编码', dataIndex: 'code', key: 'code', width: 130, ellipsis: true }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 110 }, + { title: '违法行为', dataIndex: 'violationBehavior', key: 'violationBehavior', width: 110, ellipsis: true }, + { title: '违法时间', dataIndex: 'violationTime', key: 'violationTime', width: 110 }, + { title: '罚款金额', dataIndex: 'penaltyAmount', key: 'penaltyAmount', width: 90, render: function (v) { return (v || '0.00'); } }, + { title: '缴费状态', dataIndex: 'paymentStatus', key: 'paymentStatus', width: 90 }, + { title: '记分值', dataIndex: 'score', key: 'score', width: 70 }, + { title: '是否处理', dataIndex: 'handleStatus', key: 'handleStatus', width: 90 }, + { title: '违章客户', dataIndex: 'violationCustomer', key: 'violationCustomer', width: 120, ellipsis: true }, + { title: '违章照片', dataIndex: 'violationPhoto', key: 'violationPhoto', width: 90, render: function (v) { return v ? v : ''; } }, + { title: '备注', dataIndex: 'remark', key: 'remark', width: 120, ellipsis: true } + ] + }), + React.createElement('div', { style: { margin: '16px 0 12px', fontWeight: 600 } }, '事故清单'), + React.createElement(Table, { + rowKey: 'key', + size: 'small', + bordered: true, + pagination: false, + dataSource: accidentList, + locale: { emptyText: '暂无事故记录' }, + columns: [ + { title: '事故编码', dataIndex: 'accidentCode', key: 'accidentCode', width: 130, ellipsis: true }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 110 }, + { title: '事故时间', dataIndex: 'accidentTime', key: 'accidentTime', width: 110 }, + { title: '事故地点', dataIndex: 'accidentPlace', key: 'accidentPlace', width: 120, ellipsis: true }, + { title: '事故类型', dataIndex: 'accidentType', key: 'accidentType', width: 110, ellipsis: true }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 120, ellipsis: true }, + { title: '我方定损金额', dataIndex: 'ourClaimAmount', key: 'ourClaimAmount', width: 110, render: function (v) { return v || ''; } }, + { title: '对方定损金额', dataIndex: 'theirClaimAmount', key: 'theirClaimAmount', width: 110, render: function (v) { return v || ''; } }, + { title: '责任划分', dataIndex: 'responsibility', key: 'responsibility', width: 90 }, + { title: '事故状态', dataIndex: 'accidentStatus', key: 'accidentStatus', width: 90 }, + { title: '结案时间', dataIndex: 'closeTime', key: 'closeTime', width: 110 }, + { title: '其它费用', dataIndex: 'otherFee', key: 'otherFee', width: 90, render: function (v) { return v || ''; } }, + { title: '备注', dataIndex: 'remark', key: 'remark', width: 120, ellipsis: true } + ] + }) + ) + ), + + React.createElement('div', { style: footerStyle }, + React.createElement(Button, { type: 'primary', onClick: handleSubmit, disabled: !canSubmitReview() }, canSubmitReview() ? '提交审核' : remainingText()), + React.createElement(Button, { onClick: handleCancel }, '取消') + ), + React.createElement(Modal, { + open: requirementModalOpenState[0], + onCancel: function () { requirementModalOpenState[1](false); }, + onOk: function () { requirementModalOpenState[1](false); }, + title: '需求说明', + width: 860 + }, + React.createElement('div', { style: { maxHeight: '70vh', overflow: 'auto', whiteSpace: 'pre-wrap', lineHeight: 1.7, color: '#333' } }, requirementDocContent) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-finance/pages/04-提车收款单-编辑.jsx b/src/prototypes/oneos-web-finance/pages/04-提车收款单-编辑.jsx new file mode 100644 index 0000000..ceeb2ba --- /dev/null +++ b/src/prototypes/oneos-web-finance/pages/04-提车收款单-编辑.jsx @@ -0,0 +1,538 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 财务管理 - 提车应收款 - 提车收款单-编辑(2026年3月10日版本) + +const Component = function () { + var useState = React.useState; + var useCallback = React.useCallback; + var useMemo = React.useMemo; + var useEffect = React.useEffect; + var useRef = React.useRef; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Input = antd.Input; + var Button = antd.Button; + var Select = antd.Select; + var Table = antd.Table; + var Popover = antd.Popover; + var Tooltip = antd.Tooltip; + var Modal = antd.Modal; + var message = antd.message; + var Option = Select.Option; + + var requirementModalVisible = useState(false); + var setRequirementModalVisible = requirementModalVisible[1]; + var edited = useState(false); + var setEdited = edited[1]; + var submitConfirmVisible = useState(false); + var setSubmitConfirmVisible = submitConfirmVisible[1]; + var cancelConfirmVisible = useState(false); + var setCancelConfirmVisible = cancelConfirmVisible[1]; + var projectCardExpanded = useState(true); + var setProjectCardExpanded = projectCardExpanded[1]; + var receivableCardExpanded = useState(true); + var setReceivableCardExpanded = receivableCardExpanded[1]; + var servicePopoverRowIndex = useState(null); + var setServicePopoverRowIndex = servicePopoverRowIndex[1]; + + // 模拟:项目信息(实际由路由/接口拉取) + var projectInfo = useMemo(function () { + return { + collectCode: 'HT-ZL-2025-001TC0001', + contractCode: 'HT-ZL-2025-001', + contractType: '正式合同', + projectName: '嘉兴氢能示范项目', + customerName: '嘉兴某某物流有限公司', + paymentMethod: '预付', + paymentCycle: '6个月', + contractStart: '2025-01-15', + contractEnd: '2026-01-14', + businessDept: '业务1部', + businessPerson: '张经理' + }; + }, []); + + // 模拟:车辆应收款明细(租赁合同中所有车辆),付款周期 6 个月 => 应收月租金 = 月租金*6 + // 编辑页:默认勾选为“此前保存时已选中的车辆” + var paymentCycleMonths = 6; + var vehicleRows = useState([ + { key: 'v1', index: 1, brand: '东风', model: 'DFH1180', plateNo: '浙A12345', monthRent: 5000, receivableRent: 30000, actualRent: '29800.00', rentRemark: '首期六期一次性付清', receivableDeposit: 10000, serviceItems: [{ name: '代处理费用', receivable: 200, actual: '200.00', discount: '0.00', remark: '' }, { name: '保险上浮', receivable: 500, actual: '480.00', discount: '20.00', remark: '客户协商' }], receivableService: 700, actualService: '680.00', discountAmount: '200.00', discountRemark: '首月优惠', discountProof: [{ name: '优惠审批单.pdf' }], selected: true, receivableCompleted: true }, + { key: 'v2', index: 2, brand: '福田', model: 'BJ1180', plateNo: '浙A23456', monthRent: 4500, receivableRent: 27000, actualRent: '27000.00', rentRemark: '', receivableDeposit: 8000, serviceItems: [{ name: '保养费用', receivable: 300, actual: '300.00', discount: '0.00', remark: '含首保' }], receivableService: 300, actualService: '300.00', discountAmount: '0.00', discountRemark: '', discountProof: [], selected: true, receivableCompleted: true }, + { key: 'v3', index: 3, brand: '重汽', model: 'ZZ1187', plateNo: '浙A34567', monthRent: 5200, receivableRent: 31200, actualRent: '31200.00', rentRemark: '按合同约定', receivableDeposit: 10000, serviceItems: [{ name: '代处理费用', receivable: 180, actual: '180.00', discount: '0.00', remark: '' }, { name: '上牌服务', receivable: 400, actual: '400.00', discount: '0.00', remark: '已含上牌' }], receivableService: 580, actualService: '580.00', discountAmount: '0.00', discountRemark: '', discountProof: [], selected: true, receivableCompleted: true }, + { key: 'v4', index: 4, brand: '陕汽', model: 'SX1313', plateNo: '', monthRent: 4800, receivableRent: 28800, actualRent: '28500.00', rentRemark: '待交车后补全', receivableDeposit: 9000, serviceItems: [{ name: '保险上浮', receivable: 350, actual: '350.00', discount: '0.00', remark: '' }], receivableService: 350, actualService: '350.00', discountAmount: '300.00', discountRemark: '客户协商减免', discountProof: [], selected: true }, + { key: 'v5', index: 5, brand: '解放', model: 'J6P', plateNo: '浙A45678', monthRent: 5500, receivableRent: 33000, actualRent: '33000.00', rentRemark: '', receivableDeposit: 11000, serviceItems: [{ name: '代处理费用', receivable: 220, actual: '220.00', discount: '0.00', remark: '' }, { name: '保养费用', receivable: 280, actual: '280.00', discount: '0.00', remark: '' }], receivableService: 500, actualService: '500.00', discountAmount: '0.00', discountRemark: '', discountProof: [], selected: false }, + { key: 'v6', index: 6, brand: '江淮', model: '格尔发K5', plateNo: '浙A56789', monthRent: 4300, receivableRent: 25800, actualRent: '25800.00', rentRemark: '', receivableDeposit: 8500, serviceItems: [{ name: '保养费用', receivable: 260, actual: '260.00', discount: '0.00', remark: '' }], receivableService: 260, actualService: '260.00', discountAmount: '0.00', discountRemark: '', discountProof: [], selected: false } + ]); + var vehicles = vehicleRows[0]; + var setVehicles = vehicleRows[1]; + + // 氢费预付款(非预付款模式时不显示) + var hasHydrogenPrepay = true; + var hydrogenRow = useState({ + receivable: '3580.00', + actual: '3500.00', + discount: '80.00', + discountRemark: '预付款批量减免' + }); + var hydrogen = hydrogenRow[0]; + var setHydrogen = hydrogenRow[1]; + + var invoiceMethod = useState('先开票后付款'); + var setInvoiceMethod = invoiceMethod[1]; + var invoiceRemark = useState('增值税专用发票,税率13%,开票项目:*现代服务*车辆租赁费;备注:嘉兴氢能示范项目-提车首付款'); + var setInvoiceRemark = invoiceRemark[1]; + + var toggleVehicleSelected = useCallback(function (key) { + setEdited(true); + setVehicles(function (prev) { + return prev.map(function (r) { + if (r.key !== key || r.receivableCompleted) return r; + return Object.assign({}, r, { selected: !r.selected }); + }); + }); + }, []); + + var toggleAllSelected = useCallback(function (checked) { + setEdited(true); + setVehicles(function (prev) { + return prev.map(function (r) { + if (r.receivableCompleted) return r; + return Object.assign({}, r, { selected: !!checked }); + }); + }); + }, []); + + var updateVehicle = useCallback(function (key, field, value) { + setEdited(true); + setVehicles(function (prev) { + return prev.map(function (r) { + if (r.key !== key) return r; + var next = Object.assign({}, r); + next[field] = value; + if (field === 'serviceItems') { + var total = 0; + (value || []).forEach(function (s) { total += parseFloat(s.actual) || 0; }); + next.actualService = total.toFixed(2); + } + return next; + }); + }); + }, []); + + var updateServiceItem = useCallback(function (vehicleKey, itemIndex, field, value) { + setEdited(true); + setVehicles(function (prev) { + return prev.map(function (r) { + if (r.key !== vehicleKey) return r; + var items = (r.serviceItems || []).slice(); + if (!items[itemIndex]) return r; + items[itemIndex] = Object.assign({}, items[itemIndex], { [field]: value }); + var total = 0; + items.forEach(function (s) { total += parseFloat(s.actual) || 0; }); + return Object.assign({}, r, { serviceItems: items, actualService: total.toFixed(2) }); + }); + }); + }, []); + + var totals = useMemo(function () { + // 只计算已选中且未完成提车应收款的车辆 + var selected = vehicles.filter(function (v) { return v.selected && !v.receivableCompleted; }); + var receivableRent = 0, actualRent = 0, receivableDeposit = 0, receivableService = 0, actualService = 0, discountTotal = 0; + selected.forEach(function (v) { + receivableRent += Number(v.receivableRent) || 0; + actualRent += parseFloat(v.actualRent) || 0; + receivableDeposit += Number(v.receivableDeposit) || 0; + receivableService += Number(v.receivableService) || 0; + actualService += parseFloat(v.actualService) || 0; + discountTotal += parseFloat(v.discountAmount) || 0; + }); + return { + receivableRent: receivableRent.toFixed(2), + actualRent: actualRent.toFixed(2), + receivableDeposit: receivableDeposit.toFixed(2), + receivableService: receivableService.toFixed(2), + actualService: actualService.toFixed(2), + discountTotal: discountTotal.toFixed(2) + }; + }, [vehicles]); + + var receivableTotal = useMemo(function () { + return (parseFloat(totals.receivableRent) + parseFloat(totals.receivableDeposit) + parseFloat(totals.receivableService)).toFixed(2); + }, [totals]); + + var actualTotal = useMemo(function () { + return (parseFloat(totals.actualRent) + parseFloat(totals.receivableDeposit) + parseFloat(totals.actualService) - parseFloat(totals.discountTotal)).toFixed(2); + }, [totals]); + + var receivablePopoverContent = useMemo(function () { + return React.createElement('div', { style: { padding: 8, minWidth: 220 } }, + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 13 } }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '项目'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '金额') + ) + ), + React.createElement('tbody', null, + React.createElement('tr', null, React.createElement('td', { style: { padding: '6px 8px' } }, '总计应收车辆月租金'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.receivableRent + ' 元')), + React.createElement('tr', null, React.createElement('td', { style: { padding: '6px 8px' } }, '总计应收车辆保证金'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.receivableDeposit + ' 元')), + React.createElement('tr', null, React.createElement('td', { style: { padding: '6px 8px' } }, '总计应收服务费'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.receivableService + ' 元')) + ) + ) + ); + }, [totals]); + + var actualPopoverContent = useMemo(function () { + return React.createElement('div', { style: { padding: 8, minWidth: 220 } }, + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 13 } }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '项目'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '金额') + ) + ), + React.createElement('tbody', null, + React.createElement('tr', null, React.createElement('td', { style: { padding: '6px 8px' } }, '总计实收车辆月租金'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.actualRent + ' 元')), + React.createElement('tr', null, React.createElement('td', { style: { padding: '6px 8px' } }, '总计应收车辆保证金'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.receivableDeposit + ' 元')), + React.createElement('tr', null, React.createElement('td', { style: { padding: '6px 8px' } }, '总计实收服务费'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.actualService + ' 元')), + React.createElement('tr', null, React.createElement('td', { style: { padding: '6px 8px' } }, '总计减免金额'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.discountTotal + ' 元')) + ) + ) + ); + }, [totals]); + + var selectableVehicles = vehicles.filter(function (v) { return !v.receivableCompleted; }); + var allSelected = selectableVehicles.length > 0 && selectableVehicles.every(function (v) { return v.selected; }); + var indeterminate = selectableVehicles.some(function (v) { return v.selected; }) && !allSelected; + var headerCheckRef = useRef(null); + useEffect(function () { + if (headerCheckRef.current) headerCheckRef.current.indeterminate = indeterminate; + }, [indeterminate]); + + var handleSubmit = useCallback(function () { + setSubmitConfirmVisible(true); + }, []); + + var handleSubmitConfirm = useCallback(function () { + setSubmitConfirmVisible(false); + message.success('提车收款单已提交审核'); + setEdited(false); + if (window.__receivableBack) window.__receivableBack(); + }, []); + + var handleSave = useCallback(function () { + message.success('保存成功'); + setEdited(false); + if (window.__receivableBack) window.__receivableBack(); + }, []); + + var handleCancel = useCallback(function () { + if (edited[0]) setCancelConfirmVisible(true); + else if (window.__receivableBack) window.__receivableBack(); else message.info('返回提车应收款列表(原型)'); + }, [edited[0]]); + + var handleCancelConfirm = useCallback(function () { + setCancelConfirmVisible(false); + if (window.__receivableBack) window.__receivableBack(); else message.info('返回提车应收款列表(原型)'); + }, []); + + var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; + var labelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var requiredStar = { color: '#ff4d4f', marginRight: 2 }; + var formRowStyle = { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px 24px', marginBottom: 16 }; + var formItemStyle = { marginBottom: 12 }; + var highlightStyle = { color: '#1890ff', fontWeight: 600, cursor: 'pointer' }; + var valueStyle = { color: 'rgba(0,0,0,0.85)', fontSize: 14, lineHeight: '22px', minHeight: 22 }; + var thBase = { padding: '10px 12px', border: '1px solid #f0f0f0', whiteSpace: 'nowrap' }; + + var requirementContent = '提车应收款-提车收款单-编辑(2026年3月10日版本)\n一个「数字化资产ONEOS运管平台」中的「财务管理」「提车应收款」「提车收款单」「编辑」模块\n#面包屑:财务管理-提车应收款-提车收款单-编辑;\n\n页面分为3个卡片,业务流程请参考流程图;\n1.项目信息:\n#显示项目详细信息,包括:\n1.1.合同编码:显示该租赁合同对应合同编码;\n1.2.合同类型:显示该租赁合同对应合同类型;\n1.3.项目名称:显示该租赁合同对应项目名称;\n1.4.客户名称:显示该租赁合同对应客户名称;\n1.5.付款方式:显示该租赁合同对应付款方式,分为:预付、后付两种,根据实际合同反写;\n1.6.付款周期:显示该租赁合同对应付款周期,格式为:x个月;\n1.7.合同生效时间:显示该租赁合同对应合同生效时间;\n1.8.合同结束时间:显示该租赁合同对应合同结束时间;\n1.9.业务部门:显示该租赁合同对应业务部门名称;\n1.10.业务负责人:显示该租赁合同对应业务负责人;\n\n2.提车应收款信息:\n#上方显示应收款总额、实收款总额;\n2.1.应收款总额:格式为:xx.xx元,重点色,计算方式为:「选中车辆应收车辆月租金总和」+「选中车辆应收车辆保证金总和」+「选中车辆应收服务费总和」;\n 点击应收款总额金额,弹出气泡卡片,气泡卡片列表展示:项目、金额;\n 2.1.1.项目:包括总计应收车辆月租金、总计应收车辆保证金、总计应收服务费三项;\n 2.1.2.金额:显示该项目对应应收金额;\n2.2.实收款总额:格式为:xx.xx元,重点色,计算方式为:「选中车辆实收车辆月租金总和」+「选中车辆应收车辆保证金总和」+「选中车辆实收服务费总和」-「选中车辆减免金额总和」;\n 点击实收款总额金额,弹出气泡卡片,气泡卡片列表展示:项目、金额;\n 2.2.1.项目:包括总计实收车辆月租金、总计应收车辆保证金、总计实收服务费、总计减免金额四项;\n 2.2.2.金额:显示该项目对应实收金额;\n\n#中间为车辆应收款明细,以列表展示租赁合同中所有车辆明细,包括以下字段:\n2.1.全选/多选:多选按钮组,支持表头点击全选,编辑页面显示此前保存时已选中相关车辆,支持重新选择车辆;\n 提车应收款支持分批次进行收款,如果该车辆之前已提交提车应收款流程,则多选框不能勾选,悬浮多选框时提示:该车辆已完成提车应收款;\n2.2.序号:根据租赁合同中车辆对应序号顺序展示;\n2.3.品牌:显示租赁合同中车辆对应品牌;\n2.4.型号:显示租赁合同中车辆对应型号;\n2.5.车牌号:显示租赁合同中车辆对应车牌号,如果车牌号为空则显示为-(后续如果交车成功,则车牌号会反显在该处);\n2.6.应收车辆月租金:根据租赁合同付款周期自动计算,例如:付款周期为6个月,则这里显示金额为:车辆月租金*6;\n2.7.实收车辆月租金:必填项,输入框,默认反写实收车辆月租金,支持修改,由业务员自行输入车辆月租金金额,精确至2位小数,后缀为元;\n2.8.车辆租金备注:选填项,输入框,由业务员自行输入;\n2.9.减免金额:选填项,输入框,由业务员自行输入减免金额,默认为0.00,支持2位小数,后缀为元;\n2.10.减免金额备注:选填项,输入框,由业务员自行输入备注信息;\n2.11.应收车辆保证金:显示租赁合同中当前车辆保证金金额;\n2.12.服务费项目:点击管理,弹出气泡卡片,气泡卡片标题为服务费项目,下方为列表,显示服务项目、应收费用、实收费用、备注;\n 2.12.1.服务项目:显示租赁合同中所有服务项目名称;\n 2.12.2.应收费用:显示租赁合同中所有服务项目对应费用;\n 2.12.3.实收费用:必填项,输入框,默认反写应收费用,支持修改,由业务员自行输入实收费用金额,精确至2位小数,后缀为元;\n 2.12.4.减免费用:选填项,输入框,默认为:0.00,精确至2位小数,后缀为元;\n 2.12.4.备注:选填项,输入框,由业务员自行输入备注信息;\n2.13.应收服务费:显示当前车辆所有服务费应收费用总和,格式为:xx.xx元;\n2.14.实收服务费:显示当前车辆所有服务费实收费用总和,格式为:xx.xx元;\n2.15.减免金额:选填项,输入框,由业务员自行输入减免金额,默认为0.00,支持2位小数,后缀为元;\n2.16.减免金额备注:选填项,输入框,由业务员自行输入备注信息;\n2.17.减免证明:选填项,附件上传按钮,点击后上传本地文件,支持:jpg、png、pdf等格式,支持多附件上传;\n\n列表不支持分页功能;\n#下方为氢费预付款情况,以列表展示租赁合同中所有氢费预付款明细,包括以下字段,如租赁合同氢费非预付款模式,则不显示该部分内容:\n2.19.氢费预付款应收金额:显示租赁合同中氢费预付款金额,格式为:xx.xx元,支持2位小数;\n2.20.氢费预付款实收金额:必填项,输入框,默认反写氢费预付款应收金额,支持修改,由业务员自行输入金额,格式为:xx.xx元,支持2位小数;\n2.21.减免金额:选填项,输入框,由业务员自行输入减免金额,默认为:0.00元,格式为:xx.xx元,支持2位小数;\n2.22.减免金额备注:选填项,输入框,由业务员自行输入减免金额备注信息;\n#最底部为开票方式、开票备注;\n2.23.开票方式:选择器,选项包括:先开票后付款、先付款后开票,默认为先开票后付款;\n2.24.开票备注:必填项,文本域,默认提示文本为:请输入开票项目、税率以及其他备注信息,财务将以此进行开票;\n\n3.底部为提交审核,保存,取消;\n3.1.提交审核:点击提交审核,进行二次确认,文案为:请仔细核对提车首付款实收金额,点击确认则进入工作流;\n3.2.保存:点击保存,提示保存成功,跳转至提车应收款列表页,同时该条数据审核状态为:待提交:\n3.3.取消:点击取消,进行二次确认,文案为:取消将会丢失所有已填写数据,是否确认,点击确认则跳转至提车应收款列表页;'; + + // 兼容:用于 “服务费项目” 气泡卡片定位 + var activeServiceIndex = servicePopoverRowIndex[0]; + + return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + React.createElement(Breadcrumb, { + items: [ + { title: '财务管理' }, + { title: '提车应收款' }, + { title: '提车收款单-编辑' } + ] + }), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { setRequirementModalVisible(true); } }, '查看需求说明') + ), + React.createElement(Card, { + title: '项目信息', + style: cardStyle, + extra: React.createElement(Button, { type: 'link', size: 'small', onClick: function () { setProjectCardExpanded(!projectCardExpanded[0]); } }, projectCardExpanded[0] ? '收起' : '展开') + }, + projectCardExpanded[0] ? React.createElement('div', { style: formRowStyle }, + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '提车收款单编码'), + React.createElement('div', { style: valueStyle }, projectInfo.collectCode || '-') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '合同编码'), + React.createElement('div', { style: valueStyle }, projectInfo.contractCode || '-') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '合同类型'), + React.createElement('div', { style: valueStyle }, projectInfo.contractType || '-') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '项目名称'), + React.createElement('div', { style: valueStyle }, projectInfo.projectName || '-') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '客户名称'), + React.createElement('div', { style: valueStyle }, projectInfo.customerName || '-') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '付款方式'), + React.createElement('div', { style: valueStyle }, projectInfo.paymentMethod || '-') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '付款周期'), + React.createElement('div', { style: valueStyle }, projectInfo.paymentCycle || '-') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '合同生效时间'), + React.createElement('div', { style: valueStyle }, projectInfo.contractStart || '-') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '合同结束时间'), + React.createElement('div', { style: valueStyle }, projectInfo.contractEnd || '-') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '业务部门'), + React.createElement('div', { style: valueStyle }, projectInfo.businessDept || '-') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '业务负责人'), + React.createElement('div', { style: valueStyle }, projectInfo.businessPerson || '-') + ) + ) : null + ), + React.createElement(Card, { + title: '提车应收款信息', + style: cardStyle, + extra: React.createElement(Button, { type: 'link', size: 'small', onClick: function () { setReceivableCardExpanded(!receivableCardExpanded[0]); } }, receivableCardExpanded[0] ? '收起' : '展开') + }, + receivableCardExpanded[0] ? React.createElement(React.Fragment, null, + React.createElement('div', { style: { marginBottom: 16, display: 'flex', gap: 24, alignItems: 'center' } }, + React.createElement(Popover, { content: receivablePopoverContent, title: '应收款明细', trigger: 'click' }, + React.createElement('span', { style: { cursor: 'pointer' } }, '应收款总额:', React.createElement('span', { style: highlightStyle }, receivableTotal, ' 元')) + ), + React.createElement(Popover, { content: actualPopoverContent, title: '实收款明细', trigger: 'click' }, + React.createElement('span', { style: { cursor: 'pointer' } }, '实收款总额:', React.createElement('span', { style: highlightStyle }, actualTotal, ' 元')) + ) + ), + React.createElement('div', { style: { overflowX: 'auto', marginBottom: 16, overflowY: 'visible' } }, + React.createElement('table', { style: { width: '100%', minWidth: 1600, borderCollapse: 'collapse', fontSize: 13, tableLayout: 'fixed' } }, + React.createElement('thead', null, + React.createElement('tr', { style: { backgroundColor: '#fafafa' } }, + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 48 }) }, + React.createElement('input', { type: 'checkbox', checked: allSelected, ref: headerCheckRef, onChange: function (e) { toggleAllSelected(e.target.checked); } }) + ), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 50 }) }, '序号'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 80 }) }, '品牌'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 90 }) }, '型号'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 100 }) }, '车牌号'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 120 }) }, '应收车辆月租金'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 130 }) }, React.createElement('span', null, React.createElement('span', { style: requiredStar }, '*'), '实收车辆月租金')), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 130 }) }, '车辆租金备注'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 130 }) }, '减免金额'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 130 }) }, '减免金额备注'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 130 }) }, '减免证明'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 110 }) }, '应收车辆保证金'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 90 }) }, '服务费项目'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 90 }) }, '应收服务费'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 130 }) }, '实收服务费') + ) + ), + React.createElement('tbody', null, + vehicles.map(function (row) { + var disabled = !row.selected || row.receivableCompleted; + + var servicePopover = React.createElement('div', { style: { padding: 8, minWidth: 360 } }, + React.createElement('div', { style: { fontWeight: 600, marginBottom: 8 } }, '服务费项目'), + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 12 } }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '服务项目'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '应收费用'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, React.createElement('span', null, React.createElement('span', { style: requiredStar }, '*'), '实收费用')), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '减免费用'), + React.createElement('th', { style: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '备注') + ) + ), + React.createElement('tbody', null, + (row.serviceItems || []).map(function (s, si) { + return React.createElement('tr', { key: si }, + React.createElement('td', { style: { padding: '6px 8px' } }, s.name), + React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, (s.receivable != null ? s.receivable : '') + ' 元'), + React.createElement('td', { style: { padding: '6px 8px' } }, + React.createElement(Input, { size: 'small', value: s.actual, disabled: disabled, suffix: '元', onChange: function (e) { updateServiceItem(row.key, si, 'actual', e.target.value); } }) + ), + React.createElement('td', { style: { padding: '6px 8px' } }, + React.createElement(Input, { size: 'small', value: s.discount, disabled: disabled, suffix: '元', onChange: function (e) { updateServiceItem(row.key, si, 'discount', e.target.value); } }) + ), + React.createElement('td', { style: { padding: '6px 8px' } }, + React.createElement(Input, { size: 'small', value: s.remark, disabled: disabled, placeholder: '备注', onChange: function (e) { updateServiceItem(row.key, si, 'remark', e.target.value); } }) + ) + ); + }) + ) + ) + ); + + var proofList = row.discountProof || []; + var onProofUpload = function (e) { + var files = e.target.files; + if (!files || !files.length) return; + setEdited(true); + var next = proofList.slice(); + for (var i = 0; i < files.length; i++) { next.push({ name: files[i].name }); } + updateVehicle(row.key, 'discountProof', next); + e.target.value = ''; + }; + var removeProof = function (idx) { + setEdited(true); + var next = (row.discountProof || []).slice(); + next.splice(idx, 1); + updateVehicle(row.key, 'discountProof', next); + }; + + var checkboxCell = row.receivableCompleted + ? React.createElement(Tooltip, { title: '该车辆已完成提车应收款' }, + React.createElement('span', { style: { display: 'inline-block', cursor: 'not-allowed' } }, + React.createElement('input', { type: 'checkbox', checked: true, disabled: true }) + ) + ) + : React.createElement('input', { type: 'checkbox', checked: row.selected, onChange: function () { toggleVehicleSelected(row.key); } }); + + return React.createElement('tr', { key: row.key }, + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, checkboxCell), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, row.index), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, row.brand), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, row.model), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, row.plateNo || '-'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, (row.receivableRent || 0) + ' 元'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', width: 130 } }, + React.createElement(Input, { size: 'small', value: row.actualRent, disabled: disabled, suffix: '元', onChange: function (e) { updateVehicle(row.key, 'actualRent', e.target.value); } }) + ), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', width: 130 } }, + React.createElement(Input, { size: 'small', value: row.rentRemark, disabled: disabled, onChange: function (e) { updateVehicle(row.key, 'rentRemark', e.target.value); } }) + ), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', width: 130 } }, + React.createElement(Input, { size: 'small', value: row.discountAmount, disabled: disabled, suffix: '元', onChange: function (e) { updateVehicle(row.key, 'discountAmount', e.target.value); } }) + ), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', width: 130 } }, + React.createElement(Input, { size: 'small', value: row.discountRemark, disabled: disabled, onChange: function (e) { updateVehicle(row.key, 'discountRemark', e.target.value); } }) + ), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', width: 130 } }, + disabled ? '-' : React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } }, + proofList.map(function (p, pidx) { + return React.createElement('div', { key: pidx, style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 12 } }, + React.createElement('span', { style: { flex: 1, overflow: 'hidden', textOverflow: 'ellipsis' } }, p.name || '附件'), + React.createElement(Button, { type: 'link', size: 'small', danger: true, style: { padding: 0, minWidth: 'auto' }, onClick: function () { removeProof(pidx); } }, '删除') + ); + }), + React.createElement('span', null, + React.createElement('input', { type: 'file', multiple: true, accept: '.jpg,.jpeg,.png,.pdf', style: { display: 'none' }, id: 'proof-' + row.key, onChange: onProofUpload }), + React.createElement(Button, { type: 'default', size: 'small', onClick: function () { var el = document.getElementById('proof-' + row.key); if (el) el.click(); } }, '附件上传') + ) + ) + ), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, (row.receivableDeposit || 0) + ' 元'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, + React.createElement(Popover, { + content: servicePopover, + title: null, + trigger: 'click', + open: activeServiceIndex === row.key, + onOpenChange: function (open) { setServicePopoverRowIndex(open ? row.key : null); } + }, + React.createElement(Button, { type: 'link', size: 'small', disabled: disabled }, '管理') + ) + ), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, (row.receivableService || 0) + ' 元'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', textAlign: 'right', width: 130 } }, (row.actualService || '0.00') + ' 元') + ); + }) + ) + ) + ), + hasHydrogenPrepay ? React.createElement('div', { style: { marginBottom: 16 } }, + React.createElement('div', { style: { fontSize: 14, fontWeight: 500, marginBottom: 8 } }, '氢费预付款情况'), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr', gap: 16, maxWidth: 800 } }, + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '氢费预付款应收金额'), + React.createElement(Input, { value: hydrogen.receivable + ' 元', disabled: true }) + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, React.createElement('span', { style: requiredStar }, '*'), '氢费预付款实收金额'), + React.createElement(Input, { value: hydrogen.actual, suffix: '元', onChange: function (e) { setEdited(true); setHydrogen(function (p) { return Object.assign({}, p, { actual: e.target.value }); }); } }) + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '减免金额'), + React.createElement(Input, { value: hydrogen.discount, suffix: '元', onChange: function (e) { setEdited(true); setHydrogen(function (p) { return Object.assign({}, p, { discount: e.target.value }); }); } }) + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '减免金额备注'), + React.createElement(Input, { value: hydrogen.discountRemark, onChange: function (e) { setEdited(true); setHydrogen(function (p) { return Object.assign({}, p, { discountRemark: e.target.value }); }); } }) + ) + ) + ) : null, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px 24px', marginTop: 16 } }, + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '开票方式'), + React.createElement(Select, { style: { width: '100%' }, value: invoiceMethod[0], onChange: function (v) { setInvoiceMethod(v); setEdited(true); }, placeholder: '请选择' }, + React.createElement(Option, { value: '先开票后付款' }, '先开票后付款'), + React.createElement(Option, { value: '先付款后开票' }, '先付款后开票') + ) + ), + React.createElement('div', { style: Object.assign({}, formItemStyle, { gridColumn: '1 / -1' }) }, + React.createElement('div', { style: labelStyle }, React.createElement('span', { style: requiredStar }, '*'), '开票备注'), + React.createElement(Input.TextArea, { value: invoiceRemark[0], onChange: function (e) { setInvoiceRemark(e.target.value); setEdited(true); }, rows: 3, placeholder: '请输入开票项目、税率以及其他备注信息,财务将以此进行开票', style: { width: '100%' } }) + ) + ) + ) : null + ), + React.createElement('div', { style: { display: 'flex', gap: 8, marginTop: 24 } }, + React.createElement(Button, { type: 'primary', onClick: handleSubmit }, '提交审核'), + React.createElement(Button, { onClick: handleSave }, '保存'), + React.createElement(Button, { onClick: handleCancel }, '取消') + ), + React.createElement(Modal, { + title: '确认', + open: submitConfirmVisible[0], + onCancel: function () { setSubmitConfirmVisible(false); }, + onOk: handleSubmitConfirm, + okText: '确认', + cancelText: '取消' + }, React.createElement('div', null, '请仔细核对提车首付款实收金额,点击确认则进入工作流')), + React.createElement(Modal, { + title: '确认取消', + open: cancelConfirmVisible[0], + onCancel: function () { setCancelConfirmVisible(false); }, + onOk: handleCancelConfirm, + okText: '确认', + cancelText: '取消' + }, React.createElement('div', null, '取消将会丢失所有已填写数据,是否确认?')), + React.createElement(Modal, { + title: '需求说明', + open: requirementModalVisible[0], + onCancel: function () { setRequirementModalVisible(false); }, + width: 720, + footer: React.createElement(Button, { onClick: function () { setRequirementModalVisible(false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, React.createElement('div', { style: { padding: '8px 0' } }, + React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6 } }, requirementContent) + )) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-finance/pages/05-提车应收款-查看.jsx b/src/prototypes/oneos-web-finance/pages/05-提车应收款-查看.jsx new file mode 100644 index 0000000..9d80088 --- /dev/null +++ b/src/prototypes/oneos-web-finance/pages/05-提车应收款-查看.jsx @@ -0,0 +1,297 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 财务管理 - 提车应收款-查看(2026年3月10日版本) + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Button = antd.Button; + var Popover = antd.Popover; + var Steps = antd.Steps; + var Tooltip = antd.Tooltip; + var message = antd.message; + + var servicePopoverOpen = useState(null); + + // 模拟:项目信息 + var projectInfo = useMemo(function () { + return { + collectCode: 'HT-ZL-2025-001TC0001', + contractCode: 'HT-ZL-2025-001', + contractType: '正式合同', + projectName: '嘉兴氢能示范项目', + customerName: '嘉兴某某物流有限公司', + paymentMethod: '预付', + paymentCycle: '6个月', + contractStart: '2025-01-15', + contractEnd: '2026-01-14', + businessDept: '业务1部', + businessPerson: '张经理' + }; + }, []); + + // 模拟:本单提车收款单已选车辆及填写内容(只读展示) + var vehicles = useMemo(function () { + return [ + { key: 'v1', index: 1, brand: '东风', model: 'DFH1180', plateNo: '浙A12345', receivableRent: 30000, actualRent: '29800.00', rentRemark: '首期六期一次性付清', receivableDeposit: 10000, serviceItems: [{ name: '代处理费用', receivable: 200, actual: '200.00', discount: '0.00', remark: '' }, { name: '保险上浮', receivable: 500, actual: '480.00', discount: '20.00', remark: '客户协商' }], receivableService: 700, actualService: '680.00', discountAmount: '200.00', discountRemark: '首月优惠', discountProof: [{ name: '优惠审批单.pdf' }] }, + { key: 'v2', index: 2, brand: '福田', model: 'BJ1180', plateNo: '浙A23456', receivableRent: 27000, actualRent: '27000.00', rentRemark: '', receivableDeposit: 8000, serviceItems: [{ name: '保养费用', receivable: 300, actual: '300.00', discount: '0.00', remark: '含首保' }], receivableService: 300, actualService: '300.00', discountAmount: '0.00', discountRemark: '', discountProof: [] }, + { key: 'v3', index: 3, brand: '重汽', model: 'ZZ1187', plateNo: '浙A34567', receivableRent: 31200, actualRent: '31200.00', rentRemark: '按合同约定', receivableDeposit: 10000, serviceItems: [{ name: '代处理费用', receivable: 180, actual: '180.00', discount: '0.00', remark: '' }, { name: '上牌服务', receivable: 400, actual: '400.00', discount: '0.00', remark: '已含上牌' }], receivableService: 580, actualService: '580.00', discountAmount: '0.00', discountRemark: '', discountProof: [] }, + { key: 'v4', index: 4, brand: '陕汽', model: 'SX1313', plateNo: '', receivableRent: 28800, actualRent: '28500.00', rentRemark: '待交车后补全', receivableDeposit: 9000, serviceItems: [{ name: '保险上浮', receivable: 350, actual: '350.00', discount: '0.00', remark: '' }], receivableService: 350, actualService: '350.00', discountAmount: '300.00', discountRemark: '客户协商减免', discountProof: [] } + ]; + }, []); + + // 与提车收款单一致:存在氢费预付款时参与总额;应收/实收金额取值与编辑页 hydrogen 字段一致 + var hasHydrogenPrepay = true; + var hydrogen = useMemo(function () { + return { receivable: '3580.00', actual: '3500.00', discount: '80.00', discountRemark: '预付款批量减免' }; + }, []); + + var totals = useMemo(function () { + var receivableRent = 0, actualRent = 0, receivableDeposit = 0, receivableService = 0, actualService = 0, discountTotal = 0; + vehicles.forEach(function (v) { + receivableRent += Number(v.receivableRent) || 0; + actualRent += parseFloat(v.actualRent) || 0; + receivableDeposit += Number(v.receivableDeposit) || 0; + receivableService += Number(v.receivableService) || 0; + actualService += parseFloat(v.actualService) || 0; + discountTotal += parseFloat(v.discountAmount) || 0; + }); + return { + receivableRent: receivableRent.toFixed(2), + actualRent: actualRent.toFixed(2), + receivableDeposit: receivableDeposit.toFixed(2), + receivableService: receivableService.toFixed(2), + actualService: actualService.toFixed(2), + discountTotal: discountTotal.toFixed(2) + }; + }, [vehicles]); + + var hydrogenReceivable = hasHydrogenPrepay ? (parseFloat(hydrogen.receivable) || 0) : 0; + var hydrogenActual = hasHydrogenPrepay ? (parseFloat(hydrogen.actual) || 0) : 0; + + var receivableTotal = useMemo(function () { + var base = parseFloat(totals.receivableRent) + parseFloat(totals.receivableDeposit) + parseFloat(totals.receivableService); + return (base + hydrogenReceivable).toFixed(2); + }, [totals, hydrogenReceivable]); + + var actualTotal = useMemo(function () { + var base = parseFloat(totals.actualRent) + parseFloat(totals.receivableDeposit) + parseFloat(totals.actualService) - parseFloat(totals.discountTotal); + return (base + hydrogenActual).toFixed(2); + }, [totals, hydrogenActual]); + + var receivablePopoverContent = useMemo(function () { + var rows = [ + React.createElement('tr', { key: 'rent' }, React.createElement('td', { style: { padding: '6px 8px' } }, '总计应收车辆月租金'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.receivableRent + ' 元')), + React.createElement('tr', { key: 'deposit' }, React.createElement('td', { style: { padding: '6px 8px' } }, '总计应收车辆保证金'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.receivableDeposit + ' 元')), + React.createElement('tr', { key: 'service' }, React.createElement('td', { style: { padding: '6px 8px' } }, '总计应收服务费'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.receivableService + ' 元')) + ]; + if (hasHydrogenPrepay) { + rows.push(React.createElement('tr', { key: 'hydrogen' }, React.createElement('td', { style: { padding: '6px 8px' } }, '氢费预充值应收金额'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, (hydrogen.receivable || '0.00') + ' 元'))); + } + return React.createElement('div', { style: { padding: 8, minWidth: 220 } }, + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 13 } }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '项目'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '金额') + ) + ), + React.createElement('tbody', null, rows) + ) + ); + }, [totals, hasHydrogenPrepay, hydrogen.receivable]); + + var actualPopoverContent = useMemo(function () { + var rows = [ + React.createElement('tr', { key: 'rent' }, React.createElement('td', { style: { padding: '6px 8px' } }, '总计实收车辆月租金'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.actualRent + ' 元')), + React.createElement('tr', { key: 'deposit' }, React.createElement('td', { style: { padding: '6px 8px' } }, '总计应收车辆保证金'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.receivableDeposit + ' 元')), + React.createElement('tr', { key: 'service' }, React.createElement('td', { style: { padding: '6px 8px' } }, '总计实收服务费'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.actualService + ' 元')), + React.createElement('tr', { key: 'discount' }, React.createElement('td', { style: { padding: '6px 8px' } }, '总计减免金额'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.discountTotal + ' 元')) + ]; + if (hasHydrogenPrepay) { + rows.push(React.createElement('tr', { key: 'hydrogen' }, React.createElement('td', { style: { padding: '6px 8px' } }, '氢费预充值实收金额'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, (hydrogen.actual || '0.00') + ' 元'))); + } + return React.createElement('div', { style: { padding: 8, minWidth: 220 } }, + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 13 } }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '项目'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '金额') + ) + ), + React.createElement('tbody', null, rows) + ) + ); + }, [totals, hasHydrogenPrepay, hydrogen.actual]); + + // 审批情况:竖向步骤条 + var approvalSteps = useMemo(function () { + return [ + { title: '业务部主管', department: '业务1部', status: '已通过', person: '张经理', approveTime: '2026-02-28 09:30' }, + { title: '财务部', department: '财务部', status: '已通过', person: '李财务', approveTime: '2026-02-28 10:15' }, + { title: '事业部主管', department: '事业部', status: '待审批', person: '-', approveTime: '-' } + ]; + }, []); + + var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; + var labelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var formRowStyle = { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px 24px', marginBottom: 16 }; + var formItemStyle = { marginBottom: 12 }; + var highlightStyle = { color: '#1890ff', fontWeight: 600, cursor: 'pointer' }; + var valueStyle = { color: 'rgba(0,0,0,0.85)', fontSize: 14, lineHeight: '22px', minHeight: 22 }; + var thBase = { padding: '10px 12px', border: '1px solid #f0f0f0', whiteSpace: 'nowrap', backgroundColor: '#fafafa' }; + var tdBase = { padding: '8px 12px', border: '1px solid #f0f0f0', fontSize: 13 }; + + function handleBack() { + if (window.__receivableBack) window.__receivableBack(); + else message.info('返回提车应收款列表(原型)'); + } + + return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { marginBottom: 16 } }, + React.createElement(Breadcrumb, { + items: [ + { title: '财务管理' }, + { title: '提车应收款' }, + { title: '提车收款单' } + ] + }) + ), + React.createElement(Card, { title: '项目信息', style: cardStyle }, + React.createElement('div', { style: formRowStyle }, + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '提车收款单编码'), React.createElement('div', { style: valueStyle }, projectInfo.collectCode || '—')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '合同编码'), React.createElement('div', { style: valueStyle }, projectInfo.contractCode || '—')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '合同类型'), React.createElement('div', { style: valueStyle }, projectInfo.contractType || '—')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '项目名称'), React.createElement('div', { style: valueStyle }, projectInfo.projectName || '—')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '客户名称'), React.createElement('div', { style: valueStyle }, projectInfo.customerName || '—')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '付款方式'), React.createElement('div', { style: valueStyle }, projectInfo.paymentMethod || '—')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '付款周期'), React.createElement('div', { style: valueStyle }, projectInfo.paymentCycle || '—')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '合同生效时间'), React.createElement('div', { style: valueStyle }, projectInfo.contractStart || '—')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '合同结束时间'), React.createElement('div', { style: valueStyle }, projectInfo.contractEnd || '—')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '业务部门'), React.createElement('div', { style: valueStyle }, projectInfo.businessDept || '—')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '业务负责人'), React.createElement('div', { style: valueStyle }, projectInfo.businessPerson || '—')) + ) + ), + React.createElement(Card, { title: '提车应收款信息', style: cardStyle }, + React.createElement(React.Fragment, null, + React.createElement('div', { style: { marginBottom: 16, display: 'flex', gap: 24, alignItems: 'center' } }, + React.createElement(Popover, { content: receivablePopoverContent, title: '应收款明细', trigger: 'click' }, + React.createElement('span', { style: { cursor: 'pointer' } }, '应收款总额:', React.createElement('span', { style: highlightStyle }, receivableTotal, ' 元')) + ), + React.createElement(Popover, { content: actualPopoverContent, title: '实收款明细', trigger: 'click' }, + React.createElement('span', { style: { cursor: 'pointer' } }, '实收款总额:', React.createElement('span', { style: highlightStyle }, actualTotal, ' 元')) + ) + ), + React.createElement('div', { style: { overflowX: 'auto', marginBottom: 0 } }, + React.createElement('table', { style: { width: '100%', minWidth: 1500, borderCollapse: 'collapse', fontSize: 13, tableLayout: 'fixed' } }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: Object.assign({}, thBase, { width: 50 }) }, '序号'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 80 }) }, '品牌'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 90 }) }, '型号'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 100 }) }, '车牌号'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 120 }) }, '应收车辆月租金'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 120 }) }, '实收车辆月租金'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 130 }) }, '车辆租金备注'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 100 }) }, '减免金额'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 120 }) }, '减免金额备注'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 100 }) }, '减免证明'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 110 }) }, '应收车辆保证金'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 90 }) }, '服务费项目'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 90 }) }, '应收服务费'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 100 }) }, '实收服务费') + ) + ), + React.createElement('tbody', null, + vehicles.map(function (row) { + var servicePopover = React.createElement('div', { style: { padding: 8, minWidth: 320 } }, + React.createElement('div', { style: { fontWeight: 600, marginBottom: 8 } }, '服务费项目'), + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 12 } }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '服务项目'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '应收费用'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '实收费用'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '减免费用'), + React.createElement('th', { style: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '备注') + ) + ), + React.createElement('tbody', null, + (row.serviceItems || []).map(function (s, si) { + return React.createElement('tr', { key: si }, + React.createElement('td', { style: { padding: '6px 8px' } }, s.name), + React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, (s.receivable != null ? s.receivable : '') + ' 元'), + React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, (s.actual != null ? s.actual : '') + ' 元'), + React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, (s.discount != null ? s.discount : '0.00') + ' 元'), + React.createElement('td', { style: { padding: '6px 8px' } }, s.remark || '—') + ); + }) + ) + ) + ); + var proofNames = (row.discountProof || []).map(function (p) { return p.name; }).join('、') || '—'; + return React.createElement('tr', { key: row.key }, + React.createElement('td', { style: tdBase }, row.index), + React.createElement('td', { style: tdBase }, row.brand), + React.createElement('td', { style: tdBase }, row.model), + React.createElement('td', { style: tdBase }, row.plateNo || '—'), + React.createElement('td', { style: Object.assign({}, tdBase, { textAlign: 'right' }) }, (row.receivableRent || 0) + ' 元'), + React.createElement('td', { style: tdBase }, (row.actualRent || '0.00') + ' 元'), + React.createElement('td', { style: tdBase }, row.rentRemark || '—'), + React.createElement('td', { style: tdBase }, (row.discountAmount || '0.00') + ' 元'), + React.createElement('td', { style: tdBase }, row.discountRemark || '—'), + React.createElement('td', { style: tdBase }, proofNames), + React.createElement('td', { style: Object.assign({}, tdBase, { textAlign: 'right' }) }, (row.receivableDeposit || 0) + ' 元'), + React.createElement('td', { style: tdBase }, + React.createElement(Popover, { content: servicePopover, title: null, trigger: 'click', open: servicePopoverOpen[0] === row.key, onOpenChange: function (open) { servicePopoverOpen[1](open ? row.key : null); } }, + React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0 } }, '管理') + ) + ), + React.createElement('td', { style: Object.assign({}, tdBase, { textAlign: 'right' }) }, (row.receivableService || 0) + ' 元'), + React.createElement('td', { style: Object.assign({}, tdBase, { textAlign: 'right' }) }, (row.actualService || '0.00') + ' 元') + ); + }) + ) + ) + ), + React.createElement('div', { style: { marginTop: 16, display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr', gap: 16, maxWidth: 800 } }, + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '氢费预付款应收金额'), React.createElement('div', { style: valueStyle }, (hydrogen.receivable || '0.00') + ' 元')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '氢费预付款实收金额'), React.createElement('div', { style: valueStyle }, (hydrogen.actual || '0.00') + ' 元')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '减免金额'), React.createElement('div', { style: valueStyle }, (hydrogen.discount || '0.00') + ' 元')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '减免金额备注'), React.createElement('div', { style: valueStyle }, hydrogen.discountRemark || '—')) + ), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px 24px', marginTop: 16 } }, + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '开票方式'), React.createElement('div', { style: valueStyle }, '先开票后付款')), + React.createElement('div', { style: Object.assign({}, formItemStyle, { gridColumn: '1 / -1' }) }, React.createElement('div', { style: labelStyle }, '开票备注'), React.createElement('div', { style: valueStyle }, '增值税专用发票,税率13%,开票项目:*现代服务*车辆租赁费;备注:嘉兴氢能示范项目-提车首付款')) + ) + ) + ), + React.createElement(Card, { title: '审批情况', style: cardStyle }, + React.createElement(Steps, { + direction: 'vertical', + current: approvalSteps.findIndex(function (s) { return s.status === '待审批'; }) >= 0 ? approvalSteps.findIndex(function (s) { return s.status === '待审批'; }) : approvalSteps.length, + style: { paddingLeft: 8 }, + items: approvalSteps.map(function (step, idx) { + return { + title: step.department || step.title, + description: React.createElement('div', { style: { marginTop: 4, fontSize: 13, color: 'rgba(0,0,0,0.65)' } }, + React.createElement('div', null, '审批状态:', step.status), + React.createElement('div', null, '审批人:', step.person), + React.createElement('div', null, '审批时间:', step.approveTime) + ), + status: step.status === '已通过' ? 'finish' : step.status === '待审批' ? 'wait' : 'process' + }; + }) + }) + ), + React.createElement('div', { style: { marginTop: 24 } }, + React.createElement(Button, { onClick: handleBack }, '返回') + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-finance/pages/06-提车应收款-开票信息.jsx b/src/prototypes/oneos-web-finance/pages/06-提车应收款-开票信息.jsx new file mode 100644 index 0000000..b7ea105 --- /dev/null +++ b/src/prototypes/oneos-web-finance/pages/06-提车应收款-开票信息.jsx @@ -0,0 +1,592 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 财务管理 - 提车应收款 - 开票信息(2026年3月5日版本) + +const Component = function () { + var useState = React.useState; + var useCallback = React.useCallback; + var useMemo = React.useMemo; + var useRef = React.useRef; + var useEffect = React.useEffect; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Input = antd.Input; + var Button = antd.Button; + var Popover = antd.Popover; + var Tooltip = antd.Tooltip; + var Modal = antd.Modal; + var message = antd.message; + var Steps = antd.Steps; + var DatePicker = antd.DatePicker; + + var dayjs = window.dayjs || null; + function nowStr() { + return dayjs && dayjs().format ? dayjs().format('YYYY-MM-DD HH:mm') : (new Date().getFullYear() + '-' + String(new Date().getMonth() + 1).padStart(2, '0') + '-' + String(new Date().getDate()).padStart(2, '0') + ' ' + String(new Date().getHours()).padStart(2, '0') + ':' + String(new Date().getMinutes()).padStart(2, '0')); + } + + var projectCardExpanded = useState(true); + var receivableCardExpanded = useState(true); + var invoiceCardExpanded = useState(true); + var approveCardExpanded = useState(true); + var submitConfirmVisible = useState(false); + var cancelConfirmVisible = useState(false); + var requirementModalVisible = useState(false); + var proofPreviewVisible = useState(false); + var proofPreviewFile = useState(null); + var invoicePreviewVisible = useState(false); + var invoicePreviewFile = useState(null); + + // 开票记录列表:历史记录 submitted=true 不可删,新增行 submitted=false 可删 + var invoiceListState = useState([ + { id: 'hist1', arrivalTime: '2026-03-01 10:00', arrivalAmount: '50000.00', invoiceTime: '2026-03-02 14:30', invoiceFiles: [{ name: '发票1.pdf' }], remark: '首笔到账', invoicePerson: '李财务', submitted: true }, + { id: 'new1', arrivalTime: '', arrivalAmount: '', invoiceTime: nowStr(), invoiceFiles: [], remark: '', invoicePerson: '-', submitted: false } + ]); + var invoiceList = invoiceListState[0]; + var setInvoiceList = invoiceListState[1]; + var nextIdRef = useRef(2); + + var projectInfo = useMemo(function () { + return { + contractCode: 'HT-ZL-2025-001', + contractType: '正式合同', + projectName: '嘉兴氢能示范项目', + customerName: '嘉兴某某物流有限公司', + paymentMethod: '预付', + paymentCycle: '6个月', + contractStart: '2025-01-15', + contractEnd: '2026-01-14', + businessDept: '业务1部', + businessPerson: '张经理' + }; + }, []); + + // 车辆应收款明细:全选默认为取消勾选;已完成提车应收款的不可勾选 + var vehicleRows = useState([ + { key: 'v1', index: 1, brand: '东风', model: 'DFH1180', plateNo: '浙A12345', receivableRent: 30000, actualRent: '30000.00', rentRemark: '', receivableDeposit: 10000, serviceItems: [{ name: '代处理费用', receivable: 200, actual: '200.00', discount: '0.00', remark: '' }, { name: '保险上浮', receivable: 500, actual: '500.00', discount: '0.00', remark: '' }], receivableService: 700, actualService: '700.00', discountAmount: '0.00', discountRemark: '', discountProof: [], selected: false, receivableCompleted: true }, + { key: 'v2', index: 2, brand: '福田', model: 'BJ1180', plateNo: '浙A23456', receivableRent: 27000, actualRent: '27000.00', rentRemark: '', receivableDeposit: 8000, serviceItems: [{ name: '保养费用', receivable: 300, actual: '300.00', discount: '0.00', remark: '' }], receivableService: 300, actualService: '300.00', discountAmount: '0.00', discountRemark: '', discountProof: [], selected: false, receivableCompleted: true }, + { key: 'v3', index: 3, brand: '重汽', model: 'ZZ1187', plateNo: '浙A34567', receivableRent: 31200, actualRent: '31200.00', rentRemark: '', receivableDeposit: 10000, serviceItems: [{ name: '代处理费用', receivable: 180, actual: '180.00', discount: '0.00', remark: '' }, { name: '上牌服务', receivable: 400, actual: '400.00', discount: '0.00', remark: '' }], receivableService: 580, actualService: '580.00', discountAmount: '0.00', discountRemark: '', discountProof: [{ name: '减免说明.pdf' }], selected: true, receivableCompleted: false }, + { key: 'v4', index: 4, brand: '陕汽', model: 'SX1313', plateNo: '', receivableRent: 28800, actualRent: '28800.00', rentRemark: '', receivableDeposit: 9000, serviceItems: [{ name: '保险上浮', receivable: 350, actual: '350.00', discount: '0.00', remark: '' }], receivableService: 350, actualService: '350.00', discountAmount: '0.00', discountRemark: '', discountProof: [], selected: true, receivableCompleted: false }, + { key: 'v5', index: 5, brand: '解放', model: 'J6P', plateNo: '浙A45678', receivableRent: 33000, actualRent: '33000.00', rentRemark: '', receivableDeposit: 11000, serviceItems: [{ name: '代处理费用', receivable: 220, actual: '220.00', discount: '0.00', remark: '' }, { name: '保养费用', receivable: 280, actual: '280.00', discount: '0.00', remark: '' }], receivableService: 500, actualService: '500.00', discountAmount: '0.00', discountRemark: '', discountProof: [], selected: true, receivableCompleted: false } + ]); + var vehicles = vehicleRows[0]; + var setVehicles = vehicleRows[1]; + + var hasHydrogenPrepay = true; + // 氢费预付款(仅一条,提交审核时的数据,只读展示) + var hydrogen = useMemo(function () { + return { receivable: '3580.00', actual: '3580.00', discount: '0.00', discountRemark: '氢费预付款减免说明,与客户签订补充协议' }; + }, []); + var invoiceMethod = '先开票后付款'; + var invoiceRemark = '开票项目:租赁费,税率 13%。请开具增值税专用发票。'; + + var updateVehicle = useCallback(function (key, field, value) { + setVehicles(function (prev) { + return prev.map(function (r) { + if (r.key !== key) return r; + var next = Object.assign({}, r); + next[field] = value; + if (field === 'serviceItems') { + var total = 0; + (value || []).forEach(function (s) { total += parseFloat(s.actual) || 0; }); + next.actualService = total.toFixed(2); + } + return next; + }); + }); + }, []); + + var updateServiceItem = useCallback(function (vehicleKey, itemIndex, field, value) { + setVehicles(function (prev) { + return prev.map(function (r) { + if (r.key !== vehicleKey) return r; + var items = (r.serviceItems || []).slice(); + if (!items[itemIndex]) return r; + items[itemIndex] = Object.assign({}, items[itemIndex], { [field]: value }); + var total = 0; + items.forEach(function (s) { total += parseFloat(s.actual) || 0; }); + return Object.assign({}, r, { serviceItems: items, actualService: total.toFixed(2) }); + }); + }); + }, []); + + var totals = useMemo(function () { + var selected = vehicles.filter(function (v) { return v.selected; }); + var receivableRent = 0, actualRent = 0, receivableDeposit = 0, receivableService = 0, actualService = 0, discountTotal = 0; + selected.forEach(function (v) { + receivableRent += Number(v.receivableRent) || 0; + actualRent += parseFloat(v.actualRent) || 0; + receivableDeposit += Number(v.receivableDeposit) || 0; + receivableService += Number(v.receivableService) || 0; + actualService += parseFloat(v.actualService) || 0; + discountTotal += parseFloat(v.discountAmount) || 0; + }); + return { + receivableRent: receivableRent.toFixed(2), + actualRent: actualRent.toFixed(2), + receivableDeposit: receivableDeposit.toFixed(2), + receivableService: receivableService.toFixed(2), + actualService: actualService.toFixed(2), + discountTotal: discountTotal.toFixed(2) + }; + }, [vehicles]); + + var selectedVehicles = vehicles.filter(function (v) { return v.selected; }); + + var receivableTotal = (parseFloat(totals.receivableRent) + parseFloat(totals.receivableDeposit) + parseFloat(totals.receivableService)).toFixed(2); + var actualTotal = (parseFloat(totals.actualRent) + parseFloat(totals.receivableDeposit) + parseFloat(totals.actualService) - parseFloat(totals.discountTotal)).toFixed(2); + + var receivablePopoverContent = useMemo(function () { + return React.createElement('div', { style: { padding: 8, minWidth: 220 } }, + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 13 } }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '项目'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '金额') + ) + ), + React.createElement('tbody', null, + React.createElement('tr', null, React.createElement('td', { style: { padding: '6px 8px' } }, '总计应收车辆月租金'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.receivableRent + ' 元')), + React.createElement('tr', null, React.createElement('td', { style: { padding: '6px 8px' } }, '总计应收车辆保证金'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.receivableDeposit + ' 元')), + React.createElement('tr', null, React.createElement('td', { style: { padding: '6px 8px' } }, '总计应收服务费'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.receivableService + ' 元')) + ) + ) + ); + }, [totals]); + + var actualPopoverContent = useMemo(function () { + return React.createElement('div', { style: { padding: 8, minWidth: 220 } }, + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 13 } }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '项目'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '金额') + ) + ), + React.createElement('tbody', null, + React.createElement('tr', null, React.createElement('td', { style: { padding: '6px 8px' } }, '总计实收车辆月租金'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.actualRent + ' 元')), + React.createElement('tr', null, React.createElement('td', { style: { padding: '6px 8px' } }, '总计应收车辆保证金'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.receivableDeposit + ' 元')), + React.createElement('tr', null, React.createElement('td', { style: { padding: '6px 8px' } }, '总计实收服务费'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.actualService + ' 元')), + React.createElement('tr', null, React.createElement('td', { style: { padding: '6px 8px' } }, '总计减免金额'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.discountTotal + ' 元')) + ) + ) + ); + }, [totals]); + + var invoiceInfo = useMemo(function () { + return { + customerName: '嘉兴某某物流有限公司', + taxId: '91330400MA2XXXXX1', + address: '浙江省嘉兴市南湖区科技大道1号', + phone: '0571-88888888', + account: '6222021234567890123', + bank: '中国工商银行嘉兴分行', + mailingAddress: '浙江省嘉兴市南湖区科技大道1号' + }; + }, []); + + var approvalSteps = useMemo(function () { + return [ + { title: '业务部门', status: 'finish', person: '张经理', time: '2026-03-01 10:00' }, + { title: '财务部门', status: 'finish', person: '李四', time: '2026-03-03 09:00' }, + { title: '财务负责人', status: 'finish', person: '王五', time: '2026-03-03 11:00' } + ]; + }, []); + + var unpaidAmount = useMemo(function () { + var sumArrival = invoiceList.reduce(function (s, r) { return s + (parseFloat(r.arrivalAmount) || 0); }, 0); + return (Math.max(0, parseFloat(actualTotal) - sumArrival)).toFixed(2); + }, [invoiceList, actualTotal]); + + function updateInvoiceRow(id, field, value) { + setInvoiceList(function (prev) { + return prev.map(function (r) { + if (r.id !== id) return r; + var next = Object.assign({}, r); + next[field] = value; + if (field === 'invoiceFiles') next.invoiceFiles = value || []; + return next; + }); + }); + } + + function addInvoiceRow() { + nextIdRef.current += 1; + setInvoiceList(function (prev) { + return prev.concat([{ id: 'new' + nextIdRef.current, arrivalTime: '', arrivalAmount: '', invoiceTime: nowStr(), invoiceFiles: [], remark: '', invoicePerson: '-', submitted: false }]); + }); + } + + function removeInvoiceRow(id) { + var row = invoiceList.find(function (r) { return r.id === id; }); + if (row && row.submitted) return; + setInvoiceList(function (prev) { return prev.filter(function (r) { return r.id !== id; }); }); + } + + var handleSubmit = useCallback(function () { + submitConfirmVisible[1](true); + }, []); + + var handleSubmitConfirm = useCallback(function () { + message.success('提交成功'); + submitConfirmVisible[1](false); + if (window.__receivableBack) window.__receivableBack(); else message.info('跳转至提车应收款列表页(原型)'); + }, []); + + var handleCancel = useCallback(function () { + cancelConfirmVisible[1](true); + }, []); + + var handleCancelConfirm = useCallback(function () { + cancelConfirmVisible[1](false); + if (window.__receivableBack) window.__receivableBack(); else message.info('返回提车应收款列表页(原型)'); + }, []); + + var requirementContent = '提车应收款-开票信息(2026年3月5日版本)\n一个「数字化资产ONEOS运管平台」中的「财务管理」「提车应收款」「开票信息」模块\n#面包屑:财务管理-提车应收款-开票信息;\n\n页面分为3个卡片,业务流程请参考流程图;\n1.项目信息:\n#显示项目详细信息,包括:\n1.1.合同编码:显示该租赁合同对应合同编码;\n1.2.合同类型:显示该租赁合同对应合同编码;\n1.3.项目名称:显示该租赁合同对应项目名称;\n1.4.客户名称:显示该租赁合同对应客户名称;\n1.5.付款方式:显示该租赁合同对应付款方式,分为:预付、后付两种,根据实际合同反写;\n1.6.付款周期:显示该租赁合同对应付款周期,格式为:x个月;\n1.7.合同生效时间:显示该租赁合同对应合同生效时间;\n1.8.合同结束时间:显示该租赁合同对应合同结束时间;\n1.9.业务部门:显示该租赁合同对应业务部门名称;\n1.10.业务负责人:显示该租赁合同对应业务负责人;\n\n2.提车应收款信息:\n#上方显示应收款总额、实收款总额;\n2.1.应收款总额:显示提交审核时的应收款总额,格式为:xx.xx元,点击金额弹出气泡卡片,卡片中显示列表,字段为:\n 2.1.1.项目:包括总计应收车辆月租金、总计应收车辆保证金、总计应收服务费三项;\n 2.1.2.金额:显示该项目对应应收金额;\n2.2.实收款总额:显示提交审核时的实收款总额,格式为:xx.xx元,点击金额弹出气泡卡片,卡片中显示列表,字段为:\n 2.2.1.项目:包括总计实收车辆月租金、总计应收车辆保证金、总计实收服务费、总计减免金额四项;\n 2.2.2.金额:显示该项目对应实收金额;\n#中间为车辆应收款明细,以列表展示提车应收款已选中所有车辆明细,包括以下字段:\n2.1.序号:根据租赁合同中车辆对应序号顺序展示;\n2.2.品牌:显示租赁合同中车辆对应品牌;\n2.3.型号:显示租赁合同中车辆对应型号;\n2.4.车牌号:显示租赁合同中车辆对应车牌号,如果车牌号为空则显示为-(后续如果交车成功,则车牌号会反显在该处);\n2.5.应收车辆月租金:根据租赁合同付款周期自动计算,例如:付款周期为6个月,则这里显示金额为:车辆月租金*6;\n2.6.实收车辆月租金:显示提交审核时的实收车辆月租金,格式为:xx.xx元;\n2.7.车辆租金备注:显示提交审核时的车辆租金备注,超长则显示...,悬浮时显示全部详细内容;\n2.8.减免金额:显示提交审核时的减免金额,格式为:xx.xx元;\n2.9.减免金额备注:显示提交审核时的减免金额备注,超长则显示...,悬浮时显示全部详细内容;\n2.10.减免证明:显示所有附件,可点击预览;\n2.11.应收车辆保证金:显示租赁合同中当前车辆保证金金额,格式为:xx.xx元;\n2.12.服务费项目:点击管理,弹出气泡卡片,气泡卡片标题为服务费项目,下方为列表,显示服务项目、应收费用、实收费用、备注;\n 2.12.1.服务项目:显示租赁合同中所有服务项目名称;\n 2.12.2.应收费用:显示租赁合同中所有服务项目对应费用;\n 2.12.3.实收费用:显示提交审核时的实收费用金额,格式为:xx.xx元;\n 2.12.4.减免费用:显示提交审核时的减免费用金额,格式为:xx.xx元;\n 2.12.4.备注:显示提交审核时的服务费减免备注,超长则显示...,悬浮时显示全部详细内容;\n2.13.应收服务费:显示当前车辆所有服务费应收费用总和,格式为:xx.xx元;\n2.14.实收服务费:显示当前车辆所有服务费实收费用总和,格式为:xx.xx元;\n2.15.列表下方对应字段下方显示总计数据,包括:总计应收车辆月租金、总计实收车辆月租金、总计应收车辆保证金、总计应收服务费、总计实收服务费、总计减免金额;\n列表不支持分页功能;\n#下方为氢费预付款情况,以列表展示租赁合同中所有氢费预付款明细,包括以下字段,如租赁合同氢费非预付款模式,则不显示该部分内容:\n2.19.氢费预付款应收金额:显示租赁合同中氢费预付款金额,格式为:xx.xx元,支持2位小数;\n2.20.氢费预付款实收金额:显示提交审核时的氢费预付款实收金额,格式为:xx.xx元;\n2.21.减免金额:显示提交审核时的减免金额,格式为:xx.xx元;\n2.22.减免金额备注:显示提交审核时的氢费预付款减免备注,超长则显示...,悬浮时显示全部详细内容;\n#最底部为开票方式、开票备注;\n2.23.开票方式:显示提交审核时的开票方式;\n2.24.开票备注:显示提交审核时的开票备注;\n\n3.开票信息:\n#上方显示该租赁合同客户信息(如果该合同转为三方合同,则未开票部分开票信息修改为丙方开票信息),包括:\n3.1.客户名称:显示租赁合同客户名称;\n3.2.纳税人识别号:显示该客户对应纳税人识别号;\n3.3.地址:显示该客户对应地址;\n3.4.电话:显示该客户对应电话;\n3.5.账户:显示该客户对应账户;\n3.6.开户行:显示该客户对应开户行;\n3.7.邮寄地址:显示该客户对应邮寄地址;\n#下方显示列表,包括以下字段,列表上方显示未付金额,格式为xx.xx元(计算方式为:实收款总额-(所有到账金额记录总和)),列表显示过往提交的历史记录,同时默认一行可编辑,支持继续新增一行和删除行操作(过往提交的历史记录无法删除):\n3.8.到账时间:时间选择器,格式为:YYYY-MM-DD HH:MM,新增未提交的记录支持删除,但之前已完成提交的记录无法删除;\n3.9.到账金额:输入框,支持2位小数,格式为:xx.xx元,新增未提交的记录支持删除,但之前已完成提交的记录无法删除;;\n3.10.开票时间:时间选择器,格式为:YYYY-MM-DD HH:MM,默认为打开页面的时间,新增未提交的记录支持删除,但之前已完成提交的记录无法删除;\n3.11.发票附件:附件上传按钮,支持多附件上传,上传后显示附件名称,新增未提交的记录支持删除,但之前已完成提交的记录无法删除;\n3.12.备注:输入框,新增未提交的记录支持删除,但之前已完成提交的记录无法删除;;\n3.13.开票人:显示上传发票的操作用户姓名;\n3.14.支持新增一行、删除一行(新增未提交的记录可以删除,已完成提交的历史记录不能删除);\n\n4.审批情况:\n#上方为纵向步骤条,显示所有审批步骤\n4.1.步骤中显示部门名称,部门名称后方显示审批状态、审批人、审批时间,所有审批记录已完成;\n\n5.底部为提交,取消;\n5.1.提交:点击提交,进行二次确认,文案为:请仔细核对提车首付款到账和开票信息,提交后无法修改;\n5.2.取消:点击取消,进行二次确认,文案为:取消将会丢失所有已填写数据,是否确认,点击确认则跳转至提车应收款列表页;'; + + var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; + var labelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var requiredStar = { color: '#ff4d4f', marginRight: 2 }; + var formRowStyle = { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px 24px', marginBottom: 16 }; + var formItemStyle = { marginBottom: 12 }; + var highlightStyle = { color: '#1890ff', fontWeight: 600, cursor: 'pointer' }; + var valueStyle = { color: 'rgba(0,0,0,0.85)', fontSize: 14, lineHeight: '22px', minHeight: 22 }; + var thBase = { padding: '10px 12px', border: '1px solid #f0f0f0', whiteSpace: 'nowrap' }; + + return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + React.createElement(Breadcrumb, { + items: [ + { title: '财务管理' }, + { title: '提车应收款' }, + { title: '开票信息' } + ] + }), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { requirementModalVisible[1](true); } }, '查看需求说明') + ), + React.createElement(Card, { + title: '项目信息', + style: cardStyle, + extra: React.createElement(Button, { type: 'link', size: 'small', onClick: function () { projectCardExpanded[1](!projectCardExpanded[0]); } }, projectCardExpanded[0] ? '收起' : '展开') + }, + projectCardExpanded[0] ? React.createElement('div', { style: formRowStyle }, + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '合同编码'), React.createElement('div', { style: valueStyle }, projectInfo.contractCode || '-')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '合同类型'), React.createElement('div', { style: valueStyle }, projectInfo.contractType || '-')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '项目名称'), React.createElement('div', { style: valueStyle }, projectInfo.projectName || '-')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '客户名称'), React.createElement('div', { style: valueStyle }, projectInfo.customerName || '-')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '付款方式'), React.createElement('div', { style: valueStyle }, projectInfo.paymentMethod || '-')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '付款周期'), React.createElement('div', { style: valueStyle }, projectInfo.paymentCycle || '-')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '合同生效时间'), React.createElement('div', { style: valueStyle }, projectInfo.contractStart || '-')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '合同结束时间'), React.createElement('div', { style: valueStyle }, projectInfo.contractEnd || '-')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '业务部门'), React.createElement('div', { style: valueStyle }, projectInfo.businessDept || '-')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '业务负责人'), React.createElement('div', { style: valueStyle }, projectInfo.businessPerson || '-')) + ) : null + ), + React.createElement(Card, { + title: '提车应收款信息', + style: cardStyle, + extra: React.createElement(Button, { type: 'link', size: 'small', onClick: function () { receivableCardExpanded[1](!receivableCardExpanded[0]); } }, receivableCardExpanded[0] ? '收起' : '展开') + }, + receivableCardExpanded[0] ? React.createElement(React.Fragment, null, + React.createElement('div', { style: { marginBottom: 16, display: 'flex', gap: 24, alignItems: 'center' } }, + React.createElement(Popover, { content: receivablePopoverContent, title: '应收款明细', trigger: 'click' }, + React.createElement('span', { style: { cursor: 'pointer' } }, '应收款总额:', React.createElement('span', { style: highlightStyle }, receivableTotal, ' 元')) + ), + React.createElement(Popover, { content: actualPopoverContent, title: '实收款明细', trigger: 'click' }, + React.createElement('span', { style: { cursor: 'pointer' } }, '实收款总额:', React.createElement('span', { style: highlightStyle }, actualTotal, ' 元')) + ) + ), + React.createElement('div', { style: { overflowX: 'auto', marginBottom: 16, overflowY: 'visible' } }, + React.createElement('table', { style: { width: '100%', minWidth: 1600, borderCollapse: 'collapse', fontSize: 13, tableLayout: 'fixed' } }, + React.createElement('thead', null, + React.createElement('tr', { style: { backgroundColor: '#fafafa' } }, + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 50 }) }, '序号'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 80 }) }, '品牌'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 90 }) }, '型号'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 100 }) }, '车牌号'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 120 }) }, '应收车辆月租金'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 130 }) }, React.createElement('span', null, React.createElement('span', { style: requiredStar }, '*'), '实收车辆月租金')), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 130 }) }, '车辆租金备注'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 130 }) }, '减免金额'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 130 }) }, '减免金额备注'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 130 }) }, '减免证明'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 110 }) }, '应收车辆保证金'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 90 }) }, '服务费项目'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 90 }) }, '应收服务费'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 130 }) }, '实收服务费') + ) + ), + React.createElement('tbody', null, + selectedVehicles.map(function (row) { + var disabled = row.receivableCompleted; + var servicePopover = React.createElement('div', { style: { padding: 8, minWidth: 360 } }, + React.createElement('div', { style: { fontWeight: 600, marginBottom: 8 } }, '服务费项目'), + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 12 } }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '服务项目'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '应收费用'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '实收费用'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '减免费用'), + React.createElement('th', { style: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '备注') + ) + ), + React.createElement('tbody', null, + (row.serviceItems || []).map(function (s, si) { + return React.createElement('tr', { key: si }, + React.createElement('td', { style: { padding: '6px 8px' } }, s.name), + React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, (s.receivable != null ? s.receivable : '') + ' 元'), + React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, (s.actual || '') + ' 元'), + React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, (s.discount || '0.00') + ' 元'), + React.createElement('td', { style: { padding: '6px 8px' } }, s.remark || '-') + ); + }) + ) + ) + ); + return React.createElement('tr', { key: row.key }, + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, row.index), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, row.brand), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, row.model), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, row.plateNo || '-'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, (row.receivableRent || 0) + ' 元'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', width: 130, textAlign: 'right' } }, (row.actualRent || '0.00') + ' 元'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', width: 130 } }, row.rentRemark || '-'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', width: 130, textAlign: 'right' } }, (row.discountAmount || '0.00') + ' 元'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', width: 130 } }, row.discountRemark || '-'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', width: 130 } }, + (row.discountProof && row.discountProof.length) + ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 2, fontSize: 12 } }, + row.discountProof.map(function (p, pidx) { + return React.createElement(Button, { key: pidx, type: 'link', size: 'small', style: { padding: 0, height: 'auto', fontSize: 12, textAlign: 'left' }, onClick: function () { proofPreviewFile[1](p); proofPreviewVisible[1](true); } }, p.name || '附件'); + }) + ) + : '-' + ), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, (row.receivableDeposit || 0) + ' 元'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, + React.createElement(Popover, { content: servicePopover, title: null, trigger: 'click' }, + React.createElement(Button, { type: 'link', size: 'small' }, '管理') + ) + ), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, (row.receivableService || 0) + ' 元'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', textAlign: 'right', width: 130 } }, (row.actualService || '0.00') + ' 元') + ); + }) + ), + React.createElement('tfoot', null, + React.createElement('tr', { style: { backgroundColor: '#fafafa', fontWeight: 500 } }, + React.createElement('td', { style: { padding: '10px 12px', border: '1px solid #f0f0f0', fontWeight: 600 } }, '总计'), + React.createElement('td', { style: { padding: '10px 12px', border: '1px solid #f0f0f0' } }), + React.createElement('td', { style: { padding: '10px 12px', border: '1px solid #f0f0f0' } }), + React.createElement('td', { style: { padding: '10px 12px', border: '1px solid #f0f0f0' } }), + React.createElement('td', { style: { padding: '10px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, totals.receivableRent + ' 元'), + React.createElement('td', { style: { padding: '10px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, totals.actualRent + ' 元'), + React.createElement('td', { style: { padding: '10px 12px', border: '1px solid #f0f0f0' } }), + React.createElement('td', { style: { padding: '10px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, totals.discountTotal + ' 元'), + React.createElement('td', { style: { padding: '10px 12px', border: '1px solid #f0f0f0' } }), + React.createElement('td', { style: { padding: '10px 12px', border: '1px solid #f0f0f0' } }), + React.createElement('td', { style: { padding: '10px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, totals.receivableDeposit + ' 元'), + React.createElement('td', { style: { padding: '10px 12px', border: '1px solid #f0f0f0' } }), + React.createElement('td', { style: { padding: '10px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, totals.receivableService + ' 元'), + React.createElement('td', { style: { padding: '10px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, totals.actualService + ' 元') + ) + ) + ) + ), + hasHydrogenPrepay ? React.createElement('div', { style: { marginBottom: 16 } }, + React.createElement('div', { style: { fontSize: 14, fontWeight: 500, marginBottom: 8 } }, '氢费预付款情况'), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr', gap: 16, maxWidth: 800 } }, + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '氢费预付款应收金额'), + React.createElement('div', { style: valueStyle }, hydrogen.receivable + ' 元') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '氢费预付款实收金额'), + React.createElement('div', { style: valueStyle }, hydrogen.actual + ' 元') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '减免金额'), + React.createElement('div', { style: valueStyle }, hydrogen.discount + ' 元') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '减免金额备注'), + React.createElement('div', { style: valueStyle }, hydrogen.discountRemark || '-') + ) + ) + ) : null, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px 24px', marginTop: 16 } }, + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '开票方式'), React.createElement('div', { style: valueStyle }, invoiceMethod)), + React.createElement('div', { style: Object.assign({}, formItemStyle, { gridColumn: '1 / -1' }) }, React.createElement('div', { style: labelStyle }, '开票备注'), React.createElement('div', { style: valueStyle }, invoiceRemark || '-')) + ) + ) : null + ), + React.createElement(Card, { + title: '开票信息', + style: cardStyle, + extra: React.createElement(Button, { type: 'link', size: 'small', onClick: function () { invoiceCardExpanded[1](!invoiceCardExpanded[0]); } }, invoiceCardExpanded[0] ? '收起' : '展开') + }, + invoiceCardExpanded[0] ? React.createElement(React.Fragment, null, + React.createElement('div', { style: { fontSize: 14, fontWeight: 500, marginBottom: 12 } }, '客户信息'), + React.createElement('div', { style: formRowStyle }, + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '客户名称'), React.createElement('div', { style: valueStyle }, invoiceInfo.customerName || '-')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '纳税人识别号'), React.createElement('div', { style: valueStyle }, invoiceInfo.taxId || '-')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '地址'), React.createElement('div', { style: valueStyle }, invoiceInfo.address || '-')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '电话'), React.createElement('div', { style: valueStyle }, invoiceInfo.phone || '-')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '账户'), React.createElement('div', { style: valueStyle }, invoiceInfo.account || '-')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '开户行'), React.createElement('div', { style: valueStyle }, invoiceInfo.bank || '-')), + React.createElement('div', { style: Object.assign({}, formItemStyle, { gridColumn: '1 / -1' }) }, React.createElement('div', { style: labelStyle }, '邮寄地址'), React.createElement('div', { style: valueStyle }, invoiceInfo.mailingAddress || '-')) + ), + React.createElement('div', { style: { fontSize: 14, fontWeight: 500, marginBottom: 4, marginTop: 16 } }, '到账与开票记录'), + React.createElement('div', { style: { marginBottom: 8, fontSize: 14 } }, '未付金额:', React.createElement('span', { style: highlightStyle }, unpaidAmount, ' 元')), + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 13 } }, + React.createElement('thead', null, + React.createElement('tr', { style: { backgroundColor: '#fafafa' } }, + React.createElement('th', { style: Object.assign({}, thBase, { width: 160 }) }, '到账时间'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 120 }) }, '到账金额'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 160 }) }, '开票时间'), + React.createElement('th', { style: Object.assign({}, thBase) }, '发票附件'), + React.createElement('th', { style: Object.assign({}, thBase) }, '备注'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 80 }) }, '开票人'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 70 }) }, '操作') + ) + ), + React.createElement('tbody', null, + invoiceList.map(function (r) { + var dateValArrival = r.arrivalTime && dayjs ? dayjs(r.arrivalTime, 'YYYY-MM-DD HH:mm') : null; + var dateValInvoice = r.invoiceTime && dayjs ? dayjs(r.invoiceTime, 'YYYY-MM-DD HH:mm') : null; + if (dateValInvoice && dateValInvoice.isValid && !dateValInvoice.isValid()) dateValInvoice = null; + if (dateValArrival && dateValArrival.isValid && !dateValArrival.isValid()) dateValArrival = null; + return React.createElement('tr', { key: r.id }, + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, + r.submitted ? (r.arrivalTime || '-') : React.createElement(DatePicker, { + showTime: true, + style: { width: '100%' }, + format: 'YYYY-MM-DD HH:mm', + placeholder: '请选择到账时间', + value: dateValArrival, + onChange: function (d, dateStr) { updateInvoiceRow(r.id, 'arrivalTime', dateStr || ''); } + }) + ), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, + r.submitted ? (r.arrivalAmount ? r.arrivalAmount + ' 元' : '-') : React.createElement(Input, { size: 'small', value: r.arrivalAmount, suffix: '元', placeholder: '0.00', onChange: function (e) { updateInvoiceRow(r.id, 'arrivalAmount', e.target.value); } }) + ), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, + r.submitted ? (r.invoiceTime || '-') : React.createElement(DatePicker, { + showTime: true, + style: { width: '100%' }, + format: 'YYYY-MM-DD HH:mm', + placeholder: '请选择开票时间', + value: dateValInvoice, + onChange: function (d, dateStr) { updateInvoiceRow(r.id, 'invoiceTime', dateStr || ''); } + }) + ), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, + r.submitted + ? ((r.invoiceFiles && r.invoiceFiles.length) ? r.invoiceFiles.map(function (f, i) { return React.createElement(Button, { key: i, type: 'link', size: 'small', style: { padding: 0, height: 'auto', fontSize: 12, marginRight: 4 }, onClick: function () { invoicePreviewFile[1](f); invoicePreviewVisible[1](true); } }, f.name || '附件'); }) : '-') + : React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } }, + (r.invoiceFiles || []).map(function (f, i) { + return React.createElement('div', { key: i, style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 12 } }, + React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0, flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', textAlign: 'left', height: 'auto' }, onClick: function () { invoicePreviewFile[1](f); invoicePreviewVisible[1](true); } }, f.name || '附件'), + React.createElement(Button, { type: 'link', size: 'small', danger: true, style: { padding: 0 }, onClick: function () { var next = (r.invoiceFiles || []).slice(); next.splice(i, 1); updateInvoiceRow(r.id, 'invoiceFiles', next); } }, '删除') + ); + }), + React.createElement('span', null, + React.createElement('input', { type: 'file', multiple: true, accept: '.jpg,.jpeg,.png,.pdf', style: { display: 'none' }, id: 'inv-file-' + r.id, onChange: function (e) { + var files = e.target.files; + if (!files || !files.length) return; + var next = (r.invoiceFiles || []).slice(); + for (var i = 0; i < files.length; i++) next.push({ name: files[i].name }); + updateInvoiceRow(r.id, 'invoiceFiles', next); + e.target.value = ''; + } }), + React.createElement(Button, { type: 'default', size: 'small', onClick: function () { var el = document.getElementById('inv-file-' + r.id); if (el) el.click(); } }, '附件上传') + ) + ) + ), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, + r.submitted ? (r.remark || '-') : React.createElement(Input, { size: 'small', value: r.remark, placeholder: '选填', onChange: function (e) { updateInvoiceRow(r.id, 'remark', e.target.value); } }) + ), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, r.invoicePerson || '-'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, + r.submitted ? '-' : React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function () { removeInvoiceRow(r.id); } }, '删除') + ) + ); + }) + ) + ), + React.createElement('div', { style: { marginTop: 8 } }, React.createElement(Button, { type: 'dashed', size: 'small', onClick: addInvoiceRow }, '新增一行')) + ) : null + ), + React.createElement(Card, { + title: '审批情况', + style: cardStyle, + extra: React.createElement(Button, { type: 'link', size: 'small', onClick: function () { approveCardExpanded[1](!approveCardExpanded[0]); } }, approveCardExpanded[0] ? '收起' : '展开') + }, + approveCardExpanded[0] ? React.createElement(Steps, { + direction: 'vertical', + current: approvalSteps.length, + items: approvalSteps.map(function (s) { + var statusText = '审批通过'; + var desc = React.createElement('div', { style: { fontSize: 13, color: 'rgba(0,0,0,0.65)', marginTop: 4 } }, + React.createElement('div', null, '审批状态:', statusText), + React.createElement('div', null, '审批人:', s.person || '-'), + s.time ? React.createElement('div', null, '审批时间:', s.time) : null + ); + return { title: s.title, description: desc, status: 'finish' }; + }) + }) : null + ), + React.createElement('div', { style: { display: 'flex', gap: 8, marginTop: 24 } }, + React.createElement(Button, { type: 'primary', onClick: handleSubmit }, '提交'), + React.createElement(Button, { onClick: handleCancel }, '取消') + ), + React.createElement(Modal, { + title: '确认提交', + open: submitConfirmVisible[0], + onCancel: function () { submitConfirmVisible[1](false); }, + onOk: handleSubmitConfirm, + okText: '确认', + cancelText: '取消' + }, React.createElement('div', null, '请仔细核对提车首付款到账和开票信息,提交后无法修改。')), + React.createElement(Modal, { + title: '确认取消', + open: cancelConfirmVisible[0], + onCancel: function () { cancelConfirmVisible[1](false); }, + onOk: handleCancelConfirm, + okText: '确认', + cancelText: '取消' + }, React.createElement('div', null, '取消将会丢失所有已填写数据,是否确认?')), + React.createElement(Modal, { + title: '附件预览', + open: proofPreviewVisible[0], + onCancel: function () { proofPreviewVisible[1](false); proofPreviewFile[1](null); }, + footer: React.createElement(Button, { onClick: function () { proofPreviewVisible[1](false); proofPreviewFile[1](null); } }, '关闭'), + width: 640 + }, React.createElement('div', { style: { padding: '16px 0' } }, + React.createElement('div', { style: { marginBottom: 12, fontSize: 14, color: 'rgba(0,0,0,0.65)' } }, '文件:', proofPreviewFile[0] ? (proofPreviewFile[0].name || '附件') : ''), + React.createElement('div', { style: { minHeight: 360, background: '#fafafa', borderRadius: 4, display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'rgba(0,0,0,0.45)', fontSize: 14 } }, '此处可预览附件内容,支持 jpg、png、pdf 等格式') + )), + React.createElement(Modal, { + title: '发票附件预览', + open: invoicePreviewVisible[0], + onCancel: function () { invoicePreviewVisible[1](false); invoicePreviewFile[1](null); }, + footer: React.createElement(Button, { onClick: function () { invoicePreviewVisible[1](false); invoicePreviewFile[1](null); } }, '关闭'), + width: 640 + }, React.createElement('div', { style: { padding: '16px 0' } }, + React.createElement('div', { style: { marginBottom: 12, fontSize: 14, color: 'rgba(0,0,0,0.65)' } }, '文件:', invoicePreviewFile[0] ? (invoicePreviewFile[0].name || '附件') : ''), + React.createElement('div', { style: { minHeight: 360, background: '#fafafa', borderRadius: 4, display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'rgba(0,0,0,0.45)', fontSize: 14 } }, '此处可预览附件内容,支持 jpg、png、pdf 等格式') + )), + React.createElement(Modal, { + title: '需求说明', + open: requirementModalVisible[0], + onCancel: function () { requirementModalVisible[1](false); }, + width: 720, + footer: React.createElement(Button, { onClick: function () { requirementModalVisible[1](false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, React.createElement('div', { style: { padding: '8px 0', whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6 } }, requirementContent)) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-finance/pages/07-提车应收款-审核.jsx b/src/prototypes/oneos-web-finance/pages/07-提车应收款-审核.jsx new file mode 100644 index 0000000..6dbee66 --- /dev/null +++ b/src/prototypes/oneos-web-finance/pages/07-提车应收款-审核.jsx @@ -0,0 +1,399 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 财务管理 - 提车应收款 - 审批(2026年3月4日版本) + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Button = antd.Button; + var Modal = antd.Modal; + var Input = antd.Input; + var Popover = antd.Popover; + var Tooltip = antd.Tooltip; + var Steps = antd.Steps; + var message = antd.message; + + var projectCardExpanded = useState(true); + var receivableCardExpanded = useState(true); + var approveCardExpanded = useState(true); + var rejectModalVisible = useState(false); + var setRejectModalVisible = rejectModalVisible[1]; + var rejectRemark = useState(''); + var setRejectRemark = rejectRemark[1]; + var approveRemark = useState(''); + var setApproveRemark = approveRemark[1]; + var requirementModalVisible = useState(false); + var setRequirementModalVisible = requirementModalVisible[1]; + var proofPreviewVisible = useState(false); + var setProofPreviewVisible = proofPreviewVisible[1]; + var proofPreviewFile = useState(null); + var setProofPreviewFile = proofPreviewFile[1]; + + var requirementContent = '提车应收款-审批(2026年3月4日版本)\n一个「数字化资产ONEOS运管平台」中的「财务管理」「提车应收款」「审批」模块\n#面包屑:财务管理-提车应收款-审批;\n\n页面分为4个卡片,业务流程请参考流程图;\n1.项目信息:\n#显示项目详细信息,包括:\n1.1.合同编码:显示该租赁合同对应合同编码;\n1.2.合同类型:显示该租赁合同对应合同编码;\n1.3.项目名称:显示该租赁合同对应项目名称;\n1.4.客户名称:显示该租赁合同对应客户名称;\n1.5.付款方式:显示该租赁合同对应付款方式,分为:预付、后付两种,根据实际合同反写;\n1.6.付款周期:显示该租赁合同对应付款周期,格式为:x个月;\n1.7.合同生效时间:显示该租赁合同对应合同生效时间;\n1.8.合同结束时间:显示该租赁合同对应合同结束时间;\n1.9.业务部门:显示该租赁合同对应业务部门名称;\n1.10.业务负责人:显示该租赁合同对应业务负责人;\n\n2.提车应收款信息:\n#上方显示应收款总额、实收款总额;\n2.1.应收款总额:显示提交审核时的应收款总额,格式为:xx.xx元,点击金额弹出气泡卡片,卡片中显示列表,字段为:\n 2.1.1.项目:包括总计应收车辆月租金、总计应收车辆保证金、总计应收服务费三项;\n 2.1.2.金额:显示该项目对应应收金额;\n2.2.实收款总额:显示提交审核时的实收款总额,格式为:xx.xx元,点击金额弹出气泡卡片,卡片中显示列表,字段为:\n 2.2.1.项目:包括总计实收车辆月租金、总计应收车辆保证金、总计实收服务费、总计减免金额四项;\n 2.2.2.金额:显示该项目对应实收金额;\n#中间为车辆应收款明细,以列表展示提车应收款已选中所有车辆明细,包括以下字段:\n2.1.序号:根据租赁合同中车辆对应序号顺序展示;\n2.2.品牌:显示租赁合同中车辆对应品牌;\n2.3.型号:显示租赁合同中车辆对应型号;\n2.4.车牌号:显示租赁合同中车辆对应车牌号,如果车牌号为空则显示为-(后续如果交车成功,则车牌号会反显在该处);\n2.5.应收车辆月租金:根据租赁合同付款周期自动计算,例如:付款周期为6个月,则这里显示金额为:车辆月租金*6;\n2.6.实收车辆月租金:显示提交审核时的实收车辆月租金,格式为:xx.xx元;\n2.7.车辆租金备注:显示提交审核时的车辆租金备注,超长则显示...,悬浮时显示全部详细内容;\n2.8.减免金额:显示提交审核时的减免金额,格式为:xx.xx元;\n2.9.减免金额备注:显示提交审核时的减免金额备注,超长则显示...,悬浮时显示全部详细内容;\n2.10.减免证明:显示所有附件,可点击预览;\n2.11.应收车辆保证金:显示租赁合同中当前车辆保证金金额,格式为:xx.xx元;\n2.12.服务费项目:点击管理,弹出气泡卡片,气泡卡片标题为服务费项目,下方为列表,显示服务项目、应收费用、实收费用、备注;\n 2.12.1.服务项目:显示租赁合同中所有服务项目名称;\n 2.12.2.应收费用:显示租赁合同中所有服务项目对应费用;\n 2.12.3.实收费用:显示提交审核时的实收费用金额,格式为:xx.xx元;\n 2.12.4.减免费用:显示提交审核时的减免费用金额,格式为:xx.xx元;\n 2.12.4.备注:显示提交审核时的服务费减免备注,超长则显示...,悬浮时显示全部详细内容;\n2.13.应收服务费:显示当前车辆所有服务费应收费用总和,格式为:xx.xx元;\n2.14.实收服务费:显示当前车辆所有服务费实收费用总和,格式为:xx.xx元;\n2.15.列表下方对应字段下方显示总计数据,包括:总计应收车辆月租金、总计实收车辆月租金、总计应收车辆保证金、总计应收服务费、总计实收服务费、总计减免金额;\n列表不支持分页功能;\n#下方为氢费预付款情况,以列表展示租赁合同中所有氢费预付款明细,包括以下字段,如租赁合同氢费非预付款模式,则不显示该部分内容:\n2.19.氢费预付款应收金额:显示租赁合同中氢费预付款金额,格式为:xx.xx元,支持2位小数;\n2.20.氢费预付款实收金额:显示提交审核时的氢费预付款实收金额,格式为:xx.xx元;\n2.21.减免金额:显示提交审核时的减免金额,格式为:xx.xx元;\n2.22.减免金额备注:显示提交审核时的氢费预付款减免备注,超长则显示...,悬浮时显示全部详细内容;\n#最底部为开票方式、开票备注;\n2.23.开票方式:显示提交审核时的开票方式;\n2.24.开票备注:显示提交审核时的开票备注;\n\n3.审核情况:\n#上方为纵向步骤条,显示所有审批步骤\n3.1.步骤中显示部门名称,部门名称后方显示审批状态、审批人、审批时间;\n 3.1.1.部门名称:当前节点部门名称;\n 3.1.2.审批状态:分为待审批、审批通过、审批驳回;\n 3.1.3.审批人:步骤节点对应审批人姓名;\n 3.1.4.审批时间:显示审批通过/驳回时间,格式为:YYYY-MM-DD HH:MM;\n3.2.当前步骤的审批人下方为审批说明:非必填,文本域,支持自定义输入;\n3.3.底部为审批通过、驳回、取消;\n 3.3.1.审批通过:点击提示:审批完成,同时下个流程节点审批人工作台待办任务中收到审批任务和消息提示;如当前已是最后节点,则完成审批,同时进入财务开票环节;\n 3.3.2.驳回:点击进行二次确认,二次确认后提示:驳回成功,同时该任务在列表中显示为审核驳回,发起人可重新编辑;\n 3.3.3.取消:点击返回提车应收款列表页;'; + + var projectInfo = useMemo(function () { + return { + contractCode: 'HT-ZL-2025-001', + contractType: '正式合同', + projectName: '嘉兴氢能示范项目', + customerName: '嘉兴某某物流有限公司', + paymentMethod: '预付', + paymentCycle: '6个月', + contractStart: '2025-01-15', + contractEnd: '2026-01-14', + businessDept: '业务1部', + businessPerson: '张经理' + }; + }, []); + + var vehicles = useMemo(function () { + return [ + { key: 'v1', index: 1, brand: '东风', model: 'DFH1180', plateNo: '浙A12345', receivableRent: 30000, actualRent: '30000.00', rentRemark: '首月全额', receivableDeposit: 10000, receivableService: 700, actualService: '700.00', discountAmount: '0.00', discountRemark: '', discountProof: [{ name: '证明1.pdf' }], serviceItems: [{ name: '代处理费用', receivable: 200, actual: '200.00', discount: '0.00', remark: '' }, { name: '保险上浮', receivable: 500, actual: '500.00', discount: '0.00', remark: '' }] }, + { key: 'v2', index: 2, brand: '福田', model: 'BJ1180', plateNo: '浙A23456', receivableRent: 27000, actualRent: '27000.00', rentRemark: '', receivableDeposit: 8000, receivableService: 300, actualService: '300.00', discountAmount: '0.00', discountRemark: '', discountProof: [], serviceItems: [{ name: '保养费用', receivable: 300, actual: '300.00', discount: '0.00', remark: '' }] }, + { key: 'v3', index: 3, brand: '重汽', model: 'ZZ1187', plateNo: '浙A34567', receivableRent: 31200, actualRent: '31200.00', rentRemark: '客户协商延期支付部分', receivableDeposit: 10000, receivableService: 580, actualService: '580.00', discountAmount: '0.00', discountRemark: '无', discountProof: [{ name: '减免说明.jpg' }], serviceItems: [{ name: '代处理费用', receivable: 180, actual: '180.00', discount: '0.00', remark: '' }, { name: '上牌服务', receivable: 400, actual: '400.00', discount: '0.00', remark: '' }] } + ]; + }, []); + + var hasHydrogenPrepay = true; + var hydrogen = useMemo(function () { return { receivable: '3580.00', actual: '3580.00', discount: '0.00', discountRemark: '氢费预付款减免说明,与客户签订补充协议' }; }, []); + var invoiceMethod = '先开票后付款'; + var invoiceRemark = '开票项目:租赁费,税率 13%。请开具增值税专用发票。'; + + var totals = useMemo(function () { + var receivableRent = 0, actualRent = 0, receivableDeposit = 0, receivableService = 0, actualService = 0, discountTotal = 0; + vehicles.forEach(function (v) { + receivableRent += Number(v.receivableRent) || 0; + actualRent += parseFloat(v.actualRent) || 0; + receivableDeposit += Number(v.receivableDeposit) || 0; + receivableService += Number(v.receivableService) || 0; + actualService += parseFloat(v.actualService) || 0; + discountTotal += parseFloat(v.discountAmount) || 0; + }); + return { + receivableRent: receivableRent.toFixed(2), + actualRent: actualRent.toFixed(2), + receivableDeposit: receivableDeposit.toFixed(2), + receivableService: receivableService.toFixed(2), + actualService: actualService.toFixed(2), + discountTotal: discountTotal.toFixed(2) + }; + }, [vehicles]); + + var receivableTotal = (parseFloat(totals.receivableRent) + parseFloat(totals.receivableDeposit) + parseFloat(totals.receivableService)).toFixed(2); + var actualTotal = (parseFloat(totals.actualRent) + parseFloat(totals.receivableDeposit) + parseFloat(totals.actualService) - parseFloat(totals.discountTotal)).toFixed(2); + + var receivablePopoverContent = useMemo(function () { + return React.createElement('div', { style: { padding: 8, minWidth: 220 } }, + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 13 } }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '项目'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '金额') + ) + ), + React.createElement('tbody', null, + React.createElement('tr', null, React.createElement('td', { style: { padding: '6px 8px' } }, '总计应收车辆月租金'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.receivableRent + ' 元')), + React.createElement('tr', null, React.createElement('td', { style: { padding: '6px 8px' } }, '总计应收车辆保证金'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.receivableDeposit + ' 元')), + React.createElement('tr', null, React.createElement('td', { style: { padding: '6px 8px' } }, '总计应收服务费'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.receivableService + ' 元')) + ) + ) + ); + }, [totals]); + + var actualPopoverContent = useMemo(function () { + return React.createElement('div', { style: { padding: 8, minWidth: 220 } }, + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 13 } }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '项目'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '金额') + ) + ), + React.createElement('tbody', null, + React.createElement('tr', null, React.createElement('td', { style: { padding: '6px 8px' } }, '总计实收车辆月租金'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.actualRent + ' 元')), + React.createElement('tr', null, React.createElement('td', { style: { padding: '6px 8px' } }, '总计应收车辆保证金'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.receivableDeposit + ' 元')), + React.createElement('tr', null, React.createElement('td', { style: { padding: '6px 8px' } }, '总计实收服务费'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.actualService + ' 元')), + React.createElement('tr', null, React.createElement('td', { style: { padding: '6px 8px' } }, '总计减免金额'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.discountTotal + ' 元')) + ) + ) + ); + }, [totals]); + + var approvalSteps = useMemo(function () { + return [ + { title: '业务部门', status: 'finish', person: '张经理', time: '2026-03-01 10:00' }, + { title: '财务部门', status: 'process', person: '李四', time: '' }, + { title: '财务负责人', status: 'wait', person: '王五', time: '' } + ]; + }, []); + + var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; + var labelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var formRowStyle = { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px 24px', marginBottom: 16 }; + var formItemStyle = { marginBottom: 12 }; + var valueStyle = { color: 'rgba(0,0,0,0.85)', fontSize: 14, lineHeight: '22px', minHeight: 22 }; + var thBase = { padding: '10px 12px', border: '1px solid #f0f0f0', whiteSpace: 'nowrap' }; + var highlightStyle = { color: '#1890ff', fontWeight: 600, cursor: 'pointer' }; + + function ellipsisCell(text, maxLen) { + var str = (text == null || text === '') ? '-' : String(text); + var show = str.length <= (maxLen || 8) ? str : str.slice(0, maxLen || 8) + '...'; + return React.createElement(Tooltip, { title: str === '-' ? '' : str }, React.createElement('span', { style: { display: 'inline-block', maxWidth: 120, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, show)); + } + + var handlePass = function () { + message.success('审批完成,同时下个流程节点审批人工作台待办任务中收到审批任务和消息提示'); + if (window.__receivableBack) window.__receivableBack(); else message.info('返回提车应收款列表(原型)'); + }; + + var handleRejectClick = function () { + setRejectModalVisible(true); + }; + + var handleRejectConfirm = function () { + setRejectModalVisible(false); + setRejectRemark(''); + message.success('驳回成功,同时该任务在列表中显示为审核驳回,发起人可重新编辑'); + if (window.__receivableBack) window.__receivableBack(); else message.info('返回提车应收款列表(原型)'); + }; + + var handleCancel = function () { + if (window.__receivableBack) window.__receivableBack(); else message.info('返回提车应收款列表(原型)'); + }; + + return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + React.createElement(Breadcrumb, { + items: [ + { title: '财务管理' }, + { title: '提车应收款' }, + { title: '审批' } + ] + }), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { setRequirementModalVisible(true); } }, '查看需求说明') + ), + React.createElement(Card, { + title: '项目信息', + style: cardStyle, + extra: React.createElement(Button, { type: 'link', size: 'small', onClick: function () { projectCardExpanded[1](!projectCardExpanded[0]); } }, projectCardExpanded[0] ? '收起' : '展开') + }, + projectCardExpanded[0] ? React.createElement('div', { style: formRowStyle }, + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '合同编码'), React.createElement('div', { style: valueStyle }, projectInfo.contractCode || '-')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '合同类型'), React.createElement('div', { style: valueStyle }, projectInfo.contractType || '-')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '项目名称'), React.createElement('div', { style: valueStyle }, projectInfo.projectName || '-')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '客户名称'), React.createElement('div', { style: valueStyle }, projectInfo.customerName || '-')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '付款方式'), React.createElement('div', { style: valueStyle }, projectInfo.paymentMethod || '-')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '付款周期'), React.createElement('div', { style: valueStyle }, projectInfo.paymentCycle || '-')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '合同生效时间'), React.createElement('div', { style: valueStyle }, projectInfo.contractStart || '-')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '合同结束时间'), React.createElement('div', { style: valueStyle }, projectInfo.contractEnd || '-')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '业务部门'), React.createElement('div', { style: valueStyle }, projectInfo.businessDept || '-')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '业务负责人'), React.createElement('div', { style: valueStyle }, projectInfo.businessPerson || '-')) + ) : null + ), + React.createElement(Card, { + title: '提车应收款信息', + style: cardStyle, + extra: React.createElement(Button, { type: 'link', size: 'small', onClick: function () { receivableCardExpanded[1](!receivableCardExpanded[0]); } }, receivableCardExpanded[0] ? '收起' : '展开') + }, + receivableCardExpanded[0] ? React.createElement(React.Fragment, null, + React.createElement('div', { style: { marginBottom: 16, display: 'flex', gap: 24, alignItems: 'center' } }, + React.createElement(Popover, { content: receivablePopoverContent, title: '应收款明细', trigger: 'click' }, + React.createElement('span', { style: { cursor: 'pointer' } }, '应收款总额:', React.createElement('span', { style: highlightStyle }, receivableTotal, ' 元')) + ), + React.createElement(Popover, { content: actualPopoverContent, title: '实收款明细', trigger: 'click' }, + React.createElement('span', { style: { cursor: 'pointer' } }, '实收款总额:', React.createElement('span', { style: highlightStyle }, actualTotal, ' 元')) + ) + ), + React.createElement('div', { style: { overflowX: 'auto', marginBottom: 16 } }, + React.createElement('table', { style: { width: '100%', minWidth: 1500, borderCollapse: 'collapse', fontSize: 13 } }, + React.createElement('thead', null, + React.createElement('tr', { style: { backgroundColor: '#fafafa' } }, + React.createElement('th', { style: Object.assign({}, thBase, { width: 50 }) }, '序号'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 80 }) }, '品牌'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 90 }) }, '型号'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 100 }) }, '车牌号'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 120 }) }, '应收车辆月租金'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 130 }) }, '实收车辆月租金'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 130 }) }, '车辆租金备注'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 100 }) }, '减免金额'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 130 }) }, '减免金额备注'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 130 }) }, '减免证明'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 110 }) }, '应收车辆保证金'), + React.createElement('th', { style: Object.assign({}, thBase, { width: 90 }) }, '服务费项目'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 90 }) }, '应收服务费'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 90 }) }, '实收服务费') + ) + ), + React.createElement('tbody', null, + vehicles.map(function (row) { + var servicePopover = React.createElement('div', { style: { padding: 8, minWidth: 360 } }, + React.createElement('div', { style: { fontWeight: 600, marginBottom: 8 } }, '服务费项目'), + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 12 } }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '服务项目'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '应收费用'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '实收费用'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '减免费用'), + React.createElement('th', { style: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '备注') + ) + ), + React.createElement('tbody', null, + (row.serviceItems || []).map(function (s, si) { + return React.createElement('tr', { key: si }, + React.createElement('td', { style: { padding: '6px 8px' } }, s.name), + React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, (s.receivable != null ? s.receivable : '') + ' 元'), + React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, (s.actual != null ? s.actual : '') + ' 元'), + React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, (s.discount != null ? s.discount : '') + ' 元'), + React.createElement('td', { style: { padding: '6px 8px' } }, s.remark || '-') + ); + }) + ) + ) + ); + return React.createElement('tr', { key: row.key }, + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, row.index), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, row.brand), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, row.model), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, row.plateNo || '-'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, (row.receivableRent || 0) + ' 元'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, (row.actualRent || '') + ' 元'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', maxWidth: 130 } }, ellipsisCell(row.rentRemark, 10)), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, (row.discountAmount || '0.00') + ' 元'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', maxWidth: 130 } }, ellipsisCell(row.discountRemark, 10)), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, (row.discountProof && row.discountProof.length) ? row.discountProof.map(function (p, i) { + return React.createElement('div', { key: i, style: { fontSize: 12, marginBottom: 2 } }, + React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0, height: 'auto', fontSize: 12 }, onClick: function () { setProofPreviewFile(p); setProofPreviewVisible(true); } }, p.name || '附件') + ); + }) : '-'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, (row.receivableDeposit || 0) + ' 元'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, + React.createElement(Popover, { content: servicePopover, title: null, trigger: 'click' }, + React.createElement(Button, { type: 'link', size: 'small' }, '管理') + ) + ), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, (row.receivableService || 0) + ' 元'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, (row.actualService || '0.00') + ' 元') + ); + }) + ), + React.createElement('tfoot', null, + React.createElement('tr', { style: { backgroundColor: '#fafafa', fontWeight: 500 } }, + React.createElement('td', { style: { padding: '10px 12px', border: '1px solid #f0f0f0' }, colSpan: 4 }, '总计'), + React.createElement('td', { style: { padding: '10px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, totals.receivableRent + ' 元'), + React.createElement('td', { style: { padding: '10px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, totals.actualRent + ' 元'), + React.createElement('td', { style: { padding: '10px 12px', border: '1px solid #f0f0f0' } }), + React.createElement('td', { style: { padding: '10px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, totals.discountTotal + ' 元'), + React.createElement('td', { style: { padding: '10px 12px', border: '1px solid #f0f0f0' } }), + React.createElement('td', { style: { padding: '10px 12px', border: '1px solid #f0f0f0' } }), + React.createElement('td', { style: { padding: '10px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, totals.receivableDeposit + ' 元'), + React.createElement('td', { style: { padding: '10px 12px', border: '1px solid #f0f0f0' } }), + React.createElement('td', { style: { padding: '10px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, totals.receivableService + ' 元'), + React.createElement('td', { style: { padding: '10px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, totals.actualService + ' 元') + ) + ) + ) + ), + hasHydrogenPrepay ? React.createElement('div', { style: { marginBottom: 16 } }, + React.createElement('div', { style: { fontSize: 14, fontWeight: 500, marginBottom: 8 } }, '氢费预付款情况'), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr', gap: 16, maxWidth: 800 } }, + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '氢费预付款应收金额'), React.createElement('div', { style: valueStyle }, hydrogen.receivable + ' 元')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '氢费预付款实收金额'), React.createElement('div', { style: valueStyle }, hydrogen.actual + ' 元')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '减免金额'), React.createElement('div', { style: valueStyle }, hydrogen.discount + ' 元')), + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '减免金额备注'), React.createElement('div', { style: valueStyle }, ellipsisCell(hydrogen.discountRemark, 12))) + ) + ) : null, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px 24px', marginTop: 16 } }, + React.createElement('div', { style: formItemStyle }, React.createElement('div', { style: labelStyle }, '开票方式'), React.createElement('div', { style: valueStyle }, invoiceMethod)), + React.createElement('div', { style: Object.assign({}, formItemStyle, { gridColumn: '1 / -1' }) }, React.createElement('div', { style: labelStyle }, '开票备注'), React.createElement('div', { style: valueStyle }, invoiceRemark || '-')) + ) + ) : null + ), + React.createElement(Card, { + title: '审核情况', + style: cardStyle, + extra: React.createElement(Button, { type: 'link', size: 'small', onClick: function () { approveCardExpanded[1](!approveCardExpanded[0]); } }, approveCardExpanded[0] ? '收起' : '展开') + }, + approveCardExpanded[0] ? React.createElement(React.Fragment, null, + React.createElement(Steps, { + direction: 'vertical', + current: 1, + items: approvalSteps.map(function (s, i) { + var statusText = s.status === 'finish' ? '审批通过' : s.status === 'process' ? '待审批' : '待审批'; + var desc = React.createElement('div', { style: { fontSize: 13, color: 'rgba(0,0,0,0.65)', marginTop: 4 } }, + React.createElement('div', null, '审批状态:', statusText), + React.createElement('div', null, '审批人:', s.person || '-'), + s.time ? React.createElement('div', null, '审批时间:', s.time) : null + ); + return { + title: s.title, + description: desc, + status: s.status === 'finish' ? 'finish' : s.status === 'process' ? 'process' : 'wait' + }; + }) + }), + React.createElement('div', { style: { marginTop: 24, marginBottom: 16 } }, + React.createElement('div', { style: labelStyle }, '审批说明'), + React.createElement(Input.TextArea, { + value: approveRemark[0], + onChange: function (e) { setApproveRemark(e.target.value); }, + placeholder: '非必填,可输入审批说明', + rows: 3, + style: { width: '100%', maxWidth: 560 } }) + ), + React.createElement('div', { style: { display: 'flex', gap: 8 } }, + React.createElement(Button, { type: 'primary', onClick: handlePass }, '审批通过'), + React.createElement(Button, { danger: true, onClick: handleRejectClick }, '驳回'), + React.createElement(Button, { onClick: handleCancel }, '取消') + ) + ) : null + ), + React.createElement(Modal, { + title: '需求说明', + open: requirementModalVisible[0], + onCancel: function () { setRequirementModalVisible(false); }, + footer: React.createElement(Button, { onClick: function () { setRequirementModalVisible(false); } }, '关闭'), + width: 720 + }, + React.createElement('div', { style: { padding: '8px 0', whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6, maxHeight: '70vh', overflow: 'auto' } }, requirementContent) + ), + React.createElement(Modal, { + title: '附件预览', + open: proofPreviewVisible[0], + onCancel: function () { setProofPreviewVisible(false); setProofPreviewFile(null); }, + footer: React.createElement(Button, { onClick: function () { setProofPreviewVisible(false); setProofPreviewFile(null); } }, '关闭'), + width: 640 + }, + React.createElement('div', { style: { padding: '16px 0' } }, + React.createElement('div', { style: { marginBottom: 12, fontSize: 14, color: 'rgba(0,0,0,0.65)' } }, '文件:', proofPreviewFile[0] ? (proofPreviewFile[0].name || '附件') : ''), + React.createElement('div', { style: { minHeight: 360, background: '#fafafa', borderRadius: 4, display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'rgba(0,0,0,0.45)', fontSize: 14 } }, '此处可预览附件内容,支持 jpg、png、pdf 等格式') + ) + ), + React.createElement(Modal, { + title: '确认驳回', + open: rejectModalVisible[0], + onCancel: function () { setRejectModalVisible(false); setRejectRemark(''); }, + onOk: handleRejectConfirm, + okText: '确认驳回' + }, + React.createElement(React.Fragment, null, + React.createElement('p', { style: { marginBottom: 12 } }, '驳回后进行二次确认,驳回成功后该任务在列表中显示为审核驳回,发起人可重新编辑。'), + React.createElement('div', { style: { marginBottom: 8 } }, '驳回原因:'), + React.createElement(Input.TextArea, { + value: rejectRemark[0], + onChange: function (e) { setRejectRemark(e.target.value); }, + placeholder: '请填写驳回原因', + rows: 4, + style: { width: '100%' } }) + ) + ) + ); +}; + +if (typeof module !== 'undefined' && module.exports) module.exports = Component; + +export default Component; diff --git a/src/prototypes/oneos-web-finance/pages/08-提车应收款-提车收款单.jsx b/src/prototypes/oneos-web-finance/pages/08-提车应收款-提车收款单.jsx new file mode 100644 index 0000000..78dc359 --- /dev/null +++ b/src/prototypes/oneos-web-finance/pages/08-提车应收款-提车收款单.jsx @@ -0,0 +1,556 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 财务管理 - 提车应收款 - 提车收款单(2026年3月10日版本) + +const Component = function () { + var useState = React.useState; + var useCallback = React.useCallback; + var useMemo = React.useMemo; + var useEffect = React.useEffect; + var useRef = React.useRef; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Input = antd.Input; + var Button = antd.Button; + var Select = antd.Select; + var Table = antd.Table; + var Popover = antd.Popover; + var Tooltip = antd.Tooltip; + var Modal = antd.Modal; + var message = antd.message; + var Option = Select.Option; + + var requirementModalVisible = useState(false); + var setRequirementModalVisible = requirementModalVisible[1]; + var edited = useState(false); + var setEdited = edited[1]; + var submitConfirmVisible = useState(false); + var setSubmitConfirmVisible = submitConfirmVisible[1]; + var cancelConfirmVisible = useState(false); + var setCancelConfirmVisible = cancelConfirmVisible[1]; + var projectCardExpanded = useState(true); + var setProjectCardExpanded = projectCardExpanded[1]; + var receivableCardExpanded = useState(true); + var setReceivableCardExpanded = receivableCardExpanded[1]; + var servicePopoverRowIndex = useState(null); + var setServicePopoverRowIndex = servicePopoverRowIndex[1]; + + // 模拟:项目信息(实际由路由/接口拉取) + var projectInfo = useMemo(function () { + return { + collectCode: 'HT-ZL-2025-001TC0001', + contractCode: 'HT-ZL-2025-001', + contractType: '正式合同', + projectName: '嘉兴氢能示范项目', + customerName: '嘉兴某某物流有限公司', + paymentMethod: '预付', + paymentCycle: '6个月', + contractStart: '2025-01-15', + contractEnd: '2026-01-14', + businessDept: '业务1部', + businessPerson: '张经理' + }; + }, []); + + // 模拟:车辆应收款明细(租赁合同中所有车辆),付款周期 6 个月 => 应收月租金 = 月租金*6 + var paymentCycleMonths = 6; + var vehicleRows = useState([ + { key: 'v1', index: 1, brand: '东风', model: 'DFH1180', plateNo: '浙A12345', monthRent: 5000, receivableRent: 30000, actualRent: '30000.00', rentRemark: '', receivableDeposit: 10000, serviceItems: [{ name: '代处理费用', receivable: 200, actual: '200.00', discount: '0.00', remark: '' }, { name: '保险上浮', receivable: 500, actual: '500.00', discount: '0.00', remark: '' }], receivableService: 700, actualService: '700.00', discountAmount: '0.00', discountRemark: '', discountProof: [], selected: true, receivableCompleted: true }, + { key: 'v2', index: 2, brand: '福田', model: 'BJ1180', plateNo: '浙A23456', monthRent: 4500, receivableRent: 27000, actualRent: '27000.00', rentRemark: '', receivableDeposit: 8000, serviceItems: [{ name: '保养费用', receivable: 300, actual: '300.00', discount: '0.00', remark: '' }], receivableService: 300, actualService: '300.00', discountAmount: '0.00', discountRemark: '', discountProof: [], selected: true, receivableCompleted: true }, + { key: 'v3', index: 3, brand: '重汽', model: 'ZZ1187', plateNo: '浙A34567', monthRent: 5200, receivableRent: 31200, actualRent: '31200.00', rentRemark: '', receivableDeposit: 10000, serviceItems: [{ name: '代处理费用', receivable: 180, actual: '180.00', discount: '0.00', remark: '' }, { name: '上牌服务', receivable: 400, actual: '400.00', discount: '0.00', remark: '' }], receivableService: 580, actualService: '580.00', discountAmount: '0.00', discountRemark: '', discountProof: [], selected: true, receivableCompleted: true }, + { key: 'v4', index: 4, brand: '陕汽', model: 'SX1313', plateNo: '', monthRent: 4800, receivableRent: 28800, actualRent: '28800.00', rentRemark: '', receivableDeposit: 9000, serviceItems: [{ name: '保险上浮', receivable: 350, actual: '350.00', discount: '0.00', remark: '' }], receivableService: 350, actualService: '350.00', discountAmount: '0.00', discountRemark: '', discountProof: [], selected: false }, + { key: 'v5', index: 5, brand: '解放', model: 'J6P', plateNo: '浙A45678', monthRent: 5500, receivableRent: 33000, actualRent: '33000.00', rentRemark: '', receivableDeposit: 11000, serviceItems: [{ name: '代处理费用', receivable: 220, actual: '220.00', discount: '0.00', remark: '' }, { name: '保养费用', receivable: 280, actual: '280.00', discount: '0.00', remark: '' }], receivableService: 500, actualService: '500.00', discountAmount: '0.00', discountRemark: '', discountProof: [], selected: false }, + { key: 'v6', index: 6, brand: '江淮', model: '格尔发K5', plateNo: '浙A56789', monthRent: 4300, receivableRent: 25800, actualRent: '25800.00', rentRemark: '', receivableDeposit: 8500, serviceItems: [{ name: '保养费用', receivable: 260, actual: '260.00', discount: '0.00', remark: '' }], receivableService: 260, actualService: '260.00', discountAmount: '0.00', discountRemark: '', discountProof: [], selected: false }, + { key: 'v7', index: 7, brand: '东风', model: 'DFH1250', plateNo: '浙A67890', monthRent: 6000, receivableRent: 36000, actualRent: '36000.00', rentRemark: '', receivableDeposit: 12000, serviceItems: [{ name: '代处理费用', receivable: 250, actual: '250.00', discount: '0.00', remark: '' }, { name: '保险上浮', receivable: 600, actual: '600.00', discount: '0.00', remark: '' }], receivableService: 850, actualService: '850.00', discountAmount: '0.00', discountRemark: '', discountProof: [], selected: false }, + { key: 'v8', index: 8, brand: '福田', model: '欧曼EST', plateNo: '', monthRent: 4700, receivableRent: 28200, actualRent: '28200.00', rentRemark: '', receivableDeposit: 9000, serviceItems: [{ name: '上牌服务', receivable: 380, actual: '380.00', discount: '0.00', remark: '' }], receivableService: 380, actualService: '380.00', discountAmount: '0.00', discountRemark: '', discountProof: [], selected: false }, + { key: 'v9', index: 9, brand: '重汽', model: '豪沃T7H', plateNo: '浙A78901', monthRent: 5300, receivableRent: 31800, actualRent: '31800.00', rentRemark: '', receivableDeposit: 10500, serviceItems: [{ name: '代处理费用', receivable: 200, actual: '200.00', discount: '0.00', remark: '' }, { name: '保养费用', receivable: 320, actual: '320.00', discount: '0.00', remark: '' }], receivableService: 520, actualService: '520.00', discountAmount: '0.00', discountRemark: '', discountProof: [], selected: false }, + { key: 'v10', index: 10, brand: '陕汽', model: '德龙X3000', plateNo: '浙A89012', monthRent: 5100, receivableRent: 30600, actualRent: '30600.00', rentRemark: '', receivableDeposit: 10000, serviceItems: [{ name: '保险上浮', receivable: 450, actual: '450.00', discount: '0.00', remark: '' }], receivableService: 450, actualService: '450.00', discountAmount: '0.00', discountRemark: '', discountProof: [], selected: false }, + { key: 'v11', index: 11, brand: '解放', model: 'JH6', plateNo: '浙A90123', monthRent: 5400, receivableRent: 32400, actualRent: '32400.00', rentRemark: '', receivableDeposit: 10800, serviceItems: [{ name: '代处理费用', receivable: 210, actual: '210.00', discount: '0.00', remark: '' }, { name: '上牌服务', receivable: 420, actual: '420.00', discount: '0.00', remark: '' }], receivableService: 630, actualService: '630.00', discountAmount: '0.00', discountRemark: '', discountProof: [], selected: false }, + { key: 'v12', index: 12, brand: '江淮', model: '帅铃', plateNo: '', monthRent: 4000, receivableRent: 24000, actualRent: '24000.00', rentRemark: '', receivableDeposit: 8000, serviceItems: [{ name: '保养费用', receivable: 240, actual: '240.00', discount: '0.00', remark: '' }], receivableService: 240, actualService: '240.00', discountAmount: '0.00', discountRemark: '', discountProof: [], selected: false }, + { key: 'v13', index: 13, brand: '东风', model: 'DFH1180', plateNo: '浙B12345', monthRent: 5000, receivableRent: 30000, actualRent: '30000.00', rentRemark: '', receivableDeposit: 10000, serviceItems: [{ name: '代处理费用', receivable: 200, actual: '200.00', discount: '0.00', remark: '' }], receivableService: 200, actualService: '200.00', discountAmount: '500.00', discountRemark: '首月优惠', discountProof: [], selected: false }, + { key: 'v14', index: 14, brand: '福田', model: 'BJ1180', plateNo: '浙B23456', monthRent: 4600, receivableRent: 27600, actualRent: '27600.00', rentRemark: '', receivableDeposit: 9200, serviceItems: [{ name: '保险上浮', receivable: 380, actual: '380.00', discount: '0.00', remark: '' }], receivableService: 380, actualService: '380.00', discountAmount: '0.00', discountRemark: '', discountProof: [], selected: false }, + { key: 'v15', index: 15, brand: '重汽', model: 'ZZ1187', plateNo: '浙B34567', monthRent: 4900, receivableRent: 29400, actualRent: '29400.00', rentRemark: '', receivableDeposit: 9800, serviceItems: [{ name: '代处理费用', receivable: 190, actual: '190.00', discount: '0.00', remark: '' }, { name: '保养费用', receivable: 310, actual: '310.00', discount: '0.00', remark: '' }], receivableService: 500, actualService: '500.00', discountAmount: '0.00', discountRemark: '', discountProof: [], selected: false }, + { key: 'v16', index: 16, brand: '陕汽', model: 'SX1313', plateNo: '', monthRent: 4750, receivableRent: 28500, actualRent: '28500.00', rentRemark: '', receivableDeposit: 9500, serviceItems: [{ name: '上牌服务', receivable: 360, actual: '360.00', discount: '0.00', remark: '' }], receivableService: 360, actualService: '360.00', discountAmount: '0.00', discountRemark: '', discountProof: [], selected: false }, + { key: 'v17', index: 17, brand: '解放', model: 'J6P', plateNo: '浙B45678', monthRent: 5600, receivableRent: 33600, actualRent: '33600.00', rentRemark: '', receivableDeposit: 11200, serviceItems: [{ name: '代处理费用', receivable: 230, actual: '230.00', discount: '0.00', remark: '' }, { name: '保险上浮', receivable: 550, actual: '550.00', discount: '0.00', remark: '' }], receivableService: 780, actualService: '780.00', discountAmount: '0.00', discountRemark: '', discountProof: [], selected: false }, + { key: 'v18', index: 18, brand: '江淮', model: '格尔发K5', plateNo: '浙B56789', monthRent: 4400, receivableRent: 26400, actualRent: '26400.00', rentRemark: '', receivableDeposit: 8800, serviceItems: [{ name: '保养费用', receivable: 270, actual: '270.00', discount: '0.00', remark: '' }], receivableService: 270, actualService: '270.00', discountAmount: '0.00', discountRemark: '', discountProof: [], selected: false }, + { key: 'v19', index: 19, brand: '东风', model: 'DFH1250', plateNo: '浙B67890', monthRent: 5800, receivableRent: 34800, actualRent: '34800.00', rentRemark: '', receivableDeposit: 11600, serviceItems: [{ name: '代处理费用', receivable: 240, actual: '240.00', discount: '0.00', remark: '' }], receivableService: 240, actualService: '240.00', discountAmount: '200.00', discountRemark: '客户协商', discountProof: [], selected: false }, + { key: 'v20', index: 20, brand: '福田', model: '欧曼EST', plateNo: '浙B78901', monthRent: 4650, receivableRent: 27900, actualRent: '27900.00', rentRemark: '', receivableDeposit: 9300, serviceItems: [{ name: '上牌服务', receivable: 390, actual: '390.00', discount: '0.00', remark: '' }, { name: '保养费用', receivable: 290, actual: '290.00', discount: '0.00', remark: '' }], receivableService: 680, actualService: '680.00', discountAmount: '0.00', discountRemark: '', discountProof: [], selected: false } + ]); + var vehicles = vehicleRows[0]; + var setVehicles = vehicleRows[1]; + + // 氢费预付款(非预付款模式时不显示) + var hasHydrogenPrepay = true; + var hydrogenRow = useState({ + receivable: '3580.00', + actual: '3580.00', + discount: '0.00', + discountRemark: '' + }); + var hydrogen = hydrogenRow[0]; + var setHydrogen = hydrogenRow[1]; + + var invoiceMethod = useState('先开票后付款'); + var setInvoiceMethod = invoiceMethod[1]; + var invoiceRemark = useState(''); + var setInvoiceRemark = invoiceRemark[1]; + + var toggleVehicleSelected = useCallback(function (key) { + setEdited(true); + setVehicles(function (prev) { + return prev.map(function (r) { + if (r.key !== key || r.receivableCompleted) return r; + return Object.assign({}, r, { selected: !r.selected }); + }); + }); + }, []); + + var toggleAllSelected = useCallback(function (checked) { + setEdited(true); + setVehicles(function (prev) { + return prev.map(function (r) { + if (r.receivableCompleted) return r; + return Object.assign({}, r, { selected: !!checked }); + }); + }); + }, []); + + var updateVehicle = useCallback(function (key, field, value) { + setEdited(true); + setVehicles(function (prev) { + return prev.map(function (r) { + if (r.key !== key) return r; + var next = Object.assign({}, r); + next[field] = value; + if (field === 'serviceItems') { + var total = 0; + (value || []).forEach(function (s) { total += parseFloat(s.actual) || 0; }); + next.actualService = total.toFixed(2); + } + return next; + }); + }); + }, []); + + var updateServiceItem = useCallback(function (vehicleKey, itemIndex, field, value) { + setEdited(true); + setVehicles(function (prev) { + return prev.map(function (r) { + if (r.key !== vehicleKey) return r; + var items = (r.serviceItems || []).slice(); + if (!items[itemIndex]) return r; + items[itemIndex] = Object.assign({}, items[itemIndex], { [field]: value }); + var total = 0; + items.forEach(function (s) { total += parseFloat(s.actual) || 0; }); + return Object.assign({}, r, { serviceItems: items, actualService: total.toFixed(2) }); + }); + }); + }, []); + + var totals = useMemo(function () { + // 只计算已选中且未完成提车应收款的车辆 + var selected = vehicles.filter(function (v) { return v.selected && !v.receivableCompleted; }); + var receivableRent = 0, actualRent = 0, receivableDeposit = 0, receivableService = 0, actualService = 0, discountTotal = 0; + selected.forEach(function (v) { + receivableRent += Number(v.receivableRent) || 0; + actualRent += parseFloat(v.actualRent) || 0; + receivableDeposit += Number(v.receivableDeposit) || 0; + receivableService += Number(v.receivableService) || 0; + actualService += parseFloat(v.actualService) || 0; + discountTotal += parseFloat(v.discountAmount) || 0; + }); + return { + receivableRent: receivableRent.toFixed(2), + actualRent: actualRent.toFixed(2), + receivableDeposit: receivableDeposit.toFixed(2), + receivableService: receivableService.toFixed(2), + actualService: actualService.toFixed(2), + discountTotal: discountTotal.toFixed(2) + }; + }, [vehicles]); + + var hydrogenReceivable = hasHydrogenPrepay ? (parseFloat(hydrogen.receivable) || 0) : 0; + var hydrogenActual = hasHydrogenPrepay ? (parseFloat(hydrogen.actual) || 0) : 0; + + var receivableTotal = useMemo(function () { + var base = parseFloat(totals.receivableRent) + parseFloat(totals.receivableDeposit) + parseFloat(totals.receivableService); + return (base + hydrogenReceivable).toFixed(2); + }, [totals, hydrogenReceivable]); + + var actualTotal = useMemo(function () { + var base = parseFloat(totals.actualRent) + parseFloat(totals.receivableDeposit) + parseFloat(totals.actualService) - parseFloat(totals.discountTotal); + return (base + hydrogenActual).toFixed(2); + }, [totals, hydrogenActual]); + + var invoiceTotal = useMemo(function () { + var base = parseFloat(totals.actualRent) + parseFloat(totals.actualService) - parseFloat(totals.discountTotal); + return (base + hydrogenActual).toFixed(2); + }, [totals, hydrogenActual]); + + var receivablePopoverContent = useMemo(function () { + var rows = [ + React.createElement('tr', { key: 'rent' }, React.createElement('td', { style: { padding: '6px 8px' } }, '总计应收车辆月租金'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.receivableRent + ' 元')), + React.createElement('tr', { key: 'deposit' }, React.createElement('td', { style: { padding: '6px 8px' } }, '总计应收车辆保证金'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.receivableDeposit + ' 元')), + React.createElement('tr', { key: 'service' }, React.createElement('td', { style: { padding: '6px 8px' } }, '总计应收服务费'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.receivableService + ' 元')) + ]; + if (hasHydrogenPrepay) { + rows.push(React.createElement('tr', { key: 'hydrogen' }, React.createElement('td', { style: { padding: '6px 8px' } }, '氢费预充值应收金额'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, (hydrogen.receivable || '0.00') + ' 元'))); + } + return React.createElement('div', { style: { padding: 8, minWidth: 220 } }, + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 13 } }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '项目'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '金额') + ) + ), + React.createElement('tbody', null, rows) + ) + ); + }, [totals, hasHydrogenPrepay, hydrogen.receivable]); + + var actualPopoverContent = useMemo(function () { + var rows = [ + React.createElement('tr', { key: 'rent' }, React.createElement('td', { style: { padding: '6px 8px' } }, '总计实收车辆月租金'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.actualRent + ' 元')), + React.createElement('tr', { key: 'deposit' }, React.createElement('td', { style: { padding: '6px 8px' } }, '总计应收车辆保证金'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.receivableDeposit + ' 元')), + React.createElement('tr', { key: 'service' }, React.createElement('td', { style: { padding: '6px 8px' } }, '总计实收服务费'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.actualService + ' 元')), + React.createElement('tr', { key: 'discount' }, React.createElement('td', { style: { padding: '6px 8px' } }, '总计减免金额'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, totals.discountTotal + ' 元')) + ]; + if (hasHydrogenPrepay) { + rows.push(React.createElement('tr', { key: 'hydrogen' }, React.createElement('td', { style: { padding: '6px 8px' } }, '氢费预充值实收金额'), React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, (hydrogen.actual || '0.00') + ' 元'))); + } + return React.createElement('div', { style: { padding: 8, minWidth: 220 } }, + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 13 } }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '项目'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '金额') + ) + ), + React.createElement('tbody', null, rows) + ) + ); + }, [totals, hasHydrogenPrepay, hydrogen.actual]); + + var handleSubmit = useCallback(function () { + setSubmitConfirmVisible(true); + }, []); + + var handleSubmitConfirm = useCallback(function () { + message.success('提车应收款已提交审核'); + setSubmitConfirmVisible(false); + if (window.__receivableSubmit) window.__receivableSubmit(); + }, []); + + var handleSave = useCallback(function () { + message.success('保存成功'); + if (window.__receivableBack) window.__receivableBack(); else message.info('跳转至提车应收款列表页(原型)'); + }, []); + + var handleCancel = useCallback(function () { + if (edited[0]) { + setCancelConfirmVisible(true); + } else { + if (window.__receivableBack) window.__receivableBack(); else message.info('返回提车应收款列表(原型)'); + } + }, [edited[0]]); + + var handleCancelConfirm = useCallback(function () { + setCancelConfirmVisible(false); + if (window.__receivableBack) window.__receivableBack(); else message.info('返回提车应收款列表(原型)'); + }, []); + + var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; + var labelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var requiredStar = { color: '#ff4d4f', marginRight: 2 }; + var formRowStyle = { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px 24px', marginBottom: 16 }; + var formItemStyle = { marginBottom: 12 }; + var highlightStyle = { color: '#1890ff', fontWeight: 600, cursor: 'pointer' }; + var valueStyle = { color: 'rgba(0,0,0,0.85)', fontSize: 14, lineHeight: '22px', minHeight: 22 }; + var thBase = { padding: '10px 12px', border: '1px solid #f0f0f0', whiteSpace: 'nowrap' }; + + var requirementContent = '提车应收款-提车收款单(2026年3月10日版本)\n一个「数字化资产ONEOS运管平台」中的「财务管理」「提车应收款」「提车收款单」模块\n#面包屑:财务管理-提车应收款-提车收款单;\n\n页面分为3个卡片,业务流程请参考流程图;\n1.项目信息:\n#显示项目详细信息,包括:\n1.1.合同编码:显示该租赁合同对应合同编码;\n1.2.合同类型:显示该租赁合同对应合同类型;\n1.3.项目名称:显示该租赁合同对应项目名称;\n1.4.客户名称:显示该租赁合同对应客户名称;\n1.5.付款方式:显示该租赁合同对应付款方式,分为:预付、后付两种,根据实际合同反写;\n1.6.付款周期:显示该租赁合同对应付款周期,格式为:x个月;\n1.7.合同生效时间:显示该租赁合同对应合同生效时间;\n1.8.合同结束时间:显示该租赁合同对应合同结束时间;\n1.9.业务部门:显示该租赁合同对应业务部门名称;\n1.10.业务负责人:显示该租赁合同对应业务负责人;\n\n2.提车应收款信息:\n#上方显示应收款总额、实收款总额;\n2.1.应收款总额:格式为:xx.xx元,重点色,计算方式为:「选中车辆应收车辆月租金总和」+「选中车辆应收车辆保证金总和」+「选中车辆应收服务费总和」+「氢费预充值应收金额」;\n 点击应收款总额金额,弹出气泡卡片,气泡卡片列表展示:项目、金额;\n 2.1.1.项目:包括总计应收车辆月租金、总计应收车辆保证金、总计应收服务费三项;\n 2.1.2.金额:显示该项目对应应收金额;\n2.2.实收款总额:格式为:xx.xx元,重点色,计算方式为:「选中车辆实收车辆月租金总和」+「选中车辆应收车辆保证金总和」+「选中车辆实收服务费总和」+「氢费预充值实收金额」-「选中车辆减免金额总和」;\n 点击实收款总额金额,弹出气泡卡片,气泡卡片列表展示:项目、金额;\n 2.2.1.项目:包括总计实收车辆月租金、总计应收车辆保证金、总计实收服务费、总计减免金额四项;\n 2.2.2.金额:显示该项目对应实收金额;\n2.3.应开票总额:格式为:xx.xx元,计算方式为:「选中车辆实收车辆月租金总和」+「选中车辆实收服务费总和」-「选中车辆减免金额总和」;\n\n#中间为车辆应收款明细,以列表展示租赁合同中所有车辆明细,包括以下字段:\n2.1.全选/多选:多选按钮组,支持表头点击全选,默认为取消勾选(整个车辆清单);\n 选择代表本次提车应收款只收取该部分车辆费用,可以编辑以下相应字段,不选择则代表本次提车应收款不收取该部分车辆费用,不能编辑车辆相应字段;\n 提车应收款支持分批次进行收款,如果该车辆之前已提交提车应收款流程,则多选框不能勾选,悬浮多选框时提示:该车辆已完成提车应收款/该车辆已提交提车收款单;\n2.2.序号:根据租赁合同中车辆对应序号顺序展示;\n2.3.品牌:显示租赁合同中车辆对应品牌;\n2.4.型号:显示租赁合同中车辆对应型号;\n2.5.车牌号:显示租赁合同中车辆对应车牌号,如果车牌号为空则显示为-(后续如果交车成功,则车牌号会反显在该处);\n2.6.应收车辆月租金:根据租赁合同付款周期自动计算,例如:付款周期为6个月,则这里显示金额为:车辆月租金*6;\n2.7.实收车辆月租金:必填项,输入框,默认反写实收车辆月租金,支持修改,由业务员自行输入车辆月租金金额,精确至2位小数,后缀为元;\n2.8.车辆租金备注:选填项,输入框,由业务员自行输入;\n2.9.减免金额:选填项,输入框,由业务员自行输入减免金额,默认为0.00,支持2位小数,后缀为元;\n2.10.减免金额备注:选填项,输入框,由业务员自行输入备注信息;\n2.11.应收车辆保证金:显示租赁合同中当前车辆保证金金额;\n2.12.服务费项目:点击管理,弹出气泡卡片,气泡卡片标题为服务费项目,下方为列表,显示服务项目、应收费用、实收费用、备注;\n 2.12.1.服务项目:显示租赁合同中所有服务项目名称;\n 2.12.2.应收费用:显示租赁合同中所有服务项目对应费用;\n 2.12.3.实收费用:必填项,输入框,默认反写应收费用,支持修改,由业务员自行输入实收费用金额,精确至2位小数,后缀为元;\n 2.12.4.减免费用:选填项,输入框,默认为:0.00,精确至2位小数,后缀为元;\n 2.12.4.备注:选填项,输入框,由业务员自行输入备注信息;\n2.13.应收服务费:显示当前车辆所有服务费应收费用总和,格式为:xx.xx元;\n2.14.实收服务费:显示当前车辆所有服务费实收费用总和,格式为:xx.xx元;\n2.15.减免金额:选填项,输入框,由业务员自行输入减免金额,默认为0.00,支持2位小数,后缀为元;\n2.16.减免金额备注:选填项,输入框,由业务员自行输入备注信息;\n2.17.减免证明:选填项,附件上传按钮,点击后上传本地文件,支持:jpg、png、pdf等格式,支持多附件上传;\n\n列表不支持分页功能;\n#下方为氢费预付款情况,以列表展示租赁合同中所有氢费预付款明细,包括以下字段,如租赁合同氢费非预付款模式,则不显示该部分内容:\n2.19.氢费预付款应收金额:显示租赁合同中氢费预付款金额,格式为:xx.xx元,支持2位小数;\n2.20.氢费预付款实收金额:必填项,输入框,默认反写氢费预付款应收金额,支持修改,由业务员自行输入金额,格式为:xx.xx元,支持2位小数;\n2.21.减免金额:选填项,输入框,由业务员自行输入减免金额,默认为:0.00元,格式为:xx.xx元,支持2位小数;\n2.22.减免金额备注:选填项,输入框,由业务员自行输入减免金额备注信息;\n#最底部为开票方式、开票备注;\n2.23.开票方式:选择器,选项包括:先开票后付款、先付款后开票,默认为先开票后付款;\n2.24.开票备注:必填项,文本域,默认提示文本为:请输入开票项目、税率以及其他备注信息,财务将以此进行开票;\n\n3.底部为提交审核,保存,取消;\n3.1.提交审核:点击提交审核,进行二次确认,文案为:请仔细核对提车首付款实收金额,点击确认则进入工作流;\n3.2.保存:点击保存,提示保存成功,跳转至提车应收款列表页,同时该条数据审核状态为:待提交:\n3.3.取消:点击取消,进行二次确认,文案为:取消将会丢失所有已填写数据,是否确认,点击确认则跳转至提车应收款列表页;'; + + var selectableVehicles = vehicles.filter(function (v) { return !v.receivableCompleted; }); + var allSelected = selectableVehicles.length > 0 && selectableVehicles.every(function (v) { return v.selected; }); + var indeterminate = selectableVehicles.some(function (v) { return v.selected; }) && !allSelected; + var headerCheckRef = useRef(null); + useEffect(function () { + if (headerCheckRef.current) headerCheckRef.current.indeterminate = indeterminate; + }, [indeterminate]); + + return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + React.createElement(Breadcrumb, { + items: [ + { title: '财务管理' }, + { title: '提车应收款' }, + { title: '提车收款单' } + ] + }), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { setRequirementModalVisible(true); } }, '查看需求说明') + ), + React.createElement(Card, { + title: '项目信息', + style: cardStyle, + extra: React.createElement(Button, { type: 'link', size: 'small', onClick: function () { setProjectCardExpanded(!projectCardExpanded[0]); } }, projectCardExpanded[0] ? '收起' : '展开') + }, + projectCardExpanded[0] ? React.createElement('div', { style: formRowStyle }, + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '提车收款单编码'), + React.createElement('div', { style: valueStyle }, projectInfo.collectCode || '-') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '合同编码'), + React.createElement('div', { style: valueStyle }, projectInfo.contractCode || '-') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '合同类型'), + React.createElement('div', { style: valueStyle }, projectInfo.contractType || '-') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '项目名称'), + React.createElement('div', { style: valueStyle }, projectInfo.projectName || '-') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '客户名称'), + React.createElement('div', { style: valueStyle }, projectInfo.customerName || '-') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '付款方式'), + React.createElement('div', { style: valueStyle }, projectInfo.paymentMethod || '-') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '付款周期'), + React.createElement('div', { style: valueStyle }, projectInfo.paymentCycle || '-') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '合同生效时间'), + React.createElement('div', { style: valueStyle }, projectInfo.contractStart || '-') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '合同结束时间'), + React.createElement('div', { style: valueStyle }, projectInfo.contractEnd || '-') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '业务部门'), + React.createElement('div', { style: valueStyle }, projectInfo.businessDept || '-') + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '业务负责人'), + React.createElement('div', { style: valueStyle }, projectInfo.businessPerson || '-') + ) + ) : null + ), + React.createElement(Card, { + title: '提车应收款信息', + style: cardStyle, + extra: React.createElement(Button, { type: 'link', size: 'small', onClick: function () { setReceivableCardExpanded(!receivableCardExpanded[0]); } }, receivableCardExpanded[0] ? '收起' : '展开') + }, + receivableCardExpanded[0] ? React.createElement(React.Fragment, null, + React.createElement('div', { style: { marginBottom: 16, display: 'flex', gap: 24, alignItems: 'center', flexWrap: 'wrap' } }, + React.createElement(Popover, { content: receivablePopoverContent, title: '应收款明细', trigger: 'click' }, + React.createElement('span', { style: { cursor: 'pointer' } }, '应收款总额:', React.createElement('span', { style: highlightStyle }, receivableTotal, ' 元')) + ), + React.createElement(Popover, { content: actualPopoverContent, title: '实收款明细', trigger: 'click' }, + React.createElement('span', { style: { cursor: 'pointer' } }, '实收款总额:', React.createElement('span', { style: highlightStyle }, actualTotal, ' 元')) + ), + React.createElement('span', null, '应开票总额:', React.createElement('span', { style: highlightStyle }, invoiceTotal, ' 元')) + ), + React.createElement('div', { style: { overflowX: 'auto', marginBottom: 16, overflowY: 'visible' } }, + React.createElement('table', { style: { width: '100%', minWidth: 1600, borderCollapse: 'collapse', fontSize: 13, tableLayout: 'fixed' } }, + React.createElement('thead', null, + React.createElement('tr', { style: { backgroundColor: '#fafafa' } }, + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 48 }) }, + React.createElement('input', { type: 'checkbox', checked: allSelected, ref: headerCheckRef, onChange: function (e) { toggleAllSelected(e.target.checked); } }) + ), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 50 }) }, '序号'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 80 }) }, '品牌'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 90 }) }, '型号'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 100 }) }, '车牌号'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 120 }) }, '应收车辆月租金'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 130 }) }, React.createElement('span', null, React.createElement('span', { style: requiredStar }, '*'), '实收车辆月租金')), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 130 }) }, '车辆租金备注'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 130 }) }, '减免金额'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 130 }) }, '减免金额备注'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 130 }) }, '减免证明'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 110 }) }, '应收车辆保证金'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'left', width: 90 }) }, '服务费项目'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 90 }) }, '应收服务费'), + React.createElement('th', { style: Object.assign({}, thBase, { textAlign: 'right', width: 130 }) }, '实收服务费') + ) + ), + React.createElement('tbody', null, + vehicles.map(function (row) { + var disabled = !row.selected || row.receivableCompleted; + var servicePopover = React.createElement('div', { style: { padding: 8, minWidth: 360 } }, + React.createElement('div', { style: { fontWeight: 600, marginBottom: 8 } }, '服务费项目'), + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 12 } }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '服务项目'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '应收费用'), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, React.createElement('span', null, React.createElement('span', { style: requiredStar }, '*'), '实收费用')), + React.createElement('th', { style: { textAlign: 'right', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '减免费用'), + React.createElement('th', { style: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #f0f0f0' } }, '备注') + ) + ), + React.createElement('tbody', null, + (row.serviceItems || []).map(function (s, si) { + return React.createElement('tr', { key: si }, + React.createElement('td', { style: { padding: '6px 8px' } }, s.name), + React.createElement('td', { style: { textAlign: 'right', padding: '6px 8px' } }, (s.receivable != null ? s.receivable : '') + ' 元'), + React.createElement('td', { style: { padding: '6px 8px' } }, + React.createElement(Input, { size: 'small', value: s.actual, disabled: disabled, suffix: '元', onChange: function (e) { updateServiceItem(row.key, si, 'actual', e.target.value); } }) + ), + React.createElement('td', { style: { padding: '6px 8px' } }, + React.createElement(Input, { size: 'small', value: s.discount, disabled: disabled, suffix: '元', onChange: function (e) { updateServiceItem(row.key, si, 'discount', e.target.value); } }) + ), + React.createElement('td', { style: { padding: '6px 8px' } }, + React.createElement(Input, { size: 'small', value: s.remark, disabled: disabled, placeholder: '备注', onChange: function (e) { updateServiceItem(row.key, si, 'remark', e.target.value); } }) + ) + ); + }) + ) + ) + ); + var proofList = row.discountProof || []; + var onProofUpload = function (e) { + var files = e.target.files; + if (!files || !files.length) return; + setEdited(true); + var next = proofList.slice(); + for (var i = 0; i < files.length; i++) { next.push({ name: files[i].name }); } + updateVehicle(row.key, 'discountProof', next); + e.target.value = ''; + }; + var removeProof = function (idx) { + setEdited(true); + var next = (row.discountProof || []).slice(); + next.splice(idx, 1); + updateVehicle(row.key, 'discountProof', next); + }; + var checkboxCell = row.receivableCompleted + ? React.createElement(Tooltip, { title: '该车辆已完成提车应收款' }, + React.createElement('span', { style: { display: 'inline-block', cursor: 'not-allowed' } }, + React.createElement('input', { type: 'checkbox', checked: true, disabled: true }) + ) + ) + : React.createElement('input', { type: 'checkbox', checked: row.selected, onChange: function () { toggleVehicleSelected(row.key); } }); + return React.createElement('tr', { key: row.key }, + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, checkboxCell), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, row.index), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, row.brand), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, row.model), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, row.plateNo || '-'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, (row.receivableRent || 0) + ' 元'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', width: 130 } }, + React.createElement(Input, { size: 'small', value: row.actualRent, disabled: disabled, suffix: '元', onChange: function (e) { updateVehicle(row.key, 'actualRent', e.target.value); } }) + ), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', width: 130 } }, + React.createElement(Input, { size: 'small', value: row.rentRemark, disabled: disabled, onChange: function (e) { updateVehicle(row.key, 'rentRemark', e.target.value); } }) + ), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', width: 130 } }, + React.createElement(Input, { size: 'small', value: row.discountAmount, disabled: disabled, suffix: '元', onChange: function (e) { updateVehicle(row.key, 'discountAmount', e.target.value); } }) + ), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', width: 130 } }, + React.createElement(Input, { size: 'small', value: row.discountRemark, disabled: disabled, onChange: function (e) { updateVehicle(row.key, 'discountRemark', e.target.value); } }) + ), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', width: 130 } }, + disabled ? '-' : React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } }, + proofList.map(function (p, pidx) { + return React.createElement('div', { key: pidx, style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 12 } }, + React.createElement('span', { style: { flex: 1, overflow: 'hidden', textOverflow: 'ellipsis' } }, p.name || '附件'), + React.createElement(Button, { type: 'link', size: 'small', danger: true, style: { padding: 0, minWidth: 'auto' }, onClick: function () { removeProof(pidx); } }, '删除') + ); + }), + React.createElement('span', null, + React.createElement('input', { type: 'file', multiple: true, accept: '.jpg,.jpeg,.png,.pdf', style: { display: 'none' }, id: 'proof-' + row.key, onChange: onProofUpload }), + React.createElement(Button, { type: 'default', size: 'small', onClick: function () { var el = document.getElementById('proof-' + row.key); if (el) el.click(); } }, '附件上传') + ) + ) + ), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, (row.receivableDeposit || 0) + ' 元'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0' } }, + React.createElement(Popover, { content: servicePopover, title: null, trigger: 'click' }, + React.createElement(Button, { type: 'link', size: 'small', disabled: disabled }, '管理') + ) + ), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', textAlign: 'right' } }, (row.receivableService || 0) + ' 元'), + React.createElement('td', { style: { padding: '8px 12px', border: '1px solid #f0f0f0', textAlign: 'right', width: 130 } }, (row.actualService || '0.00') + ' 元') + ); + }) + ) + ) + ), + hasHydrogenPrepay ? React.createElement('div', { style: { marginBottom: 16 } }, + React.createElement('div', { style: { fontSize: 14, fontWeight: 500, marginBottom: 8 } }, '氢费预付款情况'), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr', gap: 16, maxWidth: 800 } }, + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '氢费预付款应收金额'), + React.createElement(Input, { value: hydrogen.receivable + ' 元', disabled: true }) + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, React.createElement('span', { style: requiredStar }, '*'), '氢费预付款实收金额'), + React.createElement(Input, { value: hydrogen.actual, suffix: '元', onChange: function (e) { setHydrogen(function (p) { return Object.assign({}, p, { actual: e.target.value }); }); } }) + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '减免金额'), + React.createElement(Input, { value: hydrogen.discount, suffix: '元', onChange: function (e) { setHydrogen(function (p) { return Object.assign({}, p, { discount: e.target.value }); }); } }) + ), + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '减免金额备注'), + React.createElement(Input, { value: hydrogen.discountRemark, onChange: function (e) { setHydrogen(function (p) { return Object.assign({}, p, { discountRemark: e.target.value }); }); } }) + ) + ) + ) : null, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px 24px', marginTop: 16 } }, + React.createElement('div', { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '开票方式'), + React.createElement(Select, { style: { width: '100%' }, value: invoiceMethod[0], onChange: function (v) { setInvoiceMethod(v); }, placeholder: '请选择' }, + React.createElement(Option, { value: '先开票后付款' }, '先开票后付款'), + React.createElement(Option, { value: '先付款后开票' }, '先付款后开票') + ) + ), + React.createElement('div', { style: Object.assign({}, formItemStyle, { gridColumn: '1 / -1' }) }, + React.createElement('div', { style: labelStyle }, React.createElement('span', { style: requiredStar }, '*'), '开票备注'), + React.createElement(Input.TextArea, { value: invoiceRemark[0], onChange: function (e) { setInvoiceRemark(e.target.value); setEdited(true); }, rows: 3, placeholder: '请输入开票项目、税率以及其他备注信息,财务将以此进行开票', style: { width: '100%' } }) + ) + ) + ) : null + ), + React.createElement('div', { style: { display: 'flex', gap: 8, marginTop: 24 } }, + React.createElement(Button, { type: 'primary', onClick: handleSubmit }, '提交审核'), + React.createElement(Button, { onClick: handleSave }, '保存'), + React.createElement(Button, { onClick: handleCancel }, '取消') + ), + React.createElement(Modal, { + title: '确认', + open: submitConfirmVisible[0], + onCancel: function () { setSubmitConfirmVisible(false); }, + onOk: handleSubmitConfirm, + okText: '确认', + cancelText: '取消' + }, React.createElement('div', null, '请仔细核对提车首付款实收金额,点击确认则进入工作流')), + React.createElement(Modal, { + title: '确认取消', + open: cancelConfirmVisible[0], + onCancel: function () { setCancelConfirmVisible(false); }, + onOk: handleCancelConfirm, + okText: '确认', + cancelText: '取消' + }, React.createElement('div', null, '取消将会丢失所有已填写数据,是否确认?')), + React.createElement(Modal, { + title: '需求说明', + open: requirementModalVisible[0], + onCancel: function () { setRequirementModalVisible(false); }, + width: 720, + footer: React.createElement(Button, { onClick: function () { setRequirementModalVisible(false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, React.createElement('div', { style: { padding: '8px 0', whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6 } }, requirementContent)) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-finance/pages/09-提车应收款.jsx b/src/prototypes/oneos-web-finance/pages/09-提车应收款.jsx new file mode 100644 index 0000000..c12cd8f --- /dev/null +++ b/src/prototypes/oneos-web-finance/pages/09-提车应收款.jsx @@ -0,0 +1,454 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 财务管理 - 提车应收款(2026年3月5日版本) + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Table = antd.Table; + var Button = antd.Button; + var Select = antd.Select; + var Input = antd.Input; + var Space = antd.Space; + var Popover = antd.Popover; + var Tag = antd.Tag; + var Modal = antd.Modal; + var message = antd.message; + + // 筛选条件 + var filterContractCode = useState(undefined); + var filterProjectName = useState(undefined); + var filterCustomerName = useState(undefined); + var filterBusinessDept = useState(undefined); + var filterBusinessPerson = useState(undefined); + // 主表展开行:只允许同时展开一行,展开新的则收起其他 + var expandedRowKeysState = useState([]); + var expandedRowKeys = expandedRowKeysState[0]; + var setExpandedRowKeys = expandedRowKeysState[1]; + var deleteConfirmVisible = useState(false); + var deleteConfirmRecord = useState(null); + var deleteConfirmParentRecord = useState(null); + var requirementModalVisible = useState(false); + var filterExpanded = useState(false); + + // 主表数据:按合同编码聚合,每条主表下有多条提车应收款明细(子表) + var mainListData = [ + { + contractCode: 'HT-ZL-2025-001', + contractType: '正式合同', + projectName: '嘉兴氢能示范项目', + customerName: '嘉兴某某物流有限公司', + businessDept: '业务1部', + businessPerson: '张经理', + contractEffectiveDate: '2025-01-15', + allVehiclesReceivableCompleted: false, + totalReceivable: 256800.00, + totalActual: 255700.00, + totalDiscount: 1100.00, + totalFinanceReceived: 209900.00, + children: [ + { seq: 1, auditStatus: '审批通过', creator: '张三', chargeTime: '2026-02-20 10:00', deliveryCount: 3, deliveryVehicles: [{ brand: '东风', model: 'DFH1180', plateNo: '浙A12345' }, { brand: '福田', model: 'BJ1180', plateNo: '浙A23456' }, { brand: '重汽', model: 'ZZ1187', plateNo: '浙A34567' }], receivableTotal: 98700.00, actualTotal: 98200.00, discountTotal: 500.00, discountProof: '首月优惠审批单', arrivalAmount: 98200.00, arrivalTime: '2026-02-21 14:30', financeReceived: 98200.00, isInvoiced: '已开票', invoiceMethod: '先开票后付款', invoiceTime: '2026-02-22 11:00', invoicedAmount: 98200.00, invoiceAttachment: '发票-HT-ZL-2025-001-001.pdf' }, + { seq: 2, auditStatus: '待审批', creator: '李四', chargeTime: '2026-03-01 09:30', deliveryCount: 2, deliveryVehicles: [{ brand: '陕汽', model: 'SX1313', plateNo: '浙A45678' }, { brand: '解放', model: 'J6P', plateNo: '浙A56789' }], receivableTotal: 57300.00, actualTotal: 57300.00, discountTotal: 0.00, discountProof: '-', arrivalAmount: 57300.00, arrivalTime: '2026-03-02 10:00', financeReceived: 57300.00, isInvoiced: '部分开票', invoiceMethod: '先付款后开票', invoiceTime: '-', invoicedAmount: '0.00', invoiceAttachment: '-' }, + { seq: 3, auditStatus: '审批中', creator: '王五', chargeTime: '2026-03-10 11:00', deliveryCount: 1, deliveryVehicles: [{ brand: '江淮', model: '格尔发K5', plateNo: '浙A67890' }], receivableTotal: 32800.00, actualTotal: 32800.00, discountTotal: 0.00, discountProof: '-', arrivalAmount: 32800.00, arrivalTime: '2026-03-11 09:00', financeReceived: 32800.00, isInvoiced: '未开票', invoiceMethod: '先付款后开票', invoiceTime: '-', invoicedAmount: '0.00', invoiceAttachment: '-' }, + { seq: 4, auditStatus: '已驳回', creator: '赵六', chargeTime: '2026-03-15 14:20', deliveryCount: 2, deliveryVehicles: [{ brand: '东风', model: 'DFH1250', plateNo: '浙A11111' }, { brand: '福田', model: '欧曼EST', plateNo: '浙A22222' }], receivableTotal: 45800.00, actualTotal: 45800.00, discountTotal: 0.00, discountProof: '-', arrivalAmount: 0.00, arrivalTime: '-', financeReceived: 0.00, isInvoiced: '未开票', invoiceMethod: '先付款后开票', invoiceTime: '-', invoicedAmount: '0.00', invoiceAttachment: '-' }, + { seq: 5, auditStatus: '审批通过', creator: '张三', chargeTime: '2026-03-20 09:00', deliveryCount: 2, deliveryVehicles: [{ brand: '重汽', model: '豪沃T7H', plateNo: '浙B33333' }, { brand: '陕汽', model: '德龙X3000', plateNo: '浙B44444' }], receivableTotal: 22200.00, actualTotal: 21600.00, discountTotal: 600.00, discountProof: '客户协商减免', arrivalAmount: 21600.00, arrivalTime: '2026-03-21 10:30', financeReceived: 21600.00, isInvoiced: '已开票', invoiceMethod: '先开票后付款', invoiceTime: '2026-03-22 14:00', invoicedAmount: 21600.00, invoiceAttachment: '发票-HT-ZL-2025-001-005.pdf' } + ] + }, + { + contractCode: 'HT-ZL-2025-002', + contractType: '正式合同', + projectName: '上海物流租赁项目', + customerName: '上海某某运输公司', + businessDept: '业务2部', + businessPerson: '李专员', + contractEffectiveDate: '2025-02-01', + allVehiclesReceivableCompleted: true, + totalReceivable: 132600.00, + totalActual: 132600.00, + totalDiscount: 0.00, + totalFinanceReceived: 132600.00, + children: [ + { seq: 1, auditStatus: '审批通过', creator: '李专员', chargeTime: '2026-02-28 14:00', deliveryCount: 2, deliveryVehicles: [{ brand: '江淮', model: '格尔发K5', plateNo: '沪B11111' }, { brand: '东风', model: 'DFH1250', plateNo: '沪B22222' }], receivableTotal: 65800.00, actualTotal: 65800.00, discountTotal: 0.00, discountProof: '-', arrivalAmount: 65800.00, arrivalTime: '2026-03-01 09:00', financeReceived: 65800.00, isInvoiced: '已开票', invoiceMethod: '先开票后付款', invoiceTime: '2026-03-02 15:20', invoicedAmount: 65800.00, invoiceAttachment: '发票-HT-ZL-2025-002.pdf' }, + { seq: 2, auditStatus: '审批通过', creator: '李专员', chargeTime: '2026-03-08 10:30', deliveryCount: 1, deliveryVehicles: [{ brand: '解放', model: 'JH6', plateNo: '沪B33333' }], receivableTotal: 33400.00, actualTotal: 33400.00, discountTotal: 0.00, discountProof: '-', arrivalAmount: 33400.00, arrivalTime: '2026-03-09 11:00', financeReceived: 33400.00, isInvoiced: '部分开票', invoiceMethod: '先付款后开票', invoiceTime: '-', invoicedAmount: '0.00', invoiceAttachment: '-' }, + { seq: 3, auditStatus: '待审批', creator: '李专员', chargeTime: '2026-03-18 16:00', deliveryCount: 2, deliveryVehicles: [{ brand: '福田', model: 'BJ1180', plateNo: '沪B44444' }, { brand: '重汽', model: 'ZZ1187', plateNo: '沪B55555' }], receivableTotal: 33400.00, actualTotal: 33400.00, discountTotal: 0.00, discountProof: '-', arrivalAmount: 33400.00, arrivalTime: '2026-03-19 09:00', financeReceived: 33400.00, isInvoiced: '未开票', invoiceMethod: '先开票后付款', invoiceTime: '-', invoicedAmount: '0.00', invoiceAttachment: '-' } + ] + }, + { + contractCode: 'HT-ZL-2025-003', + contractType: '正式合同', + projectName: '杭州城配租赁项目', + customerName: '杭州某某租赁有限公司', + businessDept: '业务3部', + businessPerson: '王专员', + contractEffectiveDate: '2025-02-10', + allVehiclesReceivableCompleted: false, + totalReceivable: 82400.00, + totalActual: 82400.00, + totalDiscount: 0.00, + totalFinanceReceived: 41200.00, + children: [ + { seq: 1, auditStatus: '已驳回', creator: '王专员', chargeTime: '2026-03-05 11:00', deliveryCount: 1, deliveryVehicles: [{ brand: '福田', model: '欧曼EST', plateNo: '浙C33333' }], receivableTotal: 41200.00, actualTotal: 41200.00, discountTotal: 0.00, discountProof: '-', arrivalAmount: 0.00, arrivalTime: '-', financeReceived: 0.00, isInvoiced: '未开票', invoiceMethod: '先付款后开票', invoiceTime: '-', invoicedAmount: '0.00', invoiceAttachment: '-' }, + { seq: 2, auditStatus: '审批通过', creator: '王专员', chargeTime: '2026-03-12 09:15', deliveryCount: 1, deliveryVehicles: [{ brand: '东风', model: 'DFH1180', plateNo: '浙C44444' }], receivableTotal: 41200.00, actualTotal: 41200.00, discountTotal: 0.00, discountProof: '-', arrivalAmount: 41200.00, arrivalTime: '2026-03-13 14:00', financeReceived: 41200.00, isInvoiced: '已开票', invoiceMethod: '先开票后付款', invoiceTime: '2026-03-14 10:00', invoicedAmount: 41200.00, invoiceAttachment: '发票-HT-ZL-2025-003.pdf' } + ] + } + ]; + var mainListState = useState(mainListData); + var mainList = mainListState[0]; + var setMainList = mainListState[1]; + + var filterOptions = useMemo(function () { + var list = mainList; + var codes = [], projects = [], customers = [], depts = [], persons = []; + list.forEach(function (r) { + if (r.contractCode && codes.indexOf(r.contractCode) === -1) codes.push(r.contractCode); + if (r.projectName && projects.indexOf(r.projectName) === -1) projects.push(r.projectName); + if (r.customerName && customers.indexOf(r.customerName) === -1) customers.push(r.customerName); + if (r.businessDept && depts.indexOf(r.businessDept) === -1) depts.push(r.businessDept); + if (r.businessPerson && persons.indexOf(r.businessPerson) === -1) persons.push(r.businessPerson); + }); + return { + contractCode: codes.map(function (v) { return { value: v, label: v }; }), + projectName: projects.map(function (v) { return { value: v, label: v }; }), + customerName: customers.map(function (v) { return { value: v, label: v }; }), + businessDept: depts.map(function (v) { return { value: v, label: v }; }), + businessPerson: persons.map(function (v) { return { value: v, label: v }; }) + }; + }, [mainList]); + var filteredMainList = useMemo(function () { + var list = mainList; + var code = filterContractCode[0]; + var project = filterProjectName[0]; + var customer = filterCustomerName[0]; + var dept = filterBusinessDept[0]; + var person = filterBusinessPerson[0]; + if (code) list = list.filter(function (r) { return r.contractCode === code; }); + if (project) list = list.filter(function (r) { return r.projectName === project; }); + if (customer) list = list.filter(function (r) { return r.customerName === customer; }); + if (dept) list = list.filter(function (r) { return r.businessDept === dept; }); + if (person) list = list.filter(function (r) { return r.businessPerson === person; }); + return list; + }, [mainList, filterContractCode[0], filterProjectName[0], filterCustomerName[0], filterBusinessDept[0], filterBusinessPerson[0]]); + + // 主表 dataSource 去掉 children,避免 Table 把子节点当树形数据渲染出多余空白行;子表数据通过 _detailList 在展开行内使用 + var mainTableDataSource = useMemo(function () { + return filteredMainList.map(function (r) { + var o = {}; + for (var k in r) if (k !== 'children') o[k] = r[k]; + o._detailList = r.children || []; + return o; + }); + }, [filteredMainList]); + + function fmtMoney(v) { + if (v === null || v === undefined) return '0.00元'; + var n = typeof v === 'number' ? v : parseFloat(v); + return (isNaN(n) ? '0.00' : n.toFixed(2)) + '元'; + } + + function goCollect(record) { + if (window.__receivableToCollect) window.__receivableToCollect(record); + else message.info('跳转至提车应收款-收款(原型)'); + } + function goView(record) { + message.info('查看提车收款单详情(原型)'); + } + function goEdit(record) { + if (window.__receivableToEdit) window.__receivableToEdit(record); + else message.info('跳转至提车应收款-编辑(原型)'); + } + + var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; + var filterLabelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var filterItemStyle = { marginBottom: 12 }; + var filterControlStyle = { width: '100%' }; + + // 主表列(宽度压缩在一屏内显示) + var mainColumns = [ + { title: '合同编码', dataIndex: 'contractCode', key: 'contractCode', width: 120, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '合同类型', dataIndex: 'contractType', key: 'contractType', width: 88, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '项目名称', dataIndex: 'projectName', key: 'projectName', width: 120, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 120, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '合同生效日期', dataIndex: 'contractEffectiveDate', key: 'contractEffectiveDate', width: 110, render: function (v) { return v || '—'; } }, + { title: '业务部门', dataIndex: 'businessDept', key: 'businessDept', width: 100, render: function (v) { return v || '—'; } }, + { title: '业务负责人', dataIndex: 'businessPerson', key: 'businessPerson', width: 100, render: function (v) { return v || '—'; } }, + { + title: '操作', + key: 'action', + width: 100, + fixed: 'right', + render: function (_, record) { + // 该合同所有车辆都已完成提车应收款时,不显示“提车收款单” + if (record && record.allVehiclesReceivableCompleted) return '—'; + return React.createElement(Button, { type: 'link', size: 'small', onClick: function () { goCollect(record); } }, '提车收款单'); + } + } + ]; + + // 子表:提车数量气泡用 state 控制当前展开的行 + var deliveryPopoverOpen = useState(null); + + function renderDeliveryPopover(record) { + var vehicles = record.deliveryVehicles || []; + var listStyle = { width: '100%', borderCollapse: 'collapse', fontSize: 13 }; + var thStyle = { padding: '6px 10px', textAlign: 'left', borderBottom: '1px solid #f0f0f0', backgroundColor: '#fafafa', fontWeight: 600 }; + var tdStyle = { padding: '6px 10px', borderBottom: '1px solid #f0f0f0' }; + var content = vehicles.length === 0 ? React.createElement('div', { style: { padding: 8 } }, '—') : React.createElement('div', { style: { padding: 0, minWidth: 200 } }, + React.createElement('table', { style: listStyle }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: thStyle }, '品牌'), + React.createElement('th', { style: thStyle }, '型号'), + React.createElement('th', { style: thStyle }, '车牌号') + ) + ), + React.createElement('tbody', null, + vehicles.map(function (v, i) { + return React.createElement('tr', { key: i }, + React.createElement('td', { style: tdStyle }, v.brand || '—'), + React.createElement('td', { style: tdStyle }, v.model || '—'), + React.createElement('td', { style: tdStyle }, v.plateNo || '—') + ); + }) + ) + ) + ); + var popoverKey = (record._parentCode || '') + '-' + record.seq + '-' + (record.chargeTime || ''); + return React.createElement(Popover, { + content: content, + title: '车辆详情', + trigger: 'click', + open: deliveryPopoverOpen[0] === popoverKey, + onOpenChange: function (open) { deliveryPopoverOpen[1](open ? popoverKey : null); } + }, React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0 } }, String(record.deliveryCount))); + } + + // 子表左缩进与主表展开列宽一致,使序号左侧边框与合同编码左侧边框对齐 + var expandColumnWidth = 48; + function buildCollectCode(contractCode, seq) { + var suffix = 'TC' + ('0000' + String(seq != null ? seq : '')).slice(-4); + return String(contractCode || '') + suffix; + } + var subColumns = [ + { title: '序号', dataIndex: 'seq', key: 'seq', width: 52, align: 'center' }, + { + title: '提车收款单编码', + key: 'collectCode', + width: 180, + ellipsis: true, + render: function (_, record) { + var parent = record && record._parentRecord; + var code = parent ? parent.contractCode : (record._parentCode || ''); + return buildCollectCode(code, record.seq) || '—'; + } + }, + { + title: '审批状态', + dataIndex: 'auditStatus', + key: 'auditStatus', + width: 90, + render: function (v, record) { + var status = (record && record.auditStatus != null) ? String(record.auditStatus).trim() : (v != null ? String(v).trim() : ''); + var isRejected = status === '已驳回' || status === '审批驳回'; + var color = status === '审批通过' ? 'success' : status === '待审批' ? 'processing' : status === '审批中' ? 'warning' : isRejected ? 'error' : 'default'; + var text = isRejected ? '审批驳回' : (status || '—'); + return React.createElement(Tag, { color: color }, text); + } + }, + { title: '创建时间', dataIndex: 'chargeTime', key: 'chargeTime', width: 128, render: function (v) { return v || '—'; } }, + { title: '创建人', dataIndex: 'creator', key: 'creator', width: 88, render: function (v) { return v || '—'; } }, + { title: '提车数量', key: 'deliveryCount', width: 76, render: function (_, record) { return renderDeliveryPopover(record); } }, + { title: '应收款总额', dataIndex: 'receivableTotal', key: 'receivableTotal', width: 98, align: 'right', render: function (v) { return fmtMoney(v); } }, + { title: '实收款总额', dataIndex: 'actualTotal', key: 'actualTotal', width: 98, align: 'right', render: function (v) { return fmtMoney(v); } }, + { title: '减免总金额', dataIndex: 'discountTotal', key: 'discountTotal', width: 88, align: 'right', render: function (v) { return fmtMoney(v); } }, + { + title: '操作', + key: 'action', + width: 180, + fixed: 'right', + render: function (_, record) { + var auditStatus = record && record.auditStatus; + var isPending = auditStatus === '待审批'; + var isRejected = auditStatus === '已驳回' || auditStatus === '审批驳回'; + var canEdit = isPending || isRejected; + var parentRecord = record && record._parentRecord; + function handleDelete() { + deleteConfirmRecord[1](record); + deleteConfirmParentRecord[1](parentRecord); + deleteConfirmVisible[1](true); + } + return React.createElement(Space, { size: 'small' }, + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { goView(record); } }, '查看'), + canEdit ? React.createElement(Button, { type: 'link', size: 'small', onClick: function () { goEdit(record); } }, '编辑') : null, + (isPending || isRejected) ? React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: handleDelete }, '删除') : null + ); + } + } + ]; + function confirmDeleteSub() { + var parent = deleteConfirmParentRecord[0]; + var row = deleteConfirmRecord[0]; + var list = (parent && (parent._detailList || parent.children)) || []; + if (parent && row && list.length >= 0) { + var newChildren = list.filter(function (r) { return !(r.seq === row.seq && r.chargeTime === row.chargeTime); }); + // 删除后序号重新生成 1、2、3... + newChildren = newChildren.map(function (r, index) { var o = {}; for (var k in r) o[k] = r[k]; o.seq = index + 1; return o; }); + setMainList(function (prev) { + return prev.map(function (p) { + if (p.contractCode !== parent.contractCode) return p; + var next = {}; for (var k in p) next[k] = p[k]; next.children = newChildren; return next; + }); + }); + } + deleteConfirmVisible[1](false); + deleteConfirmRecord[1](null); + deleteConfirmParentRecord[1](null); + message.success('已删除'); + } + function expandRowRender(record) { + var rows = (record._detailList || []).map(function (r) { var o = {}; for (var k in r) o[k] = r[k]; o._parentCode = record.contractCode; o._parentRecord = record; return o; }); + return React.createElement('div', { + style: { marginBottom: 0, paddingLeft: expandColumnWidth, boxSizing: 'border-box' }, + draggable: false, + onDragStart: function (e) { e.preventDefault(); } + }, + React.createElement(Table, { + rowKey: function (r) { return (record.contractCode || '') + '-' + (r.seq || r.chargeTime || Math.random()); }, + columns: subColumns, + dataSource: rows, + pagination: false, + size: 'small', + bordered: true + }) + ); + } + + var requirementContent = '提车应收款(2026年3月10日版本)\n一个「数字化资产ONEOS运管平台」中的「财务管理」「提车应收款」模块\n#面包屑:财务管理-提车应收款;\n\n页面分为2个卡片;\n1.筛选:\n#支持合同编码/项目名称/客户名称/业务部门/业务负责人等筛选方式;\n1.1.合同编码:选择器,支持输入框内手动输入下拉模糊匹配对应项;\n1.2.项目名称:选择器,支持输入框内手动输入下拉模糊匹配对应项;\n1.3.客户名称:选择器,支持输入框内手动输入下拉模糊匹配对应项;\n1.4.业务部门:选择器,支持选择所有业务部门;\n1.5.业务负责人:选择器,支持选择所有业务负责人;\n\n2.列表:\n#列表展示方式为:嵌套子表格,分为主表和子表,可点击主表展开子表,一个合同编码可以展开多个提车应收款明细;\n2.1.主表显示字段为:合同编码、合同类型、项目名称、客户名称、合同生效日期、业务部门、业务负责人、操作;\n2.1.1.合同编码:显示该提车应收款租赁合同对应合同编码;\n2.1.2.合同类型:显示该租赁合同对应合同类型,显示该合同为试用合同还是正式合同;\n2.1.3.项目名称:显示该租赁合同对应项目名称;\n2.1.4.客户名称:显示该租赁合同对应客户名称;\n2.1.5.合同生效日期:显示该租赁合同生效日期,格式为:YYYY-MM-DD;\n2.1.6.业务部门:显示该租赁合同对应业务部门名称;\n2.1.7.业务负责人:显示该租赁合同对应业务负责人;\n2.1.8.操作:提车收款单;\n 2.1.8.1.提车收款单:点击提车收款单,跳转提车收款单页进行子表创建,该合同所有车辆都已完成提车应收款后,该按钮不显示;\n主表右下角为分页符,支持选择单页显示多少条数据;\n\n2.2.子表显示字段为:序号、审批状态、创建时间、提车数量、应收款总额、实收款总额、减免总金额、实际到账金额、是否已开票、已开票金额、操作;\n2.2.1.序号:按照提车首付款收费单据提交时间从最早开始显示第1条,按照顺序1.2.3....;\n2.2.2.审批状态:审批通过、待审批、审批中、已驳回;\n 2.2.2.1.审批通过:该审批通过最终节点审批,显示为审批通过;\n 2.2.2.2.待审批:审批提车收款单仅保存,但未提交审批;\n 2.2.2.3.审批中:已发起审批流程,但未完成最终节点审批;\n 2.2.2.4.审批驳回:发起审批流程,但任意节点被驳回。被驳回的提车收款单支持重新修改后提交审批;\n2.2.3.创建时间:显示提车收款单创建时间,格式为:YYYY-MM-DD HH:MM;\n2.2.4.创建人:显示已提车收款单创建人姓名;\n2.2.5.提车数量:显示该提车收费单对应提车数量,点击提车数量弹出气泡卡片,卡片中列表显示品牌、型号、车牌号;\n2.2.6.应收款金额:按照该提车收费单据应收款金额,格式为:xx.xx元;\n2.2.7.实收款金额:按照该提车收费单据实收款总额,格式为:xx.xx元;\n2.2.8.减免金额:按照该提车收费单减免总金额,显示当前子表对应减免总额,格式为:xx.xx元;\n2.2.9.实际到账金额:显示该提车收费单财务填写的实际到账金额,格式为:xx.xx元;\n2.2.10.是否已开票:显示财务是否完成开票,已开票显示为:已开票,未开票显示为:未开票,部分开票显示为:部分开票;\n2.2.11.已开票金额:显示财务已开票金额,格式为:xx.xx元;\n2.2.12.总计:显示应收款总额、实收款总额、减免总金额、实际到账金额、已开票金额等所有子表求和数据;\n2.2.13.操作:查看、编辑、删除;\n 2.2.12.1.查看:点击查看后,跳转提车应收款-查看页面,审批通过、待审批、审批中、审批驳回均可点击查看来查看提车收款单详情;\n 2.2.12.2.编辑:点击编辑后,跳转提车应收款-编辑页面,待审批、审批驳回均可进行编辑操作;\n 2.2.12.2.删除:待审批、审批驳回状态子表支持删除功能,点击弹出二次确认,点击确认后删除该记录;\n '; + + return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + React.createElement(Breadcrumb, { + items: [ + { title: '财务管理' }, + { title: '提车应收款' } + ] + }), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { requirementModalVisible[1](true); } }, '查看需求说明') + ), + React.createElement(Card, { title: '筛选', style: cardStyle }, + React.createElement('div', { + style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px 24px', alignItems: 'start' } + }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '合同编码'), + React.createElement(Select, { + placeholder: '请选择或输入合同编码', + allowClear: true, + showSearch: true, + style: filterControlStyle, + value: filterContractCode[0], + onChange: function (v) { filterContractCode[1](v); }, + options: filterOptions.contractCode, + filterOption: function (input, opt) { return (opt.label || '').toString().toLowerCase().indexOf((input || '').toLowerCase()) !== -1; } + }) + ), + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '项目名称'), + React.createElement(Select, { + placeholder: '请选择或输入项目名称', + allowClear: true, + showSearch: true, + style: filterControlStyle, + value: filterProjectName[0], + onChange: function (v) { filterProjectName[1](v); }, + options: filterOptions.projectName, + filterOption: function (input, opt) { return (opt.label || '').toString().toLowerCase().indexOf((input || '').toLowerCase()) !== -1; } + }) + ), + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '客户名称'), + React.createElement(Select, { + placeholder: '请选择或输入客户名称', + allowClear: true, + showSearch: true, + style: filterControlStyle, + value: filterCustomerName[0], + onChange: function (v) { filterCustomerName[1](v); }, + options: filterOptions.customerName, + filterOption: function (input, opt) { return (opt.label || '').toString().toLowerCase().indexOf((input || '').toLowerCase()) !== -1; } + }) + ), + filterExpanded[0] ? React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '业务部门'), + React.createElement(Select, { + placeholder: '请选择业务部门', + allowClear: true, + style: filterControlStyle, + value: filterBusinessDept[0], + onChange: function (v) { filterBusinessDept[1](v); }, + options: filterOptions.businessDept + }) + ) : null, + filterExpanded[0] ? React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '业务负责人'), + React.createElement(Select, { + placeholder: '请选择业务负责人', + allowClear: true, + style: filterControlStyle, + value: filterBusinessPerson[0], + onChange: function (v) { filterBusinessPerson[1](v); }, + options: filterOptions.businessPerson + }) + ) : null + ), + React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 } }, + React.createElement(Button, { onClick: function () { filterContractCode[1](undefined); filterProjectName[1](undefined); filterCustomerName[1](undefined); filterBusinessDept[1](undefined); filterBusinessPerson[1](undefined); } }, '重置'), + React.createElement(Button, { type: 'primary' }, '查询'), + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { filterExpanded[1](!filterExpanded[0]); } }, filterExpanded[0] ? '收起' : '展开') + ) + ), + React.createElement(Card, { title: '提车应收款', style: cardStyle }, + React.createElement(Table, { + rowKey: 'contractCode', + columns: mainColumns, + dataSource: mainTableDataSource, + expandable: { + expandedRowKeys: expandedRowKeys, + onExpandedRowsChange: function (keys) { + setExpandedRowKeys(keys.length ? [keys[keys.length - 1]] : []); + }, + expandedRowRender: expandRowRender, + rowExpandable: function (record) { return (record._detailList && record._detailList.length > 0); }, + columnWidth: expandColumnWidth + }, + pagination: { pageSize: 10, showSizeChanger: true, showTotal: function (t) { return '共 ' + t + ' 条'; } }, + size: 'middle', + bordered: true, + scroll: { x: 758 } + }) + ), + React.createElement(Modal, { + title: '确认删除', + open: deleteConfirmVisible[0], + onCancel: function () { deleteConfirmVisible[1](false); deleteConfirmRecord[1](null); deleteConfirmParentRecord[1](null); }, + onOk: confirmDeleteSub, + okText: '确认', + cancelText: '取消' + }, '确定要删除该提车收款单记录吗?'), + React.createElement(Modal, { + title: '需求说明', + open: requirementModalVisible[0], + onCancel: function () { requirementModalVisible[1](false); }, + width: 720, + footer: React.createElement(Button, { onClick: function () { requirementModalVisible[1](false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, React.createElement('div', { style: { padding: '8px 0' } }, + React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6 } }, requirementContent)) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-finance/pages/10-租赁账单.jsx b/src/prototypes/oneos-web-finance/pages/10-租赁账单.jsx new file mode 100644 index 0000000..7d8a668 --- /dev/null +++ b/src/prototypes/oneos-web-finance/pages/10-租赁账单.jsx @@ -0,0 +1,536 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 财务管理 - 租赁账单(基于租赁业务运营明细台账-明细表) + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + + var antd = window.antd; + var App = antd.App; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Table = antd.Table; + var Button = antd.Button; + var Select = antd.Select; + var DatePicker = antd.DatePicker; + var Space = antd.Space; + var Tag = antd.Tag; + var Modal = antd.Modal; + var Row = antd.Row; + var Col = antd.Col; + var message = antd.message; + + var RangePicker = DatePicker.RangePicker; + + // 客户主数据(联调对接「业务管理-客户管理」) + var CUSTOMER_MASTER = [ + { customerCode: 'KH-2025-001', customerName: '嘉兴某某物流有限公司', region: '华东', city: '浙江省-嘉兴市' }, + { customerCode: 'KH-2025-002', customerName: '上海某某运输公司', region: '华东', city: '上海市-上海市' }, + { customerCode: 'KH-2025-003', customerName: '杭州某某租赁有限公司', region: '华东', city: '浙江省-杭州市' }, + { customerCode: 'KH-2025-004', customerName: '南京某某供应链公司', region: '华东', city: '江苏省-南京市' }, + { customerCode: 'KH-2026-101', customerName: '宁波港集装箱运输有限公司嘉兴分公司', region: '华东', city: '浙江省-嘉兴市' }, + { customerCode: 'KH-2026-102', customerName: '嘉兴市乍浦港口经营有限公司', region: '华东', city: '浙江省-嘉兴市' }, + { customerCode: 'KH-2026-103', customerName: '上海馨想事成物流有限公司', region: '华东', city: '上海市-上海市' }, + { customerCode: 'KH-2026-104', customerName: '北京海龙运输有限公司', region: '华北', city: '北京市-北京市' } + ]; + + // 车辆主数据(联调对接「车辆管理」;品牌/型号取自业务台账样例) + var VEHICLE_MASTER = [ + { plateNo: '浙F07033F', brand: '现代', model: '4.5吨货车', vehicleType: '轻型厢式货车', operateStatus: '租赁' }, + { plateNo: '浙F06909F', brand: '现代', model: '帕力安牌4.5吨冷链车', vehicleType: '厢式货车', operateStatus: '租赁' }, + { plateNo: '浙F39003F', brand: '现代', model: '帕力安牌18吨双飞翼货车', vehicleType: '重型厢式货车', operateStatus: '租赁' }, + { plateNo: '沪A33198F', brand: '楚风', model: '18吨厢式货车', vehicleType: '重型厢式货车', operateStatus: '租赁' }, + { plateNo: '浙F09038F', brand: '苏龙', model: '海格牌18吨双飞翼货车', vehicleType: '重型厢式货车', operateStatus: '租赁' }, + { plateNo: '浙F00661F', brand: '跃进', model: '4.5吨冷链车', vehicleType: '厢式货车', operateStatus: '租赁' }, + { plateNo: '浙F00688F', brand: '飞驰', model: '49吨牵引车头', vehicleType: '牵引车', operateStatus: '租赁' }, + { plateNo: '浙F01115F', brand: '飞驰', model: '49吨牵引车头', vehicleType: '牵引车', operateStatus: '租赁' }, + { plateNo: '浙F03218F', brand: '飞驰', model: '49吨牵引车头', vehicleType: '牵引车', operateStatus: '租赁' }, + { plateNo: '粤AGP5621', brand: '现代', model: '帕力安牌4.5吨冷链车', vehicleType: '厢式货车', operateStatus: '租赁' }, + { plateNo: '京A29256F', brand: '飞驰', model: '49吨牵引车头', vehicleType: '牵引车', operateStatus: '租赁' }, + { plateNo: '浙F01505F', brand: '飞驰', model: '49吨牵引车头', vehicleType: '牵引车', operateStatus: '租赁' } + ]; + + var listDataState = useState([ + { key: 'b1', seq: 1, year: '2026', month: '1', businessDept: '业务一部', salesperson: '陈高伟', billDate: '2026-01-01', plateNo: '浙F07033F', customerName: '宁波港集装箱运输有限公司嘉兴分公司', pickupDate: '2025-08-27', contractStart: '2025-09-01', contractEnd: '2025-11-30', startDate: '', returnDate: '-', avgDays: '', deposit: 0, contractRent: 6696, receivableTotal: 6696, receivableRent: 6696, insuranceSurcharge: 0, opsFee: 0, otherIncome: 0, discount: 0, actualReceived: 6696, unreceived: 0, invoiceDate: '2026-01-16', paymentDate: '2026-01-27', paymentMethod: '月度后付', vehicleTargetCost: 7100, vehicleActualCost: null, insuranceFee: null, hydrogenFee: null, opsCost: null, brokerageFee: 0, otherCost: null, totalCost: 7100, profit: -404, actualProfit: null, discountDetail: '', assetOwner: '浙江氢能产业发展有限公司', policy: '', signCompany: '浙江羚牛氢能科技有限公司', remark: '' }, + { key: 'b2', seq: 2, year: '2026', month: '1', businessDept: '业务一部', salesperson: '陈高伟', billDate: '2026-01-01', plateNo: '浙F06909F', customerName: '宁波港集装箱运输有限公司嘉兴分公司', pickupDate: '2025-08-27', contractStart: '2025-09-01', contractEnd: '2025-11-30', startDate: '', returnDate: '-', avgDays: '', deposit: 0, contractRent: 6696, receivableTotal: 6696, receivableRent: 6696, insuranceSurcharge: 0, opsFee: 0, otherIncome: 0, discount: 0, actualReceived: 6696, unreceived: 0, invoiceDate: '2026-01-16', paymentDate: '2026-01-27', paymentMethod: '月度后付', vehicleTargetCost: 7100, vehicleActualCost: null, insuranceFee: null, hydrogenFee: null, opsCost: null, brokerageFee: 0, otherCost: null, totalCost: 7100, profit: -404, actualProfit: null, discountDetail: '', assetOwner: '浙江氢能产业发展有限公司', policy: '', signCompany: '浙江羚牛氢能科技有限公司', remark: '' }, + { key: 'b3', seq: 3, year: '2026', month: '1', businessDept: '业务一部', salesperson: '陈高伟', billDate: '2026-01-01', plateNo: '浙F39003F', customerName: '宁波港集装箱运输有限公司嘉兴分公司', pickupDate: '2025-08-27', contractStart: '2025-09-01', contractEnd: '2025-11-30', startDate: '', returnDate: '-', avgDays: '', deposit: 0, contractRent: 6696, receivableTotal: 6696, receivableRent: 6696, insuranceSurcharge: 0, opsFee: 0, otherIncome: 0, discount: 0, actualReceived: 6696, unreceived: 0, invoiceDate: '2026-01-16', paymentDate: '2026-01-27', paymentMethod: '月度后付', vehicleTargetCost: 7100, vehicleActualCost: null, insuranceFee: null, hydrogenFee: null, opsCost: null, brokerageFee: 0, otherCost: null, totalCost: 7100, profit: -404, actualProfit: null, discountDetail: '', assetOwner: '浙江氢能产业发展有限公司', policy: '', signCompany: '浙江羚牛氢能科技有限公司', remark: '' }, + { key: 'b4', seq: 4, year: '2026', month: '1', businessDept: '业务一部', salesperson: '陈高伟', billDate: '2026-01-01', plateNo: '沪A33198F', customerName: '宁波港集装箱运输有限公司嘉兴分公司', pickupDate: '2025-08-27', contractStart: '2025-09-01', contractEnd: '2025-11-30', startDate: '', returnDate: '-', avgDays: '', deposit: 0, contractRent: 6696, receivableTotal: 6696, receivableRent: 6696, insuranceSurcharge: 0, opsFee: 0, otherIncome: 0, discount: 0, actualReceived: 6696, unreceived: 0, invoiceDate: '2026-01-16', paymentDate: '2026-01-27', paymentMethod: '月度后付', vehicleTargetCost: 7100, vehicleActualCost: null, insuranceFee: null, hydrogenFee: null, opsCost: null, brokerageFee: 0, otherCost: null, totalCost: 7100, profit: -404, actualProfit: null, discountDetail: '', assetOwner: '上海羚牛氢运物联网科技有限公司', policy: '', signCompany: '浙江羚牛氢能科技有限公司', remark: '' }, + { key: 'b5', seq: 5, year: '2026', month: '1', businessDept: '业务一部', salesperson: '陈高伟', billDate: '2026-01-01', plateNo: '浙F09038F', customerName: '宁波港集装箱运输有限公司嘉兴分公司', pickupDate: '2025-08-27', contractStart: '2025-09-01', contractEnd: '2025-11-30', startDate: '', returnDate: '-', avgDays: '', deposit: 0, contractRent: 6696, receivableTotal: 6696, receivableRent: 6696, insuranceSurcharge: 0, opsFee: 0, otherIncome: 0, discount: 0, actualReceived: 6696, unreceived: 0, invoiceDate: '2026-01-16', paymentDate: '2026-01-27', paymentMethod: '月度后付', vehicleTargetCost: 7100, vehicleActualCost: null, insuranceFee: null, hydrogenFee: null, opsCost: null, brokerageFee: 0, otherCost: null, totalCost: 7100, profit: -404, actualProfit: null, discountDetail: '', assetOwner: '浙江氢能产业发展有限公司', policy: '', signCompany: '浙江羚牛氢能科技有限公司', remark: '' }, + { key: 'b6', seq: 6, year: '2026', month: '1', businessDept: '业务一部', salesperson: '陈高伟', billDate: '2026-01-01', plateNo: '浙F00661F', customerName: '嘉兴市乍浦港口经营有限公司', pickupDate: '2022-12-01', contractStart: '2022-12-01', contractEnd: '2025-11-30', startDate: '', returnDate: '-', avgDays: '', deposit: 0, contractRent: 18700, receivableTotal: 18700, receivableRent: 18700, insuranceSurcharge: 0, opsFee: 0, otherIncome: 0, discount: 0, actualReceived: 18700, unreceived: 0, invoiceDate: '2026-02-11', paymentDate: '2026-02-28', paymentMethod: '月度后付', vehicleTargetCost: 7100, vehicleActualCost: null, insuranceFee: null, hydrogenFee: null, opsCost: null, brokerageFee: 0, otherCost: null, totalCost: 7100, profit: 11600, actualProfit: null, discountDetail: '', assetOwner: '嘉兴氢能产业发展股份有限公司', policy: '', signCompany: '浙江羚牛氢能科技有限公司', remark: '' }, + { key: 'b7', seq: 7, year: '2026', month: '1', businessDept: '业务一部', salesperson: '陈高伟', billDate: '2026-01-01', plateNo: '浙F00688F', customerName: '嘉兴市乍浦港口经营有限公司', pickupDate: '2022-12-01', contractStart: '2022-12-01', contractEnd: '2025-11-30', startDate: '', returnDate: '-', avgDays: '', deposit: 0, contractRent: 18700, receivableTotal: 18700, receivableRent: 18700, insuranceSurcharge: 0, opsFee: 0, otherIncome: 0, discount: 0, actualReceived: 18700, unreceived: 0, invoiceDate: '2026-02-11', paymentDate: '2026-02-28', paymentMethod: '月度后付', vehicleTargetCost: 7100, vehicleActualCost: null, insuranceFee: null, hydrogenFee: null, opsCost: null, brokerageFee: 0, otherCost: null, totalCost: 7100, profit: 11600, actualProfit: null, discountDetail: '', assetOwner: '嘉兴氢能产业发展股份有限公司', policy: '', signCompany: '浙江羚牛氢能科技有限公司', remark: '' }, + { key: 'b8', seq: 8, year: '2026', month: '1', businessDept: '业务一部', salesperson: '陈高伟', billDate: '2026-01-01', plateNo: '浙F01115F', customerName: '嘉兴市乍浦港口经营有限公司', pickupDate: '2022-12-01', contractStart: '2022-12-01', contractEnd: '2025-11-30', startDate: '', returnDate: '-', avgDays: '', deposit: 0, contractRent: 18700, receivableTotal: 18700, receivableRent: 18700, insuranceSurcharge: 0, opsFee: 0, otherIncome: 0, discount: 0, actualReceived: 18700, unreceived: 0, invoiceDate: '2026-02-11', paymentDate: '2026-02-28', paymentMethod: '月度后付', vehicleTargetCost: 7100, vehicleActualCost: null, insuranceFee: null, hydrogenFee: null, opsCost: null, brokerageFee: 0, otherCost: null, totalCost: 7100, profit: 11600, actualProfit: null, discountDetail: '', assetOwner: '嘉兴氢能产业发展股份有限公司', policy: '', signCompany: '浙江羚牛氢能科技有限公司', remark: '' }, + { key: 'b9', seq: 9, year: '2026', month: '2', businessDept: '业务二部', salesperson: '董剑煜', billDate: '2026-02-01', plateNo: '浙F03218F', customerName: '上海馨想事成物流有限公司', pickupDate: '2026-01-10', contractStart: '2026-01-15', contractEnd: '2027-01-14', startDate: '', returnDate: '-', avgDays: '', deposit: 10000, contractRent: 12800, receivableTotal: 12800, receivableRent: 12000, insuranceSurcharge: 500, opsFee: 300, otherIncome: 0, discount: 0, actualReceived: 12800, unreceived: 0, invoiceDate: '2026-02-15', paymentDate: '2026-02-28', paymentMethod: '月度预付', vehicleTargetCost: 8500, vehicleActualCost: 8200, insuranceFee: 600, hydrogenFee: 1200, opsCost: 300, brokerageFee: 0, otherCost: 0, totalCost: 10300, profit: 2500, actualProfit: 2200, discountDetail: '', assetOwner: '浙江羚牛氢能科技有限公司', policy: '首月免运维费', signCompany: '浙江羚牛氢能科技有限公司', remark: '' }, + { key: 'b10', seq: 10, year: '2026', month: '2', businessDept: '业务二部', salesperson: '赵连飞', billDate: '2026-02-01', plateNo: '粤AGP5621', customerName: '嘉兴某某物流有限公司', pickupDate: '2026-02-01', contractStart: '2026-02-01', contractEnd: '2027-01-31', startDate: '', returnDate: '-', avgDays: '', deposit: 8000, contractRent: 9800, receivableTotal: 9800, receivableRent: 9500, insuranceSurcharge: 200, opsFee: 100, otherIncome: 0, discount: 0, actualReceived: 9500, unreceived: 300, invoiceDate: '2026-02-20', paymentDate: '-', paymentMethod: '月度后付', vehicleTargetCost: 7200, vehicleActualCost: 7000, insuranceFee: 450, hydrogenFee: 800, opsCost: 150, brokerageFee: 0, otherCost: 0, totalCost: 8400, profit: 1400, actualProfit: null, discountDetail: '', assetOwner: '浙江羚牛氢能科技有限公司', policy: '', signCompany: '浙江羚牛氢能科技有限公司', remark: '部分未收' }, + { key: 'b11', seq: 11, year: '2026', month: '2', businessDept: '业务一部', salesperson: '刘念念', billDate: '2026-02-01', plateNo: '京A29256F', customerName: '北京海龙运输有限公司', pickupDate: '2025-08-01', contractStart: '2025-08-15', contractEnd: '2026-08-14', startDate: '', returnDate: '-', avgDays: '', deposit: 15000, contractRent: 22000, receivableTotal: 22000, receivableRent: 21000, insuranceSurcharge: 600, opsFee: 400, otherIncome: 0, discount: 0, actualReceived: 22000, unreceived: 0, invoiceDate: '2026-02-10', paymentDate: '2026-02-25', paymentMethod: '季度预付', vehicleTargetCost: 12000, vehicleActualCost: 11500, insuranceFee: 900, hydrogenFee: 2500, opsCost: 400, brokerageFee: 500, otherCost: 0, totalCost: 15800, profit: 6200, actualProfit: 5800, discountDetail: '', assetOwner: '北京羚牛氢运科技有限公司', policy: '季度结算优惠', signCompany: '浙江羚牛氢能科技有限公司', remark: '' }, + { key: 'b12', seq: 12, year: '2026', month: '2', businessDept: '业务一部', salesperson: '尚建华', billDate: '2026-02-01', plateNo: '浙F01505F', customerName: '嘉兴市乍浦港口经营有限公司', pickupDate: '2022-12-01', contractStart: '2022-12-01', contractEnd: '2025-11-30', startDate: '', returnDate: '-', avgDays: '', deposit: 0, contractRent: 18700, receivableTotal: 18700, receivableRent: 18700, insuranceSurcharge: 0, opsFee: 0, otherIncome: 0, discount: 500, actualReceived: 18200, unreceived: 0, invoiceDate: '2026-02-11', paymentDate: '2026-02-28', paymentMethod: '月度后付', vehicleTargetCost: 7100, vehicleActualCost: 7100, insuranceFee: null, hydrogenFee: null, opsCost: null, brokerageFee: 0, otherCost: null, totalCost: 7100, profit: 11600, actualProfit: 11100, discountDetail: '客户协商减免500元', assetOwner: '嘉兴氢能产业发展股份有限公司', policy: '', signCompany: '浙江羚牛氢能科技有限公司', remark: '' } + ]); + var listData = listDataState[0]; + + // 筛选草稿 / 已应用(点击查询后生效) + var periodDraftState = useState(undefined); + var periodDraft = periodDraftState[0]; + var setPeriodDraft = periodDraftState[1]; + var customerDraftState = useState(undefined); + var customerDraft = customerDraftState[0]; + var setCustomerDraft = customerDraftState[1]; + var plateDraftState = useState(undefined); + var plateDraft = plateDraftState[0]; + var setPlateDraft = plateDraftState[1]; + var salespersonDraftState = useState(undefined); + var salespersonDraft = salespersonDraftState[0]; + var setSalespersonDraft = salespersonDraftState[1]; + var billDateDraftState = useState(null); + var billDateDraft = billDateDraftState[0]; + var setBillDateDraft = billDateDraftState[1]; + var paymentDraftState = useState(undefined); + var paymentDraft = paymentDraftState[0]; + var setPaymentDraft = paymentDraftState[1]; + + var periodAppliedState = useState(undefined); + var periodApplied = periodAppliedState[0]; + var setPeriodApplied = periodAppliedState[1]; + var customerAppliedState = useState(undefined); + var customerApplied = customerAppliedState[0]; + var setCustomerApplied = customerAppliedState[1]; + var plateAppliedState = useState(undefined); + var plateApplied = plateAppliedState[0]; + var setPlateApplied = plateAppliedState[1]; + var salespersonAppliedState = useState(undefined); + var salespersonApplied = salespersonAppliedState[0]; + var setSalespersonApplied = salespersonAppliedState[1]; + var billDateAppliedState = useState(null); + var billDateApplied = billDateAppliedState[0]; + var setBillDateApplied = billDateAppliedState[1]; + var paymentAppliedState = useState(undefined); + var paymentApplied = paymentAppliedState[0]; + var setPaymentApplied = paymentAppliedState[1]; + + var filterExpandedState = useState(false); + var filterExpanded = filterExpandedState[0]; + var setFilterExpanded = filterExpandedState[1]; + var requirementModalVisible = useState(false); + + var layoutStyle = { + padding: '16px 24px 24px', + minHeight: '100vh', + background: 'linear-gradient(165deg, #eef4ff 0%, #f5f7fa 42%, #f0f2f5 100%)' + }; + var filterLabelStyle = { marginBottom: 6, fontSize: 13, color: 'rgba(0,0,0,0.55)', fontWeight: 500 }; + var filterItemStyle = { marginBottom: 12 }; + var filterControlStyle = { width: '100%' }; + var filterCardStyle = { + marginBottom: 20, + borderRadius: 16, + boxShadow: '0 4px 20px -4px rgba(16,24,40,0.03), 0 0 0 1px rgba(16,24,40,0.06)', + border: 'none', + background: '#ffffff' + }; + var tableCardStyle = { + borderRadius: 16, + boxShadow: '0 10px 32px -4px rgba(16,24,40,0.06), 0 0 0 1px rgba(16,24,40,0.04)', + border: 'none', + background: '#ffffff', + overflow: 'hidden' + }; + var ledgerTableStyle = + '.rb-ledger-table-wrap{border-radius:12px;overflow:hidden;box-shadow:0 4px 24px -6px rgba(15,23,42,0.05),0 0 0 1px rgba(22,119,255,0.1)}' + + '.rb-ledger-table .ant-table-thead>tr>th,.rb-ledger-table .ant-table-thead .ant-table-cell{white-space:nowrap;color:#0f172a!important;font-weight:600!important;font-size:12px!important;' + + 'background:#e8f4fc!important;border-bottom:1px solid #bae6fd!important;border-inline-end:1px solid #dbeafe!important;padding:0 6px!important;height:36px!important;text-align:center!important}' + + '.rb-ledger-table .ant-table-tbody>tr:not(.ant-table-measure-row)>td{padding:4px 6px!important;vertical-align:middle!important;font-size:12px}' + + '.rb-ledger-table .ant-table-tbody>tr.rb-row-data:hover>td{background:#f0f9ff!important}' + + '.rb-ledger-totals-bar{display:flex;align-items:stretch;gap:0;margin-bottom:10px;border:1px solid #bae6fd;border-radius:10px;overflow:hidden;background:#f8fafc;box-shadow:0 1px 0 rgba(15,23,42,0.04)}' + + '.rb-ledger-totals-bar__title{display:flex;align-items:center;justify-content:center;min-width:72px;padding:10px 14px;font-size:14px;font-weight:700;color:#0f172a;background:#e8f4fc;border-right:1px solid #bae6fd}' + + '.rb-ledger-totals-bar__items{display:flex;flex:1;flex-wrap:wrap}' + + '.rb-ledger-totals-bar__item{flex:1;min-width:140px;padding:8px 20px;border-right:1px solid #e2e8f0;display:flex;flex-direction:column;justify-content:center;gap:4px}' + + '.rb-ledger-totals-bar__item:last-child{border-right:none}' + + '.rb-ledger-totals-bar__label{font-size:12px;color:rgba(15,23,42,0.55);font-weight:500;line-height:1.2}' + + '.rb-ledger-totals-bar__value{font-size:16px;font-weight:700;color:#0f172a;font-variant-numeric:tabular-nums;line-height:1.3}' + + '.rb-ledger-totals-bar__value.is-profit-pos{color:#389e0d}' + + '.rb-ledger-totals-bar__value.is-profit-neg{color:#cf1322}'; + + var customerOptions = useMemo(function () { + return CUSTOMER_MASTER.map(function (c) { + return { value: c.customerName, label: c.customerName + '(' + c.customerCode + ')' }; + }); + }, []); + + var plateOptions = useMemo(function () { + return VEHICLE_MASTER.map(function (v) { + return { value: v.plateNo, label: v.plateNo + ' · ' + (v.brand || '') + ' · ' + (v.model || '') }; + }); + }, []); + + var vehicleByPlate = useMemo(function () { + var map = {}; + VEHICLE_MASTER.forEach(function (v) { map[v.plateNo] = v; }); + return map; + }, []); + + function fmtYearMonth(year, month) { + if (!year) return ''; + var m = String(month || ''); + if (m.length === 1) m = '0' + m; + return year + '-' + m; + } + + var periodOptions = useMemo(function () { + var set = {}; + listData.forEach(function (r) { + var p = fmtYearMonth(r.year, r.month); + if (p) set[p] = true; + }); + return Object.keys(set).sort().reverse().map(function (v) { return { value: v, label: v }; }); + }, [listData]); + + var salespersonOptions = useMemo(function () { + var map = {}; + listData.forEach(function (r) { + if (!r.salesperson) return; + var key = r.salesperson; + if (!map[key]) map[key] = r.businessDept || ''; + }); + return Object.keys(map).map(function (v) { + var dept = map[v]; + return { value: v, label: dept ? dept + ' · ' + v : v }; + }); + }, [listData]); + + var paymentMethodOptions = useMemo(function () { + var set = {}; + listData.forEach(function (r) { if (r.paymentMethod) set[r.paymentMethod] = true; }); + return Object.keys(set).map(function (v) { return { value: v, label: v }; }); + }, [listData]); + + function inDateRange(dateStr, range) { + if (!range || !range[0] || !range[1]) return true; + var s = new Date(String(range[0]).replace(/-/g, '/')); + var e = new Date(String(range[1]).replace(/-/g, '/')); + var d = new Date(String(dateStr || '').replace(/-/g, '/')); + if (isNaN(s.getTime()) || isNaN(e.getTime()) || isNaN(d.getTime())) return true; + var ms = d.getTime(); + return ms >= s.getTime() && ms <= (e.getTime() + 24 * 60 * 60 * 1000 - 1); + } + + var filteredList = useMemo(function () { + return listData.filter(function (r) { + if (periodApplied && fmtYearMonth(r.year, r.month) !== periodApplied) return false; + if (salespersonApplied && r.salesperson !== salespersonApplied) return false; + if (customerApplied && r.customerName !== customerApplied) return false; + if (plateApplied && r.plateNo !== plateApplied) return false; + if (paymentApplied && r.paymentMethod !== paymentApplied) return false; + if (!inDateRange(r.billDate, billDateApplied)) return false; + return true; + }); + }, [listData, periodApplied, salespersonApplied, customerApplied, plateApplied, paymentApplied, billDateApplied]); + + var totals = useMemo(function () { + var fields = ['receivableTotal', 'actualReceived', 'unreceived', 'totalCost', 'profit']; + var result = { receivableTotal: 0, actualReceived: 0, unreceived: 0, totalCost: 0, profit: 0 }; + filteredList.forEach(function (r) { + fields.forEach(function (f) { + var n = parseFloat(r[f]); + if (!isNaN(n)) result[f] += n; + }); + }); + return result; + }, [filteredList]); + + function fmtMoney(v) { + if (v === null || v === undefined || v === '') return '—'; + var n = typeof v === 'number' ? v : parseFloat(v); + if (isNaN(n)) return '—'; + return n.toFixed(2); + } + + function fmtDate(v) { + if (!v || v === '-') return '—'; + return String(v).slice(0, 10); + } + + function getMonthLastDay(year, month) { + var y = parseInt(year, 10); + var m = parseInt(month, 10); + if (isNaN(y) || isNaN(m)) return ''; + var last = new Date(y, m, 0).getDate(); + var mm = m < 10 ? '0' + m : String(m); + var dd = last < 10 ? '0' + last : String(last); + return y + '-' + mm + '-' + dd; + } + + function renderBillDateRange(_, record) { + var start = fmtDate(record.startDate || record.billDate); + var end = fmtDate(record.billDateEnd || getMonthLastDay(record.year, record.month)); + if (start === '—' && end === '—') return '—'; + return React.createElement('div', { style: { lineHeight: 1.35 } }, + React.createElement('div', null, start), + React.createElement('div', { style: { fontSize: 11, color: '#64748b' } }, '至'), + React.createElement('div', null, end) + ); + } + + function renderProfit(v) { + if (v === null || v === undefined || v === '') return '—'; + var n = typeof v === 'number' ? v : parseFloat(v); + if (isNaN(n)) return '—'; + var color = n > 0 ? '#389e0d' : n < 0 ? '#cf1322' : '#666'; + return React.createElement('span', { style: { color: color, fontWeight: 600 } }, n.toFixed(2)); + } + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + var handleQuery = useCallback(function () { + setPeriodApplied(periodDraft); + setCustomerApplied(customerDraft); + setPlateApplied(plateDraft); + setSalespersonApplied(salespersonDraft); + setBillDateApplied(billDateDraft); + setPaymentApplied(paymentDraft); + message.success('查询成功'); + }, [periodDraft, customerDraft, plateDraft, salespersonDraft, billDateDraft, paymentDraft]); + + var handleReset = useCallback(function () { + setPeriodDraft(undefined); + setCustomerDraft(undefined); + setPlateDraft(undefined); + setSalespersonDraft(undefined); + setBillDateDraft(null); + setPaymentDraft(undefined); + setPeriodApplied(undefined); + setCustomerApplied(undefined); + setPlateApplied(undefined); + setSalespersonApplied(undefined); + setBillDateApplied(null); + setPaymentApplied(undefined); + }, []); + + function handleExport() { + message.success('租赁账单导出任务已提交(原型)'); + } + + var columns = [ + { title: '序号', dataIndex: 'seq', key: 'seq', width: 56, fixed: 'left', align: 'center' }, + { + title: '月份', + key: 'yearMonth', + width: 88, + fixed: 'left', + align: 'center', + render: function (_, record) { + var v = fmtYearMonth(record.year, record.month); + return v || '—'; + } + }, + { + title: '业务员', + key: 'salesperson', + width: 96, + align: 'center', + render: function (_, record) { + return React.createElement('div', { style: { lineHeight: 1.3 } }, + React.createElement('div', { style: { fontWeight: 500 } }, record.businessDept || '—'), + React.createElement('div', { style: { fontSize: 11, color: '#64748b' } }, record.salesperson || '—') + ); + } + }, + { title: '账单日期', key: 'billDate', width: 112, align: 'center', render: renderBillDateRange }, + { + title: '车牌号码', + dataIndex: 'plateNo', + key: 'plateNo', + width: 148, + align: 'center', + render: function (v) { + if (!v) return '—'; + var veh = vehicleByPlate[v]; + return React.createElement('div', { style: { lineHeight: 1.35 } }, + React.createElement('div', { style: { fontWeight: 600 } }, v), + veh ? React.createElement(React.Fragment, null, + React.createElement('div', { style: { fontSize: 11, color: '#64748b' } }, veh.brand || '—'), + React.createElement('div', { style: { fontSize: 11, color: '#64748b' } }, veh.model || '—') + ) : null + ); + } + }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 200, ellipsis: true }, + { title: '提车日期', dataIndex: 'pickupDate', key: 'pickupDate', width: 108, align: 'center', render: fmtDate }, + { title: '合同生效日期', dataIndex: 'contractStart', key: 'contractStart', width: 118, align: 'center', render: fmtDate }, + { title: '合同到期日期', dataIndex: 'contractEnd', key: 'contractEnd', width: 118, align: 'center', render: fmtDate }, + { title: '起始日期', dataIndex: 'startDate', key: 'startDate', width: 108, align: 'center', render: fmtDate }, + { title: '退车日期', dataIndex: 'returnDate', key: 'returnDate', width: 108, align: 'center', render: function (v) { return v || '—'; } }, + { title: '平均天数', dataIndex: 'avgDays', key: 'avgDays', width: 80, align: 'center', render: function (v) { return v || '—'; } }, + { title: '押金', dataIndex: 'deposit', key: 'deposit', width: 88, align: 'right', render: fmtMoney }, + { title: '合同标的租金', dataIndex: 'contractRent', key: 'contractRent', width: 108, align: 'right', render: fmtMoney }, + { title: '应收合计', dataIndex: 'receivableTotal', key: 'receivableTotal', width: 96, align: 'right', render: fmtMoney }, + { title: '应收租金', dataIndex: 'receivableRent', key: 'receivableRent', width: 96, align: 'right', render: fmtMoney }, + { title: '保险上浮费', dataIndex: 'insuranceSurcharge', key: 'insuranceSurcharge', width: 96, align: 'right', render: fmtMoney }, + { title: '运维费', dataIndex: 'opsFee', key: 'opsFee', width: 80, align: 'right', render: fmtMoney }, + { title: '其他收入', dataIndex: 'otherIncome', key: 'otherIncome', width: 88, align: 'right', render: fmtMoney }, + { title: '减免金额', dataIndex: 'discount', key: 'discount', width: 88, align: 'right', render: fmtMoney }, + { title: '实收金额', dataIndex: 'actualReceived', key: 'actualReceived', width: 96, align: 'right', render: fmtMoney }, + { + title: '未收', + dataIndex: 'unreceived', + key: 'unreceived', + width: 88, + align: 'right', + render: function (v) { + var n = parseFloat(v); + if (isNaN(n) || n === 0) return fmtMoney(v); + return React.createElement(Tag, { color: 'error', style: { margin: 0 } }, fmtMoney(v)); + } + }, + { title: '开票日期', dataIndex: 'invoiceDate', key: 'invoiceDate', width: 108, align: 'center', render: fmtDate }, + { title: '实际付款日期', dataIndex: 'paymentDate', key: 'paymentDate', width: 118, align: 'center', render: fmtDate }, + { title: '付款方式', dataIndex: 'paymentMethod', key: 'paymentMethod', width: 96, align: 'center' }, + { title: '车辆标的成本', dataIndex: 'vehicleTargetCost', key: 'vehicleTargetCost', width: 108, align: 'right', render: fmtMoney }, + { title: '车辆实际成本', dataIndex: 'vehicleActualCost', key: 'vehicleActualCost', width: 108, align: 'right', render: fmtMoney }, + { title: '保险费', dataIndex: 'insuranceFee', key: 'insuranceFee', width: 80, align: 'right', render: fmtMoney }, + { title: '氢费', dataIndex: 'hydrogenFee', key: 'hydrogenFee', width: 80, align: 'right', render: fmtMoney }, + { title: '运维费(成本)', dataIndex: 'opsCost', key: 'opsCost', width: 100, align: 'right', render: fmtMoney }, + { title: '居间费', dataIndex: 'brokerageFee', key: 'brokerageFee', width: 80, align: 'right', render: fmtMoney }, + { title: '其他', dataIndex: 'otherCost', key: 'otherCost', width: 80, align: 'right', render: fmtMoney }, + { title: '总成本', dataIndex: 'totalCost', key: 'totalCost', width: 96, align: 'right', render: fmtMoney }, + { title: '盈亏', dataIndex: 'profit', key: 'profit', width: 88, align: 'right', render: renderProfit }, + { title: '实际盈亏', dataIndex: 'actualProfit', key: 'actualProfit', width: 96, align: 'right', render: renderProfit }, + { title: '减免金额明细', dataIndex: 'discountDetail', key: 'discountDetail', width: 140, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '资产归属', dataIndex: 'assetOwner', key: 'assetOwner', width: 180, ellipsis: true }, + { title: '享免政策及增值服务', dataIndex: 'policy', key: 'policy', width: 140, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '签约公司', dataIndex: 'signCompany', key: 'signCompany', width: 180, ellipsis: true }, + { title: '备注', dataIndex: 'remark', key: 'remark', width: 120, ellipsis: true, render: function (v) { return v || '—'; } } + ]; + + var renderTotalsBar = useCallback(function () { + var profitCls = 'rb-ledger-totals-bar__value'; + if (totals.profit > 0) profitCls += ' is-profit-pos'; + else if (totals.profit < 0) profitCls += ' is-profit-neg'; + var items = [ + { key: 'receivableTotal', label: '应收合计(元)', value: fmtMoney(totals.receivableTotal) }, + { key: 'actualReceived', label: '实收金额(元)', value: fmtMoney(totals.actualReceived) }, + { key: 'unreceived', label: '未收(元)', value: fmtMoney(totals.unreceived) }, + { key: 'totalCost', label: '总成本(元)', value: fmtMoney(totals.totalCost) }, + { key: 'profit', label: '盈亏(元)', value: fmtMoney(totals.profit), valueClass: profitCls } + ]; + return React.createElement('div', { className: 'rb-ledger-totals-bar' }, + React.createElement('div', { className: 'rb-ledger-totals-bar__title' }, '合计'), + React.createElement('div', { className: 'rb-ledger-totals-bar__items' }, + items.map(function (item) { + return React.createElement('div', { key: item.key, className: 'rb-ledger-totals-bar__item' }, + React.createElement('div', { className: 'rb-ledger-totals-bar__label' }, item.label), + React.createElement('div', { className: item.valueClass || 'rb-ledger-totals-bar__value' }, item.value) + ); + }) + ) + ); + }, [totals]); + + var requirementContent = '租赁账单(2026年6月版本)\n一个「数字化资产ONEOS运管平台」中的「财务管理」「租赁账单」模块\n#面包屑:财务管理-租赁账单;\n\n页面基于「租赁业务运营明细台账-明细表」字段设计,交互与视觉参照「台账数据-车辆氢费明细」。\n\n1.筛选:修改条件后点击「查询」生效;\n1.1.客户名称:选项取自系统「客户管理」;\n1.2.车牌号码:选项取自系统「车辆管理」;\n\n2.列表:顶部合计条展示应收、实收、未收、总成本、盈亏;\n#右上角支持导出;'; + + var filterColProps = { xs: 24, sm: 12, lg: 6 }; + var filterActions = React.createElement(Space, { wrap: true, style: { justifyContent: 'flex-end', width: '100%' } }, + React.createElement(Button, { type: 'link', style: { padding: '4px 0' }, onClick: function () { setFilterExpanded(!filterExpanded); } }, filterExpanded ? '收起' : '展开'), + React.createElement(Button, { onClick: handleReset }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleQuery }, '查询') + ); + + return React.createElement(App, null, + React.createElement('style', null, ledgerTableStyle), + React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12, gap: 16 } }, + React.createElement(Breadcrumb, { items: [{ title: '财务管理' }, { title: '租赁账单' }] }), + React.createElement(Button, { type: 'link', style: { padding: 0, flexShrink: 0, fontSize: 14 }, onClick: function () { requirementModalVisible[1](true); } }, '查看需求说明') + ), + + React.createElement(Card, { style: filterCardStyle, bodyStyle: { paddingBottom: 4 } }, + React.createElement(Row, { gutter: [16, 0], align: 'bottom' }, + React.createElement(Col, filterColProps, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '月份'), + React.createElement(Select, { placeholder: '全部', allowClear: true, style: filterControlStyle, value: periodDraft, onChange: setPeriodDraft, options: periodOptions }) + ) + ), + React.createElement(Col, filterColProps, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, filterExpanded ? '客户名称' : '\u00a0'), + filterExpanded + ? React.createElement(Select, { placeholder: '全部', allowClear: true, showSearch: true, style: filterControlStyle, value: customerDraft, onChange: setCustomerDraft, options: customerOptions, filterOption: filterOption }) + : React.createElement('div', { style: { textAlign: 'right' } }, filterActions) + ) + ), + filterExpanded ? React.createElement(Col, filterColProps, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '车牌号码'), + React.createElement(Select, { placeholder: '全部', allowClear: true, showSearch: true, style: filterControlStyle, value: plateDraft, onChange: setPlateDraft, options: plateOptions, filterOption: filterOption }) + ) + ) : null + ), + filterExpanded ? [ + React.createElement(Row, { key: 'filter-row-2', gutter: [16, 0], align: 'top' }, + React.createElement(Col, filterColProps, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '业务员'), + React.createElement(Select, { placeholder: '全部', allowClear: true, showSearch: true, style: filterControlStyle, value: salespersonDraft, onChange: setSalespersonDraft, options: salespersonOptions, filterOption: filterOption }) + ) + ), + React.createElement(Col, filterColProps, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '账单日期'), + React.createElement(RangePicker, { style: filterControlStyle, value: billDateDraft, onChange: setBillDateDraft, format: 'YYYY-MM-DD', separator: '至' }) + ) + ), + React.createElement(Col, filterColProps, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '付款方式'), + React.createElement(Select, { placeholder: '全部', allowClear: true, style: filterControlStyle, value: paymentDraft, onChange: setPaymentDraft, options: paymentMethodOptions }) + ) + ) + ), + React.createElement(Row, { key: 'filter-actions', justify: 'end', style: { marginTop: -4 } }, + React.createElement(Col, { span: 24 }, + React.createElement('div', { style: { textAlign: 'right', marginBottom: 4 } }, filterActions) + ) + ) + ] : null + ), + + React.createElement(Card, { style: tableCardStyle, bodyStyle: { padding: '20px 20px 24px' } }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8, minHeight: 36, gap: 12 } }, + React.createElement('span', { style: { fontSize: 13, color: 'rgba(15,23,42,0.55)' } }, '共 ' + filteredList.length + ' 条'), + React.createElement('div', { style: { flex: 1, textAlign: 'center', fontSize: 18, fontWeight: 700, color: 'rgba(15,23,42,0.92)' } }, '租赁账单'), + React.createElement(Space, { size: 8 }, + React.createElement(Button, { onClick: handleExport }, '导出') + ) + ), + renderTotalsBar(), + React.createElement('div', { className: 'rb-ledger-table-wrap' }, + React.createElement(Table, { + className: 'rb-ledger-table', + rowKey: 'key', + columns: columns, + dataSource: filteredList, + size: 'small', + bordered: true, + rowClassName: function () { return 'rb-row-data'; }, + pagination: { pageSize: 20, showSizeChanger: true, showTotal: function (t) { return '共 ' + t + ' 条'; } }, + scroll: { x: 'max-content', y: filterExpanded ? 'calc(100vh - 524px)' : 'calc(100vh - 464px)' }, + sticky: true + }) + ) + ), + + React.createElement(Modal, { + title: '需求说明', + open: requirementModalVisible[0], + onCancel: function () { requirementModalVisible[1](false); }, + width: 720, + footer: React.createElement(Button, { onClick: function () { requirementModalVisible[1](false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6 } }, requirementContent)) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-h2-station/index.tsx b/src/prototypes/oneos-web-h2-station/index.tsx new file mode 100644 index 0000000..a0c37f4 --- /dev/null +++ b/src/prototypes/oneos-web-h2-station/index.tsx @@ -0,0 +1,25 @@ +/** + * @name 加氢站管理 + * 自 ONE-OS web端 原稿复刻(3 个页面) + */ +import React from 'react'; +import Page1 from './pages/01-加氢订单.jsx'; +import Page2 from './pages/02-加氢记录.jsx'; +import Page3 from './pages/03-站点信息.jsx'; +import { OneosWebLegacyShell } from '../../common/oneosWebLegacy/OneosWebLegacyShell'; + +const pages = [ + { id: 'u52a0u6c22u7ad9u7ba1u7406-u52a0u6c22u8ba2u5355', title: '加氢订单', component: Page1 }, + { id: 'u52a0u6c22u7ad9u7ba1u7406-u52a0u6c22u8bb0u5f55', title: '加氢记录', component: Page2 }, + { id: 'u52a0u6c22u7ad9u7ba1u7406-u7ad9u70b9u4fe1u606f', title: '站点信息', component: Page3 }, +]; + +export default function OneosWebH2Station() { + return ( + + ); +} diff --git a/src/prototypes/oneos-web-h2-station/pages/01-加氢订单.jsx b/src/prototypes/oneos-web-h2-station/pages/01-加氢订单.jsx new file mode 100644 index 0000000..94bd53a --- /dev/null +++ b/src/prototypes/oneos-web-h2-station/pages/01-加氢订单.jsx @@ -0,0 +1,182 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 加氢站管理 - 加氢订单(列表 + 筛选)(2026年3月版) + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Table = antd.Table; + var Button = antd.Button; + var Select = antd.Select; + var Input = antd.Input; + var DatePicker = antd.DatePicker; + var Space = antd.Space; + var Modal = antd.Modal; + var message = antd.message; + + var RangePicker = DatePicker.RangePicker || DatePicker.RangePicker; + + // 筛选状态 + var stationState = useState(undefined); + var timeRangeState = useState([]); + var plateNoState = useState(''); + var station = stationState[0]; + var setStation = stationState[1]; + var timeRange = timeRangeState[0]; + var setTimeRange = timeRangeState[1]; + var plateNo = plateNoState[0]; + var setPlateNo = plateNoState[1]; + + var requirementModalVisible = useState(false); + + // 模拟:加氢站与加氢记录数据 + var stationList = useMemo(function () { + return [ + { value: 'JX-H2-001', label: '嘉兴加氢站(一期)' }, + { value: 'HZ-H2-002', label: '杭州临平加氢站' }, + { value: 'SH-H2-003', label: '上海宝山加氢站' } + ]; + }, []); + + var rawData = useMemo(function () { + return [ + { id: 1, stationCode: 'JX-H2-001', stationName: '嘉兴加氢站(一期)', time: '2026-03-01 10:21', plateNo: '浙A12345', amountKg: 12.5, amountYuan: 625.00 }, + { id: 2, stationCode: 'JX-H2-001', stationName: '嘉兴加氢站(一期)', time: '2026-03-01 14:08', plateNo: '浙A67890', amountKg: 10.0, amountYuan: 500.00 }, + { id: 3, stationCode: 'HZ-H2-002', stationName: '杭州临平加氢站', time: '2026-03-02 09:30', plateNo: '浙B23456', amountKg: 15.3, amountYuan: 765.00 }, + { id: 4, stationCode: 'SH-H2-003', stationName: '上海宝山加氢站', time: '2026-03-03 16:45', plateNo: '沪A88888', amountKg: 8.0, amountYuan: 400.00 }, + { id: 5, stationCode: 'HZ-H2-002', stationName: '杭州临平加氢站', time: '2026-03-03 18:10', plateNo: '浙B99999', amountKg: 18.2, amountYuan: 910.00 } + ]; + }, []); + + // 简单字符串时间过滤(示例用,实际可改为 dayjs 比较) + function inRange(timeStr, range) { + if (!range || !range.length || !range[0] || !range[1]) return true; + // timeStr: 'YYYY-MM-DD HH:mm',只做字符串比较以示意 + var t = timeStr.replace(/[-:\\s]/g, ''); + var s = range[0].format ? range[0].format('YYYYMMDDHHmm') : ''; + var e = range[1].format ? range[1].format('YYYYMMDDHHmm') : ''; + if (!s || !e) return true; + return t >= s && t <= e; + } + + var filteredData = useMemo(function () { + var list = rawData.slice(); + if (station) { + list = list.filter(function (r) { return r.stationCode === station; }); + } + if (plateNo && plateNo.trim()) { + var kw = plateNo.trim().toLowerCase(); + list = list.filter(function (r) { return (r.plateNo || '').toLowerCase().indexOf(kw) !== -1; }); + } + if (timeRange && timeRange.length === 2) { + list = list.filter(function (r) { return inRange(r.time, timeRange); }); + } + return list.map(function (r, idx) { + return Object.assign({}, r, { seq: idx + 1 }); + }); + }, [rawData, station, plateNo, timeRange]); + + var columns = [ + { title: '序号', dataIndex: 'seq', key: 'seq', width: 70, align: 'center' }, + { title: '加氢站', dataIndex: 'stationName', key: 'stationName', width: 200 }, + { title: '加氢时间', dataIndex: 'time', key: 'time', width: 180 }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 120 }, + { title: '加氢量(kg)', dataIndex: 'amountKg', key: 'amountKg', width: 120, align: 'right', render: function (v) { return (v != null ? v.toFixed(2) : '0.00'); } }, + { title: '金额(元)', dataIndex: 'amountYuan', key: 'amountYuan', width: 120, align: 'right', render: function (v) { var n = typeof v === 'number' ? v : parseFloat(v); return (isNaN(n) ? '0.00' : n.toFixed(2)); } } + ]; + + var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; + var filterLabelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var filterItemStyle = { marginBottom: 12 }; + + function handleReset() { + setStation(undefined); + setTimeRange([]); + setPlateNo(''); + } + + function handleExport() { + message.info('导出当前筛选条件下的加氢订单(示例)'); + } + + var requirementContent = '加氢订单:一个「数字化资产ONEOS运管平台」中的「加氢站管理-加氢订单」模块。\n\n#1.面包屑:加氢站管理-加氢订单\n\n#2.筛选:支持加氢站、加氢时间、车牌号等筛选;\n2.1.加氢站:选择器,选择加氢站;\n2.2.加氢时间:日期时间范围选择器,支持开始时间-结束时间;\n2.3.车牌号:输入框,支持模糊匹配;\n右侧为查询、重置按钮。\n\n#3.列表:加氢订单列表,展示序号、加氢站、加氢时间、车牌号、加氢量(kg)、金额(元);支持分页与导出。'; + + return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + React.createElement(Breadcrumb, { + items: [ + { title: '加氢站管理' }, + { title: '加氢订单' } + ] + }), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { requirementModalVisible[1](true); } }, '查看需求说明') + ), + React.createElement(Card, { title: '筛选', style: cardStyle }, + React.createElement('div', { + style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px 24px', alignItems: 'flex-end' } + }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '加氢站'), + React.createElement(Select, { + placeholder: '请选择加氢站', + allowClear: true, + style: { width: '100%' }, + value: station, + onChange: function (v) { setStation(v); }, + options: stationList + }) + ), + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '加氢时间'), + React.createElement(RangePicker, { + style: { width: '100%' }, + showTime: true, + placeholder: ['开始时间', '结束时间'], + value: timeRange, + onChange: function (v) { setTimeRange(v || []); } + }) + ), + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '车牌号'), + React.createElement(Input, { + placeholder: '请输入车牌号', + value: plateNo, + onChange: function (e) { setPlateNo(e.target.value); } + }) + ) + ), + React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 } }, + React.createElement(Button, { onClick: handleReset }, '重置'), + React.createElement(Button, { type: 'primary' }, '查询') + ) + ), + React.createElement(Card, { title: '加氢订单列表', style: cardStyle }, + React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', marginBottom: 16 } }, + React.createElement(Button, { onClick: handleExport }, '导出') + ), + React.createElement(Table, { + rowKey: 'id', + columns: columns, + dataSource: filteredData, + pagination: { pageSize: 10, showSizeChanger: true, showTotal: function (t) { return '共 ' + t + ' 条'; } }, + bordered: true, + size: 'middle', + scroll: { x: 700 } + }) + ), + React.createElement(Modal, { + title: '需求说明', + open: requirementModalVisible[0], + onCancel: function () { requirementModalVisible[1](false); }, + width: 560, + footer: React.createElement(Button, { onClick: function () { requirementModalVisible[1](false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, React.createElement('div', { style: { padding: '8px 0', whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6 } }, requirementContent)) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-h2-station/pages/02-加氢记录.jsx b/src/prototypes/oneos-web-h2-station/pages/02-加氢记录.jsx new file mode 100644 index 0000000..a30fee1 --- /dev/null +++ b/src/prototypes/oneos-web-h2-station/pages/02-加氢记录.jsx @@ -0,0 +1,1809 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 加氢站管理 - 加氢记录(列表 + 筛选 + 预约加氢弹窗 + 卡片式新增/编辑 + 导出) + +function hrSvgIcon(paths, size) { + return React.createElement('svg', { + viewBox: '0 0 24 24', + width: size || 18, + height: size || 18, + fill: 'none', + stroke: 'currentColor', + strokeWidth: 2, + strokeLinecap: 'round', + strokeLinejoin: 'round', + 'aria-hidden': true + }, paths.map(function (p, i) { + if (p.tag === 'circle') return React.createElement('circle', { key: i, cx: p.cx, cy: p.cy, r: p.r }); + if (p.tag === 'line') return React.createElement('line', { key: i, x1: p.x1, y1: p.y1, x2: p.x2, y2: p.y2 }); + return React.createElement('path', { key: i, d: p.d }); + })); +} + +var HR_ICONS = { + back: hrSvgIcon([{ tag: 'line', x1: 19, y1: 12, x2: 5, y2: 12 }, { d: 'M12 19l-7-7 7-7' }], 16), + fuel: hrSvgIcon([{ d: 'M3 22h12M5 22V10l7-4v16M19 22V8l-4-2v16' }, { tag: 'circle', cx: 8.5, cy: 17.5, r: 1.5 }], 18), + user: hrSvgIcon([{ d: 'M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2' }, { tag: 'circle', cx: 12, cy: 7, r: 4 }], 16), + calendar: hrSvgIcon([{ d: 'M19 4H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z' }, { tag: 'line', x1: 16, y1: 2, x2: 16, y2: 6 }, { tag: 'line', x1: 8, y1: 2, x2: 8, y2: 6 }, { tag: 'line', x1: 3, y1: 10, x2: 21, y2: 10 }], 16), + empty: hrSvgIcon([{ tag: 'circle', cx: 12, cy: 12, r: 10 }, { tag: 'line', x1: 8, y1: 12, x2: 16, y2: 12 }], 40) +}; + +/** @see web端/styles/oneos-ant-table-global-fix.js */ +var ONEOS_ANT_TABLE_GLOBAL_FIX = [ + '.ant-table-container .ant-table-header { margin-bottom: 0 !important; }', + '.ant-table-container .ant-table-body { margin-top: 0 !important; }', + '.ant-table-container .ant-table-body > table, .ant-table-content table { margin-top: 0 !important; }', + '.ant-table-tbody > tr.ant-table-measure-row, .ant-table-tbody > tr.ant-table-measure-row > td, .ant-table-tbody > tr.ant-table-measure-row > th { display: none !important; height: 0 !important; max-height: 0 !important; min-height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; line-height: 0 !important; font-size: 0 !important; overflow: hidden !important; visibility: hidden !important; pointer-events: none !important; }' +]; + +var HR_PAGE_STYLE = ONEOS_ANT_TABLE_GLOBAL_FIX.concat([ + '.h2-station-page { --h2-form-row-gap: 20px; --h2-form-col-gap: 24px; --h2-form-label-gap: 8px; padding: 24px 24px 80px; height: 100vh; display: flex; flex-direction: column; background: linear-gradient(165deg, #f1f5f9 0%, #f8fafc 50%, #f1f5f9 100%); overflow: hidden; box-sizing: border-box; }', + '.h2-station-page.h2-station-page--create { padding: 0 0 96px; height: auto; min-height: 100dvh; overflow: auto; }', + '.h2-station-page--create .h2-create-shell { width: 100%; max-width: none; margin: 0; padding: 24px 24px 0; box-sizing: border-box; }', + '.h2-station-page--create .h2-create-topbar { display: flex; align-items: center; min-height: 40px; margin-bottom: 16px; position: relative; }', + '.h2-station-page--create .h2-create-topbar-title { position: absolute; left: 50%; transform: translateX(-50%); margin: 0; font-size: 18px; font-weight: 700; color: #0f172a; line-height: 1.35; pointer-events: none; white-space: nowrap; }', + '.h2-station-page--create .h2-create-back-btn { display: inline-flex !important; align-items: center; gap: 6px; height: 32px; padding: 0 12px !important; border-radius: 8px !important; font-weight: 500; color: #475569 !important; border: 1px solid #e2e8f0 !important; background: #fff !important; }', + '.h2-station-page--create .h2-create-back-btn:hover { color: #059669 !important; border-color: #10b981 !important; background: #f0fdf4 !important; }', + '.h2-station-page .h2-card-title-bar--step::before { display: none !important; }', + '.h2-station-page .h2-card-step-badge { width: 28px; height: 28px; border-radius: 50%; background: linear-gradient(145deg, #10b981 0%, #059669 100%); color: #fff; font-size: 14px; font-weight: 700; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; box-shadow: 0 2px 8px rgba(16, 185, 129, 0.32); }', + '.h2-station-page .h2-card-title-text { font-size: 15px; font-weight: 700; color: #0f172a; }', + '.h2-station-page--create .h2-create-form > .h2-create-card + .h2-create-card { margin-top: 16px; }', + '.h2-station-page--create .h2-create-footer-inner { justify-content: flex-end; }', + '@media (prefers-reduced-motion: no-preference) { .h2-station-page--create .h2-create-card { animation: hrCreateCardIn 0.35s ease-out backwards; } .h2-station-page--create .h2-create-card + .h2-create-card { animation-delay: 0.06s; } }', + '@keyframes hrCreateCardIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }', + '.h2-station-page .h2-create-card.ant-card { border-radius: 16px !important; border: 1px solid #e2e8f0 !important; box-shadow: 0 4px 24px -6px rgba(15, 23, 42, 0.06) !important; margin-bottom: 16px !important; }', + '.h2-station-page .h2-create-card > .ant-card-head { border-bottom: 1px solid #f1f5f9 !important; min-height: auto; padding: 16px 22px !important; background: linear-gradient(180deg, #fafbfc 0%, #fff 100%); }', + '.h2-station-page .h2-create-card > .ant-card-head .ant-card-head-title { font-size: 15px !important; font-weight: 700 !important; color: #0f172a !important; padding: 0 !important; }', + '.h2-station-page .h2-create-card > .ant-card-body { padding: 20px 24px 24px !important; }', + '.h2-station-page .h2-card-title-bar { display: inline-flex; align-items: center; gap: 10px; }', + '.h2-station-page .h2-card-title-bar::before { content: ""; width: 3px; height: 16px; border-radius: 2px; background: linear-gradient(180deg, #10b981, #34d399); flex-shrink: 0; }', + '.h2-station-page .h2-card-title-icon { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 10px; background: #ecfdf5; color: #059669; flex-shrink: 0; }', + '.h2-station-page--create .h2-create-form--grid .h2-create-form-grid { width: 100%; }', + '.h2-station-page--create .h2-create-form--grid .ant-row { width: 100% !important; margin-left: 0 !important; margin-right: 0 !important; }', + '.h2-station-page--create .h2-create-form--grid .ant-col { min-width: 0; }', + '.h2-station-page--create .h2-create-form--grid .ant-form-item { margin-bottom: 0; width: 100%; }', + '.h2-station-page--create .h2-create-form--grid .ant-form-item-label { padding: 0 0 var(--h2-form-label-gap); min-height: 22px; }', + '.h2-station-page--create .h2-create-form--grid .ant-form-item-label > label { font-size: 13px; font-weight: 500; color: #475569; }', + '.h2-station-page--create .h2-create-form--grid .ant-col > .ant-form-item { display: flex; flex-direction: column; }', + '.h2-station-page--create .h2-create-form--grid .ant-form-item-control { flex: 1; width: 100%; }', + '.h2-station-page--create .h2-create-form--grid .ant-form-item-control-input, .h2-station-page--create .h2-create-form--grid .ant-form-item-control-input-content { width: 100%; max-width: 100%; }', + '.h2-station-page--create .h2-create-form--grid .h2-create-input, .h2-station-page--create .h2-create-form--grid .ant-select, .h2-station-page--create .h2-create-form--grid .ant-picker, .h2-station-page--create .h2-create-form--grid .ant-input-number { width: 100% !important; max-width: 100% !important; }', + '.h2-station-page--create .h2-create-form--list .ant-input:not(.ant-input-disabled), .h2-station-page--create .h2-create-form--list .ant-select:not(.ant-select-disabled) .ant-select-selector, .h2-station-page--create .h2-create-form--list .ant-picker:not(.ant-picker-disabled), .h2-station-page--create .h2-create-form--list .ant-input-number:not(.ant-input-number-disabled) { border-radius: 8px !important; border: 1px solid #e2e8f0 !important; min-height: 32px !important; height: 32px !important; font-size: 13px !important; background: #fff !important; color: #0f172a !important; width: 100% !important; }', + '.h2-station-page--create .h2-create-subsection-title { margin: 4px 0 0; padding: 12px 0 10px; font-size: 13px; font-weight: 600; color: #475569; border-bottom: 1px solid #f1f5f9; width: 100%; box-sizing: border-box; }', + '.h2-station-page .h2-create-footer { position: fixed; bottom: 0; left: 0; right: 0; z-index: 100; background: rgba(255, 255, 255, 0.92); backdrop-filter: blur(10px); border-top: 1px solid #e2e8f0; box-shadow: 0 -8px 24px rgba(15, 23, 42, 0.06); }', + '.h2-station-page .h2-create-footer-inner { width: 100%; padding: 14px 24px; display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; box-sizing: border-box; }', + '.h2-station-page .h2-create-footer-hint { font-size: 13px; color: #64748b; display: flex; align-items: center; gap: 10px; min-width: 0; }', + '.h2-station-page .h2-create-footer-progress { flex: 1; min-width: 120px; max-width: 200px; }', + '.h2-station-page .h2-create-footer-progress .ant-progress-text { font-size: 12px !important; font-weight: 700; color: #059669 !important; }', + '.h2-station-page .h2-create-footer-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-left: auto; }', + '.h2-station-page .h2-create-footer-actions .ant-btn { min-height: 44px; padding: 0 20px; border-radius: 10px; font-weight: 600; }', + '.h2-station-page .h2-create-footer-actions .ant-btn-primary { min-width: 120px; box-shadow: 0 4px 14px -2px rgba(16, 185, 129, 0.45); }', + '.h2-station-page .lc-filter-card.ant-card { border-radius: 16px !important; border: 1px solid #e2e8f0 !important; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.03) !important; margin-bottom: 16px; }', + '.h2-station-page .lc-filter-card > .ant-card-head { border-bottom: 1px solid #f1f5f9 !important; min-height: auto; padding: 12px 20px !important; }', + '.h2-station-page .lc-filter-card > .ant-card-head .ant-card-head-title { font-size: 15px !important; font-weight: 700 !important; color: #0f172a !important; padding: 0 !important; }', + '.h2-station-page .lc-filter-card > .ant-card-body { padding: 16px 20px 20px !important; }', + '.h2-station-page .lc-filter-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px 24px; align-items: center; }', + '@media (max-width: 1280px) { .h2-station-page .lc-filter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }', + '@media (max-width: 640px) { .h2-station-page .lc-filter-grid { grid-template-columns: 1fr; } }', + '.h2-station-page .lc-filter-field { display: flex; align-items: center; gap: 12px; min-width: 0; min-height: 32px; }', + '.h2-station-page .lc-filter-field-label { flex: 0 0 88px; text-align: right; font-size: 13px; font-weight: 500; color: #475569; line-height: 32px; white-space: nowrap; }', + '.h2-station-page .lc-filter-field-control { flex: 1; min-width: 0; }', + '.h2-station-page .lc-filter-field-control .ant-input, .h2-station-page .lc-filter-field-control .ant-input-affix-wrapper, .h2-station-page .lc-filter-field-control .ant-select .ant-select-selector, .h2-station-page .lc-filter-field-control .ant-picker, .h2-station-page .lc-filter-field-control .ant-picker-range { width: 100%; height: 32px !important; min-height: 32px !important; border-radius: 8px !important; box-sizing: border-box; }', + '.h2-station-page .lc-filter-field-control .ant-input-affix-wrapper { display: inline-flex; align-items: center; padding-top: 0 !important; padding-bottom: 0 !important; }', + '.h2-station-page .lc-filter-field-control .ant-select-single .ant-select-selector { display: flex; align-items: center; }', + '.h2-station-page .lc-filter-field-control .ant-select-single .ant-select-selector .ant-select-selection-item, .h2-station-page .lc-filter-field-control .ant-select-single .ant-select-selector .ant-select-selection-placeholder { line-height: 30px !important; }', + '.h2-station-page .lc-filter-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; padding-top: 16px; border-top: 1px solid #f1f5f9; }', + '.h2-station-page .lc-table-section { margin-bottom: 0; flex: 1; display: flex; flex-direction: column; min-height: 0; }', + '.h2-station-page .lc-table-toolbar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px 16px; margin-bottom: 8px; min-height: 32px; }', + '.h2-station-page .lc-table-toolbar-left { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }', + '.h2-station-page .lc-table-toolbar-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-left: auto; }', + '.h2-station-page .lc-table-card { background: #fff; border-radius: 16px; border: 1px solid #e2e8f0; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.03); overflow: hidden; flex: 1; overflow-y: auto; display: flex; flex-direction: column; }', + '.h2-station-page .lc-list-table .ant-table-wrapper, .h2-station-page .lc-list-table .ant-table { width: 100% !important; }', + '.h2-station-page .lc-table-card .ant-table-thead > tr > th { background: #f8fafc !important; color: #475569 !important; font-weight: 700 !important; font-size: 13px !important; border-bottom: 1px solid #e2e8f0 !important; padding: 12px 16px !important; }', + '.h2-station-page .lc-table-card .ant-table-tbody > tr.ant-table-measure-row, .h2-station-page .lc-table-card .ant-table-tbody > tr.ant-table-measure-row > td { height: 0 !important; max-height: 0 !important; padding: 0 !important; border: none !important; visibility: hidden !important; }', + '.h2-station-page .lc-table-card .ant-table-tbody > tr:not(.ant-table-measure-row) > td { padding: 12px 16px !important; font-size: 13px; }', + '.h2-station-page .lc-table-card .ant-table-tbody > tr:not(.ant-table-measure-row):hover > td { background: #f8fafc !important; }', + '.h2-station-page .lc-table-card .ant-pagination { margin: 0 !important; padding: 12px 16px !important; border-top: 1px solid #f1f5f9; }', + '.h2-station-page .lc-action-btn { font-weight: 600 !important; color: #10b981 !important; padding: 0 !important; }', + '.h2-station-page .lc-action-btn-danger { color: #ef4444 !important; }', + '.h2-station-page .h2-row-actions { display: inline-flex; align-items: center; gap: 4px; }', + '.h2-station-page .hr-refuel-plate { font-weight: 700; color: #0f172a; font-variant-numeric: tabular-nums; }', + '.h2-station-page .hr-refuel-kg { font-weight: 700; color: #059669; font-variant-numeric: tabular-nums; }', + '.h2-station-page .hr-refuel-money { font-weight: 600; color: #4f46e5; font-variant-numeric: tabular-nums; }', + '.h2-station-page .hr-refuel-km { font-variant-numeric: tabular-nums; color: #334155; }', + '.h2-station-page .hr-refuel-time { font-variant-numeric: tabular-nums; color: #334155; font-size: 13px; white-space: nowrap; }', + '.h2-station-page .hr-appointment-driver { font-weight: 600; color: #0f172a; }', + '.h2-station-page .hr-appointment-contact { font-variant-numeric: tabular-nums; color: #475569; }', + '.h2-station-page .hr-appointment-reject-reason { font-size: 12px; color: #94a3b8; margin-top: 4px; max-width: 220px; }', + '.h2-refuel-drill-modal .ant-modal-content { border-radius: 16px !important; overflow: hidden; box-shadow: 0 24px 48px -12px rgba(15, 23, 42, 0.18) !important; }', + '.h2-refuel-drill-modal .ant-modal-header { padding: 18px 24px 14px !important; border-bottom: 1px solid #f1f5f9 !important; margin-bottom: 0 !important; }', + '.h2-refuel-drill-modal .ant-modal-title { font-size: 17px !important; font-weight: 700 !important; color: #0f172a !important; }', + '.h2-refuel-drill-modal .ant-modal-body { padding: 16px 24px 20px !important; background: #f8fafc; }', + '.h2-refuel-drill-modal .ant-modal-footer { padding: 12px 24px 18px !important; border-top: 1px solid #f1f5f9 !important; background: #fff; }', + '.h2-refuel-drill-modal .h2-refuel-drill-panel { display: flex; flex-direction: column; gap: 14px; }', + '.h2-refuel-drill-modal .h2-refuel-drill-station-card { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 12px 16px; background: linear-gradient(135deg, #eff6ff 0%, #ecfdf5 50%, #f8fafc 100%); border: 1px solid #bfdbfe; border-radius: 12px; }', + '.h2-refuel-drill-modal .h2-refuel-drill-station-card__name { font-size: 15px; font-weight: 700; color: #0f172a; line-height: 1.35; }', + '.h2-refuel-drill-modal .h2-refuel-drill-station-card__meta { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; white-space: nowrap; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }', + '@media (max-width: 860px) { .h2-refuel-drill-modal .h2-refuel-drill-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } }', + '@media (max-width: 520px) { .h2-refuel-drill-modal .h2-refuel-drill-stats { grid-template-columns: 1fr; } }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat { display: flex; flex-direction: column; justify-content: center; min-height: 78px; padding: 12px 14px; border-radius: 12px; border: 1px solid #e2e8f0; background: #fff; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06); min-width: 0; box-sizing: border-box; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat--count { border-left: 4px solid #3b82f6; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat--kg { border-left: 4px solid #10b981; background: linear-gradient(180deg, #fff 0%, #f0fdf4 100%); }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat--cost { border-left: 4px solid #f97316; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat--customer { border-left: 4px solid #6366f1; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat__label { font-size: 12px; font-weight: 600; color: #64748b; margin-bottom: 8px; line-height: 1.2; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat__value { font-size: 20px; font-weight: 800; font-variant-numeric: tabular-nums; line-height: 1.2; color: #0f172a; word-break: break-all; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat__value--count { color: #2563eb; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat__value--kg { color: #059669; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat__value--cost { color: #ea580c; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat__value--customer { color: #4f46e5; }', + '.h2-refuel-drill-modal .h2-refuel-drill-table-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-refuel-drill-modal .h2-refuel-drill-table-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 14px; border-bottom: 1px solid #f1f5f9; background: #fafbfc; }', + '.h2-refuel-drill-modal .h2-refuel-drill-table-head__left { display: flex; align-items: center; gap: 10px; min-width: 0; }', + '.h2-refuel-drill-modal .h2-refuel-drill-table-head__title { font-size: 13px; font-weight: 700; color: #334155; }', + '.h2-refuel-drill-modal .h2-refuel-drill-table-head__count { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; }', + '.h2-refuel-drill-modal .h2-refuel-record-table { border-radius: 0 !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table { background: transparent !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-container { border: none !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-header { margin-bottom: 0 !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-body { margin-top: 0 !important; overflow-y: auto !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-body table { margin-top: 0 !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-tbody > tr.ant-table-measure-row, .h2-refuel-drill-modal .h2-refuel-record-table .ant-table-tbody > tr.ant-table-measure-row > td { height: 0 !important; max-height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; line-height: 0 !important; font-size: 0 !important; overflow: hidden !important; visibility: hidden !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-thead > tr > th { background: #f8fafc !important; font-size: 12px !important; font-weight: 700 !important; color: #475569 !important; padding: 10px 12px !important; border-bottom: 1px solid #e2e8f0 !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-tbody > tr:not(.ant-table-measure-row) > td { font-size: 13px !important; padding: 10px 12px !important; vertical-align: middle !important; border-bottom: 1px solid #f8fafc !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-tbody > tr:not(.ant-table-measure-row):last-child > td { border-bottom: none !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-tbody > tr:hover > td { background: #eff6ff !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-seq { display: inline-flex; align-items: center; justify-content: center; min-width: 24px; height: 24px; border-radius: 6px; background: #f1f5f9; color: #64748b; font-size: 12px; font-weight: 700; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-time { color: #334155; font-variant-numeric: tabular-nums; font-size: 12px; white-space: nowrap; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-plate { font-weight: 700; color: #0f172a; font-variant-numeric: tabular-nums; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-kg { font-weight: 700; color: #059669; font-variant-numeric: tabular-nums; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-contact { font-variant-numeric: tabular-nums; color: #475569; font-size: 13px; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-driver { font-weight: 600; color: #0f172a; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .hr-appointment-reject-reason { font-size: 12px; color: #94a3b8; margin-top: 4px; max-width: 220px; }', + '.h2-station-page .hr-daily-stats-section { margin-bottom: 16px; }', + '.h2-station-page .hr-daily-stats-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 10px; }', + '.h2-station-page .hr-daily-stats-head__title { font-size: 15px; font-weight: 700; color: #0f172a; }', + '.h2-station-page .hr-daily-stats-head__meta { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; }', + '.h2-station-page .hr-daily-stats-carousel { display: flex; align-items: stretch; gap: 8px; }', + '.h2-station-page .hr-daily-stats-nav-btn { flex: 0 0 36px; width: 36px !important; min-width: 36px !important; height: auto !important; min-height: 88px !important; padding: 0 !important; border-radius: 10px !important; font-size: 20px !important; font-weight: 700 !important; color: #475569 !important; border: 1px solid #e2e8f0 !important; background: #fff !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06); }', + '.h2-station-page .hr-daily-stats-nav-btn:hover:not(:disabled) { color: #059669 !important; border-color: #10b981 !important; background: #f0fdf4 !important; }', + '.h2-station-page .hr-daily-stats-nav-btn:disabled { opacity: 0.35; cursor: not-allowed; box-shadow: none; }', + '.h2-station-page .hr-daily-stats-track { flex: 1; min-width: 0; display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 8px; }', + '.h2-station-page .hr-daily-stat-card { display: flex; flex-direction: column; gap: 8px; min-height: 88px; padding: 12px 10px; border-radius: 12px; border: 1px solid #e2e8f0; background: #fff; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06); box-sizing: border-box; min-width: 0; }', + '.h2-station-page .hr-daily-stat-card--today { border-color: #86efac; background: linear-gradient(180deg, #fff 0%, #f0fdf4 100%); box-shadow: 0 2px 8px rgba(16, 185, 129, 0.12); }', + '.h2-station-page .hr-daily-stat-card__date { font-size: 12px; font-weight: 700; color: #0f172a; font-variant-numeric: tabular-nums; padding-bottom: 8px; border-bottom: 1px solid #f1f5f9; display: flex; align-items: center; justify-content: space-between; gap: 4px; }', + '.h2-station-page .hr-daily-stat-card__today-tag { font-size: 10px; font-weight: 700; color: #059669; background: #ecfdf5; padding: 1px 6px; border-radius: 999px; flex-shrink: 0; }', + '.h2-station-page .hr-daily-stat-card__row { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 12px; }', + '.h2-station-page .hr-daily-stat-card__label { color: #64748b; font-weight: 600; white-space: nowrap; }', + '.h2-station-page .hr-daily-stat-card__value { font-weight: 800; font-variant-numeric: tabular-nums; }', + '.h2-station-page .hr-daily-stat-card__value--count { color: #2563eb; }', + '.h2-station-page .hr-daily-stat-card__value--kg { color: #059669; }', + '.h2-station-page--create .hr-batch-create-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }', + '.h2-station-page--create .hr-batch-create-toolbar__count { font-size: 13px; color: #64748b; font-variant-numeric: tabular-nums; }', + '.h2-station-page--create .hr-batch-create-table-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-station-page--create .hr-batch-create-table .ant-table-thead > tr > th { background: #f8fafc !important; font-size: 12px !important; font-weight: 700 !important; color: #475569 !important; padding: 10px 12px !important; }', + '.h2-station-page--create .hr-batch-create-table .ant-table-tbody > tr > td { padding: 8px 10px !important; vertical-align: middle !important; }', + '.h2-station-page--create .hr-batch-create-table .ant-input, .h2-station-page--create .hr-batch-create-table .ant-input-number, .h2-station-page--create .hr-batch-create-table .ant-select .ant-select-selector, .h2-station-page--create .hr-batch-create-table .ant-picker { border-radius: 8px !important; font-size: 13px !important; }', + '.h2-station-page--create .hr-batch-create-seq { display: inline-flex; align-items: center; justify-content: center; min-width: 24px; height: 24px; border-radius: 6px; background: #f1f5f9; color: #64748b; font-size: 12px; font-weight: 700; }' +]).join('\n'); + +var HR_DAILY_STATS_WINDOW = 28; +var HR_DAILY_STATS_PAGE_SIZE = 7; +var HR_DAILY_STATS_MAX_PAGE = Math.ceil(HR_DAILY_STATS_WINDOW / HR_DAILY_STATS_PAGE_SIZE) - 1; + +var HR_PRIMARY_BTN_STYLE = { + borderRadius: 8, + fontWeight: 600, + background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)', + border: 'none' +}; + +var HR_MOCK_FILLERS = ['张三', '李四', '王静', '赵敏', '陈浩']; + +var HR_SETTLEMENT_STATUS_OPTIONS = [ + { value: 'customer', label: '客户承担' }, + { value: 'internal', label: '我司承担' }, + { value: 'customer_self', label: '客户自行结算' } +]; + +var HR_SETTLEMENT_STATUS_MAP = { + customer: { label: '客户承担', color: 'blue' }, + internal: { label: '我司承担', color: 'green' }, + customer_self: { label: '客户自行结算', color: 'orange' } +}; + +function hrSettlementStatusLabel(value) { + var info = HR_SETTLEMENT_STATUS_MAP[value]; + return info ? info.label : '—'; +} + +var HR_INITIAL_RECORDS = [ + { id: 'hr-1', hydrogenTime: '2026-05-28 10:21:08', plateNo: '浙A12345F', hydrogenKg: 12.5, customerAmount: 562.5, mileageKm: 128560, fillerName: '张三', settlementStatus: 'customer' }, + { id: 'hr-2', hydrogenTime: '2026-05-26 14:08:33', plateNo: '浙A67890F', hydrogenKg: 10.0, customerAmount: 450.0, mileageKm: 95230, fillerName: '李四', settlementStatus: 'internal' }, + { id: 'hr-3', hydrogenTime: '2026-05-22 09:15:00', plateNo: '浙A88888F', hydrogenKg: 18.3, customerAmount: 823.5, mileageKm: 201880, fillerName: '王静', settlementStatus: 'customer_self' }, + { id: 'hr-4', hydrogenTime: '2026-05-18 16:42:11', plateNo: '浙A03561F', hydrogenKg: 15.6, customerAmount: 702.0, mileageKm: 167420, fillerName: '张三', settlementStatus: 'customer' }, + { id: 'hr-5', hydrogenTime: '2026-05-30 09:30:22', plateNo: '浙B23456F', hydrogenKg: 15.3, customerAmount: 703.8, mileageKm: 143200, fillerName: '赵敏', settlementStatus: 'internal' }, + { id: 'hr-6', hydrogenTime: '2026-05-27 18:10:05', plateNo: '浙B99999F', hydrogenKg: 18.2, customerAmount: 837.2, mileageKm: 189650, fillerName: '陈浩', settlementStatus: 'customer_self' }, + { id: 'hr-7', hydrogenTime: '2026-05-24 11:05:40', plateNo: '浙B58888F', hydrogenKg: 11.8, customerAmount: 542.8, mileageKm: 110340, fillerName: '李四', settlementStatus: 'customer' }, + { id: 'hr-8', hydrogenTime: '2026-04-20 16:45:18', plateNo: '沪A88888F', hydrogenKg: 8.0, customerAmount: 376.0, mileageKm: 88420, fillerName: '王静', settlementStatus: 'internal' }, + { id: 'hr-9', hydrogenTime: '2026-04-08 09:12:55', plateNo: '沪BDB9161F', hydrogenKg: 9.5, customerAmount: 446.5, mileageKm: 76500, fillerName: '张三', settlementStatus: 'customer_self' }, + { id: 'hr-10', hydrogenTime: '2026-03-15 10:00:00', plateNo: '苏E33333F', hydrogenKg: 6.2, customerAmount: 272.8, mileageKm: 45210, fillerName: '赵敏', settlementStatus: 'customer' } +]; + +var HR_APPOINTMENT_STATUS_MAP = { + pending: { label: '待处理', color: 'processing' }, + accepted: { label: '已接受', color: 'success' }, + rejected: { label: '已拒绝', color: 'error' } +}; + +var HR_INITIAL_APPOINTMENTS = [ + { id: 'ap-1', plateNo: '浙A12345F', appointmentTime: '2026-06-10 09:00:00', appointmentKg: 15.0, driverName: '刘建国', contact: '13800138011', status: 'pending' }, + { id: 'ap-2', plateNo: '浙B23456F', appointmentTime: '2026-06-10 11:30:00', appointmentKg: 12.5, driverName: '陈师傅', contact: '13900139022', status: 'pending' }, + { id: 'ap-3', plateNo: '浙A67890F', appointmentTime: '2026-06-09 14:00:00', appointmentKg: 10.0, driverName: '王磊', contact: '13700137033', status: 'pending' }, + { id: 'ap-4', plateNo: '沪A88888F', appointmentTime: '2026-06-08 16:20:00', appointmentKg: 8.5, driverName: '张强', contact: '13600136044', status: 'accepted', handledTime: '2026-06-08 10:15:00' }, + { id: 'ap-5', plateNo: '浙B99999F', appointmentTime: '2026-06-07 08:45:00', appointmentKg: 18.0, driverName: '赵明', contact: '13500135055', status: 'rejected', rejectReason: '预约时段站点设备检修,请改约其他时间', handledTime: '2026-06-06 17:30:00' } +]; + +function hrFormatKg(v) { + var n = typeof v === 'number' ? v : parseFloat(v); + return isNaN(n) ? '0.00' : n.toFixed(2); +} + +function hrFormatYuan(v) { + var n = typeof v === 'number' ? v : parseFloat(v); + return isNaN(n) ? '0.00' : n.toFixed(2); +} + +function hrFormatYuanSymbol(v) { + if (v == null || v === '') return '—'; + var n = typeof v === 'number' ? v : parseFloat(v); + if (isNaN(n)) return '—'; + return '¥' + n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); +} + +function hrFormatKm(v) { + if (v == null || v === '') return '—'; + var n = typeof v === 'number' ? v : parseFloat(String(v).replace(/,/g, '')); + if (isNaN(n)) return '—'; + return n.toLocaleString('zh-CN', { maximumFractionDigits: 0 }) + ' km'; +} + +function hrCsvEscape(v) { + var s = v == null ? '' : String(v); + if (/[",\n\r]/.test(s)) return '"' + s.replace(/"/g, '""') + '"'; + return s; +} + +function hrDownloadCsv(filename, headers, rows) { + var lines = [headers.map(hrCsvEscape).join(',')].concat( + (rows || []).map(function (r) { return r.map(hrCsvEscape).join(','); }) + ); + var blob = new Blob(['\uFEFF' + lines.join('\n')], { type: 'text/csv;charset=utf-8;' }); + var a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = filename; + a.click(); + URL.revokeObjectURL(a.href); +} + +function hrExtractDateKey(timeStr) { + if (!timeStr) return ''; + var s = String(timeStr).trim(); + return s.length >= 10 ? s.slice(0, 10) : s; +} + +function hrPad2(n) { + return n < 10 ? '0' + n : String(n); +} + +function hrDateToKey(d) { + return d.getFullYear() + '-' + hrPad2(d.getMonth() + 1) + '-' + hrPad2(d.getDate()); +} + +function hrBuildRecentDailyWindow(list, dayjs, windowDays) { + windowDays = windowDays || HR_DAILY_STATS_WINDOW; + var statsMap = {}; + (list || []).forEach(function (r) { + var day = hrExtractDateKey(r.hydrogenTime); + if (!day) return; + if (!statsMap[day]) statsMap[day] = { count: 0, totalKg: 0 }; + statsMap[day].count += 1; + var kg = parseFloat(r.hydrogenKg); + if (!isNaN(kg)) statsMap[day].totalKg += kg; + }); + + var days = []; + var i; + for (i = 0; i < windowDays; i++) { + var key; + var shortLabel; + if (dayjs) { + var d = dayjs().startOf('day').subtract(i, 'day'); + key = d.format('YYYY-MM-DD'); + shortLabel = d.format('MM-DD'); + } else { + var native = new Date(); + native.setHours(0, 0, 0, 0); + native.setDate(native.getDate() - i); + key = hrDateToKey(native); + shortLabel = hrPad2(native.getMonth() + 1) + '-' + hrPad2(native.getDate()); + } + var stat = statsMap[key]; + days.push({ + date: key, + dateLabel: shortLabel, + isToday: i === 0, + count: stat ? stat.count : 0, + totalKg: stat ? Math.round(stat.totalKg * 100) / 100 : 0 + }); + } + return days; +} + +function hrInTimeRange(timeStr, range, dayjs) { + if (!range || range.length < 2 || !range[0] || !range[1]) return true; + if (!timeStr) return false; + if (dayjs) { + var t = dayjs(timeStr); + var start = dayjs(range[0]); + var end = dayjs(range[1]); + if (!t.isValid() || !start.isValid() || !end.isValid()) return true; + return (t.isAfter(start) || t.isSame(start)) && (t.isBefore(end) || t.isSame(end)); + } + var tKey = String(timeStr).replace(/[-:\s]/g, ''); + var sKey = range[0].format ? range[0].format('YYYYMMDDHHmmss') : ''; + var eKey = range[1].format ? range[1].format('YYYYMMDDHHmmss') : ''; + if (!sKey || !eKey) return true; + return tKey >= sKey && tKey <= eKey; +} + +function hrEmptyForm() { + return { + hydrogenTime: '', + plateNo: '', + hydrogenKg: '', + customerAmount: '', + mileageKm: '', + fillerName: undefined, + settlementStatus: undefined + }; +} + +function hrNextId() { + return 'hr-' + Date.now() + '-' + Math.floor(Math.random() * 1000); +} + +function hrNextBatchRowId() { + return 'hr-row-' + Date.now() + '-' + Math.floor(Math.random() * 10000); +} + +function hrEmptyBatchRow() { + return { + _rowId: hrNextBatchRowId(), + hydrogenTime: '', + plateNo: '', + hydrogenKg: '', + customerAmount: '', + mileageKm: '', + fillerName: undefined, + settlementStatus: undefined + }; +} + +function hrIsBatchRowEmpty(row) { + if (!row) return true; + if ((row.hydrogenTime || '').trim()) return false; + if ((row.plateNo || '').trim()) return false; + if ((row.hydrogenKg || '').trim()) return false; + if ((row.customerAmount || '').trim()) return false; + if ((row.mileageKm || '').trim()) return false; + if (row.fillerName) return false; + return true; +} + +function hrBatchRowDirty(row) { + return !hrIsBatchRowEmpty(row); +} + +function hrBatchRowsDirty(rows) { + return (rows || []).some(hrBatchRowDirty); +} + +function hrValidateBatchRow(row, rowIndex) { + var label = '第 ' + rowIndex + ' 行'; + if (!(row.hydrogenTime || '').trim()) return label + ':请填写加氢时间'; + if (!(row.plateNo || '').trim()) return label + ':请填写车牌号'; + var kg = parseFloat(row.hydrogenKg); + if (isNaN(kg) || kg <= 0) return label + ':请填写有效的加氢量(kg)'; + var amount = parseFloat(row.customerAmount); + if (isNaN(amount) || amount < 0) return label + ':请填写有效的加氢金额'; + var km = parseFloat(row.mileageKm); + if (isNaN(km) || km < 0) return label + ':请填写有效的公里数'; + if (!row.fillerName) return label + ':请选择充装员'; + if (!row.settlementStatus) return label + ':请选择承担方式'; + return null; +} + +function hrBatchRowToPayload(row) { + return { + hydrogenTime: (row.hydrogenTime || '').trim(), + plateNo: (row.plateNo || '').trim().toUpperCase(), + hydrogenKg: Math.round(parseFloat(row.hydrogenKg) * 100) / 100, + customerAmount: Math.round(parseFloat(row.customerAmount) * 100) / 100, + mileageKm: Math.round(parseFloat(row.mileageKm)), + fillerName: row.fillerName, + settlementStatus: row.settlementStatus + }; +} + +function hrCardTitleWithStep(step, text) { + return React.createElement('span', { className: 'h2-card-title-bar h2-card-title-bar--step' }, + React.createElement('span', { className: 'h2-card-step-badge', 'aria-hidden': true }, step), + React.createElement('span', { className: 'h2-card-title-text' }, text) + ); +} + +function hrFormDirty(form) { + if ((form.hydrogenTime || '').trim()) return true; + if ((form.plateNo || '').trim()) return true; + if ((form.hydrogenKg || '').trim()) return true; + if ((form.customerAmount || '').trim()) return true; + if ((form.mileageKm || '').trim()) return true; + if (form.fillerName) return true; + if (form.settlementStatus) return true; + return false; +} + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Table = antd.Table; + var Button = antd.Button; + var Select = antd.Select; + var Input = antd.Input; + var InputNumber = antd.InputNumber; + var DatePicker = antd.DatePicker; + var Modal = antd.Modal; + var Form = antd.Form; + var Row = antd.Row; + var Col = antd.Col; + var Tag = antd.Tag; + var Badge = antd.Badge; + var message = antd.message; + var dayjs = window.dayjs; + + var RangePicker = DatePicker.RangePicker; + + var subViewState = useState('list'); + var subView = subViewState[0]; + var setSubView = subViewState[1]; + + var formModeState = useState('create'); + var formMode = formModeState[0]; + var setFormMode = formModeState[1]; + + var editingIdState = useState(null); + var editingId = editingIdState[0]; + var setEditingId = editingIdState[1]; + + var listDataState = useState(function () { + return HR_INITIAL_RECORDS.map(function (r) { return Object.assign({}, r); }); + }); + var listData = listDataState[0]; + var setListData = listDataState[1]; + + var draftFiltersState = useState({ timeRange: [], plateNo: undefined, fillerName: undefined }); + var draftFilters = draftFiltersState[0]; + var setDraftFilters = draftFiltersState[1]; + + var appliedFiltersState = useState({ timeRange: [], plateNo: undefined, fillerName: undefined }); + var appliedFilters = appliedFiltersState[0]; + var setAppliedFilters = appliedFiltersState[1]; + + var pageState = useState(1); + var page = pageState[0]; + var setPage = pageState[1]; + var pageSizeState = useState(10); + var pageSize = pageSizeState[0]; + var setPageSize = pageSizeState[1]; + + var formState = useState(hrEmptyForm()); + var form = formState[0]; + var setForm = formState[1]; + + var batchRowsState = useState([]); + var batchRows = batchRowsState[0]; + var setBatchRows = batchRowsState[1]; + + var formSubmittingState = useState(false); + var formSubmitting = formSubmittingState[0]; + var setFormSubmitting = formSubmittingState[1]; + + var deleteModalState = useState({ open: false, record: null }); + var deleteModal = deleteModalState[0]; + var setDeleteModal = deleteModalState[1]; + + var appointmentModalOpenState = useState(false); + var appointmentModalOpen = appointmentModalOpenState[0]; + var setAppointmentModalOpen = appointmentModalOpenState[1]; + + var appointmentDataState = useState(function () { + return HR_INITIAL_APPOINTMENTS.map(function (r) { return Object.assign({}, r); }); + }); + var appointmentData = appointmentDataState[0]; + var setAppointmentData = appointmentDataState[1]; + + var appointmentPageState = useState(1); + var appointmentPage = appointmentPageState[0]; + var setAppointmentPage = appointmentPageState[1]; + var appointmentPageSizeState = useState(10); + var appointmentPageSize = appointmentPageSizeState[0]; + var setAppointmentPageSize = appointmentPageSizeState[1]; + + var rejectModalState = useState({ open: false, record: null, reason: '' }); + var rejectModal = rejectModalState[0]; + var setRejectModal = rejectModalState[1]; + + var dailyStatsPageState = useState(0); + var dailyStatsPage = dailyStatsPageState[0]; + var setDailyStatsPage = dailyStatsPageState[1]; + + var plateOptions = useMemo(function () { + var map = {}; + listData.forEach(function (r) { + if (r.plateNo) map[r.plateNo] = true; + }); + return Object.keys(map).sort().map(function (p) { + return { value: p, label: p }; + }); + }, [listData]); + + var fillerOptions = useMemo(function () { + var map = {}; + HR_MOCK_FILLERS.forEach(function (n) { map[n] = true; }); + listData.forEach(function (r) { + if (r.fillerName) map[r.fillerName] = true; + }); + return Object.keys(map).sort().map(function (n) { + return { value: n, label: n }; + }); + }, [listData]); + + var pendingAppointmentCount = useMemo(function () { + var count = 0; + var i; + for (i = 0; i < appointmentData.length; i++) { + if (appointmentData[i].status === 'pending') count += 1; + } + return count; + }, [appointmentData]); + + var sortedAppointments = useMemo(function () { + return appointmentData.slice().sort(function (a, b) { + return String(b.appointmentTime || '').localeCompare(String(a.appointmentTime || '')); + }); + }, [appointmentData]); + + var appointmentSummary = useMemo(function () { + var pending = 0; + var accepted = 0; + var rejected = 0; + var totalKg = 0; + var i; + for (i = 0; i < appointmentData.length; i++) { + var item = appointmentData[i]; + var kg = parseFloat(item.appointmentKg); + if (!isNaN(kg)) totalKg += kg; + if (item.status === 'pending') pending += 1; + else if (item.status === 'accepted') accepted += 1; + else if (item.status === 'rejected') rejected += 1; + } + return { pending: pending, accepted: accepted, rejected: rejected, totalKg: totalKg, total: appointmentData.length }; + }, [appointmentData]); + + var filteredList = useMemo(function () { + var list = listData.slice(); + if (appliedFilters.plateNo) { + list = list.filter(function (r) { return r.plateNo === appliedFilters.plateNo; }); + } + if (appliedFilters.fillerName) { + list = list.filter(function (r) { return r.fillerName === appliedFilters.fillerName; }); + } + if (appliedFilters.timeRange && appliedFilters.timeRange.length === 2) { + list = list.filter(function (r) { + return hrInTimeRange(r.hydrogenTime, appliedFilters.timeRange, dayjs); + }); + } + return list.sort(function (a, b) { + return String(b.hydrogenTime || '').localeCompare(String(a.hydrogenTime || '')); + }); + }, [listData, appliedFilters, dayjs]); + + var dailyStatsWindow = useMemo(function () { + return hrBuildRecentDailyWindow(filteredList, dayjs, HR_DAILY_STATS_WINDOW); + }, [filteredList, dayjs]); + + var dailyStatsPageItems = useMemo(function () { + var start = dailyStatsPage * HR_DAILY_STATS_PAGE_SIZE; + var chunk = dailyStatsWindow.slice(start, start + HR_DAILY_STATS_PAGE_SIZE); + return chunk.slice().reverse(); + }, [dailyStatsWindow, dailyStatsPage]); + + var renderFilterField = useCallback(function (label, control) { + return React.createElement('div', { className: 'lc-filter-field' }, + React.createElement('span', { className: 'lc-filter-field-label' }, label), + React.createElement('div', { className: 'lc-filter-field-control' }, control) + ); + }, []); + + var handleFilterQuery = useCallback(function () { + setAppliedFilters({ + timeRange: (draftFilters.timeRange || []).slice(), + plateNo: draftFilters.plateNo, + fillerName: draftFilters.fillerName + }); + setPage(1); + setDailyStatsPage(0); + }, [draftFilters]); + + var handleFilterReset = useCallback(function () { + var empty = { timeRange: [], plateNo: undefined, fillerName: undefined }; + setDraftFilters(empty); + setAppliedFilters(empty); + setPage(1); + setDailyStatsPage(0); + }, []); + + var resetFormView = useCallback(function () { + setSubView('list'); + setForm(hrEmptyForm()); + setBatchRows([]); + setEditingId(null); + }, []); + + var handleFormPageBack = useCallback(function () { + if (formSubmitting) return; + var dirty = formMode === 'create' ? hrBatchRowsDirty(batchRows) : hrFormDirty(form); + if (dirty) { + Modal.confirm({ + title: '确认离开', + content: '当前有未保存内容,确定返回列表吗?', + okText: '离开', + cancelText: '继续编辑', + centered: true, + onOk: resetFormView + }); + return; + } + resetFormView(); + }, [form, batchRows, formMode, formSubmitting, resetFormView]); + + var openCreate = useCallback(function () { + setForm(hrEmptyForm()); + setBatchRows([hrEmptyBatchRow()]); + setFormMode('create'); + setEditingId(null); + setSubView('form'); + }, []); + + var updateBatchRow = useCallback(function (rowId, patch) { + setBatchRows(function (prev) { + return prev.map(function (r) { + if (r._rowId !== rowId) return r; + return Object.assign({}, r, patch); + }); + }); + }, []); + + var addBatchRow = useCallback(function () { + setBatchRows(function (prev) { return prev.concat([hrEmptyBatchRow()]); }); + }, []); + + var removeBatchRow = useCallback(function (rowId) { + setBatchRows(function (prev) { + if (prev.length <= 1) return [hrEmptyBatchRow()]; + return prev.filter(function (r) { return r._rowId !== rowId; }); + }); + }, []); + + var openEdit = useCallback(function (record) { + setForm({ + hydrogenTime: record.hydrogenTime || '', + plateNo: record.plateNo || '', + hydrogenKg: record.hydrogenKg != null ? String(record.hydrogenKg) : '', + customerAmount: record.customerAmount != null ? String(record.customerAmount) : '', + mileageKm: record.mileageKm != null ? String(record.mileageKm) : '', + fillerName: record.fillerName, + settlementStatus: record.settlementStatus + }); + setFormMode('edit'); + setEditingId(record.id); + setSubView('form'); + }, []); + + var validateForm = useCallback(function () { + if (!(form.hydrogenTime || '').trim()) { + message.warning('请填写加氢时间'); + return false; + } + if (!(form.plateNo || '').trim()) { + message.warning('请填写车牌号'); + return false; + } + var kg = parseFloat(form.hydrogenKg); + if (isNaN(kg) || kg <= 0) { + message.warning('请填写有效的加氢量(kg)'); + return false; + } + var amount = parseFloat(form.customerAmount); + if (isNaN(amount) || amount < 0) { + message.warning('请填写有效的加氢金额'); + return false; + } + var km = parseFloat(form.mileageKm); + if (isNaN(km) || km < 0) { + message.warning('请填写有效的公里数'); + return false; + } + if (!form.fillerName) { + message.warning('请选择充装员'); + return false; + } + if (!form.settlementStatus) { + message.warning('请选择承担方式'); + return false; + } + return true; + }, [form]); + + var handleFormSubmit = useCallback(function () { + if (formSubmitting) return; + if (!validateForm()) return; + setFormSubmitting(true); + var payload = { + hydrogenTime: (form.hydrogenTime || '').trim(), + plateNo: (form.plateNo || '').trim().toUpperCase(), + hydrogenKg: Math.round(parseFloat(form.hydrogenKg) * 100) / 100, + customerAmount: Math.round(parseFloat(form.customerAmount) * 100) / 100, + mileageKm: Math.round(parseFloat(form.mileageKm)), + fillerName: form.fillerName, + settlementStatus: form.settlementStatus + }; + if (formMode === 'edit' && editingId) { + setListData(function (prev) { + return prev.map(function (r) { + if (r.id !== editingId) return r; + return Object.assign({}, r, payload); + }); + }); + message.success('加氢记录已更新'); + } + window.setTimeout(function () { + setFormSubmitting(false); + resetFormView(); + }, 280); + }, [form, formMode, editingId, formSubmitting, validateForm, resetFormView]); + + var handleBatchSubmit = useCallback(function () { + if (formSubmitting) return; + var filledRows = batchRows.filter(function (r) { return !hrIsBatchRowEmpty(r); }); + if (!filledRows.length) { + message.warning('请至少填写一行加氢记录'); + return; + } + var i; + for (i = 0; i < filledRows.length; i++) { + var rowIndex = 0; + var j; + for (j = 0; j < batchRows.length; j++) { + if (batchRows[j]._rowId === filledRows[i]._rowId) { + rowIndex = j + 1; + break; + } + } + var err = hrValidateBatchRow(filledRows[i], rowIndex); + if (err) { + message.warning(err); + return; + } + } + setFormSubmitting(true); + var newRecords = filledRows.map(function (row) { + return Object.assign({ id: hrNextId() }, hrBatchRowToPayload(row)); + }); + setListData(function (prev) { return newRecords.concat(prev); }); + message.success('已成功新增 ' + newRecords.length + ' 条加氢记录'); + window.setTimeout(function () { + setFormSubmitting(false); + resetFormView(); + }, 280); + }, [batchRows, formSubmitting, resetFormView]); + + var handleAcceptAppointment = useCallback(function (record) { + if (!record || record.status !== 'pending') return; + Modal.confirm({ + title: '接受预约', + content: '确认接受车牌「' + (record.plateNo || '') + '」的加氢预约?预约时间:' + (record.appointmentTime || '—'), + okText: '接受预约', + cancelText: '取消', + centered: true, + okButtonProps: { style: HR_PRIMARY_BTN_STYLE }, + onOk: function () { + var now = dayjs ? dayjs().format('YYYY-MM-DD HH:mm:ss') : ''; + setAppointmentData(function (prev) { + return prev.map(function (r) { + if (r.id !== record.id) return r; + return Object.assign({}, r, { status: 'accepted', handledTime: now, rejectReason: '' }); + }); + }); + message.success('已接受预约'); + } + }); + }, [dayjs]); + + var openRejectAppointment = useCallback(function (record) { + if (!record || record.status !== 'pending') return; + setRejectModal({ open: true, record: record, reason: '' }); + }, []); + + var closeRejectModal = useCallback(function () { + setRejectModal({ open: false, record: null, reason: '' }); + }, []); + + var handleConfirmRejectAppointment = useCallback(function () { + if (!rejectModal.record) return; + var reason = (rejectModal.reason || '').trim(); + if (!reason) { + message.warning('请填写拒绝原因'); + return; + } + var recordId = rejectModal.record.id; + var now = dayjs ? dayjs().format('YYYY-MM-DD HH:mm:ss') : ''; + setAppointmentData(function (prev) { + return prev.map(function (r) { + if (r.id !== recordId) return r; + return Object.assign({}, r, { status: 'rejected', rejectReason: reason, handledTime: now }); + }); + }); + message.success('已拒绝预约'); + closeRejectModal(); + }, [rejectModal, dayjs, closeRejectModal]); + + var handleExport = useCallback(function () { + if (!filteredList.length) { + message.warning('当前筛选条件下暂无数据可导出'); + return; + } + var headers = ['序号', '加氢时间', '车牌号', '加氢量(kg)', '加氢金额(元)', '公里数(km)', '承担方式', '充装员']; + var rows = filteredList.map(function (r, index) { + return [ + index + 1, + r.hydrogenTime || '', + r.plateNo || '', + hrFormatKg(r.hydrogenKg), + hrFormatYuan(r.customerAmount), + r.mileageKm != null ? r.mileageKm : '', + hrSettlementStatusLabel(r.settlementStatus), + r.fillerName || '' + ]; + }); + hrDownloadCsv('加氢记录_' + new Date().getTime() + '.csv', headers, rows); + message.success('已导出 ' + filteredList.length + ' 条加氢记录'); + }, [filteredList]); + + var columns = useMemo(function () { + return [ + { + title: '加氢时间', + dataIndex: 'hydrogenTime', + key: 'hydrogenTime', + width: 168, + render: function (v) { + return React.createElement('span', { className: 'hr-refuel-time' }, v || '—'); + } + }, + { + title: '车牌号', + dataIndex: 'plateNo', + key: 'plateNo', + width: 120, + render: function (v) { + return React.createElement('span', { className: 'hr-refuel-plate' }, v || '—'); + } + }, + { + title: '加氢量(kg)', + dataIndex: 'hydrogenKg', + key: 'hydrogenKg', + width: 120, + align: 'right', + render: function (v) { + return React.createElement('span', { className: 'hr-refuel-kg' }, hrFormatKg(v)); + } + }, + { + title: '加氢金额', + dataIndex: 'customerAmount', + key: 'customerAmount', + width: 120, + align: 'right', + render: function (v) { + return React.createElement('span', { className: 'hr-refuel-money' }, hrFormatYuanSymbol(v)); + } + }, + { + title: '公里数', + dataIndex: 'mileageKm', + key: 'mileageKm', + width: 120, + align: 'right', + render: function (v) { + return React.createElement('span', { className: 'hr-refuel-km' }, hrFormatKm(v)); + } + }, + { + title: '承担方式', + dataIndex: 'settlementStatus', + key: 'settlementStatus', + width: 120, + render: function (v) { + var info = HR_SETTLEMENT_STATUS_MAP[v]; + if (!info) return '—'; + return React.createElement(Tag, { color: info.color, style: { margin: 0, borderRadius: 6, fontWeight: 600 } }, info.label); + } + }, + { + title: '充装员', + dataIndex: 'fillerName', + key: 'fillerName', + width: 100, + ellipsis: true + }, + { + title: '操作', + key: 'actions', + width: 140, + fixed: 'right', + render: function (_, record) { + return React.createElement('div', { className: 'h2-row-actions' }, + React.createElement(Button, { + type: 'link', + size: 'small', + className: 'lc-action-btn', + onClick: function () { openEdit(record); } + }, '编辑'), + React.createElement(Button, { + type: 'link', + size: 'small', + className: 'lc-action-btn lc-action-btn-danger', + onClick: function () { setDeleteModal({ open: true, record: record }); } + }, '删除') + ); + } + } + ]; + }, [openEdit]); + + var appointmentColumns = useMemo(function () { + return [ + { + title: '序号', + key: 'seq', + width: 52, + align: 'center', + render: function (_, __, index) { + return React.createElement('span', { className: 'h2-refuel-drill-seq' }, index + 1); + } + }, + { + title: '车牌号', + dataIndex: 'plateNo', + key: 'plateNo', + width: 120, + render: function (v) { + return React.createElement('span', { className: 'h2-refuel-drill-plate' }, v || '—'); + } + }, + { + title: '预约时间', + dataIndex: 'appointmentTime', + key: 'appointmentTime', + width: 168, + render: function (v) { + return React.createElement('span', { className: 'h2-refuel-drill-time' }, v || '—'); + } + }, + { + title: '预约加氢量(kg)', + dataIndex: 'appointmentKg', + key: 'appointmentKg', + width: 140, + align: 'right', + render: function (v) { + return React.createElement('span', { className: 'h2-refuel-drill-kg' }, hrFormatKg(v)); + } + }, + { + title: '预约司机', + dataIndex: 'driverName', + key: 'driverName', + width: 110, + render: function (v) { + return React.createElement('span', { className: 'h2-refuel-drill-driver' }, v || '—'); + } + }, + { + title: '联系方式', + dataIndex: 'contact', + key: 'contact', + width: 130, + render: function (v) { + return React.createElement('span', { className: 'h2-refuel-drill-contact' }, v || '—'); + } + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + width: 100, + render: function (v) { + var info = HR_APPOINTMENT_STATUS_MAP[v] || { label: v || '—', color: 'default' }; + return React.createElement(Tag, { color: info.color, style: { margin: 0, borderRadius: 6, fontWeight: 600 } }, info.label); + } + }, + { + title: '操作', + key: 'actions', + width: 200, + fixed: 'right', + render: function (_, record) { + if (record.status !== 'pending') { + return record.status === 'rejected' && record.rejectReason + ? React.createElement('div', null, + React.createElement('span', { style: { color: '#94a3b8', fontSize: 12 } }, '已处理'), + React.createElement('div', { className: 'hr-appointment-reject-reason', title: record.rejectReason }, '拒绝原因:' + record.rejectReason) + ) + : React.createElement('span', { style: { color: '#94a3b8', fontSize: 12 } }, '已处理'); + } + return React.createElement('div', { className: 'h2-row-actions' }, + React.createElement(Button, { + type: 'link', + size: 'small', + className: 'lc-action-btn', + onClick: function () { handleAcceptAppointment(record); } + }, '接受预约'), + React.createElement(Button, { + type: 'link', + size: 'small', + className: 'lc-action-btn lc-action-btn-danger', + onClick: function () { openRejectAppointment(record); } + }, '拒绝预约') + ); + } + } + ]; + }, [handleAcceptAppointment, openRejectAppointment]); + + var formTimeValue = null; + if (dayjs && form.hydrogenTime) { + var ft = dayjs(form.hydrogenTime); + if (ft.isValid()) formTimeValue = ft; + } + + var inputCls = 'h2-create-input'; + var H2_CREATE_GUTTER = [24, 20]; + + var formItem = function (label, required, node) { + return React.createElement(Form.Item, { + label: required + ? React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), label) + : label + }, node); + }; + + var formRow = function () { + return React.createElement.apply(null, [Row, { gutter: H2_CREATE_GUTTER, align: 'stretch' }].concat(Array.prototype.slice.call(arguments))); + }; + + var col24 = function (node) { + return React.createElement(Col, { xs: 24, span: 24 }, node); + }; + + var col12 = function (node) { + return React.createElement(Col, { xs: 24, md: 12 }, node); + }; + + var col8 = function (node) { + return React.createElement(Col, { xs: 24, lg: 8 }, node); + }; + + var gridBlock = function () { + var rows = Array.prototype.slice.call(arguments, 0); + return React.createElement('div', { className: 'h2-create-form-grid' }, rows); + }; + + var formPageTitle = formMode === 'edit' ? '编辑加氢记录' : '批量新增加氢记录'; + + var batchFilledCount = useMemo(function () { + var n = 0; + var i; + for (i = 0; i < batchRows.length; i++) { + if (!hrIsBatchRowEmpty(batchRows[i])) n += 1; + } + return n; + }, [batchRows]); + + var batchCreateColumns = useMemo(function () { + return [ + { + title: '序号', + width: 56, + align: 'center', + render: function (_, __, index) { + return React.createElement('span', { className: 'hr-batch-create-seq' }, index + 1); + } + }, + { + title: React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), '加氢时间'), + width: 200, + render: function (_, record) { + var timeVal = null; + if (dayjs && record.hydrogenTime) { + var t = dayjs(record.hydrogenTime); + if (t.isValid()) timeVal = t; + } + return dayjs && DatePicker + ? React.createElement(DatePicker, { + showTime: true, + style: { width: '100%' }, + format: 'YYYY-MM-DD HH:mm:ss', + placeholder: '加氢时间', + value: timeVal, + onChange: function (v) { + updateBatchRow(record._rowId, { + hydrogenTime: v && v.isValid && v.isValid() ? v.format('YYYY-MM-DD HH:mm:ss') : '' + }); + } + }) + : React.createElement(Input, { + placeholder: 'YYYY-MM-DD HH:mm:ss', + value: record.hydrogenTime || '', + onChange: function (e) { updateBatchRow(record._rowId, { hydrogenTime: e.target.value }); } + }); + } + }, + { + title: React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), '车牌号'), + width: 130, + render: function (_, record) { + return React.createElement(Input, { + placeholder: '车牌号', + value: record.plateNo || '', + maxLength: 16, + onChange: function (e) { updateBatchRow(record._rowId, { plateNo: e.target.value }); } + }); + } + }, + { + title: React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), '加氢量(kg)'), + width: 110, + render: function (_, record) { + return React.createElement(InputNumber, { + style: { width: '100%' }, + min: 0.01, + step: 0.1, + precision: 2, + placeholder: 'kg', + value: record.hydrogenKg === '' ? null : parseFloat(record.hydrogenKg), + onChange: function (v) { updateBatchRow(record._rowId, { hydrogenKg: v == null ? '' : String(v) }); } + }); + } + }, + { + title: React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), '加氢金额(元)'), + width: 120, + render: function (_, record) { + return React.createElement(InputNumber, { + style: { width: '100%' }, + min: 0, + step: 0.01, + precision: 2, + placeholder: '金额', + value: record.customerAmount === '' ? null : parseFloat(record.customerAmount), + onChange: function (v) { updateBatchRow(record._rowId, { customerAmount: v == null ? '' : String(v) }); } + }); + } + }, + { + title: React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), '公里数(km)'), + width: 110, + render: function (_, record) { + return React.createElement(InputNumber, { + style: { width: '100%' }, + min: 0, + step: 1, + precision: 0, + placeholder: 'km', + value: record.mileageKm === '' ? null : parseFloat(record.mileageKm), + onChange: function (v) { updateBatchRow(record._rowId, { mileageKm: v == null ? '' : String(v) }); } + }); + } + }, + { + title: React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), '承担方式'), + width: 130, + render: function (_, record) { + return React.createElement(Select, { + placeholder: '承担方式', + value: record.settlementStatus, + options: HR_SETTLEMENT_STATUS_OPTIONS, + style: { width: '100%' }, + onChange: function (v) { updateBatchRow(record._rowId, { settlementStatus: v }); }, + dropdownStyle: { borderRadius: 8 } + }); + } + }, + { + title: React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), '充装员'), + width: 120, + render: function (_, record) { + return React.createElement(Select, { + showSearch: true, + placeholder: '充装员', + optionFilterProp: 'label', + value: record.fillerName, + options: fillerOptions, + style: { width: '100%' }, + onChange: function (v) { updateBatchRow(record._rowId, { fillerName: v }); }, + dropdownStyle: { borderRadius: 8 } + }); + } + }, + { + title: '操作', + width: 72, + fixed: 'right', + render: function (_, record) { + return React.createElement(Button, { + type: 'link', + size: 'small', + className: 'lc-action-btn-danger', + disabled: batchRows.length <= 1, + onClick: function () { removeBatchRow(record._rowId); } + }, '删除'); + } + } + ]; + }, [batchRows.length, dayjs, DatePicker, Input, InputNumber, Select, fillerOptions, updateBatchRow, removeBatchRow]); + + var emptyNode = React.createElement('div', { style: { padding: '40px 0', textAlign: 'center' } }, + HR_ICONS.empty, + React.createElement('div', { style: { color: '#94a3b8', marginTop: 12 } }, '暂无符合检索条件的加氢记录') + ); + + var renderAppointmentPanel = function () { + var records = sortedAppointments; + var summary = appointmentSummary; + var stats = [ + { key: 'pending', label: '待处理预约', value: String(summary.pending || 0), mod: 'count', valueMod: 'count' }, + { key: 'accepted', label: '已接受', value: String(summary.accepted || 0), mod: 'kg', valueMod: 'kg' }, + { key: 'rejected', label: '已拒绝', value: String(summary.rejected || 0), mod: 'cost', valueMod: 'cost' }, + { key: 'totalKg', label: '预约加氢量合计(kg)', value: hrFormatKg(summary.totalKg), mod: 'customer', valueMod: 'customer' } + ]; + var tablePagination = records.length > 8 + ? { + current: appointmentPage, + pageSize: appointmentPageSize, + total: records.length, + showSizeChanger: true, + pageSizeOptions: ['5', '10', '20', '50'], + size: 'small', + showTotal: function (t) { return '共 ' + t + ' 条'; }, + onChange: function (p, ps) { setAppointmentPage(p); setAppointmentPageSize(ps); } + } + : false; + return React.createElement('div', { className: 'h2-refuel-drill-panel' }, + React.createElement('div', { className: 'h2-refuel-drill-station-card' }, + React.createElement('div', { className: 'h2-refuel-drill-station-card__name' }, '预约加氢管理'), + React.createElement('div', { className: 'h2-refuel-drill-station-card__meta' }, + '司机预约记录 · 待处理 ' + (summary.pending || 0) + ' 条 · 共 ' + records.length + ' 条' + ) + ), + React.createElement('div', { className: 'h2-refuel-drill-stats', role: 'group', 'aria-label': '预约统计' }, + stats.map(function (item) { + return React.createElement('div', { + key: item.key, + className: 'h2-refuel-drill-stat h2-refuel-drill-stat--' + item.mod, + role: 'article', + 'aria-label': item.label + ' ' + item.value + }, + React.createElement('div', { className: 'h2-refuel-drill-stat__label' }, item.label), + React.createElement('div', { + className: 'h2-refuel-drill-stat__value h2-refuel-drill-stat__value--' + item.valueMod + }, item.value) + ); + }) + ), + React.createElement('div', { className: 'h2-refuel-drill-table-wrap' }, + React.createElement('div', { className: 'h2-refuel-drill-table-head' }, + React.createElement('div', { className: 'h2-refuel-drill-table-head__left' }, + React.createElement('span', { className: 'h2-refuel-drill-table-head__title' }, '预约明细'), + React.createElement('span', { className: 'h2-refuel-drill-table-head__count' }, '共 ' + records.length + ' 条') + ), + summary.pending + ? React.createElement(Tag, { color: 'processing', style: { margin: 0, borderRadius: 6, fontWeight: 600 } }, summary.pending + ' 条待处理') + : React.createElement(Tag, { style: { margin: 0, borderRadius: 6, color: '#64748b' } }, '暂无待处理') + ), + React.createElement(Table, { + className: 'h2-refuel-record-table', + size: 'small', + bordered: false, + rowKey: 'id', + columns: appointmentColumns, + dataSource: records, + pagination: tablePagination, + locale: { emptyText: '暂无预约记录' }, + scroll: records.length > 8 ? { x: 'max-content', y: 320 } : { x: 'max-content' } + }) + ) + ); + }; + + var batchCreateViewNode = subView === 'form' && formMode === 'create' ? React.createElement('div', { className: 'h2-create-shell' }, + React.createElement('div', { className: 'h2-create-topbar' }, + React.createElement(Button, { + className: 'h2-create-back-btn', + type: 'default', + icon: HR_ICONS.back, + onClick: handleFormPageBack, + disabled: formSubmitting, + style: { borderRadius: 8 } + }, '返回'), + React.createElement('h1', { className: 'h2-create-topbar-title' }, formPageTitle) + ), + React.createElement(Card, { + className: 'h2-create-card', + id: 'hr-batch-create-card', + title: React.createElement('span', { className: 'h2-card-title-bar' }, + React.createElement('span', { className: 'h2-card-title-icon' }, HR_ICONS.fuel), + React.createElement('span', { className: 'h2-card-title-text' }, '加氢记录明细') + ), + bordered: false + }, + React.createElement('div', { className: 'hr-batch-create-toolbar' }, + React.createElement('span', { className: 'hr-batch-create-toolbar__count' }, + '共 ' + batchRows.length + ' 行 · 已填写 ' + batchFilledCount + ' 条' + ), + React.createElement(Button, { + type: 'dashed', + style: { borderRadius: 8, fontWeight: 600, borderColor: '#10b981', color: '#059669' }, + onClick: addBatchRow, + 'aria-label': '添加一行加氢记录' + }, '+ 添加一行') + ), + React.createElement('div', { className: 'hr-batch-create-table-wrap' }, + React.createElement(Table, { + className: 'hr-batch-create-table', + size: 'small', + bordered: false, + rowKey: '_rowId', + columns: batchCreateColumns, + dataSource: batchRows, + pagination: false, + locale: { emptyText: '请点击「添加一行」开始录入' }, + scroll: { x: 1240 } + }) + ) + ), + React.createElement('footer', { className: 'h2-create-footer' }, + React.createElement('div', { className: 'h2-create-footer-inner' }, + React.createElement('div', { className: 'h2-create-footer-hint' }, + '将提交 ' + batchFilledCount + ' 条有效记录' + ), + React.createElement('div', { className: 'h2-create-footer-actions' }, + React.createElement(Button, { + onClick: handleFormPageBack, + disabled: formSubmitting, + style: { borderRadius: 8 }, + 'aria-label': '取消并返回列表' + }, '取消'), + React.createElement(Button, { + type: 'primary', + style: HR_PRIMARY_BTN_STYLE, + onClick: handleBatchSubmit, + loading: formSubmitting, + disabled: batchFilledCount === 0, + 'aria-label': '批量提交加氢记录' + }, '批量提交') + ) + ) + ) + ) : null; + + var editFormViewNode = subView === 'form' && formMode === 'edit' ? React.createElement('div', { className: 'h2-create-shell' }, + React.createElement('div', { className: 'h2-create-topbar' }, + React.createElement(Button, { + className: 'h2-create-back-btn', + type: 'default', + icon: HR_ICONS.back, + onClick: handleFormPageBack, + disabled: formSubmitting, + style: { borderRadius: 8 } + }, '返回'), + React.createElement('h1', { className: 'h2-create-topbar-title' }, formPageTitle) + ), + React.createElement(Form, { + layout: 'vertical', + requiredMark: false, + className: 'h2-create-form h2-create-form--list h2-create-form--grid' + }, + React.createElement(Card, { + className: 'h2-create-card', + id: 'hr-create-basic-card', + title: hrCardTitleWithStep(1, '基本信息'), + bordered: false + }, + gridBlock( + formRow( + col12(formItem('加氢时间', true, dayjs && DatePicker + ? React.createElement(DatePicker, { + className: inputCls, + showTime: true, + style: { width: '100%' }, + format: 'YYYY-MM-DD HH:mm:ss', + placeholder: '请选择加氢时间', + value: formTimeValue, + onChange: function (v) { + setForm(function (f) { + return Object.assign({}, f, { + hydrogenTime: v && v.isValid && v.isValid() ? v.format('YYYY-MM-DD HH:mm:ss') : '' + }); + }); + } + }) + : React.createElement(Input, { + className: inputCls, + placeholder: 'YYYY-MM-DD HH:mm:ss', + value: form.hydrogenTime, + onChange: function (e) { setForm(function (f) { return Object.assign({}, f, { hydrogenTime: e.target.value }); }); } + }) + )), + col12(formItem('车牌号', true, React.createElement(Input, { + className: inputCls, + placeholder: '请输入车牌号', + value: form.plateNo || '', + maxLength: 16, + onChange: function (e) { setForm(function (f) { return Object.assign({}, f, { plateNo: e.target.value }); }); } + }))) + ) + ) + ), + React.createElement(Card, { + className: 'h2-create-card', + id: 'hr-create-data-card', + title: hrCardTitleWithStep(2, '加氢数据'), + bordered: false + }, + gridBlock( + formRow( + col8(formItem('加氢量(kg)', true, React.createElement(InputNumber, { + className: inputCls, + style: { width: '100%' }, + min: 0.01, + step: 0.1, + precision: 2, + placeholder: '请输入加氢量', + value: form.hydrogenKg === '' ? null : parseFloat(form.hydrogenKg), + onChange: function (v) { setForm(function (f) { return Object.assign({}, f, { hydrogenKg: v == null ? '' : String(v) }); }); } + }))), + col8(formItem('加氢金额(元)', true, React.createElement(InputNumber, { + className: inputCls, + style: { width: '100%' }, + min: 0, + step: 0.01, + precision: 2, + placeholder: '请输入加氢金额', + value: form.customerAmount === '' ? null : parseFloat(form.customerAmount), + onChange: function (v) { setForm(function (f) { return Object.assign({}, f, { customerAmount: v == null ? '' : String(v) }); }); } + }))), + col8(formItem('公里数(km)', true, React.createElement(InputNumber, { + className: inputCls, + style: { width: '100%' }, + min: 0, + step: 1, + precision: 0, + placeholder: '请输入公里数', + value: form.mileageKm === '' ? null : parseFloat(form.mileageKm), + onChange: function (v) { setForm(function (f) { return Object.assign({}, f, { mileageKm: v == null ? '' : String(v) }); }); } + }))) + ) + ) + ), + React.createElement(Card, { + className: 'h2-create-card', + id: 'hr-create-operator-card', + title: hrCardTitleWithStep(3, '操作人员'), + bordered: false + }, + gridBlock( + formRow( + col12(formItem('充装员', true, React.createElement(Select, { + className: inputCls, + showSearch: true, + placeholder: '请选择充装员', + optionFilterProp: 'label', + value: form.fillerName, + options: fillerOptions, + onChange: function (v) { setForm(function (f) { return Object.assign({}, f, { fillerName: v }); }); }, + dropdownStyle: { borderRadius: 8 } + }))), + col12(formItem('承担方式', true, React.createElement(Select, { + className: inputCls, + placeholder: '请选择承担方式', + value: form.settlementStatus, + options: HR_SETTLEMENT_STATUS_OPTIONS, + onChange: function (v) { setForm(function (f) { return Object.assign({}, f, { settlementStatus: v }); }); }, + dropdownStyle: { borderRadius: 8 } + }))) + ) + ) + ) + ), + React.createElement('footer', { className: 'h2-create-footer' }, + React.createElement('div', { className: 'h2-create-footer-inner' }, + React.createElement('div', { className: 'h2-create-footer-actions' }, + React.createElement(Button, { + onClick: handleFormPageBack, + disabled: formSubmitting, + style: { borderRadius: 8 }, + 'aria-label': '取消并返回列表' + }, '取消'), + React.createElement(Button, { + type: 'primary', + style: HR_PRIMARY_BTN_STYLE, + onClick: handleFormSubmit, + loading: formSubmitting, + 'aria-label': '保存编辑加氢记录' + }, '保存修改') + ) + ) + ) + ) : null; + + return React.createElement('div', { + className: 'h2-station-page lc-edit-page' + (subView === 'form' ? ' h2-station-page--create' : '') + }, + React.createElement('style', null, HR_PAGE_STYLE), + batchCreateViewNode, + editFormViewNode, + subView === 'list' ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' } }, + React.createElement('div', { style: { marginBottom: 16 } }, + React.createElement(Breadcrumb, { items: [{ title: '加氢站管理' }, { title: '加氢记录' }] }) + ), + + React.createElement(Card, { className: 'lc-filter-card', title: '筛选条件', bordered: false }, + React.createElement('div', { className: 'lc-filter-grid' }, + renderFilterField('加氢时间', React.createElement(RangePicker, { + showTime: true, + style: { width: '100%' }, + placeholder: ['开始时间', '结束时间'], + value: draftFilters.timeRange && draftFilters.timeRange.length ? draftFilters.timeRange : null, + onChange: function (v) { + setDraftFilters(function (p) { return Object.assign({}, p, { timeRange: v || [] }); }); + } + })), + renderFilterField('车牌号', React.createElement(Select, { + placeholder: '请选择车牌号', + allowClear: true, + showSearch: true, + optionFilterProp: 'label', + value: draftFilters.plateNo, + options: plateOptions, + onChange: function (v) { setDraftFilters(function (p) { return Object.assign({}, p, { plateNo: v }); }); }, + style: { width: '100%' }, + dropdownStyle: { borderRadius: 8 } + })), + renderFilterField('充装员', React.createElement(Select, { + placeholder: '请选择充装员', + allowClear: true, + showSearch: true, + optionFilterProp: 'label', + value: draftFilters.fillerName, + options: fillerOptions, + onChange: function (v) { setDraftFilters(function (p) { return Object.assign({}, p, { fillerName: v }); }); }, + style: { width: '100%' }, + dropdownStyle: { borderRadius: 8 } + })) + ), + React.createElement('div', { className: 'lc-filter-actions' }, + React.createElement(Button, { onClick: handleFilterReset, style: { borderRadius: 8 } }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleFilterQuery, style: HR_PRIMARY_BTN_STYLE }, '查询') + ) + ), + + React.createElement('div', { className: 'hr-daily-stats-section' }, + React.createElement('div', { className: 'hr-daily-stats-head' }, + React.createElement('span', { className: 'hr-daily-stats-head__title' }, '每日统计'), + React.createElement('span', { className: 'hr-daily-stats-head__meta' }, + (function () { + var rangeStart = dailyStatsPageItems[0] ? dailyStatsPageItems[0].date : '—'; + var rangeEnd = dailyStatsPageItems.length ? dailyStatsPageItems[dailyStatsPageItems.length - 1].date : '—'; + return '近28天(含今天)· 第 ' + (dailyStatsPage + 1) + '/' + (HR_DAILY_STATS_MAX_PAGE + 1) + ' 组 · ' + + rangeStart + ' 至 ' + rangeEnd + ' · 筛选记录 ' + filteredList.length + ' 笔'; + })() + ) + ), + React.createElement('div', { className: 'hr-daily-stats-carousel' }, + React.createElement(Button, { + type: 'default', + className: 'hr-daily-stats-nav-btn', + disabled: dailyStatsPage >= HR_DAILY_STATS_MAX_PAGE, + onClick: function () { setDailyStatsPage(function (p) { return Math.min(HR_DAILY_STATS_MAX_PAGE, p + 1); }); }, + 'aria-label': '向左翻页,查看更早的7天' + }, '‹'), + React.createElement('div', { className: 'hr-daily-stats-track', role: 'group', 'aria-label': '每日加氢统计' }, + dailyStatsPageItems.map(function (item) { + return React.createElement('div', { + key: item.date, + className: 'hr-daily-stat-card' + (item.isToday ? ' hr-daily-stat-card--today' : ''), + role: 'article', + 'aria-label': item.date + ' 加氢 ' + item.count + ' 次,加氢量 ' + hrFormatKg(item.totalKg) + ' kg' + }, + React.createElement('div', { className: 'hr-daily-stat-card__date' }, + React.createElement('span', null, item.dateLabel), + item.isToday ? React.createElement('span', { className: 'hr-daily-stat-card__today-tag' }, '今天') : null + ), + React.createElement('div', { className: 'hr-daily-stat-card__row' }, + React.createElement('span', { className: 'hr-daily-stat-card__label' }, '加氢次数'), + React.createElement('span', { className: 'hr-daily-stat-card__value hr-daily-stat-card__value--count' }, String(item.count)) + ), + React.createElement('div', { className: 'hr-daily-stat-card__row' }, + React.createElement('span', { className: 'hr-daily-stat-card__label' }, '加氢量(kg)'), + React.createElement('span', { className: 'hr-daily-stat-card__value hr-daily-stat-card__value--kg' }, hrFormatKg(item.totalKg)) + ) + ); + }) + ), + React.createElement(Button, { + type: 'default', + className: 'hr-daily-stats-nav-btn', + disabled: dailyStatsPage <= 0, + onClick: function () { setDailyStatsPage(function (p) { return Math.max(0, p - 1); }); }, + 'aria-label': '向右翻页,查看更近的7天' + }, '›') + ) + ), + + React.createElement('div', { className: 'lc-table-section' }, + React.createElement('div', { className: 'lc-table-toolbar' }, + React.createElement('div', { className: 'lc-table-toolbar-left' }, + React.createElement(Badge, { count: pendingAppointmentCount, offset: [8, 0], size: 'small' }, + React.createElement(Button, { + type: 'default', + icon: HR_ICONS.calendar, + style: { borderRadius: 8, fontWeight: 600, borderColor: '#3b82f6', color: '#2563eb' }, + onClick: function () { setAppointmentModalOpen(true); }, + 'aria-label': '预约加氢' + }, '预约加氢') + ) + ), + React.createElement('div', { className: 'lc-table-toolbar-actions' }, + React.createElement(Button, { + type: 'default', + style: { borderRadius: 8, fontWeight: 600, borderColor: '#10b981', color: '#059669' }, + onClick: handleExport, + 'aria-label': '导出加氢记录' + }, '导出'), + React.createElement(Button, { + type: 'primary', + style: HR_PRIMARY_BTN_STYLE, + onClick: openCreate, + 'aria-label': '新增加氢记录' + }, '新增') + ) + ), + React.createElement('div', { className: 'lc-table-card' }, + React.createElement(Table, { + className: 'lc-list-table', + rowKey: 'id', + columns: columns, + dataSource: filteredList, + size: 'middle', + scroll: { x: 1100 }, + pagination: { + current: page, + pageSize: pageSize, + total: filteredList.length, + showSizeChanger: true, + pageSizeOptions: ['5', '10', '20', '50'], + showTotal: function (t) { return '共 ' + t + ' 条'; }, + onChange: function (p, ps) { setPage(p); setPageSize(ps); } + }, + locale: { emptyText: emptyNode } + }) + ) + ) + ) : null, + + React.createElement(Modal, { + className: 'h2-refuel-drill-modal', + title: '预约加氢', + open: appointmentModalOpen, + onCancel: function () { setAppointmentModalOpen(false); }, + footer: React.createElement(Button, { + type: 'primary', + onClick: function () { setAppointmentModalOpen(false); }, + style: HR_PRIMARY_BTN_STYLE + }, '知道了'), + width: 980, + centered: true, + destroyOnClose: true, + styles: { body: { maxHeight: '78vh', overflow: 'auto' } } + }, + renderAppointmentPanel() + ), + + React.createElement(Modal, { + title: '确认删除', + open: deleteModal.open, + centered: true, + onCancel: function () { setDeleteModal({ open: false, record: null }); }, + onOk: function () { + if (deleteModal.record) { + setListData(function (prev) { + return prev.filter(function (r) { return r.id !== deleteModal.record.id; }); + }); + message.success('已删除'); + } + setDeleteModal({ open: false, record: null }); + }, + okButtonProps: { danger: true }, + okText: '删除', + cancelText: '取消' + }, '确定删除该条加氢记录吗?车牌号「' + ((deleteModal.record && deleteModal.record.plateNo) || '') + '」,此操作不可撤销。'), + + React.createElement(Modal, { + title: '拒绝预约', + open: rejectModal.open, + centered: true, + width: 480, + onCancel: closeRejectModal, + onOk: handleConfirmRejectAppointment, + okText: '确认拒绝', + cancelText: '取消', + okButtonProps: { danger: true }, + destroyOnClose: true + }, + React.createElement('div', { style: { marginBottom: 12, fontSize: 13, color: '#475569', lineHeight: 1.6 } }, + '车牌号:', + React.createElement('strong', { style: { color: '#0f172a' } }, (rejectModal.record && rejectModal.record.plateNo) || '—'), + ' · 预约时间:', + React.createElement('span', { style: { fontVariantNumeric: 'tabular-nums' } }, (rejectModal.record && rejectModal.record.appointmentTime) || '—') + ), + React.createElement(Form, { layout: 'vertical', requiredMark: false }, + React.createElement(Form.Item, { + label: React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), '拒绝原因') + }, + React.createElement(Input.TextArea, { + placeholder: '请填写拒绝原因,将通知预约司机', + value: rejectModal.reason, + rows: 4, + maxLength: 200, + showCount: true, + style: { borderRadius: 8 }, + onChange: function (e) { + setRejectModal(function (m) { return Object.assign({}, m, { reason: e.target.value }); }); + } + }) + ) + ) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-h2-station/pages/03-站点信息.jsx b/src/prototypes/oneos-web-h2-station/pages/03-站点信息.jsx new file mode 100644 index 0000000..7382f91 --- /dev/null +++ b/src/prototypes/oneos-web-h2-station/pages/03-站点信息.jsx @@ -0,0 +1,10044 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 加氢站管理 - 站点信息(列表 + KPI 分类 + 独立新建/编辑页 + 使用说明书) + +var H2_REGION_CASCADER_OPTIONS = [ + { value: '浙江省', label: '浙江省', children: [{ value: '嘉兴市', label: '嘉兴市' }, { value: '杭州市', label: '杭州市' }, { value: '宁波市', label: '宁波市' }] }, + { value: '上海市', label: '上海市', children: [{ value: '上海市', label: '上海市' }] }, + { value: '江苏省', label: '江苏省', children: [{ value: '南京市', label: '南京市' }, { value: '苏州市', label: '苏州市' }] }, + { value: '广东省', label: '广东省', children: [{ value: '广州市', label: '广州市' }, { value: '深圳市', label: '深圳市' }] } +]; + +var H2_STATION_KPI_CARDS = [ + { key: 'all', type: 'total', title: '全部加氢站', desc: '纳入台账管理的全部站点' }, + { key: 'balanceAlert', type: 'warning', title: '预付余额预警站点', desc: '预付余额低于所设提醒阈值的站点' }, + { key: 'arrears', type: 'unuploaded', title: '已欠费站点', desc: '预付余额小于 0 的站点' }, + { key: 'none', type: 'unuploaded', title: '无加氢站点', desc: '暂无加氢记录的站点' } +]; + +var H2_SIGNED_FILTER_CARDS = [ + { key: 'yes', type: 'normal', title: '签约站点', desc: '已签约的加氢站点,点击快捷筛选' }, + { key: 'no', type: 'unuploaded', title: '普通站点', desc: '尚未签约的加氢站点,点击快捷筛选' } +]; + +var H2_BUSINESS_STATUS_OPTIONS = [ + { value: '营业中', label: '营业中' }, + { value: '暂停营业', label: '暂停营业' }, + { value: '停止营业', label: '停止营业' } +]; + +/** @see web端/styles/oneos-ant-table-global-fix.js — 与 ant-table-global-fix.css 保持同步 */ +var ONEOS_ANT_TABLE_GLOBAL_FIX = [ + '.ant-table-container .ant-table-header { margin-bottom: 0 !important; }', + '.ant-table-container .ant-table-body { margin-top: 0 !important; }', + '.ant-table-container .ant-table-body > table, .ant-table-content table { margin-top: 0 !important; }', + '.ant-table-tbody > tr.ant-table-measure-row, .ant-table-tbody > tr.ant-table-measure-row > td, .ant-table-tbody > tr.ant-table-measure-row > th { display: none !important; height: 0 !important; max-height: 0 !important; min-height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; line-height: 0 !important; font-size: 0 !important; overflow: hidden !important; visibility: hidden !important; pointer-events: none !important; }' +]; + +var H2_PAGE_STYLE = ONEOS_ANT_TABLE_GLOBAL_FIX.concat([ + '.h2-station-page { --h2-form-row-gap: 20px; --h2-form-col-gap: 24px; --h2-form-label-gap: 8px; padding: 24px 24px 80px; height: 100vh; display: flex; flex-direction: column; background: linear-gradient(165deg, #f1f5f9 0%, #f8fafc 50%, #f1f5f9 100%); overflow: hidden; box-sizing: border-box; }', + '.h2-station-page .lc-filter-card.ant-card { border-radius: 16px !important; border: 1px solid #e2e8f0 !important; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.03) !important; margin-bottom: 16px; }', + '.h2-station-page .lc-filter-card > .ant-card-head { border-bottom: 1px solid #f1f5f9 !important; min-height: auto; padding: 12px 20px !important; }', + '.h2-station-page .lc-filter-card > .ant-card-head .ant-card-head-title { font-size: 15px !important; font-weight: 700 !important; color: #0f172a !important; padding: 0 !important; }', + '.h2-station-page .lc-filter-card > .ant-card-body { padding: 16px 20px 20px !important; }', + '.h2-station-page .lc-filter-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px 24px; align-items: center; }', + '@media (max-width: 1280px) { .h2-station-page .lc-filter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }', + '@media (max-width: 640px) { .h2-station-page .lc-filter-grid { grid-template-columns: 1fr; } }', + '.h2-station-page .lc-filter-field { display: flex; align-items: center; gap: 12px; min-width: 0; min-height: 32px; }', + '.h2-station-page .lc-filter-field-label { flex: 0 0 88px; text-align: right; font-size: 13px; font-weight: 500; color: #475569; line-height: 32px; white-space: nowrap; }', + '.h2-station-page .lc-filter-field-control { flex: 1; min-width: 0; }', + '.h2-station-page .lc-filter-field-control .ant-input, .h2-station-page .lc-filter-field-control .ant-input-affix-wrapper, .h2-station-page .lc-filter-field-control .ant-select .ant-select-selector, .h2-station-page .lc-filter-field-control .ant-cascader .ant-select-selector { width: 100%; height: 32px !important; min-height: 32px !important; border-radius: 8px !important; box-sizing: border-box; }', + '.h2-station-page .lc-filter-field-control .ant-input-affix-wrapper { display: inline-flex; align-items: center; padding-top: 0 !important; padding-bottom: 0 !important; }', + '.h2-station-page .lc-filter-field-control .ant-input-affix-wrapper > input.ant-input { height: 30px; line-height: 30px; padding-top: 0; padding-bottom: 0; }', + '.h2-station-page .lc-filter-field-control .ant-select-single .ant-select-selector { display: flex; align-items: center; }', + '.h2-station-page .lc-filter-field-control .ant-select-single .ant-select-selector .ant-select-selection-search-input { height: 30px; }', + '.h2-station-page .lc-filter-field-control .ant-select-single .ant-select-selector .ant-select-selection-item, .h2-station-page .lc-filter-field-control .ant-select-single .ant-select-selector .ant-select-selection-placeholder { line-height: 30px !important; }', + '.h2-station-page .lc-filter-field-control .ant-cascader .ant-select-selector { display: flex; align-items: center; }', + '.h2-station-page .lc-filter-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; padding-top: 16px; border-top: 1px solid #f1f5f9; }', + '.h2-station-page .lc-alert-stats-row { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; }', + '@media (max-width: 1400px) { .h2-station-page .lc-alert-stats-row { grid-template-columns: repeat(3, minmax(0, 1fr)); } }', + '@media (max-width: 900px) { .h2-station-page .lc-alert-stats-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } }', + '@media (max-width: 640px) { .h2-station-page .lc-alert-stats-row { grid-template-columns: 1fr; } }', + '.h2-station-page .lc-alert-card { display: flex; align-items: center; gap: 14px; padding: 16px 30px 16px 16px; border-radius: 12px; border: 1px solid #e2e8f0; background: #fff; position: relative; overflow: hidden; min-width: 0; min-height: 72px; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-station-page .lc-alert-card-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }', + '.h2-station-page .lc-alert-card-icon { flex-shrink: 0; width: 48px; height: 48px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: linear-gradient(145deg, #0d7a63 0%, #065f4a 100%); color: #fff; box-shadow: 0 4px 12px rgba(6, 95, 74, 0.28); }', + '.h2-station-page .lc-alert-card-icon svg { display: block; flex-shrink: 0; }', + '.h2-station-page .lc-alert-card-val { font-size: 24px; font-weight: 800; line-height: 1.1; color: #0f172a; font-variant-numeric: tabular-nums; }', + '.h2-station-page .lc-alert-card-title { font-size: 13px; font-weight: 500; color: #64748b; line-height: 1.3; }', + '.h2-station-page .lc-alert-card-tip-anchor { position: absolute; top: 8px; right: 8px; z-index: 2; line-height: 0; }', + '.h2-station-page .lc-alert-card-tip { width: 18px; height: 18px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; color: #94a3b8; background: rgba(255,255,255,0.92); border: 1px solid #e2e8f0; cursor: help; line-height: 0; }', + '.h2-station-page .lc-alert-card-tip:hover { color: #64748b; border-color: #cbd5e1; background: #fff; }', + '.h2-station-page .lc-alert-card--total, .h2-station-page .lc-alert-card--normal, .h2-station-page .lc-alert-card--warning, .h2-station-page .lc-alert-card--unuploaded { background: #fff; border-color: #e2e8f0; }', + '.h2-station-page .lc-alert-card--normal .lc-alert-card-val, .h2-station-page .lc-alert-card--warning .lc-alert-card-val, .h2-station-page .lc-alert-card--unuploaded .lc-alert-card-val { color: #0f172a; }', + '.h2-station-page .lc-alert-card-clickable { cursor: pointer; transition: box-shadow 0.2s ease, border-color 0.2s ease, transform 0.2s ease; }', + '.h2-station-page .lc-alert-card-clickable:hover { box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08); }', + '.h2-station-page .lc-alert-card-clickable:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; }', + '.h2-station-page .lc-alert-card-active { box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.25) !important; border-color: #10b981 !important; }', + '.h2-station-page .lc-table-section { margin-bottom: 0; flex: 1; display: flex; flex-direction: column; min-height: 0; }', + '.h2-station-page .lc-table-toolbar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px 16px; margin-bottom: 8px; min-height: 32px; }', + '.h2-station-page .lc-table-legend-outer { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; padding: 6px 4px; font-size: 12px; color: #64748b; }', + '.h2-station-page .lc-table-legend-label { font-weight: 600; color: #64748b; }', + '.h2-station-page .lc-table-legend-item { display: inline-flex; align-items: center; gap: 6px; }', + '.h2-station-page .lc-table-legend-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }', + '.h2-station-page .lc-table-toolbar-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-left: auto; }', + '.h2-station-page .lc-table-card { background: #fff; border-radius: 16px; border: 1px solid #e2e8f0; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.03); overflow: hidden; flex: 1; overflow-y: auto; display: flex; flex-direction: column; }', + '.h2-station-page .lc-list-table .ant-table-wrapper, .h2-station-page .lc-list-table .ant-table { width: 100% !important; }', + '.h2-station-page .lc-table-card .ant-table-thead > tr > th { background: #f8fafc !important; color: #475569 !important; font-weight: 700 !important; font-size: 13px !important; border-bottom: 1px solid #e2e8f0 !important; padding: 12px 16px !important; }', + '.h2-station-page .lc-table-card .ant-table-tbody > tr.ant-table-measure-row, .h2-station-page .lc-table-card .ant-table-tbody > tr.ant-table-measure-row > td { height: 0 !important; max-height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; line-height: 0 !important; font-size: 0 !important; overflow: hidden !important; visibility: hidden !important; }', + '.h2-station-page .lc-table-card .ant-table-tbody > tr:not(.ant-table-measure-row) > td { padding: 12px 16px !important; font-size: 13px; }', + '.h2-station-page .lc-table-card .ant-table-tbody > tr:not(.ant-table-measure-row):hover > td { background: #f8fafc !important; }', + '.h2-station-page .lc-table-card .ant-pagination { margin: 0 !important; padding: 12px 16px !important; border-top: 1px solid #f1f5f9; }', + '.h2-station-page .lc-action-btn { font-weight: 600 !important; color: #10b981 !important; padding: 0 !important; min-height: 44px; }', + '.h2-station-page .lc-action-btn-danger { color: #ef4444 !important; }', + '.h2-station-page .h2-action-more-btn { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 8px; color: #64748b; cursor: pointer; transition: background 0.15s ease, color 0.15s ease; }', + '.h2-station-page .h2-action-more-btn:hover { background: #f1f5f9; color: #334155; }', + '.h2-station-page .h2-action-more-btn:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; }', + '.h2-station-page .h2-row-actions { display: inline-flex; align-items: center; gap: 4px; }', + '.h2-station-page .lc-station-name { font-weight: 700; color: #0f172a; font-size: 13px; }', + '.h2-station-page .lc-station-name-cell { min-width: 0; }', + '.h2-station-page .h2-current-cost-price-cell { display: inline-flex; align-items: center; justify-content: flex-end; gap: 6px; max-width: 100%; flex-wrap: nowrap; }', + '.h2-station-page .h2-price-type-tag.ant-tag { margin: 0; font-size: 11px; line-height: 18px; padding: 0 6px; flex-shrink: 0; }', + '.h2-station-page .h2-price-multi-warn { display: inline-flex; align-items: center; justify-content: center; color: #f59e0b; cursor: help; flex-shrink: 0; line-height: 0; }', + '.h2-station-page .h2-price-multi-warn:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; border-radius: 4px; }', + '.h2-station-page .lc-station-name-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }', + '.h2-station-page .lc-station-name-row .lc-station-name { min-width: 0; }', + '.h2-station-page .lc-station-address-line { margin-top: 4px; font-size: 12px; color: #64748b; line-height: 1.4; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }', + '.h2-station-page .lc-station-signed-tag { margin: 0 !important; border-radius: 6px !important; font-weight: 600 !important; flex-shrink: 0; line-height: 20px !important; }', + '.h2-station-page .lc-refuel-kg-row { display: flex; align-items: center; justify-content: flex-end; gap: 8px; flex-wrap: wrap; min-width: 0; }', + '.h2-station-page .lc-refuel-freq-tag { margin: 0 !important; border-radius: 6px !important; font-weight: 600 !important; flex-shrink: 0; line-height: 20px !important; }', + '.h2-station-page .lc-station-region { color: #64748b; font-size: 12px; margin-bottom: 2px; }', + '.h2-station-page .lc-station-contact-name { font-weight: 600; color: #0f172a; font-size: 13px; line-height: 1.35; }', + '.h2-station-page .lc-station-contact-phone { color: #64748b; font-size: 12px; margin-top: 2px; line-height: 1.35; font-variant-numeric: tabular-nums; }', + '.h2-station-page .h2-region-address { display: flex; flex-direction: column; gap: 12px; width: 100%; }', + '.h2-station-page .h2-region-address .ant-select, .h2-station-page .h2-region-address .ant-input { width: 100%; }', + '.h2-station-page .h2-region-address--inline { flex-direction: row; align-items: center; gap: 8px; }', + '.h2-station-page .h2-region-address--inline .h2-region-address-cascader { flex: 0 0 38%; max-width: 220px; min-width: 140px; }', + '.h2-station-page .h2-region-address--inline .h2-region-address-detail { flex: 1; min-width: 0; }', + '.h2-station-page .h2-create-radio-group { display: flex; flex-wrap: wrap; align-items: center; gap: 16px; min-height: 32px; }', + '.h2-station-page .h2-create-radio-group .ant-radio-wrapper { margin-inline-end: 0; font-size: 14px; color: #1d2129; }', + '.h2-station-page .h2-contract-dates { display: flex; align-items: center; gap: 8px; width: 100%; flex-wrap: wrap; }', + '.h2-station-page .h2-contract-dates-sep { color: #94a3b8; font-size: 13px; flex-shrink: 0; }', + '.h2-station-page .h2-contract-upload { display: flex; flex-direction: column; gap: 10px; width: 100%; }', + '.h2-station-page .h2-contract-upload-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }', + '.h2-station-page .h2-contract-file-list { display: flex; flex-direction: column; gap: 6px; }', + '.h2-station-page .h2-contract-file-item { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 10px; border-radius: 6px; background: #f8fafc; border: 1px solid #e2e8f0; font-size: 13px; }', + '.h2-station-page .h2-contract-file-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #334155; }', + '.h2-station-page .h2-contract-file-btns { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }', + '.h2-station-page--create .h2-create-form .h2-create-upload-btn.ant-upload-wrapper { width: auto; }', + '.h2-station-page--create .h2-create-form .h2-region-address { gap: 12px; margin-top: 0; }', + '.h2-station-page .h2-option-btn-group, .h2-business-status-modal .h2-option-btn-group { display: flex; flex-wrap: wrap; gap: 8px; }', + '.h2-station-page .h2-option-btn, .h2-business-status-modal .h2-option-btn { display: inline-flex; align-items: center; justify-content: center; min-height: 36px; padding: 0 16px; border-radius: 8px; border: 1px solid #e2e8f0; background: #fff; color: #475569; font-size: 13px; font-weight: 600; cursor: pointer; transition: border-color 0.2s ease, background 0.2s ease, color 0.2s ease, box-shadow 0.2s ease; }', + '.h2-station-page .h2-option-btn:hover:not(:disabled), .h2-business-status-modal .h2-option-btn:hover:not(:disabled) { border-color: #86efac; color: #047857; }', + '.h2-station-page .h2-option-btn:focus-visible, .h2-business-status-modal .h2-option-btn:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; }', + '.h2-station-page .h2-option-btn:disabled, .h2-business-status-modal .h2-option-btn:disabled { opacity: 0.55; cursor: not-allowed; }', + '.h2-station-page .h2-option-btn--active, .h2-business-status-modal .h2-option-btn--active { box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08); }', + '.h2-station-page .h2-option-btn--active.h2-option-btn--open, .h2-business-status-modal .h2-option-btn--active.h2-option-btn--open { border-color: #10b981; background: #ecfdf5; color: #047857; }', + '.h2-station-page .h2-option-btn--active.h2-option-btn--pause, .h2-business-status-modal .h2-option-btn--active.h2-option-btn--pause { border-color: #f97316; background: #fff7ed; color: #c2410c; }', + '.h2-station-page .h2-option-btn--active.h2-option-btn--stop, .h2-business-status-modal .h2-option-btn--active.h2-option-btn--stop { border-color: #ef4444; background: #fef2f2; color: #b91c1c; }', + '.h2-station-page .h2-option-btn--active.h2-option-btn--allday, .h2-business-status-modal .h2-option-btn--active.h2-option-btn--allday { border-color: #10b981; background: #ecfdf5; color: #047857; }', + '.h2-station-page .h2-option-btn--active.h2-option-btn--custom, .h2-business-status-modal .h2-option-btn--active.h2-option-btn--custom { border-color: #3b82f6; background: #eff6ff; color: #1d4ed8; }', + '.h2-station-page .h2-business-hours, .h2-business-status-modal .h2-business-hours { display: flex; flex-direction: column; gap: 12px; width: 100%; }', + '.h2-business-status-modal .h2-business-hours-custom-panel { margin-top: 4px; }', + '.h2-station-page .h2-business-hours-custom-panel, .h2-business-status-modal .h2-business-hours-custom-panel { padding: 12px 14px; border-radius: 10px; background: #f8fafc; border: 1px solid #e2e8f0; }', + '.h2-station-page .h2-business-hours-custom-panel__title, .h2-business-status-modal .h2-business-hours-custom-panel__title { margin-bottom: 10px; font-size: 12px; font-weight: 700; color: #475569; }', + '.h2-station-page .h2-business-hours-range, .h2-business-status-modal .h2-business-hours-range { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; width: 100%; }', + '.h2-station-page .h2-business-hours-range-item, .h2-business-status-modal .h2-business-hours-range-item { display: flex; flex-direction: column; gap: 6px; min-width: 0; }', + '.h2-station-page .h2-business-hours-range-label, .h2-business-status-modal .h2-business-hours-range-label { font-size: 12px; color: #64748b; line-height: 1.2; }', + '.h2-station-page--create .h2-create-form .h2-business-hours .ant-picker { width: 100% !important; height: 32px !important; border-radius: 2px !important; border: 1px solid #e5e6eb !important; background: #fff !important; }', + '.h2-station-page--create .h2-create-form .h2-business-hours .ant-picker .ant-picker-input > input { font-size: 14px !important; color: #1d2129 !important; }', + '.h2-station-page--create .h2-create-form .h2-business-hours .ant-picker:not(.ant-picker-disabled):hover { border-color: #c9cdd4 !important; background: #fff !important; }', + '.h2-station-page--create .h2-create-form .h2-business-hours .ant-picker-focused { border-color: #10b981 !important; box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2) !important; background: #fff !important; }', + '.h2-station-page--create .h2-create-form .h2-business-hours--col4 { gap: 8px; max-width: 100%; }', + '.h2-station-page--create .h2-create-form .h2-business-hours--col4 .ant-select { width: 100% !important; max-width: 100%; }', + '.h2-station-page--create .h2-create-form .h2-business-hours--col4 .h2-business-hours-range { grid-template-columns: 1fr; gap: 8px; }', + '.h2-station-page--create .h2-create-form .ant-col-6 .ant-form-item-control-input { max-width: 100%; }', + '.h2-station-page--create .h2-create-form--grid-4 .h2-region-address--stack-col4 { gap: 8px; }', + '.h2-station-page--create .h2-create-form--grid-4 .h2-region-address--stack-col4 .h2-region-address-cascader, .h2-station-page--create .h2-create-form--grid-4 .h2-region-address--stack-col4 .h2-region-address-detail { flex: none; max-width: 100%; width: 100%; min-width: 0; }', + '.h2-station-page--create .h2-create-form--grid-4 .h2-contract-panel { padding: 10px 12px; }', + '.h2-station-page--create .h2-create-form--grid-4 .h2-contract-upload-actions { flex-direction: column; align-items: stretch; gap: 6px; }', + '.h2-station-page--create .h2-create-form--grid-4 .h2-contract-upload-actions .ant-btn { width: 100%; }', + '.h2-station-page--create .h2-create-form--grid-4 .h2-contract-dates { flex-direction: column; align-items: stretch; gap: 6px; }', + '.h2-station-page--create .h2-create-form--grid-4 .h2-contract-dates-sep { text-align: center; line-height: 1; }', + '.h2-station-page--create .h2-create-form--grid-4 .h2-create-radio-group { gap: 8px 12px; }', + '.h2-station-page--create .h2-supplier-mode-card--compact { flex-direction: column; align-items: stretch; justify-content: flex-start; min-height: 88px; padding: 12px; gap: 8px; }', + '.h2-station-page--create .h2-supplier-mode-card--compact .h2-supplier-mode-card-icon { width: 32px; height: 32px; }', + '.h2-station-page--create .h2-supplier-mode-card--compact .h2-supplier-mode-card-title { font-size: 13px; }', + '.h2-station-page--create .h2-supplier-mode-card--compact .h2-supplier-mode-card-desc { font-size: 11px; line-height: 1.35; }', + '.h2-station-page--create .h2-create-form--grid-4 .h2-supplier-link-select { padding: 0; margin: 0; border: none; background: transparent; }', + '.h2-station-page--create .h2-create-form--grid-4 .h2-create-upload.ant-upload-wrapper .ant-upload-drag { padding: 10px 8px !important; }', + '.h2-station-page--create .h2-create-form--grid-4 .h2-create-upload.ant-upload-wrapper .ant-upload-drag .ant-upload-drag-icon { margin-bottom: 4px !important; }', + '.h2-station-page--create .h2-create-form--grid-4 .h2-create-upload.ant-upload-wrapper .ant-upload-drag p { font-size: 12px !important; }', + '.h2-station-page .h2-address-paste { display: flex; flex-direction: column; width: 100%; }', + '.h2-station-page .ant-drawer-body .ant-form .ant-form-item { margin-bottom: 20px; }', + '.h2-station-page .ant-drawer-body .ant-form .ant-form-item:last-child { margin-bottom: 0; }', + '.h2-station-page .ant-drawer-body .ant-form .ant-form-item-label { padding-bottom: var(--h2-form-label-gap, 8px); }', + '.h2-station-page .ant-drawer-body .ant-form .ant-form-item-label > label { min-height: 22px; line-height: 22px; }', + '.h2-station-page .ant-drawer-body .ant-divider { margin: 4px 0 20px !important; }', + '.h2-station-page .h2-address-paste .ant-input-textarea { font-size: 13px !important; line-height: 1.45 !important; resize: vertical; min-height: 56px; }', + '.h2-station-page .h2-prd-modal .ant-modal-body { max-height: 70vh; overflow: auto; }', + '.h2-station-page .h2-prd-content { padding: 8px 0; white-space: pre-wrap; font-size: 13px; line-height: 1.65; color: #475569; }', + '.h2-station-page .h2-req-doc-panel { max-width: 100%; }', + '.h2-station-page .h2-req-doc-panel h1:first-child { margin-top: 0; }', + '.h2-station-page .h2-import-template-bar { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding: 14px 16px; margin-bottom: 14px; border-radius: 12px; background: linear-gradient(135deg, #ecfdf5 0%, #f8fafc 100%); border: 1px solid #bbf7d0; }', + '.h2-station-page .h2-import-template-bar-text { font-size: 13px; color: #475569; line-height: 1.55; flex: 1; min-width: 0; }', + '.h2-station-page .h2-import-preview { margin-top: 14px; padding: 12px 14px; border-radius: 10px; background: #f8fafc; border: 1px solid #e2e8f0; font-size: 13px; color: #334155; }', + '.h2-station-page .h2-import-error-list { margin: 8px 0 0; padding-left: 18px; max-height: 120px; overflow: auto; font-size: 12px; color: #b91c1c; }', + '.h2-station-page .h2-status-log-section { margin-top: 20px; padding-top: 16px; border-top: 1px solid #f1f5f9; }', + '.h2-station-page .h2-status-log-title { font-size: 14px; font-weight: 700; color: #0f172a; margin-bottom: 10px; }', + '.h2-station-page .h2-status-log-table .ant-table-thead > tr > th { background: #f8fafc !important; font-size: 12px !important; padding: 8px 12px !important; }', + '.h2-station-page .h2-status-log-table .ant-table-tbody > tr > td { font-size: 12px !important; padding: 8px 12px !important; }', + '.h2-station-page .h2-refuel-drill-link { border: none; background: none; padding: 0; font-weight: 700; color: #059669; cursor: pointer; font-variant-numeric: tabular-nums; font-size: 13px; }', + '.h2-station-page .h2-refuel-drill-link:hover { color: #047857; text-decoration: underline; }', + '.h2-station-page .h2-refuel-drill-link:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; border-radius: 4px; }', + '.h2-station-page .h2-ledger-totals-bar { display: flex; align-items: stretch; gap: 0; margin-bottom: 12px; border: 1px solid #bae6fd; border-radius: 10px; overflow: hidden; background: #f8fafc; box-shadow: 0 1px 0 rgba(15, 23, 42, 0.04); }', + '.h2-station-page .h2-ledger-totals-bar__title { display: flex; align-items: center; justify-content: center; min-width: 72px; padding: 10px 14px; font-size: 14px; font-weight: 700; color: #0f172a; background: #e8f4fc; border-right: 1px solid #bae6fd; flex-shrink: 0; }', + '.h2-station-page .h2-ledger-totals-bar__items { display: flex; flex: 1; flex-wrap: wrap; }', + '.h2-station-page .h2-ledger-totals-bar__item { flex: 1; min-width: 140px; padding: 8px 20px; border-right: 1px solid #e2e8f0; display: flex; flex-direction: column; justify-content: center; gap: 4px; }', + '.h2-station-page .h2-ledger-totals-bar__item:last-child { border-right: none; }', + '.h2-station-page .h2-ledger-totals-bar__label { font-size: 12px; color: rgba(15, 23, 42, 0.55); font-weight: 500; line-height: 1.2; }', + '.h2-station-page .h2-ledger-totals-bar__value { font-size: 16px; font-weight: 700; color: #0f172a; font-variant-numeric: tabular-nums; line-height: 1.3; }', + '.h2-refuel-drill-modal .ant-modal-content { border-radius: 16px !important; overflow: hidden; box-shadow: 0 24px 48px -12px rgba(15, 23, 42, 0.18) !important; }', + '.h2-refuel-drill-modal .ant-modal-header { padding: 18px 24px 14px !important; border-bottom: 1px solid #f1f5f9 !important; margin-bottom: 0 !important; }', + '.h2-refuel-drill-modal .ant-modal-title { font-size: 17px !important; font-weight: 700 !important; color: #0f172a !important; }', + '.h2-refuel-drill-modal .ant-modal-body { padding: 16px 24px 20px !important; background: #f8fafc; }', + '.h2-refuel-drill-modal .ant-modal-footer { padding: 12px 24px 18px !important; border-top: 1px solid #f1f5f9 !important; background: #fff; }', + '.h2-refuel-drill-modal .h2-refuel-drill-panel { display: flex; flex-direction: column; gap: 14px; }', + '.h2-refuel-drill-modal .h2-refuel-drill-station-card { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 12px 16px; background: linear-gradient(135deg, #eff6ff 0%, #ecfdf5 50%, #f8fafc 100%); border: 1px solid #bfdbfe; border-radius: 12px; }', + '.h2-refuel-drill-modal .h2-refuel-drill-station-card__name { font-size: 15px; font-weight: 700; color: #0f172a; line-height: 1.35; }', + '.h2-refuel-drill-modal .h2-refuel-drill-station-card__meta { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; white-space: nowrap; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }', + '@media (max-width: 860px) { .h2-refuel-drill-modal .h2-refuel-drill-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } }', + '@media (max-width: 520px) { .h2-refuel-drill-modal .h2-refuel-drill-stats { grid-template-columns: 1fr; } }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat { display: flex; flex-direction: column; justify-content: center; min-height: 78px; padding: 12px 14px; border-radius: 12px; border: 1px solid #e2e8f0; background: #fff; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06); min-width: 0; box-sizing: border-box; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat--count { border-left: 4px solid #3b82f6; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat--kg { border-left: 4px solid #10b981; background: linear-gradient(180deg, #fff 0%, #f0fdf4 100%); }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat--cost { border-left: 4px solid #f97316; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat--customer { border-left: 4px solid #6366f1; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat__label { font-size: 12px; font-weight: 600; color: #64748b; margin-bottom: 8px; line-height: 1.2; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat__value { font-size: 20px; font-weight: 800; font-variant-numeric: tabular-nums; line-height: 1.2; color: #0f172a; word-break: break-all; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat__value--count { color: #2563eb; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat__value--kg { color: #059669; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat__value--cost { color: #ea580c; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat__value--customer { color: #4f46e5; }', + '.h2-refuel-drill-modal .h2-refuel-drill-table-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-refuel-drill-modal .h2-refuel-drill-table-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 14px; border-bottom: 1px solid #f1f5f9; background: #fafbfc; }', + '.h2-refuel-drill-modal .h2-refuel-drill-table-head__left { display: flex; align-items: center; gap: 10px; min-width: 0; }', + '.h2-refuel-drill-modal .h2-refuel-drill-table-head__title { font-size: 13px; font-weight: 700; color: #334155; }', + '.h2-refuel-drill-modal .h2-refuel-drill-table-head__count { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; }', + '.h2-refuel-drill-modal .h2-refuel-record-table { border-radius: 0 !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table { background: transparent !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-container { border: none !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-header { margin-bottom: 0 !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-body { margin-top: 0 !important; overflow-y: auto !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-body table { margin-top: 0 !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-tbody > tr.ant-table-measure-row, .h2-refuel-drill-modal .h2-refuel-record-table .ant-table-tbody > tr.ant-table-measure-row > td { height: 0 !important; max-height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; line-height: 0 !important; font-size: 0 !important; overflow: hidden !important; visibility: hidden !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-thead > tr > th { background: #f8fafc !important; font-size: 12px !important; font-weight: 700 !important; color: #475569 !important; padding: 10px 12px !important; border-bottom: 1px solid #e2e8f0 !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-tbody > tr:not(.ant-table-measure-row) > td { font-size: 13px !important; padding: 10px 12px !important; vertical-align: middle !important; border-bottom: 1px solid #f8fafc !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-tbody > tr:not(.ant-table-measure-row):last-child > td { border-bottom: none !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-tbody > tr:hover > td { background: #eff6ff !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-seq { display: inline-flex; align-items: center; justify-content: center; min-width: 24px; height: 24px; border-radius: 6px; background: #f1f5f9; color: #64748b; font-size: 12px; font-weight: 700; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-time { color: #334155; font-variant-numeric: tabular-nums; font-size: 12px; white-space: nowrap; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-plate { font-weight: 700; color: #0f172a; font-variant-numeric: tabular-nums; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-kg { font-weight: 700; color: #059669; font-variant-numeric: tabular-nums; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-money { font-variant-numeric: tabular-nums; white-space: nowrap; font-weight: 600; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-money--cost { color: #ea580c; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-money--customer { color: #4f46e5; font-weight: 700; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-order-no { display: inline-block; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; color: #475569; background: #f1f5f9; border: 1px solid #e2e8f0; padding: 2px 7px; border-radius: 6px; letter-spacing: 0.02em; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }', + '.h2-station-page .h2-prepaid-balance-cell { display: flex; align-items: center; justify-content: flex-end; gap: 6px; flex-wrap: nowrap; box-sizing: border-box; }', + '.h2-station-page .h2-prepaid-balance-cell .h2-prepaid-balance-amount { flex: 0 0 auto; white-space: nowrap; border: none; background: none; padding: 0; font: inherit; font-weight: 700; cursor: pointer; text-decoration: underline; text-underline-offset: 2px; font-variant-numeric: tabular-nums; }', + '.h2-station-page .h2-prepaid-balance-cell .h2-prepaid-balance-amount:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; border-radius: 4px; }', + '.h2-station-page .h2-prepaid-balance-cell .lc-station-signed-tag { flex-shrink: 0; }', + '.h2-station-page .lc-table-card .ant-table-tbody > tr:not(.ant-table-measure-row) > td.h2-cell-prepaid-balance { overflow: visible !important; }', + '.h2-balance-drill-modal .ant-modal-content { border-radius: 16px !important; overflow: hidden; box-shadow: 0 24px 48px -12px rgba(15, 23, 42, 0.18) !important; }', + '.h2-balance-drill-modal .ant-modal-header { padding: 18px 24px 14px !important; border-bottom: 1px solid #f1f5f9 !important; margin-bottom: 0 !important; }', + '.h2-balance-drill-modal .ant-modal-title { font-size: 17px !important; font-weight: 700 !important; color: #0f172a !important; }', + '.h2-balance-drill-modal .ant-modal-body { padding: 16px 24px 20px !important; background: #f8fafc; }', + '.h2-balance-drill-modal .ant-modal-footer { padding: 12px 24px 18px !important; border-top: 1px solid #f1f5f9 !important; background: #fff; }', + '.h2-balance-drill-modal .h2-balance-drill-panel { display: flex; flex-direction: column; gap: 14px; }', + '.h2-balance-drill-modal .h2-balance-drill-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }', + '@media (max-width: 860px) { .h2-balance-drill-modal .h2-balance-drill-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } }', + '@media (max-width: 520px) { .h2-balance-drill-modal .h2-balance-drill-stats { grid-template-columns: 1fr; } }', + '.h2-balance-drill-modal .h2-balance-drill-stat { display: flex; flex-direction: column; justify-content: center; min-height: 78px; padding: 12px 14px; border-radius: 12px; border: 1px solid #e2e8f0; background: #fff; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06); min-width: 0; box-sizing: border-box; }', + '.h2-balance-drill-modal .h2-balance-drill-stat--balance { border-left: 4px solid #10b981; background: linear-gradient(180deg, #fff 0%, #f0fdf4 100%); }', + '.h2-balance-drill-modal .h2-balance-drill-stat--income { border-left: 4px solid #10b981; }', + '.h2-balance-drill-modal .h2-balance-drill-stat--expense { border-left: 4px solid #f97316; }', + '.h2-balance-drill-modal .h2-balance-drill-stat__label { font-size: 12px; font-weight: 600; color: #64748b; margin-bottom: 8px; line-height: 1.2; }', + '.h2-balance-drill-modal .h2-balance-trend-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-balance-drill-modal .h2-balance-trend-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 14px; border-bottom: 1px solid #f1f5f9; background: #fafbfc; }', + '.h2-balance-drill-modal .h2-balance-trend-head__title { font-size: 13px; font-weight: 700; color: #334155; }', + '.h2-balance-drill-modal .h2-balance-trend-head__meta { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; }', + '.h2-balance-drill-modal .h2-balance-trend-body { padding: 12px 14px 8px; }', + '.h2-balance-drill-modal .h2-balance-trend-chart-wrap { position: relative; }', + '.h2-balance-drill-modal .h2-balance-trend-svg { display: block; width: 100%; height: auto; }', + '.h2-balance-drill-modal .h2-balance-trend-tooltip { position: absolute; z-index: 2; padding: 8px 10px; border-radius: 8px; background: rgba(15, 23, 42, 0.92); color: #f8fafc; font-size: 12px; line-height: 1.45; white-space: nowrap; pointer-events: none; box-shadow: 0 8px 20px rgba(15, 23, 42, 0.18); transform: translate(-50%, calc(-100% - 10px)); }', + '.h2-balance-drill-modal .h2-balance-trend-tooltip__date { color: #cbd5e1; font-size: 11px; margin-bottom: 2px; }', + '.h2-balance-drill-modal .h2-balance-trend-tooltip__balance { font-weight: 700; font-variant-numeric: tabular-nums; color: #fff; }', + '.h2-balance-drill-modal .h2-balance-trend-tooltip__balance--negative { color: #fca5a5; }', + '.h2-balance-drill-modal .h2-balance-trend-hit { fill: transparent; cursor: pointer; }', + '.h2-balance-drill-modal .h2-balance-trend-point--active { r: 5.5; stroke-width: 2.5; }', + '.h2-balance-drill-modal .h2-balance-trend-empty { padding: 36px 16px; text-align: center; font-size: 13px; color: #94a3b8; }', + '.h2-balance-drill-modal .h2-balance-trend-legend { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; padding: 8px 14px 12px; border-top: 1px solid #f8fafc; }', + '.h2-balance-drill-modal .h2-balance-trend-legend__item { display: inline-flex; align-items: center; gap: 6px; font-size: 11px; color: #64748b; }', + '.h2-balance-drill-modal .h2-balance-trend-legend__dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }', + '.h2-balance-drill-modal .h2-balance-trend-legend__dot--line { background: #10b981; }', + '.h2-balance-drill-modal .h2-balance-trend-legend__dot--area { background: rgba(16, 185, 129, 0.35); }', + '.h2-balance-drill-modal .h2-balance-drill-stat__value { font-size: 20px; font-weight: 800; font-variant-numeric: tabular-nums; line-height: 1.2; color: #0f172a; word-break: break-all; }', + '.h2-balance-drill-modal .h2-balance-drill-stat__value--income { color: #059669; }', + '.h2-balance-drill-modal .h2-balance-drill-stat__value--expense { color: #ea580c; }', + '.h2-balance-drill-modal .h2-balance-drill-stat__value--negative { color: #dc2626; }', + '.h2-balance-drill-modal .h2-balance-drill-table-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-balance-drill-modal .h2-balance-drill-table-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 14px; border-bottom: 1px solid #f1f5f9; background: #fafbfc; }', + '.h2-balance-drill-modal .h2-balance-drill-table-head__left { display: flex; align-items: center; gap: 10px; min-width: 0; }', + '.h2-balance-drill-modal .h2-balance-drill-table-head__title { font-size: 13px; font-weight: 700; color: #334155; }', + '.h2-balance-drill-modal .h2-balance-drill-table-head__count { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; }', + '.h2-balance-drill-modal .h2-balance-record-table { border-radius: 0 !important; }', + '.h2-balance-drill-modal .h2-balance-record-table .ant-table { background: transparent !important; }', + '.h2-balance-drill-modal .h2-balance-record-table .ant-table-container { border: none !important; }', + '.h2-balance-drill-modal .h2-balance-record-table .ant-table-header { margin-bottom: 0 !important; }', + '.h2-balance-drill-modal .h2-balance-record-table .ant-table-body { margin-top: 0 !important; overflow-y: auto !important; }', + '.h2-balance-drill-modal .h2-balance-record-table .ant-table-body table { margin-top: 0 !important; }', + '.h2-balance-drill-modal .h2-balance-record-table .ant-table-tbody > tr.ant-table-measure-row, .h2-balance-drill-modal .h2-balance-record-table .ant-table-tbody > tr.ant-table-measure-row > td { height: 0 !important; max-height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; line-height: 0 !important; font-size: 0 !important; overflow: hidden !important; visibility: hidden !important; }', + '.h2-balance-drill-modal .h2-balance-record-table .ant-table-thead > tr > th { background: #f8fafc !important; font-size: 12px !important; font-weight: 700 !important; color: #475569 !important; padding: 10px 12px !important; border-bottom: 1px solid #e2e8f0 !important; }', + '.h2-balance-drill-modal .h2-balance-record-table .ant-table-tbody > tr:not(.ant-table-measure-row) > td { font-size: 13px !important; padding: 10px 12px !important; vertical-align: middle !important; border-bottom: 1px solid #f8fafc !important; }', + '.h2-balance-drill-modal .h2-balance-record-table .ant-table-tbody > tr:not(.ant-table-measure-row):last-child > td { border-bottom: none !important; }', + '.h2-balance-drill-modal .h2-balance-record-table .ant-table-tbody > tr:hover > td { background: #f0fdf4 !important; }', + '.h2-balance-drill-modal .h2-balance-record-table .h2-balance-money { font-variant-numeric: tabular-nums; white-space: nowrap; letter-spacing: -0.01em; }', + '.h2-balance-drill-modal .h2-balance-record-table .h2-balance-money--income { color: #059669; font-weight: 600; }', + '.h2-balance-drill-modal .h2-balance-record-table .h2-balance-money--expense { color: #ea580c; font-weight: 600; }', + '.h2-balance-drill-modal .h2-balance-record-table .h2-balance-money--balance { color: #0f172a; font-weight: 700; }', + '.h2-balance-drill-modal .h2-balance-record-table .h2-balance-money--negative { color: #dc2626 !important; }', + '.h2-balance-drill-modal .h2-balance-record-table .h2-balance-money--muted { color: #cbd5e1; font-weight: 400; }', + '.h2-balance-drill-modal .h2-balance-record-table .h2-balance-order-no { display: inline-block; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; color: #475569; background: #f1f5f9; border: 1px solid #e2e8f0; padding: 2px 7px; border-radius: 6px; letter-spacing: 0.02em; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }', + '.h2-balance-drill-modal .h2-balance-record-table .h2-balance-seq { display: inline-flex; align-items: center; justify-content: center; min-width: 24px; height: 24px; border-radius: 6px; background: #f1f5f9; color: #64748b; font-size: 12px; font-weight: 700; }', + '.h2-station-view-modal .ant-modal-content { border-radius: 16px !important; overflow: hidden; box-shadow: 0 24px 48px -12px rgba(15, 23, 42, 0.18) !important; }', + '.h2-station-view-modal .ant-modal-header { padding: 18px 24px 14px !important; border-bottom: 1px solid #f1f5f9 !important; margin-bottom: 0 !important; }', + '.h2-station-view-modal .ant-modal-title { font-size: 17px !important; font-weight: 700 !important; color: #0f172a !important; }', + '.h2-station-view-modal .ant-modal-body { padding: 16px 24px 20px !important; background: #f8fafc; }', + '.h2-station-view-modal .ant-modal-footer { padding: 12px 24px 18px !important; border-top: 1px solid #f1f5f9 !important; background: #fff; }', + '.h2-station-view-modal .h2-station-view-panel { display: flex; flex-direction: column; gap: 20px; }', + '.h2-station-view-modal .h2-station-view-section__title { margin: 0 0 10px; padding-left: 10px; font-size: 14px; font-weight: 700; color: #334155; line-height: 1.35; border-left: 4px solid #10b981; }', + '.h2-station-view-modal .h2-station-view-section .ant-descriptions { background: #fff; border-radius: 10px; overflow: hidden; }', + '.h2-station-view-modal .h2-station-view-section .ant-descriptions-item-label { width: 132px !important; font-size: 12px !important; font-weight: 600 !important; color: #64748b !important; background: #fafbfc !important; }', + '.h2-station-view-modal .h2-station-view-section .ant-descriptions-item-content { font-size: 13px !important; color: #0f172a !important; font-weight: 500 !important; word-break: break-word; }', + '.h2-station-view-modal .h2-station-view-files { display: flex; flex-wrap: wrap; gap: 8px; }', + '.h2-station-view-modal .h2-station-view-file-link { display: inline-flex; align-items: center; max-width: 100%; padding: 4px 10px; border: 1px solid #e2e8f0; border-radius: 8px; background: #f8fafc; color: #059669; font-size: 13px; font-weight: 600; cursor: pointer; transition: background 0.2s ease, border-color 0.2s ease; }', + '.h2-station-view-modal .h2-station-view-file-link:hover { background: #ecfdf5; border-color: #86efac; }', + '.h2-station-view-modal .h2-station-view-file-link:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; }', + '.h2-station-view-modal .h2-station-view-license-count { font-size: 13px; color: #64748b; margin-bottom: 8px; }', + '.h2-station-view-modal .h2-station-view-license-images { display: flex; flex-wrap: wrap; gap: 10px; }', + '.h2-station-view-modal .h2-station-view-license-thumb { display: block; padding: 0; border: 1px solid #e2e8f0; border-radius: 10px; overflow: hidden; background: #f8fafc; cursor: pointer; transition: box-shadow 0.2s ease, border-color 0.2s ease; }', + '.h2-station-view-modal .h2-station-view-license-thumb:hover { border-color: #86efac; box-shadow: 0 4px 14px rgba(16, 185, 129, 0.15); }', + '.h2-station-view-modal .h2-station-view-license-thumb img { display: block; width: 168px; height: 118px; object-fit: cover; }', + '.h2-station-view-modal .h2-station-view-license-empty { font-size: 13px; color: #94a3b8; }', + '.h2-file-preview-modal .ant-modal-body { padding: 0 !important; background: #0f172a; }', + '.h2-file-preview-modal .h2-file-preview-body { display: flex; align-items: center; justify-content: center; min-height: 360px; max-height: 78vh; overflow: auto; padding: 12px; box-sizing: border-box; }', + '.h2-file-preview-modal .h2-file-preview-body iframe { width: 100%; min-height: 70vh; border: none; background: #fff; border-radius: 8px; }', + '.h2-file-preview-modal .h2-file-preview-body img { max-width: 100%; max-height: 76vh; border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,0.25); }', + '.h2-station-view-modal .h2-station-view-empty { padding: 20px 16px; text-align: center; font-size: 13px; color: #94a3b8; background: #fff; border: 1px dashed #e2e8f0; border-radius: 10px; }', + '.h2-price-config-modal .ant-modal-content { border-radius: 16px !important; overflow: hidden; box-shadow: 0 24px 48px -12px rgba(15, 23, 42, 0.18) !important; }', + '.h2-price-config-modal .ant-modal-header { padding: 18px 24px 14px !important; border-bottom: 1px solid #f1f5f9 !important; margin-bottom: 0 !important; }', + '.h2-price-config-modal .ant-modal-title { font-size: 17px !important; font-weight: 700 !important; color: #0f172a !important; }', + '.h2-price-config-modal .ant-modal-body { padding: 16px 24px 20px !important; background: #f8fafc; }', + '.h2-price-config-modal .ant-modal-footer { padding: 12px 24px 18px !important; border-top: 1px solid #f1f5f9 !important; background: #fff; }', + '.h2-price-config-modal .h2-price-config-panel { display: flex; flex-direction: column; gap: 14px; }', + '.h2-price-config-modal .h2-price-config-station-card { padding: 12px 16px; background: linear-gradient(135deg, #fff7ed 0%, #fffbeb 50%, #f8fafc 100%); border: 1px solid #fed7aa; border-radius: 12px; }', + '.h2-price-config-modal .h2-price-config-station-card__row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }', + '.h2-price-config-modal .h2-price-config-station-card__name { font-size: 15px; font-weight: 700; color: #0f172a; line-height: 1.35; min-width: 0; }', + '.h2-price-config-modal .h2-price-config-station-card .lc-station-signed-tag { margin: 0 !important; border-radius: 6px !important; font-weight: 600 !important; flex-shrink: 0; line-height: 20px !important; }', + '.h2-price-config-modal .h2-price-config-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }', + '@media (max-width: 640px) { .h2-price-config-modal .h2-price-config-stats { grid-template-columns: 1fr; } }', + '.h2-price-config-modal .h2-price-config-stat { display: flex; flex-direction: column; justify-content: center; min-height: 78px; padding: 12px 14px; border-radius: 12px; border: 1px solid #e2e8f0; background: #fff; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06); min-width: 0; box-sizing: border-box; }', + '.h2-price-config-modal .h2-price-config-stat--current { border-left: 4px solid #f97316; background: linear-gradient(180deg, #fff 0%, #fff7ed 100%); }', + '.h2-price-config-modal .h2-price-config-stat--history { border-left: 4px solid #3b82f6; }', + '.h2-price-config-modal .h2-price-config-stat--pending { border-left: 4px solid #6366f1; }', + '.h2-price-config-modal .h2-price-config-stat__label { font-size: 12px; font-weight: 600; color: #64748b; margin-bottom: 8px; line-height: 1.2; }', + '.h2-price-config-modal .h2-price-config-stat__value { font-size: 20px; font-weight: 800; font-variant-numeric: tabular-nums; line-height: 1.2; color: #0f172a; word-break: break-all; }', + '.h2-price-config-modal .h2-price-config-stat__value--current { color: #ea580c; }', + '.h2-price-config-modal .h2-price-config-stat__value--history { color: #2563eb; }', + '.h2-price-config-modal .h2-price-config-stat__value--pending { color: #4f46e5; }', + '.h2-price-config-modal .h2-price-config-form-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 14px 16px; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-price-config-modal .h2-price-config-form-wrap .ant-form-item { margin-bottom: 14px; }', + '.h2-price-config-modal .h2-price-config-form-wrap .ant-form-item:last-child { margin-bottom: 0; }', + '.h2-price-config-modal .h2-price-config-form-wrap .ant-form-item-label > label { font-size: 13px; font-weight: 600; color: #334155; }', + '.h2-price-config-modal .h2-price-config-form-wrap .ant-input, .h2-price-config-modal .h2-price-config-form-wrap .ant-picker { width: 100% !important; border-radius: 8px !important; }', + '.h2-price-config-modal .h2-price-config-form-hint { margin-top: 10px; padding: 8px 12px; font-size: 12px; color: #64748b; line-height: 1.5; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; }', + '.h2-price-config-modal .h2-price-config-form-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; padding-bottom: 10px; border-bottom: 1px solid #f1f5f9; }', + '.h2-price-config-modal .h2-price-config-form-head__title { font-size: 14px; font-weight: 700; color: #0f172a; }', + '.h2-price-config-modal .h2-price-config-table-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-price-config-modal .h2-price-config-table-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 14px; border-bottom: 1px solid #f1f5f9; background: #fafbfc; }', + '.h2-price-config-modal .h2-price-config-table-head__title { font-size: 13px; font-weight: 700; color: #334155; }', + '.h2-price-config-modal .h2-price-config-table-head__count { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; }', + '.h2-price-config-modal .h2-price-config-record-table { border-radius: 0 !important; }', + '.h2-price-config-modal .h2-price-config-record-table .ant-table { background: transparent !important; }', + '.h2-price-config-modal .h2-price-config-record-table .ant-table-container { border: none !important; }', + '.h2-price-config-modal .h2-price-config-record-table .ant-table-header { margin-bottom: 0 !important; }', + '.h2-price-config-modal .h2-price-config-record-table .ant-table-body { margin-top: 0 !important; overflow-y: auto !important; }', + '.h2-price-config-modal .h2-price-config-record-table .ant-table-body table { margin-top: 0 !important; }', + '.h2-price-config-modal .h2-price-config-record-table .ant-table-tbody > tr.ant-table-measure-row, .h2-price-config-modal .h2-price-config-record-table .ant-table-tbody > tr.ant-table-measure-row > td { height: 0 !important; max-height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; line-height: 0 !important; font-size: 0 !important; overflow: hidden !important; visibility: hidden !important; }', + '.h2-price-config-modal .h2-price-config-record-table .ant-table-thead > tr > th { background: #f8fafc !important; font-size: 12px !important; font-weight: 700 !important; color: #475569 !important; padding: 10px 12px !important; border-bottom: 1px solid #e2e8f0 !important; }', + '.h2-price-config-modal .h2-price-config-record-table .ant-table-tbody > tr:not(.ant-table-measure-row) > td { font-size: 13px !important; padding: 10px 12px !important; vertical-align: middle !important; border-bottom: 1px solid #f8fafc !important; }', + '.h2-price-config-modal .h2-price-config-record-table .ant-table-tbody > tr:not(.ant-table-measure-row):last-child > td { border-bottom: none !important; }', + '.h2-price-config-modal .h2-price-config-record-table .ant-table-tbody > tr:hover > td { background: #fff7ed !important; }', + '.h2-price-config-modal .h2-price-config-record-table .h2-price-config-money { font-variant-numeric: tabular-nums; white-space: nowrap; font-weight: 600; }', + '.h2-price-config-modal .h2-price-config-record-table .h2-price-config-money--before { color: #64748b; }', + '.h2-price-config-modal .h2-price-config-record-table .h2-price-config-money--after { color: #ea580c; font-weight: 700; }', + '.h2-price-config-modal .h2-price-config-after-lines { display: flex; flex-direction: column; gap: 4px; min-width: 0; }', + '.h2-price-config-modal .h2-price-config-after-line { font-size: 12px; line-height: 1.45; color: #334155; font-variant-numeric: tabular-nums; white-space: nowrap; }', + '.h2-price-config-modal .h2-price-config-after-line .h2-price-config-money { font-size: 12px; }', + '.h2-price-config-modal .h2-price-tier-rules { display: flex; flex-direction: column; gap: 10px; margin-top: 4px; }', + '.h2-price-config-modal .h2-price-tier-rule-row { display: grid; grid-template-columns: 1fr 1fr auto; gap: 10px; align-items: end; padding: 10px 12px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 10px; }', + '@media (max-width: 640px) { .h2-price-config-modal .h2-price-tier-rule-row { grid-template-columns: 1fr; } }', + '.h2-price-config-modal .h2-price-tier-rule-field label { display: block; font-size: 12px; font-weight: 600; color: #64748b; margin-bottom: 6px; }', + '.h2-price-config-modal .h2-price-config-effective-price { font-size: 12px; color: #334155; font-variant-numeric: tabular-nums; }', + '.h2-price-config-modal .h2-price-config-effective-table .ant-table-tbody > tr { cursor: default; }', + '.h2-price-config-modal .h2-price-config-rule-list--empty { font-size: 13px; color: #94a3b8; text-align: center; line-height: 1.5; }', + '.h2-price-config-modal .h2-price-config-rule-item { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 10px 12px; background: #fff; border: 1px solid #e2e8f0; border-radius: 10px; cursor: pointer; transition: border-color 0.2s ease, box-shadow 0.2s ease; }', + '.h2-price-config-modal .h2-price-config-rule-item:hover { border-color: #fdba74; }', + '.h2-price-config-modal .h2-price-config-rule-item.is-active { border-color: #f97316; box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.12); background: #fff7ed; }', + '.h2-price-config-modal .h2-price-config-rule-item__main { min-width: 0; flex: 1; }', + '.h2-price-config-modal .h2-price-config-rule-item__title { font-size: 13px; font-weight: 700; color: #0f172a; margin-bottom: 6px; }', + '.h2-price-config-modal .h2-price-config-rule-item__tags { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; margin-bottom: 6px; }', + '.h2-price-config-modal .h2-price-config-rule-item__price-text { font-size: 12px; color: #334155; font-variant-numeric: tabular-nums; line-height: 1.5; }', + '.h2-price-config-modal .h2-price-config-rule-item__time { font-size: 12px; color: #64748b; line-height: 1.5; font-variant-numeric: tabular-nums; }', + '.h2-price-config-modal .h2-price-config-rule-item__time-sep { margin: 0 8px; color: #cbd5e1; }', + '.h2-price-config-modal .h2-price-config-rule-item__meta { font-size: 12px; color: #64748b; line-height: 1.5; }', + '.h2-price-config-modal .h2-price-config-rule-item__timing { display: flex; flex-wrap: wrap; gap: 8px 16px; margin-top: 4px; font-size: 12px; color: #64748b; }', + '.h2-price-config-modal .h2-price-config-rule-item__timing-item { white-space: nowrap; }', + '.h2-price-config-modal .h2-price-tier-timing-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }', + '@media (max-width: 640px) { .h2-price-config-modal .h2-price-tier-timing-row { grid-template-columns: 1fr; } }', + '.h2-price-config-modal .h2-price-config-type-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 16px; margin-bottom: 14px; }', + '.h2-price-config-modal .h2-price-config-type-row__col { min-width: 0; }', + '.h2-price-config-modal .h2-price-config-type-row .ant-form-item { margin-bottom: 0; }', + '@media (max-width: 768px) { .h2-price-config-modal .h2-price-config-type-row { grid-template-columns: 1fr; } }', + '.h2-price-config-modal .h2-tier-customer-picker { width: 100%; }', + '.h2-price-config-modal .h2-tier-customer-picker__trigger { width: 100%; display: flex; align-items: center; justify-content: space-between; min-height: 32px; padding: 4px 11px; border: 1px solid #d9d9d9; border-radius: 8px; background: #fff; cursor: pointer; transition: border-color 0.2s ease, box-shadow 0.2s ease; }', + '.h2-price-config-modal .h2-tier-customer-picker__trigger:hover { border-color: #f97316; }', + '.h2-price-config-modal .h2-tier-customer-picker__trigger.is-open { border-color: #f97316; box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.12); }', + '.h2-price-config-modal .h2-tier-customer-picker__trigger-text { font-size: 13px; color: #334155; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; flex: 1; text-align: left; }', + '.h2-price-config-modal .h2-tier-customer-picker__trigger-text.is-placeholder { color: #94a3b8; }', + '.h2-price-config-modal .h2-tier-customer-dropdown { min-width: 300px; max-width: 360px; max-height: 300px; overflow-y: auto; padding: 6px 0; background: #fff; border-radius: 10px; box-shadow: 0 12px 32px rgba(15, 23, 42, 0.14); }', + '.h2-price-config-modal .h2-tier-customer-dropdown__item { display: flex; align-items: center; gap: 8px; padding: 8px 12px; font-size: 13px; color: #334155; cursor: pointer; transition: background 0.15s ease; }', + '.h2-price-config-modal .h2-tier-customer-dropdown__item:hover:not(.is-disabled) { background: #fff7ed; }', + '.h2-price-config-modal .h2-tier-customer-dropdown__item.is-disabled { color: #94a3b8; cursor: not-allowed; }', + '.h2-price-config-modal .h2-tier-customer-dropdown__item.is-all { border-bottom: 1px solid #f1f5f9; margin-bottom: 4px; font-weight: 600; }', + '.h2-price-config-modal .h2-tier-customer-dropdown__hint { padding: 0 12px 8px; font-size: 11px; color: #94a3b8; line-height: 1.45; }', + /* 阶梯客户下拉挂到 body,需全局样式(不可仅写在 .h2-price-config-modal 内) */ + '.h2-tier-customer-dropdown-overlay.ant-dropdown { background: transparent !important; box-shadow: none !important; padding: 0 !important; }', + '.h2-tier-customer-dropdown-overlay .ant-dropdown-menu { background: transparent !important; box-shadow: none !important; padding: 0 !important; }', + '.h2-tier-customer-dropdown { min-width: 300px; max-width: 360px; max-height: 300px; overflow-y: auto; padding: 6px 0; background: #fff !important; border: 1px solid #e2e8f0; border-radius: 10px; box-shadow: 0 12px 32px rgba(15, 23, 42, 0.14); }', + '.h2-tier-customer-dropdown__item { display: flex; align-items: center; gap: 8px; padding: 8px 12px; font-size: 13px; color: #334155; cursor: pointer; transition: background 0.15s ease; background: #fff; }', + '.h2-tier-customer-dropdown__item:hover:not(.is-disabled) { background: #fff7ed; }', + '.h2-tier-customer-dropdown__item.is-disabled { color: #94a3b8; cursor: not-allowed; background: #fff; }', + '.h2-tier-customer-dropdown__item.is-all { border-bottom: 1px solid #f1f5f9; margin-bottom: 4px; font-weight: 600; }', + '.h2-tier-customer-dropdown__hint { padding: 0 12px 8px; font-size: 11px; color: #94a3b8; line-height: 1.45; background: #fff; }', + '.h2-price-config-modal .h2-price-config-stat--tier { border-left: 4px solid #14b8a6; background: linear-gradient(180deg, #fff 0%, #f0fdfa 100%); }', + '.h2-price-config-modal .h2-price-config-stat__value--tier { color: #0f766e; }', + '.h2-business-status-modal .ant-modal-content { border-radius: 16px !important; overflow: hidden; box-shadow: 0 24px 48px -12px rgba(15, 23, 42, 0.18) !important; }', + '.h2-business-status-modal .ant-modal-header { padding: 18px 24px 14px !important; border-bottom: 1px solid #f1f5f9 !important; margin-bottom: 0 !important; }', + '.h2-business-status-modal .ant-modal-title { font-size: 17px !important; font-weight: 700 !important; color: #0f172a !important; }', + '.h2-business-status-modal .ant-modal-body { padding: 16px 24px 20px !important; background: #f8fafc; }', + '.h2-business-status-modal .ant-modal-footer { padding: 12px 24px 18px !important; border-top: 1px solid #f1f5f9 !important; background: #fff; }', + '.h2-business-status-modal .h2-business-status-panel { display: flex; flex-direction: column; gap: 14px; }', + '.h2-business-status-modal .h2-business-status-station-card { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 12px 16px; background: linear-gradient(135deg, #ecfdf5 0%, #f0fdf4 50%, #f8fafc 100%); border: 1px solid #bbf7d0; border-radius: 12px; }', + '.h2-business-status-modal .h2-business-status-station-card__name { font-size: 15px; font-weight: 700; color: #0f172a; line-height: 1.35; }', + '.h2-business-status-modal .h2-business-status-station-card__meta { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; white-space: nowrap; }', + '.h2-business-status-modal .h2-business-status-stats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }', + '@media (max-width: 640px) { .h2-business-status-modal .h2-business-status-stats { grid-template-columns: 1fr; } }', + '.h2-business-status-modal .h2-business-status-stat { display: flex; flex-direction: column; justify-content: center; min-height: 78px; padding: 12px 14px; border-radius: 12px; border: 1px solid #e2e8f0; background: #fff; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06); min-width: 0; box-sizing: border-box; }', + '.h2-business-status-modal .h2-business-status-stat--status { border-left: 4px solid #10b981; background: linear-gradient(180deg, #fff 0%, #f0fdf4 100%); }', + '.h2-business-status-modal .h2-business-status-stat--hours { border-left: 4px solid #3b82f6; }', + '.h2-business-status-modal .h2-business-status-stat__label { font-size: 12px; font-weight: 600; color: #64748b; margin-bottom: 8px; line-height: 1.2; }', + '.h2-business-status-modal .h2-business-status-stat__value { font-size: 16px; font-weight: 700; line-height: 1.35; color: #0f172a; word-break: break-word; }', + '.h2-business-status-modal .h2-business-status-stat__value--hours { font-size: 15px; font-weight: 700; color: #2563eb; font-variant-numeric: tabular-nums; }', + '.h2-business-status-modal .h2-business-status-form-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 14px 16px; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-business-status-modal .h2-business-status-form-wrap .ant-form-item { margin-bottom: 14px; }', + '.h2-business-status-modal .h2-business-status-form-wrap .ant-form-item:last-child { margin-bottom: 0; }', + '.h2-business-status-modal .h2-business-status-form-wrap .ant-form-item-label > label { font-size: 13px; font-weight: 600; color: #334155; }', + '.h2-business-status-modal .h2-business-status-form-wrap .ant-picker { width: 100% !important; border-radius: 8px !important; }', + '.h2-business-status-modal .h2-business-status-form-hint { margin-top: 10px; padding: 8px 12px; font-size: 12px; color: #64748b; line-height: 1.5; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; }', + '.h2-business-status-modal .h2-business-status-table-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-business-status-modal .h2-business-status-table-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 14px; border-bottom: 1px solid #f1f5f9; background: #fafbfc; }', + '.h2-business-status-modal .h2-business-status-table-head__title { font-size: 13px; font-weight: 700; color: #334155; }', + '.h2-business-status-modal .h2-business-status-table-head__count { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; }', + '.h2-business-status-modal .h2-business-status-record-table { border-radius: 0 !important; }', + '.h2-business-status-modal .h2-business-status-record-table .ant-table { background: transparent !important; }', + '.h2-business-status-modal .h2-business-status-record-table .ant-table-container { border: none !important; }', + '.h2-business-status-modal .h2-business-status-record-table .ant-table-header { margin-bottom: 0 !important; }', + '.h2-business-status-modal .h2-business-status-record-table .ant-table-body { margin-top: 0 !important; overflow-y: auto !important; }', + '.h2-business-status-modal .h2-business-status-record-table .ant-table-body table { margin-top: 0 !important; }', + '.h2-business-status-modal .h2-business-status-record-table .ant-table-tbody > tr.ant-table-measure-row, .h2-business-status-modal .h2-business-status-record-table .ant-table-tbody > tr.ant-table-measure-row > td { height: 0 !important; max-height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; line-height: 0 !important; font-size: 0 !important; overflow: hidden !important; visibility: hidden !important; }', + '.h2-business-status-modal .h2-business-status-record-table .ant-table-thead > tr > th { background: #f8fafc !important; font-size: 12px !important; font-weight: 700 !important; color: #475569 !important; padding: 10px 12px !important; border-bottom: 1px solid #e2e8f0 !important; }', + '.h2-business-status-modal .h2-business-status-record-table .ant-table-tbody > tr:not(.ant-table-measure-row) > td { font-size: 13px !important; padding: 10px 12px !important; vertical-align: middle !important; border-bottom: 1px solid #f8fafc !important; }', + '.h2-business-status-modal .h2-business-status-record-table .ant-table-tbody > tr:not(.ant-table-measure-row):last-child > td { border-bottom: none !important; }', + '.h2-business-status-modal .h2-business-status-record-table .ant-table-tbody > tr:hover > td { background: #f0fdf4 !important; }', + '.h2-statement-modal .ant-modal-content { border-radius: 16px !important; overflow: hidden; box-shadow: 0 24px 48px -12px rgba(15, 23, 42, 0.18) !important; }', + '.h2-statement-modal .ant-modal-header { padding: 18px 24px 14px !important; border-bottom: 1px solid #f1f5f9 !important; margin-bottom: 0 !important; }', + '.h2-statement-modal .ant-modal-title { font-size: 17px !important; font-weight: 700 !important; color: #0f172a !important; }', + '.h2-statement-modal .ant-modal-body { padding: 16px 24px 20px !important; background: #f8fafc; }', + '.h2-statement-modal .ant-modal-footer { padding: 12px 24px 18px !important; border-top: 1px solid #f1f5f9 !important; background: #fff; }', + '.h2-statement-modal .h2-statement-panel { display: flex; flex-direction: column; gap: 14px; }', + '.h2-statement-modal .h2-statement-station-card { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 12px 16px; background: linear-gradient(135deg, #f5f3ff 0%, #ede9fe 50%, #f8fafc 100%); border: 1px solid #ddd6fe; border-radius: 12px; }', + '.h2-statement-modal .h2-statement-station-card__name { font-size: 15px; font-weight: 700; color: #0f172a; line-height: 1.35; }', + '.h2-statement-modal .h2-statement-station-card__meta { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; white-space: nowrap; }', + '.h2-statement-modal .h2-statement-form-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 14px 16px; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-statement-modal .h2-statement-form-wrap .ant-form-item { margin-bottom: 0; }', + '.h2-statement-modal .h2-statement-form-wrap .ant-form-item-label > label { font-size: 13px; font-weight: 600; color: #334155; }', + '.h2-statement-modal .h2-statement-form-wrap .ant-input { border-radius: 8px !important; }', + '.h2-statement-modal .h2-statement-date-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }', + '@media (max-width: 520px) { .h2-statement-modal .h2-statement-date-grid { grid-template-columns: 1fr; } }', + '.h2-statement-modal .h2-statement-form-hint { margin-top: 10px; padding: 8px 12px; font-size: 12px; color: #64748b; line-height: 1.5; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; }', + '.h2-statement-modal .h2-statement-form-hint strong { color: #5b21b6; font-weight: 700; }', + '.h2-statement-modal .h2-statement-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }', + '@media (max-width: 640px) { .h2-statement-modal .h2-statement-stats { grid-template-columns: 1fr; } }', + '.h2-statement-modal .h2-statement-stat { display: flex; flex-direction: column; justify-content: center; min-height: 78px; padding: 12px 14px; border-radius: 12px; border: 1px solid #e2e8f0; background: #fff; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06); min-width: 0; box-sizing: border-box; }', + '.h2-statement-modal .h2-statement-stat--count { border-left: 4px solid #7c3aed; }', + '.h2-statement-modal .h2-statement-stat--kg { border-left: 4px solid #10b981; background: linear-gradient(180deg, #fff 0%, #f0fdf4 100%); }', + '.h2-statement-modal .h2-statement-stat--cost { border-left: 4px solid #f97316; }', + '.h2-statement-modal .h2-statement-stat__label { font-size: 12px; font-weight: 600; color: #64748b; margin-bottom: 8px; line-height: 1.2; }', + '.h2-statement-modal .h2-statement-stat__value { font-size: 20px; font-weight: 800; font-variant-numeric: tabular-nums; line-height: 1.2; color: #0f172a; word-break: break-all; }', + '.h2-statement-modal .h2-statement-stat__value--count { color: #6d28d9; }', + '.h2-statement-modal .h2-statement-stat__value--kg { color: #059669; }', + '.h2-statement-modal .h2-statement-stat__value--cost { color: #ea580c; }', + '.h2-statement-modal .h2-statement-table-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-statement-modal .h2-statement-table-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 14px; border-bottom: 1px solid #f1f5f9; background: #fafbfc; }', + '.h2-statement-modal .h2-statement-table-head__left { display: flex; align-items: center; gap: 10px; min-width: 0; }', + '.h2-statement-modal .h2-statement-table-head__title { font-size: 13px; font-weight: 700; color: #334155; }', + '.h2-statement-modal .h2-statement-table-head__count { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; }', + '.h2-statement-modal .h2-statement-record-table { border-radius: 0 !important; }', + '.h2-statement-modal .h2-statement-record-table .ant-table { background: transparent !important; }', + '.h2-statement-modal .h2-statement-record-table .ant-table-container { border: none !important; }', + '.h2-statement-modal .h2-statement-record-table .ant-table-header { margin-bottom: 0 !important; }', + '.h2-statement-modal .h2-statement-record-table .ant-table-body { margin-top: 0 !important; overflow-y: auto !important; }', + '.h2-statement-modal .h2-statement-record-table .ant-table-body table { margin-top: 0 !important; }', + '.h2-statement-modal .h2-statement-record-table .ant-table-tbody > tr.ant-table-measure-row, .h2-statement-modal .h2-statement-record-table .ant-table-tbody > tr.ant-table-measure-row > td { height: 0 !important; max-height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; line-height: 0 !important; font-size: 0 !important; overflow: hidden !important; visibility: hidden !important; }', + '.h2-statement-modal .h2-statement-record-table .ant-table-thead > tr > th { background: #f8fafc !important; font-size: 12px !important; font-weight: 700 !important; color: #475569 !important; padding: 10px 12px !important; border-bottom: 1px solid #e2e8f0 !important; }', + '.h2-statement-modal .h2-statement-record-table .ant-table-tbody > tr:not(.ant-table-measure-row) > td { font-size: 13px !important; padding: 10px 12px !important; vertical-align: middle !important; border-bottom: 1px solid #f8fafc !important; }', + '.h2-statement-modal .h2-statement-record-table .ant-table-tbody > tr:not(.ant-table-measure-row):last-child > td { border-bottom: none !important; }', + '.h2-statement-modal .h2-statement-record-table .ant-table-tbody > tr:hover > td { background: #f5f3ff !important; }', + '.h2-statement-modal .h2-statement-last-end { margin-top: 10px; padding: 8px 12px; font-size: 12px; color: #475569; line-height: 1.5; background: #faf5ff; border: 1px solid #e9d5ff; border-radius: 8px; font-variant-numeric: tabular-nums; }', + '.h2-statement-modal .h2-statement-last-end strong { color: #6d28d9; font-weight: 700; }', + '.h2-statement-modal .h2-statement-settlement-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 14px 16px; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); display: flex; flex-direction: column; gap: 14px; }', + '.h2-statement-modal .h2-statement-settlement-wrap .ant-form-item { margin-bottom: 0; }', + '.h2-statement-modal .h2-statement-settlement-wrap .ant-form-item-label > label { font-size: 13px; font-weight: 600; color: #334155; }', + '.h2-statement-modal .h2-statement-receipt-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }', + '@media (max-width: 520px) { .h2-statement-modal .h2-statement-receipt-grid { grid-template-columns: 1fr; } }', + '.h2-statement-modal .h2-statement-receipt-amount-input .ant-input { font-variant-numeric: tabular-nums; }', + '.h2-statement-modal .h2-statement-receipt-amount-input .ant-input-prefix { color: #64748b; font-weight: 600; margin-inline-end: 4px; }', + '.h2-statement-modal .h2-statement-settlement-wrap .ant-picker { width: 100% !important; border-radius: 8px !important; }', + '.h2-statement-modal .h2-statement-invoice-upload .h2-contract-upload-actions { gap: 0; }', + '.h2-statement-modal .h2-statement-invoice-upload .h2-statement-upload-btn.ant-btn { display: inline-flex !important; align-items: center; gap: 6px; height: 32px !important; padding: 0 14px !important; border-radius: 8px !important; font-size: 13px !important; font-weight: 600 !important; border: 1px solid #e2e8f0 !important; color: #475569 !important; background: #fff !important; box-shadow: none !important; }', + '.h2-statement-modal .h2-statement-invoice-upload .h2-statement-upload-btn.ant-btn:hover { border-color: #10b981 !important; color: #059669 !important; background: #f0fdf4 !important; }', + '.h2-statement-modal .h2-statement-invoice-upload .h2-statement-upload-btn.ant-btn:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; }', + '.h2-recharge-modal .ant-modal-content { border-radius: 16px !important; overflow: hidden; box-shadow: 0 24px 48px -12px rgba(15, 23, 42, 0.18) !important; }', + '.h2-recharge-modal .ant-modal-header { padding: 18px 24px 14px !important; border-bottom: 1px solid #f1f5f9 !important; margin-bottom: 0 !important; }', + '.h2-recharge-modal .ant-modal-title { font-size: 17px !important; font-weight: 700 !important; color: #0f172a !important; }', + '.h2-recharge-modal .ant-modal-body { padding: 16px 24px 20px !important; background: #f8fafc; }', + '.h2-recharge-modal .ant-modal-footer { padding: 12px 24px 18px !important; border-top: 1px solid #f1f5f9 !important; background: #fff; }', + '.h2-recharge-modal .h2-recharge-panel { display: flex; flex-direction: column; gap: 14px; }', + '.h2-recharge-modal .h2-recharge-hint { margin: 0; padding: 10px 12px; font-size: 12px; color: #64748b; line-height: 1.55; background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; }', + '.h2-recharge-modal .h2-recharge-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }', + '.h2-recharge-modal .h2-recharge-toolbar__count { font-size: 13px; color: #64748b; font-variant-numeric: tabular-nums; }', + '.h2-recharge-modal .h2-recharge-table-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-recharge-modal .h2-recharge-record-table .ant-table-thead > tr > th { background: #f8fafc !important; font-size: 12px !important; font-weight: 700 !important; color: #475569 !important; padding: 10px 12px !important; }', + '.h2-recharge-modal .h2-recharge-record-table .ant-table-tbody > tr > td { font-size: 13px !important; padding: 8px 10px !important; vertical-align: middle !important; }', + '.h2-recharge-modal .h2-recharge-readonly { font-size: 13px; color: #334155; line-height: 1.45; word-break: break-all; }', + '.h2-recharge-modal .h2-recharge-readonly--muted { color: #94a3b8; }', + '.h2-recharge-modal .h2-recharge-readonly-input.ant-input[disabled] { background: #f8fafc !important; color: #334155 !important; cursor: default !important; border: 1px solid #e2e8f0 !important; }', + '.h2-alert-station-modal .ant-modal-content { border-radius: 16px !important; overflow: hidden; box-shadow: 0 24px 48px -12px rgba(15, 23, 42, 0.18) !important; }', + '.h2-alert-station-modal .ant-modal-header { padding: 18px 24px 14px !important; border-bottom: 1px solid #f1f5f9 !important; margin-bottom: 0 !important; }', + '.h2-alert-station-modal .ant-modal-title { font-size: 17px !important; font-weight: 700 !important; color: #0f172a !important; }', + '.h2-alert-station-modal .ant-modal-body { padding: 16px 24px 20px !important; background: #f8fafc; }', + '.h2-alert-station-modal .ant-modal-footer { padding: 12px 24px 18px !important; border-top: 1px solid #f1f5f9 !important; background: #fff; }', + '.h2-alert-station-modal .h2-alert-station-panel { display: flex; flex-direction: column; gap: 14px; }', + '.h2-alert-station-modal .h2-alert-station-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 12px 16px; background: linear-gradient(135deg, #fff7ed 0%, #fffbeb 50%, #f8fafc 100%); border: 1px solid #fed7aa; border-radius: 12px; }', + '.h2-alert-station-modal .h2-alert-station-head__title { font-size: 14px; font-weight: 700; color: #0f172a; }', + '.h2-alert-station-modal .h2-alert-station-head__count { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; }', + '.h2-alert-station-modal .h2-alert-station-table-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-statement-modal .h2-statement-detail-receipt-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 14px 16px; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-statement-modal .h2-statement-detail-receipt-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 20px; }', + '@media (max-width: 520px) { .h2-statement-modal .h2-statement-detail-receipt-grid { grid-template-columns: 1fr; } }', + '.h2-statement-modal .h2-statement-detail-receipt-item__label { font-size: 12px; font-weight: 600; color: #64748b; margin-bottom: 6px; }', + '.h2-statement-modal .h2-statement-detail-receipt-item__value { font-size: 14px; font-weight: 700; color: #0f172a; font-variant-numeric: tabular-nums; }', + '.h2-statement-modal .h2-statement-detail-receipt-item__value--amount { color: #ea580c; }', + '.h2-statement-modal .h2-statement-detail-receipt-files { margin-top: 12px; padding-top: 12px; border-top: 1px solid #f1f5f9; }', + '.h2-statement-modal .h2-statement-detail-receipt-file-list { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; }', + '.h2-statement-modal .h2-statement-detail-receipt-file-item { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 12px; border-radius: 8px; background: #f8fafc; border: 1px solid #e2e8f0; }', + '.h2-statement-modal .h2-statement-detail-receipt-file-name { font-size: 13px; color: #334155; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }', + '.h2-statement-modal .h2-statement-balance-readonly { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-height: 32px; padding: 6px 12px; border-radius: 8px; background: #f8fafc; border: 1px solid #e2e8f0; }', + '.h2-statement-modal .h2-statement-balance-readonly__amount { font-size: 16px; font-weight: 800; font-variant-numeric: tabular-nums; }', + '.h2-statement-modal .h2-statement-balance-readonly__amount--positive { color: #059669; }', + '.h2-statement-modal .h2-statement-balance-readonly__amount--negative { color: #dc2626; }', + '.h2-statement-history-modal .ant-modal-content { border-radius: 16px !important; overflow: hidden; box-shadow: 0 24px 48px -12px rgba(15, 23, 42, 0.18) !important; }', + '.h2-statement-history-modal .ant-modal-header { padding: 18px 24px 14px !important; border-bottom: 1px solid #f1f5f9 !important; margin-bottom: 0 !important; }', + '.h2-statement-history-modal .ant-modal-title { font-size: 17px !important; font-weight: 700 !important; color: #0f172a !important; }', + '.h2-statement-history-modal .ant-modal-body { padding: 16px 24px 20px !important; background: #f8fafc; }', + '.h2-statement-history-modal .ant-modal-footer { padding: 12px 24px 18px !important; border-top: 1px solid #f1f5f9 !important; background: #fff; }', + '.h2-statement-history-modal .h2-statement-history-panel { display: flex; flex-direction: column; gap: 14px; }', + '.h2-statement-history-modal .h2-statement-history-station-card { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 12px 16px; background: linear-gradient(135deg, #f5f3ff 0%, #ede9fe 50%, #f8fafc 100%); border: 1px solid #ddd6fe; border-radius: 12px; }', + '.h2-statement-history-modal .h2-statement-history-station-card__name { font-size: 15px; font-weight: 700; color: #0f172a; line-height: 1.35; }', + '.h2-statement-history-modal .h2-statement-history-station-card__meta { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; white-space: nowrap; }', + '.h2-statement-history-modal .h2-statement-history-table-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-statement-history-modal .h2-statement-history-table-wrap .ant-table-thead > tr > th { background: #f8fafc !important; font-size: 12px !important; font-weight: 700 !important; color: #475569 !important; }', + '.h2-statement-history-modal .h2-statement-history-table-wrap .ant-table-tbody > tr:hover > td { background: #f5f3ff !important; }', + '.h2-statement-history-modal .h2-statement-history-balance { font-weight: 700; font-variant-numeric: tabular-nums; }', + '.h2-statement-history-modal .h2-statement-history-balance--negative { color: #dc2626; }', + '.h2-statement-history-modal .h2-statement-history-balance--positive { color: #059669; }', + '.h2-station-page .h2-ledger-totals-bar__value--income { color: #059669 !important; }', + '.h2-station-page .h2-ledger-totals-bar__value--expense { color: #ea580c !important; }', + '.h2-station-page .h2-ledger-totals-bar__value--negative { color: #dc2626 !important; }', + '.h2-station-page.h2-station-page--create { padding: 0 0 96px; height: auto; min-height: 100dvh; overflow: auto; }', + '.h2-station-page--create .h2-create-shell { width: 100%; max-width: none; margin: 0; padding: 24px 24px 0; box-sizing: border-box; }', + '.h2-station-page.h2-station-page--edit { padding: 0 0 96px; height: auto; min-height: 100dvh; overflow: auto; }', + '.h2-station-page--edit .h2-create-shell { width: 100%; max-width: none; margin: 0; padding: 24px 24px 0; box-sizing: border-box; }', + '.h2-account-bind-readonly-wrap { padding: 12px 14px; border-radius: 10px; background: #f8fafc; border: 1px solid #e2e8f0; }', + '.h2-account-bind-readonly-item { padding: 10px 12px; border-radius: 8px; background: #fff; border: 1px solid #e2e8f0; margin-bottom: 8px; }', + '.h2-account-bind-readonly-item:last-child { margin-bottom: 0; }', + '.h2-account-bind-readonly-item__name { font-size: 14px; font-weight: 700; color: #0f172a; margin-bottom: 4px; }', + '.h2-account-bind-readonly-item__meta { font-size: 12px; color: #64748b; }', + '.h2-station-page--create .h2-create-pagehead { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; margin-bottom: 16px; }', + '.h2-station-page--create .h2-create-pagehead-left { display: flex; align-items: center; gap: 12px; min-width: 0; }', + '.h2-station-page--create .h2-create-pagehead-title { margin: 0; font-size: 18px; font-weight: 700; color: #0f172a; line-height: 1.35; }', + '.h2-station-page--create .h2-create-pagehead-desc { margin: 2px 0 0; font-size: 13px; color: #64748b; line-height: 1.45; }', + '.h2-station-page--create .h2-create-card.ant-card { margin-bottom: 16px !important; }', + '.h2-station-page--create .h2-create-form--grid .h2-create-form-grid { width: 100%; display: flex; flex-direction: column; gap: var(--h2-form-row-gap); }', + '.h2-station-page--create .h2-create-form--grid .h2-create-form-grid > .ant-row { margin-bottom: 0 !important; }', + '.h2-station-page--create .h2-create-form--grid .ant-form-item-label { padding: 0 0 var(--h2-form-label-gap) !important; min-height: 22px; }', + '.h2-station-page--create .h2-create-form--grid .ant-form-item-label > label { display: inline-flex; align-items: center; min-height: 22px; line-height: 22px; font-size: 13px; font-weight: 500; color: #475569; height: auto; }', + '.h2-station-page--create .h2-create-form--grid .ant-row { width: 100% !important; margin-left: 0 !important; margin-right: 0 !important; }', + '.h2-station-page--create .h2-create-form--grid .ant-col { min-width: 0; }', + '.h2-station-page--create .h2-create-form--grid .ant-form-item { margin-bottom: 0; width: 100%; }', + '.h2-station-page--create .h2-create-form--grid .ant-col > .ant-form-item { display: flex; flex-direction: column; }', + '.h2-station-page--create .h2-create-form--grid .ant-form-item-control { flex: 1; width: 100%; }', + '.h2-station-page--create .h2-create-form--grid .ant-form-item-control-input, .h2-station-page--create .h2-create-form--grid .ant-form-item-control-input-content { width: 100%; max-width: 100%; }', + '.h2-station-page--create .h2-create-form--grid .h2-create-input, .h2-station-page--create .h2-create-form--grid .ant-select, .h2-station-page--create .h2-create-form--grid .ant-cascader, .h2-station-page--create .h2-create-form--grid .ant-picker { width: 100% !important; max-width: 100% !important; }', + '.h2-station-page--create .h2-create-form--grid .h2-address-paste, .h2-station-page--create .h2-create-form--grid .h2-contract-panel, .h2-station-page--create .h2-create-form--grid .h2-create-upload { width: 100%; }', + '.h2-station-page--create .h2-create-subsection-title { margin: 4px 0 0; padding: 12px 0 10px; font-size: 13px; font-weight: 600; color: #475569; border-bottom: 1px solid #f1f5f9; width: 100%; box-sizing: border-box; }', + '.h2-station-page--create .h2-supplier-mode-picker-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; width: 100%; }', + '.h2-station-page--create .h2-supplier-mode-picker-row .h2-supplier-mode-card { height: 100%; min-height: 88px; }', + '@media (max-width: 768px) { .h2-station-page--create .h2-supplier-mode-picker-row { grid-template-columns: 1fr; gap: 12px; } }', + '.h2-station-page--create .h2-create-panel { padding: 16px 18px; border-radius: 12px; background: #f8fafc; border: 1px solid #e8ecf0; }', + '.h2-station-page--create .h2-create-panel + .h2-create-panel, .h2-station-page--create .h2-create-panel + .ant-row, .h2-station-page--create .ant-row + .h2-create-panel { margin-top: 16px; }', + '.h2-station-page--create .h2-create-panel-head { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; font-size: 13px; font-weight: 700; color: #334155; }', + '.h2-station-page--create .h2-create-panel-head::before { content: ""; width: 4px; height: 14px; border-radius: 2px; background: linear-gradient(180deg, #10b981, #6ee7b7); flex-shrink: 0; }', + '.h2-station-page--create .h2-create-panel--address .h2-region-address--inline { gap: 12px; }', + '.h2-station-page--create .h2-create-panel--address .h2-region-address--inline .h2-region-address-cascader { flex: 0 0 42%; max-width: 280px; }', + '.h2-station-page--create .h2-create-panel--address .h2-address-paste { margin-top: 12px; padding-top: 12px; border-top: 1px dashed #e2e8f0; }', + '.h2-station-page--create .h2-station-map-picker { display: flex; flex-direction: column; gap: 12px; width: 100%; }', + '.h2-station-page--create .h2-station-map-picker__toolbar { display: flex; align-items: center; gap: 8px; width: 100%; }', + '.h2-station-page--create .h2-station-map-picker__toolbar .ant-input { flex: 1; min-width: 0; }', + '.h2-station-page--create .h2-station-map-picker__toolbar .ant-btn { flex-shrink: 0; min-height: 32px; border-radius: 8px; font-weight: 600; }', + '.h2-station-page--create .h2-station-map-picker__map { position: relative; width: 100%; height: 280px; border-radius: 10px; border: 1px solid #e2e8f0; overflow: hidden; background: #f1f5f9; }', + '.h2-station-page--create .h2-station-map-picker__map .leaflet-container { height: 100%; width: 100%; font-family: inherit; z-index: 0; }', + '.h2-station-page--create .h2-station-map-picker__coords { display: flex; flex-wrap: wrap; gap: 8px 20px; font-size: 12px; color: #64748b; line-height: 1.5; }', + '.h2-station-page--create .h2-station-map-picker__coords-item { display: inline-flex; align-items: center; gap: 6px; }', + '.h2-station-page--create .h2-station-map-picker__coords-item strong { color: #0f172a; font-weight: 700; font-variant-numeric: tabular-nums; }', + '.h2-station-page--create .h2-station-map-picker__hint { font-size: 12px; color: #94a3b8; line-height: 1.45; }', + '.h2-station-page--create .h2-create-panel--contract .h2-create-radio-group { padding: 10px 14px; background: #fff; border-radius: 10px; border: 1px solid #e2e8f0; }', + '.h2-station-page--create .h2-create-contract-fields { display: grid; grid-template-columns: 1fr; gap: 16px; margin-top: 14px; }', + '@media (min-width: 640px) { .h2-station-page--create .h2-create-contract-fields { grid-template-columns: 1fr 1fr; } }', + '.h2-station-page--create .h2-supplier-link-panel { padding: 16px 18px; border-radius: 12px; background: linear-gradient(135deg, #eff6ff 0%, #f8fafc 100%); border: 1px solid #bfdbfe; }', + '.h2-station-page--create .h2-account-bind-layout { display: grid; grid-template-columns: minmax(280px, 1fr) minmax(0, 1.65fr); gap: 24px; align-items: start; width: 100%; }', + '@media (max-width: 960px) { .h2-station-page--create .h2-account-bind-layout { grid-template-columns: 1fr; } }', + '.h2-station-page--create .h2-account-bind-side { display: flex; flex-direction: column; gap: 12px; min-width: 0; }', + '.h2-station-page--create .h2-account-bind-side .ant-form-item { margin-bottom: 0 !important; }', + '.h2-station-page--create .h2-account-bind-side__hint { margin: 0; font-size: 12px; color: #94a3b8; line-height: 1.55; }', + '.h2-station-page--create .h2-account-bind-selected-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }', + '.h2-station-page--create .h2-account-bind-selected-label { font-size: 13px; color: #64748b; }', + '.h2-station-page--create .h2-account-bind-selected-count.ant-tag { margin: 0 !important; border-radius: 6px !important; font-weight: 600 !important; line-height: 20px !important; padding: 0 8px !important; }', + '.h2-station-page--create .h2-account-bind-preview-area { min-width: 0; padding: 14px 16px; border-radius: 12px; background: #f8fafc; border: 1px solid #e8ecf0; min-height: 180px; box-sizing: border-box; }', + '.h2-station-page--create .h2-account-bind-preview-title { font-size: 14px; font-weight: 600; color: #334155; margin-bottom: 12px; }', + '.h2-station-page--create .h2-account-bind-preview-cards { display: flex; flex-wrap: wrap; gap: 12px; }', + '.h2-station-page--create .h2-account-bind-preview-empty { font-size: 13px; color: #94a3b8; line-height: 1.5; padding: 24px 8px; text-align: center; }', + '.h2-station-page--create .h2-account-bind-user-card { position: relative; display: flex; align-items: flex-start; gap: 12px; padding: 12px 34px 12px 12px; width: 220px; max-width: 100%; border: 1px solid #e2e8f0; border-radius: 10px; background: #fff; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); box-sizing: border-box; }', + '.h2-station-page--create .h2-account-bind-user-card__avatar { flex-shrink: 0; width: 40px; height: 40px; border-radius: 50%; background: linear-gradient(145deg, #10b981 0%, #059669 100%); color: #fff; display: inline-flex; align-items: center; justify-content: center; }', + '.h2-station-page--create .h2-account-bind-user-card__body { flex: 1; min-width: 0; }', + '.h2-station-page--create .h2-account-bind-user-card__name-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }', + '.h2-station-page--create .h2-account-bind-user-card__name { font-size: 14px; font-weight: 700; color: #0f172a; line-height: 1.3; }', + '.h2-station-page--create .h2-account-bind-user-card__role.ant-tag { margin: 0 !important; border-radius: 4px !important; font-size: 11px !important; font-weight: 600 !important; line-height: 18px !important; padding: 0 6px !important; }', + '.h2-station-page--create .h2-account-bind-user-card__meta { margin-top: 6px; font-size: 12px; color: #94a3b8; line-height: 1.5; }', + '.h2-station-page--create .h2-account-bind-user-card__meta + .h2-account-bind-user-card__meta { margin-top: 2px; }', + '.h2-station-page--create .h2-account-bind-user-card__remove { position: absolute; top: 8px; right: 8px; width: 20px; height: 20px; border: none; background: transparent; color: #94a3b8; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; border-radius: 4px; padding: 0; font-size: 14px; line-height: 1; }', + '.h2-station-page--create .h2-account-bind-user-card__remove:hover { color: #64748b; background: #f1f5f9; }', + '.h2-station-page--create .h2-account-bind-user-card__remove:focus-visible { outline: 2px solid #10b981; outline-offset: 1px; }', + '.h2-station-page--create .h2-account-bind-hint { margin: 0; font-size: 12px; color: #64748b; line-height: 1.55; }', + '.h2-station-page--create .h2-create-readonly-input.ant-input[disabled] { background: #f8fafc !important; color: #64748b !important; cursor: default !important; border: 1px dashed #e2e8f0 !important; }', + '.h2-station-page--create .h2-station-type-field { display: flex; flex-direction: column; gap: 8px; width: 100%; }', + '.h2-station-page--create .h2-supplier-tabs { width: 100%; }', + '.h2-station-page--create .h2-supplier-tabs__nav { display: flex; align-items: flex-end; gap: 32px; border-bottom: 1px solid #e2e8f0; margin-bottom: var(--h2-form-row-gap); }', + '.h2-station-page--create .h2-supplier-tabs__item { position: relative; border: none; background: transparent; padding: 0 0 12px; font-size: 14px; font-weight: 500; color: #64748b; cursor: pointer; line-height: 1.4; }', + '.h2-station-page--create .h2-supplier-tabs__item:hover { color: #059669; }', + '.h2-station-page--create .h2-supplier-tabs__item--active { color: #059669; font-weight: 600; }', + '.h2-station-page--create .h2-supplier-tabs__item--active::after { content: ""; position: absolute; left: 0; right: 0; bottom: -1px; height: 2px; background: #10b981; border-radius: 1px 1px 0 0; }', + '.h2-station-page--create .h2-supplier-tabs__panel { width: 100%; }', + '.h2-station-page--create .h2-supplier-linked-preview { margin-top: var(--h2-form-row-gap); }', + '.h2-station-page--create .h2-supplier-none-hint { margin: 0; padding: 20px 16px; text-align: center; font-size: 13px; color: #94a3b8; line-height: 1.55; background: #f8fafc; border: 1px dashed #e2e8f0; border-radius: 10px; }', + '.h2-station-page--create .h2-create-upload--readonly.ant-upload-wrapper .ant-upload-drag { cursor: default !important; pointer-events: none; opacity: 0.92; }', + '.h2-station-page--create .h2-supplier-new-stack { display: flex; flex-direction: column; gap: 16px; }', + '.h2-station-page--create .h2-create-form--visual .ant-form-item-label > label { font-weight: 500; color: #334155; }', + '.h2-station-page--create .h2-create-form--visual .h2-create-input.ant-input:not(.ant-input-disabled), .h2-station-page--create .h2-create-form--visual .ant-select:not(.ant-select-disabled) .ant-select-selector, .h2-station-page--create .h2-create-form--visual .ant-cascader:not(.ant-select-disabled) .ant-select-selector, .h2-station-page--create .h2-create-form--visual .h2-create-input.ant-picker:not(.ant-picker-disabled) { border-radius: 10px !important; height: 40px !important; min-height: 40px !important; border-color: #e2e8f0 !important; }', + '.h2-station-page--create .h2-create-form--visual .ant-input { padding: 8px 14px !important; }', + '.h2-station-page--create .h2-create-form--visual .ant-select-selection-item { line-height: 38px !important; }', + '.h2-station-page--create .h2-create-form--visual .ant-input-lg { height: 44px !important; font-size: 15px !important; border-radius: 10px !important; }', + '.h2-station-page--create .h2-create-panel .ant-form-item + .ant-form-item, .h2-station-page--create .h2-create-panel .ant-form-item + .ant-row, .h2-station-page--create .h2-create-panel .ant-row + .ant-form-item, .h2-station-page--create .h2-create-panel .ant-form-item + .h2-address-paste, .h2-station-page--create .h2-create-panel .ant-row + .h2-address-paste { margin-top: 12px; }', + '.h2-station-page--create .h2-create-form--visual .h2-create-radio-group { min-height: 40px; }', + '.h2-station-page--create .h2-create-form--visual .ant-form-item-control-input { max-width: 100%; }', + '.h2-station-page--create .h2-create-back-only, .h2-station-page--edit .h2-create-back-only { display: flex; align-items: center; margin-bottom: 16px; }', + '.h2-station-page--create .h2-create-back-btn, .h2-station-page--edit .h2-create-back-btn { display: inline-flex !important; align-items: center; gap: 6px; height: 32px; padding: 0 12px !important; border-radius: 8px !important; font-weight: 500; color: #475569 !important; border: 1px solid #e2e8f0 !important; background: #fff !important; }', + '.h2-station-page--create .h2-create-back-btn:hover, .h2-station-page--edit .h2-create-back-btn:hover { color: #059669 !important; border-color: #10b981 !important; background: #f0fdf4 !important; }', + '.h2-station-page--create .h2-create-back-btn:focus-visible, .h2-station-page--edit .h2-create-back-btn:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; }', + '.h2-station-page .h2-create-card.ant-card { border-radius: 16px !important; border: 1px solid #e2e8f0 !important; box-shadow: 0 4px 24px -6px rgba(15, 23, 42, 0.06) !important; margin-bottom: 0 !important; transition: box-shadow 0.2s ease, border-color 0.2s ease, transform 0.2s ease; }', + '.h2-station-page--create .h2-create-card.ant-card:hover { box-shadow: 0 4px 24px -6px rgba(15, 23, 42, 0.06) !important; transform: none; }', + '.h2-station-page .h2-create-card > .ant-card-head { border-bottom: 1px solid #f1f5f9 !important; min-height: auto; padding: 16px 22px !important; background: linear-gradient(180deg, #fafbfc 0%, #fff 100%); }', + '.h2-station-page .h2-create-card > .ant-card-head .ant-card-head-title { font-size: 15px !important; font-weight: 700 !important; color: #0f172a !important; padding: 0 !important; }', + '.h2-station-page .h2-create-card > .ant-card-body { padding: 20px 24px 24px !important; display: flex; flex-direction: column; gap: 16px; }', + '@media (max-width: 640px) { .h2-station-page--create .h2-create-card > .ant-card-body { padding: 16px 16px 20px !important; } }', + '.h2-station-page .h2-card-title-bar { display: inline-flex; align-items: center; gap: 10px; }', + '.h2-station-page .h2-card-title-bar::before { content: ""; width: 3px; height: 16px; border-radius: 2px; background: linear-gradient(180deg, #10b981, #34d399); flex-shrink: 0; }', + '.h2-station-page .h2-card-title-icon { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 10px; background: #ecfdf5; color: #059669; flex-shrink: 0; }', + '.h2-station-page .h2-form-section { margin-bottom: 0; }', + '.h2-station-page .h2-form-section + .h2-form-section { margin-top: var(--h2-form-row-gap); }', + '.h2-station-page .h2-form-section-body { display: flex; flex-direction: column; gap: var(--h2-form-row-gap); }', + '.h2-station-page .h2-form-section-body > .ant-row { margin: 0 !important; align-items: flex-start; }', + '.h2-station-page .h2-form-section-body .ant-form-item { margin-bottom: 0 !important; width: 100%; }', + '.h2-station-page .h2-form-section-body .ant-col { display: flex; flex-direction: column; min-width: 0; }', + '.h2-station-page .h2-form-section-body .ant-col > .ant-form-item { flex: 1; min-width: 0; }', + '.h2-station-page--create .h2-create-form .h2-form-section-body .h2-supplier-mode-card { height: 100%; box-sizing: border-box; }', + '.h2-station-page .h2-contract-panel { padding: 14px 16px; border-radius: 10px; background: linear-gradient(180deg, #fafbfc 0%, #fff 100%); border: 1px solid #e8ecf0; }', + '.h2-station-page--create .h2-create-topbar-main { display: flex; align-items: center; gap: 14px; min-width: 0; }', + '.h2-station-page--create .h2-create-page-title { min-width: 0; }', + '.h2-station-page--create .h2-create-page-title h1 { margin: 0; font-size: 16px; font-weight: 700; color: #0f172a; line-height: 1.35; }', + '.h2-station-page--create .h2-create-page-title p { margin: 2px 0 0; font-size: 12px; color: #94a3b8; line-height: 1.4; }', + '.h2-station-page--create .h2-create-form .ant-picker-range { width: 100% !important; max-width: 100%; }', + '.h2-station-page--create .h2-create-form .ant-picker-range .ant-picker-input > input { font-size: 13px !important; }', + '.h2-station-page .h2-supplier-mode-card-body { flex: 1; min-width: 0; }', + '.h2-station-page--create .h2-create-form .ant-row { width: 100%; }', + '.h2-station-page--create .h2-create-form .ant-col-6 { min-width: 0; }', + '.h2-station-page--create .h2-create-form .ant-form-item { margin-bottom: 0; }', + '.h2-station-page--create .h2-create-form .ant-form-item-label { padding: 0 0 var(--h2-form-label-gap); min-height: 22px; }', + '.h2-station-page--create .h2-create-form .ant-form-item-label > label { display: inline-flex; align-items: center; min-height: 22px; line-height: 22px; font-size: 14px; font-weight: 400; color: #1d2129; height: auto; }', + '.h2-station-page--create .h2-create-form .ant-form-item-extra { margin-top: 6px; font-size: 12px; line-height: 1.45; }', + '.h2-station-page--create .h2-create-form .ant-form-item-label > label::after { display: none !important; }', + '.h2-station-page--create .h2-create-input.ant-input:not(.ant-input-disabled):not(:disabled), .h2-station-page--create .h2-create-form .ant-select:not(.ant-select-disabled) .ant-select-selector, .h2-station-page--create .h2-create-form .ant-cascader:not(.ant-select-disabled) .ant-select-selector, .h2-station-page--create .h2-create-form .h2-create-input.ant-picker:not(.ant-picker-disabled) { height: 32px !important; min-height: 32px !important; border-radius: 2px !important; font-size: 14px !important; border: 1px solid #e5e6eb !important; background: #fff !important; color: #1d2129 !important; }', + '.h2-station-page--create .h2-create-form .h2-create-input.ant-picker:not(.ant-picker-disabled):hover { border-color: #c9cdd4 !important; background: #fff !important; }', + '.h2-station-page--create .h2-create-form .h2-create-input.ant-picker-focused { border-color: #10b981 !important; box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2) !important; background: #fff !important; }', + '.h2-station-page .h2-supplier-mode-card { font-family: inherit; width: 100%; box-sizing: border-box; }', + '.h2-station-page--create .h2-create-form .ant-input { padding: 4px 12px !important; }', + '.h2-station-page--create .h2-create-form .ant-input-textarea:not(.ant-input-disabled):not(:disabled), .h2-station-page--create .h2-create-form .h2-address-paste .ant-input-textarea:not(.ant-input-disabled) { height: auto !important; min-height: auto !important; border-radius: 2px !important; background: #fff !important; color: #1d2129 !important; border: 1px solid #e5e6eb !important; }', + '.h2-station-page--create .h2-create-form .ant-input:not(:disabled):not(.ant-input-disabled):hover, .h2-station-page--create .h2-create-form .ant-select:not(.ant-select-disabled):hover .ant-select-selector, .h2-station-page--create .h2-create-form .ant-cascader:not(.ant-select-disabled):hover .ant-select-selector { background: #fff !important; border-color: #c9cdd4 !important; }', + '.h2-station-page--create .h2-create-form .ant-input:focus, .h2-station-page--create .h2-create-form .ant-input-focused, .h2-station-page--create .h2-create-form .ant-select-focused .ant-select-selector, .h2-station-page--create .h2-create-form .ant-cascader-focused .ant-select-selector { background: #fff !important; border-color: #10b981 !important; box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2) !important; }', + '.h2-station-page--create .h2-create-form .ant-input-disabled, .h2-station-page--create .h2-create-form .ant-input:disabled, .h2-station-page--create .h2-create-form .ant-select-disabled .ant-select-selector, .h2-station-page--create .h2-create-form .ant-cascader.ant-select-disabled .ant-select-selector { background: #f2f3f5 !important; color: rgba(0, 0, 0, 0.25) !important; cursor: not-allowed !important; }', + '.h2-station-page--create .h2-create-form .ant-select-selection-item { line-height: 30px !important; }', + '.h2-station-page--create .h2-signed-toggle { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; padding: 14px 16px; border-radius: 12px; background: #f8fafc; border: 1px solid #e2e8f0; margin-bottom: 4px; }', + '.h2-station-page--create .h2-signed-toggle-label { font-size: 13px; font-weight: 600; color: #334155; }', + '.h2-station-page--create .h2-signed-panel { display: grid; grid-template-rows: 0fr; opacity: 0; transition: grid-template-rows 0.28s ease-out, opacity 0.22s ease-out; }', + '.h2-station-page--create .h2-signed-panel--open { grid-template-rows: 1fr; opacity: 1; }', + '.h2-station-page--create .h2-signed-panel-inner { overflow: hidden; min-height: 0; }', + '.h2-station-page--create .h2-signed-panel-content { padding-top: 12px; }', + '.h2-station-page--create .h2-create-upload.ant-upload-wrapper .ant-upload-drag { border-radius: 12px !important; border: 1px dashed #cbd5e1 !important; background: #fafbfc !important; padding: 16px 12px !important; transition: border-color 0.2s ease, background 0.2s ease; }', + '.h2-station-page--create .h2-create-upload.ant-upload-wrapper .ant-upload-drag:hover { border-color: #10b981 !important; background: #f0fdf4 !important; }', + '.h2-station-page--create .h2-create-upload-hint { font-size: 12px; color: #64748b; margin-top: 4px; }', + '.h2-station-page .h2-supplier-mode-bar { margin-bottom: 0; }', + '.h2-station-page .h2-supplier-mode-card { position: relative; display: flex; align-items: flex-start; gap: 12px; width: 100%; padding: 14px 16px; min-height: 72px; border-radius: 12px; border: 2px solid #e2e8f0; background: #fff; cursor: pointer; text-align: left; transition: border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease; }', + '.h2-station-page .h2-supplier-mode-card:hover { border-color: #86efac; background: #fafffe; }', + '.h2-station-page .h2-supplier-mode-card:active { transform: scale(0.99); }', + '.h2-station-page .h2-supplier-mode-card:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; }', + '.h2-station-page .h2-supplier-mode-card--active { border-color: #10b981; background: linear-gradient(135deg, #ecfdf5 0%, #fff 70%); box-shadow: 0 4px 16px -4px rgba(16, 185, 129, 0.25); }', + '.h2-station-page .h2-supplier-mode-card-icon { flex-shrink: 0; width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; background: #f1f5f9; color: #64748b; transition: background 0.2s ease, color 0.2s ease; }', + '.h2-station-page .h2-supplier-mode-card--active .h2-supplier-mode-card-icon { background: #d1fae5; color: #047857; }', + '.h2-station-page .h2-supplier-mode-card-title { font-size: 14px; font-weight: 700; color: #0f172a; line-height: 1.3; }', + '.h2-station-page .h2-supplier-mode-card-desc { font-size: 12px; color: #64748b; margin-top: 4px; line-height: 1.45; }', + '.h2-station-page .h2-supplier-mode-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 10px 14px; margin-bottom: 16px; padding: 10px 14px; border-radius: 10px; background: #f8fafc; border: 1px solid #e2e8f0; }', + '.h2-station-page .h2-supplier-type-tag { margin: 0 !important; border-radius: 6px !important; font-weight: 600 !important; }', + '.h2-station-page .h2-supplier-link-select { margin-bottom: 16px; padding: 14px 16px; border-radius: 12px; background: linear-gradient(135deg, #eff6ff 0%, #f8fafc 100%); border: 1px solid #bfdbfe; }', + '.h2-station-page .h2-create-footer { position: fixed; bottom: 0; left: 0; right: 0; z-index: 100; background: rgba(255, 255, 255, 0.92); backdrop-filter: blur(10px); border-top: 1px solid #e2e8f0; box-shadow: 0 -8px 24px rgba(15, 23, 42, 0.06); }', + '.h2-station-page .h2-create-footer-inner { width: 100%; max-width: none; margin: 0; padding: 14px 24px; display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; box-sizing: border-box; }', + '.h2-station-page .h2-create-footer-hint { font-size: 13px; color: #64748b; display: flex; align-items: center; gap: 10px; min-width: 0; }', + '.h2-station-page .h2-create-footer-progress { flex: 1; min-width: 120px; max-width: 200px; }', + '.h2-station-page .h2-create-footer-progress .ant-progress-text { font-size: 12px !important; font-weight: 700; color: #059669 !important; }', + '.h2-station-page .h2-create-footer-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-left: auto; }', + '.h2-station-page .h2-create-footer-actions .ant-btn { min-height: 44px; padding: 0 20px; border-radius: 10px; font-weight: 600; }', + '.h2-station-page .h2-create-footer-actions .ant-btn-primary { min-width: 120px; box-shadow: 0 4px 14px -2px rgba(16, 185, 129, 0.45); }', + '.h2-station-page .h2-create-footer-actions .ant-btn-primary:not(:disabled):hover { box-shadow: 0 6px 18px -2px rgba(16, 185, 129, 0.5); transform: translateY(-1px); }', + '.h2-station-page .h2-field-readonly .ant-input, .h2-station-page .h2-field-readonly .ant-select-selector, .h2-station-page .h2-field-readonly .ant-input-textarea { background: #f1f5f9 !important; color: #64748b !important; cursor: default !important; border-style: dashed !important; }', + '@media (prefers-reduced-motion: no-preference) { .h2-station-page--create .h2-create-card { animation: h2CreateCardIn 0.35s ease-out backwards; } .h2-station-page--create .h2-create-card + .h2-create-card { animation-delay: 0.06s; } }', + '@keyframes h2CreateCardIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }', + '@media (prefers-reduced-motion: reduce) { .h2-station-page--create .h2-create-card { animation: none; } .h2-station-page--create .h2-signed-panel { transition: none; } }', + '.h2-station-page--create .h2-create-form--list .ant-input:not(.ant-input-disabled):not(:disabled), .h2-station-page--create .h2-create-form--list .ant-input-affix-wrapper:not(.ant-input-affix-wrapper-disabled), .h2-station-page--create .h2-create-form--list .ant-select:not(.ant-select-disabled) .ant-select-selector, .h2-station-page--create .h2-create-form--list .ant-cascader:not(.ant-select-disabled) .ant-select-selector, .h2-station-page--create .h2-create-form--list .ant-picker:not(.ant-picker-disabled) { border-radius: 8px !important; border: 1px solid #e2e8f0 !important; min-height: 32px !important; height: 32px !important; font-size: 13px !important; background: #fff !important; color: #0f172a !important; box-shadow: none !important; }', + '.h2-station-page--create .h2-create-form--list .ant-input-textarea:not(.ant-input-disabled) { border-radius: 8px !important; border: 1px solid #e2e8f0 !important; height: auto !important; min-height: auto !important; font-size: 13px !important; }', + '.h2-station-page--create .h2-create-form--list .ant-input:not(:disabled):hover, .h2-station-page--create .h2-create-form--list .ant-input-affix-wrapper:not(.ant-input-affix-wrapper-disabled):hover, .h2-station-page--create .h2-create-form--list .ant-select:not(.ant-select-disabled):hover .ant-select-selector, .h2-station-page--create .h2-create-form--list .ant-cascader:not(.ant-select-disabled):hover .ant-select-selector, .h2-station-page--create .h2-create-form--list .ant-picker:not(.ant-picker-disabled):hover { border-color: #cbd5e1 !important; }', + '.h2-station-page--create .h2-create-form--list .ant-input:focus, .h2-station-page--create .h2-create-form--list .ant-input-focused, .h2-station-page--create .h2-create-form--list .ant-input-affix-wrapper-focused, .h2-station-page--create .h2-create-form--list .ant-select-focused .ant-select-selector, .h2-station-page--create .h2-create-form--list .ant-cascader-focused .ant-select-selector, .h2-station-page--create .h2-create-form--list .ant-picker-focused { border-color: #10b981 !important; box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2) !important; }', + '.h2-station-page--create .h2-create-form--list .ant-form-item-label > label { font-size: 13px; font-weight: 500; color: #475569; }', + '.h2-station-page--create .h2-create-form--list .ant-radio-wrapper .ant-radio-checked .ant-radio-inner { border-color: #10b981; background-color: #10b981; }', + '.h2-station-page--create .h2-create-pagehead { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; margin-bottom: 16px; padding: 12px 20px; background: #fff; border-radius: 16px; border: 1px solid #e2e8f0; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.03); }', + '.h2-station-page--create .h2-create-card.ant-card { border-radius: 16px !important; }', + '.h2-station-page .h2-card-title-bar--step::before { display: none !important; }', + '.h2-station-page .h2-card-step-badge { width: 28px; height: 28px; border-radius: 50%; background: linear-gradient(145deg, #10b981 0%, #059669 100%); color: #fff; font-size: 14px; font-weight: 700; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; box-shadow: 0 2px 8px rgba(16, 185, 129, 0.32); }', + '.h2-station-page .h2-card-title-text { font-size: 15px; font-weight: 700; color: #0f172a; }', + '.h2-station-page--create .h2-create-form > .h2-create-card + .h2-create-card, .h2-station-page--edit .h2-create-form > .h2-create-card + .h2-create-card { margin-top: 16px; }', + '.h2-station-page--create .h2-create-footer-inner, .h2-station-page--edit .h2-create-footer-inner { justify-content: flex-end; }' +]).join('\n'); + +var H2_CURRENT_OPERATOR = '系统管理员'; +/** 原型登录角色:admin 平台管理员可改系统账号;station 加氢站账号不可改 */ +var H2_CURRENT_USER_ROLE = 'admin'; + +function h2IsAdminUser() { + if (typeof window !== 'undefined' && window.H2_STATION_PAGE_USER_ROLE) { + return String(window.H2_STATION_PAGE_USER_ROLE).toLowerCase() === 'admin'; + } + return H2_CURRENT_USER_ROLE === 'admin'; +} + +function h2OperateTimestamp() { + return new Date().toISOString().slice(0, 16).replace('T', ' '); +} + +function h2CreateBusinessStatusLog(beforeStatus, afterStatus, operator) { + return { + id: 'bsl-' + Date.now() + '-' + Math.floor(Math.random() * 1000), + operateTime: h2OperateTimestamp(), + operator: operator || H2_CURRENT_OPERATOR, + beforeStatus: beforeStatus || '—', + afterStatus: afterStatus || '—' + }; +} + +function h2CreateCostPriceLog(beforePrice, afterPrice, effectiveTime, operator, extra) { + return Object.assign({ + id: 'cpl-' + Date.now() + '-' + Math.floor(Math.random() * 1000), + operator: operator || H2_CURRENT_OPERATOR, + operateTime: h2OperateTimestamp(), + beforeCostPrice: beforePrice, + afterCostPrice: afterPrice, + effectiveTime: effectiveTime + }, extra || {}); +} + +function h2CreateEmptyTierRule() { + return { id: 'tr-' + Date.now() + '-' + Math.floor(Math.random() * 1000), thresholdKg: '', price: '' }; +} + +function h2CreatePriceConfigId() { + return 'pc-' + Date.now() + '-' + Math.floor(Math.random() * 1000); +} + +function h2CollectCustomerOptions(refuelRecords) { + var map = {}; + var i; + for (i = 0; i < (refuelRecords || []).length; i++) { + var r = refuelRecords[i]; + var name = (r.customerName || '').trim(); + if (!name) continue; + var id = 'cust-' + name; + map[id] = name; + } + return Object.keys(map).map(function (id) { + return { value: id, label: map[id] }; + }); +} + +function h2MigrateLegacyPriceConfig(record) { + return { + id: 'pc-default', + priceType: 'fixed', + customerScope: 'all', + customerId: null, + customerName: null, + fixedPrice: record && record.costUnitPrice != null ? record.costUnitPrice : null, + tierRules: [], + tierResetDay: 1, + tierPeriodVolumeKg: 0, + tierPeriodKey: '', + logs: (record && record.costPriceLogs) ? record.costPriceLogs.slice() : [] + }; +} + +function h2GetStationPriceConfigs(record) { + if (record && record.priceConfigs && record.priceConfigs.length) { + return record.priceConfigs.map(function (cfg) { + return Object.assign({}, cfg, { + logs: (cfg.logs || []).slice(), + tierRules: (cfg.tierRules || []).map(function (rule) { return Object.assign({}, rule); }) + }); + }); + } + return [h2MigrateLegacyPriceConfig(record)]; +} + +function h2SortTierRules(rules) { + return (rules || []).slice().sort(function (a, b) { + return (parseFloat(a.thresholdKg) || 0) - (parseFloat(b.thresholdKg) || 0); + }); +} + +function h2ResolveTierUnitPrice(volumeKg, tierRules) { + var rules = h2SortTierRules(tierRules); + if (!rules.length) return null; + var vol = parseFloat(volumeKg); + if (isNaN(vol)) vol = 0; + var matched = rules[0]; + var i; + for (i = 0; i < rules.length; i++) { + var threshold = parseFloat(rules[i].thresholdKg); + if (isNaN(threshold)) threshold = 0; + if (vol >= threshold) matched = rules[i]; + } + var price = parseFloat(matched.price); + return isNaN(price) ? null : price; +} + +function h2GetTierPeriodKey(resetDay, nowMs) { + var dayjs = window.dayjs; + if (!dayjs) return ''; + var d = dayjs(nowMs || Date.now()); + var day = parseInt(resetDay, 10) || 1; + if (d.date() < day) d = d.subtract(1, 'month'); + return d.format('YYYY-MM'); +} + +function h2GetTierPeriodVolume(config, nowMs) { + var now = nowMs || Date.now(); + var periodKey = h2GetTierPeriodKey(config.tierResetDay, now); + if (config.tierPeriodKey && config.tierPeriodKey !== periodKey) return 0; + return config.tierPeriodVolumeKg != null ? parseFloat(config.tierPeriodVolumeKg) || 0 : 0; +} + +function h2ComputeTierStats(config, nowMs) { + var volume = h2GetTierPeriodVolume(config, nowMs); + var unitPrice = h2ResolveTierUnitPrice(volume, config.tierRules); + var total = unitPrice != null ? volume * unitPrice : null; + return { volumeKg: volume, unitPrice: unitPrice, totalCost: total }; +} + +function h2ResolveFixedPriceFromConfig(config, nowMs) { + var logs = (config && config.logs) || []; + var now = nowMs || Date.now(); + var best = null; + var i; + for (i = 0; i < logs.length; i++) { + var log = logs[i]; + var t = h2ParseDateTimeMs(log.effectiveTime); + if (isNaN(t) || t > now) continue; + if (!best || t >= best.t) best = { t: t, price: log.afterCostPrice }; + } + if (best && best.price != null) return best.price; + if (config && config.fixedPrice != null) return config.fixedPrice; + return null; +} + +function h2ResolveConfigCurrentPrice(config, nowMs) { + if (!config) return null; + if (config.priceType === 'tiered') { + var stats = h2ComputeTierStats(config, nowMs); + return stats.unitPrice; + } + return h2ResolveFixedPriceFromConfig(config, nowMs); +} + +function h2GetDefaultAllCustomerPriceConfig(record) { + var configs = h2GetStationPriceConfigs(record); + var i; + for (i = 0; i < configs.length; i++) { + if (configs[i].customerScope === 'all') return configs[i]; + } + return configs[0] || null; +} + +function h2GetPrimaryPriceConfig(record) { + var configs = h2GetStationPriceConfigs(record); + return configs.length ? configs[0] : null; +} + +function h2HasMultiCustomerPriceSystems(record) { + return h2GetStationPriceConfigs(record).length > 1; +} + +function h2GetCustomerPriceConfig(record, customerId) { + var configs = h2GetStationPriceConfigs(record); + var i; + for (i = 0; i < configs.length; i++) { + if (configs[i].customerScope === 'customer' && configs[i].customerId === customerId) return configs[i]; + } + return h2GetDefaultAllCustomerPriceConfig(record); +} + +function h2GetAssignedCustomerIds(priceConfigs, exceptConfigId) { + var ids = {}; + var i; + for (i = 0; i < (priceConfigs || []).length; i++) { + var cfg = priceConfigs[i]; + if (exceptConfigId && cfg.id === exceptConfigId) continue; + if (cfg.customerScope === 'customer' && cfg.customerId) ids[cfg.customerId] = cfg.customerName || cfg.customerId; + } + return ids; +} + +function h2FindPriceConfigByCustomerId(priceConfigs, customerId, exceptConfigId) { + var i; + for (i = 0; i < (priceConfigs || []).length; i++) { + var cfg = priceConfigs[i]; + if (exceptConfigId && cfg.id === exceptConfigId) continue; + if (cfg.customerScope === 'customer' && cfg.customerId === customerId) return cfg; + } + return null; +} + +function h2GetConfigEffectiveTimeLabel(config) { + var logs = (config && config.logs) || []; + return logs.length && logs[0].effectiveTime ? logs[0].effectiveTime : '—'; +} + +function h2FormatConfigEffectiveDateTime(config) { + var raw = h2GetConfigEffectiveTimeLabel(config); + if (!raw || raw === '—') return '—'; + var dayjs = window.dayjs; + if (dayjs) { + var d = dayjs(raw.indexOf('T') >= 0 ? raw : raw.replace(' ', 'T')); + if (d.isValid && d.isValid()) return d.format('YYYY-MM-DD HH:mm'); + } + var s = String(raw).trim(); + if (/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}/.test(s)) return s.slice(0, 16); + return s; +} + +function h2GetTierResetTimeLabel(config) { + var resetDay = parseInt(config && config.tierResetDay, 10) || 1; + return '每月' + resetDay + '日'; +} + +function h2GetTierGapToNextThreshold(config, nowMs) { + var rules = h2SortTierRules(config && config.tierRules); + if (!rules.length) return null; + var volume = h2GetTierPeriodVolume(config, nowMs); + var i; + for (i = 0; i < rules.length; i++) { + var threshold = parseFloat(rules[i].thresholdKg); + if (isNaN(threshold)) threshold = 0; + if (threshold > volume) return threshold - volume; + } + return null; +} + +function h2FormatPriceConfigScopeLabel(config) { + if (!config || config.customerScope !== 'customer') return '全部客户'; + return config.customerName || config.customerId || '指定客户'; +} + +function h2FormatPriceTypeLabel(priceType) { + return priceType === 'tiered' ? '阶梯价格' : '固定价格'; +} + +function h2FormatTierRulesSummary(tierRules) { + var rules = h2SortTierRules(tierRules); + if (!rules.length) return '—'; + return rules.map(function (rule) { + return h2FormatTierRuleLine(rule); + }).join(';'); +} + +function h2FormatTierRuleLine(rule) { + return '≥' + h2FormatKgNum(rule.thresholdKg) + 'kg → ' + h2FormatYuanNum(rule.price) + '元/kg'; +} + +function h2GetTierRuleDisplayLines(row) { + if (row && row.tierRules && row.tierRules.length) { + return h2SortTierRules(row.tierRules).map(function (rule) { + return h2FormatTierRuleLine(rule); + }); + } + var summary = row && row.tierRulesSummary; + if (!summary || summary === '—') return []; + return String(summary).split(';').map(function (line) { return line.trim(); }).filter(Boolean); +} + +function h2SyncRecordPriceFields(record) { + var configs = h2GetStationPriceConfigs(record); + var primaryCfg = h2GetPrimaryPriceConfig(record); + var resolved = primaryCfg ? h2ResolveConfigCurrentPrice(primaryCfg) : null; + var legacyLogs = primaryCfg && primaryCfg.logs && primaryCfg.logs.length + ? primaryCfg.logs.slice() + : (record.costPriceLogs || []); + return Object.assign({}, record, { + priceConfigs: configs, + costPriceLogs: legacyLogs, + costUnitPrice: resolved != null ? resolved : record.costUnitPrice + }); +} + +function h2ApplyCustomerExclusion(priceConfigs, savedConfig) { + var savedId = savedConfig.id; + var savedCustomerId = savedConfig.customerScope === 'customer' ? savedConfig.customerId : null; + return (priceConfigs || []).filter(function (cfg) { + if (cfg.id === savedId) return false; + if (savedCustomerId && cfg.customerScope === 'customer' && cfg.customerId === savedCustomerId) return false; + return true; + }); +} + +function h2EmptyPriceModalForm() { + var dayjs = window.dayjs; + return { + editingConfigId: null, + priceFormVisible: false, + priceType: 'fixed', + customerScope: 'all', + customerId: null, + customerName: null, + selectedCustomerIds: [], + tierCustomerPickerOpen: false, + costUnitPrice: '', + effectiveTime: window.dayjs ? window.dayjs().format('YYYY-MM-DD HH:mm') : h2OperateTimestamp(), + tierRules: [h2CreateEmptyTierRule()], + tierResetDay: 1 + }; +} + +function h2PriceModalFormFromConfig(config) { + if (!config) return h2EmptyPriceModalForm(); + var selectedCustomerIds = config.customerScope === 'customer' && config.customerId ? [config.customerId] : []; + return { + editingConfigId: config.id, + priceType: config.priceType || 'fixed', + customerScope: config.customerScope || 'all', + customerId: config.customerId || null, + customerName: config.customerName || null, + selectedCustomerIds: selectedCustomerIds, + tierCustomerPickerOpen: false, + costUnitPrice: config.priceType !== 'tiered' + ? String(h2ResolveFixedPriceFromConfig(config) != null ? h2ResolveFixedPriceFromConfig(config) : (config.fixedPrice != null ? config.fixedPrice : '')) + : '', + effectiveTime: window.dayjs ? window.dayjs().format('YYYY-MM-DD HH:mm') : h2OperateTimestamp(), + tierRules: (config.tierRules && config.tierRules.length) + ? config.tierRules.map(function (rule) { + return { + id: rule.id || h2CreateEmptyTierRule().id, + thresholdKg: rule.thresholdKg != null ? String(rule.thresholdKg) : '', + price: rule.price != null ? String(rule.price) : '' + }; + }) + : [h2CreateEmptyTierRule()], + tierResetDay: config.tierResetDay != null ? config.tierResetDay : 1 + }; +} + +function h2ParseDateTimeMs(val) { + if (!val) return NaN; + var s = String(val).trim(); + if (!s) return NaN; + return new Date(s.indexOf('T') >= 0 ? s : s.replace(' ', 'T')).getTime(); +} + +function h2ToDateTimeDayjs(text) { + var dayjs = window.dayjs; + if (!dayjs || !text) return null; + var s = String(text).trim(); + if (!s) return null; + var d = dayjs(s.indexOf('T') >= 0 ? s : s.replace(' ', 'T')); + if (!(d.isValid && d.isValid())) d = dayjs(s); + return (d.isValid && d.isValid()) ? d : null; +} + +function h2ToDateDayjs(text) { + var dayjs = window.dayjs; + if (!dayjs || !text) return null; + var s = String(text).trim(); + if (!s) return null; + var d = dayjs(s); + return (d.isValid && d.isValid()) ? d : null; +} + +function h2SanitizeReceiptAmountInput(text) { + var s = String(text || '').replace(/[^\d.]/g, ''); + var parts = s.split('.'); + if (parts.length > 2) s = parts[0] + '.' + parts.slice(1).join(''); + var dot = s.indexOf('.'); + if (dot >= 0 && s.length - dot - 1 > 2) s = s.slice(0, dot + 3); + return s; +} + +function h2FormatReceiptAmountInput(text) { + var s = String(text || '').trim(); + if (!s) return ''; + var n = parseFloat(s); + if (isNaN(n) || n < 0) return ''; + return n.toFixed(2); +} + +function h2ResolveCurrentCostPrice(record) { + var primaryCfg = h2GetPrimaryPriceConfig(record); + if (primaryCfg) { + var resolved = h2ResolveConfigCurrentPrice(primaryCfg); + if (resolved != null) return resolved; + } + var logs = (record && record.costPriceLogs) || []; + var now = Date.now(); + var best = null; + var i; + for (i = 0; i < logs.length; i++) { + var log = logs[i]; + var t = h2ParseDateTimeMs(log.effectiveTime); + if (isNaN(t) || t > now) continue; + if (!best || t >= best.t) { + best = { t: t, price: log.afterCostPrice }; + } + } + if (best && best.price != null) return best.price; + return record && record.costUnitPrice != null ? record.costUnitPrice : null; +} + +function h2ApplyDueCostPriceToRecord(record) { + return h2SyncRecordPriceFields(record); +} + +function h2ApplyDueCostPricesToList(records) { + var changed = false; + var next = records.map(function (r) { + var updated = h2ApplyDueCostPriceToRecord(r); + if (updated !== r) changed = true; + return updated; + }); + return { records: next, changed: changed }; +} + +function h2FormatCostPriceDisplay(v) { + if (v == null || v === '') return '—'; + return h2FormatYuanNum(v) + ' 元/kg'; +} + +function h2CountPendingCostPriceLogs(logs) { + var now = Date.now(); + var count = 0; + var i; + for (i = 0; i < (logs || []).length; i++) { + var t = h2ParseDateTimeMs(logs[i].effectiveTime); + if (!isNaN(t) && t > now) count += 1; + } + return count; +} + +var H2_IMPORT_TEMPLATE_HEADERS = [ + '加氢站名称', '省', '市', '详细地址', '是否签约', '签约开始时间', '签约结束时间', '营业状态', '营业时间', '联系人', '联系电话' +]; + +var H2_IMPORT_TEMPLATE_SAMPLE = [ + ['苏州新区加氢站(导入示例)', '江苏省', '苏州市', '工业园区星湖街100号', '是', '2025-01-01', '2027-12-31', '营业中', '08:00-20:00', '周八', '13900001111'] +]; + +var H2_IMPORT_HEADER_MAP = { + '加氢站名称': 'name', + '站点名称': 'name', + '名称': 'name', + '省': 'province', + '省份': 'province', + '市': 'city', + '城市': 'city', + '详细地址': 'detail', + '地址详情': 'detail', + '是否签约': 'isSigned', + '签约状态': 'isSigned', + '签约开始时间': 'contractStart', + '签约开始': 'contractStart', + '签约结束时间': 'contractEnd', + '签约结束': 'contractEnd', + '营业状态': 'businessStatus', + '营业时间': 'businessHours', + '联系人': 'contact', + '联系电话': 'phone', + '电话': 'phone' +}; + +function h2CsvEscape(v) { + var s = String(v == null ? '' : v); + if (/[",\n\r]/.test(s)) return '"' + s.replace(/"/g, '""') + '"'; + return s; +} + +function h2DownloadCsv(filename, headers, rows) { + var lines = [headers.map(h2CsvEscape).join(',')].concat( + (rows || []).map(function (r) { return r.map(h2CsvEscape).join(','); }) + ); + var blob = new Blob(['\uFEFF' + lines.join('\n')], { type: 'text/csv;charset=utf-8;' }); + var a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = filename; + a.click(); + URL.revokeObjectURL(a.href); +} + +function h2ParseCsvLine(line) { + var out = []; + var cur = ''; + var inQuote = false; + var i; + for (i = 0; i < line.length; i++) { + var ch = line.charAt(i); + if (inQuote) { + if (ch === '"') { + if (line.charAt(i + 1) === '"') { cur += '"'; i++; } + else inQuote = false; + } else cur += ch; + } else if (ch === '"') inQuote = true; + else if (ch === ',') { out.push(cur.trim()); cur = ''; } + else cur += ch; + } + out.push(cur.trim()); + return out; +} + +function h2ParseSignedLabel(val) { + var s = String(val || '').trim(); + if (!s) return false; + var lower = s.toLowerCase(); + if (s === '是' || s === '已签约' || lower === 'yes' || lower === 'y' || lower === 'true' || lower === '1') return true; + if (s === '否' || s === '未签约' || lower === 'no' || lower === 'n' || lower === 'false' || lower === '0') return false; + return false; +} + +function h2NormalizeBusinessStatus(val) { + var s = String(val || '').trim(); + var i; + for (i = 0; i < H2_BUSINESS_STATUS_OPTIONS.length; i++) { + if (H2_BUSINESS_STATUS_OPTIONS[i].value === s) return s; + } + if (s.indexOf('暂停') >= 0) return '暂停营业'; + if (s.indexOf('停止') >= 0 || s.indexOf('停业') >= 0) return '停止营业'; + if (s.indexOf('营业') >= 0) return '营业中'; + return ''; +} + +function h2ParseImportCsv(text) { + var raw = String(text || '').replace(/^\uFEFF/, ''); + var lines = raw.split(/\r?\n/).filter(function (ln) { return ln.trim().length > 0; }); + if (lines.length < 2) return { rows: [], headerError: '文件至少需要表头行与一行数据' }; + var headers = h2ParseCsvLine(lines[0]).map(function (h) { return String(h || '').trim().replace(/^\uFEFF/, ''); }); + var fieldIdx = {}; + var hi; + for (hi = 0; hi < headers.length; hi++) { + var key = H2_IMPORT_HEADER_MAP[headers[hi]]; + if (key && fieldIdx[key] == null) fieldIdx[key] = hi; + } + if (fieldIdx.name == null) return { rows: [], headerError: '缺少必填列「加氢站名称」' }; + var rows = []; + var li; + for (li = 1; li < lines.length; li++) { + var cells = h2ParseCsvLine(lines[li]); + if (!cells.some(function (c) { return String(c || '').trim(); })) continue; + var get = function (field) { + var idx = fieldIdx[field]; + return idx == null || idx < 0 ? '' : String(cells[idx] == null ? '' : cells[idx]).trim(); + }; + rows.push({ + lineNo: li + 1, + name: get('name'), + province: get('province'), + city: get('city'), + detail: get('detail'), + isSigned: h2ParseSignedLabel(get('isSigned')), + contractStart: get('contractStart'), + contractEnd: get('contractEnd'), + businessStatus: h2NormalizeBusinessStatus(get('businessStatus')), + businessHours: get('businessHours'), + contact: get('contact'), + phone: get('phone') + }); + } + return { rows: rows, headerError: null }; +} + +function h2ValidateImportRow(row, existingNames) { + var errors = []; + if (!(row.name || '').trim()) errors.push('加氢站名称不能为空'); + else if (existingNames[(row.name || '').trim()]) errors.push('站点名称「' + row.name + '」已存在'); + if (!(row.province || '').trim()) errors.push('省不能为空'); + if (!(row.city || '').trim()) errors.push('市不能为空'); + if (!(row.detail || '').trim()) errors.push('详细地址不能为空'); + if (!(row.contact || '').trim()) errors.push('联系人不能为空'); + if (!(row.phone || '').trim()) errors.push('联系电话不能为空'); + if (!(row.businessStatus || '').trim()) errors.push('营业状态无效,请填写:营业中 / 暂停营业 / 停止营业'); + if (row.isSigned) { + if (!row.contractStart) errors.push('已签约站点需填写签约开始时间'); + if (!row.contractEnd) errors.push('已签约站点需填写签约结束时间'); + } + return errors; +} + +function h2ImportRowToRecord(row, id) { + var region = [row.province, row.city]; + var form = { + name: row.name, + address: { region: region, detail: row.detail }, + isSigned: row.isSigned, + contractStart: row.isSigned ? (row.contractStart || '') : '', + contractEnd: row.isSigned ? (row.contractEnd || '') : '', + contractFiles: [], + businessStatus: row.businessStatus || '营业中', + businessHours: row.businessHours || '', + contact: row.contact, + phone: row.phone + }; + var record = h2FormToRecord(form, id); + record.businessStatusLogs = []; + return record; +} + +function h2SvgIcon(paths, size) { + var s = size || 18; + return React.createElement('svg', { + width: s, + height: s, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + strokeWidth: 2, + strokeLinecap: 'round', + strokeLinejoin: 'round', + 'aria-hidden': true + }, paths.map(function (p, i) { + if (p.tag === 'circle') return React.createElement('circle', { key: i, cx: p.cx, cy: p.cy, r: p.r }); + if (p.tag === 'line') return React.createElement('line', { key: i, x1: p.x1, y1: p.y1, x2: p.x2, y2: p.y2 }); + if (p.tag === 'rect') return React.createElement('rect', { key: i, x: p.x, y: p.y, width: p.width, height: p.height, rx: p.rx }); + return React.createElement('path', { key: i, d: p.d }); + })); +} + +var H2_ICONS = { + station: h2SvgIcon([{ d: 'M3 21h18M5 21V7l8-4v18M19 21V11l-6-4' }]), + high: h2SvgIcon([{ d: 'M13 2L3 14h9l-1 8 10-12h-9l1-8z' }], 16), + low: h2SvgIcon([{ d: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' }], 16), + none: h2SvgIcon([{ d: 'M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z' }, { tag: 'line', x1: 12, y1: 9, x2: 12, y2: 13 }, { tag: 'line', x1: 12, y1: 17, x2: 12.01, y2: 17 }], 16), + doc: h2SvgIcon([{ d: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z' }, { d: 'M14 2v6h6' }, { tag: 'line', x1: 16, y1: 13, x2: 8, y2: 13 }, { tag: 'line', x1: 16, y1: 17, x2: 8, y2: 17 }], 14), + upload: h2SvgIcon([{ d: 'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4' }, { d: 'M17 8l-5-5-5 5' }, { tag: 'line', x1: 12, y1: 3, x2: 12, y2: 15 }], 14), + download: h2SvgIcon([{ d: 'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4' }, { d: 'M7 10l5 5 5-5' }, { tag: 'line', x1: 12, y1: 15, x2: 12, y2: 3 }], 14), + empty: h2SvgIcon([{ tag: 'circle', cx: 12, cy: 12, r: 10 }, { tag: 'line', x1: 8, y1: 12, x2: 16, y2: 12 }], 40), + back: h2SvgIcon([{ tag: 'line', x1: 19, y1: 12, x2: 5, y2: 12 }, { d: 'M12 19l-7-7 7-7' }], 16), + mapPin: h2SvgIcon([{ tag: 'path', d: 'M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z' }, { tag: 'circle', cx: 12, cy: 10, r: 3 }], 16), + user: h2SvgIcon([{ tag: 'path', d: 'M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2' }, { tag: 'circle', cx: 12, cy: 7, r: 4 }], 16), + building: h2SvgIcon([{ d: 'M3 21h18M5 21V7l8-4v18M19 21V11l-6-4' }], 18), + truck: h2SvgIcon([{ d: 'M1 3h15v13H1zM16 8h4l3 3v5h-7V8z' }, { tag: 'circle', cx: 5.5, cy: 18.5, r: 2.5 }, { tag: 'circle', cx: 18.5, cy: 18.5, r: 2.5 }], 16), + link: h2SvgIcon([{ tag: 'path', d: 'M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71' }, { tag: 'path', d: 'M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71' }], 16), + plus: h2SvgIcon([{ tag: 'line', x1: 12, y1: 5, x2: 12, y2: 19 }, { tag: 'line', x1: 5, y1: 12, x2: 19, y2: 12 }], 16), + file: h2SvgIcon([{ d: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z' }, { d: 'M14 2v6h6' }], 16) +}; + +function h2MoreIcon() { + return React.createElement('svg', { + viewBox: '0 0 16 16', + width: 16, + height: 16, + fill: 'currentColor', + 'aria-hidden': true + }, + React.createElement('circle', { cx: 8, cy: 3, r: 1.5 }), + React.createElement('circle', { cx: 8, cy: 8, r: 1.5 }), + React.createElement('circle', { cx: 8, cy: 13, r: 1.5 }) + ); +} + +function h2DeriveFrequencyByRefuelCount(refuelCount) { + var n = typeof refuelCount === 'number' ? refuelCount : parseInt(refuelCount, 10); + if (!n || n <= 0) return 'none'; + if (n >= 3) return 'high'; + return 'low'; +} + +function h2ResolveBalanceAlertThreshold(record) { + if (!record || record.balanceAlertThreshold == null || record.balanceAlertThreshold === '') return null; + var n = parseFloat(record.balanceAlertThreshold); + return isNaN(n) || n <= 0 ? null : n; +} + +function h2IsArrearsStation(record) { + return h2NumOrZero(record && record.prepaidBalance) < 0; +} + +function h2IsBalanceAlertStation(record) { + if (h2IsArrearsStation(record)) return false; + var threshold = h2ResolveBalanceAlertThreshold(record); + if (threshold == null) return false; + return h2NumOrZero(record.prepaidBalance) < threshold; +} + +function h2CreateRechargeLineId() { + return 'rcl-' + Date.now() + '-' + Math.floor(Math.random() * 10000); +} + +function h2BuildRechargeTransferPurpose(date) { + var d = date || new Date(); + var y = d.getFullYear(); + var m = String(d.getMonth() + 1).padStart(2, '0'); + var day = String(d.getDate()).padStart(2, '0'); + return '羚牛预付氢费款' + y + '-' + m + '-' + day; +} + +function h2CreateEmptyRechargeLine() { + return { + id: h2CreateRechargeLineId(), + stationId: undefined, + stationName: '', + currentBalance: '', + payAmount: '', + companyName: '', + bankAccount: '', + bankName: '', + transferPurpose: h2BuildRechargeTransferPurpose() + }; +} + +function h2BuildRechargeLineFromStation(record, payAmount) { + var supplier = h2ResolveStationSupplier(record); + return { + id: h2CreateRechargeLineId(), + stationId: record.id, + stationName: record.name || '', + currentBalance: h2FormatYuanNum(record.prepaidBalance), + payAmount: payAmount != null && payAmount !== '' ? payAmount : '', + companyName: supplier ? (supplier.name || '') : '', + bankAccount: supplier ? (supplier.bankAccount || '') : '', + bankName: supplier ? (supplier.bankName || '') : '', + transferPurpose: h2BuildRechargeTransferPurpose() + }; +} + +function h2EnrichStationListRows(records) { + return (records || []).map(function (r) { + var stats = h2CalcRefuelStats(r.name); + return Object.assign({}, r, { + refuelCount: stats.count, + refuelTotalKg: stats.totalKg, + refuelFreqKey: h2DeriveFrequencyByRefuelCount(stats.count) + }); + }); +} + +function h2RenderRechargeAutoField(value, hasStation, placeholder) { + if (!hasStation) { + return React.createElement('span', { className: 'h2-recharge-readonly h2-recharge-readonly--muted' }, '—'); + } + var text = value != null && value !== '' ? String(value) : ''; + if (!text) { + return React.createElement('span', { className: 'h2-recharge-readonly h2-recharge-readonly--muted' }, placeholder || '—'); + } + var InputComp = window.antd && window.antd.Input; + if (!InputComp) { + return React.createElement('span', { className: 'h2-recharge-readonly' }, text); + } + return React.createElement(InputComp, { + className: 'h2-recharge-readonly-input', + value: text, + disabled: true, + style: { width: '100%', borderRadius: 8 } + }); +} + +function h2FormatRegion(region) { + if (!region || !region.length) return '—'; + return region.join('-'); +} + +function h2FormatStationAddressLine(record) { + var regionText = h2FormatRegion(record && record.region); + var detail = String((record && record.addressDetail) || '').trim(); + if (regionText === '—' && !detail) return '—'; + if (regionText === '—') return detail; + if (!detail) return regionText; + return regionText + ' ' + detail; +} + +function h2MatchRegionFilter(recordRegion, filterRegion) { + if (!filterRegion || !filterRegion.length) return true; + var rr = recordRegion || []; + if (filterRegion[0] && rr[0] !== filterRegion[0]) return false; + if (filterRegion[1] && rr[1] !== filterRegion[1]) return false; + return true; +} + +function h2EmptyListFilters() { + return { name: '', signed: undefined, region: undefined, businessStatus: undefined }; +} + +/** 签约站点是否已上传合同附件 */ +function h2HasUploadedContract(record) { + var files = record && record.contractFiles; + if (!files || !files.length) return false; + var i; + for (i = 0; i < files.length; i++) { + var f = files[i]; + if (!f) continue; + if ((f.name || '').trim()) return true; + if (f.url || (f.response && (f.response.url || f.response.fileUrl))) return true; + } + return false; +} + +/** 列表签约站点标签文案与样式 */ +function h2RenderStationSignedTag(record) { + var Tag = window.antd && window.antd.Tag; + if (!Tag || !record || !record.isSigned) return null; + var uploaded = h2HasUploadedContract(record); + return React.createElement(Tag, { + color: uploaded ? 'success' : 'warning', + className: 'lc-station-signed-tag' + }, uploaded ? '签约站点' : '签约站点-未上传合同'); +} + +/** 距签约结束日天数:正=剩余,负=已过期 */ +function h2DaysUntilContractEnd(dateStr) { + if (!dateStr) return null; + var parts = String(dateStr).trim().split('-'); + if (parts.length < 3) return null; + var y = parseInt(parts[0], 10); + var m = parseInt(parts[1], 10) - 1; + var d = parseInt(parts[2], 10); + if (isNaN(y) || isNaN(m) || isNaN(d)) return null; + var end = new Date(y, m, d, 23, 59, 59, 999); + var today = new Date(); + today.setHours(0, 0, 0, 0); + return Math.ceil((end.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)); +} + +function h2RenderContractRemainTag(contractEnd) { + var days = h2DaysUntilContractEnd(contractEnd); + if (days == null) return null; + var Tag = window.antd && window.antd.Tag; + if (!Tag) return null; + var tagProps = { style: { marginTop: 4, borderRadius: 6, fontWeight: 600, fontSize: 11 } }; + if (days < 0) { + return React.createElement(Tag, Object.assign({}, tagProps, { + color: 'error', + 'aria-label': '已过期 ' + Math.abs(days) + ' 天' + }), '已过期 ' + Math.abs(days) + ' 天'); + } + if (days <= 30) { + return React.createElement(Tag, Object.assign({}, tagProps, { + color: 'warning', + 'aria-label': '剩余 ' + days + ' 天' + }), '剩余 ' + days + ' 天'); + } + return React.createElement(Tag, Object.assign({}, tagProps, { + color: 'success', + 'aria-label': '剩余 ' + days + ' 天' + }), '剩余 ' + days + ' 天'); +} + +function h2BuildFullAddress(region, detail) { + var base = h2FormatRegion(region); + if (!detail) return base === '—' ? '' : base; + return base === '—' ? detail : base + detail; +} + +function h2EmptyAddressValue() { + return { region: [], detail: '' }; +} + +/** 从完整地址文本解析省/市与详细地址(基于站点省市区选项) */ +function h2ParseAddressText(rawText) { + var text = String(rawText || '').trim(); + if (!text) return { region: [], detail: '', matched: false }; + + var compact = text.replace(/\s+/g, ''); + var provinces = H2_REGION_CASCADER_OPTIONS.slice().sort(function (a, b) { + return String(b.value || b.label || '').length - String(a.value || a.label || '').length; + }); + + var matchedProvince = null; + var provinceStart = -1; + var matchedProvinceLen = 0; + for (var i = 0; i < provinces.length; i++) { + var pName = provinces[i].value || provinces[i].label; + var aliases = [pName]; + if (pName.slice(-1) === '省') aliases.push(pName.slice(0, -1)); + if (pName.slice(-1) === '市') aliases.push(pName.replace(/市$/, '')); + var ai; + for (ai = 0; ai < aliases.length; ai++) { + var alias = aliases[ai]; + var idx = compact.indexOf(alias); + if (idx !== -1 && (provinceStart === -1 || idx < provinceStart || (idx === provinceStart && alias.length > matchedProvinceLen))) { + provinceStart = idx; + matchedProvince = provinces[i]; + matchedProvinceLen = alias.length; + } + } + } + + if (!matchedProvince) { + return { region: [], detail: text, matched: false }; + } + + var provinceName = matchedProvince.value || matchedProvince.label; + var rest = compact.slice(provinceStart + matchedProvinceLen).replace(/^[\s\-—,,、·]+/, ''); + var cities = matchedProvince.children || []; + var cityMatch = h2FindCityInAddressRest(rest, cities); + + if (!cityMatch && cities.length === 1) { + return { + region: [provinceName, cities[0].value || cities[0].label], + detail: rest.replace(/^[\s\-—,,、·]+/, ''), + matched: true + }; + } + + if (!cityMatch) { + return { region: [provinceName], detail: rest || text, matched: true, partial: true }; + } + + var cityName = cityMatch.city.value || cityMatch.city.label; + var detail = rest.slice(cityMatch.end); + detail = detail.replace(/^[\s\-—,,、·省市区县]+/, ''); + if (!detail && cityMatch.prefix) detail = cityMatch.prefix.replace(/^[\s\-—,,、·省市区县]+/, ''); + + return { region: [provinceName, cityName], detail: detail, matched: true }; +} + +function h2StripRegionPrefixFromDetail(region, detail) { + var text = String(detail || '').trim(); + if (!text || !region || region.length < 2) return text; + var cityName = region[1] || ''; + var cityPlain = cityName.replace(/市$/, ''); + if (cityName && text.indexOf(cityName) === 0) text = text.slice(cityName.length); + else if (cityPlain && text.indexOf(cityPlain) === 0) text = text.slice(cityPlain.length); + return text.replace(/^[\s\-—,,、·省市区县]+/, ''); +} + +function h2FindCityInAddressRest(rest, cities) { + if (!rest || !cities || !cities.length) return null; + var sorted = cities.slice().sort(function (a, b) { + return String(b.value || b.label || '').length - String(a.value || a.label || '').length; + }); + var i; + for (i = 0; i < sorted.length; i++) { + var cn = sorted[i].value || sorted[i].label; + if (rest.indexOf(cn) === 0) return { city: sorted[i], prefix: '', end: cn.length }; + } + for (i = 0; i < sorted.length; i++) { + var cn2 = sorted[i].value || sorted[i].label; + var idx = rest.indexOf(cn2); + if (idx > 0 && idx <= 8) return { city: sorted[i], prefix: rest.slice(0, idx), end: idx + cn2.length }; + } + return null; +} + +/** 地址解析:失焦后自动识别并回填省/市与详细地址 */ +function AddressPasteInput(props) { + var antd = window.antd; + var Input = antd.Input; + var message = antd.message; + var _paste = React.useState(''); + var pasteText = _paste[0]; + var setPasteText = _paste[1]; + var disabled = props.disabled; + var inputClassName = props.inputClassName || ''; + + var runParse = function (raw) { + raw = (raw || '').trim(); + if (!raw) return; + var result = h2ParseAddressText(raw); + if (!result.matched) { + message.warning('未能识别省/市,请将内容填入详细地址'); + if (props.onParsed) props.onParsed({ region: [], detail: raw }); + return; + } + if (result.partial || !result.region || result.region.length < 2) { + message.warning('已识别省份,请补全城市信息'); + } + var region = result.region || []; + var detail = h2StripRegionPrefixFromDetail(region, result.detail || ''); + if (props.onParsed) props.onParsed({ region: region, detail: detail }); + }; + + return React.createElement('div', { className: 'h2-address-paste' }, + React.createElement(Input.TextArea, { + className: inputClassName, + value: pasteText, + disabled: disabled, + placeholder: props.placeholder || '填写完整地址自动解析省市和详细地址', + autoSize: { minRows: 2, maxRows: 4 }, + onChange: function (e) { setPasteText(e.target.value); }, + onBlur: function () { + if (disabled) return; + runParse(pasteText); + } + }) + ); +} + +var H2_DEFAULT_MAP_CENTER = { lat: 30.274084, lng: 120.15507 }; +var H2_MAP_LEAFLET_READY = null; + +var H2_CITY_GEO_COORDS = { + '浙江省|嘉兴市': { lat: 30.746129, lng: 120.755486 }, + '浙江省|杭州市': { lat: 30.274084, lng: 120.15507 }, + '浙江省|宁波市': { lat: 29.868336, lng: 121.54399 }, + '上海市|上海市': { lat: 31.230416, lng: 121.473701 }, + '江苏省|南京市': { lat: 32.060255, lng: 118.796877 }, + '江苏省|苏州市': { lat: 31.298886, lng: 120.585316 }, + '广东省|广州市': { lat: 23.12911, lng: 113.264385 }, + '广东省|深圳市': { lat: 22.543099, lng: 114.057868 } +}; + +function h2EnsureLeafletLoaded() { + if (H2_MAP_LEAFLET_READY) return H2_MAP_LEAFLET_READY; + H2_MAP_LEAFLET_READY = new Promise(function (resolve, reject) { + if (window.L && window.L.map) { + resolve(window.L); + return; + } + if (!document.querySelector('link[data-h2-leaflet-css]')) { + var link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'; + link.setAttribute('data-h2-leaflet-css', '1'); + document.head.appendChild(link); + } + var existing = document.querySelector('script[data-h2-leaflet-js]'); + if (existing) { + existing.addEventListener('load', function () { resolve(window.L); }); + existing.addEventListener('error', reject); + return; + } + var script = document.createElement('script'); + script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js'; + script.setAttribute('data-h2-leaflet-js', '1'); + script.onload = function () { resolve(window.L); }; + script.onerror = reject; + document.head.appendChild(script); + }); + return H2_MAP_LEAFLET_READY; +} + +function h2FixLeafletDefaultIcon(L) { + if (!L || !L.Icon || !L.Icon.Default) return; + delete L.Icon.Default.prototype._getIconUrl; + L.Icon.Default.mergeOptions({ + iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png', + iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png', + shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png' + }); +} + +function h2ResolveGeoFromRegion(region) { + if (!region || region.length < 2) return null; + return H2_CITY_GEO_COORDS[region[0] + '|' + region[1]] || null; +} + +function h2GeocodeAddressPromise(address) { + var text = String(address || '').trim(); + if (!text) return Promise.resolve(null); + var parsed = h2ParseAddressText(text); + if (parsed.region && parsed.region.length >= 2) { + var city = h2ResolveGeoFromRegion(parsed.region); + if (city) return Promise.resolve({ lat: city.lat, lng: city.lng }); + } + if (!window.fetch) return Promise.resolve(null); + return fetch('https://nominatim.openstreetmap.org/search?format=json&limit=1&q=' + encodeURIComponent(text), { + headers: { 'Accept-Language': 'zh-CN' } + }) + .then(function (res) { return res.json(); }) + .then(function (data) { + if (!data || !data.length) return null; + var lat = parseFloat(data[0].lat); + var lng = parseFloat(data[0].lon); + if (isNaN(lat) || isNaN(lng)) return null; + return { lat: lat, lng: lng }; + }) + .catch(function () { return null; }); +} + +function h2FormatCoordNum(value) { + if (value == null || value === '') return '—'; + var n = parseFloat(value); + if (isNaN(n)) return '—'; + return n.toFixed(6); +} + +function h2NormalizeCoord(value) { + if (value == null || value === '') return null; + var n = parseFloat(value); + return isNaN(n) ? null : n; +} + +/** 地图定位:输入地址自动定位,拖动标记获取经纬度 */ +function StationMapPicker(props) { + var antd = window.antd; + var Input = antd.Input; + var Button = antd.Button; + var message = antd.message; + var mapElRef = React.useRef(null); + var mapRef = React.useRef(null); + var markerRef = React.useRef(null); + var geocodeReqRef = React.useRef(0); + var lastExternalAddressRef = React.useRef(''); + var locateByAddressRef = React.useRef(null); + + var _localAddr = React.useState(props.address || ''); + var localAddress = _localAddr[0]; + var setLocalAddress = _localAddr[1]; + + var _loading = React.useState(false); + var loading = _loading[0]; + var setLoading = _loading[1]; + + var _mapReady = React.useState(false); + var mapReady = _mapReady[0]; + var setMapReady = _mapReady[1]; + + var inputClassName = props.inputClassName || ''; + var disabled = !!props.disabled; + var latitude = h2NormalizeCoord(props.latitude); + var longitude = h2NormalizeCoord(props.longitude); + + var applyMarkerPosition = React.useCallback(function (lat, lng, moveView) { + if (lat == null || lng == null || isNaN(lat) || isNaN(lng)) return; + if (markerRef.current) markerRef.current.setLatLng([lat, lng]); + if (moveView !== false && mapRef.current) mapRef.current.setView([lat, lng], Math.max(mapRef.current.getZoom() || 15, 14)); + if (props.onPositionChange) props.onPositionChange(lat, lng); + }, [props.onPositionChange]); + + var locateByAddress = React.useCallback(function (addr, showWarn) { + var text = String(addr || '').trim(); + if (!text || disabled) return; + setLoading(true); + var reqId = geocodeReqRef.current + 1; + geocodeReqRef.current = reqId; + h2GeocodeAddressPromise(text).then(function (result) { + if (reqId !== geocodeReqRef.current) return; + setLoading(false); + if (!result) { + if (showWarn !== false) message.warning('未能定位该地址,请拖动地图标记手动选点'); + return; + } + applyMarkerPosition(result.lat, result.lng, true); + }); + }, [disabled, applyMarkerPosition]); + + locateByAddressRef.current = locateByAddress; + + React.useEffect(function () { + var ext = props.address || ''; + if (ext === lastExternalAddressRef.current) return; + lastExternalAddressRef.current = ext; + setLocalAddress(ext); + if (ext && locateByAddressRef.current) locateByAddressRef.current(ext, false); + }, [props.address]); + + React.useEffect(function () { + var cancelled = false; + h2EnsureLeafletLoaded().then(function (L) { + if (cancelled || !mapElRef.current || mapRef.current) return; + h2FixLeafletDefaultIcon(L); + var initLat = latitude != null ? latitude : H2_DEFAULT_MAP_CENTER.lat; + var initLng = longitude != null ? longitude : H2_DEFAULT_MAP_CENTER.lng; + var map = L.map(mapElRef.current, { + zoomControl: true, + attributionControl: true + }).setView([initLat, initLng], latitude != null && longitude != null ? 15 : 11); + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 19, + attribution: '© OpenStreetMap' + }).addTo(map); + var marker = L.marker([initLat, initLng], { draggable: !disabled }).addTo(map); + marker.on('dragend', function () { + if (disabled) return; + var ll = marker.getLatLng(); + if (props.onPositionChange) props.onPositionChange(ll.lat, ll.lng); + }); + mapRef.current = map; + markerRef.current = marker; + setMapReady(true); + setTimeout(function () { map.invalidateSize(); }, 80); + }).catch(function () { + if (!cancelled) message.error('地图加载失败,请检查网络后刷新重试'); + }); + return function () { + cancelled = true; + if (mapRef.current) { + mapRef.current.remove(); + mapRef.current = null; + markerRef.current = null; + } + }; + }, []); + + React.useEffect(function () { + if (!mapReady || latitude == null || longitude == null) return; + if (markerRef.current) { + var ll = markerRef.current.getLatLng(); + if (Math.abs(ll.lat - latitude) < 0.000001 && Math.abs(ll.lng - longitude) < 0.000001) return; + } + applyMarkerPosition(latitude, longitude, false); + }, [latitude, longitude, mapReady, applyMarkerPosition]); + + React.useEffect(function () { + if (markerRef.current) { + if (disabled) markerRef.current.dragging.disable(); + else markerRef.current.dragging.enable(); + } + }, [disabled, mapReady]); + + return React.createElement('div', { className: 'h2-station-map-picker' }, + React.createElement('div', { className: 'h2-station-map-picker__toolbar' }, + React.createElement(Input, { + className: inputClassName, + value: localAddress, + disabled: disabled, + placeholder: '输入地址后定位,或从上方地址解析自动填入', + onChange: function (e) { + var next = e.target.value; + setLocalAddress(next); + if (props.onAddressChange) props.onAddressChange(next); + }, + onBlur: function () { + if (disabled) return; + locateByAddress(localAddress, true); + }, + onPressEnter: function () { + if (disabled) return; + locateByAddress(localAddress, true); + } + }), + React.createElement(Button, { + type: 'default', + disabled: disabled || loading || !String(localAddress || '').trim(), + loading: loading, + onClick: function () { locateByAddress(localAddress, true); } + }, '定位') + ), + React.createElement('div', { className: 'h2-station-map-picker__map', ref: mapElRef, role: 'application', 'aria-label': '加氢站地图定位' }), + React.createElement('div', { className: 'h2-station-map-picker__coords' }, + React.createElement('span', { className: 'h2-station-map-picker__coords-item' }, '经度:', React.createElement('strong', null, h2FormatCoordNum(longitude))), + React.createElement('span', { className: 'h2-station-map-picker__coords-item' }, '纬度:', React.createElement('strong', null, h2FormatCoordNum(latitude))) + ), + React.createElement('div', { className: 'h2-station-map-picker__hint' }, '拖动地图标记可微调位置,经纬度将自动更新') + ); +} + +var H2_BUSINESS_LICENSE_OCR_MOCKS = [ + { + test: function (name) { return /嘉兴|南湖|氢能供应/i.test(name || ''); }, + name: '嘉兴氢能供应有限公司', + taxId: '91330400MA2ABCDEF1', + communicationAddress: '浙江省嘉兴市南湖区科技大道66号' + }, + { + test: function (name) { return /临平|杭州/i.test(name || ''); }, + name: '杭州临平氢能运营有限公司', + taxId: '91330110MA3LINPING1', + communicationAddress: '浙江省杭州市临平区东湖街道能源路16号' + }, + { + test: function (name) { return /羚牛|宝山|上海/i.test(name || ''); }, + name: '上海羚牛氢能科技有限公司', + taxId: '91310100MA1XYZ7890', + communicationAddress: '上海市宝山区富联路200号' + }, + { + test: function (name) { return /苏州|星湖/i.test(name || ''); }, + name: '苏州工业园区氢源科技有限公司', + taxId: '91320500MA1ABCDEF2', + communicationAddress: '江苏省苏州市工业园区星湖街328号' + }, + { + test: function () { return true; }, + name: '浙江羚牛氢能科技有限公司', + taxId: '91330481MA2JXXXXXX', + communicationAddress: '浙江省嘉兴市平湖市乍浦镇杭州湾大道1号' + } +]; + +function h2MatchBusinessLicenseOcrMock(file) { + var fileName = (file && file.name) || ''; + var i; + for (i = 0; i < H2_BUSINESS_LICENSE_OCR_MOCKS.length; i++) { + if (H2_BUSINESS_LICENSE_OCR_MOCKS[i].test(fileName)) { + return H2_BUSINESS_LICENSE_OCR_MOCKS[i]; + } + } + return H2_BUSINESS_LICENSE_OCR_MOCKS[H2_BUSINESS_LICENSE_OCR_MOCKS.length - 1]; +} + +function h2ApplyBusinessLicenseOcr(ctx, ocrData) { + if (!ocrData || !ctx || !ctx.updateSupplier) return; + var commAddr = String(ocrData.communicationAddress || '').trim(); + var parsed = h2ParseAddressText(commAddr); + var region = parsed.region || []; + var detail = commAddr + ? h2StripRegionPrefixFromDetail(region, parsed.detail || commAddr) + : ''; + var supplierPatch = { + name: (ocrData.name || '').trim(), + taxId: (ocrData.taxId || '').trim(), + address: detail, + mailingAddress: commAddr || detail, + businessAddress: commAddr || detail + }; + if (region.length >= 2) { + supplierPatch.city = region.slice(); + supplierPatch.region = h2GetRegionByProvince(region[0]); + } + ctx.updateSupplier(supplierPatch); + if (ctx.updateStation && ctx.supplierMode === 'new' && region.length >= 2) { + var fullAddr = h2BuildFullAddress(region, detail); + ctx.updateStation({ + address: { region: region.slice(), detail: detail }, + mapAddress: fullAddr + }); + } +} + +function h2TriggerBusinessLicenseOcr(file, ctx) { + var antd = window.antd; + var message = antd && antd.message; + var hide = message && message.loading('营业执照识别中…', 0); + window.setTimeout(function () { + if (hide) hide(); + var ocrData = h2MatchBusinessLicenseOcrMock(file); + h2ApplyBusinessLicenseOcr(ctx, ocrData); + if (message) { + message.success('识别完成:已填写供应商名称、纳税人识别号与通讯地址(未写入注册地址)'); + } + }, 720); +} + +function h2KpiCardIcon(key) { + var size = 22; + if (key === 'all') { + return React.createElement('svg', { + width: size, + height: size, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + strokeWidth: 1.75, + strokeLinecap: 'round', + strokeLinejoin: 'round', + 'aria-hidden': true + }, + React.createElement('path', { d: 'M4 20h16' }), + React.createElement('path', { d: 'M7 20v-9h4v9' }), + React.createElement('path', { d: 'M14 20V8l5-3v15' }), + React.createElement('text', { + x: 8.5, + y: 13.5, + fill: 'currentColor', + stroke: 'none', + fontSize: 5.5, + fontWeight: 700, + fontFamily: 'system-ui, -apple-system, sans-serif' + }, 'H₂') + ); + } + if (key === 'yes') { + return h2SvgIcon([ + { d: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z' }, + { d: 'M14 2v6h6' }, + { tag: 'circle', cx: 17.5, cy: 17.5, r: 3.5 }, + { d: 'M16 17.5l1 1 2-2' } + ], size); + } + if (key === 'no') { + return h2SvgIcon([ + { d: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z' }, + { d: 'M14 2v6h6' }, + { tag: 'circle', cx: 17.5, cy: 17.5, r: 3.5 }, + { tag: 'line', x1: 16, y1: 16, x2: 19, y2: 19 }, + { tag: 'line', x1: 19, y1: 16, x2: 16, y2: 19 } + ], size); + } + if (key === 'balanceAlert') { + return h2SvgIcon([ + { d: 'M12 2v4' }, + { d: 'M12 18v4' }, + { d: 'M4.93 4.93l2.83 2.83' }, + { d: 'M16.24 16.24l2.83 2.83' }, + { d: 'M2 12h4' }, + { d: 'M18 12h4' }, + { d: 'M4.93 19.07l2.83-2.83' }, + { d: 'M16.24 7.76l2.83-2.83' }, + { tag: 'circle', cx: 12, cy: 12, r: 4 } + ], size); + } + if (key === 'arrears') { + return h2SvgIcon([ + { d: 'M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z' }, + { tag: 'line', x1: 12, y1: 9, x2: 12, y2: 13 }, + { tag: 'line', x1: 12, y1: 17, x2: 12.01, y2: 17 } + ], size); + } + if (key === 'none') { + return h2SvgIcon([ + { d: 'M6 20h12' }, + { d: 'M9 20v-6h2c1.2 0 2.2 1 2.2 2.2S12.2 18.4 11 18.4H9' }, + { tag: 'circle', cx: 12, cy: 11, r: 7.5 }, + { tag: 'line', x1: 6.8, y1: 6.8, x2: 17.2, y2: 17.2 } + ], size); + } + return h2KpiCardIcon('all'); +} + +function h2KpiIcon(key) { + return h2KpiCardIcon(key); +} + +function h2SignedFilterIcon(key) { + return h2KpiCardIcon(key === 'yes' ? 'yes' : 'no'); +} + +/** 地址组件:省/市级联 + 详细地址(无子标题,挂在「地址」表单项下) */ +function RegionAddressInput(props) { + var antd = window.antd; + var Cascader = antd.Cascader; + var Input = antd.Input; + var value = props.value || h2EmptyAddressValue(); + var onChange = props.onChange; + var disabled = props.disabled; + var inputClassName = props.inputClassName || ''; + var layout = props.layout || 'stack'; + var detailPlaceholder = props.detailPlaceholder || '请输入街道、门牌号等详细地址'; + var regionPlaceholder = props.regionPlaceholder || '请选择省 / 市'; + var fieldStyle = props.fieldStyle || { width: '100%', borderRadius: 8 }; + var rootCls = 'h2-region-address' + (layout === 'inline' ? ' h2-region-address--inline' : '') + (props.className ? ' ' + props.className : ''); + + return React.createElement('div', { className: rootCls }, + React.createElement(Cascader, { + className: inputClassName + (layout === 'inline' ? ' h2-region-address-cascader' : ''), + options: H2_REGION_CASCADER_OPTIONS, + value: value.region && value.region.length ? value.region : undefined, + onChange: function (v) { onChange && onChange({ region: v || [], detail: value.detail || '' }); }, + placeholder: regionPlaceholder, + style: layout === 'inline' ? { width: '100%', borderRadius: fieldStyle.borderRadius || 8 } : fieldStyle, + disabled: disabled, + allowClear: !disabled + }), + React.createElement(Input, { + className: inputClassName + (layout === 'inline' ? ' h2-region-address-detail' : ''), + value: value.detail || '', + onChange: function (e) { onChange && onChange({ region: value.region || [], detail: e.target.value }); }, + placeholder: detailPlaceholder, + disabled: disabled, + maxLength: 200, + style: layout === 'inline' ? { width: '100%', borderRadius: fieldStyle.borderRadius || 8 } : fieldStyle + }) + ); +} + +/** 供应商通讯地址:省/市 + 详细地址(横向嵌入) */ +function SupplierCityAddressInput(props) { + var antd = window.antd; + var Cascader = antd.Cascader; + var Input = antd.Input; + var city = props.city; + var address = props.address || ''; + var onChange = props.onChange; + var disabled = props.disabled; + var inputClassName = props.inputClassName || ''; + var onCityChange = props.onCityChange; + var layout = props.layout || 'inline'; + var rootCls = 'h2-region-address' + (layout === 'inline' ? ' h2-region-address--inline' : '') + (props.className ? ' ' + props.className : ''); + + return React.createElement('div', { className: rootCls }, + React.createElement(Cascader, { + className: inputClassName + (layout === 'inline' ? ' h2-region-address-cascader' : ''), + options: H2_REGION_CASCADER_OPTIONS, + value: city && city.length ? city : undefined, + onChange: onCityChange, + placeholder: '请选择省 / 市', + style: { width: '100%' }, + disabled: disabled, + allowClear: !disabled + }), + React.createElement(Input, { + className: inputClassName + (layout === 'inline' ? ' h2-region-address-detail' : ''), + value: address, + disabled: disabled, + placeholder: '请输入详细地址', + maxLength: 200, + style: { width: '100%' }, + onChange: function (e) { onChange && onChange(e.target.value); } + }) + ); +} + +function h2GetUploadFileUrl(file) { + if (!file) return ''; + if (file.url) return file.url; + if (file.thumbUrl) return file.thumbUrl; + if (file.originFileObj && typeof URL !== 'undefined' && URL.createObjectURL) { + try { return URL.createObjectURL(file.originFileObj); } catch (e) { return ''; } + } + return ''; +} + +function h2DownloadUploadFile(file) { + var url = h2GetUploadFileUrl(file); + if (!url) return; + var a = document.createElement('a'); + a.href = url; + a.download = file.name || '附件'; + a.target = '_blank'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); +} + +function h2IsPdfFileName(name) { + return /\.pdf$/i.test(String(name || '')); +} + +function h2EscapeSvgText(text) { + return String(text || '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + +function h2BuildPrototypeDocPreviewSvg(title, variant) { + var accent = variant === 'license' ? '#10b981' : '#3b82f6'; + var heading = variant === 'license' ? '证照预览' : '附件预览'; + var label = h2EscapeSvgText(String(title || '附件').slice(0, 28)); + var svg = '' + + '' + + '' + + '' + + '' + + '' + heading + '' + + '' + label + '' + + ''; + return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg); +} + +function h2ResolveLicenseDisplayUrl(file) { + var url = h2GetUploadFileUrl(file); + if (url) return url; + return h2BuildPrototypeDocPreviewSvg(file && file.name, 'license'); +} + +function h2ResolveUploadPreview(file, variant) { + var name = (file && file.name) || '附件'; + var url = h2GetUploadFileUrl(file); + var isPdf = h2IsPdfFileName(name); + if (url && isPdf) { + return { url: url, name: name, type: 'pdf' }; + } + if (url) { + return { url: url, name: name, type: 'image' }; + } + return { + url: h2BuildPrototypeDocPreviewSvg(name, variant || 'doc'), + name: name, + type: 'image' + }; +} + +/** 合同附件:按钮上传 + 预览/下载 */ +function ContractFilesUpload(props) { + var antd = window.antd; + var Upload = antd.Upload; + var Button = antd.Button; + var fileList = props.fileList || []; + var disabled = props.disabled; + var showHint = props.showHint !== false; + var wrapClass = 'h2-contract-upload' + (props.wrapClassName ? ' ' + props.wrapClassName : ''); + var uploadClass = props.uploadClassName || 'h2-create-upload-btn'; + + return React.createElement('div', { className: wrapClass }, + React.createElement('div', { className: 'h2-contract-upload-actions' }, + React.createElement(Upload, { + className: uploadClass, + multiple: true, + disabled: disabled, + fileList: fileList, + beforeUpload: function () { return false; }, + onChange: props.onChange, + showUploadList: false + }, + React.createElement(Button, { + type: props.buttonType || 'default', + icon: H2_ICONS.upload, + disabled: disabled, + className: props.buttonClassName, + style: props.buttonStyle + }, props.uploadLabel || '上传附件') + ), + showHint ? React.createElement('span', { style: { fontSize: 12, color: '#94a3b8' } }, '点击上传,可多选') : null + ), + fileList.length + ? React.createElement('div', { className: 'h2-contract-file-list' }, + fileList.map(function (file) { + return React.createElement('div', { key: file.uid, className: 'h2-contract-file-item' }, + React.createElement('span', { className: 'h2-contract-file-name', title: file.name }, file.name || '未命名附件'), + React.createElement('div', { className: 'h2-contract-file-btns' }, + React.createElement(Button, { + type: 'link', + size: 'small', + disabled: disabled, + onClick: function () { + var url = h2GetUploadFileUrl(file); + if (url) window.open(url, '_blank'); + } + }, '预览'), + React.createElement(Button, { + type: 'link', + size: 'small', + disabled: disabled, + onClick: function () { h2DownloadUploadFile(file); } + }, '下载'), + !disabled + ? React.createElement(Button, { + type: 'link', + size: 'small', + danger: true, + onClick: function () { + if (!props.onChange) return; + props.onChange({ + fileList: fileList.filter(function (f) { return f.uid !== file.uid; }) + }); + } + }, '删除') + : null + ) + ); + }) + ) + : null + ); +} + +function h2StationContactPhone(station) { + return (station.mobilePhone || '').trim() || (station.landlinePhone || '').trim(); +} + +function h2FilterContactPhoneInput(raw) { + var v = String(raw || '').replace(/[^\d-]/g, ''); + var digits = v.replace(/-/g, ''); + if (!digits) return ''; + if (digits.charAt(0) === '1') return digits.slice(0, 11); + var parts = v.split('-'); + if (parts.length > 2) v = parts[0] + '-' + parts.slice(1).join(''); + return v.slice(0, 16); +} + +function h2IsMobilePhone(text) { + var digits = String(text || '').replace(/[\s-]/g, ''); + return /^1[3-9]\d{9}$/.test(digits); +} + +function h2IsLandlinePhone(text) { + var raw = String(text || '').trim(); + if (!raw) return false; + var compact = raw.replace(/[\s-]/g, ''); + if (/^0\d{2,3}\d{7,8}$/.test(compact)) return true; + if (/^0\d{2,3}-\d{7,8}$/.test(raw)) return true; + if (/^\d{7,8}$/.test(compact)) return true; + return false; +} + +function h2IsValidContactPhone(text) { + var v = String(text || '').trim(); + if (!v) return false; + return h2IsMobilePhone(v) || h2IsLandlinePhone(v); +} + +function h2GetContactPhoneInputTip(text) { + var v = String(text || '').trim(); + if (!v) return ''; + if (h2IsValidContactPhone(v)) return ''; + var digits = v.replace(/[^\d]/g, ''); + if (digits.charAt(0) === '1') { + if (digits.length < 11) return '手机号码应为11位'; + return '手机号码格式不正确'; + } + if (digits.charAt(0) === '0') { + return '固定电话格式不正确,示例:0573-12345678'; + } + return '固定电话应为7-8位本地号码,或带区号如 0573-12345678'; +} + +function h2FilterMobilePhoneInput(raw) { + return String(raw || '').replace(/\D/g, '').slice(0, 11); +} + +function h2FilterLandlinePhoneInput(raw) { + var v = String(raw || '').replace(/[^\d-]/g, ''); + var parts = v.split('-'); + if (parts.length > 2) v = parts[0] + '-' + parts.slice(1).join(''); + return v.slice(0, 16); +} + +function h2GetMobilePhoneInputTip(text) { + var v = String(text || '').trim(); + if (!v) return ''; + if (h2IsMobilePhone(v)) return ''; + var digits = v.replace(/\D/g, ''); + if (digits.length < 11) return '手机号码应为11位'; + return '手机号码格式不正确'; +} + +function h2GetLandlinePhoneInputTip(text) { + var v = String(text || '').trim(); + if (!v) return ''; + if (h2IsLandlinePhone(v)) return ''; + return '固定电话格式不正确,示例:0573-12345678'; +} + +function h2ValidateStationPhones(station) { + var mobile = (station.mobilePhone || '').trim(); + var landline = (station.landlinePhone || '').trim(); + if (!mobile && !landline) return '请至少填写手机号或固定电话'; + if (mobile && !h2IsMobilePhone(mobile)) return h2GetMobilePhoneInputTip(mobile) || '手机号码格式不正确'; + if (landline && !h2IsLandlinePhone(landline)) return h2GetLandlinePhoneInputTip(landline) || '固定电话格式不正确'; + return ''; +} + +function h2ApplyStationContactPhone(station, text) { + var v = h2FilterContactPhoneInput(text); + var digits = v.replace(/-/g, ''); + if (/^1/.test(digits)) { + return Object.assign({}, station, { mobilePhone: v, landlinePhone: '' }); + } + return Object.assign({}, station, { mobilePhone: '', landlinePhone: v }); +} + +var H2_BUSINESS_HOURS_MODE_OPTIONS = [ + { label: '全天营业', value: 'allDay', tone: 'allday' }, + { label: '非全天营业', value: 'custom', tone: 'custom' } +]; + +var H2_BUSINESS_STATUS_BTN_OPTIONS = [ + { label: '营业中', value: '营业中', tone: 'open' }, + { label: '暂停营业', value: '暂停营业', tone: 'pause' }, + { label: '停止营业', value: '停止营业', tone: 'stop' } +]; + +function h2RenderOptionButtonGroup(options, value, onChange, opts) { + opts = opts || {}; + var groupClass = 'h2-option-btn-group' + (opts.className ? ' ' + opts.className : ''); + return React.createElement('div', { + className: groupClass, + role: 'group', + 'aria-label': opts.ariaLabel || '' + }, + (options || []).map(function (opt) { + var active = value === opt.value; + var btnClass = 'h2-option-btn' + (active ? ' h2-option-btn--active' : '') + (opt.tone ? ' h2-option-btn--' + opt.tone : ''); + return React.createElement('button', { + type: 'button', + key: String(opt.value), + className: btnClass, + 'aria-pressed': active, + disabled: !!opts.disabled, + onClick: function () { if (onChange) onChange(opt.value); } + }, opt.label); + }) + ); +} + +function h2IsAllDayBusinessHours(str) { + var s = String(str || '').trim(); + if (!s || s === '—') return false; + if (s === '全天营业' || s === '24小时' || s === '24 小时') return true; + return /^0?0:00\s*[-–]\s*24:00$/.test(s); +} + +function h2ParseBusinessHours(str) { + var s = String(str || '').trim(); + if (!s || s === '—') { + return { mode: 'allDay', start: '08:00', end: '22:00' }; + } + if (h2IsAllDayBusinessHours(s)) { + return { mode: 'allDay', start: '08:00', end: '22:00' }; + } + var parts = s.split('-'); + if (parts.length >= 2) { + return { + mode: 'custom', + start: (parts[0] || '').trim(), + end: (parts.slice(1).join('-') || '').trim() + }; + } + return { mode: 'custom', start: '', end: '' }; +} + +function h2FormatBusinessHours(val) { + if (!val || val.mode === 'allDay') return '00:00-24:00'; + var start = (val.start || '').trim(); + var end = (val.end || '').trim(); + if (!start || !end) return ''; + return start + '-' + end; +} + +function h2DisplayBusinessHours(str) { + var s = String(str || '').trim(); + if (!s || s === '—') return '—'; + if (h2IsAllDayBusinessHours(s) || s === '全天营业') return '00:00-24:00'; + var parsed = h2ParseBusinessHours(s); + if (parsed.mode === 'custom' && parsed.start && parsed.end) return parsed.start + '-' + parsed.end; + return s; +} + +function h2NormalizeTimeText(text) { + var s = String(text || '').trim(); + if (!s) return ''; + var m = /^(\d{1,2}):(\d{2})$/.exec(s); + if (!m) return s; + var h = parseInt(m[1], 10); + var min = parseInt(m[2], 10); + if (isNaN(h) || isNaN(min) || h < 0 || h > 23 || min < 0 || min > 59) return s; + return (h < 10 ? '0' : '') + h + ':' + (min < 10 ? '0' : '') + min; +} + +function h2TimeTextToDayjs(text) { + var dayjs = window.dayjs; + if (!dayjs) return null; + var norm = h2NormalizeTimeText(text); + if (!norm) return null; + var parts = norm.split(':'); + return dayjs().hour(parseInt(parts[0], 10)).minute(parseInt(parts[1], 10)).second(0).millisecond(0); +} + +function h2DayjsToTimeText(d) { + var dayjs = window.dayjs; + if (!d || !dayjs) return ''; + return dayjs(d).format('HH:mm'); +} + +/** 营业时间:全天 / 非全天按钮组 + 起止时间(时:分,可输入) */ +function BusinessHoursInput(props) { + var antd = window.antd; + var TimePicker = antd.TimePicker; + var Input = antd.Input; + var parsed = h2ParseBusinessHours(props.value); + var onChange = props.onChange; + var disabled = props.disabled; + var inputClassName = props.inputClassName || ''; + var fieldStyle = props.fieldStyle || { width: '100%' }; + var layout = props.layout || 'full'; + var dayjs = window.dayjs; + var rootClass = 'h2-business-hours' + (layout === 'col4' ? ' h2-business-hours--col4' : ''); + + var emit = function (next) { + onChange && onChange(h2FormatBusinessHours(next)); + }; + + var renderTimeField = function (label, timeKey, placeholder) { + var timeVal = parsed[timeKey] || ''; + var onTimeChange = function (nextText) { + var patch = { mode: 'custom', start: parsed.start || '', end: parsed.end || '' }; + patch[timeKey] = h2NormalizeTimeText(nextText); + emit(patch); + }; + return React.createElement('div', { className: 'h2-business-hours-range-item', key: timeKey }, + React.createElement('span', { className: 'h2-business-hours-range-label' }, label), + dayjs + ? React.createElement(TimePicker, { + className: inputClassName, + format: 'HH:mm', + minuteStep: 1, + hourStep: 1, + showNow: false, + allowClear: !disabled, + inputReadOnly: false, + disabled: disabled, + style: fieldStyle, + placeholder: placeholder, + value: h2TimeTextToDayjs(timeVal), + onChange: function (d) { onTimeChange(d ? h2DayjsToTimeText(d) : ''); } + }) + : React.createElement(Input, { + className: inputClassName, + disabled: disabled, + style: fieldStyle, + placeholder: placeholder || 'HH:mm', + value: timeVal, + maxLength: 5, + onChange: function (e) { onTimeChange(e.target.value); }, + onBlur: function (e) { onTimeChange(e.target.value); } + }) + ); + }; + + return React.createElement('div', { className: rootClass }, + h2RenderOptionButtonGroup(H2_BUSINESS_HOURS_MODE_OPTIONS, parsed.mode, function (mode) { + if (mode === 'allDay') emit({ mode: 'allDay', start: parsed.start, end: parsed.end }); + else emit({ + mode: 'custom', + start: parsed.start || '08:00', + end: parsed.end || '22:00' + }); + }, { ariaLabel: '营业时间类型', disabled: disabled }), + parsed.mode === 'custom' + ? React.createElement('div', { className: 'h2-business-hours-custom-panel' }, + React.createElement('div', { className: 'h2-business-hours-custom-panel__title' }, '营业时段'), + React.createElement('div', { className: 'h2-business-hours-range' }, + renderTimeField('开始时间', 'start', '请选择开始时间'), + renderTimeField('结束时间', 'end', '请选择结束时间') + ) + ) + : null + ); +} + +function h2CreateEmptyForm() { + return { + name: '', + address: h2EmptyAddressValue(), + isSigned: true, + contractStart: '', + contractEnd: '', + contractFiles: [], + businessStatus: '营业中', + businessHours: '', + contact: '', + phone: '' + }; +} + +function h2RecordToForm(record) { + if (!record) return h2CreateEmptyForm(); + return { + name: record.name || '', + address: { + region: (record.region || []).slice(), + detail: record.addressDetail || '' + }, + isSigned: record.isSigned !== false, + contractStart: record.contractStart || '', + contractEnd: record.contractEnd || '', + contractFiles: (record.contractFiles || []).map(function (f, i) { + return Object.assign({}, f, { uid: f.uid || ('f-' + i), status: 'done' }); + }), + businessStatus: record.businessStatus || '营业中', + businessHours: record.businessHours || '', + contact: record.contact || '', + phone: record.phone || '' + }; +} + +function h2FormToRecord(form, id) { + var files = (form.contractFiles || []).map(function (f) { + return { uid: f.uid, name: f.name || '合同附件.pdf', url: f.url || '' }; + }); + return { + id: id, + name: (form.name || '').trim(), + region: form.address.region || [], + addressDetail: (form.address.detail || '').trim(), + fullAddress: h2BuildFullAddress(form.address.region, form.address.detail), + isSigned: !!form.isSigned, + contractStart: form.contractStart || '', + contractEnd: form.contractEnd || '', + contractFiles: files, + businessStatus: form.businessStatus || '营业中', + businessHours: (form.businessHours || '').trim(), + contact: (form.contact || '').trim(), + phone: (form.phone || '').trim(), + updateTime: new Date().toISOString().slice(0, 16).replace('T', ' ') + }; +} + +var H2_MOCK_STATIONS = [ + { + id: 1, + name: '嘉兴加氢站(一期)', + region: ['浙江省', '嘉兴市'], + addressDetail: '南湖区科技大道88号', + fullAddress: '浙江省-嘉兴市南湖区科技大道88号', + mapAddress: '浙江省嘉兴市南湖区科技大道88号', + latitude: 30.7512, + longitude: 120.7588, + isSigned: true, + contractStart: '2024-01-01', + contractEnd: '2026-12-31', + contractFiles: [{ uid: 'c1', name: '嘉兴加氢站签约合同.pdf' }], + businessStatus: '营业中', + businessHours: '08:00-22:00', + costUnitPrice: 42.5, + customerUnitPrice: 45, + costPriceLogs: [ + { id: 'cpl-1-1', operator: '王静', operateTime: '2026-01-10 09:00', beforeCostPrice: null, afterCostPrice: 41.0, effectiveTime: '2026-01-15 00:00', priceType: 'fixed', customerScope: 'all' }, + { id: 'cpl-1-2', operator: '系统管理员', operateTime: '2026-05-01 14:20', beforeCostPrice: 41.0, afterCostPrice: 42.5, effectiveTime: '2026-05-05 00:00', priceType: 'fixed', customerScope: 'all' } + ], + priceConfigs: [ + { + id: 'pc-default', + priceType: 'fixed', + customerScope: 'all', + customerId: null, + customerName: null, + fixedPrice: 42.5, + tierRules: [], + tierResetDay: 1, + tierPeriodVolumeKg: 0, + tierPeriodKey: '2026-06', + logs: [ + { id: 'cpl-1-1', operator: '王静', operateTime: '2026-01-10 09:00', beforeCostPrice: null, afterCostPrice: 41.0, effectiveTime: '2026-01-15 00:00', priceType: 'fixed', customerScope: 'all' }, + { id: 'cpl-1-2', operator: '系统管理员', operateTime: '2026-05-01 14:20', beforeCostPrice: 41.0, afterCostPrice: 42.5, effectiveTime: '2026-05-05 00:00', priceType: 'fixed', customerScope: 'all' } + ] + }, + { + id: 'pc-tier-xf', + priceType: 'tiered', + customerScope: 'customer', + customerId: 'cust-嘉兴市鑫峤供应链科技有限公司', + customerName: '嘉兴市鑫峤供应链科技有限公司', + fixedPrice: null, + tierRules: [ + { id: 'tr-xf-1', thresholdKg: 0, price: 43.0 }, + { id: 'tr-xf-2', thresholdKg: 300, price: 41.5 }, + { id: 'tr-xf-3', thresholdKg: 600, price: 40.0 } + ], + tierResetDay: 1, + tierPeriodVolumeKg: 312.5, + tierPeriodKey: '2026-06', + logs: [ + { id: 'cpl-xf-1', operator: '王静', operateTime: '2026-05-10 10:00', beforeCostPrice: null, afterCostPrice: 43.0, effectiveTime: '2026-05-10 10:00', priceType: 'tiered', customerScope: 'customer', customerName: '嘉兴市鑫峤供应链科技有限公司', tierRulesSummary: '≥0.00kg → 43.00元/kg;≥300.00kg → 41.50元/kg;≥600.00kg → 40.00元/kg' } + ] + } + ], + prepaidBalance: 85620.0, + balanceAlertThreshold: 50000, + contact: '张三', + phone: '13800138001', + businessStatusLogs: [], + supplierMode: 'link', + linkedSupplierId: 'sup-h2-1', + bindAccountMode: 'bind', + boundAccountId: 'acc-h2-1', + boundAccount: { + id: 'acc-h2-1', + username: 'jx_h2_admin', + displayName: '嘉兴站管理员', + role: '加氢站管理员', + mobile: '13800138001', + status: '启用' + }, + updateTime: '2026-05-20 14:30' + }, + { + id: 2, + name: '杭州临平加氢站', + region: ['浙江省', '杭州市'], + addressDetail: '临平区东湖街道能源路16号', + fullAddress: '浙江省-杭州市临平区东湖街道能源路16号', + isSigned: true, + contractStart: '2025-03-01', + contractEnd: '2027-02-28', + contractFiles: [{ uid: 'c2', name: '临平加氢站合作协议.pdf' }, { uid: 'c3', name: '临平加氢站补充协议.pdf' }], + businessStatus: '营业中', + businessHours: '00:00-24:00', + costUnitPrice: 43.0, + customerUnitPrice: 46, + costPriceLogs: [ + { id: 'cpl-2-1', operator: '李四', operateTime: '2026-05-20 11:00', beforeCostPrice: 42.0, afterCostPrice: 43.0, effectiveTime: '2026-05-25 00:00' } + ], + prepaidBalance: 125000.5, + contact: '李四', + phone: '13900139002', + supplierMode: 'new', + supplier: { + signingCompany: '浙江羚牛氢能科技有限公司', + name: '杭州临平氢能运营有限公司', + type: '加氢站', + city: ['浙江省', '杭州市'], + address: '临平区东湖街道能源路16号', + region: '华东', + contactName: '李四', + contactMobile: '13900139002', + contactTitle: '站长', + taxId: '91330110MA3LINPING1', + invoiceAddress: '浙江省杭州市临平区东湖街道能源路16号', + invoicePhone: '0571-88882222', + bankName: '中国农业银行杭州临平支行', + bankAccount: '6228480402631234567', + mailingAddress: '浙江省杭州市临平区东湖街道能源路16号', + businessAddress: '浙江省杭州市临平区东湖街道能源路16号', + businessLicenseFiles: [{ uid: 'bl-hz', name: '营业执照_临平氢能.pdf' }], + fillingLicenseFiles: [ + { uid: 'fl-hz-1', name: '加氢站经营许可证.pdf' }, + { uid: 'fl-hz-2', name: '压力容器检验报告.pdf' } + ] + }, + updateTime: '2026-06-01 09:15' + }, + { + id: 3, + name: '上海宝山加氢站', + region: ['上海市', '上海市'], + addressDetail: '宝山区富联路128号', + fullAddress: '上海市-上海市宝山区富联路128号', + isSigned: true, + contractStart: '2023-06-01', + contractEnd: '2025-05-31', + contractFiles: [{ uid: 'c4', name: '宝山加氢站合同(即将到期).pdf' }], + businessStatus: '暂停营业', + businessHours: '09:00-18:00', + costUnitPrice: 44.0, + customerUnitPrice: 47, + costPriceLogs: [ + { id: 'cpl-3-1', operator: '王五', operateTime: '2026-03-01 10:30', beforeCostPrice: 43.5, afterCostPrice: 44.0, effectiveTime: '2026-03-05 00:00' } + ], + prepaidBalance: -3250.0, + balanceAlertThreshold: 10000, + businessStatusLogs: [ + { id: 'bsl-3-2', operateTime: '2026-04-01 10:00', operator: '王五', beforeStatus: '营业中', afterStatus: '暂停营业' }, + { id: 'bsl-3-1', operateTime: '2026-03-15 09:30', operator: '系统管理员', beforeStatus: '暂停营业', afterStatus: '营业中' } + ], + contact: '王五', + phone: '13700137003', + supplierMode: 'link', + linkedSupplierId: 'sup-h2-2', + bindAccountMode: 'bind', + boundAccountId: 'acc-h2-3', + boundAccount: { + id: 'acc-h2-3', + username: 'sh_bs_h2', + displayName: '宝山站管理员', + role: '加氢站管理员', + mobile: '13700137003', + status: '启用' + }, + updateTime: '2026-04-12 16:40' + }, + { + id: 4, + name: '平湖合作停车场(待建加氢)', + region: ['浙江省', '嘉兴市'], + addressDetail: '平湖市乍嘉公路与平善大道交叉口', + fullAddress: '浙江省-嘉兴市平湖市乍嘉公路与平善大道交叉口', + isSigned: false, + contractStart: '', + contractEnd: '', + contractFiles: [], + businessStatus: '停止营业', + businessHours: '—', + prepaidBalance: 0, + contact: '赵六', + phone: '13600136004', + updateTime: '2026-03-08 11:20' + }, + { + id: 5, + name: '苏州工业园区备用站', + region: ['江苏省', '苏州市'], + addressDetail: '工业园区星湖街328号', + fullAddress: '江苏省-苏州市工业园区星湖街328号', + isSigned: false, + contractStart: '', + contractEnd: '', + contractFiles: [], + businessStatus: '停止营业', + businessHours: '10:00-17:00', + prepaidBalance: 15800.0, + balanceAlertThreshold: 20000, + contact: '钱七', + phone: '13500135005', + supplierMode: 'link', + linkedSupplierId: 'sup-h2-3', + updateTime: '2026-02-18 08:50' + }, + { + id: 6, + name: '桐乡合作加氢站', + region: ['浙江省', '嘉兴市'], + addressDetail: '桐乡市梧桐街道庆丰路66号', + fullAddress: '浙江省-嘉兴市桐乡市梧桐街道庆丰路66号', + isSigned: true, + contractStart: '2025-06-01', + contractEnd: '2027-05-31', + contractFiles: [], + businessStatus: '营业中', + businessHours: '08:00-20:00', + costUnitPrice: 41.8, + customerUnitPrice: 44.5, + costPriceLogs: [], + prepaidBalance: 42000.0, + contact: '周八', + phone: '13800138006', + updateTime: '2026-05-10 15:20' + } +]; + +/** 解析站点关联的供应商(内嵌 supplier 或 linkedSupplierId) */ +function h2ResolveStationSupplier(record) { + if (!record) return null; + if (record.supplier && ((record.supplier.name || '').trim() || (record.supplier.taxId || '').trim())) { + return Object.assign({ type: '加氢站' }, record.supplier); + } + if (record.linkedSupplierId) { + var i; + for (i = 0; i < H2_MOCK_EXISTING_SUPPLIERS.length; i++) { + if (H2_MOCK_EXISTING_SUPPLIERS[i].id === record.linkedSupplierId) { + return H2_MOCK_EXISTING_SUPPLIERS[i]; + } + } + } + return null; +} + +var H2_SETTLEMENT_STATUS_OPTIONS = [ + { value: 'customer', label: '客户承担' }, + { value: 'internal', label: '我司承担' }, + { value: 'customer_self', label: '客户自行结算' } +]; + +var H2_SETTLEMENT_STATUS_MAP = { + customer: { label: '客户承担', color: 'blue' }, + internal: { label: '我司承担', color: 'green' }, + customer_self: { label: '客户自行结算', color: 'orange' } +}; + +function h2SettlementStatusLabel(value) { + var info = H2_SETTLEMENT_STATUS_MAP[value]; + return info ? info.label : '—'; +} + +/** 站点加氢记录 Mock(字段对齐车辆氢费明细) */ +var H2_MOCK_REFUEL_RECORDS = [ + { id: 'rf-1', stationName: '嘉兴加氢站(一期)', hydrogenTime: '2026-05-28 10:21:08', plateNo: '浙A12345F', customerName: '嘉兴市鑫峤供应链科技有限公司', hydrogenKg: 12.5, costUnitPrice: 42.5, costAmount: 531.25, customerUnitPrice: 45, customerAmount: 562.5, settlementStatus: 'customer' }, + { id: 'rf-2', stationName: '嘉兴加氢站(一期)', hydrogenTime: '2026-05-26 14:08:33', plateNo: '浙A67890F', customerName: '浙江绿运物流有限公司', hydrogenKg: 10.0, costUnitPrice: 42.5, costAmount: 425.0, customerUnitPrice: 45, customerAmount: 450.0, settlementStatus: 'internal' }, + { id: 'rf-3', stationName: '嘉兴加氢站(一期)', hydrogenTime: '2026-05-22 09:15:00', plateNo: '浙A88888F', customerName: '嘉兴市鑫峤供应链科技有限公司', hydrogenKg: 18.3, costUnitPrice: 42.5, costAmount: 777.75, customerUnitPrice: 45, customerAmount: 823.5, settlementStatus: 'customer_self' }, + { id: 'rf-4', stationName: '嘉兴加氢站(一期)', hydrogenTime: '2026-05-18 16:42:11', plateNo: '浙A03561F', customerName: '嘉兴港务氢能运输队', hydrogenKg: 15.6, costUnitPrice: 42.5, costAmount: 663.0, customerUnitPrice: 45, customerAmount: 702.0, settlementStatus: 'customer' }, + { id: 'rf-5', stationName: '杭州临平加氢站', hydrogenTime: '2026-05-30 09:30:22', plateNo: '浙B23456F', customerName: '杭州临平城配中心', hydrogenKg: 15.3, costUnitPrice: 43.0, costAmount: 657.9, customerUnitPrice: 46, customerAmount: 703.8, settlementStatus: 'internal' }, + { id: 'rf-6', stationName: '杭州临平加氢站', hydrogenTime: '2026-05-27 18:10:05', plateNo: '浙B99999F', customerName: '浙江氢运科技', hydrogenKg: 18.2, costUnitPrice: 43.0, costAmount: 782.6, customerUnitPrice: 46, customerAmount: 837.2, settlementStatus: 'customer_self' }, + { id: 'rf-7', stationName: '杭州临平加氢站', hydrogenTime: '2026-05-24 11:05:40', plateNo: '浙B58888F', customerName: '杭州临平城配中心', hydrogenKg: 11.8, costUnitPrice: 43.0, costAmount: 507.4, customerUnitPrice: 46, customerAmount: 542.8, settlementStatus: 'customer' }, + { id: 'rf-8', stationName: '上海宝山加氢站', hydrogenTime: '2026-04-20 16:45:18', plateNo: '沪A88888F', customerName: '上海羚牛氢运', hydrogenKg: 8.0, costUnitPrice: 44.0, costAmount: 352.0, customerUnitPrice: 47, customerAmount: 376.0, settlementStatus: 'internal' }, + { id: 'rf-9', stationName: '上海宝山加氢站', hydrogenTime: '2026-04-08 09:12:55', plateNo: '沪BDB9161F', customerName: '宝山园区试运车队', hydrogenKg: 9.5, costUnitPrice: 44.0, costAmount: 418.0, customerUnitPrice: 47, customerAmount: 446.5, settlementStatus: 'customer_self' }, + { id: 'rf-10', stationName: '苏州工业园区备用站', hydrogenTime: '2026-03-15 10:00:00', plateNo: '苏E33333F', customerName: '苏州试运客户', hydrogenKg: 6.2, costUnitPrice: 41.0, costAmount: 254.2, customerUnitPrice: 44, customerAmount: 272.8, settlementStatus: 'customer' } +]; + +function h2FormatKgNum(v) { + var n = typeof v === 'number' ? v : parseFloat(v); + return isNaN(n) ? '0.00' : n.toFixed(2); +} + +function h2FormatYuanNum(v) { + var n = typeof v === 'number' ? v : parseFloat(v); + return isNaN(n) ? '0.00' : n.toFixed(2); +} + +function h2FormatYuanSymbol(v, options) { + var opts = options || {}; + if (v == null || v === '') return '—'; + if (!opts.keepZero && Number(v) === 0) return '—'; + var n = typeof v === 'number' ? v : parseFloat(v); + if (isNaN(n)) return '—'; + return '¥' + n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); +} + +var H2_BALANCE_BIZ_TYPE_MAP = { + recharge: { key: 'recharge', label: '充值', color: 'success' }, + refuel: { key: 'refuel', label: '车辆加氢', color: 'warning' }, + ending: { key: 'ending', label: '期末', color: 'default' } +}; + +function h2Pad2(n) { + return String(n).padStart(2, '0'); +} + +function h2BuildBalanceChangeTime(seqIndex) { + var day = Math.max(1, 28 - seqIndex * 3); + var hour = 8 + ((seqIndex * 3) % 12); + var minute = (seqIndex * 17) % 60; + return '2026-05-' + h2Pad2(day) + ' ' + h2Pad2(hour) + ':' + h2Pad2(minute) + ':00'; +} + +function h2ResolveBalanceBizType(row) { + if (row && row.bizType && H2_BALANCE_BIZ_TYPE_MAP[row.bizType]) { + return H2_BALANCE_BIZ_TYPE_MAP[row.bizType]; + } + if (row && row.incomeAmount != null && row.incomeAmount !== '') return H2_BALANCE_BIZ_TYPE_MAP.recharge; + if (row && row.expenseAmount != null && row.expenseAmount !== '') return H2_BALANCE_BIZ_TYPE_MAP.refuel; + return H2_BALANCE_BIZ_TYPE_MAP.ending; +} + +function h2FormatBalanceExportAmount(v) { + if (v == null || v === '') return ''; + return h2FormatYuanNum(v); +} + +function h2ExportBalanceDrillCsv(stationName, rows) { + var headers = ['序号', '时间', '类型', '收入(元)', '支出(元)', '余额(元)', '订单编号']; + var csvRows = (rows || []).map(function (r, index) { + var typeInfo = h2ResolveBalanceBizType(r); + return [ + index + 1, + r.changeTime || '', + typeInfo.label, + h2FormatBalanceExportAmount(r.incomeAmount), + h2FormatBalanceExportAmount(r.expenseAmount), + h2FormatBalanceExportAmount(r.balance), + r.orderNo || '' + ]; + }); + var safeName = String(stationName || '站点').replace(/[\\/:*?"<>|]/g, '_'); + h2DownloadCsv('余额变更明细_' + safeName + '_' + new Date().getTime() + '.csv', headers, csvRows); +} + +function h2NumOrZero(v) { + var n = typeof v === 'number' ? v : parseFloat(v); + return isNaN(n) ? 0 : n; +} + +function h2FormatLedgerMoney(v) { + if (v === null || v === undefined || v === '' || Number(v) === 0) return '—'; + return h2FormatYuanNum(v); +} + +/** 原型:加氢站预付余额变更明细(联调后对接加氢站打款/余额流水接口) */ +function h2BuildMockPrepaidBalanceRows(record) { + var seed = 0; + var code = String(record && record.id != null ? record.id : ''); + var nameKey = String((record && record.name) || ''); + var i; + for (i = 0; i < code.length; i++) seed += code.charCodeAt(i); + for (i = 0; i < nameKey.length; i++) seed += nameKey.charCodeAt(i); + seed = seed % 100; + var stationName = (record && record.name) || '—'; + var finalBalance = h2NumOrZero(record && record.prepaidBalance); + var lineCount = Math.max(5, 6 + (seed % 4)); + var rows = []; + var openingBalance = Math.round((42000 + seed * 520) * 100) / 100; + if (openingBalance < 0) openingBalance = Math.abs(openingBalance) + 30000; + + rows.push({ + key: code + '-bal-0', + stationName: stationName, + changeTime: '2026-01-01 00:00:00', + bizType: 'recharge', + incomeAmount: openingBalance, + expenseAmount: null, + balance: openingBalance, + orderNo: 'QC' + code + '260101' + }); + + var balance = openingBalance; + for (i = 1; i < lineCount - 1; i++) { + var isIncome = (seed + i) % 3 === 0; + var incomeAmount = null; + var expenseAmount = null; + if (isIncome) { + incomeAmount = Math.round((5000 + (seed + i) * 42) * 100) / 100; + balance = Math.round((balance + incomeAmount) * 100) / 100; + } else { + expenseAmount = Math.round((2200 + (seed + i) * 26) * 100) / 100; + balance = Math.round((balance - expenseAmount) * 100) / 100; + } + rows.push({ + key: code + '-bal-' + i, + stationName: stationName, + changeTime: h2BuildBalanceChangeTime(i), + bizType: isIncome ? 'recharge' : 'refuel', + incomeAmount: incomeAmount, + expenseAmount: expenseAmount, + balance: balance, + orderNo: (isIncome ? 'DK' : 'JQ') + code + String(200 + i).padStart(5, '0') + }); + } + + var diff = Math.round((finalBalance - balance) * 100) / 100; + if (Math.abs(diff) < 0.01) { + rows.push({ + key: code + '-bal-last', + stationName: stationName, + changeTime: '2026-06-01 23:59:59', + bizType: 'ending', + incomeAmount: null, + expenseAmount: null, + balance: finalBalance, + orderNo: 'BZ' + code + String(900).padStart(5, '0') + }); + } else if (diff > 0) { + rows.push({ + key: code + '-bal-last', + stationName: stationName, + changeTime: '2026-06-01 18:30:00', + bizType: 'recharge', + incomeAmount: diff, + expenseAmount: null, + balance: finalBalance, + orderNo: 'DK' + code + String(999).padStart(5, '0') + }); + } else { + rows.push({ + key: code + '-bal-last', + stationName: stationName, + changeTime: '2026-06-01 18:30:00', + bizType: 'refuel', + incomeAmount: null, + expenseAmount: Math.round(Math.abs(diff) * 100) / 100, + balance: finalBalance, + orderNo: 'JQ' + code + String(999).padStart(5, '0') + }); + } + + return rows; +} + +function h2FormatBalanceTrendLabel(changeTime) { + if (!changeTime) return '—'; + var text = String(changeTime).trim(); + if (text.length >= 10) return text.slice(5, 10); + return text; +} + +function h2ParseChangeTimeMs(changeTime) { + if (!changeTime) return 0; + var text = String(changeTime).trim(); + var ms = Date.parse(text.indexOf('T') === -1 ? text.replace(' ', 'T') : text); + return isNaN(ms) ? 0 : ms; +} + +function h2ExtractBalanceDateKey(changeTime) { + if (!changeTime) return ''; + var text = String(changeTime).trim(); + return text.length >= 10 ? text.slice(0, 10) : text; +} + +function h2FormatDateOnlyFromDate(date) { + return date.getFullYear() + '-' + String(date.getMonth() + 1).padStart(2, '0') + '-' + String(date.getDate()).padStart(2, '0'); +} + +function h2GetYesterdayDateOnly() { + var d = new Date(); + d.setHours(0, 0, 0, 0); + d.setDate(d.getDate() - 1); + return h2FormatDateOnlyFromDate(d); +} + +function h2AddMonths(dateStr, months) { + var ms = h2ParseDateOnlyMs(dateStr); + if (isNaN(ms)) return dateStr; + var d = new Date(ms); + d.setMonth(d.getMonth() + (months || 0)); + return h2FormatDateOnlyFromDate(d); +} + +/** 趋势图固定展示近三个月:结束日为昨日,起始日为昨日往前推 3 个月 */ +function h2ResolveBalanceTrendRange() { + var endDate = h2GetYesterdayDateOnly(); + var startDate = h2AddMonths(endDate, -3); + return { startDate: startDate, endDate: endDate }; +} + +/** 流水按日聚合为趋势点:每日取末笔余额,无流水日沿用前一日余额 */ +function h2BuildBalanceTrendSeries(rows) { + var range = h2ResolveBalanceTrendRange(); + var startDate = range.startDate; + var endDate = range.endDate; + var startMs = h2ParseDateOnlyMs(startDate); + var endMs = h2ParseDateOnlyMs(endDate); + if (!rows || !rows.length || isNaN(startMs) || isNaN(endMs)) { + return { + points: [], + min: 0, + max: 0, + hasNegative: false, + granularity: 'day', + startDate: startDate, + endDate: endDate + }; + } + var sorted = rows.slice().sort(function (a, b) { + return h2ParseChangeTimeMs(a.changeTime) - h2ParseChangeTimeMs(b.changeTime); + }); + var dayBuckets = {}; + var i; + for (i = 0; i < sorted.length; i++) { + var row = sorted[i]; + var dateKey = h2ExtractBalanceDateKey(row.changeTime); + if (!dateKey) continue; + if (!dayBuckets[dateKey]) { + dayBuckets[dateKey] = row; + } else if (h2ParseChangeTimeMs(row.changeTime) >= h2ParseChangeTimeMs(dayBuckets[dateKey].changeTime)) { + dayBuckets[dateKey] = row; + } + } + var seedBalance = null; + for (i = sorted.length - 1; i >= 0; i--) { + var seedKey = h2ExtractBalanceDateKey(sorted[i].changeTime); + if (seedKey && h2ParseDateOnlyMs(seedKey) < startMs) { + seedBalance = h2NumOrZero(sorted[i].balance); + break; + } + } + var points = []; + var lastBalance = seedBalance; + var cursor = startDate; + var guard = 0; + while (guard < 4000) { + guard += 1; + var cursorMs = h2ParseDateOnlyMs(cursor); + if (isNaN(cursorMs) || cursorMs > endMs) break; + if (dayBuckets[cursor]) { + lastBalance = h2NumOrZero(dayBuckets[cursor].balance); + } + if (lastBalance != null) { + points.push({ + key: 'day-' + cursor, + label: h2FormatBalanceTrendLabel(cursor + ' 00:00:00'), + fullTime: cursor, + balance: lastBalance + }); + } + if (cursor === endDate) break; + cursor = h2AddDays(cursor, 1); + } + if (!points.length) { + return { + points: [], + min: 0, + max: 0, + hasNegative: false, + granularity: 'day', + startDate: startDate, + endDate: endDate + }; + } + var min = points[0].balance; + var max = points[0].balance; + var hasNegative = false; + for (i = 0; i < points.length; i++) { + if (points[i].balance < min) min = points[i].balance; + if (points[i].balance > max) max = points[i].balance; + if (points[i].balance < 0) hasNegative = true; + } + var pad = Math.max((max - min) * 0.12, max === min ? Math.abs(max) * 0.1 || 500 : 0); + if (hasNegative && min > 0) min = 0; + return { + points: points, + min: min - pad, + max: max + pad, + hasNegative: hasNegative, + granularity: 'day', + startDate: startDate, + endDate: endDate + }; +} + +function H2BalanceTrendChart(props) { + var series = props.series || {}; + var points = series.points || []; + var hoverState = React.useState(null); + var hoverIndex = hoverState[0]; + var setHoverIndex = hoverState[1]; + if (!points.length) { + return React.createElement('div', { className: 'h2-balance-trend-empty' }, '暂无趋势数据'); + } + var W = 820; + var H = 220; + var PL = 56; + var PR = 20; + var PT = 18; + var PB = 36; + var chartW = W - PL - PR; + var chartH = H - PT - PB; + var min = series.min; + var max = series.max; + var range = max - min || 1; + var coords = points.map(function (p, idx) { + var x = PL + (points.length === 1 ? chartW / 2 : (idx / (points.length - 1)) * chartW); + var y = PT + chartH - ((p.balance - min) / range) * chartH; + return { x: x, y: y, label: p.label, balance: p.balance, fullTime: p.fullTime, key: p.key }; + }); + var linePath = coords.map(function (c, i) { + return (i === 0 ? 'M' : 'L') + c.x.toFixed(1) + ' ' + c.y.toFixed(1); + }).join(' '); + var areaPath = linePath + + ' L' + coords[coords.length - 1].x.toFixed(1) + ' ' + (PT + chartH) + + ' L' + coords[0].x.toFixed(1) + ' ' + (PT + chartH) + + ' Z'; + var zeroY = null; + if (min < 0 && max > 0) { + zeroY = PT + chartH - ((0 - min) / range) * chartH; + } + var yTicks = [ + { value: max, y: PT }, + { value: min + range / 2, y: PT + chartH / 2 }, + { value: min, y: PT + chartH } + ]; + var xLabels = [ + { x: coords[0].x, label: coords[0].label }, + { x: coords[coords.length - 1].x, label: coords[coords.length - 1].label } + ]; + var pickNearestIndex = function (mouseX) { + var nearest = 0; + var minDist = Infinity; + var j; + for (j = 0; j < coords.length; j++) { + var dist = Math.abs(coords[j].x - mouseX); + if (dist < minDist) { + minDist = dist; + nearest = j; + } + } + return nearest; + }; + var handleChartMouseMove = function (e) { + var rect = e.currentTarget.getBoundingClientRect(); + if (!rect.width) return; + var mouseX = ((e.clientX - rect.left) / rect.width) * W; + if (mouseX < PL - 8 || mouseX > W - PR + 8) { + setHoverIndex(null); + return; + } + setHoverIndex(pickNearestIndex(mouseX)); + }; + var active = hoverIndex != null && coords[hoverIndex] ? coords[hoverIndex] : null; + var tooltipNode = active + ? React.createElement('div', { + className: 'h2-balance-trend-tooltip', + style: { + left: (active.x / W * 100) + '%', + top: (active.y / H * 100) + '%' + }, + role: 'tooltip' + }, + React.createElement('div', { className: 'h2-balance-trend-tooltip__date' }, active.fullTime || '—'), + React.createElement('div', { + className: 'h2-balance-trend-tooltip__balance' + (active.balance < 0 ? ' h2-balance-trend-tooltip__balance--negative' : '') + }, h2FormatYuanSymbol(active.balance, { keepZero: true })) + ) + : null; + + return React.createElement('div', { + className: 'h2-balance-trend-chart-wrap', + onMouseLeave: function () { setHoverIndex(null); } + }, + tooltipNode, + React.createElement('svg', { + className: 'h2-balance-trend-svg', + viewBox: '0 0 ' + W + ' ' + H, + role: 'img', + 'aria-label': '预付余额趋势图', + onMouseMove: handleChartMouseMove, + onMouseLeave: function () { setHoverIndex(null); } + }, + React.createElement('defs', null, + React.createElement('linearGradient', { id: 'h2BalanceTrendArea', x1: '0', y1: '0', x2: '0', y2: '1' }, + React.createElement('stop', { offset: '0%', stopColor: '#10b981', stopOpacity: '0.28' }), + React.createElement('stop', { offset: '100%', stopColor: '#10b981', stopOpacity: '0.02' }) + ) + ), + yTicks.map(function (tick, idx) { + return React.createElement('g', { key: 'yt-' + idx }, + React.createElement('line', { + x1: PL, y1: tick.y, x2: W - PR, y2: tick.y, + stroke: '#f1f5f9', strokeWidth: 1 + }), + React.createElement('text', { + x: PL - 8, y: tick.y + 4, + textAnchor: 'end', fontSize: 10, fill: '#94a3b8', fontFamily: 'ui-sans-serif, system-ui, sans-serif' + }, h2FormatYuanNum(tick.value)) + ); + }), + zeroY != null + ? React.createElement('line', { + x1: PL, y1: zeroY, x2: W - PR, y2: zeroY, + stroke: '#fca5a5', strokeWidth: 1, strokeDasharray: '4 4' + }) + : null, + active + ? React.createElement('line', { + x1: active.x, y1: PT, x2: active.x, y2: PT + chartH, + stroke: '#a7f3d0', strokeWidth: 1, strokeDasharray: '3 3' + }) + : null, + React.createElement('path', { d: areaPath, fill: 'url(#h2BalanceTrendArea)' }), + React.createElement('path', { + d: linePath, fill: 'none', stroke: '#10b981', strokeWidth: 2.5, strokeLinejoin: 'round', strokeLinecap: 'round' + }), + coords.map(function (c, idx) { + var negative = c.balance < 0; + var isActive = hoverIndex === idx; + return React.createElement('g', { key: c.key }, + React.createElement('circle', { + className: 'h2-balance-trend-hit', + cx: c.x, cy: c.y, r: 10, + onMouseEnter: function () { setHoverIndex(idx); } + }), + React.createElement('circle', { + className: isActive ? 'h2-balance-trend-point--active' : '', + cx: c.x, cy: c.y, r: isActive ? 5.5 : 4, + fill: '#fff', + stroke: negative ? '#dc2626' : '#10b981', + strokeWidth: isActive ? 2.5 : 2, + pointerEvents: 'none' + }) + ); + }), + xLabels.map(function (item, idx) { + return React.createElement('text', { + key: 'xl-' + idx, + x: item.x, + y: H - 10, + textAnchor: idx === 0 ? 'start' : 'end', + fontSize: 10, + fill: '#64748b', + fontFamily: 'ui-sans-serif, system-ui, sans-serif' + }, item.label); + }) + ) + ); +} + +function h2BuildRefuelOrderNo(record, index) { + if (record && record.orderNo) return record.orderNo; + var idPart = String((record && record.id) || '').replace(/\D/g, '') || String(index + 1); + return 'JQ' + idPart.padStart(4, '0') + String(100 + index); +} + +/** 对账状态(对齐车辆氢费明细) */ +var H2_RECONCILE_RECONCILED = 'reconciled'; +var H2_RECONCILE_PENDING = 'pending'; +var H2_PAYMENT_STATUS_PAID = 'paid'; +var H2_PAYMENT_STATUS_UNPAID = 'unpaid'; + +function h2ParseDateOnlyMs(dateStr) { + if (!dateStr) return NaN; + var parts = String(dateStr).trim().split('-'); + if (parts.length < 3) return NaN; + var y = parseInt(parts[0], 10); + var m = parseInt(parts[1], 10); + var d = parseInt(parts[2], 10); + if (isNaN(y) || isNaN(m) || isNaN(d)) return NaN; + return new Date(y, m - 1, d, 0, 0, 0, 0).getTime(); +} + +function h2AddDays(dateStr, days) { + var ms = h2ParseDateOnlyMs(dateStr); + if (isNaN(ms)) return dateStr; + var d = new Date(ms + (days || 0) * 86400000); + return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); +} + +function h2FormatOperateDateOnly() { + return h2OperateTimestamp().slice(0, 10); +} + +/** 台账加氢记录 Mock(字段对齐车辆氢费明细,costTotal 对应成本总价) */ +function h2BuildLedgerReconciledRecords() { + return H2_MOCK_REFUEL_RECORDS.map(function (r, index) { + var isReconciled = index < 9; + return { + id: r.id, + stationName: r.stationName, + hydrogenTime: r.hydrogenTime, + plateNo: r.plateNo, + customerName: r.customerName, + hydrogenKg: r.hydrogenKg, + costUnitPrice: r.costUnitPrice, + costTotal: r.costAmount, + settlementStatus: r.settlementStatus, + orderNo: h2BuildRefuelOrderNo(r, index), + reconcileStatus: isReconciled ? H2_RECONCILE_RECONCILED : H2_RECONCILE_PENDING, + statementRecordId: null, + reconcileDate: null, + receiptDate: null, + paymentStatus: isReconciled ? H2_PAYMENT_STATUS_UNPAID : null + }; + }); +} + +function h2InitLedgerRecordsStore() { + return h2BuildLedgerReconciledRecords(); +} + +function h2InitStatementRecordsStore() { + return []; +} + +function h2GetLastStatementEndDate(statementRecords, stationName) { + var name = String(stationName || '').trim(); + var list = (statementRecords || []).filter(function (s) { return (s.stationName || '') === name; }); + if (!list.length) return null; + var max = list[0].endDate; + var i; + for (i = 1; i < list.length; i++) { + if (String(list[i].endDate || '') > String(max || '')) max = list[i].endDate; + } + return max || null; +} + +function h2GetAvailableStatementLedgerRows(store, stationName, startDate, endDate) { + var name = String(stationName || '').trim(); + var startMs = h2ParseDateOnlyMs(startDate); + var endMs = h2ParseDateOnlyMs(endDate); + if (!name || isNaN(startMs) || isNaN(endMs)) return []; + var endInclusive = endMs + 86400000 - 1; + return (store || []).filter(function (r) { + if (r.reconcileStatus !== H2_RECONCILE_RECONCILED) return false; + if (r.statementRecordId) return false; + if ((r.stationName || '') !== name) return false; + var t = h2ParseDateTimeMs(r.hydrogenTime); + if (isNaN(t)) return false; + return t >= startMs && t <= endInclusive; + }).slice().sort(function (a, b) { + return String(b.hydrogenTime || '').localeCompare(String(a.hydrogenTime || '')); + }); +} + +function h2GetStatementRecordsByStation(statementRecords, stationName) { + var name = String(stationName || '').trim(); + return (statementRecords || []).filter(function (s) { return (s.stationName || '') === name; }).slice().sort(function (a, b) { + return String(b.reconcileDate || '').localeCompare(String(a.reconcileDate || '')); + }); +} + +function h2GetLedgerRowsByIds(store, ids) { + var idSet = {}; + var i; + for (i = 0; i < (ids || []).length; i++) idSet[ids[i]] = true; + return (store || []).filter(function (r) { return idSet[r.id]; }).slice().sort(function (a, b) { + return String(b.hydrogenTime || '').localeCompare(String(a.hydrogenTime || '')); + }); +} + +function h2PushStatementLedgerPatches(patches) { + if (typeof window === 'undefined') return; + window.H2_STATION_STATEMENT_LEDGER_UPDATES = (window.H2_STATION_STATEMENT_LEDGER_UPDATES || []).concat(patches || []); +} + +function h2CalcStatementSummary(records) { + var rows = records || []; + var totalKg = 0; + var totalCost = 0; + var i; + for (i = 0; i < rows.length; i++) { + totalKg += rows[i].hydrogenKg || 0; + totalCost += rows[i].costTotal || 0; + } + return { count: rows.length, totalKg: totalKg, totalCost: totalCost }; +} + +function h2ExportStatementCsv(stationName, startDate, endDate, records) { + var headers = ['序号', '加氢时间', '车牌号', '加氢量(kg)', '成本单价(元/kg)', '成本总价(元)', '订单编号']; + var csvRows = (records || []).map(function (r, index) { + return [ + index + 1, + r.hydrogenTime || '', + r.plateNo || '', + h2FormatKgNum(r.hydrogenKg), + h2FormatBalanceExportAmount(r.costUnitPrice), + h2FormatBalanceExportAmount(r.costTotal), + r.orderNo || '' + ]; + }); + var safeName = String(stationName || '站点').replace(/[\\/:*?"<>|]/g, '_'); + var rangePart = String(startDate || '').replace(/-/g, '') + '-' + String(endDate || '').replace(/-/g, ''); + h2DownloadCsv('氢费对账单_' + safeName + '_' + rangePart + '_' + new Date().getTime() + '.csv', headers, csvRows); +} + +function h2GetRefuelRecordsByStation(stationName) { + var name = String(stationName || '').trim(); + return H2_MOCK_REFUEL_RECORDS.filter(function (r) { return r.stationName === name; }).slice().sort(function (a, b) { + return String(b.hydrogenTime || '').localeCompare(String(a.hydrogenTime || '')); + }).map(function (r, index) { + return Object.assign({}, r, { orderNo: h2BuildRefuelOrderNo(r, index) }); + }); +} + +function h2ExportRefuelDrillCsv(stationName, records) { + var headers = ['序号', '加氢时间', '车牌号', '客户名称', '加氢量(kg)', '成本单价(元/kg)', '成本总价(元)', '加氢单价(元/kg)', '加氢总价(元)', '承担方式', '订单编号']; + var csvRows = (records || []).map(function (r, index) { + return [ + index + 1, + r.hydrogenTime || '', + r.plateNo || '', + r.customerName || '', + h2FormatKgNum(r.hydrogenKg), + h2FormatBalanceExportAmount(r.costUnitPrice), + h2FormatBalanceExportAmount(r.costAmount), + h2FormatBalanceExportAmount(r.customerUnitPrice), + h2FormatBalanceExportAmount(r.customerAmount), + h2SettlementStatusLabel(r.settlementStatus), + r.orderNo || '' + ]; + }); + var safeName = String(stationName || '站点').replace(/[\\/:*?"<>|]/g, '_'); + h2DownloadCsv('加氢明细_' + safeName + '_' + new Date().getTime() + '.csv', headers, csvRows); +} + +function h2CalcRefuelStats(stationName) { + var records = h2GetRefuelRecordsByStation(stationName); + var totalKg = 0; + var i; + for (i = 0; i < records.length; i++) totalKg += records[i].hydrogenKg || 0; + return { count: records.length, totalKg: totalKg }; +} + +var H2_LINGNIU_SIGNING_COMPANY_NAMES = [ + '浙江羚牛氢能科技有限公司', + '羚牛氢能科技(上海)有限公司', + '羚牛氢能科技(广东)有限公司', + '羚牛新能源运营有限公司' +]; + +function h2GetLingniuSigningCompanyOptions() { + var names = H2_LINGNIU_SIGNING_COMPANY_NAMES.slice(); + if (typeof window !== 'undefined' && window.ONEOS_LESSOR_COMPANIES && window.ONEOS_LESSOR_COMPANIES.length) { + window.ONEOS_LESSOR_COMPANIES.forEach(function (c) { + var n = (c.legalName || '').trim(); + if (n && names.indexOf(n) < 0) names.push(n); + }); + } + return names.map(function (name) { + return { value: name, label: name }; + }); +} + +var H2_MOCK_EXISTING_SUPPLIERS = [ + { + id: 'sup-h2-1', + name: '嘉兴氢能供应有限公司', + type: '加氢站', + city: ['浙江省', '嘉兴市'], + address: '南湖区科技大道66号', + region: '华东', + dept: '能源部', + manager: '张经理', + remark: '嘉兴区域主力加氢站运营商', + contactName: '李供', + contactMobile: '13800138088', + contactTitle: '站长', + taxId: '91330400MA2ABCDEF1', + invoiceAddress: '浙江省嘉兴市南湖区科技大道66号', + invoicePhone: '0573-88886666', + bankName: '中国工商银行嘉兴南湖支行', + bankAccount: '6222021203001234567', + mailingAddress: '浙江省嘉兴市南湖区科技大道66号', + businessAddress: '浙江省嘉兴市南湖区科技大道66号', + businessLicenseFiles: [{ uid: 'bl-1', name: '营业执照_嘉兴氢能.pdf', status: 'done' }], + fillingLicenseFiles: [ + { uid: 'fl-1a', name: '危化品经营许可证.pdf', status: 'done' }, + { uid: 'fl-1b', name: '特种设备使用登记证.pdf', status: 'done' }, + { uid: 'fl-1c', name: '消防验收合格证.pdf', status: 'done' } + ] + }, + { + id: 'sup-h2-2', + name: '上海羚牛氢能科技', + type: '加氢站', + city: ['上海市', '上海市'], + address: '宝山区富联路200号', + region: '华东', + dept: '能源部', + manager: '王专员', + remark: '', + contactName: '陈氢', + contactMobile: '13900139001', + contactTitle: '运营主管', + taxId: '91310100MA1XYZ7890', + invoiceAddress: '上海市宝山区富联路200号', + invoicePhone: '021-66886688', + bankName: '招商银行上海宝山支行', + bankAccount: '6214830210123456789', + mailingAddress: '上海市宝山区富联路200号', + businessAddress: '上海市宝山区富联路200号', + businessLicenseFiles: [{ uid: 'bl-2', name: '营业执照_羚牛氢能.pdf', status: 'done' }], + fillingLicenseFiles: [{ uid: 'fl-2', name: '加氢站备案证明.pdf', status: 'done' }] + }, + { + id: 'sup-h2-3', + name: '苏州工业园区氢源科技', + type: '加氢站', + city: ['江苏省', '苏州市'], + address: '工业园区星湖街328号', + region: '华东', + dept: '华东大区', + manager: '赵经理', + remark: '备用站点供应商', + contactName: '钱七', + contactMobile: '13500135005', + contactTitle: '联系人', + taxId: '91320500MA2SUPPLIER', + invoiceAddress: '江苏省苏州市工业园区星湖街328号', + invoicePhone: '0512-66668888', + bankName: '中国银行苏州工业园区支行', + bankAccount: '6216600100009876543', + mailingAddress: '江苏省苏州市工业园区星湖街328号', + businessAddress: '江苏省苏州市工业园区星湖街328号', + businessLicenseFiles: [], + fillingLicenseFiles: [] + }, + { + id: 'sup-other-1', + name: '杭州绿能备件有限公司', + type: '备件供应商', + city: ['浙江省', '杭州市'], + address: '余杭区文一西路998号', + region: '华东', + dept: '采购部', + manager: '孙采购', + remark: '', + contactName: '周备件', + contactMobile: '13600136001', + contactTitle: '销售', + taxId: '91330100MA3OTHER01', + invoiceAddress: '浙江省杭州市余杭区文一西路998号', + invoicePhone: '0571-88880001', + bankName: '中国建设银行杭州余杭支行', + bankAccount: '6227000012345678901', + mailingAddress: '浙江省杭州市余杭区文一西路998号', + businessAddress: '浙江省杭州市余杭区文一西路998号', + businessLicenseFiles: [], + fillingLicenseFiles: [] + } +]; + +/** 可加氢站绑定的系统账号 Mock */ +var H2_MOCK_SYSTEM_ACCOUNTS = [ + { id: 'acc-h2-1', username: 'jx_h2_admin', displayName: '嘉兴站管理员', role: '加氢站管理员', mobile: '13800138001', status: '启用' }, + { id: 'acc-h2-2', username: 'hz_lp_station', displayName: '临平站操作员', role: '加氢站管理员', mobile: '13900139002', status: '启用' }, + { id: 'acc-h2-3', username: 'sh_bs_h2', displayName: '宝山站管理员', role: '加氢站管理员', mobile: '13700137003', status: '启用' }, + { id: 'acc-h2-4', username: 'sz_h2_ops', displayName: '苏州备用站账号', role: '加氢站管理员', mobile: '13500135005', status: '启用' }, + { id: 'acc-h2-5', username: 'h2_station_new', displayName: '待分配站点账号', role: '加氢站管理员', mobile: '13600136099', status: '启用' } +]; + +function h2FindSystemAccountById(accountId) { + if (!accountId) return null; + var i; + for (i = 0; i < H2_MOCK_SYSTEM_ACCOUNTS.length; i++) { + if (H2_MOCK_SYSTEM_ACCOUNTS[i].id === accountId) return H2_MOCK_SYSTEM_ACCOUNTS[i]; + } + return null; +} + +function h2NormalizeBindAccountIds(source) { + if (!source) return []; + if (source.bindAccountIds && source.bindAccountIds.length) return source.bindAccountIds.slice(); + if (source.boundAccountIds && source.boundAccountIds.length) return source.boundAccountIds.slice(); + var single = source.bindAccountId || source.boundAccountId; + return single ? [single] : []; +} + +function h2BuildBoundAccountsFromIds(ids) { + return (ids || []).map(function (id) { + var acc = h2FindSystemAccountById(id); + if (!acc) return null; + return { + id: acc.id, + username: acc.username, + displayName: acc.displayName, + role: acc.role, + mobile: acc.mobile, + status: acc.status + }; + }).filter(Boolean); +} + +function h2CollectUsedBindAccountMap(stations, excludeStationId) { + var map = {}; + (stations || []).forEach(function (s) { + if (excludeStationId != null && s.id === excludeStationId) return; + h2NormalizeBindAccountIds(s).forEach(function (aid) { + map[aid] = s.name || '—'; + }); + }); + return map; +} + +function h2ResolveStationSystemAccounts(record) { + if (!record) return []; + if (record.boundAccounts && record.boundAccounts.length) return record.boundAccounts.slice(); + if (record.boundAccount && (record.boundAccount.username || record.boundAccount.displayName)) { + return [record.boundAccount]; + } + return h2BuildBoundAccountsFromIds(h2NormalizeBindAccountIds(record)); +} + +function h2ResolveStationSystemAccount(record) { + var list = h2ResolveStationSystemAccounts(record); + return list.length ? list[0] : null; +} + +function h2RecordToEditStationForm(record) { + if (!record) return h2CreateEmptyStationForm(); + var ids = h2NormalizeBindAccountIds(record); + return { + name: record.name || '', + address: { + region: (record.region || []).slice(), + detail: record.addressDetail || '' + }, + mapAddress: record.mapAddress || record.fullAddress || h2BuildFullAddress(record.region, record.addressDetail), + latitude: h2NormalizeCoord(record.latitude), + longitude: h2NormalizeCoord(record.longitude), + isSigned: record.isSigned !== false, + contractStart: record.contractStart || '', + contractEnd: record.contractEnd || '', + contractFiles: (record.contractFiles || []).map(function (f, i) { + return Object.assign({}, f, { uid: f.uid || ('f-' + i), status: 'done' }); + }), + businessStatus: record.businessStatus || '营业中', + businessHours: record.businessHours === '—' ? '' : (record.businessHours || ''), + contact: record.contact || '', + mobilePhone: record.mobilePhone || record.phone || '', + landlinePhone: record.landlinePhone || '', + costUnitPrice: record.costUnitPrice != null ? String(record.costUnitPrice) : '', + customerUnitPrice: record.customerUnitPrice != null ? String(record.customerUnitPrice) : '', + prepaidBalance: record.prepaidBalance || 0, + bindAccountMode: record.bindAccountMode || (ids.length ? 'bind' : 'none'), + bindAccountIds: ids + }; +} + +function h2CreateEmptyStationForm() { + return { + name: '', + address: h2EmptyAddressValue(), + mapAddress: '', + latitude: null, + longitude: null, + isSigned: true, + contractStart: '', + contractEnd: '', + contractFiles: [], + businessStatus: '营业中', + businessHours: '00:00-24:00', + contact: '', + mobilePhone: '', + landlinePhone: '', + costUnitPrice: '', + customerUnitPrice: '', + prepaidBalance: 0, + bindAccountMode: 'none', + bindAccountIds: [] + }; +} + +function h2CreateEmptySupplierForm() { + return { + signingCompany: undefined, + name: '', + city: undefined, + address: '', + region: '', + dept: '', + manager: '', + remark: '', + contactName: '', + contactMobile: '', + contactTitle: '', + taxId: '', + invoiceAddress: '', + invoicePhone: '', + bankName: '', + bankAccount: '', + mailingAddress: '', + businessAddress: '', + businessLicenseFiles: [], + fillingLicenseFiles: [] + }; +} + +function h2GetRegionByProvince(province) { + var a = String(province || '').trim(); + if (!a) return ''; + if (a === '北京市' || a === '天津市' || a === '河北省' || a === '山西省' || a === '内蒙古') return '华北'; + if (a === '上海市' || a === '江苏省' || a === '浙江省' || a === '安徽省' || a === '福建省' || a === '江西省' || a === '山东省') return '华东'; + if (a === '广东省' || a === '广西省' || a === '海南省') return '华南'; + if (a === '河南省' || a === '湖北省' || a === '湖南省') return '华中'; + if (a === '辽宁省' || a === '吉林省' || a === '黑龙江省') return '东北'; + if (a === '四川省' || a === '云南省' || a === '贵州省' || a === '重庆市') return '西南'; + if (a === '陕西省' || a === '甘肃省' || a === '青海省' || a === '宁夏' || a === '新疆') return '西北'; + return ''; +} + +function h2SupplierToForm(s) { + if (!s) return h2CreateEmptySupplierForm(); + return { + signingCompany: s.signingCompany || undefined, + name: s.name || '', + city: s.city ? s.city.slice() : undefined, + address: s.address || '', + region: s.region || '', + dept: s.dept || '', + manager: s.manager || '', + remark: s.remark || '', + contactName: s.contactName || '', + contactMobile: s.contactMobile || '', + contactTitle: s.contactTitle || '', + taxId: s.taxId || '', + invoiceAddress: s.invoiceAddress || '', + invoicePhone: s.invoicePhone || '', + bankName: s.bankName || '', + bankAccount: s.bankAccount || '', + mailingAddress: s.mailingAddress || '', + businessAddress: s.businessAddress || s.mailingAddress || '', + businessLicenseFiles: (s.businessLicenseFiles || []).map(function (f, i) { + return Object.assign({}, f, { uid: f.uid || ('bl-' + i), status: 'done' }); + }), + fillingLicenseFiles: (s.fillingLicenseFiles || []).map(function (f, i) { + return Object.assign({}, f, { uid: f.uid || ('fl-' + i), status: 'done' }); + }) + }; +} + +var H2_STATION_USER_MANUAL_DOC = `# 加氢站管理 · 站点信息 — 用户使用说明书 + +| 项目 | 内容 | +|------|------| +| 文档版本 | v1.0 | +| 产品模块 | 加氢站管理 → 站点信息 | +| 文档类型 | **用户使用说明书** | +| 适用读者 | 加氢站运营、财务结算、平台管理员、培训与汇报人员 | +| 关联模块 | 台账数据 → 车辆氢费明细 | +| 配图目录 | 与本文件同级的 docs/ 文件夹 | + +--- + +## 使用本说明书 + +本说明书用于**教会一线用户操作**站点管理模块,也可作为**项目汇报、培训宣讲**的配套材料。建议按以下顺序阅读: + +1. **第二章** — 认识页面布局(5 分钟) +2. **第四章** — 新建 / 编辑 / 导入站点(15 分钟) +3. **第七章** — 生成对账单与结算(重点,20 分钟) +4. **第九章** — 汇报话术与常见问题 + +页面右上角可点击 **「查看使用说明书」** 随时打开本文档。 + +--- + +## 一、模块能做什么 + +### 1.1 一句话说明 + +站点信息模块是加氢站的**主数据与运营台账中心**:维护站点档案、营业与成本价,查看加氢量与预付余额,并按周期与加氢站完成**氢费对账、收票登记与付款闭环**。 + +### 1.2 功能地图 + +\`\`\`mermaid +mindmap + root((站点信息)) + 查询统计 + 筛选条件 + KPI 分类卡 + 列表排序分页 + 主数据 + 新建站点 + 编辑站点 + 查看详情 + 批量导入 + 删除站点 + 运营配置 + 营业状态 + 成本价格 + 数据分析 + 加氢量下钻 + 预付余额下钻 + 财务结算 + 生成对账单 + 查看对账记录 +\`\`\` + +### 1.3 谁来做什么事 + +| 角色 | 常用功能 | 注意事项 | +|------|----------|----------| +| **平台管理员(admin)** | 新建、编辑、批量导入、删除、绑定系统账号 | 系统账号支持多选绑定 | +| **加氢站运营账号** | 编辑本站资料、营业状态、价格配置 | 编辑时**不能改**供应商与系统账号 | +| **财务 / 结算人员** | 生成对账单、登记收票、查看对账记录 | 需先在台账完成「已对账」 | + +--- + +## 二、进入模块与页面总览 + +### 2.1 菜单路径 + +左侧导航:**加氢站管理 → 站点信息** + +### 2.2 页面结构(自上而下) + +\`\`\` +┌─────────────────────────────────────────────────────────────┐ +│ 面包屑:加氢站管理 > 站点信息 [查看需求说明][使用说明书] │ +├─────────────────────────────────────────────────────────────┤ +│ 【筛选条件】名称 | 是否签约 | 地区 | 营业状态 [重置][查询] │ +├─────────────────────────────────────────────────────────────┤ +│ KPI:全部 | 已签约 | 未签约 | 高频 | 低频 | 无加氢 │ +├─────────────────────────────────────────────────────────────┤ +│ 工具栏:[批量导入] [新建站点] │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ 站点列表(名称/签约/营业/价格/加氢量/余额/操作) │ │ +│ └─────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +\`\`\` + +![图 2-1 站点列表页总览示意](docs/h2-station-manual-list-page.png) + +### 2.3 列表字段速查 + +| 列名 | 怎么用 | +|------|--------| +| 加氢站名称 | 含「已签约」标签;下方显示完整地址 | +| 合作时间 | 未签约显示为空 | +| 营业状态 / 营业时间 | 只读;在「更多 → 营业状态」修改 | +| 当前成本价格 | 元/kg;由价格配置生效时间驱动 | +| 加氢次数 / 加氢量 | **点击加氢量**可下钻明细 | +| 预付余额 | **点击余额**可下钻流水;负数标红「已欠费」 | +| 操作 | **查看** + **更多**(⋮) | + +--- + +## 三、日常查询与 KPI 筛选 + +### 3.1 操作流程 + +\`\`\`mermaid +flowchart LR + A[输入筛选条件] --> B[点击查询] + B --> C[列表刷新] + D[点击 KPI 卡] --> C + E[点击重置] --> F[清空条件] + F --> B +\`\`\` + +### 3.2 操作步骤 + +1. 在「筛选条件」卡片填写一个或多个条件。 +2. 点击 **查询**;列表按条件过滤,并回到第 1 页。 +3. 点击 **重置** 可清空所有筛选。 +4. 也可直接点击 KPI 卡片快速筛选: + - **全部站点** — 显示所有 + - **已签约 / 未签约** — 按签约状态 + - **高频站点** — 加氢次数 ≥ 3 + - **低频站点** — 加氢次数 1~2 + - **无加氢站点** — 加氢次数 = 0 + +### 3.3 列表排序与分页 + +- 点击 **加氢次数**、**加氢量** 列表头可升序 / 降序排序。 +- 底部分页默认每页 10 条,可切换 5 / 10 / 20 / 50。 + +--- + +## 四、站点主数据维护 + +### 4.1 整体流程 + +\`\`\`mermaid +flowchart TB + subgraph 入口 + N[新建站点] --> CP[整页新建表单] + I[批量导入] --> IM[上传模板文件] + E[更多-编辑] --> EP[整页编辑表单] + V[查看] --> VM[详情弹窗] + D[更多-删除] --> DM[确认删除] + end + CP --> S[提交创建] + EP --> U[保存修改] + IM --> CI[确认导入] +\`\`\` + +![图 4-1 新建/编辑整页表单示意](docs/h2-station-manual-create-page.png) + +### 4.2 新建站点 + +**入口**:列表工具栏 → **新建站点** + +整页表单分三块卡片: + +#### (1)加氢站基本信息 + +| 字段 | 必填 | 说明 | +|------|------|------| +| 加氢站名称 | 是 | 不可与已有站点重名 | +| 省 / 市、详细地址 | 是 | 可用「地址解析」自动解析 | +| 联系人、联系电话 | 是 | 手机或固话 | +| 是否签约 | 否 | 选「签约站点」后须填合作时间与合同附件 | + +#### (2)系统账号绑定 + +1. 选择 **绑定系统账号** 或 **暂不绑定**。 +2. 若绑定:在「选择系统账号」中**多选**账号(已绑定其他站点的不可选)。 +3. 下方可预览已选账号的登录名、姓名、角色、手机号。 + +> 绑定后,所选账号可登录系统并管理该站点。 + +#### (3)供应商相关信息 + +- **关联已有供应商** — 搜索档案绑定 +- **新增供应商** — 填写主体、开票、银行账户、营业执照与其他证件 + +**底部操作**: + +- **返回 / 取消** — 有未保存内容时会二次确认 +- **提交创建** — 底部进度条达 100% 后可提交(新建页**无「重置」按钮**) + +### 4.3 编辑站点 + +**入口**:列表操作 → **更多(⋮)→ 编辑** + +编辑页与新建页**同布局整页**,但有以下差异: + +| 项目 | 编辑页规则 | +|------|------------| +| 供应商信息 | **不展示**,不可修改 | +| 系统账号 | **仅 admin 可改**;加氢站账号登录时为只读展示 | +| 营业状态 | 不在此页维护,请用「更多 → 营业状态」 | +| 底部按钮 | **保存修改**(无重置) | + +\`\`\`mermaid +flowchart TD + A[打开编辑页] --> B{当前登录角色?} + B -->|admin| C[可改基本信息 + 系统账号多选] + B -->|加氢站账号| D[仅可改基本信息] + D --> E[系统账号只读展示] + C --> F[保存修改] + D --> F +\`\`\` + +### 4.4 查看站点 + +**入口**:列表操作 → **查看** + +弹窗分四块只读信息: + +1. 加氢站信息(含签约、营业时间等) +2. 系统账号(支持展示多个绑定账号) +3. 供应商信息 +4. 付款信息 + +### 4.5 删除站点 + +**入口**:列表操作 → **更多 → 删除** + +弹出确认框,确认后从列表移除,**不可撤销**。 + +### 4.6 批量导入 + +**入口**:列表工具栏 → **批量导入** + +\`\`\`mermaid +flowchart LR + A[下载 CSV 模板] --> B[按模板填写] + B --> C[上传 csv/xlsx/xls] + C --> D[系统校验预览] + D --> E{可导入条数>0?} + E -->|是| F[确认导入] + E -->|否| G[修正文件重传] +\`\`\` + +**注意**: + +- 站点名称不能与现有台账重复 +- 模板字段与新建表单一致 +- 上传后查看「可导入 / 不可导入」条数与错误原因 + +--- + +## 五、运营配置 + +### 5.1 营业状态 + +**入口**:更多 → **营业状态** + +\`\`\` +┌──────────────────────────────────────┐ +│ 站点信息卡(名称、地址、当前状态) │ +├──────────────────────────────────────┤ +│ 营业状态:[营业中][暂停营业][停止营业] │ +│ 营业时间:[全天营业][自定义时段] │ +├──────────────────────────────────────┤ +│ 营业状态变更记录(历史列表) │ +│ [取消] [保存] │ +└──────────────────────────────────────┘ +\`\`\` + +**操作要点**: + +1. 营业状态、营业时间均用**按钮组**点选(非下拉)。 +2. 非「全天营业」时,必须填写开始与结束时间(HH:mm)。 +3. 保存后列表「营业状态 / 营业时间」同步更新;若状态变更会写入记录表。 + +### 5.2 价格配置 + +**入口**:更多 → **价格配置** + +1. 填写 **成本价格(元/kg)** 与 **生效时间**。 +2. 保存后: + - 若已到生效时间 → 列表「当前成本价格」**立即更新** + - 若未到生效时间 → 到点后自动更新 +3. 下方展示价格调整记录(操作人、调整前后价格、生效时间)。 + +--- + +## 六、数据下钻分析 + +### 6.1 加氢量下钻 + +**入口**:列表点击 **加氢量** 数字 + +弹窗内容: + +- 统计卡:加氢次数、加氢总量(kg)、成本总价、加氢总价 +- 明细表:字段对齐「车辆氢费明细」 +- **导出 CSV** 按钮 + +### 6.2 预付余额下钻 + +**入口**:列表点击 **预付余额** 数字 + +弹窗内容: + +- 当前余额(负数显示「已欠费」) +- 充值 / 车辆加氢流水明细 +- **导出 CSV** 按钮 + +--- + +## 七、对账与结算(核心流程) + +本流程与 **车辆氢费明细** 联动,完成站点侧付款闭环。 + +### 7.1 前置条件(必知) + +生成对账单前,台账中须已有满足以下**全部**条件的记录: + +1. 对账状态 = **已对账**(业务员已在台账点击「完成对账」) +2. **尚未**被本站点历史对账单结算过 +3. 加氢时间在所选账单日期区间内 +4. 加氢站名称与当前站点一致 + +> 对账单只展示**成本侧**字段(加氢量、成本单价、成本总价),不展示客户加氢价。 + +### 7.2 生成对账单 — 两阶段操作 + +**入口**:更多 → **生成对账单** + +![图 7-1 生成对账单两阶段流程示意](docs/h2-station-manual-statement-flow.png) + +\`\`\`mermaid +flowchart TD + A[打开生成对账单] --> B[选择账单开始/结束日期] + B --> C[查看上次对账单结束时间] + C --> D[点击生成对账单] + D -->|无明细| E[提示调整日期] + D -->|有明细| F[进入结算阶段] + F --> G[查看统计卡与成本明细表] + G --> H[填写收票日期/金额/发票附件] + H --> I[点击提交对账单] + I --> J[更新余额 + 写对账历史 + 回写台账] +\`\`\` + +#### 阶段一:选期生成 + +| 步骤 | 操作 | +|------|------| +| 1 | 选择 **账单开始日期**、**账单结束日期** | +| 2 | 参考「上次对账单结束时间」(无历史显示「暂无」) | +| 3 | 点击 **生成对账单** | +| 4 | 成功后进入阶段二,**日期锁定不可改** | + +#### 阶段二:结算提交 + +**统计卡**(明细表上方): + +| 指标 | 含义 | +|------|------| +| 加氢次数 | 本账单包含的记录笔数 | +| 加氢总量 | kg 合计 | +| 成本总金额 | 成本总价合计 | + +**结算表单**(必填): + +| 字段 | 规则 | +|------|------| +| 结算后加氢站预付款余额 | **只读**,= 当前余额 − 成本总金额 | +| 收票日期 | 必填,与收票金额同一行 | +| 收票金额 | 必填,带 ¥ 前缀;默认等于成本总金额 | +| 发票附件 | 必填,支持上传 | + +点击 **提交对账单** 后系统将: + +1. 写入对账历史(供后续查询) +2. 扣减站点预付余额 +3. 标记台账记录已结算(防重复入账) +4. 回写车辆氢费明细:对账日期、收票日期、加氢站付款状态 → **已付款** + +\`\`\`mermaid +sequenceDiagram + participant 业务 as 台账业务员 + participant 财务 as 财务操作人 + participant 站点 as 站点信息 + participant 台账 as 车辆氢费明细 + + 业务->>台账: 完成对账 → 已对账 + 财务->>站点: 更多 → 生成对账单 + 站点->>台账: 拉取已对账且未结算记录 + 财务->>站点: 填写收票信息并提交 + 站点->>站点: 更新预付余额 + 站点->>台账: 回写已付款 +\`\`\` + +### 7.3 查看对账记录 + +**入口**:更多 → **查看对账记录** + +历史列表字段:对账日期、对账人、账单区间、加氢次数、加氢金额、对账后余额、操作。 + +点击 **查看明细** 可看到: + +- 账单区间与统计三卡 +- 收票时间、收票金额 +- 发票附件下载 +- 成本明细表 + +--- + +## 八、操作列「更多」菜单速查 + +| 菜单项 | 作用 | +|--------|------| +| 编辑 | 整页编辑站点主数据 | +| 营业状态 | 维护营业状态与营业时间 | +| 价格配置 | 设置成本价与生效时间 | +| 生成对账单 | 两阶段对账结算 | +| 查看对账记录 | 历史对账单与明细 | +| 删除 | 删除站点(不可恢复) | + +--- + +## 九、汇报与培训话术要点 + +向领导或客户汇报时,可围绕 **「一个中心、三条主线」** 组织: + +**一个中心**:站点全生命周期台账(档案 → 运营 → 结算) + +**三条主线**: + +1. **主数据线** — 新建 / 导入 / 编辑,保证站点档案完整准确 +2. **运营线** — 营业状态、成本价格、加氢量与余额下钻,支撑日常经营分析 +3. **结算线** — 台账已对账 → 站点生成对账单 → 收票登记 → 余额扣减 → 台账已付款,形成闭环 + +**可量化成果表述示例**: + +- 支持按 KPI(高频/低频/无加氢)快速识别运营薄弱站点 +- 对账单两阶段设计,避免选错账期;提交后自动更新余额并防重复结算 +- 多账号绑定 + 角色权限,满足平台管理与站点自治并存 + +--- + +## 十、常见问题 + +| 问题 | 解答 | +|------|------| +| 生成对账单提示无记录? | 检查台账是否「已对账」、日期区间是否正确、是否已被历史对账单结算 | +| 编辑时看不到供应商? | 编辑页 intentionally 不展示供应商,需在供应商模块或新建时维护 | +| 加氢站账号改不了系统账号? | 仅 admin 可修改绑定;加氢站账号只能查看 | +| 预付余额为负还能提交吗? | 可以;系统标「已欠费」,提醒后续充值 | +| 批量导入失败? | 检查名称重复、营业状态枚举、日期格式;建议用 CSV UTF-8 | + +--- + +## 十一、快速检查清单 + +培训验收时可按此清单实操一遍: + +- [ ] 用筛选 + KPI 找到目标站点 +- [ ] 新建一个站点(含供应商 + 多账号绑定) +- [ ] 用加氢站角色编辑,确认账号只读 +- [ ] 用 admin 编辑,调整系统账号绑定 +- [ ] 配置营业状态与成本价格 +- [ ] 下钻加氢量与预付余额并导出 CSV +- [ ] 完成一次「生成对账单 → 提交」全流程 +- [ ] 在「查看对账记录」中核对明细与发票信息 + +--- + +**文档结束** +`; + +var H2_STATION_REQUIREMENT_DOC = `# 加氢站管理 · 站点信息 — 产品需求说明(PRD) + +| 项目 | 内容 | +|------|------| +| 文档版本 | v1.0 | +| 产品模块 | 加氢站管理 → 站点信息 | +| 文档类型 | 产品需求说明 | +| 适用读者 | 研发、测试、产品、项目 | +| 关联模块 | 台账数据 → 车辆氢费明细 | + +--- + +## 一、为什么做这件事 + +### 1.1 业务背景 + +加氢站是氢费成本结算的关键节点。运营侧需要维护站点主数据(签约、营业、成本价、预付余额),并在财务周期内按站点与加氢站完成对账、收票与付款闭环。 + +### 1.2 本期目标 + +建设 Web 端「站点信息」页面,支撑: + +- 站点台账的查询、新建、编辑、查看、删除与批量导入 +- 营业状态、成本价格、预付余额等运营配置 +- **按站点生成氢费对账单 → 填写收票信息 → 提交结算**,并与「车辆氢费明细」联动回写 + +### 1.3 用户角色 + +| 角色 | 典型诉求 | +|------|----------| +| **加氢站运营** | 维护站点资料、营业与价格;查看加氢量与余额 | +| **财务/结算** | 按周期生成对账单、登记收票、完成站点付款闭环 | +| **管理员** | 批量导入站点、删除无效站点 | + +--- + +## 二、页面整体结构 + +用户进入「加氢站管理 → 站点信息」后,页面自上而下为: + +1. **筛选区** — 名称、签约、地区、营业状态 +2. **KPI 分类** — 全部 / 高频 / 低频 / 无加氢;已签约快捷筛选 +3. **站点列表** — 主数据 + 运营指标 + 操作列 +4. **弹窗/抽屉** — 查看、编辑、营业状态、价格配置、对账单、对账记录等 + +\`\`\`mermaid +flowchart TB + subgraph 列表页 + A[筛选 / KPI] --> B[站点列表] + B --> C[查看站点] + B --> D[编辑 / 删除] + B --> E[更多菜单] + end + E --> F[营业状态] + E --> G[价格配置] + E --> H[生成对账单] + E --> I[查看对账记录] + B --> J[加氢量下钻] + B --> K[预付余额下钻] +\`\`\` + +--- + +## 三、列表与 KPI 规则 + +### 3.1 列表字段 + +| 字段 | 说明 | +|------|------| +| 加氢站名称 | 含签约标签;名称下一行展示省市区+详细地址 | +| 合作时间 | 未签约可空 | +| 营业状态 / 营业时间 | 列表只读;在「更多-营业状态」维护 | +| 当前成本价格 | 元/kg;受价格配置生效时间驱动 | +| 加氢次数 / 加氢量 | 加氢量可点击下钻;列表头支持排序 | +| 预付余额 | 可点击下钻流水;负值标红并显示「已欠费」 | +| 联系方式 | 联系人 + 电话 | +| 操作 | 查看 + 更多 | + +### 3.2 KPI 划分逻辑 + +| 分类 | 规则 | +|------|------| +| 高频站点 | 加氢次数 ≥ 3 | +| 低频站点 | 加氢次数 1~2 | +| 无加氢站点 | 加氢次数 = 0 | + +筛选或 KPI 切换后列表回到第 1 页;默认分页 10 条,可选 5/10/20/50。 + +--- + +## 四、站点主数据(新建 / 编辑 / 查看) + +### 4.1 新建站点 + +同页内嵌视图,包含: + +- **基本信息**:名称、地址、营业时间、签约、联系方式 +- **系统账号绑定**:可选绑定,绑定后该账号可登录管理对应站点 +- **供应商信息**:新建或关联已有供应商、付款信息 + +### 4.2 编辑 / 查看 + +| 模式 | 交互 | +|------|------| +| 编辑 | 整页(与新建同结构);不含供应商、不含营业状态;系统账号仅 admin 可改,支持多选 | +| 查看 | 弹窗分块:加氢站信息、系统账号、供应商信息、付款信息 | + +### 4.3 批量导入 + +- 下载 CSV 模板 → 上传 .csv / .xlsx / .xls +- 站点名称不可与现有台账重复;解析预览后确认导入 + +--- + +## 五、运营配置 + +### 5.1 营业状态 + +- 卡片式弹窗:营业状态、营业时间均为**按钮组** +- 非「全天营业」须配置开始/结束时段 +- 保存时校验时段完整性;下方展示状态变更记录 + +### 5.2 价格配置 + +- 配置**成本价格(元/kg)**与**生效时间** +- 到达生效时间后自动刷新列表「当前成本价格」 +- 下方展示价格调整记录(操作人、调整前后价格、生效时间) + +### 5.3 加氢量 / 预付余额下钻 + +**加氢量下钻**:统计卡(次数、加氢量、成本总价、加氢总价)+ 明细表 + 导出 CSV,字段对齐车辆氢费明细。 + +**预付余额下钻**:充值/车辆加氢流水 + 导出 CSV;余额为负显示已欠费。 + +--- + +## 六、核心流程:生成对账单(重点) + +本模块与「车辆氢费明细」配合完成**站点侧氢费结算闭环**。研发需重点理解以下逻辑。 + +### 6.1 数据来源 + +对账单明细取自 **车辆氢费明细** 中同时满足以下条件的记录: + +1. 对账状态 = **已对账**(业务侧已在台账完成「完成对账」) +2. **尚未**被本站点历史对账单结算过(未绑定 statementRecordId) +3. 加氢时间落在用户选择的 **[账单开始日期, 账单结束日期]** 内 +4. 加氢站名称与当前操作站点一致 + +> 对账单明细**仅展示成本字段**(加氢量、成本单价、成本总价等),不展示加氢单价、加氢总价。 + +### 6.2 两阶段交互 + +\`\`\`mermaid +flowchart TD + A[打开「生成对账单」] --> B[选择账单开始/结束日期] + B --> C[展示上次对账单结束时间] + C --> D{点击「生成对账单」} + D -->|无符合条件记录| E[提示调整日期] + D -->|有记录| F[生成对账记录草稿] + F --> G[展示统计:加氢次数/总量/成本总金额] + G --> H[展示成本明细表] + H --> I[填写结算信息] + I --> J{点击「提交对账单」} + J -->|校验失败| K[提示必填项] + J -->|校验通过| L[完成闭环] +\`\`\` + +**阶段一 · 选期生成** + +- 日期区下方展示:**上次对账单结束时间 YYYY-MM-DD**(无历史则「暂无」) +- 默认开始日期建议 = 上次结束日 + 1 天 +- 点击「生成对账单」后进入阶段二,日期锁定不可改 + +**阶段二 · 结算提交** + +统计卡(明细表上方): + +| 指标 | 说明 | +|------|------| +| 加氢次数 | 本账单包含的已对账记录笔数 | +| 加氢总量 | kg 合计 | +| 成本总金额 | 成本总价合计 | + +明细表下方**必填**结算项: + +| 字段 | 规则 | +|------|------| +| 结算后加氢站预付款余额 | **只读**,= 当前预付余额 − 成本总金额;不足为负值并标「已欠费」 | +| 收票日期 | 必填,与收票金额同一行展示 | +| 收票金额 | 必填,输入框带 ¥ 前缀;默认填入成本总金额 | +| 发票附件 | 必填,支持上传 | + +### 6.3 提交后的系统行为(关键) + +提交成功后需**原子性**完成: + +1. **写入对账历史**(供「查看对账记录」查询) +2. **扣减站点预付余额** = 结算后余额 +3. **标记台账记录已结算**(绑定 statementRecordId,避免重复入账) +4. **回写车辆氢费明细**(原型通过 window 补丁 / API 同步): + +| 车辆氢费明细字段 | 回写值 | +|------------------|--------| +| 对账日期 | 操作完成时间,格式 YYYY-MM-DD HH:MM | +| 收票日期 | 操作完成日期,格式 YYYY-MM-DD | +| 加氢站付款状态 | 已付款 | + +\`\`\`mermaid +sequenceDiagram + participant 台账 as 车辆氢费明细 + participant 站点 as 站点信息 + participant 财务 as 结算操作人 + + 台账->>台账: 业务员「完成对账」→ 已对账 + 财务->>站点: 更多 → 生成对账单 + 站点->>台账: 拉取已对账且未结算记录 + 财务->>站点: 生成对账记录 + 填收票信息 + 财务->>站点: 提交对账单 + 站点->>站点: 更新预付余额 / 写对账历史 + 站点->>台账: 回写对账日期、收票日期、已付款 +\`\`\` + +--- + +## 七、查看对账记录 + +入口:列表操作列 → 更多 → **查看对账记录** + +### 7.1 历史列表字段 + +| 列 | 说明 | +|----|------| +| 对账日期 | 提交对账单的操作时间 | +| 对账人 | 当前操作人 | +| 账单开始/结束日期 | 本次账单覆盖区间 | +| 加氢次数 | 本单笔数 | +| 加氢金额 | 成本总金额 | +| 对账后加氢站预付款余额 | 结算后余额;负值标「已欠费」 | +| 操作 | 查看明细 | + +### 7.2 查看明细 + +弹窗展示: + +- 账单区间、加氢统计三卡 +- **收票时间、收票金额**(同一信息区) +- **发票附件下载** +- 成本明细表(同生成对账单字段范围) + +--- + +## 八、校验与异常 + +| 场景 | 处理 | +|------|------| +| 日期未填 / 开始晚于结束 | 阻止生成,提示用户 | +| 区间内无已对账未结算记录 | 阻止生成,提示调整日期 | +| 收票日期/金额/附件未填 | 阻止提交 | +| 收票金额 ≤ 0 | 阻止提交 | +| 预付余额结算后为负 | 允许提交,列表与明细标「已欠费」 | + +--- + +## 九、研发实现要点(给开发) + +1. **对账单幂等**:同一笔已对账记录只能进入一次成功提交的对账单(statementRecordId 约束)。 +2. **上次结束时间**:取该站点已成功提交对账单的最大 endDate 字段。 +3. **余额计算**:balanceAfter = prepaidBalance - sum(costTotal),提交后写回站点主数据。 +4. **跨模块同步**:生产环境应对接统一台账服务;原型使用 H2_STATION_STATEMENT_LEDGER_UPDATES / H2_VEHICLE_LEDGER_API 模拟回写。 +5. **明细字段范围**:对账相关弹窗统一不展示客户侧加氢单价/总价,避免与成本结算混淆。 +6. **权限**(后续迭代):生成/提交对账单建议限制财务或站点管理员角色。 + +--- + +## 十、本期不做 + +- 对账单审批流、ERP 自动过账 +- 发票 OCR 识别与验真 +- 多币种 / 多税率 +- 按组织架构的复杂数据权限(本期按功能原型描述) + +--- + +**文档结束** +`; + +function h2StationParsePrdInlineText(text) { + var parts = String(text || '').split(/(\*\*[^*]+\*\*)/g); + var nodes = []; + var i; + for (i = 0; i < parts.length; i++) { + var p = parts[i]; + if (!p) continue; + if (p.indexOf('**') === 0 && p.lastIndexOf('**') === p.length - 2) { + nodes.push(React.createElement('strong', { key: i }, p.slice(2, -2))); + } else { + nodes.push(p); + } + } + return nodes.length === 1 ? nodes[0] : nodes; +} + +function h2StationIsPrdTableRow(line) { + return /^\|.+\|$/.test(String(line || '').trim()); +} + +function h2StationIsPrdTableSep(line) { + return /^\|[\s\-:|]+\|$/.test(String(line || '').trim()); +} + +function h2StationRenderPrdTableRow(line, rowKey, isHeader) { + var cells = String(line).trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map(function (c) { return c.trim(); }); + return React.createElement('tr', { key: rowKey }, + cells.map(function (cell, ci) { + var Tag = isHeader ? 'th' : 'td'; + return React.createElement(Tag, { + key: ci, + style: { + border: '1px solid #e5e7eb', + padding: '8px 10px', + textAlign: 'left', + verticalAlign: 'top', + fontWeight: isHeader ? 600 : 400, + background: isHeader ? '#f8fafc' : '#fff', + fontSize: 13, + lineHeight: 1.5 + } + }, h2StationParsePrdInlineText(cell)); + }) + ); +} + +function h2StationRenderPrdMarkdown(markdown) { + var lines = String(markdown || '').split(/\r?\n/); + var nodes = []; + var i = 0; + var inCode = false; + var codeBuf = []; + + while (i < lines.length) { + var line = lines[i]; + var trimmed = String(line || '').trim(); + + if (trimmed.indexOf('```') === 0) { + if (inCode) { + nodes.push(React.createElement('pre', { + key: 'code-' + i, + style: { + margin: '12px 0', + padding: '12px 14px', + background: '#f6f8fa', + border: '1px solid #e5e7eb', + borderRadius: 8, + fontSize: 12, + lineHeight: 1.6, + overflow: 'auto', + color: '#334155', + whiteSpace: 'pre-wrap' + } + }, codeBuf.join('\n'))); + codeBuf = []; + inCode = false; + } else { + inCode = true; + } + i += 1; + continue; + } + + if (inCode) { + codeBuf.push(line); + i += 1; + continue; + } + + if (trimmed === '---') { + nodes.push(React.createElement('hr', { key: 'hr-' + i, style: { border: 'none', borderTop: '1px solid #e8ecf0', margin: '20px 0' } })); + i += 1; + continue; + } + + if (h2StationIsPrdTableRow(trimmed)) { + var tableLines = []; + while (i < lines.length && h2StationIsPrdTableRow(String(lines[i]).trim())) { + tableLines.push(String(lines[i]).trim()); + i += 1; + } + var bodyRows = []; + var ti; + for (ti = 0; ti < tableLines.length; ti++) { + if (h2StationIsPrdTableSep(tableLines[ti])) continue; + bodyRows.push(h2StationRenderPrdTableRow(tableLines[ti], 'tr-' + i + '-' + ti, ti === 0)); + } + if (bodyRows.length) { + nodes.push(React.createElement('div', { key: 'tbl-' + i, style: { overflowX: 'auto', margin: '12px 0 16px' } }, + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 13 } }, + React.createElement('tbody', null, bodyRows) + ) + )); + } + continue; + } + + if (!trimmed) { + i += 1; + continue; + } + + if (trimmed.indexOf('# ') === 0) { + nodes.push(React.createElement('h1', { key: 'h1-' + i, style: { fontSize: 20, fontWeight: 700, color: '#0f172a', margin: '0 0 16px', lineHeight: 1.35 } }, h2StationParsePrdInlineText(trimmed.slice(2).trim()))); + i += 1; + continue; + } + + if (trimmed.indexOf('## ') === 0) { + nodes.push(React.createElement('h2', { key: 'h2-' + i, style: { fontSize: 16, fontWeight: 700, color: '#1e293b', margin: '24px 0 12px', paddingBottom: 6, borderBottom: '2px solid #e0f2fe', lineHeight: 1.4 } }, h2StationParsePrdInlineText(trimmed.slice(3).trim()))); + i += 1; + continue; + } + + if (trimmed.indexOf('### ') === 0) { + nodes.push(React.createElement('h3', { key: 'h3-' + i, style: { fontSize: 14, fontWeight: 600, color: '#334155', margin: '16px 0 8px', lineHeight: 1.45 } }, h2StationParsePrdInlineText(trimmed.slice(4).trim()))); + i += 1; + continue; + } + + if (/^!\[([^\]]*)\]\(([^)]+)\)$/.test(trimmed)) { + var imgMatch = trimmed.match(/^!\[([^\]]*)\]\(([^)]+)\)$/); + var imgAlt = imgMatch[1] || '配图'; + var imgSrc = imgMatch[2] || ''; + nodes.push(React.createElement('figure', { + key: 'img-' + i, + style: { margin: '16px 0 20px', textAlign: 'center' } + }, + React.createElement('img', { + src: imgSrc, + alt: imgAlt, + style: { + maxWidth: '100%', + borderRadius: 10, + border: '1px solid #e5e7eb', + boxShadow: '0 4px 12px rgba(15,23,42,0.06)' + }, + onError: function (e) { + if (e && e.target) { + e.target.style.display = 'none'; + } + } + }), + React.createElement('figcaption', { + style: { fontSize: 12, color: '#94a3b8', marginTop: 8, lineHeight: 1.5 } + }, imgAlt) + )); + i += 1; + continue; + } + + if (trimmed === '**文档结束**') { + nodes.push(React.createElement('div', { key: 'end-' + i, style: { marginTop: 24, paddingTop: 16, borderTop: '1px dashed #e2e8f0', color: '#94a3b8', fontSize: 13, textAlign: 'center' } }, '— 文档结束 —')); + i += 1; + continue; + } + + if (/^\d+\.\s/.test(trimmed)) { + nodes.push(React.createElement('div', { key: 'ol-' + i, style: { fontSize: 13, color: '#475569', lineHeight: 1.75, margin: '6px 0 6px 4px', paddingLeft: 4 } }, h2StationParsePrdInlineText(trimmed))); + i += 1; + continue; + } + + if (trimmed.indexOf('- ') === 0) { + nodes.push(React.createElement('div', { key: 'ul-' + i, style: { display: 'flex', gap: 8, fontSize: 13, color: '#475569', lineHeight: 1.75, margin: '4px 0 4px 2px' } }, + React.createElement('span', { style: { color: '#1677ff', flexShrink: 0 } }, '•'), + React.createElement('span', { style: { flex: 1 } }, h2StationParsePrdInlineText(trimmed.slice(2).trim())) + )); + i += 1; + continue; + } + + if (trimmed.indexOf('> ') === 0) { + nodes.push(React.createElement('div', { key: 'quote-' + i, style: { margin: '8px 0', padding: '8px 12px', borderLeft: '3px solid #c4b5fd', background: '#faf5ff', borderRadius: '0 8px 8px 0', fontSize: 13, color: '#5b21b6', lineHeight: 1.65 } }, h2StationParsePrdInlineText(trimmed.slice(2).trim()))); + i += 1; + continue; + } + + nodes.push(React.createElement('p', { key: 'p-' + i, style: { fontSize: 13, color: '#475569', lineHeight: 1.75, margin: '6px 0' } }, h2StationParsePrdInlineText(trimmed))); + i += 1; + } + + return nodes; +} + +function renderH2StationRequirementDocPanel() { + return React.createElement('div', { className: 'h2-req-doc-panel', style: { padding: '4px 4px 16px' } }, + h2StationRenderPrdMarkdown(H2_STATION_REQUIREMENT_DOC) + ); +} + +function renderH2StationUserManualDocPanel() { + return React.createElement('div', { className: 'h2-req-doc-panel h2-user-manual-panel', style: { padding: '4px 4px 16px' } }, + h2StationRenderPrdMarkdown(H2_STATION_USER_MANUAL_DOC) + ); +} + +var H2_PRIMARY_BTN_STYLE = { + borderRadius: 8, + fontWeight: 600, + background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)', + border: 'none' +}; + +var H2_REQ_BTN_STYLE = { + borderRadius: 8, + border: '1px solid #cbd5e1', + fontWeight: 600, + display: 'inline-flex', + alignItems: 'center', + boxShadow: '0 1px 2px rgba(0,0,0,0.05)', + color: '#475569' +}; + +function h2ResolveRecordSupplierMode(record) { + if (!record) return 'link'; + if (record.supplierMode === 'none') return 'none'; + if (record.supplierMode === 'link' || record.linkedSupplierId) return 'link'; + if (record.supplierMode === 'new') return 'new'; + var supplier = h2ResolveStationSupplier(record); + if (supplier && (supplier.name || '').trim()) return 'new'; + return 'none'; +} + +function h2SupplierModeLabel(mode) { + if (mode === 'link') return '关联已有供应商'; + if (mode === 'new') return '新增供应商'; + if (mode === 'none') return '不关联供应商'; + return '—'; +} + +function h2CreateFormDirty(station, supplier, supplierMode, linkedSupplierId) { + if ((station.name || '').trim()) return true; + if ((station.contact || '').trim()) return true; + if ((station.mobilePhone || '').trim() || (station.landlinePhone || '').trim()) return true; + if (station.address && ((station.address.region && station.address.region.length) || (station.address.detail || '').trim())) return true; + if ((station.mapAddress || '').trim()) return true; + if (station.latitude != null || station.longitude != null) return true; + if (supplierMode !== 'none') { + if ((supplier.signingCompany || '').trim()) return true; + if ((supplier.name || '').trim()) return true; + if (supplierMode === 'link' && linkedSupplierId) return true; + if ((supplier.businessLicenseFiles || []).length || (supplier.fillingLicenseFiles || []).length) return true; + } + if ((station.contractFiles || []).length) return true; + if (supplierMode !== 'none' && station.bindAccountMode === 'bind' && station.bindAccountIds && station.bindAccountIds.length) return true; + return false; +} + +function h2EditFormDirty(station, originForm) { + if (!originForm) return h2CreateFormDirty(station, h2CreateEmptySupplierForm(), 'new', undefined); + var keys = ['name', 'contact', 'mobilePhone', 'landlinePhone', 'businessHours', 'isSigned', 'contractStart', 'contractEnd', 'bindAccountMode', 'mapAddress', 'latitude', 'longitude']; + var i; + for (i = 0; i < keys.length; i++) { + if (String(station[keys[i]] || '') !== String(originForm[keys[i]] || '')) return true; + } + if (JSON.stringify(station.address || {}) !== JSON.stringify(originForm.address || {})) return true; + if (JSON.stringify(station.bindAccountIds || []) !== JSON.stringify(originForm.bindAccountIds || [])) return true; + if ((station.contractFiles || []).length !== (originForm.contractFiles || []).length) return true; + return false; +} + +function h2EditFormProgress(station) { + var checks = [ + !!(station.name || '').trim(), + station.address && station.address.region && station.address.region.length >= 2 && !!(station.address.detail || '').trim(), + !!(station.contact || '').trim(), + !!((station.mobilePhone || '').trim() || (station.landlinePhone || '').trim()) + ]; + var done = checks.filter(Boolean).length; + return Math.round((done / checks.length) * 100); +} + +function h2CreateFormProgress(station, supplier, supplierMode, linkedSupplierId) { + var checks = [ + !!(station.name || '').trim(), + station.address && station.address.region && station.address.region.length >= 2 && !!(station.address.detail || '').trim(), + !!(station.contact || '').trim(), + !!((station.mobilePhone || '').trim() || (station.landlinePhone || '').trim()), + supplierMode === 'none' ? true : (supplierMode === 'link' ? !!linkedSupplierId : !!(supplier.name || '').trim()) + ]; + var done = checks.filter(Boolean).length; + return Math.round((done / checks.length) * 100); +} + +function h2CardTitleWithStep(step, text) { + return React.createElement('span', { className: 'h2-card-title-bar h2-card-title-bar--step' }, + React.createElement('span', { className: 'h2-card-step-badge', 'aria-hidden': true }, step), + React.createElement('span', { className: 'h2-card-title-text' }, text) + ); +} + +function h2MaskMobile(mobile) { + if (!mobile) return '—'; + var s = String(mobile); + if (s.length >= 11) return s.slice(0, 3) + '****' + s.slice(-4); + return s; +} + +function h2AccountRoleTagColor(role) { + var text = String(role || ''); + if (text.indexOf('管理') >= 0) return 'success'; + if (text.indexOf('运营') >= 0) return 'processing'; + return 'default'; +} + +function h2BuildStationCreateView(ctx) { + var antd = window.antd; + var Card = antd.Card; + var Button = antd.Button; + var Input = antd.Input; + var Select = antd.Select; + var Cascader = antd.Cascader; + var Radio = antd.Radio; + var DatePicker = antd.DatePicker; + var Upload = antd.Upload; + var Form = antd.Form; + var Row = antd.Row; + var Col = antd.Col; + var Tag = antd.Tag; + var Divider = antd.Divider; + var dayjs = window.dayjs; + + var inputCls = 'h2-create-input'; + var pageMode = ctx.pageMode || 'create'; + var isEdit = pageMode === 'edit'; + var showSupplier = ctx.showSupplier !== false; + var accountEditable = ctx.accountEditable !== false; + var stationAddr = ctx.station.address || { region: [], detail: '' }; + var bindAccountIds = ctx.station.bindAccountIds || []; + var showAccountBind = ctx.supplierMode !== 'none'; + + var formItem = function (label, required, node, extra) { + return React.createElement(Form.Item, { + label: required ? React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), label) : label, + extra: extra + }, node); + }; + + var H2_CREATE_GUTTER = [24, 20]; + + var formRow = function () { + return React.createElement.apply(null, [Row, { gutter: H2_CREATE_GUTTER, align: 'stretch' }].concat(Array.prototype.slice.call(arguments))); + }; + + var col24 = function (node) { + return React.createElement(Col, { xs: 24, span: 24 }, node); + }; + + var col16 = function (node) { + return React.createElement(Col, { xs: 24, lg: 16 }, node); + }; + + var col12 = function (node) { + return React.createElement(Col, { xs: 24, md: 12 }, node); + }; + + var col8 = function (node) { + return React.createElement(Col, { xs: 24, lg: 8 }, node); + }; + + var gridBlock = function () { + var rows = Array.prototype.slice.call(arguments, 0); + return React.createElement('div', { className: 'h2-create-form-grid' }, rows); + }; + + var gridSubsection = function (text) { + return formRow(col24(React.createElement('div', { className: 'h2-create-subsection-title' }, text))); + }; + + var renderAccountBindLayout = function () { + var removeAccount = function (accountId) { + var nextIds = bindAccountIds.filter(function (id) { return id !== accountId; }); + ctx.updateStation({ + bindAccountMode: nextIds.length ? 'bind' : 'none', + bindAccountIds: nextIds + }); + }; + return React.createElement('div', { className: 'h2-account-bind-layout' }, + React.createElement('div', { className: 'h2-account-bind-side' }, + formItem('绑定系统账号', false, React.createElement(Select, { + className: inputCls, + mode: 'multiple', + showSearch: true, + placeholder: '请选择要绑定的系统账号', + value: bindAccountIds, + options: systemAccountOptions, + optionFilterProp: 'label', + maxTagCount: 'responsive', + onChange: function (v) { + var ids = v || []; + ctx.updateStation({ bindAccountMode: ids.length ? 'bind' : 'none', bindAccountIds: ids }); + }, + dropdownStyle: { borderRadius: 8 } + })), + React.createElement('p', { className: 'h2-account-bind-side__hint' }, '可选择多个账号,绑定后可使用对应账号登录系统'), + React.createElement('div', { className: 'h2-account-bind-selected-row' }, + React.createElement('span', { className: 'h2-account-bind-selected-label' }, '已选账号'), + React.createElement(Tag, { color: 'success', className: 'h2-account-bind-selected-count' }, selectedSystemAccounts.length + ' 个') + ) + ), + React.createElement('div', { className: 'h2-account-bind-preview-area' }, + React.createElement('div', { className: 'h2-account-bind-preview-title' }, '账号预览'), + selectedSystemAccounts.length + ? React.createElement('div', { className: 'h2-account-bind-preview-cards' }, + selectedSystemAccounts.map(function (acc) { + return React.createElement('div', { key: acc.id, className: 'h2-account-bind-user-card' }, + React.createElement('button', { + type: 'button', + className: 'h2-account-bind-user-card__remove', + 'aria-label': '移除账号 ' + (acc.displayName || acc.username || ''), + onClick: function () { removeAccount(acc.id); } + }, '×'), + React.createElement('span', { className: 'h2-account-bind-user-card__avatar', 'aria-hidden': true }, H2_ICONS.user), + React.createElement('div', { className: 'h2-account-bind-user-card__body' }, + React.createElement('div', { className: 'h2-account-bind-user-card__name-row' }, + React.createElement('span', { className: 'h2-account-bind-user-card__name' }, acc.displayName || acc.username || '—'), + acc.role + ? React.createElement(Tag, { color: h2AccountRoleTagColor(acc.role), className: 'h2-account-bind-user-card__role' }, acc.role) + : null + ), + React.createElement('div', { className: 'h2-account-bind-user-card__meta' }, '账号:' + (acc.username || '—')), + React.createElement('div', { className: 'h2-account-bind-user-card__meta' }, '手机号:' + h2MaskMobile(acc.mobile)) + ) + ); + }) + ) + : React.createElement('div', { className: 'h2-account-bind-preview-empty' }, '请在左侧选择系统账号,预览将显示在此处') + ) + ); + }; + + var uploadDragger = function (fileList, onChange, btnText, hint, disabled, options) { + options = options || {}; + var uploadProps = { + className: 'h2-create-upload' + (disabled ? ' h2-create-upload--readonly' : ''), + multiple: options.multiple !== false, + disabled: disabled, + fileList: fileList, + beforeUpload: function () { return false; }, + onChange: onChange, + showUploadList: true + }; + if (options.maxCount != null) uploadProps.maxCount = options.maxCount; + return React.createElement(Upload.Dragger, uploadProps, + React.createElement('p', { className: 'ant-upload-drag-icon', style: { marginBottom: 8 } }, H2_ICONS.upload), + React.createElement('p', { style: { margin: 0, fontWeight: 600, color: '#334155' } }, btnText), + React.createElement('p', { className: 'h2-create-upload-hint' }, hint) + ); + }; + + var supplierOptions = H2_MOCK_EXISTING_SUPPLIERS.map(function (s) { + return { + value: s.id, + label: s.name + (s.type ? '(' + s.type + ')' : '') + (s.city && s.city.length ? ' · ' + s.city.join('-') : '') + }; + }); + var lingniuSigningCompanyOptions = h2GetLingniuSigningCompanyOptions(); + + var supplierReadonlyInput = function (value, placeholder) { + return React.createElement(Input, { + className: inputCls + ' h2-create-readonly-input', + value: value != null && value !== '' ? String(value) : '', + placeholder: placeholder || '—', + disabled: true + }); + }; + + var renderSigningCompanySelect = function (supplier) { + return formItem('签约公司', true, React.createElement(Select, { + className: inputCls, + showSearch: true, + placeholder: '请选择羚牛签约主体', + value: supplier.signingCompany, + options: lingniuSigningCompanyOptions, + optionFilterProp: 'label', + onChange: function (v) { ctx.updateSupplier({ signingCompany: v }); }, + dropdownStyle: { borderRadius: 8 } + })); + }; + + var renderSupplierDetailFields = function (opts) { + opts = opts || {}; + var supplier = opts.supplier || ctx.supplier; + var readonly = !!opts.readonly; + var licensesEditable = !!opts.licensesEditable; + var licensesReadonly = readonly && !licensesEditable; + var rows = [ + gridSubsection('主体信息') + ]; + if (!readonly) { + rows.push(formRow(col16(renderSigningCompanySelect(supplier)))); + } + rows.push( + formRow( + col8(formItem('供应商名称', true, readonly + ? supplierReadonlyInput(supplier.name) + : React.createElement(Input, { + className: inputCls, + value: supplier.name, + placeholder: '请输入供应商名称', + onChange: function (e) { ctx.updateSupplier({ name: e.target.value }); } + }))), + col8(formItem('省市', false, supplierReadonlyInput( + supplier.city && supplier.city.length ? h2FormatRegion(supplier.city) : '', + '自动从地址解析' + ))), + col8(formItem('详细地址', false, supplierReadonlyInput(supplier.address, '自动从地址解析'))) + ) + ); + if (!readonly) { + rows.push(formRow( + col24(formItem('地址解析', false, React.createElement(AddressPasteInput, { + inputClassName: inputCls, + onParsed: function (parsed) { + ctx.updateSupplier({ + city: parsed.region, + address: parsed.detail + }); + } + }))) + )); + } + rows.push( + gridSubsection('开票信息'), + formRow( + col8(formItem('纳税人识别号', false, readonly + ? supplierReadonlyInput(supplier.taxId) + : React.createElement(Input, { + className: inputCls, + value: supplier.taxId, + placeholder: '请输入纳税人识别号', + onChange: function (e) { ctx.updateSupplier({ taxId: e.target.value }); } + }))), + col8(formItem('注册电话', false, readonly + ? supplierReadonlyInput(supplier.invoicePhone) + : React.createElement(Input, { + className: inputCls, + value: supplier.invoicePhone, + placeholder: '手机号或固定电话', + type: 'tel', + onChange: function (e) { ctx.updateSupplier({ invoicePhone: e.target.value }); } + }))), + col8(formItem('注册地址', false, readonly + ? supplierReadonlyInput(supplier.invoiceAddress) + : React.createElement(Input, { + className: inputCls, + value: supplier.invoiceAddress, + placeholder: '请输入注册地址', + onChange: function (e) { ctx.updateSupplier({ invoiceAddress: e.target.value }); } + }))) + ), + gridSubsection('银行账户'), + formRow( + col8(formItem('开户行', false, readonly + ? supplierReadonlyInput(supplier.bankName) + : React.createElement(Input, { + className: inputCls, + value: supplier.bankName, + placeholder: '请输入开户行', + onChange: function (e) { ctx.updateSupplier({ bankName: e.target.value }); } + }))), + col16(formItem('银行账号', false, readonly + ? supplierReadonlyInput(supplier.bankAccount) + : React.createElement(Input, { + className: inputCls, + value: supplier.bankAccount, + placeholder: '请输入银行账号', + onChange: function (e) { ctx.updateSupplier({ bankAccount: e.target.value }); } + }))) + ), + gridSubsection('资质证照'), + formRow( + col12(formItem('营业执照', false, uploadDragger( + supplier.businessLicenseFiles, + function (info) { + ctx.updateSupplier({ businessLicenseFiles: info.fileList }); + if (licensesReadonly) return; + if (info.file && info.file.status !== 'removed') { + h2TriggerBusinessLicenseOcr(info.file, ctx); + } + }, + licensesReadonly ? '已关联证照' : '点击或拖拽上传', + licensesReadonly ? '仅展示,不可修改' : '支持 OCR 识别,上传后自动反写名称、税号、通讯地址', + licensesReadonly, + { multiple: false } + ))), + col12(formItem('其他证件', false, uploadDragger( + supplier.fillingLicenseFiles, + licensesReadonly ? function () {} : function (info) { ctx.updateSupplier({ fillingLicenseFiles: info.fileList }); }, + licensesReadonly ? '已关联证照' : '点击或拖拽上传', + licensesReadonly ? '仅展示,不可修改' : '支持多文件上传,不限数量,PDF、图片', + licensesReadonly, + { multiple: true } + ))) + ) + ); + return gridBlock.apply(null, rows); + }; + + var renderSupplierLinkForm = function () { + return React.createElement(React.Fragment, null, + gridBlock( + formRow( + col16(formItem('选择供应商', true, React.createElement(Select, { + className: inputCls, + showSearch: true, + placeholder: '请搜索并选择供应商', + value: ctx.linkedSupplierId, + options: supplierOptions, + optionFilterProp: 'label', + onChange: ctx.handleLinkSupplierChange, + dropdownStyle: { borderRadius: 8 } + }))), + col8(renderSigningCompanySelect(ctx.supplier)) + ) + ), + ctx.linkedSupplierId + ? React.createElement('div', { className: 'h2-supplier-linked-preview' }, + renderSupplierDetailFields({ readonly: true, licensesEditable: true, supplier: ctx.supplier })) + : null + ); + }; + + var renderSupplierNewForm = function () { + return renderSupplierDetailFields({ readonly: false }); + }; + + var renderSupplierNonePanel = function () { + return gridBlock( + formRow( + col24(React.createElement('p', { className: 'h2-supplier-none-hint' }, '当前不关联供应商,无需填写或关联任何供应商信息。')) + ) + ); + }; + + var renderSupplierTabs = function () { + var tabs = [ + { key: 'link', label: '关联已有' }, + { key: 'new', label: '新建供应商' }, + { key: 'none', label: '不关联供应商' } + ]; + return React.createElement('div', { className: 'h2-supplier-tabs' }, + React.createElement('div', { className: 'h2-supplier-tabs__nav', role: 'tablist', 'aria-label': '供应商关联方式' }, + tabs.map(function (tab) { + var active = ctx.supplierMode === tab.key; + return React.createElement('button', { + type: 'button', + key: tab.key, + role: 'tab', + className: 'h2-supplier-tabs__item' + (active ? ' h2-supplier-tabs__item--active' : ''), + 'aria-selected': active, + onClick: function () { + if (ctx.supplierMode !== tab.key) { + ctx.handleSupplierModeChange({ target: { value: tab.key } }); + } + } + }, tab.label); + }) + ), + React.createElement('div', { className: 'h2-supplier-tabs__panel', role: 'tabpanel' }, + ctx.supplierMode === 'link' + ? renderSupplierLinkForm() + : ctx.supplierMode === 'new' + ? renderSupplierNewForm() + : renderSupplierNonePanel() + ) + ); + }; + + var usedBindAccountMap = ctx.usedBindAccountMap || {}; + var systemAccountOptions = H2_MOCK_SYSTEM_ACCOUNTS.filter(function (a) { + return !usedBindAccountMap[a.id]; + }).map(function (a) { + return { + value: a.id, + label: '「' + (a.username || '—') + '」「' + (a.role || '—') + '」' + }; + }); + var selectedSystemAccounts = h2BuildBoundAccountsFromIds(bindAccountIds); + + var contractDateRangeValue = null; + if (dayjs && ctx.station.contractStart && ctx.station.contractEnd) { + var ds = dayjs(ctx.station.contractStart); + var de = dayjs(ctx.station.contractEnd); + if (ds.isValid() && de.isValid()) contractDateRangeValue = [ds, de]; + } + + var mobilePhoneVal = (ctx.station.mobilePhone || '').trim(); + var landlinePhoneVal = (ctx.station.landlinePhone || '').trim(); + var mobilePhoneError = mobilePhoneVal && !h2IsMobilePhone(mobilePhoneVal) + ? h2GetMobilePhoneInputTip(mobilePhoneVal) + : ''; + var landlinePhoneError = landlinePhoneVal && !h2IsLandlinePhone(landlinePhoneVal) + ? h2GetLandlinePhoneInputTip(landlinePhoneVal) + : ''; + + var renderContractDates = function () { + if (dayjs && DatePicker && DatePicker.RangePicker) { + return React.createElement(DatePicker.RangePicker, { + className: inputCls, + style: { width: '100%' }, + format: 'YYYY-MM-DD', + placeholder: ['开始日期', '结束日期'], + value: contractDateRangeValue, + onChange: function (dates) { + if (!dates || dates.length < 2) { + ctx.updateStation({ contractStart: '', contractEnd: '' }); + return; + } + ctx.updateStation({ + contractStart: dayjs(dates[0]).format('YYYY-MM-DD'), + contractEnd: dayjs(dates[1]).format('YYYY-MM-DD') + }); + } + }); + } + return React.createElement('div', { className: 'h2-contract-dates' }, + React.createElement(Input, { + className: inputCls, + type: 'date', + value: ctx.station.contractStart || '', + style: { flex: 1, minWidth: 0 }, + onChange: function (e) { ctx.updateStation({ contractStart: e.target.value }); } + }), + React.createElement('span', { className: 'h2-contract-dates-sep' }, '至'), + React.createElement(Input, { + className: inputCls, + type: 'date', + value: ctx.station.contractEnd || '', + style: { flex: 1, minWidth: 0 }, + onChange: function (e) { ctx.updateStation({ contractEnd: e.target.value }); } + }) + ); + }; + + return React.createElement(React.Fragment, null, + React.createElement('div', { className: 'h2-create-shell' }, + React.createElement('div', { className: 'h2-create-back-only' }, + React.createElement(Button, { + className: 'h2-create-back-btn', + type: 'default', + icon: H2_ICONS.back, + onClick: ctx.onCancel, + disabled: ctx.submitting + }, '返回') + ), + + React.createElement(Form, { layout: 'vertical', requiredMark: false, className: 'h2-create-form h2-create-form--list h2-create-form--grid' }, + React.createElement(Card, { + className: 'h2-create-card', + id: 'h2-create-station-card', + title: h2CardTitleWithStep(1, '加氢站基本信息'), + bordered: false + }, + gridBlock( + formRow( + col8(formItem('加氢站名称', true, React.createElement(Input, { + className: inputCls, + value: ctx.station.name, + placeholder: '请输入加氢站名称', + maxLength: 80, + onChange: function (e) { ctx.updateStation({ name: e.target.value }); }, + onBlur: ctx.syncSupplierFromStation + }))), + col8(formItem('省市', true, React.createElement(Input, { + className: inputCls + ' h2-create-readonly-input', + value: stationAddr.region && stationAddr.region.length ? h2FormatRegion(stationAddr.region) : '', + placeholder: '自动从地址解析', + disabled: true + }))), + col8(formItem('通讯地址', true, React.createElement(Input, { + className: inputCls + ' h2-create-readonly-input', + value: stationAddr.detail || '', + placeholder: '自动从地址解析', + disabled: true + }))) + ), + formRow( + col24(formItem('地址解析', false, React.createElement(AddressPasteInput, { + inputClassName: inputCls, + onParsed: function (parsed) { + var fullAddr = h2BuildFullAddress(parsed.region, parsed.detail); + ctx.updateStation({ + address: { region: parsed.region, detail: parsed.detail }, + mapAddress: fullAddr + }); + if (ctx.supplierMode === 'new' && parsed.region && parsed.region.length >= 2) { + ctx.updateSupplier({ + city: parsed.region.slice(), + address: parsed.detail || '' + }); + } + } + }))) + ), + formRow( + col24(formItem('地图定位', false, React.createElement(StationMapPicker, { + inputClassName: inputCls, + address: ctx.station.mapAddress || h2BuildFullAddress(stationAddr.region, stationAddr.detail) || '', + latitude: ctx.station.latitude, + longitude: ctx.station.longitude, + onAddressChange: function (addr) { ctx.updateStation({ mapAddress: addr }); }, + onPositionChange: function (lat, lng) { + ctx.updateStation({ latitude: lat, longitude: lng }); + } + }))) + ), + formRow( + col8(formItem('联系人', true, React.createElement(Input, { + className: inputCls, + value: ctx.station.contact, + placeholder: '请输入联系人', + onChange: function (e) { ctx.updateStation({ contact: e.target.value }); }, + onBlur: ctx.syncSupplierFromStation + }))), + col8(formItem('手机号', false, React.createElement(Input, { + className: inputCls, + value: ctx.station.mobilePhone || '', + placeholder: '请输入手机号码', + type: 'tel', + autoComplete: 'tel', + maxLength: 11, + status: mobilePhoneError ? 'error' : undefined, + onChange: function (e) { + ctx.updateStation({ mobilePhone: h2FilterMobilePhoneInput(e.target.value) }); + }, + onBlur: ctx.syncSupplierFromStation + }), mobilePhoneError)), + col8(formItem('固定电话', false, React.createElement(Input, { + className: inputCls, + value: ctx.station.landlinePhone || '', + placeholder: '请输入固定电话', + type: 'tel', + autoComplete: 'tel', + status: landlinePhoneError ? 'error' : undefined, + onChange: function (e) { + ctx.updateStation({ landlinePhone: h2FilterLandlinePhoneInput(e.target.value) }); + }, + onBlur: ctx.syncSupplierFromStation + }), landlinePhoneError)) + ), + formRow( + col8(formItem('站点类型', false, React.createElement('div', { className: 'h2-station-type-field' }, + h2RenderOptionButtonGroup([ + { label: '签约站点', value: 'signed', tone: 'allday' }, + { label: '普通站点', value: 'unsigned', tone: 'custom' } + ], ctx.station.isSigned ? 'signed' : 'unsigned', function (v) { + var signed = v === 'signed'; + ctx.updateStation({ + isSigned: signed, + contractStart: signed ? ctx.station.contractStart : '', + contractEnd: signed ? ctx.station.contractEnd : '' + }); + }, { ariaLabel: '站点类型' }) + ))), + ctx.station.isSigned + ? col16(formItem('合作时间', true, renderContractDates())) + : null + ), + ctx.station.isSigned + ? formRow( + col24(formItem('合同附件', false, React.createElement('div', { className: 'h2-contract-panel' }, + React.createElement(ContractFilesUpload, { + fileList: ctx.station.contractFiles, + onChange: function (info) { ctx.updateStation({ contractFiles: info.fileList }); } + }) + ))) + ) + : null + ) + ), + + showSupplier ? React.createElement(Card, { + className: 'h2-create-card', + id: 'h2-create-supplier-card', + title: h2CardTitleWithStep(2, '供应商相关信息'), + bordered: false + }, + renderSupplierTabs() + ) : null, + + showSupplier && showAccountBind ? React.createElement(Card, { + className: 'h2-create-card', + id: 'h2-create-account-card', + title: h2CardTitleWithStep(3, '系统账号绑定'), + bordered: false + }, + gridBlock( + !accountEditable && isEdit + ? formRow( + col24(React.createElement('div', { className: 'h2-account-bind-readonly-wrap' }, + React.createElement('p', { className: 'h2-account-bind-hint', style: { marginBottom: 10 } }, + '当前为加氢站账号登录,系统账号绑定仅管理员(admin)可修改。' + ), + selectedSystemAccounts.length + ? selectedSystemAccounts.map(function (acc) { + return React.createElement('div', { key: acc.id, className: 'h2-account-bind-readonly-item' }, + React.createElement('div', { className: 'h2-account-bind-readonly-item__name' }, acc.displayName || acc.username || '—'), + React.createElement('div', { className: 'h2-account-bind-readonly-item__meta' }, + (acc.username || '—') + ' · ' + (acc.role || '—') + ' · ' + (acc.mobile || '—') + ) + ); + }) + : React.createElement('p', { className: 'h2-account-bind-hint' }, '暂未绑定系统账号') + )) + ) + : null, + accountEditable + ? formRow(col24(renderAccountBindLayout())) + : null + ) + ) : null + ) + ), + + React.createElement('footer', { className: 'h2-create-footer' }, + React.createElement('div', { className: 'h2-create-footer-inner' }, + React.createElement('div', { className: 'h2-create-footer-actions' }, + React.createElement(Button, { + onClick: ctx.onCancel, + disabled: ctx.submitting, + style: { borderRadius: 8 }, + 'aria-label': '取消并返回列表' + }, '取消'), + ctx.showReset ? React.createElement(Button, { + onClick: ctx.onReset, + disabled: ctx.submitting, + style: { borderRadius: 8 }, + 'aria-label': '重置表单' + }, '重置') : null, + React.createElement(Button, { + type: 'primary', + style: H2_PRIMARY_BTN_STYLE, + onClick: ctx.onSubmit, + loading: ctx.submitting, + 'aria-label': isEdit ? '保存编辑加氢站点' : '提交新建加氢站点' + }, ctx.submitLabel || (isEdit ? '保存修改' : '提交创建')) + ) + ) + ) + ); +} + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Table = antd.Table; + var Button = antd.Button; + var Select = antd.Select; + var Input = antd.Input; + var Space = antd.Space; + var Image = antd.Image; + var Modal = antd.Modal; + var Form = antd.Form; + var Switch = antd.Switch; + var Upload = antd.Upload; + var Tag = antd.Tag; + var Tooltip = antd.Tooltip; + var Divider = antd.Divider; + var Alert = antd.Alert; + var Dropdown = antd.Dropdown; + var Checkbox = antd.Checkbox; + var Descriptions = antd.Descriptions; + var Cascader = antd.Cascader; + var Radio = antd.Radio; + var DatePicker = antd.DatePicker; + var message = antd.message; + var dayjs = window.dayjs; + + var listState = useState(function () { + return h2ApplyDueCostPricesToList(H2_MOCK_STATIONS.slice()).records; + }); + var listData = listState[0]; + var setListData = listState[1]; + + var categoryTabState = useState('all'); + var categoryTab = categoryTabState[0]; + var setCategoryTab = categoryTabState[1]; + + var listFiltersState = useState(h2EmptyListFilters()); + var listFilters = listFiltersState[0]; + var setListFilters = listFiltersState[1]; + + var appliedFiltersState = useState(h2EmptyListFilters()); + var appliedFilters = appliedFiltersState[0]; + var setAppliedFilters = appliedFiltersState[1]; + + var deleteModalState = useState({ open: false, record: null }); + var deleteModal = deleteModalState[0]; + var setDeleteModal = deleteModalState[1]; + + var viewModalState = useState({ open: false, record: null }); + var viewModal = viewModalState[0]; + var setViewModal = viewModalState[1]; + + var filePreviewModalState = useState({ open: false, url: '', name: '', type: 'image' }); + var filePreviewModal = filePreviewModalState[0]; + var setFilePreviewModal = filePreviewModalState[1]; + + var requirementModalState = useState(false); + var requirementModalOpen = requirementModalState[0]; + var setRequirementModalOpen = requirementModalState[1]; + + var userManualModalState = useState(false); + var userManualModalOpen = userManualModalState[0]; + var setUserManualModalOpen = userManualModalState[1]; + + var importModalState = useState(false); + var importModalOpen = importModalState[0]; + var setImportModalOpen = importModalState[1]; + + var importFileListState = useState([]); + var importFileList = importFileListState[0]; + var setImportFileList = importFileListState[1]; + + var importPreviewState = useState(null); + var importPreview = importPreviewState[0]; + var setImportPreview = importPreviewState[1]; + + var pageState = useState(1); + var page = pageState[0]; + var setPage = pageState[1]; + + var pageSizeState = useState(10); + var pageSize = pageSizeState[0]; + var setPageSize = pageSizeState[1]; + + var refuelSortState = useState({ key: null, order: null }); + var refuelSort = refuelSortState[0]; + var setRefuelSort = refuelSortState[1]; + + var businessModalState = useState({ open: false, record: null, businessStatus: '营业中', businessHours: '', originBusinessStatus: '营业中', statusLogs: [] }); + var businessModal = businessModalState[0]; + var setBusinessModal = businessModalState[1]; + + var priceModalState = useState({ + open: false, + record: null, + priceConfigs: [], + priceLogs: [], + priceFormVisible: false, + editingConfigId: null, + priceType: 'fixed', + customerScope: 'all', + customerId: null, + customerName: null, + selectedCustomerIds: [], + tierCustomerPickerOpen: false, + costUnitPrice: '', + effectiveTime: '', + tierRules: [], + tierResetDay: 1 + }); + var priceModal = priceModalState[0]; + var setPriceModal = priceModalState[1]; + + var balanceAlertModalState = useState({ open: false, record: null, threshold: '' }); + var balanceAlertModal = balanceAlertModalState[0]; + var setBalanceAlertModal = balanceAlertModalState[1]; + + var globalBalanceAlertModalState = useState({ open: false, threshold: '' }); + var globalBalanceAlertModal = globalBalanceAlertModalState[0]; + var setGlobalBalanceAlertModal = globalBalanceAlertModalState[1]; + + var rechargeModalState = useState({ open: false, lines: [] }); + var rechargeModal = rechargeModalState[0]; + var setRechargeModal = rechargeModalState[1]; + + var alertStationModalState = useState({ open: false, type: '', title: '', stations: [] }); + var alertStationModal = alertStationModalState[0]; + var setAlertStationModal = alertStationModalState[1]; + + var refuelModalState = useState({ open: false, station: null, records: [] }); + var refuelModal = refuelModalState[0]; + var setRefuelModal = refuelModalState[1]; + + var ledgerStoreState = useState(function () { return h2InitLedgerRecordsStore(); }); + var ledgerStore = ledgerStoreState[0]; + var setLedgerStore = ledgerStoreState[1]; + + var statementRecordsState = useState(function () { return h2InitStatementRecordsStore(); }); + var statementRecords = statementRecordsState[0]; + var setStatementRecords = statementRecordsState[1]; + + var statementModalState = useState({ + open: false, + record: null, + startDate: '', + endDate: '', + phase: 'select', + draftRows: [], + balanceAfterSettlement: 0, + receiptDate: '', + receiptAmount: '', + invoiceFiles: [] + }); + var statementModal = statementModalState[0]; + var setStatementModal = statementModalState[1]; + + var statementHistoryModalState = useState({ open: false, record: null }); + var statementHistoryModal = statementHistoryModalState[0]; + var setStatementHistoryModal = statementHistoryModalState[1]; + + var statementDetailModalState = useState({ open: false, statementRecord: null, detailRows: [] }); + var statementDetailModal = statementDetailModalState[0]; + var setStatementDetailModal = statementDetailModalState[1]; + + var prepaidBalanceDrillState = useState({ open: false, stationName: '', endingBalance: 0, rows: [] }); + var prepaidBalanceDrill = prepaidBalanceDrillState[0]; + var setPrepaidBalanceDrill = prepaidBalanceDrillState[1]; + + var subViewState = useState('list'); + var subView = subViewState[0]; + var setSubView = subViewState[1]; + + var createStationState = useState(h2CreateEmptyStationForm()); + var createStation = createStationState[0]; + var setCreateStation = createStationState[1]; + + var createSupplierModeState = useState('link'); + var createSupplierMode = createSupplierModeState[0]; + var setCreateSupplierMode = createSupplierModeState[1]; + + var createLinkedSupplierIdState = useState(undefined); + var createLinkedSupplierId = createLinkedSupplierIdState[0]; + var setCreateLinkedSupplierId = createLinkedSupplierIdState[1]; + + var createSupplierState = useState(h2CreateEmptySupplierForm()); + var createSupplier = createSupplierState[0]; + var setCreateSupplier = createSupplierState[1]; + + var createSubmittingState = useState(false); + var createSubmitting = createSubmittingState[0]; + var setCreateSubmitting = createSubmittingState[1]; + + var editStationState = useState(h2CreateEmptyStationForm()); + var editStation = editStationState[0]; + var setEditStation = editStationState[1]; + + var editRecordState = useState(null); + var editRecord = editRecordState[0]; + var setEditRecord = editRecordState[1]; + + var editOriginFormState = useState(null); + var editOriginForm = editOriginFormState[0]; + var setEditOriginForm = editOriginFormState[1]; + + var editSubmittingState = useState(false); + var editSubmitting = editSubmittingState[0]; + var setEditSubmitting = editSubmittingState[1]; + + var editSupplierModeState = useState('link'); + var editSupplierMode = editSupplierModeState[0]; + var setEditSupplierMode = editSupplierModeState[1]; + + var editLinkedSupplierIdState = useState(undefined); + var editLinkedSupplierId = editLinkedSupplierIdState[0]; + var setEditLinkedSupplierId = editLinkedSupplierIdState[1]; + + var editSupplierState = useState(h2CreateEmptySupplierForm()); + var editSupplier = editSupplierState[0]; + var setEditSupplier = editSupplierState[1]; + + var isAdminUser = h2IsAdminUser(); + + var createSupplierReadonly = createSupplierMode === 'link' && !!createLinkedSupplierId; + + var existingNameMap = useMemo(function () { + var map = {}; + listData.forEach(function (r) { + if (r.name) map[String(r.name).trim()] = true; + }); + return map; + }, [listData]); + + var categoryCounts = useMemo(function () { + var counts = { all: listData.length, balanceAlert: 0, arrears: 0, none: 0 }; + listData.forEach(function (r) { + if (h2IsArrearsStation(r)) counts.arrears += 1; + else if (h2IsBalanceAlertStation(r)) counts.balanceAlert += 1; + var stats = h2CalcRefuelStats(r.name); + if (h2DeriveFrequencyByRefuelCount(stats.count) === 'none') counts.none += 1; + }); + return counts; + }, [listData]); + + var signedStats = useMemo(function () { + var signed = 0; + listData.forEach(function (r) { if (r.isSigned) signed += 1; }); + return { signed: signed, unsigned: listData.length - signed }; + }, [listData]); + + var filteredList = useMemo(function () { + var list = listData.slice(); + if (categoryTab !== 'all') { + list = list.filter(function (r) { + if (categoryTab === 'balanceAlert') return h2IsBalanceAlertStation(r); + if (categoryTab === 'arrears') return h2IsArrearsStation(r); + if (categoryTab === 'none') { + var stats = h2CalcRefuelStats(r.name); + return h2DeriveFrequencyByRefuelCount(stats.count) === 'none'; + } + return true; + }); + } + var kw = (appliedFilters.name || '').trim().toLowerCase(); + if (kw) list = list.filter(function (r) { return (r.name || '').toLowerCase().indexOf(kw) !== -1; }); + if (appliedFilters.signed === 'yes') list = list.filter(function (r) { return r.isSigned; }); + if (appliedFilters.signed === 'no') list = list.filter(function (r) { return !r.isSigned; }); + if (appliedFilters.region && appliedFilters.region.length) { + list = list.filter(function (r) { return h2MatchRegionFilter(r.region, appliedFilters.region); }); + } + if (appliedFilters.businessStatus) { + list = list.filter(function (r) { return r.businessStatus === appliedFilters.businessStatus; }); + } + return list.map(function (r) { + var stats = h2CalcRefuelStats(r.name); + return Object.assign({}, r, { + refuelCount: stats.count, + refuelTotalKg: stats.totalKg, + refuelFreqKey: h2DeriveFrequencyByRefuelCount(stats.count) + }); + }); + }, [listData, categoryTab, appliedFilters]); + + var sortedList = useMemo(function () { + var list = filteredList.slice(); + if (!refuelSort.key || !refuelSort.order) return list; + var dir = refuelSort.order === 'ascend' ? 1 : -1; + list.sort(function (a, b) { + var av = refuelSort.key === 'refuelCount' ? (a.refuelCount || 0) : (a.refuelTotalKg || 0); + var bv = refuelSort.key === 'refuelCount' ? (b.refuelCount || 0) : (b.refuelTotalKg || 0); + if (av === bv) return 0; + return av > bv ? dir : -dir; + }); + return list; + }, [filteredList, refuelSort]); + + var totalCount = sortedList.length; + + var displayList = useMemo(function () { + var start = (page - 1) * pageSize; + return sortedList.slice(start, start + pageSize); + }, [sortedList, page, pageSize]); + + var tablePagination = useMemo(function () { + return { + current: page, + pageSize: pageSize, + total: totalCount, + showSizeChanger: true, + pageSizeOptions: ['5', '10', '20', '50'], + showTotal: function (t) { return '共 ' + t + ' 条'; }, + onChange: function (p, size) { + setPage(p); + if (size && size !== pageSize) setPageSize(size); + } + }; + }, [page, pageSize, totalCount]); + + var handleKpiCardClick = useCallback(function (key) { + setCategoryTab(key); + if (key === 'all' || key === 'none') { + setListFilters(function (p) { return Object.assign({}, p, { signed: undefined }); }); + setAppliedFilters(function (p) { return Object.assign({}, p, { signed: undefined }); }); + } + setPage(1); + }, []); + + var handleSignedFilterCardClick = useCallback(function (key) { + var nextSigned = appliedFilters.signed === key ? undefined : key; + if (nextSigned) setCategoryTab('all'); + setListFilters(function (p) { return Object.assign({}, p, { signed: nextSigned }); }); + setAppliedFilters(function (p) { return Object.assign({}, p, { signed: nextSigned }); }); + setPage(1); + }, [appliedFilters.signed]); + + var handleListFilterQuery = useCallback(function () { + setAppliedFilters({ + name: listFilters.name, + signed: listFilters.signed, + region: listFilters.region, + businessStatus: listFilters.businessStatus + }); + if (listFilters.signed) setCategoryTab('all'); + setPage(1); + }, [listFilters]); + + var handleListFilterReset = useCallback(function () { + var empty = h2EmptyListFilters(); + setListFilters(empty); + setAppliedFilters(empty); + setCategoryTab('all'); + setPage(1); + }, []); + + var handleListTableChange = useCallback(function (_pagination, _filters, sorter) { + var nextSorter = Array.isArray(sorter) ? sorter[0] : sorter; + if (!nextSorter || !nextSorter.columnKey) { + setRefuelSort({ key: null, order: null }); + return; + } + if (nextSorter.columnKey !== 'refuelCount' && nextSorter.columnKey !== 'refuelTotalKg') return; + setRefuelSort({ + key: nextSorter.columnKey, + order: nextSorter.order || 'descend' + }); + setPage(1); + }, []); + + var renderFilterField = useCallback(function (label, control) { + return React.createElement('div', { className: 'lc-filter-field' }, + React.createElement('span', { className: 'lc-filter-field-label' }, label), + React.createElement('div', { className: 'lc-filter-field-control' }, control) + ); + }, []); + + var renderAlertStatCard = useCallback(function (card, options) { + var active = options.active; + var count = options.count; + var onClick = options.onClick; + var icon = options.icon; + return React.createElement('div', { + key: card.key, + role: 'button', + tabIndex: 0, + className: 'lc-alert-card lc-alert-card--' + card.type + ' lc-alert-card-clickable' + (active ? ' lc-alert-card-active' : ''), + onClick: onClick, + onKeyDown: function (e) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onClick(); + } + }, + 'aria-pressed': active, + 'aria-label': card.title + ',共 ' + count + ' 个' + }, + React.createElement('div', { className: 'lc-alert-card-tip-anchor' }, + React.createElement(Tooltip, { title: card.desc, placement: 'topRight', overlayStyle: { maxWidth: 320 } }, + React.createElement('span', { + className: 'lc-alert-card-tip', + role: 'img', + 'aria-label': card.title + '说明', + onClick: function (e) { e.stopPropagation(); }, + onMouseDown: function (e) { e.stopPropagation(); } + }, + h2SvgIcon([{ tag: 'circle', cx: 12, cy: 12, r: 10 }, { tag: 'line', x1: 12, y1: 16, x2: 12, y2: 12 }, { tag: 'line', x1: 12, y1: 8, x2: 12.01, y2: 8 }], 12) + ) + ) + ), + React.createElement('div', { className: 'lc-alert-card-icon', 'aria-hidden': true }, icon), + React.createElement('div', { className: 'lc-alert-card-main' }, + React.createElement('div', { className: 'lc-alert-card-title' }, card.title), + React.createElement('div', { className: 'lc-alert-card-val' }, count) + ) + ); + }, []); + + var resetCreateForm = useCallback(function () { + setCreateStation(h2CreateEmptyStationForm()); + setCreateSupplierMode('link'); + setCreateLinkedSupplierId(undefined); + setCreateSupplier(h2CreateEmptySupplierForm()); + }, []); + + var openCreate = useCallback(function () { + resetCreateForm(); + setSubView('create'); + }, [resetCreateForm]); + + var handleCreatePageBack = useCallback(function () { + setCreateSubmitting(false); + setSubView('list'); + }, []); + + var handleCreateCancel = useCallback(function () { + if (createSubmitting) return; + if (h2CreateFormDirty(createStation, createSupplier, createSupplierMode, createLinkedSupplierId)) { + Modal.confirm({ + title: '放弃未保存的更改?', + content: '当前填写内容尚未提交,返回列表后将丢失。', + okText: '放弃并返回', + cancelText: '继续填写', + okButtonProps: { danger: true }, + centered: true, + onOk: handleCreatePageBack + }); + return; + } + handleCreatePageBack(); + }, [createSubmitting, createStation, createSupplier, createSupplierMode, createLinkedSupplierId, handleCreatePageBack]); + + var updateCreateStation = useCallback(function (patch) { + setCreateStation(function (prev) { return Object.assign({}, prev, patch); }); + }, []); + + var updateCreateSupplier = useCallback(function (patch) { + setCreateSupplier(function (prev) { return Object.assign({}, prev, patch); }); + }, []); + + var handleCreateSupplierModeChange = useCallback(function (e) { + var nextMode = e.target.value; + setCreateSupplierMode(nextMode); + setCreateLinkedSupplierId(undefined); + setCreateSupplier(h2CreateEmptySupplierForm()); + if (nextMode === 'none') { + setCreateStation(function (prev) { + return Object.assign({}, prev, { bindAccountMode: 'none', bindAccountIds: [] }); + }); + } + }, []); + + var handleCreateLinkSupplierChange = useCallback(function (id) { + setCreateLinkedSupplierId(id); + var found = H2_MOCK_EXISTING_SUPPLIERS.filter(function (s) { return s.id === id; })[0]; + setCreateSupplier(h2SupplierToForm(found)); + }, []); + + var handleCreateSupplierCityChange = useCallback(function (v) { + updateCreateSupplier({ + city: v, + region: v && v[0] ? h2GetRegionByProvince(v[0]) : '' + }); + }, [updateCreateSupplier]); + + var syncCreateSupplierFromStation = useCallback(function () { + if (createSupplierMode !== 'new') return; + setCreateSupplier(function (supplier) { + var patch = {}; + if (createStation.name && !supplier.name) patch.name = createStation.name; + if (createStation.address && createStation.address.region && createStation.address.region.length >= 2) { + patch.city = createStation.address.region.slice(); + patch.region = h2GetRegionByProvince(createStation.address.region[0]); + } + if (createStation.address && createStation.address.detail && !supplier.address) patch.address = createStation.address.detail; + if (createStation.contact && !supplier.contactName) patch.contactName = createStation.contact; + if (createStation.mobilePhone && !supplier.contactMobile) patch.contactMobile = createStation.mobilePhone; + return Object.keys(patch).length ? Object.assign({}, supplier, patch) : supplier; + }); + }, [createSupplierMode, createStation]); + + var validateCreateForm = useCallback(function () { + if (!(createStation.name || '').trim()) { + message.warning('请填写加氢站名称'); + return false; + } + if (!createStation.address.region || createStation.address.region.length < 2) { + message.warning('请通过地址解析填写省 / 市'); + return false; + } + if (!(createStation.address.detail || '').trim()) { + message.warning('请通过地址解析填写通讯地址'); + return false; + } + if (!(createStation.contact || '').trim()) { + message.warning('请填写联系人'); + return false; + } + var createPhoneErr = h2ValidateStationPhones(createStation); + if (createPhoneErr) { + message.warning(createPhoneErr); + return false; + } + if (createStation.isSigned) { + if (!createStation.contractStart || !createStation.contractEnd) { + message.warning('签约站点请填写合作时间'); + return false; + } + } + var hoursParsed = h2ParseBusinessHours(createStation.businessHours); + if (hoursParsed.mode === 'custom') { + if (!(hoursParsed.start || '').trim() || !(hoursParsed.end || '').trim()) { + message.warning('非全天营业请选择开始与结束时间'); + return false; + } + if (!/^(\d{1,2}):(\d{2})$/.test(h2NormalizeTimeText(hoursParsed.start)) || !/^(\d{1,2}):(\d{2})$/.test(h2NormalizeTimeText(hoursParsed.end))) { + message.warning('营业时间格式须为 HH:mm'); + return false; + } + } + if (createSupplierMode === 'link' && !createLinkedSupplierId) { + message.warning('请选择要关联的已有供应商'); + return false; + } + if (createSupplierMode === 'new' && !(createSupplier.name || '').trim()) { + message.warning('请填写供应商名称'); + return false; + } + if (createSupplierMode !== 'none' && !(createSupplier.signingCompany || '').trim()) { + message.warning('请选择签约公司'); + return false; + } + if (createSupplierMode !== 'none' && createStation.bindAccountIds && createStation.bindAccountIds.length) { + var usedMapCreate = h2CollectUsedBindAccountMap(listData); + var ci; + for (ci = 0; ci < createStation.bindAccountIds.length; ci++) { + var boundName = usedMapCreate[createStation.bindAccountIds[ci]]; + if (boundName) { + message.warning('系统账号已绑定站点「' + boundName + '」,请重新选择'); + return false; + } + } + } + return true; + }, [createStation, createSupplierMode, createLinkedSupplierId, createSupplier, listData]); + + var handleCreatePageSubmit = useCallback(function (payload) { + var station = payload && payload.station; + if (!station) return; + var nextId = listData.reduce(function (m, r) { return Math.max(m, r.id || 0); }, 0) + 1; + var created = Object.assign({}, station, { id: nextId }); + setListData(function (prev) { return [created].concat(prev); }); + setPage(1); + setSubView('list'); + var supplierHint = payload.supplierMode === 'link' + ? '已关联已有供应商' + : payload.supplierMode === 'new' + ? '已同步创建加氢站类型供应商' + : '未关联供应商'; + var accountHint = ''; + if (station.bindAccountMode === 'bind' && station.boundAccounts && station.boundAccounts.length) { + accountHint = ';已绑定 ' + station.boundAccounts.length + ' 个系统账号'; + } + message.success('站点已创建;' + supplierHint + accountHint + '(原型)'); + }, [listData]); + + var handleCreateSubmit = useCallback(function () { + if (createSubmitting) return; + if (!validateCreateForm()) return; + setCreateSubmitting(true); + syncCreateSupplierFromStation(); + var now = h2OperateTimestamp(); + var phoneDisplay = createStation.mobilePhone || createStation.landlinePhone || ''; + if (createStation.mobilePhone && createStation.landlinePhone) phoneDisplay = createStation.mobilePhone; + var createBindIds = createSupplierMode === 'none' ? [] : (createStation.bindAccountIds || []).slice(); + var boundAccounts = h2BuildBoundAccountsFromIds(createBindIds); + var boundAccount = boundAccounts.length ? boundAccounts[0] : null; + var stationRecord = { + name: (createStation.name || '').trim(), + region: createStation.address.region || [], + addressDetail: (createStation.address.detail || '').trim(), + fullAddress: h2BuildFullAddress(createStation.address.region, createStation.address.detail), + mapAddress: (createStation.mapAddress || '').trim() || h2BuildFullAddress(createStation.address.region, createStation.address.detail), + latitude: h2NormalizeCoord(createStation.latitude), + longitude: h2NormalizeCoord(createStation.longitude), + isSigned: !!createStation.isSigned, + contractStart: createStation.isSigned ? createStation.contractStart : '', + contractEnd: createStation.isSigned ? createStation.contractEnd : '', + contractFiles: (createStation.contractFiles || []).map(function (f) { + return { uid: f.uid, name: f.name || '合同附件.pdf', url: f.url || '' }; + }), + businessStatus: createStation.businessStatus || '营业中', + businessHours: (createStation.businessHours || '').trim(), + contact: (createStation.contact || '').trim(), + phone: phoneDisplay, + mobilePhone: (createStation.mobilePhone || '').trim(), + landlinePhone: (createStation.landlinePhone || '').trim(), + costUnitPrice: createStation.costUnitPrice ? parseFloat(createStation.costUnitPrice) : undefined, + customerUnitPrice: createStation.customerUnitPrice ? parseFloat(createStation.customerUnitPrice) : undefined, + prepaidBalance: 0, + businessStatusLogs: [], + costPriceLogs: [], + updateTime: now, + supplierMode: createSupplierMode, + linkedSupplierId: createSupplierMode === 'link' ? createLinkedSupplierId : undefined, + supplier: createSupplierMode === 'none' ? undefined : Object.assign({ type: '加氢站' }, createSupplier), + bindAccountMode: createBindIds.length ? 'bind' : 'none', + bindAccountIds: createBindIds, + boundAccountIds: createBindIds, + boundAccountId: boundAccount ? boundAccount.id : undefined, + boundAccount: boundAccount, + boundAccounts: boundAccounts + }; + window.setTimeout(function () { + handleCreatePageSubmit({ station: stationRecord, supplier: createSupplier, supplierMode: createSupplierMode }); + setCreateSubmitting(false); + }, 380); + }, [createSubmitting, validateCreateForm, syncCreateSupplierFromStation, createStation, createSupplier, createSupplierMode, createLinkedSupplierId, handleCreatePageSubmit]); + + var handleEditPageBack = useCallback(function () { + setEditSubmitting(false); + setEditRecord(null); + setEditOriginForm(null); + setSubView('list'); + }, []); + + var handleEditCancel = useCallback(function () { + if (editSubmitting) return; + if (h2EditFormDirty(editStation, editOriginForm)) { + Modal.confirm({ + title: '放弃未保存的更改?', + content: '当前修改尚未保存,返回列表后将丢失。', + okText: '放弃并返回', + cancelText: '继续编辑', + okButtonProps: { danger: true }, + centered: true, + onOk: handleEditPageBack + }); + return; + } + handleEditPageBack(); + }, [editSubmitting, editStation, editOriginForm, handleEditPageBack]); + + var updateEditStation = useCallback(function (patch) { + setEditStation(function (prev) { return Object.assign({}, prev, patch); }); + }, []); + + var updateEditSupplier = useCallback(function (patch) { + setEditSupplier(function (prev) { return Object.assign({}, prev, patch); }); + }, []); + + var confirmEditSupplierChange = useCallback(function (applyChange) { + Modal.confirm({ + title: '确认修改供应商', + content: '修改供应商会影响对账时的供应商信息,请确认是否进行修改', + okText: '确认修改', + cancelText: '取消', + centered: true, + onOk: function () { + applyChange(); + } + }); + }, []); + + var handleEditSupplierModeChange = useCallback(function (e) { + var nextMode = e.target.value; + if (nextMode === editSupplierMode) return; + confirmEditSupplierChange(function () { + setEditSupplierMode(nextMode); + setEditLinkedSupplierId(undefined); + setEditSupplier(h2CreateEmptySupplierForm()); + if (nextMode === 'none') { + setEditStation(function (prev) { + return Object.assign({}, prev, { bindAccountMode: 'none', bindAccountIds: [] }); + }); + } + }); + }, [editSupplierMode, confirmEditSupplierChange]); + + var handleEditLinkSupplierChange = useCallback(function (id) { + if (id === editLinkedSupplierId) return; + confirmEditSupplierChange(function () { + setEditLinkedSupplierId(id); + var found = H2_MOCK_EXISTING_SUPPLIERS.filter(function (s) { return s.id === id; })[0]; + setEditSupplier(h2SupplierToForm(found)); + }); + }, [editLinkedSupplierId, confirmEditSupplierChange]); + + var handleEditSupplierCityChange = useCallback(function (v) { + updateEditSupplier({ + city: v, + region: v && v[0] ? h2GetRegionByProvince(v[0]) : '' + }); + }, [updateEditSupplier]); + + var syncEditSupplierFromStation = useCallback(function () { + if (editSupplierMode !== 'new') return; + setEditSupplier(function (supplier) { + var patch = {}; + if (editStation.name && !supplier.name) patch.name = editStation.name; + if (editStation.address && editStation.address.region && editStation.address.region.length >= 2) { + patch.city = editStation.address.region.slice(); + patch.region = h2GetRegionByProvince(editStation.address.region[0]); + } + if (editStation.address && editStation.address.detail && !supplier.address) patch.address = editStation.address.detail; + if (editStation.contact && !supplier.contactName) patch.contactName = editStation.contact; + if (editStation.mobilePhone && !supplier.contactMobile) patch.contactMobile = editStation.mobilePhone; + return Object.keys(patch).length ? Object.assign({}, supplier, patch) : supplier; + }); + }, [editSupplierMode, editStation]); + + var validateEditForm = useCallback(function () { + if (!(editStation.name || '').trim()) { + message.warning('请填写加氢站名称'); + return false; + } + if (!editStation.address.region || editStation.address.region.length < 2) { + message.warning('请通过地址解析填写省 / 市'); + return false; + } + if (!(editStation.address.detail || '').trim()) { + message.warning('请通过地址解析填写通讯地址'); + return false; + } + if (!(editStation.contact || '').trim()) { + message.warning('请填写联系人'); + return false; + } + var editPhoneErr = h2ValidateStationPhones(editStation); + if (editPhoneErr) { + message.warning(editPhoneErr); + return false; + } + if (editStation.isSigned) { + if (!editStation.contractStart || !editStation.contractEnd) { + message.warning('签约站点请填写合作时间'); + return false; + } + } + var hoursParsedEdit = h2ParseBusinessHours(editStation.businessHours); + if (hoursParsedEdit.mode === 'custom') { + if (!(hoursParsedEdit.start || '').trim() || !(hoursParsedEdit.end || '').trim()) { + message.warning('非全天营业请选择开始与结束时间'); + return false; + } + } + if (editSupplierMode === 'link' && !editLinkedSupplierId) { + message.warning('请选择要关联的已有供应商'); + return false; + } + if (editSupplierMode === 'new' && !(editSupplier.name || '').trim()) { + message.warning('请填写供应商名称'); + return false; + } + if (editSupplierMode !== 'none' && !(editSupplier.signingCompany || '').trim()) { + message.warning('请选择签约公司'); + return false; + } + if (isAdminUser && editSupplierMode !== 'none' && editStation.bindAccountIds && editStation.bindAccountIds.length) { + var usedMapEdit = h2CollectUsedBindAccountMap(listData, editRecord && editRecord.id); + var ei; + for (ei = 0; ei < editStation.bindAccountIds.length; ei++) { + var boundEditName = usedMapEdit[editStation.bindAccountIds[ei]]; + if (boundEditName) { + message.warning('系统账号已绑定站点「' + boundEditName + '」,请重新选择'); + return false; + } + } + } + return true; + }, [editStation, editRecord, editSupplierMode, editLinkedSupplierId, editSupplier, listData, isAdminUser]); + + var handleEditSubmit = useCallback(function () { + if (editSubmitting || !editRecord) return; + if (!validateEditForm()) return; + setEditSubmitting(true); + syncEditSupplierFromStation(); + var now = h2OperateTimestamp(); + var phoneDisplay = editStation.mobilePhone || editStation.landlinePhone || ''; + if (editStation.mobilePhone && editStation.landlinePhone) phoneDisplay = editStation.mobilePhone; + var nextBoundAccounts = []; + var nextBindMode = editRecord.bindAccountMode || 'none'; + var nextBindIds = h2NormalizeBindAccountIds(editRecord); + if (editSupplierMode === 'none') { + nextBindMode = 'none'; + nextBindIds = []; + nextBoundAccounts = []; + } else if (isAdminUser) { + nextBindIds = (editStation.bindAccountIds || []).slice(); + nextBindMode = nextBindIds.length ? 'bind' : 'none'; + nextBoundAccounts = h2BuildBoundAccountsFromIds(nextBindIds); + } else { + nextBoundAccounts = h2ResolveStationSystemAccounts(editRecord); + nextBindIds = h2NormalizeBindAccountIds(editRecord); + nextBindMode = editRecord.bindAccountMode || (nextBindIds.length ? 'bind' : 'none'); + } + var nextBoundAccount = nextBoundAccounts.length ? nextBoundAccounts[0] : null; + var recordId = editRecord.id; + setListData(function (prev) { + return prev.map(function (r) { + if (r.id !== recordId) return r; + return Object.assign({}, r, { + name: (editStation.name || '').trim(), + region: editStation.address.region || [], + addressDetail: (editStation.address.detail || '').trim(), + fullAddress: h2BuildFullAddress(editStation.address.region, editStation.address.detail), + mapAddress: (editStation.mapAddress || '').trim() || h2BuildFullAddress(editStation.address.region, editStation.address.detail), + latitude: h2NormalizeCoord(editStation.latitude), + longitude: h2NormalizeCoord(editStation.longitude), + isSigned: !!editStation.isSigned, + contractStart: editStation.isSigned ? editStation.contractStart : '', + contractEnd: editStation.isSigned ? editStation.contractEnd : '', + contractFiles: (editStation.contractFiles || []).map(function (f) { + return { uid: f.uid, name: f.name || '合同附件.pdf', url: f.url || '' }; + }), + businessHours: (editStation.businessHours || '').trim() || '—', + contact: (editStation.contact || '').trim(), + phone: phoneDisplay, + mobilePhone: (editStation.mobilePhone || '').trim(), + landlinePhone: (editStation.landlinePhone || '').trim(), + bindAccountMode: nextBindMode, + bindAccountIds: nextBindIds, + boundAccountIds: nextBindIds, + boundAccountId: nextBoundAccount ? nextBoundAccount.id : undefined, + boundAccount: nextBoundAccount, + boundAccounts: nextBoundAccounts, + supplierMode: editSupplierMode, + linkedSupplierId: editSupplierMode === 'link' ? editLinkedSupplierId : undefined, + supplier: editSupplierMode === 'none' ? undefined : Object.assign({ type: '加氢站' }, editSupplier), + updateTime: now + }); + }); + }); + window.setTimeout(function () { + message.success('站点已保存(原型)'); + setEditSubmitting(false); + handleEditPageBack(); + }, 320); + }, [editSubmitting, editRecord, editStation, editSupplierMode, editLinkedSupplierId, editSupplier, validateEditForm, syncEditSupplierFromStation, isAdminUser, handleEditPageBack]); + + var openRefuelModal = useCallback(function (record) { + setRefuelModal({ + open: true, + station: record, + records: h2GetRefuelRecordsByStation(record.name) + }); + }, []); + + var closeRefuelModal = useCallback(function () { + setRefuelModal({ open: false, station: null, records: [] }); + }, []); + + var handleExportRefuelDrill = useCallback(function () { + var records = refuelModal.records || []; + if (!records.length) { + message.warning('暂无加氢明细可导出'); + return; + } + var stationName = (refuelModal.station && refuelModal.station.name) || ''; + h2ExportRefuelDrillCsv(stationName, records); + message.success('已导出 ' + records.length + ' 条加氢记录'); + }, [refuelModal]); + + var openPrepaidBalanceDrill = useCallback(function (record) { + setPrepaidBalanceDrill({ + open: true, + stationName: record.name || '', + endingBalance: h2NumOrZero(record.prepaidBalance), + rows: h2BuildMockPrepaidBalanceRows(record) + }); + }, []); + + var closePrepaidBalanceDrill = useCallback(function () { + setPrepaidBalanceDrill({ open: false, stationName: '', endingBalance: 0, rows: [] }); + }, []); + + var handleExportBalanceDrill = useCallback(function () { + var rows = prepaidBalanceDrill.rows || []; + if (!rows.length) { + message.warning('暂无流水明细可导出'); + return; + } + h2ExportBalanceDrillCsv(prepaidBalanceDrill.stationName, rows); + message.success('已导出 ' + rows.length + ' 条流水记录'); + }, [prepaidBalanceDrill]); + + var openStatementModal = useCallback(function (record) { + var lastEnd = h2GetLastStatementEndDate(statementRecords, record.name); + var defaultStart = lastEnd ? h2AddDays(lastEnd, 1) : '2026-05-01'; + setStatementModal({ + open: true, + record: record, + startDate: defaultStart, + endDate: '2026-05-31', + phase: 'select', + draftRows: [], + balanceAfterSettlement: h2NumOrZero(record.prepaidBalance), + receiptDate: '', + receiptAmount: '', + invoiceFiles: [] + }); + }, [statementRecords]); + + var closeStatementModal = useCallback(function () { + setStatementModal({ + open: false, + record: null, + startDate: '', + endDate: '', + phase: 'select', + draftRows: [], + balanceAfterSettlement: 0, + receiptDate: '', + receiptAmount: '', + invoiceFiles: [] + }); + }, []); + + var statementDraftSummary = useMemo(function () { + return h2CalcStatementSummary(statementModal.draftRows || []); + }, [statementModal.draftRows]); + + var handleExportStatement = useCallback(function () { + var rows = statementModal.draftRows || []; + if (!statementModal.record || !rows.length) { + message.warning('暂无可导出的对账单明细'); + return; + } + h2ExportStatementCsv(statementModal.record.name, statementModal.startDate, statementModal.endDate, rows); + message.success('已导出 ' + rows.length + ' 条对账明细'); + }, [statementModal]); + + var handleGenerateStatement = useCallback(function () { + if (!statementModal.record) return; + if (!statementModal.startDate || !statementModal.endDate) { + message.warning('请选择账单开始日期和结束日期'); + return; + } + var startMs = h2ParseDateOnlyMs(statementModal.startDate); + var endMs = h2ParseDateOnlyMs(statementModal.endDate); + if (isNaN(startMs) || isNaN(endMs)) { + message.warning('日期格式不正确'); + return; + } + if (startMs > endMs) { + message.warning('开始日期不能晚于结束日期'); + return; + } + var rows = h2GetAvailableStatementLedgerRows(ledgerStore, statementModal.record.name, statementModal.startDate, statementModal.endDate); + if (!rows.length) { + message.warning('所选日期范围内暂无「已对账」且未结算的加氢记录,请调整日期后重试'); + return; + } + var summary = h2CalcStatementSummary(rows); + var balanceAfter = Math.round((h2NumOrZero(statementModal.record.prepaidBalance) - summary.totalCost) * 100) / 100; + setStatementModal(function (m) { + return Object.assign({}, m, { + phase: 'draft', + draftRows: rows, + balanceAfterSettlement: balanceAfter, + receiptAmount: summary.totalCost ? h2FormatYuanNum(summary.totalCost) : '', + receiptDate: '', + invoiceFiles: [] + }); + }); + message.success('已生成对账记录,共 ' + summary.count + ' 笔,请填写结算信息并提交'); + }, [statementModal, ledgerStore]); + + var handleSubmitStatement = useCallback(function () { + if (!statementModal.record || statementModal.phase !== 'draft') return; + var rows = statementModal.draftRows || []; + if (!rows.length) { + message.warning('暂无对账明细可提交'); + return; + } + if (!(statementModal.receiptDate || '').trim()) { + message.warning('请填写收票日期'); + return; + } + var receiptAmountNum = parseFloat(statementModal.receiptAmount); + if (isNaN(receiptAmountNum) || receiptAmountNum <= 0) { + message.warning('请填写有效的收票金额'); + return; + } + if (!statementModal.invoiceFiles || !statementModal.invoiceFiles.length) { + message.warning('请上传发票附件'); + return; + } + var summary = h2CalcStatementSummary(rows); + var operateTime = h2OperateTimestamp(); + var operateDate = h2FormatOperateDateOnly(); + var statementId = 'stmt-' + Date.now(); + var recordIds = rows.map(function (r) { return r.id; }); + var stationRecord = statementModal.record; + var balanceAfter = h2NumOrZero(statementModal.balanceAfterSettlement); + var newStatement = { + id: statementId, + stationId: stationRecord.id, + stationName: stationRecord.name, + reconcileDate: operateTime, + reconciler: H2_CURRENT_OPERATOR, + startDate: statementModal.startDate, + endDate: statementModal.endDate, + refuelCount: summary.count, + totalKg: summary.totalKg, + totalCost: summary.totalCost, + balanceAfterSettlement: balanceAfter, + receiptDate: statementModal.receiptDate, + receiptAmount: receiptAmountNum, + invoiceFiles: (statementModal.invoiceFiles || []).slice(), + recordIds: recordIds + }; + var patches = []; + setLedgerStore(function (prev) { + return prev.map(function (r) { + if (recordIds.indexOf(r.id) < 0) return r; + patches.push({ + ledgerId: r.id, + orderNo: r.orderNo, + stationName: r.stationName, + plateNo: r.plateNo, + hydrogenTime: r.hydrogenTime, + reconcileDate: operateTime, + receiptDate: operateDate, + paymentStatus: H2_PAYMENT_STATUS_PAID + }); + return Object.assign({}, r, { + statementRecordId: statementId, + reconcileDate: operateTime, + receiptDate: operateDate, + paymentStatus: H2_PAYMENT_STATUS_PAID + }); + }); + }); + setStatementRecords(function (prev) { return [newStatement].concat(prev); }); + setListData(function (prev) { + return prev.map(function (item) { + if (item.id !== stationRecord.id) return item; + return Object.assign({}, item, { + prepaidBalance: balanceAfter, + updateTime: operateTime + }); + }); + }); + h2PushStatementLedgerPatches(patches); + if (typeof window !== 'undefined' && window.H2_VEHICLE_LEDGER_API && typeof window.H2_VEHICLE_LEDGER_API.applyPatches === 'function') { + window.H2_VEHICLE_LEDGER_API.applyPatches(patches); + } + message.success('对账单已提交,共结算 ' + summary.count + ' 笔加氢记录'); + closeStatementModal(); + }, [statementModal, closeStatementModal]); + + var openStatementHistoryModal = useCallback(function (record) { + setStatementHistoryModal({ open: true, record: record }); + }, []); + + var closeStatementHistoryModal = useCallback(function () { + setStatementHistoryModal({ open: false, record: null }); + }, []); + + var openStatementDetailModal = useCallback(function (statementRecord) { + var detailRows = h2GetLedgerRowsByIds(ledgerStore, statementRecord.recordIds); + setStatementDetailModal({ open: true, statementRecord: statementRecord, detailRows: detailRows }); + }, [ledgerStore]); + + var closeStatementDetailModal = useCallback(function () { + setStatementDetailModal({ open: false, statementRecord: null, detailRows: [] }); + }, []); + + var openEdit = useCallback(function (record) { + var formSnapshot = h2RecordToEditStationForm(record); + var supplierMode = h2ResolveRecordSupplierMode(record); + var linkedSupplier = record.linkedSupplierId; + var supplierForm = supplierMode === 'none' + ? h2CreateEmptySupplierForm() + : h2SupplierToForm(h2ResolveStationSupplier(record)); + setEditStation(formSnapshot); + setEditOriginForm(JSON.parse(JSON.stringify(formSnapshot))); + setEditSupplierMode(supplierMode); + setEditLinkedSupplierId(linkedSupplier); + setEditSupplier(supplierForm); + setEditRecord(record); + setSubView('edit'); + }, []); + + var openView = useCallback(function (record) { + setViewModal({ open: true, record: record }); + }, []); + + var closeViewModal = useCallback(function () { + setViewModal({ open: false, record: null }); + setFilePreviewModal({ open: false, url: '', name: '', type: 'image' }); + }, []); + + var openFilePreview = useCallback(function (file, variant) { + if (!file) return; + var preview = h2ResolveUploadPreview(file, variant || 'doc'); + if (preview.type === 'image' && Image && typeof Image.preview === 'function') { + Image.preview({ src: preview.url }); + return; + } + setFilePreviewModal({ + open: true, + url: preview.url, + name: preview.name, + type: preview.type + }); + }, [Image]); + + var closeFilePreviewModal = useCallback(function () { + setFilePreviewModal({ open: false, url: '', name: '', type: 'image' }); + }, []); + + var openBusinessSetting = useCallback(function (record) { + setBusinessModal({ + open: true, + record: record, + businessStatus: record.businessStatus || '营业中', + businessHours: record.businessHours === '—' ? '' : (record.businessHours || ''), + originBusinessStatus: record.businessStatus || '营业中', + statusLogs: (record.businessStatusLogs || []).slice() + }); + }, []); + + var closeBusinessModal = useCallback(function () { + setBusinessModal({ open: false, record: null, businessStatus: '营业中', businessHours: '', originBusinessStatus: '营业中', statusLogs: [] }); + }, []); + + var handleSaveBusinessSetting = useCallback(function () { + if (!businessModal.record) return; + var hoursParsed = h2ParseBusinessHours(businessModal.businessHours); + if (hoursParsed.mode === 'custom') { + if (!(hoursParsed.start || '').trim() || !(hoursParsed.end || '').trim()) { + message.warning('非全天营业请选择开始与结束时间'); + return; + } + if (!/^(\d{1,2}):(\d{2})$/.test(h2NormalizeTimeText(hoursParsed.start)) || !/^(\d{1,2}):(\d{2})$/.test(h2NormalizeTimeText(hoursParsed.end))) { + message.warning('营业时间格式须为 HH:mm'); + return; + } + } + var nextStatus = businessModal.businessStatus || '营业中'; + var statusChanged = nextStatus !== businessModal.originBusinessStatus; + var newLog = statusChanged + ? h2CreateBusinessStatusLog(businessModal.originBusinessStatus, nextStatus, H2_CURRENT_OPERATOR) + : null; + setListData(function (prev) { + return prev.map(function (r) { + if (r.id !== businessModal.record.id) return r; + var logs = (r.businessStatusLogs || []).slice(); + if (newLog) logs.unshift(newLog); + return Object.assign({}, r, { + businessStatus: nextStatus, + businessHours: (businessModal.businessHours || '').trim() || '—', + businessStatusLogs: logs, + updateTime: h2OperateTimestamp() + }); + }); + }); + message.success(statusChanged ? '营业状态已更新(原型)' : '营业时间已保存(原型)'); + closeBusinessModal(); + }, [businessModal, closeBusinessModal]); + + var openPriceConfig = useCallback(function (record) { + var configs = h2GetStationPriceConfigs(record); + setPriceModal(Object.assign({ + open: true, + record: record, + priceConfigs: configs, + priceLogs: [], + priceFormVisible: false + }, h2EmptyPriceModalForm())); + }, []); + + var closePriceModal = useCallback(function () { + setPriceModal(Object.assign({ open: false, record: null, priceConfigs: [], priceLogs: [], priceFormVisible: false }, h2EmptyPriceModalForm())); + }, []); + + var loadPriceModalFromConfig = useCallback(function (config) { + var form = h2PriceModalFormFromConfig(config); + setPriceModal(function (m) { + return Object.assign({}, m, form, { + priceLogs: (config && config.logs) ? config.logs.slice() : [], + priceFormVisible: true + }); + }); + }, []); + + var resetPriceModalToNew = useCallback(function () { + setPriceModal(function (m) { + var form = h2EmptyPriceModalForm(); + return Object.assign({}, m, form, { + priceLogs: [], + priceFormVisible: true + }); + }); + }, []); + + var cancelPriceFormEdit = useCallback(function () { + setPriceModal(function (m) { + return Object.assign({}, m, h2EmptyPriceModalForm(), { + priceLogs: [], + priceFormVisible: false + }); + }); + }, []); + + var updateTierRuleField = useCallback(function (ruleId, field, value) { + setPriceModal(function (m) { + return Object.assign({}, m, { + tierRules: (m.tierRules || []).map(function (rule) { + if (rule.id !== ruleId) return rule; + return Object.assign({}, rule, { [field]: value }); + }) + }); + }); + }, []); + + var addTierRuleRow = useCallback(function () { + setPriceModal(function (m) { + return Object.assign({}, m, { tierRules: (m.tierRules || []).concat([h2CreateEmptyTierRule()]) }); + }); + }, []); + + var removeTierRuleRow = useCallback(function (ruleId) { + setPriceModal(function (m) { + var next = (m.tierRules || []).filter(function (rule) { return rule.id !== ruleId; }); + if (!next.length) next = [h2CreateEmptyTierRule()]; + return Object.assign({}, m, { tierRules: next }); + }); + }, []); + + var handleSavePriceConfig = useCallback(function () { + if (!priceModal.record) return; + var priceType = priceModal.priceType || 'fixed'; + var selectedCustomerIds = (priceModal.selectedCustomerIds || []).slice(); + var effectiveText = (priceModal.effectiveTime || '').trim(); + if (!effectiveText) { + message.warning('请选择生效时间'); + return; + } + if (priceType === 'tiered') { + if (!selectedCustomerIds.length) { + message.warning('请至少选择一个适用客户'); + return; + } + } + + var tierRules = []; + var fixedPrice = null; + var afterPrice = null; + var tierRulesSummary = ''; + + if (priceType === 'fixed') { + var costText = (priceModal.costUnitPrice || '').trim(); + if (!costText) { + message.warning('请填写成本价格'); + return; + } + fixedPrice = parseFloat(costText); + if (isNaN(fixedPrice) || fixedPrice < 0) { + message.warning('请输入有效的成本价格(元/kg)'); + return; + } + afterPrice = fixedPrice; + } else { + var i; + for (i = 0; i < (priceModal.tierRules || []).length; i++) { + var row = priceModal.tierRules[i]; + var thresholdText = String(row.thresholdKg || '').trim(); + var priceText = h2FormatReceiptAmountInput(row.price); + if (!thresholdText && !priceText) continue; + if (!thresholdText) { + message.warning('请填写第 ' + (i + 1) + ' 条阶梯条件的加氢总量值'); + return; + } + if (!priceText) { + message.warning('请填写第 ' + (i + 1) + ' 条阶梯条件的价格'); + return; + } + var thresholdKg = parseFloat(thresholdText); + var tierPrice = parseFloat(priceText); + if (isNaN(thresholdKg) || thresholdKg < 0) { + message.warning('第 ' + (i + 1) + ' 条加氢总量值无效'); + return; + } + if (isNaN(tierPrice) || tierPrice < 0) { + message.warning('第 ' + (i + 1) + ' 条价格无效'); + return; + } + tierRules.push({ id: row.id || h2CreateEmptyTierRule().id, thresholdKg: thresholdKg, price: tierPrice }); + } + if (!tierRules.length) { + message.warning('请至少添加一条阶梯价格条件'); + return; + } + tierRules = h2SortTierRules(tierRules); + tierRulesSummary = h2FormatTierRulesSummary(tierRules); + afterPrice = h2ResolveTierUnitPrice(0, tierRules); + } + + var recordId = priceModal.record.id; + var editingId = priceModal.editingConfigId; + var existingConfigs = h2GetStationPriceConfigs(priceModal.record); + var existingCfg = null; + var j; + for (j = 0; j < existingConfigs.length; j++) { + if (existingConfigs[j].id === editingId) { + existingCfg = existingConfigs[j]; + break; + } + } + + var allCustomerOpts = h2CollectCustomerOptions(H2_MOCK_REFUEL_RECORDS); + var customerNameMap = {}; + allCustomerOpts.forEach(function (opt) { customerNameMap[opt.value] = opt.label; }); + + var assignedCustomers = h2GetAssignedCustomerIds(existingConfigs, editingId); + if (selectedCustomerIds.length) { + for (j = 0; j < selectedCustomerIds.length; j++) { + var sid = selectedCustomerIds[j]; + if (assignedCustomers[sid] && !(editingId && existingCfg && existingCfg.customerId === sid)) { + message.warning('客户「' + (customerNameMap[sid] || sid) + '」已有生效规则,请编辑原规则'); + return; + } + } + } + + var configsToSave = []; + if (priceType === 'tiered') { + selectedCustomerIds.forEach(function (cid) { + var isEditTarget = !!(editingId && existingCfg && existingCfg.customerId === cid); + var priorCfg = isEditTarget ? existingCfg : h2FindPriceConfigByCustomerId(existingConfigs, cid, null); + var configId = isEditTarget ? editingId : (priorCfg ? priorCfg.id : h2CreatePriceConfigId()); + var beforePrice = priorCfg ? h2ResolveConfigCurrentPrice(priorCfg) : null; + var cname = customerNameMap[cid] || (priorCfg && priorCfg.customerName) || cid; + var newLog = h2CreateCostPriceLog(beforePrice, afterPrice, effectiveText, H2_CURRENT_OPERATOR, { + priceType: 'tiered', + customerScope: 'customer', + customerName: cname, + tierRules: tierRules.map(function (rule) { return Object.assign({}, rule); }), + tierRulesSummary: tierRulesSummary + }); + configsToSave.push({ + id: configId, + priceType: 'tiered', + customerScope: 'customer', + customerId: cid, + customerName: cname, + fixedPrice: null, + tierRules: tierRules.map(function (rule) { return Object.assign({}, rule); }), + tierResetDay: parseInt(priceModal.tierResetDay, 10) || 1, + tierPeriodVolumeKg: priorCfg && priorCfg.tierPeriodVolumeKg != null ? priorCfg.tierPeriodVolumeKg : 0, + tierPeriodKey: priorCfg && priorCfg.tierPeriodKey ? priorCfg.tierPeriodKey : h2GetTierPeriodKey(priceModal.tierResetDay), + logs: [newLog].concat(priorCfg ? (priorCfg.logs || []) : []) + }); + }); + } else if (selectedCustomerIds.length) { + selectedCustomerIds.forEach(function (cid) { + var isEditTarget = !!(editingId && existingCfg && existingCfg.customerScope === 'customer' && existingCfg.customerId === cid); + var priorCfg = isEditTarget ? existingCfg : h2FindPriceConfigByCustomerId(existingConfigs, cid, null); + var configId = isEditTarget ? editingId : (priorCfg ? priorCfg.id : h2CreatePriceConfigId()); + var beforePriceFixed = priorCfg + ? h2ResolveConfigCurrentPrice(priorCfg) + : h2ResolveCurrentCostPrice(priceModal.record); + var cname = customerNameMap[cid] || (priorCfg && priorCfg.customerName) || cid; + var newLogFixed = h2CreateCostPriceLog(beforePriceFixed, afterPrice, effectiveText, H2_CURRENT_OPERATOR, { + priceType: 'fixed', + customerScope: 'customer', + customerName: cname + }); + configsToSave.push({ + id: configId, + priceType: 'fixed', + customerScope: 'customer', + customerId: cid, + customerName: cname, + fixedPrice: fixedPrice, + tierRules: tierRules, + tierResetDay: parseInt(priceModal.tierResetDay, 10) || 1, + tierPeriodVolumeKg: priorCfg && priorCfg.tierPeriodVolumeKg != null ? priorCfg.tierPeriodVolumeKg : 0, + tierPeriodKey: priorCfg && priorCfg.tierPeriodKey ? priorCfg.tierPeriodKey : h2GetTierPeriodKey(priceModal.tierResetDay), + logs: [newLogFixed].concat(priorCfg ? (priorCfg.logs || []) : []) + }); + }); + } else { + var isAllEdit = !!(editingId && existingCfg && existingCfg.customerScope === 'all'); + var beforePriceAll = existingCfg + ? h2ResolveConfigCurrentPrice(existingCfg) + : h2ResolveCurrentCostPrice(priceModal.record); + var newLogAll = h2CreateCostPriceLog(beforePriceAll, afterPrice, effectiveText, H2_CURRENT_OPERATOR, { + priceType: 'fixed', + customerScope: 'all', + customerName: null + }); + configsToSave.push({ + id: isAllEdit ? editingId : h2CreatePriceConfigId(), + priceType: 'fixed', + customerScope: 'all', + customerId: null, + customerName: null, + fixedPrice: fixedPrice, + tierRules: tierRules, + tierResetDay: parseInt(priceModal.tierResetDay, 10) || 1, + tierPeriodVolumeKg: existingCfg && existingCfg.tierPeriodVolumeKg != null ? existingCfg.tierPeriodVolumeKg : 0, + tierPeriodKey: existingCfg && existingCfg.tierPeriodKey ? existingCfg.tierPeriodKey : h2GetTierPeriodKey(priceModal.tierResetDay), + logs: [newLogAll].concat(existingCfg && existingCfg.customerScope === 'all' ? (existingCfg.logs || []) : []) + }); + } + + var dropEditingConfig = editingId && !configsToSave.some(function (c) { return c.id === editingId; }); + + setListData(function (prev) { + return prev.map(function (r) { + if (r.id !== recordId) return r; + var configs = h2GetStationPriceConfigs(r); + if (dropEditingConfig) { + configs = configs.filter(function (cfg) { return cfg.id !== editingId; }); + } + configsToSave.forEach(function (savedConfig) { + configs = h2ApplyCustomerExclusion(configs, savedConfig); + var replaced = false; + configs = configs.map(function (cfg) { + if (cfg.id === savedConfig.id) { + replaced = true; + return savedConfig; + } + return cfg; + }); + if (!replaced) configs = configs.concat([savedConfig]); + }); + var updated = Object.assign({}, r, { + priceConfigs: configs, + updateTime: h2OperateTimestamp() + }); + return h2SyncRecordPriceFields(h2ApplyDueCostPriceToRecord(updated)); + }); + }); + + var nextConfigs = h2GetStationPriceConfigs(priceModal.record); + if (dropEditingConfig) { + nextConfigs = nextConfigs.filter(function (cfg) { return cfg.id !== editingId; }); + } + configsToSave.forEach(function (savedConfig) { + nextConfigs = h2ApplyCustomerExclusion(nextConfigs, savedConfig); + var replaced = false; + nextConfigs = nextConfigs.map(function (cfg) { + if (cfg.id === savedConfig.id) { + replaced = true; + return savedConfig; + } + return cfg; + }); + if (!replaced) nextConfigs = nextConfigs.concat([savedConfig]); + }); + var updatedRecord = h2SyncRecordPriceFields(h2ApplyDueCostPriceToRecord(Object.assign({}, priceModal.record, { + priceConfigs: nextConfigs, + updateTime: h2OperateTimestamp() + }))); + setPriceModal(function (m) { + return Object.assign({}, m, h2EmptyPriceModalForm(), { + record: updatedRecord, + priceConfigs: nextConfigs, + priceLogs: [], + priceFormVisible: false + }); + }); + + var appliedNow = h2ParseDateTimeMs(effectiveText) <= Date.now(); + var scopeTip = selectedCustomerIds.length + ? '(' + selectedCustomerIds.length + ' 个客户)' + : '(全部客户)'; + message.success(appliedNow + ? '价格配置已保存' + scopeTip + ',当前规则已更新(原型)' + : '价格配置已保存' + scopeTip + ',将于生效时间自动更新(原型)'); + }, [priceModal]); + + var openBalanceAlertSetting = useCallback(function (record) { + var threshold = h2ResolveBalanceAlertThreshold(record); + setBalanceAlertModal({ + open: true, + record: record, + threshold: threshold != null ? h2FormatYuanNum(threshold) : '' + }); + }, []); + + var closeBalanceAlertModal = useCallback(function () { + setBalanceAlertModal({ open: false, record: null, threshold: '' }); + }, []); + + var handleSaveBalanceAlert = useCallback(function () { + if (!balanceAlertModal.record) return; + var thresholdText = h2FormatReceiptAmountInput(balanceAlertModal.threshold); + if (!thresholdText) { + message.warning('请填写有效的提醒金额'); + return; + } + var thresholdNum = parseFloat(thresholdText); + if (isNaN(thresholdNum) || thresholdNum <= 0) { + message.warning('提醒金额须大于 0'); + return; + } + var recordId = balanceAlertModal.record.id; + setListData(function (prev) { + return prev.map(function (r) { + if (r.id !== recordId) return r; + return Object.assign({}, r, { + balanceAlertThreshold: thresholdNum, + updateTime: h2OperateTimestamp() + }); + }); + }); + message.success('余额提醒设置已保存(原型)'); + closeBalanceAlertModal(); + }, [balanceAlertModal, closeBalanceAlertModal]); + + var openGlobalBalanceAlertSetting = useCallback(function () { + setGlobalBalanceAlertModal({ open: true, threshold: '' }); + }, []); + + var closeGlobalBalanceAlertModal = useCallback(function () { + setGlobalBalanceAlertModal({ open: false, threshold: '' }); + }, []); + + var handleSaveGlobalBalanceAlert = useCallback(function () { + var thresholdText = h2FormatReceiptAmountInput(globalBalanceAlertModal.threshold); + if (!thresholdText) { + message.warning('请填写有效的提醒金额'); + return; + } + var thresholdNum = parseFloat(thresholdText); + if (isNaN(thresholdNum) || thresholdNum <= 0) { + message.warning('提醒金额须大于 0'); + return; + } + setListData(function (prev) { + return prev.map(function (r) { + return Object.assign({}, r, { + balanceAlertThreshold: thresholdNum, + updateTime: h2OperateTimestamp() + }); + }); + }); + message.success('已为全部加氢站保存余额提醒设置(原型)'); + closeGlobalBalanceAlertModal(); + }, [globalBalanceAlertModal, closeGlobalBalanceAlertModal]); + + var openRechargeModal = useCallback(function (stations) { + var lines = []; + if (stations && stations.length) { + lines = stations.map(function (r) { return h2BuildRechargeLineFromStation(r); }); + } else { + lines = [h2CreateEmptyRechargeLine()]; + } + setRechargeModal({ open: true, lines: lines }); + }, []); + + var closeRechargeModal = useCallback(function () { + setRechargeModal({ open: false, lines: [] }); + }, []); + + var updateRechargeLineField = useCallback(function (lineId, field, value) { + setRechargeModal(function (m) { + return Object.assign({}, m, { + lines: m.lines.map(function (line) { + if (line.id !== lineId) return line; + return Object.assign({}, line, { [field]: value }); + }) + }); + }); + }, []); + + var handleRechargeStationSelect = useCallback(function (lineId, stationId) { + if (!stationId) { + setRechargeModal(function (m) { + return Object.assign({}, m, { + lines: m.lines.map(function (line) { + if (line.id !== lineId) return line; + var empty = h2CreateEmptyRechargeLine(); + empty.id = lineId; + empty.payAmount = line.payAmount; + return empty; + }) + }); + }); + return; + } + var record = listData.filter(function (r) { return r.id === stationId; })[0]; + if (!record) return; + setRechargeModal(function (m) { + return Object.assign({}, m, { + lines: m.lines.map(function (line) { + if (line.id !== lineId) return line; + var built = h2BuildRechargeLineFromStation(record, line.payAmount); + built.id = lineId; + return built; + }) + }); + }); + }, [listData]); + + var addRechargeLine = useCallback(function () { + setRechargeModal(function (m) { + return Object.assign({}, m, { lines: m.lines.concat([h2CreateEmptyRechargeLine()]) }); + }); + }, []); + + var removeRechargeLine = useCallback(function (lineId) { + setRechargeModal(function (m) { + if (m.lines.length <= 1) return m; + return Object.assign({}, m, { lines: m.lines.filter(function (l) { return l.id !== lineId; }) }); + }); + }, []); + + var handleSubmitRecharge = useCallback(function () { + var lines = rechargeModal.lines || []; + var valid = []; + var i; + for (i = 0; i < lines.length; i++) { + var line = lines[i]; + if (!line.stationId) continue; + var payText = h2FormatReceiptAmountInput(line.payAmount); + var payNum = parseFloat(payText); + if (!payText || isNaN(payNum) || payNum <= 0) continue; + valid.push(line); + } + if (!valid.length) { + message.warning('请至少添加一条有效的付款信息(已选站点且付款金额大于 0)'); + return; + } + var missingSupplier = valid.filter(function (l) { + return !(l.companyName || '').trim() || !(l.bankAccount || '').trim() || !(l.bankName || '').trim(); + }); + if (missingSupplier.length) { + message.warning('部分站点未关联供应商或缺少收款信息,请补全后再发起'); + return; + } + message.success('已发起审批流程'); + closeRechargeModal(); + }, [rechargeModal, closeRechargeModal]); + + var openAlertStationModal = useCallback(function (type) { + var title = type === 'balanceAlert' ? '预付余额预警站点' : '已欠费站点'; + var stations = listData.filter(function (r) { + if (type === 'balanceAlert') return h2IsBalanceAlertStation(r); + if (type === 'arrears') return h2IsArrearsStation(r); + return false; + }); + setAlertStationModal({ + open: true, + type: type, + title: title, + stations: h2EnrichStationListRows(stations) + }); + }, [listData]); + + var closeAlertStationModal = useCallback(function () { + setAlertStationModal({ open: false, type: '', title: '', stations: [] }); + }, []); + + var handleBatchRechargeFromAlert = useCallback(function () { + var stations = alertStationModal.stations || []; + if (!stations.length) { + message.warning('暂无站点可发起充值'); + return; + } + var type = alertStationModal.type; + closeAlertStationModal(); + openRechargeModal(stations); + if (type === 'balanceAlert') message.info('已载入 ' + stations.length + ' 个预警站点,请填写付款金额后发起'); + else if (type === 'arrears') message.info('已载入 ' + stations.length + ' 个欠费站点,请填写付款金额后发起'); + }, [alertStationModal, closeAlertStationModal, openRechargeModal]); + + var getRowMoreMenuItems = useCallback(function (record) { + return [ + { key: 'edit', label: '编辑', onClick: function () { openEdit(record); } }, + { key: 'business', label: '营业状态', onClick: function () { openBusinessSetting(record); } }, + { key: 'price', label: '价格配置', onClick: function () { openPriceConfig(record); } }, + { key: 'balanceAlert', label: '余额提醒设置', onClick: function () { openBalanceAlertSetting(record); } }, + { key: 'statement', label: '生成对账单', onClick: function () { openStatementModal(record); } }, + { key: 'statementHistory', label: '查看对账记录', onClick: function () { openStatementHistoryModal(record); } }, + { type: 'divider' }, + { key: 'delete', label: '删除', danger: true, onClick: function () { setDeleteModal({ open: true, record: record }); } } + ]; + }, [openEdit, openBusinessSetting, openPriceConfig, openBalanceAlertSetting, openStatementModal, openStatementHistoryModal]); + + var closeImportModal = useCallback(function () { + setImportModalOpen(false); + setImportFileList([]); + setImportPreview(null); + }, []); + + var downloadImportTemplate = useCallback(function () { + h2DownloadCsv('加氢站点批量导入模板.csv', H2_IMPORT_TEMPLATE_HEADERS, H2_IMPORT_TEMPLATE_SAMPLE); + message.success('已下载导入模板'); + }, []); + + var parseImportFile = useCallback(function (file) { + if (!file) return; + var name = String(file.name || '').toLowerCase(); + if (name.endsWith('.xls') || name.endsWith('.xlsx')) { + message.warning('原型环境请使用 CSV 模板导入;联调后将支持 Excel 直接上传'); + setImportFileList([file]); + setImportPreview(null); + return; + } + if (!name.endsWith('.csv')) { + message.error('仅支持 .csv、.xls、.xlsx 格式'); + return; + } + setImportFileList([file]); + var reader = new FileReader(); + reader.onload = function (ev) { + var text = (ev && ev.target && ev.target.result) || ''; + var parsed = h2ParseImportCsv(text); + if (parsed.headerError) { + message.error(parsed.headerError); + setImportPreview(null); + return; + } + if (!parsed.rows.length) { + message.warning('未解析到有效数据,请检查文件内容'); + setImportPreview(null); + return; + } + var nameMap = Object.assign({}, existingNameMap); + var valid = []; + var errors = []; + var ri; + for (ri = 0; ri < parsed.rows.length; ri++) { + var row = parsed.rows[ri]; + var rowErrors = h2ValidateImportRow(row, nameMap); + if (rowErrors.length) { + errors.push({ lineNo: row.lineNo, name: row.name || '—', reasons: rowErrors }); + } else { + valid.push(row); + if (row.name) nameMap[String(row.name).trim()] = true; + } + } + setImportPreview({ valid: valid, errors: errors, fileName: file.name }); + }; + reader.onerror = function () { + message.error('文件读取失败'); + }; + reader.readAsText(file, 'UTF-8'); + }, [existingNameMap]); + + var handleConfirmImport = useCallback(function () { + if (!importPreview || !importPreview.valid.length) { + message.warning('没有可导入的有效数据'); + return; + } + setListData(function (prev) { + var maxId = prev.reduce(function (m, r) { return Math.max(m, r.id || 0); }, 0); + var imported = importPreview.valid.map(function (row, idx) { + return h2ImportRowToRecord(row, maxId + idx + 1); + }); + return imported.concat(prev); + }); + var errN = (importPreview.errors || []).length; + if (errN) { + message.success('成功导入 ' + importPreview.valid.length + ' 条,' + errN + ' 条校验失败已跳过'); + } else { + message.success('成功导入 ' + importPreview.valid.length + ' 条站点'); + } + setPage(1); + closeImportModal(); + }, [importPreview, closeImportModal]); + + React.useEffect(function () { + var maxPage = Math.max(1, Math.ceil(totalCount / pageSize) || 1); + if (page > maxPage) setPage(maxPage); + }, [totalCount, pageSize, page]); + + React.useEffect(function () { + var applyDuePrices = function () { + setListData(function (prev) { + var result = h2ApplyDueCostPricesToList(prev); + return result.changed ? result.records : prev; + }); + }; + applyDuePrices(); + var timer = window.setInterval(applyDuePrices, 60000); + return function () { window.clearInterval(timer); }; + }, []); + + var renderRefuelFreqShortTag = function (freqKey) { + if (freqKey === 'high') { + return React.createElement(Tag, { + color: 'success', + className: 'lc-refuel-freq-tag' + }, '高频'); + } + if (freqKey === 'low') { + return React.createElement(Tag, { + color: 'warning', + className: 'lc-refuel-freq-tag' + }, '低频'); + } + return null; + }; + + var renderBusinessStatusTag = function (v) { + if (v === '营业中') return React.createElement(Tag, { color: 'success', style: { borderRadius: 6, fontWeight: 600 } }, v); + if (v === '暂停营业') return React.createElement(Tag, { color: 'warning', style: { borderRadius: 6, fontWeight: 600 } }, v); + if (v === '停止营业') return React.createElement(Tag, { color: 'error', style: { borderRadius: 6, fontWeight: 600 } }, v); + return React.createElement(Tag, { style: { borderRadius: 6, fontWeight: 600, color: '#64748b', background: '#f1f5f9', border: '1px solid #e2e8f0' } }, v || '—'); + }; + + var renderBusinessStatusText = function (v, tone) { + var color = v === '营业中' ? '#059669' : v === '暂停营业' ? '#ea580c' : v === '停止营业' ? '#dc2626' : '#64748b'; + if (tone === 'muted') color = '#94a3b8'; + return React.createElement('span', { style: { fontWeight: 600, color: color } }, v || '—'); + }; + + var renderRefuelDrillLink = function (text, record, ariaLabel) { + return React.createElement('button', { + type: 'button', + className: 'h2-refuel-drill-link', + onClick: function (e) { + e.stopPropagation(); + openRefuelModal(record); + }, + 'aria-label': ariaLabel + }, text); + }; + + var renderPrepaidBalance = useCallback(function (v, record) { + if (v == null || v === '') return React.createElement('span', { style: { color: '#94a3b8' } }, '—'); + var n = typeof v === 'number' ? v : parseFloat(v); + if (isNaN(n)) return '—'; + if (!record) { + var plainColor = n < 0 ? '#dc2626' : '#0f172a'; + return React.createElement('span', { + style: { fontWeight: 700, color: plainColor, fontVariantNumeric: 'tabular-nums' }, + title: n < 0 ? '预付余额不足,已欠费' : undefined + }, h2FormatYuanNum(n)); + } + var isArrears = n < 0; + var amountStyle = isArrears + ? { color: '#dc2626', fontVariantNumeric: 'tabular-nums' } + : { color: '#059669', fontVariantNumeric: 'tabular-nums' }; + return React.createElement('div', { className: 'h2-prepaid-balance-cell' }, + isArrears ? React.createElement(Tag, { color: 'error', className: 'lc-station-signed-tag' }, '已欠费') : null, + React.createElement('button', { + type: 'button', + className: 'h2-prepaid-balance-amount', + style: amountStyle, + title: '点击查看余额变更明细', + 'aria-label': '查看「' + (record.name || '') + '」余额变更明细', + onClick: function (e) { + e.stopPropagation(); + openPrepaidBalanceDrill(record); + } + }, h2FormatYuanNum(n)) + ); + }, [openPrepaidBalanceDrill]); + + var renderBalanceDrillMoney = function (text, tone, rawValue) { + if (text === '—') { + return React.createElement('span', { className: 'h2-balance-money h2-balance-money--muted' }, '—'); + } + var className = 'h2-balance-money h2-balance-money--' + tone; + if (tone === 'balance' && rawValue != null && h2NumOrZero(rawValue) < 0) className += ' h2-balance-money--negative'; + return React.createElement('span', { className: className }, text); + }; + + var prepaidBalanceDrillColumns = [ + { + title: '序号', + key: 'seq', + width: 52, + align: 'center', + render: function (_, __, index) { + return React.createElement('span', { className: 'h2-balance-seq' }, index + 1); + } + }, + { + title: '时间', + dataIndex: 'changeTime', + key: 'changeTime', + width: 158, + render: function (v) { + return React.createElement('span', { + style: { color: '#334155', fontVariantNumeric: 'tabular-nums', fontSize: 12, whiteSpace: 'nowrap' } + }, v || '—'); + } + }, + { + title: '类型', + key: 'bizType', + width: 92, + align: 'center', + render: function (_, row) { + var typeInfo = h2ResolveBalanceBizType(row); + return React.createElement(Tag, { + color: typeInfo.color, + style: { margin: 0, borderRadius: 6, fontWeight: 600, fontSize: 12 } + }, typeInfo.label); + } + }, + { + title: '收入(元)', + dataIndex: 'incomeAmount', + key: 'incomeAmount', + width: 120, + align: 'right', + render: function (v) { return renderBalanceDrillMoney(h2FormatYuanSymbol(v), 'income'); } + }, + { + title: '支出(元)', + dataIndex: 'expenseAmount', + key: 'expenseAmount', + width: 120, + align: 'right', + render: function (v) { return renderBalanceDrillMoney(h2FormatYuanSymbol(v), 'expense'); } + }, + { + title: '余额(元)', + dataIndex: 'balance', + key: 'balance', + width: 128, + align: 'right', + render: function (v) { + return renderBalanceDrillMoney(h2FormatYuanSymbol(v, { keepZero: true }), 'balance', v); + } + }, + { + title: '订单编号', + dataIndex: 'orderNo', + key: 'orderNo', + width: 148, + ellipsis: true, + render: function (v) { + if (!v) return React.createElement('span', { className: 'h2-balance-money--muted' }, '—'); + return React.createElement('span', { className: 'h2-balance-order-no', title: v }, v); + } + } + ]; + + var prepaidBalanceDrillSummary = useMemo(function () { + if (!prepaidBalanceDrill.rows || !prepaidBalanceDrill.rows.length) { + return { incomeTotal: 0, expenseTotal: 0, currentBalance: 0 }; + } + var last = prepaidBalanceDrill.rows[prepaidBalanceDrill.rows.length - 1]; + return prepaidBalanceDrill.rows.reduce(function (acc, r) { + acc.incomeTotal += h2NumOrZero(r.incomeAmount); + acc.expenseTotal += h2NumOrZero(r.expenseAmount); + return acc; + }, { incomeTotal: 0, expenseTotal: 0, currentBalance: h2NumOrZero(last.balance) }); + }, [prepaidBalanceDrill.rows]); + + var prepaidBalanceTrendSeries = useMemo(function () { + return h2BuildBalanceTrendSeries(prepaidBalanceDrill.rows || []); + }, [prepaidBalanceDrill.rows]); + + var renderPrepaidBalanceDrillPanel = useCallback(function () { + var rows = prepaidBalanceDrill.rows || []; + var summary = prepaidBalanceDrillSummary; + var trendSeries = prepaidBalanceTrendSeries; + var balanceNegative = h2NumOrZero(summary.currentBalance) < 0; + var balanceText = h2FormatYuanSymbol(summary.currentBalance, { keepZero: true }); + var stats = [ + { key: 'balance', label: '当前预付余额', value: balanceText, mod: 'balance', valueMod: balanceNegative ? 'negative' : 'income' }, + { key: 'income', label: '收入合计', value: h2FormatYuanSymbol(summary.incomeTotal, { keepZero: true }), mod: 'income', valueMod: 'income' }, + { key: 'expense', label: '支出合计', value: h2FormatYuanSymbol(summary.expenseTotal, { keepZero: true }), mod: 'expense', valueMod: 'expense' } + ]; + return React.createElement('div', { className: 'h2-balance-drill-panel' }, + React.createElement('div', { className: 'h2-balance-drill-stats', role: 'group', 'aria-label': '余额统计' }, + stats.map(function (item) { + return React.createElement('div', { + key: item.key, + className: 'h2-balance-drill-stat h2-balance-drill-stat--' + item.mod, + role: 'article', + 'aria-label': item.label + ' ' + item.value + }, + React.createElement('div', { className: 'h2-balance-drill-stat__label' }, item.label), + React.createElement('div', { + className: 'h2-balance-drill-stat__value' + (item.valueMod ? ' h2-balance-drill-stat__value--' + item.valueMod : '') + }, item.value) + ); + }) + ), + React.createElement('div', { className: 'h2-balance-trend-wrap' }, + React.createElement('div', { className: 'h2-balance-trend-head' }, + React.createElement('span', { className: 'h2-balance-trend-head__title' }, '余额趋势'), + React.createElement('span', { className: 'h2-balance-trend-head__meta' }, + (trendSeries.startDate || '—') + ' 至 ' + (trendSeries.endDate || '—') + ' · 按日 · 悬浮查看明细' + ) + ), + React.createElement('div', { className: 'h2-balance-trend-body' }, + React.createElement(H2BalanceTrendChart, { series: trendSeries }) + ), + rows.length + ? React.createElement('div', { className: 'h2-balance-trend-legend' }, + React.createElement('span', { className: 'h2-balance-trend-legend__item' }, + React.createElement('span', { className: 'h2-balance-trend-legend__dot h2-balance-trend-legend__dot--line' }), + '余额走势' + ), + React.createElement('span', { className: 'h2-balance-trend-legend__item' }, + React.createElement('span', { className: 'h2-balance-trend-legend__dot h2-balance-trend-legend__dot--area' }), + '区间填充' + ), + trendSeries.hasNegative + ? React.createElement('span', { className: 'h2-balance-trend-legend__item', style: { color: '#dc2626' } }, '红点表示欠费时点') + : null + ) + : null + ), + React.createElement('div', { className: 'h2-balance-drill-table-wrap' }, + React.createElement('div', { className: 'h2-balance-drill-table-head' }, + React.createElement('div', { className: 'h2-balance-drill-table-head__left' }, + React.createElement('span', { className: 'h2-balance-drill-table-head__title' }, '流水明细'), + React.createElement('span', { className: 'h2-balance-drill-table-head__count' }, '共 ' + rows.length + ' 条') + ), + React.createElement(Button, { + size: 'small', + icon: H2_ICONS.download, + onClick: handleExportBalanceDrill, + style: { borderRadius: 8, fontWeight: 600, borderColor: '#10b981', color: '#059669' }, + 'aria-label': '导出流水明细' + }, '导出') + ), + React.createElement(Table, { + className: 'h2-balance-record-table', + size: 'small', + bordered: false, + rowKey: 'key', + columns: prepaidBalanceDrillColumns, + dataSource: rows, + pagination: rows.length > 8 ? { pageSize: 8, showSizeChanger: false, size: 'small', showTotal: function (t) { return '共 ' + t + ' 条'; } } : false, + locale: { emptyText: '暂无余额变更记录' }, + scroll: rows.length > 8 ? { x: 'max-content', y: 320 } : { x: 'max-content' } + }) + ) + ); + }, [prepaidBalanceDrill, prepaidBalanceDrillSummary, prepaidBalanceTrendSeries, prepaidBalanceDrillColumns, handleExportBalanceDrill]); + + var renderStationViewPanel = useCallback(function () { + var record = viewModal.record; + if (!record) return null; + var supplier = h2ResolveStationSupplier(record); + var descColumn = { xs: 1, sm: 1, md: 2, lg: 2, xl: 2, xxl: 2 }; + var price = h2ResolveCurrentCostPrice(record); + var balanceNegative = h2NumOrZero(record.prepaidBalance) < 0; + var balanceText = h2FormatYuanSymbol(record.prepaidBalance, { keepZero: true }); + + var renderText = function (v) { + return v != null && v !== '' ? v : '—'; + }; + + var renderViewContractFiles = function (files) { + if (!files || !files.length) return '—'; + return React.createElement('div', { className: 'h2-station-view-files' }, + files.map(function (f, idx) { + return React.createElement('button', { + key: f.uid || f.name || idx, + type: 'button', + className: 'h2-station-view-file-link', + title: '点击预览 ' + (f.name || '附件'), + onClick: function () { openFilePreview(f, 'doc'); } + }, f.name || '附件'); + }) + ); + }; + + var renderViewLicenseImages = function (files, opts) { + opts = opts || {}; + if (!files || !files.length) { + return React.createElement('span', { className: 'h2-station-view-license-empty' }, '未上传'); + } + return React.createElement('div', { className: 'h2-station-view-license-block' }, + opts.showCount + ? React.createElement('div', { className: 'h2-station-view-license-count' }, '共 ' + files.length + ' 张') + : null, + React.createElement('div', { className: 'h2-station-view-license-images' }, + files.map(function (f, idx) { + var imgUrl = h2ResolveLicenseDisplayUrl(f); + return React.createElement('button', { + key: f.uid || f.name || idx, + type: 'button', + className: 'h2-station-view-license-thumb', + title: '点击预览 ' + (f.name || '证照'), + onClick: function () { openFilePreview(f, 'license'); } + }, + React.createElement('img', { + src: imgUrl, + alt: f.name || '证照图片' + }) + ); + }) + ) + ); + }; + + var renderViewSection = function (title, items) { + return React.createElement('div', { className: 'h2-station-view-section' }, + React.createElement('div', { className: 'h2-station-view-section__title' }, title), + React.createElement(Descriptions, { + bordered: true, + size: 'small', + column: descColumn + }, + items.map(function (item) { + return React.createElement(Descriptions.Item, { + key: item.key, + label: item.label, + span: item.span || 1 + }, item.value); + }) + ) + ); + }; + + var renderEmptySection = function (title, hint) { + return React.createElement('div', { className: 'h2-station-view-section' }, + React.createElement('div', { className: 'h2-station-view-section__title' }, title), + React.createElement('div', { className: 'h2-station-view-empty' }, hint) + ); + }; + + var contractValue = '—'; + if (record.isSigned) { + contractValue = React.createElement('div', null, + React.createElement('span', null, (record.contractStart || '—') + ' 至 ' + (record.contractEnd || '—')), + record.contractEnd ? React.createElement('div', { style: { marginTop: 4 } }, h2RenderContractRemainTag(record.contractEnd)) : null + ); + } + + var stationItems = [ + { key: 'name', label: '加氢站名称', value: renderText(record.name) }, + { key: 'region', label: '省 / 市', value: renderText(h2FormatRegion(record.region)) }, + { key: 'detail', label: '详细地址', value: renderText(record.addressDetail), span: 2 }, + { key: 'status', label: '营业状态', value: renderBusinessStatusTag(record.businessStatus) }, + { key: 'hours', label: '营业时间', value: renderText(h2DisplayBusinessHours(record.businessHours)) }, + { key: 'signed', label: '是否签约', value: record.isSigned ? '已签约' : '未签约' } + ]; + if (record.isSigned) { + stationItems.push( + { key: 'contract', label: '合作时间', value: contractValue, span: 2 }, + { key: 'files', label: '签约附件', value: renderViewContractFiles(record.contractFiles), span: 2 } + ); + } + stationItems.push( + { key: 'contact', label: '联系人', value: renderText(record.contact) }, + { key: 'phone', label: '联系电话', value: renderText(record.phone) }, + { + key: 'balance', + label: '预付余额', + value: React.createElement('span', { + style: { fontWeight: 700, color: balanceNegative ? '#dc2626' : '#059669', fontVariantNumeric: 'tabular-nums' } + }, balanceText) + }, + { + key: 'price', + label: '当前成本价格', + value: price != null ? h2FormatYuanSymbol(price, { keepZero: true }) + ' / kg' : '—' + } + ); + + var supplierItems = supplier ? [ + { key: 'name', label: '供应商名称', value: renderText(supplier.name), span: 2 }, + { key: 'signingCompany', label: '签约公司', value: renderText(supplier.signingCompany), span: 2 }, + { key: 'type', label: '供应商类型', value: renderText(supplier.type || '加氢站') }, + { key: 'city', label: '省 / 市', value: renderText(h2FormatRegion(supplier.city)) }, + { key: 'address', label: '详细地址', value: renderText(supplier.address), span: 2 }, + { key: 'region', label: '所属区域', value: renderText(supplier.region) }, + { key: 'dept', label: '归属部门', value: renderText(supplier.dept) }, + { key: 'manager', label: '负责人', value: renderText(supplier.manager) }, + { key: 'contactName', label: '联系人', value: renderText(supplier.contactName) }, + { key: 'contactMobile', label: '联系电话', value: renderText(supplier.contactMobile) }, + { key: 'bl', label: '营业执照', value: renderViewLicenseImages(supplier.businessLicenseFiles), span: 2 }, + { key: 'fl', label: '其他证照', value: renderViewLicenseImages(supplier.fillingLicenseFiles, { showCount: true }), span: 2 } + ] : []; + + var paymentItems = supplier ? [ + { key: 'taxId', label: '纳税人识别号', value: renderText(supplier.taxId), span: 2 }, + { key: 'invoicePhone', label: '注册电话', value: renderText(supplier.invoicePhone) }, + { key: 'invoiceAddress', label: '注册地址', value: renderText(supplier.invoiceAddress), span: 2 }, + { key: 'bankName', label: '开户行', value: renderText(supplier.bankName), span: 2 }, + { key: 'bankAccount', label: '银行账号', value: renderText(supplier.bankAccount), span: 2 }, + { key: 'businessAddress', label: '营业地址', value: renderText(supplier.businessAddress), span: 2 } + ] : []; + + return React.createElement('div', { className: 'h2-station-view-panel' }, + renderViewSection('加氢站信息', stationItems), + supplier ? renderViewSection('供应商信息', supplierItems) : renderEmptySection('供应商信息', '暂未关联供应商'), + supplier ? renderViewSection('付款信息', paymentItems) : renderEmptySection('付款信息', '暂无付款信息') + ); + }, [viewModal.record, openFilePreview]); + + var renderRefuelDrillMoney = function (text, tone) { + return React.createElement('span', { className: 'h2-refuel-drill-money h2-refuel-drill-money--' + tone }, text); + }; + + var refuelRecordColumns = [ + { + title: '序号', + key: 'seq', + width: 52, + align: 'center', + render: function (_, __, index) { + return React.createElement('span', { className: 'h2-refuel-drill-seq' }, index + 1); + } + }, + { + title: '加氢时间', + dataIndex: 'hydrogenTime', + key: 'hydrogenTime', + width: 158, + render: function (v) { + return React.createElement('span', { className: 'h2-refuel-drill-time' }, v || '—'); + } + }, + { + title: '车牌号', + dataIndex: 'plateNo', + key: 'plateNo', + width: 108, + render: function (v) { + return React.createElement('span', { className: 'h2-refuel-drill-plate' }, v || '—'); + } + }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 168, ellipsis: true }, + { + title: '加氢量(kg)', + dataIndex: 'hydrogenKg', + key: 'hydrogenKg', + width: 96, + align: 'right', + render: function (v) { + return React.createElement('span', { className: 'h2-refuel-drill-kg' }, h2FormatKgNum(v)); + } + }, + { + title: '成本单价(元/kg)', + dataIndex: 'costUnitPrice', + key: 'costUnitPrice', + width: 118, + align: 'right', + render: function (v) { + return renderRefuelDrillMoney(h2FormatYuanSymbol(v, { keepZero: true }), 'cost'); + } + }, + { + title: '成本总价(元)', + dataIndex: 'costAmount', + key: 'costAmount', + width: 118, + align: 'right', + render: function (v) { + return renderRefuelDrillMoney(h2FormatYuanSymbol(v, { keepZero: true }), 'cost'); + } + }, + { + title: '加氢单价(元/kg)', + dataIndex: 'customerUnitPrice', + key: 'customerUnitPrice', + width: 118, + align: 'right', + render: function (v) { + return renderRefuelDrillMoney(h2FormatYuanSymbol(v, { keepZero: true }), 'customer'); + } + }, + { + title: '加氢总价(元)', + dataIndex: 'customerAmount', + key: 'customerAmount', + width: 118, + align: 'right', + render: function (v) { + return renderRefuelDrillMoney(h2FormatYuanSymbol(v, { keepZero: true }), 'customer'); + } + }, + { + title: '承担方式', + dataIndex: 'settlementStatus', + key: 'settlementStatus', + width: 120, + render: function (v) { + var info = H2_SETTLEMENT_STATUS_MAP[v]; + if (!info) return '—'; + return React.createElement(Tag, { color: info.color, style: { margin: 0, borderRadius: 6, fontWeight: 600 } }, info.label); + } + }, + { + title: '订单编号', + dataIndex: 'orderNo', + key: 'orderNo', + width: 148, + align: 'center', + ellipsis: true, + render: function (v) { + if (!v) return React.createElement('span', { style: { color: '#cbd5e1' } }, '—'); + return React.createElement('span', { className: 'h2-refuel-drill-order-no', title: v }, v); + } + } + ]; + + var refuelModalSummary = useMemo(function () { + var records = refuelModal.records || []; + var totalKg = 0; + var totalCost = 0; + var totalCustomer = 0; + var i; + for (i = 0; i < records.length; i++) { + totalKg += records[i].hydrogenKg || 0; + totalCost += records[i].costAmount || 0; + totalCustomer += records[i].customerAmount || 0; + } + return { count: records.length, totalKg: totalKg, totalCost: totalCost, totalCustomer: totalCustomer }; + }, [refuelModal.records]); + + var renderRefuelDrillPanel = useCallback(function () { + var records = refuelModal.records || []; + var summary = refuelModalSummary; + var stationName = (refuelModal.station && refuelModal.station.name) || '—'; + var stats = [ + { key: 'count', label: '加氢次数', value: String(summary.count || 0), mod: 'count', valueMod: 'count' }, + { key: 'kg', label: '加氢量合计(kg)', value: h2FormatKgNum(summary.totalKg), mod: 'kg', valueMod: 'kg' }, + { key: 'cost', label: '成本总价', value: h2FormatYuanSymbol(summary.totalCost, { keepZero: true }), mod: 'cost', valueMod: 'cost' }, + { key: 'customer', label: '加氢总价', value: h2FormatYuanSymbol(summary.totalCustomer, { keepZero: true }), mod: 'customer', valueMod: 'customer' } + ]; + return React.createElement('div', { className: 'h2-refuel-drill-panel' }, + React.createElement('div', { className: 'h2-refuel-drill-station-card' }, + React.createElement('div', { className: 'h2-refuel-drill-station-card__name' }, stationName), + React.createElement('div', { className: 'h2-refuel-drill-station-card__meta' }, '车辆加氢记录 · 共 ' + records.length + ' 笔') + ), + React.createElement('div', { className: 'h2-refuel-drill-stats', role: 'group', 'aria-label': '加氢统计' }, + stats.map(function (item) { + return React.createElement('div', { + key: item.key, + className: 'h2-refuel-drill-stat h2-refuel-drill-stat--' + item.mod, + role: 'article', + 'aria-label': item.label + ' ' + item.value + }, + React.createElement('div', { className: 'h2-refuel-drill-stat__label' }, item.label), + React.createElement('div', { + className: 'h2-refuel-drill-stat__value h2-refuel-drill-stat__value--' + item.valueMod + }, item.value) + ); + }) + ), + React.createElement('div', { className: 'h2-refuel-drill-table-wrap' }, + React.createElement('div', { className: 'h2-refuel-drill-table-head' }, + React.createElement('div', { className: 'h2-refuel-drill-table-head__left' }, + React.createElement('span', { className: 'h2-refuel-drill-table-head__title' }, '加氢明细'), + React.createElement('span', { className: 'h2-refuel-drill-table-head__count' }, '共 ' + records.length + ' 条') + ), + React.createElement(Button, { + size: 'small', + icon: H2_ICONS.download, + onClick: handleExportRefuelDrill, + style: { borderRadius: 8, fontWeight: 600, borderColor: '#10b981', color: '#059669' }, + 'aria-label': '导出加氢明细' + }, '导出') + ), + React.createElement(Table, { + className: 'h2-refuel-record-table', + size: 'small', + bordered: false, + rowKey: 'id', + columns: refuelRecordColumns, + dataSource: records, + pagination: records.length > 8 ? { pageSize: 8, showSizeChanger: false, size: 'small', showTotal: function (t) { return '共 ' + t + ' 条'; } } : false, + locale: { emptyText: '暂无加氢记录' }, + scroll: records.length > 8 ? { x: 'max-content', y: 320 } : { x: 'max-content' } + }) + ) + ); + }, [refuelModal, refuelModalSummary, refuelRecordColumns, handleExportRefuelDrill]); + + var statementRecordColumns = [ + { + title: '序号', + key: 'seq', + width: 52, + align: 'center', + render: function (_, __, index) { + return React.createElement('span', { className: 'h2-refuel-drill-seq' }, index + 1); + } + }, + { + title: '加氢时间', + dataIndex: 'hydrogenTime', + key: 'hydrogenTime', + width: 158, + render: function (v) { + return React.createElement('span', { className: 'h2-refuel-drill-time' }, v || '—'); + } + }, + { + title: '车牌号', + dataIndex: 'plateNo', + key: 'plateNo', + width: 108, + render: function (v) { + return React.createElement('span', { className: 'h2-refuel-drill-plate' }, v || '—'); + } + }, + { + title: '加氢量(kg)', + dataIndex: 'hydrogenKg', + key: 'hydrogenKg', + width: 96, + align: 'right', + render: function (v) { + return React.createElement('span', { className: 'h2-refuel-drill-kg' }, h2FormatKgNum(v)); + } + }, + { + title: '成本单价(元/kg)', + dataIndex: 'costUnitPrice', + key: 'costUnitPrice', + width: 118, + align: 'right', + render: function (v) { + return renderRefuelDrillMoney(h2FormatYuanSymbol(v, { keepZero: true }), 'cost'); + } + }, + { + title: '成本总价(元)', + dataIndex: 'costTotal', + key: 'costTotal', + width: 118, + align: 'right', + render: function (v) { + return renderRefuelDrillMoney(h2FormatYuanSymbol(v, { keepZero: true }), 'cost'); + } + }, + { + title: '订单编号', + dataIndex: 'orderNo', + key: 'orderNo', + width: 148, + align: 'center', + ellipsis: true, + render: function (v) { + if (!v) return React.createElement('span', { style: { color: '#cbd5e1' } }, '—'); + return React.createElement('span', { className: 'h2-refuel-drill-order-no', title: v }, v); + } + } + ]; + + var renderStatementBalanceReadonly = function (balance) { + var n = h2NumOrZero(balance); + var isArrears = n < 0; + return React.createElement('div', { className: 'h2-statement-balance-readonly' }, + React.createElement('span', { + className: 'h2-statement-balance-readonly__amount ' + (isArrears ? 'h2-statement-balance-readonly__amount--negative' : 'h2-statement-balance-readonly__amount--positive') + }, h2FormatYuanNum(n)), + isArrears ? React.createElement(Tag, { color: 'error', className: 'lc-station-signed-tag' }, '已欠费') : null + ); + }; + + var renderStatementPanel = useCallback(function () { + var record = statementModal.record; + var stationName = (record && record.name) || '—'; + var isDraft = statementModal.phase === 'draft'; + var rows = isDraft ? (statementModal.draftRows || []) : []; + var summary = isDraft ? statementDraftSummary : { count: 0, totalKg: 0, totalCost: 0 }; + var lastEndDate = h2GetLastStatementEndDate(statementRecords, stationName); + var stats = [ + { key: 'count', label: '加氢次数', value: String(summary.count || 0), mod: 'count', valueMod: 'count' }, + { key: 'kg', label: '加氢总量(kg)', value: h2FormatKgNum(summary.totalKg), mod: 'kg', valueMod: 'kg' }, + { key: 'cost', label: '成本总金额', value: h2FormatYuanSymbol(summary.totalCost, { keepZero: true }), mod: 'cost', valueMod: 'cost' } + ]; + return React.createElement('div', { className: 'h2-statement-panel' }, + React.createElement('div', { className: 'h2-statement-station-card' }, + React.createElement('div', { className: 'h2-statement-station-card__name' }, stationName), + React.createElement('div', { className: 'h2-statement-station-card__meta' }, + isDraft ? '对账记录 · 待提交' : '选择账单日期 · 生成对账记录' + ) + ), + React.createElement('div', { className: 'h2-statement-form-wrap' }, + React.createElement('div', { className: 'h2-statement-date-grid' }, + formItem('账单开始日期', true, React.createElement(Input, { + type: 'date', + style: { width: '100%', borderRadius: 8 }, + disabled: isDraft, + value: statementModal.startDate || '', + onChange: function (e) { + setStatementModal(function (m) { return Object.assign({}, m, { startDate: e.target.value, phase: 'select', draftRows: [] }); }); + } + })), + formItem('账单结束日期', true, React.createElement(Input, { + type: 'date', + style: { width: '100%', borderRadius: 8 }, + disabled: isDraft, + value: statementModal.endDate || '', + onChange: function (e) { + setStatementModal(function (m) { return Object.assign({}, m, { endDate: e.target.value, phase: 'select', draftRows: [] }); }); + } + })) + ), + React.createElement('div', { className: 'h2-statement-last-end' }, + '上次对账单结束时间:', + React.createElement('strong', null, lastEndDate || '暂无') + ), + !isDraft ? React.createElement('div', { className: 'h2-statement-form-hint' }, + '数据来源:', + React.createElement('strong', null, '车辆氢费明细'), + ' 中标记为「已对账」且尚未结算的加氢记录;明细仅展示成本价与成本总价。' + ) : null + ), + isDraft ? React.createElement('div', { className: 'h2-statement-stats', role: 'group', 'aria-label': '对账统计' }, + stats.map(function (item) { + return React.createElement('div', { + key: item.key, + className: 'h2-statement-stat h2-statement-stat--' + item.mod, + role: 'article', + 'aria-label': item.label + ' ' + item.value + }, + React.createElement('div', { className: 'h2-statement-stat__label' }, item.label), + React.createElement('div', { + className: 'h2-statement-stat__value h2-statement-stat__value--' + item.valueMod + }, item.value) + ); + }) + ) : null, + isDraft ? React.createElement('div', { className: 'h2-statement-table-wrap' }, + React.createElement('div', { className: 'h2-statement-table-head' }, + React.createElement('div', { className: 'h2-statement-table-head__left' }, + React.createElement('span', { className: 'h2-statement-table-head__title' }, '对账明细(成本字段)'), + React.createElement('span', { className: 'h2-statement-table-head__count' }, '共 ' + rows.length + ' 条') + ), + React.createElement(Button, { + size: 'small', + icon: H2_ICONS.download, + disabled: !rows.length, + onClick: handleExportStatement, + style: { borderRadius: 8, fontWeight: 600, borderColor: '#7c3aed', color: '#6d28d9' }, + 'aria-label': '导出对账单明细' + }, '导出') + ), + React.createElement(Table, { + className: 'h2-statement-record-table', + size: 'small', + bordered: false, + rowKey: 'id', + columns: statementRecordColumns, + dataSource: rows, + pagination: rows.length > 8 ? { pageSize: 8, showSizeChanger: false, size: 'small', showTotal: function (t) { return '共 ' + t + ' 条'; } } : false, + locale: { emptyText: '暂无对账明细' }, + scroll: rows.length > 8 ? { x: 'max-content', y: 320 } : { x: 'max-content' } + }) + ) : null, + isDraft ? React.createElement('div', { className: 'h2-statement-settlement-wrap' }, + formItem('结算后加氢站预付款余额', true, renderStatementBalanceReadonly(statementModal.balanceAfterSettlement)), + React.createElement('div', { className: 'h2-statement-receipt-grid' }, + formItem('收票日期', true, dayjs && DatePicker + ? React.createElement(DatePicker, { + style: { width: '100%', borderRadius: 8 }, + format: 'YYYY-MM-DD', + placeholder: '请选择收票日期', + allowClear: true, + value: h2ToDateDayjs(statementModal.receiptDate), + onChange: function (d, dateStr) { + setStatementModal(function (m) { + return Object.assign({}, m, { receiptDate: dateStr || '' }); + }); + } + }) + : React.createElement(Input, { + type: 'date', + style: { width: '100%', borderRadius: 8 }, + placeholder: '请选择收票日期', + value: statementModal.receiptDate || '', + onChange: function (e) { + setStatementModal(function (m) { return Object.assign({}, m, { receiptDate: e.target.value }); }); + } + })), + formItem('收票金额', true, React.createElement(Input, { + className: 'h2-statement-receipt-amount-input', + inputMode: 'decimal', + prefix: '¥', + style: { width: '100%', borderRadius: 8 }, + placeholder: '请输入收票金额', + value: statementModal.receiptAmount || '', + onChange: function (e) { + var next = h2SanitizeReceiptAmountInput(e.target.value); + setStatementModal(function (m) { return Object.assign({}, m, { receiptAmount: next }); }); + }, + onBlur: function (e) { + var formatted = h2FormatReceiptAmountInput(e.target.value); + setStatementModal(function (m) { return Object.assign({}, m, { receiptAmount: formatted }); }); + } + })) + ), + formItem('发票附件', true, React.createElement(ContractFilesUpload, { + fileList: statementModal.invoiceFiles || [], + showHint: false, + wrapClassName: 'h2-statement-invoice-upload', + uploadClassName: 'h2-statement-upload-btn-wrap', + buttonClassName: 'h2-statement-upload-btn', + onChange: function (info) { + setStatementModal(function (m) { return Object.assign({}, m, { invoiceFiles: info.fileList || [] }); }); + } + })) + ) : null + ); + }, [statementModal, statementRecords, statementDraftSummary, statementRecordColumns, handleExportStatement]); + + var statementHistoryColumns = [ + { title: '对账日期', dataIndex: 'reconcileDate', key: 'reconcileDate', width: 150 }, + { title: '对账人', dataIndex: 'reconciler', key: 'reconciler', width: 100, ellipsis: true }, + { title: '账单开始日期', dataIndex: 'startDate', key: 'startDate', width: 118 }, + { title: '账单结束日期', dataIndex: 'endDate', key: 'endDate', width: 118 }, + { title: '加氢次数', dataIndex: 'refuelCount', key: 'refuelCount', width: 88, align: 'right' }, + { + title: '加氢金额', + dataIndex: 'totalCost', + key: 'totalCost', + width: 110, + align: 'right', + render: function (v) { return h2FormatYuanSymbol(v, { keepZero: true }); } + }, + { + title: '对账后加氢站预付款余额', + dataIndex: 'balanceAfterSettlement', + key: 'balanceAfterSettlement', + width: 168, + align: 'right', + render: function (v) { + var n = h2NumOrZero(v); + var isArrears = n < 0; + return React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 6, flexWrap: 'wrap' } }, + React.createElement('span', { + className: 'h2-statement-history-balance ' + (isArrears ? 'h2-statement-history-balance--negative' : 'h2-statement-history-balance--positive') + }, h2FormatYuanNum(n)), + isArrears ? React.createElement(Tag, { color: 'error', className: 'lc-station-signed-tag' }, '已欠费') : null + ); + } + }, + { + title: '操作', + key: 'action', + width: 96, + fixed: 'right', + render: function (_, row) { + return React.createElement(Button, { + type: 'link', + size: 'small', + onClick: function () { openStatementDetailModal(row); } + }, '查看明细'); + } + } + ]; + + var renderStatementHistoryPanel = useCallback(function () { + var record = statementHistoryModal.record; + var stationName = (record && record.name) || '—'; + var rows = h2GetStatementRecordsByStation(statementRecords, stationName); + return React.createElement('div', { className: 'h2-statement-history-panel' }, + React.createElement('div', { className: 'h2-statement-history-station-card' }, + React.createElement('div', { className: 'h2-statement-history-station-card__name' }, stationName), + React.createElement('div', { className: 'h2-statement-history-station-card__meta' }, '历史对账记录 · 共 ' + rows.length + ' 条') + ), + React.createElement('div', { className: 'h2-statement-history-table-wrap' }, + React.createElement(Table, { + size: 'small', + bordered: false, + rowKey: 'id', + columns: statementHistoryColumns, + dataSource: rows, + pagination: rows.length > 8 ? { pageSize: 8, showSizeChanger: false, size: 'small', showTotal: function (t) { return '共 ' + t + ' 条'; } } : false, + locale: { emptyText: '暂无对账记录' }, + scroll: { x: 'max-content' } + }) + ) + ); + }, [statementHistoryModal, statementRecords, statementHistoryColumns, openStatementDetailModal]); + + var renderStatementDetailPanel = useCallback(function () { + var stmt = statementDetailModal.statementRecord; + var rows = statementDetailModal.detailRows || []; + if (!stmt) return null; + var summary = h2CalcStatementSummary(rows); + var invoiceFiles = stmt.invoiceFiles || []; + return React.createElement('div', { className: 'h2-statement-panel' }, + React.createElement('div', { className: 'h2-statement-station-card' }, + React.createElement('div', { className: 'h2-statement-station-card__name' }, stmt.stationName || '—'), + React.createElement('div', { className: 'h2-statement-station-card__meta' }, + (stmt.startDate || '—') + ' 至 ' + (stmt.endDate || '—') + ) + ), + React.createElement('div', { className: 'h2-statement-stats', role: 'group', 'aria-label': '对账统计' }, + [ + { key: 'count', label: '加氢次数', value: String(summary.count || stmt.refuelCount || 0), mod: 'count', valueMod: 'count' }, + { key: 'kg', label: '加氢总量(kg)', value: h2FormatKgNum(summary.totalKg || stmt.totalKg), mod: 'kg', valueMod: 'kg' }, + { key: 'cost', label: '成本总金额', value: h2FormatYuanSymbol(summary.totalCost || stmt.totalCost, { keepZero: true }), mod: 'cost', valueMod: 'cost' } + ].map(function (item) { + return React.createElement('div', { + key: item.key, + className: 'h2-statement-stat h2-statement-stat--' + item.mod + }, + React.createElement('div', { className: 'h2-statement-stat__label' }, item.label), + React.createElement('div', { className: 'h2-statement-stat__value h2-statement-stat__value--' + item.valueMod }, item.value) + ); + }) + ), + React.createElement('div', { className: 'h2-statement-detail-receipt-wrap' }, + React.createElement('div', { className: 'h2-statement-detail-receipt-grid' }, + React.createElement('div', { className: 'h2-statement-detail-receipt-item' }, + React.createElement('div', { className: 'h2-statement-detail-receipt-item__label' }, '收票时间'), + React.createElement('div', { className: 'h2-statement-detail-receipt-item__value' }, stmt.receiptDate || '—') + ), + React.createElement('div', { className: 'h2-statement-detail-receipt-item' }, + React.createElement('div', { className: 'h2-statement-detail-receipt-item__label' }, '收票金额'), + React.createElement('div', { className: 'h2-statement-detail-receipt-item__value h2-statement-detail-receipt-item__value--amount' }, + stmt.receiptAmount != null ? h2FormatYuanSymbol(stmt.receiptAmount, { keepZero: true }) : '—' + ) + ) + ), + React.createElement('div', { className: 'h2-statement-detail-receipt-files' }, + React.createElement('div', { className: 'h2-statement-detail-receipt-item__label' }, '发票附件'), + invoiceFiles.length + ? React.createElement('div', { className: 'h2-statement-detail-receipt-file-list' }, + invoiceFiles.map(function (file) { + return React.createElement('div', { key: file.uid || file.name, className: 'h2-statement-detail-receipt-file-item' }, + React.createElement('span', { className: 'h2-statement-detail-receipt-file-name', title: file.name }, file.name || '未命名附件'), + React.createElement(Button, { + type: 'link', + size: 'small', + icon: H2_ICONS.download, + onClick: function () { h2DownloadUploadFile(file); } + }, '下载') + ); + }) + ) + : React.createElement('div', { style: { fontSize: 13, color: '#94a3b8', marginTop: 8 } }, '暂无发票附件') + ) + ), + React.createElement('div', { className: 'h2-statement-table-wrap' }, + React.createElement('div', { className: 'h2-statement-table-head' }, + React.createElement('div', { className: 'h2-statement-table-head__left' }, + React.createElement('span', { className: 'h2-statement-table-head__title' }, '对账明细'), + React.createElement('span', { className: 'h2-statement-table-head__count' }, '共 ' + rows.length + ' 条') + ) + ), + React.createElement(Table, { + className: 'h2-statement-record-table', + size: 'small', + bordered: false, + rowKey: 'id', + columns: statementRecordColumns, + dataSource: rows, + pagination: rows.length > 8 ? { pageSize: 8, showSizeChanger: false, size: 'small' } : false, + locale: { emptyText: '暂无明细' }, + scroll: rows.length > 8 ? { x: 'max-content', y: 320 } : { x: 'max-content' } + }) + ) + ); + }, [statementDetailModal, statementRecordColumns]); + + var renderPriceConfigMoney = function (text, tone) { + return React.createElement('span', { className: 'h2-price-config-money h2-price-config-money--' + tone }, text); + }; + + var costPriceLogColumns = [ + { title: '操作人', dataIndex: 'operator', key: 'operator', width: 92, ellipsis: true }, + { title: '操作时间', dataIndex: 'operateTime', key: 'operateTime', width: 138 }, + { + title: '定价方式', + dataIndex: 'priceType', + key: 'priceType', + width: 88, + render: function (v) { return h2FormatPriceTypeLabel(v || 'fixed'); } + }, + { + title: '适用客户', + dataIndex: 'customerName', + key: 'customerName', + width: 120, + ellipsis: true, + render: function (v, row) { + return row.customerScope === 'customer' ? (v || '指定客户') : '全部客户'; + } + }, + { + title: '调整前', + dataIndex: 'beforeCostPrice', + key: 'beforeCostPrice', + width: 96, + align: 'right', + render: function (v) { return renderPriceConfigMoney(h2FormatCostPriceDisplay(v), 'before'); } + }, + { + title: '调整后', + dataIndex: 'afterCostPrice', + key: 'afterCostPrice', + width: 148, + align: 'left', + render: function (v, row) { + if (row.priceType === 'tiered') { + var lines = h2GetTierRuleDisplayLines(row); + if (lines.length) { + return React.createElement('div', { className: 'h2-price-config-after-lines' }, + lines.map(function (line, idx) { + return React.createElement('div', { key: idx, className: 'h2-price-config-after-line' }, + renderPriceConfigMoney(line, 'after') + ); + }) + ); + } + } + return renderPriceConfigMoney(h2FormatCostPriceDisplay(v), 'after'); + } + }, + { title: '生效时间', dataIndex: 'effectiveTime', key: 'effectiveTime', width: 138 } + ]; + + var effectivePriceConfigColumns = useMemo(function () { + return [ + { + title: '适用客户', + key: 'scope', + width: 120, + ellipsis: true, + render: function (_, cfg) { + return h2FormatPriceConfigScopeLabel(cfg); + } + }, + { + title: '定价方式', + key: 'priceType', + width: 96, + render: function (_, cfg) { + var isTiered = cfg.priceType === 'tiered'; + return React.createElement(Tag, { + color: isTiered ? 'purple' : 'processing', + bordered: false, + style: { margin: 0, borderRadius: 6, fontWeight: 600 } + }, h2FormatPriceTypeLabel(cfg.priceType)); + } + }, + { + title: '当前价格', + key: 'price', + width: 220, + render: function (_, cfg) { + var isTiered = cfg.priceType === 'tiered'; + var cfgPrice = h2ResolveConfigCurrentPrice(cfg); + var priceText = cfgPrice != null ? h2FormatYuanNum(cfgPrice) + '元/kg' : '—'; + if (isTiered) { + var gapKg = h2GetTierGapToNextThreshold(cfg); + if (gapKg != null) { + priceText += ',距下个阶梯还差' + h2FormatKgNum(gapKg) + ' kg'; + } + } + return React.createElement('span', { + className: 'h2-price-config-effective-price', + style: { whiteSpace: 'normal', lineHeight: 1.5 } + }, priceText); + } + }, + { + title: '生效时间', + key: 'effectiveTime', + width: 150, + render: function (_, cfg) { + return h2FormatConfigEffectiveDateTime(cfg); + } + }, + { + title: '阶梯重置', + key: 'tierReset', + width: 100, + render: function (_, cfg) { + return cfg.priceType === 'tiered' ? h2GetTierResetTimeLabel(cfg) : '—'; + } + }, + { + title: '操作', + key: 'action', + width: 72, + fixed: 'right', + render: function (_, cfg) { + return React.createElement(Button, { + type: 'link', + size: 'small', + onClick: function () { loadPriceModalFromConfig(cfg); } + }, '修改'); + } + } + ]; + }, [loadPriceModalFromConfig]); + + var businessStatusLogColumns = [ + { title: '操作时间', dataIndex: 'operateTime', key: 'operateTime', width: 150 }, + { title: '操作人', dataIndex: 'operator', key: 'operator', width: 100, ellipsis: true }, + { + title: '修改前状态', + dataIndex: 'beforeStatus', + key: 'beforeStatus', + width: 110, + render: function (v) { return renderBusinessStatusText(v, 'muted'); } + }, + { + title: '修改后状态', + dataIndex: 'afterStatus', + key: 'afterStatus', + width: 110, + render: function (v) { return renderBusinessStatusTag(v); } + } + ]; + + var renderCurrentCostPriceCell = function (record) { + var primaryCfg = h2GetPrimaryPriceConfig(record); + var price = h2ResolveCurrentCostPrice(record); + var multiPrice = h2HasMultiCustomerPriceSystems(record); + var isTiered = primaryCfg && primaryCfg.priceType === 'tiered'; + var typeTag = React.createElement(Tag, { + className: 'h2-price-type-tag', + color: isTiered ? 'purple' : 'processing', + bordered: false + }, isTiered ? '阶梯' : '固定'); + var priceNode = price == null + ? React.createElement('span', { style: { color: '#94a3b8' } }, '—') + : React.createElement('span', { + style: { fontVariantNumeric: 'tabular-nums', fontWeight: 600, color: '#0f172a' } + }, '¥' + h2FormatYuanNum(price)); + var warnNode = multiPrice + ? React.createElement(Tooltip, { title: '多个客户存在不同价格体系' }, + React.createElement('span', { + className: 'h2-price-multi-warn', + role: 'img', + 'aria-label': '多个客户存在不同价格体系', + tabIndex: 0 + }, H2_ICONS.none) + ) + : null; + return React.createElement('div', { className: 'h2-current-cost-price-cell' }, typeTag, priceNode, warnNode); + }; + + var columns = [ + { + title: '加氢站名称', + dataIndex: 'name', + key: 'name', + width: 300, + fixed: 'left', + ellipsis: true, + render: function (v, r) { + var addressLine = h2FormatStationAddressLine(r); + return React.createElement('div', { className: 'lc-station-name-cell' }, + React.createElement('div', { className: 'lc-station-name-row' }, + React.createElement('div', { className: 'lc-station-name' }, v || '—'), + h2RenderStationSignedTag(r) + ), + React.createElement('div', { className: 'lc-station-address-line', title: addressLine }, addressLine) + ); + } + }, + { + title: '合作时间', + key: 'contractRange', + width: 220, + render: function (_, r) { + if (!r.isSigned) return React.createElement('span', { style: { color: '#94a3b8' } }, '—'); + return React.createElement('div', null, + React.createElement('div', { style: { fontSize: 12, fontWeight: 600, color: '#334155' } }, + (r.contractStart || '—') + ' 至 ' + (r.contractEnd || '—') + ), + r.contractEnd ? h2RenderContractRemainTag(r.contractEnd) : null + ); + } + }, + { + title: '营业状态', + dataIndex: 'businessStatus', + key: 'businessStatus', + width: 110, + render: renderBusinessStatusTag + }, + { title: '营业时间', dataIndex: 'businessHours', key: 'businessHours', width: 140, render: function (v) { return h2DisplayBusinessHours(v); } }, + { + title: '当前成本价格(元/kg)', + key: 'currentCostPrice', + width: 168, + align: 'right', + render: function (_, r) { + return renderCurrentCostPriceCell(r); + } + }, + { + title: '加氢次数', + dataIndex: 'refuelCount', + key: 'refuelCount', + width: 110, + align: 'right', + sortOrder: refuelSort.key === 'refuelCount' ? refuelSort.order : null, + sorter: function () { return 0; }, + sortDirections: ['descend', 'ascend'], + showSorterTooltip: { title: '点击切换倒序 / 正序' }, + render: function (v) { + return React.createElement('span', { + style: { fontVariantNumeric: 'tabular-nums', fontWeight: 600, color: '#0f172a' } + }, String(v || 0)); + } + }, + { + title: '加氢量(kg)', + dataIndex: 'refuelTotalKg', + key: 'refuelTotalKg', + width: 160, + align: 'right', + sortOrder: refuelSort.key === 'refuelTotalKg' ? refuelSort.order : null, + sorter: function () { return 0; }, + sortDirections: ['descend', 'ascend'], + showSorterTooltip: { title: '点击切换倒序 / 正序' }, + render: function (v, record) { + return React.createElement('div', { className: 'lc-refuel-kg-row' }, + renderRefuelFreqShortTag(record.refuelFreqKey), + renderRefuelDrillLink(h2FormatKgNum(v), record, '查看加氢记录,合计 ' + h2FormatKgNum(v) + ' kg') + ); + } + }, + { + title: '预付余额(元)', + dataIndex: 'prepaidBalance', + key: 'prepaidBalance', + width: 188, + align: 'right', + className: 'h2-col-prepaid-balance', + onCell: function () { + return { className: 'h2-cell-prepaid-balance' }; + }, + render: function (v, record) { return renderPrepaidBalance(v, record); } + }, + { + title: '联系方式', + key: 'contactInfo', + width: 120, + render: function (_, r) { + var mobile = r.mobilePhone || r.phone || ''; + var landline = r.landlinePhone || ''; + return React.createElement('div', null, + React.createElement('div', { className: 'lc-station-contact-name' }, r.contact || '—'), + mobile ? React.createElement('div', { className: 'lc-station-contact-phone' }, mobile) : null, + landline ? React.createElement('div', { className: 'lc-station-contact-phone' }, landline) : null, + !mobile && !landline ? React.createElement('div', { className: 'lc-station-contact-phone' }, '—') : null + ); + } + }, + { + title: '操作', + key: 'action', + width: 108, + fixed: 'right', + render: function (_, record) { + return React.createElement('div', { className: 'h2-row-actions' }, + React.createElement(Button, { + type: 'link', + size: 'small', + className: 'lc-action-btn', + onClick: function () { openView(record); } + }, '查看'), + React.createElement(Dropdown, { + trigger: ['click'], + placement: 'bottomRight', + menu: { items: getRowMoreMenuItems(record) } + }, + React.createElement(Tooltip, { title: '更多' }, + React.createElement('span', { + className: 'h2-action-more-btn', + role: 'button', + tabIndex: 0, + 'aria-label': '更多操作', + onClick: function (e) { e.stopPropagation(); } + }, h2MoreIcon()) + ) + ) + ); + } + } + ]; + + var formItem = function (label, required, node, extra) { + return React.createElement(Form.Item, { + label: required ? React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), label) : label, + extra: extra + }, node); + }; + + var renderBusinessStatusPanel = useCallback(function () { + var record = businessModal.record; + var logs = businessModal.statusLogs || []; + var stationName = (record && record.name) || '—'; + var originStatus = businessModal.originBusinessStatus || (record && record.businessStatus) || '营业中'; + var originHours = (record && record.businessHours) || '—'; + var displayHours = businessModal.businessHours || originHours; + var pendingChanged = (businessModal.businessStatus || '营业中') !== originStatus; + var stats = [ + { + key: 'status', + label: '当前营业状态', + value: renderBusinessStatusTag(originStatus), + mod: 'status', + valueMod: '' + }, + { + key: 'hours', + label: '当前营业时间', + value: h2DisplayBusinessHours(displayHours), + mod: 'hours', + valueMod: 'hours' + } + ]; + + return React.createElement('div', { className: 'h2-business-status-panel' }, + React.createElement('div', { className: 'h2-business-status-station-card' }, + React.createElement('div', { className: 'h2-business-status-station-card__name' }, stationName), + React.createElement('div', { className: 'h2-business-status-station-card__meta' }, '营业状态维护 · 共 ' + logs.length + ' 条记录') + ), + React.createElement('div', { className: 'h2-business-status-stats', role: 'group', 'aria-label': '营业状态统计' }, + stats.map(function (item) { + return React.createElement('div', { + key: item.key, + className: 'h2-business-status-stat h2-business-status-stat--' + item.mod, + role: 'article', + 'aria-label': item.label + }, + React.createElement('div', { className: 'h2-business-status-stat__label' }, item.label), + React.createElement('div', { + className: 'h2-business-status-stat__value' + (item.valueMod ? ' h2-business-status-stat__value--' + item.valueMod : '') + }, item.value) + ); + }) + ), + React.createElement('div', { className: 'h2-business-status-form-wrap' }, + React.createElement(Form, { layout: 'vertical', requiredMark: false }, + formItem('营业状态', true, h2RenderOptionButtonGroup( + H2_BUSINESS_STATUS_BTN_OPTIONS, + businessModal.businessStatus || '营业中', + function (v) { setBusinessModal(function (m) { return Object.assign({}, m, { businessStatus: v }); }); }, + { ariaLabel: '营业状态' } + )), + formItem('营业时间', false, React.createElement(BusinessHoursInput, { + value: businessModal.businessHours, + fieldStyle: { width: '100%', borderRadius: 8 }, + onChange: function (v) { setBusinessModal(function (m) { return Object.assign({}, m, { businessHours: v }); }); } + })) + ), + React.createElement('div', { className: 'h2-business-status-form-hint', role: 'note' }, + pendingChanged + ? '保存后将记录营业状态变更,并更新列表展示。' + : '选择「非全天营业」后需设置开始与结束时间;暂停或停止营业时建议配置具体时段。' + ) + ), + React.createElement('div', { className: 'h2-business-status-table-wrap' }, + React.createElement('div', { className: 'h2-business-status-table-head' }, + React.createElement('span', { className: 'h2-business-status-table-head__title' }, '营业状态变更记录'), + React.createElement('span', { className: 'h2-business-status-table-head__count' }, '共 ' + logs.length + ' 条') + ), + React.createElement(Table, { + className: 'h2-business-status-record-table', + size: 'small', + bordered: false, + rowKey: 'id', + columns: businessStatusLogColumns, + dataSource: logs, + pagination: logs.length > 5 ? { pageSize: 5, showSizeChanger: false, size: 'small', showTotal: function (t) { return '共 ' + t + ' 条'; } } : false, + locale: { emptyText: '暂无状态变更记录' }, + scroll: logs.length > 5 ? { x: 520, y: 240 } : { x: 520 } + }) + ) + ); + }, [businessModal, businessStatusLogColumns]); + + var renderPriceConfigPanel = useCallback(function () { + var record = priceModal.record; + var logs = priceModal.priceLogs || []; + var stationName = (record && record.name) || '—'; + var priceConfigs = priceModal.priceConfigs || []; + var priceType = priceModal.priceType || 'fixed'; + var assignedCustomerIds = h2GetAssignedCustomerIds(priceConfigs, priceModal.editingConfigId); + var allCustomerOptions = h2CollectCustomerOptions(H2_MOCK_REFUEL_RECORDS); + var selectedCustomerIds = priceModal.selectedCustomerIds || []; + var enabledTierCustomerOptions = allCustomerOptions.filter(function (opt) { + return !assignedCustomerIds[opt.value]; + }); + var enabledSelectedCount = enabledTierCustomerOptions.filter(function (opt) { + return selectedCustomerIds.indexOf(opt.value) >= 0; + }).length; + var tierAllSelected = enabledTierCustomerOptions.length > 0 && enabledSelectedCount === enabledTierCustomerOptions.length; + var tierIndeterminate = enabledSelectedCount > 0 && !tierAllSelected; + + var toggleTierCustomer = function (customerId, checked) { + setPriceModal(function (m) { + var current = (m.selectedCustomerIds || []).slice(); + var next = checked + ? (current.indexOf(customerId) >= 0 ? current : current.concat([customerId])) + : current.filter(function (id) { return id !== customerId; }); + var names = allCustomerOptions.filter(function (opt) { return next.indexOf(opt.value) >= 0; }).map(function (opt) { return opt.label; }); + return Object.assign({}, m, { + selectedCustomerIds: next, + customerScope: next.length ? 'customer' : 'all', + customerId: next.length === 1 ? next[0] : null, + customerName: next.length === 1 ? (names[0] || null) : (names.length ? names.join('、') : null) + }); + }); + }; + + var toggleTierCustomerAll = function (checked) { + setPriceModal(function (m) { + var next = checked ? enabledTierCustomerOptions.map(function (opt) { return opt.value; }) : []; + var names = allCustomerOptions.filter(function (opt) { return next.indexOf(opt.value) >= 0; }).map(function (opt) { return opt.label; }); + return Object.assign({}, m, { + selectedCustomerIds: next, + customerScope: next.length ? 'customer' : 'all', + customerId: next.length === 1 ? next[0] : null, + customerName: next.length === 1 ? (names[0] || null) : (names.length ? names.join('、') : null) + }); + }); + }; + + var tierCustomerTriggerText = selectedCustomerIds.length + ? allCustomerOptions.filter(function (opt) { return selectedCustomerIds.indexOf(opt.value) >= 0; }).map(function (opt) { return opt.label; }).join('、') + : (priceType === 'fixed' && (priceModal.customerScope === 'all' || !priceModal.editingConfigId) ? '全部客户' : ''); + var customerPickerHint = priceType === 'fixed' + ? '默认对全部客户生效;已为某客户单独配置的,将自动从本规则中排除' + : undefined; + var resetDayOptions = []; + var di; + for (di = 1; di <= 28; di++) { + resetDayOptions.push({ value: di, label: '每月 ' + di + ' 日' }); + } + var effectiveTimeValue = h2ToDateTimeDayjs(priceModal.effectiveTime); + var effectivePicker = dayjs && DatePicker + ? React.createElement(DatePicker, { + showTime: true, + needConfirm: false, + style: { width: '100%', borderRadius: 8 }, + format: 'YYYY-MM-DD HH:mm', + placeholder: '请选择生效时间', + allowClear: true, + value: effectiveTimeValue, + onChange: function (d, dateStr) { + setPriceModal(function (m) { + var next = dateStr || (d && d.isValid && d.isValid() ? d.format('YYYY-MM-DD HH:mm') : ''); + return Object.assign({}, m, { effectiveTime: next }); + }); + } + }) + : React.createElement(Input, { + type: 'datetime-local', + style: { borderRadius: 8 }, + value: priceModal.effectiveTime ? priceModal.effectiveTime.replace(' ', 'T') : '', + onChange: function (e) { + var raw = e.target.value || ''; + setPriceModal(function (m) { + return Object.assign({}, m, { effectiveTime: raw ? raw.replace('T', ' ') : '' }); + }); + } + }); + + return React.createElement('div', { className: 'h2-price-config-panel' }, + React.createElement('div', { className: 'h2-price-config-station-card' }, + React.createElement('div', { className: 'h2-price-config-station-card__row' }, + React.createElement('div', { className: 'h2-price-config-station-card__name' }, stationName), + h2RenderStationSignedTag(record) + ) + ), + React.createElement('div', { className: 'h2-price-config-table-wrap' }, + React.createElement('div', { className: 'h2-price-config-table-head' }, + React.createElement('span', { className: 'h2-price-config-table-head__title' }, '当前已生效价格配置'), + React.createElement(Button, { type: 'link', size: 'small', onClick: resetPriceModalToNew, style: { padding: 0, height: 'auto' } }, '+ 新增规则') + ), + React.createElement(Table, { + className: 'h2-price-config-record-table h2-price-config-effective-table', + size: 'small', + bordered: false, + rowKey: 'id', + columns: effectivePriceConfigColumns, + dataSource: priceConfigs, + pagination: priceConfigs.length > 5 ? { pageSize: 5, showSizeChanger: false, size: 'small', showTotal: function (t) { return '共 ' + t + ' 条'; } } : false, + locale: { emptyText: '暂无已生效价格配置,点击「+ 新增规则」创建' }, + scroll: { x: 860 } + }) + ), + priceModal.priceFormVisible + ? React.createElement('div', { className: 'h2-price-config-form-wrap' }, + React.createElement('div', { className: 'h2-price-config-form-head' }, + React.createElement('span', { className: 'h2-price-config-form-head__title' }, priceModal.editingConfigId ? '修改价格配置' : '新增价格配置'), + React.createElement(Button, { type: 'link', size: 'small', onClick: cancelPriceFormEdit, style: { padding: 0, height: 'auto' } }, '收起') + ), + React.createElement(Form, { layout: 'vertical', requiredMark: false }, + React.createElement('div', { className: 'h2-price-config-type-row' }, + React.createElement('div', { className: 'h2-price-config-type-row__col' }, + formItem('定价方式', true, React.createElement(Radio.Group, { + value: priceType, + onChange: function (e) { + var nextType = e.target.value; + setPriceModal(function (m) { + var patch = { priceType: nextType, tierCustomerPickerOpen: false }; + if (!(m.selectedCustomerIds || []).length && m.customerScope === 'customer' && m.customerId) { + patch.selectedCustomerIds = [m.customerId]; + } + return Object.assign({}, m, patch); + }); + } + }, + React.createElement(Radio.Button, { value: 'fixed' }, '固定价格'), + React.createElement(Radio.Button, { value: 'tiered' }, '阶梯价格') + )) + ), + React.createElement('div', { className: 'h2-price-config-type-row__col' }, + formItem('适用客户', true, React.createElement(Dropdown, { + trigger: ['click'], + overlayClassName: 'h2-tier-customer-dropdown-overlay', + open: priceModal.tierCustomerPickerOpen, + onOpenChange: function (open) { + setPriceModal(function (m) { return Object.assign({}, m, { tierCustomerPickerOpen: open }); }); + }, + dropdownRender: function () { + return React.createElement('div', { className: 'h2-tier-customer-dropdown', onClick: function (e) { e.stopPropagation(); } }, + React.createElement('div', { + className: 'h2-tier-customer-dropdown__item is-all', + onClick: function () { toggleTierCustomerAll(!tierAllSelected); } + }, + React.createElement(Checkbox, { + checked: tierAllSelected, + indeterminate: tierIndeterminate, + onChange: function (e) { toggleTierCustomerAll(e.target.checked); } + }), + React.createElement('span', null, '全选') + ), + allCustomerOptions.length + ? allCustomerOptions.map(function (opt) { + var disabled = !!assignedCustomerIds[opt.value]; + var checked = selectedCustomerIds.indexOf(opt.value) >= 0; + return React.createElement('div', { + key: opt.value, + className: 'h2-tier-customer-dropdown__item' + (disabled ? ' is-disabled' : ''), + onClick: function () { + if (disabled) return; + toggleTierCustomer(opt.value, !checked); + } + }, + React.createElement(Checkbox, { + checked: checked, + disabled: disabled, + onChange: function (e) { + if (disabled) return; + toggleTierCustomer(opt.value, e.target.checked); + } + }), + React.createElement('span', { title: opt.label }, opt.label), + disabled ? React.createElement('span', { style: { marginLeft: 'auto', fontSize: 11, color: '#94a3b8' } }, '已有规则') : null + ); + }) + : React.createElement('div', { className: 'h2-tier-customer-dropdown__hint' }, '暂无可选客户'), + priceType === 'tiered' + ? React.createElement('div', { className: 'h2-tier-customer-dropdown__hint' }, '同一客户仅可配置一套生效规则;已有规则的客户不可重复选择') + : null + ); + } + }, + React.createElement('div', { + className: 'h2-tier-customer-picker__trigger' + (priceModal.tierCustomerPickerOpen ? ' is-open' : ''), + role: 'button', + tabIndex: 0, + 'aria-haspopup': 'listbox', + 'aria-expanded': !!priceModal.tierCustomerPickerOpen + }, + React.createElement('span', { + className: 'h2-tier-customer-picker__trigger-text' + (tierCustomerTriggerText ? '' : ' is-placeholder') + }, tierCustomerTriggerText || (priceType === 'fixed' ? '全部客户' : '请选择适用客户')), + React.createElement('span', { style: { color: '#94a3b8', fontSize: 12, flexShrink: 0 } }, selectedCustomerIds.length ? ('已选 ' + selectedCustomerIds.length) : '▼') + ) + ), customerPickerHint) + ) + ), + priceType === 'fixed' + ? React.createElement(Form.Item, { + label: React.createElement('span', null, + React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), + '价格配置' + ) + }, + React.createElement('div', { className: 'h2-price-tier-timing-row' }, + React.createElement('div', null, + React.createElement('div', { className: 'h2-price-tier-rule-field' }, + React.createElement('label', null, '成本价格(元/kg)'), + React.createElement(Input, { + value: priceModal.costUnitPrice, + placeholder: '请输入成本价格,保留两位小数', + style: { borderRadius: 8 }, + onChange: function (e) { setPriceModal(function (m) { return Object.assign({}, m, { costUnitPrice: e.target.value }); }); }, + onBlur: function (e) { + var formatted = h2FormatReceiptAmountInput(e.target.value); + if (formatted) setPriceModal(function (m) { return Object.assign({}, m, { costUnitPrice: formatted }); }); + } + }) + ) + ), + React.createElement('div', null, + React.createElement('div', { className: 'h2-price-tier-rule-field' }, + React.createElement('label', null, '生效时间'), + effectivePicker + ) + ) + ) + ) + : React.createElement(Form.Item, { + label: React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), '阶梯价格条件') + }, + React.createElement('div', { className: 'h2-price-tier-rules' }, + (priceModal.tierRules || []).map(function (rule, idx) { + return React.createElement('div', { key: rule.id, className: 'h2-price-tier-rule-row' }, + React.createElement('div', { className: 'h2-price-tier-rule-field' }, + React.createElement('label', null, '加氢总量 ≥(kg)'), + React.createElement(Input, { + value: rule.thresholdKg, + placeholder: idx === 0 ? '0' : '如 300', + style: { borderRadius: 8 }, + onChange: function (e) { updateTierRuleField(rule.id, 'thresholdKg', e.target.value); } + }) + ), + React.createElement('div', { className: 'h2-price-tier-rule-field' }, + React.createElement('label', null, '成本价格(元/kg)'), + React.createElement(Input, { + value: rule.price, + placeholder: '两位小数', + style: { borderRadius: 8 }, + onChange: function (e) { updateTierRuleField(rule.id, 'price', e.target.value); }, + onBlur: function (e) { + var formatted = h2FormatReceiptAmountInput(e.target.value); + if (formatted) updateTierRuleField(rule.id, 'price', formatted); + } + }) + ), + React.createElement(Button, { + type: 'link', + danger: true, + disabled: (priceModal.tierRules || []).length <= 1, + onClick: function () { removeTierRuleRow(rule.id); }, + style: { marginBottom: 2 } + }, '删除') + ); + }), + React.createElement(Button, { type: 'dashed', block: true, onClick: addTierRuleRow, style: { borderRadius: 8 } }, '+ 新增条件') + ) + ), + priceType === 'tiered' + ? React.createElement(Form.Item, { + label: React.createElement('span', null, + React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), + '阶梯量重置日期 / 生效时间' + ) + }, + React.createElement('div', { className: 'h2-price-tier-timing-row' }, + React.createElement('div', null, + React.createElement('div', { className: 'h2-price-tier-rule-field' }, + React.createElement('label', null, '阶梯量重置日期'), + React.createElement(Select, { + style: { width: '100%' }, + value: priceModal.tierResetDay || 1, + options: resetDayOptions, + onChange: function (val) { + setPriceModal(function (m) { return Object.assign({}, m, { tierResetDay: val }); }); + } + }) + ) + ), + React.createElement('div', null, + React.createElement('div', { className: 'h2-price-tier-rule-field' }, + React.createElement('label', null, '生效时间'), + effectivePicker + ) + ) + ) + ) + : null + ), + priceType === 'tiered' + ? React.createElement('div', { className: 'h2-price-config-form-hint', role: 'note' }, + '保存后,所选客户在生效时间起按阶梯规则计价;每月重置日将清零周期加氢量并重新匹配阶梯单价' + ) + : null + ) + : null, + priceModal.priceFormVisible + ? React.createElement('div', { className: 'h2-price-config-table-wrap' }, + React.createElement('div', { className: 'h2-price-config-table-head' }, + React.createElement('span', { className: 'h2-price-config-table-head__title' }, '当前规则调整记录'), + React.createElement('span', { className: 'h2-price-config-table-head__count' }, '共 ' + logs.length + ' 条') + ), + React.createElement(Table, { + className: 'h2-price-config-record-table', + size: 'small', + bordered: false, + rowKey: 'id', + columns: costPriceLogColumns, + dataSource: logs, + pagination: logs.length > 5 ? { pageSize: 5, showSizeChanger: false, size: 'small', showTotal: function (t) { return '共 ' + t + ' 条'; } } : false, + locale: { emptyText: '暂无价格调整记录' }, + scroll: logs.length > 5 ? { x: 920, y: 240 } : { x: 920 } + }) + ) + : null + ); + }, [priceModal, costPriceLogColumns, effectivePriceConfigColumns, loadPriceModalFromConfig, resetPriceModalToNew, cancelPriceFormEdit, addTierRuleRow, removeTierRuleRow, updateTierRuleField]); + + var getRechargeStationOptions = useCallback(function (currentLineId) { + var usedIds = {}; + (rechargeModal.lines || []).forEach(function (line) { + if (line.id !== currentLineId && line.stationId != null) usedIds[line.stationId] = true; + }); + return listData + .filter(function (r) { return !usedIds[r.id]; }) + .map(function (r) { return { value: r.id, label: r.name || '—' }; }); + }, [listData, rechargeModal.lines]); + + var rechargeModalColumns = useMemo(function () { + return [ + { + title: '站点', + key: 'station', + width: 180, + fixed: 'left', + render: function (_, line) { + return React.createElement(Select, { + showSearch: true, + allowClear: true, + placeholder: '请选择站点', + style: { width: '100%' }, + value: line.stationId, + options: getRechargeStationOptions(line.id), + optionFilterProp: 'label', + onChange: function (v) { handleRechargeStationSelect(line.id, v); } + }); + } + }, + { + title: '当前余额', + key: 'currentBalance', + width: 108, + align: 'right', + render: function (_, line) { + if (!line.stationId) { + return React.createElement('span', { className: 'h2-recharge-readonly h2-recharge-readonly--muted' }, '—'); + } + var num = h2NumOrZero(line.currentBalance); + return React.createElement('span', { + className: 'h2-recharge-readonly', + style: { color: num < 0 ? '#dc2626' : '#059669', fontWeight: 600, fontVariantNumeric: 'tabular-nums' } + }, '¥' + (line.currentBalance || '0.00')); + } + }, + { + title: '付款金额', + key: 'payAmount', + width: 130, + render: function (_, line) { + return React.createElement(Input, { + className: 'h2-statement-receipt-amount-input', + inputMode: 'decimal', + prefix: '¥', + placeholder: '请输入金额', + style: { width: '100%', borderRadius: 8 }, + value: line.payAmount || '', + onChange: function (e) { + updateRechargeLineField(line.id, 'payAmount', h2SanitizeReceiptAmountInput(e.target.value)); + }, + onBlur: function (e) { + updateRechargeLineField(line.id, 'payAmount', h2FormatReceiptAmountInput(e.target.value)); + } + }); + } + }, + { + title: '企业全称', + key: 'companyName', + width: 160, + render: function (_, line) { + return h2RenderRechargeAutoField(line.companyName, !!line.stationId, '未关联供应商'); + } + }, + { + title: '收款银行账号', + key: 'bankAccount', + width: 150, + render: function (_, line) { + return h2RenderRechargeAutoField(line.bankAccount, !!line.stationId, '未配置账号'); + } + }, + { + title: '收款开户行全称', + key: 'bankName', + width: 160, + render: function (_, line) { + return h2RenderRechargeAutoField(line.bankName, !!line.stationId, '未配置开户行'); + } + }, + { + title: '转账用途', + key: 'transferPurpose', + width: 200, + render: function (_, line) { + return h2RenderRechargeAutoField(line.transferPurpose, !!line.stationId, '—'); + } + }, + { + title: '操作', + key: 'action', + width: 72, + fixed: 'right', + render: function (_, line) { + return React.createElement(Button, { + type: 'link', + size: 'small', + danger: true, + disabled: (rechargeModal.lines || []).length <= 1, + onClick: function () { removeRechargeLine(line.id); } + }, '删除'); + } + } + ]; + }, [rechargeModal.lines, getRechargeStationOptions, handleRechargeStationSelect, updateRechargeLineField, removeRechargeLine]); + + var listColumnsWithoutAction = useMemo(function () { + return columns.filter(function (c) { return c.key !== 'action'; }); + }, [columns]); + + var renderRechargeModalPanel = useCallback(function () { + var lines = rechargeModal.lines || []; + return React.createElement('div', { className: 'h2-recharge-panel' }, + React.createElement('p', { className: 'h2-recharge-hint' }, + '选择站点后将根据供应商信息自动带出企业全称、收款银行账号、开户行及转账用途(均为只读);仅需填写付款金额。支持添加多行,一次发起多个站点的充值付款。' + ), + React.createElement('div', { className: 'h2-recharge-toolbar' }, + React.createElement('span', { className: 'h2-recharge-toolbar__count' }, '共 ' + lines.length + ' 条付款信息'), + React.createElement(Button, { + type: 'dashed', + icon: H2_ICONS.plus, + style: { borderRadius: 8, fontWeight: 600, borderColor: '#10b981', color: '#059669' }, + onClick: addRechargeLine + }, '添加站点') + ), + React.createElement('div', { className: 'h2-recharge-table-wrap' }, + React.createElement(Table, { + className: 'h2-recharge-record-table', + size: 'small', + bordered: false, + rowKey: 'id', + columns: rechargeModalColumns, + dataSource: lines, + pagination: false, + locale: { emptyText: '请添加付款站点' }, + scroll: { x: 1180 } + }) + ) + ); + }, [rechargeModal.lines, rechargeModalColumns, addRechargeLine]); + + var renderAlertStationPanel = useCallback(function () { + var stations = alertStationModal.stations || []; + return React.createElement('div', { className: 'h2-alert-station-panel' }, + React.createElement('div', { className: 'h2-alert-station-head' }, + React.createElement('div', null, + React.createElement('div', { className: 'h2-alert-station-head__title' }, alertStationModal.title || '站点列表'), + React.createElement('div', { className: 'h2-alert-station-head__count' }, + '共 ' + stations.length + ' 个站点 · 字段与列表页一致' + ) + ), + React.createElement(Button, { + type: 'primary', + style: H2_PRIMARY_BTN_STYLE, + disabled: !stations.length, + onClick: handleBatchRechargeFromAlert + }, '一键发起充值单') + ), + React.createElement('div', { className: 'h2-alert-station-table-wrap' }, + React.createElement(Table, { + className: 'lc-list-table', + rowKey: 'id', + columns: listColumnsWithoutAction, + dataSource: stations, + pagination: stations.length > 8 ? { pageSize: 8, showSizeChanger: false, size: 'small', showTotal: function (t) { return '共 ' + t + ' 条'; } } : false, + size: 'middle', + scroll: { x: 1182 }, + locale: { emptyText: '暂无站点' } + }) + ) + ); + }, [alertStationModal, listColumnsWithoutAction, handleBatchRechargeFromAlert]); + + var usedBindAccountMap = useMemo(function () { + var excludeId = subView === 'edit' && editRecord ? editRecord.id : null; + return h2CollectUsedBindAccountMap(listData, excludeId); + }, [listData, subView, editRecord]); + + var createViewNode = subView === 'create' ? h2BuildStationCreateView({ + pageMode: 'create', + showSupplier: true, + accountEditable: true, + showReset: false, + station: createStation, + supplier: createSupplier, + supplierMode: createSupplierMode, + linkedSupplierId: createLinkedSupplierId, + supplierReadonly: createSupplierReadonly, + usedBindAccountMap: usedBindAccountMap, + updateStation: updateCreateStation, + updateSupplier: updateCreateSupplier, + syncSupplierFromStation: syncCreateSupplierFromStation, + handleSupplierModeChange: handleCreateSupplierModeChange, + handleLinkSupplierChange: handleCreateLinkSupplierChange, + handleSupplierCityChange: handleCreateSupplierCityChange, + onCancel: handleCreateCancel, + onSubmit: handleCreateSubmit, + submitting: createSubmitting, + submitLabel: '提交创建' + }) : null; + + var editViewNode = subView === 'edit' ? h2BuildStationCreateView({ + pageMode: 'edit', + showSupplier: true, + accountEditable: isAdminUser, + showReset: false, + station: editStation, + supplier: editSupplier, + supplierMode: editSupplierMode, + linkedSupplierId: editLinkedSupplierId, + supplierReadonly: editSupplierMode === 'link' && !!editLinkedSupplierId, + usedBindAccountMap: usedBindAccountMap, + updateStation: updateEditStation, + updateSupplier: updateEditSupplier, + syncSupplierFromStation: syncEditSupplierFromStation, + handleSupplierModeChange: handleEditSupplierModeChange, + handleLinkSupplierChange: handleEditLinkSupplierChange, + handleSupplierCityChange: handleEditSupplierCityChange, + onCancel: handleEditCancel, + onSubmit: handleEditSubmit, + submitting: editSubmitting, + submitLabel: '保存修改' + }) : null; + + var emptyNode = React.createElement('div', { style: { padding: '40px 0', textAlign: 'center' } }, + H2_ICONS.empty, + React.createElement('div', { style: { color: '#94a3b8', marginTop: 12 } }, '暂无符合检索条件的站点') + ); + + return React.createElement('div', { + className: 'h2-station-page lc-edit-page' + + (subView === 'create' || subView === 'edit' ? ' h2-station-page--create' : '') + }, + React.createElement('style', null, H2_PAGE_STYLE), + createViewNode, + editViewNode, + subView === 'list' ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' } }, + React.createElement('div', { style: { marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12 } }, + React.createElement(Breadcrumb, { items: [{ title: '加氢站管理' }, { title: '站点信息' }] }), + React.createElement(Space, { size: 8 }, + React.createElement(Button, { + type: 'default', + icon: H2_ICONS.doc, + style: H2_REQ_BTN_STYLE, + onClick: function () { setUserManualModalOpen(true); }, + 'aria-label': '查看使用说明书' + }, '查看使用说明书'), + React.createElement(Button, { + type: 'default', + icon: H2_ICONS.doc, + style: H2_REQ_BTN_STYLE, + onClick: function () { setRequirementModalOpen(true); }, + 'aria-label': '查看需求说明' + }, '查看需求说明') + ) + ), + + React.createElement(Card, { className: 'lc-filter-card', title: '筛选条件', bordered: false }, + React.createElement('div', { className: 'lc-filter-grid' }, + renderFilterField('加氢站名称', React.createElement(Input, { + placeholder: '请输入加氢站名称', + value: listFilters.name, + onChange: function (e) { setListFilters(function (p) { return Object.assign({}, p, { name: e.target.value }); }); }, + onPressEnter: handleListFilterQuery, + allowClear: true, + style: { width: '100%', height: 32, borderRadius: 8 } + })), + renderFilterField('是否签约', React.createElement(Select, { + placeholder: '全部', + allowClear: true, + value: listFilters.signed, + onChange: function (v) { setListFilters(function (p) { return Object.assign({}, p, { signed: v }); }); }, + options: [{ value: 'yes', label: '签约站点' }, { value: 'no', label: '普通站点' }], + style: { width: '100%' }, + dropdownStyle: { borderRadius: 8 } + })), + renderFilterField('地区(省-市)', React.createElement(Cascader, { + options: H2_REGION_CASCADER_OPTIONS, + value: listFilters.region && listFilters.region.length ? listFilters.region : undefined, + onChange: function (v) { setListFilters(function (p) { return Object.assign({}, p, { region: v || undefined }); }); }, + placeholder: '请选择省 / 市', + allowClear: true, + style: { width: '100%', borderRadius: 8 }, + changeOnSelect: true + })), + renderFilterField('营业状态', React.createElement(Select, { + placeholder: '全部', + allowClear: true, + value: listFilters.businessStatus, + onChange: function (v) { setListFilters(function (p) { return Object.assign({}, p, { businessStatus: v }); }); }, + options: H2_BUSINESS_STATUS_OPTIONS, + style: { width: '100%' }, + dropdownStyle: { borderRadius: 8 } + })) + ), + React.createElement('div', { className: 'lc-filter-actions' }, + React.createElement(Button, { onClick: handleListFilterReset, style: { borderRadius: 8 } }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleListFilterQuery, style: H2_PRIMARY_BTN_STYLE }, '查询') + ) + ), + + React.createElement('div', { className: 'lc-alert-stats-row' }, + renderAlertStatCard(H2_STATION_KPI_CARDS[0], { + active: categoryTab === 'all' && !appliedFilters.signed, + count: categoryCounts.all || 0, + onClick: function () { handleKpiCardClick('all'); }, + icon: h2KpiCardIcon('all') + }), + H2_SIGNED_FILTER_CARDS.map(function (card) { + return renderAlertStatCard(card, { + active: appliedFilters.signed === card.key, + count: card.key === 'yes' ? signedStats.signed : signedStats.unsigned, + onClick: function () { handleSignedFilterCardClick(card.key); }, + icon: h2SignedFilterIcon(card.key) + }); + }), + H2_STATION_KPI_CARDS.slice(1).map(function (card) { + var isAlertKpi = card.key === 'balanceAlert' || card.key === 'arrears'; + return renderAlertStatCard(card, { + active: isAlertKpi + ? (alertStationModal.open && alertStationModal.type === card.key) + : (categoryTab === card.key && !appliedFilters.signed), + count: categoryCounts[card.key] || 0, + onClick: function () { + if (isAlertKpi) openAlertStationModal(card.key); + else handleKpiCardClick(card.key); + }, + icon: h2KpiIcon(card.key) + }); + }) + ), + + React.createElement('div', { className: 'lc-table-section' }, + React.createElement('div', { className: 'lc-table-toolbar' }, + React.createElement('div', { className: 'lc-table-toolbar-actions' }, + React.createElement(Button, { + type: 'default', + icon: H2_ICONS.none, + style: { borderRadius: 8, fontWeight: 600, borderColor: '#f59e0b', color: '#d97706' }, + onClick: openGlobalBalanceAlertSetting, + 'aria-label': '全站余额提醒设置' + }, '全站余额提醒设置'), + React.createElement(Button, { + type: 'default', + icon: H2_ICONS.upload, + style: { borderRadius: 8, fontWeight: 600, borderColor: '#10b981', color: '#059669' }, + onClick: function () { setImportModalOpen(true); }, + 'aria-label': '批量导入加氢站点' + }, '批量导入'), + React.createElement(Button, { + type: 'default', + icon: H2_ICONS.doc, + style: { borderRadius: 8, fontWeight: 600, borderColor: '#10b981', color: '#059669' }, + onClick: function () { openRechargeModal(); }, + 'aria-label': '发起充值单' + }, '发起充值单'), + React.createElement(Button, { + type: 'primary', + style: H2_PRIMARY_BTN_STYLE, + onClick: openCreate, + 'aria-label': '新建加氢站点' + }, '新建站点') + ) + ), + React.createElement('div', { className: 'lc-table-card' }, + React.createElement(Table, { + className: 'lc-list-table', + rowKey: 'id', + columns: columns, + dataSource: displayList, + pagination: tablePagination, + size: 'middle', + scroll: { x: 1290 }, + locale: { emptyText: emptyNode }, + onChange: handleListTableChange + }) + ) + ) + ) : null, + + React.createElement(Modal, { + title: '确认删除', + open: deleteModal.open, + centered: true, + onCancel: function () { setDeleteModal({ open: false, record: null }); }, + onOk: function () { + if (deleteModal.record) { + setListData(function (prev) { return prev.filter(function (r) { return r.id !== deleteModal.record.id; }); }); + message.success('已删除'); + } + setDeleteModal({ open: false, record: null }); + }, + okButtonProps: { danger: true }, + okText: '删除', + cancelText: '取消' + }, '确定删除站点「' + ((deleteModal.record && deleteModal.record.name) || '') + '」吗?此操作不可撤销。'), + + React.createElement(Modal, { + className: 'h2-prd-modal h2-user-manual-modal', + title: React.createElement('span', { style: { fontWeight: 700 } }, '使用说明书 · 站点信息'), + open: userManualModalOpen, + onCancel: function () { setUserManualModalOpen(false); }, + width: 900, + centered: true, + destroyOnClose: true, + styles: { body: { maxHeight: '72vh', overflow: 'auto', paddingTop: 8, paddingBottom: 16 } }, + footer: React.createElement(Button, { onClick: function () { setUserManualModalOpen(false); }, style: { borderRadius: 8 } }, '关闭') + }, renderH2StationUserManualDocPanel()), + + React.createElement(Modal, { + className: 'h2-prd-modal', + title: React.createElement('span', { style: { fontWeight: 700 } }, '需求说明 · 站点信息'), + open: requirementModalOpen, + onCancel: function () { setRequirementModalOpen(false); }, + width: 840, + centered: true, + destroyOnClose: true, + styles: { body: { maxHeight: '72vh', overflow: 'auto', paddingTop: 8, paddingBottom: 16 } }, + footer: React.createElement(Button, { onClick: function () { setRequirementModalOpen(false); }, style: { borderRadius: 8 } }, '关闭') + }, renderH2StationRequirementDocPanel()), + + React.createElement(Modal, { + title: '批量导入加氢站点', + open: importModalOpen, + onCancel: closeImportModal, + width: 560, + centered: true, + destroyOnClose: true, + okText: '确认导入', + cancelText: '取消', + okButtonProps: { + disabled: !importPreview || !importPreview.valid.length, + style: H2_PRIMARY_BTN_STYLE + }, + onOk: handleConfirmImport + }, + React.createElement('div', { className: 'h2-import-template-bar' }, + React.createElement('div', { className: 'h2-import-template-bar-text' }, + React.createElement('div', { style: { fontWeight: 700, marginBottom: 4, color: '#0f172a' } }, '第一步:下载 CSV 导入模板'), + '模板含加氢站名称、省、市、详细地址、是否签约、签约起止时间、营业状态、营业时间、联系人、联系电话;与新建表单字段一致。' + ), + React.createElement(Button, { + type: 'primary', + ghost: true, + style: { borderColor: '#10b981', color: '#059669', fontWeight: 600, flexShrink: 0, borderRadius: 8 }, + onClick: downloadImportTemplate + }, '下载模板') + ), + React.createElement(Alert, { + type: 'info', + showIcon: true, + style: { marginBottom: 14, borderRadius: 10 }, + message: '第二步:填写模板后上传文件', + description: '支持 .csv(推荐)、.xlsx、.xls;原型阶段 Excel 请另存为 CSV UTF-8 后上传。上传后系统将校验并预览可导入条数。' + }), + React.createElement(Upload.Dragger, { + accept: '.csv,.xlsx,.xls', + multiple: false, + maxCount: 1, + fileList: importFileList, + beforeUpload: function (file) { + parseImportFile(file); + return false; + }, + onRemove: function () { + setImportFileList([]); + setImportPreview(null); + return true; + } + }, + React.createElement('p', { style: { margin: '8px 0 4px', fontWeight: 600, color: '#334155' } }, '点击或拖拽文件到此处上传'), + React.createElement('p', { style: { margin: 0, fontSize: 12, color: '#94a3b8' } }, '单次上传一个文件') + ), + importPreview ? React.createElement('div', { className: 'h2-import-preview' }, + React.createElement('div', { style: { fontWeight: 700, marginBottom: 6 } }, '解析结果 · ' + (importPreview.fileName || '')), + React.createElement('div', null, + React.createElement('span', { style: { color: '#059669', fontWeight: 600 } }, '可导入 ' + importPreview.valid.length + ' 条'), + importPreview.errors.length ? React.createElement('span', { style: { marginLeft: 12, color: '#dc2626', fontWeight: 600 } }, '失败 ' + importPreview.errors.length + ' 条') : null + ), + importPreview.errors.length ? React.createElement('ul', { className: 'h2-import-error-list' }, + importPreview.errors.slice(0, 8).map(function (err, ei) { + return React.createElement('li', { key: ei }, + '第 ' + err.lineNo + ' 行「' + err.name + '」:' + err.reasons.join(';') + ); + }).concat( + importPreview.errors.length > 8 + ? [React.createElement('li', { key: 'more' }, '… 另有 ' + (importPreview.errors.length - 8) + ' 条错误未展示')] + : [] + ) + ) : null + ) : null + ), + + React.createElement(Modal, { + className: 'h2-business-status-modal', + title: '营业状态', + open: businessModal.open, + onCancel: closeBusinessModal, + footer: React.createElement(Space, null, + React.createElement(Button, { onClick: closeBusinessModal, style: { borderRadius: 8 } }, '取消'), + React.createElement(Button, { type: 'primary', onClick: handleSaveBusinessSetting, style: H2_PRIMARY_BTN_STYLE }, '保存') + ), + centered: true, + width: 760, + destroyOnClose: true, + styles: { body: { maxHeight: '78vh', overflow: 'auto' } } + }, + renderBusinessStatusPanel() + ), + + React.createElement(Modal, { + className: 'h2-refuel-drill-modal', + title: '加氢记录', + open: refuelModal.open, + onCancel: closeRefuelModal, + footer: React.createElement(Button, { type: 'primary', onClick: closeRefuelModal, style: H2_PRIMARY_BTN_STYLE }, '知道了'), + width: 980, + centered: true, + destroyOnClose: true, + styles: { body: { maxHeight: '78vh', overflow: 'auto' } } + }, + renderRefuelDrillPanel() + ), + + React.createElement(Modal, { + className: 'h2-station-view-modal', + title: '查看加氢站点', + open: viewModal.open, + onCancel: closeViewModal, + footer: React.createElement(Button, { type: 'primary', onClick: closeViewModal, style: H2_PRIMARY_BTN_STYLE }, '知道了'), + width: 920, + centered: true, + destroyOnClose: true, + styles: { body: { maxHeight: '78vh', overflow: 'auto' } } + }, + renderStationViewPanel() + ), + + React.createElement(Modal, { + className: 'h2-file-preview-modal', + title: filePreviewModal.name || '附件预览', + open: filePreviewModal.open, + onCancel: closeFilePreviewModal, + footer: React.createElement(Button, { type: 'primary', onClick: closeFilePreviewModal, style: H2_PRIMARY_BTN_STYLE }, '关闭'), + width: filePreviewModal.type === 'pdf' ? 920 : 720, + centered: true, + destroyOnClose: true + }, + React.createElement('div', { className: 'h2-file-preview-body' }, + filePreviewModal.type === 'pdf' && filePreviewModal.url + ? React.createElement('iframe', { title: filePreviewModal.name || '附件预览', src: filePreviewModal.url }) + : React.createElement('img', { + src: filePreviewModal.url, + alt: filePreviewModal.name || '附件预览' + }) + ) + ), + + React.createElement(Modal, { + className: 'h2-balance-drill-modal', + title: '余额变更明细', + open: prepaidBalanceDrill.open, + onCancel: closePrepaidBalanceDrill, + footer: React.createElement(Button, { type: 'primary', onClick: closePrepaidBalanceDrill, style: H2_PRIMARY_BTN_STYLE }, '知道了'), + width: 920, + centered: true, + destroyOnClose: true, + styles: { body: { maxHeight: '78vh', overflow: 'auto' } } + }, + renderPrepaidBalanceDrillPanel() + ), + + React.createElement(Modal, { + className: 'h2-statement-modal', + title: '生成对账单', + open: statementModal.open, + onCancel: closeStatementModal, + footer: React.createElement(Space, null, + React.createElement(Button, { onClick: closeStatementModal, style: { borderRadius: 8 } }, '取消'), + statementModal.phase === 'draft' + ? React.createElement(Button, { + type: 'primary', + onClick: handleSubmitStatement, + style: H2_PRIMARY_BTN_STYLE + }, '提交对账单') + : React.createElement(Button, { + type: 'primary', + onClick: handleGenerateStatement, + style: H2_PRIMARY_BTN_STYLE + }, '生成对账单') + ), + centered: true, + width: 820, + destroyOnClose: true, + styles: { body: { maxHeight: '78vh', overflow: 'auto' } } + }, + renderStatementPanel() + ), + + React.createElement(Modal, { + className: 'h2-statement-history-modal', + title: '查看对账记录', + open: statementHistoryModal.open, + onCancel: closeStatementHistoryModal, + footer: React.createElement(Button, { type: 'primary', onClick: closeStatementHistoryModal, style: H2_PRIMARY_BTN_STYLE }, '知道了'), + centered: true, + width: 1080, + destroyOnClose: true, + styles: { body: { maxHeight: '78vh', overflow: 'auto' } } + }, + renderStatementHistoryPanel() + ), + + React.createElement(Modal, { + className: 'h2-statement-modal', + title: '对账明细', + open: statementDetailModal.open, + onCancel: closeStatementDetailModal, + footer: React.createElement(Button, { type: 'primary', onClick: closeStatementDetailModal, style: H2_PRIMARY_BTN_STYLE }, '知道了'), + centered: true, + width: 820, + destroyOnClose: true, + styles: { body: { maxHeight: '78vh', overflow: 'auto' } } + }, + renderStatementDetailPanel() + ), + + React.createElement(Modal, { + className: 'h2-price-config-modal', + title: '价格配置', + open: priceModal.open, + onCancel: closePriceModal, + footer: priceModal.priceFormVisible + ? React.createElement(Space, null, + React.createElement(Button, { onClick: cancelPriceFormEdit, style: { borderRadius: 8 } }, '取消'), + React.createElement(Button, { type: 'primary', onClick: handleSavePriceConfig, style: H2_PRIMARY_BTN_STYLE }, '保存') + ) + : React.createElement(Button, { onClick: closePriceModal, style: H2_PRIMARY_BTN_STYLE }, '关闭'), + centered: true, + width: 920, + destroyOnClose: true, + styles: { body: { maxHeight: '78vh', overflow: 'auto' } } + }, + renderPriceConfigPanel() + ), + + React.createElement(Modal, { + title: '余额提醒设置', + open: balanceAlertModal.open, + onCancel: closeBalanceAlertModal, + footer: React.createElement(Space, null, + React.createElement(Button, { onClick: closeBalanceAlertModal, style: { borderRadius: 8 } }, '取消'), + React.createElement(Button, { type: 'primary', onClick: handleSaveBalanceAlert, style: H2_PRIMARY_BTN_STYLE }, '保存') + ), + centered: true, + width: 480, + destroyOnClose: true + }, + balanceAlertModal.record + ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 14 } }, + React.createElement('div', { + style: { + padding: '12px 14px', + borderRadius: 10, + background: '#f8fafc', + border: '1px solid #e2e8f0', + fontSize: 13, + color: '#475569', + lineHeight: 1.55 + } + }, + React.createElement('div', { style: { fontWeight: 700, color: '#0f172a', marginBottom: 4 } }, balanceAlertModal.record.name || '—'), + '当预付余额低于所设金额时,站点将列入「预付余额预警站点」。当前预付余额:', + React.createElement('strong', { + style: { + color: h2NumOrZero(balanceAlertModal.record.prepaidBalance) < 0 ? '#dc2626' : '#059669', + fontVariantNumeric: 'tabular-nums' + } + }, h2FormatYuanNum(balanceAlertModal.record.prepaidBalance)), + ' 元' + ), + React.createElement(Form, { layout: 'vertical', requiredMark: false }, + React.createElement(Form.Item, { + label: React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), '提醒金额(元)') + }, + React.createElement(Input, { + className: 'h2-statement-receipt-amount-input', + inputMode: 'decimal', + prefix: '¥', + style: { width: '100%', borderRadius: 8 }, + placeholder: '请输入提醒金额', + value: balanceAlertModal.threshold || '', + onChange: function (e) { + var next = h2SanitizeReceiptAmountInput(e.target.value); + setBalanceAlertModal(function (m) { return Object.assign({}, m, { threshold: next }); }); + }, + onBlur: function (e) { + var formatted = h2FormatReceiptAmountInput(e.target.value); + setBalanceAlertModal(function (m) { return Object.assign({}, m, { threshold: formatted }); }); + } + }) + ) + ) + ) + : null + ), + + React.createElement(Modal, { + title: '全站余额提醒设置', + open: globalBalanceAlertModal.open, + onCancel: closeGlobalBalanceAlertModal, + footer: React.createElement(Space, null, + React.createElement(Button, { onClick: closeGlobalBalanceAlertModal, style: { borderRadius: 8 } }, '取消'), + React.createElement(Button, { type: 'primary', onClick: handleSaveGlobalBalanceAlert, style: H2_PRIMARY_BTN_STYLE }, '保存') + ), + centered: true, + width: 480, + destroyOnClose: true + }, + React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 14 } }, + React.createElement('div', { + style: { + padding: '12px 14px', + borderRadius: 10, + background: '#f8fafc', + border: '1px solid #e2e8f0', + fontSize: 13, + color: '#475569', + lineHeight: 1.55 + } + }, + React.createElement('div', { style: { fontWeight: 700, color: '#0f172a', marginBottom: 4 } }, '全部加氢站'), + '将对全部 ', + React.createElement('strong', { style: { color: '#0f172a', fontVariantNumeric: 'tabular-nums' } }, listData.length), + ' 个加氢站统一设置余额提醒阈值。当某站点预付余额低于所设金额时,该站点将列入「预付余额预警站点」。' + ), + React.createElement(Form, { layout: 'vertical', requiredMark: false }, + React.createElement(Form.Item, { + label: React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), '提醒金额(元)') + }, + React.createElement(Input, { + className: 'h2-statement-receipt-amount-input', + inputMode: 'decimal', + prefix: '¥', + style: { width: '100%', borderRadius: 8 }, + placeholder: '请输入提醒金额', + value: globalBalanceAlertModal.threshold || '', + onChange: function (e) { + var next = h2SanitizeReceiptAmountInput(e.target.value); + setGlobalBalanceAlertModal(function (m) { return Object.assign({}, m, { threshold: next }); }); + }, + onBlur: function (e) { + var formatted = h2FormatReceiptAmountInput(e.target.value); + setGlobalBalanceAlertModal(function (m) { return Object.assign({}, m, { threshold: formatted }); }); + } + }) + ) + ) + ) + ), + + React.createElement(Modal, { + className: 'h2-recharge-modal', + title: '发起充值单', + open: rechargeModal.open, + onCancel: closeRechargeModal, + footer: React.createElement(Space, null, + React.createElement(Button, { onClick: closeRechargeModal, style: { borderRadius: 8 } }, '取消'), + React.createElement(Button, { type: 'primary', onClick: handleSubmitRecharge, style: H2_PRIMARY_BTN_STYLE }, '发起付款流程') + ), + centered: true, + width: 1120, + destroyOnClose: true, + styles: { body: { maxHeight: '78vh', overflow: 'auto' } } + }, + renderRechargeModalPanel() + ), + + React.createElement(Modal, { + className: 'h2-alert-station-modal', + title: alertStationModal.title || '站点列表', + open: alertStationModal.open, + onCancel: closeAlertStationModal, + footer: React.createElement(Space, null, + React.createElement(Button, { onClick: closeAlertStationModal, style: { borderRadius: 8 } }, '关闭'), + React.createElement(Button, { + type: 'primary', + disabled: !(alertStationModal.stations || []).length, + onClick: handleBatchRechargeFromAlert, + style: H2_PRIMARY_BTN_STYLE + }, '一键发起充值单') + ), + centered: true, + width: 1200, + destroyOnClose: true, + styles: { body: { maxHeight: '78vh', overflow: 'auto' } } + }, + renderAlertStationPanel() + ) + ); +}; + +if (typeof window !== 'undefined') window.Component = Component; + +export default Component; diff --git a/src/prototypes/oneos-web-help-center/index.tsx b/src/prototypes/oneos-web-help-center/index.tsx new file mode 100644 index 0000000..075f8d3 --- /dev/null +++ b/src/prototypes/oneos-web-help-center/index.tsx @@ -0,0 +1,21 @@ +/** + * @name 帮助中心 + * 自 ONE-OS web端 原稿复刻(1 个页面) + */ +import React from 'react'; +import Page1 from './pages/01-功能说明书.jsx'; +import { OneosWebLegacyShell } from '../../common/oneosWebLegacy/OneosWebLegacyShell'; + +const pages = [ + { id: 'u5e2eu52a9u4e2du5fc3-u529fu80fdu8bf4u660eu4e66', title: '功能说明书', component: Page1 }, +]; + +export default function OneosWebHelpCenter() { + return ( + + ); +} diff --git a/src/prototypes/oneos-web-help-center/pages/01-功能说明书.jsx b/src/prototypes/oneos-web-help-center/pages/01-功能说明书.jsx new file mode 100644 index 0000000..0130600 --- /dev/null +++ b/src/prototypes/oneos-web-help-center/pages/01-功能说明书.jsx @@ -0,0 +1,234 @@ +// 【重要】必须使用 const Component 作为组件变量名 - Axhub 产品原型 +// 帮助中心 - 功能说明书 + +const Component = function () { + var antd = window.antd; + var Layout = antd.Layout; + var Menu = antd.Menu; + var Typography = antd.Typography; + var Button = antd.Button; + var Space = antd.Space; + var Divider = antd.Divider; + + var Title = Typography.Title; + var Paragraph = Typography.Paragraph; + var Text = Typography.Text; + + var Sider = Layout.Sider; + var Content = Layout.Content; + + var React = window.React; + var useState = React.useState; + var useMemo = React.useMemo; + + // 菜单数据源(依据系统最新结构) + var menuItems = [ + { + key: '1', + label: '工作台' + }, + { + key: '2', + label: '业务管理组', + children: [ + { key: '2-1', label: '客户管理' }, + { key: '2-2', label: '合同创建' }, + { key: '2-3', label: '提车应收款' }, + { key: '2-4', label: '交还车任务' }, + { key: '2-5', label: '租赁账单' }, + { key: '2-6', label: '还车应结款' }, + { key: '2-7', label: '替换车' }, + { key: '2-8', label: '调拨' } + ] + }, + { + key: '3', + label: '运维服务组', + children: [ + { key: '3-1', label: '车辆管理' }, + { key: '3-2', label: '备件库管理' }, + { key: '3-3', label: '备车' }, + { key: '3-4', label: '交车' }, + { key: '3-5', label: '还车' }, + { key: '3-6', label: '异动' } + ] + }, + { + key: '4', + label: '业务管理组-能源部', + children: [ + { key: '4-1', label: '加氢记录' }, + { key: '4-2', label: 'ETC记录' }, + { key: '4-3', label: '充电记录' }, + { key: '4-4', label: '能源账单' }, + { key: '4-5', label: '能源账户' }, + { key: '4-6', label: '充值管理' } + ] + }, + { + key: '5', + label: '安全部', + children: [ + { key: '5-1', label: '事故管理' }, + { key: '5-2', label: '违章管理' }, + { key: '5-3', label: '司机管理' }, + { key: '5-4', label: '安全培训资料' }, + { key: '5-5', label: '安全培训记录' } + ] + }, + { + key: '6', + label: '审批中心' + } + ]; + + // 扁平化数据用于获取上下文和翻页 + var flattenMenu = useMemo(function () { + var list = []; + var traverse = function (items) { + items.forEach(function (item) { + if (!item.children) { + list.push(item); + } else { + list.push(item); + traverse(item.children); + } + }); + }; + traverse(menuItems); + return list; + }, []); + + var ms1 = useState('1'); + var selectedKey = ms1[0]; + var setSelectedKey = ms1[1]; + + var currentIndex = flattenMenu.findIndex(function (item) { + return item.key === selectedKey; + }); + var currentItem = flattenMenu[currentIndex]; + var prevItem = currentIndex > 0 ? flattenMenu[currentIndex - 1] : null; + var nextItem = currentIndex < flattenMenu.length - 1 ? flattenMenu[currentIndex + 1] : null; + + var handleMenuClick = function (e) { + setSelectedKey(e.key); + }; + + // 渲染正文内容 + var renderContent = function () { + return React.createElement( + Typography, + null, + React.createElement(Title, { level: 2, className: 'acro-text-xxl' }, currentItem ? currentItem.label : '内容建设中'), + React.createElement( + Paragraph, + { className: 'acro-text-base', style: { color: 'rgba(0,0,0,0.45)' } }, + '该章节(' + (currentItem ? currentItem.label : '') + ')的功能说明正在建设中,您可以随时在这里修改补充...' + ) + ); + }; + + var styles = { + layout: { + minHeight: '100vh', + backgroundColor: '#fff', + }, + sider: { + backgroundColor: '#fafafa', + borderRight: '1px solid #f0f0f0', + overflowY: 'auto', + height: '100vh', + position: 'fixed', + left: 0, + top: 0, + bottom: 0, + }, + siderHeader: { + padding: '24px 20px 12px', + fontSize: 18, + fontWeight: 600, + color: 'rgba(0,0,0,0.85)', + borderBottom: '1px solid #f0f0f0' + }, + contentWrapper: { + marginLeft: 280, + padding: '40px 60px', + maxWidth: 1000, + display: 'flex', + flexDirection: 'column', + minHeight: '100vh' + }, + contentInner: { + flex: 1, + marginBottom: 40 + }, + footerNav: { + display: 'flex', + justifyContent: 'space-between', + borderTop: '1px solid #f0f0f0', + paddingTop: 24, + marginTop: 'auto' + } + }; + + return React.createElement( + Layout, + { style: styles.layout }, + React.createElement( + Sider, + { width: 280, style: styles.sider, theme: 'light' }, + React.createElement('div', { style: styles.siderHeader }, '《 功能说明书 》'), + React.createElement(Menu, { + mode: 'inline', + selectedKeys: [selectedKey], + defaultOpenKeys: ['2', '3', '4', '5'], + style: { borderRight: 0, padding: '12px 0' }, + items: menuItems, + onClick: handleMenuClick + }) + ), + React.createElement( + Layout, + { style: { backgroundColor: '#fff' } }, + React.createElement( + Content, + { style: styles.contentWrapper }, + React.createElement( + 'div', + { style: styles.contentInner }, + renderContent() + ), + React.createElement( + 'div', + { style: styles.footerNav }, + prevItem + ? React.createElement( + Button, + { + type: 'link', + size: 'large', + onClick: function () { setSelectedKey(prevItem.key); }, + style: { padding: 0 } + }, + '← 上一页:' + prevItem.label + ) + : React.createElement('div', null), + nextItem + ? React.createElement( + Button, + { + type: 'link', + size: 'large', + onClick: function () { setSelectedKey(nextItem.key); }, + style: { padding: 0 } + }, + '下一页:' + nextItem.label + ' →' + ) + : React.createElement('div', null) + ) + ) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-lease-contract/index.tsx b/src/prototypes/oneos-web-lease-contract/index.tsx new file mode 100644 index 0000000..4d240f1 --- /dev/null +++ b/src/prototypes/oneos-web-lease-contract/index.tsx @@ -0,0 +1,35 @@ +/** + * @name 车辆租赁合同 + * 自 ONE-OS web端 原稿复刻(8 个页面) + */ +import React from 'react'; +import Page1 from './pages/01-车辆租赁合同-变更为三方合同.jsx'; +import Page2 from './pages/02-车辆租赁合同-查看.jsx'; +import Page3 from './pages/03-车辆租赁合同-附加费用.jsx'; +import Page4 from './pages/04-车辆租赁合同-新增.jsx'; +import Page5 from './pages/05-车辆租赁合同-新增车辆.jsx'; +import Page6 from './pages/06-车辆租赁合同-续签合同.jsx'; +import Page7 from './pages/07-车辆租赁合同-转正式合同.jsx'; +import Page8 from './pages/08-车辆租赁合同.jsx'; +import { OneosWebLegacyShell } from '../../common/oneosWebLegacy/OneosWebLegacyShell'; + +const pages = [ + { id: 'u8f66u8f86u79dfu8d41u5408u540c-u8f66u8f86u79dfu8', title: '车辆租赁合同-变更为三方合同', component: Page1 }, + { id: 'u8f66u8f86u79dfu8d41u5408u540c-u8f66u8f86u79dfu8-2', title: '车辆租赁合同-查看', component: Page2 }, + { id: 'u8f66u8f86u79dfu8d41u5408u540c-u8f66u8f86u79dfu8-3', title: '车辆租赁合同-附加费用', component: Page3 }, + { id: 'u8f66u8f86u79dfu8d41u5408u540c-u8f66u8f86u79dfu8-4', title: '车辆租赁合同-新增', component: Page4 }, + { id: 'u8f66u8f86u79dfu8d41u5408u540c-u8f66u8f86u79dfu8-5', title: '车辆租赁合同-新增车辆', component: Page5 }, + { id: 'u8f66u8f86u79dfu8d41u5408u540c-u8f66u8f86u79dfu8-6', title: '车辆租赁合同-续签合同', component: Page6 }, + { id: 'u8f66u8f86u79dfu8d41u5408u540c-u8f66u8f86u79dfu8-7', title: '车辆租赁合同-转正式合同', component: Page7 }, + { id: 'u8f66u8f86u79dfu8d41u5408u540c-u8f66u8f86u79dfu8-8', title: '车辆租赁合同', component: Page8 }, +]; + +export default function OneosWebLeaseContract() { + return ( + + ); +} diff --git a/src/prototypes/oneos-web-lease-contract/pages/01-车辆租赁合同-变更为三方合同.jsx b/src/prototypes/oneos-web-lease-contract/pages/01-车辆租赁合同-变更为三方合同.jsx new file mode 100644 index 0000000..b35c134 --- /dev/null +++ b/src/prototypes/oneos-web-lease-contract/pages/01-车辆租赁合同-变更为三方合同.jsx @@ -0,0 +1,461 @@ +// 【重要】必须使用 const Component 作为组件变量名 - Axhub 产品原型 +// 车辆租赁合同 - 变更为三方合同模块(从原合同反写,增加丙方客户选择,IE11+ 兼容,采用 antd) + +const Component = function() { + var antd = window.antd; + var Input = antd.Input; + var Select = antd.Select; + var Button = antd.Button; + var message = antd.message; + var Option = Select.Option; + var FormItem = (function() { + return function FormItemComp(props) { + var colStyle = props.fullWidth ? { flex: '0 0 100%', marginBottom: 8 } : { flex: '0 0 33.33%', minWidth: 200, paddingRight: 16, marginBottom: 8 }; + return React.createElement('div', { style: colStyle }, + React.createElement('label', { style: { display: 'block', marginBottom: 6, color: '#333' } }, props.required ? React.createElement('span', { style: { color: '#ff4d4f', marginRight: 4 } }, '*') : null, props.label), + props.children, + props.error ? React.createElement('div', { style: { color: '#ff4d4f', fontSize: 12, marginTop: 4 } }, props.error) : null + ); + }; + })(); + + var customerList = [ + { id: '1', name: '嘉兴某某物流有限公司', creditCode: '91330400MA2XXXXX1', address: '浙江省嘉兴市南湖区科技大道1号', contact: '张三', phone: '13800138001', email: 'zhangsan@example.com', companyName: '嘉兴某某物流有限公司', companyPhone: '0571-88888888', mailingAddress: '浙江省嘉兴市南湖区科技大道1号', bank: '中国工商银行嘉兴分行', bankAccount: '6222021234567890123', taxId: '91330400MA2XXXXX1' }, + { id: '2', name: '上海某某运输公司', creditCode: '91310000MA2XXXXX2', address: '上海市浦东新区张江高科技园区', contact: '李四', phone: '13800138002', email: 'lisi@example.com', companyName: '上海某某运输公司', companyPhone: '021-66666666', mailingAddress: '上海市浦东新区张江高科技园区', bank: '中国建设银行上海分行', bankAccount: '6217001234567890123', taxId: '91310000MA2XXXXX2' }, + { id: '3', name: '杭州某某供应链有限公司', creditCode: '91330100MA2YYYYY2', address: '浙江省杭州市余杭区未来科技城', contact: '李四', phone: '13800138002', email: 'lisi@example.com', companyName: '杭州某某供应链有限公司', companyPhone: '0571-99999999', mailingAddress: '浙江省杭州市余杭区未来科技城', bank: '中国农业银行杭州分行', bankAccount: '6228481234567890123', taxId: '91330100MA2YYYYY2' } + ]; + var deptList = [{ id: 'YW1', name: '业务1部', owners: ['张经理', '李专员', '王专员'] }, { id: 'YW2', name: '业务2部', owners: ['赵经理', '钱专员'] }, { id: 'YW3', name: '业务3部', owners: ['孙经理', '周专员'] }]; + var orgList = ['嘉兴羚牛', '上海羚牛', '广东羚牛']; + var regionList = [ + { province: '浙江省', cities: ['杭州市', '宁波市', '嘉兴市', '湖州市'] }, + { province: '上海市', cities: ['上海市'] }, + { province: '广东省', cities: ['广州市', '深圳市', '东莞市'] } + ]; + var feeTemplates = ['标准费用模板A', '标准费用模板B', '定制费用模板C']; + var feeTemplateCertFees = [{ project: '补办行驶证', standard: '50元/次', serviceFee: '20' }, { project: '补办驾驶证', standard: '30元/次', serviceFee: '10' }, { project: '补办牌照', standard: '100元/次', serviceFee: '50' }]; + var feeTemplatePenaltyFees = [{ project: '提前退车违约金', standard: '月租金×1', serviceFee: '0' }, { project: '违章处理违约金', standard: '按实际发生', serviceFee: '50' }]; + var feeTemplateConsumables = [{ category: '轮胎', part: '前轮', partName: '轮胎A型', qty: 1, feeDetail: '500.00' }, { category: '易损件', part: '雨刮', partName: '雨刮片', qty: 2, feeDetail: '80.00' }]; + var feeTemplateOtherFees = [{ project: '上门送车费', standard: '100元/次', serviceFee: '50' }, { project: '上门收车费', standard: '100元/次', serviceFee: '50' }, { project: '清洗费', standard: '80元/次', serviceFee: '30' }]; + + var originalContractCode = 'JXZL20260216YW101235A'; + var prevContractSample = { + customerId: '1', + businessDept: 'YW1', + businessOwner: '张经理', + projectName: '嘉兴氢能运输项目', + contractType: '正式合同', + effectiveDate: '2026-02-16', + paymentMethod: '预付', + endDate: '2027-02-16', + paymentPeriod: '1', + signingCompany: '嘉兴羚牛', + deliveryProvince: '浙江省', + deliveryCity: '嘉兴市', + deliveryLocation: '嘉兴市南湖区科技大道1号', + contractApprovalType: '标准合同审批', + remarks: '', + authorizedList: [{ name: '张三', phone: '13800138001', idCard: '330102199001011234' }], + rentalOrders: [ + { brand: '品牌A', model: '型号A1', plateNo: '浙A10001', vin: 'L1234567890ABCDEF', monthRent: '8000', serviceFee: '500', deposit: '10000', remark: '' }, + { brand: '品牌A', model: '型号A2', plateNo: '浙B20002', vin: 'L2234567890ABCDEF', monthRent: '8000', serviceFee: '500', deposit: '10000', remark: '' } + ], + hydrogenBearer: '客户', + hydrogenPaymentMethod: '预付', + hydrogenPrepay: '5000', + returnHydrogenPrice: '80', + feeTemplate: '标准费用模板A', + billingMethod: '按自然月结算' + }; + var prevCustomer = customerList.find(function(c) { return c.id === prevContractSample.customerId; }) || null; + + var selectedCustomer = React.useState(prevCustomer)[0]; + var setSelectedCustomer = React.useState(prevCustomer)[1]; + var cs4 = React.useState(prevContractSample.businessDept); + var businessDept = cs4[0]; + var setBusinessDept = cs4[1]; + var cs5 = React.useState(prevContractSample.businessOwner); + var businessOwner = cs5[0]; + + var thirdPartyCustomer = React.useState(null)[0]; + var setThirdPartyCustomer = React.useState(null)[1]; + var tpDeptState = React.useState(prevContractSample.businessDept); + var thirdPartyDept = tpDeptState[0]; + var setThirdPartyDept = tpDeptState[1]; + var tpOwnerState = React.useState(prevContractSample.businessOwner); + var thirdPartyOwner = tpOwnerState[0]; + var setThirdPartyOwner = tpOwnerState[1]; + var thirdPartyOwnerFocusError = React.useState('')[0]; + var setThirdPartyOwnerFocusError = React.useState('')[1]; + + var bs1 = React.useState(prevContractSample.projectName); + var projectName = bs1[0]; + var setProjectName = bs1[1]; + var bs6 = React.useState(prevContractSample.endDate); + var endDate = bs6[0]; + var setEndDate = bs6[1]; + var contractOriginalRef = React.useRef(null); + var csContractOriginal = React.useState([ + { name: '租赁合同-原件.pdf', size: '1.2 MB', uploadTime: '2026-02-16 14:30', isOriginal: true }, + { name: '租赁合同-补充协议.pdf', size: '0.6 MB', uploadTime: '2026-02-16 14:35', isOriginal: true } + ]); + var contractOriginalFiles = csContractOriginal[0]; + var setContractOriginalFiles = csContractOriginal[1]; + + var cc1State = React.useState(false); + var cc1 = cc1State[0]; + var setCc1 = cc1State[1]; + var cc1bState = React.useState(false); + var cc1b = cc1bState[0]; + var setCc1b = cc1bState[1]; + var cc2State = React.useState(false); + var cc2 = cc2State[0]; + var setCc2 = cc2State[1]; + var cc3State = React.useState(false); + var cc3 = cc3State[0]; + var setCc3 = cc3State[1]; + var cc4State = React.useState(false); + var cc4 = cc4State[0]; + var setCc4 = cc4State[1]; + var cc5State = React.useState(false); + var cc5 = cc5State[0]; + var setCc5 = cc5State[1]; + var cc6State = React.useState(false); + var cc6 = cc6State[0]; + var setCc6 = cc6State[1]; + var reqSpecState = React.useState(false); + var reqSpecOpen = reqSpecState[0]; + var setReqSpecOpen = reqSpecState[1]; + var cancelConfirmState = React.useState(false); + var cancelConfirmOpen = cancelConfirmState[0]; + var setCancelConfirmOpen = cancelConfirmState[1]; + var editedState = React.useState(false); + var edited = editedState[0]; + var setEdited = editedState[1]; + var formErrorsState = React.useState({}); + var formErrors = formErrorsState[0]; + var setFormErrors = formErrorsState[1]; + + var currentDept = deptList.find(function(d) { return d.id === businessDept; }); + var thirdPartyDeptObj = deptList.find(function(d) { return d.id === thirdPartyDept; }); + var thirdPartyOwnerOptions = thirdPartyDeptObj ? thirdPartyDeptObj.owners : []; + + var contractCodeDisplay = originalContractCode + '(变更为三方合同)'; + var mainVehicleModelsDisplay = '型号A1、型号A2'; + var deliveryRegionDisplay = prevContractSample.deliveryProvince + ' / ' + prevContractSample.deliveryCity; + var rentalTotalVehicles = prevContractSample.rentalOrders.length; + var rentalTotalRentService = prevContractSample.rentalOrders.reduce(function(s, r) { return s + (parseFloat(r.monthRent) || 0) + (parseFloat(r.serviceFee) || 0); }, 0); + var rentalTotalDeposit = prevContractSample.rentalOrders.reduce(function(s, r) { return s + (parseFloat(r.deposit) || 0); }, 0); + var rentalTotalHydrogen = prevContractSample.hydrogenPaymentMethod === '预付' && prevContractSample.hydrogenBearer === '客户' ? (parseFloat(prevContractSample.hydrogenPrepay) || 0) : 0; + + var selectThirdPartyCustomer = function(c) { + setEdited(true); + setThirdPartyCustomer(c); + }; + var handleThirdPartyOwnerFocus = function() { + if (!thirdPartyDept) setThirdPartyOwnerFocusError('请先选择业务部门'); + else setThirdPartyOwnerFocusError(''); + }; + var scrollToCard = function(id) { + var el = document.getElementById(id); + if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }; + var pad2 = function(n) { + var s = String(n); + return s.length === 1 ? ('0' + s) : s; + }; + var addContractOriginalFiles = function(fileList) { + if (!fileList || !fileList.length) return; + setEdited(true); + setContractOriginalFiles(function(prev) { + var next = (prev || []).slice(); + for (var i = 0; i < fileList.length; i++) { + var f = fileList[i]; + if (!f) continue; + var sizeDisplay = f.size >= 1024 * 1024 ? (f.size / 1024 / 1024).toFixed(1) + ' MB' : (f.size / 1024).toFixed(1) + ' KB'; + var now = window.moment ? window.moment() : new Date(); + var uploadTimeStr = window.moment ? now.format('YYYY-MM-DD HH:mm') : (now.getFullYear() + '-' + pad2(now.getMonth() + 1) + '-' + pad2(now.getDate()) + ' ' + pad2(now.getHours()) + ':' + pad2(now.getMinutes())); + next.push({ name: f.name, file: f, size: sizeDisplay, uploadTime: uploadTimeStr, isOriginal: false }); + } + return next; + }); + }; + var removeContractOriginalFile = function(index) { + var f = contractOriginalFiles && contractOriginalFiles[index]; + if (f && f.isOriginal) return; + setEdited(true); + setContractOriginalFiles(function(prev) { + var next = (prev || []).slice(); + next.splice(index, 1); + return next; + }); + }; + var openContractOriginalFile = function(index) { + var f = contractOriginalFiles && contractOriginalFiles[index]; + if (!f) return; + if (f.file) { + var url = URL.createObjectURL(f.file); + window.open(url); + return; + } + message.info('原合同反写附件(原型),暂无可预览源文件'); + }; + + var validateSubmitAndReview = function() { + var errs = {}; + if (!thirdPartyCustomer) errs.thirdPartyCustomer = '请选择丙方客户'; + if (!thirdPartyDept) errs.thirdPartyDept = '请选择丙方业务部门'; + if (!thirdPartyOwner) errs.thirdPartyOwner = '请选择丙方业务负责人'; + if (!contractOriginalFiles || contractOriginalFiles.length === 0) errs.contractOriginal = '请上传合同原件'; + setFormErrors(errs); + if (Object.keys(errs).length > 0) { + setCc1(false); setCc1b(false); setCc2(false); setCc3(false); setCc4(false); setCc5(false); setCc6(false); + var firstId = errs.contractOriginal ? 'card-contract' : 'card-third-party'; + setTimeout(function() { var el = document.getElementById(firstId); if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, 100); + return false; + } + return true; + }; + + var styles = { + page: { padding: '16px 24px 48px', backgroundColor: '#f5f5f5', minHeight: '100vh', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', fontSize: 14 }, + breadcrumb: { marginBottom: 16, color: '#666' }, + breadcrumbSep: { margin: '0 8px', color: '#999' }, + anchorWrap: { position: 'fixed', top: 80, right: 24, zIndex: 100, backgroundColor: '#fff', borderRadius: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.12)', padding: '12px 16px', minWidth: 160 }, + anchorItem: { display: 'block', padding: '6px 0', color: '#1890ff', cursor: 'pointer', border: 'none', background: 'none', width: '100%', textAlign: 'left', fontSize: 13 }, + card: { backgroundColor: '#fff', borderRadius: 8, marginBottom: 16, boxShadow: '0 1px 2px rgba(0,0,0,0.05)', overflow: 'hidden' }, + cardHeader: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '16px 20px', borderBottom: '1px solid #f0f0f0', cursor: 'pointer' }, + cardTitle: { fontSize: 16, fontWeight: 600, color: '#333' }, + cardToggle: { color: '#999', fontSize: 14 }, + cardBody: { padding: '20px 24px' }, + formRow: { display: 'flex', flexWrap: 'wrap', marginBottom: 16 }, + formCol: { flex: '0 0 33.33%', minWidth: 200, paddingRight: 16, marginBottom: 8 }, + formColFull: { flex: '0 0 100%', marginBottom: 8 }, + label: { display: 'block', marginBottom: 6, color: '#333' }, + inputDisabled: { backgroundColor: '#f5f5f5', color: '#666', cursor: 'default', border: '1px solid #d9d9d9', borderRadius: 4, padding: '8px 12px', width: '100%', fontSize: 14 }, + summaryList: { marginBottom: 16, display: 'flex', flexWrap: 'wrap', gap: 16 }, + summaryListItem: { flex: '0 0 calc(50% - 8px)', display: 'flex', alignItems: 'center', padding: '12px 16px', border: '1px solid #e8e8e8', borderRadius: 4, backgroundColor: '#fafafa', fontSize: 14, boxSizing: 'border-box' }, + summaryListLabel: { flex: '0 0 140px', color: '#666' }, + summaryListValue: { flex: 1, fontWeight: 600, color: '#333' }, + rentalTable: { width: '100%', borderCollapse: 'collapse', fontSize: 13 }, + rentalTh: { padding: '10px 8px', textAlign: 'left', borderBottom: '1px solid #e8e8e8', backgroundColor: '#fafafa', fontWeight: 600 }, + rentalTd: { padding: '8px', borderBottom: '1px solid #f0f0f0', verticalAlign: 'middle' }, + rentalInputDisabled: { backgroundColor: '#f5f5f5', color: '#666', border: '1px solid #e8e8e8', padding: '6px 10px', borderRadius: 4, width: '100%', fontSize: 13 }, + footer: { position: 'fixed', bottom: 0, left: 0, right: 0, padding: '12px 24px', backgroundColor: '#fff', borderTop: '1px solid #e8e8e8', display: 'flex', gap: 12, justifyContent: 'flex-start', zIndex: 99 }, + btn: { padding: '8px 24px', borderRadius: 4, border: '1px solid #d9d9d9', cursor: 'pointer', fontSize: 14 }, + btnPrimary: { backgroundColor: '#1890ff', color: '#fff', borderColor: '#1890ff' }, + btnDefault: { backgroundColor: '#fff', color: '#333' }, + tag: { display: 'inline-block', padding: '2px 8px', marginRight: 8, marginBottom: 4, backgroundColor: '#e6f7ff', color: '#1890ff', borderRadius: 4, fontSize: 12 }, + modalMask: { position: 'fixed', left: 0, top: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.45)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center' }, + modalBox: { backgroundColor: '#fff', borderRadius: 8, width: '90%', maxWidth: 720, maxHeight: '85vh', overflow: 'hidden', display: 'flex', flexDirection: 'column', boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }, + modalHeader: { padding: '16px 20px', borderBottom: '1px solid #f0f0f0', fontSize: 16, fontWeight: 600 }, + modalBody: { padding: 20, overflow: 'auto', flex: 1 }, + feeSectionTitle: { fontSize: 15, fontWeight: 600, color: '#333', marginTop: 20, marginBottom: 10 }, + feeSectionTitleFirst: { marginTop: 0 } + }; + + var CardBlock = function(props) { + return React.createElement('div', { id: props.id, style: styles.card }, + React.createElement('div', { style: styles.cardHeader, onClick: function() { props.setCollapsed(!props.collapsed); } }, + React.createElement('span', { style: styles.cardTitle }, props.title), + React.createElement('span', { style: styles.cardToggle }, props.collapsed ? '展开' : '收起') + ), + !props.collapsed ? React.createElement('div', { style: styles.cardBody }, props.children) : null + ); + }; + + var customerFields = React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '客户名称' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.name : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '客户统一信用代码' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.creditCode : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '客户地址' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.address : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '客户联系人' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.contact : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '客户电话' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.phone : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '客户电子邮箱' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.email : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '企业名称' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.companyName : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '企业电话' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.companyPhone : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '邮寄地址' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.mailingAddress : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '开户银行' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.bank : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '银行账号' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.bankAccount : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '纳税人识别号' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.taxId : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '业务部门' }, React.createElement(Input, { value: currentDept ? currentDept.name : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '业务负责人' }, React.createElement(Input, { value: businessOwner || '', disabled: true, style: { width: '100%' } })) + ); + + var thirdPartyOptions = customerList.filter(function(c) { return !selectedCustomer || c.id !== selectedCustomer.id; }).map(function(c) { return React.createElement(Option, { key: c.id, value: c.id }, c.name); }); + var thirdPartyCustomerFields = React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '客户名称', required: true, error: formErrors.thirdPartyCustomer }, React.createElement(Select, { placeholder: '请选择或输入搜索丙方客户(只显示已审核客户)', style: { width: '100%' }, value: thirdPartyCustomer ? thirdPartyCustomer.id : undefined, onChange: function(id) { var c = customerList.find(function(x) { return x.id === id; }); selectThirdPartyCustomer(c || null); }, showSearch: true, allowClear: true, filterOption: function(input, opt) { return opt && opt.children && String(opt.children).toLowerCase().indexOf((input || '').toLowerCase()) >= 0; }, status: formErrors.thirdPartyCustomer ? 'error' : undefined }, thirdPartyOptions)), + React.createElement(FormItem, { label: '客户统一信用代码' }, React.createElement(Input, { value: thirdPartyCustomer ? thirdPartyCustomer.creditCode : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '客户地址' }, React.createElement(Input, { value: thirdPartyCustomer ? thirdPartyCustomer.address : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '客户联系人' }, React.createElement(Input, { value: thirdPartyCustomer ? thirdPartyCustomer.contact : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '客户电话' }, React.createElement(Input, { value: thirdPartyCustomer ? thirdPartyCustomer.phone : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '客户电子邮箱' }, React.createElement(Input, { value: thirdPartyCustomer ? thirdPartyCustomer.email : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '企业名称' }, React.createElement(Input, { value: thirdPartyCustomer ? thirdPartyCustomer.companyName : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '企业电话' }, React.createElement(Input, { value: thirdPartyCustomer ? thirdPartyCustomer.companyPhone : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '邮寄地址' }, React.createElement(Input, { value: thirdPartyCustomer ? thirdPartyCustomer.mailingAddress : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '开户银行' }, React.createElement(Input, { value: thirdPartyCustomer ? thirdPartyCustomer.bank : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '银行账号' }, React.createElement(Input, { value: thirdPartyCustomer ? thirdPartyCustomer.bankAccount : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '纳税人识别号' }, React.createElement(Input, { value: thirdPartyCustomer ? thirdPartyCustomer.taxId : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '业务部门', required: true, error: formErrors.thirdPartyDept }, React.createElement(Select, { placeholder: '请选择业务部门', style: { width: '100%' }, value: thirdPartyDept || undefined, onChange: function(v) { setEdited(true); setThirdPartyDept(v || ''); setThirdPartyOwner(''); }, status: formErrors.thirdPartyDept ? 'error' : undefined }, deptList.map(function(d, i) { return React.createElement(Option, { key: i, value: d.id }, d.name); }))), + React.createElement(FormItem, { label: '业务负责人', required: true, error: formErrors.thirdPartyOwner || thirdPartyOwnerFocusError }, React.createElement(Select, { placeholder: thirdPartyDept ? '请选择' : '请先选择业务部门', style: { width: '100%' }, value: thirdPartyOwner || undefined, onChange: function(v) { setEdited(true); setThirdPartyOwner(v || ''); setThirdPartyOwnerFocusError(''); }, onFocus: handleThirdPartyOwnerFocus, onBlur: function() { setThirdPartyOwnerFocusError(''); }, disabled: !thirdPartyDept, status: (formErrors.thirdPartyOwner || thirdPartyOwnerFocusError) ? 'error' : undefined }, thirdPartyOwnerOptions.map(function(o, i) { return React.createElement(Option, { key: i, value: o }, o); }))) + ); + + var contractFormRow1 = React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '项目名称' }, React.createElement(Input, { value: projectName, disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '合同编码' }, React.createElement(Input, { value: contractCodeDisplay, disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '合同类型' }, React.createElement(Input, { value: prevContractSample.contractType, disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '生效日期' }, React.createElement(Input, { value: prevContractSample.effectiveDate, disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '付款方式' }, React.createElement(Input, { value: prevContractSample.paymentMethod, disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '主要车型' }, React.createElement('div', { style: { padding: '8px 12px', minHeight: 36, border: '1px solid #d9d9d9', borderRadius: 4, backgroundColor: '#f5f5f5', display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' } }, mainVehicleModelsDisplay ? mainVehicleModelsDisplay.split('、').map(function(m, i) { return React.createElement('span', { key: i, style: styles.tag }, m); }) : '—')), + React.createElement(FormItem, { label: '结束日期' }, React.createElement(Input, { value: endDate || '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '付款周期' }, React.createElement(Input, { value: prevContractSample.paymentPeriod + '个月', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '签约公司' }, React.createElement(Input, { value: prevContractSample.signingCompany, disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '交车区域' }, React.createElement(Input, { value: deliveryRegionDisplay, disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '交车地点' }, React.createElement(Input, { value: prevContractSample.deliveryLocation, disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '合同审批类型' }, React.createElement(Input, { value: prevContractSample.contractApprovalType || '—', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '合同原件', required: true, error: formErrors.contractOriginal, fullWidth: true }, + React.createElement('div', null, + React.createElement('div', { style: { color: '#999', fontSize: 12, marginBottom: 8 } }, '显示原合同附件,支持多个附件上传(doc/docx/pdf)'), + contractOriginalFiles && contractOriginalFiles.length + ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 12 } }, + contractOriginalFiles.map(function(f, fi) { + return React.createElement('div', { key: fi, style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap', padding: '8px 12px', border: '1px solid #f0f0f0', borderRadius: 6, backgroundColor: '#fafafa' } }, + React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 2 } }, + React.createElement('a', { href: '#', style: { color: '#1890ff' }, onClick: function(e) { e.preventDefault(); openContractOriginalFile(fi); } }, f.name), + React.createElement('span', { style: { color: '#999', fontSize: 12 } }, (f.size ? f.size : '—') + ' · ' + (f.uploadTime ? f.uploadTime : '—')) + ), + f.isOriginal ? null : React.createElement(Button, { type: 'link', danger: true, onClick: function() { removeContractOriginalFile(fi); } }, '删除') + ); + }) + ) + : null, + React.createElement('input', { ref: contractOriginalRef, type: 'file', multiple: true, accept: '.doc,.docx,.pdf', style: { display: 'none' }, onChange: function(e) { + var files = e.target.files ? Array.prototype.slice.call(e.target.files) : []; + addContractOriginalFiles(files); + e.target.value = ''; + } }), + React.createElement(Button, { type: 'default', style: { padding: '8px 16px', border: '1px solid #d9d9d9', borderRadius: 4, backgroundColor: '#fff', color: '#333', cursor: 'pointer', fontSize: 14 }, onClick: function() { if (contractOriginalRef.current) contractOriginalRef.current.click(); } }, '上传附件') + ) + ), + React.createElement(FormItem, { label: '备注', fullWidth: true }, React.createElement(Input.TextArea, { placeholder: '请输入备注信息', value: prevContractSample.remarks, disabled: true, style: { width: '100%', minHeight: 80, resize: 'vertical', backgroundColor: '#f5f5f5' }, rows: 4 })) + ); + + var authorizedContent = React.createElement('div', null, + prevContractSample.authorizedList.map(function(item, index) { + return React.createElement('div', { key: index, style: { display: 'flex', gap: 12, alignItems: 'center', marginBottom: 12 } }, + React.createElement('div', { style: Object.assign({}, styles.inputDisabled, { flex: 1 }) }, item.name), + React.createElement('div', { style: Object.assign({}, styles.inputDisabled, { flex: 1 }) }, item.phone), + React.createElement('div', { style: Object.assign({}, styles.inputDisabled, { flex: 1 }) }, item.idCard) + ); + }) + ); + + var rentalSummaryEl = React.createElement('div', { style: styles.summaryList }, + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '租赁车辆数'), React.createElement('span', { style: styles.summaryListValue }, rentalTotalVehicles + ' 辆')), + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '租金及服务费合计'), React.createElement('span', { style: styles.summaryListValue }, rentalTotalRentService.toFixed(2) + ' 元')), + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '保证金总额'), React.createElement('span', { style: styles.summaryListValue }, rentalTotalDeposit.toFixed(2) + ' 元')), + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '氢气预付款金额'), React.createElement('span', { style: styles.summaryListValue }, rentalTotalHydrogen.toFixed(2) + ' 元')) + ); + var rentalTableBody = prevContractSample.rentalOrders.map(function(row, idx) { + return React.createElement('tr', { key: idx }, + React.createElement('td', { style: styles.rentalTd }, idx + 1), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.brand)), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.model)), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.plateNo)), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.vin)), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.monthRent + ' 元')), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.serviceFee + ' 元')), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.deposit + ' 元')), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.remark || '—')) + ); + }); + var rentalTableEl = React.createElement('table', { style: styles.rentalTable }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: styles.rentalTh, width: 50 }, '序号'), + React.createElement('th', { style: styles.rentalTh, width: 100 }, '品牌'), + React.createElement('th', { style: styles.rentalTh, width: 100 }, '型号'), + React.createElement('th', { style: styles.rentalTh, width: 120 }, '车牌号'), + React.createElement('th', { style: styles.rentalTh, width: 160 }, '车辆识别代码'), + React.createElement('th', { style: styles.rentalTh, width: 120 }, '车辆月租金'), + React.createElement('th', { style: styles.rentalTh, width: 90 }, '服务费'), + React.createElement('th', { style: styles.rentalTh, width: 100 }, '保证金'), + React.createElement('th', { style: styles.rentalTh, width: 80 }, '备注') + ) + ), + React.createElement('tbody', null, rentalTableBody) + ); + var hydrogenReadOnly = React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '氢费承担方' }, React.createElement(Input, { value: prevContractSample.hydrogenBearer, disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '付款方式' }, React.createElement(Input, { value: prevContractSample.hydrogenPaymentMethod, disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '氢气预付款' }, React.createElement(Input, { value: prevContractSample.hydrogenPrepay + ' 元', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '退还车氢气单价' }, React.createElement(Input, { value: prevContractSample.returnHydrogenPrice + ' 元', disabled: true, style: { width: '100%' } })) + ); + var rentalContent = React.createElement('div', null, rentalSummaryEl, React.createElement('div', { style: { overflowX: 'auto', marginBottom: 16 } }, rentalTableEl), hydrogenReadOnly); + + var feeTableHeader3 = React.createElement('tr', null, React.createElement('th', { style: styles.rentalTh }, '项目'), React.createElement('th', { style: styles.rentalTh }, '收费标准'), React.createElement('th', { style: styles.rentalTh }, '服务费')); + var feeTableHeader5 = React.createElement('tr', null, React.createElement('th', { style: styles.rentalTh }, '类别'), React.createElement('th', { style: styles.rentalTh }, '损坏部位'), React.createElement('th', { style: styles.rentalTh }, '配件'), React.createElement('th', { style: styles.rentalTh }, '数量'), React.createElement('th', { style: styles.rentalTh }, '费用明细')); + var makeFeeRow3 = function(r, i) { return React.createElement('tr', { key: i }, React.createElement('td', { style: styles.rentalTd }, r.project), React.createElement('td', { style: styles.rentalTd }, r.standard), React.createElement('td', { style: styles.rentalTd }, r.serviceFee)); }; + var makeFeeRow5 = function(r, i) { return React.createElement('tr', { key: i }, React.createElement('td', { style: styles.rentalTd }, r.category), React.createElement('td', { style: styles.rentalTd }, r.part), React.createElement('td', { style: styles.rentalTd }, r.partName), React.createElement('td', { style: styles.rentalTd }, r.qty), React.createElement('td', { style: styles.rentalTd }, r.feeDetail)); }; + var feeCertTable = React.createElement('table', { style: styles.rentalTable }, React.createElement('thead', null, feeTableHeader3), React.createElement('tbody', null, feeTemplateCertFees.map(makeFeeRow3))); + var feePenaltyTable = React.createElement('table', { style: styles.rentalTable }, React.createElement('thead', null, feeTableHeader3), React.createElement('tbody', null, feeTemplatePenaltyFees.map(makeFeeRow3))); + var feeConsumablesTable = React.createElement('table', { style: styles.rentalTable }, React.createElement('thead', null, feeTableHeader5), React.createElement('tbody', null, feeTemplateConsumables.map(makeFeeRow5))); + var feeOtherTable = React.createElement('table', { style: styles.rentalTable }, React.createElement('thead', null, feeTableHeader3), React.createElement('tbody', null, feeTemplateOtherFees.map(makeFeeRow3))); + var feeContent = React.createElement('div', null, + React.createElement('div', { style: styles.formRow }, React.createElement(FormItem, { label: '选择费用模板' }, React.createElement(Input, { value: prevContractSample.feeTemplate, disabled: true, style: { width: '100%' } }))), + React.createElement('div', { style: Object.assign({}, styles.feeSectionTitle, styles.feeSectionTitleFirst) }, '证照补办费用'), feeCertTable, + React.createElement('div', { style: styles.feeSectionTitle }, '违约金费用'), feePenaltyTable, + React.createElement('div', { style: styles.feeSectionTitle }, '易损件信息'), feeConsumablesTable, + React.createElement('div', { style: styles.feeSectionTitle }, '其他费用信息'), feeOtherTable + ); + + var billingContent = React.createElement('div', null, React.createElement('div', { style: { padding: '12px 16px', border: '1px solid #e8e8e8', borderRadius: 4, backgroundColor: '#fafafa', fontSize: 14, color: '#333' } }, prevContractSample.billingMethod)); + + var requirementContent = '车辆租赁合同-变更为三方合同(2026年3月3日版本)\n「数字化资产ONE-OS运管平台」中的「车辆租赁合同」-「变更为三方合同」模块,在车辆租赁合同操作列点击「变更为三方合同」进行操作;\n\n1.面包屑:\n#业务管理-车辆租赁合同-变更为三方合同\n\n2.客户基本信息卡片:\n#用于从客户列表中选择客户,并将该合同绑定到业务部门及业务负责人(绑定业务部门/业务负责人主要为了后期从部门/业务负责人维度进行数据统计);\n2.1.客户名称:从原合同自动反写,如原客户信息发生改变则按照新信息进行反写;必选项,选择器,支持从输入框内输入内容进行模糊搜索,从客户信息列表中选择对应客户(只显示已通过审核的客户);\n2.2.客户统一信用代码:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「信用代码」字段;\n2.3.客户地址:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「客户地址」字段;\n2.4.客户联系人:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「客户联系人」字段;\n2.5.客户电话:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「客户电话」字段;\n2.6.客户电子邮箱:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「客户电子邮箱」字段;\n2.7.企业名称:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「企业名称」字段;\n2.8.企业电话:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「企业电话」字段;\n2.9.邮寄地址:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「邮寄地址」字段;\n2.10.开户银行:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「开户银行」字段;\n2.11.银行账号:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「银行账号」字段;\n2.12.纳税人识别号:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「纳税人识别号」字段;\n2.13.业务部门:从该条合同自动反写,必选项,选择器,从部门表中选择该租赁合同对应部门;\n2.14.业务负责人:从该条合同自动反写,必选项,选择器,从已选业务部门下拉取对应业务负责人,未选择业务部门时,业务负责人字段不可选;\n\n3.丙方客户基本信息卡片:\n#通过选择丙方,将客户交车应收款、租赁账单、还车应结算合并至丙方下;\n3.1.客户名称:从该条合同自动反查;\n3.2.客户统一信用代码:从该条合同自动反查;\n3.3.客户地址:从该条合同自动反查;\n3.4.客户联系人:从该条合同自动反查;\n3.5.客户电话:从该条合同自动反查;\n3.6.客户电子邮箱:从该条合同自动反查;\n3.7.企业名称:从该条合同自动反查;\n3.8.企业电话:从该条合同自动反查;\n3.9.邮寄地址:从该条合同自动反查;\n3.10.开户银行:从该条合同自动反查;\n3.11.银行账号:从该条合同自动反查;\n3.12.纳税人识别号:从该条合同自动反查;\n3.13.业务部门:从客户基本信息卡片中反写,支持修改;\n3.14.业务负责人:从客户基本信息卡片中反写,支持修改;\n\n4.合同基本信息卡片:\n#用于定义租赁合同基本情况和付款方式;\n4.1.项目名称:从原合同自动反写,必填项,输入框,用于定义该合同项目名称,默认提示信息"请输入项目名称";\n4.2.合同类型:从原合同自动反写,必选项,选择器,合同类型分为「正式合同」「试用合同」;\n4.3.生效日期:从原合同自动反写,必选项,日期选择器,格式为YYYY-MM-DD,精确至天,默认为点击新增日期;\n4.4.付款方式:从原合同自动反写,必选项,付款方式分为「预付」「后付」两种;\n 4.4.1.如果选择预付,以对应「付款方式」和「付款周期」规则,在每一期新账单生成就列入业务待办(工作台功能),同时以消息通知对应用户;\n 4.4.2.如果选择后付,以对应「付款方式」和「付款周期」规则,在每一期新账单生成时,将前一期账单列入业务待办(工作台功能),同时以消息通知对应用户;\n4.5.主要车型:输入框(禁用状态),根据租赁订单信息中所有所选车型,自动反写入输入框并以标签形式显示,支持多车型显示,标签显示:型号名称;\n4.6.结束日期:需要重新填写,必选项,日期选择器,格式为YYYY-MM-DD,精确至天;\n 4.6.1.合同结束日期前30天将以消息提醒方式提醒(消息中心、工作台);\n 4.6.2.到达合同结束日期时,租赁账单将会立刻停止计算,作为最后一期账单;\n 4.6.3.续签合同/转正式合同将重新生成账单,不会对旧合同账单做任何继承处理;\n4.7.付款周期:从原合同自动反写,必选项,选择器,支持1个月-12个月 12种付款周期,账单将以此周期和账单计算方式规则,从交车任务形成的交车单进行完整交车后,定时自动生成账单;\n4.8.签约公司:从原合同自动反写,必选项,选择器,从组织机构表中获取所有根组织机构(如嘉兴羚牛、上海羚牛、广东羚牛等),默认显示新增用户当前机构,可手动修改,后期需要考虑从签约公司维度统计合同相关数据;\n4.9.交车区域:从原合同自动反写,必选项,地区选择器,支持省-市2级,选择区域后,该任务生成交车任务时会自动推送至该区域负责运维人员;\n4.10.交车地点:从原合同自动反写,必填项,输入框,支持自定义输入交车地点;\n4.11.合同审批类型:从原合同自动反写,必填项,选择器,分为「标准合同审批」「非标准合同审批」(原型只读展示);\n4.12.合同原件:显示原合同附件,同时支持上传新附件,必填项,按钮,按钮文字为:上传附件,支持多个附件上传(doc/docx/pdf格式);\n4.13.备注:从原合同自动反写,如果该合同为续签合同,则自动在已填备注信息上方额外添加:续签自:旧合同合同编码xxx,文本域,支持自定义输入备注信息;\n\n5.被授权人信息卡片:\n#用于定义租赁合同相关被授权人相关信息,被授权人在交车单完成时,需要选择被授权人,并通过被授权人手机短信,在E签宝进行签字确认;\n5.1.被授权人:从原合同自动反写,必填项,输入框,用于输入被授权人信息;\n5.2.被授权人联系电话:从原合同自动反写,必填项,输入框,用于输入被授权人联系电话,该电话后续需要接收E签宝签字链接;\n5.3.被授权人身份证:从原合同自动反写,必填项,输入框,用于输入被授权人身份证信息;\n5.4.支持通过新增/删除一行的方式,创建或管理多个授权人,后续交车单完成时可从多个授权人中选择接收授权人;\n\n6.租赁订单信息卡片:\n#用于定义租赁合同对应车辆明细费用、氢费明细费用等相关信息;\n6.1.上方为租赁车辆总计数据,包括租赁车辆数、租金及服务费合计、保证金总额、氢气预付款金额等相关信息;\n 6.1.1.租赁车辆数:显示下方租赁订单信息包含多少辆车;\n 6.1.2.租金及服务费合计:显示下方租赁订单信息中车辆租金总额及服务费总额;\n 6.1.3.保证金总额:显示下方租赁订单信息中车辆保证金总额;\n 6.1.4.氢气预付款金额:显示氢气预付款金额,如客户选择自行承担氢费或羚牛承担氢费,则该处为0不计入氢气预付款金额;\n6.2.下方为列表,显示序号、品牌、型号、车牌号、车辆识别代码、车辆月租金(元)、服务费项目、服务费、保证金、备注,续签合同时,根据续签时的车辆信息填写(包括车牌号);\n 6.2.1.序号:从原合同自动反写,自动按照条数生成,规则为1、2、3....以此类推;\n 6.2.2.品牌:从原合同自动反写,必选项,选择器,从型号参数库中「品牌」字段拉取所有品牌;\n 6.2.3.型号:从原合同自动反写,必选项,选择器,与品牌存在级联关系,未选择品牌则无法选择型号;\n 6.2.4.车牌号:从原合同自动反写,选填项,选择器(支持从输入框输入车牌号关键字下拉匹配),可通过选择车牌号对品牌、型号进行反写;\n 6.2.5.车辆识别代码:从原合同自动反写,输入框(禁用),显示该车辆对应车辆识别代码,根据所选车牌号从车辆表直接拉取进行反写;\n 6.2.6.车辆月租金:从原合同自动反写,输入框,支持两位小数,用于输入该车辆月租金金额,输入框后缀为元,如还车时账单周期不满1个月,则按照:(车辆月租金/30)* 实际天数进行计算;\n 6.2.7.服务费项目:从原合同自动反写,点击管理按钮弹出卡片,卡片标题为:服务项目,下方列表显示服务项目、费用、生效时间、操作;\n 6.2.7.1.服务项目:从原合同自动反写,必选项,选择器(支持输入框输入服务项目关键字进行下拉匹配),选项包含:代处理费用、罚款、违章处理违约金、未参加安全培训、车辆出险、年检年审违约、停车费、设备损坏金(包含易损件)、清洗费、上门收车人工费、上门收车送车行驶费、上门收车基础服务费、保险上浮、保养费用、补办驾驶证、补办牌照、补办营运证、补办加氢证、借用备用钥匙、补配钥匙、租金、氢气费-客、退还车氢量差、能源费补缴、能源费退款、送车上门人工费、送车上门送车行驶费、送车上门基础服务费、保证金、氢气预付费、维修费用、ETC-客、ETC卡缺损费、ETC设备缺损费、电费-客、未结算保养费、未结算维修费、车损费、工具损坏或丢失费、证件费、广告损坏费、送车服务费、接车服务费、补办行驶证、超赔险、轮胎磨损费、无忧包、轮胎保、养护保、尾板;\n 6.2.7.2.费用:从原合同自动反写,必填项,输入框,支持2位小数,输入框后缀为元;\n 6.2.7.3.生效时间:从原合同自动反写,必选项,日期选择器,格式为YYYY-MM-DD;\n 6.2.7.4.操作:删除,点击删除直接删除该行数据;\n 6.2.7.5.新增一行数据:点击添加一行服务项目;\n #在提车应收款中,车辆的交车日期和服务费生效日期可能会存在不同的情况,所以服务费需要单独以生效时间进行计算,例如:\n 交车后车辆从1月1日开始计费,付款周期为2个月,则提车应收款租金会计算到3月1日,但是服务费生效时间为1月30日,此情况下需要以3月1日-1月30日,计算出服务费具体收费天数为30天,然后根据:(服务费费用/30)* 服务费收费天数30)进行计算;\n 6.2.8.服务费:从原合同自动反写,自动根据添加的所有服务费项目计算总额,支持2位小数,格式为:xx.xx元;\n 6.2.9.保证金:从原合同自动反写,必填项,输入框,支持2位小数,后缀为元,用于填写车辆需要支付的保证金金额,保证金为提车应收款一次性支付,还车时需要计入退还费用中;\n 6.2.10.备注:选填项,输入框,用于备注车辆复杂情况;\n 6.2.11.操作:删除,点击删除删除该行数据;\n 6.2.12.添加一行:点击后列表新增一行,用于填写一条新的车辆租金费用信息;\n6.3.氢费承担方:从原合同自动反写,必选项,选择器,选项为:「我方」、「客户」;\n 6.3.1.选择「我方」:不显示付款方式、氢气预付款;\n 6.3.2.选择「客户」:付款方式字段默认为预付,可手动修改;\n6.4.付款方式:从原合同自动反写,必选项,选择器,选项为:「预付」、「月付款」、「自行结算」;\n 6.4.1.预付:选择「预付」,需要填写:「氢气预付款」,氢气预付款指合同签署时客户就需预先付出的氢费款项,该部分款项会自动计入提车应收款中氢气预付款金额;\n 6.4.2.月付款:选择「月付款」,提车应收款中不进行收费,而是由业务人员按照实际情况,通过氢费账单功能生成对应氢费账单,单独与客户进行结算;\n 6.4.3.自行结算:选择「自行结算」,指合同签署后,所有氢气费用由客户自行承担;\n6.5.氢气预付款:从原合同自动反写,选择「客户」「预付」时显示,必填项,输入框,支持2位小数,氢气预付款金额会计算入该合同交车应收款中,并计入5.1.4.氢气预付款金额中;\n6.6.退还车氢气单价:从原合同自动反写,必填项,输入框,支持2位小数,后缀为元,不管氢费承担方和付款方式选择任何选项都需要进行维护,该金额主要用于与客户约定还车时,与交车时氢气差值以此费用进行自动计算和结算;\n\n7.其他费用信息卡片:\n#用于选择对应租赁费用模板,选择后展示证照补办费用、违约金费用、易损件费用、其他费用等信息,租赁费用模板管理功能位于「车辆租赁合同」列表左上角;\n7.1.选择费用模板:从原合同自动反写,如果费用模板有更新,则按照更新后内容显示,必选项,从「租赁费用模板」中拉取,选择后自动将该费用模板所有环节费用显示在合同中;\n 7.1.1.证照补办费用:单独标题显示,内容为列表,列表中包括项目、收费标准、服务费,选择费用模板后自动反显;\n 7.1.2.违约金费用:单独标题显示,内容为列表,列表中包含项目、收费标准、服务费,选择费用模板后自动反显;\n 7.1.3.易损件信息:单独标题显示,内容为列表,列表中包含类别、损坏部位、配件、数量、费用明细,选择费用模板后自动反显;\n 7.1.4.其他费用信息:单独标题显示,内容为列表,列表中包含项目、收费标准、服务费,选择费用模板后自动反显;\n\n8.账单计算方式卡片:\n#必选项,从原合同自动反写,填充按钮组,可手动修改,用于定义租赁合同的账单计算方式,分为「按自然月结算」、「按付款周期天数结算」两种方式;\n8.1.按付款周期天数结算:账单按照合同基本信息卡片中每隔付款周期*30天形成一期账单;\n 例如付款周期为2个月,则交车任务交车成功时,提车应收款从交车任务配置的开始计费时间开始,根据60天收取车辆租金,此后每隔60天生成一期租赁账单;\n8.2.按自然月结算:账单按照第一个月计费开始日期到当月最后一天为第一期,之后按照付款周期所选月份间隔,从开始月份第一天到间隔月份最后一天的自然月方式形成一期账单;\n 例如付款周期为2个月,则交车任务交车成功时,提车应收款车辆租金需要收取≥2个月租金作为标准流程,<2个月租金作为非标流程;\n 租赁账单首期从交车任务配置的开始计费时间开始,如付款周期为2个月,则首期账单结束时间为第二个月最后一天,付款周期为3个月,则首期账单结束时间为第三个月最后一天,此后每一期按照实际自然月开始-结束形成账单,例如付款周期为2个月,则第二期账单从2月1日-3月31日,以此类推;\n\n9.最下方为提交并审核、保存、取消三个按钮;\n9.1.点击提交并审核,toast提示:租赁合同已提交审核。同时该租赁合同重新进入租赁合同审核列表中;\n9.2.点击保存,会存储租赁订单已填写内容,不做必填项校验,同时显示在租赁合同列表中,该条数据只能保存人自己查看并编辑,其他人无法操作;\n9.3.点击取消,如当前页面有已编辑内容时,点击取消会进行二次提示,内容为:取消将会丢失所有已填写内容,是否确认?点击确认返回车辆租赁合同列表页;\n\n所有卡片支持收起/展开功能,通过点击卡片右侧收起/展开实现;并增加锚点功能,锚点固定于页面右上角,点击锚点对应卡片名称,页面自动跳转至该卡片所在区域;'; + + var reqSpecModalContent = reqSpecOpen ? React.createElement('div', { style: styles.modalMask, onClick: function(e) { if (e.target === e.currentTarget) setReqSpecOpen(false); } }, + React.createElement('div', { style: Object.assign({}, styles.modalBox, { maxWidth: 720 }), onClick: function(e) { e.stopPropagation(); } }, + React.createElement('div', { style: styles.modalHeader }, '需求说明'), + React.createElement('div', { style: Object.assign({}, styles.modalBody, { maxHeight: '70vh', padding: '20px 24px' }) }, React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6 } }, requirementContent)), + React.createElement('div', { style: { padding: '12px 20px', borderTop: '1px solid #f0f0f0', textAlign: 'right' } }, React.createElement(Button, { onClick: function() { setReqSpecOpen(false); } }, '关闭')) + ) + ) : null; + + return React.createElement('div', { style: styles.page }, + React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 } }, React.createElement('div', { style: styles.breadcrumb }, React.createElement('span', null, '业务管理'), React.createElement('span', { style: styles.breadcrumbSep }, ' / '), React.createElement('span', null, '车辆租赁合同'), React.createElement('span', { style: styles.breadcrumbSep }, ' / '), React.createElement('span', { style: { color: '#1890ff' } }, '变更为三方合同')), React.createElement('span', { style: { color: '#1890ff', cursor: 'pointer', fontSize: 14 }, onClick: function() { setReqSpecOpen(true); } }, '查看需求说明')), + React.createElement('div', { style: styles.anchorWrap }, + React.createElement('div', { style: { marginBottom: 8, fontWeight: 600, fontSize: 14 } }, '锚点导航'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-customer'); } }, '客户基本信息'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-third-party'); } }, '丙方客户基本信息'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-contract'); } }, '合同基本信息'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-authorized'); } }, '被授权人信息'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-rental'); } }, '租赁订单信息'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-fee'); } }, '其他费用信息'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-billing'); } }, '账单计算方式') + ), + React.createElement('div', { id: 'card-customer' }, React.createElement(CardBlock, { id: 'card-customer', title: '客户基本信息', collapsed: cc1, setCollapsed: setCc1 }, customerFields)), + React.createElement('div', { id: 'card-third-party', style: { marginTop: 16 } }, React.createElement(CardBlock, { id: 'card-third-party', title: '丙方客户基本信息', collapsed: cc1b, setCollapsed: setCc1b }, thirdPartyCustomerFields)), + React.createElement('div', { id: 'card-contract', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '合同基本信息', collapsed: cc2, setCollapsed: setCc2 }, contractFormRow1)), + React.createElement('div', { id: 'card-authorized', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '被授权人信息', collapsed: cc3, setCollapsed: setCc3 }, authorizedContent)), + React.createElement('div', { id: 'card-rental', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '租赁订单信息', collapsed: cc4, setCollapsed: setCc4 }, rentalContent)), + React.createElement('div', { id: 'card-fee', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '其他费用信息', collapsed: cc5, setCollapsed: setCc5 }, feeContent)), + React.createElement('div', { id: 'card-billing', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '账单计算方式', collapsed: cc6, setCollapsed: setCc6 }, billingContent)), + React.createElement('div', { style: { height: 60 } }), + reqSpecModalContent, + cancelConfirmOpen ? React.createElement('div', { style: styles.modalMask, onClick: function(e) { if (e.target === e.currentTarget) setCancelConfirmOpen(false); } }, + React.createElement('div', { style: Object.assign({}, styles.modalBox, { maxWidth: 520 }), onClick: function(e) { e.stopPropagation(); } }, + React.createElement('div', { style: styles.modalHeader }, '提示'), + React.createElement('div', { style: Object.assign({}, styles.modalBody, { padding: '20px 24px' }) }, '取消将会丢失所有已填写内容,是否确认?'), + React.createElement('div', { style: { padding: '12px 20px', borderTop: '1px solid #f0f0f0', textAlign: 'right', display: 'flex', gap: 12, justifyContent: 'flex-end' } }, + React.createElement(Button, { onClick: function() { setCancelConfirmOpen(false); } }, '否'), + React.createElement(Button, { type: 'primary', onClick: function() { setCancelConfirmOpen(false); setEdited(false); message.info('已返回车辆租赁合同列表(原型)'); } }, '是') + ) + ) + ) : null, + React.createElement('div', { style: styles.footer }, + React.createElement(Button, { type: 'primary', onClick: function() { if (validateSubmitAndReview()) { setEdited(false); message.success('租赁合同已提交审核。同时该租赁合同重新进入租赁合同审核列表中。'); } } }, '提交并审核'), + React.createElement(Button, { onClick: function() { setEdited(false); message.success('已保存(不校验必填项),该条数据只能保存人自己查看并编辑,其他人无法操作。'); } }, '保存'), + React.createElement(Button, { onClick: function() { if (edited) setCancelConfirmOpen(true); else message.info('已返回车辆租赁合同列表(原型)'); } }, '取消') + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-lease-contract/pages/02-车辆租赁合同-查看.jsx b/src/prototypes/oneos-web-lease-contract/pages/02-车辆租赁合同-查看.jsx new file mode 100644 index 0000000..fa1b1a1 --- /dev/null +++ b/src/prototypes/oneos-web-lease-contract/pages/02-车辆租赁合同-查看.jsx @@ -0,0 +1,562 @@ +// 【重要】必须使用 const Component 作为组件变量名 - Axhub 产品原型 +// 车辆资产管理系统 - 查看租赁合同模块(只读 + 审批状态 + 盖章附件,IE11+ 兼容) + +const Component = function() { + var cc1State = React.useState(false); + var cc1 = cc1State[0]; + var setCc1 = cc1State[1]; + var cc1bState = React.useState(false); + var cc1b = cc1bState[0]; + var setCc1b = cc1bState[1]; + var cc2State = React.useState(false); + var cc2 = cc2State[0]; + var setCc2 = cc2State[1]; + var cc3State = React.useState(false); + var cc3 = cc3State[0]; + var setCc3 = cc3State[1]; + var cc4State = React.useState(false); + var cc4 = cc4State[0]; + var setCc4 = cc4State[1]; + var cc4bState = React.useState(false); + var cc4b = cc4bState[0]; + var setCc4b = cc4bState[1]; + var cc5State = React.useState(false); + var cc5 = cc5State[0]; + var setCc5 = cc5State[1]; + var cc6State = React.useState(false); + var cc6 = cc6State[0]; + var setCc6 = cc6State[1]; + var stampedHoverIndexState = React.useState(null); + var stampedHoverIndex = stampedHoverIndexState[0]; + var setStampedHoverIndex = stampedHoverIndexState[1]; + var reqSpecState = React.useState(false); + var reqSpecOpen = reqSpecState[0]; + var setReqSpecOpen = reqSpecState[1]; + var servicePopoverRowState = React.useState(null); + var servicePopoverRow = servicePopoverRowState[0]; + var setServicePopoverRow = servicePopoverRowState[1]; + var servicePopoverRowNewVehicleState = React.useState(null); + var servicePopoverRowNewVehicle = servicePopoverRowNewVehicleState[0]; + var setServicePopoverRowNewVehicle = servicePopoverRowNewVehicleState[1]; + + // 模拟已上传的盖章合同附件(法务上传,可多附件;三方合同重新审批后会有新附件) + var stampedContractFiles = [ + { name: '盖章合同-租赁合同-盖章版.pdf', size: '1.2 MB', uploadTime: '2026-02-18 10:30' }, + { name: '盖章合同-补充协议-盖章版.pdf', size: '0.6 MB', uploadTime: '2026-02-18 10:35' } + ]; + + // 合同变更历史记录(变更时间倒序,序号 1.2.3...) + var changeHistoryRaw = [ + { changeTime: '2026-03-03 10:20', opType: '保存', operator: '张三', remark: '无' }, + { changeTime: '2026-03-02 16:10', opType: '变更为三方合同', operator: '李四', remark: '增加丙方客户“客户名称杭州某某供应链有限公司”' }, + { changeTime: '2026-03-01 11:40', opType: '添加车辆', operator: '王五', remark: '添加车辆“沪A30003”\n添加车辆“浙A10001”\n添加车辆“浙B20002”' }, + { changeTime: '2026-02-28 09:05', opType: '添加授权人', operator: '赵六', remark: '添加授权人“授权人张三”\n添加授权人“授权人李四”' }, + { changeTime: '2026-02-26 15:30', opType: '续签合同', operator: '周九', remark: '续签自“原合同编码JXZL20250210YW101100A”' }, + { changeTime: '2026-02-23 14:18', opType: '撤回合同', operator: '钱七', remark: '主动撤回' }, + { changeTime: '2026-02-20 10:00', opType: '终止合同', operator: '孙八', remark: '主动终止' }, + { changeTime: '2026-02-16 14:30', opType: '转正式合同', operator: '张经理', remark: '转正式合同自“原合同编码JXZL20260210YW101230B”' } + ]; + var changeHistorySorted = changeHistoryRaw.slice().sort(function(a, b) { return b.changeTime.localeCompare(a.changeTime); }); + + var scrollToCard = function(id) { + var el = document.getElementById(id); + if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }; + + var styles = { + page: { padding: '16px 24px 48px', backgroundColor: '#f5f5f5', minHeight: '100vh', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', fontSize: 14 }, + breadcrumb: { marginBottom: 16, color: '#666' }, + breadcrumbSep: { margin: '0 8px', color: '#999' }, + anchorWrap: { position: 'fixed', top: 80, right: 24, zIndex: 100, backgroundColor: '#fff', borderRadius: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.12)', padding: '12px 16px', minWidth: 160 }, + anchorItem: { display: 'block', padding: '6px 0', color: '#1890ff', cursor: 'pointer', border: 'none', background: 'none', width: '100%', textAlign: 'left', fontSize: 13 }, + card: { backgroundColor: '#fff', borderRadius: 8, marginBottom: 16, boxShadow: '0 1px 2px rgba(0,0,0,0.05)', overflow: 'hidden' }, + cardHeader: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '16px 20px', borderBottom: '1px solid #f0f0f0', cursor: 'pointer' }, + cardTitle: { fontSize: 16, fontWeight: 600, color: '#333' }, + cardToggle: { color: '#999', fontSize: 14 }, + cardBody: { padding: '20px 24px' }, + formRow: { display: 'flex', flexWrap: 'wrap', marginBottom: 16 }, + formCol: { flex: '0 0 33.33%', minWidth: 200, paddingRight: 16, marginBottom: 8 }, + formColFull: { flex: '0 0 100%', marginBottom: 8 }, + label: { display: 'block', marginBottom: 6, color: '#333' }, + input: { width: '100%', padding: '8px 12px', border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 14 }, + inputDisabled: { backgroundColor: '#f5f5f5', color: '#666', cursor: 'default', borderColor: '#e8e8e8' }, + summaryList: { marginBottom: 16, display: 'flex', flexWrap: 'wrap', gap: 16 }, + summaryListItem: { flex: '0 0 calc(50% - 8px)', display: 'flex', alignItems: 'center', padding: '12px 16px', border: '1px solid #e8e8e8', borderRadius: 4, backgroundColor: '#fafafa', fontSize: 14, boxSizing: 'border-box' }, + summaryListLabel: { flex: '0 0 140px', color: '#666' }, + summaryListValue: { flex: 1, fontWeight: 600, color: '#333' }, + rentalTable: { width: '100%', borderCollapse: 'collapse', fontSize: 13 }, + rentalTh: { padding: '10px 8px', textAlign: 'left', borderBottom: '1px solid #e8e8e8', backgroundColor: '#fafafa', fontWeight: 600 }, + rentalTd: { padding: '8px', borderBottom: '1px solid #f0f0f0', verticalAlign: 'middle' }, + rentalInputDisabled: { backgroundColor: '#f5f5f5', color: '#666', border: '1px solid #e8e8e8', padding: '6px 10px', borderRadius: 4, width: '100%', fontSize: 13 }, + tag: { display: 'inline-block', padding: '2px 8px', marginRight: 8, marginBottom: 4, backgroundColor: '#e6f7ff', color: '#1890ff', borderRadius: 4, fontSize: 12 }, + footer: { position: 'fixed', bottom: 0, left: 0, right: 0, padding: '12px 24px', backgroundColor: '#fff', borderTop: '1px solid #e8e8e8', display: 'flex', gap: 12, justifyContent: 'flex-start', zIndex: 99 }, + btn: { padding: '8px 24px', borderRadius: 4, border: '1px solid #d9d9d9', cursor: 'pointer', fontSize: 14 }, + btnDefault: { backgroundColor: '#fff', color: '#333' }, + approvalCard: { backgroundColor: '#fff', borderRadius: 8, marginBottom: 16, boxShadow: '0 1px 2px rgba(0,0,0,0.05)', overflow: 'hidden' }, + approvalCardHeader: { padding: '16px 20px', borderBottom: '1px solid #f0f0f0', fontSize: 16, fontWeight: 600, color: '#333', textAlign: 'center' }, + approvalCardBody: { padding: '24px 20px', display: 'flex', justifyContent: 'center' }, + stepWrap: { display: 'flex', alignItems: 'flex-start', flexWrap: 'wrap', justifyContent: 'center' }, + stepItem: { flex: '1 1 0', minWidth: 140, maxWidth: 220, textAlign: 'center', position: 'relative' }, + stepIcon: { width: 32, height: 32, borderRadius: '50%', margin: '0 auto 8px', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 16, fontWeight: 600 }, + stepIconDone: { backgroundColor: '#52c41a', color: '#fff' }, + stepLine: { position: 'absolute', top: 16, left: '50%', right: '-50%', height: 2, backgroundColor: '#e8e8e8', zIndex: 0 }, + stepLineDone: { backgroundColor: '#52c41a' }, + stepTitle: { fontSize: 13, color: '#333', fontWeight: 500, marginBottom: 4 }, + stepDesc: { fontSize: 12, color: '#666' }, + stepStatus: { fontSize: 12, color: '#52c41a', marginTop: 4 }, + stepTime: { fontSize: 12, color: '#999', marginTop: 2 }, + attachmentRow: { display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 12 }, + attachmentFile: { display: 'inline-flex', alignItems: 'center', padding: '8px 12px', backgroundColor: '#f5f5f5', borderRadius: 4, border: '1px solid #d9d9d9', fontSize: 14, color: '#1890ff', cursor: 'pointer', textDecoration: 'none', fontFamily: 'inherit', margin: 0, outline: 'none' }, + attachmentFileHover: { color: '#40a9ff', borderColor: '#1890ff', backgroundColor: '#e6f7ff' }, + attachmentMeta: { fontSize: 12, color: '#999', marginLeft: 8 }, + feeSectionTitle: { fontSize: 15, fontWeight: 600, color: '#333', marginTop: 20, marginBottom: 10 }, + feeSectionTitleFirst: { marginTop: 0 }, + historyLink: { color: '#1890ff', cursor: 'pointer', background: 'none', border: 'none', padding: 0, fontSize: 14 }, + modalMask: { position: 'fixed', left: 0, top: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.45)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center' }, + modalBox: { backgroundColor: '#fff', borderRadius: 8, width: '90%', maxWidth: 640, maxHeight: '85vh', overflow: 'hidden', display: 'flex', flexDirection: 'column', boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }, + modalHeader: { padding: '16px 20px', borderBottom: '1px solid #f0f0f0', fontSize: 16, fontWeight: 600 }, + modalBody: { padding: 20, overflow: 'auto', flex: 1 } + }; + + var CardBlock = function(props) { + return React.createElement('div', { id: props.id, style: styles.card }, + React.createElement('div', { style: styles.cardHeader, onClick: function() { props.setCollapsed(!props.collapsed); } }, + React.createElement('span', { style: styles.cardTitle }, props.title), + React.createElement('span', { style: styles.cardToggle }, props.collapsed ? '展开' : '收起') + ), + !props.collapsed ? React.createElement('div', { style: styles.cardBody }, props.children) : null + ); + }; + + var FormItemReadOnly = function(props) { + var colStyle = props.fullWidth ? styles.formColFull : styles.formCol; + return React.createElement('div', { style: colStyle }, + React.createElement('label', { style: styles.label }, props.label), + React.createElement('div', { style: Object.assign({}, styles.input, styles.inputDisabled) }, props.value || '—') + ); + }; + + // 审批流程步骤数据(全部已通过,含审核时间 YYYY-MM-DD HH:MM) + var approvalSteps = [ + { title: '业务部主管', person: '姚守涛', status: '已通过', approveTime: '2026-02-16 09:30' }, + { title: '事业部主管', person: '尚建华', status: '已通过', approveTime: '2026-02-16 10:15' }, + { title: '财务部', person: '宋欣怡 / 吕红', status: '已通过', approveTime: '2026-02-16 11:20' }, + { title: '法务部', person: '高洁 / 彭青松', status: '已通过', approveTime: '2026-02-16 14:00' } + ]; + + var Steps = window.antd && window.antd.Steps; + var approvalCurrent = (function() { + for (var i = 0; i < approvalSteps.length; i++) { + if (approvalSteps[i].status !== '已通过') return i; + } + return approvalSteps.length - 1; + })(); + var approvalCardEl = React.createElement('div', { style: Object.assign({}, styles.card, { marginTop: 16 }) }, + React.createElement('div', { style: Object.assign({}, styles.cardHeader, { cursor: 'default' }) }, + React.createElement('span', { style: styles.cardTitle }, '审批状态') + ), + React.createElement('div', { style: styles.cardBody }, + Steps + ? React.createElement(Steps, { + direction: 'vertical', + current: approvalCurrent, + items: approvalSteps.map(function(s) { + var statusText = s.status === '已通过' ? '审批通过' : s.status === '审批驳回' ? '审批驳回' : '待审批'; + var desc = React.createElement('div', { style: { fontSize: 13, color: 'rgba(0,0,0,0.65)', marginTop: 4 } }, + React.createElement('div', null, '审批状态:', statusText), + React.createElement('div', null, '审批人:', s.person || '-'), + React.createElement('div', null, '审批时间:', s.approveTime || '-') + ); + return { + title: s.title, + description: desc, + status: s.status === '已通过' ? 'finish' : s.status === '审批驳回' ? 'error' : 'wait' + }; + }) + }) + : React.createElement('div', { style: { color: '#999', fontSize: 13 } }, '暂无审批信息') + ) + ); + + // 模拟只读数据(与新增合同同结构) + var mockCustomer = { name: '嘉兴某某物流有限公司', creditCode: '91330400MA2XXXXX1', address: '浙江省嘉兴市南湖区科技大道1号', contact: '张三', phone: '13800138001', email: 'zhangsan@example.com', companyName: '嘉兴某某物流有限公司', companyPhone: '0571-88888888', mailingAddress: '浙江省嘉兴市南湖区科技大道1号', bank: '中国工商银行嘉兴分行', bankAccount: '6222021234567890123', taxId: '91330400MA2XXXXX1' }; + var isThreePartyContract = true; + var mockThirdPartyCustomer = { name: '杭州某某供应链有限公司', creditCode: '91330100MA2YYYYY2', address: '浙江省杭州市余杭区未来科技城', contact: '李四', phone: '13800138002', email: 'lisi@example.com', companyName: '杭州某某供应链有限公司', companyPhone: '0571-99999999', mailingAddress: '浙江省杭州市余杭区未来科技城', bank: '中国农业银行杭州分行', bankAccount: '6228481234567890123', taxId: '91330100MA2YYYYY2', department: '业务2部', responsible: '李专员' }; + var contractOriginalFiles = [ + { name: '租赁合同-原件.pdf', size: '1.2 MB', uploadTime: '2026-02-16 14:30' }, + { name: '租赁合同-补充协议.pdf', size: '0.6 MB', uploadTime: '2026-02-16 14:35' } + ]; + if (isThreePartyContract) { + contractOriginalFiles = contractOriginalFiles.concat([{ name: '三方合同-原件.pdf', size: '0.8 MB', uploadTime: '2026-03-02 16:10' }]); + stampedContractFiles = stampedContractFiles.concat([{ name: '三方盖章合同-盖章版.pdf', size: '1.1 MB', uploadTime: '2026-03-03 09:50' }]); + } + var mockContract = { projectName: '嘉兴氢能运输项目', contractCode: 'JXZL20260216YW101235A', contractType: '正式合同', effectiveDate: '2026-02-16', paymentMethod: '预付', mainVehicleModels: '型号A1、型号A2', endDate: '2027-02-16', paymentPeriod: '1个月', signingCompany: '嘉兴羚牛', deliveryRegion: '浙江省 / 嘉兴市', deliveryLocation: '嘉兴市南湖区科技大道1号', contractApprovalType: '标准合同审批', remarks: '' }; + var mockAuthorized = [{ name: '张三', phone: '13800138001', idCard: '330102199001011234' }]; + var mockRentalOrders = [ + { brand: '品牌A', model: '型号A1', plateNo: '浙A10001', vehicleStatus: '已交车', vin: 'L1234567890ABCDEF', monthRent: '8000', serviceFee: '500', deposit: '10000', remark: '', serviceItems: [{ project: '保养费用', fee: '200', effectiveDate: '2026-03-01' }, { project: '清洗费', fee: '80', effectiveDate: '2026-03-01' }] }, + { brand: '品牌A', model: '型号A2', plateNo: '浙B20002', vehicleStatus: '已交车-临时替换', vin: 'L2234567890ABCDEF', monthRent: '8000', serviceFee: '500', deposit: '10000', remark: '', serviceItems: [{ project: '清洗费', fee: '80', effectiveDate: '2026-03-01' }] } + ]; + var mockRentalSummary = { vehicleCount: 2, totalRentService: '17000.00', totalDeposit: '20000.00', hydrogenPrepay: '5000.00' }; + var addVehicleDate = '2026-03-15'; + var mockNewVehicleOrders = [ + { brand: '品牌B', model: '型号B1', plateNo: '沪A30003', vehicleStatus: '待交车', vin: 'L3234567890ABCDEF', monthRent: '7000', serviceFee: '400', deposit: '8000', remark: '新增车辆', serviceItems: [{ project: '保养费用', fee: '180', effectiveDate: '2026-04-01' }] } + ]; + var mockNewVehicleSummary = { vehicleCount: 1, totalRentService: '7400.00', totalDeposit: '8000.00' }; + var mockHydrogen = { bearer: '客户', paymentMethod: '预付', hydrogenPrepay: '5000', returnPrice: '80' }; + var mockFeeTemplate = '标准费用模板A'; + var mockBillingMethod = '按自然月结算'; + var feeTemplateCertFees = [{ project: '补办行驶证', standard: '50元/次', serviceFee: '20' }, { project: '补办驾驶证', standard: '30元/次', serviceFee: '10' }, { project: '补办牌照', standard: '100元/次', serviceFee: '50' }]; + var feeTemplatePenaltyFees = [{ project: '提前退车违约金', standard: '月租金×1', serviceFee: '0' }, { project: '违章处理违约金', standard: '按实际发生', serviceFee: '50' }]; + var feeTemplateConsumables = [{ category: '轮胎', part: '前轮', partName: '轮胎A型', qty: 1, feeDetail: '500.00' }, { category: '易损件', part: '雨刮', partName: '雨刮片', qty: 2, feeDetail: '80.00' }]; + var feeTemplateOtherFees = [{ project: '上门送车费', standard: '100元/次', serviceFee: '50' }, { project: '上门收车费', standard: '100元/次', serviceFee: '50' }, { project: '清洗费', standard: '80元/次', serviceFee: '30' }]; + var feeTableHeader3 = React.createElement('tr', null, React.createElement('th', { style: styles.rentalTh }, '项目'), React.createElement('th', { style: styles.rentalTh }, '收费标准'), React.createElement('th', { style: styles.rentalTh }, '服务费')); + var feeTableHeader5 = React.createElement('tr', null, React.createElement('th', { style: styles.rentalTh }, '类别'), React.createElement('th', { style: styles.rentalTh }, '损坏部位'), React.createElement('th', { style: styles.rentalTh }, '配件'), React.createElement('th', { style: styles.rentalTh }, '数量'), React.createElement('th', { style: styles.rentalTh }, '费用明细')); + var makeFeeRow3 = function(r, i) { + var td1 = React.createElement('td', { style: styles.rentalTd }, r.project); + var td2 = React.createElement('td', { style: styles.rentalTd }, r.standard); + var td3 = React.createElement('td', { style: styles.rentalTd }, r.serviceFee); + return React.createElement('tr', { key: i }, td1, td2, td3); + }; + var makeFeeRow5 = function(r, i) { + var td1 = React.createElement('td', { style: styles.rentalTd }, r.category); + var td2 = React.createElement('td', { style: styles.rentalTd }, r.part); + var td3 = React.createElement('td', { style: styles.rentalTd }, r.partName); + var td4 = React.createElement('td', { style: styles.rentalTd }, r.qty); + var td5 = React.createElement('td', { style: styles.rentalTd }, r.feeDetail); + return React.createElement('tr', { key: i }, td1, td2, td3, td4, td5); + }; + var feeCertRows = feeTemplateCertFees.map(makeFeeRow3); + var feePenaltyRows = feeTemplatePenaltyFees.map(makeFeeRow3); + var feeConsumablesRows = feeTemplateConsumables.map(makeFeeRow5); + var feeOtherRows = feeTemplateOtherFees.map(makeFeeRow3); + var feeCertTable = React.createElement('table', { style: styles.rentalTable }, React.createElement('thead', null, feeTableHeader3), React.createElement('tbody', null, feeCertRows)); + var feePenaltyTable = React.createElement('table', { style: styles.rentalTable }, React.createElement('thead', null, feeTableHeader3), React.createElement('tbody', null, feePenaltyRows)); + var feeConsumablesTable = React.createElement('table', { style: styles.rentalTable }, React.createElement('thead', null, feeTableHeader5), React.createElement('tbody', null, feeConsumablesRows)); + var feeOtherTable = React.createElement('table', { style: styles.rentalTable }, React.createElement('thead', null, feeTableHeader3), React.createElement('tbody', null, feeOtherRows)); + var feeTemplateBody = React.createElement('div', null, + React.createElement('div', { style: Object.assign({}, styles.feeSectionTitle, styles.feeSectionTitleFirst) }, '证照补办费用'), + feeCertTable, + React.createElement('div', { style: styles.feeSectionTitle }, '违约金费用'), + feePenaltyTable, + React.createElement('div', { style: styles.feeSectionTitle }, '易损件信息'), + feeConsumablesTable, + React.createElement('div', { style: styles.feeSectionTitle }, '其他费用信息'), + feeOtherTable + ); + + var customerFields = React.createElement('div', { style: styles.formRow }, + React.createElement(FormItemReadOnly, { label: '客户名称', value: mockCustomer.name }), + React.createElement(FormItemReadOnly, { label: '客户统一信用代码', value: mockCustomer.creditCode }), + React.createElement(FormItemReadOnly, { label: '客户地址', value: mockCustomer.address }), + React.createElement(FormItemReadOnly, { label: '客户联系人', value: mockCustomer.contact }), + React.createElement(FormItemReadOnly, { label: '客户电话', value: mockCustomer.phone }), + React.createElement(FormItemReadOnly, { label: '客户电子邮箱', value: mockCustomer.email }), + React.createElement(FormItemReadOnly, { label: '企业名称', value: mockCustomer.companyName }), + React.createElement(FormItemReadOnly, { label: '企业电话', value: mockCustomer.companyPhone }), + React.createElement(FormItemReadOnly, { label: '邮寄地址', value: mockCustomer.mailingAddress }), + React.createElement(FormItemReadOnly, { label: '开户银行', value: mockCustomer.bank }), + React.createElement(FormItemReadOnly, { label: '银行账号', value: mockCustomer.bankAccount }), + React.createElement(FormItemReadOnly, { label: '纳税人识别号', value: mockCustomer.taxId }), + React.createElement(FormItemReadOnly, { label: '业务部门', value: '业务1部' }), + React.createElement(FormItemReadOnly, { label: '业务负责人', value: '张经理' }) + ); + + var thirdPartyCustomerFields = React.createElement('div', { style: styles.formRow }, + React.createElement(FormItemReadOnly, { label: '客户名称', value: mockThirdPartyCustomer.name }), + React.createElement(FormItemReadOnly, { label: '客户统一信用代码', value: mockThirdPartyCustomer.creditCode }), + React.createElement(FormItemReadOnly, { label: '客户地址', value: mockThirdPartyCustomer.address }), + React.createElement(FormItemReadOnly, { label: '客户联系人', value: mockThirdPartyCustomer.contact }), + React.createElement(FormItemReadOnly, { label: '客户电话', value: mockThirdPartyCustomer.phone }), + React.createElement(FormItemReadOnly, { label: '客户电子邮箱', value: mockThirdPartyCustomer.email }), + React.createElement(FormItemReadOnly, { label: '企业名称', value: mockThirdPartyCustomer.companyName }), + React.createElement(FormItemReadOnly, { label: '企业电话', value: mockThirdPartyCustomer.companyPhone }), + React.createElement(FormItemReadOnly, { label: '邮寄地址', value: mockThirdPartyCustomer.mailingAddress }), + React.createElement(FormItemReadOnly, { label: '开户银行', value: mockThirdPartyCustomer.bank }), + React.createElement(FormItemReadOnly, { label: '银行账号', value: mockThirdPartyCustomer.bankAccount }), + React.createElement(FormItemReadOnly, { label: '纳税人识别号', value: mockThirdPartyCustomer.taxId }), + React.createElement(FormItemReadOnly, { label: '业务部门', value: mockThirdPartyCustomer.department }), + React.createElement(FormItemReadOnly, { label: '业务负责人', value: mockThirdPartyCustomer.responsible }) + ); + + var contractFormRow1 = React.createElement('div', { style: styles.formRow }, + React.createElement(FormItemReadOnly, { label: '项目名称', value: mockContract.projectName }), + React.createElement(FormItemReadOnly, { label: '合同编码', value: mockContract.contractCode }), + React.createElement(FormItemReadOnly, { label: '合同类型', value: mockContract.contractType }), + React.createElement(FormItemReadOnly, { label: '生效日期', value: mockContract.effectiveDate }), + React.createElement(FormItemReadOnly, { label: '付款方式', value: mockContract.paymentMethod }), + React.createElement(FormItemReadOnly, { label: '主要车型', value: mockContract.mainVehicleModels }), + React.createElement(FormItemReadOnly, { label: '结束日期', value: mockContract.endDate }), + React.createElement(FormItemReadOnly, { label: '付款周期', value: mockContract.paymentPeriod }), + React.createElement(FormItemReadOnly, { label: '签约公司', value: mockContract.signingCompany }), + React.createElement('div', { style: styles.formCol }, + React.createElement(FormItemReadOnly, { label: '交车区域', value: mockContract.deliveryRegion }), + React.createElement('div', null, + React.createElement('label', { style: styles.label }, '合同原件'), + React.createElement('div', { style: Object.assign({}, styles.input, styles.inputDisabled, { padding: '8px 12px' }) }, + contractOriginalFiles && contractOriginalFiles.length + ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8 } }, + contractOriginalFiles.map(function(f, i) { + return React.createElement('div', { key: i, style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } }, + React.createElement('a', { href: '#', style: { color: '#1890ff', cursor: 'pointer', textDecoration: 'none' }, onClick: function(e) { e.preventDefault(); window.open('#', '_blank'); } }, f.name), + (f.size || f.uploadTime) ? React.createElement('span', { style: { color: '#999', fontSize: 12 } }, (f.size || '') + (f.size && f.uploadTime ? ' · ' : '') + (f.uploadTime || '')) : null + ); + }) + ) + : '—' + ) + ) + ), + React.createElement(FormItemReadOnly, { label: '交车地点', value: mockContract.deliveryLocation }), + React.createElement(FormItemReadOnly, { label: '合同审批类型', value: mockContract.contractApprovalType || '—' }) + ); + var contractFormRow2 = React.createElement('div', { style: styles.formRow }, React.createElement(FormItemReadOnly, { label: '备注', fullWidth: true, value: mockContract.remarks || '—' })); + + var authorizedContent = React.createElement('div', null, + React.createElement('div', { style: { display: 'flex', gap: 12, alignItems: 'center', marginBottom: 8 } }, + React.createElement('span', { style: { flex: 1, fontWeight: 600, fontSize: 13 } }, '被授权人姓名'), + React.createElement('span', { style: { flex: 1, fontWeight: 600, fontSize: 13 } }, '被授权人联系电话'), + React.createElement('span', { style: { flex: 1, fontWeight: 600, fontSize: 13 } }, '被授权人身份证') + ), + mockAuthorized.map(function(item, index) { + return React.createElement('div', { key: index, style: { display: 'flex', gap: 12, alignItems: 'center', marginBottom: 12 } }, + React.createElement('div', { style: Object.assign({}, styles.input, styles.inputDisabled, { flex: 1 }) }, item.name), + React.createElement('div', { style: Object.assign({}, styles.input, styles.inputDisabled, { flex: 1 }) }, item.phone), + React.createElement('div', { style: Object.assign({}, styles.input, styles.inputDisabled, { flex: 1 }) }, item.idCard) + ); + }) + ); + + var rentalSummaryEl = React.createElement('div', { style: styles.summaryList }, + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '租赁车辆数'), React.createElement('span', { style: styles.summaryListValue }, mockRentalSummary.vehicleCount + ' 辆')), + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '租金及服务费合计'), React.createElement('span', { style: styles.summaryListValue }, mockRentalSummary.totalRentService + ' 元')), + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '保证金总额'), React.createElement('span', { style: styles.summaryListValue }, mockRentalSummary.totalDeposit + ' 元')), + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '氢气预付款金额'), React.createElement('span', { style: styles.summaryListValue }, mockRentalSummary.hydrogenPrepay + ' 元')) + ); + var Popover = window.antd && window.antd.Popover; + var renderServiceItemsPopover = function(row) { + var items = row.serviceItems || []; + if (items.length === 0) { + return React.createElement('div', { style: { padding: 12, minWidth: 320, fontSize: 13 } }, '暂无服务项明细'); + } + var thead = React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: styles.rentalTh }, '服务项目'), + React.createElement('th', { style: styles.rentalTh }, '费用'), + React.createElement('th', { style: styles.rentalTh }, '生效时间') + ) + ); + var tbody = React.createElement('tbody', null, + items.map(function(si, i) { + return React.createElement('tr', { key: i }, + React.createElement('td', { style: styles.rentalTd }, si.project || '—'), + React.createElement('td', { style: styles.rentalTd }, si.fee != null && si.fee !== '' ? si.fee + ' 元' : '—'), + React.createElement('td', { style: styles.rentalTd }, si.effectiveDate || '—') + ); + }) + ); + return React.createElement('div', { style: { padding: 8 } }, + React.createElement('table', { style: styles.rentalTable }, thead, tbody) + ); + }; + var rentalTableBody = mockRentalOrders.map(function(row, idx) { + var servicePopoverContent = renderServiceItemsPopover(row); + var serviceCell = Popover + ? React.createElement(Popover, { content: servicePopoverContent, title: '服务项明细', trigger: 'click' }, + React.createElement('span', { style: { color: '#1890ff', cursor: 'pointer', fontSize: 13 } }, '查看')) + : React.createElement('span', { + style: { color: '#1890ff', cursor: 'pointer', fontSize: 13 }, + onClick: function() { setServicePopoverRowNewVehicle(null); setServicePopoverRow(servicePopoverRow === idx ? null : idx); } + }, '查看'); + return React.createElement('tr', { key: idx }, + React.createElement('td', { style: styles.rentalTd }, idx + 1), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.brand)), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.model)), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.plateNo)), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.vehicleStatus || '—')), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.vin)), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.monthRent + ' 元')), + React.createElement('td', { style: styles.rentalTd }, serviceCell), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.serviceFee + ' 元')), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.deposit + ' 元')), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.remark || '—')) + ); + }); + var rentalTableEl = React.createElement('table', { style: styles.rentalTable }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: styles.rentalTh, width: 50 }, '序号'), + React.createElement('th', { style: styles.rentalTh, width: 100 }, '品牌'), + React.createElement('th', { style: styles.rentalTh, width: 100 }, '型号'), + React.createElement('th', { style: styles.rentalTh, width: 120 }, '车牌号'), + React.createElement('th', { style: styles.rentalTh, width: 160 }, '车辆状态'), + React.createElement('th', { style: styles.rentalTh, width: 160 }, '车辆识别代码'), + React.createElement('th', { style: styles.rentalTh, width: 120 }, '车辆月租金'), + React.createElement('th', { style: styles.rentalTh, width: 80 }, '服务费项目'), + React.createElement('th', { style: styles.rentalTh, width: 90 }, '服务费'), + React.createElement('th', { style: styles.rentalTh, width: 100 }, '保证金'), + React.createElement('th', { style: styles.rentalTh, width: 80 }, '备注') + ) + ), + React.createElement('tbody', null, rentalTableBody) + ); + var hydrogenReadOnly = React.createElement('div', { style: styles.formRow }, + React.createElement(FormItemReadOnly, { label: '氢费承担方', value: mockHydrogen.bearer }), + React.createElement(FormItemReadOnly, { label: '付款方式', value: mockHydrogen.paymentMethod }), + React.createElement(FormItemReadOnly, { label: '氢气预付款', value: mockHydrogen.hydrogenPrepay + ' 元' }), + React.createElement(FormItemReadOnly, { label: '退还车氢气单价', value: mockHydrogen.returnPrice + ' 元' }) + ); + var rentalContent = React.createElement('div', null, rentalSummaryEl, React.createElement('div', { style: { overflowX: 'auto', marginBottom: 16 } }, rentalTableEl), hydrogenReadOnly); + + var newVehicleSummaryEl = React.createElement('div', { style: styles.summaryList }, + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '租赁车辆数'), React.createElement('span', { style: styles.summaryListValue }, mockNewVehicleSummary.vehicleCount + ' 辆')), + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '租金及服务费合计'), React.createElement('span', { style: styles.summaryListValue }, mockNewVehicleSummary.totalRentService + ' 元')), + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '保证金总额'), React.createElement('span', { style: styles.summaryListValue }, mockNewVehicleSummary.totalDeposit + ' 元')) + ); + var newVehicleTableBody = mockNewVehicleOrders.map(function(row, idx) { + var servicePopoverContent = renderServiceItemsPopover(row); + var serviceCell = Popover + ? React.createElement(Popover, { content: servicePopoverContent, title: '服务项目', trigger: 'click' }, + React.createElement('span', { style: { color: '#1890ff', cursor: 'pointer', fontSize: 13 } }, '查看')) + : React.createElement('span', { + style: { color: '#1890ff', cursor: 'pointer', fontSize: 13 }, + onClick: function() { setServicePopoverRow(null); setServicePopoverRowNewVehicle(idx); } + }, '查看'); + return React.createElement('tr', { key: idx }, + React.createElement('td', { style: styles.rentalTd }, idx + 1), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.brand)), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.model)), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.plateNo)), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.vin)), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.monthRent + ' 元')), + React.createElement('td', { style: styles.rentalTd }, serviceCell), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.serviceFee + ' 元')), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.deposit + ' 元')), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.remark || '—')) + ); + }); + var newVehicleTableEl = React.createElement('table', { style: styles.rentalTable }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: styles.rentalTh, width: 50 }, '序号'), + React.createElement('th', { style: styles.rentalTh, width: 100 }, '品牌'), + React.createElement('th', { style: styles.rentalTh, width: 100 }, '型号'), + React.createElement('th', { style: styles.rentalTh, width: 120 }, '车牌号'), + React.createElement('th', { style: styles.rentalTh, width: 160 }, '车辆识别代码'), + React.createElement('th', { style: styles.rentalTh, width: 120 }, '车辆月租金'), + React.createElement('th', { style: styles.rentalTh, width: 80 }, '服务费项目'), + React.createElement('th', { style: styles.rentalTh, width: 90 }, '服务费'), + React.createElement('th', { style: styles.rentalTh, width: 100 }, '保证金'), + React.createElement('th', { style: styles.rentalTh, width: 80 }, '备注') + ) + ), + React.createElement('tbody', null, newVehicleTableBody) + ); + var newVehicleContent = React.createElement('div', null, newVehicleSummaryEl, React.createElement('div', { style: { overflowX: 'auto', marginBottom: 16 } }, newVehicleTableEl)); + + var feeContent = React.createElement('div', null, React.createElement('div', { style: styles.formRow }, React.createElement(FormItemReadOnly, { label: '选择费用模板', value: mockFeeTemplate })), feeTemplateBody); + + var billingContent = React.createElement('div', null, React.createElement('div', { style: { padding: '12px 16px', border: '1px solid #e8e8e8', borderRadius: 4, backgroundColor: '#fafafa', fontSize: 14, color: '#333' } }, mockBillingMethod)); + + var attachmentContent = React.createElement('div', { style: styles.card }, + React.createElement('div', { style: styles.cardHeader }, + React.createElement('span', { style: styles.cardTitle }, '盖章合同附件') + ), + React.createElement('div', { style: styles.cardBody }, + stampedContractFiles && stampedContractFiles.length + ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 10 } }, + stampedContractFiles.map(function(f, i) { + return React.createElement('div', { key: i, style: styles.attachmentRow }, + React.createElement('button', { + type: 'button', + style: Object.assign({}, styles.attachmentFile, stampedHoverIndex === i ? styles.attachmentFileHover : {}), + onMouseEnter: function() { setStampedHoverIndex(i); }, + onMouseLeave: function() { setStampedHoverIndex(null); }, + onClick: function() { window.open('#', '_blank'); } + }, '📄 ' + f.name, React.createElement('span', { style: styles.attachmentMeta }, f.size + ' · ' + f.uploadTime)) + ); + }) + ) + : React.createElement('div', { style: { color: '#999', fontSize: 13 } }, '暂无附件') + ) + ); + + var historyTableRows = changeHistorySorted.map(function(row, index) { + return React.createElement('tr', { key: index }, + React.createElement('td', { style: styles.rentalTd }, index + 1), + React.createElement('td', { style: styles.rentalTd }, row.changeTime), + React.createElement('td', { style: styles.rentalTd }, row.opType), + React.createElement('td', { style: styles.rentalTd }, row.operator), + React.createElement('td', { style: styles.rentalTd }, React.createElement('button', { type: 'button', style: styles.historyLink, onClick: function() { window.open('#', '_blank'); } }, '查看变更前记录')), + React.createElement('td', { style: Object.assign({}, styles.rentalTd, { verticalAlign: 'top', whiteSpace: 'pre-line' }) }, row.remark) + ); + }); + var historyTable = React.createElement('table', { style: styles.rentalTable }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: styles.rentalTh, width: 60 }, '序号'), + React.createElement('th', { style: styles.rentalTh, width: 140 }, '变更时间'), + React.createElement('th', { style: styles.rentalTh, width: 100 }, '操作类型'), + React.createElement('th', { style: styles.rentalTh, width: 90 }, '操作人'), + React.createElement('th', { style: styles.rentalTh, width: 120 }, '原始记录'), + React.createElement('th', { style: styles.rentalTh }, '备注') + ) + ), + React.createElement('tbody', null, historyTableRows) + ); + var changeHistoryContent = React.createElement('div', { style: styles.card }, + React.createElement('div', { style: styles.cardHeader }, + React.createElement('span', { style: styles.cardTitle }, '合同变更历史记录') + ), + React.createElement('div', { style: styles.cardBody }, + React.createElement('div', { style: { overflowX: 'auto' } }, historyTable) + ) + ); + + var requirementContent = '车辆租赁合同-查看(2026年3月3日版本)\n「数字化资产ONE-OS运管平台」中的「车辆租赁合同」-「查看租赁合同」模块,在车辆租赁合同操作列点击「查看」进行查看;\n1.面包屑:\n#业务管理-车辆租赁合同-查看合同\n\n3.客户基本信息卡片:\n#显示已审批合同实际信息,不可修改,当客户信息产生变更时,对该条历史合同数据不作更新,合同变更为三方合同时不在原有客户基本信息做修改,而是增加三方客户基本信息卡片;\n3.1.客户名称:从该条合同自动反查;\n3.2.客户统一信用代码:从该条合同自动反查;\n3.3.客户地址:从该条合同自动反查;\n3.4.客户联系人:从该条合同自动反查;\n3.5.客户电话:从该条合同自动反查;\n3.6.客户电子邮箱:从该条合同自动反查;\n3.7.企业名称:从该条合同自动反查;\n3.8.企业电话:从该条合同自动反查;\n3.9.邮寄地址:从该条合同自动反查;\n3.10.开户银行:从该条合同自动反查;\n3.11.银行账号:从该条合同自动反查;\n3.12.纳税人识别号:从该条合同自动反查;\n3.13.业务部门:从该条合同自动反查;\n3.14.业务负责人:从该条合同自动反查;\n\n4.丙方客户基本信息卡片(当合同转三方合同时显示):\n#显示已审批合同丙方实际信息,不可修改;;\n4.1.客户名称:从该条合同自动反查;\n4.2.客户统一信用代码:从该条合同自动反查;\n4.3.客户地址:从该条合同自动反查;\n4.4.客户联系人:从该条合同自动反查;\n4.5.客户电话:从该条合同自动反查;\n4.6.客户电子邮箱:从该条合同自动反查;\n4.7.企业名称:从该条合同自动反查;\n4.8.企业电话:从该条合同自动反查;\n4.9.邮寄地址:从该条合同自动反查;\n4.10.开户银行:从该条合同自动反查;\n4.11.银行账号:从该条合同自动反查;\n4.12.纳税人识别号:从该条合同自动反查;\n4.13.业务部门:从该条合同自动反查;\n4.14.业务负责人:从该条合同自动反查;\n\n5.合同基本信息卡片:\n#显示合同基本信息,不可修改;\n5.1.项目名称:从该条合同自动反查;\n5.2.合同编码:从该条合同自动反查,在新增租赁合同点击提交审核时生成;\n 合同编码由:[城市简写][合同类型][签约时间][业务部门代码][顺序流水号][签署状态]组成;\n 5.2.1.地区简写:如上海为SH,嘉兴为JX;\n 5.2.2.合同类型:采购合同为CG、租赁合同为ZL、自营合同为ZY;\n 5.2.3.签约时间:显示合同签约时间,如20260216\n 5.2.4.业务部门代码:显示合同签约业务部门信息,如YW1代表业务1部,YW2代表业务2部;\n 5.2.5.顺序流水号:实施5位编号补零规则,如01235,代表是集团编号为1235的合同;\n 5.2.6.签署状态:A为正式合同,B为试用合同;\n 如编号为:JXZL20260216YW101235A,则代表嘉兴业务1部在2026年2月16日签署的租赁正式合同,在集团中编号为1235,也可以理解为第1235份合同;\n 5.2.7.如果该合同为续签合同,则自动在新合同编码后额外添加:(续签自:旧合同合同编码xxx);\n 5.2.8.如果该合同为转正式合同,则自动在新合同编码后额外添加:(转正式合同自:旧合同合同编码xxx);\n5.3.合同类型:从该条合同自动反查;\n5.4.生效日期:从该条合同自动反查;\n5.5.付款方式:从该条合同自动反查;\n5.6.主要车型:从该条合同自动反查;\n5.7.结束日期:从该条合同自动反查;\n 5.7.1.合同结束日期前30天将以消息提醒方式提醒(消息中心、工作台);\n 5.7.2.到达合同结束日期时,租赁账单将会立刻停止计算,作为最后一期账单;\n 5.7.3.续签合同/转正式合同将重新生成账单,不会对旧合同账单做任何继承处理;\n5.8.付款周期:从该条合同自动反查;\n5.9.签约公司:从该条合同自动反查;\n5.10.交车区域:从该条合同自动反查;\n5.11.交车地点:从该条合同自动反查;\n5.12.合同审批类型:从该条合同自动反查,可选值「标准合同审批」「非标准合同审批」;\n5.13.合同原件:从该条合同自动反查,显示「合同原件名称.格式」「文件大小」「上传时间」,可能会存在多个附件,显示为一列,如果转为三方合同,则会显示新附件,显示「三方合同原件名称.格式」「文件大小」「上传时间」;\n5.14.备注:从该条合同自动反查;\n 5.14.1.如果该合同为续签合同,则自动在已填备注信息上方额外添加:续签自:旧合同合同编码xxx;\n 5.14.2.如果该合同为转正式合同,则自动在已填备注信息上方额外添加:转正式合同自:旧合同合同编码xxx;\n\n6.被授权人信息卡片:\n#显示所有被授权人信息,不可修改,如要新增被授权人,需要在列表中点击添加被授权人进行处理;\n6.1.被授权人:从该条合同自动反查;\n6.2.被授权人联系电话:从该条合同自动反查;\n6.3.被授权人身份证:从该条合同自动反查;\n\n7.租赁订单信息卡片:\n#显示租赁合同对应车辆明细费用、氢费明细费用等相关信息;\n7.1.上方为租赁车辆总计数据,包括租赁车辆数、租金及服务费合计、保证金总额、氢气预付款金额等相关信息;\n 7.1.1.租赁车辆数:显示下方租赁订单信息包含多少辆车;\n 7.1.2.租金及服务费合计:显示下方租赁订单信息中车辆租金总额及服务费总额;\n 7.1.3.保证金总额:显示下方租赁订单信息中车辆保证金总额;\n 7.1.4.氢气预付款金额:显示氢气预付款金额,如客户选择自行承担氢费或羚牛承担氢费,则该处为0不计入氢气预付款金额;\n7.2.下方为列表,显示序号、品牌、型号、车牌号、车辆识别代码、车辆月租金(元)、服务费项目、服务费、保证金、备注;\n 7.2.1.序号:从该条合同自动反查;\n 7.2.2.品牌:从该条合同自动反查;\n 7.2.3.型号:从该条合同自动反查;\n 7.2.4.车牌号:从该条合同自动反查;\n 7.2.5.车辆状态:分为已交车、已交车-临时替换、已交车-永久替换、待交车;\n 7.2.6.车辆识别代码:从该条合同自动反查;\n 7.2.7.车辆月租金:从该条合同自动反查;\n 7.2.7.服务费项目:点击查看按钮弹出卡片,卡片标题为:服务项目,下方列表显示服务项目、费用、生效时间;\n 7.2.7.1.服务项目:从该条合同自动反查;\n 7.2.7.2.费用:从该条合同自动反查;\n 7.2.7.3.生效时间:从该条合同自动反查;\n #在提车应收款中,车辆的交车日期和服务费生效日期可能会存在不同的情况,所以服务费需要单独以生效时间进行计算,例如:\n 交车后车辆从1月1日开始计费,付款周期为2个月,则提车应收款租金会计算到3月1日,但是服务费生效时间为1月30日,此情况下需要以3月1日-1月30日,计算出服务费具体收费天数为30天,然后根据:(服务费费用/30)* 服务费收费天数30)进行计算;\n 7.2.8.服务费:从该条合同自动反查;\n 7.2.9.保证金:从该条合同自动反查;\n 7.2.10.备注:从该条合同自动反查;\n7.3.氢费承担方:从该条合同自动反查;\n7.4.付款方式:从该条合同自动反查;\n7.5.氢气预付款:从该条合同自动反查;\n7.6.退还车氢气单价:从该条合同自动反查,该金额主要用于与客户约定还车时,与交车时氢气差值以此费用进行自动计算和结算;\n\n8.新增车辆信息卡片(用户每次为合同新增车辆时,都会生成一个新增车辆信息卡片):\n#显示租赁合同对应新增车辆的明细费用、氢费明细费用等相关信息;\n8.1.卡片标题显示为:新增车辆信息(YYYY-MM-DD)\n8.2.上方为租赁车辆总计数据,包括租赁车辆数、租金及服务费合计、保证金总额等相关信息;\n 8.2.1.租赁车辆数:显示下方租赁订单信息包含多少辆车;\n 8.2.2.租金及服务费合计:显示下方租赁订单信息中车辆租金总额及服务费总额;\n 8.2.3.保证金总额:显示下方租赁订单信息中车辆保证金总额;\n8.3.下方为列表,显示序号、品牌、型号、车牌号、车辆识别代码、车辆月租金(元)、服务费项目、服务费、保证金、备注;\n 8.3.1.序号:从该条合同自动反查;\n 8.3.2.品牌:从该条合同自动反查;\n 8.3.3.型号:从该条合同自动反查;\n 8.3.4.车牌号:从该条合同自动反查;\n 8.3.5.车辆识别代码:从该条合同自动反查;\n 8.3.6.车辆月租金:从该条合同自动反查;\n 8.3.7.服务费项目:点击查看按钮弹出卡片,卡片标题为:服务项目,下方列表显示服务项目、费用、生效时间;\n 8.3.7.1.服务项目:从该条合同自动反查;\n 8.3.7.2.费用:从该条合同自动反查;\n 8.3.7.3.生效时间:从该条合同自动反查;\n #在提车应收款中,车辆的交车日期和服务费生效日期可能会存在不同的情况,所以服务费需要单独以生效时间进行计算,例如:\n 交车后车辆从1月1日开始计费,付款周期为2个月,则提车应收款租金会计算到3月1日,但是服务费生效时间为1月30日,此情况下需要以3月1日-1月30日,计算出服务费具体收费天数为30天,然后根据:(服务费费用/30)* 服务费收费天数30)进行计算;\n 8.3.8.服务费:从该条合同自动反查;\n 8.3.9.保证金:从该条合同自动反查;\n 8.3.10.备注:从该条合同自动反查;\n\n9.其他费用信息卡片:\n#显示对应租赁费用模板证照补办费用、违约金费用、易损件费用、其他费用等信息;\n9.1.选择费用模板:必选项,从「租赁费用模板」中拉取,选择后自动将该费用模板所有环节费用显示在合同中;\n 9.1.1.证照补办费用:单独标题显示,内容为列表,列表中包括项目、收费标准、服务费,从该条合同自动反查;\n 9.1.2.违约金费用:单独标题显示,内容为列表,列表中包含项目、收费标准、服务费,从该条合同自动反查;\n 9.1.3.易损件信息:单独标题显示,内容为列表,列表中包含类别、损坏部位、配件、数量、费用明细,从该条合同自动反查;\n 9.1.4.其他费用信息:单独标题显示,内容为列表,列表中包含项目、收费标准、服务费,从该条合同自动反查;\n\n10.账单计算方式卡片:\n#显示实际结算方式;\n\n11.盖章合同附件:\n#显示财务审核完成后,由法务上传的盖章版合同文件,显示「盖章合同名称.格式」「文件大小」「上传时间」,可能会存在多个附件,显示为一列;如果转为三方合同,则会重新审批,法务上传显示新附件,显示「三方盖章合同名称.格式」「文件大小」「上传时间」\n\n12.合同变更历史记录:\n#显示合同变更历史,包括序号、变更时间、操作类型、操作人、原始记录、备注;\n12.1.序号:显示变更历史序号,按照变更时间倒序(从近到远)进行排序;\n12.2.变更时间:显示变更时间,格式为:YYYY-MM-DD HH:MM;\n12.3.操作类型:包括变更为三方合同、添加车辆、添加授权人、续签合同、撤回合同、终止合同、转正式合同、保存;\n12.4.操作人:显示对应操作类型操作人员;\n12.5.原始记录:显示查看变更前记录,点击打开新页面至查看原始合同快照;\n12.6.备注:备注改动内容,包括:\n 12.6.1.变更为三方合同时:增加丙方客户“客户名称xxxxxx”;\n 12.6.2.添加车辆时:添加车辆“车牌号xxx”、“车牌号xxx”、“车牌号xxx”,每辆车单独一行显示;\n 12.6.3.添加授权人时:添加授权人“授权人xxxx”、“授权人xxx”,每个授权人单独一行显示;\n 12.6.4.续签合同时:续签自”原合同编码xxxxxx“;\n 12.6.5.撤回合同时:主动撤回;\n 12.6.6.终止合同时:主动终止;\n 12.6.7.转正式合同时:转正式合同自“原合同编码xxxx”;\n 12.6.8.保存:无;\n\n2.审批状态:\n按照流程引擎设置节点显示;\n\n13.最下方为返回按钮,点击返回车辆租赁合同列表;\n\n所有卡片支持收起/展开功能,通过点击卡片右侧收起/展开实现;并增加锚点功能,锚点固定于页面右上角,点击锚点对应卡片名称,页面自动跳转至该卡片所在区域;'; + var reqSpecModalContent = reqSpecOpen ? React.createElement('div', { style: styles.modalMask, onClick: function(e) { if (e.target === e.currentTarget) setReqSpecOpen(false); } }, React.createElement('div', { style: Object.assign({}, styles.modalBox, { maxWidth: 720 }), onClick: function(e) { e.stopPropagation(); } }, React.createElement('div', { style: styles.modalHeader }, '需求说明'), React.createElement('div', { style: Object.assign({}, styles.modalBody, { maxHeight: '70vh', padding: '20px 24px', overflow: 'auto' }) }, React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6, color: 'rgba(0,0,0,0.85)' } }, requirementContent)), React.createElement('div', { style: { padding: '12px 20px', borderTop: '1px solid #f0f0f0', textAlign: 'right' } }, React.createElement('button', { style: Object.assign({}, styles.btn, styles.btnDefault), onClick: function() { setReqSpecOpen(false); } }, '关闭')))) : null; + + var servicePopoverRentalOpen = !Popover && servicePopoverRow !== null && mockRentalOrders[servicePopoverRow]; + var servicePopoverNewVehicleOpen = !Popover && servicePopoverRowNewVehicle !== null && mockNewVehicleOrders[servicePopoverRowNewVehicle]; + var servicePopoverContentCustom = (servicePopoverRentalOpen || servicePopoverNewVehicleOpen) + ? React.createElement('div', { style: styles.modalMask, onClick: function(e) { if (e.target === e.currentTarget) { setServicePopoverRow(null); setServicePopoverRowNewVehicle(null); } } }, + React.createElement('div', { style: Object.assign({}, styles.modalBox, { maxWidth: 480 }), onClick: function(e) { e.stopPropagation(); } }, + React.createElement('div', { style: styles.modalHeader }, '服务项目'), + React.createElement('div', { style: Object.assign({}, styles.modalBody, { padding: '16px 20px' }) }, servicePopoverRentalOpen ? renderServiceItemsPopover(mockRentalOrders[servicePopoverRow]) : renderServiceItemsPopover(mockNewVehicleOrders[servicePopoverRowNewVehicle])), + React.createElement('div', { style: { padding: '12px 20px', borderTop: '1px solid #f0f0f0', textAlign: 'right' } }, React.createElement('button', { style: Object.assign({}, styles.btn, styles.btnDefault), onClick: function() { setServicePopoverRow(null); setServicePopoverRowNewVehicle(null); } }, '关闭')) + )) + : null; + + return React.createElement('div', { style: styles.page }, + servicePopoverContentCustom, + React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 } }, React.createElement('div', { style: styles.breadcrumb }, React.createElement('span', null, '业务管理'), React.createElement('span', { style: styles.breadcrumbSep }, ' / '), React.createElement('span', null, '车辆租赁合同'), React.createElement('span', { style: styles.breadcrumbSep }, ' / '), React.createElement('span', { style: { color: '#1890ff' } }, '查看合同')), React.createElement('span', { style: { color: '#1890ff', cursor: 'pointer', fontSize: 14 }, onClick: function() { setReqSpecOpen(true); } }, '查看需求说明')), + React.createElement('div', { style: styles.anchorWrap }, + React.createElement('div', { style: { marginBottom: 8, fontWeight: 600, fontSize: 14 } }, '锚点导航'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-customer'); } }, '客户基本信息'), + isThreePartyContract ? React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-third-party'); } }, '丙方客户基本信息') : null, + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-contract'); } }, '合同基本信息'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-authorized'); } }, '被授权人信息'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-rental'); } }, '租赁订单信息'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-new-vehicle'); } }, '新增车辆信息(' + addVehicleDate + ')'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-fee'); } }, '其他费用信息'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-billing'); } }, '账单计算方式'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-attachment'); } }, '盖章合同附件'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-history'); } }, '合同变更历史记录') + ), + React.createElement('div', { id: 'card-customer' }, React.createElement(CardBlock, { id: 'card-customer', title: '客户基本信息', collapsed: cc1, setCollapsed: setCc1 }, customerFields)), + isThreePartyContract ? React.createElement('div', { id: 'card-third-party', style: { marginTop: 16 } }, React.createElement(CardBlock, { id: 'card-third-party', title: '丙方客户基本信息', collapsed: cc1b, setCollapsed: setCc1b }, thirdPartyCustomerFields)) : null, + React.createElement('div', { id: 'card-contract', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '合同基本信息', collapsed: cc2, setCollapsed: setCc2 }, React.createElement('div', null, contractFormRow1, contractFormRow2))), + React.createElement('div', { id: 'card-authorized', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '被授权人信息', collapsed: cc3, setCollapsed: setCc3 }, authorizedContent)), + React.createElement('div', { id: 'card-rental', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '租赁订单信息', collapsed: cc4, setCollapsed: setCc4 }, rentalContent)), + React.createElement('div', { id: 'card-new-vehicle', style: { marginTop: 16 } }, React.createElement(CardBlock, { id: 'card-new-vehicle', title: '新增车辆信息(' + addVehicleDate + ')', collapsed: cc4b, setCollapsed: setCc4b }, newVehicleContent)), + React.createElement('div', { id: 'card-fee', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '其他费用信息', collapsed: cc5, setCollapsed: setCc5 }, feeContent)), + React.createElement('div', { id: 'card-billing', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '账单计算方式', collapsed: cc6, setCollapsed: setCc6 }, billingContent)), + React.createElement('div', { id: 'card-attachment', style: { marginTop: 16 } }, attachmentContent), + React.createElement('div', { id: 'card-history', style: { marginTop: 16 } }, changeHistoryContent), + approvalCardEl, + React.createElement('div', { style: { height: 60 } }), + reqSpecModalContent, + React.createElement('div', { style: styles.footer }, React.createElement('button', { style: Object.assign({}, styles.btn, styles.btnDefault) }, '返回')) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-lease-contract/pages/03-车辆租赁合同-附加费用.jsx b/src/prototypes/oneos-web-lease-contract/pages/03-车辆租赁合同-附加费用.jsx new file mode 100644 index 0000000..5e9310c --- /dev/null +++ b/src/prototypes/oneos-web-lease-contract/pages/03-车辆租赁合同-附加费用.jsx @@ -0,0 +1,358 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 车辆租赁合同 - 附加费用(参照新增合同,除服务费项目-管理弹层可编辑外其余只读+样例数据) + +const Component = function() { + var antd = window.antd; + var Input = antd.Input; + var Select = antd.Select; + var Button = antd.Button; + var DatePicker = antd.DatePicker; + var message = antd.message; + var Option = Select.Option; + + // 样例数据(只读展示) + var detail = React.useMemo(function() { + return { + customerName: '嘉兴某某物流有限公司', + creditCode: '91330400MA2XXXXX1', + address: '浙江省嘉兴市南湖区科技大道1号', + contact: '张三', + phone: '13800138001', + email: 'zhangsan@example.com', + companyName: '嘉兴某某物流有限公司', + companyPhone: '0571-88888888', + mailingAddress: '浙江省嘉兴市南湖区科技大道1号', + bank: '中国工商银行嘉兴分行', + bankAccount: '6222021234567890123', + taxId: '91330400MA2XXXXX1', + businessDept: '业务1部', + businessOwner: '张经理', + projectName: '嘉兴城配运输项目', + contractType: '正式合同', + effectiveDate: '2026-02-16', + paymentMethod: '预付', + mainVehicleModels: '型号A1、型号B1', + endDate: '2027-02-16', + paymentPeriod: '2', + signingCompany: '嘉兴羚牛', + deliveryRegion: '浙江省 / 嘉兴市', + deliveryLocation: '嘉兴市南湖区科技大道1号', + contractApprovalType: '标准合同审批', + contractOriginalName: '租赁合同-嘉兴某某物流.pdf', + remarks: '首年优惠', + authorizedList: [ + { name: '张三', phone: '13800138001', idCard: '330102199001011234' }, + { name: '李四', phone: '13800138002', idCard: '330102199002022345' } + ], + feeTemplate: '标准费用模板A', + billingMethod: 'month', + hydrogenBearer: '客户', + hydrogenPaymentMethod: '预付', + hydrogenPrepay: '5000.00', + returnHydrogenPrice: '12.50', + rentalOrders: [ + { id: 1, brand: '品牌A', model: '型号A1', plateNo: '浙A10001', vin: 'L1234567890ABCDEF', monthRent: '8000', serviceItems: [{ project: '保养费用', fee: '200.00', effectiveDate: '2026-03-01' }, { project: '清洗费', fee: '80.00', effectiveDate: '2026-03-01' }], deposit: '10000', remark: '' }, + { id: 2, brand: '品牌B', model: '型号B1', plateNo: '沪A30003', vin: 'L3234567890ABCDEF', monthRent: '7500', serviceItems: [{ project: 'ETC-客', fee: '50.00', effectiveDate: '2026-02-20' }], deposit: '8000', remark: '试用车' } + ] + }; + }, []); + + var rentalOrdersInit = detail.rentalOrders.map(function(r) { + return { id: r.id, brand: r.brand, model: r.model, plateNo: r.plateNo, vin: r.vin, monthRent: r.monthRent, serviceItems: (r.serviceItems || []).map(function(si) { return { project: si.project || '', fee: si.fee || '', effectiveDate: si.effectiveDate || '' }; }), deposit: r.deposit, remark: r.remark || '' }; + }); + var os1 = React.useState(rentalOrdersInit); + var rentalOrders = os1[0]; + var setRentalOrders = os1[1]; + var os1b = React.useState(null); + var serviceModalRowIndex = os1b[0]; + var setServiceModalRowIndex = os1b[1]; + + var serviceItemOptions = ['代处理费用', '罚款', '违章处理违约金', '未参加安全培训', '车辆出险', '年检年审违约', '停车费', '设备损坏金(包含易损件)', '清洗费', '上门收车人工费', '上门收车送车行驶费', '上门收车基础服务费', '保险上浮', '保养费用', '补办驾驶证', '补办牌照', '补办营运证', '补办加氢证', '借用备用钥匙', '补配钥匙', '租金', '氢气费-客', '退还车氢量差', '能源费补缴', '能源费退款', '送车上门人工费', '送车上门送车行驶费', '送车上门基础服务费', '保证金', '氢气预付费', '维修费用', 'ETC-客', 'ETC卡缺损费', 'ETC设备缺损费', '电费-客', '未结算保养费', '未结算维修费', '车损费', '工具损坏或丢失费', '证件费', '广告损坏费', '送车服务费', '接车服务费', '补办行驶证', '超赔险', '轮胎磨损费', '无忧包', '轮胎保', '养护保', '尾板']; + + var calcRowServiceFee = function(row) { + var sum = 0; + for (var i = 0; i < (row.serviceItems || []).length; i++) { + var fee = parseFloat(row.serviceItems[i].fee); + if (!isNaN(fee)) sum += fee; + } + return sum.toFixed(2); + }; + var rentalTotalVehicles = rentalOrders.length; + var rentalTotalRentService = rentalOrders.reduce(function(s, r) { return s + (parseFloat(r.monthRent) || 0) + (parseFloat(calcRowServiceFee(r)) || 0); }, 0); + var rentalTotalDeposit = rentalOrders.reduce(function(s, r) { return s + (parseFloat(r.deposit) || 0); }, 0); + var rentalTotalHydrogen = detail.hydrogenBearer === '客户' && detail.hydrogenPaymentMethod === '预付' ? (parseFloat(detail.hydrogenPrepay) || 0) : 0; + + var openServiceModal = function(index) { setServiceModalRowIndex(index); }; + var closeServiceModal = function() { setServiceModalRowIndex(null); }; + var addServiceItem = function() { + if (serviceModalRowIndex === null) return; + var next = rentalOrders.slice(0); + var row = next[serviceModalRowIndex]; + var items = (row.serviceItems || []).concat([{ project: '', fee: '', effectiveDate: '' }]); + var newRow = {}; + for (var k in row) { if (row.hasOwnProperty(k) && k !== 'serviceItems') newRow[k] = row[k]; } + newRow.serviceItems = items; + next[serviceModalRowIndex] = newRow; + setRentalOrders(next); + }; + var removeServiceItem = function(siIndex) { + if (serviceModalRowIndex === null) return; + var next = rentalOrders.slice(0); + var row = next[serviceModalRowIndex]; + var items = (row.serviceItems || []).slice(0); + items.splice(siIndex, 1); + if (items.length === 0) items = [{ project: '', fee: '', effectiveDate: '' }]; + var newRow = {}; + for (var k in row) { if (row.hasOwnProperty(k) && k !== 'serviceItems') newRow[k] = row[k]; } + newRow.serviceItems = items; + next[serviceModalRowIndex] = newRow; + setRentalOrders(next); + }; + var updateServiceItem = function(siIndex, field, value) { + if (serviceModalRowIndex === null) return; + var next = rentalOrders.slice(0); + var row = next[serviceModalRowIndex]; + var items = (row.serviceItems || []).slice(0); + var item = items[siIndex] || {}; + var newItem = {}; + for (var k in item) { if (item.hasOwnProperty(k)) newItem[k] = item[k]; } + newItem[field] = value; + items[siIndex] = newItem; + var newRow = {}; + for (var rk in row) { if (row.hasOwnProperty(rk) && rk !== 'serviceItems') newRow[rk] = row[rk]; } + newRow.serviceItems = items; + next[serviceModalRowIndex] = newRow; + setRentalOrders(next); + }; + + var cc1 = React.useState(false)[0]; + var setCc1 = React.useState(false)[1]; + var cc2 = React.useState(false)[0]; + var setCc2 = React.useState(false)[1]; + var cc3 = React.useState(false)[0]; + var setCc3 = React.useState(false)[1]; + var cc4 = React.useState(false)[0]; + var setCc4 = React.useState(false)[1]; + var cc5 = React.useState(false)[0]; + var setCc5 = React.useState(false)[1]; + var cc6 = React.useState(false)[0]; + var setCc6 = React.useState(false)[1]; + + var styles = { + page: { padding: '16px 24px 48px', backgroundColor: '#f5f5f5', minHeight: '100vh', fontFamily: '"PingFang SC", "苹方-简", -apple-system, BlinkMacSystemFont, "Microsoft YaHei", sans-serif', fontSize: 14 }, + breadcrumb: { marginBottom: 16, color: '#666' }, + breadcrumbSep: { margin: '0 8px', color: '#999' }, + anchorWrap: { position: 'fixed', top: 80, right: 24, zIndex: 100, backgroundColor: '#fff', borderRadius: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.12)', padding: '12px 16px', minWidth: 160 }, + anchorItem: { display: 'block', padding: '6px 0', color: '#1890ff', cursor: 'pointer', border: 'none', background: 'none', width: '100%', textAlign: 'left', fontSize: 13 }, + card: { backgroundColor: '#fff', borderRadius: 8, marginBottom: 16, boxShadow: '0 1px 2px rgba(0,0,0,0.05)', overflow: 'hidden' }, + cardHeader: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '16px 20px', borderBottom: '1px solid #f0f0f0', cursor: 'pointer' }, + cardTitle: { fontSize: 16, fontWeight: 600, color: '#333' }, + cardToggle: { color: '#999', fontSize: 14 }, + cardBody: { padding: '20px 24px' }, + formRow: { display: 'flex', flexWrap: 'wrap', marginBottom: 16 }, + formCol: { flex: '0 0 33.33%', minWidth: 200, paddingRight: 16, marginBottom: 8 }, + formColFull: { flex: '0 0 100%', marginBottom: 8 }, + label: { display: 'block', marginBottom: 6, color: '#333' }, + inputDisabled: { width: '100%', padding: '8px 12px', border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 14, backgroundColor: '#f5f5f5', color: 'rgba(0,0,0,0.65)' }, + summaryList: { marginBottom: 16, display: 'flex', flexWrap: 'wrap', gap: 16 }, + summaryListItem: { flex: '0 0 calc(50% - 8px)', display: 'flex', alignItems: 'center', padding: '12px 16px', border: '1px solid #e8e8e8', borderRadius: 4, backgroundColor: '#fafafa', fontSize: 14, boxSizing: 'border-box' }, + summaryListLabel: { flex: '0 0 140px', color: '#666' }, + summaryListValue: { flex: 1, fontWeight: 600, color: '#333' }, + rentalTable: { width: '100%', borderCollapse: 'collapse', fontSize: 13 }, + rentalTh: { padding: '10px 8px', textAlign: 'left', borderBottom: '1px solid #e8e8e8', backgroundColor: '#fafafa', fontWeight: 600 }, + rentalTd: { padding: '8px', borderBottom: '1px solid #f0f0f0', verticalAlign: 'top' }, + rentalTdCenter: { padding: '8px', borderBottom: '1px solid #f0f0f0', verticalAlign: 'middle' }, + rentalInput: { width: '100%', padding: '6px 10px', border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 13 }, + rentalInputDisabled: { backgroundColor: '#f5f5f5', color: '#999' }, + modalMask: { position: 'fixed', left: 0, top: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.45)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center' }, + modalBox: { backgroundColor: '#fff', borderRadius: 8, width: '90%', maxWidth: 720, maxHeight: '85vh', overflow: 'hidden', display: 'flex', flexDirection: 'column', boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }, + modalHeader: { padding: '16px 20px', borderBottom: '1px solid #f0f0f0', fontSize: 16, fontWeight: 600 }, + modalBody: { padding: 20, overflow: 'auto', flex: 1 }, + footer: { position: 'fixed', bottom: 0, left: 0, right: 0, padding: '12px 24px', backgroundColor: '#fff', borderTop: '1px solid #e8e8e8', display: 'flex', gap: 12, justifyContent: 'flex-start', zIndex: 99 } + }; + + var scrollToCard = function(id) { + var el = document.getElementById(id); + if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }; + + var CardBlock = function(props) { + return React.createElement('div', { id: props.id, style: styles.card }, + React.createElement('div', { style: styles.cardHeader, onClick: function() { props.setCollapsed(!props.collapsed); } }, + React.createElement('span', { style: styles.cardTitle }, props.title), + React.createElement('span', { style: styles.cardToggle }, props.collapsed ? '展开' : '收起') + ), + !props.collapsed ? React.createElement('div', { style: styles.cardBody }, props.children) : null + ); + }; + + var FormItem = function(props) { + var colStyle = props.fullWidth ? styles.formColFull : styles.formCol; + return React.createElement('div', { style: colStyle }, + React.createElement('label', { style: styles.label }, props.label), + props.children + ); + }; + + var customerFields = React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '客户名称' }, React.createElement(Input, { value: detail.customerName, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '客户统一信用代码' }, React.createElement(Input, { value: detail.creditCode, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '客户地址' }, React.createElement(Input, { value: detail.address, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '客户联系人' }, React.createElement(Input, { value: detail.contact, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '客户电话' }, React.createElement(Input, { value: detail.phone, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '客户电子邮箱' }, React.createElement(Input, { value: detail.email, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '企业名称' }, React.createElement(Input, { value: detail.companyName, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '企业电话' }, React.createElement(Input, { value: detail.companyPhone, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '邮寄地址' }, React.createElement(Input, { value: detail.mailingAddress, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '开户银行' }, React.createElement(Input, { value: detail.bank, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '银行账号' }, React.createElement(Input, { value: detail.bankAccount, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '纳税人识别号' }, React.createElement(Input, { value: detail.taxId, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '业务部门' }, React.createElement(Input, { value: detail.businessDept, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '业务负责人' }, React.createElement(Input, { value: detail.businessOwner, disabled: true, style: styles.inputDisabled })) + ); + + var contractFormRow1 = React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '项目名称' }, React.createElement(Input, { value: detail.projectName, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '合同类型' }, React.createElement(Input, { value: detail.contractType, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '生效日期' }, React.createElement(Input, { value: detail.effectiveDate, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '付款方式' }, React.createElement(Input, { value: detail.paymentMethod, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '主要车型' }, React.createElement(Input, { value: detail.mainVehicleModels, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '结束日期' }, React.createElement(Input, { value: detail.endDate, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '付款周期' }, React.createElement(Input, { value: detail.paymentPeriod + '个月', disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '签约公司' }, React.createElement(Input, { value: detail.signingCompany, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '交车区域' }, React.createElement(Input, { value: detail.deliveryRegion, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '交车地点' }, React.createElement(Input, { value: detail.deliveryLocation, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '合同审批类型' }, React.createElement(Input, { value: detail.contractApprovalType || '—', disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '合同原件' }, React.createElement(Input, { value: detail.contractOriginalName, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '备注', fullWidth: true }, React.createElement(Input.TextArea, { value: detail.remarks, disabled: true, style: { width: '100%', minHeight: 80, backgroundColor: '#f5f5f5' } })) + ); + + var authorizedContent = React.createElement('div', null, + detail.authorizedList.map(function(item, index) { + return React.createElement('div', { key: index, style: { display: 'flex', gap: 12, alignItems: 'flex-start', marginBottom: 12 } }, + React.createElement(Input, { value: item.name, disabled: true, style: Object.assign({}, styles.rentalInput, styles.rentalInputDisabled, { flex: 1 }) }), + React.createElement(Input, { value: item.phone, disabled: true, style: Object.assign({}, styles.rentalInput, styles.rentalInputDisabled, { flex: 1 }) }), + React.createElement(Input, { value: item.idCard, disabled: true, style: Object.assign({}, styles.rentalInput, styles.rentalInputDisabled, { flex: 1 }) }) + ); + }) + ); + + var rentalTableBody = rentalOrders.map(function(row, idx) { + return React.createElement('tr', { key: row.id != null ? row.id : idx }, + React.createElement('td', { style: styles.rentalTdCenter }, idx + 1), + React.createElement('td', { style: styles.rentalTd }, React.createElement(Input, { value: row.brand || '', disabled: true, style: Object.assign({}, styles.rentalInput, styles.rentalInputDisabled) })), + React.createElement('td', { style: styles.rentalTd }, React.createElement(Input, { value: row.model || '', disabled: true, style: Object.assign({}, styles.rentalInput, styles.rentalInputDisabled) })), + React.createElement('td', { style: styles.rentalTd }, React.createElement(Input, { value: row.plateNo || '', disabled: true, style: Object.assign({}, styles.rentalInput, styles.rentalInputDisabled) })), + React.createElement('td', { style: styles.rentalTd }, React.createElement(Input, { value: row.vin || '', disabled: true, style: Object.assign({}, styles.rentalInput, styles.rentalInputDisabled) })), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: { display: 'flex', alignItems: 'center' } }, React.createElement(Input, { value: row.monthRent || '', disabled: true, style: Object.assign({}, styles.rentalInput, styles.rentalInputDisabled) }), React.createElement('span', { style: { marginLeft: 4 } }, '元'))), + React.createElement('td', { style: styles.rentalTdCenter }, React.createElement(Button, { type: 'link', size: 'small', onClick: function() { openServiceModal(idx); } }, '管理')), + React.createElement('td', { style: styles.rentalTdCenter }, calcRowServiceFee(row) + ' 元'), + React.createElement('td', { style: styles.rentalTd }, React.createElement(Input, { value: row.deposit || '', disabled: true, style: Object.assign({}, styles.rentalInput, styles.rentalInputDisabled) })), + React.createElement('td', { style: styles.rentalTd }, React.createElement(Input, { value: row.remark || '', disabled: true, style: Object.assign({}, styles.rentalInput, styles.rentalInputDisabled) })) + ); + }); + + var rentalSummary = React.createElement('div', { style: styles.summaryList }, + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '租赁车辆数'), React.createElement('span', { style: styles.summaryListValue }, rentalTotalVehicles + ' 辆')), + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '租金及服务费合计'), React.createElement('span', { style: styles.summaryListValue }, rentalTotalRentService.toFixed(2) + ' 元')), + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '保证金总额'), React.createElement('span', { style: styles.summaryListValue }, rentalTotalDeposit.toFixed(2) + ' 元')), + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '氢气预付款金额'), React.createElement('span', { style: styles.summaryListValue }, rentalTotalHydrogen.toFixed(2) + ' 元')) + ); + + var rentalTableThead = React.createElement('thead', null, React.createElement('tr', null, + React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 50 }) }, '序号'), + React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 100 }) }, '品牌'), + React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 100 }) }, '型号'), + React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 120 }) }, '车牌号'), + React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 160 }) }, '车辆识别代码'), + React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 120 }) }, '车辆月租金'), + React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 80 }) }, '服务费项目'), + React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 90 }) }, '服务费'), + React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 100 }) }, '保证金'), + React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 80 }) }, '备注') + )); + var rentalTableEl = React.createElement('table', { style: styles.rentalTable }, rentalTableThead, React.createElement('tbody', null, rentalTableBody)); + var rentalContent = React.createElement('div', null, + rentalSummary, + React.createElement('div', { style: { overflowX: 'auto', marginBottom: 16 } }, rentalTableEl), + React.createElement('div', { style: Object.assign({}, styles.formRow, { marginTop: 20 }) }, + React.createElement(FormItem, { label: '氢费承担方' }, React.createElement(Input, { value: detail.hydrogenBearer, disabled: true, style: styles.inputDisabled })), + detail.hydrogenBearer === '客户' ? React.createElement(FormItem, { label: '付款方式' }, React.createElement(Input, { value: detail.hydrogenPaymentMethod, disabled: true, style: styles.inputDisabled })) : null, + detail.hydrogenPaymentMethod === '预付' && detail.hydrogenBearer === '客户' ? React.createElement(FormItem, { label: '氢气预付款' }, React.createElement(Input, { value: detail.hydrogenPrepay, disabled: true, style: styles.inputDisabled })) : null, + React.createElement(FormItem, { label: '退还车氢气单价' }, React.createElement(Input, { value: detail.returnHydrogenPrice, disabled: true, style: styles.inputDisabled })) + ) + ); + + var feeContent = React.createElement('div', null, + React.createElement(FormItem, { label: '选择费用模板' }, React.createElement(Input, { value: detail.feeTemplate, disabled: true, style: styles.inputDisabled })) + ); + + var billingLabel = detail.billingMethod === 'month' ? '按自然月结算' : '按付款周期天数结算'; + var billingContent = React.createElement('div', null, React.createElement(FormItem, { label: '账单计算方式' }, React.createElement(Input, { value: billingLabel, disabled: true, style: styles.inputDisabled }))); + + var serviceModalRows = serviceModalRowIndex !== null && rentalOrders[serviceModalRowIndex] ? rentalOrders[serviceModalRowIndex].serviceItems : []; + var serviceModalContent = serviceModalRowIndex !== null ? React.createElement('div', { style: styles.modalMask, onClick: function(e) { if (e.target === e.currentTarget) closeServiceModal(); } }, + React.createElement('div', { style: styles.modalBox, onClick: function(e) { e.stopPropagation(); } }, + React.createElement('div', { style: styles.modalHeader }, '服务项目'), + React.createElement('div', { style: styles.modalBody }, + React.createElement('table', { style: styles.rentalTable }, + React.createElement('thead', null, React.createElement('tr', null, + React.createElement('th', { style: styles.rentalTh }, '服务项目'), + React.createElement('th', { style: styles.rentalTh }, '费用'), + React.createElement('th', { style: styles.rentalTh }, '生效时间'), + React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 80 }) }, '操作') + )), + React.createElement('tbody', null, serviceModalRows.map(function(si, siIdx) { + return React.createElement('tr', { key: siIdx }, + React.createElement('td', { style: styles.rentalTd }, React.createElement(Select, { style: { width: '100%' }, placeholder: '请选择服务项目', value: si.project || undefined, onChange: function(v) { updateServiceItem(siIdx, 'project', v || ''); }, showSearch: true, filterOption: function(input, opt) { return opt && opt.children && String(opt.children).toLowerCase().indexOf((input || '').toLowerCase()) >= 0; }, allowClear: true }, serviceItemOptions.map(function(opt, oi) { return React.createElement(Option, { key: oi, value: opt }, opt); }))), + React.createElement('td', { style: styles.rentalTd }, React.createElement(Input, { placeholder: '0.00', value: si.fee || '', onChange: function(e) { updateServiceItem(siIdx, 'fee', e.target.value); }, addonAfter: '元', style: { width: '100%' } })), + React.createElement('td', { style: styles.rentalTd }, React.createElement(DatePicker, { style: { width: '100%' }, format: 'YYYY-MM-DD', placeholder: '请选择生效时间', value: si.effectiveDate && window.moment ? window.moment(si.effectiveDate, 'YYYY-MM-DD') : null, onChange: function(d, dateStr) { updateServiceItem(siIdx, 'effectiveDate', dateStr || ''); } })), + React.createElement('td', { style: styles.rentalTd }, React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function() { removeServiceItem(siIdx); } }, '删除')) + ); + })) + ), + React.createElement(Button, { type: 'dashed', style: { marginTop: 12, width: '100%' }, onClick: addServiceItem }, '添加一行') + ), + React.createElement('div', { style: { padding: '12px 20px', borderTop: '1px solid #f0f0f0', display: 'flex', gap: 12, justifyContent: 'flex-end' } }, + React.createElement(Button, { type: 'primary', onClick: function() { closeServiceModal(); } }, '保存'), + React.createElement(Button, { onClick: closeServiceModal }, '关闭') + ) + ) + ) : null; + + var handleSubmit = function() { + message.success('已提交审核(原型)'); + if (typeof window !== 'undefined' && window.history) window.history.back(); + }; + var handleBack = function() { + if (typeof window !== 'undefined' && window.history) window.history.back(); + else message.info('返回'); + }; + + return React.createElement('div', { style: styles.page }, + React.createElement('div', { style: { marginBottom: 16 } }, React.createElement('div', { style: styles.breadcrumb }, React.createElement('span', null, '业务管理'), React.createElement('span', { style: styles.breadcrumbSep }, ' / '), React.createElement('span', null, '车辆租赁合同'), React.createElement('span', { style: styles.breadcrumbSep }, ' / '), React.createElement('span', { style: { color: '#1890ff' } }, '附加费用'))), + React.createElement('div', { style: styles.anchorWrap }, + React.createElement('div', { style: { marginBottom: 8, fontWeight: 600, fontSize: 14 } }, '锚点导航'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-customer'); } }, '客户基本信息'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-contract'); } }, '合同基本信息'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-authorized'); } }, '被授权人信息'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-rental'); } }, '租赁订单信息'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-fee'); } }, '其他费用信息'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-billing'); } }, '账单计算方式') + ), + React.createElement('div', { id: 'card-customer' }, React.createElement(CardBlock, { id: 'card-customer', title: '客户基本信息', collapsed: cc1, setCollapsed: setCc1 }, customerFields)), + React.createElement('div', { id: 'card-contract', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '合同基本信息', collapsed: cc2, setCollapsed: setCc2 }, contractFormRow1)), + React.createElement('div', { id: 'card-authorized', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '被授权人信息', collapsed: cc3, setCollapsed: setCc3 }, authorizedContent)), + React.createElement('div', { id: 'card-rental', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '租赁订单信息', collapsed: cc4, setCollapsed: setCc4 }, rentalContent)), + React.createElement('div', { id: 'card-fee', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '其他费用信息', collapsed: cc5, setCollapsed: setCc5 }, feeContent)), + React.createElement('div', { id: 'card-billing', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '账单计算方式', collapsed: cc6, setCollapsed: setCc6 }, billingContent)), + React.createElement('div', { style: { height: 60 } }), + serviceModalContent, + React.createElement('div', { style: styles.footer }, + React.createElement(Button, { type: 'primary', onClick: handleSubmit }, '提交审核'), + React.createElement(Button, { onClick: handleBack }, '返回') + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-lease-contract/pages/04-车辆租赁合同-新增.jsx b/src/prototypes/oneos-web-lease-contract/pages/04-车辆租赁合同-新增.jsx new file mode 100644 index 0000000..66100cb --- /dev/null +++ b/src/prototypes/oneos-web-lease-contract/pages/04-车辆租赁合同-新增.jsx @@ -0,0 +1,815 @@ +// 【重要】必须使用 const Component 作为组件变量名 - Axhub 产品原型 +// 车辆管理系统 - 新增租赁合同模块(整体实现,IE11+ 兼容,采用 antd) + +const Component = function() { + var antd = window.antd; + var Input = antd.Input; + var Select = antd.Select; + var Button = antd.Button; + var DatePicker = antd.DatePicker; + var Modal = antd.Modal; + var message = antd.message; + var Option = Select.Option; + var Row = antd.Row; + var Col = antd.Col; + + var cs1 = React.useState(''); + var customerSearch = cs1[0]; + var setCustomerSearch = cs1[1]; + var cs2 = React.useState(null); + var selectedCustomer = cs2[0]; + var setSelectedCustomer = cs2[1]; + var cs3 = React.useState(false); + var customerDropdownOpen = cs3[0]; + var setCustomerDropdownOpen = cs3[1]; + var cs4 = React.useState(''); + var businessDept = cs4[0]; + var setBusinessDept = cs4[1]; + var cs5 = React.useState(''); + var businessOwner = cs5[0]; + var setBusinessOwner = cs5[1]; + var cs6 = React.useState(''); + var ownerFocusError = cs6[0]; + var setOwnerFocusError = cs6[1]; + var contractOriginalRef = React.useRef(null); + var csContractOriginal = React.useState(null); + var contractOriginal = csContractOriginal[0]; + var setContractOriginal = csContractOriginal[1]; + + var bs1 = React.useState(''); + var projectName = bs1[0]; + var setProjectName = bs1[1]; + var bs2 = React.useState(''); + var contractType = bs2[0]; + var setContractType = bs2[1]; + var bs3 = React.useState('2026-02-16'); + var effectiveDate = bs3[0]; + var setEffectiveDate = bs3[1]; + var bs4 = React.useState(''); + var paymentMethod = bs4[0]; + var setPaymentMethod = bs4[1]; + var bs6 = React.useState('2027-02-16'); + var endDate = bs6[0]; + var setEndDate = bs6[1]; + var bs7 = React.useState('1'); + var paymentPeriod = bs7[0]; + var setPaymentPeriod = bs7[1]; + var bs8 = React.useState('嘉兴羚牛'); + var signingCompany = bs8[0]; + var setSigningCompany = bs8[1]; + var bs9 = React.useState(''); + var deliveryProvince = bs9[0]; + var setDeliveryProvince = bs9[1]; + var bs10 = React.useState(''); + var deliveryCity = bs10[0]; + var setDeliveryCity = bs10[1]; + var bs10b = React.useState(false); + var deliveryRegionOpen = bs10b[0]; + var setDeliveryRegionOpen = bs10b[1]; + var deliveryRegionClickInsideRef = React.useRef(false); + var nextRentalRowIdRef = React.useRef(2); + var bs11 = React.useState(''); + var deliveryLocation = bs11[0]; + var setDeliveryLocation = bs11[1]; + var bs11a = React.useState(''); + var contractApprovalType = bs11a[0]; + var setContractApprovalType = bs11a[1]; + var bs12 = React.useState(''); + var remarks = bs12[0]; + var setRemarks = bs12[1]; + + var as1 = React.useState([{ name: '', phone: '', idCard: '' }]); + var authorizedList = as1[0]; + var setAuthorizedList = as1[1]; + + var emptyRentalRow = { id: 1, brand: '', model: '', plateNo: '', vin: '', monthRent: '', serviceItems: [{ project: '', fee: '', effectiveDate: '' }], deposit: '', remark: '' }; + var os1 = React.useState([emptyRentalRow]); + var rentalOrders = os1[0]; + var setRentalOrders = os1[1]; + var os1b = React.useState(null); + var serviceModalRowIndex = os1b[0]; + var setServiceModalRowIndex = os1b[1]; + var os1c = React.useState('客户'); + var hydrogenBearer = os1c[0]; + var setHydrogenBearer = os1c[1]; + var os1d = React.useState('预付'); + var hydrogenPaymentMethod = os1d[0]; + var setHydrogenPaymentMethod = os1d[1]; + var os1e = React.useState(''); + var hydrogenPrepay = os1e[0]; + var setHydrogenPrepay = os1e[1]; + var os1f = React.useState(''); + var returnHydrogenPrice = os1f[0]; + var setReturnHydrogenPrice = os1f[1]; + var os1g = React.useState(null); + var plateNoFocusRow = os1g[0]; + var setPlateNoFocusRow = os1g[1]; + var os1h = React.useState(''); + var plateNoSearch = os1h[0]; + var setPlateNoSearch = os1h[1]; + var os1i = React.useState(null); + var serviceItemFocusRow = os1i[0]; + var setServiceItemFocusRow = os1i[1]; + var os1j = React.useState(''); + var serviceItemSearch = os1j[0]; + var setServiceItemSearch = os1j[1]; + var copyPopState = React.useState(null); + var copyPopover = copyPopState[0]; + var setCopyPopover = copyPopState[1]; + var copyQtyState = React.useState('1'); + var copyQuantity = copyQtyState[0]; + var setCopyQuantity = copyQtyState[1]; + var os1k = React.useState(null); + var plateNoDropdownRect = os1k[0]; + var setPlateNoDropdownRect = os1k[1]; + var os2 = React.useState(''); + var feeTemplate = os2[0]; + var setFeeTemplate = os2[1]; + var os3 = React.useState('month'); + var billingMethod = os3[0]; + var setBillingMethod = os3[1]; + + var cc1State = React.useState(false); + var cc1 = cc1State[0]; + var setCc1 = cc1State[1]; + var cc2State = React.useState(false); + var cc2 = cc2State[0]; + var setCc2 = cc2State[1]; + var cc3State = React.useState(false); + var cc3 = cc3State[0]; + var setCc3 = cc3State[1]; + var cc4State = React.useState(false); + var cc4 = cc4State[0]; + var setCc4 = cc4State[1]; + var cc5State = React.useState(false); + var cc5 = cc5State[0]; + var setCc5 = cc5State[1]; + var cc6State = React.useState(false); + var cc6 = cc6State[0]; + var setCc6 = cc6State[1]; + var reqSpecState = React.useState(false); + var reqSpecOpen = reqSpecState[0]; + var setReqSpecOpen = reqSpecState[1]; + var formErrorsState = React.useState({}); + var formErrors = formErrorsState[0]; + var setFormErrors = formErrorsState[1]; + + var customerList = [ + { id: '1', name: '嘉兴某某物流有限公司', creditCode: '91330400MA2XXXXX1', address: '浙江省嘉兴市南湖区科技大道1号', contact: '张三', phone: '13800138001', email: 'zhangsan@example.com', companyName: '嘉兴某某物流有限公司', companyPhone: '0571-88888888', mailingAddress: '浙江省嘉兴市南湖区科技大道1号', bank: '中国工商银行嘉兴分行', bankAccount: '6222021234567890123', taxId: '91330400MA2XXXXX1' }, + { id: '2', name: '上海某某运输公司', creditCode: '91310000MA2XXXXX2', address: '上海市浦东新区张江高科技园区', contact: '李四', phone: '13800138002', email: 'lisi@example.com', companyName: '上海某某运输公司', companyPhone: '021-66666666', mailingAddress: '上海市浦东新区张江高科技园区', bank: '中国建设银行上海分行', bankAccount: '6217001234567890123', taxId: '91310000MA2XXXXX2' }, + { id: '3', name: '杭州某某租赁有限公司', creditCode: '91330100MA2XXXXX3', address: '浙江省杭州市余杭区未来科技城', contact: '王五', phone: '13800138003', email: 'wangwu@example.com', companyName: '杭州某某租赁有限公司', companyPhone: '0571-99999999', mailingAddress: '浙江省杭州市余杭区未来科技城', bank: '中国农业银行杭州分行', bankAccount: '6228481234567890123', taxId: '91330100MA2XXXXX3' } + ]; + var deptList = [{ id: 'YW1', name: '业务1部', owners: ['张经理', '李专员', '王专员'] }, { id: 'YW2', name: '业务2部', owners: ['赵经理', '钱专员'] }, { id: 'YW3', name: '业务3部', owners: ['孙经理', '周专员'] }]; + var lessorCompanyList = (typeof window !== 'undefined' && window.ONEOS_LESSOR_COMPANIES) || [ + { id: 'jx', shortName: '嘉兴羚牛', legalName: '浙江羚牛氢能科技有限公司', creditCode: '91330481MA2JXXXXX', address: '浙江省嘉兴市平湖市乍浦镇杭州湾大道1号', contact: '法务部', phone: '0573-88888888', email: 'legal@lingniu-jx.com', accountName: '浙江羚牛氢能科技有限公司', bankName: '招商银行嘉兴分行', bankAccount: '120924165110301', hotline: '400-021-1773' }, + { id: 'sh', shortName: '上海羚牛', legalName: '羚牛氢能科技(上海)有限公司', creditCode: '91310115MA1HXXXXX', address: '上海市浦东新区张江高科技园区科苑路88号', contact: '法务部', phone: '021-88888888', email: 'legal@lingniu-sh.com', accountName: '羚牛氢能科技(上海)有限公司', bankName: '招商银行上海张江支行', bankAccount: '120924165110401', hotline: '400-021-1773' }, + { id: 'gd', shortName: '广东羚牛', legalName: '羚牛氢能科技(广东)有限公司', creditCode: '91440101MA5CXXXXX', address: '广东省广州市黄埔区科学城开源大道11号', contact: '法务部', phone: '020-88888888', email: 'legal@lingniu-gd.com', accountName: '羚牛氢能科技(广东)有限公司', bankName: '招商银行广州萝岗支行', bankAccount: '120924165110201', hotline: '400-021-1773' } + ]; + var selectedLessor = lessorCompanyList.find(function(c) { return c.id === signingCompany; }) || null; + var regionList = [ + { province: '浙江省', cities: ['杭州市', '宁波市', '嘉兴市', '湖州市'] }, + { province: '上海市', cities: ['上海市'] }, + { province: '广东省', cities: ['广州市', '深圳市', '东莞市'] } + ]; + var feeTemplates = ['标准费用模板A', '标准费用模板B', '定制费用模板C']; + var feeTemplateCertFees = [{ project: '补办行驶证', standard: '50元/次', serviceFee: '20' }, { project: '补办驾驶证', standard: '30元/次', serviceFee: '10' }, { project: '补办牌照', standard: '100元/次', serviceFee: '50' }]; + var feeTemplatePenaltyFees = [{ project: '提前退车违约金', standard: '月租金×1', serviceFee: '0' }, { project: '违章处理违约金', standard: '按实际发生', serviceFee: '50' }]; + var feeTemplateConsumables = [{ category: '轮胎', part: '前轮', partName: '轮胎A型', qty: 1, feeDetail: '500.00' }, { category: '易损件', part: '雨刮', partName: '雨刮片', qty: 2, feeDetail: '80.00' }]; + var feeTemplateOtherFees = [{ project: '上门送车费', standard: '100元/次', serviceFee: '50' }, { project: '上门收车费', standard: '100元/次', serviceFee: '50' }, { project: '清洗费', standard: '80元/次', serviceFee: '30' }]; + var brandList = ['品牌A', '品牌B', '品牌C', '品牌D']; + var modelByBrand = { '品牌A': ['型号A1', '型号A2', '型号A3'], '品牌B': ['型号B1', '型号B2'], '品牌C': ['型号C1', '型号C2', '型号C3'], '品牌D': ['型号D1'] }; + var vehicleList = [ + { plateNo: '浙A10001', vin: 'L1234567890ABCDEF', brand: '品牌A', model: '型号A1' }, + { plateNo: '浙B20002', vin: 'L2234567890ABCDEF', brand: '品牌A', model: '型号A2' }, + { plateNo: '沪A30003', vin: 'L3234567890ABCDEF', brand: '品牌B', model: '型号B1' }, + { plateNo: '粤A40004', vin: 'L4234567890ABCDEF', brand: '品牌C', model: '型号C1' } + ]; + var serviceItemOptions = ['代处理费用', '罚款', '违章处理违约金', '未参加安全培训', '车辆出险', '年检年审违约', '停车费', '设备损坏金(包含易损件)', '清洗费', '上门收车人工费', '上门收车送车行驶费', '上门收车基础服务费', '保险上浮', '保养费用', '补办驾驶证', '补办牌照', '补办营运证', '补办加氢证', '借用备用钥匙', '补配钥匙', '租金', '氢气费-客', '退还车氢量差', '能源费补缴', '能源费退款', '送车上门人工费', '送车上门送车行驶费', '送车上门基础服务费', '保证金', '氢气预付费', '维修费用', 'ETC-客', 'ETC卡缺损费', 'ETC设备缺损费', '电费-客', '未结算保养费', '未结算维修费', '车损费', '工具损坏或丢失费', '证件费', '广告损坏费', '送车服务费', '接车服务费', '补办行驶证', '超赔险', '轮胎磨损费', '无忧包', '轮胎保', '养护保', '尾板']; + + var filteredCustomers = customerList.filter(function(c) { return !customerSearch || c.name.indexOf(customerSearch) !== -1; }); + var currentDept = deptList.find(function(d) { return d.id === businessDept; }); + var ownerOptions = currentDept ? currentDept.owners : []; + var contractCode = (function() { + var cityCode = 'JX'; + var typeCode = 'ZL'; + var signCode = contractType === '正式合同' ? 'A' : 'B'; + var dateStr = effectiveDate ? effectiveDate.replace(/-/g, '') : '20260216'; + var deptCode = businessDept || 'YW1'; + return cityCode + typeCode + dateStr + deptCode + '01235' + signCode; + })(); + var mainVehicleModelsDisplay = (function() { + var models = []; + var seen = {}; + for (var i = 0; i < rentalOrders.length; i++) { + var m = rentalOrders[i].model; + if (m && !seen[m]) { seen[m] = true; models.push(m); } + } + return models.join('、'); + })(); + var calcRowServiceFee = function(row) { + var sum = 0; + for (var i = 0; i < (row.serviceItems || []).length; i++) { + var fee = parseFloat(row.serviceItems[i].fee); + if (!isNaN(fee)) sum += fee; + } + return sum.toFixed(2); + }; + var rentalTotalVehicles = rentalOrders.length; + var rentalTotalRentService = rentalOrders.reduce(function(s, r) { return s + (parseFloat(r.monthRent) || 0) + (parseFloat(calcRowServiceFee(r)) || 0); }, 0); + var rentalTotalDeposit = rentalOrders.reduce(function(s, r) { return s + (parseFloat(r.deposit) || 0); }, 0); + var rentalTotalHydrogen = (hydrogenPaymentMethod === '预付' && hydrogenBearer === '客户') ? (parseFloat(hydrogenPrepay) || 0) : 0; + + var selectCustomer = function(c) { + setSelectedCustomer(c); + setCustomerSearch(c ? c.name : ''); + setCustomerDropdownOpen(false); + }; + var deliveryRegionDisplay = deliveryProvince && deliveryCity ? deliveryProvince + ' / ' + deliveryCity : ''; + var selectDeliveryRegion = function(province, city) { + setDeliveryProvince(province); + setDeliveryCity(city); + setDeliveryRegionOpen(false); + }; + var scrollToCard = function(id) { + var el = document.getElementById(id); + if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }; + var handleOwnerFocus = function() { + if (!businessDept) setOwnerFocusError('请先选择业务部门'); + else setOwnerFocusError(''); + }; + var handleOwnerBlur = function() { setOwnerFocusError(''); }; + var openContractOriginal = function() { + if (contractOriginal && contractOriginal.file) { + var url = URL.createObjectURL(contractOriginal.file); + window.open(url); + } + }; + + var validateSubmitAndReview = function() { + var errs = {}; + if (!selectedCustomer) errs.customer = '请选择客户'; + if (!businessDept) errs.businessDept = '请选择业务部门'; + if (!contractOriginal) errs.contractOriginal = '请上传合同原件'; + if (!businessOwner) errs.businessOwner = '请选择业务负责人'; + if (!projectName || !projectName.trim()) errs.projectName = '请输入项目名称'; + if (!contractType) errs.contractType = '请选择合同类型'; + if (!effectiveDate) errs.effectiveDate = '请选择生效日期'; + if (!paymentMethod) errs.paymentMethod = '请选择付款方式'; + if (!endDate) errs.endDate = '请选择结束日期'; + if (!paymentPeriod) errs.paymentPeriod = '请选择付款周期'; + if (!signingCompany) errs.signingCompany = '请选择签约公司'; + if (!deliveryProvince || !deliveryCity) errs.deliveryRegion = '请选择交车区域'; + if (!deliveryLocation || !String(deliveryLocation).trim()) errs.deliveryLocation = '请输入交车地点'; + if (!contractApprovalType) errs.contractApprovalType = '请选择合同审批类型'; + var authInvalid = authorizedList.some(function(a) { return !a.name || !a.name.trim() || !a.phone || !a.phone.trim() || !a.idCard || !a.idCard.trim(); }); + if (authInvalid) errs.authorizedList = '请完整填写被授权人姓名、联系电话、身份证'; + var rentalInvalid = rentalOrders.some(function(r) { return !r.brand || !r.model || !(r.monthRent && String(r.monthRent).trim()) || !(r.deposit && String(r.deposit).trim()); }); + if (rentalInvalid) errs.rentalOrders = '请完整填写租赁订单的品牌、型号、车辆月租金、保证金'; + if (!hydrogenBearer) errs.hydrogenBearer = '请选择氢费承担方'; + if (hydrogenBearer === '客户' && !hydrogenPaymentMethod) errs.hydrogenPaymentMethod = '请选择付款方式'; + if (hydrogenBearer === '客户' && hydrogenPaymentMethod === '预付' && (!hydrogenPrepay || !String(hydrogenPrepay).trim())) errs.hydrogenPrepay = '请输入氢气预付款'; + if (!returnHydrogenPrice || !String(returnHydrogenPrice).trim()) errs.returnHydrogenPrice = '请输入退还车氢气单价'; + if (!feeTemplate) errs.feeTemplate = '请选择费用模板'; + if (!billingMethod) errs.billingMethod = '请选择账单计算方式'; + setFormErrors(errs); + if (Object.keys(errs).length > 0) { + var firstId = errs.customer || errs.businessDept || errs.businessOwner ? 'card-customer' : (errs.projectName || errs.contractType || errs.effectiveDate || errs.paymentMethod || errs.endDate || errs.paymentPeriod || errs.signingCompany || errs.deliveryRegion || errs.deliveryLocation || errs.contractApprovalType || errs.contractOriginal) ? 'card-contract' : errs.authorizedList ? 'card-authorized' : (errs.rentalOrders || errs.hydrogenBearer || errs.hydrogenPaymentMethod || errs.hydrogenPrepay || errs.returnHydrogenPrice) ? 'card-rental' : errs.feeTemplate ? 'card-fee' : 'card-billing'; + setCc1(false); setCc2(false); setCc3(false); setCc4(false); setCc5(false); setCc6(false); + setTimeout(function() { var el = document.getElementById(firstId); if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, 100); + return false; + } + return true; + }; + + var addAuthorized = function() { + setAuthorizedList(authorizedList.concat([{ name: '', phone: '', idCard: '' }])); + }; + var removeAuthorized = function(index) { + var next = authorizedList.slice(0); + next.splice(index, 1); + setAuthorizedList(next.length ? next : [{ name: '', phone: '', idCard: '' }]); + }; + var updateAuthorized = function(index, field, value) { + var next = authorizedList.slice(0); + var cur = next[index] || {}; + var patch = {}; + patch[field] = value; + next[index] = Object.assign({}, cur, patch); + setAuthorizedList(next); + }; + + var addRentalRow = function() { + setRentalOrders(rentalOrders.concat([{ id: nextRentalRowIdRef.current++, brand: '', model: '', plateNo: '', vin: '', monthRent: '', serviceItems: [{ project: '', fee: '', effectiveDate: '' }], deposit: '', remark: '' }])); + }; + var removeRentalRow = function(index) { + var next = rentalOrders.slice(0); + next.splice(index, 1); + setRentalOrders(next.length ? next : [Object.assign({}, emptyRentalRow)]); + }; + var buildDuplicateRentalRow = function(sourceRow) { + var serviceItemsCopy = (sourceRow.serviceItems || []).map(function(si) { return { project: si.project || '', fee: si.fee || '', effectiveDate: si.effectiveDate || '' }; }); + if (serviceItemsCopy.length === 0) serviceItemsCopy = [{ project: '', fee: '', effectiveDate: '' }]; + return { id: nextRentalRowIdRef.current++, brand: sourceRow.brand || '', model: sourceRow.model || '', plateNo: '', vin: '', monthRent: sourceRow.monthRent || '', serviceItems: serviceItemsCopy, deposit: sourceRow.deposit || '', remark: sourceRow.remark || '' }; + }; + var applyRentalRowCopies = function(rowIndex, count) { + var row = rentalOrders[rowIndex]; + if (!row) return; + var n = typeof count === 'number' ? count : parseInt(String(count), 10); + if (isNaN(n) || n < 0) { + if (message.warning) message.warning('请输入非负整数'); + return; + } + n = Math.floor(n); + if (n === 0) { + setCopyPopover(null); + if (message.info) message.info('复制数量为 0,未新增行'); + return; + } + var next = rentalOrders.slice(0); + var insertAt = rowIndex + 1; + for (var i = 0; i < n; i++) { + next.splice(insertAt + i, 0, buildDuplicateRentalRow(row)); + } + setRentalOrders(next); + setCopyPopover(null); + if (message.success) message.success('已复制 ' + n + ' 条记录(车牌号已清空)'); + }; + var confirmRentalCopyPopover = function() { + if (!copyPopover) return; + if (copyQuantity === '') { + if (message.warning) message.warning('请输入复制数量'); + return; + } + if (!/^\d+$/.test(copyQuantity)) { + if (message.warning) message.warning('请输入非负整数'); + return; + } + applyRentalRowCopies(copyPopover.index, parseInt(copyQuantity, 10)); + }; + var openRentalCopyPopover = function(rowIndex, anchorEl) { + if (!anchorEl || !anchorEl.getBoundingClientRect) return; + var r = anchorEl.getBoundingClientRect(); + var cardH = 168; + var top = r.bottom + 8; + if (top + cardH > window.innerHeight - 8) top = Math.max(8, r.top - cardH - 8); + var left = r.left; + var cardW = 240; + if (left + cardW > window.innerWidth - 8) left = Math.max(8, window.innerWidth - cardW - 8); + setCopyPopover({ index: rowIndex, top: top, left: left }); + setCopyQuantity('1'); + }; + var updateRentalOrder = function(index, field, value) { + var next = rentalOrders.slice(0); + var cur = next[index] || {}; + var newRow = Object.assign({}, cur); + newRow[field] = value; + if (field === 'plateNo') { + var v = vehicleList.find(function(x) { return x.plateNo === value; }); + if (v) { + newRow.vin = v.vin || ''; + if (v.brand || v.model) { + if (!cur.brand && !cur.model) { + newRow.brand = v.brand || ''; + newRow.model = v.model || ''; + } + } + } else { + newRow.vin = ''; + } + } + if (field === 'brand') newRow.model = ''; + next[index] = newRow; + setRentalOrders(next); + }; + var openServiceModal = function(index) { setServiceModalRowIndex(index); }; + var closeServiceModal = function() { setServiceModalRowIndex(null); }; + var addServiceItem = function() { + if (serviceModalRowIndex === null) return; + var next = rentalOrders.slice(0); + var row = next[serviceModalRowIndex]; + var items = (row.serviceItems || []).concat([{ project: '', fee: '', effectiveDate: '' }]); + var newRow = {}; + for (var k in row) { if (row.hasOwnProperty(k) && k !== 'serviceItems') newRow[k] = row[k]; } + newRow.serviceItems = items; + next[serviceModalRowIndex] = newRow; + setRentalOrders(next); + }; + var removeServiceItem = function(siIndex) { + if (serviceModalRowIndex === null) return; + var next = rentalOrders.slice(0); + var row = next[serviceModalRowIndex]; + var items = (row.serviceItems || []).slice(0); + items.splice(siIndex, 1); + if (items.length === 0) items = [{ project: '', fee: '', effectiveDate: '' }]; + var newRow = {}; + for (var k in row) { if (row.hasOwnProperty(k) && k !== 'serviceItems') newRow[k] = row[k]; } + newRow.serviceItems = items; + next[serviceModalRowIndex] = newRow; + setRentalOrders(next); + }; + var updateServiceItem = function(siIndex, field, value) { + if (serviceModalRowIndex === null) return; + var next = rentalOrders.slice(0); + var row = next[serviceModalRowIndex]; + var items = (row.serviceItems || []).slice(0); + var item = items[siIndex] || {}; + var newItem = {}; + for (var k in item) { if (item.hasOwnProperty(k)) newItem[k] = item[k]; } + newItem[field] = value; + items[siIndex] = newItem; + var newRow = {}; + for (var rk in row) { if (row.hasOwnProperty(rk) && rk !== 'serviceItems') newRow[rk] = row[rk]; } + newRow.serviceItems = items; + next[serviceModalRowIndex] = newRow; + setRentalOrders(next); + }; + + React.useEffect(function() { + if (!deliveryRegionOpen) return; + var handler = function(e) { + if (deliveryRegionClickInsideRef.current) { deliveryRegionClickInsideRef.current = false; return; } + var el = document.getElementById('delivery-region-wrap'); + if (el && !el.contains(e.target)) setDeliveryRegionOpen(false); + }; + document.addEventListener('mousedown', handler); + return function() { document.removeEventListener('mousedown', handler); }; + }, [deliveryRegionOpen]); + + React.useEffect(function() { + if (!copyPopover) return; + var handler = function(e) { + var pop = document.getElementById('rental-copy-popover'); + if (pop && pop.contains(e.target)) return; + setCopyPopover(null); + }; + var t = window.setTimeout(function() { + document.addEventListener('mousedown', handler); + }, 0); + return function() { + window.clearTimeout(t); + document.removeEventListener('mousedown', handler); + }; + }, [copyPopover]); + + React.useEffect(function() { + if (plateNoFocusRow === null) { setPlateNoDropdownRect(null); return; } + var timer = setTimeout(function() { + var el = document.getElementById('plate-no-input-' + plateNoFocusRow); + if (el && el.getBoundingClientRect) { + var rect = el.getBoundingClientRect(); + setPlateNoDropdownRect({ top: rect.bottom + 2, left: rect.left, width: rect.width }); + } else { setPlateNoDropdownRect(null); } + }, 0); + return function() { clearTimeout(timer); }; + }, [plateNoFocusRow]); + + var styles = { + page: { padding: '16px 24px 88px', backgroundColor: '#f5f7fa', minHeight: '100vh', fontFamily: '"PingFang SC", "苹方-简", -apple-system, BlinkMacSystemFont, "Microsoft YaHei", sans-serif', fontSize: 14, color: 'rgba(0,0,0,0.88)' }, + breadcrumb: { marginBottom: 16, color: 'rgba(0,0,0,0.65)' }, + breadcrumbSep: { margin: '0 8px', color: 'rgba(0,0,0,0.35)' }, + anchorWrap: { position: 'fixed', top: 80, right: 24, zIndex: 100, backgroundColor: '#fff', borderRadius: 12, border: '1px solid rgba(0,0,0,0.06)', boxShadow: '0 1px 2px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.06)', padding: '12px 16px', minWidth: 168 }, + anchorItem: { display: 'block', padding: '8px 4px', minHeight: 36, color: '#1677ff', cursor: 'pointer', border: 'none', background: 'none', width: '100%', textAlign: 'left', fontSize: 14, borderRadius: 6, outline: 'none' }, + card: { backgroundColor: '#fff', borderRadius: 12, marginBottom: 16, border: '1px solid rgba(0,0,0,0.04)', boxShadow: '0 1px 2px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.06)', overflow: 'hidden' }, + cardHeader: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '16px 20px', borderBottom: '1px solid rgba(0,0,0,0.06)', cursor: 'pointer' }, + cardTitle: { fontSize: 16, fontWeight: 600, color: 'rgba(0,0,0,0.88)' }, + cardToggle: { color: 'rgba(0,0,0,0.45)', fontSize: 14 }, + cardBody: { padding: '20px 24px' }, + formRow: { display: 'flex', flexWrap: 'wrap', marginBottom: 16 }, + formCol: { flex: '0 0 33.33%', minWidth: 200, paddingRight: 16, marginBottom: 8 }, + formColFull: { flex: '0 0 100%', marginBottom: 8 }, + label: { display: 'block', marginBottom: 6, color: 'rgba(0,0,0,0.88)', fontWeight: 500 }, + labelRequired: { color: '#ff4d4f', marginRight: 4 }, + input: { width: '100%', padding: '8px 12px', border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 14 }, + inputDisabled: { backgroundColor: '#f5f5f5', color: '#999', cursor: 'not-allowed' }, + inputError: { borderColor: '#ff4d4f' }, + select: { width: '100%', padding: '8px 12px', border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 14 }, + textarea: { width: '100%', padding: '8px 12px', border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 14, minHeight: 80, resize: 'vertical' }, + errMsg: { color: '#ff4d4f', fontSize: 12, marginTop: 4 }, + summaryList: { marginBottom: 16, display: 'flex', flexWrap: 'wrap', gap: 16 }, + summaryListItem: { flex: '0 0 calc(50% - 8px)', display: 'flex', alignItems: 'center', padding: '12px 16px', border: '1px solid #e8e8e8', borderRadius: 4, backgroundColor: '#fafafa', fontSize: 14, boxSizing: 'border-box' }, + summaryListLabel: { flex: '0 0 140px', color: '#666' }, + summaryListValue: { flex: 1, fontWeight: 600, color: '#333' }, + authRow: { display: 'flex', gap: 12, alignItems: 'flex-start', marginBottom: 12 }, + authInput: { flex: 1, padding: '8px 12px', border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 14 }, + btnDel: { padding: '8px 16px', color: '#ff4d4f', border: '1px solid #ff4d4f', borderRadius: 4, backgroundColor: '#fff', cursor: 'pointer', fontSize: 14 }, + btnAdd: { padding: '8px 16px', color: '#1677ff', border: '1px dashed #1677ff', borderRadius: 8, backgroundColor: '#fff', cursor: 'pointer', marginBottom: 16, fontSize: 14 }, + footer: { position: 'fixed', bottom: 0, left: 0, right: 0, padding: '12px 24px', backgroundColor: '#fff', borderTop: '1px solid rgba(0,0,0,0.06)', display: 'flex', gap: 12, justifyContent: 'flex-start', alignItems: 'center', zIndex: 99, boxShadow: '0 -2px 8px rgba(0,0,0,0.04)' }, + btn: { padding: '8px 24px', borderRadius: 8, border: '1px solid #d9d9d9', cursor: 'pointer', fontSize: 14, minHeight: 40 }, + btnPrimary: { backgroundColor: '#1677ff', color: '#fff', borderColor: '#1677ff' }, + btnDefault: { backgroundColor: '#fff', color: 'rgba(0,0,0,0.88)' }, + tag: { display: 'inline-block', padding: '2px 8px', marginRight: 8, marginBottom: 4, backgroundColor: '#e6f4ff', color: '#1677ff', borderRadius: 6, fontSize: 12 }, + regionCascader: { position: 'absolute', top: '100%', left: 0, right: 0, marginTop: 4, backgroundColor: '#fff', border: '1px solid #d9d9d9', borderRadius: 4, boxShadow: '0 2px 8px rgba(0,0,0,0.12)', zIndex: 10, display: 'flex', minHeight: 200 }, + regionCascaderCol: { flex: 1, borderRight: '1px solid #f0f0f0', overflowY: 'auto' }, + regionCascaderColLast: { flex: 1 }, + regionCascaderItem: { padding: '10px 12px', cursor: 'pointer', fontSize: 14 }, + rentalTable: { width: '100%', borderCollapse: 'collapse', fontSize: 14 }, + rentalTh: { padding: '10px 8px', textAlign: 'left', borderBottom: '1px solid #e8e8e8', backgroundColor: '#fafafa', fontWeight: 600, fontSize: 14 }, + rentalTd: { padding: '8px', borderBottom: '1px solid #f0f0f0', verticalAlign: 'top', fontSize: 14 }, + rentalTdCenter: { padding: '8px', borderBottom: '1px solid #f0f0f0', verticalAlign: 'middle', fontSize: 14 }, + rentalInput: { width: '100%', padding: '6px 10px', border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 14 }, + rentalInputDisabled: { backgroundColor: '#f5f5f5', color: '#999' }, + modalMask: { position: 'fixed', left: 0, top: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.45)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center' }, + modalBox: { backgroundColor: '#fff', borderRadius: 8, width: '90%', maxWidth: 720, maxHeight: '85vh', overflow: 'hidden', display: 'flex', flexDirection: 'column', boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }, + modalHeader: { padding: '16px 20px', borderBottom: '1px solid #f0f0f0', fontSize: 16, fontWeight: 600 }, + modalBody: { padding: 20, overflow: 'auto', flex: 1 }, + btnGroup: { display: 'inline-flex', border: '1px solid #d9d9d9', borderRadius: 4, overflow: 'hidden' }, + btnGroupItem: { padding: '8px 16px', border: 'none', borderRight: '1px solid #d9d9d9', backgroundColor: '#fff', color: '#333', cursor: 'pointer', fontSize: 14 }, + btnGroupItemLast: { borderRight: 'none' }, + btnGroupItemActive: { backgroundColor: '#1677ff', color: '#fff', borderColor: '#1677ff', borderRightColor: '#1677ff' }, + feeSectionTitle: { fontSize: 16, fontWeight: 600, color: '#333', marginTop: 20, marginBottom: 10 }, + feeSectionTitleFirst: { marginTop: 0 }, + modalFormInput: { width: '100%', padding: '8px 12px', border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 14, height: 36, boxSizing: 'border-box' }, + copyPopoverCard: { position: 'fixed', zIndex: 2000, backgroundColor: '#fff', borderRadius: 8, boxShadow: '0 4px 20px rgba(0,0,0,0.12), 0 0 1px rgba(0,0,0,0.08)', border: '1px solid #f0f0f0', padding: '14px 16px', minWidth: 232, maxWidth: 'calc(100vw - 24px)' }, + copyPopoverTitle: { fontSize: 14, fontWeight: 600, color: 'rgba(0,0,0,0.88)', marginBottom: 12 }, + copyPopoverFooter: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 14 } + }; + + var CardBlock = function(props) { + var panelId = 'card-panel-' + (props.id || String(props.title || '').replace(/\s/g, '-')); + var titleId = 'card-title-' + panelId; + return React.createElement('div', { id: props.id, style: Object.assign({}, styles.card, props.cardStyle || {}) }, + React.createElement('div', { + style: styles.cardHeader, + onClick: function() { props.setCollapsed(!props.collapsed); }, + role: 'button', + tabIndex: 0, + 'aria-expanded': !props.collapsed, + 'aria-controls': panelId, + onKeyDown: function(e) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + props.setCollapsed(!props.collapsed); + } + } + }, + React.createElement('span', { style: styles.cardTitle, id: titleId }, props.title), + React.createElement('span', { style: styles.cardToggle }, props.collapsed ? '展开' : '收起') + ), + !props.collapsed ? React.createElement('div', { id: panelId, style: styles.cardBody, role: 'region', 'aria-labelledby': titleId }, props.children) : null + ); + }; + + var FormItem = function(props) { + var span = props.fullWidth ? 24 : (props.span || 8); // 满宽占 24 列,常规占 8 列(1/3 宽度) + var colStyle = props.colStyle || {}; + return React.createElement(Col, { span: span, style: Object.assign({ marginBottom: 16 }, colStyle) }, + React.createElement('label', { style: styles.label, className: 'acro-text-base' }, props.required ? React.createElement('span', { style: styles.labelRequired }, '*') : null, props.label), + props.children, + props.error ? React.createElement('div', { style: styles.errMsg, className: 'acro-text-xs', role: 'alert' }, props.error) : null + ); + }; + + var customerOptions = customerList.map(function(c) { return React.createElement(Option, { key: c.id, value: c.id }, c.name); }); + var customerFields = React.createElement(Row, { gutter: 24 }, + React.createElement(FormItem, { label: '客户名称', required: true, error: formErrors.customer }, React.createElement(Select, { placeholder: '请选择或输入搜索客户', style: { width: '100%' }, value: selectedCustomer ? selectedCustomer.id : undefined, onChange: function(id) { var c = customerList.find(function(x) { return x.id === id; }); selectCustomer(c || null); }, showSearch: true, allowClear: true, filterOption: function(input, opt) { return opt.children && String(opt.children).toLowerCase().indexOf((input || '').toLowerCase()) >= 0; }, status: formErrors.customer ? 'error' : undefined }, customerOptions)), + React.createElement(FormItem, { label: '客户统一信用代码' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.creditCode : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '客户地址' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.address : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '客户联系人' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.contact : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '客户电话' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.phone : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '客户电子邮箱' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.email : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '企业名称' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.companyName : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '企业电话' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.companyPhone : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '邮寄地址' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.mailingAddress : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '开户银行' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.bank : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '银行账号' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.bankAccount : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '纳税人识别号' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.taxId : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '业务部门', required: true, error: formErrors.businessDept }, React.createElement(Select, { placeholder: '请选择业务部门', style: { width: '100%' }, value: businessDept || undefined, onChange: function(v) { setBusinessDept(v || ''); setBusinessOwner(''); }, status: formErrors.businessDept ? 'error' : undefined }, deptList.map(function(d, i) { return React.createElement(Option, { key: i, value: d.id }, d.name); }))), + React.createElement(FormItem, { label: '业务负责人', required: true, error: formErrors.businessOwner || ownerFocusError }, React.createElement(Select, { placeholder: businessDept ? '请选择' : '请先选择业务部门', style: { width: '100%' }, value: businessOwner || undefined, onChange: function(v) { setBusinessOwner(v || ''); setOwnerFocusError(''); }, onFocus: handleOwnerFocus, onBlur: handleOwnerBlur, disabled: !businessDept, status: (formErrors.businessOwner || ownerFocusError) ? 'error' : undefined }, ownerOptions.map(function(o, i) { return React.createElement(Option, { key: i, value: o }, o); }))) + ); + + var contractFormRow1 = React.createElement(Row, { gutter: 24 }, + React.createElement(FormItem, { label: '项目名称', required: true, error: formErrors.projectName }, React.createElement(Input, { placeholder: '请输入项目名称', value: projectName, onChange: function(e) { setProjectName(e.target.value); }, status: formErrors.projectName ? 'error' : undefined, style: { width: '100%' } })), + React.createElement(FormItem, { label: '合同类型', required: true, error: formErrors.contractType }, React.createElement(Select, { placeholder: '请选择合同类型', style: { width: '100%' }, value: contractType || undefined, onChange: function(v) { setContractType(v || ''); }, status: formErrors.contractType ? 'error' : undefined }, React.createElement(Option, { value: '正式合同' }, '正式合同'), React.createElement(Option, { value: '试用合同' }, '试用合同'))), + React.createElement(FormItem, { label: '生效日期', required: true, error: formErrors.effectiveDate }, React.createElement(DatePicker, { style: { width: '100%' }, format: 'YYYY-MM-DD', placeholder: '请选择生效日期', value: effectiveDate && window.moment ? window.moment(effectiveDate, 'YYYY-MM-DD') : null, onChange: function(d, dateStr) { setEffectiveDate(dateStr || ''); }, status: formErrors.effectiveDate ? 'error' : undefined })), + React.createElement(FormItem, { label: '付款方式', required: true, error: formErrors.paymentMethod }, React.createElement(Select, { placeholder: '请选择付款方式', style: { width: '100%' }, value: paymentMethod || undefined, onChange: function(v) { setPaymentMethod(v || ''); }, status: formErrors.paymentMethod ? 'error' : undefined }, React.createElement(Option, { value: '预付' }, '预付'), React.createElement(Option, { value: '后付' }, '后付'))), + React.createElement(FormItem, { label: '主要车型' }, React.createElement('div', { style: { padding: '8px 12px', minHeight: 36, border: '1px solid #d9d9d9', borderRadius: 4, backgroundColor: '#f5f5f5', display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' } }, mainVehicleModelsDisplay ? mainVehicleModelsDisplay.split('、').map(function(m, i) { return React.createElement('span', { key: i, style: styles.tag }, m); }) : React.createElement('span', { style: { color: '#999' } }, '根据租赁订单自动反写'))), + React.createElement(FormItem, { label: '结束日期', required: true, error: formErrors.endDate }, React.createElement(DatePicker, { style: { width: '100%' }, format: 'YYYY-MM-DD', placeholder: '请选择结束日期', value: endDate && window.moment ? window.moment(endDate, 'YYYY-MM-DD') : null, onChange: function(d, dateStr) { setEndDate(dateStr || ''); }, status: formErrors.endDate ? 'error' : undefined })), + React.createElement(FormItem, { label: '付款周期', required: true, error: formErrors.paymentPeriod }, React.createElement(Select, { placeholder: '请选择', style: { width: '100%' }, value: paymentPeriod || undefined, onChange: function(v) { setPaymentPeriod(v || ''); }, status: formErrors.paymentPeriod ? 'error' : undefined }, [1,2,3,4,5,6,7,8,9,10,11,12].map(function(n) { return React.createElement(Option, { key: n, value: String(n) }, n + '个月'); }))), + React.createElement(FormItem, { label: '签约公司(甲方)', required: true, error: formErrors.signingCompany }, React.createElement(Select, { placeholder: '请选择旗下签约主体', style: { width: '100%' }, value: signingCompany || undefined, onChange: function(v) { setSigningCompany(v || ''); }, status: formErrors.signingCompany ? 'error' : undefined }, lessorCompanyList.map(function(c) { return React.createElement(Option, { key: c.id, value: c.id }, c.legalName); }))), + React.createElement(FormItem, { label: '甲方全称' }, React.createElement(Input, { value: selectedLessor ? selectedLessor.legalName : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '甲方地址' }, React.createElement(Input, { value: selectedLessor ? selectedLessor.address : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '甲方联系人' }, React.createElement(Input, { value: selectedLessor ? selectedLessor.contact : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '甲方电话' }, React.createElement(Input, { value: selectedLessor ? selectedLessor.phone : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '收款户名' }, React.createElement(Input, { value: selectedLessor ? selectedLessor.accountName : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '开户行' }, React.createElement(Input, { value: selectedLessor ? selectedLessor.bankName : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '银行账号' }, React.createElement(Input, { value: selectedLessor ? selectedLessor.bankAccount : '', disabled: true, style: { width: '100%' } })), + React.createElement(React.Fragment, null, + React.createElement(FormItem, { label: '交车区域', required: true, error: formErrors.deliveryRegion }, React.createElement('div', { id: 'delivery-region-wrap', style: { position: 'relative' } }, React.createElement(Input, { style: Object.assign({}, formErrors.deliveryRegion ? { borderColor: '#ff4d4f' } : {}, { cursor: 'pointer', caretColor: 'transparent', width: '100%' }), placeholder: '请选择省-市', value: deliveryRegionDisplay, readOnly: true, role: 'combobox', 'aria-expanded': deliveryRegionOpen, 'aria-haspopup': 'listbox', 'aria-controls': 'delivery-region-cascader', 'aria-invalid': !!formErrors.deliveryRegion, 'aria-label': '交车区域,省-市', onClick: function() { setDeliveryRegionOpen(!deliveryRegionOpen); }, onKeyDown: function(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setDeliveryRegionOpen(!deliveryRegionOpen); } } }), deliveryRegionOpen ? React.createElement('div', { id: 'delivery-region-cascader', style: styles.regionCascader, onMouseDown: function() { deliveryRegionClickInsideRef.current = true; } }, React.createElement('div', { style: styles.regionCascaderCol }, regionList.map(function(r, i) { var isActive = r.province === deliveryProvince; return React.createElement('div', { key: i, style: Object.assign({}, styles.regionCascaderItem, isActive ? { backgroundColor: '#e6f4ff', color: '#1677ff' } : {}), onMouseEnter: function(e) { if (!isActive) e.currentTarget.style.backgroundColor = '#f5f5f5'; }, onMouseLeave: function(e) { if (!isActive) e.currentTarget.style.backgroundColor = 'transparent'; }, onMouseDown: function(e) { e.preventDefault(); setDeliveryProvince(r.province); setDeliveryCity(''); } }, r.province); })), React.createElement('div', { style: styles.regionCascaderColLast }, deliveryProvince ? (regionList.find(function(x) { return x.province === deliveryProvince; }) || { cities: [] }).cities.map(function(c, i) { var isActive = c === deliveryCity; return React.createElement('div', { key: i, style: Object.assign({}, styles.regionCascaderItem, isActive ? { backgroundColor: '#e6f4ff', color: '#1677ff' } : {}), onMouseEnter: function(e) { if (!isActive) e.currentTarget.style.backgroundColor = '#f5f5f5'; }, onMouseLeave: function(e) { if (!isActive) e.currentTarget.style.backgroundColor = 'transparent'; }, onMouseDown: function(e) { e.preventDefault(); selectDeliveryRegion(deliveryProvince, c); } }, c); }) : React.createElement('div', { style: { padding: 16, color: '#999', fontSize: 13 } }, '请先选择省'))) : null)) + ), + React.createElement(FormItem, { label: '交车地点', required: true, error: formErrors.deliveryLocation }, React.createElement(Input, { placeholder: '请输入交车地点', value: deliveryLocation, onChange: function(e) { setDeliveryLocation(e.target.value); }, status: formErrors.deliveryLocation ? 'error' : undefined, style: { width: '100%' } })), + React.createElement(FormItem, { label: '合同审批类型', required: true, error: formErrors.contractApprovalType }, React.createElement(Select, { placeholder: '请选择合同审批类型', style: { width: '100%' }, value: contractApprovalType || undefined, onChange: function(v) { setContractApprovalType(v || ''); }, status: formErrors.contractApprovalType ? 'error' : undefined }, React.createElement(Option, { value: '标准合同审批' }, '标准合同审批'), React.createElement(Option, { value: '非标准合同审批' }, '非标准合同审批'))) + ); + var contractFormRow1Part2 = React.createElement(Row, { gutter: 24 }, + React.createElement(FormItem, { label: '合同原件', required: true, error: formErrors.contractOriginal }, + contractOriginal + ? React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' } }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } }, + React.createElement(Button, { type: 'link', size: 'small', 'aria-label': '打开或预览合同原件:' + (contractOriginal.name || ''), onClick: function(e) { e.preventDefault(); openContractOriginal(); } }, contractOriginal.name), + contractOriginal.size || contractOriginal.uploadTime ? React.createElement('span', { style: { color: '#999', fontSize: 12 } }, (contractOriginal.size ? contractOriginal.size : '') + (contractOriginal.size && contractOriginal.uploadTime ? ' · ' : '') + (contractOriginal.uploadTime || '')) : null + ), + React.createElement(Button, { type: 'button', size: 'small', danger: true, onClick: function() { setContractOriginal(null); } }, '删除') + ) + : React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } }, + React.createElement('input', { ref: contractOriginalRef, type: 'file', style: { display: 'none' }, onChange: function(e) { + var f = e.target.files && e.target.files[0]; + if (f) { + var sizeDisplay = f.size >= 1024 * 1024 ? (f.size / 1024 / 1024).toFixed(1) + ' MB' : (f.size / 1024).toFixed(1) + ' KB'; + var now = window.moment ? window.moment() : new Date(); + var uploadTimeStr = window.moment ? now.format('YYYY-MM-DD HH:mm') : now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0') + '-' + String(now.getDate()).padStart(2, '0') + ' ' + String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0'); + setContractOriginal({ name: f.name, file: f, size: sizeDisplay, uploadTime: uploadTimeStr }); + } + e.target.value = ''; + } }), + React.createElement(Button, { type: 'default', style: { padding: '8px 16px', border: '1px solid #d9d9d9', borderRadius: 4, backgroundColor: '#fff', color: '#333', cursor: 'pointer', fontSize: 14 }, onClick: function() { if (contractOriginalRef.current) contractOriginalRef.current.click(); } }, '上传附件') + ) + ) + ); + var contractFormRow2 = React.createElement(Row, { gutter: 24 }, React.createElement(FormItem, { label: '备注', fullWidth: true }, React.createElement(Input.TextArea, { placeholder: '请输入备注信息', value: remarks, onChange: function(e) { setRemarks(e.target.value); }, style: styles.textarea, rows: 4 }))); + + var authorizedContent = React.createElement('div', null, + React.createElement('div', { style: { display: 'flex', gap: 12, alignItems: 'center', marginBottom: 8 } }, React.createElement('span', { style: { flex: 1, fontWeight: 600, fontSize: 13 } }, React.createElement('span', { style: { color: '#ff4d4f', marginRight: 4 } }, '*'), '被授权人姓名'), React.createElement('span', { style: { flex: 1, fontWeight: 600, fontSize: 13 } }, React.createElement('span', { style: { color: '#ff4d4f', marginRight: 4 } }, '*'), '被授权人联系电话'), React.createElement('span', { style: { flex: 1, fontWeight: 600, fontSize: 13 } }, React.createElement('span', { style: { color: '#ff4d4f', marginRight: 4 } }, '*'), '被授权人身份证'), React.createElement('span', { style: { flex: '0 0 80px' } })), + authorizedList.map(function(item, index) { return React.createElement('div', { key: index, style: styles.authRow }, React.createElement(Input, { style: Object.assign({}, styles.authInput, formErrors.authorizedList ? { borderColor: '#ff4d4f' } : {}, { flex: 1 }), placeholder: '请输入被授权人姓名', value: item.name, onChange: function(e) { updateAuthorized(index, 'name', e.target.value); } }), React.createElement(Input, { style: Object.assign({}, styles.authInput, formErrors.authorizedList ? { borderColor: '#ff4d4f' } : {}, { flex: 1 }), placeholder: '请输入被授权人联系电话', value: item.phone, onChange: function(e) { updateAuthorized(index, 'phone', e.target.value); } }), React.createElement(Input, { style: Object.assign({}, styles.authInput, formErrors.authorizedList ? { borderColor: '#ff4d4f' } : {}, { flex: 1 }), placeholder: '请输入被授权人身份证号', value: item.idCard, onChange: function(e) { updateAuthorized(index, 'idCard', e.target.value); } }), React.createElement(Button, { type: 'button', danger: true, onClick: function() { removeAuthorized(index); } }, '删除')); }), + formErrors.authorizedList ? React.createElement('div', { style: styles.errMsg }, formErrors.authorizedList) : null, + React.createElement(Button, { type: 'dashed', style: { width: '100%', marginBottom: 16 }, onClick: addAuthorized }, '添加一行') + ); + + var hydrogenFormRow = (function() { + var hydrogenFields = []; + hydrogenFields.push(React.createElement(FormItem, { label: '氢费承担方', required: true, error: formErrors.hydrogenBearer }, React.createElement(Select, { style: { width: '100%' }, value: hydrogenBearer || undefined, onChange: function(v) { setHydrogenBearer(v || ''); setHydrogenPaymentMethod(v === '客户' ? '预付' : ''); }, status: formErrors.hydrogenBearer ? 'error' : undefined }, React.createElement(Option, { value: '我方' }, '我方'), React.createElement(Option, { value: '客户' }, '客户')))); + if (hydrogenBearer === '客户') { hydrogenFields.push(React.createElement(FormItem, { label: '付款方式', required: true, error: formErrors.hydrogenPaymentMethod }, React.createElement(Select, { style: { width: '100%' }, value: hydrogenPaymentMethod || undefined, onChange: function(v) { setHydrogenPaymentMethod(v || ''); }, status: formErrors.hydrogenPaymentMethod ? 'error' : undefined }, React.createElement(Option, { value: '预付' }, '预付'), React.createElement(Option, { value: '月付款' }, '月付款'), React.createElement(Option, { value: '自行结算' }, '自行结算')))); } + var hydrogenPrepayInput = React.createElement(Input, { placeholder: '0.00', value: hydrogenPrepay, onChange: function(e) { setHydrogenPrepay(e.target.value); }, addonAfter: '元', status: formErrors.hydrogenPrepay ? 'error' : undefined, style: { width: '100%' } }); + var returnHydrogenInput = React.createElement(Input, { placeholder: '0.00', value: returnHydrogenPrice, onChange: function(e) { setReturnHydrogenPrice(e.target.value); }, addonAfter: '元', status: formErrors.returnHydrogenPrice ? 'error' : undefined, style: { width: '100%' } }); + var returnHydrogenColStyle = hydrogenPaymentMethod === '预付' ? { flex: '1 1 0', minWidth: 180 } : { flex: '0 0 calc(50% - 8px)', minWidth: 180 }; + hydrogenFields.push(React.createElement('div', { key: 'hydrogen-amount-row', style: { display: 'flex', gap: 16, flex: '1 1 100%' } }, hydrogenPaymentMethod === '预付' ? React.createElement(FormItem, { label: '氢气预付款', required: true, error: formErrors.hydrogenPrepay, colStyle: { flex: '1 1 0', minWidth: 180 } }, hydrogenPrepayInput) : null, React.createElement(FormItem, { label: '退还车氢气单价', required: true, error: formErrors.returnHydrogenPrice, colStyle: returnHydrogenColStyle }, returnHydrogenInput))); + return React.createElement.apply(React, [Row, { gutter: 24, style: { marginTop: 20 } }].concat(hydrogenFields)); + })(); + + var plateNoOptions = vehicleList.map(function(v) { return React.createElement(Option, { key: v.plateNo, value: v.plateNo }, v.plateNo); }); + var rentalTableBody = rentalOrders.map(function(row, idx) { + var modelOpts = row.brand ? (modelByBrand[row.brand] || []) : []; + var rowId = row.id != null ? row.id : idx; + return React.createElement('tr', { key: rowId }, + React.createElement('td', { style: styles.rentalTdCenter }, idx + 1), + React.createElement('td', { style: styles.rentalTd }, React.createElement(Select, { style: { width: '100%', minWidth: 90 }, value: row.brand || undefined, onChange: function(v) { updateRentalOrder(idx, 'brand', v || ''); }, placeholder: '请选择' }, brandList.map(function(b, i) { return React.createElement(Option, { key: i, value: b }, b); }))), + React.createElement('td', { style: styles.rentalTd }, React.createElement(Select, { style: { width: '100%', minWidth: 90 }, value: row.model || undefined, onChange: function(v) { updateRentalOrder(idx, 'model', v || ''); }, disabled: !row.brand, placeholder: '请选择' }, modelOpts.map(function(m, i) { return React.createElement(Option, { key: i, value: m }, m); }))), + React.createElement('td', { style: styles.rentalTd }, React.createElement(Select, { style: { width: '100%' }, placeholder: '请选择或输入搜索', value: row.plateNo || undefined, onChange: function(v) { updateRentalOrder(idx, 'plateNo', v || ''); }, showSearch: true, allowClear: true, filterOption: function(input, opt) { return opt && opt.children && String(opt.children).toLowerCase().indexOf((input || '').toLowerCase()) >= 0; } }, plateNoOptions)), + React.createElement('td', { style: styles.rentalTd }, React.createElement(Input, { value: row.vin || '', disabled: true, style: { width: '100%' } })), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: { display: 'flex', alignItems: 'center' } }, React.createElement(Input, { placeholder: '0.00', value: row.monthRent || '', onChange: function(e) { updateRentalOrder(idx, 'monthRent', e.target.value); }, style: styles.rentalInput }), React.createElement('span', { style: { marginLeft: 4, whiteSpace: 'nowrap' } }, '元'))), + React.createElement('td', { style: styles.rentalTdCenter }, React.createElement(Button, { type: 'link', size: 'small', onClick: function() { openServiceModal(idx); } }, '管理')), + React.createElement('td', { style: styles.rentalTdCenter }, calcRowServiceFee(row) + ' 元'), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: { display: 'flex', alignItems: 'center' } }, React.createElement(Input, { placeholder: '0.00', value: row.deposit || '', onChange: function(e) { updateRentalOrder(idx, 'deposit', e.target.value); }, style: styles.rentalInput }), React.createElement('span', { style: { marginLeft: 4, whiteSpace: 'nowrap' } }, '元'))), + React.createElement('td', { style: styles.rentalTd }, React.createElement(Input, { placeholder: '备注', value: row.remark || '', onChange: function(e) { updateRentalOrder(idx, 'remark', e.target.value); }, style: Object.assign({}, styles.rentalInput, { width: '100%' }) })), + React.createElement('td', { style: styles.rentalTdCenter }, React.createElement('div', { style: { display: 'inline-flex', gap: 4, alignItems: 'center' } }, React.createElement(Button, { type: 'link', size: 'small', 'aria-haspopup': 'dialog', 'aria-expanded': copyPopover && copyPopover.index === idx, onClick: function(e) { e.preventDefault(); e.stopPropagation(); openRentalCopyPopover(idx, e.currentTarget); } }, '复制'), React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function() { removeRentalRow(idx); } }, '删除'))) + ); + }); + + var rentalSummary = React.createElement('div', { style: styles.summaryList }, + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '租赁车辆数'), React.createElement('span', { style: styles.summaryListValue }, rentalTotalVehicles + ' 辆')), + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '租金及服务费合计'), React.createElement('span', { style: styles.summaryListValue }, rentalTotalRentService.toFixed(2) + ' 元')), + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '保证金总额'), React.createElement('span', { style: styles.summaryListValue }, rentalTotalDeposit.toFixed(2) + ' 元')), + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '氢气预付款金额'), React.createElement('span', { style: styles.summaryListValue }, rentalTotalHydrogen.toFixed(2) + ' 元')) + ); + var reqStarStyle = { color: '#ff4d4f', marginRight: 4 }; + var reqStar = React.createElement('span', { style: reqStarStyle }, '*'); + var rentalTh1 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 50, verticalAlign: 'middle' }) }, '序号'); + var rentalTh2 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 100 }) }, reqStar, '品牌'); + var rentalTh3 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 100 }) }, reqStar, '型号'); + var rentalTh4 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 120 }) }, '车牌号'); + var rentalTh5 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 160 }) }, '车辆识别代码'); + var rentalTh6 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 120 }) }, reqStar, '车辆月租金'); + var rentalTh7 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 80, verticalAlign: 'middle' }) }, '服务费项目'); + var rentalTh8 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 90, verticalAlign: 'middle' }) }, '服务费'); + var rentalTh9 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 100 }) }, reqStar, '保证金'); + var rentalTh10 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 80 }) }, '备注'); + var rentalTh11 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 90, verticalAlign: 'middle' }) }, '操作'); + var rentalTableThead = React.createElement('thead', null, React.createElement('tr', null, rentalTh1, rentalTh2, rentalTh3, rentalTh4, rentalTh5, rentalTh6, rentalTh7, rentalTh8, rentalTh9, rentalTh10, rentalTh11)); + var rentalTableTbody = React.createElement('tbody', null, rentalTableBody); + var rentalTableEl = React.createElement('table', { style: styles.rentalTable }, rentalTableThead, rentalTableTbody); + var rentalTableWrap = React.createElement('div', { style: { overflowX: 'auto', marginBottom: 16 } }, rentalTableEl); + var rentalCopyPopoverEl = copyPopover + ? React.createElement('div', { + id: 'rental-copy-popover', + role: 'dialog', + 'aria-modal': 'false', + 'aria-label': '复制租赁订单行', + style: Object.assign({}, styles.copyPopoverCard, { top: copyPopover.top, left: copyPopover.left }), + onMouseDown: function(e) { e.stopPropagation(); } + }, + React.createElement('div', { style: styles.copyPopoverTitle }, '复制本行'), + React.createElement('div', null, + React.createElement('label', { style: { display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 } }, '复制数量'), + React.createElement(Input, { + value: copyQuantity, + placeholder: '非负整数,如 3', + inputMode: 'numeric', + pattern: '[0-9]*', + autoComplete: 'off', + 'aria-label': '复制数量,非负整数', + onChange: function(e) { + var raw = (e.target && e.target.value) || ''; + var digits = raw.replace(/\D/g, ''); + setCopyQuantity(digits); + }, + onPressEnter: confirmRentalCopyPopover, + style: { width: '100%' } + }) + ), + React.createElement('div', { style: styles.copyPopoverFooter }, + React.createElement(Button, { size: 'small', onClick: function() { setCopyPopover(null); } }, '取消'), + React.createElement(Button, { type: 'primary', size: 'small', onClick: confirmRentalCopyPopover }, '确认') + ) + ) + : null; + + var rentalContent = React.createElement('div', null, rentalSummary, formErrors.rentalOrders ? React.createElement('div', { style: styles.errMsg }, formErrors.rentalOrders) : null, rentalTableWrap, React.createElement(Button, { type: 'dashed', style: { marginTop: 12, width: '100%' }, onClick: addRentalRow }, '添加一行'), hydrogenFormRow); + + var feeTableHeader3 = React.createElement('tr', null, React.createElement('th', { style: styles.rentalTh }, '项目'), React.createElement('th', { style: styles.rentalTh }, '收费标准'), React.createElement('th', { style: styles.rentalTh }, '服务费')); + var feeTableHeader5 = React.createElement('tr', null, React.createElement('th', { style: styles.rentalTh }, '类别'), React.createElement('th', { style: styles.rentalTh }, '损坏部位'), React.createElement('th', { style: styles.rentalTh }, '配件'), React.createElement('th', { style: styles.rentalTh }, '数量'), React.createElement('th', { style: styles.rentalTh }, '费用明细')); + var makeFeeRow3 = function(r, i) { + var td1 = React.createElement('td', { style: styles.rentalTd }, r.project); + var td2 = React.createElement('td', { style: styles.rentalTd }, r.standard); + var td3 = React.createElement('td', { style: styles.rentalTd }, r.serviceFee); + return React.createElement('tr', { key: i }, td1, td2, td3); + }; + var makeFeeRow5 = function(r, i) { + var td1 = React.createElement('td', { style: styles.rentalTd }, r.category); + var td2 = React.createElement('td', { style: styles.rentalTd }, r.part); + var td3 = React.createElement('td', { style: styles.rentalTd }, r.partName); + var td4 = React.createElement('td', { style: styles.rentalTd }, r.qty); + var td5 = React.createElement('td', { style: styles.rentalTd }, r.feeDetail); + return React.createElement('tr', { key: i }, td1, td2, td3, td4, td5); + }; + var feeCertRows = feeTemplateCertFees.map(makeFeeRow3); + var feePenaltyRows = feeTemplatePenaltyFees.map(makeFeeRow3); + var feeConsumablesRows = feeTemplateConsumables.map(makeFeeRow5); + var feeOtherRows = feeTemplateOtherFees.map(makeFeeRow3); + var feeCertTable = React.createElement('table', { style: styles.rentalTable }, React.createElement('thead', null, feeTableHeader3), React.createElement('tbody', null, feeCertRows)); + var feePenaltyTable = React.createElement('table', { style: styles.rentalTable }, React.createElement('thead', null, feeTableHeader3), React.createElement('tbody', null, feePenaltyRows)); + var feeConsumablesTable = React.createElement('table', { style: styles.rentalTable }, React.createElement('thead', null, feeTableHeader5), React.createElement('tbody', null, feeConsumablesRows)); + var feeOtherTable = React.createElement('table', { style: styles.rentalTable }, React.createElement('thead', null, feeTableHeader3), React.createElement('tbody', null, feeOtherRows)); + var feeTemplateSelect = React.createElement(Row, { gutter: 24 }, React.createElement(FormItem, { label: '选择费用模板', required: true, error: formErrors.feeTemplate }, React.createElement(Select, { placeholder: '请选择费用模板', style: { width: '100%' }, value: feeTemplate || undefined, onChange: function(v) { setFeeTemplate(v || ''); }, status: formErrors.feeTemplate ? 'error' : undefined }, feeTemplates.map(function(f, i) { return React.createElement(Option, { key: i, value: f }, f); })))); + var feeTemplateBody = feeTemplate ? React.createElement('div', null, + React.createElement('div', { style: Object.assign({}, styles.feeSectionTitle, styles.feeSectionTitleFirst) }, '证照补办费用'), + feeCertTable, + React.createElement('div', { style: styles.feeSectionTitle }, '违约金费用'), + feePenaltyTable, + React.createElement('div', { style: styles.feeSectionTitle }, '易损件信息'), + feeConsumablesTable, + React.createElement('div', { style: styles.feeSectionTitle }, '其他费用信息'), + feeOtherTable + ) : null; + var feeContent = React.createElement('div', null, feeTemplateSelect, feeTemplateBody); + + var billingBtnBase = { padding: '12px 16px', border: '1px solid #d9d9d9', borderRadius: 0, backgroundColor: '#fff', color: '#333', cursor: 'pointer', fontSize: 14, textAlign: 'left', flex: 1, minWidth: 0, width: 0, minHeight: 88, height: 'auto', display: 'flex', flexDirection: 'column', alignItems: 'flex-start', justifyContent: 'flex-start', overflow: 'visible' }; + var billingContent = React.createElement('div', null, + React.createElement('div', { style: { display: 'flex', border: formErrors.billingMethod ? '1px solid #ff4d4f' : '1px solid #d9d9d9', borderRadius: 4, overflow: 'hidden', alignItems: 'stretch' } }, + React.createElement(Button, { type: 'button', 'aria-pressed': billingMethod === 'month', 'aria-label': '账单按自然月结算', style: Object.assign({}, billingBtnBase, { borderRight: '1px solid #d9d9d9' }, billingMethod === 'month' ? { backgroundColor: '#1677ff', color: '#fff', borderColor: '#1677ff' } : {}), onClick: function() { setBillingMethod('month'); } }, React.createElement('div', { style: { width: '100%' } }, React.createElement('div', { style: { fontWeight: 600, marginBottom: 6 } }, '按自然月结算'), React.createElement('div', { style: { fontSize: 12, opacity: 0.9, whiteSpace: 'normal', wordBreak: 'break-word', lineHeight: 1.5 } }, '账单按照第一个月计费日期开始-当月最后一天为第一期,之后按自然月方式形成每一期账单,例如付款周期为2个月,则第二期账单从2月1日-3月31日。'))), + React.createElement(Button, { type: 'button', 'aria-pressed': billingMethod === 'period', 'aria-label': '账单按付款周期天数结算', style: Object.assign({}, billingBtnBase, billingMethod === 'period' ? { backgroundColor: '#1677ff', color: '#fff', borderColor: '#1677ff' } : {}), onClick: function() { setBillingMethod('period'); } }, React.createElement('div', { style: { width: '100%' } }, React.createElement('div', { style: { fontWeight: 600, marginBottom: 6 } }, '按付款周期天数结算'), React.createElement('div', { style: { fontSize: 12, opacity: 0.9, whiteSpace: 'normal', wordBreak: 'break-word', lineHeight: 1.5 } }, '账单按照合同基本信息中付款周期实际天数形成一期账单,例如付款周期为60天,则该账单账期为60天,每60天会自动生成新一期账单。'))) + ), + formErrors.billingMethod ? React.createElement('div', { style: styles.errMsg }, formErrors.billingMethod) : null + ); + + var serviceModalRows = serviceModalRowIndex !== null && rentalOrders[serviceModalRowIndex] ? rentalOrders[serviceModalRowIndex].serviceItems : []; + var serviceModalContent = serviceModalRowIndex !== null ? React.createElement('div', { style: styles.modalMask, onClick: function(e) { if (e.target === e.currentTarget) closeServiceModal(); } }, React.createElement('div', { style: styles.modalBox, onClick: function(e) { e.stopPropagation(); } }, React.createElement('div', { style: styles.modalHeader }, '服务项目'), React.createElement('div', { style: styles.modalBody }, React.createElement('table', { style: styles.rentalTable }, React.createElement('thead', null, React.createElement('tr', null, React.createElement('th', { style: styles.rentalTh }, '服务项目'), React.createElement('th', { style: styles.rentalTh }, '费用'), React.createElement('th', { style: styles.rentalTh }, '生效时间'), React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 80 }) }, '操作'))), React.createElement('tbody', null, serviceModalRows.map(function(si, siIdx) { var filteredServiceOpts = serviceItemOptions.filter(function(o) { return !serviceItemSearch || o.indexOf(serviceItemSearch) !== -1; }); return React.createElement('tr', { key: siIdx }, React.createElement('td', { style: styles.rentalTd }, React.createElement(Select, { style: { width: '100%' }, placeholder: '请选择服务项目', value: si.project || undefined, onChange: function(v) { updateServiceItem(siIdx, 'project', v || ''); }, showSearch: true, filterOption: function(input, opt) { return opt && opt.children && String(opt.children).toLowerCase().indexOf((input || '').toLowerCase()) >= 0; }, allowClear: true }, serviceItemOptions.map(function(opt, oi) { return React.createElement(Option, { key: oi, value: opt }, opt); }))), React.createElement('td', { style: styles.rentalTd }, React.createElement(Input, { placeholder: '0.00', value: si.fee || '', onChange: function(e) { updateServiceItem(siIdx, 'fee', e.target.value); }, addonAfter: '元', style: { width: '100%' } })), React.createElement('td', { style: styles.rentalTd }, React.createElement(DatePicker, { style: { width: '100%' }, format: 'YYYY-MM-DD', placeholder: '请选择生效时间', value: si.effectiveDate && window.moment ? window.moment(si.effectiveDate, 'YYYY-MM-DD') : null, onChange: function(d, dateStr) { updateServiceItem(siIdx, 'effectiveDate', dateStr || ''); } })), React.createElement('td', { style: styles.rentalTd }, React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function() { removeServiceItem(siIdx); } }, '删除'))); }))), React.createElement(Button, { type: 'dashed', style: { marginTop: 12, width: '100%' }, onClick: addServiceItem }, '添加一行')), React.createElement('div', { style: { padding: '12px 20px', borderTop: '1px solid #f0f0f0', textAlign: 'right', display: 'flex', gap: 12, justifyContent: 'flex-end' } }, React.createElement(Button, { type: 'primary', onClick: function() { closeServiceModal(); } }, '保存'), React.createElement(Button, { onClick: closeServiceModal }, '关闭')))) : null; + + var requirementContent = '车辆租赁合同-新增(2026年3月3日版本)\n「数字化资产ONE-OS运管平台」中的「车辆租赁合同」-「新增租赁合同」模块,点击车辆租赁合同右上角「新增」进行创建;\n1.面包屑:\n#业务管理-车辆租赁合同-新增合同\n\n2.客户基本信息卡片:\n#用于从客户列表中选择客户,并将该合同绑定到业务部门及业务负责人(绑定业务部门/业务负责人主要为了后期从部门/业务负责人维度进行数据统计);\n2.1.客户名称:必选项,选择器,支持从输入框内输入内容进行模糊搜索,从客户信息列表中选择对应客户(只显示已通过审核的客户);\n2.2.客户统一信用代码:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「信用代码」字段;\n2.3.客户地址:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「客户地址」字段;\n2.4.客户联系人:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「客户联系人」字段;\n2.5.客户电话:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「客户电话」字段;\n2.6.客户电子邮箱:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「客户电子邮箱」字段;\n2.7.企业名称:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「企业名称」字段;\n2.8.企业电话:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「企业电话」字段;\n2.9.邮寄地址:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「邮寄地址」字段;\n2.10.开户银行:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「开户银行」字段;\n2.11.银行账号:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「银行账号」字段;\n2.12.纳税人识别号:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「纳税人识别号」字段;\n2.13.业务部门:必选项,选择器,从部门表中选择该租赁合同对应部门;\n2.14.业务负责人:必选项,选择器,从已选业务部门下拉取对应业务负责人,未选择业务部门时,业务负责人字段不可选;\n\n3.合同基本信息卡片:\n#用于定义租赁合同基本情况和付款方式;\n3.1.项目名称:必填项,输入框,用于定义该合同项目名称,默认提示信息"请输入项目名称";\n3.2.合同类型:必选项,选择器,合同类型分为「正式合同」「试用合同」;\n3.3.生效日期:必选项,日期选择器,格式为YYYY-MM-DD,精确至天,默认为点击新增日期;\n3.4.付款方式:必选项,付款方式分为「预付」「后付」两种;\n 3.4.1.如果选择预付,以对应「付款方式」和「付款周期」规则,在每一期新账单生成就列入业务待办(工作台功能),同时以消息通知对应用户;\n 3.4.2.如果选择后付,以对应「付款方式」和「付款周期」规则,在每一期新账单生成时,将前一期账单列入业务待办(工作台功能),同时以消息通知对应用户;\n3.5.主要车型:输入框(禁用状态),根据租赁订单信息中所有所选车型,自动反写入输入框并以标签形式显示,支持多车型显示,标签显示:型号名称;\n3.6.结束日期:必选项,日期选择器,格式为YYYY-MM-DD,精确至天;\n 3.6.1.合同结束日期前30天将以消息提醒方式提醒(消息中心、工作台);\n 3.6.2.到达合同结束日期时,租赁账单将会立刻停止计算,作为最后一期账单;\n 3.6.3.续签合同/转正式合同将重新生成账单,不会对旧合同账单做任何继承处理;\n3.7.付款周期:必选项,选择器,支持1个月-12个月 12种付款周期,账单将以此周期和账单计算方式规则,从交车任务形成的交车单进行完整交车后,定时自动生成账单;\n3.8.签约公司:必选项,选择器,从组织机构表中获取所有根组织机构(如嘉兴羚牛、上海羚牛、广东羚牛等),默认显示新增用户当前机构,可手动修改,后期需要考虑从签约公司维度统计合同相关数据;\n3.9.交车区域:必选项,地区选择器,支持省-市2级,选择区域后,该任务生成交车任务时会自动推送至该区域负责运维人员;\n3.10.交车地点:必填项,输入框,支持自定义输入交车地点;\n3.11.合同审批类型:必填项,选择器,分为「标准合同审批」「非标准合同审批」;\n3.12.合同原件:必填项,按钮,按钮文字为:上传附件,支持多个附件上传(doc/docx/pdf格式);\n3.13.备注:文本域,支持自定义输入备注信息;\n\n4.被授权人信息卡片:\n#用于定义租赁合同相关被授权人相关信息,被授权人在交车单完成时,需要选择被授权人,并通过被授权人手机短信,在E签宝进行签字确认;\n4.1.被授权人:必填项,输入框,用于输入被授权人信息;\n4.2.被授权人联系电话:必填项,输入框,用于输入被授权人联系电话,该电话后续需要接收E签宝签字链接;\n4.3.被授权人身份证:必填项,输入框,用于输入被授权人身份证信息;\n4.4.支持通过新增/删除一行的方式,创建或管理多个授权人,后续交车单完成时可从多个授权人中选择接收授权人;\n\n5.租赁订单信息卡片:\n#用于定义租赁合同对应车辆明细费用、氢费明细费用等相关信息;\n5.1.上方为租赁车辆总计数据,包括租赁车辆数、租金及服务费合计、保证金总额、氢气预付款金额等相关信息;\n 5.1.1.租赁车辆数:显示下方租赁订单信息包含多少辆车;\n 5.1.2.租金及服务费合计:显示下方租赁订单信息中车辆租金总额及服务费总额;\n 5.1.3.保证金总额:显示下方租赁订单信息中车辆保证金总额;\n 5.1.4.氢气预付款金额:显示氢气预付款金额,如客户选择自行承担氢费或羚牛承担氢费,则该处为0不计入氢气预付款金额;\n5.2.下方为列表,显示序号、品牌、型号、车牌号、车辆识别代码、车辆月租金(元)、服务费项目、服务费、保证金、备注,默认且至少显示一行空数据;\n 5.2.1.序号:自动按照条数生成,规则为1、2、3....以此类推;\n 5.2.2.品牌:必选项,选择器,从型号参数库中「品牌」字段拉取所有品牌;\n 5.2.3.型号:必选项,选择器,与品牌存在级联关系,未选择品牌则无法选择型号;\n 5.2.4.车牌号:选填项,选择器(支持从输入框输入车牌号关键字下拉匹配),可通过选择车牌号对品牌、型号进行反写;\n 5.2.5.车辆识别代码:输入框(禁用),显示该车辆对应车辆识别代码,根据所选车牌号从车辆表直接拉取进行反写;\n 5.2.6.车辆月租金:输入框,支持两位小数,用于输入该车辆月租金金额,输入框后缀为元,如还车时账单周期不满1个月,则按照:(车辆月租金/30)* 实际天数进行计算;\n 5.2.7.服务费项目:点击管理按钮弹出卡片,卡片标题为:服务项目,下方列表显示服务项目、费用、生效时间、操作;\n 5.2.7.1.服务项目:必选项,选择器(支持输入框输入服务项目关键字进行下拉匹配),选项包含:代处理费用、罚款、违章处理违约金、未参加安全培训、车辆出险、年检年审违约、停车费、设备损坏金(包含易损件)、清洗费、上门收车人工费、上门收车送车行驶费、上门收车基础服务费、保险上浮、保养费用、补办驾驶证、补办牌照、补办营运证、补办加氢证、借用备用钥匙、补配钥匙、租金、氢气费-客、退还车氢量差、能源费补缴、能源费退款、送车上门人工费、送车上门送车行驶费、送车上门基础服务费、保证金、氢气预付费、维修费用、ETC-客、ETC卡缺损费、ETC设备缺损费、电费-客、未结算保养费、未结算维修费、车损费、工具损坏或丢失费、证件费、广告损坏费、送车服务费、接车服务费、补办行驶证、超赔险、轮胎磨损费、无忧包、轮胎保、养护保、尾板;\n 5.2.7.2.费用:必填项,输入框,支持2位小数,输入框后缀为元;\n 5.2.7.3.生效时间:必选项,日期选择器,格式为YYYY-MM-DD;\n 5.2.7.4.操作:删除,点击删除直接删除该行数据;\n 5.2.7.5.新增一行数据:点击添加一行服务项目;\n #在提车应收款中,车辆的交车日期和服务费生效日期可能会存在不同的情况,所以服务费需要单独以生效时间进行计算,例如:\n 交车后车辆从1月1日开始计费,付款周期为2个月,则提车应收款租金会计算到3月1日,但是服务费生效时间为1月30日,此情况下需要以3月1日-1月30日,计算出服务费具体收费天数为30天,然后根据:(服务费费用/30)* 服务费收费天数30)进行计算;\n 5.2.8.服务费:自动根据添加的所有服务费项目计算总额,支持2位小数,格式为:xx.xx元;\n 5.2.9.保证金:必填项,输入框,支持2位小数,后缀为元,用于填写车辆需要支付的保证金金额,保证金为提车应收款一次性支付,还车时需要计入退还费用中;\n 5.2.10.备注:选填项,输入框,用于备注车辆复杂情况;\n 5.2.11.操作:删除,点击删除删除该行数据;\n 5.2.12.添加一行:点击后列表新增一行,用于填写一条新的车辆租金费用信息;\n5.3.氢费承担方:必选项,选择器,选项为:「我方」、「客户」,默认选择「客户」;\n 5.3.1.选择「我方」:不显示付款方式、氢气预付款;\n 5.3.2.选择「客户」:付款方式字段默认为预付,可手动修改;\n5.4.付款方式:必选项,选择器,选项为:「预付」、「月付款」、「自行结算」,默认为:「预付」;\n 5.4.1.预付:选择「预付」,需要填写:「氢气预付款」,氢气预付款指合同签署时客户就需预先付出的氢费款项,该部分款项会自动计入提车应收款中氢气预付款金额;\n 5.4.2.月付款:选择「月付款」,提车应收款中不进行收费,而是由业务人员按照实际情况,通过氢费账单功能生成对应氢费账单,单独与客户进行结算;\n 5.4.3.自行结算:选择「自行结算」,指合同签署后,所有氢气费用由客户自行承担;\n5.5.氢气预付款:选择「客户」「预付」时显示,必填项,输入框,支持2位小数,氢气预付款金额会计算入该合同交车应收款中,并计入5.1.4.氢气预付款金额中;\n5.6.退还车氢气单价:必填项,输入框,支持2位小数,后缀为元,不管氢费承担方和付款方式选择任何选项都需要进行维护,该金额主要用于与客户约定还车时,与交车时氢气差值以此费用进行自动计算和结算;\n\n6.其他费用信息卡片:\n#用于选择对应租赁费用模板,选择后展示证照补办费用、违约金费用、易损件费用、其他费用等信息,租赁费用模板管理功能位于「车辆租赁合同」列表左上角;\n6.1.选择费用模板:必选项,从「租赁费用模板」中拉取,选择后自动将该费用模板所有环节费用显示在合同中;\n 6.1.1.证照补办费用:单独标题显示,内容为列表,列表中包括项目、收费标准、服务费,选择费用模板后自动反显;\n 6.1.2.违约金费用:单独标题显示,内容为列表,列表中包含项目、收费标准、服务费,选择费用模板后自动反显;\n 6.1.3.易损件信息:单独标题显示,内容为列表,列表中包含类别、损坏部位、配件、数量、费用明细,选择费用模板后自动反显;\n 6.1.4.其他费用信息:单独标题显示,内容为列表,列表中包含项目、收费标准、服务费,选择费用模板后自动反显;\n\n7.账单计算方式卡片:\n#必选项,填充按钮组,默认为按自然月结算,可手动修改,用于定义租赁合同的账单计算方式,分为「按自然月结算」、「按付款周期天数结算」两种方式;\n7.1.按付款周期天数结算:账单按照合同基本信息卡片中每隔付款周期*30天形成一期账单;\n 例如付款周期为2个月,则交车任务交车成功时,提车应收款从交车任务配置的开始计费时间开始,根据60天收取车辆租金,此后每隔60天生成一期租赁账单;\n7.2.按自然月结算:账单按照第一个月计费开始日期到当月最后一天为第一期,之后按照付款周期所选月份间隔,从开始月份第一天到间隔月份最后一天的自然月方式形成一期账单;\n 例如付款周期为2个月,则交车任务交车成功时,提车应收款车辆租金需要收取≥2个月租金作为标准流程,<2个月租金作为非标流程;\n 租赁账单首期从交车任务配置的开始计费时间开始,如付款周期为2个月,则首期账单结束时间为第二个月最后一天,付款周期为3个月,则首期账单结束时间为第三个月最后一天,此后每一期按照实际自然月开始-结束形成账单,例如付款周期为2个月,则第二期账单从2月1日-3月31日,以此类推;\n\n8.最下方为提交并审核、保存、取消三个按钮;\n8.1.点击提交并审核,toast提示:租赁合同已提交审核。同时该租赁合同进入租赁合同审核列表中;\n8.2.点击保存,会存储租赁订单已填写内容,不做必填项校验,同时显示在租赁合同列表中,该条数据只能保存人自己查看并编辑,其他人无法操作;\n8.3.点击取消,如当前页面有已编辑内容时,点击取消会进行二次提示,内容为:取消将会丢失所有已填写内容,是否确认?点击确认返回车辆租赁合同列表页;\n\n所有卡片支持收起/展开功能,通过点击卡片右侧收起/展开实现;并增加锚点功能,锚点固定于页面右上角,点击锚点对应卡片名称,页面自动跳转至该卡片所在区域;'; + var reqSpecModalContent = reqSpecOpen ? React.createElement('div', { style: styles.modalMask, onClick: function(e) { if (e.target === e.currentTarget) setReqSpecOpen(false); } }, React.createElement('div', { style: Object.assign({}, styles.modalBox, { maxWidth: 720 }), onClick: function(e) { e.stopPropagation(); } }, React.createElement('div', { style: styles.modalHeader }, '需求说明'), React.createElement('div', { style: Object.assign({}, styles.modalBody, { maxHeight: '70vh', padding: '20px 24px', overflow: 'auto' }) }, React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6, color: 'rgba(0,0,0,0.85)' } }, requirementContent)), React.createElement('div', { style: { padding: '12px 20px', borderTop: '1px solid #f0f0f0', textAlign: 'right' } }, React.createElement(Button, { onClick: function() { setReqSpecOpen(false); } }, '关闭')))) : null; + + var acroCss = React.createElement('style', null, ` + .acro-text-xs { font-size: 12px; } + .acro-text-sm { font-size: 13px; } + .acro-text-base { font-size: 14px; } + .acro-text-lg { font-size: 16px; } + .acro-text-xl { font-size: 20px; } + .acro-text-xxl { font-size: 24px; } + .acro-title-promo { font-size: 36px; } + `); + + return React.createElement('div', { style: styles.page }, + acroCss, + React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 } }, React.createElement('div', { style: styles.breadcrumb, className: 'acro-text-base' }, React.createElement('span', null, '业务管理'), React.createElement('span', { style: styles.breadcrumbSep }, ' / '), React.createElement('span', null, '车辆租赁合同'), React.createElement('span', { style: styles.breadcrumbSep }, ' / '), React.createElement('span', { style: { color: '#1677ff' } }, '新增租赁合同')), React.createElement('button', { type: 'button', 'aria-label': '打开需求说明', style: { color: '#1677ff', cursor: 'pointer', border: 'none', background: 'none', padding: '8px 4px', minHeight: 40, borderRadius: 6 }, className: 'acro-text-base', onClick: function() { setReqSpecOpen(true); } }, '查看需求说明')), + React.createElement('div', { style: styles.anchorWrap, role: 'navigation', 'aria-label': '页面区块锚点导航' }, + React.createElement('div', { style: { marginBottom: 8, fontWeight: 600, fontSize: 14 } }, '锚点导航'), + React.createElement('button', { type: 'button', style: styles.anchorItem, 'aria-label': '跳转到客户基本信息', onClick: function() { scrollToCard('card-customer'); } }, '客户基本信息'), + React.createElement('button', { type: 'button', style: styles.anchorItem, 'aria-label': '跳转到合同基本信息', onClick: function() { scrollToCard('card-contract'); } }, '合同基本信息'), + React.createElement('button', { type: 'button', style: styles.anchorItem, 'aria-label': '跳转到被授权人信息', onClick: function() { scrollToCard('card-authorized'); } }, '被授权人信息'), + React.createElement('button', { type: 'button', style: styles.anchorItem, 'aria-label': '跳转到租赁订单信息', onClick: function() { scrollToCard('card-rental'); } }, '租赁订单信息'), + React.createElement('button', { type: 'button', style: styles.anchorItem, 'aria-label': '跳转到其他费用信息', onClick: function() { scrollToCard('card-fee'); } }, '其他费用信息'), + React.createElement('button', { type: 'button', style: styles.anchorItem, 'aria-label': '跳转到账单计算方式', onClick: function() { scrollToCard('card-billing'); } }, '账单计算方式') + ), + React.createElement(CardBlock, { id: 'card-customer', title: '客户基本信息', collapsed: cc1, setCollapsed: setCc1 }, customerFields), + React.createElement(CardBlock, { id: 'card-contract', cardStyle: { marginTop: 16 }, title: '合同基本信息', collapsed: cc2, setCollapsed: setCc2 }, React.createElement('div', null, contractFormRow1, contractFormRow1Part2, contractFormRow2)), + React.createElement(CardBlock, { id: 'card-authorized', cardStyle: { marginTop: 16 }, title: '被授权人信息', collapsed: cc3, setCollapsed: setCc3 }, authorizedContent), + React.createElement(CardBlock, { id: 'card-rental', cardStyle: { marginTop: 16 }, title: '租赁订单信息', collapsed: cc4, setCollapsed: setCc4 }, rentalContent), + React.createElement(CardBlock, { id: 'card-fee', cardStyle: { marginTop: 16 }, title: '其他费用信息', collapsed: cc5, setCollapsed: setCc5 }, feeContent), + React.createElement(CardBlock, { id: 'card-billing', cardStyle: { marginTop: 16 }, title: '账单计算方式', collapsed: cc6, setCollapsed: setCc6 }, billingContent), + React.createElement('div', { style: { height: 60 } }), + rentalCopyPopoverEl, + serviceModalContent, + reqSpecModalContent, + React.createElement('div', { style: styles.footer }, React.createElement(Button, { type: 'primary', size: 'large', onClick: function() { if (validateSubmitAndReview()) { message.success('租赁合同已提交审核。'); } } }, '提交并审核'), React.createElement(Button, { size: 'large', onClick: function() { message.info('保存,加入租赁合同列表(仅操作人可查看编辑)'); } }, '保存'), React.createElement(Button, { size: 'large', onClick: function() { message.info('取消'); } }, '取消')) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-lease-contract/pages/05-车辆租赁合同-新增车辆.jsx b/src/prototypes/oneos-web-lease-contract/pages/05-车辆租赁合同-新增车辆.jsx new file mode 100644 index 0000000..1b80454 --- /dev/null +++ b/src/prototypes/oneos-web-lease-contract/pages/05-车辆租赁合同-新增车辆.jsx @@ -0,0 +1,635 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 业务管理 - 车辆租赁合同 - 新增车辆(与查看合同页布局一致,仅新增车辆信息卡片可编辑) + +const Component = function() { + var useState = React.useState; + var useCallback = React.useCallback; + var antd = window.antd; + var Select = antd.Select; + var Input = antd.Input; + var Button = antd.Button; + var Table = antd.Table; + var DatePicker = antd.DatePicker; + var message = antd.message; + var Option = Select.Option; + + var cc1State = React.useState(false); + var cc1 = cc1State[0]; + var setCc1 = cc1State[1]; + var cc1bState = React.useState(false); + var cc1b = cc1bState[0]; + var setCc1b = cc1bState[1]; + var cc2State = React.useState(false); + var cc2 = cc2State[0]; + var setCc2 = cc2State[1]; + var cc3State = React.useState(false); + var cc3 = cc3State[0]; + var setCc3 = cc3State[1]; + var cc4State = React.useState(false); + var cc4 = cc4State[0]; + var setCc4 = cc4State[1]; + var cc5State = React.useState(false); + var cc5 = cc5State[0]; + var setCc5 = cc5State[1]; + var cc6State = React.useState(false); + var cc6 = cc6State[0]; + var setCc6 = cc6State[1]; + var cc7State = React.useState(false); + var cc7 = cc7State[0]; + var setCc7 = cc7State[1]; + var cc8State = React.useState(false); + var cc8 = cc8State[0]; + var setCc8 = cc8State[1]; + var cc9State = React.useState(false); + var cc9 = cc9State[0]; + var setCc9 = cc9State[1]; + + var reqSpecState = React.useState(false); + var reqSpecOpen = reqSpecState[0]; + var setReqSpecOpen = reqSpecState[1]; + var cancelConfirmState = React.useState(false); + var cancelConfirmOpen = cancelConfirmState[0]; + var setCancelConfirmOpen = cancelConfirmState[1]; + var editedState = React.useState(false); + var edited = editedState[0]; + var setEdited = editedState[1]; + + var emptyNewRow = { brand: '', model: '', plateNo: '', vin: '', monthRent: '', serviceItems: [{ project: '', fee: '', effectiveDate: '' }], deposit: '', remark: '' }; + var _newVehicleRows = useState([Object.assign({}, emptyNewRow)]); + var newVehicleRows = _newVehicleRows[0]; + var setNewVehicleRows = _newVehicleRows[1]; + var _newVehicleContractOriginals = useState([]); + var newVehicleContractOriginals = _newVehicleContractOriginals[0]; + var setNewVehicleContractOriginals = _newVehicleContractOriginals[1]; + var newVehicleContractInputRef = React.useRef(null); + var _serviceModalRowIndex = useState(null); + var serviceModalRowIndex = _serviceModalRowIndex[0]; + var setServiceModalRowIndex = _serviceModalRowIndex[1]; + + var brandList = ['品牌A', '品牌B', '品牌C', '品牌D']; + var serviceItemOptions = ['代处理费用', '罚款', '违章处理违约金', '未参加安全培训', '车辆出险', '年检年审违约', '停车费', '设备损坏金(包含易损件)', '清洗费', '上门收车人工费', '上门收车送车行驶费', '上门收车基础服务费', '保险上浮', '保养费用', '补办驾驶证', '补办牌照', '补办营运证', '补办加氢证', '借用备用钥匙', '补配钥匙', '租金', '氢气费-客', '退还车氢量差', '能源费补缴', '能源费退款', '送车上门人工费', '送车上门送车行驶费', '送车上门基础服务费', '保证金', '氢气预付费', '维修费用', 'ETC-客', 'ETC卡缺损费', 'ETC设备缺损费', '电费-客', '未结算保养费', '未结算维修费', '车损费', '工具损坏或丢失费', '证件费', '广告损坏费', '送车服务费', '接车服务费', '补办行驶证', '超赔险', '轮胎磨损费', '无忧包', '轮胎保', '养护保', '尾板']; + var modelByBrand = { '品牌A': ['型号A1', '型号A2', '型号A3'], '品牌B': ['型号B1', '型号B2'], '品牌C': ['型号C1', '型号C2'], '品牌D': ['型号D1'] }; + var plateNoOptions = ['浙A10001', '浙A10002', '浙B20001', '浙B20002', '浙C30001', '浙C30002', '沪D40001', '沪D40002', '苏E50001', '苏E50002', '京F60001', '京F60002'].map(function(p) { return { value: p, label: p }; }); + + var vehicleList = [ + { plateNo: '浙A10001', vin: 'L1234567890ABCDEF', brand: '品牌A', model: '型号A1' }, + { plateNo: '浙B20002', vin: 'L2234567890ABCDEF', brand: '品牌A', model: '型号A2' }, + { plateNo: '沪D40001', vin: 'L3234567890ABCDEF', brand: '品牌B', model: '型号B1' } + ]; + + var addNewVehicleRow = useCallback(function() { + setEdited(true); + setNewVehicleRows(function(prev) { return prev.concat([{ brand: '', model: '', plateNo: '', vin: '', monthRent: '', serviceItems: [{ project: '', fee: '', effectiveDate: '' }], deposit: '', remark: '' }]); }); + }, []); + var removeNewVehicleRow = useCallback(function(index) { + setEdited(true); + setNewVehicleRows(function(prev) { + var next = prev.slice(); + next.splice(index, 1); + if (next.length === 0) next = [{ brand: '', model: '', plateNo: '', vin: '', monthRent: '', serviceItems: [{ project: '', fee: '', effectiveDate: '' }], deposit: '', remark: '' }]; + return next; + }); + }, []); + var copyNewVehicleRow = useCallback(function(index) { + setEdited(true); + var row = newVehicleRows[index]; + if (!row) return; + var serviceItemsCopy = (row.serviceItems || []).map(function(si) { return { project: si.project || '', fee: si.fee || '', effectiveDate: si.effectiveDate || '' }; }); + if (serviceItemsCopy.length === 0) serviceItemsCopy = [{ project: '', fee: '', effectiveDate: '' }]; + var newRow = { brand: row.brand || '', model: row.model || '', plateNo: '', vin: '', monthRent: row.monthRent || '', serviceItems: serviceItemsCopy, deposit: row.deposit || '', remark: row.remark || '' }; + setNewVehicleRows(function(prev) { + var next = prev.slice(); + next.splice(index + 1, 0, newRow); + return next; + }); + message.success('已复制该行(车牌号已清空)'); + }, [newVehicleRows]); + var updateNewVehicleRow = useCallback(function(index, field, value) { + setEdited(true); + setNewVehicleRows(function(prev) { + var next = prev.slice(); + var row = next[index] || emptyNewRow; + var o = {}; + o[field] = value; + if (field === 'brand') o.model = ''; + if (field === 'plateNo') { + var v = null; + for (var vi = 0; vi < vehicleList.length; vi++) { + if (vehicleList[vi].plateNo === value) { v = vehicleList[vi]; break; } + } + if (v) { + o.vin = v.vin || ''; + if ((!row.brand && !row.model) && (v.brand || v.model)) { + o.brand = v.brand || ''; + o.model = v.model || ''; + } + } else { + o.vin = ''; + } + } + next[index] = Object.assign({}, row, o); + return next; + }); + }, []); + + var calcRowServiceFee = function(row) { + var sum = 0; + for (var i = 0; i < (row.serviceItems || []).length; i++) { + var fee = parseFloat(row.serviceItems[i].fee); + if (!isNaN(fee)) sum += fee; + } + return sum.toFixed(2); + }; + var openServiceModal = function(index) { setServiceModalRowIndex(index); }; + var closeServiceModal = function() { setServiceModalRowIndex(null); }; + var addServiceItem = function() { + if (serviceModalRowIndex === null) return; + setEdited(true); + var next = newVehicleRows.slice(); + var row = next[serviceModalRowIndex]; + var items = (row.serviceItems || []).concat([{ project: '', fee: '', effectiveDate: '' }]); + var newRow = {}; + for (var k in row) { if (row.hasOwnProperty(k) && k !== 'serviceItems') newRow[k] = row[k]; } + newRow.serviceItems = items; + next[serviceModalRowIndex] = newRow; + setNewVehicleRows(next); + }; + var removeServiceItem = function(siIndex) { + if (serviceModalRowIndex === null) return; + setEdited(true); + var next = newVehicleRows.slice(); + var row = next[serviceModalRowIndex]; + var items = (row.serviceItems || []).slice(); + items.splice(siIndex, 1); + if (items.length === 0) items = [{ project: '', fee: '', effectiveDate: '' }]; + var newRow = {}; + for (var k in row) { if (row.hasOwnProperty(k) && k !== 'serviceItems') newRow[k] = row[k]; } + newRow.serviceItems = items; + next[serviceModalRowIndex] = newRow; + setNewVehicleRows(next); + }; + var updateServiceItem = function(siIndex, field, value) { + if (serviceModalRowIndex === null) return; + setEdited(true); + var next = newVehicleRows.slice(); + var row = next[serviceModalRowIndex]; + var items = (row.serviceItems || []).slice(); + var item = items[siIndex] || {}; + var newItem = {}; + for (var k in item) { if (item.hasOwnProperty(k)) newItem[k] = item[k]; } + newItem[field] = value; + items[siIndex] = newItem; + var newRow = {}; + for (var rk in row) { if (row.hasOwnProperty(rk) && rk !== 'serviceItems') newRow[rk] = row[rk]; } + newRow.serviceItems = items; + next[serviceModalRowIndex] = newRow; + setNewVehicleRows(next); + }; + var newOrderSummary = (function() { + var count = newVehicleRows.length; + var rentService = 0; + var depositTotal = 0; + for (var i = 0; i < newVehicleRows.length; i++) { + rentService += parseFloat(newVehicleRows[i].monthRent) || 0; + rentService += parseFloat(calcRowServiceFee(newVehicleRows[i])) || 0; + depositTotal += parseFloat(newVehicleRows[i].deposit) || 0; + } + return { vehicleCount: count, totalRentService: rentService.toFixed(2), totalDeposit: depositTotal.toFixed(2) }; + })(); + + var scrollToCard = function(id) { + var el = document.getElementById(id); + if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }; + + var styles = { + page: { padding: '16px 24px 48px', backgroundColor: '#f5f5f5', minHeight: '100vh', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', fontSize: 14 }, + breadcrumb: { marginBottom: 16, color: '#666' }, + breadcrumbSep: { margin: '0 8px', color: '#999' }, + anchorWrap: { position: 'fixed', top: 80, right: 24, zIndex: 100, backgroundColor: '#fff', borderRadius: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.12)', padding: '12px 16px', minWidth: 160 }, + anchorItem: { display: 'block', padding: '6px 0', color: '#1890ff', cursor: 'pointer', border: 'none', background: 'none', width: '100%', textAlign: 'left', fontSize: 13 }, + card: { backgroundColor: '#fff', borderRadius: 8, marginBottom: 16, boxShadow: '0 1px 2px rgba(0,0,0,0.05)', overflow: 'hidden' }, + cardHeader: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '16px 20px', borderBottom: '1px solid #f0f0f0', cursor: 'pointer' }, + cardTitle: { fontSize: 16, fontWeight: 600, color: '#333' }, + cardToggle: { color: '#999', fontSize: 14 }, + cardBody: { padding: '20px 24px' }, + formRow: { display: 'flex', flexWrap: 'wrap', marginBottom: 16 }, + formCol: { flex: '0 0 33.33%', minWidth: 200, paddingRight: 16, marginBottom: 8 }, + formColFull: { flex: '0 0 100%', marginBottom: 8 }, + label: { display: 'block', marginBottom: 6, color: '#333' }, + input: { width: '100%', padding: '8px 12px', border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 14 }, + inputDisabled: { backgroundColor: '#f5f5f5', color: '#666', cursor: 'default', borderColor: '#e8e8e8' }, + summaryList: { marginBottom: 16, display: 'flex', flexWrap: 'wrap', gap: 16 }, + summaryListItem: { flex: '0 0 calc(50% - 8px)', display: 'flex', alignItems: 'center', padding: '12px 16px', border: '1px solid #e8e8e8', borderRadius: 4, backgroundColor: '#fafafa', fontSize: 14, boxSizing: 'border-box' }, + summaryListLabel: { flex: '0 0 140px', color: '#666' }, + summaryListValue: { flex: 1, fontWeight: 600, color: '#333' }, + rentalTable: { width: '100%', borderCollapse: 'collapse', fontSize: 13 }, + rentalTh: { padding: '10px 8px', textAlign: 'left', borderBottom: '1px solid #e8e8e8', backgroundColor: '#fafafa', fontWeight: 600 }, + rentalTd: { padding: '8px', borderBottom: '1px solid #f0f0f0', verticalAlign: 'middle' }, + rentalInputDisabled: { backgroundColor: '#f5f5f5', color: '#666', border: '1px solid #e8e8e8', padding: '6px 10px', borderRadius: 4, width: '100%', fontSize: 13 }, + historyLink: { color: '#1890ff', cursor: 'pointer', background: 'none', border: 'none', padding: 0, fontSize: 14 }, + footer: { position: 'fixed', bottom: 0, left: 0, right: 0, padding: '12px 24px', backgroundColor: '#fff', borderTop: '1px solid #e8e8e8', display: 'flex', gap: 12, justifyContent: 'flex-start', zIndex: 99 }, + btn: { padding: '8px 24px', borderRadius: 4, border: '1px solid #d9d9d9', cursor: 'pointer', fontSize: 14 }, + btnDefault: { backgroundColor: '#fff', color: '#333' }, + btnPrimary: { backgroundColor: '#1890ff', color: '#fff', borderColor: '#1890ff' }, + feeSectionTitle: { fontSize: 15, fontWeight: 600, color: '#333', marginTop: 20, marginBottom: 10 }, + feeSectionTitleFirst: { marginTop: 0 }, + sectionTitle: { fontSize: 15, fontWeight: 600, color: '#333', marginBottom: 12 }, + modalMask: { position: 'fixed', left: 0, top: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.45)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center' }, + modalBox: { backgroundColor: '#fff', borderRadius: 8, width: '90%', maxWidth: 720, maxHeight: '85vh', overflow: 'hidden', display: 'flex', flexDirection: 'column', boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }, + modalHeader: { padding: '16px 20px', borderBottom: '1px solid #f0f0f0', fontSize: 16, fontWeight: 600 }, + modalBody: { padding: 20, overflow: 'auto', flex: 1 }, + rentalTdCenter: { padding: '8px', borderBottom: '1px solid #f0f0f0', verticalAlign: 'middle' } + }; + + var CardBlock = function(props) { + return React.createElement('div', { id: props.id, style: styles.card }, + React.createElement('div', { style: styles.cardHeader, onClick: function() { props.setCollapsed(!props.collapsed); } }, + React.createElement('span', { style: styles.cardTitle }, props.title), + React.createElement('span', { style: styles.cardToggle }, props.collapsed ? '展开' : '收起') + ), + !props.collapsed ? React.createElement('div', { style: styles.cardBody }, props.children) : null + ); + }; + + var FormItemReadOnly = function(props) { + var colStyle = props.fullWidth ? styles.formColFull : styles.formCol; + return React.createElement('div', { style: colStyle }, + React.createElement('label', { style: styles.label }, props.label), + React.createElement('div', { style: Object.assign({}, styles.input, styles.inputDisabled) }, props.value || '—') + ); + }; + + var mockCustomer = { name: '嘉兴某某物流有限公司', creditCode: '91330400MA2XXXXX1', address: '浙江省嘉兴市南湖区科技大道1号', contact: '张三', phone: '13800138001', email: 'zhangsan@example.com', companyName: '嘉兴某某物流有限公司', companyPhone: '0571-88888888', mailingAddress: '浙江省嘉兴市南湖区科技大道1号', bank: '中国工商银行嘉兴分行', bankAccount: '6222021234567890123', taxId: '91330400MA2XXXXX1' }; + var isThreePartyContract = true; + var mockThirdPartyCustomer = { name: '杭州某某供应链有限公司', creditCode: '91330100MA2YYYYY2', address: '浙江省杭州市余杭区未来科技城', contact: '李四', phone: '13800138002', email: 'lisi@example.com', companyName: '杭州某某供应链有限公司', companyPhone: '0571-99999999', mailingAddress: '浙江省杭州市余杭区未来科技城', bank: '中国农业银行杭州分行', bankAccount: '6228481234567890123', taxId: '91330100MA2YYYYY2', department: '业务2部', responsible: '李专员' }; + var contractOriginalFiles = [ + { name: '租赁合同-原件.pdf', size: '1.2 MB', uploadTime: '2026-02-16 14:30' }, + { name: '租赁合同-补充协议.pdf', size: '0.6 MB', uploadTime: '2026-02-16 14:35' } + ]; + if (isThreePartyContract) { + contractOriginalFiles = contractOriginalFiles.concat([{ name: '三方合同-原件.pdf', size: '0.8 MB', uploadTime: '2026-03-02 16:10' }]); + } + var _contractBasicOriginals = useState(contractOriginalFiles.slice()); + var contractBasicOriginals = _contractBasicOriginals[0]; + var setContractBasicOriginals = _contractBasicOriginals[1]; + var contractBasicInputRef = React.useRef(null); + var addVehicleDate = '2026-03-15'; + var mockContract = { projectName: '嘉兴氢能运输项目', contractCode: 'JXZL20260216YW101235A', contractType: '正式合同', effectiveDate: '2026-02-16', paymentMethod: '预付', mainVehicleModels: '型号A1、型号A2', endDate: '2027-02-16', paymentPeriod: '1个月', signingCompany: '嘉兴羚牛', deliveryRegion: '浙江省 / 嘉兴市', deliveryLocation: '嘉兴市南湖区科技大道1号', contractApprovalType: '标准合同审批', remarks: '' }; + var mockAuthorized = [{ name: '张三', phone: '13800138001', idCard: '330102199001011234' }]; + var mockRentalOrders = [{ brand: '品牌A', model: '型号A1', plateNo: '浙A10001', vin: 'L1234567890ABCDEF', monthRent: '8000', serviceFee: '500', deposit: '10000', remark: '' }, { brand: '品牌A', model: '型号A2', plateNo: '浙B20002', vin: 'L2234567890ABCDEF', monthRent: '8000', serviceFee: '500', deposit: '10000', remark: '' }]; + var mockRentalSummary = { vehicleCount: 2, totalRentService: '17000.00', totalDeposit: '20000.00', hydrogenPrepay: '5000.00' }; + var mockHydrogen = { bearer: '客户', paymentMethod: '预付', hydrogenPrepay: '5000', returnPrice: '80' }; + var mockFeeTemplate = '标准费用模板A'; + var mockBillingMethod = '按自然月结算'; + var feeTemplateCertFees = [{ project: '补办行驶证', standard: '50元/次', serviceFee: '20' }, { project: '补办驾驶证', standard: '30元/次', serviceFee: '10' }, { project: '补办牌照', standard: '100元/次', serviceFee: '50' }]; + var feeTemplatePenaltyFees = [{ project: '提前退车违约金', standard: '月租金×1', serviceFee: '0' }, { project: '违章处理违约金', standard: '按实际发生', serviceFee: '50' }]; + var feeTemplateConsumables = [{ category: '轮胎', part: '前轮', partName: '轮胎A型', qty: 1, feeDetail: '500.00' }, { category: '易损件', part: '雨刮', partName: '雨刮片', qty: 2, feeDetail: '80.00' }]; + var feeTemplateOtherFees = [{ project: '上门送车费', standard: '100元/次', serviceFee: '50' }, { project: '上门收车费', standard: '100元/次', serviceFee: '50' }, { project: '清洗费', standard: '80元/次', serviceFee: '30' }]; + var feeTableHeader3 = React.createElement('tr', null, React.createElement('th', { style: styles.rentalTh }, '项目'), React.createElement('th', { style: styles.rentalTh }, '收费标准'), React.createElement('th', { style: styles.rentalTh }, '服务费')); + var feeTableHeader5 = React.createElement('tr', null, React.createElement('th', { style: styles.rentalTh }, '类别'), React.createElement('th', { style: styles.rentalTh }, '损坏部位'), React.createElement('th', { style: styles.rentalTh }, '配件'), React.createElement('th', { style: styles.rentalTh }, '数量'), React.createElement('th', { style: styles.rentalTh }, '费用明细')); + var makeFeeRow3 = function(r, i) { return React.createElement('tr', { key: i }, React.createElement('td', { style: styles.rentalTd }, r.project), React.createElement('td', { style: styles.rentalTd }, r.standard), React.createElement('td', { style: styles.rentalTd }, r.serviceFee)); }; + var makeFeeRow5 = function(r, i) { return React.createElement('tr', { key: i }, React.createElement('td', { style: styles.rentalTd }, r.category), React.createElement('td', { style: styles.rentalTd }, r.part), React.createElement('td', { style: styles.rentalTd }, r.partName), React.createElement('td', { style: styles.rentalTd }, r.qty), React.createElement('td', { style: styles.rentalTd }, r.feeDetail)); }; + var feeCertTable = React.createElement('table', { style: styles.rentalTable }, React.createElement('thead', null, feeTableHeader3), React.createElement('tbody', null, feeTemplateCertFees.map(makeFeeRow3))); + var feePenaltyTable = React.createElement('table', { style: styles.rentalTable }, React.createElement('thead', null, feeTableHeader3), React.createElement('tbody', null, feeTemplatePenaltyFees.map(makeFeeRow3))); + var feeConsumablesTable = React.createElement('table', { style: styles.rentalTable }, React.createElement('thead', null, feeTableHeader5), React.createElement('tbody', null, feeTemplateConsumables.map(makeFeeRow5))); + var feeOtherTable = React.createElement('table', { style: styles.rentalTable }, React.createElement('thead', null, feeTableHeader3), React.createElement('tbody', null, feeTemplateOtherFees.map(makeFeeRow3))); + var feeTemplateBody = React.createElement('div', null, + React.createElement('div', { style: Object.assign({}, styles.feeSectionTitle, styles.feeSectionTitleFirst) }, '证照补办费用'), feeCertTable, + React.createElement('div', { style: styles.feeSectionTitle }, '违约金费用'), feePenaltyTable, + React.createElement('div', { style: styles.feeSectionTitle }, '易损件信息'), feeConsumablesTable, + React.createElement('div', { style: styles.feeSectionTitle }, '其他费用信息'), feeOtherTable + ); + + var customerFields = React.createElement('div', { style: styles.formRow }, + React.createElement(FormItemReadOnly, { label: '客户名称', value: mockCustomer.name }), + React.createElement(FormItemReadOnly, { label: '客户统一信用代码', value: mockCustomer.creditCode }), + React.createElement(FormItemReadOnly, { label: '客户地址', value: mockCustomer.address }), + React.createElement(FormItemReadOnly, { label: '客户联系人', value: mockCustomer.contact }), + React.createElement(FormItemReadOnly, { label: '客户电话', value: mockCustomer.phone }), + React.createElement(FormItemReadOnly, { label: '客户电子邮箱', value: mockCustomer.email }), + React.createElement(FormItemReadOnly, { label: '企业名称', value: mockCustomer.companyName }), + React.createElement(FormItemReadOnly, { label: '企业电话', value: mockCustomer.companyPhone }), + React.createElement(FormItemReadOnly, { label: '邮寄地址', value: mockCustomer.mailingAddress }), + React.createElement(FormItemReadOnly, { label: '开户银行', value: mockCustomer.bank }), + React.createElement(FormItemReadOnly, { label: '银行账号', value: mockCustomer.bankAccount }), + React.createElement(FormItemReadOnly, { label: '纳税人识别号', value: mockCustomer.taxId }), + React.createElement(FormItemReadOnly, { label: '业务部门', value: '业务1部' }), + React.createElement(FormItemReadOnly, { label: '业务负责人', value: '张经理' }) + ); + + var thirdPartyCustomerFields = React.createElement('div', { style: styles.formRow }, + React.createElement(FormItemReadOnly, { label: '客户名称', value: mockThirdPartyCustomer.name }), + React.createElement(FormItemReadOnly, { label: '客户统一信用代码', value: mockThirdPartyCustomer.creditCode }), + React.createElement(FormItemReadOnly, { label: '客户地址', value: mockThirdPartyCustomer.address }), + React.createElement(FormItemReadOnly, { label: '客户联系人', value: mockThirdPartyCustomer.contact }), + React.createElement(FormItemReadOnly, { label: '客户电话', value: mockThirdPartyCustomer.phone }), + React.createElement(FormItemReadOnly, { label: '客户电子邮箱', value: mockThirdPartyCustomer.email }), + React.createElement(FormItemReadOnly, { label: '企业名称', value: mockThirdPartyCustomer.companyName }), + React.createElement(FormItemReadOnly, { label: '企业电话', value: mockThirdPartyCustomer.companyPhone }), + React.createElement(FormItemReadOnly, { label: '邮寄地址', value: mockThirdPartyCustomer.mailingAddress }), + React.createElement(FormItemReadOnly, { label: '开户银行', value: mockThirdPartyCustomer.bank }), + React.createElement(FormItemReadOnly, { label: '银行账号', value: mockThirdPartyCustomer.bankAccount }), + React.createElement(FormItemReadOnly, { label: '纳税人识别号', value: mockThirdPartyCustomer.taxId }), + React.createElement(FormItemReadOnly, { label: '业务部门', value: mockThirdPartyCustomer.department }), + React.createElement(FormItemReadOnly, { label: '业务负责人', value: mockThirdPartyCustomer.responsible }) + ); + + var contractFormRow1 = React.createElement('div', { style: styles.formRow }, + React.createElement(FormItemReadOnly, { label: '项目名称', value: mockContract.projectName }), + React.createElement(FormItemReadOnly, { label: '合同编码', value: mockContract.contractCode }), + React.createElement(FormItemReadOnly, { label: '合同类型', value: mockContract.contractType }), + React.createElement(FormItemReadOnly, { label: '生效日期', value: mockContract.effectiveDate }), + React.createElement(FormItemReadOnly, { label: '付款方式', value: mockContract.paymentMethod }), + React.createElement(FormItemReadOnly, { label: '主要车型', value: mockContract.mainVehicleModels }), + React.createElement(FormItemReadOnly, { label: '结束日期', value: mockContract.endDate }), + React.createElement(FormItemReadOnly, { label: '付款周期', value: mockContract.paymentPeriod }), + React.createElement(FormItemReadOnly, { label: '签约公司', value: mockContract.signingCompany }), + React.createElement('div', { style: styles.formCol }, + React.createElement(FormItemReadOnly, { label: '交车区域', value: mockContract.deliveryRegion }), + React.createElement('div', null, + React.createElement('label', { style: styles.label }, '合同原件'), + contractBasicOriginals && contractBasicOriginals.length > 0 + ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 12 } }, + contractBasicOriginals.map(function(f, i) { + return React.createElement('div', { key: i, style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', padding: '8px 12px', backgroundColor: '#fafafa', borderRadius: 4, border: '1px solid #e8e8e8' } }, + React.createElement('a', { href: '#', style: { color: '#1890ff', cursor: 'pointer', textDecoration: 'none' }, onClick: function(e) { e.preventDefault(); if (f.file && typeof URL !== 'undefined') { var u = URL.createObjectURL(f.file); window.open(u); } else { window.open('#', '_blank'); } } }, f.name), + (f.size || f.uploadTime) ? React.createElement('span', { style: { color: '#999', fontSize: 12 } }, (f.size || '') + (f.size && f.uploadTime ? ' · ' : '') + (f.uploadTime || '')) : null, + f.file ? React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function() { setContractBasicOriginals(function(prev) { var n = prev.slice(); n.splice(i, 1); return n; }); } }, '删除') : null + ); + }) + ) + : null, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } }, + React.createElement('input', { ref: contractBasicInputRef, type: 'file', style: { display: 'none' }, accept: '.doc,.docx,.pdf', onChange: function(e) { + var f = e.target.files && e.target.files[0]; + if (f) { + var sizeDisplay = f.size >= 1024 * 1024 ? (f.size / 1024 / 1024).toFixed(1) + ' MB' : (f.size / 1024).toFixed(1) + ' KB'; + var now = window.moment ? window.moment() : new Date(); + var uploadTimeStr = window.moment ? now.format('YYYY-MM-DD HH:mm') : now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0') + '-' + String(now.getDate()).padStart(2, '0') + ' ' + String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0'); + setContractBasicOriginals(function(prev) { return prev.concat([{ name: f.name, file: f, size: sizeDisplay, uploadTime: uploadTimeStr }]); }); + } + e.target.value = ''; + } }), + React.createElement(Button, { type: 'default', onClick: function() { if (contractBasicInputRef.current) contractBasicInputRef.current.click(); } }, '上传附件') + ) + ) + ), + React.createElement(FormItemReadOnly, { label: '交车地点', value: mockContract.deliveryLocation }), + React.createElement(FormItemReadOnly, { label: '合同审批类型', value: mockContract.contractApprovalType || '—' }) + ); + var contractFormRow2 = React.createElement('div', { style: styles.formRow }, React.createElement(FormItemReadOnly, { label: '备注', fullWidth: true, value: mockContract.remarks || '—' })); + + var authorizedContent = React.createElement('div', null, + React.createElement('div', { style: { display: 'flex', gap: 12, alignItems: 'center', marginBottom: 8 } }, + React.createElement('span', { style: { flex: 1, fontWeight: 600, fontSize: 13 } }, '被授权人姓名'), + React.createElement('span', { style: { flex: 1, fontWeight: 600, fontSize: 13 } }, '被授权人联系电话'), + React.createElement('span', { style: { flex: 1, fontWeight: 600, fontSize: 13 } }, '被授权人身份证') + ), + mockAuthorized.map(function(item, index) { + return React.createElement('div', { key: index, style: { display: 'flex', gap: 12, alignItems: 'center', marginBottom: 12 } }, + React.createElement('div', { style: Object.assign({}, styles.input, styles.inputDisabled, { flex: 1 }) }, item.name), + React.createElement('div', { style: Object.assign({}, styles.input, styles.inputDisabled, { flex: 1 }) }, item.phone), + React.createElement('div', { style: Object.assign({}, styles.input, styles.inputDisabled, { flex: 1 }) }, item.idCard) + ); + }) + ); + + var rentalSummaryEl = React.createElement('div', { style: styles.summaryList }, + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '租赁车辆数'), React.createElement('span', { style: styles.summaryListValue }, mockRentalSummary.vehicleCount + ' 辆')), + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '租金及服务费合计'), React.createElement('span', { style: styles.summaryListValue }, mockRentalSummary.totalRentService + ' 元')), + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '保证金总额'), React.createElement('span', { style: styles.summaryListValue }, mockRentalSummary.totalDeposit + ' 元')), + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '氢气预付款金额'), React.createElement('span', { style: styles.summaryListValue }, mockRentalSummary.hydrogenPrepay + ' 元')) + ); + var rentalTableBody = mockRentalOrders.map(function(row, idx) { + return React.createElement('tr', { key: idx }, + React.createElement('td', { style: styles.rentalTd }, idx + 1), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.brand)), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.model)), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.plateNo)), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.vin)), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.monthRent + ' 元')), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.serviceFee + ' 元')), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.deposit + ' 元')), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: styles.rentalInputDisabled }, row.remark || '—')) + ); + }); + var rentalTableEl = React.createElement('table', { style: styles.rentalTable }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: styles.rentalTh, width: 50 }, '序号'), + React.createElement('th', { style: styles.rentalTh, width: 100 }, '品牌'), + React.createElement('th', { style: styles.rentalTh, width: 100 }, '型号'), + React.createElement('th', { style: styles.rentalTh, width: 120 }, '车牌号'), + React.createElement('th', { style: styles.rentalTh, width: 160 }, '车辆识别代码'), + React.createElement('th', { style: styles.rentalTh, width: 120 }, '车辆月租金'), + React.createElement('th', { style: styles.rentalTh, width: 90 }, '服务费'), + React.createElement('th', { style: styles.rentalTh, width: 100 }, '保证金'), + React.createElement('th', { style: styles.rentalTh, width: 80 }, '备注') + ) + ), + React.createElement('tbody', null, rentalTableBody) + ); + var hydrogenReadOnly = React.createElement('div', { style: styles.formRow }, + React.createElement(FormItemReadOnly, { label: '氢费承担方', value: mockHydrogen.bearer }), + React.createElement(FormItemReadOnly, { label: '付款方式', value: mockHydrogen.paymentMethod }), + React.createElement(FormItemReadOnly, { label: '氢气预付款', value: mockHydrogen.hydrogenPrepay + ' 元' }), + React.createElement(FormItemReadOnly, { label: '退还车氢气单价', value: mockHydrogen.returnPrice + ' 元' }) + ); + var rentalContent = React.createElement('div', null, rentalSummaryEl, React.createElement('div', { style: { overflowX: 'auto', marginBottom: 16 } }, rentalTableEl), hydrogenReadOnly); + + var newVehicleColumns = [ + { title: '序号', key: 'no', width: 60, render: function(_, __, i) { return i + 1; } }, + { title: '品牌', key: 'brand', width: 90, render: function(_, row, i) { return React.createElement(Select, { placeholder: '请选择', style: { width: '100%' }, value: row.brand || undefined, onChange: function(v) { updateNewVehicleRow(i, 'brand', v || ''); }, options: brandList.map(function(b) { return { value: b, label: b }; }), allowClear: true }); } }, + { title: '型号', key: 'model', width: 100, render: function(_, row, i) { var opts = (row.brand && modelByBrand[row.brand]) ? modelByBrand[row.brand].map(function(m) { return { value: m, label: m }; }) : []; return React.createElement(Select, { placeholder: '请选择', style: { width: '100%' }, value: row.model || undefined, onChange: function(v) { updateNewVehicleRow(i, 'model', v || ''); }, options: opts, allowClear: true, disabled: !row.brand }); } }, + { title: '车牌号', key: 'plateNo', width: 120, render: function(_, row, i) { return React.createElement(Select, { placeholder: '请选择或输入搜索', style: { width: '100%' }, value: row.plateNo || undefined, onChange: function(v) { updateNewVehicleRow(i, 'plateNo', v || ''); }, options: plateNoOptions, allowClear: true, showSearch: true, filterOption: function(input, option) { var label = (option && option.label) ? option.label : ''; return label.toLowerCase().indexOf((input || '').toLowerCase()) >= 0; } }); } }, + { title: '车辆识别代码', key: 'vin', width: 160, render: function(_, row, i) { return React.createElement(Input, { placeholder: '由车牌号反写', value: row.plateNo || row.vin || '', disabled: true }); } }, + { title: '车辆月租金', key: 'monthRent', width: 120, render: function(_, row, i) { return React.createElement(Input, { placeholder: '0.00', value: row.monthRent || '', onChange: function(e) { updateNewVehicleRow(i, 'monthRent', e.target.value); }, addonAfter: '元' }); } }, + { title: '服务费项目', key: 'serviceItems', width: 80, render: function(_, row, i) { return React.createElement(Button, { type: 'link', size: 'small', onClick: function() { openServiceModal(i); } }, '管理'); } }, + { title: '服务费', key: 'serviceFee', width: 90, render: function(_, row, i) { return calcRowServiceFee(row) + ' 元'; } }, + { title: '保证金', key: 'deposit', width: 100, render: function(_, row, i) { return React.createElement(Input, { placeholder: '0.00', value: row.deposit || '', onChange: function(e) { updateNewVehicleRow(i, 'deposit', e.target.value); }, addonAfter: '元' }); } }, + { title: '备注', key: 'remark', width: 120, render: function(_, row, i) { return React.createElement(Input, { placeholder: '选填', value: row.remark || '', onChange: function(e) { updateNewVehicleRow(i, 'remark', e.target.value); } }); } }, + { title: '操作', key: 'action', width: 100, render: function(_, row, i) { return React.createElement('span', { style: { display: 'inline-flex', gap: 4, alignItems: 'center' } }, React.createElement(Button, { type: 'link', size: 'small', onClick: function() { copyNewVehicleRow(i); } }, '复制'), React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function() { removeNewVehicleRow(i); } }, '删除')); } } + ]; + + var newOrderSummaryEl = React.createElement('div', { style: styles.summaryList }, + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '租赁车辆数'), React.createElement('span', { style: styles.summaryListValue }, newOrderSummary.vehicleCount + ' 辆')), + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '租金及服务费合计'), React.createElement('span', { style: styles.summaryListValue }, newOrderSummary.totalRentService + ' 元')), + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '保证金总额'), React.createElement('span', { style: styles.summaryListValue }, newOrderSummary.totalDeposit + ' 元')) + ); + + var serviceModalRows = serviceModalRowIndex !== null && newVehicleRows[serviceModalRowIndex] ? newVehicleRows[serviceModalRowIndex].serviceItems : []; + var serviceModalContent = serviceModalRowIndex !== null ? React.createElement('div', { style: styles.modalMask, onClick: function(e) { if (e.target === e.currentTarget) closeServiceModal(); } }, + React.createElement('div', { style: styles.modalBox, onClick: function(e) { e.stopPropagation(); } }, + React.createElement('div', { style: styles.modalHeader }, '服务项目'), + React.createElement('div', { style: styles.modalBody }, + React.createElement('table', { style: styles.rentalTable }, + React.createElement('thead', null, React.createElement('tr', null, React.createElement('th', { style: styles.rentalTh }, '服务项目'), React.createElement('th', { style: styles.rentalTh }, '费用'), React.createElement('th', { style: styles.rentalTh }, '生效时间'), React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 80 }) }, '操作'))), + React.createElement('tbody', null, serviceModalRows.map(function(si, siIdx) { + return React.createElement('tr', { key: siIdx }, + React.createElement('td', { style: styles.rentalTd }, React.createElement(Select, { style: { width: '100%' }, placeholder: '请选择服务项目', value: si.project || undefined, onChange: function(v) { updateServiceItem(siIdx, 'project', v || ''); }, showSearch: true, filterOption: function(input, opt) { return opt && opt.children && String(opt.children).toLowerCase().indexOf((input || '').toLowerCase()) >= 0; }, allowClear: true }, serviceItemOptions.map(function(opt, oi) { return React.createElement(Option, { key: oi, value: opt }, opt); }))), + React.createElement('td', { style: styles.rentalTd }, React.createElement(Input, { placeholder: '0.00', value: si.fee || '', onChange: function(e) { updateServiceItem(siIdx, 'fee', e.target.value); }, addonAfter: '元', style: { width: '100%' } })), + React.createElement('td', { style: styles.rentalTd }, React.createElement(DatePicker, { style: { width: '100%' }, format: 'YYYY-MM-DD', placeholder: '请选择生效时间', value: si.effectiveDate && window.moment ? window.moment(si.effectiveDate, 'YYYY-MM-DD') : null, onChange: function(d, dateStr) { updateServiceItem(siIdx, 'effectiveDate', dateStr || ''); } })), + React.createElement('td', { style: styles.rentalTd }, React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function() { removeServiceItem(siIdx); } }, '删除')) + ); + })) + ), + React.createElement(Button, { type: 'dashed', style: { marginTop: 12, width: '100%' }, onClick: addServiceItem }, '添加一行') + ), + React.createElement('div', { style: { padding: '12px 20px', borderTop: '1px solid #f0f0f0', textAlign: 'right', display: 'flex', gap: 12, justifyContent: 'flex-end' } }, + React.createElement(Button, { type: 'primary', onClick: function() { closeServiceModal(); } }, '保存'), + React.createElement(Button, { onClick: closeServiceModal }, '关闭') + ) + ) + ) : null; + + var openNewVehicleContract = function(item) { + if (item && item.file && typeof URL !== 'undefined') { var url = URL.createObjectURL(item.file); window.open(url); } + }; + var newVehicleContractSection = React.createElement('div', { style: styles.formRow }, + React.createElement('div', { style: styles.formColFull }, + React.createElement('label', { style: styles.label }, '合同原件'), + newVehicleContractOriginals && newVehicleContractOriginals.length > 0 + ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 12 } }, + newVehicleContractOriginals.map(function(f, i) { + return React.createElement('div', { key: i, style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', padding: '8px 12px', backgroundColor: '#f5f5f5', borderRadius: 4, border: '1px solid #d9d9d9' } }, + React.createElement('a', { href: '#', style: { color: '#1890ff', cursor: 'pointer', textDecoration: 'none' }, onClick: function(e) { e.preventDefault(); openNewVehicleContract(f); } }, f.name), + (f.size || f.uploadTime) ? React.createElement('span', { style: { color: '#999', fontSize: 12 } }, (f.size || '') + (f.size && f.uploadTime ? ' · ' : '') + (f.uploadTime || '')) : null, + React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function() { setEdited(true); setNewVehicleContractOriginals(function(prev) { var n = prev.slice(); n.splice(i, 1); return n; }); } }, '删除') + ); + }) + ) + : null, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } }, + React.createElement('input', { ref: newVehicleContractInputRef, type: 'file', style: { display: 'none' }, accept: '.doc,.docx,.pdf', onChange: function(e) { + var f = e.target.files && e.target.files[0]; + if (f) { + setEdited(true); + var sizeDisplay = f.size >= 1024 * 1024 ? (f.size / 1024 / 1024).toFixed(1) + ' MB' : (f.size / 1024).toFixed(1) + ' KB'; + var now = window.moment ? window.moment() : new Date(); + var uploadTimeStr = window.moment ? now.format('YYYY-MM-DD HH:mm') : now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0') + '-' + String(now.getDate()).padStart(2, '0') + ' ' + String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0'); + setNewVehicleContractOriginals(function(prev) { return prev.concat([{ name: f.name, file: f, size: sizeDisplay, uploadTime: uploadTimeStr }]); }); + } + e.target.value = ''; + } }), + React.createElement(Button, { type: 'default', onClick: function() { if (newVehicleContractInputRef.current) newVehicleContractInputRef.current.click(); } }, '上传附件') + ) + ) + ); + var newVehicleCardContent = React.createElement('div', null, + newVehicleContractSection, + newOrderSummaryEl, + React.createElement(Table, { rowKey: function(_, i) { return String(i); }, size: 'small', columns: newVehicleColumns, dataSource: newVehicleRows, pagination: false, scroll: { x: 1100 } }), + React.createElement(Button, { type: 'dashed', style: { marginTop: 12, width: '100%' }, onClick: addNewVehicleRow }, '添加一行') + ); + + var feeContent = React.createElement('div', null, React.createElement('div', { style: styles.formRow }, React.createElement(FormItemReadOnly, { label: '选择费用模板', value: mockFeeTemplate })), feeTemplateBody); + var billingContent = React.createElement('div', null, React.createElement('div', { style: { padding: '12px 16px', border: '1px solid #e8e8e8', borderRadius: 4, backgroundColor: '#fafafa', fontSize: 14, color: '#333' } }, mockBillingMethod)); + var changeHistoryRaw = [ + { changeTime: '2026-03-03 10:20', opType: '保存', operator: '张三', remark: '无' }, + { changeTime: '2026-03-02 16:10', opType: '变更为三方合同', operator: '李四', remark: '增加丙方客户“客户名称杭州某某供应链有限公司”' }, + { changeTime: '2026-03-01 11:40', opType: '添加车辆', operator: '王五', remark: '添加车辆“沪A30003”\n添加车辆“浙A10001”\n添加车辆“浙B20002”' }, + { changeTime: '2026-02-28 09:05', opType: '添加授权人', operator: '赵六', remark: '添加授权人“授权人张三”\n添加授权人“授权人李四”' }, + { changeTime: '2026-02-26 15:30', opType: '续签合同', operator: '周九', remark: '续签自“原合同编码JXZL20250210YW101100A”' }, + { changeTime: '2026-02-23 14:18', opType: '撤回合同', operator: '钱七', remark: '主动撤回' }, + { changeTime: '2026-02-20 10:00', opType: '终止合同', operator: '孙八', remark: '主动终止' }, + { changeTime: '2026-02-16 14:30', opType: '转正式合同', operator: '张经理', remark: '转正式合同自“原合同编码JXZL20260210YW101230B”' } + ]; + var changeHistorySorted = changeHistoryRaw.slice().sort(function(a, b) { return b.changeTime.localeCompare(a.changeTime); }); + var historyTableRows = changeHistorySorted.map(function(row, index) { + return React.createElement('tr', { key: index }, + React.createElement('td', { style: styles.rentalTd }, index + 1), + React.createElement('td', { style: styles.rentalTd }, row.changeTime), + React.createElement('td', { style: styles.rentalTd }, row.opType), + React.createElement('td', { style: styles.rentalTd }, row.operator), + React.createElement('td', { style: styles.rentalTd }, React.createElement('button', { type: 'button', style: styles.historyLink, onClick: function() { window.open('#', '_blank'); } }, '查看变更前记录')), + React.createElement('td', { style: Object.assign({}, styles.rentalTd, { verticalAlign: 'top', whiteSpace: 'pre-line' }) }, row.remark) + ); + }); + var historyTable = React.createElement('table', { style: styles.rentalTable }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: styles.rentalTh, width: 60 }, '序号'), + React.createElement('th', { style: styles.rentalTh, width: 140 }, '变更时间'), + React.createElement('th', { style: styles.rentalTh, width: 120 }, '操作类型'), + React.createElement('th', { style: styles.rentalTh, width: 90 }, '操作人'), + React.createElement('th', { style: styles.rentalTh, width: 120 }, '原始记录'), + React.createElement('th', { style: styles.rentalTh }, '备注') + ) + ), + React.createElement('tbody', null, historyTableRows) + ); + + var requirementContent = '车辆租赁合同-新增(2026年3月3日版本)\n「数字化资产ONE-OS运管平台」中的「车辆租赁合同」-「新增租赁合同」模块,点击车辆租赁合同右上角「新增」进行创建;\n1.面包屑:\n#业务管理-车辆租赁合同-新增合同\n\n2.客户基本信息卡片:\n#用于从客户列表中选择客户,并将该合同绑定到业务部门及业务负责人(绑定业务部门/业务负责人主要为了后期从部门/业务负责人维度进行数据统计);\n2.1.客户名称:必选项,选择器,支持从输入框内输入内容进行模糊搜索,从客户信息列表中选择对应客户(只显示已通过审核的客户);\n2.2.客户统一信用代码:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「信用代码」字段;\n2.3.客户地址:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「客户地址」字段;\n2.4.客户联系人:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「客户联系人」字段;\n2.5.客户电话:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「客户电话」字段;\n2.6.客户电子邮箱:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「客户电子邮箱」字段;\n2.7.企业名称:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「企业名称」字段;\n2.8.企业电话:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「企业电话」字段;\n2.9.邮寄地址:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「邮寄地址」字段;\n2.10.开户银行:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「开户银行」字段;\n2.11.银行账号:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「银行账号」字段;\n2.12.纳税人识别号:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「纳税人识别号」字段;\n2.13.业务部门:必选项,选择器,从部门表中选择该租赁合同对应部门;\n2.14.业务负责人:必选项,选择器,从已选业务部门下拉取对应业务负责人,未选择业务部门时,业务负责人字段不可选;\n\n3.合同基本信息卡片:\n#用于定义租赁合同基本情况和付款方式;\n3.1.项目名称:必填项,输入框,用于定义该合同项目名称,默认提示信息"请输入项目名称";\n3.2.合同类型:必选项,选择器,合同类型分为「正式合同」「试用合同」;\n3.3.生效日期:必选项,日期选择器,格式为YYYY-MM-DD,精确至天,默认为点击新增日期;\n3.4.付款方式:必选项,付款方式分为「预付」「后付」两种;\n 3.4.1.如果选择预付,以对应「付款方式」和「付款周期」规则,在每一期新账单生成就列入业务待办(工作台功能),同时以消息通知对应用户;\n 3.4.2.如果选择后付,以对应「付款方式」和「付款周期」规则,在每一期新账单生成时,将前一期账单列入业务待办(工作台功能),同时以消息通知对应用户;\n3.5.主要车型:输入框(禁用状态),根据租赁订单信息中所有所选车型,自动反写入输入框并以标签形式显示,支持多车型显示,标签显示:型号名称;\n3.6.结束日期:必选项,日期选择器,格式为YYYY-MM-DD,精确至天;\n 3.6.1.合同结束日期前30天将以消息提醒方式提醒(消息中心、工作台);\n 3.6.2.到达合同结束日期时,租赁账单将会立刻停止计算,作为最后一期账单;\n 3.6.3.续签合同/转正式合同将重新生成账单,不会对旧合同账单做任何继承处理;\n3.7.付款周期:必选项,选择器,支持1个月-12个月 12种付款周期,账单将以此周期和账单计算方式规则,从交车任务形成的交车单进行完整交车后,定时自动生成账单;\n3.8.签约公司:必选项,选择器,从组织机构表中获取所有根组织机构(如嘉兴羚牛、上海羚牛、广东羚牛等),默认显示新增用户当前机构,可手动修改,后期需要考虑从签约公司维度统计合同相关数据;\n3.9.交车区域:必选项,地区选择器,支持省-市2级,选择区域后,该任务生成交车任务时会自动推送至该区域负责运维人员;\n3.10.交车地点:必填项,输入框,支持自定义输入交车地点;\n3.11.合同审批类型:必填项,选择器,分为「标准合同审批」「非标准合同审批」;\n3.12.合同原件:必填项,按钮,按钮文字为:上传附件,支持多个附件上传(doc/docx/pdf格式);\n3.13.备注:文本域,支持自定义输入备注信息;\n\n4.被授权人信息卡片:\n#用于定义租赁合同相关被授权人相关信息,被授权人在交车单完成时,需要选择被授权人,并通过被授权人手机短信,在E签宝进行签字确认;\n4.1.被授权人:必填项,输入框,用于输入被授权人信息;\n4.2.被授权人联系电话:必填项,输入框,用于输入被授权人联系电话,该电话后续需要接收E签宝签字链接;\n4.3.被授权人身份证:必填项,输入框,用于输入被授权人身份证信息;\n4.4.支持通过新增/删除一行的方式,创建或管理多个授权人,后续交车单完成时可从多个授权人中选择接收授权人;\n\n5.租赁订单信息卡片:\n#用于定义租赁合同对应车辆明细费用、氢费明细费用等相关信息;\n5.1.上方为租赁车辆总计数据,包括租赁车辆数、租金及服务费合计、保证金总额、氢气预付款金额等相关信息;\n 5.1.1.租赁车辆数:显示下方租赁订单信息包含多少辆车;\n 5.1.2.租金及服务费合计:显示下方租赁订单信息中车辆租金总额及服务费总额;\n 5.1.3.保证金总额:显示下方租赁订单信息中车辆保证金总额;\n 5.1.4.氢气预付款金额:显示氢气预付款金额,如客户选择自行承担氢费或羚牛承担氢费,则该处为0不计入氢气预付款金额;\n5.2.下方为列表,显示序号、品牌、型号、车牌号、车辆识别代码、车辆月租金(元)、服务费项目、服务费、保证金、备注,默认且至少显示一行空数据;\n 5.2.1.序号:自动按照条数生成,规则为1、2、3....以此类推;\n 5.2.2.品牌:必选项,选择器,从型号参数库中「品牌」字段拉取所有品牌;\n 5.2.3.型号:必选项,选择器,与品牌存在级联关系,未选择品牌则无法选择型号;\n 5.2.4.车牌号:选填项,选择器(支持从输入框输入车牌号关键字下拉匹配),可通过选择车牌号对品牌、型号进行反写;\n 5.2.5.车辆识别代码:输入框(禁用),显示该车辆对应车辆识别代码,根据所选车牌号从车辆表直接拉取进行反写;\n 5.2.6.车辆月租金:输入框,支持两位小数,用于输入该车辆月租金金额,输入框后缀为元,如还车时账单周期不满1个月,则按照:(车辆月租金/30)* 实际天数进行计算;\n 5.2.7.服务费项目:点击管理按钮弹出卡片,卡片标题为:服务项目,下方列表显示服务项目、费用、生效时间、操作;\n 5.2.7.1.服务项目:必选项,选择器(支持输入框输入服务项目关键字进行下拉匹配),选项包含:代处理费用、罚款、违章处理违约金、未参加安全培训、车辆出险、年检年审违约、停车费、设备损坏金(包含易损件)、清洗费、上门收车人工费、上门收车送车行驶费、上门收车基础服务费、保险上浮、保养费用、补办驾驶证、补办牌照、补办营运证、补办加氢证、借用备用钥匙、补配钥匙、租金、氢气费-客、退还车氢量差、能源费补缴、能源费退款、送车上门人工费、送车上门送车行驶费、送车上门基础服务费、保证金、氢气预付费、维修费用、ETC-客、ETC卡缺损费、ETC设备缺损费、电费-客、未结算保养费、未结算维修费、车损费、工具损坏或丢失费、证件费、广告损坏费、送车服务费、接车服务费、补办行驶证、超赔险、轮胎磨损费、无忧包、轮胎保、养护保、尾板;\n 5.2.7.2.费用:必填项,输入框,支持2位小数,输入框后缀为元;\n 5.2.7.3.生效时间:必选项,日期选择器,格式为YYYY-MM-DD;\n 5.2.7.4.操作:删除,点击删除直接删除该行数据;\n 5.2.7.5.新增一行数据:点击添加一行服务项目;\n #在提车应收款中,车辆的交车日期和服务费生效日期可能会存在不同的情况,所以服务费需要单独以生效时间进行计算,例如:\n 交车后车辆从1月1日开始计费,付款周期为2个月,则提车应收款租金会计算到3月1日,但是服务费生效时间为1月30日,此情况下需要以3月1日-1月30日,计算出服务费具体收费天数为30天,然后根据:(服务费费用/30)* 服务费收费天数30)进行计算;\n 5.2.8.服务费:自动根据添加的所有服务费项目计算总额,支持2位小数,格式为:xx.xx元;\n 5.2.9.保证金:必填项,输入框,支持2位小数,后缀为元,用于填写车辆需要支付的保证金金额,保证金为提车应收款一次性支付,还车时需要计入退还费用中;\n 5.2.10.备注:选填项,输入框,用于备注车辆复杂情况;\n 5.2.11.操作:删除,点击删除删除该行数据;\n 5.2.12.添加一行:点击后列表新增一行,用于填写一条新的车辆租金费用信息;\n5.3.氢费承担方:必选项,选择器,选项为:「我方」、「客户」,默认选择「客户」;\n 5.3.1.选择「我方」:不显示付款方式、氢气预付款;\n 5.3.2.选择「客户」:付款方式字段默认为预付,可手动修改;\n5.4.付款方式:必选项,选择器,选项为:「预付」、「月付款」、「自行结算」,默认为:「预付」;\n 5.4.1.预付:选择「预付」,需要填写:「氢气预付款」,氢气预付款指合同签署时客户就需预先付出的氢费款项,该部分款项会自动计入提车应收款中氢气预付款金额;\n 5.4.2.月付款:选择「月付款」,提车应收款中不进行收费,而是由业务人员按照实际情况,通过氢费账单功能生成对应氢费账单,单独与客户进行结算;\n 5.4.3.自行结算:选择「自行结算」,指合同签署后,所有氢气费用由客户自行承担;\n5.5.氢气预付款:选择「客户」「预付」时显示,必填项,输入框,支持2位小数,氢气预付款金额会计算入该合同交车应收款中,并计入5.1.4.氢气预付款金额中;\n5.6.退还车氢气单价:必填项,输入框,支持2位小数,后缀为元,不管氢费承担方和付款方式选择任何选项都需要进行维护,该金额主要用于与客户约定还车时,与交车时氢气差值以此费用进行自动计算和结算;\n\n6.其他费用信息卡片:\n#用于选择对应租赁费用模板,选择后展示证照补办费用、违约金费用、易损件费用、其他费用等信息,租赁费用模板管理功能位于「车辆租赁合同」列表左上角;\n6.1.选择费用模板:必选项,从「租赁费用模板」中拉取,选择后自动将该费用模板所有环节费用显示在合同中;\n 6.1.1.证照补办费用:单独标题显示,内容为列表,列表中包括项目、收费标准、服务费,选择费用模板后自动反显;\n 6.1.2.违约金费用:单独标题显示,内容为列表,列表中包含项目、收费标准、服务费,选择费用模板后自动反显;\n 6.1.3.易损件信息:单独标题显示,内容为列表,列表中包含类别、损坏部位、配件、数量、费用明细,选择费用模板后自动反显;\n 6.1.4.其他费用信息:单独标题显示,内容为列表,列表中包含项目、收费标准、服务费,选择费用模板后自动反显;\n\n7.账单计算方式卡片:\n#必选项,填充按钮组,默认为按自然月结算,可手动修改,用于定义租赁合同的账单计算方式,分为「按自然月结算」、「按付款周期天数结算」两种方式;\n7.1.按付款周期天数结算:账单按照合同基本信息卡片中每隔付款周期*30天形成一期账单;\n 例如付款周期为2个月,则交车任务交车成功时,提车应收款从交车任务配置的开始计费时间开始,根据60天收取车辆租金,此后每隔60天生成一期租赁账单;\n7.2.按自然月结算:账单按照第一个月计费开始日期到当月最后一天为第一期,之后按照付款周期所选月份间隔,从开始月份第一天到间隔月份最后一天的自然月方式形成一期账单;\n 例如付款周期为2个月,则交车任务交车成功时,提车应收款车辆租金需要收取≥2个月租金作为标准流程,<2个月租金作为非标流程;\n 租赁账单首期从交车任务配置的开始计费时间开始,如付款周期为2个月,则首期账单结束时间为第二个月最后一天,付款周期为3个月,则首期账单结束时间为第三个月最后一天,此后每一期按照实际自然月开始-结束形成账单,例如付款周期为2个月,则第二期账单从2月1日-3月31日,以此类推;\n\n8.最下方为提交并审核、保存、取消三个按钮;\n8.1.点击提交并审核,toast提示:租赁合同已提交审核。同时该租赁合同进入租赁合同审核列表中;\n8.2.点击保存,会存储租赁订单已填写内容,不做必填项校验,同时显示在租赁合同列表中,该条数据只能保存人自己查看并编辑,其他人无法操作;\n8.3.点击取消,如当前页面有已编辑内容时,点击取消会进行二次提示,内容为:取消将会丢失所有已填写内容,是否确认?点击确认返回车辆租赁合同列表页;\n\n所有卡片支持收起/展开功能,通过点击卡片右侧收起/展开实现;并增加锚点功能,锚点固定于页面右上角,点击锚点对应卡片名称,页面自动跳转至该卡片所在区域;'; + + return React.createElement('div', { style: styles.page }, + React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 } }, + React.createElement('div', { style: styles.breadcrumb }, React.createElement('span', null, '业务管理'), React.createElement('span', { style: styles.breadcrumbSep }, ' / '), React.createElement('span', null, '车辆租赁合同'), React.createElement('span', { style: styles.breadcrumbSep }, ' / '), React.createElement('span', { style: { color: '#1890ff' } }, '新增车辆')), + React.createElement('span', { style: { color: '#1890ff', cursor: 'pointer', fontSize: 14 }, onClick: function() { setReqSpecOpen(true); } }, '查看需求说明') + ), + React.createElement('div', { style: styles.anchorWrap }, + React.createElement('div', { style: { marginBottom: 8, fontWeight: 600, fontSize: 14 } }, '锚点导航'), + React.createElement('button', { type: 'button', style: styles.anchorItem, onClick: function() { scrollToCard('card-customer'); } }, '客户基本信息'), + isThreePartyContract ? React.createElement('button', { type: 'button', style: styles.anchorItem, onClick: function() { scrollToCard('card-third-party'); } }, '丙方客户基本信息') : null, + React.createElement('button', { type: 'button', style: styles.anchorItem, onClick: function() { scrollToCard('card-contract'); } }, '合同基本信息'), + React.createElement('button', { type: 'button', style: styles.anchorItem, onClick: function() { scrollToCard('card-authorized'); } }, '被授权人信息'), + React.createElement('button', { type: 'button', style: styles.anchorItem, onClick: function() { scrollToCard('card-rental'); } }, '租赁订单信息'), + React.createElement('button', { type: 'button', style: styles.anchorItem, onClick: function() { scrollToCard('card-new-vehicle'); } }, '新增车辆信息(' + addVehicleDate + ')'), + React.createElement('button', { type: 'button', style: styles.anchorItem, onClick: function() { scrollToCard('card-fee'); } }, '其他费用信息'), + React.createElement('button', { type: 'button', style: styles.anchorItem, onClick: function() { scrollToCard('card-billing'); } }, '账单计算方式'), + ), + React.createElement('div', { id: 'card-customer' }, React.createElement(CardBlock, { title: '客户基本信息', collapsed: cc1, setCollapsed: setCc1 }, customerFields)), + isThreePartyContract ? React.createElement('div', { id: 'card-third-party', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '丙方客户基本信息', collapsed: cc1b, setCollapsed: setCc1b }, thirdPartyCustomerFields)) : null, + React.createElement('div', { id: 'card-contract', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '合同基本信息', collapsed: cc2, setCollapsed: setCc2 }, React.createElement('div', null, contractFormRow1, contractFormRow2))), + React.createElement('div', { id: 'card-authorized', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '被授权人信息', collapsed: cc3, setCollapsed: setCc3 }, authorizedContent)), + React.createElement('div', { id: 'card-rental', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '租赁订单信息', collapsed: cc4, setCollapsed: setCc4 }, rentalContent)), + React.createElement('div', { id: 'card-new-vehicle', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '新增车辆信息(' + addVehicleDate + ')', collapsed: cc7, setCollapsed: setCc7 }, newVehicleCardContent)), + React.createElement('div', { id: 'card-fee', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '其他费用信息', collapsed: cc5, setCollapsed: setCc5 }, feeContent)), + React.createElement('div', { id: 'card-billing', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '账单计算方式', collapsed: cc6, setCollapsed: setCc6 }, billingContent)), + React.createElement('div', { style: { height: 60 } }), + serviceModalContent, + reqSpecOpen ? React.createElement('div', { style: styles.modalMask, onClick: function(e) { if (e.target === e.currentTarget) setReqSpecOpen(false); } }, + React.createElement('div', { style: Object.assign({}, styles.modalBox, { maxWidth: 720 }), onClick: function(e) { e.stopPropagation(); } }, + React.createElement('div', { style: styles.modalHeader }, '需求说明'), + React.createElement('div', { style: Object.assign({}, styles.modalBody, { maxHeight: '70vh', padding: '20px 24px' }) }, React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6, color: 'rgba(0,0,0,0.85)' } }, requirementContent)), + React.createElement('div', { style: { padding: '12px 20px', borderTop: '1px solid #f0f0f0', textAlign: 'right' } }, React.createElement(Button, { onClick: function() { setReqSpecOpen(false); } }, '关闭')) + ) + ) : null, + cancelConfirmOpen ? React.createElement('div', { style: styles.modalMask, onClick: function(e) { if (e.target === e.currentTarget) setCancelConfirmOpen(false); } }, + React.createElement('div', { style: Object.assign({}, styles.modalBox, { maxWidth: 520 }), onClick: function(e) { e.stopPropagation(); } }, + React.createElement('div', { style: styles.modalHeader }, '提示'), + React.createElement('div', { style: Object.assign({}, styles.modalBody, { padding: '20px 24px' }) }, '取消将会丢失所有已填写内容,是否确认?'), + React.createElement('div', { style: { padding: '12px 20px', borderTop: '1px solid #f0f0f0', textAlign: 'right', display: 'flex', gap: 12, justifyContent: 'flex-end' } }, + React.createElement(Button, { onClick: function() { setCancelConfirmOpen(false); } }, '否'), + React.createElement(Button, { type: 'primary', onClick: function() { setCancelConfirmOpen(false); setEdited(false); message.info('已返回车辆租赁合同列表(原型)'); } }, '是') + ) + ) + ) : null, + React.createElement('div', { style: styles.footer }, + React.createElement(Button, { type: 'primary', onClick: function() { + // 提交并审核:做必填校验(品牌/型号/月租金/保证金/服务项目/费用/生效时间) + var errs = []; + for (var i = 0; i < newVehicleRows.length; i++) { + var r = newVehicleRows[i]; + if (!r.brand) errs.push('第' + (i + 1) + '行品牌'); + if (!r.model) errs.push('第' + (i + 1) + '行型号'); + if (!r.monthRent || String(r.monthRent).trim() === '') errs.push('第' + (i + 1) + '行车辆月租金'); + if (!r.deposit || String(r.deposit).trim() === '') errs.push('第' + (i + 1) + '行保证金'); + var items = r.serviceItems || []; + for (var j = 0; j < items.length; j++) { + if (!items[j].project) errs.push('第' + (i + 1) + '行服务项目'); + if (!items[j].fee || String(items[j].fee).trim() === '') errs.push('第' + (i + 1) + '行服务费用'); + if (!items[j].effectiveDate) errs.push('第' + (i + 1) + '行生效时间'); + } + } + if (errs.length) { message.warning('请完善必填项:' + errs.join('、')); return; } + setEdited(false); + message.success('租赁合同已提交审核。同时该租赁合同重新触发审核流程;'); + } }, '提交并审核'), + React.createElement(Button, { onClick: function() { setEdited(false); message.success('已保存(不校验必填项),仅操作人可查看编辑'); } }, '保存'), + React.createElement(Button, { onClick: function() { if (edited) setCancelConfirmOpen(true); else message.info('已返回车辆租赁合同列表(原型)'); } }, '取消') + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-lease-contract/pages/06-车辆租赁合同-续签合同.jsx b/src/prototypes/oneos-web-lease-contract/pages/06-车辆租赁合同-续签合同.jsx new file mode 100644 index 0000000..77d9198 --- /dev/null +++ b/src/prototypes/oneos-web-lease-contract/pages/06-车辆租赁合同-续签合同.jsx @@ -0,0 +1,806 @@ +// 【重要】必须使用 const Component 作为组件变量名 - Axhub 产品原型 +// 车辆管理系统 - 合同续签模块(布局与新增租赁合同一致,合同编码后增加续签自原合同编号,IE11+ 兼容,采用 antd) + +const Component = function() { + var antd = window.antd; + var Input = antd.Input; + var Select = antd.Select; + var Button = antd.Button; + var DatePicker = antd.DatePicker; + var Modal = antd.Modal; + var message = antd.message; + var Option = Select.Option; + + var customerList = [ + { id: '1', name: '嘉兴某某物流有限公司', creditCode: '91330400MA2XXXXX1', address: '浙江省嘉兴市南湖区科技大道1号', contact: '张三', phone: '13800138001', email: 'zhangsan@example.com', companyName: '嘉兴某某物流有限公司', companyPhone: '0571-88888888', mailingAddress: '浙江省嘉兴市南湖区科技大道1号', bank: '中国工商银行嘉兴分行', bankAccount: '6222021234567890123', taxId: '91330400MA2XXXXX1' }, + { id: '2', name: '上海某某运输公司', creditCode: '91310000MA2XXXXX2', address: '上海市浦东新区张江高科技园区', contact: '李四', phone: '13800138002', email: 'lisi@example.com', companyName: '上海某某运输公司', companyPhone: '021-66666666', mailingAddress: '上海市浦东新区张江高科技园区', bank: '中国建设银行上海分行', bankAccount: '6217001234567890123', taxId: '91310000MA2XXXXX2' }, + { id: '3', name: '杭州某某租赁有限公司', creditCode: '91330100MA2XXXXX3', address: '浙江省杭州市余杭区未来科技城', contact: '王五', phone: '13800138003', email: 'wangwu@example.com', companyName: '杭州某某租赁有限公司', companyPhone: '0571-99999999', mailingAddress: '浙江省杭州市余杭区未来科技城', bank: '中国农业银行杭州分行', bankAccount: '6228481234567890123', taxId: '91330100MA2XXXXX3' } + ]; + var deptList = [{ id: 'YW1', name: '业务1部', owners: ['张经理', '李专员', '王专员'] }, { id: 'YW2', name: '业务2部', owners: ['赵经理', '钱专员'] }, { id: 'YW3', name: '业务3部', owners: ['孙经理', '周专员'] }]; + var orgList = ['嘉兴羚牛', '上海羚牛', '广东羚牛']; + var regionList = [ + { province: '浙江省', cities: ['杭州市', '宁波市', '嘉兴市', '湖州市'] }, + { province: '上海市', cities: ['上海市'] }, + { province: '广东省', cities: ['广州市', '深圳市', '东莞市'] } + ]; + var feeTemplates = ['标准费用模板A', '标准费用模板B', '定制费用模板C']; + var feeTemplateCertFees = [{ project: '补办行驶证', standard: '50元/次', serviceFee: '20' }, { project: '补办驾驶证', standard: '30元/次', serviceFee: '10' }, { project: '补办牌照', standard: '100元/次', serviceFee: '50' }]; + var feeTemplatePenaltyFees = [{ project: '提前退车违约金', standard: '月租金×1', serviceFee: '0' }, { project: '违章处理违约金', standard: '按实际发生', serviceFee: '50' }]; + var feeTemplateConsumables = [{ category: '轮胎', part: '前轮', partName: '轮胎A型', qty: 1, feeDetail: '500.00' }, { category: '易损件', part: '雨刮', partName: '雨刮片', qty: 2, feeDetail: '80.00' }]; + var feeTemplateOtherFees = [{ project: '上门送车费', standard: '100元/次', serviceFee: '50' }, { project: '上门收车费', standard: '100元/次', serviceFee: '50' }, { project: '清洗费', standard: '80元/次', serviceFee: '30' }]; + var brandList = ['品牌A', '品牌B', '品牌C', '品牌D']; + var modelByBrand = { '品牌A': ['型号A1', '型号A2', '型号A3'], '品牌B': ['型号B1', '型号B2'], '品牌C': ['型号C1', '型号C2', '型号C3'], '品牌D': ['型号D1'] }; + var vehicleList = [ + { plateNo: '浙A10001', vin: 'L1234567890ABCDEF', brand: '品牌A', model: '型号A1' }, + { plateNo: '浙B20002', vin: 'L2234567890ABCDEF', brand: '品牌A', model: '型号A2' }, + { plateNo: '沪A30003', vin: 'L3234567890ABCDEF', brand: '品牌B', model: '型号B1' }, + { plateNo: '粤A40004', vin: 'L4234567890ABCDEF', brand: '品牌C', model: '型号C2' } + ]; + var serviceItemOptions = ['代处理费用', '罚款', '违章处理违约金', '未参加安全培训', '车辆出险', '年检年审违约', '停车费', '设备损坏金(包含易损件)', '清洗费', '上门收车人工费', '上门收车送车行驶费', '上门收车基础服务费', '保险上浮', '保养费用', '补办驾驶证', '补办牌照', '补办营运证', '补办加氢证', '借用备用钥匙', '补配钥匙', '租金', '氢气费-客', '退还车氢量差', '能源费补缴', '能源费退款', '送车上门人工费', '送车上门送车行驶费', '送车上门基础服务费', '保证金', '氢气预付费', '维修费用', 'ETC-客', 'ETC卡缺损费', 'ETC设备缺损费', '电费-客', '未结算保养费', '未结算维修费', '车损费', '工具损坏或丢失费', '证件费', '广告损坏费', '送车服务费', '接车服务费', '补办行驶证', '超赔险', '轮胎磨损费', '无忧包', '轮胎保', '养护保', '尾板']; + + var prevContractSample = { + customerId: '1', + businessDept: 'YW1', + businessOwner: '张经理', + contractOriginalFiles: [], + projectName: '嘉兴氢能运输项目', + contractType: '正式合同', + effectiveDate: '2027-02-17', + paymentMethod: '预付', + endDate: '2028-02-17', + paymentPeriod: '1', + signingCompany: '嘉兴羚牛', + deliveryProvince: '浙江省', + deliveryCity: '嘉兴市', + deliveryLocation: '嘉兴市南湖区科技大道1号', + contractApprovalType: '标准合同审批', + remarks: '续签自原合同 JXZL20260216YW101235A', + authorizedList: [{ name: '张三', phone: '13800138001', idCard: '330102199001011234' }], + rentalOrders: [ + { brand: '品牌A', model: '型号A1', plateNo: '浙A10001', vin: 'L1234567890ABCDEF', monthRent: '8000', serviceItems: [{ project: '保养费用', fee: '200', effectiveDate: '2026-03-01' }], deposit: '10000', remark: '' }, + { brand: '品牌A', model: '型号A2', plateNo: '浙B20002', vin: 'L2234567890ABCDEF', monthRent: '8000', serviceItems: [{ project: '清洗费', fee: '80', effectiveDate: '2026-03-01' }], deposit: '10000', remark: '' } + ], + hydrogenBearer: '客户', + hydrogenPaymentMethod: '预付', + hydrogenPrepay: '5000', + returnHydrogenPrice: '80', + feeTemplate: '标准费用模板A', + billingMethod: 'month' + }; + var prevCustomer = customerList.find(function(c) { return c.id === prevContractSample.customerId; }) || null; + + var cs1 = React.useState(prevCustomer ? prevCustomer.name : ''); + var customerSearch = cs1[0]; + var setCustomerSearch = cs1[1]; + var cs2 = React.useState(prevCustomer); + var selectedCustomer = cs2[0]; + var setSelectedCustomer = cs2[1]; + var cs3 = React.useState(false); + var customerDropdownOpen = cs3[0]; + var setCustomerDropdownOpen = cs3[1]; + var cs4 = React.useState(prevContractSample.businessDept); + var businessDept = cs4[0]; + var setBusinessDept = cs4[1]; + var cs5 = React.useState(prevContractSample.businessOwner); + var businessOwner = cs5[0]; + var setBusinessOwner = cs5[1]; + var cs6 = React.useState(''); + var ownerFocusError = cs6[0]; + var setOwnerFocusError = cs6[1]; + var contractOriginalRef = React.useRef(null); + var csContractOriginal = React.useState([]); + var contractOriginalFiles = csContractOriginal[0]; + var setContractOriginalFiles = csContractOriginal[1]; + + var bs1 = React.useState(prevContractSample.projectName); + var projectName = bs1[0]; + var setProjectName = bs1[1]; + var bs2 = React.useState(prevContractSample.contractType); + var contractType = bs2[0]; + var setContractType = bs2[1]; + var bs3 = React.useState(prevContractSample.effectiveDate); + var effectiveDate = bs3[0]; + var setEffectiveDate = bs3[1]; + var bs4 = React.useState(prevContractSample.paymentMethod); + var paymentMethod = bs4[0]; + var setPaymentMethod = bs4[1]; + // 续签合同:结束日期需重新填写 + var bs6 = React.useState(''); + var endDate = bs6[0]; + var setEndDate = bs6[1]; + var bs7 = React.useState(prevContractSample.paymentPeriod); + var paymentPeriod = bs7[0]; + var setPaymentPeriod = bs7[1]; + var bs8 = React.useState(prevContractSample.signingCompany); + var signingCompany = bs8[0]; + var setSigningCompany = bs8[1]; + var bs9 = React.useState(prevContractSample.deliveryProvince); + var deliveryProvince = bs9[0]; + var setDeliveryProvince = bs9[1]; + var bs10 = React.useState(prevContractSample.deliveryCity); + var deliveryCity = bs10[0]; + var setDeliveryCity = bs10[1]; + var bs10b = React.useState(false); + var deliveryRegionOpen = bs10b[0]; + var setDeliveryRegionOpen = bs10b[1]; + var deliveryRegionClickInsideRef = React.useRef(false); + var bs11 = React.useState(prevContractSample.deliveryLocation); + var deliveryLocation = bs11[0]; + var setDeliveryLocation = bs11[1]; + var bs11a = React.useState(prevContractSample.contractApprovalType); + var contractApprovalType = bs11a[0]; + var setContractApprovalType = bs11a[1]; + var bs12 = React.useState('续签自:旧合同编码JXZL20260216YW101235A'); + var remarks = bs12[0]; + var setRemarks = bs12[1]; + + var as1 = React.useState(prevContractSample.authorizedList.slice()); + var authorizedList = as1[0]; + var setAuthorizedList = as1[1]; + + var emptyRentalRow = { brand: '', model: '', plateNo: '', vin: '', monthRent: '', serviceItems: [{ project: '', fee: '', effectiveDate: '' }], deposit: '', remark: '' }; + var os1 = React.useState(prevContractSample.rentalOrders.map(function(r) { return { brand: r.brand, model: r.model, plateNo: r.plateNo, vin: r.vin, monthRent: r.monthRent, serviceItems: (r.serviceItems || []).map(function(s) { return { project: s.project, fee: s.fee, effectiveDate: s.effectiveDate }; }), deposit: r.deposit, remark: r.remark || '' }; })); + var rentalOrders = os1[0]; + var setRentalOrders = os1[1]; + var os1b = React.useState(null); + var serviceModalRowIndex = os1b[0]; + var setServiceModalRowIndex = os1b[1]; + var os1c = React.useState(prevContractSample.hydrogenBearer); + var hydrogenBearer = os1c[0]; + var setHydrogenBearer = os1c[1]; + var os1d = React.useState(prevContractSample.hydrogenPaymentMethod); + var hydrogenPaymentMethod = os1d[0]; + var setHydrogenPaymentMethod = os1d[1]; + var os1e = React.useState(prevContractSample.hydrogenPrepay); + var hydrogenPrepay = os1e[0]; + var setHydrogenPrepay = os1e[1]; + var os1f = React.useState(prevContractSample.returnHydrogenPrice); + var returnHydrogenPrice = os1f[0]; + var setReturnHydrogenPrice = os1f[1]; + var os1g = React.useState(null); + var plateNoFocusRow = os1g[0]; + var setPlateNoFocusRow = os1g[1]; + var os1h = React.useState(''); + var plateNoSearch = os1h[0]; + var setPlateNoSearch = os1h[1]; + var os1i = React.useState(null); + var serviceItemFocusRow = os1i[0]; + var setServiceItemFocusRow = os1i[1]; + var os1j = React.useState(''); + var serviceItemSearch = os1j[0]; + var setServiceItemSearch = os1j[1]; + var os1k = React.useState(null); + var plateNoDropdownRect = os1k[0]; + var setPlateNoDropdownRect = os1k[1]; + var os2 = React.useState(prevContractSample.feeTemplate); + var feeTemplate = os2[0]; + var setFeeTemplate = os2[1]; + var os3 = React.useState(prevContractSample.billingMethod); + var billingMethod = os3[0]; + var setBillingMethod = os3[1]; + + var cc1State = React.useState(false); + var cc1 = cc1State[0]; + var setCc1 = cc1State[1]; + var cc2State = React.useState(false); + var cc2 = cc2State[0]; + var setCc2 = cc2State[1]; + var cc3State = React.useState(false); + var cc3 = cc3State[0]; + var setCc3 = cc3State[1]; + var cc4State = React.useState(false); + var cc4 = cc4State[0]; + var setCc4 = cc4State[1]; + var cc5State = React.useState(false); + var cc5 = cc5State[0]; + var setCc5 = cc5State[1]; + var cc6State = React.useState(false); + var cc6 = cc6State[0]; + var setCc6 = cc6State[1]; + var reqSpecState = React.useState(false); + var reqSpecOpen = reqSpecState[0]; + var setReqSpecOpen = reqSpecState[1]; + var cancelConfirmState = React.useState(false); + var cancelConfirmOpen = cancelConfirmState[0]; + var setCancelConfirmOpen = cancelConfirmState[1]; + var editedState = React.useState(false); + var edited = editedState[0]; + var setEdited = editedState[1]; + var formErrorsState = React.useState({}); + var formErrors = formErrorsState[0]; + var setFormErrors = formErrorsState[1]; + + var filteredCustomers = customerList.filter(function(c) { return !customerSearch || c.name.indexOf(customerSearch) !== -1; }); + var currentDept = deptList.find(function(d) { return d.id === businessDept; }); + var ownerOptions = currentDept ? currentDept.owners : []; + var contractCode = (function() { + var cityCode = 'JX'; + var typeCode = 'ZL'; + var signCode = contractType === '正式合同' ? 'A' : 'B'; + var dateStr = effectiveDate ? effectiveDate.replace(/-/g, '') : '20260216'; + var deptCode = businessDept || 'YW1'; + return cityCode + typeCode + dateStr + deptCode + '01235' + signCode; + })(); + var originalContractCode = 'JXZL20260216YW101235A'; + var contractCodeDisplay = contractCode + '(续签自:合同编号' + originalContractCode + ')'; + var mainVehicleModelsDisplay = (function() { + var models = []; + var seen = {}; + for (var i = 0; i < rentalOrders.length; i++) { + var m = rentalOrders[i].model; + if (m && !seen[m]) { seen[m] = true; models.push(m); } + } + return models.join('、'); + })(); + var calcRowServiceFee = function(row) { + var sum = 0; + for (var i = 0; i < (row.serviceItems || []).length; i++) { + var fee = parseFloat(row.serviceItems[i].fee); + if (!isNaN(fee)) sum += fee; + } + return sum.toFixed(2); + }; + var rentalTotalVehicles = rentalOrders.length; + var rentalTotalRentService = rentalOrders.reduce(function(s, r) { return s + (parseFloat(r.monthRent) || 0) + (parseFloat(calcRowServiceFee(r)) || 0); }, 0); + var rentalTotalDeposit = rentalOrders.reduce(function(s, r) { return s + (parseFloat(r.deposit) || 0); }, 0); + var rentalTotalHydrogen = (hydrogenPaymentMethod === '预付' && hydrogenBearer === '客户') ? (parseFloat(hydrogenPrepay) || 0) : 0; + + var selectCustomer = function(c) { + setEdited(true); + setSelectedCustomer(c); + setCustomerSearch(c ? c.name : ''); + setCustomerDropdownOpen(false); + }; + var deliveryRegionDisplay = deliveryProvince && deliveryCity ? deliveryProvince + ' / ' + deliveryCity : ''; + var selectDeliveryRegion = function(province, city) { + setEdited(true); + setDeliveryProvince(province); + setDeliveryCity(city); + setDeliveryRegionOpen(false); + }; + var scrollToCard = function(id) { + var el = document.getElementById(id); + if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }; + var handleOwnerFocus = function() { + if (!businessDept) setOwnerFocusError('请先选择业务部门'); + else setOwnerFocusError(''); + }; + var handleOwnerBlur = function() { setOwnerFocusError(''); }; + var openContractOriginal = function() { + // 兼容旧逻辑(保留函数名):不再使用单附件预览 + message.info('请点击附件列表中的文件名预览(原型)'); + }; + + var pad2 = function(n) { + var s = String(n); + return s.length === 1 ? ('0' + s) : s; + }; + + var addContractOriginalFiles = function(fileList) { + if (!fileList || !fileList.length) return; + setEdited(true); + setContractOriginalFiles(function(prev) { + var next = (prev || []).slice(); + for (var i = 0; i < fileList.length; i++) { + var f = fileList[i]; + if (!f) continue; + var sizeDisplay = f.size >= 1024 * 1024 ? (f.size / 1024 / 1024).toFixed(1) + ' MB' : (f.size / 1024).toFixed(1) + ' KB'; + var now = window.moment ? window.moment() : new Date(); + var uploadTimeStr = window.moment ? now.format('YYYY-MM-DD HH:mm') : (now.getFullYear() + '-' + pad2(now.getMonth() + 1) + '-' + pad2(now.getDate()) + ' ' + pad2(now.getHours()) + ':' + pad2(now.getMinutes())); + next.push({ name: f.name, file: f, size: sizeDisplay, uploadTime: uploadTimeStr }); + } + return next; + }); + }; + + var removeContractOriginalFile = function(index) { + setEdited(true); + setContractOriginalFiles(function(prev) { + var next = (prev || []).slice(); + next.splice(index, 1); + return next; + }); + }; + + var openContractOriginalFile = function(index) { + var f = contractOriginalFiles && contractOriginalFiles[index]; + if (!f) return; + if (f.file) { + var url = URL.createObjectURL(f.file); + window.open(url); + return; + } + message.info('原合同反写附件(原型),暂无可预览源文件'); + }; + + var validateSubmitAndReview = function() { + var errs = {}; + if (!selectedCustomer) errs.customer = '请选择客户'; + if (!businessDept) errs.businessDept = '请选择业务部门'; + if (!contractOriginalFiles || contractOriginalFiles.length === 0) errs.contractOriginal = '请上传合同原件'; + if (!businessOwner) errs.businessOwner = '请选择业务负责人'; + if (!projectName || !projectName.trim()) errs.projectName = '请输入项目名称'; + if (!contractType) errs.contractType = '请选择合同类型'; + if (!effectiveDate) errs.effectiveDate = '请选择生效日期'; + if (!paymentMethod) errs.paymentMethod = '请选择付款方式'; + if (!endDate) errs.endDate = '请选择结束日期'; + if (!paymentPeriod) errs.paymentPeriod = '请选择付款周期'; + if (!signingCompany) errs.signingCompany = '请选择签约公司'; + if (!deliveryProvince || !deliveryCity) errs.deliveryRegion = '请选择交车区域'; + if (!deliveryLocation || !deliveryLocation.trim()) errs.deliveryLocation = '请输入交车地点'; + if (!contractApprovalType) errs.contractApprovalType = '请选择合同审批类型'; + var authInvalid = authorizedList.some(function(a) { return !a.name || !a.name.trim() || !a.phone || !a.phone.trim() || !a.idCard || !a.idCard.trim(); }); + if (authInvalid) errs.authorizedList = '请完整填写被授权人姓名、联系电话、身份证'; + var rentalInvalid = rentalOrders.some(function(r) { return !r.brand || !r.model || !(r.monthRent && String(r.monthRent).trim()) || !(r.deposit && String(r.deposit).trim()); }); + if (rentalInvalid) errs.rentalOrders = '请完整填写租赁订单的品牌、型号、车辆月租金、保证金'; + var serviceInvalid = rentalOrders.some(function(r) { + var items = r.serviceItems || []; + for (var i = 0; i < items.length; i++) { + var it = items[i] || {}; + var hasAny = (it.project && String(it.project).trim()) || (it.fee && String(it.fee).trim()) || (it.effectiveDate && String(it.effectiveDate).trim()); + if (hasAny) { + if (!(it.project && String(it.project).trim())) return true; + if (!(it.fee && String(it.fee).trim())) return true; + if (!(it.effectiveDate && String(it.effectiveDate).trim())) return true; + } + } + return false; + }); + if (serviceInvalid) errs.serviceItems = '请完整填写服务项目的服务项目、费用、生效时间'; + if (!hydrogenBearer) errs.hydrogenBearer = '请选择氢费承担方'; + if (hydrogenBearer === '客户' && !hydrogenPaymentMethod) errs.hydrogenPaymentMethod = '请选择付款方式'; + if (hydrogenBearer === '客户' && hydrogenPaymentMethod === '预付' && (!hydrogenPrepay || !String(hydrogenPrepay).trim())) errs.hydrogenPrepay = '请输入氢气预付款'; + if (!returnHydrogenPrice || !String(returnHydrogenPrice).trim()) errs.returnHydrogenPrice = '请输入退还车氢气单价'; + if (!feeTemplate) errs.feeTemplate = '请选择费用模板'; + if (!billingMethod) errs.billingMethod = '请选择账单计算方式'; + setFormErrors(errs); + if (Object.keys(errs).length > 0) { + var firstId = errs.customer || errs.businessDept || errs.businessOwner ? 'card-customer' + : (errs.projectName || errs.contractType || errs.effectiveDate || errs.paymentMethod || errs.endDate || errs.paymentPeriod || errs.signingCompany || errs.deliveryRegion || errs.deliveryLocation || errs.contractApprovalType || errs.contractOriginal) ? 'card-contract' + : errs.authorizedList ? 'card-authorized' + : (errs.rentalOrders || errs.serviceItems || errs.hydrogenBearer || errs.hydrogenPaymentMethod || errs.hydrogenPrepay || errs.returnHydrogenPrice) ? 'card-rental' + : errs.feeTemplate ? 'card-fee' + : 'card-billing'; + setCc1(false); setCc2(false); setCc3(false); setCc4(false); setCc5(false); setCc6(false); + setTimeout(function() { var el = document.getElementById(firstId); if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, 100); + return false; + } + return true; + }; + + var addAuthorized = function() { + setEdited(true); + setAuthorizedList(authorizedList.concat([{ name: '', phone: '', idCard: '' }])); + }; + var removeAuthorized = function(index) { + setEdited(true); + var next = authorizedList.slice(0); + next.splice(index, 1); + setAuthorizedList(next.length ? next : [{ name: '', phone: '', idCard: '' }]); + }; + var updateAuthorized = function(index, field, value) { + setEdited(true); + var next = authorizedList.slice(0); + var cur = next[index] || {}; + var patch = {}; + patch[field] = value; + next[index] = Object.assign({}, cur, patch); + setAuthorizedList(next); + }; + + var addRentalRow = function() { + setEdited(true); + setRentalOrders(rentalOrders.concat([{ brand: '', model: '', plateNo: '', vin: '', monthRent: '', serviceItems: [{ project: '', fee: '', effectiveDate: '' }], deposit: '', remark: '' }])); + }; + var removeRentalRow = function(index) { + setEdited(true); + var next = rentalOrders.slice(0); + next.splice(index, 1); + setRentalOrders(next.length ? next : [{ brand: '', model: '', plateNo: '', vin: '', monthRent: '', serviceItems: [{ project: '', fee: '', effectiveDate: '' }], deposit: '', remark: '' }]); + }; + var copyRentalRow = function(index) { + setEdited(true); + var row = rentalOrders[index]; + if (!row) return; + var serviceItemsCopy = (row.serviceItems || []).map(function(si) { return { project: si.project || '', fee: si.fee || '', effectiveDate: si.effectiveDate || '' }; }); + if (serviceItemsCopy.length === 0) serviceItemsCopy = [{ project: '', fee: '', effectiveDate: '' }]; + var newRow = { brand: row.brand || '', model: row.model || '', plateNo: '', vin: '', monthRent: row.monthRent || '', serviceItems: serviceItemsCopy, deposit: row.deposit || '', remark: row.remark || '' }; + var next = rentalOrders.slice(0); + next.splice(index + 1, 0, newRow); + setRentalOrders(next); + if (typeof message !== 'undefined' && message.success) message.success('已复制该行(车牌号已清空)'); + }; + var updateRentalOrder = function(index, field, value) { + setEdited(true); + var next = rentalOrders.slice(0); + var cur = next[index] || {}; + var newRow = Object.assign({}, cur); + newRow[field] = value; + if (field === 'plateNo') { + var v = vehicleList.find(function(x) { return x.plateNo === value; }); + newRow.vin = v ? v.vin : ''; + if (v && v.brand) { + newRow.brand = v.brand; + newRow.model = v.model || ''; + } + } + if (field === 'brand') newRow.model = ''; + next[index] = newRow; + setRentalOrders(next); + }; + var openServiceModal = function(index) { setServiceModalRowIndex(index); }; + var closeServiceModal = function() { setServiceModalRowIndex(null); }; + var addServiceItem = function() { + if (serviceModalRowIndex === null) return; + setEdited(true); + var next = rentalOrders.slice(0); + var row = next[serviceModalRowIndex]; + var items = (row.serviceItems || []).concat([{ project: '', fee: '', effectiveDate: '' }]); + var newRow = {}; + for (var k in row) { if (row.hasOwnProperty(k) && k !== 'serviceItems') newRow[k] = row[k]; } + newRow.serviceItems = items; + next[serviceModalRowIndex] = newRow; + setRentalOrders(next); + }; + var removeServiceItem = function(siIndex) { + if (serviceModalRowIndex === null) return; + setEdited(true); + var next = rentalOrders.slice(0); + var row = next[serviceModalRowIndex]; + var items = (row.serviceItems || []).slice(0); + items.splice(siIndex, 1); + if (items.length === 0) items = [{ project: '', fee: '', effectiveDate: '' }]; + var newRow = {}; + for (var k in row) { if (row.hasOwnProperty(k) && k !== 'serviceItems') newRow[k] = row[k]; } + newRow.serviceItems = items; + next[serviceModalRowIndex] = newRow; + setRentalOrders(next); + }; + var updateServiceItem = function(siIndex, field, value) { + if (serviceModalRowIndex === null) return; + setEdited(true); + var next = rentalOrders.slice(0); + var row = next[serviceModalRowIndex]; + var items = (row.serviceItems || []).slice(0); + var item = items[siIndex] || {}; + var newItem = {}; + for (var k in item) { if (item.hasOwnProperty(k)) newItem[k] = item[k]; } + newItem[field] = value; + items[siIndex] = newItem; + var newRow = {}; + for (var rk in row) { if (row.hasOwnProperty(rk) && rk !== 'serviceItems') newRow[rk] = row[rk]; } + newRow.serviceItems = items; + next[serviceModalRowIndex] = newRow; + setRentalOrders(next); + }; + + React.useEffect(function() { + if (!deliveryRegionOpen) return; + var handler = function(e) { + if (deliveryRegionClickInsideRef.current) { deliveryRegionClickInsideRef.current = false; return; } + var el = document.getElementById('delivery-region-wrap'); + if (el && !el.contains(e.target)) setDeliveryRegionOpen(false); + }; + document.addEventListener('mousedown', handler); + return function() { document.removeEventListener('mousedown', handler); }; + }, [deliveryRegionOpen]); + + React.useEffect(function() { + if (plateNoFocusRow === null) { setPlateNoDropdownRect(null); return; } + var timer = setTimeout(function() { + var el = document.getElementById('plate-no-input-' + plateNoFocusRow); + if (el && el.getBoundingClientRect) { + var rect = el.getBoundingClientRect(); + setPlateNoDropdownRect({ top: rect.bottom + 2, left: rect.left, width: rect.width }); + } else { setPlateNoDropdownRect(null); } + }, 0); + return function() { clearTimeout(timer); }; + }, [plateNoFocusRow]); + + var styles = { + page: { padding: '16px 24px 48px', backgroundColor: '#f5f5f5', minHeight: '100vh', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', fontSize: 14 }, + breadcrumb: { marginBottom: 16, color: '#666' }, + breadcrumbSep: { margin: '0 8px', color: '#999' }, + anchorWrap: { position: 'fixed', top: 80, right: 24, zIndex: 100, backgroundColor: '#fff', borderRadius: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.12)', padding: '12px 16px', minWidth: 160 }, + anchorItem: { display: 'block', padding: '6px 0', color: '#1890ff', cursor: 'pointer', border: 'none', background: 'none', width: '100%', textAlign: 'left', fontSize: 13 }, + card: { backgroundColor: '#fff', borderRadius: 8, marginBottom: 16, boxShadow: '0 1px 2px rgba(0,0,0,0.05)', overflow: 'hidden' }, + cardHeader: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '16px 20px', borderBottom: '1px solid #f0f0f0', cursor: 'pointer' }, + cardTitle: { fontSize: 16, fontWeight: 600, color: '#333' }, + cardToggle: { color: '#999', fontSize: 14 }, + cardBody: { padding: '20px 24px' }, + formRow: { display: 'flex', flexWrap: 'wrap', marginBottom: 16 }, + formCol: { flex: '0 0 33.33%', minWidth: 200, paddingRight: 16, marginBottom: 8 }, + formColFull: { flex: '0 0 100%', marginBottom: 8 }, + label: { display: 'block', marginBottom: 6, color: '#333' }, + labelRequired: { color: '#ff4d4f', marginRight: 4 }, + input: { width: '100%', padding: '8px 12px', border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 14 }, + inputDisabled: { backgroundColor: '#f5f5f5', color: '#999', cursor: 'not-allowed' }, + inputError: { borderColor: '#ff4d4f' }, + select: { width: '100%', padding: '8px 12px', border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 14 }, + textarea: { width: '100%', padding: '8px 12px', border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 14, minHeight: 80, resize: 'vertical' }, + errMsg: { color: '#ff4d4f', fontSize: 12, marginTop: 4 }, + summaryList: { marginBottom: 16, display: 'flex', flexWrap: 'wrap', gap: 16 }, + summaryListItem: { flex: '0 0 calc(50% - 8px)', display: 'flex', alignItems: 'center', padding: '12px 16px', border: '1px solid #e8e8e8', borderRadius: 4, backgroundColor: '#fafafa', fontSize: 14, boxSizing: 'border-box' }, + summaryListLabel: { flex: '0 0 140px', color: '#666' }, + summaryListValue: { flex: 1, fontWeight: 600, color: '#333' }, + authRow: { display: 'flex', gap: 12, alignItems: 'flex-start', marginBottom: 12 }, + authInput: { flex: 1, padding: '8px 12px', border: '1px solid #d9d9d9', borderRadius: 4 }, + btnDel: { padding: '8px 16px', color: '#ff4d4f', border: '1px solid #ff4d4f', borderRadius: 4, backgroundColor: '#fff', cursor: 'pointer' }, + btnAdd: { padding: '8px 16px', color: '#1890ff', border: '1px dashed #1890ff', borderRadius: 4, backgroundColor: '#fff', cursor: 'pointer', marginBottom: 16 }, + footer: { position: 'fixed', bottom: 0, left: 0, right: 0, padding: '12px 24px', backgroundColor: '#fff', borderTop: '1px solid #e8e8e8', display: 'flex', gap: 12, justifyContent: 'flex-start', zIndex: 99 }, + btn: { padding: '8px 24px', borderRadius: 4, border: '1px solid #d9d9d9', cursor: 'pointer', fontSize: 14 }, + btnPrimary: { backgroundColor: '#1890ff', color: '#fff', borderColor: '#1890ff' }, + btnDefault: { backgroundColor: '#fff', color: '#333' }, + tag: { display: 'inline-block', padding: '2px 8px', marginRight: 8, marginBottom: 4, backgroundColor: '#e6f7ff', color: '#1890ff', borderRadius: 4, fontSize: 12 }, + regionCascader: { position: 'absolute', top: '100%', left: 0, right: 0, marginTop: 4, backgroundColor: '#fff', border: '1px solid #d9d9d9', borderRadius: 4, boxShadow: '0 2px 8px rgba(0,0,0,0.12)', zIndex: 10, display: 'flex', minHeight: 200 }, + regionCascaderCol: { flex: 1, borderRight: '1px solid #f0f0f0', overflowY: 'auto' }, + regionCascaderColLast: { flex: 1 }, + regionCascaderItem: { padding: '10px 12px', cursor: 'pointer' }, + rentalTable: { width: '100%', borderCollapse: 'collapse', fontSize: 13 }, + rentalTh: { padding: '10px 8px', textAlign: 'left', borderBottom: '1px solid #e8e8e8', backgroundColor: '#fafafa', fontWeight: 600 }, + rentalTd: { padding: '8px', borderBottom: '1px solid #f0f0f0', verticalAlign: 'top' }, + rentalTdCenter: { padding: '8px', borderBottom: '1px solid #f0f0f0', verticalAlign: 'middle' }, + rentalInput: { width: '100%', padding: '6px 10px', border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 13 }, + rentalInputDisabled: { backgroundColor: '#f5f5f5', color: '#999' }, + modalMask: { position: 'fixed', left: 0, top: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.45)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center' }, + modalBox: { backgroundColor: '#fff', borderRadius: 8, width: '90%', maxWidth: 720, maxHeight: '85vh', overflow: 'hidden', display: 'flex', flexDirection: 'column', boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }, + modalHeader: { padding: '16px 20px', borderBottom: '1px solid #f0f0f0', fontSize: 16, fontWeight: 600 }, + modalBody: { padding: 20, overflow: 'auto', flex: 1 }, + btnGroup: { display: 'inline-flex', border: '1px solid #d9d9d9', borderRadius: 4, overflow: 'hidden' }, + btnGroupItem: { padding: '8px 16px', border: 'none', borderRight: '1px solid #d9d9d9', backgroundColor: '#fff', color: '#333', cursor: 'pointer', fontSize: 14 }, + btnGroupItemLast: { borderRight: 'none' }, + btnGroupItemActive: { backgroundColor: '#1890ff', color: '#fff', borderColor: '#1890ff', borderRightColor: '#1890ff' }, + feeSectionTitle: { fontSize: 15, fontWeight: 600, color: '#333', marginTop: 20, marginBottom: 10 }, + feeSectionTitleFirst: { marginTop: 0 }, + modalFormInput: { width: '100%', padding: '8px 12px', border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 14, height: 36, boxSizing: 'border-box' } + }; + + var CardBlock = function(props) { + return React.createElement('div', { id: props.id, style: styles.card }, + React.createElement('div', { style: styles.cardHeader, onClick: function() { props.setCollapsed(!props.collapsed); } }, + React.createElement('span', { style: styles.cardTitle }, props.title), + React.createElement('span', { style: styles.cardToggle }, props.collapsed ? '展开' : '收起') + ), + !props.collapsed ? React.createElement('div', { style: styles.cardBody }, props.children) : null + ); + }; + + var FormItem = function(props) { + var colStyle = props.fullWidth ? styles.formColFull : (props.colStyle ? Object.assign({}, styles.formCol, props.colStyle) : styles.formCol); + return React.createElement('div', { style: colStyle }, + React.createElement('label', { style: styles.label }, props.required ? React.createElement('span', { style: styles.labelRequired }, '*') : null, props.label), + props.children, + props.error ? React.createElement('div', { style: styles.errMsg }, props.error) : null + ); + }; + + var customerOptions = customerList.map(function(c) { return React.createElement(Option, { key: c.id, value: c.id }, c.name); }); + var customerFields = React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '客户名称', required: true, error: formErrors.customer }, React.createElement(Select, { placeholder: '请选择或输入搜索客户', style: { width: '100%' }, value: selectedCustomer ? selectedCustomer.id : undefined, disabled: true, status: formErrors.customer ? 'error' : undefined }, customerOptions)), + React.createElement(FormItem, { label: '客户统一信用代码' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.creditCode : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '客户地址' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.address : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '客户联系人' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.contact : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '客户电话' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.phone : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '客户电子邮箱' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.email : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '企业名称' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.companyName : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '企业电话' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.companyPhone : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '邮寄地址' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.mailingAddress : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '开户银行' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.bank : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '银行账号' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.bankAccount : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '纳税人识别号' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.taxId : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '业务部门', required: true, error: formErrors.businessDept }, React.createElement(Select, { placeholder: '请选择业务部门', style: { width: '100%' }, value: businessDept || undefined, onChange: function(v) { setEdited(true); setBusinessDept(v || ''); setBusinessOwner(''); }, status: formErrors.businessDept ? 'error' : undefined }, deptList.map(function(d, i) { return React.createElement(Option, { key: i, value: d.id }, d.name); }))), + React.createElement(FormItem, { label: '业务负责人', required: true, error: formErrors.businessOwner || ownerFocusError }, React.createElement(Select, { placeholder: businessDept ? '请选择' : '请先选择业务部门', style: { width: '100%' }, value: businessOwner || undefined, onChange: function(v) { setEdited(true); setBusinessOwner(v || ''); setOwnerFocusError(''); }, onFocus: handleOwnerFocus, onBlur: handleOwnerBlur, disabled: !businessDept, status: (formErrors.businessOwner || ownerFocusError) ? 'error' : undefined }, ownerOptions.map(function(o, i) { return React.createElement(Option, { key: i, value: o }, o); }))) + ); + + var contractFormRow1 = React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '项目名称', required: true, error: formErrors.projectName }, React.createElement(Input, { placeholder: '请输入项目名称', value: projectName, onChange: function(e) { setEdited(true); setProjectName(e.target.value); }, status: formErrors.projectName ? 'error' : undefined, style: { width: '100%' } })), + React.createElement(FormItem, { label: '合同类型', required: true, error: formErrors.contractType }, React.createElement(Select, { placeholder: '请选择合同类型', style: { width: '100%' }, value: contractType || undefined, onChange: function(v) { setEdited(true); setContractType(v || ''); }, status: formErrors.contractType ? 'error' : undefined }, React.createElement(Option, { value: '正式合同' }, '正式合同'), React.createElement(Option, { value: '试用合同' }, '试用合同'))), + React.createElement(FormItem, { label: '生效日期', required: true, error: formErrors.effectiveDate }, React.createElement(DatePicker, { style: { width: '100%' }, format: 'YYYY-MM-DD', placeholder: '请选择生效日期', value: effectiveDate && window.moment ? window.moment(effectiveDate, 'YYYY-MM-DD') : null, onChange: function(d, dateStr) { setEdited(true); setEffectiveDate(dateStr || ''); }, status: formErrors.effectiveDate ? 'error' : undefined })), + React.createElement(FormItem, { label: '付款方式', required: true, error: formErrors.paymentMethod }, React.createElement(Select, { placeholder: '请选择付款方式', style: { width: '100%' }, value: paymentMethod || undefined, onChange: function(v) { setEdited(true); setPaymentMethod(v || ''); }, status: formErrors.paymentMethod ? 'error' : undefined }, React.createElement(Option, { value: '预付' }, '预付'), React.createElement(Option, { value: '后付' }, '后付'))), + React.createElement(FormItem, { label: '主要车型' }, React.createElement('div', { style: { padding: '8px 12px', minHeight: 36, border: '1px solid #d9d9d9', borderRadius: 4, backgroundColor: '#f5f5f5', display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' } }, mainVehicleModelsDisplay ? mainVehicleModelsDisplay.split('、').map(function(m, i) { return React.createElement('span', { key: i, style: styles.tag }, m); }) : React.createElement('span', { style: { color: '#999' } }, '根据租赁订单自动反写'))), + React.createElement(FormItem, { label: '结束日期', required: true, error: formErrors.endDate }, React.createElement(DatePicker, { style: { width: '100%' }, format: 'YYYY-MM-DD', placeholder: '请选择结束日期', value: endDate && window.moment ? window.moment(endDate, 'YYYY-MM-DD') : null, onChange: function(d, dateStr) { setEdited(true); setEndDate(dateStr || ''); }, status: formErrors.endDate ? 'error' : undefined })), + React.createElement(FormItem, { label: '付款周期', required: true, error: formErrors.paymentPeriod }, React.createElement(Select, { placeholder: '请选择', style: { width: '100%' }, value: paymentPeriod || undefined, onChange: function(v) { setEdited(true); setPaymentPeriod(v || ''); }, status: formErrors.paymentPeriod ? 'error' : undefined }, [1,2,3,4,5,6,7,8,9,10,11,12].map(function(n) { return React.createElement(Option, { key: n, value: String(n) }, n + '个月'); }))), + React.createElement(FormItem, { label: '签约公司', required: true, error: formErrors.signingCompany }, React.createElement(Select, { placeholder: '请选择', style: { width: '100%' }, value: signingCompany || undefined, onChange: function(v) { setEdited(true); setSigningCompany(v || ''); }, status: formErrors.signingCompany ? 'error' : undefined }, orgList.map(function(o, i) { return React.createElement(Option, { key: i, value: o }, o); }))), + React.createElement(FormItem, { label: '交车区域', required: true, error: formErrors.deliveryRegion }, React.createElement('div', { id: 'delivery-region-wrap', style: { position: 'relative' } }, React.createElement(Input, { style: Object.assign({}, formErrors.deliveryRegion ? { borderColor: '#ff4d4f' } : {}, { cursor: 'pointer', caretColor: 'transparent', width: '100%' }), placeholder: '请选择省-市', value: deliveryRegionDisplay, readOnly: true, onClick: function() { setDeliveryRegionOpen(!deliveryRegionOpen); } }), deliveryRegionOpen ? React.createElement('div', { style: styles.regionCascader, onMouseDown: function() { deliveryRegionClickInsideRef.current = true; } }, React.createElement('div', { style: styles.regionCascaderCol }, regionList.map(function(r, i) { var isActive = r.province === deliveryProvince; return React.createElement('div', { key: i, style: Object.assign({}, styles.regionCascaderItem, isActive ? { backgroundColor: '#e6f7ff', color: '#1890ff' } : {}), onMouseEnter: function(e) { if (!isActive) e.currentTarget.style.backgroundColor = '#f5f5f5'; }, onMouseLeave: function(e) { if (!isActive) e.currentTarget.style.backgroundColor = 'transparent'; }, onMouseDown: function(e) { e.preventDefault(); setEdited(true); setDeliveryProvince(r.province); setDeliveryCity(''); } }, r.province); })), React.createElement('div', { style: styles.regionCascaderColLast }, deliveryProvince ? (regionList.find(function(x) { return x.province === deliveryProvince; }) || { cities: [] }).cities.map(function(c, i) { var isActive = c === deliveryCity; return React.createElement('div', { key: i, style: Object.assign({}, styles.regionCascaderItem, isActive ? { backgroundColor: '#e6f7ff', color: '#1890ff' } : {}), onMouseEnter: function(e) { if (!isActive) e.currentTarget.style.backgroundColor = '#f5f5f5'; }, onMouseLeave: function(e) { if (!isActive) e.currentTarget.style.backgroundColor = 'transparent'; }, onMouseDown: function(e) { e.preventDefault(); selectDeliveryRegion(deliveryProvince, c); } }, c); }) : React.createElement('div', { style: { padding: 16, color: '#999', fontSize: 13 } }, '请先选择省'))) : null)) + ); + var contractFormRow4 = React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '交车地点', required: true, error: formErrors.deliveryLocation }, React.createElement(Input, { placeholder: '请输入交车地点', value: deliveryLocation, onChange: function(e) { setEdited(true); setDeliveryLocation(e.target.value); }, status: formErrors.deliveryLocation ? 'error' : undefined, style: { width: '100%' } })), + React.createElement(FormItem, { label: '合同审批类型', required: true, error: formErrors.contractApprovalType }, React.createElement(Select, { placeholder: '请选择合同审批类型', style: { width: '100%' }, value: contractApprovalType || undefined, onChange: function(v) { setEdited(true); setContractApprovalType(v || ''); }, status: formErrors.contractApprovalType ? 'error' : undefined }, React.createElement(Option, { value: '标准合同审批' }, '标准合同审批'), React.createElement(Option, { value: '非标准合同审批' }, '非标准合同审批'))) + ); + var contractFormRow5 = React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '合同原件', required: true, error: formErrors.contractOriginal }, + React.createElement('div', null, + React.createElement('div', { style: { color: '#999', fontSize: 12, marginBottom: 8 } }, '支持多个附件上传(doc/docx/pdf)'), + contractOriginalFiles && contractOriginalFiles.length + ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 12 } }, + contractOriginalFiles.map(function(f, fi) { + return React.createElement('div', { key: fi, style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap', padding: '8px 12px', border: '1px solid #f0f0f0', borderRadius: 6, backgroundColor: '#fafafa' } }, + React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 2 } }, + React.createElement('a', { href: '#', style: { color: '#1890ff' }, onClick: function(e) { e.preventDefault(); openContractOriginalFile(fi); } }, f.name), + React.createElement('span', { style: { color: '#999', fontSize: 12 } }, (f.size ? f.size : '-') + ' · ' + (f.uploadTime ? f.uploadTime : '-')) + ), + React.createElement(Button, { type: 'link', danger: true, onClick: function() { removeContractOriginalFile(fi); } }, '删除') + ); + }) + ) + : null, + React.createElement('input', { ref: contractOriginalRef, type: 'file', multiple: true, accept: '.doc,.docx,.pdf', style: { display: 'none' }, onChange: function(e) { + var files = e.target.files ? Array.prototype.slice.call(e.target.files) : []; + addContractOriginalFiles(files); + e.target.value = ''; + } }), + React.createElement(Button, { type: 'default', style: { padding: '8px 16px', border: '1px solid #d9d9d9', borderRadius: 4, backgroundColor: '#fff', color: '#333', cursor: 'pointer', fontSize: 14 }, onClick: function() { if (contractOriginalRef.current) contractOriginalRef.current.click(); } }, '上传附件') + ) + ) + ); + var contractFormRow2 = React.createElement('div', { style: styles.formRow }, React.createElement(FormItem, { label: '备注', fullWidth: true }, React.createElement(Input.TextArea, { placeholder: '请输入备注信息', value: remarks, onChange: function(e) { setEdited(true); setRemarks(e.target.value); }, style: styles.textarea, rows: 4 }))); + + var authorizedContent = React.createElement('div', null, + React.createElement('div', { style: { display: 'flex', gap: 12, alignItems: 'center', marginBottom: 8 } }, React.createElement('span', { style: { flex: 1, fontWeight: 600, fontSize: 13 } }, React.createElement('span', { style: { color: '#ff4d4f', marginRight: 4 } }, '*'), '被授权人姓名'), React.createElement('span', { style: { flex: 1, fontWeight: 600, fontSize: 13 } }, React.createElement('span', { style: { color: '#ff4d4f', marginRight: 4 } }, '*'), '被授权人联系电话'), React.createElement('span', { style: { flex: 1, fontWeight: 600, fontSize: 13 } }, React.createElement('span', { style: { color: '#ff4d4f', marginRight: 4 } }, '*'), '被授权人身份证'), React.createElement('span', { style: { flex: '0 0 80px' } })), + authorizedList.map(function(item, index) { return React.createElement('div', { key: index, style: styles.authRow }, React.createElement(Input, { style: Object.assign({}, styles.authInput, formErrors.authorizedList ? { borderColor: '#ff4d4f' } : {}, { flex: 1 }), placeholder: '请输入被授权人姓名', value: item.name, onChange: function(e) { updateAuthorized(index, 'name', e.target.value); } }), React.createElement(Input, { style: Object.assign({}, styles.authInput, formErrors.authorizedList ? { borderColor: '#ff4d4f' } : {}, { flex: 1 }), placeholder: '请输入被授权人联系电话', value: item.phone, onChange: function(e) { updateAuthorized(index, 'phone', e.target.value); } }), React.createElement(Input, { style: Object.assign({}, styles.authInput, formErrors.authorizedList ? { borderColor: '#ff4d4f' } : {}, { flex: 1 }), placeholder: '请输入被授权人身份证号', value: item.idCard, onChange: function(e) { updateAuthorized(index, 'idCard', e.target.value); } }), React.createElement(Button, { type: 'button', danger: true, onClick: function() { removeAuthorized(index); } }, '删除')); }), + formErrors.authorizedList ? React.createElement('div', { style: styles.errMsg }, formErrors.authorizedList) : null, + React.createElement(Button, { type: 'dashed', style: { width: '100%', marginBottom: 16 }, onClick: addAuthorized }, '添加一行') + ); + + var hydrogenFormRow = (function() { + var hydrogenFields = []; + hydrogenFields.push(React.createElement(FormItem, { label: '氢费承担方', required: true, error: formErrors.hydrogenBearer }, React.createElement(Select, { style: { width: '100%' }, value: hydrogenBearer || undefined, onChange: function(v) { setEdited(true); setHydrogenBearer(v || ''); setHydrogenPaymentMethod(v === '客户' ? '预付' : ''); }, status: formErrors.hydrogenBearer ? 'error' : undefined }, React.createElement(Option, { value: '我方' }, '我方'), React.createElement(Option, { value: '客户' }, '客户')))); + if (hydrogenBearer === '客户') { hydrogenFields.push(React.createElement(FormItem, { label: '付款方式', required: true, error: formErrors.hydrogenPaymentMethod }, React.createElement(Select, { style: { width: '100%' }, value: hydrogenPaymentMethod || undefined, onChange: function(v) { setEdited(true); setHydrogenPaymentMethod(v || ''); }, status: formErrors.hydrogenPaymentMethod ? 'error' : undefined }, React.createElement(Option, { value: '预付' }, '预付'), React.createElement(Option, { value: '月付款' }, '月付款'), React.createElement(Option, { value: '自行结算' }, '自行结算')))); } + var hydrogenPrepayInput = React.createElement(Input, { placeholder: '0.00', value: hydrogenPrepay, onChange: function(e) { setEdited(true); setHydrogenPrepay(e.target.value); }, addonAfter: '元', status: formErrors.hydrogenPrepay ? 'error' : undefined, style: { width: '100%' } }); + var returnHydrogenInput = React.createElement(Input, { placeholder: '0.00', value: returnHydrogenPrice, onChange: function(e) { setEdited(true); setReturnHydrogenPrice(e.target.value); }, addonAfter: '元', status: formErrors.returnHydrogenPrice ? 'error' : undefined, style: { width: '100%' } }); + var returnHydrogenColStyle = hydrogenPaymentMethod === '预付' ? { flex: '1 1 0', minWidth: 180 } : { flex: '0 0 calc(50% - 8px)', minWidth: 180 }; + hydrogenFields.push(React.createElement('div', { key: 'hydrogen-amount-row', style: { display: 'flex', gap: 16, flex: '1 1 100%' } }, hydrogenPaymentMethod === '预付' ? React.createElement(FormItem, { label: '氢气预付款', required: true, error: formErrors.hydrogenPrepay, colStyle: { flex: '1 1 0', minWidth: 180 } }, hydrogenPrepayInput) : null, React.createElement(FormItem, { label: '退还车氢气单价', required: true, error: formErrors.returnHydrogenPrice, colStyle: returnHydrogenColStyle }, returnHydrogenInput))); + return React.createElement.apply(React, ['div', { style: Object.assign({}, styles.formRow, { marginTop: 20 }) }].concat(hydrogenFields)); + })(); + + var plateNoOptions = vehicleList.map(function(v) { return React.createElement(Option, { key: v.plateNo, value: v.plateNo }, v.plateNo); }); + var rentalTableBody = rentalOrders.map(function(row, idx) { + var modelOpts = row.brand ? (modelByBrand[row.brand] || []) : []; + return React.createElement('tr', { key: idx }, + React.createElement('td', { style: styles.rentalTdCenter }, idx + 1), + React.createElement('td', { style: styles.rentalTd }, React.createElement(Select, { style: { width: '100%', minWidth: 90 }, value: row.brand || undefined, onChange: function(v) { updateRentalOrder(idx, 'brand', v || ''); }, placeholder: '请选择' }, brandList.map(function(b, i) { return React.createElement(Option, { key: i, value: b }, b); }))), + React.createElement('td', { style: styles.rentalTd }, React.createElement(Select, { style: { width: '100%', minWidth: 90 }, value: row.model || undefined, onChange: function(v) { updateRentalOrder(idx, 'model', v || ''); }, disabled: !row.brand, placeholder: '请选择' }, modelOpts.map(function(m, i) { return React.createElement(Option, { key: i, value: m }, m); }))), + React.createElement('td', { style: styles.rentalTd }, React.createElement(Select, { style: { width: '100%' }, placeholder: '请选择或输入搜索', value: row.plateNo || undefined, onChange: function(v) { updateRentalOrder(idx, 'plateNo', v || ''); }, showSearch: true, allowClear: true, filterOption: function(input, opt) { return opt && opt.children && String(opt.children).toLowerCase().indexOf((input || '').toLowerCase()) >= 0; } }, plateNoOptions)), + React.createElement('td', { style: styles.rentalTd }, React.createElement(Input, { value: row.vin || '', disabled: true, style: { width: '100%' } })), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: { display: 'flex', alignItems: 'center' } }, React.createElement(Input, { placeholder: '0.00', value: row.monthRent || '', onChange: function(e) { updateRentalOrder(idx, 'monthRent', e.target.value); }, style: styles.rentalInput }), React.createElement('span', { style: { marginLeft: 4, whiteSpace: 'nowrap' } }, '元'))), + React.createElement('td', { style: styles.rentalTdCenter }, React.createElement(Button, { type: 'link', size: 'small', onClick: function() { openServiceModal(idx); } }, '管理')), + React.createElement('td', { style: styles.rentalTdCenter }, calcRowServiceFee(row) + ' 元'), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: { display: 'flex', alignItems: 'center' } }, React.createElement(Input, { placeholder: '0.00', value: row.deposit || '', onChange: function(e) { updateRentalOrder(idx, 'deposit', e.target.value); }, style: styles.rentalInput }), React.createElement('span', { style: { marginLeft: 4, whiteSpace: 'nowrap' } }, '元'))), + React.createElement('td', { style: styles.rentalTd }, React.createElement(Input, { placeholder: '备注', value: row.remark || '', onChange: function(e) { updateRentalOrder(idx, 'remark', e.target.value); }, style: Object.assign({}, styles.rentalInput, { width: '100%' }) })), + React.createElement('td', { style: styles.rentalTdCenter }, React.createElement('div', { style: { display: 'inline-flex', gap: 4, alignItems: 'center' } }, React.createElement(Button, { type: 'link', size: 'small', onClick: function() { copyRentalRow(idx); } }, '复制'), React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function() { removeRentalRow(idx); } }, '删除'))) + ); + }); + + var rentalSummary = React.createElement('div', { style: styles.summaryList }, + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '租赁车辆数'), React.createElement('span', { style: styles.summaryListValue }, rentalTotalVehicles + ' 辆')), + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '租金及服务费合计'), React.createElement('span', { style: styles.summaryListValue }, rentalTotalRentService.toFixed(2) + ' 元')), + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '保证金总额'), React.createElement('span', { style: styles.summaryListValue }, rentalTotalDeposit.toFixed(2) + ' 元')), + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '氢气预付款金额'), React.createElement('span', { style: styles.summaryListValue }, rentalTotalHydrogen.toFixed(2) + ' 元')) + ); + var reqStarStyle = { color: '#ff4d4f', marginRight: 4 }; + var reqStar = React.createElement('span', { style: reqStarStyle }, '*'); + var rentalTh1 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 50, verticalAlign: 'middle' }) }, '序号'); + var rentalTh2 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 100 }) }, reqStar, '品牌'); + var rentalTh3 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 100 }) }, reqStar, '型号'); + var rentalTh4 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 120 }) }, '车牌号'); + var rentalTh5 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 160 }) }, '车辆识别代码'); + var rentalTh6 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 120 }) }, reqStar, '车辆月租金'); + var rentalTh7 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 80, verticalAlign: 'middle' }) }, '服务费项目'); + var rentalTh8 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 90, verticalAlign: 'middle' }) }, '服务费'); + var rentalTh9 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 100 }) }, reqStar, '保证金'); + var rentalTh10 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 80 }) }, '备注'); + var rentalTh11 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 90, verticalAlign: 'middle' }) }, '操作'); + var rentalTableThead = React.createElement('thead', null, React.createElement('tr', null, rentalTh1, rentalTh2, rentalTh3, rentalTh4, rentalTh5, rentalTh6, rentalTh7, rentalTh8, rentalTh9, rentalTh10, rentalTh11)); + var rentalTableTbody = React.createElement('tbody', null, rentalTableBody); + var rentalTableEl = React.createElement('table', { style: styles.rentalTable }, rentalTableThead, rentalTableTbody); + var rentalTableWrap = React.createElement('div', { style: { overflowX: 'auto', marginBottom: 16 } }, rentalTableEl); + var rentalContent = React.createElement('div', null, + rentalSummary, + formErrors.rentalOrders ? React.createElement('div', { style: styles.errMsg }, formErrors.rentalOrders) : null, + formErrors.serviceItems ? React.createElement('div', { style: styles.errMsg }, formErrors.serviceItems) : null, + rentalTableWrap, + React.createElement(Button, { type: 'dashed', style: { marginTop: 12, width: '100%' }, onClick: addRentalRow }, '添加一行'), + hydrogenFormRow + ); + + var feeTableHeader3 = React.createElement('tr', null, React.createElement('th', { style: styles.rentalTh }, '项目'), React.createElement('th', { style: styles.rentalTh }, '收费标准'), React.createElement('th', { style: styles.rentalTh }, '服务费')); + var feeTableHeader5 = React.createElement('tr', null, React.createElement('th', { style: styles.rentalTh }, '类别'), React.createElement('th', { style: styles.rentalTh }, '损坏部位'), React.createElement('th', { style: styles.rentalTh }, '配件'), React.createElement('th', { style: styles.rentalTh }, '数量'), React.createElement('th', { style: styles.rentalTh }, '费用明细')); + var makeFeeRow3 = function(r, i) { + var td1 = React.createElement('td', { style: styles.rentalTd }, r.project); + var td2 = React.createElement('td', { style: styles.rentalTd }, r.standard); + var td3 = React.createElement('td', { style: styles.rentalTd }, r.serviceFee); + return React.createElement('tr', { key: i }, td1, td2, td3); + }; + var makeFeeRow5 = function(r, i) { + var td1 = React.createElement('td', { style: styles.rentalTd }, r.category); + var td2 = React.createElement('td', { style: styles.rentalTd }, r.part); + var td3 = React.createElement('td', { style: styles.rentalTd }, r.partName); + var td4 = React.createElement('td', { style: styles.rentalTd }, r.qty); + var td5 = React.createElement('td', { style: styles.rentalTd }, r.feeDetail); + return React.createElement('tr', { key: i }, td1, td2, td3, td4, td5); + }; + var feeCertRows = feeTemplateCertFees.map(makeFeeRow3); + var feePenaltyRows = feeTemplatePenaltyFees.map(makeFeeRow3); + var feeConsumablesRows = feeTemplateConsumables.map(makeFeeRow5); + var feeOtherRows = feeTemplateOtherFees.map(makeFeeRow3); + var feeCertTable = React.createElement('table', { style: styles.rentalTable }, React.createElement('thead', null, feeTableHeader3), React.createElement('tbody', null, feeCertRows)); + var feePenaltyTable = React.createElement('table', { style: styles.rentalTable }, React.createElement('thead', null, feeTableHeader3), React.createElement('tbody', null, feePenaltyRows)); + var feeConsumablesTable = React.createElement('table', { style: styles.rentalTable }, React.createElement('thead', null, feeTableHeader5), React.createElement('tbody', null, feeConsumablesRows)); + var feeOtherTable = React.createElement('table', { style: styles.rentalTable }, React.createElement('thead', null, feeTableHeader3), React.createElement('tbody', null, feeOtherRows)); + var feeTemplateSelect = React.createElement('div', { style: styles.formRow }, React.createElement(FormItem, { label: '选择费用模板', required: true, error: formErrors.feeTemplate }, React.createElement(Select, { placeholder: '请选择费用模板', style: { width: '100%' }, value: feeTemplate || undefined, onChange: function(v) { setEdited(true); setFeeTemplate(v || ''); }, status: formErrors.feeTemplate ? 'error' : undefined }, feeTemplates.map(function(f, i) { return React.createElement(Option, { key: i, value: f }, f); })))); + var feeTemplateBody = feeTemplate ? React.createElement('div', null, + React.createElement('div', { style: Object.assign({}, styles.feeSectionTitle, styles.feeSectionTitleFirst) }, '证照补办费用'), + feeCertTable, + React.createElement('div', { style: styles.feeSectionTitle }, '违约金费用'), + feePenaltyTable, + React.createElement('div', { style: styles.feeSectionTitle }, '易损件信息'), + feeConsumablesTable, + React.createElement('div', { style: styles.feeSectionTitle }, '其他费用信息'), + feeOtherTable + ) : null; + var feeContent = React.createElement('div', null, feeTemplateSelect, feeTemplateBody); + + var billingBtnBase = { padding: '12px 16px', border: '1px solid #d9d9d9', borderRadius: 0, backgroundColor: '#fff', color: '#333', cursor: 'pointer', fontSize: 14, textAlign: 'left', flex: 1, minWidth: 0, width: 0, height: 'auto', display: 'flex', flexDirection: 'column', alignItems: 'flex-start', justifyContent: 'flex-start', overflow: 'visible' }; + var billingContent = React.createElement('div', null, + React.createElement('div', { style: { display: 'flex', border: formErrors.billingMethod ? '1px solid #ff4d4f' : '1px solid #d9d9d9', borderRadius: 4, overflow: 'hidden', alignItems: 'stretch' } }, + React.createElement(Button, { type: 'button', style: Object.assign({}, billingBtnBase, { borderRight: '1px solid #d9d9d9' }, billingMethod === 'month' ? { backgroundColor: '#1890ff', color: '#fff', borderColor: '#1890ff' } : {}), onClick: function() { setEdited(true); setBillingMethod('month'); } }, React.createElement('div', { style: { width: '100%' } }, React.createElement('div', { style: { fontWeight: 600, marginBottom: 6 } }, '按自然月结算'), React.createElement('div', { style: { fontSize: 12, opacity: 0.9, whiteSpace: 'normal', wordBreak: 'break-word', lineHeight: 1.5 } }, '账单按照第一个月计费日期开始-当月最后一天为第一期,之后按自然月方式形成每一期账单,例如付款周期为2个月,则第二期账单从2月1日-3月31日。'))), + React.createElement(Button, { type: 'button', style: Object.assign({}, billingBtnBase, billingMethod === 'period' ? { backgroundColor: '#1890ff', color: '#fff', borderColor: '#1890ff' } : {}), onClick: function() { setEdited(true); setBillingMethod('period'); } }, React.createElement('div', { style: { width: '100%' } }, React.createElement('div', { style: { fontWeight: 600, marginBottom: 6 } }, '按付款周期天数结算'), React.createElement('div', { style: { fontSize: 12, opacity: 0.9, whiteSpace: 'normal', wordBreak: 'break-word', lineHeight: 1.5 } }, '账单按照合同基本信息中付款周期实际天数形成一期账单,例如付款周期为60天,则该账单账期为60天,每60天会自动生成新一期账单。'))) + ), + formErrors.billingMethod ? React.createElement('div', { style: styles.errMsg }, formErrors.billingMethod) : null + ); + + var serviceModalRows = serviceModalRowIndex !== null && rentalOrders[serviceModalRowIndex] ? rentalOrders[serviceModalRowIndex].serviceItems : []; + var serviceModalContent = serviceModalRowIndex !== null ? React.createElement('div', { style: styles.modalMask, onClick: function(e) { if (e.target === e.currentTarget) closeServiceModal(); } }, React.createElement('div', { style: styles.modalBox, onClick: function(e) { e.stopPropagation(); } }, React.createElement('div', { style: styles.modalHeader }, '服务项目'), React.createElement('div', { style: styles.modalBody }, React.createElement('table', { style: styles.rentalTable }, React.createElement('thead', null, React.createElement('tr', null, React.createElement('th', { style: styles.rentalTh }, '服务项目'), React.createElement('th', { style: styles.rentalTh }, '费用'), React.createElement('th', { style: styles.rentalTh }, '生效时间'), React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 80 }) }, '操作'))), React.createElement('tbody', null, serviceModalRows.map(function(si, siIdx) { var filteredServiceOpts = serviceItemOptions.filter(function(o) { return !serviceItemSearch || o.indexOf(serviceItemSearch) !== -1; }); return React.createElement('tr', { key: siIdx }, React.createElement('td', { style: styles.rentalTd }, React.createElement(Select, { style: { width: '100%' }, placeholder: '请选择服务项目', value: si.project || undefined, onChange: function(v) { updateServiceItem(siIdx, 'project', v || ''); }, showSearch: true, filterOption: function(input, opt) { return opt && opt.children && String(opt.children).toLowerCase().indexOf((input || '').toLowerCase()) >= 0; }, allowClear: true }, serviceItemOptions.map(function(opt, oi) { return React.createElement(Option, { key: oi, value: opt }, opt); }))), React.createElement('td', { style: styles.rentalTd }, React.createElement(Input, { placeholder: '0.00', value: si.fee || '', onChange: function(e) { updateServiceItem(siIdx, 'fee', e.target.value); }, addonAfter: '元', style: { width: '100%' } })), React.createElement('td', { style: styles.rentalTd }, React.createElement(DatePicker, { style: { width: '100%' }, format: 'YYYY-MM-DD', placeholder: '请选择生效时间', value: si.effectiveDate && window.moment ? window.moment(si.effectiveDate, 'YYYY-MM-DD') : null, onChange: function(d, dateStr) { updateServiceItem(siIdx, 'effectiveDate', dateStr || ''); } })), React.createElement('td', { style: styles.rentalTd }, React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function() { removeServiceItem(siIdx); } }, '删除'))); }))), React.createElement(Button, { type: 'dashed', style: { marginTop: 12, width: '100%' }, onClick: addServiceItem }, '添加一行')), React.createElement('div', { style: { padding: '12px 20px', borderTop: '1px solid #f0f0f0', textAlign: 'right', display: 'flex', gap: 12, justifyContent: 'flex-end' } }, React.createElement(Button, { type: 'primary', onClick: function() { closeServiceModal(); } }, '保存'), React.createElement(Button, { onClick: closeServiceModal }, '关闭')))) : null; + + var requirementContent = '车辆租赁合同-续签合同(2026年3月3日版本)\n「数字化资产ONE-OS运管平台」中的「车辆租赁合同」-「续签合同」模块,在车辆租赁合同操作列点击「续签合同」进行查看;\n1.面包屑:\n#业务管理-车辆租赁合同-续签合同\n\n2.客户基本信息卡片:\n#用于从客户列表中选择客户,并将该合同绑定到业务部门及业务负责人(绑定业务部门/业务负责人主要为了后期从部门/业务负责人维度进行数据统计);\n2.1.客户名称:从原合同自动反写,不可修改,如原客户信息发生改变则按照新信息进行反写;必选项,选择器,支持从输入框内输入内容进行模糊搜索,从客户信息列表中选择对应客户(只显示已通过审核的客户);\n2.2.客户统一信用代码:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「信用代码」字段;\n2.3.客户地址:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「客户地址」字段;\n2.4.客户联系人:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「客户联系人」字段;\n2.5.客户电话:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「客户电话」字段;\n2.6.客户电子邮箱:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「客户电子邮箱」字段;\n2.7.企业名称:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「企业名称」字段;\n2.8.企业电话:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「企业电话」字段;\n2.9.邮寄地址:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「邮寄地址」字段;\n2.10.开户银行:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「开户银行」字段;\n2.11.银行账号:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「银行账号」字段;\n2.12.纳税人识别号:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「纳税人识别号」字段;\n2.13.业务部门:从该条合同自动反写,必选项,选择器,从部门表中选择该租赁合同对应部门;\n2.14.业务负责人:从该条合同自动反写,必选项,选择器,从已选业务部门下拉取对应业务负责人,未选择业务部门时,业务负责人字段不可选;\n\n3.合同基本信息卡片:\n#用于定义租赁合同基本情况和付款方式;\n3.1.项目名称:从原合同自动反写,必填项,输入框,用于定义该合同项目名称,默认提示信息”请输入项目名称“;\n3.2.合同类型:从原合同自动反写,必选项,选择器,合同类型分为「正式合同」「试用合同」;\n3.3.生效日期:从原合同自动反写,必选项,日期选择器,格式为YYYY-MM-DD,精确至天,默认为点击新增日期;\n3.4.付款方式:从原合同自动反写,必选项,付款方式分为「预付」「后付」两种;\n 3.4.1.如果选择预付,以对应「付款方式」和「付款周期」规则,在每一期新账单生成就列入业务待办(工作台功能),同时以消息通知对应用户;\n 3.4.2.如果选择后付,以对应「付款方式」和「付款周期」规则,在每一期新账单生成时,将前一期账单列入业务待办(工作台功能),同时以消息通知对应用户;\n3.5.主要车型:输入框(禁用状态),根据租赁订单信息中所有所选车型,自动反写入输入框并以标签形式显示,支持多车型显示,标签显示:型号名称;\n3.6.结束日期:需要重新填写,必选项,日期选择器,格式为YYYY-MM-DD,精确至天;\n 3.6.1.合同结束日期前30天将以消息提醒方式提醒(消息中心、工作台);\n 3.6.2.到达合同结束日期时,租赁账单将会立刻停止计算,作为最后一期账单;\n 3.6.3.续签合同/转正式合同将重新生成账单,不会对旧合同账单做任何继承处理;\n3.7.付款周期:从原合同自动反写,必选项,选择器,支持1个月-12个月 12种付款周期,账单将以此周期和账单计算方式规则,从交车任务形成的交车单进行完整交车后,定时自动生成账单;\n3.8.签约公司:从原合同自动反写,必选项,选择器,从组织机构表中获取所有根组织机构(如嘉兴羚牛、上海羚牛、广东羚牛等),默认显示新增用户当前机构,可手动修改,后期需要考虑从签约公司维度统计合同相关数据;\n3.9.交车区域:从原合同自动反写,必选项,地区选择器,支持省-市2级,选择区域后,该任务生成交车任务时会自动推送至该区域负责运维人员;\n3.10.交车地点:从原合同自动反写,必填项,输入框,支持自定义输入交车地点;\n3.11.合同审批类型:从原合同自动反写,必填项,选择器,分为「标准合同审批」「非标准合同审批」;\n3.12.合同原件:从原合同自动反写,必填项,按钮,按钮文字为:上传附件,支持多个附件上传(doc/docx/pdf格式);\n3.13.备注:从原合同自动反写,如果该合同为续签合同,则自动在已填备注信息上方额外添加:续签自:旧合同合同编码xxx,文本域,支持自定义输入备注信息;\n\n4.被授权人信息卡片:\n#用于定义租赁合同相关被授权人相关信息,被授权人在交车单完成时,需要选择被授权人,并通过被授权人手机短信,在E签宝进行签字确认;\n4.1.被授权人:从原合同自动反写,必填项,输入框,用于输入被授权人信息;\n4.2.被授权人联系电话:从原合同自动反写,必填项,输入框,用于输入被授权人联系电话,该电话后续需要接收E签宝签字链接;\n4.3.被授权人身份证:从原合同自动反写,必填项,输入框,用于输入被授权人身份证信息;\n4.4.支持通过新增/删除一行的方式,创建或管理多个授权人,后续交车单完成时可从多个授权人中选择接收授权人;\n\n5.租赁订单信息卡片:\n#用于定义租赁合同对应车辆明细费用、氢费明细费用等相关信息;\n5.1.上方为租赁车辆总计数据,包括租赁车辆数、租金及服务费合计、保证金总额、氢气预付款金额等相关信息;\n 5.1.1.租赁车辆数:显示下方租赁订单信息包含多少辆车;\n 5.1.2.租金及服务费合计:显示下方租赁订单信息中车辆租金总额及服务费总额;\n 5.1.3.保证金总额:显示下方租赁订单信息中车辆保证金总额;\n 5.1.4.氢气预付款金额:显示氢气预付款金额,如客户选择自行承担氢费或羚牛承担氢费,则该处为0不计入氢气预付款金额;\n5.2.下方为列表,显示序号、品牌、型号、车牌号、车辆识别代码、车辆月租金(元)、服务费项目、服务费、保证金、备注,续签合同时,根据续签时的车辆信息填写(包括车牌号);\n 5.2.1.序号:从原合同自动反写,自动按照条数生成,规则为1、2、3....以此类推;\n 5.2.2.品牌:从原合同自动反写,必选项,选择器,从型号参数库中「品牌」字段拉取所有品牌;\n 5.2.3.型号:从原合同自动反写,必选项,选择器,与品牌存在级联关系,未选择品牌则无法选择型号;\n 5.2.4.车牌号:从原合同自动反写,选填项,选择器(支持从输入框输入车牌号关键字下拉匹配),可通过选择车牌号对品牌、型号进行反写;\n 5.2.5.车辆识别代码:从原合同自动反写,输入框(禁用),显示该车辆对应车辆识别代码,根据所选车牌号从车辆表直接拉取进行反写;\n 5.2.6.车辆月租金:从原合同自动反写,输入框,支持两位小数,用于输入该车辆月租金金额,输入框后缀为元,如还车时账单周期不满1个月,则按照:(车辆月租金/30)* 实际天数进行计算;\n 5.2.7.服务费项目:从原合同自动反写,点击管理按钮弹出卡片,卡片标题为:服务项目,下方列表显示服务项目、费用、生效时间、操作;\n 5.2.7.1.服务项目:从原合同自动反写,必选项,选择器(支持输入框输入服务项目关键字进行下拉匹配),选项包含:代处理费用、罚款、违章处理违约金、未参加安全培训、车辆出险、年检年审违约、停车费、设备损坏金(包含易损件)、清洗费、上门收车人工费、上门收车送车行驶费、上门收车基础服务费、保险上浮、保养费用、补办驾驶证、补办牌照、补办营运证、补办加氢证、借用备用钥匙、补配钥匙、租金、氢气费-客、退还车氢量差、能源费补缴、能源费退款、送车上门人工费、送车上门送车行驶费、送车上门基础服务费、保证金、氢气预付费、维修费用、ETC-客、ETC卡缺损费、ETC设备缺损费、电费-客、未结算保养费、未结算维修费、车损费、工具损坏或丢失费、证件费、广告损坏费、送车服务费、接车服务费、补办行驶证、超赔险、轮胎磨损费、无忧包、轮胎保、养护保、尾板;\n 5.2.7.2.费用:从原合同自动反写,必填项,输入框,支持2位小数,输入框后缀为元;\n 5.2.7.3.生效时间:从原合同自动反写,必选项,日期选择器,格式为YYYY-MM-DD;\n 5.2.7.4.操作:删除,点击删除直接删除该行数据;\n 5.2.7.5.新增一行数据:点击添加一行服务项目;\n #在提车应收款中,车辆的交车日期和服务费生效日期可能会存在不同的情况,所以服务费需要单独以生效时间进行计算,例如:\n 交车后车辆从1月1日开始计费,付款周期为2个月,则提车应收款租金会计算到3月1日,但是服务费生效时间为1月30日,此情况下需要以3月1日-1月30日,计算出服务费具体收费天数为30天,然后根据:(服务费费用/30)* 服务费收费天数30)进行计算;\n 5.2.8.服务费:从原合同自动反写,自动根据添加的所有服务费项目计算总额,支持2位小数,格式为:xx.xx元;\n 5.2.9.保证金:从原合同自动反写,必填项,输入框,支持2位小数,后缀为元,用于填写车辆需要支付的保证金金额,保证金为提车应收款一次性支付,还车时需要计入退还费用中;\n 5.2.10.备注:选填项,输入框,用于备注车辆复杂情况;\n 5.2.11.操作:删除,点击删除删除该行数据;\n 5.2.12.添加一行:点击后列表新增一行,用于填写一条新的车辆租金费用信息;\n5.3.氢费承担方:从原合同自动反写,必选项,选择器,选项为:「我方」、「客户」;\n 5.3.1.选择「我方」:不显示付款方式、氢气预付款;\n 5.3.2.选择「客户」:付款方式字段默认为预付,可手动修改;\n5.4.付款方式:从原合同自动反写,必选项,选择器,选项为:「预付」、「月付款」、「自行结算」;\n 5.4.1.预付:选择「预付」,需要填写:「氢气预付款」,氢气预付款指合同签署时客户就需预先付出的氢费款项,该部分款项会自动计入提车应收款中氢气预付款金额;\n 5.4.2.月付款:选择「月付款」,提车应收款中不进行收费,而是由业务人员按照实际情况,通过氢费账单功能生成对应氢费账单,单独与客户进行结算;\n 5.4.3.自行结算:选择「自行结算」,指合同签署后,所有氢气费用由客户自行承担;\n5.5.氢气预付款:从原合同自动反写,选择「客户」「预付」时显示,必填项,输入框,支持2位小数,氢气预付款金额会计算入该合同交车应收款中,并计入5.1.4.氢气预付款金额中;\n5.6.退还车氢气单价:从原合同自动反写,必填项,输入框,支持2位小数,后缀为元,不管氢费承担方和付款方式选择任何选项都需要进行维护,该金额主要用于与客户约定还车时,与交车时氢气差值以此费用进行自动计算和结算;\n\n6.其他费用信息卡片:\n#用于选择对应租赁费用模板,选择后展示证照补办费用、违约金费用、易损件费用、其他费用等信息,租赁费用模板管理功能位于「车辆租赁合同」列表左上角;\n6.1.选择费用模板:从原合同自动反写,如果费用模板有更新,则按照更新后内容显示,必选项,从「租赁费用模板」中拉取,选择后自动将该费用模板所有环节费用显示在合同中;\n 6.1.1.证照补办费用:单独标题显示,内容为列表,列表中包括项目、收费标准、服务费,选择费用模板后自动反显;\n 6.1.2.违约金费用:单独标题显示,内容为列表,列表中包含项目、收费标准、服务费,选择费用模板后自动反显;\n 6.1.3.易损件信息:单独标题显示,内容为列表,列表中包含类别、损坏部位、配件、数量、费用明细,选择费用模板后自动反显;\n 6.1.4.其他费用信息:单独标题显示,内容为列表,列表中包含项目、收费标准、服务费,选择费用模板后自动反显;\n\n7.账单计算方式卡片:\n#必选项,从原合同自动反写,填充按钮组,可手动修改,用于定义租赁合同的账单计算方式,分为「按自然月结算」、「按付款周期天数结算」两种方式;\n7.1.按付款周期天数结算:账单按照合同基本信息卡片中每隔付款周期*30天形成一期账单;\n 例如付款周期为2个月,则交车任务交车成功时,提车应收款从交车任务配置的开始计费时间开始,根据60天收取车辆租金,此后每隔60天生成一期租赁账单;\n7.2.按自然月结算:账单按照第一个月计费开始日期到当月最后一天为第一期,之后按照付款周期所选月份间隔,从开始月份第一天到间隔月份最后一天的自然月方式形成一期账单;\n 例如付款周期为2个月,则交车任务交车成功时,提车应收款车辆租金需要收取≥2个月租金作为标准流程,<2个月租金作为非标流程;\n 租赁账单首期从交车任务配置的开始计费时间开始,如付款周期为2个月,则首期账单结束时间为第二个月最后一天,付款周期为3个月,则首期账单结束时间为第三个月最后一天,此后每一期按照实际自然月开始-结束形成账单,例如付款周期为2个月,则第二期账单从2月1日-3月31日,以此类推;\n\n8.最下方为提交并审核、保存、取消三个按钮;\n8.1.点击提交并审核,toast提示:租赁合同已提交审核。同时该租赁合同重新进入租赁合同审核列表中;\n8.2.点击保存,会存储租赁订单已填写内容,不做必填项校验,同时显示在租赁合同列表中,该条数据只能保存人自己查看并编辑,其他人无法操作;\n8.3.点击取消,如当前页面有已编辑内容时,点击取消会进行二次提示,内容为:取消将会丢失所有已填写内容,是否确认?点击确认返回车辆租赁合同列表页;\n\n所有卡片支持收起/展开功能,通过点击卡片右侧收起/展开实现;并增加锚点功能,锚点固定于页面右上角,点击锚点对应卡片名称,页面自动跳转至该卡片所在区域;'; + + var reqSpecModalContent = reqSpecOpen ? React.createElement('div', { style: styles.modalMask, onClick: function(e) { if (e.target === e.currentTarget) setReqSpecOpen(false); } }, + React.createElement('div', { style: Object.assign({}, styles.modalBox, { maxWidth: 720 }), onClick: function(e) { e.stopPropagation(); } }, + React.createElement('div', { style: styles.modalHeader }, '需求说明'), + React.createElement('div', { style: Object.assign({}, styles.modalBody, { maxHeight: '70vh', padding: '20px 24px' }) }, + React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6, color: 'rgba(0,0,0,0.85)' } }, requirementContent) + ), + React.createElement('div', { style: { padding: '12px 20px', borderTop: '1px solid #f0f0f0', textAlign: 'right' } }, React.createElement(Button, { onClick: function() { setReqSpecOpen(false); } }, '关闭')) + ) + ) : null; + + return React.createElement('div', { style: styles.page }, + React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 } }, React.createElement('div', { style: styles.breadcrumb }, React.createElement('span', null, '业务管理'), React.createElement('span', { style: styles.breadcrumbSep }, ' / '), React.createElement('span', null, '车辆租赁合同'), React.createElement('span', { style: styles.breadcrumbSep }, ' / '), React.createElement('span', { style: { color: '#1890ff' } }, '续签合同')), React.createElement('span', { style: { color: '#1890ff', cursor: 'pointer', fontSize: 14 }, onClick: function() { setReqSpecOpen(true); } }, '查看需求说明')), + React.createElement('div', { style: styles.anchorWrap }, + React.createElement('div', { style: { marginBottom: 8, fontWeight: 600, fontSize: 14 } }, '锚点导航'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-customer'); } }, '客户基本信息'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-contract'); } }, '合同基本信息'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-authorized'); } }, '被授权人信息'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-rental'); } }, '租赁订单信息'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-fee'); } }, '其他费用信息'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-billing'); } }, '账单计算方式') + ), + React.createElement('div', { id: 'card-customer' }, React.createElement(CardBlock, { id: 'card-customer', title: '客户基本信息', collapsed: cc1, setCollapsed: setCc1 }, customerFields)), + React.createElement('div', { id: 'card-contract', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '合同基本信息', collapsed: cc2, setCollapsed: setCc2 }, React.createElement('div', null, contractFormRow1, contractFormRow4, contractFormRow5, contractFormRow2))), + React.createElement('div', { id: 'card-authorized', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '被授权人信息', collapsed: cc3, setCollapsed: setCc3 }, authorizedContent)), + React.createElement('div', { id: 'card-rental', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '租赁订单信息', collapsed: cc4, setCollapsed: setCc4 }, rentalContent)), + React.createElement('div', { id: 'card-fee', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '其他费用信息', collapsed: cc5, setCollapsed: setCc5 }, feeContent)), + React.createElement('div', { id: 'card-billing', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '账单计算方式', collapsed: cc6, setCollapsed: setCc6 }, billingContent)), + React.createElement('div', { style: { height: 60 } }), + serviceModalContent, + reqSpecModalContent, + cancelConfirmOpen ? React.createElement('div', { style: styles.modalMask, onClick: function(e) { if (e.target === e.currentTarget) setCancelConfirmOpen(false); } }, + React.createElement('div', { style: Object.assign({}, styles.modalBox, { maxWidth: 520 }), onClick: function(e) { e.stopPropagation(); } }, + React.createElement('div', { style: styles.modalHeader }, '提示'), + React.createElement('div', { style: Object.assign({}, styles.modalBody, { padding: '20px 24px' }) }, '取消将会丢失所有已填写内容,是否确认?'), + React.createElement('div', { style: { padding: '12px 20px', borderTop: '1px solid #f0f0f0', textAlign: 'right', display: 'flex', gap: 12, justifyContent: 'flex-end' } }, + React.createElement(Button, { onClick: function() { setCancelConfirmOpen(false); } }, '否'), + React.createElement(Button, { type: 'primary', onClick: function() { setCancelConfirmOpen(false); setEdited(false); message.info('已返回车辆租赁合同列表(原型)'); } }, '是') + ) + ) + ) : null, + React.createElement('div', { style: styles.footer }, + React.createElement(Button, { type: 'primary', onClick: function() { if (validateSubmitAndReview()) { setEdited(false); message.success('租赁合同已提交审核。同时该租赁合同重新进入租赁合同审核列表中;'); } } }, '提交并审核'), + React.createElement(Button, { onClick: function() { setEdited(false); message.success('已保存(不校验必填项),该条数据只能保存人自己查看并编辑,其他人无法操作;'); } }, '保存'), + React.createElement(Button, { onClick: function() { if (edited) setCancelConfirmOpen(true); else message.info('已返回车辆租赁合同列表(原型)'); } }, '取消') + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-lease-contract/pages/07-车辆租赁合同-转正式合同.jsx b/src/prototypes/oneos-web-lease-contract/pages/07-车辆租赁合同-转正式合同.jsx new file mode 100644 index 0000000..eab7881 --- /dev/null +++ b/src/prototypes/oneos-web-lease-contract/pages/07-车辆租赁合同-转正式合同.jsx @@ -0,0 +1,878 @@ +// 【重要】必须使用 const Component 作为组件变量名 - Axhub 产品原型 +// 车辆管理系统 - 转正式合同模块(布局与合同续签一致,合同编码后增加转正式合同自:合同编号xxxxxxx,IE11+ 兼容,采用 antd) + +const Component = function() { + var antd = window.antd; + var Input = antd.Input; + var Select = antd.Select; + var Button = antd.Button; + var DatePicker = antd.DatePicker; + var Modal = antd.Modal; + var message = antd.message; + var Option = Select.Option; + + var customerList = [ + { id: '1', name: '嘉兴某某物流有限公司', creditCode: '91330400MA2XXXXX1', address: '浙江省嘉兴市南湖区科技大道1号', contact: '张三', phone: '13800138001', email: 'zhangsan@example.com', companyName: '嘉兴某某物流有限公司', companyPhone: '0571-88888888', mailingAddress: '浙江省嘉兴市南湖区科技大道1号', bank: '中国工商银行嘉兴分行', bankAccount: '6222021234567890123', taxId: '91330400MA2XXXXX1' }, + { id: '2', name: '上海某某运输公司', creditCode: '91310000MA2XXXXX2', address: '上海市浦东新区张江高科技园区', contact: '李四', phone: '13800138002', email: 'lisi@example.com', companyName: '上海某某运输公司', companyPhone: '021-66666666', mailingAddress: '上海市浦东新区张江高科技园区', bank: '中国建设银行上海分行', bankAccount: '6217001234567890123', taxId: '91310000MA2XXXXX2' }, + { id: '3', name: '杭州某某租赁有限公司', creditCode: '91330100MA2XXXXX3', address: '浙江省杭州市余杭区未来科技城', contact: '王五', phone: '13800138003', email: 'wangwu@example.com', companyName: '杭州某某租赁有限公司', companyPhone: '0571-99999999', mailingAddress: '浙江省杭州市余杭区未来科技城', bank: '中国农业银行杭州分行', bankAccount: '6228481234567890123', taxId: '91330100MA2XXXXX3' } + ]; + var deptList = [{ id: 'YW1', name: '业务1部', owners: ['张经理', '李专员', '王专员'] }, { id: 'YW2', name: '业务2部', owners: ['赵经理', '钱专员'] }, { id: 'YW3', name: '业务3部', owners: ['孙经理', '周专员'] }]; + var orgList = ['嘉兴羚牛', '上海羚牛', '广东羚牛']; + var regionList = [ + { province: '浙江省', cities: ['杭州市', '宁波市', '嘉兴市', '湖州市'] }, + { province: '上海市', cities: ['上海市'] }, + { province: '广东省', cities: ['广州市', '深圳市', '东莞市'] } + ]; + var feeTemplates = ['标准费用模板A', '标准费用模板B', '定制费用模板C']; + var feeTemplateCertFees = [{ project: '补办行驶证', standard: '50元/次', serviceFee: '20' }, { project: '补办驾驶证', standard: '30元/次', serviceFee: '10' }, { project: '补办牌照', standard: '100元/次', serviceFee: '50' }]; + var feeTemplatePenaltyFees = [{ project: '提前退车违约金', standard: '月租金×1', serviceFee: '0' }, { project: '违章处理违约金', standard: '按实际发生', serviceFee: '50' }]; + var feeTemplateConsumables = [{ category: '轮胎', part: '前轮', partName: '轮胎A型', qty: 1, feeDetail: '500.00' }, { category: '易损件', part: '雨刮', partName: '雨刮片', qty: 2, feeDetail: '80.00' }]; + var feeTemplateOtherFees = [{ project: '上门送车费', standard: '100元/次', serviceFee: '50' }, { project: '上门收车费', standard: '100元/次', serviceFee: '50' }, { project: '清洗费', standard: '80元/次', serviceFee: '30' }]; + var brandList = ['品牌A', '品牌B', '品牌C', '品牌D']; + var modelByBrand = { '品牌A': ['型号A1', '型号A2', '型号A3'], '品牌B': ['型号B1', '型号B2'], '品牌C': ['型号C1', '型号C2', '型号C3'], '品牌D': ['型号D1'] }; + var vehicleList = [ + { plateNo: '浙A10001', vin: 'L1234567890ABCDEF', brand: '品牌A', model: '型号A1' }, + { plateNo: '浙B20002', vin: 'L2234567890ABCDEF', brand: '品牌A', model: '型号A2' }, + { plateNo: '沪A30003', vin: 'L3234567890ABCDEF', brand: '品牌B', model: '型号B1' }, + { plateNo: '粤A40004', vin: 'L4234567890ABCDEF', brand: '品牌C', model: '型号C2' } + ]; + var serviceItemOptions = ['代处理费用', '罚款', '违章处理违约金', '未参加安全培训', '车辆出险', '年检年审违约', '停车费', '设备损坏金(包含易损件)', '清洗费', '上门收车人工费', '上门收车送车行驶费', '上门收车基础服务费', '保险上浮', '保养费用', '补办驾驶证', '补办牌照', '补办营运证', '补办加氢证', '借用备用钥匙', '补配钥匙', '租金', '氢气费-客', '退还车氢量差', '能源费补缴', '能源费退款', '送车上门人工费', '送车上门送车行驶费', '送车上门基础服务费', '保证金', '氢气预付费', '维修费用', 'ETC-客', 'ETC卡缺损费', 'ETC设备缺损费', '电费-客', '未结算保养费', '未结算维修费', '车损费', '工具损坏或丢失费', '证件费', '广告损坏费', '送车服务费', '接车服务费', '补办行驶证', '超赔险', '轮胎磨损费', '无忧包', '轮胎保', '养护保', '尾板']; + + var prevContractSample = { + customerId: '1', + businessDept: 'YW1', + businessOwner: '张经理', + contractOriginal: null, + projectName: '嘉兴氢能运输项目', + contractType: '试用合同', + effectiveDate: '2027-02-17', + paymentMethod: '预付', + endDate: '', + paymentPeriod: '1', + signingCompany: '嘉兴羚牛', + deliveryProvince: '浙江省', + deliveryCity: '嘉兴市', + deliveryLocation: '嘉兴市南湖区科技大道1号', + contractApprovalType: '标准合同审批', + remarks: '', + authorizedList: [{ name: '张三', phone: '13800138001', idCard: '330102199001011234' }], + rentalOrders: [ + { brand: '品牌A', model: '型号A1', plateNo: '浙A10001', vin: 'L1234567890ABCDEF', monthRent: '8000', serviceItems: [{ project: '保养费用', fee: '200', effectiveDate: '2026-03-01' }], deposit: '10000', remark: '' }, + { brand: '品牌A', model: '型号A2', plateNo: '浙B20002', vin: 'L2234567890ABCDEF', monthRent: '8000', serviceItems: [{ project: '清洗费', fee: '80', effectiveDate: '2026-03-01' }], deposit: '10000', remark: '' } + ], + hydrogenBearer: '客户', + hydrogenPaymentMethod: '预付', + hydrogenPrepay: '5000', + returnHydrogenPrice: '80', + feeTemplate: '标准费用模板A', + billingMethod: 'month' + }; + var prevCustomer = customerList.find(function(c) { return c.id === prevContractSample.customerId; }) || null; + + var cs1 = React.useState(prevCustomer ? prevCustomer.name : ''); + var customerSearch = cs1[0]; + var setCustomerSearch = cs1[1]; + var cs2 = React.useState(prevCustomer); + var selectedCustomer = cs2[0]; + var setSelectedCustomer = cs2[1]; + var cs3 = React.useState(false); + var customerDropdownOpen = cs3[0]; + var setCustomerDropdownOpen = cs3[1]; + var cs4 = React.useState(prevContractSample.businessDept); + var businessDept = cs4[0]; + var setBusinessDept = cs4[1]; + var cs5 = React.useState(prevContractSample.businessOwner); + var businessOwner = cs5[0]; + var setBusinessOwner = cs5[1]; + var cs6 = React.useState(''); + var ownerFocusError = cs6[0]; + var setOwnerFocusError = cs6[1]; + var contractOriginalRef = React.useRef(null); + var csContractOriginal = React.useState([ + { name: '租赁合同-试用版原件.pdf', size: '1.2 MB', uploadTime: '2026-02-16 14:30', isOriginal: true } + ]); + var contractOriginalFiles = csContractOriginal[0]; + var setContractOriginalFiles = csContractOriginal[1]; + + var bs1 = React.useState(prevContractSample.projectName); + var projectName = bs1[0]; + var setProjectName = bs1[1]; + // 转正式合同:合同类型自动变更为正式合同 + var bs2 = React.useState('正式合同'); + var contractType = bs2[0]; + var setContractType = bs2[1]; + var bs3 = React.useState(prevContractSample.effectiveDate); + var effectiveDate = bs3[0]; + var setEffectiveDate = bs3[1]; + var bs4 = React.useState(prevContractSample.paymentMethod); + var paymentMethod = bs4[0]; + var setPaymentMethod = bs4[1]; + var bs6 = React.useState(''); + var endDate = bs6[0]; + var setEndDate = bs6[1]; + var bs7 = React.useState(prevContractSample.paymentPeriod); + var paymentPeriod = bs7[0]; + var setPaymentPeriod = bs7[1]; + var bs8 = React.useState(prevContractSample.signingCompany); + var signingCompany = bs8[0]; + var setSigningCompany = bs8[1]; + var bs9 = React.useState(prevContractSample.deliveryProvince); + var deliveryProvince = bs9[0]; + var setDeliveryProvince = bs9[1]; + var bs10 = React.useState(prevContractSample.deliveryCity); + var deliveryCity = bs10[0]; + var setDeliveryCity = bs10[1]; + var bs10b = React.useState(false); + var deliveryRegionOpen = bs10b[0]; + var setDeliveryRegionOpen = bs10b[1]; + var deliveryRegionClickInsideRef = React.useRef(false); + var bs11 = React.useState(prevContractSample.deliveryLocation); + var deliveryLocation = bs11[0]; + var setDeliveryLocation = bs11[1]; + var bs11a = React.useState(prevContractSample.contractApprovalType); + var contractApprovalType = bs11a[0]; + var setContractApprovalType = bs11a[1]; + var bs12 = React.useState('转正式合同自:旧合同编码JXZL20260216YW101235A'); + var remarks = bs12[0]; + var setRemarks = bs12[1]; + + var as1 = React.useState(prevContractSample.authorizedList.slice()); + var authorizedList = as1[0]; + var setAuthorizedList = as1[1]; + + var emptyRentalRow = { brand: '', model: '', plateNo: '', vin: '', monthRent: '', serviceItems: [{ project: '', fee: '', effectiveDate: '' }], deposit: '', remark: '' }; + var os1 = React.useState(prevContractSample.rentalOrders.map(function(r) { return { brand: r.brand, model: r.model, plateNo: r.plateNo, vin: r.vin, monthRent: r.monthRent, serviceItems: (r.serviceItems || []).map(function(s) { return { project: s.project, fee: s.fee, effectiveDate: s.effectiveDate }; }), deposit: r.deposit, remark: r.remark || '' }; })); + var rentalOrders = os1[0]; + var setRentalOrders = os1[1]; + var os1b = React.useState(null); + var serviceModalRowIndex = os1b[0]; + var setServiceModalRowIndex = os1b[1]; + var os1c = React.useState(prevContractSample.hydrogenBearer); + var hydrogenBearer = os1c[0]; + var setHydrogenBearer = os1c[1]; + var os1d = React.useState(prevContractSample.hydrogenPaymentMethod); + var hydrogenPaymentMethod = os1d[0]; + var setHydrogenPaymentMethod = os1d[1]; + var os1e = React.useState(prevContractSample.hydrogenPrepay); + var hydrogenPrepay = os1e[0]; + var setHydrogenPrepay = os1e[1]; + var os1f = React.useState(prevContractSample.returnHydrogenPrice); + var returnHydrogenPrice = os1f[0]; + var setReturnHydrogenPrice = os1f[1]; + var os1g = React.useState(null); + var plateNoFocusRow = os1g[0]; + var setPlateNoFocusRow = os1g[1]; + var os1h = React.useState(''); + var plateNoSearch = os1h[0]; + var setPlateNoSearch = os1h[1]; + var os1i = React.useState(null); + var serviceItemFocusRow = os1i[0]; + var setServiceItemFocusRow = os1i[1]; + var os1j = React.useState(''); + var serviceItemSearch = os1j[0]; + var setServiceItemSearch = os1j[1]; + var os1k = React.useState(null); + var plateNoDropdownRect = os1k[0]; + var setPlateNoDropdownRect = os1k[1]; + var os2 = React.useState(prevContractSample.feeTemplate); + var feeTemplate = os2[0]; + var setFeeTemplate = os2[1]; + var os3 = React.useState(prevContractSample.billingMethod); + var billingMethod = os3[0]; + var setBillingMethod = os3[1]; + + var cc1State = React.useState(false); + var cc1 = cc1State[0]; + var setCc1 = cc1State[1]; + var cc2State = React.useState(false); + var cc2 = cc2State[0]; + var setCc2 = cc2State[1]; + var cc3State = React.useState(false); + var cc3 = cc3State[0]; + var setCc3 = cc3State[1]; + var cc4State = React.useState(false); + var cc4 = cc4State[0]; + var setCc4 = cc4State[1]; + var cc5State = React.useState(false); + var cc5 = cc5State[0]; + var setCc5 = cc5State[1]; + var cc6State = React.useState(false); + var cc6 = cc6State[0]; + var setCc6 = cc6State[1]; + var reqSpecState = React.useState(false); + var reqSpecOpen = reqSpecState[0]; + var setReqSpecOpen = reqSpecState[1]; + var cancelConfirmState = React.useState(false); + var cancelConfirmOpen = cancelConfirmState[0]; + var setCancelConfirmOpen = cancelConfirmState[1]; + var editedState = React.useState(false); + var edited = editedState[0]; + var setEdited = editedState[1]; + var formErrorsState = React.useState({}); + var formErrors = formErrorsState[0]; + var setFormErrors = formErrorsState[1]; + + var filteredCustomers = customerList.filter(function(c) { return !customerSearch || c.name.indexOf(customerSearch) !== -1; }); + var currentDept = deptList.find(function(d) { return d.id === businessDept; }); + var ownerOptions = currentDept ? currentDept.owners : []; + var contractCode = (function() { + var cityCode = 'JX'; + var typeCode = 'ZL'; + var signCode = contractType === '正式合同' ? 'A' : 'B'; + var dateStr = effectiveDate ? effectiveDate.replace(/-/g, '') : '20260216'; + var deptCode = businessDept || 'YW1'; + return cityCode + typeCode + dateStr + deptCode + '01235' + signCode; + })(); + var originalContractCode = 'JXZL20260216YW101235A'; + var contractCodeDisplay = contractCode + '(转正式合同自:合同编号' + originalContractCode + ')'; + var mainVehicleModelsDisplay = (function() { + var models = []; + var seen = {}; + for (var i = 0; i < rentalOrders.length; i++) { + var m = rentalOrders[i].model; + if (m && !seen[m]) { seen[m] = true; models.push(m); } + } + return models.join('、'); + })(); + var calcRowServiceFee = function(row) { + var sum = 0; + for (var i = 0; i < (row.serviceItems || []).length; i++) { + var fee = parseFloat(row.serviceItems[i].fee); + if (!isNaN(fee)) sum += fee; + } + return sum.toFixed(2); + }; + var rentalTotalVehicles = rentalOrders.length; + var rentalTotalRentService = rentalOrders.reduce(function(s, r) { return s + (parseFloat(r.monthRent) || 0) + (parseFloat(calcRowServiceFee(r)) || 0); }, 0); + var rentalTotalDeposit = rentalOrders.reduce(function(s, r) { return s + (parseFloat(r.deposit) || 0); }, 0); + var rentalTotalHydrogen = (hydrogenPaymentMethod === '预付' && hydrogenBearer === '客户') ? (parseFloat(hydrogenPrepay) || 0) : 0; + + var selectCustomer = function(c) { + setEdited(true); + setSelectedCustomer(c); + setCustomerSearch(c ? c.name : ''); + setCustomerDropdownOpen(false); + }; + var deliveryRegionDisplay = deliveryProvince && deliveryCity ? deliveryProvince + ' / ' + deliveryCity : ''; + var selectDeliveryRegion = function(province, city) { + setEdited(true); + setDeliveryProvince(province); + setDeliveryCity(city); + setDeliveryRegionOpen(false); + }; + var scrollToCard = function(id) { + var el = document.getElementById(id); + if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }; + var handleOwnerFocus = function() { + if (!businessDept) setOwnerFocusError('请先选择业务部门'); + else setOwnerFocusError(''); + }; + var handleOwnerBlur = function() { setOwnerFocusError(''); }; + var pad2 = function(n) { + var s = String(n); + return s.length === 1 ? ('0' + s) : s; + }; + var addContractOriginalFiles = function(fileList) { + if (!fileList || !fileList.length) return; + setEdited(true); + setContractOriginalFiles(function(prev) { + var next = (prev || []).slice(); + for (var i = 0; i < fileList.length; i++) { + var f = fileList[i]; + if (!f) continue; + var sizeDisplay = f.size >= 1024 * 1024 ? (f.size / 1024 / 1024).toFixed(1) + ' MB' : (f.size / 1024).toFixed(1) + ' KB'; + var now = window.moment ? window.moment() : new Date(); + var uploadTimeStr = window.moment ? now.format('YYYY-MM-DD HH:mm') : (now.getFullYear() + '-' + pad2(now.getMonth() + 1) + '-' + pad2(now.getDate()) + ' ' + pad2(now.getHours()) + ':' + pad2(now.getMinutes())); + next.push({ name: f.name, file: f, size: sizeDisplay, uploadTime: uploadTimeStr, isOriginal: false }); + } + return next; + }); + }; + var removeContractOriginalFile = function(index) { + var f = contractOriginalFiles && contractOriginalFiles[index]; + if (f && f.isOriginal) return; + setEdited(true); + setContractOriginalFiles(function(prev) { + var next = (prev || []).slice(); + next.splice(index, 1); + return next; + }); + }; + var openContractOriginalFile = function(index) { + var f = contractOriginalFiles && contractOriginalFiles[index]; + if (!f) return; + if (f.file) { + var url = URL.createObjectURL(f.file); + window.open(url); + return; + } + message.info('原合同反写附件(原型),暂无可预览源文件'); + }; + + var validateSubmitAndReview = function() { + var errs = {}; + if (!selectedCustomer) errs.customer = '请选择客户'; + if (!businessDept) errs.businessDept = '请选择业务部门'; + if (!contractOriginalFiles || contractOriginalFiles.length === 0) errs.contractOriginal = '请上传合同原件'; + if (!businessOwner) errs.businessOwner = '请选择业务负责人'; + if (!projectName || !projectName.trim()) errs.projectName = '请输入项目名称'; + // 转正式合同:合同类型固定为正式合同,无需校验 + if (!effectiveDate) errs.effectiveDate = '请选择生效日期'; + if (!paymentMethod) errs.paymentMethod = '请选择付款方式'; + if (!endDate) errs.endDate = '请选择结束日期'; + if (!paymentPeriod) errs.paymentPeriod = '请选择付款周期'; + if (!signingCompany) errs.signingCompany = '请选择签约公司'; + if (!deliveryProvince || !deliveryCity) errs.deliveryRegion = '请选择交车区域'; + if (!deliveryLocation || !deliveryLocation.trim()) errs.deliveryLocation = '请输入交车地点'; + if (!contractApprovalType) errs.contractApprovalType = '请选择合同审批类型'; + var authInvalid = authorizedList.some(function(a) { return !a.name || !a.name.trim() || !a.phone || !a.phone.trim() || !a.idCard || !a.idCard.trim(); }); + if (authInvalid) errs.authorizedList = '请完整填写被授权人姓名、联系电话、身份证'; + var rentalInvalid = rentalOrders.some(function(r) { return !r.brand || !r.model || !(r.monthRent && String(r.monthRent).trim()) || !(r.deposit && String(r.deposit).trim()); }); + if (rentalInvalid) errs.rentalOrders = '请完整填写租赁订单的品牌、型号、车辆月租金、保证金'; + var serviceInvalid = rentalOrders.some(function(r) { + var items = r.serviceItems || []; + for (var i = 0; i < items.length; i++) { + var it = items[i] || {}; + var hasAny = (it.project && String(it.project).trim()) || (it.fee && String(it.fee).trim()) || (it.effectiveDate && String(it.effectiveDate).trim()); + if (hasAny) { + if (!(it.project && String(it.project).trim())) return true; + if (!(it.fee && String(it.fee).trim())) return true; + if (!(it.effectiveDate && String(it.effectiveDate).trim())) return true; + } + } + return false; + }); + if (serviceInvalid) errs.serviceItems = '请完整填写服务项目的服务项目、费用、生效时间'; + if (!hydrogenBearer) errs.hydrogenBearer = '请选择氢费承担方'; + if (hydrogenBearer === '客户' && !hydrogenPaymentMethod) errs.hydrogenPaymentMethod = '请选择付款方式'; + if (hydrogenBearer === '客户' && hydrogenPaymentMethod === '预付' && (!hydrogenPrepay || !String(hydrogenPrepay).trim())) errs.hydrogenPrepay = '请输入氢气预付款'; + if (!returnHydrogenPrice || !String(returnHydrogenPrice).trim()) errs.returnHydrogenPrice = '请输入退还车氢气单价'; + if (!feeTemplate) errs.feeTemplate = '请选择费用模板'; + if (!billingMethod) errs.billingMethod = '请选择账单计算方式'; + setFormErrors(errs); + if (Object.keys(errs).length > 0) { + var firstId = errs.customer || errs.businessDept || errs.businessOwner ? 'card-customer' : (errs.projectName || errs.effectiveDate || errs.paymentMethod || errs.endDate || errs.paymentPeriod || errs.signingCompany || errs.deliveryRegion || errs.deliveryLocation || errs.contractApprovalType || errs.contractOriginal) ? 'card-contract' : errs.authorizedList ? 'card-authorized' : (errs.rentalOrders || errs.serviceItems || errs.hydrogenBearer || errs.hydrogenPaymentMethod || errs.hydrogenPrepay || errs.returnHydrogenPrice) ? 'card-rental' : errs.feeTemplate ? 'card-fee' : 'card-billing'; + setCc1(false); setCc2(false); setCc3(false); setCc4(false); setCc5(false); setCc6(false); + setTimeout(function() { var el = document.getElementById(firstId); if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, 100); + return false; + } + return true; + }; + + var addAuthorized = function() { + setEdited(true); + setAuthorizedList(authorizedList.concat([{ name: '', phone: '', idCard: '' }])); + }; + var removeAuthorized = function(index) { + setEdited(true); + var next = authorizedList.slice(0); + next.splice(index, 1); + setAuthorizedList(next.length ? next : [{ name: '', phone: '', idCard: '' }]); + }; + var updateAuthorized = function(index, field, value) { + setEdited(true); + var next = authorizedList.slice(0); + var cur = next[index] || {}; + var patch = {}; + patch[field] = value; + next[index] = Object.assign({}, cur, patch); + setAuthorizedList(next); + }; + + var addRentalRow = function() { + setEdited(true); + setRentalOrders(rentalOrders.concat([{ brand: '', model: '', plateNo: '', vin: '', monthRent: '', serviceItems: [{ project: '', fee: '', effectiveDate: '' }], deposit: '', remark: '' }])); + }; + var removeRentalRow = function(index) { + setEdited(true); + var next = rentalOrders.slice(0); + next.splice(index, 1); + setRentalOrders(next.length ? next : [{ brand: '', model: '', plateNo: '', vin: '', monthRent: '', serviceItems: [{ project: '', fee: '', effectiveDate: '' }], deposit: '', remark: '' }]); + }; + var updateRentalOrder = function(index, field, value) { + setEdited(true); + var next = rentalOrders.slice(0); + var cur = next[index] || {}; + var newRow = Object.assign({}, cur); + newRow[field] = value; + if (field === 'plateNo') { + var v = vehicleList.find(function(x) { return x.plateNo === value; }); + newRow.vin = v ? v.vin : ''; + if (v && v.brand) { + newRow.brand = v.brand; + newRow.model = v.model || ''; + } + } + if (field === 'brand') newRow.model = ''; + next[index] = newRow; + setRentalOrders(next); + }; + var openServiceModal = function(index) { setServiceModalRowIndex(index); }; + var closeServiceModal = function() { setServiceModalRowIndex(null); }; + var addServiceItem = function() { + if (serviceModalRowIndex === null) return; + setEdited(true); + var next = rentalOrders.slice(0); + var row = next[serviceModalRowIndex]; + var items = (row.serviceItems || []).concat([{ project: '', fee: '', effectiveDate: '' }]); + var newRow = {}; + for (var k in row) { if (row.hasOwnProperty(k) && k !== 'serviceItems') newRow[k] = row[k]; } + newRow.serviceItems = items; + next[serviceModalRowIndex] = newRow; + setRentalOrders(next); + }; + var removeServiceItem = function(siIndex) { + if (serviceModalRowIndex === null) return; + setEdited(true); + var next = rentalOrders.slice(0); + var row = next[serviceModalRowIndex]; + var items = (row.serviceItems || []).slice(0); + items.splice(siIndex, 1); + if (items.length === 0) items = [{ project: '', fee: '', effectiveDate: '' }]; + var newRow = {}; + for (var k in row) { if (row.hasOwnProperty(k) && k !== 'serviceItems') newRow[k] = row[k]; } + newRow.serviceItems = items; + next[serviceModalRowIndex] = newRow; + setRentalOrders(next); + }; + var updateServiceItem = function(siIndex, field, value) { + if (serviceModalRowIndex === null) return; + setEdited(true); + var next = rentalOrders.slice(0); + var row = next[serviceModalRowIndex]; + var items = (row.serviceItems || []).slice(0); + var item = items[siIndex] || {}; + var newItem = {}; + for (var k in item) { if (item.hasOwnProperty(k)) newItem[k] = item[k]; } + newItem[field] = value; + items[siIndex] = newItem; + var newRow = {}; + for (var rk in row) { if (row.hasOwnProperty(rk) && rk !== 'serviceItems') newRow[rk] = row[rk]; } + newRow.serviceItems = items; + next[serviceModalRowIndex] = newRow; + setRentalOrders(next); + }; + + React.useEffect(function() { + if (!deliveryRegionOpen) return; + var handler = function(e) { + if (deliveryRegionClickInsideRef.current) { deliveryRegionClickInsideRef.current = false; return; } + var el = document.getElementById('delivery-region-wrap'); + if (el && !el.contains(e.target)) setDeliveryRegionOpen(false); + }; + document.addEventListener('mousedown', handler); + return function() { document.removeEventListener('mousedown', handler); }; + }, [deliveryRegionOpen]); + + React.useEffect(function() { + if (plateNoFocusRow === null) { setPlateNoDropdownRect(null); return; } + var timer = setTimeout(function() { + var el = document.getElementById('plate-no-input-' + plateNoFocusRow); + if (el && el.getBoundingClientRect) { + var rect = el.getBoundingClientRect(); + setPlateNoDropdownRect({ top: rect.bottom + 2, left: rect.left, width: rect.width }); + } else { setPlateNoDropdownRect(null); } + }, 0); + return function() { clearTimeout(timer); }; + }, [plateNoFocusRow]); + + var styles = { + page: { padding: '16px 24px 48px', backgroundColor: '#f5f5f5', minHeight: '100vh', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', fontSize: 14 }, + breadcrumb: { marginBottom: 16, color: '#666' }, + breadcrumbSep: { margin: '0 8px', color: '#999' }, + anchorWrap: { position: 'fixed', top: 80, right: 24, zIndex: 100, backgroundColor: '#fff', borderRadius: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.12)', padding: '12px 16px', minWidth: 160 }, + anchorItem: { display: 'block', padding: '6px 0', color: '#1890ff', cursor: 'pointer', border: 'none', background: 'none', width: '100%', textAlign: 'left', fontSize: 13 }, + card: { backgroundColor: '#fff', borderRadius: 8, marginBottom: 16, boxShadow: '0 1px 2px rgba(0,0,0,0.05)', overflow: 'hidden' }, + cardHeader: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '16px 20px', borderBottom: '1px solid #f0f0f0', cursor: 'pointer' }, + cardTitle: { fontSize: 16, fontWeight: 600, color: '#333' }, + cardToggle: { color: '#999', fontSize: 14 }, + cardBody: { padding: '20px 24px' }, + formRow: { display: 'flex', flexWrap: 'wrap', marginBottom: 16 }, + formCol: { flex: '0 0 33.33%', minWidth: 200, paddingRight: 16, marginBottom: 8 }, + formColFull: { flex: '0 0 100%', marginBottom: 8 }, + label: { display: 'block', marginBottom: 6, color: '#333' }, + labelRequired: { color: '#ff4d4f', marginRight: 4 }, + input: { width: '100%', padding: '8px 12px', border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 14 }, + inputDisabled: { backgroundColor: '#f5f5f5', color: '#999', cursor: 'not-allowed' }, + inputError: { borderColor: '#ff4d4f' }, + select: { width: '100%', padding: '8px 12px', border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 14 }, + textarea: { width: '100%', padding: '8px 12px', border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 14, minHeight: 80, resize: 'vertical' }, + errMsg: { color: '#ff4d4f', fontSize: 12, marginTop: 4 }, + summaryList: { marginBottom: 16, display: 'flex', flexWrap: 'wrap', gap: 16 }, + summaryListItem: { flex: '0 0 calc(50% - 8px)', display: 'flex', alignItems: 'center', padding: '12px 16px', border: '1px solid #e8e8e8', borderRadius: 4, backgroundColor: '#fafafa', fontSize: 14, boxSizing: 'border-box' }, + summaryListLabel: { flex: '0 0 140px', color: '#666' }, + summaryListValue: { flex: 1, fontWeight: 600, color: '#333' }, + authRow: { display: 'flex', gap: 12, alignItems: 'flex-start', marginBottom: 12 }, + authInput: { flex: 1, padding: '8px 12px', border: '1px solid #d9d9d9', borderRadius: 4 }, + btnDel: { padding: '8px 16px', color: '#ff4d4f', border: '1px solid #ff4d4f', borderRadius: 4, backgroundColor: '#fff', cursor: 'pointer' }, + btnAdd: { padding: '8px 16px', color: '#1890ff', border: '1px dashed #1890ff', borderRadius: 4, backgroundColor: '#fff', cursor: 'pointer', marginBottom: 16 }, + footer: { position: 'fixed', bottom: 0, left: 0, right: 0, padding: '12px 24px', backgroundColor: '#fff', borderTop: '1px solid #e8e8e8', display: 'flex', gap: 12, justifyContent: 'flex-start', zIndex: 99 }, + btn: { padding: '8px 24px', borderRadius: 4, border: '1px solid #d9d9d9', cursor: 'pointer', fontSize: 14 }, + btnPrimary: { backgroundColor: '#1890ff', color: '#fff', borderColor: '#1890ff' }, + btnDefault: { backgroundColor: '#fff', color: '#333' }, + tag: { display: 'inline-block', padding: '2px 8px', marginRight: 8, marginBottom: 4, backgroundColor: '#e6f7ff', color: '#1890ff', borderRadius: 4, fontSize: 12 }, + regionCascader: { position: 'absolute', top: '100%', left: 0, right: 0, marginTop: 4, backgroundColor: '#fff', border: '1px solid #d9d9d9', borderRadius: 4, boxShadow: '0 2px 8px rgba(0,0,0,0.12)', zIndex: 10, display: 'flex', minHeight: 200 }, + regionCascaderCol: { flex: 1, borderRight: '1px solid #f0f0f0', overflowY: 'auto' }, + regionCascaderColLast: { flex: 1 }, + regionCascaderItem: { padding: '10px 12px', cursor: 'pointer' }, + rentalTable: { width: '100%', borderCollapse: 'collapse', fontSize: 13 }, + rentalTh: { padding: '10px 8px', textAlign: 'left', borderBottom: '1px solid #e8e8e8', backgroundColor: '#fafafa', fontWeight: 600 }, + rentalTd: { padding: '8px', borderBottom: '1px solid #f0f0f0', verticalAlign: 'top' }, + rentalTdCenter: { padding: '8px', borderBottom: '1px solid #f0f0f0', verticalAlign: 'middle' }, + rentalInput: { width: '100%', padding: '6px 10px', border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 13 }, + rentalInputDisabled: { backgroundColor: '#f5f5f5', color: '#999' }, + modalMask: { position: 'fixed', left: 0, top: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.45)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center' }, + modalBox: { backgroundColor: '#fff', borderRadius: 8, width: '90%', maxWidth: 720, maxHeight: '85vh', overflow: 'hidden', display: 'flex', flexDirection: 'column', boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }, + modalHeader: { padding: '16px 20px', borderBottom: '1px solid #f0f0f0', fontSize: 16, fontWeight: 600 }, + modalBody: { padding: 20, overflow: 'auto', flex: 1 }, + btnGroup: { display: 'inline-flex', border: '1px solid #d9d9d9', borderRadius: 4, overflow: 'hidden' }, + btnGroupItem: { padding: '8px 16px', border: 'none', borderRight: '1px solid #d9d9d9', backgroundColor: '#fff', color: '#333', cursor: 'pointer', fontSize: 14 }, + btnGroupItemLast: { borderRight: 'none' }, + btnGroupItemActive: { backgroundColor: '#1890ff', color: '#fff', borderColor: '#1890ff', borderRightColor: '#1890ff' }, + feeSectionTitle: { fontSize: 15, fontWeight: 600, color: '#333', marginTop: 20, marginBottom: 10 }, + feeSectionTitleFirst: { marginTop: 0 }, + modalFormInput: { width: '100%', padding: '8px 12px', border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 14, height: 36, boxSizing: 'border-box' } + }; + + var CardBlock = function(props) { + return React.createElement('div', { id: props.id, style: styles.card }, + React.createElement('div', { style: styles.cardHeader, onClick: function() { props.setCollapsed(!props.collapsed); } }, + React.createElement('span', { style: styles.cardTitle }, props.title), + React.createElement('span', { style: styles.cardToggle }, props.collapsed ? '展开' : '收起') + ), + !props.collapsed ? React.createElement('div', { style: styles.cardBody }, props.children) : null + ); + }; + + var FormItem = function(props) { + var colStyle = props.fullWidth ? styles.formColFull : (props.colStyle ? Object.assign({}, styles.formCol, props.colStyle) : styles.formCol); + return React.createElement('div', { style: colStyle }, + React.createElement('label', { style: styles.label }, props.required ? React.createElement('span', { style: styles.labelRequired }, '*') : null, props.label), + props.children, + props.error ? React.createElement('div', { style: styles.errMsg }, props.error) : null + ); + }; + + var customerOptions = customerList.map(function(c) { return React.createElement(Option, { key: c.id, value: c.id }, c.name); }); + var customerFields = React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '客户名称', required: true, error: formErrors.customer }, React.createElement(Select, { placeholder: '请选择或输入搜索客户', style: { width: '100%' }, value: selectedCustomer ? selectedCustomer.id : undefined, disabled: true, status: formErrors.customer ? 'error' : undefined }, customerOptions)), + React.createElement(FormItem, { label: '客户统一信用代码' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.creditCode : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '客户地址' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.address : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '客户联系人' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.contact : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '客户电话' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.phone : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '客户电子邮箱' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.email : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '企业名称' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.companyName : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '企业电话' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.companyPhone : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '邮寄地址' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.mailingAddress : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '开户银行' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.bank : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '银行账号' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.bankAccount : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '纳税人识别号' }, React.createElement(Input, { value: selectedCustomer ? selectedCustomer.taxId : '', disabled: true, style: { width: '100%' } })), + React.createElement(FormItem, { label: '业务部门', required: true, error: formErrors.businessDept }, React.createElement(Select, { placeholder: '请选择业务部门', style: { width: '100%' }, value: businessDept || undefined, onChange: function(v) { setEdited(true); setBusinessDept(v || ''); setBusinessOwner(''); }, status: formErrors.businessDept ? 'error' : undefined }, deptList.map(function(d, i) { return React.createElement(Option, { key: i, value: d.id }, d.name); }))), + React.createElement(FormItem, { label: '业务负责人', required: true, error: formErrors.businessOwner || ownerFocusError }, React.createElement(Select, { placeholder: businessDept ? '请选择' : '请先选择业务部门', style: { width: '100%' }, value: businessOwner || undefined, onChange: function(v) { setEdited(true); setBusinessOwner(v || ''); setOwnerFocusError(''); }, onFocus: handleOwnerFocus, onBlur: handleOwnerBlur, disabled: !businessDept, status: (formErrors.businessOwner || ownerFocusError) ? 'error' : undefined }, ownerOptions.map(function(o, i) { return React.createElement(Option, { key: i, value: o }, o); }))) + ); + + var contractFormRow1 = React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '项目名称', required: true, error: formErrors.projectName }, React.createElement(Input, { placeholder: '请输入项目名称', value: projectName, onChange: function(e) { setEdited(true); setProjectName(e.target.value); }, status: formErrors.projectName ? 'error' : undefined, style: { width: '100%' } })), + React.createElement(FormItem, { label: '合同类型', required: true }, React.createElement(Select, { placeholder: '请选择合同类型', style: { width: '100%' }, value: '正式合同', disabled: true }, React.createElement(Option, { value: '正式合同' }, '正式合同'))), + React.createElement(FormItem, { label: '生效日期', required: true, error: formErrors.effectiveDate }, React.createElement(DatePicker, { style: { width: '100%' }, format: 'YYYY-MM-DD', placeholder: '请选择生效日期', value: effectiveDate && window.moment ? window.moment(effectiveDate, 'YYYY-MM-DD') : null, onChange: function(d, dateStr) { setEdited(true); setEffectiveDate(dateStr || ''); }, status: formErrors.effectiveDate ? 'error' : undefined })), + React.createElement(FormItem, { label: '付款方式', required: true, error: formErrors.paymentMethod }, React.createElement(Select, { placeholder: '请选择付款方式', style: { width: '100%' }, value: paymentMethod || undefined, onChange: function(v) { setEdited(true); setPaymentMethod(v || ''); }, status: formErrors.paymentMethod ? 'error' : undefined }, React.createElement(Option, { value: '预付' }, '预付'), React.createElement(Option, { value: '后付' }, '后付'))), + React.createElement(FormItem, { label: '主要车型' }, React.createElement('div', { style: { padding: '8px 12px', minHeight: 36, border: '1px solid #d9d9d9', borderRadius: 4, backgroundColor: '#f5f5f5', display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' } }, mainVehicleModelsDisplay ? mainVehicleModelsDisplay.split('、').map(function(m, i) { return React.createElement('span', { key: i, style: styles.tag }, m); }) : React.createElement('span', { style: { color: '#999' } }, '根据租赁订单自动反写'))), + React.createElement(FormItem, { label: '结束日期', required: true, error: formErrors.endDate }, React.createElement(DatePicker, { style: { width: '100%' }, format: 'YYYY-MM-DD', placeholder: '请选择结束日期', value: endDate && window.moment ? window.moment(endDate, 'YYYY-MM-DD') : null, onChange: function(d, dateStr) { setEdited(true); setEndDate(dateStr || ''); }, status: formErrors.endDate ? 'error' : undefined })), + React.createElement(FormItem, { label: '付款周期', required: true, error: formErrors.paymentPeriod }, React.createElement(Select, { placeholder: '请选择', style: { width: '100%' }, value: paymentPeriod || undefined, onChange: function(v) { setEdited(true); setPaymentPeriod(v || ''); }, status: formErrors.paymentPeriod ? 'error' : undefined }, [1,2,3,4,5,6,7,8,9,10,11,12].map(function(n) { return React.createElement(Option, { key: n, value: String(n) }, n + '个月'); }))), + React.createElement(FormItem, { label: '签约公司', required: true, error: formErrors.signingCompany }, React.createElement(Select, { placeholder: '请选择', style: { width: '100%' }, value: signingCompany || undefined, onChange: function(v) { setEdited(true); setSigningCompany(v || ''); }, status: formErrors.signingCompany ? 'error' : undefined }, orgList.map(function(o, i) { return React.createElement(Option, { key: i, value: o }, o); }))), + React.createElement(FormItem, { label: '交车区域', required: true, error: formErrors.deliveryRegion }, React.createElement('div', { id: 'delivery-region-wrap', style: { position: 'relative' } }, React.createElement(Input, { style: Object.assign({}, formErrors.deliveryRegion ? { borderColor: '#ff4d4f' } : {}, { cursor: 'pointer', caretColor: 'transparent', width: '100%' }), placeholder: '请选择省-市', value: deliveryRegionDisplay, readOnly: true, onClick: function() { setDeliveryRegionOpen(!deliveryRegionOpen); } }), deliveryRegionOpen ? React.createElement('div', { style: styles.regionCascader, onMouseDown: function() { deliveryRegionClickInsideRef.current = true; } }, React.createElement('div', { style: styles.regionCascaderCol }, regionList.map(function(r, i) { var isActive = r.province === deliveryProvince; return React.createElement('div', { key: i, style: Object.assign({}, styles.regionCascaderItem, isActive ? { backgroundColor: '#e6f7ff', color: '#1890ff' } : {}), onMouseEnter: function(e) { if (!isActive) e.currentTarget.style.backgroundColor = '#f5f5f5'; }, onMouseLeave: function(e) { if (!isActive) e.currentTarget.style.backgroundColor = 'transparent'; }, onMouseDown: function(e) { e.preventDefault(); setEdited(true); setDeliveryProvince(r.province); setDeliveryCity(''); } }, r.province); })), React.createElement('div', { style: styles.regionCascaderColLast }, deliveryProvince ? (regionList.find(function(x) { return x.province === deliveryProvince; }) || { cities: [] }).cities.map(function(c, i) { var isActive = c === deliveryCity; return React.createElement('div', { key: i, style: Object.assign({}, styles.regionCascaderItem, isActive ? { backgroundColor: '#e6f7ff', color: '#1890ff' } : {}), onMouseEnter: function(e) { if (!isActive) e.currentTarget.style.backgroundColor = '#f5f5f5'; }, onMouseLeave: function(e) { if (!isActive) e.currentTarget.style.backgroundColor = 'transparent'; }, onMouseDown: function(e) { e.preventDefault(); selectDeliveryRegion(deliveryProvince, c); } }, c); }) : React.createElement('div', { style: { padding: 16, color: '#999', fontSize: 13 } }, '请先选择省'))) : null)) + ); + var contractFormRow4 = React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '交车地点', required: true, error: formErrors.deliveryLocation }, React.createElement(Input, { placeholder: '请输入交车地点', value: deliveryLocation, onChange: function(e) { setEdited(true); setDeliveryLocation(e.target.value); }, status: formErrors.deliveryLocation ? 'error' : undefined, style: { width: '100%' } })), + React.createElement(FormItem, { label: '合同审批类型', required: true, error: formErrors.contractApprovalType }, React.createElement(Select, { placeholder: '请选择合同审批类型', style: { width: '100%' }, value: contractApprovalType || undefined, onChange: function(v) { setEdited(true); setContractApprovalType(v || ''); }, status: formErrors.contractApprovalType ? 'error' : undefined }, React.createElement(Option, { value: '标准合同审批' }, '标准合同审批'), React.createElement(Option, { value: '非标准合同审批' }, '非标准合同审批'))) + ); + var contractFormRow5 = React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '合同原件', required: true, error: formErrors.contractOriginal }, + React.createElement('div', null, + React.createElement('div', { style: { color: '#999', fontSize: 12, marginBottom: 8 } }, '从原合同自动反写,支持多个附件上传(doc/docx/pdf)'), + contractOriginalFiles && contractOriginalFiles.length + ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 12 } }, + contractOriginalFiles.map(function(f, fi) { + return React.createElement('div', { key: fi, style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap', padding: '8px 12px', border: '1px solid #f0f0f0', borderRadius: 6, backgroundColor: '#fafafa' } }, + React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 2 } }, + React.createElement('a', { href: '#', style: { color: '#1890ff' }, onClick: function(e) { e.preventDefault(); openContractOriginalFile(fi); } }, f.name), + React.createElement('span', { style: { color: '#999', fontSize: 12 } }, (f.size ? f.size : '—') + ' · ' + (f.uploadTime ? f.uploadTime : '—')) + ), + f.isOriginal ? null : React.createElement(Button, { type: 'link', danger: true, onClick: function() { removeContractOriginalFile(fi); } }, '删除') + ); + }) + ) + : null, + React.createElement('input', { ref: contractOriginalRef, type: 'file', multiple: true, accept: '.doc,.docx,.pdf', style: { display: 'none' }, onChange: function(e) { + var files = e.target.files ? Array.prototype.slice.call(e.target.files) : []; + addContractOriginalFiles(files); + e.target.value = ''; + } }), + React.createElement(Button, { type: 'default', style: { padding: '8px 16px', border: '1px solid #d9d9d9', borderRadius: 4, backgroundColor: '#fff', color: '#333', cursor: 'pointer', fontSize: 14 }, onClick: function() { if (contractOriginalRef.current) contractOriginalRef.current.click(); } }, '上传附件') + ) + ) + ); + var contractFormRow2 = React.createElement('div', { style: styles.formRow }, React.createElement(FormItem, { label: '备注', fullWidth: true }, React.createElement(Input.TextArea, { placeholder: '请输入备注信息', value: remarks, onChange: function(e) { setEdited(true); setRemarks(e.target.value); }, style: styles.textarea, rows: 4 }))); + + var authorizedContent = React.createElement('div', null, + React.createElement('div', { style: { display: 'flex', gap: 12, alignItems: 'center', marginBottom: 8 } }, React.createElement('span', { style: { flex: 1, fontWeight: 600, fontSize: 13 } }, React.createElement('span', { style: { color: '#ff4d4f', marginRight: 4 } }, '*'), '被授权人姓名'), React.createElement('span', { style: { flex: 1, fontWeight: 600, fontSize: 13 } }, React.createElement('span', { style: { color: '#ff4d4f', marginRight: 4 } }, '*'), '被授权人联系电话'), React.createElement('span', { style: { flex: 1, fontWeight: 600, fontSize: 13 } }, React.createElement('span', { style: { color: '#ff4d4f', marginRight: 4 } }, '*'), '被授权人身份证'), React.createElement('span', { style: { flex: '0 0 80px' } })), + authorizedList.map(function(item, index) { return React.createElement('div', { key: index, style: styles.authRow }, React.createElement(Input, { style: Object.assign({}, styles.authInput, formErrors.authorizedList ? { borderColor: '#ff4d4f' } : {}, { flex: 1 }), placeholder: '请输入被授权人姓名', value: item.name, onChange: function(e) { updateAuthorized(index, 'name', e.target.value); } }), React.createElement(Input, { style: Object.assign({}, styles.authInput, formErrors.authorizedList ? { borderColor: '#ff4d4f' } : {}, { flex: 1 }), placeholder: '请输入被授权人联系电话', value: item.phone, onChange: function(e) { updateAuthorized(index, 'phone', e.target.value); } }), React.createElement(Input, { style: Object.assign({}, styles.authInput, formErrors.authorizedList ? { borderColor: '#ff4d4f' } : {}, { flex: 1 }), placeholder: '请输入被授权人身份证号', value: item.idCard, onChange: function(e) { updateAuthorized(index, 'idCard', e.target.value); } }), React.createElement(Button, { type: 'button', danger: true, onClick: function() { removeAuthorized(index); } }, '删除')); }), + formErrors.authorizedList ? React.createElement('div', { style: styles.errMsg }, formErrors.authorizedList) : null, + React.createElement(Button, { type: 'dashed', style: { width: '100%', marginBottom: 16 }, onClick: addAuthorized }, '添加一行') + ); + + var hydrogenFormRow = (function() { + var hydrogenFields = []; + hydrogenFields.push(React.createElement(FormItem, { label: '氢费承担方', required: true, error: formErrors.hydrogenBearer }, React.createElement(Select, { style: { width: '100%' }, value: hydrogenBearer || undefined, onChange: function(v) { setEdited(true); setHydrogenBearer(v || ''); setHydrogenPaymentMethod(v === '客户' ? '预付' : ''); }, status: formErrors.hydrogenBearer ? 'error' : undefined }, React.createElement(Option, { value: '我方' }, '我方'), React.createElement(Option, { value: '客户' }, '客户')))); + if (hydrogenBearer === '客户') { hydrogenFields.push(React.createElement(FormItem, { label: '付款方式', required: true, error: formErrors.hydrogenPaymentMethod }, React.createElement(Select, { style: { width: '100%' }, value: hydrogenPaymentMethod || undefined, onChange: function(v) { setEdited(true); setHydrogenPaymentMethod(v || ''); }, status: formErrors.hydrogenPaymentMethod ? 'error' : undefined }, React.createElement(Option, { value: '预付' }, '预付'), React.createElement(Option, { value: '月付款' }, '月付款'), React.createElement(Option, { value: '自行结算' }, '自行结算')))); } + var hydrogenPrepayInput = React.createElement(Input, { placeholder: '0.00', value: hydrogenPrepay, onChange: function(e) { setEdited(true); setHydrogenPrepay(e.target.value); }, addonAfter: '元', status: formErrors.hydrogenPrepay ? 'error' : undefined, style: { width: '100%' } }); + var returnHydrogenInput = React.createElement(Input, { placeholder: '0.00', value: returnHydrogenPrice, onChange: function(e) { setEdited(true); setReturnHydrogenPrice(e.target.value); }, addonAfter: '元', status: formErrors.returnHydrogenPrice ? 'error' : undefined, style: { width: '100%' } }); + var returnHydrogenColStyle = hydrogenPaymentMethod === '预付' ? { flex: '1 1 0', minWidth: 180 } : { flex: '0 0 calc(50% - 8px)', minWidth: 180 }; + hydrogenFields.push(React.createElement('div', { key: 'hydrogen-amount-row', style: { display: 'flex', gap: 16, flex: '1 1 100%' } }, hydrogenPaymentMethod === '预付' ? React.createElement(FormItem, { label: '氢气预付款', required: true, error: formErrors.hydrogenPrepay, colStyle: { flex: '1 1 0', minWidth: 180 } }, hydrogenPrepayInput) : null, React.createElement(FormItem, { label: '退还车氢气单价', required: true, error: formErrors.returnHydrogenPrice, colStyle: returnHydrogenColStyle }, returnHydrogenInput))); + return React.createElement.apply(React, ['div', { style: Object.assign({}, styles.formRow, { marginTop: 20 }) }].concat(hydrogenFields)); + })(); + + var plateNoOptions = vehicleList.map(function(v) { return React.createElement(Option, { key: v.plateNo, value: v.plateNo }, v.plateNo); }); + var rentalTableBody = rentalOrders.map(function(row, idx) { + var modelOpts = row.brand ? (modelByBrand[row.brand] || []) : []; + return React.createElement('tr', { key: idx }, + React.createElement('td', { style: styles.rentalTdCenter }, idx + 1), + React.createElement('td', { style: styles.rentalTd }, React.createElement(Select, { style: { width: '100%', minWidth: 90 }, value: row.brand || undefined, onChange: function(v) { updateRentalOrder(idx, 'brand', v || ''); }, placeholder: '请选择' }, brandList.map(function(b, i) { return React.createElement(Option, { key: i, value: b }, b); }))), + React.createElement('td', { style: styles.rentalTd }, React.createElement(Select, { style: { width: '100%', minWidth: 90 }, value: row.model || undefined, onChange: function(v) { updateRentalOrder(idx, 'model', v || ''); }, disabled: !row.brand, placeholder: '请选择' }, modelOpts.map(function(m, i) { return React.createElement(Option, { key: i, value: m }, m); }))), + React.createElement('td', { style: styles.rentalTd }, React.createElement(Select, { style: { width: '100%' }, placeholder: '请选择或输入搜索', value: row.plateNo || undefined, onChange: function(v) { updateRentalOrder(idx, 'plateNo', v || ''); }, showSearch: true, allowClear: true, filterOption: function(input, opt) { return opt && opt.children && String(opt.children).toLowerCase().indexOf((input || '').toLowerCase()) >= 0; } }, plateNoOptions)), + React.createElement('td', { style: styles.rentalTd }, React.createElement(Input, { value: row.vin || '', disabled: true, style: { width: '100%' } })), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: { display: 'flex', alignItems: 'center' } }, React.createElement(Input, { placeholder: '0.00', value: row.monthRent || '', onChange: function(e) { updateRentalOrder(idx, 'monthRent', e.target.value); }, style: styles.rentalInput }), React.createElement('span', { style: { marginLeft: 4, whiteSpace: 'nowrap' } }, '元'))), + React.createElement('td', { style: styles.rentalTdCenter }, React.createElement(Button, { type: 'link', size: 'small', onClick: function() { openServiceModal(idx); } }, '管理')), + React.createElement('td', { style: styles.rentalTdCenter }, calcRowServiceFee(row) + ' 元'), + React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: { display: 'flex', alignItems: 'center' } }, React.createElement(Input, { placeholder: '0.00', value: row.deposit || '', onChange: function(e) { updateRentalOrder(idx, 'deposit', e.target.value); }, style: styles.rentalInput }), React.createElement('span', { style: { marginLeft: 4, whiteSpace: 'nowrap' } }, '元'))), + React.createElement('td', { style: styles.rentalTd }, React.createElement(Input, { placeholder: '备注', value: row.remark || '', onChange: function(e) { updateRentalOrder(idx, 'remark', e.target.value); }, style: Object.assign({}, styles.rentalInput, { width: '100%' }) })), + React.createElement('td', { style: styles.rentalTdCenter }, React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function() { removeRentalRow(idx); } }, '删除')) + ); + }); + + var rentalSummary = React.createElement('div', { style: styles.summaryList }, + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '租赁车辆数'), React.createElement('span', { style: styles.summaryListValue }, rentalTotalVehicles + ' 辆')), + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '租金及服务费合计'), React.createElement('span', { style: styles.summaryListValue }, rentalTotalRentService.toFixed(2) + ' 元')), + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '保证金总额'), React.createElement('span', { style: styles.summaryListValue }, rentalTotalDeposit.toFixed(2) + ' 元')), + React.createElement('div', { style: styles.summaryListItem }, React.createElement('span', { style: styles.summaryListLabel }, '氢气预付款金额'), React.createElement('span', { style: styles.summaryListValue }, rentalTotalHydrogen.toFixed(2) + ' 元')) + ); + var reqStarStyle = { color: '#ff4d4f', marginRight: 4 }; + var reqStar = React.createElement('span', { style: reqStarStyle }, '*'); + var rentalTh1 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 50, verticalAlign: 'middle' }) }, '序号'); + var rentalTh2 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 100 }) }, reqStar, '品牌'); + var rentalTh3 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 100 }) }, reqStar, '型号'); + var rentalTh4 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 120 }) }, '车牌号'); + var rentalTh5 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 160 }) }, '车辆识别代码'); + var rentalTh6 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 120 }) }, reqStar, '车辆月租金'); + var rentalTh7 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 80, verticalAlign: 'middle' }) }, '服务费项目'); + var rentalTh8 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 90, verticalAlign: 'middle' }) }, '服务费'); + var rentalTh9 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 100 }) }, reqStar, '保证金'); + var rentalTh10 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 80 }) }, '备注'); + var rentalTh11 = React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 60, verticalAlign: 'middle' }) }, '操作'); + var rentalTableThead = React.createElement('thead', null, React.createElement('tr', null, rentalTh1, rentalTh2, rentalTh3, rentalTh4, rentalTh5, rentalTh6, rentalTh7, rentalTh8, rentalTh9, rentalTh10, rentalTh11)); + var rentalTableTbody = React.createElement('tbody', null, rentalTableBody); + var rentalTableEl = React.createElement('table', { style: styles.rentalTable }, rentalTableThead, rentalTableTbody); + var rentalTableWrap = React.createElement('div', { style: { overflowX: 'auto', marginBottom: 16 } }, rentalTableEl); + var rentalContent = React.createElement('div', null, rentalSummary, formErrors.rentalOrders ? React.createElement('div', { style: styles.errMsg }, formErrors.rentalOrders) : null, formErrors.serviceItems ? React.createElement('div', { style: styles.errMsg }, formErrors.serviceItems) : null, rentalTableWrap, React.createElement(Button, { type: 'dashed', style: { marginTop: 12, width: '100%' }, onClick: addRentalRow }, '添加一行'), hydrogenFormRow); + + var feeTableHeader3 = React.createElement('tr', null, React.createElement('th', { style: styles.rentalTh }, '项目'), React.createElement('th', { style: styles.rentalTh }, '收费标准'), React.createElement('th', { style: styles.rentalTh }, '服务费')); + var feeTableHeader5 = React.createElement('tr', null, React.createElement('th', { style: styles.rentalTh }, '类别'), React.createElement('th', { style: styles.rentalTh }, '损坏部位'), React.createElement('th', { style: styles.rentalTh }, '配件'), React.createElement('th', { style: styles.rentalTh }, '数量'), React.createElement('th', { style: styles.rentalTh }, '费用明细')); + var makeFeeRow3 = function(r, i) { + var td1 = React.createElement('td', { style: styles.rentalTd }, r.project); + var td2 = React.createElement('td', { style: styles.rentalTd }, r.standard); + var td3 = React.createElement('td', { style: styles.rentalTd }, r.serviceFee); + return React.createElement('tr', { key: i }, td1, td2, td3); + }; + var makeFeeRow5 = function(r, i) { + var td1 = React.createElement('td', { style: styles.rentalTd }, r.category); + var td2 = React.createElement('td', { style: styles.rentalTd }, r.part); + var td3 = React.createElement('td', { style: styles.rentalTd }, r.partName); + var td4 = React.createElement('td', { style: styles.rentalTd }, r.qty); + var td5 = React.createElement('td', { style: styles.rentalTd }, r.feeDetail); + return React.createElement('tr', { key: i }, td1, td2, td3, td4, td5); + }; + var feeCertRows = feeTemplateCertFees.map(makeFeeRow3); + var feePenaltyRows = feeTemplatePenaltyFees.map(makeFeeRow3); + var feeConsumablesRows = feeTemplateConsumables.map(makeFeeRow5); + var feeOtherRows = feeTemplateOtherFees.map(makeFeeRow3); + var feeCertTable = React.createElement('table', { style: styles.rentalTable }, React.createElement('thead', null, feeTableHeader3), React.createElement('tbody', null, feeCertRows)); + var feePenaltyTable = React.createElement('table', { style: styles.rentalTable }, React.createElement('thead', null, feeTableHeader3), React.createElement('tbody', null, feePenaltyRows)); + var feeConsumablesTable = React.createElement('table', { style: styles.rentalTable }, React.createElement('thead', null, feeTableHeader5), React.createElement('tbody', null, feeConsumablesRows)); + var feeOtherTable = React.createElement('table', { style: styles.rentalTable }, React.createElement('thead', null, feeTableHeader3), React.createElement('tbody', null, feeOtherRows)); + var feeTemplateSelect = React.createElement('div', { style: styles.formRow }, React.createElement(FormItem, { label: '选择费用模板', required: true, error: formErrors.feeTemplate }, React.createElement(Select, { placeholder: '请选择费用模板', style: { width: '100%' }, value: feeTemplate || undefined, onChange: function(v) { setEdited(true); setFeeTemplate(v || ''); }, status: formErrors.feeTemplate ? 'error' : undefined }, feeTemplates.map(function(f, i) { return React.createElement(Option, { key: i, value: f }, f); })))); + var feeTemplateBody = feeTemplate ? React.createElement('div', null, + React.createElement('div', { style: Object.assign({}, styles.feeSectionTitle, styles.feeSectionTitleFirst) }, '证照补办费用'), + feeCertTable, + React.createElement('div', { style: styles.feeSectionTitle }, '违约金费用'), + feePenaltyTable, + React.createElement('div', { style: styles.feeSectionTitle }, '易损件信息'), + feeConsumablesTable, + React.createElement('div', { style: styles.feeSectionTitle }, '其他费用信息'), + feeOtherTable + ) : null; + var feeContent = React.createElement('div', null, feeTemplateSelect, feeTemplateBody); + + var billingBtnBase = { padding: '12px 16px', border: '1px solid #d9d9d9', borderRadius: 0, backgroundColor: '#fff', color: '#333', cursor: 'pointer', fontSize: 14, textAlign: 'left', flex: 1, minWidth: 0, width: 0, height: 'auto', display: 'flex', flexDirection: 'column', alignItems: 'flex-start', justifyContent: 'flex-start', overflow: 'visible' }; + var billingContent = React.createElement('div', null, + React.createElement('div', { style: { display: 'flex', border: formErrors.billingMethod ? '1px solid #ff4d4f' : '1px solid #d9d9d9', borderRadius: 4, overflow: 'hidden', alignItems: 'stretch' } }, + React.createElement(Button, { type: 'button', style: Object.assign({}, billingBtnBase, { borderRight: '1px solid #d9d9d9' }, billingMethod === 'month' ? { backgroundColor: '#1890ff', color: '#fff', borderColor: '#1890ff' } : {}), onClick: function() { setEdited(true); setBillingMethod('month'); } }, React.createElement('div', { style: { width: '100%' } }, React.createElement('div', { style: { fontWeight: 600, marginBottom: 6 } }, '按自然月结算'), React.createElement('div', { style: { fontSize: 12, opacity: 0.9, whiteSpace: 'normal', wordBreak: 'break-word', lineHeight: 1.5 } }, '账单按照第一个月计费日期开始-当月最后一天为第一期,之后按自然月方式形成每一期账单,例如付款周期为2个月,则第二期账单从2月1日-3月31日。'))), + React.createElement(Button, { type: 'button', style: Object.assign({}, billingBtnBase, billingMethod === 'period' ? { backgroundColor: '#1890ff', color: '#fff', borderColor: '#1890ff' } : {}), onClick: function() { setEdited(true); setBillingMethod('period'); } }, React.createElement('div', { style: { width: '100%' } }, React.createElement('div', { style: { fontWeight: 600, marginBottom: 6 } }, '按付款周期天数结算'), React.createElement('div', { style: { fontSize: 12, opacity: 0.9, whiteSpace: 'normal', wordBreak: 'break-word', lineHeight: 1.5 } }, '账单按照合同基本信息中付款周期实际天数形成一期账单,例如付款周期为60天,则该账单账期为60天,每60天会自动生成新一期账单。'))) + ), + formErrors.billingMethod ? React.createElement('div', { style: styles.errMsg }, formErrors.billingMethod) : null + ); + + var serviceModalRows = serviceModalRowIndex !== null && rentalOrders[serviceModalRowIndex] ? rentalOrders[serviceModalRowIndex].serviceItems : []; + var serviceModalContent = serviceModalRowIndex !== null ? React.createElement('div', { style: styles.modalMask, onClick: function(e) { if (e.target === e.currentTarget) closeServiceModal(); } }, React.createElement('div', { style: styles.modalBox, onClick: function(e) { e.stopPropagation(); } }, React.createElement('div', { style: styles.modalHeader }, '服务项目'), React.createElement('div', { style: styles.modalBody }, React.createElement('table', { style: styles.rentalTable }, React.createElement('thead', null, React.createElement('tr', null, React.createElement('th', { style: styles.rentalTh }, '服务项目'), React.createElement('th', { style: styles.rentalTh }, '费用'), React.createElement('th', { style: styles.rentalTh }, '生效时间'), React.createElement('th', { style: Object.assign({}, styles.rentalTh, { width: 80 }) }, '操作'))), React.createElement('tbody', null, serviceModalRows.map(function(si, siIdx) { var filteredServiceOpts = serviceItemOptions.filter(function(o) { return !serviceItemSearch || o.indexOf(serviceItemSearch) !== -1; }); return React.createElement('tr', { key: siIdx }, React.createElement('td', { style: styles.rentalTd }, React.createElement(Select, { style: { width: '100%' }, placeholder: '请选择服务项目', value: si.project || undefined, onChange: function(v) { updateServiceItem(siIdx, 'project', v || ''); }, showSearch: true, filterOption: function(input, opt) { return opt && opt.children && String(opt.children).toLowerCase().indexOf((input || '').toLowerCase()) >= 0; }, allowClear: true }, serviceItemOptions.map(function(opt, oi) { return React.createElement(Option, { key: oi, value: opt }, opt); }))), React.createElement('td', { style: styles.rentalTd }, React.createElement(Input, { placeholder: '0.00', value: si.fee || '', onChange: function(e) { updateServiceItem(siIdx, 'fee', e.target.value); }, addonAfter: '元', style: { width: '100%' } })), React.createElement('td', { style: styles.rentalTd }, React.createElement(DatePicker, { style: { width: '100%' }, format: 'YYYY-MM-DD', placeholder: '请选择生效时间', value: si.effectiveDate && window.moment ? window.moment(si.effectiveDate, 'YYYY-MM-DD') : null, onChange: function(d, dateStr) { updateServiceItem(siIdx, 'effectiveDate', dateStr || ''); } })), React.createElement('td', { style: styles.rentalTd }, React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function() { removeServiceItem(siIdx); } }, '删除'))); }))), React.createElement(Button, { type: 'dashed', style: { marginTop: 12, width: '100%' }, onClick: addServiceItem }, '添加一行')), React.createElement('div', { style: { padding: '12px 20px', borderTop: '1px solid #f0f0f0', textAlign: 'right', display: 'flex', gap: 12, justifyContent: 'flex-end' } }, React.createElement(Button, { type: 'primary', onClick: function() { closeServiceModal(); } }, '保存'), React.createElement(Button, { onClick: closeServiceModal }, '关闭')))) : null; + + var requirementContent = `车辆租赁合同-转正式合同(2026年3月3日版本) +「数字化资产ONE-OS运管平台」中的「车辆租赁合同」-「转正式合同」模块,在车辆租赁合同操作列点击「转正式合同」进行操作; + +1.面包屑: +#业务管理-车辆租赁合同-转正式合同 + +2.客户基本信息卡片: +#用于从客户列表中选择客户,并将该合同绑定到业务部门及业务负责人(绑定业务部门/业务负责人主要为了后期从部门/业务负责人维度进行数据统计); +2.1.客户名称:从原合同自动反写,不可编辑,如原客户信息发生改变则按照新信息进行反写;必选项,选择器,支持从输入框内输入内容进行模糊搜索,从客户信息列表中选择对应客户(只显示已通过审核的客户); +2.2.客户统一信用代码:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「信用代码」字段; +2.3.客户地址:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「客户地址」字段; +2.4.客户联系人:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「客户联系人」字段; +2.5.客户电话:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「客户电话」字段; +2.6.客户电子邮箱:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「客户电子邮箱」字段; +2.7.企业名称:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「企业名称」字段; +2.8.企业电话:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「企业电话」字段; +2.9.邮寄地址:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「邮寄地址」字段; +2.10.开户银行:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「开户银行」字段; +2.11.银行账号:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「银行账号」字段; +2.12.纳税人识别号:输入框(禁用状态),根据所选客户自动从客户信息列表中拉取对应客户-「纳税人识别号」字段; +2.13.业务部门:从该条合同自动反写,必选项,选择器,从部门表中选择该租赁合同对应部门; +2.14.业务负责人:从该条合同自动反写,必选项,选择器,从已选业务部门下拉取对应业务负责人,未选择业务部门时,业务负责人字段不可选; + +3.合同基本信息卡片: +#用于定义租赁合同基本情况和付款方式; +3.1.项目名称:从原合同自动反写,必填项,输入框,用于定义该合同项目名称,默认提示信息"请输入项目名称"; +3.2.合同类型:自动变更为正式合同,必选项,选择器,合同类型分为「正式合同」「试用合同」; +3.3.生效日期:从原合同自动反写,必选项,日期选择器,格式为YYYY-MM-DD,精确至天,默认为点击新增日期; +3.4.付款方式:从原合同自动反写,必选项,付款方式分为「预付」「后付」两种; + 3.4.1.如果选择预付,以对应「付款方式」和「付款周期」规则,在每一期新账单生成就列入业务待办(工作台功能),同时以消息通知对应用户; + 3.4.2.如果选择后付,以对应「付款方式」和「付款周期」规则,在每一期新账单生成时,将前一期账单列入业务待办(工作台功能),同时以消息通知对应用户; +3.5.主要车型:输入框(禁用状态),根据租赁订单信息中所有所选车型,自动反写入输入框并以标签形式显示,支持多车型显示,标签显示:型号名称; +3.6.结束日期:需要重新填写,必选项,日期选择器,格式为YYYY-MM-DD,精确至天; + 3.6.1.合同结束日期前30天将以消息提醒方式提醒(消息中心、工作台); + 3.6.2.到达合同结束日期时,租赁账单将会立刻停止计算,作为最后一期账单; + 3.6.3.续签合同/转正式合同将重新生成账单,不会对旧合同账单做任何继承处理; +3.7.付款周期:从原合同自动反写,必选项,选择器,支持1个月-12个月 12种付款周期,账单将以此周期和账单计算方式规则,从交车任务形成的交车单进行完整交车后,定时自动生成账单; +3.8.签约公司:从原合同自动反写,必选项,选择器,从组织机构表中获取所有根组织机构(如嘉兴羚牛、上海羚牛、广东羚牛等),默认显示新增用户当前机构,可手动修改,后期需要考虑从签约公司维度统计合同相关数据; +3.9.交车区域:从原合同自动反写,必选项,地区选择器,支持省-市2级,选择区域后,该任务生成交车任务时会自动推送至该区域负责运维人员; +3.10.交车地点:从原合同自动反写,必填项,输入框,支持自定义输入交车地点; +3.11.合同审批类型:从原合同自动反写,必填项,选择器,分为「标准合同审批」「非标准合同审批」; +3.12.合同原件:从原合同自动反写,必填项,按钮,按钮文字为:上传附件,支持多个附件上传(doc/docx/pdf格式); +3.13.备注:从原合同自动反写,如果该合同为转正式合同,则自动在已填备注信息上方额外添加:转正式合同自:旧合同合同编码xxx,文本域,支持自定义输入备注信息; + +4.被授权人信息卡片: +#用于定义租赁合同相关被授权人相关信息,被授权人在交车单完成时,需要选择被授权人,并通过被授权人手机短信,在E签宝进行签字确认; +4.1.被授权人:从原合同自动反写,必填项,输入框,用于输入被授权人信息; +4.2.被授权人联系电话:从原合同自动反写,必填项,输入框,用于输入被授权人联系电话,该电话后续需要接收E签宝签字链接; +4.3.被授权人身份证:从原合同自动反写,必填项,输入框,用于输入被授权人身份证信息; +4.4.支持通过新增/删除一行的方式,创建或管理多个授权人,后续交车单完成时可从多个授权人中选择接收授权人; + +5.租赁订单信息卡片: +#用于定义租赁合同对应车辆明细费用、氢费明细费用等相关信息; +5.1.上方为租赁车辆总计数据,包括租赁车辆数、租金及服务费合计、保证金总额、氢气预付款金额等相关信息; + 5.1.1.租赁车辆数:显示下方租赁订单信息包含多少辆车; + 5.1.2.租金及服务费合计:显示下方租赁订单信息中车辆租金总额及服务费总额; + 5.1.3.保证金总额:显示下方租赁订单信息中车辆保证金总额; + 5.1.4.氢气预付款金额:显示氢气预付款金额,如客户选择自行承担氢费或羚牛承担氢费,则该处为0不计入氢气预付款金额; +5.2.下方为列表,显示序号、品牌、型号、车牌号、车辆识别代码、车辆月租金(元)、服务费项目、服务费、保证金、备注,续签合同时,根据续签时的车辆信息填写(包括车牌号); + 5.2.1.序号:从原合同自动反写,自动按照条数生成,规则为1、2、3....以此类推; + 5.2.2.品牌:从原合同自动反写,必选项,选择器,从型号参数库中「品牌」字段拉取所有品牌; + 5.2.3.型号:从原合同自动反写,必选项,选择器,与品牌存在级联关系,未选择品牌则无法选择型号; + 5.2.4.车牌号:从原合同自动反写,选填项,选择器(支持从输入框输入车牌号关键字下拉匹配),可通过选择车牌号对品牌、型号进行反写; + 5.2.5.车辆识别代码:从原合同自动反写,输入框(禁用),显示该车辆对应车辆识别代码,根据所选车牌号从车辆表直接拉取进行反写; + 5.2.6.车辆月租金:从原合同自动反写,输入框,支持两位小数,用于输入该车辆月租金金额,输入框后缀为元,如还车时账单周期不满1个月,则按照:(车辆月租金/30)* 实际天数进行计算; + 5.2.7.服务费项目:从原合同自动反写,点击管理按钮弹出卡片,卡片标题为:服务项目,下方列表显示服务项目、费用、生效时间、操作; + 5.2.7.1.服务项目:从原合同自动反写,必选项,选择器(支持输入框输入服务项目关键字进行下拉匹配),选项包含:代处理费用、罚款、违章处理违约金、未参加安全培训、车辆出险、年检年审违约、停车费、设备损坏金(包含易损件)、清洗费、上门收车人工费、上门收车送车行驶费、上门收车基础服务费、保险上浮、保养费用、补办驾驶证、补办牌照、补办营运证、补办加氢证、借用备用钥匙、补配钥匙、租金、氢气费-客、退还车氢量差、能源费补缴、能源费退款、送车上门人工费、送车上门送车行驶费、送车上门基础服务费、保证金、氢气预付费、维修费用、ETC-客、ETC卡缺损费、ETC设备缺损费、电费-客、未结算保养费、未结算维修费、车损费、工具损坏或丢失费、证件费、广告损坏费、送车服务费、接车服务费、补办行驶证、超赔险、轮胎磨损费、无忧包、轮胎保、养护保、尾板; + 5.2.7.2.费用:从原合同自动反写,必填项,输入框,支持2位小数,输入框后缀为元; + 5.2.7.3.生效时间:从原合同自动反写,必选项,日期选择器,格式为YYYY-MM-DD; + 5.2.7.4.操作:删除,点击删除直接删除该行数据; + 5.2.7.5.新增一行数据:点击添加一行服务项目; + #在提车应收款中,车辆的交车日期和服务费生效日期可能会存在不同的情况,所以服务费需要单独以生效时间进行计算,例如: + 交车后车辆从1月1日开始计费,付款周期为2个月,则提车应收款租金会计算到3月1日,但是服务费生效时间为1月30日,此情况下需要以3月1日-1月30日,计算出服务费具体收费天数为30天,然后根据:(服务费费用/30)* 服务费收费天数30)进行计算; + 5.2.8.服务费:从原合同自动反写,自动根据添加的所有服务费项目计算总额,支持2位小数,格式为:xx.xx元; + 5.2.9.保证金:从原合同自动反写,必填项,输入框,支持2位小数,后缀为元,用于填写车辆需要支付的保证金金额,保证金为提车应收款一次性支付,还车时需要计入退还费用中; + 5.2.10.备注:选填项,输入框,用于备注车辆复杂情况; + 5.2.11.操作:删除,点击删除删除该行数据; + 5.2.12.添加一行:点击后列表新增一行,用于填写一条新的车辆租金费用信息; +5.3.氢费承担方:从原合同自动反写,必选项,选择器,选项为:「我方」、「客户」; + 5.3.1.选择「我方」:不显示付款方式、氢气预付款; + 5.3.2.选择「客户」:付款方式字段默认为预付,可手动修改; +5.4.付款方式:从原合同自动反写,必选项,选择器,选项为:「预付」、「月付款」、「自行结算」; + 5.4.1.预付:选择「预付」,需要填写:「氢气预付款」,氢气预付款指合同签署时客户就需预先付出的氢费款项,该部分款项会自动计入提车应收款中氢气预付款金额; + 5.4.2.月付款:选择「月付款」,提车应收款中不进行收费,而是由业务人员按照实际情况,通过氢费账单功能生成对应氢费账单,单独与客户进行结算; + 5.4.3.自行结算:选择「自行结算」,指合同签署后,所有氢气费用由客户自行承担; +5.5.氢气预付款:从原合同自动反写,选择「客户」「预付」时显示,必填项,输入框,支持2位小数,氢气预付款金额会计算入该合同交车应收款中,并计入5.1.4.氢气预付款金额中; +5.6.退还车氢气单价:从原合同自动反写,必填项,输入框,支持2位小数,后缀为元,不管氢费承担方和付款方式选择任何选项都需要进行维护,该金额主要用于与客户约定还车时,与交车时氢气差值以此费用进行自动计算和结算; + +6.其他费用信息卡片: +#用于选择对应租赁费用模板,选择后展示证照补办费用、违约金费用、易损件费用、其他费用等信息,租赁费用模板管理功能位于「车辆租赁合同」列表左上角; +6.1.选择费用模板:从原合同自动反写,如果费用模板有更新,则按照更新后内容显示,必选项,从「租赁费用模板」中拉取,选择后自动将该费用模板所有环节费用显示在合同中; + 6.1.1.证照补办费用:单独标题显示,内容为列表,列表中包括项目、收费标准、服务费,选择费用模板后自动反显; + 6.1.2.违约金费用:单独标题显示,内容为列表,列表中包含项目、收费标准、服务费,选择费用模板后自动反显; + 6.1.3.易损件信息:单独标题显示,内容为列表,列表中包含类别、损坏部位、配件、数量、费用明细,选择费用模板后自动反显; + 6.1.4.其他费用信息:单独标题显示,内容为列表,列表中包含项目、收费标准、服务费,选择费用模板后自动反显; + +7.账单计算方式卡片: +#必选项,从原合同自动反写,填充按钮组,可手动修改,用于定义租赁合同的账单计算方式,分为「按自然月结算」、「按付款周期天数结算」两种方式; +7.1.按付款周期天数结算:账单按照合同基本信息卡片中每隔付款周期*30天形成一期账单; + 例如付款周期为2个月,则交车任务交车成功时,提车应收款从交车任务配置的开始计费时间开始,根据60天收取车辆租金,此后每隔60天生成一期租赁账单; +7.2.按自然月结算:账单按照第一个月计费开始日期到当月最后一天为第一期,之后按照付款周期所选月份间隔,从开始月份第一天到间隔月份最后一天的自然月方式形成一期账单; + 例如付款周期为2个月,则交车任务交车成功时,提车应收款车辆租金需要收取≥2个月租金作为标准流程,<2个月租金作为非标流程; + 租赁账单首期从交车任务配置的开始计费时间开始,如付款周期为2个月,则首期账单结束时间为第二个月最后一天,付款周期为3个月,则首期账单结束时间为第三个月最后一天,此后每一期按照实际自然月开始-结束形成账单,例如付款周期为2个月,则第二期账单从2月1日-3月31日,以此类推; + +8.最下方为提交并审核、保存、取消三个按钮; +8.1.点击提交并审核,toast提示:租赁合同已提交审核。同时该租赁合同重新进入租赁合同审核列表中; +8.2.点击保存,会存储租赁订单已填写内容,不做必填项校验,同时显示在租赁合同列表中,该条数据只能保存人自己查看并编辑,其他人无法操作; +8.3.点击取消,如当前页面有已编辑内容时,点击取消会进行二次提示,内容为:取消将会丢失所有已填写内容,是否确认?点击确认返回车辆租赁合同列表页; + +所有卡片支持收起/展开功能,通过点击卡片右侧收起/展开实现;并增加锚点功能,锚点固定于页面右上角,点击锚点对应卡片名称,页面自动跳转至该卡片所在区域;`; + var reqSpecModalContent = reqSpecOpen ? React.createElement('div', { style: styles.modalMask, onClick: function(e) { if (e.target === e.currentTarget) setReqSpecOpen(false); } }, React.createElement('div', { style: Object.assign({}, styles.modalBox, { maxWidth: 720 }), onClick: function(e) { e.stopPropagation(); } }, React.createElement('div', { style: styles.modalHeader }, '需求说明'), React.createElement('div', { style: Object.assign({}, styles.modalBody, { maxHeight: '70vh', padding: '20px 24px' }) }, React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6 } }, requirementContent)), React.createElement('div', { style: { padding: '12px 20px', borderTop: '1px solid #f0f0f0', textAlign: 'right' } }, React.createElement(Button, { onClick: function() { setReqSpecOpen(false); } }, '关闭')))) : null; + + return React.createElement('div', { style: styles.page }, + React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 } }, React.createElement('div', { style: styles.breadcrumb }, React.createElement('span', null, '业务管理'), React.createElement('span', { style: styles.breadcrumbSep }, ' / '), React.createElement('span', null, '车辆租赁合同'), React.createElement('span', { style: styles.breadcrumbSep }, ' / '), React.createElement('span', { style: { color: '#1890ff' } }, '转正式合同')), React.createElement('span', { style: { color: '#1890ff', cursor: 'pointer', fontSize: 14 }, onClick: function() { setReqSpecOpen(true); } }, '查看需求说明')), + React.createElement('div', { style: styles.anchorWrap }, + React.createElement('div', { style: { marginBottom: 8, fontWeight: 600, fontSize: 14 } }, '锚点导航'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-customer'); } }, '客户基本信息'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-contract'); } }, '合同基本信息'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-authorized'); } }, '被授权人信息'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-rental'); } }, '租赁订单信息'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-fee'); } }, '其他费用信息'), + React.createElement('button', { style: styles.anchorItem, onClick: function() { scrollToCard('card-billing'); } }, '账单计算方式') + ), + React.createElement('div', { id: 'card-customer' }, React.createElement(CardBlock, { id: 'card-customer', title: '客户基本信息', collapsed: cc1, setCollapsed: setCc1 }, customerFields)), + React.createElement('div', { id: 'card-contract', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '合同基本信息', collapsed: cc2, setCollapsed: setCc2 }, React.createElement('div', null, contractFormRow1, contractFormRow4, contractFormRow5, contractFormRow2))), + React.createElement('div', { id: 'card-authorized', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '被授权人信息', collapsed: cc3, setCollapsed: setCc3 }, authorizedContent)), + React.createElement('div', { id: 'card-rental', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '租赁订单信息', collapsed: cc4, setCollapsed: setCc4 }, rentalContent)), + React.createElement('div', { id: 'card-fee', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '其他费用信息', collapsed: cc5, setCollapsed: setCc5 }, feeContent)), + React.createElement('div', { id: 'card-billing', style: { marginTop: 16 } }, React.createElement(CardBlock, { title: '账单计算方式', collapsed: cc6, setCollapsed: setCc6 }, billingContent)), + React.createElement('div', { style: { height: 60 } }), + serviceModalContent, + reqSpecModalContent, + cancelConfirmOpen ? React.createElement('div', { style: styles.modalMask, onClick: function(e) { if (e.target === e.currentTarget) setCancelConfirmOpen(false); } }, + React.createElement('div', { style: Object.assign({}, styles.modalBox, { maxWidth: 520 }), onClick: function(e) { e.stopPropagation(); } }, + React.createElement('div', { style: styles.modalHeader }, '提示'), + React.createElement('div', { style: Object.assign({}, styles.modalBody, { padding: '20px 24px' }) }, '取消将会丢失所有已填写内容,是否确认?'), + React.createElement('div', { style: { padding: '12px 20px', borderTop: '1px solid #f0f0f0', textAlign: 'right', display: 'flex', gap: 12, justifyContent: 'flex-end' } }, + React.createElement(Button, { onClick: function() { setCancelConfirmOpen(false); } }, '否'), + React.createElement(Button, { type: 'primary', onClick: function() { setCancelConfirmOpen(false); setEdited(false); message.info('已返回车辆租赁合同列表(原型)'); } }, '是') + ) + ) + ) : null, + React.createElement('div', { style: styles.footer }, + React.createElement(Button, { type: 'primary', onClick: function() { if (validateSubmitAndReview()) { setEdited(false); message.success('租赁合同已提交审核。同时该租赁合同重新进入租赁合同审核列表中。'); } } }, '提交并审核'), + React.createElement(Button, { onClick: function() { setEdited(false); message.success('已保存(不校验必填项),该条数据只能保存人自己查看并编辑,其他人无法操作。'); } }, '保存'), + React.createElement(Button, { onClick: function() { if (edited) setCancelConfirmOpen(true); else message.info('已返回车辆租赁合同列表(原型)'); } }, '取消') + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-lease-contract/pages/08-车辆租赁合同.jsx b/src/prototypes/oneos-web-lease-contract/pages/08-车辆租赁合同.jsx new file mode 100644 index 0000000..7960ea0 --- /dev/null +++ b/src/prototypes/oneos-web-lease-contract/pages/08-车辆租赁合同.jsx @@ -0,0 +1,1430 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 业务管理 - 车辆租赁合同(列表页,租赁合同管理) + +const Component = function() { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Select = antd.Select; + var Input = antd.Input; + var Button = antd.Button; + var Table = antd.Table; + var Space = antd.Space; + var Card = antd.Card; + var DatePicker = antd.DatePicker; + var Popover = antd.Popover; + var Tag = antd.Tag; + var Avatar = antd.Avatar; + var Dropdown = antd.Dropdown; + var Modal = antd.Modal; + var Upload = antd.Upload; + var message = antd.message; + var App = antd.App; + + var RangePicker = DatePicker.RangePicker; + + // 筛选展开(默认收起,只显示第一行 3 列) + var _filterExpanded = useState(false); + var _contractCode = useState(undefined); + var _projectName = useState(undefined); + var _customerName = useState(undefined); + var _signingCompany = useState(undefined); + var _approvalStatus = useState(['全部']); + var _contractStatus = useState(['全部']); + var _businessDept = useState([]); + var _businessOwner = useState([]); + var _contractType = useState(['全部']); + var _contractApprovalType = useState(['全部']); + var _creator = useState([]); + var _endDateRange = useState(null); + + var _appliedFilter = useState({ + contractCode: undefined, + projectName: undefined, + customerName: undefined, + signingCompany: undefined, + approvalStatus: ['全部'], + contractStatus: ['全部'], + businessDept: [], + businessOwner: [], + contractType: ['全部'], + contractApprovalType: ['全部'], + creator: [], + endDateRange: null + }); + + var _vehiclePopoverRecord = useState(null); + var _deliveredPopoverRecord = useState(null); + var _authorizedModalVisible = useState(false); + var _authorizedModalRecord = useState(null); + var _authorizedList = useState([{ name: '', phone: '', idCard: '' }]); + var _extraFeeModalVisible = useState(false); + var _extraFeeModalRecord = useState(null); + var _extraFeeList = useState([]); + var _deleteModalVisible = useState(false); + var _deleteModalRecord = useState(null); + var _withdrawModalVisible = useState(false); + var _withdrawModalRecord = useState(null); + var _terminateModalVisible = useState(false); + var _terminateModalRecord = useState(null); + var _requirementModalVisible = useState(false); + var _stampModalVisible = useState(false); + var _stampModalRecord = useState(null); + var _stampFileList = useState([]); + // 上传盖章合同完成后,按合同 id 记录已上传(原型:与列表 legalStampedContractUploaded 合并判断) + var _stampedUploadedOverride = useState({}); + + // 联调时:当前登录用户所属部门为法务部时为 true(原型默认 true 便于演示「上传盖章合同」) + var isLegalDeptUser = true; + + // 模拟选项(与新增租赁合同保持一致) + var contractCodeOptions = [ + { value: 'HT-ZL-2025-001', label: 'HT-ZL-2025-001' }, + { value: 'HT-ZL-2025-002', label: 'HT-ZL-2025-002' }, + { value: 'HT-ZL-2025-003', label: 'HT-ZL-2025-003' }, + { value: 'HT-ZL-2025-004', label: 'HT-ZL-2025-004' }, + { value: 'HT-ZL-2025-005', label: 'HT-ZL-2025-005' }, + { value: 'HT-ZL-2025-006', label: 'HT-ZL-2025-006' }, + { value: 'HT-ZL-2025-007', label: 'HT-ZL-2025-007' }, + { value: 'HT-ZL-2025-008', label: 'HT-ZL-2025-008' }, + { value: 'HT-ZL-2024-009', label: 'HT-ZL-2024-009' }, + { value: 'HT-ZL-2024-010', label: 'HT-ZL-2024-010' } + ]; + var projectNameOptions = [ + { value: 'p1', label: '嘉兴氢能示范项目' }, + { value: 'p2', label: '上海物流租赁项目' }, + { value: 'p3', label: '杭州城配租赁项目' } + ]; + var customerNameOptions = [ + { value: 'c1', label: '嘉兴某某物流有限公司' }, + { value: 'c2', label: '上海某某运输公司' }, + { value: 'c3', label: '杭州某某租赁有限公司' } + ]; + var signingCompanyOptions = [ + { value: '嘉兴羚牛', label: '嘉兴羚牛' }, + { value: '上海羚牛', label: '上海羚牛' }, + { value: '广东羚牛', label: '广东羚牛' } + ]; + // 审批状态:待审批、审批中、审批通过、审批驳回、未提交(无已结束,已结束归属合同状态) + var approvalStatusOptions = [ + { value: '全部', label: '全部' }, + { value: '待审批', label: '待审批' }, + { value: '审批中', label: '审批中' }, + { value: '审批通过', label: '审批通过' }, + { value: '审批驳回', label: '审批驳回' }, + { value: '未提交', label: '未提交' } + ]; + // 合同状态:草稿、变更、合同进行中、到期合同、已提交审批、已结束 + var contractStatusOptions = [ + { value: '全部', label: '全部' }, + { value: '草稿', label: '草稿' }, + { value: '变更', label: '变更' }, + { value: '合同进行中', label: '合同进行中' }, + { value: '到期合同', label: '到期合同' }, + { value: '已提交审批', label: '已提交审批' }, + { value: '已结束', label: '已结束' } + ]; + var contractTypeOptions = [ + { value: '全部', label: '全部' }, + { value: '正式合同', label: '正式合同' }, + { value: '试用合同', label: '试用合同' } + ]; + var contractApprovalTypeOptions = [ + { value: '全部', label: '全部' }, + { value: '标准合同审批', label: '标准合同审批' }, + { value: '非标准合同审批', label: '非标准合同审批' } + ]; + var deptOptions = [ + { value: '业务1部', label: '业务1部' }, + { value: '业务2部', label: '业务2部' }, + { value: '业务3部', label: '业务3部' } + ]; + var userOptions = [ + { value: '张经理', label: '张经理' }, + { value: '李专员', label: '李专员' }, + { value: '王专员', label: '王专员' }, + { value: '赵经理', label: '赵经理' }, + { value: '钱专员', label: '钱专员' } + ]; + + // 模拟列表数据:按审批状态 × 合同状态组合生成样例 + // 合同状态:草稿、变更、合同进行中、到期合同、已提交审批、已结束 + var rawList = [ + // 1. 未提交 + 草稿(仅保存未提交) + { + id: '1', + contractCode: 'HT-ZL-2025-001', + projectName: '嘉兴氢能示范项目', + vehicleCount: 2, + vehicles: [ + { vehicleType: '4.5吨冷链车-轻型厢式货车', brand: '品牌A', model: '型号A1', plateNo: '浙A12345', actualDelivery: '2025-01-10 09:00', deliveryPerson: '张运维' }, + { vehicleType: '18吨厢式货车-重型厢式货车', brand: '品牌B', model: '型号B1', plateNo: '-', actualDelivery: '2025-01-12 14:30', deliveryPerson: '李运维' } + ], + approvalStatus: '未提交', + contractStatus: '草稿', + customerName: '嘉兴某某物流有限公司', + signingCompany: '嘉兴羚牛', + businessDept: '业务1部', + businessOwner: '张经理', + contractType: '正式合同', + contractApprovalType: '标准合同审批', + contractEndDate: '2026-02-16', + contactName: '张三', + contactPhone: '13800138001', + creator: '张经理', + createTime: '2025-01-05 10:00', + updater: '-', + updateTime: '-', + remark: '草稿待完善', + legalStampedContractUploaded: undefined + }, + // 2. 未提交 + 草稿(试用合同) + { + id: '2', + contractCode: 'HT-ZL-2025-002', + projectName: '上海物流租赁项目', + vehicleCount: 1, + vehicles: [ + { vehicleType: '公务用车/小客车-小型普通客车', brand: '品牌C', model: '型号C1', plateNo: '沪D66666', actualDelivery: '2025-02-01 11:00', deliveryPerson: '王运维' } + ], + approvalStatus: '未提交', + contractStatus: '草稿', + customerName: '上海某某运输公司', + signingCompany: '上海羚牛', + businessDept: '业务2部', + businessOwner: '李专员', + contractType: '试用合同', + contractApprovalType: '非标准合同审批', + contractEndDate: '2025-08-01', + contactName: '李四', + contactPhone: '13800138002', + creator: '李专员', + createTime: '2025-02-10 09:00', + updater: '-', + updateTime: '-', + remark: '试用期 3 个月', + legalStampedContractUploaded: undefined + }, + // 3. 待审批 + 已提交审批(初次提交,尚未有任何节点审批) + { + id: '3', + contractCode: 'HT-ZL-2025-003', + projectName: '杭州城配租赁项目', + vehicleCount: 1, + vehicles: [ + { vehicleType: '4.5吨货车-轻型厢式货车', brand: '品牌A', model: '型号A2', plateNo: '浙B20002', actualDelivery: '2025-02-15 08:30', deliveryPerson: '赵运维' } + ], + approvalStatus: '待审批', + contractStatus: '已提交审批', + customerName: '杭州某某租赁有限公司', + signingCompany: '嘉兴羚牛', + businessDept: '业务3部', + businessOwner: '王专员', + contractType: '正式合同', + contractApprovalType: '标准合同审批', + contractEndDate: '2026-06-30', + contactName: '王五', + contactPhone: '13800138003', + creator: '王专员', + createTime: '2025-02-12 11:00', + updater: '-', + updateTime: '-', + remark: '-', + legalStampedContractUploaded: undefined + }, + // 4. 审批中 + 已提交审批(已有节点审批,未走完) + { + id: '4', + contractCode: 'HT-ZL-2025-004', + projectName: '宁波冷链运输项目', + vehicleCount: 2, + vehicles: [ + { vehicleType: '18吨双飞翼货车-重型厢式货车', brand: '品牌B', model: '型号B2', plateNo: '-', actualDelivery: '2025-02-16 10:00', deliveryPerson: '钱运维' }, + { vehicleType: '49吨牵引车头-重型半挂牵引车', brand: '品牌D', model: '型号D1', plateNo: '浙C30003', actualDelivery: '2025-02-18 14:00', deliveryPerson: '孙运维' } + ], + approvalStatus: '审批中', + contractStatus: '已提交审批', + customerName: '嘉兴某某物流有限公司', + signingCompany: '嘉兴羚牛', + businessDept: '业务1部', + businessOwner: '张经理', + contractType: '正式合同', + contractApprovalType: '非标准合同审批', + contractEndDate: '2026-03-01', + contactName: '赵六', + contactPhone: '13900139001', + creator: '张经理', + createTime: '2025-02-14 09:00', + updater: '李专员', + updateTime: '2025-02-15 16:00', + remark: '-', + legalStampedContractUploaded: undefined, + approvalFlowNodes: [ + { nodeTitle: '业务服务主管审批', result: 'passed', operatorName: '姚守涛', operatorTime: '2026-04-29 17:57:15' }, + { nodeTitle: '发起审批', result: 'passed', operatorName: '超级用户', operatorTime: '2026-04-28 17:44:45' }, + { nodeTitle: '业务负责人审批', result: 'pending', pendingApprovers: ['超级用户', '金可鹏'] } + ] + }, + // 5. 审批中 + 变更(已通过审批后做了变更并重新提交) + { + id: '5', + contractCode: 'HT-ZL-2025-005', + projectName: '苏州城配试点项目', + vehicleCount: 1, + vehicles: [ + { vehicleType: '重型平板半挂车-重型平板半挂车', brand: '品牌D', model: '型号D2', plateNo: '苏E50005', actualDelivery: '2025-02-20 09:00', deliveryPerson: '周运维' } + ], + approvalStatus: '审批中', + contractStatus: '变更', + customerName: '上海某某运输公司', + signingCompany: '上海羚牛', + businessDept: '业务2部', + businessOwner: '李专员', + contractType: '正式合同', + contractApprovalType: '标准合同审批', + contractEndDate: '2026-05-31', + contactName: '孙七', + contactPhone: '13900139002', + creator: '李专员', + createTime: '2025-02-18 10:00', + updater: '李专员', + updateTime: '2025-02-22 14:00', + remark: '变更车辆数量', + legalStampedContractUploaded: undefined, + approvalFlowNodes: [ + { nodeTitle: '法务审核', result: 'passed', operatorName: '李法务', operatorTime: '2025-02-21 18:00:00' }, + { nodeTitle: '发起审批', result: 'passed', operatorName: '李专员', operatorTime: '2025-02-22 10:00:00' }, + { nodeTitle: '业务负责人审批', result: 'pending', pendingApprovers: ['张经理', '赵总监'] } + ] + }, + // 6. 审批通过 + 合同进行中(正式合同) + { + id: '6', + contractCode: 'HT-ZL-2025-006', + projectName: '南京氢能示范项目', + vehicleCount: 3, + vehicles: [ + { vehicleType: '4.5吨冷链车-轻型厢式货车', brand: '品牌A', model: '型号A1', plateNo: '苏A60006', actualDelivery: '2025-01-20 08:00', deliveryPerson: '吴运维' }, + { vehicleType: '18吨厢式货车-重型厢式货车', brand: '品牌B', model: '型号B1', plateNo: '苏A60007', actualDelivery: '2025-01-21 10:00', deliveryPerson: '郑运维' }, + { vehicleType: '35吨牵引车头-重型半挂牵引车', brand: '品牌D', model: '型号D1', plateNo: '苏A60008', actualDelivery: '2025-01-22 14:00', deliveryPerson: '冯运维' } + ], + approvalStatus: '审批通过', + contractStatus: '合同进行中', + customerName: '杭州某某租赁有限公司', + signingCompany: '广东羚牛', + businessDept: '业务3部', + businessOwner: '王专员', + contractType: '正式合同', + contractApprovalType: '标准合同审批', + contractEndDate: '2026-12-31', + contactName: '周八', + contactPhone: '13900139003', + creator: '王专员', + createTime: '2025-01-15 09:00', + updater: '王专员', + updateTime: '2025-01-25 11:00', + remark: '-', + legalStampedContractUploaded: true + }, + // 7. 审批通过 + 合同进行中(试用合同,可转正式);法务未上传盖章件时「更多」显示上传盖章合同(演示仅此合同编码为未上传) + { + id: '7', + contractCode: 'HT-ZL-2025-007', + projectName: '无锡试用租赁项目', + vehicleCount: 1, + vehicles: [ + { vehicleType: '公务用车/小客车-小型普通客车', brand: '品牌C', model: '型号C2', plateNo: '苏B70007', actualDelivery: '2025-02-01 09:30', deliveryPerson: '陈运维' } + ], + approvalStatus: '审批通过', + contractStatus: '合同进行中', + customerName: '嘉兴某某物流有限公司', + signingCompany: '嘉兴羚牛', + businessDept: '业务1部', + businessOwner: '张经理', + contractType: '试用合同', + contractApprovalType: '非标准合同审批', + contractEndDate: '2025-05-31', + contactName: '吴九', + contactPhone: '13900139004', + creator: '张经理', + createTime: '2025-01-28 10:00', + updater: '-', + updateTime: '-', + remark: '试用 3 个月', + legalStampedContractUploaded: false + }, + // 8. 审批驳回 + 已提交审批(可编辑和重新提交) + { + id: '8', + contractCode: 'HT-ZL-2025-008', + projectName: '常州物流合作项目', + vehicleCount: 2, + vehicles: [ + { vehicleType: '重型集装箱半挂车-重型集装箱半挂车', brand: '品牌D', model: '型号D1', plateNo: '-', actualDelivery: '-' }, + { vehicleType: '4.5吨货车-轻型厢式货车', brand: '品牌A', model: '型号A2', plateNo: '-', actualDelivery: '-' } + ], + approvalStatus: '审批驳回', + contractStatus: '已提交审批', + customerName: '上海某某运输公司', + signingCompany: '上海羚牛', + businessDept: '业务2部', + businessOwner: '李专员', + contractType: '正式合同', + contractApprovalType: '标准合同审批', + contractEndDate: '2026-08-15', + contactName: '郑十', + contactPhone: '13900139005', + creator: '李专员', + createTime: '2025-02-20 14:00', + updater: '李专员', + updateTime: '2025-02-23 09:00', + remark: '驳回原因:费用条款需调整', + legalStampedContractUploaded: undefined + }, + // 9. 审批通过 + 到期合同(审批已通过但合同结束日期已过) + { + id: '9', + contractCode: 'HT-ZL-2024-009', + projectName: '南通去年到期项目', + vehicleCount: 2, + vehicles: [ + { vehicleType: '18吨双飞翼货车-重型厢式货车', brand: '品牌B', model: '型号B1', plateNo: '苏F90009', actualDelivery: '2024-03-01 09:00', deliveryPerson: '褚运维' }, + { vehicleType: '4.5吨冷链车-轻型厢式货车', brand: '品牌A', model: '型号A1', plateNo: '苏F90010', actualDelivery: '2024-03-02 10:00', deliveryPerson: '卫运维' } + ], + approvalStatus: '审批通过', + contractStatus: '到期合同', + customerName: '杭州某某租赁有限公司', + signingCompany: '嘉兴羚牛', + businessDept: '业务3部', + businessOwner: '王专员', + contractType: '正式合同', + contractApprovalType: '标准合同审批', + contractEndDate: '2024-12-31', + contactName: '王五', + contactPhone: '13800138003', + creator: '王专员', + createTime: '2024-02-20 10:00', + updater: '王专员', + updateTime: '2024-12-20 16:00', + remark: '已到期可续签', + legalStampedContractUploaded: true + }, + // 10. 审批通过 + 已结束(操作列终止合同并完成审核) + { + id: '10', + contractCode: 'HT-ZL-2024-010', + projectName: '镇江到期合同项目', + vehicleCount: 1, + vehicles: [ + { vehicleType: '公务用车/小客车-小型普通客车', brand: '品牌C', model: '型号C1', plateNo: '苏L00100', actualDelivery: '2024-06-01 11:00', deliveryPerson: '蒋运维' } + ], + approvalStatus: '审批通过', + contractStatus: '已结束', + customerName: '嘉兴某某物流有限公司', + signingCompany: '嘉兴羚牛', + businessDept: '业务1部', + businessOwner: '张经理', + contractType: '正式合同', + contractApprovalType: '非标准合同审批', + contractEndDate: '2025-01-15', + contactName: '张三', + contactPhone: '13800138001', + creator: '张经理', + createTime: '2024-05-20 09:00', + updater: '张经理', + updateTime: '2025-01-10 14:00', + remark: '-', + legalStampedContractUploaded: true + } + ]; + + var appliedFilter = _appliedFilter[0]; + var filteredList = useMemo(function() { + var list = rawList.slice(); + var f = appliedFilter; + if (f.contractCode) { + list = list.filter(function(r) { + return (r.contractCode || '').indexOf(f.contractCode) !== -1; + }); + } + if (f.projectName) { + list = list.filter(function(r) { return r.projectName === f.projectName; }); + } + if (f.customerName) { + list = list.filter(function(r) { return r.customerName === f.customerName; }); + } + if (f.signingCompany) { + list = list.filter(function(r) { return r.signingCompany === f.signingCompany; }); + } + var approval = f.approvalStatus; + if (approval && approval.length > 0 && approval.indexOf('全部') === -1) { + list = list.filter(function(r) { return approval.indexOf(r.approvalStatus) !== -1; }); + } + var status = f.contractStatus; + if (status && status.length > 0 && status.indexOf('全部') === -1) { + list = list.filter(function(r) { return status.indexOf(r.contractStatus) !== -1; }); + } + if (f.businessDept && f.businessDept.length > 0) { + list = list.filter(function(r) { return f.businessDept.indexOf(r.businessDept) !== -1; }); + } + if (f.businessOwner && f.businessOwner.length > 0) { + list = list.filter(function(r) { return f.businessOwner.indexOf(r.businessOwner) !== -1; }); + } + var type = f.contractType; + if (type && type.length > 0 && type.indexOf('全部') === -1) { + list = list.filter(function(r) { return type.indexOf(r.contractType) !== -1; }); + } + var capType = f.contractApprovalType; + if (capType && capType.length > 0 && capType.indexOf('全部') === -1) { + list = list.filter(function(r) { return capType.indexOf(r.contractApprovalType) !== -1; }); + } + if (f.creator && f.creator.length > 0) { + list = list.filter(function(r) { return f.creator.indexOf(r.creator) !== -1; }); + } + if (f.endDateRange && f.endDateRange.length === 2) { + var start = ''; + var end = ''; + if (f.endDateRange[0] && f.endDateRange[0].format) { + start = f.endDateRange[0].format('YYYY-MM-DD'); + } + if (f.endDateRange[1] && f.endDateRange[1].format) { + end = f.endDateRange[1].format('YYYY-MM-DD'); + } + if (start || end) { + list = list.filter(function(r) { + var d = r.contractEndDate || ''; + if (start && d < start) return false; + if (end && d > end) return false; + return true; + }); + } + } + return list; + }, [rawList, appliedFilter]); + + var handleQuery = useCallback(function() { + _appliedFilter[1]({ + contractCode: _contractCode[0], + projectName: _projectName[0], + customerName: _customerName[0], + signingCompany: _signingCompany[0], + approvalStatus: _approvalStatus[0] ? _approvalStatus[0].slice() : ['全部'], + contractStatus: _contractStatus[0] ? _contractStatus[0].slice() : ['全部'], + businessDept: _businessDept[0] ? _businessDept[0].slice() : [], + businessOwner: _businessOwner[0] ? _businessOwner[0].slice() : [], + contractType: _contractType[0] ? _contractType[0].slice() : ['全部'], + contractApprovalType: _contractApprovalType[0] ? _contractApprovalType[0].slice() : ['全部'], + creator: _creator[0] ? _creator[0].slice() : [], + endDateRange: _endDateRange[0] + }); + }, []); + + var handleReset = useCallback(function() { + _contractCode[1](undefined); + _projectName[1](undefined); + _customerName[1](undefined); + _signingCompany[1](undefined); + _approvalStatus[1](['全部']); + _contractStatus[1](['全部']); + _businessDept[1]([]); + _businessOwner[1]([]); + _contractType[1](['全部']); + _contractApprovalType[1](['全部']); + _creator[1]([]); + _endDateRange[1](null); + _appliedFilter[1]({ + contractCode: undefined, + projectName: undefined, + customerName: undefined, + signingCompany: undefined, + approvalStatus: ['全部'], + contractStatus: ['全部'], + businessDept: [], + businessOwner: [], + contractType: ['全部'], + contractApprovalType: ['全部'], + creator: [], + endDateRange: null + }); + }, []); + + // 审批状态/合同状态/合同类型:「全部」与其它选项互斥,不能同时多选 + var handleApprovalStatusChange = useCallback(function(v) { + if (!v || v.length === 0) { _approvalStatus[1](['全部']); return; } + if (v.indexOf('全部') !== -1 && v.length > 1) { + var prev = _approvalStatus[0] || []; + var hadAllOnly = prev.length === 1 && prev.indexOf('全部') !== -1; + if (hadAllOnly) { + var next = []; + for (var i = 0; i < v.length; i++) { if (v[i] !== '全部') next.push(v[i]); } + _approvalStatus[1](next); + } else { + _approvalStatus[1](['全部']); + } + return; + } + _approvalStatus[1](v); + }, []); + var handleContractStatusChange = useCallback(function(v) { + if (!v || v.length === 0) { _contractStatus[1](['全部']); return; } + if (v.indexOf('全部') !== -1 && v.length > 1) { + var prev = _contractStatus[0] || []; + var hadAllOnly = prev.length === 1 && prev.indexOf('全部') !== -1; + if (hadAllOnly) { + var next = []; + for (var j = 0; j < v.length; j++) { if (v[j] !== '全部') next.push(v[j]); } + _contractStatus[1](next); + } else { + _contractStatus[1](['全部']); + } + return; + } + _contractStatus[1](v); + }, []); + var handleContractTypeChange = useCallback(function(v) { + if (!v || v.length === 0) { _contractType[1](['全部']); return; } + if (v.indexOf('全部') !== -1 && v.length > 1) { + var prev = _contractType[0] || []; + var hadAllOnly = prev.length === 1 && prev.indexOf('全部') !== -1; + if (hadAllOnly) { + var next = []; + for (var k = 0; k < v.length; k++) { if (v[k] !== '全部') next.push(v[k]); } + _contractType[1](next); + } else { + _contractType[1](['全部']); + } + return; + } + _contractType[1](v); + }, []); + var handleContractApprovalTypeChange = useCallback(function(v) { + if (!v || v.length === 0) { _contractApprovalType[1](['全部']); return; } + if (v.indexOf('全部') !== -1 && v.length > 1) { + var prevA = _contractApprovalType[0] || []; + var hadAllOnlyA = prevA.length === 1 && prevA.indexOf('全部') !== -1; + if (hadAllOnlyA) { + var nextA = []; + for (var ia = 0; ia < v.length; ia++) { if (v[ia] !== '全部') nextA.push(v[ia]); } + _contractApprovalType[1](nextA); + } else { + _contractApprovalType[1](['全部']); + } + return; + } + _contractApprovalType[1](v); + }, []); + + var addAuthorizedRow = useCallback(function() { + _authorizedList[1](function(prev) { return prev.concat([{ name: '', phone: '', idCard: '' }]); }); + }, []); + var removeAuthorizedRow = useCallback(function(index) { + _authorizedList[1](function(prev) { + var list = prev.slice(); + list.splice(index, 1); + if (list.length === 0) list = [{ name: '', phone: '', idCard: '' }]; + return list; + }); + }, []); + var updateAuthorizedRow = useCallback(function(index, field, value) { + _authorizedList[1](function(prev) { + var list = prev.slice(); + var row = list[index] || { name: '', phone: '', idCard: '' }; + var next = {}; + next[field] = value; + list[index] = Object.assign({}, row, next); + return list; + }); + }, []); + + // 附加费用:服务项目枚举(与需求文档一致) + var extraFeeServiceOptions = [ + { value: '代处理费用', label: '代处理费用' }, { value: '罚款', label: '罚款' }, { value: '违章处理违约金', label: '违章处理违约金' }, { value: '未参加安全培训', label: '未参加安全培训' }, { value: '车辆出险', label: '车辆出险' }, { value: '年检年审违约', label: '年检年审违约' }, { value: '停车费', label: '停车费' }, { value: '设备损坏金(包含易损件)', label: '设备损坏金(包含易损件)' }, { value: '清洗费', label: '清洗费' }, { value: '上门收车人工费', label: '上门收车人工费' }, { value: '上门收车送车行驶费', label: '上门收车送车行驶费' }, { value: '上门收车基础服务费', label: '上门收车基础服务费' }, { value: '保险上浮', label: '保险上浮' }, { value: '保养费用', label: '保养费用' }, { value: '补办驾驶证', label: '补办驾驶证' }, { value: '补办牌照', label: '补办牌照' }, { value: '补办营运证', label: '补办营运证' }, { value: '补办加氢证', label: '补办加氢证' }, { value: '借用备用钥匙', label: '借用备用钥匙' }, { value: '补配钥匙', label: '补配钥匙' }, { value: '租金', label: '租金' }, { value: '氢气费-客', label: '氢气费-客' }, { value: '退还车氢量差', label: '退还车氢量差' }, { value: '能源费补缴', label: '能源费补缴' }, { value: '能源费退款', label: '能源费退款' }, { value: '送车上门人工费', label: '送车上门人工费' }, { value: '送车上门送车行驶费', label: '送车上门送车行驶费' }, { value: '送车上门基础服务费', label: '送车上门基础服务费' }, { value: '保证金', label: '保证金' }, { value: '氢气预付费', label: '氢气预付费' }, { value: '维修费用', label: '维修费用' }, { value: 'ETC-客', label: 'ETC-客' }, { value: 'ETC卡缺损费', label: 'ETC卡缺损费' }, { value: 'ETC设备缺损费', label: 'ETC设备缺损费' }, { value: '电费-客', label: '电费-客' }, { value: '未结算保养费', label: '未结算保养费' }, { value: '未结算维修费', label: '未结算维修费' }, { value: '车损费', label: '车损费' }, { value: '工具损坏或丢失费', label: '工具损坏或丢失费' }, { value: '证件费', label: '证件费' }, { value: '广告损坏费', label: '广告损坏费' }, { value: '送车服务费', label: '送车服务费' }, { value: '接车服务费', label: '接车服务费' }, { value: '补办行驶证', label: '补办行驶证' }, { value: '超赔险', label: '超赔险' }, { value: '轮胎磨损费', label: '轮胎磨损费' }, { value: '无忧包', label: '无忧包' }, { value: '轮胎保', label: '轮胎保' }, { value: '养护保', label: '养护保' }, { value: '尾板', label: '尾板' } + ]; + var updateExtraFeeRow = useCallback(function(index, field, value) { + _extraFeeList[1](function(prev) { + var list = prev.slice(); + var row = list[index] || {}; + var next = {}; + next[field] = value; + list[index] = Object.assign({}, row, next); + return list; + }); + }, []); + + var filterLabelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var filterItemStyle = { marginBottom: 12 }; + var filterControlStyle = { width: '100%' }; + var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var tableSingleLineStyle = '.contract-list-table .ant-table-thead th,.contract-list-table .ant-table-tbody td{white-space:nowrap;}'; + + // 租赁车辆数气泡:不含实际交车日期 + var vehicleColumnsRental = [ + { title: '车辆类型', dataIndex: 'vehicleType', key: 'vehicleType', width: 180 }, + { title: '品牌', dataIndex: 'brand', key: 'brand', width: 80 }, + { title: '型号', dataIndex: 'model', key: 'model', width: 100 }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 100 } + ]; + // 已交车辆数气泡:含实际交车日期,后方增加交车人 + var vehicleColumnsDelivered = [ + { title: '车辆类型', dataIndex: 'vehicleType', key: 'vehicleType', width: 180 }, + { title: '品牌', dataIndex: 'brand', key: 'brand', width: 80 }, + { title: '型号', dataIndex: 'model', key: 'model', width: 100 }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 100 }, + { title: '实际交车日期', dataIndex: 'actualDelivery', key: 'actualDelivery', width: 140 }, + { title: '交车人', dataIndex: 'deliveryPerson', key: 'deliveryPerson', width: 100 } + ]; + + function approvalAvatarText(name) { + if (!name) return '?'; + var s = String(name); + return s.length <= 2 ? s : s.slice(-2); + } + + function renderApprovalFlowContent(nodes) { + var list = nodes && nodes.length ? nodes : []; + if (!list.length) { + return React.createElement('div', { style: { color: 'rgba(0,0,0,0.45)', fontSize: 13, padding: '4px 0' } }, '暂无审批节点明细'); + } + return React.createElement( + 'div', + { style: { maxWidth: 360, paddingTop: 4 } }, + list.map(function(node, index) { + var isLast = index === list.length - 1; + var isPending = node.result === 'pending'; + var dot = React.createElement( + Avatar, + { + size: 28, + style: { + backgroundColor: isPending ? '#1890ff' : '#f0f0f0', + color: isPending ? '#fff' : 'rgba(0,0,0,0.45)', + fontSize: 11, + lineHeight: '28px', + flexShrink: 0 + }, + children: isPending + ? React.createElement( + 'svg', + { viewBox: '0 0 24 24', width: 14, height: 14, fill: 'currentColor', style: { verticalAlign: 'middle' } }, + React.createElement('path', { + d: 'M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4zm5-9h2v2h-2V5zm0 4h2v2h-2V9z' + }) + ) + : approvalAvatarText(node.operatorName) + } + ); + var body = React.createElement( + 'div', + { style: { flex: 1, paddingLeft: 12, paddingBottom: isLast ? 0 : 14, minWidth: 0 } }, + React.createElement( + 'div', + { style: { display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 8, marginBottom: 6 } }, + React.createElement('span', { style: { fontSize: 14, color: 'rgba(0,0,0,0.88)', fontWeight: 500 } }, node.nodeTitle || '-'), + React.createElement(Tag, { color: isPending ? 'processing' : 'success', style: { margin: 0 } }, isPending ? '待审核' : '通过') + ), + isPending && node.pendingApprovers && node.pendingApprovers.length + ? React.createElement( + Space, + { size: [8, 8], wrap: true }, + node.pendingApprovers.map(function(apName, i) { + return React.createElement( + Tag, + { + key: i, + style: { + margin: 0, + color: '#1890ff', + background: '#e6f7ff', + borderColor: '#91d5ff' + } + }, + React.createElement( + 'span', + null, + React.createElement( + 'svg', + { + viewBox: '0 0 24 24', + width: 12, + height: 12, + fill: '#1890ff', + style: { marginRight: 4, verticalAlign: '-2px' } + }, + React.createElement('path', { + d: 'M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z' + }) + ), + apName + ) + ); + }) + ) + : React.createElement( + 'div', + { style: { fontSize: 13, color: 'rgba(0,0,0,0.45)' } }, + (node.operatorName || '') + (node.operatorTime ? ' ' + node.operatorTime : '') + ) + ); + return React.createElement( + 'div', + { key: index, style: { display: 'flex', alignItems: 'stretch' } }, + React.createElement( + 'div', + { + style: { + width: 36, + flexShrink: 0, + display: 'flex', + flexDirection: 'column', + alignItems: 'center' + } + }, + React.createElement('div', { style: { lineHeight: 0 } }, dot), + isLast + ? null + : React.createElement('div', { + style: { + flex: 1, + width: 2, + minHeight: 12, + marginTop: 4, + background: '#f0f0f0', + borderRadius: 1 + } + }) + ), + body + ); + }) + ); + } + + function getMoreMenuItems(record) { + var status = record.contractStatus; + var type = record.contractType; + var approval = record.approvalStatus; + var items = []; + if (status === '草稿') { + items.push({ key: 'edit', label: '编辑', onClick: function() { message.info('编辑(原型)'); } }); + items.push({ key: 'del', label: '删除合同', danger: true, onClick: function() { _deleteModalRecord[1](record); _deleteModalVisible[1](true); } }); + } + if (status === '合同进行中') { + items.push({ key: 'addVehicle', label: '新增车辆', onClick: function() { message.info('新增车辆(原型)'); } }); + items.push({ key: 'renew', label: '续签合同', onClick: function() { message.info('续签合同(原型)'); } }); + items.push({ key: 'authorized', label: '添加被授权人', onClick: function() { _authorizedModalRecord[1](record); _authorizedList[1]([{ name: '', phone: '', idCard: '' }]); _authorizedModalVisible[1](true); } }); + items.push({ key: 'extraFee', label: '附加费用', onClick: function() { + _extraFeeModalRecord[1](record); + var vehicles = record.vehicles || []; + _extraFeeList[1](vehicles.map(function(v) { + return { + vehicleType: v.vehicleType || '-', + brand: v.brand || '-', + model: v.model || '-', + plateNo: v.plateNo || '-', + serviceItem: undefined, + fee: '', + effectiveDate: null + }; + })); + _extraFeeModalVisible[1](true); + } }); + items.push({ key: 'toTripartite', label: '变更为三方合同', onClick: function() { message.info('变更为三方合同(原型)'); } }); + items.push({ key: 'terminate', label: '终止合同', danger: true, onClick: function() { _terminateModalRecord[1](record); _terminateModalVisible[1](true); } }); + } + if (status === '到期合同') { + items.push({ key: 'renew2', label: '续签合同', onClick: function() { message.info('续签合同(原型)'); } }); + } + if (status === '已提交审批' && approval !== '审批驳回') { + items.push({ key: 'withdraw', label: '撤回合同', onClick: function() { _withdrawModalRecord[1](record); _withdrawModalVisible[1](true); } }); + } + if (approval === '审批驳回') { + items.push({ key: 'editReject', label: '编辑', onClick: function() { message.info('编辑(原型)'); } }); + } + if (type === '试用合同') { + items.push({ key: 'toFormal', label: '转正式合同', onClick: function() { message.info('转正式合同(原型)'); } }); + } + // 审批通过且法务审核环节尚未上传盖章合同附件时,法务部员工可在「更多」中上传(上传完成后入口关闭) + if (approval === '审批通过' && isLegalDeptUser) { + var needStampUpload = (record.legalStampedContractUploaded === false) && !(_stampedUploadedOverride[0][record.id] === true); + if (needStampUpload) { + items.push({ + key: 'uploadStamped', + label: '上传盖章合同', + onClick: function() { + _stampModalRecord[1](record); + _stampFileList[1]([]); + _stampModalVisible[1](true); + } + }); + } + } + return items; + } + + function getOperationButtons(record) { + var menuItems = getMoreMenuItems(record); + var moreDropdown = menuItems.length > 0 + ? React.createElement( + Dropdown, + { + menu: { items: menuItems }, + trigger: ['click'] + }, + React.createElement(Button, { type: 'link', size: 'small' }, '更多') + ) + : null; + return React.createElement( + Space, + { size: 4, wrap: true }, + React.createElement(Button, { type: 'link', size: 'small', onClick: function() { message.info('查看(跳转车辆租赁合同-查看)'); } }, '查看'), + moreDropdown + ); + } + + var columns = [ + { title: '合同编码', dataIndex: 'contractCode', key: 'contractCode', width: 140, fixed: 'left' }, + { title: '项目名称', dataIndex: 'projectName', key: 'projectName', width: 140, fixed: 'left' }, + { + title: '租赁车辆数', + key: 'vehicleCount', + width: 100, + render: function(_, record) { + var open = _vehiclePopoverRecord[0] && _vehiclePopoverRecord[0].id === record.id; + var content = React.createElement('div', { style: { padding: 8 } }, + React.createElement(Table, { + size: 'small', + rowKey: function(r, i) { return String(i); }, + columns: vehicleColumnsRental, + dataSource: record.vehicles || [], + pagination: false, + scroll: { x: 460 } + }) + ); + return React.createElement( + Popover, + { + content: content, + title: '车辆明细', + open: open, + onOpenChange: function(visible) { + if (!visible) _vehiclePopoverRecord[1](null); + else _vehiclePopoverRecord[1](record); + }, + trigger: 'click' + }, + React.createElement('a', { style: { cursor: 'pointer', color: '#1890ff', fontWeight: 500 } }, record.vehicleCount) + ); + } + }, + { + title: '已交车辆数', + key: 'deliveredCount', + width: 100, + render: function(_, record) { + var deliveredVehicles = (record.vehicles || []).filter(function(v) { + var d = v.actualDelivery; + return d && String(d).trim() && d !== '-'; + }); + var deliveredCount = deliveredVehicles.length; + var open = _deliveredPopoverRecord[0] && _deliveredPopoverRecord[0].id === record.id; + var content = React.createElement('div', { style: { padding: 8 } }, + React.createElement(Table, { + size: 'small', + rowKey: function(r, i) { return String(i); }, + columns: vehicleColumnsDelivered, + dataSource: deliveredVehicles, + pagination: false, + scroll: { x: 700 } + }) + ); + return React.createElement( + Popover, + { + content: content, + title: '已交车列表', + open: open, + onOpenChange: function(visible) { + if (!visible) _deliveredPopoverRecord[1](null); + else _deliveredPopoverRecord[1](record); + }, + trigger: 'click' + }, + React.createElement('a', { style: { cursor: 'pointer', color: '#1890ff', fontWeight: 500 } }, deliveredCount) + ); + } + }, + { + title: '审批状态', + dataIndex: 'approvalStatus', + key: 'approvalStatus', + width: 100, + render: function(text, record) { + if (text !== '审批中') return text; + var nodes = record.approvalFlowNodes; + return React.createElement( + Popover, + { + content: renderApprovalFlowContent(nodes), + trigger: 'hover', + placement: 'rightTop', + overlayInnerStyle: { maxWidth: 400 }, + mouseEnterDelay: 0.15 + }, + React.createElement( + 'span', + { + style: { + cursor: 'help', + borderBottom: '1px dashed rgba(0,0,0,0.35)', + color: 'rgba(0,0,0,0.88)' + } + }, + text + ) + ); + } + }, + { title: '合同状态', dataIndex: 'contractStatus', key: 'contractStatus', width: 110 }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 140 }, + { title: '签约公司', dataIndex: 'signingCompany', key: 'signingCompany', width: 100 }, + { title: '业务部门', dataIndex: 'businessDept', key: 'businessDept', width: 100 }, + { title: '业务负责人', dataIndex: 'businessOwner', key: 'businessOwner', width: 100 }, + { title: '合同类型', dataIndex: 'contractType', key: 'contractType', width: 100 }, + { title: '合同审批类型', dataIndex: 'contractApprovalType', key: 'contractApprovalType', width: 130 }, + { title: '合同结束日期', dataIndex: 'contractEndDate', key: 'contractEndDate', width: 120 }, + { title: '客户联系人', dataIndex: 'contactName', key: 'contactName', width: 100 }, + { title: '联系电话', dataIndex: 'contactPhone', key: 'contactPhone', width: 120 }, + { title: '创建人', dataIndex: 'creator', key: 'creator', width: 90 }, + { title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 150 }, + { title: '更新人', dataIndex: 'updater', key: 'updater', width: 90 }, + { title: '最后更新时间', dataIndex: 'updateTime', key: 'updateTime', width: 150 }, + { title: '备注', dataIndex: 'remark', key: 'remark', width: 100, ellipsis: true }, + { title: '操作', key: 'action', width: 140, fixed: 'right', render: function(_, record) { return getOperationButtons(record); } } + ]; + + var filterItems = [ + React.createElement('div', { key: 'contractCode', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '合同编码'), + React.createElement(Select, { + placeholder: '请选择或输入合同编码', + style: filterControlStyle, + value: _contractCode[0], + onChange: function(v) { _contractCode[1](v); }, + allowClear: true, + showSearch: true, + options: contractCodeOptions, + filterOption: function(input, opt) { + return (opt.label || '').toString().toLowerCase().indexOf((input || '').toLowerCase()) !== -1; + } + })), + React.createElement('div', { key: 'projectName', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '项目名称'), + React.createElement(Select, { + placeholder: '请选择或输入项目名称', + style: filterControlStyle, + value: _projectName[0], + onChange: function(v) { _projectName[1](v); }, + allowClear: true, + showSearch: true, + options: projectNameOptions, + filterOption: function(input, opt) { + return (opt.label || '').toString().toLowerCase().indexOf((input || '').toLowerCase()) !== -1; + } + })), + React.createElement('div', { key: 'customerName', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '客户名称'), + React.createElement(Select, { + placeholder: '请选择或输入客户名称', + style: filterControlStyle, + value: _customerName[0], + onChange: function(v) { _customerName[1](v); }, + allowClear: true, + showSearch: true, + options: customerNameOptions, + filterOption: function(input, opt) { + return (opt.label || '').toString().toLowerCase().indexOf((input || '').toLowerCase()) !== -1; + } + })), + React.createElement('div', { key: 'signingCompany', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '签约公司'), + React.createElement(Select, { + placeholder: '请选择或输入签约公司名称', + style: filterControlStyle, + value: _signingCompany[0], + onChange: function(v) { _signingCompany[1](v); }, + allowClear: true, + options: signingCompanyOptions + })), + React.createElement('div', { key: 'approvalStatus', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '审批状态'), + React.createElement(Select, { + mode: 'multiple', + placeholder: '请选择', + style: filterControlStyle, + value: _approvalStatus[0], + onChange: handleApprovalStatusChange, + options: approvalStatusOptions + })), + React.createElement('div', { key: 'contractStatus', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '合同状态'), + React.createElement(Select, { + mode: 'multiple', + placeholder: '请选择', + style: filterControlStyle, + value: _contractStatus[0], + onChange: handleContractStatusChange, + options: contractStatusOptions + })), + React.createElement('div', { key: 'businessDept', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '业务部门'), + React.createElement(Select, { + mode: 'multiple', + placeholder: '请选择或输入业务部门名称', + style: filterControlStyle, + value: _businessDept[0], + onChange: function(v) { _businessDept[1](v); }, + options: deptOptions + })), + React.createElement('div', { key: 'businessOwner', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '业务负责人'), + React.createElement(Select, { + mode: 'multiple', + placeholder: '请选择或输入业务负责人姓名', + style: filterControlStyle, + value: _businessOwner[0], + onChange: function(v) { _businessOwner[1](v); }, + options: userOptions + })), + React.createElement('div', { key: 'contractType', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '合同类型'), + React.createElement(Select, { + mode: 'multiple', + placeholder: '请选择', + style: filterControlStyle, + value: _contractType[0], + onChange: handleContractTypeChange, + options: contractTypeOptions + })), + React.createElement('div', { key: 'contractApprovalType', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '合同审批类型'), + React.createElement(Select, { + mode: 'multiple', + placeholder: '请选择', + style: filterControlStyle, + value: _contractApprovalType[0], + onChange: handleContractApprovalTypeChange, + options: contractApprovalTypeOptions + })), + React.createElement('div', { key: 'creator', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '创建人'), + React.createElement(Select, { + mode: 'multiple', + placeholder: '请选择或输入创建人姓名', + style: filterControlStyle, + value: _creator[0], + onChange: function(v) { _creator[1](v); }, + options: userOptions + })), + React.createElement('div', { key: 'endDate', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '合同结束日期'), + React.createElement(RangePicker, { + style: filterControlStyle, + placeholder: ['开始日期', '结束日期'], + value: _endDateRange[0], + onChange: function(v) { _endDateRange[1](v); } + })) + ]; + + var filterCount = _filterExpanded[0] ? 12 : 3; + var filterNodes = []; + for (var i = 0; i < filterCount && i < filterItems.length; i++) { + filterNodes.push(filterItems[i]); + } + + var reqTitleStyle = { fontSize: 18, fontWeight: 600, marginBottom: 16, color: 'rgba(0,0,0,0.85)' }; + var reqSectionStyle = { fontSize: 15, fontWeight: 600, marginTop: 16, marginBottom: 8, color: 'rgba(0,0,0,0.85)' }; + var reqSubStyle = { fontSize: 14, fontWeight: 500, marginLeft: 16, marginTop: 8, marginBottom: 4, color: 'rgba(0,0,0,0.85)' }; + var reqItemStyle = { fontSize: 13, marginLeft: 32, marginTop: 4, marginBottom: 2, lineHeight: 1.6, color: 'rgba(0,0,0,0.75)' }; + var reqSubItemStyle = { fontSize: 13, marginLeft: 48, marginTop: 2, marginBottom: 2, lineHeight: 1.6, color: 'rgba(0,0,0,0.7)' }; + + return React.createElement(App, null, + React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + React.createElement(Breadcrumb, { + items: [ + { title: '业务管理' }, + { title: '车辆租赁合同' } + ] + }), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function() { _requirementModalVisible[1](true); } }, '查看需求说明') + ), + React.createElement(Card, { style: { marginBottom: 16 } }, + React.createElement('div', { + style: { + display: 'grid', + gridTemplateColumns: '1fr 1fr 1fr', + gap: '16px 24px', + alignItems: 'start', + flex: 1, + minWidth: 0 + } + }, filterNodes), + React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 } }, + React.createElement(Button, { onClick: handleReset }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleQuery }, '查询'), + React.createElement(Button, { type: 'link', size: 'small', onClick: function() { _filterExpanded[1](!_filterExpanded[0]); } }, _filterExpanded[0] ? '收起' : '展开') + ) + ), + React.createElement('div', { style: { marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 8 } }, + React.createElement(Button, { onClick: function() { message.info('租赁费用模板(原型)'); } }, '租赁费用模板'), + React.createElement('div', { style: { display: 'flex', gap: 8 } }, + React.createElement(Button, { type: 'primary', onClick: function() { message.info('请在原型菜单列表中点击:业务管理-车辆租赁合同-新建合同 查看具体原型'); } }, '新增'), + React.createElement(Button, { onClick: function() { message.info('根据筛选条件,导出相应记录'); } }, '导出') + ) + ), + React.createElement(Card, null, + React.createElement(React.Fragment, null, + React.createElement('style', null, tableSingleLineStyle), + React.createElement('div', { className: 'contract-list-table' }, + React.createElement(Table, { + rowKey: 'id', + columns: columns, + dataSource: filteredList, + scroll: { x: 2400 }, + size: 'small', + pagination: { showSizeChanger: true, showQuickJumper: true, showTotal: function(t) { return '共 ' + t + ' 条'; } } + }) + ) + ) + ), + React.createElement(Modal, { + title: '是否确认删除该合同草稿', + open: _deleteModalVisible[0], + onCancel: function() { _deleteModalVisible[1](false); _deleteModalRecord[1](null); }, + onOk: function() { + message.success('已删除(原型)'); + _deleteModalVisible[1](false); + _deleteModalRecord[1](null); + }, + okText: '确定', + cancelText: '取消' + }), + React.createElement(Modal, { + title: '是否确认撤回该合同', + open: _withdrawModalVisible[0], + onCancel: function() { _withdrawModalVisible[1](false); _withdrawModalRecord[1](null); }, + onOk: function() { + message.success('已撤回(原型)'); + _withdrawModalVisible[1](false); + _withdrawModalRecord[1](null); + }, + okText: '确定', + cancelText: '取消' + }), + React.createElement(Modal, { + title: '是否确认终止合同', + open: _terminateModalVisible[0], + onCancel: function() { _terminateModalVisible[1](false); _terminateModalRecord[1](null); }, + onOk: function() { + message.success('已提交审核(原型),审核通过后合同状态变更为已结束'); + _terminateModalVisible[1](false); + _terminateModalRecord[1](null); + }, + okText: '提交审核', + cancelText: '取消' + }), + React.createElement(Modal, { + title: '添加被授权人', + open: _authorizedModalVisible[0], + onCancel: function() { _authorizedModalVisible[1](false); _authorizedModalRecord[1](null); }, + width: 640, + footer: [ + React.createElement(Button, { key: 'cancel', onClick: function() { _authorizedModalVisible[1](false); _authorizedModalRecord[1](null); } }, '取消'), + React.createElement(Button, { key: 'ok', type: 'primary', onClick: function() { message.success('已提交审核(原型)'); _authorizedModalVisible[1](false); _authorizedModalRecord[1](null); } }, '提交审核') + ] + }, React.createElement('div', { style: { padding: '8px 0' } }, + React.createElement(Table, { + rowKey: function(_, i) { return String(i); }, + size: 'small', + columns: [ + { title: '被授权人', key: 'name', width: 140, render: function(_, row, index) { return React.createElement(Input, { value: row.name, onChange: function(e) { updateAuthorizedRow(index, 'name', e.target.value); }, placeholder: '请输入' }); } }, + { title: '被授权人联系电话', key: 'phone', width: 160, render: function(_, row, index) { return React.createElement(Input, { value: row.phone, onChange: function(e) { updateAuthorizedRow(index, 'phone', e.target.value); }, placeholder: '请输入' }); } }, + { title: '被授权人身份证', key: 'idCard', width: 200, render: function(_, row, index) { return React.createElement(Input, { value: row.idCard, onChange: function(e) { updateAuthorizedRow(index, 'idCard', e.target.value); }, placeholder: '请输入' }); } }, + { title: '操作', key: 'action', width: 80, render: function(_, row, index) { return React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function() { removeAuthorizedRow(index); } }, '删除'); } } + ], + dataSource: _authorizedList[0], + pagination: false + }), + React.createElement(Button, { type: 'dashed', style: { marginTop: 12, width: '100%' }, onClick: addAuthorizedRow }, '添加一行') + )), + React.createElement(Modal, { + title: '附加费用', + open: _extraFeeModalVisible[0], + onCancel: function() { _extraFeeModalVisible[1](false); _extraFeeModalRecord[1](null); _extraFeeList[1]([]); }, + width: 900, + footer: [ + React.createElement(Button, { key: 'cancel', onClick: function() { _extraFeeModalVisible[1](false); _extraFeeModalRecord[1](null); _extraFeeList[1]([]); } }, '取消'), + React.createElement(Button, { key: 'ok', type: 'primary', onClick: function() { message.success('已提交审核(原型)'); _extraFeeModalVisible[1](false); _extraFeeModalRecord[1](null); _extraFeeList[1]([]); } }, '提交审核') + ] + }, React.createElement('div', { style: { padding: '8px 0' } }, + React.createElement(Table, { + rowKey: function(_, i) { return String(i); }, + size: 'small', + columns: [ + { title: '车辆类型', dataIndex: 'vehicleType', key: 'vehicleType', width: 160, render: function(v) { return v || '-'; } }, + { title: '品牌', dataIndex: 'brand', key: 'brand', width: 90, render: function(v) { return v || '-'; } }, + { title: '型号', dataIndex: 'model', key: 'model', width: 100, render: function(v) { return v || '-'; } }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 100, render: function(v) { return v || '-'; } }, + { title: '服务项目', key: 'serviceItem', width: 180, render: function(_, row, index) { return React.createElement(Select, { placeholder: '请选择', style: { width: '100%' }, value: row.serviceItem, onChange: function(v) { updateExtraFeeRow(index, 'serviceItem', v); }, options: extraFeeServiceOptions, allowClear: true }); } }, + { title: '费用(元)', key: 'fee', width: 140, render: function(_, row, index) { return React.createElement(Input, { value: row.fee, onChange: function(e) { updateExtraFeeRow(index, 'fee', e.target.value); }, placeholder: '请输入', addonAfter: '元' }); } }, + { title: '生效时间', key: 'effectiveDate', width: 140, render: function(_, row, index) { var dayjs = window.dayjs; var val = row.effectiveDate && dayjs ? dayjs(row.effectiveDate) : null; return React.createElement(DatePicker, { style: { width: '100%' }, placeholder: '请选择日期', value: val, onChange: function(date, dateString) { updateExtraFeeRow(index, 'effectiveDate', dateString || null); } }); } } + ], + dataSource: _extraFeeList[0], + pagination: false, + scroll: { x: 920 } + }) + )), + React.createElement(Modal, { + title: '上传盖章合同', + open: _stampModalVisible[0], + onCancel: function() { + _stampModalVisible[1](false); + _stampModalRecord[1](null); + _stampFileList[1]([]); + }, + width: 560, + footer: [ + React.createElement(Button, { key: 'cancel', onClick: function() { + _stampModalVisible[1](false); + _stampModalRecord[1](null); + _stampFileList[1]([]); + } }, '取消'), + React.createElement(Button, { key: 'ok', type: 'primary', onClick: function() { + var rec = _stampModalRecord[0]; + var list = _stampFileList[0] || []; + if (!list.length) { + message.warning('请先选择要上传的文件'); + return; + } + var pending = list.some(function(f) { return f.status === 'uploading'; }); + if (pending) { + message.warning('请等待文件上传完成'); + return; + } + _stampedUploadedOverride[1](function(prev) { + var n = Object.assign({}, prev); + if (rec) n[rec.id] = true; + return n; + }); + message.success('盖章合同已上传(原型)'); + _stampModalVisible[1](false); + _stampModalRecord[1](null); + _stampFileList[1]([]); + } }, '确认上传') + ] + }, React.createElement('div', { style: { padding: '8px 0' } }, + React.createElement('div', { style: { marginBottom: 12, fontSize: 14, color: 'rgba(0,0,0,0.65)' } }, + '合同编码:', + React.createElement('span', { style: { color: 'rgba(0,0,0,0.85)', fontWeight: 500 } }, _stampModalRecord[0] ? _stampModalRecord[0].contractCode : '-') + ), + React.createElement(Upload.Dragger, { + multiple: true, + fileList: _stampFileList[0], + onChange: function(info) { _stampFileList[1](info.fileList); }, + customRequest: function(opts) { + setTimeout(function() { + if (opts.onSuccess) opts.onSuccess('ok'); + }, 200); + }, + accept: '.pdf,.doc,.docx,image/*' + }, React.createElement('p', { style: { margin: 0, padding: '20px 0' } }, '点击或拖拽文件到此区域上传;支持一次选择多个附件,可分批添加')) + )), + React.createElement(Modal, { + title: '需求说明', + open: _requirementModalVisible[0], + onCancel: function() { _requirementModalVisible[1](false); }, + width: 720, + footer: React.createElement(Button, { onClick: function() { _requirementModalVisible[1](false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, React.createElement('div', { style: { padding: '8px 0' } }, + React.createElement('div', { style: reqTitleStyle }, '车辆租赁合同(2026年3月3日版本)'), + React.createElement('div', { style: { fontSize: 14, marginBottom: 12, color: 'rgba(0,0,0,0.85)' } }, '一个「数字化资产ONEOS运管平台」中的「车辆租赁合同」模块'), + React.createElement('div', { style: reqSectionStyle }, '1.面包屑:'), + React.createElement('div', { style: reqSubStyle }, '1.1.业务管理-车辆租赁合同'), + React.createElement('div', { style: reqSectionStyle }, '2.筛选:'), + React.createElement('div', { style: reqSubStyle }, '2.1.支持通过合同编码、项目名称、客户名称、签约公司、审批状态、合同状态、业务部门、业务负责人、合同类型、合同审批类型、创建人、合同结束日期等条件进行筛选,右侧为重置、查询、展开/收起(筛选条件以3列显示,默认显示一行,点击展开/收起对筛选栏卡片进行展开/收起所有筛选条件),点击查询后,筛选条件与列表内容联动。点击重置会回到默认筛选条件并在列表展示结果:'), + React.createElement('div', { style: reqItemStyle }, '2.1.1.合同编码:选择器,支持从输入框内输入内容进行模糊搜索,下拉显示匹配选项;'), + React.createElement('div', { style: reqItemStyle }, '2.1.2.项目名称:选择器,支持从输入框内输入内容进行模糊搜索,下拉显示匹配选项;'), + React.createElement('div', { style: reqItemStyle }, '2.1.3.客户名称:选择器,支持从输入框内输入内容进行模糊搜索,下拉显示匹配选项;'), + React.createElement('div', { style: reqItemStyle }, '2.1.4.签约公司:选择器,显示租赁合同所有签约公司;'), + React.createElement('div', { style: reqItemStyle }, '2.1.5.审批状态:选择器,支持全选或多选,选项为:全部、待审批、审批中、审批通过、审批驳回、未提交,默认显示全部;'), + React.createElement('div', { style: reqItemStyle }, '2.1.6.合同状态:选择器,支持全选或多选,选项为:全部、草稿、变更、合同进行中、到期合同、已提交审批、已结束;'), + React.createElement('div', { style: reqItemStyle }, '2.1.7.业务部门:选择器,支持全选或多选,拉取部门下所有业务相关部门;'), + React.createElement('div', { style: reqItemStyle }, '2.1.8.业务负责人:选择器,支持全选或多选,拉取所有业务相关部门下所有用户姓名;'), + React.createElement('div', { style: reqItemStyle }, '2.1.9.合同类型:选择器,支持全选或多选,选项为:全部、正式合同、试用合同;'), + React.createElement('div', { style: reqItemStyle }, '2.1.10.合同审批类型:选择器,支持全选或多选,选项为:全部、标准合同审批、非标准合同审批;'), + React.createElement('div', { style: reqItemStyle }, '2.1.11.创建人:选择器,支持全选或多选,拉取所有业务相关部门下所有用户姓名;'), + React.createElement('div', { style: reqItemStyle }, '2.1.12.合同结束日期:日期选择器,支持单输入框内双日历选择开始-结束时间;'), + React.createElement('div', { style: reqSectionStyle }, '3.列表:'), + React.createElement('div', { style: reqSubStyle }, '3.1.列表展示所有租赁合同信息,字段依次为:合同编码、项目名称、租赁车辆数、已交车辆数、审批状态、合同状态、客户名称、签约公司、业务部门、业务负责人、合同类型、合同审批类型、合同结束日期、客户联系人、联系电话、创建人、创建时间、更新人、最后更新时间、备注、操作;列表右上角为新增、导出;'), + React.createElement('div', { style: reqItemStyle }, '3.1.1.合同编码:显示租赁合同对应合同编码;'), + React.createElement('div', { style: reqItemStyle }, '3.1.2.项目名称:显示租赁合同对应项目名称;'), + React.createElement('div', { style: reqItemStyle }, '3.1.3.租赁车辆数:显示租赁车辆数,点击显示气泡卡片,卡片中列表显示:车辆类型、品牌、型号、车牌号、实际交车日期;'), + React.createElement('div', { style: reqItemStyle }, '3.1.4.已交车辆数:显示已交车辆数,样式与交互同租赁车辆数,点击显示气泡卡片,卡片中显示已交车列表(同租赁车辆数列表结构);'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.3.1.车辆类型:4.5吨冷链车-轻型厢式货车、18吨双飞翼货车-重型厢式货车、49吨牵引车头-重型半挂牵引车、4.5吨货车-轻型厢式货车、18吨厢式货车-重型厢式货车、重型集装箱半挂车-重型集装箱半挂车、公务用车/小客车-小型普通客车、35吨牵引车头-重型半挂牵引车、重型平板半挂车-重型平板半挂车;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.3.2.品牌:显示租赁合同中对应车辆品牌;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.3.3.型号:显示租赁合同中对应车辆型号;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.3.4.车牌号:显示租赁合同中对应车牌号,无则显示为-,有则显示具体车牌号;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.3.5.实际交车日期:显示租赁合同中对应车辆对应实际交车日期,精确至分钟,格式为YYYY-MM-DD HH:MM;'), + React.createElement('div', { style: reqItemStyle }, '3.1.4.审批状态:显示租赁合同实际审批状态,状态分为:待审批、审批中、审批通过、审批驳回、未提交;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.4.1.待审批:发起人已提交,但还没有任何流程节点完成审批;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.4.2.审批中:发起人已提交,已有1个以上节点完成审批,但未完成最终节点审批;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.4.3.审批通过:发起人已提交,最终节点完成审批;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.4.4.审批驳回:发起人已提交,任意流程节点驳回,该状态下操作列支持编辑和重新提交;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.4.5.未提交:发起人仅保存,但未提交审批;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.4.6.撤回:发起人主动撤回审批流程;'), + React.createElement('div', { style: reqItemStyle }, '3.1.5.合同状态:显示租赁合同状态,状态分为:草稿、变更、合同进行中、到期合同、已提交审批、已结束;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.5.1.草稿:发起人仅保存,但未提交审批;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.5.2.变更:发起人提交后,合同已通过审批的基础上,进行了「变更内容」操作,且已提交审批,但未完成最终节点审批;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.5.3.合同进行中:发起人提交后,合同完成最终节点审批;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.5.4.到期合同:发起人已提交,最终节点完成审批,但当前日期超过合同结束日期;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.5.5.已提交审批:发起人初次提交,但未完成最终节点审批;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.5.6.已结束:当前日期超过合同结束日期或在操作列终止合同并完成审核;'), + React.createElement('div', { style: reqItemStyle }, '3.1.6.客户名称:显示租赁合同创建时所选客户名称,客户名称来自「客户管理」-「客户名称」;'), + React.createElement('div', { style: reqItemStyle }, '3.1.7.签约公司:显示租赁合同创建时所选签约公司,签约公司来自组织机构表;'), + React.createElement('div', { style: reqItemStyle }, '3.1.8.业务部门:显示租赁合同创建时所选业务部门,业务部门来自部门表(业务组);'), + React.createElement('div', { style: reqItemStyle }, '3.1.9.业务负责人:显示租赁合同创建时所选业务负责人,与业务部门联动,查询该业务组下人员;'), + React.createElement('div', { style: reqItemStyle }, '3.1.10.合同类型:显示租赁合同类型,类型包括:正式合同、试用合同,于创建租赁合同时选取;'), + React.createElement('div', { style: reqItemStyle }, '3.1.11.合同审批类型:显示租赁合同创建时所选合同审批类型,包括:标准合同审批、非标准合同审批;'), + React.createElement('div', { style: reqItemStyle }, '3.1.12.合同结束日期:显示租赁合同结束日期,精确至日,格式为YYYY-MM-DD;'), + React.createElement('div', { style: reqItemStyle }, '3.1.13.客户联系人:显示租赁合同客户联系人姓名,客户联系人姓名来自「客户管理」-「联系人」;'), + React.createElement('div', { style: reqItemStyle }, '3.1.14.联系电话:显示租赁合同客户联系电话,客户联系电话来自「客户管理」-「联系人手机」;'), + React.createElement('div', { style: reqItemStyle }, '3.1.15.创建人:显示租赁合同创建人姓名,取自操作用户姓名;'), + React.createElement('div', { style: reqItemStyle }, '3.1.16.创建时间:显示租赁合同创建时间,精确至分钟,格式为YYYY-MM-DD HH:MM;'), + React.createElement('div', { style: reqItemStyle }, '3.1.17.更新人:显示租赁合同最后一次更新人姓名,取自操作用户姓名,如无则显示:-;'), + React.createElement('div', { style: reqItemStyle }, '3.1.18.最后更新时间:显示租赁合同最后一次更新时间,精确至分钟,格式为YYYY-MM-DD HH:MM,如无则显示:-;'), + React.createElement('div', { style: reqItemStyle }, '3.1.19.备注:显示租赁合同创建时输入的备注信息,如无则显示:-;'), + React.createElement('div', { style: reqItemStyle }, '3.1.20.操作:操作分为:查看、编辑、新增车辆、续签合同、删除合同、撤回合同、添加被授权人、附加费用、变更为三方合同、转正式合同、终止合同、上传盖章合同;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.20.1.查看:跳转车辆租赁合同-查看;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.20.2.编辑:当「合同状态」为「草稿」时显示,点击跳转编辑租赁合同页面,编辑租赁合同页面可输入项参考「新增租赁合同」页面,并支持对保存时已填内容进行修改;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.20.3.新增车辆:当「合同状态」为「合同进行中」时显示,仅能在租赁订单信息卡片下新订单中进行车辆新增;新增车辆提交后触发重新审核流程,审核通过后生效(不影响原有合同正常业务);'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.20.4.续签合同:当「合同状态」为「合同进行中」、「合同到期」时显示,点击跳转车辆租赁合同-续签合同页,提交时重新触发租赁合同审核流程;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.20.5.删除合同:当「合同状态」为「草稿」时显示,点击删除合同时进行二次确认,提示语:是否确认删除该合同草稿;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.20.6.撤回合同:当「合同状态」为「已提交审核」时显示,点击撤回合同时进行二次确认,提示语:是否确认撤回该合同;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.20.7.添加被授权人:当「合同状态」为「合同进行中」时显示,点击弹出卡片,卡片中可编辑被授权人、被授权人联系电话、被授权人身份证,同时支持添加一行、删除已有行等操作;添加被授权人触发重新审核流程,审核通过后生效;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.20.8.附加费用:当「合同状态」为「合同进行中」时显示,点击弹出卡片,卡片中显示该租赁合同内:车辆类型、品牌、型号、车牌号,同时可对服务项目、费用、生效时间进行编辑,触发租赁合同审核流程,审核通过后生效;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.20.8.1.服务项目:代处理费用、罚款、违章处理违约金、未参加安全培训、车辆出险、年检年审违约、停车费、设备损坏金(包含易损件)、清洗费、上门收车人工费、上门收车送车行驶费、上门收车基础服务费、保险上浮、保养费用、补办驾驶证、补办牌照、补办营运证、补办加氢证、借用备用钥匙、补配钥匙、租金、氢气费-客、退还车氢量差、能源费补缴、能源费退款、送车上门人工费、送车上门送车行驶费、送车上门基础服务费、保证金、氢气预付费、维修费用、ETC-客、ETC卡缺损费、ETC设备缺损费、电费-客、未结算保养费、未结算维修费、车损费、工具损坏或丢失费、证件费、广告损坏费、送车服务费、接车服务费、补办行驶证、超赔险、轮胎磨损费、无忧包、轮胎保、养护保、尾板;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.20.8.2.费用:输入框,后缀为元,支持2位小数输入;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.20.8.3.生效时间:日期选择器,精确至日;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.20.9.变更为三方合同:当「合同状态」为「合同进行中」时显示,点击跳转车辆租赁合同-变更为三方合同页面,提交时重新触发租赁合同审核流程;;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.20.10.转正式合同:当「合同类型」为「试用合同」时显示,点击后跳转车辆租赁合同-转正式合同页面,提交时重新触发租赁合同审核流程;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.20.11.终止合同:当「合同状态」为「合同进行中」时显示,点击后进行二次确认,提示语:是否确认终止合同,确认按钮为提交审核,会重新发起合同审核流程,审核通过后,合同状态变更为:已结束;'), + React.createElement('div', { style: reqSubItemStyle }, '3.1.20.12.上传盖章合同:当「审批状态」为「审批通过」且法务审核环节尚未上传盖章合同附件时,在「更多」中显示,仅法务部员工可见并可操作;点击后弹出上传窗口,可浏览本地文件上传,支持多文件;上传完成后关闭该入口(列表不再展示「上传盖章合同」);') + )) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-ledger-data/index.tsx b/src/prototypes/oneos-web-ledger-data/index.tsx new file mode 100644 index 0000000..ff6969c --- /dev/null +++ b/src/prototypes/oneos-web-ledger-data/index.tsx @@ -0,0 +1,29 @@ +/** + * @name 台账数据 + * 自 ONE-OS web端 原稿复刻(5 个页面) + */ +import React from 'react'; +import Page1 from './pages/01-保险分摊明细.jsx'; +import Page2 from './pages/02-车辆氢费明细.jsx'; +import Page3 from './pages/03-车辆维修明细.jsx'; +import Page4 from './pages/04-氢费采购端汇总报表.jsx'; +import Page5 from './pages/05-租赁业务台账.jsx'; +import { OneosWebLegacyShell } from '../../common/oneosWebLegacy/OneosWebLegacyShell'; + +const pages = [ + { id: 'u53f0u8d26u6570u636e-u4fddu9669u5206u644au660eu7', title: '保险分摊明细', component: Page1 }, + { id: 'u53f0u8d26u6570u636e-u8f66u8f86u6c22u8d39u660eu7', title: '车辆氢费明细', component: Page2 }, + { id: 'u53f0u8d26u6570u636e-u8f66u8f86u7ef4u4feeu660eu7', title: '车辆维修明细', component: Page3 }, + { id: 'u53f0u8d26u6570u636e-u6c22u8d39u91c7u8d2du7aefu6', title: '氢费采购端汇总报表', component: Page4 }, + { id: 'u53f0u8d26u6570u636e-u79dfu8d41u4e1au52a1u53f0u8', title: '租赁业务台账', component: Page5 }, +]; + +export default function OneosWebLedgerData() { + return ( + + ); +} diff --git a/src/prototypes/oneos-web-ledger-data/pages/01-保险分摊明细.jsx b/src/prototypes/oneos-web-ledger-data/pages/01-保险分摊明细.jsx new file mode 100644 index 0000000..77d23be --- /dev/null +++ b/src/prototypes/oneos-web-ledger-data/pages/01-保险分摊明细.jsx @@ -0,0 +1,517 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 台账数据 - 车辆保险台账(保险分摊明细) +// 原型:客户名称 + 结算周期筛选、导出;日成本与分摊成本按业务公式计算(联调可替换为接口) + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + + var antd = window.antd; + var App = antd.App; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Button = antd.Button; + var Table = antd.Table; + var Select = antd.Select; + var DatePicker = antd.DatePicker; + var Row = antd.Row; + var Col = antd.Col; + var Space = antd.Space; + var message = antd.message; + + var CUSTOMER_OPTIONS = [ + { value: '浙江羚牛氢能科技有限公司', label: '浙江羚牛氢能科技有限公司' }, + { value: '杭州绿运物流有限公司', label: '杭州绿运物流有限公司' }, + { value: '宁波港城新能源车队', label: '宁波港城新能源车队' }, + { value: '绍兴氢能示范运营中心', label: '绍兴氢能示范运营中心' } + ]; + + var PROJECT_BY_CUSTOMER = { + '浙江羚牛氢能科技有限公司': ['氢能重卡租赁一期', '园区通勤包车'], + '杭州绿运物流有限公司': ['城配氢能车辆项目', '冷链专线'], + '宁波港城新能源车队': ['港口短驳氢能车', '堆场转运'], + '绍兴氢能示范运营中心': ['示范线路运营', '加氢站接驳'] + }; + + var PLATE_PREFIX = ['浙A', '浙B', '浙D', '浙G']; + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + function numOrZero(v) { + if (v === null || v === undefined || v === '') return 0; + var n = Number(v); + return isNaN(n) ? 0 : n; + } + + function fmtMoney(n, digits) { + if (n === null || n === undefined || n === '') return '-'; + var x = Number(n); + if (isNaN(x)) return '-'; + var d = digits === undefined ? 2 : digits; + return x.toLocaleString('zh-CN', { minimumFractionDigits: d, maximumFractionDigits: d }); + } + + function escapeCsv(v) { + var s = v == null ? '' : String(v); + if (s.indexOf(',') !== -1 || s.indexOf('"') !== -1 || s.indexOf('\n') !== -1 || s.indexOf('\r') !== -1) { + return '"' + s.replace(/"/g, '""') + '"'; + } + return s; + } + + function downloadCsv(filename, lines) { + var csv = lines.map(function (row) { return row.map(escapeCsv).join(','); }).join('\n'); + var blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' }); + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); + } + + function initialSettlementMonth() { + try { + if (window.dayjs) return window.dayjs('2026-05-01'); + } catch (e1) {} + return null; + } + + function settlementYm(d) { + if (!d || !window.dayjs) return ''; + try { + return window.dayjs(d).format('YYYY-MM'); + } catch (e2) { + return ''; + } + } + + /** 日成本 = 缴费金额 / 生效天数 */ + function dailyCost(payment, effectiveDays) { + var pay = numOrZero(payment); + var days = numOrZero(effectiveDays); + if (days <= 0) return null; + return Math.round((pay / days) * 10000) / 10000; + } + + /** 保险分摊成本 = Σ(各险日成本 × 分摊天数) */ + function calcApportionCost(row) { + var d = numOrZero(row.apportionDays); + if (d <= 0) return null; + var sum = 0; + var has = false; + ['compulsoryDaily', 'commercialDaily', 'excessDaily', 'cargoDaily'].forEach(function (k) { + var v = row[k]; + if (v !== null && v !== undefined && !isNaN(Number(v))) { + sum += Number(v) * d; + has = true; + } + }); + if (!has) return null; + return Math.round(sum * 100) / 100; + } + + function buildMockRows(ym) { + var rows = []; + var seed = 0; + CUSTOMER_OPTIONS.forEach(function (cust, ci) { + var projects = PROJECT_BY_CUSTOMER[cust.value] || ['默认项目']; + projects.forEach(function (proj, pi) { + seed += 1; + var plate = PLATE_PREFIX[ci % PLATE_PREFIX.length] + String(10000 + seed).slice(-5) + 'F'; + var effDays = 365; + var compulsoryPay = 950 + seed * 3; + var commercialPay = 4200 + seed * 17; + var excessPay = seed % 3 === 0 ? 1800 + seed * 5 : 0; + var cargoPay = seed % 2 === 0 ? 600 + seed * 2 : 0; + var apportionDays = 18 + ((seed + (ym ? ym.length : 0)) % 13); + + var row = { + key: 'r' + seed, + seq: seed, + settlementCycle: ym || '2026-05', + customerName: cust.value, + projectName: proj, + plateNo: plate, + compulsoryDaily: dailyCost(compulsoryPay, effDays), + commercialDaily: dailyCost(commercialPay, effDays), + excessDaily: excessPay > 0 ? dailyCost(excessPay, effDays) : null, + cargoDaily: cargoPay > 0 ? dailyCost(cargoPay, effDays) : null, + apportionDays: apportionDays + }; + row.apportionCost = calcApportionCost(row); + rows.push(row); + }); + }); + return rows; + } + + + var layoutStyle = { + padding: '16px 24px 24px', + minHeight: '100vh', + background: 'linear-gradient(165deg, #eef4ff 0%, #f5f7fa 42%, #f0f2f5 100%)' + }; + var filterLabelStyle = { marginBottom: 6, fontSize: 13, color: 'rgba(0,0,0,0.55)', fontWeight: 500 }; + var filterItemStyle = { marginBottom: 12 }; + var filterControlStyle = { width: '100%' }; + var filterActionsColStyle = { flex: '0 0 auto', marginLeft: 'auto' }; + + var filterCardStyle = { + marginBottom: 20, + borderRadius: 16, + boxShadow: '0 4px 20px -4px rgba(16,24,40,0.03), 0 0 0 1px rgba(16,24,40,0.06)', + border: 'none', + background: '#ffffff' + }; + + var tableCardStyle = { + borderRadius: 16, + boxShadow: '0 10px 32px -4px rgba(16,24,40,0.06), 0 0 0 1px rgba(16,24,40,0.04)', + border: 'none', + background: '#ffffff', + overflow: 'hidden' + }; + + var ledgerTableStyle = + '.ins-ledger-table-wrap{border-radius:12px;overflow:hidden;box-shadow:0 4px 24px -6px rgba(15,23,42,0.05),0 0 0 1px rgba(22,119,255,0.1)}' + + '.ins-ledger-table .ant-table-thead>tr>th{white-space:nowrap;color:#1e293b!important;font-weight:600!important;font-size:13px!important;' + + 'background:#e8f4fc!important;border-bottom:1px solid #bae6fd!important;border-inline-end:1px solid #dbeafe!important;padding:0 8px!important;height:38px!important}' + + '.ins-ledger-table .ant-table-tbody>tr:not(.ant-table-measure-row)>td{white-space:nowrap;font-variant-numeric:tabular-nums;color:#334155;border-bottom:1px solid #f1f5f9!important;border-inline-end:1px solid #f8fafc!important;padding:0 8px!important;height:38px!important}' + + '.ins-ledger-table .ant-table-tbody>tr.ins-row-data:hover>td{background:#f0f9ff!important}' + + '.ins-ledger-table .ant-table-summary>tr>td{font-weight:700;background:#f8fafc!important;color:#0f172a!important;border-top:2px solid #cbd5e1!important;padding:0 8px!important;height:38px!important}'; + + var customerDraftState = useState(undefined); + var customerDraft = customerDraftState[0]; + var setCustomerDraft = customerDraftState[1]; + + var monthDraftState = useState(initialSettlementMonth); + var monthDraft = monthDraftState[0]; + var setMonthDraft = monthDraftState[1]; + + var customerAppliedState = useState(undefined); + var customerApplied = customerAppliedState[0]; + var setCustomerApplied = customerAppliedState[1]; + + var monthAppliedState = useState(initialSettlementMonth); + var monthApplied = monthAppliedState[0]; + var setMonthApplied = monthAppliedState[1]; + + var appliedYm = useMemo(function () { + return settlementYm(monthApplied) || '2026-05'; + }, [monthApplied]); + + var allRows = useMemo(function () { + return buildMockRows(appliedYm); + }, [appliedYm]); + + var dataSource = useMemo(function () { + var list = allRows.filter(function (r) { + if (customerApplied && r.customerName !== customerApplied) return false; + return true; + }); + return list.map(function (r, idx) { + return Object.assign({}, r, { seq: idx + 1 }); + }); + }, [allRows, customerApplied]); + + var totalApportionCost = useMemo(function () { + return dataSource.reduce(function (acc, r) { + return acc + numOrZero(r.apportionCost); + }, 0); + }, [dataSource]); + + var customerDisplayLabel = customerApplied || '默认全量数据'; + var cycleDisplayLabel = appliedYm || '-'; + + var handleQuery = useCallback(function () { + setCustomerApplied(customerDraft); + setMonthApplied(monthDraft); + message.success('查询成功'); + }, [customerDraft, monthDraft]); + + var handleReset = useCallback(function () { + setCustomerDraft(undefined); + setMonthDraft(initialSettlementMonth()); + setCustomerApplied(undefined); + setMonthApplied(initialSettlementMonth()); + }, []); + + var handleExport = useCallback(function () { + var headers = [ + '序号', + '结算周期', + '客户名称', + '项目名称', + '车牌号', + '交强险日成本', + '商业险日成本', + '超赔险日成本', + '货物险日成本', + '分摊天数', + '保险分摊成本' + ]; + var body = dataSource.map(function (r) { + return [ + r.seq, + r.settlementCycle, + r.customerName, + r.projectName, + r.plateNo, + r.compulsoryDaily, + r.commercialDaily, + r.excessDaily, + r.cargoDaily, + r.apportionDays, + r.apportionCost + ]; + }); + body.push(['合计', '', '', '', '', '', '', '', '', '', totalApportionCost]); + downloadCsv('车辆保险台账_' + cycleDisplayLabel + '_' + new Date().getTime() + '.csv', [headers].concat(body)); + message.success('已导出 CSV'); + }, [dataSource, cycleDisplayLabel, totalApportionCost]); + + var columns = useMemo(function () { + return [ + { + title: '序号', + dataIndex: 'seq', + key: 'seq', + width: 64, + align: 'center', + fixed: 'left' + }, + { + title: '结算周期', + dataIndex: 'settlementCycle', + key: 'settlementCycle', + width: 100, + align: 'center' + }, + { + title: '客户名称', + dataIndex: 'customerName', + key: 'customerName', + width: 200, + align: 'center', + ellipsis: true + }, + { + title: '项目名称', + dataIndex: 'projectName', + key: 'projectName', + width: 160, + align: 'center', + ellipsis: true + }, + { + title: '车牌号', + dataIndex: 'plateNo', + key: 'plateNo', + width: 110, + align: 'center' + }, + { + title: '交强险日成本', + dataIndex: 'compulsoryDaily', + key: 'compulsoryDaily', + width: 130, + align: 'right', + render: function (v) { return fmtMoney(v, 4); } + }, + { + title: '商业险日成本', + dataIndex: 'commercialDaily', + key: 'commercialDaily', + width: 130, + align: 'right', + render: function (v) { return fmtMoney(v, 4); } + }, + { + title: '超赔险日成本', + dataIndex: 'excessDaily', + key: 'excessDaily', + width: 130, + align: 'right', + render: function (v) { return fmtMoney(v, 4); } + }, + { + title: '货物险日成本', + dataIndex: 'cargoDaily', + key: 'cargoDaily', + width: 130, + align: 'right', + render: function (v) { return fmtMoney(v, 4); } + }, + { + title: '分摊天数', + dataIndex: 'apportionDays', + key: 'apportionDays', + width: 120, + align: 'center' + }, + { + title: '保险分摊成本', + dataIndex: 'apportionCost', + key: 'apportionCost', + width: 140, + align: 'right', + fixed: 'right', + render: function (v) { return fmtMoney(v, 2); } + } + ]; + }, []); + + var tableSummary = useCallback(function () { + return React.createElement( + Table.Summary, + null, + React.createElement( + Table.Summary.Row, + null, + React.createElement(Table.Summary.Cell, { index: 0, align: 'center', colSpan: 1 }, '合计'), + React.createElement(Table.Summary.Cell, { index: 1, colSpan: 9 }), + React.createElement(Table.Summary.Cell, { index: 10, align: 'right' }, fmtMoney(totalApportionCost, 2)) + ) + ); + }, [totalApportionCost]); + + return React.createElement( + App, + null, + React.createElement('style', null, ledgerTableStyle), + React.createElement( + 'div', + { style: layoutStyle }, + React.createElement(Breadcrumb, { + style: { marginBottom: 12 }, + items: [{ title: '台账数据' }, { title: '保险分摊明细' }] + }), + React.createElement( + Card, + { style: filterCardStyle, bodyStyle: { paddingBottom: 4 } }, + React.createElement( + Row, + { gutter: [16, 16], align: 'bottom' }, + React.createElement( + Col, + { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement( + 'div', + { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '客户名称'), + React.createElement(Select, { + allowClear: true, + showSearch: true, + placeholder: '默认全量数据', + style: filterControlStyle, + value: customerDraft, + onChange: function (v) { setCustomerDraft(v); }, + options: CUSTOMER_OPTIONS, + filterOption: filterOption + }) + ) + ), + React.createElement( + Col, + { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement( + 'div', + { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '结算周期'), + React.createElement(DatePicker, { + picker: 'month', + style: filterControlStyle, + placeholder: '请选择结算周期', + format: 'YYYY-MM', + value: monthDraft, + onChange: function (v) { setMonthDraft(v); } + }) + ) + ), + React.createElement( + Col, + { xs: 24, sm: 12, md: 8, lg: 6, style: filterActionsColStyle }, + React.createElement( + 'div', + { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '\u00a0'), + React.createElement( + Space, + { wrap: true }, + React.createElement(Button, { onClick: handleReset }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleQuery }, '查询') + ) + ) + ) + ) + ), + React.createElement( + Card, + { style: tableCardStyle, bodyStyle: { padding: '20px 20px 24px' } }, + React.createElement( + 'div', + { style: { position: 'relative', marginBottom: 8, minHeight: 36 } }, + React.createElement( + 'div', + { + style: { + textAlign: 'center', + fontSize: 18, + fontWeight: 700, + color: 'rgba(15,23,42,0.92)', + letterSpacing: '0.02em', + padding: '0 88px' + } + }, + '车辆保险台账' + ), + React.createElement( + 'div', + { style: { position: 'absolute', right: 0, top: '50%', transform: 'translateY(-50%)' } }, + React.createElement(Button, { onClick: handleExport }, '导出') + ) + ), + React.createElement( + 'div', + { + style: { + textAlign: 'center', + marginBottom: 16, + fontSize: 13, + color: 'rgba(15,23,42,0.55)', + fontWeight: 500 + } + }, + '结算周期:', + cycleDisplayLabel, + '\u00A0\u00A0\u00A0\u00A0客户:', + customerDisplayLabel + ), + React.createElement( + 'div', + { className: 'ins-ledger-table-wrap' }, + React.createElement(Table, { + className: 'ins-ledger-table', + size: 'small', + bordered: true, + rowKey: 'key', + columns: columns, + dataSource: dataSource, + pagination: false, + rowClassName: function () { return 'ins-row-data'; }, + scroll: { x: 'max-content', y: 'calc(100vh - 320px)' }, + sticky: true, + summary: tableSummary + }) + ) + ) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-ledger-data/pages/02-车辆氢费明细.jsx b/src/prototypes/oneos-web-ledger-data/pages/02-车辆氢费明细.jsx new file mode 100644 index 0000000..d2deb64 --- /dev/null +++ b/src/prototypes/oneos-web-ledger-data/pages/02-车辆氢费明细.jsx @@ -0,0 +1,4541 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 台账数据 - 车辆氢费明细(业务人员手动录入;保存后为未对账,勾选后「完成对账」) +// 原型:联调后对接加氢站、车辆、客户、公司及登录账号接口 + +// 车辆氢费明细 PRD 正文(与 车辆氢费明细-需求文档.md 同步) +var H2_LEDGER_REQUIREMENT_DOC = `# 车辆氢费明细 — 产品需求说明(PRD) + +| 项目 | 内容 | +|------|------| +| 文档版本 | v2.1(业务版) | +| 产品模块 | 台账数据 → 车辆氢费明细 | +| 文档类型 | 产品需求说明 | +| 适用读者 | 产品、业务、运营、测试、项目 | +| 修订说明 | 同步列表字段顺序、顶部合计条、承担方式、系统带出字段等现行页面行为 | + +--- + +## 一、为什么做这件事 + +### 1.1 业务痛点 + +- 车辆加氢费用分散在业务同事手工记录中,缺少统一台账,难以按客户、站点、时间核对。 +- 录入后需区分「是否已正式提交」「是否已完成对账」,避免未核实数据进入对账与结算环节。 +- 加氢量偏大、单价与公司在该站该时段的标准价不一致等情况,需要**提前暴露**,减少后续财务纠纷。 + +### 1.2 产品价值 + +| 价值点 | 说明 | +|--------|------| +| 统一台账 | 一条记录对应一次加氢业务,关联车辆、客户、站点与金额 | +| 流程可控 | 待保存 → 未对账 → 已对账,责任边界清晰 | +| 效率提升 | 支持批量导入、批量改价、复制同类记录 | +| 风险可见 | 异常数据标黄,可一键只看有问题记录 | +| 可追溯 | 关键修改留痕,便于主管复核与客诉查证 | + +### 1.3 本期目标 + +建设 Web 端「车辆氢费明细」页面,支撑业务人员**查询、录入、保存、对账、导入导出**全流程;财务相关字段(开票、付款等)由系统按客户等信息自动带出,减少手工填写错误。 + +### 1.4 本期不做 + +- 多级审批流、与 ERP 自动过账(可后续迭代)。 +- 按组织架构的复杂数据权限(本期按「业务员 / 主管」两类角色描述,上线后对接权限中心)。 +- 导入文件格式的完整兼容方案以业务验收为准(需支持模板化批量录入)。 + +--- + +## 二、谁在用、用来干什么 + +### 2.1 用户角色 + +| 角色 | 典型诉求 | +|------|----------| +| **业务员** | 日常录入加氢记录;批量导入历史数据;保存后推进对账;导出给财务或客户核对 | +| **主管** | 复核异常数据;必要时修正或删除**已对账**记录;查看变更记录 | + +### 2.2 核心使用场景(用户故事) + +1. **单笔补录** + 业务员得知某车在某站加氢后,在页面新增一行,填写时间、站点、车牌、加氢量与单价,保存后进入「未对账」列表等待勾选完成对账。 + +2. **批量导入历史** + 业务员下载模板,在 Excel 中整理多笔记录后上传,系统生成多条状态为「未对账」数据。 + +3. **日常对账** + 业务员按客户或时间筛选「未对账」记录,勾选确认无误后点击「完成对账」,数据变为「已对账」,本人不可再随意修改。 + +4. **异常排查** + 业务员勾选「仅显示异常数据」,优先处理加氢量过大(>60kg)或单价与标准不一致的记录,修正后再保存或对账。 + +5. **主管纠错** + 已对账数据发现错误,主管进入编辑后修改;业务员侧编辑/删除按钮不可用,需联系主管处理。 + +6. **导出报送** + 业务员按筛选条件导出「未对账 + 已对账」正式数据(不含尚未保存的草稿),自选列字段生成表格文件。 + +--- + +## 三、业务主流程 + +### 3.1 端到端流程(推荐操作顺序) + +\`\`\`mermaid +flowchart LR + A[录入/复制] --> B[待保存] + B --> C[点击保存] + C[保存/导入] --> D[未对账] + D --> E[勾选并完成对账] + E --> F[已对账] +\`\`\` + +**说明:** + +- **待保存**:草稿态,可反复修改;尚未进入对账池。 +- **未对账**:已保存的正式数据,可「编辑」「删除」勾选「完成对账」。 +- **已对账**:对账完成,业务员不可改删;主管可特殊处理「编辑」「删除」。 + +### 3.2 「保存」与「完成对账」的区别 + +| 操作 | 业务含义 | 对象 | 结果 | +|------|----------|------|------| +| **保存** | 将草稿确认为可参与对账的正式记录 | 当前页所有「待保存」记录 | 全部变为「未对账」 | +| **完成对账** | 确认与客户/内部核对无误,结案 | 用户勾选的「未对账」记录 | 变为「已对账」 | + +**保存前校验:** 必填项未填完整时,在对应格子标红提示,**不弹出成功提示**,也不变更状态。 + +**完成对账前确认:** 系统提示「完成对账后只能联系主管修改」,用户确认后执行。 + +--- + +## 四、页面功能说明(业务视角) + +### 4.1 页面组成 + +用户进入「台账数据 → 车辆氢费明细」后,自上而下为: + +1. **查询条件区** — 按时间、订单、车辆、客户等筛选 +2. **数据列表区** — 展示氢费明细、合计、操作按钮 +3. **弹窗** — 批量导入、批量导出、变更日志 + +列表下方提供 **「新增一行」**,用于手工补录。 + +### 4.2 查询条件 + +**交互原则:** 修改条件后不会立刻刷新列表,需点击 **「查询」** 才生效;**「重置」** 清空条件并恢复全量展示。 + +**默认展示:** 第一行条件(加氢时间、订单编号、查询按钮);点击 **「展开」** 可看到更多条件。 + +| 查询项 | 业务说明 | +|--------|----------| +| 加氢时间 | 起止日期范围 | +| 订单编号 | 支持模糊搜索 | +| 车牌号 | 从公司车辆库选择 | +| 客户名称 | 从客户库选择 | +| 加氢站名称 | 从加氢站库选择 | +| 业务员 | 按客户归属业务员筛选 | +| 承担方式 | 客户承担 / 我司承担 / 客户自行结算 / 其他结算 | +| 客户收款状态 | 已付款 / 未付款(系统带出,用于筛选) | +| 开票公司 | 从开票主体选择(系统带出字段对应公司) | + +查询成功给予简短成功提示。 + +**与列表的关系:** + +- 列表默认展示符合查询条件的记录。 +- **例外:** 本人刚新增、尚未保存的记录,即使暂时不符合筛选条件,也会显示在列表中,避免「点了新增却找不到行」。 +- **「仅显示异常数据」** 开启时,只展示有风险提示的记录;本人待保存的新增行仍会显示,以免录入中断。 + +### 4.3 列表展示规则 + +**分区展示(便于扫读):** + +| 区域顺序(上→下) | 记录状态 | 排序习惯 | +|-------------------|----------|----------| +| 第一区 | 已对账 | 加氢时间新的在前 | +| 第二区 | 未对账 | 加氢时间新的在前 | +| 第三区 | 待保存 | 先录入的在前,新加的靠下 | + +不同区域之间有明显分隔,已对账区背景略灰、待保存区背景略黄,降低误操作概率。 + +**顶部合计条:** 表格上方独立展示 **加氢量(kg)、成本总价(元)、加氢总价(元)** 三项合计。 + +- **统计范围:** 与当前列表可见行一致。 +- **随查询重算:** 点击「查询」应用筛选条件后,合计立即按新结果重新汇总。 +- **随「仅显示异常数据」变化:** 开关开启时,仅对当前可见的异常记录(及本人待保存行)合计。 +- **说明:** 合计条含「待保存 / 未对账 / 已对账」等当前列表中的全部可见记录,与导出范围(仅未对账+已对账)不同。 + +**列表列顺序(左→右,业务视角):** +序号 → 加氢日期 → 加氢时间 → 加氢站名称 → 客户名称 → 车牌号 → 加氢量 → 成本单价 → 成本总价 → 加氢单价 → 加氢总价 → 行驶里程 → 备注 → 业务员 → 承担方式 → 对账日期 → **收票日期** → **加氢站付款状态** → **开票日期** → **客户收款状态** → **开票公司** → 状态 → 订单编号 → 操作(操作列固定右侧)。 + +**系统带出字段(不可手工维护):** 收票日期、加氢站付款状态、开票日期、客户收款状态、开票公司;选择/变更客户等信息后由系统自动刷新,单元格为浅灰底只读样式。 + +**列表勾选:** 仅 **未对账** 记录可勾选,用于批量「完成对账」。 + +### 4.4 工具栏操作 + +从左到右、从业务使用频率排列: + +| 按钮 | 业务作用 | +|------|----------| +| 仅显示异常数据 | 开关,快速聚焦有风险提示的记录 | +| 批量导入 | 按模板上传多条待保存记录 | +| 批量导出 | 按当前查询条件导出正式数据 | +| 保存 | 将全部待保存记录提交为未对账 | +| 完成对账 | 将勾选的未对账记录结案为已对账 | + +--- + +## 五、单条记录:填什么、怎么算 + +### 5.1 用户可见字段说明 + +| 信息项 | 用户是否填写 | 业务说明 | +|--------|--------------|----------| +| 加氢时间 | 是* | 精确到秒,必填 | +| 加氢日期 | 否 | 由加氢时间自动得出,只读 | +| 加氢站名称 | 是* | 必选 | +| 客户名称 | 是* | 必选,位于加氢站右侧;决定业务员与系统带出信息 | +| 车牌号 | 是* | 必选,须为公司登记车辆 | +| 加氢量(kg) | 是* | 必填,用于算总价 | +| 成本单价 | 是* | 必填,向站点的采购成本价 | +| 成本总价 | 否 | 加氢量 × 成本单价,自动计算 | +| 加氢单价 | 是* | 必填,对客户的销售单价 | +| 加氢总价 | 否 | 加氢量 × 加氢单价,自动计算 | +| 行驶里程 | 否 | 选填,位于加氢总价之后 | +| 备注 | 否 | 自由文本 | +| 业务员 | 否 | 选客户后自动带出 | +| 承担方式 | 是* | 客户承担 / 我司承担 / 客户自行结算 / 其他结算 | +| 对账日期 | 否 | 完成对账等流程带出,只读 | +| 收票日期 | 否 | 由财务/收票模块自动带出,不可编辑 | +| 加氢站付款状态 | 否 | 已付款 / 未付款,由加氢站打款管理带出 | +| 开票日期 | 否 | 由开票模块自动带出 | +| 客户收款状态 | 否 | 已付款 / 未付款,由客户收款模块带出 | +| 开票公司 | 否 | 显示开票公司名称,系统自动带出 | +| 状态 | 否 | 展示「未对账」「已对账」;待保存不显示标签;列在列表末尾 | +| 订单编号 | 否 | 系统按规则自动生成,不可改;列在列表末尾 | + +\\* 保存时校验,缺失则标红提示。 + +### 5.2 订单编号规则(业务口径) + +便于业务与财务对齐、按站按日追溯: + +- 编号以 **JQ** 开头,后接 **加氢站编码**、**加氢日期(6 位)**、**当日流水号(5 位)**。 +- 同一加氢站、同一天内,按加氢时间先后依次编号,每日从 1 号起。 +- 用户无需手填,保存或数据重算时由系统生成。 + +### 5.3 金额计算 + +- **成本总价** = 加氢量 × 成本单价(保留两位小数) +- **加氢总价** = 加氢量 × 加氢单价(保留两位小数) + +任一项未填齐时,对应总价为空,填齐后自动更新。 + +--- + +## 六、谁能改、谁能删 + +### 6.1 按状态的权限(业务员 vs 主管) + +| 数据状态 | 业务员 | 主管 | +|----------|--------|------| +| 待保存(本人录入) | 可直接改表格;可复制、删除 | 同业务员(本期不单独区分待保存) | +| 待保存(他人录入) | 不可见或不可改(按账号权限,上线后明确) | — | +| 未对账 | 需点「编辑」后才能改;可复制、删除;可勾选完成对账 | 同业务员 | +| 已对账 | 不可编辑、不可删除(按钮灰显+提示联系主管) | 可点「编辑」修改;可删除 | + +### 6.2 行内操作说明 + +| 操作 | 适用情况 | 业务说明 | +|------|----------|----------| +| 编辑 | 未对账、已对账(主管) | 进入编辑模式后方可改单元格;待保存无需点编辑 | +| 复制 | 未对账、本人待保存 | 可一次复制 1~100 条相似记录,生成新的待保存行 | +| 删除 | 未对账、本人待保存;已对账仅主管 | 二次确认后删除 | +| 更多 → 变更日志 | 未对账、已对账 | 查看该条历史修改记录 | + +--- + +## 七、异常数据(标黄)— 业务规则 + +### 7.1 什么是「异常」 + +系统在列表中用**黄色背景**提示以下三类情况(不影响保存,但需业务留意): + +| 异常类型 | 判定逻辑(业务语言) | 用户看到的提示 | +|----------|----------------------|----------------| +| 加氢量偏大 | 单次加氢量超过 60 kg | 加氢量疑似有误 | +| 成本单价异常 | 所填成本单价与「该站该时段公司标准成本价」不一致 | 成本单价与系统该时间段不一致 | +| 加氢单价异常 | 所填加氢单价与「该站该时段公司标准对客户价」不一致 | 加氢单价与系统该时间段不一致 | + +标准价随 **加氢站 + 加氢时间** 匹配;若公司未配置该站该时段价格,则不提示单价类异常。 + +### 7.2 「仅显示异常数据」 + +- 默认关闭,展示符合查询条件的全部记录(含待保存例外规则)。 +- 开启后只显示至少有一项异常提示的记录,方便集中核对。 +- **导出不受此开关影响**(见下文)。 + +--- + +## 八、批量能力 + +### 8.1 表头批量改价 / 改结算 + +- 在 **成本单价**、**加氢单价** 列表头可批量填入统一单价,作用于当前可编辑的所有记录(待保存 + 未对账)。 +- 在 **承担方式** 列表头可批量改为四种承担方式之一。 +- **已对账** 记录不参与批量修改。 + +### 8.2 批量导入 + +**适用:** 历史数据、线下表格整理后的批量录入。 + +**流程:** + +1. 点击「批量导入」→ 下载标准模板 +2. 按模板填写(无需填对账日期及收票/开票/付款等系统带出字段) +3. 上传文件 → 系统生成多条 **待保存** 记录 +4. 业务核对列表 → 点击 **保存** 进入未对账 + +**模板包含列:** 加氢时间、加氢站名称、车牌号、加氢量、成本单价、客户名称、加氢单价、承担方式、备注(行驶里程可在备注前按业务需要填写,导入模板列顺序以页面下载为准)。 + +**导入后状态:** 均为待保存,与手工新增一致,须保存后才参与对账与导出。 + +### 8.3 批量导出 + +**适用:** 向财务、客户或内部汇报正式台账。 + +**规则:** + +- 导出范围 = **当前查询条件下** 的 **未对账 + 已对账** 数据。 +- **不包含待保存**(草稿不对外报送)。 +- **不受**「仅显示异常数据」开关限制。 +- 弹窗文案:「请选择导出的列」;支持全选/取消全选;默认勾选全部可导出列。 +- 无符合条件数据时提示:暂无未对账或已对账数据可导出。 + +**可导出列(业务名称):** 序号、状态、订单编号、加氢时间、加氢日期、加氢站名称、车牌号、加氢量、成本单价、成本总价、客户名称、加氢单价、加氢总价、行驶里程、业务员、承担方式、对账日期、备注、收票日期、加氢站付款状态、开票日期、客户收款状态、开票公司。 + +--- + +## 九、变更日志 + +### 9.1 记录什么 + +满足审计与纠纷查证,以下变化需留痕: + +- 保存(待保存 → 未对账) +- 完成对账(未对账 → 已对账) +- 各业务字段修改(前后值不同) +- 新增、复制、批量导入产生记录 + +### 9.2 怎么查看 + +在操作列点击 **「更多」→ 变更日志**,弹窗展示: + +- 修改时间、修改人、修改字段、修改前(红)、修改后(绿) +- 按时间倒序排列 + +--- + +## 十、业务规则汇总(验收必读) + +### 10.1 保存 + +- 一次保存处理页面上**全部**待保存记录。 +- 必填:加氢时间、加氢站、车牌(须为登记车辆)、客户、加氢量、成本单价、加氢单价、承担方式。 +- 校验失败:仅格子标红,无「保存成功」类提示。 + +### 10.2 完成对账 + +- 必须先勾选未对账记录。 +- 仅未对账可勾选。 +- 确认文案强调:完成后需主管才能改。 + +### 10.3 筛选与列表 + +- 查询点击后生效;重置恢复。 +- **顶部合计条** 对当前列表可见行实时汇总;修改筛选并点击「查询」后重新计算;「仅显示异常数据」开启时仅统计可见行。 +- 本人待保存新增行在筛选后仍可见(合计亦包含这些行,若其在列表中展示)。 + +### 10.4 导出 + +- 仅未对账、已对账;遵循查询条件;可自选列。 + +### 10.5 复制 + +- 单次最多 100 条;生成待保存记录。 + +--- + +## 十一、业务验收场景(UAT 建议) + +| 编号 | 场景 | 预期结果 | +|------|------|----------| +| U1 | 新增一行并填齐必填项后保存 | 变为未对账,出现订单编号 | +| U2 | 必填项缺一项点保存 | 对应格标红,状态仍为待保存 | +| U3 | 勾选多条未对账并完成对账 | 变为已对账,业务员不可改删 | +| U4 | 业务员打开已对账记录 | 编辑/删除不可用,有主管联系提示 | +| U5 | 加氢量填 65kg | 单元格标黄,提示加氢量疑似有误 | +| U6 | 单价填与标准价不一致 | 标黄并提示与系统该时段不一致 | +| U7 | 开启「仅显示异常数据」 | 仅异常记录可见,本人新加待保存仍可见 | +| U8 | 设置筛选后导出 | 仅导出未对账+已对账且符合筛选的数据 | +| U9 | 导入模板 5 条后保存 | 5 条均变未对账 | +| U10 | 未对账记录点编辑后改客户 | 业务员、开票信息等随客户更新,变更日志有记录 | + +--- + +## 十二、名词与枚举(业务词典) + +| 名词 | 含义 | +|------|------| +| 待保存 | 草稿,未进入对账流程 | +| 未对账 | 已保存,等待业务确认并完成对账 | +| 已对账 | 对账完成,业务员不可随意改动 | +| 客户承担 | 氢费由客户承担 | +| 我司承担 | 氢费由我司承担 | +| 客户自行结算 | 由客户自行与加氢站等方结算 | +| 其他结算 | 其他结算方式 | +| 加氢站付款状态 | 加氢站侧是否已付款,已付款 / 未付款 | +| 客户收款状态 | 客户侧是否已收款,已付款 / 未付款 | +| 收票日期 | 财务收票日期,系统带出 | +| 标准价 | 公司维护的、按加氢站与生效时段确定的参考单价 | + +--- + +## 十三、版本与协作说明 + +- 本文档描述的是 **车辆氢费明细** 页面当前已对齐的产品行为,供评审、培训与 UAT 使用。 +- 研发实现细节、接口定义、字段编码见单独的技术设计文档(如有)。 +- 需求变更请更新本文档版本号,并在评审纪要中注明影响章节。 + +--- + +**文档结束** +`; + +/** 将 PRD Markdown 排版为 React 节点(车辆氢费明细) */ +function parsePrdInlineText(text) { + var parts = String(text || '').split(/(\*\*[^*]+\*\*)/g); + var nodes = []; + var i; + for (i = 0; i < parts.length; i++) { + var p = parts[i]; + if (!p) continue; + if (p.indexOf('**') === 0 && p.lastIndexOf('**') === p.length - 2) { + nodes.push( + React.createElement('strong', { key: i }, p.slice(2, -2)) + ); + } else { + nodes.push(p); + } + } + return nodes.length === 1 ? nodes[0] : nodes; +} + +function isPrdTableRow(line) { + return /^\|.+\|$/.test(String(line || '').trim()); +} + +function isPrdTableSep(line) { + return /^\|[\s\-:|]+\|$/.test(String(line || '').trim()); +} + +function renderPrdTableRow(line, rowKey, isHeader) { + var cells = String(line) + .trim() + .replace(/^\|/, '') + .replace(/\|$/, '') + .split('|') + .map(function (c) { + return c.trim(); + }); + return React.createElement( + 'tr', + { key: rowKey }, + cells.map(function (cell, ci) { + var Tag = isHeader ? 'th' : 'td'; + return React.createElement( + Tag, + { + key: ci, + style: { + border: '1px solid #e5e7eb', + padding: '8px 10px', + textAlign: 'left', + verticalAlign: 'top', + fontWeight: isHeader ? 600 : 400, + background: isHeader ? '#f8fafc' : '#fff', + fontSize: 13, + lineHeight: 1.5 + } + }, + parsePrdInlineText(cell) + ); + }) + ); +} + +function renderPrdMarkdown(markdown) { + var lines = String(markdown || '').split(/\r?\n/); + var nodes = []; + var i = 0; + var inCode = false; + var codeBuf = []; + + while (i < lines.length) { + var line = lines[i]; + var trimmed = String(line || '').trim(); + + if (trimmed.indexOf('```') === 0) { + if (inCode) { + nodes.push( + React.createElement( + 'pre', + { + key: 'code-' + i, + style: { + margin: '12px 0', + padding: '12px 14px', + background: '#f6f8fa', + border: '1px solid #e5e7eb', + borderRadius: 8, + fontSize: 12, + lineHeight: 1.6, + overflow: 'auto', + color: '#334155', + whiteSpace: 'pre-wrap' + } + }, + codeBuf.join('\n') + ) + ); + codeBuf = []; + inCode = false; + } else { + inCode = true; + } + i += 1; + continue; + } + + if (inCode) { + codeBuf.push(line); + i += 1; + continue; + } + + if (trimmed === '---') { + nodes.push( + React.createElement('hr', { + key: 'hr-' + i, + style: { border: 'none', borderTop: '1px solid #e8ecf0', margin: '20px 0' } + }) + ); + i += 1; + continue; + } + + if (isPrdTableRow(trimmed)) { + var tableLines = []; + while (i < lines.length && isPrdTableRow(String(lines[i]).trim())) { + tableLines.push(String(lines[i]).trim()); + i += 1; + } + var bodyRows = []; + var ti; + for (ti = 0; ti < tableLines.length; ti++) { + if (isPrdTableSep(tableLines[ti])) continue; + bodyRows.push( + renderPrdTableRow(tableLines[ti], 'tr-' + i + '-' + ti, ti === 0) + ); + } + if (bodyRows.length) { + nodes.push( + React.createElement( + 'div', + { + key: 'tbl-' + i, + style: { overflowX: 'auto', margin: '12px 0 16px' } + }, + React.createElement( + 'table', + { + style: { + width: '100%', + borderCollapse: 'collapse', + fontSize: 13 + } + }, + React.createElement('tbody', null, bodyRows) + ) + ) + ); + } + continue; + } + + if (!trimmed) { + i += 1; + continue; + } + + if (trimmed.indexOf('# ') === 0) { + nodes.push( + React.createElement( + 'h1', + { + key: 'h1-' + i, + style: { + fontSize: 20, + fontWeight: 700, + color: '#0f172a', + margin: '0 0 16px', + lineHeight: 1.35 + } + }, + parsePrdInlineText(trimmed.slice(2).trim()) + ) + ); + i += 1; + continue; + } + + if (trimmed.indexOf('## ') === 0) { + nodes.push( + React.createElement( + 'h2', + { + key: 'h2-' + i, + style: { + fontSize: 16, + fontWeight: 700, + color: '#1e293b', + margin: '24px 0 12px', + paddingBottom: 6, + borderBottom: '2px solid #e0f2fe', + lineHeight: 1.4 + } + }, + parsePrdInlineText(trimmed.slice(3).trim()) + ) + ); + i += 1; + continue; + } + + if (trimmed.indexOf('### ') === 0) { + nodes.push( + React.createElement( + 'h3', + { + key: 'h3-' + i, + style: { + fontSize: 14, + fontWeight: 600, + color: '#334155', + margin: '16px 0 8px', + lineHeight: 1.45 + } + }, + parsePrdInlineText(trimmed.slice(4).trim()) + ) + ); + i += 1; + continue; + } + + if (trimmed === '**文档结束**') { + nodes.push( + React.createElement( + 'div', + { + key: 'end-' + i, + style: { + marginTop: 24, + paddingTop: 16, + borderTop: '1px dashed #e2e8f0', + color: '#94a3b8', + fontSize: 13, + textAlign: 'center' + } + }, + '— 文档结束 —' + ) + ); + i += 1; + continue; + } + + if (/^\d+\.\s/.test(trimmed)) { + nodes.push( + React.createElement( + 'div', + { + key: 'ol-' + i, + style: { + fontSize: 13, + color: '#475569', + lineHeight: 1.75, + margin: '6px 0 6px 4px', + paddingLeft: 4 + } + }, + parsePrdInlineText(trimmed) + ) + ); + i += 1; + continue; + } + + if (trimmed.indexOf('- ') === 0) { + nodes.push( + React.createElement( + 'div', + { + key: 'ul-' + i, + style: { + display: 'flex', + gap: 8, + fontSize: 13, + color: '#475569', + lineHeight: 1.75, + margin: '4px 0 4px 2px' + } + }, + React.createElement('span', { style: { color: '#1677ff', flexShrink: 0 } }, '•'), + React.createElement('span', { style: { flex: 1 } }, parsePrdInlineText(trimmed.slice(2).trim())) + ) + ); + i += 1; + continue; + } + + nodes.push( + React.createElement( + 'p', + { + key: 'p-' + i, + style: { + fontSize: 13, + color: '#475569', + lineHeight: 1.75, + margin: '6px 0' + } + }, + parsePrdInlineText(trimmed) + ) + ); + i += 1; + } + + return nodes; +} + +function renderH2RequirementDocPanel() { + var md = + typeof H2_LEDGER_REQUIREMENT_DOC !== 'undefined' && H2_LEDGER_REQUIREMENT_DOC + ? H2_LEDGER_REQUIREMENT_DOC + : ''; + if (!md) { + return React.createElement( + 'div', + { style: { padding: 24, color: '#64748b', textAlign: 'center' } }, + '需求文档未加载,请确认已引入同目录文件:车辆氢费明细-需求内容.js' + ); + } + return React.createElement( + 'div', + { className: 'h2-req-doc-panel', style: { padding: '4px 4px 16px' } }, + renderPrdMarkdown(md) + ); +} + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + var useRef = React.useRef; + + var antd = window.antd; + var App = antd.App; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Button = antd.Button; + var Table = antd.Table; + var Select = antd.Select; + var DatePicker = antd.DatePicker; + var Input = antd.Input; + var InputNumber = antd.InputNumber; + var Row = antd.Row; + var Col = antd.Col; + var Space = antd.Space; + var Tag = antd.Tag; + var Popconfirm = antd.Popconfirm; + var Popover = antd.Popover; + var Tooltip = antd.Tooltip; + var Modal = antd.Modal; + var Upload = antd.Upload; + var Checkbox = antd.Checkbox; + var Dropdown = antd.Dropdown; + var message = antd.message; + + var WARN_HYDROGEN_KG_TIP = '加氢量疑似有误'; + var WARN_COST_PRICE_TIP = '成本单价与系统该时间段不一致'; + var WARN_CUSTOMER_PRICE_TIP = '加氢单价与系统该时间段不一致'; + var WARN_CELL_WRAP_STYLE = { background: '#fffbe6', border: '1px solid #ffe58f', borderRadius: 4, display: 'block', width: '100%' }; + + var CURRENT_USER = { id: 'u_biz_li', name: '李业务', role: 'sales' }; + + var RECONCILE_DRAFT = 'draft'; + var RECONCILE_PENDING = 'pending'; + var RECONCILE_RECONCILED = 'reconciled'; + + var VEHICLE_INFO_TABLE = [ + '浙AD12345F', + '浙AH55660F', + '浙BK33210F', + '粤BK33210F', + '沪AD12345F', + '苏EF99887F', + '京CN88771F', + '川AL55602F' + ]; + + var HYDROGEN_STATION_LIST = [ + { value: '000082', label: '广安弘正能源官盛综合能源站', stationCode: '000082' }, + { value: '000045', label: '九江城区城西港加油站', stationCode: '000045' }, + { value: '000074', label: '金瑞汽油柴油LNG加油加气站', stationCode: '000074' }, + { value: 'HS000042', label: '中国石化南二环油电氢合建站', stationCode: 'HS000042' }, + { value: '000077', label: '凯豪达氢能源加氢站', stationCode: '000077' }, + { value: 'HS000053', label: '四周能源云浮新区加氢站', stationCode: 'HS000053' }, + { value: '000091', label: '重庆双溪加氢站', stationCode: '000091' }, + { value: 'HS000041', label: '宿迁沭阳开发区加氢站', stationCode: 'HS000041' }, + { value: '000062', label: '滨化加氢站', stationCode: '000062' }, + { value: 'HS000054', label: '中国石化绿能加气加氢站', stationCode: 'HS000054' }, + { value: '000067', label: '中国石油天然气股份有限公司四川成都销售分公司郫都区古城加油站', stationCode: '000067' }, + { value: '000088', label: '永川高升加氢站', stationCode: '000088' }, + { value: '000058', label: '中国石化青云店气能加油加气站', stationCode: '000058' }, + { value: 'HS000037', label: '佛罗路加氢站', stationCode: 'HS000037' }, + { value: '000068', label: '丹灶海德利森加氢站', stationCode: '000068' }, + { value: '000059', label: '神华加氢站', stationCode: '000059' }, + { value: 'HS000040', label: '扬州文昌西路站', stationCode: 'HS000040' }, + { value: '000086', label: '涪陵长寿化中大道加能站', stationCode: '000086' }, + { value: '000070', label: '中国石化龙珠源加油加气站', stationCode: '000070' }, + { value: 'HS000030', label: '大兴国际氢能示范区海珀尔加氢站', stationCode: 'HS000030' }, + { value: 'HS000038', label: '普特氢能加氢站', stationCode: 'HS000038' }, + { value: 'HS000043', label: '淄博齐鲁加氢站', stationCode: 'HS000043' }, + { value: '000048', label: '南京溧水柘塘东站', stationCode: '000048' }, + { value: '000063', label: '张家港公交保养场站', stationCode: '000063' }, + { value: '000073', label: '河北欣聚元氢能科技有限公司加氢站', stationCode: '000073' }, + { value: 'HS000035', label: '广州联新氢能东晖加氢站', stationCode: 'HS000035' }, + { value: 'HS000048', label: '如皋神华加氢站', stationCode: 'HS000048' }, + { value: 'HS000059', label: '佛山南海羚牛加氢站', stationCode: 'HS000059' }, + { value: '000085', label: '江南空港加气站', stationCode: '000085' }, + { value: '000078', label: '中国石化江南半山环道加能站', stationCode: '000078' }, + { value: '000090', label: '成都天府机场高速北站', stationCode: '000090' }, + { value: 'HS000057', label: '重庆元琨双宝氢能综合能源站', stationCode: 'HS000057' }, + { value: '000080', label: '重庆龙洲湾氢能综合能源站', stationCode: '000080' }, + { value: '000089', label: '成都天府机场高速南站', stationCode: '000089' }, + { value: 'HS000034', label: '中国石油时顺苑加油加氢站', stationCode: 'HS000034' }, + { value: 'HS000039', label: '海盐JUPITER厂内加氢站', stationCode: 'HS000039' }, + { value: '000081', label: '成都博能燃气综合能源站', stationCode: '000081' }, + { value: 'HS000049', label: '东明三路综合能源站', stationCode: 'HS000049' }, + { value: '000053', label: '武汉革新大道加油站', stationCode: '000053' }, + { value: '000052', label: '强劲荔村加氢站', stationCode: '000052' }, + { value: '000043', label: '豪汇综合能源站', stationCode: '000043' }, + { value: 'HS000033', label: '氢源嘉创东方氢港加氢站', stationCode: 'HS000033' }, + { value: 'HS000052', label: '中国石化滨文路CNG加气站', stationCode: 'HS000052' }, + { value: '000044', label: '佛山青龙加油站', stationCode: '000044' }, + { value: '000056', label: '绕城大道加气加氢加油站', stationCode: '000056' }, + { value: '000049', label: '宁波镇海区中国石化加氢站', stationCode: '000049' }, + { value: 'HS000050', label: '硕放加氢站', stationCode: 'HS000050' }, + { value: 'HS000047', label: '盐城创咏加氢站', stationCode: 'HS000047' }, + { value: 'HS000051', label: '成都华通加氢站', stationCode: 'HS000051' }, + { value: 'HS000045', label: '佛山佛西加气站', stationCode: 'HS000045' }, + { value: '000075', label: '河南焦煤能源有限公司演马庄矿冯营加氢站', stationCode: '000075' }, + { value: '000087', label: '高管汉宜潜江服务北站', stationCode: '000087' }, + { value: 'HS000081', label: '武汉群力加油站', stationCode: 'HS000081' }, + { value: '000069', label: '中国石化广州开泰北加油加氢站', stationCode: '000069' }, + { value: '000072', label: '中国石化广州金坑加氢站', stationCode: '000072' }, + { value: 'HS000007', label: '上海嘉氢实业加氢站(江桥重塑)', stationCode: 'HS000007' }, + { value: 'HS000082', label: '武汉雄众加氢站', stationCode: 'HS000082' }, + { value: 'HS000083', label: '中国石化青岛炼油化工有限责任公司', stationCode: 'HS000083' }, + { value: 'HS000084', label: '皖能集团龙塘综合能源港(CNG、H2、充换电)', stationCode: 'HS000084' }, + { value: 'HS000085', label: '佛燃', stationCode: 'HS000085' }, + { value: 'HS000086', label: '空气化工产品(浙江)有限公司海盐经济开发区加氢站', stationCode: 'HS000086' }, + { value: 'HS000087', label: '国家电投宁波慈溪加氢站', stationCode: 'HS000087' }, + { value: 'HS000088', label: '诚志空气产品银河路加氢站', stationCode: 'HS000088' }, + { value: 'HS000089', label: '乌鲁木齐市隆盛达环保科技有限公司沙坪加油加气加氢充电站', stationCode: 'HS000089' }, + { value: 'HS000090', label: '茄子岗综合能源站', stationCode: 'HS000090' }, + { value: 'HS000091', label: '中国石化江南西彭综合能源站', stationCode: 'HS000091' }, + { value: 'HS000092', label: '昆山市中国石油昆山花桥加油站(花安路北50米)', stationCode: 'HS000092' }, + { value: 'HS000093', label: '武汉中石化双龙站', stationCode: 'HS000093' }, + { value: 'HS000094', label: '重庆丝路加氢站', stationCode: 'HS000094' }, + { value: 'HS000095', label: '超亿LNG-CNG加气站-H2加氢站', stationCode: 'HS000095' }, + { value: 'HS000096', label: '松江九亭加油站', stationCode: 'HS000096' }, + { value: 'HS000017', label: '中国石化张家港朝阳加能站', stationCode: 'HS000017' }, + { value: 'HS000097', label: 'K零售第六十二站', stationCode: 'HS000097' }, + { value: 'HS000098', label: '沪实油气站', stationCode: 'HS000098' }, + { value: 'HS000099', label: '洛阳新红山加油站', stationCode: 'HS000099' }, + { value: 'HS000100', label: 'K闵茂加油站', stationCode: 'HS000100' }, + { value: 'HS000101', label: '虹井加油站', stationCode: 'HS000101' }, + { value: 'HS000102', label: '幸福加油站', stationCode: 'HS000102' }, + { value: 'HS000103', label: '佛山南海金三角加油站', stationCode: 'HS000103' }, + { value: 'HS000104', label: '虹光经营部', stationCode: 'HS000104' }, + { value: 'HS000105', label: '江阴河塘站', stationCode: 'HS000105' }, + { value: 'HS000106', label: '安徽高速清溪南站', stationCode: 'HS000106' }, + { value: 'HS000107', label: '嘉定解放岛加油站', stationCode: 'HS000107' }, + { value: 'HS000108', label: '闵行航华加油站', stationCode: 'HS000108' }, + { value: 'HS000109', label: 'K祁山加油站', stationCode: 'HS000109' }, + { value: 'HS000110', label: '嘉定曹王加油站', stationCode: 'HS000110' }, + { value: 'HS000111', label: '徐州铜山兴华站', stationCode: 'HS000111' }, + { value: 'HS000112', label: '广州交投沙贝南加油站', stationCode: 'HS000112' }, + { value: 'HS000113', label: '肇庆四会龙甫加油站', stationCode: 'HS000113' }, + { value: 'HS000016', label: '东莞沙田大道综合能源服务站', stationCode: 'HS000016' }, + { value: 'HS000114', label: '佛山新城加油站', stationCode: 'HS000114' }, + { value: 'HS000002', label: '上海浦江加氢站', stationCode: 'HS000002' }, + { value: 'HS000009', label: '中石化西上海发展站加油站', stationCode: 'HS000009' }, + { value: 'HS000011', label: '中国石化站前路加气加氢站', stationCode: 'HS000011' }, + { value: 'HS000003', label: '上海安亭加氢站', stationCode: 'HS000003' }, + { value: 'HS000004', label: '常熟嘉化氢能加氢站', stationCode: 'HS000004' }, + { value: 'HS000012', label: '中国石化善通加油加氢站', stationCode: 'HS000012' }, + { value: 'HS000008', label: '驿蓝能源加氢站', stationCode: 'HS000008' }, + { value: 'HS000018', label: '韶钢产业园加氢站', stationCode: 'HS000018' }, + { value: 'HS000006', label: '上海临港平霄路油氢合建站', stationCode: 'HS000006' }, + { value: 'HS000001', label: '嘉兴滨海大道加油加气站', stationCode: 'HS000001' }, + { value: 'HS000014', label: '嘉兴嘉燃经开站', stationCode: 'HS000014' }, + { value: 'HS000013', label: '嘉兴嘉锦亭桥北综合供能服务站', stationCode: 'HS000013' }, + { value: 'HS000005', label: '江苏嘉化氢能港城加氢站', stationCode: 'HS000005' }, + { value: 'HS000010', label: '中石化青卫油氢混合站', stationCode: 'HS000010' }, + ]; + + var STATION_SYSTEM_PRICE_MAP = { + '000043': { costPrice: 35.0, costPriceEffective: '2025-11-01', customerPrice: 35.0, customerPriceEffective: '2025-11-01' }, + '000044': { costPrice: 35.0, costPriceEffective: '2025-11-01', customerPrice: 35.0, customerPriceEffective: '2025-11-01' }, + '000045': { costPrice: 25.0, costPriceEffective: '2025-11-01', customerPrice: 25.0, customerPriceEffective: '2025-11-01' }, + '000048': { costPrice: 60.0, costPriceEffective: '2025-11-01', customerPrice: 60.0, customerPriceEffective: '2025-11-01' }, + '000049': { costPrice: 35.0, costPriceEffective: '2025-11-01', customerPrice: 35.0, customerPriceEffective: '2025-11-01' }, + '000052': { costPrice: 40.0, costPriceEffective: '2025-11-01', customerPrice: 40.0, customerPriceEffective: '2025-11-01' }, + '000053': { costPrice: 35.0, costPriceEffective: '2025-11-01', customerPrice: 35.0, customerPriceEffective: '2025-11-01' }, + '000056': { costPrice: 30.0, costPriceEffective: '2025-10-31', customerPrice: 30.0, customerPriceEffective: '2025-10-31' }, + '000058': { costPrice: 30.0, costPriceEffective: '2025-01-01', customerPrice: 30.0, customerPriceEffective: '2025-01-01' }, + '000059': { costPrice: 35.0, costPriceEffective: '2025-01-01', customerPrice: 35.0, customerPriceEffective: '2025-01-01' }, + '000062': { costPrice: 25.0, costPriceEffective: '2025-01-01', customerPrice: 25.0, customerPriceEffective: '2025-01-01' }, + '000063': { costPrice: 55.0, costPriceEffective: '2025-01-01', customerPrice: 55.0, customerPriceEffective: '2025-01-01' }, + '000067': { costPrice: 30.0, costPriceEffective: '2025-01-01', customerPrice: 30.0, customerPriceEffective: '2025-01-01' }, + '000068': { costPrice: 44.0, costPriceEffective: '2025-01-01', customerPrice: 44.0, customerPriceEffective: '2025-01-01' }, + '000069': { costPrice: 35.0, costPriceEffective: '2025-01-01', customerPrice: 35.0, customerPriceEffective: '2025-01-01' }, + '000070': { costPrice: 40.0, costPriceEffective: '2025-01-01', customerPrice: 40.0, customerPriceEffective: '2025-01-01' }, + '000072': { costPrice: 35.0, costPriceEffective: '2025-01-01', customerPrice: 35.0, customerPriceEffective: '2025-01-01' }, + '000073': { costPrice: 21.0, costPriceEffective: '2025-01-01', customerPrice: 21.0, customerPriceEffective: '2025-01-01' }, + '000074': { costPrice: 25.0, costPriceEffective: '2025-01-01', customerPrice: 25.0, customerPriceEffective: '2025-01-01' }, + '000075': { costPrice: 35.0, costPriceEffective: '2025-01-01', customerPrice: 35.0, customerPriceEffective: '2025-01-01' }, + '000077': { costPrice: 35.0, costPriceEffective: '2026-01-01', customerPrice: 35.0, customerPriceEffective: '2026-01-01' }, + '000078': { costPrice: 25.0, costPriceEffective: '2026-01-01', customerPrice: 25.0, customerPriceEffective: '2026-01-01' }, + '000080': { costPrice: 25.0, costPriceEffective: '2026-01-01', customerPrice: 25.0, customerPriceEffective: '2026-01-01' }, + '000081': { costPrice: 29.9, costPriceEffective: '2026-01-01', customerPrice: 29.9, customerPriceEffective: '2026-01-01' }, + '000082': { costPrice: 30.0, costPriceEffective: '2026-02-28', customerPrice: 30.0, customerPriceEffective: '2026-02-28' }, + '000085': { costPrice: 25.0, costPriceEffective: '2026-03-01', customerPrice: 25.0, customerPriceEffective: '2026-03-01' }, + '000086': { costPrice: 25.0, costPriceEffective: '2026-03-01', customerPrice: 25.0, customerPriceEffective: '2026-03-01' }, + '000087': { costPrice: 35.0, costPriceEffective: '2026-03-01', customerPrice: 35.0, customerPriceEffective: '2026-03-01' }, + '000088': { costPrice: 25.0, costPriceEffective: '2026-03-01', customerPrice: 25.0, customerPriceEffective: '2026-03-01' }, + '000089': { costPrice: 29.99, costPriceEffective: '2026-03-01', customerPrice: 29.99, customerPriceEffective: '2026-03-01' }, + '000090': { costPrice: 29.99, costPriceEffective: '2026-03-01', customerPrice: 29.99, customerPriceEffective: '2026-03-01' }, + '000091': { costPrice: 25.0, costPriceEffective: '2026-03-01', customerPrice: 25.0, customerPriceEffective: '2026-03-01' }, + 'HS000001': { costPrice: 35.0, costPriceEffective: '2025-01-01', customerPrice: 35.0, customerPriceEffective: '2025-01-01' }, + 'HS000002': { costPrice: 35.0, costPriceEffective: '2025-01-01', customerPrice: 35.0, customerPriceEffective: '2025-01-01' }, + 'HS000003': { costPrice: 58.0, costPriceEffective: '2025-01-01', customerPrice: 58.0, customerPriceEffective: '2025-01-01' }, + 'HS000004': { costPrice: 50.0, costPriceEffective: '2025-01-01', customerPrice: 50.0, customerPriceEffective: '2025-01-01' }, + 'HS000005': { costPrice: 50.0, costPriceEffective: '2025-01-01', customerPrice: 50.0, customerPriceEffective: '2025-01-01' }, + 'HS000006': { costPrice: 55.0, costPriceEffective: '2026-01-01', customerPrice: 55.0, customerPriceEffective: '2026-01-01' }, + 'HS000007': { costPrice: 55.0, costPriceEffective: '2025-01-01', customerPrice: 55.0, customerPriceEffective: '2025-01-01' }, + 'HS000008': { costPrice: 55.0, costPriceEffective: '2025-01-01', customerPrice: 55.0, customerPriceEffective: '2025-01-01' }, + 'HS000009': { costPrice: 58.0, costPriceEffective: '2025-01-01', customerPrice: 58.0, customerPriceEffective: '2025-01-01' }, + 'HS000010': { costPrice: 58.0, costPriceEffective: '2025-01-01', customerPrice: 58.0, customerPriceEffective: '2025-01-01' }, + 'HS000011': { costPrice: 36.0, costPriceEffective: '2025-01-01', customerPrice: 36.0, customerPriceEffective: '2025-01-01' }, + 'HS000012': { costPrice: 36.0, costPriceEffective: '2025-01-01', customerPrice: 36.0, customerPriceEffective: '2025-01-01' }, + 'HS000013': { costPrice: 26.0, costPriceEffective: '2025-01-01', customerPrice: 35.0, customerPriceEffective: '2025-01-01' }, + 'HS000014': { costPrice: 35.0, costPriceEffective: '2025-01-01', customerPrice: 35.0, customerPriceEffective: '2025-01-01' }, + 'HS000016': { costPrice: 45.0, costPriceEffective: '2023-04-25', customerPrice: 65.0, customerPriceEffective: '2023-04-25' }, + 'HS000017': { costPrice: 48.0, costPriceEffective: '2025-01-01', customerPrice: 48.0, customerPriceEffective: '2025-01-01' }, + 'HS000018': { costPrice: 35.0, costPriceEffective: '2025-01-01', customerPrice: 35.0, customerPriceEffective: '2025-01-01' }, + 'HS000030': { costPrice: 30.0, costPriceEffective: '2025-01-01', customerPrice: 30.0, customerPriceEffective: '2025-01-01' }, + 'HS000033': { costPrice: 35.0, costPriceEffective: '2025-01-01', customerPrice: 35.0, customerPriceEffective: '2025-01-01' }, + 'HS000034': { costPrice: 30.0, costPriceEffective: '2025-01-01', customerPrice: 30.0, customerPriceEffective: '2025-01-01' }, + 'HS000035': { costPrice: 40.0, costPriceEffective: '2025-01-01', customerPrice: 40.0, customerPriceEffective: '2025-01-01' }, + 'HS000037': { costPrice: 40.0, costPriceEffective: '2024-10-01', customerPrice: 40.0, customerPriceEffective: '2025-12-31' }, + 'HS000038': { costPrice: 35.0, costPriceEffective: '2025-01-01', customerPrice: 40.0, customerPriceEffective: '2025-12-31' }, + 'HS000039': { costPrice: 35.0, costPriceEffective: '2025-01-01', customerPrice: 35.0, customerPriceEffective: '2025-01-01' }, + 'HS000040': { costPrice: 46.0, costPriceEffective: '2025-01-01', customerPrice: 46.0, customerPriceEffective: '2025-01-01' }, + 'HS000041': { costPrice: 43.0, costPriceEffective: '2024-12-01', customerPrice: 43.0, customerPriceEffective: '2024-12-01' }, + 'HS000042': { costPrice: 35.0, costPriceEffective: '2024-12-01', customerPrice: 35.0, customerPriceEffective: '2024-12-01' }, + 'HS000043': { costPrice: 33.6, costPriceEffective: '2024-12-01', customerPrice: 33.6, customerPriceEffective: '2024-12-01' }, + 'HS000045': { costPrice: 38.0, costPriceEffective: '2025-06-01', customerPrice: 38.0, customerPriceEffective: '2025-06-01' }, + 'HS000047': { costPrice: 76.0, costPriceEffective: '2025-05-26', customerPrice: 76.0, customerPriceEffective: '2025-05-26' }, + 'HS000048': { costPrice: 35.0, costPriceEffective: '2025-05-27', customerPrice: 35.0, customerPriceEffective: '2025-05-26' }, + 'HS000049': { costPrice: 35.0, costPriceEffective: '2025-04-27', customerPrice: 35.0, customerPriceEffective: '2025-05-25' }, + 'HS000050': { costPrice: 65.0, costPriceEffective: '2025-05-25', customerPrice: 65.0, customerPriceEffective: '2025-05-25' }, + 'HS000051': { costPrice: 30.0, costPriceEffective: '2025-05-25', customerPrice: 30.0, customerPriceEffective: '2025-05-25' }, + 'HS000052': { costPrice: 35.0, costPriceEffective: '2025-05-25', customerPrice: 35.0, customerPriceEffective: '2025-05-25' }, + 'HS000053': { costPrice: 35.0, costPriceEffective: '2025-05-25', customerPrice: 35.0, customerPriceEffective: '2025-05-25' }, + 'HS000054': { costPrice: 35.0, costPriceEffective: '2025-08-01', customerPrice: 35.0, customerPriceEffective: '2025-08-01' }, + 'HS000057': { costPrice: 25.0, costPriceEffective: '2025-10-01', customerPrice: 25.0, customerPriceEffective: '2025-10-01' }, + 'HS000059': { costPrice: 35.0, costPriceEffective: '2025-09-01', customerPrice: 35.0, customerPriceEffective: '2025-09-01' }, + 'HS000081': { costPrice: 25.0, costPriceEffective: '2026-05-14', customerPrice: 0.0, customerPriceEffective: '2020-01-01' }, + 'HS000082': { costPrice: 25.0, costPriceEffective: '2026-05-14', customerPrice: 0.0, customerPriceEffective: '2020-01-01' }, + 'HS000083': { costPrice: 35.0, costPriceEffective: '2026-05-14', customerPrice: 0.0, customerPriceEffective: '2020-01-01' }, + 'HS000084': { costPrice: 25.0, costPriceEffective: '2026-05-14', customerPrice: 0.0, customerPriceEffective: '2020-01-01' }, + 'HS000085': { costPrice: 0.0, costPriceEffective: '2020-01-01', customerPrice: 0.0, customerPriceEffective: '2020-01-01' }, + 'HS000086': { costPrice: 35.0, costPriceEffective: '2025-01-01', customerPrice: 35.0, customerPriceEffective: '2025-01-01' }, + 'HS000087': { costPrice: 35.0, costPriceEffective: '2025-10-01', customerPrice: 35.0, customerPriceEffective: '2025-10-01' }, + 'HS000088': { costPrice: 45.0, costPriceEffective: '2025-10-01', customerPrice: 45.0, customerPriceEffective: '2025-10-01' }, + 'HS000089': { costPrice: 32.0, costPriceEffective: '2025-01-01', customerPrice: 32.0, customerPriceEffective: '2025-01-01' }, + 'HS000090': { costPrice: 35.0, costPriceEffective: '2025-01-01', customerPrice: 35.0, customerPriceEffective: '2025-01-01' }, + 'HS000091': { costPrice: 25.0, costPriceEffective: '2026-01-01', customerPrice: 25.0, customerPriceEffective: '2026-01-01' }, + 'HS000092': { costPrice: 40.0, costPriceEffective: '2024-06-01', customerPrice: 45.0, customerPriceEffective: '2024-06-01' }, + 'HS000093': { costPrice: 25.0, costPriceEffective: '2026-03-01', customerPrice: 25.0, customerPriceEffective: '2026-03-01' }, + 'HS000094': { costPrice: 25.0, costPriceEffective: '2025-01-01', customerPrice: 25.0, customerPriceEffective: '2025-01-01' }, + 'HS000095': { costPrice: 25.0, costPriceEffective: '2026-01-01', customerPrice: 35.0, customerPriceEffective: '2026-01-01' }, + 'HS000096': { costPrice: 0.0, costPriceEffective: '2020-01-01', customerPrice: 0.0, customerPriceEffective: '2020-01-01' }, + 'HS000097': { costPrice: 0.0, costPriceEffective: '2020-01-01', customerPrice: 0.0, customerPriceEffective: '2020-01-01' }, + 'HS000098': { costPrice: 0.0, costPriceEffective: '2020-01-01', customerPrice: 0.0, customerPriceEffective: '2020-01-01' }, + 'HS000099': { costPrice: 0.0, costPriceEffective: '2020-01-01', customerPrice: 0.0, customerPriceEffective: '2020-01-01' }, + 'HS000100': { costPrice: 0.0, costPriceEffective: '2020-01-01', customerPrice: 0.0, customerPriceEffective: '2020-01-01' }, + 'HS000101': { costPrice: 0.0, costPriceEffective: '2020-01-01', customerPrice: 0.0, customerPriceEffective: '2020-01-01' }, + 'HS000102': { costPrice: 0.0, costPriceEffective: '2020-01-01', customerPrice: 0.0, customerPriceEffective: '2020-01-01' }, + 'HS000103': { costPrice: 0.0, costPriceEffective: '2020-01-01', customerPrice: 0.0, customerPriceEffective: '2020-01-01' }, + 'HS000104': { costPrice: 0.0, costPriceEffective: '2020-01-01', customerPrice: 0.0, customerPriceEffective: '2020-01-01' }, + 'HS000105': { costPrice: 0.0, costPriceEffective: '2020-01-01', customerPrice: 0.0, customerPriceEffective: '2020-01-01' }, + 'HS000106': { costPrice: 0.0, costPriceEffective: '2020-01-01', customerPrice: 0.0, customerPriceEffective: '2020-01-01' }, + 'HS000107': { costPrice: 0.0, costPriceEffective: '2020-01-01', customerPrice: 0.0, customerPriceEffective: '2020-01-01' }, + 'HS000108': { costPrice: 0.0, costPriceEffective: '2020-01-01', customerPrice: 0.0, customerPriceEffective: '2020-01-01' }, + 'HS000109': { costPrice: 0.0, costPriceEffective: '2020-01-01', customerPrice: 0.0, customerPriceEffective: '2020-01-01' }, + 'HS000110': { costPrice: 0.0, costPriceEffective: '2020-01-01', customerPrice: 0.0, customerPriceEffective: '2020-01-01' }, + 'HS000111': { costPrice: 0.0, costPriceEffective: '2020-01-01', customerPrice: 0.0, customerPriceEffective: '2020-01-01' }, + 'HS000112': { costPrice: 0.0, costPriceEffective: '2020-01-01', customerPrice: 0.0, customerPriceEffective: '2020-01-01' }, + 'HS000113': { costPrice: 0.0, costPriceEffective: '2020-01-01', customerPrice: 0.0, customerPriceEffective: '2020-01-01' }, + 'HS000114': { costPrice: 0.0, costPriceEffective: '2020-01-01', customerPrice: 0.0, customerPriceEffective: '2020-01-01' }, + }; + var CUSTOMER_LIST = [ + { value: '00317', label: '嘉兴市鑫峤供应链科技有限公司', salesperson: '董剑煜' }, + { value: '00316', label: '嘉兴琳曜供应链管理有限公司', salesperson: '刘念念' }, + { value: '00315', label: '苏州睿幄物流有限公司', salesperson: '董剑煜' }, + { value: '00314', label: '上海恒建智运供应链管理有限公司', salesperson: '金可鹏' }, + { value: '00313', label: '扬州力骏物流有限公司', salesperson: '董剑煜' }, + { value: '00312', label: '上海岚通国际物流有限公司', salesperson: '董剑煜' }, + { value: '00311', label: '上海川渝之星物流有限公司', salesperson: '金可鹏' }, + { value: '00310', label: '广东新正东能源有限公司', salesperson: '金可鹏' }, + { value: '00309', label: '佛山市晋膳美食品供应链有限公司', salesperson: '岑彦' }, + { value: '00308', label: '上海子初供应链管理有限公司', salesperson: '赵连飞' }, + { value: '00307', label: '广州玉川物流有限公司', salesperson: '金可鹏' }, + { value: '00306', label: '杭州冠泽物流有限公司', salesperson: '董剑煜' }, + { value: '00305', label: '广州市晨康贸易发展有限公司', salesperson: '吴纬涛' }, + { value: '00304', label: '广州新运多租赁有限公司', salesperson: '金可鹏' }, + { value: '00303', label: '黑龙江圣合商贸有限公司新疆分公司', salesperson: '尚建华' }, + { value: '00302', label: '广东明新供应链有限公司', salesperson: '岑彦' }, + { value: '00244', label: '上海壹陆捌捌国际物流有限公司', salesperson: '金可鹏' }, + { value: '00279', label: '山东聚隆供应链管理有限公司', salesperson: '金可鹏' }, + { value: '00301', label: '广州雪萌食品有限公司', salesperson: '伍仲文' }, + { value: '00300', label: '重庆渝怀供应链有限公司', salesperson: '吴纬涛' }, + { value: '00202', label: '邓振雄', salesperson: '金可鹏' }, + { value: '00230', label: '深圳市明浩货运代理有限公司', salesperson: '金可鹏' }, + { value: '00272', label: '武汉洛安供应链有限公司', salesperson: '金可鹏' }, + { value: '00282', label: '广州市博众供应链管理有限公司', salesperson: '金可鹏' }, + { value: '00293', label: '广东云通供应链有限公司', salesperson: '金可鹏' }, + { value: '00295', label: '广州壹心膳食管理有限公司', salesperson: '金可鹏' }, + { value: '00291', label: '武汉至上云合供应链管理有限公司', salesperson: '金可鹏' }, + { value: '00268', label: '陕西裕新物流有限公司', salesperson: '金可鹏' }, + { value: '00259', label: '上海冉酷物流有限公司', salesperson: '金可鹏' }, + { value: '00299', label: '成都易达创想物流科技有限责任公司', salesperson: '秦挺' }, + { value: '00298', label: '嘉兴摩格嘉伟物流有限公司', salesperson: '陈高伟' }, + { value: '00297', label: '广州腾通物流有限公司', salesperson: '钟祥' }, + { value: '00296', label: '韶关市金诺郎餐饮管理服务有限公司', salesperson: '伍仲文' }, + { value: '00294', label: '嘉兴市双瑜食品有限公司', salesperson: '刘念念' }, + { value: '00292', label: '佛山市昊峰物流供应链有限公司', salesperson: '岑彦' }, + { value: '00290', label: '重庆氢汀供应链有限公司', salesperson: '秦挺' }, + { value: '00163', label: '上海心坦物流有限公司', salesperson: '秦挺' }, + { value: '00289', label: '胡帅鹏', salesperson: '岑彦' }, + { value: '00288', label: '广州华轩供应链管理有限公司', salesperson: '钟祥' }, + { value: '00287', label: '无锡铭康物流有限公司-1', salesperson: '董剑煜' }, + { value: '00286', label: '杭州宏狼物流有限公司', salesperson: '陈高伟' }, + { value: '00285', label: '范兵', salesperson: '赵连飞' }, + { value: '00284', label: '嘉兴吉时物流有限公司', salesperson: '刘念念' }, + { value: '00283', label: '嘉兴坤鑫物流有限公司', salesperson: '刘念念' }, + { value: '00281', label: '四川富庆物流有限公司', salesperson: '秦挺' }, + { value: '00280', label: '住商国际物流有限公司无锡分公司', salesperson: '秦挺' }, + { value: '00206', label: '广东氢沣科技有限公司', salesperson: '吴纬涛' }, + { value: '00278', label: '佛山市顺德区南番顺货运代理有限公司', salesperson: '钟祥' }, + { value: '00277', label: '广东省双惠食品有限公司', salesperson: '钟祥' }, + { value: '00276', label: '广东吉胜食品有限公司', salesperson: '岑彦' }, + { value: '00275', label: '重塑氢侣(重庆)氢能汽车科技有限公司', salesperson: '金可鹏' }, + { value: '00075', label: '嘉兴市京宝物流有限公司', salesperson: '刘念念' }, + { value: '00274', label: '贺州明德供应链管理有限公司', salesperson: '金可鹏' }, + { value: '00273', label: '德阳雄丰供应链管理有限公司', salesperson: '秦挺' }, + { value: '00262', label: '四川拱照物流有限公司', salesperson: '范军军' }, + { value: '00229', label: '广州星达供应链管理有限公司', salesperson: '吴纬涛' }, + { value: '00271', label: '重庆逸茜物流有限公司', salesperson: '金可鹏' }, + { value: '00205', label: '广东鹏升冷链物流有限公司', salesperson: '岑彦' }, + { value: '00270', label: '上海利合供应链管理有限公司 (二部)', salesperson: '刘念念' }, + { value: '00269', label: '广州瑞运物流有限公司', salesperson: '钟祥' }, + { value: '00267', label: '运中博(广州) 速运有限公司', salesperson: '钟祥' }, + { value: '00266', label: '佛山市盛朗世供应链管理有限公司', salesperson: '钟祥' }, + { value: '00264', label: '氢侣重塑(九江)汽车科技有限公司', salesperson: '赵连飞' }, + { value: '00265', label: '无锡铭康物流有限公司', salesperson: '董剑煜' }, + { value: '00056', label: '嘉兴大森物流有限公司', salesperson: '刘念念' }, + { value: '00121', label: '嘉兴古道物流有限公司', salesperson: '刘念念' }, + { value: '00106', label: '宁波乐驰物流有限公司', salesperson: '赵连飞' }, + { value: '00120', label: '上海哲准供应链管理有限公司', salesperson: '金可鹏' }, + { value: '00263', label: '上海畅霸实业有限公司', salesperson: '秦挺' }, + { value: '00261', label: '嘉兴市嘉顺物流有限公司', salesperson: '赵连飞' }, + { value: '00260', label: '嘉兴必出彩供应链有限公司', salesperson: '董剑煜' }, + { value: '00258', label: '桐乡市丰韵快递有限责任公司', salesperson: '刘念念' }, + { value: '00257', label: '四川蓉新顺通运输有限公司', salesperson: '秦挺' }, + { value: '00256', label: '嘉兴智奇供应链管理有限公司', salesperson: '刘念念' }, + { value: '00255', label: '浙江恒运集装箱运输有限公司', salesperson: '陈高伟' }, + { value: '00254', label: '四川邦达蜀运供应链管理有限公司', salesperson: '秦挺' }, + { value: '00253', label: '成都诺和物流有限公司', salesperson: '秦挺' }, + { value: '00252', label: '广东星空海实业有限公司', salesperson: '伍仲文' }, + { value: '00251', label: '日邮物流(中国)有限公司苏州分公司', salesperson: '金可鹏' }, + { value: '00250', label: '上海武赢货运代理有限公司', salesperson: '秦挺' }, + { value: '00235', label: '广州市黄埔区小胖兄弟蔬果配送 中心 (个体工商户)', salesperson: '金可鹏' }, + { value: '00248', label: '成都玉池物流有限公司', salesperson: '秦挺' }, + { value: '00247', label: '四川群彬物流有限公司', salesperson: '秦挺' }, + { value: '00246', label: '深圳市兴峰物流有限公司', salesperson: '吴纬涛' }, + { value: '00245', label: '无锡双庙运输有限公司', salesperson: '谯云' }, + { value: '00243', label: '浙江集佑供应链有限公司', salesperson: '陈高伟' }, + { value: '00242', label: '嘉兴市南湖区新丰镇乐果货物运输服务部(个体工商户)', salesperson: '刘念念' }, + { value: '00241', label: '武汉吉辰速运配送有限公司', salesperson: '秦挺' }, + { value: '00240', label: '重庆金时源供应链有限公司', salesperson: '秦挺' }, + { value: '00239', label: '四川云港冷链物流有限公司', salesperson: '秦挺' }, + { value: '00237', label: '嘉兴港区韵达快递有限公司新仓分理部', salesperson: '刘念念' }, + { value: '00238', label: '江苏诚之源供应链管理有限公司', salesperson: '刘念念' }, + { value: '00139', label: '广东布谷供应链管理有限公司', salesperson: '岑彦' }, + { value: '00232', label: '广东谷登餐饮管理服务有限公司', salesperson: '伍仲文' }, + { value: '00187', label: '韶关市乐诚农产品有限公司', salesperson: '伍仲文' }, + { value: '00193', label: '广东黎耀供应链管理有限公司', salesperson: '伍仲文' }, + { value: '00162', label: '广东雄鑫农业发展有限公司', salesperson: '伍仲文' }, + { value: '00136', label: '广东兴顺佳集团有限公司韶关市曲江分公司', salesperson: '伍仲文' }, + { value: '00055', label: '韶关市曲江区新供销荣丰商贸有限公司', salesperson: '伍仲文' }, + { value: '00006', label: '广东宝地南华产城发展有限公司', salesperson: '伍仲文' }, + { value: '00225', label: '广州金吉顺国际物流有限公司', salesperson: '岑彦' }, + { value: '00228', label: '吴志彭', salesperson: '岑彦' }, + { value: '00214', label: '嘉口福食品(肇庆)有限公司', salesperson: '岑彦' }, + { value: '00191', label: '中山市蜂驰物流服务有限公司', salesperson: '岑彦' }, + { value: '00112', label: '创氢物流科技(佛山)有限公司', salesperson: '岑彦' }, + { value: '00236', label: '上海宏宙国际物流有限公司', salesperson: '秦挺' }, + { value: '00234', label: '乐清市磐南装卸有限公司', salesperson: '陈高伟' }, + { value: '00233', label: '上海明纳物流有限公司', salesperson: '谯云' }, + { value: '00231', label: '广州长运冷链服务有限公司', salesperson: '金可鹏' }, + { value: '00174', label: '广东利拉贝物流科技有限公司', salesperson: '金可鹏' }, + { value: '00154', label: '盈龙物流(广州)有限公司', salesperson: '金可鹏' }, + { value: '00177', label: '贵州省兴氢时代新能源科技有限公司', salesperson: '秦挺' }, + { value: '00227', label: '嘉兴市飞宇物流有限公司', salesperson: '赵连飞' }, + { value: '00226', label: '嘉兴鹏腾物流有限公司', salesperson: '刘念念' }, + { value: '00224', label: '平湖市易皓服装经营部', salesperson: '刘念念' }, + { value: '00223', label: '上海昂久国际物流有限公司', salesperson: '金可鹏' }, + { value: '00218', label: '佛山车鑫邦供应链管理有限公司', salesperson: '钟祥' }, + { value: '00222', label: '杭州张建丽信息咨询有限公司', salesperson: '刘念念' }, + { value: '00221', label: '佛山市南海区厨神膳食管理有限公司', salesperson: '李俊作' }, + { value: '00220', label: '嘉兴市南湖区大桥镇雪观运输装卸服务站', salesperson: '刘念念' }, + { value: '00219', label: '嘉兴玲利供应链科技有限公司', salesperson: '陈高伟' }, + { value: '00217', label: '昆山毅通麦物流科技有限公司', salesperson: '刘念念' }, + { value: '00216', label: '上海朔宇物流有限公司', salesperson: '刘念念' }, + { value: '00215', label: '阿坝州川达物流有限公司', salesperson: '秦挺' }, + { value: '00213', label: '广东旺佳食品科技有限公司', salesperson: '金可鹏' }, + { value: '00212', label: '广东新农人农业科技集团股份有限公司', salesperson: '金可鹏' }, + { value: '00211', label: '普途供应链管理(重庆)有限公司', salesperson: '秦挺' }, + { value: '00210', label: '佛山市三水区国裕饮食管理有限公司', salesperson: '钟祥' }, + { value: '00209', label: '新疆佳淇信息科技有限公司', salesperson: '尚建华' }, + { value: '00208', label: '广东粤祥食品供应链运营管理有限公司', salesperson: '钟祥' }, + { value: '00207', label: '广州市汇优餐饮服务有限责任公司', salesperson: '金可鹏' }, + { value: '00204', label: '佛山市三水淼供农产品有限公司', salesperson: '钟祥' }, + { value: '00203', label: '深圳市扬浠物流有限公司', salesperson: '李俊作' }, + { value: '00201', label: '浙江运畅供应链管理有限公司-1', salesperson: '陈高伟' }, + { value: '00085', label: '嘉兴市嘉驰运输有限公司', salesperson: '陈高伟' }, + { value: '00200', label: '广州市畅远供应链管理有限公司广州分公司', salesperson: '金可鹏' }, + { value: '00199', label: '广州市德利新隆生鲜食品有限公司', salesperson: '金可鹏' }, + { value: '00198', label: '浙江洋开供应链管理有限公司', salesperson: '刘念念' }, + { value: '00197', label: '平湖市博赢物品存放服务部(个体工商户)', salesperson: '刘念念' }, + { value: '00196', label: '广州文创供应链管理有限公司', salesperson: '金可鹏' }, + { value: '00195', label: '广州仁昌物流仓储有限公司', salesperson: '金可鹏' }, + { value: '00194', label: '广州市范来餐饮管理有限公司', salesperson: '金可鹏' }, + { value: '00192', label: '佛山日隆能源科技有限公司', salesperson: '钟祥' }, + { value: '00189', label: '重庆正合印务有限公司', salesperson: '秦挺' }, + { value: '00124', label: '上海馨想事辰供应链有限公司', salesperson: '金可鹏' }, + { value: '00188', label: '广州埔供优选配送服务有限公司', salesperson: '金可鹏' }, + { value: '00186', label: '荣达餐饮(广东)集团有限公司', salesperson: '金可鹏' }, + { value: '00185', label: '陈海松', salesperson: '李俊作' }, + { value: '00089', label: '新疆中铁黑豹物流有限公司', salesperson: '尚建华' }, + { value: '00184', label: '陈肖云', salesperson: '李俊作' }, + { value: '00183', label: '广州翼冕物流有限公司', salesperson: '金可鹏' }, + { value: '00182', label: '东莞市浩洋物流有限公司', salesperson: '钟祥' }, + { value: '00181', label: '嘉兴益顺冷链物流有限公司', salesperson: '尚建华' }, + { value: '00180', label: '中山市森汇食品有限公司', salesperson: '金可鹏' }, + { value: '00178', label: '广州健好物流有限公司', salesperson: '钟祥' }, + { value: '00176', label: '昆山司益通物流有限公司', salesperson: '刘念念' }, + { value: '00175', label: '乐山辰农物流有限公司', salesperson: '秦挺' }, + { value: '00173', label: '广州毅斌物流有限公司', salesperson: '钟祥' }, + { value: '00172', label: '平湖聚笙餐饮管理有限公司', salesperson: '刘念念' }, + { value: '00171', label: '广东中膳金勺子食品集团有限公司', salesperson: '金可鹏' }, + { value: '00170', label: '佛山龙天红专汽车服务有限公司', salesperson: '钟祥' }, + { value: '00169', label: '上海大金物流有限公司', salesperson: '秦挺' }, + { value: '00168', label: '嘉兴市豪瑞贸易有限公司', salesperson: '刘念念' }, + { value: '00167', label: '西安御盛合供应链管理有限公司', salesperson: '金可鹏' }, + { value: '00166', label: '现代汽车氢燃料电池系统(广州)有限公司', salesperson: '金可鹏' }, + { value: '00165', label: '广东千寻物流有限公司', salesperson: '钟祥' }, + { value: '00082', label: '宁波港集装箱运输有限公司嘉兴分公司', salesperson: '陈高伟' }, + { value: '00164', label: '广东祥鑫物流有限公司', salesperson: '李俊作' }, + { value: '00160', label: '广州市骏川物流有限公司', salesperson: '李俊作' }, + { value: '00159', label: '新疆新铁新集供应链管理有限公司', salesperson: '尚建华' }, + { value: '00158', label: '浙江睿迅供应链管理有限公司', salesperson: '刘念念' }, + { value: '00157', label: '东莞沙田炽瑞物流有限公司', salesperson: '金可鹏' }, + { value: '00156', label: '重庆东迅物流有限公司', salesperson: '张明勇' }, + { value: '00093', label: '海珀特科技(北京)有限公司', salesperson: '尚建华' }, + { value: '00155', label: '广州佳晟餐饮服务有限公司', salesperson: '金可鹏' }, + { value: '00151', label: '安徽政江物流有限公司', salesperson: '刘念念' }, + { value: '00148', label: '浙江运畅供应链管理有限公司', salesperson: '刘念念' }, + { value: '00147', label: '万通亟至(天津)物流科技有限公司', salesperson: '金可鹏' }, + { value: '00146', label: '广州天叶冷藏物流有限公司', salesperson: '李俊作' }, + { value: '00145', label: '云浮市佳讯物流有限公司', salesperson: '黄卓华' }, + { value: '00144', label: '芜湖莱立奇供应链有限公司', salesperson: '金可鹏' }, + { value: '00335', label: '保正(上海)供应链管理股份有限公司', salesperson: '金可鹏' }, + { value: '00033', label: '广东雪印集团有限公司', salesperson: '金可鹏' }, + { value: '00115', label: '佛山市志豪物流有限公司', salesperson: '钟祥' }, + { value: '00113', label: '源鸿物联科技有限公司', salesperson: '金可鹏' }, + { value: '00128', label: '苏州尚工物流有限公司', salesperson: '李俊作' }, + { value: '00132', label: '广州华文国际供应链有限公司', salesperson: '黄卓华' }, + { value: '00138', label: '湖北安捷楚道供应链有限公司', salesperson: '金可鹏' }, + { value: '00140', label: '佛山市捷程通供应链管理服务有限公司', salesperson: '李俊作' }, + { value: '00141', label: '同沃新能源汽车服务(深圳)有限公司', salesperson: '钟祥' }, + { value: '00143', label: '广州萧金物流有限公司', salesperson: '钟祥' }, + { value: '00065', label: '新汽(北京)新能源有限公司', salesperson: '刘念念' }, + { value: '00142', label: '北京海龙一诺物流有限责任公司', salesperson: '尚建华' }, + { value: '00137', label: '广州市多顺储运有限公司', salesperson: '金可鹏' }, + { value: '00129', label: '江苏港泽碳谷开发建设有限公司(盐城大丰汇祺开发建设有限公司)', salesperson: '陈高伟' }, + { value: '00135', label: '宁波卓辉物流有限公司', salesperson: '陈高伟' }, + { value: '00134', label: '苏州望亭远方物流有限公司', salesperson: '刘念念' }, + { value: '00133', label: '广州市福禧农业发展有限公司', salesperson: '金可鹏' }, + { value: '00131', label: '沈阳春风物流有限公司', salesperson: '金可鹏' }, + { value: '00130', label: '广东省好友多味商贸有限公司', salesperson: '金可鹏' }, + { value: '00127', label: '嘉兴市超越物流有限公司', salesperson: '陈高伟' }, + { value: '00126', label: '连云港丰嘉物流有限公司', salesperson: '张明勇' }, + { value: '00125', label: '苏州灿辉物流供应链有限公司', salesperson: '' }, + { value: '00103', label: '深圳市钉铛运输有限公司', salesperson: '钟祥' }, + { value: '00107', label: '安徽巨海国际物流有限公司', salesperson: '金可鹏' }, + { value: '00105', label: '深圳市光音通信技术有限公司', salesperson: '钟祥' }, + { value: '00088', label: '广东梓源供应链有限公司', salesperson: '钟祥' }, + { value: '00038', label: '徐青', salesperson: '金可鹏' }, + { value: '00109', label: '上海庆刚国际物流有限公司', salesperson: '陈高伟' }, + { value: '00123', label: '山东华邑氢通新能源有限公司', salesperson: '陈高伟' }, + { value: '00329', label: '沈阳聚德物流有限公司', salesperson: '金可鹏' }, + { value: '00045', label: '梅兵', salesperson: '刘念念' }, + { value: '00042', label: '李知成', salesperson: '刘念念' }, + { value: '00122', label: '嘉兴港区众通快递有限公司', salesperson: '刘念念' }, + { value: '00018', label: '上海浦江特种气体有限公司', salesperson: '金可鹏' }, + { value: '00316', label: '嘉兴市乍浦港口经营有限公司', salesperson: '陈高伟' }, + { value: '00016', label: '上海利合供应链管理有限公司', salesperson: '金可鹏' }, + { value: '00020', label: '日邮物流(中国)有限公司', salesperson: '金可鹏' }, + { value: '00022', label: '上海铭晨物流有限公司', salesperson: '金可鹏' }, + { value: '00069', label: '上海协创国际货运有限公司', salesperson: '金可鹏' }, + { value: '00072', label: '上海绿远电池有限公司', salesperson: '金可鹏' }, + { value: '00081', label: '无锡远畅运输有限公司', salesperson: '金可鹏' }, + { value: '00073', label: '上海聚富祥供应链有限公司', salesperson: '金可鹏' }, + { value: '00074', label: '南京威路物流有限公司', salesperson: '金可鹏' }, + { value: '00086', label: '嘉兴路诚物流有限公司', salesperson: '' }, + { value: '00087', label: '科运物流(深圳)有限公司', salesperson: '陈高伟' }, + { value: '00091', label: '南京福佑在线电子商务有限公司', salesperson: '' }, + { value: '00102', label: '天津华兴捷运物流有限公司', salesperson: '金可鹏' }, + { value: '00101', label: '上海安迦顺物流有限公司', salesperson: '金可鹏' }, + { value: '00090', label: '上海晟杰供应链管理有限公司', salesperson: '刘念念' }, + { value: '00035', label: '嘉兴港区韵达快递有限公司', salesperson: '刘念念' }, + { value: '00063', label: '上海昕海供应链管理有限公司', salesperson: '刘念念' }, + { value: '00099', label: '佛山市龙天运力新能源汽车租赁有限公司', salesperson: '钟祥' }, + { value: '00119', label: '中天华氢有限公司', salesperson: '陈高伟' }, + { value: '00079', label: '苏州臣服国际货运代理有限公司', salesperson: '金可鹏' }, + { value: '00118', label: '沧州骐骥物流有限公司', salesperson: '金可鹏' }, + { value: '00080', label: '德州市永顺运输有限公司', salesperson: '金可鹏' }, + { value: '00117', label: '平湖市申通快递有限公司', salesperson: '刘念念' }, + { value: '00096', label: '安徽驰远供应链管理有限公司', salesperson: '刘念念' }, + { value: '00116', label: '新疆安能数字物流有限公司', salesperson: '尚建华' }, + { value: '00014', label: '嘉兴中外运物流有限公司', salesperson: '尚建华' }, + { value: '00050', label: '粤北寒牛(韶关市)畜牧贸易有限公司', salesperson: '' }, + { value: '00114', label: '嘉兴锦湖湾集装箱运输有限责任公司', salesperson: '陈高伟' }, + { value: '00030', label: '嘉兴启元物流有限公司', salesperson: '尚建华' }, + { value: '00104', label: '广东政通人和供应链有限公司', salesperson: '钟祥' }, + { value: '00111', label: '苏州下马刘物流有限公司', salesperson: '' }, + { value: '00110', label: '上海交运沪北物流发展有限公司', salesperson: '尚建华' }, + { value: '00108', label: '四川初尧供应链管理有限公司', salesperson: '尚建华' }, + { value: '00334', label: '深圳市宝德田供应链有限公司', salesperson: '金可鹏' }, + { value: '00333', label: '嘉兴市福鑫物流有限公司', salesperson: '金可鹏' }, + { value: '00332', label: '昆山燊达物流供应链有限公司', salesperson: '金可鹏' }, + { value: '00331', label: '上海彤鑫物流有限公司', salesperson: '金可鹏' }, + { value: '00330', label: '浙江雷明供应链有限公司', salesperson: '金可鹏' }, + { value: '00327', label: '李可全', salesperson: '金可鹏' }, + { value: '00326', label: '苏州乾泰货运有限公司', salesperson: '金可鹏' }, + { value: '00325', label: '昆山达和供应链管理有限司', salesperson: '刘念念' }, + { value: '00324', label: '上海艺凡快递有限公司', salesperson: '金可鹏' }, + { value: '00323', label: '倪国锋', salesperson: '金可鹏' }, + { value: '00322', label: '胡敏敏', salesperson: '金可鹏' }, + { value: '00321', label: '李康', salesperson: '金可鹏' }, + { value: '00320', label: '王东连', salesperson: '金可鹏' }, + { value: '00319', label: '魏小彦', salesperson: '金可鹏' }, + { value: '00318', label: '上海豫声国际物流有限公司', salesperson: '金可鹏' }, + { value: '00317', label: '新森(上海)物流有限公司', salesperson: '金可鹏' }, + { value: '00038', label: '上海保正供应链有限公司', salesperson: '金可鹏' }, + { value: '00003', label: '上海知伟物流有限公司', salesperson: '刘念念' }, + { value: '00025', label: '韶宝新增试用车辆申请', salesperson: '' }, + { value: '00026', label: '郁海洋', salesperson: '金可鹏' }, + { value: '00027', label: '上海栀影运输有限公司', salesperson: '金可鹏' }, + { value: '00028', label: '上海小吉供应链有限公司', salesperson: '金可鹏' }, + { value: '00029', label: '运满满', salesperson: '' }, + { value: '00034', label: '李刚', salesperson: '陈高伟' }, + { value: '00037', label: '徐丽', salesperson: '金可鹏' }, + { value: '00036', label: '韶关市丰和速递有限公司', salesperson: '' }, + { value: '00046', label: '苏州众揽星河供应链管理有限公司', salesperson: '' }, + { value: '00047', label: '上海艾讯物流有限公司', salesperson: '' }, + { value: '00048', label: '徐洪波', salesperson: '金可鹏' }, + { value: '00051', label: '高春时', salesperson: '金可鹏' }, + { value: '00052', label: '上海双田物流有限公司', salesperson: '金可鹏' }, + { value: '00053', label: '韶关市海成船务有限公司', salesperson: '' }, + { value: '00057', label: '广东马坝油粘科技开发有限公司', salesperson: '' }, + { value: '00059', label: '广东省保炎信息服务有限公司', salesperson: '' }, + { value: '00066', label: '嘉兴锐腾物流有限公司', salesperson: '陈高伟' }, + { value: '00060', label: '浙江氢能产业发展有限公司', salesperson: '陈高伟' }, + { value: '00064', label: '韶关市仁弘汽车销售服务有限公司', salesperson: '' }, + { value: '00067', label: '嘉兴市骏达物流有限公司', salesperson: '' }, + { value: '00092', label: '韶关市兴达运输有限公司', salesperson: '' }, + { value: '00095', label: '上海春平实业有限公司', salesperson: '尚建华' }, + { value: '00098', label: '龙勇', salesperson: '钟祥' }, + { value: '00071', label: '4.2上海宣传项目', salesperson: '金可鹏' }, + { value: '00100', label: '北京五方合创物流有限公司', salesperson: '' }, + { value: '00097', label: '北京锋行物流服务有限公司', salesperson: '' }, + { value: '00002', label: '上海斯派杰人力资源有限公司', salesperson: '刘念念' }, + { value: '00084', label: '宇通商用车有限公司', salesperson: '陈高伟' }, + { value: '00083', label: '上海广轩国际物流有限公司', salesperson: '刘念念' }, + { value: '00078', label: '宁波鑫荣国际物流有限公司', salesperson: '' }, + { value: '00077', label: '嘉兴晋茂供应链管理有限公司', salesperson: '' }, + { value: '00076', label: '安吉天地物流科技有限公司', salesperson: '' }, + { value: '00070', label: '嘉兴市良荣物流有限公司', salesperson: '刘念念' }, + { value: '00068', label: '上海万地物流有限公司', salesperson: '尚建华' }, + { value: '00061', label: '羚牛公务车专用', salesperson: '' }, + { value: '00058', label: '测试用客户', salesperson: '' }, + { value: '00054', label: '上海翼赴科技有限公司', salesperson: '刘念念' }, + { value: '00044', label: '王兵', salesperson: '尚建华' }, + { value: '00043', label: '龙胜', salesperson: '刘念念' }, + { value: '00041', label: '李朝飞', salesperson: '刘念念' }, + { value: '00040', label: '朱俊霖', salesperson: '刘念念' }, + { value: '00032', label: '嘉兴市众立物流有限公司', salesperson: '尚建华' }, + { value: '00031', label: '嘉兴鼎华国际货运代理有限责任公司', salesperson: '尚建华' }, + { value: '00021', label: '羚牛氢能-物流中心', salesperson: '尚建华' }, + { value: '00017', label: '上海怡喆供应链有限公司', salesperson: '金可鹏' }, + { value: '00015', label: '上海有常物流有限公司', salesperson: '尚建华' }, + { value: '00013', label: '嘉兴市申通快递有限公司', salesperson: '尚建华' }, + { value: '00012', label: '嘉兴金小悦贸易有限公司', salesperson: '尚建华' }, + { value: '00010', label: '韶关市韶宝物流有限公司', salesperson: '' }, + { value: '00008', label: '宜章县顺章建材有限公司', salesperson: '金可鹏' }, + { value: '00007', label: '上海迅杰物流有限公司', salesperson: '金可鹏' }, + { value: '00005', label: '张家港中润物流有限公司', salesperson: '金可鹏' }, + { value: '00004', label: '嘉兴日蒸食品有限公司', salesperson: '金可鹏' }, + { value: '00001', label: '锡庆供应链管理(上海)有限公司', salesperson: '金可鹏' }, + { value: '00036', label: '上海米来物流股份有限公司', salesperson: '金可鹏' }, + { value: '00035', label: '上海宏顺物流有限公司', salesperson: '金可鹏' }, + { value: '00315', label: '中外运物流华东有限公司', salesperson: '尚建华' }, + { value: '00328', label: '北京中谊快运有限公司', salesperson: '金可鹏' }, + ]; + + /** 客户名称下拉:除客户库外的固定选项 */ + var CUSTOMER_ID_VEHICLE_MOVE = '__h2_customer_vehicle_move__'; + var CUSTOMER_ID_PENDING_CONFIRM = '__h2_customer_pending_confirm__'; + var CUSTOMER_EXTRA_OPTIONS = [ + { value: CUSTOMER_ID_VEHICLE_MOVE, label: '车辆异动', salesperson: '' }, + { value: CUSTOMER_ID_PENDING_CONFIRM, label: '特殊待确认客户', salesperson: '' } + ]; + + function isSpecialCustomerId(customerId) { + return ( + customerId === CUSTOMER_ID_VEHICLE_MOVE || + customerId === CUSTOMER_ID_PENDING_CONFIRM + ); + } + + function isRegularCustomerId(customerId) { + return !!customerId && !isSpecialCustomerId(customerId); + } + + function buildCustomerSelectOptions() { + return CUSTOMER_EXTRA_OPTIONS.map(function (c) { + return { value: c.value, label: c.label }; + }).concat( + CUSTOMER_LIST.map(function (c) { + return { value: c.value, label: c.label }; + }) + ); + } + + var SALESPERSON_FILTER_OPTIONS = (function () { + var seen = {}; + var list = []; + CUSTOMER_LIST.forEach(function (c) { + var s = c.salesperson; + if (s && !seen[s]) { + seen[s] = true; + list.push({ value: s, label: s }); + } + }); + list.sort(function (a, b) { + return String(a.label).localeCompare(String(b.label), 'zh-CN'); + }); + return list; + })(); + + var COMPANY_LIST = [ + { value: 'co1', label: '浙江羚牛氢能科技有限公司' }, + { value: 'co2', label: '羚牛新能源运营有限公司' }, + { value: 'co3', label: '华东氢能服务有限公司' } + ]; + + var SETTLEMENT_STATUS_OPTIONS = [ + { value: 'customer', label: '客户承担' }, + { value: 'internal', label: '我司承担' }, + { value: 'customer_self', label: '客户自行结算' }, + { value: 'other', label: '其他结算' } + ]; + + var PAYMENT_STATUS_OPTIONS = [ + { value: 'unpaid', label: '未付款' }, + { value: 'paid', label: '已付款' }, + { value: 'partial', label: '部分付款' } + ]; + + /** 加氢站付款 / 客户收款(系统带出,仅已付款、未付款) */ + var BINARY_PAID_STATUS_OPTIONS = [ + { value: 'paid', label: '已付款' }, + { value: 'unpaid', label: '未付款' } + ]; + + var CHANGE_LOG_FIELD_LABELS = { + reconcileStatus: '状态', + hydrogenTime: '加氢时间', + stationId: '加氢站名称', + plateNo: '车牌号', + mileageKm: '行驶里程(km)', + hydrogenKg: '加氢量(kg)', + costUnitPrice: '成本单价(元/kg)', + costTotal: '成本总价(元)', + customerId: '客户名称', + customerUnitPrice: '加氢单价(元/kg)', + customerAmount: '加氢总价(元)', + settlementStatus: '承担方式', + remark: '备注', + orderNo: '订单编号', + _copy: '复制新增', + _import: '批量导入', + _create: '新增记录' + }; + + var OTHER_CREATORS = [ + { id: 'u_biz_wang', name: '王业务' }, + { id: 'u_biz_zhao', name: '赵业务' } + ]; + + var IMPORT_TEMPLATE_HEADERS = [ + '加氢时间', + '加氢站名称', + '车牌号', + '行驶里程(km)', + '加氢量(kg)', + '成本单价(元/kg)', + '客户名称', + '加氢单价(元/kg)', + '承担方式', + '备注' + ]; + + var SETTLEMENT_LABEL_TO_VALUE = { + 客户承担: 'customer', + 我司承担: 'internal', + 客户自行结算: 'customer_self', + 其他结算: 'other', + 客户结算: 'customer', + 公司内部: 'internal' + }; + var PAYMENT_LABEL_TO_VALUE = { 未付款: 'unpaid', 已付款: 'paid', 部分付款: 'partial' }; + + function isPlateInVehicleTable(plate) { + return VEHICLE_INFO_TABLE.indexOf(plate) >= 0; + } + + function findStation(val) { + var i; + for (i = 0; i < HYDROGEN_STATION_LIST.length; i++) { + if (HYDROGEN_STATION_LIST[i].value === val) { + var base = HYDROGEN_STATION_LIST[i]; + var priceMeta = STATION_SYSTEM_PRICE_MAP[base.value]; + return priceMeta ? Object.assign({}, base, priceMeta) : base; + } + } + return null; + } + + function findStationByLabel(label) { + var i; + var text = String(label || '').trim(); + if (!text) return null; + for (i = 0; i < HYDROGEN_STATION_LIST.length; i++) { + if (HYDROGEN_STATION_LIST[i].label === text) return findStation(HYDROGEN_STATION_LIST[i].value); + } + return null; + } + + function findCustomerByLabel(label) { + var i; + var text = String(label || '').trim(); + if (!text) return null; + for (i = 0; i < CUSTOMER_EXTRA_OPTIONS.length; i++) { + if (CUSTOMER_EXTRA_OPTIONS[i].label === text) return CUSTOMER_EXTRA_OPTIONS[i]; + } + for (i = 0; i < CUSTOMER_LIST.length; i++) { + if (CUSTOMER_LIST[i].label === text) return CUSTOMER_LIST[i]; + } + return null; + } + + function csvEscape(v) { + var s = String(v == null ? '' : v); + if (/[",\n\r]/.test(s)) return '"' + s.replace(/"/g, '""') + '"'; + return s; + } + + function downloadCsv(filename, headers, rows) { + var lines = [headers.map(csvEscape).join(',')].concat( + (rows || []).map(function (r) { + return r.map(csvEscape).join(','); + }) + ); + var blob = new Blob(['\uFEFF' + lines.join('\n')], { type: 'text/csv;charset=utf-8;' }); + var a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = filename; + a.click(); + URL.revokeObjectURL(a.href); + } + + function parseCsvLine(line) { + var out = []; + var cur = ''; + var inQuote = false; + var i; + for (i = 0; i < line.length; i++) { + var ch = line.charAt(i); + if (inQuote) { + if (ch === '"') { + if (line.charAt(i + 1) === '"') { + cur += '"'; + i++; + } else inQuote = false; + } else cur += ch; + } else if (ch === '"') inQuote = true; + else if (ch === ',') { + out.push(cur); + cur = ''; + } else cur += ch; + } + out.push(cur); + return out; + } + + function parseImportCsv(text) { + var raw = String(text || '').replace(/^\uFEFF/, ''); + var lines = raw.split(/\r?\n/).filter(function (ln) { + return ln.trim().length > 0; + }); + if (lines.length < 2) return []; + var headers = parseCsvLine(lines[0]).map(function (h) { + return String(h || '').trim(); + }); + var idx = {}; + headers.forEach(function (h, i) { + idx[h] = i; + }); + var rows = []; + var li; + for (li = 1; li < lines.length; li++) { + var cells = parseCsvLine(lines[li]); + if (!cells.some(function (c) { return String(c || '').trim(); })) continue; + rows.push({ cells: cells, idx: idx }); + } + return rows; + } + + function parseImportDate(val) { + if (!val || !window.dayjs) return null; + var s = String(val).trim(); + if (!s) return null; + var d = window.dayjs(s); + return d.isValid() ? d : null; + } + + function buildRowFromImport(item) { + var cells = item.cells; + var idx = item.idx; + var get = function (h) { + var i = idx[h]; + return i == null || i < 0 ? '' : String(cells[i] == null ? '' : cells[i]).trim(); + }; + var station = findStationByLabel(get('加氢站名称')); + var customerLabel = get('客户名称'); + var customer = customerLabel ? findCustomerByLabel(customerLabel) : null; + var settlementLabel = get('承担方式') || get('结算状态'); + var now = nowDayjs(); + var draft = { + key: nextRowKey(), + createdBy: CURRENT_USER.id, + creatorName: CURRENT_USER.name, + orderNo: '', + hydrogenTime: parseImportDate(get('加氢时间')) || now, + stationId: station ? station.value : undefined, + stationName: station ? station.label : get('加氢站名称'), + plateNo: get('车牌号') || undefined, + mileageKm: normalizeMileageKm(get('行驶里程(km)')), + hydrogenKg: normalizeBatchUnitPrice(get('加氢量(kg)')), + costUnitPrice: normalizeBatchUnitPrice(get('成本单价(元/kg)')), + costTotal: null, + customerId: customer ? customer.value : undefined, + customerName: customer ? customer.label : '', + customerUnitPrice: normalizeBatchUnitPrice(get('加氢单价(元/kg)') || get('对客单价(元/kg)')), + customerAmount: null, + salesperson: customer ? customer.salesperson || '' : '', + settlementStatus: SETTLEMENT_LABEL_TO_VALUE[settlementLabel] || 'customer', + remark: get('备注'), + createdAt: now, + reconcileStatus: null + }; + return Object.assign({}, draft, resolveLinkedModuleFields(draft)); + } + + function getSystemPricesForRow(row) { + var station = findStation(row.stationId); + if (!station || !row.hydrogenTime || !window.dayjs) return { cost: null, customer: null }; + var ht; + try { + ht = window.dayjs(row.hydrogenTime); + } catch (e7) { + return { cost: null, customer: null }; + } + var cost = null; + var customer = null; + if (station.costPrice != null && station.costPriceEffective) { + try { + if (!ht.isBefore(window.dayjs(station.costPriceEffective).startOf('day'))) { + cost = roundMoney(station.costPrice); + } + } catch (e8) {} + } + if (station.customerPrice != null && station.customerPriceEffective) { + try { + if (!ht.isBefore(window.dayjs(station.customerPriceEffective).startOf('day'))) { + customer = roundMoney(station.customerPrice); + } + } catch (e9) {} + } + return { cost: cost, customer: customer }; + } + + function isHydrogenKgWarn(row) { + return !isEmptyNum(row.hydrogenKg) && numOrZero(row.hydrogenKg) > 60; + } + + function isCostPriceWarn(row) { + if (isEmptyNum(row.costUnitPrice)) return false; + var sys = getSystemPricesForRow(row); + if (sys.cost == null) return false; + return roundMoney(row.costUnitPrice) !== sys.cost; + } + + function isCustomerPriceWarn(row) { + if (isEmptyNum(row.customerUnitPrice)) return false; + var sys = getSystemPricesForRow(row); + if (sys.customer == null) return false; + return roundMoney(row.customerUnitPrice) !== sys.customer; + } + + function isRowDataWarn(row) { + return isHydrogenKgWarn(row) || isCostPriceWarn(row) || isCustomerPriceWarn(row); + } + + function findCustomer(val) { + var i; + if (!val) return null; + for (i = 0; i < CUSTOMER_EXTRA_OPTIONS.length; i++) { + if (CUSTOMER_EXTRA_OPTIONS[i].value === val) return CUSTOMER_EXTRA_OPTIONS[i]; + } + for (i = 0; i < CUSTOMER_LIST.length; i++) { + if (CUSTOMER_LIST[i].value === val) return CUSTOMER_LIST[i]; + } + return null; + } + + /** 原型:联调后从客户/合同/财务模块自动拉取,不可手工录入 */ + function resolveLinkedModuleFields(row) { + if (!row || !isRegularCustomerId(row.customerId)) { + return { + receiptDate: null, + stationPaymentStatus: null, + invoiceDate: null, + reconcileDate: null, + paymentStatus: null, + customerReceiptStatus: null, + companyId: undefined, + companyName: '' + }; + } + var idx = 0; + var i; + for (i = 0; i < CUSTOMER_LIST.length; i++) { + if (CUSTOMER_LIST[i].value === row.customerId) { + idx = i; + break; + } + } + var company = COMPANY_LIST[idx % COMPANY_LIST.length]; + var base = row.hydrogenTime || row.createdAt || nowDayjs(); + var invoiceDate = base; + var reconcileDate = base; + if (window.dayjs && base) { + try { + invoiceDate = window.dayjs(base).subtract(idx % 4, 'day'); + reconcileDate = window.dayjs(base).subtract((idx % 4) + 1, 'day'); + } catch (eLink) {} + } + var paymentStatus = idx % 3 === 0 ? 'paid' : idx % 3 === 1 ? 'partial' : 'unpaid'; + var receiptDate = base; + if (window.dayjs && base) { + try { + receiptDate = window.dayjs(base).subtract((idx % 5) + 2, 'day'); + } catch (eReceipt) {} + } + var stationPaymentStatus = idx % 2 === 0 ? 'paid' : 'unpaid'; + var customerReceiptStatus = paymentStatus === 'paid' ? 'paid' : 'unpaid'; + return { + receiptDate: receiptDate, + stationPaymentStatus: stationPaymentStatus, + invoiceDate: invoiceDate, + reconcileDate: reconcileDate, + paymentStatus: paymentStatus, + customerReceiptStatus: customerReceiptStatus, + companyId: company.value, + companyName: company.label + }; + } + + function isEmptyStr(v) { + return !String(v == null ? '' : v).trim(); + } + + function isEmptyNum(v) { + return v === null || v === undefined || v === ''; + } + + function getRowInvalidMap(row) { + var inv = {}; + if (!row.hydrogenTime) inv.hydrogenTime = true; + if (!row.stationId) inv.stationId = true; + if (!row.plateNo) inv.plateNo = 'empty'; + else if (!isPlateInVehicleTable(row.plateNo)) inv.plateNo = 'not_exist'; + if (isEmptyNum(row.hydrogenKg) || numOrZero(row.hydrogenKg) <= 0) inv.hydrogenKg = true; + if (isEmptyNum(row.costUnitPrice) || numOrZero(row.costUnitPrice) <= 0) inv.costUnitPrice = true; + if (isEmptyNum(row.customerUnitPrice) || numOrZero(row.customerUnitPrice) <= 0) inv.customerUnitPrice = true; + if (!row.settlementStatus) inv.settlementStatus = true; + return inv; + } + + function isNewRow(row) { + return row && (!row.reconcileStatus || row.reconcileStatus === RECONCILE_DRAFT); + } + + function isPendingRow(row) { + return row && row.reconcileStatus === RECONCILE_PENDING; + } + + function isReconciledRow(row) { + return row && row.reconcileStatus === RECONCILE_RECONCILED; + } + + function rowDisplaySortTier(row) { + if (isReconciledRow(row)) return 0; + if (isPendingRow(row)) return 1; + return 2; + } + + function rowStatusLabel(row) { + if (isReconciledRow(row)) return '已对账'; + if (isPendingRow(row)) return '未对账'; + return ''; + } + + function canShowPendingActions(row) { + return isPendingRow(row); + } + + function canCopyRow(row) { + if (!row || isReconciledRow(row)) return false; + if (isPendingRow(row)) return true; + return isNewRow(row) && row.createdBy === CURRENT_USER.id; + } + + function cloneRowTimeValue(t) { + if (!t) return t; + try { + if (window.dayjs && window.dayjs.isDayjs && window.dayjs.isDayjs(t)) return t.clone(); + if (window.dayjs) { + var d = window.dayjs(t); + return d.isValid() ? d : t; + } + } catch (eClone) {} + return t; + } + + function buildCopiedRow(sourceRow) { + var now = nowDayjs(); + var draft = { + key: nextRowKey(), + createdBy: CURRENT_USER.id, + creatorName: CURRENT_USER.name, + orderNo: '', + hydrogenTime: cloneRowTimeValue(sourceRow.hydrogenTime), + stationId: sourceRow.stationId, + stationName: sourceRow.stationName || '', + plateNo: sourceRow.plateNo, + mileageKm: sourceRow.mileageKm, + hydrogenKg: sourceRow.hydrogenKg, + costUnitPrice: sourceRow.costUnitPrice, + costTotal: sourceRow.costTotal, + customerId: sourceRow.customerId, + customerName: sourceRow.customerName || '', + customerUnitPrice: sourceRow.customerUnitPrice, + customerAmount: sourceRow.customerAmount, + salesperson: sourceRow.salesperson || '', + settlementStatus: sourceRow.settlementStatus || 'customer', + remark: sourceRow.remark || '', + invoiceDate: cloneRowTimeValue(sourceRow.invoiceDate), + reconcileDate: cloneRowTimeValue(sourceRow.reconcileDate), + paymentStatus: sourceRow.paymentStatus, + companyId: sourceRow.companyId, + companyName: sourceRow.companyName || '', + createdAt: now, + reconcileStatus: null, + savedAt: null, + reconciledAt: null + }; + return recalcAllRows([draft])[0]; + } + + function isRowBatchEditable(row) { + if (isReconciledRow(row)) return false; + if (isPendingRow(row)) return true; + return isNewRow(row) && row.createdBy === CURRENT_USER.id; + } + + function rowSortTime(row) { + if (isReconciledRow(row)) return rowTimeValue(row, 'reconciledAt') || rowTimeValue(row, 'savedAt'); + if (isPendingRow(row)) return rowTimeValue(row, 'savedAt') || rowTimeValue(row, 'createdAt'); + return rowTimeValue(row, 'createdAt'); + } + + function canDeleteRow(row, isSupervisorRole) { + if (isSupervisorRole) return true; + if (isPendingRow(row)) return true; + if (isNewRow(row)) return row.createdBy === CURRENT_USER.id; + return false; + } + + function rowTimeValue(row, field) { + var v = row && row[field]; + if (!v || !window.dayjs) return 0; + try { + return window.dayjs(v).valueOf(); + } catch (e1) { + return 0; + } + } + + function sortRowsWithinTier(rows, ascending) { + return rows.slice().sort(function (a, b) { + var ta = rowSortTime(a); + var tb = rowSortTime(b); + if (ta !== tb) return ascending ? ta - tb : tb - ta; + var keyCmp = String(a.key || '').localeCompare(String(b.key || '')); + return ascending ? keyCmp : -keyCmp; + }); + } + + function sortRowsByDisplayTier(rows) { + var reconciled = []; + var pending = []; + var drafts = []; + (rows || []).forEach(function (r) { + var tier = rowDisplaySortTier(r); + if (tier === 0) reconciled.push(r); + else if (tier === 1) pending.push(r); + else drafts.push(r); + }); + // 已对账/未对账:组内时间倒序;待保存:组内时间正序,新增行在表格最底部 + return sortRowsWithinTier(reconciled, false) + .concat(sortRowsWithinTier(pending, false)) + .concat(sortRowsWithinTier(drafts, true)); + } + + var rowIdSeed = 0; + function nextRowKey() { + rowIdSeed += 1; + return 'h2-' + Date.now() + '-' + rowIdSeed; + } + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + function numOrZero(v) { + if (v === null || v === undefined || v === '') return 0; + var n = Number(v); + return isNaN(n) ? 0 : n; + } + + function roundMoney(n) { + return Math.round(numOrZero(n) * 100) / 100; + } + + function normalizeBatchUnitPrice(val) { + if (val === null || val === undefined || val === '') return null; + var n = Number(val); + if (!isFinite(n) || n <= 0) return null; + return roundMoney(n); + } + + function normalizeMileageKm(val) { + return normalizeBatchUnitPrice(val); + } + + function fmtMoney(n) { + if (n === null || n === undefined || n === '') return '0.00'; + return roundMoney(n).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + } + + function fmtQty(n) { + if (n === null || n === undefined || n === '') return '-'; + return roundMoney(n).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + } + + function nowDayjs() { + try { + if (window.dayjs) return window.dayjs(); + } catch (e1) {} + return null; + } + + function formatDate(d) { + if (!d || !window.dayjs) return ''; + try { + return window.dayjs(d).format('YYYY-MM-DD'); + } catch (e2) { + return ''; + } + } + + var HYDROGEN_DATETIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; + + function formatDateTime(d) { + if (!d || !window.dayjs) return ''; + try { + return window.dayjs(d).format(HYDROGEN_DATETIME_FORMAT); + } catch (e5) { + return ''; + } + } + + function exportLabelOf(opts, val) { + var i; + for (i = 0; i < opts.length; i++) { + if (opts[i].value === val) return opts[i].label; + } + return val == null || val === '' ? '' : String(val); + } + + function exportPlainNum(v) { + if (isEmptyNum(v)) return ''; + return roundMoney(v).toFixed(2); + } + + function reconcileStatusLogLabel(status) { + if (!status || status === RECONCILE_DRAFT) return '待保存'; + if (status === RECONCILE_PENDING) return '未对账'; + if (status === RECONCILE_RECONCILED) return '已对账'; + return String(status); + } + + function valuesEqualForLog(field, a, b) { + if (a === b) return true; + if ((a == null || a === '') && (b == null || b === '')) return true; + if (field === 'hydrogenTime' && window.dayjs) { + try { + return window.dayjs(a).valueOf() === window.dayjs(b).valueOf(); + } catch (eLog) { + return false; + } + } + if ( + field === 'mileageKm' || + field === 'hydrogenKg' || + field === 'costUnitPrice' || + field === 'customerUnitPrice' || + field === 'costTotal' || + field === 'customerAmount' + ) { + if (isEmptyNum(a) && isEmptyNum(b)) return true; + return roundMoney(a) === roundMoney(b); + } + return String(a) === String(b); + } + + function formatLogValue(field, value, rowCtx) { + if (value == null || value === '') return '—'; + if (field === 'reconcileStatus') return reconcileStatusLogLabel(value); + if (field === 'hydrogenTime') return formatDateTime(value) || String(value); + if (field === 'stationId') { + var st = findStation(value); + return st ? st.label : (rowCtx && rowCtx.stationName) || String(value); + } + if (field === 'customerId') { + var cu = findCustomer(value); + return cu ? cu.label : (rowCtx && rowCtx.customerName) || String(value); + } + if (field === 'settlementStatus') return exportLabelOf(SETTLEMENT_STATUS_OPTIONS, value) || '—'; + if (field === 'paymentStatus') return exportLabelOf(PAYMENT_STATUS_OPTIONS, value) || '—'; + if (field === 'companyId') { + return (rowCtx && rowCtx.companyName) || exportLabelOf(COMPANY_LIST, value) || String(value); + } + if ( + field === 'mileageKm' || + field === 'hydrogenKg' || + field === 'costUnitPrice' || + field === 'customerUnitPrice' || + field === 'costTotal' || + field === 'customerAmount' + ) { + return isEmptyNum(value) ? '—' : roundMoney(value).toFixed(2); + } + return String(value); + } + + function buildExportColumnDefs() { + return [ + { key: 'seq', label: '序号', getValue: function (r) { return r.seq != null ? String(r.seq) : ''; } }, + { + key: 'recordStatus', + label: '状态', + getValue: function (r) { + var lb = rowStatusLabel(r); + return lb || '—'; + } + }, + { key: 'orderNo', label: '订单编号', getValue: function (r) { return r.orderNo || ''; } }, + { key: 'hydrogenTime', label: '加氢时间', getValue: function (r) { return formatDateTime(r.hydrogenTime) || ''; } }, + { key: 'hydrogenDate', label: '加氢日期', getValue: function (r) { return formatDate(r.hydrogenTime) || ''; } }, + { key: 'stationName', label: '加氢站名称', getValue: function (r) { return r.stationName || ''; } }, + { key: 'plateNo', label: '车牌号', getValue: function (r) { return r.plateNo || ''; } }, + { key: 'hydrogenKg', label: '加氢量(kg)', getValue: function (r) { return exportPlainNum(r.hydrogenKg); } }, + { key: 'costUnitPrice', label: '成本单价(元/kg)', getValue: function (r) { return exportPlainNum(r.costUnitPrice); } }, + { key: 'costTotal', label: '成本总价(元)', getValue: function (r) { return exportPlainNum(r.costTotal); } }, + { key: 'customerName', label: '客户名称', getValue: function (r) { return r.customerName || ''; } }, + { key: 'customerUnitPrice', label: '加氢单价(元/kg)', getValue: function (r) { return exportPlainNum(r.customerUnitPrice); } }, + { key: 'customerAmount', label: '加氢总价(元)', getValue: function (r) { return exportPlainNum(r.customerAmount); } }, + { key: 'mileageKm', label: '行驶里程(km)', getValue: function (r) { return exportPlainNum(r.mileageKm); } }, + { key: 'salesperson', label: '业务员', getValue: function (r) { return r.salesperson || ''; } }, + { + key: 'settlementStatus', + label: '承担方式', + getValue: function (r) { return exportLabelOf(SETTLEMENT_STATUS_OPTIONS, r.settlementStatus); } + }, + { key: 'reconcileDate', label: '对账日期', getValue: function (r) { return formatDate(r.reconcileDate) || ''; } }, + { key: 'remark', label: '备注', getValue: function (r) { return r.remark || ''; } }, + { key: 'receiptDate', label: '收票日期', getValue: function (r) { return formatDate(r.receiptDate) || ''; } }, + { + key: 'stationPaymentStatus', + label: '加氢站付款状态', + getValue: function (r) { return exportLabelOf(BINARY_PAID_STATUS_OPTIONS, r.stationPaymentStatus); } + }, + { key: 'invoiceDate', label: '开票日期', getValue: function (r) { return formatDate(r.invoiceDate) || ''; } }, + { + key: 'customerReceiptStatus', + label: '客户收款状态', + getValue: function (r) { return exportLabelOf(BINARY_PAID_STATUS_OPTIONS, r.customerReceiptStatus); } + }, + { + key: 'companyName', + label: '开票公司', + getValue: function (r) { return r.companyName || exportLabelOf(COMPANY_LIST, r.companyId); } + } + ]; + } + + function timeSortValue(t) { + if (!t || !window.dayjs) return 0; + try { + return window.dayjs(t).valueOf(); + } catch (e3) { + return 0; + } + } + + /** 订单编号:JQ + 站点编码 + YYMMDD + 五位序号(同站同日按加氢时间升序,每日从 00001 起) */ + function assignOrderNumbers(rows) { + if (!window.dayjs) return rows; + var groups = {}; + (rows || []).forEach(function (r) { + var station = findStation(r.stationId); + if (!station || !station.stationCode || !r.hydrogenTime) return; + var ymd; + try { + ymd = window.dayjs(r.hydrogenTime).format('YYMMDD'); + } catch (e4) { + return; + } + var gk = station.stationCode + '|' + ymd; + if (!groups[gk]) groups[gk] = []; + groups[gk].push({ + key: r.key, + stationCode: station.stationCode, + ymd: ymd, + time: timeSortValue(r.hydrogenTime), + createdAt: timeSortValue(r.createdAt) + }); + }); + var orderMap = {}; + Object.keys(groups).forEach(function (gk) { + var list = groups[gk]; + var stationCode = list[0].stationCode; + var ymd = list[0].ymd; + var prefix = 'JQ' + stationCode + ymd; + list.sort(function (a, b) { + if (a.time !== b.time) return a.time - b.time; + if (a.createdAt !== b.createdAt) return a.createdAt - b.createdAt; + return String(a.key).localeCompare(String(b.key)); + }); + list.forEach(function (item, idx) { + orderMap[item.key] = prefix + String(idx + 1).padStart(5, '0'); + }); + }); + return (rows || []).map(function (r) { + return Object.assign({}, r, { orderNo: orderMap[r.key] || '' }); + }); + } + + function matchStatementPatch(row, patch) { + if (!row || !patch) return false; + if (patch.orderNo && row.orderNo && patch.orderNo === row.orderNo) return true; + if (patch.ledgerId && row.ledgerSyncId && patch.ledgerId === row.ledgerSyncId) return true; + var rowTime = formatDateTime(row.hydrogenTime) || String(row.hydrogenTime || ''); + var patchTime = String(patch.hydrogenTime || ''); + return (row.stationName || '') === (patch.stationName || '') + && (row.plateNo || '') === (patch.plateNo || '') + && (rowTime === patchTime || String(row.hydrogenTime || '') === patchTime); + } + + function applyStatementPatchesToRows(rows, patches) { + if (!patches || !patches.length) return rows || []; + return (rows || []).map(function (r) { + var patch = null; + var i; + for (i = 0; i < patches.length; i++) { + if (matchStatementPatch(r, patches[i])) { + patch = patches[i]; + break; + } + } + if (!patch) return r; + return Object.assign({}, r, { + statementPatched: true, + reconcileDate: patch.reconcileDate, + receiptDate: patch.receiptDate, + stationPaymentStatus: patch.paymentStatus || 'paid', + paymentStatus: patch.paymentStatus || 'paid', + reconciledAt: patch.reconcileDate + }); + }); + } + + function recalcRowFields(row) { + var mileage = normalizeMileageKm(row.mileageKm); + var kg = isEmptyNum(row.hydrogenKg) ? null : roundMoney(row.hydrogenKg); + var costUnit = isEmptyNum(row.costUnitPrice) ? null : roundMoney(row.costUnitPrice); + var customerUnit = isEmptyNum(row.customerUnitPrice) ? null : roundMoney(row.customerUnitPrice); + var costTotal = kg != null && costUnit != null ? roundMoney(kg * costUnit) : null; + var customerAmount = kg != null && customerUnit != null ? roundMoney(kg * customerUnit) : null; + var station = findStation(row.stationId); + var customer = findCustomer(row.customerId); + var linked = resolveLinkedModuleFields(row); + if (row && row.statementPatched) { + linked = Object.assign({}, linked, { + reconcileDate: row.reconcileDate, + receiptDate: row.receiptDate, + stationPaymentStatus: row.stationPaymentStatus, + paymentStatus: row.paymentStatus + }); + } + return Object.assign({}, row, { + mileageKm: mileage, + hydrogenKg: kg, + costUnitPrice: costUnit, + customerUnitPrice: customerUnit, + costTotal: costTotal, + customerAmount: customerAmount, + stationName: station ? station.label : row.stationName, + customerName: customer ? customer.label : '', + salesperson: customer ? customer.salesperson || '' : '', + receiptDate: linked.receiptDate, + stationPaymentStatus: linked.stationPaymentStatus, + invoiceDate: linked.invoiceDate, + reconcileDate: linked.reconcileDate, + paymentStatus: linked.paymentStatus, + customerReceiptStatus: linked.customerReceiptStatus, + companyId: linked.companyId, + companyName: linked.companyName + }); + } + + function recalcAllRows(rows) { + return assignOrderNumbers((rows || []).map(recalcRowFields)); + } + + function buildEmptyRow(user) { + var now = nowDayjs(); + return recalcAllRows([ + { + key: nextRowKey(), + createdBy: user.id, + creatorName: user.name, + orderNo: '', + hydrogenTime: now, + stationId: undefined, + stationName: '', + plateNo: undefined, + mileageKm: null, + hydrogenKg: null, + costUnitPrice: null, + costTotal: null, + customerId: undefined, + customerName: '', + customerUnitPrice: null, + customerAmount: null, + salesperson: '', + settlementStatus: 'customer', + remark: '', + createdAt: now, + reconcileStatus: null + } + ])[0]; + } + + function buildMockAllRows() { + var rows = []; + var creators = [{ id: CURRENT_USER.id, name: CURRENT_USER.name }].concat(OTHER_CREATORS); + var i; + /** 10 条无异常演示:加氢量≤60kg,成本/加氢单价与系统价一致 */ + for (i = 0; i < 10; i++) { + var creator = creators[i % creators.length]; + var stationBase = HYDROGEN_STATION_LIST[i % HYDROGEN_STATION_LIST.length]; + var station = findStation(stationBase.value) || stationBase; + var customer = CUSTOMER_LIST[i % CUSTOMER_LIST.length]; + var company = COMPANY_LIST[i % COMPANY_LIST.length]; + var d = nowDayjs(); + var day = d && d.subtract ? d.subtract(i, 'day') : d; + var reconcileStatus = RECONCILE_DRAFT; + if (creator.id === CURRENT_USER.id) { + if (i < 3) reconcileStatus = RECONCILE_RECONCILED; + else if (i < 7) reconcileStatus = RECONCILE_PENDING; + } else { + reconcileStatus = i % 2 === 0 ? RECONCILE_RECONCILED : RECONCILE_PENDING; + } + var draftRow = { + stationId: station.value, + hydrogenTime: day + }; + var sys = getSystemPricesForRow(draftRow); + var costUnit = sys.cost != null ? sys.cost : roundMoney(station.costPrice != null ? station.costPrice : 30); + var custUnit = sys.customer != null ? sys.customer : roundMoney(station.customerPrice != null ? station.customerPrice : 30); + var kg = roundMoney(28 + (i % 6) * 2.5); + rows.push({ + key: 'mock-h2-' + (i + 1), + createdBy: creator.id, + creatorName: creator.name, + hydrogenTime: day, + stationId: station.value, + stationName: station.label, + plateNo: VEHICLE_INFO_TABLE[i % VEHICLE_INFO_TABLE.length], + mileageKm: 12000 + i * 320, + hydrogenKg: kg, + costUnitPrice: costUnit, + customerId: customer.value, + customerName: customer.label, + customerUnitPrice: custUnit, + salesperson: customer.salesperson, + settlementStatus: ['customer', 'internal', 'customer_self', 'other'][i % 4], + invoiceDate: day, + reconcileDate: day, + paymentStatus: i % 3 === 0 ? 'paid' : i % 3 === 1 ? 'partial' : 'unpaid', + companyId: company.value, + companyName: company.label, + remark: '', + createdAt: day, + reconcileStatus: reconcileStatus, + savedAt: reconcileStatus !== RECONCILE_DRAFT ? day : null, + reconciledAt: reconcileStatus === RECONCILE_RECONCILED ? day : null + }); + } + return recalcAllRows(rows); + } + + var layoutStyle = { + padding: '16px 24px 24px', + minHeight: '100vh', + background: 'linear-gradient(165deg, #eef4ff 0%, #f5f7fa 42%, #f0f2f5 100%)' + }; + + var filterLabelStyle = { marginBottom: 6, fontSize: 13, color: 'rgba(0,0,0,0.55)', fontWeight: 500 }; + var filterItemStyle = { marginBottom: 12 }; + var filterControlStyle = { width: '100%' }; + var filterCardStyle = { + marginBottom: 20, + borderRadius: 16, + boxShadow: '0 4px 20px -4px rgba(16,24,40,0.03), 0 0 0 1px rgba(16,24,40,0.06)', + border: 'none', + background: '#ffffff' + }; + + var tableCardStyle = { + borderRadius: 16, + boxShadow: '0 10px 32px -4px rgba(16,24,40,0.06), 0 0 0 1px rgba(16,24,40,0.04)', + border: 'none', + background: '#ffffff', + overflow: 'hidden' + }; + + var ledgerTableStyle = + '.h2-ledger-table-wrap{border-radius:12px;overflow:hidden;box-shadow:0 4px 24px -6px rgba(15,23,42,0.05),0 0 0 1px rgba(22,119,255,0.1)}' + + '.h2-ledger-table .ant-table-thead>tr>th,.h2-ledger-table .ant-table-thead .ant-table-cell{white-space:nowrap;color:#0f172a!important;font-weight:600!important;font-size:12px!important;' + + 'background:#e8f4fc!important;border-bottom:1px solid #bae6fd!important;border-inline-end:1px solid #dbeafe!important;padding:0 6px!important;height:36px!important;text-align:center!important}' + + '.h2-ledger-table .ant-table-tbody>tr:not(.ant-table-measure-row)>td{padding:4px 6px!important;vertical-align:middle!important;font-size:12px}' + + '.h2-ledger-table .ant-table-tbody>tr.h2-row-data:hover>td{background:#f0f9ff!important}' + + '.h2-ledger-totals-bar{display:flex;align-items:stretch;gap:0;margin-bottom:10px;border:1px solid #bae6fd;border-radius:10px;overflow:hidden;background:#f8fafc;box-shadow:0 1px 0 rgba(15,23,42,0.04)}' + + '.h2-ledger-totals-bar__title{display:flex;align-items:center;justify-content:center;min-width:72px;padding:10px 14px;font-size:14px;font-weight:700;color:#0f172a;background:#e8f4fc;border-right:1px solid #bae6fd}' + + '.h2-ledger-totals-bar__items{display:flex;flex:1;flex-wrap:wrap}' + + '.h2-ledger-totals-bar__item{flex:1;min-width:160px;padding:8px 20px;border-right:1px solid #e2e8f0;display:flex;flex-direction:column;justify-content:center;gap:4px}' + + '.h2-ledger-totals-bar__item:last-child{border-right:none}' + + '.h2-ledger-totals-bar__label{font-size:12px;color:rgba(15,23,42,0.55);font-weight:500;line-height:1.2}' + + '.h2-ledger-totals-bar__value{font-size:16px;font-weight:700;color:#0f172a;font-variant-numeric:tabular-nums;line-height:1.3}' + + '.h2-cell-readonly{color:#64748b;background:#f8fafc;padding:4px 6px}' + + '.h2-cell-auto{color:#475569;background:#f1f5f9;padding:4px 6px}' + + '.h2-cell-warn{color:#874d00;background:#fffbe6!important;border:1px solid #ffe58f}' + + '.h2-warn-input-wrap .ant-input-number,.h2-warn-input-wrap .ant-input-number-input{background:#fffbe6!important}' + + '.h2-ledger-table .ant-picker.h2-datetime-picker{min-width:178px}' + + '.h2-row-tier-0>td{background:#f8fafc!important}' + + '.h2-row-tier-2>td{background:#fffbeb!important}' + + '.h2-row-tier-boundary>td{border-top:2px solid #7dd3fc!important}' + + '.h2-action-icon-btn{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:4px;cursor:pointer;color:rgba(15,23,42,0.55);transition:background .15s,color .15s}' + + '.h2-action-icon-btn:hover{background:#f0f9ff;color:#1677ff}' + + '.h2-action-icon-btn.h2-action-icon-danger:hover{background:#fff1f0;color:#ff4d4f}' + + '.h2-action-icon-btn.is-disabled{color:rgba(15,23,42,0.25);cursor:not-allowed;pointer-events:none}' + + '.h2-row-more-btn.h2-action-icon-btn:hover{background:#f5f5f5;color:rgba(15,23,42,0.75)}' + + '.h2-req-doc-panel{max-width:100%}' + + '.h2-req-doc-panel h1:first-child{margin-top:0}'; + + var rowsState = useState(function () { + var base = buildMockAllRows(); + var patches = (typeof window !== 'undefined' && window.H2_STATION_STATEMENT_LEDGER_UPDATES) || []; + return recalcAllRows(applyStatementPatchesToRows(base, patches)); + }); + var allRows = rowsState[0]; + var setAllRows = rowsState[1]; + + if (typeof window !== 'undefined') { + window.H2_VEHICLE_LEDGER_API = { + applyPatches: function (patches) { + if (!patches || !patches.length) return; + setAllRows(function (prev) { + return recalcAllRows(applyStatementPatchesToRows(prev, patches)); + }); + } + }; + } + + var dateRangeDraftState = useState(null); + var dateRangeDraft = dateRangeDraftState[0]; + var setDateRangeDraft = dateRangeDraftState[1]; + + var plateDraftState = useState(undefined); + var plateDraft = plateDraftState[0]; + var setPlateDraft = plateDraftState[1]; + + var customerDraftState = useState(undefined); + var customerDraft = customerDraftState[0]; + var setCustomerDraft = customerDraftState[1]; + + var stationDraftState = useState(undefined); + var stationDraft = stationDraftState[0]; + var setStationDraft = stationDraftState[1]; + + var dateRangeAppliedState = useState(null); + var dateRangeApplied = dateRangeAppliedState[0]; + var setDateRangeApplied = dateRangeAppliedState[1]; + + var plateAppliedState = useState(undefined); + var plateApplied = plateAppliedState[0]; + var setPlateApplied = plateAppliedState[1]; + + var customerAppliedState = useState(undefined); + var customerApplied = customerAppliedState[0]; + var setCustomerApplied = customerAppliedState[1]; + + var stationAppliedState = useState(undefined); + var stationApplied = stationAppliedState[0]; + var setStationApplied = stationAppliedState[1]; + + var orderNoDraftState = useState(''); + var orderNoDraft = orderNoDraftState[0]; + var setOrderNoDraft = orderNoDraftState[1]; + + var salespersonDraftState = useState(undefined); + var salespersonDraft = salespersonDraftState[0]; + var setSalespersonDraft = salespersonDraftState[1]; + + var settlementDraftState = useState(undefined); + var settlementDraft = settlementDraftState[0]; + var setSettlementDraft = settlementDraftState[1]; + + var paymentDraftState = useState(undefined); + var paymentDraft = paymentDraftState[0]; + var setPaymentDraft = paymentDraftState[1]; + + var companyDraftState = useState(undefined); + var companyDraft = companyDraftState[0]; + var setCompanyDraft = companyDraftState[1]; + + var orderNoAppliedState = useState(''); + var orderNoApplied = orderNoAppliedState[0]; + var setOrderNoApplied = orderNoAppliedState[1]; + + var salespersonAppliedState = useState(undefined); + var salespersonApplied = salespersonAppliedState[0]; + var setSalespersonApplied = salespersonAppliedState[1]; + + var settlementAppliedState = useState(undefined); + var settlementApplied = settlementAppliedState[0]; + var setSettlementApplied = settlementAppliedState[1]; + + var paymentAppliedState = useState(undefined); + var paymentApplied = paymentAppliedState[0]; + var setPaymentApplied = paymentAppliedState[1]; + + var companyAppliedState = useState(undefined); + var companyApplied = companyAppliedState[0]; + var setCompanyApplied = companyAppliedState[1]; + + var filterExpandedState = useState(false); + var filterExpanded = filterExpandedState[0]; + var setFilterExpanded = filterExpandedState[1]; + + var onlyShowWarnRowsState = useState(false); + var onlyShowWarnRows = onlyShowWarnRowsState[0]; + var setOnlyShowWarnRows = onlyShowWarnRowsState[1]; + + var isSupervisor = false; + + var editingKeysState = useState([]); + var editingKeys = editingKeysState[0]; + var setEditingKeys = editingKeysState[1]; + + var rowInvalidState = useState({}); + var rowInvalid = rowInvalidState[0]; + var setRowInvalid = rowInvalidState[1]; + + var selectedRowKeysState = useState([]); + var selectedRowKeys = selectedRowKeysState[0]; + var setSelectedRowKeys = selectedRowKeysState[1]; + + var changeLogsByKeyState = useState({}); + var changeLogsByKey = changeLogsByKeyState[0]; + var setChangeLogsByKey = changeLogsByKeyState[1]; + + var changeLogModalState = useState({ open: false, rowKey: null, orderNo: '' }); + var changeLogModal = changeLogModalState[0]; + var setChangeLogModal = changeLogModalState[1]; + + var reqDetailOpenState = useState(false); + var reqDetailOpen = reqDetailOpenState[0]; + var setReqDetailOpen = reqDetailOpenState[1]; + + var changeLogsSeededRef = useRef(false); + + var appendChangeLog = useCallback(function (rowKey, field, before, after, rowCtx, meta) { + if (!rowKey) return; + meta = meta || {}; + var fieldLabel = meta.fieldLabel || CHANGE_LOG_FIELD_LABELS[field] || field; + var beforeText = meta.beforeText != null ? meta.beforeText : formatLogValue(field, before, rowCtx); + var afterText = meta.afterText != null ? meta.afterText : formatLogValue(field, after, rowCtx); + if (!meta.force && beforeText === afterText) return; + setChangeLogsByKey(function (prev) { + var list = (prev[rowKey] || []).slice(); + list.unshift({ + id: 'clog-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8), + at: nowDayjs(), + userId: CURRENT_USER.id, + userName: CURRENT_USER.name, + field: field, + fieldLabel: fieldLabel, + before: beforeText, + after: afterText + }); + var next = Object.assign({}, prev); + next[rowKey] = list; + return next; + }); + }, []); + + var openChangeLogModal = useCallback(function (record) { + if (!record) return; + setChangeLogModal({ + open: true, + rowKey: record.key, + orderNo: record.orderNo || '' + }); + }, []); + + var closeChangeLogModal = useCallback(function () { + setChangeLogModal({ open: false, rowKey: null, orderNo: '' }); + }, []); + + var latestRefs = useRef({}); + latestRefs.current = { + setAllRows: setAllRows, + setEditingKeys: setEditingKeys, + setSelectedRowKeys: setSelectedRowKeys, + setRowInvalid: setRowInvalid, + editingKeys: editingKeys, + rowInvalid: rowInvalid, + isSupervisor: isSupervisor, + allRows: allRows, + appendChangeLog: appendChangeLog + }; + + React.useEffect(function () { + if (changeLogsSeededRef.current) return; + changeLogsSeededRef.current = true; + var demoAt = nowDayjs(); + var demoAt2 = demoAt && demoAt.subtract ? demoAt.subtract(2, 'hour') : demoAt; + setChangeLogsByKey({ + 'mock-h2-3': [ + { + id: 'clog-demo-2', + at: demoAt, + userId: CURRENT_USER.id, + userName: CURRENT_USER.name, + field: 'hydrogenKg', + fieldLabel: '加氢量(kg)', + before: '39.50', + after: '42.50' + }, + { + id: 'clog-demo-1', + at: demoAt2, + userId: CURRENT_USER.id, + userName: CURRENT_USER.name, + field: 'reconcileStatus', + fieldLabel: '状态', + before: '待保存', + after: '未对账' + } + ] + }); + }, []); + + var canEditRow = useCallback( + function (row) { + if (!row) return false; + if (isReconciledRow(row)) { + return isSupervisor && editingKeys.indexOf(row.key) >= 0; + } + if (isPendingRow(row)) return editingKeys.indexOf(row.key) >= 0; + if (isNewRow(row)) return row.createdBy === CURRENT_USER.id; + return false; + }, + [editingKeys, isSupervisor] + ); + + function filterRowsByAppliedFilters(rows) { + var list = rows.slice(); + if (plateApplied) list = list.filter(function (r) { return r.plateNo === plateApplied; }); + if (customerApplied) list = list.filter(function (r) { return r.customerId === customerApplied; }); + if (stationApplied) list = list.filter(function (r) { return r.stationId === stationApplied; }); + if (salespersonApplied) { + list = list.filter(function (r) { + return r.salesperson === salespersonApplied; + }); + } + if (settlementApplied) { + list = list.filter(function (r) { + return r.settlementStatus === settlementApplied; + }); + } + if (paymentApplied) { + list = list.filter(function (r) { + return r.customerReceiptStatus === paymentApplied; + }); + } + if (companyApplied) { + list = list.filter(function (r) { + return r.companyId === companyApplied; + }); + } + if (orderNoApplied && String(orderNoApplied).trim()) { + var kw = String(orderNoApplied).trim().toLowerCase(); + list = list.filter(function (r) { + return r.orderNo && String(r.orderNo).toLowerCase().indexOf(kw) >= 0; + }); + } + if (dateRangeApplied && dateRangeApplied[0] && dateRangeApplied[1] && window.dayjs) { + var start = window.dayjs(dateRangeApplied[0]).startOf('day'); + var end = window.dayjs(dateRangeApplied[1]).endOf('day'); + list = list.filter(function (r) { + if (!r.hydrogenTime) return false; + var d = window.dayjs(r.hydrogenTime); + return d.isAfter(start.subtract(1, 'ms')) && d.isBefore(end.add(1, 'ms')); + }); + } + return list; + } + + function appendCurrentUserDraftRows(list, sourceRows) { + var keyInList = {}; + list.forEach(function (r) { + keyInList[r.key] = true; + }); + (sourceRows || []).forEach(function (r) { + if (!keyInList[r.key] && isNewRow(r) && r.createdBy === CURRENT_USER.id) { + list.push(r); + keyInList[r.key] = true; + } + }); + return list; + } + + var visibleRows = useMemo(function () { + var list = filterRowsByAppliedFilters(allRows); + list = appendCurrentUserDraftRows(list, allRows); + if (onlyShowWarnRows) { + list = list.filter(function (r) { + if (isNewRow(r) && r.createdBy === CURRENT_USER.id) return true; + return isRowDataWarn(r); + }); + } + list = sortRowsByDisplayTier(list); + return list.map(function (r, idx) { + return Object.assign({}, r, { seq: idx + 1, displayTier: rowDisplaySortTier(r) }); + }); + }, [ + allRows, + plateApplied, + customerApplied, + stationApplied, + dateRangeApplied, + orderNoApplied, + salespersonApplied, + settlementApplied, + paymentApplied, + companyApplied, + onlyShowWarnRows + ]); + + /** 导出:按已生效筛选条件,仅含未对账/已对账(不含待保存) */ + var exportRows = useMemo(function () { + var list = filterRowsByAppliedFilters(allRows).filter(function (r) { + return isPendingRow(r) || isReconciledRow(r); + }); + list = sortRowsByDisplayTier(list); + return list.map(function (r, idx) { + return Object.assign({}, r, { seq: idx + 1, displayTier: rowDisplaySortTier(r) }); + }); + }, [ + allRows, + plateApplied, + customerApplied, + stationApplied, + dateRangeApplied, + orderNoApplied, + salespersonApplied, + settlementApplied, + paymentApplied, + companyApplied + ]); + + var totals = useMemo(function () { + return visibleRows.reduce( + function (acc, r) { + acc.hydrogenKg += numOrZero(r.hydrogenKg); + acc.costTotal += numOrZero(r.costTotal); + acc.customerAmount += numOrZero(r.customerAmount); + return acc; + }, + { hydrogenKg: 0, costTotal: 0, customerAmount: 0 } + ); + }, [visibleRows]); + + var handleQuery = useCallback(function () { + setPlateApplied(plateDraft); + setCustomerApplied(customerDraft); + setStationApplied(stationDraft); + setDateRangeApplied(dateRangeDraft); + setOrderNoApplied(orderNoDraft); + setSalespersonApplied(salespersonDraft); + setSettlementApplied(settlementDraft); + setPaymentApplied(paymentDraft); + setCompanyApplied(companyDraft); + message.success('查询成功'); + }, [ + plateDraft, + customerDraft, + stationDraft, + dateRangeDraft, + orderNoDraft, + salespersonDraft, + settlementDraft, + paymentDraft, + companyDraft + ]); + + var handleReset = useCallback(function () { + setDateRangeDraft(null); + setPlateDraft(undefined); + setCustomerDraft(undefined); + setStationDraft(undefined); + setOrderNoDraft(''); + setSalespersonDraft(undefined); + setSettlementDraft(undefined); + setPaymentDraft(undefined); + setCompanyDraft(undefined); + setDateRangeApplied(null); + setPlateApplied(undefined); + setCustomerApplied(undefined); + setStationApplied(undefined); + setOrderNoApplied(''); + setSalespersonApplied(undefined); + setSettlementApplied(undefined); + setPaymentApplied(undefined); + setCompanyApplied(undefined); + }, []); + + function validateMaintainableRows(rows) { + var allInvalid = {}; + (rows || []).forEach(function (r) { + var inv = getRowInvalidMap(r); + if (Object.keys(inv).length > 0) { + allInvalid[r.key] = inv; + } + }); + if (Object.keys(allInvalid).length > 0) { + setRowInvalid(allInvalid); + return false; + } + setRowInvalid({}); + return true; + } + + var handleSave = useCallback(function () { + var toSave = allRows.filter(function (r) { + return isNewRow(r); + }); + if (!toSave.length) return; + if (!validateMaintainableRows(toSave)) return; + var savedAt = nowDayjs(); + toSave.forEach(function (r) { + appendChangeLog(r.key, 'reconcileStatus', r.reconcileStatus, RECONCILE_PENDING, r, { force: true }); + }); + setAllRows(function (prev) { + var next = prev.map(function (r) { + if (isNewRow(r)) { + return Object.assign({}, r, { reconcileStatus: RECONCILE_PENDING, savedAt: savedAt }); + } + return r; + }); + return recalcAllRows(next); + }); + setEditingKeys([]); + }, [allRows, appendChangeLog]); + + var handleCompleteReconcile = useCallback(function () { + if (!selectedRowKeys.length) { + message.warning('请先勾选需要对账的记录'); + return; + } + var keySet = {}; + selectedRowKeys.forEach(function (k) { keySet[k] = true; }); + var selected = allRows.filter(function (r) { return keySet[r.key]; }); + var notPending = selected.filter(function (r) { return !isPendingRow(r); }); + if (notPending.length) { + message.warning('仅可对「未对账」状态的记录完成对账'); + return; + } + var reconciledAt = nowDayjs(); + selected.forEach(function (r) { + appendChangeLog(r.key, 'reconcileStatus', RECONCILE_PENDING, RECONCILE_RECONCILED, r, { force: true }); + }); + setAllRows(function (prev) { + return prev.map(function (r) { + if (keySet[r.key] && isPendingRow(r)) { + return Object.assign({}, r, { reconcileStatus: RECONCILE_RECONCILED, reconciledAt: reconciledAt }); + } + return r; + }); + }); + setSelectedRowKeys([]); + setEditingKeys([]); + message.success('已完成对账,共 ' + selected.length + ' 条'); + }, [selectedRowKeys, allRows, appendChangeLog]); + + var rowSelection = useMemo( + function () { + return { + selectedRowKeys: selectedRowKeys, + onChange: function (keys) { + setSelectedRowKeys(keys); + }, + getCheckboxProps: function (record) { + return { disabled: !isPendingRow(record) }; + } + }; + }, + [selectedRowKeys] + ); + + var addRow = useCallback(function () { + var row = buildEmptyRow(CURRENT_USER); + appendChangeLog(row.key, '_create', null, null, row, { + fieldLabel: '新增记录', + beforeText: '—', + afterText: '新增空白行', + force: true + }); + setAllRows(function (prev) { + return recalcAllRows(prev.concat([row])); + }); + message.success('已新增一行,请填写氢费信息'); + }, [appendChangeLog]); + + var copyPopoverKeyState = useState(null); + var copyPopoverKey = copyPopoverKeyState[0]; + var setCopyPopoverKey = copyPopoverKeyState[1]; + var copyRowCountState = useState(1); + var copyRowCount = copyRowCountState[0]; + var setCopyRowCount = copyRowCountState[1]; + + var applyCopyRows = useCallback(function (sourceKey, count) { + var n = Math.floor(Number(count)); + if (!isFinite(n) || n < 1) { + message.warning('请输入大于0的复制行数'); + return; + } + if (n > 100) { + message.warning('单次最多复制100行'); + return; + } + var didCopy = false; + setAllRows(function (prev) { + var source = null; + var i; + for (i = 0; i < prev.length; i++) { + if (prev[i].key === sourceKey) { + source = prev[i]; + break; + } + } + if (!source || !canCopyRow(source)) return prev; + var copies = []; + for (i = 0; i < n; i++) { + copies.push(buildCopiedRow(source)); + } + var ci; + for (ci = 0; ci < copies.length; ci++) { + appendChangeLog(copies[ci].key, '_copy', null, null, source, { + fieldLabel: '复制新增', + beforeText: '—', + afterText: '复制自「' + (source.orderNo || '当前行') + '」', + force: true + }); + } + didCopy = true; + return recalcAllRows(prev.concat(copies)); + }); + setCopyPopoverKey(null); + if (didCopy) message.success('已复制 ' + n + ' 行'); + else message.warning('无法复制该条记录'); + }, [appendChangeLog]); + + var importModalOpenState = useState(false); + var importModalOpen = importModalOpenState[0]; + var setImportModalOpen = importModalOpenState[1]; + var importFileListState = useState([]); + var importFileList = importFileListState[0]; + var setImportFileList = importFileListState[1]; + + var openImportModal = useCallback(function () { + setImportFileList([]); + setImportModalOpen(true); + }, []); + + var closeImportModal = useCallback(function () { + setImportModalOpen(false); + setImportFileList([]); + }, []); + + var exportColumnDefs = useMemo(function () { + return buildExportColumnDefs(); + }, []); + + var exportModalOpenState = useState(false); + var exportModalOpen = exportModalOpenState[0]; + var setExportModalOpen = exportModalOpenState[1]; + var exportSelectedKeysState = useState([]); + var exportSelectedKeys = exportSelectedKeysState[0]; + var setExportSelectedKeys = exportSelectedKeysState[1]; + + var openExportModal = useCallback(function () { + setExportSelectedKeys(exportColumnDefs.map(function (d) { return d.key; })); + setExportModalOpen(true); + }, [exportColumnDefs]); + + var closeExportModal = useCallback(function () { + setExportModalOpen(false); + }, []); + + var selectAllExportFields = useCallback(function () { + setExportSelectedKeys(exportColumnDefs.map(function (d) { return d.key; })); + }, [exportColumnDefs]); + + var clearAllExportFields = useCallback(function () { + setExportSelectedKeys([]); + }, []); + + var handleConfirmExport = useCallback(function () { + if (!exportSelectedKeys || !exportSelectedKeys.length) { + message.warning('请至少选择一个导出字段'); + return; + } + if (!exportRows.length) { + message.warning('当前筛选条件下暂无未对账或已对账数据可导出'); + return; + } + var keySet = {}; + var i; + for (i = 0; i < exportSelectedKeys.length; i++) { + keySet[exportSelectedKeys[i]] = true; + } + var selectedDefs = exportColumnDefs.filter(function (d) { + return keySet[d.key]; + }); + var headers = selectedDefs.map(function (d) { return d.label; }); + var rows = exportRows.map(function (r) { + return selectedDefs.map(function (d) { return d.getValue(r); }); + }); + var stamp = ''; + try { + stamp = window.dayjs ? window.dayjs().format('YYYYMMDD_HHmmss') : String(Date.now()); + } catch (eExp) { + stamp = String(Date.now()); + } + downloadCsv('车辆氢费明细_' + stamp + '.csv', headers, rows); + setExportModalOpen(false); + message.success('已导出 ' + rows.length + ' 条记录'); + }, [exportSelectedKeys, exportColumnDefs, exportRows]); + + var downloadImportTemplate = useCallback(function () { + var sample = [ + [ + '2026-05-19 10:30:00', + '广安弘正能源官盛综合能源站', + '浙AD12345F', + '12580.50', + '42.50', + '30.00', + '嘉兴市鑫峤供应链科技有限公司', + '32.00', + '客户承担', + '' + ] + ]; + downloadCsv('车辆氢费明细导入模板.csv', IMPORT_TEMPLATE_HEADERS, sample); + message.success('已下载导入模板'); + }, []); + + var handleImportFile = useCallback(function (file) { + if (!file) return; + var name = String(file.name || '').toLowerCase(); + if (name.endsWith('.xls') || name.endsWith('.xlsx')) { + message.warning('原型环境请使用 CSV 模板导入;联调后将支持 Excel 直接上传'); + setImportFileList([file]); + return; + } + if (!name.endsWith('.csv')) { + message.error('仅支持 .csv、.xls、.xlsx 格式'); + return; + } + setImportFileList([file]); + var reader = new FileReader(); + reader.onload = function (ev) { + var text = (ev && ev.target && ev.target.result) || ''; + var parsed = parseImportCsv(text); + if (!parsed.length) { + message.warning('未解析到有效数据,请检查文件内容'); + return; + } + var imported = []; + var pi; + for (pi = 0; pi < parsed.length; pi++) { + imported.push(buildRowFromImport(parsed[pi])); + } + for (pi = 0; pi < imported.length; pi++) { + appendChangeLog(imported[pi].key, '_import', null, null, imported[pi], { + fieldLabel: '批量导入', + beforeText: '—', + afterText: '导入创建', + force: true + }); + } + setAllRows(function (prev) { + return recalcAllRows(prev.concat(imported)); + }); + setImportModalOpen(false); + setImportFileList([]); + message.success('导入成功,共 ' + imported.length + ' 条,请核对后点击保存'); + }; + reader.onerror = function () { + message.error('文件读取失败'); + }; + reader.readAsText(file, 'UTF-8'); + }, [appendChangeLog]); + + var batchCostOpenState = useState(false); + var batchCostOpen = batchCostOpenState[0]; + var setBatchCostOpen = batchCostOpenState[1]; + var batchCostValueState = useState(null); + var batchCostValue = batchCostValueState[0]; + var setBatchCostValue = batchCostValueState[1]; + + var batchCustomerOpenState = useState(false); + var batchCustomerOpen = batchCustomerOpenState[0]; + var setBatchCustomerOpen = batchCustomerOpenState[1]; + var batchCustomerValueState = useState(null); + var batchCustomerValue = batchCustomerValueState[0]; + var setBatchCustomerValue = batchCustomerValueState[1]; + + var batchSettlementOpenState = useState(false); + var batchSettlementOpen = batchSettlementOpenState[0]; + var setBatchSettlementOpen = batchSettlementOpenState[1]; + var batchSettlementValueState = useState(undefined); + var batchSettlementValue = batchSettlementValueState[0]; + var setBatchSettlementValue = batchSettlementValueState[1]; + + var applyBatchSettlement = useCallback(function (value, setPopoverOpen, setSelectValue) { + if (!value) { + message.warning('请选择承担方式'); + return; + } + var count = 0; + setAllRows(function (prev) { + var next = prev.map(function (r) { + if (!isRowBatchEditable(r)) return r; + if (!valuesEqualForLog('settlementStatus', r.settlementStatus, value)) { + appendChangeLog(r.key, 'settlementStatus', r.settlementStatus, value, r); + } + count += 1; + return Object.assign({}, r, { settlementStatus: value }); + }); + return recalcAllRows(next); + }); + setPopoverOpen(false); + setSelectValue(undefined); + message.success(count > 0 ? '已批量更新 ' + count + ' 条记录的承担方式' : '暂无可批量修改的记录'); + }, [appendChangeLog]); + + var applyBatchUnitPrice = useCallback(function (field, rawValue, setPopoverOpen, setInputValue) { + var normalized = normalizeBatchUnitPrice(rawValue); + if (normalized == null) { + message.warning('请输入大于0的单价,最多保留2位小数'); + return; + } + var count = 0; + setAllRows(function (prev) { + var next = prev.map(function (r) { + if (!isRowBatchEditable(r)) return r; + if (!valuesEqualForLog(field, r[field], normalized)) { + appendChangeLog(r.key, field, r[field], normalized, r); + } + count += 1; + var patchObj = {}; + patchObj[field] = normalized; + return Object.assign({}, r, patchObj); + }); + return recalcAllRows(next); + }); + setPopoverOpen(false); + setInputValue(null); + var label = field === 'costUnitPrice' ? '成本单价' : '加氢单价'; + message.success(count > 0 ? '已批量更新 ' + count + ' 条记录的' + label : '暂无可批量修改的记录'); + }, [appendChangeLog]); + + var cellInputStyle = { width: '100%' }; + var cellNumStyle = { width: '100%' }; + var hydrogenDateTimePickerStyle = { width: '100%', minWidth: 178 }; + + var columns = useMemo(function () { + function patch(key, field, val) { + var ref = latestRefs.current; + var patchObj = {}; + patchObj[field] = val; + ref.setAllRows(function (prev) { + var next = prev.map(function (r) { + if (r.key !== key) return r; + if (ref.appendChangeLog && !valuesEqualForLog(field, r[field], val)) { + ref.appendChangeLog(key, field, r[field], val, r); + } + return Object.assign({}, r, patchObj); + }); + return recalcAllRows(next); + }); + ref.setRowInvalid(function (prev) { + if (!prev || !prev[key] || !prev[key][field]) return prev; + var next = Object.assign({}, prev); + var rowInv = Object.assign({}, next[key]); + delete rowInv[field]; + if (Object.keys(rowInv).length === 0) delete next[key]; + else next[key] = rowInv; + return next; + }); + } + + function autoLinkedCell(v, align) { + var text = v == null || v === '' ? '—' : v; + return React.createElement( + 'span', + { className: 'h2-cell-auto', style: { display: 'block', textAlign: align || 'center' } }, + text + ); + } + + function autoLinkedPaidStatus(v) { + var label = labelOf(BINARY_PAID_STATUS_OPTIONS, v); + if (!label || label === '-') return autoLinkedCell('—', 'center'); + var color = v === 'paid' ? 'success' : 'default'; + return React.createElement( + 'span', + { className: 'h2-cell-auto', style: { display: 'block', textAlign: 'center' } }, + React.createElement(Tag, { color: color, style: { margin: 0 } }, label) + ); + } + + function readOnlyCell(v, align, warn, tip) { + var cls = warn ? 'h2-cell-readonly h2-cell-warn' : 'h2-cell-readonly'; + var inner = React.createElement( + 'div', + { className: cls, style: { textAlign: align || 'center', whiteSpace: 'nowrap' } }, + v === 0 || v === '0' ? v : v || '-' + ); + if (warn && tip) return React.createElement(Tooltip, { title: tip }, inner); + return inner; + } + + function wrapWarnInput(node, warn, tip) { + if (!warn || !tip) return node; + return React.createElement( + Tooltip, + { title: tip }, + React.createElement('div', { className: 'h2-warn-input-wrap', style: WARN_CELL_WRAP_STYLE }, node) + ); + } + + function fieldInvalid(record, field) { + var ref = latestRefs.current; + var rowInv = (ref.rowInvalid && ref.rowInvalid[record.key]) || {}; + return rowInv[field]; + } + + function reqTitle(text) { + return React.createElement( + 'span', + null, + React.createElement('span', { style: { color: '#ff4d4f', marginRight: 2 } }, '*'), + text + ); + } + + function renderBatchEditIcon() { + return React.createElement( + 'svg', + { + viewBox: '64 64 896 896', + width: 14, + height: 14, + fill: 'currentColor', + 'aria-hidden': true + }, + React.createElement('path', { + d: 'M257.7 752c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 0 0 0-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 0 0 9.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9z' + }) + ); + } + + function makeBatchPriceTitle(columnLabel, batchLabel, field, popOpen, setPopOpen, inputVal, setInputVal) { + var popContent = React.createElement( + 'div', + { + style: { minWidth: 200 }, + onClick: function (e) { + e.stopPropagation(); + } + }, + React.createElement( + Space, + { align: 'center', size: 4, wrap: true }, + React.createElement('span', { style: { color: 'rgba(0,0,0,0.88)' } }, batchLabel + ':'), + React.createElement(InputNumber, { + style: { width: 120 }, + min: 0.01, + precision: 2, + step: 0.01, + placeholder: '0.00', + value: inputVal, + onChange: function (val) { + setInputVal(val == null ? null : val); + }, + onBlur: function () { + setInputVal(normalizeBatchUnitPrice(inputVal)); + } + }), + React.createElement('span', null, '元') + ), + React.createElement( + 'div', + { style: { marginTop: 10, textAlign: 'right' } }, + React.createElement(Button, { + type: 'primary', + size: 'small', + onClick: function () { + applyBatchUnitPrice(field, inputVal, setPopOpen, setInputVal); + } + }, '确定') + ) + ); + + return React.createElement( + 'span', + { + style: { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + gap: 4, + whiteSpace: 'nowrap' + } + }, + reqTitle(columnLabel), + React.createElement( + Popover, + { + trigger: 'click', + open: popOpen, + placement: 'bottom', + onOpenChange: function (open) { + setPopOpen(open); + if (!open) setInputVal(null); + }, + content: popContent + }, + React.createElement( + 'span', + { + className: 'h2-batch-edit-icon', + style: { + color: '#1677ff', + cursor: 'pointer', + display: 'inline-flex', + alignItems: 'center', + lineHeight: 1 + }, + onClick: function (e) { + e.stopPropagation(); + } + }, + renderBatchEditIcon() + ) + ) + ); + } + + function makeBatchSettlementTitle(columnLabel, batchLabel, popOpen, setPopOpen, selectVal, setSelectVal) { + var popContent = React.createElement( + 'div', + { + style: { minWidth: 220 }, + onClick: function (e) { + e.stopPropagation(); + } + }, + React.createElement( + 'div', + { style: { marginBottom: 8, fontSize: 13, color: 'rgba(0,0,0,0.88)' } }, + batchLabel + ), + React.createElement(Select, { + style: { width: '100%' }, + placeholder: '请选择', + value: selectVal, + options: SETTLEMENT_STATUS_OPTIONS, + onChange: function (val) { + setSelectVal(val); + } + }), + React.createElement( + 'div', + { style: { marginTop: 10, textAlign: 'right' } }, + React.createElement(Button, { + type: 'primary', + size: 'small', + onClick: function () { + applyBatchSettlement(selectVal, setPopOpen, setSelectVal); + } + }, '确定') + ) + ); + + return React.createElement( + 'span', + { + style: { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + gap: 4, + whiteSpace: 'nowrap' + } + }, + reqTitle(columnLabel), + React.createElement( + Popover, + { + trigger: 'click', + open: popOpen, + placement: 'bottom', + onOpenChange: function (open) { + setPopOpen(open); + if (!open) setSelectVal(undefined); + }, + content: popContent + }, + React.createElement( + 'span', + { + className: 'h2-batch-edit-icon', + style: { + color: '#1677ff', + cursor: 'pointer', + display: 'inline-flex', + alignItems: 'center', + lineHeight: 1 + }, + onClick: function (e) { + e.stopPropagation(); + } + }, + renderBatchEditIcon() + ) + ) + ); + } + + function labelOf(opts, val) { + var i; + for (i = 0; i < opts.length; i++) { + if (opts[i].value === val) return opts[i].label; + } + return val || '-'; + } + + return [ + { title: '序号', dataIndex: 'seq', key: 'seq', width: 52, align: 'center', fixed: 'left' }, + { + title: '加氢日期', + key: 'hydrogenDate', + width: 108, + align: 'center', + render: function (_, record) { + return readOnlyCell(formatDate(record.hydrogenTime) || '-'); + } + }, + { + title: reqTitle('加氢时间'), + dataIndex: 'hydrogenTime', + key: 'hydrogenTime', + width: 192, + align: 'center', + render: function (v, record) { + if (!canEditRow(record)) return readOnlyCell(formatDateTime(v) || '-'); + return React.createElement(DatePicker, { + className: 'h2-datetime-picker', + style: hydrogenDateTimePickerStyle, + showTime: { format: 'HH:mm:ss' }, + format: HYDROGEN_DATETIME_FORMAT, + placeholder: 'YYYY-MM-DD HH:mm:ss', + allowClear: true, + value: v, + status: fieldInvalid(record, 'hydrogenTime') ? 'error' : undefined, + onChange: function (d) { patch(record.key, 'hydrogenTime', d); } + }); + } + }, + { + title: reqTitle('加氢站名称'), + dataIndex: 'stationId', + key: 'stationId', + width: 150, + align: 'center', + render: function (v, record) { + if (!canEditRow(record)) return readOnlyCell(record.stationName); + return React.createElement(Select, { + style: cellInputStyle, + showSearch: true, + allowClear: true, + placeholder: '请选择', + value: v, + status: fieldInvalid(record, 'stationId') ? 'error' : undefined, + options: HYDROGEN_STATION_LIST.map(function (s) { return { value: s.value, label: s.label }; }), + filterOption: filterOption, + onChange: function (val) { patch(record.key, 'stationId', val); } + }); + } + }, + { + title: '客户名称', + dataIndex: 'customerId', + key: 'customerId', + width: 180, + align: 'center', + ellipsis: true, + render: function (v, record) { + if (!canEditRow(record)) { + return readOnlyCell(record.customerName || '-'); + } + return React.createElement(Select, { + style: cellInputStyle, + showSearch: true, + allowClear: true, + placeholder: '请选择', + value: v, + status: fieldInvalid(record, 'customerId') ? 'error' : undefined, + options: buildCustomerSelectOptions(), + filterOption: filterOption, + onChange: function (val) { patch(record.key, 'customerId', val); } + }); + } + }, + { + title: reqTitle('车牌号'), + dataIndex: 'plateNo', + key: 'plateNo', + width: 118, + align: 'center', + render: function (v, record) { + if (!canEditRow(record)) return readOnlyCell(v); + return React.createElement(Select, { + style: cellInputStyle, + showSearch: true, + allowClear: true, + placeholder: '请选择', + value: v, + status: fieldInvalid(record, 'plateNo') ? 'error' : undefined, + options: VEHICLE_INFO_TABLE.map(function (p) { return { value: p, label: p }; }), + filterOption: filterOption, + notFoundContent: '车辆不存在', + onChange: function (val) { patch(record.key, 'plateNo', val); } + }); + } + }, + { + title: reqTitle('加氢量(kg)'), + dataIndex: 'hydrogenKg', + key: 'hydrogenKg', + width: 100, + align: 'right', + render: function (v, record) { + var kgWarn = isHydrogenKgWarn(record); + if (!canEditRow(record)) return readOnlyCell(fmtQty(v), 'right', kgWarn, WARN_HYDROGEN_KG_TIP); + return wrapWarnInput( + React.createElement(InputNumber, { + style: cellNumStyle, + min: 0, + precision: 2, + placeholder: 'kg', + value: isEmptyNum(v) ? null : v, + status: fieldInvalid(record, 'hydrogenKg') ? 'error' : undefined, + onChange: function (val) { patch(record.key, 'hydrogenKg', val == null ? null : val); } + }), + kgWarn, + WARN_HYDROGEN_KG_TIP + ); + } + }, + { + title: makeBatchPriceTitle( + '成本单价(元/kg)', + '批量修改成本单价', + 'costUnitPrice', + batchCostOpen, + setBatchCostOpen, + batchCostValue, + setBatchCostValue + ), + dataIndex: 'costUnitPrice', + key: 'costUnitPrice', + width: 132, + align: 'right', + render: function (v, record) { + var costWarn = isCostPriceWarn(record); + if (!canEditRow(record)) return readOnlyCell(fmtMoney(v), 'right', costWarn, WARN_COST_PRICE_TIP); + return wrapWarnInput( + React.createElement(InputNumber, { + style: cellNumStyle, + min: 0, + precision: 2, + placeholder: '单价', + value: isEmptyNum(v) ? null : v, + status: fieldInvalid(record, 'costUnitPrice') ? 'error' : undefined, + onChange: function (val) { patch(record.key, 'costUnitPrice', val == null ? null : val); } + }), + costWarn, + WARN_COST_PRICE_TIP + ); + } + }, + { + title: '成本总价(元)', + dataIndex: 'costTotal', + key: 'costTotal', + width: 108, + align: 'right', + render: function (v) { return readOnlyCell(fmtMoney(v), 'right'); } + }, + { + title: makeBatchPriceTitle( + '加氢单价(元/kg)', + '批量修改加氢单价', + 'customerUnitPrice', + batchCustomerOpen, + setBatchCustomerOpen, + batchCustomerValue, + setBatchCustomerValue + ), + dataIndex: 'customerUnitPrice', + key: 'customerUnitPrice', + width: 132, + align: 'right', + render: function (v, record) { + var custWarn = isCustomerPriceWarn(record); + if (!canEditRow(record)) return readOnlyCell(fmtMoney(v), 'right', custWarn, WARN_CUSTOMER_PRICE_TIP); + return wrapWarnInput( + React.createElement(InputNumber, { + style: cellNumStyle, + min: 0, + precision: 2, + placeholder: '单价', + value: isEmptyNum(v) ? null : v, + status: fieldInvalid(record, 'customerUnitPrice') ? 'error' : undefined, + onChange: function (val) { patch(record.key, 'customerUnitPrice', val == null ? null : val); } + }), + custWarn, + WARN_CUSTOMER_PRICE_TIP + ); + } + }, + { + title: '加氢总价(元)', + dataIndex: 'customerAmount', + key: 'customerAmount', + width: 108, + align: 'right', + render: function (v) { return readOnlyCell(fmtMoney(v), 'right'); } + }, + { + title: '行驶里程(km)', + dataIndex: 'mileageKm', + key: 'mileageKm', + width: 108, + align: 'right', + render: function (v, record) { + if (!canEditRow(record)) return readOnlyCell(isEmptyNum(v) ? '-' : fmtQty(v), 'right'); + return React.createElement(InputNumber, { + style: cellNumStyle, + min: 0.01, + precision: 2, + step: 0.01, + placeholder: '0.00', + value: isEmptyNum(v) ? null : v, + onChange: function (val) { patch(record.key, 'mileageKm', val == null ? null : val); }, + onBlur: function () { + patch(record.key, 'mileageKm', normalizeMileageKm(v)); + } + }); + } + }, + { + title: '备注', + dataIndex: 'remark', + key: 'remark', + width: 120, + align: 'center', + render: function (v, record) { + if (!canEditRow(record)) return readOnlyCell(v); + return React.createElement(Input, { + className: 'h2-inline-input', + value: v, + placeholder: '备注', + onChange: function (e) { patch(record.key, 'remark', e.target.value); } + }); + } + }, + { + title: '业务员', + dataIndex: 'salesperson', + key: 'salesperson', + width: 88, + align: 'center', + render: function (v) { return readOnlyCell(v); } + }, + { + title: makeBatchSettlementTitle( + '承担方式', + '批量修改承担方式', + batchSettlementOpen, + setBatchSettlementOpen, + batchSettlementValue, + setBatchSettlementValue + ), + dataIndex: 'settlementStatus', + key: 'settlementStatus', + width: 132, + align: 'center', + render: function (v, record) { + if (!canEditRow(record)) return readOnlyCell(labelOf(SETTLEMENT_STATUS_OPTIONS, v)); + return React.createElement(Select, { + style: cellInputStyle, + placeholder: '请选择', + value: v, + status: fieldInvalid(record, 'settlementStatus') ? 'error' : undefined, + options: SETTLEMENT_STATUS_OPTIONS, + onChange: function (val) { patch(record.key, 'settlementStatus', val); } + }); + } + }, + { + title: '对账日期', + dataIndex: 'reconcileDate', + key: 'reconcileDate', + width: 148, + align: 'center', + render: function (v) { + if (!v) return readOnlyCell('-'); + var text = String(v); + if (text.indexOf(':') >= 0) return readOnlyCell(text.length > 16 ? text.slice(0, 16) : text); + return readOnlyCell(formatDateTime(v) || formatDate(v) || '-'); + } + }, + { + title: '收票日期', + dataIndex: 'receiptDate', + key: 'receiptDate', + width: 112, + align: 'center', + render: function (v) { + return autoLinkedCell(formatDate(v) || '—', 'center'); + } + }, + { + title: '加氢站付款状态', + dataIndex: 'stationPaymentStatus', + key: 'stationPaymentStatus', + width: 120, + align: 'center', + render: function (v) { + return autoLinkedPaidStatus(v); + } + }, + { + title: '开票日期', + dataIndex: 'invoiceDate', + key: 'invoiceDate', + width: 112, + align: 'center', + render: function (v) { + return autoLinkedCell(formatDate(v) || '—', 'center'); + } + }, + { + title: '客户收款状态', + dataIndex: 'customerReceiptStatus', + key: 'customerReceiptStatus', + width: 112, + align: 'center', + render: function (v) { + return autoLinkedPaidStatus(v); + } + }, + { + title: '开票公司', + dataIndex: 'companyId', + key: 'companyId', + width: 180, + align: 'center', + ellipsis: true, + render: function (v, record) { + return autoLinkedCell(record.companyName || labelOf(COMPANY_LIST, v) || '—', 'center'); + } + }, + { + title: '状态', + key: 'recordStatus', + width: 72, + align: 'center', + render: function (_, record) { + var label = rowStatusLabel(record); + if (!label) return React.createElement('span', { style: { color: '#cbd5e1' } }, '—'); + var color = label === '已对账' ? 'blue' : 'orange'; + return React.createElement(Tag, { color: color, style: { margin: 0 } }, label); + } + }, + { + title: '订单编号', + dataIndex: 'orderNo', + key: 'orderNo', + width: 168, + align: 'center', + render: function (v) { + return React.createElement('span', { style: { fontFamily: 'monospace', fontSize: 12 } }, v || '-'); + } + }, + { + title: '操作', + key: 'action', + width: 56, + align: 'center', + fixed: 'right', + render: function (_, record) { + var ref = latestRefs.current; + + function renderIconAction(title, iconNode, onClick, extraClass, disabled) { + var cls = 'h2-action-icon-btn' + (extraClass ? ' ' + extraClass : '') + (disabled ? ' is-disabled' : ''); + var node = React.createElement( + 'span', + { + className: cls, + role: 'button', + tabIndex: disabled ? -1 : 0, + 'aria-label': title, + onClick: + disabled || !onClick + ? undefined + : function (e) { + e.stopPropagation(); + onClick(e); + } + }, + iconNode + ); + return React.createElement(Tooltip, { title: title }, node); + } + + function renderMoreIcon() { + return React.createElement( + 'svg', + { viewBox: '0 0 16 16', width: 16, height: 16, fill: 'currentColor', 'aria-hidden': true }, + React.createElement('circle', { cx: '8', cy: '3', r: '1.5' }), + React.createElement('circle', { cx: '8', cy: '8', r: '1.5' }), + React.createElement('circle', { cx: '8', cy: '13', r: '1.5' }) + ); + } + + function toggleEditRow() { + ref.setEditingKeys(function (prev) { + if (prev.indexOf(record.key) >= 0) { + return prev.filter(function (k) { return k !== record.key; }); + } + return prev.concat([record.key]); + }); + } + + function confirmDeleteRow() { + ref.setAllRows(function (prev) { + return prev.filter(function (r) { return r.key !== record.key; }); + }); + ref.setSelectedRowKeys(function (prev) { + return prev.filter(function (k) { return k !== record.key; }); + }); + ref.setEditingKeys(function (prev) { + return prev.filter(function (k) { return k !== record.key; }); + }); + message.success('已删除'); + } + + function buildMoreMenuItems() { + var items = []; + var isEditing = ref.editingKeys.indexOf(record.key) >= 0; + var allowDelete = canDeleteRow(record, ref.isSupervisor); + var allowCopy = canCopyRow(record); + var editLabel = isEditing ? '完成编辑' : '编辑'; + + if (isReconciledRow(record)) { + if (!ref.isSupervisor) { + items.push({ + key: 'edit', + label: '编辑', + disabled: true, + title: '仅主管可编辑已对账数据' + }); + } else { + items.push({ + key: 'edit', + label: editLabel, + onClick: function () { toggleEditRow(); } + }); + } + } else if (isPendingRow(record) && canShowPendingActions(record)) { + items.push({ + key: 'edit', + label: editLabel, + onClick: function () { toggleEditRow(); } + }); + } else if (isNewRow(record) && record.createdBy === CURRENT_USER.id) { + items.push({ + key: 'edit', + label: editLabel, + onClick: function () { toggleEditRow(); } + }); + } + + if (allowCopy) { + items.push({ + key: 'copy', + label: '复制', + onClick: function () { + setCopyPopoverKey(record.key); + setCopyRowCount(1); + } + }); + } + + if (isReconciledRow(record)) { + if (!ref.isSupervisor) { + items.push({ + key: 'delete', + label: '删除', + disabled: true, + danger: true, + title: '仅主管可删除已对账数据' + }); + } else { + items.push({ + key: 'delete', + label: '删除', + danger: true, + onClick: function () { + Modal.confirm({ + title: '确认删除该条氢费记录?', + okText: '删除', + okType: 'danger', + cancelText: '取消', + onOk: confirmDeleteRow + }); + } + }); + } + } else if ( + (isPendingRow(record) && canShowPendingActions(record)) || + (isNewRow(record) && record.createdBy === CURRENT_USER.id) + ) { + items.push({ + key: 'delete', + label: '删除', + disabled: !allowDelete, + danger: true, + onClick: function () { + if (!allowDelete) return; + Modal.confirm({ + title: '确认删除该条氢费记录?', + okText: '删除', + okType: 'danger', + cancelText: '取消', + onOk: confirmDeleteRow + }); + } + }); + } + + if (items.length) { + items.push({ type: 'divider', key: 'divider-actions' }); + } + items.push({ + key: 'changelog', + label: '变更日志', + onClick: function () { + openChangeLogModal(record); + } + }); + return items; + } + + function renderCopyPopoverContent() { + var popOpen = copyPopoverKey === record.key; + return React.createElement( + 'div', + { style: { width: 220 } }, + React.createElement( + 'div', + { style: { marginBottom: 8, fontSize: 13, color: 'rgba(15,23,42,0.65)' } }, + '复制当前行已填写的全部数据' + ), + React.createElement( + 'div', + { style: { marginBottom: 6, fontSize: 13, color: 'rgba(15,23,42,0.55)' } }, + '复制行数' + ), + React.createElement(InputNumber, { + style: { width: '100%', marginBottom: 10 }, + min: 1, + max: 100, + precision: 0, + value: popOpen ? copyRowCount : 1, + onChange: function (val) { + setCopyRowCount(val == null ? 1 : val); + } + }), + React.createElement( + Button, + { + type: 'primary', + block: true, + size: 'small', + onClick: function () { + applyCopyRows(record.key, copyRowCount); + setCopyPopoverKey(null); + } + }, + '确认复制' + ) + ); + } + + function renderMoreAction() { + var popOpen = copyPopoverKey === record.key; + var dropdownNode = React.createElement( + Dropdown, + { + trigger: ['click'], + placement: 'bottomRight', + menu: { items: buildMoreMenuItems() } + }, + renderIconAction('更多', renderMoreIcon(), null, 'h2-row-more-btn') + ); + return React.createElement( + Popover, + { + trigger: [], + placement: 'leftTop', + open: popOpen, + content: renderCopyPopoverContent(), + onOpenChange: function (open) { + if (!open && copyPopoverKey === record.key) { + setCopyPopoverKey(null); + } + } + }, + dropdownNode + ); + } + + return renderMoreAction(); + } + } + ]; + }, [ + canEditRow, + editingKeys, + isSupervisor, + rowInvalid, + batchCostOpen, + batchCostValue, + batchCustomerOpen, + batchCustomerValue, + batchSettlementOpen, + batchSettlementValue, + applyBatchUnitPrice, + applyBatchSettlement, + copyPopoverKey, + copyRowCount, + applyCopyRows, + openChangeLogModal + ]); + + var changeLogRows = useMemo( + function () { + if (!changeLogModal.rowKey) return []; + return changeLogsByKey[changeLogModal.rowKey] || []; + }, + [changeLogModal.rowKey, changeLogsByKey] + ); + + var changeLogColumns = useMemo( + function () { + return [ + { + title: '修改时间', + dataIndex: 'at', + key: 'at', + width: 168, + render: function (v) { + return formatDateTime(v) || '—'; + } + }, + { + title: '修改人', + dataIndex: 'userName', + key: 'userName', + width: 88, + align: 'center' + }, + { + title: '修改字段', + dataIndex: 'fieldLabel', + key: 'fieldLabel', + width: 120, + align: 'center' + }, + { + title: '修改前', + dataIndex: 'before', + key: 'before', + ellipsis: true, + render: function (v) { + return React.createElement( + 'span', + { style: { color: '#cf1322' } }, + v == null || v === '' ? '—' : v + ); + } + }, + { + title: '修改后', + dataIndex: 'after', + key: 'after', + ellipsis: true, + render: function (v) { + return React.createElement( + 'span', + { style: { color: '#389e0d' } }, + v == null || v === '' ? '—' : v + ); + } + } + ]; + }, + [] + ); + + var renderTotalsBar = useCallback(function () { + var items = [ + { key: 'hydrogenKg', label: '加氢量(kg)', value: fmtQty(totals.hydrogenKg) }, + { key: 'costTotal', label: '成本总价(元)', value: fmtMoney(totals.costTotal) }, + { key: 'customerAmount', label: '加氢总价(元)', value: fmtMoney(totals.customerAmount) } + ]; + return React.createElement( + 'div', + { className: 'h2-ledger-totals-bar' }, + React.createElement('div', { className: 'h2-ledger-totals-bar__title' }, '合计'), + React.createElement( + 'div', + { className: 'h2-ledger-totals-bar__items' }, + items.map(function (item) { + return React.createElement( + 'div', + { key: item.key, className: 'h2-ledger-totals-bar__item' }, + React.createElement('div', { className: 'h2-ledger-totals-bar__label' }, item.label), + React.createElement('div', { className: 'h2-ledger-totals-bar__value' }, item.value) + ); + }) + ) + ); + }, [totals]); + + return React.createElement( + App, + null, + React.createElement('style', null, ledgerTableStyle), + React.createElement( + 'div', + { style: layoutStyle }, + React.createElement( + 'div', + { + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: 12, + gap: 16 + } + }, + React.createElement(Breadcrumb, { + items: [{ title: '台账数据' }, { title: '车辆氢费明细' }] + }), + React.createElement( + Button, + { + type: 'link', + style: { padding: 0, flexShrink: 0, fontSize: 14 }, + onClick: function () { + setReqDetailOpen(true); + } + }, + '查看需求明细' + ) + ), + (function () { + var filterColProps = { xs: 24, sm: 12, lg: 8 }; + var filterActions = React.createElement( + Space, + { wrap: true, style: { justifyContent: 'flex-end', width: '100%' } }, + React.createElement( + Button, + { + type: 'link', + style: { padding: '4px 0' }, + onClick: function () { + setFilterExpanded(!filterExpanded); + } + }, + filterExpanded ? '收起' : '展开' + ), + React.createElement(Button, { onClick: handleReset }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleQuery }, '查询') + ); + var plateSelect = React.createElement(Select, { + allowClear: true, + showSearch: true, + placeholder: '全部', + style: filterControlStyle, + value: plateDraft, + onChange: function (v) { setPlateDraft(v); }, + options: VEHICLE_INFO_TABLE.map(function (p) { return { value: p, label: p }; }), + filterOption: filterOption + }); + return React.createElement( + Card, + { style: filterCardStyle, bodyStyle: { paddingBottom: 4 } }, + React.createElement( + Row, + { gutter: [16, 0], align: 'bottom' }, + React.createElement( + Col, + filterColProps, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '加氢时间'), + React.createElement(DatePicker.RangePicker, { + style: filterControlStyle, + format: 'YYYY-MM-DD', + value: dateRangeDraft, + onChange: function (v) { setDateRangeDraft(v); } + }) + ) + ), + React.createElement( + Col, + filterColProps, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '订单编号'), + React.createElement(Input, { + allowClear: true, + placeholder: '支持模糊搜索', + style: filterControlStyle, + value: orderNoDraft, + onChange: function (e) { + setOrderNoDraft(e && e.target ? e.target.value : ''); + } + }) + ) + ), + React.createElement( + Col, + filterColProps, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, filterExpanded ? '车牌号' : '\u00a0'), + filterExpanded + ? plateSelect + : React.createElement('div', { style: { textAlign: 'right' } }, filterActions) + ) + ) + ), + filterExpanded + ? [ + React.createElement( + Row, + { key: 'filter-row-2', gutter: [16, 0], align: 'top' }, + React.createElement( + Col, + filterColProps, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '客户名称'), + React.createElement(Select, { + allowClear: true, + showSearch: true, + placeholder: '全部', + style: filterControlStyle, + value: customerDraft, + onChange: function (v) { setCustomerDraft(v); }, + options: buildCustomerSelectOptions(), + filterOption: filterOption + }) + ) + ), + React.createElement( + Col, + filterColProps, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '加氢站名称'), + React.createElement(Select, { + allowClear: true, + showSearch: true, + placeholder: '全部', + style: filterControlStyle, + value: stationDraft, + onChange: function (v) { setStationDraft(v); }, + options: HYDROGEN_STATION_LIST.map(function (s) { return { value: s.value, label: s.label }; }), + filterOption: filterOption + }) + ) + ), + React.createElement( + Col, + filterColProps, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '业务员'), + React.createElement(Select, { + allowClear: true, + showSearch: true, + placeholder: '全部', + style: filterControlStyle, + value: salespersonDraft, + onChange: function (v) { setSalespersonDraft(v); }, + options: SALESPERSON_FILTER_OPTIONS, + filterOption: filterOption + }) + ) + ) + ), + React.createElement( + Row, + { key: 'filter-row-3', gutter: [16, 0], align: 'top' }, + React.createElement( + Col, + filterColProps, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '承担方式'), + React.createElement(Select, { + allowClear: true, + placeholder: '全部', + style: filterControlStyle, + value: settlementDraft, + onChange: function (v) { setSettlementDraft(v); }, + options: SETTLEMENT_STATUS_OPTIONS + }) + ) + ), + React.createElement( + Col, + filterColProps, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '客户收款状态'), + React.createElement(Select, { + allowClear: true, + placeholder: '全部', + style: filterControlStyle, + value: paymentDraft, + onChange: function (v) { setPaymentDraft(v); }, + options: BINARY_PAID_STATUS_OPTIONS + }) + ) + ), + React.createElement( + Col, + filterColProps, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '开票公司'), + React.createElement(Select, { + allowClear: true, + showSearch: true, + placeholder: '全部', + style: filterControlStyle, + value: companyDraft, + onChange: function (v) { setCompanyDraft(v); }, + options: COMPANY_LIST, + filterOption: filterOption + }) + ) + ) + ), + React.createElement( + Row, + { key: 'filter-actions', justify: 'end', style: { marginTop: -4 } }, + React.createElement( + Col, + { span: 24 }, + React.createElement('div', { style: { textAlign: 'right', marginBottom: 4 } }, filterActions) + ) + ) + ] + : null + ); + })(), + React.createElement( + Card, + { style: tableCardStyle, bodyStyle: { padding: '20px 20px 24px' } }, + React.createElement( + 'div', + { + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: 8, + minHeight: 36, + gap: 12 + } + }, + React.createElement( + Checkbox, + { + checked: onlyShowWarnRows, + onChange: function (e) { + setOnlyShowWarnRows(e.target.checked); + } + }, + '仅显示异常数据' + ), + React.createElement( + 'div', + { + style: { + flex: 1, + textAlign: 'center', + fontSize: 18, + fontWeight: 700, + color: 'rgba(15,23,42,0.92)' + } + }, + '车辆氢费明细' + ), + React.createElement( + Space, + { size: 8 }, + React.createElement(Button, { onClick: openImportModal }, '批量导入'), + React.createElement(Button, { onClick: openExportModal }, '批量导出'), + React.createElement(Button, { onClick: handleSave }, '保存'), + React.createElement( + Popconfirm, + { + title: '完成对账后数据将标记为已对账,只能联系主管进行修改,是否确认。', + okText: '确认', + cancelText: '取消', + onConfirm: handleCompleteReconcile + }, + React.createElement(Button, { type: 'primary' }, '完成对账') + ) + ) + ), + renderTotalsBar(), + React.createElement( + 'div', + { className: 'h2-ledger-table-wrap' }, + React.createElement(Table, { + className: 'h2-ledger-table', + size: 'small', + bordered: true, + rowKey: 'key', + rowSelection: rowSelection, + columns: columns, + dataSource: visibleRows, + pagination: false, + rowClassName: function (record, index) { + var tier = record.displayTier != null ? record.displayTier : rowDisplaySortTier(record); + var prev = visibleRows[index - 1]; + var prevTier = prev ? (prev.displayTier != null ? prev.displayTier : rowDisplaySortTier(prev)) : tier; + var cls = 'h2-row-data h2-row-tier-' + tier; + if (index > 0 && prevTier !== tier) cls += ' h2-row-tier-boundary'; + return cls; + }, + scroll: { x: 'max-content', y: filterExpanded ? 'calc(100vh - 524px)' : 'calc(100vh - 464px)' }, + sticky: true + }) + ), + React.createElement( + Button, + { type: 'dashed', block: true, style: { marginTop: 12 }, onClick: addRow }, + '新增一行' + ) + ), + React.createElement( + Modal, + { + title: '批量导入', + open: importModalOpen, + onCancel: closeImportModal, + footer: null, + width: 520, + destroyOnClose: true + }, + React.createElement( + 'div', + { style: { marginBottom: 12, fontSize: 13, color: 'rgba(15,23,42,0.55)' } }, + '请下载模板填写后上传;支持 .csv、.xls、.xlsx(原型阶段 Excel 请另存为 CSV 后导入)。开票日期、对账日期、付款状态、开票公司由系统按客户等信息自动带出,无需在模板中填写。导入后为待保存状态,需点击「保存」标记为未对账。' + ), + React.createElement( + Upload.Dragger, + { + accept: '.csv,.xls,.xlsx', + multiple: false, + maxCount: 1, + fileList: importFileList, + beforeUpload: function (file) { + handleImportFile(file); + return false; + }, + onRemove: function () { + setImportFileList([]); + return true; + } + }, + React.createElement('p', { style: { margin: '8px 0 4px', color: 'rgba(15,23,42,0.65)' } }, '点击或拖拽文件到此处上传'), + React.createElement('p', { style: { margin: 0, fontSize: 12, color: 'rgba(15,23,42,0.45)' } }, '支持 .csv / .xls / .xlsx') + ), + React.createElement( + 'div', + { style: { marginTop: 12 } }, + React.createElement(Button, { type: 'link', onClick: downloadImportTemplate, style: { padding: 0 } }, '下载导入模板') + ) + ), + React.createElement( + Modal, + { + title: '批量导出', + open: exportModalOpen, + onCancel: closeExportModal, + width: 640, + destroyOnClose: true, + footer: [ + React.createElement(Button, { key: 'cancel', onClick: closeExportModal }, '取消'), + React.createElement(Button, { key: 'ok', type: 'primary', onClick: handleConfirmExport }, '确认导出') + ] + }, + React.createElement( + 'div', + { style: { marginBottom: 12, fontSize: 13, color: 'rgba(15,23,42,0.55)' } }, + '请选择导出的列' + ), + React.createElement( + 'div', + { style: { marginBottom: 10 } }, + React.createElement( + Space, + { size: 12 }, + React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0 }, onClick: selectAllExportFields }, '全选'), + React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0 }, onClick: clearAllExportFields }, '取消全选') + ) + ), + React.createElement(Checkbox.Group, { + value: exportSelectedKeys, + onChange: function (vals) { + setExportSelectedKeys(vals || []); + }, + style: { + display: 'grid', + gridTemplateColumns: 'repeat(3, 1fr)', + gap: '10px 16px', + width: '100%' + }, + options: exportColumnDefs.map(function (d) { + return { label: d.label, value: d.key }; + }) + }) + ), + React.createElement( + Modal, + { + title: '变更日志' + (changeLogModal.orderNo ? ' · ' + changeLogModal.orderNo : ''), + open: changeLogModal.open, + onCancel: closeChangeLogModal, + footer: React.createElement(Button, { onClick: closeChangeLogModal }, '关闭'), + width: 880, + destroyOnClose: true + }, + React.createElement(Table, { + size: 'small', + bordered: true, + rowKey: 'id', + pagination: changeLogRows.length > 8 ? { pageSize: 8, showSizeChanger: false } : false, + columns: changeLogColumns, + dataSource: changeLogRows, + locale: { emptyText: '暂无变更记录' }, + scroll: { x: 'max-content', y: 360 } + }) + ), + React.createElement( + Modal, + { + title: '车辆氢费明细 — 需求明细', + open: reqDetailOpen, + onCancel: function () { + setReqDetailOpen(false); + }, + footer: React.createElement( + Button, + { + type: 'primary', + onClick: function () { + setReqDetailOpen(false); + } + }, + '知道了' + ), + width: 840, + centered: true, + destroyOnClose: true, + styles: { + body: { + maxHeight: '72vh', + overflow: 'auto', + paddingTop: 8, + paddingBottom: 16 + } + } + }, + renderH2RequirementDocPanel() + ) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-ledger-data/pages/03-车辆维修明细.jsx b/src/prototypes/oneos-web-ledger-data/pages/03-车辆维修明细.jsx new file mode 100644 index 0000000..2d35967 --- /dev/null +++ b/src/prototypes/oneos-web-ledger-data/pages/03-车辆维修明细.jsx @@ -0,0 +1,1825 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 台账数据 - 车辆维修/保养明细(运维手动录入;提报人/类型多选筛选;确认提交后锁定,主管可编辑) +// 原型:保存/确认提交、表格内联编辑;联调后对接登录用户、角色与车辆清单接口 + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + var useRef = React.useRef; + + var antd = window.antd; + var App = antd.App; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Button = antd.Button; + var Table = antd.Table; + var Select = antd.Select; + var DatePicker = antd.DatePicker; + var Input = antd.Input; + var InputNumber = antd.InputNumber; + var Row = antd.Row; + var Col = antd.Col; + var Space = antd.Space; + var Tag = antd.Tag; + var Popconfirm = antd.Popconfirm; + var Popover = antd.Popover; + var Tooltip = antd.Tooltip; + var Dropdown = antd.Dropdown; + var Modal = antd.Modal; + var message = antd.message; + + var CURRENT_USER = { id: 'u_zhang', name: '张运维', role: 'staff' }; + + var SUBMIT_CONFIRM_TEXT = '提交后数据将无法修改,如需修改请联系运维主管进行修改'; + + var RECORD_TYPE_OPTIONS = [ + { value: '维修', label: '维修' }, + { value: '保养', label: '保养' } + ]; + + var CHANGE_LOG_FIELD_LABELS = { + repairDate: '进修日期', + plateNo: '车牌号', + recordType: '类型', + itemDesc: '项目/材料', + laborUnit: '人工费单位', + laborQty: '人工费数量', + laborUnitPrice: '人工费单价(含税)', + partsUnit: '配件费单位', + partsQty: '配件费数量', + partsUnitPrice: '配件费单价(含税)', + submitStatus: '提交状态', + saveStatus: '保存状态', + _copy: '复制新增', + _create: '新增记录' + }; + + /** + * 系统全部车辆车牌(原型枚举,与车辆管理模块一致;联调后由车辆清单接口加载) + */ + var SYSTEM_VEHICLE_PLATES = [ + '京C12345', + '京CN88771F', + '京E88888', + '京F33333', + '京G12345', + '京H88888', + '川AL55602F', + '沪A12345', + '沪AD12345F', + '沪B99999', + '沪C11111', + '沪D66666', + '浙AD12345F', + '浙AH55660F', + '浙BK33210F', + '苏EF99887F', + '粤A11B22', + '粤A12345', + '粤A66D66', + '粤A77F77', + '粤A88K88', + '粤A99A99', + '粤B12345', + '粤B22C33', + '粤B44E44', + '粤B55B55', + '粤B67890', + '粤BK33210F' + ]; + + function normPlateKey(plate) { + return String(plate == null ? '' : plate) + .replace(/[\s\-·]/g, '') + .toUpperCase(); + } + + function isPlateInVehicleTable(plate) { + if (!plate) return false; + var key = normPlateKey(plate); + for (var i = 0; i < SYSTEM_VEHICLE_PLATES.length; i++) { + if (normPlateKey(SYSTEM_VEHICLE_PLATES[i]) === key) return true; + } + return false; + } + + function isEmptyStr(v) { + return !String(v == null ? '' : v).trim(); + } + + function isEmptyNum(v) { + return v === null || v === undefined || v === ''; + } + + function getRowInvalidMap(row) { + var inv = {}; + if (!row.repairDate) inv.repairDate = true; + if (!row.plateNo) inv.plateNo = 'empty'; + else if (!isPlateInVehicleTable(row.plateNo)) inv.plateNo = 'not_exist'; + if (isEmptyStr(row.recordType)) inv.recordType = true; + if (isEmptyStr(row.itemDesc)) inv.itemDesc = true; + if (isEmptyStr(row.laborUnit)) inv.laborUnit = true; + if (isEmptyNum(row.laborQty)) inv.laborQty = true; + if (isEmptyNum(row.laborUnitPrice)) inv.laborUnitPrice = true; + if (isEmptyStr(row.partsUnit)) inv.partsUnit = true; + if (isEmptyNum(row.partsQty)) inv.partsQty = true; + if (isEmptyNum(row.partsUnitPrice)) inv.partsUnitPrice = true; + return inv; + } + + /** 列表展示顺序:0 已确认提交 → 1 已保存 → 2 新增未保存 */ + function rowDisplaySortTier(row) { + if (row && row.submitStatus === 'submitted') return 0; + if (row && row.saveStatus === 'saved') return 1; + return 2; + } + + function rowStatusLabel(row) { + if (row && row.submitStatus === 'submitted') return '已提交'; + if (row && row.saveStatus === 'saved') return '已保存'; + return '待保存'; + } + + function isSavedDraftRow(row) { + return row && row.submitStatus !== 'submitted' && row.saveStatus === 'saved'; + } + + /** 状态为「已保存」的行均在操作列展示「编辑」「删除」 */ + function canShowSavedActions(row) { + return isSavedDraftRow(row); + } + + /** 状态为「已提交」:仅运维主管在操作列展示「编辑」「删除」 */ + function canShowSubmittedActions(row, isSupervisorRole) { + return !!isSupervisorRole && row && row.submitStatus === 'submitted'; + } + + function canDeleteRow(row, isSupervisorRole) { + if (row && row.submitStatus === 'submitted') { + return !!isSupervisorRole; + } + if (isSupervisorRole) return true; + return row && row.createdBy === CURRENT_USER.id; + } + + /** 已提交不可复制;其余本人或主管可复制 */ + function canCopyRow(row, isSupervisorRole) { + if (!row || row.submitStatus === 'submitted') return false; + if (isSupervisorRole) return true; + return row.createdBy === CURRENT_USER.id; + } + + function cloneRowTimeValue(t) { + if (!t) return t; + try { + if (window.dayjs && window.dayjs.isDayjs && window.dayjs.isDayjs(t)) return t.clone(); + if (window.dayjs) { + var d = window.dayjs(t); + return d.isValid() ? d : t; + } + } catch (eClone) {} + return t; + } + + function buildCopiedRow(sourceRow) { + var now = nowDayjs(); + return recalcRow({ + key: nextRowKey(), + createdBy: CURRENT_USER.id, + reporterName: CURRENT_USER.name, + repairDate: cloneRowTimeValue(sourceRow.repairDate), + plateNo: sourceRow.plateNo, + reportTime: now, + recordType: sourceRow.recordType, + itemDesc: sourceRow.itemDesc || '', + laborUnit: sourceRow.laborUnit || '', + laborQty: sourceRow.laborQty, + laborUnitPrice: sourceRow.laborUnitPrice, + laborAmount: 0, + partsUnit: sourceRow.partsUnit || '', + partsQty: sourceRow.partsQty, + partsUnitPrice: sourceRow.partsUnitPrice, + partsAmount: 0, + totalCost: 0, + submitStatus: 'draft', + saveStatus: 'unsaved', + submittedAt: null + }); + } + + function valuesEqualForLog(field, a, b) { + if (a === b) return true; + if ((a == null || a === '') && (b == null || b === '')) return true; + if (field === 'repairDate' && window.dayjs) { + try { + return window.dayjs(a).valueOf() === window.dayjs(b).valueOf(); + } catch (eLog) { + return false; + } + } + if ( + field === 'laborQty' || + field === 'laborUnitPrice' || + field === 'partsQty' || + field === 'partsUnitPrice' + ) { + if (isEmptyNum(a) && isEmptyNum(b)) return true; + if (field === 'laborQty' || field === 'partsQty') return toIntQty(a) === toIntQty(b); + return roundMoney(a) === roundMoney(b); + } + return String(a) === String(b); + } + + function formatLogValue(field, value, rowCtx) { + if (value == null || value === '') return '—'; + if (field === 'repairDate') return formatDate(value) || formatDateTime(value) || String(value); + if (field === 'submitStatus') { + if (value === 'submitted') return '已提交'; + return '未提交'; + } + if (field === 'saveStatus') { + if (value === 'saved') return '已保存'; + return '待保存'; + } + if ( + field === 'laborQty' || + field === 'partsQty' + ) { + return isEmptyNum(value) ? '—' : String(toIntQty(value)); + } + if (field === 'laborUnitPrice' || field === 'partsUnitPrice') { + return isEmptyNum(value) ? '—' : fmtMoney(value); + } + return String(value); + } + + function rowTimeValue(row, field) { + var v = row && row[field]; + if (!v || !window.dayjs) return 0; + try { + return window.dayjs(v).valueOf(); + } catch (e1) { + return 0; + } + } + + function sortRowsWithinTier(rows) { + return rows.slice().sort(function (a, b) { + var ta = rowTimeValue(a, 'submittedAt') || rowTimeValue(a, 'reportTime'); + var tb = rowTimeValue(b, 'submittedAt') || rowTimeValue(b, 'reportTime'); + if (tb !== ta) return tb - ta; + return String(a.key || '').localeCompare(String(b.key || '')); + }); + } + + /** 分区合并,避免同层数据按日期与另一层穿插 */ + function sortRowsByDisplayTier(rows) { + var submitted = []; + var saved = []; + var unsaved = []; + (rows || []).forEach(function (r) { + var tier = rowDisplaySortTier(r); + if (tier === 0) submitted.push(r); + else if (tier === 1) saved.push(r); + else unsaved.push(r); + }); + return sortRowsWithinTier(submitted).concat(sortRowsWithinTier(saved)).concat(sortRowsWithinTier(unsaved)); + } + + function firstInvalidMessage(row, inv, seq) { + var prefix = '第' + seq + '行:'; + if (inv.repairDate) return prefix + '请填写进修日期'; + if (inv.plateNo === 'empty') return prefix + '请填写车牌号'; + if (inv.plateNo === 'not_exist') return prefix + '车辆不存在'; + if (inv.recordType) return prefix + '请选择类型'; + if (inv.itemDesc) return prefix + '请填写项目/材料'; + if (inv.laborUnit) return prefix + '请填写人工费单位'; + if (inv.laborQty) return prefix + '请填写人工费数量'; + if (inv.laborUnitPrice) return prefix + '请填写人工费单价(含税)'; + if (inv.partsUnit) return prefix + '请填写配件费单位'; + if (inv.partsQty) return prefix + '请填写配件费数量'; + if (inv.partsUnitPrice) return prefix + '请填写配件费单价(含税)'; + return prefix + '请完善必填项'; + } + + var OTHER_REPORTERS = [ + { id: 'u_zhou', name: '周敏' }, + { id: 'u_li_h', name: '李航' }, + { id: 'u_wang', name: '王磊' } + ]; + + var rowIdSeed = 0; + function nextRowKey() { + rowIdSeed += 1; + return 'mr-' + Date.now() + '-' + rowIdSeed; + } + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + /** 车牌号下拉:支持输入模糊匹配(忽略空格、大小写) */ + function filterPlateOption(input, option) { + var q = normPlateKey(input); + if (!q) return true; + var label = normPlateKey(option && (option.label != null ? option.label : option.value)); + return label.indexOf(q) >= 0; + } + + function numOrZero(v) { + if (v === null || v === undefined || v === '') return 0; + var n = Number(v); + return isNaN(n) ? 0 : n; + } + + function roundMoney(n) { + return Math.round(numOrZero(n) * 100) / 100; + } + + function toIntQty(v) { + if (v === null || v === undefined || v === '') return 0; + var n = Math.floor(Number(v)); + return isNaN(n) || n < 0 ? 0 : n; + } + + function fmtMoney(n) { + if (n === null || n === undefined || n === '') return '0.00'; + return roundMoney(n).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + } + + function nowDayjs() { + try { + if (window.dayjs) return window.dayjs(); + } catch (e1) {} + return null; + } + + function formatDate(d) { + if (!d || !window.dayjs) return ''; + try { + return window.dayjs(d).format('YYYY-MM-DD'); + } catch (e2) { + return ''; + } + } + + function formatDateTime(d) { + if (!d || !window.dayjs) return ''; + try { + return window.dayjs(d).format('YYYY-MM-DD HH:mm'); + } catch (e3) { + return ''; + } + } + + function recalcRow(row) { + var laborQty = toIntQty(row.laborQty); + var partsQty = toIntQty(row.partsQty); + var laborAmt = roundMoney(laborQty * numOrZero(row.laborUnitPrice)); + var partsAmt = roundMoney(partsQty * numOrZero(row.partsUnitPrice)); + var total = roundMoney(laborAmt + partsAmt); + return Object.assign({}, row, { + laborQty: laborQty, + partsQty: partsQty, + laborAmount: laborAmt, + partsAmount: partsAmt, + totalCost: total + }); + } + + function buildEmptyRow(user) { + var now = nowDayjs(); + return recalcRow({ + key: nextRowKey(), + createdBy: user.id, + reporterName: user.name, + repairDate: now, + plateNo: undefined, + reportTime: now, + recordType: undefined, + itemDesc: '', + laborUnit: '', + laborQty: null, + laborUnitPrice: null, + laborAmount: 0, + partsUnit: '', + partsQty: null, + partsUnitPrice: null, + partsAmount: 0, + totalCost: 0, + submitStatus: 'draft', + saveStatus: 'unsaved' + }); + } + + function buildMockAllRows() { + var plates = SYSTEM_VEHICLE_PLATES; + var items = ['更换空压机滤芯', '制动片检修', '冷却液补充', '高压线束检查', '轮胎换位']; + var rows = []; + var reporters = [{ id: CURRENT_USER.id, name: CURRENT_USER.name }].concat(OTHER_REPORTERS); + var i; + for (i = 0; i < 8; i++) { + var rep = reporters[i % reporters.length]; + var submitStatus = 'draft'; + var saveStatus = 'unsaved'; + if (rep.id === CURRENT_USER.id) { + if (i < 3) { + submitStatus = 'submitted'; + saveStatus = 'saved'; + } else if (i === 3) { + saveStatus = 'saved'; + } + } else { + saveStatus = 'saved'; + } + var laborQty = 1 + (i % 4); + var laborPrice = 120 + i * 15; + var partsQty = i % 2 === 0 ? 2 : 0; + var partsPrice = 80 + i * 20; + var d = nowDayjs(); + var repairDay = d && d.subtract ? d.subtract(i, 'day') : d; + rows.push( + recalcRow({ + key: 'mock-' + (i + 1), + createdBy: rep.id, + reporterName: rep.name, + repairDate: repairDay, + plateNo: plates[i % plates.length], + reportTime: repairDay, + recordType: i % 3 === 0 ? '保养' : '维修', + itemDesc: items[i % items.length], + laborUnit: '工时', + laborQty: laborQty, + laborUnitPrice: laborPrice, + laborAmount: 0, + partsUnit: '件', + partsQty: partsQty, + partsUnitPrice: partsPrice, + partsAmount: 0, + totalCost: 0, + submitStatus: submitStatus, + saveStatus: saveStatus, + submittedAt: submitStatus === 'submitted' ? repairDay : null + }) + ); + } + return rows; + } + + var layoutStyle = { + padding: '16px 24px 24px', + minHeight: '100vh', + background: 'linear-gradient(165deg, #eef4ff 0%, #f5f7fa 42%, #f0f2f5 100%)' + }; + var filterLabelStyle = { marginBottom: 6, fontSize: 13, color: 'rgba(0,0,0,0.55)', fontWeight: 500 }; + var filterItemStyle = { marginBottom: 12 }; + var filterControlStyle = { width: '100%' }; + var filterActionsColStyle = { flex: '0 0 auto', marginLeft: 'auto' }; + + var filterCardStyle = { + marginBottom: 20, + borderRadius: 16, + boxShadow: '0 4px 20px -4px rgba(16,24,40,0.03), 0 0 0 1px rgba(16,24,40,0.06)', + border: 'none', + background: '#ffffff' + }; + + var tableCardStyle = { + borderRadius: 16, + boxShadow: '0 10px 32px -4px rgba(16,24,40,0.06), 0 0 0 1px rgba(16,24,40,0.04)', + border: 'none', + background: '#ffffff', + overflow: 'hidden' + }; + + var ledgerTableStyle = + '.maint-ledger-table-wrap{border-radius:12px;overflow:hidden;box-shadow:0 4px 24px -6px rgba(15,23,42,0.05),0 0 0 1px rgba(22,119,255,0.1)}' + + '.maint-ledger-table .ant-table-thead>tr>th,.maint-ledger-table .ant-table-thead .ant-table-cell{white-space:nowrap;color:#0f172a!important;font-weight:600!important;font-size:13px!important;' + + 'background:#e8f4fc!important;border-bottom:1px solid #bae6fd!important;border-inline-end:1px solid #dbeafe!important;padding:0 8px!important;height:38px!important;text-align:center!important}' + + '.maint-ledger-table .ant-table-thead>tr:not(:last-child)>th{border-bottom:1px solid #bae6fd!important}' + + '.maint-repair-date-picker,.maint-repair-date-picker .ant-picker-input{width:100%;min-width:132px}' + + '.maint-repair-date-picker .ant-picker-input>input{min-width:118px}' + + '.maint-ledger-table .ant-table-tbody>tr:not(.ant-table-measure-row)>td{padding:4px 6px!important;vertical-align:middle!important}' + + '.maint-ledger-table .ant-table-tbody>tr.maint-row-data:hover>td{background:#f0f9ff!important}' + + '.maint-ledger-table .ant-table-summary>tr>td{font-weight:700;background:#f8fafc!important;color:#0f172a!important;border-top:2px solid #cbd5e1!important;padding:0 8px!important;height:38px!important}' + + '.maint-cell-readonly{color:#64748b;background:#f8fafc}' + + '.maint-inline-input{width:100%}' + + '.maint-row-tier-0>td{background:#f8fafc!important}' + + '.maint-row-tier-1>td{background:#fff!important}' + + '.maint-row-tier-2>td{background:#fffbeb!important}' + + '.maint-row-tier-boundary>td{border-top:2px solid #7dd3fc!important}' + + '.maint-action-icon-btn{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:4px;cursor:pointer;color:rgba(15,23,42,0.55);transition:background .15s,color .15s}' + + '.maint-action-icon-btn:hover{background:#f0f9ff;color:#1677ff}' + + '.maint-action-icon-btn.maint-action-icon-danger:hover{background:#fff1f0;color:#ff4d4f}' + + '.maint-action-icon-btn.is-disabled{color:rgba(15,23,42,0.25);cursor:not-allowed;pointer-events:none}' + + '.maint-row-more-btn.maint-action-icon-btn:hover{background:#f5f5f5;color:rgba(15,23,42,0.75)}'; + + var rowsState = useState(buildMockAllRows); + var allRows = rowsState[0]; + var setAllRows = rowsState[1]; + + var vehiclePlateOptions = useMemo(function () { + return SYSTEM_VEHICLE_PLATES.map(function (p) { + return { value: p, label: p }; + }); + }, []); + + var plateDraftState = useState(undefined); + var plateDraft = plateDraftState[0]; + var setPlateDraft = plateDraftState[1]; + + /** 提报人多选:空数组表示全部 */ + var reporterDraftState = useState([]); + var reporterDraft = reporterDraftState[0]; + var setReporterDraft = reporterDraftState[1]; + + var dateRangeDraftState = useState(null); + var dateRangeDraft = dateRangeDraftState[0]; + var setDateRangeDraft = dateRangeDraftState[1]; + + /** 类型多选:空数组表示全部(保养、维修) */ + var typeDraftState = useState([]); + var typeDraft = typeDraftState[0]; + var setTypeDraft = typeDraftState[1]; + + var plateAppliedState = useState(undefined); + var plateApplied = plateAppliedState[0]; + var setPlateApplied = plateAppliedState[1]; + + var reporterAppliedState = useState([]); + var reporterApplied = reporterAppliedState[0]; + var setReporterApplied = reporterAppliedState[1]; + + var dateRangeAppliedState = useState(null); + var dateRangeApplied = dateRangeAppliedState[0]; + var setDateRangeApplied = dateRangeAppliedState[1]; + + var typeAppliedState = useState([]); + var typeApplied = typeAppliedState[0]; + var setTypeApplied = typeAppliedState[1]; + + /** 联调后由账号权限判断是否运维主管 */ + var isSupervisor = false; + var editingKeysState = useState([]); + var editingKeys = editingKeysState[0]; + var setEditingKeys = editingKeysState[1]; + + var rowInvalidState = useState({}); + var rowInvalid = rowInvalidState[0]; + var setRowInvalid = rowInvalidState[1]; + + var changeLogsByKeyState = useState({}); + var changeLogsByKey = changeLogsByKeyState[0]; + var setChangeLogsByKey = changeLogsByKeyState[1]; + + var changeLogModalState = useState({ open: false, rowKey: null, rowLabel: '' }); + var changeLogModal = changeLogModalState[0]; + var setChangeLogModal = changeLogModalState[1]; + + var changeLogsSeededRef = useRef(false); + + var appendChangeLog = useCallback(function (rowKey, field, before, after, rowCtx, meta) { + if (!rowKey) return; + meta = meta || {}; + var fieldLabel = meta.fieldLabel || CHANGE_LOG_FIELD_LABELS[field] || field; + var beforeText = meta.beforeText != null ? meta.beforeText : formatLogValue(field, before, rowCtx); + var afterText = meta.afterText != null ? meta.afterText : formatLogValue(field, after, rowCtx); + if (!meta.force && beforeText === afterText) return; + setChangeLogsByKey(function (prev) { + var list = (prev[rowKey] || []).slice(); + list.unshift({ + id: 'clog-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8), + at: nowDayjs(), + userId: CURRENT_USER.id, + userName: CURRENT_USER.name, + field: field, + fieldLabel: fieldLabel, + before: beforeText, + after: afterText + }); + var next = Object.assign({}, prev); + next[rowKey] = list; + return next; + }); + }, []); + + var openChangeLogModal = useCallback(function (record) { + if (!record) return; + var label = (record.plateNo || '') + (record.repairDate ? ' · ' + formatDate(record.repairDate) : ''); + setChangeLogModal({ + open: true, + rowKey: record.key, + rowLabel: label.trim() + }); + }, []); + + var closeChangeLogModal = useCallback(function () { + setChangeLogModal({ open: false, rowKey: null, rowLabel: '' }); + }, []); + + var copyPopoverKeyState = useState(null); + var copyPopoverKey = copyPopoverKeyState[0]; + var setCopyPopoverKey = copyPopoverKeyState[1]; + var copyRowCountState = useState(1); + var copyRowCount = copyRowCountState[0]; + var setCopyRowCount = copyRowCountState[1]; + + var applyCopyRows = useCallback(function (sourceKey, count) { + var n = Math.floor(Number(count)); + if (!isFinite(n) || n < 1) { + message.warning('请输入大于0的复制行数'); + return; + } + if (n > 100) { + message.warning('单次最多复制100行'); + return; + } + var didCopy = false; + setAllRows(function (prev) { + var source = null; + var i; + for (i = 0; i < prev.length; i++) { + if (prev[i].key === sourceKey) { + source = prev[i]; + break; + } + } + if (!source || !canCopyRow(source, isSupervisor)) return prev; + var copies = []; + for (i = 0; i < n; i++) { + copies.push(buildCopiedRow(source)); + } + var ci; + var sourceLabel = (source.plateNo || '当前行') + (source.itemDesc ? ' · ' + source.itemDesc : ''); + for (ci = 0; ci < copies.length; ci++) { + appendChangeLog(copies[ci].key, '_copy', null, null, source, { + fieldLabel: '复制新增', + beforeText: '—', + afterText: '复制自「' + sourceLabel + '」', + force: true + }); + } + didCopy = true; + return prev.concat(copies); + }); + setCopyPopoverKey(null); + if (didCopy) message.success('已复制 ' + n + ' 行'); + else message.warning('无法复制该条记录'); + }, [appendChangeLog, isSupervisor]); + + var latestRefs = useRef({}); + latestRefs.current = { + setAllRows: setAllRows, + setEditingKeys: setEditingKeys, + setRowInvalid: setRowInvalid, + editingKeys: editingKeys, + rowInvalid: rowInvalid, + isSupervisor: isSupervisor, + vehicleOptions: vehiclePlateOptions, + appendChangeLog: appendChangeLog + }; + + React.useEffect(function () { + if (changeLogsSeededRef.current) return; + changeLogsSeededRef.current = true; + var demoAt = nowDayjs(); + var demoAt2 = demoAt && demoAt.subtract ? demoAt.subtract(2, 'hour') : demoAt; + setChangeLogsByKey({ + 'mock-1': [ + { + id: 'clog-demo-2', + at: demoAt, + userId: CURRENT_USER.id, + userName: CURRENT_USER.name, + field: 'itemDesc', + fieldLabel: '项目/材料', + before: '更换空压机滤芯', + after: '制动片检修' + }, + { + id: 'clog-demo-1', + at: demoAt2, + userId: CURRENT_USER.id, + userName: CURRENT_USER.name, + field: 'recordType', + fieldLabel: '类型', + before: '维修', + after: '保养' + } + ] + }); + }, []); + + var isRowSubmitted = useCallback(function (row) { + return row && row.submitStatus === 'submitted'; + }, []); + + var isRowSaved = useCallback(function (row) { + return row && row.saveStatus === 'saved'; + }, []); + + /** 是否可编辑单元格:未保存草稿直接编辑;已保存需点「编辑」;已提交仅运维主管点「编辑」后可改 */ + var canEditRow = useCallback( + function (row) { + if (!row) return false; + if (isRowSubmitted(row)) { + if (!isSupervisor) return false; + return editingKeys.indexOf(row.key) >= 0; + } + if (isSavedDraftRow(row)) { + if (isSupervisor) return editingKeys.indexOf(row.key) >= 0; + if (row.createdBy !== CURRENT_USER.id) return false; + return editingKeys.indexOf(row.key) >= 0; + } + if (isSupervisor) return false; + if (row.createdBy !== CURRENT_USER.id) return false; + return true; + }, + [isSupervisor, editingKeys, isRowSubmitted] + ); + + var visibleRows = useMemo(function () { + var list = allRows.slice(); + if (plateApplied) { + list = list.filter(function (r) { return r.plateNo === plateApplied; }); + } + if (reporterApplied && reporterApplied.length > 0) { + list = list.filter(function (r) { return reporterApplied.indexOf(r.reporterName) >= 0; }); + } + if (typeApplied && typeApplied.length > 0) { + list = list.filter(function (r) { return typeApplied.indexOf(r.recordType) >= 0; }); + } + if (dateRangeApplied && dateRangeApplied[0] && dateRangeApplied[1] && window.dayjs) { + var start = window.dayjs(dateRangeApplied[0]).startOf('day'); + var end = window.dayjs(dateRangeApplied[1]).endOf('day'); + list = list.filter(function (r) { + if (!r.repairDate) return false; + var d = window.dayjs(r.repairDate); + return d.isAfter(start.subtract(1, 'ms')) && d.isBefore(end.add(1, 'ms')); + }); + } + list = sortRowsByDisplayTier(list); + return list.map(function (r, idx) { + return Object.assign({}, r, { seq: idx + 1, displayTier: rowDisplaySortTier(r) }); + }); + }, [allRows, plateApplied, reporterApplied, typeApplied, dateRangeApplied]); + + /** 合计行仅汇总三个人含税金额字段 */ + var totals = useMemo(function () { + return visibleRows.reduce( + function (acc, r) { + acc.laborAmount += numOrZero(r.laborAmount); + acc.partsAmount += numOrZero(r.partsAmount); + acc.totalCost += numOrZero(r.totalCost); + return acc; + }, + { laborAmount: 0, partsAmount: 0, totalCost: 0 } + ); + }, [visibleRows]); + + var reporterOptions = useMemo(function () { + var names = {}; + names[CURRENT_USER.name] = true; + OTHER_REPORTERS.forEach(function (rep) { + if (rep.name) names[rep.name] = true; + }); + allRows.forEach(function (r) { + if (r.reporterName) names[r.reporterName] = true; + }); + return Object.keys(names).map(function (n) { return { value: n, label: n }; }); + }, [allRows]); + + var updateRow = useCallback(function (key, patch) { + setAllRows(function (prev) { + return prev.map(function (r) { + if (r.key !== key) return r; + return recalcRow(Object.assign({}, r, patch)); + }); + }); + }, []); + + var addRow = useCallback(function () { + var row = buildEmptyRow(CURRENT_USER); + appendChangeLog(row.key, '_create', null, null, row, { + fieldLabel: '新增记录', + beforeText: '—', + afterText: '新增空白行', + force: true + }); + setAllRows(function (prev) { + return prev.concat([row]); + }); + message.success('已新增一行,请填写维修/保养信息'); + }, [appendChangeLog]); + + var handleQuery = useCallback(function () { + setPlateApplied(plateDraft); + setReporterApplied(reporterDraft); + setTypeApplied(typeDraft); + setDateRangeApplied(dateRangeDraft); + message.success('查询成功'); + }, [plateDraft, reporterDraft, typeDraft, dateRangeDraft]); + + var handleReset = useCallback(function () { + setPlateDraft(undefined); + setReporterDraft([]); + setTypeDraft([]); + setDateRangeDraft(null); + setPlateApplied(undefined); + setReporterApplied([]); + setTypeApplied([]); + setDateRangeApplied(null); + }, []); + + function validateMaintainableRows(rows) { + var allInvalid = {}; + var firstMsg = ''; + (rows || []).forEach(function (r, idx) { + var inv = getRowInvalidMap(r); + if (Object.keys(inv).length > 0) { + allInvalid[r.key] = inv; + if (!firstMsg) firstMsg = firstInvalidMessage(r, inv, r.seq || idx + 1); + } + }); + if (firstMsg) { + setRowInvalid(allInvalid); + message.warning(firstMsg); + return false; + } + setRowInvalid({}); + return true; + } + + var pendingSubmitRows = useMemo(function () { + return allRows.filter(function (r) { + return r.createdBy === CURRENT_USER.id && r.submitStatus !== 'submitted' && r.saveStatus === 'saved'; + }); + }, [allRows]); + + var handleSave = useCallback(function () { + var toSave = allRows.filter(function (r) { + return r.createdBy === CURRENT_USER.id && r.submitStatus !== 'submitted'; + }); + if (!toSave.length) { + message.info('暂无待保存的草稿记录'); + return; + } + if (!validateMaintainableRows(toSave)) return; + setAllRows(function (prev) { + return prev.map(function (r) { + if (r.createdBy === CURRENT_USER.id && r.submitStatus !== 'submitted') { + return Object.assign({}, r, { saveStatus: 'saved' }); + } + return r; + }); + }); + setEditingKeys([]); + message.success('保存成功(原型)'); + }, [allRows]); + + var handleConfirmSubmit = useCallback(function () { + if (isSupervisor) { + message.info('运维主管无需提交,请使用编辑修改已提交记录'); + return; + } + if (!pendingSubmitRows.length) { + var hasUnsaved = allRows.some(function (r) { + return r.createdBy === CURRENT_USER.id && r.submitStatus !== 'submitted' && r.saveStatus !== 'saved'; + }); + message.info(hasUnsaved ? '请先保存后再确认提交' : '暂无待提交的已保存记录'); + return; + } + if (!validateMaintainableRows(pendingSubmitRows)) return; + Modal.confirm({ + title: '确认提交', + content: SUBMIT_CONFIRM_TEXT, + okText: '确认', + cancelText: '取消', + onOk: function () { + setAllRows(function (prev) { + return prev.map(function (r) { + if (r.createdBy === CURRENT_USER.id && r.submitStatus !== 'submitted' && r.saveStatus === 'saved') { + if (!valuesEqualForLog('submitStatus', r.submitStatus, 'submitted')) { + appendChangeLog(r.key, 'submitStatus', r.submitStatus, 'submitted', r, { force: true }); + } + return Object.assign({}, r, { + submitStatus: 'submitted', + saveStatus: 'saved', + submittedAt: nowDayjs() + }); + } + return r; + }); + }); + setEditingKeys([]); + message.success('提交成功,数据已锁定'); + } + }); + }, [isSupervisor, pendingSubmitRows, allRows, appendChangeLog]); + + var cellInputStyle = { width: '100%' }; + var cellNumStyle = { width: '100%' }; + + var columns = useMemo(function () { + function patch(key, field, val) { + var ref = latestRefs.current; + var patchObj = {}; + patchObj[field] = val; + ref.setAllRows(function (prev) { + return prev.map(function (r) { + if (r.key !== key) return r; + var next = recalcRow(Object.assign({}, r, patchObj)); + if (ref.appendChangeLog && !valuesEqualForLog(field, r[field], val)) { + ref.appendChangeLog(key, field, r[field], val, next); + } + return next; + }); + }); + ref.setRowInvalid(function (prev) { + if (!prev || !prev[key] || !prev[key][field]) return prev; + var next = Object.assign({}, prev); + var rowInv = Object.assign({}, next[key]); + delete rowInv[field]; + if (Object.keys(rowInv).length === 0) { + delete next[key]; + } else { + next[key] = rowInv; + } + return next; + }); + } + + function readOnlyCell(v, align) { + return React.createElement( + 'div', + { + className: 'maint-cell-readonly', + style: { padding: '4px 8px', textAlign: align || 'center' } + }, + v === 0 || v === '0' ? v : v || '-' + ); + } + + function fieldInvalid(record, field) { + var ref = latestRefs.current; + var rowInv = (ref.rowInvalid && ref.rowInvalid[record.key]) || {}; + return rowInv[field]; + } + + function reqTitle(text) { + return React.createElement( + 'span', + null, + React.createElement('span', { style: { color: '#ff4d4f', marginRight: 2 } }, '*'), + text + ); + } + + return [ + { + title: '序号', + dataIndex: 'seq', + key: 'seq', + width: 56, + align: 'center', + fixed: 'left' + }, + { + title: '状态', + dataIndex: 'displayTier', + key: 'recordStatus', + width: 76, + align: 'center', + fixed: 'left', + render: function (_, record) { + var label = rowStatusLabel(record); + var color = label === '已提交' ? 'blue' : label === '已保存' ? 'orange' : 'default'; + return React.createElement(Tag, { color: color, style: { margin: 0 } }, label); + } + }, + { + title: reqTitle('进修日期'), + dataIndex: 'repairDate', + key: 'repairDate', + width: 152, + align: 'center', + render: function (v, record) { + if (!canEditRow(record)) return readOnlyCell(formatDate(v)); + return React.createElement(DatePicker, { + className: 'maint-repair-date-picker', + style: { width: '100%', minWidth: 132 }, + format: 'YYYY-MM-DD', + placeholder: '请选择进修日期', + allowClear: true, + value: v, + status: fieldInvalid(record, 'repairDate') ? 'error' : undefined, + onChange: function (d) { patch(record.key, 'repairDate', d); } + }); + } + }, + { + title: reqTitle('车牌号'), + dataIndex: 'plateNo', + key: 'plateNo', + width: 140, + align: 'center', + render: function (v, record) { + var ref = latestRefs.current; + if (!canEditRow(record)) return readOnlyCell(v); + var plateInv = fieldInvalid(record, 'plateNo'); + return React.createElement(Select, { + style: cellInputStyle, + showSearch: true, + allowClear: true, + placeholder: '请输入车牌号搜索', + value: v, + status: plateInv ? 'error' : undefined, + options: ref.vehicleOptions, + optionFilterProp: 'label', + filterOption: filterPlateOption, + notFoundContent: '未找到匹配车辆', + onChange: function (val) { patch(record.key, 'plateNo', val); } + }); + } + }, + { + title: '提报人', + dataIndex: 'reporterName', + key: 'reporterName', + width: 88, + align: 'center', + render: function (v) { return readOnlyCell(v); } + }, + { + title: '提报时间', + dataIndex: 'reportTime', + key: 'reportTime', + width: 168, + align: 'center', + render: function (v) { + return React.createElement( + 'div', + { className: 'maint-cell-readonly', style: { padding: '4px 8px', textAlign: 'center', whiteSpace: 'nowrap', minWidth: 152 } }, + formatDateTime(v) || '-' + ); + } + }, + { + title: reqTitle('类型'), + dataIndex: 'recordType', + key: 'recordType', + width: 100, + align: 'center', + render: function (v, record) { + if (!canEditRow(record)) return readOnlyCell(v); + return React.createElement(Select, { + style: cellInputStyle, + allowClear: true, + placeholder: '请选择类型', + value: v, + status: fieldInvalid(record, 'recordType') ? 'error' : undefined, + options: RECORD_TYPE_OPTIONS, + onChange: function (val) { patch(record.key, 'recordType', val); } + }); + } + }, + { + title: reqTitle('项目/材料'), + dataIndex: 'itemDesc', + key: 'itemDesc', + width: 160, + align: 'center', + render: function (v, record) { + if (!canEditRow(record)) return readOnlyCell(v); + return React.createElement(Input, { + className: 'maint-inline-input', + value: v, + placeholder: '请输入项目/材料', + status: fieldInvalid(record, 'itemDesc') ? 'error' : undefined, + onChange: function (e) { patch(record.key, 'itemDesc', e.target.value); } + }); + } + }, + { + title: '人工费', + key: 'laborGroup', + children: [ + { + title: reqTitle('单位'), + dataIndex: 'laborUnit', + key: 'laborUnit', + width: 72, + align: 'center', + render: function (v, record) { + if (!canEditRow(record)) return readOnlyCell(v); + return React.createElement(Input, { + className: 'maint-inline-input', + value: v, + placeholder: '单位', + status: fieldInvalid(record, 'laborUnit') ? 'error' : undefined, + onChange: function (e) { patch(record.key, 'laborUnit', e.target.value); } + }); + } + }, + { + title: reqTitle('数量'), + dataIndex: 'laborQty', + key: 'laborQty', + width: 88, + align: 'right', + render: function (v, record) { + if (!canEditRow(record)) return readOnlyCell(toIntQty(v), 'right'); + return React.createElement(InputNumber, { + style: cellNumStyle, + min: 0, + step: 1, + precision: 0, + placeholder: '数量', + value: isEmptyNum(v) ? null : toIntQty(v), + status: fieldInvalid(record, 'laborQty') ? 'error' : undefined, + onChange: function (val) { patch(record.key, 'laborQty', val == null ? null : toIntQty(val)); } + }); + } + }, + { + title: reqTitle('单价(含税)'), + dataIndex: 'laborUnitPrice', + key: 'laborUnitPrice', + width: 100, + align: 'right', + render: function (v, record) { + if (!canEditRow(record)) return readOnlyCell(fmtMoney(v), 'right'); + return React.createElement(InputNumber, { + style: cellNumStyle, + min: 0, + precision: 2, + placeholder: '单价', + value: isEmptyNum(v) ? null : v, + status: fieldInvalid(record, 'laborUnitPrice') ? 'error' : undefined, + onChange: function (val) { patch(record.key, 'laborUnitPrice', val == null ? null : val); } + }); + } + }, + { + title: '金额(含税)', + dataIndex: 'laborAmount', + key: 'laborAmount', + width: 100, + align: 'right', + render: function (v) { return React.createElement('span', { style: { paddingRight: 8 } }, fmtMoney(v)); } + } + ] + }, + { + title: '配件费', + key: 'partsGroup', + children: [ + { + title: reqTitle('单位'), + dataIndex: 'partsUnit', + key: 'partsUnit', + width: 72, + align: 'center', + render: function (v, record) { + if (!canEditRow(record)) return readOnlyCell(v); + return React.createElement(Input, { + className: 'maint-inline-input', + value: v, + placeholder: '单位', + status: fieldInvalid(record, 'partsUnit') ? 'error' : undefined, + onChange: function (e) { patch(record.key, 'partsUnit', e.target.value); } + }); + } + }, + { + title: reqTitle('数量'), + dataIndex: 'partsQty', + key: 'partsQty', + width: 88, + align: 'right', + render: function (v, record) { + if (!canEditRow(record)) return readOnlyCell(toIntQty(v), 'right'); + return React.createElement(InputNumber, { + style: cellNumStyle, + min: 0, + step: 1, + precision: 0, + placeholder: '数量', + value: isEmptyNum(v) ? null : toIntQty(v), + status: fieldInvalid(record, 'partsQty') ? 'error' : undefined, + onChange: function (val) { patch(record.key, 'partsQty', val == null ? null : toIntQty(val)); } + }); + } + }, + { + title: reqTitle('单价(含税)'), + dataIndex: 'partsUnitPrice', + key: 'partsUnitPrice', + width: 100, + align: 'right', + render: function (v, record) { + if (!canEditRow(record)) return readOnlyCell(fmtMoney(v), 'right'); + return React.createElement(InputNumber, { + style: cellNumStyle, + min: 0, + precision: 2, + placeholder: '单价', + value: isEmptyNum(v) ? null : v, + status: fieldInvalid(record, 'partsUnitPrice') ? 'error' : undefined, + onChange: function (val) { patch(record.key, 'partsUnitPrice', val == null ? null : val); } + }); + } + }, + { + title: '金额(含税)', + dataIndex: 'partsAmount', + key: 'partsAmount', + width: 100, + align: 'right', + render: function (v) { return React.createElement('span', { style: { paddingRight: 8 } }, fmtMoney(v)); } + } + ] + }, + { + title: '费用总计(含税)', + dataIndex: 'totalCost', + key: 'totalCost', + width: 120, + align: 'right', + fixed: 'right', + render: function (v) { + return React.createElement('span', { style: { paddingRight: 8, fontWeight: 600, color: '#0f172a' } }, fmtMoney(v)); + } + }, + { + title: '操作', + key: 'action', + width: 128, + align: 'center', + fixed: 'right', + render: function (_, record) { + var ref = latestRefs.current; + var submitted = record.submitStatus === 'submitted'; + var isOwn = record.createdBy === CURRENT_USER.id; + var allowCopy = canCopyRow(record, ref.isSupervisor); + + function renderEditIcon() { + return React.createElement( + 'svg', + { viewBox: '64 64 896 896', width: 14, height: 14, fill: 'currentColor', 'aria-hidden': true }, + React.createElement('path', { + d: 'M257.7 752c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 0 0 0-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 0 0 9.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9z' + }) + ); + } + + function renderCopyIcon() { + return React.createElement( + 'svg', + { viewBox: '64 64 896 896', width: 14, height: 14, fill: 'currentColor', 'aria-hidden': true }, + React.createElement('path', { + d: 'M832 64H296c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h496v688c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V96c0-17.7-14.3-32-32-32zM704 192H192c-17.7 0-32 14.3-32 32v530c0 17.7 14.3 32 32 32h512c17.7 0 32-14.3 32-32V224c0-17.7-14.3-32-32-32z' + }) + ); + } + + function renderDeleteIcon() { + return React.createElement( + 'svg', + { viewBox: '64 64 896 896', width: 14, height: 14, fill: 'currentColor', 'aria-hidden': true }, + React.createElement('path', { + d: 'M360 184h-8c4.4 0 8-3.6 8-8v8h304v-8c0 4.4 3.6 8 8 8h-8v72h72v-80c0-35.3-28.7-64-64-64H352c-35.3 0-64 28.7-64 64v80h72v-72zm504 72H160c-17.7 0-32 14.3-32 32v32c0 4.4 3.6 8 8 8h60v520c0 17.7 14.3 32 32 32h632c17.7 0 32-14.3 32-32V336h60c4.4 0 8-3.6 8-8v-32c0-17.7-14.3-32-32-32zM512 464c-17.7 0-32 14.3-32 32v288c0 17.7 14.3 32 32 32h64c17.7 0 32-14.3 32-32V496c0-17.7-14.3-32-32-32h-64zm-192 0c-17.7 0-32 14.3-32 32v288c0 17.7 14.3 32 32 32h64c17.7 0 32-14.3 32-32V496c0-17.7-14.3-32-32-32h-64zm-192 0c-17.7 0-32 14.3-32 32v288c0 17.7 14.3 32 32 32h64c17.7 0 32-14.3 32-32V496c0-17.7-14.3-32-32-32h-64z' + }) + ); + } + + function renderMoreIcon() { + return React.createElement( + 'svg', + { viewBox: '0 0 16 16', width: 16, height: 16, fill: 'currentColor', 'aria-hidden': true }, + React.createElement('circle', { cx: '8', cy: '3', r: '1.5' }), + React.createElement('circle', { cx: '8', cy: '8', r: '1.5' }), + React.createElement('circle', { cx: '8', cy: '13', r: '1.5' }) + ); + } + + function renderIconAction(title, iconNode, onClick, extraClass, disabled) { + var cls = + 'maint-action-icon-btn' + (extraClass ? ' ' + extraClass : '') + (disabled ? ' is-disabled' : ''); + var node = React.createElement( + 'span', + { + className: cls, + role: 'button', + tabIndex: disabled ? -1 : 0, + 'aria-label': title, + onClick: + disabled || !onClick + ? undefined + : function (e) { + e.stopPropagation(); + onClick(e); + } + }, + iconNode + ); + return React.createElement(Tooltip, { title: title }, node); + } + + function renderMoreAction() { + return React.createElement( + Dropdown, + { + trigger: ['hover'], + placement: 'bottomRight', + menu: { + items: [ + { + key: 'changelog', + label: '变更日志', + onClick: function () { + openChangeLogModal(record); + } + } + ] + } + }, + renderIconAction('更多', renderMoreIcon(), null, 'maint-row-more-btn') + ); + } + + function renderCopyButton() { + if (!allowCopy) return null; + var popOpen = copyPopoverKey === record.key; + var popContent = React.createElement( + 'div', + { style: { width: 220 } }, + React.createElement( + 'div', + { style: { marginBottom: 8, fontSize: 13, color: 'rgba(15,23,42,0.65)' } }, + '复制当前行已填写的全部数据' + ), + React.createElement( + 'div', + { style: { marginBottom: 6, fontSize: 13, color: 'rgba(15,23,42,0.55)' } }, + '复制行数' + ), + React.createElement(InputNumber, { + style: { width: '100%', marginBottom: 10 }, + min: 1, + max: 100, + precision: 0, + value: popOpen ? copyRowCount : 1, + onChange: function (val) { + setCopyRowCount(val == null ? 1 : val); + } + }), + React.createElement( + Button, + { + type: 'primary', + block: true, + size: 'small', + onClick: function () { + applyCopyRows(record.key, copyRowCount); + } + }, + '确认复制' + ) + ); + return React.createElement( + Popover, + { + trigger: 'click', + placement: 'leftTop', + open: popOpen, + content: popContent, + onOpenChange: function (open) { + if (open) { + setCopyPopoverKey(record.key); + setCopyRowCount(1); + } else if (copyPopoverKey === record.key) { + setCopyPopoverKey(null); + } + } + }, + renderIconAction('复制', renderCopyIcon()) + ); + } + + function toggleEditKeys() { + ref.setEditingKeys(function (prev) { + if (prev.indexOf(record.key) >= 0) { + return prev.filter(function (k) { return k !== record.key; }); + } + return prev.concat([record.key]); + }); + } + + function renderEditDeletePair(onEditClick, opts) { + opts = opts || {}; + var showCopy = opts.showCopy !== false && allowCopy; + var allowEdit = opts.allowEdit !== false; + var allowDelete = + opts.allowDelete !== false && canDeleteRow(record, ref.isSupervisor); + var isEditing = ref.editingKeys.indexOf(record.key) >= 0; + var editTitle = opts.editDisabledTitle && !allowEdit + ? opts.editDisabledTitle + : isEditing + ? '完成编辑' + : '编辑'; + var deleteTitle = opts.deleteDisabledTitle && !allowDelete + ? opts.deleteDisabledTitle + : '删除'; + var deleteBtn = React.createElement( + Popconfirm, + { + title: '确认删除该条维修/保养记录?', + disabled: !allowDelete, + onConfirm: function () { + if (!allowDelete) { + if (record.submitStatus === 'submitted') { + message.info('已提交记录仅运维主管可删除'); + } + return; + } + ref.setAllRows(function (prev) { + return prev.filter(function (r) { return r.key !== record.key; }); + }); + ref.setEditingKeys(function (prev) { + return prev.filter(function (k) { return k !== record.key; }); + }); + message.success('已删除'); + } + }, + renderIconAction( + deleteTitle, + renderDeleteIcon(), + null, + 'maint-action-icon-danger', + !allowDelete + ) + ); + var nodes = [ + renderIconAction( + editTitle, + renderEditIcon(), + allowEdit ? onEditClick : null, + '', + !allowEdit + ) + ]; + if (showCopy) nodes.push(renderCopyButton()); + nodes.push(deleteBtn, renderMoreAction()); + return React.createElement( + 'div', + { style: { display: 'inline-flex', alignItems: 'center', gap: 2, flexWrap: 'nowrap' } }, + nodes + ); + } + + if (submitted) { + if (!canShowSubmittedActions(record, ref.isSupervisor)) { + return renderMoreAction(); + } + return renderEditDeletePair( + function () { + if (!ref.isSupervisor) { + message.info('已提交记录仅运维主管可编辑'); + return; + } + toggleEditKeys(); + }, + { showCopy: false } + ); + } + + if (isSavedDraftRow(record)) { + if (!canShowSavedActions(record)) return renderMoreAction(); + return renderEditDeletePair(function () { + if (!ref.isSupervisor && record.createdBy !== CURRENT_USER.id) { + message.info('仅可编辑本人提报的维修/保养记录'); + return; + } + toggleEditKeys(); + }); + } + + if (ref.isSupervisor || !isOwn) return renderMoreAction(); + + return React.createElement( + 'div', + { style: { display: 'inline-flex', alignItems: 'center', gap: 2, flexWrap: 'nowrap' } }, + renderCopyButton(), + React.createElement( + Popconfirm, + { + title: '确认删除该条维修/保养记录?', + onConfirm: function () { + ref.setAllRows(function (prev) { + return prev.filter(function (r) { return r.key !== record.key; }); + }); + message.success('已删除'); + } + }, + renderIconAction('删除', renderDeleteIcon(), null, 'maint-action-icon-danger') + ), + renderMoreAction() + ); + } + } + ]; + }, [ + canEditRow, + editingKeys, + isSupervisor, + rowInvalid, + copyPopoverKey, + copyRowCount, + applyCopyRows, + openChangeLogModal + ]); + + var changeLogRows = useMemo( + function () { + if (!changeLogModal.rowKey) return []; + return changeLogsByKey[changeLogModal.rowKey] || []; + }, + [changeLogModal.rowKey, changeLogsByKey] + ); + + var changeLogColumns = useMemo( + function () { + return [ + { + title: '修改时间', + dataIndex: 'at', + key: 'at', + width: 168, + render: function (v) { + return formatDateTime(v) || '—'; + } + }, + { + title: '修改人', + dataIndex: 'userName', + key: 'userName', + width: 88, + align: 'center' + }, + { + title: '修改字段', + dataIndex: 'fieldLabel', + key: 'fieldLabel', + width: 140, + align: 'center' + }, + { + title: '修改前', + dataIndex: 'before', + key: 'before', + ellipsis: true, + render: function (v) { + return React.createElement('span', { style: { color: '#cf1322' } }, v == null || v === '' ? '—' : v); + } + }, + { + title: '修改后', + dataIndex: 'after', + key: 'after', + ellipsis: true, + render: function (v) { + return React.createElement('span', { style: { color: '#389e0d' } }, v == null || v === '' ? '—' : v); + } + } + ]; + }, + [] + ); + + var tableSummary = useCallback(function () { + return React.createElement( + Table.Summary, + null, + React.createElement( + Table.Summary.Row, + null, + React.createElement(Table.Summary.Cell, { index: 0, align: 'center' }, '合计'), + React.createElement(Table.Summary.Cell, { index: 1, colSpan: 7 }), + React.createElement(Table.Summary.Cell, { index: 8, colSpan: 3 }), + React.createElement(Table.Summary.Cell, { index: 11, align: 'right' }, fmtMoney(totals.laborAmount)), + React.createElement(Table.Summary.Cell, { index: 12, colSpan: 3 }), + React.createElement(Table.Summary.Cell, { index: 15, align: 'right' }, fmtMoney(totals.partsAmount)), + React.createElement(Table.Summary.Cell, { index: 16, align: 'right' }, fmtMoney(totals.totalCost)), + React.createElement(Table.Summary.Cell, { index: 17 }) + ) + ); + }, [totals]); + + return React.createElement( + App, + null, + React.createElement('style', null, ledgerTableStyle), + React.createElement( + 'div', + { style: layoutStyle }, + React.createElement(Breadcrumb, { + style: { marginBottom: 12 }, + items: [{ title: '台账数据' }, { title: '车辆维修/保养明细' }] + }), + React.createElement( + Card, + { style: filterCardStyle, bodyStyle: { paddingBottom: 4 } }, + React.createElement( + Row, + { gutter: [16, 16], align: 'bottom' }, + React.createElement( + Col, + { xs: 24, sm: 12, md: 8, lg: 5 }, + React.createElement( + 'div', + { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '进修日期'), + React.createElement(DatePicker.RangePicker, { + style: filterControlStyle, + format: 'YYYY-MM-DD', + value: dateRangeDraft, + onChange: function (v) { setDateRangeDraft(v); } + }) + ) + ), + React.createElement( + Col, + { xs: 24, sm: 12, md: 8, lg: 4 }, + React.createElement( + 'div', + { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '车牌号'), + React.createElement(Select, { + allowClear: true, + showSearch: true, + placeholder: '全部,可输入车牌搜索', + style: filterControlStyle, + value: plateDraft, + onChange: function (v) { setPlateDraft(v); }, + options: vehiclePlateOptions, + optionFilterProp: 'label', + filterOption: filterPlateOption + }) + ) + ), + React.createElement( + Col, + { xs: 24, sm: 12, md: 8, lg: 4 }, + React.createElement( + 'div', + { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '类型'), + React.createElement(Select, { + mode: 'multiple', + allowClear: true, + placeholder: '全部', + style: filterControlStyle, + value: typeDraft, + onChange: function (v) { setTypeDraft(v || []); }, + options: RECORD_TYPE_OPTIONS, + maxTagCount: 2 + }) + ) + ), + React.createElement( + Col, + { xs: 24, sm: 12, md: 8, lg: 5 }, + React.createElement( + 'div', + { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '提报人'), + React.createElement(Select, { + mode: 'multiple', + allowClear: true, + showSearch: true, + placeholder: '全部', + style: filterControlStyle, + value: reporterDraft, + onChange: function (v) { setReporterDraft(v || []); }, + options: reporterOptions, + filterOption: filterOption, + maxTagCount: 2 + }) + ) + ), + React.createElement( + Col, + { xs: 24, sm: 12, md: 8, lg: 6, style: filterActionsColStyle }, + React.createElement( + 'div', + { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '\u00a0'), + React.createElement( + Space, + { wrap: true }, + React.createElement(Button, { onClick: handleReset }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleQuery }, '查询') + ) + ) + ) + ) + ), + React.createElement( + Card, + { style: tableCardStyle, bodyStyle: { padding: '20px 20px 24px' } }, + React.createElement( + 'div', + { style: { position: 'relative', marginBottom: 8, minHeight: 36 } }, + React.createElement( + 'div', + { + style: { + textAlign: 'center', + fontSize: 18, + fontWeight: 700, + color: 'rgba(15,23,42,0.92)', + letterSpacing: '0.02em', + padding: '0 200px' + } + }, + '车辆维修/保养明细' + ), + React.createElement( + 'div', + { style: { position: 'absolute', right: 0, top: '50%', transform: 'translateY(-50%)' } }, + React.createElement( + Space, + null, + React.createElement(Button, { onClick: handleSave }, '保存'), + !isSupervisor + ? React.createElement(Button, { type: 'primary', onClick: handleConfirmSubmit }, '确认提交') + : null + ) + ) + ), + React.createElement( + 'div', + { className: 'maint-ledger-table-wrap' }, + React.createElement(Table, { + className: 'maint-ledger-table', + size: 'small', + bordered: true, + rowKey: 'key', + columns: columns, + dataSource: visibleRows, + pagination: false, + rowClassName: function (record, index) { + var tier = record.displayTier != null ? record.displayTier : rowDisplaySortTier(record); + var prev = visibleRows[index - 1]; + var prevTier = prev ? (prev.displayTier != null ? prev.displayTier : rowDisplaySortTier(prev)) : tier; + var cls = 'maint-row-data maint-row-tier-' + tier; + if (index > 0 && prevTier !== tier) cls += ' maint-row-tier-boundary'; + return cls; + }, + scroll: { x: 'max-content', y: 'calc(100vh - 360px)' }, + sticky: true, + summary: tableSummary + }) + ), + !isSupervisor + ? React.createElement( + Button, + { + type: 'dashed', + block: true, + style: { marginTop: 12 }, + onClick: addRow + }, + '新增一行' + ) + : null + ), + React.createElement( + Modal, + { + title: '变更日志' + (changeLogModal.rowLabel ? ' · ' + changeLogModal.rowLabel : ''), + open: changeLogModal.open, + onCancel: closeChangeLogModal, + footer: React.createElement(Button, { onClick: closeChangeLogModal }, '关闭'), + width: 880, + destroyOnClose: true + }, + React.createElement(Table, { + size: 'small', + bordered: true, + rowKey: 'id', + pagination: changeLogRows.length > 8 ? { pageSize: 8, showSizeChanger: false } : false, + columns: changeLogColumns, + dataSource: changeLogRows, + locale: { emptyText: '暂无变更记录' }, + scroll: { x: 'max-content', y: 360 } + }) + ) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-ledger-data/pages/04-氢费采购端汇总报表.jsx b/src/prototypes/oneos-web-ledger-data/pages/04-氢费采购端汇总报表.jsx new file mode 100644 index 0000000..da550f0 --- /dev/null +++ b/src/prototypes/oneos-web-ledger-data/pages/04-氢费采购端汇总报表.jsx @@ -0,0 +1,2331 @@ +// 氢费(采购端)汇总报表 PRD(与 氢费采购端汇总报表-需求文档.md 同步) +var H2_PURCHASE_SUMMARY_REQUIREMENT_DOC = `# 氢费(采购端)汇总报表 — 产品需求说明(PRD) + +| 项目 | 内容 | +|------|------| +| 文档版本 | v1.0(业务版) | +| 产品模块 | 台账数据 → 氢费(采购端)汇总报表 | +| 文档类型 | 产品需求说明 | +| 适用读者 | 产品、采购、财务、运营、测试、项目 | + +--- + +## 一、为什么做这件事 + +### 1.1 业务痛点 + +- 采购侧需按加氢站维度掌握氢费规模、应付与实付、开票与预付余额,现有数据分散在车辆氢费明细、打款、开票等模块,缺少一站汇总视图。 +- 财务对账需快速识别「未付金额」「站点欠费」等风险,并下钻到账单、流水、发票凭据。 + +### 1.2 产品价值 + +| 价值点 | 说明 | +|--------|------| +| 站点汇总 | 一行一站,看清加氢量、应付、已付、未付、开票、未开票、当前余额 | +| 对账效率 | 顶部合计条随筛选即时汇总,支撑采购与财务日常核对 | +| 可追溯 | 关键金额可钻取明细,付款凭据、发票支持在线预览与下载 | +| 风险可见 | 未付/未开票金额为正、当前余额为负时突出展示 | + +### 1.3 本期目标 + +建设 Web 端「氢费(采购端)汇总报表」,按地区、加氢站、结算方式查询,展示站点汇总列表及合计;支持多类钻取与 CSV 导出。 + +### 1.4 本期不做 + +- 报表内直接发起打款、开票(跳转或对接后续迭代)。 +- 跨年度历史数据全量迁移方案(本期以 2026 年起统计口径描述为准,上线以数据准备结果为准)。 + +--- + +## 二、谁在用、用来干什么 + +### 2.1 用户角色 + +| 角色 | 典型诉求 | +|------|----------| +| **采购人员** | 按站点查看氢费与支付进度,核对当前余额、欠费站点 | +| **财务人员** | 核对应付/实付/未付/已开票,查看付款证明与发票附件 | +| **运营/主管** | 按地区、结算方式浏览整体规模与异常站点 | + +### 2.2 核心使用场景 + +1. **日常巡检**:按地区或结算方式筛选,查看顶部合计与列表,关注未付、已欠费站点。 +2. **站点下钻**:点击加氢总量、应付总金额等,打开明细弹窗核对构成。 +3. **付款与发票核查**:从已付总金额、已开票金额钻取,预览付款凭据或发票。 +4. **导出报送**:按当前筛选结果导出 CSV。 + +--- + +## 三、页面功能说明 + +### 3.1 页面组成 + +路径:**台账数据 → 氢费(采购端)汇总报表** + +自上而下: + +1. **查询条件区** — 地区、加氢站全称、结算方式 +2. **汇总列表区** — 标题、导出、筛选摘要、顶部合计条、站点表格 +3. **弹窗** — 需求说明、各类钻取明细、附件预览 + +面包屑行右上角提供 **「查看需求说明」**(本文档);汇总列表标题右侧提供 **「导出」**。 + +### 3.2 查询条件 + +| 查询项 | 业务说明 | +|--------|----------| +| 地区 | 加氢站所属省/市,支持搜索;空为全部 | +| 加氢站全称 | 从加氢站主数据选择;空为全部 | +| 结算方式 | 预付 / 月结;空为全部 | + +**交互:** 修改条件后点击 **「查询」** 生效;**「重置」** 清空条件。查询成功给予简短提示。列表上方展示当前筛选摘要(地区、加氢站、结算方式)。 + +### 3.3 顶部合计条 + +表格上方独立展示(随当前列表筛选结果汇总): + +| 合计项 | 计算口径 | +|--------|----------| +| 加氢总量(kg) | 列表各行加氢总量之和 | +| 应付总金额(元) | 列表各行应付总金额之和 | +| 已付总金额(元) | 列表各行已付总金额之和 | +| 未付总金额(元) | **应付总金额合计 − 已付总金额合计**;大于 0 时红色显示 | +| 未开票总金额(元) | **应付总金额合计 − 已开票总金额合计**;大于 0 时红色显示 | +| 充值总额(元) | 列表各行充值总额之和 | +| 当前总余额(元) | 列表各行当前余额之和;合计为负时红色显示 | + +### 3.4 汇总列表字段 + +**列顺序(左→右):** +序号 → 地区 → 加氢站全称 → 结算方式 → 加氢总量(kg) → 应付总金额(元) → 已付总金额(元) → 未付总金额(元) → 已开票金额(元) → 未开票金额(元) → 充值总额(元) → 当前余额(元) + +**表头列宽:** 支持鼠标拖动表头右侧调整列宽(最小宽度限制),便于长站名与金额列阅读。 + +| 字段 | 业务口径 | 交互 | +|------|----------|------| +| 结算方式 | 预付 / 月结 | 只读 | +| 加氢总量(kg) | 自 **2026 年起** 该站全部氢费加氢量之和 | 点击钻取 **加氢量明细**(仅加氢量,不含金额) | +| 应付总金额(元) | 该站「车辆氢费明细」**加氢总价(元)** 求和 | 点击钻取明细(加氢量、成本单价、成本总价) | +| 已付总金额(元) | 加氢站打款管理中 **已支付金额** 求和 | 点击钻取支付账单明细 | +| 未付总金额(元) | **应付总金额 − 已付总金额**(行内计算) | 只读;大于 0 红色 | +| 已开票金额(元) | 该站已开票金额合计 | 点击钻取开票记录 | +| 未开票金额(元) | **应付总金额 − 已开票金额**(行内计算) | 只读;大于 0 红色 | +| 充值总额(元) | 该站预充值(打款入账)金额合计 | 点击钻取充值明细 | +| 当前余额(元) | 2026 年期初余额,扣减 2025 年及以前未扣款记录后的实时余额 | 点击钻取余额变更明细;**为负** 时金额红色 + 左侧 **「已欠费」** 标签 | + +### 3.5 钻取明细说明 + +#### (1)加氢量明细(由加氢总量进入) + +| 列 | 说明 | +|----|------| +| 序号、加氢时间、订单编号、车牌号、加氢量(kg) | 来源于车辆氢费明细,按当前加氢站筛选 | +| 合计 | 加氢量合计 | + +#### (2)应付总金额明细 + +| 列 | 说明 | +|----|------| +| 序号、加氢时间、订单编号、车牌号、加氢量(kg)、成本单价(元/kg)、成本总价(元) | 按站筛选的车辆氢费明细 | +| 合计 | 加氢量、成本总价;弹窗标题含站名 | + +#### (3)已付总金额明细(由已付总金额进入) + +| 列 | 说明 | +|----|------| +| 加氢站全称、账单开始时间、账单结束时间、应付总金额(元)、已付总金额(元)、银行付款证明 | 按账单维度展示 | +| 银行付款证明 | **查看付款凭据** — 预览付款证明图片 | +| 合计 | 应付、实付合计 | + +#### (4)已开票明细(由已开票金额进入) + +| 列 | 说明 | +|----|------| +| 加氢站全称、开票时间、开票金额(元)、发票 | | +| 发票 | **查看发票** — 在线预览 PDF/图片;**下载发票** — 下载 PDF/图片文件 | +| 合计 | 开票金额合计 | + +#### (5)充值总额明细(由充值总额进入) + +| 列 | 说明 | +|----|------| +| 加氢站全称、支付时间、预充金额(元)、付款凭证 | 支付时间为 **YYYY-MM-DD** | +| 付款凭证 | **预览** — 在线查看;**下载** — 下载凭证文件 | +| 合计 | 预充金额(充值总额)合计 | + +#### (6)余额变更明细(由当前余额进入) + +| 列 | 说明 | +|----|------| +| 加氢站全称、收入金额(元)、支出金额(元)、余额(元)、订单编号 | 收入/支出为空显示「—」;余额为负红色 | +| 合计 | 收入合计、支出合计、末行余额(与列表该行当前余额一致) | + +### 3.6 附件预览 + +- 付款凭据、发票在弹窗中预览;PDF 使用文档预览,图片直接展示。 +- 发票支持下载,文件名与开票记录一致。 + +### 3.7 导出 + +- 点击 **「导出」** 下载当前筛选结果 CSV(含列表字段及合计行)。 +- 编码 UTF-8(带 BOM),便于 Excel 打开。 + +--- + +## 四、业务规则摘要 + +| 规则 | 说明 | +|------|------| +| 统计起点 | 加氢总量等业务口径默认 **2026 年起**(与车辆氢费明细、主数据生效规则一致) | +| 未付(行) | 应付总金额 − 已付总金额 | +| 未付(合计条) | 各站未付之和,等价于应付合计 − 已付合计 | +| 未开票(行) | 应付总金额 − 已开票金额 | +| 未开票(合计条) | 各站未开票之和,等价于应付合计 − 已开票合计 | +| 已欠费 | 仅 **当前余额 < 0** 时展示标签 | +| 钻取范围 | 均限定为 **当前汇总行对应加氢站** | + +--- + +## 五、验收要点(业务) + +1. 查询、重置、筛选摘要、合计条与列表数据一致。 +2. 各可点击金额/加氢量钻取弹窗字段与上文一致,合计正确。 +3. 未付、已欠费展示规则符合第四节。 +4. 付款凭据可预览;发票可预览与下载。 +5. 列宽可拖动;导出字段完整。 +6. 「查看需求说明」可打开本 PRD 全文。 + +--- + +**文档结束** +`; + +/** PRD Markdown 渲染(氢费采购端汇总报表) */ +function parsePurchasePrdInlineText(text) { + var parts = String(text || '').split(/(\*\*[^*]+\*\*)/g); + var nodes = []; + var i; + for (i = 0; i < parts.length; i++) { + var p = parts[i]; + if (!p) continue; + if (p.indexOf('**') === 0 && p.lastIndexOf('**') === p.length - 2) { + nodes.push(React.createElement('strong', { key: i }, p.slice(2, -2))); + } else { + nodes.push(p); + } + } + return nodes.length === 1 ? nodes[0] : nodes; +} +function isPurchasePrdTableRow(line) { + return /^\|.+\|$/.test(String(line || '').trim()); +} +function isPurchasePrdTableSep(line) { + return /^\|[\s\-:|]+\|$/.test(String(line || '').trim()); +} +function renderPurchasePrdTableRow(line, rowKey, isHeader) { + var cells = String(line).trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map(function (c) { return c.trim(); }); + return React.createElement('tr', { key: rowKey }, cells.map(function (cell, ci) { + var Tag = isHeader ? 'th' : 'td'; + return React.createElement(Tag, { + key: ci, + style: { border: '1px solid #e5e7eb', padding: '8px 10px', textAlign: 'left', verticalAlign: 'top', fontWeight: isHeader ? 600 : 400, background: isHeader ? '#f8fafc' : '#fff', fontSize: 13, lineHeight: 1.5 } + }, parsePurchasePrdInlineText(cell)); + })); +} +function renderPurchasePrdMarkdown(markdown) { + var lines = String(markdown || '').split(/\r?\n/); + var nodes = []; + var i = 0; + var inCode = false; + var codeBuf = []; + while (i < lines.length) { + var line = lines[i]; + var trimmed = String(line || '').trim(); + if (trimmed.indexOf('```') === 0) { + if (inCode) { + nodes.push(React.createElement('pre', { key: 'code-' + i, style: { margin: '12px 0', padding: '12px 14px', background: '#f6f8fa', border: '1px solid #e5e7eb', borderRadius: 8, fontSize: 12, lineHeight: 1.6, overflow: 'auto', color: '#334155', whiteSpace: 'pre-wrap' } }, codeBuf.join('\n'))); + codeBuf = []; + inCode = false; + } else inCode = true; + i += 1; + continue; + } + if (inCode) { codeBuf.push(line); i += 1; continue; } + if (trimmed === '---') { + nodes.push(React.createElement('hr', { key: 'hr-' + i, style: { border: 'none', borderTop: '1px solid #e8ecf0', margin: '20px 0' } })); + i += 1; + continue; + } + if (isPurchasePrdTableRow(trimmed)) { + var tableLines = []; + while (i < lines.length && isPurchasePrdTableRow(String(lines[i]).trim())) { + tableLines.push(String(lines[i]).trim()); + i += 1; + } + var bodyRows = []; + var ti; + for (ti = 0; ti < tableLines.length; ti++) { + if (isPurchasePrdTableSep(tableLines[ti])) continue; + bodyRows.push(renderPurchasePrdTableRow(tableLines[ti], 'tr-' + i + '-' + ti, ti === 0)); + } + if (bodyRows.length) { + nodes.push(React.createElement('div', { key: 'tbl-' + i, style: { overflowX: 'auto', margin: '12px 0 16px' } }, + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 13 } }, + React.createElement('tbody', null, bodyRows)))); + } + continue; + } + if (!trimmed) { i += 1; continue; } + if (trimmed.indexOf('# ') === 0) { + nodes.push(React.createElement('h1', { key: 'h1-' + i, style: { fontSize: 20, fontWeight: 700, color: '#0f172a', margin: '0 0 16px', lineHeight: 1.35 } }, parsePurchasePrdInlineText(trimmed.slice(2).trim()))); + i += 1; + continue; + } + if (trimmed.indexOf('## ') === 0) { + nodes.push(React.createElement('h2', { key: 'h2-' + i, style: { fontSize: 16, fontWeight: 700, color: '#1e293b', margin: '24px 0 12px', paddingBottom: 6, borderBottom: '2px solid #e0f2fe', lineHeight: 1.4 } }, parsePurchasePrdInlineText(trimmed.slice(3).trim()))); + i += 1; + continue; + } + if (trimmed.indexOf('### ') === 0) { + nodes.push(React.createElement('h3', { key: 'h3-' + i, style: { fontSize: 14, fontWeight: 600, color: '#334155', margin: '16px 0 8px', lineHeight: 1.45 } }, parsePurchasePrdInlineText(trimmed.slice(4).trim()))); + i += 1; + continue; + } + if (trimmed === '**文档结束**') { + nodes.push(React.createElement('div', { key: 'end-' + i, style: { marginTop: 24, paddingTop: 16, borderTop: '1px dashed #e2e8f0', color: '#94a3b8', fontSize: 13, textAlign: 'center' } }, '— 文档结束 —')); + i += 1; + continue; + } + if (/^\d+\.\s/.test(trimmed)) { + nodes.push(React.createElement('div', { key: 'ol-' + i, style: { fontSize: 13, color: '#475569', lineHeight: 1.75, margin: '6px 0 6px 4px', paddingLeft: 4 } }, parsePurchasePrdInlineText(trimmed))); + i += 1; + continue; + } + if (trimmed.indexOf('- ') === 0) { + nodes.push(React.createElement('div', { key: 'ul-' + i, style: { display: 'flex', gap: 8, fontSize: 13, color: '#475569', lineHeight: 1.75, margin: '4px 0 4px 2px' } }, + React.createElement('span', { style: { color: '#1677ff', flexShrink: 0 } }, '•'), + React.createElement('span', { style: { flex: 1 } }, parsePurchasePrdInlineText(trimmed.slice(2).trim())))); + i += 1; + continue; + } + nodes.push(React.createElement('p', { key: 'p-' + i, style: { fontSize: 13, color: '#475569', lineHeight: 1.75, margin: '6px 0' } }, parsePurchasePrdInlineText(trimmed))); + i += 1; + } + return nodes; +} +function renderPurchaseRequirementDocPanel() { + var md = typeof H2_PURCHASE_SUMMARY_REQUIREMENT_DOC !== 'undefined' && H2_PURCHASE_SUMMARY_REQUIREMENT_DOC ? H2_PURCHASE_SUMMARY_REQUIREMENT_DOC : ''; + if (!md) { + return React.createElement('div', { style: { padding: 24, color: '#64748b', textAlign: 'center' } }, '需求文档未加载'); + } + return React.createElement('div', { className: 'h2-req-doc-panel', style: { padding: '4px 4px 16px' } }, renderPurchasePrdMarkdown(md)); +} + + +// 【重要】必须使用 const Component 作为组件变量名 +// 台账数据 - 氢费(采购端)汇总报表 + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + + var antd = window.antd; + var App = antd.App; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Button = antd.Button; + var Table = antd.Table; + var Select = antd.Select; + var Row = antd.Row; + var Col = antd.Col; + var Space = antd.Space; + var Tag = antd.Tag; + var Tooltip = antd.Tooltip; + var Modal = antd.Modal; + var message = antd.message; + + var SETTLEMENT_OPTIONS = [ + { value: '预付', label: '预付' }, + { value: '月结', label: '月结' } + ]; + + /** 原型:加氢站主数据(联调后由接口加载) */ + var STATION_MASTER = [ + { code: 'HS000059', name: '佛山南海羚牛加氢站', region: '广东省/佛山市', project: '佛山氢能城配', settlement: '预付' }, + { code: 'HS000035', name: '广州联新氢能东晖加氢站', region: '广东省/广州市', project: '广州干线运输', settlement: '月结' }, + { code: '000069', name: '中国石化广州开泰北加油加氢站', region: '广东省/广州市', project: '广州干线运输', settlement: '月结' }, + { code: '000072', name: '中国石化广州金坑加氢站', region: '广东省/广州市', project: '广州冷链专线', settlement: '预付' }, + { code: 'HS000030', name: '大兴国际氢能示范区海珀尔加氢站', region: '北京市/大兴区', project: '北京示范运营', settlement: '预付' }, + { code: 'HS000007', name: '上海嘉氢实业加氢站(江桥重塑)', region: '上海市/嘉定区', project: '长三角城际', settlement: '月结' }, + { code: 'HS000096', name: '松江九亭加油站', region: '上海市/松江区', project: '长三角城际', settlement: '月结' }, + { code: 'HS000041', name: '宿迁沭阳开发区加氢站', region: '江苏省/宿迁市', project: '苏北物流', settlement: '预付' }, + { code: 'HS000040', name: '扬州文昌西路站', region: '江苏省/扬州市', project: '苏北物流', settlement: '月结' }, + { code: '000048', name: '南京溧水柘塘东站', region: '江苏省/南京市', project: '南京港口短驳', settlement: '预付' }, + { code: 'HS000051', name: '成都华通加氢站', region: '四川省/成都市', project: '西南干线', settlement: '月结' }, + { code: '000090', name: '成都天府机场高速北站', region: '四川省/成都市', project: '西南干线', settlement: '预付' }, + { code: 'HS000057', name: '重庆元琨双宝氢能综合能源站', region: '重庆市/渝北区', project: '西南干线', settlement: '月结' }, + { code: '000091', name: '重庆双溪加氢站', region: '重庆市/涪陵区', project: '西南干线', settlement: '预付' }, + { code: '000053', name: '武汉革新大道加油站', region: '湖北省/武汉市', project: '华中城配', settlement: '月结' }, + { code: 'HS000081', name: '武汉群力加油站', region: '湖北省/武汉市', project: '华中城配', settlement: '预付' }, + { code: '000049', name: '宁波镇海区中国石化加氢站', region: '浙江省/宁波市', project: '浙江氢能租赁', settlement: '月结' }, + { code: 'HS000086', name: '空气化工产品(浙江)有限公司海盐经济开发区加氢站', region: '浙江省/嘉兴市', project: '浙江氢能租赁', settlement: '预付' }, + { code: 'HS000039', name: '海盐JUPITER厂内加氢站', region: '浙江省/嘉兴市', project: '园区通勤', settlement: '预付' }, + { code: '000043', name: '豪汇综合能源站', region: '山东省/淄博市', project: '鲁中物流', settlement: '月结' } + ]; + + var REGION_OPTIONS = (function () { + var map = {}; + STATION_MASTER.forEach(function (s) { + map[s.region] = s.region; + }); + return Object.keys(map).sort().map(function (r) { + return { value: r, label: r }; + }); + })(); + + var STATION_OPTIONS = STATION_MASTER.map(function (s) { + return { value: s.code, label: s.name }; + }); + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + function numOrZero(v) { + if (v === null || v === undefined || v === '') return 0; + var n = Number(v); + return isNaN(n) ? 0 : n; + } + + function fmtMoney(n, digits) { + if (n === null || n === undefined || n === '') return '-'; + var x = Number(n); + if (isNaN(x)) return '-'; + var d = digits === undefined ? 2 : digits; + return x.toLocaleString('zh-CN', { minimumFractionDigits: d, maximumFractionDigits: d }); + } + + function fmtQty(n) { + if (n === null || n === undefined || n === '') return '-'; + var x = Number(n); + if (isNaN(x)) return '-'; + return x.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + } + + function escapeCsv(v) { + var s = v == null ? '' : String(v); + if (s.indexOf(',') !== -1 || s.indexOf('"') !== -1 || s.indexOf('\n') !== -1 || s.indexOf('\r') !== -1) { + return '"' + s.replace(/"/g, '""') + '"'; + } + return s; + } + + function downloadCsv(filename, lines) { + var csv = lines.map(function (row) { return row.map(escapeCsv).join(','); }).join('\n'); + var blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' }); + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); + } + + function findStation(code) { + for (var i = 0; i < STATION_MASTER.length; i++) { + if (STATION_MASTER[i].code === code) return STATION_MASTER[i]; + } + return null; + } + + function formatDateTime(d) { + if (!d || !window.dayjs) return '—'; + try { + var x = window.dayjs(d); + return x.isValid() ? x.format('YYYY-MM-DD HH:mm:ss') : '—'; + } catch (eDt) { + return '—'; + } + } + + function formatDate(d) { + if (!d || !window.dayjs) return '—'; + try { + var x = window.dayjs(d); + return x.isValid() ? x.format('YYYY-MM-DD') : '—'; + } catch (eD) { + return '—'; + } + } + + var MOCK_SAMPLE_PDF_URL = + 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf'; + + var MOCK_PLATES = ['粤A12345F', '浙AD12345F', '沪A06695F', '苏EF99887F', '京CN88771F', '渝A88888F']; + + /** + * 原型:车辆氢费明细钻取数据(联调后按加氢站 + 汇总条件查询车辆氢费明细接口) + * payable:应付总金额 = 该站车辆氢费明细「加氢总价」求和 + */ + function buildMockStationDetailRows(summaryRow, drillType) { + var seed = 0; + var code = summaryRow.stationCode || ''; + var i; + for (i = 0; i < code.length; i++) seed += code.charCodeAt(i); + var lineCount = Math.max(4, 5 + (seed % 6)); + var rows = []; + var unitPrice = numOrZero(summaryRow.unitPrice) || 28; + var baseDay = window.dayjs ? window.dayjs('2026-05-01') : null; + + if (drillType === 'hydrogenKg') { + var totalKg = numOrZero(summaryRow.hydrogenKg); + var remainKg = totalKg; + for (i = 0; i < lineCount; i++) { + var isLast = i === lineCount - 1; + var kg = isLast + ? Math.round(remainKg * 100) / 100 + : Math.round((totalKg / lineCount) * (0.82 + ((seed + i) % 4) * 0.06) * 100) / 100; + if (!isLast) remainKg -= kg; + var costTotal = Math.round(kg * unitPrice * 100) / 100; + var customerAmount = Math.round(kg * (unitPrice + 2) * 100) / 100; + var day = baseDay && baseDay.subtract ? baseDay.subtract((seed + i) % 40, 'day') : null; + rows.push({ + key: code + '-kg-' + i, + seq: i + 1, + hydrogenTime: day, + orderNo: 'JQ' + code + (day && day.format ? day.format('YYMMDD') : '') + String(i + 1).padStart(5, '0'), + plateNo: MOCK_PLATES[(seed + i) % MOCK_PLATES.length], + hydrogenKg: kg, + costUnitPrice: unitPrice, + costTotal: costTotal, + customerAmount: customerAmount + }); + } + return rows; + } + + if (drillType === 'payableAmount') { + var totalPay = numOrZero(summaryRow.payableAmount); + var remainPay = totalPay; + for (i = 0; i < lineCount; i++) { + var isLast = i === lineCount - 1; + var customerAmount = isLast + ? Math.round(remainPay * 100) / 100 + : Math.round((totalPay / lineCount) * (0.8 + ((seed + i) % 5) * 0.05) * 100) / 100; + if (!isLast) remainPay -= customerAmount; + var kg = unitPrice > 0 ? Math.round((customerAmount / (unitPrice + 2)) * 100) / 100 : 0; + var costTotal = Math.round(kg * unitPrice * 100) / 100; + var day = baseDay && baseDay.subtract ? baseDay.subtract((seed + i) % 40, 'day') : null; + rows.push({ + key: code + '-pay-' + i, + seq: i + 1, + hydrogenTime: day, + orderNo: 'JQ' + code + (day && day.format ? day.format('YYMMDD') : '') + String(100 + i).padStart(5, '0'), + plateNo: MOCK_PLATES[(seed + i + 2) % MOCK_PLATES.length], + hydrogenKg: kg, + costUnitPrice: unitPrice, + costTotal: costTotal, + customerAmount: customerAmount + }); + } + return rows; + } + + return rows; + } + + /** + * 原型:加氢站预付余额变更明细(联调后对接加氢站打款/余额流水接口) + */ + function buildMockPrepaidBalanceRows(summaryRow) { + var seed = 0; + var code = summaryRow.stationCode || ''; + var i; + for (i = 0; i < code.length; i++) seed += code.charCodeAt(i); + var stationName = summaryRow.stationName || '—'; + var finalBalance = numOrZero(summaryRow.prepaidBalance); + var lineCount = Math.max(5, 6 + (seed % 5)); + var rows = []; + var openingBalance = Math.round((finalBalance + 38000 + seed * 620) * 100) / 100; + if (openingBalance < 0) openingBalance = Math.abs(openingBalance) + 50000; + + rows.push({ + key: code + '-bal-0', + stationName: stationName, + incomeAmount: openingBalance, + expenseAmount: null, + balance: openingBalance, + orderNo: 'QC' + code + '260101' + }); + + var balance = openingBalance; + for (i = 1; i < lineCount - 1; i++) { + var isIncome = (seed + i) % 3 === 0; + var incomeAmount = null; + var expenseAmount = null; + if (isIncome) { + incomeAmount = Math.round((6000 + (seed + i) * 380) * 100) / 100; + balance = Math.round((balance + incomeAmount) * 100) / 100; + } else { + expenseAmount = Math.round((2800 + (seed + i) * 210) * 100) / 100; + balance = Math.round((balance - expenseAmount) * 100) / 100; + } + rows.push({ + key: code + '-bal-' + i, + stationName: stationName, + incomeAmount: incomeAmount, + expenseAmount: expenseAmount, + balance: balance, + orderNo: (isIncome ? 'DK' : 'JQ') + code + String(200 + i).padStart(5, '0') + }); + } + + var diff = Math.round((finalBalance - balance) * 100) / 100; + if (Math.abs(diff) < 0.01) { + rows.push({ + key: code + '-bal-last', + stationName: stationName, + incomeAmount: null, + expenseAmount: null, + balance: finalBalance, + orderNo: 'BZ' + code + String(900).padStart(5, '0') + }); + } else if (diff > 0) { + rows.push({ + key: code + '-bal-last', + stationName: stationName, + incomeAmount: diff, + expenseAmount: null, + balance: finalBalance, + orderNo: 'DK' + code + String(999).padStart(5, '0') + }); + } else { + rows.push({ + key: code + '-bal-last', + stationName: stationName, + incomeAmount: null, + expenseAmount: Math.round(Math.abs(diff) * 100) / 100, + balance: finalBalance, + orderNo: 'JQ' + code + String(999).padStart(5, '0') + }); + } + + return rows; + } + + /** 原型:充值总额钻取(联调后对接加氢站预充值记录接口) */ + function buildMockRechargeDrillRows(summaryRow) { + var seed = 0; + var code = summaryRow.stationCode || ''; + var i; + for (i = 0; i < code.length; i++) seed += code.charCodeAt(i); + var stationName = summaryRow.stationName || '—'; + var totalRecharge = numOrZero(summaryRow.rechargeTotal); + var rechargeCount = Math.max(2, 4 + (seed % 4)); + var rows = []; + var remain = totalRecharge; + var baseDay = window.dayjs ? window.dayjs('2026-01-15') : null; + + for (i = 0; i < rechargeCount; i++) { + var isLast = i === rechargeCount - 1; + var prepayAmount = isLast + ? Math.round(remain * 100) / 100 + : Math.round((totalRecharge / rechargeCount) * (0.75 + ((seed + i) % 5) * 0.05) * 100) / 100; + if (!isLast) remain -= prepayAmount; + var payDay = baseDay && baseDay.add ? baseDay.add((seed + i) * 17, 'day') : null; + var isPdf = (seed + i) % 3 === 0; + rows.push({ + key: code + '-rc-' + i, + stationName: stationName, + payTime: payDay, + prepayAmount: prepayAmount, + voucherUrl: isPdf + ? MOCK_SAMPLE_PDF_URL + : 'https://picsum.photos/seed/rc-' + code + '-' + i + '/960/640', + voucherFileName: '付款凭证_' + code + '_' + String(i + 1).padStart(2, '0') + (isPdf ? '.pdf' : '.jpg'), + voucherMime: isPdf ? 'pdf' : 'image' + }); + } + return rows; + } + + /** 原型:采购支付钻取(联调后对接加氢站打款账单接口) */ + function buildMockPaymentDrillRows(summaryRow) { + var seed = 0; + var code = summaryRow.stationCode || ''; + var i; + for (i = 0; i < code.length; i++) seed += code.charCodeAt(i); + var stationName = summaryRow.stationName || '—'; + var totalPaid = numOrZero(summaryRow.paidAmount); + var billCount = Math.max(2, 3 + (seed % 3)); + var rows = []; + var remainPaid = totalPaid; + var baseMonth = window.dayjs ? window.dayjs('2026-01-01') : null; + + for (i = 0; i < billCount; i++) { + var isLast = i === billCount - 1; + var paidPart = isLast + ? Math.round(remainPaid * 100) / 100 + : Math.round((totalPaid / billCount) * (0.78 + ((seed + i) % 4) * 0.06) * 100) / 100; + if (!isLast) remainPaid -= paidPart; + var payablePart = Math.round(paidPart * (1.04 + ((seed + i) % 3) * 0.02) * 100) / 100; + var start = baseMonth && baseMonth.add ? baseMonth.add(i * 2, 'month').startOf('month') : null; + var end = start && start.endOf ? start.endOf('month') : null; + rows.push({ + key: code + '-payment-' + i, + stationName: stationName, + billStartDate: start, + billEndDate: end, + payableAmount: payablePart, + paidAmount: paidPart, + voucherUrl: 'https://picsum.photos/seed/pay-' + code + '-' + i + '/960/640', + voucherName: '付款凭据_' + code + '_' + String(i + 1).padStart(2, '0') + '.jpg', + voucherMime: 'image' + }); + } + return rows; + } + + /** 原型:已开票金额钻取(联调后对接开票记录接口) */ + function buildMockInvoiceDrillRows(summaryRow) { + var seed = 0; + var code = summaryRow.stationCode || ''; + var i; + for (i = 0; i < code.length; i++) seed += code.charCodeAt(i); + var stationName = summaryRow.stationName || '—'; + var totalInvoiced = numOrZero(summaryRow.invoicedAmount); + var invoiceCount = Math.max(2, 3 + (seed % 3)); + var rows = []; + var remainInv = totalInvoiced; + var baseDay = window.dayjs ? window.dayjs('2026-02-01') : null; + + for (i = 0; i < invoiceCount; i++) { + var isLast = i === invoiceCount - 1; + var amount = isLast + ? Math.round(remainInv * 100) / 100 + : Math.round((totalInvoiced / invoiceCount) * (0.8 + ((seed + i) % 4) * 0.05) * 100) / 100; + if (!isLast) remainInv -= amount; + var isPdf = (seed + i) % 2 === 0; + var invDay = baseDay && baseDay.add ? baseDay.add((seed + i) * 11, 'day') : null; + rows.push({ + key: code + '-inv-' + i, + stationName: stationName, + invoiceTime: invDay, + invoiceAmount: amount, + invoiceUrl: isPdf + ? MOCK_SAMPLE_PDF_URL + : 'https://picsum.photos/seed/inv-' + code + '-' + i + '/860/1200', + invoiceFileName: '发票_' + code + '_' + String(i + 1).padStart(2, '0') + (isPdf ? '.pdf' : '.jpg'), + invoiceMime: isPdf ? 'pdf' : 'image' + }); + } + return rows; + } + + function fmtLedgerMoney(v) { + if (v === null || v === undefined || v === '' || Number(v) === 0) return '—'; + return fmtMoney(v, 2); + } + + /** 原型:按站点生成汇总行(联调后由后端聚合) */ + function buildMockSummaryRows() { + return STATION_MASTER.map(function (s, idx) { + var seed = idx + 1; + var hydrogenKg = Math.round((1200 + seed * 137.5) * 100) / 100; + var unitPrice = 28 + (seed % 5) * 0.5; + /** 应付总金额 = 车辆氢费明细「加氢总价」按站点求和(原型用加氢单价近似) */ + var customerUnitPrice = unitPrice + 2; + var payable = Math.round(hydrogenKg * customerUnitPrice * 100) / 100; + var paid = Math.round(payable * (0.55 + (seed % 4) * 0.1) * 100) / 100; + var unpaid = Math.round((payable - paid) * 100) / 100; + var invoiced = Math.round(Math.min(payable, paid * (0.72 + (seed % 5) * 0.05)) * 100) / 100; + var prepaid = Math.round((80000 - seed * 4200 + (seed % 3 === 0 ? -15000 : 0)) * 100) / 100; + /** 原型:首行演示预付余额为负(已欠费) */ + if (idx === 0) { + prepaid = -12580.5; + } + var rechargeTotal = Math.round((48000 + seed * 3200 + (idx === 0 ? 8000 : 0)) * 100) / 100; + return { + key: s.code, + stationCode: s.code, + region: s.region, + stationName: s.name, + hydrogenKg: hydrogenKg, + unitPrice: unitPrice, + payableAmount: payable, + paidAmount: paid, + unpaidAmount: unpaid, + invoicedAmount: invoiced, + rechargeTotal: rechargeTotal, + prepaidBalance: prepaid, + settlement: s.settlement + }; + }); + } + + var layoutStyle = { + padding: '16px 24px 24px', + minHeight: '100vh', + background: 'linear-gradient(165deg, #eef4ff 0%, #f5f7fa 42%, #f0f2f5 100%)' + }; + var filterLabelStyle = { marginBottom: 6, fontSize: 13, color: 'rgba(0,0,0,0.55)', fontWeight: 500 }; + var filterItemStyle = { marginBottom: 12 }; + var filterControlStyle = { width: '100%' }; + var filterActionsColStyle = { flex: '0 0 auto', marginLeft: 'auto' }; + var filterCardStyle = { + marginBottom: 20, + borderRadius: 16, + boxShadow: '0 4px 20px -4px rgba(16,24,40,0.03), 0 0 0 1px rgba(16,24,40,0.06)', + border: 'none', + background: '#ffffff' + }; + var tableCardStyle = { + borderRadius: 16, + boxShadow: '0 10px 32px -4px rgba(16,24,40,0.06), 0 0 0 1px rgba(16,24,40,0.04)', + border: 'none', + background: '#ffffff', + overflow: 'hidden' + }; + var ledgerTableStyle = + '.h2-purchase-summary-wrap{border-radius:12px;overflow:hidden;box-shadow:0 4px 24px -6px rgba(15,23,42,0.05),0 0 0 1px rgba(22,119,255,0.1)}' + + '.h2-purchase-summary-wrap .ant-table-wrapper,.h2-purchase-summary-wrap .ant-table-content{overflow-x:hidden!important}' + + '.h2-purchase-summary-wrap .h2-purchase-summary-table{width:100%!important;table-layout:fixed}' + + '.h2-purchase-summary-table .ant-table-thead>tr>th{white-space:nowrap;color:#1e293b!important;font-weight:600!important;font-size:13px!important;' + + 'background:#e8f4fc!important;border-bottom:1px solid #bae6fd!important;border-inline-end:1px solid #dbeafe!important;padding:0 8px!important;height:38px!important}' + + '.h2-purchase-summary-table .ant-table-tbody>tr:not(.ant-table-measure-row)>td{white-space:nowrap;font-variant-numeric:tabular-nums;color:#334155;border-bottom:1px solid #f1f5f9!important;border-inline-end:1px solid #f8fafc!important;padding:0 8px!important;height:38px!important;overflow:hidden!important}' + + '.h2-purchase-summary-table td.h2-cell-prepaid-balance{overflow:hidden!important}' + + '.h2-purchase-summary-table .h2-prepaid-balance-cell{display:flex;align-items:center;justify-content:flex-end;gap:4px;max-width:100%;min-width:0;overflow:hidden;box-sizing:border-box}' + + '.h2-purchase-summary-table .h2-prepaid-balance-cell .h2-prepaid-balance-amount{flex:0 1 auto;min-width:0;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border:none;background:none;padding:0;font:inherit;cursor:pointer;text-decoration:underline;text-underline-offset:2px}' + + '.h2-purchase-summary-table .h2-prepaid-balance-cell .ant-tag{flex-shrink:0;margin:0!important;font-size:11px;line-height:16px;padding:0 5px}' + + '.h2-purchase-summary-table .ant-table-tbody>tr.h2-purchase-row:hover>td{background:#f0f9ff!important}' + + '.h2-purchase-summary-table .ant-table-summary>tr>td{font-weight:700;background:#f8fafc!important;color:#0f172a!important;border-top:2px solid #cbd5e1!important;padding:0 8px!important;height:38px!important}' + + '.h2-ledger-totals-bar{display:flex;align-items:stretch;gap:0;margin-bottom:10px;border:1px solid #bae6fd;border-radius:10px;overflow:hidden;background:#f8fafc;box-shadow:0 1px 0 rgba(15,23,42,0.04)}' + + '.h2-ledger-totals-bar__title{display:flex;align-items:center;justify-content:center;min-width:72px;padding:10px 14px;font-size:14px;font-weight:700;color:#0f172a;background:#e8f4fc;border-right:1px solid #bae6fd}' + + '.h2-ledger-totals-bar__items{display:flex;flex:1;flex-wrap:wrap}' + + '.h2-ledger-totals-bar__item{flex:1;min-width:140px;padding:8px 16px;border-right:1px solid #e2e8f0;display:flex;flex-direction:column;justify-content:center;gap:4px}' + + '.h2-ledger-totals-bar__item:last-child{border-right:none}' + + '.h2-ledger-totals-bar__label{font-size:12px;color:rgba(15,23,42,0.55);font-weight:500;line-height:1.2}' + + '.h2-ledger-totals-bar__value{font-size:16px;font-weight:700;color:#0f172a;font-variant-numeric:tabular-nums;line-height:1.3}' + + '.h2-ledger-totals-bar__value.is-warn{color:#cf1322}' + + '.h2-purchase-summary-table .h2-col-header{position:relative;display:flex;align-items:center;width:100%;height:100%;min-height:38px;padding:0 10px 0 8px;box-sizing:border-box}' + + '.h2-purchase-summary-table .h2-col-header__text{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' + + '.h2-purchase-summary-table .h2-col-header__resizer{position:absolute;right:0;top:0;bottom:0;width:8px;cursor:col-resize;z-index:2;touch-action:none}' + + '.h2-purchase-summary-table .h2-col-header__resizer:hover{background:rgba(22,119,255,0.12)}' + + '.h2-purchase-summary-table.ant-table-wrapper .ant-table-thead>tr>th{position:relative}' + + '.h2-req-doc-panel{max-width:100%}' + + '.h2-req-doc-panel h1:first-child{margin-top:0}'; + + /** 主表默认列宽 */ + var DEFAULT_MAIN_COLUMN_WIDTHS = { + seq: 48, + region: 96, + stationName: 148, + settlement: 72, + hydrogenKg: 96, + payableAmount: 100, + paidAmount: 100, + unpaidAmount: 100, + invoicedAmount: 100, + uninvoicedAmount: 100, + rechargeTotal: 100, + prepaidBalance: 118 + }; + var MIN_COLUMN_WIDTH = 48; + + var drillLinkStyle = { + cursor: 'pointer', + color: '#1677ff', + border: 'none', + background: 'none', + padding: 0, + font: 'inherit', + fontVariantNumeric: 'tabular-nums', + textDecoration: 'underline', + textUnderlineOffset: 2 + }; + + var regionDraftState = useState(undefined); + var regionDraft = regionDraftState[0]; + var setRegionDraft = regionDraftState[1]; + var stationDraftState = useState(undefined); + var stationDraft = stationDraftState[0]; + var setStationDraft = stationDraftState[1]; + var settlementDraftState = useState(undefined); + var settlementDraft = settlementDraftState[0]; + var setSettlementDraft = settlementDraftState[1]; + + var regionAppliedState = useState(undefined); + var regionApplied = regionAppliedState[0]; + var setRegionApplied = regionAppliedState[1]; + var stationAppliedState = useState(undefined); + var stationApplied = stationAppliedState[0]; + var setStationApplied = stationAppliedState[1]; + var settlementAppliedState = useState(undefined); + var settlementApplied = settlementAppliedState[0]; + var setSettlementApplied = settlementAppliedState[1]; + + var detailDrillModalState = useState({ + open: false, + type: '', + stationName: '', + summary: null, + rows: [] + }); + var detailDrillModal = detailDrillModalState[0]; + var setDetailDrillModal = detailDrillModalState[1]; + + var prepaidBalanceDrillModalState = useState({ + open: false, + stationName: '', + endingBalance: 0, + rows: [] + }); + var prepaidBalanceDrillModal = prepaidBalanceDrillModalState[0]; + var setPrepaidBalanceDrillModal = prepaidBalanceDrillModalState[1]; + + var paymentDrillModalState = useState({ open: false, stationName: '', rows: [] }); + var paymentDrillModal = paymentDrillModalState[0]; + var setPaymentDrillModal = paymentDrillModalState[1]; + + var invoiceDrillModalState = useState({ open: false, stationName: '', rows: [] }); + var invoiceDrillModal = invoiceDrillModalState[0]; + var setInvoiceDrillModal = invoiceDrillModalState[1]; + + var rechargeDrillModalState = useState({ open: false, stationName: '', rows: [] }); + var rechargeDrillModal = rechargeDrillModalState[0]; + var setRechargeDrillModal = rechargeDrillModalState[1]; + + var attachmentPreviewState = useState({ open: false, url: '', title: '', mime: 'image' }); + var attachmentPreview = attachmentPreviewState[0]; + var setAttachmentPreview = attachmentPreviewState[1]; + + var reqDetailOpenState = useState(false); + var reqDetailOpen = reqDetailOpenState[0]; + var setReqDetailOpen = reqDetailOpenState[1]; + + var columnWidthsState = useState(function () { + return Object.assign({}, DEFAULT_MAIN_COLUMN_WIDTHS); + }); + var columnWidths = columnWidthsState[0]; + var setColumnWidths = columnWidthsState[1]; + var columnWidthsRef = { current: columnWidths }; + columnWidthsRef.current = columnWidths; + + var handleColumnResize = useCallback(function (key, width) { + setColumnWidths(function (prev) { + var next = Object.assign({}, prev); + next[key] = Math.max(MIN_COLUMN_WIDTH, Math.round(width)); + return next; + }); + }, []); + + var makeResizableTitle = useCallback( + function (title, key) { + return function ResizableColumnTitle() { + var onResizeMouseDown = function (e) { + if (e.button !== 0) return; + e.preventDefault(); + e.stopPropagation(); + var startX = e.clientX; + var startWidth = columnWidthsRef.current[key] || DEFAULT_MAIN_COLUMN_WIDTHS[key] || 100; + var onMouseMove = function (ev) { + handleColumnResize(key, startWidth + ev.clientX - startX); + }; + var onMouseUp = function () { + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + }; + document.body.style.cursor = 'col-resize'; + document.body.style.userSelect = 'none'; + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + }; + return React.createElement( + 'div', + { className: 'h2-col-header' }, + React.createElement('span', { className: 'h2-col-header__text' }, title), + React.createElement('span', { + className: 'h2-col-header__resizer', + onMouseDown: onResizeMouseDown, + onClick: function (ev) { + ev.stopPropagation(); + } + }) + ); + }; + }, + [handleColumnResize] + ); + + var allRows = useMemo(function () { + return buildMockSummaryRows(); + }, []); + + var dataSource = useMemo(function () { + var list = allRows.filter(function (r) { + if (regionApplied && r.region !== regionApplied) return false; + if (stationApplied && r.stationCode !== stationApplied) return false; + if (settlementApplied && r.settlement !== settlementApplied) return false; + return true; + }); + return list.map(function (r, idx) { + var unpaid = Math.round((numOrZero(r.payableAmount) - numOrZero(r.paidAmount)) * 100) / 100; + var uninvoiced = Math.round((numOrZero(r.payableAmount) - numOrZero(r.invoicedAmount)) * 100) / 100; + return Object.assign({}, r, { seq: idx + 1, unpaidAmount: unpaid, uninvoicedAmount: uninvoiced }); + }); + }, [allRows, regionApplied, stationApplied, settlementApplied]); + + var totals = useMemo(function () { + var acc = dataSource.reduce( + function (sum, r) { + sum.hydrogenKg += numOrZero(r.hydrogenKg); + sum.payableAmount += numOrZero(r.payableAmount); + sum.paidAmount += numOrZero(r.paidAmount); + sum.invoicedAmount += numOrZero(r.invoicedAmount); + sum.rechargeTotal += numOrZero(r.rechargeTotal); + sum.prepaidBalance += numOrZero(r.prepaidBalance); + return sum; + }, + { hydrogenKg: 0, payableAmount: 0, paidAmount: 0, invoicedAmount: 0, rechargeTotal: 0, prepaidBalance: 0 } + ); + acc.unpaidAmount = Math.round((acc.payableAmount - acc.paidAmount) * 100) / 100; + acc.uninvoicedAmount = Math.round((acc.payableAmount - acc.invoicedAmount) * 100) / 100; + return acc; + }, [dataSource]); + + var filterSummaryText = useMemo(function () { + var parts = []; + parts.push('地区:' + (regionApplied || '全部')); + var st = findStation(stationApplied); + parts.push('加氢站:' + (st ? st.name : '全部')); + parts.push('结算方式:' + (settlementApplied || '全部')); + return parts.join('  '); + }, [regionApplied, stationApplied, settlementApplied]); + + var openDetailDrill = useCallback(function (record, drillType) { + var rows = buildMockStationDetailRows(record, drillType); + var title = + drillType === 'payableAmount' + ? '应付总金额明细' + : '加氢总量明细'; + setDetailDrillModal({ + open: true, + type: drillType, + stationName: record.stationName, + summary: record, + rows: rows + }); + }, []); + + var closeDetailDrill = useCallback(function () { + setDetailDrillModal({ open: false, type: '', stationName: '', summary: null, rows: [] }); + }, []); + + var openPrepaidBalanceDrill = useCallback(function (record) { + setPrepaidBalanceDrillModal({ + open: true, + stationName: record.stationName, + endingBalance: numOrZero(record.prepaidBalance), + rows: buildMockPrepaidBalanceRows(record) + }); + }, []); + + var closePrepaidBalanceDrill = useCallback(function () { + setPrepaidBalanceDrillModal({ open: false, stationName: '', endingBalance: 0, rows: [] }); + }, []); + + var openPaymentDrill = useCallback(function (record) { + setPaymentDrillModal({ + open: true, + stationName: record.stationName, + rows: buildMockPaymentDrillRows(record) + }); + }, []); + + var closePaymentDrill = useCallback(function () { + setPaymentDrillModal({ open: false, stationName: '', rows: [] }); + }, []); + + var openInvoiceDrill = useCallback(function (record) { + setInvoiceDrillModal({ + open: true, + stationName: record.stationName, + rows: buildMockInvoiceDrillRows(record) + }); + }, []); + + var closeInvoiceDrill = useCallback(function () { + setInvoiceDrillModal({ open: false, stationName: '', rows: [] }); + }, []); + + var openRechargeDrill = useCallback(function (record) { + setRechargeDrillModal({ + open: true, + stationName: record.stationName, + rows: buildMockRechargeDrillRows(record) + }); + }, []); + + var closeRechargeDrill = useCallback(function () { + setRechargeDrillModal({ open: false, stationName: '', rows: [] }); + }, []); + + var openAttachmentPreview = useCallback(function (file) { + if (!file || !file.url) { + message.warning('暂无附件'); + return; + } + setAttachmentPreview({ + open: true, + url: file.url, + title: file.title || '预览', + mime: file.mime || 'image' + }); + }, []); + + var closeAttachmentPreview = useCallback(function () { + setAttachmentPreview({ open: false, url: '', title: '', mime: 'image' }); + }, []); + + var downloadAttachment = useCallback(function (file) { + if (!file || !file.url) { + message.warning('暂无文件'); + return; + } + var a = document.createElement('a'); + a.href = file.url; + a.download = file.fileName || 'download'; + a.target = '_blank'; + a.rel = 'noopener noreferrer'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + message.success('开始下载'); + }, []); + + var renderDrillLink = useCallback(function (display, record, drillType) { + return React.createElement( + 'button', + { + type: 'button', + style: drillLinkStyle, + title: '点击查看明细', + onClick: function (e) { + if (e && e.stopPropagation) e.stopPropagation(); + openDetailDrill(record, drillType); + } + }, + display + ); + }, [openDetailDrill]); + + var renderDrillLinkWithHandler = useCallback(function (display, record, onDrill) { + return React.createElement( + 'button', + { + type: 'button', + style: drillLinkStyle, + title: '点击查看明细', + onClick: function (e) { + if (e && e.stopPropagation) e.stopPropagation(); + onDrill(record); + } + }, + display + ); + }, []); + + var renderPrepaidBalanceCell = useCallback(function (v, record) { + var n = numOrZero(v); + var isArrears = n < 0; + var amountStyle = isArrears + ? { color: '#cf1322', fontVariantNumeric: 'tabular-nums' } + : { color: '#1677ff', fontVariantNumeric: 'tabular-nums' }; + return React.createElement( + 'div', + { className: 'h2-prepaid-balance-cell' }, + isArrears + ? React.createElement(Tag, { color: 'error' }, '已欠费') + : null, + React.createElement( + 'button', + { + type: 'button', + className: 'h2-prepaid-balance-amount', + style: amountStyle, + title: '点击查看明细', + onClick: function (e) { + if (e && e.stopPropagation) e.stopPropagation(); + openPrepaidBalanceDrill(record); + } + }, + fmtMoney(v, 2) + ) + ); + }, [openPrepaidBalanceDrill]); + + var renderUnpaidAmountCell = useCallback(function (v) { + var n = numOrZero(v); + var style = n > 0 ? { color: '#cf1322', fontWeight: 600, fontVariantNumeric: 'tabular-nums' } : { fontVariantNumeric: 'tabular-nums' }; + return React.createElement('span', { style: style }, fmtMoney(v, 2)); + }, []); + + var renderUninvoicedAmountCell = renderUnpaidAmountCell; + + var handleQuery = useCallback(function () { + setRegionApplied(regionDraft); + setStationApplied(stationDraft); + setSettlementApplied(settlementDraft); + message.success('查询成功'); + }, [regionDraft, stationDraft, settlementDraft]); + + var handleReset = useCallback(function () { + setRegionDraft(undefined); + setStationDraft(undefined); + setSettlementDraft(undefined); + setRegionApplied(undefined); + setStationApplied(undefined); + setSettlementApplied(undefined); + }, []); + + var handleExport = useCallback(function () { + var headers = [ + '序号', + '地区', + '加氢站全称', + '结算方式', + '加氢总量(kg)', + '应付总金额(元)', + '已付总金额(元)', + '未付总金额(元)', + '已开票金额(元)', + '未开票金额(元)', + '充值总额(元)', + '当前余额(元)', + '结算方式' + ]; + var body = dataSource.map(function (r) { + return [ + r.seq, + r.region, + r.stationName, + r.settlement, + r.hydrogenKg, + r.payableAmount, + r.paidAmount, + r.unpaidAmount, + r.invoicedAmount, + r.uninvoicedAmount, + r.rechargeTotal, + r.prepaidBalance + ]; + }); + body.push([ + '合计', + '', + '', + '', + '', + Math.round(totals.hydrogenKg * 100) / 100, + '', + Math.round(totals.payableAmount * 100) / 100, + Math.round(totals.paidAmount * 100) / 100, + Math.round(totals.unpaidAmount * 100) / 100, + Math.round(totals.invoicedAmount * 100) / 100, + Math.round(totals.uninvoicedAmount * 100) / 100, + Math.round(totals.rechargeTotal * 100) / 100, + Math.round(totals.prepaidBalance * 100) / 100 + ]); + downloadCsv('氢费采购端汇总报表_' + new Date().getTime() + '.csv', [headers].concat(body)); + message.success('已导出 CSV'); + }, [dataSource, totals]); + + var columns = useMemo(function () { + var w = columnWidths; + return [ + { + title: makeResizableTitle('序号', 'seq'), + dataIndex: 'seq', + key: 'seq', + width: w.seq, + align: 'center', + fixed: 'left' + }, + { + title: makeResizableTitle('地区', 'region'), + dataIndex: 'region', + key: 'region', + width: w.region, + align: 'center', + ellipsis: true + }, + { + title: makeResizableTitle('加氢站全称', 'stationName'), + dataIndex: 'stationName', + key: 'stationName', + width: w.stationName, + align: 'left', + ellipsis: true + }, + { + title: makeResizableTitle('结算方式', 'settlement'), + dataIndex: 'settlement', + key: 'settlement', + width: w.settlement, + align: 'center' + }, + { + title: makeResizableTitle('加氢总量(kg)', 'hydrogenKg'), + dataIndex: 'hydrogenKg', + key: 'hydrogenKg', + width: w.hydrogenKg, + align: 'right', + render: function (v, record) { + return renderDrillLink(fmtQty(v), record, 'hydrogenKg'); + } + }, + { + title: makeResizableTitle('应付总金额(元)', 'payableAmount'), + dataIndex: 'payableAmount', + key: 'payableAmount', + width: w.payableAmount, + align: 'right', + render: function (v, record) { + return renderDrillLink(fmtMoney(v, 2), record, 'payableAmount'); + } + }, + { + title: makeResizableTitle('已付总金额(元)', 'paidAmount'), + dataIndex: 'paidAmount', + key: 'paidAmount', + width: w.paidAmount, + align: 'right', + render: function (v, record) { + return renderDrillLinkWithHandler(fmtMoney(v, 2), record, openPaymentDrill); + } + }, + { + title: makeResizableTitle('未付总金额(元)', 'unpaidAmount'), + dataIndex: 'unpaidAmount', + key: 'unpaidAmount', + width: w.unpaidAmount, + align: 'right', + render: renderUnpaidAmountCell + }, + { + title: makeResizableTitle('已开票金额(元)', 'invoicedAmount'), + dataIndex: 'invoicedAmount', + key: 'invoicedAmount', + width: w.invoicedAmount, + align: 'right', + render: function (v, record) { + return renderDrillLinkWithHandler(fmtMoney(v, 2), record, openInvoiceDrill); + } + }, + { + title: makeResizableTitle('未开票金额(元)', 'uninvoicedAmount'), + dataIndex: 'uninvoicedAmount', + key: 'uninvoicedAmount', + width: w.uninvoicedAmount, + align: 'right', + render: renderUninvoicedAmountCell + }, + { + title: makeResizableTitle('充值总额(元)', 'rechargeTotal'), + dataIndex: 'rechargeTotal', + key: 'rechargeTotal', + width: w.rechargeTotal, + align: 'right', + render: function (v, record) { + return renderDrillLinkWithHandler(fmtMoney(v, 2), record, openRechargeDrill); + } + }, + { + title: makeResizableTitle('当前余额(元)', 'prepaidBalance'), + dataIndex: 'prepaidBalance', + key: 'prepaidBalance', + width: w.prepaidBalance, + align: 'right', + className: 'h2-col-prepaid-balance', + onCell: function () { + return { className: 'h2-cell-prepaid-balance' }; + }, + render: renderPrepaidBalanceCell + } + ]; + }, [ + columnWidths, + makeResizableTitle, + renderDrillLink, + renderDrillLinkWithHandler, + openPaymentDrill, + openInvoiceDrill, + openRechargeDrill, + renderUnpaidAmountCell, + renderUninvoicedAmountCell, + renderPrepaidBalanceCell + ]); + + var detailDrillBaseColumns = useMemo(function () { + return [ + { title: '序号', dataIndex: 'seq', key: 'seq', width: 56, align: 'center' }, + { + title: '加氢时间', + dataIndex: 'hydrogenTime', + key: 'hydrogenTime', + width: 168, + align: 'center', + render: function (v) { return formatDateTime(v); } + }, + { + title: '订单编号', + dataIndex: 'orderNo', + key: 'orderNo', + width: 168, + align: 'center', + render: function (v) { + return React.createElement('span', { style: { fontFamily: 'monospace', fontSize: 12 } }, v || '—'); + } + }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 108, align: 'center' } + ]; + }, []); + + var detailDrillQtyColumn = useMemo(function () { + return { + title: '加氢量(kg)', + dataIndex: 'hydrogenKg', + key: 'hydrogenKg', + width: 100, + align: 'right', + render: function (v) { return fmtQty(v); } + }; + }, []); + + var detailDrillCostColumns = useMemo(function () { + return [ + { + title: '成本单价(元/kg)', + dataIndex: 'costUnitPrice', + key: 'costUnitPrice', + width: 120, + align: 'right', + render: function (v) { return fmtMoney(v, 2); } + }, + { + title: '成本总价(元)', + dataIndex: 'costTotal', + key: 'costTotal', + width: 110, + align: 'right', + render: function (v) { return fmtMoney(v, 2); } + } + ]; + }, []); + + var detailDrillColumns = useMemo( + function () { + if (detailDrillModal.type === 'payableAmount') { + return detailDrillBaseColumns.concat([detailDrillQtyColumn], detailDrillCostColumns); + } + return detailDrillBaseColumns.concat([detailDrillQtyColumn]); + }, + [detailDrillModal.type, detailDrillBaseColumns, detailDrillQtyColumn, detailDrillCostColumns] + ); + + var detailDrillSummary = useMemo(function () { + if (!detailDrillModal.rows || !detailDrillModal.rows.length) { + return { hydrogenKg: 0, costTotal: 0 }; + } + return detailDrillModal.rows.reduce( + function (acc, r) { + acc.hydrogenKg += numOrZero(r.hydrogenKg); + acc.costTotal += numOrZero(r.costTotal); + return acc; + }, + { hydrogenKg: 0, costTotal: 0 } + ); + }, [detailDrillModal.rows]); + + var detailDrillTableSummary = useCallback(function () { + if (detailDrillModal.type === 'payableAmount') { + return React.createElement( + Table.Summary, + null, + React.createElement( + Table.Summary.Row, + null, + React.createElement(Table.Summary.Cell, { index: 0, align: 'center' }, '合计'), + React.createElement(Table.Summary.Cell, { index: 1, colSpan: 3 }), + React.createElement(Table.Summary.Cell, { index: 4, align: 'right' }, fmtQty(detailDrillSummary.hydrogenKg)), + React.createElement(Table.Summary.Cell, { index: 5 }), + React.createElement(Table.Summary.Cell, { index: 6, align: 'right' }, fmtMoney(detailDrillSummary.costTotal, 2)) + ) + ); + } + return React.createElement( + Table.Summary, + null, + React.createElement( + Table.Summary.Row, + null, + React.createElement(Table.Summary.Cell, { index: 0, align: 'center' }, '合计'), + React.createElement(Table.Summary.Cell, { index: 1, colSpan: 3 }), + React.createElement(Table.Summary.Cell, { index: 4, align: 'right' }, fmtQty(detailDrillSummary.hydrogenKg)) + ) + ); + }, [detailDrillModal.type, detailDrillSummary]); + + var detailDrillTitle = useMemo(function () { + if (!detailDrillModal.open) return ''; + var name = detailDrillModal.stationName ? ' · ' + detailDrillModal.stationName : ''; + return (detailDrillModal.type === 'payableAmount' ? '应付总金额明细' : '加氢量明细') + name; + }, [detailDrillModal]); + + var renderTotalsBar = useCallback(function () { + var items = [ + { key: 'hydrogenKg', label: '加氢总量(kg)', value: fmtQty(totals.hydrogenKg) }, + { key: 'payableAmount', label: '应付总金额(元)', value: fmtMoney(totals.payableAmount, 2) }, + { key: 'paidAmount', label: '已付总金额(元)', value: fmtMoney(totals.paidAmount, 2) }, + { + key: 'unpaidAmount', + label: '未付总金额(元)', + value: fmtMoney(totals.unpaidAmount, 2), + warn: numOrZero(totals.unpaidAmount) > 0 + }, + { + key: 'uninvoicedAmount', + label: '未开票总金额(元)', + value: fmtMoney(totals.uninvoicedAmount, 2), + warn: numOrZero(totals.uninvoicedAmount) > 0 + }, + { key: 'rechargeTotal', label: '充值总额(元)', value: fmtMoney(totals.rechargeTotal, 2) }, + { + key: 'prepaidBalance', + label: '当前总余额(元)', + value: fmtMoney(totals.prepaidBalance, 2), + warn: numOrZero(totals.prepaidBalance) < 0 + } + ]; + return React.createElement( + 'div', + { className: 'h2-ledger-totals-bar' }, + React.createElement('div', { className: 'h2-ledger-totals-bar__title' }, '合计'), + React.createElement( + 'div', + { className: 'h2-ledger-totals-bar__items' }, + items.map(function (item) { + return React.createElement( + 'div', + { key: item.key, className: 'h2-ledger-totals-bar__item' }, + React.createElement('div', { className: 'h2-ledger-totals-bar__label' }, item.label), + React.createElement( + 'div', + { + className: + 'h2-ledger-totals-bar__value' + (item.warn ? ' is-warn' : '') + }, + item.value + ) + ); + }) + ) + ); + }, [totals]); + + var prepaidBalanceDrillColumns = useMemo(function () { + return [ + { + title: '加氢站全称', + dataIndex: 'stationName', + key: 'stationName', + width: 220, + ellipsis: true + }, + { + title: '收入金额(元)', + dataIndex: 'incomeAmount', + key: 'incomeAmount', + width: 120, + align: 'right', + render: function (v) { return fmtLedgerMoney(v); } + }, + { + title: '支出金额(元)', + dataIndex: 'expenseAmount', + key: 'expenseAmount', + width: 120, + align: 'right', + render: function (v) { return fmtLedgerMoney(v); } + }, + { + title: '余额(元)', + dataIndex: 'balance', + key: 'balance', + width: 120, + align: 'right', + render: function (v) { + var n = numOrZero(v); + var style = n < 0 ? { color: '#cf1322', fontWeight: 600 } : undefined; + return React.createElement('span', { style: style }, fmtMoney(v, 2)); + } + }, + { + title: '订单编号', + dataIndex: 'orderNo', + key: 'orderNo', + width: 168, + align: 'center', + render: function (v) { + return React.createElement('span', { style: { fontFamily: 'monospace', fontSize: 12 } }, v || '—'); + } + } + ]; + }, []); + + var prepaidBalanceDrillSummary = useMemo(function () { + if (!prepaidBalanceDrillModal.rows || !prepaidBalanceDrillModal.rows.length) { + return { incomeTotal: 0, expenseTotal: 0, endingBalance: 0 }; + } + var last = prepaidBalanceDrillModal.rows[prepaidBalanceDrillModal.rows.length - 1]; + return prepaidBalanceDrillModal.rows.reduce( + function (acc, r) { + acc.incomeTotal += numOrZero(r.incomeAmount); + acc.expenseTotal += numOrZero(r.expenseAmount); + return acc; + }, + { incomeTotal: 0, expenseTotal: 0, endingBalance: numOrZero(last.balance) } + ); + }, [prepaidBalanceDrillModal.rows]); + + var prepaidBalanceDrillTableSummary = useCallback(function () { + return React.createElement( + Table.Summary, + null, + React.createElement( + Table.Summary.Row, + null, + React.createElement(Table.Summary.Cell, { index: 0, align: 'center' }, '合计'), + React.createElement( + Table.Summary.Cell, + { index: 1, align: 'right' }, + fmtMoney(prepaidBalanceDrillSummary.incomeTotal, 2) + ), + React.createElement( + Table.Summary.Cell, + { index: 2, align: 'right' }, + fmtMoney(prepaidBalanceDrillSummary.expenseTotal, 2) + ), + React.createElement( + Table.Summary.Cell, + { index: 3, align: 'right' }, + fmtMoney(prepaidBalanceDrillSummary.endingBalance, 2) + ), + React.createElement(Table.Summary.Cell, { index: 4 }) + ) + ); + }, [prepaidBalanceDrillSummary]); + + var rechargeDrillColumns = useMemo( + function () { + return [ + { + title: '加氢站全称', + dataIndex: 'stationName', + key: 'stationName', + width: 220, + ellipsis: true + }, + { + title: '支付时间', + dataIndex: 'payTime', + key: 'payTime', + width: 120, + align: 'center', + render: function (v) { return formatDate(v); } + }, + { + title: '预充金额(元)', + dataIndex: 'prepayAmount', + key: 'prepayAmount', + width: 120, + align: 'right', + render: function (v) { return fmtMoney(v, 2); } + }, + { + title: '付款凭证', + key: 'voucher', + width: 160, + align: 'center', + render: function (_, row) { + if (!row.voucherUrl) return '—'; + var file = { + url: row.voucherUrl, + fileName: row.voucherFileName, + mime: row.voucherMime || 'image', + title: row.voucherFileName || '付款凭证' + }; + return React.createElement( + Space, + { size: 4, wrap: true, style: { justifyContent: 'center' } }, + React.createElement( + Button, + { + type: 'link', + size: 'small', + style: { padding: 0 }, + onClick: function () { + openAttachmentPreview({ + url: file.url, + title: file.title, + mime: file.mime + }); + } + }, + '预览' + ), + React.createElement( + Button, + { + type: 'link', + size: 'small', + style: { padding: 0 }, + onClick: function () { + downloadAttachment(file); + } + }, + '下载' + ) + ); + } + } + ]; + }, + [openAttachmentPreview, downloadAttachment] + ); + + var rechargeDrillSummary = useMemo(function () { + if (!rechargeDrillModal.rows || !rechargeDrillModal.rows.length) { + return { prepayAmount: 0 }; + } + return rechargeDrillModal.rows.reduce( + function (acc, r) { + acc.prepayAmount += numOrZero(r.prepayAmount); + return acc; + }, + { prepayAmount: 0 } + ); + }, [rechargeDrillModal.rows]); + + var rechargeDrillTableSummary = useCallback(function () { + return React.createElement( + Table.Summary, + null, + React.createElement( + Table.Summary.Row, + null, + React.createElement(Table.Summary.Cell, { index: 0, align: 'center' }, '合计'), + React.createElement(Table.Summary.Cell, { index: 1 }), + React.createElement( + Table.Summary.Cell, + { index: 2, align: 'right' }, + fmtMoney(rechargeDrillSummary.prepayAmount, 2) + ), + React.createElement(Table.Summary.Cell, { index: 3 }) + ) + ); + }, [rechargeDrillSummary]); + + var paymentDrillColumns = useMemo( + function () { + return [ + { + title: '加氢站全称', + dataIndex: 'stationName', + key: 'stationName', + width: 220, + ellipsis: true + }, + { + title: '账单开始时间', + dataIndex: 'billStartDate', + key: 'billStartDate', + width: 120, + align: 'center', + render: function (v) { return formatDate(v); } + }, + { + title: '账单结束时间', + dataIndex: 'billEndDate', + key: 'billEndDate', + width: 120, + align: 'center', + render: function (v) { return formatDate(v); } + }, + { + title: '应付总金额(元)', + dataIndex: 'payableAmount', + key: 'payableAmount', + width: 120, + align: 'right', + render: function (v) { return fmtMoney(v, 2); } + }, + { + title: '已付总金额(元)', + dataIndex: 'paidAmount', + key: 'paidAmount', + width: 140, + align: 'right', + render: function (v) { return fmtMoney(v, 2); } + }, + { + title: '银行付款证明', + key: 'voucher', + width: 130, + align: 'center', + render: function (_, row) { + if (!row.voucherUrl) return '—'; + return React.createElement( + Button, + { + type: 'link', + size: 'small', + style: { padding: 0 }, + onClick: function () { + openAttachmentPreview({ + url: row.voucherUrl, + title: row.voucherName || '付款凭据', + mime: row.voucherMime || 'image' + }); + } + }, + '查看付款凭据' + ); + } + } + ]; + }, + [openAttachmentPreview] + ); + + var paymentDrillSummary = useMemo(function () { + if (!paymentDrillModal.rows || !paymentDrillModal.rows.length) { + return { payableAmount: 0, paidAmount: 0 }; + } + return paymentDrillModal.rows.reduce( + function (acc, r) { + acc.payableAmount += numOrZero(r.payableAmount); + acc.paidAmount += numOrZero(r.paidAmount); + return acc; + }, + { payableAmount: 0, paidAmount: 0 } + ); + }, [paymentDrillModal.rows]); + + var paymentDrillTableSummary = useCallback(function () { + return React.createElement( + Table.Summary, + null, + React.createElement( + Table.Summary.Row, + null, + React.createElement(Table.Summary.Cell, { index: 0, align: 'center' }, '合计'), + React.createElement(Table.Summary.Cell, { index: 1, colSpan: 2 }), + React.createElement( + Table.Summary.Cell, + { index: 3, align: 'right' }, + fmtMoney(paymentDrillSummary.payableAmount, 2) + ), + React.createElement( + Table.Summary.Cell, + { index: 4, align: 'right' }, + fmtMoney(paymentDrillSummary.paidAmount, 2) + ), + React.createElement(Table.Summary.Cell, { index: 5 }) + ) + ); + }, [paymentDrillSummary]); + + var invoiceDrillColumns = useMemo( + function () { + return [ + { + title: '加氢站全称', + dataIndex: 'stationName', + key: 'stationName', + width: 220, + ellipsis: true + }, + { + title: '开票时间', + dataIndex: 'invoiceTime', + key: 'invoiceTime', + width: 168, + align: 'center', + render: function (v) { return formatDateTime(v); } + }, + { + title: '开票金额(元)', + dataIndex: 'invoiceAmount', + key: 'invoiceAmount', + width: 120, + align: 'right', + render: function (v) { return fmtMoney(v, 2); } + }, + { + title: '发票', + key: 'invoiceFile', + width: 200, + align: 'center', + render: function (_, row) { + if (!row.invoiceUrl) return '—'; + var file = { + url: row.invoiceUrl, + fileName: row.invoiceFileName, + mime: row.invoiceMime || 'image', + title: row.invoiceFileName || '发票' + }; + return React.createElement( + Space, + { size: 4, wrap: true, style: { justifyContent: 'center' } }, + React.createElement( + Button, + { + type: 'link', + size: 'small', + style: { padding: 0 }, + onClick: function () { + openAttachmentPreview({ + url: file.url, + title: file.title, + mime: file.mime + }); + } + }, + '查看发票' + ), + React.createElement( + Button, + { + type: 'link', + size: 'small', + style: { padding: 0 }, + onClick: function () { + downloadAttachment(file); + } + }, + '下载发票' + ) + ); + } + } + ]; + }, + [openAttachmentPreview, downloadAttachment] + ); + + var invoiceDrillSummary = useMemo(function () { + if (!invoiceDrillModal.rows || !invoiceDrillModal.rows.length) { + return { invoiceAmount: 0 }; + } + return invoiceDrillModal.rows.reduce( + function (acc, r) { + acc.invoiceAmount += numOrZero(r.invoiceAmount); + return acc; + }, + { invoiceAmount: 0 } + ); + }, [invoiceDrillModal.rows]); + + var invoiceDrillTableSummary = useCallback(function () { + return React.createElement( + Table.Summary, + null, + React.createElement( + Table.Summary.Row, + null, + React.createElement(Table.Summary.Cell, { index: 0, align: 'center' }, '合计'), + React.createElement(Table.Summary.Cell, { index: 1 }), + React.createElement( + Table.Summary.Cell, + { index: 2, align: 'right' }, + fmtMoney(invoiceDrillSummary.invoiceAmount, 2) + ), + React.createElement(Table.Summary.Cell, { index: 3 }) + ) + ); + }, [invoiceDrillSummary]); + + return React.createElement( + App, + null, + React.createElement('style', null, ledgerTableStyle), + React.createElement( + 'div', + { style: layoutStyle }, + React.createElement( + 'div', + { + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: 12, + gap: 16 + } + }, + React.createElement(Breadcrumb, { + items: [{ title: '台账数据' }, { title: '氢费(采购端)汇总报表' }] + }), + React.createElement( + Button, + { + type: 'link', + style: { padding: 0, flexShrink: 0, fontSize: 14 }, + onClick: function () { + setReqDetailOpen(true); + } + }, + '查看需求说明' + ) + ), + React.createElement( + Card, + { style: filterCardStyle, bodyStyle: { paddingBottom: 4 } }, + React.createElement( + Row, + { gutter: [16, 0], align: 'bottom' }, + React.createElement( + Col, + { xs: 24, sm: 12, lg: 8 }, + React.createElement( + 'div', + { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '地区'), + React.createElement(Select, { + allowClear: true, + showSearch: true, + placeholder: '全部', + style: filterControlStyle, + value: regionDraft, + onChange: function (v) { setRegionDraft(v); }, + options: REGION_OPTIONS, + filterOption: filterOption + }) + ) + ), + React.createElement( + Col, + { xs: 24, sm: 12, lg: 8 }, + React.createElement( + 'div', + { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '加氢站全称'), + React.createElement(Select, { + allowClear: true, + showSearch: true, + placeholder: '全部', + style: filterControlStyle, + value: stationDraft, + onChange: function (v) { setStationDraft(v); }, + options: STATION_OPTIONS, + filterOption: filterOption + }) + ) + ), + React.createElement( + Col, + { xs: 24, sm: 12, lg: 8 }, + React.createElement( + 'div', + { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '结算方式'), + React.createElement(Select, { + allowClear: true, + placeholder: '全部', + style: filterControlStyle, + value: settlementDraft, + onChange: function (v) { setSettlementDraft(v); }, + options: SETTLEMENT_OPTIONS + }) + ) + ), + React.createElement( + Col, + { xs: 24, sm: 24, lg: 24, style: Object.assign({}, filterActionsColStyle, { textAlign: 'right' }) }, + React.createElement( + 'div', + { style: Object.assign({}, filterItemStyle, { marginBottom: 4 }) }, + React.createElement( + Space, + { wrap: true, style: { justifyContent: 'flex-end', width: '100%' } }, + React.createElement(Button, { onClick: handleReset }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleQuery }, '查询') + ) + ) + ) + ) + ), + React.createElement( + Card, + { style: tableCardStyle, bodyStyle: { padding: '20px 20px 24px' } }, + React.createElement( + 'div', + { style: { position: 'relative', marginBottom: 8, minHeight: 36 } }, + React.createElement( + 'div', + { + style: { + textAlign: 'center', + fontSize: 18, + fontWeight: 700, + color: 'rgba(15,23,42,0.92)', + letterSpacing: '0.02em', + padding: '0 88px' + } + }, + '氢费(采购端)汇总报表' + ), + React.createElement( + 'div', + { style: { position: 'absolute', right: 0, top: '50%', transform: 'translateY(-50%)' } }, + React.createElement(Button, { onClick: handleExport }, '导出') + ) + ), + React.createElement( + 'div', + { + style: { + textAlign: 'center', + marginBottom: 16, + fontSize: 13, + color: 'rgba(15,23,42,0.55)', + fontWeight: 500 + } + }, + filterSummaryText + ), + renderTotalsBar(), + React.createElement( + 'div', + { className: 'h2-purchase-summary-wrap' }, + React.createElement(Table, { + className: 'h2-purchase-summary-table h2-purchase-summary-table--resizable', + size: 'small', + bordered: true, + rowKey: 'key', + columns: columns, + dataSource: dataSource, + pagination: false, + rowClassName: function () { return 'h2-purchase-row'; }, + scroll: { y: 'calc(100vh - 430px)' }, + sticky: true, + tableLayout: 'fixed', + locale: { emptyText: '暂无数据,请调整筛选条件后查询' } + }) + ) + ), + React.createElement( + Modal, + { + title: detailDrillTitle, + open: detailDrillModal.open, + onCancel: closeDetailDrill, + footer: React.createElement(Button, { onClick: closeDetailDrill }, '关闭'), + width: 960, + destroyOnClose: true, + styles: { body: { maxHeight: '72vh', overflow: 'auto', paddingTop: 8 } } + }, + React.createElement(Table, { + className: 'h2-purchase-summary-table', + size: 'small', + bordered: true, + rowKey: 'key', + pagination: detailDrillModal.rows.length > 10 ? { pageSize: 10, showSizeChanger: false } : false, + columns: detailDrillColumns, + dataSource: detailDrillModal.rows, + summary: detailDrillTableSummary, + scroll: { x: 'max-content' }, + locale: { emptyText: '暂无明细数据' } + }) + ), + React.createElement( + Modal, + { + title: + '充值总额明细' + + (rechargeDrillModal.stationName ? ' · ' + rechargeDrillModal.stationName : ''), + open: rechargeDrillModal.open, + onCancel: closeRechargeDrill, + footer: React.createElement(Button, { onClick: closeRechargeDrill }, '关闭'), + width: 880, + destroyOnClose: true, + styles: { body: { maxHeight: '72vh', overflow: 'auto', paddingTop: 8 } } + }, + React.createElement(Table, { + className: 'h2-purchase-summary-table', + size: 'small', + bordered: true, + rowKey: 'key', + pagination: + rechargeDrillModal.rows.length > 10 + ? { pageSize: 10, showSizeChanger: false } + : false, + columns: rechargeDrillColumns, + dataSource: rechargeDrillModal.rows, + summary: rechargeDrillTableSummary, + scroll: { x: 'max-content' }, + locale: { emptyText: '暂无充值记录' } + }) + ), + React.createElement( + Modal, + { + title: + '余额变更明细' + + (prepaidBalanceDrillModal.stationName ? ' · ' + prepaidBalanceDrillModal.stationName : ''), + open: prepaidBalanceDrillModal.open, + onCancel: closePrepaidBalanceDrill, + footer: React.createElement(Button, { onClick: closePrepaidBalanceDrill }, '关闭'), + width: 880, + destroyOnClose: true, + styles: { body: { maxHeight: '72vh', overflow: 'auto', paddingTop: 8 } } + }, + React.createElement(Table, { + className: 'h2-purchase-summary-table', + size: 'small', + bordered: true, + rowKey: 'key', + pagination: + prepaidBalanceDrillModal.rows.length > 10 + ? { pageSize: 10, showSizeChanger: false } + : false, + columns: prepaidBalanceDrillColumns, + dataSource: prepaidBalanceDrillModal.rows, + summary: prepaidBalanceDrillTableSummary, + scroll: { x: 'max-content' }, + locale: { emptyText: '暂无余额变更记录' } + }) + ), + React.createElement( + Modal, + { + title: + '已付总金额明细' + + (paymentDrillModal.stationName ? ' · ' + paymentDrillModal.stationName : ''), + open: paymentDrillModal.open, + onCancel: closePaymentDrill, + footer: React.createElement(Button, { onClick: closePaymentDrill }, '关闭'), + width: 1020, + destroyOnClose: true, + styles: { body: { maxHeight: '72vh', overflow: 'auto', paddingTop: 8 } } + }, + React.createElement(Table, { + className: 'h2-purchase-summary-table', + size: 'small', + bordered: true, + rowKey: 'key', + pagination: + paymentDrillModal.rows.length > 10 + ? { pageSize: 10, showSizeChanger: false } + : false, + columns: paymentDrillColumns, + dataSource: paymentDrillModal.rows, + summary: paymentDrillTableSummary, + scroll: { x: 'max-content' }, + locale: { emptyText: '暂无支付明细' } + }) + ), + React.createElement( + Modal, + { + title: + '已开票明细' + (invoiceDrillModal.stationName ? ' · ' + invoiceDrillModal.stationName : ''), + open: invoiceDrillModal.open, + onCancel: closeInvoiceDrill, + footer: React.createElement(Button, { onClick: closeInvoiceDrill }, '关闭'), + width: 920, + destroyOnClose: true, + styles: { body: { maxHeight: '72vh', overflow: 'auto', paddingTop: 8 } } + }, + React.createElement(Table, { + className: 'h2-purchase-summary-table', + size: 'small', + bordered: true, + rowKey: 'key', + pagination: + invoiceDrillModal.rows.length > 10 + ? { pageSize: 10, showSizeChanger: false } + : false, + columns: invoiceDrillColumns, + dataSource: invoiceDrillModal.rows, + summary: invoiceDrillTableSummary, + scroll: { x: 'max-content' }, + locale: { emptyText: '暂无开票记录' } + }) + ), + React.createElement( + Modal, + { + title: attachmentPreview.title || '附件预览', + open: attachmentPreview.open, + onCancel: closeAttachmentPreview, + footer: React.createElement(Button, { onClick: closeAttachmentPreview }, '关闭'), + width: attachmentPreview.mime === 'pdf' ? 920 : 720, + destroyOnClose: true, + centered: true, + styles: { body: { paddingTop: 12, textAlign: 'center' } } + }, + attachmentPreview.mime === 'pdf' + ? React.createElement('iframe', { + src: attachmentPreview.url, + title: attachmentPreview.title, + style: { width: '100%', height: '70vh', border: 'none' } + }) + : React.createElement('img', { + src: attachmentPreview.url, + alt: attachmentPreview.title, + style: { maxWidth: '100%', maxHeight: '70vh', objectFit: 'contain' } + }) + ), + React.createElement( + Modal, + { + title: '氢费(采购端)汇总报表 — 需求说明', + open: reqDetailOpen, + onCancel: function () { + setReqDetailOpen(false); + }, + footer: React.createElement( + Button, + { + type: 'primary', + onClick: function () { + setReqDetailOpen(false); + } + }, + '知道了' + ), + width: 840, + centered: true, + destroyOnClose: true, + styles: { + body: { + maxHeight: '72vh', + overflow: 'auto', + paddingTop: 8, + paddingBottom: 16 + } + } + }, + renderPurchaseRequirementDocPanel() + ) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-ledger-data/pages/05-租赁业务台账.jsx b/src/prototypes/oneos-web-ledger-data/pages/05-租赁业务台账.jsx new file mode 100644 index 0000000..b028bef --- /dev/null +++ b/src/prototypes/oneos-web-ledger-data/pages/05-租赁业务台账.jsx @@ -0,0 +1,3178 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 台账数据 - 租赁业务运营明细台账(客服维护各自台账;保存→已保存;勾选提交收款审批→账单预览→财务审批闭环) +// 字段对齐《租赁业务运营明细台账模版最终.xlsx》;UI/布局对齐 web端/加氢站管理/站点信息.jsx + +var LEASE_PAGE_STYLE = [ + '.lease-ledger-page { padding: 24px 24px 80px; height: 100vh; display: flex; flex-direction: column; background: linear-gradient(165deg, #f1f5f9 0%, #f8fafc 50%, #f1f5f9 100%); overflow: hidden; box-sizing: border-box; }', + '.lease-ledger-page .lc-filter-card.ant-card { border-radius: 16px !important; border: 1px solid #e2e8f0 !important; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.03) !important; margin-bottom: 16px; }', + '.lease-ledger-page .lc-filter-card > .ant-card-head { border-bottom: 1px solid #f1f5f9 !important; min-height: auto; padding: 12px 20px !important; }', + '.lease-ledger-page .lc-filter-card > .ant-card-head .ant-card-head-title { font-size: 15px !important; font-weight: 700 !important; color: #0f172a !important; padding: 0 !important; }', + '.lease-ledger-page .lc-filter-card > .ant-card-body { padding: 16px 20px 20px !important; }', + '.lease-ledger-page .lc-filter-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px 24px; align-items: center; }', + '@media (max-width: 1280px) { .lease-ledger-page .lc-filter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }', + '@media (max-width: 640px) { .lease-ledger-page .lc-filter-grid { grid-template-columns: 1fr; } }', + '.lease-ledger-page .lc-filter-field { display: flex; align-items: center; gap: 12px; min-width: 0; min-height: 32px; }', + '.lease-ledger-page .lc-filter-field-label { flex: 0 0 88px; text-align: right; font-size: 13px; font-weight: 500; color: #475569; line-height: 32px; white-space: nowrap; }', + '.lease-ledger-page .lc-filter-field-control { flex: 1; min-width: 0; }', + '.lease-ledger-page .lc-filter-field-control .ant-input, .lease-ledger-page .lc-filter-field-control .ant-input-affix-wrapper, .lease-ledger-page .lc-filter-field-control .ant-select .ant-select-selector, .lease-ledger-page .lc-filter-field-control .ant-input-number, .lease-ledger-page .lc-filter-field-control .ant-picker { width: 100%; height: 32px !important; min-height: 32px !important; border-radius: 8px !important; box-sizing: border-box; }', + '.lease-ledger-page .lc-filter-field-control .ant-input-number .ant-input-number-input { height: 30px; }', + '.lease-ledger-page .lc-filter-field-control .ant-picker-range { height: 32px !important; }', + '.lease-ledger-page .lc-filter-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; padding-top: 16px; border-top: 1px solid #f1f5f9; }', + '.lease-ledger-page .lc-alert-stats-row { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; }', + '@media (max-width: 1400px) { .lease-ledger-page .lc-alert-stats-row { grid-template-columns: repeat(3, minmax(0, 1fr)); } }', + '@media (max-width: 640px) { .lease-ledger-page .lc-alert-stats-row { grid-template-columns: 1fr; } }', + '.lease-ledger-page .lc-alert-card { display: flex; align-items: center; gap: 14px; padding: 16px 30px 16px 16px; border-radius: 12px; border: 1px solid #e2e8f0; background: #fff; position: relative; overflow: hidden; min-width: 0; min-height: 72px; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.lease-ledger-page .lc-alert-card-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }', + '.lease-ledger-page .lc-alert-card-icon { flex-shrink: 0; width: 48px; height: 48px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: linear-gradient(145deg, #0d7a63 0%, #065f4a 100%); color: #fff; box-shadow: 0 4px 12px rgba(6, 95, 74, 0.28); }', + '.lease-ledger-page .lc-alert-card-val { font-size: 24px; font-weight: 800; line-height: 1.1; color: #0f172a; font-variant-numeric: tabular-nums; }', + '.lease-ledger-page .lc-alert-card-title { font-size: 13px; font-weight: 500; color: #64748b; line-height: 1.3; }', + '.lease-ledger-page .lc-alert-card-tip-anchor { position: absolute; top: 8px; right: 8px; z-index: 2; line-height: 0; }', + '.lease-ledger-page .lc-alert-card-tip { width: 18px; height: 18px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; color: #94a3b8; background: rgba(255,255,255,0.92); border: 1px solid #e2e8f0; cursor: help; line-height: 0; }', + '.lease-ledger-page .lc-stat-val--profit { color: #047857 !important; }', + '.lease-ledger-page .lc-stat-val--loss { color: #b91c1c !important; }', + '.lease-ledger-page .lc-table-section { margin-bottom: 0; flex: 1; display: flex; flex-direction: column; min-height: 0; }', + '.lease-ledger-page .lc-table-toolbar { display: flex; justify-content: sp 1·ace-between; align-items: center; flex-wrap: wrap; gap: 10px 16px; margin-bottom: 8px; min-height: 32px; }', + '.lease-ledger-page .lc-table-legend-outer { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; padding: 6px 4px; font-size: 12px; color: #64748b; }', + '.lease-ledger-page .lc-table-legend-label { font-weight: 600; color: #64748b; }', + '.lease-ledger-page .lc-table-legend-item { display: inline-flex; align-items: center; gap: 6px; }', + '.lease-ledger-page .lc-table-legend-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }', + '.lease-ledger-page .lc-table-toolbar-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-left: auto; }', + '.lease-ledger-page .lc-table-card { background: #fff; border-radius: 16px; border: 1px solid #e2e8f0; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.03); flex: 1; min-height: 0; padding: 12px 16px 16px; }', + '.lease-ledger-page .lc-action-btn { font-weight: 600 !important; color: #10b981 !important; padding: 0 !important; }', + '.lease-ledger-page .lc-action-btn-danger { color: #ef4444 !important; }', + '.lease-ledger-page .lease-cell-readonly { color: #64748b; background: #f8fafc; padding: 4px 6px; text-align: center; border-radius: 4px; }', + '.lease-ledger-page .lease-cell-calc { font-weight: 600; color: #0f766e; }', + '.lease-ledger-page .lease-add-row-btn { margin-top: 12px; border-radius: 8px !important; border-style: dashed !important; border-color: #cbd5e1 !important; color: #475569 !important; font-weight: 600 !important; }', + '.lease-ledger-page .lease-add-row-btn:hover { border-color: #10b981 !important; color: #059669 !important; }', + '.lease-ledger-page .h2-import-template-bar { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding: 14px 16px; margin-bottom: 14px; border-radius: 12px; background: linear-gradient(135deg, #ecfdf5 0%, #f8fafc 100%); border: 1px solid #bbf7d0; }', + '.lease-ledger-page .h2-import-template-bar-text { font-size: 13px; color: #475569; line-height: 1.55; flex: 1; min-width: 0; }', + '.lease-ledger-page .h2-req-doc-panel h2:first-child { margin-top: 0; }', + '.lc-bill-preview-modal .ant-modal-content { border-radius: 16px !important; overflow: hidden; box-shadow: 0 24px 48px -12px rgba(15, 23, 42, 0.18) !important; }', + '.lc-bill-preview-modal .ant-modal-header { padding: 18px 24px 14px !important; border-bottom: 1px solid #f1f5f9 !important; margin-bottom: 0 !important; }', + '.lc-bill-preview-modal .ant-modal-title { font-size: 17px !important; font-weight: 700 !important; color: #0f172a !important; }', + '.lc-bill-preview-modal .ant-modal-body { padding: 16px 24px 20px !important; background: #f8fafc; }', + '.lc-bill-preview-modal .ant-modal-footer { padding: 12px 24px 18px !important; border-top: 1px solid #f1f5f9 !important; background: #fff; }', + '.lc-bill-preview-panel { display: flex; flex-direction: column; gap: 14px; }', + '.lc-bill-preview-hero { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 14px 16px; background: linear-gradient(135deg, #ecfdf5 0%, #f0fdf4 50%, #f8fafc 100%); border: 1px solid #bbf7d0; border-radius: 12px; }', + '.lc-bill-preview-hero__main { flex: 1; min-width: 0; }', + '.lc-bill-preview-hero__customer { font-size: 16px; font-weight: 700; color: #0f172a; line-height: 1.35; margin-bottom: 4px; }', + '.lc-bill-preview-hero__project { font-size: 13px; color: #475569; line-height: 1.45; }', + '.lc-bill-preview-hero__meta { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; white-space: nowrap; text-align: right; }', + '.lc-bill-preview-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }', + '@media (max-width: 720px) { .lc-bill-preview-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } }', + '.lc-bill-preview-stat { display: flex; flex-direction: column; justify-content: center; min-height: 72px; padding: 12px 14px; border-radius: 12px; border: 1px solid #e2e8f0; background: #fff; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06); min-width: 0; box-sizing: border-box; }', + '.lc-bill-preview-stat--amount { border-left: 4px solid #10b981; background: linear-gradient(180deg, #fff 0%, #f0fdf4 100%); }', + '.lc-bill-preview-stat--invoice { border-left: 4px solid #8b5cf6; }', + '.lc-bill-preview-stat--arrival { border-left: 4px solid #3b82f6; }', + '.lc-bill-preview-stat--company { border-left: 4px solid #f59e0b; grid-column: span 2; }', + '.lc-bill-preview-stat__label { font-size: 12px; font-weight: 600; color: #64748b; margin-bottom: 6px; line-height: 1.2; }', + '.lc-bill-preview-stat__value { font-size: 15px; font-weight: 700; line-height: 1.35; color: #0f172a; word-break: break-word; }', + '.lc-bill-preview-stat__value--money { font-size: 18px; font-weight: 800; color: #059669; font-variant-numeric: tabular-nums; }', + '.lc-bill-preview-attach-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }', + '@media (max-width: 640px) { .lc-bill-preview-attach-row { grid-template-columns: 1fr; } .lc-bill-preview-stat--company { grid-column: span 1; } }', + '.lc-bill-preview-attach { padding: 12px 14px; border-radius: 12px; border: 1px dashed #e2e8f0; background: #fff; min-height: 56px; box-sizing: border-box; }', + '.lc-bill-preview-attach__label { font-size: 12px; font-weight: 600; color: #64748b; margin-bottom: 6px; }', + '.lc-bill-preview-attach__value { font-size: 13px; color: #334155; }', + '.lc-bill-preview-attach-link { color: #059669; cursor: pointer; font-size: 12px; font-weight: 600; margin-right: 10px; text-decoration: none; }', + '.lc-bill-preview-attach-link:hover { color: #047857; text-decoration: underline; }', + '.lc-bill-preview-table-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.05); }', + '.lc-bill-preview-table-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 12px 16px; border-bottom: 1px solid #f1f5f9; background: #fafafa; }', + '.lc-bill-preview-table-head__title { font-size: 14px; font-weight: 700; color: #0f172a; }', + '.lc-bill-preview-table-head__count { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; }', + '.lc-bill-preview-table-wrap .ant-table { border-radius: 0 !important; }', + '.lc-bill-preview-table-wrap .ant-table-thead>tr>th { background: #f8fafc !important; font-size: 12px !important; font-weight: 600 !important; color: #475569 !important; }', + '.lc-bill-preview-actions { display: flex; justify-content: flex-end; gap: 10px; padding-top: 4px; }' +].join(''); + +/** 宽表内联编辑样式(对齐车辆维修明细 maint-ledger-table,避免破坏 scroll 表头) */ +var LEASE_LEDGER_TABLE_STYLE = + '.lease-ledger-table-wrap{border-radius:12px;overflow:hidden;box-shadow:0 4px 24px -6px rgba(15,23,42,0.05),0 0 0 1px rgba(226,232,240,0.9)}' + + '.lease-ledger-table .ant-table-thead>tr>th,.lease-ledger-table .ant-table-thead .ant-table-cell{white-space:nowrap;color:#475569!important;font-weight:700!important;font-size:13px!important;' + + 'background:#f8fafc!important;border-bottom:1px solid #e2e8f0!important;border-inline-end:1px solid #e2e8f0!important;padding:0 8px!important;height:40px!important;text-align:center!important;vertical-align:middle!important}' + + '.lease-ledger-table .ant-table-header .ant-table-thead>tr>th,.lease-ledger-table .ant-table-header .ant-table-thead .ant-table-cell{height:40px!important;line-height:1.35!important}' + + '.lease-ledger-table .ant-table-tbody>tr:not(.ant-table-measure-row){height:36px!important}' + + '.lease-ledger-table .ant-table-tbody>tr:not(.ant-table-measure-row)>td{height:36px!important;max-height:36px!important;padding:3px 6px!important;vertical-align:middle!important;font-size:12px!important;line-height:1.4!important;box-sizing:border-box!important;overflow:hidden}' + + '.lease-ledger-table .ant-table-tbody>tr.lease-row-tier-0>td{background:#ecfdf5!important}' + + '.lease-ledger-table .ant-table-tbody>tr.lease-row-tier-1>td{background:#f5f3ff!important}' + + '.lease-ledger-table .ant-table-tbody>tr.lease-row-tier-2>td{background:#ede9fe!important}' + + '.lease-ledger-table .ant-table-tbody>tr.lease-row-tier-3>td{background:#fff!important}' + + '.lease-ledger-table .ant-table-tbody>tr.lease-row-tier-4>td{background:#f1f5f9!important}' + + '.lease-ledger-table .ant-table-tbody>tr:not(.ant-table-measure-row):hover>td{background:#f0f9ff!important}' + + '.lease-ledger-table .ant-table-tbody>tr.ant-table-measure-row,.lease-ledger-table .ant-table-tbody>tr.ant-table-measure-row>td{height:0!important;max-height:0!important;padding:0!important;border:none!important;line-height:0!important;font-size:0!important;overflow:hidden!important;visibility:hidden!important}' + + '.lease-ledger-table .ant-table-cell-fix-left,.lease-ledger-table .ant-table-cell-fix-right{z-index:2!important}' + + '.lease-ledger-table .ant-table-container .ant-table-header{position:relative;z-index:3;background:#f8fafc;min-height:40px!important}' + + '.lease-ledger-table .ant-table-container .ant-table-header .ant-table-thead>tr>th,.lease-ledger-table .ant-table-container .ant-table-header .ant-table-thead .ant-table-cell{opacity:1!important;visibility:visible!important}' + + '.lease-ledger-table .ant-table-header>table,.lease-ledger-table .ant-table-body>table,.lease-ledger-table .ant-table-content>table{min-width:max-content!important}' + + '.lease-ledger-table .ant-table-cell{white-space:nowrap}' + + '.lease-ledger-table .lease-col-header-wrap{display:inline-flex;align-items:center;justify-content:center;gap:3px;max-width:100%;vertical-align:middle}' + + '.lease-ledger-table .lease-col-header-label{line-height:1.35}' + + '.lease-ledger-table .lease-col-header-tip{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;color:#94a3b8;cursor:help;line-height:0}' + + '.lease-ledger-table .lease-cell-plain{display:block;height:30px;line-height:30px;font-size:12px!important;color:#334155;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;box-sizing:border-box}' + + '.lease-ledger-table .lease-cell-readonly{font-size:12px!important;line-height:20px!important;min-height:28px!important;max-height:28px!important;padding:3px 6px!important;box-sizing:border-box!important;display:flex;align-items:center;justify-content:center}' + + '.lease-ledger-table .lease-cell-readonly.lease-cell-calc{justify-content:flex-end}' + + '.lease-ledger-table .lease-cell-calc{font-size:12px!important}' + + '.lease-ledger-table .ant-tag{margin:0!important;font-size:12px!important;line-height:20px!important;padding:0 6px!important}' + + '.lease-ledger-table .lease-cell-input,.lease-ledger-table .lease-cell-input.ant-input-affix-wrapper,.lease-ledger-table .lease-cell-input-number,.lease-ledger-table .lease-cell-input-number .ant-input-number-input,.lease-ledger-table .lease-cell-date,.lease-ledger-table .lease-cell-date .ant-picker-input>input{font-size:12px!important}' + + '.lease-ledger-table .lease-cell-input,.lease-ledger-table .lease-cell-input.ant-input-affix-wrapper{height:28px!important;min-height:28px!important;padding:2px 8px!important;line-height:24px!important;border-radius:6px!important}' + + '.lease-ledger-table .lease-cell-input-number{width:100%!important;height:28px!important;min-height:28px!important;border-radius:6px!important}' + + '.lease-ledger-table .lease-cell-input-number .ant-input-number-input{height:26px!important;padding:0 8px!important}' + + '.lease-ledger-table .lease-cell-date{width:100%!important;height:28px!important;min-height:28px!important;border-radius:6px!important;padding:2px 8px 2px!important}' + + '.lease-ledger-table .lease-cell-date .ant-picker-input>input{height:24px!important}' + + '.lease-ledger-table .lease-cell-select.ant-select-single{height:28px!important;font-size:12px!important}' + + '.lease-ledger-table .lease-cell-select.ant-select-single .ant-select-selector{height:28px!important;min-height:28px!important;padding:0 8px!important;font-size:12px!important;border-radius:6px!important}' + + '.lease-ledger-table .lease-cell-select .ant-select-selection-item,.lease-ledger-table .lease-cell-select .ant-select-selection-placeholder{line-height:26px!important;font-size:12px!important}' + + '.lease-ledger-table .lease-date-warn-cell{display:inline-flex;align-items:center;justify-content:center;gap:4px;width:100%;max-width:100%;height:30px;line-height:30px}' + + '.lease-plate-select-dropdown{min-width:112px!important}' + + '.lease-plate-select-dropdown .ant-select-item,.lease-plate-select-dropdown .ant-select-item-option-content{font-size:12px!important;white-space:nowrap!important}' + + '.lease-cell-select-dropdown .ant-select-item,.lease-cell-select-dropdown .ant-select-item-option-content{font-size:12px!important;white-space:normal!important;word-break:break-all;line-height:1.4!important}'; + +var LEASE_PRIMARY_BTN_STYLE = { + borderRadius: 8, + fontWeight: 600, + background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)', + border: 'none' +}; + +var LEASE_OUTLINE_BTN_STYLE = { + borderRadius: 8, + fontWeight: 600, + borderColor: '#10b981', + color: '#059669' +}; + +var LEASE_REQ_BTN_STYLE = { + borderRadius: 8, + border: '1px solid #cbd5e1', + color: '#475569', + fontWeight: 600 +}; + +var LEASE_KPI_CARDS = [ + { key: 'count', type: 'total', title: '台账条数', desc: '当前筛选条件下的台账记录条数', format: 'count' }, + { key: 'receivableTotal', type: 'normal', title: '应收总计', desc: '当前筛选范围内应收合计之和', format: 'money' }, + { key: 'receivedAmount', type: 'normal', title: '实收总计', desc: '当前筛选范围内实收金额之和', format: 'money' }, + { key: 'unreceived', type: 'warning', title: '未收总计', desc: '当前筛选范围内未收金额之和', format: 'money' }, + { key: 'totalCost', type: 'unuploaded', title: '成本总计', desc: '当前筛选范围内总成本之和', format: 'money' }, + { key: 'profitLoss', type: 'total', title: '盈亏总计', desc: '当前筛选范围内盈亏之和', format: 'money' } +]; + +function leaseSvgIcon(paths, size) { + var s = size || 18; + return React.createElement('svg', { + width: s, height: s, viewBox: '0 0 24 24', fill: 'none', + stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', + 'aria-hidden': true + }, paths.map(function (p, i) { + if (p.tag === 'circle') return React.createElement('circle', { key: i, cx: p.cx, cy: p.cy, r: p.r }); + if (p.tag === 'line') return React.createElement('line', { key: i, x1: p.x1, y1: p.y1, x2: p.x2, y2: p.y2 }); + if (p.tag === 'rect') return React.createElement('rect', { key: i, x: p.x, y: p.y, width: p.width, height: p.height, rx: p.rx }); + return React.createElement('path', { key: i, d: p.d }); + })); +} + +var LEASE_ICONS = { + upload: leaseSvgIcon([{ d: 'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4' }, { d: 'M17 8l-5-5-5 5' }, { tag: 'line', x1: 12, y1: 3, x2: 12, y2: 15 }], 14), + plus: leaseSvgIcon([{ tag: 'line', x1: 12, y1: 5, x2: 12, y2: 19 }, { tag: 'line', x1: 5, y1: 12, x2: 19, y2: 12 }], 14), + doc: leaseSvgIcon([{ d: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z' }, { d: 'M14 2v6h6' }, { tag: 'line', x1: 16, y1: 13, x2: 8, y2: 13 }, { tag: 'line', x1: 16, y1: 17, x2: 8, y2: 17 }], 14), + all: leaseSvgIcon([{ d: 'M4 20h16' }, { d: 'M7 20v-9h4v9' }, { d: 'M14 20V8l5-3v15' }], 22), + edit: leaseSvgIcon([{ d: 'M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7' }, { d: 'M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z' }], 22), + saved: leaseSvgIcon([{ d: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z' }, { d: 'M14 2v6h6' }, { tag: 'circle', cx: 17.5, cy: 17.5, r: 3.5 }, { d: 'M16 17.5l1 1 2-2' }], 22), + lock: leaseSvgIcon([{ tag: 'rect', x: 3, y: 11, width: 18, height: 11, rx: 2 }, { d: 'M7 11V7a5 5 0 0 1 10 0v4' }], 22), + warn: leaseSvgIcon([ + { d: 'M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z' }, + { tag: 'line', x1: 12, y1: 9, x2: 12, y2: 13 }, + { tag: 'line', x1: 12, y1: 17, x2: 12.01, y2: 17 } + ], 14) +}; + +function leaseKpiIcon(key) { + if (key === 'count') return LEASE_ICONS.all; + if (key === 'profitLoss') return leaseSvgIcon([ + { tag: 'line', x1: 12, y1: 1, x2: 12, y2: 23 }, + { tag: 'path', d: 'M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6' } + ], 22); + if (key === 'totalCost') return leaseSvgIcon([{ d: 'M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6' }], 22); + return leaseSvgIcon([{ d: 'M4 10h16M4 14h16M6 6h12M6 18h12' }], 22); +} + +/** 宽表列头字段说明(悬停 ? 图标查看;含自动计算字段公式) */ +var LEASE_COLUMN_TIPS = { + seq: '当前列表展示顺序号,自动生成。', + status: '数据状态:待保存、已保存、已生成账单、审批中、审批通过。', + maintainerName: '台账维护人,新增行时取当前登录客服姓名。', + year: '账单所属年份,必填。', + month: '账单所属月份(1–12),必填;与年份共同决定当月在租天数计算区间。', + dept: '业务部门,必填;选择车牌后从车辆档案自动带出。', + salesman: '业务员,必填;选择车牌后从车辆档案自动带出。', + billDate: '账单日期,必填;按「车牌 + 账单日期」匹配车辆租赁合同。不在任何合同有效期内时显示警告。', + plateNo: '车牌号码,必填;选项来自车辆管理。选择后自动带出车型、客户、部门、业务员、标准成本、合同日期等。', + vehicleModel: '车型(品牌+型号),选择车牌后自动带出,只读展示。', + customerName: '客户名称,选择车牌后自动带出,只读展示。', + valueAddedService: '增值费用类型,手选(如包氢租赁、无忧包等);部分车辆可由车牌带出默认值。', + exemptionPolicy: '享免政策,手选(如里程优惠政策等)。', + pickupDate: '提车日期,手填;与系统「交车管理」交车日期不一致时显示警告图标。', + contractStart: '合同生效日期;选车牌或修改账单日后按匹配合同自动带出;与系统不一致时警告。', + contractEnd: '合同到期日期;选车牌或修改账单日后按匹配合同自动带出;与系统不一致时警告。', + rentStartDate: '【自动计算】起始日期。提车日在账单月内 → 显示提车日期;提车日在账单月外 → 显示当月完整天数(如 10 月 31、11 月 30)。在租天数仍按退车日/月末截断后计算平均天数。', + returnDate: '退车日期,手填;未填视为租至月末。与系统还车日期不一致时警告;提车日晚于账单月末则本月在租天数为 0。', + avgDays: '【自动计算】平均天数 = 自起始日期起在租天数 ÷ 当月天数,保留 2 位小数。实际成本 = 标准成本 × 平均天数。', + deposit: '押金,手填,支持 2 位小数;保存/提交时与系统租赁合同押金比对,不一致显示警告。', + contractRent: '合同标的租金,手填,支持 2 位小数;保存/提交时与系统合同车辆租金比对,不一致显示警告。', + receivableTotal: '【自动计算】应收合计 = 应收租金 + 保险上浮 + 运维费(收) + 其他收入 + 里程减免 + 其他减免。', + receivableRent: '应收租金,手填,支持 2 位小数。', + monthlyIncome: '【自动计算】月度收入 = 月度租金 + 维保包干 + 保险上浮 + 运维费(收) + 其他收入 + 里程减免 + 其他减免。', + monthlyRent: '月度租金,手填;选车牌或改账单日时,可从匹配合同自动带出。', + maintPackageIncome: '维保包干收入,手填,支持 2 位小数。', + insuranceSurcharge: '保险上浮费(收入侧),手填,支持 2 位小数。', + opsFeeIncome: '运维费(收入侧),手填,支持 2 位小数。', + otherIncome: '其他收入,手填,支持 2 位小数。', + mileageReduction: '里程减免,手填,可为负数,支持 2 位小数。', + otherReduction: '其他减免,手填,可为负数,支持 2 位小数。', + receivedAmount: '实收金额,手填,支持 2 位小数。', + unreceived: '【自动计算】未收 = 应收合计 − 实收金额。', + invoiceDate: '开票日期,手填。', + paymentDate: '付款日期,手填。', + paymentMethod: '付款方式,手选(月度后付、季度后付、预付等)。', + vehicleStdCost: '车辆标准成本(单车月标准),手填;选择车牌可从车辆成本档案自动带出。', + vehicleActualCost: '【自动计算】实际成本 = 标准成本 × 平均天数。', + insuranceCost: '保险费(成本侧),手填,支持 2 位小数。', + h2Cost: '氢费(成本侧),手填,支持 2 位小数。', + opsCost: '运维费,手填,支持 2 位小数(成本项)。', + intermediaryFee: '居间费,手填,支持 2 位小数。', + otherCost: '其他成本,手填,支持 2 位小数。', + totalCost: '【自动计算】总成本 = 实际成本 + 保险费 + 氢费 + 运维费 + 居间费 + 其他成本。', + profitLoss: '【自动计算】盈亏 = 月度收入 − 总成本;正数为盈利,负数为亏损。', + assetOwner: '资产归属公司;选择车牌后可自动带出,亦可手填修改。', + signCompany: '签约公司;选择车牌后可自动带出,亦可手填修改。', + remark: '备注,手填。', + action: '行操作:编辑、删除等,权限随数据状态与角色变化。' +}; + +function leaseColTitle(label, tip) { + if (!tip) return label; + var Tooltip = window.antd && window.antd.Tooltip; + if (!Tooltip) return label; + return React.createElement('span', { className: 'lease-col-header-wrap' }, + React.createElement('span', { className: 'lease-col-header-label' }, label), + React.createElement(Tooltip, { + title: tip, + placement: 'top', + overlayStyle: { maxWidth: 380 } + }, + React.createElement('span', { + className: 'lease-col-header-tip', + role: 'img', + 'aria-label': label + '字段说明', + onClick: function (e) { e.stopPropagation(); } + }, + leaseSvgIcon([ + { tag: 'circle', cx: 12, cy: 12, r: 10 }, + { tag: 'line', x1: 12, y1: 16, x2: 12, y2: 12 }, + { tag: 'line', x1: 12, y1: 8, x2: 12.01, y2: 8 } + ], 11) + ) + ) + ); +} + +var LEASE_REQUIREMENT_DOC = + '## 租赁业务运营明细台账 · 需求说明\n\n' + + '### 角色与权限\n' + + '- **客服人员**:仅可查看、维护本人台账数据。\n' + + '- **客服主管**:可查看全部客服人员的台账;对审批通过记录可继续编辑、删除。\n\n' + + '### 数据状态\n' + + '- **待保存**:新增一行或导入后,尚未点击「保存」。\n' + + '- **已保存**:点击「保存」后可勾选并提交收款审批。\n' + + '- **已生成账单**:提交收款审批后按客户+账单月+合同拆分账单,进入账单预览确认。\n' + + '- **审批中**:发起收款审批后锁定;多选框禁用。\n' + + '- **审批通过**:财务审批闭环后锁定;仅客服主管可维护。\n\n' + + '### 维护方式\n' + + '- **新增一行**:在表格底部追加空白可编辑行。\n' + + '- **导入**:下载 CSV 模板填写后上传,生成待保存记录。\n' + + '- **保存**:将本人待保存记录更新为已保存。\n' + + '- **提交收款审批**:勾选已保存记录,按项目自动拆分账单并进入预览。\n' + + '- **账单管理**:查看账单列表,支持取消账单、撤回、财务审批模拟。\n\n' + + '### 筛选\n' + + '支持按年份、月份、业务部门、业务员、车牌、客户名称、明细状态、账单日期区间筛选;统计卡片可快捷按状态筛选。\n\n' + + '### 联调说明\n' + + '原型阶段使用模拟登录用户;正式环境由系统登录态与角色权限接口驱动。\n' + + '- **车牌号码**:选项取自系统「车辆管理」;选择后自动带出业务部门、业务员、车型、客户名称及租赁合同/标准成本等关联字段。\n' + + '- **提车日期**:与系统「交车管理」完成交车时间(日期)校验;不一致时显示警告图标,悬浮可查看系统交车日期。\n' + + '- **退车日期**:与系统「还车管理」实际还车时间(日期)校验;不一致时显示警告图标,悬浮可查看系统还车时间。\n' + + '- **合同生效/到期**:根据车牌 + 账单日期匹配「车辆租赁合同」;选择车牌或修改账单日后自动带出对应合同起止日期,与系统不一致时显示警告图标。\n' + + '- **起始日期**:由提车日期 + 账单年月自动计算——提车日在账单月内显示提车日期;提车日不在账单月内显示当月完整天数(如 10 月 31)。\n' + + '- **平均天数**:自起始日期起在租天数 ÷ 当月天数,保留 2 位小数、千分位四舍五入(实际成本 = 标准成本 × 平均天数)。\n' + + '- **押金**:手填,支持 2 位小数;保存时与系统租赁合同押金比对,不一致时显示警告图标。\n' + + '- **合同标的租金**:手填,支持 2 位小数;保存时与系统合同车辆租金比对,不一致时显示警告图标。'; + +function renderLeaseRequirementPanel() { + var lines = LEASE_REQUIREMENT_DOC.split('\n'); + return React.createElement('div', { className: 'h2-req-doc-panel', style: { padding: '4px 4px 16px', fontSize: 13, lineHeight: 1.65, color: '#475569' } }, + lines.map(function (line, idx) { + if (line.indexOf('## ') === 0) { + return React.createElement('h2', { key: idx, style: { fontSize: 17, fontWeight: 700, color: '#0f172a', margin: '16px 0 10px' } }, line.replace(/^##\s*/, '')); + } + if (line.indexOf('### ') === 0) { + return React.createElement('h3', { key: idx, style: { fontSize: 14, fontWeight: 700, color: '#334155', margin: '12px 0 8px' } }, line.replace(/^###\s*/, '')); + } + if (!line.trim()) return React.createElement('div', { key: idx, style: { height: 8 } }); + var parts = line.split(/(\*\*[^*]+\*\*)/g); + return React.createElement('p', { key: idx, style: { margin: '0 0 6px' } }, + parts.map(function (part, pi) { + if (part.indexOf('**') === 0 && part.lastIndexOf('**') === part.length - 2) { + return React.createElement('strong', { key: pi, style: { color: '#0f172a' } }, part.slice(2, -2)); + } + return part; + }) + ); + }) + ); +} + +/** 车牌主数据(联调对接「车辆管理」+「车辆租赁合同」) */ +var LEASE_PLATE_MASTER = [ + { plateNo: '浙F07033F', brand: '现代', model: '4.5吨货车', customerName: '宁波港集装箱运输有限公司嘉兴分公司', dept: '业务一部', salesman: '陈高伟', assetOwner: '浙江氢能产业发展有限公司', signCompany: '浙江羚牛氢能科技有限公司', vehicleStdCost: 7100, pickupDate: '2025-08-27', contractStart: '2025-09-01', contractEnd: '2025-11-30', contractRent: 6696, monthlyRent: 6696, valueAddedService: '包氢租赁' }, + { plateNo: '浙F06909F', brand: '现代', model: '帕力安牌4.5吨冷链车', customerName: '宁波港集装箱运输有限公司嘉兴分公司', dept: '业务一部', salesman: '陈高伟', assetOwner: '浙江氢能产业发展有限公司', signCompany: '浙江羚牛氢能科技有限公司', vehicleStdCost: 7100, pickupDate: '2025-08-27', contractStart: '2025-09-01', contractEnd: '2025-11-30', contractRent: 6696, monthlyRent: 6696 }, + { plateNo: '浙F39003F', brand: '现代', model: '帕力安牌18吨双飞翼货车', customerName: '宁波港集装箱运输有限公司嘉兴分公司', dept: '业务一部', salesman: '陈高伟', assetOwner: '浙江氢能产业发展有限公司', signCompany: '浙江羚牛氢能科技有限公司', vehicleStdCost: 7100, pickupDate: '2025-08-27', contractStart: '2025-09-01', contractEnd: '2025-11-30', contractRent: 6696, monthlyRent: 6696 }, + { plateNo: '沪A33198F', brand: '楚风', model: '18吨厢式货车', customerName: '宁波港集装箱运输有限公司嘉兴分公司', dept: '业务一部', salesman: '陈高伟', assetOwner: '上海羚牛氢运物联网科技有限公司', signCompany: '浙江羚牛氢能科技有限公司', vehicleStdCost: 7100, pickupDate: '2025-08-27', contractStart: '2025-09-01', contractEnd: '2025-11-30', contractRent: 6696, monthlyRent: 6696 }, + { plateNo: '浙F09038F', brand: '苏龙', model: '海格牌18吨双飞翼货车', customerName: '宁波港集装箱运输有限公司嘉兴分公司', dept: '业务一部', salesman: '陈高伟', assetOwner: '浙江氢能产业发展有限公司', signCompany: '浙江羚牛氢能科技有限公司', vehicleStdCost: 7100, pickupDate: '2025-08-27', contractStart: '2025-09-01', contractEnd: '2025-11-30', contractRent: 6696, monthlyRent: 6696 }, + { plateNo: '浙F00661F', brand: '跃进', model: '4.5吨冷链车', customerName: '嘉兴市乍浦港口经营有限公司', dept: '业务一部', salesman: '陈高伟', assetOwner: '嘉兴氢能产业发展股份有限公司', signCompany: '浙江羚牛氢能科技有限公司', vehicleStdCost: 7100, pickupDate: '2022-12-01', contractStart: '2022-12-01', contractEnd: '2025-11-30', contractRent: 18700, monthlyRent: 18700 }, + { plateNo: '浙F00688F', brand: '飞驰', model: '49吨牵引车头', customerName: '嘉兴市乍浦港口经营有限公司', dept: '业务一部', salesman: '陈高伟', assetOwner: '嘉兴氢能产业发展股份有限公司', signCompany: '浙江羚牛氢能科技有限公司', vehicleStdCost: 7100, pickupDate: '2022-12-01', contractStart: '2022-12-01', contractEnd: '2025-11-30', contractRent: 18700, monthlyRent: 18700 }, + { plateNo: '浙F01115F', brand: '飞驰', model: '49吨牵引车头', customerName: '嘉兴市乍浦港口经营有限公司', dept: '业务一部', salesman: '陈高伟', assetOwner: '嘉兴氢能产业发展股份有限公司', signCompany: '浙江羚牛氢能科技有限公司', vehicleStdCost: 7100, pickupDate: '2022-12-01', contractStart: '2022-12-01', contractEnd: '2025-11-30', contractRent: 18700, monthlyRent: 18700 }, + { plateNo: '浙F03218F', brand: '飞驰', model: '49吨牵引车头', customerName: '上海馨想事成物流有限公司', dept: '业务二部', salesman: '董剑煜', assetOwner: '浙江羚牛氢能科技有限公司', signCompany: '浙江羚牛氢能科技有限公司', vehicleStdCost: 8500, pickupDate: '2026-01-10', contractStart: '2026-01-15', contractEnd: '2027-01-14', contractRent: 12800, monthlyRent: 12000, deposit: 10000 }, + { plateNo: '粤AGP5621', brand: '现代', model: '帕力安牌4.5吨冷链车', customerName: '嘉兴某某物流有限公司', dept: '业务二部', salesman: '赵连飞', assetOwner: '浙江羚牛氢能科技有限公司', signCompany: '浙江羚牛氢能科技有限公司', vehicleStdCost: 7200, pickupDate: '2026-02-01', contractStart: '2026-02-01', contractEnd: '2027-01-31', contractRent: 9800, monthlyRent: 9500, deposit: 8000 }, + { plateNo: '京A29256F', brand: '飞驰', model: '49吨牵引车头', customerName: '北京海龙运输有限公司', dept: '业务一部', salesman: '刘念念', assetOwner: '北京羚牛氢运科技有限公司', signCompany: '浙江羚牛氢能科技有限公司', vehicleStdCost: 12000, pickupDate: '2025-08-01', contractStart: '2025-08-15', contractEnd: '2026-08-14', contractRent: 22000, monthlyRent: 21000, deposit: 15000 }, + { plateNo: '浙F01505F', brand: '飞驰', model: '49吨牵引车头', customerName: '嘉兴市乍浦港口经营有限公司', dept: '业务一部', salesman: '尚建华', assetOwner: '嘉兴氢能产业发展股份有限公司', signCompany: '浙江羚牛氢能科技有限公司', vehicleStdCost: 7100, pickupDate: '2022-12-01', contractStart: '2022-12-01', contractEnd: '2025-11-30', contractRent: 18700, monthlyRent: 18700 }, + { plateNo: '沪A12345', brand: '飞驰', model: '49T', customerName: '嘉兴某某物流有限公司', dept: '业务二部', salesman: '李婷婷', assetOwner: '浙江羚牛氢能科技有限公司', signCompany: '浙江羚牛氢能科技有限公司', vehicleStdCost: 7100, pickupDate: '2025-06-01', systemReturnAt: '2025-10-15 16:20' }, + { plateNo: '粤B67890', brand: '现代', model: '帕力安牌4.5吨冷链车', customerName: '嘉兴某某物流有限公司', dept: '业务二部', salesman: '李婷婷', assetOwner: '浙江羚牛氢能科技有限公司', signCompany: '浙江羚牛氢能科技有限公司', vehicleStdCost: 7200 }, + { plateNo: '苏E88888F', brand: '楚风', model: '18吨厢式货车', customerName: '杭州某某租赁有限公司', dept: '业务三部', salesman: '王磊', assetOwner: '浙江羚牛氢能科技有限公司', signCompany: '浙江羚牛氢能科技有限公司', vehicleStdCost: 7100 }, + { plateNo: '京C12345', brand: '飞驰', model: '49吨牵引车头', customerName: '北京海龙运输有限公司', dept: '业务一部', salesman: '刘念念', assetOwner: '北京羚牛氢运科技有限公司', signCompany: '浙江羚牛氢能科技有限公司', vehicleStdCost: 12000 } +]; + +LEASE_PLATE_MASTER.forEach(function (item, idx) { + if (!item.systemDeliveryAt && item.pickupDate) { + var times = ['09:30', '10:15', '14:00', '11:20', '15:45']; + item.systemDeliveryAt = item.pickupDate + ' ' + times[idx % times.length]; + } +}); + +var LEASE_PLATE_CONTEXT_MAP = (function () { + var map = {}; + LEASE_PLATE_MASTER.forEach(function (item) { map[item.plateNo] = item; }); + return map; +})(); + +/** 车辆租赁合同列表(同一车牌可有多份合同;按账单日落在 [contractStart, contractEnd] 内匹配) */ +var LEASE_VEHICLE_CONTRACTS = []; +LEASE_PLATE_MASTER.forEach(function (item) { + if (!item.contractStart || !item.contractEnd) return; + LEASE_VEHICLE_CONTRACTS.push({ + plateNo: item.plateNo, + contractCode: 'LNZL-' + item.plateNo + '-01', + projectName: (item.customerName || '租赁项目') + '·' + (item.model || '车辆'), + contractStart: item.contractStart, + contractEnd: item.contractEnd, + contractRent: item.contractRent, + monthlyRent: item.monthlyRent, + deposit: item.deposit != null ? item.deposit : 0, + invoiceMode: item.invoiceMode || 'after' + }); +}); +LEASE_VEHICLE_CONTRACTS.push( + { plateNo: '浙F07033F', contractCode: 'LNZLHT-2026-07033', projectName: '宁波港嘉兴4.5吨租赁项目', contractStart: '2025-12-01', contractEnd: '2026-11-30', contractRent: 7200, monthlyRent: 7200, deposit: 0, invoiceMode: 'after' }, + { plateNo: '京A29256F', contractCode: 'LNZLHT-2026-29256', projectName: '北京海龙49吨牵引租赁项目', contractStart: '2026-08-15', contractEnd: '2027-08-14', contractRent: 23000, monthlyRent: 22000, deposit: 15000, invoiceMode: 'first' } +); + +function getDetailStatus(row) { + if (!row) return 'unsaved'; + if (row.detailStatus) return row.detailStatus; + if (row.submitStatus === 'submitted') return 'approved'; + if (row.saveStatus === 'saved') return 'saved'; + return 'unsaved'; +} + +function canSelectDetailRow(row) { + return getDetailStatus(row) === 'saved'; +} + +var LEASE_CONTRACTS_BY_PLATE = (function () { + var map = {}; + LEASE_VEHICLE_CONTRACTS.forEach(function (c) { + if (!map[c.plateNo]) map[c.plateNo] = []; + map[c.plateNo].push(c); + }); + Object.keys(map).forEach(function (plate) { + map[plate].sort(function (a, b) { return String(a.contractStart).localeCompare(String(b.contractStart)); }); + }); + return map; +})(); + +function formatLeaseVehicleModel(ctx) { + if (!ctx) return ''; + if (ctx.vehicleModel) return ctx.vehicleModel; + var parts = []; + if (ctx.brand) parts.push(ctx.brand); + if (ctx.model) parts.push(ctx.model); + return parts.join(' ') || ctx.vehicleType || ''; +} + +function formatPickupDay(d) { + if (!d || !window.dayjs) return ''; + try { + var x = window.dayjs(d); + return x.isValid() ? x.format('YYYY-MM-DD') : ''; + } catch (e) { + return ''; + } +} + +function getSystemDeliveryAt(plateNo) { + var ctx = LEASE_PLATE_CONTEXT_MAP[plateNo]; + if (!ctx) return ''; + return ctx.systemDeliveryAt || ''; +} + +function isPickupDateMismatch(plateNo, pickupDate) { + if (!plateNo || !pickupDate) return false; + var systemAt = getSystemDeliveryAt(plateNo); + if (!systemAt) return false; + return formatPickupDay(pickupDate) !== systemAt.slice(0, 10); +} + +function getSystemReturnAt(plateNo) { + var ctx = LEASE_PLATE_CONTEXT_MAP[plateNo]; + if (!ctx) return ''; + return ctx.systemReturnAt || ''; +} + +function hasLedgerReturnDate(returnDate) { + return !!returnDate && returnDate !== '-'; +} + +function isReturnDateMismatch(plateNo, returnDate) { + if (!plateNo) return false; + var systemAt = getSystemReturnAt(plateNo); + var hasReturn = hasLedgerReturnDate(returnDate); + if (!systemAt) return hasReturn; + if (!hasReturn) return true; + return formatPickupDay(returnDate) !== systemAt.slice(0, 10); +} + +function getReturnDateMismatchTip(plateNo, returnDate) { + var systemAt = getSystemReturnAt(plateNo); + if (systemAt) return '系统还车时间:' + systemAt; + if (hasLedgerReturnDate(returnDate)) return '系统尚未还车'; + return ''; +} + +function parseBillDay(billDate) { + if (!billDate || !window.dayjs) return null; + try { + var d = window.dayjs(billDate).startOf('day'); + return d.isValid() ? d : null; + } catch (e) { + return null; + } +} + +function findContractForBillDate(plateNo, billDate) { + if (!plateNo || !billDate) return null; + var day = parseBillDay(billDate); + if (!day) return null; + var list = LEASE_CONTRACTS_BY_PLATE[plateNo] || []; + for (var i = 0; i < list.length; i++) { + var c = list[i]; + var start = parseBillDay(c.contractStart); + var end = parseBillDay(c.contractEnd); + if (start && end && !day.isBefore(start, 'day') && !day.isAfter(end, 'day')) return c; + } + return null; +} + +function getSystemContractForBillDate(plateNo, billDate) { + var c = findContractForBillDate(plateNo, billDate); + if (!c) return null; + return { + contractCode: c.contractCode || '', + contractStart: c.contractStart, + contractEnd: c.contractEnd, + contractRent: c.contractRent, + monthlyRent: c.monthlyRent + }; +} + +function isContractStartMismatch(plateNo, billDate, contractStart) { + if (!plateNo || !billDate || !contractStart) return false; + var sys = getSystemContractForBillDate(plateNo, billDate); + if (!sys) return false; + return formatPickupDay(contractStart) !== sys.contractStart; +} + +function isContractEndMismatch(plateNo, billDate, contractEnd) { + if (!plateNo || !billDate || !contractEnd) return false; + var sys = getSystemContractForBillDate(plateNo, billDate); + if (!sys) return false; + return formatPickupDay(contractEnd) !== sys.contractEnd; +} + +function roundDepositAmount(n) { + if (n === null || n === undefined || n === '') return 0; + var v = Number(n); + if (isNaN(v)) return 0; + return Math.round(v * 100) / 100; +} + +function getSystemDeposit(plateNo, billDate) { + var c = findContractForBillDate(plateNo, billDate); + if (c && c.deposit != null) return roundDepositAmount(c.deposit); + var ctx = LEASE_PLATE_CONTEXT_MAP[plateNo]; + if (ctx && ctx.deposit != null) return roundDepositAmount(ctx.deposit); + return 0; +} + +function isDepositMismatch(plateNo, billDate, deposit) { + if (!plateNo) return false; + return roundDepositAmount(deposit) !== getSystemDeposit(plateNo, billDate); +} + +function getSystemContractRent(plateNo, billDate) { + var c = findContractForBillDate(plateNo, billDate); + if (c && c.contractRent != null) return roundDepositAmount(c.contractRent); + var ctx = LEASE_PLATE_CONTEXT_MAP[plateNo]; + if (ctx && ctx.contractRent != null) return roundDepositAmount(ctx.contractRent); + return 0; +} + +function isContractRentMismatch(plateNo, billDate, contractRent) { + if (!plateNo) return false; + return roundDepositAmount(contractRent) !== getSystemContractRent(plateNo, billDate); +} + +var LEASE_DETAIL_STATUS_LABEL = { + unsaved: '待保存', + saved: '已保存', + bill_generated: '已生成账单', + approving: '审批中', + approved: '审批通过' +}; + +var LEASE_BILL_STATUS_LABEL = { + preview: '待确认', + approving: '审批中', + approved: '审批通过', + rejected: '已驳回', + cancelled: '已取消' +}; + +var LEASE_INVOICE_MODE_LABEL = { first: '先开', after: '后开' }; + +var LEASE_PAYMENT_ACCOUNTS = { + '浙江羚牛氢能科技有限公司': { bankName: '中国工商银行嘉兴分行', accountName: '浙江羚牛氢能科技有限公司', accountNo: '1202 0800 0900 1234 567' }, + '上海羚牛氢运物联网科技有限公司': { bankName: '招商银行上海浦东支行', accountName: '上海羚牛氢运物联网科技有限公司', accountNo: '1219 0001 2345 6789' } +}; + +var leaseBillIdSeed = 0; +function nextLeaseBillId() { + leaseBillIdSeed += 1; + return 'lbill-' + Date.now() + '-' + leaseBillIdSeed; +} + +function getRowBillingYearMonth(row) { + if (row && row.billDate && window.dayjs) { + var d = parseBillDay(row.billDate); + if (d) return { year: d.year(), month: d.month() + 1 }; + } + return { year: Number(row && row.year) || 0, month: Number(row && row.month) || 0 }; +} + +function getRowContractMeta(row) { + var c = findContractForBillDate(row.plateNo, row.billDate); + if (!c) { + return { + contractCode: '', + projectName: (row.customerName || '未命名') + '租赁项目', + invoiceMode: 'after', + signCompany: row.signCompany || '' + }; + } + return { + contractCode: c.contractCode || '', + projectName: c.projectName || ((row.customerName || '') + '租赁项目'), + invoiceMode: c.invoiceMode === 'first' ? 'first' : 'after', + signCompany: row.signCompany || '' + }; +} + +function calcRowReductionAmount(row) { + var a = Number(row.mileageReduction) || 0; + var b = Number(row.otherReduction) || 0; + return Math.round((a + b) * 100) / 100; +} + +function formatRentStartForBill(row) { + if (row.rentStartDays != null && row.rentStartDays !== '') return String(row.rentStartDays); + if (!row.rentStartDate || !window.dayjs) return '-'; + try { + return window.dayjs(row.rentStartDate).format('YYYY-MM-DD'); + } catch (e) { + return '-'; + } +} + +function formatBillPeriod(year, month) { + if (!year || !month) return '-'; + return year + '年' + month + '月'; +} + +function buildBillGroupKey(row) { + var billing = getRowBillingYearMonth(row); + var meta = getRowContractMeta(row); + var customer = String(row.customerName || '').trim() || '未命名客户'; + var contractKey = meta.contractCode || meta.projectName || 'default'; + return customer + '|' + billing.year + '|' + billing.month + '|' + contractKey; +} + +/** 按「客户 + 账单月 + 合同/项目」拆分;支持跨客户、跨月份批量勾选 */ +function buildBillsFromRows(rows) { + if (!rows || !rows.length) return []; + var groups = {}; + rows.forEach(function (row) { + var gkey = buildBillGroupKey(row); + var meta = getRowContractMeta(row); + var billing = getRowBillingYearMonth(row); + if (!groups[gkey]) { + groups[gkey] = { + id: nextLeaseBillId(), + status: 'preview', + customerName: row.customerName || '未命名客户', + projectName: meta.projectName, + contractCode: meta.contractCode, + year: billing.year, + month: billing.month, + billPeriod: formatBillPeriod(billing.year, billing.month), + billDate: row.billDate, + invoiceMode: meta.invoiceMode, + signCompany: meta.signCompany || row.signCompany || '浙江羚牛氢能科技有限公司', + receivedTotal: 0, + arrivalAmount: null, + receiptAttachment: null, + invoiceAttachment: null, + invoiceStep: meta.invoiceMode === 'first' ? 0 : 1, + rowKeys: [], + lines: [], + pdfGenerated: false, + createdAt: Date.now() + }; + } + var reduction = calcRowReductionAmount(row); + groups[gkey].rowKeys.push(row.key); + groups[gkey].lines.push({ + key: row.key, + plateNo: row.plateNo, + billDate: row.billDate, + pickupDate: row.pickupDate, + returnDate: row.returnDate, + rentStart: formatRentStartForBill(row), + receivableTotal: row.receivableTotal, + reductionAmount: reduction, + receivedAmount: row.receivedAmount + }); + groups[gkey].receivedTotal += Number(row.receivedAmount) || 0; + }); + return Object.keys(groups).map(function (k) { + var b = groups[k]; + b.receivedTotal = Math.round(b.receivedTotal * 100) / 100; + return b; + }); +} + +function openLeaseBillPdf(bill) { + var pay = LEASE_PAYMENT_ACCOUNTS[bill.signCompany] || LEASE_PAYMENT_ACCOUNTS['浙江羚牛氢能科技有限公司']; + var linesHtml = (bill.lines || []).map(function (ln, idx) { + return '' + (idx + 1) + '' + (ln.plateNo || '-') + '' + (ln.receivableTotal || 0) + '' + (ln.reductionAmount || 0) + '' + (ln.receivedAmount || 0) + ''; + }).join(''); + var html = '对账单' + + '

租赁收款对账单

' + + '

客户:' + (bill.customerName || '-') + ' | 项目:' + (bill.projectName || '-') + ' | 账单周期:' + (bill.billPeriod || '-') + '

' + + '

实收总额:' + (bill.receivedTotal || 0) + ' | 开票方式:' + (LEASE_INVOICE_MODE_LABEL[bill.invoiceMode] || '-') + '

' + + '

收款账户:' + pay.bankName + ' ' + pay.accountName + ' ' + pay.accountNo + '

' + + '' + linesHtml + '
序号车牌应收合计减免实收
' + + ''; + var w = window.open('', '_blank'); + if (!w) return; + w.document.write(html); + w.document.close(); + w.focus(); + w.print(); +} + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + var useRef = React.useRef; + + var antd = window.antd; + var App = antd.App; + var Card = antd.Card; + var Button = antd.Button; + var Table = antd.Table; + var Select = antd.Select; + var DatePicker = antd.DatePicker; + var Input = antd.Input; + var InputNumber = antd.InputNumber; + var Space = antd.Space; + var Tag = antd.Tag; + var Popconfirm = antd.Popconfirm; + var Tooltip = antd.Tooltip; + var Modal = antd.Modal; + var Drawer = antd.Drawer; + var Upload = antd.Upload; + var Alert = antd.Alert; + var message = antd.message; + + /** 原型:切换客服 / 客服主管视角 */ + var CURRENT_USER = { id: 'u_chen', name: '陈高伟', role: 'staff' }; + + var OTHER_STAFF = [ + { id: 'u_chen', name: '陈高伟', dept: '业务一部' }, + { id: 'u_li', name: '李婷婷', dept: '业务二部' }, + { id: 'u_wang', name: '王磊', dept: '业务一部' } + ]; + + var DEPT_OPTIONS = [ + { value: '业务一部', label: '业务一部' }, + { value: '业务二部', label: '业务二部' }, + { value: '业务三部', label: '业务三部' } + ]; + + var VALUE_ADDED_OPTIONS = [ + { value: '包氢租赁', label: '包氢租赁' }, + { value: '包车租赁', label: '包车租赁' }, + { value: '无忧包', label: '无忧包' }, + { value: '含保养', label: '含保养' }, + { value: '含运维包干', label: '含运维包干' } + ]; + + var EXEMPTION_OPTIONS = [ + { value: '帕力安18T里程优惠政策', label: '帕力安18T里程优惠政策' }, + { value: '帕力安4.5T里程优惠政策', label: '帕力安4.5T里程优惠政策' } + ]; + + var PAYMENT_METHOD_OPTIONS = [ + { value: '月度后付', label: '月度后付' }, + { value: '季度后付', label: '季度后付' }, + { value: '预付', label: '预付' } + ]; + + var DETAIL_STATUS_FILTER_OPTIONS = [ + { value: 'unsaved', label: '待保存' }, + { value: 'saved', label: '已保存' }, + { value: 'bill_generated', label: '已生成账单' }, + { value: 'approving', label: '审批中' }, + { value: 'approved', label: '审批通过' } + ]; + + var IMPORT_HEADERS = [ + '年份', '月份', '业务部门', '业务员', '账单日期', '车牌号码', '车型', '客户名称', + '增值费用', '享免政策', '提车日期', '合同生效日期', '合同到期日期', '合同标的租金', + '月度租金', '保险上浮费', '运维费', '其他收入', '实收金额', '付款方式', '备注' + ]; + + function isSupervisorRole(user) { + return user && user.role === 'supervisor'; + } + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + function numOrZero(v) { + if (v === null || v === undefined || v === '') return 0; + var n = Number(v); + return isNaN(n) ? 0 : n; + } + + function roundMoney(n) { + return Math.round(numOrZero(n) * 100) / 100; + } + + function fmtMoney(n) { + if (n === null || n === undefined || n === '') return '0.00'; + return roundMoney(n).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + } + + function isEmptyStr(v) { + return !String(v == null ? '' : v).trim(); + } + + function nowDayjs() { + try { + if (window.dayjs) return window.dayjs(); + } catch (e) {} + return null; + } + + function formatDate(d) { + if (!d || !window.dayjs) return ''; + try { + return window.dayjs(d).format('YYYY-MM-DD'); + } catch (e) { + return ''; + } + } + + function parseDateVal(v) { + if (!v || v === '-' || !window.dayjs) return null; + try { + var d = window.dayjs(v); + return d.isValid() ? d : null; + } catch (e) { + return null; + } + } + + function padMonth(n) { + return String(n).padStart(2, '0'); + } + + function getDaysInMonth(year, month) { + var y = Number(year); + var m = Number(month); + if (!y || !m) return 0; + if (window.dayjs) { + var d = window.dayjs(String(y) + '-' + padMonth(m) + '-01'); + return d.isValid() ? d.daysInMonth() : 0; + } + return new Date(y, m, 0).getDate(); + } + + /** 账单年月:优先取账单日期,其次取年份/月份列 */ + function getBillingYearMonth(row) { + var bill = parseDateVal(row && row.billDate); + if (bill && bill.isValid()) { + return { year: bill.year(), month: bill.month() + 1 }; + } + return { year: Number(row && row.year) || 0, month: Number(row && row.month) || 0 }; + } + + function syncBillDateFromYearMonth(row, year, month) { + if (!window.dayjs || !year || !month) return parseDateVal(row && row.billDate); + var base = parseDateVal(row && row.billDate) || window.dayjs(String(year) + '-' + padMonth(month) + '-01'); + var dim = getDaysInMonth(year, month); + var day = Math.min(base.date(), dim || 1); + var d = window.dayjs(String(year) + '-' + padMonth(month) + '-' + padMonth(day)); + return d.isValid() ? d.startOf('day') : parseDateVal(row && row.billDate); + } + + function enrichBillingPeriodPatch(row, patchObj) { + if (!patchObj) return patchObj; + var merged = Object.assign({}, row, patchObj); + if (patchObj.billDate !== undefined) { + var bill = parseDateVal(patchObj.billDate); + if (bill && bill.isValid()) { + patchObj.year = bill.year(); + patchObj.month = bill.month() + 1; + } + return patchObj; + } + if (patchObj.year !== undefined || patchObj.month !== undefined) { + var y = patchObj.year !== undefined ? patchObj.year : row.year; + var m = patchObj.month !== undefined ? patchObj.month : row.month; + y = Number(y); + m = Number(m); + if (y && m) { + patchObj.year = y; + patchObj.month = m; + var syncedBill = syncBillDateFromYearMonth(row, y, m); + if (syncedBill) patchObj.billDate = syncedBill; + } + } + return patchObj; + } + + function isPickupInBillingMonth(year, month, pickupDate) { + var pickup = parseDateVal(pickupDate); + if (!pickup) return false; + return pickup.year() === Number(year) && pickup.month() + 1 === Number(month); + } + + /** + * 起始日期展示: + * 1. 提车日在账单月内 → rentStartDate = 提车日(显示日期) + * 2. 提车日不在账单月内(或未填) → rentStartDays = 当月天数(如 10 月 31) + */ + function calcRentStartFields(year, month, pickupDate) { + var y = Number(year); + var m = Number(month); + var daysInMonth = getDaysInMonth(y, m); + if (!y || !m || !daysInMonth) { + return { rentStartDate: null, rentStartDays: null }; + } + if (isPickupInBillingMonth(y, m, pickupDate)) { + var pickup = parseDateVal(pickupDate); + return { + rentStartDate: pickup ? pickup.startOf('day') : null, + rentStartDays: null + }; + } + return { rentStartDate: null, rentStartDays: daysInMonth }; + } + + function getRentStartForCalc(year, month, pickupDate) { + var y = Number(year); + var m = Number(month); + if (!y || !m || !window.dayjs) return null; + var monthStart = window.dayjs(String(y) + '-' + padMonth(m) + '-01').startOf('day'); + var fields = calcRentStartFields(y, m, pickupDate); + return fields.rentStartDate || monthStart; + } + + function formatRentStartDisplay(row) { + if (row && row.rentStartDays != null && row.rentStartDays !== '') { + return String(row.rentStartDays); + } + return formatDate(row && row.rentStartDate) || '-'; + } + + function getRentStartDateHint(year, month, pickupDate, rentDaysFromStart, daysInMonth) { + if (!pickupDate) { + return '未填写提车日期:起始日期=当月完整天数' + + (daysInMonth ? '(' + daysInMonth + '天)' : '') + + (rentDaysFromStart != null && daysInMonth ? ';在租天数=' + rentDaysFromStart + '天' : ''); + } + if (isPickupInBillingMonth(year, month, pickupDate)) { + return '提车日在账单月内:起始日期=提车日期;在租天数=提车日至退车日/月末(' + + (rentDaysFromStart != null ? rentDaysFromStart : '-') + '天)'; + } + return '提车日在账单月外:起始日期=当月完整天数' + + (daysInMonth ? '(' + daysInMonth + '天)' : '') + + (rentDaysFromStart != null ? ';在租天数=' + rentDaysFromStart + '天' : ''); + } + + /** + * 自起始日期起算的本月在租天数(至退车日或月末,含首尾日); + * 未退车视为租至月末。 + */ + function calcRentDaysFromStartDate(year, month, rentStartDate, returnDate, pickupDate) { + var y = Number(year); + var m = Number(month); + if (!y || !m) return 0; + + var daysInMonth = getDaysInMonth(y, m); + if (!daysInMonth) return 0; + + if (!window.dayjs) return daysInMonth; + + var monthStart = window.dayjs(String(y) + '-' + padMonth(m) + '-01').startOf('day'); + var monthEnd = monthStart.endOf('month').startOf('day'); + + var pickup = parseDateVal(pickupDate); + if (pickup && pickup.isAfter(monthEnd, 'day')) return 0; + + var rentStart = parseDateVal(rentStartDate) || monthStart; + var rentEnd = parseDateVal(returnDate) || monthEnd; + rentStart = rentStart.startOf('day'); + rentEnd = rentEnd.startOf('day'); + + if (rentStart.isAfter(monthEnd, 'day') || rentEnd.isBefore(monthStart, 'day')) return 0; + + var effectiveStart = rentStart.isBefore(monthStart, 'day') ? monthStart : rentStart; + var effectiveEnd = rentEnd.isAfter(monthEnd, 'day') ? monthEnd : rentEnd; + return effectiveEnd.diff(effectiveStart, 'day') + 1; + } + + function getAvgDaysHint(row, rentDaysFromStart, daysInMonth) { + return '平均天数 = 自起始日期起在租天数 ÷ 当月天数(' + + rentDaysFromStart + ' ÷ ' + daysInMonth + ')'; + } + + function buildPlateAutoPatch(plateNo) { + if (!plateNo) { + return { + plateNo: undefined, + vehicleModel: '', + customerName: '', + dept: undefined, + salesman: '' + }; + } + var ctx = LEASE_PLATE_CONTEXT_MAP[plateNo]; + if (!ctx) { + return { + plateNo: plateNo, + vehicleModel: '', + customerName: '', + dept: undefined, + salesman: '' + }; + } + var patch = { + plateNo: plateNo, + vehicleModel: formatLeaseVehicleModel(ctx), + customerName: ctx.customerName || '', + dept: ctx.dept || undefined, + salesman: ctx.salesman || '' + }; + if (ctx.assetOwner) patch.assetOwner = ctx.assetOwner; + if (ctx.signCompany) patch.signCompany = ctx.signCompany; + if (ctx.vehicleStdCost != null) patch.vehicleStdCost = ctx.vehicleStdCost; + if (ctx.pickupDate) patch.pickupDate = parseDateVal(ctx.pickupDate); + if (ctx.valueAddedService) patch.valueAddedService = ctx.valueAddedService; + return patch; + } + + function buildContractAutoPatch(plateNo, billDate) { + var c = findContractForBillDate(plateNo, billDate); + if (!c) return {}; + var patch = { + contractStart: parseDateVal(c.contractStart), + contractEnd: parseDateVal(c.contractEnd) + }; + if (c.monthlyRent != null) patch.monthlyRent = c.monthlyRent; + return patch; + } + + function mergePlateAndContractPatch(plateNo, billDate) { + return Object.assign({}, buildPlateAutoPatch(plateNo), buildContractAutoPatch(plateNo, billDate)); + } + + var rowIdSeed = 0; + function nextRowKey() { + rowIdSeed += 1; + return 'lease-' + Date.now() + '-' + rowIdSeed; + } + + function recalcRow(row) { + var receivableRent = roundMoney(row.receivableRent); + var insuranceSurcharge = roundMoney(row.insuranceSurcharge); + var opsFeeIncome = roundMoney(row.opsFeeIncome); + var otherIncome = roundMoney(row.otherIncome); + var mileageReduction = roundMoney(row.mileageReduction); + var otherReduction = roundMoney(row.otherReduction); + var monthlyRent = roundMoney(row.monthlyRent); + var maintPackageIncome = roundMoney(row.maintPackageIncome); + var receivedAmount = roundMoney(row.receivedAmount); + + var billing = getBillingYearMonth(row); + var daysInMonth = getDaysInMonth(billing.year, billing.month); + var rentStartFields = calcRentStartFields(billing.year, billing.month, row.pickupDate); + var rentStartForCalc = getRentStartForCalc(billing.year, billing.month, row.pickupDate); + var rentDaysFromStart = calcRentDaysFromStartDate(billing.year, billing.month, rentStartForCalc, row.returnDate, row.pickupDate); + var avgDays = daysInMonth > 0 ? roundMoney(rentDaysFromStart / daysInMonth) : 0; + + var receivableTotal = roundMoney( + receivableRent + insuranceSurcharge + opsFeeIncome + otherIncome + mileageReduction + otherReduction + ); + var monthlyIncome = roundMoney( + monthlyRent + maintPackageIncome + insuranceSurcharge + opsFeeIncome + otherIncome + mileageReduction + otherReduction + ); + var unreceived = roundMoney(receivableTotal - receivedAmount); + + var vehicleStdCost = roundMoney(row.vehicleStdCost); + var vehicleActualCost = roundMoney(vehicleStdCost * avgDays); + var insuranceCost = roundMoney(row.insuranceCost); + var h2Cost = roundMoney(row.h2Cost); + var opsCost = roundMoney(row.opsCost); + var intermediaryFee = roundMoney(row.intermediaryFee); + var otherCost = roundMoney(row.otherCost); + var totalCost = roundMoney( + vehicleActualCost + insuranceCost + h2Cost + opsCost + intermediaryFee + otherCost + ); + var profitLoss = roundMoney(monthlyIncome - totalCost); + + return Object.assign({}, row, { + year: billing.year || row.year, + month: billing.month || row.month, + receivableRent: receivableRent, + insuranceSurcharge: insuranceSurcharge, + opsFeeIncome: opsFeeIncome, + otherIncome: otherIncome, + mileageReduction: mileageReduction, + otherReduction: otherReduction, + monthlyRent: monthlyRent, + maintPackageIncome: maintPackageIncome, + receivedAmount: receivedAmount, + receivableTotal: receivableTotal, + monthlyIncome: monthlyIncome, + unreceived: unreceived, + daysInMonth: daysInMonth, + rentStartDate: rentStartFields.rentStartDate, + rentStartDays: rentStartFields.rentStartDays, + monthRentDays: rentDaysFromStart, + avgDays: avgDays, + vehicleStdCost: vehicleStdCost, + vehicleActualCost: vehicleActualCost, + insuranceCost: insuranceCost, + h2Cost: h2Cost, + opsCost: opsCost, + intermediaryFee: intermediaryFee, + otherCost: otherCost, + totalCost: totalCost, + profitLoss: profitLoss, + deposit: roundMoney(row.deposit), + contractRent: roundMoney(row.contractRent) + }); + } + + function rowStatusLabel(row) { + return LEASE_DETAIL_STATUS_LABEL[getDetailStatus(row)] || '待保存'; + } + + function rowDisplaySortTier(row) { + var ds = getDetailStatus(row); + if (ds === 'approved') return 0; + if (ds === 'approving') return 1; + if (ds === 'bill_generated') return 2; + if (ds === 'saved') return 3; + return 4; + } + + function isSavedDraftRow(row) { + return getDetailStatus(row) === 'saved'; + } + + function detailStatusTagColor(ds) { + if (ds === 'approved') return 'green'; + if (ds === 'approving') return 'processing'; + if (ds === 'bill_generated') return 'purple'; + if (ds === 'saved') return 'orange'; + return 'default'; + } + + function buildEmptyRow(user) { + var now = nowDayjs(); + return recalcRow({ + key: nextRowKey(), + createdBy: user.id, + maintainerName: user.name, + createdAt: now, + year: now ? now.year() : 2026, + month: now ? now.month() + 1 : 1, + dept: user.dept || undefined, + salesman: user.name, + billDate: now, + plateNo: undefined, + vehicleModel: '', + customerName: '', + valueAddedService: undefined, + exemptionPolicy: undefined, + pickupDate: null, + contractStart: null, + contractEnd: null, + rentStartDate: null, + rentStartDays: null, + returnDate: null, + deposit: 0, + contractRent: null, + receivableRent: 0, + monthlyRent: 0, + maintPackageIncome: 0, + insuranceSurcharge: 0, + opsFeeIncome: 0, + otherIncome: 0, + mileageReduction: 0, + otherReduction: 0, + receivedAmount: 0, + invoiceDate: null, + paymentDate: null, + paymentMethod: undefined, + vehicleStdCost: null, + vehicleActualCost: 0, + insuranceCost: 0, + h2Cost: 0, + opsCost: 0, + intermediaryFee: 0, + otherCost: 0, + gpsMileage: null, + reductionDetail: '', + assetOwner: '', + signCompany: '', + remark: '', + submitStatus: 'draft', + saveStatus: 'unsaved', + detailStatus: 'unsaved', + billId: null, + submittedAt: null + }); + } + + function mockBillDate(year, month, day) { + var d = day != null ? day : 1; + return parseDateVal(String(year) + '-' + padMonth(month) + '-' + padMonth(d)); + } + + /** 样例行:合并车牌/合同带出,并强制账单年月与账单日期一致 */ + function finalizeMockRow(base, override) { + var row = Object.assign({}, base, override || {}); + if (row.plateNo && row.billDate) { + Object.assign(row, mergePlateAndContractPatch(row.plateNo, row.billDate)); + } + if (override) Object.assign(row, override); + var bill = parseDateVal(row.billDate); + if (bill && bill.isValid()) { + row.billDate = bill; + row.year = bill.year(); + row.month = bill.month() + 1; + } + if (!row.detailStatus) { + if (row.submitStatus === 'submitted') row.detailStatus = 'approved'; + else if (row.saveStatus === 'saved') row.detailStatus = 'saved'; + else row.detailStatus = 'unsaved'; + } + if (row.billId == null) row.billId = null; + return recalcRow(row); + } + + function buildMockRows() { + var staffScenarios = [ + { + plateNo: '浙F07033F', + year: 2026, + month: 1, + billDay: 1, + submitStatus: 'submitted', + contractRent: 7200, + receivableRent: 7200, + receivedAmount: 7200, + invoiceDate: '2026-01-16', + paymentDate: '2026-01-27' + }, + { + plateNo: '浙F06909F', + year: 2025, + month: 9, + billDay: 1, + submitStatus: 'draft', + contractRent: 6696, + receivableRent: 6696, + receivedAmount: 6696, + invoiceDate: '2025-09-16', + paymentDate: '2025-09-27' + }, + { + plateNo: '浙F39003F', + year: 2025, + month: 10, + billDay: 1, + submitStatus: 'draft', + contractRent: 6696, + receivableRent: 6696, + receivedAmount: 6696, + invoiceDate: '2025-10-16', + paymentDate: '2025-10-27' + } + ]; + var rows = OTHER_STAFF.map(function (staff, i) { + var sc = staffScenarios[i] || staffScenarios[0]; + return finalizeMockRow({ + key: 'mock-' + (i + 1), + createdBy: staff.id, + maintainerName: staff.name, + createdAt: nowDayjs(), + year: sc.year, + month: sc.month, + billDate: mockBillDate(sc.year, sc.month, sc.billDay), + dept: staff.dept, + salesman: staff.name, + plateNo: sc.plateNo, + valueAddedService: '包氢租赁', + exemptionPolicy: undefined, + returnDate: '-', + deposit: 0, + contractRent: sc.contractRent, + receivableRent: sc.receivableRent, + maintPackageIncome: 0, + insuranceSurcharge: 0, + opsFeeIncome: 0, + otherIncome: 0, + mileageReduction: 0, + otherReduction: 0, + receivedAmount: sc.receivedAmount, + invoiceDate: parseDateVal(sc.invoiceDate), + paymentDate: parseDateVal(sc.paymentDate), + paymentMethod: '月度后付', + insuranceCost: 0, + h2Cost: 0, + opsCost: 0, + intermediaryFee: 0, + otherCost: 0, + gpsMileage: null, + reductionDetail: '', + remark: '', + submitStatus: sc.submitStatus, + saveStatus: 'saved', + submittedAt: sc.submitStatus === 'submitted' ? nowDayjs() : null + }); + }); + + var jan2026Bill = mockBillDate(2026, 1, 1); + var feb2026Bill = mockBillDate(2026, 2, 1); + var oct2025Bill = mockBillDate(2025, 10, 1); + + // 交车时间不匹配样例:浙F07033F 系统交车 2025-08-27,台账提车日期填 2025-09-01 + rows.push(finalizeMockRow({ + key: 'mock-pickup-mismatch', + createdBy: 'u_chen', + maintainerName: '陈高伟', + createdAt: nowDayjs(), + year: 2026, + month: 1, + billDate: jan2026Bill, + plateNo: '浙F07033F', + receivedAmount: 0, + submitStatus: 'draft', + saveStatus: 'saved', + submittedAt: null, + remark: '【样例】提车日期与系统交车日期不一致' + }, { + pickupDate: parseDateVal('2025-09-01'), + remark: '【样例】提车日期与系统交车日期不一致' + })); + + // 合同日期不匹配样例:账单日 2026-01-01 应匹配续签合同 2025-12-01~2026-11-30 + rows.push(finalizeMockRow({ + key: 'mock-contract-mismatch', + createdBy: 'u_chen', + maintainerName: '陈高伟', + createdAt: nowDayjs(), + year: 2026, + month: 1, + billDate: jan2026Bill, + plateNo: '浙F07033F', + receivedAmount: 0, + submitStatus: 'draft', + saveStatus: 'saved', + submittedAt: null, + remark: '【样例】合同日期与账单日所在合同不一致' + }, { + contractStart: parseDateVal('2025-09-01'), + contractEnd: parseDateVal('2025-11-30'), + remark: '【样例】合同日期与账单日所在合同不一致' + })); + + // 还车时间不匹配样例:沪A12345 系统还车 2025-10-15,台账退车日期填 2025-10-20(账单月 2025-10) + rows.push(finalizeMockRow({ + key: 'mock-return-mismatch', + createdBy: 'u_chen', + maintainerName: '陈高伟', + createdAt: nowDayjs(), + year: 2025, + month: 10, + billDate: oct2025Bill, + plateNo: '沪A12345', + receivedAmount: 0, + submitStatus: 'draft', + saveStatus: 'saved', + submittedAt: null, + remark: '【样例】退车日期与系统还车时间不一致' + }, { + returnDate: parseDateVal('2025-10-20'), + remark: '【样例】退车日期与系统还车时间不一致' + })); + + // 押金不匹配样例:浙F03218F 系统押金 10000,台账填 9500(保存后显示警告) + rows.push(finalizeMockRow({ + key: 'mock-deposit-mismatch', + createdBy: 'u_chen', + maintainerName: '陈高伟', + createdAt: nowDayjs(), + year: 2026, + month: 2, + billDate: feb2026Bill, + plateNo: '浙F03218F', + receivedAmount: 0, + submitStatus: 'draft', + saveStatus: 'saved', + submittedAt: null, + remark: '【样例】押金与系统不一致(保存后显示警告)' + }, { + deposit: 9500, + remark: '【样例】押金与系统不一致(保存后显示警告)' + })); + + // 合同标的租金不匹配样例:浙F07033F 账单日 2026-01-01 系统租金 7200,台账填 6696 + rows.push(finalizeMockRow({ + key: 'mock-contract-rent-mismatch', + createdBy: 'u_chen', + maintainerName: '陈高伟', + createdAt: nowDayjs(), + year: 2026, + month: 1, + billDate: jan2026Bill, + plateNo: '浙F07033F', + receivedAmount: 0, + submitStatus: 'draft', + saveStatus: 'saved', + submittedAt: null, + remark: '【样例】合同标的租金与系统不一致(保存后显示警告)' + }, { + contractRent: 6696, + receivableRent: 6696, + remark: '【样例】合同标的租金与系统不一致(保存后显示警告)' + })); + + return rows; + } + + function getRowInvalidMap(row) { + var inv = {}; + if (!row.year) inv.year = true; + if (!row.month) inv.month = true; + if (isEmptyStr(row.dept)) inv.dept = true; + if (isEmptyStr(row.salesman)) inv.salesman = true; + if (!row.billDate) inv.billDate = true; + if (isEmptyStr(row.plateNo)) inv.plateNo = true; + if (isEmptyStr(row.customerName)) inv.customerName = true; + return inv; + } + + function canViewRow(row, user, supervisor) { + if (supervisor) return true; + return row && row.createdBy === user.id; + } + + function canEditRow(row, user, supervisor, editingKeys) { + if (!row) return false; + var ds = getDetailStatus(row); + if (ds === 'bill_generated' || ds === 'approving') return false; + if (ds === 'approved') { + return supervisor && editingKeys.indexOf(row.key) >= 0; + } + if (ds === 'saved') { + if (supervisor) return editingKeys.indexOf(row.key) >= 0; + if (row.createdBy !== user.id) return false; + return editingKeys.indexOf(row.key) >= 0; + } + if (supervisor) return false; + if (row.createdBy !== user.id) return false; + return true; + } + + function canDeleteRow(row, user, supervisor) { + var ds = getDetailStatus(row); + if (ds === 'approved') return supervisor; + if (ds === 'approving' || ds === 'bill_generated') return false; + if (supervisor) return true; + return row && row.createdBy === user.id; + } + + var userState = useState(CURRENT_USER); + var currentUser = userState[0]; + var isSupervisor = isSupervisorRole(currentUser); + + var rowsState = useState(buildMockRows); + var allRows = rowsState[0]; + var setAllRows = rowsState[1]; + + var editingKeysState = useState([]); + var editingKeys = editingKeysState[0]; + var setEditingKeys = editingKeysState[1]; + + var rowInvalidState = useState({}); + var rowInvalid = rowInvalidState[0]; + var setRowInvalid = rowInvalidState[1]; + var depositWarnKeysState = useState({}); + var depositWarnKeys = depositWarnKeysState[0]; + var setDepositWarnKeys = depositWarnKeysState[1]; + var contractRentWarnKeysState = useState({}); + var contractRentWarnKeys = contractRentWarnKeysState[0]; + var setContractRentWarnKeys = contractRentWarnKeysState[1]; + + var importModalState = useState(false); + var importModalOpen = importModalState[0]; + var setImportModalOpen = importModalState[1]; + + var requirementModalState = useState(false); + var requirementModalOpen = requirementModalState[0]; + var setRequirementModalOpen = requirementModalState[1]; + + var yearDraftState = useState(undefined); + var yearDraft = yearDraftState[0]; + var setYearDraft = yearDraftState[1]; + var monthDraftState = useState(undefined); + var monthDraft = monthDraftState[0]; + var setMonthDraft = monthDraftState[1]; + var deptDraftState = useState(undefined); + var deptDraft = deptDraftState[0]; + var setDeptDraft = deptDraftState[1]; + var salesmanDraftState = useState(undefined); + var salesmanDraft = salesmanDraftState[0]; + var setSalesmanDraft = salesmanDraftState[1]; + var plateDraftState = useState(undefined); + var plateDraft = plateDraftState[0]; + var setPlateDraft = plateDraftState[1]; + var customerDraftState = useState(undefined); + var customerDraft = customerDraftState[0]; + var setCustomerDraft = customerDraftState[1]; + var statusDraftState = useState(undefined); + var statusDraft = statusDraftState[0]; + var setStatusDraft = statusDraftState[1]; + var billRangeDraftState = useState(null); + var billRangeDraft = billRangeDraftState[0]; + var setBillRangeDraft = billRangeDraftState[1]; + + var yearAppliedState = useState(undefined); + var yearApplied = yearAppliedState[0]; + var setYearApplied = yearAppliedState[1]; + var monthAppliedState = useState(undefined); + var monthApplied = monthAppliedState[0]; + var setMonthApplied = monthAppliedState[1]; + var deptAppliedState = useState(undefined); + var deptApplied = deptAppliedState[0]; + var setDeptApplied = deptAppliedState[1]; + var salesmanAppliedState = useState(undefined); + var salesmanApplied = salesmanAppliedState[0]; + var setSalesmanApplied = salesmanAppliedState[1]; + var plateAppliedState = useState(undefined); + var plateApplied = plateAppliedState[0]; + var setPlateApplied = plateAppliedState[1]; + var customerAppliedState = useState(undefined); + var customerApplied = customerAppliedState[0]; + var setCustomerApplied = customerAppliedState[1]; + var statusAppliedState = useState(undefined); + var statusApplied = statusAppliedState[0]; + var setStatusApplied = statusAppliedState[1]; + var billRangeAppliedState = useState(null); + var billRangeApplied = billRangeAppliedState[0]; + var setBillRangeApplied = billRangeAppliedState[1]; + + var billsState = useState([]); + var bills = billsState[0]; + var setBills = billsState[1]; + + var selectedRowKeysState = useState([]); + var selectedRowKeys = selectedRowKeysState[0]; + var setSelectedRowKeys = selectedRowKeysState[1]; + + var billPreviewOpenState = useState(false); + var billPreviewOpen = billPreviewOpenState[0]; + var setBillPreviewOpen = billPreviewOpenState[1]; + + var billPreviewIndexState = useState(0); + var billPreviewIndex = billPreviewIndexState[0]; + var setBillPreviewIndex = billPreviewIndexState[1]; + + var previewBillsState = useState([]); + var previewBills = previewBillsState[0]; + var setPreviewBills = previewBillsState[1]; + + var billManageOpenState = useState(false); + var billManageOpen = billManageOpenState[0]; + var setBillManageOpen = billManageOpenState[1]; + + var latestRefs = useRef({}); + latestRefs.current = { + setAllRows: setAllRows, + setRowInvalid: setRowInvalid, + rowInvalid: rowInvalid, + currentUser: currentUser, + isSupervisor: isSupervisor, + editingKeys: editingKeys, + setEditingKeys: setEditingKeys, + depositWarnKeys: depositWarnKeys, + setDepositWarnKeys: setDepositWarnKeys, + contractRentWarnKeys: contractRentWarnKeys, + setContractRentWarnKeys: setContractRentWarnKeys + }; + + var visibleRows = useMemo(function () { + var list = allRows.filter(function (r) { + return canViewRow(r, currentUser, isSupervisor); + }); + if (yearApplied != null) list = list.filter(function (r) { return r.year === yearApplied; }); + if (monthApplied != null) list = list.filter(function (r) { return r.month === monthApplied; }); + if (deptApplied) list = list.filter(function (r) { return r.dept === deptApplied; }); + if (salesmanApplied) list = list.filter(function (r) { return r.salesman === salesmanApplied; }); + if (plateApplied) list = list.filter(function (r) { return r.plateNo === plateApplied; }); + if (customerApplied) { + var q = String(customerApplied).toLowerCase(); + list = list.filter(function (r) { + return String(r.customerName || '').toLowerCase().indexOf(q) >= 0; + }); + } + if (statusApplied) list = list.filter(function (r) { return getDetailStatus(r) === statusApplied; }); + if (billRangeApplied && billRangeApplied[0] && billRangeApplied[1] && window.dayjs) { + var start = window.dayjs(billRangeApplied[0]).startOf('day'); + var end = window.dayjs(billRangeApplied[1]).endOf('day'); + list = list.filter(function (r) { + if (!r.billDate) return false; + var d = window.dayjs(r.billDate); + return d.isAfter(start.subtract(1, 'ms')) && d.isBefore(end.add(1, 'ms')); + }); + } + list.sort(function (a, b) { + var ta = rowDisplaySortTier(a); + var tb = rowDisplaySortTier(b); + if (ta !== tb) return ta - tb; + return String(b.key).localeCompare(String(a.key)); + }); + return list.map(function (r, idx) { + return Object.assign({}, r, { seq: idx + 1, displayTier: rowDisplaySortTier(r) }); + }); + }, [ + allRows, currentUser, isSupervisor, + yearApplied, monthApplied, deptApplied, salesmanApplied, + plateApplied, customerApplied, statusApplied, billRangeApplied + ]); + + var rowsForStats = useMemo(function () { + var list = allRows.filter(function (r) { + return canViewRow(r, currentUser, isSupervisor); + }); + if (yearApplied != null) list = list.filter(function (r) { return r.year === yearApplied; }); + if (monthApplied != null) list = list.filter(function (r) { return r.month === monthApplied; }); + if (deptApplied) list = list.filter(function (r) { return r.dept === deptApplied; }); + if (salesmanApplied) list = list.filter(function (r) { return r.salesman === salesmanApplied; }); + if (plateApplied) list = list.filter(function (r) { return r.plateNo === plateApplied; }); + if (customerApplied) { + var q = String(customerApplied).toLowerCase(); + list = list.filter(function (r) { + return String(r.customerName || '').toLowerCase().indexOf(q) >= 0; + }); + } + if (statusApplied) list = list.filter(function (r) { return getDetailStatus(r) === statusApplied; }); + if (billRangeApplied && billRangeApplied[0] && billRangeApplied[1] && window.dayjs) { + var start = window.dayjs(billRangeApplied[0]).startOf('day'); + var end = window.dayjs(billRangeApplied[1]).endOf('day'); + list = list.filter(function (r) { + if (!r.billDate) return false; + var d = window.dayjs(r.billDate); + return d.isAfter(start.subtract(1, 'ms')) && d.isBefore(end.add(1, 'ms')); + }); + } + return list; + }, [ + allRows, currentUser, isSupervisor, + yearApplied, monthApplied, deptApplied, salesmanApplied, + plateApplied, customerApplied, statusApplied, billRangeApplied + ]); + + var statSummary = useMemo(function () { + return rowsForStats.reduce(function (acc, r) { + acc.receivableTotal += numOrZero(r.receivableTotal); + acc.receivedAmount += numOrZero(r.receivedAmount); + acc.unreceived += numOrZero(r.unreceived); + acc.totalCost += numOrZero(r.totalCost); + acc.profitLoss += numOrZero(r.profitLoss); + return acc; + }, { count: rowsForStats.length, receivableTotal: 0, receivedAmount: 0, unreceived: 0, totalCost: 0, profitLoss: 0 }); + }, [rowsForStats]); + + var salesmanOptions = useMemo(function () { + var names = {}; + OTHER_STAFF.forEach(function (s) { names[s.name] = true; }); + allRows.forEach(function (r) { if (r.salesman) names[r.salesman] = true; }); + return Object.keys(names).map(function (n) { return { value: n, label: n }; }); + }, [allRows]); + + var plateOptions = useMemo(function () { + var map = {}; + LEASE_PLATE_MASTER.forEach(function (item) { map[item.plateNo] = item; }); + allRows.forEach(function (r) { + if (r.plateNo && !map[r.plateNo]) map[r.plateNo] = { plateNo: r.plateNo }; + }); + return Object.keys(map).map(function (p) { + return { value: p, label: p }; + }); + }, [allRows]); + + var updateRow = useCallback(function (key, patch) { + setAllRows(function (prev) { + return prev.map(function (r) { + if (r.key !== key) return r; + return recalcRow(Object.assign({}, r, patch)); + }); + }); + }, []); + + function applyDepositWarnings(rows) { + var mismatchCount = 0; + var next = {}; + (rows || []).forEach(function (r) { + if (isDepositMismatch(r.plateNo, r.billDate, r.deposit)) { + next[r.key] = true; + mismatchCount += 1; + } + }); + setDepositWarnKeys(function (prev) { + var merged = Object.assign({}, prev); + (rows || []).forEach(function (r) { + if (next[r.key]) merged[r.key] = true; + else delete merged[r.key]; + }); + return merged; + }); + return mismatchCount; + } + + function applyContractRentWarnings(rows) { + var mismatchCount = 0; + var next = {}; + (rows || []).forEach(function (r) { + if (isContractRentMismatch(r.plateNo, r.billDate, r.contractRent)) { + next[r.key] = true; + mismatchCount += 1; + } + }); + setContractRentWarnKeys(function (prev) { + var merged = Object.assign({}, prev); + (rows || []).forEach(function (r) { + if (next[r.key]) merged[r.key] = true; + else delete merged[r.key]; + }); + return merged; + }); + return mismatchCount; + } + + function buildSaveMismatchMessage(depositCount, contractRentCount, actionLabel) { + var parts = []; + if (depositCount > 0) parts.push(depositCount + ' 条押金'); + if (contractRentCount > 0) parts.push(contractRentCount + ' 条合同标的租金'); + if (!parts.length) return ''; + return '已' + actionLabel + ',但有 ' + parts.join('、') + ' 与系统不一致,请核对'; + } + + function validateRows(rows) { + var allInvalid = {}; + var firstMsg = ''; + (rows || []).forEach(function (r, idx) { + var inv = getRowInvalidMap(r); + if (Object.keys(inv).length) { + allInvalid[r.key] = inv; + if (!firstMsg) firstMsg = '第' + (r.seq || idx + 1) + '行请完善必填项(年份/月份/部门/业务员/账单日期/车牌/客户)'; + } + }); + if (firstMsg) { + setRowInvalid(allInvalid); + message.warning(firstMsg); + return false; + } + setRowInvalid({}); + return true; + } + + var handleSave = useCallback(function () { + var toSave = allRows.filter(function (r) { + return r.createdBy === currentUser.id && getDetailStatus(r) === 'unsaved'; + }); + if (!toSave.length) { + message.info('暂无待保存的记录'); + return; + } + if (!validateRows(toSave)) return; + var depositMismatchCount = applyDepositWarnings(toSave); + var contractRentMismatchCount = applyContractRentWarnings(toSave); + setAllRows(function (prev) { + return prev.map(function (r) { + if (r.createdBy === currentUser.id && getDetailStatus(r) === 'unsaved') { + return Object.assign({}, r, { saveStatus: 'saved', detailStatus: 'saved' }); + } + return r; + }); + }); + setEditingKeys([]); + var mismatchMsg = buildSaveMismatchMessage(depositMismatchCount, contractRentMismatchCount, '保存'); + if (mismatchMsg) { + message.warning(mismatchMsg); + } else { + message.success('已保存'); + } + }, [allRows, currentUser]); + + var patchBillInLists = useCallback(function (billId, patch) { + setBills(function (prev) { + return prev.map(function (b) { + if (b.id !== billId) return b; + return Object.assign({}, b, patch); + }); + }); + setPreviewBills(function (prev) { + return prev.map(function (b) { + if (b.id !== billId) return b; + return Object.assign({}, b, patch); + }); + }); + }, []); + + var patchRowsByBillId = useCallback(function (billId, patch) { + setAllRows(function (prev) { + return prev.map(function (r) { + if (r.billId !== billId) return r; + return Object.assign({}, r, patch); + }); + }); + }, []); + + var handleSubmitCollectionApproval = useCallback(function () { + if (isSupervisor) { + message.info('客服主管无需提交收款审批'); + return; + } + if (!selectedRowKeys.length) { + message.warning('请先勾选已保存的明细记录'); + return; + } + var selected = allRows.filter(function (r) { + return selectedRowKeys.indexOf(r.key) >= 0; + }); + if (selected.some(function (r) { return !canSelectDetailRow(r); })) { + message.warning('仅「已保存」状态的记录可提交收款审批'); + return; + } + if (!validateRows(selected)) return; + var newBills = buildBillsFromRows(selected); + if (!newBills.length) { + message.error('未能生成账单,请检查所选记录'); + return; + } + var billIdMap = {}; + newBills.forEach(function (b) { + b.rowKeys.forEach(function (k) { billIdMap[k] = b.id; }); + }); + setBills(function (prev) { return prev.concat(newBills); }); + setAllRows(function (prev) { + return prev.map(function (r) { + if (!billIdMap[r.key]) return r; + return Object.assign({}, r, { + detailStatus: 'bill_generated', + billId: billIdMap[r.key], + saveStatus: 'saved' + }); + }); + }); + setSelectedRowKeys([]); + setPreviewBills(newBills); + setBillPreviewIndex(0); + setBillPreviewOpen(true); + var customerCount = {}; + newBills.forEach(function (b) { customerCount[b.customerName] = true; }); + var customerN = Object.keys(customerCount).length; + message.success('已按客户·账单月·项目拆分生成 ' + newBills.length + ' 份账单' + + (customerN > 1 ? '(涉及 ' + customerN + ' 个客户)' : '') + + ',请逐份确认'); + }, [allRows, currentUser, isSupervisor, selectedRowKeys]); + + var handleGenerateBillPdf = useCallback(function (bill) { + openLeaseBillPdf(bill); + patchBillInLists(bill.id, { pdfGenerated: true }); + message.success('已打开对账单预览,可在打印对话框中保存为 PDF'); + }, [patchBillInLists]); + + var handleStartBillApproval = useCallback(function (bill) { + if (!bill || bill.status === 'approving' || bill.status === 'approved') return; + patchBillInLists(bill.id, { status: 'approving' }); + patchRowsByBillId(bill.id, { detailStatus: 'approving' }); + message.success('收款审批已发起'); + var idx = previewBills.findIndex(function (b) { return b.id === bill.id; }); + if (idx >= 0 && idx < previewBills.length - 1) { + setBillPreviewIndex(idx + 1); + } else { + setBillPreviewOpen(false); + } + }, [patchBillInLists, patchRowsByBillId, previewBills]); + + var handleCancelBill = useCallback(function (bill) { + if (!bill) return; + if (bill.status === 'approving') { + message.warning('审批中的账单无法取消,请先撤回'); + return; + } + patchRowsByBillId(bill.id, { detailStatus: 'saved', billId: null }); + patchBillInLists(bill.id, { status: 'cancelled' }); + message.success('已取消账单,明细已释放为「已保存」'); + }, [patchBillInLists, patchRowsByBillId]); + + var handleWithdrawBill = useCallback(function (bill) { + if (!bill || bill.status !== 'approving') { + message.warning('仅审批中的账单可撤回'); + return; + } + patchBillInLists(bill.id, { status: 'preview' }); + patchRowsByBillId(bill.id, { detailStatus: 'bill_generated' }); + message.success('已撤回收款审批'); + }, [patchBillInLists, patchRowsByBillId]); + + var handleFinanceApproveBill = useCallback(function (bill, step) { + if (!bill || bill.status !== 'approving') return; + var patch = {}; + if (bill.invoiceMode === 'first' && step === 1) { + patch.invoiceAttachment = { name: '发票-' + bill.projectName + '.pdf', url: '#' }; + patch.invoiceStep = 1; + patchBillInLists(bill.id, patch); + message.success('财务第一步审批完成,发票附件已上传'); + return; + } + patch.receiptAttachment = { name: '收款凭据-' + bill.projectName + '.pdf', url: '#' }; + patch.arrivalAmount = bill.receivedTotal; + if (bill.invoiceMode === 'after' || bill.invoiceMode === 'first') { + patch.invoiceAttachment = patch.invoiceAttachment || { name: '发票-' + bill.projectName + '.pdf', url: '#' }; + } + patch.status = 'approved'; + patch.invoiceStep = 2; + patchBillInLists(bill.id, patch); + patchRowsByBillId(bill.id, { detailStatus: 'approved', submitStatus: 'submitted' }); + message.success('财务审批完成,账单与明细已闭环'); + }, [patchBillInLists, patchRowsByBillId]); + + var handleFinanceRejectBill = useCallback(function (bill) { + if (!bill || bill.status !== 'approving') return; + patchBillInLists(bill.id, { status: 'rejected' }); + patchRowsByBillId(bill.id, { detailStatus: 'bill_generated' }); + message.warning('审批已驳回,可取消账单后修改明细重新提交'); + }, [patchBillInLists, patchRowsByBillId]); + + var openBillPreviewFromManage = useCallback(function (bill) { + var pending = bills.filter(function (b) { + return b.status === 'preview' || b.status === 'rejected'; + }); + var idx = pending.findIndex(function (b) { return b.id === bill.id; }); + if (idx < 0) pending = [bill]; + else pending = pending.slice(idx).concat(pending.slice(0, idx)); + setPreviewBills(pending); + setBillPreviewIndex(0); + setBillPreviewOpen(true); + setBillManageOpen(false); + }, [bills]); + + var addRow = useCallback(function () { + var row = buildEmptyRow(Object.assign({}, currentUser, { + dept: OTHER_STAFF.filter(function (s) { return s.id === currentUser.id; })[0] + ? OTHER_STAFF.filter(function (s) { return s.id === currentUser.id; })[0].dept + : undefined + })); + setAllRows(function (prev) { return prev.concat([row]); }); + message.success('已新增一行草稿'); + }, [currentUser]); + + var handleQuery = useCallback(function () { + setYearApplied(yearDraft); + setMonthApplied(monthDraft); + setDeptApplied(deptDraft); + setSalesmanApplied(salesmanDraft); + setPlateApplied(plateDraft); + setCustomerApplied(customerDraft); + setStatusApplied(statusDraft); + setBillRangeApplied(billRangeDraft); + message.success('查询成功'); + }, [yearDraft, monthDraft, deptDraft, salesmanDraft, plateDraft, customerDraft, statusDraft, billRangeDraft]); + + var handleReset = useCallback(function () { + setYearDraft(undefined); + setMonthDraft(undefined); + setDeptDraft(undefined); + setSalesmanDraft(undefined); + setPlateDraft(undefined); + setCustomerDraft(undefined); + setStatusDraft(undefined); + setBillRangeDraft(null); + setYearApplied(undefined); + setMonthApplied(undefined); + setDeptApplied(undefined); + setSalesmanApplied(undefined); + setPlateApplied(undefined); + setCustomerApplied(undefined); + setStatusApplied(undefined); + setBillRangeApplied(null); + }, []); + + var renderFilterField = useCallback(function (label, control) { + return React.createElement('div', { className: 'lc-filter-field' }, + React.createElement('span', { className: 'lc-filter-field-label' }, label), + React.createElement('div', { className: 'lc-filter-field-control' }, control) + ); + }, []); + + var renderStatCard = useCallback(function (card, displayValue, valueClass) { + return React.createElement('div', { + key: card.key, + className: 'lc-alert-card lc-alert-card--' + card.type, + 'aria-label': card.title + ':' + displayValue + }, + React.createElement('div', { className: 'lc-alert-card-tip-anchor' }, + React.createElement(Tooltip, { title: card.desc, placement: 'topRight', overlayStyle: { maxWidth: 320 } }, + React.createElement('span', { + className: 'lc-alert-card-tip', + role: 'img', + 'aria-label': card.title + '说明' + }, + leaseSvgIcon([{ tag: 'circle', cx: 12, cy: 12, r: 10 }, { tag: 'line', x1: 12, y1: 16, x2: 12, y2: 12 }, { tag: 'line', x1: 12, y1: 8, x2: 12.01, y2: 8 }], 12) + ) + ) + ), + React.createElement('div', { className: 'lc-alert-card-icon', 'aria-hidden': true }, leaseKpiIcon(card.key)), + React.createElement('div', { className: 'lc-alert-card-main' }, + React.createElement('div', { className: 'lc-alert-card-title' }, card.title), + React.createElement('div', { className: 'lc-alert-card-val' + (valueClass ? ' ' + valueClass : '') }, displayValue) + ) + ); + }, []); + + function downloadImportTemplate() { + var csv = IMPORT_HEADERS.join(',') + '\n' + + '2026,1,,,' + currentUser.name + ',2026-01-01,浙F07033F,,,包氢租赁,,,,6696,6696,0,0,0,6696,月度后付,导入示例(车型/客户由车牌自动带出)'; + var blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' }); + var a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = '租赁业务台账导入模板.csv'; + a.click(); + URL.revokeObjectURL(a.href); + } + + function parseImportCsv(text) { + var lines = String(text || '').split(/\r?\n/).filter(function (l) { return l.trim(); }); + if (lines.length < 2) return []; + var imported = []; + for (var i = 1; i < lines.length; i++) { + var cols = lines[i].split(','); + if (!cols[5]) continue; + var plateNo = cols[5]; + imported.push( + recalcRow(Object.assign({ + key: nextRowKey(), + createdBy: currentUser.id, + maintainerName: currentUser.name, + createdAt: nowDayjs(), + year: Number(cols[0]) || 2026, + month: Number(cols[1]) || 1, + dept: cols[2] || undefined, + salesman: cols[3] || currentUser.name, + billDate: parseDateVal(cols[4]), + plateNo: plateNo, + valueAddedService: cols[8] || undefined, + exemptionPolicy: cols[9] || undefined, + pickupDate: parseDateVal(cols[10]), + contractStart: parseDateVal(cols[11]), + contractEnd: parseDateVal(cols[12]), + contractRent: numOrZero(cols[13]), + monthlyRent: numOrZero(cols[14]), + insuranceSurcharge: numOrZero(cols[15]), + opsFeeIncome: numOrZero(cols[16]), + otherIncome: numOrZero(cols[17]), + receivedAmount: numOrZero(cols[18]), + paymentMethod: cols[19] || undefined, + remark: cols[20] || '', + receivableRent: numOrZero(cols[13]), + maintPackageIncome: 0, + mileageReduction: 0, + otherReduction: 0, + deposit: 0, + vehicleActualCost: 0, + insuranceCost: 0, + h2Cost: 0, + opsCost: 0, + intermediaryFee: 0, + otherCost: 0, + submitStatus: 'draft', + saveStatus: 'unsaved', + detailStatus: 'unsaved', + billId: null, + submittedAt: null + }, mergePlateAndContractPatch(plateNo, parseDateVal(cols[4])))) + ); + } + return imported; + } + + var handleImportFile = useCallback(function (file) { + var reader = new FileReader(); + reader.onload = function (e) { + var rows = parseImportCsv(e.target.result); + if (!rows.length) { + message.error('未解析到有效数据,请检查模板格式'); + return; + } + setAllRows(function (prev) { return prev.concat(rows); }); + setImportModalOpen(false); + message.success('已导入 ' + rows.length + ' 条草稿记录'); + }; + reader.readAsText(file, 'UTF-8'); + return false; + }, [currentUser]); + + var columns = useMemo(function () { + var cellSelectDropdown = { + dropdownMatchSelectWidth: false, + popupClassName: 'lease-cell-select-dropdown', + dropdownStyle: { borderRadius: 8 } + }; + + function cellInputNumber(props) { + return React.createElement(InputNumber, Object.assign({ + className: 'lease-cell-input-number', + style: { width: '100%' } + }, props)); + } + + function cellInput(props) { + return React.createElement(Input, Object.assign({ + size: 'small', + className: 'lease-cell-input', + style: { width: '100%' } + }, props)); + } + + function cellDatePicker(props) { + return React.createElement(DatePicker, Object.assign({ + size: 'small', + className: 'lease-cell-date', + format: 'YYYY-MM-DD', + allowClear: true, + style: { width: '100%' } + }, props)); + } + + function cellSelect(props) { + return React.createElement(Select, Object.assign({ + size: 'small', + className: 'lease-cell-select' + }, cellSelectDropdown, props)); + } + + function cellPlateSelect(props) { + return React.createElement(Select, Object.assign({ + size: 'small', + className: 'lease-cell-select lease-cell-select-plate', + showSearch: true, + allowClear: true, + placeholder: '请选择车牌', + optionLabelProp: 'value', + filterOption: filterOption, + dropdownMatchSelectWidth: false, + popupClassName: 'lease-plate-select-dropdown', + dropdownStyle: { borderRadius: 8, minWidth: 112 } + }, props)); + } + + function patch(key, field, val) { + var ref = latestRefs.current; + var patchObj = {}; + patchObj[field] = val; + ref.setAllRows(function (prev) { + return prev.map(function (r) { + if (r.key !== key) return r; + var nextPatch = enrichBillingPeriodPatch(r, patchObj); + var nextRow = Object.assign({}, r, nextPatch); + if (field === 'billDate') { + Object.assign(nextRow, buildContractAutoPatch(r.plateNo, nextPatch.billDate)); + } + return recalcRow(nextRow); + }); + }); + if (field === 'deposit' || field === 'contractRent' || field === 'plateNo' || field === 'billDate') { + if (field === 'deposit' || field === 'plateNo' || field === 'billDate') { + ref.setDepositWarnKeys(function (prev) { + if (!prev[key]) return prev; + var next = Object.assign({}, prev); + delete next[key]; + return next; + }); + } + if (field === 'contractRent' || field === 'plateNo' || field === 'billDate') { + ref.setContractRentWarnKeys(function (prev) { + if (!prev[key]) return prev; + var next = Object.assign({}, prev); + delete next[key]; + return next; + }); + } + } + } + + function patchPlate(key, plateNo) { + var ref = latestRefs.current; + if (plateNo && !LEASE_PLATE_CONTEXT_MAP[plateNo]) { + message.warning('未在系统中找到车牌「' + plateNo + '」的关联信息'); + } + ref.setAllRows(function (prev) { + return prev.map(function (r) { + if (r.key !== key) return r; + return recalcRow(Object.assign({}, r, mergePlateAndContractPatch(plateNo, r.billDate))); + }); + }); + ref.setDepositWarnKeys(function (prev) { + if (!prev[key]) return prev; + var next = Object.assign({}, prev); + delete next[key]; + return next; + }); + ref.setContractRentWarnKeys(function (prev) { + if (!prev[key]) return prev; + var next = Object.assign({}, prev); + delete next[key]; + return next; + }); + } + + function patchBillDate(key, billDate) { + var ref = latestRefs.current; + ref.setAllRows(function (prev) { + return prev.map(function (r) { + if (r.key !== key) return r; + var nextPatch = enrichBillingPeriodPatch(r, { billDate: billDate }); + return recalcRow(Object.assign({}, r, nextPatch, buildContractAutoPatch(r.plateNo, nextPatch.billDate || billDate))); + }); + }); + ref.setDepositWarnKeys(function (prev) { + if (!prev[key]) return prev; + var next = Object.assign({}, prev); + delete next[key]; + return next; + }); + ref.setContractRentWarnKeys(function (prev) { + if (!prev[key]) return prev; + var next = Object.assign({}, prev); + delete next[key]; + return next; + }); + } + + function plainCell(v, align, cls) { + return React.createElement('span', { + className: 'lease-cell-plain' + (cls ? ' ' + cls : ''), + style: { textAlign: align || 'center' } + }, v === 0 || v === '0' ? v : v || '-'); + } + + function systemAutoCell(v, record) { + if (record && !editable(record)) return plainCell(v || '-', 'center'); + return readOnly(v || '-', 'center', 'lease-cell-readonly'); + } + + function readOnly(v, align, cls) { + return React.createElement('div', { + className: cls || 'lease-cell-readonly', + style: { textAlign: align || 'center' } + }, v === 0 || v === '0' ? v : v || '-'); + } + + function calcCell(v, record, align, formatFn) { + var text = formatFn ? formatFn(v) : v; + align = align || 'right'; + if (!editable(record)) return plainCell(text, align, 'lease-cell-calc'); + return readOnly(text, align, 'lease-cell-readonly lease-cell-calc'); + } + + function fieldInvalid(record, field) { + var inv = (latestRefs.current.rowInvalid && latestRefs.current.rowInvalid[record.key]) || {}; + return inv[field]; + } + + function editable(record) { + return canEditRow( + record, + latestRefs.current.currentUser, + latestRefs.current.isSupervisor, + latestRefs.current.editingKeys + ); + } + + function numCell(field, record, opts) { + opts = opts || {}; + var v = record[field]; + if (!editable(record)) { + return plainCell(fmtMoney(v), 'right', opts.calc ? 'lease-cell-calc' : ''); + } + return cellInputNumber({ + min: opts.min != null ? opts.min : undefined, + precision: 2, + value: v == null || v === '' ? null : v, + status: fieldInvalid(record, field) ? 'error' : undefined, + onChange: function (val) { patch(record.key, field, val == null ? 0 : val); } + }); + } + + function textCell(field, record, width) { + var v = record[field]; + if (!editable(record)) return plainCell(v || '-', 'center'); + return cellInput({ + value: v, + style: { width: width || '100%' }, + status: fieldInvalid(record, field) ? 'error' : undefined, + onChange: function (e) { patch(record.key, field, e.target.value); } + }); + } + + function dateCell(field, record) { + var v = record[field]; + if (!editable(record)) return plainCell(formatDate(v) || '-', 'center'); + return cellDatePicker({ + value: v, + status: fieldInvalid(record, field) ? 'error' : undefined, + onChange: function (d) { patch(record.key, field, d); } + }); + } + + function dateWarnWrap(inner, tooltipTitle) { + if (!tooltipTitle) return inner; + return React.createElement('div', { className: 'lease-date-warn-cell' }, + inner, + React.createElement(Tooltip, { title: tooltipTitle, placement: 'top' }, + React.createElement('span', { + role: 'img', + 'aria-label': tooltipTitle, + style: { flexShrink: 0, color: '#f59e0b', display: 'inline-flex', cursor: 'help', lineHeight: 0 } + }, LEASE_ICONS.warn) + ) + ); + } + + function billDateCell(record) { + var field = 'billDate'; + var v = record[field]; + var inner = editable(record) + ? cellDatePicker({ + value: v, + status: fieldInvalid(record, field) ? 'error' : undefined, + onChange: function (d) { patchBillDate(record.key, d); } + }) + : plainCell(formatDate(v) || '-', 'center'); + if (!record.plateNo || !v) return inner; + if (!getSystemContractForBillDate(record.plateNo, v)) { + return dateWarnWrap(inner, '账单日不在系统任何合同有效期内'); + } + return inner; + } + + function contractStartCell(record) { + var field = 'contractStart'; + var v = record[field]; + var inner = editable(record) + ? cellDatePicker({ value: v, onChange: function (d) { patch(record.key, field, d); } }) + : plainCell(formatDate(v) || '-', 'center'); + var sys = getSystemContractForBillDate(record.plateNo, record.billDate); + if (isContractStartMismatch(record.plateNo, record.billDate, v) && sys) { + return dateWarnWrap(inner, '系统合同生效日期:' + sys.contractStart); + } + return inner; + } + + function contractEndCell(record) { + var field = 'contractEnd'; + var v = record[field]; + var inner = editable(record) + ? cellDatePicker({ value: v, onChange: function (d) { patch(record.key, field, d); } }) + : plainCell(formatDate(v) || '-', 'center'); + var sys = getSystemContractForBillDate(record.plateNo, record.billDate); + if (isContractEndMismatch(record.plateNo, record.billDate, v) && sys) { + return dateWarnWrap(inner, '系统合同结束日期:' + sys.contractEnd); + } + return inner; + } + + function pickupDateCell(record) { + var field = 'pickupDate'; + var v = record[field]; + var inner = editable(record) + ? cellDatePicker({ + value: v, + status: fieldInvalid(record, field) ? 'error' : undefined, + onChange: function (d) { patch(record.key, field, d); } + }) + : plainCell(formatDate(v) || '-', 'center'); + if (!isPickupDateMismatch(record.plateNo, v)) return inner; + return dateWarnWrap(inner, '系统交车日期:' + getSystemDeliveryAt(record.plateNo)); + } + + function contractRentCell(record) { + var field = 'contractRent'; + var v = record[field]; + var inner; + if (!editable(record)) { + inner = plainCell(fmtMoney(v), 'right'); + } else { + inner = cellInputNumber({ + min: 0, + precision: 2, + placeholder: '手填', + value: v == null || v === '' ? null : v, + status: fieldInvalid(record, field) ? 'error' : undefined, + onChange: function (val) { patch(record.key, field, val == null ? 0 : val); } + }); + } + var ref = latestRefs.current; + if (!ref.contractRentWarnKeys || !ref.contractRentWarnKeys[record.key]) return inner; + if (!isContractRentMismatch(record.plateNo, record.billDate, v)) return inner; + return dateWarnWrap(inner, '系统合同标的租金:' + fmtMoney(getSystemContractRent(record.plateNo, record.billDate))); + } + + function depositCell(record) { + var field = 'deposit'; + var v = record[field]; + var inner; + if (!editable(record)) { + inner = plainCell(fmtMoney(v), 'right'); + } else { + inner = cellInputNumber({ + min: 0, + precision: 2, + placeholder: '手填', + value: v == null || v === '' ? null : v, + status: fieldInvalid(record, field) ? 'error' : undefined, + onChange: function (val) { patch(record.key, field, val == null ? 0 : val); } + }); + } + var ref = latestRefs.current; + if (!ref.depositWarnKeys || !ref.depositWarnKeys[record.key]) return inner; + if (!isDepositMismatch(record.plateNo, record.billDate, v)) return inner; + return dateWarnWrap(inner, '系统押金:' + fmtMoney(getSystemDeposit(record.plateNo, record.billDate))); + } + + function returnDateCell(record) { + var v = record.returnDate; + var inner; + if (!editable(record)) { + inner = plainCell((!v || v === '-') ? '-' : formatDate(v), 'center'); + } else { + inner = cellDatePicker({ + placeholder: '未退车', + value: v && v !== '-' ? v : null, + onChange: function (d) { patch(record.key, 'returnDate', d); } + }); + } + if (!isReturnDateMismatch(record.plateNo, v)) return inner; + var tip = getReturnDateMismatchTip(record.plateNo, v); + if (!tip) return inner; + return dateWarnWrap(inner, tip); + } + + function ct(label, tipKey) { + return leaseColTitle(label, LEASE_COLUMN_TIPS[tipKey]); + } + + return [ + { title: ct('序号', 'seq'), dataIndex: 'seq', width: 52, align: 'center' }, + { + title: ct('状态', 'status'), key: 'status', width: 72, align: 'center', + render: function (_, r) { + var ds = getDetailStatus(r); + var label = rowStatusLabel(r); + return React.createElement(Tag, { color: detailStatusTagColor(ds), style: { margin: 0 } }, label); + } + }, + { title: ct('维护人', 'maintainerName'), dataIndex: 'maintainerName', width: 72, align: 'center' }, + { + title: ct('年份', 'year'), dataIndex: 'year', width: 68, align: 'center', + render: function (v, r) { + if (!editable(r)) return plainCell(v, 'center'); + return cellInputNumber({ + min: 2020, max: 2099, precision: 0, + value: v, status: fieldInvalid(r, 'year') ? 'error' : undefined, + onChange: function (val) { patch(r.key, 'year', val); } + }); + } + }, + { + title: ct('月份', 'month'), dataIndex: 'month', width: 58, align: 'center', + render: function (v, r) { + if (!editable(r)) return plainCell(v, 'center'); + return cellInputNumber({ + min: 1, max: 12, precision: 0, + value: v, status: fieldInvalid(r, 'month') ? 'error' : undefined, + onChange: function (val) { patch(r.key, 'month', val); } + }); + } + }, + { + title: ct('业务部门', 'dept'), dataIndex: 'dept', width: 96, align: 'center', + render: function (v, r) { + var invalid = fieldInvalid(r, 'dept'); + if (invalid && editable(r)) { + return React.createElement('div', { + className: 'lease-cell-readonly', + style: { textAlign: 'center', border: '1px solid #ff4d4f', borderRadius: 4, color: '#ff4d4f' } + }, v || '-'); + } + if (invalid) { + return React.createElement('span', { + className: 'lease-cell-plain', + style: { color: '#ef4444', textAlign: 'center', display: 'block' } + }, v || '-'); + } + return systemAutoCell(v, r); + } + }, + { + title: ct('业务员', 'salesman'), dataIndex: 'salesman', width: 80, align: 'center', + render: function (v, r) { + var invalid = fieldInvalid(r, 'salesman'); + if (invalid && editable(r)) { + return React.createElement('div', { + className: 'lease-cell-readonly', + style: { textAlign: 'center', border: '1px solid #ff4d4f', borderRadius: 4, color: '#ff4d4f' } + }, v || '-'); + } + if (invalid) return React.createElement('span', { className: 'lease-cell-plain', style: { color: '#ef4444', textAlign: 'center', display: 'block' } }, v || '-'); + return systemAutoCell(v, r); + } + }, + { title: ct('账单日期', 'billDate'), dataIndex: 'billDate', width: 128, align: 'center', render: function (v, r) { return billDateCell(r); } }, + { + title: ct('车牌号码', 'plateNo'), dataIndex: 'plateNo', width: 112, align: 'center', + render: function (v, r) { + if (!editable(r)) return plainCell(v || '-', 'center'); + return cellPlateSelect({ + style: { width: '100%' }, + value: v, + options: plateOptions, + status: fieldInvalid(r, 'plateNo') ? 'error' : undefined, + onChange: function (val) { patchPlate(r.key, val); } + }); + } + }, + { + title: ct('车型', 'vehicleModel'), dataIndex: 'vehicleModel', width: 120, align: 'center', + render: function (v, r) { + var invalid = fieldInvalid(r, 'vehicleModel'); + if (invalid && editable(r)) { + return React.createElement('div', { + className: 'lease-cell-readonly', + style: { textAlign: 'center', border: '1px solid #ff4d4f', borderRadius: 4, color: '#ff4d4f' } + }, v || '-'); + } + if (invalid) { + return React.createElement('span', { + className: 'lease-cell-plain', + style: { color: '#ef4444', textAlign: 'center', display: 'block' } + }, v || '-'); + } + return systemAutoCell(v, r); + } + }, + { + title: ct('客户名称', 'customerName'), dataIndex: 'customerName', width: 160, align: 'center', + render: function (v, r) { + var invalid = fieldInvalid(r, 'customerName'); + if (invalid && editable(r)) { + return React.createElement('div', { + className: 'lease-cell-readonly', + style: { textAlign: 'center', border: '1px solid #ff4d4f', borderRadius: 4, color: '#ff4d4f' } + }, v || '-'); + } + if (invalid) { + return React.createElement('span', { + className: 'lease-cell-plain', + style: { color: '#ef4444', textAlign: 'center', display: 'block' } + }, v || '-'); + } + return systemAutoCell(v, r); + } + }, + { + title: ct('增值费用', 'valueAddedService'), dataIndex: 'valueAddedService', width: 110, align: 'center', + render: function (v, r) { + if (!editable(r)) return plainCell(v || '-', 'center'); + return cellSelect({ + allowClear: true, + showSearch: true, + placeholder: '请选择', + style: { width: '100%' }, + dropdownStyle: { borderRadius: 8, minWidth: 140 }, + value: v || undefined, + options: VALUE_ADDED_OPTIONS, + filterOption: filterOption, + onChange: function (val) { patch(r.key, 'valueAddedService', val); } + }); + } + }, + { + title: ct('享免政策', 'exemptionPolicy'), dataIndex: 'exemptionPolicy', width: 160, align: 'center', + render: function (v, r) { + if (!editable(r)) return plainCell(v || '-', 'center'); + return cellSelect({ + allowClear: true, + showSearch: true, + placeholder: '请选择', + style: { width: '100%' }, + dropdownStyle: { borderRadius: 8, minWidth: 220 }, + value: v || undefined, + options: EXEMPTION_OPTIONS, + filterOption: filterOption, + onChange: function (val) { patch(r.key, 'exemptionPolicy', val); } + }); + } + }, + { title: ct('提车日期', 'pickupDate'), dataIndex: 'pickupDate', width: 138, align: 'center', render: function (v, r) { return pickupDateCell(r); } }, + { title: ct('合同生效', 'contractStart'), dataIndex: 'contractStart', width: 138, align: 'center', render: function (v, r) { return contractStartCell(r); } }, + { title: ct('合同到期', 'contractEnd'), dataIndex: 'contractEnd', width: 138, align: 'center', render: function (v, r) { return contractEndCell(r); } }, + { + title: ct('起始日期', 'rentStartDate'), dataIndex: 'rentStartDate', width: 118, align: 'center', + render: function (v, r) { + var billing = getBillingYearMonth(r); + var text = formatRentStartDisplay(r); + var inner = !editable(r) + ? plainCell(text, 'center', 'lease-cell-calc') + : readOnly(text, 'center', 'lease-cell-readonly lease-cell-calc'); + return React.createElement(Tooltip, { + title: getRentStartDateHint(billing.year, billing.month, r.pickupDate, r.monthRentDays, r.daysInMonth), + placement: 'top' + }, inner); + } + }, + { title: ct('退车日期', 'returnDate'), dataIndex: 'returnDate', width: 138, align: 'center', render: function (v, r) { return returnDateCell(r); } }, + { + title: ct('平均天数', 'avgDays'), dataIndex: 'avgDays', width: 88, align: 'right', + render: function (v, r) { + var display = calcCell(v, r, 'right', fmtMoney); + return React.createElement(Tooltip, { + title: getAvgDaysHint(r, r.monthRentDays, r.daysInMonth), + placement: 'top' + }, display); + } + }, + { + title: ct('押金', 'deposit'), dataIndex: 'deposit', width: 108, align: 'right', + render: function (v, r) { return depositCell(r); } + }, + { + title: ct('合同标的租金', 'contractRent'), dataIndex: 'contractRent', width: 128, align: 'right', + render: function (v, r) { return contractRentCell(r); } + }, + { title: ct('应收合计', 'receivableTotal'), dataIndex: 'receivableTotal', width: 96, align: 'right', render: function (v, r) { return calcCell(v, r, 'right', fmtMoney); } }, + { title: ct('应收租金', 'receivableRent'), dataIndex: 'receivableRent', width: 96, align: 'right', render: function (v, r) { return numCell('receivableRent', r); } }, + { title: ct('月度收入', 'monthlyIncome'), dataIndex: 'monthlyIncome', width: 96, align: 'right', render: function (v, r) { return calcCell(v, r, 'right', fmtMoney); } }, + { title: ct('月度租金', 'monthlyRent'), dataIndex: 'monthlyRent', width: 88, align: 'right', render: function (v, r) { return numCell('monthlyRent', r); } }, + { title: ct('维保包干', 'maintPackageIncome'), dataIndex: 'maintPackageIncome', width: 88, align: 'right', render: function (v, r) { return numCell('maintPackageIncome', r); } }, + { title: ct('保险上浮', 'insuranceSurcharge'), dataIndex: 'insuranceSurcharge', width: 88, align: 'right', render: function (v, r) { return numCell('insuranceSurcharge', r); } }, + { title: ct('运维费(收)', 'opsFeeIncome'), dataIndex: 'opsFeeIncome', width: 88, align: 'right', render: function (v, r) { return numCell('opsFeeIncome', r); } }, + { title: ct('其他收入', 'otherIncome'), dataIndex: 'otherIncome', width: 88, align: 'right', render: function (v, r) { return numCell('otherIncome', r); } }, + { title: ct('里程减免', 'mileageReduction'), dataIndex: 'mileageReduction', width: 88, align: 'right', render: function (v, r) { return numCell('mileageReduction', r, { min: -9999999 }); } }, + { title: ct('其他减免', 'otherReduction'), dataIndex: 'otherReduction', width: 88, align: 'right', render: function (v, r) { return numCell('otherReduction', r, { min: -9999999 }); } }, + { title: ct('实收金额', 'receivedAmount'), dataIndex: 'receivedAmount', width: 96, align: 'right', render: function (v, r) { return numCell('receivedAmount', r); } }, + { title: ct('未收', 'unreceived'), dataIndex: 'unreceived', width: 88, align: 'right', render: function (v, r) { return calcCell(v, r, 'right', fmtMoney); } }, + { title: ct('开票日期', 'invoiceDate'), dataIndex: 'invoiceDate', width: 118, align: 'center', render: function (v, r) { return dateCell('invoiceDate', r); } }, + { title: ct('付款日期', 'paymentDate'), dataIndex: 'paymentDate', width: 118, align: 'center', render: function (v, r) { return dateCell('paymentDate', r); } }, + { + title: ct('付款方式', 'paymentMethod'), dataIndex: 'paymentMethod', width: 96, align: 'center', + render: function (v, r) { + if (!editable(r)) return plainCell(v || '-', 'center'); + return cellSelect({ + allowClear: true, + style: { width: '100%' }, + dropdownStyle: { borderRadius: 8, minWidth: 120 }, + value: v, + options: PAYMENT_METHOD_OPTIONS, + onChange: function (val) { patch(r.key, 'paymentMethod', val); } + }); + } + }, + { title: ct('标准成本', 'vehicleStdCost'), dataIndex: 'vehicleStdCost', width: 88, align: 'right', render: function (v, r) { return numCell('vehicleStdCost', r); } }, + { title: ct('实际成本', 'vehicleActualCost'), dataIndex: 'vehicleActualCost', width: 88, align: 'right', render: function (v, r) { return calcCell(v, r, 'right', fmtMoney); } }, + { title: ct('保险费', 'insuranceCost'), dataIndex: 'insuranceCost', width: 80, align: 'right', render: function (v, r) { return numCell('insuranceCost', r); } }, + { title: ct('氢费', 'h2Cost'), dataIndex: 'h2Cost', width: 72, align: 'right', render: function (v, r) { return numCell('h2Cost', r); } }, + { title: ct('运维费', 'opsCost'), dataIndex: 'opsCost', width: 88, align: 'right', render: function (v, r) { return numCell('opsCost', r); } }, + { title: ct('居间费', 'intermediaryFee'), dataIndex: 'intermediaryFee', width: 80, align: 'right', render: function (v, r) { return numCell('intermediaryFee', r); } }, + { title: ct('其他成本', 'otherCost'), dataIndex: 'otherCost', width: 80, align: 'right', render: function (v, r) { return numCell('otherCost', r); } }, + { title: ct('总成本', 'totalCost'), dataIndex: 'totalCost', width: 88, align: 'right', render: function (v, r) { return calcCell(v, r, 'right', fmtMoney); } }, + { title: ct('盈亏', 'profitLoss'), dataIndex: 'profitLoss', width: 80, align: 'right', render: function (v, r) { return calcCell(v, r, 'right', fmtMoney); } }, + { title: ct('资产归属', 'assetOwner'), dataIndex: 'assetOwner', width: 140, align: 'center', render: function (v, r) { return textCell('assetOwner', r); } }, + { title: ct('签约公司', 'signCompany'), dataIndex: 'signCompany', width: 140, align: 'center', render: function (v, r) { return textCell('signCompany', r); } }, + { title: ct('备注', 'remark'), dataIndex: 'remark', width: 120, align: 'center', render: function (v, r) { return textCell('remark', r); } }, + { + title: ct('操作', 'action'), key: 'action', width: 100, fixed: 'right', align: 'center', + render: function (_, record) { + var ref = latestRefs.current; + var ds = getDetailStatus(record); + var isOwn = record.createdBy === ref.currentUser.id; + var isEditing = ref.editingKeys.indexOf(record.key) >= 0; + + function toggleEdit() { + ref.setEditingKeys(function (prev) { + if (prev.indexOf(record.key) >= 0) return prev.filter(function (k) { return k !== record.key; }); + return prev.concat([record.key]); + }); + } + + if (ds === 'bill_generated' || ds === 'approving') { + return React.createElement('span', { style: { color: '#94a3b8', fontSize: 12 } }, '账单锁定'); + } + + if (ds === 'approved') { + if (!ref.isSupervisor) return React.createElement('span', { style: { color: '#94a3b8', fontSize: 12 } }, '已锁定'); + return React.createElement(Space, { size: 4 }, + React.createElement(Button, { type: 'link', size: 'small', className: 'lc-action-btn', onClick: toggleEdit }, isEditing ? '完成' : '编辑'), + React.createElement(Popconfirm, { + title: '确认删除该条审批通过记录?', + onConfirm: function () { + ref.setAllRows(function (prev) { return prev.filter(function (r) { return r.key !== record.key; }); }); + message.success('已删除'); + } + }, React.createElement(Button, { type: 'link', size: 'small', className: 'lc-action-btn lc-action-btn-danger', danger: true }, '删除')) + ); + } + + if (isSavedDraftRow(record)) { + if (!ref.isSupervisor && !isOwn) return null; + return React.createElement(Space, { size: 4 }, + React.createElement(Button, { type: 'link', size: 'small', className: 'lc-action-btn', onClick: toggleEdit }, isEditing ? '完成' : '编辑'), + React.createElement(Popconfirm, { + title: '确认删除该条草稿?', + onConfirm: function () { + ref.setAllRows(function (prev) { return prev.filter(function (r) { return r.key !== record.key; }); }); + message.success('已删除'); + } + }, React.createElement(Button, { type: 'link', size: 'small', className: 'lc-action-btn lc-action-btn-danger', danger: true }, '删除')) + ); + } + + if (!ref.isSupervisor && isOwn) { + return React.createElement(Popconfirm, { + title: '确认删除该条待保存记录?', + onConfirm: function () { + ref.setAllRows(function (prev) { return prev.filter(function (r) { return r.key !== record.key; }); }); + message.success('已删除'); + } + }, React.createElement(Button, { type: 'link', size: 'small', className: 'lc-action-btn lc-action-btn-danger', danger: true }, '删除')); + } + return null; + } + } + ]; + }, [plateOptions, editingKeys, currentUser, isSupervisor, depositWarnKeys, contractRentWarnKeys]); + + var activePreviewBill = useMemo(function () { + if (!previewBills.length) return null; + var bill = previewBills[billPreviewIndex] || previewBills[0]; + if (!bill) return null; + var live = bills.filter(function (b) { return b.id === bill.id; })[0]; + return live || bill; + }, [previewBills, billPreviewIndex, bills]); + + var billManageColumns = useMemo(function () { + return [ + { + title: '状态', dataIndex: 'status', width: 96, align: 'center', + render: function (v) { + return React.createElement(Tag, { + color: v === 'approved' ? 'green' : v === 'approving' ? 'processing' : v === 'rejected' ? 'error' : 'default' + }, LEASE_BILL_STATUS_LABEL[v] || v); + } + }, + { title: '账单周期', dataIndex: 'billPeriod', width: 110, align: 'center' }, + { title: '客户名称', dataIndex: 'customerName', width: 160, ellipsis: true }, + { title: '项目名称', dataIndex: 'projectName', width: 180, ellipsis: true }, + { + title: '实收金额', dataIndex: 'receivedTotal', width: 100, align: 'right', + render: function (v) { return fmtMoney(v); } + }, + { + title: '操作', key: 'action', width: 220, fixed: 'right', align: 'center', + render: function (_, bill) { + var actions = []; + if (bill.status === 'preview' || bill.status === 'rejected') { + actions.push(React.createElement(Button, { + key: 'confirm', type: 'link', size: 'small', + onClick: function () { openBillPreviewFromManage(bill); } + }, '继续确认')); + } + if (bill.status !== 'approving' && bill.status !== 'cancelled' && bill.status !== 'approved') { + actions.push(React.createElement(Popconfirm, { + key: 'cancel', + title: '取消账单将释放明细为已保存,是否继续?', + onConfirm: function () { handleCancelBill(bill); } + }, React.createElement(Button, { type: 'link', size: 'small', danger: true }, '取消账单'))); + } + if (bill.status === 'approving') { + actions.push(React.createElement(Button, { + key: 'withdraw', type: 'link', size: 'small', + onClick: function () { handleWithdrawBill(bill); } + }, '撤回')); + } + if (bill.status === 'approved') { + actions.push(React.createElement(Button, { + key: 'withdraw-approved', type: 'link', size: 'small', + onClick: function () { + patchBillInLists(bill.id, { status: 'preview' }); + patchRowsByBillId(bill.id, { detailStatus: 'bill_generated', submitStatus: 'draft' }); + message.success('已撤回至待确认'); + } + }, '撤回')); + } + if (bill.status === 'approving') { + if (bill.invoiceMode === 'first' && bill.invoiceStep < 1) { + actions.push(React.createElement(Button, { + key: 'fin1', type: 'link', size: 'small', + onClick: function () { handleFinanceApproveBill(bill, 1); } + }, '财务·上传发票')); + } else { + actions.push(React.createElement(Button, { + key: 'fin2', type: 'link', size: 'small', + onClick: function () { handleFinanceApproveBill(bill, 2); } + }, '财务·确认到账')); + } + actions.push(React.createElement(Button, { + key: 'reject', type: 'link', size: 'small', danger: true, + onClick: function () { handleFinanceRejectBill(bill); } + }, '驳回')); + } + return React.createElement(Space, { size: 4, wrap: true }, actions); + } + } + ]; + }, [bills, openBillPreviewFromManage, handleCancelBill, handleWithdrawBill, handleFinanceApproveBill, handleFinanceRejectBill, patchBillInLists, patchRowsByBillId]); + + function renderBillAttach(label, file) { + if (!file) return React.createElement('span', { style: { color: '#94a3b8' } }, '—'); + return React.createElement('span', null, + React.createElement('a', { className: 'lc-bill-preview-attach-link', href: file.url || '#', onClick: function (e) { e.preventDefault(); message.info('预览:' + file.name); } }, '预览'), + React.createElement('a', { className: 'lc-bill-preview-attach-link', href: file.url || '#', onClick: function (e) { e.preventDefault(); message.success('已开始下载:' + file.name); } }, '下载') + ); + } + + function renderBillPreviewBody(bill) { + if (!bill) return null; + var billDateText = bill.billDate && window.dayjs ? window.dayjs(bill.billDate).format('YYYY-MM-DD') : '-'; + var lineCount = (bill.lines || []).length; + var invoiceLabel = LEASE_INVOICE_MODE_LABEL[bill.invoiceMode] || '-'; + var invoiceTagColor = bill.invoiceMode === 'first' ? 'purple' : 'blue'; + var lineCols = [ + { title: '车牌号', dataIndex: 'plateNo', width: 100, align: 'center' }, + { title: '账单日期', dataIndex: 'billDate', width: 110, align: 'center', render: function (v) { return v && window.dayjs ? window.dayjs(v).format('YYYY-MM-DD') : '-'; } }, + { title: '提车日期', dataIndex: 'pickupDate', width: 110, align: 'center', render: function (v) { return formatDate(v); } }, + { title: '退车日期', dataIndex: 'returnDate', width: 110, align: 'center', render: function (v) { return formatDate(v); } }, + { title: '起始日期', dataIndex: 'rentStart', width: 100, align: 'center' }, + { title: '应收合计', dataIndex: 'receivableTotal', width: 96, align: 'right', render: function (v) { return fmtMoney(v); } }, + { title: '减免金额', dataIndex: 'reductionAmount', width: 96, align: 'right', render: function (v) { return fmtMoney(v); } }, + { title: '实收金额', dataIndex: 'receivedAmount', width: 96, align: 'right', render: function (v) { return fmtMoney(v); } } + ]; + return React.createElement('div', { className: 'lc-bill-preview-panel' }, + React.createElement('div', { className: 'lc-bill-preview-hero' }, + React.createElement('div', { className: 'lc-bill-preview-hero__main' }, + React.createElement('div', { className: 'lc-bill-preview-hero__customer' }, bill.customerName || '-'), + React.createElement('div', { className: 'lc-bill-preview-hero__project' }, bill.projectName || '-') + ), + React.createElement('div', { className: 'lc-bill-preview-hero__meta' }, + React.createElement('div', null, bill.billPeriod || '-'), + React.createElement('div', null, '账单日 ' + billDateText) + ) + ), + React.createElement('div', { className: 'lc-bill-preview-stats', role: 'group', 'aria-label': '账单摘要' }, + React.createElement('div', { className: 'lc-bill-preview-stat lc-bill-preview-stat--amount' }, + React.createElement('div', { className: 'lc-bill-preview-stat__label' }, '实收总额'), + React.createElement('div', { className: 'lc-bill-preview-stat__value lc-bill-preview-stat__value--money' }, fmtMoney(bill.receivedTotal)) + ), + React.createElement('div', { className: 'lc-bill-preview-stat lc-bill-preview-stat--invoice' }, + React.createElement('div', { className: 'lc-bill-preview-stat__label' }, '开票方式'), + React.createElement('div', { className: 'lc-bill-preview-stat__value' }, + React.createElement(Tag, { color: invoiceTagColor, style: { margin: 0 } }, invoiceLabel) + ) + ), + React.createElement('div', { className: 'lc-bill-preview-stat lc-bill-preview-stat--arrival' }, + React.createElement('div', { className: 'lc-bill-preview-stat__label' }, '到账金额'), + React.createElement('div', { className: 'lc-bill-preview-stat__value' }, + bill.arrivalAmount != null ? fmtMoney(bill.arrivalAmount) : React.createElement('span', { style: { color: '#94a3b8', fontWeight: 500 } }, '待财务填写') + ) + ), + React.createElement('div', { className: 'lc-bill-preview-stat lc-bill-preview-stat--company' }, + React.createElement('div', { className: 'lc-bill-preview-stat__label' }, '签约公司'), + React.createElement('div', { className: 'lc-bill-preview-stat__value' }, bill.signCompany || '-') + ) + ), + React.createElement('div', { className: 'lc-bill-preview-attach-row' }, + React.createElement('div', { className: 'lc-bill-preview-attach' }, + React.createElement('div', { className: 'lc-bill-preview-attach__label' }, '收款凭据'), + React.createElement('div', { className: 'lc-bill-preview-attach__value' }, renderBillAttach('', bill.receiptAttachment)) + ), + React.createElement('div', { className: 'lc-bill-preview-attach' }, + React.createElement('div', { className: 'lc-bill-preview-attach__label' }, '发票附件'), + React.createElement('div', { className: 'lc-bill-preview-attach__value' }, renderBillAttach('', bill.invoiceAttachment)) + ) + ), + React.createElement('div', { className: 'lc-bill-preview-table-wrap' }, + React.createElement('div', { className: 'lc-bill-preview-table-head' }, + React.createElement('span', { className: 'lc-bill-preview-table-head__title' }, '账单明细'), + React.createElement('span', { className: 'lc-bill-preview-table-head__count' }, '共 ' + lineCount + ' 条') + ), + React.createElement(Table, { + size: 'small', bordered: false, pagination: false, rowKey: 'key', + columns: lineCols, dataSource: bill.lines || [], + scroll: { x: 'max-content' } + }) + ), + React.createElement('div', { className: 'lc-bill-preview-actions' }, + React.createElement(Button, { + style: { borderRadius: 8, fontWeight: 600 }, + onClick: function () { handleGenerateBillPdf(bill); } + }, '生成该对账单'), + bill.status === 'preview' || bill.status === 'rejected' ? React.createElement(Button, { + type: 'primary', style: LEASE_PRIMARY_BTN_STYLE, + onClick: function () { handleStartBillApproval(bill); } + }, '发起收款审批') : React.createElement(Button, { disabled: true, style: { borderRadius: 8 } }, + bill.status === 'approving' ? '审批中' : '已处理') + ) + ); + } + + return React.createElement( + App, + null, + React.createElement('style', null, LEASE_PAGE_STYLE + LEASE_LEDGER_TABLE_STYLE), + React.createElement('div', { className: 'lease-ledger-page' }, + React.createElement('div', { style: { display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' } }, + React.createElement('div', { style: { marginBottom: 16, display: 'flex', justifyContent: 'flex-end' } }, + React.createElement(Button, { + type: 'default', + icon: LEASE_ICONS.doc, + style: LEASE_REQ_BTN_STYLE, + onClick: function () { setRequirementModalOpen(true); }, + 'aria-label': '查看需求说明' + }, '查看需求说明') + ), + React.createElement(Card, { className: 'lc-filter-card', title: '筛选条件', bordered: false }, + React.createElement('div', { className: 'lc-filter-grid' }, + renderFilterField('年份', React.createElement(InputNumber, { + placeholder: '全部', min: 2020, max: 2099, style: { width: '100%' }, + value: yearDraft, onChange: setYearDraft + })), + renderFilterField('月份', React.createElement(InputNumber, { + placeholder: '全部', min: 1, max: 12, style: { width: '100%' }, + value: monthDraft, onChange: setMonthDraft + })), + renderFilterField('业务部门', React.createElement(Select, { + allowClear: true, placeholder: '全部', style: { width: '100%' }, + value: deptDraft, onChange: setDeptDraft, options: DEPT_OPTIONS, + dropdownStyle: { borderRadius: 8 } + })), + renderFilterField('业务员', React.createElement(Select, { + allowClear: true, showSearch: true, placeholder: '全部', style: { width: '100%' }, + value: salesmanDraft, onChange: setSalesmanDraft, + options: salesmanOptions, filterOption: filterOption, + dropdownStyle: { borderRadius: 8 } + })), + renderFilterField('车牌号码', React.createElement(Select, { + allowClear: true, showSearch: true, placeholder: '全部', style: { width: '100%' }, + value: plateDraft, onChange: setPlateDraft, + options: plateOptions, filterOption: filterOption, + dropdownStyle: { borderRadius: 8 } + })), + renderFilterField('客户名称', React.createElement(Input, { + allowClear: true, placeholder: '模糊搜索', style: { width: '100%', height: 32, borderRadius: 8 }, + value: customerDraft, + onChange: function (e) { setCustomerDraft(e.target.value); }, + onPressEnter: handleQuery + })), + renderFilterField('明细状态', React.createElement(Select, { + allowClear: true, placeholder: '全部', style: { width: '100%' }, + value: statusDraft, onChange: setStatusDraft, + options: DETAIL_STATUS_FILTER_OPTIONS, + dropdownStyle: { borderRadius: 8 } + })), + renderFilterField('账单日期', React.createElement(DatePicker.RangePicker, { + style: { width: '100%' }, format: 'YYYY-MM-DD', + value: billRangeDraft, onChange: setBillRangeDraft + })) + ), + React.createElement('div', { className: 'lc-filter-actions' }, + React.createElement(Button, { onClick: handleReset, style: { borderRadius: 8 } }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleQuery, style: LEASE_PRIMARY_BTN_STYLE }, '查询') + ) + ), + + React.createElement('div', { className: 'lc-alert-stats-row' }, + LEASE_KPI_CARDS.map(function (card) { + var displayValue = card.format === 'count' + ? String(statSummary.count) + : fmtMoney(statSummary[card.key]); + var valueClass = card.key === 'profitLoss' + ? (statSummary.profitLoss >= 0 ? 'lc-stat-val--profit' : 'lc-stat-val--loss') + : ''; + return renderStatCard(card, displayValue, valueClass); + }) + ), + + React.createElement('div', { className: 'lc-table-section' }, + React.createElement('div', { className: 'lc-table-toolbar' }, + React.createElement('div', { className: 'lc-table-legend-outer' }, + React.createElement('span', { className: 'lc-table-legend-label' }, '行底色:'), + React.createElement('span', { className: 'lc-table-legend-item' }, + React.createElement('span', { className: 'lc-table-legend-dot', style: { background: '#f1f5f9', border: '1px solid #e2e8f0' } }), + '待保存' + ), + React.createElement('span', { className: 'lc-table-legend-item' }, + React.createElement('span', { className: 'lc-table-legend-dot', style: { background: '#fff', border: '1px solid #e2e8f0' } }), + '已保存' + ), + React.createElement('span', { className: 'lc-table-legend-item' }, + React.createElement('span', { className: 'lc-table-legend-dot', style: { background: '#f5f3ff', border: '1px solid #ddd6fe' } }), + '已生成账单/审批中' + ), + React.createElement('span', { className: 'lc-table-legend-item' }, + React.createElement('span', { className: 'lc-table-legend-dot', style: { background: '#ecfdf5', border: '1px solid #a7f3d0' } }), + '审批通过' + ), + React.createElement('span', { style: { color: '#94a3b8', marginLeft: 4 } }, '表头 ? 可查看字段说明与计算公式'), + React.createElement('span', { style: { color: '#94a3b8' } }, '共 ' + visibleRows.length + ' 条') + ), + React.createElement('div', { className: 'lc-table-toolbar-actions' }, + !isSupervisor ? React.createElement(Button, { + type: 'default', + icon: LEASE_ICONS.upload, + style: LEASE_OUTLINE_BTN_STYLE, + onClick: function () { setImportModalOpen(true); }, + 'aria-label': '导入租赁业务台账' + }, '导入') : null, + !isSupervisor ? React.createElement(Button, { + onClick: function () { setBillManageOpen(true); }, + style: { borderRadius: 8, fontWeight: 600 } + }, '账单管理') : null, + !isSupervisor ? React.createElement(Button, { + onClick: handleSave, + style: { borderRadius: 8, fontWeight: 600 } + }, '保存') : null, + !isSupervisor ? React.createElement(Button, { + type: 'primary', + style: LEASE_PRIMARY_BTN_STYLE, + onClick: handleSubmitCollectionApproval, + disabled: !selectedRowKeys.length, + 'aria-label': '提交收款审批' + }, '提交收款审批') : null + ) + ), + React.createElement('div', { className: 'lc-table-card' }, + React.createElement('div', { className: 'lease-ledger-table-wrap' }, + React.createElement(Table, { + className: 'lease-ledger-table', + size: 'small', + bordered: true, + rowKey: 'key', + columns: columns, + dataSource: visibleRows, + pagination: false, + showHeader: true, + scroll: { x: 'max-content', y: 'calc(100vh - 500px)' }, + sticky: true, + rowSelection: !isSupervisor ? { + selectedRowKeys: selectedRowKeys, + onChange: function (keys) { setSelectedRowKeys(keys); }, + getCheckboxProps: function (record) { + return { disabled: !canSelectDetailRow(record) }; + }, + columnWidth: 40 + } : undefined, + rowClassName: function (r) { + return 'lease-row-tier-' + (r.displayTier != null ? r.displayTier : rowDisplaySortTier(r)); + }, + locale: { + emptyText: React.createElement('div', { style: { padding: '32px 0', color: '#94a3b8' } }, '暂无符合筛选条件的台账记录') + } + }) + ), + !isSupervisor ? React.createElement(Button, { + type: 'dashed', + block: true, + className: 'lease-add-row-btn', + icon: LEASE_ICONS.plus, + onClick: addRow + }, '新增一行') : null + ) + ) + ), + + React.createElement(Modal, { + className: 'h2-prd-modal', + title: React.createElement('span', { style: { fontWeight: 700 } }, '需求说明 · 租赁业务台账'), + open: requirementModalOpen, + onCancel: function () { setRequirementModalOpen(false); }, + width: 720, + centered: true, + destroyOnClose: true, + styles: { body: { maxHeight: '72vh', overflow: 'auto', paddingTop: 8, paddingBottom: 16 } }, + footer: React.createElement(Button, { + onClick: function () { setRequirementModalOpen(false); }, + style: { borderRadius: 8 } + }, '关闭') + }, renderLeaseRequirementPanel()), + + React.createElement(Modal, { + title: '导入租赁业务台账', + open: importModalOpen, + onCancel: function () { setImportModalOpen(false); }, + footer: null, + width: 560, + centered: true, + destroyOnClose: true + }, + React.createElement('div', { className: 'h2-import-template-bar' }, + React.createElement('div', { className: 'h2-import-template-bar-text' }, + React.createElement('div', { style: { fontWeight: 700, marginBottom: 4, color: '#0f172a' } }, '第一步:下载 CSV 导入模板'), + '模板字段与台账主要列一致;填写后上传,导入记录为「待保存」状态,确认无误后点击保存,再勾选提交收款审批。' + ), + React.createElement(Button, { + type: 'primary', + ghost: true, + style: Object.assign({}, LEASE_OUTLINE_BTN_STYLE, { flexShrink: 0 }), + onClick: downloadImportTemplate + }, '下载模板') + ), + React.createElement(Alert, { + type: 'info', + showIcon: true, + style: { marginBottom: 14, borderRadius: 10 }, + message: '第二步:填写模板后上传文件', + description: '支持 .csv;上传后生成可编辑草稿,归属当前登录客服。' + }), + React.createElement(Upload.Dragger, { + accept: '.csv', + maxCount: 1, + showUploadList: false, + beforeUpload: handleImportFile + }, + React.createElement('p', { style: { margin: '8px 0 4px', fontWeight: 600, color: '#334155' } }, '点击或拖拽 CSV 文件到此处上传'), + React.createElement('p', { style: { margin: 0, fontSize: 12, color: '#94a3b8' } }, '单次上传一个文件') + ) + ), + + React.createElement(Drawer, { + title: '账单管理', + open: billManageOpen, + onClose: function () { setBillManageOpen(false); }, + width: 920, + destroyOnClose: false + }, + React.createElement(Table, { + size: 'small', + bordered: true, + rowKey: 'id', + columns: billManageColumns, + dataSource: bills.filter(function (b) { return b.status !== 'cancelled'; }), + pagination: { pageSize: 8, showSizeChanger: false }, + scroll: { x: 'max-content' }, + locale: { emptyText: '暂无账单,请勾选已保存明细后提交收款审批' } + }), + React.createElement(Alert, { + type: 'info', + showIcon: true, + style: { marginTop: 12, borderRadius: 8 }, + message: '审批说明(原型模拟)', + description: '后开:财务一次确认到账并上传凭据与发票;先开:财务分两步——先上传发票,再确认到账。审批中可撤回;非审批中可取消账单释放明细。' + }) + ), + + React.createElement(Modal, { + className: 'lc-bill-preview-modal', + title: '账单预览(第 ' + (billPreviewIndex + 1) + ' 份 / 共 ' + previewBills.length + ' 份)', + open: billPreviewOpen, + onCancel: function () { setBillPreviewOpen(false); }, + width: 920, + centered: true, + destroyOnClose: false, + styles: { body: { maxHeight: '78vh', overflow: 'auto' } }, + footer: previewBills.length > 1 ? React.createElement(Space, null, + React.createElement(Button, { + disabled: billPreviewIndex <= 0, + onClick: function () { setBillPreviewIndex(function (i) { return Math.max(0, i - 1); }); } + }, '上一份'), + React.createElement(Button, { + disabled: billPreviewIndex >= previewBills.length - 1, + onClick: function () { setBillPreviewIndex(function (i) { return Math.min(previewBills.length - 1, i + 1); }); } + }, '下一份'), + React.createElement(Button, { onClick: function () { setBillPreviewOpen(false); } }, '关闭') + ) : React.createElement(Button, { onClick: function () { setBillPreviewOpen(false); } }, '关闭') + }, renderBillPreviewBody(activePreviewBill)) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-login/index.tsx b/src/prototypes/oneos-web-login/index.tsx new file mode 100644 index 0000000..90afa5e --- /dev/null +++ b/src/prototypes/oneos-web-login/index.tsx @@ -0,0 +1,21 @@ +/** + * @name 登录 + * 自 ONE-OS web端 原稿复刻(1 个页面) + */ +import React from 'react'; +import Page1 from './pages/01-登录.jsx'; +import { OneosWebLegacyShell } from '../../common/oneosWebLegacy/OneosWebLegacyShell'; + +const pages = [ + { id: 'u767bu5f55', title: '登录', component: Page1 }, +]; + +export default function OneosWebLogin() { + return ( + + ); +} diff --git a/src/prototypes/oneos-web-login/pages/01-登录.jsx b/src/prototypes/oneos-web-login/pages/01-登录.jsx new file mode 100644 index 0000000..7d33e6e --- /dev/null +++ b/src/prototypes/oneos-web-login/pages/01-登录.jsx @@ -0,0 +1,457 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 参考 Arco Design Pro 登录页布局:左侧品牌区 + 右侧登录表单 +const Component = function () { + var antd = window.antd; + var useState = React.useState; + var useRef = React.useRef; + var useEffect = React.useEffect; + + var _state = useState({ username: '', password: '', remember: true, agreeTerms: false, phone: '', smsCode: '' }); + var formData = _state[0]; + var setFormData = _state[1]; + + var _mode = useState('account'); + var loginMode = _mode[0]; + var setLoginMode = _mode[1]; + + var _countdown = useState(0); + var countdown = _countdown[0]; + var setCountdown = _countdown[1]; + + var _hasRequestedCode = useState(false); + var hasRequestedCode = _hasRequestedCode[0]; + var setHasRequestedCode = _hasRequestedCode[1]; + + var timerRef = useRef(null); + + useEffect(function () { + return function () { + if (timerRef.current) { + clearInterval(timerRef.current); + } + }; + }, []); + + var handleChange = function (field, value) { + var next = {}; + next[field] = value; + setFormData(function (prev) { + return Object.assign({}, prev, next); + }); + }; + + var handleGetCode = function () { + if (countdown > 0) return; + setHasRequestedCode(true); + setCountdown(60); + timerRef.current = setInterval(function () { + setCountdown(function (prev) { + if (prev <= 1) { + clearInterval(timerRef.current); + timerRef.current = null; + return 0; + } + return prev - 1; + }); + }, 1000); + }; + + var handleSubmit = function () { + if (!formData.agreeTerms) { + antd.message.warning('请阅读并同意《用户服务协议》和《隐私政策》'); + return; + } + if (loginMode === 'phone') { + if (!formData.phone) { + antd.message.warning('请输入手机号'); + return; + } + if (!formData.smsCode) { + antd.message.warning('请输入验证码'); + return; + } + } else { + if (!formData.username) { + antd.message.warning('请输入用户名'); + return; + } + if (!formData.password) { + antd.message.warning('请输入密码'); + return; + } + } + antd.message.success('登录成功(原型演示)'); + if (window.$axure && typeof window.$axure.navigateToUrl === 'function') { + window.$axure.navigateToUrl('工作台'); + } else { + window.location.hash = '工作台'; + } + }; + + var switchToPhone = function (e) { + if (e && e.preventDefault) e.preventDefault(); + setLoginMode('phone'); + }; + var switchToAccount = function (e) { + if (e && e.preventDefault) e.preventDefault(); + setLoginMode('account'); + }; + + var styles = { + wrap: { + display: 'flex', + minHeight: '100vh', + width: '100%', + fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', + backgroundColor: '#f5f5f5' + }, + left: { + flex: '1', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + padding: '48px', + background: 'linear-gradient(135deg, #1d2129 0%, #2d3748 50%, #1d2129 100%)', + color: '#fff' + }, + leftContent: { + maxWidth: '400px', + textAlign: 'center' + }, + title: { + fontSize: '28px', + fontWeight: '600', + marginBottom: '16px', + letterSpacing: '0.5px' + }, + slogan: { + fontSize: '17px', + fontWeight: '500', + opacity: 0.92, + lineHeight: 1.65, + marginBottom: '20px', + letterSpacing: '0.02em' + }, + desc: { + fontSize: '14px', + opacity: 0.72, + lineHeight: 1.6 + }, + right: { + width: '480px', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + padding: '48px', + backgroundColor: '#fff', + boxShadow: '-4px 0 24px rgba(0,0,0,0.06)' + }, + formCard: { + width: '100%', + maxWidth: '360px' + }, + formTitle: { + fontSize: '24px', + fontWeight: '600', + color: '#1d2129', + marginBottom: '8px', + textAlign: 'center' + }, + formSub: { + fontSize: '14px', + color: '#86909c', + marginBottom: '32px', + textAlign: 'center' + }, + formItem: { + marginBottom: '20px' + }, + label: { + display: 'block', + fontSize: '14px', + color: '#4e5969', + marginBottom: '8px' + }, + input: { + width: '100%', + height: '40px', + padding: '8px 12px', + fontSize: '14px', + border: '1px solid #e5e6eb', + borderRadius: '6px', + boxSizing: 'border-box', + outline: 'none' + }, + codeRow: { + display: 'flex', + gap: '8px', + alignItems: 'center' + }, + codeInput: { + flex: '1', + height: '40px', + padding: '8px 12px', + fontSize: '14px', + border: '1px solid #e5e6eb', + borderRadius: '6px', + boxSizing: 'border-box', + outline: 'none' + }, + getCodeBtn: { + flexShrink: 0, + height: '40px', + padding: '0 16px', + fontSize: '14px', + color: '#165dff', + backgroundColor: '#fff', + border: '1px solid #165dff', + borderRadius: '6px', + cursor: 'pointer' + }, + getCodeBtnDisabled: { + color: '#86909c', + borderColor: '#e5e6eb', + cursor: 'not-allowed' + }, + row: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: '24px' + }, + submitBtn: { + width: '100%', + height: '44px', + fontSize: '16px', + fontWeight: '500', + borderRadius: '6px' + }, + tabBar: { + display: 'flex', + marginBottom: '24px', + borderBottom: '1px solid #e5e6eb' + }, + tab: { + flex: 1, + textAlign: 'center', + padding: '12px 0', + fontSize: '15px', + color: '#86909c', + cursor: 'pointer', + borderBottom: '2px solid transparent', + marginBottom: '-1px' + }, + tabActive: { + color: '#165dff', + fontWeight: '500', + borderBottomColor: '#165dff' + } + }; + + return React.createElement( + React.Fragment, + null, + React.createElement('style', { + dangerouslySetInnerHTML: { + __html: '@media (max-width: 768px) { .login-right { width: 100% !important; min-width: 100% !important; } .login-left { min-height: 200px !important; } .login-wrap { flex-direction: column !important; } }' + } + }), + React.createElement( + 'div', + { style: styles.wrap, className: 'login-wrap' }, + React.createElement( + 'div', + { style: styles.left, className: 'login-left' }, + React.createElement( + 'div', + { style: styles.leftContent }, + React.createElement('h1', { style: styles.title }, '羚牛氢能'), + React.createElement('p', { style: styles.slogan }, '氢能资产数字化,业务运营一体化。'), + React.createElement('p', { style: styles.desc }, '数字化资产 · ONE-OS 运管平台') + ) + ), + React.createElement( + 'div', + { style: styles.right, className: 'login-right' }, + React.createElement( + 'div', + { style: styles.formCard }, + React.createElement('h2', { style: styles.formTitle }, '欢迎登录'), + React.createElement( + 'div', + { style: styles.tabBar }, + React.createElement( + 'div', + { + style: loginMode === 'account' ? Object.assign({}, styles.tab, styles.tabActive) : styles.tab, + onClick: switchToAccount, + role: 'button', + tabIndex: 0 + }, + '账号密码登录' + ), + React.createElement( + 'div', + { + style: loginMode === 'phone' ? Object.assign({}, styles.tab, styles.tabActive) : styles.tab, + onClick: switchToPhone, + role: 'button', + tabIndex: 0 + }, + '手机号登录' + ) + ), + React.createElement('p', { style: styles.formSub }, loginMode === 'phone' ? '请输入手机号与验证码' : '请输入您的账号与密码'), + loginMode === 'phone' + ? React.createElement( + React.Fragment, + null, + React.createElement( + 'div', + { style: styles.formItem }, + React.createElement('label', { style: styles.label }, '手机号'), + React.createElement('input', { + style: styles.input, + placeholder: '请输入手机号', + value: formData.phone, + onChange: function (e) { + handleChange('phone', e.target.value); + } + }) + ), + React.createElement( + 'div', + { style: styles.formItem }, + React.createElement('label', { style: styles.label }, '验证码'), + React.createElement( + 'div', + { style: styles.codeRow }, + React.createElement('input', { + style: styles.codeInput, + placeholder: '请输入验证码', + value: formData.smsCode, + onChange: function (e) { + handleChange('smsCode', e.target.value); + } + }), + React.createElement( + 'button', + { + type: 'button', + style: countdown > 0 ? Object.assign({}, styles.getCodeBtn, styles.getCodeBtnDisabled) : styles.getCodeBtn, + disabled: countdown > 0, + onClick: handleGetCode + }, + countdown > 0 ? countdown + '秒后重新获取' : (hasRequestedCode ? '再次获取' : '获取验证码') + ) + ) + ) + ) + : React.createElement( + React.Fragment, + null, + React.createElement( + 'div', + { style: styles.formItem }, + React.createElement('label', { style: styles.label }, '用户名'), + React.createElement('input', { + style: styles.input, + placeholder: '请输入用户名', + value: formData.username, + onChange: function (e) { + handleChange('username', e.target.value); + } + }) + ), + React.createElement( + 'div', + { style: styles.formItem }, + React.createElement('label', { style: styles.label }, '密码'), + React.createElement('input', { + style: styles.input, + type: 'password', + placeholder: '请输入密码', + value: formData.password, + onChange: function (e) { + handleChange('password', e.target.value); + } + }) + ) + ), + React.createElement( + 'div', + { style: styles.row }, + React.createElement( + 'label', + { style: { display: 'flex', alignItems: 'center', cursor: 'pointer', fontSize: '14px', color: '#4e5969' } }, + React.createElement('input', { + type: 'checkbox', + checked: formData.remember, + onChange: function (e) { + handleChange('remember', e.target.checked); + }, + style: { marginRight: '8px' } + }), + '记住我' + ) + ), + React.createElement( + 'label', + { + style: { + display: 'flex', + alignItems: 'flex-start', + cursor: 'pointer', + fontSize: '14px', + color: '#4e5969', + marginBottom: '20px', + lineHeight: 1.5 + } + }, + React.createElement('input', { + type: 'checkbox', + checked: formData.agreeTerms, + onChange: function (e) { + handleChange('agreeTerms', e.target.checked); + }, + style: { marginRight: '8px', marginTop: '3px', flexShrink: 0 } + }), + React.createElement( + 'span', + null, + '阅读并同意', + React.createElement('a', { href: '#', style: { color: '#165dff', marginLeft: '2px' }, onClick: function (e) { e.preventDefault(); } }, '《用户服务协议》'), + '和', + React.createElement('a', { href: '#', style: { color: '#165dff' }, onClick: function (e) { e.preventDefault(); } }, '《隐私政策》') + ) + ), + antd && antd.Button + ? React.createElement(antd.Button, { + type: 'primary', + block: true, + size: 'large', + style: styles.submitBtn, + onClick: handleSubmit + }, '登 录') + : React.createElement( + 'button', + { + style: Object.assign({}, styles.submitBtn, { + backgroundColor: '#165dff', + color: '#fff', + border: 'none', + cursor: 'pointer' + }), + onClick: handleSubmit + }, + '登 录' + ) + ) + ) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-ops/index.tsx b/src/prototypes/oneos-web-ops/index.tsx new file mode 100644 index 0000000..c45dfcb --- /dev/null +++ b/src/prototypes/oneos-web-ops/index.tsx @@ -0,0 +1,117 @@ +/** + * @name 运维管理 + * 自 ONE-OS web端 原稿复刻(49 个页面) + */ +import React from 'react'; +import Page1 from './pages/01-备件库存.jsx'; +import Page2 from './pages/02-仓库管理.jsx'; +import Page3 from './pages/03-备车-编辑.jsx'; +import Page4 from './pages/04-备车-查看.jsx'; +import Page5 from './pages/05-备车-新增.jsx'; +import Page6 from './pages/06-备车管理.jsx'; +import Page7 from './pages/07-查看故障.jsx'; +import Page8 from './pages/08-调拨管理-查看.jsx'; +import Page9 from './pages/09-调拨管理-调拨信息.jsx'; +import Page10 from './pages/10-调拨管理-接收信息-第三方运输.jsx'; +import Page11 from './pages/11-调拨管理-接收信息-司机运输.jsx'; +import Page12 from './pages/12-调拨管理-新增.jsx'; +import Page13 from './pages/13-调拨管理-业务提交-编辑.jsx'; +import Page14 from './pages/14-调拨管理-运维(调拨方)-编辑.jsx'; +import Page15 from './pages/15-调拨管理-运维(接收)-编辑.jsx'; +import Page16 from './pages/16-调拨管理.jsx'; +import Page17 from './pages/17-故障管理-编辑.jsx'; +import Page18 from './pages/18-故障管理.jsx'; +import Page19 from './pages/19-还车管理-查看.jsx'; +import Page20 from './pages/20-还车管理-还车.jsx'; +import Page21 from './pages/21-还车管理.jsx'; +import Page22 from './pages/22-后装设备.jsx'; +import Page23 from './pages/23-交车管理-编辑抽屉.jsx'; +import Page24 from './pages/24-交车管理-交车单-编辑.jsx'; +import Page25 from './pages/25-交车管理-交车单-查看.jsx'; +import Page26 from './pages/26-交车管理-交车单.jsx'; +import Page27 from './pages/27-交车管理.jsx'; +import Page28 from './pages/28-年审管理-办理.jsx'; +import Page29 from './pages/29-年审管理-查看.jsx'; +import Page30 from './pages/30-年审管理.jsx'; +import Page31 from './pages/31-上牌管理.jsx'; +import Page32 from './pages/32-替换车管理-编辑.jsx'; +import Page33 from './pages/33-替换车管理-查看.jsx'; +import Page34 from './pages/34-替换车管理-新增.jsx'; +import Page35 from './pages/35-替换车管理.jsx'; +import Page36 from './pages/36-新增故障.jsx'; +import Page37 from './pages/37-新增后装设备.jsx'; +import Page38 from './pages/38-异动管理-编辑.jsx'; +import Page39 from './pages/39-异动管理-查看.jsx'; +import Page40 from './pages/40-异动管理-结束异动.jsx'; +import Page41 from './pages/41-异动管理-新增.jsx'; +import Page42 from './pages/42-异动管理.jsx'; +import Page43 from './pages/43-证照管理-编辑.jsx'; +import Page44 from './pages/44-证照管理.jsx'; +import Page45 from './pages/45-停车场管理.jsx'; +import Page46 from './pages/46-型号参数-编辑.jsx'; +import Page47 from './pages/47-型号参数-查看.jsx'; +import Page48 from './pages/48-型号参数-新增.jsx'; +import Page49 from './pages/49-型号参数.jsx'; +import { OneosWebLegacyShell } from '../../common/oneosWebLegacy/OneosWebLegacyShell'; + +const pages = [ + { id: 'u8fd0u7ef4u7ba1u7406-u5907u4ef6u7ba1u7406-u5907u', title: '备件库存', component: Page1 }, + { id: 'u8fd0u7ef4u7ba1u7406-u5907u4ef6u7ba1u7406-u4ed3u', title: '仓库管理', component: Page2 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u5907u', title: '备车-编辑', component: Page3 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u5907u-4', title: '备车-查看', component: Page4 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u5907u-5', title: '备车-新增', component: Page5 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u5907u-6', title: '备车管理', component: Page6 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u67e5u', title: '查看故障', component: Page7 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u8c03u', title: '调拨管理-查看', component: Page8 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u8c03u-9', title: '调拨管理-调拨信息', component: Page9 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u8c03u-10', title: '调拨管理-接收信息-第三方运输', component: Page10 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u8c03u-11', title: '调拨管理-接收信息-司机运输', component: Page11 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u8c03u-12', title: '调拨管理-新增', component: Page12 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u8c03u-13', title: '调拨管理-业务提交-编辑', component: Page13 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u8c03u-14', title: '调拨管理-运维(调拨方)-编辑', component: Page14 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u8c03u-15', title: '调拨管理-运维(接收)-编辑', component: Page15 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u8c03u-16', title: '调拨管理', component: Page16 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u6545u', title: '故障管理-编辑', component: Page17 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u6545u-18', title: '故障管理', component: Page18 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u8fd8u', title: '还车管理-查看', component: Page19 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u8fd8u-20', title: '还车管理-还车', component: Page20 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u8fd8u-21', title: '还车管理', component: Page21 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u540eu', title: '后装设备', component: Page22 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u4ea4u', title: '交车管理-编辑抽屉', component: Page23 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u4ea4u-24', title: '交车管理-交车单-编辑', component: Page24 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u4ea4u-25', title: '交车管理-交车单-查看', component: Page25 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u4ea4u-26', title: '交车管理-交车单', component: Page26 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u4ea4u-27', title: '交车管理', component: Page27 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u5e74u', title: '年审管理-办理', component: Page28 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u5e74u-29', title: '年审管理-查看', component: Page29 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u5e74u-30', title: '年审管理', component: Page30 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u4e0au', title: '上牌管理', component: Page31 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u66ffu', title: '替换车管理-编辑', component: Page32 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u66ffu-33', title: '替换车管理-查看', component: Page33 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u66ffu-34', title: '替换车管理-新增', component: Page34 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u66ffu-35', title: '替换车管理', component: Page35 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u65b0u', title: '新增故障', component: Page36 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u65b0u-37', title: '新增后装设备', component: Page37 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u5f02u', title: '异动管理-编辑', component: Page38 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u5f02u-39', title: '异动管理-查看', component: Page39 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u5f02u-40', title: '异动管理-结束异动', component: Page40 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u5f02u-41', title: '异动管理-新增', component: Page41 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u5f02u-42', title: '异动管理', component: Page42 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u8bc1u', title: '证照管理-编辑', component: Page43 }, + { id: 'u8fd0u7ef4u7ba1u7406-u8f66u8f86u4e1au52a1-u8bc1u-44', title: '证照管理', component: Page44 }, + { id: 'u8fd0u7ef4u7ba1u7406-u57fau672cu6570u636eu7ef4u6', title: '停车场管理', component: Page45 }, + { id: 'u8fd0u7ef4u7ba1u7406-u57fau672cu6570u636eu7ef4u6-46', title: '型号参数-编辑', component: Page46 }, + { id: 'u8fd0u7ef4u7ba1u7406-u57fau672cu6570u636eu7ef4u6-47', title: '型号参数-查看', component: Page47 }, + { id: 'u8fd0u7ef4u7ba1u7406-u57fau672cu6570u636eu7ef4u6-48', title: '型号参数-新增', component: Page48 }, + { id: 'u8fd0u7ef4u7ba1u7406-u57fau672cu6570u636eu7ef4u6-49', title: '型号参数', component: Page49 }, +]; + +export default function OneosWebOps() { + return ( + + ); +} diff --git a/src/prototypes/oneos-web-ops/pages/01-备件库存.jsx b/src/prototypes/oneos-web-ops/pages/01-备件库存.jsx new file mode 100644 index 0000000..9511bf2 --- /dev/null +++ b/src/prototypes/oneos-web-ops/pages/01-备件库存.jsx @@ -0,0 +1,1220 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 数字化资产 ONEOS 运管平台 - 运维管理 - 备件库 - 备件库存(原型) + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Button = antd.Button; + var Table = antd.Table; + var Select = antd.Select; + var Modal = antd.Modal; + var Drawer = antd.Drawer; + var Form = antd.Form; + var Input = antd.Input; + var InputNumber = antd.InputNumber; + var DatePicker = antd.DatePicker; + var message = antd.message; + var Space = antd.Space; + var Descriptions = antd.Descriptions; + var Upload = antd.Upload; + var Image = antd.Image; + + var dayjs = window.dayjs; + + function normUploadFileList(e) { + if (Array.isArray(e)) return e; + return e && e.fileList ? e.fileList : []; + } + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + function todayDayjs() { + if (dayjs) return dayjs().startOf('day'); + return null; + } + + function todayYMD() { + if (dayjs) return dayjs().format('YYYY-MM-DD'); + var d = new Date(); + return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); + } + + function csvEscape(s) { + var x = String(s == null ? '' : s); + if (/[",\n\r]/.test(x)) return '"' + x.replace(/"/g, '""') + '"'; + return x; + } + + function downloadCsv(filename, headers, rows) { + var lines = [headers.map(csvEscape).join(',')].concat( + rows.map(function (r) { return r.map(csvEscape).join(','); }) + ); + var blob = new Blob(['\uFEFF' + lines.join('\n')], { type: 'text/csv;charset=utf-8;' }); + var a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = filename; + a.click(); + URL.revokeObjectURL(a.href); + } + + var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var filterLabelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var filterItemStyle = { marginBottom: 12 }; + var filterControlStyle = { width: '100%' }; + var tableStyle = '.spare-stock-table .ant-table-thead th,.spare-stock-table .ant-table-tbody td{white-space:nowrap;}' + + '.spare-stock-table .col-stock-qty{text-align:left !important;}' + + '.spare-stock-table th.col-stock-qty{text-align:left !important;}'; + + // 与「仓库管理」一致的仓库名称选项(入库/领用出库) + var warehouseManageNames = useMemo(function () { + return [ + '天河备件库', '西湖服务备件库', '上海浦东备件仓', '深圳南山备件仓', + '成都高新备件库', '宁波江北备件库', '北京朝阳备件仓', '重庆两江备件库' + ]; + }, []); + + var vehicleModelSpecMap = { + '飞驰49t': '海格牌KLQ5180XYKFCEV', + '宇通49t': 'KLQ6129', + '现代4.5t': 'BJ5180', + '跃进4.5t': '帅铃Q6', + '苏龙18t': '海格牌KLQ5180XYKFCEV' + }; + var allVehicleModelOptions = useMemo(function () { + return [ + { value: '飞驰49t', label: '飞驰49t' }, + { value: '宇通49t', label: '宇通49t' }, + { value: '现代4.5t', label: '现代4.5t' }, + { value: '跃进4.5t', label: '跃进4.5t' }, + { value: '苏龙18t', label: '苏龙18t' } + ]; + }, []); + var addDrawerVehicleOptions = useMemo(function () { + return allVehicleModelOptions.map(function (opt) { + var spec = vehicleModelSpecMap[opt.value]; + return { + value: opt.value, + label: spec ? opt.value + '(型号:' + spec + ')' : opt.label + }; + }); + }, [allVehicleModelOptions]); + + function buildInitialStockRows() { + var XJ = '新件库'; + var XJCode = 'WH-XJK'; + var YS = '易损件库'; + var YSCode = 'WH-YSK'; + return [ + { id: 'ss-xj-01', warehouseCode: XJCode, warehouseName: XJ, spareCode: 'FC49t-XJ-0001', spareName: '去离子柱(飞驰49t-新件)', vehicleModels: ['飞驰49t'], stockQty: 34, totalInbound: 42, totalOutbound: 8 }, + { id: 'ss-xj-02', warehouseCode: XJCode, warehouseName: XJ, spareCode: 'FC49t-XJ-0002', spareName: '燃料电池空气滤芯(飞驰49t-新件)', vehicleModels: ['飞驰49t'], stockQty: 28, totalInbound: 55, totalOutbound: 27 }, + { id: 'ss-xj-03', warehouseCode: XJCode, warehouseName: XJ, spareCode: 'YT49t-XJ-0001', spareName: '去离子柱(宇通49t-新件)', vehicleModels: ['宇通49t'], stockQty: 20, totalInbound: 28, totalOutbound: 8 }, + { id: 'ss-xj-04', warehouseCode: XJCode, warehouseName: XJ, spareCode: 'YT49t-XJ-0002', spareName: '燃料电池空气滤芯(宇通49t-新件)', vehicleModels: ['宇通49t'], stockQty: 12, totalInbound: 18, totalOutbound: 6 }, + { id: 'ss-xj-05', warehouseCode: XJCode, warehouseName: XJ, spareCode: 'YT49t-XJ-0005', spareName: '燃料电池去离子水(宇通49t-新件)', vehicleModels: ['宇通49t'], stockQty: 5, totalInbound: 5, totalOutbound: 0 }, + { id: 'ss-xj-06', warehouseCode: XJCode, warehouseName: XJ, spareCode: 'YT49t-XJ-0029', spareName: '小循环滤网(宇通49t-新件)', vehicleModels: ['宇通49t'], stockQty: 11, totalInbound: 13, totalOutbound: 2 }, + { id: 'ss-xj-07', warehouseCode: XJCode, warehouseName: XJ, spareCode: 'YT49t-XJ-0030', spareName: '大循环滤网(宇通49t-新件)', vehicleModels: ['宇通49t'], stockQty: 10, totalInbound: 10, totalOutbound: 0 }, + { id: 'ss-xj-08', warehouseCode: XJCode, warehouseName: XJ, spareCode: 'XD4.5t-XJ-0008', spareName: '后尾灯(现代4.5t-新件)', vehicleModels: ['现代4.5t'], stockQty: 18, totalInbound: 40, totalOutbound: 22 }, + { id: 'ss-xj-09', warehouseCode: XJCode, warehouseName: XJ, spareCode: 'YJ4.5t-XJ-0035', spareName: '12V蓄电池(跃进4.5t-新件)', vehicleModels: ['跃进4.5t'], stockQty: 23, totalInbound: 26, totalOutbound: 3 }, + { id: 'ss-xj-10', warehouseCode: XJCode, warehouseName: XJ, spareCode: 'SL18t-XJ-0001', spareName: '去离子柱(苏龙18t-新件)', vehicleModels: ['苏龙18t'], stockQty: 11, totalInbound: 12, totalOutbound: 1 }, + { id: 'ss-xj-11', warehouseCode: XJCode, warehouseName: XJ, spareCode: 'SL18t-XJ-0002', spareName: '燃料电池空气滤芯(苏龙18t-新件)', vehicleModels: ['苏龙18t'], stockQty: 16, totalInbound: 16, totalOutbound: 0 }, + { id: 'ss-ys-01', warehouseCode: YSCode, warehouseName: YS, spareCode: 'TY-YS-0001', spareName: 'H1灯泡-易损件', vehicleModels: [], stockQty: 55, totalInbound: 55, totalOutbound: 0 }, + { id: 'ss-ys-02', warehouseCode: YSCode, warehouseName: YS, spareCode: 'TY-YS-0002', spareName: 'H3灯泡-易损件', vehicleModels: [], stockQty: 50, totalInbound: 50, totalOutbound: 0 }, + { id: 'ss-ys-03', warehouseCode: YSCode, warehouseName: YS, spareCode: 'TY-YS-0003', spareName: 'H7灯泡-易损件', vehicleModels: [], stockQty: 53, totalInbound: 53, totalOutbound: 0 }, + { id: 'ss-ys-04', warehouseCode: YSCode, warehouseName: YS, spareCode: 'TY-YS-0004', spareName: '小灯泡-易损件', vehicleModels: [], stockQty: 52, totalInbound: 52, totalOutbound: 0 }, + { id: 'ss-ys-05', warehouseCode: YSCode, warehouseName: YS, spareCode: 'TY-YS-0005', spareName: '2405灯泡-易损件', vehicleModels: [], stockQty: 110, totalInbound: 110, totalOutbound: 0 } + ]; + } + + function buildSpareCatalogFromRows(rows) { + var map = {}; + (rows || []).forEach(function (r) { + if (!r || !r.spareCode || map[r.spareCode]) return; + var isBulb = /灯泡/.test(r.spareName || ''); + map[r.spareCode] = { + spareCode: r.spareCode, + spareName: r.spareName, + unit: isBulb ? '个' : '件', + vehicleModels: (r.vehicleModels || []).slice(), + alertThreshold: isBulb ? 20 : 8 + }; + }); + return map; + } + + var listState = useState(buildInitialStockRows); + var list = listState[0]; + + var spareCatalogState = useState(function () { + return buildSpareCatalogFromRows(buildInitialStockRows()); + }); + var spareCatalog = spareCatalogState[0]; + var setSpareCatalog = spareCatalogState[1]; + + function getCatalogEntry(spareCode) { + return spareCatalog[spareCode] || null; + } + + function getUnitForCode(spareCode) { + var c = getCatalogEntry(spareCode); + return (c && c.unit) || '件'; + } + + var inboundRecordsState = useState(function () { + var ph1 = 'https://via.placeholder.com/160x120.png?text=Inbound+1'; + var ph2 = 'https://via.placeholder.com/160x120.png?text=Inbound+2'; + return [ + { id: 'in-001', warehouseName: '新件库', spareCode: 'FC49t-XJ-0001', spareName: '去离子柱(飞驰49t-新件)', inboundType: '采购', qty: 4, inboundDate: '2025-02-27', operator: '张三', unitPrice: 128.5, remark: '季度补货', photoUrls: [ph1] }, + { id: 'in-002', warehouseName: '新件库', spareCode: 'YT49t-XJ-0002', spareName: '燃料电池空气滤芯(宇通49t-新件)', inboundType: '采购', qty: 6, inboundDate: '2025-02-26', operator: '李四', unitPrice: 86, remark: '', photoUrls: [ph1, ph2] }, + { id: 'in-003', warehouseName: '易损件库', spareCode: 'TY-YS-0001', spareName: 'H1灯泡-易损件', inboundType: '其他', qty: 20, inboundDate: '2025-02-25', operator: '王五', unitPrice: 3.5, remark: '调拨转入', photoUrls: [] }, + { id: 'in-004', warehouseName: '新件库', spareCode: 'SL18t-XJ-0002', spareName: '燃料电池空气滤芯(苏龙18t-新件)', inboundType: '采购', qty: 8, inboundDate: '2025-02-24', operator: '张三', unitPrice: 92.8, remark: '', photoUrls: [ph2] }, + { id: 'in-005', warehouseName: '新件库', spareCode: 'FC49t-XJ-0001', spareName: '去离子柱(飞驰49t-新件)', inboundType: '其他', qty: 2, inboundDate: '2025-02-20', operator: '赵六', unitPrice: 0, remark: '盘盈', photoUrls: [] } + ]; + }); + var inboundRecords = inboundRecordsState[0]; + + var outboundRecordsState = useState(function () { + return [ + { id: 'out-001', warehouseName: '新件库', spareCode: 'FC49t-XJ-0002', spareName: '燃料电池空气滤芯(飞驰49t-新件)', outboundType: '维修', qty: 2, outboundDate: '2025-02-27', operator: '周强', remark: '车辆粤A12345' }, + { id: 'out-002', warehouseName: '新件库', spareCode: 'XD4.5t-XJ-0008', spareName: '后尾灯(现代4.5t-新件)', outboundType: '保养', qty: 1, outboundDate: '2025-02-26', operator: '钱敏', remark: '' }, + { id: 'out-003', warehouseName: '易损件库', spareCode: 'TY-YS-0003', spareName: 'H7灯泡-易损件', outboundType: '其他', qty: 5, outboundDate: '2025-02-25', operator: '孙亮', remark: '场站备用' }, + { id: 'out-004', warehouseName: '新件库', spareCode: 'YJ4.5t-XJ-0035', spareName: '12V蓄电池(跃进4.5t-新件)', outboundType: '维修', qty: 1, outboundDate: '2025-02-24', operator: '周强', remark: '' }, + { id: 'out-005', warehouseName: '新件库', spareCode: 'FC49t-XJ-0001', spareName: '去离子柱(飞驰49t-新件)', outboundType: '保养', qty: 1, outboundDate: '2025-02-23', operator: '钱敏', remark: '定期更换' }, + { id: 'out-006', warehouseName: '新件库', spareCode: 'FC49t-XJ-0002', spareName: '燃料电池空气滤芯(飞驰49t-新件)', outboundType: '维修', qty: 1, outboundDate: '2025-02-22', operator: '周强', remark: '粤B88888 进站' }, + { id: 'out-007', warehouseName: '新件库', spareCode: 'YT49t-XJ-0001', spareName: '去离子柱(宇通49t-新件)', outboundType: '维修', qty: 2, outboundDate: '2025-02-21', operator: '吴刚', remark: '' }, + { id: 'out-008', warehouseName: '新件库', spareCode: 'YT49t-XJ-0002', spareName: '燃料电池空气滤芯(宇通49t-新件)', outboundType: '保养', qty: 3, outboundDate: '2025-02-20', operator: '钱敏', remark: '车队批量保养' }, + { id: 'out-009', warehouseName: '新件库', spareCode: 'YT49t-XJ-0029', spareName: '小循环滤网(宇通49t-新件)', outboundType: '维修', qty: 1, outboundDate: '2025-02-19', operator: '郑华', remark: '泄漏检修更换' }, + { id: 'out-010', warehouseName: '新件库', spareCode: 'SL18t-XJ-0001', spareName: '去离子柱(苏龙18t-新件)', outboundType: '其他', qty: 1, outboundDate: '2025-02-18', operator: '孙亮', remark: '调拨至华东服务站' }, + { id: 'out-011', warehouseName: '新件库', spareCode: 'SL18t-XJ-0002', spareName: '燃料电池空气滤芯(苏龙18t-新件)', outboundType: '维修', qty: 2, outboundDate: '2025-02-17', operator: '周强', remark: '' }, + { id: 'out-012', warehouseName: '易损件库', spareCode: 'TY-YS-0001', spareName: 'H1灯泡-易损件', outboundType: '维修', qty: 8, outboundDate: '2025-02-16', operator: '赵磊', remark: '夜间抢修多车' }, + { id: 'out-013', warehouseName: '易损件库', spareCode: 'TY-YS-0001', spareName: 'H1灯泡-易损件', outboundType: '其他', qty: 4, outboundDate: '2025-02-15', operator: '孙亮', remark: '场站灯箱维护' }, + { id: 'out-014', warehouseName: '易损件库', spareCode: 'TY-YS-0002', spareName: 'H3灯泡-易损件', outboundType: '保养', qty: 6, outboundDate: '2025-02-14', operator: '钱敏', remark: '' }, + { id: 'out-015', warehouseName: '易损件库', spareCode: 'TY-YS-0004', spareName: '小灯泡-易损件', outboundType: '其他', qty: 10, outboundDate: '2025-02-13', operator: '吴刚', remark: '库存盘点出库' }, + { id: 'out-016', warehouseName: '易损件库', spareCode: 'TY-YS-0005', spareName: '2405灯泡-易损件', outboundType: '维修', qty: 12, outboundDate: '2025-02-12', operator: '郑华', remark: '批量更换老化灯泡' }, + { id: 'out-017', warehouseName: '新件库', spareCode: 'YT49t-XJ-0030', spareName: '大循环滤网(宇通49t-新件)', outboundType: '保养', qty: 2, outboundDate: '2025-02-11', operator: '钱敏', remark: '粤C22334' }, + { id: 'out-018', warehouseName: '新件库', spareCode: 'YT49t-XJ-0005', spareName: '燃料电池去离子水(宇通49t-新件)', outboundType: '其他', qty: 1, outboundDate: '2025-02-10', operator: '孙亮', remark: '试验用料' }, + { id: 'out-019', warehouseName: '新件库', spareCode: 'FC49t-XJ-0001', spareName: '去离子柱(飞驰49t-新件)', outboundType: '维修', qty: 3, outboundDate: '2025-02-09', operator: '周强', remark: '质保期内更换' }, + { id: 'out-020', warehouseName: '新件库', spareCode: 'XD4.5t-XJ-0008', spareName: '后尾灯(现代4.5t-新件)', outboundType: '维修', qty: 2, outboundDate: '2025-02-08', operator: '赵磊', remark: '追尾事故修复' } + ]; + }); + var outboundRecords = outboundRecordsState[0]; + + var defaultDraft = { + warehouseName: undefined, + spareCode: undefined, + spareName: undefined, + vehicleModels: [] + }; + + var draftState = useState(Object.assign({}, defaultDraft)); + var draft = draftState[0]; + var setDraft = draftState[1]; + + var appliedState = useState(Object.assign({}, defaultDraft)); + var applied = appliedState[0]; + var setApplied = appliedState[1]; + + var pageState = useState(1); + var page = pageState[0]; + var setPage = pageState[1]; + + var pageSizeState = useState(10); + var pageSize = pageSizeState[0]; + var setPageSize = pageSizeState[1]; + + var requirementModalState = useState(false); + var requirementModalOpen = requirementModalState[0]; + var setRequirementModalOpen = requirementModalState[1]; + + var addDrawerState = useState(false); + var addDrawerOpen = addDrawerState[0]; + var setAddDrawerOpen = addDrawerState[1]; + + var inboundDrawerState = useState(false); + var inboundDrawerOpen = inboundDrawerState[0]; + var setInboundDrawerOpen = inboundDrawerState[1]; + + var outboundDrawerState = useState(false); + var outboundDrawerOpen = outboundDrawerState[0]; + var setOutboundDrawerOpen = outboundDrawerState[1]; + + var detailRowState = useState(null); + var detailRow = detailRowState[0]; + var setDetailRow = detailRowState[1]; + + var inboundHistRowState = useState(null); + var inboundHistRow = inboundHistRowState[0]; + var setInboundHistRow = inboundHistRowState[1]; + + var outboundHistRowState = useState(null); + var outboundHistRow = outboundHistRowState[0]; + var setOutboundHistRow = outboundHistRowState[1]; + + var histInboundPageState = useState(1); + var histInboundPage = histInboundPageState[0]; + var setHistInboundPage = histInboundPageState[1]; + var histInboundPageSizeState = useState(10); + var histInboundPageSize = histInboundPageSizeState[0]; + var setHistInboundPageSize = histInboundPageSizeState[1]; + + var histOutboundPageState = useState(1); + var histOutboundPage = histOutboundPageState[0]; + var setHistOutboundPage = histOutboundPageState[1]; + var histOutboundPageSizeState = useState(10); + var histOutboundPageSize = histOutboundPageSizeState[0]; + var setHistOutboundPageSize = histOutboundPageSizeState[1]; + + var addForm = Form.useForm()[0]; + var inboundForm = Form.useForm()[0]; + var outboundForm = Form.useForm()[0]; + + var requirementDocContent = [ + '一个「数字化资产ONEOS运管平台」中的「备件库存」模块', + '#面包屑:运维管理-备件库-备件库存', + '', + '1.筛选:', + '1.1.仓库名称:选择器,支持输入框内输入模糊搜索,下拉匹配正确项;', + '1.2.备件编码:选择器,支持输入框内输入模糊搜索,下拉匹配正确项;', + '1.3.备件名称:选择器,支持输入框内输入模糊搜索,下拉匹配正确项;', + '1.4.适配车型:选择器,支持多选,显示所有型号;', + '', + '2.列表;右侧为新增备件信息、入库、领用出库、批量导入、导出按钮', + '#列表:', + '2.1.序号:1.2.3.以此类推;', + '2.2.仓库编码:显示仓库编码;', + '2.3.仓库名称:显示仓库名称;', + '2.4.备件编码:显示备件编码;', + '2.5.备件名称:显示备件名称;', + '2.6.适配车型:显示适配车型,一个备件可能有多个车型,非必填;', + '2.7.库存数量:显示库存数量;', + '2.8.累积入库数量:显示该备件入库数量总和;', + '2.9.累积出库数量:显示该备件出库数量总和;', + '2.10.操作:备件明细、入库记录、出库记录;', + ' 2.10.1.备件明细:点击弹出抽屉,字段为:', + ' 2.10.1.1.备件编码:显示备件编码;', + ' 2.10.1.2.备件名称:显示备件名称;', + ' 2.10.1.3.计算单位:显示备件计算单位;', + ' 2.10.1.4.适配车型:显示备件适配车型;', + ' 2.10.1.5.告警阈值:显示备件告警阈值;', + ' 2.10.2.入库记录:点击弹出抽屉,显示列表,支持导出csv,列表字段为:', + ' 2.10.2.1.备件编码:显示备件编码;', + ' 2.10.2.2.备件名称:显示备件名称;', + ' 2.10.2.3.入库类型:显示入库类型,分为采购、其他;', + ' 2.10.2.4.入库数量:显示入库数量,单位根据备件明细单位显示,格式为:xx(单位);', + ' 2.10.2.5.入库日期:显示入库日期,YYYY-MM-DD;', + ' 2.10.2.6.入库人员:显示入库人员姓名;', + ' 2.10.2.7.单价:显示入库单价,格式为:xx.xx元;', + ' 2.10.2.8.入库照片:显示入库照片,点击可放大预览;', + ' 2.10.2.9.备注:显示备注信息;', + ' 2.10.3.出库记录:点击弹出抽屉,显示列表,支持导出csv,字段为:', + ' 2.10.3.1.备件编码:显示备件编码;', + ' 2.10.3.2.备件名称:显示备件名称;', + ' 2.10.3.3.出库类型:显示出库类型;', + ' 2.10.3.4.出库数量:显示出库数量,单位根据备件明细单位显示,格式为:xx(单位);', + ' 2.10.3.5.出库日期:显示出库日期,YYYY-MM-DD;', + ' 2.10.3.6.出库人员:显示出库人员姓名;', + ' 2.10.3.7.备注:显示备注信息;', + '2.11.右下角为分页功能,支持选择单页显示数据条数;', + '', + '3.新增备件信息', + '点击弹出抽屉,字段为:', + '3.1.备件编码:输入框,默认提示为请输入备件编码;', + '3.2.备件名称:输入框,默认提示为请输入备件名称;', + '3.3.计算单位:输入框,默认提示为请输入计量单位;', + '3.4.适配车型:非必填,选择器,支持多选,选项来自型号表,默认提示:请选择车型;', + '3.5.告警阈值:输入框,支持正数整数输入,支持输入框上下按钮调整数量,默认提示为请设置备件告警阈值;', + '', + '4.入库:', + '点击弹出抽屉,字段为:', + '4.1.仓库名称:选择器,选项来自于仓库管理;', + '4.2.备件名称:选择器,选项来自于备件管理;', + '4.3.入库类型:选择器,选项为:采购、其他;', + '4.4.入库数量:输入框,支持正数整数输入;支持点击增加减少变动数量;', + '4.5.入库日期:日期选择器,精确至天,默认为当日;', + '4.6.单价:输入框,支持2位小数输入,后缀为元;', + '4.7.入库照片:照片上传按钮,点击上传本地文件,上传后,支持预览和点击右上角删除按钮删除;', + '4.8.备注:文本域,支持自定义输入;', + '', + '5.领用出库:', + '点击弹出抽屉,字段为:', + '5.1.仓库名称:选择器,选项来自于仓库管理;', + '5.2.备件名称:选择器,选项来自于备件管理;', + '5.3.出库类型:选择器,选项为:维修、保养、其他;', + '5.4.出库数量:输入框,支持正数整数输入;支持点击增加减少变动数量;', + '5.5.出库日期:日期选择器,精确至天,默认为当天;', + '5.6.备注:文本域,支持自定义输入;', + '', + '6.批量导入:', + '根据入库字段,生成csv模板;', + '', + '7.导出:根据筛选条件自动导出csv文件;' + ].join('\n'); + + var warehouseNameOptions = useMemo(function () { + var set = new Set(); + warehouseManageNames.forEach(function (w) { set.add(w); }); + (list || []).forEach(function (r) { if (r && r.warehouseName) set.add(r.warehouseName); }); + return Array.from(set).map(function (v) { return { value: v, label: v }; }); + }, [list, warehouseManageNames]); + + var warehouseOptionsForForm = useMemo(function () { + return warehouseManageNames.map(function (w) { return { value: w, label: w }; }); + }, [warehouseManageNames]); + + var spareCodeOptions = useMemo(function () { + var set = new Set(); + (list || []).forEach(function (r) { if (r && r.spareCode) set.add(r.spareCode); }); + Object.keys(spareCatalog).forEach(function (k) { set.add(k); }); + return Array.from(set).map(function (v) { return { value: v, label: v }; }); + }, [list, spareCatalog]); + + var spareNameOptions = useMemo(function () { + var set = new Set(); + (list || []).forEach(function (r) { if (r && r.spareName) set.add(r.spareName); }); + Object.keys(spareCatalog).forEach(function (k) { + var c = spareCatalog[k]; + if (c && c.spareName) set.add(c.spareName); + }); + return Array.from(set).map(function (v) { return { value: v, label: v }; }); + }, [list, spareCatalog]); + + var spareSelectByNameOptions = useMemo(function () { + var byCode = {}; + Object.keys(spareCatalog).forEach(function (code) { + var c = spareCatalog[code]; + if (c) byCode[code] = { value: code, label: c.spareName + '(' + code + ')' }; + }); + (list || []).forEach(function (r) { + if (r && r.spareCode && !byCode[r.spareCode]) { + byCode[r.spareCode] = { value: r.spareCode, label: r.spareName + '(' + r.spareCode + ')' }; + } + }); + return Object.keys(byCode).map(function (k) { return byCode[k]; }); + }, [list, spareCatalog]); + + function rowMatchesVehicleModels(row, selected) { + if (!selected || selected.length === 0) return true; + var models = row.vehicleModels || []; + for (var i = 0; i < selected.length; i++) { + if (models.indexOf(selected[i]) >= 0) return true; + } + return false; + } + + var filtered = useMemo(function () { + return (list || []).filter(function (r) { + if (applied.warehouseName && r.warehouseName !== applied.warehouseName) return false; + if (applied.spareCode && r.spareCode !== applied.spareCode) return false; + if (applied.spareName && r.spareName !== applied.spareName) return false; + if (!rowMatchesVehicleModels(r, applied.vehicleModels)) return false; + return true; + }); + }, [list, applied]); + + var paged = useMemo(function () { + var start = (page - 1) * pageSize; + return filtered.slice(start, start + pageSize); + }, [filtered, page, pageSize]); + + var serialStart = (page - 1) * pageSize; + + var handleQuery = useCallback(function () { + setApplied({ + warehouseName: draft.warehouseName, + spareCode: draft.spareCode, + spareName: draft.spareName, + vehicleModels: draft.vehicleModels ? draft.vehicleModels.slice() : [] + }); + setPage(1); + }, [draft]); + + var handleReset = useCallback(function () { + setDraft(Object.assign({}, defaultDraft)); + setApplied(Object.assign({}, defaultDraft)); + setPage(1); + }, []); + + var handleAdd = useCallback(function () { + addForm.resetFields(); + setAddDrawerOpen(true); + }, [addForm]); + + var handleAddDrawerClose = useCallback(function () { + setAddDrawerOpen(false); + addForm.resetFields(); + }, [addForm]); + + var handleAddDrawerSubmit = useCallback(function () { + addForm.validateFields().then(function (vals) { + setSpareCatalog(function (prev) { + var next = Object.assign({}, prev); + next[vals.spareCode] = { + spareCode: vals.spareCode, + spareName: vals.spareName, + unit: vals.unit, + vehicleModels: vals.vehicleModels || [], + alertThreshold: vals.alertThreshold + }; + return next; + }); + message.success('已保存(原型,联调时对接建档接口)'); + setAddDrawerOpen(false); + addForm.resetFields(); + }).catch(function () {}); + }, [addForm, setSpareCatalog]); + + var handleOpenInboundForm = useCallback(function () { + inboundForm.resetFields(); + inboundForm.setFieldsValue({ + inboundDate: todayDayjs() || undefined, + qty: 1, + inboundType: '采购', + unitPrice: undefined, + remark: '', + inboundPhotos: [] + }); + setInboundDrawerOpen(true); + }, [inboundForm]); + + var handleInboundFormClose = useCallback(function () { + var fl = inboundForm.getFieldValue('inboundPhotos') || []; + fl.forEach(function (f) { + var u = f.url || f.thumbUrl; + if (u && String(u).indexOf('blob:') === 0) { + try { URL.revokeObjectURL(u); } catch (e) {} + } + }); + setInboundDrawerOpen(false); + inboundForm.resetFields(); + }, [inboundForm]); + + var handleInboundFormSubmit = useCallback(function () { + inboundForm.validateFields().then(function () { + message.success('入库已提交(原型,联调对接接口)'); + setInboundDrawerOpen(false); + inboundForm.resetFields(); + }).catch(function () {}); + }, [inboundForm]); + + var handleOpenOutboundForm = useCallback(function () { + outboundForm.resetFields(); + outboundForm.setFieldsValue({ + outboundDate: todayDayjs() || undefined, + qty: 1, + outboundType: '维修', + remark: '' + }); + setOutboundDrawerOpen(true); + }, [outboundForm]); + + var handleOutboundFormClose = useCallback(function () { + setOutboundDrawerOpen(false); + outboundForm.resetFields(); + }, [outboundForm]); + + var handleOutboundFormSubmit = useCallback(function () { + outboundForm.validateFields().then(function () { + message.success('领用出库已提交(原型,联调对接接口)'); + setOutboundDrawerOpen(false); + outboundForm.resetFields(); + }).catch(function () {}); + }, [outboundForm]); + + var handleDownloadImportTemplate = useCallback(function () { + var headers = ['仓库名称', '备件名称', '入库类型', '入库数量', '入库日期', '单价', '入库照片', '备注']; + var sample = [ + ['天河备件库', '示例备件名称', '采购', '10', todayYMD(), '99.99', '图片文件名或URL,可留空', '模板示例行,可删除'] + ]; + downloadCsv('备件入库导入模板.csv', headers, sample); + message.success('已下载 CSV 模板'); + }, []); + + var handleExportList = useCallback(function () { + var headers = ['序号', '仓库编码', '仓库名称', '备件编码', '备件名称', '适配车型', '库存数量', '累积入库数量', '累积出库数量']; + var rows = filtered.map(function (r, i) { + var vm = (r.vehicleModels || []).length ? r.vehicleModels.join('、') : '-'; + return [ + String(i + 1), + r.warehouseCode, + r.warehouseName, + r.spareCode, + r.spareName, + vm, + r.stockQty, + r.totalInbound, + r.totalOutbound + ]; + }); + downloadCsv('备件库存导出_' + todayYMD() + '.csv', headers, rows); + message.success('已按当前筛选条件导出 ' + rows.length + ' 条'); + }, [filtered]); + + var openDetail = useCallback(function (row) { + setDetailRow(row); + }, []); + + var openInboundHist = useCallback(function (row) { + setInboundHistRow(row); + setHistInboundPage(1); + }, []); + + var openOutboundHist = useCallback(function (row) { + setOutboundHistRow(row); + setHistOutboundPage(1); + }, []); + + var filteredInboundHist = useMemo(function () { + if (!inboundHistRow) return []; + return (inboundRecords || []).filter(function (r) { + return r.warehouseName === inboundHistRow.warehouseName && r.spareCode === inboundHistRow.spareCode; + }); + }, [inboundRecords, inboundHistRow]); + + var filteredOutboundHist = useMemo(function () { + if (!outboundHistRow) return []; + return (outboundRecords || []).filter(function (r) { + return r.warehouseName === outboundHistRow.warehouseName && r.spareCode === outboundHistRow.spareCode; + }); + }, [outboundRecords, outboundHistRow]); + + var exportInboundHistCsv = useCallback(function () { + if (!inboundHistRow) return; + var unit = getUnitForCode(inboundHistRow.spareCode); + var headers = ['备件编码', '备件名称', '入库类型', '入库数量', '入库日期', '入库人员', '单价', '入库照片', '备注']; + var rows = filteredInboundHist.map(function (r) { + var pics = (r.photoUrls && r.photoUrls.length) ? r.photoUrls.join(';') : '无'; + return [ + r.spareCode, + r.spareName, + r.inboundType, + String(r.qty) + '(' + unit + ')', + r.inboundDate, + r.operator, + (Number(r.unitPrice).toFixed(2)) + '元', + pics, + r.remark || '' + ]; + }); + downloadCsv('入库记录_' + inboundHistRow.spareCode + '_' + todayYMD() + '.csv', headers, rows); + message.success('已导出 ' + rows.length + ' 条'); + }, [filteredInboundHist, inboundHistRow, spareCatalog]); + + var exportOutboundHistCsv = useCallback(function () { + if (!outboundHistRow) return; + var unit = getUnitForCode(outboundHistRow.spareCode); + var headers = ['备件编码', '备件名称', '出库类型', '出库数量', '出库日期', '出库人员', '备注']; + var rows = filteredOutboundHist.map(function (r) { + return [ + r.spareCode, + r.spareName, + r.outboundType, + String(r.qty) + '(' + unit + ')', + r.outboundDate, + r.operator, + r.remark || '' + ]; + }); + downloadCsv('出库记录_' + outboundHistRow.spareCode + '_' + todayYMD() + '.csv', headers, rows); + message.success('已导出 ' + rows.length + ' 条'); + }, [filteredOutboundHist, outboundHistRow, spareCatalog]); + + var columns = useMemo(function () { + return [ + { + title: '序号', + key: 'serial', + width: 64, + fixed: 'left', + render: function (_, r, idx) { + return serialStart + idx + 1; + } + }, + { title: '仓库编码', dataIndex: 'warehouseCode', key: 'warehouseCode', width: 112, fixed: 'left' }, + { title: '仓库名称', dataIndex: 'warehouseName', key: 'warehouseName', width: 140, ellipsis: true }, + { title: '备件编码', dataIndex: 'spareCode', key: 'spareCode', width: 130 }, + { title: '备件名称', dataIndex: 'spareName', key: 'spareName', width: 240, ellipsis: true }, + { + title: '适配车型', + key: 'vehicleModels', + width: 200, + ellipsis: true, + render: function (_, r) { + var arr = r.vehicleModels || []; + return arr.length ? arr.join('、') : '-'; + } + }, + { + title: '库存数量', + dataIndex: 'stockQty', + key: 'stockQty', + width: 96, + align: 'left', + className: 'col-stock-qty', + onHeaderCell: function () { return { className: 'col-stock-qty' }; }, + render: function (v) { + if (v == null) return '-'; + return React.createElement('span', { + style: { + color: 'var(--ant-color-primary, #1677ff)', + fontWeight: 600, + fontSize: 15 + } + }, v); + } + }, + { + title: '累积入库数量', + dataIndex: 'totalInbound', + key: 'totalInbound', + width: 120, + render: function (v) { return v != null ? v : '-'; } + }, + { + title: '累积出库数量', + dataIndex: 'totalOutbound', + key: 'totalOutbound', + width: 120, + render: function (v) { return v != null ? v : '-'; } + }, + { + title: '操作', + key: 'actions', + width: 220, + fixed: 'right', + render: function (_, row) { + return React.createElement(Space, { size: 'small', wrap: true }, + React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0 }, onClick: function () { openDetail(row); } }, '备件明细'), + React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0 }, onClick: function () { openInboundHist(row); } }, '入库记录'), + React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0 }, onClick: function () { openOutboundHist(row); } }, '出库记录') + ); + } + } + ]; + }, [serialStart, openDetail, openInboundHist, openOutboundHist]); + + var histInboundSerial = (histInboundPage - 1) * histInboundPageSize; + var histInboundPaged = useMemo(function () { + var s = (histInboundPage - 1) * histInboundPageSize; + return filteredInboundHist.slice(s, s + histInboundPageSize); + }, [filteredInboundHist, histInboundPage, histInboundPageSize]); + + var histOutboundSerial = (histOutboundPage - 1) * histOutboundPageSize; + var histOutboundPaged = useMemo(function () { + var s = (histOutboundPage - 1) * histOutboundPageSize; + return filteredOutboundHist.slice(s, s + histOutboundPageSize); + }, [filteredOutboundHist, histOutboundPage, histOutboundPageSize]); + + var inboundHistColumns = useMemo(function () { + function renderInboundPhotos(_, r) { + var urls = r.photoUrls || []; + if (!urls.length) return '-'; + if (Image && Image.PreviewGroup) { + return React.createElement( + Image.PreviewGroup, + null, + urls.map(function (u, i) { + return React.createElement(Image, { + key: i, + width: 52, + height: 52, + style: { objectFit: 'cover', borderRadius: 4, marginRight: 4, cursor: 'pointer', verticalAlign: 'middle' }, + src: u, + alt: '入库照片' + }); + }) + ); + } + return React.createElement(Space, { size: 4, wrap: true }, + urls.map(function (u, i) { + return React.createElement('img', { + key: i, + src: u, + alt: '', + style: { width: 52, height: 52, objectFit: 'cover', borderRadius: 4, cursor: 'pointer' }, + onClick: function () { window.open(u, '_blank'); } + }); + }) + ); + } + return [ + { title: '序号', key: 's', width: 60, render: function (_, r, i) { return histInboundSerial + i + 1; } }, + { title: '备件编码', dataIndex: 'spareCode', key: 'spareCode', width: 130 }, + { title: '备件名称', dataIndex: 'spareName', key: 'spareName', ellipsis: true }, + { title: '入库类型', dataIndex: 'inboundType', key: 'inboundType', width: 88 }, + { + title: '入库数量', + key: 'qtyDisp', + width: 110, + render: function (_, r) { + var u = getUnitForCode(r.spareCode); + return r.qty + '(' + u + ')'; + } + }, + { title: '入库日期', dataIndex: 'inboundDate', key: 'inboundDate', width: 112 }, + { title: '入库人员', dataIndex: 'operator', key: 'operator', width: 96 }, + { + title: '单价', + key: 'price', + width: 100, + render: function (_, r) { + return Number(r.unitPrice).toFixed(2) + '元'; + } + }, + { title: '入库照片', key: 'photos', width: 200, render: renderInboundPhotos }, + { title: '备注', dataIndex: 'remark', key: 'remark', ellipsis: true } + ]; + }, [histInboundSerial, spareCatalog]); + + var outboundHistColumns = useMemo(function () { + return [ + { title: '序号', key: 's', width: 60, render: function (_, r, i) { return histOutboundSerial + i + 1; } }, + { title: '备件编码', dataIndex: 'spareCode', key: 'spareCode', width: 130 }, + { title: '备件名称', dataIndex: 'spareName', key: 'spareName', ellipsis: true }, + { title: '出库类型', dataIndex: 'outboundType', key: 'outboundType', width: 88 }, + { + title: '出库数量', + key: 'qtyDisp', + width: 110, + render: function (_, r) { + var u = getUnitForCode(r.spareCode); + return r.qty + '(' + u + ')'; + } + }, + { title: '出库日期', dataIndex: 'outboundDate', key: 'outboundDate', width: 112 }, + { title: '出库人员', dataIndex: 'operator', key: 'operator', width: 96 }, + { title: '备注', dataIndex: 'remark', key: 'remark', ellipsis: true } + ]; + }, [histOutboundSerial, spareCatalog]); + + var detailDescItems = useMemo(function () { + if (!detailRow) return []; + var cat = getCatalogEntry(detailRow.spareCode); + var vm = (detailRow.vehicleModels || []).length + ? detailRow.vehicleModels.join('、') + : (cat && cat.vehicleModels && cat.vehicleModels.length ? cat.vehicleModels.join('、') : '-'); + return [ + { key: 'c', label: '备件编码', children: detailRow.spareCode }, + { key: 'n', label: '备件名称', children: detailRow.spareName }, + { key: 'u', label: '计算单位', children: (cat && cat.unit) || '-' }, + { key: 'v', label: '适配车型', children: vm }, + { key: 'a', label: '告警阈值', children: cat && cat.alertThreshold != null ? String(cat.alertThreshold) : '-' } + ]; + }, [detailRow, spareCatalog]); + + return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16, flexWrap: 'wrap', gap: 8 } }, + React.createElement(Breadcrumb, { items: [{ title: '运维管理' }, { title: '备件库' }, { title: '备件库存' }] }), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { setRequirementModalOpen(true); } }, '查看需求说明') + ), + + React.createElement(Modal, { + title: '需求说明', + open: requirementModalOpen, + onCancel: function () { setRequirementModalOpen(false); }, + width: 820, + footer: React.createElement(Button, { onClick: function () { setRequirementModalOpen(false); } }, '关闭'), + bodyStyle: { paddingTop: 12, maxHeight: '78vh', overflow: 'auto' } + }, + React.createElement(Card, { + size: 'small', + bordered: true, + style: { borderRadius: 8 }, + bodyStyle: { padding: '16px 18px' } + }, + React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.65, color: 'rgba(0,0,0,0.85)' } }, requirementDocContent) + ) + ), + + React.createElement(Drawer, { + title: '备件明细', + placement: 'right', + width: 440, + open: !!detailRow, + onClose: function () { setDetailRow(null); }, + destroyOnClose: true + }, + detailRow ? React.createElement(Descriptions, { column: 1, bordered: true, size: 'small' }, + detailDescItems.map(function (it) { + return React.createElement(Descriptions.Item, { key: it.key, label: it.label }, it.children); + }) + ) : null + ), + + React.createElement(Drawer, { + title: inboundHistRow ? '入库记录 · ' + inboundHistRow.spareName : '入库记录', + placement: 'right', + width: 1120, + open: !!inboundHistRow, + onClose: function () { setInboundHistRow(null); }, + destroyOnClose: true, + extra: React.createElement(Button, { type: 'primary', size: 'small', onClick: exportInboundHistCsv, disabled: !filteredInboundHist.length }, '导出 CSV') + }, + React.createElement(Table, { + rowKey: 'id', + size: 'small', + columns: inboundHistColumns, + dataSource: histInboundPaged, + scroll: { x: 1180 }, + pagination: { + current: histInboundPage, + pageSize: histInboundPageSize, + total: filteredInboundHist.length, + showSizeChanger: true, + showTotal: function (t) { return '共 ' + t + ' 条'; }, + onChange: function (pg, ps) { setHistInboundPage(pg); if (ps) setHistInboundPageSize(ps); } + } + }) + ), + + React.createElement(Drawer, { + title: outboundHistRow ? '出库记录 · ' + outboundHistRow.spareName : '出库记录', + placement: 'right', + width: 880, + open: !!outboundHistRow, + onClose: function () { setOutboundHistRow(null); }, + destroyOnClose: true, + extra: React.createElement(Button, { type: 'primary', size: 'small', onClick: exportOutboundHistCsv, disabled: !filteredOutboundHist.length }, '导出 CSV') + }, + React.createElement(Table, { + rowKey: 'id', + size: 'small', + columns: outboundHistColumns, + dataSource: histOutboundPaged, + scroll: { x: 860 }, + pagination: { + current: histOutboundPage, + pageSize: histOutboundPageSize, + total: filteredOutboundHist.length, + showSizeChanger: true, + showTotal: function (t) { return '共 ' + t + ' 条'; }, + onChange: function (pg, ps) { setHistOutboundPage(pg); if (ps) setHistOutboundPageSize(ps); } + } + }) + ), + + React.createElement(Drawer, { + title: '新增备件信息', + placement: 'right', + width: 440, + open: addDrawerOpen, + onClose: handleAddDrawerClose, + destroyOnClose: true, + footer: React.createElement('div', { style: { textAlign: 'right' } }, + React.createElement(Button, { onClick: handleAddDrawerClose, style: { marginRight: 8 } }, '取消'), + React.createElement(Button, { type: 'primary', onClick: handleAddDrawerSubmit }, '确定') + ) + }, + React.createElement(Form, { form: addForm, layout: 'vertical', preserve: false }, + React.createElement(Form.Item, { + name: 'spareCode', + label: '备件编码', + rules: [{ required: true, message: '请输入备件编码' }] + }, React.createElement(Input, { placeholder: '请输入备件编码', allowClear: true })), + React.createElement(Form.Item, { + name: 'spareName', + label: '备件名称', + rules: [{ required: true, message: '请输入备件名称' }] + }, React.createElement(Input, { placeholder: '请输入备件名称', allowClear: true })), + React.createElement(Form.Item, { + name: 'unit', + label: '计算单位', + rules: [{ required: true, message: '请输入计量单位' }] + }, React.createElement(Input, { placeholder: '请输入计量单位', allowClear: true })), + React.createElement(Form.Item, { + name: 'vehicleModels', + label: '适配车型' + }, React.createElement(Select, { + mode: 'multiple', + placeholder: '请选择车型', + allowClear: true, + options: addDrawerVehicleOptions, + showSearch: true, + filterOption: filterOption, + maxTagCount: 'responsive' + })), + React.createElement(Form.Item, { + name: 'alertThreshold', + label: '告警阈值', + rules: [{ required: true, message: '请设置备件告警阈值' }] + }, React.createElement(InputNumber, { + placeholder: '请设置备件告警阈值', + style: { width: '100%' }, + min: 1, + precision: 0, + step: 1 + })) + ) + ), + + React.createElement(Drawer, { + title: '入库', + placement: 'right', + width: 480, + open: inboundDrawerOpen, + onClose: handleInboundFormClose, + destroyOnClose: true, + footer: React.createElement('div', { style: { textAlign: 'right' } }, + React.createElement(Button, { onClick: handleInboundFormClose, style: { marginRight: 8 } }, '取消'), + React.createElement(Button, { type: 'primary', onClick: handleInboundFormSubmit }, '确定') + ) + }, + React.createElement(Form, { form: inboundForm, layout: 'vertical', preserve: false }, + React.createElement(Form.Item, { + name: 'warehouseName', + label: '仓库名称', + rules: [{ required: true, message: '请选择仓库名称' }] + }, React.createElement(Select, { + placeholder: '请选择仓库', + allowClear: true, + showSearch: true, + options: warehouseOptionsForForm, + filterOption: filterOption + })), + React.createElement(Form.Item, { + name: 'spareCode', + label: '备件名称', + rules: [{ required: true, message: '请选择备件' }] + }, React.createElement(Select, { + placeholder: '请选择备件', + allowClear: true, + showSearch: true, + options: spareSelectByNameOptions, + filterOption: filterOption + })), + React.createElement(Form.Item, { + name: 'inboundType', + label: '入库类型', + rules: [{ required: true, message: '请选择入库类型' }] + }, React.createElement(Select, { + placeholder: '请选择', + options: [{ value: '采购', label: '采购' }, { value: '其他', label: '其他' }] + })), + React.createElement(Form.Item, { + name: 'qty', + label: '入库数量', + rules: [{ required: true, message: '请输入入库数量' }] + }, React.createElement(InputNumber, { + style: { width: '100%' }, + min: 1, + precision: 0, + step: 1 + })), + React.createElement(Form.Item, { + name: 'inboundDate', + label: '入库日期', + rules: [{ required: true, message: '请选择入库日期' }] + }, React.createElement(DatePicker, { style: { width: '100%' }, format: 'YYYY-MM-DD' })), + React.createElement(Form.Item, { label: '单价' }, + React.createElement(Input.Group, { compact: true, style: { display: 'flex', width: '100%' } }, + React.createElement(Form.Item, { + name: 'unitPrice', + noStyle: true, + rules: [{ required: true, message: '请输入单价' }] + }, React.createElement(InputNumber, { + style: { width: 'calc(100% - 40px)' }, + min: 0, + precision: 2, + step: 0.01, + placeholder: '支持两位小数' + })), + React.createElement(Input, { + style: { width: 40, textAlign: 'center', padding: '0 4px' }, + disabled: true, + value: '元' + }) + ) + ), + React.createElement(Form.Item, { + name: 'inboundPhotos', + label: '入库照片', + valuePropName: 'fileList', + getValueFromEvent: normUploadFileList, + extra: React.createElement('span', { style: { fontSize: 12, color: 'rgba(0,0,0,0.45)' } }, '本地选择图片,点击缩略图可放大预览;卡片右上角可删除') + }, React.createElement(Upload, { + listType: 'picture-card', + accept: 'image/*', + beforeUpload: function () { return false; }, + maxCount: 9, + multiple: true + }, React.createElement('div', null, + React.createElement('div', { style: { fontSize: 22, lineHeight: 1 } }, '+'), + React.createElement('div', { style: { marginTop: 6, fontSize: 12, color: 'rgba(0,0,0,0.65)' } }, '上传照片') + ))), + React.createElement(Form.Item, { name: 'remark', label: '备注' }, + React.createElement(Input.TextArea, { rows: 3, placeholder: '请输入备注', allowClear: true })) + ) + ), + + React.createElement(Drawer, { + title: '领用出库', + placement: 'right', + width: 440, + open: outboundDrawerOpen, + onClose: handleOutboundFormClose, + destroyOnClose: true, + footer: React.createElement('div', { style: { textAlign: 'right' } }, + React.createElement(Button, { onClick: handleOutboundFormClose, style: { marginRight: 8 } }, '取消'), + React.createElement(Button, { type: 'primary', onClick: handleOutboundFormSubmit }, '确定') + ) + }, + React.createElement(Form, { form: outboundForm, layout: 'vertical', preserve: false }, + React.createElement(Form.Item, { + name: 'warehouseName', + label: '仓库名称', + rules: [{ required: true, message: '请选择仓库名称' }] + }, React.createElement(Select, { + placeholder: '请选择仓库', + allowClear: true, + showSearch: true, + options: warehouseOptionsForForm, + filterOption: filterOption + })), + React.createElement(Form.Item, { + name: 'spareCode', + label: '备件名称', + rules: [{ required: true, message: '请选择备件' }] + }, React.createElement(Select, { + placeholder: '请选择备件', + allowClear: true, + showSearch: true, + options: spareSelectByNameOptions, + filterOption: filterOption + })), + React.createElement(Form.Item, { + name: 'outboundType', + label: '出库类型', + rules: [{ required: true, message: '请选择出库类型' }] + }, React.createElement(Select, { + placeholder: '请选择', + options: [ + { value: '维修', label: '维修' }, + { value: '保养', label: '保养' }, + { value: '其他', label: '其他' } + ] + })), + React.createElement(Form.Item, { + name: 'qty', + label: '出库数量', + rules: [{ required: true, message: '请输入出库数量' }] + }, React.createElement(InputNumber, { + style: { width: '100%' }, + min: 1, + precision: 0, + step: 1 + })), + React.createElement(Form.Item, { + name: 'outboundDate', + label: '出库日期', + rules: [{ required: true, message: '请选择出库日期' }] + }, React.createElement(DatePicker, { style: { width: '100%' }, format: 'YYYY-MM-DD' })), + React.createElement(Form.Item, { name: 'remark', label: '备注' }, + React.createElement(Input.TextArea, { rows: 3, placeholder: '请输入备注', allowClear: true })) + ) + ), + + React.createElement(Card, { style: { marginBottom: 16 } }, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '16px 24px', alignItems: 'start' } }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '仓库名称'), + React.createElement(Select, { + placeholder: '请输入或选择仓库名称', + style: filterControlStyle, + value: draft.warehouseName, + onChange: function (v) { setDraft(function (p) { return Object.assign({}, p, { warehouseName: v }); }); }, + allowClear: true, + showSearch: true, + options: warehouseNameOptions, + filterOption: filterOption + }) + ), + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '备件编码'), + React.createElement(Select, { + placeholder: '请输入或选择备件编码', + style: filterControlStyle, + value: draft.spareCode, + onChange: function (v) { setDraft(function (p) { return Object.assign({}, p, { spareCode: v }); }); }, + allowClear: true, + showSearch: true, + options: spareCodeOptions, + filterOption: filterOption + }) + ), + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '备件名称'), + React.createElement(Select, { + placeholder: '请输入或选择备件名称', + style: filterControlStyle, + value: draft.spareName, + onChange: function (v) { setDraft(function (p) { return Object.assign({}, p, { spareName: v }); }); }, + allowClear: true, + showSearch: true, + options: spareNameOptions, + filterOption: filterOption + }) + ), + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '适配车型'), + React.createElement(Select, { + mode: 'multiple', + placeholder: '请选择型号(可多选)', + style: filterControlStyle, + value: draft.vehicleModels, + onChange: function (v) { setDraft(function (p) { return Object.assign({}, p, { vehicleModels: v || [] }); }); }, + allowClear: true, + showSearch: true, + filterOption: filterOption, + options: allVehicleModelOptions, + maxTagCount: 'responsive' + }) + ) + ), + React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 } }, + React.createElement(Button, { onClick: handleReset }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleQuery }, '查询') + ) + ), + + React.createElement(Card, { + title: '列表', + bordered: false, + style: { borderRadius: 8 }, + extra: React.createElement(Space, { wrap: true }, + React.createElement(Button, { type: 'primary', onClick: handleAdd }, '新增备件信息'), + React.createElement(Button, { onClick: handleOpenInboundForm }, '入库'), + React.createElement(Button, { onClick: handleOpenOutboundForm }, '领用出库'), + React.createElement(Button, { onClick: handleDownloadImportTemplate }, '批量导入'), + React.createElement(Button, { onClick: handleExportList }, '导出') + ) + }, + React.createElement('style', null, tableStyle), + React.createElement('div', { className: 'spare-stock-table' }, + React.createElement(Table, { + rowKey: 'id', + columns: columns, + dataSource: paged, + scroll: { x: 1680 }, + size: 'small', + pagination: { + current: page, + pageSize: pageSize, + total: filtered.length, + showSizeChanger: true, + showQuickJumper: true, + pageSizeOptions: ['10', '20', '50', '100'], + showTotal: function (t) { return '共 ' + t + ' 条'; }, + onChange: function (pg, ps) { setPage(pg); if (ps) setPageSize(ps); } + } + }) + ) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-ops/pages/02-仓库管理.jsx b/src/prototypes/oneos-web-ops/pages/02-仓库管理.jsx new file mode 100644 index 0000000..96e720e --- /dev/null +++ b/src/prototypes/oneos-web-ops/pages/02-仓库管理.jsx @@ -0,0 +1,499 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 运维管理 - 备件库 - 仓库管理(列表页原型) + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Button = antd.Button; + var Drawer = antd.Drawer; + var Input = antd.Input; + var Table = antd.Table; + var Select = antd.Select; + var DatePicker = antd.DatePicker; + var Modal = antd.Modal; + var message = antd.message; + + var RangePicker = DatePicker.RangePicker; + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + function pad2(n) { + return n < 10 ? '0' + n : '' + n; + } + + // 格式化:YYYY-MM-DD HH:MM(尽量保持输入原样) + function fmtYMDHM(v) { + if (v === null || v === undefined) return '-'; + var s = String(v).trim(); + if (!s) return '-'; + if (/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}/.test(s)) return s.slice(0, 16); + if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s + ' 00:00'; + try { + var d = new Date(s.replace(/-/g, '/')); + if (isNaN(d.getTime())) return s; + return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate()) + ' ' + pad2(d.getHours()) + ':' + pad2(d.getMinutes()); + } catch (e) { + return s; + } + } + + function matchRange(valStr, range) { + if (!range || !range[0] || !range[1]) return true; + var s = ''; + var e = ''; + if (range[0] && range[0].format) s = range[0].format('YYYY-MM-DD'); + if (range[1] && range[1].format) e = range[1].format('YYYY-MM-DD'); + if (!s && !e) return true; + var v = String(valStr || '').slice(0, 10); + if (s && v < s) return false; + if (e && v > e) return false; + return true; + } + + var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var filterLabelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var filterItemStyle = { marginBottom: 12 }; + var filterControlStyle = { width: '100%' }; + var tableSingleLineStyle = '.warehouse-list-table .ant-table-thead th,.warehouse-list-table .ant-table-tbody td{white-space:nowrap;}'; + + // mock 列表数据(仓库是否为空用 itemCount 表示) + var warehousesState = useState(function () { + return [ + { id: 'wh1', warehouseCode: 'WH-001', warehouseName: '天河备件库', warehouseAddress: '广州市天河区xx路1号', warehouseManager: '张明', creatorName: '张明', createTime: '2026-02-20 09:30', itemCount: 12 }, + { id: 'wh2', warehouseCode: 'WH-002', warehouseName: '西湖服务备件库', warehouseAddress: '杭州市西湖区xx街88号', warehouseManager: '王芳', creatorName: '王芳', createTime: '2026-02-21 14:10', itemCount: 0 }, + { id: 'wh3', warehouseCode: 'WH-003', warehouseName: '上海浦东备件仓', warehouseAddress: '上海市浦东新区xx大道55号', warehouseManager: '李华', creatorName: '李华', createTime: '2026-02-22 08:45', itemCount: 5 }, + { id: 'wh4', warehouseCode: 'WH-004', warehouseName: '深圳南山备件仓', warehouseAddress: '深圳市南山区xx科技园3栋', warehouseManager: '赵强', creatorName: '赵强', createTime: '2026-02-23 10:20', itemCount: 0 }, + { id: 'wh5', warehouseCode: 'WH-005', warehouseName: '成都高新备件库', warehouseAddress: '成都市高新区xx路12号', warehouseManager: '陈静', creatorName: '陈静', createTime: '2026-02-24 11:05', itemCount: 9 }, + { id: 'wh6', warehouseCode: 'WH-006', warehouseName: '宁波江北备件库', warehouseAddress: '宁波市江北区xx街道6号', warehouseManager: '张明', creatorName: '张明', createTime: '2026-02-25 16:40', itemCount: 0 }, + { id: 'wh7', warehouseCode: 'WH-007', warehouseName: '北京朝阳备件仓', warehouseAddress: '北京市朝阳区xx路77号', warehouseManager: '王芳', creatorName: '王芳', createTime: '2026-02-26 09:15', itemCount: 21 }, + { id: 'wh8', warehouseCode: 'WH-008', warehouseName: '重庆两江备件库', warehouseAddress: '重庆市两江新区xx大道18号', warehouseManager: '李华', creatorName: '李华', createTime: '2026-02-27 07:45', itemCount: 2 } + ]; + }); + var warehouses = warehousesState[0]; + var setWarehouses = warehousesState[1]; + + var defaultDraftState = { warehouseName: undefined, creatorName: undefined, createTimeRange: null }; + + var draftState = useState(Object.assign({}, defaultDraftState)); + var draft = draftState[0]; + var setDraft = draftState[1]; + + var appliedState = useState(Object.assign({}, defaultDraftState)); + var applied = appliedState[0]; + var setApplied = appliedState[1]; + + var pageState = useState(1); + var page = pageState[0]; + var setPage = pageState[1]; + + var pageSizeState = useState(10); + var pageSize = pageSizeState[0]; + var setPageSize = pageSizeState[1]; + + var requirementModalState = useState(false); + var requirementModalOpen = requirementModalState[0]; + var setRequirementModalOpen = requirementModalState[1]; + + // 新增抽屉 + var addDrawerOpenState = useState(false); + var addDrawerOpen = addDrawerOpenState[0]; + var setAddDrawerOpen = addDrawerOpenState[1]; + + var addDrawerModeState = useState('add'); // 'add' | 'edit' + var addDrawerMode = addDrawerModeState[0]; + var setAddDrawerMode = addDrawerModeState[1]; + + var editingWarehouseIdState = useState(null); + var editingWarehouseId = editingWarehouseIdState[0]; + var setEditingWarehouseId = editingWarehouseIdState[1]; + + var addFormState = useState({ + warehouseCode: '', + warehouseName: '', + warehouseAddress: '', + warehouseManager: undefined + }); + var addForm = addFormState[0]; + var setAddForm = addFormState[1]; + + var requirementDocContent = [ + '一个「数字化资产ONEOS运管平台」中的「仓库管理」模块', + '#面包屑:运维管理-备件库-仓库管理', + '', + '1.筛选:', + '1.1.仓库名称:选择器,支持输入框内输入内容模糊搜索下拉匹配结果;', + '1.2.创建人:选择器,下拉显示创建人;', + '1.3.创建时间:日期选择器,支持单输入框双日历通过开始-结束时间筛选;', + '', + '2.列表;右侧为新增;', + '2.1.序号:1.2.3.以此类推;', + '2.2.仓库编码:显示仓库编码;', + '2.3.仓库名称:显示仓库名称;', + '2.4.仓库地址:显示仓库地址;', + '2.5.仓库管理人:显示仓库管理人;', + '2.6.创建人:显示创建人;', + '2.7.创建时间:显示创建时间,格式为:YYYY-MM-DD HH:MM;', + '2.8.操作:编辑、删除;', + ' 2.8.1.编辑:跳转仓库设置-编辑页;', + ' 2.8.2.删除:点击进行二次确认,确认后判断仓库是否为空,如果仓库非空则提示:清空仓库后才可删除该仓库,仓库为空则提示:删除成功;' + ].join('\n'); + + var creatorOptions = useMemo(function () { + // 创建人下拉来源:固定示例 + 兼容 mock 数据变更 + var set = new Set(); + setWarehouses; // keep linter friendly for unused setters in some generators + (warehouses || []).forEach(function (r) { if (r && r.creatorName) set.add(r.creatorName); }); + if (set.size === 0) { + ['张明', '王芳', '李华', '赵强', '陈静'].forEach(function (v) { set.add(v); }); + } + return Array.from(set).map(function (v) { return { value: v, label: v }; }); + }, [warehouses]); + + var warehouseManagerOptions = useMemo(function () { + var set = new Set(); + (warehouses || []).forEach(function (r) { if (r && r.warehouseManager) set.add(r.warehouseManager); }); + if (set.size === 0) { + ['张明', '王芳', '李华', '赵强', '陈静'].forEach(function (v) { set.add(v); }); + } + return Array.from(set).map(function (v) { return { value: v, label: v }; }); + }, [warehouses]); + + var warehouseNameOptions = useMemo(function () { + var set = new Set(); + (warehouses || []).forEach(function (r) { if (r && r.warehouseName) set.add(r.warehouseName); }); + return Array.from(set).map(function (v) { return { value: v, label: v }; }); + }, [warehouses]); + + var filtered = useMemo(function () { + return (warehouses || []).filter(function (r) { + if (applied.warehouseName && r.warehouseName !== applied.warehouseName) return false; + if (applied.creatorName && r.creatorName !== applied.creatorName) return false; + if (!matchRange(r.createTime, applied.createTimeRange)) return false; + return true; + }); + }, [warehouses, applied]); + + var paged = useMemo(function () { + var start = (page - 1) * pageSize; + return filtered.slice(start, start + pageSize); + }, [filtered, page, pageSize]); + + var serialStart = (page - 1) * pageSize; + + var handleQuery = useCallback(function () { + setApplied({ + warehouseName: draft.warehouseName, + creatorName: draft.creatorName, + createTimeRange: draft.createTimeRange + }); + setPage(1); + }, [draft]); + + var handleReset = useCallback(function () { + setDraft(Object.assign({}, defaultDraftState)); + setApplied(Object.assign({}, defaultDraftState)); + setPage(1); + }, []); + + var handleAdd = useCallback(function () { + setAddDrawerMode('add'); + setEditingWarehouseId(null); + setAddForm({ warehouseCode: '', warehouseName: '', warehouseAddress: '', warehouseManager: undefined }); + setAddDrawerOpen(true); + }, []); + + var handleEdit = useCallback(function (row) { + if (!row) return; + setAddDrawerMode('edit'); + setEditingWarehouseId(row.id); + setAddForm({ + warehouseCode: row.warehouseCode || '', + warehouseName: row.warehouseName || '', + warehouseAddress: row.warehouseAddress || '', + warehouseManager: row.warehouseManager + }); + setAddDrawerOpen(true); + }, []); + + var handleAddCancel = useCallback(function () { + setAddDrawerOpen(false); + setAddDrawerMode('add'); + setEditingWarehouseId(null); + setAddForm({ warehouseCode: '', warehouseName: '', warehouseAddress: '', warehouseManager: undefined }); + }, []); + + function nowYMDHM() { + var d = new Date(); + return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate()) + ' ' + pad2(d.getHours()) + ':' + pad2(d.getMinutes()); + } + + var handleAddSubmit = useCallback(function () { + var code = String(addForm.warehouseCode || '').trim(); + var name = String(addForm.warehouseName || '').trim(); + var address = String(addForm.warehouseAddress || '').trim(); + var manager = addForm.warehouseManager ? addForm.warehouseManager : undefined; + + // 仅仓库编码、仓库名称必填;地址/管理人选填 + if (!code) { + message.warning('请填写仓库编码'); + return; + } + if (!name) { + message.warning('请填写仓库名称'); + return; + } + + if (addDrawerMode === 'edit') { + if (!editingWarehouseId) { + message.error('无法定位要编辑的仓库记录'); + return; + } + setWarehouses(function (prev) { + return (prev || []).map(function (r) { + if (!r || r.id !== editingWarehouseId) return r; + return Object.assign({}, r, { + warehouseCode: code, + warehouseName: name, + warehouseAddress: address, + warehouseManager: manager + }); + }); + }); + setAddDrawerOpen(false); + setAddDrawerMode('add'); + setEditingWarehouseId(null); + message.success('编辑成功'); + return; + } + + var item = { + id: 'wh' + Date.now(), + warehouseCode: code, + warehouseName: name, + warehouseAddress: address, + warehouseManager: manager, + creatorName: '当前用户', + createTime: nowYMDHM(), + itemCount: 0 + }; + + setWarehouses(function (prev) { + return [item].concat(prev || []); + }); + setAddDrawerOpen(false); + setAddDrawerMode('add'); + setEditingWarehouseId(null); + message.success('新增成功'); + }, [addForm, addDrawerMode, editingWarehouseId, setWarehouses, setAddDrawerMode, setEditingWarehouseId]); + + var handleDelete = useCallback(function (row) { + if (!row) return; + Modal.confirm({ + title: '确认删除', + content: '请确认是否删除该仓库', + okText: '确认', + cancelText: '取消', + onOk: function () { + if (row.itemCount > 0) { + message.warning('清空仓库后才可删除该仓库'); + return; + } + setWarehouses(function (prev) { + return (prev || []).filter(function (r) { return r.id !== row.id; }); + }); + message.success('删除成功'); + } + }); + }, [setWarehouses]); + + var columns = useMemo(function () { + return [ + { + title: '序号', + key: 'serial', + width: 70, + fixed: 'left', + render: function (_, r, idx) { + return serialStart + idx + 1; + } + }, + { title: '仓库编码', dataIndex: 'warehouseCode', key: 'warehouseCode', width: 160, fixed: 'left' }, + { title: '仓库名称', dataIndex: 'warehouseName', key: 'warehouseName', width: 180, ellipsis: true }, + { title: '仓库地址', dataIndex: 'warehouseAddress', key: 'warehouseAddress', width: 240, ellipsis: true }, + { title: '仓库管理人', dataIndex: 'warehouseManager', key: 'warehouseManager', width: 140 }, + { title: '创建人', dataIndex: 'creatorName', key: 'creatorName', width: 120 }, + { + title: '创建时间', + dataIndex: 'createTime', + key: 'createTime', + width: 200, + render: function (v) { return fmtYMDHM(v); } + }, + { + title: '操作', + key: 'action', + width: 180, + fixed: 'right', + render: function (_, r) { + return React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } }, + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { handleEdit(r); } }, '编辑'), + React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function () { handleDelete(r); } }, '删除') + ); + } + } + ]; + }, [serialStart, handleDelete]); + + return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16, flexWrap: 'wrap', gap: 8 } }, + React.createElement(Breadcrumb, { items: [{ title: '运维管理' }, { title: '备件库' }, { title: '仓库管理' }] }), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { setRequirementModalOpen(true); } }, '查看需求说明') + ), + + React.createElement(Modal, { + title: '需求说明', + open: requirementModalOpen, + onCancel: function () { setRequirementModalOpen(false); }, + width: 720, + footer: React.createElement(Button, { onClick: function () { setRequirementModalOpen(false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6, color: 'rgba(0,0,0,0.85)' } }, requirementDocContent)), + + React.createElement(Card, { style: { marginBottom: 16 } }, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px 24px', alignItems: 'start' } }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '仓库名称'), + React.createElement(Select, { + placeholder: '请输入或选择仓库名称', + style: filterControlStyle, + value: draft.warehouseName, + onChange: function (v) { setDraft(function (p) { return Object.assign({}, p, { warehouseName: v }); }); }, + allowClear: true, + showSearch: true, + options: warehouseNameOptions, + filterOption: filterOption + }) + ), + + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '创建人'), + React.createElement(Select, { + placeholder: '请选择创建人', + style: filterControlStyle, + value: draft.creatorName, + onChange: function (v) { setDraft(function (p) { return Object.assign({}, p, { creatorName: v }); }); }, + allowClear: true, + showSearch: true, + options: creatorOptions + }) + ), + + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '创建时间'), + React.createElement(RangePicker, { + style: filterControlStyle, + value: draft.createTimeRange, + onChange: function (v) { setDraft(function (p) { return Object.assign({}, p, { createTimeRange: v }); }); }, + placeholder: ['开始时间', '结束时间'], + showTime: false + }) + ) + ), + + React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 } }, + React.createElement(Button, { onClick: handleReset }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleQuery }, '查询') + ) + ), + + React.createElement(Card, null, + React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', marginBottom: 12 } }, + React.createElement(Button, { type: 'primary', onClick: handleAdd }, '新增') + ), + + React.createElement('style', null, tableSingleLineStyle), + React.createElement('div', { className: 'warehouse-list-table' }, + React.createElement(Table, { + rowKey: 'id', + columns: columns, + dataSource: paged, + scroll: { x: 1200 }, + size: 'small', + pagination: { + current: page, + pageSize: pageSize, + total: filtered.length, + showSizeChanger: true, + showQuickJumper: true, + showTotal: function (t) { return '共 ' + t + ' 条'; }, + onChange: function (pg, ps) { setPage(pg); if (ps) setPageSize(ps); } + } + }) + ) + ), + + React.createElement(Drawer, { + title: addDrawerMode === 'edit' ? '编辑仓库' : '新增仓库', + placement: 'right', + width: 460, + open: addDrawerOpen, + onClose: handleAddCancel, + styles: { body: { padding: 24 } }, + footer: React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8 } }, + React.createElement(Button, { type: 'primary', onClick: handleAddSubmit }, '提交'), + React.createElement(Button, { onClick: handleAddCancel }, '取消') + ) + }, + React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 14 } }, + React.createElement('div', null, + React.createElement('div', { style: { marginBottom: 6, color: '#666', fontSize: 14 } }, '仓库编码'), + React.createElement(Input, { + value: addForm.warehouseCode, + disabled: addDrawerMode === 'edit', + onChange: function (e) { setAddForm(function (p) { return Object.assign({}, p, { warehouseCode: e && e.target ? e.target.value : '' }); }); }, + placeholder: '请输入仓库编码' + }) + ), + React.createElement('div', null, + React.createElement('div', { style: { marginBottom: 6, color: '#666', fontSize: 14 } }, '仓库名称'), + React.createElement(Input, { + value: addForm.warehouseName, + onChange: function (e) { setAddForm(function (p) { return Object.assign({}, p, { warehouseName: e && e.target ? e.target.value : '' }); }); }, + placeholder: '请输入仓库名称' + }) + ), + React.createElement('div', null, + React.createElement('div', { style: { marginBottom: 6, color: '#666', fontSize: 14 } }, '仓库地址'), + React.createElement(Input, { + value: addForm.warehouseAddress, + onChange: function (e) { setAddForm(function (p) { return Object.assign({}, p, { warehouseAddress: e && e.target ? e.target.value : '' }); }); }, + placeholder: '请输入仓库地址' + }) + ), + React.createElement('div', null, + React.createElement('div', { style: { marginBottom: 6, color: '#666', fontSize: 14 } }, '仓库管理人'), + React.createElement(Select, { + placeholder: '请选择人员', + value: addForm.warehouseManager, + onChange: function (v) { setAddForm(function (p) { return Object.assign({}, p, { warehouseManager: v }); }); }, + style: { width: '100%' }, + allowClear: true, + showSearch: true, + filterOption: filterOption, + options: warehouseManagerOptions + }) + ) + ) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-ops/pages/03-备车-编辑.jsx b/src/prototypes/oneos-web-ops/pages/03-备车-编辑.jsx new file mode 100644 index 0000000..8c1b903 --- /dev/null +++ b/src/prototypes/oneos-web-ops/pages/03-备车-编辑.jsx @@ -0,0 +1,625 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 车辆业务 - 备车-编辑(布局同新增备车,车辆数据不可修改,其余字段可修改) + +const Component = function () { + var useState = React.useState; + var useCallback = React.useCallback; + var useMemo = React.useMemo; + var useRef = React.useRef; + var useEffect = React.useEffect; + + var antd = window.antd; + var Card = antd.Card; + var Input = antd.Input; + var Button = antd.Button; + var Switch = antd.Switch; + var Drawer = antd.Drawer; + var Table = antd.Table; + var message = antd.message; + var Modal = antd.Modal; + + var plateManageList = [ + { plateNo: '京A12345', vehicleType: '厢式货车', brand: '东风', model: 'DFH1180', vin: 'LGHXCAE28M1234567', bodyAd: true, tailboard: true }, + { plateNo: '京C11111', vehicleType: '平板货车', brand: '福田', model: 'BJ1180', vin: 'LGHXCAE28M7654321', bodyAd: false, tailboard: false }, + { plateNo: '京D22222', vehicleType: '厢式货车', brand: '江淮', model: 'HFC1180', vin: 'LGHXCAE28M8888888', bodyAd: true, tailboard: false }, + { plateNo: '京E33333', vehicleType: '栏板货车', brand: '重汽', model: 'ZZ1180', vin: 'LGHXCAE28M7777777', bodyAd: false, tailboard: true }, + { plateNo: '京F10001', vehicleType: '厢式货车', brand: '江淮', model: 'HFC1180', vin: 'LGHXCAE28M9990001', bodyAd: true, tailboard: true } + ]; + + var inspectionCategoryItems = { + '车灯': ['大灯', '转向灯', '小灯', '示廓灯', '刹车灯', '倒车灯', '牌照灯', '防雾灯', '室内灯'], + '仪表盘': ['氢系统指示', '电控系统指示', '数值清晰准确', '故障报警灯'], + '驾驶室': ['点烟器', '车窗升降', '按键开关', '雨刮器', '内后视镜是否正常', '内/外门把手', '安全带', '空调冷暖风', '仪表盘', '门锁功能', '手刹', '车钥匙功能是否正常', '喇叭', '音响功能', '遮阳板', '主副驾座椅', '方向盘', '内饰干净整洁'], + '轮胎': ['前左胎', '前右胎', '后左胎', '后右胎'], + '液位检查': ['冷却液', '制动液', '玻璃水'], + '外观检查': ['车身外观', '漆面', '玻璃'], + '车辆外观': ['整车外观'], + '其他': ['其他检查项'], + '随车工具': ['三角牌', '灭火器', '反光背心'], + '随车证件': ['行驶证', '营运证', '保险单'], + '整车': ['整车状态'], + '燃料电池系统': ['氢系统', '储氢瓶'], + '冷机': ['冷机运行'], + '制动系统': ['制动踏板', '驻车制动'] + }; + + function buildInspectionList() { + var list = []; + var categories = Object.keys(inspectionCategoryItems); + for (var i = 0; i < categories.length; i++) { + var cat = categories[i]; + var items = inspectionCategoryItems[cat]; + for (var j = 0; j < items.length; j++) { + list.push({ category: cat, checkItem: items[j], checked: true, remark: '', treadDepth: '' }); + } + } + return list; + } + + // 编辑时带入的当前记录(车辆数据不可改,其余可改;已上传图片支持预览、删除) + var initialData = { + plateNo: '京A12345', + vehicleType: '厢式货车', + brand: '东风', + model: 'DFH1180', + vin: 'LGHXCAE28M1234567', + bodyAd: true, + adPhotos: ['https://picsum.photos/seed/ad1/200/200'], + enlargePhotos: ['https://picsum.photos/seed/en1/200/200'], + tailboard: true, + spareTirePhotos: ['https://picsum.photos/seed/sp1/200/200'], + spareTireTreadDepth: '5.2', + flawPhotos: [ + 'https://picsum.photos/seed/flaw1/200/200', + 'https://picsum.photos/seed/flaw2/200/200', + 'https://picsum.photos/seed/flaw3/200/200' + ], + inspectionList: buildInspectionList() + }; + + var formState = useState({ + plateNo: initialData.plateNo, + vehicleType: initialData.vehicleType, + brand: initialData.brand, + model: initialData.model, + vin: initialData.vin, + bodyAd: initialData.bodyAd, + adPhotos: initialData.adPhotos.slice(), + enlargePhotos: initialData.enlargePhotos.slice(), + tailboard: initialData.tailboard, + spareTirePhotos: initialData.spareTirePhotos.slice(), + spareTireTreadDepth: initialData.spareTireTreadDepth, + flawPhotos: initialData.flawPhotos.slice(), + inspectionList: initialData.inspectionList.map(function (r) { return Object.assign({}, r); }) + }); + var form = formState[0]; + var setForm = formState[1]; + + var drawerOpenState = useState(false); + var photoPreviewState = useState({ open: false, url: null }); + var reqDocOpenState = useState(false); + var adPhotoInputRef = useRef(null); + var enlargePhotoInputRef = useRef(null); + var spareTirePhotoInputRef = useRef(null); + var flawPhotoInputRef = useRef(null); + var spareTireOcrModalState = useState({ open: false, tempUrl: null, tempDepth: '', phase: 'recognizing' }); + var spareTireOcrPhaseRef = useRef(null); + + var updateInspection = useCallback(function (index, field, value) { + setForm(function (prev) { + var n = {}; + for (var k in prev) n[k] = prev[k]; + var list = (prev.inspectionList || []).slice(); + var row = list[index] || {}; + list[index] = Object.assign({}, row, { [field]: value }); + n.inspectionList = list; + return n; + }); + }, []); + + var addAdPhoto = useCallback(function () { + if ((form.adPhotos || []).length >= 1) { message.warning('最多上传1张'); return; } + if (adPhotoInputRef.current) adPhotoInputRef.current.click(); + }, [form.adPhotos]); + var onAdPhotoFileChange = useCallback(function (e) { + var files = e.target.files; + if (!files || files.length === 0) return; + var file = files[0]; + if (!file || file.type.indexOf('image') === -1) { e.target.value = ''; return; } + var url = URL.createObjectURL(file); + setForm(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.adPhotos = [url]; return n; }); + e.target.value = ''; + }, []); + var removeAdPhoto = useCallback(function () { + setForm(function (p) { + var n = {}; for (var k in p) n[k] = p[k]; + var arr = (p.adPhotos || [])[0]; + if (arr && typeof arr === 'string' && arr.indexOf('blob:') === 0) URL.revokeObjectURL(arr); + n.adPhotos = []; + return n; + }); + }, []); + + var addEnlargePhoto = useCallback(function () { + if ((form.enlargePhotos || []).length >= 1) { message.warning('最多上传1张'); return; } + if (enlargePhotoInputRef.current) enlargePhotoInputRef.current.click(); + }, [form.enlargePhotos]); + var onEnlargePhotoFileChange = useCallback(function (e) { + var files = e.target.files; + if (!files || files.length === 0) return; + var file = files[0]; + if (!file || file.type.indexOf('image') === -1) { e.target.value = ''; return; } + var url = URL.createObjectURL(file); + setForm(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.enlargePhotos = [url]; return n; }); + e.target.value = ''; + }, []); + var removeEnlargePhoto = useCallback(function () { + setForm(function (p) { + var n = {}; for (var k in p) n[k] = p[k]; + var u = (p.enlargePhotos || [])[0]; + if (u && typeof u === 'string' && u.indexOf('blob:') === 0) URL.revokeObjectURL(u); + n.enlargePhotos = []; + return n; + }); + }, []); + + var addSpareTirePhoto = useCallback(function () { + if ((form.spareTirePhotos || []).length >= 1) { message.warning('最多上传1张'); return; } + if (spareTirePhotoInputRef.current) spareTirePhotoInputRef.current.click(); + }, [form.spareTirePhotos]); + var onSpareTirePhotoFileChange = useCallback(function (e) { + var files = e.target.files; + if (!files || files.length === 0) return; + var file = files[0]; + if (!file || file.type.indexOf('image') === -1) { e.target.value = ''; return; } + var url = URL.createObjectURL(file); + spareTireOcrModalState[1]({ open: true, tempUrl: url, tempDepth: '', phase: 'recognizing' }); + e.target.value = ''; + }, []); + useEffect(function () { + var s = spareTireOcrModalState[0]; + if (!s.open || s.phase !== 'recognizing') return; + if (spareTireOcrPhaseRef.current) clearTimeout(spareTireOcrPhaseRef.current); + spareTireOcrPhaseRef.current = setTimeout(function () { + spareTireOcrModalState[1](function (prev) { + return prev.open ? { open: true, tempUrl: prev.tempUrl, tempDepth: '5.2', phase: 'result' } : prev; + }); + }, 1500); + return function () { + if (spareTireOcrPhaseRef.current) clearTimeout(spareTireOcrPhaseRef.current); + }; + }, [spareTireOcrModalState[0].open, spareTireOcrModalState[0].phase]); + var confirmSpareTireOcr = useCallback(function () { + var s = spareTireOcrModalState[0]; + var depth = (s.tempDepth || '').trim(); + setForm(function (p) { + var n = {}; for (var k in p) n[k] = p[k]; + n.spareTireTreadDepth = depth; + n.spareTirePhotos = s.tempUrl ? [s.tempUrl] : []; + return n; + }); + spareTireOcrModalState[1]({ open: false, tempUrl: null, tempDepth: '', phase: 'recognizing' }); + }, []); + var closeSpareTireOcrWithoutSave = useCallback(function () { + var s = spareTireOcrModalState[0]; + if (s.tempUrl && s.tempUrl.indexOf('blob:') === 0) URL.revokeObjectURL(s.tempUrl); + spareTireOcrModalState[1]({ open: false, tempUrl: null, tempDepth: '', phase: 'recognizing' }); + }, []); + var setSpareTireOcrDepth = useCallback(function (v) { + spareTireOcrModalState[1](function (prev) { return { open: prev.open, tempUrl: prev.tempUrl, tempDepth: v, phase: prev.phase }; }); + }, []); + var removeSpareTirePhoto = useCallback(function () { + setForm(function (p) { + var n = {}; for (var k in p) n[k] = p[k]; + var u = (p.spareTirePhotos || [])[0]; + if (u && typeof u === 'string' && u.indexOf('blob:') === 0) URL.revokeObjectURL(u); + n.spareTirePhotos = []; return n; + }); + }, []); + + var addFlawPhoto = useCallback(function () { + if ((form.flawPhotos || []).length >= 4) { message.warning('最多上传4张'); return; } + if (flawPhotoInputRef.current) flawPhotoInputRef.current.click(); + }, [form.flawPhotos]); + var onFlawPhotoFileChange = useCallback(function (e) { + var files = e.target.files; + if (!files || files.length === 0) return; + setForm(function (p) { + var current = p.flawPhotos || []; + var remain = 4 - current.length; + if (remain <= 0) return p; + var urls = []; + for (var i = 0; i < files.length && urls.length < remain; i++) { + if (files[i].type.indexOf('image') !== -1) urls.push(URL.createObjectURL(files[i])); + } + if (urls.length === 0) return p; + var n = {}; for (var k in p) n[k] = p[k]; n.flawPhotos = current.concat(urls); return n; + }); + e.target.value = ''; + }, []); + var removeFlawPhoto = useCallback(function (idx) { + setForm(function (p) { + var n = {}; + for (var k in p) n[k] = p[k]; + var arr = (p.flawPhotos || []).slice(); + var url = arr[idx]; + if (url && url.indexOf('blob:') === 0) URL.revokeObjectURL(url); + arr.splice(idx, 1); + n.flawPhotos = arr; + return n; + }); + }, []); + + function validateForm() { + if (form.bodyAd && (!form.adPhotos || form.adPhotos.length < 1)) { message.warning('请上传广告照片'); return false; } + if (form.bodyAd && (!form.enlargePhotos || form.enlargePhotos.length < 1)) { message.warning('请上传放大字照片'); return false; } + if (!form.spareTirePhotos || form.spareTirePhotos.length < 1) { message.warning('请上传备胎照片'); return false; } + if (!String(form.spareTireTreadDepth || '').trim()) { message.warning('请填写备胎胎纹深度'); return false; } + var list = form.inspectionList || []; + for (var i = 0; i < list.length; i++) { + if (list[i].category === '轮胎' && !String(list[i].treadDepth || '').trim()) { + message.warning('请完善备车检查单:填写轮胎胎纹深度'); + return false; + } + if (!String(list[i].remark || '').trim()) { + message.warning('请完善备车检查单:所有项备注为必填'); + return false; + } + } + return true; + } + var handleSave = useCallback(function () { + if (!validateForm()) return; + message.success('提交成功(原型)'); + if (typeof window !== 'undefined' && window.history) window.history.back(); + }, [form.spareTirePhotos, form.spareTireTreadDepth, form.inspectionList]); + var handleSaveDraft = useCallback(function () { + message.success('保存成功(原型)'); + }, []); + var handleCancel = useCallback(function () { + if (typeof window !== 'undefined' && window.history) window.history.back(); + else message.info('取消'); + }, []); + + var styles = { + page: { padding: '16px 24px 80px', backgroundColor: '#f5f5f5', minHeight: '100vh', fontSize: 14 }, + breadcrumb: { marginBottom: 16, color: '#666' }, + breadcrumbSep: { margin: '0 8px', color: '#999' }, + card: { backgroundColor: '#fff', borderRadius: 8, marginBottom: 16, boxShadow: '0 1px 2px rgba(0,0,0,0.05)', overflow: 'hidden' }, + cardBody: { padding: '20px 24px' }, + formRow: { display: 'flex', flexWrap: 'wrap', marginBottom: 16 }, + formCol: { flex: '0 0 33.33%', minWidth: 200, paddingRight: 16, marginBottom: 8, boxSizing: 'border-box' }, + formColFull: { flex: '0 0 100%', marginBottom: 8, boxSizing: 'border-box' }, + label: { display: 'block', marginBottom: 6, color: '#333' }, + labelRequired: { color: '#ff4d4f', marginRight: 4 }, + footer: { position: 'fixed', bottom: 0, left: 0, right: 0, padding: '12px 24px', backgroundColor: '#fff', borderTop: '1px solid #e8e8e8', display: 'flex', gap: 12, zIndex: 99 } + }; + + var FormItem = function (props) { + var colStyle = props.fullWidth ? styles.formColFull : styles.formCol; + return React.createElement('div', { style: colStyle }, + React.createElement('label', { style: styles.label }, + props.required ? React.createElement('span', { style: styles.labelRequired }, '*') : null, + props.label + ), + props.children + ); + }; + + var inspectionList = form.inspectionList || []; + var categoryRowSpans = useMemo(function () { + var list = inspectionList; + var spans = []; + var i = 0; + while (i < list.length) { + var cat = list[i].category; + var start = i; + while (i < list.length && list[i].category === cat) i++; + spans[start] = i - start; + for (var j = start + 1; j < i; j++) spans[j] = 0; + } + return spans; + }, [form.inspectionList]); + + var inspectionColumns = [ + { + title: '类别', + dataIndex: 'category', + key: 'category', + width: 120, + onCell: function (_, index) { return { rowSpan: categoryRowSpans[index] !== undefined ? categoryRowSpans[index] : 1 }; } + }, + { title: '检查项目', dataIndex: 'checkItem', key: 'checkItem', width: 180 }, + { + title: '检查情况', + key: 'checked', + width: 140, + render: function (_, record, index) { + if (record.category === '轮胎') { + return React.createElement(Input, { + value: record.treadDepth, + onChange: function (e) { updateInspection(index, 'treadDepth', e.target.value); }, + placeholder: '请输入胎纹深度', + style: { width: '100%' } + }); + } + return React.createElement(Switch, { + checked: record.checked !== false, + checkedChildren: '正常', + unCheckedChildren: '异常', + onChange: function (checked) { updateInspection(index, 'checked', checked); } + }); + } + }, + { + title: '备注', + dataIndex: 'remark', + key: 'remark', + render: function (val, record, index) { + return React.createElement(Input, { + value: val, + onChange: function (e) { updateInspection(index, 'remark', e.target.value); }, + placeholder: '必填' + }); + } + } + ]; + + var cameraIconBox = function (onClick) { + return React.createElement('div', { + role: 'button', + tabIndex: 0, + onClick: onClick, + style: { width: 64, height: 64, border: '1px dashed #d9d9d9', borderRadius: 4, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', background: '#fafafa', boxSizing: 'border-box' } + }, React.createElement('svg', { width: 28, height: 28, viewBox: '0 0 24 24', fill: 'none', stroke: '#999', strokeWidth: 2 }, + React.createElement('path', { d: 'M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z' }), + React.createElement('circle', { cx: 12, cy: 13, r: 4 }) + )); + }; + + var breadcrumbNodes = [ + React.createElement('span', { key: '1' }, '运维管理'), + React.createElement('span', { key: '2', style: styles.breadcrumbSep }, ' / '), + React.createElement('span', { key: '3' }, '车辆业务'), + React.createElement('span', { key: '4', style: styles.breadcrumbSep }, ' / '), + React.createElement('span', { key: '5' }, '备车管理'), + React.createElement('span', { key: '6', style: styles.breadcrumbSep }, ' / '), + React.createElement('span', { key: '7', style: { color: '#1890ff' } }, '编辑') + ]; + + return React.createElement('div', { style: styles.page }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + React.createElement('div', { style: styles.breadcrumb }, breadcrumbNodes) + ), + React.createElement('div', { style: styles.card }, + React.createElement('div', { style: styles.cardBody }, + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '车牌号', required: true }, + React.createElement(Input, { value: form.plateNo, disabled: true, style: { width: '100%', background: '#f5f5f5' } }) + ), + React.createElement(FormItem, { label: '车辆类型' }, + React.createElement(Input, { value: form.vehicleType, disabled: true, style: { width: '100%', background: '#f5f5f5' } }) + ), + React.createElement(FormItem, { label: '品牌' }, + React.createElement(Input, { value: form.brand, disabled: true, style: { width: '100%', background: '#f5f5f5' } }) + ), + React.createElement(FormItem, { label: '型号' }, + React.createElement(Input, { value: form.model, disabled: true, style: { width: '100%', background: '#f5f5f5' } }) + ), + React.createElement(FormItem, { label: '车辆识别代码' }, + React.createElement(Input, { value: form.vin, disabled: true, style: { width: '100%', background: '#f5f5f5' } }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '车身广告及放大字', required: true }, + React.createElement(Switch, { + checked: form.bodyAd, + checkedChildren: '有', + unCheckedChildren: '无', + onChange: function (checked) { + setForm(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.bodyAd = checked; return n; }); + } + }) + ), + form.bodyAd ? React.createElement(FormItem, { label: '广告照片', required: true }, + React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 } }, + React.createElement('input', { type: 'file', ref: adPhotoInputRef, accept: 'image/*', style: { display: 'none' }, onChange: onAdPhotoFileChange }), + (form.adPhotos || []).length < 1 ? cameraIconBox(addAdPhoto) : null, + (form.adPhotos || []).map(function (url, i) { + return React.createElement('span', { key: i, style: { position: 'relative', display: 'inline-block' } }, + React.createElement('img', { + src: url, + alt: '', + style: { width: 64, height: 64, objectFit: 'cover', borderRadius: 4, cursor: 'pointer' }, + onClick: function () { photoPreviewState[1]({ open: true, url: url }); } + }), + React.createElement('span', { + style: { position: 'absolute', top: -6, right: -6, width: 20, height: 20, borderRadius: '50%', background: '#ff4d4f', color: '#fff', fontSize: 12, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }, + onClick: function () { removeAdPhoto(); } + }, '×') + ); + }) + ) + ) : null, + form.bodyAd ? React.createElement(FormItem, { label: '放大字照片', required: true }, + React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 } }, + React.createElement('input', { type: 'file', ref: enlargePhotoInputRef, accept: 'image/*', style: { display: 'none' }, onChange: onEnlargePhotoFileChange }), + (form.enlargePhotos || []).length < 1 ? cameraIconBox(addEnlargePhoto) : null, + (form.enlargePhotos || []).map(function (url, i) { + return React.createElement('span', { key: i, style: { position: 'relative', display: 'inline-block' } }, + React.createElement('img', { + src: url, + alt: '', + style: { width: 64, height: 64, objectFit: 'cover', borderRadius: 4, cursor: 'pointer' }, + onClick: function () { photoPreviewState[1]({ open: true, url: url }); } + }), + React.createElement('span', { + style: { position: 'absolute', top: -6, right: -6, width: 20, height: 20, borderRadius: '50%', background: '#ff4d4f', color: '#fff', fontSize: 12, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }, + onClick: function () { removeEnlargePhoto(); } + }, '×') + ); + }) + ) + ) : null + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '尾板', required: true }, + React.createElement(Switch, { + checked: form.tailboard, + checkedChildren: '有', + unCheckedChildren: '无', + onChange: function (checked) { + setForm(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.tailboard = checked; return n; }); + } + }) + ), + React.createElement(FormItem, { label: '备胎照片', required: true }, + React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 } }, + React.createElement('input', { type: 'file', ref: spareTirePhotoInputRef, accept: 'image/*', style: { display: 'none' }, onChange: onSpareTirePhotoFileChange }), + (form.spareTirePhotos || []).length < 1 ? cameraIconBox(addSpareTirePhoto) : null, + (form.spareTirePhotos || []).map(function (url, i) { + return React.createElement('span', { key: i, style: { position: 'relative', display: 'inline-block' } }, + React.createElement('img', { + src: url, + alt: '', + style: { width: 64, height: 64, objectFit: 'cover', borderRadius: 4, cursor: 'pointer' }, + onClick: function () { photoPreviewState[1]({ open: true, url: url }); } + }), + React.createElement('span', { + style: { position: 'absolute', top: -6, right: -6, width: 20, height: 20, borderRadius: '50%', background: '#ff4d4f', color: '#fff', fontSize: 12, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }, + onClick: function () { removeSpareTirePhoto(); } + }, '×') + ); + }) + ) + ), + React.createElement(FormItem, { label: '备胎胎纹深度', required: true }, + React.createElement(Input, { + value: form.spareTireTreadDepth, + onChange: function (e) { setForm(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.spareTireTreadDepth = e.target.value; return n; }); }, + placeholder: 'OCR识别结果可修改', + style: { width: '100%' }, + addonAfter: 'mm' + }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '瑕疵', fullWidth: true }, + React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 } }, + React.createElement('input', { type: 'file', ref: flawPhotoInputRef, accept: 'image/*', multiple: true, style: { display: 'none' }, onChange: onFlawPhotoFileChange }), + (form.flawPhotos || []).length < 4 ? cameraIconBox(addFlawPhoto) : null, + (form.flawPhotos || []).map(function (url, i) { + return React.createElement('span', { key: i, style: { position: 'relative', display: 'inline-block' } }, + React.createElement('img', { + src: url, + alt: '', + style: { width: 64, height: 64, objectFit: 'cover', borderRadius: 4, cursor: 'pointer' }, + onClick: function () { photoPreviewState[1]({ open: true, url: url }); } + }), + React.createElement('span', { + style: { position: 'absolute', top: -6, right: -6, width: 20, height: 20, borderRadius: '50%', background: '#ff4d4f', color: '#fff', fontSize: 12, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }, + onClick: function () { removeFlawPhoto(i); } + }, '×') + ); + }) + ) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '车辆检查', fullWidth: true, required: true }, + React.createElement(Button, { type: 'default', onClick: function () { drawerOpenState[1](true); } }, '备车检查单') + ) + ) + ) + ), + React.createElement('div', { style: styles.footer }, + React.createElement(Button, { type: 'primary', onClick: handleSave }, '提交'), + React.createElement(Button, { onClick: handleSaveDraft }, '保存'), + React.createElement(Button, { onClick: handleCancel }, '取消') + ), + React.createElement(Drawer, { + title: '备车检查单', + placement: 'right', + width: 640, + open: drawerOpenState[0], + onClose: function () { drawerOpenState[1](false); }, + styles: { body: { display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden', paddingBottom: 0 } }, + footer: React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8 } }, + React.createElement(Button, { onClick: function () { drawerOpenState[1](false); } }, '返回'), + React.createElement(Button, { type: 'primary', onClick: function () { drawerOpenState[1](false); message.success('检查单已保存'); } }, '提交') + ) + }, + React.createElement('div', { style: { flex: 1, minHeight: 0, overflow: 'auto' } }, + React.createElement(Table, { + columns: inspectionColumns, + dataSource: form.inspectionList || [], + rowKey: function (_, i) { return i; }, + pagination: false, + size: 'small' + }) + ) + ), + spareTireOcrModalState[0].open + ? React.createElement(Modal, { + title: '备胎照片识别', + open: true, + onCancel: spareTireOcrModalState[0].phase === 'result' ? closeSpareTireOcrWithoutSave : undefined, + width: 560, + maskClosable: spareTireOcrModalState[0].phase === 'result', + footer: spareTireOcrModalState[0].phase === 'result' + ? React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8 } }, + React.createElement(Button, { onClick: closeSpareTireOcrWithoutSave }, '取消'), + React.createElement(Button, { type: 'primary', onClick: confirmSpareTireOcr }, '确认') + ) + : null, + closable: spareTireOcrModalState[0].phase === 'result' + }, spareTireOcrModalState[0].phase === 'recognizing' + ? React.createElement('div', { style: { padding: '24px 0', textAlign: 'center', fontSize: 15, color: '#666' } }, '正在识别中,请勿关闭页面') + : React.createElement('div', { style: { display: 'flex', gap: 24, alignItems: 'flex-start' } }, + React.createElement('div', { style: { flex: '0 0 200px' } }, + React.createElement('img', { src: spareTireOcrModalState[0].tempUrl, alt: '', style: { width: '100%', display: 'block', borderRadius: 4 } }) + ), + React.createElement('div', { style: { flex: 1 } }, + React.createElement('div', { style: { marginBottom: 8, color: '#333' } }, '识别胎纹深度(mm)'), + React.createElement(Input, { + value: spareTireOcrModalState[0].tempDepth, + onChange: function (e) { setSpareTireOcrDepth(e.target.value); }, + placeholder: '请输入', + addonAfter: 'mm', + style: { width: '100%', maxWidth: 160 } + }) + ) + ) + ) + : null, + photoPreviewState[0].open && photoPreviewState[0].url + ? React.createElement(Modal, { + title: '预览', + open: true, + footer: null, + onCancel: function () { photoPreviewState[1]({ open: false, url: null }); } + }, React.createElement('img', { src: photoPreviewState[0].url, alt: '', style: { width: '100%' } })) + : null + ); +}; + +if (typeof window !== 'undefined') { + window.Component = Component; + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function () { + var rootEl = document.getElementById('root'); + if (rootEl && window.ReactDOM && window.React) { + var root = ReactDOM.createRoot(rootEl); + root.render(React.createElement(Component)); + } + }); + } else { + var rootEl = document.getElementById('root'); + if (rootEl && window.ReactDOM && window.React) { + var root = ReactDOM.createRoot(rootEl); + root.render(React.createElement(Component)); + } + } +} + +export default Component; diff --git a/src/prototypes/oneos-web-ops/pages/04-备车-查看.jsx b/src/prototypes/oneos-web-ops/pages/04-备车-查看.jsx new file mode 100644 index 0000000..c650a20 --- /dev/null +++ b/src/prototypes/oneos-web-ops/pages/04-备车-查看.jsx @@ -0,0 +1,315 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 车辆业务 - 查看备车(与新增备车相同布局,全部只读) + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Input = antd.Input; + var Button = antd.Button; + var Drawer = antd.Drawer; + var Table = antd.Table; + var Modal = antd.Modal; + + var plateManageList = [ + { plateNo: '京A12345', vehicleType: '厢式货车', brand: '东风', model: 'DFH1180', vin: 'LGHXCAE28M1234567', bodyAd: true, tailboard: true }, + { plateNo: '京C11111', vehicleType: '平板货车', brand: '福田', model: 'BJ1180', vin: 'LGHXCAE28M7654321', bodyAd: false, tailboard: false }, + { plateNo: '京D22222', vehicleType: '厢式货车', brand: '江淮', model: 'HFC1180', vin: 'LGHXCAE28M8888888', bodyAd: true, tailboard: false }, + { plateNo: '京E33333', vehicleType: '栏板货车', brand: '重汽', model: 'ZZ1180', vin: 'LGHXCAE28M7777777', bodyAd: false, tailboard: true }, + { plateNo: '京F10001', vehicleType: '厢式货车', brand: '江淮', model: 'HFC1180', vin: 'LGHXCAE28M9990001', bodyAd: true, tailboard: true } + ]; + + var inspectionCategoryItems = { + '车灯': ['大灯', '转向灯', '小灯', '示廓灯', '刹车灯', '倒车灯', '牌照灯', '防雾灯', '室内灯'], + '仪表盘': ['氢系统指示', '电控系统指示', '数值清晰准确', '故障报警灯'], + '驾驶室': ['点烟器', '车窗升降', '按键开关', '雨刮器', '内后视镜是否正常', '内/外门把手', '安全带', '空调冷暖风', '仪表盘', '门锁功能', '手刹', '车钥匙功能是否正常', '喇叭', '音响功能', '遮阳板', '主副驾座椅', '方向盘', '内饰干净整洁'], + '轮胎': ['前左胎', '前右胎', '后左胎', '后右胎'], + '液位检查': ['冷却液', '制动液', '玻璃水'], + '外观检查': ['车身外观', '漆面', '玻璃'], + '车辆外观': ['整车外观'], + '其他': ['其他检查项'], + '随车工具': ['三角牌', '灭火器', '反光背心'], + '随车证件': ['行驶证', '营运证', '保险单'], + '整车': ['整车状态'], + '燃料电池系统': ['氢系统', '储氢瓶'], + '冷机': ['冷机运行'], + '制动系统': ['制动踏板', '驻车制动'] + }; + + function buildInspectionList() { + var list = []; + var categories = Object.keys(inspectionCategoryItems); + for (var i = 0; i < categories.length; i++) { + var cat = categories[i]; + var items = inspectionCategoryItems[cat]; + for (var j = 0; j < items.length; j++) { + list.push({ category: cat, checkItem: items[j], checked: true, remark: '', treadDepth: '' }); + } + } + return list; + } + + // 只读:一条已填写的备车记录(与新增备车字段一致,全部不可编辑) + var form = useMemo(function () { + return { + plateNo: '京A12345', + vehicleType: '厢式货车', + brand: '东风', + model: 'DFH1180', + vin: 'LGHXCAE28M1234567', + bodyAd: true, + adPhotos: ['https://picsum.photos/200/150?r=1'], + enlargePhotos: ['https://picsum.photos/200/150?r=2'], + tailboard: true, + spareTirePhotos: ['https://picsum.photos/200/150?r=3'], + spareTireTreadDepth: '5.2', + flawPhotos: ['https://picsum.photos/200/150?f=1'], + inspectionList: buildInspectionList() + }; + }, []); + + var drawerOpenState = useState(false); + var photoPreviewState = useState({ open: false, url: null }); + var reqDocOpenState = useState(false); + + var styles = { + page: { padding: '16px 24px 80px', backgroundColor: '#f5f5f5', minHeight: '100vh', fontSize: 14 }, + breadcrumb: { marginBottom: 16, color: '#666' }, + breadcrumbSep: { margin: '0 8px', color: '#999' }, + card: { backgroundColor: '#fff', borderRadius: 8, marginBottom: 16, boxShadow: '0 1px 2px rgba(0,0,0,0.05)', overflow: 'hidden' }, + cardBody: { padding: '20px 24px' }, + formRow: { display: 'flex', flexWrap: 'wrap', marginBottom: 16 }, + formCol: { flex: '0 0 33.33%', minWidth: 200, paddingRight: 16, marginBottom: 8, boxSizing: 'border-box' }, + formColFull: { flex: '0 0 100%', marginBottom: 8, boxSizing: 'border-box' }, + label: { display: 'block', marginBottom: 6, color: '#333' }, + labelRequired: { color: '#ff4d4f', marginRight: 4 }, + footer: { position: 'fixed', bottom: 0, left: 0, right: 0, padding: '12px 24px', backgroundColor: '#fff', borderTop: '1px solid #e8e8e8', display: 'flex', gap: 12, zIndex: 99 } + }; + + var FormItem = function (props) { + var colStyle = props.fullWidth ? styles.formColFull : styles.formCol; + return React.createElement('div', { style: colStyle }, + React.createElement('label', { style: styles.label }, + props.required ? React.createElement('span', { style: styles.labelRequired }, '*') : null, + props.label + ), + props.children + ); + }; + + var inspectionList = form.inspectionList || []; + var categoryRowSpans = useMemo(function () { + var list = inspectionList; + var spans = []; + var i = 0; + while (i < list.length) { + var cat = list[i].category; + var start = i; + while (i < list.length && list[i].category === cat) i++; + spans[start] = i - start; + for (var j = start + 1; j < i; j++) spans[j] = 0; + } + return spans; + }, [form.inspectionList]); + + var inspectionColumns = [ + { + title: '类别', + dataIndex: 'category', + key: 'category', + width: 120, + onCell: function (_, index) { return { rowSpan: categoryRowSpans[index] !== undefined ? categoryRowSpans[index] : 1 }; } + }, + { title: '检查项目', dataIndex: 'checkItem', key: 'checkItem', width: 180 }, + { + title: '检查情况', + key: 'checked', + width: 140, + render: function (_, record) { + if (record.category === '轮胎') return record.treadDepth ? record.treadDepth + ' mm' : '-'; + return record.checked !== false ? '正常' : '异常'; + } + }, + { title: '备注', dataIndex: 'remark', key: 'remark', render: function (val) { return val || '-'; } } + ]; + + var requirementDocContent = '新增备车\n\n1.面包屑:\n1.1.运维管理-车辆业务-备车管理-新增备车\n\n2.表单:\n2.1.车牌号:必选项,选择器,支持从输入框内输入内容进行模糊搜索,默认拉取「车牌管理」中所有「车牌号」;\n2.2.车辆类型:输入框禁用,选择车牌号后自动拉取该车牌号对应「车辆类型」;\n2.3.品牌:输入框禁用,选择车牌号后自动拉取该车牌号对应「品牌」;\n2.4.型号:输入框禁用,选择车牌号后自动拉取该车牌号对应「型号」\n2.5.车辆识别代码:输入框禁用,选择车牌号后自动拉取该车牌号「车辆识别代码」;\n2.6.车身广告:开关,选择车辆后拉取该车辆「后装设备」「车身广告」,如果该车辆有「车身广告」则勾选为开,如果无则勾选为无。同时如果手动进行操作,会同步到「后装设备」「车身广告」中,安装时间以该条备车记录提交成功为准;\n2.7.广告照片:图片上传,最多支持上传4张图片,支持主流照片格式。上传后,上传按钮后方横排显示已上传图片缩略图,支持点击预览和删除;\n2.8.尾板:开关,选择车辆后拉取该车辆「后装设备」「尾板」,如果该车辆有「尾板」则勾选为开,如果无则勾选为无。同时如果手动进行操作,会同步到「后装设备」「尾板」中,安装时间以该条备车记录提交成功为准;\n2.9.是否有挂:输入框,提示信息为:请输入挂车牌号,不输入为无挂;\n2.10.瑕疵:图片上传,最多支持上传4张图片,支持主流照片格式。上传后,上传按钮后方横排显示已上传图片缩略图,支持点击预览和删除;\n2.11.车辆检查:按钮,文字为备车检查单,点击右侧展开抽屉,抽屉内显示列表,字段为类别、检查项目、选择、备注;\n 2.11.1.类别:分为车灯、仪表盘、驾驶室、轮胎、液位检查、外观检查、车辆外观、其他、随车工具、随车证件、整车、燃料电池系统、冷机、制动系统;\n 2.11.2.检查项目:车灯类别对应(大灯、转向灯、小灯、示廓灯、刹车灯、倒车灯、牌照灯、防雾灯、室内灯)、仪表盘对应(氢系统指示、电控系统指示、数值清晰准确、故障报警灯)、驾驶室对应(点烟器、车窗升降、按键开关、雨刮器、内后视镜是否正常、内/外门把手、安全带、空调冷暖风、仪表盘、门锁功能、手刹、车钥匙功能是否正常、喇叭、音响功能、遮阳板、主副驾座椅、方向盘、内饰干净整洁)\n 2.11.3.检查情况:开关,在检查项目每项后方显示,默认为开,可手动进行关闭;\n 2.11.4.备注:输入框;\n2.12.下方为提交和保存;\n 2.12.1.提交:点击提交,toast提示:备车成功,同时保存该条数据并跳转至「备车管理」「已完成」;\n 2.12.2.保存:点击保存,toast提示:暂存成功,同时保存该条数据并跳转至「备车管理」「待提交」;'; + + var breadcrumbNodes = [ + React.createElement('span', { key: '1' }, '运维管理'), + React.createElement('span', { key: '2', style: styles.breadcrumbSep }, ' / '), + React.createElement('span', { key: '3' }, '车辆业务'), + React.createElement('span', { key: '4', style: styles.breadcrumbSep }, ' / '), + React.createElement('span', { key: '5' }, '备车管理'), + React.createElement('span', { key: '6', style: styles.breadcrumbSep }, ' / '), + React.createElement('span', { key: '7', style: { color: '#1890ff' } }, '查看备车') + ]; + + return React.createElement('div', { style: styles.page }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + React.createElement('div', { style: styles.breadcrumb }, breadcrumbNodes), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { reqDocOpenState[1](true); } }, '查看需求说明') + ), + React.createElement('div', { style: styles.card }, + React.createElement('div', { style: styles.cardBody }, + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '车牌号', required: true }, + React.createElement(Input, { value: form.plateNo, disabled: true, style: { width: '100%', background: '#f5f5f5' } }) + ), + React.createElement(FormItem, { label: '车辆类型' }, + React.createElement(Input, { value: form.vehicleType, disabled: true, style: { width: '100%', background: '#f5f5f5' } }) + ), + React.createElement(FormItem, { label: '品牌' }, + React.createElement(Input, { value: form.brand, disabled: true, style: { width: '100%', background: '#f5f5f5' } }) + ), + React.createElement(FormItem, { label: '型号' }, + React.createElement(Input, { value: form.model, disabled: true, style: { width: '100%', background: '#f5f5f5' } }) + ), + React.createElement(FormItem, { label: '车辆识别代码' }, + React.createElement(Input, { value: form.vin, disabled: true, style: { width: '100%', background: '#f5f5f5' } }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '车身广告及放大字' }, + React.createElement(Input, { value: form.bodyAd ? '有' : '无', disabled: true, style: { width: '100%', background: '#f5f5f5' } }) + ), + form.bodyAd ? React.createElement(FormItem, { label: '广告照片' }, + React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 } }, + (form.adPhotos || []).map(function (url, i) { + return React.createElement('img', { + key: i, + src: url, + alt: '', + style: { width: 64, height: 64, objectFit: 'cover', borderRadius: 4, cursor: 'pointer' }, + onClick: function () { photoPreviewState[1]({ open: true, url: url }); } + }); + }), + (form.adPhotos || []).length === 0 ? React.createElement('span', { style: { color: '#999' } }, '无') : null + ) + ) : null, + form.bodyAd ? React.createElement(FormItem, { label: '放大字照片' }, + React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 } }, + (form.enlargePhotos || []).map(function (url, i) { + return React.createElement('img', { + key: i, + src: url, + alt: '', + style: { width: 64, height: 64, objectFit: 'cover', borderRadius: 4, cursor: 'pointer' }, + onClick: function () { photoPreviewState[1]({ open: true, url: url }); } + }); + }), + (form.enlargePhotos || []).length === 0 ? React.createElement('span', { style: { color: '#999' } }, '无') : null + ) + ) : null + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '尾板' }, + React.createElement(Input, { value: form.tailboard ? '有' : '无', disabled: true, style: { width: '100%', background: '#f5f5f5' } }) + ), + React.createElement(FormItem, { label: '备胎照片' }, + React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 } }, + (form.spareTirePhotos || []).map(function (url, i) { + return React.createElement('img', { + key: i, + src: url, + alt: '', + style: { width: 64, height: 64, objectFit: 'cover', borderRadius: 4, cursor: 'pointer' }, + onClick: function () { photoPreviewState[1]({ open: true, url: url }); } + }); + }), + (form.spareTirePhotos || []).length === 0 ? React.createElement('span', { style: { color: '#999' } }, '无') : null + ) + ), + React.createElement(FormItem, { label: '备胎胎纹深度' }, + React.createElement(Input, { value: form.spareTireTreadDepth || '', disabled: true, style: { width: '100%', background: '#f5f5f5' }, addonAfter: 'mm' }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '瑕疵', fullWidth: true }, + React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 } }, + (form.flawPhotos || []).map(function (url, i) { + return React.createElement('img', { + key: i, + src: url, + alt: '', + style: { width: 64, height: 64, objectFit: 'cover', borderRadius: 4, cursor: 'pointer' }, + onClick: function () { photoPreviewState[1]({ open: true, url: url }); } + }); + }), + (form.flawPhotos || []).length === 0 ? React.createElement('span', { style: { color: '#999' } }, '无') : null + ) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '车辆检查', fullWidth: true }, + React.createElement(Button, { type: 'default', onClick: function () { drawerOpenState[1](true); } }, '备车检查单') + ) + ) + ) + ), + React.createElement('div', { style: styles.footer }, + React.createElement(Button, { onClick: function () { if (typeof window !== 'undefined' && window.history) window.history.back(); } }, '返回') + ), + React.createElement(Drawer, { + title: '备车检查单', + placement: 'right', + width: 640, + open: drawerOpenState[0], + onClose: function () { drawerOpenState[1](false); }, + styles: { body: { display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden', paddingBottom: 0 } }, + footer: React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end' } }, + React.createElement(Button, { onClick: function () { drawerOpenState[1](false); } }, '返回') + ) + }, + React.createElement('div', { style: { flex: 1, minHeight: 0, overflow: 'auto' } }, + React.createElement(Table, { + columns: inspectionColumns, + dataSource: form.inspectionList || [], + rowKey: function (_, i) { return i; }, + pagination: false, + size: 'small' + }) + ) + ), + React.createElement(Modal, { + title: '需求说明', + open: reqDocOpenState[0], + onCancel: function () { reqDocOpenState[1](false); }, + footer: null, + width: 720, + styles: { body: { maxHeight: '70vh', overflow: 'auto', whiteSpace: 'pre-wrap', fontSize: 14, lineHeight: 1.8 } } + }, requirementDocContent), + photoPreviewState[0].open && photoPreviewState[0].url + ? React.createElement(Modal, { + title: '预览', + open: true, + footer: null, + onCancel: function () { photoPreviewState[1]({ open: false, url: null }); } + }, React.createElement('img', { src: photoPreviewState[0].url, alt: '', style: { width: '100%' } })) + : null + ); +}; + +if (typeof window !== 'undefined') { + window.Component = Component; + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function () { + var rootEl = document.getElementById('root'); + if (rootEl && window.ReactDOM && window.React) { + var root = ReactDOM.createRoot(rootEl); + root.render(React.createElement(Component)); + } + }); + } else { + var rootEl = document.getElementById('root'); + if (rootEl && window.ReactDOM && window.React) { + var root = ReactDOM.createRoot(rootEl); + root.render(React.createElement(Component)); + } + } +} + +export default Component; diff --git a/src/prototypes/oneos-web-ops/pages/05-备车-新增.jsx b/src/prototypes/oneos-web-ops/pages/05-备车-新增.jsx new file mode 100644 index 0000000..a90dba4 --- /dev/null +++ b/src/prototypes/oneos-web-ops/pages/05-备车-新增.jsx @@ -0,0 +1,660 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 车辆业务 - 新增备车(ONEOS运管平台,布局参照新增租赁合同) + +const Component = function () { + var useState = React.useState; + var useCallback = React.useCallback; + var useMemo = React.useMemo; + var useRef = React.useRef; + var useEffect = React.useEffect; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Select = antd.Select; + var Input = antd.Input; + var Button = antd.Button; + var Switch = antd.Switch; + var Drawer = antd.Drawer; + var Table = antd.Table; + var message = antd.message; + var Modal = antd.Modal; + var Space = antd.Space; + + // 车牌管理 mock:车牌号及对应车辆信息、后装设备(车身广告、尾板) + var plateManageList = [ + { plateNo: '京A12345', vehicleType: '厢式货车', brand: '东风', model: 'DFH1180', vin: 'LGHXCAE28M1234567', bodyAd: true, tailboard: true }, + { plateNo: '京C11111', vehicleType: '平板货车', brand: '福田', model: 'BJ1180', vin: 'LGHXCAE28M7654321', bodyAd: false, tailboard: false }, + { plateNo: '京D22222', vehicleType: '厢式货车', brand: '江淮', model: 'HFC1180', vin: 'LGHXCAE28M8888888', bodyAd: true, tailboard: false }, + { plateNo: '京E33333', vehicleType: '栏板货车', brand: '重汽', model: 'ZZ1180', vin: 'LGHXCAE28M7777777', bodyAd: false, tailboard: true }, + { plateNo: '京F10001', vehicleType: '厢式货车', brand: '江淮', model: 'HFC1180', vin: 'LGHXCAE28M9990001', bodyAd: true, tailboard: true } + ]; + + // 备车检查单:类别 -> 检查项目列表(与需求 2.11.1、2.11.2 一致) + var inspectionCategoryItems = { + '车灯': ['大灯', '转向灯', '小灯', '示廓灯', '刹车灯', '倒车灯', '牌照灯', '防雾灯', '室内灯'], + '仪表盘': ['氢系统指示', '电控系统指示', '数值清晰准确', '故障报警灯'], + '驾驶室': ['点烟器', '车窗升降', '按键开关', '雨刮器', '内后视镜是否正常', '内/外门把手', '安全带', '空调冷暖风', '仪表盘', '门锁功能', '手刹', '车钥匙功能是否正常', '喇叭', '音响功能', '遮阳板', '主副驾座椅', '方向盘', '内饰干净整洁'], + '轮胎': ['前左胎', '前右胎', '后左胎', '后右胎'], + '液位检查': ['冷却液', '制动液', '玻璃水'], + '外观检查': ['车身外观', '漆面', '玻璃'], + '车辆外观': ['整车外观'], + '其他': ['其他检查项'], + '随车工具': ['三角牌', '灭火器', '反光背心'], + '随车证件': ['行驶证', '营运证', '保险单'], + '整车': ['整车状态'], + '燃料电池系统': ['氢系统', '储氢瓶'], + '冷机': ['冷机运行'], + '制动系统': ['制动踏板', '驻车制动'] + }; + + function buildInspectionList() { + var list = []; + var categories = Object.keys(inspectionCategoryItems); + for (var i = 0; i < categories.length; i++) { + var cat = categories[i]; + var items = inspectionCategoryItems[cat]; + for (var j = 0; j < items.length; j++) { + list.push({ category: cat, checkItem: items[j], checked: true, remark: '', treadDepth: '' }); + } + } + return list; + } + + var formState = useState({ + plateNo: '', + vehicleType: '', + brand: '', + model: '', + vin: '', + bodyAd: true, + adPhotos: [], // 广告照片,最多1张 + enlargePhotos: [], // 放大字照片,最多1张 + tailboard: false, + spareTirePhotos: [], // 备胎照片,最多1张 + spareTireTreadDepth: '', // 备胎胎纹深度,OCR反写可改 + flawPhotos: [], // 瑕疵,最多4张 + inspectionList: buildInspectionList() + }); + var form = formState[0]; + var setForm = formState[1]; + + var drawerOpenState = useState(false); + var photoPreviewState = useState({ open: false, url: null }); + var reqDocOpenState = useState(false); + var adPhotoInputRef = useRef(null); + var enlargePhotoInputRef = useRef(null); + var spareTirePhotoInputRef = useRef(null); + var flawPhotoInputRef = useRef(null); + // 备胎照片 OCR 弹窗:识别中 -> 左侧照片、右侧胎纹深度输入、确认反写 + var spareTireOcrModalState = useState({ open: false, tempUrl: null, tempDepth: '', phase: 'recognizing' }); + var spareTireOcrPhaseRef = useRef(null); + var plateOptions = useMemo(function () { + return plateManageList.map(function (p) { return { value: p.plateNo, label: p.plateNo }; }); + }, []); + + var onPlateSelect = useCallback(function (plateNo) { + var vehicle = plateManageList.find(function (p) { return p.plateNo === plateNo; }); + if (!vehicle) return; + setForm(function (prev) { + var next = {}; + for (var k in prev) next[k] = prev[k]; + next.plateNo = vehicle.plateNo; + next.vehicleType = vehicle.vehicleType || ''; + next.brand = vehicle.brand || ''; + next.model = vehicle.model || ''; + next.vin = vehicle.vin || ''; + next.bodyAd = !!vehicle.bodyAd; + next.tailboard = !!vehicle.tailboard; + return next; + }); + }, []); + + var updateInspection = useCallback(function (index, field, value) { + setForm(function (prev) { + var n = {}; + for (var k in prev) n[k] = prev[k]; + var list = (prev.inspectionList || []).slice(); + var row = list[index] || {}; + list[index] = Object.assign({}, row, { [field]: value }); + n.inspectionList = list; + return n; + }); + }, []); + + // 广告照片:最多1张 + var addAdPhoto = useCallback(function () { + if ((form.adPhotos || []).length >= 1) { message.warning('最多上传1张'); return; } + if (adPhotoInputRef.current) adPhotoInputRef.current.click(); + }, [form.adPhotos]); + var onAdPhotoFileChange = useCallback(function (e) { + var files = e.target.files; + if (!files || files.length === 0) return; + var file = files[0]; + if (!file || file.type.indexOf('image') === -1) { e.target.value = ''; return; } + var url = URL.createObjectURL(file); + setForm(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.adPhotos = [url]; return n; }); + e.target.value = ''; + }, []); + var removeAdPhoto = useCallback(function () { + setForm(function (p) { + var n = {}; for (var k in p) n[k] = p[k]; + var arr = (p.adPhotos || [])[0]; + if (arr && typeof arr === 'string' && arr.indexOf('blob:') === 0) URL.revokeObjectURL(arr); + n.adPhotos = []; + return n; + }); + }, []); + // 放大字照片:最多1张 + var addEnlargePhoto = useCallback(function () { + if ((form.enlargePhotos || []).length >= 1) { message.warning('最多上传1张'); return; } + if (enlargePhotoInputRef.current) enlargePhotoInputRef.current.click(); + }, [form.enlargePhotos]); + var onEnlargePhotoFileChange = useCallback(function (e) { + var files = e.target.files; + if (!files || files.length === 0) return; + var file = files[0]; + if (!file || file.type.indexOf('image') === -1) { e.target.value = ''; return; } + var url = URL.createObjectURL(file); + setForm(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.enlargePhotos = [url]; return n; }); + e.target.value = ''; + }, []); + var removeEnlargePhoto = useCallback(function () { + setForm(function (p) { + var n = {}; for (var k in p) n[k] = p[k]; + var u = (p.enlargePhotos || [])[0]; + if (u && typeof u === 'string' && u.indexOf('blob:') === 0) URL.revokeObjectURL(u); + n.enlargePhotos = []; + return n; + }); + }, []); + // 备胎照片:上传后弹窗识别,确认反写至备胎胎纹深度 + var addSpareTirePhoto = useCallback(function () { + if ((form.spareTirePhotos || []).length >= 1) { message.warning('最多上传1张'); return; } + if (spareTirePhotoInputRef.current) spareTirePhotoInputRef.current.click(); + }, [form.spareTirePhotos]); + var onSpareTirePhotoFileChange = useCallback(function (e) { + var files = e.target.files; + if (!files || files.length === 0) return; + var file = files[0]; + if (!file || file.type.indexOf('image') === -1) { e.target.value = ''; return; } + var url = URL.createObjectURL(file); + spareTireOcrModalState[1]({ open: true, tempUrl: url, tempDepth: '', phase: 'recognizing' }); + e.target.value = ''; + }, []); + useEffect(function () { + var s = spareTireOcrModalState[0]; + if (!s.open || s.phase !== 'recognizing') return; + if (spareTireOcrPhaseRef.current) clearTimeout(spareTireOcrPhaseRef.current); + spareTireOcrPhaseRef.current = setTimeout(function () { + spareTireOcrModalState[1](function (prev) { + return prev.open ? { open: true, tempUrl: prev.tempUrl, tempDepth: '5.2', phase: 'result' } : prev; + }); + }, 1500); + return function () { + if (spareTireOcrPhaseRef.current) clearTimeout(spareTireOcrPhaseRef.current); + }; + }, [spareTireOcrModalState[0].open, spareTireOcrModalState[0].phase]); + var confirmSpareTireOcr = useCallback(function () { + var s = spareTireOcrModalState[0]; + var depth = (s.tempDepth || '').trim(); + setForm(function (p) { + var n = {}; for (var k in p) n[k] = p[k]; + n.spareTireTreadDepth = depth; + n.spareTirePhotos = s.tempUrl ? [s.tempUrl] : []; + return n; + }); + spareTireOcrModalState[1]({ open: false, tempUrl: null, tempDepth: '', phase: 'recognizing' }); + }, []); + var closeSpareTireOcrWithoutSave = useCallback(function () { + var s = spareTireOcrModalState[0]; + if (s.tempUrl && s.tempUrl.indexOf('blob:') === 0) URL.revokeObjectURL(s.tempUrl); + spareTireOcrModalState[1]({ open: false, tempUrl: null, tempDepth: '', phase: 'recognizing' }); + }, []); + var setSpareTireOcrDepth = useCallback(function (v) { + spareTireOcrModalState[1](function (prev) { return { open: prev.open, tempUrl: prev.tempUrl, tempDepth: v, phase: prev.phase }; }); + }, []); + var removeSpareTirePhoto = useCallback(function () { + setForm(function (p) { + var n = {}; for (var k in p) n[k] = p[k]; + var u = (p.spareTirePhotos || [])[0]; + if (u && typeof u === 'string' && u.indexOf('blob:') === 0) URL.revokeObjectURL(u); + n.spareTirePhotos = []; return n; + }); + }, []); + var addFlawPhoto = useCallback(function () { + if ((form.flawPhotos || []).length >= 4) { message.warning('最多上传4张'); return; } + if (flawPhotoInputRef.current) flawPhotoInputRef.current.click(); + }, [form.flawPhotos]); + var onFlawPhotoFileChange = useCallback(function (e) { + var files = e.target.files; + if (!files || files.length === 0) return; + setForm(function (p) { + var current = p.flawPhotos || []; + var remain = 4 - current.length; + if (remain <= 0) return p; + var urls = []; + for (var i = 0; i < files.length && urls.length < remain; i++) { + if (files[i].type.indexOf('image') !== -1) urls.push(URL.createObjectURL(files[i])); + } + if (urls.length === 0) return p; + var n = {}; for (var k in p) n[k] = p[k]; n.flawPhotos = current.concat(urls); return n; + }); + e.target.value = ''; + }, []); + var removeFlawPhoto = useCallback(function (idx) { + setForm(function (p) { + var n = {}; + for (var k in p) n[k] = p[k]; + var arr = (p.flawPhotos || []).slice(); + var url = arr[idx]; + if (url && url.indexOf('blob:') === 0) URL.revokeObjectURL(url); + arr.splice(idx, 1); + n.flawPhotos = arr; + return n; + }); + }, []); + + function validateForm() { + if (!form.plateNo) { message.warning('请选择车牌号'); return false; } + if (form.bodyAd && (!form.adPhotos || form.adPhotos.length < 1)) { message.warning('请上传广告照片'); return false; } + if (form.bodyAd && (!form.enlargePhotos || form.enlargePhotos.length < 1)) { message.warning('请上传放大字照片'); return false; } + if (!form.spareTirePhotos || form.spareTirePhotos.length < 1) { message.warning('请上传备胎照片'); return false; } + if (!String(form.spareTireTreadDepth || '').trim()) { message.warning('请填写备胎胎纹深度'); return false; } + var list = form.inspectionList || []; + for (var i = 0; i < list.length; i++) { + if (list[i].category === '轮胎' && !String(list[i].treadDepth || '').trim()) { + message.warning('请完善备车检查单:填写轮胎胎纹深度'); + return false; + } + if (!String(list[i].remark || '').trim()) { + message.warning('请完善备车检查单:所有项备注为必填'); + return false; + } + } + return true; + } + var handleSubmit = useCallback(function () { + if (!validateForm()) return; + message.success('备车成功'); + // 保存数据并跳转至备车管理-已完成(实际项目接路由) + if (typeof window !== 'undefined' && window.history) window.history.back(); + }, [form.plateNo, form.spareTirePhotos, form.spareTireTreadDepth, form.inspectionList]); + var handleSave = useCallback(function () { + if (!validateForm()) return; + message.success('暂存成功'); + // 保存数据并跳转至备车管理-待提交 + if (typeof window !== 'undefined' && window.history) window.history.back(); + }, [form.plateNo, form.spareTirePhotos, form.spareTireTreadDepth, form.inspectionList]); + var handleCancel = useCallback(function () { + if (typeof window !== 'undefined' && window.history) window.history.back(); + else message.info('取消'); + }, []); + + // 布局样式(参照新增租赁合同) + var styles = { + page: { padding: '16px 24px 80px', backgroundColor: '#f5f5f5', minHeight: '100vh', fontSize: 14 }, + breadcrumb: { marginBottom: 16, color: '#666' }, + breadcrumbSep: { margin: '0 8px', color: '#999' }, + card: { backgroundColor: '#fff', borderRadius: 8, marginBottom: 16, boxShadow: '0 1px 2px rgba(0,0,0,0.05)', overflow: 'hidden' }, + cardBody: { padding: '20px 24px' }, + formRow: { display: 'flex', flexWrap: 'wrap', marginBottom: 16 }, + formCol: { flex: '0 0 33.33%', minWidth: 200, paddingRight: 16, marginBottom: 8, boxSizing: 'border-box' }, + formColFull: { flex: '0 0 100%', marginBottom: 8, boxSizing: 'border-box' }, + label: { display: 'block', marginBottom: 6, color: '#333' }, + labelRequired: { color: '#ff4d4f', marginRight: 4 }, + footer: { position: 'fixed', bottom: 0, left: 0, right: 0, padding: '12px 24px', backgroundColor: '#fff', borderTop: '1px solid #e8e8e8', display: 'flex', gap: 12, zIndex: 99 } + }; + + var FormItem = function (props) { + var colStyle = props.fullWidth ? styles.formColFull : styles.formCol; + return React.createElement('div', { style: colStyle }, + React.createElement('label', { style: styles.label }, + props.required ? React.createElement('span', { style: styles.labelRequired }, '*') : null, + props.label + ), + props.children + ); + }; + + var inspectionList = form.inspectionList || []; + var categoryRowSpans = useMemo(function () { + var list = inspectionList; + var spans = []; + var i = 0; + while (i < list.length) { + var cat = list[i].category; + var start = i; + while (i < list.length && list[i].category === cat) i++; + spans[start] = i - start; + for (var j = start + 1; j < i; j++) spans[j] = 0; + } + return spans; + }, [form.inspectionList]); + + var inspectionColumns = [ + { + title: '类别', + dataIndex: 'category', + key: 'category', + width: 120, + onCell: function (_, index) { return { rowSpan: categoryRowSpans[index] !== undefined ? categoryRowSpans[index] : 1 }; } + }, + { title: '检查项目', dataIndex: 'checkItem', key: 'checkItem', width: 180 }, + { + title: '检查情况', + key: 'checked', + width: 140, + render: function (_, record, index) { + if (record.category === '轮胎') { + return React.createElement(Input, { + value: record.treadDepth, + onChange: function (e) { updateInspection(index, 'treadDepth', e.target.value); }, + placeholder: '请输入胎纹深度', + style: { width: '100%' } + }); + } + return React.createElement(Switch, { + checked: record.checked !== false, + checkedChildren: '正常', + unCheckedChildren: '异常', + onChange: function (checked) { updateInspection(index, 'checked', checked); } + }); + } + }, + { + title: '备注', + dataIndex: 'remark', + key: 'remark', + render: function (val, record, index) { + return React.createElement(Input, { + value: val, + onChange: function (e) { updateInspection(index, 'remark', e.target.value); }, + placeholder: '必填' + }); + } + } + ]; + + // 方形拍照图标(广告照片与瑕疵共用) + var cameraIconBox = function (onClick) { + return React.createElement('div', { + role: 'button', + tabIndex: 0, + onClick: onClick, + style: { width: 64, height: 64, border: '1px dashed #d9d9d9', borderRadius: 4, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', background: '#fafafa', boxSizing: 'border-box' } + }, React.createElement('svg', { width: 28, height: 28, viewBox: '0 0 24 24', fill: 'none', stroke: '#999', strokeWidth: 2 }, + React.createElement('path', { d: 'M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z' }), + React.createElement('circle', { cx: 12, cy: 13, r: 4 }) + )); + }; + + var requirementDocContent = '新增备车\n一个「数字化资产ONEOS运管平台」中的「新增」模块\n1.面包屑:\n1.1.运维管理-车辆业务-备车管理-新增\n\n2.表单:\n2.1.车牌号:必选项,选择器,支持从输入框内输入内容进行模糊搜索,默认拉取「车牌管理」中所有「车牌号」;\n2.2.车辆类型:输入框禁用,选择车牌号后自动拉取该车牌号对应「车辆类型」;\n2.3.品牌:输入框禁用,选择车牌号后自动拉取该车牌号对应「品牌」;\n2.4.型号:输入框禁用,选择车牌号后自动拉取该车牌号对应「型号」\n2.5.车辆识别代码:输入框禁用,选择车牌号后自动拉取该车牌号「车辆识别代码」;\n2.6.车身广告及放大字:开关,选择车辆后拉取该车辆「后装设备」「车身广告」,如果该车辆有「车身广告」则勾选为开,如果无则勾选为无。同时如果手动进行操作,会同步到「后装设备」「车身广告」中,安装时间以该条备车记录提交成功为准;\n2.7.广告照片:图片上传,最多支持上传1张图片,支持主流照片格式。上传后,上传按钮切换为显示已上传图片缩略图,支持点击预览和删除,删除后,切换为上传按钮;\n2.8.放大字照片:图片上传,最多支持上传1张图片,支持主流照片格式。上传后,上传按钮切换为显示已上传图片缩略图,支持点击预览和删除,删除后,切换为上传按钮;\n2.8.尾板:开关,选择车辆后拉取该车辆「后装设备」「尾板」,如果该车辆有「尾板」则勾选为开,如果无则勾选为无。同时如果手动进行操作,会同步到「后装设备」「尾板」中,安装时间以该条备车记录提交成功为准;\n2.9.备胎照片:图片上传,最多支持上传1张图片,支持主流照片格式。上传时弹出卡片,提示正在识别中,请勿关闭页面,之后卡片左侧显示备胎照片,右侧输入框显示识别出的胎纹深度,后缀单位为mm,点击卡片中确认按钮,反写至备胎胎纹深度字段下;\n2.10.备胎胎纹深度:输入框,反写备胎照片OCR识别胎纹深度结果,支持修改;\n2.11.瑕疵:图片上传,最多支持上传4张图片,支持主流照片格式。上传后,上传按钮后方横排显示已上传图片缩略图,支持点击预览和删除;\n2.12.车辆检查:按钮,文字为备车检查单,点击右侧展开抽屉,抽屉内显示列表,字段为类别、检查项目、选择、备注;\n 2.11.1.类别:分为车灯、仪表盘、驾驶室、轮胎、液位检查、外观检查、车辆外观、其他、随车工具、随车证件、整车、燃料电池系统、冷机、制动系统;\n 2.11.2.检查项目:车灯类别对应(大灯、转向灯、小灯、示廓灯、刹车灯、倒车灯、牌照灯、防雾灯、室内灯)、仪表盘对应(氢系统指示、电控系统指示、数值清晰准确、故障报警灯)、驾驶室对应(点烟器、车窗升降、按键开关、雨刮器、内后视镜是否正常、内/外门把手、安全带、空调冷暖风、仪表盘、门锁功能、手刹、车钥匙功能是否正常、喇叭、音响功能、遮阳板、主副驾座椅、方向盘、内饰干净整洁)\n 2.11.3.检查情况:其他项为开关,在检查项目每项后方显示,默认为开,可手动进行关闭,轮胎为输入框,提示请输入胎纹深度;\n 2.11.4.备注:输入框;\n2.12.下方为提交和保存;\n 2.12.1.提交:点击提交,toast提示:备车成功,同时保存该条数据并跳转至「备车管理」「已完成」;\n 2.12.2.保存:点击保存,toast提示:暂存成功,同时保存该条数据并跳转至「备车管理」「待提交」;'; + + var breadcrumbNodes = [ + React.createElement('span', { key: '1' }, '运维管理'), + React.createElement('span', { key: '2', style: styles.breadcrumbSep }, ' / '), + React.createElement('span', { key: '3' }, '车辆业务'), + React.createElement('span', { key: '4', style: styles.breadcrumbSep }, ' / '), + React.createElement('span', { key: '5' }, '备车管理'), + React.createElement('span', { key: '6', style: styles.breadcrumbSep }, ' / '), + React.createElement('span', { key: '7', style: { color: '#1890ff' } }, '新增') + ]; + + return React.createElement('div', { style: styles.page }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + React.createElement('div', { style: styles.breadcrumb }, breadcrumbNodes), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { reqDocOpenState[1](true); } }, '查看需求说明') + ), + React.createElement('div', { style: styles.card }, + React.createElement('div', { style: styles.cardBody }, + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '车牌号', required: true }, + React.createElement(Select, { + placeholder: '请选择或输入车牌号', + style: { width: '100%' }, + value: form.plateNo || undefined, + onChange: onPlateSelect, + showSearch: true, + allowClear: true, + optionFilterProp: 'label', + filterOption: function (input, opt) { + return (opt && opt.label && String(opt.label).toLowerCase().indexOf((input || '').toLowerCase()) >= 0); + }, + options: plateOptions + }) + ), + React.createElement(FormItem, { label: '车辆类型' }, + React.createElement(Input, { value: form.vehicleType, disabled: true, style: { width: '100%', background: '#f5f5f5' } }) + ), + React.createElement(FormItem, { label: '品牌' }, + React.createElement(Input, { value: form.brand, disabled: true, style: { width: '100%', background: '#f5f5f5' } }) + ), + React.createElement(FormItem, { label: '型号' }, + React.createElement(Input, { value: form.model, disabled: true, style: { width: '100%', background: '#f5f5f5' } }) + ), + React.createElement(FormItem, { label: '车辆识别代码' }, + React.createElement(Input, { value: form.vin, disabled: true, style: { width: '100%', background: '#f5f5f5' } }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '车身广告及放大字', required: true }, + React.createElement(Switch, { + checked: form.bodyAd, + checkedChildren: '有', + unCheckedChildren: '无', + onChange: function (checked) { + setForm(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.bodyAd = checked; return n; }); + } + }) + ), + form.bodyAd ? React.createElement(FormItem, { label: '广告照片', required: true }, + React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 } }, + React.createElement('input', { type: 'file', ref: adPhotoInputRef, accept: 'image/*', style: { display: 'none' }, onChange: onAdPhotoFileChange }), + (form.adPhotos || []).length < 1 ? cameraIconBox(addAdPhoto) : null, + (form.adPhotos || []).map(function (url, i) { + return React.createElement('span', { key: i, style: { position: 'relative', display: 'inline-block' } }, + React.createElement('img', { + src: url, + alt: '', + style: { width: 64, height: 64, objectFit: 'cover', borderRadius: 4, cursor: 'pointer' }, + onClick: function () { photoPreviewState[1]({ open: true, url: url }); } + }), + React.createElement('span', { + style: { position: 'absolute', top: -6, right: -6, width: 20, height: 20, borderRadius: '50%', background: '#ff4d4f', color: '#fff', fontSize: 12, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }, + onClick: function () { removeAdPhoto(); } + }, '×') + ); + }) + ) + ) : null, + form.bodyAd ? React.createElement(FormItem, { label: '放大字照片', required: true }, + React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 } }, + React.createElement('input', { type: 'file', ref: enlargePhotoInputRef, accept: 'image/*', style: { display: 'none' }, onChange: onEnlargePhotoFileChange }), + (form.enlargePhotos || []).length < 1 ? cameraIconBox(addEnlargePhoto) : null, + (form.enlargePhotos || []).map(function (url, i) { + return React.createElement('span', { key: i, style: { position: 'relative', display: 'inline-block' } }, + React.createElement('img', { + src: url, + alt: '', + style: { width: 64, height: 64, objectFit: 'cover', borderRadius: 4, cursor: 'pointer' }, + onClick: function () { photoPreviewState[1]({ open: true, url: url }); } + }), + React.createElement('span', { + style: { position: 'absolute', top: -6, right: -6, width: 20, height: 20, borderRadius: '50%', background: '#ff4d4f', color: '#fff', fontSize: 12, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }, + onClick: function () { removeEnlargePhoto(); } + }, '×') + ); + }) + ) + ) : null + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '尾板', required: true }, + React.createElement(Switch, { + checked: form.tailboard, + checkedChildren: '有', + unCheckedChildren: '无', + onChange: function (checked) { + setForm(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.tailboard = checked; return n; }); + } + }) + ), + React.createElement(FormItem, { label: '备胎照片', required: true }, + React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 } }, + React.createElement('input', { type: 'file', ref: spareTirePhotoInputRef, accept: 'image/*', style: { display: 'none' }, onChange: onSpareTirePhotoFileChange }), + (form.spareTirePhotos || []).length < 1 ? cameraIconBox(addSpareTirePhoto) : null, + (form.spareTirePhotos || []).map(function (url, i) { + return React.createElement('span', { key: i, style: { position: 'relative', display: 'inline-block' } }, + React.createElement('img', { + src: url, + alt: '', + style: { width: 64, height: 64, objectFit: 'cover', borderRadius: 4, cursor: 'pointer' }, + onClick: function () { photoPreviewState[1]({ open: true, url: url }); } + }), + React.createElement('span', { + style: { position: 'absolute', top: -6, right: -6, width: 20, height: 20, borderRadius: '50%', background: '#ff4d4f', color: '#fff', fontSize: 12, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }, + onClick: function () { removeSpareTirePhoto(); } + }, '×') + ); + }) + ) + ), + React.createElement(FormItem, { label: '备胎胎纹深度', required: true }, + React.createElement(Input, { + value: form.spareTireTreadDepth, + onChange: function (e) { setForm(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.spareTireTreadDepth = e.target.value; return n; }); }, + placeholder: 'OCR识别结果可修改', + style: { width: '100%' }, + addonAfter: 'mm' + }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '瑕疵', fullWidth: true }, + React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 } }, + React.createElement('input', { type: 'file', ref: flawPhotoInputRef, accept: 'image/*', multiple: true, style: { display: 'none' }, onChange: onFlawPhotoFileChange }), + (form.flawPhotos || []).length < 4 ? cameraIconBox(addFlawPhoto) : null, + (form.flawPhotos || []).map(function (url, i) { + return React.createElement('span', { key: i, style: { position: 'relative', display: 'inline-block' } }, + React.createElement('img', { + src: url, + alt: '', + style: { width: 64, height: 64, objectFit: 'cover', borderRadius: 4, cursor: 'pointer' }, + onClick: function () { photoPreviewState[1]({ open: true, url: url }); } + }), + React.createElement('span', { + style: { position: 'absolute', top: -6, right: -6, width: 20, height: 20, borderRadius: '50%', background: '#ff4d4f', color: '#fff', fontSize: 12, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }, + onClick: function () { removeFlawPhoto(i); } + }, '×') + ); + }) + ) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '车辆检查', fullWidth: true, required: true }, + React.createElement(Button, { type: 'default', onClick: function () { drawerOpenState[1](true); } }, '备车检查单') + ) + ) + ) + ), + React.createElement('div', { style: styles.footer }, + React.createElement(Button, { type: 'primary', onClick: handleSubmit }, '提交'), + React.createElement(Button, { onClick: handleSave }, '保存'), + React.createElement(Button, { onClick: handleCancel }, '取消') + ), + React.createElement(Drawer, { + title: '备车检查单', + placement: 'right', + width: 640, + open: drawerOpenState[0], + onClose: function () { drawerOpenState[1](false); }, + styles: { body: { display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden', paddingBottom: 0 } }, + footer: React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8 } }, + React.createElement(Button, { onClick: function () { drawerOpenState[1](false); } }, '返回'), + React.createElement(Button, { type: 'primary', onClick: function () { drawerOpenState[1](false); message.success('检查单已保存'); } }, '提交') + ) + }, + React.createElement('div', { style: { flex: 1, minHeight: 0, overflow: 'auto' } }, + React.createElement(Table, { + columns: inspectionColumns, + dataSource: form.inspectionList || [], + rowKey: function (_, i) { return i; }, + pagination: false, + size: 'small' + }) + ) + ), + React.createElement(Modal, { + title: '需求说明', + open: reqDocOpenState[0], + onCancel: function () { reqDocOpenState[1](false); }, + footer: null, + width: 720, + styles: { body: { maxHeight: '70vh', overflow: 'auto', whiteSpace: 'pre-wrap', fontSize: 14, lineHeight: 1.8 } } + }, requirementDocContent), + spareTireOcrModalState[0].open + ? React.createElement(Modal, { + title: '备胎照片识别', + open: true, + onCancel: spareTireOcrModalState[0].phase === 'result' ? closeSpareTireOcrWithoutSave : undefined, + width: 560, + maskClosable: spareTireOcrModalState[0].phase === 'result', + footer: spareTireOcrModalState[0].phase === 'result' + ? React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8 } }, + React.createElement(Button, { onClick: closeSpareTireOcrWithoutSave }, '取消'), + React.createElement(Button, { type: 'primary', onClick: confirmSpareTireOcr }, '确认') + ) + : null, + closable: spareTireOcrModalState[0].phase === 'result' + }, spareTireOcrModalState[0].phase === 'recognizing' + ? React.createElement('div', { style: { padding: '24px 0', textAlign: 'center', fontSize: 15, color: '#666' } }, '正在识别中,请勿关闭页面') + : React.createElement('div', { style: { display: 'flex', gap: 24, alignItems: 'flex-start' } }, + React.createElement('div', { style: { flex: '0 0 200px' } }, + React.createElement('img', { src: spareTireOcrModalState[0].tempUrl, alt: '', style: { width: '100%', display: 'block', borderRadius: 4 } }) + ), + React.createElement('div', { style: { flex: 1 } }, + React.createElement('div', { style: { marginBottom: 8, color: '#333' } }, '识别胎纹深度(mm)'), + React.createElement(Input, { + value: spareTireOcrModalState[0].tempDepth, + onChange: function (e) { setSpareTireOcrDepth(e.target.value); }, + placeholder: '请输入', + addonAfter: 'mm', + style: { width: '100%', maxWidth: 160 } + }) + ) + ) + ) + : null, + photoPreviewState[0].open && photoPreviewState[0].url + ? React.createElement(Modal, { + title: '预览', + open: true, + footer: null, + onCancel: function () { photoPreviewState[1]({ open: false, url: null }); } + }, React.createElement('img', { src: photoPreviewState[0].url, alt: '', style: { width: '100%' } })) + : null + ); +}; + +if (typeof window !== 'undefined') { + window.Component = Component; + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function () { + var rootEl = document.getElementById('root'); + if (rootEl && window.ReactDOM && window.React) { + var root = ReactDOM.createRoot(rootEl); + root.render(React.createElement(Component)); + } + }); + } else { + var rootEl = document.getElementById('root'); + if (rootEl && window.ReactDOM && window.React) { + var root = ReactDOM.createRoot(rootEl); + root.render(React.createElement(Component)); + } + } +} + +export default Component; diff --git a/src/prototypes/oneos-web-ops/pages/06-备车管理.jsx b/src/prototypes/oneos-web-ops/pages/06-备车管理.jsx new file mode 100644 index 0000000..6d62661 --- /dev/null +++ b/src/prototypes/oneos-web-ops/pages/06-备车管理.jsx @@ -0,0 +1,729 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 备车管理 - 车辆资产管理后台(按 antd 规范) + +const Component = function () { + var useState = React.useState; + var useCallback = React.useCallback; + var useMemo = React.useMemo; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var DatePicker = antd.DatePicker; + var Select = antd.Select; + var Button = antd.Button; + var Tabs = antd.Tabs; + var Table = antd.Table; + var Modal = antd.Modal; + var Steps = antd.Steps; + var Input = antd.Input; + var Cascader = antd.Cascader; + var Radio = antd.Radio; + var Space = antd.Space; + var Row = antd.Row; + var Col = antd.Col; + var message = antd.message; + + var RangePicker = DatePicker.RangePicker; + + // 当前视图:'list' | 'add' + var viewState = useState('list'); + var currentView = viewState[0]; + var setCurrentView = viewState[1]; + + var filterState = useState({ + dateStart: '', + dateEnd: '', + operator: undefined, + plateNo: undefined, + vehicleType: undefined, + vehicleModelCascader: undefined, + parkingLot: [] + }); + var filters = filterState[0]; + var setFilters = filterState[1]; + // 实际参与列表筛选的条件(点击查询后与 filters 同步) + var appliedFilterState = useState({ + dateStart: '', + dateEnd: '', + operator: undefined, + plateNo: undefined, + vehicleType: undefined, + vehicleModelCascader: undefined, + parkingLot: [] + }); + var appliedFilters = appliedFilterState[0]; + var setAppliedFilters = appliedFilterState[1]; + + var activeTabState = useState('completed'); + var activeTab = activeTabState[0]; + var setActiveTab = activeTabState[1]; + + var defaultInspection = [{ checkType: '灯光', checkItem: '前大灯', result: '正常', treadDepth: '', remark: '' }, { checkType: '灯光', checkItem: '尾灯', result: '正常', treadDepth: '', remark: '' }, { checkType: '轮胎', checkItem: '前左胎', result: '正常', treadDepth: '5.2mm', remark: '' }, { checkType: '车身', checkItem: '车身外观', result: '正常', treadDepth: '', remark: '' }]; + var completedListState = useState([ + { id: '1', completeDate: '2025-02-10 09:30', operator: '张三', vehicleType: '重型厢式货车', plateNo: '京A12345', brand: '东风', model: 'DFH1180', vin: 'LGHXCAE28M1234567', parkingLot: '朝阳停车场', bodyAd: '有', adPhotos: ['https://picsum.photos/200/150?random=1', 'https://picsum.photos/200/150?random=2', 'https://picsum.photos/200/150?random=3'], tailboard: '有', spareTireTreadDepth: '5.2mm', spareTirePhotos: ['https://picsum.photos/200/150?random=sp1'], flawPhotos: ['https://picsum.photos/200/150?random=4', 'https://picsum.photos/200/150?random=5'], inspectionList: defaultInspection }, + { id: '2', completeDate: '2025-02-09 10:15', operator: '李四', vehicleType: '重型平板半挂车', plateNo: '京C11111', brand: '福田', model: 'BJ1180', vin: 'LGHXCAE28M7654321', parkingLot: '海淀停车场', bodyAd: '无', adPhotos: [], tailboard: '无', spareTireTreadDepth: '3.1mm', spareTirePhotos: [], flawPhotos: ['https://picsum.photos/200/150?random=6'], inspectionList: defaultInspection }, + { id: '3', completeDate: '2025-02-08 11:00', operator: '王五', vehicleType: '轻型厢式货车', plateNo: '京D22222', brand: '江淮', model: 'HFC1180', vin: 'LGHXCAE28M8888888', parkingLot: '丰台停车场', bodyAd: '有', adPhotos: ['https://picsum.photos/200/150?random=7', 'https://picsum.photos/200/150?random=8'], tailboard: '有', spareTireTreadDepth: '4.0mm', spareTirePhotos: ['https://picsum.photos/200/150?random=sp2'], flawPhotos: [], inspectionList: defaultInspection }, + { id: '4', completeDate: '2025-02-07 13:45', operator: '赵六', vehicleType: '重型半挂牵引车', plateNo: '京E33333', brand: '重汽', model: 'ZZ1180', vin: 'LGHXCAE28M7777777', parkingLot: '西城停车场', bodyAd: '无', adPhotos: [], tailboard: '无', spareTireTreadDepth: '5.0mm', spareTirePhotos: ['https://picsum.photos/200/150?random=sp3'], flawPhotos: ['https://picsum.photos/200/150?random=9'], inspectionList: defaultInspection }, + { id: '5', completeDate: '2025-02-06 08:20', operator: '张三', vehicleType: '重型厢式货车', plateNo: '京A23456', brand: '东风', model: 'DFH1190', vin: 'LGHXCAE28M6666666', parkingLot: '朝阳停车场', bodyAd: '有', adPhotos: ['https://picsum.photos/200/150?random=10'], tailboard: '有', spareTireTreadDepth: '4.5mm', spareTirePhotos: [], flawPhotos: [], inspectionList: defaultInspection }, + { id: '6', completeDate: '2025-02-05 14:30', operator: '李四', vehicleType: '轻型厢式货车', plateNo: '京B34567', brand: '福田', model: 'BJ1190', vin: 'LGHXCAE28M5555555', parkingLot: '海淀停车场', bodyAd: '无', adPhotos: [], tailboard: '无', spareTireTreadDepth: '3.8mm', spareTirePhotos: ['https://picsum.photos/200/150?random=sp4'], flawPhotos: ['https://picsum.photos/200/150?random=11', 'https://picsum.photos/200/150?random=12'], inspectionList: defaultInspection }, + { id: '7', completeDate: '2025-02-04 16:00', operator: '王五', vehicleType: '重型集装箱半挂车', plateNo: '京C45678', brand: '江淮', model: 'HFC1190', vin: 'LGHXCAE28M4444444', parkingLot: '丰台停车场', bodyAd: '有', adPhotos: ['https://picsum.photos/200/150?random=13', 'https://picsum.photos/200/150?random=14'], tailboard: '有', spareTireTreadDepth: '5.2mm', spareTirePhotos: ['https://picsum.photos/200/150?random=sp5'], flawPhotos: [], inspectionList: defaultInspection }, + { id: '8', completeDate: '2025-02-03 09:00', operator: '赵六', vehicleType: '重型厢式货车', plateNo: '京D56789', brand: '重汽', model: 'ZZ1190', vin: 'LGHXCAE28M3333333', parkingLot: '西城停车场', bodyAd: '无', adPhotos: [], tailboard: '有', spareTireTreadDepth: '4.2mm', spareTirePhotos: [], flawPhotos: ['https://picsum.photos/200/150?random=15'], inspectionList: defaultInspection }, + { id: '9', completeDate: '2025-02-02 10:30', operator: '张三', vehicleType: '小型普通客车', plateNo: '京E67890', brand: '东风', model: 'DFH1200', vin: 'LGHXCAE28M2222222', parkingLot: '朝阳停车场', bodyAd: '有', adPhotos: ['https://picsum.photos/200/150?random=16', 'https://picsum.photos/200/150?random=17', 'https://picsum.photos/200/150?random=18'], tailboard: '无', spareTireTreadDepth: '5.0mm', spareTirePhotos: ['https://picsum.photos/200/150?random=sp6'], flawPhotos: [], inspectionList: defaultInspection }, + { id: '10', completeDate: '2025-02-01 15:45', operator: '李四', vehicleType: '轻型厢式货车', plateNo: '京A78901', brand: '福田', model: 'BJ1200', vin: 'LGHXCAE28M1111111', parkingLot: '海淀停车场', bodyAd: '无', adPhotos: [], tailboard: '有', spareTireTreadDepth: '3.5mm', spareTirePhotos: [], flawPhotos: [], inspectionList: defaultInspection } + ]); + var completedList = completedListState[0]; + var setCompletedList = completedListState[1]; + + var pendingListState = useState([ + { id: 'p1', operateDate: '2025-02-10 14:30', operator: '王五', vehicleType: '轻型厢式货车', plateNo: '京F10001', brand: '江淮', model: 'HFC1180', vin: 'LGHXCAE28M9990001', parkingLot: '丰台停车场', bodyAd: '有', adPhotos: ['https://picsum.photos/200/150?random=19', 'https://picsum.photos/200/150?random=20'], tailboard: '有', spareTireTreadDepth: '4.0mm', spareTirePhotos: ['https://picsum.photos/200/150?random=sp7'], flawPhotos: [] }, + { id: 'p2', operateDate: '2025-02-09 09:15', operator: '赵六', vehicleType: '重型平板半挂车', plateNo: '京F10002', brand: '重汽', model: 'ZZ1180', vin: 'LGHXCAE28M9990002', parkingLot: '西城停车场', bodyAd: '无', adPhotos: [], tailboard: '无', spareTireTreadDepth: '3.1mm', spareTirePhotos: [], flawPhotos: ['https://picsum.photos/200/150?random=21'] }, + { id: 'p3', operateDate: '2025-02-08 11:45', operator: '张三', vehicleType: '重型半挂牵引车', plateNo: '京F10003', brand: '东风', model: 'DFH1180', vin: 'LGHXCAE28M9990003', parkingLot: '朝阳停车场', bodyAd: '有', adPhotos: ['https://picsum.photos/200/150?random=22'], tailboard: '有', spareTireTreadDepth: '5.0mm', spareTirePhotos: [], flawPhotos: [] }, + { id: 'p4', operateDate: '2025-02-07 08:00', operator: '李四', vehicleType: '重型厢式货车', plateNo: '京F10005', brand: '福田', model: 'BJ1180', vin: 'LGHXCAE28M9990004', parkingLot: '海淀停车场', bodyAd: '无', adPhotos: [], tailboard: '无', spareTireTreadDepth: '4.2mm', spareTirePhotos: ['https://picsum.photos/200/150?random=sp8'], flawPhotos: [] }, + { id: 'p5', operateDate: '2025-02-06 13:20', operator: '王五', vehicleType: '轻型厢式货车', plateNo: '京F10006', brand: '江淮', model: 'HFC1190', vin: 'LGHXCAE28M9990005', parkingLot: '丰台停车场', bodyAd: '有', adPhotos: ['https://picsum.photos/200/150?random=23', 'https://picsum.photos/200/150?random=24', 'https://picsum.photos/200/150?random=25'], tailboard: '有', spareTireTreadDepth: '3.8mm', spareTirePhotos: [], flawPhotos: ['https://picsum.photos/200/150?random=26', 'https://picsum.photos/200/150?random=27'] }, + { id: 'p6', operateDate: '2025-02-05 15:30', operator: '赵六', vehicleType: '重型集装箱半挂车', plateNo: '京F10007', brand: '重汽', model: 'ZZ1190', vin: 'LGHXCAE28M9990006', parkingLot: '西城停车场', bodyAd: '无', adPhotos: [], tailboard: '无', spareTireTreadDepth: '5.2mm', spareTirePhotos: ['https://picsum.photos/200/150?random=sp9'], flawPhotos: [] }, + { id: 'p7', operateDate: '2025-02-04 10:00', operator: '张三', vehicleType: '重型厢式货车', plateNo: '京F10009', brand: '东风', model: 'DFH1190', vin: 'LGHXCAE28M9990007', parkingLot: '朝阳停车场', bodyAd: '有', adPhotos: ['https://picsum.photos/200/150?random=28', 'https://picsum.photos/200/150?random=29'], tailboard: '有', spareTireTreadDepth: '4.5mm', spareTirePhotos: [], flawPhotos: [] }, + { id: 'p8', operateDate: '2025-02-03 16:45', operator: '李四', vehicleType: '小型普通客车', plateNo: '京F10010', brand: '福田', model: 'BJ1190', vin: 'LGHXCAE28M9990008', parkingLot: '海淀停车场', bodyAd: '无', adPhotos: [], tailboard: '无', spareTireTreadDepth: '3.5mm', spareTirePhotos: [], flawPhotos: ['https://picsum.photos/200/150?random=30'] }, + { id: 'p9', operateDate: '2025-02-02 09:30', operator: '王五', vehicleType: '轻型厢式货车', plateNo: '京F10011', brand: '江淮', model: 'HFC1200', vin: 'LGHXCAE28M9990009', parkingLot: '丰台停车场', bodyAd: '有', adPhotos: ['https://picsum.photos/200/150?random=31'], tailboard: '有', spareTireTreadDepth: '5.0mm', spareTirePhotos: ['https://picsum.photos/200/150?random=sp10'], flawPhotos: [] }, + { id: 'p10', operateDate: '2025-02-01 14:00', operator: '赵六', vehicleType: '重型平板半挂车', plateNo: '京F10013', brand: '重汽', model: 'ZZ1200', vin: 'LGHXCAE28M9990010', parkingLot: '西城停车场', bodyAd: '无', adPhotos: [], tailboard: '无', spareTireTreadDepth: '4.0mm', spareTirePhotos: [], flawPhotos: [] } + ]); + var pendingList = pendingListState[0]; + var setPendingList = pendingListState[1]; + + var pageState = useState(1); + var page = pageState[0]; + var setPage = pageState[1]; + var pageSizeState = useState(10); + var pageSize = pageSizeState[0]; + var setPageSize = pageSizeState[1]; + + var operatorOptions = useMemo(function () { + var set = new Set(); + completedList.forEach(function (r) { if (r.operator) set.add(r.operator); }); + pendingList.forEach(function (r) { if (r.operator) set.add(r.operator); }); + return Array.from(set).map(function (v) { return { value: v, label: v }; }); + }, [completedList, pendingList]); + var plateOptions = useMemo(function () { + var set = new Set(); + completedList.forEach(function (r) { if (r.plateNo) set.add(r.plateNo); }); + pendingList.forEach(function (r) { if (r.plateNo) set.add(r.plateNo); }); + return Array.from(set).map(function (v) { return { value: v, label: v }; }); + }, [completedList, pendingList]); + var vehicleTypeOptions = useMemo(function () { + return [ + { value: '轻型厢式货车', label: '轻型厢式货车' }, + { value: '重型厢式货车', label: '重型厢式货车' }, + { value: '重型半挂牵引车', label: '重型半挂牵引车' }, + { value: '重型集装箱半挂车', label: '重型集装箱半挂车' }, + { value: '小型普通客车', label: '小型普通客车' }, + { value: '重型平板半挂车', label: '重型平板半挂车' }, + { value: '叉车', label: '叉车' }, + { value: '油车', label: '油车' }, + { value: '观光车', label: '观光车' } + ]; + }, []); + var parkingOptions = useMemo(function () { return [{ value: '朝阳停车场', label: '朝阳停车场' }, { value: '海淀停车场', label: '海淀停车场' }, { value: '丰台停车场', label: '丰台停车场' }, { value: '西城停车场', label: '西城停车场' }]; }, []); + var vehicleModelCascaderOptions = useMemo(function () { + var map = {}; + completedList.forEach(function (r) { + if (!r.brand) return; + if (!map[r.brand]) map[r.brand] = { value: r.brand, label: r.brand, children: [] }; + var children = map[r.brand].children; + var model = r.model || ''; + if (model && children.every(function (c) { return c.value !== model; })) children.push({ value: model, label: model }); + }); + pendingList.forEach(function (r) { + if (!r.brand) return; + if (!map[r.brand]) map[r.brand] = { value: r.brand, label: r.brand, children: [] }; + var children = map[r.brand].children; + var model = r.model || ''; + if (model && children.every(function (c) { return c.value !== model; })) children.push({ value: model, label: model }); + }); + return Object.keys(map).map(function (k) { return map[k]; }); + }, [completedList, pendingList]); + + var reqDocOpenState = useState(false); + var photoModalState = useState({ open: false, photos: [], currentIndex: 0 }); + + var addStepState = useState(1); + var addFormState = useState({ + plateNo: '', + vehicleType: '', + brand: '', + model: '', + vin: '', + bodyAd: '无', + adPhotos: [], + tailboard: '无', + trailerPlate: '', + spareTireTreadDepth: '', + spareTirePhotos: [], + flawPhotos: [], + inspectionList: [ + { checkType: '灯光', checkItem: '前大灯', result: '正常', treadDepth: '', remark: '' }, + { checkType: '灯光', checkItem: '尾灯', result: '正常', treadDepth: '', remark: '' }, + { checkType: '轮胎', checkItem: '前左胎', result: '正常', treadDepth: '', remark: '' }, + { checkType: '车身', checkItem: '车身外观', result: '正常', treadDepth: '', remark: '' } + ] + }); + + var filteredCompleted = useMemo(function () { + var list = completedList.slice(); + if (appliedFilters.operator) list = list.filter(function (r) { return (r.operator || '').indexOf(appliedFilters.operator) !== -1; }); + if (appliedFilters.plateNo) list = list.filter(function (r) { return (r.plateNo || '').indexOf(appliedFilters.plateNo) !== -1; }); + if (appliedFilters.vehicleType) list = list.filter(function (r) { return r.vehicleType === appliedFilters.vehicleType; }); + if (appliedFilters.vehicleModelCascader && appliedFilters.vehicleModelCascader.length >= 1) { + var brand = appliedFilters.vehicleModelCascader[0]; + var model = appliedFilters.vehicleModelCascader[1]; + list = list.filter(function (r) { + if (r.brand !== brand) return false; + if (model && r.model !== model) return false; + return true; + }); + } + if (appliedFilters.parkingLot && appliedFilters.parkingLot.length > 0) list = list.filter(function (r) { return appliedFilters.parkingLot.indexOf(r.parkingLot) !== -1; }); + if (appliedFilters.dateStart) list = list.filter(function (r) { var d = (r.completeDate || '').slice(0, 10); return d >= appliedFilters.dateStart; }); + if (appliedFilters.dateEnd) list = list.filter(function (r) { var d = (r.completeDate || '').slice(0, 10); return d <= appliedFilters.dateEnd; }); + return list.sort(function (a, b) { return (b.completeDate || '').localeCompare(a.completeDate || ''); }); + }, [completedList, appliedFilters]); + + var filteredPending = useMemo(function () { + var list = pendingList.slice(); + if (appliedFilters.operator) list = list.filter(function (r) { return (r.operator || '').indexOf(appliedFilters.operator) !== -1; }); + if (appliedFilters.plateNo) list = list.filter(function (r) { return (r.plateNo || '').indexOf(appliedFilters.plateNo) !== -1; }); + if (appliedFilters.vehicleType) list = list.filter(function (r) { return r.vehicleType === appliedFilters.vehicleType; }); + if (appliedFilters.vehicleModelCascader && appliedFilters.vehicleModelCascader.length >= 1) { + var brand = appliedFilters.vehicleModelCascader[0]; + var model = appliedFilters.vehicleModelCascader[1]; + list = list.filter(function (r) { + if (r.brand !== brand) return false; + if (model && r.model !== model) return false; + return true; + }); + } + if (appliedFilters.parkingLot && appliedFilters.parkingLot.length > 0) list = list.filter(function (r) { return appliedFilters.parkingLot.indexOf(r.parkingLot) !== -1; }); + if (appliedFilters.dateStart) list = list.filter(function (r) { var d = (r.operateDate || '').slice(0, 10); return d >= appliedFilters.dateStart; }); + if (appliedFilters.dateEnd) list = list.filter(function (r) { var d = (r.operateDate || '').slice(0, 10); return d <= appliedFilters.dateEnd; }); + return list.sort(function (a, b) { return (b.operateDate || '').localeCompare(a.operateDate || ''); }); + }, [pendingList, appliedFilters]); + + var displayCompleted = useMemo(function () { + var start = (page - 1) * pageSize; + return filteredCompleted.slice(start, start + pageSize); + }, [filteredCompleted, page, pageSize]); + + var displayPending = useMemo(function () { + var start = (page - 1) * pageSize; + return filteredPending.slice(start, start + pageSize); + }, [filteredPending, page, pageSize]); + + var totalCompleted = filteredCompleted.length; + var totalPending = filteredPending.length; + var totalCount = activeTab === 'completed' ? totalCompleted : totalPending; + + var handleQuery = useCallback(function () { + setAppliedFilters({ dateStart: filters.dateStart, dateEnd: filters.dateEnd, operator: filters.operator, plateNo: filters.plateNo, vehicleType: filters.vehicleType, vehicleModelCascader: filters.vehicleModelCascader, parkingLot: filters.parkingLot || [] }); + setPage(1); + }, [filters.dateStart, filters.dateEnd, filters.operator, filters.plateNo, filters.vehicleType, filters.vehicleModelCascader, filters.parkingLot]); + var handleReset = useCallback(function () { + var empty = { dateStart: '', dateEnd: '', operator: undefined, plateNo: undefined, vehicleType: undefined, vehicleModelCascader: undefined, parkingLot: [] }; + setFilters(empty); + setAppliedFilters(empty); + setPage(1); + }, []); + + var handleExport = useCallback(function () { + if (activeTab !== 'completed') { + message.warning('请在「已完成」Tab 下导出'); + return; + } + var rows = filteredCompleted; + var headers = ['备车时间', '备车人', '车辆类型', '车牌号', '品牌', '型号', '车辆识别代码', '停车场', '车身广告及放大字', '尾板', '备胎胎纹深度']; + var csv = headers.join(',') + '\n'; + rows.forEach(function (r) { + csv += [(r.completeDate || '').slice(0, 10), r.operator, r.vehicleType, r.plateNo, r.brand, r.model, r.vin, r.parkingLot, r.bodyAd, r.tailboard, r.spareTireTreadDepth || ''].join(',') + '\n'; + }); + var blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' }); + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = '备车管理导出_' + new Date().getTime() + '.csv'; + a.click(); + URL.revokeObjectURL(url); + message.success('导出成功'); + }, [activeTab, filteredCompleted]); + + var handlePendingDelete = useCallback(function () { + if (pendingDeleteId) { + setPendingList(function (prev) { return prev.filter(function (r) { return r.id !== pendingDeleteId; }); }); + message.success('已删除'); + } + setPendingDeleteId(null); + }, [pendingDeleteId]); + + var openPhotoModal = useCallback(function (photos, index) { + if (!photos || photos.length === 0) return; + photoModalState[1]({ open: true, photos: photos, currentIndex: index || 0 }); + }, []); + + var addForm = addFormState[0]; + var setAddForm = addFormState[1]; + var addStep = addStepState[0]; + var setAddStep = addStepState[1]; + + var handleAddSave = useCallback(function () { + var d = new Date(); + var pad = function (n) { return String(n).padStart(2, '0'); }; + var dateTimeStr = d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()); + var item = { + id: 'p' + Date.now(), + operateDate: dateTimeStr, + operator: '当前用户', + vehicleType: addForm.vehicleType || '轻型厢式货车', + plateNo: addForm.plateNo, + brand: addForm.brand || '-', + model: addForm.model || '-', + vin: addForm.vin, + parkingLot: addForm.parkingLot || '朝阳停车场', + bodyAd: addForm.bodyAd, + adPhotos: addForm.adPhotos || [], + tailboard: addForm.tailboard, + trailerPlate: addForm.trailerPlate || '', + spareTireTreadDepth: addForm.spareTireTreadDepth || '', + spareTirePhotos: addForm.spareTirePhotos || [], + flawPhotos: addForm.flawPhotos || [] + }; + setPendingList(pendingList.concat([item])); + setCurrentView('list'); + setActiveTab('pending'); + setAddForm({ plateNo: '', vehicleType: '', brand: '', model: '', vin: '', bodyAd: '无', adPhotos: [], tailboard: '无', trailerPlate: '', spareTireTreadDepth: '', spareTirePhotos: [], flawPhotos: [], inspectionList: addForm.inspectionList }); + setAddStep(1); + message.success('已保存至待提交列表'); + }, [addForm, pendingList]); + + var handleAddNext = useCallback(function () { + if (!addForm.plateNo) { + message.warning('请选择车牌号'); + return; + } + setAddStep(2); + }, [addForm.plateNo]); + + var handleAddSubmit = useCallback(function () { + var d = new Date(); + var pad = function (n) { return String(n).padStart(2, '0'); }; + var dateTimeStr = d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()); + var item = { + id: String(Date.now()), + completeDate: dateTimeStr, + operator: '当前用户', + vehicleType: addForm.vehicleType || '轻型厢式货车', + plateNo: addForm.plateNo, + brand: addForm.brand || '-', + model: addForm.model || '-', + vin: addForm.vin, + parkingLot: addForm.parkingLot || '朝阳停车场', + bodyAd: addForm.bodyAd, + adPhotos: addForm.adPhotos || [], + tailboard: addForm.tailboard, + spareTireTreadDepth: addForm.spareTireTreadDepth || '', + spareTirePhotos: addForm.spareTirePhotos || [], + flawPhotos: addForm.flawPhotos || [], + inspectionList: addForm.inspectionList || [] + }; + setCompletedList(completedList.concat([item])); + setCurrentView('list'); + setActiveTab('completed'); + setAddForm({ plateNo: '', vehicleType: '', brand: '', model: '', vin: '', bodyAd: '无', adPhotos: [], tailboard: '无', trailerPlate: '', spareTireTreadDepth: '', spareTirePhotos: [], flawPhotos: [], inspectionList: addForm.inspectionList }); + setAddStep(1); + message.success('提交成功'); + }, [addForm, completedList]); + + var onPlateSelect = useCallback(function (plateNo) { + setAddForm(function (prev) { + var next = {}; + for (var k in prev) next[k] = prev[k]; + next.plateNo = plateNo; + next.vehicleType = '厢式货车'; + next.brand = '东风'; + next.vin = 'LGHXCAE28M' + Math.floor(Math.random() * 10000000); + return next; + }); + }, []); + + var requirementDocContent = '备车管理:\n一个「数字化资产ONEOS运管平台」中的「备车管理」模块\n1.面包屑:\n1.1.运维管理-车辆业务-备车管理\n\n2.筛选:\n2.1.备车日期:日期选择器,精确至天,支持单输入框双日历选择开始日期-结束日期;\n2.2.备车人:选择器,支持输入框内输入进行模糊搜索下拉选项匹配,提示信息为:请选择或输入备车人姓名;\n2.3.车牌号:选择器,支持输入框内输入进行模糊搜索下拉选项匹配,提示信息为:请选择或输入车牌号;\n2.4.车辆类型:选择器,选项包含:轻型厢式货车、重型厢式货车、重型半挂牵引车、轻型厢式货车、重型厢式货车、重型集装箱半挂车、小型普通客车、重型半挂牵引车、重型平板半挂车、叉车、油车、观光车;\n2.5.车辆型号:级联选择器,选择品牌及型号;\n2.5.停车场:选择器,支持多选,选择所有停车场;\n\n3.列表:分为已完成、待提交2个tab,已完成tab下显示所有已完成备车的车辆,待提交tab下显示所有仅保存待提交的车辆;\n3.1.已完成,列表右侧为新增、导出:\n 3.1.1.备车时间:显示备车完成时间,格式为:YYYY-MM-DD;\n 3.1.2.备车人:显示备车用户姓名;\n 3.1.3.车辆类型:显示备车车辆类型;\n 3.1.4.品牌:显示备车车辆品牌;\n 3.1.5.型号:显示备车车辆型号;\n 3.1.6.车牌号:显示备车车辆车牌号;\n 3.1.7.车辆识别代码:显示备车车辆识别代码;\n 3.1.8.停车场:显示备车车辆所在停车场;\n 3.1.9.车身广告及放大字:显示备车车辆是否有车身广告及放大字,显示为有或无;\n 3.1.10.尾板:如尾板为有,显示备车车辆是否有尾板,显示为有或无;\n 3.1.11.备胎胎纹深度:显示备车车辆备胎胎纹深度,格式为:xxmm;\n 3.1.12.操作:查看;\n 3.1.12.1.查看:点击查看,进入备车管理-待提交-查看页;\n\n3.2.待提交,列表右侧为新增:\n 3.2.1.创建时间:显示记录创建时间,格式为:YYYY-MM-DD;\n 3.2.2.创建人:显示创建用户姓名;\n 3.2.3.车辆类型:显示备车车辆类型;\n 3.2.4.品牌:显示备车车辆品牌;\n 3.2.5.型号:显示备车车辆型号;\n 3.2.6.车牌号:显示备车车辆车牌号;\n 3.2.7.车辆识别代码:显示备车车辆识别代码;\n 3.2.8.停车场:显示备车车辆所在停车场;\n 3.2.9.车身广告及放大字:显示备车车辆是否有车身广告及放大字,显示为有或无;\n 3.2.10.广告照片:如车身广告及放大字为有,则显示查看照片,如车身广告及放大字为无,则显示-;\n 3.2.11.放大字照片:如车身广告及放大字为有,则显示查看照片,如车身广告及放大字为无,则显示-;\n 3.2.12.尾板:如尾板为有,显示备车车辆是否有尾板,显示为有或无;\n 3.2.13.备胎胎纹深度:显示备车车辆备胎胎纹深度,格式为:xxmm;\n 3.2.14.备胎照片:如备胎照片为有,则显示查看照片,如备胎为无,则显示-;\n 3.2.15.瑕疵照片:如瑕疵照片为有,则显示查看照片,如瑕疵照片为无,则显示-;\n 3.2.16.操作:查看、编辑、删除;\n 3.2.16.1.查看:点击查看,进入备车管理-待提交-查看页;\n 3.2.16.2.编辑:点击编辑,进入备车管理-待提交-编辑页;\n 3.2.16.3.删除:点击进行二次确认,确认后删除;\n\n已完成、待提交均支持分页功能,可设置单页查询数据条数;\n\n备车权限说明:\n1.运维人员分区域,对应区域运维人员只能新增自己区域停车场的车辆进行备车;\n2.运维人员只能查看自己区域停车场的备车记录;'; + + // 日期范围变化(antd RangePicker 返回 dayjs 或 [string, string] 依项目而定,此处用字符串存) + function onDateRangeChange(dates, dateStrings) { + setFilters(function (f) { + var g = {}; + for (var k in f) g[k] = f[k]; + g.dateStart = (dateStrings && dateStrings[0]) || ''; + g.dateEnd = (dateStrings && dateStrings[1]) || ''; + return g; + }); + } + + var dateRangeValue = useMemo(function () { + if (!filters.dateStart && !filters.dateEnd) return null; + try { + if (typeof window.dayjs === 'function' && filters.dateStart && filters.dateEnd) { + return [window.dayjs(filters.dateStart), window.dayjs(filters.dateEnd)]; + } + } catch (e) {} + return null; + }, [filters.dateStart, filters.dateEnd]); + + // 已完成表格列 + function getCompletedColumns() { + return [ + { title: '备车时间', dataIndex: 'completeDate', key: 'completeDate', width: 120, render: function (v) { return v ? String(v).slice(0, 10) : '—'; } }, + { title: '备车人', dataIndex: 'operator', key: 'operator', width: 90, render: function (v) { return v || '—'; } }, + { title: '车辆类型', dataIndex: 'vehicleType', key: 'vehicleType', width: 120, render: function (v) { return v || '—'; } }, + { title: '品牌', dataIndex: 'brand', key: 'brand', width: 80, render: function (v) { return v || '—'; } }, + { title: '型号', dataIndex: 'model', key: 'model', width: 100, render: function (v) { return v || '—'; } }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 100, render: function (v) { return v || '—'; } }, + { title: '车辆识别代码', dataIndex: 'vin', key: 'vin', width: 180, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '停车场', dataIndex: 'parkingLot', key: 'parkingLot', width: 110, render: function (v) { return v || '—'; } }, + { title: '车身广告及放大字', dataIndex: 'bodyAd', key: 'bodyAd', width: 120, render: function (v) { return v === '有' || v === '无' ? v : '—'; } }, + { title: '尾板', dataIndex: 'tailboard', key: 'tailboard', width: 70, render: function (v) { return v === '有' || v === '无' ? v : '—'; } }, + { title: '备胎胎纹深度', key: 'spareTireTreadDepth', width: 110, render: function (_, r) { var d = r.spareTireTreadDepth; return d ? (String(d).replace(/mm$/, '') + 'mm') : '—'; } }, + { title: '操作', key: 'action', width: 80, fixed: 'right', render: function (_, r) { + return React.createElement(Button, { type: 'link', size: 'small', onClick: function () {} }, '查看'); + }} + ]; + } + + var pendingDeleteIdState = useState(null); + var pendingDeleteId = pendingDeleteIdState[0]; + var setPendingDeleteId = pendingDeleteIdState[1]; + + function getPendingColumns() { + return [ + { title: '创建时间', dataIndex: 'operateDate', key: 'operateDate', width: 120, render: function (v) { return v ? String(v).slice(0, 10) : '—'; } }, + { title: '创建人', dataIndex: 'operator', key: 'operator', width: 90, render: function (v) { return v || '—'; } }, + { title: '车辆类型', dataIndex: 'vehicleType', key: 'vehicleType', width: 120, render: function (v) { return v || '—'; } }, + { title: '品牌', dataIndex: 'brand', key: 'brand', width: 80, render: function (v) { return v || '—'; } }, + { title: '型号', dataIndex: 'model', key: 'model', width: 100, render: function (v) { return v || '—'; } }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 100, render: function (v) { return v || '—'; } }, + { title: '车辆识别代码', dataIndex: 'vin', key: 'vin', width: 180, ellipsis: true, render: function (v) { return v || '—'; } }, + { title: '停车场', dataIndex: 'parkingLot', key: 'parkingLot', width: 110, render: function (v) { return v || '—'; } }, + { title: '车身广告及放大字', dataIndex: 'bodyAd', key: 'bodyAd', width: 120, render: function (v) { return v === '有' || v === '无' ? v : '—'; } }, + { title: '尾板', dataIndex: 'tailboard', key: 'tailboard', width: 70, render: function (v) { return v === '有' || v === '无' ? v : '—'; } }, + { title: '备胎胎纹深度', key: 'spareTireTreadDepth', width: 110, render: function (_, r) { var d = r.spareTireTreadDepth; return d ? (String(d).replace(/mm$/, '') + 'mm') : '—'; } }, + { title: '操作', key: 'action', width: 160, fixed: 'right', render: function (_, r) { + return React.createElement(Space, { size: 'small' }, + React.createElement(Button, { type: 'link', size: 'small', onClick: function () {} }, '查看'), + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { + setAddForm(function (prev) { + var n = {}; + for (var k in prev) n[k] = prev[k]; + n.plateNo = r.plateNo; n.vehicleType = r.vehicleType; n.brand = r.brand; n.model = r.model; n.vin = r.vin; + n.bodyAd = r.bodyAd; n.adPhotos = r.adPhotos || []; n.tailboard = r.tailboard; + n.trailerPlate = r.trailerPlate || ''; n.flawPhotos = r.flawPhotos || []; + return n; + }); + setAddStep(1); + setCurrentView('add'); + } }, '编辑'), + React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function () { setPendingDeleteId(r.id); } }, '删除') + ); + }} + ]; + } + + var tablePagination = useMemo(function () { + return { + current: page, + pageSize: pageSize, + total: totalCount, + showSizeChanger: true, + showTotal: function (t) { return '共 ' + t + ' 条'; }, + pageSizeOptions: ['10', '20', '50'], + onChange: function (p, size) { + setPage(p); + if (size !== pageSize) setPageSize(size); + } + }; + }, [page, pageSize, totalCount]); + + // —————— 列表页 —————— + function ListView() { + var breadcrumbItems = [ + { title: '运维管理' }, + { title: '车辆业务' }, + { title: '备车管理' } + ]; + return React.createElement('div', { style: { padding: 24, background: '#f5f5f5', minHeight: '100vh' } }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + React.createElement(Breadcrumb, { items: breadcrumbItems }), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { reqDocOpenState[1](true); } }, '查看需求说明') + ), + React.createElement(Modal, { + title: '需求说明', + open: reqDocOpenState[0], + onCancel: function () { reqDocOpenState[1](false); }, + footer: null, + width: 720, + styles: { body: { maxHeight: '70vh', overflow: 'auto', whiteSpace: 'pre-wrap', fontSize: 14 } } + }, requirementDocContent.split('\n').map(function (line, idx) { + var trimmed = line; + if (trimmed.indexOf('#') === 0) { + return React.createElement('div', { key: idx, style: { fontWeight: 'bold', marginTop: idx === 0 ? 0 : 16, marginBottom: 8, paddingBottom: 4, borderBottom: '1px solid #f0f0f0' } }, trimmed.replace(/^#\.?/, '').trim()); + } + if (/^\d+\./.test(trimmed)) return React.createElement('div', { key: idx, style: { marginLeft: 12, marginBottom: 4 } }, trimmed); + if (trimmed === '') return React.createElement('div', { key: idx, style: { height: 12 } }); + return React.createElement('div', { key: idx, style: { marginBottom: 8 } }, trimmed); + })), + React.createElement(Card, { style: { marginBottom: 16 } }, + React.createElement('div', { + style: { + display: 'grid', + gridTemplateColumns: '1fr 1fr 1fr', + gap: '16px 24px', + alignItems: 'start', + flex: 1, + minWidth: 0 + } + }, + React.createElement('div', null, + React.createElement('div', { style: { marginBottom: 4 } }, '备车日期'), + React.createElement(RangePicker, { + value: dateRangeValue, + onChange: onDateRangeChange, + style: { width: '100%' }, + placeholder: ['开始日期', '结束日期'] + }) + ), + React.createElement('div', null, + React.createElement('div', { style: { marginBottom: 4 } }, '备车人'), + React.createElement(Select, { + placeholder: '请选择或输入备车人姓名', + allowClear: true, + showSearch: true, + filterOption: function (input, opt) { return (opt && opt.label && String(opt.label).toLowerCase().indexOf((input || '').toLowerCase()) !== -1); }, + value: filters.operator, + onChange: function (v) { setFilters(function (f) { var g = {}; for (var k in f) g[k] = f[k]; g.operator = v; return g; }); }, + style: { width: '100%' }, + options: operatorOptions + }) + ), + React.createElement('div', null, + React.createElement('div', { style: { marginBottom: 4 } }, '车牌号'), + React.createElement(Select, { + placeholder: '请选择或输入车牌号', + allowClear: true, + showSearch: true, + filterOption: function (input, opt) { return (opt && opt.label && String(opt.label).toLowerCase().indexOf((input || '').toLowerCase()) !== -1); }, + value: filters.plateNo, + onChange: function (v) { setFilters(function (f) { var g = {}; for (var k in f) g[k] = f[k]; g.plateNo = v; return g; }); }, + style: { width: '100%' }, + options: plateOptions + }) + ), + React.createElement('div', null, + React.createElement('div', { style: { marginBottom: 4 } }, '车辆类型'), + React.createElement(Select, { + placeholder: '请选择车辆类型', + allowClear: true, + value: filters.vehicleType, + onChange: function (v) { setFilters(function (f) { var g = {}; for (var k in f) g[k] = f[k]; g.vehicleType = v; return g; }); }, + style: { width: '100%' }, + options: vehicleTypeOptions + }) + ), + React.createElement('div', null, + React.createElement('div', { style: { marginBottom: 4 } }, '车辆型号'), + React.createElement(Cascader, { + options: vehicleModelCascaderOptions, + value: filters.vehicleModelCascader, + onChange: function (v) { setFilters(function (f) { var g = {}; for (var k in f) g[k] = f[k]; g.vehicleModelCascader = v; return g; }); }, + placeholder: '请选择品牌及型号', + allowClear: true, + style: { width: '100%' }, + changeOnSelect: true + }) + ), + React.createElement('div', null, + React.createElement('div', { style: { marginBottom: 4 } }, '停车场'), + React.createElement(Select, { + placeholder: '请选择停车场', + allowClear: true, + mode: 'multiple', + value: filters.parkingLot || [], + onChange: function (v) { setFilters(function (f) { var g = {}; for (var k in f) g[k] = f[k]; g.parkingLot = v || []; return g; }); }, + style: { width: '100%' }, + options: parkingOptions + }) + ) + ), + React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 } }, + React.createElement(Button, { onClick: handleReset }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleQuery }, '查询') + ) + ), + React.createElement(Card, null, + React.createElement(Tabs, { + activeKey: activeTab, + onChange: function (k) { setActiveTab(k); setPage(1); }, + tabBarExtraContent: React.createElement('div', { style: { display: 'flex', gap: 8 } }, + React.createElement(Button, { type: 'primary', onClick: function () {} }, '新增'), + activeTab === 'completed' ? React.createElement(Button, { onClick: handleExport }, '导出') : null + ), + items: [ + { + key: 'completed', + label: '已完成', + children: React.createElement(Table, { + columns: getCompletedColumns(), + dataSource: displayCompleted, + rowKey: 'id', + pagination: tablePagination, + scroll: { x: 1600 }, + size: 'middle' + }) + }, + { + key: 'pending', + label: '待提交', + children: React.createElement(Table, { + columns: getPendingColumns(), + dataSource: displayPending, + rowKey: 'id', + pagination: tablePagination, + scroll: { x: 1600 }, + size: 'middle' + }) + } + ] + }) + ), + React.createElement(Modal, { + title: '确认删除', + open: !!pendingDeleteId, + onCancel: function () { setPendingDeleteId(null); }, + onOk: handlePendingDelete, + okText: '确认', + cancelText: '取消' + }, '确定要删除该条待提交记录吗?'), + // 照片预览:全屏遮罩 + 放大图片,点击遮罩关闭 + photoModalState[0].open && photoModalState[0].photos && photoModalState[0].photos.length > 0 ? React.createElement('div', { + style: { + position: 'fixed', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(0,0,0,0.75)', + zIndex: 1000, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + cursor: 'pointer' + }, + onClick: function () { photoModalState[1]({ open: false, photos: [], currentIndex: 0 }); } + }, React.createElement('div', { + style: { position: 'relative', display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'default', maxWidth: '100%', maxHeight: '100%' }, + onClick: function (e) { if (e) e.stopPropagation(); } + }, + React.createElement('img', { + src: photoModalState[0].photos[photoModalState[0].currentIndex] || photoModalState[0].photos[0], + alt: '', + style: { maxWidth: '90vw', maxHeight: '85vh', objectFit: 'contain' } + }), + photoModalState[0].photos.length > 1 ? React.createElement('div', { style: { marginTop: 16, display: 'flex', alignItems: 'center', gap: 16 } }, + React.createElement(Button, { + disabled: photoModalState[0].currentIndex === 0, + onClick: function (e) { e.stopPropagation(); photoModalState[1]({ open: true, photos: photoModalState[0].photos, currentIndex: Math.max(0, photoModalState[0].currentIndex - 1) }); } + }, '上一张'), + React.createElement('span', { style: { color: '#fff' } }, (photoModalState[0].currentIndex + 1) + ' / ' + photoModalState[0].photos.length), + React.createElement(Button, { + disabled: photoModalState[0].currentIndex >= photoModalState[0].photos.length - 1, + onClick: function (e) { e.stopPropagation(); photoModalState[1]({ open: true, photos: photoModalState[0].photos, currentIndex: Math.min(photoModalState[0].photos.length - 1, photoModalState[0].currentIndex + 1) }); } + }, '下一张') + ) : null + )) : null + ); + } + + // —————— 新增页 —————— + function AddView() { + var form = addFormState[0]; + var setForm = addFormState[1]; + var step = addStepState[0]; + var addBreadcrumbItems = [ + { title: '运维管理' }, + { title: '车辆业务' }, + { title: '备车管理' }, + { title: '新增备车' } + ]; + return React.createElement('div', { style: { padding: 24, background: '#f5f5f5', minHeight: '100vh' } }, + React.createElement(Breadcrumb, { items: addBreadcrumbItems, style: { marginBottom: 16 } }), + React.createElement(Card, null, + React.createElement(Steps, { current: step - 1, style: { marginBottom: 24 }, items: [ + { title: '步骤1 车辆记录' }, + { title: '步骤2 备车检查' } + ] }), + step === 1 ? React.createElement(Row, { gutter: [16, 16] }, + React.createElement(Col, { span: 24 }, React.createElement('span', { style: { color: '#666', marginRight: 8 } }, '车牌号 *'), React.createElement(Select, { placeholder: '请选择车牌号(仅库存车辆)', allowClear: true, showSearch: true, optionFilterProp: 'label', value: form.plateNo || undefined, onChange: onPlateSelect, style: { width: 200 }, options: plateOptions })), + React.createElement(Col, { span: 24 }, React.createElement('span', { style: { color: '#666', marginRight: 8 } }, '车辆类型'), React.createElement(Input, { value: form.vehicleType, readOnly: true, style: { width: 200, background: '#f5f5f5' } })), + React.createElement(Col, { span: 24 }, React.createElement('span', { style: { color: '#666', marginRight: 8 } }, '品牌'), React.createElement(Input, { value: form.brand, readOnly: true, style: { width: 200, background: '#f5f5f5' } })), + React.createElement(Col, { span: 24 }, React.createElement('span', { style: { color: '#666', marginRight: 8 } }, '车架号'), React.createElement(Input, { value: form.vin, readOnly: true, style: { width: 200, background: '#f5f5f5' } })), + React.createElement(Col, { span: 24 }, React.createElement('span', { style: { color: '#666', marginRight: 8 } }, '车身广告 *'), React.createElement(Radio.Group, { value: form.bodyAd, onChange: function (e) { setForm(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.bodyAd = e.target.value; return n; }); }, options: [{ value: '无', label: '无' }, { value: '有', label: '有' }] })), + form.bodyAd === '有' ? React.createElement(Col, { span: 24 }, React.createElement('span', { style: { color: '#666', marginRight: 8 } }, '广告照片(最多5张)'), React.createElement(Space, null, (form.adPhotos || []).slice(0, 5).map(function (url, i) { return React.createElement('img', { key: i, src: url, alt: '', style: { width: 60, height: 60, objectFit: 'cover', borderRadius: 4 } }); }), (form.adPhotos || []).length < 5 ? React.createElement(Button, { onClick: function () { setForm(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.adPhotos = (p.adPhotos || []).concat(['https://picsum.photos/200/150?r=' + Date.now()]); return n; }); } }, '上传') : null)) : null, + React.createElement(Col, { span: 24 }, React.createElement('span', { style: { color: '#666', marginRight: 8 } }, '尾板 *'), React.createElement(Radio.Group, { value: form.tailboard, onChange: function (e) { setForm(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.tailboard = e.target.value; return n; }); }, options: [{ value: '无', label: '无' }, { value: '有', label: '有' }] })), + React.createElement(Col, { span: 24 }, React.createElement('span', { style: { color: '#666', marginRight: 8 } }, '挂车牌号(有挂时填写)'), React.createElement(Input, { value: form.trailerPlate, onChange: function (e) { setForm(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.trailerPlate = e.target.value; return n; }); }, placeholder: '不填视为无挂', style: { width: 200 } })), + React.createElement(Col, { span: 24 }, React.createElement('span', { style: { color: '#666', marginRight: 8 } }, '瑕疵照片(最多5张)'), React.createElement(Space, null, (form.flawPhotos || []).slice(0, 5).map(function (url, i) { return React.createElement('img', { key: i, src: url, alt: '', style: { width: 60, height: 60, objectFit: 'cover', borderRadius: 4 } }); }), (form.flawPhotos || []).length < 5 ? React.createElement(Button, { onClick: function () { setForm(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.flawPhotos = (p.flawPhotos || []).concat(['https://picsum.photos/200/150?f=' + Date.now()]); return n; }); } }, '上传') : null)), + React.createElement(Col, { span: 24 }, React.createElement(Space, null, React.createElement(Button, { type: 'primary', onClick: handleAddNext }, '下一步'), React.createElement(Button, { onClick: handleAddSave }, '保存'), React.createElement(Button, { onClick: function () { setCurrentView('list'); } }, '取消'))) + ) : React.createElement('div', null, + React.createElement('div', { style: { marginBottom: 16, fontWeight: 600 } }, '备车检查'), + React.createElement(Table, { + dataSource: form.inspectionList || [], + rowKey: function (_, i) { return i; }, + pagination: false, + columns: [ + { title: '检查类型', dataIndex: 'checkType', width: 100 }, + { title: '检查项', dataIndex: 'checkItem', width: 120 }, + { title: '检查项结果', dataIndex: 'result', width: 120, render: function (val, record, i) { + return React.createElement(Select, { value: val, onChange: function (v) { setForm(function (p) { var n = {}; for (var k in p) n[k] = p[k]; var list = (p.inspectionList || []).slice(); list[i] = Object.assign({}, list[i], { result: v }); n.inspectionList = list; return n; }); }, style: { width: '100%' }, options: [{ value: '正常', label: '正常' }, { value: '异常', label: '异常' }] }); + }}, + { title: '胎纹深度(轮胎时)', key: 'treadDepth', width: 140, render: function (_, record, i) { + if (record.checkItem && record.checkItem.indexOf('胎') !== -1) { + return React.createElement(Input, { value: record.treadDepth, onChange: function (e) { setForm(function (p) { var n = {}; for (var k in p) n[k] = p[k]; var list = (p.inspectionList || []).slice(); list[i] = Object.assign({}, list[i], { treadDepth: e.target.value }); n.inspectionList = list; return n; }); }, placeholder: '胎纹深度' }); + } + return '-'; + }}, + { title: '备注', dataIndex: 'remark', render: function (val, record, i) { + return React.createElement(Input, { value: val, onChange: function (e) { setForm(function (p) { var n = {}; for (var k in p) n[k] = p[k]; var list = (p.inspectionList || []).slice(); list[i] = Object.assign({}, list[i], { remark: e.target.value }); n.inspectionList = list; return n; }); }, placeholder: '备注' }); + }} + ] + }), + React.createElement('div', { style: { marginTop: 16 } }, React.createElement(Space, null, React.createElement(Button, { type: 'primary', onClick: handleAddSubmit }, '提交'), React.createElement(Button, { onClick: handleAddSave }, '保存'), React.createElement(Button, { onClick: function () { setCurrentView('list'); } }, '取消'))) + ) + ) + ); + } + + return ListView(); +}; + +if (typeof window !== 'undefined') { + window.Component = Component; + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function () { + var rootEl = document.getElementById('root'); + if (rootEl && window.ReactDOM && window.React) { + var root = ReactDOM.createRoot(rootEl); + root.render(React.createElement(Component)); + } + }); + } else { + var rootEl = document.getElementById('root'); + if (rootEl && window.ReactDOM && window.React) { + var root = ReactDOM.createRoot(rootEl); + root.render(React.createElement(Component)); + } + } +} + +export default Component; diff --git a/src/prototypes/oneos-web-ops/pages/07-查看故障.jsx b/src/prototypes/oneos-web-ops/pages/07-查看故障.jsx new file mode 100644 index 0000000..98afaa0 --- /dev/null +++ b/src/prototypes/oneos-web-ops/pages/07-查看故障.jsx @@ -0,0 +1,228 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// ONEOS-web - 运维管理 - 车辆业务 - 查看故障 + +const Component = function () { + var useState = React.useState; + + var antd = window.antd; + var Button = antd.Button; + var Input = antd.Input; + var Select = antd.Select; + var DatePicker = antd.DatePicker; + var Form = antd.Form; + var Row = antd.Row; + var Col = antd.Col; + var Breadcrumb = antd.Breadcrumb; + var Layout = antd.Layout; + var message = antd.message; + var Card = antd.Card; + var Upload = antd.Upload; + + var _form = Form.useForm(); + var faultForm = _form[0]; + + // 故障等级枚举 + var faultLevelOptions = [ + { label: '特急', value: '特急' }, + { label: '紧急', value: '紧急' }, + { label: '一般', value: '一般' }, + { label: '提示', value: '提示' } + ]; + + // 故障类型枚举 + var faultTypeOptions = [ + { label: '底盘故障', value: '底盘故障' }, + { label: '三电故障', value: '三电故障' }, + { label: '整车系统', value: '整车系统' }, + { label: '燃料电池系统故障', value: '燃料电池系统故障' }, + { label: '供氢系统故障', value: '供氢系统故障' }, + { label: '空调系统故障', value: '空调系统故障' }, + { label: '冷机故障', value: '冷机故障' }, + { label: '其他故障', value: '其他故障' } + ]; + + // 故障来源枚举 + var faultSourceOptions = [ + { label: '客户报告', value: '客户报告' }, + { label: '定期保养', value: '定期保养' }, + { label: '司机操作问题', value: '司机操作问题' } + ]; + + // 解决情况枚举 + var resolveStatusOptions = [ + { label: '未解决', value: '未解决' }, + { label: '临时排故', value: '临时排故' }, + { label: '已解决', value: '已解决' } + ]; + + var plateOptions = [ + { label: '沪A12345', value: '沪A12345', brand: '一汽解放', model: 'J6P', company: '上海羚牛', vin: 'LNW1234567890ABCD' }, + { label: '浙B88888', value: '浙B88888', brand: '东风商用车', model: '天龙', company: '浙江羚牛', vin: 'LNW0987654321EFGH' }, + { label: '苏C66666', value: '苏C66666', brand: '福田欧曼', model: 'EST', company: '苏州冷链速运有限公司', vin: 'LNW1357924680IJKL' }, + { label: '沪D99999', value: '沪D99999', brand: '陕汽重卡', model: '德龙', company: '上海城配物流有限公司', vin: 'LNW2468013579MNOP' } + ]; + + var handleBack = function () { + message.info('返回列表'); + }; + + // 模拟数据加载 + React.useEffect(function() { + var mockData = { key: '1', code: 'F-2024-001', plate: '沪A12345', brand: '一汽解放', model: 'J6P', company: '上海羚牛', type: '底盘故障', level: '紧急', source: '客户报告', status: '未解决', reportTime: '2024-05-10 08:30:00', lastOperator: '王婷婷', lastOperationTime: '2024-05-10 09:00:00', desc: '车辆重载下制动踏板偏软,制动距离明显变长' }; + if (mockData.reportTime) { + if (typeof window.dayjs === 'function') { + mockData.reportTime = window.dayjs(mockData.reportTime); + } else if (typeof window.moment === 'function') { + mockData.reportTime = window.moment(mockData.reportTime); + } + } + faultForm.setFieldsValue(mockData); + }, []); + + return React.createElement(Layout, { className: 'arco-theme-overrides', style: { minHeight: '100vh', background: '#f2f3f5', fontFamily: 'Inter, Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif' } }, + React.createElement('style', null, ` + .arco-theme-overrides .ant-btn { border-radius: 4px; } + .arco-theme-overrides .ant-btn-primary { background-color: #165dff; border-color: #165dff; } + .arco-theme-overrides .ant-btn-primary:hover { background-color: #4080ff; border-color: #4080ff; } + + .arco-grouped-form-page { display: flex; flex-direction: column; min-height: 100vh; } + .arco-grouped-form-page-content { flex: 1; padding: 16px 20px 24px; } + .arco-grouped-form-page .ant-card { margin-bottom: 16px; border-radius: 4px; border: none; box-shadow: 0 1px 2px rgba(0,0,0,0.06); } + .arco-grouped-form-page .ant-card-head { border-bottom: none; padding: 20px 24px 0; min-height: auto; } + .arco-grouped-form-page .ant-card-head-title { font-size: 16px; font-weight: 500; color: #1d2129; padding: 0; } + .arco-grouped-form-page .ant-card-body { padding: 24px; } + .arco-grouped-form-page .ant-form-vertical .ant-form-item-label { padding-bottom: 8px; height: auto; line-height: 1.5715; } + .arco-grouped-form-page .ant-form-item { margin-bottom: 24px; } + + .arco-grouped-form-page .ant-input, + .arco-grouped-form-page .ant-select-selector, + .arco-grouped-form-page .ant-picker, + .arco-grouped-form-page .ant-input-affix-wrapper { background-color: #f2f3f5; border: 1px solid #e5e6eb; border-radius: 2px; transition: all 0.1s cubic-bezier(0, 0, 1, 1); } + + .arco-grouped-form-page .ant-input[disabled], + .arco-grouped-form-page .ant-select-disabled .ant-select-selector, + .arco-grouped-form-page .ant-picker-disabled { color: #86909c; background-color: #f2f3f5; border-color: #e5e6eb; cursor: not-allowed; } + .arco-grouped-form-page .ant-input-affix-wrapper[disabled] { background-color: #f2f3f5; border-color: #e5e6eb; cursor: not-allowed; } + .arco-grouped-form-page .ant-input-affix-wrapper > input.ant-input { background-color: transparent; } + + .arco-grouped-form-footer { background: #fff; padding: 16px 24px; border-top: 1px solid #e5e6eb; display: flex; justify-content: flex-end; align-items: center; gap: 12px; position: sticky; bottom: 0; z-index: 100; box-shadow: 0 -2px 10px rgba(0,0,0,0.05); } + .arco-grouped-form-footer .ant-btn { border-radius: 5px; height: 32px; padding: 4px 16px; font-size: 14px; } + + .arco-theme-overrides .ant-breadcrumb { color: #86909c; font-size: 14px; white-space: nowrap; flex-shrink: 0; margin-bottom: 20px; } + .arco-theme-overrides .ant-breadcrumb a { color: #4e5969; } + .arco-theme-overrides .ant-breadcrumb a:hover { color: #165dff; background-color: transparent; } + .arco-theme-overrides .ant-form-item-label > label { color: #4e5969; white-space: nowrap; } + .arco-theme-overrides .ant-form-item-label > label::after { display: none !important; content: "" !important; margin: 0 !important; } + `), + React.createElement('div', { className: 'arco-grouped-form-page' }, + React.createElement('div', { className: 'arco-grouped-form-page-content' }, + React.createElement(Breadcrumb, { + separator: React.createElement('span', { style: { color: '#c9cdd4' } }, '/'), + items: [ + { title: '首页' }, + { title: '运维管理' }, + { title: '车辆业务' }, + { title: React.createElement('a', { onClick: function(e) { e.preventDefault(); handleBack(); } }, '故障管理') }, + { title: React.createElement('span', { style: { color: '#1d2129' } }, '查看故障单') } + ] + }), + + React.createElement(Form, { form: faultForm, layout: 'vertical', disabled: true }, + // 车辆信息 + React.createElement(Card, { title: '车辆信息', bordered: false }, + React.createElement(Row, { gutter: 24 }, + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '车牌号', name: 'plate', rules: [{ required: true, message: '请选择车牌号' }] }, + React.createElement(Select, { + placeholder: '请选择车牌号', + options: plateOptions, + showSearch: true + }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '车辆品牌', name: 'brand' }, + React.createElement(Input, { placeholder: '自动带入', disabled: true, style: { backgroundColor: '#f2f3f5', color: '#86909c' } }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '车辆型号', name: 'model' }, + React.createElement(Input, { placeholder: '自动带入', disabled: true, style: { backgroundColor: '#f2f3f5', color: '#86909c' } }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '运营公司', name: 'company' }, + React.createElement(Input, { placeholder: '自动带入', disabled: true, style: { backgroundColor: '#f2f3f5', color: '#86909c' } }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '车辆识别代码', name: 'vin' }, + React.createElement(Input, { placeholder: '自动带入', disabled: true, style: { backgroundColor: '#f2f3f5', color: '#86909c' } }) + ) + ) + ) + ), + + // 故障信息 + React.createElement(Card, { title: '故障信息', bordered: false }, + React.createElement(Row, { gutter: 24 }, + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '故障类型', name: 'type', rules: [{ required: true, message: '请选择故障类型' }] }, + React.createElement(Select, { placeholder: '请选择故障类型', options: faultTypeOptions }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '故障来源', name: 'source', rules: [{ required: true, message: '请选择故障来源' }] }, + React.createElement(Select, { placeholder: '请选择故障来源', options: faultSourceOptions }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '故障等级', name: 'level', rules: [{ required: true, message: '请选择故障等级' }] }, + React.createElement(Select, { placeholder: '请选择故障等级', options: faultLevelOptions }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '故障上报时间', name: 'reportTime', rules: [{ required: true, message: '请选择故障上报时间' }] }, + React.createElement(DatePicker, { style: { width: '100%' }, placeholder: '请选择上报时间', showTime: true }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '解决情况', name: 'status', rules: [{ required: true, message: '请选择故障解决情况' }] }, + React.createElement(Select, { placeholder: '请选择故障解决情况', options: resolveStatusOptions }) + ) + ) + ) + ), + + // 故障证据与描述 + React.createElement(Card, { title: '故障证据与描述', bordered: false }, + React.createElement(Row, { gutter: 24 }, + React.createElement(Col, { span: 24 }, + React.createElement(Form.Item, { label: '故障描述', name: 'desc', rules: [{ required: true, message: '请填写故障描述' }] }, + React.createElement(Input.TextArea, { + placeholder: '在何种状态下,产生何种现象,导致何种事故', + style: { height: 80, minHeight: 80, resize: 'none' } + }) + ) + ), + React.createElement(Col, { span: 24 }, + React.createElement(Form.Item, { label: '故障证据', name: 'evidence' }, + React.createElement(Upload, { listType: 'picture-card' }), + React.createElement('div', { style: { fontSize: 12, color: '#86909c', marginTop: 8 } }, '支持上传照片、视频、录音') + ) + ) + ) + ) + ) + ), + // 底部操作栏 + React.createElement('div', { className: 'arco-grouped-form-footer' }, + React.createElement(Button, { onClick: handleBack, style: { borderRadius: 5 } }, '返回列表') + ) + ) + ); +}; + +if (typeof module !== 'undefined' && module.exports) module.exports = Component; + +export default Component; diff --git a/src/prototypes/oneos-web-ops/pages/08-调拨管理-查看.jsx b/src/prototypes/oneos-web-ops/pages/08-调拨管理-查看.jsx new file mode 100644 index 0000000..8550c7d --- /dev/null +++ b/src/prototypes/oneos-web-ops/pages/08-调拨管理-查看.jsx @@ -0,0 +1,297 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 运维管理 - 车辆业务 - 调拨管理 - 查看(只读) + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + + var antd = window.antd; + var App = antd.App; + var Breadcrumb = antd.Breadcrumb; + var Button = antd.Button; + var Card = antd.Card; + var DatePicker = antd.DatePicker; + var Input = antd.Input; + var Modal = antd.Modal; + var Table = antd.Table; + var message = antd.message; + + function parseDateTime(s) { + var str = String(s || '').trim(); + if (!str) return null; + try { + if (window.dayjs) return window.dayjs(str); + } catch (e1) {} + try { + if (window.moment) return window.moment(str, ['YYYY-MM-DD HH:mm', 'YYYY-MM-DD HH:mm:ss', 'YYYY-MM-DD'], true); + } catch (e2) {} + return null; + } + + function getMockSnapshot() { + return { + transferDateStr: '2026-03-31 10:30', + departRegionText: '广东省-广州市', + receiveRegionText: '浙江省-嘉兴市', + reason: '华南业务增量,需将车辆调至华东仓储节点保障运力。', + method: '司机运输', + transportLeader: '李建国', + transportPhone: '13800138000', + receivePersonName: '王芳', + receiveTimeStr: '2026-04-01 15:20', + receiveParkingName: '西湖景区停车场', + transferVehicles: [ + { + id: 1, + brand: '小鹏', + model: 'P7', + plateNo: '浙A11111', + departMileageKm: '12010.00', + departHydrogen: '60.00', + h2Unit: '%', + departElectricKwh: '55.20', + departParking: '西湖景区停车场', + arriveMileageKm: '12110.20', + arriveHydrogen: '58.00', + arriveElectricKwh: '52.30' + }, + { + id: 2, + brand: '蔚来', + model: 'ET5', + plateNo: '浙B22222', + departMileageKm: '15010.00', + departHydrogen: '22.10', + h2Unit: 'MPa', + departElectricKwh: '58.00', + departParking: '宁波江北停车场', + arriveMileageKm: '15120.00', + arriveHydrogen: '21.80', + arriveElectricKwh: '57.50' + } + ] + }; + } + + var layoutStyle = { padding: '16px 24px 88px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var labelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var formItemStyle = { marginBottom: 12 }; + var controlStyle = { width: '100%' }; + var readOnlyStyle = { background: 'rgba(0,0,0,0.02)' }; + + var snapshot = useMemo(function () { return getMockSnapshot(); }, []); + + var requirementModalState = useState(false); + var requirementModalOpen = requirementModalState[0]; + var setRequirementModalOpen = requirementModalState[1]; + + var requirementDocContent = [ + '一个「数字化资产ONEOS运管平台」中的「调拨管理」「查看」模块(只读)', + '#面包屑:运维管理-车辆业务-调拨管理-查看', + '参照「调拨管理-接收信息-司机运输」页面结构生成查看页:', + '1.调拨情况(只读)', + '2.调拨信息(只读)', + '3.接收信息(只读):接收时间、停车场(直接展示所选停车场名称)', + '4.调拨车辆清单(只读,包含到达时里程/氢量/电量)', + '5.底部按钮仅保留「返回」。' + ].join('\n'); + + var vehicleColumns = useMemo(function () { + return [ + { title: '品牌', key: 'brand', width: 120, render: function (_, r) { return React.createElement(Input, { value: r.brand || '', readOnly: true, style: readOnlyStyle }); } }, + { title: '型号', key: 'model', width: 120, render: function (_, r) { return React.createElement(Input, { value: r.model || '', readOnly: true, style: readOnlyStyle }); } }, + { title: '车牌号', key: 'plateNo', width: 140, render: function (_, r) { return React.createElement(Input, { value: r.plateNo || '', readOnly: true, style: readOnlyStyle }); } }, + { title: '出发停车场', key: 'departParking', width: 180, render: function (_, r) { return React.createElement(Input, { value: r.departParking || '', readOnly: true, style: readOnlyStyle }); } }, + { title: '出发时里程', key: 'departMileageKm', width: 130, render: function (_, r) { return React.createElement(Input, { value: r.departMileageKm || '', disabled: true, addonAfter: 'km' }); } }, + { title: '到达时里程', key: 'arriveMileageKm', width: 140, render: function (_, r) { return React.createElement(Input, { value: r.arriveMileageKm || '', disabled: true, addonAfter: 'km' }); } }, + { title: '出发时氢量', key: 'departHydrogen', width: 130, render: function (_, r) { return React.createElement(Input, { value: r.departHydrogen || '', disabled: true, addonAfter: r.h2Unit || '-' }); } }, + { title: '到达时氢量', key: 'arriveHydrogen', width: 140, render: function (_, r) { return React.createElement(Input, { value: r.arriveHydrogen || '', disabled: true, addonAfter: r.h2Unit || '%' }); } }, + { title: '出发时电量', key: 'departElectricKwh', width: 130, render: function (_, r) { return React.createElement(Input, { value: r.departElectricKwh || '', disabled: true, addonAfter: 'kWh' }); } }, + { title: '到达时电量', key: 'arriveElectricKwh', width: 140, render: function (_, r) { return React.createElement(Input, { value: r.arriveElectricKwh || '', disabled: true, addonAfter: 'kWh' }); } } + ]; + }, []); + + var handleBack = useCallback(function () { + try { + if (window.history && window.history.length > 1) { + window.history.back(); + return; + } + } catch (e) {} + message.info('返回调拨管理列表页(原型)'); + }, []); + + var receiveTimeVal = useMemo(function () { return parseDateTime(snapshot.receiveTimeStr); }, [snapshot.receiveTimeStr]); + + return React.createElement( + App, + null, + React.createElement( + 'div', + { style: layoutStyle }, + React.createElement( + 'div', + { + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: 16, + flexWrap: 'wrap', + gap: 8 + } + }, + React.createElement(Breadcrumb, { items: [{ title: '运维管理' }, { title: '车辆业务' }, { title: '调拨管理' }, { title: '查看' }] }), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { setRequirementModalOpen(true); } }, '查看需求说明') + ), + + React.createElement( + Modal, + { + title: '需求说明', + open: requirementModalOpen, + onCancel: function () { setRequirementModalOpen(false); }, + width: 760, + footer: React.createElement(Button, { onClick: function () { setRequirementModalOpen(false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, + React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.65, color: 'rgba(0,0,0,0.85)' } }, requirementDocContent) + ), + + React.createElement( + Card, + { title: '调拨情况', style: { marginBottom: 16 } }, + React.createElement( + 'div', + { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px 24px', alignItems: 'start' } }, + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '调拨日期'), + React.createElement(Input, { value: snapshot.transferDateStr, readOnly: true, style: readOnlyStyle }) + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '出发区域'), + React.createElement(Input, { value: snapshot.departRegionText, readOnly: true, style: readOnlyStyle }) + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '接收区域'), + React.createElement(Input, { value: snapshot.receiveRegionText, readOnly: true, style: readOnlyStyle }) + ), + React.createElement( + 'div', + { style: Object.assign({}, formItemStyle, { gridColumn: 'span 3' }) }, + React.createElement('div', { style: labelStyle }, '调拨原因'), + React.createElement(Input.TextArea, { value: snapshot.reason, readOnly: true, style: readOnlyStyle, autoSize: { minRows: 3, maxRows: 6 } }) + ) + ) + ), + + React.createElement( + Card, + { title: '调拨信息', style: { marginBottom: 16 } }, + React.createElement( + 'div', + { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px 24px', alignItems: 'start' } }, + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '调拨方式'), + React.createElement(Input, { value: snapshot.method, readOnly: true, style: readOnlyStyle }) + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '运输负责人'), + React.createElement(Input, { value: snapshot.transportLeader, readOnly: true, style: readOnlyStyle }) + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '运输方联系方式'), + React.createElement(Input, { value: snapshot.transportPhone, readOnly: true, style: readOnlyStyle }) + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '接收人员'), + React.createElement(Input, { value: snapshot.receivePersonName, readOnly: true, style: readOnlyStyle }) + ) + ) + ), + + React.createElement( + Card, + { title: '接收信息', style: { marginBottom: 16 } }, + React.createElement( + 'div', + { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px 24px', alignItems: 'start' } }, + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '接收时间'), + React.createElement(DatePicker, { + style: controlStyle, + format: 'YYYY-MM-DD HH:mm', + showTime: { format: 'HH:mm' }, + placeholder: '-', + value: receiveTimeVal, + disabled: true, + inputReadOnly: true + }) + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '停车场'), + React.createElement(Input, { + value: snapshot.receiveParkingName || '—', + readOnly: true, + style: readOnlyStyle + }) + ) + ) + ), + + React.createElement( + Card, + { title: '调拨车辆清单', style: { marginBottom: 16 } }, + React.createElement(Table, { + rowKey: 'id', + columns: vehicleColumns, + dataSource: snapshot.transferVehicles || [], + size: 'small', + pagination: false, + scroll: { x: 'max-content' } + }) + ), + + React.createElement('div', { style: { height: 56 } }), + React.createElement( + 'div', + { + style: { + position: 'fixed', + left: 0, + right: 0, + bottom: 0, + padding: '12px 24px', + background: '#fff', + borderTop: '1px solid #f0f0f0', + display: 'flex', + gap: 8, + zIndex: 10 + } + }, + React.createElement(Button, { onClick: handleBack }, '返回') + ) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-ops/pages/09-调拨管理-调拨信息.jsx b/src/prototypes/oneos-web-ops/pages/09-调拨管理-调拨信息.jsx new file mode 100644 index 0000000..5d0440d --- /dev/null +++ b/src/prototypes/oneos-web-ops/pages/09-调拨管理-调拨信息.jsx @@ -0,0 +1,648 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 运维管理 - 车辆业务 - 调拨管理 - 调拨信息(数字化资产 ONEOS 运管平台) + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + + var antd = window.antd; + var App = antd.App; + var Breadcrumb = antd.Breadcrumb; + var Button = antd.Button; + var Card = antd.Card; + var Input = antd.Input; + var Modal = antd.Modal; + var Select = antd.Select; + var Table = antd.Table; + var Upload = antd.Upload; + var message = antd.message; + + var METHOD_DRIVER = '司机运输'; + var METHOD_THIRD = '第三方运输'; + var PLATE_ELIGIBLE_STATUS = '可运营'; + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + var layoutStyle = { padding: '16px 24px 88px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var labelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var formItemStyle = { marginBottom: 12 }; + var controlStyle = { width: '100%' }; + var readOnlyStyle = { background: 'rgba(0,0,0,0.02)' }; + var reqStarStyle = { color: '#ff4d4f', marginRight: 4 }; + var reqStar = React.createElement('span', { style: reqStarStyle }, '*'); + + var regionOptions = useMemo(function () { + return [ + { value: 'zhejiang', label: '浙江省', children: [{ value: 'hangzhou', label: '杭州市' }, { value: 'ningbo', label: '宁波市' }, { value: 'jiaxing', label: '嘉兴市' }] }, + { value: 'shanghai', label: '上海市', children: [{ value: 'shanghai', label: '上海市' }] }, + { value: 'guangdong', label: '广东省', children: [{ value: 'guangzhou', label: '广州市' }, { value: 'shenzhen', label: '深圳市' }, { value: 'dongguan', label: '东莞市' }] } + ]; + }, []); + + function regionValueToProvinceCityLabel(values) { + if (!values || values.length < 2) return ''; + var i; + var p = null; + for (i = 0; i < regionOptions.length; i++) { + if (regionOptions[i].value === values[0]) { + p = regionOptions[i]; + break; + } + } + if (!p || !p.children) return ''; + var c = null; + for (i = 0; i < p.children.length; i++) { + if (p.children[i].value === values[1]) { + c = p.children[i]; + break; + } + } + if (!c) return p.label; + return p.label + '-' + c.label; + } + + var vehicleDb = useMemo(function () { + return [ + { plateNo: '粤A12345', status: '可运营', brand: '东风', model: 'DFH1180', dashboardH2Unit: 'MPa', currentParking: '天河智慧停车场' }, + { plateNo: '粤B11111', status: '可运营', brand: '比亚迪', model: '汉', dashboardH2Unit: '%', currentParking: '南山科技园停车场' }, + { plateNo: '浙A11111', status: '可运营', brand: '小鹏', model: 'P7', dashboardH2Unit: '%', currentParking: '西湖景区停车场' }, + { plateNo: '浙B22222', status: '可运营', brand: '蔚来', model: 'ET5', dashboardH2Unit: 'MPa', currentParking: '宁波江北停车场' }, + { plateNo: '沪A30003', status: '可运营', brand: '福田', model: 'BJ1180', dashboardH2Unit: 'MPa', currentParking: '张江园区停车场' }, + { plateNo: '粤C99999', status: '维修中', brand: '东风', model: 'DFH1180', dashboardH2Unit: 'MPa', currentParking: '白云维修基地停车场' } + ]; + }, []); + + var plateOptionsEligible = useMemo(function () { + return vehicleDb + .filter(function (v) { + return v.status === PLATE_ELIGIBLE_STATUS; + }) + .map(function (v) { + return { value: v.plateNo, label: v.plateNo }; + }); + }, [vehicleDb]); + + function lookupVehicle(plateNo) { + return vehicleDb.find(function (x) { + return x.plateNo === plateNo; + }); + } + + function buildVehicleRowsFromSnapshot(snap) { + return snap.vehicles.map(function (r) { + var v = r.plateNo ? lookupVehicle(r.plateNo) : null; + return { + id: r.id, + brand: r.brand, + model: r.model, + plateNo: undefined, + h2Unit: v && v.dashboardH2Unit ? v.dashboardH2Unit : '%', + departParking: v && v.currentParking ? v.currentParking : '', + departMileageKm: '', + departHydrogen: '', + departElectricKwh: '' + }; + }); + } + + /** 原型:模拟「新增调拨」提交后带入本页的快照(对接接口后由详情接口渲染) */ + var mockFromCreate = useMemo(function () { + return { + transferDateStr: '2026-03-31', + departRegionValues: ['guangdong', 'guangzhou'], + receiveRegionValues: ['zhejiang', 'jiaxing'], + reason: '华南业务增量,需将车辆调至华东仓储节点保障运力。', + vehicles: [ + { id: 1, brand: '小鹏', model: 'P7', plateNo: '浙A11111' }, + { id: 2, brand: '蔚来', model: 'ET5', plateNo: '浙B22222' } + ] + }; + }, []); + + var snapshotState = useState(mockFromCreate); + var snapshot = snapshotState[0]; + + var vehiclesState = useState(function () { + return buildVehicleRowsFromSnapshot(mockFromCreate); + }); + var vehicles = vehiclesState[0]; + var setVehicles = vehiclesState[1]; + + var infoState = useState({ + method: undefined, + transportLeader: '', + transportPhone: '', + transportCost: '', + receivePerson: undefined + }); + var info = infoState[0]; + var setInfo = infoState[1]; + + var fileListState = useState([]); + var fileList = fileListState[0]; + var setFileList = fileListState[1]; + + var errorsState = useState({}); + var errors = errorsState[0]; + var setErrors = errorsState[1]; + + var requirementModalState = useState(false); + var requirementModalOpen = requirementModalState[0]; + var setRequirementModalOpen = requirementModalState[1]; + + var requirementDocContent = [ + '一个「数字化资产ONEOS运管平台」中的「调拨管理」「调拨信息」模块', + '', + '1.调拨情况:', + '1.1.调拨日期:反写新增调拨时的调拨日期,格式为:YYYY-MM-DD;', + '1.2.出发区域:反写新增调拨时的出发区域,格式为:省-市;', + '1.3.接收区域:反写新增调拨时的接收区域,格式为:省-市;', + '1.4.调拨原因:反写新增调拨时的调拨原因;', + '', + '2.调拨信息:', + '2.1.调拨方式:必填项,选择器,枚举值为:司机运输、第三方运输;', + '2.2.运输负责人:必填项,输入框,支持用户自定义输入;', + '2.3.运输方联系方式:必填项,输入框,限制为11位数字手机号格式;', + '2.4.运输费用:必填项,输入框,支持2位小数,后缀为元,只有运输方式为第三方运输时显示;', + '2.5.运输合同附件:选填项,按钮,文案为附件上传,点击支持本地文件上传(doc、docx、pdf格式),只有运输方式为第三方运输时显示;', + '2.6.接收人员:必填项,选择器,选择用户表所有运维部门人员,支持输入框输入内容搜索下拉对应项;', + '', + '3.调拨车辆清单;', + '#以表格方式展示,反写新增调拨时的整个清单:', + '3.1.品牌:反写新增调拨时的品牌;', + '3.2.型号:反写新增调拨时的型号;', + '3.3.车牌号:必填项,选择器,只能选择该型号、同时运营状态为:可运营的车辆;如业务在新增时已填写车牌号信息,此处反写,但支持修改;', + '3.4.出发停车场:根据车牌号,自动反写该车辆当前停车场;', + '3.5.出发时里程:必填项,输入框,支持2位小数,后缀为km,只有调拨方式为司机运输时显示;', + '3.6.出发时氢量:必填项,输入框,支持2位小数,后缀为%或MPa(取自对应车辆该型号对应型号参数下仪表盘单位),只有调拨方式为司机运输时显示;', + '3.7.出发时电量:必填项,输入框,支持2位小数,后缀为kWh,只有调拨方式为司机运输时显示;', + '', + '4.底部按钮为提交审核、保存、取消;', + '4.1.提交审核:点击提交审核校验必填项;', + '4.2.保存:不校验必填项,只保存已填数据并保存在列表页;' + ].join('\n'); + + var methodOptions = useMemo(function () { + return [ + { value: METHOD_DRIVER, label: METHOD_DRIVER }, + { value: METHOD_THIRD, label: METHOD_THIRD } + ]; + }, []); + + var opsStaffOptions = useMemo(function () { + return [ + { value: 'zm', label: '张明(运维一部-现场工程师)' }, + { value: 'wf', label: '王芳(运维二部-调度)' }, + { value: 'lh', label: '李华(运维中心-班长)' }, + { value: 'zq', label: '赵强(运维一部-驻场)' }, + { value: 'cj', label: '陈静(运维三部-客服协同)' } + ]; + }, []); + + function toFixed2Input(v) { + var s = String(v === null || v === undefined ? '' : v); + s = s.replace(/[^\d.]/g, ''); + var firstDot = s.indexOf('.'); + if (firstDot >= 0) { + s = s.slice(0, firstDot + 1) + s.slice(firstDot + 1).replace(/\./g, ''); + } + if (firstDot >= 0) { + var a = s.split('.'); + s = a[0] + '.' + (a[1] || '').slice(0, 2); + } + return s; + } + + function phoneDigitsOnly(v) { + return String(v || '') + .replace(/\D/g, '') + .slice(0, 11); + } + + var updateInfo = useCallback(function (patch) { + setInfo(function (p) { + return Object.assign({}, p, patch); + }); + }, []); + + var updateRow = useCallback(function (index, patch) { + setVehicles(function (prev) { + var list = prev.slice(); + var cur = list[index] || {}; + list[index] = Object.assign({}, cur, patch); + return list; + }); + }, []); + + var handlePlateChange = useCallback( + function (index, plateNo) { + var v = plateNo ? lookupVehicle(plateNo) : null; + var unit = v && v.dashboardH2Unit ? v.dashboardH2Unit : '%'; + updateRow(index, { plateNo: plateNo, h2Unit: unit, departParking: v && v.currentParking ? v.currentParking : '' }); + }, + [updateRow, vehicleDb] + ); + + function runValidate() { + var e = {}; + if (!info.method) e.method = '请选择调拨方式'; + if (!String(info.transportLeader || '').trim()) e.transportLeader = '请输入运输负责人姓名'; + if (!info.receivePerson) e.receivePerson = '请选择接收人员'; + + var phone = String(info.transportPhone || '').trim(); + if (!phone) e.transportPhone = '请输入运输方联系方式'; + else if (!/^\d{11}$/.test(phone)) e.transportPhone = '请输入11位数字手机号'; + + if (info.method === METHOD_THIRD) { + if (!String(info.transportCost || '').trim()) e.transportCost = '请输入运输费用'; + } + + for (var i = 0; i < (vehicles || []).length; i++) { + var r = vehicles[i] || {}; + if (!String(r.plateNo || '').trim()) e['row_' + i + '_plate'] = '请选择车牌号'; + if (info.method === METHOD_DRIVER) { + if (!String(r.departMileageKm || '').trim()) e['row_' + i + '_mile'] = '请输入出发时里程'; + if (!String(r.departHydrogen || '').trim()) e['row_' + i + '_h2'] = '请输入出发时氢量'; + if (!String(r.departElectricKwh || '').trim()) e['row_' + i + '_kwh'] = '请输入出发时电量'; + } + } + setErrors(e); + return { ok: Object.keys(e).length === 0, err: e }; + } + + var handleSubmitAudit = useCallback(function () { + var result = runValidate(); + if (!result.ok) { + message.error('请完善必填项'); + return; + } + Modal.confirm({ + title: '确认提交审核?', + content: '提交后将进入审批流程(原型)。', + okText: '提交审核', + cancelText: '取消', + onOk: function () { + message.success('已提交审核(原型)'); + } + }); + }, [info, vehicles]); + + var handleSave = useCallback(function () { + message.success('已保存至列表页(原型:不校验必填项,仅保存当前已填数据)'); + }, []); + + var handleCancel = useCallback(function () { + Modal.confirm({ + title: '是否确认取消', + content: '未保存的修改将丢失,是否确认?', + okText: '确认', + cancelText: '取消', + onOk: function () { + message.info('返回上一页(原型)'); + } + }); + }, []); + + var vehicleColumns = useMemo( + function () { + var cols = [ + { + title: '品牌', + key: 'brand', + width: 100, + render: function (_, r) { + return React.createElement(Input, { value: r.brand || '', readOnly: true, style: readOnlyStyle }); + } + }, + { + title: '型号', + key: 'model', + width: 110, + render: function (_, r) { + return React.createElement(Input, { value: r.model || '', readOnly: true, style: readOnlyStyle }); + } + }, + { + title: React.createElement('span', null, reqStar, '车牌号'), + key: 'plateNo', + width: 220, + render: function (_, r, index) { + return React.createElement(Select, { + placeholder: '请选择或输入车牌号', + style: { width: '100%' }, + value: r.plateNo, + onChange: function (v) { + handlePlateChange(index, v); + }, + allowClear: true, + showSearch: true, + options: plateOptionsEligible, + filterOption: filterOption, + status: errors['row_' + index + '_plate'] ? 'error' : undefined + }); + } + } + ]; + + cols.push({ + title: '出发停车场', + key: 'departParking', + width: 160, + render: function (_, r) { + return React.createElement(Input, { value: r.departParking || '', readOnly: true, style: readOnlyStyle, placeholder: '—' }); + } + }); + + if (info.method === METHOD_DRIVER) { + cols.push({ + title: React.createElement('span', null, reqStar, '出发时里程'), + key: 'departMileageKm', + width: 140, + render: function (_, r, index) { + return React.createElement(Input, { + value: r.departMileageKm, + onChange: function (e) { + updateRow(index, { departMileageKm: toFixed2Input(e.target.value) }); + }, + placeholder: '0.00', + addonAfter: 'km', + status: errors['row_' + index + '_mile'] ? 'error' : undefined + }); + } + }); + cols.push({ + title: React.createElement('span', null, reqStar, '出发时氢量'), + key: 'departHydrogen', + width: 130, + render: function (_, r, index) { + return React.createElement(Input, { + value: r.departHydrogen, + onChange: function (e) { + updateRow(index, { departHydrogen: toFixed2Input(e.target.value) }); + }, + placeholder: '0.00', + addonAfter: r.h2Unit || '%', + status: errors['row_' + index + '_h2'] ? 'error' : undefined + }); + } + }); + cols.push({ + title: React.createElement('span', null, reqStar, '出发时电量'), + key: 'departElectricKwh', + width: 140, + render: function (_, r, index) { + return React.createElement(Input, { + value: r.departElectricKwh, + onChange: function (e) { + updateRow(index, { departElectricKwh: toFixed2Input(e.target.value) }); + }, + placeholder: '0.00', + addonAfter: 'kWh', + status: errors['row_' + index + '_kwh'] ? 'error' : undefined + }); + } + }); + } + + return cols; + }, + [info.method, plateOptionsEligible, errors, handlePlateChange, updateRow] + ); + + var departRegionText = regionValueToProvinceCityLabel(snapshot.departRegionValues); + var receiveRegionText = regionValueToProvinceCityLabel(snapshot.receiveRegionValues); + + return React.createElement( + App, + null, + React.createElement( + 'div', + { style: layoutStyle }, + React.createElement( + 'div', + { + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: 16, + flexWrap: 'wrap', + gap: 8 + } + }, + React.createElement(Breadcrumb, { + items: [{ title: '运维管理' }, { title: '车辆业务' }, { title: '调拨管理' }, { title: '调拨信息' }] + }), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { setRequirementModalOpen(true); } }, '查看需求说明') + ), + + React.createElement( + Modal, + { + title: '需求说明', + open: requirementModalOpen, + onCancel: function () { setRequirementModalOpen(false); }, + width: 760, + footer: React.createElement(Button, { onClick: function () { setRequirementModalOpen(false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, + React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.65, color: 'rgba(0,0,0,0.85)' } }, requirementDocContent) + ), + + React.createElement( + Card, + { title: '调拨情况', style: { marginBottom: 16 } }, + React.createElement( + 'div', + { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px 24px', alignItems: 'start' } }, + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '调拨日期'), + React.createElement(Input, { value: snapshot.transferDateStr, readOnly: true, style: readOnlyStyle, placeholder: 'YYYY-MM-DD' }) + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '出发区域'), + React.createElement(Input, { value: departRegionText, readOnly: true, style: readOnlyStyle, placeholder: '省-市' }) + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '接收区域'), + React.createElement(Input, { value: receiveRegionText, readOnly: true, style: readOnlyStyle, placeholder: '省-市' }) + ), + React.createElement( + 'div', + { style: Object.assign({}, formItemStyle, { gridColumn: 'span 3' }) }, + React.createElement('div', { style: labelStyle }, '调拨原因'), + React.createElement(Input.TextArea, { + value: snapshot.reason, + readOnly: true, + style: readOnlyStyle, + autoSize: { minRows: 3, maxRows: 6 } + }) + ) + ) + ), + + React.createElement( + Card, + { title: '调拨信息', style: { marginBottom: 16 } }, + React.createElement( + 'div', + { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px 24px', alignItems: 'start' } }, + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, reqStar, '调拨方式'), + React.createElement(Select, { + placeholder: '请选择调拨方式', + style: controlStyle, + value: info.method, + onChange: function (v) { + updateInfo({ method: v }); + }, + allowClear: true, + options: methodOptions, + status: errors.method ? 'error' : undefined + }), + errors.method ? React.createElement('div', { style: { marginTop: 4, color: '#ff4d4f', fontSize: 12 } }, errors.method) : null + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, reqStar, '运输负责人'), + React.createElement(Input, { + placeholder: '请输入运输负责人姓名', + value: info.transportLeader, + onChange: function (e) { + updateInfo({ transportLeader: e.target.value }); + }, + status: errors.transportLeader ? 'error' : undefined + }) + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, reqStar, '运输方联系方式'), + React.createElement(Input, { + placeholder: '请输入运输方联系方式', + value: info.transportPhone, + maxLength: 11, + onChange: function (e) { + updateInfo({ transportPhone: phoneDigitsOnly(e.target.value) }); + }, + status: errors.transportPhone ? 'error' : undefined + }), + errors.transportPhone ? React.createElement('div', { style: { marginTop: 4, color: '#ff4d4f', fontSize: 12 } }, errors.transportPhone) : null + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, reqStar, '接收人员'), + React.createElement(Select, { + placeholder: '请输入或选择运维部门人员', + style: controlStyle, + value: info.receivePerson, + onChange: function (v) { + updateInfo({ receivePerson: v }); + }, + allowClear: true, + showSearch: true, + options: opsStaffOptions, + filterOption: filterOption, + status: errors.receivePerson ? 'error' : undefined + }), + errors.receivePerson ? React.createElement('div', { style: { marginTop: 4, color: '#ff4d4f', fontSize: 12 } }, errors.receivePerson) : null + ) + ), + + info.method === METHOD_THIRD + ? React.createElement( + 'div', + { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px 24px', marginTop: 8 } }, + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, reqStar, '运输费用'), + React.createElement(Input, { + placeholder: '0.00', + value: info.transportCost, + onChange: function (e) { + updateInfo({ transportCost: toFixed2Input(e.target.value) }); + }, + addonAfter: '元', + status: errors.transportCost ? 'error' : undefined + }), + errors.transportCost ? React.createElement('div', { style: { marginTop: 4, color: '#ff4d4f', fontSize: 12 } }, errors.transportCost) : null + ), + React.createElement( + 'div', + { style: Object.assign({}, formItemStyle, { gridColumn: 'span 2' }) }, + React.createElement('div', { style: labelStyle }, '运输合同附件(选填)'), + React.createElement( + Upload, + { + accept: '.doc,.docx,.pdf', + multiple: true, + fileList: fileList, + beforeUpload: function () { + return false; + }, + onChange: function (i) { + setFileList(i.fileList || []); + } + }, + React.createElement(Button, null, '附件上传') + ), + React.createElement('div', { style: { marginTop: 6, fontSize: 12, color: 'rgba(0,0,0,0.45)' } }, '支持 doc、docx、pdf;原型不实际上传服务器') + ) + ) + : null + ), + + React.createElement( + Card, + { title: '调拨车辆清单', style: { marginBottom: 16 } }, + React.createElement(Table, { + rowKey: 'id', + columns: vehicleColumns, + dataSource: vehicles, + size: 'small', + pagination: false, + scroll: { x: 'max-content' } + }) + ), + + React.createElement('div', { style: { height: 56 } }), + React.createElement( + 'div', + { + style: { + position: 'fixed', + left: 0, + right: 0, + bottom: 0, + padding: '12px 24px', + background: '#fff', + borderTop: '1px solid #f0f0f0', + display: 'flex', + gap: 8, + zIndex: 10 + } + }, + React.createElement(Button, { type: 'primary', onClick: handleSubmitAudit }, '提交审核'), + React.createElement(Button, { onClick: handleSave }, '保存'), + React.createElement(Button, { onClick: handleCancel }, '取消') + ) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-ops/pages/10-调拨管理-接收信息-第三方运输.jsx b/src/prototypes/oneos-web-ops/pages/10-调拨管理-接收信息-第三方运输.jsx new file mode 100644 index 0000000..d791c5a --- /dev/null +++ b/src/prototypes/oneos-web-ops/pages/10-调拨管理-接收信息-第三方运输.jsx @@ -0,0 +1,460 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 运维管理 - 车辆业务 - 调拨管理 - 接收信息(数字化资产 ONEOS 运管平台) +// 本页用例:调拨方式为「第三方运输」 + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + + var antd = window.antd; + var App = antd.App; + var Breadcrumb = antd.Breadcrumb; + var Button = antd.Button; + var Card = antd.Card; + var DatePicker = antd.DatePicker; + var Input = antd.Input; + var Modal = antd.Modal; + var Select = antd.Select; + var Table = antd.Table; + var message = antd.message; + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + var METHOD_THIRD = '第三方运输'; + + function getMockSnapshot() { + return { + transferDateStr: '2026-03-31', + departRegionText: '广东省-广州市', + receiveRegionText: '浙江省-嘉兴市', + reason: '华南业务增量,需将车辆调至华东仓储节点保障运力。', + method: METHOD_THIRD, + transportLeader: '刘振华', + transportPhone: '13901234567', + transportCost: '8650.50', + transportContractFileLabel: '顺达物流有限公司运输合同.pdf', + receivePersonName: '王芳', + transferVehicles: [ + { id: 1, brand: '小鹏', model: 'P7', plateNo: '浙A11111', departParking: '西湖景区停车场' }, + { id: 2, brand: '蔚来', model: 'ET5', plateNo: '浙B22222', departParking: '宁波江北停车场' } + ] + }; + } + + var layoutStyle = { padding: '16px 24px 88px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var labelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var formItemStyle = { marginBottom: 12 }; + var controlStyle = { width: '100%' }; + var readOnlyStyle = { background: 'rgba(0,0,0,0.02)' }; + var reqStarStyle = { color: '#ff4d4f', marginRight: 4 }; + var reqStar = React.createElement('span', { style: reqStarStyle }, '*'); + + var snapshot = useMemo(function () { + return getMockSnapshot(); + }, []); + + function buildVehicleRows(snap) { + return snap.transferVehicles.map(function (r) { + return { + id: r.id, + brand: r.brand, + model: r.model, + plateNo: r.plateNo, + departParking: r.departParking || '' + }; + }); + } + + var receiveTimeState = useState(null); + var receiveTime = receiveTimeState[0]; + var setReceiveTime = receiveTimeState[1]; + + var receiveParkingState = useState(undefined); + var receiveParking = receiveParkingState[0]; + var setReceiveParking = receiveParkingState[1]; + + var vehiclesState = useState(function () { + return buildVehicleRows(getMockSnapshot()); + }); + var vehicles = vehiclesState[0]; + + var parkingOptions = useMemo(function () { + return [ + { value: 'p1', label: '西湖景区停车场' }, + { value: 'p2', label: '宁波江北停车场' }, + { value: 'p3', label: '张江园区停车场' }, + { value: 'p4', label: '天河智慧停车场' } + ]; + }, []); + + var errorsState = useState({}); + var errors = errorsState[0]; + var setErrors = errorsState[1]; + + var requirementModalState = useState(false); + var requirementModalOpen = requirementModalState[0]; + var setRequirementModalOpen = requirementModalState[1]; + + var attachmentPreviewState = useState(false); + var attachmentPreviewOpen = attachmentPreviewState[0]; + var setAttachmentPreviewOpen = attachmentPreviewState[1]; + + var requirementDocContent = [ + '一个「数字化资产ONEOS运管平台」中的「调拨管理」「接收信息」模块', + '', + '1.调拨情况:', + '1.1.调拨日期:反写新增调拨时的调拨日期,格式为:YYYY-MM-DD HH:mm;', + '1.2.出发区域:反写新增调拨时的出发区域,格式为:省-市;', + '1.3.接收区域:反写新增调拨时的接收区域,格式为:省-市;', + '1.4.调拨原因:反写新增调拨时的调拨原因;', + '', + '2.调拨信息:', + '2.1.调拨方式:反写,本用例为第三方运输;', + '2.2.运输负责人:反写调拨信息提交时的运输负责人;', + '2.3.运输方联系方式:反写调拨信息提交时的运输方联系方式;', + '2.4.运输费用:反写,格式为:xx.xx元;', + '2.5.运输合同附件:反写(样例:xxxxx公司运输合同.pdf),未上传显示为无;', + '2.6.接收人员:反写调拨信息提交时的接收人员姓名;', + '', + '3.接收信息:', + '3.1.接收时间:日期选择器,精确至天,格式:YYYY-MM-DD;', + '3.2.停车场:必填项,选择器,直接选择已有停车场;', + '', + '4.调拨车辆清单;', + '#以表格方式展示,反写新增调拨时的整个清单:', + '4.1.品牌:反写新增调拨时的品牌;', + '4.2.型号:反写新增调拨时的型号;', + '4.3.车牌号:反写调拨信息提交时的车牌号;显示该车辆对应停车场;', + '', + '5.底部按钮为提交、保存、取消;', + '5.1.提交:点击提交校验必填项;提交后,调拨车辆清单中车辆停车区域会更新至所选停车场;', + '5.2.保存:不校验必填项,只保存已填数据并保存在列表页;' + ].join('\n'); + + function formatCostYuan(costStr) { + var s = String(costStr == null ? '' : costStr).trim(); + if (!s) return '—'; + return s + '元'; + } + + function contractDisplay(label) { + var s = String(label == null ? '' : label).trim(); + return s ? s : '无'; + } + + function canPreviewAttachment(label) { + var s = String(label == null ? '' : label).trim(); + if (!s) return false; + var lower = s.toLowerCase(); + return lower.endsWith('.pdf') || lower.endsWith('.doc') || lower.endsWith('.docx'); + } + + function runValidate() { + var e = {}; + if (!receiveTime) e.receiveTime = '请选择接收时间'; + if (!receiveParking) e.receiveParking = '请选择停车场'; + setErrors(e); + return { ok: Object.keys(e).length === 0, err: e }; + } + + var handleSubmit = useCallback(function () { + var result = runValidate(); + if (!result.ok) { + message.error('请完善必填项'); + return; + } + Modal.confirm({ + title: '确认提交?', + content: '提交后将进入后续流程(原型)。', + okText: '提交', + cancelText: '取消', + onOk: function () { + message.success('已提交(原型)'); + } + }); + }, [receiveTime, receiveParking]); + + var handleSave = useCallback(function () { + message.success('已保存至列表页(原型:不校验必填项)'); + }, []); + + var handleCancel = useCallback(function () { + Modal.confirm({ + title: '是否确认取消', + content: '未保存的修改将丢失,是否确认?', + okText: '确认', + cancelText: '取消', + onOk: function () { + message.info('返回上一页(原型)'); + } + }); + }, []); + + var vehicleColumns = useMemo(function () { + return [ + { + title: '品牌', + key: 'brand', + width: 120, + render: function (_, r) { + return React.createElement(Input, { value: r.brand || '', readOnly: true, style: readOnlyStyle }); + } + }, + { + title: '型号', + key: 'model', + width: 120, + render: function (_, r) { + return React.createElement(Input, { value: r.model || '', readOnly: true, style: readOnlyStyle }); + } + }, + { + title: '车牌号', + key: 'plateNo', + width: 140, + render: function (_, r) { + return React.createElement(Input, { value: r.plateNo || '', readOnly: true, style: readOnlyStyle }); + } + }, + { + title: '出发停车场', + key: 'departParking', + width: 180, + render: function (_, r) { + return React.createElement(Input, { value: r.departParking || '', readOnly: true, style: readOnlyStyle }); + } + } + ]; + }, []); + + var costText = formatCostYuan(snapshot.transportCost); + var contractText = contractDisplay(snapshot.transportContractFileLabel); + var canPreview = canPreviewAttachment(snapshot.transportContractFileLabel); + + return React.createElement( + App, + null, + React.createElement( + 'div', + { style: layoutStyle }, + React.createElement( + 'div', + { + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: 16, + flexWrap: 'wrap', + gap: 8 + } + }, + React.createElement(Breadcrumb, { + items: [{ title: '运维管理' }, { title: '车辆业务' }, { title: '调拨管理' }, { title: '接收信息' }] + }), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { setRequirementModalOpen(true); } }, '查看需求说明') + ), + + React.createElement( + Modal, + { + title: '需求说明', + open: requirementModalOpen, + onCancel: function () { setRequirementModalOpen(false); }, + width: 760, + footer: React.createElement(Button, { onClick: function () { setRequirementModalOpen(false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, + React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.65, color: 'rgba(0,0,0,0.85)' } }, requirementDocContent) + ), + + React.createElement( + Modal, + { + title: '附件预览', + open: attachmentPreviewOpen, + onCancel: function () { setAttachmentPreviewOpen(false); }, + width: 820, + footer: React.createElement(Button, { onClick: function () { setAttachmentPreviewOpen(false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, + React.createElement( + 'div', + { style: { fontSize: 13, lineHeight: 1.65, color: 'rgba(0,0,0,0.85)' } }, + React.createElement('div', { style: { marginBottom: 8 } }, '文件:', snapshot.transportContractFileLabel || '无'), + React.createElement( + 'div', + { style: { padding: 12, background: 'rgba(0,0,0,0.02)', border: '1px solid #f0f0f0', borderRadius: 8 } }, + '原型预览:此处展示附件内容预览区域。对接后可通过文件 URL 进行在线预览或下载。' + ) + ) + ), + + React.createElement( + Card, + { title: '调拨情况', style: { marginBottom: 16 } }, + React.createElement( + 'div', + { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px 24px', alignItems: 'start' } }, + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '调拨日期'), + React.createElement(Input, { value: snapshot.transferDateStr, readOnly: true, style: readOnlyStyle }) + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '出发区域'), + React.createElement(Input, { value: snapshot.departRegionText, readOnly: true, style: readOnlyStyle }) + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '接收区域'), + React.createElement(Input, { value: snapshot.receiveRegionText, readOnly: true, style: readOnlyStyle }) + ), + React.createElement( + 'div', + { style: Object.assign({}, formItemStyle, { gridColumn: 'span 3' }) }, + React.createElement('div', { style: labelStyle }, '调拨原因'), + React.createElement(Input.TextArea, { value: snapshot.reason, readOnly: true, style: readOnlyStyle, autoSize: { minRows: 3, maxRows: 6 } }) + ) + ) + ), + + React.createElement( + Card, + { title: '调拨信息', style: { marginBottom: 16 } }, + React.createElement( + 'div', + { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px 24px', alignItems: 'start' } }, + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '调拨方式'), + React.createElement(Input, { value: snapshot.method, readOnly: true, style: readOnlyStyle }) + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '运输负责人'), + React.createElement(Input, { value: snapshot.transportLeader, readOnly: true, style: readOnlyStyle }) + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '运输方联系方式'), + React.createElement(Input, { value: snapshot.transportPhone, readOnly: true, style: readOnlyStyle }) + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '运输费用'), + React.createElement(Input, { value: costText, readOnly: true, style: readOnlyStyle }) + ), + React.createElement( + 'div', + { style: Object.assign({}, formItemStyle, { gridColumn: 'span 2' }) }, + React.createElement('div', { style: labelStyle }, '运输合同附件'), + canPreview + ? React.createElement( + 'div', + { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } }, + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { setAttachmentPreviewOpen(true); } }, contractText) + ) + : React.createElement(Input, { value: contractText, readOnly: true, style: readOnlyStyle }) + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '接收人员'), + React.createElement(Input, { value: snapshot.receivePersonName, readOnly: true, style: readOnlyStyle }) + ) + ) + ), + + React.createElement( + Card, + { title: '接收信息', style: { marginBottom: 16 } }, + React.createElement( + 'div', + { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px 24px', alignItems: 'start' } }, + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, reqStar, '接收时间'), + React.createElement(DatePicker, { + style: controlStyle, + format: 'YYYY-MM-DD', + placeholder: '请选择接收时间', + value: receiveTime, + onChange: function (v) { + setReceiveTime(v); + }, + status: errors.receiveTime ? 'error' : undefined + }), + errors.receiveTime ? React.createElement('div', { style: { marginTop: 4, color: '#ff4d4f', fontSize: 12 } }, errors.receiveTime) : null + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, reqStar, '停车场'), + React.createElement(Select, { + placeholder: '请选择停车场', + style: controlStyle, + value: receiveParking, + onChange: function (v) { setReceiveParking(v); }, + allowClear: true, + showSearch: true, + options: parkingOptions, + filterOption: filterOption, + status: errors.receiveParking ? 'error' : undefined + }), + errors.receiveParking ? React.createElement('div', { style: { marginTop: 4, color: '#ff4d4f', fontSize: 12 } }, errors.receiveParking) : null + ) + ) + ), + + React.createElement( + Card, + { title: '调拨车辆清单', style: { marginBottom: 16 } }, + React.createElement(Table, { + rowKey: 'id', + columns: vehicleColumns, + dataSource: vehicles, + size: 'small', + pagination: false + }) + ), + + React.createElement('div', { style: { height: 56 } }), + React.createElement( + 'div', + { + style: { + position: 'fixed', + left: 0, + right: 0, + bottom: 0, + padding: '12px 24px', + background: '#fff', + borderTop: '1px solid #f0f0f0', + display: 'flex', + gap: 8, + zIndex: 10 + } + }, + React.createElement(Button, { type: 'primary', onClick: handleSubmit }, '提交'), + React.createElement(Button, { onClick: handleSave }, '保存'), + React.createElement(Button, { onClick: handleCancel }, '取消') + ) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-ops/pages/11-调拨管理-接收信息-司机运输.jsx b/src/prototypes/oneos-web-ops/pages/11-调拨管理-接收信息-司机运输.jsx new file mode 100644 index 0000000..84044a4 --- /dev/null +++ b/src/prototypes/oneos-web-ops/pages/11-调拨管理-接收信息-司机运输.jsx @@ -0,0 +1,520 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 运维管理 - 车辆业务 - 调拨管理 - 接收信息(数字化资产 ONEOS 运管平台) +// 本页用例:调拨方式为「司机运输」 + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + + var antd = window.antd; + var App = antd.App; + var Breadcrumb = antd.Breadcrumb; + var Button = antd.Button; + var Card = antd.Card; + var DatePicker = antd.DatePicker; + var Input = antd.Input; + var Modal = antd.Modal; + var Select = antd.Select; + var Table = antd.Table; + var message = antd.message; + + var METHOD_DRIVER = '司机运输'; + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + function getMockSnapshot() { + return { + transferDateStr: '2026-03-31 10:30', + departRegionText: '广东省-广州市', + receiveRegionText: '浙江省-嘉兴市', + reason: '华南业务增量,需将车辆调至华东仓储节点保障运力。', + method: METHOD_DRIVER, + transportLeader: '李建国', + transportPhone: '13800138000', + receivePersonName: '王芳', + transferVehicles: [ + { + id: 1, + brand: '小鹏', + model: 'P7', + plateNo: '浙A11111', + departMileageKm: '12010.00', + departHydrogen: '60.00', + h2Unit: '%', + departElectricKwh: '55.20', + departParking: '西湖景区停车场' + }, + { + id: 2, + brand: '蔚来', + model: 'ET5', + plateNo: '浙B22222', + departMileageKm: '15010.00', + departHydrogen: '22.10', + h2Unit: 'MPa', + departElectricKwh: '58.00', + departParking: '宁波江北停车场' + } + ] + }; + } + + var layoutStyle = { padding: '16px 24px 88px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var labelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var formItemStyle = { marginBottom: 12 }; + var controlStyle = { width: '100%' }; + var readOnlyStyle = { background: 'rgba(0,0,0,0.02)' }; + var reqStarStyle = { color: '#ff4d4f', marginRight: 4 }; + var reqStar = React.createElement('span', { style: reqStarStyle }, '*'); + + var snapshot = useMemo(function () { + return getMockSnapshot(); + }, []); + + function buildVehicleRows(snap) { + return (snap.transferVehicles || []).map(function (r) { + return { + id: r.id, + brand: r.brand, + model: r.model, + plateNo: r.plateNo, + departParking: r.departParking || '', + departMileageKm: r.departMileageKm != null ? String(r.departMileageKm) : '', + departHydrogen: r.departHydrogen != null ? String(r.departHydrogen) : '', + h2Unit: r.h2Unit || '%', + departElectricKwh: r.departElectricKwh != null ? String(r.departElectricKwh) : '', + arriveMileageKm: '', + arriveHydrogen: '', + arriveElectricKwh: '' + }; + }); + } + + var receiveTimeState = useState(null); + var receiveTime = receiveTimeState[0]; + var setReceiveTime = receiveTimeState[1]; + + var receiveParkingState = useState(undefined); + var receiveParking = receiveParkingState[0]; + var setReceiveParking = receiveParkingState[1]; + + var parkingOptions = useMemo(function () { + return [ + { value: 'p1', label: '西湖景区停车场' }, + { value: 'p2', label: '宁波江北停车场' }, + { value: 'p3', label: '张江园区停车场' }, + { value: 'p4', label: '天河智慧停车场' } + ]; + }, []); + + var vehiclesState = useState(function () { + return buildVehicleRows(getMockSnapshot()); + }); + var vehicles = vehiclesState[0]; + var setVehicles = vehiclesState[1]; + + var errorsState = useState({}); + var errors = errorsState[0]; + var setErrors = errorsState[1]; + + var requirementModalState = useState(false); + var requirementModalOpen = requirementModalState[0]; + var setRequirementModalOpen = requirementModalState[1]; + + var requirementDocContent = [ + '一个「数字化资产ONEOS运管平台」中的「调拨管理」「接收信息」模块', + '', + '1.调拨情况:', + '1.1.调拨日期:反写新增调拨时的调拨日期,格式为:YYYY-MM-DD HH:mm;', + '1.2.出发区域:反写新增调拨时的出发区域,格式为:省-市;', + '1.3.接收区域:反写新增调拨时的接收区域,格式为:省-市;', + '1.4.调拨原因:反写新增调拨时的调拨原因;', + '', + '2.调拨信息:', + '2.1.调拨方式:反写调拨信息提交时的调拨方式,该用例请列举调拨方式为:司机运输;', + '2.2.运输负责人:反写调拨信息提交时的运输负责人;', + '2.3.运输方联系方式:反写调拨信息提交时的运输方联系方式;', + '2.6.接收人员:反写调拨信息提交时的接收人员姓名;', + '', + '3.接收信息:', + '3.1.接收时间:日期选择器,精确至分钟,格式:YYYY-MM-DD HH:mm;', + '3.2.停车场:必填项,选择器,直接选择已有停车场;', + '', + '4.调拨车辆清单;', + '#以表格方式展示,反写新增调拨时的整个清单:', + '4.1.品牌:反写新增调拨时的品牌;', + '4.2.型号:反写新增调拨时的型号;', + '4.3.车牌号:反写调拨信息提交时的车牌号;', + '4.4.出发时里程:反写调拨信息提交时的里程数,只有调拨方式为司机运输时显示;', + '4.5.到达时里程:必填项,输入框,支持2位小数,后缀为km,只有调拨方式为司机运输时显示;', + '4.6.出发时氢量:反写调拨信息提交时的氢量,只有调拨方式为司机运输时显示;', + '4.7.到达时氢量:必填项,输入框,支持2位小数,后缀为%或MPa(取自对应车辆该型号对应型号参数下仪表盘单位),只有调拨方式为司机运输时显示;', + '4.8.出发时电量:反写调拨信息提交时的电量,只有调拨方式为司机运输时显示;', + '4.9.到达时电量:必填项,输入框,支持2位小数,后缀为kWh,只有调拨方式为司机运输时显示;', + '', + '5.底部按钮为提交、保存、取消;', + '5.1.提交:点击提交审核校验必填项,提交成功后将调拨车辆最终停车区域同步到车辆列表停车场中;', + '5.2.保存:不校验必填项,只保存已填数据并保存在列表页;' + ].join('\n'); + + function toFixed2Input(v) { + var s = String(v === null || v === undefined ? '' : v); + s = s.replace(/[^\d.]/g, ''); + var firstDot = s.indexOf('.'); + if (firstDot >= 0) { + s = s.slice(0, firstDot + 1) + s.slice(firstDot + 1).replace(/\./g, ''); + } + if (firstDot >= 0) { + var a = s.split('.'); + s = a[0] + '.' + (a[1] || '').slice(0, 2); + } + return s; + } + + var updateRow = useCallback(function (index, patch) { + setVehicles(function (prev) { + var list = prev.slice(); + var cur = list[index] || {}; + list[index] = Object.assign({}, cur, patch); + return list; + }); + }, []); + + function runValidate() { + var e = {}; + if (!receiveTime) e.receiveTime = '请选择接收时间'; + if (!receiveParking) e.receiveParking = '请选择停车场'; + for (var i = 0; i < (vehicles || []).length; i++) { + var r = vehicles[i] || {}; + if (!String(r.arriveMileageKm || '').trim()) e['row_' + i + '_arrMile'] = '请输入到达时里程'; + if (!String(r.arriveHydrogen || '').trim()) e['row_' + i + '_arrH2'] = '请输入到达时氢量'; + if (!String(r.arriveElectricKwh || '').trim()) e['row_' + i + '_arrKwh'] = '请输入到达时电量'; + } + setErrors(e); + return { ok: Object.keys(e).length === 0, err: e }; + } + + var handleSubmit = useCallback(function () { + var result = runValidate(); + if (!result.ok) { + message.error('请完善必填项'); + return; + } + Modal.confirm({ + title: '确认提交?', + content: '提交后将进入后续流程(原型)。', + okText: '提交', + cancelText: '取消', + onOk: function () { + message.success('已提交(原型)'); + } + }); + }, [receiveTime, receiveParking, vehicles]); + + var handleSave = useCallback(function () { + message.success('已保存至列表页(原型:不校验必填项)'); + }, []); + + var handleCancel = useCallback(function () { + Modal.confirm({ + title: '是否确认取消', + content: '未保存的修改将丢失,是否确认?', + okText: '确认', + cancelText: '取消', + onOk: function () { + message.info('返回上一页(原型)'); + } + }); + }, []); + + var vehicleColumns = useMemo(function () { + return [ + { + title: '品牌', + key: 'brand', + width: 120, + render: function (_, r) { + return React.createElement(Input, { value: r.brand || '', readOnly: true, style: readOnlyStyle }); + } + }, + { + title: '型号', + key: 'model', + width: 120, + render: function (_, r) { + return React.createElement(Input, { value: r.model || '', readOnly: true, style: readOnlyStyle }); + } + }, + { + title: '车牌号', + key: 'plateNo', + width: 140, + render: function (_, r) { + return React.createElement(Input, { value: r.plateNo || '', readOnly: true, style: readOnlyStyle }); + } + }, + { + title: '出发停车场', + key: 'departParking', + width: 180, + render: function (_, r) { + return React.createElement(Input, { value: r.departParking || '', readOnly: true, style: readOnlyStyle }); + } + }, + { + title: '出发时里程', + key: 'departMileageKm', + width: 130, + render: function (_, r) { + return React.createElement(Input, { value: r.departMileageKm || '', disabled: true, addonAfter: 'km' }); + } + }, + { + title: React.createElement('span', null, reqStar, '到达时里程'), + key: 'arriveMileageKm', + width: 140, + render: function (_, r, index) { + var k = 'row_' + index + '_arrMile'; + return React.createElement(Input, { + value: r.arriveMileageKm, + onChange: function (e) { updateRow(index, { arriveMileageKm: toFixed2Input(e.target.value) }); }, + placeholder: '0.00', + addonAfter: 'km', + status: errors[k] ? 'error' : undefined + }); + } + }, + { + title: '出发时氢量', + key: 'departHydrogen', + width: 130, + render: function (_, r) { + return React.createElement(Input, { value: r.departHydrogen || '', disabled: true, addonAfter: r.h2Unit || '-' }); + } + }, + { + title: React.createElement('span', null, reqStar, '到达时氢量'), + key: 'arriveHydrogen', + width: 140, + render: function (_, r, index) { + var k = 'row_' + index + '_arrH2'; + return React.createElement(Input, { + value: r.arriveHydrogen, + onChange: function (e) { updateRow(index, { arriveHydrogen: toFixed2Input(e.target.value) }); }, + placeholder: '0.00', + addonAfter: r.h2Unit || '%', + status: errors[k] ? 'error' : undefined + }); + } + }, + { + title: '出发时电量', + key: 'departElectricKwh', + width: 130, + render: function (_, r) { + return React.createElement(Input, { value: r.departElectricKwh || '', disabled: true, addonAfter: 'kWh' }); + } + }, + { + title: React.createElement('span', null, reqStar, '到达时电量'), + key: 'arriveElectricKwh', + width: 140, + render: function (_, r, index) { + var k = 'row_' + index + '_arrKwh'; + return React.createElement(Input, { + value: r.arriveElectricKwh, + onChange: function (e) { updateRow(index, { arriveElectricKwh: toFixed2Input(e.target.value) }); }, + placeholder: '0.00', + addonAfter: 'kWh', + status: errors[k] ? 'error' : undefined + }); + } + } + ]; + }, [errors, updateRow]); + + return React.createElement( + App, + null, + React.createElement( + 'div', + { style: layoutStyle }, + React.createElement( + 'div', + { + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: 16, + flexWrap: 'wrap', + gap: 8 + } + }, + React.createElement(Breadcrumb, { items: [{ title: '运维管理' }, { title: '车辆业务' }, { title: '调拨管理' }, { title: '接收信息' }] }), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { setRequirementModalOpen(true); } }, '查看需求说明') + ), + + React.createElement( + Modal, + { + title: '需求说明', + open: requirementModalOpen, + onCancel: function () { setRequirementModalOpen(false); }, + width: 760, + footer: React.createElement(Button, { onClick: function () { setRequirementModalOpen(false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, + React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.65, color: 'rgba(0,0,0,0.85)' } }, requirementDocContent) + ), + + React.createElement( + Card, + { title: '调拨情况', style: { marginBottom: 16 } }, + React.createElement( + 'div', + { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px 24px', alignItems: 'start' } }, + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '调拨日期'), + React.createElement(Input, { value: snapshot.transferDateStr, readOnly: true, style: readOnlyStyle }) + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '出发区域'), + React.createElement(Input, { value: snapshot.departRegionText, readOnly: true, style: readOnlyStyle }) + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '接收区域'), + React.createElement(Input, { value: snapshot.receiveRegionText, readOnly: true, style: readOnlyStyle }) + ), + React.createElement( + 'div', + { style: Object.assign({}, formItemStyle, { gridColumn: 'span 3' }) }, + React.createElement('div', { style: labelStyle }, '调拨原因'), + React.createElement(Input.TextArea, { value: snapshot.reason, readOnly: true, style: readOnlyStyle, autoSize: { minRows: 3, maxRows: 6 } }) + ) + ) + ), + + React.createElement( + Card, + { title: '调拨信息', style: { marginBottom: 16 } }, + React.createElement( + 'div', + { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px 24px', alignItems: 'start' } }, + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '调拨方式'), + React.createElement(Input, { value: snapshot.method, readOnly: true, style: readOnlyStyle }) + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '运输负责人'), + React.createElement(Input, { value: snapshot.transportLeader, readOnly: true, style: readOnlyStyle }) + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '运输方联系方式'), + React.createElement(Input, { value: snapshot.transportPhone, readOnly: true, style: readOnlyStyle }) + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '接收人员'), + React.createElement(Input, { value: snapshot.receivePersonName, readOnly: true, style: readOnlyStyle }) + ) + ) + ), + + React.createElement( + Card, + { title: '接收信息', style: { marginBottom: 16 } }, + React.createElement( + 'div', + { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px 24px', alignItems: 'start' } }, + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, reqStar, '接收时间'), + React.createElement(DatePicker, { + style: controlStyle, + format: 'YYYY-MM-DD HH:mm', + showTime: { format: 'HH:mm' }, + placeholder: '请选择接收时间', + value: receiveTime, + onChange: function (v) { setReceiveTime(v); }, + status: errors.receiveTime ? 'error' : undefined + }), + errors.receiveTime ? React.createElement('div', { style: { marginTop: 4, color: '#ff4d4f', fontSize: 12 } }, errors.receiveTime) : null + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, reqStar, '停车场'), + React.createElement(Select, { + placeholder: '请选择停车场', + style: controlStyle, + value: receiveParking, + onChange: function (v) { setReceiveParking(v); }, + allowClear: true, + showSearch: true, + options: parkingOptions, + filterOption: filterOption, + status: errors.receiveParking ? 'error' : undefined + }), + errors.receiveParking ? React.createElement('div', { style: { marginTop: 4, color: '#ff4d4f', fontSize: 12 } }, errors.receiveParking) : null + ) + ) + ), + + React.createElement( + Card, + { title: '调拨车辆清单', style: { marginBottom: 16 } }, + React.createElement(Table, { + rowKey: 'id', + columns: vehicleColumns, + dataSource: vehicles, + size: 'small', + pagination: false, + scroll: { x: 'max-content' } + }) + ), + + React.createElement('div', { style: { height: 56 } }), + React.createElement( + 'div', + { + style: { + position: 'fixed', + left: 0, + right: 0, + bottom: 0, + padding: '12px 24px', + background: '#fff', + borderTop: '1px solid #f0f0f0', + display: 'flex', + gap: 8, + zIndex: 10 + } + }, + React.createElement(Button, { type: 'primary', onClick: handleSubmit }, '提交'), + React.createElement(Button, { onClick: handleSave }, '保存'), + React.createElement(Button, { onClick: handleCancel }, '取消') + ) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-ops/pages/12-调拨管理-新增.jsx b/src/prototypes/oneos-web-ops/pages/12-调拨管理-新增.jsx new file mode 100644 index 0000000..e91d0f3 --- /dev/null +++ b/src/prototypes/oneos-web-ops/pages/12-调拨管理-新增.jsx @@ -0,0 +1,492 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 运维管理 - 车辆业务 - 调拨管理 - 新增(数字化资产 ONEOS 运管平台) + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + + var antd = window.antd; + var App = antd.App; + var Breadcrumb = antd.Breadcrumb; + var Button = antd.Button; + var Card = antd.Card; + var Cascader = antd.Cascader; + var DatePicker = antd.DatePicker; + var Input = antd.Input; + var Modal = antd.Modal; + var Select = antd.Select; + var Table = antd.Table; + var message = antd.message; + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + var layoutStyle = { padding: '16px 24px 88px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var labelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var formItemStyle = { marginBottom: 12 }; + var controlStyle = { width: '100%' }; + var reqStarStyle = { color: '#ff4d4f', marginRight: 4 }; + var reqStar = React.createElement('span', { style: reqStarStyle }, '*'); + + /** 车牌可选车辆运营状态(需求:仅「可运营」) */ + var PLATE_ELIGIBLE_STATUS = '可运营'; + + var regionOptions = useMemo(function () { + return [ + { value: 'zhejiang', label: '浙江省', children: [{ value: 'hangzhou', label: '杭州市' }, { value: 'ningbo', label: '宁波市' }, { value: 'jiaxing', label: '嘉兴市' }] }, + { value: 'shanghai', label: '上海市', children: [{ value: 'shanghai', label: '上海市' }] }, + { value: 'guangdong', label: '广东省', children: [{ value: 'guangzhou', label: '广州市' }, { value: 'shenzhen', label: '深圳市' }, { value: 'dongguan', label: '东莞市' }] } + ]; + }, []); + + var brandOptions = useMemo(function () { + return ['东风', '比亚迪', '小鹏', '蔚来', '福田', '飞驰', '宇通'].map(function (b) { + return { value: b, label: b }; + }); + }, []); + + var modelOptionsByBrand = useMemo(function () { + return { + 东风: ['DFH1180', 'DFH5160'], + 比亚迪: ['汉', '唐'], + 小鹏: ['P7', 'G9'], + 蔚来: ['ET5', 'ES8'], + 福田: ['BJ1180'], + 飞驰: ['FSQ4250'], + 宇通: ['ZKH4250'] + }; + }, []); + + /** 车辆主数据(原型):仅运营状态为「可运营」的车牌出现在车牌下拉里 */ + var vehicleDb = useMemo(function () { + return [ + { plateNo: '粤A12345', status: '可运营', brand: '东风', model: 'DFH1180' }, + { plateNo: '粤B11111', status: '可运营', brand: '比亚迪', model: '汉' }, + { plateNo: '浙A11111', status: '可运营', brand: '小鹏', model: 'P7' }, + { plateNo: '浙B22222', status: '可运营', brand: '蔚来', model: 'ET5' }, + { plateNo: '沪A30003', status: '可运营', brand: '福田', model: 'BJ1180' }, + { plateNo: '粤C99999', status: '维修中', brand: '东风', model: 'DFH1180' } + ]; + }, []); + + var plateOptionsEligible = useMemo(function () { + return vehicleDb + .filter(function (v) { + return v.status === PLATE_ELIGIBLE_STATUS; + }) + .map(function (v) { + return { value: v.plateNo, label: v.plateNo }; + }); + }, [vehicleDb]); + + var formState = useState({ + transferDate: null, + departRegion: undefined, + receiveRegion: undefined, + reason: '' + }); + var form = formState[0]; + var setForm = formState[1]; + + var errorsState = useState({}); + var errors = errorsState[0]; + var setErrors = errorsState[1]; + + var requirementModalState = useState(false); + var requirementModalOpen = requirementModalState[0]; + var setRequirementModalOpen = requirementModalState[1]; + + var requirementDocContent = [ + '一个「数字化资产ONEOS运管平台」中的「调拨管理」「新增」模块', + '', + '1.调拨情况:', + '1.1.调拨日期:必填项,日期选择器,精确至分钟,格式为:YYYY-MM-DD HH:mm;', + '1.2.出发区域:必填项,地区选择器,支持省-市2级;', + '1.3.接收区域:必填项,地区选择器,支持省-市2级;', + '1.4.调拨原因:必填项,文本域,支持用户自定义输入;', + '', + '2.调拨车辆清单;', + '#以表格方式展示:', + '2.1.品牌:必填,选择器;', + '2.2.型号:必填,选择器;', + '2.3.车牌号:选择器,只能选择运营状态为:可运营的车辆;', + '2.4.操作:删除;', + '2.5.表格下方为新增一行,铺满整行;', + '需要至少提交一辆车才能完成调拨,否则提交审核时提示:请选择调拨车辆;', + '', + '3.底部按钮为提交审核、保存、取消;', + '3.1.提交审核:点击提交审核校验必填项', + '3.2.保存:不校验必填项,只保存已填数据并保存在列表页;' + ].join('\n'); + + var rowIdRef = React.useRef(2); + var vehiclesState = useState([ + { + id: 1, + brand: undefined, + model: undefined, + plateNo: undefined + } + ]); + var vehicles = vehiclesState[0]; + var setVehicles = vehiclesState[1]; + + var updateForm = useCallback(function (patch) { + setForm(function (p) { + return Object.assign({}, p, patch); + }); + }, []); + + var updateRow = useCallback(function (index, patch) { + setVehicles(function (prev) { + var list = prev.slice(); + var cur = list[index] || {}; + list[index] = Object.assign({}, cur, patch); + return list; + }); + }, []); + + var handlePlateChange = useCallback( + function (index, plateNo) { + var v = vehicleDb.find(function (x) { + return x.plateNo === plateNo; + }); + if (v && v.status === PLATE_ELIGIBLE_STATUS) { + updateRow(index, { + plateNo: plateNo, + brand: v.brand, + model: v.model + }); + } else { + updateRow(index, { plateNo: plateNo }); + } + }, + [vehicleDb, updateRow] + ); + + var addRow = useCallback(function () { + setVehicles(function (prev) { + return prev.concat([ + { + id: rowIdRef.current++, + brand: undefined, + model: undefined, + plateNo: undefined + } + ]); + }); + }, []); + + var removeRow = useCallback(function (index) { + setVehicles(function (prev) { + var list = prev.slice(); + list.splice(index, 1); + if (list.length === 0) { + return [ + { + id: rowIdRef.current++, + brand: undefined, + model: undefined, + plateNo: undefined + } + ]; + } + return list; + }); + }, []); + + function hasValidVehicle() { + return (vehicles || []).some(function (r) { + return r && r.brand && r.model; + }); + } + + function runValidate() { + var e = {}; + if (!form.transferDate) e.transferDate = '请选择调拨日期'; + if (!form.departRegion || form.departRegion.length < 2) e.departRegion = '请选择出发区域'; + if (!form.receiveRegion || form.receiveRegion.length < 2) e.receiveRegion = '请选择接收区域'; + if (!String(form.reason || '').trim()) e.reason = '请输入调拨原因'; + if (!hasValidVehicle()) e.vehicleList = '请选择调拨车辆'; + + for (var i = 0; i < (vehicles || []).length; i++) { + var r = vehicles[i] || {}; + if (!r.brand && !r.model && !r.plateNo) continue; + if (!r.brand) e['row_' + i + '_brand'] = '请选择品牌'; + if (!r.model) e['row_' + i + '_model'] = '请选择型号'; + } + setErrors(e); + return { ok: Object.keys(e).length === 0, err: e }; + } + + var handleSubmitAudit = useCallback(function () { + var result = runValidate(); + if (!result.ok) { + if (result.err && result.err.vehicleList) message.error(result.err.vehicleList); + else message.error('请完善必填项'); + return; + } + Modal.confirm({ + title: '确认提交审核?', + content: '提交后将进入审批流程(原型)。', + okText: '提交审核', + cancelText: '取消', + onOk: function () { + message.success('已提交审核(原型)'); + } + }); + }, [form, vehicles]); + + var handleSave = useCallback(function () { + message.success('已保存至列表页(原型:不校验必填项,仅保存当前已填数据)'); + }, []); + + var handleCancel = useCallback(function () { + Modal.confirm({ + title: '是否确认取消', + content: '取消将会丢失所有已填写内容,是否确认?', + okText: '确认', + cancelText: '取消', + onOk: function () { + message.info('返回调拨管理页(原型)'); + } + }); + }, []); + + var vehicleColumns = useMemo( + function () { + return [ + { + title: React.createElement('span', null, reqStar, '品牌'), + key: 'brand', + width: 160, + render: function (_, r, index) { + return React.createElement(Select, { + placeholder: '请选择品牌', + style: { width: '100%' }, + value: r.brand, + onChange: function (v) { + updateRow(index, { brand: v, model: undefined, plateNo: undefined }); + }, + allowClear: true, + showSearch: true, + options: brandOptions, + filterOption: filterOption, + status: errors['row_' + index + '_brand'] || errors.vehicleList ? 'error' : undefined + }); + } + }, + { + title: React.createElement('span', null, reqStar, '型号'), + key: 'model', + width: 180, + render: function (_, r, index) { + var ms = (r.brand && modelOptionsByBrand[r.brand]) || []; + var opts = ms.map(function (m) { + return { value: m, label: m }; + }); + return React.createElement(Select, { + placeholder: r.brand ? '请选择型号' : '请先选择品牌', + style: { width: '100%' }, + value: r.model, + disabled: !r.brand, + onChange: function (v) { + updateRow(index, { model: v, plateNo: undefined }); + }, + allowClear: true, + showSearch: true, + options: opts, + filterOption: filterOption, + status: errors['row_' + index + '_model'] || errors.vehicleList ? 'error' : undefined + }); + } + }, + { + title: '车牌号', + key: 'plateNo', + width: 240, + render: function (_, r, index) { + return React.createElement(Select, { + placeholder: '请选择车牌号', + style: { width: '100%' }, + value: r.plateNo, + onChange: function (v) { + handlePlateChange(index, v); + }, + allowClear: true, + showSearch: true, + options: plateOptionsEligible, + filterOption: filterOption + }); + } + }, + { + title: '操作', + key: 'action', + width: 80, + fixed: 'right', + render: function (_, __, index) { + return React.createElement(Button, { type: 'link', danger: true, size: 'small', onClick: function () { removeRow(index); } }, '删除'); + } + } + ]; + }, + [brandOptions, modelOptionsByBrand, plateOptionsEligible, errors, updateRow, handlePlateChange, removeRow] + ); + + return React.createElement( + App, + null, + React.createElement( + 'div', + { style: layoutStyle }, + React.createElement( + 'div', + { + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: 16, + flexWrap: 'wrap', + gap: 8 + } + }, + React.createElement(Breadcrumb, { + items: [{ title: '运维管理' }, { title: '车辆业务' }, { title: '调拨管理' }, { title: '新增' }] + }), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { setRequirementModalOpen(true); } }, '查看需求说明') + ), + + React.createElement( + Modal, + { + title: '需求说明', + open: requirementModalOpen, + onCancel: function () { setRequirementModalOpen(false); }, + width: 720, + footer: React.createElement(Button, { onClick: function () { setRequirementModalOpen(false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, + React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.65, color: 'rgba(0,0,0,0.85)' } }, requirementDocContent) + ), + + React.createElement( + Card, + { title: '调拨情况', style: { marginBottom: 16 } }, + React.createElement( + 'div', + { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px 24px', alignItems: 'start' } }, + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, reqStar, '调拨日期'), + React.createElement(DatePicker, { + style: controlStyle, + format: 'YYYY-MM-DD HH:mm', + showTime: { format: 'HH:mm' }, + placeholder: '请选择调拨日期', + value: form.transferDate, + onChange: function (v) { + updateForm({ transferDate: v }); + }, + status: errors.transferDate ? 'error' : undefined + }), + errors.transferDate ? React.createElement('div', { style: { marginTop: 4, color: '#ff4d4f', fontSize: 12 } }, errors.transferDate) : null + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, reqStar, '出发区域'), + React.createElement(Cascader, { + style: controlStyle, + placeholder: '请选择省 / 市', + options: regionOptions, + value: form.departRegion, + onChange: function (v) { + updateForm({ departRegion: v }); + }, + allowClear: true, + status: errors.departRegion ? 'error' : undefined + }), + errors.departRegion ? React.createElement('div', { style: { marginTop: 4, color: '#ff4d4f', fontSize: 12 } }, errors.departRegion) : null + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, reqStar, '接收区域'), + React.createElement(Cascader, { + style: controlStyle, + placeholder: '请选择省 / 市', + options: regionOptions, + value: form.receiveRegion, + onChange: function (v) { + updateForm({ receiveRegion: v }); + }, + allowClear: true, + status: errors.receiveRegion ? 'error' : undefined + }), + errors.receiveRegion ? React.createElement('div', { style: { marginTop: 4, color: '#ff4d4f', fontSize: 12 } }, errors.receiveRegion) : null + ), + React.createElement( + 'div', + { style: Object.assign({}, formItemStyle, { gridColumn: 'span 3' }) }, + React.createElement('div', { style: labelStyle }, reqStar, '调拨原因'), + React.createElement(Input.TextArea, { + placeholder: '请输入调拨原因', + value: form.reason, + onChange: function (e) { + updateForm({ reason: e.target.value }); + }, + autoSize: { minRows: 3, maxRows: 6 }, + status: errors.reason ? 'error' : undefined + }), + errors.reason ? React.createElement('div', { style: { marginTop: 4, color: '#ff4d4f', fontSize: 12 } }, errors.reason) : null + ) + ) + ), + + React.createElement( + Card, + { title: '调拨车辆清单', style: { marginBottom: 16 } }, + errors.vehicleList ? React.createElement('div', { style: { marginBottom: 12, color: '#ff4d4f', fontSize: 12 } }, errors.vehicleList) : null, + React.createElement(Table, { + rowKey: 'id', + columns: vehicleColumns, + dataSource: vehicles, + size: 'small', + pagination: false, + scroll: { x: 720 } + }), + React.createElement(Button, { type: 'dashed', style: { marginTop: 12, width: '100%' }, onClick: addRow }, '新增一行') + ), + + React.createElement('div', { style: { height: 56 } }), + React.createElement( + 'div', + { + style: { + position: 'fixed', + left: 0, + right: 0, + bottom: 0, + padding: '12px 24px', + background: '#fff', + borderTop: '1px solid #f0f0f0', + display: 'flex', + gap: 8, + zIndex: 10 + } + }, + React.createElement(Button, { type: 'primary', onClick: handleSubmitAudit }, '提交审核'), + React.createElement(Button, { onClick: handleSave }, '保存'), + React.createElement(Button, { onClick: handleCancel }, '取消') + ) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-ops/pages/13-调拨管理-业务提交-编辑.jsx b/src/prototypes/oneos-web-ops/pages/13-调拨管理-业务提交-编辑.jsx new file mode 100644 index 0000000..07017f2 --- /dev/null +++ b/src/prototypes/oneos-web-ops/pages/13-调拨管理-业务提交-编辑.jsx @@ -0,0 +1,466 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 运维管理 - 车辆业务 - 调拨管理 - 编辑(基于新增页生成,反写已保存数据) + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + + var antd = window.antd; + var App = antd.App; + var Breadcrumb = antd.Breadcrumb; + var Button = antd.Button; + var Card = antd.Card; + var Cascader = antd.Cascader; + var DatePicker = antd.DatePicker; + var Input = antd.Input; + var Modal = antd.Modal; + var Select = antd.Select; + var Table = antd.Table; + var message = antd.message; + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + function parseDateTime(s) { + var str = String(s || '').trim(); + if (!str) return null; + try { + if (window.dayjs) return window.dayjs(str); + } catch (e1) {} + try { + if (window.moment) return window.moment(str, ['YYYY-MM-DD HH:mm', 'YYYY-MM-DD HH:mm:ss', 'YYYY-MM-DD'], true); + } catch (e2) {} + return null; + } + + var layoutStyle = { padding: '16px 24px 88px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var labelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var formItemStyle = { marginBottom: 12 }; + var controlStyle = { width: '100%' }; + var reqStarStyle = { color: '#ff4d4f', marginRight: 4 }; + var reqStar = React.createElement('span', { style: reqStarStyle }, '*'); + + /** 车牌可选车辆运营状态(需求:仅「可运营」) */ + var PLATE_ELIGIBLE_STATUS = '可运营'; + + var regionOptions = useMemo(function () { + return [ + { value: 'zhejiang', label: '浙江省', children: [{ value: 'hangzhou', label: '杭州市' }, { value: 'ningbo', label: '宁波市' }, { value: 'jiaxing', label: '嘉兴市' }] }, + { value: 'shanghai', label: '上海市', children: [{ value: 'shanghai', label: '上海市' }] }, + { value: 'guangdong', label: '广东省', children: [{ value: 'guangzhou', label: '广州市' }, { value: 'shenzhen', label: '深圳市' }, { value: 'dongguan', label: '东莞市' }] } + ]; + }, []); + + var brandOptions = useMemo(function () { + return ['东风', '比亚迪', '小鹏', '蔚来', '福田', '飞驰', '宇通'].map(function (b) { + return { value: b, label: b }; + }); + }, []); + + var modelOptionsByBrand = useMemo(function () { + return { + 东风: ['DFH1180', 'DFH5160'], + 比亚迪: ['汉', '唐'], + 小鹏: ['P7', 'G9'], + 蔚来: ['ET5', 'ES8'], + 福田: ['BJ1180'], + 飞驰: ['FSQ4250'], + 宇通: ['ZKH4250'] + }; + }, []); + + /** 车辆主数据(原型):仅运营状态为「可运营」的车牌出现在车牌下拉里 */ + var vehicleDb = useMemo(function () { + return [ + { plateNo: '粤A12345', status: '可运营', brand: '东风', model: 'DFH1180' }, + { plateNo: '粤B11111', status: '可运营', brand: '比亚迪', model: '汉' }, + { plateNo: '浙A11111', status: '可运营', brand: '小鹏', model: 'P7' }, + { plateNo: '浙B22222', status: '可运营', brand: '蔚来', model: 'ET5' }, + { plateNo: '沪A30003', status: '可运营', brand: '福田', model: 'BJ1180' }, + { plateNo: '粤C99999', status: '维修中', brand: '东风', model: 'DFH1180' } + ]; + }, []); + + var plateOptionsEligible = useMemo(function () { + return vehicleDb + .filter(function (v) { return v.status === PLATE_ELIGIBLE_STATUS; }) + .map(function (v) { return { value: v.plateNo, label: v.plateNo }; }); + }, [vehicleDb]); + + // 编辑页:从服务端加载已保存数据(原型反写) + var initialSnapshot = useMemo(function () { + return { + transferId: 'TP202604010001', + savedAt: '2026-04-01 11:20', + form: { + transferDate: parseDateTime('2026-04-01 10:30'), + departRegion: ['guangdong', 'guangzhou'], + receiveRegion: ['zhejiang', 'jiaxing'], + reason: '华南业务增量,需将车辆调至华东仓储节点保障运力。' + }, + vehicles: [ + { id: 1, brand: '小鹏', model: 'P7', plateNo: '浙A11111' }, + { id: 2, brand: '蔚来', model: 'ET5', plateNo: '浙B22222' } + ] + }; + }, []); + + var formState = useState(initialSnapshot.form); + var form = formState[0]; + var setForm = formState[1]; + + var rowIdRef = React.useRef(3); + var vehiclesState = useState(initialSnapshot.vehicles); + var vehicles = vehiclesState[0]; + var setVehicles = vehiclesState[1]; + + var savedAtState = useState(initialSnapshot.savedAt); + var savedAt = savedAtState[0]; + var setSavedAt = savedAtState[1]; + + var errorsState = useState({}); + var errors = errorsState[0]; + var setErrors = errorsState[1]; + + var requirementModalState = useState(false); + var requirementModalOpen = requirementModalState[0]; + var setRequirementModalOpen = requirementModalState[1]; + + var requirementDocContent = [ + '一个「数字化资产ONEOS运管平台」中的「调拨管理」「编辑」模块', + '#面包屑:运维管理-车辆业务-调拨管理-编辑', + '页面基于「调拨管理-新增」生成:字段/校验/车辆清单一致。', + '进入页面时反写已保存数据;点击保存后更新“已保存(模拟)”状态。' + ].join('\n'); + + var updateForm = useCallback(function (patch) { + setForm(function (p) { return Object.assign({}, p, patch); }); + }, []); + + var updateRow = useCallback(function (index, patch) { + setVehicles(function (prev) { + var list = prev.slice(); + var cur = list[index] || {}; + list[index] = Object.assign({}, cur, patch); + return list; + }); + }, []); + + var handlePlateChange = useCallback(function (index, plateNo) { + var v = vehicleDb.find(function (x) { return x.plateNo === plateNo; }); + if (v && v.status === PLATE_ELIGIBLE_STATUS) { + updateRow(index, { plateNo: plateNo, brand: v.brand, model: v.model }); + } else { + updateRow(index, { plateNo: plateNo }); + } + }, [vehicleDb, updateRow]); + + var addRow = useCallback(function () { + setVehicles(function (prev) { + return prev.concat([{ id: rowIdRef.current++, brand: undefined, model: undefined, plateNo: undefined }]); + }); + }, []); + + var removeRow = useCallback(function (index) { + setVehicles(function (prev) { + var list = prev.slice(); + list.splice(index, 1); + if (list.length === 0) { + return [{ id: rowIdRef.current++, brand: undefined, model: undefined, plateNo: undefined }]; + } + return list; + }); + }, []); + + function hasValidVehicle() { + return (vehicles || []).some(function (r) { return r && r.brand && r.model; }); + } + + function runValidate() { + var e = {}; + if (!form.transferDate) e.transferDate = '请选择调拨日期'; + if (!form.departRegion || form.departRegion.length < 2) e.departRegion = '请选择出发区域'; + if (!form.receiveRegion || form.receiveRegion.length < 2) e.receiveRegion = '请选择接收区域'; + if (!String(form.reason || '').trim()) e.reason = '请输入调拨原因'; + if (!hasValidVehicle()) e.vehicleList = '请选择调拨车辆'; + + for (var i = 0; i < (vehicles || []).length; i++) { + var r = vehicles[i] || {}; + if (!r.brand && !r.model && !r.plateNo) continue; + if (!r.brand) e['row_' + i + '_brand'] = '请选择品牌'; + if (!r.model) e['row_' + i + '_model'] = '请选择型号'; + } + setErrors(e); + return { ok: Object.keys(e).length === 0, err: e }; + } + + var handleSubmitAudit = useCallback(function () { + var result = runValidate(); + if (!result.ok) { + if (result.err && result.err.vehicleList) message.error(result.err.vehicleList); + else message.error('请完善必填项'); + return; + } + Modal.confirm({ + title: '确认提交审核?', + content: '提交后将进入审批流程(原型)。', + okText: '提交审核', + cancelText: '取消', + onOk: function () { + message.success('已提交审核(原型)'); + } + }); + }, [form, vehicles]); + + var handleSave = useCallback(function () { + var nowStr = ''; + try { + if (window.dayjs) nowStr = window.dayjs().format('YYYY-MM-DD HH:mm'); + } catch (e1) {} + if (!nowStr) { + try { + if (window.moment) nowStr = window.moment().format('YYYY-MM-DD HH:mm'); + } catch (e2) {} + } + if (!nowStr) nowStr = '刚刚'; + setSavedAt(nowStr); + message.success('已保存(原型:不校验必填项,仅保存当前已填数据)'); + }, []); + + var handleCancel = useCallback(function () { + Modal.confirm({ + title: '是否确认取消', + content: '取消将返回列表,未保存的修改将丢失(原型)。是否确认?', + okText: '确认', + cancelText: '取消', + onOk: function () { + message.info('返回调拨管理页(原型)'); + } + }); + }, []); + + var vehicleColumns = useMemo(function () { + return [ + { + title: React.createElement('span', null, reqStar, '品牌'), + key: 'brand', + width: 160, + render: function (_, r, index) { + return React.createElement(Select, { + placeholder: '请选择品牌', + style: { width: '100%' }, + value: r.brand, + onChange: function (v) { updateRow(index, { brand: v, model: undefined, plateNo: undefined }); }, + allowClear: true, + showSearch: true, + options: brandOptions, + filterOption: filterOption, + status: errors['row_' + index + '_brand'] || errors.vehicleList ? 'error' : undefined + }); + } + }, + { + title: React.createElement('span', null, reqStar, '型号'), + key: 'model', + width: 180, + render: function (_, r, index) { + var ms = (r.brand && modelOptionsByBrand[r.brand]) || []; + var opts = ms.map(function (m) { return { value: m, label: m }; }); + return React.createElement(Select, { + placeholder: r.brand ? '请选择型号' : '请先选择品牌', + style: { width: '100%' }, + value: r.model, + disabled: !r.brand, + onChange: function (v) { updateRow(index, { model: v, plateNo: undefined }); }, + allowClear: true, + showSearch: true, + options: opts, + filterOption: filterOption, + status: errors['row_' + index + '_model'] || errors.vehicleList ? 'error' : undefined + }); + } + }, + { + title: '车牌号', + key: 'plateNo', + width: 240, + render: function (_, r, index) { + return React.createElement(Select, { + placeholder: '请选择车牌号', + style: { width: '100%' }, + value: r.plateNo, + onChange: function (v) { handlePlateChange(index, v); }, + allowClear: true, + showSearch: true, + options: plateOptionsEligible, + filterOption: filterOption + }); + } + }, + { + title: '操作', + key: 'action', + width: 80, + fixed: 'right', + render: function (_, __, index) { + return React.createElement(Button, { type: 'link', danger: true, size: 'small', onClick: function () { removeRow(index); } }, '删除'); + } + } + ]; + }, [brandOptions, modelOptionsByBrand, plateOptionsEligible, errors, updateRow, handlePlateChange, removeRow]); + + return React.createElement( + App, + null, + React.createElement( + 'div', + { style: layoutStyle }, + React.createElement( + 'div', + { + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: 8, + flexWrap: 'wrap', + gap: 8 + } + }, + React.createElement(Breadcrumb, { items: [{ title: '运维管理' }, { title: '车辆业务' }, { title: '调拨管理' }, { title: '编辑' }] }), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { setRequirementModalOpen(true); } }, '查看需求说明') + ), + React.createElement( + 'div', + { style: { marginBottom: 16, color: 'rgba(0,0,0,0.65)', fontSize: 12 } }, + '调拨单号:', + initialSnapshot.transferId, + ' | ', + '保存状态:', + savedAt ? ('已保存(' + savedAt + ')') : '未保存' + ), + + React.createElement( + Modal, + { + title: '需求说明', + open: requirementModalOpen, + onCancel: function () { setRequirementModalOpen(false); }, + width: 720, + footer: React.createElement(Button, { onClick: function () { setRequirementModalOpen(false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, + React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.65, color: 'rgba(0,0,0,0.85)' } }, requirementDocContent) + ), + + React.createElement( + Card, + { title: '调拨情况', style: { marginBottom: 16 } }, + React.createElement( + 'div', + { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px 24px', alignItems: 'start' } }, + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, reqStar, '调拨日期'), + React.createElement(DatePicker, { + style: controlStyle, + format: 'YYYY-MM-DD HH:mm', + showTime: { format: 'HH:mm' }, + placeholder: '请选择调拨日期', + value: form.transferDate, + onChange: function (v) { updateForm({ transferDate: v }); }, + status: errors.transferDate ? 'error' : undefined + }), + errors.transferDate ? React.createElement('div', { style: { marginTop: 4, color: '#ff4d4f', fontSize: 12 } }, errors.transferDate) : null + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, reqStar, '出发区域'), + React.createElement(Cascader, { + style: controlStyle, + placeholder: '请选择省 / 市', + options: regionOptions, + value: form.departRegion, + onChange: function (v) { updateForm({ departRegion: v }); }, + allowClear: true, + status: errors.departRegion ? 'error' : undefined + }), + errors.departRegion ? React.createElement('div', { style: { marginTop: 4, color: '#ff4d4f', fontSize: 12 } }, errors.departRegion) : null + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, reqStar, '接收区域'), + React.createElement(Cascader, { + style: controlStyle, + placeholder: '请选择省 / 市', + options: regionOptions, + value: form.receiveRegion, + onChange: function (v) { updateForm({ receiveRegion: v }); }, + allowClear: true, + status: errors.receiveRegion ? 'error' : undefined + }), + errors.receiveRegion ? React.createElement('div', { style: { marginTop: 4, color: '#ff4d4f', fontSize: 12 } }, errors.receiveRegion) : null + ), + React.createElement( + 'div', + { style: Object.assign({}, formItemStyle, { gridColumn: 'span 3' }) }, + React.createElement('div', { style: labelStyle }, reqStar, '调拨原因'), + React.createElement(Input.TextArea, { + placeholder: '请输入调拨原因', + value: form.reason, + onChange: function (e) { updateForm({ reason: e.target.value }); }, + autoSize: { minRows: 3, maxRows: 6 }, + status: errors.reason ? 'error' : undefined + }), + errors.reason ? React.createElement('div', { style: { marginTop: 4, color: '#ff4d4f', fontSize: 12 } }, errors.reason) : null + ) + ) + ), + + React.createElement( + Card, + { title: '调拨车辆清单', style: { marginBottom: 16 } }, + errors.vehicleList ? React.createElement('div', { style: { marginBottom: 12, color: '#ff4d4f', fontSize: 12 } }, errors.vehicleList) : null, + React.createElement(Table, { + rowKey: 'id', + columns: vehicleColumns, + dataSource: vehicles, + size: 'small', + pagination: false, + scroll: { x: 720 } + }), + React.createElement(Button, { type: 'dashed', style: { marginTop: 12, width: '100%' }, onClick: addRow }, '新增一行') + ), + + React.createElement('div', { style: { height: 56 } }), + React.createElement( + 'div', + { + style: { + position: 'fixed', + left: 0, + right: 0, + bottom: 0, + padding: '12px 24px', + background: '#fff', + borderTop: '1px solid #f0f0f0', + display: 'flex', + gap: 8, + zIndex: 10 + } + }, + React.createElement(Button, { type: 'primary', onClick: handleSubmitAudit }, '提交审核'), + React.createElement(Button, { onClick: handleSave }, '保存'), + React.createElement(Button, { onClick: handleCancel }, '取消') + ) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-ops/pages/14-调拨管理-运维(调拨方)-编辑.jsx b/src/prototypes/oneos-web-ops/pages/14-调拨管理-运维(调拨方)-编辑.jsx new file mode 100644 index 0000000..a7d47a8 --- /dev/null +++ b/src/prototypes/oneos-web-ops/pages/14-调拨管理-运维(调拨方)-编辑.jsx @@ -0,0 +1,634 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 运维管理 - 车辆业务 - 调拨管理 - 运维(调拨方)-编辑(参照“调拨信息”页生成) + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + + var antd = window.antd; + var App = antd.App; + var Breadcrumb = antd.Breadcrumb; + var Button = antd.Button; + var Card = antd.Card; + var Input = antd.Input; + var Modal = antd.Modal; + var Select = antd.Select; + var Table = antd.Table; + var Upload = antd.Upload; + var message = antd.message; + + var METHOD_DRIVER = '司机运输'; + var METHOD_THIRD = '第三方运输'; + var PLATE_ELIGIBLE_STATUS = '可运营'; + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + var layoutStyle = { padding: '16px 24px 88px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var labelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var formItemStyle = { marginBottom: 12 }; + var controlStyle = { width: '100%' }; + var readOnlyStyle = { background: 'rgba(0,0,0,0.02)' }; + var reqStarStyle = { color: '#ff4d4f', marginRight: 4 }; + var reqStar = React.createElement('span', { style: reqStarStyle }, '*'); + + var regionOptions = useMemo(function () { + return [ + { value: 'zhejiang', label: '浙江省', children: [{ value: 'hangzhou', label: '杭州市' }, { value: 'ningbo', label: '宁波市' }, { value: 'jiaxing', label: '嘉兴市' }] }, + { value: 'shanghai', label: '上海市', children: [{ value: 'shanghai', label: '上海市' }] }, + { value: 'guangdong', label: '广东省', children: [{ value: 'guangzhou', label: '广州市' }, { value: 'shenzhen', label: '深圳市' }, { value: 'dongguan', label: '东莞市' }] } + ]; + }, []); + + function regionValueToProvinceCityLabel(values) { + if (!values || values.length < 2) return ''; + var i; + var p = null; + for (i = 0; i < regionOptions.length; i++) { + if (regionOptions[i].value === values[0]) { + p = regionOptions[i]; + break; + } + } + if (!p || !p.children) return ''; + var c = null; + for (i = 0; i < p.children.length; i++) { + if (p.children[i].value === values[1]) { + c = p.children[i]; + break; + } + } + if (!c) return p.label; + return p.label + '-' + c.label; + } + + var vehicleDb = useMemo(function () { + return [ + { plateNo: '粤A12345', status: '可运营', brand: '东风', model: 'DFH1180', dashboardH2Unit: 'MPa', currentParking: '天河智慧停车场' }, + { plateNo: '粤B11111', status: '可运营', brand: '比亚迪', model: '汉', dashboardH2Unit: '%', currentParking: '南山科技园停车场' }, + { plateNo: '浙A11111', status: '可运营', brand: '小鹏', model: 'P7', dashboardH2Unit: '%', currentParking: '西湖景区停车场' }, + { plateNo: '浙B22222', status: '可运营', brand: '蔚来', model: 'ET5', dashboardH2Unit: 'MPa', currentParking: '宁波江北停车场' }, + { plateNo: '沪A30003', status: '可运营', brand: '福田', model: 'BJ1180', dashboardH2Unit: 'MPa', currentParking: '张江园区停车场' }, + { plateNo: '粤C99999', status: '维修中', brand: '东风', model: 'DFH1180', dashboardH2Unit: 'MPa', currentParking: '白云维修基地停车场' } + ]; + }, []); + + var plateOptionsEligible = useMemo(function () { + return vehicleDb + .filter(function (v) { + return v.status === PLATE_ELIGIBLE_STATUS; + }) + .map(function (v) { + return { value: v.plateNo, label: v.plateNo }; + }); + }, [vehicleDb]); + + function lookupVehicle(plateNo) { + return vehicleDb.find(function (x) { + return x.plateNo === plateNo; + }); + } + + function buildVehicleRowsFromSnapshot(snap) { + return snap.vehicles.map(function (r) { + var v = r.plateNo ? lookupVehicle(r.plateNo) : null; + return { + id: r.id, + brand: r.brand, + model: r.model, + plateNo: r.plateNo, + h2Unit: v && v.dashboardH2Unit ? v.dashboardH2Unit : '%', + departParking: v && v.currentParking ? v.currentParking : '', + departMileageKm: '', + departHydrogen: '', + departElectricKwh: '' + }; + }); + } + + /** 原型:模拟从“调拨管理列表/详情”进入本页 */ + var mockFromTransfer = useMemo(function () { + return { + transferId: 'TP202603310001', + transferDateStr: '2026-03-31', + departRegionValues: ['guangdong', 'guangzhou'], + receiveRegionValues: ['zhejiang', 'jiaxing'], + reason: '华南业务增量,需将车辆调至华东仓储节点保障运力。', + vehicles: [ + { id: 1, brand: '小鹏', model: 'P7', plateNo: '浙A11111' }, + { id: 2, brand: '蔚来', model: 'ET5', plateNo: '浙B22222' } + ] + }; + }, []); + + var snapshotState = useState(mockFromTransfer); + var snapshot = snapshotState[0]; + + var vehiclesState = useState(function () { + return buildVehicleRowsFromSnapshot(mockFromTransfer); + }); + var vehicles = vehiclesState[0]; + var setVehicles = vehiclesState[1]; + + var infoState = useState({ + method: undefined, + transportLeader: '', + transportPhone: '', + transportCost: '', + receivePerson: undefined + }); + var info = infoState[0]; + var setInfo = infoState[1]; + + var fileListState = useState([]); + var fileList = fileListState[0]; + var setFileList = fileListState[1]; + + var errorsState = useState({}); + var errors = errorsState[0]; + var setErrors = errorsState[1]; + + var requirementModalState = useState(false); + var requirementModalOpen = requirementModalState[0]; + var setRequirementModalOpen = requirementModalState[1]; + + var requirementDocContent = [ + '一个「数字化资产ONEOS运管平台」中的「调拨管理」「运维(调拨方)-编辑」模块', + '', + '页面参照「调拨管理-调拨信息」生成,用于运维侧(调拨方)补充/修改调拨信息并提交审核。', + '', + '1.调拨情况:反写调拨日期/出发区域/接收区域/调拨原因;', + '2.调拨信息:调拨方式、运输负责人、运输方联系方式、(第三方运输时)运输费用与合同附件、接收人员;', + '3.调拨车辆清单:车牌号可编辑;司机运输时需填写出发里程/氢量/电量;', + '4.底部按钮:提交审核、保存、取消(原型)。' + ].join('\n'); + + var methodOptions = useMemo(function () { + return [ + { value: METHOD_DRIVER, label: METHOD_DRIVER }, + { value: METHOD_THIRD, label: METHOD_THIRD } + ]; + }, []); + + var opsStaffOptions = useMemo(function () { + return [ + { value: 'zm', label: '张明(运维一部-现场工程师)' }, + { value: 'wf', label: '王芳(运维二部-调度)' }, + { value: 'lh', label: '李华(运维中心-班长)' }, + { value: 'zq', label: '赵强(运维一部-驻场)' }, + { value: 'cj', label: '陈静(运维三部-客服协同)' } + ]; + }, []); + + function toFixed2Input(v) { + var s = String(v === null || v === undefined ? '' : v); + s = s.replace(/[^\d.]/g, ''); + var firstDot = s.indexOf('.'); + if (firstDot >= 0) { + s = s.slice(0, firstDot + 1) + s.slice(firstDot + 1).replace(/\./g, ''); + } + if (firstDot >= 0) { + var a = s.split('.'); + s = a[0] + '.' + (a[1] || '').slice(0, 2); + } + return s; + } + + function phoneDigitsOnly(v) { + return String(v || '') + .replace(/\D/g, '') + .slice(0, 11); + } + + var updateInfo = useCallback(function (patch) { + setInfo(function (p) { + return Object.assign({}, p, patch); + }); + }, []); + + var updateRow = useCallback(function (index, patch) { + setVehicles(function (prev) { + var list = prev.slice(); + var cur = list[index] || {}; + list[index] = Object.assign({}, cur, patch); + return list; + }); + }, []); + + var handlePlateChange = useCallback( + function (index, plateNo) { + var v = plateNo ? lookupVehicle(plateNo) : null; + var unit = v && v.dashboardH2Unit ? v.dashboardH2Unit : '%'; + updateRow(index, { plateNo: plateNo, h2Unit: unit, departParking: v && v.currentParking ? v.currentParking : '' }); + }, + [updateRow, vehicleDb] + ); + + function runValidate() { + var e = {}; + if (!info.method) e.method = '请选择调拨方式'; + if (!String(info.transportLeader || '').trim()) e.transportLeader = '请输入运输负责人姓名'; + if (!info.receivePerson) e.receivePerson = '请选择接收人员'; + + var phone = String(info.transportPhone || '').trim(); + if (!phone) e.transportPhone = '请输入运输方联系方式'; + else if (!/^\d{11}$/.test(phone)) e.transportPhone = '请输入11位数字手机号'; + + if (info.method === METHOD_THIRD) { + if (!String(info.transportCost || '').trim()) e.transportCost = '请输入运输费用'; + } + + for (var i = 0; i < (vehicles || []).length; i++) { + var r = vehicles[i] || {}; + if (!String(r.plateNo || '').trim()) e['row_' + i + '_plate'] = '请选择车牌号'; + if (info.method === METHOD_DRIVER) { + if (!String(r.departMileageKm || '').trim()) e['row_' + i + '_mile'] = '请输入出发时里程'; + if (!String(r.departHydrogen || '').trim()) e['row_' + i + '_h2'] = '请输入出发时氢量'; + if (!String(r.departElectricKwh || '').trim()) e['row_' + i + '_kwh'] = '请输入出发时电量'; + } + } + setErrors(e); + return { ok: Object.keys(e).length === 0, err: e }; + } + + var handleSubmitAudit = useCallback(function () { + var result = runValidate(); + if (!result.ok) { + message.error('请完善必填项'); + return; + } + Modal.confirm({ + title: '确认提交审核?', + content: '提交后将进入审批流程(原型)。', + okText: '提交审核', + cancelText: '取消', + onOk: function () { + message.success('已提交审核(原型)'); + } + }); + }, [info, vehicles]); + + var handleSave = useCallback(function () { + message.success('已保存(原型:不校验必填项,仅保存当前已填数据)'); + }, []); + + var handleCancel = useCallback(function () { + Modal.confirm({ + title: '是否确认取消', + content: '未保存的修改将丢失,是否确认?', + okText: '确认', + cancelText: '取消', + onOk: function () { + message.info('返回上一页(原型)'); + } + }); + }, []); + + var vehicleColumns = useMemo( + function () { + var cols = [ + { + title: '品牌', + key: 'brand', + width: 100, + render: function (_, r) { + return React.createElement(Input, { value: r.brand || '', readOnly: true, style: readOnlyStyle }); + } + }, + { + title: '型号', + key: 'model', + width: 110, + render: function (_, r) { + return React.createElement(Input, { value: r.model || '', readOnly: true, style: readOnlyStyle }); + } + }, + { + title: React.createElement('span', null, reqStar, '车牌号'), + key: 'plateNo', + width: 220, + render: function (_, r, index) { + return React.createElement(Select, { + placeholder: '请选择或输入车牌号', + style: { width: '100%' }, + value: r.plateNo, + onChange: function (v) { + handlePlateChange(index, v); + }, + allowClear: true, + showSearch: true, + options: plateOptionsEligible, + filterOption: filterOption, + status: errors['row_' + index + '_plate'] ? 'error' : undefined + }); + } + } + ]; + + cols.push({ + title: '出发停车场', + key: 'departParking', + width: 160, + render: function (_, r) { + return React.createElement(Input, { value: r.departParking || '', readOnly: true, style: readOnlyStyle, placeholder: '—' }); + } + }); + + if (info.method === METHOD_DRIVER) { + cols.push({ + title: React.createElement('span', null, reqStar, '出发时里程'), + key: 'departMileageKm', + width: 140, + render: function (_, r, index) { + return React.createElement(Input, { + value: r.departMileageKm, + onChange: function (e) { + updateRow(index, { departMileageKm: toFixed2Input(e.target.value) }); + }, + placeholder: '0.00', + addonAfter: 'km', + status: errors['row_' + index + '_mile'] ? 'error' : undefined + }); + } + }); + cols.push({ + title: React.createElement('span', null, reqStar, '出发时氢量'), + key: 'departHydrogen', + width: 130, + render: function (_, r, index) { + return React.createElement(Input, { + value: r.departHydrogen, + onChange: function (e) { + updateRow(index, { departHydrogen: toFixed2Input(e.target.value) }); + }, + placeholder: '0.00', + addonAfter: r.h2Unit || '%', + status: errors['row_' + index + '_h2'] ? 'error' : undefined + }); + } + }); + cols.push({ + title: React.createElement('span', null, reqStar, '出发时电量'), + key: 'departElectricKwh', + width: 140, + render: function (_, r, index) { + return React.createElement(Input, { + value: r.departElectricKwh, + onChange: function (e) { + updateRow(index, { departElectricKwh: toFixed2Input(e.target.value) }); + }, + placeholder: '0.00', + addonAfter: 'kWh', + status: errors['row_' + index + '_kwh'] ? 'error' : undefined + }); + } + }); + } + + return cols; + }, + [info.method, plateOptionsEligible, errors, handlePlateChange, updateRow] + ); + + var departRegionText = regionValueToProvinceCityLabel(snapshot.departRegionValues); + var receiveRegionText = regionValueToProvinceCityLabel(snapshot.receiveRegionValues); + + return React.createElement( + App, + null, + React.createElement( + 'div', + { style: layoutStyle }, + React.createElement( + 'div', + { + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: 8, + flexWrap: 'wrap', + gap: 8 + } + }, + React.createElement(Breadcrumb, { + items: [{ title: '运维管理' }, { title: '车辆业务' }, { title: '调拨管理' }, { title: '运维(调拨方)-编辑' }] + }), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { setRequirementModalOpen(true); } }, '查看需求说明') + ), + React.createElement( + 'div', + { style: { marginBottom: 16, color: 'rgba(0,0,0,0.65)', fontSize: 12 } }, + '调拨单号:', + snapshot.transferId + ), + + React.createElement( + Modal, + { + title: '需求说明', + open: requirementModalOpen, + onCancel: function () { setRequirementModalOpen(false); }, + width: 760, + footer: React.createElement(Button, { onClick: function () { setRequirementModalOpen(false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, + React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.65, color: 'rgba(0,0,0,0.85)' } }, requirementDocContent) + ), + + React.createElement( + Card, + { title: '调拨情况', style: { marginBottom: 16 } }, + React.createElement( + 'div', + { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px 24px', alignItems: 'start' } }, + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '调拨日期'), + React.createElement(Input, { value: snapshot.transferDateStr, readOnly: true, style: readOnlyStyle, placeholder: 'YYYY-MM-DD' }) + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '出发区域'), + React.createElement(Input, { value: departRegionText, readOnly: true, style: readOnlyStyle, placeholder: '省-市' }) + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '接收区域'), + React.createElement(Input, { value: receiveRegionText, readOnly: true, style: readOnlyStyle, placeholder: '省-市' }) + ), + React.createElement( + 'div', + { style: Object.assign({}, formItemStyle, { gridColumn: 'span 3' }) }, + React.createElement('div', { style: labelStyle }, '调拨原因'), + React.createElement(Input.TextArea, { + value: snapshot.reason, + readOnly: true, + style: readOnlyStyle, + autoSize: { minRows: 3, maxRows: 6 } + }) + ) + ) + ), + + React.createElement( + Card, + { title: '调拨信息', style: { marginBottom: 16 } }, + React.createElement( + 'div', + { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px 24px', alignItems: 'start' } }, + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, reqStar, '调拨方式'), + React.createElement(Select, { + placeholder: '请选择调拨方式', + style: controlStyle, + value: info.method, + onChange: function (v) { + updateInfo({ method: v }); + }, + allowClear: true, + options: methodOptions, + status: errors.method ? 'error' : undefined + }), + errors.method ? React.createElement('div', { style: { marginTop: 4, color: '#ff4d4f', fontSize: 12 } }, errors.method) : null + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, reqStar, '运输负责人'), + React.createElement(Input, { + placeholder: '请输入运输负责人姓名', + value: info.transportLeader, + onChange: function (e) { + updateInfo({ transportLeader: e.target.value }); + }, + status: errors.transportLeader ? 'error' : undefined + }) + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, reqStar, '运输方联系方式'), + React.createElement(Input, { + placeholder: '请输入运输方联系方式', + value: info.transportPhone, + maxLength: 11, + onChange: function (e) { + updateInfo({ transportPhone: phoneDigitsOnly(e.target.value) }); + }, + status: errors.transportPhone ? 'error' : undefined + }), + errors.transportPhone ? React.createElement('div', { style: { marginTop: 4, color: '#ff4d4f', fontSize: 12 } }, errors.transportPhone) : null + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, reqStar, '接收人员'), + React.createElement(Select, { + placeholder: '请输入或选择运维部门人员', + style: controlStyle, + value: info.receivePerson, + onChange: function (v) { + updateInfo({ receivePerson: v }); + }, + allowClear: true, + showSearch: true, + options: opsStaffOptions, + filterOption: filterOption, + status: errors.receivePerson ? 'error' : undefined + }), + errors.receivePerson ? React.createElement('div', { style: { marginTop: 4, color: '#ff4d4f', fontSize: 12 } }, errors.receivePerson) : null + ) + ), + + info.method === METHOD_THIRD + ? React.createElement( + 'div', + { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px 24px', marginTop: 8 } }, + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, reqStar, '运输费用'), + React.createElement(Input, { + placeholder: '0.00', + value: info.transportCost, + onChange: function (e) { + updateInfo({ transportCost: toFixed2Input(e.target.value) }); + }, + addonAfter: '元', + status: errors.transportCost ? 'error' : undefined + }), + errors.transportCost ? React.createElement('div', { style: { marginTop: 4, color: '#ff4d4f', fontSize: 12 } }, errors.transportCost) : null + ), + React.createElement( + 'div', + { style: Object.assign({}, formItemStyle, { gridColumn: 'span 2' }) }, + React.createElement('div', { style: labelStyle }, '运输合同附件(选填)'), + React.createElement( + Upload, + { + accept: '.doc,.docx,.pdf', + multiple: true, + fileList: fileList, + beforeUpload: function () { + return false; + }, + onChange: function (i) { + setFileList(i.fileList || []); + } + }, + React.createElement(Button, null, '附件上传') + ), + React.createElement('div', { style: { marginTop: 6, fontSize: 12, color: 'rgba(0,0,0,0.45)' } }, '支持 doc、docx、pdf;原型不实际上传服务器') + ) + ) + : null + ), + + React.createElement( + Card, + { title: '调拨车辆清单', style: { marginBottom: 16 } }, + React.createElement(Table, { + rowKey: 'id', + columns: vehicleColumns, + dataSource: vehicles, + size: 'small', + pagination: false, + scroll: { x: 'max-content' } + }) + ), + + React.createElement('div', { style: { height: 56 } }), + React.createElement( + 'div', + { + style: { + position: 'fixed', + left: 0, + right: 0, + bottom: 0, + padding: '12px 24px', + background: '#fff', + borderTop: '1px solid #f0f0f0', + display: 'flex', + gap: 8, + zIndex: 10 + } + }, + React.createElement(Button, { type: 'primary', onClick: handleSubmitAudit }, '提交审核'), + React.createElement(Button, { onClick: handleSave }, '保存'), + React.createElement(Button, { onClick: handleCancel }, '取消') + ) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-ops/pages/15-调拨管理-运维(接收)-编辑.jsx b/src/prototypes/oneos-web-ops/pages/15-调拨管理-运维(接收)-编辑.jsx new file mode 100644 index 0000000..dfca410 --- /dev/null +++ b/src/prototypes/oneos-web-ops/pages/15-调拨管理-运维(接收)-编辑.jsx @@ -0,0 +1,479 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 运维管理 - 车辆业务 - 调拨管理 - 运维(接收)-编辑 +// 参照“调拨管理-接收信息-第三方运输”生成,默认反写已保存数据 + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + + var antd = window.antd; + var App = antd.App; + var Breadcrumb = antd.Breadcrumb; + var Button = antd.Button; + var Card = antd.Card; + var DatePicker = antd.DatePicker; + var Input = antd.Input; + var Modal = antd.Modal; + var Select = antd.Select; + var Table = antd.Table; + var message = antd.message; + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + var METHOD_THIRD = '第三方运输'; + + function parseDateValue(s) { + var str = String(s || '').trim(); + if (!str) return null; + try { + if (window.dayjs) return window.dayjs(str); + } catch (e1) {} + try { + if (window.moment) return window.moment(str, ['YYYY-MM-DD', 'YYYY-MM-DD HH:mm'], true); + } catch (e2) {} + return null; + } + + function getMockSnapshot() { + return { + transferId: 'TP202603310008', + savedAt: '2026-04-01 15:20', + transferDateStr: '2026-03-31', + departRegionText: '广东省-广州市', + receiveRegionText: '浙江省-嘉兴市', + reason: '华南业务增量,需将车辆调至华东仓储节点保障运力。', + method: METHOD_THIRD, + transportLeader: '刘振华', + transportPhone: '13901234567', + transportCost: '8650.50', + transportContractFileLabel: '顺达物流有限公司运输合同.pdf', + receivePersonName: '王芳', + receiveInfo: { + receiveTime: '2026-04-02', + receiveParking: 'p3' + }, + transferVehicles: [ + { id: 1, brand: '小鹏', model: 'P7', plateNo: '浙A11111', departParking: '西湖景区停车场' }, + { id: 2, brand: '蔚来', model: 'ET5', plateNo: '浙B22222', departParking: '宁波江北停车场' } + ] + }; + } + + var layoutStyle = { padding: '16px 24px 88px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var labelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var formItemStyle = { marginBottom: 12 }; + var controlStyle = { width: '100%' }; + var readOnlyStyle = { background: 'rgba(0,0,0,0.02)' }; + var reqStarStyle = { color: '#ff4d4f', marginRight: 4 }; + var reqStar = React.createElement('span', { style: reqStarStyle }, '*'); + + var snapshot = useMemo(function () { + return getMockSnapshot(); + }, []); + + function buildVehicleRows(snap) { + return (snap.transferVehicles || []).map(function (r) { + return { + id: r.id, + brand: r.brand, + model: r.model, + plateNo: r.plateNo, + departParking: r.departParking || '' + }; + }); + } + + var receiveTimeState = useState(parseDateValue(snapshot.receiveInfo && snapshot.receiveInfo.receiveTime)); + var receiveTime = receiveTimeState[0]; + var setReceiveTime = receiveTimeState[1]; + + var receiveParkingState = useState(snapshot.receiveInfo && snapshot.receiveInfo.receiveParking ? snapshot.receiveInfo.receiveParking : undefined); + var receiveParking = receiveParkingState[0]; + var setReceiveParking = receiveParkingState[1]; + + var vehiclesState = useState(function () { + return buildVehicleRows(snapshot); + }); + var vehicles = vehiclesState[0]; + + var savedAtState = useState(snapshot.savedAt); + var savedAt = savedAtState[0]; + var setSavedAt = savedAtState[1]; + + var parkingOptions = useMemo(function () { + return [ + { value: 'p1', label: '西湖景区停车场' }, + { value: 'p2', label: '宁波江北停车场' }, + { value: 'p3', label: '张江园区停车场' }, + { value: 'p4', label: '天河智慧停车场' } + ]; + }, []); + + var errorsState = useState({}); + var errors = errorsState[0]; + var setErrors = errorsState[1]; + + var requirementModalState = useState(false); + var requirementModalOpen = requirementModalState[0]; + var setRequirementModalOpen = requirementModalState[1]; + + var attachmentPreviewState = useState(false); + var attachmentPreviewOpen = attachmentPreviewState[0]; + var setAttachmentPreviewOpen = attachmentPreviewState[1]; + + var requirementDocContent = [ + '一个「数字化资产ONEOS运管平台」中的「调拨管理」「运维(接收)-编辑」模块', + '', + '页面参照「调拨管理-接收信息-第三方运输」生成,进入页面时反写已保存的接收信息,并支持继续编辑。', + '', + '1.调拨情况:调拨日期、出发区域、接收区域、调拨原因为只读反写;', + '2.调拨信息:调拨方式、运输负责人、运输方联系方式、运输费用、运输合同附件、接收人员为只读反写;', + '3.接收信息:接收时间、停车场(直接选择)可编辑;', + '4.底部按钮:提交、保存、取消。保存不校验必填项,提交校验必填项。' + ].join('\n'); + + function formatCostYuan(costStr) { + var s = String(costStr == null ? '' : costStr).trim(); + if (!s) return '—'; + return s + '元'; + } + + function contractDisplay(label) { + var s = String(label == null ? '' : label).trim(); + return s ? s : '无'; + } + + function canPreviewAttachment(label) { + var s = String(label == null ? '' : label).trim(); + if (!s) return false; + var lower = s.toLowerCase(); + return lower.endsWith('.pdf') || lower.endsWith('.doc') || lower.endsWith('.docx'); + } + + function runValidate() { + var e = {}; + if (!receiveTime) e.receiveTime = '请选择接收时间'; + if (!receiveParking) e.receiveParking = '请选择停车场'; + setErrors(e); + return { ok: Object.keys(e).length === 0, err: e }; + } + + var handleSubmit = useCallback(function () { + var result = runValidate(); + if (!result.ok) { + message.error('请完善必填项'); + return; + } + Modal.confirm({ + title: '确认提交?', + content: '提交后将进入后续流程(原型)。', + okText: '提交', + cancelText: '取消', + onOk: function () { + message.success('已提交(原型)'); + } + }); + }, [receiveTime, receiveParking]); + + var handleSave = useCallback(function () { + var nowStr = ''; + try { + if (window.dayjs) nowStr = window.dayjs().format('YYYY-MM-DD HH:mm'); + } catch (e1) {} + try { + if (!nowStr && window.moment) nowStr = window.moment().format('YYYY-MM-DD HH:mm'); + } catch (e2) {} + if (!nowStr) nowStr = '刚刚'; + setSavedAt(nowStr); + message.success('已保存(原型:不校验必填项)'); + }, []); + + var handleCancel = useCallback(function () { + Modal.confirm({ + title: '是否确认取消', + content: '未保存的修改将丢失,是否确认?', + okText: '确认', + cancelText: '取消', + onOk: function () { + message.info('返回上一页(原型)'); + } + }); + }, []); + + var vehicleColumns = useMemo(function () { + return [ + { + title: '品牌', + key: 'brand', + width: 120, + render: function (_, r) { + return React.createElement(Input, { value: r.brand || '', readOnly: true, style: readOnlyStyle }); + } + }, + { + title: '型号', + key: 'model', + width: 120, + render: function (_, r) { + return React.createElement(Input, { value: r.model || '', readOnly: true, style: readOnlyStyle }); + } + }, + { + title: '车牌号', + key: 'plateNo', + width: 140, + render: function (_, r) { + return React.createElement(Input, { value: r.plateNo || '', readOnly: true, style: readOnlyStyle }); + } + }, + { + title: '出发停车场', + key: 'departParking', + width: 180, + render: function (_, r) { + return React.createElement(Input, { value: r.departParking || '', readOnly: true, style: readOnlyStyle }); + } + } + ]; + }, []); + + var costText = formatCostYuan(snapshot.transportCost); + var contractText = contractDisplay(snapshot.transportContractFileLabel); + var canPreview = canPreviewAttachment(snapshot.transportContractFileLabel); + + return React.createElement( + App, + null, + React.createElement( + 'div', + { style: layoutStyle }, + React.createElement( + 'div', + { + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: 8, + flexWrap: 'wrap', + gap: 8 + } + }, + React.createElement(Breadcrumb, { + items: [{ title: '运维管理' }, { title: '车辆业务' }, { title: '调拨管理' }, { title: '运维(接收)-编辑' }] + }), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { setRequirementModalOpen(true); } }, '查看需求说明') + ), + React.createElement( + 'div', + { style: { marginBottom: 16, color: 'rgba(0,0,0,0.65)', fontSize: 12 } }, + '调拨单号:', + snapshot.transferId, + ' | ', + '保存状态:', + savedAt ? ('已保存(' + savedAt + ')') : '未保存' + ), + + React.createElement( + Modal, + { + title: '需求说明', + open: requirementModalOpen, + onCancel: function () { setRequirementModalOpen(false); }, + width: 760, + footer: React.createElement(Button, { onClick: function () { setRequirementModalOpen(false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, + React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.65, color: 'rgba(0,0,0,0.85)' } }, requirementDocContent) + ), + + React.createElement( + Modal, + { + title: '附件预览', + open: attachmentPreviewOpen, + onCancel: function () { setAttachmentPreviewOpen(false); }, + width: 820, + footer: React.createElement(Button, { onClick: function () { setAttachmentPreviewOpen(false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, + React.createElement( + 'div', + { style: { fontSize: 13, lineHeight: 1.65, color: 'rgba(0,0,0,0.85)' } }, + React.createElement('div', { style: { marginBottom: 8 } }, '文件:', snapshot.transportContractFileLabel || '无'), + React.createElement( + 'div', + { style: { padding: 12, background: 'rgba(0,0,0,0.02)', border: '1px solid #f0f0f0', borderRadius: 8 } }, + '原型预览:此处展示附件内容预览区域。对接后可通过文件 URL 进行在线预览或下载。' + ) + ) + ), + + React.createElement( + Card, + { title: '调拨情况', style: { marginBottom: 16 } }, + React.createElement( + 'div', + { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px 24px', alignItems: 'start' } }, + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '调拨日期'), + React.createElement(Input, { value: snapshot.transferDateStr, readOnly: true, style: readOnlyStyle }) + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '出发区域'), + React.createElement(Input, { value: snapshot.departRegionText, readOnly: true, style: readOnlyStyle }) + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '接收区域'), + React.createElement(Input, { value: snapshot.receiveRegionText, readOnly: true, style: readOnlyStyle }) + ), + React.createElement( + 'div', + { style: Object.assign({}, formItemStyle, { gridColumn: 'span 3' }) }, + React.createElement('div', { style: labelStyle }, '调拨原因'), + React.createElement(Input.TextArea, { value: snapshot.reason, readOnly: true, style: readOnlyStyle, autoSize: { minRows: 3, maxRows: 6 } }) + ) + ) + ), + + React.createElement( + Card, + { title: '调拨信息', style: { marginBottom: 16 } }, + React.createElement( + 'div', + { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px 24px', alignItems: 'start' } }, + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '调拨方式'), + React.createElement(Input, { value: snapshot.method, readOnly: true, style: readOnlyStyle }) + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '运输负责人'), + React.createElement(Input, { value: snapshot.transportLeader, readOnly: true, style: readOnlyStyle }) + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '运输方联系方式'), + React.createElement(Input, { value: snapshot.transportPhone, readOnly: true, style: readOnlyStyle }) + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '运输费用'), + React.createElement(Input, { value: costText, readOnly: true, style: readOnlyStyle }) + ), + React.createElement( + 'div', + { style: Object.assign({}, formItemStyle, { gridColumn: 'span 2' }) }, + React.createElement('div', { style: labelStyle }, '运输合同附件'), + canPreview + ? React.createElement( + 'div', + { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } }, + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { setAttachmentPreviewOpen(true); } }, contractText) + ) + : React.createElement(Input, { value: contractText, readOnly: true, style: readOnlyStyle }) + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, '接收人员'), + React.createElement(Input, { value: snapshot.receivePersonName, readOnly: true, style: readOnlyStyle }) + ) + ) + ), + + React.createElement( + Card, + { title: '接收信息', style: { marginBottom: 16 } }, + React.createElement( + 'div', + { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px 24px', alignItems: 'start' } }, + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, reqStar, '接收时间'), + React.createElement(DatePicker, { + style: controlStyle, + format: 'YYYY-MM-DD', + placeholder: '请选择接收时间', + value: receiveTime, + onChange: function (v) { + setReceiveTime(v); + }, + status: errors.receiveTime ? 'error' : undefined + }), + errors.receiveTime ? React.createElement('div', { style: { marginTop: 4, color: '#ff4d4f', fontSize: 12 } }, errors.receiveTime) : null + ), + React.createElement( + 'div', + { style: formItemStyle }, + React.createElement('div', { style: labelStyle }, reqStar, '停车场'), + React.createElement(Select, { + placeholder: '请选择停车场', + style: controlStyle, + value: receiveParking, + onChange: function (v) { setReceiveParking(v); }, + allowClear: true, + showSearch: true, + options: parkingOptions, + filterOption: filterOption, + status: errors.receiveParking ? 'error' : undefined + }), + errors.receiveParking ? React.createElement('div', { style: { marginTop: 4, color: '#ff4d4f', fontSize: 12 } }, errors.receiveParking) : null + ) + ) + ), + + React.createElement( + Card, + { title: '调拨车辆清单', style: { marginBottom: 16 } }, + React.createElement(Table, { + rowKey: 'id', + columns: vehicleColumns, + dataSource: vehicles, + size: 'small', + pagination: false + }) + ), + + React.createElement('div', { style: { height: 56 } }), + React.createElement( + 'div', + { + style: { + position: 'fixed', + left: 0, + right: 0, + bottom: 0, + padding: '12px 24px', + background: '#fff', + borderTop: '1px solid #f0f0f0', + display: 'flex', + gap: 8, + zIndex: 10 + } + }, + React.createElement(Button, { type: 'primary', onClick: handleSubmit }, '提交'), + React.createElement(Button, { onClick: handleSave }, '保存'), + React.createElement(Button, { onClick: handleCancel }, '取消') + ) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-ops/pages/16-调拨管理.jsx b/src/prototypes/oneos-web-ops/pages/16-调拨管理.jsx new file mode 100644 index 0000000..4d813da --- /dev/null +++ b/src/prototypes/oneos-web-ops/pages/16-调拨管理.jsx @@ -0,0 +1,1005 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 运维管理 - 车辆业务 - 调拨管理(列表页) + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + + var antd = window.antd; + var App = antd.App; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Button = antd.Button; + var Table = antd.Table; + var Tabs = antd.Tabs; + var Select = antd.Select; + var DatePicker = antd.DatePicker; + var Cascader = antd.Cascader; + var message = antd.message; + var Modal = antd.Modal; + var Popover = antd.Popover; + var List = antd.List; + + var RangePicker = DatePicker.RangePicker; + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + function fmtYMDHM(v) { + if (v === null || v === undefined) return '-'; + var s = String(v).trim(); + if (!s) return '-'; + if (/^\\d{4}-\\d{2}-\\d{2}\\s+\\d{2}:\\d{2}/.test(s)) return s.slice(0, 16); + if (/^\\d{4}-\\d{2}-\\d{2}$/.test(s)) return s + ' 00:00'; + try { + var d = new Date(s.replace(/-/g, '/')); + if (isNaN(d.getTime())) return s; + var p2 = function (n) { return n < 10 ? '0' + n : '' + n; }; + return d.getFullYear() + '-' + p2(d.getMonth() + 1) + '-' + p2(d.getDate()) + ' ' + p2(d.getHours()) + ':' + p2(d.getMinutes()); + } catch (e) { + return s; + } + } + + var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var filterLabelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var filterItemStyle = { marginBottom: 12 }; + var filterControlStyle = { width: '100%' }; + var tableSingleLineStyle = '.transfer-list-table .ant-table-thead th,.transfer-list-table .ant-table-tbody td{white-space:nowrap;}'; + + var regionOptions = useMemo(function () { + return [ + { value: 'zhejiang', label: '浙江省', children: [{ value: 'hangzhou', label: '杭州市' }, { value: 'ningbo', label: '宁波市' }, { value: 'jiaxing', label: '嘉兴市' }] }, + { value: 'shanghai', label: '上海市', children: [{ value: 'shanghai', label: '上海市' }] }, + { value: 'guangdong', label: '广东省', children: [{ value: 'guangzhou', label: '广州市' }, { value: 'shenzhen', label: '深圳市' }, { value: 'dongguan', label: '东莞市' }] } + ]; + }, []); + + var userOptions = useMemo(function () { + return [ + { value: '张明', label: '张明' }, + { value: '王芳', label: '王芳' }, + { value: '李华', label: '李华' }, + { value: '赵强', label: '赵强' }, + { value: '陈静', label: '陈静' } + ]; + }, []); + + var methodOptions = useMemo(function () { + return [ + { value: '第三方运输', label: '第三方运输' }, + { value: '司机驾驶', label: '司机驾驶' } + ]; + }, []); + + var approvalOptions = useMemo(function () { + return [ + { value: '待提交', label: '待提交' }, + { value: '审批中', label: '审批中' }, + { value: '审批完成', label: '审批完成' }, + { value: '审批驳回', label: '审批驳回' }, + { value: '撤回', label: '撤回' } + ]; + }, []); + + var filterExpandedState = useState(false); + var filterExpanded = filterExpandedState[0]; + var setFilterExpanded = filterExpandedState[1]; + + var draftState = useState({ + transferDateRange: null, + transferPerson: undefined, + receiveDateRange: null, + receivePerson: undefined, + departRegion: undefined, + receiveRegion: undefined, + method: undefined, + bizApprovalStatus: undefined, + opsApprovalStatus: undefined + }); + var draft = draftState[0]; + var setDraft = draftState[1]; + + var appliedState = useState({ + transferDateRange: null, + transferPerson: undefined, + receiveDateRange: null, + receivePerson: undefined, + departRegion: undefined, + receiveRegion: undefined, + method: undefined, + bizApprovalStatus: undefined, + opsApprovalStatus: undefined + }); + var applied = appliedState[0]; + var setApplied = appliedState[1]; + + var tabState = useState('in_progress'); + var tab = tabState[0]; + var setTab = tabState[1]; + + var pageState = useState(1); + var page = pageState[0]; + var setPage = pageState[1]; + var pageSizeState = useState(20); + var pageSize = pageSizeState[0]; + var setPageSize = pageSizeState[1]; + + var requirementModalState = useState(false); + var requirementModalOpen = requirementModalState[0]; + var setRequirementModalOpen = requirementModalState[1]; + + var requirementDocContent = [ + '一个「数字化资产ONEOS运管平台」中的「调拨管理」模块', + '#面包屑:运维管理-车辆业务-调拨管理', + '1.筛选:', + '1.1.调拨日期:日期选择器,支持单输入框双日历进行开始-结束时间搜索;', + '1.2.调拨人:选择器,支持输入框输入内容模糊搜索下拉选择对应调拨人,调拨人来自用户表;', + '1.3.接收日期:日期选择器,支持单输入框双日历进行开始-结束时间搜索;', + '1.4.接收人:选择器,支持输入框输入内容模糊搜索下拉选择对应接收人,接收人来自用户表;', + '1.5.出发区域:级联地区选择器,省-市2级;', + '1.6.调拨区域:级联地区选择器,省-市2级;', + '1.7.调拨方式:选择器,选项为:第三方运输、司机驾驶;', + '1.8.业务审批状态:选择器,选项为:待提交、审批中、审批完成、审批驳回、撤回;', + '1.9.运维审批状态:选择器,选项为:待提交、审批中、审批完成、审批驳回、撤回;', + '', + '2.列表:分为进行中、历史记录2个tab;tab最右侧为新增、导出;', + '2.1.进行中:', + '2.1.1.调拨日期:显示调拨日期,格式为:YYYY-MM-DD HH:MM;', + '2.1.2.业务审批状态:待提交、审批中、审批完成、驳回、撤回;', + '2.1.3.运维审批状态:待提交、审批中、审批完成、驳回、撤回;', + '2.1.4.出发区域:显示出发省-市,根据车辆出发时停车场区域获取;', + '2.1.5.接收区域:显示接受省-市;', + '2.1.6.调拨原因:显示调拨原因;', + '2.1.7.车辆数:显示调拨车辆数,点击车辆数显示气泡卡片,卡片中显示列表,列表字段为:品牌、型号、车牌号;', + '2.1.8.调拨人:显示表单创建人姓名;', + '2.1.9.调拨方式:显示调拨方式,分为第三方运输、司机运输;', + '2.1.10.运输负责人:显示调拨车辆对应运输负责人;', + '2.1.11.运输方联系方式:显示调拨车辆对应运输方联系方式;', + '2.1.12.接收人员:显示接收人姓名;', + '2.1.13.创建日期:显示调拨申请创建日期,格式为:YYYY-MM-DD HH:MM;', + '2.1.14.操作:查看、编辑、撤回、调拨信息、接收信息;', + '2.1.14.1.编辑:业务审批状态或运维审批状态任意一个为「待提交/撤回/审批驳回」时显示;', + '2.1.14.2.接收信息:运维审批状态为「审批中/审批完成」时显示;', + '2.1.14.3.调拨信息:业务审批状态为「审批完成」且运维审批状态为空或「待提交」时显示;', + '', + '2.2.历史记录:', + '2.2.1.调拨日期:显示调拨日期,格式为:YYYY-MM-DD HH:MM;', + '2.2.2.业务审批状态:待提交、审批中、审批完成、驳回、撤回;', + '2.2.3.运维审批状态:待提交、审批中、审批完成、驳回、撤回;', + '2.2.4.出发区域:显示出发省-市,根据车辆出发时停车场区域获取;', + '2.2.5.接收区域:显示接受省-市;', + '2.2.6.调拨原因:显示调拨原因;', + '2.2.7.车辆数:显示调拨车辆数,点击车辆数显示气泡卡片,卡片中显示列表,列表字段为:品牌、型号、车牌号;', + '2.2.8.调拨人:显示表单创建人姓名;', + '2.2.9.调拨方式:显示调拨方式,分为第三方运输、司机运输;', + '2.2.10.运输负责人:显示调拨车辆对应运输负责人;', + '2.2.11.运输方联系方式:显示调拨车辆对应运输方联系方式;', + '2.2.12.接收人员:显示接收人姓名;', + '2.2.13.接收时间:显示接收时间,格式为:YYYY-MM-DD HH:MM;', + '2.2.14.接收区域:显示接受区域,停车场或其他;', + '2.2.15.停车场/停车地址:显示停车场名称或停车地址;', + '2.2.16.创建日期:显示调拨申请创建日期,格式为:YYYY-MM-DD HH:MM;', + '2.2.17.操作:查看;' + ].join('\n'); + + var listInProgressState = useState(function () { + return [ + { + id: 'tp1', + transferDate: '2026-02-20 09:30', + departRegionText: '广东省-广州市', + receiveRegionText: '浙江省-嘉兴市', + bizApprovalStatus: '待提交', + opsApprovalStatus: '待提交', + method: '第三方运输', + reason: '场站资源调整', + vehicles: [ + { brand: '东风', model: 'DFH1180', plateNo: '粤A12345' }, + { brand: '比亚迪', model: '汉', plateNo: '粤B11111' } + ], + transferPerson: '张明', + transportLeader: '周海', + transportContact: '13800000001', + receivePerson: '王芳', + transferInfoStatus: '未填写', + createDate: '2026-02-20 08:10' + }, + { + id: 'tp2', + transferDate: '2026-02-22 14:10', + departRegionText: '浙江省-杭州市', + receiveRegionText: '上海市-上海市', + bizApprovalStatus: '审批完成', + opsApprovalStatus: '待提交', + method: '司机驾驶', + reason: '项目调度需要', + vehicles: [ + { brand: '小鹏', model: 'P7', plateNo: '浙A11111' }, + { brand: '蔚来', model: 'ET5', plateNo: '浙B22222' }, + { brand: '福田', model: 'BJ1180', plateNo: '沪A30003' } + ], + transferPerson: '李华', + transportLeader: '李华', + transportContact: '13800000002', + receivePerson: '赵强', + transferInfoStatus: '已通过', + createDate: '2026-02-22 12:40' + }, + { + id: 'tp3', + transferDate: '2026-02-24 11:05', + departRegionText: '广东省-深圳市', + receiveRegionText: '浙江省-宁波市', + bizApprovalStatus: '审批完成', + opsApprovalStatus: '审批完成', + method: '第三方运输', + reason: '维修站资源调整', + vehicles: [ + { brand: '比亚迪', model: '汉', plateNo: '粤B11111' } + ], + transferPerson: '王芳', + transportLeader: '黄蕾', + transportContact: '13800000003', + receivePerson: '陈静', + transferInfoStatus: '已通过', + createDate: '2026-02-24 10:20' + }, + { + id: 'tp4', + transferDate: '2026-02-25 16:40', + departRegionText: '浙江省-嘉兴市', + receiveRegionText: '上海市-上海市', + bizApprovalStatus: '审批完成', + opsApprovalStatus: '待提交', + method: '司机驾驶', + reason: '项目调度需要', + vehicles: [ + { brand: '蔚来', model: 'ET5', plateNo: '浙B22222' } + ], + transferPerson: '赵强', + transportLeader: '赵强', + transportContact: '13800000004', + receivePerson: '李华', + transferInfoStatus: '未填写', + createDate: '2026-02-25 15:55' + }, + { + id: 'tp5', + transferDate: '2026-02-26 09:15', + departRegionText: '上海市-上海市', + receiveRegionText: '广东省-广州市', + bizApprovalStatus: '审批完成', + opsApprovalStatus: '审批中', + method: '第三方运输', + reason: '车辆调拨申请驳回样例', + vehicles: [ + { brand: '福田', model: 'BJ1180', plateNo: '沪A30003' }, + { brand: '东风', model: 'DFH1180', plateNo: '粤A12345' } + ], + transferPerson: '陈静', + transportLeader: '周海', + transportContact: '13800000005', + receivePerson: '张明', + transferInfoStatus: '未填写', + createDate: '2026-02-26 08:30' + }, + { + id: 'tp6', + transferDate: '2026-02-27 13:30', + departRegionText: '浙江省-杭州市', + receiveRegionText: '浙江省-嘉兴市', + bizApprovalStatus: '撤回', + opsApprovalStatus: '待提交', + method: '司机驾驶', + reason: '计划变更,撤回申请', + vehicles: [ + { brand: '小鹏', model: 'P7', plateNo: '浙A11111' } + ], + transferPerson: '张明', + transportLeader: '张明', + transportContact: '13800000006', + receivePerson: '王芳', + transferInfoStatus: '未填写', + createDate: '2026-02-27 12:45' + }, + { + id: 'tp7', + transferDate: '2026-02-28 08:50', + departRegionText: '广东省-东莞市', + receiveRegionText: '广东省-深圳市', + bizApprovalStatus: '审批中', + opsApprovalStatus: '待提交', + method: '第三方运输', + reason: '跨区域调拨', + vehicles: [ + { brand: '东风', model: 'DFH1180', plateNo: '粤A12345' }, + { brand: '福田', model: 'BJ1180', plateNo: '沪A30003' } + ], + transferPerson: '李华', + transportLeader: '黄蕾', + transportContact: '13800000007', + receivePerson: '赵强', + transferInfoStatus: '未填写', + createDate: '2026-02-28 08:05' + }, + { + id: 'tp8', + transferDate: '2026-03-01 10:10', + departRegionText: '浙江省-宁波市', + receiveRegionText: '上海市-上海市', + bizApprovalStatus: '待提交', + opsApprovalStatus: '待提交', + method: '司机驾驶', + reason: '项目新增点位支持', + vehicles: [ + { brand: '蔚来', model: 'ET5', plateNo: '浙B22222' }, + { brand: '小鹏', model: 'P7', plateNo: '浙A11111' } + ], + transferPerson: '王芳', + transportLeader: '王芳', + transportContact: '13800000008', + receivePerson: '陈静', + transferInfoStatus: '未填写', + createDate: '2026-03-01 09:30' + }, + { + id: 'tp9', + transferDate: '2026-03-02 15:25', + departRegionText: '上海市-上海市', + receiveRegionText: '浙江省-杭州市', + bizApprovalStatus: '审批中', + opsApprovalStatus: '待提交', + method: '第三方运输', + reason: '场站容量调整', + vehicles: [ + { brand: '福田', model: 'BJ1180', plateNo: '沪A30003' } + ], + transferPerson: '赵强', + transportLeader: '周海', + transportContact: '13800000009', + receivePerson: '李华', + transferInfoStatus: '未填写', + createDate: '2026-03-02 14:40' + }, + { + id: 'tp10', + transferDate: '2026-03-03 09:05', + departRegionText: '浙江省-嘉兴市', + receiveRegionText: '广东省-广州市', + bizApprovalStatus: '待提交', + opsApprovalStatus: '待提交', + method: '第三方运输', + reason: '车辆调拨支持新项目', + vehicles: [ + { brand: '小鹏', model: 'P7', plateNo: '浙A11111' }, + { brand: '比亚迪', model: '汉', plateNo: '粤B11111' }, + { brand: '福田', model: 'BJ1180', plateNo: '沪A30003' }, + { brand: '蔚来', model: 'ET5', plateNo: '浙B22222' } + ], + transferPerson: '陈静', + transportLeader: '黄蕾', + transportContact: '13800000010', + receivePerson: '张明', + transferInfoStatus: '未填写', + createDate: '2026-03-03 08:20' + } + ]; + }); + var listInProgress = listInProgressState[0]; + var setListInProgress = listInProgressState[1]; + + var listHistory = useMemo(function () { + return [ + { + id: 'th1', + transferDate: '2026-01-10 10:00', + departRegionText: '上海市-上海市', + receiveRegionText: '广东省-深圳市', + bizApprovalStatus: '审批完成', + opsApprovalStatus: '审批完成', + method: '第三方运输', + reason: '场站资源调整', + vehicles: [ + { brand: '福田', model: 'BJ1180', plateNo: '沪A30003' }, + { brand: '东风', model: 'DFH1180', plateNo: '粤A12345' } + ], + transferPerson: '张明', + transportLeader: '周海', + transportContact: '13800001001', + receivePerson: '陈静', + receiveTime: '2026-01-12 18:20', + receiveAreaType: '停车场', + parkingOrAddress: '南山科技园停车场', + createDate: '2026-01-09 16:45' + }, + { + id: 'th2', + transferDate: '2026-01-15 09:30', + departRegionText: '广东省-广州市', + receiveRegionText: '浙江省-杭州市', + bizApprovalStatus: '审批完成', + opsApprovalStatus: '审批完成', + method: '司机驾驶', + reason: '项目调度需要', + vehicles: [ + { brand: '东风', model: 'DFH1180', plateNo: '粤A12345' } + ], + transferPerson: '王芳', + transportLeader: '王芳', + transportContact: '13800001002', + receivePerson: '赵强', + receiveTime: '2026-01-17 14:10', + receiveAreaType: '其他', + parkingOrAddress: '杭州市西湖区文三路 88 号', + createDate: '2026-01-14 18:00' + }, + { + id: 'th3', + transferDate: '2026-01-20 16:05', + departRegionText: '浙江省-宁波市', + receiveRegionText: '广东省-深圳市', + bizApprovalStatus: '审批完成', + opsApprovalStatus: '审批完成', + method: '第三方运输', + reason: '场站资源调整', + vehicles: [ + { brand: '蔚来', model: 'ET5', plateNo: '浙B22222' }, + { brand: '比亚迪', model: '汉', plateNo: '粤B11111' } + ], + transferPerson: '李华', + transportLeader: '黄蕾', + transportContact: '13800001003', + receivePerson: '陈静', + receiveTime: '2026-01-22 10:30', + receiveAreaType: '停车场', + parkingOrAddress: '南山科技园停车场', + createDate: '2026-01-20 15:15' + }, + { + id: 'th4', + transferDate: '2026-01-25 11:20', + departRegionText: '上海市-上海市', + receiveRegionText: '广东省-东莞市', + bizApprovalStatus: '审批完成', + opsApprovalStatus: '审批完成', + method: '第三方运输', + reason: '跨区域调拨', + vehicles: [ + { brand: '福田', model: 'BJ1180', plateNo: '沪A30003' } + ], + transferPerson: '张明', + transportLeader: '周海', + transportContact: '13800001004', + receivePerson: '王芳', + receiveTime: '2026-01-27 19:45', + receiveAreaType: '停车场', + parkingOrAddress: '松山湖停车场', + createDate: '2026-01-25 10:40' + }, + { + id: 'th5', + transferDate: '2026-01-28 08:50', + departRegionText: '广东省-深圳市', + receiveRegionText: '浙江省-嘉兴市', + bizApprovalStatus: '审批完成', + opsApprovalStatus: '审批完成', + method: '司机驾驶', + reason: '项目新增点位支持', + vehicles: [ + { brand: '比亚迪', model: '汉', plateNo: '粤B11111' }, + { brand: '小鹏', model: 'P7', plateNo: '浙A11111' } + ], + transferPerson: '陈静', + transportLeader: '陈静', + transportContact: '13800001005', + receivePerson: '李华', + receiveTime: '2026-01-30 16:10', + receiveAreaType: '其他', + parkingOrAddress: '嘉兴市秀洲区中山西路 66 号', + createDate: '2026-01-28 08:05' + }, + { + id: 'th6', + transferDate: '2026-02-02 14:30', + departRegionText: '浙江省-杭州市', + receiveRegionText: '上海市-上海市', + bizApprovalStatus: '审批完成', + opsApprovalStatus: '审批完成', + method: '第三方运输', + reason: '场站容量调整', + vehicles: [ + { brand: '小鹏', model: 'P7', plateNo: '浙A11111' } + ], + transferPerson: '赵强', + transportLeader: '周海', + transportContact: '13800001006', + receivePerson: '张明', + receiveTime: '2026-02-04 09:20', + receiveAreaType: '停车场', + parkingOrAddress: '张江园区停车场', + createDate: '2026-02-02 13:45' + }, + { + id: 'th7', + transferDate: '2026-02-05 10:10', + departRegionText: '广东省-东莞市', + receiveRegionText: '广东省-深圳市', + bizApprovalStatus: '审批完成', + opsApprovalStatus: '审批完成', + method: '司机驾驶', + reason: '场站资源调整', + vehicles: [ + { brand: '东风', model: 'DFH1180', plateNo: '粤A12345' }, + { brand: '福田', model: 'BJ1180', plateNo: '沪A30003' } + ], + transferPerson: '李华', + transportLeader: '李华', + transportContact: '13800001007', + receivePerson: '陈静', + receiveTime: '2026-02-07 18:30', + receiveAreaType: '停车场', + parkingOrAddress: '南山科技园停车场', + createDate: '2026-02-05 09:20' + }, + { + id: 'th8', + transferDate: '2026-02-08 15:40', + departRegionText: '上海市-上海市', + receiveRegionText: '浙江省-宁波市', + bizApprovalStatus: '审批完成', + opsApprovalStatus: '审批完成', + method: '第三方运输', + reason: '项目调度需要', + vehicles: [ + { brand: '福田', model: 'BJ1180', plateNo: '沪A30003' } + ], + transferPerson: '张明', + transportLeader: '周海', + transportContact: '13800001008', + receivePerson: '王芳', + receiveTime: '2026-02-10 11:15', + receiveAreaType: '停车场', + parkingOrAddress: '宁波江北停车场', + createDate: '2026-02-08 14:50' + }, + { + id: 'th9', + transferDate: '2026-02-12 09:00', + departRegionText: '浙江省-嘉兴市', + receiveRegionText: '广东省-广州市', + bizApprovalStatus: '审批完成', + opsApprovalStatus: '审批完成', + method: '第三方运输', + reason: '车辆调拨支持新项目', + vehicles: [ + { brand: '小鹏', model: 'P7', plateNo: '浙A11111' }, + { brand: '蔚来', model: 'ET5', plateNo: '浙B22222' } + ], + transferPerson: '陈静', + transportLeader: '黄蕾', + transportContact: '13800001009', + receivePerson: '赵强', + receiveTime: '2026-02-14 17:20', + receiveAreaType: '其他', + parkingOrAddress: '广州市海珠区新港中路 18 号', + createDate: '2026-02-12 08:15' + }, + { + id: 'th10', + transferDate: '2026-02-16 13:10', + departRegionText: '广东省-深圳市', + receiveRegionText: '上海市-上海市', + bizApprovalStatus: '审批完成', + opsApprovalStatus: '审批完成', + method: '司机驾驶', + reason: '项目调度需要', + vehicles: [ + { brand: '比亚迪', model: '汉', plateNo: '粤B11111' } + ], + transferPerson: '王芳', + transportLeader: '王芳', + transportContact: '13800001010', + receivePerson: '李华', + receiveTime: '2026-02-18 09:40', + receiveAreaType: '停车场', + parkingOrAddress: '张江园区停车场', + createDate: '2026-02-16 12:20' + } + ]; + }, []); + + function matchRange(valStr, range) { + if (!range || !range[0] || !range[1]) return true; + var s = ''; + var e = ''; + if (range[0] && range[0].format) s = range[0].format('YYYY-MM-DD'); + if (range[1] && range[1].format) e = range[1].format('YYYY-MM-DD'); + if (!s && !e) return true; + var v = String(valStr || '').slice(0, 10); + if (s && v < s) return false; + if (e && v > e) return false; + return true; + } + + function matchRegionText(text, cascaderVal) { + if (!cascaderVal || cascaderVal.length < 2) return true; + var map = { + zhejiang: '浙江省', shanghai: '上海市', guangdong: '广东省', + hangzhou: '杭州市', ningbo: '宁波市', jiaxing: '嘉兴市', + guangzhou: '广州市', shenzhen: '深圳市', dongguan: '东莞市' + }; + var pt = map[cascaderVal[0]] || ''; + var ct = map[cascaderVal[1]] || ''; + return String(text || '').indexOf(pt + '-' + ct) >= 0; + } + + function applyFilters(list, isHistory) { + return (list || []).filter(function (r) { + if (applied.transferPerson && r.transferPerson !== applied.transferPerson) return false; + if (applied.receivePerson && r.receivePerson !== applied.receivePerson) return false; + if (applied.method && r.method !== applied.method) return false; + if (applied.bizApprovalStatus && r.bizApprovalStatus !== applied.bizApprovalStatus) return false; + if (applied.opsApprovalStatus && r.opsApprovalStatus !== applied.opsApprovalStatus) return false; + if (!matchRange(r.transferDate, applied.transferDateRange)) return false; + if (isHistory) { + if (!matchRange(r.receiveTime, applied.receiveDateRange)) return false; + } + if (!matchRegionText(r.departRegionText, applied.departRegion)) return false; + if (!matchRegionText(r.receiveRegionText, applied.receiveRegion)) return false; + return true; + }); + } + + var filteredInProgress = useMemo(function () { return applyFilters(listInProgress, false); }, [listInProgress, applied]); + var filteredHistory = useMemo(function () { return applyFilters(listHistory, true); }, [listHistory, applied]); + + var pagedInProgress = useMemo(function () { + var start = (page - 1) * pageSize; + return filteredInProgress.slice(start, start + pageSize); + }, [filteredInProgress, page, pageSize]); + + var pagedHistory = useMemo(function () { + var start = (page - 1) * pageSize; + return filteredHistory.slice(start, start + pageSize); + }, [filteredHistory, page, pageSize]); + + var handleQuery = useCallback(function () { + setApplied(Object.assign({}, draft)); + setPage(1); + }, [draft]); + + var handleReset = useCallback(function () { + var empty = { + transferDateRange: null, + transferPerson: undefined, + receiveDateRange: null, + receivePerson: undefined, + departRegion: undefined, + receiveRegion: undefined, + method: undefined, + bizApprovalStatus: undefined, + opsApprovalStatus: undefined + }; + setDraft(empty); + setApplied(empty); + setPage(1); + }, []); + + var handleAdd = useCallback(function () { message.info('跳转调拨管理-新增(原型)'); }, []); + var handleExport = useCallback(function () { message.success('导出(原型)'); }, []); + + var columnsInProgress = useMemo(function () { + return [ + { title: '调拨日期', dataIndex: 'transferDate', key: 'transferDate', width: 160, render: function (v) { return fmtYMDHM(v); } }, + { title: '业务审批状态', dataIndex: 'bizApprovalStatus', key: 'bizApprovalStatus', width: 110 }, + { + title: '运维审批状态', + dataIndex: 'opsApprovalStatus', + key: 'opsApprovalStatus', + width: 110, + render: function (v, r) { + if (String(r && r.bizApprovalStatus ? r.bizApprovalStatus : '') === '待提交') return '-'; + return v || '-'; + } + }, + { title: '出发区域', dataIndex: 'departRegionText', key: 'departRegionText', width: 130 }, + { title: '接收区域', dataIndex: 'receiveRegionText', key: 'receiveRegionText', width: 130 }, + { title: '调拨原因', dataIndex: 'reason', key: 'reason', width: 160, ellipsis: true }, + { + title: '车辆数', + key: 'vehicleCount', + width: 90, + render: function (_, r) { + var vs = (r && r.vehicles) || []; + var cnt = vs.length; + if (!cnt) return '-'; + var content = React.createElement('div', { style: { width: 320 } }, + React.createElement('div', { style: { fontWeight: 600, marginBottom: 8 } }, '车辆明细'), + React.createElement(List, { + size: 'small', + dataSource: vs, + renderItem: function (it) { + return React.createElement(List.Item, null, + React.createElement('div', { style: { display: 'flex', width: '100%', gap: 8 } }, + React.createElement('div', { style: { flex: 1, minWidth: 0 } }, (it && it.brand) || '-'), + React.createElement('div', { style: { flex: 1, minWidth: 0 } }, (it && it.model) || '-'), + React.createElement('div', { style: { width: 110, textAlign: 'right' } }, (it && it.plateNo) || '-') + ) + ); + } + }) + ); + return React.createElement(Popover, { content: content, trigger: 'click', placement: 'bottomLeft' }, + React.createElement(Button, { type: 'link', size: 'small' }, String(cnt)) + ); + } + }, + { title: '调拨人', dataIndex: 'transferPerson', key: 'transferPerson', width: 100 }, + { + title: '调拨方式', + dataIndex: 'method', + key: 'method', + width: 110, + render: function (v) { return v === '司机驾驶' ? '司机运输' : (v || '-'); } + }, + { title: '运输负责人', dataIndex: 'transportLeader', key: 'transportLeader', width: 110 }, + { title: '运输方联系方式', dataIndex: 'transportContact', key: 'transportContact', width: 130 }, + { title: '接收人员', dataIndex: 'receivePerson', key: 'receivePerson', width: 100 }, + { title: '创建日期', dataIndex: 'createDate', key: 'createDate', width: 160, render: function (v) { return fmtYMDHM(v); } }, + { + title: '操作', + key: 'action', + width: 340, + fixed: 'right', + render: function (_, r) { + var bizSt = String(r && r.bizApprovalStatus ? r.bizApprovalStatus : ''); + var opsSt = String(r && r.opsApprovalStatus ? r.opsApprovalStatus : ''); + var showEdit = bizSt === '待提交' || bizSt === '撤回' || bizSt === '审批驳回' || opsSt === '待提交' || opsSt === '撤回' || opsSt === '审批驳回'; + var canRevoke = bizSt === '审批中'; + var rowId = r && r.id; + var showTransferInfo = bizSt === '审批完成' && (!opsSt || opsSt === '待提交'); + var showReceiveInfo = opsSt === '审批中' || opsSt === '审批完成'; + return React.createElement('span', { style: { display: 'inline-flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' } }, + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { message.info('跳转调拨管理-查看页(原型)ID:' + rowId); } }, '查看'), + showEdit ? React.createElement(Button, { type: 'link', size: 'small', onClick: function () { message.info('跳转调拨管理-编辑页(原型)ID:' + rowId); } }, '编辑') : null, + showTransferInfo ? React.createElement(Button, { type: 'link', size: 'small', onClick: function () { message.info('跳转调拨管理-调拨信息页(原型)ID:' + rowId); } }, '调拨信息') : null, + showReceiveInfo ? React.createElement(Button, { type: 'link', size: 'small', onClick: function () { message.info('跳转调拨管理-接收信息页(原型)ID:' + rowId); } }, '接收信息') : null, + canRevoke ? React.createElement(Button, { + type: 'link', + size: 'small', + danger: true, + onClick: function () { + Modal.confirm({ + title: '是否确认撤回?', + content: '确认后该调拨单审批状态将变更为「撤回」。若该流程存在多个分支工作流,将按撤回节点回退到对应的前置操作状态(原型展示)。', + okText: '确认', + cancelText: '取消', + onOk: function () { + setListInProgress(function (prev) { + return (prev || []).map(function (row) { + if (row && row.id === rowId) return Object.assign({}, row, { bizApprovalStatus: '撤回', opsApprovalStatus: '撤回' }); + return row; + }); + }); + message.success('已撤回,业务/运维审批状态已更新为撤回'); + } + }); + } + }, '撤回') : null + ); + } + } + ]; + }, [setListInProgress]); + + var columnsHistory = useMemo(function () { + return [ + { title: '调拨日期', dataIndex: 'transferDate', key: 'transferDate', width: 160, render: function (v) { return fmtYMDHM(v); } }, + { title: '业务审批状态', dataIndex: 'bizApprovalStatus', key: 'bizApprovalStatus', width: 110 }, + { + title: '运维审批状态', + dataIndex: 'opsApprovalStatus', + key: 'opsApprovalStatus', + width: 110, + render: function (v, r) { + if (String(r && r.bizApprovalStatus ? r.bizApprovalStatus : '') === '待提交') return '-'; + return v || '-'; + } + }, + { title: '出发区域', dataIndex: 'departRegionText', key: 'departRegionText', width: 130 }, + { title: '接收区域(省市)', dataIndex: 'receiveRegionText', key: 'receiveRegionText', width: 140 }, + { title: '调拨原因', dataIndex: 'reason', key: 'reason', width: 160, ellipsis: true }, + { + title: '车辆数', + key: 'vehicleCount', + width: 90, + render: function (_, r) { + var vs = (r && r.vehicles) || []; + var cnt = vs.length; + if (!cnt) return '-'; + var content = React.createElement('div', { style: { width: 320 } }, + React.createElement('div', { style: { fontWeight: 600, marginBottom: 8 } }, '车辆明细'), + React.createElement(List, { + size: 'small', + dataSource: vs, + renderItem: function (it) { + return React.createElement(List.Item, null, + React.createElement('div', { style: { display: 'flex', width: '100%', gap: 8 } }, + React.createElement('div', { style: { flex: 1, minWidth: 0 } }, (it && it.brand) || '-'), + React.createElement('div', { style: { flex: 1, minWidth: 0 } }, (it && it.model) || '-'), + React.createElement('div', { style: { width: 110, textAlign: 'right' } }, (it && it.plateNo) || '-') + ) + ); + } + }) + ); + return React.createElement(Popover, { content: content, trigger: 'click', placement: 'bottomLeft' }, + React.createElement(Button, { type: 'link', size: 'small' }, String(cnt)) + ); + } + }, + { title: '调拨人', dataIndex: 'transferPerson', key: 'transferPerson', width: 100 }, + { + title: '调拨方式', + dataIndex: 'method', + key: 'method', + width: 110, + render: function (v) { return v === '司机驾驶' ? '司机运输' : (v || '-'); } + }, + { title: '运输负责人', dataIndex: 'transportLeader', key: 'transportLeader', width: 110 }, + { title: '运输方联系方式', dataIndex: 'transportContact', key: 'transportContact', width: 130 }, + { title: '接收人员', dataIndex: 'receivePerson', key: 'receivePerson', width: 100 }, + { title: '接收时间', dataIndex: 'receiveTime', key: 'receiveTime', width: 160, render: function (v) { return fmtYMDHM(v); } }, + { title: '接收区域', dataIndex: 'receiveAreaType', key: 'receiveAreaType', width: 100 }, + { title: '停车场/停车地址', dataIndex: 'parkingOrAddress', key: 'parkingOrAddress', width: 180, ellipsis: true }, + { title: '创建日期', dataIndex: 'createDate', key: 'createDate', width: 160, render: function (v) { return fmtYMDHM(v); } }, + { + title: '操作', + key: 'action', + width: 100, + fixed: 'right', + render: function (_, r) { + return React.createElement(Button, { type: 'link', size: 'small', onClick: function () { message.info('跳转调拨管理-查看页(原型)ID:' + r.id); } }, '查看'); + } + } + ]; + }, []); + + var filterItems = [ + React.createElement('div', { key: 'transferDate', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '调拨日期'), + React.createElement(RangePicker, { style: filterControlStyle, value: draft.transferDateRange, onChange: function (v) { setDraft(function (p) { return Object.assign({}, p, { transferDateRange: v }); }); }, placeholder: ['开始日期', '结束日期'] }) + ), + React.createElement('div', { key: 'transferPerson', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '调拨人'), + React.createElement(Select, { placeholder: '请输入或选择调拨人', style: filterControlStyle, value: draft.transferPerson, onChange: function (v) { setDraft(function (p) { return Object.assign({}, p, { transferPerson: v }); }); }, allowClear: true, showSearch: true, options: userOptions, filterOption: filterOption }) + ), + React.createElement('div', { key: 'receiveDate', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '接收日期'), + React.createElement(RangePicker, { style: filterControlStyle, value: draft.receiveDateRange, onChange: function (v) { setDraft(function (p) { return Object.assign({}, p, { receiveDateRange: v }); }); }, placeholder: ['开始日期', '结束日期'] }) + ), + React.createElement('div', { key: 'receivePerson', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '接收人'), + React.createElement(Select, { placeholder: '请输入或选择接收人', style: filterControlStyle, value: draft.receivePerson, onChange: function (v) { setDraft(function (p) { return Object.assign({}, p, { receivePerson: v }); }); }, allowClear: true, showSearch: true, options: userOptions, filterOption: filterOption }) + ), + React.createElement('div', { key: 'departRegion', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '出发区域'), + React.createElement(Cascader, { placeholder: '请选择出发区域', style: filterControlStyle, options: regionOptions, value: draft.departRegion, onChange: function (v) { setDraft(function (p) { return Object.assign({}, p, { departRegion: v }); }); }, allowClear: true }) + ), + React.createElement('div', { key: 'receiveRegion', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '调拨区域'), + React.createElement(Cascader, { placeholder: '请选择调拨区域', style: filterControlStyle, options: regionOptions, value: draft.receiveRegion, onChange: function (v) { setDraft(function (p) { return Object.assign({}, p, { receiveRegion: v }); }); }, allowClear: true }) + ), + React.createElement('div', { key: 'method', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '调拨方式'), + React.createElement(Select, { placeholder: '请选择调拨方式', style: filterControlStyle, value: draft.method, onChange: function (v) { setDraft(function (p) { return Object.assign({}, p, { method: v }); }); }, allowClear: true, options: methodOptions }) + ), + React.createElement('div', { key: 'bizApprovalStatus', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '业务审批状态'), + React.createElement(Select, { placeholder: '请选择业务审批状态', style: filterControlStyle, value: draft.bizApprovalStatus, onChange: function (v) { setDraft(function (p) { return Object.assign({}, p, { bizApprovalStatus: v }); }); }, allowClear: true, options: approvalOptions }) + ), + React.createElement('div', { key: 'opsApprovalStatus', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '运维审批状态'), + React.createElement(Select, { placeholder: '请选择运维审批状态', style: filterControlStyle, value: draft.opsApprovalStatus, onChange: function (v) { setDraft(function (p) { return Object.assign({}, p, { opsApprovalStatus: v }); }); }, allowClear: true, options: approvalOptions }) + ) + ]; + + var filterCount = filterExpanded ? 9 : 3; + var filterNodes = []; + for (var i = 0; i < filterCount && i < filterItems.length; i++) filterNodes.push(filterItems[i]); + + var tabItems = useMemo(function () { + return [ + { + key: 'in_progress', + label: '进行中', + children: React.createElement(Table, { + rowKey: 'id', + columns: columnsInProgress, + dataSource: pagedInProgress, + scroll: { x: 1850 }, + size: 'small', + pagination: { + current: page, + pageSize: pageSize, + total: filteredInProgress.length, + showSizeChanger: true, + showQuickJumper: true, + showTotal: function (t) { return '共 ' + t + ' 条'; }, + onChange: function (pg, ps) { setPage(pg); if (ps) setPageSize(ps); } + } + }) + }, + { + key: 'history', + label: '历史记录', + children: React.createElement(Table, { + rowKey: 'id', + columns: columnsHistory, + dataSource: pagedHistory, + scroll: { x: 2100 }, + size: 'small', + pagination: { + current: page, + pageSize: pageSize, + total: filteredHistory.length, + showSizeChanger: true, + showQuickJumper: true, + showTotal: function (t) { return '共 ' + t + ' 条'; }, + onChange: function (pg, ps) { setPage(pg); if (ps) setPageSize(ps); } + } + }) + } + ]; + }, [columnsInProgress, columnsHistory, pagedInProgress, pagedHistory, page, pageSize, filteredInProgress.length, filteredHistory.length]); + + return React.createElement(App, null, + React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16, flexWrap: 'wrap', gap: 8 } }, + React.createElement(Breadcrumb, { items: [{ title: '运维管理' }, { title: '车辆业务' }, { title: '调拨管理' }] }), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { setRequirementModalOpen(true); } }, '查看需求说明') + ), + + React.createElement(Modal, { + title: '需求说明', + open: requirementModalOpen, + onCancel: function () { setRequirementModalOpen(false); }, + width: 720, + footer: React.createElement(Button, { onClick: function () { setRequirementModalOpen(false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6, color: 'rgba(0,0,0,0.85)' } }, requirementDocContent)), + + React.createElement(Card, { style: { marginBottom: 16 } }, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px 24px', alignItems: 'start', flex: 1, minWidth: 0 } }, filterNodes), + React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 } }, + React.createElement(Button, { onClick: handleReset }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleQuery }, '查询'), + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { setFilterExpanded(!filterExpanded); } }, filterExpanded ? '收起' : '展开') + ) + ), + + React.createElement(Card, null, + React.createElement(React.Fragment, null, + React.createElement('style', null, tableSingleLineStyle), + React.createElement('div', { className: 'transfer-list-table' }, + React.createElement(Tabs, { + activeKey: tab, + onChange: function (k) { setTab(k); setPage(1); }, + tabBarExtraContent: React.createElement('div', { style: { display: 'flex', gap: 8 } }, + React.createElement(Button, { type: 'primary', onClick: handleAdd }, '新增'), + React.createElement(Button, { onClick: handleExport }, '导出') + ), + items: tabItems + }) + ) + ) + ) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-ops/pages/17-故障管理-编辑.jsx b/src/prototypes/oneos-web-ops/pages/17-故障管理-编辑.jsx new file mode 100644 index 0000000..1fcb01c --- /dev/null +++ b/src/prototypes/oneos-web-ops/pages/17-故障管理-编辑.jsx @@ -0,0 +1,269 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// ONEOS-web - 运维管理 - 车辆业务 - 故障管理-编辑(已回填数据,可修改) + +const Component = function () { + var antd = window.antd; + var Button = antd.Button; + var Input = antd.Input; + var Select = antd.Select; + var DatePicker = antd.DatePicker; + var Form = antd.Form; + var Row = antd.Row; + var Col = antd.Col; + var Breadcrumb = antd.Breadcrumb; + var Layout = antd.Layout; + var message = antd.message; + var Card = antd.Card; + var Upload = antd.Upload; + + var _form = Form.useForm(); + var faultForm = _form[0]; + + // 故障等级枚举 + var faultLevelOptions = [ + { label: '特急', value: '特急' }, + { label: '紧急', value: '紧急' }, + { label: '一般', value: '一般' }, + { label: '提示', value: '提示' } + ]; + + // 故障类型枚举 + var faultTypeOptions = [ + { label: '底盘故障', value: '底盘故障' }, + { label: '三电故障', value: '三电故障' }, + { label: '整车系统', value: '整车系统' }, + { label: '燃料电池系统故障', value: '燃料电池系统故障' }, + { label: '供氢系统故障', value: '供氢系统故障' }, + { label: '空调系统故障', value: '空调系统故障' }, + { label: '冷机故障', value: '冷机故障' }, + { label: '其他故障', value: '其他故障' } + ]; + + // 故障来源枚举 + var faultSourceOptions = [ + { label: '客户报告', value: '客户报告' }, + { label: '定期保养', value: '定期保养' }, + { label: '司机操作问题', value: '司机操作问题' } + ]; + + // 解决情况枚举 + var resolveStatusOptions = [ + { label: '未解决', value: '未解决' }, + { label: '临时排故', value: '临时排故' }, + { label: '已解决', value: '已解决' } + ]; + + var plateOptions = [ + { label: '沪A12345', value: '沪A12345', brand: '一汽解放', model: 'J6P', company: '上海羚牛', vin: 'LNW1234567890ABCD' }, + { label: '浙B88888', value: '浙B88888', brand: '东风商用车', model: '天龙', company: '浙江羚牛', vin: 'LNW0987654321EFGH' }, + { label: '苏C66666', value: '苏C66666', brand: '福田欧曼', model: 'EST', company: '苏州冷链速运有限公司', vin: 'LNW1357924680IJKL' }, + { label: '沪D99999', value: '沪D99999', brand: '陕汽重卡', model: '德龙', company: '上海城配物流有限公司', vin: 'LNW2468013579MNOP' } + ]; + + var PlusIcon = function() { return React.createElement('svg', { viewBox: '0 0 1024 1024', width: 14, height: 14, fill: 'currentColor' }, React.createElement('path', { d: 'M482 152h60q8 0 8 8v704q0 8-8 8h-60q-8 0-8-8V160q0-8 8-8z' }), React.createElement('path', { d: 'M192 474h672q8 0 8 8v60q0 8-8 8H192q-8 0-8-8v-60q0-8 8-8z' })); }; + + var handleBack = function () { + message.info('返回列表'); + }; + + // 编辑态:模拟从列表带入的已存在数据(实际接入路由/接口后替换为 record) + React.useEffect(function () { + var initial = { + code: 'F-2024-001', + plate: '沪A12345', + brand: '一汽解放', + model: 'J6P', + company: '上海羚牛', + vin: 'LNW1234567890ABCD', + type: '底盘故障', + source: '客户报告', + level: '紧急', + reportTime: '2024-05-10 08:30:00', + status: '未解决', + desc: '车辆重载下制动踏板偏软,制动距离明显变长' + }; + if (initial.reportTime) { + if (typeof window.dayjs === 'function') { + initial.reportTime = window.dayjs(initial.reportTime); + } else if (typeof window.moment === 'function') { + initial.reportTime = window.moment(initial.reportTime); + } + } + faultForm.setFieldsValue(initial); + }, []); + + return React.createElement(Layout, { className: 'arco-theme-overrides', style: { minHeight: '100vh', background: '#f2f3f5', fontFamily: 'Inter, Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif' } }, + React.createElement('style', null, ` + .arco-theme-overrides .ant-btn { border-radius: 4px; } + .arco-theme-overrides .ant-btn-primary { background-color: #165dff; border-color: #165dff; } + .arco-theme-overrides .ant-btn-primary:hover { background-color: #4080ff; border-color: #4080ff; } + + .arco-grouped-form-page { display: flex; flex-direction: column; min-height: 100vh; } + .arco-grouped-form-page-content { flex: 1; padding: 16px 20px 24px; } + .arco-grouped-form-page .ant-card { margin-bottom: 16px; border-radius: 4px; border: none; box-shadow: 0 1px 2px rgba(0,0,0,0.06); } + .arco-grouped-form-page .ant-card-head { border-bottom: none; padding: 20px 24px 0; min-height: auto; } + .arco-grouped-form-page .ant-card-head-title { font-size: 16px; font-weight: 500; color: #1d2129; padding: 0; } + .arco-grouped-form-page .ant-card-body { padding: 24px; } + .arco-grouped-form-page .ant-form-vertical .ant-form-item-label { padding-bottom: 8px; height: auto; line-height: 1.5715; } + .arco-grouped-form-page .ant-form-item { margin-bottom: 24px; } + + .arco-grouped-form-page .ant-input, + .arco-grouped-form-page .ant-select-selector, + .arco-grouped-form-page .ant-picker, + .arco-grouped-form-page .ant-input-affix-wrapper { background-color: #f2f3f5; border: 1px solid #e5e6eb; border-radius: 2px; transition: all 0.1s cubic-bezier(0, 0, 1, 1); } + .arco-grouped-form-page .ant-input:hover, + .arco-grouped-form-page .ant-select:not(.ant-select-disabled):hover .ant-select-selector, + .arco-grouped-form-page .ant-picker:hover, + .arco-grouped-form-page .ant-input-affix-wrapper:hover { background-color: #f2f3f5; border-color: #165dff; } + .arco-grouped-form-page .ant-input:focus, + .arco-grouped-form-page .ant-input-focused, + .arco-grouped-form-page .ant-select-focused .ant-select-selector, + .arco-grouped-form-page .ant-picker-focused, + .arco-grouped-form-page .ant-input-affix-wrapper-focused { background-color: #fff; border: 1px solid #165dff !important; box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.2) !important; outline: 0; } + .arco-grouped-form-page .ant-input[disabled], + .arco-grouped-form-page .ant-select-disabled .ant-select-selector, + .arco-grouped-form-page .ant-picker-disabled { color: #86909c; background-color: #f2f3f5; border-color: #e5e6eb; cursor: not-allowed; } + .arco-grouped-form-page .ant-input-affix-wrapper[disabled] { background-color: #f2f3f5; border-color: #e5e6eb; cursor: not-allowed; } + .arco-grouped-form-page .ant-input-affix-wrapper > input.ant-input { background-color: transparent; } + .arco-grouped-form-page .ant-input-affix-wrapper > input.ant-input:focus { background-color: transparent; box-shadow: none !important; border: none !important; } + + .arco-grouped-form-footer { background: #fff; padding: 16px 24px; border-top: 1px solid #e5e6eb; display: flex; justify-content: flex-end; align-items: center; gap: 12px; position: sticky; bottom: 0; z-index: 100; box-shadow: 0 -2px 10px rgba(0,0,0,0.05); } + .arco-grouped-form-footer .ant-btn { border-radius: 5px; height: 32px; padding: 4px 16px; font-size: 14px; } + + .arco-theme-overrides .ant-breadcrumb { color: #86909c; font-size: 14px; white-space: nowrap; flex-shrink: 0; margin-bottom: 20px; } + .arco-theme-overrides .ant-breadcrumb a { color: #4e5969; } + .arco-theme-overrides .ant-breadcrumb a:hover { color: #165dff; background-color: transparent; } + .arco-theme-overrides .ant-form-item-label > label { color: #4e5969; white-space: nowrap; } + .arco-theme-overrides .ant-form-item-label > label::after { display: none !important; content: "" !important; margin: 0 !important; } + `), + React.createElement('div', { className: 'arco-grouped-form-page' }, + React.createElement('div', { className: 'arco-grouped-form-page-content' }, + React.createElement(Breadcrumb, { + separator: React.createElement('span', { style: { color: '#c9cdd4' } }, '/'), + items: [ + { title: '首页' }, + { title: '运维管理' }, + { title: '车辆业务' }, + { title: React.createElement('a', { onClick: function(e) { e.preventDefault(); handleBack(); } }, '故障管理') }, + { title: React.createElement('span', { style: { color: '#1d2129' } }, '编辑故障单') } + ] + }), + + React.createElement(Form, { form: faultForm, layout: 'vertical' }, + React.createElement(Card, { title: '车辆信息', bordered: false }, + React.createElement(Row, { gutter: 24 }, + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '故障编码', name: 'code' }, + React.createElement(Input, { disabled: true, style: { backgroundColor: '#f2f3f5', color: '#86909c' } }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '车牌号', name: 'plate', rules: [{ required: true, message: '请选择车牌号' }] }, + React.createElement(Select, { + placeholder: '请选择车牌号', + options: plateOptions, + showSearch: true, + onChange: function(val, option) { + if (option) { + faultForm.setFieldsValue({ + brand: option.brand, + model: option.model, + company: option.company, + vin: option.vin + }); + } else { + faultForm.setFieldsValue({ brand: undefined, model: undefined, company: undefined, vin: undefined }); + } + } + }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '车辆品牌', name: 'brand' }, + React.createElement(Input, { placeholder: '自动带入', disabled: true, style: { backgroundColor: '#f2f3f5', color: '#86909c' } }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '车辆型号', name: 'model' }, + React.createElement(Input, { placeholder: '自动带入', disabled: true, style: { backgroundColor: '#f2f3f5', color: '#86909c' } }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '运营公司', name: 'company' }, + React.createElement(Input, { placeholder: '自动带入', disabled: true, style: { backgroundColor: '#f2f3f5', color: '#86909c' } }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '车辆识别代码', name: 'vin' }, + React.createElement(Input, { placeholder: '自动带入', disabled: true, style: { backgroundColor: '#f2f3f5', color: '#86909c' } }) + ) + ) + ) + ), + + React.createElement(Card, { title: '故障信息', bordered: false }, + React.createElement(Row, { gutter: 24 }, + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '故障类型', name: 'type', rules: [{ required: true, message: '请选择故障类型' }] }, + React.createElement(Select, { placeholder: '请选择故障类型', options: faultTypeOptions }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '故障来源', name: 'source', rules: [{ required: true, message: '请选择故障来源' }] }, + React.createElement(Select, { placeholder: '请选择故障来源', options: faultSourceOptions }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '故障等级', name: 'level', rules: [{ required: true, message: '请选择故障等级' }] }, + React.createElement(Select, { placeholder: '请选择故障等级', options: faultLevelOptions }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '故障上报时间', name: 'reportTime', rules: [{ required: true, message: '请选择故障上报时间' }] }, + React.createElement(DatePicker, { style: { width: '100%' }, placeholder: '请选择上报时间', showTime: true }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '解决情况', name: 'status', rules: [{ required: true, message: '请选择故障解决情况' }] }, + React.createElement(Select, { placeholder: '请选择故障解决情况', options: resolveStatusOptions }) + ) + ) + ) + ), + + React.createElement(Card, { title: '故障证据与描述', bordered: false }, + React.createElement(Row, { gutter: 24 }, + React.createElement(Col, { span: 24 }, + React.createElement(Form.Item, { label: '故障描述', name: 'desc', rules: [{ required: true, message: '请填写故障描述' }] }, + React.createElement(Input.TextArea, { + placeholder: '在何种状态下,产生何种现象,导致何种事故', + style: { height: 80, minHeight: 80, resize: 'none' } + }) + ) + ), + React.createElement(Col, { span: 24 }, + React.createElement(Form.Item, { label: '故障证据', name: 'evidence' }, + React.createElement(Upload, { listType: 'picture-card' }, + React.createElement('div', null, + React.createElement(PlusIcon, null), + React.createElement('div', { style: { marginTop: 8 } }, '上传文件') + ) + ), + React.createElement('div', { style: { fontSize: 12, color: '#86909c', marginTop: 8 } }, '支持上传照片、视频、录音') + ) + ) + ) + ) + ) + ), + React.createElement('div', { className: 'arco-grouped-form-footer' }, + React.createElement(Button, { onClick: handleBack, style: { borderRadius: 5 } }, '取消'), + React.createElement(Button, { type: 'primary', onClick: function () { faultForm.submit(); message.success('保存成功'); }, style: { borderRadius: 5 } }, '保存') + ) + ) + ); +}; + +if (typeof module !== 'undefined' && module.exports) module.exports = Component; + +export default Component; diff --git a/src/prototypes/oneos-web-ops/pages/18-故障管理.jsx b/src/prototypes/oneos-web-ops/pages/18-故障管理.jsx new file mode 100644 index 0000000..488afaa --- /dev/null +++ b/src/prototypes/oneos-web-ops/pages/18-故障管理.jsx @@ -0,0 +1,505 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// ONEOS-web - 运维管理 - 车辆业务 - 故障管理(列表与表单) + +const Component = function () { + var useState = React.useState; + var useEffect = React.useEffect; + + var antd = window.antd; + var Table = antd.Table; + var Button = antd.Button; + var Space = antd.Space; + var Input = antd.Input; + var Select = antd.Select; + var DatePicker = antd.DatePicker; + var Form = antd.Form; + var Row = antd.Row; + var Col = antd.Col; + var Divider = antd.Divider; + var Breadcrumb = antd.Breadcrumb; + var Layout = antd.Layout; + var message = antd.message; + var Tag = antd.Tag; + var Card = antd.Card; + var Modal = antd.Modal; + var Tabs = antd.Tabs; + var Tooltip = antd.Tooltip; + var RangePicker = DatePicker.RangePicker; + + var _filterFormInst = Form.useForm(); + var filterForm = _filterFormInst[0]; + + var _specOpen = useState(false); + var specModalOpen = _specOpen[0]; + var setSpecModalOpen = _specOpen[1]; + + var _listTab = useState('pending'); + var listTab = _listTab[0]; + var setListTab = _listTab[1]; + + var _tableRows = useState(null); + var tableRowsOverride = _tableRows[0]; + var setTableRowsOverride = _tableRows[1]; + + var faultLevelOptions = [ + { label: 'L1-特急', value: '特急' }, + { label: 'L2-紧急', value: '紧急' }, + { label: 'L3-一般', value: '一般' }, + { label: 'L4-提示', value: '提示' } + ]; + + var lastOperatorOptions = [ + { label: '王婷婷', value: '王婷婷' }, + { label: '刘若楠', value: '刘若楠' }, + { label: '陈嘉豪', value: '陈嘉豪' }, + { label: '赵海峰', value: '赵海峰' } + ]; + + // 故障类型枚举 + var faultTypeOptions = [ + { label: '底盘故障', value: '底盘故障' }, + { label: '三电故障', value: '三电故障' }, + { label: '整车系统', value: '整车系统' }, + { label: '燃料电池系统故障', value: '燃料电池系统故障' }, + { label: '供氢系统故障', value: '供氢系统故障' }, + { label: '空调系统故障', value: '空调系统故障' }, + { label: '冷机故障', value: '冷机故障' }, + { label: '其他故障', value: '其他故障' } + ]; + + // 解决情况枚举 + var resolveStatusOptions = [ + { label: '未解决', value: '未解决' }, + { label: '临时排故', value: '临时排故' }, + { label: '已解决', value: '已解决' } + ]; + + var SearchIcon = function() { return React.createElement('svg', { viewBox: '0 0 1024 1024', width: 14, height: 14, fill: 'currentColor' }, React.createElement('path', { d: 'M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z' })); }; + var FileTextIcon = function() { return React.createElement('svg', { viewBox: '0 0 1024 1024', width: 16, height: 16, fill: 'currentColor' }, React.createElement('path', { d: 'M854.6 288.7L639.4 73.4c-6-6-14.2-9.4-22.7-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM790.2 326H602V137.8L790.2 326zm1.8 562H232V136h302v216c0 22.1 17.9 40 40 40h216v494zM504 618H320c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8zM702 458H320c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h382c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z' })); }; + var ResetIcon = function() { return React.createElement('svg', { viewBox: '0 0 1024 1024', width: 14, height: 14, fill: 'currentColor' }, React.createElement('path', { d: 'M793 242H366v-74c0-6.7-7.7-10.4-12.9-6.3l-142 112a8 8 0 000 12.6l142 112c5.2 4.1 12.9.4 12.9-6.3v-74h415v470H175c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h618c35.3 0 64-28.7 64-64V306c0-35.3-28.7-64-64-64z' })); }; + var PlusIcon = function() { return React.createElement('svg', { viewBox: '0 0 1024 1024', width: 14, height: 14, fill: 'currentColor' }, React.createElement('path', { d: 'M482 152h60q8 0 8 8v704q0 8-8 8h-60q-8 0-8-8V160q0-8 8-8z' }), React.createElement('path', { d: 'M192 474h672q8 0 8 8v60q0 8-8 8H192q-8 0-8-8v-60q0-8 8-8z' })); }; + var DownloadIcon = function() { return React.createElement('svg', { viewBox: '0 0 1024 1024', width: 16, height: 16, fill: 'currentColor' }, React.createElement('path', { d: 'M505.7 661a8 8 0 0012.6 0l112-141.7c4.1-5.2.4-12.9-6.3-12.9h-74.1V168c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v338.3H400c-6.7 0-10.4 7.7-6.3 12.9l112 141.8zM878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8z' })); }; + var ListIcon = function() { return React.createElement('svg', { viewBox: '0 0 48 48', width: 14, height: 14, fill: 'none', stroke: 'currentColor', strokeWidth: 4, strokeLinecap: 'butt', strokeLinejoin: 'miter' }, React.createElement('path', { d: 'M17 12H42' }), React.createElement('path', { d: 'M17 24H42' }), React.createElement('path', { d: 'M17 36H42' }), React.createElement('path', { d: 'M8 12H9' }), React.createElement('path', { d: 'M8 24H9' }), React.createElement('path', { d: 'M8 36H9' })); }; + var UploadIcon = function() { return React.createElement('svg', { viewBox: '0 0 1024 1024', width: 14, height: 14, fill: 'currentColor' }, React.createElement('path', { d: 'M512 256l-256 256h160v256h192V512h160L512 256zM256 832v64h512v-64H256z' })); }; + + var levelLabelMap = { 特急: 'L1-特急', 紧急: 'L2-紧急', 一般: 'L3-一般', 提示: 'L4-提示' }; + var renderFaultLevel = function (text) { + var color = 'default'; + if (text === '特急') color = 'red'; + else if (text === '紧急') color = 'orange'; + else if (text === '一般') color = 'blue'; + else if (text === '提示') color = 'green'; + var label = levelLabelMap[text] || text; + return React.createElement(Tag, { color: color }, label); + }; + + var renderResolveStatus = function (text) { + var dot = '#86909c'; + if (text === '已解决') dot = '#00b42a'; + else if (text === '临时排故') dot = '#ff7d00'; + else if (text === '未解决') dot = '#f53f3f'; + return React.createElement(Space, { size: 6 }, + React.createElement('span', { style: { display: 'inline-block', width: 6, height: 6, borderRadius: '50%', backgroundColor: dot } }), + React.createElement('span', null, text) + ); + }; + + var columns = [ + { title: '故障编码', dataIndex: 'code', key: 'code', width: 132, fixed: 'left' }, + { title: '解决情况', dataIndex: 'status', key: 'status', width: 120, render: renderResolveStatus }, + { title: '车牌号码', dataIndex: 'plate', key: 'plate', width: 112 }, + { title: '车辆品牌', dataIndex: 'brand', key: 'brand', width: 100, ellipsis: true }, + { title: '车辆型号', dataIndex: 'model', key: 'model', width: 100, ellipsis: true }, + { title: '运营公司', dataIndex: 'company', key: 'company', width: 140, ellipsis: true }, + { title: '故障等级', dataIndex: 'level', key: 'level', width: 108, render: renderFaultLevel }, + { title: '故障类型', dataIndex: 'type', key: 'type', width: 128, ellipsis: true }, + { + title: '故障描述', + dataIndex: 'desc', + key: 'desc', + width: 200, + ellipsis: { showTitle: false }, + render: function (text) { + return React.createElement(Tooltip, { placement: 'topLeft', title: text }, + React.createElement('span', { style: { cursor: 'default' } }, text) + ); + } + }, + { title: '故障上报时间', dataIndex: 'reportTime', key: 'reportTime', width: 176, ellipsis: true, className: 'fault-col-report-time' }, + { title: '最后操作时间', dataIndex: 'lastOperationTime', key: 'lastOperationTime', width: 176, ellipsis: true }, + { title: '最后操作人', dataIndex: 'lastOperator', key: 'lastOperator', width: 104, ellipsis: true }, + { + title: '操作', + key: 'action', + fixed: 'right', + width: listTab === 'history' ? 72 : 104, + render: function (_, record) { + if (listTab === 'history') { + return React.createElement(Button, { type: 'link', size: 'small', style: { padding: '0 4px' }, onClick: function () { handleView(record); } }, '查看'); + } + return React.createElement(Space, { size: 0, split: React.createElement('span', { style: { color: '#e5e6eb' } }, '|') }, + React.createElement(Button, { type: 'link', size: 'small', style: { padding: '0 4px' }, onClick: function () { handleView(record); } }, '查看'), + React.createElement(Button, { type: 'link', size: 'small', style: { padding: '0 4px' }, onClick: function () { handleEdit(record); } }, '编辑') + ); + } + } + ]; + + var ALL_DATA = [ + { key: '1', code: 'F-2024-001', plate: '沪A12345', brand: '一汽解放', model: 'J6P', company: '上海羚牛', type: '底盘故障', level: '紧急', source: '客户报告', status: '未解决', reportTime: '2024-05-10 08:30:00', lastOperator: '王婷婷', lastOperationTime: '2024-05-10 09:00:00', desc: '车辆重载下制动踏板偏软,制动距离明显变长' }, + { key: '2', code: 'F-2024-002', plate: '浙B88888', brand: '东风商用车', model: '天龙', company: '浙江羚牛', type: '三电故障', level: '特急', source: '司机操作问题', status: '临时排故', reportTime: '2024-05-11 14:15:00', lastOperator: '刘若楠', lastOperationTime: '2024-05-11 16:20:00', desc: '高压系统绝缘报警频繁触发,车辆进入限扭模式' }, + { key: '3', code: 'F-2024-003', plate: '苏C66666', brand: '福田欧曼', model: 'EST', company: '苏州冷链速运有限公司', type: '整车系统', level: '一般', source: '定期保养', status: '已解决', reportTime: '2024-05-12 10:00:00', lastOperator: '陈嘉豪', lastOperationTime: '2024-05-12 11:30:00', desc: '后桥轮胎偏磨,建议更换并做四轮定位' }, + { key: '4', code: 'F-2024-004', plate: '沪D99999', brand: '陕汽重卡', model: '德龙', company: '上海城配物流有限公司', type: '空调系统故障', level: '提示', source: '客户报告', status: '已解决', reportTime: '2024-05-13 09:45:00', lastOperator: '赵海峰', lastOperationTime: '2024-05-13 10:10:00', desc: '空调制冷效果差,出风口温度偏高' } + ]; + + var parseReportTime = function (str) { + if (!str) return null; + var p = str.replace(/-/g, '/'); + var t = new Date(p); + return isNaN(t.getTime()) ? null : t; + }; + + var applyListFilter = function () { + var v = filterForm.getFieldsValue(); + var rows = ALL_DATA.filter(function (r) { + if (listTab === 'pending') return r.status !== '已解决'; + return r.status === '已解决'; + }); + if (v.filterPlate && String(v.filterPlate).trim()) { + var q = String(v.filterPlate).trim(); + rows = rows.filter(function (r) { return (r.plate || '').indexOf(q) !== -1; }); + } + if (v.filterLevels && v.filterLevels.length) { + rows = rows.filter(function (r) { return v.filterLevels.indexOf(r.level) !== -1; }); + } + if (v.filterTypes && v.filterTypes.length) { + rows = rows.filter(function (r) { return v.filterTypes.indexOf(r.type) !== -1; }); + } + if (v.filterLastOperator) { + rows = rows.filter(function (r) { return r.lastOperator === v.filterLastOperator; }); + } + if (v.filterDateRange && v.filterDateRange.length === 2 && v.filterDateRange[0] && v.filterDateRange[1]) { + var start = v.filterDateRange[0]; + var end = v.filterDateRange[1]; + var startMs = start.valueOf ? start.valueOf() : new Date(start).getTime(); + var endMs = end.valueOf ? end.valueOf() : new Date(end).getTime(); + var startDay = new Date(startMs); + startDay.setHours(0, 0, 0, 0); + startMs = startDay.getTime(); + var endDay = new Date(endMs); + endDay.setHours(23, 59, 59, 999); + endMs = endDay.getTime(); + rows = rows.filter(function (r) { + var rt = parseReportTime(r.reportTime); + if (!rt) return false; + var m = rt.getTime(); + return m >= startMs && m <= endMs; + }); + } + setTableRowsOverride(rows); + }; + + useEffect(function () { + applyListFilter(); + }, [listTab]); + + var displayData = tableRowsOverride !== null && tableRowsOverride !== undefined + ? tableRowsOverride + : ALL_DATA.filter(function (r) { + if (listTab === 'pending') return r.status !== '已解决'; + return r.status === '已解决'; + }); + + var handleCreate = function () { + message.info('跳转到新增故障页面'); + }; + + var handleEdit = function (record) { + message.info('跳转到编辑故障页面'); + }; + + var handleView = function (record) { + message.info('跳转到查看故障页面'); + }; + + + var formItemLayout = { + labelAlign: 'left', + colon: false, + labelCol: { flex: '0 0 100px' }, + wrapperCol: { flex: '1 1 0' } + }; + + var specSection = function (title, children) { + return React.createElement('div', { style: { marginBottom: 16 } }, + React.createElement('div', { style: { fontWeight: 600, color: '#1d2129', marginBottom: 8, fontSize: 14 } }, title), + children + ); + }; + var specLine = function (text) { + return React.createElement('div', { style: { fontSize: 13, color: '#4e5969', lineHeight: 1.75, paddingLeft: 0 } }, text); + }; + + var renderSpecModal = function () { + return React.createElement(Modal, { + title: '故障列表页 — 需求说明', + open: specModalOpen, + onCancel: function () { setSpecModalOpen(false); }, + footer: React.createElement(Button, { type: 'primary', onClick: function () { setSpecModalOpen(false); } }, '知道了'), + width: 720, + centered: true, + destroyOnClose: true + }, + React.createElement('div', { style: { maxHeight: '70vh', overflowY: 'auto', paddingRight: 4 } }, + specSection('2. 故障列表页', React.createElement(React.Fragment, null, + specLine('用于展示待处理与历史记录,支持多条件检索、查看、编辑、删除、导入导出。') + )), + specSection('2.1 顶部筛选区(支持单条件或多条件组合)', React.createElement(React.Fragment, null, + specLine('2.1.1 车牌号:输入框,支持模糊搜索;'), + specLine('2.1.2 故障等级:多选下拉,选项:L1-特急、L2-紧急、L3-一般、L4-提示;'), + specLine('2.1.3 上报时间:日期范围选择(开始~结束);'), + specLine('2.1.4 故障类型:多选下拉,选项为故障类型枚举;'), + specLine('2.1.5 最后操作人:下拉选择器;'), + specLine('2.1.6 查询按钮:执行筛选;'), + specLine('2.1.7 重置按钮:清空筛选条件。') + )), + specSection('2.2 列表工具栏', React.createElement(React.Fragment, null, + specLine('2.2.1 新建:进入故障表单页;'), + specLine('2.2.2 导出:导出当前筛选结果;'), + specLine('2.2.3 导入:批量导入故障数据;'), + specLine('2.2.4 展示设置:字段展示配置(当前版本可预留)。') + )), + specSection('2.3 Tab分区', React.createElement(React.Fragment, null, + specLine('2.3.1 待处理;'), + specLine('2.3.2 历史记录。') + )), + specSection('2.4 列表字段展示(当前版本)', React.createElement(React.Fragment, null, + specLine('故障编码、解决情况、车牌号码、车辆品牌、车辆型号、运营公司、故障等级、故障类型、故障描述(省略展示,悬停显示完整)、故障上报时间、最后操作时间、最后操作人、操作(待处理:查看/编辑;历史记录:仅查看)。'), + specLine('说明:车辆识别代码不在列表展示,仅在表单车辆信息中展示。') + )), + specSection('2.5 状态与操作规则', React.createElement(React.Fragment, null, + specLine('2.5.1 解决情况状态值:临时排故、已解决、未解决;'), + specLine('2.5.2 操作列:待处理 Tab 为查看、编辑(无删除);历史记录 Tab 仅查看;'), + specLine('2.5.3 分页:每页10条(固定分页)。') + )) + ) + ); + }; + + return React.createElement(Layout, { className: 'arco-theme-overrides', style: { minHeight: '100vh', background: '#f2f3f5', fontFamily: 'Inter, Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif' } }, + React.createElement('style', null, ` + .arco-theme-overrides .ant-btn { border-radius: 4px; } + .arco-theme-overrides .ant-btn-primary { background-color: #165dff; border-color: #165dff; } + .arco-theme-overrides .ant-btn-primary:hover { background-color: #4080ff; border-color: #4080ff; } + .arco-theme-overrides .ant-btn-link { color: #165dff; } + .arco-theme-overrides .ant-btn-link:hover { background-color: transparent; color: #4080ff; } + + .arco-theme-overrides .ant-table-thead > tr > th { + background-color: #f2f3f5; + color: #909399; + font-weight: 400; + font-size: 14px; + height: 40px; + padding: 9px 16px; + line-height: 22px; + box-sizing: border-box; + border-bottom: 1px solid #e5e6eb; + border-top: none; + vertical-align: middle; + } + .arco-theme-overrides .ant-table-thead > tr > th .ant-table-column-title { + font-size: 14px; + color: #909399; + line-height: 22px; + min-height: 22px; + white-space: nowrap; + } + .arco-theme-overrides .ant-table-thead > tr > th::before { display: none !important; } + .arco-theme-overrides .ant-table-tbody > tr > td { border-bottom: 1px solid #e5e6eb; padding: 13px 16px; color: #4e5969; vertical-align: middle; line-height: 22px; } + .arco-theme-overrides .ant-table-tbody > tr { height: 52px; } + .arco-theme-overrides .customer-table-scroll-wrap { width: 100%; min-width: 0; max-width: 100%; } + .arco-theme-overrides .ant-table-wrapper { border: none; } + .arco-theme-overrides .ant-table { border: none; } + .arco-theme-overrides .ant-table-container { border: none; } + .arco-theme-overrides .ant-table-pagination.ant-pagination { margin: 16px 0 0 0; } + + .arco-theme-overrides .ant-card { border: none; border-radius: 4px; } + .arco-theme-overrides .customer-page-card.ant-card { border: 1px solid #e5e6eb !important; border-radius: 4px; box-shadow: 0 1px 2px rgba(0,0,0,0.06); background: #fff; } + .arco-theme-overrides .customer-page-card .ant-card-body { padding: 20px 24px 24px; } + + .arco-theme-overrides .ant-input, .arco-theme-overrides .ant-select-selector, .arco-theme-overrides .ant-picker, .arco-theme-overrides .ant-input-affix-wrapper { border-radius: 2px; border: 1px solid #e5e6eb; background-color: #fff; transition: all 0.1s cubic-bezier(0, 0, 1, 1); } + .arco-theme-overrides .ant-input:hover, .arco-theme-overrides .ant-select:not(.ant-select-disabled):hover .ant-select-selector, .arco-theme-overrides .ant-picker:hover, .arco-theme-overrides .ant-input-affix-wrapper:hover { background-color: #fff; border-color: #165dff; } + .arco-theme-overrides .ant-input:focus, .arco-theme-overrides .ant-input-focused, .arco-theme-overrides .ant-select-focused .ant-select-selector, .arco-theme-overrides .ant-picker-focused, .arco-theme-overrides .ant-input-affix-wrapper-focused { background-color: #fff; border: 1px solid #165dff !important; box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.2) !important; outline: 0; } + + .arco-theme-overrides .ant-breadcrumb { color: #86909c; font-size: 14px; white-space: nowrap; flex-shrink: 0; } + .arco-theme-overrides .customer-page-header-row { display: flex; align-items: center; flex-wrap: nowrap; gap: 0; margin-bottom: 20px; } + .arco-theme-overrides .ant-breadcrumb a { color: #4e5969; } + .arco-theme-overrides .ant-breadcrumb a:hover { color: #165dff; background-color: transparent; } + + .arco-theme-overrides .ant-form-item-label { padding: 0 16px 0 0 !important; line-height: 32px; height: 32px; display: flex; align-items: center; } + .arco-theme-overrides .ant-form-item-control { display: flex; align-items: center; line-height: 32px; min-height: 32px; } + .arco-theme-overrides .ant-form-item-control-input { min-height: 32px; width: 100%; display: flex; align-items: center; } + .arco-theme-overrides .ant-form-item-control-input-content { display: flex; align-items: center; width: 100%; height: 100%; } + .arco-theme-overrides .ant-select { width: 100%; height: 32px; } + .arco-theme-overrides .ant-select-selector { height: 32px !important; min-height: 32px !important; display: flex; align-items: center; width: 100%; padding: 0 12px; } + .arco-theme-overrides .ant-input { height: 32px; padding: 4px 12px; } + .arco-theme-overrides .ant-input-affix-wrapper { height: 32px; padding: 0 12px; display: flex; align-items: center; } + .arco-theme-overrides .ant-input-affix-wrapper > input.ant-input, + .arco-theme-overrides .ant-input-affix-wrapper > input.ant-input:focus, + .arco-theme-overrides .ant-input-affix-wrapper > input.ant-input:hover { height: 100%; padding: 0; border: none !important; box-shadow: none !important; outline: none !important; background-color: transparent !important; } + .arco-theme-overrides .ant-picker { height: 32px !important; min-height: 32px !important; display: flex; align-items: center; padding: 4px 12px; width: 100%; } + .arco-theme-overrides .ant-form-item-label > label { color: #4e5969; white-space: nowrap; } + .arco-theme-overrides .ant-form-item-label > label::after { display: none !important; content: "" !important; margin: 0 !important; } + .arco-theme-overrides .ant-select-multiple .ant-select-selector { height: auto !important; min-height: 32px !important; align-items: center; padding-top: 2px; padding-bottom: 2px; } + .arco-theme-overrides .fault-col-report-time { white-space: nowrap; } + `), + React.createElement('div', { style: { padding: '16px 20px 24px', display: 'flex', flexDirection: 'column', flex: 1 } }, + React.createElement(Card, { className: 'customer-page-card', bordered: false }, + React.createElement('div', { className: 'customer-page-header-row', style: { marginBottom: 16, justifyContent: 'space-between', width: '100%' } }, + React.createElement(Breadcrumb, { + separator: React.createElement('span', { style: { color: '#c9cdd4' } }, '/'), + items: [ + { title: React.createElement(ListIcon, { style: { display: 'inline-flex', alignItems: 'center', fontSize: 14, transform: 'translate(-2px, 1px)' } }) }, + { title: '运维管理' }, + { title: '车辆业务' }, + { title: React.createElement('span', { style: { color: '#1d2129', fontWeight: 700, fontSize: 16, lineHeight: '22px' } }, '故障管理') } + ] + }), + React.createElement(Button, { + type: 'link', + icon: React.createElement(FileTextIcon, null), + style: { display: 'flex', alignItems: 'center', gap: 4, padding: '0 4px', color: '#165dff', fontWeight: 500 }, + onClick: function () { setSpecModalOpen(true); } + }, '查看需求说明') + ), + React.createElement('div', { style: { fontSize: 18, fontWeight: 600, color: '#1d2129', lineHeight: '26px', marginBottom: 20 } }, '故障管理'), + + // 搜索表单区域 + React.createElement('div', { style: { marginBottom: 0 } }, + React.createElement(Row, { style: { flexWrap: 'nowrap', alignItems: 'stretch' } }, + React.createElement(Col, { flex: 1, style: { minWidth: 0, paddingRight: 40 } }, + React.createElement(Form, Object.assign({ layout: 'horizontal', form: filterForm }, formItemLayout), + React.createElement(Row, { gutter: 24, style: { rowGap: 0 } }, + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { name: 'filterPlate', label: '车牌号', style: { marginBottom: 16 } }, + React.createElement(Input, { placeholder: '支持模糊搜索', allowClear: true }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { name: 'filterLevels', label: '故障等级', style: { marginBottom: 16 } }, + React.createElement(Select, { + mode: 'multiple', + allowClear: true, + placeholder: '多选:L1~L4', + options: faultLevelOptions, + maxTagCount: 'responsive' + }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { name: 'filterDateRange', label: '上报时间', style: { marginBottom: 16 } }, + React.createElement(RangePicker, { style: { width: '100%' }, showTime: false, placeholder: ['开始', '结束'] }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { name: 'filterTypes', label: '故障类型', style: { marginBottom: 16 } }, + React.createElement(Select, { + mode: 'multiple', + allowClear: true, + placeholder: '多选故障类型', + options: faultTypeOptions, + maxTagCount: 'responsive' + }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { name: 'filterLastOperator', label: '最后操作人', style: { marginBottom: 16 } }, + React.createElement(Select, { + allowClear: true, + placeholder: '请选择', + options: lastOperatorOptions + }) + ) + ) + ) + ) + ), + React.createElement(Divider, { + type: 'vertical', + style: { + alignSelf: 'stretch', + height: 'auto', + minHeight: 100, + borderLeftColor: 'rgb(229, 230, 235)', + borderLeftStyle: 'dashed', + marginLeft: 20, + marginRight: 20 + } + }), + React.createElement(Col, { flex: 'none', style: { textAlign: 'right', display: 'flex', flexDirection: 'column', alignItems: 'flex-end', minWidth: 100, maxWidth: 168 } }, + React.createElement(Space, { direction: 'vertical', size: 12, style: { width: '100%', alignItems: 'flex-end' } }, + React.createElement(Button, { type: 'primary', icon: React.createElement(SearchIcon, null), style: { display: 'flex', alignItems: 'center', gap: 6, justifyContent: 'center', width: 86 }, onClick: function () { applyListFilter(); message.success('已按条件筛选'); } }, '查询'), + React.createElement(Button, { + icon: React.createElement(ResetIcon, null), + style: { display: 'flex', alignItems: 'center', gap: 6, justifyContent: 'center', width: 86 }, + onClick: function () { + filterForm.resetFields(); + applyListFilter(); + message.info('已清空筛选条件'); + } + }, '重置') + ) + ) + ) + ), + React.createElement(Divider, { style: { margin: '16px 0 20px', borderColor: '#e5e6eb' } }), + + // 工具栏 + 表格 + React.createElement('div', { style: { display: 'flex', flexDirection: 'column', flex: 1, minWidth: 0 } }, + React.createElement(Row, { justify: 'space-between', align: 'middle', style: { marginBottom: 16 } }, + React.createElement(Col, null, + React.createElement(Space, { size: 12 }, + React.createElement(Button, { type: 'primary', icon: React.createElement(PlusIcon, null), style: { display: 'flex', alignItems: 'center', gap: 6 }, onClick: handleCreate }, '新建'), + React.createElement(Button, { + style: { padding: '4px 10px', height: 32, display: 'flex', alignItems: 'center', gap: 6, color: '#4e5969' }, + onClick: function () { message.info('已导出当前筛选结果'); } + }, React.createElement(DownloadIcon, null), React.createElement('span', null, '导出')), + React.createElement(Button, { icon: React.createElement(UploadIcon, null), style: { display: 'flex', alignItems: 'center', gap: 6 }, onClick: function () { message.info('批量导入故障数据'); } }, '导入'), + React.createElement(Button, { style: { color: '#4e5969' }, onClick: function () { message.info('字段展示配置(当前版本预留)'); } }, '展示设置') + ) + ), + React.createElement(Col, null) + ), + React.createElement(Tabs, { + activeKey: listTab, + onChange: function (k) { + setListTab(k); + }, + style: { marginBottom: 12 }, + items: [ + { key: 'pending', label: '待处理' }, + { key: 'history', label: '历史记录' } + ] + }), + React.createElement('div', { className: 'customer-table-scroll-wrap', style: { flex: 1, minHeight: 0, minWidth: 0 } }, + React.createElement(Table, { + columns: columns, + dataSource: displayData, + pagination: { + pageSize: 10, + showSizeChanger: false, + showTotal: function (total) { return '共 ' + total + ' 条'; } + }, + scroll: { x: 2100 } + }) + ) + ) + ) + ), + renderSpecModal() + ); +}; + +if (typeof module !== 'undefined' && module.exports) module.exports = Component; + +export default Component; diff --git a/src/prototypes/oneos-web-ops/pages/19-还车管理-查看.jsx b/src/prototypes/oneos-web-ops/pages/19-还车管理-查看.jsx new file mode 100644 index 0000000..829213c --- /dev/null +++ b/src/prototypes/oneos-web-ops/pages/19-还车管理-查看.jsx @@ -0,0 +1,667 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 运维管理 - 车辆业务 - 还车管理 - 查看(只读) + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + var useRef = React.useRef; + var useEffect = React.useEffect; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Input = antd.Input; + var Select = antd.Select; + var Switch = antd.Switch; + var DatePicker = antd.DatePicker; + var Table = antd.Table; + var Button = antd.Button; + var Drawer = antd.Drawer; + var Modal = antd.Modal; + var message = antd.message; + + // ---------- utils ---------- + function RequiredLabel(text) { + return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 4 } }, + React.createElement('span', { style: { color: '#f5222d', fontWeight: 600 } }, '*'), + React.createElement('span', null, text) + ); + } + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + function makeThumb(url, onPreview, onRemove, disabled) { + return React.createElement('div', { + style: { + width: 64, + height: 64, + borderRadius: 4, + border: '1px solid #f0f0f0', + background: '#fafafa', + position: 'relative', + overflow: 'hidden' + } + }, + url ? React.createElement('img', { + src: url, + style: { width: '100%', height: '100%', objectFit: 'cover', cursor: disabled ? 'default' : 'pointer' }, + onClick: disabled ? undefined : onPreview + }) : null, + (disabled || !onRemove) ? null : React.createElement('div', { + style: { + position: 'absolute', + right: 6, + top: 6, + width: 20, + height: 20, + borderRadius: 999, + background: 'rgba(0,0,0,0.55)', + color: '#fff', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + cursor: 'pointer', + fontSize: 12, + userSelect: 'none' + }, + onClick: function (e) { e.stopPropagation(); onRemove && onRemove(); } + }, '×') + ); + } + + function UploadBox(props) { + var label = props.label; + var value = props.value || []; // array of {uid,name,url} + var max = props.max || 1; + var disabled = !!props.disabled; + var tip = props.tip; + + return React.createElement('div', null, + label ? React.createElement('div', { style: { fontSize: 12, color: '#666', marginBottom: 6 } }, label) : null, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' } }, + (value || []).map(function (f) { + return React.createElement('div', { key: f.uid }, + makeThumb( + f.url, + function () { previewState[1]({ open: true, url: f.url, title: f.name || '预览' }); }, + null, + false + ) + ); + }), + disabled ? null : ((value || []).length >= max ? null : React.createElement('div', { + style: { + width: 64, + height: 64, + borderRadius: 4, + border: '1px dashed #d9d9d9', + background: '#fff', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: '#999' + } + }, '上传')) + ), + tip ? React.createElement('div', { style: { marginTop: 6, fontSize: 12, color: '#999' } }, tip) : null + ); + } + + // ---------- styles ---------- + var layoutStyle = { padding: '16px 24px 88px', background: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; + var footerStyle = { + position: 'fixed', + left: 0, + right: 0, + bottom: 0, + background: '#fff', + borderTop: '1px solid #f0f0f0', + padding: '12px 24px', + display: 'flex', + justifyContent: 'flex-start', + gap: 12, + zIndex: 10 + }; + var styles = { + formRow: { display: 'flex', gap: 16, marginBottom: 12, alignItems: 'flex-start' }, + formItem: { flex: 1, minWidth: 0 }, + label: { fontSize: 12, color: '#666', marginBottom: 6 } + }; + + function FormItem(props) { + var label = props.label; + var required = !!props.required; + var fullWidth = !!props.fullWidth; + return React.createElement( + 'div', + { style: fullWidth ? Object.assign({}, styles.formItem, { flex: '0 0 100%' }) : styles.formItem }, + React.createElement('div', { style: styles.label }, required ? RequiredLabel(label) : label), + props.children + ); + } + + // ---------- mock data(与还车页一致) ---------- + var vehicleInfo = useMemo(function () { + return { + contractCode: 'HT-ZL-2025-001', + projectName: '嘉兴氢能示范项目', + customerName: '嘉兴某某物流有限公司', + businessDept: '华东业务部', + businessOwner: '李经理', + plateNo: '京A12345', + vehicleType: '重型厢式货车', + brand: '东风', + model: 'DFH1180', + deliveryTime: '2025-02-28 14:30', + deliveryRegion: '浙江省-嘉兴市', + deliveryAddress: '嘉兴市南湖区科技大道1号', + deliverySpareTireDepth: '6.50', + deliveryMileageKm: '15230.12', + deliveryBatteryKwh: '68.40', + deliveryHydrogenAmount: '28.30', + hydrogenUnit: 'MPa', + deliverySpareTirePhotoUrl: 'https://dummyimage.com/600x400/eee/666&text=交车备胎照片' + }; + }, []); + + var deliveryPhotos = useMemo(function () { + function u(txt) { return 'https://dummyimage.com/600x400/eee/666&text=' + encodeURIComponent(txt); } + return { + vehicle: { + '仪表盘': [{ uid: 'dv1', name: '仪表盘.jpg', url: u('交车-仪表盘') }], + '车辆正面': [{ uid: 'dv2', name: '车辆正面.jpg', url: u('交车-车辆正面') }], + '车辆左前方': [{ uid: 'dv3', name: '车辆左前方.jpg', url: u('交车-车辆左前方') }], + '车辆左后方': [{ uid: 'dv4', name: '车辆左后方.jpg', url: u('交车-车辆左后方') }], + '车辆右后方': [{ uid: 'dv5', name: '车辆右后方.jpg', url: u('交车-车辆右后方') }], + '车辆右前方': [{ uid: 'dv6', name: '车辆右前方.jpg', url: u('交车-车辆右前方') }] + }, + chassis: { + '正前方底部': [{ uid: 'dc1', name: '正前方底部.jpg', url: u('交车-正前方底部') }], + '左侧前方底部': [{ uid: 'dc2', name: '左侧前方底部.jpg', url: u('交车-左侧前方底部') }], + '左侧后方底部': [{ uid: 'dc3', name: '左侧后方底部.jpg', url: u('交车-左侧后方底部') }], + '正后方底部': [{ uid: 'dc4', name: '正后方底部.jpg', url: u('交车-正后方底部') }], + '右侧后方底部': [{ uid: 'dc5', name: '右侧后方底部.jpg', url: u('交车-右侧后方底部') }], + '右侧前方底部': [{ uid: 'dc6', name: '右侧前方底部.jpg', url: u('交车-右侧前方底部') }] + }, + tire: { + '左前轮': [{ uid: 'dt1', name: '左前轮.jpg', url: u('交车-左前轮') }], + '左后轮(内)': [{ uid: 'dt2', name: '左后轮(内).jpg', url: u('交车-左后轮(内)') }], + '左后轮(外)': [{ uid: 'dt3', name: '左后轮(外).jpg', url: u('交车-左后轮(外)') }], + '右前轮': [{ uid: 'dt4', name: '右前轮.jpg', url: u('交车-右前轮') }], + '右后轮(内)': [{ uid: 'dt5', name: '右后轮(内).jpg', url: u('交车-右后轮(内)') }], + '右后轮(外)': [{ uid: 'dt6', name: '右后轮(外).jpg', url: u('交车-右后轮(外)') }], + '备胎': [{ uid: 'dt7', name: '备胎.jpg', url: u('交车-备胎') }] + }, + defect: [ + { uid: 'dd1', name: '交车瑕疵1.jpg', url: u('交车-瑕疵1') }, + { uid: 'dd2', name: '交车瑕疵2.jpg', url: u('交车-瑕疵2') } + ], + other: [ + { uid: 'do1', name: '交车其他1.jpg', url: u('交车-其他1') }, + { uid: 'do2', name: '交车其他2.jpg', url: u('交车-其他2') } + ] + }; + }, []); + + // ---------- states ---------- + var previewState = useState({ open: false, url: '', title: '' }); + var requirementModalOpenState = useState(false); + var inspectionDrawerOpenState = useState(false); + + var [returnPhotos, setReturnPhotos] = useState(function () { + function u(txt) { return 'https://dummyimage.com/600x400/eee/666&text=' + encodeURIComponent(txt); } + return { + vehicle: {}, + chassis: {}, + tire: {}, + defect: [ + { uid: 'rd1', name: '还车瑕疵1.jpg', url: u('还车-瑕疵1') } + ], + other: [ + { uid: 'ro1', name: '还车其他1.jpg', url: u('还车-其他1') } + ] + }; + }); + + useEffect(function () { + setReturnPhotos(function (prev) { + var next = Object.assign({}, prev || {}); + ['vehicle', 'chassis', 'tire'].forEach(function (k) { + next[k] = Object.assign({}, next[k] || {}); + }); + Object.keys(deliveryPhotos.vehicle || {}).forEach(function (slot) { if (!next.vehicle[slot]) next.vehicle[slot] = []; }); + Object.keys(deliveryPhotos.chassis || {}).forEach(function (slot) { if (!next.chassis[slot]) next.chassis[slot] = []; }); + Object.keys(deliveryPhotos.tire || {}).forEach(function (slot) { if (!next.tire[slot]) next.tire[slot] = []; }); + return next; + }); + }, []); + + var [form] = useState(function () { + return { + returnPerson: '张三', + returnPhone: '13800138001', + returnIdCard: '320101199001018888', + returnPlaceType: 'parking', + returnPlaceName: '停车场A', + arriveTime: '2025-03-12 10:20', + spareTirePhoto: [{ uid: 'sp1', name: '还车备胎照片.jpg', url: 'https://dummyimage.com/600x400/eee/666&text=还车备胎照片' }], + spareTireDepth: '6.10', + returnMileageKm: '16280.50', + returnBatteryKwh: '60.20', + returnHydrogenAmount: '20.10', + hydrogenUnit: vehicleInfo.hydrogenUnit, + serviceFee: '120.00' + }; + }); + + // ---------- inspection checklist(只读) ---------- + var inspectionCategoryItems = useMemo(function () { + return { + '车灯': ['大灯', '转向灯', '小灯', '示廓灯', '刹车灯', '倒车灯', '牌照灯', '防雾灯', '室内灯'], + '仪表盘': ['氢系统指示', '电控系统指示', '数值清晰准确', '故障报警灯'], + '驾驶室': ['点烟器', '车窗升降', '按键开关', '雨刮器', '内后视镜是否正常', '内/外门把手', '安全带', '空调冷暖风', '仪表盘', '门锁功能', '手刹', '车钥匙功能是否正常', '喇叭', '音响功能', '遮阳板', '主副驾座椅', '方向盘', '内饰干净整洁'], + '轮胎': ['前左胎', '前右胎', '后左胎', '后右胎', '备胎'], + '液位检查': ['冷却液', '制动液', '玻璃水'], + '外观检查': ['车身外观', '漆面', '玻璃'], + '车辆外观': ['整车外观'], + '其他': ['其他检查项'], + '随车工具': ['三角牌', '灭火器', '反光背心'], + '随车证件': ['行驶证', '营运证', '保险单'], + '整车': ['整车状态'], + '燃料电池系统': ['氢系统', '储氢瓶'], + '冷机': ['冷机运行'], + '制动系统': ['制动踏板', '驻车制动'] + }; + }, []); + + function buildInspectionList() { + var list = []; + var categories = Object.keys(inspectionCategoryItems); + for (var i = 0; i < categories.length; i++) { + var cat = categories[i]; + var items = inspectionCategoryItems[cat] || []; + for (var j = 0; j < items.length; j++) { + var it = items[j]; + var isTire = cat === '轮胎'; + list.push({ + key: 'v-ins-' + i + '-' + j, + category: cat, + item: it, + checked: true, + treadDepth: isTire ? '6.50' : '', + remark: '' + }); + } + } + return list; + } + + var inspectionList = useMemo(function () { return buildInspectionList(); }, []); + var inspectionListRef = useRef(null); + inspectionListRef.current = inspectionList; + + var inspectionColumns = useMemo(function () { + return [ + { + title: '类别', + dataIndex: 'category', + key: 'category', + width: 140, + render: function (text, record, index) { + var rows = inspectionListRef.current || []; + var cat = record && record.category; + if (!cat) return { children: text, props: { rowSpan: 1 } }; + + var isFirst = true; + for (var i = index - 1; i >= 0; i--) { + if (!rows[i] || rows[i].category !== cat) break; + isFirst = false; + break; + } + if (!isFirst) return { children: null, props: { rowSpan: 0 } }; + + var span = 1; + for (var j = index + 1; j < rows.length; j++) { + if (!rows[j] || rows[j].category !== cat) break; + span++; + } + return { children: text, props: { rowSpan: span } }; + } + }, + { title: '检查项目', dataIndex: 'item', key: 'item', width: 220 }, + { + title: '检查情况', + dataIndex: 'checked', + key: 'checked', + width: 220, + render: function (_, record) { + var isTire = record && (record.category === '轮胎' || String(record.item || '').indexOf('胎纹') >= 0); + return isTire + ? React.createElement(Input, { value: record.treadDepth, disabled: true, addonAfter: 'mm' }) + : React.createElement(Switch, { checked: !!record.checked, disabled: true }); + } + }, + { + title: '备注', + dataIndex: 'remark', + key: 'remark', + render: function (_, record) { + return React.createElement(Input, { value: record.remark, disabled: true }); + } + } + ]; + }, []); + + // ---------- helpers ---------- + function PreviewDeliveryThumb(props) { + var url = props.url; + if (!url) return React.createElement('div', { style: { width: 64, height: 64, borderRadius: 4, border: '1px solid #f0f0f0', background: '#fafafa' } }); + return makeThumb(url, + function () { previewState[1]({ open: true, url: url, title: props.title || '交车照片预览' }); }, + null, + false + ); + } + + function ReturnPhotoGridColumn(title, slots, required, deliveryMap, returnMap) { + return React.createElement('div', { style: { background: '#fff', border: '1px solid #f0f0f0', borderRadius: 8, padding: 12 } }, + React.createElement('div', { style: { fontWeight: 600, marginBottom: 12 } }, required ? RequiredLabel(title) : title), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 12 } }, + (slots || []).map(function (slot) { + var deliveryArr = (deliveryMap && deliveryMap[slot]) || []; + var deliveryUrl = deliveryArr && deliveryArr[0] ? deliveryArr[0].url : ''; + var retArr = (returnMap && returnMap[slot]) || []; + return React.createElement('div', { key: slot }, + React.createElement('div', { style: { fontSize: 12, color: '#666', marginBottom: 6 } }, required ? RequiredLabel(slot) : slot), + React.createElement('div', { style: { display: 'flex', gap: 12, alignItems: 'flex-start', flexWrap: 'wrap' } }, + React.createElement('div', { style: { display: 'flex', flexDirection: 'column', alignItems: 'center' } }, + PreviewDeliveryThumb({ url: deliveryUrl, title: slot }), + React.createElement('div', { style: { fontSize: 11, color: '#999', marginTop: 6 } }, '交车时照片') + ), + React.createElement('div', { style: { display: 'flex', flexDirection: 'column', alignItems: 'center' } }, + retArr && retArr[0] + ? makeThumb(retArr[0].url, function () { previewState[1]({ open: true, url: retArr[0].url, title: retArr[0].name || '还车照片' }); }, null, false) + : React.createElement('div', { style: { width: 64, height: 64, borderRadius: 4, border: '1px dashed #d9d9d9', background: '#fff' } }), + React.createElement('div', { style: { fontSize: 11, color: '#999', marginTop: 6 } }, '还车照片') + ) + ) + ); + }) + ) + ); + } + + var handleBack = useCallback(function () { + message.info('返回还车管理页(原型)'); + }, []); + + // ---------- render ---------- + return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + React.createElement(Breadcrumb, { items: [{ title: '运维管理' }, { title: '车辆业务' }, { title: '还车管理' }, { title: '查看' }] }), + React.createElement(Button, { type: 'link', onClick: function () { requirementModalOpenState[1](true); } }, '查看需求说明') + ), + + React.createElement(Card, { title: '合同信息', style: cardStyle }, + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '合同编码' }, + React.createElement(Input, { value: vehicleInfo.contractCode, disabled: true }) + ), + React.createElement(FormItem, { label: '项目名称' }, + React.createElement(Input, { value: vehicleInfo.projectName, disabled: true }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '客户名称' }, + React.createElement(Input, { value: vehicleInfo.customerName, disabled: true }) + ), + React.createElement(FormItem, { label: '业务部门' }, + React.createElement(Input, { value: vehicleInfo.businessDept, disabled: true }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '业务负责人', fullWidth: true }, + React.createElement(Input, { value: vehicleInfo.businessOwner, disabled: true }) + ) + ) + ), + + React.createElement(Card, { title: '车辆信息', style: cardStyle }, + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '车牌号' }, + React.createElement(Input, { value: vehicleInfo.plateNo, disabled: true }) + ), + React.createElement(FormItem, { label: '车辆型号' }, + React.createElement(Input, { value: vehicleInfo.vehicleType, disabled: true }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '品牌' }, + React.createElement(Input, { value: vehicleInfo.brand, disabled: true }) + ), + React.createElement(FormItem, { label: '型号' }, + React.createElement(Input, { value: vehicleInfo.model, disabled: true }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '交车时间' }, + React.createElement(Input, { value: vehicleInfo.deliveryTime, disabled: true }) + ), + React.createElement(FormItem, { label: '交车地点(省-市)' }, + React.createElement(Input, { value: vehicleInfo.deliveryRegion, disabled: true }) + ) + ) + ), + + React.createElement(Card, { title: '还车明细', style: cardStyle }, + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '还车人', required: true }, + React.createElement(Input, { value: form.returnPerson, disabled: true }) + ), + React.createElement(FormItem, { label: '还车人电话', required: true }, + React.createElement(Input, { value: form.returnPhone, disabled: true }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '还车人身份证', required: true }, + React.createElement(Input, { value: form.returnIdCard, disabled: true }) + ), + React.createElement(FormItem, { label: '还车地点', required: true }, + React.createElement(Select, { + placeholder: '请选择还车地点', + value: form.returnPlaceType, + disabled: true, + options: [ + { value: 'parking', label: '停车场' }, + { value: 'repair', label: '维修站' } + ] + }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: form.returnPlaceType === 'parking' ? '停车场' : '维修站', required: true }, + React.createElement(Select, { + placeholder: '请选择', + value: form.returnPlaceName, + disabled: true, + showSearch: true, + filterOption: filterOption, + options: [{ value: form.returnPlaceName, label: form.returnPlaceName }] + }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '到达时间', required: true, fullWidth: true }, + React.createElement(DatePicker, { + showTime: { format: 'HH:mm' }, + format: 'YYYY-MM-DD HH:mm', + style: { width: '100%' }, + disabled: true, + value: (function () { + var dayjs = window.dayjs; + return form.arriveTime && dayjs ? dayjs(form.arriveTime) : null; + })() + }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '备胎照片', required: true, fullWidth: true }, + React.createElement('div', { style: { display: 'flex', gap: 16, alignItems: 'flex-start', flexWrap: 'wrap' } }, + React.createElement('div', { style: { display: 'flex', flexDirection: 'column', alignItems: 'center' } }, + makeThumb( + vehicleInfo.deliverySpareTirePhotoUrl, + function () { previewState[1]({ open: true, url: vehicleInfo.deliverySpareTirePhotoUrl, title: '交车时备胎照片' }); }, + null, + false + ), + React.createElement('div', { style: { fontSize: 11, color: '#999', marginTop: 6 } }, '交车时照片') + ), + React.createElement('div', { style: { flex: '1 1 240px', minWidth: 240 } }, + UploadBox({ label: null, value: form.spareTirePhoto, max: 1, disabled: true }) + ) + ) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '交车时备胎胎纹深度' }, + React.createElement(Input, { value: vehicleInfo.deliverySpareTireDepth, disabled: true, addonAfter: 'mm' }) + ), + React.createElement(FormItem, { label: '还车时备胎胎纹深度', required: true }, + React.createElement(Input, { value: form.spareTireDepth, disabled: true, addonAfter: 'mm' }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '交车里程(km)' }, + React.createElement(Input, { value: vehicleInfo.deliveryMileageKm, disabled: true, addonAfter: 'km' }) + ), + React.createElement(FormItem, { label: '还车里程(km)', required: true }, + React.createElement(Input, { value: form.returnMileageKm, disabled: true, addonAfter: 'km' }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '交车电量(kWh)' }, + React.createElement(Input, { value: vehicleInfo.deliveryBatteryKwh, disabled: true, addonAfter: 'kWh' }) + ), + React.createElement(FormItem, { label: '还车电量(kWh)', required: true }, + React.createElement(Input, { value: form.returnBatteryKwh, disabled: true, addonAfter: 'kWh' }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '交车氢量(' + form.hydrogenUnit + ')' }, + React.createElement(Input, { value: vehicleInfo.deliveryHydrogenAmount, disabled: true, addonAfter: form.hydrogenUnit }) + ), + React.createElement(FormItem, { label: '还车氢量(' + form.hydrogenUnit + ')', required: true }, + React.createElement(Input, { value: form.returnHydrogenAmount, disabled: true, addonAfter: form.hydrogenUnit }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '接车服务费' }, + React.createElement(Input, { value: form.serviceFee, disabled: true, addonAfter: '元' }) + ), + React.createElement('div', { style: styles.formItem }) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '还车检查单', fullWidth: true }, + React.createElement(Button, { onClick: function () { inspectionDrawerOpenState[1](true); } }, '还车检查单') + ) + ) + ), + + React.createElement(Card, { title: '还车照片', style: cardStyle }, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 12 } }, + ReturnPhotoGridColumn('车辆', Object.keys(deliveryPhotos.vehicle || {}), true, deliveryPhotos.vehicle, returnPhotos.vehicle), + ReturnPhotoGridColumn('底盘', Object.keys(deliveryPhotos.chassis || {}), true, deliveryPhotos.chassis, returnPhotos.chassis), + React.createElement('div', { style: { gridColumn: 'span 2' } }, + ReturnPhotoGridColumn('轮胎', Object.keys(deliveryPhotos.tire || {}), true, deliveryPhotos.tire, returnPhotos.tire) + ) + ), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 12, marginTop: 12 } }, + (function () { + var left = (deliveryPhotos.defect || []).slice(0, 4); + return React.createElement('div', { style: { background: '#fff', border: '1px solid #f0f0f0', borderRadius: 8, padding: 12 } }, + React.createElement('div', { style: { fontWeight: 600, marginBottom: 12 } }, '瑕疵'), + React.createElement('div', { style: { display: 'flex', gap: 16, alignItems: 'flex-start', flexWrap: 'wrap' } }, + React.createElement('div', { style: { width: 260 } }, + React.createElement('div', { style: { display: 'flex', gap: 12, flexWrap: 'wrap' } }, left.map(function (f) { + return React.createElement('div', { key: f.uid }, PreviewDeliveryThumb({ url: f.url, title: '交车瑕疵' })); + })), + React.createElement('div', { style: { fontSize: 12, color: '#999', marginTop: 6 } }, '交车时照片') + ), + React.createElement('div', { style: { flex: '1 1 220px', minWidth: 220 } }, + UploadBox({ label: null, value: returnPhotos.defect, max: 4, disabled: true, tip: '还车照片' }) + ) + ) + ); + })(), + (function () { + var left = (deliveryPhotos.other || []).slice(0, 4); + return React.createElement('div', { style: { background: '#fff', border: '1px solid #f0f0f0', borderRadius: 8, padding: 12 } }, + React.createElement('div', { style: { fontWeight: 600, marginBottom: 12 } }, '其他'), + React.createElement('div', { style: { display: 'flex', gap: 16, alignItems: 'flex-start', flexWrap: 'wrap' } }, + React.createElement('div', { style: { width: 260 } }, + React.createElement('div', { style: { display: 'flex', gap: 12, flexWrap: 'wrap' } }, left.map(function (f) { + return React.createElement('div', { key: f.uid }, PreviewDeliveryThumb({ url: f.url, title: '交车其他' })); + })), + React.createElement('div', { style: { fontSize: 12, color: '#999', marginTop: 6 } }, '交车时照片') + ), + React.createElement('div', { style: { flex: '1 1 220px', minWidth: 220 } }, + UploadBox({ label: null, value: returnPhotos.other, max: 4, disabled: true, tip: '还车照片' }) + ) + ) + ); + })() + ) + ), + + React.createElement('div', { style: footerStyle }, + React.createElement(Button, { onClick: handleBack }, '返回') + ), + + React.createElement(Modal, { + open: !!previewState[0].open, + title: previewState[0].title || '预览', + footer: null, + onCancel: function () { previewState[1]({ open: false, url: '', title: '' }); }, + width: 860 + }, + previewState[0].url ? React.createElement('img', { src: previewState[0].url, style: { width: '100%', maxHeight: '70vh', objectFit: 'contain' } }) : null + ), + + React.createElement(Drawer, { + open: inspectionDrawerOpenState[0], + title: '还车检查单', + width: 920, + placement: 'right', + onClose: function () { inspectionDrawerOpenState[1](false); }, + styles: { body: { display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden', paddingBottom: 0 } }, + footer: React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-start', gap: 8 } }, + React.createElement(Button, { type: 'primary', disabled: true }, '提交'), + React.createElement(Button, { onClick: function () { inspectionDrawerOpenState[1](false); } }, '返回') + ) + }, + React.createElement('div', { style: { flex: 1, minHeight: 0, overflow: 'auto' } }, + React.createElement(Table, { rowKey: 'key', columns: inspectionColumns, dataSource: inspectionList, pagination: false, bordered: true, size: 'small' }) + ) + ), + + React.createElement(Modal, { + open: requirementModalOpenState[0], + onCancel: function () { requirementModalOpenState[1](false); }, + onOk: function () { requirementModalOpenState[1](false); }, + title: '需求说明', + width: 860, + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, + React.createElement('div', { style: { whiteSpace: 'pre-wrap', lineHeight: 1.7 } }, + '还车管理-查看\\n一个「数字化资产ONEOS运管平台」中的「还车管理」「查看」模块\\n\\n说明:本页为只读查看态,所有控件禁用,仅用于查看还车单信息与照片。\\n\\n1.面包屑:\\n1.1.运维管理-车辆业务-还车管理-查看\\n\\n2.合同信息:合同编码、项目名称、客户名称、业务部门、业务负责人\\n3.车辆信息:车牌号、车辆型号、品牌、型号、交车时间、交车地点(省-市)\\n4.还车明细:展示还车人/电话/身份证、还车地点、停车场/维修站、到达时间、备胎照片与胎纹深度、里程/电量/氢量、接车服务费、还车检查单(可展开查看)\\n5.还车照片:左侧展示交车时照片(含“交车时照片”提示),右侧展示还车照片(只读可预览)\\n6.底部:返回按钮,返回还车管理页' + ) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-ops/pages/20-还车管理-还车.jsx b/src/prototypes/oneos-web-ops/pages/20-还车管理-还车.jsx new file mode 100644 index 0000000..8fae36c --- /dev/null +++ b/src/prototypes/oneos-web-ops/pages/20-还车管理-还车.jsx @@ -0,0 +1,1007 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 运维管理 - 车辆业务 - 还车管理 - 还车 + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + var useRef = React.useRef; + var useEffect = React.useEffect; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Input = antd.Input; + var Select = antd.Select; + var Switch = antd.Switch; + var DatePicker = antd.DatePicker; + var Table = antd.Table; + var Button = antd.Button; + var Drawer = antd.Drawer; + var Modal = antd.Modal; + var message = antd.message; + + var TextArea = Input.TextArea; + + // ---------- utils ---------- + function RequiredLabel(text) { + return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 4 } }, + React.createElement('span', { style: { color: '#f5222d', fontWeight: 600 } }, '*'), + React.createElement('span', null, text) + ); + } + + function isEmpty(v) { + return v === null || v === undefined || String(v).trim() === ''; + } + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + function fileToDataUrl(file, cb) { + try { + var reader = new FileReader(); + reader.onload = function (e) { cb(null, (e && e.target && e.target.result) || ''); }; + reader.onerror = function () { cb(new Error('read error')); }; + reader.readAsDataURL(file); + } catch (e) { + cb(e); + } + } + + function makeThumb(url, onPreview, onRemove, disabled) { + return React.createElement('div', { + style: { + width: 64, + height: 64, + borderRadius: 4, + border: '1px solid #f0f0f0', + background: '#fafafa', + position: 'relative', + overflow: 'hidden' + } + }, + React.createElement('img', { + src: url, + style: { width: '100%', height: '100%', objectFit: 'cover', cursor: disabled ? 'default' : 'pointer' }, + onClick: disabled ? undefined : onPreview + }), + (disabled || !onRemove) ? null : React.createElement('div', { + style: { + position: 'absolute', + right: 6, + top: 6, + width: 20, + height: 20, + borderRadius: 999, + background: 'rgba(0,0,0,0.55)', + color: '#fff', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + cursor: 'pointer', + fontSize: 12, + userSelect: 'none' + }, + onClick: function (e) { e.stopPropagation(); onRemove && onRemove(); } + }, '×') + ); + } + + function sanitize2(v) { + if (v === null || v === undefined) return ''; + var s = String(v); + if (s === '') return ''; + // 只保留数字和一个小数点 + if (!/^\d*\.?\d*$/.test(s)) return ''; + var parts = s.split('.'); + if (parts.length === 1) return parts[0]; + return parts[0] + '.' + (parts[1].slice(0, 2)); + } + + function UploadBox(props) { + var label = props.label; + var value = props.value || []; // array of {uid,name,url} + var max = props.max || 1; + var onChange = props.onChange; + var disabled = !!props.disabled; + var tip = props.tip; + + function handlePick(e) { + var f = e && e.target && e.target.files && e.target.files[0]; + if (!f) return; + fileToDataUrl(f, function (err, url) { + if (err) { message.error('上传失败(原型)'); return; } + var next = (value || []).slice(); + next.push({ uid: String(Date.now()) + '_' + Math.random(), name: f.name || 'image', url: url }); + if (next.length > max) next = next.slice(next.length - max); + onChange && onChange(next); + }); + e.target.value = ''; + } + + return React.createElement('div', null, + label ? React.createElement('div', { style: { fontSize: 12, color: '#666', marginBottom: 6 } }, label) : null, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' } }, + (value || []).map(function (f) { + return React.createElement('div', { key: f.uid }, + makeThumb( + f.url, + function () { previewState[1]({ open: true, url: f.url, title: f.name || '预览' }); }, + function () { onChange && onChange((value || []).filter(function (x) { return x.uid !== f.uid; })); }, + disabled + ) + ); + }), + disabled ? null : ((value || []).length >= max ? null : React.createElement('label', { + style: { + width: 64, + height: 64, + borderRadius: 4, + border: '1px dashed #d9d9d9', + background: '#fff', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: '#999', + cursor: 'pointer' + } + }, + React.createElement('input', { type: 'file', accept: 'image/*', style: { display: 'none' }, onChange: handlePick }), + '上传' + )) + ), + tip ? React.createElement('div', { style: { marginTop: 6, fontSize: 12, color: '#999' } }, tip) : null + ); + } + + var layoutStyle = { padding: '16px 24px 88px', background: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; + var footerStyle = { + position: 'fixed', + left: 0, + right: 0, + bottom: 0, + background: '#fff', + borderTop: '1px solid #f0f0f0', + padding: '12px 24px', + display: 'flex', + justifyContent: 'flex-start', + gap: 12, + zIndex: 10 + }; + + var styles = { + formRow: { display: 'flex', gap: 16, marginBottom: 12, alignItems: 'flex-start' }, + formItem: { flex: 1, minWidth: 0 }, + label: { fontSize: 12, color: '#666', marginBottom: 6 } + }; + + function FormItem(props) { + var label = props.label; + var required = !!props.required; + var fullWidth = !!props.fullWidth; + return React.createElement( + 'div', + { style: fullWidth ? Object.assign({}, styles.formItem, { flex: '0 0 100%' }) : styles.formItem }, + React.createElement('div', { style: styles.label }, required ? RequiredLabel(label) : label), + props.children + ); + } + + // ---------- mock data ---------- + var vehicleInfo = useMemo(function () { + return { + contractCode: 'HT-ZL-2025-001', + projectName: '嘉兴氢能示范项目', + customerName: '嘉兴某某物流有限公司', + businessDept: '华东业务部', + businessOwner: '李经理', + plateNo: '京A12345', + vehicleType: '重型厢式货车', + brand: '东风', + model: 'DFH1180', + deliveryTime: '2025-02-28 14:30', + deliveryRegion: '浙江省-嘉兴市', + deliveryAddress: '嘉兴市南湖区科技大道1号', + deliverySpareTireDepth: '6.50', + deliveryMileageKm: '15230.12', + deliveryBatteryKwh: '68.40', + deliveryHydrogenAmount: '28.30', + hydrogenUnit: 'MPa', + deliverySpareTirePhotoUrl: 'https://dummyimage.com/600x400/eee/666&text=交车备胎照片' + }; + }, []); + + var returnLocationOptions = useMemo(function () { + return { + parking: [ + { value: '停车场A', label: '停车场A' }, + { value: '停车场B', label: '停车场B' } + ], + repair: [ + { value: '维修站A', label: '维修站A' }, + { value: '维修站B', label: '维修站B' } + ] + }; + }, []); + + var deliveryDriver = useMemo(function () { + return { + name: '张三', + phone: '13800138001', + idCard: '320101199001018888' + }; + }, []); + + // 交车时照片(用于展示,仅查看) + var deliveryPhotos = useMemo(function () { + function u(txt) { return 'https://dummyimage.com/600x400/eee/666&text=' + encodeURIComponent(txt); } + return { + vehicle: { + '仪表盘': [{ uid: 'dv1', name: '仪表盘.jpg', url: u('交车-仪表盘') }], + '车辆正面': [{ uid: 'dv2', name: '车辆正面.jpg', url: u('交车-车辆正面') }], + '车辆左前方': [{ uid: 'dv3', name: '车辆左前方.jpg', url: u('交车-车辆左前方') }], + '车辆左后方': [{ uid: 'dv4', name: '车辆左后方.jpg', url: u('交车-车辆左后方') }], + '车辆右后方': [{ uid: 'dv5', name: '车辆右后方.jpg', url: u('交车-车辆右后方') }], + '车辆右前方': [{ uid: 'dv6', name: '车辆右前方.jpg', url: u('交车-车辆右前方') }] + }, + chassis: { + '正前方底部': [{ uid: 'dc1', name: '正前方底部.jpg', url: u('交车-正前方底部') }], + '左侧前方底部': [{ uid: 'dc2', name: '左侧前方底部.jpg', url: u('交车-左侧前方底部') }], + '左侧后方底部': [{ uid: 'dc3', name: '左侧后方底部.jpg', url: u('交车-左侧后方底部') }], + '正后方底部': [{ uid: 'dc4', name: '正后方底部.jpg', url: u('交车-正后方底部') }], + '右侧后方底部': [{ uid: 'dc5', name: '右侧后方底部.jpg', url: u('交车-右侧后方底部') }], + '右侧前方底部': [{ uid: 'dc6', name: '右侧前方底部.jpg', url: u('交车-右侧前方底部') }] + }, + tire: { + '左前轮': [{ uid: 'dt1', name: '左前轮.jpg', url: u('交车-左前轮') }], + '左后轮(内)': [{ uid: 'dt2', name: '左后轮(内).jpg', url: u('交车-左后轮(内)') }], + '左后轮(外)': [{ uid: 'dt3', name: '左后轮(外).jpg', url: u('交车-左后轮(外)') }], + '右前轮': [{ uid: 'dt4', name: '右前轮.jpg', url: u('交车-右前轮') }], + '右后轮(内)': [{ uid: 'dt5', name: '右后轮(内).jpg', url: u('交车-右后轮(内)') }], + '右后轮(外)': [{ uid: 'dt6', name: '右后轮(外).jpg', url: u('交车-右后轮(外)') }], + '备胎': [{ uid: 'dt7', name: '备胎.jpg', url: u('交车-备胎') }] + }, + defect: [ + { uid: 'dd1', name: '交车瑕疵1.jpg', url: u('交车-瑕疵1') }, + { uid: 'dd2', name: '交车瑕疵2.jpg', url: u('交车-瑕疵2') } + ], + other: [ + { uid: 'do1', name: '交车其他1.jpg', url: u('交车-其他1') }, + { uid: 'do2', name: '交车其他2.jpg', url: u('交车-其他2') } + ] + }; + }, []); + + // ---------- states ---------- + var previewState = useState({ open: false, url: '', title: '' }); + var ocrModalState = useState({ open: false, photoUrl: '', depth: '' }); + + var requirementModalOpenState = useState(false); + + var inspectionDrawerOpenState = useState(false); + + var [returnPhotos, setReturnPhotos] = useState(function () { + return { + vehicle: {}, + chassis: {}, + tire: {}, + defect: [], + other: [] + }; + }); + + // 初始化 returnPhotos 的各 slot + useEffect(function () { + setReturnPhotos(function (prev) { + var next = Object.assign({}, prev || {}); + ['vehicle', 'chassis', 'tire'].forEach(function (k) { + next[k] = Object.assign({}, next[k] || {}); + }); + Object.keys(deliveryPhotos.vehicle || {}).forEach(function (slot) { if (!next.vehicle[slot]) next.vehicle[slot] = []; }); + Object.keys(deliveryPhotos.chassis || {}).forEach(function (slot) { if (!next.chassis[slot]) next.chassis[slot] = []; }); + Object.keys(deliveryPhotos.tire || {}).forEach(function (slot) { if (!next.tire[slot]) next.tire[slot] = []; }); + return next; + }); + }, []); + + var [form, setForm] = useState(function () { + return { + // 还车人信息(默认来自交车司机) + returnPerson: deliveryDriver.name, + returnPhone: deliveryDriver.phone, + returnIdCard: deliveryDriver.idCard, + + // 还车地点 + returnPlaceType: undefined, // 'parking' | 'repair' + returnPlaceName: undefined, + arriveTime: '', + + // 备胎照片与胎纹深度 + spareTirePhoto: [], // 上传 + spareTireDepth: '', // OCR/手填 + + // 里程/电量/氢量 + returnMileageKm: '', + returnBatteryKwh: '', + returnHydrogenAmount: '', + hydrogenUnit: vehicleInfo.hydrogenUnit, + + // 接车服务费(可选) + serviceFee: '' + }; + }); + + function updateForm(patch) { + setForm(function (prev) { return Object.assign({}, prev, patch); }); + } + + function handleSpareTirePhotoChange(list) { + updateForm({ spareTirePhoto: list || [] }); + if ((list || []).length > 0) { + ocrModalState[1]({ open: true, photoUrl: list[0].url, depth: '6.50' }); + } + } + + function confirmOcr() { + updateForm({ spareTireDepth: ocrModalState[0].depth }); + ocrModalState[1](Object.assign({}, ocrModalState[0], { open: false })); + message.success('已反写还车备胎胎纹深度(原型)'); + } + + // ---------- inspection checklist ---------- + // 参照交车检查单生成“还车检查项”(原型:默认全开/正常) + var inspectionCategoryItems = useMemo(function () { + return { + '车灯': ['大灯', '转向灯', '小灯', '示廓灯', '刹车灯', '倒车灯', '牌照灯', '防雾灯', '室内灯'], + '仪表盘': ['氢系统指示', '电控系统指示', '数值清晰准确', '故障报警灯'], + '驾驶室': ['点烟器', '车窗升降', '按键开关', '雨刮器', '内后视镜是否正常', '内/外门把手', '安全带', '空调冷暖风', '仪表盘', '门锁功能', '手刹', '车钥匙功能是否正常', '喇叭', '音响功能', '遮阳板', '主副驾座椅', '方向盘', '内饰干净整洁'], + '轮胎': ['前左胎', '前右胎', '后左胎', '后右胎', '备胎'], + '液位检查': ['冷却液', '制动液', '玻璃水'], + '外观检查': ['车身外观', '漆面', '玻璃'], + '车辆外观': ['整车外观'], + '其他': ['其他检查项'], + '随车工具': ['三角牌', '灭火器', '反光背心'], + '随车证件': ['行驶证', '营运证', '保险单'], + '整车': ['整车状态'], + '燃料电池系统': ['氢系统', '储氢瓶'], + '冷机': ['冷机运行'], + '制动系统': ['制动踏板', '驻车制动'] + }; + }, []); + + function buildInspectionList() { + var list = []; + var categories = Object.keys(inspectionCategoryItems); + for (var i = 0; i < categories.length; i++) { + var cat = categories[i]; + var items = inspectionCategoryItems[cat] || []; + for (var j = 0; j < items.length; j++) { + var it = items[j]; + var isTire = cat === '轮胎'; + list.push({ + key: 'r-ins-' + i + '-' + j, + category: cat, + item: it, + checked: true, + treadDepth: isTire ? '6.50' : '', + remark: '' + }); + } + } + return list; + } + + var inspectionListState = useState(function () { return buildInspectionList(); }); + var inspectionList = inspectionListState[0]; + var setInspectionList = inspectionListState[1]; + var inspectionListRef = useRef(null); + inspectionListRef.current = inspectionList; + + function updateInspectionRow(key, patch) { + setInspectionList(function (prev) { + return (prev || []).map(function (r) { + if (r.key !== key) return r; + return Object.assign({}, r, patch); + }); + }); + } + + var inspectionColumns = useMemo(function () { + return [ + { + title: '类别', + dataIndex: 'category', + key: 'category', + width: 140, + render: function (text, record, index) { + var rows = inspectionListRef.current || []; + var cat = record && record.category; + if (!cat) return { children: text, props: { rowSpan: 1 } }; + + // 仅合并同类别的连续行 + var isFirst = true; + for (var i = index - 1; i >= 0; i--) { + if (!rows[i] || rows[i].category !== cat) break; + isFirst = false; + break; + } + if (!isFirst) return { children: null, props: { rowSpan: 0 } }; + + var span = 1; + for (var j = index + 1; j < rows.length; j++) { + if (!rows[j] || rows[j].category !== cat) break; + span++; + } + return { children: text, props: { rowSpan: span } }; + } + }, + { title: '检查项目', dataIndex: 'item', key: 'item', width: 220 }, + { + title: '检查情况', + dataIndex: 'checked', + key: 'checked', + width: 220, + render: function (_, record) { + var isTire = record && (record.category === '轮胎' || String(record.item || '').indexOf('胎纹') >= 0); + return isTire + ? React.createElement(Input, { + value: record.treadDepth, + placeholder: '请输入胎纹深度', + addonAfter: 'mm', + onChange: function (e) { updateInspectionRow(record.key, { treadDepth: e.target.value }); } + }) + : React.createElement(Switch, { + checked: !!record.checked, + onChange: function (v) { updateInspectionRow(record.key, { checked: !!v }); } + }); + } + }, + { + title: '备注', + dataIndex: 'remark', + key: 'remark', + render: function (_, record) { + return React.createElement(Input, { + value: record.remark, + placeholder: '请输入', + onChange: function (e) { updateInspectionRow(record.key, { remark: e.target.value }); } + }); + } + } + ]; + }, []); + + // ---------- validation & actions ---------- + function validateSubmit() { + if (isEmpty(form.returnPerson)) return '请填写还车人'; + if (isEmpty(form.returnPhone)) return '请填写还车人电话'; + if (isEmpty(form.returnIdCard)) return '请填写还车人身份证'; + if (!form.returnPlaceType) return '请选择还车地点'; + if (isEmpty(form.returnPlaceName)) return '请选择' + (form.returnPlaceType === 'parking' ? '停车场' : '维修站'); + if (isEmpty(form.arriveTime)) return '请选择到达时间'; + if (!form.spareTirePhoto || form.spareTirePhoto.length === 0) return '请上传备胎照片'; + if (isEmpty(form.spareTireDepth)) return '请填写还车时备胎胎纹深度'; + if (isEmpty(form.returnMileageKm)) return '请填写还车里程'; + if (isEmpty(form.returnBatteryKwh)) return '请填写还车电量'; + if (isEmpty(form.returnHydrogenAmount)) return '请填写还车氢量'; + return ''; + } + + function handleSubmit() { + var err = validateSubmit(); + if (err) { message.error(err); return; } + Modal.confirm({ + title: '确认还车', + content: '请确认信息填写无误,点击确认完成该车辆还车,并将该记录加入历史记录。', + okText: '确认', + cancelText: '取消', + onOk: function () { + message.success('还车成功(原型)'); + message.success('已加入历史记录(原型)'); + } + }); + } + + function handleSave() { + message.success('已暂存还车单(原型)'); + } + + function handleCancel() { + message.info('返回还车管理页(原型)'); + } + + // ---------- photo render helpers ---------- + function PreviewDeliveryThumb(props) { + var url = props.url; + if (!url) return React.createElement('div', { style: { width: 64, height: 64, borderRadius: 4, border: '1px solid #f0f0f0', background: '#fafafa' } }); + return makeThumb(url, + function () { previewState[1]({ open: true, url: url, title: props.title || '交车照片预览' }); }, + null, + false + ); + } + + function ReturnPhotoGridColumn(title, slots, required, deliveryMap, returnMap, onReturnMapChange) { + return React.createElement('div', { style: { background: '#fff', border: '1px solid #f0f0f0', borderRadius: 8, padding: 12 } }, + React.createElement('div', { style: { fontWeight: 600, marginBottom: 12 } }, required ? RequiredLabel(title) : title), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 12 } }, + (slots || []).map(function (slot) { + var deliveryArr = (deliveryMap && deliveryMap[slot]) || []; + var deliveryUrl = deliveryArr && deliveryArr[0] ? deliveryArr[0].url : ''; + return React.createElement('div', { key: slot }, + React.createElement('div', { style: { fontSize: 12, color: '#666', marginBottom: 6 } }, required ? RequiredLabel(slot) : slot), + React.createElement('div', { style: { display: 'flex', gap: 12, alignItems: 'flex-start', flexWrap: 'wrap' } }, + React.createElement('div', { style: { display: 'flex', flexDirection: 'column', alignItems: 'center' } }, + PreviewDeliveryThumb({ url: deliveryUrl, title: slot }), + React.createElement('div', { style: { fontSize: 11, color: '#999', marginTop: 6 } }, '交车时照片') + ), + React.createElement('div', { style: { flex: '1 1 120px', minWidth: 120 } }, + UploadBox({ + label: null, + value: (returnMap && returnMap[slot]) || [], + max: 1, + onChange: function (l) { + onReturnMapChange(slot, l || []); + } + }) + ) + ) + ); + }) + ) + ); + } + + // ---------- render ---------- + return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + React.createElement(Breadcrumb, { items: [{ title: '运维管理' }, { title: '车辆业务' }, { title: '还车管理' }] }), + React.createElement(Button, { type: 'link', onClick: function () { requirementModalOpenState[1](true); } }, '查看需求说明') + ), + + React.createElement(Card, { title: '合同信息', style: cardStyle }, + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '合同编码' }, + React.createElement(Input, { value: vehicleInfo.contractCode, disabled: true }) + ), + React.createElement(FormItem, { label: '项目名称' }, + React.createElement(Input, { value: vehicleInfo.projectName, disabled: true }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '客户名称' }, + React.createElement(Input, { value: vehicleInfo.customerName, disabled: true }) + ), + React.createElement(FormItem, { label: '业务部门' }, + React.createElement(Input, { value: vehicleInfo.businessDept, disabled: true }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '业务负责人', fullWidth: true }, + React.createElement(Input, { value: vehicleInfo.businessOwner, disabled: true }) + ) + ) + ), + + React.createElement(Card, { title: '车辆信息', style: cardStyle }, + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '车牌号' }, + React.createElement(Input, { value: vehicleInfo.plateNo, disabled: true }) + ), + React.createElement(FormItem, { label: '车辆型号' }, + React.createElement(Input, { value: vehicleInfo.vehicleType, disabled: true }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '品牌' }, + React.createElement(Input, { value: vehicleInfo.brand, disabled: true }) + ), + React.createElement(FormItem, { label: '型号' }, + React.createElement(Input, { value: vehicleInfo.model, disabled: true }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '交车时间' }, + React.createElement(Input, { value: vehicleInfo.deliveryTime, disabled: true, placeholder: '' }) + ), + React.createElement(FormItem, { label: '交车地点(省-市)' }, + React.createElement(Input, { value: vehicleInfo.deliveryRegion, disabled: true }) + ) + ) + ), + + React.createElement(Card, { title: '还车明细', style: cardStyle }, + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '还车人', required: true }, + React.createElement(Input, { value: form.returnPerson, placeholder: '请输入还车人', onChange: function (e) { updateForm({ returnPerson: e.target.value }); } }) + ), + React.createElement(FormItem, { label: '还车人电话', required: true }, + React.createElement(Input, { value: form.returnPhone, placeholder: '请输入还车人电话', onChange: function (e) { updateForm({ returnPhone: e.target.value }); } }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '还车人身份证', required: true }, + React.createElement(Input, { value: form.returnIdCard, placeholder: '请输入还车人身份证', onChange: function (e) { updateForm({ returnIdCard: e.target.value }); } }) + ), + React.createElement(FormItem, { label: '还车地点', required: true }, + React.createElement(Select, { + placeholder: '请选择还车地点', + value: form.returnPlaceType, + allowClear: true, + options: [ + { value: 'parking', label: '停车场' }, + { value: 'repair', label: '维修站' } + ], + onChange: function (v) { updateForm({ returnPlaceType: v, returnPlaceName: undefined }); } + }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { + label: form.returnPlaceType === 'parking' ? '停车场' : form.returnPlaceType === 'repair' ? '维修站' : '停车场/维修站', + required: true + }, + React.createElement(Select, { + placeholder: '请选择' + (form.returnPlaceType === 'parking' ? '停车场' : form.returnPlaceType === 'repair' ? '维修站' : '停车场或维修站'), + value: form.returnPlaceName, + allowClear: true, + showSearch: true, + filterOption: filterOption, + options: form.returnPlaceType === 'parking' ? returnLocationOptions.parking : (form.returnPlaceType === 'repair' ? returnLocationOptions.repair : []), + onChange: function (v) { updateForm({ returnPlaceName: v }); } + }) + ) + ), + + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '到达时间', required: true, fullWidth: true }, + React.createElement(DatePicker, { + showTime: { format: 'HH:mm' }, + format: 'YYYY-MM-DD HH:mm', + placeholder: '请选择到达时间', + style: { width: '100%' }, + value: (function () { + var dayjs = window.dayjs; + return form.arriveTime && dayjs ? dayjs(form.arriveTime) : null; + })(), + onChange: function (date, dateString) { + updateForm({ arriveTime: dateString || '' }); + } + }) + ) + ), + + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '备胎照片', required: true, fullWidth: true }, + React.createElement('div', { style: { display: 'flex', gap: 16, alignItems: 'flex-start', flexWrap: 'wrap' } }, + React.createElement('div', { style: { display: 'flex', flexDirection: 'column', alignItems: 'center' } }, + makeThumb( + vehicleInfo.deliverySpareTirePhotoUrl, + function () { previewState[1]({ open: true, url: vehicleInfo.deliverySpareTirePhotoUrl, title: '交车时备胎照片' }); }, + null, + false + ), + React.createElement('div', { style: { fontSize: 11, color: '#999', marginTop: 6 } }, '交车时照片') + ), + React.createElement('div', { style: { flex: '1 1 240px', minWidth: 240 } }, + UploadBox({ + label: null, + value: form.spareTirePhoto, + max: 1, + onChange: function (l) { handleSpareTirePhotoChange(l); }, + tip: '上传后将自动识别胎纹深度(原型)' + }) + ) + ) + ) + ), + + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '交车时备胎胎纹深度', fullWidth: false }, + React.createElement(Input, { value: vehicleInfo.deliverySpareTireDepth, disabled: true, addonAfter: 'mm' }) + ), + React.createElement(FormItem, { label: '还车时备胎胎纹深度', required: true }, + React.createElement(Input, { + value: form.spareTireDepth, + placeholder: '请输入备胎胎纹深度', + addonAfter: 'mm', + onChange: function (e) { updateForm({ spareTireDepth: e.target.value }); } + }) + ) + ), + + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '交车里程(km)' }, + React.createElement(Input, { value: vehicleInfo.deliveryMileageKm, disabled: true, addonAfter: 'km' }) + ), + React.createElement(FormItem, { label: '还车里程(km)', required: true }, + React.createElement(Input, { + value: form.returnMileageKm, + placeholder: '请输入还车里程', + addonAfter: 'km', + onChange: function (e) { updateForm({ returnMileageKm: sanitize2(e.target.value) }); } + }) + ) + ), + + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '交车电量(kWh)' }, + React.createElement(Input, { value: vehicleInfo.deliveryBatteryKwh, disabled: true, addonAfter: 'kWh' }) + ), + React.createElement(FormItem, { label: '还车电量(kWh)', required: true }, + React.createElement(Input, { + value: form.returnBatteryKwh, + placeholder: '请输入还车电量', + addonAfter: 'kWh', + onChange: function (e) { updateForm({ returnBatteryKwh: sanitize2(e.target.value) }); } + }) + ) + ), + + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '交车氢量(' + form.hydrogenUnit + ')' }, + React.createElement(Input, { value: vehicleInfo.deliveryHydrogenAmount, disabled: true, addonAfter: form.hydrogenUnit }) + ), + React.createElement(FormItem, { label: '还车氢量(' + form.hydrogenUnit + ')', required: true }, + React.createElement(Input, { + value: form.returnHydrogenAmount, + placeholder: '请输入还车氢量', + addonAfter: form.hydrogenUnit, + onChange: function (e) { updateForm({ returnHydrogenAmount: sanitize2(e.target.value) }); } + }) + ) + ), + + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '接车服务费' }, + React.createElement(Input, { + value: form.serviceFee, + placeholder: '请输入接车服务费金额', + addonAfter: '元', + onChange: function (e) { updateForm({ serviceFee: sanitize2(e.target.value) }); } + }) + ) + ), + + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '还车检查单', fullWidth: true }, + React.createElement(Button, { onClick: function () { inspectionDrawerOpenState[1](true); } }, '还车检查单') + ) + ) + ), + + React.createElement(Card, { title: '还车照片', style: cardStyle }, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 12 } }, + ReturnPhotoGridColumn( + '车辆', + Object.keys(deliveryPhotos.vehicle || {}), + true, + deliveryPhotos.vehicle, + returnPhotos.vehicle, + function (slot, list) { + setReturnPhotos(function (prev) { + var next = Object.assign({}, prev); + next.vehicle = Object.assign({}, prev.vehicle); + next.vehicle[slot] = list; + return next; + }); + } + ), + ReturnPhotoGridColumn( + '底盘', + Object.keys(deliveryPhotos.chassis || {}), + true, + deliveryPhotos.chassis, + returnPhotos.chassis, + function (slot, list) { + setReturnPhotos(function (prev) { + var next = Object.assign({}, prev); + next.chassis = Object.assign({}, prev.chassis); + next.chassis[slot] = list; + return next; + }); + } + ), + React.createElement('div', { style: { gridColumn: 'span 2' } }, + ReturnPhotoGridColumn( + '轮胎', + Object.keys(deliveryPhotos.tire || {}), + true, + deliveryPhotos.tire, + returnPhotos.tire, + function (slot, list) { + setReturnPhotos(function (prev) { + var next = Object.assign({}, prev); + next.tire = Object.assign({}, prev.tire); + next.tire[slot] = list; + return next; + }); + } + ) + ) + ), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 12, marginTop: 12 } }, + (function () { + // 瑕疵:左侧展示交车时照片(仅查看),右侧上传还车照片(最多4张) + var left = (deliveryPhotos.defect || []).slice(0, 4); + return React.createElement('div', { style: { background: '#fff', border: '1px solid #f0f0f0', borderRadius: 8, padding: 12 } }, + React.createElement('div', { style: { fontWeight: 600, marginBottom: 12 } }, '瑕疵'), + React.createElement('div', { style: { display: 'flex', gap: 16, alignItems: 'flex-start', flexWrap: 'wrap' } }, + React.createElement('div', { style: { width: 260 } }, + React.createElement('div', { style: { display: 'flex', gap: 12, flexWrap: 'wrap' } }, left.map(function (f) { + return React.createElement('div', { key: f.uid }, + PreviewDeliveryThumb({ url: f.url, title: '交车瑕疵' }), + null + ); + })), + React.createElement('div', { style: { fontSize: 12, color: '#999', marginTop: 6 } }, '交车时照片') + ), + React.createElement('div', { style: { flex: '1 1 220px', minWidth: 220 } }, + React.createElement(UploadBox, { + label: null, + value: returnPhotos.defect, + max: 4, + onChange: function (l) { setReturnPhotos(function (prev) { return Object.assign({}, prev, { defect: l || [] }); }); }, + tip: '最多支持上传4张' + }) + ) + ) + ); + })(), + (function () { + // 其他:同瑕疵结构 + var left = (deliveryPhotos.other || []).slice(0, 4); + return React.createElement('div', { style: { background: '#fff', border: '1px solid #f0f0f0', borderRadius: 8, padding: 12 } }, + React.createElement('div', { style: { fontWeight: 600, marginBottom: 12 } }, '其他'), + React.createElement('div', { style: { display: 'flex', gap: 16, alignItems: 'flex-start', flexWrap: 'wrap' } }, + React.createElement('div', { style: { width: 260 } }, + React.createElement('div', { style: { display: 'flex', gap: 12, flexWrap: 'wrap' } }, left.map(function (f) { + return React.createElement('div', { key: f.uid }, + PreviewDeliveryThumb({ url: f.url, title: '交车其他' }) + ); + })), + React.createElement('div', { style: { fontSize: 12, color: '#999', marginTop: 6 } }, '交车时照片') + ), + React.createElement('div', { style: { flex: '1 1 220px', minWidth: 220 } }, + React.createElement(UploadBox, { + label: null, + value: returnPhotos.other, + max: 4, + onChange: function (l) { setReturnPhotos(function (prev) { return Object.assign({}, prev, { other: l || [] }); }); }, + tip: '最多支持上传4张' + }) + ) + ) + ); + })() + ) + ), + + React.createElement('div', { style: footerStyle }, + React.createElement(Button, { type: 'primary', onClick: handleSubmit }, '提交'), + React.createElement(Button, { onClick: handleSave }, '保存'), + React.createElement(Button, { onClick: handleCancel }, '取消') + ), + + // 预览 Modal + React.createElement(Modal, { + open: !!previewState[0].open, + title: previewState[0].title || '预览', + footer: null, + onCancel: function () { previewState[1]({ open: false, url: '', title: '' }); }, + width: 860 + }, + previewState[0].url ? React.createElement('img', { src: previewState[0].url, style: { width: '100%', maxHeight: '70vh', objectFit: 'contain' } }) : null + ), + + // OCR Modal(备胎胎纹深度识别) + React.createElement(Modal, { + open: !!ocrModalState[0].open, + title: '正在识别中(原型)', + onCancel: function () { ocrModalState[1](Object.assign({}, ocrModalState[0], { open: false })); }, + onOk: confirmOcr, + okText: '确认', + cancelText: '取消', + width: 860 + }, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 320px', gap: 16, alignItems: 'start' } }, + React.createElement('div', { style: { border: '1px solid #f0f0f0', borderRadius: 8, overflow: 'hidden', background: '#fafafa' } }, + ocrModalState[0].photoUrl ? React.createElement('img', { src: ocrModalState[0].photoUrl, style: { width: '100%', maxHeight: 420, objectFit: 'contain', display: 'block' } }) : null + ), + React.createElement('div', null, + React.createElement('div', { style: { fontSize: 12, color: '#666', marginBottom: 6 } }, '识别出的胎纹深度'), + React.createElement(Input, { + value: ocrModalState[0].depth, + addonAfter: 'mm', + onChange: function (e) { ocrModalState[1](Object.assign({}, ocrModalState[0], { depth: e.target.value })); } + }), + React.createElement('div', { style: { marginTop: 8, fontSize: 12, color: '#999', lineHeight: 1.7 } }, + '提示:识别中请勿关闭页面;点击确认后将反写至“还车时备胎胎纹深度”。' + ) + ) + ) + ), + + // 还车检查抽屉 + React.createElement(Drawer, { + open: inspectionDrawerOpenState[0], + title: '还车检查单', + width: 920, + placement: 'right', + onClose: function () { inspectionDrawerOpenState[1](false); }, + styles: { body: { display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden', paddingBottom: 0 } }, + footer: React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-start', gap: 8 } }, + React.createElement(Button, { + type: 'primary', + onClick: function () { + message.success('提交成功(原型)'); + inspectionDrawerOpenState[1](false); + } + }, '提交'), + React.createElement(Button, { onClick: function () { inspectionDrawerOpenState[1](false); } }, '返回') + ) + }, + React.createElement('div', { style: { flex: 1, minHeight: 0, overflow: 'auto' } }, + React.createElement(Table, { rowKey: 'key', columns: inspectionColumns, dataSource: inspectionList, pagination: false, bordered: true, size: 'small' }) + ) + ), + + React.createElement(Modal, { + open: requirementModalOpenState[0], + onCancel: function () { requirementModalOpenState[1](false); }, + onOk: function () { requirementModalOpenState[1](false); }, + title: '需求说明', + width: 860, + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, + React.createElement('div', { style: { whiteSpace: 'pre-wrap', lineHeight: 1.7 } }, + '一个「数字化资产ONEOS运管平台」中的「还车管理」「还车」模块\\n' + + '1.面包屑:\\n' + + '1.1.运维管理-车辆业务-还车管理-还车\\n\\n' + + '2.合同信息:\\n' + + '2.1.合同编码:显示该车辆对应合同编码;\\n' + + '2.2.项目名称:显示该车辆对应项目名称;\\n' + + '2.3.客户名称:显示该车辆对应客户名称;\\n' + + '2.4.业务部门:显示该车辆合同对应业务部门;\\n' + + '2.5.业务负责人:显示该车辆合同对应业务负责人;\\n\\n' + + '3.车辆信息:\\n' + + '3.1.车牌号:显示该车辆车牌号;\\n' + + '3.2.车辆型号:显示该车辆型号;\\n' + + '3.3.品牌:显示该车辆品牌;\\n' + + '3.4.型号:显示该车辆型号;\\n' + + '3.5.交车时间:显示该车辆交车时间,格式为:YYYY-MM-DD HH:MM;\\n' + + '3.6.交车地点:显示该车辆交车地点,格式为:省-市;\\n\\n' + + '4.还车明细:\\n' + + '4.1.还车人:必填项,输入框,默认显示交车时司机姓名(从OCR自动识别身份证);\\n' + + '4.2.还车人电话:必填项,输入框,默认显示交车时司机手机号(从司机扫码培训时验证的手机号);\\n' + + '4.3.还车人身份证:必填项,输入框,默认显示还车时司机身份证号(从OCR自动识别身份证);\\n' + + '4.4.还车地点:必填项,选择器,选项为停车场、维修站,默认提示:请选择还车地点;\\n' + + '4.5.停车场/维修站:必填项,选择器,选择停车场或维修站;当还车地点为停车场时,字段名称为停车场,当还车地点为维修站时,字段名称为维修站;\\n' + + '4.6.到达时间:必填项,日期选择器,输入框单日历,支持选择年月日,精确到分钟,格式为:YYYY-MM-DD HH:MM;\\n' + + '4.7.备胎照片:左侧为交车时备胎照片(仅查看,支持点击预览),右侧为图片上传按钮,支持单张照片上传,上传后按钮变为图片,支持点击图片预览和右上角删除,图片上传后提示识别中,然后弹出卡片,左侧为图片,右侧为胎纹深度:输入框(自动反写胎纹深度),后缀为mm;\\n' + + '4.8.交车时备胎胎纹深度:输入框(禁用),显示交车时胎纹深度,后缀为mm;\\n' + + '4.9.还车时备胎胎纹深度:输入框,默认提示为请输入备胎胎纹深度,如上传备胎照片并确认后则自动反写OCR识别结果;\\n' + + '4.10.交车里程:输入框(禁用),显示交车时交车里程,后缀为km;\\n' + + '4.11.还车里程:必填项,输入框,支持2位小数输入,后缀为km;\\n' + + '4.12.交车电量:输入框(禁用),显示交车时交车电量,后缀为kWh;\\n' + + '4.13.还车电量:必填项,输入框,支持2位小数输入,后缀为kWh;\\n' + + '4.14.交车氢量:输入框(禁用),显示交车时交车氢量,后缀为MPa或%(根据车辆型号参数列表中仪表盘显示单位显示);\\n' + + '4.15.还车氢量:必填项,输入框,支持2位小数输入,后缀为MPa或%(根据车辆型号参数列表中仪表盘显示单位显示);\\n' + + '4.16.接车服务费:输入框,支持2位小数输入,后缀为元;\\n' + + '4.17.还车检查单:点击弹出抽屉,抽屉显示还车检查项(默认为交车时状态),下方为提交和返回按钮,参照交车管理-交车单-编辑中交车检查单;\\n\\n' + + '5.还车照片:参照交车管理-交车单-编辑中交车照片,只是所有上传按钮左侧增加交车照片显示(仅查看,支持点击预览);\\n\\n' + + '所有交车时照片下方增加文字提示:交车时照片\\n\\n' + + '4.底部为提交、取消按钮;\\n' + + ' 4.1.提交:点击进行二次确认,点击确认完成该车辆还车,并将该记录加入历史记录;\\n' + + ' 4.2.保存:点击暂存还车单(不做校验);\\n' + + ' 4.2.取消:点击返回还车管理页;' + ) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-ops/pages/21-还车管理.jsx b/src/prototypes/oneos-web-ops/pages/21-还车管理.jsx new file mode 100644 index 0000000..a73f241 --- /dev/null +++ b/src/prototypes/oneos-web-ops/pages/21-还车管理.jsx @@ -0,0 +1,481 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 运维管理 - 车辆业务 - 还车管理 + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Select = antd.Select; + var Button = antd.Button; + var Table = antd.Table; + var Tabs = antd.Tabs; + var Modal = antd.Modal; + var message = antd.message; + var App = antd.App; + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + var contractOptions = [ + { value: 'HT-ZL-2025-001', label: 'HT-ZL-2025-001' }, + { value: 'HT-ZL-2025-002', label: 'HT-ZL-2025-002' }, + { value: 'HT-ZL-2025-003', label: 'HT-ZL-2025-003' }, + { value: 'HT-ZL-2025-004', label: 'HT-ZL-2025-004' } + ]; + var projectOptions = [ + { value: '嘉兴氢能示范项目', label: '嘉兴氢能示范项目' }, + { value: '上海物流租赁项目', label: '上海物流租赁项目' }, + { value: '杭州城配租赁项目', label: '杭州城配租赁项目' } + ]; + var customerOptions = [ + { value: '嘉兴某某物流有限公司', label: '嘉兴某某物流有限公司' }, + { value: '上海某某运输公司', label: '上海某某运输公司' }, + { value: '杭州某某租赁有限公司', label: '杭州某某租赁有限公司' } + ]; + var plateOptions = [ + { value: '京A12345', label: '京A12345' }, + { value: '沪B20001', label: '沪B20001' }, + { value: '浙A88888', label: '浙A88888' }, + { value: '浙F66666', label: '浙F66666' } + ]; + var vehicleTypeOptions = [ + { value: '轻型厢式货车', label: '轻型厢式货车' }, + { value: '重型厢式货车', label: '重型厢式货车' }, + { value: '厢式货车', label: '厢式货车' }, + { value: '平板货车', label: '平板货车' }, + { value: '栏板货车', label: '栏板货车' }, + { value: '小型普通客车', label: '小型普通客车' } + ]; + var brandOptions = [ + { value: '东风', label: '东风' }, + { value: '福田', label: '福田' }, + { value: '江淮', label: '江淮' }, + { value: '重汽', label: '重汽' }, + { value: '陕汽', label: '陕汽' }, + { value: '苏龙', label: '苏龙' } + ]; + var modelOptions = [ + { value: 'DFH1180', label: 'DFH1180' }, + { value: 'BJ1180', label: 'BJ1180' }, + { value: 'HFC1180', label: 'HFC1180' }, + { value: 'ZZ1180', label: 'ZZ1180' }, + { value: 'KLQ6129', label: 'KLQ6129' } + ]; + + var defaultPending = [ + { id: 'p1', deliveryTime: '2025-02-20 09:30', deliveryPerson: '张三', plateNo: '京A12345', vehicleType: '重型厢式货车', brand: '东风', model: 'DFH1180', vin: 'LGHXCAE28M1234567', contractCode: 'HT-ZL-2025-001', customerName: '嘉兴某某物流有限公司', projectName: '嘉兴氢能示范项目', dept: '华东业务部', bizOwner: '李经理', vehicleArrived: false }, + { id: 'p2', deliveryTime: '2025-02-21 14:00', deliveryPerson: '李四', plateNo: '沪B20001', vehicleType: '厢式货车', brand: '福田', model: 'BJ1180', vin: 'LGHXCAE28M7654321', contractCode: 'HT-ZL-2025-002', customerName: '上海某某运输公司', projectName: '上海物流租赁项目', dept: '华东业务部', bizOwner: '王经理', vehicleArrived: true }, + { id: 'p3', deliveryTime: '2025-02-22 10:15', deliveryPerson: '王五', plateNo: '浙A88888', vehicleType: '轻型厢式货车', brand: '江淮', model: 'HFC1180', vin: 'LGHXCAE28M8888888', contractCode: 'HT-ZL-2025-003', customerName: '杭州某某租赁有限公司', projectName: '杭州城配租赁项目', dept: '浙江业务部', bizOwner: '赵经理', vehicleArrived: false }, + { id: 'p4', deliveryTime: '2025-02-23 16:45', deliveryPerson: '张三', plateNo: '浙F66666', vehicleType: '栏板货车', brand: '重汽', model: 'ZZ1180', vin: 'LGHXCAE28M7777777', contractCode: 'HT-ZL-2025-001', customerName: '嘉兴某某物流有限公司', projectName: '嘉兴氢能示范项目', dept: '华东业务部', bizOwner: '李经理', vehicleArrived: false } + ]; + + var defaultHistory = [ + { id: 'h1', deliveryTime: '2025-01-10 08:00', deliveryPerson: '张三', returnTime: '2025-02-01 17:30', plateNo: '京C11111', vehicleType: '重型厢式货车', brand: '东风', model: 'DFH1180', vin: 'LGHXCAE28M1111111', contractCode: 'HT-ZL-2024-001', customerName: '嘉兴某某物流有限公司', projectName: '嘉兴氢能示范项目', dept: '华东业务部', bizOwner: '李经理' }, + { id: 'h2', deliveryTime: '2025-01-15 11:20', deliveryPerson: '李四', returnTime: '2025-02-05 09:00', plateNo: '沪A10001', vehicleType: '厢式货车', brand: '江淮', model: 'HFC1180', vin: 'LGHXCAE28M2222222', contractCode: 'HT-ZL-2024-002', customerName: '上海某某运输公司', projectName: '上海物流租赁项目', dept: '华东业务部', bizOwner: '王经理' }, + { id: 'h3', deliveryTime: '2025-01-20 13:00', deliveryPerson: '王五', returnTime: '2025-02-10 15:45', plateNo: '浙B99999', vehicleType: '平板货车', brand: '福田', model: 'BJ1180', vin: 'LGHXCAE28M3333333', contractCode: 'HT-ZL-2024-003', customerName: '杭州某某租赁有限公司', projectName: '杭州城配租赁项目', dept: '浙江业务部', bizOwner: '赵经理' } + ]; + + var filterDraftState = useState({ + contractCode: undefined, + projectName: undefined, + customerName: undefined, + plateNo: undefined, + vehicleTypes: [], + brands: [], + model: undefined + }); + var fd = filterDraftState[0]; + var setFd = filterDraftState[1]; + + var appliedState = useState({ + contractCode: undefined, + projectName: undefined, + customerName: undefined, + plateNo: undefined, + vehicleTypes: [], + brands: [], + model: undefined + }); + var applied = appliedState[0]; + var setApplied = appliedState[1]; + + var pendingDataState = useState(defaultPending.slice()); + var pendingData = pendingDataState[0]; + var setPendingData = pendingDataState[1]; + + var historyDataState = useState(defaultHistory.slice()); + var historyData = historyDataState[0]; + + var tabState = useState('pending'); + var tab = tabState[0]; + var setTab = tabState[1]; + + var pageState = useState(1); + var page = pageState[0]; + var setPage = pageState[1]; + var pageSizeState = useState(10); + var pageSize = pageSizeState[0]; + var setPageSize = pageSizeState[1]; + + var filterExpandedState = useState(false); + var filterExpanded = filterExpandedState[0]; + var setFilterExpanded = filterExpandedState[1]; + var requirementModalVisibleState = useState(false); + var requirementModalVisible = requirementModalVisibleState[0]; + var setRequirementModalVisible = requirementModalVisibleState[1]; + + var handleQuery = useCallback(function () { + setApplied(Object.assign({}, fd)); + setPage(1); + message.success('已查询(原型)'); + }, [fd]); + + var handleReset = useCallback(function () { + var empty = { contractCode: undefined, projectName: undefined, customerName: undefined, plateNo: undefined, vehicleTypes: [], brands: [], model: undefined }; + setFd(empty); + setApplied(empty); + setPage(1); + }, []); + + function matchFilters(row) { + if (applied.contractCode && row.contractCode !== applied.contractCode) return false; + if (applied.projectName && row.projectName !== applied.projectName) return false; + if (applied.customerName && row.customerName !== applied.customerName) return false; + if (applied.plateNo && row.plateNo !== applied.plateNo) return false; + if (applied.vehicleTypes && applied.vehicleTypes.length > 0 && applied.vehicleTypes.indexOf(row.vehicleType) === -1) return false; + if (applied.brands && applied.brands.length > 0 && applied.brands.indexOf(row.brand) === -1) return false; + if (applied.model && row.model !== applied.model) return false; + return true; + } + + var filteredPending = useMemo(function () { + return pendingData.filter(matchFilters); + }, [pendingData, applied]); + + var filteredHistory = useMemo(function () { + return historyData.filter(matchFilters); + }, [historyData, applied]); + + var pagedPending = useMemo(function () { + var start = (page - 1) * pageSize; + return filteredPending.slice(start, start + pageSize); + }, [filteredPending, page, pageSize]); + + var pagedHistory = useMemo(function () { + var start = (page - 1) * pageSize; + return filteredHistory.slice(start, start + pageSize); + }, [filteredHistory, page, pageSize]); + + var handleVehicleArrived = useCallback(function (record) { + Modal.confirm({ + title: '确认', + content: '请确认车辆是否到达', + okText: '确认', + cancelText: '取消', + onOk: function () { + setPendingData(function (prev) { + return prev.map(function (r) { + if (r.id !== record.id) return r; + return Object.assign({}, r, { vehicleArrived: true }); + }); + }); + message.success('已确认车辆到达(原型)'); + } + }); + }, []); + + var handleReturnCar = useCallback(function () { + message.info('跳转还车管理-还车页(原型)'); + }, []); + + var handleViewHistory = useCallback(function () { + message.info('跳转还车管理-查看(原型)'); + }, []); + + var handleExport = useCallback(function () { + message.success('导出(原型)'); + }, []); + + var filterLabelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; + var filterItemStyle = { marginBottom: 12 }; + var filterControlStyle = { width: '100%' }; + var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; + var tableSingleLineStyle = '.return-car-list-table .ant-table-thead th,.return-car-list-table .ant-table-tbody td{white-space:nowrap;}'; + var reqTitleStyle = { fontSize: 18, fontWeight: 600, marginBottom: 16, color: 'rgba(0,0,0,0.85)' }; + var reqSectionStyle = { fontSize: 15, fontWeight: 600, marginTop: 16, marginBottom: 8, color: 'rgba(0,0,0,0.85)' }; + var reqItemStyle = { fontSize: 13, marginLeft: 16, marginTop: 4, marginBottom: 2, lineHeight: 1.6, color: 'rgba(0,0,0,0.75)' }; + + var pendingColumns = [ + { title: '交车时间', dataIndex: 'deliveryTime', key: 'deliveryTime', width: 160 }, + { title: '交车人', dataIndex: 'deliveryPerson', key: 'deliveryPerson', width: 90 }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 100 }, + { title: '车辆类型', dataIndex: 'vehicleType', key: 'vehicleType', width: 120, ellipsis: true }, + { title: '品牌', dataIndex: 'brand', key: 'brand', width: 80 }, + { title: '型号', dataIndex: 'model', key: 'model', width: 110, ellipsis: true }, + { title: '车辆识别代码', dataIndex: 'vin', key: 'vin', width: 160, ellipsis: true }, + { title: '合同编码', dataIndex: 'contractCode', key: 'contractCode', width: 130, ellipsis: true }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 160, ellipsis: true }, + { title: '项目名称', dataIndex: 'projectName', key: 'projectName', width: 140, ellipsis: true }, + { title: '业务部门', dataIndex: 'dept', key: 'dept', width: 110, ellipsis: true }, + { title: '业务负责人', dataIndex: 'bizOwner', key: 'bizOwner', width: 100 }, + { + title: '操作', + key: 'action', + width: 160, + fixed: 'right', + render: function (_, record) { + return React.createElement('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } }, + record.vehicleArrived + ? React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0 }, onClick: handleReturnCar }, '还车') + : React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0 }, onClick: function () { handleVehicleArrived(record); } }, '车辆到达') + ); + } + } + ]; + + var historyColumns = [ + { title: '交车时间', dataIndex: 'deliveryTime', key: 'deliveryTime', width: 160 }, + { title: '交车人', dataIndex: 'deliveryPerson', key: 'deliveryPerson', width: 90 }, + { title: '还车时间', dataIndex: 'returnTime', key: 'returnTime', width: 160 }, + { title: '还车人', dataIndex: 'deliveryPerson', key: 'returnPersonAsDelivery', width: 90 }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 100 }, + { title: '车辆类型', dataIndex: 'vehicleType', key: 'vehicleType', width: 120, ellipsis: true }, + { title: '品牌', dataIndex: 'brand', key: 'brand', width: 80 }, + { title: '型号', dataIndex: 'model', key: 'model', width: 110, ellipsis: true }, + { title: '车辆识别代码', dataIndex: 'vin', key: 'vin', width: 160, ellipsis: true }, + { title: '合同编码', dataIndex: 'contractCode', key: 'contractCode', width: 130, ellipsis: true }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 160, ellipsis: true }, + { title: '项目名称', dataIndex: 'projectName', key: 'projectName', width: 140, ellipsis: true }, + { title: '业务部门', dataIndex: 'dept', key: 'dept', width: 110, ellipsis: true }, + { title: '业务负责人', dataIndex: 'bizOwner', key: 'bizOwner', width: 100 }, + { + title: '操作', + key: 'action', + width: 80, + fixed: 'right', + render: function () { + return React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0 }, onClick: handleViewHistory }, '查看'); + } + } + ]; + + var filterItems = [ + React.createElement('div', { key: 'contractCode', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '合同编码'), + React.createElement(Select, { + placeholder: '请选择或输入合同编码', + style: filterControlStyle, + value: fd.contractCode, + onChange: function (v) { setFd(function (p) { return Object.assign({}, p, { contractCode: v }); }); }, + allowClear: true, + showSearch: true, + options: contractOptions, + filterOption: filterOption + })), + React.createElement('div', { key: 'projectName', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '项目名称'), + React.createElement(Select, { + placeholder: '请选择或输入项目名称', + style: filterControlStyle, + value: fd.projectName, + onChange: function (v) { setFd(function (p) { return Object.assign({}, p, { projectName: v }); }); }, + allowClear: true, + showSearch: true, + options: projectOptions, + filterOption: filterOption + })), + React.createElement('div', { key: 'customerName', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '客户名称'), + React.createElement(Select, { + placeholder: '请选择或输入客户名称', + style: filterControlStyle, + value: fd.customerName, + onChange: function (v) { setFd(function (p) { return Object.assign({}, p, { customerName: v }); }); }, + allowClear: true, + showSearch: true, + options: customerOptions, + filterOption: filterOption + })), + React.createElement('div', { key: 'plateNo', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '车牌号'), + React.createElement(Select, { + placeholder: '请选择或输入车牌号', + style: filterControlStyle, + value: fd.plateNo, + onChange: function (v) { setFd(function (p) { return Object.assign({}, p, { plateNo: v }); }); }, + allowClear: true, + showSearch: true, + options: plateOptions, + filterOption: filterOption + })), + React.createElement('div', { key: 'vehicleTypes', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '车辆类型'), + React.createElement(Select, { + mode: 'multiple', + placeholder: '请选择(可多选)', + style: filterControlStyle, + value: fd.vehicleTypes, + onChange: function (v) { setFd(function (p) { return Object.assign({}, p, { vehicleTypes: v || [] }); }); }, + allowClear: true, + options: vehicleTypeOptions + })), + React.createElement('div', { key: 'brands', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '品牌'), + React.createElement(Select, { + mode: 'multiple', + placeholder: '请选择(可多选)', + style: filterControlStyle, + value: fd.brands, + onChange: function (v) { setFd(function (p) { return Object.assign({}, p, { brands: v || [] }); }); }, + allowClear: true, + options: brandOptions + })), + React.createElement('div', { key: 'model', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '型号'), + React.createElement(Select, { + placeholder: '请选择或输入型号', + style: filterControlStyle, + value: fd.model, + onChange: function (v) { setFd(function (p) { return Object.assign({}, p, { model: v }); }); }, + allowClear: true, + showSearch: true, + options: modelOptions, + filterOption: filterOption + })) + ]; + + var filterCount = filterExpanded ? 7 : 3; + var filterNodes = []; + for (var fi = 0; fi < filterCount && fi < filterItems.length; fi++) { + filterNodes.push(filterItems[fi]); + } + + var tabItems = [ + { + key: 'pending', + label: '待处理', + children: React.createElement(Table, { + rowKey: 'id', + columns: pendingColumns, + dataSource: pagedPending, + scroll: { x: 1680 }, + size: 'small', + pagination: { + current: page, + pageSize: pageSize, + total: filteredPending.length, + showSizeChanger: true, + showQuickJumper: true, + showTotal: function (t) { return '共 ' + t + ' 条'; }, + onChange: function (pg, ps) { setPage(pg); if (ps) setPageSize(ps); } + } + }) + }, + { + key: 'history', + label: '历史记录', + children: React.createElement(Table, { + rowKey: 'id', + columns: historyColumns, + dataSource: pagedHistory, + scroll: { x: 1880 }, + size: 'small', + pagination: { + current: page, + pageSize: pageSize, + total: filteredHistory.length, + showSizeChanger: true, + showQuickJumper: true, + showTotal: function (t) { return '共 ' + t + ' 条'; }, + onChange: function (pg, ps) { setPage(pg); if (ps) setPageSize(ps); } + } + }) + } + ]; + + return React.createElement(App, null, + React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + React.createElement(Breadcrumb, { + items: [ + { title: '运维管理' }, + { title: '车辆业务' }, + { title: '还车管理' } + ] + }), + React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { setRequirementModalVisible(true); } }, '查看需求说明') + ), + React.createElement(Card, { style: { marginBottom: 16 } }, + React.createElement('div', { + style: { + display: 'grid', + gridTemplateColumns: '1fr 1fr 1fr', + gap: '16px 24px', + alignItems: 'start', + flex: 1, + minWidth: 0 + } + }, filterNodes), + React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 } }, + React.createElement(Button, { onClick: handleReset }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleQuery }, '查询'), + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { setFilterExpanded(!filterExpanded); } }, filterExpanded ? '收起' : '展开') + ) + ), + React.createElement(Card, null, + React.createElement(React.Fragment, null, + React.createElement('style', null, tableSingleLineStyle), + React.createElement('div', { className: 'return-car-list-table' }, + React.createElement(Tabs, { + activeKey: tab, + onChange: function (k) { setTab(k); setPage(1); }, + tabBarExtraContent: React.createElement(Button, { onClick: handleExport }, '导出'), + items: tabItems + }) + ) + ) + ), + React.createElement(Modal, { + title: '需求说明', + open: requirementModalVisible, + onCancel: function () { setRequirementModalVisible(false); }, + width: 720, + footer: React.createElement(Button, { onClick: function () { setRequirementModalVisible(false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, React.createElement('div', { style: { padding: '8px 0' } }, + React.createElement('div', { style: reqTitleStyle }, '还车管理'), + React.createElement('div', { style: { fontSize: 14, marginBottom: 12, color: 'rgba(0,0,0,0.85)' } }, '数字化资产 ONEOS 运管平台 · 运维管理 - 车辆业务 - 还车管理'), + React.createElement('div', { style: reqSectionStyle }, '筛选与列表'), + React.createElement('div', { style: reqItemStyle }, '面包屑:运维管理 - 车辆业务 - 还车管理;筛选区为三列栅格,默认一行(合同编码、项目名称、客户名称),展开后显示车牌号、车辆类型(多选)、品牌(多选)、型号;重置、查询、展开/收起与列表联动。'), + React.createElement('div', { style: reqItemStyle }, '列表分「待处理」「历史记录」Tab,Tab 右侧为导出;待处理支持车辆到达确认后显示还车;历史记录支持查看。') + )) + ) + ); +}; + +if (typeof window !== 'undefined') { + window.Component = Component; + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function () { + var rootEl = document.getElementById('root'); + if (rootEl && window.ReactDOM && window.React) { + var root = ReactDOM.createRoot(rootEl); + root.render(React.createElement(Component)); + } + }); + } else { + var rootEl = document.getElementById('root'); + if (rootEl && window.ReactDOM && window.React) { + var root = ReactDOM.createRoot(rootEl); + root.render(React.createElement(Component)); + } + } +} + +export default Component; diff --git a/src/prototypes/oneos-web-ops/pages/22-后装设备.jsx b/src/prototypes/oneos-web-ops/pages/22-后装设备.jsx new file mode 100644 index 0000000..1fd5eef --- /dev/null +++ b/src/prototypes/oneos-web-ops/pages/22-后装设备.jsx @@ -0,0 +1,483 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 后装设备 - 运维管理-车辆业务 +var ARCO_TOKEN = { + primary: '#165DFF', + primaryHover: '#4080FF', + danger: '#F53F3F', + success: '#00B42A', + neutral1: '#FFFFFF', + neutral2: '#F7F8FA', + neutral3: '#F2F3F5', + neutral4: '#E5E6EB', + neutral5: '#C9CDD4', + neutral6: '#86909C', + neutral7: '#4E5969', + neutral8: '#1D2129', + border: '#E5E6EB', + fill: '#F2F3F5', + fillSecondary: '#F7F8FA', + shadowLight: '0 1px 2px rgba(0,0,0,0.05)', + radiusMedium: '4px', + radiusLarge: '8px', + spacing8: '8px', + spacing12: '12px', + spacing16: '16px', + spacing24: '24px', + fontSize14: '14px', + fontSize16: '16px', + fontFamily: '-apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", Arial, sans-serif', + link: '#165DFF' +}; + +var DEVICE_TYPES = ['GPS', '尾板', '车身广告', 'G7安全套件', 'G7普通设备', 'G7温控设备', '备胎']; + +var MOCK_VEHICLE_OPTIONS = ['粤A12345', '粤A67890', '粤B11111', '粤B22222', '粤C33333', '京A88888']; + +var MOCK_LIST = [ + { id: '1', deviceType: 'GPS', plateNo: '粤A12345', supplier: '深圳智联', lastOpType: '安装', createTime: '2025-02-20 10:30', removeTime: '', createOperator: '张明', removeOperator: '' }, + { id: '2', deviceType: '尾板', plateNo: '粤A67890', supplier: '广州机械', lastOpType: '拆除', createTime: '2025-02-01 09:00', removeTime: '2025-02-18 14:20', createOperator: '李强', removeOperator: '王芳' }, + { id: '3', deviceType: '车身广告', plateNo: '粤B11111', supplier: '东莞广告', lastOpType: '安装', createTime: '2025-02-15 09:00', removeTime: '', createOperator: '李强', removeOperator: '' }, + { id: '4', deviceType: 'G7安全套件', plateNo: '粤A12345', supplier: 'G7科技', lastOpType: '安装', createTime: '2025-02-10 16:45', removeTime: '', createOperator: '张明', removeOperator: '' }, + { id: '5', deviceType: '备胎', plateNo: '粤C33333', supplier: '华南轮胎', lastOpType: '拆除', createTime: '2025-01-20 08:00', removeTime: '2025-02-08 11:20', createOperator: '赵六', removeOperator: '赵六' } +]; + +const Component = function () { + var antd = window.antd; + var Select = antd.Select; + var Button = antd.Button; + var DatePicker = antd.DatePicker; + var Table = antd.Table; + var Modal = antd.Modal; + var Input = antd.Input; + var message = antd.message; + var Option = Select.Option; + var RangePicker = DatePicker.RangePicker; + var Tabs = antd.Tabs; + + var _useStateTab = React.useState('installed'); + var activeTab = _useStateTab[0]; + var setActiveTab = _useStateTab[1]; + + var _useState = React.useState(''); + var filterDeviceType = _useState[0]; + var setFilterDeviceType = _useState[1]; + + var _useState2 = React.useState(''); + var filterVehicle = _useState2[0]; + var setFilterVehicle = _useState2[1]; + + var _useState3 = React.useState([]); + var filterDateRange = _useState3[0]; + var setFilterDateRange = _useState3[1]; + + var _useState4 = React.useState({ deviceType: '', vehicle: '', dateStart: '', dateEnd: '' }); + var appliedFilter = _useState4[0]; + var setAppliedFilter = _useState4[1]; + + var _useState5 = React.useState(1); + var currentPage = _useState5[0]; + var setCurrentPage = _useState5[1]; + + var _useState6 = React.useState(10); + var pageSize = _useState6[0]; + var setPageSize = _useState6[1]; + + var _useState7 = React.useState(MOCK_LIST); + var dataList = _useState7[0]; + var setDataList = _useState7[1]; + + var _useState8 = React.useState(null); + var viewRecord = _useState8[0]; + var setViewRecord = _useState8[1]; + + var _useState9 = React.useState(null); + var editRecord = _useState9[0]; + var setEditRecord = _useState9[1]; + + var _useState10 = React.useState(null); + var removeConfirmRecord = _useState10[0]; + var setRemoveConfirmRecord = _useState10[1]; + + var _useState11 = React.useState(false); + var addModalVisible = _useState11[0]; + var setAddModalVisible = _useState11[1]; + + var _useState12 = React.useState(false); + var showRequirementModal = _useState12[0]; + var setShowRequirementModal = _useState12[1]; + + var applyBaseFilter = function (list) { + if (appliedFilter.deviceType) { + list = list.filter(function (r) { return r.deviceType === appliedFilter.deviceType; }); + } + if (appliedFilter.vehicle) { + list = list.filter(function (r) { return r.plateNo && r.plateNo.indexOf(appliedFilter.vehicle) >= 0; }); + } + return list; + }; + + var installedList = dataList.filter(function (r) { return !r.removeTime || r.removeTime === ''; }); + var removedList = dataList.filter(function (r) { return r.removeTime && r.removeTime !== ''; }); + + var installedFiltered = applyBaseFilter(installedList).filter(function (r) { + if (appliedFilter.dateStart && r.createTime) { if (r.createTime.slice(0, 10) < appliedFilter.dateStart) return false; } + if (appliedFilter.dateEnd && r.createTime) { if (r.createTime.slice(0, 10) > appliedFilter.dateEnd) return false; } + return true; + }); + var removedFiltered = applyBaseFilter(removedList).filter(function (r) { + if (appliedFilter.dateStart && r.removeTime) { if (r.removeTime.slice(0, 10) < appliedFilter.dateStart) return false; } + if (appliedFilter.dateEnd && r.removeTime) { if (r.removeTime.slice(0, 10) > appliedFilter.dateEnd) return false; } + return true; + }); + + var filteredList = activeTab === 'installed' ? installedFiltered : removedFiltered; + var totalItems = filteredList.length; + var totalPages = Math.ceil(totalItems / pageSize) || 1; + var validPage = currentPage > totalPages && totalPages > 0 ? 1 : (currentPage < 1 ? 1 : currentPage); + var startIndex = (validPage - 1) * pageSize; + var paginatedList = filteredList.slice(startIndex, startIndex + pageSize); + + var handleQuery = function () { + var dateStart = ''; + var dateEnd = ''; + if (filterDateRange && filterDateRange.length >= 2 && filterDateRange[0] && filterDateRange[1]) { + if (filterDateRange[0].format) { + dateStart = filterDateRange[0].format('YYYY-MM-DD'); + dateEnd = filterDateRange[1].format('YYYY-MM-DD'); + } else if (window.dayjs) { + dateStart = window.dayjs(filterDateRange[0]).format('YYYY-MM-DD'); + dateEnd = window.dayjs(filterDateRange[1]).format('YYYY-MM-DD'); + } + } + setAppliedFilter({ + deviceType: filterDeviceType, + vehicle: filterVehicle, + dateStart: dateStart, + dateEnd: dateEnd + }); + setCurrentPage(1); + message.success('查询成功'); + }; + + var handleReset = function () { + setFilterDeviceType(''); + setFilterVehicle(''); + setFilterDateRange([]); + setAppliedFilter({ deviceType: '', vehicle: '', dateStart: '', dateEnd: '' }); + setCurrentPage(1); + }; + + var handleRemoveConfirm = function (row) { + var nowStr = '2025-02-24 15:00'; + if (window.dayjs) { nowStr = window.dayjs().format('YYYY-MM-DD HH:mm'); } + setDataList(dataList.map(function (r) { + if (r.id === row.id) { + return { id: r.id, deviceType: r.deviceType, plateNo: r.plateNo, supplier: r.supplier, lastOpType: '拆除', createTime: r.createTime, removeTime: nowStr, createOperator: r.createOperator, removeOperator: '当前用户' }; + } + return r; + })); + setRemoveConfirmRecord(null); + message.success('拆除成功'); + }; + + var t = ARCO_TOKEN; + var styles = { + page: { padding: t.spacing24, fontFamily: t.fontFamily, backgroundColor: t.fill, minHeight: '100vh' }, + breadcrumb: { marginBottom: t.spacing16, fontSize: t.fontSize14, color: t.neutral6, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }, + breadcrumbLeft: { display: 'flex', alignItems: 'center' }, + breadcrumbLink: { color: t.link, textDecoration: 'none', marginRight: '8px' }, + breadcrumbCurrent: { color: t.neutral8 }, + requirementLink: { color: t.link, textDecoration: 'none', fontSize: t.fontSize14, cursor: 'pointer' }, + modalContent: { fontSize: t.fontSize14, color: t.neutral8, lineHeight: 1.6 }, + requirementSection: { marginBottom: t.spacing16 }, + requirementSectionTitle: { fontSize: t.fontSize16, fontWeight: 600, color: t.neutral8, marginBottom: 8 }, + requirementItem: { marginBottom: 8, paddingLeft: 16 }, + requirementSubItem: { marginBottom: 4, paddingLeft: 16, color: t.neutral7 }, + card: { backgroundColor: t.neutral1, borderRadius: t.radiusLarge, boxShadow: t.shadowLight, marginBottom: t.spacing16, padding: t.spacing16 }, + filterRow: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: t.spacing12 }, + label: { marginRight: t.spacing8, fontSize: t.fontSize14, color: t.neutral8, whiteSpace: 'nowrap' }, + filterRight: { marginLeft: 'auto', display: 'flex', gap: t.spacing8 }, + toolbar: { display: 'flex', justifyContent: 'flex-end', gap: t.spacing12, marginBottom: t.spacing8 } + }; + + var deviceTypeOptions = DEVICE_TYPES.map(function (type) { + return React.createElement(Option, { key: type, value: type }, type); + }); + + var vehicleOptions = MOCK_VEHICLE_OPTIONS.map(function (v) { + return React.createElement(Option, { key: v, value: v }, v); + }); + + return React.createElement( + 'div', + { style: styles.page }, + React.createElement( + 'div', + { style: styles.breadcrumb }, + React.createElement('div', { style: styles.breadcrumbLeft }, + React.createElement('a', { href: '#', style: styles.breadcrumbLink, onClick: function (e) { e.preventDefault(); } }, '运维管理'), + React.createElement('span', { style: { marginRight: '8px' } }, '/'), + React.createElement('a', { href: '#', style: styles.breadcrumbLink, onClick: function (e) { e.preventDefault(); } }, '车辆业务'), + React.createElement('span', { style: { marginRight: '8px' } }, '/'), + React.createElement('span', { style: styles.breadcrumbCurrent }, '后装设备') + ), + React.createElement('a', { href: '#', style: styles.requirementLink, onClick: function (e) { e.preventDefault(); setShowRequirementModal(true); } }, '查看需求说明') + ), + React.createElement( + 'div', + { style: styles.card }, + React.createElement( + 'div', + { style: styles.filterRow }, + React.createElement('span', { style: styles.label }, '设备类型:'), + React.createElement(Select, { + placeholder: '请选择设备类型', + style: { width: 160 }, + value: filterDeviceType || undefined, + onChange: function (v) { setFilterDeviceType(v || ''); }, + allowClear: true + }, deviceTypeOptions), + React.createElement('span', { style: styles.label }, '使用车辆:'), + React.createElement(Select, { + placeholder: '请选择或输入搜索', + style: { width: 180 }, + value: filterVehicle || undefined, + onChange: function (v) { setFilterVehicle(v || ''); }, + showSearch: true, + allowClear: true, + filterOption: function (input, opt) { + var c = opt && opt.children; + return c && String(c).toLowerCase().indexOf((input || '').toLowerCase()) >= 0; + } + }, vehicleOptions), + React.createElement('span', { style: styles.label }, '安装时间:'), + React.createElement(RangePicker, { + style: { width: 260 }, + format: 'YYYY-MM-DD', + placeholder: ['开始日期', '结束日期'], + value: filterDateRange, + onChange: function (dates) { setFilterDateRange(dates || []); } + }), + React.createElement('div', { style: styles.filterRight }, + React.createElement(Button, { type: 'primary', onClick: handleQuery }, '查询'), + React.createElement(Button, { onClick: handleReset }, '重置') + ) + ) + ), + React.createElement( + 'div', + { style: styles.card }, + React.createElement( + 'div', + { style: styles.toolbar }, + activeTab === 'installed' && React.createElement(Button, { type: 'primary', onClick: function () { message.info('请查看运维管理-车辆业务-后装设备-新增后装设备'); } }, '新增'), + React.createElement(Button, { onClick: function () { message.info('根据筛选条件导出excel'); } }, '导出') + ), + React.createElement(Tabs, { + activeKey: activeTab, + onChange: function (key) { setActiveTab(key); setCurrentPage(1); }, + items: [ + { + key: 'installed', + label: '已安装', + children: React.createElement(Table, { + rowKey: 'id', + size: 'small', + columns: [ + { title: '设备类型', dataIndex: 'deviceType', key: 'deviceType', width: 120 }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 120 }, + { title: '供应商', dataIndex: 'supplier', key: 'supplier', width: 120 }, + { title: '安装时间', dataIndex: 'createTime', key: 'createTime', width: 160 }, + { title: '安装人', dataIndex: 'createOperator', key: 'createOperator', width: 100 }, + { + title: '操作', + key: 'action', + width: 160, + render: function (_, row) { + return React.createElement(React.Fragment, null, + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { message.info('请查看运维管理-车辆业务-后装设备-查看'); } }, '查看'), + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { message.info('请查看运维管理-车辆业务-后装设备-编辑'); } }, '编辑'), + React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function () { setRemoveConfirmRecord(row); } }, '拆除') + ); + } + } + ], + dataSource: paginatedList, + pagination: { + current: validPage, + pageSize: pageSize, + total: totalItems, + showSizeChanger: true, + showQuickJumper: true, + pageSizeOptions: ['10', '20', '50', '100'], + showTotal: function (total) { return '共 ' + total + ' 条'; }, + onChange: function (page, size) { + setCurrentPage(page); + if (size !== pageSize) setPageSize(size); + } + } + }) + }, + { + key: 'removed', + label: '拆除记录', + children: React.createElement(Table, { + rowKey: 'id', + size: 'small', + columns: [ + { title: '设备类型', dataIndex: 'deviceType', key: 'deviceType', width: 120 }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 120 }, + { title: '供应商', dataIndex: 'supplier', key: 'supplier', width: 120 }, + { title: '拆除时间', dataIndex: 'removeTime', key: 'removeTime', width: 160 }, + { title: '拆除人', dataIndex: 'removeOperator', key: 'removeOperator', width: 100 }, + { + title: '操作', + key: 'action', + width: 80, + render: function (_, row) { + return React.createElement(Button, { type: 'link', size: 'small', onClick: function () { message.info('请查看运维管理-车辆业务-后装设备-查看'); } }, '查看'); + } + } + ], + dataSource: paginatedList, + pagination: { + current: validPage, + pageSize: pageSize, + total: totalItems, + showSizeChanger: true, + showQuickJumper: true, + pageSizeOptions: ['10', '20', '50', '100'], + showTotal: function (total) { return '共 ' + total + ' 条'; }, + onChange: function (page, size) { + setCurrentPage(page); + if (size !== pageSize) setPageSize(size); + } + } + }) + } + ] + }) + ), + viewRecord && React.createElement(Modal, { + title: '查看后装设备', + open: !!viewRecord, + onCancel: function () { setViewRecord(null); }, + footer: React.createElement(Button, { onClick: function () { setViewRecord(null); } }, '关闭'), + width: 520, + children: viewRecord ? React.createElement('div', { style: { lineHeight: '2' } }, + React.createElement('div', null, '设备类型:', viewRecord.deviceType), + React.createElement('div', null, '车牌号:', viewRecord.plateNo), + React.createElement('div', null, '供应商:', viewRecord.supplier), + React.createElement('div', null, '安装时间:', viewRecord.createTime || '-'), + React.createElement('div', null, '安装人:', viewRecord.createOperator || '-'), + React.createElement('div', null, '拆除时间:', viewRecord.removeTime || '-'), + React.createElement('div', null, '拆除人:', viewRecord.removeOperator || '-') + ) : null + }), + editRecord && React.createElement(Modal, { + title: '编辑后装设备', + open: !!editRecord, + onCancel: function () { setEditRecord(null); }, + onOk: function () { + setEditRecord(null); + message.success('保存成功'); + }, + okText: '保存', + cancelText: '取消', + width: 520, + children: editRecord ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 16 } }, + React.createElement('div', null, React.createElement('span', { style: styles.label }, '设备类型'), React.createElement(Select, { style: { width: '100%' }, value: editRecord.deviceType, options: DEVICE_TYPES.map(function (d) { return { label: d, value: d }; }) })), + React.createElement('div', null, React.createElement('span', { style: styles.label }, '车牌号'), React.createElement(Input, { style: { width: '100%' }, value: editRecord.plateNo, readOnly: true })), + React.createElement('div', null, React.createElement('span', { style: styles.label }, '供应商'), React.createElement(Input, { style: { width: '100%' }, defaultValue: editRecord.supplier })) + ) : null + }), + removeConfirmRecord && React.createElement(Modal, { + title: '确认拆除', + open: !!removeConfirmRecord, + onCancel: function () { setRemoveConfirmRecord(null); }, + onOk: function () { handleRemoveConfirm(removeConfirmRecord); }, + okText: '确认拆除', + cancelText: '取消', + okButtonProps: { danger: true }, + children: removeConfirmRecord ? React.createElement('div', null, '确定要拆除该车辆「', removeConfirmRecord.deviceType, '」后装设备吗?') : null + }), + React.createElement(Modal, { + title: '需求说明', + open: showRequirementModal, + onCancel: function () { setShowRequirementModal(false); }, + footer: React.createElement(Button, { onClick: function () { setShowRequirementModal(false); } }, '关闭'), + width: 720, + children: React.createElement('div', { style: styles.modalContent }, + React.createElement('div', { style: styles.requirementSection }, + React.createElement('div', { style: styles.requirementSectionTitle }, '1. 面包屑:'), + React.createElement('div', { style: styles.requirementItem }, '1.1. 运维管理-车辆业务-后装设备') + ), + React.createElement('div', { style: styles.requirementSection }, + React.createElement('div', { style: styles.requirementSectionTitle }, '2. 筛选:'), + React.createElement('div', { style: styles.requirementItem }, '2.1. 支持通过设备类型、使用车辆、安装日期进行管理,点击查询后,筛选条件与列表内容联动。点击重置会回到默认筛选条件并在列表展示结果:'), + React.createElement('div', { style: styles.requirementSubItem }, '2.1.1. 设备类型:选择器,包括GPS、尾板、车身广告、G7安全套件、G7普通设备、G7温控设备、备胎;'), + React.createElement('div', { style: styles.requirementSubItem }, '2.1.2. 使用车辆:选择器,支持通过输入框对输入内容模糊搜索,并下拉对应选项;'), + React.createElement('div', { style: styles.requirementSubItem }, '2.1.3. 安装时间:日期选择器,支持单输入框内双日历选择开始-结束时间;') + ), + React.createElement('div', { style: styles.requirementSection }, + React.createElement('div', { style: styles.requirementSectionTitle }, '3. 列表:'), + React.createElement('div', { style: styles.requirementItem }, '列表分为2个tab,已安装/拆除记录;'), + React.createElement('div', { style: styles.requirementItem }, '当已安装列表对应车辆存在车身广告/尾板时,在备车时会自动拉取车身广告/尾板为有的状态;'), + React.createElement('div', { style: styles.requirementItem }, '当已安装列表不存在车身广告/尾板时,在备车时车身广告/尾板为无的状态;'), + React.createElement('div', { style: styles.requirementItem }, '如果在备车/交车时手动取消或勾选车身广告尾板,并完成提交,后装设备列表也会新增一条该车辆的车身广告/尾板安装数据;'), + React.createElement('div', { style: styles.requirementSectionTitle }, '3.1. 已安装:'), + React.createElement('div', { style: styles.requirementSubItem }, '3.1.1. 列表展示所有后装设备信息,字段依次为:设备类型、车牌号、供应商、安装时间、安装人、操作;列表右上角为新增、导出;'), + React.createElement('div', { style: styles.requirementSubItem }, '3.1.1.1. 设备类型:显示后装设备类型,包括:GPS、尾板、车身广告、G7安全套件、G7普通设备、G7温控设备、备胎;'), + React.createElement('div', { style: styles.requirementSubItem }, '3.1.1.2. 车牌号:显示后装设备对应车牌号信息;'), + React.createElement('div', { style: styles.requirementSubItem }, '3.1.1.3. 供应商:显示后装设备供应商信息;'), + React.createElement('div', { style: styles.requirementSubItem }, '3.1.1.4. 安装时间:显示后装设备安装时间,格式为YYYY-MM-DD HH:MM;'), + React.createElement('div', { style: styles.requirementSubItem }, '3.1.1.5. 安装人:显示后装设备安装用户名称;'), + React.createElement('div', { style: styles.requirementSubItem }, '3.1.1.6. 操作:支持查看、编辑、拆除;'), + React.createElement('div', { style: styles.requirementSectionTitle }, '3.2. 拆除记录:'), + React.createElement('div', { style: styles.requirementSubItem }, '3.2.1. 列表展示所有后装设备拆除记录,字段依次为:设备类型、车牌号、供应商、拆除时间、拆除人、操作;列表右上角为新增、导出;'), + React.createElement('div', { style: styles.requirementSubItem }, '3.2.1.1. 设备类型:显示后装设备类型,包括:GPS、尾板、车身广告、G7安全套件、G7普通设备、G7温控设备、备胎;'), + React.createElement('div', { style: styles.requirementSubItem }, '3.2.1.2. 车牌号:显示后装设备对应车牌号信息;'), + React.createElement('div', { style: styles.requirementSubItem }, '3.2.1.3. 供应商:显示后装设备供应商信息;'), + React.createElement('div', { style: styles.requirementSubItem }, '3.2.1.4. 拆除时间:显示后装设备拆除时间,格式为YYYY-MM-DD HH:MM;'), + React.createElement('div', { style: styles.requirementSubItem }, '3.2.1.5. 拆除人:显示后装设备拆除用户名称;'), + React.createElement('div', { style: styles.requirementSubItem }, '3.2.1.6. 操作:支持查看;') + ) + ) + }), + addModalVisible && React.createElement(Modal, { + title: '新增后装设备', + open: addModalVisible, + onCancel: function () { setAddModalVisible(false); }, + onOk: function () { + setDataList([{ id: String(Date.now()), deviceType: 'GPS', plateNo: '粤A12345', supplier: '新供应商', lastOpType: '安装', createTime: '2025-02-24 12:00', removeTime: '', createOperator: '当前用户', removeOperator: '' }].concat(dataList)); + setAddModalVisible(false); + message.success('新增成功'); + }, + okText: '确定', + cancelText: '取消', + width: 520, + children: React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 16 } }, + React.createElement('div', null, React.createElement('span', { style: styles.label }, '设备类型'), React.createElement(Select, { style: { width: '100%' }, placeholder: '请选择', options: DEVICE_TYPES.map(function (d) { return { label: d, value: d }; }) })), + React.createElement('div', null, React.createElement('span', { style: styles.label }, '使用车辆'), React.createElement(Select, { style: { width: '100%' }, placeholder: '请选择或搜索', showSearch: true, options: MOCK_VEHICLE_OPTIONS.map(function (v) { return { label: v, value: v }; }) })), + React.createElement('div', null, React.createElement('span', { style: styles.label }, '供应商'), React.createElement(Input, { style: { width: '100%' }, placeholder: '请输入供应商' })) + ) + }) + ); +}; +if (typeof window !== 'undefined') { + window.Component = Component; + function mount() { + var rootEl = document.getElementById('root'); + if (rootEl && window.ReactDOM && window.React) { + var root = ReactDOM.createRoot(rootEl); + root.render(React.createElement(Component)); + } + } + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', mount); + } else { + setTimeout(mount, 0); + } +} + +export default Component; diff --git a/src/prototypes/oneos-web-ops/pages/23-交车管理-编辑抽屉.jsx b/src/prototypes/oneos-web-ops/pages/23-交车管理-编辑抽屉.jsx new file mode 100644 index 0000000..1f48994 --- /dev/null +++ b/src/prototypes/oneos-web-ops/pages/23-交车管理-编辑抽屉.jsx @@ -0,0 +1,505 @@ +// 交车管理 - 列表内编辑抽屉(参照 交车管理-交车单-编辑.jsx) +// 使用方式:window.DeliveryEditDrawer(props) + +function DeliveryEditDrawer(props) { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + var useRef = React.useRef; + var useEffect = React.useEffect; + + var open = props.open; + var record = props.record; + var onClose = props.onClose; + var onSave = props.onSave; + var onSubmit = props.onSubmit; + + var antd = window.antd; + var Drawer = antd.Drawer; + var Button = antd.Button; + var Input = antd.Input; + var Select = antd.Select; + var Switch = antd.Switch; + var Modal = antd.Modal; + var Table = antd.Table; + var Tag = antd.Tag; + var message = antd.message; + + function RequiredLabel(text) { + return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 4 } }, + React.createElement('span', { style: { color: '#ef4444', fontWeight: 600 } }, '*'), + React.createElement('span', null, text) + ); + } + + function isEmpty(v) { + return v === null || v === undefined || String(v).trim() === ''; + } + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + function fileToDataUrl(file, cb) { + try { + var reader = new FileReader(); + reader.onload = function (e) { cb(null, (e && e.target && e.target.result) || ''); }; + reader.onerror = function () { cb(new Error('read error')); }; + reader.readAsDataURL(file); + } catch (e) { cb(e); } + } + + var reserveVehicles = useMemo(function () { + return [ + { plateNo: '京A12345', vehicleType: '牵引车', brand: '东风', model: 'DFH1180', vin: 'LJNAU1A2XK1234567', hasAd: true, adPhoto: [{ uid: 'ad1', name: '广告照片.jpg', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=Ad' }], bigWordPhoto: [{ uid: 'bw1', name: '放大字.jpg', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=BigWord' }], hasTailboard: true }, + { plateNo: '浙F80088', vehicleType: '厢式车', brand: '福田', model: 'BJ1180', vin: 'LJNAU1A2XK7654321', hasAd: false, adPhoto: [], bigWordPhoto: [], hasTailboard: false }, + { plateNo: '沪A30003', vehicleType: '厢式车', brand: '重汽', model: 'HOWO-T5G', vin: 'LJNAU1A2XK9999000', hasAd: true, adPhoto: [{ uid: 'ad2', name: '广告2.jpg', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=Ad2' }], bigWordPhoto: [{ uid: 'bw2', name: '放大字2.jpg', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=BW2' }], hasTailboard: true }, + { plateNo: '粤AGP4598', vehicleType: '4.5吨冷链车', brand: '现代', model: '帕力安牌4.5吨冷链车', vin: 'LNBSCPKB9RR223402', hasAd: false, adPhoto: [], bigWordPhoto: [], hasTailboard: false } + ]; + }, []); + + var plateOptions = useMemo(function () { + return reserveVehicles.map(function (v) { return { value: v.plateNo, label: v.plateNo }; }); + }, [reserveVehicles]); + + var vehicleByPlate = useMemo(function () { + var map = {}; + reserveVehicles.forEach(function (v) { map[v.plateNo] = v; }); + return map; + }, [reserveVehicles]); + + function buildInitialForm(rec) { + var plate = (rec && rec.plateNo && String(rec.plateNo).trim()) || undefined; + var veh = plate ? vehicleByPlate[plate] : null; + if (!veh && rec && rec.brand) { + veh = { plateNo: plate, vehicleType: rec.vehicleType || '', brand: rec.brand || '', model: rec.model || '', vin: rec.vin || '', hasAd: false, adPhoto: [], bigWordPhoto: [], hasTailboard: false }; + } + return { + plateNo: plate, + vehicleType: (veh && veh.vehicleType) || (rec && rec.vehicleType) || '', + brand: (veh && veh.brand) || (rec && rec.brand) || '', + model: (veh && veh.model) || (rec && rec.model) || '', + vin: (veh && veh.vin) || (rec && rec.vin) || '', + hasAd: !!(veh && veh.hasAd), + adPhoto: (veh && veh.adPhoto) || [], + bigWordPhoto: (veh && veh.bigWordPhoto) || [], + hasTailboard: !!(veh && veh.hasTailboard), + spareTirePhoto: [], + spareTireDepth: '', + trainingRecognized: false, + driverLicenses: [], + mileageKm: rec && rec.deliveryMileage != null ? String(rec.deliveryMileage) : '', + batteryPct: rec && rec.deliveryElec != null ? String(rec.deliveryElec) : '', + hydrogenAmount: rec && rec.deliveryH2 != null ? String(rec.deliveryH2) : '', + hydrogenUnit: (rec && rec.deliveryH2Unit === 'MPa') ? 'MPa' : '%', + serviceFee: '' + }; + } + + var formState = useState(buildInitialForm(null)); + var form = formState[0]; + var setForm = formState[1]; + var activeSectionState = useState('basic'); + var activeSection = activeSectionState[0]; + var setActiveSection = activeSectionState[1]; + var submittingState = useState(false); + var submitting = submittingState[0]; + var setSubmitting = submittingState[1]; + var previewState = useState({ open: false, url: '', title: '' }); + var ocrModalState = useState({ open: false, photoUrl: '', depth: '6.50' }); + var inspectionOpenState = useState(false); + var trainingInputRef = useRef(null); + + useEffect(function () { + if (open && record) { + setForm(buildInitialForm(record)); + setActiveSection('basic'); + setSubmitting(false); + } + }, [open, record && record.id]); + + function updateForm(patch) { + setForm(function (p) { return Object.assign({}, p, patch); }); + } + + function handlePlateChange(v) { + var veh = vehicleByPlate[v]; + updateForm({ + plateNo: v, + vehicleType: (veh && veh.vehicleType) || '', + brand: (veh && veh.brand) || '', + model: (veh && veh.model) || '', + vin: (veh && veh.vin) || '', + hasAd: !!(veh && veh.hasAd), + adPhoto: (veh && (veh.adPhoto || [])) || [], + bigWordPhoto: (veh && (veh.bigWordPhoto || [])) || [], + hasTailboard: !!(veh && veh.hasTailboard), + spareTirePhoto: [], + spareTireDepth: '', + trainingRecognized: false, + driverLicenses: [] + }); + } + + function makeThumb(url, onPreview, onRemove) { + return React.createElement('div', { style: { width: 72, height: 72, borderRadius: 8, border: '1px solid #e2e8f0', overflow: 'hidden', position: 'relative', background: '#f8fafc' } }, + React.createElement('img', { src: url, style: { width: '100%', height: '100%', objectFit: 'cover', cursor: 'pointer' }, onClick: onPreview }), + onRemove ? React.createElement('button', { + type: 'button', + 'aria-label': '删除图片', + style: { position: 'absolute', right: 4, top: 4, width: 22, height: 22, borderRadius: 999, border: 'none', background: 'rgba(15,23,42,.65)', color: '#fff', cursor: 'pointer', fontSize: 12, lineHeight: '22px', padding: 0 }, + onClick: function (e) { e.stopPropagation(); onRemove(); } + }, '×') : null + ); + } + + function UploadBox(uploadProps) { + var label = uploadProps.label; + var value = uploadProps.value || []; + var max = uploadProps.max || 1; + var onChange = uploadProps.onChange; + function handlePick(e) { + var f = e && e.target && e.target.files && e.target.files[0]; + if (!f) return; + fileToDataUrl(f, function (err, url) { + if (err) { message.error('上传失败'); return; } + var next = value.slice(); + next.push({ uid: String(Date.now()), name: f.name || 'image', url: url }); + if (next.length > max) next = next.slice(next.length - max); + onChange && onChange(next); + }); + e.target.value = ''; + } + return React.createElement('div', null, + label ? React.createElement('div', { style: { fontSize: 13, color: '#475569', marginBottom: 8, fontWeight: 500 } }, label) : null, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' } }, + value.map(function (f) { + return React.createElement('div', { key: f.uid }, + makeThumb(f.url, function () { previewState[1]({ open: true, url: f.url, title: f.name }); }, function () { onChange && onChange(value.filter(function (x) { return x.uid !== f.uid; })); }) + ); + }), + value.length >= max ? null : React.createElement('label', { style: { width: 72, height: 72, borderRadius: 8, border: '1px dashed #cbd5e1', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#64748b', cursor: 'pointer', fontSize: 13, background: '#fff', transition: 'border-color .2s' } }, + React.createElement('input', { type: 'file', accept: 'image/*', style: { display: 'none' }, onChange: handlePick }), + '上传' + ) + ) + ); + } + + function FormItem(itemProps) { + return React.createElement('div', { style: { marginBottom: 16, minWidth: 0 } }, + React.createElement('div', { style: { fontSize: 13, color: '#475569', marginBottom: 6, fontWeight: 500 } }, + itemProps.required ? RequiredLabel(itemProps.label) : itemProps.label + ), + itemProps.children + ); + } + + var inspectionListState = useState([{ key: 'ins-1', category: '车灯', item: '大灯', checked: true, treadDepth: '', remark: '' }]); + var inspectionList = inspectionListState[0]; + + var sectionNav = [ + { key: 'basic', label: '车辆信息' }, + { key: 'equip', label: '设备证照' }, + { key: 'metrics', label: '交车数据' }, + { key: 'photos', label: '交车照片' } + ]; + + function scrollToSection(key) { + setActiveSection(key); + var el = document.getElementById('dv-edit-section-' + key); + if (el && el.scrollIntoView) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + + function validateSubmit() { + if (isEmpty(form.plateNo)) return '请选择车牌号'; + if (form.hasAd) { + if (!form.adPhoto.length) return '请上传广告照片'; + if (!form.bigWordPhoto.length) return '请上传放大字照片'; + } + if (!form.spareTirePhoto.length) return '请上传备胎照片'; + if (isEmpty(form.spareTireDepth)) return '请填写备胎胎纹深度'; + if (!form.trainingRecognized) return '请上传验车码并完成识别'; + if (isEmpty(form.mileageKm)) return '请填写交车里程'; + if (isEmpty(form.batteryPct)) return '请填写交车电量'; + if (isEmpty(form.hydrogenAmount)) return '请填写交车氢量'; + return ''; + } + + function buildPatchFromForm() { + return { + plateNo: form.plateNo || '', + vehicleType: form.vehicleType, + brand: form.brand, + model: form.model, + vin: form.vin, + deliveryMileage: form.mileageKm === '' ? null : Number(form.mileageKm), + deliveryElec: form.batteryPct === '' ? null : Number(form.batteryPct), + deliveryH2: form.hydrogenAmount === '' ? null : Number(form.hydrogenAmount), + deliveryH2Unit: form.hydrogenUnit, + deliveryStatus: '已保存' + }; + } + + function handleSaveClick() { + onSave && onSave(buildPatchFromForm()); + message.success('已保存'); + } + + function handleSubmitClick() { + var err = validateSubmit(); + if (err) { message.error(err); return; } + Modal.confirm({ + title: '确认交车', + content: '请确认信息填写无误,点击确认完成该车辆交车。', + okText: '确认', + cancelText: '取消', + onOk: function () { + setSubmitting(true); + setTimeout(function () { + setSubmitting(false); + onSubmit && onSubmit(buildPatchFromForm()); + message.success('交车成功'); + onClose && onClose(); + }, 400); + } + }); + } + + function handleTrainingPick(e) { + var f = e && e.target && e.target.files && e.target.files[0]; + if (!f) return; + setTimeout(function () { + updateForm({ + trainingRecognized: true, + driverLicenses: [ + { uid: 'id1', name: '身份证正面', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=ID-F' }, + { uid: 'id2', name: '身份证反面', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=ID-B' }, + { uid: 'dl', name: '驾驶证', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=DL' }, + { uid: 'qc', name: '从业资格证', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=QC' } + ] + }); + message.success('验车码识别成功'); + }, 500); + e.target.value = ''; + } + + if (!open) return null; + + var drawerTitle = React.createElement('div', { style: { paddingRight: 24 } }, + React.createElement('div', { style: { fontSize: 16, fontWeight: 700, color: '#0f172a', lineHeight: 1.4 } }, '编辑交车单'), + record ? React.createElement('div', { style: { marginTop: 6, fontSize: 13, color: '#64748b', display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' } }, + React.createElement(Tag, { style: { margin: 0, border: 'none', background: '#eff6ff', color: '#2563eb', fontWeight: 600 } }, record.customerName || '-'), + React.createElement('span', null, record.contractCode || '-'), + React.createElement(Tag, { style: { margin: 0, border: 'none', background: '#f1f5f9', color: '#475569' } }, record.deliveryStatus || '未开始') + ) : null + ); + + var summaryCard = record ? React.createElement('div', { style: { marginBottom: 16, padding: '12px 14px', borderRadius: 10, background: 'linear-gradient(135deg,#f8fafc 0%,#fff 100%)', border: '1px solid #e2e8f0', display: 'grid', gridTemplateColumns: 'repeat(2,minmax(0,1fr))', gap: '8px 16px', fontSize: 13 } }, + React.createElement('div', null, React.createElement('span', { style: { color: '#94a3b8' } }, '项目:'), record.projectName || '-'), + React.createElement('div', null, React.createElement('span', { style: { color: '#94a3b8' } }, '交车地点:'), record.deliveryAddress || '-'), + React.createElement('div', null, React.createElement('span', { style: { color: '#94a3b8' } }, '品牌型号:'), (record.brand || '-') + ' / ' + (record.model || '-')), + React.createElement('div', null, React.createElement('span', { style: { color: '#94a3b8' } }, '任务来源:'), record.taskSource || '-') + ) : null; + + var sectionNavEl = React.createElement('div', { style: { position: 'sticky', top: 0, zIndex: 3, background: '#fff', padding: '8px 0 12px', marginBottom: 8, borderBottom: '1px solid #f1f5f9', display: 'flex', gap: 8, flexWrap: 'wrap' } }, + sectionNav.map(function (s) { + var active = activeSection === s.key; + return React.createElement('button', { + key: s.key, + type: 'button', + onClick: function () { scrollToSection(s.key); }, + style: { + border: 'none', + cursor: 'pointer', + padding: '6px 14px', + borderRadius: 999, + fontSize: 13, + fontWeight: active ? 600 : 500, + background: active ? '#2563eb' : '#f1f5f9', + color: active ? '#fff' : '#475569', + transition: 'background .2s,color .2s' + } + }, s.label); + }) + ); + + var sectionBasic = React.createElement('div', { id: 'dv-edit-section-basic', style: { scrollMarginTop: 56 } }, + React.createElement('div', { style: { fontSize: 14, fontWeight: 700, color: '#0f172a', marginBottom: 12, paddingLeft: 10, borderLeft: '3px solid #2563eb' } }, '车辆信息'), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(2,minmax(0,1fr))', gap: '0 16px' } }, + React.createElement(FormItem, { label: '车牌号', required: true }, + React.createElement(Select, { value: form.plateNo, options: plateOptions, showSearch: true, filterOption: filterOption, placeholder: '请输入或选择车牌号', allowClear: true, style: { width: '100%' }, onChange: handlePlateChange }) + ), + React.createElement(FormItem, { label: '车辆类型' }, React.createElement(Input, { value: form.vehicleType, disabled: true })), + React.createElement(FormItem, { label: '品牌' }, React.createElement(Input, { value: form.brand, disabled: true })), + React.createElement(FormItem, { label: '型号' }, React.createElement(Input, { value: form.model, disabled: true })), + React.createElement(FormItem, { label: '车辆识别代码' }, React.createElement(Input, { value: form.vin, disabled: true })) + ) + ); + + var sectionEquip = React.createElement('div', { id: 'dv-edit-section-equip', style: { scrollMarginTop: 56, marginTop: 24 } }, + React.createElement('div', { style: { fontSize: 14, fontWeight: 700, color: '#0f172a', marginBottom: 12, paddingLeft: 10, borderLeft: '3px solid #2563eb' } }, '设备与证照'), + React.createElement(FormItem, { label: '车身广告及放大字', required: true }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10 } }, + React.createElement(Switch, { checked: !!form.hasAd, onChange: function (v) { updateForm({ hasAd: !!v }); } }), + React.createElement('span', { style: { fontSize: 13, color: '#64748b' } }, form.hasAd ? '有车身广告' : '无车身广告') + ) + ), + form.hasAd ? React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 8 } }, + React.createElement(UploadBox, { label: RequiredLabel('广告照片'), value: form.adPhoto, max: 1, onChange: function (l) { updateForm({ adPhoto: l }); } }), + React.createElement(UploadBox, { label: RequiredLabel('放大字照片'), value: form.bigWordPhoto, max: 1, onChange: function (l) { updateForm({ bigWordPhoto: l }); } }) + ) : null, + React.createElement(FormItem, { label: '尾板', required: true }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10 } }, + React.createElement(Switch, { checked: !!form.hasTailboard, onChange: function (v) { updateForm({ hasTailboard: !!v }); } }), + React.createElement('span', { style: { fontSize: 13, color: '#64748b' } }, form.hasTailboard ? '有尾板' : '无尾板') + ) + ), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 } }, + React.createElement(UploadBox, { label: RequiredLabel('备胎照片'), value: form.spareTirePhoto, max: 1, onChange: function (l) { updateForm({ spareTirePhoto: l }); if (l && l.length) ocrModalState[1]({ open: true, photoUrl: l[0].url, depth: '6.50' }); } }), + React.createElement(FormItem, { label: '备胎胎纹深度', required: true }, + React.createElement(Input, { value: form.spareTireDepth, placeholder: '请输入', addonAfter: 'mm', onChange: function (e) { updateForm({ spareTireDepth: e.target.value }); } }) + ) + ), + React.createElement(FormItem, { label: '驾驶培训', required: true }, + form.trainingRecognized + ? React.createElement('span', { style: { color: '#16a34a', fontWeight: 600, fontSize: 13 } }, '已完成视频培训') + : React.createElement(React.Fragment, null, + React.createElement('input', { type: 'file', ref: trainingInputRef, accept: 'image/*', style: { display: 'none' }, onChange: handleTrainingPick }), + React.createElement(Button, { onClick: function () { trainingInputRef.current && trainingInputRef.current.click(); } }, '上传验车码') + ) + ), + form.driverLicenses && form.driverLicenses.length ? React.createElement(FormItem, { label: '司机证照' }, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(4,minmax(0,1fr))', gap: 12 } }, + form.driverLicenses.map(function (f) { + return React.createElement('div', { key: f.uid }, + makeThumb(f.url, function () { previewState[1]({ open: true, url: f.url, title: f.name }); }), + React.createElement('div', { style: { marginTop: 4, fontSize: 12, color: '#64748b', textAlign: 'center' } }, f.name) + ); + }) + ) + ) : null, + React.createElement(FormItem, { label: '车辆检查' }, + React.createElement(Button, { onClick: function () { inspectionOpenState[1](true); } }, '交车检查单') + ) + ); + + var sectionMetrics = React.createElement('div', { id: 'dv-edit-section-metrics', style: { scrollMarginTop: 56, marginTop: 24 } }, + React.createElement('div', { style: { fontSize: 14, fontWeight: 700, color: '#0f172a', marginBottom: 12, paddingLeft: 10, borderLeft: '3px solid #2563eb' } }, '交车数据'), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(2,minmax(0,1fr))', gap: '0 16px' } }, + React.createElement(FormItem, { label: '交车里程', required: true }, + React.createElement(Input, { value: form.mileageKm, placeholder: '请输入', addonAfter: 'km', onChange: function (e) { updateForm({ mileageKm: e.target.value }); } }) + ), + React.createElement(FormItem, { label: '交车电量', required: true }, + React.createElement(Input, { value: form.batteryPct, placeholder: '请输入', addonAfter: '%', onChange: function (e) { updateForm({ batteryPct: e.target.value }); } }) + ), + React.createElement(FormItem, { label: '交车氢量', required: true }, + React.createElement(Input, { value: form.hydrogenAmount, placeholder: '请输入', addonAfter: form.hydrogenUnit, onChange: function (e) { updateForm({ hydrogenAmount: e.target.value }); } }) + ), + React.createElement(FormItem, { label: '送车服务费' }, + React.createElement(Input, { value: form.serviceFee, placeholder: '选填', addonAfter: '元', onChange: function (e) { updateForm({ serviceFee: e.target.value }); } }) + ) + ) + ); + + var photoKeys = ['仪表盘', '车辆正面', '车辆左前方']; + var photoState = useState({ vehicle: { '仪表盘': [], '车辆正面': [], '车辆左前方': [] } }); + var photos = photoState[0]; + var setPhotos = photoState[1]; + + var sectionPhotos = React.createElement('div', { id: 'dv-edit-section-photos', style: { scrollMarginTop: 56, marginTop: 24, marginBottom: 24 } }, + React.createElement('div', { style: { fontSize: 14, fontWeight: 700, color: '#0f172a', marginBottom: 12, paddingLeft: 10, borderLeft: '3px solid #2563eb' } }, '交车照片'), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(3,minmax(0,1fr))', gap: 16 } }, + photoKeys.map(function (k) { + return React.createElement(UploadBox, { + key: k, + label: RequiredLabel(k), + value: photos.vehicle[k] || [], + max: 1, + onChange: function (l) { + setPhotos(function (p) { + var n = Object.assign({}, p); + n.vehicle = Object.assign({}, p.vehicle); + n.vehicle[k] = l; + return n; + }); + } + }); + }) + ), + React.createElement('div', { style: { marginTop: 8, fontSize: 12, color: '#94a3b8' } }, '原型:完整照片清单见交车单编辑页需求说明') + ); + + return React.createElement(React.Fragment, null, + React.createElement(Drawer, { + open: open, + onClose: onClose, + width: Math.min(960, typeof window !== 'undefined' ? window.innerWidth - 24 : 960), + title: drawerTitle, + destroyOnClose: true, + styles: { + body: { padding: '16px 20px 88px', background: '#f8fafc' }, + footer: { borderTop: '1px solid #e2e8f0', padding: '12px 20px' } + }, + footer: React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 10 } }, + React.createElement(Button, { onClick: onClose, disabled: submitting }, '取消'), + React.createElement(Button, { onClick: handleSaveClick, disabled: submitting }, '保存'), + React.createElement(Button, { type: 'primary', loading: submitting, onClick: handleSubmitClick }, '提交') + ) + }, + summaryCard, + sectionNavEl, + React.createElement('div', { style: { background: '#fff', borderRadius: 12, border: '1px solid #e2e8f0', padding: '16px 16px 8px' } }, + sectionBasic, + sectionEquip, + sectionMetrics, + sectionPhotos + ) + ), + React.createElement(Modal, { + open: !!previewState[0].open, + title: previewState[0].title || '预览', + footer: null, + onCancel: function () { previewState[1]({ open: false, url: '', title: '' }); }, + width: 860 + }, previewState[0].url ? React.createElement('img', { src: previewState[0].url, alt: previewState[0].title, style: { width: '100%', maxHeight: '70vh', objectFit: 'contain' } }) : null), + React.createElement(Modal, { + open: !!ocrModalState[0].open, + title: '备胎识别', + onCancel: function () { ocrModalState[1](Object.assign({}, ocrModalState[0], { open: false })); }, + onOk: function () { updateForm({ spareTireDepth: ocrModalState[0].depth }); ocrModalState[1](Object.assign({}, ocrModalState[0], { open: false })); message.success('已反写胎纹深度'); }, + okText: '确认', + cancelText: '取消' + }, + React.createElement(Input, { value: ocrModalState[0].depth, addonAfter: 'mm', onChange: function (e) { ocrModalState[1](Object.assign({}, ocrModalState[0], { depth: e.target.value })); } }) + ), + React.createElement(Drawer, { + open: inspectionOpenState[0], + title: '交车检查单', + width: 720, + onClose: function () { inspectionOpenState[1](false); }, + footer: React.createElement(Button, { type: 'primary', onClick: function () { message.success('检查单已保存'); inspectionOpenState[1](false); } }, '确定') + }, + React.createElement(Table, { + rowKey: 'key', + size: 'small', + pagination: false, + bordered: true, + dataSource: inspectionList, + columns: [ + { title: '类别', dataIndex: 'category', width: 100 }, + { title: '检查项目', dataIndex: 'item', width: 140 }, + { title: '检查情况', dataIndex: 'checked', width: 100, render: function (v) { return React.createElement(Switch, { checked: !!v, disabled: true }); } }, + { title: '备注', dataIndex: 'remark' } + ] + }) + ) + ); +} + +if (typeof window !== 'undefined') { + window.DeliveryEditDrawer = DeliveryEditDrawer; +} + +export default Component; diff --git a/src/prototypes/oneos-web-ops/pages/24-交车管理-交车单-编辑.jsx b/src/prototypes/oneos-web-ops/pages/24-交车管理-交车单-编辑.jsx new file mode 100644 index 0000000..c2c32d4 --- /dev/null +++ b/src/prototypes/oneos-web-ops/pages/24-交车管理-交车单-编辑.jsx @@ -0,0 +1,855 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 运维管理 - 车辆业务 - 交车管理 - 交车单 - 编辑 + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + var useRef = React.useRef; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Table = antd.Table; + var Button = antd.Button; + var Input = antd.Input; + var Select = antd.Select; + var Switch = antd.Switch; + var Modal = antd.Modal; + var Drawer = antd.Drawer; + var message = antd.message; + + var TextArea = Input.TextArea; + + // ---------- utils ---------- + function RequiredLabel(text) { + return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 4 } }, + React.createElement('span', { style: { color: '#f5222d', fontWeight: 600 } }, '*'), + React.createElement('span', null, text) + ); + } + + function toFixed2(v) { + if (v === null || v === undefined || v === '') return ''; + var n = typeof v === 'number' ? v : parseFloat(v); + return isNaN(n) ? '' : n.toFixed(2); + } + + function isEmpty(v) { + return v === null || v === undefined || String(v).trim() === ''; + } + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + function fileToDataUrl(file, cb) { + try { + var reader = new FileReader(); + reader.onload = function (e) { cb(null, (e && e.target && e.target.result) || ''); }; + reader.onerror = function () { cb(new Error('read error')); }; + reader.readAsDataURL(file); + } catch (e) { + cb(e); + } + } + + function makeThumb(url, onPreview, onRemove, disabled) { + return React.createElement('div', { + style: { + width: 64, + height: 64, + borderRadius: 4, + border: '1px solid #f0f0f0', + background: '#fafafa', + position: 'relative', + overflow: 'hidden' + } + }, + React.createElement('img', { + src: url, + style: { width: '100%', height: '100%', objectFit: 'cover', cursor: 'pointer' }, + onClick: disabled ? undefined : onPreview + }), + (disabled || !onRemove) ? null : React.createElement('div', { + style: { + position: 'absolute', + right: 6, + top: 6, + width: 20, + height: 20, + borderRadius: 999, + background: 'rgba(0,0,0,0.55)', + color: '#fff', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + cursor: 'pointer', + fontSize: 12, + userSelect: 'none' + }, + onClick: function (e) { e.stopPropagation(); onRemove && onRemove(); } + }, '×') + ); + } + + function UploadBox(props) { + var label = props.label; + var value = props.value; // array of {name,url} + var max = props.max || 1; + var onChange = props.onChange; + var disabled = !!props.disabled; + var tip = props.tip; + + function handlePick(e) { + var f = e && e.target && e.target.files && e.target.files[0]; + if (!f) return; + fileToDataUrl(f, function (err, url) { + if (err) { message.error('上传失败(原型)'); return; } + var next = (value || []).slice(); + next.push({ uid: String(Date.now()), name: f.name || 'image', url: url }); + if (next.length > max) next = next.slice(next.length - max); + onChange && onChange(next); + }); + e.target.value = ''; + } + + return React.createElement('div', null, + React.createElement('div', { style: { fontSize: 12, color: '#666', marginBottom: 6 } }, label), + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' } }, + (value || []).map(function (f) { + return React.createElement('div', { key: f.uid }, + makeThumb( + f.url, + function () { previewState[1]({ open: true, url: f.url, title: f.name || '预览' }); }, + function () { onChange && onChange((value || []).filter(function (x) { return x.uid !== f.uid; })); }, + disabled + ) + ); + }), + disabled ? null : ((value || []).length >= max ? null : React.createElement('label', { + style: { + width: 64, + height: 64, + borderRadius: 4, + border: '1px dashed #d9d9d9', + background: '#fff', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: '#999', + cursor: 'pointer' + } + }, + React.createElement('input', { type: 'file', accept: 'image/*', style: { display: 'none' }, onChange: handlePick }), + '上传' + )) + ), + tip ? React.createElement('div', { style: { marginTop: 6, fontSize: 12, color: '#999' } }, tip) : null + ); + } + + // ---------- styles ---------- + var layoutStyle = { padding: '16px 24px 88px', background: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; +var footerStyle = { position: 'fixed', left: 0, right: 0, bottom: 0, background: '#fff', borderTop: '1px solid #f0f0f0', padding: '12px 24px', display: 'flex', justifyContent: 'flex-start', gap: 12, zIndex: 10 }; + +var styles = { + formRow: { display: 'flex', gap: 16, marginBottom: 12, alignItems: 'flex-start' }, + formItem: { flex: 1, minWidth: 0 }, + label: { fontSize: 12, color: '#666', marginBottom: 6 } +}; + +function FormItem(props) { + var label = props.label; + var required = !!props.required; + var fullWidth = !!props.fullWidth; + return React.createElement('div', { style: fullWidth ? Object.assign({}, styles.formItem, { flex: '0 0 100%' }) : styles.formItem }, + React.createElement('div', { style: styles.label }, required ? RequiredLabel(label) : label), + props.children + ); +} + + // ---------- mock reserve vehicles ---------- + var reserveVehicles = useMemo(function () { + return [ + { + plateNo: '京A12345', + vehicleType: '牵引车', + brand: '东风', + model: 'DFH1180', + vin: 'LJNAU1A2XK1234567', + hasAd: true, + adPhoto: [{ uid: 'ad1', name: '广告照片.jpg', url: 'https://dummyimage.com/600x400/eee/666&text=Ad' }], + bigWordPhoto: [{ uid: 'bw1', name: '放大字照片.jpg', url: 'https://dummyimage.com/600x400/eee/666&text=BigWord' }], + hasTailboard: true + }, + { + plateNo: '浙F80088', + vehicleType: '厢式车', + brand: '福田', + model: 'BJ1180', + vin: 'LJNAU1A2XK7654321', + hasAd: false, + adPhoto: [], + bigWordPhoto: [], + hasTailboard: false + }, + { + plateNo: '沪A30003', + vehicleType: '厢式车', + brand: '重汽', + model: 'HOWO-T5G', + vin: 'LJNAU1A2XK9999000', + hasAd: true, + adPhoto: [{ uid: 'ad2', name: '广告照片2.jpg', url: 'https://dummyimage.com/600x400/eee/666&text=Ad2' }], + bigWordPhoto: [{ uid: 'bw2', name: '放大字照片2.jpg', url: 'https://dummyimage.com/600x400/eee/666&text=BigWord2' }], + hasTailboard: true + } + ]; + }, []); + + var plateOptions = useMemo(function () { + return reserveVehicles.map(function (v) { return { value: v.plateNo, label: v.plateNo }; }); + }, [reserveVehicles]); + var vehicleByPlate = useMemo(function () { + var map = {}; + reserveVehicles.forEach(function (v) { map[v.plateNo] = v; }); + return map; + }, [reserveVehicles]); + + // ---------- states ---------- + var previewState = useState({ open: false, url: '', title: '' }); + + var formState = useState({ + plateNo: undefined, + vehicleType: '', + brand: '', + model: '', + vin: '', + + hasAd: false, + adPhoto: [], + bigWordPhoto: [], + + hasTailboard: false, + + spareTirePhoto: [], + spareTireDepth: '', + + trainingFile: null, // {name} + trainingRecognized: false, + + driverLicenses: [], // 4 photos {uid,name,url} + + mileageKm: '', + batteryKwh: '', + hydrogenAmount: '', + serviceFee: '' + }); + var form = formState[0]; + var setForm = formState[1]; + + var ocrModalState = useState({ open: false, photoUrl: '', depth: '6.50' }); + + var inspectionDrawerOpenState = useState(false); + + var requirementModalOpenState = useState(false); + var requirementDocContent = + '交车管理-交车单-编辑\n' + + '一个「数字化资产ONEOS运管平台」中的「交车管理-交车单-编辑」模块\n' + + '1.面包屑:\n' + + '1.1.运维管理-车辆业务-交车管理-交车单-编辑\n' + + '每个模块为一个单独卡片:\n\n' + + '2.交车明细:列表结构;\n' + + '2.1.车辆类型:输入框(禁用),根据车牌号反写车辆类型;\n' + + '2.2.品牌:输入框(禁用),根据车牌号反写品牌;\n' + + '2.2.型号:输入框(禁用),根据车牌号反写型号;\n' + + '2.3.车牌号:选择器,输入框,支持输入内容下拉模糊匹配选项,选择车辆有3个判断条件,1.仅能选择备车库中该型号车辆 2.选择时判断商业险是否到期,到期则报错不让选;\n' + + '2.4.车辆识别代码:输入框(禁用),根据车牌号反写车辆识别代码;\n' + + '2.5.车身广告及放大字:必选项,开关,选择车辆后拉取该车辆「后装设备」「车身广告」,如果该车辆有「车身广告」则勾选为开,如果无则勾选为无。同时如果手动进行操作,会同步到「后装设备」「车身广告」中, 安装时间以该条备车记录提交成功为准,不够选择广告照片和放大字照片字段隐藏不显示;\n' + + '2.6.广告照片:必填项,图片上传,最多支持上传1张图片,支持主流照片格式。上传后,上传按钮切换为显示已上传图片缩略图,支持点击预览和删除,删除后,切换为上传按钮,从备车记录自动反写;\n' + + '2.7.放大字照片:必填项,图片上传,最多支持上传1张图片,支持主流照片格式。上传后,上传按钮切换为显示已上传图片缩略图,支持点击预览和删除,删除后,切换为上传按钮,从备车记录自动反写;\n' + + '2.8.尾板:必填项,开关,选择车辆后拉取该车辆「后装设备」「尾板」,如果该车辆有「尾板」则勾选为开,如果无则勾选为无。同时如果手动进行操作,会同步到「后装设备」「尾板」中,安装时间以该条备车记录提交成功为准;\n' + + '2.9.备胎照片:必填项,图片上传,最多支持上传1张图片,支持主流照片格式。上传时弹出卡片,提示正在识别中,请勿关闭页面,之后卡片左侧显示备胎照片,右侧输入框显示识别出的胎纹深度,后缀单位为mm,点击卡片中确认按钮,反写至备胎胎纹深度字段下;\n' + + '2.10.备胎胎纹深度:必填项,输入框,反写备胎照片OCR识别胎纹深度结果,支持修改;\n' + + '2.11.驾驶培训:必填项,附件上传按钮,后方为提示信息:请上传司机现场培训二维码图片。识别成功后隐藏驾驶培训按钮和提示,显示:已完成视频培训;\n' + + '2.12.司机证照:显示4张照片、身份证(正面)、身份证(反面)、驾驶证、从业资格证,根据驾驶培训上传二维码识别后自动反写;\n' + + '2.13.交车里程:必填项,输入框,单位为公里;\n' + + '2.14.交车电量:必填项,输入框,单位为kWh;\n' + + '2.15.交车氢量:必填项,输入框,单位为%或MPa,根据型号参数中该车型实际仪表盘单位显示;\n' + + '2.16.送车服务费:选填项,输入框,精确至2位小数,格式为xx.xx元;\n' + + '2.17.车辆检查:按钮,文字为交车检查单,点击右侧展开抽屉,抽屉内显示列表,字段为类别、检查项目、检查情况、备注;\n' + + ' 2.11.1.类别:分为车灯、仪表盘、驾驶室、轮胎、液位检查、外观检查、车辆外观、其他、随车工具、随车证件、整车、燃料电池系统、冷机、制动系统;\n' + + ' 2.11.2.检查项目:车灯类别对应(大灯、转向灯、小灯、示廓灯、刹车灯、倒车灯、牌照灯、防雾灯、室内灯)、仪表盘对应(氢系统指示、电控系统指示、数值清晰准确、故障报警灯)、驾驶室对应(点烟器、车窗升降、按键开关、雨刮器、内后视镜是否正常、内/外门把手、安全带、空调冷暖风、仪表盘、门锁功能、手刹、车钥匙功能是否正常、喇叭、音响功能、遮阳板、主副驾座椅、方向盘、内饰干净整洁)\n' + + ' 2.11.3.检查情况:其他项为开关,在检查项目每项后方显示,从备车记录反写,可手动进行关闭,轮胎为输入框,提示请输入胎纹深度;\n' + + ' 2.11.4.备注:输入框;\n\n' + + '3.交车照片:多个模块分3列显示,由照片标题和照片上传按钮组成,照片点击上传,从本地文件上传单张图片,上传成功后可通过图片右上角删除按钮删除,点击图片可放大预览;;\n' + + '3.1.车辆:必填项,包括仪表盘、车辆正面、车辆左前方、车辆左后方、车辆右后方、车辆右前方;\n' + + '3.2.底盘:必填项,包括正前方底部、左侧前方底部、左侧后方底部、正后方底部、右侧后方底部、右侧前方底部;\n' + + '3.3.轮胎:必填项,包括左前轮、左后轮(内)、左后轮(外)、右前轮、右后轮(内)、右后轮(外)、备胎;\n' + + '3.4.瑕疵:必填项,包括照片上传按钮,最多支持4张照片;\n' + + '3.5.其他:必填项,包括照片上传按钮,最多支持4张照片;\n' + + '照片点击上传,从本地文件上传单张图片,上传成功后可通过图片右上角删除按钮删除,点击图片可放大预览;\n\n' + + '4.底部为提交、取消按钮;\n' + + ' 4.1.提交:点击进行二次确认,点击确认完成该车辆交车;\n' + + ' 4.2.保存:点击暂存交车单(不做校验);\n' + + ' 4.2.取消:点击返回交车单页;\n'; + + var trainingInputRef = useRef(null); + + // photo sections + var photoState = useState({ + vehicle: { + '仪表盘': [], + '车辆正面': [], + '车辆左前方': [], + '车辆左后方': [], + '车辆右后方': [], + '车辆右前方': [] + }, + chassis: { + '正前方底部': [], + '左侧前方底部': [], + '左侧后方底部': [], + '正后方底部': [], + '右侧后方底部': [], + '右侧前方底部': [] + }, + tire: { + '左前轮': [], + '左后轮(内)': [], + '左后轮(外)': [], + '右前轮': [], + '右后轮(内)': [], + '右后轮(外)': [], + '备胎': [] + }, + defect: [], + other: [] + }); + var photos = photoState[0]; + var setPhotos = photoState[1]; + + function updateForm(patch) { + setForm(function (p) { return Object.assign({}, p, patch); }); + } + + function handlePlateChange(v) { + var veh = vehicleByPlate[v]; + updateForm({ + plateNo: v, + vehicleType: (veh && veh.vehicleType) || '', + brand: (veh && veh.brand) || '', + model: (veh && veh.model) || '', + vin: (veh && veh.vin) || '', + hasAd: !!(veh && veh.hasAd), + adPhoto: (veh && (veh.adPhoto || [])) || [], + bigWordPhoto: (veh && (veh.bigWordPhoto || [])) || [], + hasTailboard: !!(veh && veh.hasTailboard), + spareTirePhoto: [], + spareTireDepth: '', + trainingFile: null, + trainingRecognized: false, + driverLicenses: [] + }); + } + + function handleSparePhotoChange(list) { + updateForm({ spareTirePhoto: list || [] }); + if ((list || []).length > 0) { + ocrModalState[1]({ open: true, photoUrl: list[0].url, depth: '6.50' }); + } + } + + function confirmOcr() { + updateForm({ spareTireDepth: ocrModalState[0].depth }); + ocrModalState[1](Object.assign({}, ocrModalState[0], { open: false })); + message.success('已反写备胎胎纹深度'); + } + + function handleTrainingPick(e) { + var f = e && e.target && e.target.files && e.target.files[0]; + if (!f) return; + // 原型:上传即识别成功 + setTimeout(function () { + updateForm({ trainingFile: { name: f.name || '二维码图片' }, trainingRecognized: true }); + // 自动反写司机证照(示例) + updateForm({ + driverLicenses: [ + { uid: 'id1', name: '身份证正面.jpg', url: 'https://dummyimage.com/600x400/eee/666&text=ID-Front' }, + { uid: 'id2', name: '身份证反面.jpg', url: 'https://dummyimage.com/600x400/eee/666&text=ID-Back' }, + { uid: 'dl', name: '驾驶证.jpg', url: 'https://dummyimage.com/600x400/eee/666&text=DriverLicense' }, + { uid: 'qc', name: '从业资格证.jpg', url: 'https://dummyimage.com/600x400/eee/666&text=Qualification' } + ] + }); + message.success('识别成功:已完成视频培训(原型)'); + }, 600); + e.target.value = ''; + } + + function validateSubmit() { + if (isEmpty(form.plateNo)) return '请选择车牌号'; + // 车身广告及放大字:必选项(hasAd 视为必选,允许开/关,但若开则必须有两张图) + if (form.hasAd) { + if (!form.adPhoto || form.adPhoto.length === 0) return '请上传广告照片'; + if (!form.bigWordPhoto || form.bigWordPhoto.length === 0) return '请上传放大字照片'; + } + if (form.hasTailboard !== true && form.hasTailboard !== false) return '请填写尾板'; + // 尾板必填:但开关值本身必选,这里只要求已选;若未选 plateChange 已赋默认 false + // 备胎照片/胎纹深度必填 + if (!form.spareTirePhoto || form.spareTirePhoto.length === 0) return '请上传备胎照片'; + if (isEmpty(form.spareTireDepth)) return '请填写备胎胎纹深度'; + // 驾驶培训必填:识别成功才算完成 + if (!form.trainingRecognized) return '请上传验车码并完成识别'; + if (isEmpty(form.mileageKm)) return '请填写交车里程'; + if (isEmpty(form.batteryKwh)) return '请填写交车电量'; + if (isEmpty(form.hydrogenAmount)) return '请填写交车氢量'; + // 交车照片必填:车辆/底盘/轮胎(瑕疵、其他非必填) + var requiredGroups = [ + { key: 'vehicle', label: '车辆' }, + { key: 'chassis', label: '底盘' }, + { key: 'tire', label: '轮胎' } + ]; + for (var gi = 0; gi < requiredGroups.length; gi++) { + var g = requiredGroups[gi]; + var groupMap = photos && photos[g.key]; + var keys = groupMap ? Object.keys(groupMap) : []; + for (var ki = 0; ki < keys.length; ki++) { + var k = keys[ki]; + var arr = groupMap[k] || []; + if (!arr || arr.length === 0) return '请上传' + g.label + '照片:' + k; + } + } + // 尾板为必填项:如果业务上要求必须开/关都可以,这里不做强制为开,仅保证已选择 + return ''; + } + + function handleSubmit() { + var err = validateSubmit(); + if (err) { message.error(err); return; } + Modal.confirm({ + title: '确认交车', + content: '请确认信息填写无误,点击确认完成该车辆交车。', + okText: '确认', + cancelText: '取消', + onOk: function () { + message.success('交车成功(原型)'); + } + }); + } + +function handleSave() { + message.success('已保存(原型)'); +} + + function handleCancel() { + message.info('返回交车单页(原型)'); + } + + // ---------- inspection checklist ---------- + // 按需求 2.11.1~2.11.4 生成完整检查清单(原型:默认从备车记录反写为“开/正常”) + var inspectionCategoryItems = { + '车灯': ['大灯', '转向灯', '小灯', '示廓灯', '刹车灯', '倒车灯', '牌照灯', '防雾灯', '室内灯'], + '仪表盘': ['氢系统指示', '电控系统指示', '数值清晰准确', '故障报警灯'], + '驾驶室': ['点烟器', '车窗升降', '按键开关', '雨刮器', '内后视镜是否正常', '内/外门把手', '安全带', '空调冷暖风', '仪表盘', '门锁功能', '手刹', '车钥匙功能是否正常', '喇叭', '音响功能', '遮阳板', '主副驾座椅', '方向盘', '内饰干净整洁'], + '轮胎': ['前左胎', '前右胎', '后左胎', '后右胎', '备胎'], + '液位检查': ['冷却液', '制动液', '玻璃水'], + '外观检查': ['车身外观', '漆面', '玻璃'], + '车辆外观': ['整车外观'], + '其他': ['其他检查项'], + '随车工具': ['三角牌', '灭火器', '反光背心'], + '随车证件': ['行驶证', '营运证', '保险单'], + '整车': ['整车状态'], + '燃料电池系统': ['氢系统', '储氢瓶'], + '冷机': ['冷机运行'], + '制动系统': ['制动踏板', '驻车制动'] + }; + + function buildInspectionList() { + var list = []; + var categories = Object.keys(inspectionCategoryItems); + for (var i = 0; i < categories.length; i++) { + var cat = categories[i]; + var items = inspectionCategoryItems[cat] || []; + for (var j = 0; j < items.length; j++) { + var it = items[j]; + var isTire = cat === '轮胎'; + list.push({ + key: 'ins-' + i + '-' + j, + category: cat, + item: it, + checked: true, + treadDepth: isTire ? '6.5' : '', + remark: '' + }); + } + } + return list; + } + + var inspectionListState = useState(function () { + return buildInspectionList(); + }); + var inspectionList = inspectionListState[0]; + var setInspectionList = inspectionListState[1]; + var inspectionListRef = useRef(null); + inspectionListRef.current = inspectionList; + + function updateInspectionRow(key, patch) { + setInspectionList(function (prev) { + return (prev || []).map(function (r) { + if (r.key !== key) return r; + return Object.assign({}, r, patch); + }); + }); + } + + var inspectionColumns = useMemo(function () { + return [ + { + title: '类别', + dataIndex: 'category', + key: 'category', + width: 140, + render: function (text, record, index) { + var rows = inspectionListRef.current || []; + var cat = record && record.category; + if (!cat) return { children: text, props: { rowSpan: 1 } }; + + // 仅合并同类别的连续行 + var isFirst = true; + for (var i = index - 1; i >= 0; i--) { + if (!rows[i] || rows[i].category !== cat) break; + isFirst = false; + break; + } + if (!isFirst) return { children: null, props: { rowSpan: 0 } }; + + var span = 1; + for (var j = index + 1; j < rows.length; j++) { + if (!rows[j] || rows[j].category !== cat) break; + span++; + } + return { children: text, props: { rowSpan: span } }; + } + }, + { title: '检查项目', dataIndex: 'item', key: 'item', width: 220 }, + { + title: '检查情况', + dataIndex: 'checked', + key: 'checked', + width: 220, + render: function (_, record) { + var isTire = record && (record.category === '轮胎' || String(record.item || '').indexOf('胎纹') >= 0); + return isTire + ? React.createElement(Input, { + value: record.treadDepth, + placeholder: '请输入胎纹深度', + addonAfter: 'mm', + onChange: function (e) { updateInspectionRow(record.key, { treadDepth: e.target.value }); } + }) + : React.createElement(Switch, { + checked: !!record.checked, + onChange: function (v) { updateInspectionRow(record.key, { checked: !!v }); } + }); + } + }, + { + title: '备注', + dataIndex: 'remark', + key: 'remark', + render: function (_, record) { + return React.createElement(Input, { + value: record.remark, + placeholder: '请输入', + onChange: function (e) { updateInspectionRow(record.key, { remark: e.target.value }); } + }); + } + } + ]; + }, []); + + // ---------- render helpers ---------- + // 交车明细布局对齐「备车-新增」:使用 FormItem + styles.formRow + + function PhotoGridColumn(title, items, required) { + return React.createElement('div', { style: { background: '#fff', border: '1px solid #f0f0f0', borderRadius: 8, padding: 12 } }, + React.createElement('div', { style: { fontWeight: 600, marginBottom: 12 } }, required ? RequiredLabel(title) : title), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 12 } }, + items.map(function (it) { + return React.createElement('div', { key: it.key }, + UploadBox({ + label: required ? RequiredLabel(it.label) : it.label, + value: it.value, + max: 1, + onChange: it.onChange + }) + ); + }) + ) + ); + } + + function PhotoGridSimple(title, value, max, onChange) { + return React.createElement('div', { style: { background: '#fff', border: '1px solid #f0f0f0', borderRadius: 8, padding: 12 } }, + React.createElement('div', { style: { fontWeight: 600, marginBottom: 12 } }, title), + UploadBox({ label: '照片', value: value, max: max, onChange: onChange, tip: '最多支持上传' + max + '张' }) + ); + } + +// 在线打印已按最新需求移除 + + // ---------- render ---------- + return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + React.createElement(Breadcrumb, { items: [{ title: '运维管理' }, { title: '车辆业务' }, { title: '交车管理' }, { title: '交车单-编辑' }] }), + React.createElement(Button, { type: 'link', onClick: function () { requirementModalOpenState[1](true); } }, '查看需求说明') + ), + + React.createElement(Card, { title: '交车明细', style: cardStyle }, + React.createElement('div', null, + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '车牌号', required: true }, + React.createElement(Select, { + value: form.plateNo, + options: plateOptions, + showSearch: true, + filterOption: filterOption, + placeholder: '请输入或选择车牌号', + allowClear: true, + style: { width: '100%' }, + onChange: handlePlateChange + }) + ), + React.createElement(FormItem, { label: '车辆类型' }, + React.createElement(Input, { value: form.vehicleType, disabled: true }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '品牌' }, + React.createElement(Input, { value: form.brand, disabled: true }) + ), + React.createElement(FormItem, { label: '型号' }, + React.createElement(Input, { value: form.model, disabled: true }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '车辆识别代码' }, + React.createElement(Input, { value: form.vin, disabled: true }) + ), + React.createElement('div', { style: styles.formItem }) + ), + + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '车身广告及放大字', required: true, fullWidth: true }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10 } }, + React.createElement(Switch, { checked: !!form.hasAd, onChange: function (v) { updateForm({ hasAd: !!v }); } }), + React.createElement('span', { style: { color: '#666', fontSize: 12 } }, form.hasAd ? '有车身广告' : '无车身广告') + ) + ) + ), + form.hasAd ? React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 12 } }, + React.createElement('div', null, + UploadBox({ label: RequiredLabel('广告照片'), value: form.adPhoto, max: 1, onChange: function (l) { updateForm({ adPhoto: l }); } }) + ), + React.createElement('div', null, + UploadBox({ label: RequiredLabel('放大字照片'), value: form.bigWordPhoto, max: 1, onChange: function (l) { updateForm({ bigWordPhoto: l }); } }) + ) + ) : null, + + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '尾板', required: true, fullWidth: true }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10 } }, + React.createElement(Switch, { checked: !!form.hasTailboard, onChange: function (v) { updateForm({ hasTailboard: !!v }); } }), + React.createElement('span', { style: { color: '#666', fontSize: 12 } }, form.hasTailboard ? '有尾板' : '无尾板') + ) + ) + ), + + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '备胎照片', required: true }, + UploadBox({ label: '', value: form.spareTirePhoto, max: 1, onChange: handleSparePhotoChange }) + ), + React.createElement(FormItem, { label: '备胎胎纹深度', required: true }, + React.createElement(Input, { value: form.spareTireDepth, placeholder: '请输入备胎胎纹深度', addonAfter: 'mm', onChange: function (e) { updateForm({ spareTireDepth: e.target.value }); } }) + ) + ), + + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '驾驶培训', required: true, fullWidth: true }, + React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } }, + form.trainingRecognized + ? React.createElement('span', { style: { color: '#52c41a', fontWeight: 600 } }, '已完成视频培训') + : React.createElement(React.Fragment, null, + React.createElement('div', { style: { display: 'inline-flex', alignItems: 'center', gap: 8 } }, + React.createElement('input', { type: 'file', ref: trainingInputRef, accept: 'image/*', style: { display: 'none' }, onChange: handleTrainingPick }), + React.createElement(Button, { onClick: function () { trainingInputRef.current && trainingInputRef.current.click(); } }, '上传验车码') + ) + ) + ) + ) + ), + + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '司机证照', fullWidth: true }, + React.createElement('div', null, + (form.driverLicenses && form.driverLicenses.length > 0) + ? React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12 } }, + form.driverLicenses.map(function (f) { + return React.createElement('div', { key: f.uid }, + makeThumb( + f.url, + function () { previewState[1]({ open: true, url: f.url, title: f.name || '预览' }); }, + null, + false + ), + React.createElement('div', { style: { marginTop: 6, fontSize: 12, color: '#666' } }, f.name) + ); + }) + ) + : React.createElement('div', { style: { fontSize: 12, color: '#999' } }, '上传验车码后显示') + ) + ) + ), + + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '交车里程', required: true }, + React.createElement(Input, { value: form.mileageKm, placeholder: '请输入', addonAfter: '公里', onChange: function (e) { updateForm({ mileageKm: e.target.value }); } }) + ), + React.createElement(FormItem, { label: '交车电量', required: true }, + React.createElement(Input, { value: form.batteryKwh, placeholder: '请输入', addonAfter: 'kWh', onChange: function (e) { updateForm({ batteryKwh: e.target.value }); } }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '交车氢量', required: true }, + React.createElement(Input, { value: form.hydrogenAmount, placeholder: '请输入交车氢量', addonAfter: 'MPa', onChange: function (e) { updateForm({ hydrogenAmount: e.target.value }); } }) + ), + React.createElement(FormItem, { label: '送车服务费' }, + React.createElement(Input, { value: form.serviceFee, placeholder: '请输入送车服务费金额', addonAfter: '元', onChange: function (e) { updateForm({ serviceFee: e.target.value }); } }) + ) + ), + + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '车辆检查', fullWidth: true }, + React.createElement(Button, { onClick: function () { inspectionDrawerOpenState[1](true); } }, '交车检查单') + ) + ) + ) + ), + + React.createElement(Card, { title: '交车照片', style: cardStyle }, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 12 } }, + PhotoGridColumn('车辆', Object.keys(photos.vehicle).map(function (k) { + return { + key: 'v-' + k, + label: k, + value: photos.vehicle[k], + onChange: function (l) { setPhotos(function (p) { var n = Object.assign({}, p); n.vehicle = Object.assign({}, p.vehicle); n.vehicle[k] = l; return n; }); } + }; + }), true), + PhotoGridColumn('底盘', Object.keys(photos.chassis).map(function (k) { + return { + key: 'c-' + k, + label: k, + value: photos.chassis[k], + onChange: function (l) { setPhotos(function (p) { var n = Object.assign({}, p); n.chassis = Object.assign({}, p.chassis); n.chassis[k] = l; return n; }); } + }; + }), true), + PhotoGridColumn('轮胎', Object.keys(photos.tire).map(function (k) { + return { + key: 't-' + k, + label: k, + value: photos.tire[k], + onChange: function (l) { setPhotos(function (p) { var n = Object.assign({}, p); n.tire = Object.assign({}, p.tire); n.tire[k] = l; return n; }); } + }; + }), true) + ), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 12, marginTop: 12 } }, + PhotoGridSimple('瑕疵', photos.defect, 4, function (l) { setPhotos(function (p) { return Object.assign({}, p, { defect: l }); }); }), + PhotoGridSimple('其他', photos.other, 4, function (l) { setPhotos(function (p) { return Object.assign({}, p, { other: l }); }); }) + ) + ), + + React.createElement('div', { style: footerStyle }, + React.createElement(Button, { type: 'primary', onClick: handleSubmit }, '提交'), + React.createElement(Button, { onClick: handleSave }, '保存'), + React.createElement(Button, { onClick: handleCancel }, '取消') + ), + + // preview modal + React.createElement(Modal, { + open: !!previewState[0].open, + title: previewState[0].title || '预览', + footer: null, + onCancel: function () { previewState[1]({ open: false, url: '', title: '' }); }, + width: 860 + }, + previewState[0].url ? React.createElement('img', { src: previewState[0].url, style: { width: '100%', maxHeight: '70vh', objectFit: 'contain' } }) : null + ), + + // OCR modal + React.createElement(Modal, { + open: !!ocrModalState[0].open, + title: '正在识别中(原型)', + onCancel: function () { ocrModalState[1](Object.assign({}, ocrModalState[0], { open: false })); }, + onOk: confirmOcr, + okText: '确认', + cancelText: '取消', + width: 860 + }, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 320px', gap: 16, alignItems: 'start' } }, + React.createElement('div', { style: { border: '1px solid #f0f0f0', borderRadius: 8, overflow: 'hidden', background: '#fafafa' } }, + ocrModalState[0].photoUrl ? React.createElement('img', { src: ocrModalState[0].photoUrl, style: { width: '100%', maxHeight: 420, objectFit: 'contain', display: 'block' } }) : null + ), + React.createElement('div', null, + React.createElement('div', { style: { fontSize: 12, color: '#666', marginBottom: 6 } }, '识别出的胎纹深度'), + React.createElement(Input, { + value: ocrModalState[0].depth, + addonAfter: 'mm', + onChange: function (e) { ocrModalState[1](Object.assign({}, ocrModalState[0], { depth: e.target.value })); } + }), + React.createElement('div', { style: { marginTop: 8, fontSize: 12, color: '#999', lineHeight: 1.7 } }, '提示:识别中请勿关闭页面;点击确认后将反写至“备胎胎纹深度”。') + ) + ) + ), + + // inspection drawer + React.createElement(Drawer, { + open: inspectionDrawerOpenState[0], + title: '交车检查单', + width: 920, + placement: 'right', + onClose: function () { inspectionDrawerOpenState[1](false); }, + styles: { body: { display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden', paddingBottom: 0 } }, + footer: React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-start', gap: 8 } }, + React.createElement(Button, { + type: 'primary', + onClick: function () { + message.success('提交成功(原型)'); + inspectionDrawerOpenState[1](false); + } + }, '提交'), + React.createElement(Button, { onClick: function () { inspectionDrawerOpenState[1](false); } }, '返回') + ) + }, + React.createElement('div', { style: { flex: 1, minHeight: 0, overflow: 'auto' } }, + React.createElement(Table, { rowKey: 'key', columns: inspectionColumns, dataSource: inspectionList, pagination: false, bordered: true, size: 'small' }) + ) + ), + React.createElement(Modal, { + open: requirementModalOpenState[0], + onCancel: function () { requirementModalOpenState[1](false); }, + onOk: function () { requirementModalOpenState[1](false); }, + title: '需求说明', + width: 860 + }, + React.createElement('div', { style: { maxHeight: '70vh', overflow: 'auto', whiteSpace: 'pre-wrap', lineHeight: 1.7, color: '#333' } }, requirementDocContent) + ), + + // 在线打印已按最新需求移除 + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-ops/pages/25-交车管理-交车单-查看.jsx b/src/prototypes/oneos-web-ops/pages/25-交车管理-交车单-查看.jsx new file mode 100644 index 0000000..f456657 --- /dev/null +++ b/src/prototypes/oneos-web-ops/pages/25-交车管理-交车单-查看.jsx @@ -0,0 +1,495 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 运维管理 - 车辆业务 - 交车管理 - 交车单 - 查看(只读) + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useRef = React.useRef; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Table = antd.Table; + var Button = antd.Button; + var Input = antd.Input; + var Select = antd.Select; + var Switch = antd.Switch; + var Modal = antd.Modal; + var Drawer = antd.Drawer; + var message = antd.message; + + // ---------- utils ---------- + function RequiredLabel(text) { + return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 4 } }, + React.createElement('span', { style: { color: '#f5222d', fontWeight: 600 } }, '*'), + React.createElement('span', null, text) + ); + } + + function toFixed2(v) { + if (v === null || v === undefined || v === '') return ''; + var n = typeof v === 'number' ? v : parseFloat(v); + return isNaN(n) ? '' : n.toFixed(2); + } + + function isEmpty(v) { + return v === null || v === undefined || String(v).trim() === ''; + } + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + function makeThumb(url, onPreview) { + return React.createElement('div', { + style: { + width: 64, + height: 64, + borderRadius: 4, + border: '1px solid #f0f0f0', + background: '#fafafa', + position: 'relative', + overflow: 'hidden' + } + }, + React.createElement('img', { + src: url, + style: { width: '100%', height: '100%', objectFit: 'cover', cursor: 'pointer' }, + onClick: onPreview + }) + ); + } + + function ReadonlyUploadBox(props) { + var label = props.label; + var value = props.value; // array of {name,url} + var tip = props.tip; + var onPreview = props.onPreview; + + return React.createElement('div', null, + React.createElement('div', { style: { fontSize: 12, color: '#666', marginBottom: 6 } }, label), + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' } }, + (value || []).length > 0 + ? (value || []).map(function (f) { + return React.createElement('div', { key: f.uid }, + makeThumb(f.url, function () { onPreview && onPreview(f); }) + ); + }) + : React.createElement('div', { style: { fontSize: 12, color: '#999' } }, '暂无') + ), + tip ? React.createElement('div', { style: { marginTop: 6, fontSize: 12, color: '#999' } }, tip) : null + ); + } + + // ---------- styles ---------- + var layoutStyle = { padding: '16px 24px 88px', background: '#f5f5f5', minHeight: '100vh' }; + var cardStyle = { marginBottom: 16 }; + var footerStyle = { position: 'fixed', left: 0, right: 0, bottom: 0, background: '#fff', borderTop: '1px solid #f0f0f0', padding: '12px 24px', display: 'flex', justifyContent: 'flex-start', gap: 12, zIndex: 10 }; + + var styles = { + formRow: { display: 'flex', gap: 16, marginBottom: 12, alignItems: 'flex-start' }, + formItem: { flex: 1, minWidth: 0 }, + label: { fontSize: 12, color: '#666', marginBottom: 6 } + }; + + function FormItem(props) { + var label = props.label; + var fullWidth = !!props.fullWidth; + return React.createElement('div', { style: fullWidth ? Object.assign({}, styles.formItem, { flex: '0 0 100%' }) : styles.formItem }, + React.createElement('div', { style: styles.label }, label), + props.children + ); + } + + // ---------- mock data (只读样例) ---------- + var reserveVehicles = useMemo(function () { + return [ + { plateNo: '京A12345', vehicleType: '牵引车', brand: '东风', model: 'DFH1180', vin: 'LGHXCAE28M1234567', hasAd: true, hasTailboard: true }, + { plateNo: '京C11111', vehicleType: '厢式货车', brand: '福田', model: 'BJ1180', vin: 'LGHXCAE28M7654321', hasAd: false, hasTailboard: false } + ]; + }, []); + + var plateOptions = useMemo(function () { + return reserveVehicles.map(function (v) { return { value: v.plateNo, label: v.plateNo }; }); + }, [reserveVehicles]); + + var initialForm = useMemo(function () { + return { + plateNo: '京A12345', + vehicleType: '牵引车', + brand: '东风', + model: 'DFH1180', + vin: 'LGHXCAE28M1234567', + + hasAd: true, + adPhoto: [{ uid: 'ad1', name: '广告照片.jpg', url: 'https://dummyimage.com/640x360/eee/666&text=Ad' }], + bigWordPhoto: [{ uid: 'bw1', name: '放大字照片.jpg', url: 'https://dummyimage.com/640x360/eee/666&text=BigWord' }], + + hasTailboard: true, + + spareTirePhoto: [{ uid: 'sp1', name: '备胎照片.jpg', url: 'https://dummyimage.com/640x360/eee/666&text=SpareTire' }], + spareTireDepth: '6.50', + + trainingRecognized: true, + driverLicenses: [ + { uid: 'id1', name: '身份证正面.jpg', url: 'https://dummyimage.com/600x400/eee/666&text=ID-Front' }, + { uid: 'id2', name: '身份证反面.jpg', url: 'https://dummyimage.com/600x400/eee/666&text=ID-Back' }, + { uid: 'dl', name: '驾驶证.jpg', url: 'https://dummyimage.com/600x400/eee/666&text=DriverLicense' }, + { uid: 'qc', name: '从业资格证.jpg', url: 'https://dummyimage.com/600x400/eee/666&text=Qualification' } + ], + + mileageKm: '120', + batteryKwh: '86', + hydrogenAmount: '35.60', + serviceFee: '200.00' + }; + }, []); + + var photos = useMemo(function () { + function one(uid, name) { + return [{ uid: uid, name: name + '.jpg', url: 'https://dummyimage.com/640x360/eee/666&text=' + encodeURIComponent(name) }]; + } + return { + vehicle: { + '仪表盘': one('v1', '车辆-仪表盘'), + '车辆正面': one('v2', '车辆-正面'), + '车辆左前方': one('v3', '车辆-左前方'), + '车辆左后方': one('v4', '车辆-左后方'), + '车辆右后方': one('v5', '车辆-右后方'), + '车辆右前方': one('v6', '车辆-右前方') + }, + chassis: { + '正前方底部': one('c1', '底盘-正前方'), + '左侧前方底部': one('c2', '底盘-左前'), + '左侧后方底部': one('c3', '底盘-左后'), + '正后方底部': one('c4', '底盘-正后方'), + '右侧后方底部': one('c5', '底盘-右后'), + '右侧前方底部': one('c6', '底盘-右前') + }, + tire: { + '左前轮': one('t1', '轮胎-左前'), + '左后轮(内)': one('t2', '轮胎-左后内'), + '左后轮(外)': one('t3', '轮胎-左后外'), + '右前轮': one('t4', '轮胎-右前'), + '右后轮(内)': one('t5', '轮胎-右后内'), + '右后轮(外)': one('t6', '轮胎-右后外'), + '备胎': one('t7', '轮胎-备胎') + }, + defect: [{ uid: 'd1', name: '瑕疵1.jpg', url: 'https://dummyimage.com/640x360/eee/666&text=Defect-1' }], + other: [{ uid: 'o1', name: '其他1.jpg', url: 'https://dummyimage.com/640x360/eee/666&text=Other-1' }] + }; + }, []); + + // ---------- inspection checklist (只读样例) ---------- + var inspectionCategoryItems = useMemo(function () { + return { + '车灯': ['大灯', '转向灯', '小灯', '示廓灯', '刹车灯', '倒车灯', '牌照灯', '防雾灯', '室内灯'], + '仪表盘': ['氢系统指示', '电控系统指示', '数值清晰准确', '故障报警灯'], + '驾驶室': ['点烟器', '车窗升降', '按键开关', '雨刮器', '内后视镜是否正常', '内/外门把手', '安全带', '空调冷暖风', '仪表盘', '门锁功能', '手刹', '车钥匙功能是否正常', '喇叭', '音响功能', '遮阳板', '主副驾座椅', '方向盘', '内饰干净整洁'], + '轮胎': ['前左胎', '前右胎', '后左胎', '后右胎', '备胎'], + '液位检查': ['冷却液', '制动液', '玻璃水'], + '外观检查': ['车身外观', '漆面', '玻璃'], + '车辆外观': ['整车外观'], + '其他': ['其他检查项'], + '随车工具': ['三角牌', '灭火器', '反光背心'], + '随车证件': ['行驶证', '营运证', '保险单'], + '整车': ['整车状态'], + '燃料电池系统': ['氢系统', '储氢瓶'], + '冷机': ['冷机运行'], + '制动系统': ['制动踏板', '驻车制动'] + }; + }, []); + + var inspectionList = useMemo(function () { + var list = []; + var cats = Object.keys(inspectionCategoryItems); + for (var i = 0; i < cats.length; i++) { + var cat = cats[i]; + var items = inspectionCategoryItems[cat] || []; + for (var j = 0; j < items.length; j++) { + var it = items[j]; + var isTire = cat === '轮胎'; + list.push({ + key: 'ins-' + i + '-' + j, + category: cat, + item: it, + checked: true, + treadDepth: isTire ? '6.5' : '', + remark: '' + }); + } + } + return list; + }, [inspectionCategoryItems]); + + var inspectionListRef = useRef(null); + inspectionListRef.current = inspectionList; + + var inspectionColumns = useMemo(function () { + return [ + { + title: '类别', + dataIndex: 'category', + key: 'category', + width: 140, + render: function (text, record, index) { + var rows = inspectionListRef.current || []; + var cat = record && record.category; + if (!cat) return { children: text, props: { rowSpan: 1 } }; + + var isFirst = true; + for (var i = index - 1; i >= 0; i--) { + if (!rows[i] || rows[i].category !== cat) break; + isFirst = false; + break; + } + if (!isFirst) return { children: null, props: { rowSpan: 0 } }; + + var span = 1; + for (var j = index + 1; j < rows.length; j++) { + if (!rows[j] || rows[j].category !== cat) break; + span++; + } + return { children: text, props: { rowSpan: span } }; + } + }, + { title: '检查项目', dataIndex: 'item', key: 'item', width: 220 }, + { + title: '检查情况', + dataIndex: 'checked', + key: 'checked', + width: 220, + render: function (_, record) { + var isTire = record && record.category === '轮胎'; + return isTire + ? React.createElement(Input, { value: record.treadDepth, disabled: true, placeholder: '请输入胎纹深度', addonAfter: 'mm' }) + : React.createElement(Switch, { checked: !!record.checked, disabled: true }); + } + }, + { + title: '备注', + dataIndex: 'remark', + key: 'remark', + render: function (_, record) { + return React.createElement(Input, { value: record.remark, disabled: true, placeholder: '请输入' }); + } + } + ]; + }, []); + + // ---------- states ---------- + var previewState = useState({ open: false, url: '', title: '' }); + var inspectionDrawerOpenState = useState(false); + + function openPreview(file) { + if (!file) return; + previewState[1]({ open: true, url: file.url, title: file.name || '预览' }); + } + + function goBack() { + try { + if (window.history && window.history.length > 1) window.history.back(); + else message.info('返回上一页(原型)'); + } catch (e) { + message.info('返回上一页(原型)'); + } + } + + // ---------- render helpers ---------- + function PhotoGridColumn(title, items, required) { + return React.createElement('div', { style: { background: '#fff', border: '1px solid #f0f0f0', borderRadius: 8, padding: 12 } }, + React.createElement('div', { style: { fontWeight: 600, marginBottom: 12 } }, required ? RequiredLabel(title) : title), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 12 } }, + items.map(function (it) { + return React.createElement('div', { key: it.key }, + ReadonlyUploadBox({ label: required ? RequiredLabel(it.label) : it.label, value: it.value, onPreview: openPreview }) + ); + }) + ) + ); + } + + function PhotoGridSimple(title, value) { + return React.createElement('div', { style: { background: '#fff', border: '1px solid #f0f0f0', borderRadius: 8, padding: 12 } }, + React.createElement('div', { style: { fontWeight: 600, marginBottom: 12 } }, title), + ReadonlyUploadBox({ label: '照片', value: value, onPreview: openPreview }) + ); + } + + return React.createElement('div', { style: layoutStyle }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + React.createElement(Breadcrumb, { items: [{ title: '运维管理' }, { title: '车辆业务' }, { title: '交车管理' }, { title: '交车单-查看' }] }) + ), + + React.createElement(Card, { title: '交车明细', style: cardStyle }, + React.createElement('div', null, + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '车牌号' }, + React.createElement(Select, { + value: initialForm.plateNo, + options: plateOptions, + showSearch: true, + filterOption: filterOption, + placeholder: '请输入或选择车牌号', + allowClear: true, + disabled: true, + style: { width: '100%' } + }) + ), + React.createElement(FormItem, { label: '车辆类型' }, + React.createElement(Input, { value: initialForm.vehicleType, disabled: true }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '品牌' }, + React.createElement(Input, { value: initialForm.brand, disabled: true }) + ), + React.createElement(FormItem, { label: '型号' }, + React.createElement(Input, { value: initialForm.model, disabled: true }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '车辆识别代码' }, + React.createElement(Input, { value: initialForm.vin, disabled: true }) + ), + React.createElement('div', { style: styles.formItem }) + ), + + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '车身广告及放大字', fullWidth: true }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10 } }, + React.createElement(Switch, { checked: !!initialForm.hasAd, disabled: true }), + React.createElement('span', { style: { color: '#666', fontSize: 12 } }, initialForm.hasAd ? '有车身广告' : '无车身广告') + ) + ) + ), + initialForm.hasAd ? React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 12 } }, + React.createElement('div', null, + ReadonlyUploadBox({ label: '广告照片', value: initialForm.adPhoto, max: 1, onPreview: openPreview }) + ), + React.createElement('div', null, + ReadonlyUploadBox({ label: '放大字照片', value: initialForm.bigWordPhoto, max: 1, onPreview: openPreview }) + ) + ) : null, + + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '尾板', fullWidth: true }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10 } }, + React.createElement(Switch, { checked: !!initialForm.hasTailboard, disabled: true }), + React.createElement('span', { style: { color: '#666', fontSize: 12 } }, initialForm.hasTailboard ? '有尾板' : '无尾板') + ) + ) + ), + + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '备胎照片' }, + ReadonlyUploadBox({ label: '', value: initialForm.spareTirePhoto, onPreview: openPreview }) + ), + React.createElement(FormItem, { label: '备胎胎纹深度' }, + React.createElement(Input, { value: initialForm.spareTireDepth, placeholder: '请输入备胎胎纹深度', addonAfter: 'mm', disabled: true }) + ) + ), + + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '驾驶培训', fullWidth: true }, + React.createElement('span', { style: { color: '#52c41a', fontWeight: 600 } }, '已完成视频培训') + ) + ), + + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '司机证照', fullWidth: true }, + React.createElement('div', null, + (initialForm.driverLicenses && initialForm.driverLicenses.length > 0) + ? React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12 } }, + initialForm.driverLicenses.map(function (f) { + return React.createElement('div', { key: f.uid }, + makeThumb(f.url, function () { previewState[1]({ open: true, url: f.url, title: f.name || '预览' }); }), + React.createElement('div', { style: { marginTop: 6, fontSize: 12, color: '#666' } }, f.name) + ); + }) + ) + : React.createElement('div', { style: { fontSize: 12, color: '#999' } }, '上传验车码后显示') + ) + ) + ), + + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '交车里程' }, + React.createElement(Input, { value: initialForm.mileageKm, addonAfter: '公里', disabled: true }) + ), + React.createElement(FormItem, { label: '交车电量' }, + React.createElement(Input, { value: initialForm.batteryKwh, addonAfter: 'kWh', disabled: true }) + ) + ), + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '交车氢量' }, + React.createElement(Input, { value: initialForm.hydrogenAmount, addonAfter: 'MPa', disabled: true }) + ), + React.createElement(FormItem, { label: '送车服务费' }, + React.createElement(Input, { value: isEmpty(initialForm.serviceFee) ? '' : toFixed2(initialForm.serviceFee), placeholder: '请输入送车服务费金额', addonAfter: '元', disabled: true }) + ) + ), + + React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '车辆检查', fullWidth: true }, + React.createElement(Button, { onClick: function () { inspectionDrawerOpenState[1](true); } }, '交车检查单') + ) + ) + ) + ), + + React.createElement(Card, { title: '交车照片', style: cardStyle }, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 12 } }, + PhotoGridColumn('车辆', Object.keys(photos.vehicle).map(function (k) { + return { key: 'v-' + k, label: k, value: photos.vehicle[k] }; + }), true), + PhotoGridColumn('底盘', Object.keys(photos.chassis).map(function (k) { + return { key: 'c-' + k, label: k, value: photos.chassis[k] }; + }), true), + PhotoGridColumn('轮胎', Object.keys(photos.tire).map(function (k) { + return { key: 't-' + k, label: k, value: photos.tire[k] }; + }), true) + ), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 12, marginTop: 12 } }, + PhotoGridSimple('瑕疵', photos.defect), + PhotoGridSimple('其他', photos.other), + React.createElement('div', { style: { background: 'transparent' } }) + ) + ), + + React.createElement('div', { style: footerStyle }, + React.createElement(Button, { onClick: goBack }, '返回') + ), + + React.createElement(Modal, { + open: !!previewState[0].open, + title: previewState[0].title || '预览', + footer: null, + onCancel: function () { previewState[1]({ open: false, url: '', title: '' }); }, + width: 860 + }, + React.createElement('div', { style: { textAlign: 'center' } }, + previewState[0].url ? React.createElement('img', { src: previewState[0].url, style: { maxWidth: '100%', maxHeight: '70vh' } }) : null + ) + ), + + React.createElement(Drawer, { + open: inspectionDrawerOpenState[0], + title: '交车检查单', + width: 920, + placement: 'right', + onClose: function () { inspectionDrawerOpenState[1](false); }, + styles: { body: { display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden', paddingBottom: 0 } }, + footer: React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-start', gap: 8 } }, + React.createElement(Button, { onClick: function () { inspectionDrawerOpenState[1](false); } }, '返回') + ) + }, + React.createElement('div', { style: { flex: 1, minHeight: 0, overflow: 'auto' } }, + React.createElement(Table, { rowKey: 'key', columns: inspectionColumns, dataSource: inspectionList, pagination: false, bordered: true, size: 'small' }) + ) + ) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-ops/pages/26-交车管理-交车单.jsx b/src/prototypes/oneos-web-ops/pages/26-交车管理-交车单.jsx new file mode 100644 index 0000000..019784c --- /dev/null +++ b/src/prototypes/oneos-web-ops/pages/26-交车管理-交车单.jsx @@ -0,0 +1,245 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 交车管理 - 交车单(运维管理-车辆业务-交车管理-交车单) + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Input = antd.Input; + var Button = antd.Button; + var Select = antd.Select; + var Table = antd.Table; + var DatePicker = antd.DatePicker; + var Modal = antd.Modal; + var message = antd.message; + + var requirementModalOpen = useState(false); + var setRequirementModalOpen = requirementModalOpen[1]; + var requirementDocContent = '交车管理-交车单\n一个「数字化资产ONEOS运管平台」中的「交车管理-交车单」模块\n\n1.面包屑:\n1.1.运维管理-车辆业务-交车管理-交车单\n\n每个模块为一个单独卡片:\n\n2.项目详情:表单结构:\n2.1.项目名称:输入框禁用,显示该交车单对应车辆租赁合同中项目名称;\n2.2.合同编码:输入框禁用,显示该交车单对应车辆租赁合同中项目合同编码;\n2.3.客户名称:输入框禁用,显示该交车单对应车辆租赁合同中客户名称;\n2.4.预计交车日期:日期选择器禁用,显示该交车单预计交车日期,分为单日和日期区间两种,格式为:YYYY-MM-DD、YYYY-MM-DD至YYYY-MM-DD;\n2.5.交车区域:输入框禁用,显示该交车单对应车辆租赁合同中交车区域;\n2.6.交车地点:输入框禁用,显示该交车单对应车辆租赁合同中交车地点;\n2.7.业务部门:输入框禁用,显示该交车单对应车辆租赁合同中业务部门;\n2.8.业务负责人:输入框禁用,显示该交车单对应车辆租赁合同中业务负责人;\n\n3.交车明细:列表结构;\n3.1.序号:与车辆租赁合同中序号对应,显示该交车任务所有车辆序号;\n3.2.品牌:选择器禁用,显示该交车单对应车辆租赁合同中品牌;\n3.3.型号:选择器禁用,显示该交车单对应车辆租赁合同中型号;\n3.4.车牌号:已交车车辆显示车牌号,未交车车辆显示-;\n3.5.实际交车日期:日期选择器禁用,显示该交车单实际交车日期,精确至日,格式为:YYYY-MM-DD;\n3.6.交车人:输入框禁用,显示该交车单中该车辆实际交车人员(注意这里是交车单中某辆车的交车人员,一个交车单可同时多个交车人员操作同时交多辆车,而不是最终提交整个交车单的人员);\n3.7.交车状态:已完成、待提交、已签章;\n 3.7.1.已完成:已完成交车提交,但还未完成被授权人签章;\n 3.7.2.待提交:仅点击保存,未完成交车提交;\n 3.7.3.已签章:已完成交车提交并完成被授权人签章;\n3.8.操作:查看、编辑、下载签章文件;\n 3.8.1.查看:点击跳转该车辆交车明细;\n 3.8.2.编辑:交车状态为待提交时显示,点击弹出卡片至交车明细编辑页;\n 3.8.3.下载签章文件:交车完成并完成被授权人签章时显示,点击下载签章文件;\n\n4.底部为提交、取消按钮;\n 4.1.提交:提交按钮必须所有车辆交车状态为已签章时才可提交,否则为禁用状态;\n 4.2.取消:点击返回交车管理列表页;'; + + // 项目详情(只读,来自交车任务/租赁合同) + var projectDetail = useMemo(function () { + return { + projectName: '嘉兴氢能示范项目', + contractCode: 'HT-ZL-2025-001', + customerName: '嘉兴某某物流有限公司', + expectedDate: '2025-02-28 至 2025-03-05', + deliveryRegion: '浙江省-嘉兴市', + deliveryAddress: '嘉兴市南湖区科技大道1号', + businessDept: '业务1部', + businessOwner: '张经理' + }; + }, []); + + // 备车库车辆选项(车牌号下拉仅能选备车库中车辆) + var reservePlateOptions = useMemo(function () { + return [ + { value: '京A12345', label: '京A12345' }, + { value: '京C11111', label: '京C11111' }, + { value: '京D22222', label: '京D22222' }, + { value: '浙A10001', label: '浙A10001' }, + { value: '浙F80088', label: '浙F80088' }, + { value: '沪A30003', label: '沪A30003' } + ]; + }, []); + + // 交车明细列表(序号、品牌、型号、车牌号可编辑、实际交车日期、交车人、交车状态、操作) + var detailListState = useState([ + { key: 1, seq: 1, brand: '东风', model: 'DFH1180', plateNo: '京A12345', actualDate: '2025-02-28', deliveryPerson: '张三', status: '客户已签章' }, + { key: 2, seq: 2, brand: '东风', model: 'DFH1250', plateNo: '京C11111', actualDate: '2025-02-28', deliveryPerson: '张三', status: '客户已签章' }, + { key: 3, seq: 3, brand: '福田', model: 'BJ1180', plateNo: '浙A10001', actualDate: '2025-03-01', deliveryPerson: '李四', status: '待客户签章' }, + { key: 4, seq: 4, brand: '福田', model: 'BJ1250', plateNo: '浙F80088', actualDate: '2025-03-01', deliveryPerson: '李四', status: '待客户签章' }, + { key: 5, seq: 5, brand: '重汽', model: 'HOWO-T5G', plateNo: '沪A30003', actualDate: '2025-03-02', deliveryPerson: '王五', status: '已保存' }, + { key: 6, seq: 6, brand: '陕汽', model: '德龙X3000', plateNo: '京D22222', actualDate: '2025-03-02', deliveryPerson: '王五', status: '已保存' }, + { key: 7, seq: 7, brand: '解放', model: 'J6P', plateNo: '', actualDate: '', deliveryPerson: '', status: '未开始' }, + { key: 8, seq: 8, brand: '欧曼', model: 'EST-A', plateNo: '', actualDate: '', deliveryPerson: '', status: '未开始' }, + { key: 9, seq: 9, brand: '江淮', model: '格尔发K7', plateNo: '', actualDate: '', deliveryPerson: '', status: '未开始' }, + { key: 10, seq: 10, brand: '红岩', model: '杰狮C6', plateNo: '', actualDate: '', deliveryPerson: '', status: '未开始' } + ]); + var detailList = detailListState[0]; + var setDetailList = detailListState[1]; + + function isDetailHistoryStatus(status) { + return status === '客户已签章' || status === '已签章'; + } + + function canEditDetailRow(record) { + var s = record.status; + return s === '未开始' || s === '已保存'; + } + + function hasPlateSelected(record) { + var p = record.plateNo; + return p && String(p).trim() !== '' && p !== '-'; + } + + var allCustomerSigned = useMemo(function () { + return detailList.length > 0 && detailList.every(function (row) { return isDetailHistoryStatus(row.status); }); + }, [detailList]); + + var updateDetailRow = useCallback(function (index, field, value) { + setDetailList(function (prev) { + var next = prev.slice(); + var row = next[index] || {}; + next[index] = Object.assign({}, row, { [field]: value }); + return next; + }); + }, []); + + var handleSubmit = useCallback(function () { + if (!allCustomerSigned) return; + message.success('提交成功(原型)'); + if (typeof window !== 'undefined' && window.history) window.history.back(); + }, [allCustomerSigned]); + + var handleCancel = useCallback(function () { + if (typeof window !== 'undefined' && window.history) window.history.back(); + else message.info('返回交车管理列表页'); + }, []); + + var handleViewDetail = useCallback(function (record) { + message.info('跳转该车辆交车明细(原型)'); + }, []); + + var handleEditDetail = useCallback(function (record, index) { + message.info('弹出卡片至交车明细编辑页(原型)'); + }, []); + + var handleDownloadSign = useCallback(function (record) { + message.success('下载签章文件(原型)'); + }, []); + + var styles = { + page: { padding: '16px 24px 48px', backgroundColor: '#f5f5f5', minHeight: '100vh', fontFamily: '"PingFang SC", "苹方-简", -apple-system, BlinkMacSystemFont, "Microsoft YaHei", sans-serif', fontSize: 14 }, + breadcrumb: { marginBottom: 16, color: '#666' }, + breadcrumbSep: { margin: '0 8px', color: '#999' }, + card: { backgroundColor: '#fff', borderRadius: 8, marginBottom: 16, boxShadow: '0 1px 2px rgba(0,0,0,0.05)', overflow: 'hidden' }, + cardHeader: { display: 'flex', alignItems: 'center', padding: '16px 20px', borderBottom: '1px solid #f0f0f0' }, + cardTitle: { fontSize: 16, fontWeight: 600, color: '#333' }, + cardBody: { padding: '20px 24px' }, + formRow: { display: 'flex', flexWrap: 'wrap', marginBottom: 16 }, + formCol: { flex: '0 0 33.33%', minWidth: 200, paddingRight: 16, marginBottom: 8 }, + formColFull: { flex: '0 0 100%', marginBottom: 8 }, + label: { display: 'block', marginBottom: 6, color: '#333' }, + inputDisabled: { width: '100%', backgroundColor: '#f5f5f5', color: 'rgba(0,0,0,0.65)', cursor: 'not-allowed', border: '1px solid #d9d9d9', borderRadius: 4, padding: '8px 12px', fontSize: 14 }, + footer: { position: 'fixed', bottom: 0, left: 0, right: 0, padding: '12px 24px', backgroundColor: '#fff', borderTop: '1px solid #e8e8e8', display: 'flex', gap: 12, justifyContent: 'flex-start', zIndex: 99 } + }; + + var CardBlock = function (props) { + return React.createElement('div', { id: props.id, style: styles.card }, + React.createElement('div', { style: styles.cardHeader }, + React.createElement('span', { style: styles.cardTitle }, props.title) + ), + React.createElement('div', { style: styles.cardBody }, props.children) + ); + }; + + var FormItem = function (props) { + var colStyle = props.fullWidth ? styles.formColFull : styles.formCol; + return React.createElement('div', { style: colStyle }, + React.createElement('label', { style: styles.label }, props.label), + props.children + ); + }; + + var columns = [ + { title: '序号', dataIndex: 'seq', key: 'seq', width: 70, render: function (v) { return v; } }, + { title: '品牌', dataIndex: 'brand', key: 'brand', width: 100, render: function (v) { return React.createElement(Input, { value: v || '', disabled: true, style: { width: '100%', background: '#f5f5f5' } }); } }, + { title: '型号', dataIndex: 'model', key: 'model', width: 110, render: function (v) { return React.createElement(Input, { value: v || '', disabled: true, style: { width: '100%', background: '#f5f5f5' } }); } }, + { + title: '车牌号', + dataIndex: 'plateNo', + key: 'plateNo', + width: 120, + render: function (v, record) { + if (hasPlateSelected(record)) { + return React.createElement(Input, { value: v || '', disabled: true, style: { width: '100%', background: '#f5f5f5' } }); + } + return React.createElement(Input, { value: '车牌待选', disabled: true, style: { width: '100%', background: '#f5f5f5', color: '#d48806' } }); + } + }, + { + title: '实际交车日期', + dataIndex: 'actualDate', + key: 'actualDate', + width: 130, + render: function (v) { return React.createElement(Input, { value: v || '', disabled: true, style: { width: '100%', background: '#f5f5f5' } }); } + }, + { + title: '交车人', + dataIndex: 'deliveryPerson', + key: 'deliveryPerson', + width: 90, + render: function (v) { return React.createElement(Input, { value: v || '', disabled: true, style: { width: '100%', background: '#f5f5f5' } }); } + }, + { title: '交车状态', dataIndex: 'status', key: 'status', width: 108, render: function (v) { return v || '-'; } }, + { + title: '操作', + key: 'action', + width: 200, + fixed: 'right', + render: function (_, record, index) { + return React.createElement(React.Fragment, null, + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { handleViewDetail(record); } }, '查看'), + canEditDetailRow(record) ? React.createElement(Button, { type: 'link', size: 'small', onClick: function () { handleEditDetail(record, index); } }, '编辑') : null, + isDetailHistoryStatus(record.status) ? React.createElement(Button, { type: 'link', size: 'small', onClick: function () { handleDownloadSign(record); } }, '下载签章文件') : null + ); + } + } + ]; + + return React.createElement('div', { style: styles.page }, + React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 } }, + React.createElement('div', { style: styles.breadcrumb }, + React.createElement('span', null, '运维管理'), + React.createElement('span', { style: styles.breadcrumbSep }, ' / '), + React.createElement('span', null, '车辆业务'), + React.createElement('span', { style: styles.breadcrumbSep }, ' / '), + React.createElement('span', null, '交车管理'), + React.createElement('span', { style: styles.breadcrumbSep }, ' / '), + React.createElement('span', { style: { color: '#1890ff' } }, '交车单') + ), + React.createElement(Button, { type: 'link', onClick: function () { setRequirementModalOpen(true); } }, '查看需求说明') + ), + + React.createElement('div', { id: 'card-project' }, React.createElement(CardBlock, { id: 'card-project', title: '项目详情' }, React.createElement('div', { style: styles.formRow }, + React.createElement(FormItem, { label: '项目名称' }, React.createElement(Input, { value: projectDetail.projectName, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '合同编码' }, React.createElement(Input, { value: projectDetail.contractCode, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '客户名称' }, React.createElement(Input, { value: projectDetail.customerName, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '预计交车日期' }, React.createElement(Input, { value: projectDetail.expectedDate, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '交车区域' }, React.createElement(Input, { value: projectDetail.deliveryRegion, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '交车地点' }, React.createElement(Input, { value: projectDetail.deliveryAddress, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '业务部门' }, React.createElement(Input, { value: projectDetail.businessDept, disabled: true, style: styles.inputDisabled })), + React.createElement(FormItem, { label: '业务负责人' }, React.createElement(Input, { value: projectDetail.businessOwner, disabled: true, style: styles.inputDisabled })) + ))), + + React.createElement('div', { id: 'card-detail', style: { marginTop: 16 } }, React.createElement(CardBlock, { id: 'card-detail', title: '交车明细' }, React.createElement(Table, { + columns: columns, + dataSource: detailList, + rowKey: 'key', + pagination: false, + size: 'small', + scroll: { x: 920 } + }))), + + React.createElement('div', { style: { height: 60 } }), + React.createElement('div', { style: styles.footer }, + React.createElement(Button, { type: 'primary', disabled: !allCustomerSigned, onClick: handleSubmit }, '提交'), + React.createElement(Button, { onClick: handleCancel }, '取消') + ), + + React.createElement(Modal, { + title: '需求说明', + open: requirementModalOpen[0], + onCancel: function () { setRequirementModalOpen(false); }, + footer: React.createElement(Button, { onClick: function () { setRequirementModalOpen(false); } }, '关闭'), + width: 640, + destroyOnClose: true + }, React.createElement('div', { style: { maxHeight: 560, overflowY: 'auto', whiteSpace: 'pre-wrap', lineHeight: 1.6, fontSize: 13 } }, requirementDocContent)) + ); +}; + +export default Component; diff --git a/src/prototypes/oneos-web-ops/pages/27-交车管理.jsx b/src/prototypes/oneos-web-ops/pages/27-交车管理.jsx new file mode 100644 index 0000000..cf91354 --- /dev/null +++ b/src/prototypes/oneos-web-ops/pages/27-交车管理.jsx @@ -0,0 +1,3362 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 车辆业务 - 交车管理(ONEOS运管平台,布局参照新增租赁合同) + +var DV_KPI_STYLE = '' + + '.dv-kpi-stats-row{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-bottom:16px;}' + + '@media (max-width:768px){.dv-kpi-stats-row{grid-template-columns:repeat(1,minmax(0,1fr));}}' + + '.lc-alert-card{display:flex;align-items:flex-start;gap:12px;padding:14px 30px 14px 16px;border-radius:12px;border:1px solid #e2e8f0;background:#fff;position:relative;overflow:hidden;min-width:0;}' + + '.lc-alert-card-main{flex:1;min-width:0;}' + + '.lc-alert-card-icon{flex-shrink:0;width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;}' + + '.lc-alert-card-val{font-size:26px;font-weight:800;line-height:1.1;color:#0f172a;font-variant-numeric:tabular-nums;}' + + '.lc-alert-card-title{font-size:13px;font-weight:600;color:#334155;margin-top:2px;}' + + '.lc-alert-card-tip-anchor{position:absolute;top:8px;right:8px;z-index:2;line-height:0;}' + + '.lc-alert-card-tip{width:18px;height:18px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;color:#94a3b8;background:rgba(255,255,255,.92);border:1px solid #e2e8f0;cursor:help;line-height:0;}' + + '.lc-alert-card-tip:hover{color:#64748b;border-color:#cbd5e1;background:#fff;}' + + '.lc-alert-card--total{background:linear-gradient(135deg,#f8fafc 0%,#fff 100%);}' + + '.lc-alert-card--total .lc-alert-card-icon{background:#e2e8f0;color:#475569;}' + + '.lc-alert-card--progress{background:linear-gradient(135deg,#fff7ed 0%,#fff 55%);border-color:#fed7aa;}' + + '.lc-alert-card--progress .lc-alert-card-icon{background:#ffedd5;color:#ea580c;}' + + '.lc-alert-card--progress .lc-alert-card-val{color:#c2410c;}' + + '.lc-alert-card--completed{background:linear-gradient(135deg,#ecfdf5 0%,#fff 55%);border-color:#bbf7d0;}' + + '.lc-alert-card--completed .lc-alert-card-icon{background:#d1fae5;color:#059669;}' + + '.lc-alert-card--completed .lc-alert-card-val{color:#047857;}' + + '.lc-alert-card-clickable{cursor:pointer;transition:box-shadow .2s ease,border-color .2s ease,transform .2s ease;}' + + '.lc-alert-card-clickable:hover{box-shadow:0 4px 14px rgba(15,23,42,.08);}' + + '.lc-alert-card-active{box-shadow:0 0 0 2px rgba(22,93,255,.2)!important;border-color:#165dff!important;}' + + '.lc-multi-plate-pop{width:320px;padding:4px 2px;}' + + '.lc-multi-plate-pop-hint{font-size:12px;color:#64748b;margin-bottom:8px;line-height:1.5;}' + + '.lc-multi-plate-pop-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:10px;}' + + '.lc-multi-plate-trigger{cursor:pointer;}' + + '.lc-multi-plate-trigger .ant-input{cursor:pointer;}'; + +var DV_KPI_ICONS = { + total: React.createElement('svg', { width: 18, height: 18, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' }, + React.createElement('rect', { x: 3, y: 3, width: 7, height: 7 }), React.createElement('rect', { x: 14, y: 3, width: 7, height: 7 }), + React.createElement('rect', { x: 14, y: 14, width: 7, height: 7 }), React.createElement('rect', { x: 3, y: 14, width: 7, height: 7 })), + progress: React.createElement('svg', { width: 18, height: 18, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' }, + React.createElement('circle', { cx: 12, cy: 12, r: 10 }), React.createElement('polyline', { points: '12 6 12 12 16 14' })), + completed: React.createElement('svg', { width: 18, height: 18, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' }, + React.createElement('path', { d: 'M22 11.08V12a10 10 0 1 1-5.93-9.14' }), React.createElement('polyline', { points: '22 4 12 14.01 9 11.01' })) +}; + +var DV_KPI_TIP_SVG = React.createElement('svg', { width: 12, height: 12, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2.2, strokeLinecap: 'round', strokeLinejoin: 'round' }, + React.createElement('circle', { cx: 12, cy: 12, r: 10 }), React.createElement('line', { x1: 12, y1: 16, x2: 12, y2: 12 }), React.createElement('line', { x1: 12, y1: 8, x2: 12.01, y2: 8 })); + +function formatDeliveryRegion(region) { + if (!region || region === '-') return '-'; + var r = String(region).trim(); + if (r.indexOf('-') === -1) { + var m = r.match(/^(.+?(?:省|市|自治区|特别行政区))(.+)$/); + if (m && m[2]) r = m[1] + '-' + m[2]; + } + return r; +} + +function parseDeliveryRegionParts(region) { + if (!region || region === '-') return { province: '', city: '', raw: '' }; + var r = String(region).trim(); + var idx = r.indexOf('-'); + if (idx >= 0) { + return { province: r.slice(0, idx).trim(), city: r.slice(idx + 1).trim(), raw: r }; + } + if (r.indexOf('省') >= 0) return { province: r, city: '', raw: r }; + if (r.indexOf('市') >= 0) return { province: '', city: r, raw: r }; + return { province: r, city: '', raw: r }; +} + +function regionCityMatch(cityA, cityB) { + if (!cityA || !cityB) return false; + return cityA === cityB || cityA.indexOf(cityB) >= 0 || cityB.indexOf(cityA) >= 0; +} + +/** 运维人员区域权限是否覆盖目标区域(省-市) */ +function matchRegionPermission(permissions, targetRegion) { + var target = parseDeliveryRegionParts(formatDeliveryRegion(targetRegion)); + if (!target.province && !target.city) return true; + var perms = permissions || []; + for (var i = 0; i < perms.length; i++) { + var perm = String(perms[i] || '').trim(); + if (!perm) continue; + if (perm.indexOf('-') >= 0) { + var scoped = parseDeliveryRegionParts(perm); + if (scoped.province && target.province && scoped.province !== target.province) continue; + if (scoped.city) { + if (regionCityMatch(target.city, scoped.city)) return true; + continue; + } + if (scoped.province && scoped.province === target.province) return true; + continue; + } + if (perm.indexOf('省') >= 0) { + if (target.province === perm) return true; + continue; + } + if (regionCityMatch(target.city, perm)) return true; + } + return false; +} + +var DV_RESERVE_VEHICLE_STATUS_READY = '已备车'; + +function getOperatorRegionPermissions() { + if (typeof window !== 'undefined' && window.DV_MOCK_OPERATOR_REGION_PERMISSIONS && window.DV_MOCK_OPERATOR_REGION_PERMISSIONS.length) { + return window.DV_MOCK_OPERATOR_REGION_PERMISSIONS.slice(); + } + return ['浙江省-嘉兴市']; +} + +function filterRowsByOperatorRegion(rows) { + var perms = getOperatorRegionPermissions(); + return (rows || []).filter(function (row) { + return matchRegionPermission(perms, row.deliveryRegion); + }); +} + +function filterSelectableReserveVehicles(vehicles, operatorPermissions) { + var perms = operatorPermissions || getOperatorRegionPermissions(); + return (vehicles || []).filter(function (vehicle) { + if ((vehicle.vehicleStatus || '') !== DV_RESERVE_VEHICLE_STATUS_READY) return false; + return matchRegionPermission(perms, vehicle.parkingRegion); + }); +} + +function isDeliverySignedStatus(status) { + return status === '客户已签章' || status === '已签章'; +} + +function isDeliveryPendingCustomerSignStatus(status) { + return status === '待客户签章'; +} + +/** 待客户签章 / 客户已签章 使用统一 Tag 样式 */ +var DV_CUSTOMER_SIGN_STATUS_BG = '#2563eb'; + +function isCustomerSignRelatedStatus(status) { + return isDeliveryPendingCustomerSignStatus(status) || isDeliverySignedStatus(status); +} + +function getAuthorizedListFromRecord(record) { + var list = record && record.authorizedList; + if (!Array.isArray(list)) return []; + return list.filter(function (a) { + return a && ((a.name && String(a.name).trim()) || (a.phone && String(a.phone).trim())); + }); +} + +function buildCustomerSignPendingPopoverContent(record) { + var list = getAuthorizedListFromRecord(record); + var boxStyle = { minWidth: 168, fontSize: 13, lineHeight: 1.65 }; + var labelStyle = { color: '#64748b' }; + var valStyle = { color: '#334155', fontWeight: 600 }; + if (!list.length) { + return React.createElement('div', { style: boxStyle }, + React.createElement('div', null, + React.createElement('span', { style: labelStyle }, '授权人姓名:'), + React.createElement('span', { style: valStyle }, '-') + ), + React.createElement('div', { style: { marginTop: 4 } }, + React.createElement('span', { style: labelStyle }, '授权人手机号:'), + React.createElement('span', { style: valStyle }, '-') + ) + ); + } + var children = []; + list.forEach(function (item, idx) { + if (idx > 0) { + children.push(React.createElement('div', { key: 'sep-' + idx, style: { borderTop: '1px solid #f1f5f9', margin: '8px 0' } })); + } + children.push( + React.createElement('div', { key: 'name-' + idx }, + React.createElement('span', { style: labelStyle }, '授权人姓名:'), + React.createElement('span', { style: valStyle }, (item.name && String(item.name).trim()) || '-') + ), + React.createElement('div', { key: 'phone-' + idx, style: { marginTop: 4 } }, + React.createElement('span', { style: labelStyle }, '授权人手机号:'), + React.createElement('span', { style: valStyle }, (item.phone && String(item.phone).trim()) || '-') + ) + ); + }); + return React.createElement('div', { style: boxStyle }, children); +} + +function buildDeliverySignFileName(record) { + var plate = (record.plateNo && String(record.plateNo).trim()) ? String(record.plateNo).trim() : '车牌待选'; + var orderId = record.orderId != null ? String(record.orderId) : 'unknown'; + var vehicleKey = record.vehicleKey != null ? String(record.vehicleKey) : ''; + return '交车签章文件_' + orderId + (vehicleKey ? '_' + vehicleKey : '') + '_' + plate + '.pdf'; +} + +function buildDeliverySignFileContent(record) { + var plate = (record.plateNo && String(record.plateNo).trim()) ? String(record.plateNo).trim() : '车牌待选'; + return [ + '交车签章文件(原型 Mock,联调后对接 E 签宝签章 PDF)', + '', + '合同编号:' + (record.contractCode || '-'), + '项目名称:' + (record.projectName || '-'), + '客户名称:' + (record.customerName || '-'), + '车牌号:' + plate, + '品牌型号:' + (record.brand || '-') + ' / ' + (record.model || '-'), + '交车人:' + (record.deliveryPerson || '-'), + '完成交车时间:' + (record.deliveryTime || '-'), + '签章状态:客户已签章', + '', + '生成时间:' + new Date().toLocaleString('zh-CN', { hour12: false }) + ].join('\n'); +} + +function escapeSignHtml(text) { + return String(text == null ? '-' : text) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + +function buildDeliverySignPreviewHtml(record) { + var filename = buildDeliverySignFileName(record); + var plate = (record.plateNo && String(record.plateNo).trim()) ? String(record.plateNo).trim() : '车牌待选'; + var rows = [ + ['文件名称', filename], + ['合同编号', record.contractCode || '-'], + ['项目名称', record.projectName || '-'], + ['客户名称', record.customerName || '-'], + ['车牌号', plate], + ['品牌型号', (record.brand || '-') + ' / ' + (record.model || '-')], + ['交车人', record.deliveryPerson || '-'], + ['完成交车时间', record.deliveryTime || '-'], + ['签章状态', '客户已签章'], + ['签章方', record.customerName || '-'], + ['预览时间', new Date().toLocaleString('zh-CN', { hour12: false })] + ]; + var bodyRows = rows.map(function (row) { + return '' + escapeSignHtml(row[0]) + '' + escapeSignHtml(row[1]) + ''; + }).join(''); + return '' + + '' + escapeSignHtml(filename) + '' + + '
E签宝签章文件' + + '

' + escapeSignHtml(filename) + '

数字化资产 ONEOS · 交车管理 · 签章文件预览(原型 Mock)
' + + '
' + bodyRows + '
' + + '
本页为原型预览占位,联调后将展示 E 签宝返回的真实签章 PDF 或在线预览地址。
' + + '
'; +} + +function previewDeliverySignFile(record) { + if (!record || !isDeliverySignedStatus(record.deliveryStatus || record.status)) return; + if (typeof window === 'undefined') return; + var html = buildDeliverySignPreviewHtml(record); + var blob = new Blob([html], { type: 'text/html;charset=utf-8' }); + var url = URL.createObjectURL(blob); + var opened = window.open(url, '_blank', 'noopener,noreferrer'); + if (!opened) { + URL.revokeObjectURL(url); + if (typeof message !== 'undefined' && message.warning) message.warning('请允许浏览器弹出窗口以预览签章文件'); + return; + } + if (typeof message !== 'undefined' && message.success) message.success('已在新页面打开签章文件预览'); + setTimeout(function () { URL.revokeObjectURL(url); }, 120000); +} + +function downloadDeliverySignFile(record) { + if (!record || !isDeliverySignedStatus(record.deliveryStatus || record.status)) return; + var filename = buildDeliverySignFileName(record); + var content = buildDeliverySignFileContent(record); + var blob = new Blob([content], { type: 'application/octet-stream' }); + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); + if (typeof message !== 'undefined' && message.success) message.success('已开始下载签章文件'); +} + +// 查看交车单:禁用输入框/选择器可读性增强(背景更淡、文字纯黑) +var DV_VIEW_READONLY_CTRL_CSS = '' + + '.dv-edit-drawer-view .ant-input[disabled],' + + '.dv-edit-drawer-view textarea.ant-input[disabled]{' + + 'color:#000!important;-webkit-text-fill-color:#000!important;' + + 'background-color:#fafafa!important;border-color:#e5e7eb!important;' + + 'cursor:default!important;opacity:1!important;' + + '}' + + '.dv-edit-drawer-view .ant-input-affix-wrapper-disabled,' + + '.dv-edit-drawer-view .ant-input-affix-wrapper[disabled]{' + + 'color:#000!important;background-color:#fafafa!important;' + + 'border-color:#e5e7eb!important;cursor:default!important;opacity:1!important;' + + '}' + + '.dv-edit-drawer-view .ant-input-affix-wrapper-disabled input[disabled],' + + '.dv-edit-drawer-view .ant-input-affix-wrapper[disabled] input[disabled]{' + + 'color:#000!important;-webkit-text-fill-color:#000!important;' + + 'background-color:transparent!important;cursor:default!important;' + + '}' + + '.dv-edit-drawer-view .ant-input-affix-wrapper-disabled .ant-input-suffix,' + + '.dv-edit-drawer-view .ant-input-affix-wrapper[disabled] .ant-input-suffix{' + + 'color:rgba(0,0,0,.65)!important;' + + '}' + + '.dv-edit-drawer-view .ant-select-disabled.ant-select .ant-select-selector{' + + 'color:#000!important;background-color:#fafafa!important;' + + 'border-color:#e5e7eb!important;cursor:default!important;opacity:1!important;' + + '}' + + '.dv-edit-drawer-view .ant-select-disabled .ant-select-selection-item,' + + '.dv-edit-drawer-view .ant-select-disabled .ant-select-selection-placeholder{' + + 'color:#000!important;' + + '}'; + +// 使用方式:window.DeliveryEditDrawer(props) + +function DeliveryEditDrawer(props) { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + var useRef = React.useRef; + var useEffect = React.useEffect; + + var open = props.open; + var record = props.record; + var onClose = props.onClose; + var onSave = props.onSave; + var onSubmit = props.onSubmit; + var readOnly = props.readOnly === true || props.mode === 'view'; + + var antd = window.antd; + var Drawer = antd.Drawer; + var Button = antd.Button; + var Input = antd.Input; + var Select = antd.Select; + var Switch = antd.Switch; + var Modal = antd.Modal; + var Table = antd.Table; + var Tag = antd.Tag; + var Popover = antd.Popover; + var message = antd.message; + + function RequiredLabel(text) { + return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 4 } }, + React.createElement('span', { style: { color: '#ef4444', fontWeight: 600 } }, '*'), + React.createElement('span', null, text) + ); + } + + function isEmpty(v) { + return v === null || v === undefined || String(v).trim() === ''; + } + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + function fileToDataUrl(file, cb) { + try { + var reader = new FileReader(); + reader.onload = function (e) { cb(null, (e && e.target && e.target.result) || ''); }; + reader.onerror = function () { cb(new Error('read error')); }; + reader.readAsDataURL(file); + } catch (e) { cb(e); } + } + + var reserveVehicles = useMemo(function () { + return [ + { plateNo: '浙F80088', vehicleStatus: DV_RESERVE_VEHICLE_STATUS_READY, parkingRegion: '浙江省-嘉兴市', parkingLot: '嘉兴港区氢能停车场', vehicleType: '厢式车', brand: '福田', model: 'BJ1180', vin: 'LJNAU1A2XK7654321', hasAd: false, adPhoto: [], bigWordPhoto: [], hasTailboard: false }, + { plateNo: '浙F88601', vehicleStatus: DV_RESERVE_VEHICLE_STATUS_READY, parkingRegion: '浙江省-嘉兴市', parkingLot: '平湖指定停车场', vehicleType: '4.5吨冷链车', brand: '现代', model: '帕力安牌4.5吨冷链车', vin: 'LNBSCPKB8RR123888', hasAd: false, adPhoto: [], bigWordPhoto: [], hasTailboard: true }, + { plateNo: '浙A10088', vehicleStatus: DV_RESERVE_VEHICLE_STATUS_READY, parkingRegion: '浙江省-杭州市', parkingLot: '未来科技城地下停车场', vehicleType: '城配货车', brand: '福田', model: 'BJ1190', vin: 'LJNAU1A2XK8888001', hasAd: true, adPhoto: [{ uid: 'ad-hz', name: '广告照片.jpg', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=Ad-HZ' }], bigWordPhoto: [], hasTailboard: false }, + { plateNo: '粤AGP4598', vehicleStatus: DV_RESERVE_VEHICLE_STATUS_READY, parkingRegion: '广东省-广州市', parkingLot: '广州南沙物流园停车场', vehicleType: '4.5吨冷链车', brand: '现代', model: '帕力安牌4.5吨冷链车', vin: 'LNBSCPKB9RR223402', hasAd: false, adPhoto: [], bigWordPhoto: [], hasTailboard: false }, + { plateNo: '川A99999', vehicleStatus: DV_RESERVE_VEHICLE_STATUS_READY, parkingRegion: '四川省-成都市', parkingLot: '成都龙泉驿停车场', vehicleType: '4.5吨冷链车', brand: '现代', model: '帕力安牌4.5吨冷链车', vin: 'LNBSCPKB9RR223999', hasAd: false, adPhoto: [], bigWordPhoto: [], hasTailboard: false }, + { plateNo: '京A12345', vehicleStatus: '备车中', parkingRegion: '浙江省-嘉兴市', parkingLot: '嘉兴测试停车场', vehicleType: '牵引车', brand: '东风', model: 'DFH1180', vin: 'LJNAU1A2XK1234567', hasAd: true, adPhoto: [{ uid: 'ad1', name: '广告照片.jpg', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=Ad' }], bigWordPhoto: [{ uid: 'bw1', name: '放大字.jpg', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=BigWord' }], hasTailboard: true }, + { plateNo: '沪A30003', vehicleStatus: DV_RESERVE_VEHICLE_STATUS_READY, parkingRegion: '上海市-上海市', parkingLot: '浦东停车场', vehicleType: '厢式车', brand: '重汽', model: 'HOWO-T5G', vin: 'LJNAU1A2XK9999000', hasAd: true, adPhoto: [{ uid: 'ad2', name: '广告2.jpg', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=Ad2' }], bigWordPhoto: [{ uid: 'bw2', name: '放大字2.jpg', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=BW2' }], hasTailboard: true } + ]; + }, []); + + var plateOptions = useMemo(function () { + return filterSelectableReserveVehicles(reserveVehicles, getOperatorRegionPermissions()).map(function (v) { + return { + value: v.plateNo, + label: v.plateNo + ' · ' + (v.parkingLot || formatDeliveryRegion(v.parkingRegion)) + }; + }); + }, [reserveVehicles]); + + var vehicleByPlate = useMemo(function () { + var map = {}; + reserveVehicles.forEach(function (v) { map[v.plateNo] = v; }); + return map; + }, [reserveVehicles]); + + function isPlatePendingInForm(plateNo) { + if (plateNo == null || plateNo === undefined) return true; + var s = String(plateNo).trim(); + return s === '' || s === '-'; + } + + function displayDisabledField(v) { + var s = v == null || v === undefined ? '' : String(v).trim(); + return s || '-'; + } + + function displayFormVin(plateNo, vin) { + if (isPlatePendingInForm(plateNo)) return '-'; + return displayDisabledField(vin); + } + + function syncVehicleInfoFromReserveRecord(reserveRecord, deliveryRec, plate) { + // 备车记录 → 交车单「车辆信息」同步(驾驶培训字段不参与同步,见 buildInitialForm / handlePlateChange) + var hasPlate = plate && !isPlatePendingInForm(plate); + return { + vehicleType: (reserveRecord && reserveRecord.vehicleType) || (deliveryRec && deliveryRec.vehicleType) || '', + brand: (reserveRecord && reserveRecord.brand) || (deliveryRec && deliveryRec.brand) || '', + model: (reserveRecord && reserveRecord.model) || (deliveryRec && deliveryRec.model) || '', + vin: hasPlate ? ((reserveRecord && reserveRecord.vin) || (deliveryRec && deliveryRec.vin) || '') : '', + hasAd: !!(reserveRecord && reserveRecord.hasAd), + adPhoto: (reserveRecord && reserveRecord.adPhoto) ? reserveRecord.adPhoto.slice() : [], + bigWordPhoto: (reserveRecord && reserveRecord.bigWordPhoto) ? reserveRecord.bigWordPhoto.slice() : [], + hasTailboard: reserveRecord ? !!reserveRecord.hasTailboard : false, + spareTirePhoto: (reserveRecord && reserveRecord.spareTirePhoto) ? reserveRecord.spareTirePhoto.slice() : [], + spareTireDepth: (reserveRecord && reserveRecord.spareTireDepth) || '' + }; + } + + function buildInitialForm(rec) { + var plate = (rec && rec.plateNo && String(rec.plateNo).trim()) || undefined; + var reserveRecord = plate ? vehicleByPlate[plate] : null; + if (!reserveRecord && rec && rec.brand && plate) { + reserveRecord = { plateNo: plate, vehicleType: rec.vehicleType || '', brand: rec.brand || '', model: rec.model || '', vin: rec.vin || '', hasAd: false, adPhoto: [], bigWordPhoto: [], hasTailboard: false }; + } + var synced = syncVehicleInfoFromReserveRecord(reserveRecord, rec, plate); + return Object.assign({ + plateNo: plate, + trainingRecognized: false, + driverLicenses: [], + driverFrontPhoto: [], + mileageKm: rec && rec.deliveryMileage != null ? String(rec.deliveryMileage) : '', + batteryPct: rec && rec.deliveryElec != null ? String(rec.deliveryElec) : '', + hydrogenAmount: rec && rec.deliveryH2 != null ? String(rec.deliveryH2) : '', + hydrogenUnit: (rec && rec.deliveryH2Unit === 'MPa') ? 'MPa' : '%', + serviceFee: '' + }, synced); + } + + var formState = useState(buildInitialForm(null)); + var form = formState[0]; + var setForm = formState[1]; + var activeSectionState = useState('basic'); + var activeSection = activeSectionState[0]; + var setActiveSection = activeSectionState[1]; + var submittingState = useState(false); + var submitting = submittingState[0]; + var setSubmitting = submittingState[1]; + var previewState = useState({ open: false, url: '', title: '', gallery: [], index: 0 }); + var ocrModalState = useState({ open: false, photoUrl: '', depth: '6.50' }); + var trainingInputRef = useRef(null); + + useEffect(function () { + if (open && record) { + var initial = buildInitialForm(record); + var nextPhotos = createEmptyPhotos(); + if (readOnly && record.deliveryStatus && record.deliveryStatus !== '未开始' && record.deliveryStatus !== '已保存') { + initial = Object.assign({}, initial, buildDriverInfoFromPickupCode(), buildViewFormPhotoExtras()); + nextPhotos = buildViewDeliveryPhotos(); + } + setForm(initial); + setActiveSection('basic'); + setSubmitting(false); + setPhotos(nextPhotos); + setInspectionList(buildInspectionList()); + } + }, [open, record && record.id, readOnly]); + + function updateForm(patch) { + setForm(function (p) { return Object.assign({}, p, patch); }); + } + + function handlePlateChange(v) { + if (!v) { + updateForm({ + plateNo: undefined, + vehicleType: '', + brand: '', + model: '', + vin: '', + hasAd: false, + adPhoto: [], + bigWordPhoto: [], + hasTailboard: false, + spareTirePhoto: [], + spareTireDepth: '', + trainingRecognized: false, + driverLicenses: [], + driverFrontPhoto: [] + }); + return; + } + var reserveRecord = vehicleByPlate[v]; + updateForm(Object.assign({ + plateNo: v, + trainingRecognized: false, + driverLicenses: [], + driverFrontPhoto: [] + }, syncVehicleInfoFromReserveRecord(reserveRecord, null, v))); + } + + function makeThumb(url, onPreview, onRemove) { + return React.createElement('div', { style: { width: 72, height: 72, borderRadius: 8, border: '1px solid #e2e8f0', overflow: 'hidden', position: 'relative', background: '#f8fafc' } }, + React.createElement('img', { src: url, style: { width: '100%', height: '100%', objectFit: 'cover', cursor: 'pointer' }, onClick: onPreview }), + onRemove ? React.createElement('button', { + type: 'button', + 'aria-label': '删除图片', + style: { position: 'absolute', right: 4, top: 4, width: 22, height: 22, borderRadius: 999, border: 'none', background: 'rgba(15,23,42,.65)', color: '#fff', cursor: 'pointer', fontSize: 12, lineHeight: '22px', padding: 0 }, + onClick: function (e) { e.stopPropagation(); onRemove(); } + }, '×') : null + ); + } + + function mockViewPhoto(uid, name) { + return [{ uid: uid, name: name + '.jpg', url: 'https://dummyimage.com/640x360/e2e8f0/475569&text=' + encodeURIComponent(name) }]; + } + + function mockDeliveryPhotoItem(uid, seed, slotLabel) { + return { + uid: uid, + name: slotLabel + '.jpg', + url: 'https://picsum.photos/seed/dv-' + seed + '/960/540' + }; + } + + function mockDeliveryPhotoSlot(uid, seed, slotLabel) { + return [mockDeliveryPhotoItem(uid, seed, slotLabel)]; + } + + function mockDeliveryPhotoList(items) { + return items.map(function (it) { + return mockDeliveryPhotoItem(it.uid, it.seed, it.label); + }); + } + + function buildDeliveryPhotoGallery(photoData) { + var list = []; + var slotGroups = [ + { key: 'vehicle', label: '车辆' }, + { key: 'chassis', label: '底盘' }, + { key: 'tire', label: '轮胎' } + ]; + slotGroups.forEach(function (group) { + var groupMap = (photoData && photoData[group.key]) || {}; + Object.keys(groupMap).forEach(function (slotLabel) { + (groupMap[slotLabel] || []).forEach(function (file) { + list.push({ + uid: file.uid, + url: file.url, + name: file.name, + title: group.label + ' · ' + slotLabel + }); + }); + }); + }); + ['defect', 'other'].forEach(function (groupKey) { + var groupLabel = groupKey === 'defect' ? '瑕疵' : '其他'; + ((photoData && photoData[groupKey]) || []).forEach(function (file, idx) { + var files = (photoData && photoData[groupKey]) || []; + list.push({ + uid: file.uid, + url: file.url, + name: file.name, + title: files.length > 1 ? groupLabel + ' · ' + (file.name || (groupLabel + (idx + 1))) : groupLabel + ' · ' + (file.name || groupLabel) + }); + }); + }); + return list; + } + + function closePhotoPreview() { + previewState[1]({ open: false, url: '', title: '', gallery: [], index: 0 }); + } + + function openPhotoPreview(options) { + var gallery = (options && options.gallery) || []; + var index = 0; + if (gallery.length && options && options.uid) { + for (var i = 0; i < gallery.length; i++) { + if (gallery[i].uid === options.uid) { + index = i; + break; + } + } + } + var current = gallery.length ? gallery[index] : { url: options.url, title: options.title }; + previewState[1]({ + open: true, + url: current.url, + title: current.title || (options && options.title) || '预览', + gallery: gallery, + index: index + }); + } + + function shiftPhotoPreview(step) { + previewState[1](function (prev) { + var gallery = prev.gallery || []; + if (gallery.length <= 1) return prev; + var nextIndex = prev.index + step; + if (nextIndex < 0) nextIndex = gallery.length - 1; + if (nextIndex >= gallery.length) nextIndex = 0; + var item = gallery[nextIndex]; + return Object.assign({}, prev, { + index: nextIndex, + url: item.url, + title: item.title + }); + }); + } + + function buildViewFormPhotoExtras() { + return { + spareTirePhoto: mockViewPhoto('sp1', '备胎照片'), + spareTireDepth: '6.50' + }; + } + + function buildViewDeliveryPhotos() { + return { + vehicle: { + '仪表盘': mockDeliveryPhotoSlot('v1', 'vehicle-dash', '仪表盘'), + '车辆正前': mockDeliveryPhotoSlot('v2', 'vehicle-front', '车辆正前'), + '车辆左前方': mockDeliveryPhotoSlot('v3', 'vehicle-left-front', '车辆左前方'), + '车辆左后方': mockDeliveryPhotoSlot('v4', 'vehicle-left-rear', '车辆左后方'), + '车辆右前方': mockDeliveryPhotoSlot('v5', 'vehicle-right-front', '车辆右前方'), + '车辆右后方': mockDeliveryPhotoSlot('v6', 'vehicle-right-rear', '车辆右后方') + }, + chassis: { + '正前方位底部': mockDeliveryPhotoSlot('c1', 'chassis-front', '正前方位底部'), + '左侧前方底部': mockDeliveryPhotoSlot('c2', 'chassis-left-front', '左侧前方底部'), + '左侧后方底部': mockDeliveryPhotoSlot('c3', 'chassis-left-rear', '左侧后方底部'), + '正后方位底部': mockDeliveryPhotoSlot('c4', 'chassis-rear', '正后方位底部'), + '右侧前方底部': mockDeliveryPhotoSlot('c5', 'chassis-right-front', '右侧前方底部'), + '右侧后方底部': mockDeliveryPhotoSlot('c6', 'chassis-right-rear', '右侧后方底部') + }, + tire: { + '左前': mockDeliveryPhotoSlot('t1', 'tire-left-front', '左前'), + '右前': mockDeliveryPhotoSlot('t2', 'tire-right-front', '右前'), + '左后内': mockDeliveryPhotoSlot('t3', 'tire-left-rear-in', '左后内'), + '左后外': mockDeliveryPhotoSlot('t4', 'tire-left-rear-out', '左后外'), + '右后内': mockDeliveryPhotoSlot('t5', 'tire-right-rear-in', '右后内'), + '右后外': mockDeliveryPhotoSlot('t6', 'tire-right-rear-out', '右后外') + }, + defect: mockDeliveryPhotoList([ + { uid: 'd1', seed: 'defect-scratch', label: '瑕疵-刮擦' }, + { uid: 'd2', seed: 'defect-dent', label: '瑕疵-凹陷' }, + { uid: 'd3', seed: 'defect-paint', label: '瑕疵-漆面' } + ]), + other: mockDeliveryPhotoList([ + { uid: 'o1', seed: 'other-tools', label: '其他-随车工具' }, + { uid: 'o2', seed: 'other-cargo', label: '其他-车厢物品' } + ]) + }; + } + + function ReadonlyPhotoBox(boxProps) { + var label = boxProps.label; + var value = boxProps.value || []; + var gallery = boxProps.gallery; + return React.createElement('div', null, + label ? React.createElement('div', { style: { fontSize: 13, color: '#475569', marginBottom: 8, fontWeight: 500 } }, label) : null, + value.length > 0 + ? React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' } }, + value.map(function (f) { + return React.createElement('div', { key: f.uid }, + makeThumb(f.url, function () { + openPhotoPreview({ + gallery: gallery && gallery.length ? gallery : null, + uid: f.uid, + url: f.url, + title: (gallery && gallery.length) + ? ((gallery.filter(function (g) { return g.uid === f.uid; })[0] || {}).title || f.name || label || '预览') + : (f.name || label || '预览') + }); + }) + ); + }) + ) + : React.createElement('span', { style: { fontSize: 13, color: '#94a3b8' } }, '-') + ); + } + + function UploadBox(uploadProps) { + var label = uploadProps.label; + var value = uploadProps.value || []; + var unlimited = !!uploadProps.unlimited; + var max = unlimited ? Infinity : (uploadProps.max || 1); + var onChange = uploadProps.onChange; + var disabled = !!uploadProps.disabled; + function handlePick(e) { + if (disabled) return; + var f = e && e.target && e.target.files && e.target.files[0]; + if (!f) return; + fileToDataUrl(f, function (err, url) { + if (err) { message.error('上传失败'); return; } + var next = value.slice(); + next.push({ uid: String(Date.now()), name: f.name || 'image', url: url }); + if (!unlimited && next.length > max) next = next.slice(next.length - max); + onChange && onChange(next); + }); + e.target.value = ''; + } + return React.createElement('div', null, + label ? React.createElement('div', { style: { fontSize: 13, color: '#475569', marginBottom: 8, fontWeight: 500 } }, label) : null, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' } }, + value.map(function (f) { + return React.createElement('div', { key: f.uid }, + makeThumb(f.url, function () { previewState[1]({ open: true, url: f.url, title: f.name }); }, disabled ? null : function () { onChange && onChange(value.filter(function (x) { return x.uid !== f.uid; })); }) + ); + }), + (!disabled && (unlimited || value.length < max)) ? React.createElement('label', { style: { width: 72, height: 72, borderRadius: 8, border: '1px dashed #cbd5e1', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', color: '#64748b', cursor: 'pointer', fontSize: 20, lineHeight: 1, background: '#fff', transition: 'border-color .2s' } }, + React.createElement('input', { type: 'file', accept: 'image/*', style: { display: 'none' }, onChange: handlePick }), + React.createElement('span', { style: { fontSize: 22, lineHeight: 1, marginBottom: 2 } }, '+'), + React.createElement('span', { style: { fontSize: 12 } }, '上传') + ) : null + ), + uploadProps.tip ? React.createElement('div', { style: { marginTop: 6, fontSize: 12, color: '#94a3b8', lineHeight: 1.5 } }, uploadProps.tip) : null + ); + } + + var EDIT_PHOTO_UPLOAD_HINT = '照片上传说明:jpg、jpeg、png、gif、webp 格式,单张不超过 5MB,支持预览与删除(适用于本页所有照片上传项)'; + + function createEmptyPhotos() { + return { + vehicle: { + '仪表盘': [], + '车辆正前': [], + '车辆左前方': [], + '车辆左后方': [], + '车辆右前方': [], + '车辆右后方': [] + }, + chassis: { + '正前方位底部': [], + '左侧前方底部': [], + '左侧后方底部': [], + '正后方位底部': [], + '右侧前方底部': [], + '右侧后方底部': [] + }, + tire: { + '左前': [], + '右前': [], + '左后内': [], + '左后外': [], + '右后内': [], + '右后外': [] + }, + defect: [], + other: [] + }; + } + + function updatePhotoSlot(groupKey, slotKey, list) { + setPhotos(function (p) { + var n = Object.assign({}, p); + if (slotKey) { + n[groupKey] = Object.assign({}, p[groupKey] || {}); + n[groupKey][slotKey] = list; + } else { + n[groupKey] = list; + } + return n; + }); + } + + function PhotoModuleTitle(title, required) { + return React.createElement('div', { + style: { + display: 'flex', + alignItems: 'center', + gap: 8, + marginBottom: 10, + fontSize: 14, + fontWeight: 700, + color: '#0f172a' + } + }, + React.createElement('span', { style: { width: 3, height: 14, borderRadius: 2, background: '#2563eb', flexShrink: 0 } }), + (required && !readOnly) ? RequiredLabel(title) : title + ); + } + + function PhotoGridColumn(title, items, required, previewGallery) { + return React.createElement('div', { style: { marginBottom: 20, minWidth: 0 } }, + PhotoModuleTitle(title, required), + React.createElement('div', { style: { border: '1px solid #e2e8f0', borderRadius: 10, padding: 12, background: '#fafbfc' } }, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 12 } }, + items.map(function (it) { + return React.createElement('div', { key: it.key }, + readOnly + ? ReadonlyPhotoBox({ label: it.label, value: it.value, gallery: previewGallery }) + : UploadBox({ + label: required ? RequiredLabel(it.label) : it.label, + value: it.value, + max: 1, + disabled: readOnly, + onChange: it.onChange + }) + ); + }) + ) + ) + ); + } + + function PhotoGridSimple(title, value, onChange, previewGallery) { + return React.createElement('div', { style: { marginBottom: 20, minWidth: 0 } }, + PhotoModuleTitle(title, false), + React.createElement('div', { style: { border: '1px solid #e2e8f0', borderRadius: 10, padding: 12, background: '#fafbfc' } }, + readOnly + ? ReadonlyPhotoBox({ value: value, gallery: previewGallery }) + : UploadBox({ value: value, unlimited: true, disabled: readOnly, onChange: onChange }) + ) + ); + } + + function buildPhotoGridItems(groupKey) { + var group = photos[groupKey] || {}; + return Object.keys(group).map(function (k) { + return { + key: groupKey + '-' + k, + label: k, + value: group[k] || [], + onChange: function (l) { updatePhotoSlot(groupKey, k, l); } + }; + }); + } + + function FormItem(itemProps) { + return React.createElement('div', { style: { marginBottom: 16, minWidth: 0 } }, + React.createElement('div', { style: { fontSize: 13, color: '#475569', marginBottom: 6, fontWeight: 500 } }, + itemProps.required ? RequiredLabel(itemProps.label) : itemProps.label + ), + itemProps.children + ); + } + + var inspectionCategoryItems = { + '车灯': ['大灯', '转向灯', '小灯', '示廓灯', '刹车灯', '倒车灯', '牌照灯', '防雾灯', '室内灯'], + '仪表盘': ['氢系统指示', '电控系统指示', '数值清晰准确', '故障报警灯'], + '驾驶室': ['点烟器', '车窗升降', '按键开关', '雨刮器', '内后视镜是否正常', '内/外门把手', '安全带', '空调冷暖风', '仪表盘', '门锁功能', '手刹', '车钥匙功能是否正常', '喇叭', '音响功能', '遮阳板', '主副驾座椅', '方向盘', '内饰干净整洁'], + '轮胎': ['前左胎', '前右胎', '后左胎', '后右胎', '备胎'], + '液位检查': ['冷却液', '制动液', '玻璃水'], + '外观检查': ['车身外观', '漆面', '玻璃'], + '车辆外观': ['整车外观'], + '其他': ['其他检查项'], + '随车工具': ['三角牌', '灭火器', '反光背心'], + '随车证件': ['行驶证', '营运证', '保险单'], + '整车': ['整车状态'], + '燃料电池系统': ['氢系统', '储氢瓶'], + '冷机': ['冷机运行'], + '制动系统': ['制动踏板', '驻车制动'] + }; + + function buildInspectionList() { + var list = []; + var categories = Object.keys(inspectionCategoryItems); + for (var ci = 0; ci < categories.length; ci++) { + var cat = categories[ci]; + var items = inspectionCategoryItems[cat] || []; + for (var ji = 0; ji < items.length; ji++) { + var it = items[ji]; + var isTire = cat === '轮胎'; + list.push({ + key: 'ins-' + ci + '-' + ji, + category: cat, + item: it, + checked: true, + treadDepth: isTire ? '6.5' : '', + remark: '' + }); + } + } + return list; + } + + var inspectionListState = useState(buildInspectionList); + var inspectionList = inspectionListState[0]; + var setInspectionList = inspectionListState[1]; + var inspectionListRef = useRef(null); + inspectionListRef.current = inspectionList; + + function updateInspectionRow(key, patch) { + setInspectionList(function (prev) { + return (prev || []).map(function (r) { + if (r.key !== key) return r; + return Object.assign({}, r, patch); + }); + }); + } + + var inspectionColumns = useMemo(function () { + return [ + { + title: '类别', + dataIndex: 'category', + key: 'category', + width: 120, + render: function (text, record, index) { + var rows = inspectionListRef.current || []; + var cat = record && record.category; + if (!cat) return { children: text, props: { rowSpan: 1 } }; + var isFirst = true; + for (var i = index - 1; i >= 0; i--) { + if (!rows[i] || rows[i].category !== cat) break; + isFirst = false; + break; + } + if (!isFirst) return { children: null, props: { rowSpan: 0 } }; + var span = 1; + for (var j = index + 1; j < rows.length; j++) { + if (!rows[j] || rows[j].category !== cat) break; + span++; + } + return { children: text, props: { rowSpan: span } }; + } + }, + { title: '检查项目', dataIndex: 'item', key: 'item', width: 180 }, + { + title: '检查情况', + dataIndex: 'checked', + key: 'checked', + width: 160, + render: function (_, insRecord) { + var isTire = insRecord && insRecord.category === '轮胎'; + return isTire + ? React.createElement(Input, { + value: insRecord.treadDepth, + placeholder: '请输入胎纹深度', + addonAfter: 'mm', + disabled: readOnly, + onChange: function (e) { updateInspectionRow(insRecord.key, { treadDepth: e.target.value }); } + }) + : React.createElement(Switch, { + checked: !!insRecord.checked, + disabled: readOnly, + onChange: function (v) { updateInspectionRow(insRecord.key, { checked: !!v }); } + }); + } + }, + { + title: '备注', + dataIndex: 'remark', + key: 'remark', + render: function (_, insRecord) { + return React.createElement(Input, { + value: insRecord.remark, + placeholder: '请输入', + disabled: readOnly, + onChange: function (e) { updateInspectionRow(insRecord.key, { remark: e.target.value }); } + }); + } + } + ]; + }, [readOnly]); + + var photoState = useState(createEmptyPhotos); + var photos = photoState[0]; + var setPhotos = photoState[1]; + + var showSignFileSection = readOnly && record && isDeliverySignedStatus(record.deliveryStatus || record.status); + var signFileName = showSignFileSection ? buildDeliverySignFileName(record) : ''; + + var sectionNav = [ + { key: 'basic', label: '交车车辆' }, + { key: 'equip', label: '车辆信息' }, + { key: 'metrics', label: '交车数据' }, + { key: 'inspection', label: '交车检查单' }, + { key: 'photos', label: '交车照片' } + ]; + if (showSignFileSection) { + sectionNav.push({ key: 'esign', label: 'E签宝签章' }); + } + + var SECTION_SCROLL_MARGIN = 12; + + function scrollToSection(key) { + setActiveSection(key); + var el = document.getElementById('dv-edit-section-' + key); + if (el && el.scrollIntoView) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + + function SectionCard(cardProps) { + return React.createElement('div', { + id: cardProps.id, + style: { + scrollMarginTop: SECTION_SCROLL_MARGIN, + marginBottom: 16, + background: '#fff', + borderRadius: 12, + border: '1px solid #e2e8f0', + boxShadow: '0 1px 2px rgba(15,23,42,.04)', + overflow: 'hidden' + } + }, + React.createElement('div', { + style: { + padding: '14px 16px', + borderBottom: '1px solid #f1f5f9', + fontSize: 14, + fontWeight: 700, + color: '#0f172a', + display: 'flex', + alignItems: 'center', + gap: 8 + } + }, + React.createElement('span', { style: { width: 3, height: 14, borderRadius: 2, background: '#2563eb', flexShrink: 0 } }), + cardProps.title + ), + React.createElement('div', { style: { padding: '16px 16px 8px' } }, cardProps.children) + ); + } + + function validateSubmit() { + if (isEmpty(form.plateNo)) return '请选择车牌号'; + if (form.hasAd) { + if (!form.adPhoto.length) return '请上传广告照片'; + if (!form.bigWordPhoto.length) return '请上传放大字照片'; + } + if (!form.trainingRecognized) return '请上传司机提车码并完成识别'; + if (isEmpty(form.mileageKm)) return '请填写交车里程'; + if (isEmpty(form.batteryPct)) return '请填写交车电量'; + if (isEmpty(form.hydrogenAmount)) return '请填写交车氢量'; + var requiredPhotoGroups = [ + { key: 'vehicle', label: '车辆' }, + { key: 'chassis', label: '底盘' }, + { key: 'tire', label: '轮胎' } + ]; + for (var pi = 0; pi < requiredPhotoGroups.length; pi++) { + var pg = requiredPhotoGroups[pi]; + var groupMap = photos && photos[pg.key]; + var keys = groupMap ? Object.keys(groupMap) : []; + for (var ki = 0; ki < keys.length; ki++) { + var pk = keys[ki]; + if (!(groupMap[pk] && groupMap[pk].length)) return '请上传' + pg.label + '照片:' + pk; + } + } + return ''; + } + + function buildPatchFromForm() { + return { + plateNo: form.plateNo || '', + vehicleType: form.vehicleType, + brand: form.brand, + model: form.model, + vin: form.vin, + deliveryMileage: form.mileageKm === '' ? null : Number(form.mileageKm), + deliveryElec: form.batteryPct === '' ? null : Number(form.batteryPct), + deliveryH2: form.hydrogenAmount === '' ? null : Number(form.hydrogenAmount), + deliveryH2Unit: form.hydrogenUnit, + deliveryStatus: '已保存' + }; + } + + function handleSaveClick() { + onSave && onSave(buildPatchFromForm()); + message.success('已保存'); + } + + function handleSubmitClick() { + var err = validateSubmit(); + if (err) { message.error(err); return; } + Modal.confirm({ + title: '确认交车', + content: '请确认信息填写无误,点击确认完成该车辆交车。', + okText: '确认', + cancelText: '取消', + onOk: function () { + setSubmitting(true); + setTimeout(function () { + setSubmitting(false); + onSubmit && onSubmit(buildPatchFromForm()); + message.success('交车成功'); + onClose && onClose(); + }, 400); + } + }); + } + + function isInvalidPickupCodeFile(file) { + var name = ((file && file.name) || '').toLowerCase(); + return name.indexOf('invalid') >= 0 || name.indexOf('无效') >= 0; + } + + function buildDriverInfoFromPickupCode() { + return { + trainingRecognized: true, + driverFrontPhoto: [{ uid: 'front', name: '司机正面照', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=Driver-Front' }], + driverLicenses: [ + { uid: 'id1', name: '身份证(正面)', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=ID-F' }, + { uid: 'id2', name: '身份证(反面)', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=ID-B' }, + { uid: 'dl', name: '驾驶证', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=DL' }, + { uid: 'qc', name: '从业资格证', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=QC' } + ] + }; + } + + function handleTrainingPick(e) { + var f = e && e.target && e.target.files && e.target.files[0]; + if (!f) return; + var pickedFile = f; + setTimeout(function () { + if (isInvalidPickupCodeFile(pickedFile)) { + updateForm({ trainingRecognized: false, driverFrontPhoto: [], driverLicenses: [] }); + message.error('提车码无效,请重新选择'); + return; + } + updateForm(buildDriverInfoFromPickupCode()); + message.success('提车码识别成功,已加载司机证照'); + }, 500); + e.target.value = ''; + } + + function openPickupCodePicker() { + trainingInputRef.current && trainingInputRef.current.click(); + } + + if (!open) return null; + + var drawerTitle = readOnly ? '查看交车单' : '编辑交车单'; + + var summaryLabelWidth = 80; + function renderSummaryField(labelText, valueText) { + return React.createElement('div', { style: { display: 'flex', alignItems: 'flex-start', gap: 6, minWidth: 0 } }, + React.createElement('span', { style: { color: '#94a3b8', flexShrink: 0, width: summaryLabelWidth, textAlign: 'left' } }, labelText), + React.createElement('span', { style: { color: '#334155', flex: 1, minWidth: 0, textAlign: 'left', wordBreak: 'break-all', lineHeight: 1.5 } }, valueText) + ); + } + + var summaryCard = record ? React.createElement('div', { style: { marginBottom: 16, padding: '12px 14px', borderRadius: 10, background: 'linear-gradient(135deg,#f8fafc 0%,#fff 100%)', border: '1px solid #e2e8f0', fontSize: 13 } }, + React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center', marginBottom: 10, paddingBottom: 10, borderBottom: '1px solid #f1f5f9' } }, + React.createElement(Tag, { style: { margin: 0, border: 'none', background: '#eff6ff', color: '#2563eb', fontWeight: 600 } }, record.customerName || '-'), + React.createElement('span', { style: { color: '#475569' } }, record.contractCode || '-'), + (function () { + var status = record.deliveryStatus || '未开始'; + var signed = isDeliverySignedStatus(status); + var pendingSign = isDeliveryPendingCustomerSignStatus(status); + var signRelated = isCustomerSignRelatedStatus(status); + var tag = React.createElement(Tag, { + style: { + margin: 0, + border: 'none', + background: signRelated ? DV_CUSTOMER_SIGN_STATUS_BG : '#f1f5f9', + color: signRelated ? '#fff' : '#475569', + fontWeight: signRelated ? 600 : 400 + }, + title: signed ? '点击下载签章文件' : undefined, + role: signed ? 'button' : undefined, + tabIndex: signed ? 0 : undefined, + onClick: signed ? function (e) { e.stopPropagation(); downloadDeliverySignFile(record); } : undefined, + onKeyDown: signed ? function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); downloadDeliverySignFile(record); } } : undefined + }, status); + if (pendingSign) { + return React.createElement(Popover, { + content: buildCustomerSignPendingPopoverContent(record), + trigger: 'hover', + placement: 'topLeft', + mouseEnterDelay: 0.15, + mouseLeaveDelay: 0.1, + destroyTooltipOnHide: true + }, tag); + } + return tag; + })() + ), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(2,minmax(0,1fr))', gap: '8px 16px' } }, + renderSummaryField('项目:', record.projectName || '-'), + renderSummaryField('交车地点:', formatDeliveryRegion(record.deliveryRegion)), + renderSummaryField('品牌型号:', (record.brand || '-') + ' / ' + (record.model || '-')), + renderSummaryField('任务来源:', record.taskSource || '-') + ) + ) : null; + + var sectionNavEl = React.createElement('nav', { + 'aria-label': '交车单分节导航', + style: { + width: 108, + flexShrink: 0, + position: 'sticky', + top: 0, + alignSelf: 'flex-start', + paddingTop: 4, + zIndex: 2 + } + }, + React.createElement('div', { style: { fontSize: 12, color: '#94a3b8', fontWeight: 600, marginBottom: 8, paddingLeft: 12, letterSpacing: '.02em' } }, '目录'), + sectionNav.map(function (s) { + var active = activeSection === s.key; + return React.createElement('button', { + key: s.key, + type: 'button', + onClick: function () { scrollToSection(s.key); }, + style: { + display: 'block', + width: '100%', + textAlign: 'left', + border: 'none', + cursor: 'pointer', + padding: '9px 12px', + borderRadius: 8, + fontSize: 13, + lineHeight: 1.4, + fontWeight: active ? 600 : 500, + background: active ? '#eff6ff' : 'transparent', + color: active ? '#2563eb' : '#64748b', + borderLeft: active ? '3px solid #2563eb' : '3px solid transparent', + marginBottom: 2, + transition: 'background .2s,color .2s' + } + }, s.label); + }) + ); + + var sectionBasic = React.createElement(SectionCard, { id: 'dv-edit-section-basic', title: '交车车辆' }, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(2,minmax(0,1fr))', gap: '0 16px' } }, + React.createElement(FormItem, { label: '车牌号', required: !readOnly }, + React.createElement(Select, { value: form.plateNo, options: plateOptions, showSearch: true, filterOption: filterOption, placeholder: '请选择已备车车辆(按区域权限过滤)', allowClear: !readOnly, disabled: readOnly, style: { width: '100%' }, onChange: handlePlateChange }) + ), + React.createElement(FormItem, { label: '车辆类型' }, React.createElement(Input, { value: displayDisabledField(form.vehicleType), disabled: true })), + React.createElement(FormItem, { label: '品牌' }, React.createElement(Input, { value: displayDisabledField(form.brand), disabled: true })), + React.createElement(FormItem, { label: '型号' }, React.createElement(Input, { value: displayDisabledField(form.model), disabled: true })), + React.createElement(FormItem, { label: '车辆识别代码' }, React.createElement(Input, { value: displayFormVin(form.plateNo, form.vin), disabled: true })) + ) + ); + + var sectionEquip = React.createElement(SectionCard, { id: 'dv-edit-section-equip', title: '车辆信息' }, + !readOnly ? React.createElement('div', { style: { marginBottom: 12, fontSize: 12, color: '#94a3b8', lineHeight: 1.6 } }, EDIT_PHOTO_UPLOAD_HINT) : null, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0 16px', marginBottom: form.hasAd ? 8 : 0 } }, + React.createElement(FormItem, { label: '车身广告及放大字', required: !readOnly }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10 } }, + React.createElement(Switch, { checked: !!form.hasAd, disabled: readOnly, onChange: function (v) { updateForm({ hasAd: !!v }); } }), + React.createElement('span', { style: { fontSize: 13, color: '#64748b' } }, form.hasAd ? '有车身广告' : '无车身广告') + ) + ), + React.createElement(FormItem, { label: '尾板', required: !readOnly }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10 } }, + React.createElement(Switch, { checked: !!form.hasTailboard, disabled: readOnly, onChange: function (v) { updateForm({ hasTailboard: !!v }); } }), + React.createElement('span', { style: { fontSize: 13, color: '#64748b' } }, form.hasTailboard ? '有尾板' : '无尾板') + ) + ) + ), + form.hasAd ? React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 8 } }, + readOnly + ? React.createElement(React.Fragment, null, + React.createElement(ReadonlyPhotoBox, { label: '广告照片', value: form.adPhoto }), + React.createElement(ReadonlyPhotoBox, { label: '放大字照片', value: form.bigWordPhoto }) + ) + : React.createElement(React.Fragment, null, + React.createElement(UploadBox, { label: RequiredLabel('广告照片'), value: form.adPhoto, max: 1, disabled: readOnly, onChange: function (l) { updateForm({ adPhoto: l }); } }), + React.createElement(UploadBox, { label: RequiredLabel('放大字照片'), value: form.bigWordPhoto, max: 1, disabled: readOnly, onChange: function (l) { updateForm({ bigWordPhoto: l }); } }) + ) + ) : null, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 } }, + readOnly + ? React.createElement(ReadonlyPhotoBox, { label: '备胎照片', value: form.spareTirePhoto }) + : React.createElement(UploadBox, { label: '备胎照片', value: form.spareTirePhoto, max: 1, disabled: readOnly, onChange: function (l) { updateForm({ spareTirePhoto: l }); if (!readOnly && l && l.length) ocrModalState[1]({ open: true, photoUrl: l[0].url, depth: '6.50' }); } }), + React.createElement(FormItem, { label: '备胎胎纹深度' }, + React.createElement(Input, { value: form.spareTireDepth, placeholder: readOnly ? undefined : '请输入', addonAfter: 'mm', disabled: readOnly, onChange: function (e) { updateForm({ spareTireDepth: e.target.value }); } }) + ) + ), + React.createElement(FormItem, { label: '驾驶培训', required: !readOnly }, + form.trainingRecognized + ? React.createElement('div', null, + readOnly + ? React.createElement('span', { style: { color: '#16a34a', fontWeight: 600, fontSize: 13 } }, '已完成视频培训') + : React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' } }, + React.createElement('span', { style: { color: '#16a34a', fontWeight: 600, fontSize: 13 } }, '已完成视频培训'), + React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0, height: 'auto' }, onClick: openPickupCodePicker }, '重新选择提车码') + ), + !readOnly ? React.createElement('div', { style: { marginTop: 8, fontSize: 12, color: '#64748b', lineHeight: 1.5 } }, '已根据提车码内绑定的司机信息,自动加载司机证照等照片') : null, + form.driverFrontPhoto && form.driverFrontPhoto.length ? React.createElement('div', { style: { marginTop: 12 } }, + React.createElement('div', { style: { fontSize: 13, color: '#475569', marginBottom: 8, fontWeight: 500 } }, '司机正面照'), + makeThumb(form.driverFrontPhoto[0].url, function () { previewState[1]({ open: true, url: form.driverFrontPhoto[0].url, title: '司机正面照' }); }) + ) : null + ) + : (readOnly + ? React.createElement('span', { style: { fontSize: 13, color: '#94a3b8' } }, '-') + : React.createElement(React.Fragment, null, + React.createElement('input', { type: 'file', ref: trainingInputRef, accept: 'image/*', style: { display: 'none' }, onChange: handleTrainingPick }), + React.createElement(Button, { onClick: openPickupCodePicker }, '上传司机提车码'), + React.createElement('div', { style: { marginTop: 8, fontSize: 12, color: '#94a3b8', lineHeight: 1.5 } }, '上传后将自动识别提车码内绑定的司机信息,并展示司机证照等照片') + )) + ), + form.driverLicenses && form.driverLicenses.length ? React.createElement(FormItem, { label: '司机证照' }, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(4,minmax(0,1fr))', gap: 12 } }, + form.driverLicenses.map(function (f) { + return React.createElement('div', { key: f.uid }, + makeThumb(f.url, function () { previewState[1]({ open: true, url: f.url, title: f.name }); }), + React.createElement('div', { style: { marginTop: 4, fontSize: 12, color: '#64748b', textAlign: 'center' } }, f.name) + ); + }) + ) + ) : null + ); + + var sectionInspection = React.createElement(SectionCard, { id: 'dv-edit-section-inspection', title: '交车检查单' }, + React.createElement(Table, { + rowKey: 'key', + size: 'small', + pagination: false, + bordered: true, + dataSource: inspectionList, + columns: inspectionColumns, + scroll: { x: 640 } + }) + ); + + var sectionMetrics = React.createElement(SectionCard, { id: 'dv-edit-section-metrics', title: '交车数据' }, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(2,minmax(0,1fr))', gap: '0 16px' } }, + React.createElement(FormItem, { label: '交车里程', required: !readOnly }, + React.createElement(Input, { value: form.mileageKm, placeholder: '请输入', addonAfter: 'km', disabled: readOnly, onChange: function (e) { updateForm({ mileageKm: e.target.value }); } }) + ), + React.createElement(FormItem, { label: '交车电量', required: !readOnly }, + React.createElement(Input, { value: form.batteryPct, placeholder: '请输入', addonAfter: '%', disabled: readOnly, onChange: function (e) { updateForm({ batteryPct: e.target.value }); } }) + ), + React.createElement(FormItem, { label: '交车氢量', required: !readOnly }, + React.createElement(Input, { value: form.hydrogenAmount, placeholder: '请输入', addonAfter: form.hydrogenUnit, disabled: readOnly, onChange: function (e) { updateForm({ hydrogenAmount: e.target.value }); } }) + ), + React.createElement(FormItem, { label: '送车服务费' }, + React.createElement(Input, { value: form.serviceFee, placeholder: '选填', addonAfter: '元', disabled: readOnly, onChange: function (e) { updateForm({ serviceFee: e.target.value }); } }) + ) + ) + ); + + var deliveryPhotoGallery = readOnly ? buildDeliveryPhotoGallery(photos) : []; + + var sectionPhotos = React.createElement(SectionCard, { id: 'dv-edit-section-photos', title: '交车照片' }, + !readOnly ? React.createElement('div', { style: { marginBottom: 12, fontSize: 12, color: '#94a3b8', lineHeight: 1.6 } }, EDIT_PHOTO_UPLOAD_HINT) : null, + PhotoGridColumn('车辆', buildPhotoGridItems('vehicle'), !readOnly, deliveryPhotoGallery), + PhotoGridColumn('底盘', buildPhotoGridItems('chassis'), !readOnly, deliveryPhotoGallery), + PhotoGridColumn('轮胎', buildPhotoGridItems('tire'), !readOnly, deliveryPhotoGallery), + PhotoGridSimple('瑕疵', photos.defect || [], function (l) { updatePhotoSlot('defect', null, l); }, deliveryPhotoGallery), + PhotoGridSimple('其他', photos.other || [], function (l) { updatePhotoSlot('other', null, l); }, deliveryPhotoGallery) + ); + + var sectionEsign = showSignFileSection ? React.createElement(SectionCard, { id: 'dv-edit-section-esign', title: 'E签宝签章文件' }, + React.createElement('div', { + style: { + display: 'flex', + alignItems: 'flex-start', + gap: 14, + padding: '12px 14px', + borderRadius: 10, + border: '1px solid #e2e8f0', + background: 'linear-gradient(135deg,#f8fafc 0%,#fff 100%)' + } + }, + React.createElement('div', { + style: { + width: 44, + height: 44, + borderRadius: 10, + background: '#fee2e2', + color: '#dc2626', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontSize: 12, + fontWeight: 700, + flexShrink: 0 + } + }, 'PDF'), + React.createElement('div', { style: { flex: 1, minWidth: 0 } }, + React.createElement('div', { style: { fontSize: 14, fontWeight: 600, color: '#0f172a', wordBreak: 'break-all' } }, signFileName), + React.createElement('div', { style: { marginTop: 6, fontSize: 12, color: '#64748b', lineHeight: 1.6 } }, + React.createElement('div', null, '签章时间:', record.deliveryTime || '-'), + React.createElement('div', null, '签章方:', record.customerName || '-') + ) + ), + React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8, flexShrink: 0 } }, + React.createElement(Button, { size: 'small', onClick: function () { previewDeliverySignFile(record); } }, '预览'), + React.createElement(Button, { type: 'primary', size: 'small', onClick: function () { downloadDeliverySignFile(record); } }, '下载') + ) + ) + ) : null; + + return React.createElement(React.Fragment, null, + readOnly ? React.createElement('style', null, DV_VIEW_READONLY_CTRL_CSS) : null, + React.createElement(Drawer, { + open: open, + onClose: onClose, + width: Math.min(960, typeof window !== 'undefined' ? window.innerWidth - 24 : 960), + title: drawerTitle, + destroyOnClose: true, + styles: { + body: { padding: '16px 20px 88px', background: '#f8fafc' }, + footer: { borderTop: '1px solid #e2e8f0', padding: '12px 20px' } + }, + footer: readOnly + ? React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end' } }, + React.createElement(Button, { onClick: onClose }, '关闭') + ) + : React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 10 } }, + React.createElement(Button, { onClick: onClose, disabled: submitting }, '取消'), + React.createElement(Button, { onClick: handleSaveClick, disabled: submitting }, '保存'), + React.createElement(Button, { type: 'primary', loading: submitting, onClick: handleSubmitClick }, '提交') + ) + }, + React.createElement('div', { className: readOnly ? 'dv-edit-drawer-view' : undefined }, + summaryCard, + React.createElement('div', { style: { display: 'flex', gap: 16, alignItems: 'flex-start' } }, + sectionNavEl, + React.createElement('div', { style: { flex: 1, minWidth: 0 } }, + sectionBasic, + sectionEquip, + sectionMetrics, + sectionInspection, + sectionPhotos, + sectionEsign + ) + ) + ) + ), + React.createElement(Modal, { + open: !!previewState[0].open, + title: previewState[0].title || '预览', + footer: null, + onCancel: closePhotoPreview, + width: 920, + styles: { body: { padding: '12px 20px 20px' } } + }, + previewState[0].url ? React.createElement('div', null, + React.createElement('div', { style: { textAlign: 'center', background: '#0f172a', borderRadius: 10, padding: 12 } }, + React.createElement('img', { + src: previewState[0].url, + alt: previewState[0].title, + style: { width: '100%', maxHeight: '72vh', objectFit: 'contain' } + }) + ), + previewState[0].gallery && previewState[0].gallery.length > 1 ? React.createElement('div', { + style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, marginTop: 16 } + }, + React.createElement(Button, { onClick: function () { shiftPhotoPreview(-1); } }, '上一张'), + React.createElement('span', { style: { fontSize: 13, color: '#64748b', fontVariantNumeric: 'tabular-nums' } }, + String(previewState[0].index + 1) + ' / ' + previewState[0].gallery.length + ), + React.createElement(Button, { type: 'primary', onClick: function () { shiftPhotoPreview(1); } }, '下一张') + ) : null + ) : null + ), + React.createElement(Modal, { + open: !!ocrModalState[0].open, + title: '备胎识别', + onCancel: function () { ocrModalState[1](Object.assign({}, ocrModalState[0], { open: false })); }, + onOk: function () { updateForm({ spareTireDepth: ocrModalState[0].depth }); ocrModalState[1](Object.assign({}, ocrModalState[0], { open: false })); message.success('已反写胎纹深度'); }, + okText: '确认', + cancelText: '取消' + }, + React.createElement(Input, { value: ocrModalState[0].depth, addonAfter: 'mm', onChange: function (e) { ocrModalState[1](Object.assign({}, ocrModalState[0], { depth: e.target.value })); } }) + ) + ); +} + +if (typeof window !== 'undefined') { + window.DeliveryEditDrawer = DeliveryEditDrawer; +} + + +var DV_REQ_SCROLL_STYLE = { fontSize: 13, color: '#334155', lineHeight: 1.75, maxHeight: '68vh', overflowY: 'auto', paddingRight: 4 }; +var DV_REQ_TH_STYLE = { border: '1px solid #e2e8f0', padding: '8px 10px', textAlign: 'left', background: '#f8fafc', fontWeight: 600 }; +var DV_REQ_TD_STYLE = { border: '1px solid #e2e8f0', padding: '8px 10px', verticalAlign: 'top' }; + +function dvReqTitle(text) { + return React.createElement('p', { style: { margin: '0 0 8px', fontWeight: 700, color: '#0f172a', fontSize: 14 } }, text); +} + +function dvReqSubtitle(text) { + return React.createElement('p', { style: { margin: '14px 0 6px', fontWeight: 600, color: '#334155' } }, text); +} + +function dvReqHint(text) { + return React.createElement('p', { style: { margin: '4px 0 8px', color: '#64748b', fontSize: 12 } }, text); +} + +function dvReqStrong(label, text) { + return React.createElement(React.Fragment, null, React.createElement('strong', null, label), text); +} + +function dvReqUl(items) { + return React.createElement('ul', { style: { paddingLeft: 20, margin: '6px 0 14px' } }, + items.map(function (item, idx) { + return React.createElement('li', { key: idx, style: { marginBottom: 4 } }, item); + }) + ); +} + +function dvReqOl(items) { + return React.createElement('ol', { style: { paddingLeft: 20, margin: '6px 0 14px' } }, + items.map(function (item, idx) { + return React.createElement('li', { key: idx, style: { marginBottom: 4 } }, item); + }) + ); +} + +function dvReqTable(headers, rows) { + return React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 12, margin: '8px 0 14px' } }, + React.createElement('thead', null, + React.createElement('tr', null, + headers.map(function (h, i) { + return React.createElement('th', { key: i, style: DV_REQ_TH_STYLE }, h); + }) + ) + ), + React.createElement('tbody', null, + rows.map(function (row, ri) { + return React.createElement('tr', { key: ri }, + row.map(function (cell, ci) { + return React.createElement('td', { key: ci, style: DV_REQ_TD_STYLE }, cell); + }) + ); + }) + ) + ); +} + +function renderDeliveryRequirementPrdTab(Alert) { + return React.createElement('div', { style: DV_REQ_SCROLL_STYLE }, + React.createElement(Alert, { + type: 'info', + showIcon: true, + style: { marginBottom: 14, borderRadius: 10 }, + message: '模块定位', + description: '交车管理面向运维人员,按「单车一行」跟进交车任务进度。运维在列表抽屉内完成交车单录入与提交;客户被授权人通过 E 签宝完成签章后,交车流程完结。本文从产品经理视角描述业务规则与页面行为,不涉及表结构、接口字段及实现细节。' + }), + + dvReqTitle('一、业务对象说明'), + dvReqUl([ + dvReqStrong('交车任务:', '一次交车业务单元,通常对应一份车辆租赁合同或替换车场景,可包含多台车。'), + dvReqStrong('交车单(单车):', '交车任务下某一台车的交车记录,列表以「一车一行」展示。'), + dvReqStrong('运维交车:', '运维人员现场验车、拍照、录入里程/电量/氢量等,保存或提交交车单。'), + dvReqStrong('客户签章:', '运维提交后,客户侧被授权人在 E 签宝完成电子签章,状态由「待客户签章」变为「客户已签章」。'), + dvReqStrong('被授权人:', '取自关联租赁合同的被授权人信息;本模块只读展示姓名与手机号,供运维跟进签章。') + ]), + + dvReqTitle('二、页面结构'), + dvReqUl([ + '面包屑:运维管理 / 车辆业务 / 交车管理', + '右上角「查看需求说明」:打开本说明文档', + '页面自上而下:筛选区 → KPI 统计卡片 → 列表区(含导出)' + ]), + + dvReqTitle('三、数据统计(KPI 卡片)'), + dvReqHint('三张可点击卡片替代原 Tab,统计范围均为「当前筛选条件命中后的全部车辆行」,不受分页影响。'), + dvReqTable( + ['卡片', '统计口径', '默认'], + [ + ['全部交车任务', '进行中 + 已完成', '—'], + ['进行中的交车任务', '交车状态为「未开始」「已保存」「待客户签章」', React.createElement('strong', null, '默认选中')], + ['已完成的交车任务', '交车状态为「客户已签章」', '—'] + ] + ), + dvReqUl([ + '卡片右上角问号:悬停展示指标说明', + '点击卡片切换列表数据,选中卡片高亮' + ]), + + dvReqTitle('四、筛选区'), + dvReqSubtitle('4.1 通用规则'), + dvReqUl([ + '默认展示首行 4 项;点击「展开」显示全部 16 项,「收起」恢复默认', + '多条件之间为「且」关系;修改筛选项后须点击「搜索」才生效', + '「重置」清空全部筛选并恢复默认' + ]), + dvReqSubtitle('4.2 车辆批量筛选(第一项)'), + dvReqHint('交互体验与「保险采购 · 比价单选车」一致。'), + dvReqOl([ + '点击输入框弹出批量录入面板,支持车牌号、车辆识别代码(VIN)', + '每行一条;可从 Excel 等批量复制粘贴;同一行内亦可用逗号、顿号、分号分隔', + '面板内「确定」仅保存录入内容;须再点筛选区「搜索」后列表才刷新', + '面板内「清空」或触发器一键清除:立即清除该条件并刷新列表', + '已生效时触发器展示「已选 N 辆车」', + dvReqStrong('匹配规则:', '命中任一输入值即展示(OR);车牌/VIN 均支持精确或包含匹配') + ]), + dvReqSubtitle('4.3 其余筛选项'), + dvReqUl([ + '合同编号、项目名称、客户名称:可搜索下拉', + '交车区域:省-市二级联动', + '完成交车时间:日期段,精确至天', + '交车人、车辆识别代码、车辆类型、品牌、型号', + '业务部门、业务负责人、任务来源、业务类型、是否延期', + dvReqStrong('说明:', '「车辆」批量筛选与「车辆识别代码」单项筛选可同时使用,结果为且关系') + ]), + + dvReqTitle('五、列表(一车一行)'), + dvReqSubtitle('5.1 数据粒度'), + dvReqUl([ + '一个交车任务含多车时,按车辆拆分为独立行展示', + '列表数据范围受运维人员区域权限约束(详见第十二节)' + ]), + dvReqSubtitle('5.2 列说明'), + dvReqTable( + ['列', '展示与交互'], + [ + ['车辆信息', '三行:车牌号、品牌-型号、VIN;车牌未选时橙色「车牌待选」,VIN 显示 -;点击车牌号打开查看抽屉'], + ['合同信息', '客户名称 + 业务类型 Tag、合同编号(可跳转)、项目名称(可跳转)'], + ['业务负责人', '业务部门、业务负责人(两行)'], + ['任务来源', '实心 Tag;来源为「替换车」时悬停展示 旧车 → 新车'], + ['交车地点', '第一行交车区域(省-市),第二行停车场(车牌待选时显示 -)'], + ['交车状态', '详见第六节'], + ['完成交车时间', '单行展示'], + ['交车人', '单行展示'], + ['是否归还', '未交车显示 -;已交车未还车显示「未归还」;已还车显示「已归还」且悬停展示还车时间、还车人'], + ['交车记录', '交车里程(km)、交车氢量(%或MPa)、交车电量(%) 三行合并'], + ['创建时间 / 创建人', '交车任务维度字段'], + ['操作', '「查看」始终可用;「编辑」仅「未开始」「已保存」时展示'] + ] + ), + dvReqSubtitle('5.3 其他规则'), + dvReqUl([ + '合同信息列宽支持拖拽调整', + '分页:默认 10 条/页,可选 10 / 20 / 50' + ]), + + dvReqTitle('六、交车状态与客户签章'), + dvReqSubtitle('6.1 状态定义'), + dvReqTable( + ['状态', '含义', 'KPI 归属'], + [ + ['未开始', '交车单尚未保存有效数据', '进行中'], + ['已保存', '运维已保存但未正式提交', '进行中'], + ['待客户签章', '运维已提交,等待客户被授权人 E 签宝签章', '进行中'], + ['客户已签章', '客户被授权人已完成签章,流程完结', '已完成'] + ] + ), + dvReqSubtitle('6.2 Tag 视觉与交互规则'), + dvReqUl([ + dvReqStrong('统一视觉:', '「待客户签章」与「客户已签章」使用相同 Tag 样式(蓝色实心),不做颜色或下划线区分'), + dvReqStrong('待客户签章:', '悬停 Popover 展示租赁合同被授权人姓名、手机号;多人逐条展示;无数据显示 -'), + dvReqStrong('客户已签章:', '点击 Tag 下载 E 签宝签章文件;文件名建议含交车单标识、车辆序号、车牌号'), + '抽屉摘要卡中的状态 Tag 遵循与列表相同的视觉与交互规则' + ]), + dvReqSubtitle('6.3 被授权人数据来源'), + dvReqUl([ + '取自交车任务关联的车辆租赁合同「被授权人信息」', + '交车管理侧只读展示,不在本模块维护被授权人', + '已提交/已签章交车单以提交当时合同快照为准(具体以业务归档规则为准)' + ]), + + dvReqTitle('七、交车状态流转'), + React.createElement('div', { style: { padding: '10px 14px', background: '#f8fafc', borderRadius: 8, border: '1px solid #e2e8f0', margin: '8px 0 14px', fontSize: 13, fontWeight: 600, color: '#334155' } }, + '未开始 → 已保存(保存)→ 待客户签章(提交)→ 客户已签章(客户 E 签宝完成)' + ), + dvReqUl([ + '仅「未开始」「已保存」可编辑;任意状态均可查看' + ]), + + dvReqTitle('八、编辑交车单(抽屉)'), + dvReqSubtitle('8.1 入口与布局'), + dvReqUl([ + '入口:列表操作列「编辑」(仅未开始/已保存)', + '顶部摘要卡:客户名称、合同编号、交车状态、项目、交车地点、品牌型号、任务来源', + '左侧目录锚点:交车车辆 · 车辆信息 · 交车数据 · 交车检查单 · 交车照片', + '底部操作:取消、保存、提交' + ]), + dvReqSubtitle('8.2 交车车辆'), + dvReqUl([ + '车牌号(必填):可搜索选择停车场内「已备车」车辆(详见第十二节)', + '车牌待选时 VIN 显示 -;车辆类型/品牌/型号/VIN 只读,随车牌联动' + ]), + dvReqSubtitle('8.3 车辆信息'), + dvReqHint('备车数据同步 — 打开抽屉或选择车牌后自动同步,页面不单独提示。'), + dvReqUl([ + '同步:车身广告及照片、尾板、备胎照片/胎纹深度、车辆基础信息', + '不同步:驾驶培训相关(提车码、司机证照等),须在交车环节单独上传识别', + '车身广告开关与备车「后装设备-车身广告」双向同步', + '车身广告及放大字(开关必填;开启后各 1 张照片必填)', + '尾板(开关必填);备胎照片/胎纹深度(选填,支持 OCR)', + '驾驶培训(必填):上传提车码 → 识别成功后加载司机证照;失败提示重新选择' + ]), + dvReqSubtitle('8.4 交车数据'), + dvReqUl([ + '交车里程(必填,km)、交车电量(必填,%)、交车氢量(必填,% 或 MPa)', + '送车服务费(选填,元)', + dvReqStrong('里程自动取值:', '优先车机当前里程 → 其次最近一次异动/调拨/还车结束里程 → 均不可用则手工录入') + ]), + dvReqSubtitle('8.5 交车检查单'), + dvReqUl([ + '列:类别(同类合并)、检查项目、检查情况、备注', + '覆盖车灯、仪表盘、驾驶室、轮胎、液位、外观、随车工具/证件、燃料电池、冷机、制动等' + ]), + dvReqSubtitle('8.6 交车照片'), + dvReqUl([ + '分模块:车辆、底盘、轮胎、瑕疵、其他', + '车辆/底盘/轮胎各 6 个必传点位;瑕疵、其他不限张数', + '单张:jpg/jpeg/png/gif/webp,不超过 5MB;支持预览、删除' + ]), + dvReqSubtitle('8.7 保存与提交校验'), + dvReqTable( + ['业务动作', '须满足的条件'], + [ + [React.createElement('strong', null, '保存'), React.createElement('ul', { style: { margin: 0, paddingLeft: 18 } }, + React.createElement('li', null, '通过当前已填字段校验'), + React.createElement('li', null, '状态变为「已保存」,抽屉不关闭') + )], + [React.createElement('strong', null, '提交'), React.createElement('ul', { style: { margin: 0, paddingLeft: 18 } }, + React.createElement('li', null, '车牌已选择'), + React.createElement('li', null, '车身广告(若开启)及放大字照片已上传'), + React.createElement('li', null, '提车码识别成功,驾驶培训证照齐全'), + React.createElement('li', null, '交车里程、电量、氢量已填写'), + React.createElement('li', null, '车辆/底盘/轮胎全部必传照片点已上传'), + React.createElement('li', null, '二次确认后状态变为「待客户签章」,写入完成交车时间/交车人,关闭抽屉') + )] + ] + ), + + dvReqTitle('九、查看交车单(抽屉)'), + dvReqUl([ + '入口:列表点击车牌号,或操作列「查看」', + '布局与编辑页一致;全部只读,底部仅「关闭」', + '「客户已签章」时额外展示「E签宝签章文件」卡片:文件名、签章时间、签章方;支持预览与下载', + '已提交交车单展示完整交车照片;驾驶培训区展示状态与证照图片' + ]), + + dvReqTitle('十、导出'), + dvReqUl([ + '范围:当前 KPI 卡片选中项 + 全部筛选条件(含车辆批量筛选)下的全部命中行,不受分页限制', + '导出列与业务导出样表一致;交车里程、氢量、电量为独立三列(非列表合并列)', + '列表区展示当前 KPI 名称及导出范围说明' + ]), + + dvReqTitle('十一、指标单位约定'), + dvReqUl([ + '交车里程:km', + '交车电量:%', + '交车氢量:% 或 MPa(按车辆/合同配置)', + '列表「交车记录」合并展示;导出仍为三列' + ]), + + dvReqTitle('十二、区域权限与车牌选择'), + dvReqSubtitle('12.1 交车任务可见范围'), + dvReqUl([ + '以交车单「交车区域」(省-市)为任务所属区域', + '运维人员须具备该区域或其上级区域权限,方可查看并执行', + '示例:交车区域「浙江省-嘉兴市」→ 具备「浙江省」或「嘉兴市」权限可见;仅「杭州市」权限不可见', + '无权限时整条交车任务(含其下所有车辆行)均不可见' + ]), + dvReqSubtitle('12.2 停车场车牌可选范围'), + dvReqUl([ + '下拉仅展示车辆状态为「已备车」的车辆', + '在「已备车」前提下,停车场区域须落在当前运维权限覆盖范围内', + '省级权限:可选该省各停车场内已备车车辆;市级权限:仅可选该市', + '下拉建议展示:车牌号 + 停车场名称' + ]), + + dvReqTitle('十三、关联模块与数据依赖'), + dvReqTable( + ['关联模块', '提供的数据 / 能力'], + [ + ['车辆租赁合同', '合同编号、客户、项目、交车区域/地点、被授权人信息'], + ['备车管理', '已备车车辆、车身广告、备胎等同步数据'], + ['提车码 / 司机培训', '驾驶培训识别与证照数据'], + ['E 签宝', '客户签章状态与签章文件'], + ['合同管理', '列表点击合同编号/项目名称跳转详情'], + ['替换车', '任务来源为「替换车」时展示旧车 → 新车对照'] + ] + ) + ); +} + +function renderDeliveryRequirementManualTab(Alert, Table) { + return React.createElement('div', { style: Object.assign({}, DV_REQ_SCROLL_STYLE, { maxHeight: '62vh' }) }, + React.createElement(Alert, { + type: 'success', + showIcon: true, + style: { marginBottom: 14, borderRadius: 10 }, + message: '交车全流程(简版)', + description: '选车 → 录入交车数据 → 拍照 → 保存/提交 → 跟进客户签章。运维侧以「提交」为节点,客户签章在 E 签宝侧完成。' + }), + React.createElement(Table, { + size: 'small', + pagination: false, + bordered: true, + style: { marginBottom: 14 }, + columns: [ + { title: '步骤', dataIndex: 'step', width: 56 }, + { title: '操作', dataIndex: 'action' }, + { title: '要点', dataIndex: 'tip', width: 220 } + ], + dataSource: [ + { key: '1', step: '①', action: '筛选定位车辆', tip: 'KPI 卡片切换进行中/已完成;车辆批量筛选粘贴车牌' }, + { key: '2', step: '②', action: '编辑交车单', tip: '仅「未开始/已保存」可编辑;选择已备车车牌' }, + { key: '3', step: '③', action: '填写车辆信息', tip: '广告/尾板/备胎;上传提车码完成驾驶培训' }, + { key: '4', step: '④', action: '录入交车数据', tip: '里程/电量/氢量必填;里程可自动带入后修改' }, + { key: '5', step: '⑤', action: '完成检查单与拍照', tip: '检查单逐项确认;车辆/底盘/轮胎 18 点位必传' }, + { key: '6', step: '⑥', action: '保存或提交', tip: '保存→已保存;提交→待客户签章' }, + { key: '7', step: '⑦', action: '跟进客户签章', tip: '悬停「待客户签章」查看被授权人联系方式' }, + { key: '8', step: '⑧', action: '签章完成', tip: '状态变「客户已签章」;点击 Tag 下载签章文件' } + ] + }), + dvReqTitle('提交前自查'), + dvReqUl([ + '车牌已从「已备车」列表中选择', + '提车码识别成功,司机证照已加载', + '交车里程、电量、氢量均已填写', + '车辆 / 底盘 / 轮胎全部必传照片点已上传', + '车身广告若开启,广告图与放大字图均已上传' + ]), + dvReqTitle('常见情况'), + dvReqUl([ + '车牌待选:VIN 不展示,交车地点停车场行显示 -', + '待客户签章:运维不可再编辑,可悬停查看被授权人手机号跟进', + '客户已签章:归入 KPI「已完成」,可下载签章 PDF', + '替换车任务:任务来源列悬停可查看旧车 → 新车对照', + '导出:与当前 KPI + 筛选条件一致,不受分页限制' + ]) + ); +} + +function createDeliveryRequirementModalItems(antd) { + var Alert = antd.Alert; + var Table = antd.Table; + return [ + { + key: 'prd', + label: '需求说明', + children: renderDeliveryRequirementPrdTab(Alert) + }, + { + key: 'manual', + label: '操作手册', + children: renderDeliveryRequirementManualTab(Alert, Table) + } + ]; +} + +const Component = function () { + var useState = React.useState; + var useCallback = React.useCallback; + var useMemo = React.useMemo; + var useEffect = React.useEffect; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var DatePicker = antd.DatePicker; + var Select = antd.Select; + var Button = antd.Button; + var Table = antd.Table; + var Cascader = antd.Cascader; + var Tag = antd.Tag; + var Tooltip = antd.Tooltip; + var Popover = antd.Popover; + var Modal = antd.Modal; + var Tabs = antd.Tabs; + var message = antd.message; + var Input = antd.Input; + + var RangePicker = DatePicker.RangePicker; + + function parseMultiPlates(text) { + var raw = (text || '').trim(); + if (!raw) return []; + var lines = raw.split(/\r?\n/).map(function (line) { return line.trim(); }).filter(Boolean); + var expanded = []; + lines.forEach(function (line) { + if (/[,,、;;]/.test(line)) { + line.split(/[,,、;;]+/).forEach(function (s) { + var t = s.trim(); + if (t) expanded.push(t); + }); + } else { + expanded.push(line); + } + }); + var seen = {}; + var out = []; + expanded.forEach(function (s) { + var key = s.toUpperCase(); + if (!seen[key]) { + seen[key] = true; + out.push(key); + } + }); + return out; + } + + function rowMatchesVehicleFilter(row, vehicleText) { + var tokens = parseMultiPlates(vehicleText); + if (!tokens.length) return true; + var plateRaw = (row.plateNo || '').trim(); + var plate = (!plateRaw || plateRaw === '-') ? '' : plateRaw.toUpperCase(); + var vin = (row.vin || '').trim().toUpperCase(); + for (var i = 0; i < tokens.length; i++) { + var token = tokens[i]; + if (plate && (plate === token || plate.indexOf(token) !== -1)) return true; + if (vin && (vin === token || vin.indexOf(token) !== -1)) return true; + } + return false; + } + + function createEmptyFilters() { + return { + contractCode: undefined, + projectName: undefined, + customerName: undefined, + deliveryRegion: undefined, + dateStart: '', + dateEnd: '', + deliveryPerson: undefined, + plateNos: '', + vin: undefined, + vehicleType: undefined, + brand: undefined, + model: undefined, + businessDept: undefined, + businessOwner: undefined, + taskSource: undefined, + bizType: undefined, + isDelayed: undefined + }; + } + + function patchFilters(prev, patch) { + var next = {}; + var k; + for (k in prev) next[k] = prev[k]; + for (k in patch) next[k] = patch[k]; + return next; + } + + var filterState = useState(createEmptyFilters); + var filters = filterState[0]; + var setFilters = filterState[1]; + var appliedFilterState = useState(createEmptyFilters); + var appliedFilters = appliedFilterState[0]; + var setAppliedFilters = appliedFilterState[1]; + var filterExpandedState = useState(false); + var filterExpanded = filterExpandedState[0]; + var setFilterExpanded = filterExpandedState[1]; + var multiPlateOpenState = useState(false); + var multiPlateOpen = multiPlateOpenState[0]; + var setMultiPlateOpen = multiPlateOpenState[1]; + var multiPlateDraftState = useState(''); + var multiPlateDraft = multiPlateDraftState[0]; + var setMultiPlateDraft = multiPlateDraftState[1]; + + /** total | inProgress | completed */ + var kpiFilterState = useState('inProgress'); + var kpiFilter = kpiFilterState[0]; + var setKpiFilter = kpiFilterState[1]; + var pageState = useState(1); + var pageSizeState = useState(10); + var colWidthsState = useState({ contractInfo: 240 }); + var colWidths = colWidthsState[0]; + var setColWidths = colWidthsState[1]; + var requirementModalOpen = useState(false); + var setRequirementModalOpen = requirementModalOpen[1]; + + var requirementModalItems = useMemo(function () { + return createDeliveryRequirementModalItems(antd); + }, []); + + // 交车区域:省-市 二级 + var regionOptions = [ + { value: 'zhejiang', label: '浙江省', children: [{ value: 'hangzhou', label: '杭州市' }, { value: 'jiaxing', label: '嘉兴市' }, { value: 'ningbo', label: '宁波市' }] }, + { value: 'shanghai', label: '上海市', children: [{ value: 'shanghai', label: '上海市' }] }, + { value: 'guangdong', label: '广东省', children: [{ value: 'guangzhou', label: '广州市' }, { value: 'shenzhen', label: '深圳市' }] } + ]; + + var contractCodeOptions = [ + { value: 'LNZLHT', label: 'LNZLHT' }, + { value: 'HT-ZL-2024', label: 'HT-ZL-2024' }, + { value: 'HT-ZL-2025', label: 'HT-ZL-2025' } + ]; + var projectNameOptions = [ + { value: 'p1', label: '桐乡韵达租赁4.5T*10' }, + { value: 'p2', label: '洛安供应链-租赁帕力安4.5T*30' }, + { value: 'p3', label: '嘉兴氢能示范项目' }, + { value: 'p4', label: '杭州城配租赁项目' } + ]; + var customerNameOptions = [ + { value: 'c1', label: '桐乡市丰韵快递有限责任公司' }, + { value: 'c2', label: '武汉洛安供应链有限公司' }, + { value: 'c3', label: '嘉兴某某物流有限公司' }, + { value: 'c4', label: '杭州某某租赁有限公司' } + ]; + var deliveryPersonOptions = [ + { value: '张三', label: '张三' }, + { value: '李四', label: '李四' }, + { value: '王五', label: '王五' }, + { value: '魏山', label: '魏山' }, + { value: '何苗苗', label: '何苗苗' } + ]; + var taskSourceOptions = [ + { value: '替换车', label: '替换车' }, + { value: '交车任务', label: '交车任务' } + ]; + var bizTypeOptions = [ + { value: '租赁', label: '租赁' }, + { value: '自营', label: '自营' } + ]; + var isDelayedOptions = [ + { value: 'yes', label: '是' }, + { value: 'no', label: '否' } + ]; + + var DV_ORDER_KEY = 'oneos_delivery_order_id'; + var DV_VEHICLE_KEY = 'oneos_delivery_vehicle_key'; + var DV_NAV_KEY = 'oneos_delivery_navigate_target'; + var DV_CONTRACT_CODE_KEY = 'oneos_contract_code'; + var DV_LIST_LINK_STYLE = { color: '#165dff', cursor: 'pointer' }; + + /** 交车状态:进行中=未开始/已保存/待客户签章;历史=客户已签章(运维+客户 E 签宝均完成) */ + var DELIVERY_STATUS_HISTORY = '客户已签章'; + var DELIVERY_STATUS_IN_PROGRESS = ['未开始', '已保存', '待客户签章']; + + function isDeliveryHistoryStatus(status) { + return status === DELIVERY_STATUS_HISTORY || status === '已签章'; + } + + function isDeliveryInProgressStatus(status) { + return DELIVERY_STATUS_IN_PROGRESS.indexOf(status || '未开始') >= 0; + } + + /** 运维侧已完成交车提交(待签章或已签章)视为车辆已交车 */ + function isVehicleDelivered(status) { + return status === '待客户签章' || isDeliveryHistoryStatus(status); + } + + function canEditDeliveryRow(record) { + var s = record.deliveryStatus; + return s === '未开始' || s === '已保存'; + } + + // 交车任务(一单多车);列表按车辆拆分为独立行,表头对齐导出样表 + var deliveryOrdersState = useState([ + { + id: 'o1', expectedDate: '2025-02-28 至 2025-03-05', contractCode: 'LNZLHT 20260104001', projectName: '桐乡韵达租赁4.5T*10', customerName: '桐乡市丰韵快递有限责任公司', businessDept: '业务二部', businessOwner: '刘念念', taskSource: '替换车', replaceOldPlate: '浙A88601F', bizType: '租赁', deliveryRegion: '浙江省-嘉兴市', deliveryAddress: '平湖指定停车场', createTime: '2026-06-04 11:28', createBy: '赵小峰', + authorizedList: [{ name: '周授权', phone: '13805731234', idCard: '330402199001011234' }], + vehicleList: [ + { vehicleKey: 1, seq: 1, vehicleType: '4.5吨冷链车', brand: '现代', model: '帕力安牌4.5吨冷链车', vin: 'LNBSCPKB8RR123401', replaceOldPlate: '浙A88601F', plateNo: '', deliveryTime: '', deliveryPerson: '', deliveryStatus: '未开始', deliveryMileage: null, deliveryH2: null, deliveryElec: null }, + { vehicleKey: 2, seq: 2, vehicleType: '4.5吨冷链车', brand: '现代', model: '帕力安牌4.5吨冷链车', vin: 'LNBSCPKB8RR123402', replaceOldPlate: '浙A88602F', plateNo: '', deliveryTime: '', deliveryPerson: '', deliveryStatus: '未开始', deliveryMileage: null, deliveryH2: null, deliveryElec: null } + ] + }, + { + id: 'o2', expectedDate: '2025-03-01', contractCode: 'LNZLHT2026040301-042', projectName: '洛安供应链-租赁帕力安4.5T*30', customerName: '武汉洛安供应链有限公司', businessDept: '业务三部', businessOwner: '金可鹏', taskSource: '交车任务', bizType: '租赁', deliveryRegion: '四川省-成都市', deliveryAddress: '成都龙泉驿停车场', createTime: '2026-05-31 14:07', createBy: '何苗苗', + authorizedList: [{ name: '陈明', phone: '13800138088', idCard: '420102199002022345' }], + vehicleList: [ + { vehicleKey: 1, seq: 1, vehicleType: '4.5吨冷链车', brand: '现代', model: '帕力安牌4.5吨冷链车', vin: 'LNBSCPKB9RR223401', plateNo: '粤AGP9827', deliveryTime: '2026-06-03 18:20', deliveryPerson: '魏山', deliveryStatus: '待客户签章', deliveryMileage: 48202, deliveryH2: 19, deliveryH2Unit: '%', deliveryElec: 82 }, + { vehicleKey: 2, seq: 2, vehicleType: '4.5吨冷链车', brand: '现代', model: '帕力安牌4.5吨冷链车', vin: 'LNBSCPKB9RR223402', plateNo: '粤AGP4598', deliveryTime: '2026-06-02 11:00', deliveryPerson: '魏山', deliveryStatus: '已保存', deliveryMileage: null, deliveryH2: null, deliveryElec: null } + ] + }, + { + id: 'o3', expectedDate: '2025-03-08', contractCode: 'LNZLHT2025042201', projectName: '炽瑞-租赁现代4.5T', customerName: '东莞沙田炽瑞物流有限公司', businessDept: '业务三部', businessOwner: '金可鹏', taskSource: '替换车', replaceOldPlate: '粤B58888F', bizType: '租赁', deliveryRegion: '广东省-广州市', deliveryAddress: '广州南沙物流园停车场', createTime: '2026-05-28 20:30', createBy: '童军林', + authorizedList: [{ name: '黄签章', phone: '13600136066', idCard: '441900199003033456' }], + vehicleList: [ + { vehicleKey: 1, seq: 1, vehicleType: '4.5吨货车', brand: '现代', model: '4.5吨货车', vin: 'LNBSCPKB7RR323401', replaceOldPlate: '粤B58888F', plateNo: '', deliveryTime: '', deliveryPerson: '', deliveryStatus: '未开始', deliveryMileage: null, deliveryH2: null, deliveryElec: null } + ] + }, + { + id: 'o4', expectedDate: '2024-11-15', contractCode: 'LNZLHT2024111401', projectName: '聚德11月新增苏龙18T*2', customerName: '沈阳聚德物流有限公司', businessDept: '业务三部', businessOwner: '金可鹏', taskSource: '交车任务', bizType: '租赁', deliveryRegion: '浙江省-嘉兴市', deliveryAddress: '嘉兴港区氢能停车场', createTime: '2024-11-15 15:05', createBy: '何苗苗', + authorizedList: [ + { name: '李签章', phone: '13900139099', idCard: '210102199004044567' }, + { name: '王签章', phone: '13700137077', idCard: '210103199005055678' } + ], + vehicleList: [ + { vehicleKey: 1, seq: 1, vehicleType: '18吨双飞翼货车', brand: '苏龙', model: '海格牌18吨双飞翼货车', vin: 'LKLG7C4E4NA774701', plateNo: '浙F80088', deliveryTime: '2026-06-02 16:00', deliveryPerson: '魏山', deliveryStatus: '待客户签章', deliveryMileage: 46200, deliveryH2: 21, deliveryH2Unit: '%', deliveryElec: 80 }, + { vehicleKey: 2, seq: 2, vehicleType: '18吨双飞翼货车', brand: '苏龙', model: '海格牌18吨双飞翼货车', vin: 'LKLG7C4E4NA774702', plateNo: '沪A03802F', deliveryTime: '2025-11-20 09:30', deliveryPerson: '何苗苗', deliveryStatus: '已保存', deliveryMileage: null, deliveryH2: null, deliveryElec: null } + ] + }, + { + id: 'o5', expectedDate: '2025-02-15', contractCode: 'HT-ZL-2024-001', projectName: '嘉兴氢能示范项目', customerName: '嘉兴某某物流有限公司', businessDept: '业务一部', businessOwner: '张经理', taskSource: '交车任务', bizType: '租赁', deliveryRegion: '浙江省-嘉兴市', deliveryAddress: '南湖科技大道停车场', createTime: '2025-02-10 09:00', createBy: '系统', + authorizedList: [{ name: '张授权', phone: '13500135055', idCard: '330402199006066789' }], + vehicleList: [ + { vehicleKey: 1, seq: 1, vehicleType: '厢式货车', brand: '东风', model: 'DFH1180', vin: 'LKLG7C4E4NA774759', plateNo: '京A12345', deliveryTime: '2025-02-15 10:30', deliveryPerson: '张三', deliveryStatus: '客户已签章', deliveryMileage: 12580, deliveryH2: 35, deliveryH2Unit: 'MPa', deliveryElec: 45, vehicleReturned: true, returnTime: '2025-08-20 11:30', returnPerson: '王五' }, + { vehicleKey: 2, seq: 2, vehicleType: '厢式货车', brand: '福田', model: 'BJ1180', vin: 'LKLG7C4E4NA774760', plateNo: '京C11111', deliveryTime: '2025-02-15 14:00', deliveryPerson: '李四', deliveryStatus: '客户已签章', deliveryMileage: 13200, deliveryH2: 68, deliveryH2Unit: '%', deliveryElec: 38, vehicleReturned: false } + ] + }, + { + id: 'o6', expectedDate: '2025-02-18', contractCode: 'HT-ZL-2024-003', projectName: '杭州城配租赁项目', customerName: '杭州某某租赁有限公司', businessDept: '业务二部', businessOwner: '李经理', taskSource: '交车任务', bizType: '租赁', deliveryRegion: '浙江省-杭州市', deliveryAddress: '未来科技城地下停车场', createTime: '2025-02-12 08:30', createBy: '李四', + authorizedList: [{ name: '赵授权', phone: '13300133033', idCard: '330106199007077890' }], + vehicleList: [ + { vehicleKey: 1, seq: 1, vehicleType: '城配货车', brand: '重汽', model: 'ZZ1180', vin: 'LKLG7C4E4NA774801', plateNo: '浙A10001', deliveryTime: '2025-02-18 09:15', deliveryPerson: '张三', deliveryStatus: '客户已签章', deliveryMileage: 9800, deliveryH2: 10.8, deliveryH2Unit: '%', deliveryElec: 52, vehicleReturned: true, returnTime: '2026-05-12 09:20', returnPerson: '张三' }, + { vehicleKey: 2, seq: 2, vehicleType: '城配货车', brand: '东风', model: 'DFH1190', vin: 'LKLG7C4E4NA774802', plateNo: '浙A10002', deliveryTime: '2025-02-18 11:40', deliveryPerson: '李四', deliveryStatus: '客户已签章', deliveryMileage: 10120, deliveryH2: 9.6, deliveryH2Unit: '%', deliveryElec: 48, vehicleReturned: false }, + { vehicleKey: 3, seq: 3, vehicleType: '城配货车', brand: '福田', model: 'BJ1190', vin: 'LKLG7C4E4NA774803', plateNo: '浙A10003', deliveryTime: '2025-02-18 16:20', deliveryPerson: '王五', deliveryStatus: '客户已签章', deliveryMileage: 11500, deliveryH2: 32, deliveryH2Unit: 'MPa', deliveryElec: 55, vehicleReturned: false } + ] + } + ]); + var deliveryOrders = deliveryOrdersState[0]; + var setDeliveryOrders = deliveryOrdersState[1]; + + var editDrawerState = useState({ open: false, record: null, mode: 'edit' }); + var editDrawer = editDrawerState[0]; + var setEditDrawer = editDrawerState[1]; + + function resolveVehicleReturnedFields(deliveryStatus, v) { + if (!isVehicleDelivered(deliveryStatus || '未开始')) { + return { vehicleReturned: false, returnTime: '', returnPerson: '' }; + } + if (!isDeliveryHistoryStatus(deliveryStatus || '未开始')) { + return { vehicleReturned: false, returnTime: '', returnPerson: '' }; + } + return { + vehicleReturned: v.vehicleReturned === true || !!(v.returnTime && String(v.returnTime).trim()), + returnTime: v.returnTime || '', + returnPerson: (v.returnPerson && v.returnPerson !== '-') ? String(v.returnPerson).trim() : '' + }; + } + + function expandOrdersToVehicleRows(orders) { + var rows = []; + (orders || []).forEach(function (order) { + (order.vehicleList || []).forEach(function (v, idx) { + var vk = v.vehicleKey != null ? v.vehicleKey : idx + 1; + var deliveryStatus = v.deliveryStatus || '未开始'; + var returnFields = resolveVehicleReturnedFields(deliveryStatus, v); + rows.push({ + id: order.id + '_v' + vk, + orderId: order.id, + vehicleKey: vk, + seq: v.seq != null ? v.seq : idx + 1, + deliveryTime: v.deliveryTime || v.actualDate || '', + deliveryPerson: (v.deliveryPerson && v.deliveryPerson !== '-') ? v.deliveryPerson : '', + plateNo: (v.plateNo && v.plateNo !== '-') ? String(v.plateNo).trim() : '', + vin: (v.plateNo && v.plateNo !== '-' && String(v.plateNo).trim()) ? (v.vin || '') : '', + vehicleType: v.vehicleType || '', + brand: v.brand || '', + model: v.model || '', + contractCode: order.contractCode, + customerName: order.customerName, + projectName: order.projectName, + businessDept: order.businessDept || '-', + businessOwner: order.businessOwner || '-', + taskSource: order.taskSource || '-', + replaceOldPlate: (v.replaceOldPlate || order.replaceOldPlate || '').trim(), + bizType: order.bizType || '-', + deliveryRegion: order.deliveryRegion || '-', + deliveryAddress: order.deliveryAddress || '-', + deliveryStatus: deliveryStatus, + deliveryMileage: v.deliveryMileage, + deliveryH2: v.deliveryH2, + deliveryH2Unit: v.deliveryH2Unit === 'MPa' ? 'MPa' : '%', + deliveryElec: v.deliveryElec, + createTime: order.createTime, + createBy: order.createBy, + expectedDate: order.expectedDate, + authorizedList: Array.isArray(order.authorizedList) ? order.authorizedList.slice() : [], + vehicleReturned: returnFields.vehicleReturned, + returnTime: returnFields.returnTime, + returnPerson: returnFields.returnPerson + }); + }); + }); + return rows; + } + + function formatDeliveryMileage(v) { + if (v === null || v === undefined || v === '') return '-'; + return String(v) + ' km'; + } + + function formatDeliveryH2(v, unit) { + if (v === null || v === undefined || v === '') return '-'; + var u = unit === 'MPa' ? 'MPa' : '%'; + return String(v) + ' ' + u; + } + + function formatDeliveryElec(v) { + if (v === null || v === undefined || v === '') return '-'; + return String(v) + ' %'; + } + + function renderDeliveryRecordCell(record) { + return React.createElement('div', null, + React.createElement('div', { style: cellLineSubStyle }, '里程 ', formatDeliveryMileage(record.deliveryMileage)), + React.createElement('div', { style: cellLineSubStyle }, '氢量 ', formatDeliveryH2(record.deliveryH2, record.deliveryH2Unit)), + React.createElement('div', { style: cellLineSubStyle }, '电量 ', formatDeliveryElec(record.deliveryElec)) + ); + } + + function rowDateKey(row) { + var t = row.deliveryTime || ''; + if (t && t.length >= 10) return t.slice(0, 10); + return (row.createTime || '').slice(0, 10); + } + + function parseExpectedEndDate(expectedDate) { + if (!expectedDate) return ''; + var s = String(expectedDate).trim(); + if (s.indexOf('至') >= 0) { + var seg = s.split('至'); + return (seg[seg.length - 1] || '').trim().slice(0, 10); + } + return s.slice(0, 10); + } + + function computeRowDelayed(row) { + if (isDeliveryHistoryStatus(row.deliveryStatus)) return false; + var end = parseExpectedEndDate(row.expectedDate); + if (!end) return false; + return end < '2026-06-01'; + } + + function filterSelectOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + function filterVehicleRows(rows) { + var list = rows.slice(); + var f = appliedFilters; + if (f.contractCode) { + var cc = (contractCodeOptions.find(function (o) { return o.value === f.contractCode; }) || {}).label || f.contractCode; + list = list.filter(function (r) { return (r.contractCode || '').indexOf(cc) !== -1; }); + } + if (f.projectName) { + var projLabel = (projectNameOptions.find(function (o) { return o.value === f.projectName; }) || {}).label; + if (projLabel) list = list.filter(function (r) { return r.projectName === projLabel; }); + } + if (f.customerName) { + var custLabel = (customerNameOptions.find(function (o) { return o.value === f.customerName; }) || {}).label; + if (custLabel) list = list.filter(function (r) { return r.customerName === custLabel; }); + } + if (f.deliveryRegion) list = list.filter(function (r) { return (r.deliveryRegion || '').indexOf(f.deliveryRegion) !== -1; }); + if (f.dateStart) list = list.filter(function (r) { return rowDateKey(r) >= f.dateStart; }); + if (f.dateEnd) list = list.filter(function (r) { return rowDateKey(r) <= f.dateEnd; }); + if (f.deliveryPerson) list = list.filter(function (r) { return (r.deliveryPerson || '').indexOf(f.deliveryPerson) !== -1; }); + if (parseMultiPlates(f.plateNos).length) { + list = list.filter(function (r) { return rowMatchesVehicleFilter(r, f.plateNos); }); + } + if (f.vin) list = list.filter(function (r) { return (r.vin || '').indexOf(f.vin) !== -1; }); + if (f.vehicleType) list = list.filter(function (r) { return r.vehicleType === f.vehicleType; }); + if (f.brand) list = list.filter(function (r) { return r.brand === f.brand; }); + if (f.model) list = list.filter(function (r) { return r.model === f.model; }); + if (f.businessDept) list = list.filter(function (r) { return (r.businessDept || '').indexOf(f.businessDept) !== -1; }); + if (f.businessOwner) list = list.filter(function (r) { return (r.businessOwner || '').indexOf(f.businessOwner) !== -1; }); + if (f.taskSource) list = list.filter(function (r) { return r.taskSource === f.taskSource; }); + if (f.bizType) list = list.filter(function (r) { return r.bizType === f.bizType; }); + if (f.isDelayed === 'yes') list = list.filter(function (r) { return computeRowDelayed(r); }); + if (f.isDelayed === 'no') list = list.filter(function (r) { return !computeRowDelayed(r); }); + return list; + } + + var allVehicleRows = useMemo(function () { + return expandOrdersToVehicleRows(deliveryOrders); + }, [deliveryOrders]); + + var operatorVisibleRows = useMemo(function () { + return filterRowsByOperatorRegion(allVehicleRows); + }, [allVehicleRows]); + + function buildSelectOptions(values) { + var seen = {}; + var opts = []; + (values || []).forEach(function (v) { + var s = v == null ? '' : String(v).trim(); + if (!s || seen[s]) return; + seen[s] = true; + opts.push({ value: s, label: s }); + }); + opts.sort(function (a, b) { return a.label.localeCompare(b.label, 'zh-CN'); }); + return opts; + } + + var dynamicFilterOptions = useMemo(function () { + var vins = []; + var vehicleTypes = []; + var brands = []; + var models = []; + var depts = []; + var owners = []; + operatorVisibleRows.forEach(function (r) { + if (r.vin) vins.push(r.vin); + if (r.vehicleType) vehicleTypes.push(r.vehicleType); + if (r.brand) brands.push(r.brand); + if (r.model) models.push(r.model); + if (r.businessDept && r.businessDept !== '-') depts.push(r.businessDept); + if (r.businessOwner && r.businessOwner !== '-') owners.push(r.businessOwner); + }); + return { + vinOptions: buildSelectOptions(vins), + vehicleTypeOptions: buildSelectOptions(vehicleTypes), + brandOptions: buildSelectOptions(brands), + modelOptions: buildSelectOptions(models), + businessDeptOptions: buildSelectOptions(depts), + businessOwnerOptions: buildSelectOptions(owners) + }; + }, [operatorVisibleRows]); + + var filteredBySearch = useMemo(function () { + return filterVehicleRows(operatorVisibleRows); + }, [operatorVisibleRows, appliedFilters]); + + var kpiStats = useMemo(function () { + var inProgress = 0; + var completed = 0; + filteredBySearch.forEach(function (r) { + if (isDeliveryHistoryStatus(r.deliveryStatus)) completed++; + else if (isDeliveryInProgressStatus(r.deliveryStatus)) inProgress++; + }); + return { total: filteredBySearch.length, inProgress: inProgress, completed: completed }; + }, [filteredBySearch]); + + function matchKpiFilter(row, filterKey) { + if (filterKey === 'total') return true; + if (filterKey === 'inProgress') return isDeliveryInProgressStatus(row.deliveryStatus); + if (filterKey === 'completed') return isDeliveryHistoryStatus(row.deliveryStatus); + return true; + } + + var filteredList = useMemo(function () { + return filteredBySearch.filter(function (r) { return matchKpiFilter(r, kpiFilter); }); + }, [filteredBySearch, kpiFilter]); + + function patchVehicleInOrder(orderId, vehicleKey, patch) { + setDeliveryOrders(function (orders) { + return orders.map(function (order) { + if (order.id !== orderId) return order; + return Object.assign({}, order, { + vehicleList: (order.vehicleList || []).map(function (v) { + var vk = v.vehicleKey != null ? v.vehicleKey : v.seq; + if (vk !== vehicleKey) return v; + return Object.assign({}, v, patch); + }) + }); + }); + }); + } + + var openEditDrawer = useCallback(function (record) { + setEditDrawer({ open: true, record: record, mode: 'edit' }); + }, []); + + var openViewDrawer = useCallback(function (record) { + setEditDrawer({ open: true, record: record, mode: 'view' }); + }, []); + + var closeEditDrawer = useCallback(function () { + setEditDrawer({ open: false, record: null, mode: 'edit' }); + }, []); + + var handleEditSave = useCallback(function (patch) { + var rec = editDrawer.record; + if (!rec) return; + patchVehicleInOrder(rec.orderId, rec.vehicleKey, Object.assign({}, patch, { deliveryStatus: '已保存' })); + }, [editDrawer.record]); + + var handleEditSubmit = useCallback(function (patch) { + var rec = editDrawer.record; + if (!rec) return; + patchVehicleInOrder(rec.orderId, rec.vehicleKey, Object.assign({}, patch, { deliveryStatus: '待客户签章', deliveryTime: patch.deliveryTime || '2026-06-04 10:00', deliveryPerson: patch.deliveryPerson || '魏山' })); + closeEditDrawer(); + }, [editDrawer.record, closeEditDrawer]); + + var navigateToContractDetail = useCallback(function (record) { + try { + sessionStorage.setItem(DV_CONTRACT_CODE_KEY, record.contractCode || ''); + sessionStorage.setItem(DV_ORDER_KEY, record.orderId || ''); + } catch (e) {} + message.info('跳转 合同管理-该合同详情页(合同编号 ' + (record.contractCode || '-') + ')'); + }, []); + + var navigateDelivery = useCallback(function (record, target) { + try { + sessionStorage.setItem(DV_ORDER_KEY, record.orderId || ''); + sessionStorage.setItem(DV_VEHICLE_KEY, String(record.vehicleKey != null ? record.vehicleKey : '')); + sessionStorage.setItem(DV_NAV_KEY, target || 'order'); + } catch (e) {} + var pageMap = { order: '交车管理-交车单', edit: '交车管理-交车单-编辑', view: '交车管理-交车单-查看' }; + message.info('跳转 ' + (pageMap[target] || target) + '(交车单 ' + record.orderId + ' · 车辆 ' + record.plateNo + ')'); + }, []); + + var page = pageState[0]; + var setPage = pageState[1]; + var pageSize = pageSizeState[0]; + var setPageSize = pageSizeState[1]; + + var handleKpiCardClick = useCallback(function (key) { + setKpiFilter(key); + setPage(1); + }, []); + + var totalCount = filteredList.length; + var displayList = useMemo(function () { + var start = (page - 1) * pageSize; + return filteredList.slice(start, start + pageSize); + }, [filteredList, page, pageSize]); + + var kpiExportLabelMap = { total: '全部交车任务', inProgress: '进行中的交车任务', completed: '已完成的交车任务' }; + + function normalizeRegionFilter(regionVal) { + if (Array.isArray(regionVal) && regionVal.length >= 2) { + var prov = regionOptions.find(function (r) { return r.value === regionVal[0]; }); + var city = prov && prov.children && prov.children.find(function (c) { return c.value === regionVal[1]; }); + return prov && city ? prov.label + '-' + city.label : undefined; + } + if (Array.isArray(regionVal) && regionVal.length === 1) { + var pOnly = regionOptions.find(function (r) { return r.value === regionVal[0]; }); + return pOnly ? pOnly.label : undefined; + } + return regionVal; + } + + var appliedMultiVehicles = useMemo(function () { + return parseMultiPlates(appliedFilters.plateNos); + }, [appliedFilters.plateNos]); + + var vehicleFilterTriggerText = appliedMultiVehicles.length + ? ('已选 ' + appliedMultiVehicles.length + ' 辆车') + : ''; + + var handleMultiPlateOpenChange = useCallback(function (open) { + setMultiPlateOpen(open); + if (open) setMultiPlateDraft(filters.plateNos || ''); + }, [filters.plateNos]); + + var handleMultiPlateClear = useCallback(function () { + setMultiPlateDraft(''); + setMultiPlateOpen(false); + var next = patchFilters(filters, { plateNos: '' }); + setFilters(next); + setAppliedFilters(patchFilters(next, {})); + setPage(1); + }, [filters]); + + var handleMultiPlateApply = useCallback(function () { + var trimmed = multiPlateDraft.trim(); + setFilters(function (f) { return patchFilters(f, { plateNos: trimmed }); }); + setMultiPlateOpen(false); + }, [multiPlateDraft]); + + var handleQuery = useCallback(function () { + var plateText = (multiPlateDraft.trim() || (filters.plateNos || '')).trim(); + var next = patchFilters(filters, { + deliveryRegion: normalizeRegionFilter(filters.deliveryRegion), + plateNos: plateText + }); + setFilters(next); + setAppliedFilters(patchFilters(next, {})); + setMultiPlateDraft(plateText); + setMultiPlateOpen(false); + setPage(1); + var tokens = parseMultiPlates(plateText); + if (tokens.length) { + message.success('已按 ' + tokens.length + ' 辆车筛选'); + } + }, [filters, multiPlateDraft]); + + var handleReset = useCallback(function () { + var empty = createEmptyFilters(); + setFilters(empty); + setAppliedFilters(empty); + setMultiPlateDraft(''); + setMultiPlateOpen(false); + setPage(1); + }, []); + + var getExportRowsByKpi = useCallback(function (filterKey) { + return filteredBySearch.filter(function (r) { return matchKpiFilter(r, filterKey); }); + }, [filteredBySearch]); + + var handleExport = useCallback(function () { + var tabLabel = kpiExportLabelMap[kpiFilter] || '交车任务'; + var rows = getExportRowsByKpi(kpiFilter); + if (!rows || rows.length === 0) { + message.warning('当前「' + tabLabel + '」无数据可导出'); + return; + } + var escapeCsv = function (v) { + var s = v == null ? '' : String(v); + if (s.indexOf(',') !== -1 || s.indexOf('"') !== -1 || s.indexOf('\n') !== -1) return '"' + s.replace(/"/g, '""') + '"'; + return s; + }; + var headers = ['完成交车时间', '交车人', '车牌号', '品牌', '型号', '合同编号', '客户名称', '项目名称', '业务部门', '业务负责人', '任务来源', '业务类型', '交车区域', '交车地点', '交车状态', '交车里程', '交车氢量', '交车电量', '创建时间', '创建人']; + var rowToCells = function (r) { + return [formatCompletedDeliveryTimeExport(r.deliveryTime), r.deliveryPerson || '-', displayPlateNo(r.plateNo), r.brand || '-', r.model || '-', r.contractCode, r.customerName, r.projectName, r.businessDept, r.businessOwner, r.taskSource, r.bizType, r.deliveryRegion, r.deliveryAddress, r.deliveryStatus, formatDeliveryMileage(r.deliveryMileage), formatDeliveryH2(r.deliveryH2, r.deliveryH2Unit), formatDeliveryElec(r.deliveryElec), r.createTime, r.createBy]; + }; + var csv = headers.map(escapeCsv).join(',') + '\n'; + rows.forEach(function (r) { + csv += rowToCells(r).map(escapeCsv).join(',') + '\n'; + }); + var blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' }); + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = '交车管理_' + tabLabel + '_' + new Date().getTime() + '.csv'; + a.click(); + URL.revokeObjectURL(url); + message.success('已导出「' + tabLabel + '」共 ' + rows.length + ' 条(含当前筛选条件)'); + }, [kpiFilter, getExportRowsByKpi]); + + var dateRangeValue = useMemo(function () { + if (!filters.dateStart && !filters.dateEnd) return null; + try { + if (typeof window !== 'undefined' && window.dayjs && filters.dateStart && filters.dateEnd) { + return [window.dayjs(filters.dateStart), window.dayjs(filters.dateEnd)]; + } + } catch (e) {} + return null; + }, [filters.dateStart, filters.dateEnd]); + + var onDateRangeChange = useCallback(function (dates, dateStrings) { + setFilters(function (f) { + var g = {}; for (var k in f) g[k] = f[k]; + g.dateStart = (dateStrings && dateStrings[0]) || ''; + g.dateEnd = (dateStrings && dateStrings[1]) || ''; + return g; + }); + }, []); + + // 交车区域 Cascader 的 value:用 appliedFilters 反推或存 value 数组。筛选用字符串比较,表单用 Cascader 选省-市 + var deliveryRegionValue = useMemo(function () { + var s = filters.deliveryRegion; + if (!s || typeof s !== 'string') return undefined; + var parts = s.split('-'); + if (parts.length < 2) return undefined; + for (var i = 0; i < regionOptions.length; i++) { + var prov = regionOptions[i]; + if (prov.label !== parts[0]) continue; + for (var j = 0; j < (prov.children || []).length; j++) { + if (prov.children[j].label === parts[1]) return [prov.value, prov.children[j].value]; + } + } + return undefined; + }, [filters.deliveryRegion]); + + var solidTagBaseStyle = { margin: 0, border: 'none', fontWeight: 600, color: '#fff', lineHeight: '20px', flexShrink: 0, flexGrow: 0 }; + + function renderSolidTag(text, bgColor) { + if (!text || text === '-') return '-'; + return React.createElement(Tag, { + style: Object.assign({}, solidTagBaseStyle, { backgroundColor: bgColor || '#64748b' }) + }, text); + } + + function renderDeliveryStatus(status, record) { + var bg = '#8c8c8c'; + if (isCustomerSignRelatedStatus(status)) bg = DV_CUSTOMER_SIGN_STATUS_BG; + else if (status === '已保存') bg = '#ea580c'; + else if (status === '未开始') bg = '#64748b'; + var signed = isDeliverySignedStatus(status); + var pendingSign = isDeliveryPendingCustomerSignStatus(status); + var style = Object.assign({}, solidTagBaseStyle, { backgroundColor: bg || '#64748b' }); + var tag = React.createElement(Tag, { + style: style, + title: signed ? '点击下载签章文件' : undefined, + role: signed ? 'button' : undefined, + tabIndex: signed ? 0 : undefined, + onClick: signed ? function (e) { e.stopPropagation(); downloadDeliverySignFile(record); } : undefined, + onKeyDown: signed ? function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); downloadDeliverySignFile(record); } } : undefined + }, status); + if (pendingSign) { + return React.createElement(Popover, { + content: buildCustomerSignPendingPopoverContent(record), + trigger: 'hover', + placement: 'topLeft', + mouseEnterDelay: 0.15, + mouseLeaveDelay: 0.1, + destroyTooltipOnHide: true + }, tag); + } + return tag; + } + + function renderBizTypeTag(bizType) { + if (!bizType || bizType === '-') return null; + var bg = '#64748b'; + if (bizType === '租赁') bg = '#2563eb'; + else if (bizType === '自营') bg = '#7c3aed'; + return React.createElement(Tag, { + style: Object.assign({}, solidTagBaseStyle, { + backgroundColor: bg, + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + textAlign: 'center', + alignSelf: 'center', + minWidth: 44, + height: 22, + padding: '0 10px', + boxSizing: 'border-box' + }) + }, bizType); + } + + function renderTaskSourceTag(text, bgColor) { + return React.createElement(Tag, { + style: Object.assign({}, solidTagBaseStyle, { backgroundColor: bgColor, cursor: 'default' }) + }, text); + } + + var handleColumnResizeStart = useCallback(function (colKey, e) { + e.preventDefault(); + e.stopPropagation(); + var startX = e.clientX; + var startW = colWidths[colKey] || 240; + function onMove(ev) { + var nextW = Math.max(140, Math.min(560, startW + ev.clientX - startX)); + setColWidths(function (prev) { + var next = {}; + for (var k in prev) next[k] = prev[k]; + next[colKey] = nextW; + return next; + }); + } + function onUp() { + document.removeEventListener('mousemove', onMove); + document.removeEventListener('mouseup', onUp); + } + document.addEventListener('mousemove', onMove); + document.addEventListener('mouseup', onUp); + }, [colWidths]); + + function renderResizableTitle(title, colKey) { + return React.createElement('div', { style: { position: 'relative', paddingRight: 10, userSelect: 'none' } }, + title, + React.createElement('span', { + role: 'separator', + 'aria-orientation': 'vertical', + title: '拖动调整列宽', + style: { + position: 'absolute', + right: 0, + top: 0, + bottom: 0, + width: 8, + cursor: 'col-resize', + zIndex: 1 + }, + onMouseDown: function (ev) { handleColumnResizeStart(colKey, ev); } + }) + ); + } + + function renderContractInfoCell(record) { + var customerName = record.customerName || '-'; + var showTip = customerName !== '-'; + var contractCode = record.contractCode || '-'; + var projectName = record.projectName || '-'; + return React.createElement('div', { style: { minWidth: 0, width: '100%' } }, + React.createElement('div', { + style: { + display: 'flex', + alignItems: 'center', + flexWrap: 'nowrap', + gap: 6, + minWidth: 0, + width: '100%', + lineHeight: 1.45 + } + }, + React.createElement(Tooltip, { title: showTip ? customerName : null, placement: 'topLeft' }, + React.createElement('span', { + style: { + flex: '1 1 auto', + minWidth: 0, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + color: '#333', + display: 'block' + } + }, customerName) + ), + React.createElement('span', { + style: { + flexShrink: 0, + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center' + } + }, renderBizTypeTag(record.bizType)) + ), + contractCode !== '-' + ? React.createElement('div', { + style: Object.assign({}, cellLineSubStyle, DV_LIST_LINK_STYLE, { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }), + role: 'button', + tabIndex: 0, + onClick: function () { navigateToContractDetail(record); }, + onKeyDown: function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); navigateToContractDetail(record); } } + }, contractCode) + : React.createElement('div', { style: cellLineSubStyle }, '-'), + projectName !== '-' + ? React.createElement('div', { + style: Object.assign({}, cellLineSubStyle, DV_LIST_LINK_STYLE, { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }), + role: 'button', + tabIndex: 0, + onClick: function () { navigateToContractDetail(record); }, + onKeyDown: function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); navigateToContractDetail(record); } } + }, projectName) + : React.createElement('div', { style: Object.assign({}, cellLineSubStyle, { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }) }, '-') + ); + } + + function renderDeliveryPlaceCell(record) { + var regionText = '交车区域:' + formatDeliveryRegion(record.deliveryRegion); + var parkingText = isPlatePending(record.plateNo) ? '-' : (record.deliveryAddress || '-'); + return renderCellLines(regionText, [parkingText]); + } + + function parseCompletedDeliveryTime(raw) { + if (!raw || String(raw).trim() === '' || raw === '-') return { date: '-', time: '-' }; + var s = String(raw).trim(); + if (s.indexOf(' ') >= 0) { + var parts = s.split(/\s+/); + var datePart = parts[0] || '-'; + var timePart = parts[1] || '-'; + if (timePart.length >= 5) timePart = timePart.slice(0, 5); + return { date: datePart, time: timePart }; + } + return { date: s, time: '-' }; + } + + function renderCompletedDeliveryTimeCell(record) { + var parsed = parseCompletedDeliveryTime(record.deliveryTime); + return renderCellLines(parsed.date, [parsed.time]); + } + + function renderTaskCreateTimeCell(record) { + var parsed = parseCompletedDeliveryTime(record.createTime); + if (parsed.date === '-' && parsed.time === '-') return '-'; + if (parsed.time === '-') return parsed.date; + return parsed.date + ' ' + parsed.time; + } + + function renderTaskCreateByCell(createBy) { + if (createBy == null || createBy === undefined) return '-'; + var name = String(createBy).trim(); + return name === '' || name === '-' ? '-' : name; + } + + function isVehicleReturned(record) { + if (!record || !isVehicleDelivered(record.deliveryStatus) || !isDeliveryHistoryStatus(record.deliveryStatus)) return false; + return record.vehicleReturned === true; + } + + function renderVehicleReturnedCell(record) { + if (!record || !isVehicleDelivered(record.deliveryStatus)) { + return '-'; + } + if (!isVehicleReturned(record)) { + return React.createElement('span', { style: { color: '#64748b' } }, '未归还'); + } + var label = React.createElement('span', { + style: { color: '#16a34a', fontWeight: 600, cursor: 'default', borderBottom: '1px dashed #86efac' } + }, '已归还'); + return React.createElement(Popover, { + content: renderReturnVehiclePopoverContent(record), + trigger: 'hover', + placement: 'topLeft', + mouseEnterDelay: 0.15, + mouseLeaveDelay: 0.1, + destroyTooltipOnHide: true + }, label); + } + + function formatReturnTimeDisplay(raw) { + var parsed = parseCompletedDeliveryTime(raw); + if (parsed.date === '-' && parsed.time === '-') return '-'; + if (parsed.time === '-') return parsed.date; + return parsed.date + ' ' + parsed.time; + } + + function renderReturnVehiclePopoverContent(record) { + return React.createElement('div', { style: { minWidth: 168, fontSize: 13, lineHeight: 1.65 } }, + React.createElement('div', null, + React.createElement('span', { style: { color: '#64748b' } }, '还车时间:'), + React.createElement('span', { style: { color: '#334155', fontWeight: 600 } }, formatReturnTimeDisplay(record.returnTime)) + ), + React.createElement('div', { style: { marginTop: 4 } }, + React.createElement('span', { style: { color: '#64748b' } }, '还车人:'), + React.createElement('span', { style: { color: '#334155', fontWeight: 600 } }, record.returnPerson || '-') + ) + ); + } + + function formatCompletedDeliveryTimeExport(raw) { + var parsed = parseCompletedDeliveryTime(raw); + if (parsed.date === '-' && parsed.time === '-') return '-'; + if (parsed.time === '-') return parsed.date; + return parsed.date + ' ' + parsed.time; + } + + var cellLineMainStyle = { lineHeight: 1.45, color: '#333', wordBreak: 'break-all' }; + var cellLineSubStyle = { lineHeight: 1.4, fontSize: 12, color: '#8c8c8c', marginTop: 2, wordBreak: 'break-all' }; + + function renderCellLines(mainText, subLines) { + var subs = subLines || []; + return React.createElement('div', null, + React.createElement('div', { style: cellLineMainStyle }, mainText || '-'), + subs.map(function (line, i) { + return React.createElement('div', { key: i, style: cellLineSubStyle }, line || '-'); + }) + ); + } + + function isPlatePending(plateNo) { + if (plateNo == null || plateNo === undefined) return true; + var s = String(plateNo).trim(); + return s === '' || s === '-'; + } + + function displayPlateNo(plateNo) { + return isPlatePending(plateNo) ? '车牌待选' : String(plateNo).trim(); + } + + function displayBrandModel(brand, model) { + var b = brand && brand !== '-' ? String(brand).trim() : ''; + var m = model && model !== '-' ? String(model).trim() : ''; + if (b && m) return b + '-' + m; + if (b) return b; + if (m) return m; + return '-'; + } + + function displayVin(vin, plateNo) { + if (plateNo != null && isPlatePending(plateNo)) return '-'; + var s = vin == null || vin === undefined ? '' : String(vin).trim(); + return s || '-'; + } + + function displayReplaceOldPlate(plate) { + var s = plate == null ? '' : String(plate).trim(); + return s || '-'; + } + + function renderReplaceVehiclePopoverContent(record) { + var oldPlate = displayReplaceOldPlate(record.replaceOldPlate); + var newPlate = displayPlateNo(record.plateNo); + return React.createElement('div', { style: { minWidth: 168, fontSize: 13, lineHeight: 1.5 } }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } }, + React.createElement('span', { style: { color: '#64748b' } }, '旧车'), + React.createElement('span', { style: { fontWeight: 600, color: '#334155' } }, oldPlate), + React.createElement('span', { style: { color: '#94a3b8', fontWeight: 600 } }, '→'), + React.createElement('span', { style: { color: '#64748b' } }, '新车'), + React.createElement('span', { + style: { fontWeight: 600, color: isPlatePending(record.plateNo) ? '#d48806' : '#334155' } + }, newPlate) + ) + ); + } + + function renderTaskSourceCell(value, record) { + if (value === '替换车') { + return React.createElement(Popover, { + content: renderReplaceVehiclePopoverContent(record), + trigger: 'hover', + placement: 'topLeft', + mouseEnterDelay: 0.15, + mouseLeaveDelay: 0.1, + destroyTooltipOnHide: true + }, renderTaskSourceTag('替换车', '#ea580c')); + } + if (value === '交车任务') return renderTaskSourceTag('交车任务', '#2563eb'); + return value || '-'; + } + + function renderVehicleInfoCell(record) { + var pending = isPlatePending(record.plateNo); + var plateStyle = Object.assign({}, cellLineMainStyle, { cursor: 'pointer' }, pending ? { color: '#d48806', fontWeight: 500 } : { color: '#165dff' }); + return React.createElement('div', null, + React.createElement('div', { + style: plateStyle, + role: 'button', + tabIndex: 0, + onClick: function () { openViewDrawer(record); }, + onKeyDown: function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openViewDrawer(record); } } + }, displayPlateNo(record.plateNo)), + React.createElement('div', { style: cellLineSubStyle }, displayBrandModel(record.brand, record.model)), + React.createElement('div', { style: cellLineSubStyle }, displayVin(record.vin, record.plateNo)) + ); + } + + // 列表列(按车辆一行;待处理/历史记录列顺序一致) + var colDeliveryTime = { title: '完成交车时间', key: 'completedDeliveryTime', width: 118, render: function (_, r) { return renderCompletedDeliveryTimeCell(r); } }; + var colDeliveryPerson = { title: '交车人', dataIndex: 'deliveryPerson', key: 'deliveryPerson', width: 80, render: function (v) { return v || '-'; } }; + var colDeliveryStatus = { title: '交车状态', dataIndex: 'deliveryStatus', key: 'deliveryStatus', width: 108, render: function (v, r) { return renderDeliveryStatus(v, r); } }; + var sharedListColumns = useMemo(function () { + return [ + { + title: '车辆信息', + key: 'vehicleInfo', + width: 188, + render: function (_, r) { return renderVehicleInfoCell(r); } + }, + { + title: renderResizableTitle('合同信息', 'contractInfo'), + key: 'contractInfo', + width: colWidths.contractInfo, + render: function (_, r) { return renderContractInfoCell(r); } + }, + { + title: '业务负责人', + key: 'businessInfo', + width: 120, + render: function (_, r) { + return renderCellLines(r.businessDept, [r.businessOwner]); + } + }, + { title: '任务来源', dataIndex: 'taskSource', key: 'taskSource', width: 92, ellipsis: true, render: function (v, r) { return renderTaskSourceCell(v, r); } }, + { title: '交车地点', key: 'deliveryPlace', width: 168, render: function (_, r) { return renderDeliveryPlaceCell(r); } }, + colDeliveryStatus, + colDeliveryTime, + colDeliveryPerson, + { title: '是否归还', key: 'vehicleReturned', width: 88, render: function (_, r) { return renderVehicleReturnedCell(r); } }, + { + title: '交车记录', + key: 'deliveryRecord', + width: 118, + render: function (_, r) { return renderDeliveryRecordCell(r); } + }, + { title: '交车任务创建时间', key: 'taskCreateTime', width: 150, ellipsis: true, render: function (_, r) { return renderTaskCreateTimeCell(r); } }, + { title: '交车任务创建人', dataIndex: 'createBy', key: 'taskCreateBy', width: 108, ellipsis: true, render: function (v) { return renderTaskCreateByCell(v); } } + ]; + }, [colWidths.contractInfo, handleColumnResizeStart, navigateToContractDetail]); + + var listColumns = useMemo(function () { + return sharedListColumns.concat([ + { + title: '操作', key: 'action', width: 96, fixed: 'right', render: function (_, r) { + var nodes = [ + React.createElement(Button, { key: 'view', type: 'link', size: 'small', onClick: function () { openViewDrawer(r); } }, '查看') + ]; + if (canEditDeliveryRow(r)) { + nodes.push(React.createElement(Button, { key: 'edit', type: 'link', size: 'small', onClick: function () { openEditDrawer(r); } }, '编辑')); + } + return React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 2, flexWrap: 'nowrap' } }, nodes); + } + } + ]); + }, [sharedListColumns, openEditDrawer, openViewDrawer]); + + var kpiCards = useMemo(function () { + return [ + { key: 'total', type: 'total', title: '全部交车任务', desc: '当前筛选条件下的全部交车任务(含进行中与已完成)', val: kpiStats.total, icon: DV_KPI_ICONS.total }, + { key: 'inProgress', type: 'progress', title: '进行中的交车任务', desc: '交车状态为「未开始」「已保存」「待客户签章」的任务(客户未完成最终签章)', val: kpiStats.inProgress, icon: DV_KPI_ICONS.progress }, + { key: 'completed', type: 'completed', title: '已完成的交车任务', desc: '客户已完成最终签章步骤的所有交车任务(状态为「客户已签章」)', val: kpiStats.completed, icon: DV_KPI_ICONS.completed } + ]; + }, [kpiStats]); + + function renderKpiCard(card) { + var active = kpiFilter === card.key; + return React.createElement('div', { + key: card.key, + role: 'button', + tabIndex: 0, + className: 'lc-alert-card lc-alert-card--' + card.type + ' lc-alert-card-clickable' + (active ? ' lc-alert-card-active' : ''), + onClick: function () { handleKpiCardClick(card.key); }, + onKeyDown: function (e) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleKpiCardClick(card.key); + } + } + }, + React.createElement('div', { className: 'lc-alert-card-tip-anchor' }, + React.createElement(Tooltip, { title: card.desc, placement: 'topRight', overlayStyle: { maxWidth: 360 } }, + React.createElement('span', { + className: 'lc-alert-card-tip', + role: 'img', + 'aria-label': card.title + '说明', + onClick: function (e) { e.stopPropagation(); }, + onMouseDown: function (e) { e.stopPropagation(); } + }, DV_KPI_TIP_SVG) + ) + ), + React.createElement('div', { className: 'lc-alert-card-icon' }, card.icon), + React.createElement('div', { className: 'lc-alert-card-main' }, + React.createElement('div', { className: 'lc-alert-card-val' }, card.val), + React.createElement('div', { className: 'lc-alert-card-title' }, card.title) + ) + ); + } + + var tablePagination = useMemo(function () { + return { + current: page, + pageSize: pageSize, + total: totalCount, + showSizeChanger: true, + showTotal: function (t) { return '共 ' + t + ' 条'; }, + pageSizeOptions: ['10', '20', '50'], + onChange: function (p, size) { setPage(p); if (size !== pageSize) setPageSize(size); } + }; + }, [page, pageSize, totalCount]); + + var filterLabelStyle = { display: 'block', marginBottom: 4, color: '#333', fontSize: 14 }; + var filterControlStyle = { width: '100%' }; + var filterItemStyle = { minWidth: 0 }; + + var styles = { + page: { padding: '16px 24px 48px', backgroundColor: '#f5f5f5', minHeight: '100vh', fontSize: 14 }, + breadcrumb: { marginBottom: 16, color: '#666' }, + breadcrumbSep: { margin: '0 8px', color: '#999' }, + card: { backgroundColor: '#fff', borderRadius: 8, marginBottom: 16, boxShadow: '0 1px 2px rgba(0,0,0,0.05)', overflow: 'hidden' }, + cardBody: { padding: '20px 24px' }, + filterGrid: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr', gap: '16px 24px', alignItems: 'start' }, + filterActions: { display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 8, marginTop: 16, paddingTop: 16, borderTop: '1px solid #f1f5f9' } + }; + + var filterItems = [ + React.createElement('div', { key: 'plateNos', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '车辆'), + React.createElement(Popover, { + open: multiPlateOpen, + onOpenChange: handleMultiPlateOpenChange, + trigger: 'click', + placement: 'bottomLeft', + overlayClassName: 'lc-multi-plate-popover', + content: React.createElement('div', { className: 'lc-multi-plate-pop' }, + React.createElement('div', { className: 'lc-multi-plate-pop-hint' }, + '支持多辆车车牌号、车辆识别代码,每行一条;可从 Excel 等批量复制粘贴,点击「确定」后于筛选区点击「搜索」生效。' + ), + React.createElement(Input.TextArea, { + value: multiPlateDraft, + onChange: function (e) { setMultiPlateDraft(e.target.value); }, + placeholder: '浙F80088\n粤AGP4598\nLNBSCPKB9RR223402', + autoSize: { minRows: 5, maxRows: 10 }, + style: { borderRadius: 8, fontFamily: 'monospace', fontSize: 13 } + }), + React.createElement('div', { className: 'lc-multi-plate-pop-actions' }, + React.createElement(Button, { size: 'small', onClick: handleMultiPlateClear }, '清空'), + React.createElement(Button, { size: 'small', type: 'primary', onClick: handleMultiPlateApply }, '确定') + ) + ) + }, + React.createElement(Input, { + className: 'lc-multi-plate-trigger', + readOnly: true, + allowClear: !!vehicleFilterTriggerText, + placeholder: '支持多辆车车牌号、车辆识别代码,每行一条', + value: vehicleFilterTriggerText, + onClick: function () { setMultiPlateOpen(true); }, + onClear: function (e) { + if (e && e.stopPropagation) e.stopPropagation(); + handleMultiPlateClear(); + }, + style: Object.assign({ borderRadius: 8 }, filterControlStyle), + suffix: React.createElement('svg', { + width: 14, height: 14, viewBox: '0 0 24 24', fill: 'none', stroke: '#94a3b8', strokeWidth: 2, + style: { pointerEvents: 'none' } + }, React.createElement('polyline', { points: '6 9 12 15 18 9' })) + }) + ) + ), + React.createElement('div', { key: 'contractCode', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '合同编号'), + React.createElement(Select, { + placeholder: '请输入或选择合同编号', + allowClear: true, + showSearch: true, + optionFilterProp: 'label', + style: filterControlStyle, + value: filters.contractCode, + onChange: function (v) { setFilters(function (f) { return patchFilters(f, { contractCode: v }); }); }, + options: contractCodeOptions + }) + ), + React.createElement('div', { key: 'projectName', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '项目名称'), + React.createElement(Select, { + placeholder: '请输入或选择项目名称', + allowClear: true, + showSearch: true, + optionFilterProp: 'label', + style: filterControlStyle, + value: filters.projectName, + onChange: function (v) { setFilters(function (f) { return patchFilters(f, { projectName: v }); }); }, + options: projectNameOptions + }) + ), + React.createElement('div', { key: 'customerName', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '客户名称'), + React.createElement(Select, { + placeholder: '请输入或选择客户名称', + allowClear: true, + showSearch: true, + optionFilterProp: 'label', + style: filterControlStyle, + value: filters.customerName, + onChange: function (v) { setFilters(function (f) { return patchFilters(f, { customerName: v }); }); }, + options: customerNameOptions + }) + ), + React.createElement('div', { key: 'deliveryRegion', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '交车区域'), + React.createElement(Cascader, { + options: regionOptions, + placeholder: '请选择省-市', + allowClear: true, + style: filterControlStyle, + value: deliveryRegionValue, + onChange: function (value) { + var s; + if (value && value.length >= 2) { + var prov = regionOptions.find(function (r) { return r.value === value[0]; }); + var city = prov && prov.children && prov.children.find(function (c) { return c.value === value[1]; }); + s = prov && city ? prov.label + '-' + city.label : undefined; + } else { s = undefined; } + setFilters(function (f) { return patchFilters(f, { deliveryRegion: s }); }); + }, + displayRender: function (labels) { return labels && labels.length ? labels.join(' / ') : ''; } + }) + ), + React.createElement('div', { key: 'completedTime', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '完成交车时间'), + React.createElement(RangePicker, { + style: filterControlStyle, + placeholder: ['请选择开始时间', '请选择结束时间'], + value: dateRangeValue, + onChange: onDateRangeChange + }) + ), + React.createElement('div', { key: 'deliveryPerson', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '交车人'), + React.createElement(Select, { + placeholder: '请输入或选择交车人', + allowClear: true, + showSearch: true, + optionFilterProp: 'label', + style: filterControlStyle, + value: filters.deliveryPerson, + onChange: function (v) { setFilters(function (f) { return patchFilters(f, { deliveryPerson: v }); }); }, + options: deliveryPersonOptions + }) + ), + React.createElement('div', { key: 'vin', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '车辆识别代码'), + React.createElement(Select, { + placeholder: '请输入或选择车辆识别代码', + allowClear: true, + showSearch: true, + style: filterControlStyle, + value: filters.vin, + onChange: function (v) { setFilters(function (f) { return patchFilters(f, { vin: v }); }); }, + options: dynamicFilterOptions.vinOptions, + filterOption: filterSelectOption + }) + ), + React.createElement('div', { key: 'vehicleType', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '车辆类型'), + React.createElement(Select, { + placeholder: '请选择车辆类型', + allowClear: true, + showSearch: true, + style: filterControlStyle, + value: filters.vehicleType, + onChange: function (v) { setFilters(function (f) { return patchFilters(f, { vehicleType: v }); }); }, + options: dynamicFilterOptions.vehicleTypeOptions, + filterOption: filterSelectOption + }) + ), + React.createElement('div', { key: 'brand', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '品牌'), + React.createElement(Select, { + placeholder: '请选择品牌', + allowClear: true, + showSearch: true, + style: filterControlStyle, + value: filters.brand, + onChange: function (v) { setFilters(function (f) { return patchFilters(f, { brand: v }); }); }, + options: dynamicFilterOptions.brandOptions, + filterOption: filterSelectOption + }) + ), + React.createElement('div', { key: 'model', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '型号'), + React.createElement(Select, { + placeholder: '请选择或输入型号', + allowClear: true, + showSearch: true, + style: filterControlStyle, + value: filters.model, + onChange: function (v) { setFilters(function (f) { return patchFilters(f, { model: v }); }); }, + options: dynamicFilterOptions.modelOptions, + filterOption: filterSelectOption + }) + ), + React.createElement('div', { key: 'businessDept', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '业务部门'), + React.createElement(Select, { + placeholder: '请输入或选择业务部门', + allowClear: true, + showSearch: true, + style: filterControlStyle, + value: filters.businessDept, + onChange: function (v) { setFilters(function (f) { return patchFilters(f, { businessDept: v }); }); }, + options: dynamicFilterOptions.businessDeptOptions, + filterOption: filterSelectOption + }) + ), + React.createElement('div', { key: 'businessOwner', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '业务负责人'), + React.createElement(Select, { + placeholder: '请输入或选择业务负责人', + allowClear: true, + showSearch: true, + style: filterControlStyle, + value: filters.businessOwner, + onChange: function (v) { setFilters(function (f) { return patchFilters(f, { businessOwner: v }); }); }, + options: dynamicFilterOptions.businessOwnerOptions, + filterOption: filterSelectOption + }) + ), + React.createElement('div', { key: 'taskSource', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '任务来源'), + React.createElement(Select, { + placeholder: '请选择任务来源', + allowClear: true, + style: filterControlStyle, + value: filters.taskSource, + onChange: function (v) { setFilters(function (f) { return patchFilters(f, { taskSource: v }); }); }, + options: taskSourceOptions + }) + ), + React.createElement('div', { key: 'bizType', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '业务类型'), + React.createElement(Select, { + placeholder: '请选择业务类型', + allowClear: true, + style: filterControlStyle, + value: filters.bizType, + onChange: function (v) { setFilters(function (f) { return patchFilters(f, { bizType: v }); }); }, + options: bizTypeOptions + }) + ), + React.createElement('div', { key: 'isDelayed', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '是否延期'), + React.createElement(Select, { + placeholder: '请选择是否延期', + allowClear: true, + style: filterControlStyle, + value: filters.isDelayed, + onChange: function (v) { setFilters(function (f) { return patchFilters(f, { isDelayed: v }); }); }, + options: isDelayedOptions + }) + ) + ]; + + var filterVisibleCount = filterExpanded ? filterItems.length : 4; + var filterNodes = []; + var fi; + for (fi = 0; fi < filterVisibleCount && fi < filterItems.length; fi++) { + filterNodes.push(filterItems[fi]); + } + + var breadcrumbItems = [ + { title: '运维管理' }, + { title: '车辆业务' }, + { title: '交车管理' } + ]; + + return React.createElement('div', { style: styles.page }, + React.createElement('style', null, DV_KPI_STYLE), + React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 } }, + React.createElement(Breadcrumb, { items: breadcrumbItems }), + React.createElement(Button, { + type: 'default', + style: { borderRadius: 8, border: '1px solid #cbd5e1', fontWeight: 600, color: '#475569' }, + onClick: function () { setRequirementModalOpen(true); } + }, '查看需求说明') + ), + React.createElement(Card, { style: { marginBottom: 16 } }, + React.createElement('div', { style: styles.cardBody }, + React.createElement('div', { style: styles.filterGrid }, filterNodes), + React.createElement('div', { style: styles.filterActions }, + React.createElement(Button, { onClick: handleReset }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleQuery }, '搜索'), + React.createElement(Button, { + type: 'link', + size: 'small', + onClick: function () { setFilterExpanded(!filterExpanded); }, + style: { display: 'inline-flex', alignItems: 'center', gap: 4, padding: '0 4px' } + }, + filterExpanded ? '收起' : '展开', + React.createElement('svg', { + width: 12, + height: 12, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + strokeWidth: 2, + strokeLinecap: 'round', + strokeLinejoin: 'round', + style: { transform: filterExpanded ? 'rotate(180deg)' : 'none', transition: 'transform 0.2s ease' } + }, React.createElement('polyline', { points: '6 9 12 15 18 9' })) + ) + ) + ) + ), + React.createElement(Card, null, + React.createElement('div', { style: styles.cardBody }, + React.createElement('div', { className: 'dv-kpi-stats-row' }, kpiCards.map(renderKpiCard)), + React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', alignItems: 'center', marginBottom: 12, flexWrap: 'wrap', gap: 12 } }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } }, + React.createElement('span', { style: { fontSize: 13, color: '#64748b' } }, + '当前标签:', + React.createElement('span', { style: { color: '#334155', fontWeight: 600 } }, kpiExportLabelMap[kpiFilter] || '-'), + ' · 导出与列表一致(含筛选,全部命中行)' + ), + React.createElement(Button, { onClick: handleExport }, '导出') + ) + ), + React.createElement(Table, { + columns: listColumns, + dataSource: displayList, + rowKey: 'id', + pagination: tablePagination, + tableLayout: 'fixed', + scroll: { x: 1760 }, + size: 'middle' + }) + ) + ), + React.createElement(Modal, { + title: '交车管理 — 说明文档', + open: requirementModalOpen[0], + onCancel: function () { setRequirementModalOpen(false); }, + footer: null, + width: 880, + centered: true, + destroyOnClose: true + }, React.createElement(Tabs, { + defaultActiveKey: 'prd', + size: 'small', + items: requirementModalItems + })), + React.createElement(DeliveryEditDrawer, { + open: editDrawer.open, + record: editDrawer.record, + mode: editDrawer.mode, + onClose: closeEditDrawer, + onSave: handleEditSave, + onSubmit: handleEditSubmit + }) + ); +}; + +if (typeof window !== 'undefined') { + window.Component = Component; + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function () { + var rootEl = document.getElementById('root'); + if (rootEl && window.ReactDOM && window.React) { + var root = ReactDOM.createRoot(rootEl); + root.render(React.createElement(Component)); + } + }); + } else { + var rootEl = document.getElementById('root'); + if (rootEl && window.ReactDOM && window.React) { + var root = ReactDOM.createRoot(rootEl); + root.render(React.createElement(Component)); + } + } +} + +export default Component; diff --git a/src/prototypes/oneos-web-ops/pages/28-年审管理-办理.jsx b/src/prototypes/oneos-web-ops/pages/28-年审管理-办理.jsx new file mode 100644 index 0000000..85e4fdd --- /dev/null +++ b/src/prototypes/oneos-web-ops/pages/28-年审管理-办理.jsx @@ -0,0 +1,1115 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 运维管理 - 车辆业务 - 年审管理 · 办理(分组表单页,逻辑对齐 ONE-OS 小程序) + +const { useState, useMemo, useEffect, useRef } = React; +const moment = window.moment || window.dayjs; +const antd = window.antd; +const { + App, + Alert, + Button, + Col, + DatePicker, + Form, + Input, + InputNumber, + Modal, + Row, + Select, + Spin, + Tag, + Typography, + Upload, + message, +} = antd; +const Image = antd.Image; + +const { Text, Paragraph } = Typography; +const TextArea = Input.TextArea; + +const AR_DRAFT_STORAGE_KEY = 'oneos_ar_operate_drafts_v1'; +const AR_TASK_ID_STORAGE_KEY = 'oneos_ar_operate_task_id'; +const AR_TASKS_STORAGE_KEY = 'oneos_ar_web_tasks_v1'; +const AR_NAV_TARGET_KEY = 'oneos_ar_navigate_target'; +const AR_NAV_EVENT = 'oneos-ar-return-list'; +const CERTIFICATE_LICENSE_SYNC_KEY = 'oneos_certificate_license_sync'; + +/** 办理页保存/提交后返回列表(Axhub 多文件原型:事件 + session 标记) */ +const navigateToAnnualReviewList = (toastMsg) => { + try { + sessionStorage.setItem(AR_NAV_TARGET_KEY, 'list'); + sessionStorage.removeItem(AR_TASK_ID_STORAGE_KEY); + } catch { + /* ignore */ + } + try { + window.dispatchEvent(new CustomEvent(AR_NAV_EVENT)); + } catch { + /* ignore */ + } + if (typeof window.__axhubNavigate === 'function') { + window.__axhubNavigate('年审管理'); + if (toastMsg) message.success(toastMsg); + return; + } + if (toastMsg) message.success(toastMsg); + else message.info('请切换至「年审管理」列表页查看'); +}; + +const INSPECTION_STATION_LIST = ['汇通检测站', '平湖检测站', '嘉兴检测站', '松江检测站']; +const REPAIR_STATION_LIST = [ + '杭州拱墅维修站', + '广州天河维修站', + '上海浦东维修站', + '深圳南山维修站', + '天津港保税区维修站', +]; +const INSPECTION_STATION_STORAGE_KEY = 'annual-review.station.inspection'; + +const MAX_SERVICE_PHOTOS = 10; +const MAX_LICENSE_PHOTOS = 4; +const LICENSE_OCR_MOCK_MS = 1500; +const MOCK_CURRENT_HANDLER = '张明辉'; + +const EMPTY_SERVICE_FORM = { station: '', cost: '', remark: '', photos: [] }; +const EMPTY_INSPECTION_FORM = { station: '', cost: '', remark: '' }; +const EMPTY_LICENSE_FORM = { photos: [], inspectionValidUntil: null, ocrStatus: 'idle' }; + +const mapOperateStatus = (raw) => { + if (raw === '可运营' || raw === '待运营') return '库存'; + return raw || '—'; +}; + +const MOCK_TASKS = [ + { + id: 'ar-1', + plateNo: '粤B58888F', + vin: 'LGHXCAE28M6789012', + brand: '福田', + model: '奥铃4.5吨冷藏车', + operateStatusRaw: '租赁', + expireDate: '2026-07-20', + daysLeft: 49, + tab: 'pending', + province: '广东省', + city: '深圳市', + executor: '', + executeTime: '', + }, + { + id: 'ar-2', + plateNo: '沪A03561F', + vin: 'LMRKH9AC0R1004086', + brand: '宇通', + model: '49吨牵引车头', + operateStatusRaw: '自营', + expireDate: '2026-07-31', + daysLeft: 60, + tab: 'pending', + province: '上海市', + city: '上海市', + executor: '', + executeTime: '', + }, + { + id: 'ar-3', + plateNo: '苏E33333', + vin: 'LSXCH9AE8M1094857', + brand: '陕汽', + model: '德龙X3000混动牵引车', + operateStatusRaw: '可运营', + expireDate: '2026-05-15', + daysLeft: -17, + tab: 'pending', + province: '江苏省', + city: '苏州市', + executor: '', + executeTime: '', + }, + { + id: 'ar-7', + plateNo: '鲁Q88901', + vin: 'LZZ5CLSB8NC778899', + brand: '重汽', + model: '豪沃T7H牵引车', + operateStatusRaw: '租赁', + expireDate: '2026-04-10', + daysLeft: -52, + tab: 'pending', + province: '山东省', + city: '临沂市', + executor: '', + executeTime: '', + }, + { + id: 'ar-8', + plateNo: '闽D55662', + vin: 'LFWNHXSD8P1122334', + brand: '金龙', + model: '凯歌纯电动厢货', + operateStatusRaw: '自营', + expireDate: '2026-04-27', + daysLeft: -35, + tab: 'pending', + province: '福建省', + city: '厦门市', + executor: '', + executeTime: '', + }, + { + id: 'ar-4', + plateNo: '浙A88888', + vin: 'LMRKH9AE2P9876543', + brand: '宇通', + model: '氢燃料电池大巴', + operateStatusRaw: '待运营', + expireDate: '2026-08-10', + daysLeft: 70, + tab: 'pending', + province: '浙江省', + city: '杭州市', + executor: '', + executeTime: '', + }, + { + id: 'ar-6', + plateNo: '皖B66221', + vin: 'LZZ5CLSB8NA123456', + brand: '江淮', + model: '格尔发A5', + operateStatusRaw: '库存', + expireDate: '2026-06-28', + daysLeft: 27, + tab: 'pending', + province: '安徽省', + city: '合肥市', + executor: '', + executeTime: '', + }, +].map((t) => ({ + ...t, + operateStatus: mapOperateStatus(t.operateStatusRaw), +})); + +const isFormCostEmpty = (cost) => cost === '' || cost == null; + +const validateStationCost = (form, stationLabel) => { + if (form?.station && isFormCostEmpty(form.cost)) { + message.error(`请填写${stationLabel}费用`); + return false; + } + return true; +}; + +const validateLicenseForm = (licenseForm) => { + if (!(licenseForm?.photos || []).length) { + message.error('请上传行驶证照片'); + return false; + } + if (!licenseForm?.inspectionValidUntil) { + message.error('请填写检验有效期'); + return false; + } + return true; +}; + +const formatTaskRegion = (task) => { + if (!task?.province) return '—'; + if (task.city) return `${task.province}-${task.city}`; + return task.province; +}; + +const formatOperateStatusDisplay = (task) => { + const base = task.operateStatus || '—'; + if (base === '库存' && (task.operateStatusRaw === '可运营' || task.operateStatusRaw === '待运营')) { + return `库存(${task.operateStatusRaw})`; + } + return base; +}; + +const readInspectionStationList = () => { + try { + const raw = localStorage.getItem(INSPECTION_STATION_STORAGE_KEY); + const parsed = JSON.parse(raw); + if (Array.isArray(parsed) && parsed.length) return parsed; + } catch { + /* ignore */ + } + return [...INSPECTION_STATION_LIST]; +}; + +const loadTasksFromStorage = () => { + try { + const raw = localStorage.getItem(AR_TASKS_STORAGE_KEY); + if (!raw) return MOCK_TASKS; + const parsed = JSON.parse(raw); + return Array.isArray(parsed) && parsed.length ? parsed : MOCK_TASKS; + } catch { + return MOCK_TASKS; + } +}; + +const persistTasksToStorage = (tasks) => { + try { + localStorage.setItem(AR_TASKS_STORAGE_KEY, JSON.stringify(tasks)); + } catch { + /* ignore */ + } +}; + +const loadOperateDrafts = () => { + try { + const raw = localStorage.getItem(AR_DRAFT_STORAGE_KEY); + if (!raw) return {}; + const parsed = JSON.parse(raw); + return parsed && typeof parsed === 'object' ? parsed : {}; + } catch { + return {}; + } +}; + +const persistOperateDrafts = (drafts) => { + try { + localStorage.setItem(AR_DRAFT_STORAGE_KEY, JSON.stringify(drafts)); + } catch { + /* ignore */ + } +}; + +const getUploadPreviewUrl = (file) => { + if (!file) return ''; + if (file.url) return file.url; + if (file.thumbUrl) return file.thumbUrl; + if (file.originFileObj && typeof URL !== 'undefined' && URL.createObjectURL) { + return URL.createObjectURL(file.originFileObj); + } + return ''; +}; + +const serializeUploadFileList = (list) => + (list || []).map((f) => ({ + uid: f.uid, + name: f.name, + url: getUploadPreviewUrl(f) || '', + status: f.status || 'done', + })); + +const deserializeUploadFileList = (list) => + (list || []).map((f) => ({ + uid: f.uid || `file-${f.name}-${Math.random().toString(36).slice(2, 8)}`, + name: f.name, + url: f.url, + thumbUrl: f.url, + status: f.status || 'done', + })); + +const syncLicenseToCertificateManagement = (task, licenseForm, operator) => { + if (!task?.plateNo) return null; + const photos = serializeUploadFileList(licenseForm?.photos || []); + const payload = { + plateNo: task.plateNo, + vin: task.vin || '', + inspectionValidUntil: licenseForm?.inspectionValidUntil || '', + photos, + photoCount: photos.length, + operator: operator || MOCK_CURRENT_HANDLER, + source: 'annual_review', + syncedAt: new Date().toISOString(), + }; + try { + const store = JSON.parse(localStorage.getItem(CERTIFICATE_LICENSE_SYNC_KEY) || '{}'); + store[task.plateNo] = payload; + localStorage.setItem(CERTIFICATE_LICENSE_SYNC_KEY, JSON.stringify(store)); + } catch { + /* ignore */ + } + return payload; +}; + +const platesMatch = (a, b) => { + const na = (a || '').replace(/\s/g, '').toUpperCase(); + const nb = (b || '').replace(/\s/g, '').toUpperCase(); + return na.length > 0 && na === nb; +}; + +const getUploadFileName = (file) => + String(file?.name || file?.originFileObj?.name || '').toLowerCase(); + +/** 模拟单张行驶证 OCR 车牌(文件名含 mismatch / 不一致 可测失败) */ +const mockOcrLicensePlateForFile = (task, file) => { + const name = getUploadFileName(file); + if (name.includes('mismatch') || name.includes('不一致')) return '粤B00000D'; + return task?.plateNo || ''; +}; + +/** 模拟单张是否识别到检验有效期至(无有效期:文件名含 novalid / 无有效期) */ +const mockOcrLicenseValidUntilForFile = (task, file, index) => { + const name = getUploadFileName(file); + if (name.includes('novalid') || name.includes('无有效期')) return null; + const base = task?.expireDate || '2026-07-20'; + if (moment) { + return moment(base).add(index, 'month').endOf('month').format('YYYY-MM-DD'); + } + const parts = String(base).split('-'); + const year = Number(parts[0]) || new Date().getFullYear(); + const month = (Number(parts[1]) || 1) + index; + const day = new Date(year, month, 0).getDate(); + return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`; +}; + +/** 批量 OCR:逐张校验车牌;多张均有有效期时取最后识别的一条 */ +const runLicenseOcrResult = (task, photos) => { + const list = photos || []; + const expectedPlate = task?.plateNo || ''; + for (let i = 0; i < list.length; i++) { + const recognizedPlate = mockOcrLicensePlateForFile(task, list[i]); + if (!platesMatch(recognizedPlate, expectedPlate)) { + return { ok: false }; + } + } + let lastValidUntil = null; + list.forEach((file, index) => { + const validUntil = mockOcrLicenseValidUntilForFile(task, file, index); + if (validUntil) lastValidUntil = validUntil; + }); + return { ok: true, validUntil: lastValidUntil }; +}; + +const readInitialTaskId = () => { + try { + return sessionStorage.getItem(AR_TASK_ID_STORAGE_KEY) || ''; + } catch { + return ''; + } +}; + +const PAGE_STYLES = ` +.ar-handle{min-height:100vh;background:#f2f3f5;font-family:Inter,Helvetica,PingFang SC,Microsoft YaHei,Arial,sans-serif} +.ar-handle-inner{max-width:1200px;margin:0 auto;padding:16px 24px 96px} +.ar-handle-top{display:flex;justify-content:flex-end;align-items:center;gap:12px;margin-bottom:12px;min-height:32px} +.ar-handle-top-extra{margin-right:auto} +.ar-form-group{background:#fff;border:1px solid #e5e6eb;border-radius:4px;margin-bottom:16px} +.ar-form-group-head{display:flex;align-items:center;justify-content:space-between;padding:14px 20px;border-bottom:1px solid #f2f3f5} +.ar-form-group-title{font-size:14px;font-weight:600;color:#1d2129} +.ar-form-group-body{padding:20px 20px 8px} +.ar-form-group-body .ant-form-item{margin-bottom:20px} +.ar-form-group-body .ant-form-item-label>label{color:#4e5969;font-size:13px} +.ar-form-label-hint{font-size:12px;font-weight:400;color:#86909c;margin-left:8px} +.ar-form-footer{position:fixed;left:0;right:0;bottom:0;z-index:100;background:#fff;border-top:1px solid #e5e6eb;box-shadow:0 -2px 8px rgba(0,0,0,.06)} +.ar-form-footer-inner{max-width:1200px;margin:0 auto;padding:12px 24px;display:flex;justify-content:flex-end;gap:12px} +.ar-ocr-banner{padding:10px 12px;border-radius:4px;font-size:13px;margin-bottom:16px} +.ar-ocr-banner--error{background:#ffece8;border:1px solid #ffccc7;color:#cb2634} +.ar-ocr-banner--done{background:#e8ffea;border:1px solid #aff0b5;color:#009a29} +.ar-ocr-mask{position:fixed;inset:0;z-index:200;background:rgba(29,33,41,.45);display:flex;align-items:center;justify-content:center} +.ar-ocr-mask-panel{background:#fff;border-radius:8px;padding:32px 48px;text-align:center} +.ar-prd-doc{max-height:65vh;overflow-y:auto;font-size:13px;line-height:1.65;color:#4e5969} +.ar-prd-highlight{background:#f0f9ff;border:1px solid #bedaff;border-radius:4px;padding:12px;margin:12px 0} +.ar-prd-h2{font-size:15px;font-weight:600;color:#1d2129;margin:16px 0 8px} +.ar-prd-h3{font-size:14px;font-weight:600;color:#1d2129;margin:12px 0 6px} +.ar-prd-ul{margin:0;padding-left:20px} +.ar-prd-ul li{margin-bottom:6px} +.ar-photo-block{width:100%} +.ar-photo-row{display:flex;align-items:flex-start;gap:12px} +.ar-photo-upload-fixed{flex-shrink:0;width:104px} +.ar-photo-upload-cell{display:block;width:100%} +.ar-photo-upload-cell .ant-upload{display:block;width:100%} +.ar-photo-upload-cell .ant-upload-select{display:block!important;width:100%!important;height:auto!important;margin:0!important;border:none!important;background:transparent!important} +.ar-photo-list-scroll{flex:1;min-width:0;display:flex;flex-wrap:wrap;gap:10px;align-content:flex-start} +.ar-photo-slot{position:relative;width:104px;height:104px;border-radius:8px;box-sizing:border-box;flex-shrink:0} +.ar-photo-slot--add{display:flex;flex-direction:column;align-items:center;justify-content:center;border:1px dashed #c9cdd4;background:#fafafa;color:#86909c;cursor:pointer;gap:2px;transition:border-color .2s,background .2s} +.ar-photo-slot--add:hover{border-color:#165dff;background:#f0f5ff;color:#165dff} +.ar-photo-add-icon{font-size:22px;line-height:1;font-weight:300} +.ar-photo-add-text{font-size:12px;line-height:1.2} +.ar-photo-item{overflow:visible} +.ar-photo-item-thumb{width:100%;height:100%;border-radius:8px;overflow:hidden;border:none;padding:0;background:#f2f3f5;cursor:pointer;display:block} +.ar-photo-item-thumb img{width:100%;height:100%;object-fit:cover;display:block} +.ar-photo-del{position:absolute;top:-6px;right:-6px;z-index:2;width:20px;height:20px;border-radius:50%;border:none;background:rgba(29,33,41,.72);color:#fff;font-size:14px;line-height:1;cursor:pointer;padding:0;display:flex;align-items:center;justify-content:center} +.ar-photo-del:hover{background:#cb2634} +.ar-photo-hint{margin-top:8px;font-size:12px;color:#86909c;line-height:1.4} +.ar-photo-preview-img{max-width:100%;max-height:70vh;object-fit:contain} +`; + +/** 照片上传:左固定上传、右照片列表,支持批量(对齐小程序 PhotoUploadBlock) */ +const PhotoUploadBlock = ({ fileList, onChange, maxPhotos = MAX_SERVICE_PHOTOS }) => { + const list = fileList || []; + const [previewOpen, setPreviewOpen] = useState(false); + const [previewUrl, setPreviewUrl] = useState(''); + const canUpload = list.length < maxPhotos; + + const handleChange = ({ fileList: nextList }) => { + onChange((nextList || []).slice(0, maxPhotos)); + }; + + const handleRemove = (uid, e) => { + e.stopPropagation(); + e.preventDefault(); + onChange(list.filter((f) => f.uid !== uid)); + }; + + const handlePreview = (file) => { + const url = getUploadPreviewUrl(file); + if (!url) return; + if (Image && typeof Image.preview === 'function') { + Image.preview({ src: url }); + return; + } + setPreviewUrl(url); + setPreviewOpen(true); + }; + + return ( +
+
+
+ {canUpload ? ( + false} + onChange={handleChange} + > +
+ + + 上传 +
+
+ ) : ( +
+ + + 已满 +
+ )} +
+
+ {list.map((file) => { + const url = getUploadPreviewUrl(file); + return ( +
+ + +
+ ); + })} +
+
+
最多上传 {maxPhotos} 张,支持批量选择
+ setPreviewOpen(false)} + centered + width={720} + destroyOnClose + title="照片预览" + > + {previewUrl ? 预览 : null} + +
+ ); +}; + +const FormGroup = ({ title, extra, children }) => ( +
+
+ {title} + {extra} +
+
{children}
+
+); + +const Component = function AnnualReviewHandlePage() { + const [tasks, setTasks] = useState(() => loadTasksFromStorage()); + const [taskId, setTaskId] = useState(() => { + const saved = readInitialTaskId(); + const pending = loadTasksFromStorage().filter((t) => t.tab === 'pending'); + if (saved && pending.some((t) => t.id === saved)) return saved; + return pending[0]?.id || ''; + }); + const [prdOpen, setPrdOpen] = useState(false); + const [inspectionForm, setInspectionForm] = useState({ ...EMPTY_INSPECTION_FORM }); + const [licenseForm, setLicenseForm] = useState({ ...EMPTY_LICENSE_FORM }); + const [m2Expanded, setM2Expanded] = useState(false); + const [m2Form, setM2Form] = useState({ ...EMPTY_SERVICE_FORM }); + const [zbExpanded, setZbExpanded] = useState(false); + const [zbForm, setZbForm] = useState({ ...EMPTY_SERVICE_FORM }); + const [operateDrafts, setOperateDrafts] = useState(() => loadOperateDrafts()); + const licenseOcrTimerRef = useRef(null); + + const pendingTasks = useMemo(() => tasks.filter((t) => t.tab === 'pending'), [tasks]); + const operateTask = useMemo( + () => pendingTasks.find((t) => t.id === taskId) || pendingTasks[0] || null, + [pendingTasks, taskId] + ); + + const inspectionStationOptions = useMemo( + () => readInspectionStationList().map((s) => ({ label: s, value: s })), + [] + ); + const repairStationOptions = useMemo(() => REPAIR_STATION_LIST.map((s) => ({ label: s, value: s })), []); + + const resetOperateForms = () => { + if (licenseOcrTimerRef.current) { + clearTimeout(licenseOcrTimerRef.current); + licenseOcrTimerRef.current = null; + } + setInspectionForm({ ...EMPTY_INSPECTION_FORM }); + setLicenseForm({ ...EMPTY_LICENSE_FORM }); + setM2Expanded(false); + setM2Form({ ...EMPTY_SERVICE_FORM }); + setZbExpanded(false); + setZbForm({ ...EMPTY_SERVICE_FORM }); + }; + + const applyOperateDraft = (draft) => { + if (!draft) return; + setInspectionForm({ ...EMPTY_INSPECTION_FORM, ...draft.inspectionForm }); + const lf = draft.licenseForm || {}; + setLicenseForm({ + ...EMPTY_LICENSE_FORM, + photos: deserializeUploadFileList(lf.photos), + inspectionValidUntil: lf.inspectionValidUntil ?? null, + ocrStatus: lf.ocrStatus === 'recognizing' ? 'idle' : lf.ocrStatus || 'idle', + }); + setM2Expanded(!!draft.m2Expanded); + const m2 = draft.m2Form || {}; + setM2Form({ ...EMPTY_SERVICE_FORM, ...m2, photos: deserializeUploadFileList(m2.photos) }); + setZbExpanded(!!draft.zbExpanded); + const zb = draft.zbForm || {}; + setZbForm({ ...EMPTY_SERVICE_FORM, ...zb, photos: deserializeUploadFileList(zb.photos) }); + }; + + const collectOperateDraft = () => ({ + savedAt: new Date().toISOString(), + inspectionForm: { ...inspectionForm }, + licenseForm: { + photos: serializeUploadFileList(licenseForm.photos), + inspectionValidUntil: licenseForm.inspectionValidUntil, + ocrStatus: licenseForm.ocrStatus === 'recognizing' ? 'idle' : licenseForm.ocrStatus, + }, + m2Expanded, + m2Form: { ...m2Form, photos: serializeUploadFileList(m2Form.photos) }, + zbExpanded, + zbForm: { ...zbForm, photos: serializeUploadFileList(zbForm.photos) }, + }); + + const upsertOperateDraft = (id, draft) => { + const next = { ...operateDrafts, [id]: draft }; + setOperateDrafts(next); + persistOperateDrafts(next); + }; + + const removeOperateDraft = (id) => { + if (!operateDrafts[id]) return; + const next = { ...operateDrafts }; + delete next[id]; + setOperateDrafts(next); + persistOperateDrafts(next); + }; + + useEffect(() => { + if (!operateTask) return; + const draft = operateDrafts[operateTask.id]; + if (draft) applyOperateDraft(draft); + else resetOperateForms(); + }, [operateTask?.id]); + + const runLicenseOcr = (task, photos) => { + if (licenseOcrTimerRef.current) clearTimeout(licenseOcrTimerRef.current); + setLicenseForm((p) => ({ ...p, ocrStatus: 'recognizing', inspectionValidUntil: null })); + licenseOcrTimerRef.current = setTimeout(() => { + const result = runLicenseOcrResult(task, photos); + if (!result.ok) { + setLicenseForm({ ...EMPTY_LICENSE_FORM }); + message.error('行驶证车牌号不一致,请检查后重新上传'); + licenseOcrTimerRef.current = null; + return; + } + setLicenseForm((p) => ({ + ...p, + photos: photos || [], + inspectionValidUntil: result.validUntil, + ocrStatus: result.validUntil ? 'done' : 'idle', + })); + if (result.validUntil) message.success('检验有效期识别完成'); + licenseOcrTimerRef.current = null; + }, LICENSE_OCR_MOCK_MS); + }; + + const handleLicensePhotosChange = (fileList) => { + const list = (fileList || []).slice(0, MAX_LICENSE_PHOTOS); + if (!list.length) { + if (licenseOcrTimerRef.current) { + clearTimeout(licenseOcrTimerRef.current); + licenseOcrTimerRef.current = null; + } + setLicenseForm({ ...EMPTY_LICENSE_FORM }); + return; + } + setLicenseForm((p) => ({ ...p, photos: list })); + if (operateTask) runLicenseOcr(operateTask, list); + }; + + const handleReset = () => { + if (!operateTask) return; + resetOperateForms(); + message.info('已重置当前表单'); + }; + + const handleSave = () => { + if (!operateTask) return; + upsertOperateDraft(operateTask.id, collectOperateDraft()); + persistTasksToStorage(tasks); + navigateToAnnualReviewList('已保存,已返回列表'); + }; + + const handleSubmit = () => { + if (!operateTask) return; + if (licenseForm.ocrStatus === 'recognizing') { + message.warning('行驶证识别中,请稍候'); + return; + } + if (!validateLicenseForm(licenseForm)) return; + if (isFormCostEmpty(inspectionForm.cost)) { + message.error('请填写检测费用'); + return; + } + if (!validateStationCost(m2Form, '二保')) return; + if (!validateStationCost(zbForm, '整备')) return; + + const snapshot = collectOperateDraft(); + const completeTime = moment + ? moment().format('YYYY-MM-DD HH:mm') + : new Date().toISOString().slice(0, 16).replace('T', ' '); + const taskIdCurrent = operateTask.id; + + removeOperateDraft(taskIdCurrent); + syncLicenseToCertificateManagement(operateTask, licenseForm, MOCK_CURRENT_HANDLER); + + const nextTasks = tasks.map((t) => + t.id === taskIdCurrent + ? { + ...t, + tab: 'history', + executor: MOCK_CURRENT_HANDLER, + executeTime: completeTime, + expireDate: licenseForm.inspectionValidUntil || t.expireDate, + operateSnapshot: snapshot, + } + : t + ); + setTasks(nextTasks); + persistTasksToStorage(nextTasks); + + navigateToAnnualReviewList( + '提交成功:新行驶证照片与检验有效期至已全量覆盖至车辆证照,任务已进入历史记录' + ); + }; + + const licenseRecognizing = licenseForm.ocrStatus === 'recognizing'; + + const pageInner = ( +
+ +
+
+ {operateTask && operateDrafts[operateTask.id] ? ( + + 已保存 + + ) : null} + +
+ + {!operateTask ? ( + + ) : ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {licenseForm.ocrStatus === 'done' && licenseForm.inspectionValidUntil && ( +
+ 已根据行驶证照片识别检验有效期(精确到月,已自动补全至月末) +
+ )} + + + + 行驶证照片 + + 提交后将自动更新年审照片到车辆证照(支持最多 4 张照片上传) + + + } + > + + + + + + + setLicenseForm((p) => ({ ...p, inspectionValidUntil: dateStr || null })) + } + disabled={licenseRecognizing} + placeholder="上传照片自动识别" + /> + + + +
+ + + + + +