hsy
7 天以前 77b083d347eb8e75eb29a4146238b3f7304489f6
Merge branch 'refs/heads/dev_pro2.0' into fix/hsy/20260812
已修改45个文件
已添加32个文件
4715 ■■■■■ 文件已修改
.env.production 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
company.config.ts 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
deploy.bat 59 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
pnpm-lock.yaml 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pnpm-workspace.yaml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
public/logos/YZFXLogo.ico 补丁 | 查看 | 原始文档 | blame | 历史
public/logos/YZFXLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
src/api/bpm/businessAttach/index.ts 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/erp/purchase/invoice/index.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/dv/metering/index.ts 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/dv/meteringcheck/index.ts 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/md/item/index.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pd/project/index.ts 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/batch/index.ts 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/srm/supplier/index.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/srm/supplierCertificate/index.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/srm/tender/index.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bootstrap.ts 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/form-create/helpers.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/auth.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/constants/src/biz-mes-enum.ts 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/constants/src/dict-enum.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/effects/layouts/src/basic/layout.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/preferences.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/types/app.d.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/businessAttach/data.ts 163 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/businessAttach/index.vue 129 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/businessAttach/modules/form.vue 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/index.vue 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/contract/detail/approve/index.vue 190 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/invoice/detail/approve/index.vue 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/receivable/components/data.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/receivable/data.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/receivable/detail/approve/index.vue 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/finance/payment/detail/approve/index.vue 172 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/invoice/data.ts 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/invoice/detail/approve/index.vue 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/invoice/index.vue 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/invoice/modules/form.vue 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/detail/index.vue 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/request/detail/index.vue 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/attendance/exception/detail/approve/index.vue 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/leave/detail/approve/index.vue 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/resignation/detail/approve/index.vue 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/transfer/detail/approve/index.vue 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/im/home/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/metering/components/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/metering/components/select-dialog.vue 213 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/metering/components/select.vue 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/metering/data.ts 437 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/metering/index.vue 176 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/metering/modules/form.vue 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/meteringcheck/data.ts 218 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/meteringcheck/index.vue 158 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/meteringcheck/modules/form.vue 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/item/data.ts 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/item/index.vue 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/detail/index.vue 58 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/index.vue 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/modules/process-select-modal.vue 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/modules/workflow-timeline.vue 23 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/batchtrace/data.ts 175 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/batchtrace/index.vue 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/batchtrace/modules/process-trace-list.vue 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/batchtrace/modules/purchase-source-list.vue 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/batchtrace/modules/sales-target-list.vue 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/batchtrace/modules/trace-detail.vue 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/statistics/index.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplierCertificate/data.ts 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplierCertificate/index.vue 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplierCertificate/modules/form.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/tender/data.ts 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/tender/detail.vue 37 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/tender/modules/form.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.ts 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.production
@@ -3,9 +3,9 @@
VITE_BASE=/
# è¯·æ±‚路径
VITE_BASE_URL=http://42.63.70.90:9003
VITE_BASE_URL=http://36.213.180.217:9501
# æŽ¥å£åœ°å€
VITE_GLOB_API_URL=http://42.63.70.90:9003/admin-api
VITE_GLOB_API_URL=http://36.213.180.217:9501/admin-api
# æ–‡ä»¶ä¸Šä¼ ç±»åž‹ï¼šserver - åŽç«¯ä¸Šä¼ ï¼Œ client - å‰ç«¯ç›´è¿žä¸Šä¼ ï¼Œä»…支持S3服务
VITE_UPLOAD_TYPE=server
company.config.ts
@@ -21,6 +21,18 @@
    name: '测试公司',
    logo: '/logos/TEST.png',
    favicon: '/logos/TEST.ico',
    apiUrl: 'http://42.63.70.90:9003/admin-api',
    apiUrl: 'http://36.213.180.217:9501/admin-api',
  },
  JHHG: {
    name: '宁夏九泓化工',
    logo: '/logos/TEST.png',
    favicon: '/logos/TEST.ico',
    apiUrl: 'http://42.63.70.90:9001/admin-api',
  },
  YZFX: {
    name: '宁夏英泽除锈',
    logo: '/logos/YZFXLogo.png',
    favicon: '/logos/YZFXLogo.ico',
    apiUrl: 'http://42.63.70.231:9001/admin-api',
  },
};
deploy.bat
@@ -1,5 +1,4 @@
@echo off
chcp 65001 >nul
setlocal enabledelayedexpansion
:: ================= Config =================
@@ -8,8 +7,8 @@
set "PASS=Xdrjkj@10086#"
set "IP=36.213.180.217"
set "PORT=65022"
set "DIST_DIR=dist"
set "REMOTE_DIR=/data/project/mom/vue/test"
set "ZIP_FILE=dist.zip"
set "REMOTE_DIR=/data/project/mom/vue/test/dist"
set "SSH_OPTS=-o StrictHostKeyChecking=no -o ConnectTimeout=10"
:: ===========================================
@@ -81,12 +80,12 @@
    echo.
)
:: Check if dist exists and ask user
if exist "%DIST_DIR%" (
    echo [WARN] dist directory already exists.
:: Check if dist.zip exists and ask user
if exist "%ZIP_FILE%" (
    echo [WARN] %ZIP_FILE% already exists.
    set /p "REBUILD=Do you want to rebuild? (Y/N, default Y): "
    if /i "!REBUILD!"=="N" (
        echo [INFO] Skip build, using existing dist
        echo [INFO] Skip build, using existing %ZIP_FILE%
        goto :upload
    )
)
@@ -95,7 +94,8 @@
echo [STEP 1/3] Building project for %COMPANY%...
echo [INFO] Running: pnpm run build
echo.
set "COMPANY=!COMPANY!" && call pnpm run build
:: Pass VITE_COMPANY so vite.config.ts picks the right company (vite reads VITE_ prefixed env vars)
set "VITE_COMPANY=!COMPANY!" && call pnpm run build
if !errorlevel! neq 0 (
    echo [ERROR] Build failed! Check your project configuration.
    goto :error_exit
@@ -104,24 +104,18 @@
echo.
:upload
if not exist "%DIST_DIR%" (
    echo [ERROR] Dist directory not found: %DIST_DIR%
    echo [HINT] Build may have failed or output directory is incorrect.
if not exist "%ZIP_FILE%" (
    echo [ERROR] %ZIP_FILE% not found.
    echo [HINT] Build may have failed or VITE_ARCHIVER is not enabled in .env.production.
    goto :error_exit
)
:: Count files and calculate size
set "FILE_COUNT=0"
set "TOTAL_SIZE=0"
for /r "%DIST_DIR%" %%F in (*) do (
    set /a "FILE_COUNT+=1"
    set /a "TOTAL_SIZE+=%%~zF"
)
set /a "TOTAL_SIZE_MB=TOTAL_SIZE/1024/1024"
:: Calculate zip size
for %%F in ("%ZIP_FILE%") do set "ZIP_BYTES=%%~zF"
set /a "ZIP_SIZE_MB=ZIP_BYTES/1024/1024"
echo [INFO] Dist directory: %DIST_DIR%
echo [INFO] Files count: %FILE_COUNT%
echo [INFO] Total size: %TOTAL_SIZE_MB% MB
echo [INFO] Zip file: %ZIP_FILE%
echo [INFO] Size: %ZIP_SIZE_MB% MB
echo [INFO] Target: %USER%@%IP%:%PORT%
echo [INFO] Remote dir: %REMOTE_DIR%
echo.
@@ -133,9 +127,9 @@
    goto :end
)
:: Upload dist directory
echo [STEP 2/3] Uploading dist directory...
sshpass -p "%PASS%" scp -P %PORT% %SSH_OPTS% -r "%DIST_DIR%" %USER%@%IP%:%REMOTE_DIR%/
:: Upload dist.zip
echo [STEP 2/3] Uploading %ZIP_FILE%...
sshpass -p "%PASS%" scp -P %PORT% %SSH_OPTS% "%ZIP_FILE%" %USER%@%IP%:%REMOTE_DIR%/
if %errorlevel% neq 0 (
    echo [ERROR] Upload failed! Check network or credentials.
    goto :error_exit
@@ -143,18 +137,23 @@
echo [OK] Upload completed!
echo.
:: Optional: Remote restart/cleanup (uncomment if needed)
:: echo [STEP 3/3] Restarting remote service...
:: sshpass -p "%PASS%" ssh -p %PORT% %SSH_OPTS% %USER%@%IP% "cd %REMOTE_DIR% && npm run restart"
:: Remote unzip: cd to %REMOTE_DIR% then unzip -o ./dist.zip
echo [STEP 3/3] Running remote: cd %REMOTE_DIR% && unzip -o ./%ZIP_FILE%
sshpass -p "%PASS%" ssh -P %PORT% %SSH_OPTS% %USER%@%IP% "cd %REMOTE_DIR% && unzip -o ./%ZIP_FILE% && rm -f ./%ZIP_FILE%"
if %errorlevel% neq 0 (
    echo [ERROR] Remote unzip failed! Check if 'unzip' is installed on server.
    goto :error_exit
)
echo [OK] Unzip completed! %ZIP_FILE% removed from remote.
echo.
echo ==========================================
echo [SUCCESS] Deploy completed!
echo ==========================================
echo.
echo Summary:
echo   - Build: Completed
echo   - Upload: %FILE_COUNT% files (%TOTAL_SIZE_MB% MB)
echo   - Upload: %ZIP_FILE% (%ZIP_SIZE_MB% MB)
echo   - Target: %USER%@%IP%:%PORT%%REMOTE_DIR%
echo.
package.json
@@ -81,11 +81,13 @@
    "@vben/tailwind-config": "workspace:*",
    "@vben/tsconfig": "workspace:*",
    "@vben/vite-config": "workspace:*",
    "cross-env": "catalog:",
    "jiti": "^2.7.0",
    "tsdown": "^0.22.3",
    "tsx": "^4.22.4",
    "typescript": "catalog:",
    "vite": "catalog:"
    "vite": "catalog:",
    "vue-tsc": "catalog:"
  },
  "packageManager": "pnpm@11.0.9+sha512.34ce82e6780233cf9cad8685029a8f81d2e06196c5a9bad98879f7424940c6817c4e4524fb7d38b8553ceed48b9758b8ebaf1abd3600c232c4c8cf7366086f38"
}
pnpm-lock.yaml
@@ -231,6 +231,9 @@
    cropperjs:
      specifier: ^1.6.2
      version: 1.6.2
    cross-env:
      specifier: ^10.1.0
      version: 10.1.0
    crypto-js:
      specifier: ^4.2.0
      version: 4.2.0
@@ -504,6 +507,9 @@
    vue-tippy:
      specifier: ^6.7.1
      version: 6.7.1
    vue-tsc:
      specifier: ^3.3.9
      version: 3.3.9
    vue3-print-nb:
      specifier: ^0.1.4
      version: 0.1.4
@@ -693,12 +699,15 @@
      '@vben/vite-config':
        specifier: workspace:*
        version: link:internal/vite-config
      cross-env:
        specifier: 'catalog:'
        version: 10.1.0
      jiti:
        specifier: ^2.7.0
        version: 2.7.0
      tsdown:
        specifier: ^0.22.3
        version: 0.22.3(tsx@4.22.4)(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13))
        version: 0.22.3(tsx@4.22.4)(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13))(vue-tsc@3.3.9(typescript@6.0.3))
      tsx:
        specifier: ^4.22.4
        version: 4.22.4
@@ -708,6 +717,9 @@
      vite:
        specifier: 'catalog:'
        version: 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)
      vue-tsc:
        specifier: 'catalog:'
        version: 3.3.9(typescript@6.0.3)
  internal/lint-configs/commitlint-config:
    dependencies:
@@ -2552,6 +2564,9 @@
  '@emotion/unitless@0.8.1':
    resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==}
  '@epic-web/invariant@1.0.0':
    resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==}
  '@es-joy/jsdoccomment@0.84.0':
    resolution: {integrity: sha512-0xew1CxOam0gV5OMjh2KjFQZsKL2bByX1+q4j3E73MpYIdyUxcZb/xQct9ccUb+ve5KGUYbCUxyPnYB7RbuP+w==}
@@ -4754,6 +4769,9 @@
  '@vue/devtools-shared@8.1.3':
    resolution: {integrity: sha512-CM3uIPL+v+lrJUk33+pxspYo0MhuMWlCvf7zC9fybifvCPyM2jUbYRPwoYEJgYbwRqPikm5HozbUhp60MF2QuA==}
  '@vue/language-core@3.3.9':
    resolution: {integrity: sha512-in/68oAa4BCtVY6n/nkuhLIkV8DHYd2UivedJ6cMZ6UYtlq9jaoaSNUBHYCVO44z3nKg7MdE5OBoHKt5SxeBKQ==}
  '@vue/reactivity@3.5.38':
    resolution: {integrity: sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==}
@@ -4914,6 +4932,9 @@
  ajv@8.20.0:
    resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==}
  alien-signals@3.2.1:
    resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==}
  ansi-regex@5.0.1:
    resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
@@ -5527,6 +5548,11 @@
  cropperjs@1.6.2:
    resolution: {integrity: sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==}
  cross-env@10.1.0:
    resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==}
    engines: {node: '>=20'}
    hasBin: true
  cross-spawn@7.0.6:
    resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
@@ -9595,6 +9621,12 @@
    peerDependencies:
      vue: ^3.2.0
  vue-tsc@3.3.9:
    resolution: {integrity: sha512-TS3Y1ux/IRoE8OCP2PpACAeOseuIs0UvWrcr7u+w3PmfY+SlCfEf8zjrBgnQksHUgLpthi5vHlffcQTQTdPBZA==}
    hasBin: true
    peerDependencies:
      typescript: '>=5.0.0'
  vue-types@3.0.0:
    resolution: {integrity: sha512-xzq2D7zLGbzn3bPQ0CWWYx4W78JWdGYZAFfRkzxz1aTm9DaNoH0qxYDiLXBofBEQ5X78l32uIq/G9INQ2E8X/Q==}
    engines: {node: '>=10.15.0'}
@@ -10976,6 +11008,8 @@
  '@emotion/hash@0.9.2': {}
  '@emotion/unitless@0.8.1': {}
  '@epic-web/invariant@1.0.0': {}
  '@es-joy/jsdoccomment@0.84.0':
    dependencies:
@@ -12903,6 +12937,16 @@
  '@vue/devtools-shared@8.1.3': {}
  '@vue/language-core@3.3.9':
    dependencies:
      '@volar/language-core': 2.4.28
      '@vue/compiler-dom': 3.5.38
      '@vue/shared': 3.5.38
      alien-signals: 3.2.1
      muggle-string: 0.4.1
      path-browserify: 1.0.1
      picomatch: 4.0.4
  '@vue/reactivity@3.5.38':
    dependencies:
      '@vue/shared': 3.5.38
@@ -13069,6 +13113,8 @@
      fast-uri: 3.1.2
      json-schema-traverse: 1.0.0
      require-from-string: 2.0.2
  alien-signals@3.2.1: {}
  ansi-regex@5.0.1: {}
@@ -13722,6 +13768,11 @@
  croner@10.0.1: {}
  cropperjs@1.6.2: {}
  cross-env@10.1.0:
    dependencies:
      '@epic-web/invariant': 1.0.0
      cross-spawn: 7.0.6
  cross-spawn@7.0.6:
    dependencies:
@@ -16668,7 +16719,7 @@
  robust-predicates@3.0.3: {}
  rolldown-plugin-dts@0.26.0(rolldown@1.1.2)(typescript@6.0.3):
  rolldown-plugin-dts@0.26.0(rolldown@1.1.2)(typescript@6.0.3)(vue-tsc@3.3.9(typescript@6.0.3)):
    dependencies:
      '@babel/generator': 8.0.0
      '@babel/helper-validator-identifier': 8.0.2
@@ -16681,6 +16732,7 @@
      rolldown: 1.1.2
    optionalDependencies:
      typescript: 6.0.3
      vue-tsc: 3.3.9(typescript@6.0.3)
    transitivePeerDependencies:
      - oxc-resolver
@@ -17573,7 +17625,7 @@
      minimist: 1.2.8
      strip-bom: 3.0.0
  tsdown@0.22.3(tsx@4.22.4)(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13)):
  tsdown@0.22.3(tsx@4.22.4)(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13))(vue-tsc@3.3.9(typescript@6.0.3)):
    dependencies:
      ansis: 4.3.1
      cac: 7.0.0
@@ -17584,7 +17636,7 @@
      obug: 2.1.3
      picomatch: 4.0.4
      rolldown: 1.1.2
      rolldown-plugin-dts: 0.26.0(rolldown@1.1.2)(typescript@6.0.3)
      rolldown-plugin-dts: 0.26.0(rolldown@1.1.2)(typescript@6.0.3)(vue-tsc@3.3.9(typescript@6.0.3))
      semver: 7.8.5
      tinyexec: 1.2.4
      tinyglobby: 0.2.17
@@ -18092,6 +18144,12 @@
      tippy.js: 6.3.7
      vue: 3.5.38(typescript@6.0.3)
  vue-tsc@3.3.9(typescript@6.0.3):
    dependencies:
      '@volar/typescript': 2.4.28
      '@vue/language-core': 3.3.9
      typescript: 6.0.3
  vue-types@3.0.0(vue@3.5.38(typescript@6.0.3)):
    dependencies:
      is-plain-object: 3.0.1
pnpm-workspace.yaml
@@ -193,7 +193,7 @@
  vue-json-pretty: ^2.6.0
  vue-router: ^5.1.0
  vue-tippy: ^6.7.1
  vue-tsc: ^3.3.3
  vue-tsc: ^3.3.9
  vue3-print-nb: ^0.1.4
  vue3-signature: ^0.4.4
  vuedraggable: ^4.1.0
public/logos/YZFXLogo.ico
public/logos/YZFXLogo.png
src/api/bpm/businessAttach/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace BpmBusinessAttachApi {
  /** ä¸šåŠ¡é™„ä»¶é…ç½® */
  export interface BusinessAttach {
    id: number;
    processDefinitionKey: string;
    businessType?: string;
    componentPath: string;
    status: number;
    remark?: string;
    createTime?: string;
  }
}
/** æŸ¥è¯¢ä¸šåŠ¡é™„ä»¶é…ç½®åˆ†é¡µ */
export async function getBusinessAttachPage(params: PageParam) {
  return requestClient.get<PageResult<BpmBusinessAttachApi.BusinessAttach>>(
    '/bpm/business-attach/page',
    { params },
  );
}
/** æŸ¥è¯¢ä¸šåŠ¡é™„ä»¶é…ç½®è¯¦æƒ… */
export async function getBusinessAttach(id: number) {
  return requestClient.get<BpmBusinessAttachApi.BusinessAttach>(
    `/bpm/business-attach/get?id=${id}`,
  );
}
/** æ–°å¢žä¸šåŠ¡é™„ä»¶é…ç½® */
export async function createBusinessAttach(
  data: BpmBusinessAttachApi.BusinessAttach,
) {
  return requestClient.post<number>('/bpm/business-attach/create', data);
}
/** ä¿®æ”¹ä¸šåŠ¡é™„ä»¶é…ç½® */
export async function updateBusinessAttach(
  data: BpmBusinessAttachApi.BusinessAttach,
) {
  return requestClient.put<boolean>('/bpm/business-attach/update', data);
}
/** åˆ é™¤ä¸šåŠ¡é™„ä»¶é…ç½® */
export async function deleteBusinessAttach(id: number) {
  return requestClient.delete<boolean>(
    `/bpm/business-attach/delete?id=${id}`,
  );
}
src/api/erp/purchase/invoice/index.ts
@@ -26,8 +26,6 @@
    invoiceTitle?: string;
    price?: number;
    invoiceTime?: string;
    auditStatus?: number;
    processInstanceId?: string;
    remark?: string;
    attachmentList?: { id: number; name?: string; url?: string }[];
    blobIds?: number[];
@@ -48,7 +46,6 @@
    price?: number;
    supplierId?: number;
    purchaseOrderId?: number;
    auditStatus?: number;
    hasAttachment?: boolean;
    remainingInvoicePrice?: number;
  }
@@ -89,19 +86,7 @@
  return requestClient.download('/erp/purchase-invoice/export-excel', { params });
}
/** æ›´æ–°æ¥ç¥¨çš„状态(审核/反审核) */
export function updatePurchaseInvoiceStatus(id: number, status: number) {
  return requestClient.put('/erp/purchase-invoice/update-status', null, {
    params: { id, status },
  });
}
/** èŽ·å¾—å¾…å®¡æ ¸æ¥ç¥¨æ•°é‡ */
export function getAuditPurchaseInvoiceCount() {
  return requestClient.get<number>('/erp/purchase-invoice/audit-count');
}
/** æŸ¥è¯¢æ¥ç¥¨ä¸‹æ‹‰åˆ—表(按供应商可选来票,仅审核通过且已上传发票附件) */
/** æŸ¥è¯¢æ¥ç¥¨ä¸‹æ‹‰åˆ—表(按供应商可选来票,已上传发票附件且剩余可付金额大于 0) */
export function getPurchaseInvoiceSimpleList(
  supplierId: number,
  purchaseOrderId?: number,
src/api/mes/dv/metering/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,72 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesDvMeteringApi {
  /** MES è®¡é‡å™¨å…· */
  export interface Metering {
    id?: number; // ä¸»é”®ID
    code?: string; // è®¡é‡å™¨å…·ç¼–码
    name?: string; // è®¡é‡å™¨å…·åç§°
    specification?: string; // è§„格型号
    brand?: string; // å“ç‰Œ/生产厂家
    category?: number; // å™¨å…·ç±»åˆ«ï¼ˆå­—典)
    precision?: string; // ç²¾åº¦
    range?: string; // é‡ç¨‹
    deptId?: number; // ä½¿ç”¨éƒ¨é—¨ç¼–号
    deptName?: string; // ä½¿ç”¨éƒ¨é—¨åç§°
    chargeUserId?: number; // è´£ä»»äººç¼–号
    chargeUserNickname?: string; // è´£ä»»äººæ˜µç§°
    checkCycleCount?: number; // æ ¡å‡†å‘¨æœŸï¼ˆæœˆï¼‰
    useDate?: string; // æŠ•入使用日期
    lastCheckDate?: string; // ä¸Šæ¬¡æ£€æµ‹æ—¥æœŸ
    nextCheckDate?: string; // ä¸‹æ¬¡æ£€æµ‹æ—¥æœŸï¼ˆåˆ°æœŸæ—¥ï¼‰
    status?: number; // çŠ¶æ€ï¼ˆå­—å…¸ï¼‰
    expireStatus?: number; // åˆ°æœŸçŠ¶æ€ï¼š1 å·²è¿‡æœŸ / 2 å³å°†åˆ°æœŸ / 3 æ­£å¸¸
    expireDays?: number; // è·åˆ°æœŸå¤©æ•°
    remark?: string; // å¤‡æ³¨
    createTime?: Date; // åˆ›å»ºæ—¶é—´
  }
}
/** æŸ¥è¯¢è®¡é‡å™¨å…·åˆ†é¡µ */
export function getMeteringPage(params: PageParam) {
  return requestClient.get<PageResult<MesDvMeteringApi.Metering>>(
    '/mes/dv/metering/page',
    { params },
  );
}
/** æŸ¥è¯¢è®¡é‡å™¨å…·è¯¦æƒ… */
export function getMetering(id: number) {
  return requestClient.get<MesDvMeteringApi.Metering>(
    `/mes/dv/metering/get?id=${id}`,
  );
}
/** æ–°å¢žè®¡é‡å™¨å…· */
export function createMetering(data: MesDvMeteringApi.Metering) {
  return requestClient.post<number>('/mes/dv/metering/create', data);
}
/** ä¿®æ”¹è®¡é‡å™¨å…· */
export function updateMetering(data: MesDvMeteringApi.Metering) {
  return requestClient.put('/mes/dv/metering/update', data);
}
/** åˆ é™¤è®¡é‡å™¨å…· */
export function deleteMetering(id: number) {
  return requestClient.delete(`/mes/dv/metering/delete?id=${id}`);
}
/** å¯¼å‡ºè®¡é‡å™¨å…· */
export function exportMetering(params: any) {
  return requestClient.download('/mes/dv/metering/export-excel', { params });
}
/** æŸ¥è¯¢è®¡é‡å™¨å…·ç²¾ç®€åˆ—表(下拉选项) */
export function getMeteringSimpleList() {
  return requestClient.get<MesDvMeteringApi.Metering[]>(
    '/mes/dv/metering/simple-list',
  );
}
src/api/mes/dv/meteringcheck/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,60 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesDvMeteringCheckApi {
  /** MES è®¡é‡æ£€æµ‹è®°å½• */
  export interface MeteringCheck {
    id?: number; // ä¸»é”®ID
    meteringId?: number; // è®¡é‡å™¨å…·ID
    meteringCode?: string; // è®¡é‡å™¨å…·ç¼–码
    meteringName?: string; // è®¡é‡å™¨å…·åç§°
    checkCode?: string; // æ£€æµ‹ç¼–号
    checkDate?: string; // æ£€æµ‹æ—¥æœŸ
    checkOrg?: string; // æ£€æµ‹æœºæž„
    checkResult?: number; // æ£€æµ‹ç»“论(字典)
    certNo?: string; // è¯ä¹¦ç¼–号
    checkUserId?: number; // æ£€æµ‹äººç¼–号
    checkUserNickname?: string; // æ£€æµ‹äººæ˜µç§°
    nextCheckDate?: string; // ä¸‹æ¬¡æ£€æµ‹æ—¥æœŸ
    remark?: string; // å¤‡æ³¨
    createTime?: Date; // åˆ›å»ºæ—¶é—´
  }
}
/** æŸ¥è¯¢è®¡é‡æ£€æµ‹è®°å½•分页 */
export function getMeteringCheckPage(params: PageParam) {
  return requestClient.get<PageResult<MesDvMeteringCheckApi.MeteringCheck>>(
    '/mes/dv/metering-check/page',
    { params },
  );
}
/** æŸ¥è¯¢è®¡é‡æ£€æµ‹è®°å½•详情 */
export function getMeteringCheck(id: number) {
  return requestClient.get<MesDvMeteringCheckApi.MeteringCheck>(
    `/mes/dv/metering-check/get?id=${id}`,
  );
}
/** æ–°å¢žè®¡é‡æ£€æµ‹è®°å½• */
export function createMeteringCheck(data: MesDvMeteringCheckApi.MeteringCheck) {
  return requestClient.post<number>('/mes/dv/metering-check/create', data);
}
/** ä¿®æ”¹è®¡é‡æ£€æµ‹è®°å½• */
export function updateMeteringCheck(data: MesDvMeteringCheckApi.MeteringCheck) {
  return requestClient.put('/mes/dv/metering-check/update', data);
}
/** åˆ é™¤è®¡é‡æ£€æµ‹è®°å½• */
export function deleteMeteringCheck(id: number) {
  return requestClient.delete(`/mes/dv/metering-check/delete?id=${id}`);
}
/** å¯¼å‡ºè®¡é‡æ£€æµ‹è®°å½• */
export function exportMeteringCheck(params: any) {
  return requestClient.download('/mes/dv/metering-check/export-excel', {
    params,
  });
}
src/api/mes/md/item/index.ts
@@ -54,13 +54,6 @@
  return requestClient.put('/mes/md/item/update', data);
}
/** ä¿®æ”¹ç‰©æ–™äº§å“çŠ¶æ€ */
export function updateItemStatus(id: number, status: number) {
  return requestClient.put('/mes/md/item/update-status', undefined, {
    params: { id, status },
  });
}
/** åˆ é™¤ç‰©æ–™äº§å“ */
export function deleteItem(id: number) {
  return requestClient.delete(`/mes/md/item/delete?id=${id}`);
src/api/mes/pd/project/index.ts
@@ -32,6 +32,8 @@
    remark?: string;
    /** åˆ›å»ºæ—¶é—´ */
    createTime?: number;
    /** BPM æµç¨‹å®žä¾‹ç¼–号 */
    processInstanceId?: string;
  }
  export interface PageParams extends PageParam {
@@ -87,13 +89,20 @@
  });
}
/** å®¡æ ¸ï¼ˆå¾…审核→待归档) */
export function auditPdProject(id: number) {
  return requestClient.put('/mes/pd/project/audit', undefined, {
    params: { id },
/** æäº¤å®¡æ‰¹ï¼ˆå¾…审核→审批中) */
export function submitPdProjectApproval(id: number, processDefinitionKey: string) {
  return requestClient.put('/mes/pd/project/submit-approval', undefined, {
    params: { id, processDefinitionKey },
  });
}
/** èŽ·å¾—å®¡æ‰¹æµç¨‹åˆ—è¡¨ */
export function getPdProjectApproveProcessList() {
  return requestClient.get<Array<{ id: string; name: string; key: string }>>(
    '/mes/pd/project/approve-process-list',
  );
}
/** å½’档(待归档→已归档),后端自动创建归档台账记录 */
export function archivePdProject(id: number) {
  return requestClient.put('/mes/pd/project/archive', undefined, {
src/api/mes/wm/batch/index.ts
@@ -56,6 +56,56 @@
    expireDate?: string[]; // è¿‡æœŸæ—¥æœŸ
    receiptDate?: string[]; // å…¥åº“日期
  }
  /** MES æ‰¹æ¬¡é‡‡è´­æ¥æºï¼ˆé‡‡è´­å…¥åº“单 + IQC) */
  export interface PurchaseSource {
    receiptCode?: string; // é‡‡è´­å…¥åº“单号
    receiptDate?: Date; // å…¥åº“日期
    purchaseOrderCode?: string; // é‡‡è´­è®¢å•编号
    vendorId?: number; // ä¾›åº”商 ID
    vendorCode?: string; // ä¾›åº”商编码
    vendorName?: string; // ä¾›åº”商名称
    iqcCode?: string; // IQC æ£€éªŒå•号
    checkResult?: number; // IQC æ£€éªŒç»“论(字典 mes_qc_check_result)
    itemId?: number; // ç‰©æ–™ ID
    itemCode?: string; // ç‰©æ–™ç¼–码
    itemName?: string; // ç‰©æ–™åç§°
    batchCode?: string; // æ‰¹æ¬¡å·
    receivedQuantity?: number; // å…¥åº“数量
  }
  /** MES æ‰¹æ¬¡é”€å”®åŽ»å‘ï¼ˆé”€å”®å‡ºåº“å• + OQC) */
  export interface SalesTarget {
    salesCode?: string; // é”€å”®å‡ºåº“单号
    salesDate?: Date; // å‡ºåº“日期
    salesOrderCode?: string; // é”€å”®è®¢å•编号
    clientId?: number; // å®¢æˆ· ID
    clientCode?: string; // å®¢æˆ·ç¼–码
    clientName?: string; // å®¢æˆ·åç§°
    oqcCode?: string; // OQC æ£€éªŒå•号
    checkResult?: number; // OQC æ£€éªŒç»“论(字典 mes_qc_check_result)
    itemId?: number; // ç‰©æ–™ ID
    itemCode?: string; // ç‰©æ–™ç¼–码
    itemName?: string; // ç‰©æ–™åç§°
    batchCode?: string; // æ‰¹æ¬¡å·
    quantity?: number; // å‡ºåº“数量
    qualityStatus?: number; // è´¨é‡çŠ¶æ€ï¼ˆå­—å…¸ mes_wm_quality_status)
  }
  /** MES æ‰¹æ¬¡å·¥åºæµè½¬ï¼ˆäº§å‡º/投入事件) */
  export interface ProcessTrace {
    eventType?: 'OUT' | 'IN'; // äº‹ä»¶ç±»åž‹ï¼šOUT-产出 / IN-投入
    workOrderId?: number; // ç”Ÿäº§å·¥å• ID
    workOrderCode?: string; // ç”Ÿäº§å·¥å•编号
    processId?: number; // å·¥åº ID
    processCode?: string; // å·¥åºç¼–码
    processName?: string; // å·¥åºåç§°
    batchCode?: string; // æ‰¹æ¬¡å·
    quantity?: number; // æ•°é‡
    qualityStatus?: number; // è´¨é‡çŠ¶æ€ï¼ˆå­—å…¸ mes_wm_quality_status),仅产出侧有值
    eventTime?: Date; // äº‹ä»¶æ—¶é—´
    processSort?: number; // å·¥åºåºå·
  }
}
/** æŸ¥è¯¢æ‰¹æ¬¡è¯¦æƒ… */
@@ -88,3 +138,34 @@
    { params: { code } },
  );
}
/** æ‰¹æ¬¡é‡‡è´­æ¥æºï¼ˆé‡‡è´­å…¥åº“单 + IQC) */
export function getPurchaseSourceList(code: string) {
  return requestClient.get<MesWmBatchApi.PurchaseSource[]>(
    '/mes/wm/batch/purchase-sources',
    { params: { code } },
  );
}
/** æ‰¹æ¬¡é”€å”®åŽ»å‘ï¼ˆé”€å”®å‡ºåº“å• + OQC) */
export function getSalesTargetList(code: string) {
  return requestClient.get<MesWmBatchApi.SalesTarget[]>(
    '/mes/wm/batch/sales-targets',
    { params: { code } },
  );
}
/** æ‰¹æ¬¡å·¥åºæµè½¬ï¼ˆäº§å‡º/投入事件) */
export function getProcessTraceList(code: string) {
  return requestClient.get<MesWmBatchApi.ProcessTrace[]>(
    '/mes/wm/batch/process-trace',
    { params: { code } },
  );
}
/** æ ¹æ® SN ç åæŸ¥æ‰¹æ¬¡ */
export function getBatchBySn(sn: string) {
  return requestClient.get<MesWmBatchApi.Batch | null>('/mes/wm/batch/get-by-sn', {
    params: { sn },
  });
}
src/api/srm/supplier/index.ts
@@ -31,6 +31,11 @@
  return requestClient.get<SrmSupplierApi.SupplierVO[]>('/srm/supplier/simple-list');
}
/** èŽ·å–ä¾›åº”å•†æ¡£æ¡ˆåˆ—è¡¨ï¼ˆä¸è¿‡æ»¤å‡†å…¥çŠ¶æ€ï¼‰ */
export function getSupplierList() {
  return requestClient.get<SrmSupplierApi.SupplierVO[]>('/srm/supplier/list');
}
/** èŽ·å–ä¾›åº”å•†è¯¦æƒ… */
export function getSupplier(id: number) {
  return requestClient.get<SrmSupplierApi.SupplierVO>(`/srm/supplier/get?id=${id}`);
src/api/srm/supplierCertificate/index.ts
@@ -1,5 +1,6 @@
import type { PageParam, PageResult } from '@vben/request';
import type { SystemStorageApi } from '#/api/system/storage';
import { requestClient } from '#/api/request';
export namespace SrmSupplierCertificateApi {
@@ -16,6 +17,8 @@
    status?: number;
    remark?: string;
    createTime?: string;
    blobIds?: number[];
    attachmentList?: SystemStorageApi.StorageBlob[];
  }
}
src/api/srm/tender/index.ts
@@ -1,5 +1,6 @@
import type { PageParam, PageResult } from '@vben/request';
import type { SystemStorageApi } from '#/api/system/storage';
import { requestClient } from '#/api/request';
export namespace SrmTenderApi {
@@ -23,6 +24,8 @@
    purchaseOrderNo?: string;
    biddingMode?: string;
    createTime?: string;
    blobIds?: number[];
    attachmentList?: SystemStorageApi.StorageBlob[];
  }
  export interface TenderMaterialVO {
@@ -105,6 +108,7 @@
    id: number;
    tenderDesc?: string;
    qualificationRequirements?: string;
    blobIds?: number[];
  }
}
src/bootstrap.ts
@@ -71,7 +71,8 @@
    if (preferences.app.dynamicTitle) {
      const routeTitle = router.currentRoute.value.meta?.title;
      const pageTitle =
        (routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name;
        (routeTitle ? `${$t(routeTitle)} - ` : '') +
        preferences.copyright.companyName;
      useTitle(pageTitle);
    }
  });
src/components/form-create/helpers.ts
@@ -28,7 +28,10 @@
}
/** è§£ç è¡¨å• Conf */
export function decodeConf(conf: string) {
export function decodeConf(conf?: string | null) {
  if (!conf) {
    return {};
  }
  return formCreate.parseJson(conf);
}
@@ -43,9 +46,9 @@
}
/** è§£ç è¡¨å• Fields */
export function decodeFields(fields: string[]) {
export function decodeFields(fields?: string[] | null) {
  const rule: Rule[] = [];
  fields.forEach((item) => {
  (fields || []).forEach((item) => {
    rule.push(formCreate.parseJson(item));
  });
  return rule;
src/layouts/auth.vue
@@ -8,7 +8,7 @@
declare const __APP_LOGO__: string;
const appName = computed(() => preferences.app.name);
const appName = computed(() => preferences.copyright.companyName);
const logo = computed(() => __APP_LOGO__ || preferences.logo.source);
const logoDark = computed(() => preferences.logo.sourceDark);
</script>
src/main.ts
@@ -5,6 +5,7 @@
declare const __APP_LOGO__: string;
declare const __APP_FAVICON__: string;
declare const __APP_NAME__: string;
/**
 * åº”用初始化完成之后再进行页面加载渲染
@@ -27,6 +28,13 @@
  if (__APP_LOGO__) {
    preferencesManager.updatePreferences({ logo: { source: __APP_LOGO__ } });
  }
  // å¼ºåˆ¶è¦†ç›–公司名称,绕过 localStorage ç¼“å­˜
  if (__APP_NAME__) {
    preferencesManager.updatePreferences({
      app: { name: __APP_NAME__ },
      copyright: { companyName: __APP_NAME__ },
    });
  }
  if (__APP_FAVICON__) {
    const link = document.querySelector('link[rel="icon"]');
    if (link) {
src/packages/constants/src/biz-mes-enum.ts
@@ -104,6 +104,26 @@
  ABNORMAL: 2,
} as const;
/** MES è®¡é‡å™¨å…·çŠ¶æ€æžšä¸¾ */
export const MesDvMeteringStatusEnum = {
  IN_USE: 1, // åœ¨ç”¨
  STOPPED: 2, // åœç”¨
  SCRAPPED: 3, // æŠ¥åºŸ
} as const;
/** MES è®¡é‡æ£€æµ‹ç»“论枚举 */
export const MesDvMeteringCheckResultEnum = {
  QUALIFIED: 1, // åˆæ ¼
  UNQUALIFIED: 2, // ä¸åˆæ ¼
} as const;
/** MES è®¡é‡å™¨å…·åˆ°æœŸçŠ¶æ€æžšä¸¾ */
export const MesDvMeteringExpireStatusEnum = {
  OVERDUE: 1, // å·²è¿‡æœŸ
  WITHIN: 2, // å³å°†åˆ°æœŸ
  NORMAL: 3, // æ­£å¸¸
} as const;
/** MES ç»´ä¿®å·¥å•状态枚举 */
export const MesDvRepairStatusEnum = {
  PREPARE: MesOrderStatusConstants.DRAFT,
@@ -510,11 +530,12 @@
  RIGHT: 2, // å³è¡¥é½
} as const;
/** MES äº§å“è®¾è®¡é¡¹ç›®çŠ¶æ€æžšä¸¾ï¼ˆçº¿æ€§æµè½¬ï¼šå¾…å‘å¸ƒâ†’å¾…ä¸Šä¼ â†’å¾…å®¡æ ¸â†’å¾…å½’æ¡£â†’å·²å½’æ¡£ï¼‰ */
/** MES äº§å“è®¾è®¡é¡¹ç›®çŠ¶æ€æžšä¸¾ï¼ˆçº¿æ€§æµè½¬ï¼šå¾…å‘å¸ƒâ†’å¾…ä¸Šä¼ â†’å¾…å®¡æ ¸â†’å®¡æ‰¹ä¸­â†’å¾…å½’æ¡£â†’å·²å½’æ¡£ï¼‰ */
export const MesPdProjectStatusEnum = {
  PENDING_PUBLISH: 0, // å¾…发布
  PENDING_UPLOAD: 1, // å¾…上传
  PENDING_AUDIT: 2, // å¾…审核
  APPROVING: 5, // å®¡æ‰¹ä¸­
  PENDING_ARCHIVE: 3, // å¾…å½’æ¡£
  ARCHIVED: 4, // å·²å½’æ¡£
} as const;
src/packages/constants/src/dict-enum.ts
@@ -234,6 +234,9 @@
  MES_DV_REPAIR_RESULT: 'mes_dv_repair_result', // MES ç»´ä¿®ç»“æžœ
  MES_DV_CHECK_RECORD_STATUS: 'mes_dv_check_record_status', // MES ç‚¹æ£€è®°å½•状态
  MES_DV_CHECK_RESULT: 'mes_dv_check_result', // MES ç‚¹æ£€ç»“æžœ
  MES_DV_METERING_CATEGORY: 'mes_dv_metering_category', // MES è®¡é‡å™¨å…·ç±»åˆ«
  MES_DV_METERING_STATUS: 'mes_dv_metering_status', // MES è®¡é‡å™¨å…·çŠ¶æ€
  MES_DV_METERING_CHECK_RESULT: 'mes_dv_metering_check_result', // MES è®¡é‡æ£€æµ‹ç»“论
  MES_PRO_LINK_TYPE: 'mes_pro_link_type', // MES å·¥åºå…³ç³»ç±»åž‹
  MES_PRO_WORK_ORDER_STATUS: 'mes_pro_work_order_status', // MES ç”Ÿäº§å·¥å•状态
  ORDER_PRODUCTION_STATUS: 'order_production_status', // ç”Ÿäº§è®¢å•状态
src/packages/effects/layouts/src/basic/layout.vue
@@ -291,7 +291,7 @@
        :collapsed="logoCollapsed"
        :src="preferences.logo.source"
        :src-dark="preferences.logo.sourceDark"
        :text="preferences.app.name"
        :text="preferences.copyright.companyName"
        :theme="logoTheme"
        @click="clickLogo"
      >
@@ -382,7 +382,7 @@
      <VbenLogo
        v-if="preferences.logo.enable"
        :fit="preferences.logo.fit"
        :text="preferences.app.name"
        :text="preferences.copyright.companyName"
        :theme="sidebarThemeSub"
      >
        <template v-if="$slots['logo-text']" #text>
src/preferences.ts
@@ -4,6 +4,7 @@
} from './packages/preferences/src';
declare const __APP_LOGO__: string;
declare const __APP_NAME__: string;
interface WebAntdPreferencesExtension {
  defaultTableSize: number;
@@ -22,7 +23,7 @@
  app: {
    /** åŽç«¯è·¯ç”±æ¨¡å¼ */
    accessMode: 'backend',
    name: import.meta.env.VITE_APP_TITLE,
    name: __APP_NAME__ || import.meta.env.VITE_APP_TITLE,
    enableRefreshToken: true,
  },
  footer: {
@@ -31,7 +32,7 @@
    fixed: false,
  },
  copyright: {
    companyName: import.meta.env.VITE_APP_TITLE,
    companyName: __APP_NAME__ || import.meta.env.VITE_APP_TITLE,
    companySiteLink: '#',
  },
  logo: {
src/types/app.d.ts
@@ -1,2 +1,3 @@
declare const __APP_LOGO__: string;
declare const __APP_FAVICON__: string;
declare const __APP_NAME__: string;
src/views/bpm/businessAttach/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,163 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { z } from '#/adapter/form';
import { getRangePickerDefaultProps } from '#/utils';
/** æ–°å¢ž/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'processDefinitionKey',
      label: '流程定义 Key',
      component: 'Input',
      componentProps: {
        placeholder: '请输入流程定义 Key,如 purchase_approve',
      },
      rules: 'required',
    },
    {
      fieldName: 'businessType',
      label: '业务类型',
      component: 'Input',
      componentProps: {
        placeholder: '请输入业务类型,如 erp_purchase_order',
      },
    },
    {
      fieldName: 'componentPath',
      label: '业务详情组件路径',
      component: 'Input',
      componentProps: {
        placeholder:
          '请输入业务详情组件路径,如 /erp/purchase/order/detail/index.vue',
      },
      rules: 'required',
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'RadioGroup',
      componentProps: {
        options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
        buttonStyle: 'solid',
        optionType: 'button',
      },
      rules: z.number().default(CommonStatusEnum.ENABLE),
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入备注',
      },
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'processDefinitionKey',
      label: '流程定义 Key',
      component: 'Input',
      componentProps: {
        placeholder: '请输入流程定义 Key',
        allowClear: true,
      },
    },
    {
      fieldName: 'businessType',
      label: '业务类型',
      component: 'Input',
      componentProps: {
        placeholder: '请输入业务类型',
        allowClear: true,
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
        placeholder: '请选择状态',
        allowClear: true,
      },
    },
    {
      fieldName: 'createTime',
      label: '创建时间',
      component: 'RangePicker',
      componentProps: {
        ...getRangePickerDefaultProps(),
        allowClear: true,
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
  return [
    {
      field: 'id',
      title: '编号',
      minWidth: 80,
    },
    {
      field: 'processDefinitionKey',
      title: '流程定义 Key',
      minWidth: 180,
    },
    {
      field: 'businessType',
      title: '业务类型',
      minWidth: 160,
    },
    {
      field: 'componentPath',
      title: '业务详情组件路径',
      minWidth: 260,
    },
    {
      field: 'status',
      title: '状态',
      minWidth: 80,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.COMMON_STATUS },
      },
    },
    {
      field: 'remark',
      title: '备注',
      minWidth: 160,
    },
    {
      field: 'createTime',
      title: '创建时间',
      minWidth: 180,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 160,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/bpm/businessAttach/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,129 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { BpmBusinessAttachApi } from '#/api/bpm/businessAttach';
import { Page, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteBusinessAttach,
  getBusinessAttachPage,
} from '#/api/bpm/businessAttach';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** åˆ›å»ºä¸šåŠ¡é™„ä»¶é…ç½® */
function handleCreate() {
  formModalApi.setData(null).open();
}
/** ç¼–辑业务附件配置 */
function handleEdit(row: BpmBusinessAttachApi.BusinessAttach) {
  formModalApi.setData(row).open();
}
/** åˆ é™¤ä¸šåŠ¡é™„ä»¶é…ç½® */
async function handleDelete(row: BpmBusinessAttachApi.BusinessAttach) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', [row.processDefinitionKey]),
    duration: 0,
  });
  try {
    await deleteBusinessAttach(row.id as number);
    message.success($t('ui.actionMessage.deleteSuccess'));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getBusinessAttachPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<BpmBusinessAttachApi.BusinessAttach>,
});
</script>
<template>
  <Page auto-content-height><FormModal @success="handleRefresh" />
    <Grid table-title="业务附件配置">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['业务附件配置']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['bpm:business-attach:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['bpm:business-attach:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['bpm:business-attach:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [
                  row.processDefinitionKey,
                ]),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/bpm/businessAttach/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,88 @@
<script lang="ts" setup>
import type { BpmBusinessAttachApi } from '#/api/bpm/businessAttach';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
  createBusinessAttach,
  getBusinessAttach,
  updateBusinessAttach,
} from '#/api/bpm/businessAttach';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<BpmBusinessAttachApi.BusinessAttach>();
const getTitle = computed(() => {
  return formData.value?.id
    ? $t('ui.actionTitle.edit', ['业务附件配置'])
    : $t('ui.actionTitle.create', ['业务附件配置']);
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    formItemClass: 'col-span-2',
    labelWidth: 140,
  },
  layout: 'horizontal',
  schema: useFormSchema(),
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    // æäº¤è¡¨å•
    const data = (await formApi.getValues()) as BpmBusinessAttachApi.BusinessAttach;
    try {
      await (formData.value?.id
        ? updateBusinessAttach(data)
        : createBusinessAttach(data));
      // å…³é—­å¹¶æç¤º
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      return;
    }
    // åŠ è½½æ•°æ®
    const data = modalApi.getData<BpmBusinessAttachApi.BusinessAttach>();
    if (!data || !data.id) {
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getBusinessAttach(data.id);
      // è®¾ç½®åˆ° values
      await formApi.setValues(formData.value);
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-2/3">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/bpm/model/index.vue
@@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { ModelCategoryInfo } from '#/api/bpm/model';
import { onActivated, reactive, ref, useTemplateRef, watch } from 'vue';
import { onActivated, onMounted, reactive, ref, useTemplateRef, watch } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
@@ -76,6 +76,11 @@
}
/** åˆå§‹åŒ– */
onMounted(() => {
  getList();
});
/** æ¿€æ´»æ—¶åˆ·æ–°ï¼ˆkeep-alive ç¼“存场景下从编辑页返回时触发) */
onActivated(() => {
  getList();
});
src/views/crm/contract/detail/approve/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,190 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { CrmContractApi } from '#/api/crm/contract';
import type { DescriptionItemSchema } from '#/components/description';
import { h, nextTick, onMounted, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import {
  erpCountInputFormatter,
  erpPriceInputFormatter,
  formatDateTime,
} from '@vben/utils';
import { Spin } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getContract } from '#/api/crm/contract';
import { DictTag } from '#/components/dict-tag';
import { useDescription } from '#/components/description';
defineOptions({ name: 'CrmContractApproveDetail' });
const props = defineProps<{ id: string }>();
const loading = ref(false);
const formData = ref<CrmContractApi.Contract>();
/** æ–‡æœ¬å­—段空值占位 */
function textRender(val: unknown) {
  return val === 0 || val ? String(val) : '-';
}
/** åˆåŒäº§å“æ˜Žç»†ï¼ˆå®¡æ‰¹è¯¦æƒ…只读列) */
function useProductColumns(): VxeTableGridOptions['columns'] {
  return [
    { type: 'seq', title: '序号', width: 60 },
    { field: 'itemName', title: '产品名称', minWidth: 180 },
    { field: 'itemCode', title: '物料编码', minWidth: 120 },
    { field: 'itemUnitName', title: '单位', minWidth: 80, formatter: ({ cellValue }) => textRender(cellValue) },
    {
      field: 'contractPrice',
      title: '合同价格(元)',
      minWidth: 120,
      formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-',
    },
    {
      field: 'count',
      title: '数量',
      minWidth: 100,
      formatter: ({ cellValue }) => erpCountInputFormatter(cellValue) || '-',
    },
    {
      field: 'totalPrice',
      title: '合计金额(元)',
      minWidth: 120,
      formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-',
    },
  ];
}
const [Grid, gridApi] = useVbenVxeGrid({
  gridOptions: {
    columns: useProductColumns(),
    data: [],
    minHeight: 200,
    autoResize: true,
    border: true,
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    pagerConfig: {
      enabled: false,
    },
    toolbarConfig: {
      enabled: false,
    },
  },
});
const [Descriptions] = useDescription({
  bordered: true,
  column: 2,
  schema: [
    { label: '合同编号', field: 'no', render: textRender },
    { label: '合同名称', field: 'name', render: textRender },
    { label: '客户名称', field: 'customerName', render: textRender },
    { label: '商机名称', field: 'businessName', render: textRender },
    {
      label: '合同金额(元)',
      field: 'totalPrice',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    {
      label: '定金金额(元)',
      field: 'depositPrice',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    {
      label: '回款金额(元)',
      field: 'totalReceivablePrice',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    {
      label: '已开票金额(元)',
      field: 'totalInvoicePrice',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    {
      label: '未开票金额(元)',
      field: 'unInvoicePrice',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    {
      label: '下单时间',
      field: 'orderDate',
      render: (val) => formatDateTime(val) as string,
    },
    {
      label: '合同开始时间',
      field: 'startTime',
      render: (val) => formatDateTime(val) as string,
    },
    {
      label: '合同结束时间',
      field: 'endTime',
      render: (val) => formatDateTime(val) as string,
    },
    { label: '客户签约人', field: 'signContactName', render: textRender },
    { label: '公司签约人', field: 'signUserName', render: textRender },
    { label: '销售订单', field: 'orderNo', render: textRender },
    { label: '创建人', field: 'creatorName', render: textRender },
    {
      label: '创建时间',
      field: 'createTime',
      render: (val) => formatDateTime(val) as string,
    },
    {
      label: '合同状态',
      field: 'auditStatus',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.CRM_AUDIT_STATUS, value: val }),
    },
    { label: '备注', field: 'remark', render: textRender },
  ] as DescriptionItemSchema[],
});
onMounted(async () => {
  try {
    loading.value = true;
    formData.value = await getContract(Number(props.id));
    if (formData.value?.products?.length) {
      await nextTick();
      await gridApi.grid.reloadData(formData.value.products);
    }
  } finally {
    loading.value = false;
  }
});
</script>
<template>
  <ContentWrap class="m-2">
    <Spin :spinning="loading" tip="加载中...">
      <div class="space-y-4">
        <!-- åˆåŒåŸºæœ¬ä¿¡æ¯ -->
        <div class="rounded-lg border border-border bg-background p-4">
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">合同基本信息</span>
          </div>
          <Descriptions :data="formData" />
        </div>
        <!-- äº§å“æ˜Žç»† -->
        <div
          v-if="formData?.products?.length"
          class="rounded-lg border border-border bg-background p-4"
        >
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">产品明细</span>
          </div>
          <Grid />
        </div>
      </div>
    </Spin>
  </ContentWrap>
</template>
src/views/crm/invoice/detail/approve/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,105 @@
<script lang="ts" setup>
import type { CrmInvoiceApi } from '#/api/crm/invoice';
import type { DescriptionItemSchema } from '#/components/description';
import { h, onMounted, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
import { Spin } from 'ant-design-vue';
import { getInvoice } from '#/api/crm/invoice';
import { DictTag } from '#/components/dict-tag';
import { useDescription } from '#/components/description';
defineOptions({ name: 'CrmInvoiceApproveDetail' });
const props = defineProps<{ id: string }>();
const loading = ref(false);
const formData = ref<CrmInvoiceApi.Invoice>();
/** æ–‡æœ¬å­—段空值占位 */
function textRender(val: unknown) {
  return val === 0 || val ? String(val) : '-';
}
const [Descriptions] = useDescription({
  bordered: true,
  column: 2,
  schema: [
    { label: '开票编号', field: 'no', render: textRender },
    { label: '客户名称', field: 'customerName', render: textRender },
    {
      label: '合同编号',
      field: 'contract',
      render: (_val, data) => (data?.contract?.no ? data.contract.no : '-'),
    },
    {
      label: '发票类型',
      field: 'invoiceType',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.CRM_INVOICE_TYPE, value: val }),
    },
    { label: '发票号码', field: 'invoiceNo', render: textRender },
    { label: '发票抬头', field: 'invoiceTitle', render: textRender },
    {
      label: '开票金额(元)',
      field: 'price',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    {
      label: '剩余可回款金额(元)',
      field: 'remainingInvoicePrice',
      render: (val) => erpPriceInputFormatter(val ?? 0) || '-',
    },
    {
      label: '开票日期',
      field: 'invoiceTime',
      render: (val) => formatDateTime(val) as string,
    },
    {
      label: '审批状态',
      field: 'auditStatus',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.CRM_AUDIT_STATUS, value: val }),
    },
    { label: '负责人', field: 'ownerUserName', render: textRender },
    { label: '创建人', field: 'creatorName', render: textRender },
    {
      label: '创建时间',
      field: 'createTime',
      render: (val) => formatDateTime(val) as string,
    },
    { label: '备注', field: 'remark', render: textRender },
  ] as DescriptionItemSchema[],
});
onMounted(async () => {
  try {
    loading.value = true;
    formData.value = await getInvoice(Number(props.id));
  } finally {
    loading.value = false;
  }
});
</script>
<template>
  <ContentWrap class="m-2">
    <Spin :spinning="loading" tip="加载中...">
      <div class="space-y-4">
        <!-- å¼€ç¥¨åŸºæœ¬ä¿¡æ¯ -->
        <div class="rounded-lg border border-border bg-background p-4">
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">开票基本信息</span>
          </div>
          <Descriptions :data="formData" />
        </div>
      </div>
    </Spin>
  </ContentWrap>
</template>
src/views/crm/receivable/components/data.ts
@@ -53,16 +53,6 @@
      minWidth: 150,
    },
    {
      title: '回款状态',
      field: 'auditStatus',
      minWidth: 100,
      fixed: 'right',
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
      },
    },
    {
      title: '操作',
      field: 'actions',
      width: 130,
src/views/crm/receivable/data.ts
@@ -357,16 +357,6 @@
      minWidth: 150,
    },
    {
      title: '回款状态',
      field: 'auditStatus',
      minWidth: 100,
      fixed: 'right',
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
      },
    },
    {
      title: '操作',
      field: 'actions',
      minWidth: 200,
src/views/crm/receivable/detail/approve/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,112 @@
<script lang="ts" setup>
import type { CrmReceivableApi } from '#/api/crm/receivable';
import type { DescriptionItemSchema } from '#/components/description';
import { h, onMounted, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
import { Spin } from 'ant-design-vue';
import { getReceivable } from '#/api/crm/receivable';
import { DictTag } from '#/components/dict-tag';
import { useDescription } from '#/components/description';
defineOptions({ name: 'CrmReceivableApproveDetail' });
const props = defineProps<{ id: string }>();
const loading = ref(false);
const formData = ref<CrmReceivableApi.Receivable>();
/** æ–‡æœ¬å­—段空值占位 */
function textRender(val: unknown) {
  return val === 0 || val ? String(val) : '-';
}
/** å…³è”开票展示 */
function invoiceRender(_val: unknown, data?: CrmReceivableApi.Receivable) {
  if (!data?.invoice?.no) {
    return '-';
  }
  return `${data.invoice.no} / ${data.invoice.invoiceNo ?? '-'} / Â¥${
    data.invoice.price ?? 0
  }`;
}
const [Descriptions] = useDescription({
  bordered: true,
  column: 2,
  schema: [
    { label: '回款编号', field: 'no', render: textRender },
    { label: '客户名称', field: 'customerName', render: textRender },
    {
      label: '合同编号',
      field: 'contract',
      render: (_val, data) => (data?.contract?.no ? data.contract.no : '-'),
    },
    { label: '关联开票', field: 'invoice', render: invoiceRender },
    {
      label: '回款日期',
      field: 'returnTime',
      render: (val) => formatDateTime(val) as string,
    },
    {
      label: '回款金额(元)',
      field: 'price',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    {
      label: '回款方式',
      field: 'returnType',
      render: (val) =>
        h(DictTag, {
          type: DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE,
          value: val,
        }),
    },
    {
      label: '审批状态',
      field: 'auditStatus',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.CRM_AUDIT_STATUS, value: val }),
    },
    { label: '负责人', field: 'ownerUserName', render: textRender },
    { label: '创建人', field: 'creatorName', render: textRender },
    {
      label: '创建时间',
      field: 'createTime',
      render: (val) => formatDateTime(val) as string,
    },
    { label: '备注', field: 'remark', render: textRender },
  ] as DescriptionItemSchema[],
});
onMounted(async () => {
  try {
    loading.value = true;
    formData.value = await getReceivable(Number(props.id));
  } finally {
    loading.value = false;
  }
});
</script>
<template>
  <ContentWrap class="m-2">
    <Spin :spinning="loading" tip="加载中...">
      <div class="space-y-4">
        <!-- å›žæ¬¾åŸºæœ¬ä¿¡æ¯ -->
        <div class="rounded-lg border border-border bg-background p-4">
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">回款基本信息</span>
          </div>
          <Descriptions :data="formData" />
        </div>
      </div>
    </Spin>
  </ContentWrap>
</template>
src/views/erp/finance/payment/detail/approve/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,172 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ErpFinancePaymentApi } from '#/api/erp/finance/payment';
import type { DescriptionItemSchema } from '#/components/description';
import { h, nextTick, onMounted, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
import { Spin } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getFinancePayment } from '#/api/erp/finance/payment';
import { DictTag } from '#/components/dict-tag';
import { useDescription } from '#/components/description';
defineOptions({ name: 'ErpFinancePaymentApproveDetail' });
const props = defineProps<{ id: string }>();
const loading = ref(false);
const formData = ref<ErpFinancePaymentApi.FinancePayment>();
/** æ–‡æœ¬å­—段空值占位 */
function textRender(val: unknown) {
  return val === 0 || val ? String(val) : '-';
}
/** ä»˜æ¬¾æ˜Žç»†ï¼ˆå®¡æ‰¹è¯¦æƒ…只读列) */
function usePaymentItemColumns(): VxeTableGridOptions['columns'] {
  return [
    { type: 'seq', title: '序号', width: 60 },
    { field: 'bizNo', title: '来票编号', minWidth: 200 },
    {
      field: 'totalPrice',
      title: '应付金额',
      minWidth: 120,
      formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-',
    },
    {
      field: 'paidPrice',
      title: '已付金额',
      minWidth: 120,
      formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-',
    },
    {
      field: 'paymentPrice',
      title: '本次付款',
      minWidth: 120,
      formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-',
    },
    {
      field: 'remark',
      title: '备注',
      minWidth: 150,
      formatter: ({ cellValue }) => textRender(cellValue),
    },
  ];
}
const [Grid, gridApi] = useVbenVxeGrid({
  gridOptions: {
    columns: usePaymentItemColumns(),
    data: [],
    minHeight: 200,
    autoResize: true,
    border: true,
    rowConfig: {
      keyField: 'bizId',
      isHover: true,
    },
    pagerConfig: {
      enabled: false,
    },
    toolbarConfig: {
      enabled: false,
    },
  },
});
const [Descriptions] = useDescription({
  bordered: true,
  column: 2,
  schema: [
    { label: '付款单号', field: 'no', render: textRender },
    { label: '供应商名称', field: 'supplierName', render: textRender },
    {
      label: '关联来票',
      field: 'invoice',
      render: (_val, data) => (data?.invoice?.no ? data.invoice.no : '-'),
    },
    {
      label: '付款时间',
      field: 'paymentTime',
      render: (val) => formatDateTime(val) as string,
    },
    {
      label: '合计金额(元)',
      field: 'totalPrice',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    {
      label: '优惠金额(元)',
      field: 'discountPrice',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    {
      label: '实际付款金额(元)',
      field: 'paymentPrice',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    { label: '付款账户', field: 'accountName', render: textRender },
    { label: '财务人员', field: 'financeUserName', render: textRender },
    {
      label: '审批状态',
      field: 'status',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.ERP_AUDIT_STATUS, value: val }),
    },
    { label: '创建人', field: 'creatorName', render: textRender },
    {
      label: '创建时间',
      field: 'createTime',
      render: (val) => formatDateTime(val) as string,
    },
    { label: '备注', field: 'remark', render: textRender },
  ] as DescriptionItemSchema[],
});
onMounted(async () => {
  try {
    loading.value = true;
    formData.value = await getFinancePayment(Number(props.id));
    if (formData.value?.items?.length) {
      await nextTick();
      await gridApi.grid.reloadData(formData.value.items);
    }
  } finally {
    loading.value = false;
  }
});
</script>
<template>
  <ContentWrap class="m-2">
    <Spin :spinning="loading" tip="加载中...">
      <div class="space-y-4">
        <!-- ä»˜æ¬¾å•基本信息 -->
        <div class="rounded-lg border border-border bg-background p-4">
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">付款单基本信息</span>
          </div>
          <Descriptions :data="formData" />
        </div>
        <!-- ä»˜æ¬¾æ˜Žç»† -->
        <div
          v-if="formData?.items?.length"
          class="rounded-lg border border-border bg-background p-4"
        >
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">付款明细</span>
          </div>
          <Grid />
        </div>
      </div>
    </Spin>
  </ContentWrap>
</template>
src/views/erp/purchase/invoice/data.ts
@@ -148,19 +148,6 @@
        valueField: 'id',
      },
    },
    {
      fieldName: 'auditStatus',
      label: '审核状态',
      component: 'Select',
      componentProps: {
        options: [
          { label: '未审核', value: 10 },
          { label: '已审核', value: 20 },
        ],
        placeholder: '请选择审核状态',
        allowClear: true,
      },
    },
  ];
}
@@ -221,16 +208,9 @@
      slots: { default: 'attachment' },
    },
    {
      title: '审批状态',
      field: 'auditStatus',
      minWidth: 100,
      fixed: 'right',
      slots: { default: 'auditStatus' },
    },
    {
      title: '操作',
      field: 'actions',
      minWidth: 280,
      minWidth: 200,
      fixed: 'right',
      slots: { default: 'actions' },
    },
src/views/erp/purchase/invoice/detail/approve/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,90 @@
<script lang="ts" setup>
import type { ErpPurchaseInvoiceApi } from '#/api/erp/purchase/invoice';
import type { DescriptionItemSchema } from '#/components/description';
import { onMounted, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
import { Spin } from 'ant-design-vue';
import { getPurchaseInvoice } from '#/api/erp/purchase/invoice';
import { useDescription } from '#/components/description';
defineOptions({ name: 'ErpPurchaseInvoiceApproveDetail' });
const props = defineProps<{ id: string }>();
const loading = ref(false);
const formData = ref<ErpPurchaseInvoiceApi.PurchaseInvoice>();
/** æ–‡æœ¬å­—段空值占位 */
function textRender(val: unknown) {
  return val === 0 || val ? String(val) : '-';
}
const [Descriptions] = useDescription({
  bordered: true,
  column: 2,
  schema: [
    { label: '来票编号', field: 'no', render: textRender },
    { label: '供应商名称', field: 'supplierName', render: textRender },
    {
      label: '采购订单号',
      field: 'order',
      render: (_val, data) => (data?.order?.no ? data.order.no : '-'),
    },
    { label: '发票号码', field: 'invoiceNo', render: textRender },
    { label: '发票抬头', field: 'invoiceTitle', render: textRender },
    {
      label: '来票金额(元)',
      field: 'price',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    {
      label: '剩余可付金额(元)',
      field: 'remainingInvoicePrice',
      render: (val) => erpPriceInputFormatter(val ?? 0) || '-',
    },
    {
      label: '开票日期',
      field: 'invoiceTime',
      render: (val) => formatDateTime(val) as string,
    },
    { label: '创建人', field: 'creatorName', render: textRender },
    {
      label: '创建时间',
      field: 'createTime',
      render: (val) => formatDateTime(val) as string,
    },
    { label: '备注', field: 'remark', render: textRender },
  ] as DescriptionItemSchema[],
});
onMounted(async () => {
  try {
    loading.value = true;
    formData.value = await getPurchaseInvoice(Number(props.id));
  } finally {
    loading.value = false;
  }
});
</script>
<template>
  <ContentWrap class="m-2">
    <Spin :spinning="loading" tip="加载中...">
      <div class="space-y-4">
        <!-- æ¥ç¥¨åŸºæœ¬ä¿¡æ¯ -->
        <div class="rounded-lg border border-border bg-background p-4">
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">来票基本信息</span>
          </div>
          <Descriptions :data="formData" />
        </div>
      </div>
    </Spin>
  </ContentWrap>
</template>
src/views/erp/purchase/invoice/index.vue
@@ -12,7 +12,6 @@
  deletePurchaseInvoice,
  exportPurchaseInvoice,
  getPurchaseInvoicePage,
  updatePurchaseInvoiceStatus,
} from '#/api/erp/purchase/invoice';
import { $t } from '#/locales';
@@ -62,24 +61,6 @@
  try {
    await deletePurchaseInvoice(row.id!);
    message.success($t('ui.actionMessage.deleteSuccess', [row.no]));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** æ›´æ–°æ¥ç¥¨çŠ¶æ€ï¼ˆå®¡æ ¸/反审核) */
async function handleUpdateStatus(
  row: ErpPurchaseInvoiceApi.PurchaseInvoice,
  status: number,
) {
  const hideLoading = message.loading({
    content: '更新状态中...',
    duration: 0,
  });
  try {
    await updatePurchaseInvoiceStatus(row.id!, status);
    message.success('更新状态成功');
    handleRefresh();
  } finally {
    hideLoading();
@@ -153,11 +134,6 @@
        <Tag v-if="!row.hasAttachment" color="warning">未上传</Tag>
        <Tag v-else color="success">已上传</Tag>
      </template>
      <template #auditStatus="{ row }">
        <Tag v-if="row.auditStatus === 10" color="warning">未审核</Tag>
        <Tag v-else-if="row.auditStatus === 20" color="success">已审核</Tag>
        <Tag v-else color="default">--</Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
@@ -173,24 +149,7 @@
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['erp:purchase-invoice:update'],
              ifShow: row.auditStatus === 10,
              onClick: handleEdit.bind(null, row),
            },
            {
              label: '审核',
              type: 'link',
              icon: ACTION_ICON.AUDIT,
              auth: ['erp:purchase-invoice:update-status'],
              ifShow: row.auditStatus === 10,
              onClick: handleUpdateStatus.bind(null, row, 20),
            },
            {
              label: '反审核',
              type: 'link',
              icon: ACTION_ICON.VIEW,
              auth: ['erp:purchase-invoice:update-status'],
              ifShow: row.auditStatus === 20,
              onClick: handleUpdateStatus.bind(null, row, 10),
            },
            {
              label: $t('common.delete'),
@@ -198,7 +157,6 @@
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['erp:purchase-invoice:delete'],
              ifShow: row.auditStatus !== 20,
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.no]),
                confirm: handleDelete.bind(null, row),
src/views/erp/purchase/invoice/modules/form.vue
@@ -85,9 +85,15 @@
    try {
      formData.value = await getPurchaseInvoice(data.id);
      if (formData.value?.attachmentList?.length) {
        // ç”¨ attachmentList ä»£æ›¿ blobIds,让上传组件能展示文件名和预览链接
        // è½¬ä¸ºä¸Šä¼ ç»„件的文件对象;提交时 formApi ä¼šæŒ‰ valueKey æå–附件 ID
        (formData.value as Record<string, unknown>).blobIds =
          formData.value.attachmentList;
          formData.value.attachmentList.map((item) => ({
            uid: String(item.id),
            name: item.name || '',
            url: item.url || '',
            status: 'done',
            id: item.id,
          }));
      }
      await formApi.setValues(formData.value as any);
    } finally {
src/views/erp/purchase/order/detail/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,134 @@
<script lang="ts" setup>
import type { ErpPurchaseOrderApi } from '#/api/erp/purchase/order';
import type { DescriptionItemSchema } from '#/components/description';
import { h, onMounted, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import {
  erpCountInputFormatter,
  erpPriceInputFormatter,
  formatDateTime,
} from '@vben/utils';
import { Spin, Tag } from 'ant-design-vue';
import { getPurchaseOrder } from '#/api/erp/purchase/order';
import { DictTag } from '#/components/dict-tag';
import { useDescription } from '#/components/description';
import ItemForm from '../modules/item-form.vue';
defineOptions({ name: 'ErpPurchaseOrderDetail' });
const props = defineProps<{ id: string }>();
const loading = ref(false);
const formData = ref<ErpPurchaseOrderApi.PurchaseOrder>();
/** æ–‡æœ¬å­—段空值占位 */
function textRender(val: unknown) {
  return val === 0 || val ? String(val) : '-';
}
/** å…¥åº“状态渲染 */
function renderInStatus(val?: number) {
  const map: Record<number, { text: string; color: string }> = {
    0: { text: '未入库', color: 'default' },
    1: { text: '部分入库', color: 'warning' },
    2: { text: '全部入库', color: 'success' },
  };
  const item = map[val ?? 0]!;
  return h(Tag, { color: item.color }, () => item.text);
}
const [Descriptions] = useDescription({
  bordered: true,
  column: 2,
  schema: [
    { label: '采购订单号', field: 'no' },
    { label: '供应商名称', field: 'supplierName', render: textRender },
    {
      label: '订单时间',
      field: 'orderTime',
      render: (val) => formatDateTime(val) as string,
    },
    {
      label: '合计数量',
      field: 'totalCount',
      render: (val) => erpCountInputFormatter(val) || '-',
    },
    {
      label: '合计金额',
      field: 'totalPrice',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    {
      label: '优惠金额',
      field: 'discountPrice',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    {
      label: '定金金额',
      field: 'depositPrice',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    {
      label: '审批状态',
      field: 'status',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.ERP_AUDIT_STATUS, value: val }),
    },
    {
      label: '入库状态',
      field: 'inStatus',
      render: (val) => renderInStatus(val as number),
    },
    { label: '创建人', field: 'creatorName', render: textRender },
    {
      label: '创建时间',
      field: 'createTime',
      render: (val) => formatDateTime(val) as string,
    },
    { label: '备注', field: 'remark', render: textRender },
  ] as DescriptionItemSchema[],
});
onMounted(async () => {
  try {
    loading.value = true;
    formData.value = await getPurchaseOrder(Number(props.id));
  } finally {
    loading.value = false;
  }
});
</script>
<template>
  <ContentWrap class="m-2">
    <Spin :spinning="loading" tip="加载中...">
      <div class="space-y-4">
        <!-- åŸºæœ¬ä¿¡æ¯ -->
        <div class="rounded-lg border border-border bg-background p-4">
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">基本信息</span>
          </div>
          <Descriptions :data="formData" />
        </div>
        <!-- é‡‡è´­æ˜Žç»† -->
        <div
          v-if="formData?.items?.length"
          class="rounded-lg border border-border bg-background p-4"
        >
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">采购明细</span>
          </div>
          <ItemForm :items="formData.items" :disabled="true" />
        </div>
      </div>
    </Spin>
  </ContentWrap>
</template>
src/views/erp/purchase/request/detail/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,181 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ErpPurchaseRequestApi } from '#/api/erp/purchase/request';
import type { DescriptionItemSchema } from '#/components/description';
import { h, nextTick, onMounted, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import {
  erpCountInputFormatter,
  erpPriceInputFormatter,
  formatDateTime,
} from '@vben/utils';
import { Spin } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getPurchaseRequest } from '#/api/erp/purchase/request';
import { DictTag } from '#/components/dict-tag';
import { useDescription } from '#/components/description';
defineOptions({ name: 'ErpPurchaseRequestDetail' });
const props = defineProps<{ id: string }>();
const loading = ref(false);
const formData = ref<ErpPurchaseRequestApi.PurchaseRequest>();
/** æ–‡æœ¬å­—段空值占位 */
function textRender(val: unknown) {
  return val === 0 || val ? String(val) : '-';
}
/** é‡‡è´­ç”³è¯·æ˜Žç»†ï¼ˆå®¡æ‰¹è¯¦æƒ…只读列) */
function useDetailItemColumns(): VxeTableGridOptions['columns'] {
  return [
    { type: 'seq', title: '序号', width: 60 },
    { field: 'productName', title: '产品名称', minWidth: 180 },
    { field: 'productUnitName', title: '单位', minWidth: 80 },
    {
      field: 'count',
      title: '数量',
      minWidth: 100,
      formatter: ({ cellValue }) => erpCountInputFormatter(cellValue) || '-',
    },
    {
      field: 'productPrice',
      title: '单价(参考价)',
      minWidth: 120,
      formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-',
    },
    {
      field: 'totalPrice',
      title: '预估金额',
      minWidth: 120,
      formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-',
    },
    {
      field: 'taxPercent',
      title: '税率(%)',
      minWidth: 90,
      formatter: ({ cellValue }) => (cellValue ? `${cellValue}%` : '-'),
    },
    { field: 'demandTime', title: '需求日期', minWidth: 110 },
    {
      field: 'remark',
      title: '备注',
      minWidth: 160,
      showOverflow: 'tooltip',
    },
    {
      field: 'qcCheckFlag',
      title: '是否质检',
      minWidth: 80,
      formatter: ({ cellValue }) => (cellValue ? '是' : '否'),
    },
  ];
}
const [Grid, gridApi] = useVbenVxeGrid({
  gridOptions: {
    columns: useDetailItemColumns(),
    data: [],
    minHeight: 200,
    autoResize: true,
    border: true,
    rowConfig: {
      keyField: 'seq',
      isHover: true,
    },
    pagerConfig: {
      enabled: false,
    },
    toolbarConfig: {
      enabled: false,
    },
  },
});
const [Descriptions] = useDescription({
  bordered: true,
  column: 2,
  schema: [
    { label: '申请编号', field: 'no' },
    {
      label: '审批状态',
      field: 'status',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.ERP_AUDIT_STATUS, value: val }),
    },
    { label: '申请人', field: 'requestUserName', render: textRender },
    { label: '申请部门', field: 'requestDeptName', render: textRender },
    {
      label: '申请时间',
      field: 'requestTime',
      render: (val) => formatDateTime(val) as string,
    },
    { label: '供应商名称', field: 'supplierName', render: textRender },
    {
      label: '合计数量',
      field: 'totalCount',
      render: (val) => erpCountInputFormatter(val) || '-',
    },
    {
      label: '合计金额',
      field: 'totalPrice',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    { label: '申请理由', field: 'requestReason', render: textRender },
    { label: '生成采购订单', field: 'orderNo', render: textRender },
    {
      label: '创建时间',
      field: 'createTime',
      render: (val) => formatDateTime(val) as string,
    },
    { label: '备注', field: 'remark', render: textRender },
  ] as DescriptionItemSchema[],
});
onMounted(async () => {
  try {
    loading.value = true;
    formData.value = await getPurchaseRequest(Number(props.id));
    if (formData.value?.items?.length) {
      await nextTick();
      await gridApi.grid.reloadData(formData.value.items);
    }
  } finally {
    loading.value = false;
  }
});
</script>
<template>
  <ContentWrap class="m-2">
    <Spin :spinning="loading" tip="加载中...">
      <div class="space-y-4">
        <!-- åŸºæœ¬ä¿¡æ¯ -->
        <div class="rounded-lg border border-border bg-background p-4">
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">基本信息</span>
          </div>
          <Descriptions :data="formData" />
        </div>
        <!-- ç”³è¯·æ˜Žç»† -->
        <div
          v-if="formData?.items?.length"
          class="rounded-lg border border-border bg-background p-4"
        >
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">申请明细</span>
          </div>
          <Grid />
        </div>
      </div>
    </Spin>
  </ContentWrap>
</template>
src/views/hrm/attendance/exception/detail/approve/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,82 @@
<script lang="ts" setup>
import type { HrmAttendanceExceptionApi } from '#/api/hrm/attendance/exception';
import type { DescriptionItemSchema } from '#/components/description';
import { h, onMounted, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { formatDateTime } from '@vben/utils';
import { Spin } from 'ant-design-vue';
import { getAttendanceException } from '#/api/hrm/attendance/exception';
import { DictTag } from '#/components/dict-tag';
import { useDescription } from '#/components/description';
defineOptions({ name: 'HrmAttendanceExceptionApproveDetail' });
const props = defineProps<{ id: string }>();
const loading = ref(false);
const formData = ref<HrmAttendanceExceptionApi.AttendanceException>();
/** æ–‡æœ¬å­—段空值占位 */
function textRender(val: unknown) {
  return val === 0 || val ? String(val) : '-';
}
const [Descriptions] = useDescription({
  bordered: true,
  column: 2,
  schema: [
    { label: '员工姓名', field: 'userName', render: textRender },
    { label: '所属部门', field: 'deptName', render: textRender },
    {
      label: '异常日期',
      field: 'date',
      render: (val) => formatDateTime(val) as string,
    },
    { label: '异常类型', field: 'exceptionTypeName', render: textRender },
    { label: '异常说明', field: 'reason', render: textRender },
    {
      label: '审批状态',
      field: 'status',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.HRM_AUDIT_STATUS, value: val }),
    },
    { label: '备注', field: 'remark', render: textRender },
    {
      label: '创建时间',
      field: 'createTime',
      render: (val) => formatDateTime(val) as string,
    },
  ] as DescriptionItemSchema[],
});
onMounted(async () => {
  try {
    loading.value = true;
    formData.value = await getAttendanceException(Number(props.id));
  } finally {
    loading.value = false;
  }
});
</script>
<template>
  <ContentWrap class="m-2">
    <Spin :spinning="loading" tip="加载中...">
      <div class="space-y-4">
        <!-- è€ƒå‹¤å¼‚常基本信息 -->
        <div class="rounded-lg border border-border bg-background p-4">
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">考勤异常基本信息</span>
          </div>
          <Descriptions :data="formData" />
        </div>
      </div>
    </Spin>
  </ContentWrap>
</template>
src/views/hrm/leave/detail/approve/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,93 @@
<script lang="ts" setup>
import type { HrmLeaveApi } from '#/api/hrm/leave';
import type { DescriptionItemSchema } from '#/components/description';
import { h, onMounted, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { formatDateTime } from '@vben/utils';
import { Spin } from 'ant-design-vue';
import { getLeave } from '#/api/hrm/leave';
import { DictTag } from '#/components/dict-tag';
import { useDescription } from '#/components/description';
defineOptions({ name: 'HrmLeaveApproveDetail' });
const props = defineProps<{ id: string }>();
const loading = ref(false);
const formData = ref<HrmLeaveApi.Leave>();
/** æ–‡æœ¬å­—段空值占位 */
function textRender(val: unknown) {
  return val === 0 || val ? String(val) : '-';
}
const [Descriptions] = useDescription({
  bordered: true,
  column: 2,
  schema: [
    { label: '员工姓名', field: 'employeeName', render: textRender },
    { label: '所属部门', field: 'deptName', render: textRender },
    {
      label: '请假类型',
      field: 'leaveType',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.HRM_LEAVE_TYPE, value: val }),
    },
    {
      label: '开始时间',
      field: 'startTime',
      render: (val) => formatDateTime(val) as string,
    },
    {
      label: '结束时间',
      field: 'endTime',
      render: (val) => formatDateTime(val) as string,
    },
    { label: '请假时长', field: 'duration', render: textRender },
    { label: '请假事由', field: 'reason', render: textRender },
    {
      label: '审批状态',
      field: 'status',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.HRM_AUDIT_STATUS, value: val }),
    },
    { label: '备注', field: 'remark', render: textRender },
    {
      label: '创建时间',
      field: 'createTime',
      render: (val) => formatDateTime(val) as string,
    },
  ] as DescriptionItemSchema[],
});
onMounted(async () => {
  try {
    loading.value = true;
    formData.value = await getLeave(Number(props.id));
  } finally {
    loading.value = false;
  }
});
</script>
<template>
  <ContentWrap class="m-2">
    <Spin :spinning="loading" tip="加载中...">
      <div class="space-y-4">
        <!-- è¯·å‡åŸºæœ¬ä¿¡æ¯ -->
        <div class="rounded-lg border border-border bg-background p-4">
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">请假基本信息</span>
          </div>
          <Descriptions :data="formData" />
        </div>
      </div>
    </Spin>
  </ContentWrap>
</template>
src/views/hrm/resignation/detail/approve/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,95 @@
<script lang="ts" setup>
import type { HrmResignationApi } from '#/api/hrm/resignation';
import type { DescriptionItemSchema } from '#/components/description';
import { h, onMounted, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { formatDateTime } from '@vben/utils';
import { Spin } from 'ant-design-vue';
import { getResignation } from '#/api/hrm/resignation';
import { DictTag } from '#/components/dict-tag';
import { useDescription } from '#/components/description';
defineOptions({ name: 'HrmResignationApproveDetail' });
const props = defineProps<{ id: string }>();
const loading = ref(false);
const formData = ref<HrmResignationApi.Resignation>();
/** æ–‡æœ¬å­—段空值占位 */
function textRender(val: unknown) {
  return val === 0 || val ? String(val) : '-';
}
const [Descriptions] = useDescription({
  bordered: true,
  column: 2,
  schema: [
    { label: '员工姓名', field: 'employeeName', render: textRender },
    { label: '所属部门', field: 'deptName', render: textRender },
    { label: '职位', field: 'postName', render: textRender },
    {
      label: '离职类型',
      field: 'resignationType',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.HRM_RESIGNATION_TYPE, value: val }),
    },
    {
      label: '预计离职日期',
      field: 'expectedDate',
      render: (val) => formatDateTime(val) as string,
    },
    { label: '离职原因', field: 'reason', render: textRender },
    { label: '交接人', field: 'handoverUserName', render: textRender },
    {
      label: '交接状态',
      field: 'handoverStatus',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.HRM_HANDOVER_STATUS, value: val }),
    },
    {
      label: '审批状态',
      field: 'status',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.HRM_AUDIT_STATUS, value: val }),
    },
    { label: '备注', field: 'remark', render: textRender },
    {
      label: '创建时间',
      field: 'createTime',
      render: (val) => formatDateTime(val) as string,
    },
  ] as DescriptionItemSchema[],
});
onMounted(async () => {
  try {
    loading.value = true;
    formData.value = await getResignation(Number(props.id));
  } finally {
    loading.value = false;
  }
});
</script>
<template>
  <ContentWrap class="m-2">
    <Spin :spinning="loading" tip="加载中...">
      <div class="space-y-4">
        <!-- ç¦»èŒåŸºæœ¬ä¿¡æ¯ -->
        <div class="rounded-lg border border-border bg-background p-4">
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">离职基本信息</span>
          </div>
          <Descriptions :data="formData" />
        </div>
      </div>
    </Spin>
  </ContentWrap>
</template>
src/views/hrm/transfer/detail/approve/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,90 @@
<script lang="ts" setup>
import type { HrmTransferApi } from '#/api/hrm/transfer';
import type { DescriptionItemSchema } from '#/components/description';
import { h, onMounted, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { formatDateTime } from '@vben/utils';
import { Spin } from 'ant-design-vue';
import { getTransfer } from '#/api/hrm/transfer';
import { DictTag } from '#/components/dict-tag';
import { useDescription } from '#/components/description';
defineOptions({ name: 'HrmTransferApproveDetail' });
const props = defineProps<{ id: string }>();
const loading = ref(false);
const formData = ref<HrmTransferApi.Transfer>();
/** æ–‡æœ¬å­—段空值占位 */
function textRender(val: unknown) {
  return val === 0 || val ? String(val) : '-';
}
const [Descriptions] = useDescription({
  bordered: true,
  column: 2,
  schema: [
    { label: '员工姓名', field: 'employeeName', render: textRender },
    { label: '当前部门', field: 'currentDeptName', render: textRender },
    { label: '当前职位', field: 'currentPostName', render: textRender },
    { label: '目标部门', field: 'targetDeptName', render: textRender },
    { label: '目标职位', field: 'targetPostName', render: textRender },
    {
      label: '调岗类型',
      field: 'transferType',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.HRM_TRANSFER_TYPE, value: val }),
    },
    {
      label: '生效日期',
      field: 'effectiveDate',
      render: (val) => formatDateTime(val) as string,
    },
    { label: '调岗原因', field: 'reason', render: textRender },
    {
      label: '审批状态',
      field: 'status',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.HRM_AUDIT_STATUS, value: val }),
    },
    { label: '备注', field: 'remark', render: textRender },
    {
      label: '创建时间',
      field: 'createTime',
      render: (val) => formatDateTime(val) as string,
    },
  ] as DescriptionItemSchema[],
});
onMounted(async () => {
  try {
    loading.value = true;
    formData.value = await getTransfer(Number(props.id));
  } finally {
    loading.value = false;
  }
});
</script>
<template>
  <ContentWrap class="m-2">
    <Spin :spinning="loading" tip="加载中...">
      <div class="space-y-4">
        <!-- è°ƒå²—基本信息 -->
        <div class="rounded-lg border border-border bg-background p-4">
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">调岗基本信息</span>
          </div>
          <Descriptions :data="formData" />
        </div>
      </div>
    </Spin>
  </ContentWrap>
</template>
src/views/im/home/index.vue
@@ -194,7 +194,7 @@
  [() => conversationStore.getTotalUnreadCount, () => route.fullPath],
  ([count]) => {
    nextTick(() => {
      const base = preferences.app.name
      const base = preferences.copyright.companyName
      document.title = count > 0 ? `(${count > 99 ? '99+' : count}条未读)${base}` : base
    })
  },
src/views/mes/dv/metering/components/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2 @@
export { default as DvMeteringSelectDialog } from './select-dialog.vue';
export { default as DvMeteringSelect } from './select.vue';
src/views/mes/dv/metering/components/select-dialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,213 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesDvMeteringApi } from '#/api/mes/dv/metering';
import { nextTick, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getMeteringPage } from '#/api/mes/dv/metering';
import {
  useMeteringSelectGridColumns,
  useMeteringSelectGridFormSchema,
} from '../data';
defineOptions({ name: 'DvMeteringSelectDialog' });
const emit = defineEmits<{
  selected: [rows: MesDvMeteringApi.Metering[]];
}>();
const open = ref(false); // å¼¹çª—是否打开
const multiple = ref(false); // æ˜¯å¦å¤šé€‰
const selectedRows = ref<MesDvMeteringApi.Metering[]>([]); // å·²é€‰å™¨å…·åˆ—表
const preSelectedIds = ref<number[]>([]); // é¢„选器具编号列表
/** èŽ·å–å½“å‰è¡¨æ ¼æ•°æ® */
function getTableRows() {
  return gridApi.grid.getTableData().fullData as MesDvMeteringApi.Metering[];
}
/** èŽ·å–å¤šé€‰è®°å½•ï¼ŒåŒ…å« VXE reserve è·¨é¡µè®°å½• */
function getMultipleSelectedRows() {
  const selectedMap = new Map<number, MesDvMeteringApi.Metering>();
  const records = [
    ...(gridApi.grid.getCheckboxReserveRecords?.() ?? []),
    ...(gridApi.grid.getCheckboxRecords?.() ?? []),
  ] as MesDvMeteringApi.Metering[];
  records.forEach((row) => {
    const rowId = row.id;
    if (rowId !== undefined && rowId !== null) {
      selectedMap.set(rowId, row);
    }
  });
  return [...selectedMap.values()];
}
/** å¤„理勾选变化 */
function handleCheckboxSelectChange() {
  selectedRows.value = getMultipleSelectedRows();
}
/** å¤„理单选变化 */
function handleRadioChange(row: MesDvMeteringApi.Metering) {
  selectedRows.value = [row];
}
/** å¤šé€‰æ¨¡å¼ä¸‹åˆ‡æ¢è¡Œå‹¾é€‰ */
async function toggleMultipleRow(row: MesDvMeteringApi.Metering) {
  const selected = gridApi.grid.isCheckedByCheckboxRow(row);
  await gridApi.grid.setCheckboxRow(row, !selected);
  selectedRows.value = getMultipleSelectedRows();
}
/** å¤„理行双击 */
async function handleCellDblclick({
  row,
}: {
  row: MesDvMeteringApi.Metering;
}) {
  if (multiple.value) {
    await toggleMultipleRow(row);
    return;
  }
  selectedRows.value = [row];
  await gridApi.grid.setRadioRow(row);
  handleConfirm();
}
/** å›žæ˜¾é¢„选器具 */
async function applyPreSelection() {
  if (preSelectedIds.value.length === 0) {
    return;
  }
  const rows = getTableRows();
  for (const row of rows) {
    if (
      row.id === undefined ||
      row.id === null ||
      !preSelectedIds.value.includes(row.id)
    ) {
      continue;
    }
    if (multiple.value) {
      await gridApi.grid.setCheckboxRow(row, true);
    } else {
      await gridApi.grid.setRadioRow(row);
      selectedRows.value = [row];
      return;
    }
  }
  if (multiple.value) {
    selectedRows.value = getMultipleSelectedRows();
  }
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useMeteringSelectGridFormSchema(),
  },
  gridOptions: {
    columns: useMeteringSelectGridColumns(false),
    height: 480,
    keepSource: true,
    checkboxConfig: {
      highlight: true,
      range: true,
      reserve: true,
    },
    radioConfig: {
      highlight: true,
      trigger: 'row',
    },
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getMeteringPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<MesDvMeteringApi.Metering>,
  gridEvents: {
    cellDblclick: handleCellDblclick,
    checkboxAll: handleCheckboxSelectChange,
    checkboxChange: handleCheckboxSelectChange,
    radioChange: ({ row }: { row: MesDvMeteringApi.Metering }) => {
      handleRadioChange(row);
    },
  },
});
/** é‡ç½®æŸ¥è¯¢å’Œé€‰æ‹©çŠ¶æ€ */
async function resetQueryState() {
  selectedRows.value = [];
  await gridApi.grid.clearCheckboxRow();
  await gridApi.grid.clearCheckboxReserve();
  await gridApi.grid.clearRadioRow();
  await gridApi.formApi.resetForm();
}
/** æ‰“开计量器具选择弹窗 */
async function openModal(
  selectedIds?: number[],
  options?: { multiple?: boolean },
) {
  open.value = true;
  multiple.value = options?.multiple ?? false;
  preSelectedIds.value = selectedIds || [];
  await nextTick();
  gridApi.setGridOptions({
    columns: useMeteringSelectGridColumns(multiple.value),
  });
  await resetQueryState();
  await gridApi.query();
  await nextTick();
  await applyPreSelection();
}
/** å…³é—­è®¡é‡å™¨å…·é€‰æ‹©å¼¹çª— */
async function closeModal() {
  open.value = false;
  await resetQueryState();
}
/** ç¡®è®¤é€‰æ‹©è®¡é‡å™¨å…· */
function handleConfirm() {
  const rows = multiple.value ? getMultipleSelectedRows() : selectedRows.value;
  if (rows.length === 0) {
    message.warning(multiple.value ? '请至少选择一条数据' : '请选择一条数据');
    return;
  }
  emit('selected', multiple.value ? rows : [rows[0]!]);
  open.value = false;
}
defineExpose({ open: openModal });
</script>
<template>
  <Modal
    v-model:open="open"
    title="计量器具选择"
    width="70%"
    :destroy-on-close="true"
    @ok="handleConfirm"
    @cancel="closeModal"
  >
    <Grid table-title="计量器具列表" />
  </Modal>
</template>
src/views/mes/dv/metering/components/select.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,142 @@
<script lang="ts" setup>
import type { MesDvMeteringApi } from '#/api/mes/dv/metering';
import { computed, ref, useAttrs, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { Input, Tooltip } from 'ant-design-vue';
import { getMetering } from '#/api/mes/dv/metering';
import DvMeteringSelectDialog from './select-dialog.vue';
defineOptions({ name: 'DvMeteringSelect', inheritAttrs: false });
const props = withDefaults(
  defineProps<{
    allowClear?: boolean;
    disabled?: boolean;
    modelValue?: number;
    placeholder?: string;
  }>(),
  {
    allowClear: true,
    disabled: false,
    modelValue: undefined,
    placeholder: '请选择计量器具',
  },
);
const emit = defineEmits<{
  change: [item: MesDvMeteringApi.Metering | undefined];
  'update:modelValue': [value: number | undefined];
}>();
const attrs = useAttrs(); // é€ä¼ å±žæ€§
const dialogRef = ref<InstanceType<typeof DvMeteringSelectDialog>>(); // è®¡é‡å™¨å…·é€‰æ‹©å¼¹çª—
const hovering = ref(false); // æ˜¯å¦æ‚¬åœ
const selectedItem = ref<MesDvMeteringApi.Metering>(); // å½“前选中的计量器具
const displayLabel = computed(() => selectedItem.value?.name ?? ''); // é€‰æ‹©å™¨å±•示名称
const showClear = computed(
  () =>
    props.allowClear &&
    !props.disabled &&
    hovering.value &&
    props.modelValue !== null,
);
/** æ ¹æ®å™¨å…·ç¼–号回显选择器 */
async function resolveItemById(id: number | undefined) {
  if (!id) {
    selectedItem.value = undefined;
    return;
  }
  if (selectedItem.value?.id === id) {
    return;
  }
  selectedItem.value = await getMetering(id);
}
watch(
  () => props.modelValue,
  (value) => {
    resolveItemById(value);
  },
  { immediate: true },
);
/** æ¸…空已选器具 */
function clearSelected() {
  selectedItem.value = undefined;
  emit('update:modelValue', undefined);
  emit('change', undefined);
}
/** æ‰“开计量器具选择弹窗 */
function handleClick(event: MouseEvent) {
  if (props.disabled) {
    return;
  }
  const target = event.target as HTMLElement;
  if (showClear.value && target.closest('.ant-input-suffix')) {
    event.stopPropagation();
    clearSelected();
    return;
  }
  const selectedIds = (
    props.modelValue === null ? [] : [props.modelValue]
  ) as number[];
  dialogRef.value?.open(selectedIds, { multiple: false });
}
/** å›žå¡«é€‰ä¸­çš„计量器具 */
function handleSelected(rows: MesDvMeteringApi.Metering[]) {
  const item = rows[0];
  if (!item) {
    return;
  }
  selectedItem.value = item;
  emit('update:modelValue', item.id);
  emit('change', item);
}
</script>
<template>
  <div
    v-bind="attrs"
    class="w-full"
    :class="disabled ? 'cursor-not-allowed' : 'cursor-pointer'"
    @click="handleClick"
    @mouseenter="hovering = true"
    @mouseleave="hovering = false"
  >
    <Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false">
      <template #title>
        <div v-if="selectedItem" class="leading-6">
          <div>器具编码:{{ selectedItem.code || '-' }}</div>
          <div>器具名称:{{ selectedItem.name || '-' }}</div>
          <div v-if="selectedItem.specification">
            è§„格型号:{{ selectedItem.specification }}
          </div>
          <div v-if="selectedItem.deptName">
            ä½¿ç”¨éƒ¨é—¨ï¼š{{ selectedItem.deptName }}
          </div>
        </div>
      </template>
      <Input
        :disabled="disabled"
        :placeholder="placeholder"
        :value="displayLabel"
        readonly
      >
        <template #suffix>
          <IconifyIcon
            class="size-4"
            :icon="showClear ? 'lucide:circle-x' : 'lucide:search'"
          />
        </template>
      </Input>
    </Tooltip>
  </div>
  <DvMeteringSelectDialog ref="dialogRef" @selected="handleSelected" />
</template>
src/views/mes/dv/metering/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,437 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesDvMeteringApi } from '#/api/mes/dv/metering';
import {
  DICT_TYPE,
  MesDvMeteringExpireStatusEnum,
  MesDvMeteringStatusEnum,
} from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { handleTree } from '@vben/utils';
import { z } from '#/adapter/form';
import { getSimpleDeptList } from '#/api/system/dept';
import { getSimpleUserList } from '#/api/system/user';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'update';
/** åˆ°æœŸçŠ¶æ€å±•ç¤ºé…ç½® */
export const METERING_EXPIRE_STATUS_OPTIONS: {
  color: string;
  label: string;
  value: number;
}[] = [
  {
    label: '已过期',
    value: MesDvMeteringExpireStatusEnum.OVERDUE,
    color: 'red',
  },
  {
    label: '即将到期',
    value: MesDvMeteringExpireStatusEnum.WITHIN,
    color: 'orange',
  },
  {
    label: '正常',
    value: MesDvMeteringExpireStatusEnum.NORMAL,
    color: 'green',
  },
];
/** æ–°å¢ž/修改计量器具的表单 */
export function useFormSchema(formType: FormType): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'code',
      label: '器具编码',
      component: 'Input',
      componentProps: {
        placeholder: '请输入器具编码',
      },
      dependencies: {
        triggerFields: ['id'],
        componentProps: (values) => ({ disabled: !!values.id }),
      },
      rules: 'required',
    },
    {
      fieldName: 'name',
      label: '器具名称',
      component: 'Input',
      componentProps: {
        placeholder: '请输入器具名称',
      },
      rules: 'required',
    },
    {
      fieldName: 'category',
      label: '器具类别',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_DV_METERING_CATEGORY, 'number'),
        placeholder: '请选择器具类别',
      },
      help: '如卡尺、压力表、温度计等,不同类别有常见的校准周期',
      rules: 'selectRequired',
    },
    {
      fieldName: 'specification',
      label: '规格型号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入规格型号',
      },
    },
    {
      fieldName: 'brand',
      label: '品牌/生产厂家',
      component: 'Input',
      componentProps: {
        placeholder: '请输入品牌/生产厂家',
      },
    },
    {
      fieldName: 'precision',
      label: '精度',
      component: 'Input',
      componentProps: {
        placeholder: '请输入精度',
      },
      help: '测量精度,如 0.01mm',
    },
    {
      fieldName: 'range',
      label: '量程',
      component: 'Input',
      componentProps: {
        placeholder: '请输入量程',
      },
      help: '测量范围,如 0~150mm',
    },
    {
      fieldName: 'deptId',
      label: '使用部门',
      component: 'ApiTreeSelect',
      componentProps: {
        api: async () => handleTree(await getSimpleDeptList()),
        childrenField: 'children',
        labelField: 'name',
        placeholder: '请选择使用部门',
        valueField: 'id',
      },
    },
    {
      fieldName: 'chargeUserId',
      label: '责任人',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getSimpleUserList,
        labelField: 'nickname',
        placeholder: '请选择责任人',
        valueField: 'id',
      },
      help: '该器具的维护责任人,到期提醒的站内信会发给他',
    },
    {
      fieldName: 'checkCycleCount',
      label: '校准周期',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 1,
        precision: 0,
        placeholder: '请输入校准周期',
        suffix: '月',
      },
      help: '器具每隔 N ä¸ªæœˆé¡»æ ¡å‡†/检定一次;新增检测记录时不填下次检测日期,将按「检测日期 + æ ¡å‡†å‘¨æœŸã€è‡ªåŠ¨è®¡ç®—',
      rules: 'required',
    },
    {
      fieldName: 'useDate',
      label: '投入使用日期',
      component: 'DatePicker',
      componentProps: {
        class: '!w-full',
        format: 'YYYY-MM-DD',
        placeholder: '请选择投入使用日期',
        valueFormat: 'YYYY-MM-DD',
      },
      help: '器具开始投入使用的日期(仅作记录)',
    },
    {
      fieldName: 'lastCheckDate',
      label: '上次检测日期',
      component: 'DatePicker',
      componentProps: {
        class: '!w-full',
        disabled: true,
        format: 'YYYY-MM-DD',
        placeholder: '上次检测日期',
        valueFormat: 'YYYY-MM-DD',
      },
      help: '由最新一条检测记录自动回写,不可手动修改',
      dependencies: {
        triggerFields: ['id'],
        show: () => formType === 'detail',
      },
    },
    {
      fieldName: 'nextCheckDate',
      label: '下次检测日期',
      component: 'DatePicker',
      componentProps: {
        class: '!w-full',
        disabled: true,
        format: 'YYYY-MM-DD',
        placeholder: '下次检测日期',
        valueFormat: 'YYYY-MM-DD',
      },
      help: '到期日,由检测记录自动计算回写;到期前 N å¤©ï¼ˆåŸºç¡€è®¾æ–½é…ç½®ï¼‰å°†æé†’责任人',
      dependencies: {
        triggerFields: ['id'],
        show: () => formType === 'detail',
      },
    },
    {
      fieldName: 'status',
      label: '器具状态',
      component: 'RadioGroup',
      componentProps: {
        buttonStyle: 'solid',
        optionType: 'button',
        options: getDictOptions(DICT_TYPE.MES_DV_METERING_STATUS, 'number'),
      },
      help: '在用/停用/报废',
      rules: z.number().default(MesDvMeteringStatusEnum.IN_USE),
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      formItemClass: 'col-span-3',
      componentProps: {
        placeholder: '请输入备注',
        rows: 3,
      },
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'code',
      label: '器具编码',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入器具编码',
      },
    },
    {
      fieldName: 'name',
      label: '器具名称',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入器具名称',
      },
    },
    {
      fieldName: 'category',
      label: '器具类别',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_DV_METERING_CATEGORY, 'number'),
        placeholder: '请选择器具类别',
      },
    },
    {
      fieldName: 'deptId',
      label: '使用部门',
      component: 'ApiTreeSelect',
      componentProps: {
        allowClear: true,
        api: async () => handleTree(await getSimpleDeptList()),
        childrenField: 'children',
        labelField: 'name',
        placeholder: '请选择使用部门',
        valueField: 'id',
      },
    },
    {
      fieldName: 'chargeUserId',
      label: '责任人',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getSimpleUserList,
        labelField: 'nickname',
        placeholder: '请选择责任人',
        valueField: 'id',
      },
    },
    {
      fieldName: 'status',
      label: '器具状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_DV_METERING_STATUS, 'number'),
        placeholder: '请选择器具状态',
      },
    },
    {
      fieldName: 'expireStatus',
      label: '到期状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: METERING_EXPIRE_STATUS_OPTIONS,
        placeholder: '请选择到期状态',
      },
      help: '下次检测日期距今天 â‰¤ æé†’天数(基础设施配置 mes.dv-metering.remind-days)即视为「即将到期」',
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesDvMeteringApi.Metering>['columns'] {
  return [
    {
      field: 'code',
      title: '器具编码',
      minWidth: 150,
      slots: {
        default: 'code',
      },
    },
    { field: 'name', title: '器具名称', minWidth: 150 },
    {
      field: 'category',
      title: '器具类别',
      width: 110,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_DV_METERING_CATEGORY },
      },
    },
    { field: 'specification', title: '规格型号', minWidth: 130 },
    { field: 'brand', title: '品牌/生产厂家', minWidth: 130 },
    { field: 'deptName', title: '使用部门', minWidth: 130 },
    { field: 'chargeUserNickname', title: '责任人', minWidth: 100 },
    {
      field: 'expireStatus',
      title: '到期状态',
      width: 110,
      slots: {
        default: 'expireStatus',
      },
    },
    {
      field: 'nextCheckDate',
      title: '下次检测日期',
      width: 130,
      formatter: 'formatDate',
    },
    {
      field: 'status',
      title: '器具状态',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_DV_METERING_STATUS },
      },
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 210,
      fixed: 'right',
      slots: {
        default: 'actions',
      },
    },
  ];
}
/** è®¡é‡å™¨å…·é€‰æ‹©å¼¹çª—的搜索表单 */
export function useMeteringSelectGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'code',
      label: '器具编码',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入器具编码',
      },
    },
    {
      fieldName: 'name',
      label: '器具名称',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入器具名称',
      },
    },
  ];
}
/** è®¡é‡å™¨å…·é€‰æ‹©å¼¹çª—的字段 */
export function useMeteringSelectGridColumns(
  multiple = false,
): VxeTableGridOptions<MesDvMeteringApi.Metering>['columns'] {
  return [
    { type: multiple ? 'checkbox' : 'radio', width: 50 },
    { field: 'code', title: '器具编码', width: 140 },
    { field: 'name', title: '器具名称', minWidth: 140 },
    {
      field: 'category',
      title: '器具类别',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_DV_METERING_CATEGORY },
      },
    },
    { field: 'specification', title: '规格型号', minWidth: 120 },
    { field: 'deptName', title: '使用部门', width: 120 },
    { field: 'chargeUserNickname', title: '责任人', width: 100 },
    {
      field: 'nextCheckDate',
      title: '下次检测日期',
      width: 120,
      formatter: 'formatDate',
    },
    {
      field: 'status',
      title: '器具状态',
      width: 90,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_DV_METERING_STATUS },
      },
    },
  ];
}
src/views/mes/dv/metering/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,176 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesDvMeteringApi } from '#/api/mes/dv/metering';
import { Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteMetering,
  exportMetering,
  getMeteringPage,
} from '#/api/mes/dv/metering';
import { $t } from '#/locales';
import {
  METERING_EXPIRE_STATUS_OPTIONS,
  useGridColumns,
  useGridFormSchema,
} from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** åˆ›å»ºè®¡é‡å™¨å…· */
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
/** æŸ¥çœ‹è®¡é‡å™¨å…· */
function handleDetail(row: MesDvMeteringApi.Metering) {
  formModalApi.setData({ id: row.id, formType: 'detail' }).open();
}
/** ç¼–辑计量器具 */
function handleEdit(row: MesDvMeteringApi.Metering) {
  formModalApi.setData({ id: row.id, formType: 'update' }).open();
}
/** åˆ é™¤è®¡é‡å™¨å…· */
async function handleDelete(row: MesDvMeteringApi.Metering) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', [row.name]),
    duration: 0,
  });
  try {
    await deleteMetering(row.id!);
    message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** å¯¼å‡ºè®¡é‡å™¨å…· */
async function handleExport() {
  const data = await exportMetering(await gridApi.formApi.getValues());
  downloadFileFromBlobPart({ fileName: '计量器具.xls', source: data });
}
/** åˆ°æœŸçŠ¶æ€å±•ç¤º */
function getExpireStatusConfig(status?: number) {
  return METERING_EXPIRE_STATUS_OPTIONS.find((item) => item.value === status);
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) =>
          await getMeteringPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          }),
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<MesDvMeteringApi.Metering>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="计量器具列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['计量器具']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['mes:dv-metering:create'],
              onClick: handleCreate,
            },
            {
              label: $t('ui.actionTitle.export'),
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,
              auth: ['mes:dv-metering:export'],
              onClick: handleExport,
            },
          ]"
        />
      </template>
      <template #code="{ row }">
        <Button type="link" @click="handleDetail(row)">
          {{ row.code }}
        </Button>
      </template>
      <template #expireStatus="{ row }">
        <Tag
          v-if="getExpireStatusConfig(row.expireStatus)"
          :color="getExpireStatusConfig(row.expireStatus)!.color"
        >
          {{ getExpireStatusConfig(row.expireStatus)!.label }}
        </Tag>
        <span v-else>-</span>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['mes:dv-metering:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['mes:dv-metering:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.name]),
                confirm: handleDelete.bind(null, row),
              },
            },
            {
              label: $t('common.detail'),
              type: 'link',
              icon: ACTION_ICON.VIEW,
              auth: ['mes:dv-metering:query'],
              onClick: handleDetail.bind(null, row),
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/mes/dv/metering/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,99 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { MesDvMeteringApi } from '#/api/mes/dv/metering';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
  createMetering,
  getMetering,
  updateMetering,
} from '#/api/mes/dv/metering';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const formData = ref<MesDvMeteringApi.Metering>();
const isDetail = computed(() => formType.value === 'detail');
const getTitle = computed(() => {
  if (formType.value === 'detail') {
    return '查看计量器具';
  }
  return formType.value === 'update' ? '修改计量器具' : '新增计量器具';
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    formItemClass: 'col-span-1',
    labelWidth: 110,
  },
  wrapperClass: 'grid-cols-3',
  layout: 'horizontal',
  schema: [],
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    if (isDetail.value) {
      await modalApi.close();
      return;
    }
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data = (await formApi.getValues()) as MesDvMeteringApi.Metering;
    try {
      if (formType.value === 'create') {
        await createMetering(data);
      } else {
        await updateMetering(data);
      }
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
      await modalApi.close();
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      return;
    }
    const data = modalApi.getData<{ formType: FormType; id?: number }>();
    formType.value = data.formType;
    formApi.setState({ schema: useFormSchema(formType.value) });
    formApi.setDisabled(formType.value === 'detail');
    modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
    if (!data?.id) {
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getMetering(data.id);
      await formApi.setValues(formData.value);
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-3/5">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/mes/dv/meteringcheck/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,218 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesDvMeteringCheckApi } from '#/api/mes/dv/meteringcheck';
import { markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { getSimpleUserList } from '#/api/system/user';
import { DvMeteringSelect } from '#/views/mes/dv/metering/components';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'update';
/** æ–°å¢ž/修改计量检测记录的表单 */
export function useFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'meteringId',
      label: '计量器具',
      component: markRaw(DvMeteringSelect),
      componentProps: {
        placeholder: '请选择计量器具',
      },
      help: '选择器具后,其校准周期将用于自动计算下次检测日期',
      rules: 'selectRequired',
    },
    {
      fieldName: 'checkCode',
      label: '检测编号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入检测编号',
      },
      rules: 'required',
    },
    {
      fieldName: 'checkDate',
      label: '检测日期',
      component: 'DatePicker',
      componentProps: {
        class: '!w-full',
        format: 'YYYY-MM-DD',
        placeholder: '请选择检测日期',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
    {
      fieldName: 'checkOrg',
      label: '检测机构',
      component: 'Input',
      componentProps: {
        placeholder: '请输入检测机构',
      },
    },
    {
      fieldName: 'checkResult',
      label: '检测结论',
      component: 'RadioGroup',
      componentProps: {
        buttonStyle: 'solid',
        optionType: 'button',
        options: getDictOptions(DICT_TYPE.MES_DV_METERING_CHECK_RESULT, 'number'),
      },
      rules: 'required',
    },
    {
      fieldName: 'certNo',
      label: '证书编号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入证书编号',
      },
    },
    {
      fieldName: 'checkUserId',
      label: '检测人',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getSimpleUserList,
        labelField: 'nickname',
        placeholder: '请选择检测人',
        valueField: 'id',
      },
    },
    {
      fieldName: 'nextCheckDate',
      label: '下次检测日期',
      component: 'DatePicker',
      componentProps: {
        class: '!w-full',
        format: 'YYYY-MM-DD',
        placeholder: '请选择下次检测日期',
        valueFormat: 'YYYY-MM-DD',
      },
      help: '到期日,建议不填:将按「检测日期 + æ ¡å‡†å‘¨æœŸã€è‡ªåŠ¨è®¡ç®—å¹¶å›žå†™åˆ°å™¨å…·ï¼›ä¹Ÿå¯æ‰‹åŠ¨æŒ‡å®š',
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      formItemClass: 'col-span-3',
      componentProps: {
        placeholder: '请输入备注',
        rows: 3,
      },
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'meteringId',
      label: '计量器具',
      component: markRaw(DvMeteringSelect),
      componentProps: {
        allowClear: true,
        placeholder: '请选择计量器具',
      },
    },
    {
      fieldName: 'checkCode',
      label: '检测编号',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入检测编号',
      },
    },
    {
      fieldName: 'checkResult',
      label: '检测结论',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_DV_METERING_CHECK_RESULT, 'number'),
        placeholder: '请选择检测结论',
      },
    },
    {
      fieldName: 'checkDate',
      label: '检测日期',
      component: 'RangePicker',
      componentProps: {
        allowClear: true,
        format: 'YYYY-MM-DD',
        valueFormat: 'YYYY-MM-DD',
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesDvMeteringCheckApi.MeteringCheck>['columns'] {
  return [
    {
      field: 'meteringCode',
      title: '器具编码',
      minWidth: 140,
      slots: {
        default: 'meteringCode',
      },
    },
    { field: 'meteringName', title: '器具名称', minWidth: 150 },
    { field: 'checkCode', title: '检测编号', minWidth: 140 },
    {
      field: 'checkDate',
      title: '检测日期',
      width: 120,
      formatter: 'formatDate',
    },
    { field: 'checkOrg', title: '检测机构', minWidth: 140 },
    {
      field: 'checkResult',
      title: '检测结论',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_DV_METERING_CHECK_RESULT },
      },
    },
    { field: 'certNo', title: '证书编号', minWidth: 130 },
    { field: 'checkUserNickname', title: '检测人', minWidth: 100 },
    {
      field: 'nextCheckDate',
      title: '下次检测日期',
      width: 130,
      formatter: 'formatDate',
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 200,
      fixed: 'right',
      slots: {
        default: 'actions',
      },
    },
  ];
}
src/views/mes/dv/meteringcheck/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,158 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesDvMeteringCheckApi } from '#/api/mes/dv/meteringcheck';
import { Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteMeteringCheck,
  exportMeteringCheck,
  getMeteringCheckPage,
} from '#/api/mes/dv/meteringcheck';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** åˆ›å»ºè®¡é‡æ£€æµ‹è®°å½• */
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
/** æŸ¥çœ‹è®¡é‡æ£€æµ‹è®°å½• */
function handleDetail(row: MesDvMeteringCheckApi.MeteringCheck) {
  formModalApi.setData({ id: row.id, formType: 'detail' }).open();
}
/** ç¼–辑计量检测记录 */
function handleEdit(row: MesDvMeteringCheckApi.MeteringCheck) {
  formModalApi.setData({ id: row.id, formType: 'update' }).open();
}
/** åˆ é™¤è®¡é‡æ£€æµ‹è®°å½• */
async function handleDelete(row: MesDvMeteringCheckApi.MeteringCheck) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', [row.checkCode]),
    duration: 0,
  });
  try {
    await deleteMeteringCheck(row.id!);
    message.success($t('ui.actionMessage.deleteSuccess', [row.checkCode]));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** å¯¼å‡ºè®¡é‡æ£€æµ‹è®°å½• */
async function handleExport() {
  const data = await exportMeteringCheck(await gridApi.formApi.getValues());
  downloadFileFromBlobPart({ fileName: '计量检测记录.xls', source: data });
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) =>
          await getMeteringCheckPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          }),
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<MesDvMeteringCheckApi.MeteringCheck>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="计量检测记录列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['检测记录']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['mes:dv-metering-check:create'],
              onClick: handleCreate,
            },
            {
              label: $t('ui.actionTitle.export'),
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,
              auth: ['mes:dv-metering-check:export'],
              onClick: handleExport,
            },
          ]"
        />
      </template>
      <template #meteringCode="{ row }">
        <Button type="link" @click="handleDetail(row)">
          {{ row.meteringCode }}
        </Button>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['mes:dv-metering-check:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['mes:dv-metering-check:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.checkCode]),
                confirm: handleDelete.bind(null, row),
              },
            },
            {
              label: $t('common.detail'),
              type: 'link',
              icon: ACTION_ICON.VIEW,
              auth: ['mes:dv-metering-check:query'],
              onClick: handleDetail.bind(null, row),
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/mes/dv/meteringcheck/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,99 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { MesDvMeteringCheckApi } from '#/api/mes/dv/meteringcheck';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
  createMeteringCheck,
  getMeteringCheck,
  updateMeteringCheck,
} from '#/api/mes/dv/meteringcheck';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const formData = ref<MesDvMeteringCheckApi.MeteringCheck>();
const isDetail = computed(() => formType.value === 'detail');
const getTitle = computed(() => {
  if (formType.value === 'detail') {
    return '查看计量检测记录';
  }
  return formType.value === 'update' ? '修改计量检测记录' : '新增计量检测记录';
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    formItemClass: 'col-span-1',
    labelWidth: 110,
  },
  wrapperClass: 'grid-cols-3',
  layout: 'horizontal',
  schema: [],
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    if (isDetail.value) {
      await modalApi.close();
      return;
    }
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data = (await formApi.getValues()) as MesDvMeteringCheckApi.MeteringCheck;
    try {
      if (formType.value === 'create') {
        await createMeteringCheck(data);
      } else {
        await updateMeteringCheck(data);
      }
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
      await modalApi.close();
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      return;
    }
    const data = modalApi.getData<{ formType: FormType; id?: number }>();
    formType.value = data.formType;
    formApi.setState({ schema: useFormSchema() });
    formApi.setDisabled(formType.value === 'detail');
    modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
    if (!data?.id) {
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getMeteringCheck(data.id);
      await formApi.setValues(formData.value);
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-3/5">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/mes/md/item/data.ts
@@ -111,7 +111,7 @@
      component: 'RadioGroup',
      componentProps: {
        buttonStyle: 'solid',
        disabled: false,
        disabled: true,
        optionType: 'button',
        options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
      },
@@ -257,12 +257,7 @@
}
/** åˆ—表的字段 */
export function useGridColumns(
  onStatusChange?: (
    newStatus: number,
    row: MesMdItemApi.Item,
  ) => PromiseLike<boolean | undefined>,
): VxeTableGridOptions<MesMdItemApi.Item>['columns'] {
export function useGridColumns(): VxeTableGridOptions<MesMdItemApi.Item>['columns'] {
  return [
    {
      field: 'code',
@@ -315,15 +310,11 @@
    {
      field: 'status',
      title: '状态',
      width: 120,
      width: 100,
      align: 'center',
      cellRender: {
        attrs: { beforeChange: onStatusChange },
        name: 'CellSwitch',
        props: {
          checkedValue: CommonStatusEnum.ENABLE,
          unCheckedValue: CommonStatusEnum.DISABLE,
        },
        name: 'CellDict',
        props: { type: DICT_TYPE.COMMON_STATUS },
      },
    },
    {
src/views/mes/md/item/index.vue
@@ -6,12 +6,9 @@
  import { ref } from "vue";
  import {
    confirm,
    Page,
    useVbenModal,
  } from "@vben/common-ui";
  import { DICT_TYPE } from "@vben/constants";
  import { getDictLabel } from "@vben/hooks";
  import { downloadFileFromBlobPart } from "@vben/utils";
  import { Button, Card, message } from "ant-design-vue";
@@ -21,7 +18,6 @@
    deleteItem,
    exportItem,
    getItemPage,
    updateItemStatus,
  } from "#/api/mes/md/item";
  import { $t } from "#/locales";
  import { MdItemTypeTree } from "#/views/mes/md/item/type/components";
@@ -95,32 +91,12 @@
    handleRefresh();
  }
  /** æ›´æ–°ç‰©æ–™çŠ¶æ€ */
  async function handleStatusChange(
    newStatus: number,
    row: MesMdItemApi.Item
  ): Promise<boolean | undefined> {
    try {
      await confirm(
        `确认要将"${row.name}"物料切换为【${getDictLabel(
          DICT_TYPE.COMMON_STATUS,
          newStatus
        )}】吗?`
      );
    } catch {
      return false;
    }
    await updateItemStatus(row.id!, newStatus);
    message.success($t("ui.actionMessage.operationSuccess"));
    return true;
  }
  const [Grid, gridApi] = useVbenVxeGrid({
    formOptions: {
      schema: useGridFormSchema(),
    },
    gridOptions: {
      columns: useGridColumns(handleStatusChange),
      columns: useGridColumns(),
      height: "auto",
      keepSource: true,
      proxyConfig: {
@@ -159,13 +135,6 @@
        <Grid table-title="物料产品列表">
          <template #toolbar-tools>
            <TableAction :actions="[
                {
                  label: $t('ui.actionTitle.import'),
                  type: 'primary',
                  icon: ACTION_ICON.UPLOAD,
                  auth: ['mes:md-item:import'],
                  onClick: handleImport,
                },
                {
                  label: $t('ui.actionTitle.export'),
                  type: 'primary',
src/views/mes/pd/project/detail/index.vue
@@ -13,8 +13,8 @@
import {
  archivePdProject,
  auditPdProject,
  getPdProject,
  getPdProjectApproveProcessList,
  publishPdProject,
  uploadPdProject,
} from '#/api/mes/pd/project';
@@ -25,6 +25,7 @@
import Form from '../modules/form.vue';
import DocumentAuditList from '../modules/document-audit-list.vue';
import DocumentList from '../modules/document-list.vue';
import ProcessSelectModal from '../modules/process-select-modal.vue';
import WorkflowTimeline from '../modules/workflow-timeline.vue';
import TaskDraftBanner from '../modules/task-draft-banner.vue';
import { useDetailSchema } from './data';
@@ -50,6 +51,9 @@
const isPendingArchive = computed(
  () => project.value.status === MesPdProjectStatusEnum.PENDING_ARCHIVE,
);
const isApproving = computed(
  () => project.value.status === MesPdProjectStatusEnum.APPROVING,
);
const isArchived = computed(
  () => project.value.status === MesPdProjectStatusEnum.ARCHIVED,
);
@@ -70,6 +74,11 @@
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [ProcessModal, processModalApi] = useVbenModal({
  connectedComponent: ProcessSelectModal,
  destroyOnClose: true,
});
@@ -119,16 +128,38 @@
  }
}
async function handleAudit() {
  const hideLoading = message.loading({ content: '审核中…', duration: 0 });
/** æäº¤å®¡æ‰¹ - å…ˆèŽ·å–æµç¨‹åˆ—è¡¨ */
async function handleSubmit() {
  const hideLoading = message.loading({ content: '获取审批流程...', duration: 0 });
  try {
    await auditPdProject(projectId.value);
    message.success('审核通过,进入待归档状态');
    tabs.closeCurrentTab();
    router.back();
  } finally {
    const processList = await getPdProjectApproveProcessList();
    hideLoading();
    if (!processList || processList.length === 0) {
      message.warning('暂无可用审批流程,请先配置 BPM æµç¨‹');
      return;
    }
    // æ‰“开流程选择弹窗
    processModalApi.setData({ id: projectId.value, processList }).open();
  } catch {
    hideLoading();
  }
}
/** æŸ¥çœ‹å®¡æ‰¹è¯¦æƒ… */
function handleProcessDetail() {
  if (!project.value.processInstanceId) {
    message.warning('该项目尚未提交审批');
    return;
  }
  router.push({
    name: 'BpmProcessInstanceDetail',
    query: { id: project.value.processInstanceId },
  });
}
/** æäº¤å®¡æ‰¹æˆåŠŸåŽåˆ·æ–°è¯¦æƒ… */
function handleSubmitSuccess() {
  loadProjectDetail();
}
async function handleArchive() {
@@ -155,6 +186,7 @@
    :title="project.taskName || '设计任务详情'"
  >
    <FormModal @success="loadProjectDetail" />
    <ProcessModal @success="handleSubmitSuccess" />
    <template #extra>
      <TableAction
@@ -185,10 +217,16 @@
            onClick: handleUpload,
          },
          {
            label: '审核通过',
            label: '提交审批',
            type: 'primary',
            ifShow: isPendingAudit,
            onClick: handleAudit,
            onClick: handleSubmit,
          },
          {
            label: '查看审批',
            type: 'primary',
            ifShow: isApproving,
            onClick: handleProcessDetail,
          },
          {
            label: '归档',
src/views/mes/pd/project/index.vue
@@ -17,6 +17,7 @@
  deletePdProject,
  exportPdProject,
  getPdProjectApproveProcessList,
  getPdProjectPage,
  publishPdProject,
@@ -26,6 +27,7 @@
import { useGridColumns, useGridFormSchema } from './data';
import CreateForm from './modules/create-form.vue';
import Form from './modules/form.vue';
import ProcessSelectModal from './modules/process-select-modal.vue';
const router = useRouter();
@@ -36,6 +38,11 @@
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [ProcessModal, processModalApi] = useVbenModal({
  connectedComponent: ProcessSelectModal,
  destroyOnClose: true,
});
@@ -71,6 +78,38 @@
  } finally {
    hideLoading();
  }
}
/** æäº¤å®¡æ‰¹ - å…ˆèŽ·å–æµç¨‹åˆ—è¡¨ */
async function handleSubmit(row: MesPdProjectApi.Project) {
  const hideLoading = message.loading({
    content: '获取审批流程...',
    duration: 0,
  });
  try {
    const processList = await getPdProjectApproveProcessList();
    hideLoading();
    if (!processList || processList.length === 0) {
      message.warning('暂无可用审批流程,请先配置 BPM æµç¨‹');
      return;
    }
    // æ‰“开流程选择弹窗
    processModalApi.setData({ id: row.id, processList }).open();
  } catch {
    hideLoading();
  }
}
/** æŸ¥çœ‹å®¡æ‰¹è¯¦æƒ… */
function handleProcessDetail(row: MesPdProjectApi.Project) {
  if (!row.processInstanceId) {
    message.warning('该项目尚未提交审批');
    return;
  }
  router.push({
    name: 'BpmProcessInstanceDetail',
    query: { id: row.processInstanceId },
  });
}
async function handleArchive(row: MesPdProjectApi.Project) {
@@ -137,6 +176,7 @@
  <Page auto-content-height>
    <CreateFormModal @success="handleCreateSuccess" />
    <FormModal @success="handleRefresh" />
    <ProcessModal @success="handleRefresh" />
    <Grid table-title="设计任务">
      <template #toolbar-tools>
@@ -183,11 +223,18 @@
              onClick: () => handleDetail(row),
            },
            {
              label: '审核',
              label: '提交审批',
              type: 'link',
              auth: ['mes:pd-project:audit'],
              auth: ['mes:pd-project:update'],
              ifShow: row.status === MesPdProjectStatusEnum.PENDING_AUDIT,
              onClick: () => handleDetail(row),
              onClick: handleSubmit.bind(null, row),
            },
            {
              label: '查看审批',
              type: 'link',
              auth: ['mes:pd-project:update'],
              ifShow: row.status === MesPdProjectStatusEnum.APPROVING,
              onClick: handleProcessDetail.bind(null, row),
            },
            {
              label: '归档',
src/views/mes/pd/project/modules/process-select-modal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,75 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message, Select } from 'ant-design-vue';
import { submitPdProjectApproval } from '#/api/mes/pd/project';
interface ProcessDefinition {
  id: string;
  name: string;
  key: string;
}
const emit = defineEmits(['success']);
const projectId = ref<number>();
const processList = ref<ProcessDefinition[]>([]);
const selectedProcessKey = ref<string>('');
const processOptions = computed(() => {
  return processList.value.map((p) => ({
    label: p.name,
    value: p.key,
  }));
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    if (!selectedProcessKey.value) {
      message.warning('请选择审批流程');
      return;
    }
    modalApi.lock();
    try {
      await submitPdProjectApproval(projectId.value!, selectedProcessKey.value);
      await modalApi.close();
      emit('success');
      message.success('提交审批成功');
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      projectId.value = undefined;
      selectedProcessKey.value = '';
      processList.value = [];
      return;
    }
    const data = modalApi.getData<{ id: number; processList: ProcessDefinition[] }>();
    projectId.value = data.id;
    processList.value = data.processList || [];
    // é»˜è®¤é€‰æ‹©ç¬¬ä¸€ä¸ªæµç¨‹
    if (processList.value.length > 0) {
      selectedProcessKey.value = processList.value[0].key;
    }
  },
});
</script>
<template>
  <Modal title="选择审批流程" class="w-1/3">
    <div class="mb-4">
      <span class="mr-2">审批流程:</span>
      <Select
        v-model:value="selectedProcessKey"
        :options="processOptions"
        style="width: 200px"
        placeholder="请选择审批流程"
      />
    </div>
  </Modal>
</template>
src/views/mes/pd/project/modules/workflow-timeline.vue
@@ -3,22 +3,35 @@
import { Steps } from 'ant-design-vue';
const STEPS = ['待发布', '待上传', '待审核', '待归档', '已归档'] as const;
import { MesPdProjectStatusEnum } from '#/packages/constants/src';
const STEPS = ['待发布', '待上传', '待审核', '审批中', '待归档', '已归档'] as const;
// çŠ¶æ€å€¼ â†’ æ—¶é—´çº¿æ­¥éª¤ç´¢å¼•(审批中=5 æ’在待审核之后)
const STATUS_ORDER = [
  MesPdProjectStatusEnum.PENDING_PUBLISH,
  MesPdProjectStatusEnum.PENDING_UPLOAD,
  MesPdProjectStatusEnum.PENDING_AUDIT,
  MesPdProjectStatusEnum.APPROVING,
  MesPdProjectStatusEnum.PENDING_ARCHIVE,
  MesPdProjectStatusEnum.ARCHIVED,
];
const props = defineProps<{
  /** é¡¹ç›®å½“前状态 (0-4) */
  /** é¡¹ç›®å½“前状态 */
  status: number;
}>();
const current = computed(() => {
  // çŠ¶æ€å€¼å³å½“å‰å·²å®Œæˆåˆ°ç¬¬å‡ æ­¥ï¼š0=第一步进行中,4=全部完成
  return props.status;
  // æ ¹æ®çŠ¶æ€å€¼æ˜ å°„åˆ°æ—¶é—´çº¿æ­¥éª¤ç´¢å¼•
  const index = STATUS_ORDER.indexOf(props.status);
  return index >= 0 ? index : 0;
});
const nodes = computed(() =>
  STEPS.map((name, index) => ({
    title: name,
    status: index < props.status ? 'finish' : index === props.status ? 'process' : 'wait',
    status: index < current.value ? 'finish' : index === current.value ? 'process' : 'wait',
  })),
);
</script>
src/views/mes/qc/batchtrace/data.ts
@@ -5,6 +5,8 @@
import { markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { MdClientSelect } from '#/views/mes/md/client/components';
import { MdItemSelect } from '#/views/mes/md/item/components';
import { MdVendorSelect } from '#/views/mes/md/vendor/components';
@@ -227,3 +229,176 @@
    },
  ];
}
/** é‡‡è´­æ¥æºåˆ—表的字段 */
export function usePurchaseSourceColumns(): VxeTableGridOptions<MesWmBatchApi.PurchaseSource>['columns'] {
  return [
    {
      field: 'receiptCode',
      title: '采购入库单号',
      minWidth: 160,
    },
    {
      field: 'receiptDate',
      title: '入库日期',
      minWidth: 160,
      formatter: 'formatDateTime',
    },
    {
      field: 'purchaseOrderCode',
      title: '采购订单编号',
      minWidth: 160,
    },
    {
      field: 'vendorCode',
      title: '供应商编码',
      minWidth: 140,
    },
    {
      field: 'vendorName',
      title: '供应商名称',
      minWidth: 140,
    },
    {
      field: 'itemCode',
      title: '物料编码',
      minWidth: 140,
    },
    {
      field: 'itemName',
      title: '物料名称',
      minWidth: 160,
    },
    {
      field: 'iqcCode',
      title: 'IQC æ£€éªŒå•号',
      minWidth: 160,
    },
    {
      field: 'checkResult',
      title: 'IQC æ£€éªŒç»“论',
      width: 110,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_QC_CHECK_RESULT },
      },
    },
    {
      field: 'receivedQuantity',
      title: '入库数量',
      width: 100,
    },
  ];
}
/** é”€å”®åŽ»å‘åˆ—è¡¨çš„å­—æ®µ */
export function useSalesTargetColumns(): VxeTableGridOptions<MesWmBatchApi.SalesTarget>['columns'] {
  return [
    {
      field: 'salesCode',
      title: '销售出库单号',
      minWidth: 160,
    },
    {
      field: 'salesDate',
      title: '出库日期',
      minWidth: 160,
      formatter: 'formatDateTime',
    },
    {
      field: 'salesOrderCode',
      title: '销售订单编号',
      minWidth: 160,
    },
    {
      field: 'clientName',
      title: '客户名称',
      minWidth: 140,
    },
    {
      field: 'itemCode',
      title: '物料编码',
      minWidth: 140,
    },
    {
      field: 'itemName',
      title: '物料名称',
      minWidth: 160,
    },
    {
      field: 'oqcCode',
      title: 'OQC æ£€éªŒå•号',
      minWidth: 160,
    },
    {
      field: 'checkResult',
      title: 'OQC æ£€éªŒç»“论',
      width: 110,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_QC_CHECK_RESULT },
      },
    },
    {
      field: 'quantity',
      title: '出库数量',
      width: 100,
    },
    {
      field: 'qualityStatus',
      title: '质量状态',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_WM_QUALITY_STATUS },
      },
    },
  ];
}
/** å·¥åºæµè½¬åˆ—表的字段 */
export function useProcessTraceColumns(): VxeTableGridOptions<MesWmBatchApi.ProcessTrace>['columns'] {
  return [
    {
      field: 'eventType',
      title: '事件类型',
      width: 90,
      slots: { default: 'eventType' },
    },
    {
      field: 'workOrderCode',
      title: '生产工单号',
      minWidth: 160,
    },
    {
      field: 'processCode',
      title: '工序编码',
      minWidth: 120,
    },
    {
      field: 'processName',
      title: '工序名称',
      minWidth: 140,
    },
    {
      field: 'quantity',
      title: '数量',
      width: 100,
    },
    {
      field: 'qualityStatus',
      title: '质量状态',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_WM_QUALITY_STATUS },
      },
    },
    {
      field: 'eventTime',
      title: '事件时间',
      minWidth: 160,
      formatter: 'formatDateTime',
    },
  ];
}
src/views/mes/qc/batchtrace/index.vue
@@ -4,8 +4,10 @@
import { Page, useVbenModal } from '@vben/common-ui';
import { Input, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getBatchPage } from '#/api/mes/wm/batch';
import { getBatchBySn, getBatchPage } from '#/api/mes/wm/batch';
import { useGridColumns, useGridFormSchema } from './data';
import TraceDetail from './modules/trace-detail.vue';
@@ -18,6 +20,19 @@
/** æ‰“开批次追溯详情 */
function handleTrace(row: MesWmBatchApi.Batch) {
  detailModalApi.setData(row).open();
}
/** æ ¹æ® SN åæŸ¥æ‰¹æ¬¡å¹¶æ‰“开详情 */
async function handleSearchBySn(sn: string) {
  if (!sn) {
    return;
  }
  const batch = await getBatchBySn(sn.trim());
  if (batch) {
    handleTrace(batch);
  } else {
    message.warning(`未找到 SN「${sn}」对应的批次`);
  }
}
const [Grid] = useVbenVxeGrid({
@@ -54,6 +69,15 @@
<template>
  <Page auto-content-height><DetailModal />
    <div class="mb-3 w-80">
      <Input.Search
        placeholder="输入 SN åºåˆ—号反查批次"
        enter-button="反查"
        allow-clear
        @search="handleSearchBySn"
      />
    </div>
    <Grid table-title="批次追溯列表">
      <template #actions="{ row }">
        <TableAction
src/views/mes/qc/batchtrace/modules/process-trace-list.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmBatchApi } from '#/api/mes/wm/batch';
import { watch } from 'vue';
import { Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getProcessTraceList } from '#/api/mes/wm/batch';
import { useProcessTraceColumns } from '../data';
const props = defineProps<{
  batchCode?: string;
}>();
const [Grid, gridApi] = useVbenVxeGrid({
  gridOptions: {
    columns: useProcessTraceColumns(),
    minHeight: 240,
    pagerConfig: {
      enabled: false,
    },
    rowConfig: {
      isHover: true,
    },
    showOverflow: true,
    toolbarConfig: {
      enabled: false,
    },
  } as VxeTableGridOptions<MesWmBatchApi.ProcessTrace>,
});
/** åŠ è½½å·¥åºæµè½¬åˆ—è¡¨ */
async function getList() {
  if (!props.batchCode) {
    await gridApi.grid?.loadData([]);
    return;
  }
  gridApi.setLoading(true);
  try {
    const list = await getProcessTraceList(props.batchCode);
    await gridApi.grid?.loadData(list);
  } finally {
    gridApi.setLoading(false);
  }
}
watch(
  () => props.batchCode,
  () => {
    getList();
  },
  { immediate: true },
);
</script>
<template>
  <Grid class="w-full">
    <template #eventType="{ row }">
      <Tag v-if="row.eventType === 'OUT'" color="success">产出</Tag>
      <Tag v-else color="processing">投入</Tag>
    </template>
  </Grid>
</template>
src/views/mes/qc/batchtrace/modules/purchase-source-list.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmBatchApi } from '#/api/mes/wm/batch';
import { watch } from 'vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getPurchaseSourceList } from '#/api/mes/wm/batch';
import { usePurchaseSourceColumns } from '../data';
const props = defineProps<{
  batchCode?: string;
}>();
const [Grid, gridApi] = useVbenVxeGrid({
  gridOptions: {
    columns: usePurchaseSourceColumns(),
    minHeight: 240,
    pagerConfig: {
      enabled: false,
    },
    rowConfig: {
      isHover: true,
    },
    showOverflow: true,
    toolbarConfig: {
      enabled: false,
    },
  } as VxeTableGridOptions<MesWmBatchApi.PurchaseSource>,
});
/** åŠ è½½é‡‡è´­æ¥æºåˆ—è¡¨ */
async function getList() {
  if (!props.batchCode) {
    await gridApi.grid?.loadData([]);
    return;
  }
  gridApi.setLoading(true);
  try {
    const list = await getPurchaseSourceList(props.batchCode);
    await gridApi.grid?.loadData(list);
  } finally {
    gridApi.setLoading(false);
  }
}
watch(
  () => props.batchCode,
  () => {
    getList();
  },
  { immediate: true },
);
</script>
<template>
  <Grid class="w-full" />
</template>
src/views/mes/qc/batchtrace/modules/sales-target-list.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmBatchApi } from '#/api/mes/wm/batch';
import { watch } from 'vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getSalesTargetList } from '#/api/mes/wm/batch';
import { useSalesTargetColumns } from '../data';
const props = defineProps<{
  batchCode?: string;
}>();
const [Grid, gridApi] = useVbenVxeGrid({
  gridOptions: {
    columns: useSalesTargetColumns(),
    minHeight: 240,
    pagerConfig: {
      enabled: false,
    },
    rowConfig: {
      isHover: true,
    },
    showOverflow: true,
    toolbarConfig: {
      enabled: false,
    },
  } as VxeTableGridOptions<MesWmBatchApi.SalesTarget>,
});
/** åŠ è½½é”€å”®åŽ»å‘åˆ—è¡¨ */
async function getList() {
  if (!props.batchCode) {
    await gridApi.grid?.loadData([]);
    return;
  }
  gridApi.setLoading(true);
  try {
    const list = await getSalesTargetList(props.batchCode);
    await gridApi.grid?.loadData(list);
  } finally {
    gridApi.setLoading(false);
  }
}
watch(
  () => props.batchCode,
  () => {
    getList();
  },
  { immediate: true },
);
</script>
<template>
  <Grid class="w-full" />
</template>
src/views/mes/qc/batchtrace/modules/trace-detail.vue
@@ -5,15 +5,24 @@
import { useVbenModal } from '@vben/common-ui';
import { Tabs } from 'ant-design-vue';
import { Tabs, Tooltip } from 'ant-design-vue';
import { useDescription } from '#/components/description';
import { useDetailSchema } from '../data';
import ProcessTraceList from './process-trace-list.vue';
import PurchaseSourceList from './purchase-source-list.vue';
import SalesTargetList from './sales-target-list.vue';
import TraceList from './trace-list.vue';
const detailData = ref<MesWmBatchApi.Batch>(); // å½“前批次详情
const subTabsName = ref<'backward' | 'forward'>('forward'); // å½“前激活的追溯方向
const subTabsName = ref<
  | 'backward'
  | 'forward'
  | 'purchase'
  | 'sales'
  | 'process'
>('forward'); // å½“前激活的追溯页签
const [Descriptions] = useDescription({
  bordered: true,
@@ -47,12 +56,46 @@
      class="mx-4 mt-4"
      type="card"
    >
      <Tabs.TabPane key="forward" tab="向前追溯">
      <Tabs.TabPane key="forward">
        <template #tab>
          <Tooltip title="顺着生产流往下游查:该批次被哪些工单消耗后,产出成了哪些成品批次">
            <span>向前追溯</span>
          </Tooltip>
        </template>
        <TraceList :batch-code="detailData.code" direction="forward" />
      </Tabs.TabPane>
      <Tabs.TabPane key="backward" tab="向后追溯">
      <Tabs.TabPane key="backward">
        <template #tab>
          <Tooltip title="逆着生产流往上游查:生产该批次时,消耗了哪些原料批次">
            <span>向后追溯</span>
          </Tooltip>
        </template>
        <TraceList :batch-code="detailData.code" direction="backward" />
      </Tabs.TabPane>
      <Tabs.TabPane key="purchase">
        <template #tab>
          <Tooltip title="原料侧来源:该批次对应的采购入库单与 IQC æ¥æ–™æ£€éªŒè®°å½•">
            <span>采购来源</span>
          </Tooltip>
        </template>
        <PurchaseSourceList :batch-code="detailData.code" />
      </Tabs.TabPane>
      <Tabs.TabPane key="sales">
        <template #tab>
          <Tooltip title="成品侧去向:该批次对应的销售出库单与 OQC å‡ºåŽ‚æ£€éªŒè®°å½•">
            <span>销售去向</span>
          </Tooltip>
        </template>
        <SalesTargetList :batch-code="detailData.code" />
      </Tabs.TabPane>
      <Tabs.TabPane key="process">
        <template #tab>
          <Tooltip title="各工序的投入/产出事件:该批次在哪些工序被投入或产出,及数量和质量状态">
            <span>工序流转</span>
          </Tooltip>
        </template>
        <ProcessTraceList :batch-code="detailData.code" />
      </Tabs.TabPane>
    </Tabs>
  </Modal>
</template>
src/views/srm/statistics/index.vue
@@ -96,7 +96,14 @@
        </div>
        <div class="rounded-lg bg-purple-50 p-6 text-center shadow-sm">
          <div class="text-3xl font-bold text-purple-600">{{ stats.savedAmount?.toLocaleString() ?? 0 }}</div>
          <div class="mt-2 text-gray-500">节约金额(元)</div>
          <div class="mt-2 flex items-center justify-center gap-1 text-gray-500">
            èŠ‚çº¦é‡‘é¢(元)
            <a-tooltip title="节约金额 = æ‹›æ ‡æ€»é‡‘额(预算金额) âˆ’ å®šæ ‡æ€»é‡‘额(中标金额)">
              <span
                class="inline-flex h-3.5 w-3.5 cursor-help items-center justify-center rounded-full bg-gray-400 text-[10px] leading-none text-white"
              >?</span>
            </a-tooltip>
          </div>
        </div>
        <div class="rounded-lg bg-teal-50 p-6 text-center shadow-sm">
          <div class="text-3xl font-bold text-teal-600">{{ stats.winRate ?? 0 }}%</div>
src/views/srm/supplierCertificate/data.ts
@@ -83,6 +83,19 @@
      rules: 'required',
    },
    {
      fieldName: 'blobIds',
      label: '证书附件',
      component: 'FileUpload',
      componentProps: {
        valueKey: 'id',
        maxNumber: 10,
        multiple: true,
        accept: ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx'],
        helpText: '支持jpg、jpeg、png、pdf、doc、docx格式,单个文件不超过2MB',
      },
      formItemClass: 'md:col-span-2',
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
@@ -144,6 +157,13 @@
    { field: 'validEndDate', title: '到期日期', minWidth: 120 },
    { field: 'createTime', title: '创建时间', minWidth: 160 },
    {
      field: 'attachmentList',
      title: '附件',
      minWidth: 100,
      fixed: 'right',
      slots: { default: 'attachment' },
    },
    {
      title: '操作',
      width: 130,
      fixed: 'right',
src/views/srm/supplierCertificate/index.vue
@@ -4,7 +4,7 @@
import { Page, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
@@ -112,6 +112,21 @@
          ]"
        />
      </template>
      <template #attachment="{ row }">
        <div v-if="row.attachmentList && row.attachmentList.length" class="flex flex-col gap-1">
          <a
            v-for="file in row.attachmentList"
            :key="file.id"
            :href="file.url"
            target="_blank"
            rel="noopener noreferrer"
            class="text-blue-500 hover:underline"
          >
            {{ file.name || '附件-' + file.id }}
          </a>
        </div>
        <Tag v-else color="default">--</Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
src/views/srm/supplierCertificate/modules/form.vue
@@ -65,6 +65,17 @@
    modalApi.lock();
    try {
      formData.value = await getCertificate(data.id);
      if (formData.value?.attachmentList?.length) {
        // è½¬ä¸ºä¸Šä¼ ç»„件的文件对象;提交时 formApi ä¼šæŒ‰ valueKey æå–附件 ID
        (formData.value as Record<string, unknown>).blobIds =
          formData.value.attachmentList.map((item) => ({
            uid: String(item.id),
            name: item.name || '',
            url: item.url || '',
            status: 'done',
            id: item.id,
          }));
      }
      await formApi.setValues(formData.value);
    } finally {
      modalApi.unlock();
src/views/srm/tender/data.ts
@@ -180,6 +180,19 @@
      componentProps: { placeholder: '请输入资质要求', rows: 3 },
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'blobIds',
      label: '资质要求附件',
      component: 'FileUpload',
      componentProps: {
        valueKey: 'id',
        maxNumber: 10,
        multiple: true,
        accept: ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx'],
        helpText: '支持jpg、jpeg、png、pdf、doc、docx格式,单个文件不超过2MB',
      },
      formItemClass: 'col-span-2',
    },
  ];
}
src/views/srm/tender/detail.vue
@@ -19,7 +19,7 @@
  createAward, deleteAward, getAward, getAwardByProject, approveAward, generatePurchaseOrder,
  confirmTenderProject,
} from '#/api/srm/tender';
import { getSupplierSimpleList } from '#/api/srm/supplier';
import { getSupplierList } from '#/api/srm/supplier';
import { getItemSimpleList } from '#/api/mdm/item';
import type { MdmItemApi } from '#/api/mdm/item';
import {
@@ -51,7 +51,7 @@
onMounted(async () => {
  const [items, suppliers] = await Promise.all([
    getItemSimpleList(),
    getSupplierSimpleList(),
    getSupplierList(),
  ]);
  mdmItemList.value = items;
  supplierList.value = suppliers;
@@ -363,6 +363,7 @@
const typeLabel = computed(() => getLabel(TENDER_TYPE_CELLTAG.labels, project.value?.tenderType));
const isDraft = computed(() => project.value?.tenderStatus === TENDER_STATUS.DRAFT);
const isPublished = computed(() => project.value?.tenderStatus === TENDER_STATUS.PUBLISHED);
const isBidding = computed(() => project.value?.tenderStatus === TENDER_STATUS.BIDDING);
const isPublishedOrBidding = computed(() => {
  const s = project.value?.tenderStatus;
  return s === TENDER_STATUS.PUBLISHED || s === TENDER_STATUS.BIDDING;
@@ -438,7 +439,7 @@
const bidColumns = [
  { title: '投标编号', dataIndex: 'bidNo', width: 140 },
  { title: '供应商', dataIndex: 'supplierName', width: 200 },
  { title: '投标金额', dataIndex: 'bidTotalAmount', width: 120 },
  { title: '投标总金额', dataIndex: 'bidTotalAmount', width: 120 },
  { title: '投标时间', dataIndex: 'bidTime', width: 160 },
  { title: '状态', dataIndex: 'bidStatus', width: 100, customRender: ({ text }: any) => BID_STATUS_MAP[text] || text },
  { title: '操作', key: 'actions', width: 240 },
@@ -446,7 +447,7 @@
const quoteColumns = [
  { title: '物料', dataIndex: 'materialName', width: 120 },
  { title: '报价价格', dataIndex: 'quotePrice', width: 100 },
  { title: '报价单价', dataIndex: 'quotePrice', width: 100 },
  { title: '税率(%)', dataIndex: 'taxRate', width: 80 },
  { title: '交期(天)', dataIndex: 'deliveryCycle', width: 80 },
  { title: '付款条件', dataIndex: 'paymentTerms', width: 120 },
@@ -537,14 +538,32 @@
          </div>
          <!-- æ‹›æ ‡è¯´æ˜Ž / èµ„质要求 -->
          <template v-if="project.tenderDesc || project.qualificationRequirements">
          <template
            v-if="project.tenderDesc || project.qualificationRequirements || (project.attachmentList && project.attachmentList.length)"
          >
            <a-divider class="!my-2" />
            <a-collapse :bordered="false" ghost expand-icon-position="end">
              <a-collapse-panel v-if="project.tenderDesc" key="desc" header="招标说明">
                <p class="text-gray-500 text-xs whitespace-pre-wrap">{{ project.tenderDesc }}</p>
              </a-collapse-panel>
              <a-collapse-panel v-if="project.qualificationRequirements" key="req" header="资质要求">
                <p class="text-gray-500 text-xs whitespace-pre-wrap">{{ project.qualificationRequirements }}</p>
              <a-collapse-panel
                v-if="project.qualificationRequirements || (project.attachmentList && project.attachmentList.length)"
                key="req"
                header="资质要求"
              >
                <p v-if="project.qualificationRequirements" class="text-gray-500 text-xs whitespace-pre-wrap">{{ project.qualificationRequirements }}</p>
                <div v-if="project.attachmentList && project.attachmentList.length" class="mt-2 flex flex-col gap-1">
                  <a
                    v-for="file in project.attachmentList"
                    :key="file.id"
                    :href="file.url"
                    target="_blank"
                    rel="noopener noreferrer"
                    class="text-blue-500 text-xs hover:underline"
                  >
                    {{ file.name || '附件-' + file.id }}
                  </a>
                </div>
              </a-collapse-panel>
            </a-collapse>
          </template>
@@ -594,7 +613,7 @@
              </template>
              <div class="flex items-center justify-between mb-3">
                <span class="text-xs text-gray-400">共 <b class="text-gray-600">{{ bidCount }}</b> ä¸ªæŠ•æ ‡</span>
                <a-button v-if="isPublishedOrBidding || isDraft || isPriceComparing" type="primary" size="small" @click="bidForm = { tenderProjectId: projectId, supplierId: undefined!, bidNo: '' }; bidFormVisible = true">
                <a-button v-if="isBidding || isDraft || isPriceComparing" type="primary" size="small" @click="bidForm = { tenderProjectId: projectId, supplierId: undefined!, bidNo: '' }; bidFormVisible = true">
                  <template #icon><IconifyIcon icon="ant-design:plus-outlined" /></template>
                  æ–°å¢žæŠ•æ ‡
                </a-button>
@@ -888,7 +907,7 @@
          </a-form-item>
          <a-row :gutter="12">
            <a-col :span="8">
              <a-form-item label="报价价格" required><a-input-number v-model:value="quoteForm.quotePrice" :min="0" class="!w-full" placeholder="报价"><template #addonAfter>元</template></a-input-number></a-form-item>
              <a-form-item label="报价单价" required><a-input-number v-model:value="quoteForm.quotePrice" :min="0" class="!w-full" placeholder="报价"><template #addonAfter>元</template></a-input-number></a-form-item>
            </a-col>
            <a-col :span="8">
              <a-form-item label="税率(%)"><a-input-number v-model:value="quoteForm.taxRate" :min="0" :max="100" class="!w-full" placeholder="如 13" /></a-form-item>
src/views/srm/tender/modules/form.vue
@@ -58,6 +58,7 @@
          id: data.id!,
          tenderDesc: data.tenderDesc,
          qualificationRequirements: data.qualificationRequirements,
          blobIds: data.blobIds,
        });
      } else {
        await (formData.value?.id
@@ -92,6 +93,17 @@
    modalApi.lock();
    try {
      formData.value = await getTenderProject(data.id);
      if (formData.value?.attachmentList?.length) {
        // è½¬ä¸ºä¸Šä¼ ç»„件的文件对象;提交时 formApi ä¼šæŒ‰ valueKey æå–附件 ID
        (formData.value as Record<string, unknown>).blobIds =
          formData.value.attachmentList.map((item) => ({
            uid: String(item.id),
            name: item.name || '',
            url: item.url || '',
            status: 'done',
            id: item.id,
          }));
      }
      const partial = formData.value.tenderStatus !== undefined
        && formData.value.tenderStatus !== TENDER_STATUS.DRAFT;
      await formApi.setValues({ ...formData.value, _partial: partial || undefined });
vite.config.ts
@@ -16,6 +16,7 @@
  const defineVars: Record<string, string> = {
    __APP_LOGO__: JSON.stringify(appLogo),
    __APP_FAVICON__: JSON.stringify(appFavicon),
    __APP_NAME__: JSON.stringify(company?.name ?? ''),
  };
  if (!isDev && appApiUrl) {
    defineVars['import.meta.env.VITE_GLOB_API_URL'] = JSON.stringify(appApiUrl);
@@ -75,7 +76,7 @@
        proxy: {
          "/admin-api": {
            changeOrigin: true,
            target: "http://192.168.0.10:48080",
            target: "http://36.213.180.217:9501",
            ws: true,
          },
        },