From 77b083d347eb8e75eb29a4146238b3f7304489f6 Mon Sep 17 00:00:00 2001
From: hsy <1029150275@qq.com>
Date: 星期一, 17 八月 2026 09:54:07 +0800
Subject: [PATCH] Merge branch 'refs/heads/dev_pro2.0' into fix/hsy/20260812

---
 src/api/mes/wm/batch/index.ts                                |   81 
 src/views/mes/qc/batchtrace/modules/purchase-source-list.vue |   59 
 src/views/mes/dv/metering/components/select.vue              |  142 +
 src/views/srm/statistics/index.vue                           |    9 
 src/views/mes/pd/project/index.vue                           |   53 
 src/views/srm/supplierCertificate/data.ts                    |   20 
 src/views/mes/pd/project/detail/index.vue                    |   58 
 pnpm-lock.yaml                                               |   66 
 src/views/mes/dv/metering/index.vue                          |  176 ++
 src/api/mes/pd/project/index.ts                              |   17 
 src/api/srm/supplierCertificate/index.ts                     |    3 
 src/views/erp/purchase/request/detail/index.vue              |  181 ++
 src/views/mes/qc/batchtrace/modules/trace-detail.vue         |   51 
 src/views/mes/dv/metering/components/select-dialog.vue       |  213 ++
 src/views/mes/qc/batchtrace/index.vue                        |   26 
 src/views/mes/pd/project/modules/process-select-modal.vue    |   75 
 src/api/bpm/businessAttach/index.ts                          |   52 
 src/views/bpm/businessAttach/modules/form.vue                |   88 +
 src/views/im/home/index.vue                                  |    2 
 src/packages/effects/layouts/src/basic/layout.vue            |    4 
 src/views/bpm/businessAttach/data.ts                         |  163 +
 src/views/srm/tender/modules/form.vue                        |   12 
 src/api/mes/dv/meteringcheck/index.ts                        |   60 
 src/preferences.ts                                           |    5 
 src/views/erp/purchase/invoice/index.vue                     |   42 
 src/views/hrm/leave/detail/approve/index.vue                 |   93 +
 src/views/erp/finance/payment/detail/approve/index.vue       |  172 ++
 src/views/mes/dv/metering/data.ts                            |  437 +++++
 src/views/mes/qc/batchtrace/data.ts                          |  175 ++
 src/views/erp/purchase/invoice/modules/form.vue              |   10 
 src/views/mes/dv/meteringcheck/modules/form.vue              |   99 +
 src/views/erp/purchase/invoice/data.ts                       |   22 
 src/views/crm/invoice/detail/approve/index.vue               |  105 +
 src/api/mes/dv/metering/index.ts                             |   72 
 src/views/crm/receivable/detail/approve/index.vue            |  112 +
 src/views/mes/qc/batchtrace/modules/process-trace-list.vue   |   66 
 .env.production                                              |    4 
 src/views/mes/qc/batchtrace/modules/sales-target-list.vue    |   59 
 src/packages/constants/src/dict-enum.ts                      |    3 
 pnpm-workspace.yaml                                          |    2 
 src/views/bpm/model/index.vue                                |    7 
 src/views/crm/receivable/data.ts                             |   10 
 src/views/mes/dv/metering/modules/form.vue                   |   99 +
 src/views/mes/dv/metering/components/index.ts                |    2 
 src/components/form-create/helpers.ts                        |    9 
 company.config.ts                                            |   14 
 src/views/srm/tender/detail.vue                              |   39 
 src/views/hrm/attendance/exception/detail/approve/index.vue  |   82 
 public/logos/YZFXLogo.png                                    |    0 
 src/views/crm/receivable/components/data.ts                  |   10 
 vite.config.ts                                               |    3 
 src/views/hrm/resignation/detail/approve/index.vue           |   95 +
 src/views/erp/purchase/invoice/detail/approve/index.vue      |   90 +
 src/views/srm/tender/data.ts                                 |   13 
 src/views/mes/dv/meteringcheck/index.vue                     |  158 +
 src/api/srm/tender/index.ts                                  |    4 
 public/logos/YZFXLogo.ico                                    |    0 
 src/views/srm/supplierCertificate/modules/form.vue           |   11 
 src/packages/constants/src/biz-mes-enum.ts                   |   23 
 src/views/erp/purchase/order/detail/index.vue                |  134 +
 src/bootstrap.ts                                             |    3 
 src/main.ts                                                  |    8 
 deploy.bat                                                   |  339 ++--
 src/views/srm/supplierCertificate/index.vue                  |   17 
 src/api/srm/supplier/index.ts                                |    5 
 src/views/crm/contract/detail/approve/index.vue              |  190 ++
 src/views/mes/md/item/data.ts                                |   19 
 src/views/mes/md/item/index.vue                              |   33 
 src/views/hrm/transfer/detail/approve/index.vue              |   90 +
 src/api/mes/md/item/index.ts                                 |    7 
 package.json                                                 |    4 
 src/api/erp/purchase/invoice/index.ts                        |   17 
 src/types/app.d.ts                                           |    1 
 src/layouts/auth.vue                                         |    2 
 src/views/bpm/businessAttach/index.vue                       |  129 +
 src/views/mes/pd/project/modules/workflow-timeline.vue       |   23 
 src/views/mes/dv/meteringcheck/data.ts                       |  218 ++
 77 files changed, 4,612 insertions(+), 385 deletions(-)

diff --git a/.env.production b/.env.production
index ec0c2aa..cd639f5 100644
--- a/.env.production
+++ b/.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
 # 鏂囦欢涓婁紶绫诲瀷锛歴erver - 鍚庣涓婁紶锛� client - 鍓嶇鐩磋繛涓婁紶锛屼粎鏀寔S3鏈嶅姟
 VITE_UPLOAD_TYPE=server
 
diff --git a/company.config.ts b/company.config.ts
index f4436c3..1246f58 100644
--- a/company.config.ts
+++ b/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',
   },
 };
diff --git a/deploy.bat b/deploy.bat
index 52e47e9..401195f 100644
--- a/deploy.bat
+++ b/deploy.bat
@@ -1,170 +1,169 @@
-@echo off
-chcp 65001 >nul
-setlocal enabledelayedexpansion
-
-:: ================= Config =================
-set "COMPANY=TEST"
-set "USER=root"
-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 "SSH_OPTS=-o StrictHostKeyChecking=no -o ConnectTimeout=10"
-:: ===========================================
-
-echo ==========================================
-echo        Vue Dist Auto Deploy Script
-echo ==========================================
-echo.
-
-:: Check if sshpass is available
-set "SSHPASS_DIR=%~dp0bin"
-if exist "!SSHPASS_DIR!\sshpass.exe" (
-    set "PATH=!SSHPASS_DIR!;%PATH%"
-)
-
-where sshpass >nul 2>&1
-if %errorlevel% equ 0 goto :check_node
-
-echo [WARN] sshpass not found, trying to install...
-echo.
-
-where bash >nul 2>&1
-if %errorlevel% equ 0 (
-    echo [INFO] Git Bash detected, trying pacman install...
-    bash -c "pacman -S --noconfirm sshpass" 2>nul
-    if !errorlevel! equ 0 (
-        echo [OK] sshpass installed via pacman!
-        goto :check_node
-    )
-)
-
-if not exist "!SSHPASS_DIR!" mkdir "!SSHPASS_DIR!"
-
-if exist "!SSHPASS_DIR!\sshpass.exe" (
-    set "PATH=!SSHPASS_DIR!;%PATH%"
-    echo [OK] sshpass found at: !SSHPASS_DIR!
-    goto :check_node
-)
-
-echo [INFO] Downloading sshpass...
-powershell -NoProfile -Command "$url='https://github.com/xhcoding/sshpass-win32/releases/download/v1.0.3/sshpass.exe'; $out='!SSHPASS_DIR!\sshpass.exe'; Write-Host \"Downloading to: $out\"; try { Invoke-WebRequest -Uri $url -OutFile $out -UseBasicParsing; exit 0 } catch { Write-Host $_.Exception.Message; exit 1 }"
-if !errorlevel! neq 0 (
-    echo [ERROR] Failed to download sshpass!
-    echo [HINT] Please install sshpass manually or use Git Bash: pacman -S sshpass
-    goto :error_exit
-)
-
-set "PATH=!SSHPASS_DIR!;%PATH%"
-echo [OK] sshpass downloaded to: !SSHPASS_DIR!
-echo.
-
-:check_node
-:: Check if pnpm is available
-where pnpm >nul 2>&1
-if %errorlevel% neq 0 (
-    echo [ERROR] pnpm not found! Please install pnpm first.
-    echo [HINT] npm install -g pnpm
-    goto :error_exit
-)
-
-:: Check if node_modules exists
-if not exist "node_modules" (
-    echo [INFO] node_modules not found, running pnpm install...
-    echo.
-    call pnpm install
-    if !errorlevel! neq 0 (
-        echo [ERROR] pnpm install failed!
-        goto :error_exit
-    )
-    echo.
-)
-
-:: Check if dist exists and ask user
-if exist "%DIST_DIR%" (
-    echo [WARN] dist directory 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
-        goto :upload
-    )
-)
-
-:: Build the project
-echo [STEP 1/3] Building project for %COMPANY%...
-echo [INFO] Running: pnpm run build
-echo.
-set "COMPANY=!COMPANY!" && call pnpm run build
-if !errorlevel! neq 0 (
-    echo [ERROR] Build failed! Check your project configuration.
-    goto :error_exit
-)
-echo [OK] Build completed!
-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.
-    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"
-
-echo [INFO] Dist directory: %DIST_DIR%
-echo [INFO] Files count: %FILE_COUNT%
-echo [INFO] Total size: %TOTAL_SIZE_MB% MB
-echo [INFO] Target: %USER%@%IP%:%PORT%
-echo [INFO] Remote dir: %REMOTE_DIR%
-echo.
-
-:: Optional: Ask for confirmation before upload
-set /p "CONFIRM=Proceed with upload? (Y/N, default Y): "
-if /i "!CONFIRM!"=="N" (
-    echo [INFO] Upload cancelled by user.
-    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%/
-if %errorlevel% neq 0 (
-    echo [ERROR] Upload failed! Check network or credentials.
-    goto :error_exit
-)
-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"
-
-echo.
-echo ==========================================
-echo [SUCCESS] Deploy completed!
-echo ==========================================
-echo.
-echo Summary:
-echo   - Build: Completed
-echo   - Upload: %FILE_COUNT% files (%TOTAL_SIZE_MB% MB)
-echo   - Target: %USER%@%IP%:%PORT%%REMOTE_DIR%
-echo.
-
-goto :end
-
-:error_exit
-echo.
-echo ==========================================
-echo [FAILED] Deploy failed!
-echo ==========================================
-
-:end
-pause
\ No newline at end of file
+@echo off
+setlocal enabledelayedexpansion
+
+:: ================= Config =================
+set "COMPANY=TEST"
+set "USER=root"
+set "PASS=Xdrjkj@10086#"
+set "IP=36.213.180.217"
+set "PORT=65022"
+set "ZIP_FILE=dist.zip"
+set "REMOTE_DIR=/data/project/mom/vue/test/dist"
+set "SSH_OPTS=-o StrictHostKeyChecking=no -o ConnectTimeout=10"
+:: ===========================================
+
+echo ==========================================
+echo        Vue Dist Auto Deploy Script
+echo ==========================================
+echo.
+
+:: Check if sshpass is available
+set "SSHPASS_DIR=%~dp0bin"
+if exist "!SSHPASS_DIR!\sshpass.exe" (
+    set "PATH=!SSHPASS_DIR!;%PATH%"
+)
+
+where sshpass >nul 2>&1
+if %errorlevel% equ 0 goto :check_node
+
+echo [WARN] sshpass not found, trying to install...
+echo.
+
+where bash >nul 2>&1
+if %errorlevel% equ 0 (
+    echo [INFO] Git Bash detected, trying pacman install...
+    bash -c "pacman -S --noconfirm sshpass" 2>nul
+    if !errorlevel! equ 0 (
+        echo [OK] sshpass installed via pacman!
+        goto :check_node
+    )
+)
+
+if not exist "!SSHPASS_DIR!" mkdir "!SSHPASS_DIR!"
+
+if exist "!SSHPASS_DIR!\sshpass.exe" (
+    set "PATH=!SSHPASS_DIR!;%PATH%"
+    echo [OK] sshpass found at: !SSHPASS_DIR!
+    goto :check_node
+)
+
+echo [INFO] Downloading sshpass...
+powershell -NoProfile -Command "$url='https://github.com/xhcoding/sshpass-win32/releases/download/v1.0.3/sshpass.exe'; $out='!SSHPASS_DIR!\sshpass.exe'; Write-Host \"Downloading to: $out\"; try { Invoke-WebRequest -Uri $url -OutFile $out -UseBasicParsing; exit 0 } catch { Write-Host $_.Exception.Message; exit 1 }"
+if !errorlevel! neq 0 (
+    echo [ERROR] Failed to download sshpass!
+    echo [HINT] Please install sshpass manually or use Git Bash: pacman -S sshpass
+    goto :error_exit
+)
+
+set "PATH=!SSHPASS_DIR!;%PATH%"
+echo [OK] sshpass downloaded to: !SSHPASS_DIR!
+echo.
+
+:check_node
+:: Check if pnpm is available
+where pnpm >nul 2>&1
+if %errorlevel% neq 0 (
+    echo [ERROR] pnpm not found! Please install pnpm first.
+    echo [HINT] npm install -g pnpm
+    goto :error_exit
+)
+
+:: Check if node_modules exists
+if not exist "node_modules" (
+    echo [INFO] node_modules not found, running pnpm install...
+    echo.
+    call pnpm install
+    if !errorlevel! neq 0 (
+        echo [ERROR] pnpm install failed!
+        goto :error_exit
+    )
+    echo.
+)
+
+:: 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 %ZIP_FILE%
+        goto :upload
+    )
+)
+
+:: Build the project
+echo [STEP 1/3] Building project for %COMPANY%...
+echo [INFO] Running: pnpm run build
+echo.
+:: 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
+)
+echo [OK] Build completed!
+echo.
+
+:upload
+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
+)
+
+:: Calculate zip size
+for %%F in ("%ZIP_FILE%") do set "ZIP_BYTES=%%~zF"
+set /a "ZIP_SIZE_MB=ZIP_BYTES/1024/1024"
+
+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.
+
+:: Optional: Ask for confirmation before upload
+set /p "CONFIRM=Proceed with upload? (Y/N, default Y): "
+if /i "!CONFIRM!"=="N" (
+    echo [INFO] Upload cancelled by user.
+    goto :end
+)
+
+:: 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
+)
+echo [OK] Upload completed!
+echo.
+
+:: 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: %ZIP_FILE% (%ZIP_SIZE_MB% MB)
+echo   - Target: %USER%@%IP%:%PORT%%REMOTE_DIR%
+echo.
+
+goto :end
+
+:error_exit
+echo.
+echo ==========================================
+echo [FAILED] Deploy failed!
+echo ==========================================
+
+:end
+pause
\ No newline at end of file
diff --git a/package.json b/package.json
index b7c7fbb..f8cce41 100644
--- a/package.json
+++ b/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"
 }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b02d913..81f3c62 100644
--- a/pnpm-lock.yaml
+++ b/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
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 5df4a75..6105520 100644
--- a/pnpm-workspace.yaml
+++ b/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
diff --git a/public/logos/YZFXLogo.ico b/public/logos/YZFXLogo.ico
new file mode 100644
index 0000000..2438b06
--- /dev/null
+++ b/public/logos/YZFXLogo.ico
Binary files differ
diff --git a/public/logos/YZFXLogo.png b/public/logos/YZFXLogo.png
new file mode 100644
index 0000000..2438b06
--- /dev/null
+++ b/public/logos/YZFXLogo.png
Binary files differ
diff --git a/src/api/bpm/businessAttach/index.ts b/src/api/bpm/businessAttach/index.ts
new file mode 100644
index 0000000..4dd1aa9
--- /dev/null
+++ b/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}`,
+  );
+}
diff --git a/src/api/erp/purchase/invoice/index.ts b/src/api/erp/purchase/invoice/index.ts
index bf69aaa..305230c 100644
--- a/src/api/erp/purchase/invoice/index.ts
+++ b/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,
diff --git a/src/api/mes/dv/metering/index.ts b/src/api/mes/dv/metering/index.ts
new file mode 100644
index 0000000..55f0e7e
--- /dev/null
+++ b/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; // 璐d换浜虹紪鍙�
+    chargeUserNickname?: string; // 璐d换浜烘樀绉�
+    checkCycleCount?: number; // 鏍″噯鍛ㄦ湡锛堟湀锛�
+    useDate?: string; // 鎶曞叆浣跨敤鏃ユ湡
+    lastCheckDate?: string; // 涓婃妫�娴嬫棩鏈�
+    nextCheckDate?: string; // 涓嬫妫�娴嬫棩鏈燂紙鍒版湡鏃ワ級
+    status?: number; // 鐘舵�侊紙瀛楀吀锛�
+    expireStatus?: number; // 鍒版湡鐘舵�侊細1 宸茶繃鏈� / 2 鍗冲皢鍒版湡 / 3 姝e父
+    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',
+  );
+}
diff --git a/src/api/mes/dv/meteringcheck/index.ts b/src/api/mes/dv/meteringcheck/index.ts
new file mode 100644
index 0000000..e8995ec
--- /dev/null
+++ b/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,
+  });
+}
diff --git a/src/api/mes/md/item/index.ts b/src/api/mes/md/item/index.ts
index f2a23c0..6ad491e 100644
--- a/src/api/mes/md/item/index.ts
+++ b/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}`);
diff --git a/src/api/mes/pd/project/index.ts b/src/api/mes/pd/project/index.ts
index 40e34ab..e6841ce 100644
--- a/src/api/mes/pd/project/index.ts
+++ b/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, {
diff --git a/src/api/mes/wm/batch/index.ts b/src/api/mes/wm/batch/index.ts
index 7ed63bc..569efd7 100644
--- a/src/api/mes/wm/batch/index.ts
+++ b/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'; // 浜嬩欢绫诲瀷锛歄UT-浜у嚭 / 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 },
+  });
+}
diff --git a/src/api/srm/supplier/index.ts b/src/api/srm/supplier/index.ts
index 60b176a..e75a362 100644
--- a/src/api/srm/supplier/index.ts
+++ b/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}`);
diff --git a/src/api/srm/supplierCertificate/index.ts b/src/api/srm/supplierCertificate/index.ts
index 9908b1c..2504662 100644
--- a/src/api/srm/supplierCertificate/index.ts
+++ b/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[];
   }
 }
 
diff --git a/src/api/srm/tender/index.ts b/src/api/srm/tender/index.ts
index a44c5b9..cefefdb 100644
--- a/src/api/srm/tender/index.ts
+++ b/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[];
   }
 }
 
diff --git a/src/bootstrap.ts b/src/bootstrap.ts
index 2e22e62..6a97710 100644
--- a/src/bootstrap.ts
+++ b/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);
     }
   });
diff --git a/src/components/form-create/helpers.ts b/src/components/form-create/helpers.ts
index 12d07a6..9e827de 100644
--- a/src/components/form-create/helpers.ts
+++ b/src/components/form-create/helpers.ts
@@ -28,7 +28,10 @@
 }
 
 /** 瑙g爜琛ㄥ崟 Conf */
-export function decodeConf(conf: string) {
+export function decodeConf(conf?: string | null) {
+  if (!conf) {
+    return {};
+  }
   return formCreate.parseJson(conf);
 }
 
@@ -43,9 +46,9 @@
 }
 
 /** 瑙g爜琛ㄥ崟 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;
diff --git a/src/layouts/auth.vue b/src/layouts/auth.vue
index ad41d1f..1cdccb9 100644
--- a/src/layouts/auth.vue
+++ b/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>
diff --git a/src/main.ts b/src/main.ts
index e208cd2..b692f75 100644
--- a/src/main.ts
+++ b/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) {
diff --git a/src/packages/constants/src/biz-mes-enum.ts b/src/packages/constants/src/biz-mes-enum.ts
index 1ac585c..85c58db 100644
--- a/src/packages/constants/src/biz-mes-enum.ts
+++ b/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, // 姝e父
+} as const;
+
 /** MES 缁翠慨宸ュ崟鐘舵�佹灇涓� */
 export const MesDvRepairStatusEnum = {
   PREPARE: MesOrderStatusConstants.DRAFT,
@@ -510,11 +530,12 @@
   RIGHT: 2, // 鍙宠ˉ榻�
 } as const;
 
-/** MES 浜у搧璁捐椤圭洰鐘舵�佹灇涓撅紙绾挎�ф祦杞細寰呭彂甯冣啋寰呬笂浼犫啋寰呭鏍糕啋寰呭綊妗b啋宸插綊妗o級 */
+/** MES 浜у搧璁捐椤圭洰鐘舵�佹灇涓撅紙绾挎�ф祦杞細寰呭彂甯冣啋寰呬笂浼犫啋寰呭鏍糕啋瀹℃壒涓啋寰呭綊妗b啋宸插綊妗o級 */
 export const MesPdProjectStatusEnum = {
   PENDING_PUBLISH: 0, // 寰呭彂甯�
   PENDING_UPLOAD: 1, // 寰呬笂浼�
   PENDING_AUDIT: 2, // 寰呭鏍�
+  APPROVING: 5, // 瀹℃壒涓�
   PENDING_ARCHIVE: 3, // 寰呭綊妗�
   ARCHIVED: 4, // 宸插綊妗�
 } as const;
diff --git a/src/packages/constants/src/dict-enum.ts b/src/packages/constants/src/dict-enum.ts
index ca8cf0a..2a62e5a 100644
--- a/src/packages/constants/src/dict-enum.ts
+++ b/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', // 鐢熶骇璁㈠崟鐘舵��
diff --git a/src/packages/effects/layouts/src/basic/layout.vue b/src/packages/effects/layouts/src/basic/layout.vue
index 58139d0..10467c3 100644
--- a/src/packages/effects/layouts/src/basic/layout.vue
+++ b/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>
diff --git a/src/preferences.ts b/src/preferences.ts
index fbaa7b4..4da3a4e 100644
--- a/src/preferences.ts
+++ b/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: {
diff --git a/src/types/app.d.ts b/src/types/app.d.ts
index d65d0ad..8067547 100644
--- a/src/types/app.d.ts
+++ b/src/types/app.d.ts
@@ -1,2 +1,3 @@
 declare const __APP_LOGO__: string;
 declare const __APP_FAVICON__: string;
+declare const __APP_NAME__: string;
diff --git a/src/views/bpm/businessAttach/data.ts b/src/views/bpm/businessAttach/data.ts
new file mode 100644
index 0000000..f642965
--- /dev/null
+++ b/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' },
+    },
+  ];
+}
diff --git a/src/views/bpm/businessAttach/index.vue b/src/views/bpm/businessAttach/index.vue
new file mode 100644
index 0000000..10c506d
--- /dev/null
+++ b/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>
diff --git a/src/views/bpm/businessAttach/modules/form.vue b/src/views/bpm/businessAttach/modules/form.vue
new file mode 100644
index 0000000..62e57dc
--- /dev/null
+++ b/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>
diff --git a/src/views/bpm/model/index.vue b/src/views/bpm/model/index.vue
index 91c504f..d7b2b7d 100644
--- a/src/views/bpm/model/index.vue
+++ b/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();
+});
+
+/** 婵�娲绘椂鍒锋柊锛坘eep-alive 缂撳瓨鍦烘櫙涓嬩粠缂栬緫椤佃繑鍥炴椂瑙﹀彂锛� */
 onActivated(() => {
   getList();
 });
diff --git a/src/views/crm/contract/detail/approve/index.vue b/src/views/crm/contract/detail/approve/index.vue
new file mode 100644
index 0000000..adc3157
--- /dev/null
+++ b/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>
diff --git a/src/views/crm/invoice/detail/approve/index.vue b/src/views/crm/invoice/detail/approve/index.vue
new file mode 100644
index 0000000..ec1530c
--- /dev/null
+++ b/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>
diff --git a/src/views/crm/receivable/components/data.ts b/src/views/crm/receivable/components/data.ts
index 4f3f0e1..4aa273b 100644
--- a/src/views/crm/receivable/components/data.ts
+++ b/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,
diff --git a/src/views/crm/receivable/data.ts b/src/views/crm/receivable/data.ts
index 5dc6c1a..e401e1f 100644
--- a/src/views/crm/receivable/data.ts
+++ b/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,
diff --git a/src/views/crm/receivable/detail/approve/index.vue b/src/views/crm/receivable/detail/approve/index.vue
new file mode 100644
index 0000000..cc3c795
--- /dev/null
+++ b/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>
diff --git a/src/views/erp/finance/payment/detail/approve/index.vue b/src/views/erp/finance/payment/detail/approve/index.vue
new file mode 100644
index 0000000..c098816
--- /dev/null
+++ b/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>
diff --git a/src/views/erp/purchase/invoice/data.ts b/src/views/erp/purchase/invoice/data.ts
index c70e185..a371839 100644
--- a/src/views/erp/purchase/invoice/data.ts
+++ b/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' },
     },
diff --git a/src/views/erp/purchase/invoice/detail/approve/index.vue b/src/views/erp/purchase/invoice/detail/approve/index.vue
new file mode 100644
index 0000000..993efbc
--- /dev/null
+++ b/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>
diff --git a/src/views/erp/purchase/invoice/index.vue b/src/views/erp/purchase/invoice/index.vue
index c0cbd7b..38fc81b 100644
--- a/src/views/erp/purchase/invoice/index.vue
+++ b/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),
diff --git a/src/views/erp/purchase/invoice/modules/form.vue b/src/views/erp/purchase/invoice/modules/form.vue
index 3450581..74d0462 100644
--- a/src/views/erp/purchase/invoice/modules/form.vue
+++ b/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 浠f浛 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 {
diff --git a/src/views/erp/purchase/order/detail/index.vue b/src/views/erp/purchase/order/detail/index.vue
new file mode 100644
index 0000000..fadc835
--- /dev/null
+++ b/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>
diff --git a/src/views/erp/purchase/request/detail/index.vue b/src/views/erp/purchase/request/detail/index.vue
new file mode 100644
index 0000000..62c383e
--- /dev/null
+++ b/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>
diff --git a/src/views/hrm/attendance/exception/detail/approve/index.vue b/src/views/hrm/attendance/exception/detail/approve/index.vue
new file mode 100644
index 0000000..e0fef3a
--- /dev/null
+++ b/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>
diff --git a/src/views/hrm/leave/detail/approve/index.vue b/src/views/hrm/leave/detail/approve/index.vue
new file mode 100644
index 0000000..59248e4
--- /dev/null
+++ b/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>
diff --git a/src/views/hrm/resignation/detail/approve/index.vue b/src/views/hrm/resignation/detail/approve/index.vue
new file mode 100644
index 0000000..4e3a587
--- /dev/null
+++ b/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>
diff --git a/src/views/hrm/transfer/detail/approve/index.vue b/src/views/hrm/transfer/detail/approve/index.vue
new file mode 100644
index 0000000..5d983ef
--- /dev/null
+++ b/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>
diff --git a/src/views/im/home/index.vue b/src/views/im/home/index.vue
index 5f7bdc5..74fee42 100644
--- a/src/views/im/home/index.vue
+++ b/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
     })
   },
diff --git a/src/views/mes/dv/metering/components/index.ts b/src/views/mes/dv/metering/components/index.ts
new file mode 100644
index 0000000..feacb92
--- /dev/null
+++ b/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';
diff --git a/src/views/mes/dv/metering/components/select-dialog.vue b/src/views/mes/dv/metering/components/select-dialog.vue
new file mode 100644
index 0000000..5d2cd21
--- /dev/null
+++ b/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>
diff --git a/src/views/mes/dv/metering/components/select.vue b/src/views/mes/dv/metering/components/select.vue
new file mode 100644
index 0000000..018e89d
--- /dev/null
+++ b/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>
diff --git a/src/views/mes/dv/metering/data.ts b/src/views/mes/dv/metering/data.ts
new file mode 100644
index 0000000..1f57561
--- /dev/null
+++ b/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: '姝e父',
+    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: '璐d换浜�',
+      component: 'ApiSelect',
+      componentProps: {
+        allowClear: true,
+        api: getSimpleUserList,
+        labelField: 'nickname',
+        placeholder: '璇烽�夋嫨璐d换浜�',
+        valueField: 'id',
+      },
+      help: '璇ュ櫒鍏风殑缁存姢璐d换浜猴紝鍒版湡鎻愰啋鐨勭珯鍐呬俊浼氬彂缁欎粬',
+    },
+    {
+      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 澶╋紙鍩虹璁炬柦閰嶇疆锛夊皢鎻愰啋璐d换浜�',
+      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: '璐d换浜�',
+      component: 'ApiSelect',
+      componentProps: {
+        allowClear: true,
+        api: getSimpleUserList,
+        labelField: 'nickname',
+        placeholder: '璇烽�夋嫨璐d换浜�',
+        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: '璐d换浜�', 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: '璐d换浜�', 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 },
+      },
+    },
+  ];
+}
diff --git a/src/views/mes/dv/metering/index.vue b/src/views/mes/dv/metering/index.vue
new file mode 100644
index 0000000..27cbd3c
--- /dev/null
+++ b/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>
diff --git a/src/views/mes/dv/metering/modules/form.vue b/src/views/mes/dv/metering/modules/form.vue
new file mode 100644
index 0000000..62535f4
--- /dev/null
+++ b/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>
diff --git a/src/views/mes/dv/meteringcheck/data.ts b/src/views/mes/dv/meteringcheck/data.ts
new file mode 100644
index 0000000..4e1e4e9
--- /dev/null
+++ b/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',
+      },
+    },
+  ];
+}
diff --git a/src/views/mes/dv/meteringcheck/index.vue b/src/views/mes/dv/meteringcheck/index.vue
new file mode 100644
index 0000000..c8039f3
--- /dev/null
+++ b/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>
diff --git a/src/views/mes/dv/meteringcheck/modules/form.vue b/src/views/mes/dv/meteringcheck/modules/form.vue
new file mode 100644
index 0000000..308d312
--- /dev/null
+++ b/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>
diff --git a/src/views/mes/md/item/data.ts b/src/views/mes/md/item/data.ts
index 5425bc8..5d74bb2 100644
--- a/src/views/mes/md/item/data.ts
+++ b/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 },
       },
     },
     {
diff --git a/src/views/mes/md/item/index.vue b/src/views/mes/md/item/index.vue
index f8855f6..55dd8cd 100644
--- a/src/views/mes/md/item/index.vue
+++ b/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',
diff --git a/src/views/mes/pd/project/detail/index.vue b/src/views/mes/pd/project/detail/index.vue
index 36da5ce..97f7f8b 100644
--- a/src/views/mes/pd/project/detail/index.vue
+++ b/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: '褰掓。',
diff --git a/src/views/mes/pd/project/index.vue b/src/views/mes/pd/project/index.vue
index 24516e1..6727d6d 100644
--- a/src/views/mes/pd/project/index.vue
+++ b/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: '褰掓。',
diff --git a/src/views/mes/pd/project/modules/process-select-modal.vue b/src/views/mes/pd/project/modules/process-select-modal.vue
new file mode 100644
index 0000000..c0d099a
--- /dev/null
+++ b/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>
diff --git a/src/views/mes/pd/project/modules/workflow-timeline.vue b/src/views/mes/pd/project/modules/workflow-timeline.vue
index 6989422..db03cc8 100644
--- a/src/views/mes/pd/project/modules/workflow-timeline.vue
+++ b/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>
diff --git a/src/views/mes/qc/batchtrace/data.ts b/src/views/mes/qc/batchtrace/data.ts
index 52e9620..3013c2e 100644
--- a/src/views/mes/qc/batchtrace/data.ts
+++ b/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',
+    },
+  ];
+}
diff --git a/src/views/mes/qc/batchtrace/index.vue b/src/views/mes/qc/batchtrace/index.vue
index b403dec..1762b35 100644
--- a/src/views/mes/qc/batchtrace/index.vue
+++ b/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
diff --git a/src/views/mes/qc/batchtrace/modules/process-trace-list.vue b/src/views/mes/qc/batchtrace/modules/process-trace-list.vue
new file mode 100644
index 0000000..2c3725e
--- /dev/null
+++ b/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>
diff --git a/src/views/mes/qc/batchtrace/modules/purchase-source-list.vue b/src/views/mes/qc/batchtrace/modules/purchase-source-list.vue
new file mode 100644
index 0000000..472a6a4
--- /dev/null
+++ b/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>
diff --git a/src/views/mes/qc/batchtrace/modules/sales-target-list.vue b/src/views/mes/qc/batchtrace/modules/sales-target-list.vue
new file mode 100644
index 0000000..0963502
--- /dev/null
+++ b/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>
diff --git a/src/views/mes/qc/batchtrace/modules/trace-detail.vue b/src/views/mes/qc/batchtrace/modules/trace-detail.vue
index 7fcba7e..8d01d5b 100644
--- a/src/views/mes/qc/batchtrace/modules/trace-detail.vue
+++ b/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>
diff --git a/src/views/srm/statistics/index.vue b/src/views/srm/statistics/index.vue
index 439ae78..5f9652a 100644
--- a/src/views/srm/statistics/index.vue
+++ b/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>
diff --git a/src/views/srm/supplierCertificate/data.ts b/src/views/srm/supplierCertificate/data.ts
index 1472c53..c956f2d 100644
--- a/src/views/srm/supplierCertificate/data.ts
+++ b/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銆乯peg銆乸ng銆乸df銆乨oc銆乨ocx鏍煎紡锛屽崟涓枃浠朵笉瓒呰繃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',
diff --git a/src/views/srm/supplierCertificate/index.vue b/src/views/srm/supplierCertificate/index.vue
index 29ea03f..a26aa75 100644
--- a/src/views/srm/supplierCertificate/index.vue
+++ b/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="[
diff --git a/src/views/srm/supplierCertificate/modules/form.vue b/src/views/srm/supplierCertificate/modules/form.vue
index d0bc05b..041e5e0 100644
--- a/src/views/srm/supplierCertificate/modules/form.vue
+++ b/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();
diff --git a/src/views/srm/tender/data.ts b/src/views/srm/tender/data.ts
index a3b1005..475eae7 100644
--- a/src/views/srm/tender/data.ts
+++ b/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銆乯peg銆乸ng銆乸df銆乨oc銆乨ocx鏍煎紡锛屽崟涓枃浠朵笉瓒呰繃2MB',
+      },
+      formItemClass: 'col-span-2',
+    },
   ];
 }
 
diff --git a/src/views/srm/tender/detail.vue b/src/views/srm/tender/detail.vue
index 7b2f1de..3f3a7f2 100644
--- a/src/views/srm/tender/detail.vue
+++ b/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>
@@ -998,4 +1017,4 @@
       </a-modal>
     </template>
   </Page>
-</template>
+</template>
\ No newline at end of file
diff --git a/src/views/srm/tender/modules/form.vue b/src/views/srm/tender/modules/form.vue
index bcdc8d2..652d017 100644
--- a/src/views/srm/tender/modules/form.vue
+++ b/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 });
diff --git a/vite.config.ts b/vite.config.ts
index 5b47f2d..1406863 100644
--- a/vite.config.ts
+++ b/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,
           },
         },

--
Gitblit v1.9.3