e1e0d48eb7ab2b305e6062f23610f8be9db3bed6..600fe725775e28af1f86f7f7f1e2521f494252b1
2026-09-01 liu
feat(mes): 新增管理驾驶舱页面
600fe7 对比 | 目录
2026-09-01 云
feat(mes): 追溯功能新增消费者扫码分析和窜货预警模块
10dd65 对比 | 目录
已修改3个文件
已添加23个文件
3080 ■■■■■ 文件已修改
package.json 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pnpm-lock.yaml 497 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/management-cockpit/index.ts 158 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/trace-consumer-scan/index.ts 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/trace/config.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/trace/crossregion/index.ts 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/trace/integrity/index.ts 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/management-cockpit/chart-options.ts 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/management-cockpit/index.vue 207 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/management-cockpit/modules/inventory-chart.vue 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/management-cockpit/modules/kpi-cards.vue 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/management-cockpit/modules/outbound-chart.vue 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/management-cockpit/modules/production-trend.vue 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/management-cockpit/modules/quality-chart.vue 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/management-cockpit/modules/trace-chart.vue 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/components/qr-trace-modal.vue 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/trace-consumer-scan/chart-options.ts 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/trace-consumer-scan/data.ts 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/trace-consumer-scan/index.vue 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/trace-consumer-scan/modules/distribution-chart.vue 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/trace-consumer-scan/modules/summary-cards.vue 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/trace-consumer-scan/modules/trend-chart.vue 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/trace/crossregion/data.ts 165 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/trace/crossregion/index.vue 375 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/trace/integrity/data.ts 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/trace/integrity/index.vue 229 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json
@@ -64,6 +64,8 @@
    "fast-xml-parser": "catalog:",
    "gsap": "^3.15.0",
    "highlight.js": "catalog:",
    "html-to-image": "^1.11.13",
    "jspdf": "^4.2.1",
    "livekit-client": "^2.20.0",
    "pinia": "catalog:",
    "sortablejs": "catalog:",
pnpm-lock.yaml
@@ -119,7 +119,7 @@
      version: 7.0.0
    '@types/codemirror':
      specifier: ^5.60.17
      version: 5.60.17
      version: 5.60.18
    '@types/crypto-js':
      specifier: ^4.2.2
      version: 4.2.2
@@ -134,7 +134,7 @@
      version: 4.5.9
    '@types/markdown-it':
      specifier: ^14.1.2
      version: 14.1.2
      version: 14.2.0
    '@types/node':
      specifier: ^25.9.4
      version: 25.9.4
@@ -653,6 +653,12 @@
      highlight.js:
        specifier: 'catalog:'
        version: 11.11.1
      html-to-image:
        specifier: ^1.11.13
        version: 1.11.13
      jspdf:
        specifier: ^4.2.1
        version: 4.2.1
      livekit-client:
        specifier: ^2.20.0
        version: 2.20.0(@types/dom-mediacapture-record@1.0.22)
@@ -1625,10 +1631,10 @@
    devDependencies:
      '@types/codemirror':
        specifier: 'catalog:'
        version: 5.60.17
        version: 5.60.18
      '@types/markdown-it':
        specifier: 'catalog:'
        version: 14.1.2
        version: 14.2.0
  src/packages/effects/request:
    dependencies:
@@ -3144,17 +3150,14 @@
  '@jridgewell/source-map@0.3.11':
    resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==}
  '@jridgewell/sourcemap-codec@1.4.10':
    resolution: {integrity: sha512-Ht8wIW5v165atIX1p+JvKR5ONzUyF4Ac8DZIQ5kZs9zrb6M8SJNXpx1zn04rn65VjBMygRoMXcyYwNK0fT7bEg==}
  '@jridgewell/sourcemap-codec@1.4.14':
    resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==}
  '@jridgewell/sourcemap-codec@1.5.0':
    resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
  '@jridgewell/sourcemap-codec@1.5.5':
    resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
  '@jridgewell/sourcemap-codec@1.6.0':
    resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==}
  '@jridgewell/trace-mapping@0.3.31':
    resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
@@ -4419,8 +4422,8 @@
  '@types/archiver@7.0.0':
    resolution: {integrity: sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==}
  '@types/codemirror@5.60.17':
    resolution: {integrity: sha512-AZq2FIsUHVMlp7VSe2hTfl5w4pcUkoFkM3zVsRKsn1ca8CXRDYvnin04+HP2REkwsxemuHqvDofdlhUWNpbwfw==}
  '@types/codemirror@5.60.18':
    resolution: {integrity: sha512-aSBOPXH2PXRYixxUpVP1sJ5+S0vEfKDvR+lABZ7Kju/9Qb0SSbZk72NihWaKoQbANyibs92q4DBfYasywxnNkA==}
  '@types/conventional-commits-parser@5.0.2':
    resolution: {integrity: sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==}
@@ -4476,8 +4479,8 @@
  '@types/lodash@4.17.24':
    resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==}
  '@types/markdown-it@14.1.2':
    resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
  '@types/markdown-it@14.2.0':
    resolution: {integrity: sha512-NoQ2yGlLWj4wpxMs+TYmRKk3thDrQ97agr7sFqfLsAlvoS8SNQuTrlObhFqG9iugdTtgOE9jpJ6FNM4ZGsa5xQ==}
  '@types/mdurl@2.0.0':
    resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
@@ -4497,11 +4500,17 @@
  '@types/nprogress@0.2.3':
    resolution: {integrity: sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==}
  '@types/pako@2.0.0':
    resolution: {integrity: sha512-10+iaz93qR5WYxTo+PMifD5TSxiOtdRaxBf7INGGXMQgTCu8Z/7GYWYFUOS3q/G0nE5boj1r4FEB+WSy7s5gbA==}
  '@types/qrcode@1.5.6':
    resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==}
  '@types/qs@6.15.1':
    resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==}
  '@types/raf@3.4.3':
    resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==}
  '@types/readdir-glob@1.1.5':
    resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==}
@@ -4743,8 +4752,8 @@
  '@vue/devtools-api@6.5.0':
    resolution: {integrity: sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==}
  '@vue/devtools-api@7.5.2':
    resolution: {integrity: sha512-VxPbAQxJrYSIkoGVvQ2oOoKW8u4CMpvRLySTxhoJA38z8bQEGy9GO33eoRY/DulJbSFRfjZFNvH+dh8B4qpesQ==}
  '@vue/devtools-api@7.7.10':
    resolution: {integrity: sha512-KxtEpUOOpFz/qOGRrAwA36QF7DqIA+FXgCYit9mk9wjbaZt0sXOFz81ElOZtKA4HbWHUdwNjZHBFsFFyp5BZiA==}
  '@vue/devtools-api@7.7.7':
    resolution: {integrity: sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==}
@@ -4757,8 +4766,8 @@
    peerDependencies:
      vue: ^3.0.0
  '@vue/devtools-kit@7.5.2':
    resolution: {integrity: sha512-0leUOE2HBfl8sHf9ePKzxqnCFskkU22tWWqd9OfeSlslAKE30/TViYvWcF4vgQmPlJnAAdHU0WfW5dYlCeOiuw==}
  '@vue/devtools-kit@7.7.10':
    resolution: {integrity: sha512-3WNi2Kq4tbpVbmhml7RiphmAt0279oh3fKNeWMQIrltfX8Q91b4i5PL8DtyNKdwmcsGrV4fg+erwWOmD05CLIw==}
  '@vue/devtools-kit@7.7.7':
    resolution: {integrity: sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==}
@@ -4766,11 +4775,8 @@
  '@vue/devtools-kit@8.1.3':
    resolution: {integrity: sha512-cRn7GXiCQkMYU2Z3h3pM4YO/ndbx9FY1yLDAqIqPLcmIq4H6zAOJHein6tvZU3AfPwgrodqLiPBEF+YQaS8AxA==}
  '@vue/devtools-shared@7.5.2':
    resolution: {integrity: sha512-+zmcixnD6TAo+zwm30YuwZckhL9iIi4u+gFwbq9C8zpm3SMndTlEYZtNhAHUhOXB+bCkzyunxw80KQ/T0trF4w==}
  '@vue/devtools-shared@7.7.7':
    resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==}
  '@vue/devtools-shared@7.7.10':
    resolution: {integrity: sha512-wOPslzB8vTvpxwdaOcR2qAbwmuSP0L+rhpoC6Cf56V3Jip+HWb7PQQXOUPgBNQARpXsbQX/+mvi8kKucmBGRwQ==}
  '@vue/devtools-shared@8.1.3':
    resolution: {integrity: sha512-CM3uIPL+v+lrJUk33+pxspYo0MhuMWlCvf7zC9fybifvCPyM2jUbYRPwoYEJgYbwRqPikm5HozbUhp60MF2QuA==}
@@ -4946,8 +4952,8 @@
    resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
    engines: {node: '>=8'}
  ansi-regex@6.2.2:
    resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
  ansi-regex@6.3.0:
    resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==}
    engines: {node: '>=12'}
  ansi-styles@4.3.0:
@@ -5147,6 +5153,10 @@
  bare-url@2.4.5:
    resolution: {integrity: sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==}
  base64-arraybuffer@1.0.2:
    resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
    engines: {node: '>= 0.6.0'}
  base64-js@1.5.1:
    resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
@@ -5164,9 +5174,6 @@
  bindings@1.5.0:
    resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
  birpc@0.2.19:
    resolution: {integrity: sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==}
  birpc@2.3.0:
    resolution: {integrity: sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g==}
@@ -5306,6 +5313,10 @@
  caniuse-lite@1.0.30001799:
    resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==}
  canvg@3.0.11:
    resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==}
    engines: {node: '>=10.0.0'}
  chalk@4.1.2:
    resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
    engines: {node: '>=10'}
@@ -5400,8 +5411,8 @@
  color-name@1.1.4:
    resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
  colord@2.9.3:
    resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
  colord@2.10.0:
    resolution: {integrity: sha512-AidJptpBJmjTclAp9BkLwJi0T93fo5epJnbaZslpg6QVzpHjAiveF55mE9AcUJiGMqRHgMDY8soMsQtuNYMHfw==}
  colorjs.io@0.5.0:
    resolution: {integrity: sha512-qekjTiBLM3F/sXKks/ih5aWaHIGu+Ftel0yKEvmpbKvmxpNOhojKgha5uiWEUOqEpRjC1Tq3nJRT7WgdBOxIGg==}
@@ -5577,6 +5588,7 @@
  crypto-js@4.2.0:
    resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
    deprecated: Active development of CryptoJS has been discontinued. This library is no longer maintained.
  crypto-random-string@2.0.0:
    resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
@@ -5585,6 +5597,9 @@
  css-functions-list@3.3.3:
    resolution: {integrity: sha512-8HFEBPKhOpJPEPu70wJJetjKta86Gw9+CCyCnB3sui2qQfOvRyqBy4IKLKKAwdMpWb2lHXWk9Wb4Z6AmaUT1Pg==}
    engines: {node: '>=12'}
  css-line-break@2.1.0:
    resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
  css-select@5.1.0:
    resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
@@ -5823,8 +5838,8 @@
    resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==}
    engines: {node: '>=18'}
  default-browser@5.5.0:
    resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==}
  default-browser@5.5.1:
    resolution: {integrity: sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==}
    engines: {node: '>=18'}
  default-passive-events@2.0.0:
@@ -5942,8 +5957,8 @@
  dot-case@3.0.4:
    resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
  dot-prop@10.1.0:
    resolution: {integrity: sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q==}
  dot-prop@10.2.0:
    resolution: {integrity: sha512-BTJ9aZYL3vCfZlZOBLy9v8TUqWGQ0pzFnygKwFZt5udj6viBoFIBviKPUoZLDCPn1FoXffv6McQFDenrm5Krfw==}
    engines: {node: '>=20'}
  dot-prop@5.3.0:
@@ -6049,8 +6064,8 @@
  error-stack-parser-es@1.0.5:
    resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==}
  errx@0.1.0:
    resolution: {integrity: sha512-fZmsRiDNv07K6s2KkKFTiD2aIvECa7++PKyD5NC32tpRw46qZA3sOz+aM+/V9V0GDHxVTKLziveV4JhzBHDp9Q==}
  errx@0.1.2:
    resolution: {integrity: sha512-chfpPHmCerdo/rXr/nNvPZRkV4WwDRwzwnsJ0Uzz3tVi8Z41tDctRjduYy1138ii77AFlts1qvWtX3g/Acg91Q==}
  es-abstract-get@1.0.0:
    resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==}
@@ -6265,11 +6280,6 @@
    resolution: {integrity: sha512-r5EQJcYZ2oaGbeR0jR0fFVijGOcwai07/690YRXLINuhmVeRY4UKSAsQPe/0BNuDgwP7Ophoc1PRsr2E3tkbdQ==}
    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
  esprima@2.7.3:
    resolution: {integrity: sha512-OarPfz0lFCiW4/AV2Oy1Rp9qu0iusTKqykwTspGCZtPxmF81JR4MmIebvF1F9+UOKth2ZubLQ4XGGaU+hSn99A==}
    engines: {node: '>=0.10.0'}
    hasBin: true
  esprima@4.0.1:
    resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
    engines: {node: '>=4'}
@@ -6349,6 +6359,9 @@
  fast-levenshtein@2.0.6:
    resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
  fast-png@6.2.0:
    resolution: {integrity: sha512-fO4DewoEd9WwuP8DQcfj8Tlc88Jno6lJAjlDYzvJSqMIZwxUpRT4zuzPXgqygjJqngBdCbeQRaL/FVz3InExhA==}
  fast-uri@3.1.2:
    resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==}
@@ -6676,6 +6689,10 @@
    resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
    engines: {node: '>=12.0.0'}
  highlight.js@11.12.0:
    resolution: {integrity: sha512-nbfWpyRMcMrPMmDwJB+dhX/eiaPKtc2RB+0QZskqJ3WjRA/FDS0e9hZrx8EC/lbEv8gXy98FcDbNa/dspAaJMg==}
    engines: {node: '>=12.0.0'}
  hookable@5.5.3:
    resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
@@ -6711,6 +6728,13 @@
  html-tags@5.1.0:
    resolution: {integrity: sha512-n6l5uca7/y5joxZ3LUePhzmBFUJ+U2YWzhMa8XUTecSeSlQiZdF5XAd/Q3/WUl0VsXgUwWi8I7CNIwdI5WN1SQ==}
    engines: {node: '>=20.10'}
  html-to-image@1.11.13:
    resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==}
  html2canvas@1.4.1:
    resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==}
    engines: {node: '>=8.0.0'}
  htmlparser2@10.1.0:
    resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==}
@@ -6764,10 +6788,6 @@
  ignore@5.3.2:
    resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
    engines: {node: '>= 4'}
  ignore@7.0.5:
    resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
    engines: {node: '>= 4'}
  ignore@7.0.6:
@@ -6827,6 +6847,9 @@
  internmap@2.0.3:
    resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
    engines: {node: '>=12'}
  iobuffer@5.4.0:
    resolution: {integrity: sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==}
  ioredis@5.11.1:
    resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==}
@@ -6966,8 +6989,8 @@
    resolution: {integrity: sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==}
    engines: {node: '>=0.10.0'}
  is-plain-object@5.0.0:
    resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
  is-plain-object@5.1.0:
    resolution: {integrity: sha512-bUi/yjmtKYcRVUtWRGr0UA6xEFh2I6zWUwMrUXB3s7bmYCaZ8a+0ZsTRkrawh/mzlSD1Y0Ph8bp/U+TvBpWDNw==}
    engines: {node: '>=0.10.0'}
  is-reference@1.2.1:
@@ -7101,8 +7124,8 @@
  js-tokens@9.0.1:
    resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
  js-yaml@3.6.1:
    resolution: {integrity: sha512-BLv3oxhfET+w5fjPwq3PsAsxzi9i3qzU//HMpWVz0A6KplF86HdR9x2TGnv9DXhSUrO7LO8czUiTd3yb3mLSvg==}
  js-yaml@3.15.2:
    resolution: {integrity: sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==}
    hasBin: true
  js-yaml@4.2.0:
@@ -7168,6 +7191,9 @@
  jsonpointer@5.0.1:
    resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==}
    engines: {node: '>=0.10.0'}
  jspdf@4.2.1:
    resolution: {integrity: sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==}
  katex@0.16.47:
    resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==}
@@ -7492,8 +7518,8 @@
  mdn-data@2.28.1:
    resolution: {integrity: sha512-U9w+PzSZ00Z5m9rZ5ARVFL5xOfuCHdKYi/1RRwDCJsboFgJDNT3zT6PIPD7mZQYaQLhsZM3GfDRgSMRHhSmVng==}
  mdurl@2.0.0:
    resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
  mdurl@2.1.0:
    resolution: {integrity: sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==}
  meow@12.0.1:
    resolution: {integrity: sha512-/QOqMALNoKQcJAOOdIXjNLtfcCdLXbMFyB1fOOPdm6RzfBTlsuodOCTBDjVbeUSmgDQb8UI2oONqYGtq1PKKKA==}
@@ -7669,8 +7695,8 @@
    resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
    hasBin: true
  node-mock-http@1.0.4:
    resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==}
  node-mock-http@1.0.5:
    resolution: {integrity: sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw==}
  node-releases@2.0.48:
    resolution: {integrity: sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==}
@@ -7736,8 +7762,8 @@
  ofetch@1.5.1:
    resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==}
  ohash@2.0.11:
    resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
  ohash@2.0.12:
    resolution: {integrity: sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==}
  on-finished@2.4.1:
    resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
@@ -7747,8 +7773,8 @@
    resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
    engines: {node: '>=18'}
  open@11.0.0:
    resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==}
  open@11.0.2:
    resolution: {integrity: sha512-RWqF+pBSkqecEvCKOn8QYhaNdRMJDZRIrlS/7rTDdLHaPcfXGCZ/h8zb413NfvdeAV0MR7T1yJcA34/q+CSm1Q==}
    engines: {node: '>=20'}
  optionator@0.9.4:
@@ -7823,8 +7849,8 @@
  package-json-from-dist@1.0.1:
    resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
  package-manager-detector@1.6.0:
    resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
  package-manager-detector@1.8.0:
    resolution: {integrity: sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==}
  pako@2.2.0:
    resolution: {integrity: sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==}
@@ -7915,6 +7941,9 @@
  perfect-debounce@2.1.0:
    resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==}
  performance-now@2.1.0:
    resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
  picocolors@1.1.1:
    resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -8034,6 +8063,10 @@
    resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==}
    engines: {node: '>=20'}
  powershell-utils@0.2.1:
    resolution: {integrity: sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==}
    engines: {node: '>=20'}
  preact@10.29.2:
    resolution: {integrity: sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==}
@@ -8049,12 +8082,12 @@
    resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==}
    engines: {node: ^14.13.1 || >=16.0.0}
  pretty-bytes@7.1.0:
    resolution: {integrity: sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==}
  pretty-bytes@7.1.2:
    resolution: {integrity: sha512-spZIOSORH9joSPGN0L/tYfvINH+xbOpnI3kdcEkq0mzGMjFvDZzyRVbqEOV0wACF0gggdKE9RWO2mosGsBh/Rg==}
    engines: {node: '>=20'}
  pretty-ms@9.3.0:
    resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==}
  pretty-ms@9.3.1:
    resolution: {integrity: sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA==}
    engines: {node: '>=18'}
  prismjs@1.30.0:
@@ -8071,11 +8104,11 @@
  prosemirror-changeset@2.4.1:
    resolution: {integrity: sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==}
  prosemirror-commands@1.7.1:
    resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==}
  prosemirror-commands@1.7.2:
    resolution: {integrity: sha512-q6Q6szxqdu9Xd6EcdKsqXghu5nQdZTpB4Q9yd04WRc7/jt763e/rT60Owh0L1GYY+T46o5rD+9lEN36dZS43tw==}
  prosemirror-dropcursor@1.8.2:
    resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==}
  prosemirror-dropcursor@1.8.3:
    resolution: {integrity: sha512-FoYbsJR8gK+DGlqhNoE29Loa38eIZPzQRIb1VMaDNBoo4OLP6vVof/jR8qFY/6XvUd6Dhug8MDCHl2a/h8RTfQ==}
  prosemirror-gapcursor@1.4.1:
    resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==}
@@ -8151,11 +8184,14 @@
  radix3@1.1.2:
    resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==}
  raf@3.4.1:
    resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==}
  randomcolor@0.6.2:
    resolution: {integrity: sha512-Mn6TbyYpFgwFuQ8KJKqf3bqqY9O1y37/0jgSK/61PUxV4QfIMv0+K2ioq8DfOjkBslcjwSzRfIDEXfzA9aCx7A==}
  range-parser@1.2.1:
    resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
  range-parser@1.3.0:
    resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==}
    engines: {node: '>= 0.6'}
  rc9@3.0.1:
@@ -8213,6 +8249,9 @@
  regenerate@1.4.2:
    resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==}
  regenerator-runtime@0.13.11:
    resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
  regexp.prototype.flags@1.5.4:
    resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
@@ -8282,6 +8321,10 @@
  rfdc@1.4.1:
    resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
  rgbcolor@1.0.1:
    resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==}
    engines: {node: '>= 0.8.15'}
  rimraf@5.0.5:
    resolution: {integrity: sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==}
@@ -8704,6 +8747,10 @@
  sprintf-js@1.0.3:
    resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
  stackblur-canvas@2.0.0:
    resolution: {integrity: sha512-660gH1SpjeKyfUYnne9nuIya7CDGds6NdIrxzOToSgaSlOqbh7UYGP9VlxlQ8IX7af/nPuAHLkN03l84OesX7Q==}
    deprecated: Fixed build bug in 2.2.0 as not converting ES6 source for older browsers
  standard-as-callback@2.1.0:
    resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
@@ -8749,8 +8796,8 @@
    resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==}
    engines: {node: '>=20'}
  string.prototype.matchall@4.0.12:
    resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==}
  string.prototype.matchall@4.1.0:
    resolution: {integrity: sha512-tHNHTxInrYLCga9O9YGxWA3G9/nnzQw8UGAyqGx3Ar1pSTTzIuM4woFSq4SowkXCjJIwq5sIiQvEfRI9tCH1qQ==}
    engines: {node: '>= 0.4'}
  string.prototype.trim@1.2.11:
@@ -8825,12 +8872,12 @@
  style-value-types@5.1.2:
    resolution: {integrity: sha512-Vs9fNreYF9j6W2VvuDTP7kepALi7sk0xtk2Tu8Yxi9UoajJdEVpNpCov0HsLTqXvNGKX+Uv09pkozVITi1jf3Q==}
  stylelint-config-html@1.1.0:
    resolution: {integrity: sha512-IZv4IVESjKLumUGi+HWeb7skgO6/g4VMuAYrJdlqQFndgbj6WJAXPhaysvBiXefX79upBdQVumgYcdd17gCpjQ==}
    engines: {node: ^12 || >=14}
  stylelint-config-html@2.0.0:
    resolution: {integrity: sha512-Lk1NPEdUxzHkPv3ehktjpiOk4MPaqQ3H8fkvhxdWlQpaZCm9ze0SoMbRQLqkUxEMfPE1G9/x968uxm/+d2njoA==}
    engines: {node: ^22.12 || >=24}
    peerDependencies:
      postcss-html: ^1.0.0
      stylelint: '>=14.0.0'
      postcss-html: ^2.0.0
      stylelint: '>=16.0.0'
  stylelint-config-recess-order@7.7.0:
    resolution: {integrity: sha512-TWRkg+BrwHOki4pi9y1emWgx6pFwZXOYZhNsbEObke/mzYUJVJvDEzJJQXhH7ajslQJGcrExy8ZvJqDiUbhFpA==}
@@ -8930,6 +8977,10 @@
  sver@2.0.1:
    resolution: {integrity: sha512-zqIx4tK5+gR8xE8VgLNr+IA0mqKDgMNdsfX86DTfNNnVKyuVUhjGdz6KA0rO3q445WUfGpEeJym1m3Xym7ot6w==}
  svg-pathdata@6.0.3:
    resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==}
    engines: {node: '>=12.0.0'}
  svg-tags@1.0.0:
    resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==}
@@ -9009,6 +9060,9 @@
  text-extensions@2.4.0:
    resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==}
    engines: {node: '>=8'}
  text-segmentation@1.0.3:
    resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==}
  theme-colors@0.1.0:
    resolution: {integrity: sha512-6gTEHQqWlQNiOEGHCSSQmU//E5SnXHJ4H7oHQOD8x77CvNYNQAmt73dqR71mzw5ULV87zaHLxK5pIBnsToFuZw==}
@@ -9198,8 +9252,8 @@
  ufo@1.6.4:
    resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==}
  ultrahtml@1.6.0:
    resolution: {integrity: sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==}
  ultrahtml@1.7.0:
    resolution: {integrity: sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g==}
  unbox-primitive@1.1.0:
    resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
@@ -9316,6 +9370,10 @@
  unplugin-utils@0.3.1:
    resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==}
    engines: {node: '>=20.19.0'}
  unplugin-utils@0.3.2:
    resolution: {integrity: sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==}
    engines: {node: '>=20.19.0'}
  unplugin-vue@7.2.0:
@@ -9444,6 +9502,9 @@
  util-deprecate@1.0.2:
    resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
  utrie@1.0.2:
    resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
  valibot@1.4.1:
    resolution: {integrity: sha512-klCmFTz2jeDluy9RwX+F884TCiogtdBJ/YaxSx1EOBYXa3NXNWj8kR1jjN8rzluwojJVWWaHJ4r1U5LfICnM3g==}
@@ -9804,8 +9865,8 @@
    resolution: {integrity: sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==}
    engines: {node: ^20.17.0 || >=22.9.0}
  wsl-utils@0.3.1:
    resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==}
  wsl-utils@1.0.0:
    resolution: {integrity: sha512-Hl0ZOAs672vg+06kfujwRhoS6/jehvULrlFkuF2dRu6pHgA8U06h3xqNIqNNU1LTXPcedxByAR4GS6pwQK0mgA==}
    engines: {node: '>=20'}
  xe-utils@3.9.1:
@@ -9868,16 +9929,16 @@
    resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==}
    engines: {node: '>=12'}
  yargs@18.0.0:
    resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==}
  yargs@18.1.0:
    resolution: {integrity: sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==}
    engines: {node: ^20.19.0 || ^22.12.0 || >=23}
  yocto-queue@1.2.2:
    resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==}
    engines: {node: '>=12.20'}
  yoctocolors@2.1.2:
    resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
  yoctocolors@2.2.0:
    resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==}
    engines: {node: '>=18'}
  youch-core@0.3.3:
@@ -9923,7 +9984,7 @@
  '@antfu/install-pkg@1.1.0':
    dependencies:
      package-manager-detector: 1.6.0
      package-manager-detector: 1.8.0
      tinyexec: 1.2.4
  '@apideck/better-ajv-errors@0.3.7(ajv@8.20.0)':
@@ -11185,7 +11246,7 @@
    dependencies:
      escape-string-regexp: 4.0.0
      eslint: 10.5.0(jiti@2.7.0)
      ignore: 7.0.5
      ignore: 7.0.6
  '@eslint-community/eslint-utils@4.9.1(eslint@10.5.0(jiti@2.7.0))':
    dependencies:
@@ -11456,13 +11517,13 @@
  '@jridgewell/gen-mapping@0.3.13':
    dependencies:
      '@jridgewell/sourcemap-codec': 1.5.0
      '@jridgewell/sourcemap-codec': 1.6.0
      '@jridgewell/trace-mapping': 0.3.31
  '@jridgewell/gen-mapping@0.3.5':
    dependencies:
      '@jridgewell/set-array': 1.2.1
      '@jridgewell/sourcemap-codec': 1.4.10
      '@jridgewell/sourcemap-codec': 1.6.0
      '@jridgewell/trace-mapping': 0.3.31
  '@jridgewell/remapping@2.3.5':
@@ -11479,18 +11540,16 @@
      '@jridgewell/gen-mapping': 0.3.5
      '@jridgewell/trace-mapping': 0.3.31
  '@jridgewell/sourcemap-codec@1.4.10': {}
  '@jridgewell/sourcemap-codec@1.4.14': {}
  '@jridgewell/sourcemap-codec@1.5.0': {}
  '@jridgewell/sourcemap-codec@1.5.5': {}
  '@jridgewell/sourcemap-codec@1.6.0': {}
  '@jridgewell/trace-mapping@0.3.31':
    dependencies:
      '@jridgewell/resolve-uri': 3.1.2
      '@jridgewell/sourcemap-codec': 1.4.14
      '@jridgewell/sourcemap-codec': 1.6.0
  '@jspm/fetch@0.1.0': {}
@@ -11633,14 +11692,14 @@
      consola: 3.4.2
      defu: 6.1.7
      destr: 2.0.5
      errx: 0.1.0
      errx: 0.1.2
      exsolve: 1.1.0
      ignore: 7.0.6
      jiti: 2.7.0
      klona: 2.0.6
      knitwork: 1.3.0
      mlly: 1.8.2
      ohash: 2.0.11
      ohash: 2.0.12
      pathe: 2.0.3
      pkg-types: 2.3.1
      rc9: 3.0.1
@@ -12416,8 +12475,8 @@
  '@tiptap/pm@3.27.1':
    dependencies:
      prosemirror-changeset: 2.4.1
      prosemirror-commands: 1.7.1
      prosemirror-dropcursor: 1.8.2
      prosemirror-commands: 1.7.2
      prosemirror-dropcursor: 1.8.3
      prosemirror-gapcursor: 1.4.1
      prosemirror-history: 1.5.0
      prosemirror-inputrules: 1.5.1
@@ -12471,7 +12530,7 @@
      ejs: 3.1.10
      json5: 2.2.3
      magic-string: 0.30.21
      string.prototype.matchall: 4.0.12
      string.prototype.matchall: 4.1.0
  '@tybys/wasm-util@0.10.2':
    dependencies:
@@ -12482,7 +12541,7 @@
    dependencies:
      '@types/readdir-glob': 1.1.5
  '@types/codemirror@5.60.17':
  '@types/codemirror@5.60.18':
    dependencies:
      '@types/tern': 0.23.9
@@ -12535,7 +12594,7 @@
  '@types/lodash@4.17.24': {}
  '@types/markdown-it@14.1.2':
  '@types/markdown-it@14.2.0':
    dependencies:
      '@types/linkify-it': 5.0.0
      '@types/mdurl': 2.0.0
@@ -12554,11 +12613,16 @@
  '@types/nprogress@0.2.3': {}
  '@types/pako@2.0.0': {}
  '@types/qrcode@1.5.6':
    dependencies:
      '@types/node': 25.9.4
  '@types/qs@6.15.1': {}
  '@types/raf@3.4.3':
    optional: true
  '@types/readdir-glob@1.1.5':
    dependencies:
@@ -12587,7 +12651,7 @@
      '@typescript-eslint/utils': 8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)
      '@typescript-eslint/visitor-keys': 8.62.0
      eslint: 10.5.0(jiti@2.7.0)
      ignore: 7.0.5
      ignore: 7.0.6
      natural-compare: 1.4.0
      ts-api-utils: 2.5.0(typescript@6.0.3)
      typescript: 6.0.3
@@ -12891,9 +12955,9 @@
  '@vue/devtools-api@6.5.0': {}
  '@vue/devtools-api@7.5.2':
  '@vue/devtools-api@7.7.10':
    dependencies:
      '@vue/devtools-kit': 7.5.2
      '@vue/devtools-kit': 7.7.10
  '@vue/devtools-api@7.7.7':
    dependencies:
@@ -12909,10 +12973,10 @@
      '@vue/devtools-shared': 8.1.3
      vue: 3.5.38(typescript@6.0.3)
  '@vue/devtools-kit@7.5.2':
  '@vue/devtools-kit@7.7.10':
    dependencies:
      '@vue/devtools-shared': 7.5.2
      birpc: 0.2.19
      '@vue/devtools-shared': 7.7.10
      birpc: 2.3.0
      hookable: 5.5.3
      mitt: 3.0.1
      perfect-debounce: 1.0.0
@@ -12921,7 +12985,7 @@
  '@vue/devtools-kit@7.7.7':
    dependencies:
      '@vue/devtools-shared': 7.7.7
      '@vue/devtools-shared': 7.7.10
      birpc: 2.3.0
      hookable: 5.5.3
      mitt: 3.0.1
@@ -12936,11 +13000,7 @@
      hookable: 5.5.3
      perfect-debounce: 2.1.0
  '@vue/devtools-shared@7.5.2':
    dependencies:
      rfdc: 1.4.1
  '@vue/devtools-shared@7.7.7':
  '@vue/devtools-shared@7.7.10':
    dependencies:
      rfdc: 1.4.1
@@ -13127,7 +13187,7 @@
  ansi-regex@5.0.1: {}
  ansi-regex@6.2.2: {}
  ansi-regex@6.3.0: {}
  ansi-styles@4.3.0:
    dependencies:
@@ -13350,6 +13410,9 @@
    dependencies:
      bare-path: 3.0.1
  base64-arraybuffer@1.0.2:
    optional: true
  base64-js@1.5.1: {}
  baseline-browser-mapping@2.10.38: {}
@@ -13363,8 +13426,6 @@
  bindings@1.5.0:
    dependencies:
      file-uri-to-path: 1.0.0
  birpc@0.2.19: {}
  birpc@2.3.0: {}
@@ -13473,7 +13534,7 @@
      exsolve: 1.1.0
      giget: 3.3.0
      jiti: 2.7.0
      ohash: 2.0.11
      ohash: 2.0.12
      pathe: 2.0.3
      perfect-debounce: 2.1.0
      pkg-types: 2.3.1
@@ -13539,6 +13600,18 @@
  camunda-bpmn-moddle@7.0.1: {}
  caniuse-lite@1.0.30001799: {}
  canvg@3.0.11:
    dependencies:
      '@babel/runtime': 7.29.7
      '@types/raf': 3.4.3
      core-js: 3.49.0
      raf: 3.4.1
      regenerator-runtime: 0.13.11
      rgbcolor: 1.0.1
      stackblur-canvas: 2.0.0
      svg-pathdata: 6.0.3
    optional: true
  chalk@4.1.2:
    dependencies:
@@ -13652,7 +13725,7 @@
  color-name@1.1.4: {}
  colord@2.9.3: {}
  colord@2.10.0: {}
  colorjs.io@0.5.0: {}
@@ -13800,6 +13873,11 @@
  crypto-random-string@2.0.0: {}
  css-functions-list@3.3.3: {}
  css-line-break@2.1.0:
    dependencies:
      utrie: 1.0.2
    optional: true
  css-select@5.1.0:
    dependencies:
@@ -14030,7 +14108,7 @@
  default-browser-id@5.0.1: {}
  default-browser@5.5.0:
  default-browser@5.5.1:
    dependencies:
      bundle-name: 4.1.0
      default-browser-id: 5.0.1
@@ -14146,7 +14224,7 @@
      no-case: 3.0.4
      tslib: 2.8.1
  dot-prop@10.1.0:
  dot-prop@10.2.0:
    dependencies:
      type-fest: 5.7.0
@@ -14231,7 +14309,7 @@
  error-stack-parser-es@1.0.5: {}
  errx@0.1.0:
  errx@0.1.2:
    optional: true
  es-abstract-get@1.0.0:
@@ -14607,8 +14685,6 @@
      acorn-jsx: 5.3.2(acorn@8.17.0)
      eslint-visitor-keys: 3.0.0
  esprima@2.7.3: {}
  esprima@4.0.1: {}
  esquery@1.7.0:
@@ -14617,7 +14693,7 @@
  esrap@2.2.12(@typescript-eslint/types@8.62.0):
    dependencies:
      '@jridgewell/sourcemap-codec': 1.5.0
      '@jridgewell/sourcemap-codec': 1.6.0
    optionalDependencies:
      '@typescript-eslint/types': 8.62.0
@@ -14659,10 +14735,10 @@
      is-plain-obj: 4.1.0
      is-stream: 4.0.1
      npm-run-path: 6.0.0
      pretty-ms: 9.3.0
      pretty-ms: 9.3.1
      signal-exit: 4.1.0
      strip-final-newline: 4.0.0
      yoctocolors: 2.1.2
      yoctocolors: 2.2.0
  exsolve@1.1.0: {}
@@ -14683,6 +14759,12 @@
  fast-json-stable-stringify@2.1.0: {}
  fast-levenshtein@2.0.6: {}
  fast-png@6.2.0:
    dependencies:
      '@types/pako': 2.0.0
      iobuffer: 5.4.0
      pako: 2.2.0
  fast-uri@3.1.2: {}
@@ -14983,7 +15065,7 @@
    dependencies:
      '@sindresorhus/merge-streams': 4.0.0
      fast-glob: 3.3.3
      ignore: 7.0.5
      ignore: 7.0.6
      is-path-inside: 4.0.0
      slash: 5.1.0
      unicorn-magic: 0.4.0
@@ -15009,7 +15091,7 @@
      defu: 6.1.7
      destr: 2.0.5
      iron-webcrypto: 1.2.1
      node-mock-http: 1.0.4
      node-mock-http: 1.0.5
      radix3: 1.1.2
      ufo: 1.6.4
      uncrypto: 0.1.3
@@ -15048,6 +15130,8 @@
  highlight.js@11.11.1: {}
  highlight.js@11.12.0: {}
  hookable@5.5.3: {}
  hookable@6.1.1: {}
@@ -15079,6 +15163,14 @@
  html-tags@3.3.1: {}
  html-tags@5.1.0: {}
  html-to-image@1.11.13: {}
  html2canvas@1.4.1:
    dependencies:
      css-line-break: 2.1.0
      text-segmentation: 1.0.3
    optional: true
  htmlparser2@10.1.0:
    dependencies:
@@ -15145,10 +15237,7 @@
  ignore@5.3.2: {}
  ignore@7.0.5: {}
  ignore@7.0.6:
    optional: true
  ignore@7.0.6: {}
  immutable@5.1.7: {}
@@ -15186,6 +15275,8 @@
      side-channel: 1.1.1
  internmap@2.0.3: {}
  iobuffer@5.4.0: {}
  ioredis@5.11.1:
    dependencies:
@@ -15310,7 +15401,7 @@
  is-plain-object@3.0.1: {}
  is-plain-object@5.0.0: {}
  is-plain-object@5.1.0: {}
  is-reference@1.2.1:
    dependencies:
@@ -15430,10 +15521,10 @@
  js-tokens@9.0.1: {}
  js-yaml@3.6.1:
  js-yaml@3.15.2:
    dependencies:
      argparse: 1.0.10
      esprima: 2.7.3
      esprima: 4.0.1
  js-yaml@4.2.0:
    dependencies:
@@ -15489,6 +15580,17 @@
  jsonparse@1.3.1: {}
  jsonpointer@5.0.1: {}
  jspdf@4.2.1:
    dependencies:
      '@babel/runtime': 7.29.7
      fast-png: 6.2.0
      fflate: 0.8.3
    optionalDependencies:
      canvg: 3.0.11
      core-js: 3.49.0
      dompurify: 3.4.11
      html2canvas: 1.4.1
  katex@0.16.47:
    dependencies:
@@ -15675,7 +15777,7 @@
  log-symbols@7.0.1:
    dependencies:
      is-unicode-supported: 2.1.0
      yoctocolors: 2.1.2
      yoctocolors: 2.2.0
  loglevel@1.9.2: {}
@@ -15740,7 +15842,7 @@
      argparse: 2.0.1
      entities: 4.4.0
      linkify-it: 5.0.1
      mdurl: 2.0.0
      mdurl: 2.1.0
      punycode.js: 2.3.1
      uc.micro: 2.1.0
@@ -15762,7 +15864,7 @@
    dependencies:
      '@babel/runtime': 7.29.7
      '@vscode/markdown-it-katex': 1.1.2
      highlight.js: 11.11.1
      highlight.js: 11.12.0
      katex: 0.16.47
      markdown-it: 14.2.0
      markdown-it-ins: 4.0.0
@@ -15797,7 +15899,7 @@
  mdn-data@2.28.1: {}
  mdurl@2.0.0: {}
  mdurl@2.1.0: {}
  meow@12.0.1:
    dependencies:
@@ -15957,7 +16059,7 @@
      db0: 0.3.4
      defu: 6.1.7
      destr: 2.0.5
      dot-prop: 10.1.0
      dot-prop: 10.2.0
      esbuild: 0.28.1
      escape-string-regexp: 5.0.0
      etag: 1.8.1
@@ -15977,13 +16079,13 @@
      mime: 4.1.0
      mlly: 1.8.2
      node-fetch-native: 1.6.7
      node-mock-http: 1.0.4
      node-mock-http: 1.0.5
      ofetch: 1.5.1
      ohash: 2.0.11
      ohash: 2.0.12
      pathe: 2.0.3
      perfect-debounce: 2.1.0
      pkg-types: 2.3.1
      pretty-bytes: 7.1.0
      pretty-bytes: 7.1.2
      radix3: 1.1.2
      rollup: 4.62.2
      rollup-plugin-visualizer: 7.0.1(rolldown@1.1.2)(rollup@4.62.2)
@@ -15994,12 +16096,12 @@
      source-map: 0.7.6
      std-env: 4.1.0
      ufo: 1.6.4
      ultrahtml: 1.6.0
      ultrahtml: 1.7.0
      uncrypto: 0.1.3
      unctx: 2.5.0
      unenv: 2.0.0-rc.24
      unimport: 6.3.0(rolldown@1.1.2)
      unplugin-utils: 0.3.1
      unplugin-utils: 0.3.2
      unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1)
      untyped: 2.0.0
      unwasm: 0.5.3
@@ -16055,7 +16157,7 @@
  node-gyp-build@4.8.4: {}
  node-mock-http@1.0.4: {}
  node-mock-http@1.0.5: {}
  node-releases@2.0.48: {}
@@ -16123,7 +16225,7 @@
      node-fetch-native: 1.6.7
      ufo: 1.6.4
  ohash@2.0.11: {}
  ohash@2.0.12: {}
  on-finished@2.4.1:
    dependencies:
@@ -16133,14 +16235,14 @@
    dependencies:
      mimic-function: 5.0.1
  open@11.0.0:
  open@11.0.2:
    dependencies:
      default-browser: 5.5.0
      default-browser: 5.5.1
      define-lazy-prop: 3.0.0
      is-in-ssh: 1.0.0
      is-inside-container: 1.0.0
      powershell-utils: 0.1.0
      wsl-utils: 0.3.1
      powershell-utils: 0.2.1
      wsl-utils: 1.0.0
  optionator@0.9.4:
    dependencies:
@@ -16246,7 +16348,7 @@
  package-json-from-dist@1.0.1: {}
  package-manager-detector@1.6.0: {}
  package-manager-detector@1.8.0: {}
  pako@2.2.0: {}
@@ -16330,6 +16432,9 @@
  perfect-debounce@1.0.0: {}
  perfect-debounce@2.1.0: {}
  performance-now@2.1.0:
    optional: true
  picocolors@1.1.1: {}
@@ -16433,6 +16538,8 @@
  powershell-utils@0.1.0: {}
  powershell-utils@0.2.1: {}
  preact@10.29.2: {}
  prelude-ls@1.2.1: {}
@@ -16441,9 +16548,9 @@
  pretty-bytes@6.1.1: {}
  pretty-bytes@7.1.0: {}
  pretty-bytes@7.1.2: {}
  pretty-ms@9.3.0:
  pretty-ms@9.3.1:
    dependencies:
      parse-ms: 4.0.0
@@ -16457,13 +16564,13 @@
    dependencies:
      prosemirror-transform: 1.12.0
  prosemirror-commands@1.7.1:
  prosemirror-commands@1.7.2:
    dependencies:
      prosemirror-model: 1.25.9
      prosemirror-state: 1.4.4
      prosemirror-transform: 1.12.0
  prosemirror-dropcursor@1.8.2:
  prosemirror-dropcursor@1.8.3:
    dependencies:
      prosemirror-state: 1.4.4
      prosemirror-transform: 1.12.0
@@ -16559,9 +16666,14 @@
  radix3@1.1.2: {}
  raf@3.4.1:
    dependencies:
      performance-now: 2.1.0
    optional: true
  randomcolor@0.6.2: {}
  range-parser@1.2.1: {}
  range-parser@1.3.0: {}
  rc9@3.0.1:
    dependencies:
@@ -16584,7 +16696,7 @@
  read-yaml-file@1.1.0:
    dependencies:
      graceful-fs: 4.2.11
      js-yaml: 3.6.1
      js-yaml: 3.15.2
      pify: 4.0.1
      strip-bom: 3.0.0
@@ -16645,6 +16757,9 @@
  regenerate@1.4.2: {}
  regenerator-runtime@0.13.11:
    optional: true
  regexp.prototype.flags@1.5.4:
    dependencies:
      call-bind: 1.0.9
@@ -16680,7 +16795,7 @@
      '@vueuse/shared': 14.3.0(vue@3.5.38(typescript@6.0.3))
      aria-hidden: 1.2.6
      defu: 6.1.7
      ohash: 2.0.11
      ohash: 2.0.12
      vue: 3.5.38(typescript@6.0.3)
    transitivePeerDependencies:
      - '@vue/composition-api'
@@ -16718,6 +16833,9 @@
  reusify@1.1.0: {}
  rfdc@1.4.1: {}
  rgbcolor@1.0.1:
    optional: true
  rimraf@5.0.5:
    dependencies:
@@ -16791,10 +16909,10 @@
  rollup-plugin-visualizer@7.0.1(rolldown@1.1.2)(rollup@4.62.2):
    dependencies:
      open: 11.0.0
      open: 11.0.2
      picomatch: 4.0.4
      source-map: 0.7.6
      yargs: 18.0.0
      yargs: 18.1.0
    optionalDependencies:
      rolldown: 1.1.2
      rollup: 4.62.2
@@ -17032,7 +17150,7 @@
      mime-types: 3.0.2
      ms: 2.1.3
      on-finished: 2.4.1
      range-parser: 1.2.1
      range-parser: 1.3.0
      statuses: 2.0.2
    transitivePeerDependencies:
      - supports-color
@@ -17178,6 +17296,9 @@
  sprintf-js@1.0.3: {}
  stackblur-canvas@2.0.0:
    optional: true
  standard-as-callback@2.1.0: {}
  statuses@2.0.2: {}
@@ -17231,7 +17352,7 @@
      get-east-asian-width: 1.6.0
      strip-ansi: 7.2.0
  string.prototype.matchall@4.0.12:
  string.prototype.matchall@4.1.0:
    dependencies:
      call-bind: 1.0.9
      call-bound: 1.0.4
@@ -17295,7 +17416,7 @@
  strip-ansi@7.2.0:
    dependencies:
      ansi-regex: 6.2.2
      ansi-regex: 6.3.0
  strip-bom@3.0.0: {}
@@ -17326,7 +17447,7 @@
      hey-listen: 1.0.8
      tslib: 2.4.0
  stylelint-config-html@1.1.0(postcss-html@1.8.1)(stylelint@17.13.0(typescript@6.0.3)):
  stylelint-config-html@2.0.0(postcss-html@1.8.1)(stylelint@17.13.0(typescript@6.0.3)):
    dependencies:
      postcss-html: 1.8.1
      stylelint: 17.13.0(typescript@6.0.3)
@@ -17350,7 +17471,7 @@
      postcss-html: 1.8.1
      semver: 7.8.5
      stylelint: 17.13.0(typescript@6.0.3)
      stylelint-config-html: 1.1.0(postcss-html@1.8.1)(stylelint@17.13.0(typescript@6.0.3))
      stylelint-config-html: 2.0.0(postcss-html@1.8.1)(stylelint@17.13.0(typescript@6.0.3))
      stylelint-config-recommended: 18.0.0(stylelint@17.13.0(typescript@6.0.3))
  stylelint-config-recommended@18.0.0(stylelint@17.13.0(typescript@6.0.3)):
@@ -17375,7 +17496,7 @@
      '@csstools/css-syntax-patches-for-csstree': 1.1.5(css-tree@3.2.1)
      '@csstools/css-tokenizer': 4.0.0
      css-tree: 3.2.1
      is-plain-object: 5.0.0
      is-plain-object: 5.1.0
      known-css-properties: 0.37.0
      postcss-media-query-parser: 0.2.3
      postcss-resolve-nested-selector: 0.1.6
@@ -17392,7 +17513,7 @@
      '@csstools/media-query-list-parser': 5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
      '@csstools/selector-resolve-nested': 4.0.0(postcss-selector-parser@7.1.4)
      '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.4)
      colord: 2.9.3
      colord: 2.10.0
      cosmiconfig: 9.0.2(typescript@6.0.3)
      css-functions-list: 3.3.3
      css-tree: 3.2.1
@@ -17404,7 +17525,7 @@
      globby: 16.2.0
      globjoin: 0.1.4
      html-tags: 5.1.0
      ignore: 7.0.5
      ignore: 7.0.6
      import-meta-resolve: 4.2.0
      mathml-tag-names: 4.0.0
      meow: 14.1.0
@@ -17466,7 +17587,7 @@
  svelte@5.56.3(@typescript-eslint/types@8.62.0):
    dependencies:
      '@jridgewell/remapping': 2.3.5
      '@jridgewell/sourcemap-codec': 1.5.0
      '@jridgewell/sourcemap-codec': 1.6.0
      '@sveltejs/acorn-typescript': 1.0.10(acorn@8.17.0)
      '@types/estree': 1.0.9
      '@types/trusted-types': 2.0.7
@@ -17485,6 +17606,9 @@
      - '@typescript-eslint/types'
  sver@2.0.1: {}
  svg-pathdata@6.0.3:
    optional: true
  svg-tags@1.0.0: {}
@@ -17577,6 +17701,11 @@
      - react-native-b4a
  text-extensions@2.4.0: {}
  text-segmentation@1.0.3:
    dependencies:
      utrie: 1.0.2
    optional: true
  theme-colors@0.1.0: {}
@@ -17740,7 +17869,7 @@
  ufo@1.6.4: {}
  ultrahtml@1.6.0: {}
  ultrahtml@1.7.0: {}
  unbox-primitive@1.1.0:
    dependencies:
@@ -17805,7 +17934,7 @@
      strip-literal: 3.1.0
      tinyglobby: 0.2.17
      unplugin: 3.0.0
      unplugin-utils: 0.3.1
      unplugin-utils: 0.3.2
    optionalDependencies:
      rolldown: 1.1.2
@@ -17839,6 +17968,11 @@
      - supports-color
  unplugin-utils@0.3.1:
    dependencies:
      pathe: 2.0.3
      picomatch: 4.0.4
  unplugin-utils@0.3.2:
    dependencies:
      pathe: 2.0.3
      picomatch: 4.0.4
@@ -17952,6 +18086,11 @@
  util-deprecate@1.0.2: {}
  utrie@1.0.2:
    dependencies:
      base64-arraybuffer: 1.0.2
    optional: true
  valibot@1.4.1(typescript@6.0.3):
    optionalDependencies:
      typescript: 6.0.3
@@ -17965,7 +18104,7 @@
  vee-validate@4.15.1(vue@3.5.38(typescript@6.0.3)):
    dependencies:
      '@vue/devtools-api': 7.5.2
      '@vue/devtools-api': 7.7.10
      type-fest: 4.8.3
      vue: 3.5.38(typescript@6.0.3)
@@ -18015,11 +18154,11 @@
      ansis: 4.3.1
      error-stack-parser-es: 1.0.5
      obug: 2.1.3
      ohash: 2.0.11
      open: 11.0.0
      ohash: 2.0.12
      open: 11.0.2
      perfect-debounce: 2.1.0
      sirv: 3.0.2
      unplugin-utils: 0.3.1
      unplugin-utils: 0.3.2
      vite: 8.0.10(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
      vite-dev-rpc: 2.0.0(vite@8.0.10(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))
@@ -18432,7 +18571,7 @@
    dependencies:
      signal-exit: 4.1.0
  wsl-utils@0.3.1:
  wsl-utils@1.0.0:
    dependencies:
      is-wsl: 3.1.1
      powershell-utils: 0.1.0
@@ -18499,18 +18638,18 @@
      y18n: 5.0.8
      yargs-parser: 21.1.1
  yargs@18.0.0:
  yargs@18.1.0:
    dependencies:
      cliui: 9.0.1
      escalade: 3.2.0
      get-caller-file: 2.0.5
      string-width: 7.2.0
      string-width: 8.2.1
      y18n: 5.0.8
      yargs-parser: 22.0.0
  yocto-queue@1.2.2: {}
  yoctocolors@2.1.2: {}
  yoctocolors@2.2.0: {}
  youch-core@0.3.3:
    dependencies:
src/api/mes/management-cockpit/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,158 @@
import { requestClient } from '#/api/request';
export namespace ManagementCockpitApi {
  /** é€šç”¨åç§°-数量分布项 */
  export interface NameCountItem {
    name: string; // åç§°ï¼ˆä»“库/品种/状态/经销商/省份/渠道等)
    count: number; // æ•°é‡
  }
  /** æŒ‰æ—¥æ•°é‡è¶‹åŠ¿é¡¹ */
  export interface DailyCountItem {
    date: string; // æ—¥æœŸ yyyy-MM-dd
    count: number; // æ•°é‡
  }
  /** è´¨æ£€æŒ‡æ ‡å‡å€¼é¡¹ */
  export interface IndicatorAvgItem {
    indicatorCode: string; // æŒ‡æ ‡ç¼–码
    indicatorName: string; // æŒ‡æ ‡åç§°ï¼ˆçº¯åº¦/净度/发芽率/水分)
    checkCount: number; // æ£€éªŒæ¬¡æ•°
    averageValue: number; // æŒ‡æ ‡å¹³å‡å€¼
    passRate: number; // æŒ‡æ ‡åˆæ ¼çŽ‡ï¼ˆ%)
  }
  /** äº”维度汇总统计 */
  export interface Summary {
    // ç”Ÿäº§
    batchTotal: number; // æ‰¹æ¬¡æ€»æ•°
    batchDraft: number; // è‰ç¨¿æ‰¹æ¬¡
    batchProducing: number; // ç”Ÿäº§ä¸­æ‰¹æ¬¡
    batchFinished: number; // å·²å®Œæˆæ‰¹æ¬¡
    batchCanceled: number; // å·²ä½œåºŸæ‰¹æ¬¡
    bagCodeTotal: number; // è¢‹ç æ€»æ•°
    bagCodeGenerated: number; // å·²ç”Ÿæˆè¢‹ç 
    bagCodePrinted: number; // å·²å¯¼å‡ºå°åˆ·è¢‹ç 
    bagCodeCanceled: number; // å·²ä½œåºŸè¢‹ç 
    palletTotal: number; // æ‰˜ç›˜ç æ€»æ•°
    todayInboundBagCount: number; // ä»Šæ—¥æ‰«ç å…¥åº“袋数
    // è´¨æ£€
    qualityTotal: number; // æ£€éªŒè®°å½•总数
    qualityPending: number; // å¤„理中
    qualityPass: number; // åˆæ ¼
    qualityFail: number; // ä¸åˆæ ¼
    qualityPassRate: number; // æ£€éªŒåˆæ ¼çŽ‡ï¼ˆ%)
    // åº“å­˜
    stockTotal: number; // å½“前库存总量
    stockTemporary: number; // æš‚存库存量
    stockQualified: number; // åˆæ ¼åº“存量
    stockOutbound: number; // å·²å‡ºåº“数量
    lowStockCount: number; // ä½Žåº“存预警数
    // å‡ºåº“
    outboundOrderTotal: number; // å‡ºåº“单总数
    outboundQuantityTotal: number; // å‡ºåº“总量
    todayOutboundQuantity: number; // ä»Šæ—¥å‡ºåº“量
    // è¿½æº¯
    scanTotal: number; // æ‰«ç æ€»æ•°
    todayScanCount: number; // ä»Šæ—¥æ‰«ç æ•°
    scanSuccessCount: number; // æ‰«ç æˆåŠŸæ•°
    crossRegionWarningTotal: number; // çªœè´§é¢„警总数
    crossRegionPendingCount: number; // å¾…处理窜货预警数
  }
  /** ç”Ÿäº§è¶‹åŠ¿é¡¹ */
  export interface ProductionTrendItem {
    date: string;
    batchCount: number; // æ–°å¢žæ‰¹æ¬¡
    bagCodeCount: number; // æ–°å¢žè¢‹ç 
    palletCount: number; // æ–°å¢žæ‰˜ç›˜
    inboundBagQuantity: number; // æ‰«ç å…¥åº“袋数
  }
  /** è´¨æ£€ç»´åº¦ç»Ÿè®¡ */
  export interface QualityStatistics {
    resultDistribution: NameCountItem[]; // æ£€éªŒç»“果分布
    indicatorList: IndicatorAvgItem[]; // å››é¡¹æŒ‡æ ‡å‡å€¼
  }
  /** åº“存维度统计 */
  export interface InventoryStatistics {
    warehouseDistribution: NameCountItem[]; // æŒ‰ä»“库
    itemDistribution: NameCountItem[]; // æŒ‰å“ç§
    stateDistribution: NameCountItem[]; // æŒ‰çŠ¶æ€
  }
  /** å‡ºåº“维度统计 */
  export interface OutboundStatistics {
    trend: DailyCountItem[]; // æŒ‰æ—¥å‡ºåº“趋势
    dealerDistribution: NameCountItem[]; // æŒ‰ç»é”€å•†
  }
  /** çªœè´§é¢„警汇总 */
  export interface CrossRegionSummary {
    total: number;
    pending: number;
    handled: number;
    falseAlarm: number;
    crossProvince: number;
    crossCity: number;
  }
  /** è¿½æº¯ç»´åº¦ç»Ÿè®¡ */
  export interface TraceStatistics {
    scanTrend: DailyCountItem[]; // æ‰«ç æŒ‰æ—¥è¶‹åŠ¿
    provinceDistribution: NameCountItem[]; // çœä»½åˆ†å¸ƒ
    channelDistribution: NameCountItem[]; // æ¸ é“分布
    crossRegionSummary: CrossRegionSummary; // çªœè´§é¢„警汇总
  }
}
/** èŽ·å¾—ç®¡ç†é©¾é©¶èˆ±äº”ç»´åº¦æ±‡æ€»ç»Ÿè®¡ */
export function getManagementCockpitSummary() {
  return requestClient.get<ManagementCockpitApi.Summary>(
    '/mes/dashboard/summary',
  );
}
/** èŽ·å¾—ç”Ÿäº§è¶‹åŠ¿ */
export function getManagementCockpitProductionTrend(days?: number) {
  return requestClient.get<ManagementCockpitApi.ProductionTrendItem[]>(
    '/mes/dashboard/production-trend',
    { params: { days } },
  );
}
/** èŽ·å¾—è´¨æ£€ç»´åº¦ç»Ÿè®¡ */
export function getManagementCockpitQuality() {
  return requestClient.get<ManagementCockpitApi.QualityStatistics>(
    '/mes/dashboard/quality-statistics',
  );
}
/** èŽ·å¾—åº“å­˜ç»´åº¦ç»Ÿè®¡ */
export function getManagementCockpitInventory() {
  return requestClient.get<ManagementCockpitApi.InventoryStatistics>(
    '/mes/dashboard/inventory-statistics',
  );
}
/** èŽ·å¾—å‡ºåº“ç»´åº¦ç»Ÿè®¡ */
export function getManagementCockpitOutbound(days?: number) {
  return requestClient.get<ManagementCockpitApi.OutboundStatistics>(
    '/mes/dashboard/outbound-statistics',
    { params: { days } },
  );
}
/** èŽ·å¾—è¿½æº¯ç»´åº¦ç»Ÿè®¡ */
export function getManagementCockpitTrace(days?: number) {
  return requestClient.get<ManagementCockpitApi.TraceStatistics>(
    '/mes/dashboard/trace-statistics',
    { params: { days } },
  );
}
/** å¯¼å‡ºç®¡ç†é©¾é©¶èˆ± Excel æŠ¥è¡¨ï¼ˆè¿”回 Blob) */
export function exportManagementCockpitExcel() {
  return requestClient.download('/mes/dashboard/export-excel');
}
src/api/mes/trace-consumer-scan/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
import { requestClient } from '#/api/request';
export namespace MesTraceConsumerScanApi {
  /** æ¶ˆè´¹è€…扫码访问汇总 */
  export interface Summary {
    total: number; // æ€»è®¿é—®æ¬¡æ•°
    success: number; // æˆåŠŸæ¬¡æ•°
    failed: number; // å¤±è´¥æ¬¡æ•°
    uniqueBatchCount: number; // éžç©ºæ‰¹æ¬¡åŽ»é‡æ•°é‡
  }
  /** æ¶ˆè´¹è€…扫码访问趋势 */
  export interface Trend {
    date: string; // æ—¥æœŸ yyyy-MM-dd
    total: number; // å½“日总访问次数
    success: number; // å½“日成功次数
    failed: number; // å½“日失败次数
  }
  /** æ¶ˆè´¹è€…扫码访问分布 */
  export interface Distribution {
    name: string; // åˆ†ç»„名称
    count: number; // è®¿é—®æ¬¡æ•°
  }
}
/** èŽ·å¾—æ¶ˆè´¹è€…æ‰«ç è®¿é—®æ±‡æ€» */
export function getTraceConsumerScanSummary(
  startTime?: string,
  endTime?: string,
) {
  return requestClient.get<MesTraceConsumerScanApi.Summary>(
    '/mes/trace-consumer-scan/summary',
    { params: { startTime, endTime } },
  );
}
/** èŽ·å¾—æ¶ˆè´¹è€…æ‰«ç è®¿é—®è¶‹åŠ¿ */
export function getTraceConsumerScanTrend(
  startTime?: string,
  endTime?: string,
) {
  return requestClient.get<MesTraceConsumerScanApi.Trend[]>(
    '/mes/trace-consumer-scan/trend',
    { params: { startTime, endTime } },
  );
}
/** èŽ·å¾—æ¶ˆè´¹è€…æ‰«ç è®¿é—®åˆ†å¸ƒ */
export function getTraceConsumerScanDistribution(
  groupBy: string,
  startTime?: string,
  endTime?: string,
) {
  return requestClient.get<MesTraceConsumerScanApi.Distribution[]>(
    '/mes/trace-consumer-scan/distribution',
    { params: { groupBy, startTime, endTime } },
  );
}
src/api/mes/trace/config.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
import { requestClient } from '#/api/request';
export namespace MesTraceConfigApi {
  /** æº¯æº H5 é¡µé¢åŸºç¡€åœ°å€ï¼ˆäºŒç»´ç å†…容前缀) */
  export interface QrConfig {
    traceBaseUrl?: string;
  }
}
/**
 * èŽ·å¾—æº¯æº H5 é¡µé¢åŸºç¡€åœ°å€ï¼ˆäºŒç»´ç å†…容前缀)
 * ç”¨äºŽ"查看二维码"预览/下载时生成与打印标签一致的 H5 URL äºŒç»´ç 
 */
export function getQrConfig() {
  return requestClient.get<MesTraceConfigApi.QrConfig>(
    '/mes/trace-config/qr-config',
  );
}
src/api/mes/trace/crossregion/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,124 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesTraceCrossRegionApi {
  /** çªœè´§é¢„è­¦ */
  export interface Warning {
    id?: number; // ç¼–号
    warningNo?: string; // é¢„警编号
    bagCodeId?: number; // è¢‹ç ç¼–号
    bagCode?: string; // è¢‹ç 
    palletCodeId?: number; // æ‰˜ç›˜ç ç¼–号
    palletCode?: string; // æ‰˜ç›˜ç 
    batchId?: number; // ç”Ÿäº§æ‰¹æ¬¡ç¼–号
    batchCode?: string; // æ‰¹æ¬¡å·
    itemId?: number; // ç‰©æ–™ç¼–号
    itemCode?: string; // ç‰©æ–™ç¼–码
    itemName?: string; // ç‰©æ–™åç§°
    salesId?: number; // é”€å”®å‡ºåº“单编号
    salesCode?: string; // å‡ºåº“单号
    clientId?: number; // ç»é”€å•†ç¼–号
    clientName?: string; // ç»é”€å•†åç§°
    dealerProvince?: string; // ç»é”€å•†çœä»½
    dealerCity?: string; // ç»é”€å•†åŸŽå¸‚
    scanProvince?: string; // æ‰«ç çœä»½
    scanCity?: string; // æ‰«ç åŸŽå¸‚
    warningType?: string; // é¢„警类型:CROSS_PROVINCE / CROSS_CITY
    warningTypeName?: string; // é¢„警类型名称
    scanEventId?: number; // å…³è”消费者扫码事件编号
    status?: number; // çŠ¶æ€ï¼š10 å¾…处理 / 20 å·²å¤„理 / 30 è¯¯æŠ¥
    statusName?: string; // çŠ¶æ€åç§°
    handleUserId?: number; // å¤„理人编号
    handleUserName?: string; // å¤„理人姓名
    handleTime?: string; // å¤„理时间
    handleRemark?: string; // å¤„理备注
    eventTime?: string; // é¢„警产生时间
  }
  /** çªœè´§é¢„警分页查询参数 */
  export interface PageParams extends PageParam {
    status?: number;
    warningType?: string;
    batchCode?: string;
    bagCode?: string;
    clientName?: string;
    eventTime?: string[];
  }
  /** çªœè´§é¢„警汇总 */
  export interface Summary {
    [key: string]: number | undefined; // æ±‡æ€»æŒ‡æ ‡ï¼ˆæŒ‰å­—段名取值)
    total?: number; // é¢„警总数
    pending?: number; // å¾…处理数量
    handled?: number; // å·²å¤„理数量
    falseAlarm?: number; // è¯¯æŠ¥æ•°é‡
    crossProvince?: number; // è·¨çœçªœè´§æ•°é‡
    crossCity?: number; // è·¨å¸‚窜货数量
  }
  /** çªœè´§é¢„警分布统计项 */
  export interface StatisticsItem {
    name?: string; // åˆ†ç»„名称
    count?: number; // é¢„警数量
  }
  /** é˜²çªœè´§è§„则配置 */
  export interface AntiConfig {
    warningLevel?: string; // åˆ¤å®šç²’度:PROVINCE è·¨çœ / CITY è·¨å¸‚
    remark?: string; // é…ç½®è¯´æ˜Ž
  }
}
/** èŽ·å¾—çªœè´§é¢„è­¦åˆ†é¡µ */
export function getTraceCrossRegionPage(
  params: MesTraceCrossRegionApi.PageParams,
) {
  return requestClient.get<PageResult<MesTraceCrossRegionApi.Warning>>(
    '/mes/trace-cross-region/page',
    { params },
  );
}
/** å¤„理窜货预警 */
export function handleTraceCrossRegion(data: {
  id: number;
  status: number;
  handleRemark?: string;
}) {
  return requestClient.put('/mes/trace-cross-region/handle', data);
}
/** èŽ·å¾—çªœè´§é¢„è­¦æ±‡æ€» */
export function getTraceCrossRegionSummary(startTime?: string, endTime?: string) {
  return requestClient.get<MesTraceCrossRegionApi.Summary>(
    '/mes/trace-cross-region/summary',
    { params: { startTime, endTime } },
  );
}
/** èŽ·å¾—çªœè´§é¢„è­¦åˆ†å¸ƒç»Ÿè®¡ */
export function getTraceCrossRegionStatistics(
  groupBy: string,
  startTime?: string,
  endTime?: string,
) {
  return requestClient.get<MesTraceCrossRegionApi.StatisticsItem[]>(
    '/mes/trace-cross-region/statistics',
    { params: { groupBy, startTime, endTime } },
  );
}
/** èŽ·å¾—é˜²çªœè´§è§„åˆ™é…ç½® */
export function getTraceCrossRegionConfig() {
  return requestClient.get<MesTraceCrossRegionApi.AntiConfig>(
    '/mes/trace-cross-region/config',
  );
}
/** æ›´æ–°é˜²çªœè´§è§„则配置 */
export function updateTraceCrossRegionConfig(warningLevel: string) {
  return requestClient.put(
    `/mes/trace-cross-region/config?warningLevel=${warningLevel}`,
  );
}
src/api/mes/trace/integrity/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesTraceIntegrityApi {
  /** æ•°æ®å®Œæ•´æ€§æ ¡éªŒç»“æžœ */
  export interface VerifyResult {
    bizType?: string; // ä¸šåŠ¡ç±»åž‹ï¼šBATCH / SALES / SCAN
    bizId?: number; // ä¸šåŠ¡è®°å½•ç¼–å·
    bizCode?: string; // ä¸šåŠ¡ç¼–ç 
    matched?: boolean; // æ˜¯å¦æ ¡éªŒé€šè¿‡
    storedHash?: string; // å­˜å‚¨çš„æ‘˜è¦ SHA-256
    currentHash?: string; // å½“前数据重算摘要 SHA-256
    bagSetHash?: string; // æ‰¹æ¬¡å½“前袋码集合摘要(仅 BATCH æ ¡éªŒè¿”回)
    message?: string; // æ ¡éªŒè¯´æ˜Ž
  }
  /** æ•°æ®å®Œæ•´æ€§è®°å½• */
  export interface Record {
    id?: number; // ç¼–号
    bizType?: string; // ä¸šåŠ¡ç±»åž‹
    bizId?: number; // ä¸šåŠ¡è®°å½•ç¼–å·
    bizCode?: string; // ä¸šåŠ¡ç¼–ç 
    contentHash?: string; // å†…容摘要 SHA-256
    eventTime?: string; // æ‘˜è¦ç”Ÿæˆæ—¶é—´
  }
  /** æ•°æ®å®Œæ•´æ€§è®°å½•分页查询参数 */
  export interface PageParams extends PageParam {
    bizType?: string;
    bizId?: number;
    bizCode?: string;
  }
}
/** æ ¡éªŒæ•°æ®å®Œæ•´æ€§ï¼ˆé‡ç®—摘要比对) */
export function verifyTraceIntegrity(bizType: string, bizId: number) {
  return requestClient.get<MesTraceIntegrityApi.VerifyResult>(
    '/mes/trace-integrity/verify',
    { params: { bizType, bizId } },
  );
}
/** èŽ·å¾—æ•°æ®å®Œæ•´æ€§è®°å½•åˆ†é¡µ */
export function getTraceIntegrityPage(
  params: MesTraceIntegrityApi.PageParams,
) {
  return requestClient.get<PageResult<MesTraceIntegrityApi.Record>>(
    '/mes/trace-integrity/page',
    { params },
  );
}
src/views/management-cockpit/chart-options.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,113 @@
import type { EChartsOption } from '@vben/plugins/echarts';
/** ç”Ÿäº§è¶‹åŠ¿å›¾é…ç½®ï¼ˆå¤šç³»åˆ—æŸ±çŠ¶/折线) */
export function getProductionTrendChartOptions(
  dates: string[],
  batchCounts: number[],
  bagCodeCounts: number[],
  palletCounts: number[],
  inboundCounts: number[],
): EChartsOption {
  return {
    color: ['#409EFF', '#36cfc9', '#f59e0b', '#67c23a'],
    grid: { bottom: 40, left: 50, right: 30, top: 30 },
    legend: { bottom: 0, type: 'scroll' },
    series: [
      { barMaxWidth: 24, data: batchCounts, name: '新增批次', type: 'bar' },
      { barMaxWidth: 24, data: bagCodeCounts, name: '新增袋码', type: 'bar' },
      { barMaxWidth: 24, data: palletCounts, name: '新增托盘', type: 'bar' },
      { data: inboundCounts, name: '入库袋数', smooth: true, type: 'line' },
    ],
    tooltip: { trigger: 'axis' },
    xAxis: { axisTick: { show: false }, data: dates, type: 'category' },
    yAxis: { minInterval: 1, type: 'value' },
  };
}
/** é€šç”¨é¥¼å›¾/环形图配置 */
export function getPieChartOptions(
  data: Array<{ name: string; value: number }>,
): EChartsOption {
  return {
    legend: { bottom: 0, type: 'scroll' },
    series: [
      {
        avoidLabelOverlap: true,
        data,
        emphasis: { label: { fontSize: 14, fontWeight: 'bold', show: true } },
        itemStyle: { borderColor: '#fff', borderRadius: 6, borderWidth: 2 },
        label: { formatter: '{b}\n{c}', show: true },
        radius: ['40%', '70%'],
        type: 'pie',
      },
    ],
    tooltip: { formatter: '{b}: {c} ({d}%)', trigger: 'item' },
  };
}
/** é€šç”¨æŸ±çŠ¶å›¾é…ç½® */
export function getBarChartOptions(
  categories: string[],
  values: number[],
  name: string,
  color = '#409EFF',
): EChartsOption {
  return {
    grid: { bottom: 50, left: 60, right: 20, top: 30 },
    series: [
      {
        barMaxWidth: 36,
        data: values,
        itemStyle: { borderRadius: 4, color },
        name,
        type: 'bar',
      },
    ],
    tooltip: { axisPointer: { type: 'shadow' }, trigger: 'axis' },
    xAxis: {
      axisLabel: { interval: 0, rotate: categories.length > 6 ? 30 : 0 },
      axisTick: { show: false },
      data: categories,
      type: 'category',
    },
    yAxis: { minInterval: 1, type: 'value' },
  };
}
/** é€šç”¨æŠ˜çº¿å›¾é…ç½® */
export function getLineChartOptions(
  dates: string[],
  values: number[],
  name: string,
  color = '#36cfc9',
): EChartsOption {
  return {
    grid: { bottom: 40, left: 60, right: 20, top: 30 },
    series: [
      { areaStyle: { opacity: 0.12 }, data: values, itemStyle: { color }, name, smooth: true, type: 'line' },
    ],
    tooltip: { trigger: 'axis' },
    xAxis: { axisTick: { show: false }, data: dates, type: 'category' },
    yAxis: { minInterval: 1, type: 'value' },
  };
}
/** è´¨æ£€æŒ‡æ ‡å‡å€¼ + åˆæ ¼çŽ‡åŒç³»åˆ—æŸ±çŠ¶å›¾é…ç½® */
export function getQualityIndicatorChartOptions(
  labels: string[],
  avgValues: number[],
  passRates: number[],
): EChartsOption {
  return {
    color: ['#409EFF', '#67c23a'],
    grid: { bottom: 40, left: 60, right: 60, top: 30 },
    legend: { bottom: 0, data: ['平均值', '合格率(%)'] },
    series: [
      { barMaxWidth: 30, data: avgValues, name: '平均值', type: 'bar' },
      { barMaxWidth: 30, data: passRates, name: '合格率(%)', type: 'bar' },
    ],
    tooltip: { trigger: 'axis' },
    xAxis: { axisTick: { show: false }, data: labels, type: 'category' },
    yAxis: [{ minInterval: 1, type: 'value' }],
  };
}
src/views/management-cockpit/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,207 @@
<script lang="ts" setup>
import type { ManagementCockpitApi } from '#/api/mes/management-cockpit';
import { ref } from 'vue';
import { Page } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message, Space } from 'ant-design-vue';
// å˜é‡å vPrint(v å¼€å¤´ï¼‰åœ¨ <script setup> ä¸­ä¼šè¢« Vue è‡ªåŠ¨æ³¨å†Œä¸ºå±€éƒ¨æŒ‡ä»¤ v-print
// @ts-expect-error - vue3-print-nb æœªæä¾›ç±»åž‹å£°æ˜Ž
import vPrint from 'vue3-print-nb';
import {
  exportManagementCockpitExcel,
  getManagementCockpitSummary,
} from '#/api/mes/management-cockpit';
import KpiCards from './modules/kpi-cards.vue';
import InventoryChart from './modules/inventory-chart.vue';
import OutboundChart from './modules/outbound-chart.vue';
import ProductionTrend from './modules/production-trend.vue';
import QualityChart from './modules/quality-chart.vue';
import TraceChart from './modules/trace-chart.vue';
defineOptions({ name: 'ManagementCockpit' });
// æ‰“印配置(打印报表容器)
const printObj = ref({
  id: 'management-cockpit-report',
  popTitle: '&nbsp;',
  extraCss: '/print.css',
  zIndex: 20_003,
});
const summary = ref<ManagementCockpitApi.Summary>({
  batchTotal: 0,
  batchDraft: 0,
  batchProducing: 0,
  batchFinished: 0,
  batchCanceled: 0,
  bagCodeTotal: 0,
  bagCodeGenerated: 0,
  bagCodePrinted: 0,
  bagCodeCanceled: 0,
  palletTotal: 0,
  todayInboundBagCount: 0,
  qualityTotal: 0,
  qualityPending: 0,
  qualityPass: 0,
  qualityFail: 0,
  qualityPassRate: 0,
  stockTotal: 0,
  stockTemporary: 0,
  stockQualified: 0,
  stockOutbound: 0,
  lowStockCount: 0,
  outboundOrderTotal: 0,
  outboundQuantityTotal: 0,
  todayOutboundQuantity: 0,
  scanTotal: 0,
  todayScanCount: 0,
  scanSuccessCount: 0,
  crossRegionWarningTotal: 0,
  crossRegionPendingCount: 0,
});
// é€šè¿‡é€’增 key è§¦å‘子组件重新挂载实现整体刷新
const reloadKey = ref(0);
const loading = ref(false);
/** åŠ è½½æ±‡æ€»ç»Ÿè®¡ */
async function loadSummary() {
  try {
    summary.value = await getManagementCockpitSummary();
  } catch {
    // æŽ¥å£å¼‚常时保留默认值,避免页面崩溃
  }
}
/** åˆ·æ–°å…¨éƒ¨æ•°æ® */
async function handleRefresh() {
  loading.value = true;
  try {
    await Promise.all([loadSummary()]);
    reloadKey.value += 1;
    message.success('已刷新');
  } finally {
    loading.value = false;
  }
}
/** å¯¼å‡º Excel æŠ¥è¡¨ï¼ˆåŽç«¯ç”Ÿæˆå¤š sheet å·¥ä½œç°¿ï¼‰ */
async function handleExportExcel() {
  try {
    const data = await exportManagementCockpitExcel();
    downloadFileFromBlobPart({
      fileName: `管理驾驶舱统计报表-${new Date().toLocaleDateString()}.xls`,
      source: data,
    });
    message.success('Excel æŠ¥è¡¨å·²å¯¼å‡º');
  } catch {
    // é”™è¯¯å·²ç”± requestClient ç»Ÿä¸€æç¤º
  }
}
/** å¯¼å‡º PDF æŠ¥è¡¨ï¼ˆå‰ç«¯ html2canvas + jspdf ç”Ÿæˆå¯ä¸‹è½½æ–‡ä»¶ï¼‰ */
async function handleExportPdf() {
  const target = document.getElementById('management-cockpit-report');
  if (!target) {
    message.warning('报表区域未就绪');
    return;
  }
  const hide = message.loading('正在生成 PDF,请稍候…', 0);
  try {
    // åŠ¨æ€åŠ è½½ï¼Œé¿å…æ‹–æ…¢é¦–å±ï¼ˆhtml-to-image é€šè¿‡ SVG foreignObject è®©æµè§ˆå™¨åŽŸç”Ÿæ¸²æŸ“ï¼Œ
    // è§„避 html2canvas ä¸æ”¯æŒ oklch ç­‰çް代 CSS é¢œè‰²å‡½æ•°çš„问题)
    const [{ toPng }, { jsPDF }] = await Promise.all([
      import('html-to-image'),
      import('jspdf'),
    ]);
    const dataUrl = await toPng(target, {
      backgroundColor: '#ffffff',
      cacheBust: true,
      pixelRatio: 2,
    });
    const pdf = new jsPDF({ compress: true, format: 'a4', orientation: 'p', unit: 'mm' });
    const pageWidth = pdf.internal.pageSize.getWidth();
    const pageHeight = pdf.internal.pageSize.getHeight();
    const imgWidth = pageWidth;
    const imgHeight = (target.offsetHeight * imgWidth) / target.offsetWidth;
    let heightLeft = imgHeight;
    let position = 0;
    pdf.addImage(dataUrl, 'PNG', 0, position, imgWidth, imgHeight);
    heightLeft -= pageHeight;
    while (heightLeft > 0) {
      position -= pageHeight;
      pdf.addPage();
      pdf.addImage(dataUrl, 'PNG', 0, position, imgWidth, imgHeight);
      heightLeft -= pageHeight;
    }
    pdf.save(`管理驾驶舱统计报表-${new Date().toLocaleDateString()}.pdf`);
    message.success('PDF æŠ¥è¡¨å·²å¯¼å‡º');
  } catch (error) {
    console.error('[management-cockpit] PDF å¯¼å‡ºå¤±è´¥ï¼š', error);
    message.error(
      `PDF å¯¼å‡ºå¤±è´¥ï¼š${error instanceof Error ? error.message : '未知错误'},请打开浏览器控制台(F12)查看详情`,
    );
  } finally {
    hide();
  }
}
/** åŠ è½½æ±‡æ€»ç»Ÿè®¡ */
async function loadInitial() {
  await loadSummary();
}
loadInitial();
</script>
<template>
  <Page>
    <!-- å·¥å…·æ  -->
    <div class="mb-4 flex flex-wrap items-center justify-between gap-3">
      <div class="flex items-center gap-2">
        <IconifyIcon class="text-xl text-blue-500" icon="lucide:gauge" />
        <span class="text-base font-semibold">管理驾驶舱与统计分析</span>
      </div>
      <Space>
        <Button :loading="loading" @click="handleRefresh">
          <template #icon><IconifyIcon icon="lucide:refresh-cw" /></template>
          åˆ·æ–°
        </Button>
        <Button v-access:code="['mes:dashboard:export']" type="primary" @click="handleExportExcel">
          <template #icon><IconifyIcon icon="lucide:file-spreadsheet" /></template>
          å¯¼å‡º Excel
        </Button>
        <Button v-access:code="['mes:dashboard:export']" type="primary" ghost @click="handleExportPdf">
          <template #icon><IconifyIcon icon="lucide:file-down" /></template>
          å¯¼å‡º PDF
        </Button>
        <Button v-print="printObj">
          <template #icon><IconifyIcon icon="lucide:printer" /></template>
          æ‰“印
        </Button>
      </Space>
    </div>
    <!-- æŠ¥è¡¨å†…容(Excel/PDF/打印 å‡åŸºäºŽæ­¤åŒºåŸŸï¼‰ -->
    <div id="management-cockpit-report" class="rounded-lg bg-white p-4 dark:bg-gray-800">
      <!-- äº”维度 KPI -->
      <KpiCards :key="`kpi-${reloadKey}`" :summary="summary" class="mb-2" />
      <!-- ç”Ÿäº§è¶‹åŠ¿ -->
      <ProductionTrend :key="`pt-${reloadKey}`" class="mb-4" />
      <QualityChart :key="`qc-${reloadKey}`" class="mb-4" />
      <InventoryChart :key="`iv-${reloadKey}`" class="mb-4" />
      <OutboundChart :key="`ob-${reloadKey}`" class="mb-4" />
      <TraceChart :key="`tr-${reloadKey}`" class="mb-4" />
    </div>
  </Page>
</template>
src/views/management-cockpit/modules/inventory-chart.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,82 @@
<script lang="ts" setup>
import type { EchartsUIType } from '@vben/plugins/echarts';
import { onMounted, ref } from 'vue';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { Card, Col, Row, Spin } from 'ant-design-vue';
import { getManagementCockpitInventory } from '#/api/mes/management-cockpit';
import { getBarChartOptions, getPieChartOptions } from '../chart-options';
defineOptions({ name: 'ManagementCockpitInventoryChart' });
const loading = ref(false);
const warehouseRef = ref<EchartsUIType>();
const itemRef = ref<EchartsUIType>();
const stateRef = ref<EchartsUIType>();
const { renderEcharts: renderWarehouse } = useEcharts(warehouseRef);
const { renderEcharts: renderItem } = useEcharts(itemRef);
const { renderEcharts: renderState } = useEcharts(stateRef);
/** åŠ è½½åº“å­˜ç»Ÿè®¡å¹¶æ¸²æŸ“ */
async function loadData() {
  loading.value = true;
  try {
    const data = await getManagementCockpitInventory();
    const warehouses = data.warehouseDistribution || [];
    const items = data.itemDistribution || [];
    const states = data.stateDistribution || [];
    await renderWarehouse(
      getBarChartOptions(
        warehouses.map((w) => w.name),
        warehouses.map((w) => w.count),
        '库存数量',
        '#409EFF',
      ),
    );
    await renderItem(
      getBarChartOptions(
        items.map((i) => i.name),
        items.map((i) => i.count),
        '库存数量',
        '#36cfc9',
      ),
    );
    await renderState(
      getPieChartOptions(
        states.map((s) => ({ name: s.name, value: s.count })),
      ),
    );
  } finally {
    loading.value = false;
  }
}
onMounted(() => {
  loadData();
});
</script>
<template>
  <Card title="库存维度统计" class="h-full">
    <Spin :spinning="loading">
      <Row :gutter="16">
        <Col :lg="9" :md="24" :sm="24" :xs="24">
          <div class="mb-1 text-sm text-gray-500">按仓库分布</div>
          <EchartsUI ref="warehouseRef" class="h-[280px] w-full" />
        </Col>
        <Col :lg="9" :md="24" :sm="24" :xs="24">
          <div class="mb-1 text-sm text-gray-500">按品种分布</div>
          <EchartsUI ref="itemRef" class="h-[280px] w-full" />
        </Col>
        <Col :lg="6" :md="24" :sm="24" :xs="24">
          <div class="mb-1 text-sm text-gray-500">按库存状态</div>
          <EchartsUI ref="stateRef" class="h-[280px] w-full" />
        </Col>
      </Row>
    </Spin>
  </Card>
</template>
src/views/management-cockpit/modules/kpi-cards.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,125 @@
<script lang="ts" setup>
import type { ManagementCockpitApi } from '#/api/mes/management-cockpit';
import { CountTo } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { Card, Col, Row } from 'ant-design-vue';
defineOptions({ name: 'ManagementCockpitKpiCards' });
const props = defineProps<{
  summary: ManagementCockpitApi.Summary;
}>();
/** ä¿ç•™ä¸¤ä½å°æ•° */
function fmt(value?: number): number {
  return Number((value ?? 0).toFixed(2));
}
/** äº”维度 KPI å¡é…ç½® */
const cards = [
  {
    dimension: '生产',
    gradient: 'linear-gradient(135deg, #0ea5e9, #38bdf8)',
    icon: 'lucide:factory',
    mainLabel: '生产批次',
    mainValue: () => props.summary.batchTotal,
    items: [
      { label: '袋码', value: () => props.summary.bagCodeTotal },
      { label: '托盘码', value: () => props.summary.palletTotal },
      { label: '今日入库', value: () => props.summary.todayInboundBagCount },
    ],
  },
  {
    dimension: '质检',
    gradient: 'linear-gradient(135deg, #67c23a, #95d475)',
    icon: 'lucide:clipboard-check',
    mainLabel: '合格率(%)',
    mainValue: () => fmt(props.summary.qualityPassRate),
    items: [
      { label: '合格', value: () => props.summary.qualityPass },
      { label: '不合格', value: () => props.summary.qualityFail },
      { label: '处理中', value: () => props.summary.qualityPending },
    ],
  },
  {
    dimension: '库存',
    gradient: 'linear-gradient(135deg, #f59e0b, #fbbf24)',
    icon: 'lucide:warehouse',
    mainLabel: '库存总量',
    mainValue: () => fmt(props.summary.stockTotal),
    items: [
      { label: '暂存库', value: () => fmt(props.summary.stockTemporary) },
      { label: '合格库', value: () => fmt(props.summary.stockQualified) },
      { label: '低库存预警', value: () => props.summary.lowStockCount },
    ],
  },
  {
    dimension: '出库',
    gradient: 'linear-gradient(135deg, #ec4899, #f472b6)',
    icon: 'lucide:package-open',
    mainLabel: '出库总量',
    mainValue: () => fmt(props.summary.outboundQuantityTotal),
    items: [
      { label: '出库单数', value: () => props.summary.outboundOrderTotal },
      { label: '今日出库', value: () => fmt(props.summary.todayOutboundQuantity) },
    ],
  },
  {
    dimension: '追溯',
    gradient: 'linear-gradient(135deg, #14b8a6, #2dd4bf)',
    icon: 'lucide:scan-search',
    mainLabel: '扫码总数',
    mainValue: () => props.summary.scanTotal,
    items: [
      { label: '今日扫码', value: () => props.summary.todayScanCount },
      { label: '窜货预警', value: () => props.summary.crossRegionWarningTotal },
      { label: '待处理', value: () => props.summary.crossRegionPendingCount },
    ],
  },
];
</script>
<template>
  <Row :gutter="16">
    <Col
      v-for="card in cards"
      :key="card.dimension"
      :lg="8"
      :md="12"
      :sm="12"
      :xl="8"
      :xs="24"
      class="mb-4"
    >
      <Card class="kpi-card h-full">
        <div class="flex items-start justify-between">
          <div class="flex items-center gap-3">
            <div
              class="flex size-12 flex-shrink-0 items-center justify-center rounded-xl text-white"
              :style="{ background: card.gradient }"
            >
              <IconifyIcon class="size-6" :icon="card.icon" />
            </div>
            <div>
              <div class="text-muted-foreground text-sm">{{ card.dimension }}</div>
              <CountTo
                class="text-xl font-bold leading-tight"
                :end-val="card.mainValue()"
                :duration="1200"
              />
              <div class="text-muted-foreground text-xs">{{ card.mainLabel }}</div>
            </div>
          </div>
        </div>
        <div class="mt-3 flex flex-wrap gap-x-4 gap-y-1 border-t border-dashed pt-3">
          <span v-for="item in card.items" :key="item.label" class="text-xs text-gray-500">
            {{ item.label }}:
            <b class="text-sm text-gray-800 dark:text-gray-200">{{ item.value() }}</b>
          </span>
        </div>
      </Card>
    </Col>
  </Row>
</template>
src/views/management-cockpit/modules/outbound-chart.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,77 @@
<script lang="ts" setup>
import type { EchartsUIType } from '@vben/plugins/echarts';
import { onMounted, ref } from 'vue';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { Card, Col, RadioButton, RadioGroup, Row, Spin } from 'ant-design-vue';
import { getManagementCockpitOutbound } from '#/api/mes/management-cockpit';
import { getLineChartOptions, getPieChartOptions } from '../chart-options';
defineOptions({ name: 'ManagementCockpitOutboundChart' });
const trendDays = ref(30);
const loading = ref(false);
const trendRef = ref<EchartsUIType>();
const dealerRef = ref<EchartsUIType>();
const { renderEcharts: renderTrend } = useEcharts(trendRef);
const { renderEcharts: renderDealer } = useEcharts(dealerRef);
/** åŠ è½½å‡ºåº“ç»Ÿè®¡å¹¶æ¸²æŸ“ */
async function loadData() {
  loading.value = true;
  try {
    const data = await getManagementCockpitOutbound(trendDays.value);
    const trend = data.trend || [];
    const dealers = data.dealerDistribution || [];
    await renderTrend(
      getLineChartOptions(
        trend.map((d) => d.date.slice(5)),
        trend.map((d) => Number(d.count ?? 0)),
        '出库数量',
      ),
    );
    await renderDealer(
      getPieChartOptions(dealers.map((d) => ({ name: d.name, value: d.count }))),
    );
  } finally {
    loading.value = false;
  }
}
/** åˆ‡æ¢å¤©æ•°èŒƒå›´ */
function handleDaysChange() {
  loadData();
}
onMounted(() => {
  loadData();
});
</script>
<template>
  <Card title="出库维度统计" class="h-full">
    <template #extra>
      <RadioGroup v-model:value="trendDays" size="small" @change="handleDaysChange">
        <RadioButton :value="7">近 7 å¤©</RadioButton>
        <RadioButton :value="30">近 30 å¤©</RadioButton>
        <RadioButton :value="90">近 90 å¤©</RadioButton>
      </RadioGroup>
    </template>
    <Spin :spinning="loading">
      <Row :gutter="16">
        <Col :lg="15" :md="24" :sm="24" :xs="24">
          <div class="mb-1 text-sm text-gray-500">出库趋势</div>
          <EchartsUI ref="trendRef" class="h-[280px] w-full" />
        </Col>
        <Col :lg="9" :md="24" :sm="24" :xs="24">
          <div class="mb-1 text-sm text-gray-500">经销商出库占比(TOP 20)</div>
          <EchartsUI ref="dealerRef" class="h-[280px] w-full" />
        </Col>
      </Row>
    </Spin>
  </Card>
</template>
src/views/management-cockpit/modules/production-trend.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
<script lang="ts" setup>
import type { EchartsUIType } from '@vben/plugins/echarts';
import { onMounted, ref } from 'vue';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { Card, RadioButton, RadioGroup } from 'ant-design-vue';
import { getManagementCockpitProductionTrend } from '#/api/mes/management-cockpit';
import { getProductionTrendChartOptions } from '../chart-options';
defineOptions({ name: 'ManagementCockpitProductionTrend' });
const trendDays = ref(30); // å½“前天数范围
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);
/** åŠ è½½ç”Ÿäº§è¶‹åŠ¿æ•°æ®å¹¶æ¸²æŸ“ */
async function loadData() {
  const data = await getManagementCockpitProductionTrend(trendDays.value);
  const dates = data.map((d) => d.date.slice(5));
  await renderEcharts(
    getProductionTrendChartOptions(
      dates,
      data.map((d) => d.batchCount),
      data.map((d) => d.bagCodeCount),
      data.map((d) => d.palletCount),
      data.map((d) => Number(d.inboundBagQuantity ?? 0)),
    ),
  );
}
/** åˆ‡æ¢å¤©æ•°èŒƒå›´ */
function handleDaysChange() {
  loadData();
}
onMounted(() => {
  loadData();
});
</script>
<template>
  <Card title="生产趋势(近 N å¤©ï¼‰" class="h-full">
    <template #extra>
      <RadioGroup v-model:value="trendDays" size="small" @change="handleDaysChange">
        <RadioButton :value="7">近 7 å¤©</RadioButton>
        <RadioButton :value="30">近 30 å¤©</RadioButton>
        <RadioButton :value="90">近 90 å¤©</RadioButton>
      </RadioGroup>
    </template>
    <EchartsUI ref="chartRef" class="h-[320px] w-full" />
  </Card>
</template>
src/views/management-cockpit/modules/quality-chart.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,67 @@
<script lang="ts" setup>
import type { EchartsUIType } from '@vben/plugins/echarts';
import { onMounted, ref } from 'vue';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { Card, Col, Row, Spin } from 'ant-design-vue';
import { getManagementCockpitQuality } from '#/api/mes/management-cockpit';
import { getPieChartOptions, getQualityIndicatorChartOptions } from '../chart-options';
defineOptions({ name: 'ManagementCockpitQualityChart' });
const loading = ref(false);
const resultChartRef = ref<EchartsUIType>();
const indicatorChartRef = ref<EchartsUIType>();
const { renderEcharts: renderResult } = useEcharts(resultChartRef);
const { renderEcharts: renderIndicator } = useEcharts(indicatorChartRef);
/** åŠ è½½è´¨æ£€ç»Ÿè®¡å¹¶æ¸²æŸ“ */
async function loadData() {
  loading.value = true;
  try {
    const data = await getManagementCockpitQuality();
    // 1. æ£€éªŒç»“果分布环形图
    const pieData = (data.resultDistribution || []).map((item) => ({
      name: item.name,
      value: item.count,
    }));
    await renderResult(getPieChartOptions(pieData));
    // 2. å››é¡¹æŒ‡æ ‡å‡å€¼ + åˆæ ¼çŽ‡æŸ±çŠ¶å›¾
    const indicators = data.indicatorList || [];
    await renderIndicator(
      getQualityIndicatorChartOptions(
        indicators.map((i) => i.indicatorName || i.indicatorCode),
        indicators.map((i) => Number(i.averageValue ?? 0)),
        indicators.map((i) => Number(i.passRate ?? 0)),
      ),
    );
  } finally {
    loading.value = false;
  }
}
onMounted(() => {
  loadData();
});
</script>
<template>
  <Card title="质检维度统计" class="h-full">
    <Spin :spinning="loading">
      <Row :gutter="16">
        <Col :lg="10" :md="24" :sm="24" :xs="24">
          <div class="mb-1 text-sm text-gray-500">检验结果分布</div>
          <EchartsUI ref="resultChartRef" class="h-[280px] w-full" />
        </Col>
        <Col :lg="14" :md="24" :sm="24" :xs="24">
          <div class="mb-1 text-sm text-gray-500">四项指标均值与合格率</div>
          <EchartsUI ref="indicatorChartRef" class="h-[280px] w-full" />
        </Col>
      </Row>
    </Spin>
  </Card>
</template>
src/views/management-cockpit/modules/trace-chart.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,125 @@
<script lang="ts" setup>
import type { EchartsUIType } from '@vben/plugins/echarts';
import { onMounted, ref } from 'vue';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { Card, Col, Row, Spin, Statistic } from 'ant-design-vue';
import { getManagementCockpitTrace } from '#/api/mes/management-cockpit';
import { getBarChartOptions, getLineChartOptions, getPieChartOptions } from '../chart-options';
defineOptions({ name: 'ManagementCockpitTraceChart' });
const loading = ref(false);
const scanTrendRef = ref<EchartsUIType>();
const provinceRef = ref<EchartsUIType>();
const channelRef = ref<EchartsUIType>();
const { renderEcharts: renderScanTrend } = useEcharts(scanTrendRef);
const { renderEcharts: renderProvince } = useEcharts(provinceRef);
const { renderEcharts: renderChannel } = useEcharts(channelRef);
// çªœè´§é¢„警汇总(用于展示卡片)
const crossRegion = ref({
  total: 0,
  pending: 0,
  handled: 0,
  falseAlarm: 0,
  crossProvince: 0,
  crossCity: 0,
});
/** åŠ è½½è¿½æº¯ç»Ÿè®¡å¹¶æ¸²æŸ“ */
async function loadData() {
  loading.value = true;
  try {
    const data = await getManagementCockpitTrace(30);
    const scanTrend = data.scanTrend || [];
    const provinces = data.provinceDistribution || [];
    const channels = data.channelDistribution || [];
    crossRegion.value = data.crossRegionSummary || crossRegion.value;
    await renderScanTrend(
      getLineChartOptions(
        scanTrend.map((d) => d.date.slice(5)),
        scanTrend.map((d) => Number(d.count ?? 0)),
        '扫码次数',
        '#409EFF',
      ),
    );
    await renderProvince(
      getBarChartOptions(
        provinces.map((p) => p.name),
        provinces.map((p) => p.count),
        '扫码次数',
        '#67c23a',
      ),
    );
    await renderChannel(
      getPieChartOptions(channels.map((c) => ({ name: c.name, value: c.count }))),
    );
  } finally {
    loading.value = false;
  }
}
onMounted(() => {
  loadData();
});
</script>
<template>
  <Card title="追溯维度统计" class="h-full">
    <Spin :spinning="loading">
      <!-- çªœè´§é¢„警汇总卡片 -->
      <Row :gutter="16" class="mb-2">
        <Col :lg="4" :md="8" :sm="12" :xs="24" class="mb-2">
          <Card size="small">
            <Statistic title="预警总数" :value="crossRegion.total" />
          </Card>
        </Col>
        <Col :lg="4" :md="8" :sm="12" :xs="24" class="mb-2">
          <Card size="small">
            <Statistic title="待处理" :value="crossRegion.pending" :value-style="{ color: '#f5222d' }" />
          </Card>
        </Col>
        <Col :lg="4" :md="8" :sm="12" :xs="24" class="mb-2">
          <Card size="small">
            <Statistic title="已处理" :value="crossRegion.handled" />
          </Card>
        </Col>
        <Col :lg="4" :md="8" :sm="12" :xs="24" class="mb-2">
          <Card size="small">
            <Statistic title="误报" :value="crossRegion.falseAlarm" />
          </Card>
        </Col>
        <Col :lg="4" :md="8" :sm="12" :xs="24" class="mb-2">
          <Card size="small">
            <Statistic title="跨省" :value="crossRegion.crossProvince" />
          </Card>
        </Col>
        <Col :lg="4" :md="8" :sm="12" :xs="24" class="mb-2">
          <Card size="small">
            <Statistic title="跨市" :value="crossRegion.crossCity" />
          </Card>
        </Col>
      </Row>
      <!-- æ‰«ç è¶‹åŠ¿ + çœä»½åˆ†å¸ƒ + æ¸ é“分布 -->
      <Row :gutter="16">
        <Col :lg="10" :md="24" :sm="24" :xs="24">
          <div class="mb-1 text-sm text-gray-500">消费者扫码趋势</div>
          <EchartsUI ref="scanTrendRef" class="h-[280px] w-full" />
        </Col>
        <Col :lg="9" :md="24" :sm="24" :xs="24">
          <div class="mb-1 text-sm text-gray-500">扫码省份分布</div>
          <EchartsUI ref="provinceRef" class="h-[280px] w-full" />
        </Col>
        <Col :lg="5" :md="24" :sm="24" :xs="24">
          <div class="mb-1 text-sm text-gray-500">扫码渠道分布</div>
          <EchartsUI ref="channelRef" class="h-[280px] w-full" />
        </Col>
      </Row>
    </Spin>
  </Card>
</template>
src/views/mes/pro/components/qr-trace-modal.vue
@@ -3,7 +3,7 @@
  import type { MesProBatchApi } from "#/api/mes/pro/batch";
  import type { MesProPalletApi } from "#/api/mes/pro/pallet";
  import { ref } from "vue";
  import { computed, ref } from "vue";
  import { Barcode, useVbenModal } from "@vben/common-ui";
  import { DICT_TYPE } from "@vben/constants";
@@ -14,6 +14,7 @@
  import { getBagCodeByCode } from "#/api/mes/pro/bagCode";
  import { getBatchByCode } from "#/api/mes/pro/batch";
  import { getPalletByCode } from "#/api/mes/pro/pallet";
  import { getQrConfig } from "#/api/mes/trace/config";
  import { useDescription } from "#/components/description";
  defineOptions({ name: "MesProQrTraceModal" });
@@ -34,6 +35,42 @@
  const barcodeRef = ref<InstanceType<typeof Barcode>>();
  const traceTypeLabel = ref("");
  /** æº¯æº H5 é¡µé¢åŸºç¡€åœ°å€ï¼ˆä¼šè¯å†…只拉取一次) */
  const traceBaseUrl = ref<string>("");
  async function ensureTraceBaseUrl() {
    if (traceBaseUrl.value) return;
    try {
      const res = await getQrConfig();
      traceBaseUrl.value = res.traceBaseUrl || "";
    } catch {
      traceBaseUrl.value = "";
    }
  }
  /** äºŒç»´ç å±•示内容:配置了溯源地址则拼 H5 URL,否则回退纯码值(与后端 buildQrCodeContent ä¸€è‡´ï¼‰ */
  const qrContent = computed(() => {
    if (!currentCode.value) return "";
    return traceBaseUrl.value
      ? `${traceBaseUrl.value}?code=${encodeURIComponent(currentCode.value)}`
      : currentCode.value;
  });
  /** ä»Žå®Œæ•´ H5 é“¾æŽ¥ä¸­æå–追溯码(兼容扫码枪扫描打印标签链接) */
  function extractCodeFromUrl(raw: string): string {
    const trimmed = raw.trim();
    if (/^https?:\/\//i.test(trimmed)) {
      try {
        const url = new URL(trimmed);
        const code = url.searchParams.get("code");
        if (code) return code;
      } catch {
        // è§£æžå¤±è´¥æŒ‰åŽŸå§‹è¾“å…¥å¤„ç†
      }
    }
    return trimmed;
  }
  /** è¯†åˆ«äºŒç»´ç å†…容类型:批次号(含临时批次码)/ æ‰˜ç›˜ç ï¼ˆå«ä¸´æ—¶æ‰˜ç›˜ç ï¼‰/ è¢‹ç  */
  function resolveCodeType(raw: string): TraceType | undefined {
@@ -66,7 +103,8 @@
  /** æ‰«ç /输入二维码内容查询追溯信息 */
  async function handleSearch(raw?: string) {
    const value = (raw ?? codeInput.value)?.trim().toUpperCase();
    const input = (raw ?? codeInput.value)?.trim() ?? "";
    const value = extractCodeFromUrl(input).toUpperCase();
    if (!value) {
      message.warning("请扫描或输入二维码内容");
      return;
@@ -265,6 +303,7 @@
      if (!isOpen) {
        return;
      }
      ensureTraceBaseUrl();
      const data = modalApi.getData<{ code?: string }>() || {};
      codeInput.value = data.code ?? undefined;
      resetState();
@@ -512,7 +551,7 @@
            class="mb-4 flex items-center gap-6 rounded bg-gray-100 p-5"
          >
            <div class="flex-shrink-0">
              <Barcode ref="barcodeRef" :content="currentCode" :width="200" />
              <Barcode ref="barcodeRef" :content="qrContent" :width="200" />
            </div>
            <div class="min-w-0 flex-1">
              <p class="text-lg font-bold break-all">{{ currentCode }}</p>
src/views/mes/trace-consumer-scan/chart-options.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,62 @@
import type { EChartsOption } from '@vben/plugins/echarts';
/** æ¶ˆè´¹è€…扫码访问趋势折线图配置 */
export function getScanTrendChartOptions(
  dates: string[],
  totals: number[],
  success: number[],
  failed: number[],
): EChartsOption {
  return {
    grid: { bottom: 40, left: 50, right: 20, top: 20 },
    legend: { bottom: 0, data: ['总访问', '成功', '失败'] },
    series: [
      {
        areaStyle: { color: 'rgba(64,158,255,0.15)' },
        data: totals,
        itemStyle: { color: '#409EFF' },
        name: '总访问',
        smooth: true,
        type: 'line',
      },
      {
        data: success,
        itemStyle: { color: '#67C23A' },
        name: '成功',
        smooth: true,
        type: 'line',
      },
      {
        data: failed,
        itemStyle: { color: '#F56C6C' },
        name: '失败',
        smooth: true,
        type: 'line',
      },
    ],
    tooltip: { axisPointer: { type: 'cross' }, trigger: 'axis' },
    xAxis: { boundaryGap: false, data: dates, type: 'category' },
    yAxis: { minInterval: 1, type: 'value' },
  };
}
/** æ¶ˆè´¹è€…扫码访问分布饼图配置 */
export function getScanDistributionChartOptions(
  data: Array<{ itemStyle: { color: string }; name: string; value: number }>,
): EChartsOption {
  return {
    legend: { bottom: 0, type: 'scroll' },
    series: [
      {
        avoidLabelOverlap: true,
        data,
        emphasis: { label: { fontSize: 14, fontWeight: 'bold', show: true } },
        itemStyle: { borderColor: '#fff', borderRadius: 6, borderWidth: 2 },
        label: { formatter: '{b}\n{c}', show: true },
        radius: ['40%', '70%'],
        type: 'pie',
      },
    ],
    tooltip: { formatter: '{b}: {c} ({d}%)', trigger: 'item' },
  };
}
src/views/mes/trace-consumer-scan/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
import type { MesTraceConsumerScanApi } from '#/api/mes/trace-consumer-scan';
/** æ¶ˆè´¹è€…扫码访问汇总默认值 */
export const defaultSummary: MesTraceConsumerScanApi.Summary = {
  total: 0,
  success: 0,
  failed: 0,
  uniqueBatchCount: 0,
};
/** åˆ†å¸ƒåˆ†ç»„维度选项 */
export const GROUP_BY_OPTIONS: Array<{ label: string; value: string }> = [
  { label: '扫码类型', value: 'sourceType' },
  { label: '查询结果', value: 'success' },
  { label: '扫码渠道', value: 'channel' },
  { label: '省份', value: 'province' },
  { label: '城市', value: 'city' },
  { label: '生产批次', value: 'batchId' },
];
/** æ‰«ç ç±»åž‹åç§°æ˜ å°„(后端存储原始码,展示时转换) */
export const SOURCE_TYPE_LABEL_MAP: Record<string, string> = {
  BAG: '袋码',
  PALLET: '托盘码',
  BATCH: '批次码',
};
/** åˆ†å¸ƒå›¾è¡¨é¢œè‰²é›†åˆ */
export const DISTRIBUTION_COLORS = [
  '#409EFF',
  '#67C23A',
  '#E6A23C',
  '#F56C6C',
  '#909399',
  '#7C3AED',
  '#00BCD4',
  '#FF9800',
  '#4CAF50',
  '#E91E63',
];
src/views/mes/trace-consumer-scan/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { Card, Col, DatePicker, Row } from 'ant-design-vue';
import dayjs from 'dayjs';
import { getRangePickerDefaultProps } from '#/utils';
import DistributionChart from './modules/distribution-chart.vue';
import SummaryCards from './modules/summary-cards.vue';
import TrendChart from './modules/trend-chart.vue';
defineOptions({ name: 'MesConsumerScanStatistics' });
/** ç»Ÿè®¡æ—¶é—´èŒƒå›´ï¼Œé»˜è®¤è¿‘ 30 å¤©ï¼ˆvalueFormat å·²è½¬ä¸º yyyy-MM-dd HH:mm:ss) */
const dateRange = ref<[string, string] | undefined>([
  dayjs().subtract(30, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'),
  dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss'),
]);
const startTime = computed(() => dateRange.value?.[0]);
const endTime = computed(() => dateRange.value?.[1]);
</script>
<template>
  <Page>
    <Card title="消费者扫码统计" class="mb-4">
      <div class="flex flex-wrap items-center gap-2">
        <span class="text-muted-foreground text-sm">统计时间范围</span>
        <DatePicker.RangePicker
          v-model:value="dateRange"
          v-bind="getRangePickerDefaultProps()"
        />
      </div>
    </Card>
    <SummaryCards :start-time="startTime" :end-time="endTime" />
    <Row :gutter="16">
      <Col :lg="14" :md="24" :sm="24" :xl="14" :xs="24" class="mb-4">
        <TrendChart :start-time="startTime" :end-time="endTime" />
      </Col>
      <Col :lg="10" :md="24" :sm="24" :xl="10" :xs="24" class="mb-4">
        <DistributionChart :start-time="startTime" :end-time="endTime" />
      </Col>
    </Row>
  </Page>
</template>
src/views/mes/trace-consumer-scan/modules/distribution-chart.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,99 @@
<script lang="ts" setup>
import type { EchartsUIType } from '@vben/plugins/echarts';
import { computed, ref, watch } from 'vue';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { Card, Empty, Select } from 'ant-design-vue';
import { getTraceConsumerScanDistribution } from '#/api/mes/trace-consumer-scan';
import { getScanDistributionChartOptions } from '../chart-options';
import { DISTRIBUTION_COLORS, GROUP_BY_OPTIONS, SOURCE_TYPE_LABEL_MAP } from '../data';
defineOptions({ name: 'MesTraceConsumerScanDistributionChart' });
const props = defineProps<{
  startTime?: string;
  endTime?: string;
}>();
const groupBy = ref('sourceType'); // å½“前分组维度
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);
/** åˆ†å¸ƒæ•°æ®æ˜¯å¦ä¸ºç©º */
const hasData = ref(false);
/** å°†åŽç«¯åŽŸå§‹åˆ†ç»„åç§°è½¬ä¸ºå¯è¯»åç§° */
function displayName(name: string): string {
  if (groupBy.value === 'sourceType') {
    return SOURCE_TYPE_LABEL_MAP[name] || name;
  }
  return name;
}
/** åŠ è½½åˆ†å¸ƒæ•°æ®å¹¶æ¸²æŸ“é¥¼å›¾ */
async function loadData() {
  const data = await getTraceConsumerScanDistribution(
    groupBy.value,
    props.startTime,
    props.endTime,
  );
  hasData.value = data.length > 0;
  const chartData = data.map((d, index) => ({
    itemStyle: {
      color: DISTRIBUTION_COLORS[index % DISTRIBUTION_COLORS.length] ?? '#409EFF',
    },
    name: displayName(d.name),
    value: d.count,
  }));
  await renderEcharts(getScanDistributionChartOptions(chartData));
}
/** åˆ‡æ¢åˆ†ç»„维度 */
function handleGroupByChange() {
  loadData();
}
watch(
  () => [props.startTime, props.endTime],
  () => {
    loadData();
  },
  { immediate: true },
);
/** å½“前分组维度的展示名称 */
const groupByLabel = computed(
  () =>
    GROUP_BY_OPTIONS.find((opt) => opt.value === groupBy.value)?.label ||
    '分布',
);
</script>
<template>
  <Card :title="`${groupByLabel}分布`" class="h-full">
    <template #extra>
      <Select
        v-model:value="groupBy"
        size="small"
        style="width: 120px"
        @change="handleGroupByChange"
      >
        <Select.Option
          v-for="opt in GROUP_BY_OPTIONS"
          :key="opt.value"
          :value="opt.value"
        >
          {{ opt.label }}
        </Select.Option>
      </Select>
    </template>
    <div v-if="hasData" class="relative">
      <EchartsUI ref="chartRef" class="h-[320px] w-full" />
    </div>
    <Empty v-else description="暂无扫码数据" class="h-[320px]" />
  </Card>
</template>
src/views/mes/trace-consumer-scan/modules/summary-cards.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,130 @@
<script lang="ts" setup>
import type { MesTraceConsumerScanApi } from '#/api/mes/trace-consumer-scan';
import { ref, watch } from 'vue';
import { CountTo } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { Card, Col, Row } from 'ant-design-vue';
import { getTraceConsumerScanSummary } from '#/api/mes/trace-consumer-scan';
import { defaultSummary } from '../data';
defineOptions({ name: 'MesTraceConsumerScanSummaryCards' });
const props = defineProps<{
  startTime?: string;
  endTime?: string;
}>();
const summary = ref<MesTraceConsumerScanApi.Summary>(defaultSummary);
/** åŠ è½½æ±‡æ€»æ•°æ® */
async function loadData() {
  summary.value = await getTraceConsumerScanSummary(
    props.startTime,
    props.endTime,
  );
}
watch(
  () => [props.startTime, props.endTime],
  () => {
    loadData();
  },
  { immediate: true },
);
</script>
<template>
  <Row :gutter="16">
    <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4">
      <Card class="kpi-card">
        <div class="flex items-center gap-4">
          <div
            class="flex size-14 flex-shrink-0 items-center justify-center rounded-xl text-white"
            style="background: linear-gradient(135deg, #409eff, #66b1ff)"
          >
            <IconifyIcon class="size-7" icon="lucide:scan-line" />
          </div>
          <div>
            <div class="text-muted-foreground mb-1 text-sm">总访问次数</div>
            <CountTo
              class="text-2xl font-bold leading-tight text-[#409eff]"
              :end-val="summary.total"
              :duration="1500"
            />
            <div class="text-muted-foreground mt-1 text-xs">全部扫码查询</div>
          </div>
        </div>
      </Card>
    </Col>
    <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4">
      <Card class="kpi-card">
        <div class="flex items-center gap-4">
          <div
            class="flex size-14 flex-shrink-0 items-center justify-center rounded-xl text-white"
            style="background: linear-gradient(135deg, #67c23a, #85ce61)"
          >
            <IconifyIcon class="size-7" icon="lucide:circle-check" />
          </div>
          <div>
            <div class="text-muted-foreground mb-1 text-sm">成功查询</div>
            <CountTo
              class="text-2xl font-bold leading-tight text-[#67c23a]"
              :end-val="summary.success"
              :duration="1500"
            />
            <div class="text-muted-foreground mt-1 text-xs">
              æº¯æºä¿¡æ¯æˆåŠŸè¿”å›ž
            </div>
          </div>
        </div>
      </Card>
    </Col>
    <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4">
      <Card class="kpi-card">
        <div class="flex items-center gap-4">
          <div
            class="flex size-14 flex-shrink-0 items-center justify-center rounded-xl text-white"
            style="background: linear-gradient(135deg, #f56c6c, #f78989)"
          >
            <IconifyIcon class="size-7" icon="lucide:circle-x" />
          </div>
          <div>
            <div class="text-muted-foreground mb-1 text-sm">失败查询</div>
            <CountTo
              class="text-2xl font-bold leading-tight text-[#f56c6c]"
              :end-val="summary.failed"
              :duration="1500"
            />
            <div class="text-muted-foreground mt-1 text-xs">码不存在或无效</div>
          </div>
        </div>
      </Card>
    </Col>
    <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4">
      <Card class="kpi-card">
        <div class="flex items-center gap-4">
          <div
            class="flex size-14 flex-shrink-0 items-center justify-center rounded-xl text-white"
            style="background: linear-gradient(135deg, #7c3aed, #9461f5)"
          >
            <IconifyIcon class="size-7" icon="lucide:layers" />
          </div>
          <div>
            <div class="text-muted-foreground mb-1 text-sm">涉及批次</div>
            <CountTo
              class="text-2xl font-bold leading-tight text-[#7c3aed]"
              :end-val="summary.uniqueBatchCount"
              :duration="1500"
            />
            <div class="text-muted-foreground mt-1 text-xs">去重生产批次</div>
          </div>
        </div>
      </Card>
    </Col>
  </Row>
</template>
src/views/mes/trace-consumer-scan/modules/trend-chart.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
<script lang="ts" setup>
import type { EchartsUIType } from '@vben/plugins/echarts';
import { ref, watch } from 'vue';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { Card } from 'ant-design-vue';
import { getTraceConsumerScanTrend } from '#/api/mes/trace-consumer-scan';
import { getScanTrendChartOptions } from '../chart-options';
defineOptions({ name: 'MesTraceConsumerScanTrendChart' });
const props = defineProps<{
  startTime?: string;
  endTime?: string;
}>();
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);
/** åŠ è½½è¶‹åŠ¿æ•°æ®å¹¶æ¸²æŸ“æŠ˜çº¿å›¾ */
async function loadData() {
  const data = await getTraceConsumerScanTrend(props.startTime, props.endTime);
  const dates = data.map((d) => d.date.slice(5)); // yyyy-MM-dd -> MM-dd
  const totals = data.map((d) => d.total);
  const success = data.map((d) => d.success);
  const failed = data.map((d) => d.failed);
  await renderEcharts(getScanTrendChartOptions(dates, totals, success, failed));
}
watch(
  () => [props.startTime, props.endTime],
  () => {
    loadData();
  },
  { immediate: true },
);
</script>
<template>
  <Card title="扫码访问趋势" class="h-full">
    <EchartsUI ref="chartRef" class="h-[320px] w-full" />
  </Card>
</template>
src/views/mes/trace/crossregion/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,165 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesTraceCrossRegionApi } from '#/api/mes/trace/crossregion';
import { getRangePickerDefaultProps } from '#/utils';
/** é¢„警类型选项 */
export const WARNING_TYPE_OPTIONS = [
  { label: '跨省窜货', value: 'CROSS_PROVINCE' },
  { label: '跨市窜货', value: 'CROSS_CITY' },
];
/** é¢„警状态选项 */
export const WARNING_STATUS_OPTIONS = [
  { label: '待处理', value: 10 },
  { label: '已处理', value: 20 },
  { label: '误报', value: 30 },
];
/** åˆ¤å®šç²’度选项 */
export const WARNING_LEVEL_OPTIONS = [
  { label: '跨省判定', value: 'PROVINCE' },
  { label: '跨市判定', value: 'CITY' },
];
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: WARNING_STATUS_OPTIONS,
        placeholder: '请选择状态',
      },
    },
    {
      fieldName: 'warningType',
      label: '预警类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: WARNING_TYPE_OPTIONS,
        placeholder: '请选择预警类型',
      },
    },
    {
      fieldName: 'batchCode',
      label: '批次号',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入批次号',
      },
    },
    {
      fieldName: 'bagCode',
      label: '袋码',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入袋码',
      },
    },
    {
      fieldName: 'clientName',
      label: '经销商',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入经销商名称',
      },
    },
    {
      fieldName: 'eventTime',
      label: '预警时间',
      component: 'RangePicker',
      componentProps: {
        ...getRangePickerDefaultProps(),
        allowClear: true,
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesTraceCrossRegionApi.Warning>['columns'] {
  return [
    {
      field: 'warningNo',
      title: '预警编号',
      fixed: 'left',
      minWidth: 190,
    },
    {
      field: 'bagCode',
      title: '袋码',
      minWidth: 150,
      slots: { default: 'bagCode' },
    },
    {
      field: 'batchCode',
      title: '批次号',
      minWidth: 190,
    },
    {
      field: 'itemName',
      title: '物料名称',
      minWidth: 120,
    },
    {
      field: 'salesCode',
      title: '出库单号',
      minWidth: 150,
    },
    {
      field: 'clientName',
      title: '经销商',
      minWidth: 120,
    },
    {
      field: 'dealerProvince',
      title: '经销商地区',
      minWidth: 130,
      slots: { default: 'dealerRegion' },
    },
    {
      field: 'scanProvince',
      title: '扫码地区',
      minWidth: 130,
      slots: { default: 'scanRegion' },
    },
    {
      field: 'warningTypeName',
      title: '预警类型',
      width: 110,
      slots: { default: 'warningType' },
    },
    {
      field: 'statusName',
      title: '状态',
      width: 90,
      slots: { default: 'status' },
    },
    {
      field: 'eventTime',
      title: '预警时间',
      width: 170,
      formatter: 'formatDateTime',
    },
    {
      field: 'handleUserName',
      title: '处理人',
      width: 90,
    },
    {
      title: '操作',
      width: 120,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/mes/trace/crossregion/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,375 @@
<script lang="ts" setup>
  import type { VxeTableGridOptions } from '#/adapter/vxe-table';
  import type { MesTraceCrossRegionApi } from '#/api/mes/trace/crossregion';
  import { gsap } from 'gsap';
  import { nextTick, onMounted, onUnmounted, ref } from 'vue';
  import { useAccess } from '@vben/access';
  import { CountTo, Page } from '@vben/common-ui';
  import { IconifyIcon } from '@vben/icons';
  import {
    Card,
    Col,
    message,
    Modal,
    Row,
    Segmented,
    Skeleton,
    Tag,
  } from 'ant-design-vue';
  import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
  import {
    getTraceCrossRegionConfig,
    getTraceCrossRegionPage,
    getTraceCrossRegionSummary,
    handleTraceCrossRegion,
    updateTraceCrossRegionConfig,
  } from '#/api/mes/trace/crossregion';
  import {
    WARNING_LEVEL_OPTIONS,
    useGridColumns,
    useGridFormSchema,
  } from './data';
  defineOptions({ name: 'MesTraceCrossRegion' });
  const { hasAccessByCodes } = useAccess();
  const canHandle = hasAccessByCodes(['mes:trace-cross-region:handle']);
  /** æ±‡æ€»æ•°æ® */
  const summary = ref<MesTraceCrossRegionApi.Summary>({});
  const summaryLoading = ref(false);
  /** åˆ¤å®šç²’度配置 */
  const warningLevel = ref('PROVINCE');
  /** å¤„理弹窗状态 */
  const handleVisible = ref(false);
  const handleLoading = ref(false);
  /** GSAP å…¥åœºåŠ¨ç”»ä¸Šä¸‹æ–‡ï¼ˆå¸è½½æ—¶è‡ªåŠ¨è¿˜åŽŸï¼‰ */
  let animationContext: gsap.Context | undefined;
  const handleForm = ref<{
    id?: number;
    status: number;
    handleRemark?: string;
  }>({
    id: undefined,
    status: 20,
    handleRemark: '',
  });
  /** æ±‡æ€»å¡ç‰‡é…ç½® */
  const summaryCards = [
    {
      label: '预警总数',
      value: 'total',
      icon: 'lucide:alert-triangle',
      color: '#409eff',
      bg: 'linear-gradient(135deg, #409eff, #66b1ff)',
    },
    {
      label: '待处理',
      value: 'pending',
      icon: 'lucide:clock',
      color: '#e6a23c',
      bg: 'linear-gradient(135deg, #e6a23c, #ebb563)',
    },
    {
      label: '已处理',
      value: 'handled',
      icon: 'lucide:circle-check',
      color: '#67c23a',
      bg: 'linear-gradient(135deg, #67c23a, #85ce61)',
    },
    {
      label: '误报',
      value: 'falseAlarm',
      icon: 'lucide:shield-x',
      color: '#909399',
      bg: 'linear-gradient(135deg, #909399, #b1b3b8)',
    },
    {
      label: '跨省窜货',
      value: 'crossProvince',
      icon: 'lucide:map-pin-off',
      color: '#f56c6c',
      bg: 'linear-gradient(135deg, #f56c6c, #f78989)',
    },
    {
      label: '跨市窜货',
      value: 'crossCity',
      icon: 'lucide:map-pin',
      color: '#7c3aed',
      bg: 'linear-gradient(135deg, #7c3aed, #9461f5)',
    },
  ];
  /** åŠ è½½æ±‡æ€» */
  async function loadSummary() {
    summaryLoading.value = true;
    try {
      summary.value = await getTraceCrossRegionSummary();
    } finally {
      summaryLoading.value = false;
    }
  }
  /** åŠ è½½åˆ¤å®šç²’åº¦é…ç½® */
  async function loadConfig() {
    const config = await getTraceCrossRegionConfig();
    warningLevel.value = config.warningLevel || 'PROVINCE';
  }
  /** åˆ‡æ¢åˆ¤å®šç²’度 */
  async function handleLevelChange(value: string | number) {
    try {
      const level = String(value);
      await updateTraceCrossRegionConfig(level);
      message.success(
        `判定粒度已切换为${
          WARNING_LEVEL_OPTIONS.find((opt) => opt.value === level)?.label
        }`,
      );
    } catch {
      loadConfig();
    }
  }
  /** æ‰“开处理弹窗 */
  function openHandle(row: MesTraceCrossRegionApi.Warning) {
    handleForm.value = { id: row.id, status: 20, handleRemark: '' };
    handleVisible.value = true;
  }
  /** æäº¤å¤„理 */
  async function submitHandle() {
    if (!handleForm.value.id || handleLoading.value) return;
    handleLoading.value = true;
    try {
      await handleTraceCrossRegion({
        id: handleForm.value.id,
        status: handleForm.value.status,
        handleRemark: handleForm.value.handleRemark,
      });
      message.success('预警已处理');
      handleVisible.value = false;
      handleRefresh();
    } catch {
      message.error('处理失败,请重试');
    } finally {
      handleLoading.value = false;
    }
  }
  /** åˆ·æ–°åˆ—表与汇总 */
  function handleRefresh() {
    gridApi.query();
    loadSummary();
  }
  /** å±•示地区组合 */
  function regionText(province?: string, city?: string) {
    return [province, city].filter(Boolean).join('·') || '-';
  }
  const [Grid, gridApi] = useVbenVxeGrid({
    formOptions: {
      schema: useGridFormSchema(),
    },
    gridOptions: {
      columns: useGridColumns(),
      height: 'auto',
      keepSource: true,
      proxyConfig: {
        ajax: {
          query: async ({ page }, formValues) => {
            return await getTraceCrossRegionPage({
              pageNo: page.currentPage,
              pageSize: page.pageSize,
              ...formValues,
            });
          },
        },
      },
      rowConfig: {
        keyField: 'id',
        isHover: true,
      },
      toolbarConfig: {
        refresh: true,
        search: true,
      },
    } as VxeTableGridOptions<MesTraceCrossRegionApi.Warning>,
  });
  onMounted(() => {
    loadSummary();
    loadConfig();
    initEntranceAnimation();
  });
  /** å…¥åœºåŠ¨ç”»ï¼šå°Šé‡ prefers-reduced-motion */
  async function initEntranceAnimation() {
    await nextTick();
    animationContext = gsap.context(() => {
      const reduceMotion = window.matchMedia(
        '(prefers-reduced-motion: reduce)',
      ).matches;
      gsap.from('.mes-cr-summary, .mes-cr-grid', {
        autoAlpha: reduceMotion ? 1 : 0,
        y: reduceMotion ? 0 : 14,
        duration: reduceMotion ? 0 : 0.45,
        stagger: reduceMotion ? 0 : 0.08,
        ease: 'power2.out',
      });
    });
  }
  onUnmounted(() => {
    animationContext?.revert();
  });
</script>
<template>
  <Page auto-content-height>
    <Card class="mes-cr-summary mb-4">
      <template #title>
        <div class="flex items-center justify-between">
          <span>窜货预警</span>
          <span class="flex items-center gap-2 text-sm">
            <span class="text-muted-foreground">判定粒度</span>
            <Segmented
              v-model:value="warningLevel"
              :disabled="!canHandle"
              :options="WARNING_LEVEL_OPTIONS"
              @change="handleLevelChange"
            />
          </span>
        </div>
      </template>
      <Row :gutter="16">
        <Col
          v-for="card in summaryCards"
          :key="card.value"
          :lg="4"
          :md="8"
          :sm="12"
          :xs="24"
          class="mb-4"
        >
          <Card class="kpi-card">
            <div class="flex items-center gap-4">
              <div
                class="flex size-14 flex-shrink-0 items-center justify-center rounded-xl text-white"
                :style="{ background: card.bg }"
              >
                <IconifyIcon class="size-7" :icon="card.icon" />
              </div>
              <div>
                <div class="text-muted-foreground mb-1 text-sm">
                  {{ card.label }}
                </div>
                <Skeleton.Input
                  v-if="summaryLoading"
                  :size="'small'"
                  active
                  style="width: 4rem"
                />
                <CountTo
                  v-else
                  class="text-2xl font-bold leading-tight"
                  :style="{ color: card.color }"
                  :end-val="summary[card.value] || 0"
                  :duration="1200"
                />
              </div>
            </div>
          </Card>
        </Col>
      </Row>
    </Card>
    <Grid class="mes-cr-grid" table-title="窜货预警列表">
      <template #bagCode="{ row }">
        <Tag v-if="row.bagCode" color="blue">{{ row.bagCode }}</Tag>
        <Tag v-else-if="row.palletCode" color="purple">
          {{ row.palletCode }}
        </Tag>
        <span v-else>-</span>
      </template>
      <template #dealerRegion="{ row }">
        {{ regionText(row.dealerProvince, row.dealerCity) }}
      </template>
      <template #scanRegion="{ row }">
        <span class="text-[#dc2626] font-medium">
          {{ regionText(row.scanProvince, row.scanCity) }}
        </span>
      </template>
      <template #warningType="{ row }">
        <Tag
          :color="row.warningType === 'CROSS_PROVINCE' ? 'red' : 'orange'"
        >
          {{ row.warningTypeName }}
        </Tag>
      </template>
      <template #status="{ row }">
        <Tag
          :color="
            row.status === 10
              ? 'orange'
              : row.status === 20
                ? 'green'
                : 'default'
          "
        >
          {{ row.statusName }}
        </Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: '处理',
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['mes:trace-cross-region:handle'],
              ifShow: row.status === 10,
              onClick: openHandle.bind(null, row),
            },
          ]"
        />
      </template>
    </Grid>
    <Modal
      v-model:open="handleVisible"
      title="处理窜货预警"
      :confirm-loading="handleLoading"
      ok-text="确认处理"
      cancel-text="取消"
      @ok="submitHandle"
    >
      <a-form layout="vertical">
        <a-form-item label="处理结果">
          <a-radio-group v-model:value="handleForm.status">
            <a-radio :value="20">已处理</a-radio>
            <a-radio :value="30">误报</a-radio>
          </a-radio-group>
        </a-form-item>
        <a-form-item label="处理备注">
          <a-textarea
            v-model:value="handleForm.handleRemark"
            :rows="3"
            placeholder="请输入处理备注(如:确认为物流中转)"
          />
        </a-form-item>
      </a-form>
    </Modal>
  </Page>
</template>
src/views/mes/trace/integrity/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,76 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesTraceIntegrityApi } from '#/api/mes/trace/integrity';
/** ä¸šåŠ¡ç±»åž‹é€‰é¡¹ */
export const BIZ_TYPE_OPTIONS = [
  { label: '生产批次', value: 'BATCH' },
  { label: '销售出库', value: 'SALES' },
  { label: '消费者扫码', value: 'SCAN' },
];
/** ä¸šåŠ¡ç±»åž‹ä¸­æ–‡å */
export function getBizTypeLabel(bizType?: string): string {
  return (
    BIZ_TYPE_OPTIONS.find((opt) => opt.value === bizType)?.label || bizType || '-'
  );
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'bizType',
      label: '业务类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: BIZ_TYPE_OPTIONS,
        placeholder: '请选择业务类型',
      },
    },
    {
      fieldName: 'bizCode',
      label: '业务编码',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入批次号/出库单号/追溯码',
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesTraceIntegrityApi.Record>['columns'] {
  return [
    {
      field: 'bizType',
      title: '业务类型',
      width: 110,
      slots: { default: 'bizType' },
    },
    {
      field: 'bizId',
      title: '业务编号',
      width: 110,
    },
    {
      field: 'bizCode',
      title: '业务编码',
      minWidth: 220,
    },
    {
      field: 'contentHash',
      title: '内容摘要',
      minWidth: 300,
      slots: { default: 'contentHash' },
    },
    {
      field: 'eventTime',
      title: '摘要生成时间',
      width: 170,
      formatter: 'formatDateTime',
    },
  ];
}
src/views/mes/trace/integrity/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,229 @@
<script lang="ts" setup>
  import type { VxeTableGridOptions } from '#/adapter/vxe-table';
  import type { MesTraceIntegrityApi } from '#/api/mes/trace/integrity';
  import { gsap } from 'gsap';
  import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
  import { Page } from '@vben/common-ui';
  import { IconifyIcon } from '@vben/icons';
  import {
    Button,
    Card,
    InputNumber,
    message,
    Select,
    Tag,
  } from 'ant-design-vue';
  import { useVbenVxeGrid } from '#/adapter/vxe-table';
  import {
    getTraceIntegrityPage,
    verifyTraceIntegrity,
  } from '#/api/mes/trace/integrity';
  import { BIZ_TYPE_OPTIONS, getBizTypeLabel, useGridColumns, useGridFormSchema } from './data';
  defineOptions({ name: 'MesTraceIntegrity' });
  /** æ ¡éªŒè¾“å…¥ */
  const verifyBizType = ref('BATCH');
  const verifyBizId = ref<number>();
  const verifyLoading = ref(false);
  /** æ ¡éªŒç»“æžœ */
  const verifyResult = ref<MesTraceIntegrityApi.VerifyResult>();
  /** GSAP å…¥åœºåŠ¨ç”»ä¸Šä¸‹æ–‡ï¼ˆå¸è½½æ—¶è‡ªåŠ¨è¿˜åŽŸï¼‰ */
  let animationContext: gsap.Context | undefined;
  let resultTween: gsap.core.Tween | undefined;
  /** æ‰§è¡Œæ ¡éªŒ */
  async function handleVerify() {
    if (!verifyBizId.value) {
      message.warning('请输入业务记录编号');
      return;
    }
    if (verifyLoading.value) return;
    verifyLoading.value = true;
    try {
      verifyResult.value = await verifyTraceIntegrity(
        verifyBizType.value,
        verifyBizId.value,
      );
    } catch {
      message.error('校验失败,请稍后重试');
    } finally {
      verifyLoading.value = false;
    }
  }
  onMounted(async () => {
    await nextTick();
    animationContext = gsap.context(() => {
      const reduceMotion = window.matchMedia(
        '(prefers-reduced-motion: reduce)',
      ).matches;
      gsap.from('.mes-it-verify, .mes-it-grid', {
        autoAlpha: reduceMotion ? 1 : 0,
        y: reduceMotion ? 0 : 14,
        duration: reduceMotion ? 0 : 0.45,
        stagger: reduceMotion ? 0 : 0.08,
        ease: 'power2.out',
      });
    });
  });
  onUnmounted(() => {
    resultTween?.kill();
    animationContext?.revert();
  });
  /** æ ¡éªŒç»“果卡片出现时的轻量入场 */
  watch(verifyResult, async (result) => {
    if (!result) return;
    await nextTick();
    resultTween?.kill();
    resultTween = gsap.from('.mes-it-result', {
      autoAlpha: 0,
      y: 8,
      duration: 0.35,
      ease: 'power2.out',
    });
  });
  const [Grid, gridApi] = useVbenVxeGrid({
    formOptions: {
      schema: useGridFormSchema(),
    },
    gridOptions: {
      columns: useGridColumns(),
      height: 'auto',
      keepSource: true,
      proxyConfig: {
        ajax: {
          query: async ({ page }, formValues) => {
            return await getTraceIntegrityPage({
              pageNo: page.currentPage,
              pageSize: page.pageSize,
              ...formValues,
            });
          },
        },
      },
      rowConfig: {
        keyField: 'id',
        isHover: true,
      },
      toolbarConfig: {
        refresh: true,
        search: true,
      },
    } as VxeTableGridOptions<MesTraceIntegrityApi.Record>,
  });
</script>
<template>
  <Page auto-content-height>
    <Card title="数据完整性校验" class="mes-it-verify mb-4">
      <div class="flex flex-wrap items-center gap-3">
        <span class="text-muted-foreground text-sm">业务类型</span>
        <Select
          v-model:value="verifyBizType"
          :options="BIZ_TYPE_OPTIONS"
          class="w-40"
          placeholder="请选择业务类型"
        />
        <span class="text-muted-foreground text-sm">业务编号</span>
        <InputNumber
          v-model:value="verifyBizId"
          class="w-48"
          :min="1"
          :precision="0"
          placeholder="请输入业务记录编号"
          @press-enter="handleVerify"
        />
        <Button
          v-access:code="['mes:trace-integrity:verify']"
          type="primary"
          :loading="verifyLoading"
          @click="handleVerify"
        >
          å‘起校验
        </Button>
      </div>
      <div
        v-if="verifyResult"
        class="mes-it-result mt-4 rounded-lg border border-dashed p-4"
        :class="
          verifyResult.matched
            ? 'border-[#67c23a] bg-[#67c23a]/5'
            : 'border-[#f56c6c] bg-[#f56c6c]/5'
        "
      >
        <div class="flex items-center gap-2">
          <IconifyIcon
            class="size-6"
            :class="
              verifyResult.matched ? 'text-[#67c23a]' : 'text-[#f56c6c]'
            "
            :icon="
              verifyResult.matched ? 'lucide:circle-check' : 'lucide:circle-x'
            "
          />
          <span class="text-base font-bold">
            {{ verifyResult.matched ? '校验通过' : '校验不通过' }}
          </span>
          <span class="text-muted-foreground ml-2 text-sm">
            {{ verifyResult.message }}
          </span>
        </div>
        <div class="mt-3 grid gap-3 text-sm md:grid-cols-2">
          <div class="min-w-0">
            <div class="text-muted-foreground mb-1">业务信息</div>
            <div class="break-all">
              {{ getBizTypeLabel(verifyResult.bizType) }}
              <span class="text-muted-foreground">#{{ verifyResult.bizId }}</span>
              <Tag v-if="verifyResult.bizCode" class="ml-1">
                {{ verifyResult.bizCode }}
              </Tag>
            </div>
          </div>
          <div class="min-w-0">
            <div class="text-muted-foreground mb-1">存储摘要(SHA-256)</div>
            <div class="break-all font-mono text-xs">
              {{ verifyResult.storedHash }}
            </div>
          </div>
          <div class="min-w-0">
            <div class="text-muted-foreground mb-1">当前摘要(SHA-256)</div>
            <div
              class="break-all font-mono text-xs"
              :class="verifyResult.matched ? '' : 'text-[#f56c6c]'"
            >
              {{ verifyResult.currentHash }}
            </div>
          </div>
          <div v-if="verifyResult.bagSetHash" class="min-w-0">
            <div class="text-muted-foreground mb-1">袋码集合摘要</div>
            <div class="break-all font-mono text-xs">
              {{ verifyResult.bagSetHash }}
            </div>
          </div>
        </div>
      </div>
    </Card>
    <Grid class="mes-it-grid" table-title="完整性记录列表">
      <template #bizType="{ row }">
        <Tag color="blue">{{ getBizTypeLabel(row.bizType) }}</Tag>
      </template>
      <template #contentHash="{ row }">
        <span class="font-mono text-xs">{{ row.contentHash }}</span>
      </template>
    </Grid>
  </Page>
</template>