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