From ec226b03fbd26bc178137ad20d7bd030ef2c5c8b Mon Sep 17 00:00:00 2001
From: liding <756868258@qq.com>
Date: 星期三, 24 六月 2026 15:01:40 +0800
Subject: [PATCH] Merge branch 'refs/heads/dev_NEW_pro' into dev_宁夏_铭远环保
---
src/views/collaborativeApproval/notificationManagement/summary/index.vue | 8
src/views/mock/dataCheck/index.vue | 241 +
doc/知识库模块前端实现文档.md | 1212 +++++
src/views/financialManagement/payable/payment.vue | 22
src/api/collaborativeApproval/knowledgeBase.js | 161
src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue | 2
src/views/productionManagement/teachingDemo/FactoryDemo.vue | 1153 ++++
src/api/mock/dataCheck.js | 10
src/views/collaborativeApproval/sealManagement/index.vue | 94
src/views/financialManagement/payable/input-invoice.vue | 22
src/views/productionManagement/workOrderManagement/index.vue | 2
src/api/procurementManagement/paymentLedger.js | 9
multiple/assets/logo/QFSWLogo.png | 0
src/views/safeProduction/safetyTrainingAssessment/index.vue | 11
src/views/productionManagement/productStructure/DetailNew/index.vue | 626 ++
multiple/assets/favicon/DCHXfavicon.ico | 0
multiple/assets/logo/XHKSLogo.png | 0
src/views/personnelManagement/socialSecuritySet/components/formDia.vue | 808 +-
multiple/assets/favicon/NYfavicon.ico | 0
src/views/productionManagement/teachingDemo/components/TreeNode.vue | 423 +
src/views/financialManagement/receivable/receipt.vue | 12
multiple/assets/logo/KHYYLogo.png | 0
multiple/assets/logo/NYLogo.png | 0
src/views/personnelManagement/monthlyStatistics/components/formDia.vue | 1452 +++--
multiple/assets/favicon/LSYfavicon.ico | 0
multiple/assets/favicon/BTWfavicon.ico | 0
src/views/productionManagement/workOrderEdit/index.vue | 2
src/views/financialManagement/assets/fixedAssets.vue | 11
src/views/financialManagement/receivable/salesReturn.vue | 24
src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js | 92
doc/知识库RAG功能实现文档.md | 1034 ++++
src/views/collaborativeApproval/notificationManagement/meetPublish/index.vue | 6
src/api/viewIndex.js | 3
src/views/productionManagement/teachingDemo/CombinedDemo.vue | 518 ++
multiple/assets/logo/XLGSLogo.png | 0
src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js | 3
multiple/assets/favicon/SDZZfavicon.ico | 0
multiple/assets/favicon/YSSfavicon.ico | 0
src/views/personnelManagement/monthlyStatistics/components/auditDia.vue | 17
src/api/personnelManagement/staffSalaryMain.js | 16
src/views/productionManagement/productStructure/DetailNew/MaterialCard.vue | 236
src/views/productionManagement/teachingDemo/ProcessRouteDemo.vue | 535 ++
multiple/assets/favicon/QFSWfavicon.ico | 0
doc/知识库模块传参方式和参数命名规范文档.md | 941 +++
src/views/financialManagement/voucher/index.vue | 13
src/views/salesManagement/salesLedger/index.vue | 44
src/api/mock/generate.js | 10
src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue | 741 +-
src/views/qualityManagement/metricBinding/index.vue | 1
multiple/assets/logo/DCHXLogo.png | 0
multiple/assets/logo/ZDJCLogo.png | 0
src/views/financialManagement/assets/intangibleAssets.vue | 16
src/views/financialManagement/receivable/reconciliation.vue | 25
src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/components/DetailPanel.vue | 4
src/views/productionManagement/teachingDemo/components/ProcessNode.vue | 354 +
src/views/collaborativeApproval/knowledgeBase/index.vue | 741 +++
src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue | 7
src/assets/styles/sidebar.scss | 37
bin/sshpass.exe | 0
src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue | 4
src/components/PIMTable/PIMTable.vue | 2
src/views/basicData/customerFileOpenSea/index.vue | 21
src/views/collaborativeApproval/notificationManagement/meetIndex/index.vue | 9
src/api/inventoryManagement/stockOut.js | 9
src/views/financialManagement/receivable/outputInvoice.vue | 23
src/views/personnelManagement/monthlyStatistics/index.vue | 16
src/views/financialManagement/payable/purchaseReturn.vue | 20
src/views/index.vue | 67
src/views/financialManagement/receivable/salesOut.vue | 24
src/views/productionManagement/teachingDemo/components/DemoControls.vue | 186
multiple/assets/favicon/XLGSfavicon.ico | 0
src/api/procurementManagement/procurementLedger.js | 70
src/views/basicData/parameterMaintenance/index.vue | 14
multiple/assets/favicon/RFSYfavicon.ico | 0
src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue | 129
src/views/personnelManagement/attendanceCheckin/checkinRules/components/form.vue | 5
multiple/assets/favicon/JFHYfavicon.ico | 0
src/views/basicData/customerFile/index.vue | 15
multiple/assets/logo/BLSGRLogo.png | 0
src/views/procurementManagement/procurementPlan/index.vue | 13
doc/mock_data_generate.md | 243 +
multiple/assets/favicon/RFSYLogo.png | 0
vite.config.js | 4
src/views/productionManagement/teachingDemo/index.vue | 333 +
multiple/assets/logo/YSSLogo.png | 0
src/views/inventoryManagement/stockManagement/New.vue | 8
src/views/salesManagement/deliveryLedger/index.vue | 14
multiple/assets/favicon/ZDJCfavicon.ico | 0
src/views/collaborativeApproval/notificationManagement/meetExamine/index.vue | 6
src/views/personnelManagement/employeeRecord/index.vue | 4
multiple/assets/logo/BTWLogo.png | 0
src/router/index.js | 38
multiple/assets/logo/HTHBLogo.png | 0
src/views/financialManagement/payable/paymentApply.vue | 42
src/views/procurementManagement/procurementLedger/index.vue | 64
src/views/financialManagement/payable/reconciliation.vue | 22
src/views/financialManagement/payable/purchaseIn.vue | 22
src/views/productionManagement/productionOrder/index.vue | 2
src/assets/logo/XDRJ.png | 0
.gitignore | 5
src/views/procurementManagement/qualityInspection/index.vue | 16
src/views/productionManagement/workOrder/index.vue | 2
src/views/productionManagement/teachingDemo/BomDemo.vue | 430 +
src/views/procurementManagement/purchaseOrder/index.vue | 15
multiple/assets/favicon/KHYYfavicon.ico | 0
src/views/productManagement/productIdentifier/index.vue | 11
src/views/inventoryManagement/dispatchLog/Record.vue | 41
multiple/assets/logo/SDZZLogo.png | 0
src/views/salesManagement/returnOrder/components/formDia.vue | 16
src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue | 4
src/layout/components/Sidebar/SidebarItem.vue | 107
doc/mock_data_check.md | 183
multiple/assets/favicon/HTHBfavicon.ico | 0
multiple/assets/logo/LSYLogo.png | 0
multiple/assets/favicon/BLSGRfavicon.ico | 0
multiple/assets/favicon/XHKSfavicon.ico | 0
src/views/financialManagement/receivable/invoiceApply.vue | 35
src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js | 15
src/views/officeProcessAutomation/ApproveManage/approve-shared/useApprovalInstanceModule.js | 10
deploy.bat | 124
multiple/assets/logo/XDRJ.png | 0
src/views/productionPlan/productionPlan/index.vue | 22
multiple/assets/logo/JFHYLogo.png | 0
multiple/assets/logo/RFSYLogo.png | 0
src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue | 73
125 files changed, 12,436 insertions(+), 1,801 deletions(-)
diff --git a/.gitignore b/.gitignore
index 78a752d..f979342 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,8 @@
yarn-error.log*
**/*.log
+.claude/
+
tests/**/coverage/
tests/e2e/reports
selenium-debug.log
@@ -19,5 +21,8 @@
*.sln
*.local
+# Claude Code
+.claude/
+
package-lock.json
yarn.lock
diff --git a/bin/sshpass.exe b/bin/sshpass.exe
new file mode 100644
index 0000000..da9133c
--- /dev/null
+++ b/bin/sshpass.exe
Binary files differ
diff --git a/deploy.bat b/deploy.bat
new file mode 100644
index 0000000..17c8665
--- /dev/null
+++ b/deploy.bat
@@ -0,0 +1,124 @@
+@echo off
+chcp 65001 >nul
+setlocal enabledelayedexpansion
+
+:: ================= Config =================
+set "USER=dell"
+set "PASS=xd123456@.."
+set "IP=1.15.17.182"
+set "PORT=10000"
+set "DIST_DIR=dist"
+set "REMOTE_DIR=/home/vue/newpro-product"
+set "SSH_OPTS=-o StrictHostKeyChecking=no -o ConnectTimeout=10"
+:: ===========================================
+
+echo ==========================================
+echo Vue Dist Auto Deploy Script
+echo ==========================================
+echo.
+
+:: Check if sshpass is available
+where sshpass >nul 2>&1
+if %errorlevel% neq 0 (
+ 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_dist
+ )
+ )
+
+ set "SSHPASS_DIR=%~dp0bin"
+ if not exist "!SSHPASS_DIR!" mkdir "!SSHPASS_DIR!"
+
+ 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_dist
+if not exist "%DIST_DIR%" (
+ echo [ERROR] Dist directory not found: %DIST_DIR%
+ echo [HINT] Please run 'npm run build' first!
+ 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.
+
+:: Compress dist to zip
+set "ZIP_FILE=dist.zip"
+echo [STEP 1/3] Compressing dist to %ZIP_FILE%...
+if exist "%ZIP_FILE%" del "%ZIP_FILE%"
+powershell -NoProfile -Command "Compress-Archive -Path '%DIST_DIR%\*' -DestinationPath '%ZIP_FILE%' -CompressionLevel Optimal"
+if %errorlevel% neq 0 (
+ echo [ERROR] Compression failed!
+ goto :error_exit
+)
+for %%F in ("%ZIP_FILE%") do set "ZIP_SIZE_MB=%%~zF"
+set /a "ZIP_SIZE_MB=ZIP_SIZE_MB/1024/1024"
+echo [OK] Compressed: %ZIP_SIZE_MB% MB
+echo.
+
+:: Upload zip file
+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.
+
+:: Extract on remote server
+echo [STEP 3/3] Extracting on remote server...
+sshpass -p "%PASS%" ssh -p %PORT% %SSH_OPTS% %USER%@%IP% "cd '%REMOTE_DIR%' && rm -rf dist && unzip -o '%ZIP_FILE%' -d dist && rm -f '%ZIP_FILE%'"
+if %errorlevel% neq 0 (
+ echo [ERROR] Extraction failed!
+ goto :error_exit
+)
+echo [OK] Extraction completed!
+
+:: Cleanup local zip
+del "%ZIP_FILE%"
+
+echo.
+echo ==========================================
+echo [SUCCESS] Deploy completed!
+echo ==========================================
+goto :end
+
+:error_exit
+echo.
+echo ==========================================
+echo [FAILED] Deploy failed!
+echo ==========================================
+
+:end
+pause
diff --git a/doc/mock_data_check.md b/doc/mock_data_check.md
new file mode 100644
index 0000000..8c67159
--- /dev/null
+++ b/doc/mock_data_check.md
@@ -0,0 +1,183 @@
+# 鏁版嵁妯℃嫙 - 鍩虹鏁版嵁妫�娴�
+
+## 娑夊強椤甸潰
+
+- 鏁版嵁妯℃嫙鎿嶄綔椤甸潰锛堟柊澧烇級
+
+## API
+
+| 鏂规硶 | 璺緞 | 璇存槑 |
+|------|------|------|
+| POST | /mock/dataCheck | 妫�娴嬫寚瀹氭ā鍧楃殑鍩虹鏁版嵁鏄惁灏辩华 |
+
+**璇锋眰鍙傛暟锛�**
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| modules | List\<String\> | 鏄� | 瑕佹娴嬬殑妯″潡鍒楄〃锛屽彲閫夊�硷細sales锛堥攢鍞級銆乸urchase锛堥噰璐級銆乹uality锛堣川閲忥級銆乸roduction锛堢敓浜э級 |
+
+璇锋眰浣撶ず渚嬶細
+```json
+{ "modules": ["sales", "purchase", "quality", "production"] }
+```
+
+**鍝嶅簲锛�**
+```json
+{
+ "code": 200,
+ "msg": "鎿嶄綔鎴愬姛",
+ "data": {
+ "totalItems": 6,
+ "passedItems": 3,
+ "items": [
+ {
+ "module": "common",
+ "itemName": "浜у搧鏁版嵁",
+ "minRequired": 1,
+ "currentCount": 5,
+ "passed": true,
+ "message": "閫氳繃"
+ },
+ {
+ "module": "sales",
+ "itemName": "瀹㈡埛鏁版嵁",
+ "minRequired": 1,
+ "currentCount": 0,
+ "passed": false,
+ "message": "缂哄皯瀹㈡埛鏁版嵁锛岃鍏堝湪銆愬熀纭�鏁版嵁-瀹㈡埛绠$悊銆戜腑娣诲姞鑷冲皯1鏉″鎴�"
+ }
+ ]
+ }
+}
+```
+
+## 妫�娴嬭鍒�
+
+### 閿�鍞ā鍧� (sales)
+| 妫�娴嬮」 | 鏈�浣庢暟閲� | 鏈�氳繃鎻愮ず |
+|--------|----------|------------|
+| 浜у搧鏁版嵁 | 1 | 缂哄皯浜у搧鏁版嵁锛岃鍏堝湪銆愬熀纭�鏁版嵁-浜у搧绠$悊銆戜腑娣诲姞鑷冲皯1鏉′骇鍝� |
+| 瀹㈡埛鏁版嵁 | 1 | 缂哄皯瀹㈡埛鏁版嵁锛岃鍏堝湪銆愬熀纭�鏁版嵁-瀹㈡埛绠$悊銆戜腑娣诲姞鑷冲皯1鏉″鎴� |
+| 鎶ヤ环瀹℃壒妯℃澘 | 1 | 缂哄皯鎶ヤ环瀹℃壒妯℃澘锛岃鍏堝湪銆愮郴缁熺鐞�-瀹℃壒妯℃澘銆戜腑鍒涘缓鎶ヤ环瀹℃壒妯℃澘 |
+| 鍙戣揣瀹℃壒妯℃澘 | 1 | 缂哄皯鍙戣揣瀹℃壒妯℃澘锛岃鍏堝湪銆愮郴缁熺鐞�-瀹℃壒妯℃澘銆戜腑鍒涘缓鍙戣揣瀹℃壒妯℃澘 |
+
+### 閲囪喘妯″潡 (purchase)
+| 妫�娴嬮」 | 鏈�浣庢暟閲� | 鏈�氳繃鎻愮ず |
+|--------|----------|------------|
+| 浜у搧鏁版嵁 | 1 | 缂哄皯浜у搧鏁版嵁锛岃鍏堝湪銆愬熀纭�鏁版嵁-浜у搧绠$悊銆戜腑娣诲姞鑷冲皯1鏉′骇鍝� |
+| 渚涘簲鍟嗘暟鎹� | 1 | 缂哄皯渚涘簲鍟嗘暟鎹紝璇峰厛鍦ㄣ�愬熀纭�鏁版嵁-渚涘簲鍟嗙鐞嗐�戜腑娣诲姞鑷冲皯1鏉′緵搴斿晢 |
+| 閲囪喘瀹℃壒妯℃澘 | 1 | 缂哄皯閲囪喘瀹℃壒妯℃澘锛岃鍏堝湪銆愮郴缁熺鐞�-瀹℃壒妯℃澘銆戜腑鍒涘缓閲囪喘瀹℃壒妯℃澘 |
+
+### 鐢熶骇妯″潡 (production)
+| 妫�娴嬮」 | 鏈�浣庢暟閲� | 鏈�氳繃鎻愮ず |
+|--------|----------|------------|
+| 浜у搧鏁版嵁 | 1 | 缂哄皯浜у搧鏁版嵁锛岃鍏堝湪銆愬熀纭�鏁版嵁-浜у搧绠$悊銆戜腑娣诲姞鑷冲皯1鏉′骇鍝� |
+| 浜у搧瑙勬牸 | 1 | 缂哄皯浜у搧瑙勬牸锛岃鍏堝湪銆愬熀纭�鏁版嵁-浜у搧绠$悊銆戜腑涓轰骇鍝佹坊鍔犺鏍煎瀷鍙� |
+| 宸ュ簭 | 1 | 缂哄皯宸ュ簭锛岃鍏堝湪銆愬伐鑹鸿璁�-宸ュ簭绠$悊銆戜腑鍒涘缓宸ュ簭 |
+| BOM | 1 | 缂哄皯BOM锛岃鍏堝湪銆愬伐鑹鸿璁�-BOM绠$悊銆戜腑鍒涘缓BOM |
+| BOM浜у搧缁撴瀯 | 1 | 缂哄皯BOM浜у搧缁撴瀯锛岃鍏堝湪銆愬伐鑹鸿璁�-BOM绠$悊銆戜腑涓築OM娣诲姞浜у搧缁撴瀯鑺傜偣 |
+| 宸ヨ壓璺嚎 | 1 | 缂哄皯宸ヨ壓璺嚎锛岃鍏堝湪銆愬伐鑹鸿璁�-宸ヨ壓璺嚎銆戜腑鍒涘缓宸ヨ壓璺嚎 |
+| 宸ヨ壓璺嚎宸ュ簭 | 1 | 缂哄皯宸ヨ壓璺嚎宸ュ簭锛岃鍏堝湪銆愬伐鑹鸿璁�-宸ヨ壓璺嚎銆戜腑涓哄伐鑹鸿矾绾挎坊鍔犲伐搴� |
+| 浜у搧-宸ヨ壓璺嚎鍏宠仈 | 鈮�1 | 閮ㄥ垎浜у搧瑙勬牸鏈叧鑱斿伐鑹鸿矾绾匡紝璇峰厛鍦ㄣ�愬伐鑹鸿璁�-宸ヨ壓璺嚎銆戜腑涓轰骇鍝佽鏍煎垱寤哄伐鑹鸿矾绾� |
+| 宸ヨ壓璺嚎-宸ュ簭鍏宠仈 | 鈮�1 | 閮ㄥ垎宸ヨ壓璺嚎鏈坊鍔犲伐搴忥紝璇峰厛鍦ㄣ�愬伐鑹鸿璁�-宸ヨ壓璺嚎銆戜腑涓哄伐鑹鸿矾绾挎坊鍔犲伐搴� |
+| BOM-浜у搧缁撴瀯鍏宠仈 | 鈮�1 | 閮ㄥ垎BOM鏈坊鍔犱骇鍝佺粨鏋勶紝璇峰厛鍦ㄣ�愬伐鑹鸿璁�-BOM绠$悊銆戜腑涓築OM娣诲姞浜у搧缁撴瀯鑺傜偣 |
+
+### 璐ㄩ噺妯″潡 (quality)
+| 妫�娴嬮」 | 鏈�浣庢暟閲� | 鏈�氳繃鎻愮ず |
+|--------|----------|------------|
+| 浜у搧鏁版嵁 | 1 | 缂哄皯浜у搧鏁版嵁锛岃鍏堝湪銆愬熀纭�鏁版嵁-浜у搧绠$悊銆戜腑娣诲姞鑷冲皯1鏉′骇鍝� |
+| 妫�娴嬫爣鍑� | 1 | 缂哄皯妫�娴嬫爣鍑嗭紝璇峰厛鍦ㄣ�愯川閲忕鐞�-妫�娴嬫爣鍑嗐�戜腑鍒涘缓妫�娴嬫爣鍑� |
+| 鎸囨爣缁戝畾 | 1 | 缂哄皯鎸囨爣缁戝畾锛岃鍏堝湪銆愯川閲忕鐞�-妫�娴嬫爣鍑嗙粦瀹氥�戜腑灏嗘娴嬫爣鍑嗕笌浜у搧缁戝畾 |
+
+## 鍓嶇淇敼鐐�
+
+### 1. 鏁版嵁妯℃嫙椤甸潰鍏ュ彛
+
+鍦ㄥ乏渚ц彍鍗曟柊澧�"鏁版嵁妯℃嫙"鑿滃崟椤癸紝璺敱 `/mock`銆�
+
+### 2. 鏁版嵁妫�娴嬪尯鍩�
+
+```html
+<template>
+ <div class="mock-container">
+ <el-card header="鍩虹鏁版嵁妫�娴�">
+ <el-checkbox-group v-model="selectedModules">
+ <el-checkbox label="sales">閿�鍞ā鍧�</el-checkbox>
+ <el-checkbox label="purchase">閲囪喘妯″潡</el-checkbox>
+ <el-checkbox label="quality">璐ㄩ噺妯″潡</el-checkbox>
+ <el-checkbox label="production">鐢熶骇妯″潡</el-checkbox>
+ </el-checkbox-group>
+ <el-button type="primary" @click="handleCheck" :loading="checking">
+ 寮�濮嬫娴�
+ </el-button>
+ </el-card>
+
+ <el-card v-if="checkResult" header="妫�娴嬬粨鏋�" style="margin-top: 16px">
+ <el-alert
+ :title="`閫氳繃 ${checkResult.passedItems} / ${checkResult.totalItems} 椤筦"
+ :type="checkResult.passedItems === checkResult.totalItems ? 'success' : 'warning'"
+ :closable="false"
+ show-icon
+ />
+ <el-table :data="checkResult.items" style="margin-top: 12px">
+ <el-table-column prop="module" label="妯″潡" width="100" />
+ <el-table-column prop="itemName" label="妫�娴嬮」" width="160" />
+ <el-table-column prop="minRequired" label="鏈�浣庤姹�" width="80" />
+ <el-table-column prop="currentCount" label="褰撳墠鏁伴噺" width="80" />
+ <el-table-column label="鐘舵��" width="80">
+ <template #default="{ row }">
+ <el-tag :type="row.passed ? 'success' : 'danger'">
+ {{ row.passed ? '閫氳繃' : '鏈�氳繃' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="message" label="鎻愮ず" />
+ </el-table>
+ </el-card>
+ </div>
+</template>
+```
+
+### 3. data 鏁版嵁
+
+```js
+data() {
+ return {
+ selectedModules: ['sales', 'purchase', 'quality', 'production'],
+ checking: false,
+ checkResult: null,
+ }
+}
+```
+
+### 4. 鏂规硶
+
+```js
+import request from '@/utils/request'
+
+methods: {
+ async handleCheck() {
+ if (this.selectedModules.length === 0) {
+ this.$message.warning('璇疯嚦灏戦�夋嫨涓�涓ā鍧�')
+ return
+ }
+ this.checking = true
+ try {
+ const res = await request.post('/mock/dataCheck', {
+ modules: this.selectedModules
+ })
+ this.checkResult = res.data
+ } finally {
+ this.checking = false
+ }
+ }
+}
+```
+
+## 娉ㄦ剰浜嬮」
+
+- 浜у搧鏁版嵁锛堝叕鍏卞熀纭�锛夊湪澶氫釜妯″潡闂村叡浜紝妫�娴嬬粨鏋滀腑鍙嚭鐜颁竴娆★紝涓嶄細閲嶅灞曠ず
+- 妫�娴嬩粎鍋氬彧璇绘煡璇紝涓嶅啓鍏ヤ换浣曟暟鎹�
+- 妯″潡鍙傛暟涓虹┖鏁扮粍鏃惰繑鍥炵┖妫�娴嬪垪琛�
+- 寤鸿鍦ㄦ暟鎹ā鎷熷紑濮嬪墠鍏堣皟鐢ㄦ鎺ュ彛纭鍩虹鏁版嵁灏辩华
+- 鐢熶骇妯″潡闄や簡妫�娴嬫暟鎹噺锛岃繕浼氭娴嬪叧鑱旀�э紙浜у搧-宸ヨ壓璺嚎銆佸伐鑹鸿矾绾�-宸ュ簭銆丅OM-浜у搧缁撴瀯锛夛紝纭繚宸ヨ壓璁捐鏁版嵁瀹屾暣
diff --git a/doc/mock_data_generate.md b/doc/mock_data_generate.md
new file mode 100644
index 0000000..6dc801e
--- /dev/null
+++ b/doc/mock_data_generate.md
@@ -0,0 +1,243 @@
+# 鏁版嵁妯℃嫙 - AI 鏁版嵁鐢熸垚
+
+## 娑夊強椤甸潰
+
+- 鏁版嵁妯℃嫙鎿嶄綔椤甸潰锛堟柊澧�/淇敼锛�
+
+## API
+
+| 鏂规硶 | 璺緞 | 璇存槑 |
+|------|------|------|
+| POST | /mock/generate | AI 鐢熸垚妯℃嫙鏁版嵁锛堝惈鍓嶇疆鍩虹鏁版嵁妫�娴嬶級 |
+
+**璇锋眰鍙傛暟锛�**
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| modules | List\<String\> | 鏄� | 瑕佺敓鎴愭暟鎹殑妯″潡锛歴ales锛堥攢鍞級銆乸urchase锛堥噰璐級銆乹uality锛堣川閲忥級銆乸roduction锛堢敓浜э級銆乻tock锛堝簱瀛橈級 |
+| industries | List\<String\> | 鏄� | 琛屼笟锛屽锛歔"鏈烘鍒堕��", "椋熷搧鍔犲伐", "鐢靛瓙瑁呴厤"] |
+| countMin | int | 鍚� | 姣忎釜瀹炰綋鐢熸垚鏉℃暟鏈�灏忓�硷紝榛樿 3 |
+| countMax | int | 鍚� | 姣忎釜瀹炰綋鐢熸垚鏉℃暟鏈�澶у�硷紝榛樿 10 |
+| dateStart | String | 鍚� | 鏃堕棿鑼冨洿寮�濮嬶紙yyyy-MM-dd锛� |
+| dateEnd | String | 鍚� | 鏃堕棿鑼冨洿缁撴潫锛坹yyy-MM-dd锛� |
+| additionalInfo | String | 鍚� | 鍏朵粬琛ュ厖鎻忚堪锛屽"浜у搧浠ラ噾灞為浂閮ㄤ欢涓轰富" |
+
+璇锋眰浣撶ず渚嬶細
+```json
+{
+ "modules": ["sales", "purchase", "quality"],
+ "industries": ["鏈烘鍒堕��", "鐢靛瓙瑁呴厤"],
+ "countMin": 3,
+ "countMax": 8,
+ "dateStart": "2026-01-01",
+ "dateEnd": "2026-06-01",
+ "additionalInfo": "浜у搧浠ラ噾灞為浂閮ㄤ欢鍜岀數瀛愬厓鍣ㄤ欢涓轰富"
+}
+```
+
+**鍝嶅簲锛堟垚鍔燂級锛�**
+```json
+{
+ "code": 200,
+ "msg": "鎿嶄綔鎴愬姛",
+ "data": {
+ "status": "SUCCESS",
+ "totalGenerated": 24,
+ "moduleSummaries": [
+ { "module": "sales", "entityName": "瀹㈡埛", "generatedCount": 5, "successCount": 5, "failCount": 0 },
+ { "module": "sales", "entityName": "閿�鍞彴璐�", "generatedCount": 5, "successCount": 5, "failCount": 0 },
+ { "module": "purchase", "entityName": "渚涘簲鍟�", "generatedCount": 4, "successCount": 4, "failCount": 0 },
+ { "module": "purchase", "entityName": "閲囪喘鍙拌处", "generatedCount": 4, "successCount": 4, "failCount": 0 },
+ { "module": "quality", "entityName": "妫�娴嬫爣鍑�", "generatedCount": 3, "successCount": 3, "failCount": 0 },
+ { "module": "quality", "entityName": "鎸囨爣缁戝畾", "generatedCount": 3, "successCount": 3, "failCount": 0 }
+ ],
+ "errors": [],
+ "checkResult": null
+ }
+}
+```
+
+**鍝嶅簲锛堝熀纭�鏁版嵁涓嶈冻锛屽墠缃娴嬫湭閫氳繃锛夛細**
+```json
+{
+ "code": 500,
+ "msg": "鍩虹鏁版嵁涓嶈冻锛岃鍏堣ˉ鍏呭悗鍐嶇敓鎴�",
+ "data": {
+ "status": "CHECK_FAILED",
+ "checkResult": {
+ "totalItems": 6,
+ "passedItems": 3,
+ "items": [
+ { "module": "common", "itemName": "浜у搧鏁版嵁", "passed": true, "message": "閫氳繃" },
+ { "module": "sales", "itemName": "瀹㈡埛鏁版嵁", "passed": false, "message": "缂哄皯瀹㈡埛鏁版嵁锛岃鍏�..." }
+ ]
+ }
+ }
+}
+```
+
+## 宸ヤ綔娴佺▼
+
+1. 鐢ㄦ埛鍦ㄩ〉闈㈤�夋嫨妯″潡 + 濉啓琛屼笟/鏁伴噺/鏃堕棿绛変俊鎭�
+2. 璋冪敤 `POST /mock/generate`
+3. 鍚庣棣栧厛璋冪敤 `POST /mock/dataCheck` 妫�娴嬪熀纭�鏁版嵁鏄惁灏辩华
+4. 鑻ュ熀纭�鏁版嵁涓嶈冻锛岃繑鍥炴娴嬬粨鏋滐紝鎻愮ず鐢ㄦ埛鍏堣ˉ榻�
+5. 鑻ュ熀纭�鏁版嵁灏辩华锛岃皟鐢� AI 澶фā鍨嬬敓鎴愮鍚堣涓氱壒寰佺殑 JSON 鏁版嵁
+6. 鎸変緷璧栭『搴忓垱寤烘暟鎹紙瀹㈡埛鈫掗攢鍞彴璐︺�佷緵搴斿晢鈫掗噰璐彴璐︺�佹娴嬫爣鍑嗏啋鎸囨爣缁戝畾...锛�
+7. 杩斿洖鐢熸垚鎽樿
+
+## 鍓嶇淇敼鐐�
+
+### 1. 鏁版嵁鐢熸垚鍖哄煙锛堝湪鏁版嵁妫�娴嬪尯鍩熶笅鏂癸級
+
+```html
+<el-card header="AI 鏁版嵁鐢熸垚" style="margin-top: 16px">
+ <el-form :model="generateForm" label-width="100px">
+ <el-form-item label="閫夋嫨妯″潡">
+ <el-checkbox-group v-model="generateForm.modules">
+ <el-checkbox label="sales">閿�鍞�</el-checkbox>
+ <el-checkbox label="purchase">閲囪喘</el-checkbox>
+ <el-checkbox label="quality">璐ㄩ噺</el-checkbox>
+ <el-checkbox label="production">鐢熶骇</el-checkbox>
+ <el-checkbox label="stock">搴撳瓨</el-checkbox>
+ </el-checkbox-group>
+ </el-form-item>
+
+ <el-form-item label="琛屼笟">
+ <el-select
+ v-model="generateForm.industries"
+ multiple
+ filterable
+ allow-create
+ placeholder="杈撳叆鎴栭�夋嫨琛屼笟"
+ >
+ <el-option label="鏈烘鍒堕��" value="鏈烘鍒堕��" />
+ <el-option label="椋熷搧鍔犲伐" value="椋熷搧鍔犲伐" />
+ <el-option label="鐢靛瓙瑁呴厤" value="鐢靛瓙瑁呴厤" />
+ <el-option label="姹借溅闆堕儴浠�" value="姹借溅闆堕儴浠�" />
+ <el-option label="鍖荤枟鍣ㄦ" value="鍖荤枟鍣ㄦ" />
+ <el-option label="鍖栧伐鏉愭枡" value="鍖栧伐鏉愭枡" />
+ <el-option label="绾虹粐鏈嶈" value="绾虹粐鏈嶈" />
+ <el-option label="瀹跺叿鍒堕��" value="瀹跺叿鍒堕��" />
+ </el-select>
+ </el-form-item>
+
+ <el-row :gutter="16">
+ <el-col :span="12">
+ <el-form-item label="鐢熸垚鏉℃暟">
+ <el-slider
+ v-model="generateForm.countRange"
+ range
+ :min="1"
+ :max="50"
+ :marks="{1:'1', 10:'10', 20:'20', 50:'50'}"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏃堕棿鑼冨洿">
+ <el-date-picker
+ v-model="generateForm.dateRange"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ value-format="yyyy-MM-dd"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-form-item label="琛ュ厖淇℃伅">
+ <el-input
+ v-model="generateForm.additionalInfo"
+ type="textarea"
+ :rows="2"
+ placeholder="濡傦細浜у搧浠ラ噾灞為浂閮ㄤ欢涓轰富锛屽鎴烽泦涓湪鍗庝笢鍦板尯"
+ />
+ </el-form-item>
+
+ <el-form-item>
+ <el-button type="primary" @click="handleGenerate" :loading="generating">
+ 寮�濮嬬敓鎴�
+ </el-button>
+ <el-button @click="handleCheckFirst" :loading="checking">
+ 鍏堟娴嬪啀鐢熸垚
+ </el-button>
+ </el-form-item>
+ </el-form>
+</el-card>
+```
+
+### 3. data 鏁版嵁
+
+```js
+data() {
+ return {
+ generateForm: {
+ modules: [],
+ industries: [],
+ countRange: [3, 10],
+ dateRange: [],
+ additionalInfo: '',
+ },
+ generating: false,
+ generateResult: null,
+ }
+}
+```
+
+### 4. 鏂规硶
+
+```js
+methods: {
+ // 鐩存帴鐢熸垚锛堝唴閮ㄨ嚜鍔ㄦ娴嬶級
+ async handleGenerate() {
+ if (this.generateForm.modules.length === 0) {
+ this.$message.warning('璇疯嚦灏戦�夋嫨涓�涓ā鍧�')
+ return
+ }
+ this.generating = true
+ try {
+ const res = await request.post('/mock/generate', {
+ modules: this.generateForm.modules,
+ industries: this.generateForm.industries,
+ countMin: this.generateForm.countRange[0],
+ countMax: this.generateForm.countRange[1],
+ dateStart: this.generateForm.dateRange[0] || null,
+ dateEnd: this.generateForm.dateRange[1] || null,
+ additionalInfo: this.generateForm.additionalInfo,
+ })
+ if (res.code === 200) {
+ this.generateResult = res.data
+ this.$message.success(`鎴愬姛鐢熸垚 ${res.data.totalGenerated} 鏉℃暟鎹甡)
+ } else {
+ // 鍩虹鏁版嵁涓嶈冻
+ this.generateResult = res.data
+ this.checkResult = res.data.checkResult
+ this.$message.warning(res.msg)
+ }
+ } finally {
+ this.generating = false
+ }
+ },
+
+ // 鍏堟娴嬪啀鐢熸垚
+ async handleCheckFirst() {
+ await this.handleCheck()
+ if (this.checkResult && this.checkResult.passedItems === this.checkResult.totalItems) {
+ await this.handleGenerate()
+ } else {
+ this.$message.warning('璇峰厛琛ラ綈鍩虹鏁版嵁鍚庡啀鐢熸垚')
+ }
+ },
+}
+```
+
+## 娉ㄦ剰浜嬮」
+
+- `POST /mock/generate` 鍐呴儴浼氳嚜鍔ㄨ皟鐢ㄦ暟鎹娴嬶紝涓嶉渶瑕佹墜鍔ㄥ垎涓ゆ璧�
+- AI 鐢熸垚鐨勬暟鎹細灏介噺绗﹀悎琛屼笟鐗瑰緛锛屼絾寤鸿鐢熸垚鍚庢鏌ュ叧閿暟鎹殑鍚堢悊鎬�
+- 鐢熶骇妯″潡鍜屽簱瀛樻ā鍧椾緷璧栦骇鍝佹暟鎹紙Product/ProductModel锛夛紝璇风‘淇濆熀纭�鏁版嵁涓湁浜у搧
+- 閿�鍞彴璐︾殑鍚堝悓閲戦鐢变骇鍝佹槑缁嗚嚜鍔ㄨ绠楋紝AI 鍙渶鐢熸垚浜у搧琛岀殑鍗曚环鍜屾暟閲�
+- 浜х敓鐨勬暟鎹�氳繃鏈嶅姟灞傚垱寤猴紝浼氳嚜鍔ㄥ~鍏� tenantId/deptId/createUser 绛夊瓧娈�
diff --git "a/doc/\347\237\245\350\257\206\345\272\223RAG\345\212\237\350\203\275\345\256\236\347\216\260\346\226\207\346\241\243.md" "b/doc/\347\237\245\350\257\206\345\272\223RAG\345\212\237\350\203\275\345\256\236\347\216\260\346\226\207\346\241\243.md"
new file mode 100644
index 0000000..4a94cc5
--- /dev/null
+++ "b/doc/\347\237\245\350\257\206\345\272\223RAG\345\212\237\350\203\275\345\256\236\347\216\260\346\226\207\346\241\243.md"
@@ -0,0 +1,1034 @@
+# 鐭ヨ瘑搴揜AG鍚戦噺妫�绱㈠姛鑳藉疄鐜版枃妗�
+
+## 涓�銆佸姛鑳芥杩�
+
+鍩轰簬 RAG锛圧etrieval-Augmented Generation锛夋妧鏈疄鐜扮煡璇嗗簱闂瓟鍔熻兘锛屾敮鎸侊細
+- 鐭ヨ瘑搴撶鐞嗭紙CRUD锛�
+- 鏂囦欢涓婁紶涓庡悜閲忓寲澶勭悊
+- 鍩轰簬鍚戦噺妫�绱㈢殑鏅鸿兘闂瓟
+- 澶氱鏂囦欢鏍煎紡鏀寔锛坱xt銆乵d銆乨ocx銆亁lsx銆亁ls銆乸df锛�
+
+## 浜屻�佹妧鏈灦鏋�
+
+### 2.1 鎶�鏈爤
+| 缁勪欢 | 鎶�鏈� |
+|------|------|
+| 鍚戦噺鏁版嵁搴� | Pinecone |
+| Embedding妯″瀷 | 闃块噷浜� DashScope text-embedding-v3 |
+| LLM | 闃块噷浜戦�氫箟鍗冮棶 qwen-max |
+| 妗嗘灦 | langchain4j |
+| ORM | MyBatis-Plus |
+
+### 2.2 鏋舵瀯鍥�
+
+```
+鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� 鍓嶇搴旂敤 鈹�
+鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+ 鈹�
+ 鈻�
+鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� Controller Layer 鈹�
+鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+鈹� 鈹� KnowledgeBaseCtrl 鈹� 鈹� KnowledgeChatController 鈹� 鈹�
+鈹� 鈹� (鐭ヨ瘑搴撶鐞�) 鈹� 鈹� (鐭ヨ瘑搴撻棶绛�) 鈹� 鈹�
+鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+ 鈹�
+ 鈻�
+鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� Service Layer 鈹�
+鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+鈹� 鈹侹nowledgeBaseService 鈹� 鈹� KnowledgeRagService 鈹� 鈹�
+鈹� 鈹� (鐭ヨ瘑搴揅RUD) 鈹� 鈹� (鍚戦噺鍖�/妫�绱�) 鈹� 鈹�
+鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+ 鈹�
+ 鈻�
+鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� AI Layer 鈹�
+鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+鈹� 鈹� KnowledgeChatAgent 鈹� 鈹� EmbeddingStore (Pinecone) 鈹� 鈹�
+鈹� 鈹� (闂瓟Agent) 鈹� 鈹� (鍚戦噺瀛樺偍) 鈹� 鈹�
+鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+```
+
+---
+
+## 涓夈�佸悗绔疄鐜�
+
+### 3.1 鏁版嵁搴撹璁�
+
+#### 3.1.1 鐭ヨ瘑搴撹〃锛坘nowledge_base锛�
+
+```sql
+CREATE TABLE knowledge_base (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ title VARCHAR(255) COMMENT '鐭ヨ瘑鏍囬',
+ type VARCHAR(50) COMMENT '鐭ヨ瘑绫诲瀷',
+ scenario VARCHAR(255) COMMENT '閫傜敤鍦烘櫙',
+ efficiency VARCHAR(20) COMMENT '瑙e喅鏁堢巼',
+ problem TEXT COMMENT '闂鎻忚堪',
+ solution TEXT COMMENT '瑙e喅鏂规',
+ key_points TEXT COMMENT '鍏抽敭瑕佺偣',
+ creator VARCHAR(100) COMMENT '鍒涘缓浜�',
+ usage_count INT DEFAULT 0 COMMENT '浣跨敤娆℃暟',
+ file_count INT DEFAULT 0 COMMENT '鏂囦欢鏁伴噺',
+ total_chunk_count INT DEFAULT 0 COMMENT '鎬诲垏鐗囨暟閲�',
+ description VARCHAR(500) COMMENT '鐭ヨ瘑搴撴弿杩�',
+ create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
+ update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ create_user INT,
+ update_user INT,
+ tenant_id BIGINT,
+ dept_id BIGINT
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='鐭ヨ瘑搴撹〃';
+```
+
+#### 3.1.2 鐭ヨ瘑搴撳悜閲忚褰曡〃锛坘nowledge_base_vector锛�
+
+```sql
+CREATE TABLE knowledge_base_vector (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '涓婚敭ID',
+ knowledge_base_id BIGINT NOT NULL COMMENT '鍏宠仈鐭ヨ瘑搴揑D',
+ storage_blob_id BIGINT NOT NULL COMMENT '鍏宠仈鏂囦欢blob ID',
+ file_name VARCHAR(255) NOT NULL COMMENT '鏂囦欢鍚嶇О',
+ file_type VARCHAR(50) NOT NULL COMMENT '鏂囦欢绫诲瀷',
+ vector_status TINYINT DEFAULT 0 COMMENT '鍚戦噺鍖栫姸鎬�: 0-寰呭鐞�, 1-澶勭悊涓�, 2-宸插畬鎴�, 3-澶辫触',
+ vector_error VARCHAR(500) COMMENT '鍚戦噺鍖栧け璐ュ師鍥�',
+ chunk_count INT DEFAULT 0 COMMENT '鍒囩墖鏁伴噺',
+ namespace VARCHAR(100) COMMENT '鍚戦噺鍛藉悕绌洪棿',
+ create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
+ create_user INT,
+ update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ update_user INT,
+ tenant_id BIGINT,
+ dept_id BIGINT,
+ INDEX idx_knowledge_base_id (knowledge_base_id),
+ INDEX idx_storage_blob_id (storage_blob_id),
+ INDEX idx_vector_status (vector_status)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='鐭ヨ瘑搴撴枃浠跺悜閲忚褰曡〃';
+```
+
+### 3.2 Maven渚濊禆
+
+```xml
+<!-- langchain4j BOM -->
+<dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>dev.langchain4j</groupId>
+ <artifactId>langchain4j-bom</artifactId>
+ <version>1.0.0-beta3</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+ </dependencies>
+</dependencyManagement>
+
+<dependencies>
+ <!-- langchain4j 鏍稿績 -->
+ <dependency>
+ <groupId>dev.langchain4j</groupId>
+ <artifactId>langchain4j-spring-boot-starter</artifactId>
+ </dependency>
+
+ <!-- Pinecone 鍚戦噺鏁版嵁搴� -->
+ <dependency>
+ <groupId>dev.langchain4j</groupId>
+ <artifactId>langchain4j-pinecone</artifactId>
+ </dependency>
+
+ <!-- 闃块噷浜� DashScope -->
+ <dependency>
+ <groupId>dev.langchain4j</groupId>
+ <artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
+ </dependency>
+</dependencies>
+```
+
+### 3.3 閰嶇疆鏂囦欢锛坅pplication.yml锛�
+
+```yaml
+# Pinecone 鍚戦噺鏁版嵁搴撻厤缃�
+pinecone:
+ api-key: your-pinecone-api-key
+ index: your-index-name
+ namespace: knowledge-base
+
+# langchain4j 閰嶇疆
+langchain4j:
+ community:
+ dashscope:
+ streaming-chat-model:
+ api-key: your-dashscope-api-key
+ model-name: "qwen-max"
+ embedding-model:
+ api-key: your-dashscope-api-key
+ model-name: "text-embedding-v3"
+```
+
+### 3.4 鏍稿績浠g爜瀹炵幇
+
+#### 3.4.1 瀹炰綋绫�
+
+**KnowledgeBase.java**
+```java
+@Data
+@TableName("knowledge_base")
+public class KnowledgeBase implements Serializable {
+ @TableId(type = IdType.AUTO)
+ private Long id;
+ private String title;
+ private String type;
+ private String scenario;
+ private String efficiency;
+ private String problem;
+ private String solution;
+ private String keyPoints;
+ private String creator;
+ private Integer usageCount;
+ private Integer fileCount;
+ private Integer totalChunkCount;
+ private String description;
+ @TableField(fill = FieldFill.INSERT)
+ private LocalDateTime createTime;
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private LocalDateTime updateTime;
+ @TableField(fill = FieldFill.INSERT)
+ private Integer createUser;
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private Integer updateUser;
+ @TableField(fill = FieldFill.INSERT)
+ private Long tenantId;
+ @TableField(fill = FieldFill.INSERT)
+ private Long deptId;
+}
+```
+
+**KnowledgeBaseVector.java**
+```java
+@Data
+@TableName("knowledge_base_vector")
+public class KnowledgeBaseVector implements Serializable {
+ @TableId(type = IdType.AUTO)
+ private Long id;
+ private Long knowledgeBaseId;
+ private Long storageBlobId;
+ private String fileName;
+ private String fileType;
+ private Integer vectorStatus;
+ private String vectorError;
+ private Integer chunkCount;
+ private String namespace;
+ @TableField(fill = FieldFill.INSERT)
+ private LocalDateTime createTime;
+ @TableField(fill = FieldFill.INSERT)
+ private Integer createUser;
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private LocalDateTime updateTime;
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private Integer updateUser;
+ @TableField(fill = FieldFill.INSERT)
+ private Long tenantId;
+ @TableField(fill = FieldFill.INSERT)
+ private Long deptId;
+
+ // 鍚戦噺鍖栫姸鎬佸父閲�
+ public static final int STATUS_PENDING = 0;
+ public static final int STATUS_PROCESSING = 1;
+ public static final int STATUS_COMPLETED = 2;
+ public static final int STATUS_FAILED = 3;
+}
+```
+
+#### 3.4.2 EmbeddingStore閰嶇疆
+
+**EmbeddingStoreConfig.java**
+```java
+@Configuration
+public class EmbeddingStoreConfig {
+
+ @Value("${pinecone.api-key}")
+ private String pineconeApiKey;
+
+ @Value("${pinecone.index}")
+ private String indexName;
+
+ @Value("${pinecone.namespace}")
+ private String namespace;
+
+ @Bean
+ public Pinecone pinecone() {
+ return new Pinecone.Builder(pineconeApiKey).build();
+ }
+
+ @Bean
+ public Index pineconeIndex(Pinecone pinecone) {
+ return pinecone.getIndexConnection(indexName);
+ }
+
+ @Bean
+ public EmbeddingStore<TextSegment> embeddingStore(EmbeddingModel embeddingModel) {
+ return PineconeEmbeddingStore.builder()
+ .apiKey(pineconeApiKey)
+ .index(indexName)
+ .nameSpace(namespace)
+ .createIndex(PineconeServerlessIndexConfig.builder()
+ .cloud("AWS")
+ .region("us-east-1")
+ .dimension(embeddingModel.dimension())
+ .build())
+ .build();
+ }
+}
+```
+
+#### 3.4.3 RAG鏈嶅姟瀹炵幇
+
+**KnowledgeRagService.java**
+```java
+public interface KnowledgeRagService {
+ void processVectorAsync(Long vectorId);
+ void processVector(Long vectorId);
+ List<String> searchRelevantContent(String namespace, String query, int maxResults);
+ void deleteEmbeddings(String namespace, Long storageBlobId);
+}
+```
+
+**KnowledgeRagServiceImpl.java**锛堟牳蹇冨疄鐜帮級
+```java
+@Slf4j
+@Service
+public class KnowledgeRagServiceImpl implements KnowledgeRagService {
+
+ private final KnowledgeBaseVectorService knowledgeBaseVectorService;
+ private final StorageBlobService storageBlobService;
+ private final EmbeddingModel embeddingModel;
+ private final EmbeddingStore<TextSegment> embeddingStore;
+ private final FileProperties fileProperties;
+ private final Index pineconeIndex;
+
+ @Value("${pinecone.namespace}")
+ private String namespace;
+
+ private static final int CHUNK_SIZE = 500;
+ private static final int CHUNK_OVERLAP = 100;
+ private static final long CHUNK_THRESHOLD_BYTES = 80L * 1024 * 1024;
+ private static final int EMBEDDING_MAX_LENGTH = 8000;
+
+ @Override
+ @Async("threadPoolTaskExecutor")
+ public void processVectorAsync(Long vectorId) {
+ processVector(vectorId);
+ }
+
+ @Override
+ public void processVector(Long vectorId) {
+ KnowledgeBaseVector vector = knowledgeBaseVectorService.getById(vectorId);
+ if (vector == null) return;
+
+ try {
+ // 鏇存柊鐘舵�佷负澶勭悊涓�
+ knowledgeBaseVectorService.updateVectorStatus(vectorId, STATUS_PROCESSING, null, null);
+
+ // 鑾峰彇鏂囦欢鍐呭
+ StorageBlob blob = storageBlobService.getById(vector.getStorageBlobId());
+ File file = getFile(blob);
+ String content = extractFileContent(file, vector.getFileName());
+
+ if (content == null || content.trim().isEmpty()) {
+ throw new RuntimeException("鏂囦欢鍐呭涓虹┖");
+ }
+
+ // 鏂囨湰鍒囩墖
+ List<TextSegment> chunks;
+ boolean needChunk = file.length() > CHUNK_THRESHOLD_BYTES || content.length() > EMBEDDING_MAX_LENGTH;
+ if (needChunk) {
+ chunks = splitText(content, vector);
+ } else {
+ Map<String, Object> metadata = buildMetadata(vector);
+ chunks = List.of(TextSegment.from(content, new Metadata(metadata)));
+ }
+
+ // 鐢熸垚宓屽叆鍚戦噺骞跺瓨鍌�
+ int chunkCount = 0;
+ for (TextSegment chunk : chunks) {
+ Embedding embedding = embeddingModel.embed(chunk).content();
+ embeddingStore.add(embedding, chunk);
+ chunkCount++;
+ }
+
+ // 鏇存柊鐘舵�佷负瀹屾垚
+ knowledgeBaseVectorService.updateVectorStatus(vectorId, STATUS_COMPLETED, chunkCount, null);
+
+ } catch (Exception e) {
+ log.error("鍚戦噺鍖栧鐞嗗け璐�", e);
+ knowledgeBaseVectorService.updateVectorStatus(vectorId, STATUS_FAILED, null, e.getMessage());
+ }
+ }
+
+ @Override
+ public List<String> searchRelevantContent(String namespace, String query, int maxResults) {
+ Embedding queryEmbedding = embeddingModel.embed(query).content();
+ EmbeddingSearchRequest searchRequest = EmbeddingSearchRequest.builder()
+ .queryEmbedding(queryEmbedding)
+ .maxResults(maxResults)
+ .minScore(0.7)
+ .build();
+ EmbeddingSearchResult<TextSegment> searchResult = embeddingStore.search(searchRequest);
+ return searchResult.matches().stream()
+ .map(match -> match.embedded().text())
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public void deleteEmbeddings(String namespace, Long storageBlobId) {
+ Struct filter = Struct.newBuilder()
+ .putFields("storageBlobId", Value.newBuilder()
+ .setStructValue(Struct.newBuilder()
+ .putFields("$eq", Value.newBuilder()
+ .setNumberValue(storageBlobId.doubleValue())
+ .build()))
+ .build())
+ .build();
+ pineconeIndex.delete(new ArrayList<>(), false, this.namespace, filter);
+ }
+
+ private String extractFileContent(File file, String fileName) throws Exception {
+ String ext = getFileExtension(fileName);
+ if (isPlainText(ext)) {
+ return readFileWithEncoding(file);
+ }
+ if ("docx".equals(ext)) {
+ return extractDocx(file);
+ }
+ if ("xlsx".equals(ext) || "xls".equals(ext)) {
+ return extractExcel(file);
+ }
+ return readFileWithEncoding(file);
+ }
+
+ // ... 鍏朵粬杈呭姪鏂规硶
+}
+```
+
+#### 3.4.4 鐭ヨ瘑搴撻棶绛擜gent
+
+**KnowledgeChatAgent.java**
+```java
+@AiService(
+ wiringMode = EXPLICIT,
+ streamingChatModel = "qwenStreamingChatModel",
+ chatMemoryProvider = "chatMemoryProvider"
+)
+public interface KnowledgeChatAgent {
+
+ @SystemMessage("""
+ 浣犳槸浼佷笟鐭ヨ瘑搴撻棶绛斿姪鎵嬨��
+ 浣犻渶瑕佸熀浜庢彁渚涚殑鐭ヨ瘑搴撳唴瀹瑰洖绛旂敤鎴烽棶棰樸��
+ 閬靛惊浠ヤ笅瑙勫垯锛�
+ 1. 涓ユ牸鍩轰簬鐭ヨ瘑搴撳唴瀹瑰洖绛旓紝涓嶈缂栭�犱俊鎭�
+ 2. 濡傛灉鐭ヨ瘑搴撲腑娌℃湁鐩稿叧淇℃伅锛屾槑纭憡鐭ョ敤鎴�
+ 3. 鍥炵瓟瑕佸噯纭�佺畝娲併�佹湁鏉$悊
+ 4. 寮曠敤鏉ユ簮鏃舵敞鏄�"鏍规嵁鐭ヨ瘑搴撳唴瀹�"
+ """)
+ Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage);
+}
+```
+
+#### 3.4.5 Controller灞�
+
+**KnowledgeBaseController.java**
+```java
+@RestController
+@RequestMapping("/knowledgeBase")
+@Tag(name = "鐭ヨ瘑搴撶鐞�")
+public class KnowledgeBaseController {
+
+ @GetMapping("/getList")
+ public AjaxResult getList(@RequestParam(defaultValue = "1") long current,
+ @RequestParam(defaultValue = "10") long size,
+ KnowledgeBase knowledgeBase) {
+ Page page = new Page(current, size);
+ return AjaxResult.success(knowledgeBaseService.listpage(page, knowledgeBase));
+ }
+
+ @PostMapping("/add")
+ public AjaxResult add(@RequestBody KnowledgeBase knowledgeBase) {
+ return AjaxResult.success(knowledgeBaseService.save(knowledgeBase));
+ }
+
+ @PostMapping("/update")
+ public AjaxResult update(@RequestBody KnowledgeBase knowledgeBase) {
+ return AjaxResult.success(knowledgeBaseService.updateById(knowledgeBase));
+ }
+
+ @DeleteMapping("/delete")
+ public AjaxResult delete(@RequestBody List<Long> ids) {
+ return AjaxResult.success(knowledgeBaseService.removeByIds(ids));
+ }
+
+ @GetMapping("/vector/status/{knowledgeBaseId}")
+ @Operation(summary = "鏌ヨ鐭ヨ瘑搴撴枃浠跺悜閲忓寲鐘舵��")
+ public AjaxResult getVectorStatus(@PathVariable Long knowledgeBaseId) {
+ return AjaxResult.success(knowledgeBaseVectorService.getVectorStatusByKnowledgeBaseId(knowledgeBaseId));
+ }
+
+ @PostMapping("/vector/reprocess/{vectorId}")
+ @Operation(summary = "閲嶆柊鍚戦噺鍖栨枃浠�")
+ public AjaxResult reprocessVector(@PathVariable Long vectorId) {
+ knowledgeBaseVectorService.reprocessVector(vectorId);
+ return AjaxResult.success("宸查噸鏂版彁浜ゅ悜閲忓寲浠诲姟");
+ }
+
+ @PostMapping("/file/save")
+ @Operation(summary = "淇濆瓨鐭ヨ瘑搴撴枃浠跺叧鑱�")
+ public AjaxResult saveKnowledgeBaseFiles(@RequestBody KnowledgeBaseFileDTO dto) {
+ // 淇濆瓨闄勪欢鍏宠仈骞惰Е鍙戝悜閲忓寲
+ // ...
+ }
+
+ @DeleteMapping("/file/delete")
+ @Operation(summary = "鍒犻櫎鐭ヨ瘑搴撴枃浠�")
+ public AjaxResult deleteKnowledgeBaseFiles(@RequestBody List<Long> vectorIds) {
+ knowledgeBaseVectorService.deleteVectors(vectorIds);
+ return AjaxResult.success();
+ }
+}
+```
+
+**KnowledgeChatController.java**
+```java
+@RestController
+@RequestMapping("/ai/knowledge")
+@Tag(name = "鐭ヨ瘑搴撻棶绛�")
+public class KnowledgeChatController {
+
+ private final KnowledgeChatAgent knowledgeChatAgent;
+ private final KnowledgeRagService knowledgeRagService;
+ private final KnowledgeBaseService knowledgeBaseService;
+
+ @PostMapping(value = "/chat", produces = "text/stream;charset=utf-8")
+ @Operation(summary = "鐭ヨ瘑搴撻棶绛�")
+ public Flux<String> chat(@RequestBody KnowledgeChatRequest request) {
+ // 妫�绱㈢浉鍏冲唴瀹�
+ String namespace = "kb-" + request.getKnowledgeBaseId();
+ List<String> relevantContents = knowledgeRagService.searchRelevantContent(
+ namespace, request.getQuestion(), 5);
+
+ if (relevantContents.isEmpty()) {
+ return Flux.just("鐭ヨ瘑搴撲腑鏈壘鍒扮浉鍏冲唴瀹�");
+ }
+
+ // 鏋勫缓涓婁笅鏂�
+ StringBuilder context = new StringBuilder();
+ context.append("浠ヤ笅鏄粠鐭ヨ瘑搴撲腑妫�绱㈠埌鐨勭浉鍏冲唴瀹癸細\n\n");
+ for (int i = 0; i < relevantContents.size(); i++) {
+ context.append("銆愬唴瀹�").append(i + 1).append("銆慭n");
+ context.append(relevantContents.get(i)).append("\n\n");
+ }
+ context.append("---\n璇峰熀浜庝互涓婄煡璇嗗簱鍐呭鍥炵瓟锛歕n").append(request.getQuestion());
+
+ return knowledgeChatAgent.chat(request.getMemoryId(), context.toString());
+ }
+
+ @GetMapping("/list")
+ @Operation(summary = "鐭ヨ瘑搴撳垪琛�")
+ public AjaxResult listKnowledgeBases() {
+ return AjaxResult.success(knowledgeBaseService.list());
+ }
+}
+```
+
+---
+
+## 鍥涖�丄PI鎺ュ彛鏂囨。
+
+### 4.1 鐭ヨ瘑搴撶鐞嗘帴鍙�
+
+| 鎺ュ彛 | 鏂规硶 | 璺緞 | 璇存槑 |
+|------|------|------|------|
+| 鑾峰彇鍒楄〃 | GET | /knowledgeBase/getList | 鍒嗛〉鏌ヨ鐭ヨ瘑搴撳垪琛� |
+| 鏂板鐭ヨ瘑搴� | POST | /knowledgeBase/add | 鍒涘缓鐭ヨ瘑搴� |
+| 鏇存柊鐭ヨ瘑搴� | POST | /knowledgeBase/update | 鏇存柊鐭ヨ瘑搴撲俊鎭� |
+| 鍒犻櫎鐭ヨ瘑搴� | DELETE | /knowledgeBase/delete | 鎵归噺鍒犻櫎鐭ヨ瘑搴� |
+| 鏌ヨ鍚戦噺鍖栫姸鎬� | GET | /knowledgeBase/vector/status/{id} | 鏌ヨ鏂囦欢鍚戦噺鍖栫姸鎬� |
+| 閲嶆柊鍚戦噺鍖� | POST | /knowledgeBase/vector/reprocess/{id} | 閲嶆柊澶勭悊澶辫触鐨勬枃浠� |
+| 淇濆瓨鏂囦欢鍏宠仈 | POST | /knowledgeBase/file/save | 涓婁紶鏂囦欢鍚庡叧鑱斿埌鐭ヨ瘑搴� |
+| 鍒犻櫎鏂囦欢 | DELETE | /knowledgeBase/file/delete | 鍒犻櫎鐭ヨ瘑搴撴枃浠� |
+
+### 4.2 鐭ヨ瘑搴撻棶绛旀帴鍙�
+
+| 鎺ュ彛 | 鏂规硶 | 璺緞 | 璇存槑 |
+|------|------|------|------|
+| 鐭ヨ瘑搴撻棶绛� | POST | /ai/knowledge/chat | 娴佸紡杩斿洖闂瓟缁撴灉 |
+| 鐭ヨ瘑搴撳垪琛� | GET | /ai/knowledge/list | 鑾峰彇鍙�夌煡璇嗗簱鍒楄〃 |
+
+### 4.3 鎺ュ彛璇︾粏璇存槑
+
+#### 4.3.1 淇濆瓨鐭ヨ瘑搴撴枃浠跺叧鑱�
+
+**璇锋眰**
+```json
+POST /knowledgeBase/file/save
+{
+ "knowledgeBaseId": 1,
+ "storageBlobIds": [100, 101, 102]
+}
+```
+
+**鍝嶅簲**
+```json
+{
+ "code": 200,
+ "msg": "鎿嶄綔鎴愬姛"
+}
+```
+
+#### 4.3.2 鐭ヨ瘑搴撻棶绛�
+
+**璇锋眰**
+```json
+POST /ai/knowledge/chat
+Content-Type: application/json
+
+{
+ "knowledgeBaseId": 1,
+ "memoryId": "session-uuid",
+ "question": "濡備綍澶勭悊搴撳瓨鐩樼偣宸紓锛�"
+}
+```
+
+**鍝嶅簲**锛圫SE娴佸紡锛�
+```
+鏍规嵁鐭ヨ瘑搴撳唴瀹癸紝搴撳瓨鐩樼偣宸紓鐨勫鐞嗘祦绋嬪涓嬶細
+
+1. 鍙戠幇宸紓鍚庯紝棣栧厛鏍稿鐩樼偣璁板綍...
+2. 妫�鏌ユ槸鍚︽湁婕忕洏鎴栭敊鐩�...
+3. ...
+```
+
+---
+
+## 浜斻�佸墠绔疄鐜�
+
+### 5.1 鐭ヨ瘑搴撶鐞嗛〉闈�
+
+```vue
+<template>
+ <div class="knowledge-base">
+ <!-- 鍒楄〃 -->
+ <el-table :data="tableData" border>
+ <el-table-column prop="title" label="鐭ヨ瘑鏍囬" />
+ <el-table-column prop="type" label="鐭ヨ瘑绫诲瀷" />
+ <el-table-column prop="fileCount" label="鏂囦欢鏁伴噺" />
+ <el-table-column prop="totalChunkCount" label="鍒囩墖鏁伴噺" />
+ <el-table-column label="鎿嶄綔">
+ <template #default="{ row }">
+ <el-button @click="handleEdit(row)">缂栬緫</el-button>
+ <el-button @click="handleFiles(row)">鏂囦欢绠$悊</el-button>
+ <el-button @click="handleChat(row)">闂瓟</el-button>
+ <el-button type="danger" @click="handleDelete(row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { getKnowledgeBaseList, deleteKnowledgeBase } from '@/api/knowledge'
+
+const tableData = ref([])
+
+const loadData = async () => {
+ const res = await getKnowledgeBaseList({ current: 1, size: 10 })
+ tableData.value = res.data.records
+}
+
+onMounted(loadData)
+</script>
+```
+
+### 5.2 鏂囦欢涓婁紶涓庡悜閲忓寲鐘舵��
+
+```vue
+<template>
+ <div class="file-manager">
+ <!-- 鏂囦欢涓婁紶 -->
+ <el-upload
+ :action="uploadUrl"
+ :on-success="handleUploadSuccess"
+ multiple
+ >
+ <el-button type="primary">涓婁紶鏂囦欢</el-button>
+ </el-upload>
+
+ <!-- 鏂囦欢鍒楄〃涓庡悜閲忓寲鐘舵�� -->
+ <el-table :data="fileList">
+ <el-table-column prop="fileName" label="鏂囦欢鍚�" />
+ <el-table-column label="鍚戦噺鍖栫姸鎬�">
+ <template #default="{ row }">
+ <el-tag :type="getStatusType(row.vectorStatus)">
+ {{ getStatusText(row.vectorStatus) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="chunkCount" label="鍒囩墖鏁�" />
+ <el-table-column label="鎿嶄綔">
+ <template #default="{ row }">
+ <el-button v-if="row.vectorStatus === 3" @click="reprocess(row)">
+ 閲嶆柊澶勭悊
+ </el-button>
+ <el-button type="danger" @click="deleteFile(row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+</template>
+
+<script setup>
+const uploadUrl = import.meta.env.VITE_APP_BASE_API + '/common/upload'
+
+// 涓婁紶鎴愬姛鍚庝繚瀛樺叧鑱�
+const uploadedBlobIds = ref([])
+
+const handleUploadSuccess = (response, file) => {
+ if (response.code === 200) {
+ uploadedBlobIds.value.push(response.data.id)
+ }
+}
+
+// 淇濆瓨鏂囦欢鍏宠仈
+const saveFiles = async () => {
+ await saveKnowledgeBaseFiles({
+ knowledgeBaseId: props.knowledgeBaseId,
+ storageBlobIds: uploadedBlobIds.value
+ })
+ // 鍒锋柊鏂囦欢鍒楄〃
+ loadFileList()
+}
+
+// 鐘舵�佹枃鏈槧灏�
+const getStatusText = (status) => {
+ const map = {
+ 0: '寰呭鐞�',
+ 1: '澶勭悊涓�',
+ 2: '宸插畬鎴�',
+ 3: '澶辫触'
+ }
+ return map[status] || '鏈煡'
+}
+
+const getStatusType = (status) => {
+ const map = {
+ 0: 'info',
+ 1: 'warning',
+ 2: 'success',
+ 3: 'danger'
+ }
+ return map[status] || 'info'
+}
+</script>
+```
+
+### 5.3 鐭ヨ瘑搴撻棶绛旂晫闈�
+
+```vue
+<template>
+ <div class="knowledge-chat">
+ <!-- 鐭ヨ瘑搴撻�夋嫨 -->
+ <el-select v-model="selectedKbId" placeholder="閫夋嫨鐭ヨ瘑搴�">
+ <el-option
+ v-for="kb in knowledgeBases"
+ :key="kb.id"
+ :label="kb.title"
+ :value="kb.id"
+ />
+ </el-select>
+
+ <!-- 瀵硅瘽鍖哄煙 -->
+ <div class="chat-messages">
+ <div
+ v-for="(msg, index) in messages"
+ :key="index"
+ :class="['message', msg.role]"
+ >
+ <div class="content">{{ msg.content }}</div>
+ </div>
+ </div>
+
+ <!-- 杈撳叆妗� -->
+ <el-input
+ v-model="inputQuestion"
+ placeholder="璇疯緭鍏ラ棶棰�"
+ @keyup.enter="sendMessage"
+ >
+ <template #append>
+ <el-button @click="sendMessage" :loading="loading">鍙戦��</el-button>
+ </template>
+ </el-input>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { getKnowledgeBaseList, knowledgeChat } from '@/api/knowledge'
+
+const knowledgeBases = ref([])
+const selectedKbId = ref(null)
+const messages = ref([])
+const inputQuestion = ref('')
+const loading = ref(false)
+const memoryId = ref(crypto.randomUUID())
+
+const sendMessage = async () => {
+ if (!inputQuestion.value.trim()) return
+ if (!selectedKbId.value) {
+ ElMessage.warning('璇烽�夋嫨鐭ヨ瘑搴�')
+ return
+ }
+
+ // 娣诲姞鐢ㄦ埛娑堟伅
+ messages.value.push({
+ role: 'user',
+ content: inputQuestion.value
+ })
+
+ loading.value = true
+
+ try {
+ // 娴佸紡璇锋眰
+ const response = await fetch('/api/ai/knowledge/chat', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ knowledgeBaseId: selectedKbId.value,
+ memoryId: memoryId.value,
+ question: inputQuestion.value
+ })
+ })
+
+ // 澶勭悊SSE娴佸紡鍝嶅簲
+ const reader = response.body.getReader()
+ const decoder = new TextDecoder()
+ let aiContent = ''
+
+ messages.value.push({ role: 'assistant', content: '' })
+
+ while (true) {
+ const { done, value } = await reader.read()
+ if (done) break
+
+ const text = decoder.decode(value)
+ aiContent += text
+ messages.value[messages.value.length - 1].content = aiContent
+ }
+ } finally {
+ loading.value = false
+ inputQuestion.value = ''
+ }
+}
+
+onMounted(async () => {
+ const res = await getKnowledgeBaseList()
+ knowledgeBases.value = res.data
+})
+</script>
+```
+
+### 5.4 API灏佽
+
+```javascript
+// api/knowledge.js
+import request from '@/utils/request'
+
+// 鑾峰彇鐭ヨ瘑搴撳垪琛�
+export function getKnowledgeBaseList(params) {
+ return request({
+ url: '/knowledgeBase/getList',
+ method: 'get',
+ params
+ })
+}
+
+// 鏂板鐭ヨ瘑搴�
+export function addKnowledgeBase(data) {
+ return request({
+ url: '/knowledgeBase/add',
+ method: 'post',
+ data
+ })
+}
+
+// 鏇存柊鐭ヨ瘑搴�
+export function updateKnowledgeBase(data) {
+ return request({
+ url: '/knowledgeBase/update',
+ method: 'post',
+ data
+ })
+}
+
+// 鍒犻櫎鐭ヨ瘑搴�
+export function deleteKnowledgeBase(ids) {
+ return request({
+ url: '/knowledgeBase/delete',
+ method: 'delete',
+ data: ids
+ })
+}
+
+// 鑾峰彇鏂囦欢鍚戦噺鍖栫姸鎬�
+export function getVectorStatus(knowledgeBaseId) {
+ return request({
+ url: `/knowledgeBase/vector/status/${knowledgeBaseId}`,
+ method: 'get'
+ })
+}
+
+// 閲嶆柊鍚戦噺鍖�
+export function reprocessVector(vectorId) {
+ return request({
+ url: `/knowledgeBase/vector/reprocess/${vectorId}`,
+ method: 'post'
+ })
+}
+
+// 淇濆瓨鏂囦欢鍏宠仈
+export function saveKnowledgeBaseFiles(data) {
+ return request({
+ url: '/knowledgeBase/file/save',
+ method: 'post',
+ data
+ })
+}
+
+// 鍒犻櫎鏂囦欢
+export function deleteKnowledgeBaseFiles(vectorIds) {
+ return request({
+ url: '/knowledgeBase/file/delete',
+ method: 'delete',
+ data: vectorIds
+ })
+}
+
+// 鐭ヨ瘑搴撻棶绛旓紙娴佸紡锛�
+export async function knowledgeChat(data) {
+ const response = await fetch('/api/ai/knowledge/chat', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(data)
+ })
+ return response.body
+}
+
+// 鑾峰彇鐭ヨ瘑搴撳垪琛紙闂瓟鐢級
+export function getKnowledgeBaseListForChat() {
+ return request({
+ url: '/ai/knowledge/list',
+ method: 'get'
+ })
+}
+```
+
+---
+
+## 鍏�佹牳蹇冩祦绋�
+
+### 6.1 鏂囦欢涓婁紶涓庡悜閲忓寲娴佺▼
+
+```
+1. 鍓嶇璋冪敤 /common/upload 涓婁紶鏂囦欢 鈫� 杩斿洖 storageBlobId
+2. 鍓嶇璋冪敤 /knowledgeBase/file/save 鍏宠仈鏂囦欢鍒扮煡璇嗗簱
+3. 鍚庣鍒涘缓 KnowledgeBaseVector 璁板綍锛堢姸鎬侊細寰呭鐞嗭級
+4. 鍚庣寮傛璋冪敤 KnowledgeRagService.processVectorAsync()
+ 鈹溾攢鈹� 鏇存柊鐘舵�佷负"澶勭悊涓�"
+ 鈹溾攢鈹� 鎻愬彇鏂囦欢鍐呭锛堟敮鎸佸绉嶆牸寮忥級
+ 鈹溾攢鈹� 鑷姩妫�娴嬫枃浠剁紪鐮侊紙UTF-8/GBK锛�
+ 鈹溾攢鈹� 鏂囨湰鍒囩墖锛堝ぇ鏂囦欢鎴栭暱鍐呭鎵嶅垏鐗囷級
+ 鈹溾攢鈹� 鐢熸垚 Embedding 鍚戦噺
+ 鈹溾攢鈹� 瀛樺偍鍒� Pinecone
+ 鈹斺攢鈹� 鏇存柊鐘舵�佷负"瀹屾垚"鎴�"澶辫触"
+```
+
+### 6.2 鐭ヨ瘑搴撻棶绛旀祦绋�
+
+```
+1. 鐢ㄦ埛閫夋嫨鐭ヨ瘑搴擄紝杈撳叆闂
+2. 鍓嶇璋冪敤 /ai/knowledge/chat锛堟祦寮忔帴鍙o級
+3. 鍚庣澶勭悊锛�
+ 鈹溾攢鈹� 鏋勫缓鍛藉悕绌洪棿锛歬b-{knowledgeBaseId}
+ 鈹溾攢鈹� 璋冪敤 Embedding 妯″瀷鐢熸垚闂鍚戦噺
+ 鈹溾攢鈹� 浠� Pinecone 妫�绱㈢浉鍏冲唴瀹癸紙minScore=0.7, maxResults=5锛�
+ 鈹溾攢鈹� 鏋勫缓涓婁笅鏂� Prompt
+ 鈹溾攢鈹� 璋冪敤 LLM 鐢熸垚鍥炵瓟
+ 鈹斺攢鈹� 娴佸紡杩斿洖缁撴灉
+```
+
+---
+
+## 涓冦�佹敞鎰忎簨椤�
+
+1. **Pinecone 鍛藉悕绌洪棿**锛氫笉鑳戒娇鐢� `__default__`锛屽繀椤讳娇鐢ㄨ嚜瀹氫箟鍛藉悕绌洪棿
+2. **鏂囦欢缂栫爜**锛氳嚜鍔ㄦ娴� UTF-8/GBK锛岄伩鍏嶄贡鐮�
+3. **鍒囩墖绛栫暐**锛�
+ - 鏂囦欢 > 80MB 鎴栧唴瀹� > 8000 瀛楃鏃舵墠鍒囩墖
+ - 鍒囩墖澶у皬 500 瀛楃锛岄噸鍙� 100 瀛楃
+ - 浼樺厛鍦ㄥ彞瀛愯竟鐣屽垏鍒�
+4. **Embedding 闄愬埗**锛氶樋閲屼簯 DashScope 闄愬埗鍗曟杈撳叆鏈�澶� 8192 瀛楃
+5. **鍚戦噺鍒犻櫎**锛氫娇鐢� Pinecone 鍘熺敓瀹㈡埛绔紝閫氳繃 metadata filter 鍒犻櫎
+6. **寮傛澶勭悊**锛氬悜閲忓寲浣跨敤 `@Async` 寮傛鎵ц锛岄伩鍏嶉樆濉炴帴鍙�
+
+---
+
+## 鍏�佹枃浠舵竻鍗�
+
+### 鍚庣鏂囦欢
+```
+src/main/java/com/ruoyi/
+鈹溾攢鈹� approve/
+鈹� 鈹溾攢鈹� controller/
+鈹� 鈹� 鈹斺攢鈹� KnowledgeBaseController.java
+鈹� 鈹溾攢鈹� pojo/
+鈹� 鈹� 鈹溾攢鈹� KnowledgeBase.java
+鈹� 鈹� 鈹斺攢鈹� KnowledgeBaseVector.java
+鈹� 鈹溾攢鈹� service/
+鈹� 鈹� 鈹溾攢鈹� KnowledgeBaseService.java
+鈹� 鈹� 鈹溾攢鈹� KnowledgeBaseVectorService.java
+鈹� 鈹� 鈹斺攢鈹� impl/
+鈹� 鈹� 鈹溾攢鈹� KnowledgeBaseServiceImpl.java
+鈹� 鈹� 鈹斺攢鈹� KnowledgeBaseVectorServiceImpl.java
+鈹� 鈹溾攢鈹� mapper/
+鈹� 鈹� 鈹溾攢鈹� KnowledgeBaseMapper.java
+鈹� 鈹� 鈹斺攢鈹� KnowledgeBaseVectorMapper.java
+鈹� 鈹斺攢鈹� dto/
+鈹� 鈹斺攢鈹� KnowledgeBaseVectorVO.java
+鈹斺攢鈹� ai/
+ 鈹溾攢鈹� config/
+ 鈹� 鈹溾攢鈹� EmbeddingStoreConfig.java
+ 鈹� 鈹斺攢鈹� XiaozhiAgentConfig.java
+ 鈹溾攢鈹� controller/
+ 鈹� 鈹斺攢鈹� KnowledgeChatController.java
+ 鈹溾攢鈹� assistant/
+ 鈹� 鈹斺攢鈹� KnowledgeChatAgent.java
+ 鈹溾攢鈹� service/
+ 鈹� 鈹溾攢鈹� KnowledgeRagService.java
+ 鈹� 鈹斺攢鈹� impl/
+ 鈹� 鈹斺攢鈹� KnowledgeRagServiceImpl.java
+ 鈹斺攢鈹� dto/
+ 鈹斺攢鈹� KnowledgeChatRequest.java
+```
+
+### 鍓嶇鏂囦欢
+```
+src/views/knowledge/
+鈹溾攢鈹� index.vue # 鐭ヨ瘑搴撳垪琛�
+鈹溾攢鈹� form.vue # 鏂板/缂栬緫琛ㄥ崟
+鈹溾攢鈹� files.vue # 鏂囦欢绠$悊
+鈹斺攢鈹� chat.vue # 鐭ヨ瘑搴撻棶绛�
+
+src/api/knowledge.js # API灏佽
+```
\ No newline at end of file
diff --git "a/doc/\347\237\245\350\257\206\345\272\223\346\250\241\345\235\227\344\274\240\345\217\202\346\226\271\345\274\217\345\222\214\345\217\202\346\225\260\345\221\275\345\220\215\350\247\204\350\214\203\346\226\207\346\241\243.md" "b/doc/\347\237\245\350\257\206\345\272\223\346\250\241\345\235\227\344\274\240\345\217\202\346\226\271\345\274\217\345\222\214\345\217\202\346\225\260\345\221\275\345\220\215\350\247\204\350\214\203\346\226\207\346\241\243.md"
new file mode 100644
index 0000000..e962ac2
--- /dev/null
+++ "b/doc/\347\237\245\350\257\206\345\272\223\346\250\241\345\235\227\344\274\240\345\217\202\346\226\271\345\274\217\345\222\214\345\217\202\346\225\260\345\221\275\345\220\215\350\247\204\350\214\203\346\226\207\346\241\243.md"
@@ -0,0 +1,941 @@
+# 鐭ヨ瘑搴撴ā鍧椾紶鍙傛柟寮忓拰鍙傛暟鍛藉悕瑙勮寖鏂囨。
+
+## 涓�銆佹杩�
+
+鏈枃妗h缁嗚鏄庣煡璇嗗簱妯″潡涓墍鏈夋帴鍙c�佺粍浠躲�佹柟娉曠殑浼犲弬鏂瑰紡鍜屽弬鏁板懡鍚嶈鑼�,鏃ㄥ湪:
+- 缁熶竴鍓嶅悗绔弬鏁板懡鍚嶈鑼�
+- 鏄庣‘鍙傛暟绫诲瀷鍜屽繀濉��
+- 瑙勮寖浼犲弬鏂瑰紡(GET params銆丳OST body銆丏ELETE data)
+- 鎻愪緵娓呮櫚鐨勫弬鏁版槧灏勫叧绯�
+
+---
+
+## 浜屻�佸弬鏁板懡鍚嶈鑼�
+
+### 2.1 鍩烘湰瑙勮寖
+
+#### 鍛藉悕椋庢牸
+- **鍚庣鍙傛暟**: 閬靛惊 Java 椹煎嘲鍛藉悕娉� (camelCase)
+- **鍓嶇鍙傛暟**: 閬靛惊 JavaScript 椹煎嘲鍛藉悕娉� (camelCase)
+- **鏁版嵁搴撳瓧娈�**: 閬靛惊 MySQL 涓嬪垝绾垮懡鍚嶆硶 (snake_case)
+- **鎺ュ彛URL**: 閬靛惊 RESTful 椋庢牸,浣跨敤灏忓啓鍜岃繛瀛楃
+
+#### 鍛藉悕绾﹀畾
+1. **ID鐩稿叧**: 缁熶竴浣跨敤 `id`銆乣Id` 鍚庣紑
+ - `knowledgeBaseId` - 鐭ヨ瘑搴揑D
+ - `storageBlobId` - 鏂囦欢blob ID
+ - `vectorId` - 鍚戦噺璁板綍ID
+ - `memoryId` - 浼氳瘽ID
+
+2. **鍒楄〃鐩稿叧**: 缁熶竴浣跨敤 `Ids` 鍚庣紑鎴栨暟缁勭被鍨�
+ - `storageBlobIds` - 鏂囦欢blob ID鍒楄〃
+ - `ids` - 閫氱敤ID鍒楄〃
+
+3. **鐘舵�佺浉鍏�**: 缁熶竴浣跨敤 `Status` 鍚庣紑
+ - `vectorStatus` - 鍚戦噺鍖栫姸鎬�
+
+4. **鏁伴噺鐩稿叧**: 缁熶竴浣跨敤 `Count` 鍚庣紑
+ - `fileCount` - 鏂囦欢鏁伴噺
+ - `chunkCount` - 鍒囩墖鏁伴噺
+ - `totalChunkCount` - 鎬诲垏鐗囨暟閲�
+ - `usageCount` - 浣跨敤娆℃暟
+
+5. **鏃堕棿鐩稿叧**: 缁熶竴浣跨敤 `Time` 鍚庣紑
+ - `createTime` - 鍒涘缓鏃堕棿
+ - `updateTime` - 鏇存柊鏃堕棿
+
+---
+
+## 涓夈�佹帴鍙d紶鍙傛柟寮忚鑼�
+
+### 3.1 GET 璇锋眰 - 浣跨敤 params
+
+**閫傜敤鍦烘櫙**: 鏌ヨ銆佸垪琛ㄣ�佸垎椤电瓑鑾峰彇鏁版嵁鐨勬帴鍙�
+
+**浼犲弬鏂瑰紡**: 閫氳繃 URL 鍙傛暟浼犻��,浣跨敤 `params`
+
+**绀轰緥**:
+```javascript
+// 鏌ヨ鐭ヨ瘑搴撳垪琛�
+export function listKnowledgeBase(query) {
+ return request({
+ url: "/knowledgeBase/getList",
+ method: "get",
+ params: query, // 鉁� GET璇锋眰浣跨敤 params
+ });
+}
+
+// 瀹為檯璋冪敤
+listKnowledgeBase({
+ current: 1, // 褰撳墠椤电爜
+ size: 20, // 姣忛〉鏉℃暟
+ title: "", // 鐭ヨ瘑鏍囬(鍙��)
+ type: "" // 鐭ヨ瘑绫诲瀷(鍙��)
+});
+```
+
+**URL鏍煎紡**: `/knowledgeBase/getList?current=1&size=20&title=&type=`
+
+**瑙勮寖瑕佺偣**:
+- 鉁� 鏌ヨ鍙傛暟缁熶竴鏀惧湪 `params` 涓�
+- 鉁� 鍒嗛〉鍙傛暟鍛藉悕: `current` (褰撳墠椤�)銆乣size` (姣忛〉鏉℃暟)
+- 鉁� 鎼滅储鍙傛暟鍛藉悕: 涓庡疄浣撳瓧娈典繚鎸佷竴鑷�
+- 鉁� 璺緞鍙傛暟浣跨敤 URL 鍗犱綅绗�: `/path/{id}`
+
+---
+
+### 3.2 POST 璇锋眰 - 浣跨敤 data
+
+**閫傜敤鍦烘櫙**: 鏂板銆佹洿鏂般�佷繚瀛樼瓑鎻愪氦鏁版嵁鐨勬帴鍙�
+
+**浼犲弬鏂瑰紡**: 閫氳繃璇锋眰浣撲紶閫�,浣跨敤 `data`
+
+**绀轰緥**:
+```javascript
+// 鏂板鐭ヨ瘑搴�
+export function addKnowledgeBase(data) {
+ return request({
+ url: "/knowledgeBase/add",
+ method: "post",
+ data: data, // 鉁� POST璇锋眰浣跨敤 data
+ });
+}
+
+// 瀹為檯璋冪敤
+addKnowledgeBase({
+ title: "鎿嶄綔鎵嬪唽",
+ type: "guide",
+ scenario: "绯荤粺鎿嶄綔鎸囧",
+ efficiency: "high",
+ problem: "鐢ㄦ埛涓嶄細鎿嶄綔绯荤粺",
+ solution: "鎸夌収鎿嶄綔鎵嬪唽鎵ц...",
+ keyPoints: "姝ラ1,姝ラ2,姝ラ3",
+ creator: "寮犱笁",
+ usageCount: 0
+});
+```
+
+**璇锋眰浣撴牸寮�**: JSON 鏍煎紡,`Content-Type: application/json`
+
+**瑙勮寖瑕佺偣**:
+- 鉁� 鎻愪氦鏁版嵁缁熶竴鏀惧湪 `data` 涓�
+- 鉁� 鍙傛暟鍚嶄笌鍚庣瀹炰綋瀛楁淇濇寔涓�鑷�
+- 鉁� 蹇呭~鍙傛暟闇�瑕佸湪琛ㄥ崟楠岃瘉瑙勫垯涓0鏄�
+- 鉁� 鏁板�肩被鍨嬪弬鏁伴渶鎸囧畾榛樿鍊�
+
+---
+
+### 3.3 DELETE 璇锋眰 - 浣跨敤 data
+
+**閫傜敤鍦烘櫙**: 鍒犻櫎銆佹壒閲忓垹闄ょ瓑鎿嶄綔
+
+**浼犲弬鏂瑰紡**: 閫氳繃璇锋眰浣撲紶閫掓暟缁勬垨瀵硅薄,浣跨敤 `data`
+
+**绀轰緥**:
+```javascript
+// 鍒犻櫎鐭ヨ瘑搴�
+export function delKnowledgeBase(query) {
+ return request({
+ url: "/knowledgeBase/delete",
+ method: "delete",
+ data: query, // 鉁� DELETE璇锋眰浣跨敤 data 浼犻�掓暟缁�
+ });
+}
+
+// 瀹為檯璋冪敤(鎵归噺鍒犻櫎)
+delKnowledgeBase([1, 2, 3]); // 鉁� 鐩存帴浼犻�扞D鏁扮粍
+```
+
+**璇锋眰浣撴牸寮�**: JSON 鏁扮粍 `[1, 2, 3]`
+
+**瑙勮寖瑕佺偣**:
+- 鉁� DELETE璇锋眰鐨勫弬鏁版斁鍦� `data` 涓�
+- 鉁� 鎵归噺鍒犻櫎浼犻�扞D鏁扮粍
+- 鉁� 鍗曚釜鍒犻櫎涔熷彲浠ヤ紶閫掓暟缁� `[id]`
+- 鈿狅笍 涓嶈浣跨敤 `params` 浼犻�掑垹闄ゅ弬鏁�
+
+---
+
+### 3.4 娴佸紡璇锋眰 - 浣跨敤 Fetch API
+
+**閫傜敤鍦烘櫙**: AI闂瓟銆佹祦寮忚緭鍑虹瓑闇�瑕佸疄鏃跺搷搴旂殑鎺ュ彛
+
+**浼犲弬鏂瑰紡**: 浣跨敤鍘熺敓 Fetch API,涓嶆敮鎸� axios
+
+**绀轰緥**:
+```javascript
+// 鐭ヨ瘑搴撻棶绛�(娴佸紡)
+export function knowledgeChat(data) {
+ const token = getToken();
+ return fetch(import.meta.env.VITE_APP_BASE_API + '/ai/knowledge/chat', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer ' + token
+ },
+ body: JSON.stringify(data) // 鉁� 浣跨敤 body 浼犻�掑弬鏁�
+ });
+}
+
+// 瀹為檯璋冪敤
+knowledgeChat({
+ knowledgeBaseId: 10,
+ memoryId: "session-xxx",
+ question: "濡備綍鎿嶄綔瀹℃壒娴佺▼?"
+});
+```
+
+**瑙勮寖瑕佺偣**:
+- 鉁� 娴佸紡鎺ュ彛蹇呴』浣跨敤 Fetch API
+- 鉁� 鍙傛暟浣跨敤 `JSON.stringify()` 搴忓垪鍖�
+- 鉁� 蹇呴』鎼哄甫 Authorization header
+- 鈿狅笍 axios 涓嶆敮鎸佹祦寮忓搷搴�,涓嶈浣跨敤
+
+---
+
+## 鍥涖�佸畬鏁村弬鏁板鐓ц〃
+
+### 4.1 鐭ヨ瘑搴撶鐞嗘帴鍙e弬鏁�
+
+#### 鏌ヨ鍒楄〃 (`GET /knowledgeBase/getList`)
+
+| 鍙傛暟鍚� | 绫诲瀷 | 蹇呭~ | 浼犲弬浣嶇疆 | 璇存槑 | 绀轰緥鍊� |
+|--------|------|------|----------|------|--------|
+| current | Integer | 鏄� | params | 褰撳墠椤电爜 | 1 |
+| size | Integer | 鏄� | params | 姣忛〉鏉℃暟 | 20 |
+| title | String | 鍚� | params | 鐭ヨ瘑鏍囬(妯$硦鎼滅储) | "鎿嶄綔" |
+| type | String | 鍚� | params | 鐭ヨ瘑绫诲瀷(绮剧‘鍖归厤) | "guide" |
+
+**鍓嶇璋冪敤**:
+```javascript
+listKnowledgeBase({
+ current: 1,
+ size: 20,
+ title: "",
+ type: ""
+});
+```
+
+---
+
+#### 鏂板鐭ヨ瘑搴� (`POST /knowledgeBase/add`)
+
+| 鍙傛暟鍚� | 绫诲瀷 | 蹇呭~ | 浼犲弬浣嶇疆 | 璇存槑 | 绀轰緥鍊� |
+|--------|------|------|----------|------|--------|
+| title | String | 鏄� | data | 鐭ヨ瘑鏍囬 | "鎿嶄綔鎵嬪唽" |
+| type | String | 鏄� | data | 鐭ヨ瘑绫诲瀷 | "guide" |
+| scenario | String | 鍚� | data | 閫傜敤鍦烘櫙 | "绯荤粺鎿嶄綔" |
+| efficiency | String | 鍚� | data | 瑙e喅鏁堢巼 | "high" |
+| problem | String | 鏄� | data | 闂鎻忚堪 | "鐢ㄦ埛涓嶄細鎿嶄綔" |
+| solution | String | 鏄� | data | 瑙e喅鏂规 | "鍙傝�冩墜鍐�" |
+| keyPoints | String | 鍚� | data | 鍏抽敭瑕佺偣 | "姝ラ1,姝ラ2" |
+| creator | String | 鍚� | data | 鍒涘缓浜� | "寮犱笁" |
+| usageCount | Integer | 鍚� | data | 浣跨敤娆℃暟 | 0 |
+
+**鍓嶇璋冪敤**:
+```javascript
+addKnowledgeBase({
+ title: "鎿嶄綔鎵嬪唽",
+ type: "guide",
+ scenario: "绯荤粺鎿嶄綔鎸囧",
+ efficiency: "high",
+ problem: "鐢ㄦ埛涓嶄細鎿嶄綔绯荤粺",
+ solution: "鎸夌収鎿嶄綔鎵嬪唽鎵ц...",
+ keyPoints: "姝ラ1,姝ラ2,姝ラ3",
+ creator: "寮犱笁",
+ usageCount: 0
+});
+```
+
+---
+
+#### 鏇存柊鐭ヨ瘑搴� (`POST /knowledgeBase/update`)
+
+| 鍙傛暟鍚� | 绫诲瀷 | 蹇呭~ | 浼犲弬浣嶇疆 | 璇存槑 | 绀轰緥鍊� |
+|--------|------|------|----------|------|--------|
+| id | Long | 鏄� | data | 鐭ヨ瘑搴揑D | 10 |
+| *(鍏朵粬鍙傛暟鍚屾柊澧�)* | - | - | data | - | - |
+
+**鍓嶇璋冪敤**:
+```javascript
+updateKnowledgeBase({
+ id: 10,
+ title: "鎿嶄綔鎵嬪唽(鏇存柊)",
+ type: "guide",
+ // ...鍏朵粬鍙傛暟
+});
+```
+
+---
+
+#### 鍒犻櫎鐭ヨ瘑搴� (`DELETE /knowledgeBase/delete`)
+
+| 鍙傛暟鍚� | 绫诲瀷 | 蹇呭~ | 浼犲弬浣嶇疆 | 璇存槑 | 绀轰緥鍊� |
+|--------|------|------|----------|------|--------|
+| ids | Long[] | 鏄� | data | 鐭ヨ瘑搴揑D鏁扮粍 | [1, 2, 3] |
+
+**鍓嶇璋冪敤**:
+```javascript
+delKnowledgeBase([1, 2, 3]);
+```
+
+---
+
+### 4.2 鏂囦欢绠$悊鎺ュ彛鍙傛暟
+
+#### 鏌ヨ鍚戦噺鍖栫姸鎬� (`GET /knowledgeBase/vector/status/{knowledgeBaseId}`)
+
+| 鍙傛暟鍚� | 绫诲瀷 | 蹇呭~ | 浼犲弬浣嶇疆 | 璇存槑 | 绀轰緥鍊� |
+|--------|------|------|----------|------|--------|
+| knowledgeBaseId | Long | 鏄� | URL璺緞 | 鐭ヨ瘑搴揑D | 10 |
+
+**鍓嶇璋冪敤**:
+```javascript
+getVectorStatus(10);
+```
+
+**URL**: `/knowledgeBase/vector/status/10`
+
+---
+
+#### 淇濆瓨鏂囦欢鍏宠仈 (`POST /knowledgeBase/file/save`)
+
+| 鍙傛暟鍚� | 绫诲瀷 | 蹇呭~ | 浼犲弬浣嶇疆 | 璇存槑 | 绀轰緥鍊� |
+|--------|------|------|----------|------|--------|
+| knowledgeBaseId | Long | 鏄� | data | 鐭ヨ瘑搴揑D | 10 |
+| storageBlobIds | Long[] | 鏄� | data | 鏂囦欢blob ID鏁扮粍 | [123, 124] |
+
+**鍓嶇璋冪敤**:
+```javascript
+saveKnowledgeBaseFiles({
+ knowledgeBaseId: 10,
+ storageBlobIds: [123, 124]
+});
+```
+
+**閲嶈璇存槑**:
+- 鈿狅笍 **蹇呴』鍏堣皟鐢� `/common/upload` 涓婁紶鏂囦欢**
+- 鈿狅笍 **鑾峰彇杩斿洖鐨� `data.id` 浣滀负 `storageBlobId`**
+- 鈿狅笍 **姝ゆ帴鍙hЕ鍙戝紓姝ュ悜閲忓寲澶勭悊**
+
+---
+
+#### 鍒犻櫎鐭ヨ瘑搴撴枃浠� (`DELETE /knowledgeBase/file/delete`)
+
+| 鍙傛暟鍚� | 绫诲瀷 | 蹇呭~ | 浼犲弬浣嶇疆 | 璇存槑 | 绀轰緥鍊� |
+|--------|------|------|----------|------|--------|
+| ids | Long[] | 鏄� | data | 鍚戦噺璁板綍ID鏁扮粍 | [1, 2, 3] |
+
+**鍓嶇璋冪敤**:
+```javascript
+deleteKnowledgeBaseFile([row.id]);
+// 娉ㄦ剰: row.id 鏄悜閲忚褰曠殑ID,涓嶆槸 storageBlobId
+```
+
+---
+
+#### 閲嶆柊鍚戦噺鍖� (`POST /knowledgeBase/vector/reprocess/{vectorId}`)
+
+| 鍙傛暟鍚� | 绫诲瀷 | 蹇呭~ | 浼犲弬浣嶇疆 | 璇存槑 | 绀轰緥鍊� |
+|--------|------|------|----------|------|--------|
+| vectorId | Long | 鏄� | URL璺緞 | 鍚戦噺璁板綍ID | 1 |
+
+**鍓嶇璋冪敤**:
+```javascript
+reprocessVector(1);
+```
+
+**URL**: `/knowledgeBase/vector/reprocess/1`
+
+---
+
+### 4.3 鐭ヨ瘑闂瓟鎺ュ彛鍙傛暟
+
+#### 鐭ヨ瘑搴撻棶绛� (`POST /ai/knowledge/chat` - 娴佸紡)
+
+| 鍙傛暟鍚� | 绫诲瀷 | 蹇呭~ | 浼犲弬浣嶇疆 | 璇存槑 | 绀轰緥鍊� |
+|--------|------|------|----------|------|--------|
+| knowledgeBaseId | Long | 鏄� | body | 鐭ヨ瘑搴揑D | 10 |
+| memoryId | String | 鏄� | body | 浼氳瘽ID | "kb-chat-xxx" |
+| question | String | 鏄� | body | 鐢ㄦ埛闂 | "濡備綍鎿嶄綔?" |
+
+**鍓嶇璋冪敤**:
+```javascript
+knowledgeChat({
+ knowledgeBaseId: chatKnowledgeBaseId.value,
+ memoryId: memoryId.value,
+ question: currentQuestion
+});
+```
+
+**浼氳瘽ID鐢熸垚瑙勮寖**:
+```javascript
+// 鏂瑰紡1: 浣跨敤 crypto.randomUUID() (鎺ㄨ崘)
+memoryId.value = crypto.randomUUID();
+
+// 鏂瑰紡2: 浣跨敤鏃堕棿鎴�
+memoryId.value = 'kb-chat-' + Date.now();
+```
+
+---
+
+#### 鏌ヨ闂瓟鍘嗗彶 (`GET /ai/knowledge/history/{memoryId}`)
+
+| 鍙傛暟鍚� | 绫诲瀷 | 蹇呭~ | 浼犲弬浣嶇疆 | 璇存槑 | 绀轰緥鍊� |
+|--------|------|------|----------|------|--------|
+| memoryId | String | 鏄� | URL璺緞 | 浼氳瘽ID | "kb-chat-xxx" |
+
+**鍓嶇璋冪敤**:
+```javascript
+getKnowledgeHistory('kb-chat-xxx');
+```
+
+---
+
+## 浜斻�佸搷搴旀暟鎹瓧娈靛鐓ц〃
+
+### 5.1 鐭ヨ瘑搴撳垪琛ㄥ搷搴�
+
+```javascript
+{
+ code: 200,
+ data: {
+ total: 100,
+ records: [
+ {
+ id: 1, // 鐭ヨ瘑搴揑D
+ title: "鎿嶄綔鎵嬪唽", // 鐭ヨ瘑鏍囬
+ type: "guide", // 鐭ヨ瘑绫诲瀷
+ scenario: "绯荤粺鎿嶄綔", // 閫傜敤鍦烘櫙
+ efficiency: "high", // 瑙e喅鏁堢巼
+ problem: "...", // 闂鎻忚堪
+ solution: "...", // 瑙e喅鏂规
+ keyPoints: "...", // 鍏抽敭瑕佺偣
+ creator: "寮犱笁", // 鍒涘缓浜�
+ usageCount: 10, // 浣跨敤娆℃暟
+ fileCount: 3, // 鏂囦欢鏁伴噺
+ totalChunkCount: 45, // 鎬诲垏鐗囨暟閲�
+ createTime: "2026-06-08", // 鍒涘缓鏃堕棿
+ updateTime: "2026-06-08" // 鏇存柊鏃堕棿
+ }
+ ]
+ }
+}
+```
+
+---
+
+### 5.2 鏂囦欢鍚戦噺鍖栫姸鎬佸搷搴�
+
+```javascript
+{
+ code: 200,
+ data: [
+ {
+ id: 1, // 鍚戦噺璁板綍ID
+ storageBlobId: 123, // 鏂囦欢blob ID
+ fileName: "鎿嶄綔鎵嬪唽.docx", // 鏂囦欢鍚�
+ fileType: "docx", // 鏂囦欢绫诲瀷
+ vectorStatus: 2, // 鍚戦噺鍖栫姸鎬�: 0-寰呭鐞�, 1-澶勭悊涓�, 2-宸插畬鎴�, 3-澶辫触
+ chunkCount: 15, // 鍒囩墖鏁伴噺
+ namespace: "kb-10", // 鍚戦噺鍛藉悕绌洪棿
+ vectorError: null, // 鍚戦噺鍖栭敊璇俊鎭�
+ createTime: "2026-06-08" // 鍒涘缓鏃堕棿
+ }
+ ]
+}
+```
+
+---
+
+### 5.3 鏂囦欢涓婁紶鍝嶅簲
+
+```javascript
+{
+ code: 200,
+ data: {
+ id: 123, // 鈿狅笍 杩欐槸 storageBlobId,鐢ㄤ簬淇濆瓨鏂囦欢鍏宠仈
+ name: "鎿嶄綔鎵嬪唽.docx", // 鏂囦欢鍚�
+ url: "/profile/upload/...", // 鏂囦欢URL
+ previewURL: "...", // 棰勮URL
+ downloadURL: "..." // 涓嬭浇URL
+ }
+}
+```
+
+**閲嶈**: 涓婁紶鎴愬姛鍚�,闇�瑕佹彁鍙� `response.data.id` 浣滀负 `storageBlobId`
+
+---
+
+## 鍏�佸墠绔粍浠朵紶鍙傝鑼�
+
+### 6.1 琛ㄦ牸缁勪欢浼犲弬
+
+```vue
+<PIMTable
+ rowKey="id" <!-- 琛屽敮涓�鏍囪瘑瀛楁 -->
+ :column="tableColumn" <!-- 鍒楅厤缃� -->
+ :tableData="tableData" <!-- 琛ㄦ牸鏁版嵁 -->
+ :page="page" <!-- 鍒嗛〉閰嶇疆 -->
+ :isSelection="true" <!-- 鏄惁鏀寔閫夋嫨 -->
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ :total="page.total"
+/>
+```
+
+**鍒嗛〉閰嶇疆**:
+```javascript
+page: {
+ current: 1, // 褰撳墠椤电爜
+ size: 20, // 姣忛〉鏉℃暟
+ total: 0 // 鎬昏褰曟暟
+}
+```
+
+---
+
+### 6.2 寮圭獥缁勪欢浼犲弬
+
+```vue
+<FormDialog
+ v-model="dialogVisible" <!-- 鎺у埗鏄剧ず -->
+ :title="dialogTitle" <!-- 寮圭獥鏍囬 -->
+ :width="'800px'" <!-- 寮圭獥瀹藉害 -->
+ @close="closeDialog" <!-- 鍏抽棴鍥炶皟 -->
+ @confirm="submitForm" <!-- 纭鍥炶皟 -->
+ @cancel="closeDialog" <!-- 鍙栨秷鍥炶皟 -->
+>
+ <!-- 寮圭獥鍐呭 -->
+</FormDialog>
+```
+
+---
+
+### 6.3 涓婁紶缁勪欢浼犲弬
+
+```vue
+<el-upload
+ :action="uploadUrl" <!-- 涓婁紶鍦板潃 -->
+ :headers="uploadHeaders" <!-- 璇锋眰澶� -->
+ :on-success="handleUploadSuccess" <!-- 鎴愬姛鍥炶皟 -->
+ :on-error="handleUploadError" <!-- 澶辫触鍥炶皟 -->
+ :before-upload="beforeUpload" <!-- 涓婁紶鍓嶆牎楠� -->
+ multiple <!-- 鏀寔澶氶�� -->
+ :show-file-list="false" <!-- 涓嶆樉绀烘枃浠跺垪琛� -->
+ accept=".txt,.md,.docx,.xlsx,.xls,.pdf" <!-- 鏂囦欢绫诲瀷闄愬埗 -->
+/>
+```
+
+**涓婁紶閰嶇疆**:
+```javascript
+const uploadUrl = import.meta.env.VITE_APP_BASE_API + "/common/upload";
+const uploadHeaders = {
+ Authorization: "Bearer " + getToken()
+};
+```
+
+---
+
+## 涓冦�佸弬鏁扮被鍨嬭浆鎹㈣鑼�
+
+### 7.1 瀛楃涓茶浆鏁板��
+
+```javascript
+// 鍚庣杩斿洖鐨勬暟鍊煎彲鑳芥槸瀛楃涓�,闇�瑕佽浆鎹�
+const id = Number(row.id);
+const count = parseInt(row.chunkCount, 10);
+```
+
+---
+
+### 7.2 鏁板�艰浆瀛楃涓�
+
+```javascript
+// URL璺緞鍙傛暟闇�瑕佸瓧绗︿覆
+const url = `/knowledgeBase/vector/status/${String(knowledgeBaseId)}`;
+```
+
+---
+
+### 7.3 鏁扮粍澶勭悊
+
+```javascript
+// ID鏁扮粍澶勭悊
+const ids = selection.map(item => item.id); // 鉁� 鎻愬彇ID
+await delKnowledgeBase(ids); // 鉁� 浼犻�掓暟缁�
+
+// 鏂囦欢blob ID鏁扮粍
+const blobIds = uploadedFiles.map(file => file.id);
+await saveKnowledgeBaseFiles({
+ knowledgeBaseId: currentKnowledgeBase.id,
+ storageBlobIds: blobIds
+});
+```
+
+---
+
+## 鍏�佸弬鏁伴獙璇佽鑼�
+
+### 8.1 琛ㄥ崟楠岃瘉瑙勫垯
+
+```javascript
+const rules = {
+ title: [
+ { required: true, message: "璇疯緭鍏ョ煡璇嗘爣棰�", trigger: "blur" }
+ ],
+ type: [
+ { required: true, message: "璇烽�夋嫨鐭ヨ瘑绫诲瀷", trigger: "change" }
+ ],
+ problem: [
+ { required: true, message: "璇锋弿杩伴亣鍒扮殑闂", trigger: "blur" }
+ ],
+ solution: [
+ { required: true, message: "璇疯缁嗘弿杩拌В鍐虫柟妗�", trigger: "blur" }
+ ]
+};
+```
+
+---
+
+### 8.2 涓婁紶鏂囦欢鏍¢獙
+
+```javascript
+const beforeUpload = (file) => {
+ // 鏂囦欢绫诲瀷鏍¢獙
+ const allowedTypes = ['.txt', '.md', '.docx', '.xlsx', '.xls', '.pdf'];
+ const fileName = file.name.toLowerCase();
+ const isAllowed = allowedTypes.some(type => fileName.endsWith(type));
+
+ if (!isAllowed) {
+ ElMessage.error('鍙敮鎸� txt銆乵d銆乨ocx銆亁lsx銆亁ls銆乸df 鏍煎紡鐨勬枃浠�');
+ return false;
+ }
+
+ // 鏂囦欢澶у皬鏍¢獙
+ const isLt50M = file.size / 1024 / 1024 < 50;
+ if (!isLt50M) {
+ ElMessage.error('鏂囦欢澶у皬涓嶈兘瓒呰繃 50MB');
+ return false;
+ }
+
+ return true;
+};
+```
+
+---
+
+### 8.3 闂瓟鍙傛暟鏍¢獙
+
+```javascript
+const sendQuestion = async () => {
+ // 绌哄唴瀹规牎楠�
+ if (!questionInput.value.trim()) {
+ ElMessage.warning("璇疯緭鍏ラ棶棰�");
+ return;
+ }
+
+ // 鐭ヨ瘑搴撻�夋嫨鏍¢獙
+ if (!chatKnowledgeBaseId.value) {
+ ElMessage.warning("璇峰厛閫夋嫨鐭ヨ瘑搴�");
+ return;
+ }
+
+ // 鍙戦�佺姸鎬佹牎楠�
+ if (sending.value) {
+ return; // 闃叉閲嶅鍙戦��
+ }
+
+ // ...鍙戦�佽姹�
+};
+```
+
+---
+
+## 涔濄�佸父瑙侀敊璇拰瑙e喅鏂规
+
+### 9.1 鍙傛暟鍚嶄笉鍖归厤
+
+**閿欒绀轰緥**:
+```javascript
+// 鉂� 閿欒: 浣跨敤浜嗕笅鍒掔嚎鍛藉悕
+saveKnowledgeBaseFiles({
+ knowledge_base_id: 10,
+ storage_blob_ids: [123]
+});
+
+// 鉁� 姝g‘: 浣跨敤椹煎嘲鍛藉悕
+saveKnowledgeBaseFiles({
+ knowledgeBaseId: 10,
+ storageBlobIds: [123]
+});
+```
+
+---
+
+### 9.2 浼犲弬浣嶇疆閿欒
+
+**閿欒绀轰緥**:
+```javascript
+// 鉂� 閿欒: POST璇锋眰浣跨敤 params
+export function addKnowledgeBase(data) {
+ return request({
+ url: "/knowledgeBase/add",
+ method: "post",
+ params: data // 鉂� 閿欒
+ });
+}
+
+// 鉁� 姝g‘: POST璇锋眰浣跨敤 data
+export function addKnowledgeBase(data) {
+ return request({
+ url: "/knowledgeBase/add",
+ method: "post",
+ data: data // 鉁� 姝g‘
+ });
+}
+```
+
+---
+
+### 9.3 DELETE璇锋眰鍙傛暟閿欒
+
+**閿欒绀轰緥**:
+```javascript
+// 鉂� 閿欒: DELETE浣跨敤 params 浼犻�掓暟缁�
+export function delKnowledgeBase(ids) {
+ return request({
+ url: "/knowledgeBase/delete",
+ method: "delete",
+ params: ids // 鉂� 閿欒
+ });
+}
+
+// 鉁� 姝g‘: DELETE浣跨敤 data 浼犻�掓暟缁�
+export function delKnowledgeBase(ids) {
+ return request({
+ url: "/knowledgeBase/delete",
+ method: "delete",
+ data: ids // 鉁� 姝g‘
+ });
+}
+```
+
+---
+
+### 9.4 娴佸紡鎺ュ彛閿欒
+
+**閿欒绀轰緥**:
+```javascript
+// 鉂� 閿欒: 娴佸紡鎺ュ彛浣跨敤 axios
+export function knowledgeChat(data) {
+ return request({
+ url: "/ai/knowledge/chat",
+ method: "post",
+ data: data // 鉂� axios涓嶆敮鎸佹祦寮�
+ });
+}
+
+// 鉁� 姝g‘: 娴佸紡鎺ュ彛浣跨敤 Fetch API
+export function knowledgeChat(data) {
+ const token = getToken();
+ return fetch(import.meta.env.VITE_APP_BASE_API + '/ai/knowledge/chat', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer ' + token
+ },
+ body: JSON.stringify(data) // 鉁� 姝g‘
+ });
+}
+```
+
+---
+
+### 9.5 鏂囦欢涓婁紶ID鑾峰彇閿欒
+
+**閿欒绀轰緥**:
+```javascript
+// 鉂� 閿欒: 涓婁紶鎴愬姛鍚庢病鏈変繚瀛� storageBlobId
+const handleUploadSuccess = (response) => {
+ if (response.code === 200) {
+ ElMessage.success("涓婁紶鎴愬姛");
+ // 鉂� 娌℃湁淇濆瓨 response.data.id
+ }
+};
+
+// 鉁� 姝g‘: 淇濆瓨 storageBlobId 鐢ㄤ簬鍚庣画鍏宠仈
+const handleUploadSuccess = (response, file) => {
+ if (response.code === 200) {
+ uploadedBlobIds.value.push(response.data.id); // 鉁� 淇濆瓨ID
+ ElMessage.success(`鏂囦欢 ${file.name} 涓婁紶鎴愬姛`);
+ }
+};
+```
+
+---
+
+## 鍗併�佹渶浣冲疄璺靛缓璁�
+
+### 10.1 鍙傛暟鍛藉悕涓�鑷存��
+
+鉁� **鍓嶇鍙傛暟鍚嶄笌鍚庣瀹炰綋瀛楁淇濇寔涓�鑷�**
+```javascript
+// 鍓嶇
+{
+ knowledgeBaseId: 10,
+ storageBlobIds: [123]
+}
+
+// 鍚庣瀹炰綋瀛楁
+private Long knowledgeBaseId;
+private List<Long> storageBlobIds;
+```
+
+---
+
+### 10.2 鍙傛暟绫诲瀷涓�鑷存��
+
+鉁� **鏄庣‘鍙傛暟绫诲瀷,閬垮厤鑷姩绫诲瀷杞崲**
+```javascript
+// 鏁板�肩被鍨�
+const id = 10; // number
+const count = 0; // number
+
+// 瀛楃涓茬被鍨�
+const title = ""; // string
+const type = ""; // string
+
+// 鏁扮粍绫诲瀷
+const ids = []; // array
+```
+
+---
+
+### 10.3 蹇呭~鍙傛暟鏍¢獙
+
+鉁� **鍦ㄨ皟鐢ㄦ帴鍙e墠鏍¢獙蹇呭~鍙傛暟**
+```javascript
+const saveFiles = async () => {
+ if (!currentKnowledgeBase.value?.id) {
+ ElMessage.error("鐭ヨ瘑搴撲俊鎭紓甯�");
+ return;
+ }
+
+ if (uploadedBlobIds.value.length === 0) {
+ ElMessage.warning("璇峰厛涓婁紶鏂囦欢");
+ return;
+ }
+
+ // ...璋冪敤鎺ュ彛
+};
+```
+
+---
+
+### 10.4 鍙傛暟榛樿鍊�
+
+鉁� **涓哄彲閫夊弬鏁拌缃悎鐞嗙殑榛樿鍊�**
+```javascript
+const form = {
+ title: "",
+ type: "",
+ usageCount: 0, // 鉁� 鏁板�肩被鍨嬮粯璁ゅ�间负0
+ creator: userStore.nickName || "" // 鉁� 浣跨敤褰撳墠鐢ㄦ埛鍚嶄綔涓洪粯璁ゅ��
+};
+```
+
+---
+
+### 10.5 URL璺緞鍙傛暟
+
+鉁� **璺緞鍙傛暟浣跨敤妯℃澘瀛楃涓�**
+```javascript
+const url = `/knowledgeBase/vector/status/${knowledgeBaseId}`;
+const url = `/knowledgeBase/vector/reprocess/${vectorId}`;
+```
+
+---
+
+## 鍗佷竴銆佸弬鏁版槧灏勫叧绯绘�荤粨
+
+### 鐭ヨ瘑搴揑D鐩稿叧
+| 鍦烘櫙 | 鍙傛暟鍚� | 绫诲瀷 | 鏉ユ簮 |
+|------|--------|------|------|
+| 鐭ヨ瘑搴撳垪琛ㄦ煡璇� | - | - | URL璺緞鏃犲弬鏁� |
+| 鐭ヨ瘑搴撹鎯� | id | Long | URL璺緞鍙傛暟 |
+| 鏂囦欢鍏宠仈淇濆瓨 | knowledgeBaseId | Long | POST body鍙傛暟 |
+| 鍚戦噺鍖栫姸鎬佹煡璇� | knowledgeBaseId | Long | URL璺緞鍙傛暟 |
+| 鐭ヨ瘑闂瓟 | knowledgeBaseId | Long | POST body鍙傛暟 |
+
+---
+
+### 鏂囦欢ID鐩稿叧
+| 鍦烘櫙 | 鍙傛暟鍚� | 绫诲瀷 | 鏉ユ簮 |
+|------|--------|------|------|
+| 鏂囦欢涓婁紶鍝嶅簲 | data.id | Long | 鍝嶅簲鏁版嵁(浣滀负storageBlobId) |
+| 鏂囦欢鍏宠仈淇濆瓨 | storageBlobIds | Long[] | POST body鍙傛暟 |
+| 鏂囦欢鍒犻櫎 | ids | Long[] | DELETE body鍙傛暟(鍚戦噺璁板綍ID) |
+| 閲嶆柊鍚戦噺鍖� | vectorId | Long | URL璺緞鍙傛暟 |
+
+---
+
+### 浼氳瘽ID鐩稿叧
+| 鍦烘櫙 | 鍙傛暟鍚� | 绫诲瀷 | 鏉ユ簮 |
+|------|--------|------|------|
+| 鐭ヨ瘑闂瓟 | memoryId | String | 鍓嶇鐢熸垚UUID |
+| 闂瓟鍘嗗彶鏌ヨ | memoryId | String | URL璺緞鍙傛暟 |
+
+---
+
+## 鍗佷簩銆侀檮褰�
+
+### 闄勫綍A: 鍙傛暟绫诲瀷瀵圭収琛�
+
+| 鍙傛暟绫诲瀷 | JavaScript | Java | MySQL |
+|----------|------------|------|-------|
+| ID | number/Long | Long | BIGINT |
+| 鏍囬 | String | String | VARCHAR |
+| 绫诲瀷 | String | String | VARCHAR |
+| 鐘舵�� | Integer | Integer | TINYINT |
+| 鏁伴噺 | Integer | Integer | INT |
+| 鏃堕棿 | String/Date | LocalDateTime | DATETIME |
+| 鏁扮粍 | Array | List | - |
+
+---
+
+### 闄勫綍B: HTTP鏂规硶涓庝紶鍙備綅缃鐓ц〃
+
+| HTTP鏂规硶 | 浼犲弬浣嶇疆 | request閰嶇疆 | 閫傜敤鍦烘櫙 |
+|----------|----------|--------------|----------|
+| GET | URL鍙傛暟 | `params: query` | 鏌ヨ銆佸垪琛� |
+| POST | 璇锋眰浣� | `data: data` | 鏂板銆佹洿鏂般�佷繚瀛� |
+| DELETE | 璇锋眰浣� | `data: ids` | 鍒犻櫎銆佹壒閲忓垹闄� |
+| PUT | 璇锋眰浣� | `data: data` | 鏇存柊(閮ㄥ垎浣跨敤POST) |
+| 娴佸紡POST | 璇锋眰浣� | `body: JSON.stringify()` | AI闂瓟 |
+
+---
+
+### 闄勫綍C: 鍚戦噺鍖栫姸鎬佸�煎鐓ц〃
+
+| 鐘舵�佸�� | 鐘舵�佸悕绉� | 鍓嶇鏄剧ず | 鏍囩棰滆壊 |
+|--------|----------|----------|----------|
+| 0 | 寰呭鐞� | "寰呭鐞�" | info (鐏拌壊) |
+| 1 | 澶勭悊涓� | "澶勭悊涓�" | warning (姗欒壊) |
+| 2 | 宸插畬鎴� | "宸插畬鎴�" | success (缁胯壊) |
+| 3 | 澶辫触 | "澶辫触" | danger (绾㈣壊) |
+
+---
+
+## 鍗佷笁銆佹�荤粨
+
+鏈枃妗h缁嗚鑼冧簡鐭ヨ瘑搴撴ā鍧楃殑鍙傛暟鍛藉悕鍜屼紶鍙傛柟寮�,閬靛惊浠ヤ笅鍘熷垯:
+
+1. 鉁� **鍛藉悕涓�鑷存��**: 鍓嶅悗绔弬鏁板悕淇濇寔涓�鑷�(椹煎嘲鍛藉悕)
+2. 鉁� **浼犲弬瑙勮寖鎬�**: GET鐢╬arams銆丳OST鐢╠ata銆丏ELETE鐢╠ata
+3. 鉁� **绫诲瀷鏄庣‘鎬�**: 鏄庣‘鍙傛暟绫诲瀷,鍚堢悊璁剧疆榛樿鍊�
+4. 鉁� **鏍¢獙瀹屾暣鎬�**: 蹇呭~鍙傛暟闇�鏍¢獙,鍙�夊弬鏁版湁榛樿鍊�
+5. 鉁� **閿欒閬垮厤**: 閬靛惊瑙勮寖閬垮厤甯歌閿欒
+
+寤鸿鍥㈤槦鎴愬憳涓ユ牸閬靛惊鏈鑼�,纭繚鍓嶅悗绔弬鏁颁紶閫掔殑涓�鑷存�у拰鍙潬鎬с��
\ No newline at end of file
diff --git "a/doc/\347\237\245\350\257\206\345\272\223\346\250\241\345\235\227\345\211\215\347\253\257\345\256\236\347\216\260\346\226\207\346\241\243.md" "b/doc/\347\237\245\350\257\206\345\272\223\346\250\241\345\235\227\345\211\215\347\253\257\345\256\236\347\216\260\346\226\207\346\241\243.md"
new file mode 100644
index 0000000..c2ea6e2
--- /dev/null
+++ "b/doc/\347\237\245\350\257\206\345\272\223\346\250\241\345\235\227\345\211\215\347\253\257\345\256\236\347\216\260\346\226\207\346\241\243.md"
@@ -0,0 +1,1212 @@
+# 鐭ヨ瘑搴撴ā鍧楀畬鏁村墠绔疄鐜版枃妗�
+
+## 涓�銆佹ā鍧楁杩�
+
+鐭ヨ瘑搴撴ā鍧楁槸涓�涓泦鎴愪簡RAG(妫�绱㈠寮虹敓鎴�)鎶�鏈殑鏅鸿兘鐭ヨ瘑绠$悊绯荤粺,鏀寔:
+- **鐭ヨ瘑搴揅RUD绠$悊** - 鍒涘缓銆佺紪杈戙�佸垹闄ゃ�佹煡璇㈢煡璇嗗簱
+- **鏂囦欢涓婁紶涓庡悜閲忓寲** - 鏀寔澶氱鏂囦欢鏍煎紡,鑷姩杩涜鍚戦噺鍒囩墖澶勭悊
+- **鏅鸿兘闂瓟** - 鍩轰簬涓婁紶鏂囦欢鍐呭杩涜AI闂瓟
+- **鏂囦欢绠$悊** - 鏌ョ湅鏂囦欢鍚戦噺鍖栫姸鎬�,鏀寔閲嶆柊澶勭悊鍜屽垹闄�
+
+### 鎶�鏈灦鏋�
+- **鍓嶇妗嗘灦**: Vue 3 + Composition API
+- **UI缁勪欢搴�**: Element Plus
+- **鍚戦噺鏁版嵁搴�**: Pinecone
+- **AI妯″瀷**: 闃块噷浜戦�氫箟鍗冮棶
+- **鏂囦欢澶勭悊**: 鏀寔docx銆亁lsx銆乸df銆乼xt銆乵d绛夋牸寮�
+
+---
+
+## 浜屻�佹枃浠剁粨鏋�
+
+```
+src/
+鈹溾攢鈹� api/
+鈹� 鈹斺攢鈹� collaborativeApproval/
+鈹� 鈹斺攢鈹� knowledgeBase.js # API鎺ュ彛灏佽
+鈹溾攢鈹� views/
+鈹� 鈹斺攢鈹� collaborativeApproval/
+鈹� 鈹斺攢鈹� knowledgeBase/
+鈹� 鈹斺攢鈹� index.vue # 涓婚〉闈㈢粍浠�
+鈹斺攢鈹� components/
+ 鈹溾攢鈹� PIMTable/
+ 鈹� 鈹斺攢鈹� PIMTable.vue # 琛ㄦ牸缁勪欢
+ 鈹斺攢鈹� Dialog/
+ 鈹斺攢鈹� FormDialog.vue # 寮圭獥缁勪欢
+```
+
+---
+
+## 涓夈�丄PI鎺ュ彛瀹氫箟
+
+### 3.1 鏂囦欢浣嶇疆
+`src/api/collaborativeApproval/knowledgeBase.js`
+
+### 3.2 瀹屾暣鎺ュ彛鍒楄〃
+
+```javascript
+import request from "@/utils/request";
+import { getToken } from '@/utils/auth';
+
+// 1. 鏌ヨ鐭ヨ瘑搴撳垪琛�(鍒嗛〉)
+export function listKnowledgeBase(query) {
+ return request({
+ url: "/knowledgeBase/getList",
+ method: "get",
+ params: query,
+ });
+}
+
+// 2. 鏂板鐭ヨ瘑搴�
+export function addKnowledgeBase(data) {
+ return request({
+ url: "/knowledgeBase/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 3. 淇敼鐭ヨ瘑搴�
+export function updateKnowledgeBase(data) {
+ return request({
+ url: "/knowledgeBase/update",
+ method: "post",
+ data: data,
+ });
+}
+
+// 4. 鍒犻櫎鐭ヨ瘑搴�
+export function delKnowledgeBase(query) {
+ return request({
+ url: "/knowledgeBase/delete",
+ method: "delete",
+ data: query,
+ });
+}
+
+// 5. 鏌ヨ鐭ヨ瘑搴撴枃浠跺悜閲忓寲鐘舵��(鍖呭惈鏂囦欢鍒楄〃)
+export function getVectorStatus(knowledgeBaseId) {
+ return request({
+ url: `/knowledgeBase/vector/status/${knowledgeBaseId}`,
+ method: "get",
+ });
+}
+
+// 6. 淇濆瓨鐭ヨ瘑搴撴枃浠跺叧鑱�(瑙﹀彂鍚戦噺鍖�)
+export function saveKnowledgeBaseFiles(data) {
+ return request({
+ url: "/knowledgeBase/file/save",
+ method: "post",
+ data,
+ });
+}
+
+// 7. 鍒犻櫎鐭ヨ瘑搴撴枃浠�
+export function deleteKnowledgeBaseFile(ids) {
+ return request({
+ url: "/knowledgeBase/file/delete",
+ method: "delete",
+ data: ids,
+ });
+}
+
+// 8. 閲嶆柊鍚戦噺鍖栨枃浠�
+export function reprocessVector(vectorId) {
+ return request({
+ url: `/knowledgeBase/vector/reprocess/${vectorId}`,
+ method: "post",
+ });
+}
+
+// 9. 鐭ヨ瘑搴撻棶绛�(娴佸紡)
+export function knowledgeChat(data, onMessage) {
+ const token = getToken();
+ return fetch(import.meta.env.VITE_APP_BASE_API + '/ai/knowledge/chat', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer ' + token
+ },
+ body: JSON.stringify(data)
+ });
+}
+
+// 10. 鏌ヨ鐭ヨ瘑搴撻棶绛斿巻鍙�
+export function getKnowledgeHistory(memoryId) {
+ return request({
+ url: `/ai/knowledge/history/${memoryId}`,
+ method: "get",
+ });
+}
+```
+
+### 3.3 鎺ュ彛鍙傛暟璇存槑
+
+#### 鐭ヨ瘑搴撳垪琛ㄦ煡璇�
+```javascript
+// 璇锋眰鍙傛暟
+{
+ current: 1, // 褰撳墠椤电爜
+ size: 20, // 姣忛〉鏉℃暟
+ title: "", // 鐭ヨ瘑鏍囬(鍙��)
+ type: "" // 鐭ヨ瘑绫诲瀷(鍙��)
+}
+
+// 鍝嶅簲鏁版嵁
+{
+ code: 200,
+ data: {
+ total: 100,
+ records: [
+ {
+ id: 1,
+ title: "鎿嶄綔鎵嬪唽",
+ type: "guide",
+ scenario: "绯荤粺鎿嶄綔鎸囧",
+ efficiency: "high",
+ problem: "鐢ㄦ埛涓嶄細鎿嶄綔绯荤粺",
+ solution: "鎸夌収鎿嶄綔鎵嬪唽鎵ц...",
+ keyPoints: "姝ラ1,姝ラ2,姝ラ3",
+ creator: "寮犱笁",
+ usageCount: 10,
+ fileCount: 3, // 鏂囦欢鏁伴噺
+ totalChunkCount: 45, // 鎬诲垏鐗囨暟閲�
+ createTime: "2026-06-08 10:00:00"
+ }
+ ]
+ }
+}
+```
+
+#### 淇濆瓨鏂囦欢鍏宠仈
+```javascript
+// 璇锋眰鍙傛暟
+{
+ knowledgeBaseId: 10, // 鐭ヨ瘑搴揑D
+ storageBlobIds: [123, 124] // 涓婁紶鏂囦欢杩斿洖鐨刡lob ID鍒楄〃
+}
+
+// 鍝嶅簲鏁版嵁
+{
+ code: 200,
+ msg: "鎿嶄綔鎴愬姛"
+}
+```
+
+#### 鍚戦噺鍖栫姸鎬佹煡璇�
+```javascript
+// 鍝嶅簲鏁版嵁
+{
+ code: 200,
+ data: [
+ {
+ id: 1,
+ storageBlobId: 123,
+ fileName: "鎿嶄綔鎵嬪唽.docx",
+ fileType: "docx",
+ vectorStatus: 2, // 0-寰呭鐞�,1-澶勭悊涓�,2-宸插畬鎴�,3-澶辫触
+ chunkCount: 15, // 鍒囩墖鏁伴噺
+ namespace: "kb-10",
+ vectorError: null,
+ createTime: "2026-06-08 10:00:00"
+ }
+ ]
+}
+```
+
+#### 鐭ヨ瘑搴撻棶绛�
+```javascript
+// 璇锋眰鍙傛暟
+{
+ knowledgeBaseId: 10,
+ memoryId: "session-xxx", // 浼氳瘽ID,鐢ㄤ簬淇濇寔涓婁笅鏂�
+ question: "濡備綍鎿嶄綔瀹℃壒娴佺▼?"
+}
+
+// 鍝嶅簲(娴佸紡杩斿洖 text/stream;charset=utf-8)
+// 鏍规嵁鐭ヨ瘑搴撳唴瀹�,瀹℃壒娴佺▼鐨勬搷浣滄楠ゅ涓�:
+// 1. 鐧诲綍绯荤粺鍚庤繘鍏ュ鎵圭鐞嗘ā鍧�...
+```
+
+---
+
+## 鍥涖�佹牳蹇冪粍浠跺疄鐜�
+
+### 4.1 涓婚〉闈㈢粨鏋�
+
+椤甸潰閲囩敤Tab椤电甯冨眬,鍖呭惈涓や釜涓昏鍔熻兘妯″潡:
+- **鐭ヨ瘑搴撶鐞�** - 鐭ヨ瘑搴揅RUD鎿嶄綔
+- **鐭ヨ瘑搴撻棶绛�** - 鍩轰簬RAG鐨勬櫤鑳介棶绛�
+
+### 4.2 鏁版嵁妯″瀷瀹氫箟
+
+```javascript
+// 鍝嶅簲寮忔暟鎹�
+const data = reactive({
+ // 鎼滅储琛ㄥ崟
+ searchForm: {
+ title: "",
+ type: "",
+ },
+
+ // 鍒嗛〉閰嶇疆
+ page: {
+ current: 1,
+ size: 20,
+ total: 0,
+ },
+
+ // 琛ㄦ牸鏁版嵁
+ tableData: [],
+ tableLoading: false,
+ selectedIds: [],
+
+ // 鐭ヨ瘑搴撹〃鍗�
+ form: {
+ title: "",
+ type: "",
+ scenario: "",
+ efficiency: "",
+ problem: "",
+ solution: "",
+ keyPoints: "",
+ creator: "",
+ usageCount: 0
+ },
+
+ // 寮圭獥鎺у埗
+ dialogVisible: false,
+ dialogTitle: "",
+ dialogType: "add", // add or edit
+ viewDialogVisible: false,
+ currentKnowledge: {},
+
+ // 鏂囦欢绠$悊
+ filesDialogVisible: false,
+ currentKnowledgeBase: null,
+ fileList: [],
+ uploadedBlobIds: [],
+ savingFiles: false,
+
+ // 鐭ヨ瘑搴撻棶绛�
+ chatDialogVisible: false,
+ messages: [],
+ inputQuestion: "",
+ chatLoading: false,
+ memoryId: ""
+});
+```
+
+### 4.3 琛ㄦ牸鍒楅厤缃�
+
+```javascript
+const tableColumn = ref([
+ {
+ label: "鐭ヨ瘑鏍囬",
+ prop: "title",
+ showOverflowTooltip: true,
+ },
+ {
+ label: "鐭ヨ瘑绫诲瀷",
+ prop: "type",
+ dataType: "tag",
+ formatData: (params) => getKnowledgeTypeLabel(params),
+ formatType: (params) => getKnowledgeTypeTagType(params)
+ },
+ {
+ label: "閫傜敤鍦烘櫙",
+ prop: "scenario",
+ width: 150,
+ showOverflowTooltip: true,
+ },
+ {
+ label: "瑙e喅鏁堢巼",
+ prop: "efficiency",
+ dataType: "tag",
+ formatData: (params) => {
+ const efficiencyMap = {
+ high: "鏄捐憲鎻愬崌",
+ medium: "涓�鑸彁鍗�",
+ low: "杞诲井鎻愬崌"
+ };
+ return efficiencyMap[params] || params;
+ },
+ formatType: (params) => {
+ const typeMap = {
+ high: "success",
+ medium: "warning",
+ low: "info"
+ };
+ return typeMap[params] || "info";
+ }
+ },
+ {
+ label: "鏂囦欢鏁伴噺",
+ prop: "fileCount",
+ width: 100,
+ align: "center"
+ },
+ {
+ label: "鍒囩墖鏁伴噺",
+ prop: "totalChunkCount",
+ width: 100,
+ align: "center"
+ },
+ {
+ label: "浣跨敤娆℃暟",
+ prop: "usageCount",
+ width: 100,
+ align: "center"
+ },
+ {
+ label: "鍒涘缓浜�",
+ prop: "creator",
+ width: 120,
+ },
+ {
+ label: "鍒涘缓鏃堕棿",
+ prop: "createTime",
+ width: 180,
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 280,
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: (row) => openForm("edit", row)
+ },
+ {
+ name: "鏂囦欢",
+ type: "text",
+ clickFun: (row) => openFilesDialog(row)
+ },
+ {
+ name: "闂瓟",
+ type: "text",
+ clickFun: (row) => openChatDialog(row)
+ },
+ {
+ name: "璇︽儏",
+ type: "text",
+ clickFun: (row) => viewKnowledge(row)
+ }
+ ]
+ }
+]);
+```
+
+---
+
+## 浜斻�佹牳蹇冧笟鍔¢�昏緫
+
+### 5.1 鏂囦欢涓婁紶涓庡悜閲忓寲娴佺▼
+
+#### 娴佺▼鍥�
+```
+鐢ㄦ埛鐐瑰嚮"涓婁紶鏂囦欢"
+ 鈫�
+閫夋嫨鏂囦欢(鏀寔澶氶��)
+ 鈫�
+鍓嶇鏍¢獙鏂囦欢绫诲瀷鍜屽ぇ灏�
+ 鈫�
+璋冪敤 /common/upload 涓婁紶鏂囦欢
+ 鈫�
+鑾峰彇 storageBlobId 鍒楄〃
+ 鈫�
+鐢ㄦ埛鐐瑰嚮"淇濆瓨鏂囦欢鍏宠仈"
+ 鈫�
+璋冪敤 /knowledgeBase/file/save
+ 鈫�
+鍚庣鍒涘缓鍚戦噺璁板綍 + 寮傛瑙﹀彂鍚戦噺鍖�
+ 鈫�
+鍓嶇寤惰繜1绉掑埛鏂版枃浠跺垪琛�
+ 鈫�
+鏄剧ず鍚戦噺鍖栫姸鎬�(寰呭鐞嗏啋澶勭悊涓啋宸插畬鎴�)
+```
+
+#### 浠g爜瀹炵幇
+
+```vue
+<template>
+ <div class="file-manager">
+ <!-- 鏂囦欢涓婁紶 -->
+ <div class="upload-section">
+ <el-upload
+ :action="uploadUrl"
+ :headers="uploadHeaders"
+ :on-success="handleUploadSuccess"
+ :on-error="handleUploadError"
+ :before-upload="beforeUpload"
+ multiple
+ :show-file-list="false"
+ accept=".txt,.md,.docx,.xlsx,.xls,.pdf"
+ >
+ <el-button type="primary">涓婁紶鏂囦欢</el-button>
+ </el-upload>
+ <el-button
+ type="success"
+ @click="saveFiles"
+ :disabled="uploadedBlobIds.length === 0"
+ :loading="savingFiles"
+ >
+ 淇濆瓨鏂囦欢鍏宠仈
+ </el-button>
+ </div>
+
+ <!-- 鏂囦欢鍒楄〃 -->
+ <el-table :data="fileList" style="margin-top: 20px" border>
+ <el-table-column prop="fileName" label="鏂囦欢鍚�" />
+ <el-table-column prop="fileType" label="鏂囦欢绫诲瀷" width="100" />
+ <el-table-column label="鍚戦噺鍖栫姸鎬�" width="120">
+ <template #default="{ row }">
+ <el-tag :type="getStatusType(row.vectorStatus)">
+ {{ getStatusText(row.vectorStatus) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="chunkCount" label="鍒囩墖鏁�" width="100" />
+ <el-table-column label="鎿嶄綔" width="150">
+ <template #default="{ row }">
+ <el-button
+ v-if="row.vectorStatus === 3"
+ type="text"
+ @click="reprocessFile(row)"
+ >
+ 閲嶆柊澶勭悊
+ </el-button>
+ <el-button type="text" @click="deleteFile(row)" style="color: #f56c6c">
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+</template>
+
+<script setup>
+import { getToken } from "@/utils/auth";
+
+// 鏂囦欢涓婁紶閰嶇疆
+const uploadUrl = import.meta.env.VITE_APP_BASE_API + "/common/upload";
+const uploadHeaders = { Authorization: "Bearer " + getToken() };
+const uploadedBlobIds = ref([]);
+const fileList = ref([]);
+
+// 涓婁紶鍓嶆牎楠�
+const beforeUpload = (file) => {
+ const allowedTypes = ['.txt', '.md', '.docx', '.xlsx', '.xls', '.pdf'];
+ const fileName = file.name.toLowerCase();
+ const isAllowed = allowedTypes.some(type => fileName.endsWith(type));
+
+ if (!isAllowed) {
+ ElMessage.error('鍙敮鎸� txt銆乵d銆乨ocx銆亁lsx銆亁ls銆乸df 鏍煎紡鐨勬枃浠�');
+ return false;
+ }
+
+ const isLt50M = file.size / 1024 / 1024 < 50;
+ if (!isLt50M) {
+ ElMessage.error('鏂囦欢澶у皬涓嶈兘瓒呰繃 50MB');
+ return false;
+ }
+
+ return true;
+};
+
+// 涓婁紶鎴愬姛
+const handleUploadSuccess = (response, file) => {
+ if (response.code === 200) {
+ uploadedBlobIds.value.push(response.data.id);
+ ElMessage.success(`鏂囦欢 ${file.name} 涓婁紶鎴愬姛`);
+ } else {
+ ElMessage.error(response.msg || "涓婁紶澶辫触");
+ }
+};
+
+// 淇濆瓨鏂囦欢鍏宠仈
+const saveFiles = async () => {
+ if (uploadedBlobIds.value.length === 0) {
+ ElMessage.warning("璇峰厛涓婁紶鏂囦欢");
+ return;
+ }
+
+ savingFiles.value = true;
+ try {
+ await saveKnowledgeBaseFiles({
+ knowledgeBaseId: currentKnowledgeBase.value.id,
+ storageBlobIds: uploadedBlobIds.value
+ });
+
+ ElMessage.success("鏂囦欢鍏宠仈淇濆瓨鎴愬姛,姝e湪鍚庡彴澶勭悊鍚戦噺鍖�");
+ uploadedBlobIds.value = [];
+
+ // 寤惰繜鍒锋柊鏂囦欢鍒楄〃
+ setTimeout(() => {
+ loadFileList();
+ }, 1000);
+ } catch (error) {
+ console.error("淇濆瓨鏂囦欢鍏宠仈澶辫触:", error);
+ ElMessage.error("淇濆瓨鏂囦欢鍏宠仈澶辫触");
+ } finally {
+ savingFiles.value = false;
+ }
+};
+
+// 鍔犺浇鏂囦欢鍒楄〃
+const loadFileList = async () => {
+ if (!currentKnowledgeBase.value?.id) return;
+
+ try {
+ const res = await getVectorStatus(currentKnowledgeBase.value.id);
+ fileList.value = res.data || [];
+ } catch (error) {
+ console.error("鍔犺浇鏂囦欢鍒楄〃澶辫触:", error);
+ ElMessage.error("鍔犺浇鏂囦欢鍒楄〃澶辫触");
+ }
+};
+
+// 鐘舵�佹槧灏�
+const getStatusText = (status) => {
+ const map = {
+ 0: '寰呭鐞�',
+ 1: '澶勭悊涓�',
+ 2: '宸插畬鎴�',
+ 3: '澶辫触'
+ };
+ return map[status] || '鏈煡';
+};
+
+const getStatusType = (status) => {
+ const map = {
+ 0: 'info',
+ 1: 'warning',
+ 2: 'success',
+ 3: 'danger'
+ };
+ return map[status] || 'info';
+};
+
+// 閲嶆柊澶勭悊鍚戦噺鍖栫殑鏂囦欢
+const reprocessFile = async (row) => {
+ try {
+ await reprocessVector(row.id);
+ ElMessage.success("宸查噸鏂版彁浜ゅ悜閲忓寲浠诲姟");
+ setTimeout(() => {
+ loadFileList();
+ }, 1000);
+ } catch (error) {
+ console.error("閲嶆柊澶勭悊澶辫触:", error);
+ ElMessage.error("閲嶆柊澶勭悊澶辫触");
+ }
+};
+
+// 鍒犻櫎鏂囦欢
+const deleteFile = async (row) => {
+ try {
+ await ElMessageBox.confirm(
+ "纭畾瑕佸垹闄よ鏂囦欢鍚�?鍒犻櫎鍚庡皢鏃犳硶鎭㈠鍚戦噺鏁版嵁",
+ "鍒犻櫎纭",
+ {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning"
+ }
+ );
+
+ await deleteKnowledgeBaseFiles([row.id]);
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ loadFileList();
+ } catch (error) {
+ if (error !== 'cancel') {
+ console.error("鍒犻櫎鏂囦欢澶辫触:", error);
+ ElMessage.error("鍒犻櫎鏂囦欢澶辫触");
+ }
+ }
+};
+</script>
+```
+
+### 5.2 鐭ヨ瘑搴撻棶绛旀祦绋�
+
+#### 娴佺▼鍥�
+```
+鐢ㄦ埛閫夋嫨鐭ヨ瘑搴�
+ 鈫�
+杈撳叆闂骞舵彁浜�
+ 鈫�
+鍓嶇鐢熸垚memoryId(鐢ㄤ簬浼氳瘽涓婁笅鏂�)
+ 鈫�
+璋冪敤 /ai/knowledge/chat (娴佸紡鎺ュ彛)
+ 鈫�
+鍚庣澶勭悊:
+ - 瀵归棶棰樿繘琛屽悜閲忓寲
+ - 鍦≒inecone涓绱㈢浉鍏冲垏鐗�
+ - 鏋勫缓涓婁笅鏂嘝rompt
+ - 璋冪敤LLM鐢熸垚鍥炵瓟
+ 鈫�
+娴佸紡杩斿洖AI鍥炵瓟
+ 鈫�
+鍓嶇瀹炴椂鏄剧ず鍥炵瓟鍐呭
+ 鈫�
+鑷姩婊氬姩鍒板簳閮�
+```
+
+#### 浠g爜瀹炵幇
+
+```vue
+<template>
+ <div class="knowledge-chat">
+ <div class="chat-header">
+ <el-tag type="success">褰撳墠鐭ヨ瘑搴�: {{ currentKnowledgeBase?.title }}</el-tag>
+ </div>
+
+ <!-- 瀵硅瘽鍖哄煙 -->
+ <div class="chat-messages" ref="chatMessagesRef">
+ <div
+ v-for="(msg, index) in messages"
+ :key="index"
+ :class="['message', msg.role]"
+ >
+ <div class="message-role">{{ msg.role === 'user' ? '鎴�' : 'AI鍔╂墜' }}</div>
+ <div class="message-content">{{ msg.content }}</div>
+ </div>
+ <div v-if="chatLoading" class="message assistant">
+ <div class="message-role">AI鍔╂墜</div>
+ <div class="message-content typing">姝e湪鎬濊�冧腑...</div>
+ </div>
+ </div>
+
+ <!-- 杈撳叆妗� -->
+ <div class="chat-input">
+ <el-input
+ v-model="inputQuestion"
+ placeholder="璇疯緭鍏ラ棶棰�,鎸夊洖杞﹀彂閫�"
+ @keyup.enter="sendMessage"
+ :disabled="chatLoading"
+ >
+ <template #append>
+ <el-button @click="sendMessage" :loading="chatLoading">鍙戦��</el-button>
+ </template>
+ </el-input>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { nextTick } from 'vue';
+import { getToken } from "@/utils/auth";
+
+const messages = ref([]);
+const inputQuestion = ref("");
+const chatLoading = ref(false);
+const memoryId = ref("");
+const chatMessagesRef = ref();
+
+// 鎵撳紑闂瓟寮圭獥
+const openChatDialog = (row) => {
+ currentKnowledgeBase.value = row;
+ chatDialogVisible.value = true;
+ memoryId.value = crypto.randomUUID(); // 鐢熸垚鍞竴浼氳瘽ID
+ messages.value = [];
+ inputQuestion.value = "";
+};
+
+// 鍙戦�佹秷鎭�
+const sendMessage = async () => {
+ if (!inputQuestion.value.trim()) {
+ ElMessage.warning("璇疯緭鍏ラ棶棰�");
+ return;
+ }
+
+ const question = inputQuestion.value.trim();
+
+ // 娣诲姞鐢ㄦ埛娑堟伅
+ messages.value.push({
+ role: 'user',
+ content: question
+ });
+
+ inputQuestion.value = "";
+ chatLoading.value = true;
+
+ // 婊氬姩鍒板簳閮�
+ await nextTick();
+ scrollToBottom();
+
+ try {
+ // 娴佸紡璇锋眰
+ const response = await fetch('/api/ai/knowledge/chat', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer ' + getToken()
+ },
+ body: JSON.stringify({
+ knowledgeBaseId: currentKnowledgeBase.value.id,
+ memoryId: memoryId.value,
+ question: question
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error('璇锋眰澶辫触');
+ }
+
+ // 澶勭悊SSE娴佸紡鍝嶅簲
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder();
+ let aiContent = '';
+
+ messages.value.push({ role: 'assistant', content: '' });
+
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+
+ const text = decoder.decode(value);
+ aiContent += text;
+ messages.value[messages.value.length - 1].content = aiContent;
+
+ // 婊氬姩鍒板簳閮�
+ await nextTick();
+ scrollToBottom();
+ }
+ } catch (error) {
+ console.error("闂瓟璇锋眰澶辫触:", error);
+ ElMessage.error("闂瓟璇锋眰澶辫触,璇风◢鍚庨噸璇�");
+ messages.value.push({
+ role: 'assistant',
+ content: '鎶辨瓑,鍙戠敓浜嗛敊璇�,璇风◢鍚庨噸璇�'
+ });
+ } finally {
+ chatLoading.value = false;
+ }
+};
+
+// 婊氬姩鍒板簳閮�
+const scrollToBottom = () => {
+ if (chatMessagesRef.value) {
+ chatMessagesRef.value.scrollTop = chatMessagesRef.value.scrollHeight;
+ }
+};
+</script>
+
+<style scoped>
+.knowledge-chat {
+ display: flex;
+ flex-direction: column;
+ height: 500px;
+}
+
+.chat-messages {
+ flex: 1;
+ overflow-y: auto;
+ padding: 16px;
+ background: #f5f7fa;
+ border-radius: 8px;
+ margin-bottom: 16px;
+}
+
+.message {
+ margin-bottom: 16px;
+ max-width: 80%;
+}
+
+.message.user {
+ margin-left: auto;
+ text-align: right;
+}
+
+.message.assistant {
+ margin-right: auto;
+}
+
+.message-role {
+ font-size: 12px;
+ color: #909399;
+ margin-bottom: 4px;
+}
+
+.message-content {
+ display: inline-block;
+ padding: 10px 14px;
+ border-radius: 8px;
+ line-height: 1.6;
+ word-wrap: break-word;
+ white-space: pre-wrap;
+}
+
+.message.user .message-content {
+ background: #409eff;
+ color: white;
+}
+
+.message.assistant .message-content {
+ background: white;
+ color: #303133;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.typing {
+ animation: typing 1.5s infinite;
+}
+
+@keyframes typing {
+ 0%, 50%, 100% { opacity: 1; }
+ 25%, 75% { opacity: 0.5; }
+}
+</style>
+```
+
+---
+
+## 鍏�佸叧閿疄鐜扮粏鑺�
+
+### 6.1 鏂囦欢涓婁紶閰嶇疆
+
+```javascript
+// 涓婁紶鍦板潃
+const uploadUrl = import.meta.env.VITE_APP_BASE_API + "/common/upload";
+
+// 璇锋眰澶�(蹇呴』鎼哄甫Token)
+const uploadHeaders = {
+ Authorization: "Bearer " + getToken()
+};
+
+// 鏀寔鐨勬枃浠剁被鍨�
+const acceptTypes = '.txt,.md,.docx,.xlsx,.xls,.pdf';
+
+// 鏂囦欢澶у皬闄愬埗
+const maxSize = 50 * 1024 * 1024; // 50MB
+```
+
+### 6.2 鍚戦噺鍖栫姸鎬佽疆璇�
+
+```javascript
+// 寮�濮嬭疆璇㈠悜閲忓寲鐘舵��
+const startVectorStatusPolling = () => {
+ const timer = setInterval(async () => {
+ const res = await getVectorStatus(currentKnowledgeBase.value.id);
+ const hasProcessing = res.data.some(item => item.vectorStatus === 1);
+
+ if (!hasProcessing) {
+ clearInterval(timer);
+ }
+
+ fileList.value = res.data;
+ }, 3000); // 姣�3绉掕疆璇竴娆�
+};
+```
+
+### 6.3 娴佸紡鍝嶅簲澶勭悊
+
+```javascript
+// 浣跨敤 Fetch API 澶勭悊娴佸紡鍝嶅簲
+const response = await fetch('/api/ai/knowledge/chat', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer ' + getToken()
+ },
+ body: JSON.stringify({
+ knowledgeBaseId: 10,
+ memoryId: "session-xxx",
+ question: "闂鍐呭"
+ })
+});
+
+// 鑾峰彇鍙娴�
+const reader = response.body.getReader();
+const decoder = new TextDecoder();
+
+// 閫愬潡璇诲彇鏁版嵁
+while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+
+ const text = decoder.decode(value);
+ // 澶勭悊鏂囨湰鍧�
+ processText(text);
+}
+```
+
+### 6.4 浼氳瘽绠$悊
+
+```javascript
+// 鐢熸垚鍞竴浼氳瘽ID
+const memoryId = crypto.randomUUID();
+
+// 鎴栦娇鐢ㄦ椂闂存埑
+const memoryId = 'kb-chat-' + Date.now();
+
+// 浼氳瘽ID鐢ㄤ簬:
+// 1. 淇濇寔瀵硅瘽涓婁笅鏂�
+// 2. 鏀寔澶氳疆瀵硅瘽
+// 3. 鏌ヨ鍘嗗彶璁板綍
+```
+
+---
+
+## 涓冦�佺姸鎬佺鐞�
+
+### 7.1 鍚戦噺鍖栫姸鎬佸畾涔�
+
+| 鐘舵�佸�� | 鐘舵�佸悕绉� | 璇存槑 | 鏍囩棰滆壊 |
+|--------|----------|------|----------|
+| 0 | 寰呭鐞� | 鏂囦欢宸蹭笂浼�,绛夊緟鍚戦噺鍖栧鐞� | info(鐏拌壊) |
+| 1 | 澶勭悊涓� | 姝e湪杩涜鍚戦噺鍒囩墖澶勭悊 | warning(姗欒壊) |
+| 2 | 宸插畬鎴� | 鍚戦噺鍖栧畬鎴�,鍙繘琛屾绱㈤棶绛� | success(缁胯壊) |
+| 3 | 澶辫触 | 鍚戦噺鍖栧け璐�,闇�閲嶆柊澶勭悊 | danger(绾㈣壊) |
+
+### 7.2 鐭ヨ瘑绫诲瀷閰嶇疆
+
+```javascript
+// 浣跨敤瀛楀吀閰嶇疆鐭ヨ瘑绫诲瀷
+const { knowledge_type } = proxy.useDict("knowledge_type");
+
+// 绀轰緥鏁版嵁
+const knowledgeTypeOptions = [
+ { value: 'contract', label: '鍚堝悓鐭ヨ瘑', elTagType: 'success' },
+ { value: 'approval', label: '瀹℃壒娴佺▼', elTagType: 'warning' },
+ { value: 'solution', label: '瑙e喅鏂规', elTagType: 'primary' },
+ { value: 'experience', label: '缁忛獙鍒嗕韩', elTagType: 'info' },
+ { value: 'guide', label: '鎿嶄綔鎸囧崡', elTagType: 'danger' }
+];
+```
+
+### 7.3 瑙e喅鏁堢巼鏄犲皠
+
+```javascript
+const efficiencyMap = {
+ high: { label: '鏄捐憲鎻愬崌', color: 'success', score: 40, time: '2-3澶�' },
+ medium: { label: '涓�鑸彁鍗�', color: 'warning', score: 25, time: '1-2澶�' },
+ low: { label: '杞诲井鎻愬崌', color: 'info', score: 15, time: '0.5-1澶�' }
+};
+```
+
+---
+
+## 鍏�佹牱寮忚璁�
+
+### 8.1 鏂囦欢绠$悊鏍峰紡
+
+```css
+.file-manager {
+ padding: 20px 0;
+}
+
+.upload-section {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+```
+
+### 8.2 闂瓟鐣岄潰鏍峰紡
+
+```css
+.knowledge-chat {
+ display: flex;
+ flex-direction: column;
+ height: 500px;
+}
+
+.chat-messages {
+ flex: 1;
+ overflow-y: auto;
+ padding: 16px;
+ background: #f5f7fa;
+ border-radius: 8px;
+ margin-bottom: 16px;
+}
+
+.message {
+ margin-bottom: 16px;
+ max-width: 80%;
+}
+
+.message.user {
+ margin-left: auto;
+ text-align: right;
+}
+
+.message.assistant {
+ margin-right: auto;
+}
+
+.message-content {
+ display: inline-block;
+ padding: 10px 14px;
+ border-radius: 8px;
+ line-height: 1.6;
+ word-wrap: break-word;
+ white-space: pre-wrap;
+}
+
+.message.user .message-content {
+ background: #409eff;
+ color: white;
+}
+
+.message.assistant .message-content {
+ background: white;
+ color: #303133;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+```
+
+---
+
+## 涔濄�佹敞鎰忎簨椤�
+
+### 9.1 鏂囦欢涓婁紶
+
+1. **蹇呴』璋冪敤淇濆瓨鎺ュ彛**: 涓婁紶鎴愬姛鍚庡繀椤昏皟鐢� `/knowledgeBase/file/save` 鎵嶈兘瑙﹀彂鍚戦噺鍖�
+2. **鏂囦欢绫诲瀷闄愬埗**: 鍙敮鎸� txt銆乵d銆乨ocx銆亁lsx銆亁ls銆乸df 鏍煎紡
+3. **鏂囦欢澶у皬闄愬埗**: 鍗曟枃浠舵渶澶� 50MB
+4. **寮傛澶勭悊**: 鍚戦噺鍖栨槸寮傛澶勭悊,涓嶄細闃诲鐢ㄦ埛鎿嶄綔
+
+### 9.2 鍚戦噺鍖栫姸鎬�
+
+1. **鐘舵�佽疆璇�**: 寤鸿姣�3-5绉掕疆璇竴娆$姸鎬�
+2. **鍋滄杞**: 褰撴墍鏈夋枃浠剁姸鎬侀兘涓嶆槸"澶勭悊涓�"鏃跺仠姝㈣疆璇�
+3. **澶辫触澶勭悊**: 鐘舵�佷负"澶辫触"鏃跺彲鐐瑰嚮"閲嶆柊澶勭悊"鎸夐挳
+
+### 9.3 鐭ヨ瘑搴撻棶绛�
+
+1. **浼氳瘽ID**: 姣忔鎵撳紑闂瓟寮圭獥闇�瑕佺敓鎴愭柊鐨� memoryId
+2. **娴佸紡澶勭悊**: 浣跨敤 Fetch API 澶勭悊娴佸紡鍝嶅簲,涓嶆敮鎸� axios
+3. **閿欒澶勭悊**: 闇�瑕佸鐞嗙綉缁滈敊璇拰AI鍝嶅簲閿欒
+4. **鑷姩婊氬姩**: 姣忔鏀跺埌鏂版秷鎭嚜鍔ㄦ粴鍔ㄥ埌搴曢儴
+
+### 9.4 鏁版嵁涓�鑷存��
+
+1. **鍒犻櫎鐭ヨ瘑搴�**: 闇�瑕佸悓鏃跺垹闄ゅ叧鑱旂殑鏂囦欢鍜屽悜閲忔暟鎹�
+2. **鍒犻櫎鏂囦欢**: 鍒犻櫎鏂囦欢鏃跺悓姝ュ垹闄ゅ悜閲忓簱涓殑鐩稿叧鍒囩墖
+3. **鍒锋柊鍒楄〃**: 鏂囦欢鎿嶄綔鍚庨渶瑕佸埛鏂扮煡璇嗗簱鍒楄〃,鏇存柊鏂囦欢鏁伴噺鍜屽垏鐗囨暟閲�
+
+---
+
+## 鍗併�佹祴璇曟鏌ユ竻鍗�
+
+### 10.1 鐭ヨ瘑搴撶鐞�
+
+- [ ] 鏂板鐭ヨ瘑搴撴垚鍔�
+- [ ] 缂栬緫鐭ヨ瘑搴撴垚鍔�
+- [ ] 鍒犻櫎鐭ヨ瘑搴撴垚鍔�(鍗曚釜/鎵归噺)
+- [ ] 鎼滅储鍔熻兘姝e父(鎸夋爣棰樸�佺被鍨�)
+- [ ] 鍒嗛〉鍔熻兘姝e父
+- [ ] 瀵煎嚭鍔熻兘姝e父
+
+### 10.2 鏂囦欢涓婁紶
+
+- [ ] 鍗曟枃浠朵笂浼犳垚鍔�
+- [ ] 澶氭枃浠朵笂浼犳垚鍔�
+- [ ] 鏂囦欢绫诲瀷鏍¢獙姝e父
+- [ ] 鏂囦欢澶у皬鏍¢獙姝e父
+- [ ] 淇濆瓨鏂囦欢鍏宠仈鎴愬姛
+- [ ] 鍚戦噺鍖栫姸鎬佹纭樉绀�
+
+### 10.3 鏂囦欢绠$悊
+
+- [ ] 鏌ョ湅鏂囦欢鍒楄〃姝e父
+- [ ] 鍚戦噺鍖栫姸鎬佽疆璇㈡甯�
+- [ ] 閲嶆柊澶勭悊澶辫触鏂囦欢鎴愬姛
+- [ ] 鍒犻櫎鏂囦欢鎴愬姛
+- [ ] 鏂囦欢棰勮/涓嬭浇姝e父
+
+### 10.4 鐭ヨ瘑搴撻棶绛�
+
+- [ ] 闂瓟寮圭獥鎵撳紑姝e父
+- [ ] 鍙戦�侀棶棰樻垚鍔�
+- [ ] 娴佸紡鍝嶅簲鏄剧ず姝e父
+- [ ] 澶氳疆瀵硅瘽姝e父
+- [ ] 鑷姩婊氬姩鍒板簳閮�
+- [ ] 閿欒澶勭悊姝e父
+
+---
+
+## 鍗佷竴銆佸父瑙侀棶棰�
+
+### Q1: 鏂囦欢涓婁紶鍚庢病鏈夎Е鍙戝悜閲忓寲?
+
+**A**: 妫�鏌ユ槸鍚﹁皟鐢ㄤ簡 `/knowledgeBase/file/save` 鎺ュ彛銆備笂浼犳垚鍔熷悗蹇呴』璋冪敤姝ゆ帴鍙f墠鑳借Е鍙戝悜閲忓寲銆�
+
+### Q2: 鍚戦噺鍖栫姸鎬佷竴鐩存槸"澶勭悊涓�"?
+
+**A**: 鍙兘鍘熷洜:
+1. 鍚庡彴鏈嶅姟鏈惎鍔�
+2. Embedding妯″瀷璋冪敤澶辫触
+3. Pinecone杩炴帴澶辫触
+
+寤鸿鏌ョ湅鍚庡彴鏃ュ織鎺掓煡闂銆�
+
+### Q3: 闂瓟杩斿洖绌哄唴瀹�?
+
+**A**: 鍙兘鍘熷洜:
+1. 鐭ヨ瘑搴撲腑娌℃湁鏂囦欢
+2. 鏂囦欢鏈畬鎴愬悜閲忓寲
+3. 妫�绱㈢浉浼煎害浣庝簬闃堝��
+
+寤鸿妫�鏌ユ枃浠舵暟閲忓拰鍚戦噺鍖栫姸鎬併��
+
+### Q4: 娴佸紡鍝嶅簲鏄剧ず涔辩爜?
+
+**A**: 纭繚璇锋眰澶村寘鍚纭殑缂栫爜璁剧疆:
+```javascript
+headers: {
+ 'Content-Type': 'application/json'
+}
+```
+
+### Q5: 濡備綍璋冭瘯娴佸紡鎺ュ彛?
+
+**A**: 浣跨敤娴忚鍣ㄥ紑鍙戣�呭伐鍏�:
+1. 鎵撳紑 Network 鏍囩
+2. 鎵惧埌 `/ai/knowledge/chat` 璇锋眰
+3. 鏌ョ湅 Response 鏍囩,鍙互鐪嬪埌娴佸紡杩斿洖鐨勫唴瀹�
+
+---
+
+## 鍗佷簩銆佷紭鍖栧缓璁�
+
+### 12.1 鎬ц兘浼樺寲
+
+1. **铏氭嫙婊氬姩**: 娑堟伅鍒楄〃瓒呰繃100鏉℃椂浣跨敤铏氭嫙婊氬姩
+2. **闃叉姈鑺傛祦**: 鎼滅储杈撳叆浣跨敤闃叉姈,鐘舵�佽疆璇娇鐢ㄨ妭娴�
+3. **鎳掑姞杞�**: 鏂囦欢鍒楄〃浣跨敤鎳掑姞杞�
+
+### 12.2 鐢ㄦ埛浣撻獙浼樺寲
+
+1. **杩涘害鎻愮ず**: 鍚戦噺鍖栨椂鏄剧ず杩涘害鏉�
+2. **蹇嵎閿�**: 鏀寔蹇嵎閿搷浣�(濡� Ctrl+Enter 鍙戦��)
+3. **鍘嗗彶璁板綍**: 鏀寔鏌ョ湅鍘嗗彶闂瓟璁板綍
+4. **瀵煎嚭瀵硅瘽**: 鏀寔瀵煎嚭瀵硅瘽鍐呭
+
+### 12.3 鍔熻兘鎵╁睍
+
+1. **鏂囦欢棰勮**: 鏀寔鍦ㄧ嚎棰勮鏂囦欢鍐呭
+2. **鎵归噺鎿嶄綔**: 鏀寔鎵归噺鍒犻櫎銆佹壒閲忛噸鏂板鐞�
+3. **鍚戦噺鍖栭厤缃�**: 鍏佽鐢ㄦ埛閰嶇疆鍒囩墖澶у皬銆侀噸鍙犲ぇ灏�
+4. **鐩镐技搴﹂槇鍊�**: 鍏佽鐢ㄦ埛璋冩暣妫�绱㈢浉浼煎害闃堝��
+
+---
+
+## 鍗佷笁銆佹洿鏂版棩蹇�
+
+### v1.0.0 (2026-06-08)
+- 鉁� 瀹屾垚鐭ヨ瘑搴揅RUD鍔熻兘
+- 鉁� 瀹屾垚鏂囦欢涓婁紶涓庡悜閲忓寲鍔熻兘
+- 鉁� 瀹屾垚鐭ヨ瘑搴撻棶绛斿姛鑳�
+- 鉁� 瀹屾垚鏂囦欢绠$悊鍔熻兘
+- 鉁� 瀹屾垚鍚戦噺鍖栫姸鎬佹樉绀�
+
+### v1.1.0 (璁″垝涓�)
+- 馃敳 娣诲姞鍘嗗彶璁板綍鏌ヨ
+- 馃敳 娣诲姞鎵归噺鎿嶄綔鍔熻兘
+- 馃敳 娣诲姞鏂囦欢棰勮鍔熻兘
+- 馃敳 浼樺寲鍚戦噺鍖栬繘搴︽樉绀�
\ No newline at end of file
diff --git a/multiple/assets/favicon/BLSGRfavicon.ico b/multiple/assets/favicon/BLSGRfavicon.ico
new file mode 100644
index 0000000..6d0676f
--- /dev/null
+++ b/multiple/assets/favicon/BLSGRfavicon.ico
Binary files differ
diff --git a/multiple/assets/favicon/BTWfavicon.ico b/multiple/assets/favicon/BTWfavicon.ico
new file mode 100644
index 0000000..fc89fb4
--- /dev/null
+++ b/multiple/assets/favicon/BTWfavicon.ico
Binary files differ
diff --git a/multiple/assets/favicon/DCHXfavicon.ico b/multiple/assets/favicon/DCHXfavicon.ico
new file mode 100644
index 0000000..e99ff53
--- /dev/null
+++ b/multiple/assets/favicon/DCHXfavicon.ico
Binary files differ
diff --git a/multiple/assets/favicon/HTHBfavicon.ico b/multiple/assets/favicon/HTHBfavicon.ico
new file mode 100644
index 0000000..01a5e9a
--- /dev/null
+++ b/multiple/assets/favicon/HTHBfavicon.ico
Binary files differ
diff --git a/multiple/assets/favicon/JFHYfavicon.ico b/multiple/assets/favicon/JFHYfavicon.ico
new file mode 100644
index 0000000..557913d
--- /dev/null
+++ b/multiple/assets/favicon/JFHYfavicon.ico
Binary files differ
diff --git a/multiple/assets/favicon/KHYYfavicon.ico b/multiple/assets/favicon/KHYYfavicon.ico
new file mode 100644
index 0000000..9982880
--- /dev/null
+++ b/multiple/assets/favicon/KHYYfavicon.ico
Binary files differ
diff --git a/multiple/assets/favicon/LSYfavicon.ico b/multiple/assets/favicon/LSYfavicon.ico
new file mode 100644
index 0000000..8710d1a
--- /dev/null
+++ b/multiple/assets/favicon/LSYfavicon.ico
Binary files differ
diff --git a/multiple/assets/favicon/NYfavicon.ico b/multiple/assets/favicon/NYfavicon.ico
new file mode 100644
index 0000000..a0c9f8b
--- /dev/null
+++ b/multiple/assets/favicon/NYfavicon.ico
Binary files differ
diff --git a/multiple/assets/favicon/QFSWfavicon.ico b/multiple/assets/favicon/QFSWfavicon.ico
new file mode 100644
index 0000000..79ac2a8
--- /dev/null
+++ b/multiple/assets/favicon/QFSWfavicon.ico
Binary files differ
diff --git a/multiple/assets/favicon/RFSYLogo.png b/multiple/assets/favicon/RFSYLogo.png
new file mode 100644
index 0000000..d9eaab1
--- /dev/null
+++ b/multiple/assets/favicon/RFSYLogo.png
Binary files differ
diff --git a/multiple/assets/favicon/RFSYfavicon.ico b/multiple/assets/favicon/RFSYfavicon.ico
new file mode 100644
index 0000000..ef5b5d3
--- /dev/null
+++ b/multiple/assets/favicon/RFSYfavicon.ico
Binary files differ
diff --git a/multiple/assets/favicon/SDZZfavicon.ico b/multiple/assets/favicon/SDZZfavicon.ico
new file mode 100644
index 0000000..bc93525
--- /dev/null
+++ b/multiple/assets/favicon/SDZZfavicon.ico
Binary files differ
diff --git a/multiple/assets/favicon/XHKSfavicon.ico b/multiple/assets/favicon/XHKSfavicon.ico
new file mode 100644
index 0000000..01a1259
--- /dev/null
+++ b/multiple/assets/favicon/XHKSfavicon.ico
Binary files differ
diff --git a/multiple/assets/favicon/XLGSfavicon.ico b/multiple/assets/favicon/XLGSfavicon.ico
new file mode 100644
index 0000000..75963b9
--- /dev/null
+++ b/multiple/assets/favicon/XLGSfavicon.ico
Binary files differ
diff --git a/multiple/assets/favicon/YSSfavicon.ico b/multiple/assets/favicon/YSSfavicon.ico
new file mode 100644
index 0000000..924e94a
--- /dev/null
+++ b/multiple/assets/favicon/YSSfavicon.ico
Binary files differ
diff --git a/multiple/assets/favicon/ZDJCfavicon.ico b/multiple/assets/favicon/ZDJCfavicon.ico
new file mode 100644
index 0000000..ca5d236
--- /dev/null
+++ b/multiple/assets/favicon/ZDJCfavicon.ico
Binary files differ
diff --git a/multiple/assets/logo/BLSGRLogo.png b/multiple/assets/logo/BLSGRLogo.png
new file mode 100644
index 0000000..27a029c
--- /dev/null
+++ b/multiple/assets/logo/BLSGRLogo.png
Binary files differ
diff --git a/multiple/assets/logo/BTWLogo.png b/multiple/assets/logo/BTWLogo.png
new file mode 100644
index 0000000..c824d33
--- /dev/null
+++ b/multiple/assets/logo/BTWLogo.png
Binary files differ
diff --git a/multiple/assets/logo/DCHXLogo.png b/multiple/assets/logo/DCHXLogo.png
new file mode 100644
index 0000000..da7d2f2
--- /dev/null
+++ b/multiple/assets/logo/DCHXLogo.png
Binary files differ
diff --git a/multiple/assets/logo/HTHBLogo.png b/multiple/assets/logo/HTHBLogo.png
new file mode 100644
index 0000000..2106015
--- /dev/null
+++ b/multiple/assets/logo/HTHBLogo.png
Binary files differ
diff --git a/multiple/assets/logo/JFHYLogo.png b/multiple/assets/logo/JFHYLogo.png
new file mode 100644
index 0000000..55d4131
--- /dev/null
+++ b/multiple/assets/logo/JFHYLogo.png
Binary files differ
diff --git a/multiple/assets/logo/KHYYLogo.png b/multiple/assets/logo/KHYYLogo.png
new file mode 100644
index 0000000..53b479d
--- /dev/null
+++ b/multiple/assets/logo/KHYYLogo.png
Binary files differ
diff --git a/multiple/assets/logo/LSYLogo.png b/multiple/assets/logo/LSYLogo.png
new file mode 100644
index 0000000..a4eeb9b
--- /dev/null
+++ b/multiple/assets/logo/LSYLogo.png
Binary files differ
diff --git a/multiple/assets/logo/NYLogo.png b/multiple/assets/logo/NYLogo.png
new file mode 100644
index 0000000..d6eea98
--- /dev/null
+++ b/multiple/assets/logo/NYLogo.png
Binary files differ
diff --git a/multiple/assets/logo/QFSWLogo.png b/multiple/assets/logo/QFSWLogo.png
new file mode 100644
index 0000000..e1db2cc
--- /dev/null
+++ b/multiple/assets/logo/QFSWLogo.png
Binary files differ
diff --git a/multiple/assets/logo/RFSYLogo.png b/multiple/assets/logo/RFSYLogo.png
new file mode 100644
index 0000000..d9eaab1
--- /dev/null
+++ b/multiple/assets/logo/RFSYLogo.png
Binary files differ
diff --git a/multiple/assets/logo/SDZZLogo.png b/multiple/assets/logo/SDZZLogo.png
new file mode 100644
index 0000000..b5fcf4f
--- /dev/null
+++ b/multiple/assets/logo/SDZZLogo.png
Binary files differ
diff --git "a/src/assets/logo/\350\212\257\345\257\274\350\275\257\344\273\266\357\274\210\346\261\237\350\213\217\357\274\211\346\234\211\351\231\220\345\205\254\345\217\270.png" b/multiple/assets/logo/XDRJ.png
similarity index 100%
copy from "src/assets/logo/\350\212\257\345\257\274\350\275\257\344\273\266\357\274\210\346\261\237\350\213\217\357\274\211\346\234\211\351\231\220\345\205\254\345\217\270.png"
copy to multiple/assets/logo/XDRJ.png
Binary files differ
diff --git a/multiple/assets/logo/XHKSLogo.png b/multiple/assets/logo/XHKSLogo.png
new file mode 100644
index 0000000..c09746f
--- /dev/null
+++ b/multiple/assets/logo/XHKSLogo.png
Binary files differ
diff --git a/multiple/assets/logo/XLGSLogo.png b/multiple/assets/logo/XLGSLogo.png
new file mode 100644
index 0000000..a787fd2
--- /dev/null
+++ b/multiple/assets/logo/XLGSLogo.png
Binary files differ
diff --git a/multiple/assets/logo/YSSLogo.png b/multiple/assets/logo/YSSLogo.png
new file mode 100644
index 0000000..260cffd
--- /dev/null
+++ b/multiple/assets/logo/YSSLogo.png
Binary files differ
diff --git a/multiple/assets/logo/ZDJCLogo.png b/multiple/assets/logo/ZDJCLogo.png
new file mode 100644
index 0000000..4fe753c
--- /dev/null
+++ b/multiple/assets/logo/ZDJCLogo.png
Binary files differ
diff --git a/src/api/collaborativeApproval/knowledgeBase.js b/src/api/collaborativeApproval/knowledgeBase.js
index b195525..eb44ac2 100644
--- a/src/api/collaborativeApproval/knowledgeBase.js
+++ b/src/api/collaborativeApproval/knowledgeBase.js
@@ -1,55 +1,172 @@
import request from "@/utils/request";
+import { getToken } from '@/utils/auth';
-// 鏌ヨ鐭ヨ瘑搴撳垪琛�
+/**
+ * 鐭ヨ瘑搴撶鐞嗘帴鍙�
+ * 浼犲弬瑙勮寖:
+ * - GET璇锋眰: 浣跨敤 params
+ * - POST璇锋眰: 浣跨敤 data
+ * - DELETE璇锋眰: 浣跨敤 data
+ * - 娴佸紡璇锋眰: 浣跨敤 Fetch API
+ */
+
+// ==================== 鐭ヨ瘑搴揅RUD鎺ュ彛 ====================
+
+/**
+ * 鏌ヨ鐭ヨ瘑搴撳垪琛�
+ * @param {Object} query - 鏌ヨ鍙傛暟
+ * @param {number} query.current - 褰撳墠椤电爜(蹇呭~)
+ * @param {number} query.size - 姣忛〉鏉℃暟(蹇呭~)
+ * @param {string} [query.title] - 鐭ヨ瘑鏍囬(鍙��,妯$硦鎼滅储)
+ * @param {string} [query.type] - 鐭ヨ瘑绫诲瀷(鍙��,绮剧‘鍖归厤)
+ * @returns {Promise}
+ */
export function listKnowledgeBase(query) {
return request({
url: "/knowledgeBase/getList",
method: "get",
- params: query,
+ params: query, // GET璇锋眰浣跨敤params
});
}
-// 鏌ヨ鐭ヨ瘑搴撹缁�
-// export function getKnowledgeBase(knowledgeBaseId) {
-// return request({
-// url: "/collaborativeApproval/knowledgeBase/" + knowledgeBaseId,
-// method: "get",
-// });
-// }
-
-// 鏂板鐭ヨ瘑搴�
+/**
+ * 鏂板鐭ヨ瘑搴�
+ * @param {Object} data - 鐭ヨ瘑搴撴暟鎹�
+ * @param {string} data.title - 鐭ヨ瘑鏍囬(蹇呭~)
+ * @param {string} data.type - 鐭ヨ瘑绫诲瀷(蹇呭~)
+ * @param {string} [data.scenario] - 閫傜敤鍦烘櫙(鍙��)
+ * @param {string} [data.efficiency] - 瑙e喅鏁堢巼(鍙��)
+ * @param {string} data.problem - 闂鎻忚堪(蹇呭~)
+ * @param {string} data.solution - 瑙e喅鏂规(蹇呭~)
+ * @param {string} [data.keyPoints] - 鍏抽敭瑕佺偣(鍙��)
+ * @param {string} [data.creator] - 鍒涘缓浜�(鍙��)
+ * @param {number} [data.usageCount=0] - 浣跨敤娆℃暟(鍙��)
+ * @returns {Promise}
+ */
export function addKnowledgeBase(data) {
return request({
url: "/knowledgeBase/add",
method: "post",
- data: data,
+ data: data, // POST璇锋眰浣跨敤data
});
}
-// 淇敼鐭ヨ瘑搴�
+/**
+ * 淇敼鐭ヨ瘑搴�
+ * @param {Object} data - 鐭ヨ瘑搴撴暟鎹�
+ * @param {number} data.id - 鐭ヨ瘑搴揑D(蹇呭~)
+ * @param {string} data.title - 鐭ヨ瘑鏍囬(蹇呭~)
+ * @param {string} data.type - 鐭ヨ瘑绫诲瀷(蹇呭~)
+ * @returns {Promise}
+ */
export function updateKnowledgeBase(data) {
return request({
url: "/knowledgeBase/update",
method: "post",
- data: data,
+ data: data, // POST璇锋眰浣跨敤data
});
}
-// 鍒犻櫎鐭ヨ瘑搴�
-export function delKnowledgeBase(query) {
+/**
+ * 鍒犻櫎鐭ヨ瘑搴�(鏀寔鎵归噺鍒犻櫎)
+ * @param {number[]} ids - 鐭ヨ瘑搴揑D鏁扮粍
+ * @returns {Promise}
+ */
+export function delKnowledgeBase(ids) {
return request({
url: "/knowledgeBase/delete",
method: "delete",
- data: query,
+ data: ids, // DELETE璇锋眰浣跨敤data浼犻�掓暟缁�
});
}
-// 鎵归噺鍒犻櫎鐭ヨ瘑搴�
-export function delKnowledgeBaseBatch(knowledgeBaseIds) {
+// ==================== 鏂囦欢绠$悊鎺ュ彛 ====================
+
+/**
+ * 鏌ヨ鐭ヨ瘑搴撴枃浠跺悜閲忓寲鐘舵��
+ * @param {number} knowledgeBaseId - 鐭ヨ瘑搴揑D
+ * @returns {Promise} 杩斿洖鏂囦欢鍒楄〃鍙婂悜閲忓寲鐘舵��
+ */
+export function getVectorStatus(knowledgeBaseId) {
return request({
- url: "/knowledgeBase/batch",
- method: "delete",
- data: knowledgeBaseIds,
+ url: `/knowledgeBase/vector/status/${knowledgeBaseId}`,
+ method: "get",
});
}
+/**
+ * 淇濆瓨鐭ヨ瘑搴撴枃浠跺叧鑱�(瑙﹀彂鍚戦噺鍖�)
+ * @param {Object} data - 鏂囦欢鍏宠仈鏁版嵁
+ * @param {number} data.knowledgeBaseId - 鐭ヨ瘑搴揑D(蹇呭~)
+ * @param {number[]} data.storageBlobIds - 鏂囦欢blob ID鏁扮粍(蹇呭~)
+ * @returns {Promise}
+ */
+export function saveKnowledgeBaseFiles(data) {
+ return request({
+ url: "/knowledgeBase/file/save",
+ method: "post",
+ data: data, // POST璇锋眰浣跨敤data
+ });
+}
+
+/**
+ * 鍒犻櫎鐭ヨ瘑搴撴枃浠�
+ * @param {number[]} ids - 鍚戦噺璁板綍ID鏁扮粍
+ * @returns {Promise}
+ */
+export function deleteKnowledgeBaseFile(ids) {
+ return request({
+ url: "/knowledgeBase/file/delete",
+ method: "delete",
+ data: ids, // DELETE璇锋眰浣跨敤data浼犻�掓暟缁�
+ });
+}
+
+/**
+ * 閲嶆柊鍚戦噺鍖栨枃浠�
+ * @param {number} vectorId - 鍚戦噺璁板綍ID
+ * @returns {Promise}
+ */
+export function reprocessVector(vectorId) {
+ return request({
+ url: `/knowledgeBase/vector/reprocess/${vectorId}`,
+ method: "post",
+ });
+}
+
+// ==================== 鐭ヨ瘑闂瓟鎺ュ彛 ====================
+
+/**
+ * 鐭ヨ瘑搴撻棶绛�(娴佸紡)
+ * 鍚庣鎺ュ彛: POST /ai/knowledge/chat
+ * 鍝嶅簲绫诲瀷: text/stream;charset=utf-8 (Spring Flux<String>)
+ *
+ * @param {Object} data - 闂瓟鍙傛暟
+ * @param {number} data.knowledgeBaseId - 鐭ヨ瘑搴揑D(蹇呭~)
+ * @param {string} data.memoryId - 浼氳瘽ID(蹇呭~,鐢ㄤ簬淇濇寔涓婁笅鏂�)
+ * @param {string} data.question - 鐢ㄦ埛闂(蹇呭~)
+ * @returns {Promise<Response>} 杩斿洖Fetch Response瀵硅薄
+ */
+export function knowledgeChat(data) {
+ const token = getToken();
+ return fetch(import.meta.env.VITE_APP_BASE_API + '/ai/knowledge/chat', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer ' + token
+ },
+ body: JSON.stringify(data)
+ });
+}
+
+/**
+ * 鏌ヨ鐭ヨ瘑搴撻棶绛斿巻鍙�
+ * @param {string} memoryId - 浼氳瘽ID
+ * @returns {Promise}
+ */
+export function getKnowledgeHistory(memoryId) {
+ return request({
+ url: `/ai/knowledge/history/${memoryId}`,
+ method: "get",
+ });
+}
diff --git a/src/api/inventoryManagement/stockOut.js b/src/api/inventoryManagement/stockOut.js
index 004ba99..b54e004 100644
--- a/src/api/inventoryManagement/stockOut.js
+++ b/src/api/inventoryManagement/stockOut.js
@@ -35,3 +35,12 @@
data,
});
}
+
+// 鎵归噺鍙嶅鍑哄簱璁板綍
+export const batchUnapproveStockOutRecords = (data) => {
+ return request({
+ url: "/stockOutRecord/reAudit",
+ method: "post",
+ data,
+ });
+}
diff --git a/src/api/mock/dataCheck.js b/src/api/mock/dataCheck.js
new file mode 100644
index 0000000..c090bc5
--- /dev/null
+++ b/src/api/mock/dataCheck.js
@@ -0,0 +1,10 @@
+import request from "@/utils/request";
+
+// 妫�娴嬫寚瀹氭ā鍧楃殑鍩虹鏁版嵁鏄惁灏辩华
+export function dataCheck(modules) {
+ return request({
+ url: "/mock/dataCheck",
+ method: "post",
+ data: { modules },
+ });
+}
diff --git a/src/api/mock/generate.js b/src/api/mock/generate.js
new file mode 100644
index 0000000..a3eb689
--- /dev/null
+++ b/src/api/mock/generate.js
@@ -0,0 +1,10 @@
+import request from "@/utils/request";
+
+// AI 鐢熸垚妯℃嫙鏁版嵁锛堝惈鍓嶇疆鍩虹鏁版嵁妫�娴嬶級
+export function mockGenerate(data) {
+ return request({
+ url: "/mock/generate",
+ method: "post",
+ data,
+ });
+}
\ No newline at end of file
diff --git a/src/api/personnelManagement/staffSalaryMain.js b/src/api/personnelManagement/staffSalaryMain.js
index a42f96b..7ff6667 100644
--- a/src/api/personnelManagement/staffSalaryMain.js
+++ b/src/api/personnelManagement/staffSalaryMain.js
@@ -17,6 +17,14 @@
});
}
+export function staffSalaryMainCalculateByEmployeeId(data) {
+ return request({
+ url: "/staffSalaryMain/calculateByEmployeeId",
+ method: "post",
+ data,
+ });
+}
+
export function staffSalaryMainAdd(data) {
return request({
url: "/staffSalaryMain/add",
@@ -41,3 +49,11 @@
});
}
+// 鑾峰彇鍙敤閮ㄩ棬鍒楄〃锛堟牴鎹湀浠芥煡璇紝鎺掗櫎宸插彂鏀惧伐璧勮〃鐨勯儴闂級
+export function staffSalaryMainAvailableDepts(salaryMonth) {
+ return request({
+ url: "/staffSalaryMain/availableDepts",
+ method: "get",
+ params: { salaryMonth },
+ });
+}
\ No newline at end of file
diff --git a/src/api/procurementManagement/paymentLedger.js b/src/api/procurementManagement/paymentLedger.js
index 6c5d9de..537ba9f 100644
--- a/src/api/procurementManagement/paymentLedger.js
+++ b/src/api/procurementManagement/paymentLedger.js
@@ -18,3 +18,12 @@
params,
});
}
+
+/** 浠樻鍙拌处 - 浠樻鐧昏鍒楄〃 */
+export function registrationList(params) {
+ return request({
+ url: "/purchase/report/registration",
+ method: "get",
+ params,
+ });
+}
diff --git a/src/api/procurementManagement/procurementLedger.js b/src/api/procurementManagement/procurementLedger.js
index 49416d2..88f4ce0 100644
--- a/src/api/procurementManagement/procurementLedger.js
+++ b/src/api/procurementManagement/procurementLedger.js
@@ -74,53 +74,61 @@
});
}
export function updateApprovalStatus(query) {
- return request({
- url: "/purchase/ledger/updateApprovalStatus",
- method: "post",
- data: query,
- });
+ return request({
+ url: "/purchase/ledger/updateApprovalStatus",
+ method: "post",
+ data: query,
+ });
}
// 淇濆瓨閲囪喘妯℃澘
export function addPurchaseTemplate(data) {
- return request({
- url: "/purchaseLedgerTemplate/add",
- method: "post",
- data: data,
- });
+ return request({
+ url: "/purchaseLedgerTemplate/add",
+ method: "post",
+ data: data,
+ });
}
// 淇敼閲囪喘妯℃澘
export function updatePurchaseTemplate(data) {
- return request({
- url: "/purchaseLedgerTemplate/update",
- method: "post",
- data: data,
- });
+ return request({
+ url: "/purchaseLedgerTemplate/update",
+ method: "post",
+ data: data,
+ });
}
// 鏌ヨ閲囪喘妯℃澘
export function getPurchaseTemplateList(query) {
- return request({
- url: "/purchase/ledger/getPurchaseTemplateList",
- method: "get",
- params: query,
- });
+ return request({
+ url: "/purchase/ledger/getPurchaseTemplateList",
+ method: "get",
+ params: query,
+ });
}
// 鍒犻櫎閲囪喘妯℃澘
export function delPurchaseTemplate(id) {
- return request({
- url: "/purchaseLedgerTemplate/delete",
- method: "delete",
- data: id,
- });
+ return request({
+ url: "/purchaseLedgerTemplate/delete",
+ method: "delete",
+ data: id,
+ });
}
// 鏌ヨ閲囪喘璇︽儏
export function getPurchaseByCode(id) {
- return request({
- url: "/purchase/ledger/getPurchaseByCode",
- method: "get",
- params: id,
- });
-}
\ No newline at end of file
+ return request({
+ url: "/purchase/ledger/getPurchaseByCode",
+ method: "get",
+ params: id,
+ });
+}
+
+export function batchGeneratePurchaseInboundSteps(query) {
+ return request({
+ url: "/purchase/ledger/batchInsertPurchaseSteps",
+ method: "post",
+ data: query,
+ });
+}
diff --git a/src/api/viewIndex.js b/src/api/viewIndex.js
index 9cae219..4d2923c 100644
--- a/src/api/viewIndex.js
+++ b/src/api/viewIndex.js
@@ -349,7 +349,6 @@
export const productionOrderProgress = (params = {}) => {
const safePageNum = Math.max(1, Number(params.pageNum || 1));
- const safePageSize = Math.min(50, Math.max(1, Number(params.pageSize || 10)));
const safeTab = ["all", "inProgress", "completed", "paused"].includes(params.tab)
? params.tab
: "all";
@@ -360,7 +359,7 @@
...params,
tab: safeTab,
pageNum: safePageNum,
- pageSize: safePageSize,
+ pageSize: params.pageSize,
},
headers: {
handleAuthError: false,
diff --git "a/src/assets/logo/\350\212\257\345\257\274\350\275\257\344\273\266\357\274\210\346\261\237\350\213\217\357\274\211\346\234\211\351\231\220\345\205\254\345\217\270.png" b/src/assets/logo/XDRJ.png
similarity index 100%
rename from "src/assets/logo/\350\212\257\345\257\274\350\275\257\344\273\266\357\274\210\346\261\237\350\213\217\357\274\211\346\234\211\351\231\220\345\205\254\345\217\270.png"
rename to src/assets/logo/XDRJ.png
Binary files differ
diff --git a/src/assets/styles/sidebar.scss b/src/assets/styles/sidebar.scss
index 685a0eb..5da0e59 100644
--- a/src/assets/styles/sidebar.scss
+++ b/src/assets/styles/sidebar.scss
@@ -216,33 +216,38 @@
& .nest-menu .el-sub-menu > .el-sub-menu__title,
& .el-sub-menu .el-menu-item {
min-width: 0 !important;
- width: calc(100% - 24px) !important;
- margin: 0 12px 8px !important;
- height: 46px;
- line-height: 46px;
- padding-left: 14px !important;
- padding-right: 14px !important;
- border-radius: 12px;
+ width: calc(100% - 32px) !important;
+ margin: 0 16px 6px !important;
+ height: 40px;
+ line-height: 40px;
+ padding-left: 12px !important;
+ padding-right: 12px !important;
+ border-radius: 8px;
transition: all 0.24s ease;
color: var(--sidebar-text);
- border: 1px solid rgba(255, 255, 255, 0.06) !important;
- background: linear-gradient(128deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.01));
+ border: none !important;
+ background: transparent;
+ font-size: 13px;
&:hover {
- background: linear-gradient(128deg, rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.24), rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.07)) !important;
- border-color: rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.3) !important;
- transform: translateX(2px);
+ background: rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.12) !important;
+ transform: translateX(4px);
}
&.is-active {
- background: var(--menu-active-bg, linear-gradient(135deg, var(--el-color-primary), var(--el-color-primary-light-3))) !important;
- background-size: 180% 180%;
+ background: rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.85) !important;
color: #fff !important;
font-weight: 500;
- box-shadow: var(--menu-active-glow, 0 10px 24px rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.34));
- animation: sidebarActiveFlow 4.6s ease infinite;
+ box-shadow: 0 4px 12px rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.25);
}
}
+
+ // 瀛愯彍鍗曞鍣ㄦ牱寮� - 澧炲姞缂╄繘鍜岃瑙夊眰娆�
+ & .el-sub-menu .el-menu {
+ padding: 4px 0 8px;
+ margin-left: 8px;
+ border-left: 2px solid rgba(255, 255, 255, 0.08);
+ }
}
.hideSidebar {
diff --git a/src/components/PIMTable/PIMTable.vue b/src/components/PIMTable/PIMTable.vue
index d63c197..74615af 100644
--- a/src/components/PIMTable/PIMTable.vue
+++ b/src/components/PIMTable/PIMTable.vue
@@ -11,7 +11,7 @@
:row-key="rowKey"
:style="tableStyle"
tooltip-effect="dark"
- :tooltip-options="{ appendTo: 'body' }"
+ :tooltip-options="{ popperOptions: { strategy: 'absolute' } }"
:expand-row-keys="expandRowKeys"
:show-summary="isShowSummary"
:summary-method="summaryMethod"
diff --git a/src/layout/components/Sidebar/SidebarItem.vue b/src/layout/components/Sidebar/SidebarItem.vue
index 003b489..3712c68 100644
--- a/src/layout/components/Sidebar/SidebarItem.vue
+++ b/src/layout/components/Sidebar/SidebarItem.vue
@@ -102,57 +102,70 @@
</script>
<style lang="scss" scoped>
-.sidebar-item-wrapper {
- :deep(.menu-icon) {
- width: 26px;
- height: 26px;
- margin-right: 12px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- transition: color 0.2s ease;
- color: var(--sidebar-text);
- opacity: 0.88;
- }
-
- :deep(.el-menu-item:hover .menu-icon),
- :deep(.el-sub-menu__title:hover .menu-icon) {
- color: #ffffff;
- opacity: 1;
- }
+.sidebar-item-wrapper {
+ :deep(.menu-icon) {
+ width: 26px;
+ height: 26px;
+ margin-right: 12px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ transition: color 0.2s ease;
+ color: var(--sidebar-text);
+ opacity: 0.88;
+ }
+
+ :deep(.el-menu-item:hover .menu-icon),
+ :deep(.el-sub-menu__title:hover .menu-icon) {
+ color: #ffffff;
+ opacity: 1;
+ }
:deep(.el-menu-item.is-active .menu-icon) {
color: var(--menu-active-text) !important;
opacity: 1;
}
- :deep(.menu-title) {
- font-weight: 500;
- transition: color 0.2s ease;
- color: var(--sidebar-text);
- opacity: 0.82;
- }
-
- :deep(.el-menu-item:hover .menu-title),
- :deep(.el-sub-menu__title:hover .menu-title) {
- color: #ffffff;
- opacity: 1;
- }
-
- :deep(.el-menu-item.is-active .menu-title) {
- color: var(--menu-active-text) !important;
- opacity: 1;
- }
-
- :deep(.nest-menu) {
- .menu-icon {
- width: 22px;
- height: 22px;
- margin-right: 10px;
- }
-
- .menu-title { font-size: 13px; }
- }
-}
+ :deep(.menu-title) {
+ font-weight: 500;
+ transition: color 0.2s ease;
+ color: var(--sidebar-text);
+ opacity: 0.82;
+ }
+
+ :deep(.el-menu-item:hover .menu-title),
+ :deep(.el-sub-menu__title:hover .menu-title) {
+ color: #ffffff;
+ opacity: 1;
+ }
+
+ :deep(.el-menu-item.is-active .menu-title) {
+ color: var(--menu-active-text) !important;
+ opacity: 1;
+ }
+
+ :deep(.nest-menu) {
+ .menu-icon {
+ width: 18px;
+ height: 18px;
+ margin-right: 8px;
+ opacity: 0.7;
+ }
+
+ .menu-title {
+ font-size: 13px;
+ opacity: 0.85;
+ }
+
+ .el-menu-item.is-active {
+ .menu-icon {
+ opacity: 1;
+ }
+ .menu-title {
+ opacity: 1;
+ }
+ }
+ }
+}
</style>
diff --git a/src/router/index.js b/src/router/index.js
index 7be3668..000a0c5 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -97,6 +97,44 @@
},
],
},
+ // 鏁欏婕旂ず妯″潡
+ {
+ path: "/productionManagement/teachingDemo",
+ component: Layout,
+ hidden: true,
+ children: [
+ {
+ path: "",
+ component: () => import("@/views/productionManagement/teachingDemo/index.vue"),
+ name: "TeachingDemo",
+ meta: { title: "宸ヨ壓璺嚎涓嶣OM鏁欏婕旂ず", icon: "education" },
+ },
+ {
+ path: "bom",
+ component: () => import("@/views/productionManagement/teachingDemo/BomDemo.vue"),
+ name: "BomDemo",
+ meta: { title: "BOM缁撴瀯婕旂ず", activeMenu: "/productionManagement/teachingDemo" },
+ },
+ {
+ path: "processRoute",
+ component: () => import("@/views/productionManagement/teachingDemo/ProcessRouteDemo.vue"),
+ name: "ProcessRouteDemo",
+ meta: { title: "宸ヨ壓璺嚎婕旂ず", activeMenu: "/productionManagement/teachingDemo" },
+ },
+ {
+ path: "combined",
+ component: () => import("@/views/productionManagement/teachingDemo/CombinedDemo.vue"),
+ name: "CombinedDemo",
+ meta: { title: "鑱斿姩婕旂ず", activeMenu: "/productionManagement/teachingDemo" },
+ },
+ {
+ path: "factory",
+ component: () => import("@/views/productionManagement/teachingDemo/FactoryDemo.vue"),
+ name: "FactoryDemo",
+ meta: { title: "鍔ㄦ�佸伐鍘傛紨绀�", activeMenu: "/productionManagement/teachingDemo" },
+ },
+ ],
+ },
{
path: "/user",
component: Layout,
diff --git a/src/views/basicData/customerFile/index.vue b/src/views/basicData/customerFile/index.vue
index 7091a02..c565a69 100644
--- a/src/views/basicData/customerFile/index.vue
+++ b/src/views/basicData/customerFile/index.vue
@@ -800,7 +800,12 @@
{
name: "缂栬緫",
type: "text",
+ showHide: row => !isCustomerEditLocked(row),
clickFun: row => {
+ if (isCustomerEditLocked(row)) {
+ proxy.$modal.msgWarning("宸茶閿�鍞ā鍧楀紩鐢ㄧ殑绉佹捣瀹㈡埛涓嶈兘缂栬緫");
+ return;
+ }
openForm("edit", row);
},
},
@@ -1001,6 +1006,10 @@
}
// 鎵撳紑寮规
const openForm = (type, row) => {
+ if (type === "edit" && isCustomerEditLocked(row)) {
+ proxy.$modal.msgWarning("宸茶閿�鍞ā鍧楀紩鐢ㄧ殑绉佹捣瀹㈡埛涓嶈兘缂栬緫");
+ return;
+ }
operationType.value = type;
form.value = {};
form.value.maintainer = userStore.nickName;
@@ -1030,6 +1039,12 @@
}
dialogFormVisible.value = true;
};
+ const isCustomerEditLocked = row => {
+ const salesReferenced = Number(row?.salesReferenceFlag || 0) === 1;
+ const customerType = Number(row?.type);
+ const usageStatus = Number(row?.usageStatus || 0);
+ return salesReferenced && (customerType === 0 || (customerType === 1 && usageStatus === 1));
+ };
// 鎻愪氦琛ㄥ崟
const submitForm = () => {
proxy.$refs["formRef"].validate(valid => {
diff --git a/src/views/basicData/customerFileOpenSea/index.vue b/src/views/basicData/customerFileOpenSea/index.vue
index bad28e1..f36ce02 100644
--- a/src/views/basicData/customerFileOpenSea/index.vue
+++ b/src/views/basicData/customerFileOpenSea/index.vue
@@ -917,8 +917,13 @@
{
name: "鍥炴敹",
type: "text",
+ disabled: row => isCustomerRecoveryLocked(row),
showHide: row => row.usageStatus == 1,
clickFun: row => {
+ if (isCustomerRecoveryLocked(row)) {
+ proxy.$modal.msgWarning("宸查鐢ㄥ苟涓旇閿�鍞ā鍧楀紩鐢ㄧ殑鍏捣瀹㈡埛涓嶈兘鍥炴敹");
+ return;
+ }
recycle(row);
},
},
@@ -933,7 +938,12 @@
{
name: "缂栬緫",
type: "text",
- clickFun: row => {
+ disabled: row => isCustomerEditLocked(row),
+ clickFun: row => {
+ if (isCustomerEditLocked(row)) {
+ proxy.$modal.msgWarning("宸查鐢ㄥ苟涓旇閿�鍞ā鍧楀紩鐢ㄧ殑鍏捣瀹㈡埛涓嶈兘缂栬緫");
+ return;
+ }
openForm("edit", row);
},
},
@@ -1121,6 +1131,10 @@
}
// 鎵撳紑寮规
const openForm = (type, row) => {
+ if (type === "edit" && isCustomerEditLocked(row)) {
+ proxy.$modal.msgWarning("宸查鐢ㄥ苟涓旇閿�鍞ā鍧楀紩鐢ㄧ殑鍏捣瀹㈡埛涓嶈兘缂栬緫");
+ return;
+ }
operationType.value = type;
form.value = {};
form.value.maintainer = userStore.nickName;
@@ -1150,6 +1164,11 @@
}
dialogFormVisible.value = true;
};
+ const isCustomerSalesReferenced = row => Number(row?.salesReferenceFlag || 0) === 1;
+ const isCustomerRecoveryLocked = row => Number(row?.usageStatus || 0) === 1 && isCustomerSalesReferenced(row);
+ const isCustomerEditLocked = row => isCustomerSalesReferenced(row) && (
+ Number(row?.type) === 0 || (Number(row?.type) === 1 && Number(row?.usageStatus || 0) === 1)
+ );
// 鎻愪氦琛ㄥ崟
const submitForm = () => {
proxy.$refs["formRef"].validate(valid => {
diff --git a/src/views/basicData/parameterMaintenance/index.vue b/src/views/basicData/parameterMaintenance/index.vue
index bd73467..38ddd4f 100644
--- a/src/views/basicData/parameterMaintenance/index.vue
+++ b/src/views/basicData/parameterMaintenance/index.vue
@@ -84,7 +84,7 @@
</el-form-item> -->
<el-form-item label="鍒涘缓鏃堕棿"
prop="createTime">
- <el-date-picker v-model="formData.createTime"
+ <el-date-picker v-model="formCreateTimeDate"
type="date"
placeholder="閫夋嫨鏃ユ湡"
value-format="YYYY-MM-DD"
@@ -215,7 +215,8 @@
</template>
<script setup>
- import { onMounted, ref, reactive } from "vue";
+ import { onMounted, ref, reactive, computed } from "vue";
+ import dayjs from "dayjs";
import {
parameterListPage,
addParameter,
@@ -369,6 +370,12 @@
},
},
],
+ });
+ const formCreateTimeDate = computed({
+ get: () => (formData.createTime ? String(formData.createTime).split(" ")[0] : ""),
+ set: (value) => {
+ formData.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : "";
+ },
});
// const productTypes = ref([]);
const isEdit = ref(false);
@@ -528,7 +535,7 @@
formData.unit = "";
formData.remark = "";
formData.isRequired = 0;
- formData.createTime = new Date().toISOString().split("T")[0];
+ formData.createTime = dayjs().format("YYYY-MM-DD HH:mm:ss");
dialogVisible.value = true;
};
@@ -547,6 +554,7 @@
formData.remark = row.remark || "";
formData.paramFormat = row.paramFormat || "";
formData.isRequired = row.isRequired || 0;
+ formData.createTime = row.createTime || "";
dialogVisible.value = true;
};
diff --git a/src/views/collaborativeApproval/knowledgeBase/index.vue b/src/views/collaborativeApproval/knowledgeBase/index.vue
index 43fee33..f50d06b 100644
--- a/src/views/collaborativeApproval/knowledgeBase/index.vue
+++ b/src/views/collaborativeApproval/knowledgeBase/index.vue
@@ -225,18 +225,167 @@
</div>
</div>
</FormDialog>
+
+ <!-- 鏂囦欢绠$悊寮圭獥 -->
+ <FormDialog
+ v-model="filesDialogVisible"
+ title="鏂囦欢绠$悊"
+ :width="'900px'"
+ @close="closeFilesDialog"
+ @confirm="closeFilesDialog"
+ @cancel="closeFilesDialog"
+ >
+ <div class="file-manager">
+ <!-- 鏂囦欢涓婁紶 -->
+ <div class="upload-section">
+ <el-upload
+ :action="uploadUrl"
+ :headers="uploadHeaders"
+ :on-success="handleUploadSuccess"
+ :on-error="handleUploadError"
+ :before-upload="beforeUpload"
+ name="files"
+ multiple
+ :show-file-list="false"
+ accept=".txt,.md,.docx,.xlsx,.xls,.pdf"
+ >
+ <el-button type="primary">涓婁紶鏂囦欢</el-button>
+ </el-upload>
+ <el-button
+ type="success"
+ @click="saveFiles"
+ :disabled="uploadedBlobIds.length === 0"
+ :loading="savingFiles"
+ style="margin-left: 10px"
+ >
+ 淇濆瓨鏂囦欢鍏宠仈
+ </el-button>
+ <el-button
+ v-if="uploadedBlobIds.length > 0"
+ type="text"
+ @click="clearUploadedFiles"
+ style="margin-left: 10px"
+ >
+ 娓呯┖寰呬繚瀛樺垪琛�
+ </el-button>
+ </div>
+
+ <!-- 寰呬繚瀛樼殑鏂囦欢鍒楄〃 -->
+ <div v-if="uploadedBlobIds.length > 0" class="uploaded-list">
+ <div class="uploaded-tip">
+ <el-icon style="color: #409eff"><InfoFilled /></el-icon>
+ <span>宸蹭笂浼� {{ uploadedBlobIds.length }} 涓枃浠�,璇风偣鍑�"淇濆瓨鏂囦欢鍏宠仈"鎸夐挳瑙﹀彂鍚戦噺鍖栧鐞�</span>
+ </div>
+ </div>
+
+ <!-- 鏂囦欢鍒楄〃涓庡悜閲忓寲鐘舵�� -->
+ <el-table :data="fileList" style="margin-top: 20px" border>
+ <el-table-column prop="fileName" label="鏂囦欢鍚�" show-overflow-tooltip />
+ <el-table-column prop="fileType" label="鏂囦欢绫诲瀷" width="100" />
+ <el-table-column label="鍚戦噺鍖栫姸鎬�" width="120">
+ <template #default="{ row }">
+ <el-tag :type="getStatusType(row.vectorStatus)">
+ {{ getStatusText(row.vectorStatus) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="chunkCount" label="鍒囩墖鏁�" width="100" align="center" />
+ <el-table-column label="閿欒淇℃伅" width="200" show-overflow-tooltip>
+ <template #default="{ row }">
+ <span v-if="row.vectorError" style="color: #f56c6c">{{ row.vectorError }}</span>
+ <span v-else style="color: #909399">-</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="createTime" label="涓婁紶鏃堕棿" width="180" />
+ <el-table-column label="鎿嶄綔" width="150" align="center">
+ <template #default="{ row }">
+ <el-button
+ v-if="row.vectorStatus === 3"
+ type="text"
+ @click="reprocessFile(row)"
+ >
+ 閲嶆柊澶勭悊
+ </el-button>
+ <el-button type="text" @click="deleteFile(row)" style="color: #f56c6c">
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </FormDialog>
+
+ <!-- 鐭ヨ瘑搴撻棶绛斿脊绐� -->
+ <FormDialog
+ v-model="chatDialogVisible"
+ title="鐭ヨ瘑搴撻棶绛�"
+ :width="'800px'"
+ @close="closeChatDialog"
+ @confirm="closeChatDialog"
+ @cancel="closeChatDialog"
+ >
+ <div class="knowledge-chat">
+ <div class="chat-header">
+ <el-tag type="success">褰撳墠鐭ヨ瘑搴�: {{ currentKnowledgeBase?.title }}</el-tag>
+ </div>
+
+ <!-- 瀵硅瘽鍖哄煙 -->
+ <div class="chat-messages" ref="chatMessagesRef">
+ <div
+ v-for="(msg, index) in messages"
+ :key="index"
+ :class="['message', msg.role]"
+ >
+ <div class="message-role">{{ msg.role === 'user' ? '鎴�' : 'AI鍔╂墜' }}</div>
+ <div class="message-content">{{ msg.content }}</div>
+ </div>
+ <div v-if="chatLoading" class="message assistant">
+ <div class="message-role">AI鍔╂墜</div>
+ <div class="message-content typing">姝e湪鎬濊�冧腑...</div>
+ </div>
+ </div>
+
+ <!-- 杈撳叆妗� -->
+ <div class="chat-input">
+ <el-input
+ v-model="inputQuestion"
+ placeholder="璇疯緭鍏ラ棶棰�,鎸夊洖杞﹀彂閫�(Ctrl+Enter蹇嵎鍙戦��)"
+ @keyup.enter="sendMessage"
+ :disabled="chatLoading"
+ >
+ <template #append>
+ <el-button @click="sendMessage" :loading="chatLoading">鍙戦��</el-button>
+ </template>
+ </el-input>
+ <div class="chat-actions">
+ <el-button type="text" size="small" @click="clearMessages">娓呯┖瀵硅瘽</el-button>
+ </div>
+ </div>
+ </div>
+ </FormDialog>
</div>
</template>
<script setup>
-import { Search } from "@element-plus/icons-vue";
-import { onMounted, ref, reactive, toRefs, getCurrentInstance, computed, watch } from "vue";
+import { Search, InfoFilled } from "@element-plus/icons-vue";
+import { onMounted, ref, reactive, toRefs, getCurrentInstance, computed, watch, nextTick } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import PIMTable from "@/components/PIMTable/PIMTable.vue";
import FormDialog from '@/components/Dialog/FormDialog.vue';
-import { listKnowledgeBase, delKnowledgeBase,addKnowledgeBase,updateKnowledgeBase } from "@/api/collaborativeApproval/knowledgeBase.js";
+import {
+ listKnowledgeBase,
+ delKnowledgeBase,
+ addKnowledgeBase,
+ updateKnowledgeBase,
+ getVectorStatus,
+ reprocessVector,
+ saveKnowledgeBaseFiles,
+ deleteKnowledgeBaseFile,
+ knowledgeChat
+} from "@/api/collaborativeApproval/knowledgeBase.js";
import useUserStore from '@/store/modules/user';
import { userListNoPageByTenantId } from '@/api/system/user.js';
+import { getToken } from "@/utils/auth";
// 琛ㄥ崟楠岃瘉瑙勫垯
const rules = {
@@ -283,7 +432,18 @@
dialogTitle: "",
dialogType: "add",
viewDialogVisible: false,
- currentKnowledge: {}
+ currentKnowledge: {},
+ filesDialogVisible: false,
+ currentKnowledgeBase: null,
+ fileList: [],
+ uploadedBlobIds: [],
+ savingFiles: false,
+ vectorStatusTimer: null, // 鍚戦噺鍖栫姸鎬佽疆璇㈠畾鏃跺櫒
+ chatDialogVisible: false,
+ messages: [],
+ inputQuestion: "",
+ chatLoading: false,
+ memoryId: ""
});
const {
@@ -297,7 +457,18 @@
dialogTitle,
dialogType,
viewDialogVisible,
- currentKnowledge
+ currentKnowledge,
+ filesDialogVisible,
+ currentKnowledgeBase,
+ fileList,
+ uploadedBlobIds,
+ savingFiles,
+ vectorStatusTimer,
+ chatDialogVisible,
+ messages,
+ inputQuestion,
+ chatLoading,
+ memoryId
} = toRefs(data);
// 琛ㄥ崟寮曠敤
@@ -305,6 +476,12 @@
// 鐢ㄦ埛鐩稿叧
const userStore = useUserStore();
const userList = ref([]);
+// 鑱婂ぉ娑堟伅瀹瑰櫒寮曠敤
+const chatMessagesRef = ref();
+
+// 鏂囦欢涓婁紶鐩稿叧
+const uploadUrl = import.meta.env.VITE_APP_BASE_API + "/common/upload";
+const uploadHeaders = { Authorization: "Bearer " + getToken() };
// 琛ㄦ牸鍒楅厤缃�
const tableColumn = ref([
@@ -352,6 +529,18 @@
}
},
{
+ label: "鏂囦欢鏁伴噺",
+ prop: "fileCount",
+ width: 100,
+ align: "center"
+ },
+ {
+ label: "鍒囩墖鏁伴噺",
+ prop: "totalChunkCount",
+ width: 100,
+ align: "center"
+ },
+ {
label: "浣跨敤娆℃暟",
prop: "usageCount",
width: 100,
@@ -379,6 +568,20 @@
type: "text",
clickFun: (row) => {
openForm("edit", row);
+ }
+ },
+ {
+ name: "鏂囦欢",
+ type: "text",
+ clickFun: (row) => {
+ openFilesDialog(row);
+ }
+ },
+ {
+ name: "闂瓟",
+ type: "text",
+ clickFun: (row) => {
+ openChatDialog(row);
}
},
{
@@ -422,21 +625,31 @@
const getList = () => {
tableLoading.value = true;
- listKnowledgeBase({...page.value, ...searchForm.value})
+
+ // 鉁� GET璇锋眰浣跨敤params浼犲弬
+ listKnowledgeBase({
+ current: page.value.current,
+ size: page.value.size,
+ title: searchForm.value.title,
+ type: searchForm.value.type
+ })
.then(res => {
tableLoading.value = false;
page.value.total = res.data.total;
- // 濡傛灉褰撳墠椤垫暟瓒呰繃鎬婚〉鏁帮紝閲嶇疆鍒扮1椤靛苟閲嶆柊鏌ヨ
+
+ // 濡傛灉褰撳墠椤垫暟瓒呰繃鎬婚〉鏁�,閲嶇疆鍒扮1椤靛苟閲嶆柊鏌ヨ
const maxPage = Math.ceil(res.data.total / page.value.size) || 1;
if (page.value.current > maxPage && maxPage > 0) {
page.value.current = 1;
- // 閲嶆柊鏌ヨ绗�1椤垫暟鎹�
return getList();
}
+
tableData.value = res.data.records;
- }).catch(err => {
- tableLoading.value = false;
})
+ .catch(err => {
+ tableLoading.value = false;
+ console.error("鏌ヨ鐭ヨ瘑搴撳垪琛ㄥけ璐�:", err);
+ });
};
// 鍒嗛〉澶勭悊
@@ -611,27 +824,47 @@
const submitForm = async () => {
try {
await formRef.value.validate();
+
+ // 鉁� POST璇锋眰浣跨敤data浼犲弬,鏄庣‘鍙傛暟缁撴瀯
+ const formData = {
+ title: form.value.title,
+ type: form.value.type,
+ scenario: form.value.scenario || "",
+ efficiency: form.value.efficiency || "",
+ problem: form.value.problem,
+ solution: form.value.solution,
+ keyPoints: form.value.keyPoints || "",
+ creator: form.value.creator || "",
+ usageCount: form.value.usageCount || 0
+ };
+
if (dialogType.value === "add") {
// 鏂板鐭ヨ瘑
- addKnowledgeBase({...form.value}).then(res => {
+ addKnowledgeBase(formData).then(res => {
if(res.code == 200){
ElMessage.success("娣诲姞鎴愬姛");
closeKnowledgeDialog();
getList();
}
}).catch(err => {
- ElMessage.error(err.msg);
- })
+ console.error("娣诲姞鐭ヨ瘑搴撳け璐�:", err);
+ ElMessage.error(err.msg || "娣诲姞澶辫触");
+ });
} else {
- updateKnowledgeBase({...form.value}).then(res => {
+ // 鏇存柊鐭ヨ瘑 - 娣诲姞id鍙傛暟
+ updateKnowledgeBase({
+ id: form.value.id,
+ ...formData
+ }).then(res => {
if(res.code == 200){
ElMessage.success("鏇存柊鎴愬姛");
closeKnowledgeDialog();
getList();
}
}).catch(err => {
- ElMessage.error(err.msg);
- })
+ console.error("鏇存柊鐭ヨ瘑搴撳け璐�:", err);
+ ElMessage.error(err.msg || "鏇存柊澶辫触");
+ });
}
} catch (error) {
console.error("琛ㄥ崟楠岃瘉澶辫触:", error);
@@ -645,19 +878,22 @@
return;
}
- ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎", {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄�,鏄惁纭鍒犻櫎?", "鍒犻櫎", {
confirmButtonText: "纭",
cancelButtonText: "鍙栨秷",
type: "warning",
}).then(() => {
- // console.log(selectedIds.value);
+ // 鉁� DELETE璇锋眰浣跨敤data浼犻�扞D鏁扮粍
delKnowledgeBase(selectedIds.value).then(res => {
if(res.code == 200){
ElMessage.success("鍒犻櫎鎴愬姛");
selectedIds.value = [];
getList();
}
- })
+ }).catch(err => {
+ console.error("鍒犻櫎鐭ヨ瘑搴撳け璐�:", err);
+ ElMessage.error(err.msg || "鍒犻櫎澶辫触");
+ });
}).catch(() => {
// 鐢ㄦ埛鍙栨秷
});
@@ -680,6 +916,364 @@
const handleExport = () => {
proxy.download('/knowledgeBase/export', { ...searchForm.value }, '鐭ヨ瘑搴�.xlsx')
}
+
+// ============ 鏂囦欢绠$悊鐩稿叧 ============
+
+// 鎵撳紑鏂囦欢绠$悊寮圭獥
+const openFilesDialog = (row) => {
+ currentKnowledgeBase.value = row;
+ filesDialogVisible.value = true;
+ loadFileList();
+};
+
+// 鍔犺浇鏂囦欢鍒楄〃
+const loadFileList = async () => {
+ if (!currentKnowledgeBase.value?.id) return;
+
+ try {
+ const res = await getVectorStatus(currentKnowledgeBase.value.id);
+ fileList.value = res.data || [];
+
+ // 妫�鏌ユ槸鍚︽湁澶勭悊涓殑鏂囦欢,濡傛灉鏈夊垯鍚姩杞
+ const hasProcessing = res.data.some(item => item.vectorStatus === 1);
+ if (hasProcessing && !vectorStatusTimer.value) {
+ startVectorStatusPolling();
+ } else if (!hasProcessing && vectorStatusTimer.value) {
+ stopVectorStatusPolling();
+ }
+ } catch (error) {
+ console.error("鍔犺浇鏂囦欢鍒楄〃澶辫触:", error);
+ ElMessage.error("鍔犺浇鏂囦欢鍒楄〃澶辫触");
+ }
+};
+
+// 寮�濮嬭疆璇㈠悜閲忓寲鐘舵��
+const startVectorStatusPolling = () => {
+ vectorStatusTimer.value = setInterval(async () => {
+ try {
+ const res = await getVectorStatus(currentKnowledgeBase.value.id);
+ fileList.value = res.data || [];
+
+ // 妫�鏌ユ槸鍚﹁繕鏈夊鐞嗕腑鐨勬枃浠�
+ const hasProcessing = res.data.some(item => item.vectorStatus === 1);
+ if (!hasProcessing) {
+ stopVectorStatusPolling();
+ ElMessage.success("鎵�鏈夋枃浠跺悜閲忓寲澶勭悊瀹屾垚");
+ }
+ } catch (error) {
+ console.error("杞鍚戦噺鍖栫姸鎬佸け璐�:", error);
+ stopVectorStatusPolling();
+ }
+ }, 3000); // 姣�3绉掕疆璇竴娆�
+};
+
+// 鍋滄杞鍚戦噺鍖栫姸鎬�
+const stopVectorStatusPolling = () => {
+ if (vectorStatusTimer.value) {
+ clearInterval(vectorStatusTimer.value);
+ vectorStatusTimer.value = null;
+ }
+};
+
+// 涓婁紶鍓嶆牎楠�
+const beforeUpload = (file) => {
+ const allowedTypes = ['.txt', '.md', '.docx', '.xlsx', '.xls', '.pdf'];
+ const fileName = file.name.toLowerCase();
+ const isAllowed = allowedTypes.some(type => fileName.endsWith(type));
+
+ if (!isAllowed) {
+ ElMessage.error('鍙敮鎸� txt銆乵d銆乨ocx銆亁lsx銆亁ls銆乸df 鏍煎紡鐨勬枃浠�');
+ return false;
+ }
+
+ const isLt50M = file.size / 1024 / 1024 < 50;
+ if (!isLt50M) {
+ ElMessage.error('鏂囦欢澶у皬涓嶈兘瓒呰繃 50MB');
+ return false;
+ }
+
+ return true;
+};
+
+// 涓婁紶鎴愬姛
+const handleUploadSuccess = (response, file) => {
+ console.log("涓婁紶鍝嶅簲:", response); // 璋冭瘯鏃ュ織
+
+ if (response.code === 200) {
+ // 鉁� 鍚庣杩斿洖鐨勬槸 List<StorageBlobVO>,鎵�浠ata鏄暟缁�
+ if (Array.isArray(response.data) && response.data.length > 0) {
+ // 鍙栨暟缁勭涓�涓厓绱犵殑id
+ const blobId = response.data[0].id;
+ if (blobId) {
+ uploadedBlobIds.value.push(blobId);
+ ElMessage.success(`鏂囦欢 ${file.name} 涓婁紶鎴愬姛`);
+ } else {
+ console.error("涓婁紶鍝嶅簲涓湭鎵惧埌id:", response.data[0]);
+ ElMessage.error("涓婁紶澶辫触: 鏈幏鍙栧埌鏂囦欢ID");
+ }
+ } else {
+ console.error("涓婁紶鍝嶅簲鏍煎紡閿欒:", response);
+ ElMessage.error("涓婁紶澶辫触: 鍝嶅簲鏍煎紡閿欒");
+ }
+ } else {
+ ElMessage.error(response.msg || "涓婁紶澶辫触");
+ }
+};
+
+// 涓婁紶澶辫触
+const handleUploadError = (error, file) => {
+ ElMessage.error(`鏂囦欢 ${file.name} 涓婁紶澶辫触`);
+};
+
+// 淇濆瓨鏂囦欢鍏宠仈
+const saveFiles = async () => {
+ // 鍙傛暟鏍¢獙
+ if (!currentKnowledgeBase.value?.id) {
+ ElMessage.error("鐭ヨ瘑搴撲俊鎭紓甯�");
+ return;
+ }
+
+ if (uploadedBlobIds.value.length === 0) {
+ ElMessage.warning("璇峰厛涓婁紶鏂囦欢");
+ return;
+ }
+
+ savingFiles.value = true;
+
+ try {
+ // 鉁� POST璇锋眰浣跨敤data浼犲弬,鏄庣‘鍙傛暟缁撴瀯
+ await saveKnowledgeBaseFiles({
+ knowledgeBaseId: currentKnowledgeBase.value.id, // 鐭ヨ瘑搴揑D
+ storageBlobIds: uploadedBlobIds.value // 鏂囦欢blob ID鏁扮粍
+ });
+
+ ElMessage.success("鏂囦欢鍏宠仈淇濆瓨鎴愬姛,姝e湪鍚庡彴澶勭悊鍚戦噺鍖�");
+ uploadedBlobIds.value = [];
+
+ // 寤惰繜鍒锋柊鏂囦欢鍒楄〃,缁欏悗鍙板鐞嗘椂闂�
+ setTimeout(() => {
+ loadFileList();
+ }, 1000);
+ } catch (error) {
+ console.error("淇濆瓨鏂囦欢鍏宠仈澶辫触:", error);
+ ElMessage.error("淇濆瓨鏂囦欢鍏宠仈澶辫触");
+ } finally {
+ savingFiles.value = false;
+ }
+};
+
+// 閲嶆柊澶勭悊鍚戦噺鍖栫殑鏂囦欢
+const reprocessFile = async (row) => {
+ try {
+ await reprocessVector(row.id);
+ ElMessage.success("宸查噸鏂版彁浜ゅ悜閲忓寲浠诲姟");
+ // 寤惰繜鍒锋柊
+ setTimeout(() => {
+ loadFileList();
+ }, 1000);
+ } catch (error) {
+ console.error("閲嶆柊澶勭悊澶辫触:", error);
+ ElMessage.error("閲嶆柊澶勭悊澶辫触");
+ }
+};
+
+// 娓呯┖寰呬繚瀛樼殑鏂囦欢鍒楄〃
+const clearUploadedFiles = () => {
+ uploadedBlobIds.value = [];
+ ElMessage.success("宸叉竻绌哄緟淇濆瓨鏂囦欢鍒楄〃");
+};
+
+// 鍒犻櫎鏂囦欢
+const deleteFile = async (row) => {
+ try {
+ await ElMessageBox.confirm(
+ "纭畾瑕佸垹闄よ鏂囦欢鍚�?鍒犻櫎鍚庡皢鏃犳硶鎭㈠鍚戦噺鏁版嵁",
+ "鍒犻櫎纭",
+ {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning"
+ }
+ );
+
+ // 鉁� DELETE璇锋眰浣跨敤data浼犻�扞D鏁扮粍
+ await deleteKnowledgeBaseFile([row.id]); // 娉ㄦ剰: row.id鏄悜閲忚褰旾D,涓嶆槸storageBlobId
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ loadFileList();
+ } catch (error) {
+ if (error !== 'cancel') {
+ console.error("鍒犻櫎鏂囦欢澶辫触:", error);
+ ElMessage.error("鍒犻櫎鏂囦欢澶辫触");
+ }
+ }
+};
+
+// 鐘舵�佹枃鏈槧灏�
+const getStatusText = (status) => {
+ const map = {
+ 0: '寰呭鐞�',
+ 1: '澶勭悊涓�',
+ 2: '宸插畬鎴�',
+ 3: '澶辫触'
+ };
+ return map[status] || '鏈煡';
+};
+
+// 鐘舵�佹爣绛剧被鍨嬫槧灏�
+const getStatusType = (status) => {
+ const map = {
+ 0: 'info',
+ 1: 'warning',
+ 2: 'success',
+ 3: 'danger'
+ };
+ return map[status] || 'info';
+};
+
+// 鍏抽棴鏂囦欢绠$悊寮圭獥
+const closeFilesDialog = () => {
+ filesDialogVisible.value = false;
+ currentKnowledgeBase.value = null;
+ fileList.value = [];
+ uploadedBlobIds.value = [];
+ stopVectorStatusPolling(); // 鍋滄杞
+ getList(); // 鍒锋柊涓诲垪琛�,鏇存柊鏂囦欢鏁伴噺
+};
+
+// ============ 鐭ヨ瘑搴撻棶绛旂浉鍏� ============
+
+// 鐢熸垚UUID鐨刦allback鏂规
+const generateUUID = () => {
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
+ return crypto.randomUUID();
+ }
+ // fallback: 鍏煎涓嶆敮鎸� crypto.randomUUID 鐨勭幆澧�
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+ const r = Math.random() * 16 | 0;
+ const v = c === 'x' ? r : (r & 0x3 | 0x8);
+ return v.toString(16);
+ });
+};
+
+// 鎵撳紑闂瓟寮圭獥
+const openChatDialog = (row) => {
+ currentKnowledgeBase.value = row;
+ chatDialogVisible.value = true;
+ memoryId.value = generateUUID();
+ messages.value = [];
+ inputQuestion.value = "";
+};
+
+// 鍙戦�佹秷鎭�
+const sendMessage = async () => {
+ // 鍙傛暟鏍¢獙
+ if (!inputQuestion.value.trim()) {
+ ElMessage.warning("璇疯緭鍏ラ棶棰�");
+ return;
+ }
+
+ if (!currentKnowledgeBase.value?.id) {
+ ElMessage.error("鐭ヨ瘑搴撲俊鎭紓甯�");
+ return;
+ }
+
+ const question = inputQuestion.value.trim();
+
+ // 娣诲姞鐢ㄦ埛娑堟伅
+ messages.value.push({
+ role: 'user',
+ content: question
+ });
+
+ inputQuestion.value = "";
+ chatLoading.value = true;
+
+ // 婊氬姩鍒板簳閮�
+ await nextTick();
+ scrollToBottom();
+
+ try {
+ // 鉁� 娴佸紡璇锋眰浣跨敤Fetch API
+ const response = await knowledgeChat({
+ knowledgeBaseId: currentKnowledgeBase.value.id, // 鐭ヨ瘑搴揑D
+ memoryId: memoryId.value, // 浼氳瘽ID
+ question: question // 鐢ㄦ埛闂
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(errorText || '璇锋眰澶辫触');
+ }
+
+ // 鉁� 鍚庣杩斿洖 text/stream;charset=utf-8
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder();
+ let aiContent = '';
+
+ messages.value.push({ role: 'assistant', content: '' });
+
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+
+ const text = decoder.decode(value, { stream: true }); // 鉁� 娣诲姞stream閫夐」
+ aiContent += text;
+ messages.value[messages.value.length - 1].content = aiContent;
+
+ // 婊氬姩鍒板簳閮�
+ await nextTick();
+ scrollToBottom();
+ }
+
+ // 濡傛灉AI杩斿洖绌哄唴瀹�,鏄剧ず鎻愮ず
+ if (!aiContent.trim()) {
+ messages.value[messages.value.length - 1].content = '鎶辨瓑,鐭ヨ瘑搴撲腑鏈壘鍒扮浉鍏冲唴瀹�,璇峰皾璇曞叾浠栭棶棰樸��';
+ }
+ } catch (error) {
+ console.error("闂瓟璇锋眰澶辫触:", error);
+ ElMessage.error("闂瓟璇锋眰澶辫触,璇风◢鍚庨噸璇�");
+ messages.value.push({
+ role: 'assistant',
+ content: '鎶辨瓑,鍙戠敓浜嗛敊璇�,璇风◢鍚庨噸璇�'
+ });
+ } finally {
+ chatLoading.value = false;
+ }
+};
+
+// 娓呯┖瀵硅瘽
+const clearMessages = () => {
+ ElMessageBox.confirm(
+ "纭畾瑕佹竻绌烘墍鏈夊璇濊褰曞悧?",
+ "娓呯┖纭",
+ {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning"
+ }
+ ).then(() => {
+ messages.value = [];
+ memoryId.value = generateUUID(); // 閲嶆柊鐢熸垚浼氳瘽ID
+ ElMessage.success("瀵硅瘽宸叉竻绌�");
+ }).catch(() => {
+ // 鐢ㄦ埛鍙栨秷
+ });
+};
+
+// 婊氬姩鍒板簳閮�
+const scrollToBottom = () => {
+ if (chatMessagesRef.value) {
+ chatMessagesRef.value.scrollTop = chatMessagesRef.value.scrollHeight;
+ }
+};
+
+// 鍏抽棴闂瓟寮圭獥
+const closeChatDialog = () => {
+ chatDialogVisible.value = false;
+ currentKnowledgeBase.value = null;
+ messages.value = [];
+ inputQuestion.value = "";
+};
</script>
<style scoped>
@@ -755,4 +1349,113 @@
font-size: 14px;
color: #909399;
}
+
+/* 鏂囦欢绠$悊鏍峰紡 */
+.file-manager {
+ padding: 20px 0;
+}
+
+.upload-section {
+ display: flex;
+ align-items: center;
+}
+
+.uploaded-list {
+ margin-top: 16px;
+ padding: 12px;
+ background: #f0f9ff;
+ border-radius: 6px;
+ border: 1px solid #b3d8ff;
+}
+
+.uploaded-tip {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: #409eff;
+ font-size: 14px;
+}
+
+/* 鐭ヨ瘑搴撻棶绛旀牱寮� */
+.knowledge-chat {
+ display: flex;
+ flex-direction: column;
+ height: 500px;
+}
+
+.chat-header {
+ margin-bottom: 16px;
+}
+
+.chat-messages {
+ flex: 1;
+ overflow-y: auto;
+ padding: 16px;
+ background: #f5f7fa;
+ border-radius: 8px;
+ margin-bottom: 16px;
+}
+
+.message {
+ margin-bottom: 16px;
+ max-width: 80%;
+}
+
+.message.user {
+ margin-left: auto;
+ text-align: right;
+}
+
+.message.assistant {
+ margin-right: auto;
+}
+
+.message-role {
+ font-size: 12px;
+ color: #909399;
+ margin-bottom: 4px;
+}
+
+.message-content {
+ display: inline-block;
+ padding: 10px 14px;
+ border-radius: 8px;
+ line-height: 1.6;
+ word-wrap: break-word;
+ white-space: pre-wrap;
+}
+
+.message.user .message-content {
+ background: #409eff;
+ color: white;
+}
+
+.message.assistant .message-content {
+ background: white;
+ color: #303133;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.typing {
+ animation: typing 1.5s infinite;
+}
+
+@keyframes typing {
+ 0%, 50%, 100% {
+ opacity: 1;
+ }
+ 25%, 75% {
+ opacity: 0.5;
+ }
+}
+
+.chat-input {
+ margin-top: auto;
+}
+
+.chat-actions {
+ margin-top: 8px;
+ text-align: right;
+}
+
</style>
diff --git a/src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue b/src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue
index b2fb88d..ac7a123 100644
--- a/src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue
+++ b/src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue
@@ -1,154 +1,148 @@
<template>
- <div>
-
- <!-- 鐢宠绫诲瀷閫夋嫨 -->
- <el-card class="type-card">
- <div class="type-selector">
- <div
- v-for="type in applicationTypes"
- :key="type.value"
- class="type-item"
- :class="{ active: currentType === type.value }"
- @click="changeType(type.value)"
- >
- <div class="type-icon">
- <el-icon :size="24"><component :is="type.icon"/></el-icon>
- </div>
- <div class="type-info">
- <div class="type-name">{{ type.name }}</div>
- <div class="type-desc">{{ type.desc }}</div>
- </div>
- </div>
- </div>
- </el-card>
-
- <!-- 浼氳鐢宠琛ㄥ崟 -->
- <el-card>
- <div class="form-header">
- <h3>{{ getCurrentTypeName() }}鐢宠</h3>
- </div>
-
- <el-form
- ref="meetingFormRef"
- :model="meetingForm"
- :rules="rules"
- label-width="100px"
- >
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="浼氳涓婚" prop="title">
- <el-input v-model="meetingForm.title" placeholder="璇疯緭鍏ヤ細璁富棰�"/>
- </el-form-item>
- </el-col>
- </el-row>
-
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="浼氳瀹�" prop="roomId">
- <el-select v-model="meetingForm.roomId" placeholder="璇烽�夋嫨浼氳瀹�" style="width: 100%">
- <el-option
- v-for="room in meetingRooms"
- :key="room.id"
- :label="`${room.name} (${room.location})`"
- :value="room.id"
- />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="涓绘寔浜�" prop="host">
- <el-input v-model="meetingForm.host" placeholder="璇疯緭鍏ヤ富鎸佷汉濮撳悕"/>
- </el-form-item>
- </el-col>
- </el-row>
-
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="浼氳鏃ユ湡" prop="meetingDate">
- <el-date-picker
- v-model="meetingForm.meetingDate"
- type="date"
- placeholder="璇烽�夋嫨浼氳鏃ユ湡"
- value-format="YYYY-MM-DD"
- format="YYYY-MM-DD"
- :disabled-date="disabledDate"
- style="width: 100%"
- />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <!-- 绌哄垪锛屼繚鎸佸竷灞� -->
- </el-col>
- </el-row>
-
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="寮�濮嬫椂闂�" prop="startTime">
- <el-select
- v-model="meetingForm.startTime"
- placeholder="璇烽�夋嫨寮�濮嬫椂闂�"
- style="width: 100%"
- >
- <el-option
- v-for="time in startTimeOptions"
- :key="time.value"
- :label="time.label"
- :value="time.value"
- />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="缁撴潫鏃堕棿" prop="endTime">
- <el-select
- v-model="meetingForm.endTime"
- placeholder="璇烽�夋嫨缁撴潫鏃堕棿"
- style="width: 100%"
- >
- <el-option
- v-for="time in endTimeOptions"
- :key="time.value"
- :label="time.label"
- :value="time.value"
- />
- </el-select>
- </el-form-item>
- </el-col>
- </el-row>
-
- <el-form-item label="鍙備細浜哄憳" prop="participants">
- <el-select
- v-model="meetingForm.participants"
- multiple
- filterable
- placeholder="璇烽�夋嫨鍙備細浜哄憳"
- style="width: 100%"
- >
- <el-option
- v-for="person in employees"
- :key="person.id"
- :label="`${person.staffName}${person.postName ? ` (${person.postName})` : ''}`"
- :value="person.id"
- />
- </el-select>
- </el-form-item>
-
- <el-form-item label="浼氳璇存槑" prop="description">
- <el-input
- v-model="meetingForm.description"
- type="textarea"
- :rows="4"
- placeholder="璇疯緭鍏ヤ細璁鏄�"
- />
- </el-form-item>
- </el-form>
-
- <div class="form-footer">
- <el-button @click="resetForm">閲嶇疆</el-button>
- <el-button type="primary" @click="submitForm">鎻愪氦</el-button>
- </div>
- </el-card>
- </div>
+ <div>
+
+ <!-- 鐢宠绫诲瀷閫夋嫨 -->
+ <el-card class="type-card">
+ <div class="type-selector">
+ <div
+ v-for="type in applicationTypes"
+ :key="type.value"
+ class="type-item"
+ :class="{ active: currentType === type.value }"
+ @click="changeType(type.value)"
+ >
+ <div class="type-icon">
+ <el-icon :size="24"><component :is="type.icon"/></el-icon>
+ </div>
+ <div class="type-info">
+ <div class="type-name">{{ type.name }}</div>
+ <div class="type-desc">{{ type.desc }}</div>
+ </div>
+ </div>
+ </div>
+ </el-card>
+
+ <!-- 浼氳鐢宠琛ㄥ崟 -->
+ <el-card>
+ <div class="form-header">
+ <h3>{{ getCurrentTypeName() }}鐢宠</h3>
+ </div>
+
+ <el-form
+ ref="meetingFormRef"
+ :model="meetingForm"
+ :rules="rules"
+ label-width="100px"
+ >
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="浼氳涓婚" prop="title">
+ <el-input v-model="meetingForm.title" placeholder="璇疯緭鍏ヤ細璁富棰�"/>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="浼氳瀹�" prop="roomId">
+ <el-select v-model="meetingForm.roomId" placeholder="璇烽�夋嫨浼氳瀹�" style="width: 100%">
+ <el-option
+ v-for="room in meetingRooms"
+ :key="room.id"
+ :label="`${room.name} (${room.location})`"
+ :value="room.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="涓绘寔浜�" prop="host">
+ <el-input v-model="meetingForm.host" placeholder="璇疯緭鍏ヤ富鎸佷汉濮撳悕"/>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="浼氳鏃ユ湡" prop="meetingDate">
+ <el-date-picker
+ v-model="meetingForm.meetingDate"
+ type="date"
+ placeholder="璇烽�夋嫨浼氳鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ :disabled-date="disabledDate"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <!-- 绌哄垪锛屼繚鎸佸竷灞� -->
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="寮�濮嬫椂闂�" prop="startTime">
+ <el-time-picker
+ v-model="meetingForm.startTime"
+ placeholder="璇烽�夋嫨寮�濮嬫椂闂�"
+ format="HH:mm"
+ value-format="HH:mm"
+ :disabled-hours="disabledStartHours"
+ :disabled-minutes="disabledStartMinutes"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="缁撴潫鏃堕棿" prop="endTime">
+ <el-time-picker
+ v-model="meetingForm.endTime"
+ placeholder="璇烽�夋嫨缁撴潫鏃堕棿"
+ format="HH:mm"
+ value-format="HH:mm"
+ :disabled-hours="disabledEndHours"
+ :disabled-minutes="disabledEndMinutes"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-form-item label="鍙備細浜哄憳" prop="participants">
+ <el-select
+ v-model="meetingForm.participants"
+ multiple
+ filterable
+ placeholder="璇烽�夋嫨鍙備細浜哄憳"
+ style="width: 100%"
+ >
+ <el-option
+ v-for="person in employees"
+ :key="person.id"
+ :label="`${person.staffName}${person.postName ? ` (${person.postName})` : ''}`"
+ :value="person.id"
+ />
+ </el-select>
+ </el-form-item>
+
+ <el-form-item label="浼氳璇存槑" prop="description">
+ <el-input
+ v-model="meetingForm.description"
+ type="textarea"
+ :rows="4"
+ placeholder="璇疯緭鍏ヤ細璁鏄�"
+ />
+ </el-form-item>
+ </el-form>
+
+ <div class="form-footer">
+ <el-button @click="resetForm">閲嶇疆</el-button>
+ <el-button type="primary" @click="submitForm">鎻愪氦</el-button>
+ </div>
+ </el-card>
+ </div>
</template>
<script setup>
@@ -163,37 +157,37 @@
// 鐢宠绫诲瀷閫夐」
const applicationTypes = ref([
- {
- value: 'approval',
- name: '瀹℃壒娴佺▼浼氳',
- desc: '闇�瑕佺粡杩囧绾у鎵圭殑浼氳鐢宠',
- icon: Document
- },
- {
- value: 'department',
- name: '閮ㄩ棬绾т細璁�',
- desc: '閮ㄩ棬鍐呴儴浼氳鐢宠娴佺▼',
- icon: Promotion
- },
- {
- value: 'notification',
- name: '浼氳閫氱煡',
- desc: '鏃犻渶瀹℃壒鐩存帴鍙戝竷鐨勪細璁�氱煡',
- icon: Bell
- }
+ {
+ value: 'approval',
+ name: '瀹℃壒娴佺▼浼氳',
+ desc: '闇�瑕佺粡杩囧绾у鎵圭殑浼氳鐢宠',
+ icon: Document
+ },
+ {
+ value: 'department',
+ name: '閮ㄩ棬绾т細璁�',
+ desc: '閮ㄩ棬鍐呴儴浼氳鐢宠娴佺▼',
+ icon: Promotion
+ },
+ {
+ value: 'notification',
+ name: '浼氳閫氱煡',
+ desc: '鏃犻渶瀹℃壒鐩存帴鍙戝竷鐨勪細璁�氱煡',
+ icon: Bell
+ }
])
// 琛ㄥ崟鏁版嵁
const meetingForm = reactive({
- title: '',
- type: '',
- roomId: '',
- host: '',
- meetingDate: '',
- startTime: '',
- endTime: '',
- participants: [],
- description: ''
+ title: '',
+ type: '',
+ roomId: '',
+ host: '',
+ meetingDate: '',
+ startTime: '',
+ endTime: '',
+ participants: [],
+ description: ''
})
// 琛ㄥ崟寮曠敤
@@ -205,307 +199,270 @@
// 鍛樺伐鍒楄〃
const employees = ref([])
-// 鏃堕棿閫夐」锛堜互鍗婂皬鏃朵负闂撮殧锛�
-const timeOptions = ref([])
-
const getTimeInMinutes = (time) => {
- if (!time) return -1
- const [hour, minute] = time.split(':').map(Number)
- return hour * 60 + minute
+ if (!time) return -1
+ const [hour, minute] = time.split(':').map(Number)
+ return hour * 60 + minute
}
const isToday = (dateText) => {
- if (!dateText) return false
- const [year, month, day] = dateText.split('-').map(Number)
- const now = new Date()
- return year === now.getFullYear() && month === now.getMonth() + 1 && day === now.getDate()
+ if (!dateText) return false
+ const [year, month, day] = dateText.split('-').map(Number)
+ const now = new Date()
+ return year === now.getFullYear() && month === now.getMonth() + 1 && day === now.getDate()
}
const validateStartTime = (_rule, value, callback) => {
- if (!value) {
- callback()
- return
- }
-
- if (isToday(meetingForm.meetingDate)) {
- const now = new Date()
- const currentMinutes = now.getHours() * 60 + now.getMinutes()
- if (getTimeInMinutes(value) > currentMinutes) {
- callback(new Error('褰撳ぉ寮�濮嬫椂闂翠笉鑳芥櫄浜庡綋鍓嶆椂闂�'))
- return
- }
- }
-
- callback()
+ if (!value) {
+ callback()
+ return
+ }
+ callback()
}
const validateEndTime = (_rule, value, callback) => {
- if (!value || !meetingForm.startTime) {
- callback()
- return
- }
-
- if (getTimeInMinutes(value) <= getTimeInMinutes(meetingForm.startTime)) {
- callback(new Error('缁撴潫鏃堕棿蹇呴』澶т簬寮�濮嬫椂闂�'))
- return
- }
-
- callback()
+ if (!value || !meetingForm.startTime) {
+ callback()
+ return
+ }
+ if (getTimeInMinutes(value) <= getTimeInMinutes(meetingForm.startTime)) {
+ callback(new Error('缁撴潫鏃堕棿蹇呴』澶т簬寮�濮嬫椂闂�'))
+ return
+ }
+ callback()
}
-// 琛ㄥ崟鏍¢獙瑙勫垯
const rules = {
- title: [{required: true, message: '璇疯緭鍏ヤ細璁富棰�', trigger: 'blur'}],
- roomId: [{required: true, message: '璇烽�夋嫨浼氳瀹�', trigger: 'change'}],
- host: [{required: true, message: '璇疯緭鍏ヤ富鎸佷汉', trigger: 'blur'}],
- meetingDate: [{required: true, message: '璇烽�夋嫨浼氳鏃ユ湡', trigger: 'change'}],
- startTime: [
- {required: true, message: '璇烽�夋嫨寮�濮嬫椂闂�', trigger: 'change'},
- {validator: validateStartTime, trigger: 'change'}
- ],
- endTime: [
- {required: true, message: '璇烽�夋嫨缁撴潫鏃堕棿', trigger: 'change'},
- {validator: validateEndTime, trigger: 'change'}
- ],
- participants: [{required: true, message: '璇烽�夋嫨鍙備細浜哄憳', trigger: 'change'}]
+ title: [{required: true, message: '璇疯緭鍏ヤ細璁富棰�', trigger: 'blur'}],
+ roomId: [{required: true, message: '璇烽�夋嫨浼氳瀹�', trigger: 'change'}],
+ host: [{required: true, message: '璇疯緭鍏ヤ富鎸佷汉', trigger: 'blur'}],
+ meetingDate: [{required: true, message: '璇烽�夋嫨浼氳鏃ユ湡', trigger: 'change'}],
+ startTime: [
+ {required: true, message: '璇烽�夋嫨寮�濮嬫椂闂�', trigger: 'change'},
+ {validator: validateStartTime, trigger: 'change'}
+ ],
+ endTime: [
+ {required: true, message: '璇烽�夋嫨缁撴潫鏃堕棿', trigger: 'change'},
+ {validator: validateEndTime, trigger: 'change'}
+ ],
+ participants: [{required: true, message: '璇烽�夋嫨鍙備細浜哄憳', trigger: 'change'}]
}
-const startTimeOptions = computed(() => {
- if (!isToday(meetingForm.meetingDate)) {
- return timeOptions.value
- }
- const now = new Date()
- const currentMinutes = now.getHours() * 60 + now.getMinutes()
- return timeOptions.value.filter(item => getTimeInMinutes(item.value) <= currentMinutes)
-})
-
-const endTimeOptions = computed(() => {
- if (!meetingForm.startTime) {
- return timeOptions.value
- }
- const startMinutes = getTimeInMinutes(meetingForm.startTime)
- return timeOptions.value.filter(item => getTimeInMinutes(item.value) > startMinutes)
-})
-
-// 鍒濆鍖栨椂闂撮�夐」
-const initTimeOptions = () => {
- const options = []
- const now = new Date()
- const currentHour = now.getHours()
- const currentMinute = now.getMinutes()
- // meetingDate 鏄� "yyyy-MM-dd"
- const meetingDate = new Date(meetingForm.meetingDate)
-
- const isSameDay =
- now.getFullYear() === meetingDate.getFullYear() &&
- now.getMonth() === meetingDate.getMonth() &&
- now.getDate() === meetingDate.getDate()
-
- console.log('鏄惁鍚屼竴澶�:', isSameDay)
- for (let hour = 8; hour <= 18; hour++) {
- // 寮�濮嬫椂闂村繀椤绘櫄浜庡綋鍓嶆椂闂�
- if (hour < currentHour && isSameDay) {
- continue
- }
- if (hour === currentHour && currentMinute > 30 && isSameDay) {
- continue
- }
- // 姣忎釜灏忔椂娣诲姞涓や釜閫夐」锛氭暣鐐瑰拰鍗婄偣
- options.push({
- value: `${hour.toString().padStart(2, '0')}:00`,
- label: `${hour.toString().padStart(2, '0')}:00`
- })
-
- if (hour < 18) { // 18:00涔嬪悗娌℃湁鍗婄偣閫夐」
- options.push({
- value: `${hour.toString().padStart(2, '0')}:30`,
- label: `${hour.toString().padStart(2, '0')}:30`
- })
- }
- }
- timeOptions.value = options
+// 鏃堕棿閫夋嫨鍣ㄧ鐢ㄩ�昏緫
+const disabledStartHours = () => {
+ const hours = []
+ for (let h = 0; h < 24; h++) {
+ if (h < 8 || h > 18) hours.push(h)
+ }
+ if (isToday(meetingForm.meetingDate)) {
+ const now = new Date()
+ for (let h = 8; h < now.getHours(); h++) {
+ if (!hours.includes(h)) hours.push(h)
+ }
+ }
+ return hours
}
-watch(() => meetingForm.meetingDate, () => {
- if (meetingForm.startTime && !startTimeOptions.value.some(item => item.value === meetingForm.startTime)) {
- meetingForm.startTime = ''
- }
- if (meetingForm.endTime && !endTimeOptions.value.some(item => item.value === meetingForm.endTime)) {
- meetingForm.endTime = ''
- }
- if (meetingForm.startTime) {
- meetingFormRef.value?.validateField('startTime')
- }
- if (meetingForm.endTime) {
- meetingFormRef.value?.validateField('endTime')
- }
- initTimeOptions()
-})
+const disabledStartMinutes = (hour) => {
+ const minutes = []
+ for (let m = 0; m < 60; m++) {
+ if (m !== 0 && m !== 30) minutes.push(m)
+ }
+ if (isToday(meetingForm.meetingDate)) {
+ const now = new Date()
+ if (hour === now.getHours()) {
+ if (now.getMinutes() >= 30) {
+ minutes.push(0)
+ }
+ }
+ }
+ return minutes
+}
+
+const disabledEndHours = () => {
+ const hours = []
+ for (let h = 0; h < 24; h++) {
+ if (h < 8 || h > 18) hours.push(h)
+ }
+ if (meetingForm.startTime) {
+ const startHour = parseInt(meetingForm.startTime.split(':')[0])
+ for (let h = 8; h < startHour; h++) {
+ if (!hours.includes(h)) hours.push(h)
+ }
+ }
+ return hours
+}
+
+const disabledEndMinutes = (hour) => {
+ const minutes = []
+ for (let m = 0; m < 60; m++) {
+ if (m !== 0 && m !== 30) minutes.push(m)
+ }
+ if (meetingForm.startTime) {
+ const startHour = parseInt(meetingForm.startTime.split(':')[0])
+ const startMinute = parseInt(meetingForm.startTime.split(':')[1])
+ if (hour === startHour) {
+ if (startMinute >= 0) minutes.push(0)
+ if (startMinute >= 30) minutes.push(30)
+ // only keep minutes > startMinute
+ for (let m = 0; m <= startMinute; m++) {
+ if (!minutes.includes(m)) minutes.push(m)
+ }
+ }
+ }
+ return minutes
+}
watch(() => meetingForm.startTime, () => {
- if (meetingForm.endTime && getTimeInMinutes(meetingForm.endTime) <= getTimeInMinutes(meetingForm.startTime)) {
- meetingForm.endTime = ''
- }
- if (meetingForm.endTime) {
- meetingFormRef.value?.validateField('endTime')
- }
-
+ if (meetingForm.endTime && getTimeInMinutes(meetingForm.endTime) <= getTimeInMinutes(meetingForm.startTime)) {
+ meetingForm.endTime = ''
+ }
+ if (meetingForm.endTime) {
+ meetingFormRef.value?.validateField('endTime')
+ }
+
})
// 绂佺敤鏃ユ湡锛堢鐢ㄤ粖澶╀箣鍓嶇殑鏃ユ湡锛�
const disabledDate = (time) => {
- // 绂佺敤浠婂ぉ涔嬪墠鐨勬棩鏈�
- return time.getTime() < Date.now() - 86400000
+ // 绂佺敤浠婂ぉ涔嬪墠鐨勬棩鏈�
+ return time.getTime() < Date.now() - 86400000
}
// 鍒囨崲鐢宠绫诲瀷
const changeType = (type) => {
- currentType.value = type
+ currentType.value = type
}
// 鑾峰彇褰撳墠绫诲瀷鍚嶇О
const getCurrentTypeName = () => {
- const type = applicationTypes.value.find(t => t.value === currentType.value)
- return type ? type.name : ''
+ const type = applicationTypes.value.find(t => t.value === currentType.value)
+ return type ? type.name : ''
}
// 閲嶇疆琛ㄥ崟
const resetForm = () => {
- meetingFormRef.value?.resetFields()
+ meetingFormRef.value?.resetFields()
}
// 鎻愪氦琛ㄥ崟
const submitForm = () => {
- meetingFormRef.value?.validate((valid) => {
- if (valid) {
-
- let formData = {...meetingForm}
- formData.applicationType = currentType.value
- formData.startTime = `${meetingForm.meetingDate} ${meetingForm.startTime}:00`
- formData.endTime = `${meetingForm.meetingDate} ${meetingForm.endTime}:00`
- formData.participants = JSON.stringify(formData.participants)
- console.log(formData)
- saveMeetingApplication(formData).then(() => {
-
- // 妯℃嫙鎻愪氦鎿嶄綔
- ElMessage.success(`${getCurrentTypeName()}鎻愪氦鎴愬姛`)
-
- // 鏍规嵁涓嶅悓绫诲瀷鎵ц涓嶅悓鎿嶄綔
- switch (currentType.value) {
- case 'approval':
- ElMessage.info('浼氳宸叉彁浜ゅ鎵规祦绋�')
- break
- case 'department':
- ElMessage.info('閮ㄩ棬绾т細璁敵璇峰凡鎻愪氦')
- break
- case 'notification':
- ElMessage.info('浼氳閫氱煡宸插彂甯�')
- break
- }
- resetForm()
- })
-
- }
- })
+ meetingFormRef.value?.validate((valid) => {
+ if (valid) {
+
+ let formData = {...meetingForm}
+ formData.applicationType = currentType.value
+ formData.startTime = `${meetingForm.meetingDate} ${meetingForm.startTime}:00`
+ formData.endTime = `${meetingForm.meetingDate} ${meetingForm.endTime}:00`
+ formData.participants = JSON.stringify(formData.participants)
+ console.log(formData)
+ saveMeetingApplication(formData).then(() => {
+
+ // 妯℃嫙鎻愪氦鎿嶄綔
+ ElMessage.success(`${getCurrentTypeName()}鎻愪氦鎴愬姛`)
+ resetForm()
+ })
+
+ }
+ })
}
// 椤甸潰鍔犺浇鏃跺垵濮嬪寲
onMounted(() => {
- initTimeOptions()
- getRoomEnum().then(res => {
- meetingRooms.value = res.data
- })
- staffOnJobListPage({
- current: -1,
- size: -1,
- staffState: 1
- }).then(res => {
- employees.value = res.data.records.sort((a, b) => (a.postName || '').localeCompare(b.postName || ''))
- })
+ getRoomEnum().then(res => {
+ meetingRooms.value = res.data
+ })
+ staffOnJobListPage({
+ current: -1,
+ size: -1,
+ staffState: 1
+ }).then(res => {
+ employees.value = res.data.records.sort((a, b) => (a.postName || '').localeCompare(b.postName || ''))
+ })
})
</script>
<style scoped>
.app-container {
- padding: 20px;
+ padding: 20px;
}
.page-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 20px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
}
.page-header h2 {
- margin: 0;
- color: #303133;
+ margin: 0;
+ color: #303133;
}
.type-card {
- margin-bottom: 20px;
+ margin-bottom: 20px;
}
.type-selector {
- display: flex;
- gap: 20px;
+ display: flex;
+ gap: 20px;
}
.type-item {
- flex: 1;
- display: flex;
- align-items: center;
- padding: 20px;
- border: 1px solid #ebeef5;
- border-radius: 8px;
- cursor: pointer;
- transition: all 0.3s;
+ flex: 1;
+ display: flex;
+ align-items: center;
+ padding: 20px;
+ border: 1px solid #ebeef5;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: all 0.3s;
}
.type-item:hover {
- border-color: #409eff;
- box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+ border-color: #409eff;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.type-item.active {
- border-color: #409eff;
- background-color: #ecf5ff;
+ border-color: #409eff;
+ background-color: #ecf5ff;
}
.type-icon {
- margin-right: 15px;
- color: #409eff;
+ margin-right: 15px;
+ color: #409eff;
}
.type-name {
- font-size: 16px;
- font-weight: 500;
- color: #303133;
- margin-bottom: 5px;
+ font-size: 16px;
+ font-weight: 500;
+ color: #303133;
+ margin-bottom: 5px;
}
.type-desc {
- font-size: 14px;
- color: #909399;
+ font-size: 14px;
+ color: #909399;
}
.form-header {
- margin-bottom: 20px;
- padding-bottom: 15px;
- border-bottom: 1px solid #ebeef5;
+ margin-bottom: 20px;
+ padding-bottom: 15px;
+ border-bottom: 1px solid #ebeef5;
}
.form-header h3 {
- margin: 0;
- color: #303133;
+ margin: 0;
+ color: #303133;
}
.form-footer {
- display: flex;
- justify-content: flex-end;
- gap: 10px;
- margin-top: 30px;
- padding-top: 20px;
- border-top: 1px solid #ebeef5;
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+ margin-top: 30px;
+ padding-top: 20px;
+ border-top: 1px solid #ebeef5;
}
</style>
diff --git a/src/views/collaborativeApproval/notificationManagement/meetExamine/index.vue b/src/views/collaborativeApproval/notificationManagement/meetExamine/index.vue
index 26e2c24..dea9495 100644
--- a/src/views/collaborativeApproval/notificationManagement/meetExamine/index.vue
+++ b/src/views/collaborativeApproval/notificationManagement/meetExamine/index.vue
@@ -36,7 +36,7 @@
<el-table-column prop="location" label="浼氳鍦扮偣" align="center" width="150"/>
<el-table-column prop="participants" label="鍙備細浜烘暟" align="center" width="100">
<template #default="scope">
- {{ scope.row.participants.length }}浜�
+ {{ scope.row.staffCount }}浜�
</template>
</el-table-column>
<el-table-column prop="status" label="瀹℃壒鐘舵��" align="center" width="120">
@@ -233,9 +233,9 @@
let resp = await getExamineList({...searchForm, ...queryParams})
approvalList.value = resp.data.records.map(it => {
let room = roomEnum.value.find(room => it.roomId === room.id)
- it.location = `${room.name}(${room.location})`
+ it.location = room ? `${room.name}(${room.location})` : ''
let staffs = JSON.parse(it.participants)
- it.staffCount = staffs.size
+ it.staffCount = staffs.length
it.meetingTime = `${it.meetingDate} ${dayjs(it.startTime).format('HH:mm:ss')} ~ ${dayjs(it.endTime).format('HH:mm:ss')}`
it.participants = staffList.value.filter(staff => staffs.some(id=>id === staff.id)).map(staff => {
return {
diff --git a/src/views/collaborativeApproval/notificationManagement/meetIndex/index.vue b/src/views/collaborativeApproval/notificationManagement/meetIndex/index.vue
index 9b5325f..235faa3 100644
--- a/src/views/collaborativeApproval/notificationManagement/meetIndex/index.vue
+++ b/src/views/collaborativeApproval/notificationManagement/meetIndex/index.vue
@@ -145,10 +145,11 @@
if (endIdx === -1) {
endIdx = timeSlots.value.length
}
- console.log('endIdx111', endIdx)
- if (startIdx !== -1 && endIdx !== -1) {
+ // 寰�鍚庡欢浼镐竴鏍硷紝璁╀細璁牸瀛愯鐩栧埌缁撴潫鏃堕棿鍒椾笂
+ const displayEndIdx = Math.min(endIdx + 1, timeSlots.value.length)
+ if (startIdx !== -1) {
// 鏍囪琚崰鐢ㄧ殑鏃堕棿娈�
- for (let i = startIdx; i < endIdx; i++) {
+ for (let i = startIdx; i < displayEndIdx; i++) {
occupiedSlots.add(timeSlots.value[i].value)
}
@@ -156,7 +157,7 @@
cells.push({
type: 'meeting',
meeting: meeting,
- span: endIdx - startIdx,
+ span: displayEndIdx - startIdx,
startTime: meeting.startTime,
endTime: meeting.endTime
})
diff --git a/src/views/collaborativeApproval/notificationManagement/meetPublish/index.vue b/src/views/collaborativeApproval/notificationManagement/meetPublish/index.vue
index dace531..a3ae223 100644
--- a/src/views/collaborativeApproval/notificationManagement/meetPublish/index.vue
+++ b/src/views/collaborativeApproval/notificationManagement/meetPublish/index.vue
@@ -34,7 +34,7 @@
<el-table-column prop="location" label="浼氳鍦扮偣" align="center" width="150"/>
<el-table-column prop="participants" label="鍙備細浜烘暟" align="center" width="100">
<template #default="scope">
- {{ scope.row.participants.length }}浜�
+ {{ scope.row.staffCount }}浜�
</template>
</el-table-column>
<el-table-column prop="status" label="鍙戝竷鐘舵��" align="center" width="120">
@@ -231,9 +231,9 @@
let resp = await getMeetingPublish({...searchForm, ...queryParams})
approvalList.value = resp.data.records.map(it => {
let room = roomEnum.value.find(room => it.roomId === room.id)
- it.location = `${room.name}(${room.location})`
+ it.location = room ? `${room.name}(${room.location})` : ''
let staffs = JSON.parse(it.participants)
- it.staffCount = staffs.size
+ it.staffCount = staffs.length
it.status = it.publishStatus
it.meetingTime = `${it.meetingDate} ${dayjs(it.startTime).format('HH:mm:ss')} ~ ${dayjs(it.endTime).format('HH:mm:ss')}`
it.participants = staffList.value.filter(staff => staffs.some(id=>id === staff.id)).map(staff => {
diff --git a/src/views/collaborativeApproval/notificationManagement/summary/index.vue b/src/views/collaborativeApproval/notificationManagement/summary/index.vue
index bf6e230..9f3fc7f 100644
--- a/src/views/collaborativeApproval/notificationManagement/summary/index.vue
+++ b/src/views/collaborativeApproval/notificationManagement/summary/index.vue
@@ -28,7 +28,7 @@
<el-table-column prop="location" label="浼氳鍦扮偣" align="center" width="150" />
<el-table-column prop="participants" label="鍙備細浜烘暟" align="center" width="100">
<template #default="scope">
- {{ scope.row.participants.length }}浜�
+ {{ scope.row.staffCount }}浜�
</template>
</el-table-column>
<el-table-column label="鎿嶄綔" align="center" width="200" fixed="right">
@@ -207,9 +207,9 @@
let resp = await getMeetingPublish({ ...searchForm, ...queryParams })
meetingList.value = resp.data.records.map(it => {
let room = roomEnum.value.find(room => it.roomId === room.id)
- it.location = `${room.name}(${room.location})`
+ it.location = room ? `${room.name}(${room.location})` : ''
let staffs = JSON.parse(it.participants)
- it.staffCount = staffs.size
+ it.staffCount = staffs.length
it.meetingTime = `${it.meetingDate} ${dayjs(it.startTime).format('HH:mm:ss')} ~ ${dayjs(it.endTime).format('HH:mm:ss')}`
it.participants = staffList.value.filter(staff => staffs.some(id => id === staff.id)).map(staff => {
return {
@@ -260,7 +260,7 @@
<p><strong>涓绘寔浜猴細</strong>${row.host}</p>
<p><strong>鍙備細浜哄憳锛�</strong></p>
<ol>
- ${row.participants.map(p => `<li>${p.name}</li>`).join('')}
+ ${(row.participants || []).map(p => `<li>${p?.name || ''}</li>`).join('')}
</ol>
<p><strong>浼氳鍐呭锛�</strong></p>
<ol>
diff --git a/src/views/collaborativeApproval/sealManagement/index.vue b/src/views/collaborativeApproval/sealManagement/index.vue
index 9d68848..a06f6d5 100644
--- a/src/views/collaborativeApproval/sealManagement/index.vue
+++ b/src/views/collaborativeApproval/sealManagement/index.vue
@@ -7,7 +7,6 @@
</div>
</template>
-
<!-- 鐢ㄥ嵃鐢宠绠$悊 -->
<div class="tab-content">
<el-row :gutter="20" class="mb-20 ">
@@ -45,7 +44,7 @@
:isShowPagination="true"
@pagination="paginationChange"
/>
- </div>
+ </div>
</el-card>
<!-- 鐢ㄥ嵃鐢宠瀵硅瘽妗� -->
@@ -177,6 +176,7 @@
const sealFormRef = ref()
const userList = ref([])
const sealForm = reactive({
+ id: null,
applicationNum: '',
title: '',
sealType: '',
@@ -241,9 +241,9 @@
// 鐢ㄥ嵃鐢宠琛ㄦ牸鍒楅厤缃紙闇�鍦� getStatusText/getSealTypeText 绛変箣鍚庡畾涔夛級
const sealTableColumn = ref([
- { label: '鐢宠缂栧彿', prop: 'applicationNum',},
+ { label: '鐢宠缂栧彿', prop: 'applicationNum' },
{ label: '鐢宠鏍囬', prop: 'title', showOverflowTooltip: true },
- { label: '鐢宠浜�', prop: 'createUserName', },
+ { label: '鐢宠浜�', prop: 'createUserName' },
{ label: '鎵�灞為儴闂�', prop: 'department', width: 150 },
{
label: '鐢ㄥ嵃绫诲瀷',
@@ -261,34 +261,38 @@
formatData: (v) => getStatusText(v),
formatType: (v) => getStatusType(v)
},
+ { label: '瀹℃壒浜�', prop: 'approveUserName', width: 100 },
{
dataType: 'action',
label: '鎿嶄綔',
- width: 200,
+ width: 250,
fixed: 'right',
align: 'center',
operation: [
{
name: '瀹℃壒',
clickFun: (row) => approveSeal(row),
- showHide: (row) => row.status === 'pending'
+ showHide: (row) => row.status === 'pending' && Number(userStore.id) === row.approveUserId
},
{
name: '鎷掔粷',
clickFun: (row) => rejectSeal(row),
- showHide: (row) => row.status === 'pending'
+ showHide: (row) => row.status === 'pending' && Number(userStore.id) === row.approveUserId
},
- { name: '璇︽儏', clickFun: (row) => viewSealDetail(row) }
+ {
+ name: '閲嶆柊鐢宠',
+ clickFun: (row) => reapplySeal(row),
+ showHide: (row) => row.status === 'rejected' && Number(userStore.id) === row.createUser
+ },
+ { name: '璇︽儏', clickFun: (row) => viewSealDetail(row) }
]
}
])
// 鎼滅储鍗扮珷鐢宠
const searchSealApplications = () => {
- page.current=1
+ page.current = 1
getSealApplicationList()
-
- // ElMessage.success('鎼滅储瀹屾垚')
}
// 閲嶇疆鍗扮珷鐢宠鎼滅储
const resetSealSearch = () => {
@@ -297,37 +301,58 @@
sealSearchForm.applicationNum = ''
searchSealApplications()
}
+
+// 閲嶆柊鐢宠鐢ㄥ嵃
+const reapplySeal = (row) => {
+ // 棰勫~琛ㄥ崟鏁版嵁
+ Object.assign(sealForm, {
+ id: row.id,
+ applicationNum: row.applicationNum,
+ title: row.title,
+ sealType: row.sealType,
+ reason: row.reason,
+ approveUserId: row.approveUserId,
+ urgency: row.urgency || 'normal',
+ status: 'pending',
+ storageBlobDTOs: row.storageBlobVOList || []
+ })
+ showSealApplyDialog.value = true
+}
+
// 鎻愪氦鐢ㄥ嵃鐢宠
const submitSealApplication = async () => {
try {
await sealFormRef.value.validate()
- addSealApplication(sealForm).then(res => {
- if(res.code == 200){
- ElMessage.success('鐢宠鎻愪氦鎴愬姛')
+ const request = sealForm.id ? updateSealApplication : addSealApplication
+ request(sealForm).then(res => {
+ if (res.code == 200) {
+ ElMessage.success(sealForm.id ? '閲嶆柊鐢宠鎴愬姛' : '鐢宠鎻愪氦鎴愬姛')
closeSealApplyDialog()
getSealApplicationList()
Object.assign(sealForm, {
- applicationNum: '',
- title: '',
- sealType: '',
- reason: '',
- approveUserId: '',
- urgency: 'normal',
- status: 'pending',
- storageBlobDTOs: []
- })
+ id: null,
+ applicationNum: '',
+ title: '',
+ sealType: '',
+ reason: '',
+ approveUserId: '',
+ urgency: 'normal',
+ status: 'pending',
+ storageBlobDTOs: []
+ })
}
}).catch(err => {
console.log(err.msg)
})
-
} catch (error) {
}
}
+
// 鍏抽棴鐢ㄥ嵃鐢宠瀵硅瘽妗�
const closeSealApplyDialog = () => {
// 娓呯┖琛ㄥ崟鏁版嵁
Object.assign(sealForm, {
+ id: null,
applicationNum: '',
title: '',
sealType: '',
@@ -383,7 +408,7 @@
}).then(() => {
row.status = 'approved'
updateSealApplication(row).then(res => {
- if(res.code == 200){
+ if (res.code == 200) {
ElMessage.success('瀹℃壒閫氳繃')
getSealApplicationList()
}
@@ -399,8 +424,9 @@
inputErrorMessage: '鎷掔粷鍘熷洜涓嶈兘涓虹┖'
}).then(({ value }) => {
row.status = 'rejected'
+ row.reason = value
updateSealApplication(row).then(res => {
- if(res.code == 200){
+ if (res.code == 200) {
ElMessage.success('宸叉嫆缁濈敵璇�')
getSealApplicationList()
}
@@ -418,13 +444,13 @@
const getSealApplicationList = async () => {
tableLoading.value = true
listSealApplication(page, sealSearchForm)
- .then(res => {
- sealApplications.value = res.data.records
- page.total = res.data.total
- tableLoading.value = false
- }).catch(err => {
- tableLoading.value = false
- })
+ .then(res => {
+ sealApplications.value = res.data.records
+ page.total = res.data.total
+ tableLoading.value = false
+ }).catch(err => {
+ tableLoading.value = false
+ })
}
// 鍒嗛〉鍙樺寲澶勭悊
const paginationChange = (obj) => {
@@ -491,4 +517,4 @@
.attachment-table {
border-radius: 4px;
}
-</style>
+</style>
\ No newline at end of file
diff --git a/src/views/financialManagement/assets/fixedAssets.vue b/src/views/financialManagement/assets/fixedAssets.vue
index de0f8c8..95eb017 100644
--- a/src/views/financialManagement/assets/fixedAssets.vue
+++ b/src/views/financialManagement/assets/fixedAssets.vue
@@ -178,7 +178,7 @@
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
- <el-date-picker v-model="form.createTime" type="date" placeholder="閫夋嫨鏃ユ湡" value-format="YYYY-MM-DD" style="width: 100%;" />
+ <el-date-picker v-model="createTimeDate" type="date" placeholder="閫夋嫨鏃ユ湡" value-format="YYYY-MM-DD" style="width: 100%;" />
</el-form-item>
</el-col>
</el-row>
@@ -196,6 +196,7 @@
<script setup>
import { ref, reactive, onMounted, computed } from "vue";
+import dayjs from "dayjs";
import { ElMessage, ElMessageBox } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import {
@@ -270,6 +271,12 @@
const form = reactive({
...createDefaultForm(),
+});
+const createTimeDate = computed({
+ get: () => (form.createTime ? String(form.createTime).split(" ")[0] : ""),
+ set: (value) => {
+ form.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : "";
+ },
});
const rules = {
@@ -371,7 +378,7 @@
dialogTitle.value = "鏂板鍥哄畾璧勪骇";
Object.assign(form, createDefaultForm(), {
purchaseDate: new Date().toISOString().split('T')[0],
- createTime: new Date().toISOString().split('T')[0],
+ createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
});
dialogVisible.value = true;
};
diff --git a/src/views/financialManagement/assets/intangibleAssets.vue b/src/views/financialManagement/assets/intangibleAssets.vue
index 167ae6b..b2c13b1 100644
--- a/src/views/financialManagement/assets/intangibleAssets.vue
+++ b/src/views/financialManagement/assets/intangibleAssets.vue
@@ -72,7 +72,7 @@
</template>
<template #operation="{ row }">
<el-button type="primary" link @click="view(row)">鏌ョ湅</el-button>
- <el-button type="primary" link @click="edit(row)">缂栬緫</el-button>
+ <el-button v-if="row.status !== 'amortized'" type="primary" link @click="edit(row)">缂栬緫</el-button>
<el-button type="danger" link @click="handleDelete(row)">鍒犻櫎</el-button>
</template>
</PIMTable>
@@ -160,7 +160,7 @@
<el-select v-model="form.status" placeholder="璇烽�夋嫨鐘舵��" style="width: 100%;">
<el-option label="鍦ㄧ敤" value="in_use" />
<el-option label="闂茬疆" value="idle" />
- <el-option label="宸叉憡閿�瀹屾瘯" value="amortized" />
+ <el-option v-if="isView" label="宸叉憡閿�瀹屾瘯" value="amortized" />
</el-select>
</el-form-item>
</el-col>
@@ -168,7 +168,7 @@
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
- <el-date-picker v-model="form.createTime" type="date" placeholder="閫夋嫨鏃ユ湡" value-format="YYYY-MM-DD" style="width: 100%;" />
+ <el-date-picker v-model="createTimeDate" type="date" placeholder="閫夋嫨鏃ユ湡" value-format="YYYY-MM-DD" style="width: 100%;" />
</el-form-item>
</el-col>
</el-row>
@@ -189,6 +189,7 @@
<script setup>
import { ref, reactive, onMounted, computed } from "vue";
+import dayjs from "dayjs";
import { ElMessage, ElMessageBox } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import {
@@ -243,6 +244,7 @@
);
const createDefaultForm = () => ({
+ id: null,
assetCode: "",
assetName: "",
category: "",
@@ -262,6 +264,12 @@
const form = reactive({
...createDefaultForm(),
+});
+const createTimeDate = computed({
+ get: () => (form.createTime ? String(form.createTime).split(" ")[0] : ""),
+ set: (value) => {
+ form.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : "";
+ },
});
const rules = {
@@ -369,7 +377,7 @@
dialogTitle.value = "鏂板鏃犲舰璧勪骇";
Object.assign(form, createDefaultForm(), {
acquisitionDate: new Date().toISOString().split('T')[0],
- createTime: new Date().toISOString().split('T')[0],
+ createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
});
dialogVisible.value = true;
};
diff --git a/src/views/financialManagement/payable/input-invoice.vue b/src/views/financialManagement/payable/input-invoice.vue
index 86ebd09..ae40709 100644
--- a/src/views/financialManagement/payable/input-invoice.vue
+++ b/src/views/financialManagement/payable/input-invoice.vue
@@ -51,6 +51,8 @@
:column="columns"
:tableData="dataList"
:tableLoading="tableLoading"
+ isShowSummary
+ :summaryMethod="getSummaries"
:page="{
current: pagination.currentPage,
size: pagination.pageSize,
@@ -375,6 +377,26 @@
amount: [{ required: true, message: "璇疯緭鍏ラ噾棰�", trigger: "blur" }],
};
+const summaryProps = ["amount", "taxAmount", "totalAmount"];
+
+const getSummaries = ({ columns, data }) => {
+ const sums = [];
+ columns.forEach((col, index) => {
+ if (index === 0) {
+ sums[index] = "鍚堣";
+ } else if (summaryProps.includes(col.property)) {
+ const total = data.reduce((prev, cur) => {
+ const v = Number(cur[col.property]);
+ return prev + (isNaN(v) ? 0 : v);
+ }, 0);
+ sums[index] = Number(total.toFixed(2)).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+ } else {
+ sums[index] = "";
+ }
+ });
+ return sums;
+};
+
const formatMoney = (value) => {
if (value === undefined || value === null) return "0.00";
return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
diff --git a/src/views/financialManagement/payable/payment.vue b/src/views/financialManagement/payable/payment.vue
index 18e7941..dd386d8 100644
--- a/src/views/financialManagement/payable/payment.vue
+++ b/src/views/financialManagement/payable/payment.vue
@@ -65,6 +65,8 @@
:column="columns"
:tableData="dataList"
:tableLoading="tableLoading"
+ isShowSummary
+ :summaryMethod="getSummaries"
:page="{
current: pagination.currentPage,
size: pagination.pageSize,
@@ -149,7 +151,25 @@
dataList.value.reduce((sum, item) => sum + Number(item.amount ?? 0), 0)
);
- const formatMoney = value => {
+ const getSummaries = ({ columns, data }) => {
+ const sums = [];
+ columns.forEach((col, index) => {
+ if (index === 0) {
+ sums[index] = "鍚堣";
+ } else if (col.property === "amount") {
+ const total = data.reduce((prev, cur) => {
+ const v = Number(cur.amount);
+ return prev + (isNaN(v) ? 0 : v);
+ }, 0);
+ sums[index] = total.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+ } else {
+ sums[index] = "";
+ }
+ });
+ return sums;
+};
+
+const formatMoney = value => {
if (value === undefined || value === null) return "0.00";
return Number(value)
.toFixed(2)
diff --git a/src/views/financialManagement/payable/paymentApply.vue b/src/views/financialManagement/payable/paymentApply.vue
index b83eb58..309d6d3 100644
--- a/src/views/financialManagement/payable/paymentApply.vue
+++ b/src/views/financialManagement/payable/paymentApply.vue
@@ -52,6 +52,8 @@
:column="columns"
:tableData="dataList"
:tableLoading="tableLoading"
+ isShowSummary
+ :summaryMethod="getSummaries"
:page="{
current: pagination.currentPage,
size: pagination.pageSize,
@@ -160,7 +162,7 @@
<el-col :span="12">
<el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
<el-date-picker
- v-model="form.createTime"
+ v-model="formCreateTimeDate"
type="date"
placeholder="閫夋嫨鏃ユ湡"
value-format="YYYY-MM-DD"
@@ -260,7 +262,7 @@
<el-col :span="12">
<el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
<el-date-picker
- v-model="paymentForm.createTime"
+ v-model="paymentFormCreateTimeDate"
type="date"
placeholder="閫夋嫨鏃ユ湡"
value-format="YYYY-MM-DD"
@@ -353,6 +355,7 @@
<script setup>
import { ref, reactive, computed, onMounted, nextTick, getCurrentInstance } from "vue";
+import dayjs from "dayjs";
import { ElMessage, ElMessageBox } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import { getOptions } from "@/api/procurementManagement/procurementLedger.js";
@@ -455,6 +458,18 @@
status: 0,
createTime: "",
});
+const formCreateTimeDate = computed({
+ get: () => (form.createTime ? String(form.createTime).split(" ")[0] : ""),
+ set: (value) => {
+ form.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : "";
+ },
+});
+const paymentFormCreateTimeDate = computed({
+ get: () => (paymentForm.createTime ? String(paymentForm.createTime).split(" ")[0] : ""),
+ set: (value) => {
+ paymentForm.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : "";
+ },
+});
const rules = {
supplierId: [{ required: true, message: "璇烽�夋嫨渚涘簲鍟�", trigger: "change" }],
@@ -462,6 +477,24 @@
paymentAmount: [{ required: true, message: "璇疯緭鍏ヤ粯娆鹃噾棰�", trigger: "blur" }],
paymentMethod: [{ required: true, message: "璇烽�夋嫨浠樻鏂瑰紡", trigger: "change" }],
applyDate: [{ required: true, message: "璇烽�夋嫨鐢宠鏃ユ湡", trigger: "change" }],
+};
+
+const getSummaries = ({ columns, data }) => {
+ const sums = [];
+ columns.forEach((col, index) => {
+ if (index === 0) {
+ sums[index] = "鍚堣";
+ } else if (col.property === "amount") {
+ const total = data.reduce((prev, cur) => {
+ const v = Number(cur.amount);
+ return prev + (isNaN(v) ? 0 : v);
+ }, 0);
+ sums[index] = total.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+ } else {
+ sums[index] = "";
+ }
+ });
+ return sums;
};
const formatMoney = (value) => {
@@ -620,6 +653,7 @@
remark: form.remark || "",
status: 0,
paymentAmount: form.paymentAmount,
+ createTime: form.createTime,
};
if (forUpdate) {
payload.id = currentId.value;
@@ -730,7 +764,7 @@
stockInRecordIds: [],
inboundBatches: "",
status: 0,
- createTime: new Date().toISOString().split("T")[0],
+ createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
});
inboundBatchList.value = [];
inboundBatchOptions.value = [];
@@ -809,7 +843,7 @@
bankAccount: row.bankAccountNum ?? row.bankAccount ?? "",
bankName: row.bankAccountName ?? row.bankName ?? "",
remark: "",
- createTime: new Date().toISOString().split("T")[0],
+ createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
});
paymentDialogVisible.value = true;
nextTick(() => {
diff --git a/src/views/financialManagement/payable/purchaseIn.vue b/src/views/financialManagement/payable/purchaseIn.vue
index 532bcb4..b105859 100644
--- a/src/views/financialManagement/payable/purchaseIn.vue
+++ b/src/views/financialManagement/payable/purchaseIn.vue
@@ -49,6 +49,8 @@
:column="columns"
:tableData="dataList"
:tableLoading="tableLoading"
+ isShowSummary
+ :summaryMethod="getSummaries"
:page="{
current: pagination.currentPage,
size: pagination.pageSize,
@@ -189,7 +191,25 @@
getTableData();
};
- const handleOut = () => {
+ const getSummaries = ({ columns, data }) => {
+ const sums = [];
+ columns.forEach((col, index) => {
+ if (index === 0) {
+ sums[index] = "鍚堣";
+ } else if (col.property === "inboundAmount") {
+ const total = data.reduce((prev, cur) => {
+ const v = Number(cur.inboundAmount);
+ return prev + (isNaN(v) ? 0 : v);
+ }, 0);
+ sums[index] = total.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+ } else {
+ sums[index] = "";
+ }
+ });
+ return sums;
+};
+
+const handleOut = () => {
proxy.download(
"/accountPurchase/exportAccountPurchaseInbound",
buildFilterParams(),
diff --git a/src/views/financialManagement/payable/purchaseReturn.vue b/src/views/financialManagement/payable/purchaseReturn.vue
index eeec383..4171df2 100644
--- a/src/views/financialManagement/payable/purchaseReturn.vue
+++ b/src/views/financialManagement/payable/purchaseReturn.vue
@@ -44,6 +44,8 @@
:column="columns"
:tableData="dataList"
:tableLoading="tableLoading"
+ isShowSummary
+ :summaryMethod="getSummaries"
:page="{
current: pagination.currentPage,
size: pagination.pageSize,
@@ -175,6 +177,24 @@
getTableData();
};
+const getSummaries = ({ columns, data }) => {
+ const sums = [];
+ columns.forEach((col, index) => {
+ if (index === 0) {
+ sums[index] = "鍚堣";
+ } else if (col.property === "totalAmount") {
+ const total = data.reduce((prev, cur) => {
+ const v = Number(cur.totalAmount);
+ return prev + (isNaN(v) ? 0 : v);
+ }, 0);
+ sums[index] = total.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+ } else {
+ sums[index] = "";
+ }
+ });
+ return sums;
+};
+
const handleOut = () => {
proxy.download(
"/accountPurchase/exportAccountPurchaseReturn",
diff --git a/src/views/financialManagement/payable/reconciliation.vue b/src/views/financialManagement/payable/reconciliation.vue
index e749e56..a3b8495 100644
--- a/src/views/financialManagement/payable/reconciliation.vue
+++ b/src/views/financialManagement/payable/reconciliation.vue
@@ -35,6 +35,8 @@
:column="columns"
:tableData="dataList"
:tableLoading="tableLoading"
+ isShowSummary
+ :summaryMethod="getSummaries"
:page="{
current: pagination.currentPage,
size: pagination.pageSize,
@@ -405,6 +407,26 @@
accountStatementDetails: selectedPurchases.value.map(buildDetailSubmitItem),
});
+const summaryProps = ["openingBalance", "currentPlan", "currentActually", "closingBalance"];
+
+const getSummaries = ({ columns, data }) => {
+ const sums = [];
+ columns.forEach((col, index) => {
+ if (index === 0) {
+ sums[index] = "鍚堣";
+ } else if (summaryProps.includes(col.property)) {
+ const total = data.reduce((prev, cur) => {
+ const v = Number(cur[col.property]);
+ return prev + (isNaN(v) ? 0 : v);
+ }, 0);
+ sums[index] = Number(total.toFixed(2)).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+ } else {
+ sums[index] = "";
+ }
+ });
+ return sums;
+};
+
const formatMoney = (value) => {
if (value === undefined || value === null) return "0.00";
return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
diff --git a/src/views/financialManagement/receivable/invoiceApply.vue b/src/views/financialManagement/receivable/invoiceApply.vue
index d5ab6dc..21b5f0e 100644
--- a/src/views/financialManagement/receivable/invoiceApply.vue
+++ b/src/views/financialManagement/receivable/invoiceApply.vue
@@ -48,6 +48,8 @@
v-loading="tableLoading"
:column="columns"
:tableData="dataList"
+ isShowSummary
+ :summaryMethod="getSummaries"
:page="{
current: pagination.currentPage,
size: pagination.pageSize,
@@ -193,7 +195,7 @@
<el-col :span="12">
<el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
<el-date-picker
- v-model="form.createTime"
+ v-model="formCreateTimeDate"
type="date"
placeholder="閫夋嫨鏃ユ湡"
value-format="YYYY-MM-DD"
@@ -267,6 +269,7 @@
<script setup>
import { ref, reactive, computed, onMounted, nextTick, getCurrentInstance, defineAsyncComponent } from "vue";
+import dayjs from "dayjs";
import { ElMessage, ElMessageBox } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import { listCustomer } from "@/api/basicData/customer.js";
@@ -540,6 +543,12 @@
remark: "",
createTime: "",
});
+const formCreateTimeDate = computed({
+ get: () => (form.createTime ? String(form.createTime).split(" ")[0] : ""),
+ set: (value) => {
+ form.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : "";
+ },
+});
const rules = {
customerId: [{ required: true, message: "璇烽�夋嫨瀹㈡埛", trigger: "change" }],
@@ -641,6 +650,27 @@
proxy.download("/accountInvoiceApplication/exportAccountInvoiceApplication", params, filename);
};
+const getSummaries = ({ columns, data }) => {
+ const sums = [];
+ columns.forEach((col, index) => {
+ if (index === 0) {
+ sums[index] = "鍚堣";
+ } else if (col.property === "amount") {
+ const total = data.reduce((prev, cur) => {
+ const v = Number(cur.amount);
+ return prev + (isNaN(v) ? 0 : v);
+ }, 0);
+ sums[index] = total.toLocaleString("zh-CN", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ });
+ } else {
+ sums[index] = "";
+ }
+ });
+ return sums;
+};
+
const formatMoney = (value) => {
if (value === undefined || value === null) return "0.00";
return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
@@ -721,6 +751,7 @@
status: normalizeStatus(row.status ?? row.auditStatus),
outboundBatchNos,
outboundBatches: formatOutboundBatches(row.outboundBatches),
+ createTime: row.createTime ?? "",
});
};
@@ -739,7 +770,7 @@
applyDate: new Date().toISOString().split("T")[0],
content: "",
remark: "",
- createTime: new Date().toISOString().split("T")[0],
+ createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
});
outboundBatchList.value = [];
outboundBatchOptions.value = [];
diff --git a/src/views/financialManagement/receivable/outputInvoice.vue b/src/views/financialManagement/receivable/outputInvoice.vue
index d746aea..22cc202 100644
--- a/src/views/financialManagement/receivable/outputInvoice.vue
+++ b/src/views/financialManagement/receivable/outputInvoice.vue
@@ -46,6 +46,8 @@
v-loading="tableLoading"
:column="columns"
:tableData="dataList"
+ isShowSummary
+ :summaryMethod="getSummaries"
:page="{
current: pagination.currentPage,
size: pagination.pageSize,
@@ -324,6 +326,27 @@
amount: [{ required: true, message: "璇疯緭鍏ラ噾棰�", trigger: "blur" }],
};
+const getSummaries = ({ columns, data }) => {
+ const sums = [];
+ columns.forEach((col, index) => {
+ if (index === 0) {
+ sums[index] = "鍚堣";
+ } else if (col.property === "amount") {
+ const total = data.reduce((prev, cur) => {
+ const v = Number(cur.amount);
+ return prev + (isNaN(v) ? 0 : v);
+ }, 0);
+ sums[index] = total.toLocaleString("zh-CN", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ });
+ } else {
+ sums[index] = "";
+ }
+ });
+ return sums;
+};
+
const formatMoney = (value) => {
if (value === undefined || value === null) return "0.00";
return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
diff --git a/src/views/financialManagement/receivable/receipt.vue b/src/views/financialManagement/receivable/receipt.vue
index 1c84594..ae7a763 100644
--- a/src/views/financialManagement/receivable/receipt.vue
+++ b/src/views/financialManagement/receivable/receipt.vue
@@ -196,7 +196,7 @@
<el-col :span="12">
<el-form-item label="鍒涘缓鏃堕棿"
prop="createTime">
- <el-date-picker v-model="form.createTime"
+ <el-date-picker v-model="formCreateTimeDate"
type="date"
placeholder="閫夋嫨鏃ユ湡"
value-format="YYYY-MM-DD"
@@ -299,6 +299,7 @@
nextTick,
getCurrentInstance,
} from "vue";
+ import dayjs from "dayjs";
import { ElMessage, ElMessageBox } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import { listCustomer } from "@/api/basicData/customer.js";
@@ -391,6 +392,12 @@
outboundBatches: "",
remark: "",
createTime: "",
+ });
+ const formCreateTimeDate = computed({
+ get: () => (form.createTime ? String(form.createTime).split(" ")[0] : ""),
+ set: (value) => {
+ form.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : "";
+ },
});
const rules = {
@@ -691,6 +698,7 @@
stockOutRecordIds,
outboundBatches: formatOutboundBatches(row.outboundBatches),
remark: row.remark ?? "",
+ createTime: row.createTime ?? "",
});
};
@@ -767,7 +775,7 @@
stockOutRecordIds: [],
outboundBatches: "",
remark: "",
- createTime: new Date().toISOString().split("T")[0],
+ createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
});
outboundBatchList.value = [];
outboundBatchOptions.value = [];
diff --git a/src/views/financialManagement/receivable/reconciliation.vue b/src/views/financialManagement/receivable/reconciliation.vue
index b1bff0e..03400ef 100644
--- a/src/views/financialManagement/receivable/reconciliation.vue
+++ b/src/views/financialManagement/receivable/reconciliation.vue
@@ -30,6 +30,8 @@
:column="columns"
:tableData="dataList"
:tableLoading="tableLoading"
+ isShowSummary
+ :summaryMethod="getSummaries"
:page="{
current: pagination.currentPage,
size: pagination.pageSize,
@@ -388,6 +390,29 @@
accountStatementDetails: selectedSales.value.map(buildDetailSubmitItem),
});
+const summaryProps = ["openingBalance", "currentPlan", "currentActually", "closingBalance"];
+
+const getSummaries = ({ columns, data }) => {
+ const sums = [];
+ columns.forEach((col, index) => {
+ if (index === 0) {
+ sums[index] = "鍚堣";
+ } else if (summaryProps.includes(col.property)) {
+ const total = data.reduce((prev, cur) => {
+ const v = Number(cur[col.property]);
+ return prev + (isNaN(v) ? 0 : v);
+ }, 0);
+ sums[index] = Number(total.toFixed(2)).toLocaleString("zh-CN", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ });
+ } else {
+ sums[index] = "";
+ }
+ });
+ return sums;
+};
+
const formatMoney = (value) => {
if (value === undefined || value === null) return "0.00";
return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
diff --git a/src/views/financialManagement/receivable/salesOut.vue b/src/views/financialManagement/receivable/salesOut.vue
index 0e24b37..f6205d8 100644
--- a/src/views/financialManagement/receivable/salesOut.vue
+++ b/src/views/financialManagement/receivable/salesOut.vue
@@ -43,6 +43,8 @@
:column="columns"
:tableData="dataList"
:tableLoading="tableLoading"
+ isShowSummary
+ :summaryMethod="getSummaries"
:page="{
current: pagination.currentPage,
size: pagination.pageSize,
@@ -86,7 +88,6 @@
label: "閲戦",
prop: "outboundAmount",
minWidth: "120",
- align: "right",
formatData: val =>
val === null || val === undefined || val === ""
? ""
@@ -158,6 +159,27 @@
getTableData();
};
+ const getSummaries = ({ columns, data }) => {
+ const sums = [];
+ columns.forEach((col, index) => {
+ if (index === 0) {
+ sums[index] = "鍚堣";
+ } else if (col.property === "outboundAmount") {
+ const total = data.reduce((prev, cur) => {
+ const v = Number(cur.outboundAmount);
+ return prev + (isNaN(v) ? 0 : v);
+ }, 0);
+ sums[index] = total.toLocaleString("zh-CN", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ });
+ } else {
+ sums[index] = "";
+ }
+ });
+ return sums;
+ };
+
const handleOut = () => {
proxy.download(
"/accountSales/exportAccountSalesOutbound",
diff --git a/src/views/financialManagement/receivable/salesReturn.vue b/src/views/financialManagement/receivable/salesReturn.vue
index c58d330..afe363c 100644
--- a/src/views/financialManagement/receivable/salesReturn.vue
+++ b/src/views/financialManagement/receivable/salesReturn.vue
@@ -37,6 +37,8 @@
:column="columns"
:tableData="dataList"
:tableLoading="tableLoading"
+ isShowSummary
+ :summaryMethod="getSummaries"
:page="{
current: pagination.currentPage,
size: pagination.pageSize,
@@ -80,7 +82,6 @@
label: "閫�娆炬�婚",
prop: "refundAmount",
minWidth: "120",
- align: "right",
formatData: (val) =>
val === null || val === undefined || val === ""
? ""
@@ -149,6 +150,27 @@
getTableData();
};
+const getSummaries = ({ columns, data }) => {
+ const sums = [];
+ columns.forEach((col, index) => {
+ if (index === 0) {
+ sums[index] = "鍚堣";
+ } else if (col.property === "refundAmount") {
+ const total = data.reduce((prev, cur) => {
+ const v = Number(cur.refundAmount);
+ return prev + (isNaN(v) ? 0 : v);
+ }, 0);
+ sums[index] = total.toLocaleString("zh-CN", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ });
+ } else {
+ sums[index] = "";
+ }
+ });
+ return sums;
+};
+
const handleOut = () => {
proxy.download(
"/accountSales/exportAccountSalesReturn",
diff --git a/src/views/financialManagement/voucher/index.vue b/src/views/financialManagement/voucher/index.vue
index 1aa6f69..0c02d51 100644
--- a/src/views/financialManagement/voucher/index.vue
+++ b/src/views/financialManagement/voucher/index.vue
@@ -25,7 +25,7 @@
</el-select>
</el-form-item>
<el-form-item>
- <el-button type="primary" @click="getTableData">鎼滅储</el-button>
+ <el-button type="primary" @click="onSearch">鎼滅储</el-button>
<el-button @click="resetFilters">閲嶇疆</el-button>
</el-form-item>
</el-form>
@@ -453,6 +453,11 @@
return map[key] || "";
};
+const onSearch = () => {
+ pagination.currentPage = 1;
+ getTableData();
+};
+
// 鑱旇皟绾﹀畾锛氬垎椤靛弬鏁颁娇鐢� current/size锛屾棩鏈熻寖鍥存媶鍒嗕负 startDate/endDate
const getTableData = async () => {
try {
@@ -518,9 +523,9 @@
getTableData();
};
-const changePage = ({ current, size }) => {
- pagination.currentPage = current;
- pagination.pageSize = size;
+const changePage = ({ page, limit }) => {
+ pagination.currentPage = page;
+ pagination.pageSize = limit;
getTableData();
};
diff --git a/src/views/index.vue b/src/views/index.vue
index e12ded2..5785cbe 100644
--- a/src/views/index.vue
+++ b/src/views/index.vue
@@ -120,33 +120,35 @@
<el-radio-button :value="'paused'">宸叉殏鍋�({{ orderProgressMeta.pausedCount }})</el-radio-button>
</el-radio-group>
</div>
- <el-table :data="filteredOrders" stripe>
- <el-table-column prop="orderNo" label="璁㈠崟缂栧彿" min-width="150" />
- <el-table-column prop="productName" label="浜у搧鍚嶇О" min-width="120" />
- <el-table-column prop="planQty" label="璁″垝鏁伴噺" min-width="90" />
- <el-table-column prop="completedQty" label="宸插畬鎴�" min-width="90" />
- <el-table-column label="瀹屾垚鐜�" min-width="180">
- <template #default="{ row }">
- <div class="table-progress">
- <el-progress
- :stroke-width="8"
- :percentage="row.completionRate"
- :show-text="false"
- status="success"
- />
- <span>{{ row.completionRate }}%</span>
- </div>
- </template>
- </el-table-column>
- <el-table-column prop="deliveryDate" label="浜ゆ湡" min-width="110" />
- <el-table-column label="鐘舵��" min-width="90">
- <template #default="{ row }">
- <el-tag :type="getOrderStatusType(row.status)" effect="light">
- {{ row.statusLabel || getOrderStatusText(row.status) }}
- </el-tag>
- </template>
- </el-table-column>
- </el-table>
+ <div class="order-table-wrap">
+ <el-table :data="filteredOrders" stripe max-height="440">
+ <el-table-column prop="orderNo" label="璁㈠崟缂栧彿" min-width="150" />
+ <el-table-column prop="productName" label="浜у搧鍚嶇О" min-width="120" />
+ <el-table-column prop="planQty" label="璁″垝鏁伴噺" min-width="90" />
+ <el-table-column prop="completedQty" label="宸插畬鎴�" min-width="90" />
+ <el-table-column label="瀹屾垚鐜�" min-width="180">
+ <template #default="{ row }">
+ <div class="table-progress">
+ <el-progress
+ :stroke-width="8"
+ :percentage="row.completionRate"
+ :show-text="false"
+ status="success"
+ />
+ <span>{{ row.completionRate }}%</span>
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column prop="deliveryDate" label="浜ゆ湡" min-width="110" />
+ <el-table-column label="鐘舵��" min-width="90">
+ <template #default="{ row }">
+ <el-tag :type="getOrderStatusType(row.status)" effect="light">
+ {{ row.statusLabel || getOrderStatusText(row.status) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
</div>
<div v-if="visiblePanels.contract" class="cockpit-panel contract-panel">
@@ -1232,8 +1234,8 @@
const res = await productionOrderProgress({
status: orderFilter.value,
tab: orderFilter.value,
- pageNum: 1,
- pageSize: 10,
+ pageNum: -1,
+ pageSize: -1,
});
const data = res?.data || {};
const statusValue = normalizeOrderFilter(data.status, orderFilter.value);
@@ -2194,6 +2196,13 @@
.order-panel {
min-height: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+.order-table-wrap {
+ flex: 1;
+ min-height: 0;
}
.quick-panel {
diff --git a/src/views/inventoryManagement/dispatchLog/Record.vue b/src/views/inventoryManagement/dispatchLog/Record.vue
index 2d7411d..5a056d6 100644
--- a/src/views/inventoryManagement/dispatchLog/Record.vue
+++ b/src/views/inventoryManagement/dispatchLog/Record.vue
@@ -76,6 +76,8 @@
<div class="actions">
<el-button type="primary"
@click="handleBatchApprove">瀹℃壒</el-button>
+ <el-button :disabled="!canReverseApprove"
+ @click="handleReverseApprove">鍙嶅</el-button>
<el-button @click="handleOut">瀵煎嚭</el-button>
<el-button type="danger"
plain
@@ -163,6 +165,7 @@
getStockOutPage,
delPendingStockOut,
batchApproveStockOutRecords,
+ batchUnapproveStockOutRecords,
} from "@/api/inventoryManagement/stockOut.js";
import {
findAllQualifiedStockOutRecordTypeOptions,
@@ -306,6 +309,44 @@
selectedRows.value = selection.filter(item => item.id);
console.log("selection", selectedRows.value);
};
+
+ const isRejectedApproval = status => {
+ return status === 2 || status === "2" || status === "rejected" || status === "REJECTED";
+ };
+
+ const canReverseApprove = computed(() => {
+ return (
+ selectedRows.value.length > 0 &&
+ selectedRows.value.every(row => isRejectedApproval(row.approvalStatus))
+ );
+ });
+
+ const handleReverseApprove = () => {
+ if (!canReverseApprove.value) {
+ proxy.$modal.msgWarning("璇烽�夋嫨椹冲洖鐘舵�佺殑鏁版嵁");
+ return;
+ }
+ const ids = selectedRows.value.map(item => item.id);
+ ElMessageBox.confirm("鍙嶅鍚庤褰曞皢鎭㈠涓哄緟瀹℃壒鐘舵�侊紝鏄惁纭鍙嶅锛�", "鍙嶅", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ batchUnapproveStockOutRecords({ ids })
+ .then(() => {
+ proxy.$modal.msgSuccess("鍙嶅鎴愬姛");
+ getList();
+ })
+ .catch(() => {
+ proxy.$modal.msgError("鍙嶅澶辫触");
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+
const expandedRowKeys = ref([]);
const handleBatchApprove = () => {
diff --git a/src/views/inventoryManagement/stockManagement/New.vue b/src/views/inventoryManagement/stockManagement/New.vue
index 2addb95..e55de75 100644
--- a/src/views/inventoryManagement/stockManagement/New.vue
+++ b/src/views/inventoryManagement/stockManagement/New.vue
@@ -52,8 +52,9 @@
<el-form-item label="搴撳瓨鏁伴噺"
prop="qualitity">
<el-input-number v-model="formState.qualitity"
- :step="1"
- :min="1"
+ :step="0.01"
+ :min="0.01"
+ :precision="2"
style="width: 100%" />
</el-form-item>
<el-form-item label="鎵瑰彿"
@@ -65,9 +66,10 @@
label="搴撳瓨棰勮鏁伴噺"
prop="warnNum">
<el-input-number v-model="formState.warnNum"
- :step="1"
+ :step="0.01"
:min="0"
:max="formState.qualitity"
+ :precision="2"
style="width: 100%" />
</el-form-item>
<el-form-item label="澶囨敞"
diff --git a/src/views/mock/dataCheck/index.vue b/src/views/mock/dataCheck/index.vue
new file mode 100644
index 0000000..5782e7d
--- /dev/null
+++ b/src/views/mock/dataCheck/index.vue
@@ -0,0 +1,241 @@
+<template>
+ <div class="app-container">
+ <el-card header="鍩虹鏁版嵁妫�娴�">
+ <el-checkbox-group v-model="selectedModules">
+ <el-checkbox label="sales">閿�鍞ā鍧�</el-checkbox>
+ <el-checkbox label="purchase">閲囪喘妯″潡</el-checkbox>
+ <el-checkbox label="quality">璐ㄩ噺妯″潡</el-checkbox>
+ <el-checkbox label="production">鐢熶骇妯″潡</el-checkbox>
+ </el-checkbox-group>
+ <div style="margin-top: 16px">
+ <el-button type="primary" @click="handleCheck" :loading="checking">
+ 寮�濮嬫娴�
+ </el-button>
+ </div>
+ </el-card>
+
+ <el-card v-if="checkResult" header="妫�娴嬬粨鏋�" style="margin-top: 16px">
+ <el-alert
+ :title="`閫氳繃 ${checkResult.passedItems} / ${checkResult.totalItems} 椤筦"
+ :type="checkResult.passedItems === checkResult.totalItems ? 'success' : 'warning'"
+ :closable="false"
+ show-icon
+ />
+ <el-table :data="checkResult.items" style="margin-top: 12px" border>
+ <el-table-column prop="module" label="妯″潡" width="100" />
+ <el-table-column prop="itemName" label="妫�娴嬮」" width="160" />
+ <el-table-column prop="minRequired" label="鏈�浣庤姹�" width="80" align="center" />
+ <el-table-column prop="currentCount" label="褰撳墠鏁伴噺" width="80" align="center" />
+ <el-table-column label="鐘舵��" width="80" align="center">
+ <template #default="{ row }">
+ <el-tag :type="row.passed ? 'success' : 'danger'">
+ {{ row.passed ? "閫氳繃" : "鏈�氳繃" }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="message" label="鎻愮ず" min-width="200" />
+ </el-table>
+ </el-card>
+
+ <!-- AI 鏁版嵁鐢熸垚 -->
+ <el-card header="AI 鏁版嵁鐢熸垚" style="margin-top: 16px">
+ <el-form :model="generateForm" label-width="100px">
+ <el-form-item label="閫夋嫨妯″潡">
+ <el-checkbox-group v-model="generateForm.modules">
+ <el-checkbox label="sales">閿�鍞�</el-checkbox>
+ <el-checkbox label="purchase">閲囪喘</el-checkbox>
+ <el-checkbox label="quality">璐ㄩ噺</el-checkbox>
+ <el-checkbox label="production">鐢熶骇</el-checkbox>
+ <el-checkbox label="stock">搴撳瓨</el-checkbox>
+ </el-checkbox-group>
+ </el-form-item>
+
+ <el-form-item label="琛屼笟">
+ <el-select
+ v-model="generateForm.industries"
+ multiple
+ filterable
+ allow-create
+ placeholder="杈撳叆鎴栭�夋嫨琛屼笟"
+ >
+ <el-option label="鏈烘鍒堕��" value="鏈烘鍒堕��" />
+ <el-option label="椋熷搧鍔犲伐" value="椋熷搧鍔犲伐" />
+ <el-option label="鐢靛瓙瑁呴厤" value="鐢靛瓙瑁呴厤" />
+ <el-option label="姹借溅闆堕儴浠�" value="姹借溅闆堕儴浠�" />
+ <el-option label="鍖荤枟鍣ㄦ" value="鍖荤枟鍣ㄦ" />
+ <el-option label="鍖栧伐鏉愭枡" value="鍖栧伐鏉愭枡" />
+ <el-option label="绾虹粐鏈嶈" value="绾虹粐鏈嶈" />
+ <el-option label="瀹跺叿鍒堕��" value="瀹跺叿鍒堕��" />
+ </el-select>
+ </el-form-item>
+
+ <el-row :gutter="16">
+ <el-col :span="12">
+ <el-form-item label="鐢熸垚鏉℃暟">
+ <el-slider
+ v-model="generateForm.countRange"
+ range
+ :min="1"
+ :max="50"
+ :marks="{ 1: '1', 10: '10', 20: '20', 50: '50' }"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏃堕棿鑼冨洿">
+ <el-date-picker
+ v-model="generateForm.dateRange"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ unlink-panels
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-form-item label="琛ュ厖淇℃伅">
+ <el-input
+ v-model="generateForm.additionalInfo"
+ type="textarea"
+ :rows="2"
+ placeholder="濡傦細浜у搧浠ラ噾灞為浂閮ㄤ欢涓轰富锛屽鎴烽泦涓湪鍗庝笢鍦板尯"
+ />
+ </el-form-item>
+
+ <el-form-item>
+ <el-button type="primary" @click="handleGenerate" :loading="generating">
+ 寮�濮嬬敓鎴�
+ </el-button>
+ <el-button @click="handleCheckFirst" :loading="checking">
+ 鍏堟娴嬪啀鐢熸垚
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+
+ <!-- 鐢熸垚缁撴灉 -->
+ <el-card v-if="generateResult" header="鐢熸垚缁撴灉" style="margin-top: 16px">
+ <el-alert
+ :title="`鍏辩敓鎴� ${generateResult.totalGenerated} 鏉℃暟鎹甡"
+ type="success"
+ :closable="false"
+ show-icon
+ />
+ <el-table :data="generateResult.moduleSummaries" style="margin-top: 12px" border>
+ <el-table-column prop="module" label="妯″潡" width="100" />
+ <el-table-column prop="entityName" label="瀹炰綋鍚嶇О" width="160" />
+ <el-table-column prop="generatedCount" label="鐢熸垚鏉℃暟" width="80" align="center" />
+ <el-table-column prop="successCount" label="鎴愬姛" width="80" align="center">
+ <template #default="{ row }">
+ <el-tag type="success">{{ row.successCount }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="failCount" label="澶辫触" width="80" align="center">
+ <template #default="{ row }">
+ <el-tag v-if="row.failCount > 0" type="danger">{{ row.failCount }}</el-tag>
+ <span v-else>0</span>
+ </template>
+ </el-table-column>
+ </el-table>
+ <div v-if="generateResult.errors && generateResult.errors.length > 0" style="margin-top: 12px">
+ <el-alert
+ v-for="(err, i) in generateResult.errors"
+ :key="i"
+ :title="err"
+ type="error"
+ :closable="false"
+ style="margin-bottom: 4px"
+ />
+ </div>
+ </el-card>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive } from "vue";
+import { ElMessage } from "element-plus";
+import { dataCheck } from "@/api/mock/dataCheck.js";
+import { mockGenerate } from "@/api/mock/generate.js";
+
+const selectedModules = ref(["sales", "purchase", "quality", "production"]);
+const checking = ref(false);
+const checkResult = ref(null);
+
+const generateForm = reactive({
+ modules: [],
+ industries: [],
+ countRange: [3, 10],
+ dateRange: [],
+ additionalInfo: "",
+});
+
+const generating = ref(false);
+const generateResult = ref(null);
+
+const handleCheck = async () => {
+ if (selectedModules.value.length === 0) {
+ ElMessage.warning("璇疯嚦灏戦�夋嫨涓�涓ā鍧�");
+ return;
+ }
+ checking.value = true;
+ try {
+ const res = await dataCheck(selectedModules.value);
+ checkResult.value = res.data;
+ } finally {
+ checking.value = false;
+ }
+};
+
+// 鐩存帴鐢熸垚锛堝唴閮ㄨ嚜鍔ㄦ娴嬶級
+const handleGenerate = async () => {
+ if (generateForm.modules.length === 0) {
+ ElMessage.warning("璇疯嚦灏戦�夋嫨涓�涓ā鍧�");
+ return;
+ }
+ generating.value = true;
+ try {
+ const res = await mockGenerate({
+ modules: generateForm.modules,
+ industries: generateForm.industries,
+ countMin: generateForm.countRange[0],
+ countMax: generateForm.countRange[1],
+ dateStart: generateForm.dateRange[0] || null,
+ dateEnd: generateForm.dateRange[1] || null,
+ additionalInfo: generateForm.additionalInfo,
+ });
+ generateResult.value = res.data;
+ ElMessage.success(`鎴愬姛鐢熸垚 ${res.data.totalGenerated} 鏉℃暟鎹甡);
+ } catch (err) {
+ // code 500 鏃跺搷搴旀嫤鎴櫒浼氬脊閿欒鎻愮ず锛屾澶勪笉鍐嶉噸澶嶆彁绀�
+ // 鑻ラ渶瑕佸睍绀烘娴嬬粨鏋滐紝闇�瑕佸悗绔湪闈�200鏃朵篃杩斿洖鏁版嵁锛屾澶勬殏涓嶅鐞�
+ } finally {
+ generating.value = false;
+ }
+};
+
+// 鍏堟娴嬪啀鐢熸垚
+const handleCheckFirst = async () => {
+ if (generateForm.modules.length === 0) {
+ ElMessage.warning("璇疯嚦灏戦�夋嫨涓�涓ā鍧�");
+ return;
+ }
+ checking.value = true;
+ try {
+ const res = await dataCheck(generateForm.modules);
+ const result = res.data;
+ if (result.passedItems === result.totalItems) {
+ await handleGenerate();
+ } else {
+ checkResult.value = result;
+ ElMessage.warning("璇峰厛琛ラ綈鍩虹鏁版嵁鍚庡啀鐢熸垚");
+ }
+ } finally {
+ checking.value = false;
+ }
+};
+</script>
+
+<style scoped></style>
\ No newline at end of file
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js b/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js
index 3251f0c..e2f1e45 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js
@@ -457,6 +457,9 @@
currentNodeIndex: 0,
approvalRecords,
rejectReason: approvalRecords.find(r => r.result === "rejected")?.opinion || "",
+ purchaseContractNumber: row.purchaseContractNumber || "",
+ quotationNo: row.quotationNo || "",
+ shippingNo: row.shippingNo || "",
};
}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue b/src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue
index 721a5c2..58b612b 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue
@@ -19,9 +19,9 @@
{{ approvalTypeLabel(row.approvalType) }}
</span>
</el-descriptions-item>
- <el-descriptions-item label="鐢宠浜虹紪鍙�">{{ row.applicantNo || "鈥�" }}</el-descriptions-item>
- <el-descriptions-item label="鐢宠浜哄悕绉�">{{ row.applicantName || "鈥�" }}</el-descriptions-item>
- <el-descriptions-item label="鐢宠鎽樿">{{ row.summary || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="鍙戣捣浜虹紪鍙�">{{ row.applicantNo || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="鍙戣捣浜哄悕绉�">{{ row.applicantName || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="鎽樿">{{ row.summary || "鈥�" }}</el-descriptions-item>
<el-descriptions-item v-if="row.rejectReason"
label="椹冲洖鍘熷洜"
:span="2">
@@ -35,9 +35,116 @@
</div>
<div class="detail-block">
<div class="detail-block-title">濉姤鍐呭</div>
- <FormPayloadFields :fields="formResolved.fields"
+ <!-- 榛樿琛ㄥ崟灞曠ず -->
+ <FormPayloadFields v-if="!isSpecialApprovalType"
+ :fields="formResolved.fields"
:form-payload="formResolved.formPayload"
readonly />
+
+ <!-- 鍙戣揣瀹℃壒璇︽儏 -->
+ <template v-else-if="row.businessType === 7">
+ <div v-if="detailData.shippingInfo" class="shipment-detail">
+ <el-divider content-position="left">鍙戣揣璇︽儏</el-divider>
+ <el-descriptions :column="2" border>
+ <el-descriptions-item label="閿�鍞鍗�">{{ detailData.shippingInfo.salesContractNo || "--" }}</el-descriptions-item>
+ <el-descriptions-item label="鍙戣揣璁㈠崟鍙�">{{ detailData.shippingInfo.shippingNo || "--" }}</el-descriptions-item>
+ <el-descriptions-item label="瀹㈡埛鍚嶇О">{{ detailData.shippingInfo.customerName || "--" }}</el-descriptions-item>
+ <el-descriptions-item label="鍙戣揣绫诲瀷">{{ detailData.shippingInfo.type || "--" }}</el-descriptions-item>
+ <el-descriptions-item label="鍙戣揣鏃ユ湡">{{ detailData.shippingInfo.shippingDateDate || "--" }}</el-descriptions-item>
+ <el-descriptions-item label="瀹℃牳鐘舵��">{{ detailData.shippingInfo.status || "--" }}</el-descriptions-item>
+ <el-descriptions-item label="鍙戣揣杞︾墝鍙�">{{ detailData.shippingInfo.shippingCarNumber || "--" }}</el-descriptions-item>
+ <el-descriptions-item label="蹇�掑叕鍙�">{{ detailData.shippingInfo.expressCompany || "--" }}</el-descriptions-item>
+ <el-descriptions-item label="蹇�掑崟鍙�" :span="2">{{ detailData.shippingInfo.expressNumber || "--" }}</el-descriptions-item>
+ </el-descriptions>
+ <div v-if="detailData.shippingProductDetailDtoList.length" style="margin-top: 20px;">
+ <h4>浜у搧鏄庣粏</h4>
+ <el-table :data="detailData.shippingProductDetailDtoList"
+ border
+ size="small"
+ style="width: 100%">
+ <el-table-column label="鎵瑰彿" prop="batchNo" min-width="160" show-overflow-tooltip />
+ <el-table-column label="浜у搧鍚嶇О" prop="productName" min-width="160" show-overflow-tooltip />
+ <el-table-column label="瑙勬牸鍨嬪彿" prop="specificationModel" min-width="160" show-overflow-tooltip />
+ <el-table-column label="鍙戣揣鏁伴噺" prop="deliveryQuantity" min-width="120" align="center" />
+ </el-table>
+ </div>
+ </div>
+ </template>
+
+ <!-- 閲囪喘瀹℃壒璇︽儏 -->
+ <template v-else-if="row.businessType === 5">
+ <div v-if="detailData" class="procurement-detail">
+ <el-divider content-position="left">閲囪喘璇︽儏</el-divider>
+ <el-descriptions :column="2" border>
+ <el-descriptions-item label="閲囪喘鍚堝悓鍙�">{{ detailData.purchaseContractNumber || "--" }}</el-descriptions-item>
+ <el-descriptions-item label="渚涘簲鍟嗗悕绉�">{{ detailData.supplierName || "--" }}</el-descriptions-item>
+ <el-descriptions-item label="椤圭洰鍚嶇О">{{ detailData.projectName || "--" }}</el-descriptions-item>
+ <el-descriptions-item label="閿�鍞悎鍚屽彿">{{ detailData.salesContractNo || "--" }}</el-descriptions-item>
+ <el-descriptions-item label="绛捐鏃ユ湡">{{ detailData.executionDate || "--" }}</el-descriptions-item>
+ <el-descriptions-item label="褰曞叆鏃ユ湡">{{ detailData.entryDate || "--" }}</el-descriptions-item>
+ <el-descriptions-item label="浠樻鏂瑰紡">{{ detailData.paymentMethod || "--" }}</el-descriptions-item>
+ <el-descriptions-item label="鍚堝悓閲戦" :span="2">
+ <span style="font-size: 18px; color: #e6a23c; font-weight: bold;">
+ 楼{{ Number(detailData.contractAmount ?? 0).toFixed(2) }}
+ </span>
+ </el-descriptions-item>
+ </el-descriptions>
+ <div v-if="detailData.productData.length" style="margin-top: 20px;">
+ <h4>浜у搧鏄庣粏</h4>
+ <el-table :data="detailData.productData"
+ border
+ style="width: 100%">
+ <el-table-column prop="productCategory" label="浜у搧鍚嶇О" />
+ <el-table-column prop="specificationModel" label="瑙勬牸鍨嬪彿" />
+ <el-table-column prop="unit" label="鍗曚綅" />
+ <el-table-column prop="quantity" label="鏁伴噺" />
+ <el-table-column prop="taxInclusiveUnitPrice" label="鍚◣鍗曚环">
+ <template #default="scope">楼{{ Number(scope.row.taxInclusiveUnitPrice ?? 0).toFixed(2) }}</template>
+ </el-table-column>
+ <el-table-column prop="taxInclusiveTotalPrice" label="鍚◣鎬讳环">
+ <template #default="scope">楼{{ Number(scope.row.taxInclusiveTotalPrice ?? 0).toFixed(2) }}</template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </div>
+ </template>
+
+ <!-- 鎶ヤ环瀹℃壒璇︽儏 -->
+ <template v-else-if="row.businessType === 6">
+ <div v-if="detailData" class="quotation-detail">
+ <el-divider content-position="left">鎶ヤ环璇︽儏</el-divider>
+ <el-descriptions :column="2" border>
+ <el-descriptions-item label="鎶ヤ环鍗曞彿">{{ detailData.quotationNo || "--" }}</el-descriptions-item>
+ <el-descriptions-item label="瀹㈡埛鍚嶇О">{{ detailData.customer || "--" }}</el-descriptions-item>
+ <el-descriptions-item label="涓氬姟鍛�">{{ detailData.salesperson || "--" }}</el-descriptions-item>
+ <el-descriptions-item label="鎶ヤ环鏃ユ湡">{{ detailData.quotationDate || "--" }}</el-descriptions-item>
+ <el-descriptions-item label="鏈夋晥鏈熻嚦">{{ detailData.validDate || "--" }}</el-descriptions-item>
+ <el-descriptions-item label="浠樻鏂瑰紡">{{ detailData.paymentMethod || "--" }}</el-descriptions-item>
+ <el-descriptions-item label="鎶ヤ环鎬婚" :span="2">
+ <span style="font-size: 18px; color: #e6a23c; font-weight: bold;">
+ 楼{{ Number(detailData.totalAmount ?? 0).toFixed(2) }}
+ </span>
+ </el-descriptions-item>
+ </el-descriptions>
+ <div v-if="detailData.products.length" style="margin-top: 20px;">
+ <h4>浜у搧鏄庣粏</h4>
+ <el-table :data="detailData.products"
+ border
+ style="width: 100%">
+ <el-table-column prop="product" label="浜у搧鍚嶇О" />
+ <el-table-column prop="specification" label="瑙勬牸鍨嬪彿" />
+ <el-table-column prop="unit" label="鍗曚綅" />
+ <el-table-column prop="unitPrice" label="鍗曚环">
+ <template #default="scope">楼{{ Number(scope.row.unitPrice ?? 0).toFixed(2) }}</template>
+ </el-table-column>
+ </el-table>
+ </div>
+ <div v-if="detailData.remark" style="margin-top: 20px;">
+ <h4>澶囨敞</h4>
+ <p>{{ detailData.remark }}</p>
+ </div>
+ </div>
+ </template>
</div>
<div v-if="attachmentList.length"
class="detail-block">
@@ -86,9 +193,21 @@
const props = defineProps({
row: { type: Object, default: () => ({}) },
+ detailData: { type: Object, default: () => ({}) },
});
const formResolved = computed(() => resolveInstanceFormFields(props.row));
+
+
+ // 鏄惁涓虹壒娈婂鎵圭被鍨嬶紙閲囪喘銆佸彂璐с�佹姤浠凤級
+ const isSpecialApprovalType = computed(() => {
+ return [5, 7, 6].includes(props.row.businessType);
+ });
+
+ // 璇︽儏鏁版嵁锛堢洿鎺ヤ娇鐢ㄤ紶鍏ョ殑 detail-data 鍙傛暟锛�
+ const detailData = computed(() => {
+ return props.detailData || {};
+ });
const attachmentList = computed(() => {
const list = props.row.storageBlobVOList || props.row.storageBlobDTOs || [];
@@ -164,4 +283,4 @@
align-items: center;
flex-shrink: 0;
}
-</style>
+</style>
\ No newline at end of file
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue b/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
index bbfa56a..4a9cf29 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
@@ -172,7 +172,7 @@
class="approve-detail-dialog"
>
<div class="approve-detail-body">
- <ApproveDetailPanel :row="detailRow" />
+ <ApproveDetailPanel :row="detailRow" :detail-data="detailData" />
<div class="detail-block">
<div class="detail-block-title">
瀹℃壒娴佺▼锛坽{ detailRow.tasks?.length || detailRow.flowNodes?.length || 0 }} 椤癸級
@@ -290,7 +290,7 @@
destroy-on-close
@closed="approveOpinion = ''"
>
- <ApproveDetailPanel :row="approveDialog.row" />
+ <ApproveDetailPanel :row="approveDialog.row" :detail-data="detailData" />
<div class="detail-block mt16">
<div class="detail-block-title">
瀹℃壒娴佺▼锛坽{ approveDialog.row?.tasks?.length || approveDialog.row?.flowNodes?.length || 0 }} 椤癸級
@@ -369,6 +369,7 @@
tableColumn,
detailDialog,
detailRow,
+ detailData,
reimburseDialog,
approveDialog,
approveOpinion,
@@ -610,4 +611,4 @@
border-left: 3px solid var(--el-color-primary);
line-height: 1.4;
}
-</style>
+</style>
\ No newline at end of file
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js b/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
index 67b9213..25b6ae5 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
@@ -10,6 +10,9 @@
saveApprovalInstance,
updateApprovalInstance,
} from "@/api/officeProcessAutomation/approvalInstance.js";
+import { getQuotationList } from "@/api/salesManagement/salesQuotation.js";
+import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger.js";
+import { getDeliveryDetailByShippingNo } from "@/api/salesManagement/deliveryLedger.js";
import useUserStore from "@/store/modules/user";
import { Search } from "@element-plus/icons-vue";
import { ElMessage, ElMessageBox } from "element-plus";
@@ -77,6 +80,7 @@
const detailDialog = reactive({ visible: false });
const detailRow = ref({});
+ const detailData = ref({});
const approveDialog = reactive({ visible: false, row: null });
const approveOpinion = ref("");
@@ -135,8 +139,8 @@
}));
const tableColumn = ref([
- { label: "鐢宠浜虹紪鍙�", prop: "applicantNo", width: 110 },
- { label: "鐢宠浜哄悕绉�", prop: "applicantName", minWidth: 100 },
+ // { label: "鐢宠浜虹紪鍙�", prop: "applicantNo", width: 110 },
+ { label: "鍙戣捣浜�", prop: "applicantName", minWidth: 100 },
{ label: "妯℃澘绫诲瀷", prop: "businessName", minWidth: 120 },
{
label: "瀹℃壒绫诲瀷",
@@ -150,7 +154,9 @@
prop: "unread",
width: 90,
align: "center",
+ dataType: "tag",
formatData: (v) => (v ? "鏄�" : "鍚�"),
+ formatType: (v) => (v ? "success" : "danger"),
},
{
label: "瀹℃壒鐘舵��",
@@ -186,11 +192,11 @@
disabled: (row) => row.approvalStatus !== "pending" || !row.isApprove,
clickFun: (row) => openApprove(row),
},
- {
- name: "鍒犻櫎",
- type: "danger",
- clickFun: (row) => removeInstance(row),
- },
+ // {
+ // name: "鍒犻櫎",
+ // type: "danger",
+ // clickFun: (row) => removeInstance(row),
+ // },
],
},
]);
@@ -220,7 +226,7 @@
fetchBusinessTypeOptions(),
listApprovalTemplate(TEMPLATE_TYPE_CUSTOM),
]);
- submitBusinessTypeOptions.value = typeOptions;
+ submitBusinessTypeOptions.value = typeOptions.filter((x) => ![5, 6, 7].includes(x.value));
allSubmitTemplates.value = unwrapTemplateList(customRes)
.filter((row) => mapEnabledFromApi(row.enabled))
.map(mapSubmitTemplateCard);
@@ -288,11 +294,46 @@
await openReimburseDetail(row, "detail");
return;
}
+
detailRow.value = { ...row };
+ // 鎶ヤ环瀹℃壒锛氱敤瀹℃壒浜嬬敱瀛楁鎵胯浇鐨�"鎶ヤ环鍗曞彿"鍘绘煡鎶ヤ环鍒楄〃
+ if (row.businessType === 6) {
+ const quotationNo = row?.quotationNo;
+ if (quotationNo) {
+ const res = await getQuotationList({ quotationNo });
+ const records = res?.data?.records || [];
+ detailData.value = records[0] || {};
+ }
+ }
+
+ // 閲囪喘瀹℃壒锛氱敤瀹℃壒浜嬬敱瀛楁鎵胯浇鐨�"閲囪喘鍚堝悓鍙�"鍘绘煡閲囪喘璇︽儏
+ else if (row.businessType === 5) {
+ const purchaseContractNumber = row?.purchaseContractNumber;
+ if (purchaseContractNumber) {
+ const res = await getPurchaseByCode({ purchaseContractNumber });
+ detailData.value = res || {};
+ }
+ }
+
+ // 鍙戣揣瀹℃壒锛氱敤瀹℃壒浜嬬敱瀛楁鎵胯浇鐨�"鍙戣揣鍗曞彿"鍘绘煡鍙戣揣璇︽儏
+ else if (row.businessType === 7) {
+ const shippingNo = row?.shippingNo;
+ if (shippingNo) {
+ const res = await getDeliveryDetailByShippingNo({ shippingNo });
+ detailData.value = res?.data || res || {};
+ }
+ }
+
+ // 鍏朵粬瀹℃壒绫诲瀷
+ else {
+ detailData.value = {};
+ }
+
detailDialog.visible = true;
}
async function openApprove(row) {
+
if (inferReimburseModuleKeyFromInstance(row)) {
approveOpinion.value = "";
await openReimburseDetail(row, "approve");
@@ -300,6 +341,38 @@
}
approveDialog.row = { ...row };
approveOpinion.value = "";
+ // 鎶ヤ环瀹℃壒锛氱敤瀹℃壒浜嬬敱瀛楁鎵胯浇鐨�"鎶ヤ环鍗曞彿"鍘绘煡鎶ヤ环鍒楄〃
+ if (row.businessType === 6) {
+ const quotationNo = row?.quotationNo;
+ if (quotationNo) {
+ const res = await getQuotationList({ quotationNo });
+ const records = res?.data?.records || [];
+ detailData.value = records[0] || {};
+ }
+ }
+
+ // 閲囪喘瀹℃壒锛氱敤瀹℃壒浜嬬敱瀛楁鎵胯浇鐨�"閲囪喘鍚堝悓鍙�"鍘绘煡閲囪喘璇︽儏
+ else if (row.businessType === 5) {
+ const purchaseContractNumber = row?.purchaseContractNumber;
+ if (purchaseContractNumber) {
+ const res = await getPurchaseByCode({ purchaseContractNumber });
+ detailData.value = res || {};
+ }
+ }
+
+ // 鍙戣揣瀹℃壒锛氱敤瀹℃壒浜嬬敱瀛楁鎵胯浇鐨�"鍙戣揣鍗曞彿"鍘绘煡鍙戣揣璇︽儏
+ else if (row.businessType === 7) {
+ const shippingNo = row?.shippingNo;
+ if (shippingNo) {
+ const res = await getDeliveryDetailByShippingNo({ shippingNo });
+ detailData.value = res?.data || res || {};
+ }
+ }
+
+ // 鍏朵粬瀹℃壒绫诲瀷
+ else {
+ detailData.value = {};
+ }
approveDialog.visible = true;
}
@@ -586,6 +659,7 @@
tableColumn,
detailDialog,
detailRow,
+ detailData,
reimburseDialog,
approveDialog,
approveOpinion,
@@ -625,4 +699,4 @@
openApprove,
fetchApprovalList,
};
-}
+}
\ No newline at end of file
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-shared/useApprovalInstanceModule.js b/src/views/officeProcessAutomation/ApproveManage/approve-shared/useApprovalInstanceModule.js
index b474bb2..6793523 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-shared/useApprovalInstanceModule.js
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-shared/useApprovalInstanceModule.js
@@ -357,11 +357,11 @@
disabled: (row) => !canEditBusinessInstanceRow(row),
clickFun: (row) => openEdit(row),
},
- {
- name: "鍒犻櫎",
- type: "danger",
- clickFun: (row) => removeInstance(row),
- },
+ // {
+ // name: "鍒犻櫎",
+ // type: "danger",
+ // clickFun: (row) => removeInstance(row),
+ // },
...extraOperations,
];
}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue b/src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue
index d094c13..0a495c6 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue
@@ -380,23 +380,18 @@
<template v-if="detailAttachments.length">
- <el-tag
-
- v-for="(f, i) in detailAttachments"
-
- :key="i"
-
- class="detail-attachment-tag"
-
- type="info"
-
- effect="plain"
-
- >
-
- {{ attachmentDisplayName(f) }}
-
- </el-tag>
+ <div class="detail-attachment-list">
+ <div
+ v-for="(f, i) in detailAttachments"
+ :key="i"
+ class="detail-attachment-item"
+ @click="openAttachmentFile(f)"
+ >
+ <el-icon class="attachment-icon"><Document /></el-icon>
+ <span class="attachment-name">{{ attachmentDisplayName(f) }}</span>
+ <el-icon class="attachment-download"><Download /></el-icon>
+ </div>
+ </div>
</template>
@@ -422,7 +417,7 @@
<script setup>
-import { ArrowRight, Plus, RefreshRight } from "@element-plus/icons-vue";
+import { ArrowRight, Document, Download, Plus, RefreshRight } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
@@ -525,6 +520,14 @@
}
+function openAttachmentFile(file) {
+ const url = file?.url || file?.previewURL || file?.downloadURL || file?.previewUrl || "";
+ if (url) {
+ window.open(url, "_blank");
+ } else {
+ ElMessage.warning("鏃犳硶鎵撳紑璇ラ檮浠�");
+ }
+}
function unwrapArray(payload) {
@@ -795,10 +798,40 @@
}
-.detail-attachment-tag {
+.detail-attachment-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
- margin: 0 8px 8px 0;
+.detail-attachment-item {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px 12px;
+ background: var(--el-fill-color-light);
+ border-radius: 6px;
+ cursor: pointer;
+ transition: background 0.2s;
+}
+.detail-attachment-item:hover {
+ background: var(--el-fill-color);
+}
+
+.attachment-icon {
+ font-size: 16px;
+ color: var(--el-text-color-regular);
+}
+
+.attachment-name {
+ font-size: 14px;
+ color: var(--el-text-color-primary);
+}
+
+.attachment-download {
+ font-size: 14px;
+ color: var(--el-text-color-secondary);
}
.text-muted {
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js b/src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js
index 61aa6c0..79e96bc 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js
@@ -214,10 +214,21 @@
});
}
- function openFormDialog(mode, row) {
+ async function openFormDialog(mode, row) {
formDialog.mode = mode;
formDialog.title = mode === "add" ? "鏂板缓瀹℃壒妯℃澘" : "缂栬緫瀹℃壒妯℃澘";
- resetForm(mode === "edit" ? row : null);
+ if (mode === "edit" && row?.id) {
+ // 缂栬緫鏃跺厛鏌ヨ璇︽儏鑾峰彇瀹屾暣鏁版嵁锛堝寘鍚檮浠讹級
+ try {
+ const res = await getApprovalTemplateDetail(row.id);
+ const detailData = mapTemplateFromApi(unwrapTemplateDetail(res));
+ resetForm(detailData);
+ } catch {
+ resetForm(row);
+ }
+ } else {
+ resetForm(mode === "edit" ? row : null);
+ }
formDialog.visible = true;
}
diff --git a/src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue b/src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue
index 9b3d91e..3c381dc 100644
--- a/src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue
+++ b/src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue
@@ -24,7 +24,7 @@
<el-button @click="resetSearch">閲嶇疆</el-button>
</div>
<div class="search_actions">
- <el-button type="warning" plain @click="handleExport">瀵煎嚭</el-button>
+<!-- <el-button type="warning" plain @click="handleExport">瀵煎嚭</el-button>-->
<el-button type="primary" @click="openAddWithTemplate">鏂板鍔犵彮鐢宠</el-button>
</div>
</div>
diff --git a/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue b/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue
index c9da4fc..661c990 100644
--- a/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue
+++ b/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue
@@ -16,8 +16,8 @@
<el-button @click="resetSearch">閲嶇疆</el-button>
</div>
<div class="search_actions">
- <el-button type="success" plain @click="handleImportClick">瀵煎叆</el-button>
- <el-button type="warning" plain @click="handleExport">瀵煎嚭</el-button>
+<!-- <el-button type="success" plain @click="handleImportClick">瀵煎叆</el-button>-->
+<!-- <el-button type="warning" plain @click="handleExport">瀵煎嚭</el-button>-->
<el-button type="primary" @click="openFormDialog('add')">鏂板璐圭敤鎶ラ攢</el-button>
</div>
</div>
diff --git a/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/components/DetailPanel.vue b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/components/DetailPanel.vue
index 2c1d8a4..df8cf8e 100644
--- a/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/components/DetailPanel.vue
+++ b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/components/DetailPanel.vue
@@ -33,7 +33,7 @@
<el-table-column type="index" label="搴忓彿" width="55" align="center" />
<el-table-column prop="invoiceDate" label="鍙戠エ鏃ユ湡" width="120" />
<el-table-column label="璐圭敤绉戠洰" width="100">
- <template #default="{ row: d }">{{ expenseSubjectLabel(d.expenseSubject) }}</template>
+ <template #default="{ row: d }">{{ d.expenseCategory || "鈥�" }}</template>
</el-table-column>
<el-table-column prop="amount" label="閲戦" width="100" />
<el-table-column prop="description" label="鎻忚堪" min-width="140" show-overflow-tooltip />
@@ -51,7 +51,7 @@
<script setup>
import { computed } from "vue";
-import { expenseSubjectLabel, statusLabel, statusTagType } from "../travelReimburseUtils.js";
+import { statusLabel, statusTagType } from "../travelReimburseUtils.js";
const props = defineProps({
row: { type: Object, default: () => ({}) },
diff --git a/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue
index 17737e3..e2b5977 100644
--- a/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue
+++ b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue
@@ -16,8 +16,8 @@
<el-button @click="resetSearch">閲嶇疆</el-button>
</div>
<div class="search_actions">
- <el-button type="success" plain @click="handleImportClick">瀵煎叆</el-button>
- <el-button type="warning" plain @click="handleExport">瀵煎嚭</el-button>
+<!-- <el-button type="success" plain @click="handleImportClick">瀵煎叆</el-button>-->
+<!-- <el-button type="warning" plain @click="handleExport">瀵煎嚭</el-button>-->
<el-button type="primary" @click="openFormDialog('add')">鏂板宸梾鎶ラ攢</el-button>
</div>
</div>
diff --git a/src/views/personnelManagement/attendanceCheckin/checkinRules/components/form.vue b/src/views/personnelManagement/attendanceCheckin/checkinRules/components/form.vue
index b17b234..cbbb8af 100644
--- a/src/views/personnelManagement/attendanceCheckin/checkinRules/components/form.vue
+++ b/src/views/personnelManagement/attendanceCheckin/checkinRules/components/form.vue
@@ -186,6 +186,11 @@
{ required: true, message: "璇烽�夋嫨涓嬬彮鏃堕棿", trigger: "change" },
{
validator: (rule, value, callback) => {
+ // 濡傛灉鏄鐝紝涓嶉檺鍒舵椂闂撮『搴�
+ if (form.shift && ['澶滅彮', '澶�'].includes(form.shift)) {
+ callback();
+ return;
+ }
if (form.startAt && value) {
const startParts = form.startAt.split(":");
const endParts = value.split(":");
diff --git a/src/views/personnelManagement/employeeRecord/index.vue b/src/views/personnelManagement/employeeRecord/index.vue
index a0699b0..b8496e3 100644
--- a/src/views/personnelManagement/employeeRecord/index.vue
+++ b/src/views/personnelManagement/employeeRecord/index.vue
@@ -145,6 +145,10 @@
prop: "staffNo",
},
{
+ label: "閮ㄩ棬",
+ prop: "deptName",
+ },
+ {
label: "濮撳悕",
prop: "staffName",
},
diff --git a/src/views/personnelManagement/monthlyStatistics/components/auditDia.vue b/src/views/personnelManagement/monthlyStatistics/components/auditDia.vue
index 48a41f0..e615402 100644
--- a/src/views/personnelManagement/monthlyStatistics/components/auditDia.vue
+++ b/src/views/personnelManagement/monthlyStatistics/components/auditDia.vue
@@ -86,7 +86,7 @@
<script setup>
import { ref, computed, reactive, toRefs, getCurrentInstance, watch } from "vue";
import { ElMessage } from "element-plus";
-import Cookies from "js-cookie";
+import useUserStore from "@/store/modules/user";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import { staffSalaryMainUpdate } from "@/api/personnelManagement/staffSalaryMain.js";
@@ -98,6 +98,7 @@
});
const { proxy } = getCurrentInstance();
+const userStore = useUserStore();
const dialogVisible = computed({
get: () => props.modelValue,
@@ -176,15 +177,21 @@
return;
}
- const username = Cookies.get("username") || "";
- const userIdRaw = Cookies.get("userId");
- const auditUserId = userIdRaw ? Number(userIdRaw) : undefined;
+ const username = userStore.name || "";
+ const currentUserId = Number(userStore.id);
+ const assignedAuditUserId = Number(row?.auditUserId);
+
+ // 鏉冮檺楠岃瘉锛氬彧鏈夋寚瀹氱殑瀹℃牳浜烘墠鑳借繘琛屽鏍�
+ if (!currentUserId || currentUserId !== assignedAuditUserId) {
+ ElMessage.warning("鎮ㄤ笉鏄寚瀹氱殑瀹℃牳浜猴紝鏃犳硶杩涜瀹℃牳鎿嶄綔");
+ return;
+ }
// 鏋勫缓瀹℃牳鏁版嵁
const submitData = {
id: row.id,
status: Number(auditResult.value) === 2 ? 2 : 4, // 2=涓嶉�氳繃 4=閫氳繃(寰呭彂鏀�)
- auditUserId,
+ auditUserId: currentUserId,
auditUserName: username,
};
loading.value = true;
diff --git a/src/views/personnelManagement/monthlyStatistics/components/formDia.vue b/src/views/personnelManagement/monthlyStatistics/components/formDia.vue
index 36c2ec3..46515ed 100644
--- a/src/views/personnelManagement/monthlyStatistics/components/formDia.vue
+++ b/src/views/personnelManagement/monthlyStatistics/components/formDia.vue
@@ -1,505 +1,520 @@
<template>
- <FormDialog
- v-model="dialogVisible"
- :title="operationType === 'add' ? '鏂板缓宸ヨ祫琛�' : '缂栬緫宸ヨ祫琛�'"
- width="90%"
- @close="closeDia"
- >
+ <FormDialog v-model="dialogVisible"
+ :title="operationType === 'add' ? '鏂板缓宸ヨ祫琛�' : '缂栬緫宸ヨ祫琛�'"
+ width="90%"
+ @close="closeDia">
<template #footer>
- <el-button type="info" @click="saveDraft">淇濆瓨鑽夌</el-button>
- <el-button type="primary" @click="submitForm">纭鎻愪氦</el-button>
+ <el-button type="info"
+ @click="saveDraft">淇濆瓨鑽夌</el-button>
+ <el-button type="primary"
+ @click="submitForm">纭鎻愪氦</el-button>
<el-button @click="closeDia">鍙栨秷</el-button>
</template>
<div class="form-dia-body">
<!-- 鍩虹璧勬枡 -->
- <el-card class="form-card" shadow="never">
+ <el-card class="form-card"
+ shadow="never">
<template #header>
<span class="card-title"><span class="card-title-line">|</span> 鍩虹璧勬枡</span>
- <el-icon class="card-collapse"><ArrowUp /></el-icon>
+ <el-icon class="card-collapse">
+ <ArrowUp />
+ </el-icon>
</template>
- <el-form ref="formRef" :model="form" :rules="rules" label-position="top">
+ <el-form ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-position="top">
<el-row :gutter="24">
<el-col :span="6">
- <el-form-item label="宸ヨ祫涓婚" prop="salaryTitle">
- <el-input
- v-model="form.salaryTitle"
- placeholder="璇疯緭鍏�"
- clearable
- maxlength="20"
- show-word-limit
- />
+ <el-form-item label="宸ヨ祫涓婚"
+ prop="salaryTitle">
+ <el-input v-model="form.salaryTitle"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="20"
+ show-word-limit />
</el-form-item>
</el-col>
<el-col :span="6">
- <el-form-item label="閫夋嫨閮ㄩ棬" prop="deptIds">
- <el-select
- v-model="form.deptIds"
- placeholder="璇烽�夋嫨"
- clearable
- multiple
- collapse-tags-tooltip
- style="width: 100%"
- >
- <el-option
- v-for="item in deptOptions"
- :key="item.deptId"
- :label="item.deptName"
- :value="item.deptId"
- />
+ <el-form-item label="閫夋嫨宸ヨ祫鏈堜唤"
+ prop="salaryMonth">
+ <el-date-picker v-model="form.salaryMonth"
+ type="month"
+ value-format="YYYY-MM"
+ format="YYYY-MM"
+ placeholder="璇烽�夋嫨宸ヨ祫鏈堜唤"
+ style="width: 100%"
+ clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="閫夋嫨閮ㄩ棬"
+ prop="deptIds">
+ <el-select v-model="form.deptIds"
+ placeholder="璇烽�夋嫨"
+ clearable
+ multiple
+ collapse-tags-tooltip
+ style="width: 100%">
+ <el-option v-for="item in deptOptions"
+ :key="item.deptId"
+ :label="item.deptName"
+ :value="item.deptId"
+ :disabled="isDeptDisabled(item.deptId)" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
- <el-form-item label="閫夋嫨宸ヨ祫鏈堜唤" prop="salaryMonth">
- <el-date-picker
- v-model="form.salaryMonth"
- type="month"
- value-format="YYYY-MM"
- format="YYYY-MM"
- placeholder="璇烽�夋嫨宸ヨ祫鏈堜唤"
- style="width: 100%"
- clearable
- />
- </el-form-item>
- </el-col>
- <el-col :span="6">
- <el-form-item label="澶囨敞" prop="remark">
- <el-input
- v-model="form.remark"
- placeholder="璇疯緭鍏�"
- clearable
- />
+ <el-form-item label="澶囨敞"
+ prop="remark">
+ <el-input v-model="form.remark"
+ placeholder="璇疯緭鍏�"
+ clearable />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="6">
- <el-form-item label="鏀粯閾惰" prop="payBank">
- <el-select
- v-model="form.payBank"
- placeholder="璇烽�夋嫨"
- clearable
- filterable
- style="width: 100%"
- >
- <el-option
- v-for="b in bankOptions"
- :key="b"
- :label="b"
- :value="b"
- />
+ <el-form-item label="鏀粯閾惰"
+ prop="payBank">
+ <el-select v-model="form.payBank"
+ placeholder="璇烽�夋嫨"
+ clearable
+ filterable
+ style="width: 100%">
+ <el-option v-for="b in bankOptions"
+ :key="b"
+ :label="b"
+ :value="b" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
- <el-form-item label="瀹℃牳浜�" prop="auditUserId">
- <el-select
- v-model="form.auditUserId"
- placeholder="璇烽�夋嫨瀹℃牳浜�"
- clearable
- filterable
- style="width: 100%"
- >
- <el-option
- v-for="item in userList"
- :key="item.userId"
- :label="item.nickName"
- :value="item.userId"
- />
+ <el-form-item label="瀹℃牳浜�"
+ prop="auditUserId">
+ <el-select v-model="form.auditUserId"
+ placeholder="璇烽�夋嫨瀹℃牳浜�"
+ clearable
+ filterable
+ style="width: 100%">
+ <el-option v-for="item in userList"
+ :key="item.userId"
+ :label="item.nickName"
+ :value="item.userId" />
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
-
<!-- 鎿嶄綔鎸夐挳 -->
<div class="toolbar">
- <el-button type="primary" @click="handleGenerate">鐢熸垚宸ヨ祫琛�</el-button>
+ <el-button type="primary"
+ @click="handleGenerate">鐢熸垚宸ヨ祫琛�</el-button>
<el-button @click="handleClear">娓呯┖</el-button>
<el-button @click="handleBatchDelete">鍒犻櫎</el-button>
<el-button @click="handleTaxForm">涓◣琛�</el-button>
</div>
-
<!-- 鍛樺伐宸ヨ祫璇︽儏琛ㄦ牸 -->
<div class="employee-table-wrap">
- <el-table
- ref="employeeTableRef"
- :data="employeeList"
- border
- max-height="400"
- @selection-change="onEmployeeSelectionChange"
- >
- <el-table-column type="selection" width="55" align="center" />
- <el-table-column label="鍛樺伐濮撳悕" prop="staffName" minWidth="100" />
- <el-table-column label="閮ㄩ棬" prop="deptName" minWidth="100" />
- <el-table-column label="鍩烘湰宸ヨ祫" minWidth="110">
+ <el-table ref="employeeTableRef"
+ :data="employeeList"
+ border
+ max-height="400"
+ @selection-change="onEmployeeSelectionChange">
+ <el-table-column type="selection"
+ width="55"
+ align="center" />
+ <el-table-column label="鍛樺伐濮撳悕"
+ prop="staffName"
+ minWidth="100" />
+ <el-table-column label="閮ㄩ棬"
+ prop="deptName"
+ minWidth="100" />
+ <el-table-column label="鍩烘湰宸ヨ祫"
+ minWidth="110">
<template #default="{ row }">
- <el-input
- v-model.number="row.basicSalary"
- type="number"
- placeholder="0"
- size="small"
- @input="row.basicSalary = parseNum(row.basicSalary)"
- />
+ <el-input v-model.number="row.basicSalary"
+ type="number"
+ placeholder="0"
+ size="small"
+ @input="handleSalaryInput(row)" />
</template>
</el-table-column>
- <el-table-column label="璁′欢宸ヨ祫" minWidth="110">
+ <el-table-column label="璁′欢宸ヨ祫"
+ minWidth="110">
<template #default="{ row }">
- <el-input
- v-model.number="row.pieceSalary"
- type="number"
- placeholder="0"
- size="small"
- @input="row.pieceSalary = parseNum(row.pieceSalary)"
- />
+ <el-input v-model.number="row.pieceSalary"
+ type="number"
+ placeholder="0"
+ size="small"
+ disabled
+ @input="row.pieceSalary = parseNum(row.pieceSalary)" />
</template>
</el-table-column>
- <el-table-column label="璁℃椂宸ヨ祫" minWidth="110">
+ <el-table-column label="璁℃椂宸ヨ祫"
+ minWidth="110">
<template #default="{ row }">
- <el-input
- v-model.number="row.hourlySalary"
- type="number"
- placeholder="0"
- size="small"
- @input="row.hourlySalary = parseNum(row.hourlySalary)"
- />
+ <el-input v-model.number="row.hourlySalary"
+ type="number"
+ placeholder="0"
+ size="small"
+ disabled
+ @input="row.hourlySalary = parseNum(row.hourlySalary)" />
</template>
</el-table-column>
- <el-table-column label="鍏朵粬鏀跺叆" minWidth="110">
+ <el-table-column label="鍏朵粬鏀跺叆"
+ minWidth="110">
<template #default="{ row }">
- <el-input
- v-model.number="row.otherIncome"
- type="number"
- placeholder="0"
- size="small"
- @input="row.otherIncome = parseNum(row.otherIncome)"
- />
+ <el-input v-model.number="row.otherIncome"
+ type="number"
+ placeholder="0"
+ size="small"
+ @input="handleSalaryInput(row)" />
</template>
</el-table-column>
- <el-table-column label="绀句繚涓汉" minWidth="110">
+ <el-table-column label="绀句繚涓汉"
+ minWidth="110">
<template #default="{ row }">
- <el-input
- v-model.number="row.socialPersonal"
- type="number"
- placeholder="0"
- size="small"
- @input="row.socialPersonal = parseNum(row.socialPersonal)"
- />
+ <el-input v-model.number="row.socialPersonal"
+ type="number"
+ placeholder="0"
+ size="small"
+ disabled
+ @input="row.socialPersonal = parseNum(row.socialPersonal)" />
</template>
</el-table-column>
- <el-table-column label="鍏Н閲戜釜浜�" minWidth="120">
+ <el-table-column label="鍏Н閲戜釜浜�"
+ minWidth="120">
<template #default="{ row }">
- <el-input
- v-model.number="row.fundPersonal"
- type="number"
- placeholder="0"
- size="small"
- @input="row.fundPersonal = parseNum(row.fundPersonal)"
- />
+ <el-input v-model.number="row.fundPersonal"
+ type="number"
+ placeholder="0"
+ size="small"
+ disabled
+ @input="row.fundPersonal = parseNum(row.fundPersonal)" />
</template>
</el-table-column>
- <el-table-column label="鍏朵粬鏀嚭" minWidth="110">
+ <el-table-column label="鍏朵粬鏀嚭"
+ minWidth="110">
<template #default="{ row }">
- <el-input
- v-model.number="row.otherDeduct"
- type="number"
- placeholder="0"
- size="small"
- @input="row.otherDeduct = parseNum(row.otherDeduct)"
- />
+ <el-input v-model.number="row.otherDeduct"
+ type="number"
+ placeholder="0"
+ size="small"
+ disabled
+ @input="row.otherDeduct = parseNum(row.otherDeduct)" />
</template>
</el-table-column>
- <el-table-column label="宸ヨ祫涓◣" minWidth="110">
+ <el-table-column label="绀句繚琛ョ即"
+ minWidth="120">
<template #default="{ row }">
- <el-input
- v-model.number="row.salaryTax"
- type="number"
- placeholder="0"
- size="small"
- @input="row.salaryTax = parseNum(row.salaryTax)"
- />
+ <el-input v-model.number="row.socialSupplementAmount"
+ type="number"
+ placeholder="0"
+ size="small"
+ @input="handleSalaryInput(row)" />
</template>
</el-table-column>
- <el-table-column label="搴斿彂宸ヨ祫" minWidth="110">
+ <el-table-column label="宸ヨ祫涓◣"
+ minWidth="110">
<template #default="{ row }">
- <el-input
- v-model.number="row.grossSalary"
- type="number"
- placeholder="0"
- size="small"
- @input="row.grossSalary = parseNum(row.grossSalary)"
- />
+ <el-input v-model.number="row.salaryTax"
+ type="number"
+ placeholder="0"
+ size="small"
+ disabled
+ @input="row.salaryTax = parseNum(row.salaryTax)" />
</template>
</el-table-column>
- <el-table-column label="搴旀墸宸ヨ祫" minWidth="110">
+ <el-table-column label="搴斿彂宸ヨ祫"
+ minWidth="110">
<template #default="{ row }">
- <el-input
- v-model.number="row.deductSalary"
- type="number"
- placeholder="0"
- size="small"
- @input="row.deductSalary = parseNum(row.deductSalary)"
- />
+ <el-input v-model.number="row.grossSalary"
+ type="number"
+ placeholder="0"
+ size="small"
+ disabled
+ @input="row.grossSalary = parseNum(row.grossSalary)" />
</template>
</el-table-column>
- <el-table-column label="瀹炲彂宸ヨ祫" minWidth="110">
+ <el-table-column label="搴旀墸宸ヨ祫"
+ minWidth="110">
<template #default="{ row }">
- <el-input
- v-model.number="row.netSalary"
- type="number"
- placeholder="0"
- size="small"
- @input="row.netSalary = parseNum(row.netSalary)"
- />
+ <el-input v-model.number="row.deductSalary"
+ type="number"
+ placeholder="0"
+ size="small"
+ disabled
+ @input="row.deductSalary = parseNum(row.deductSalary)" />
</template>
</el-table-column>
- <el-table-column label="澶囨敞" minWidth="120">
+ <el-table-column label="瀹炲彂宸ヨ祫"
+ minWidth="110">
<template #default="{ row }">
- <el-input
- v-model="row.remark"
- placeholder="璇疯緭鍏�"
- size="small"
- />
+ <el-input v-model.number="row.netSalary"
+ type="number"
+ placeholder="0"
+ size="small"
+ disabled
+ @input="row.netSalary = parseNum(row.netSalary)" />
</template>
</el-table-column>
- <el-table-column label="鎿嶄綔" width="80" align="center" fixed="right">
+ <el-table-column label="澶囨敞"
+ minWidth="120">
<template #default="{ row }">
- <el-button type="primary" link @click="removeEmployee(row)">鍒犻櫎</el-button>
+ <el-input v-model="row.remark"
+ placeholder="璇疯緭鍏�"
+ size="small" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔"
+ width="80"
+ align="center"
+ fixed="right">
+ <template #default="{ row }">
+ <el-button type="primary"
+ link
+ @click="removeEmployee(row)">鍒犻櫎</el-button>
</template>
</el-table-column>
</el-table>
- <div v-if="!employeeList.length" class="table-empty">鏆傛棤鏁版嵁</div>
- <div v-else class="salary-total">
+ <div v-if="!employeeList.length"
+ class="table-empty">鏆傛棤鏁版嵁</div>
+ <div v-else
+ class="salary-total">
<span class="total-label">宸ヨ祫鎬婚锛�</span>
<span class="total-value">楼 {{ totalSalary.toFixed(2) }}</span>
</div>
</div>
</div>
-
-
-
<!-- 鏂板浜哄憳寮圭獥 -->
- <el-dialog
- v-model="addPersonVisible"
- title="鏂板浜哄憳"
- width="400px"
- append-to-body
- @close="addPersonClose"
- >
+ <el-dialog v-model="addPersonVisible"
+ title="鏂板浜哄憳"
+ width="400px"
+ append-to-body
+ @close="addPersonClose">
<div class="add-person-tree">
- <el-tree
- ref="personTreeRef"
- :data="deptStaffTree"
- show-checkbox
- node-key="id"
- :props="{ label: 'label', children: 'children' }"
- default-expand-all
- />
+ <el-tree ref="personTreeRef"
+ :data="deptStaffTree"
+ show-checkbox
+ node-key="id"
+ :props="{ label: 'label', children: 'children' }"
+ default-expand-all />
</div>
<template #footer>
<el-button @click="addPersonVisible = false">鍙栨秷</el-button>
- <el-button type="primary" @click="confirmAddPerson">纭畾</el-button>
+ <el-button type="primary"
+ @click="confirmAddPerson">纭畾</el-button>
</template>
</el-dialog>
-
<!-- 涓◣琛ㄥ脊绐� -->
- <el-dialog
- v-model="taxDialogVisible"
- title="涓◣琛�"
- width="700px"
- append-to-body
- >
+ <el-dialog v-model="taxDialogVisible"
+ title="涓◣琛�"
+ width="700px"
+ append-to-body>
<div class="tax-desc">涓汉鎵�寰楃◣鍏嶅緛棰濓細5000鍏�</div>
- <el-table :data="taxTableData" border style="width: 100%;margin-bottom: 20px;">
- <el-table-column prop="level" label="绾ф暟" width="80" align="center" />
- <el-table-column
- prop="range"
- label="鍏ㄥ勾搴旂撼绋庢墍寰楅/鍏�"
- min-width="220"
- />
- <el-table-column
- prop="rate"
- label="绋庣巼(%)"
- width="100"
- align="center"
- />
- <el-table-column
- prop="quickDeduction"
- label="閫熺畻鎵i櫎鏁�/鍏�"
- width="160"
- align="center"
- />
+ <el-table :data="taxTableData"
+ border
+ style="width: 100%;margin-bottom: 20px;">
+ <el-table-column prop="level"
+ label="绾ф暟"
+ width="80"
+ align="center" />
+ <el-table-column prop="range"
+ label="鍏ㄥ勾搴旂撼绋庢墍寰楅/鍏�"
+ min-width="220" />
+ <el-table-column prop="rate"
+ label="绋庣巼(%)"
+ width="100"
+ align="center" />
+ <el-table-column prop="quickDeduction"
+ label="閫熺畻鎵i櫎鏁�/鍏�"
+ width="160"
+ align="center" />
</el-table>
</el-dialog>
</FormDialog>
</template>
<script setup>
-import { ref, reactive, toRefs, computed, getCurrentInstance, nextTick } from "vue";
-import { ArrowUp } from "@element-plus/icons-vue";
-import FormDialog from "@/components/Dialog/FormDialog.vue";
-import { listDept } from "@/api/system/dept.js";
-import { staffOnJobList } from "@/api/personnelManagement/monthlyStatistics.js";
+ import {
+ ref,
+ reactive,
+ toRefs,
+ computed,
+ getCurrentInstance,
+ nextTick,
+ watch,
+ } from "vue";
+ import { ArrowUp } from "@element-plus/icons-vue";
+ import FormDialog from "@/components/Dialog/FormDialog.vue";
+ import { listDept } from "@/api/system/dept.js";
+ import { staffOnJobList } from "@/api/personnelManagement/monthlyStatistics.js";
import { bankList } from "@/api/personnelManagement/bank.js";
import {
staffSalaryMainAdd,
staffSalaryMainUpdate,
staffSalaryMainCalculateSalary,
+ staffSalaryMainCalculateByEmployeeId,
+ staffSalaryMainAvailableDepts,
} from "@/api/personnelManagement/staffSalaryMain.js";
-import { userListNoPageByTenantId } from "@/api/system/user.js";
+ import { userListNoPageByTenantId } from "@/api/system/user.js";
+ const emit = defineEmits(["update:modelValue", "close"]);
+ const props = defineProps({
+ modelValue: { type: Boolean, default: false },
+ operationType: { type: String, default: "add" },
+ row: { type: Object, default: () => ({}) },
+ });
-const emit = defineEmits(["update:modelValue", "close"]);
-const props = defineProps({
- modelValue: { type: Boolean, default: false },
- operationType: { type: String, default: "add" },
- row: { type: Object, default: () => ({}) },
-});
+ const { proxy } = getCurrentInstance();
-const { proxy } = getCurrentInstance();
+ const dialogVisible = computed({
+ get: () => props.modelValue,
+ set: val => emit("update:modelValue", val),
+ });
-const dialogVisible = computed({
- get: () => props.modelValue,
- set: (val) => emit("update:modelValue", val),
-});
+ const formRef = ref(null);
+ const employeeTableRef = ref(null);
+ const personTreeRef = ref(null);
+ const addPersonVisible = ref(false);
+ const taxDialogVisible = ref(false);
+ const deptOptions = ref([]);
+ const availableDeptIds = ref(new Set());
+ const deptStaffTree = ref([]);
+ const employeeList = ref([]);
+ const selectedEmployees = ref([]);
+ const bankOptions = ref([]);
+ const userList = ref([]);
+ const recalcTimers = new Map();
+ const recalcVersions = new Map();
+ const taxTableData = ref([
+ { level: 1, range: "涓嶈秴杩�36000鍏�", rate: 3, quickDeduction: 0 },
+ { level: 2, range: "瓒呰繃36000-144000鍏�", rate: 10, quickDeduction: 2520 },
+ { level: 3, range: "瓒呰繃144000-300000鍏�", rate: 20, quickDeduction: 16920 },
+ { level: 4, range: "瓒呰繃300000-420000鍏�", rate: 25, quickDeduction: 31920 },
+ { level: 5, range: "瓒呰繃420000-660000鍏�", rate: 30, quickDeduction: 52920 },
+ { level: 6, range: "瓒呰繃660000-960000鍏�", rate: 35, quickDeduction: 85920 },
+ { level: 7, range: "瓒呰繃960000鍏�", rate: 45, quickDeduction: 181920 },
+ ]);
-const formRef = ref(null);
-const employeeTableRef = ref(null);
-const personTreeRef = ref(null);
-const addPersonVisible = ref(false);
-const taxDialogVisible = ref(false);
-const deptOptions = ref([]);
-const deptStaffTree = ref([]);
-const employeeList = ref([]);
-const selectedEmployees = ref([]);
-const bankOptions = ref([]);
-const userList = ref([]);
-const taxTableData = ref([
- { level: 1, range: "涓嶈秴杩�36000鍏�", rate: 3, quickDeduction: 0 },
- { level: 2, range: "瓒呰繃36000-144000鍏�", rate: 10, quickDeduction: 2520 },
- { level: 3, range: "瓒呰繃144000-300000鍏�", rate: 20, quickDeduction: 16920 },
- { level: 4, range: "瓒呰繃300000-420000鍏�", rate: 25, quickDeduction: 31920 },
- { level: 5, range: "瓒呰繃420000-660000鍏�", rate: 30, quickDeduction: 52920 },
- { level: 6, range: "瓒呰繃660000-960000鍏�", rate: 35, quickDeduction: 85920 },
- { level: 7, range: "瓒呰繃960000鍏�", rate: 45, quickDeduction: 181920 },
-]);
+ function parseNum(v) {
+ if (v === "" || v == null) return 0;
+ const n = Number(v);
+ return isNaN(n) ? 0 : n;
+ }
-function parseNum(v) {
- if (v === "" || v == null) return 0;
- const n = Number(v);
- return isNaN(n) ? 0 : n;
-}
+ function parseNullableNum(v) {
+ if (v === "" || v == null) return null;
+ const n = Number(v);
+ return isNaN(n) ? null : n;
+ }
-// 鍩虹璧勬枡琛ㄥ崟
-const data = reactive({
- form: {
+ const createEmployeeRow = () => ({
+ staffOnJobId: undefined,
id: undefined,
- salaryTitle: "",
- deptIds: [],
- salaryMonth: "",
+ staffName: "",
+ postName: "",
+ deptName: "",
+ basicSalary: 0,
+ pieceSalary: 0,
+ hourlySalary: 0,
+ otherIncome: 0,
+ socialPersonal: 0,
+ fundPersonal: 0,
+ otherDeduct: 0,
+ socialSupplementAmount: null,
+ salaryTax: 0,
+ grossSalary: 0,
+ deductSalary: 0,
+ netSalary: 0,
remark: "",
- payBank: "",
- auditUserId: undefined,
- },
- rules: {
- salaryTitle: [{ required: true, message: "璇疯緭鍏ュ伐璧勪富棰�", trigger: "blur" }],
- deptIds: [{ required: true, message: "璇烽�夋嫨閮ㄩ棬", trigger: "change" }],
- salaryMonth: [{ required: true, message: "璇烽�夋嫨宸ヨ祫鏈堜唤", trigger: "change" }],
- auditUserId: [{ required: true, message: "璇烽�夋嫨瀹℃牳浜�", trigger: "change" }],
- },
-});
-const { form, rules } = toRefs(data);
-
-// 璁$畻宸ヨ祫鎬婚锛堟墍鏈夊憳宸ュ疄鍙戝伐璧勪箣鍜岋級
-const totalSalary = computed(() => {
- return employeeList.value.reduce((sum, e) => sum + parseNum(e.netSalary), 0);
-});
-
-// 鏍规嵁瀹℃牳浜篒D鑾峰彇瀹℃牳浜哄悕绉�
-const auditUserName = computed(() => {
- if (!form.value.auditUserId) return "";
- const user = userList.value.find(u => u.userId === form.value.auditUserId);
- return user ? user.nickName : "";
-});
-
-const loadBankOptions = () => {
- return bankList().then((res) => {
- const list = Array.isArray(res?.data) ? res.data : [];
- bankOptions.value = list
- .map((b) => (b?.bankName == null ? "" : String(b.bankName).trim()))
- .filter((v) => v !== "");
});
-};
-const loadUserList = () => {
- return userListNoPageByTenantId().then((res) => {
- userList.value = res.data || [];
+ const normalizeEmployeeRow = (source, fallback = {}) => ({
+ ...createEmployeeRow(),
+ ...fallback,
+ ...source,
+ staffOnJobId: source?.staffOnJobId ?? source?.staffId ?? source?.id ?? fallback.staffOnJobId ?? fallback.id,
+ id: source?.staffOnJobId ?? source?.staffId ?? source?.id ?? fallback.staffOnJobId ?? fallback.id,
+ staffName: source?.staffName ?? fallback.staffName ?? "",
+ postName: source?.postName ?? fallback.postName ?? "",
+ deptName: source?.deptName ?? fallback.deptName ?? "",
+ basicSalary: parseNum(source?.basicSalary ?? fallback.basicSalary),
+ pieceSalary: parseNum(source?.pieceSalary ?? fallback.pieceSalary),
+ hourlySalary: parseNum(source?.hourlySalary ?? fallback.hourlySalary),
+ otherIncome: parseNum(source?.otherIncome ?? fallback.otherIncome),
+ socialPersonal: parseNum(source?.socialPersonal ?? fallback.socialPersonal),
+ fundPersonal: parseNum(source?.fundPersonal ?? fallback.fundPersonal),
+ otherDeduct: parseNum(source?.otherDeduct ?? fallback.otherDeduct),
+ socialSupplementAmount:
+ source?.socialSupplementAmount ?? fallback.socialSupplementAmount ?? null,
+ salaryTax: parseNum(source?.salaryTax ?? fallback.salaryTax),
+ grossSalary: parseNum(source?.grossSalary ?? fallback.grossSalary),
+ deductSalary: parseNum(source?.deductSalary ?? fallback.deductSalary),
+ netSalary: parseNum(source?.netSalary ?? fallback.netSalary),
+ remark: source?.remark ?? fallback.remark ?? "",
});
-};
-// 鎵佸钩鍖栭儴闂ㄦ爲渚涗笅鎷変娇鐢�
-function flattenDept(tree, list = []) {
- if (!tree?.length) return list;
- tree.forEach((node) => {
- list.push({ deptId: node.deptId, deptName: node.deptName });
- if (node.children?.length) flattenDept(node.children, list);
- });
- return list;
-}
+ const applyBackendRow = (targetRow, sourceRow) => {
+ Object.assign(targetRow, normalizeEmployeeRow(sourceRow, targetRow));
+ };
-const loadDeptOptions = () => {
- listDept().then((res) => {
- const tree = res.data ?? [];
- deptOptions.value = flattenDept(tree);
- });
-};
+ const clearRowRecalcState = row => {
+ const key = String(row?.staffOnJobId ?? row?.id ?? "");
+ if (!key) return;
+ clearTimeout(recalcTimers.get(key));
+ recalcTimers.delete(key);
+ recalcVersions.delete(key);
+ };
-// 鏋勫缓 閮ㄩ棬-浜哄憳 鏍戯紙鐢ㄤ簬鏂板浜哄憳寮圭獥锛�
-const loadDeptStaffTree = () => {
- Promise.all([listDept(), staffOnJobList()]).then(([deptRes, staffRes]) => {
- const tree = deptRes.data ?? [];
- const staffList = staffRes.data ?? [];
- const deptMap = new Map();
- function walk(nodes) {
- nodes.forEach((node) => {
- deptMap.set(node.deptId, {
- id: "dept_" + node.deptId,
- deptId: node.deptId,
- label: node.deptName,
- type: "dept",
- children: [],
+ const resetRecalcState = () => {
+ recalcTimers.forEach(timer => clearTimeout(timer));
+ recalcTimers.clear();
+ recalcVersions.clear();
+ };
+
+ const requestEmployeeRecalculate = row => {
+ const key = String(row?.staffOnJobId ?? row?.id ?? "");
+ if (!key || !form.value.salaryMonth) return;
+ const version = (recalcVersions.get(key) || 0) + 1;
+ recalcVersions.set(key, version);
+ clearTimeout(recalcTimers.get(key));
+ const payload = {
+ staffOnJobId: row.staffOnJobId ?? row.id,
+ salaryMonth: form.value.salaryMonth,
+ staffName: row.staffName ?? "",
+ postName: row.postName ?? "",
+ deptName: row.deptName ?? "",
+ basicSalary: parseNum(row.basicSalary),
+ pieceSalary: parseNum(row.pieceSalary),
+ hourlySalary: parseNum(row.hourlySalary),
+ otherIncome: parseNum(row.otherIncome),
+ otherDeduct: parseNum(row.otherDeduct),
+ socialSupplementAmount: parseNullableNum(row.socialSupplementAmount),
+ remark: row.remark ?? "",
+ };
+ recalcTimers.set(
+ key,
+ setTimeout(() => {
+ staffSalaryMainCalculateByEmployeeId(payload).then(res => {
+ if (recalcVersions.get(key) !== version) return;
+ applyBackendRow(row, res?.data || {});
});
- if (node.children?.length) walk(node.children);
- });
- }
- walk(tree);
- staffList.forEach((s) => {
- const deptId = s.deptId ?? s.dept_id;
- const node = deptMap.get(deptId);
- if (node) {
- node.children.push({
- id: s.id ?? s.staffId,
- staffId: s.id ?? s.staffId,
- label: s.staffName ?? s.name,
- type: "staff",
- ...s,
- });
- }
- });
- deptStaffTree.value = Array.from(deptMap.values()).filter(
- (n) => n.children && n.children.length > 0
+ }, 250)
);
- });
-};
+ };
-const openDialog = (type, row) => {
- nextTick(() => {
- loadDeptOptions();
- loadBankOptions();
- loadUserList();
- employeeList.value = [];
- Object.assign(form.value, {
+ const handleSalaryInput = row => {
+ row.basicSalary = parseNum(row.basicSalary);
+ row.otherIncome = parseNum(row.otherIncome);
+ requestEmployeeRecalculate(row);
+ };
+
+ // 鍩虹璧勬枡琛ㄥ崟
+ const data = reactive({
+ form: {
id: undefined,
salaryTitle: "",
deptIds: [],
@@ -507,298 +522,413 @@
remark: "",
payBank: "",
auditUserId: undefined,
+ },
+ rules: {
+ salaryTitle: [
+ { required: true, message: "璇疯緭鍏ュ伐璧勪富棰�", trigger: "blur" },
+ ],
+ deptIds: [{ required: true, message: "璇烽�夋嫨閮ㄩ棬", trigger: "change" }],
+ salaryMonth: [
+ { required: true, message: "璇烽�夋嫨宸ヨ祫鏈堜唤", trigger: "change" },
+ ],
+ auditUserId: [
+ { required: true, message: "璇烽�夋嫨瀹℃牳浜�", trigger: "change" },
+ ],
+ },
+ });
+ const { form, rules } = toRefs(data);
+
+ // 璁$畻宸ヨ祫鎬婚锛堟墍鏈夊憳宸ュ疄鍙戝伐璧勪箣鍜岋級
+ const totalSalary = computed(() => {
+ return employeeList.value.reduce((sum, e) => sum + parseNum(e.netSalary), 0);
+ });
+
+ // 鏍规嵁瀹℃牳浜篒D鑾峰彇瀹℃牳浜哄悕绉�
+ const auditUserName = computed(() => {
+ if (!form.value.auditUserId) return "";
+ const user = userList.value.find(u => u.userId === form.value.auditUserId);
+ return user ? user.nickName : "";
+ });
+
+ const loadBankOptions = () => {
+ return bankList().then(res => {
+ const list = Array.isArray(res?.data) ? res.data : [];
+ bankOptions.value = list
+ .map(b => (b?.bankName == null ? "" : String(b.bankName).trim()))
+ .filter(v => v !== "");
});
- // 缂栬緫锛氬垪琛ㄩ〉宸茶繑鍥炰富琛ㄥ瓧娈碉紱杩欓噷鍙仛鍥炴樉锛堟槑缁嗙敱鈥滅敓鎴愬伐璧勮〃/璁$畻宸ヨ祫鈥濆緱鍒帮級
- if (type === "edit" && row?.id) {
- form.value.id = row.id;
- form.value.salaryTitle = row.salaryTitle ?? "";
- // deptIds 鍚庣鏄瓧绗︿覆锛堝涓敤閫楀彿鍒嗛殧锛夛紱褰撳墠琛ㄥ崟浠嶆槸鍗曢�� deptId
- form.value.deptIds = row.deptIds
- ? String(row.deptIds).split(",").map((id) => Number(id.trim())).filter(Boolean)
- : [];
- form.value.salaryMonth = row.salaryMonth ?? "";
- form.value.remark = row.remark ?? "";
- form.value.payBank = row.payBank ?? "";
- form.value.auditUserId = row.auditUserId ?? undefined;
-
- // 濡傛灉鏈夊憳宸ユ槑缁嗘暟鎹紝鐩存帴鍙嶆樉
- if (row.staffSalaryDetailList && row.staffSalaryDetailList.length > 0) {
- employeeList.value = row.staffSalaryDetailList.map((e) => ({
- staffOnJobId: e.staffOnJobId ?? e.staffId ?? e.id,
- id: e.staffOnJobId ?? e.staffId ?? e.id,
- staffName: e.staffName ?? "",
- postName: e.postName ?? "",
- deptName: e.deptName ?? "",
- basicSalary: parseNum(e.basicSalary),
- pieceSalary: parseNum(e.pieceSalary),
- hourlySalary: parseNum(e.hourlySalary),
- otherIncome: parseNum(e.otherIncome),
- socialPersonal: parseNum(e.socialPersonal),
- fundPersonal: parseNum(e.fundPersonal),
- otherDeduct: parseNum(e.otherDeduct),
- salaryTax: parseNum(e.salaryTax),
- grossSalary: parseNum(e.grossSalary),
- deductSalary: parseNum(e.deductSalary),
- netSalary: parseNum(e.netSalary),
- remark: e.remark ?? "",
- }));
- }
- }
- });
-};
-
-const openAddPerson = () => {
- loadDeptStaffTree();
- addPersonVisible.value = true;
- nextTick(() => {
- personTreeRef.value?.setCheckedKeys([]);
- });
-};
-
-const addPersonClose = () => {};
-
-const confirmAddPerson = () => {
- const tree = personTreeRef.value;
- if (!tree) {
- addPersonVisible.value = false;
- return;
- }
- const checked = tree.getCheckedNodes();
- const staffNodes = checked.filter((n) => n.type === "staff");
- const existIds = new Set(employeeList.value.map((e) => e.staffId || e.id));
- staffNodes.forEach((node) => {
- const id = node.staffId ?? node.id;
- if (existIds.has(id)) return;
- existIds.add(id);
- employeeList.value.push({
- staffOnJobId: id,
- id,
- staffName: node.label,
- postName: node.postName ?? node.post ?? "",
- deptName: node.deptName ?? "",
- basicSalary: 0,
- pieceSalary: 0,
- hourlySalary: 0,
- otherIncome: 0,
- socialPersonal: 0,
- fundPersonal: 0,
- otherDeduct: 0,
- salaryTax: 0,
- grossSalary: 0,
- deductSalary: 0,
- netSalary: 0,
- remark: "",
- });
- });
- addPersonVisible.value = false;
-};
-
-const removeEmployee = (row) => {
- employeeList.value = employeeList.value.filter(
- (e) => (e.staffOnJobId || e.id) !== (row.staffOnJobId || row.id)
- );
-};
-
-const onEmployeeSelectionChange = (selection) => {
- selectedEmployees.value = selection;
-};
-
-const handleBatchDelete = () => {
- if (!selectedEmployees.value?.length) {
- proxy.$modal.msgWarning("璇峰厛鍕鹃�夎鍒犻櫎鐨勫憳宸�");
- return;
- }
- const ids = new Set(selectedEmployees.value.map((e) => e.staffOnJobId || e.id));
- employeeList.value = employeeList.value.filter(
- (e) => !ids.has(e.staffOnJobId || e.id)
- );
-};
-
-const handleGenerate = () => {
- if (!form.value.deptIds?.length) {
- proxy.$modal.msgWarning("璇峰厛閫夋嫨閮ㄩ棬");
- return;
- }
- if (!form.value.salaryMonth) {
- proxy.$modal.msgWarning("璇峰厛閫夋嫨宸ヨ祫鏈堜唤");
- return;
- }
- const payload = {
- ids: form.value.deptIds,
- date: form.value.salaryMonth,
};
- staffSalaryMainCalculateSalary(payload).then((res) => {
- const list = Array.isArray(res?.data) ? res.data : [];
- if (!list.length) {
- proxy.$modal.msgWarning("鏈绠楀埌宸ヨ祫鏁版嵁");
+
+ const loadUserList = () => {
+ return userListNoPageByTenantId().then(res => {
+ userList.value = res.data || [];
+ });
+ };
+
+ // 鎵佸钩鍖栭儴闂ㄦ爲渚涗笅鎷変娇鐢�
+ function flattenDept(tree, list = []) {
+ if (!tree?.length) return list;
+ tree.forEach(node => {
+ list.push({ deptId: node.deptId, deptName: node.deptName });
+ if (node.children?.length) flattenDept(node.children, list);
+ });
+ return list;
+ }
+
+ // 鍔犺浇鎵�鏈夐儴闂�
+ const loadAllDeptOptions = () => {
+ listDept().then(res => {
+ const tree = res.data ?? [];
+ deptOptions.value = flattenDept(tree);
+ });
+ };
+
+ // 鍔犺浇鍙敤閮ㄩ棬鍒楄〃锛堢敤浜庡垽鏂儴闂ㄦ槸鍚﹀彲閫夛級
+ const loadAvailableDepts = (salaryMonth) => {
+ if (!salaryMonth) {
+ availableDeptIds.value = new Set();
return;
}
- employeeList.value = list.map((e) => ({
- ...e,
- staffOnJobId: e.staffOnJobId ?? e.staffId ?? e.id,
- staffName: e.staffName,
- postName: e.postName,
- deptName: e.deptName,
- basicSalary: parseNum(e.basicSalary),
- pieceSalary: parseNum(e.pieceSalary),
- hourlySalary: parseNum(e.hourlySalary),
- otherIncome: parseNum(e.otherIncome),
- socialPersonal: parseNum(e.socialPersonal),
- fundPersonal: parseNum(e.fundPersonal),
- otherDeduct: parseNum(e.otherDeduct),
- salaryTax: parseNum(e.salaryTax),
- grossSalary: parseNum(e.grossSalary),
- deductSalary: parseNum(e.deductSalary),
- netSalary: parseNum(e.netSalary),
- remark: e.remark ?? "",
- }));
- proxy.$modal.msgSuccess("鐢熸垚鎴愬姛");
- });
-};
-
-const handleClear = () => {
- proxy.$modal.confirm("纭畾娓呯┖褰撳墠鍛樺伐鍒楄〃鍚楋紵").then(() => {
- employeeList.value = [];
- }).catch(() => {});
-};
-
-const handleTaxForm = () => {
- taxDialogVisible.value = true;
-};
-
-const submitForm = () => {
- formRef.value?.validate((valid) => {
- if (!valid) return;
- saveData(3); // 纭鎻愪氦锛岀姸鎬佷负3锛堝緟瀹℃牳锛�
- });
-};
-
-const saveDraft = () => {
- formRef.value?.validate((valid) => {
- if (!valid) return;
- saveData(1); // 淇濆瓨鑽夌锛岀姸鎬佷负1锛堣崏绋匡級
- });
-};
-
-const saveData = (status) => {
- const payload = {
- id: form.value.id,
- salaryTitle: form.value.salaryTitle,
- deptIds: form.value.deptIds?.length ? form.value.deptIds.join(",") : "",
- salaryMonth: form.value.salaryMonth,
- remark: form.value.remark,
- payBank: form.value.payBank,
- auditUserId: form.value.auditUserId,
- auditUserName: auditUserName.value,
- totalSalary: totalSalary.value,
- staffSalaryDetailList: employeeList.value.map((e) => ({
- staffOnJobId: e.staffOnJobId ?? e.staffId ?? e.id,
- staffName: e.staffName,
- postName: e.postName ?? "",
- deptName: e.deptName ?? "",
- basicSalary: parseNum(e.basicSalary),
- pieceSalary: parseNum(e.pieceSalary),
- hourlySalary: parseNum(e.hourlySalary),
- otherIncome: parseNum(e.otherIncome),
- socialPersonal: parseNum(e.socialPersonal),
- fundPersonal: parseNum(e.fundPersonal),
- otherDeduct: parseNum(e.otherDeduct),
- salaryTax: parseNum(e.salaryTax),
- grossSalary: parseNum(e.grossSalary),
- deductSalary: parseNum(e.deductSalary),
- netSalary: parseNum(e.netSalary),
- remark: e.remark ?? "",
- })),
+ staffSalaryMainAvailableDepts(salaryMonth).then(res => {
+ const availableDepts = Array.isArray(res.data) ? res.data : [];
+ availableDeptIds.value = new Set(availableDepts.map(d => d.deptId));
+ });
};
- if (props.operationType === "add") {
- staffSalaryMainAdd({ ...payload, status }).then(() => {
- proxy.$modal.msgSuccess(status === 1 ? "鑽夌淇濆瓨鎴愬姛" : "鎻愪氦鎴愬姛");
- closeDia();
- });
- } else {
- staffSalaryMainUpdate({ ...payload, status }).then(() => {
- proxy.$modal.msgSuccess(status === 1 ? "鑽夌淇濆瓨鎴愬姛" : "鎻愪氦鎴愬姛");
- closeDia();
- });
- }
-};
-const closeDia = () => {
- dialogVisible.value = false;
- emit("close");
-};
+ // 鍒ゆ柇閮ㄩ棬鏄惁绂佺敤
+ const isDeptDisabled = (deptId) => {
+ // 濡傛灉娌℃湁閫夋嫨鏈堜唤锛屾墍鏈夐儴闂ㄩ兘鍙��
+ if (!form.value.salaryMonth) return false;
+ // 濡傛灉宸查�夋嫨鏈堜唤锛屽彧鏈夊湪鍙敤閮ㄩ棬鍒楄〃涓殑閮ㄩ棬鎵嶅彲閫�
+ return !availableDeptIds.value.has(deptId);
+ };
-defineExpose({ openDialog });
+ // 鏋勫缓 閮ㄩ棬-浜哄憳 鏍戯紙鐢ㄤ簬鏂板浜哄憳寮圭獥锛�
+ const loadDeptStaffTree = () => {
+ Promise.all([listDept(), staffOnJobList()]).then(([deptRes, staffRes]) => {
+ const tree = deptRes.data ?? [];
+ const staffList = staffRes.data ?? [];
+ const deptMap = new Map();
+ function walk(nodes) {
+ nodes.forEach(node => {
+ deptMap.set(node.deptId, {
+ id: "dept_" + node.deptId,
+ deptId: node.deptId,
+ label: node.deptName,
+ type: "dept",
+ children: [],
+ });
+ if (node.children?.length) walk(node.children);
+ });
+ }
+ walk(tree);
+ staffList.forEach(s => {
+ const deptId = s.deptId ?? s.dept_id;
+ const node = deptMap.get(deptId);
+ if (node) {
+ node.children.push({
+ id: s.id ?? s.staffId,
+ staffId: s.id ?? s.staffId,
+ label: s.staffName ?? s.name,
+ type: "staff",
+ ...s,
+ });
+ }
+ });
+ deptStaffTree.value = Array.from(deptMap.values()).filter(
+ n => n.children && n.children.length > 0
+ );
+ });
+ };
+
+ const openDialog = (type, row) => {
+ nextTick(() => {
+ resetRecalcState();
+ loadBankOptions();
+ loadUserList();
+ loadAllDeptOptions();
+ employeeList.value = [];
+ Object.assign(form.value, {
+ id: undefined,
+ salaryTitle: "",
+ deptIds: [],
+ salaryMonth: "",
+ remark: "",
+ payBank: "",
+ auditUserId: undefined,
+ });
+ // 缂栬緫锛氬垪琛ㄩ〉宸茶繑鍥炰富琛ㄥ瓧娈碉紱杩欓噷鍙仛鍥炴樉锛堟槑缁嗙敱鈥滅敓鎴愬伐璧勮〃/璁$畻宸ヨ祫鈥濆緱鍒帮級
+ if (type === "edit" && row?.id) {
+ form.value.id = row.id;
+ form.value.salaryTitle = row.salaryTitle ?? "";
+ // deptIds 鍚庣鏄瓧绗︿覆锛堝涓敤閫楀彿鍒嗛殧锛夛紱褰撳墠琛ㄥ崟浠嶆槸鍗曢�� deptId
+ form.value.deptIds = row.deptIds
+ ? String(row.deptIds)
+ .split(",")
+ .map(id => Number(id.trim()))
+ .filter(Boolean)
+ : [];
+ form.value.salaryMonth = row.salaryMonth ?? "";
+ form.value.remark = row.remark ?? "";
+ form.value.payBank = row.payBank ?? "";
+ form.value.auditUserId = row.auditUserId ?? undefined;
+
+ // 濡傛灉鏈夊憳宸ユ槑缁嗘暟鎹紝鐩存帴鍙嶆樉
+ if (row.staffSalaryDetailList && row.staffSalaryDetailList.length > 0) {
+ employeeList.value = row.staffSalaryDetailList.map(e => normalizeEmployeeRow(e));
+ }
+ }
+ // 缂栬緫鐘舵�佷笖鏈夋湀浠芥椂锛屽姞杞藉彲鐢ㄩ儴闂ㄥ垪琛�
+ if (type === "edit" && form.value.salaryMonth) {
+ loadAvailableDepts(form.value.salaryMonth);
+ }
+ });
+ };
+
+ // 鐩戝惉宸ヨ祫鏈堜唤鍙樺寲锛屾竻绌洪儴闂ㄩ�夋嫨骞跺姞杞藉彲鐢ㄩ儴闂ㄥ垪琛�
+ watch(() => form.value.salaryMonth, (newMonth, oldMonth) => {
+ // 鍒囨崲鏈堜唤鏃舵竻绌哄凡閫夋嫨鐨勯儴闂�
+ if (newMonth !== oldMonth) {
+ form.value.deptIds = [];
+ }
+ loadAvailableDepts(newMonth);
+ });
+
+ const openAddPerson = () => {
+ loadDeptStaffTree();
+ addPersonVisible.value = true;
+ nextTick(() => {
+ personTreeRef.value?.setCheckedKeys([]);
+ });
+ };
+
+ const addPersonClose = () => {};
+
+ const confirmAddPerson = () => {
+ const tree = personTreeRef.value;
+ if (!tree) {
+ addPersonVisible.value = false;
+ return;
+ }
+ const checked = tree.getCheckedNodes();
+ const staffNodes = checked.filter(n => n.type === "staff");
+ const existIds = new Set(employeeList.value.map(e => e.staffId || e.id));
+ staffNodes.forEach(node => {
+ const id = node.staffId ?? node.id;
+ if (existIds.has(id)) return;
+ existIds.add(id);
+ const newRow = normalizeEmployeeRow({
+ staffOnJobId: id,
+ id,
+ staffName: node.label,
+ postName: node.postName ?? node.post ?? "",
+ deptName: node.deptName ?? "",
+ remark: "",
+ socialSupplementAmount: null,
+ });
+ employeeList.value.push(newRow);
+ requestEmployeeRecalculate(newRow);
+ });
+ addPersonVisible.value = false;
+ };
+
+ const removeEmployee = row => {
+ clearRowRecalcState(row);
+ employeeList.value = employeeList.value.filter(
+ e => (e.staffOnJobId || e.id) !== (row.staffOnJobId || row.id)
+ );
+ };
+
+ const onEmployeeSelectionChange = selection => {
+ selectedEmployees.value = selection;
+ };
+
+ const handleBatchDelete = () => {
+ if (!selectedEmployees.value?.length) {
+ proxy.$modal.msgWarning("璇峰厛鍕鹃�夎鍒犻櫎鐨勫憳宸�");
+ return;
+ }
+ const ids = new Set(selectedEmployees.value.map(e => e.staffOnJobId || e.id));
+ selectedEmployees.value.forEach(item => clearRowRecalcState(item));
+ employeeList.value = employeeList.value.filter(
+ e => !ids.has(e.staffOnJobId || e.id)
+ );
+ };
+
+ const handleGenerate = () => {
+ if (!form.value.deptIds?.length) {
+ proxy.$modal.msgWarning("璇峰厛閫夋嫨閮ㄩ棬");
+ return;
+ }
+ if (!form.value.salaryMonth) {
+ proxy.$modal.msgWarning("璇峰厛閫夋嫨宸ヨ祫鏈堜唤");
+ return;
+ }
+ const payload = {
+ ids: form.value.deptIds,
+ date: form.value.salaryMonth,
+ };
+ staffSalaryMainCalculateSalary(payload).then(res => {
+ const list = Array.isArray(res?.data) ? res.data : [];
+ if (!list.length) {
+ proxy.$modal.msgWarning("鏈绠楀埌宸ヨ祫鏁版嵁");
+ return;
+ }
+ resetRecalcState();
+ employeeList.value = list.map(e => normalizeEmployeeRow(e));
+ proxy.$modal.msgSuccess("鐢熸垚鎴愬姛");
+ });
+ };
+
+ const handleClear = () => {
+ proxy.$modal
+ .confirm("纭畾娓呯┖褰撳墠鍛樺伐鍒楄〃鍚楋紵")
+ .then(() => {
+ resetRecalcState();
+ employeeList.value = [];
+ })
+ .catch(() => {});
+ };
+
+ const handleTaxForm = () => {
+ taxDialogVisible.value = true;
+ };
+
+ const submitForm = () => {
+ formRef.value?.validate(valid => {
+ if (!valid) return;
+ saveData(3); // 纭鎻愪氦锛岀姸鎬佷负3锛堝緟瀹℃牳锛�
+ });
+ };
+
+ const saveDraft = () => {
+ formRef.value?.validate(valid => {
+ if (!valid) return;
+ saveData(1); // 淇濆瓨鑽夌锛岀姸鎬佷负1锛堣崏绋匡級
+ });
+ };
+
+ const saveData = status => {
+ const payload = {
+ id: form.value.id,
+ salaryTitle: form.value.salaryTitle,
+ deptIds: form.value.deptIds?.length ? form.value.deptIds.join(",") : "",
+ salaryMonth: form.value.salaryMonth,
+ remark: form.value.remark,
+ payBank: form.value.payBank,
+ auditUserId: form.value.auditUserId,
+ auditUserName: auditUserName.value,
+ totalSalary: totalSalary.value,
+ staffSalaryDetailList: employeeList.value.map(e => ({
+ staffOnJobId: e.staffOnJobId ?? e.staffId ?? e.id,
+ staffName: e.staffName,
+ postName: e.postName ?? "",
+ deptName: e.deptName ?? "",
+ basicSalary: parseNum(e.basicSalary),
+ pieceSalary: parseNum(e.pieceSalary),
+ hourlySalary: parseNum(e.hourlySalary),
+ otherIncome: parseNum(e.otherIncome),
+ socialPersonal: parseNum(e.socialPersonal),
+ fundPersonal: parseNum(e.fundPersonal),
+ otherDeduct: parseNum(e.otherDeduct),
+ socialSupplementAmount: parseNum(e.socialSupplementAmount),
+ salaryTax: parseNum(e.salaryTax),
+ grossSalary: parseNum(e.grossSalary),
+ deductSalary: parseNum(e.deductSalary),
+ netSalary: parseNum(e.netSalary),
+ remark: e.remark ?? "",
+ })),
+ };
+ if (props.operationType === "add") {
+ staffSalaryMainAdd({ ...payload, status }).then(() => {
+ proxy.$modal.msgSuccess(status === 1 ? "鑽夌淇濆瓨鎴愬姛" : "鎻愪氦鎴愬姛");
+ closeDia();
+ });
+ } else {
+ staffSalaryMainUpdate({ ...payload, status }).then(() => {
+ proxy.$modal.msgSuccess(status === 1 ? "鑽夌淇濆瓨鎴愬姛" : "鎻愪氦鎴愬姛");
+ closeDia();
+ });
+ }
+ };
+
+ const closeDia = () => {
+ resetRecalcState();
+ dialogVisible.value = false;
+ emit("close");
+ };
+
+ defineExpose({ openDialog });
</script>
<style scoped>
-.form-dia-body {
- padding: 0;
-}
-.card-title-line {
- color: #f56c6c;
- margin-right: 4px;
-}
-.form-card {
- margin-bottom: 16px;
-}
-.form-card :deep(.el-card__header) {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 12px 16px;
-}
-.card-title {
- font-weight: 500;
-}
-.card-collapse {
- color: #999;
- cursor: pointer;
-}
-.toolbar {
- margin-bottom: 16px;
- display: flex;
- flex-wrap: wrap;
- gap: 10px;
-}
-.employee-table-wrap {
- position: relative;
- min-height: 120px;
-}
-.table-empty {
- text-align: center;
- padding: 24px;
- color: #999;
- font-size: 14px;
-}
-.add-person-tree {
- max-height: 360px;
- overflow-y: auto;
- padding: 8px 0;
-}
-.tax-desc {
- margin-bottom: 12px;
- font-size: 14px;
- color: #606266;
-}
-.dialog-footer {
- text-align: right;
-}
-.salary-total {
- margin-top: 16px;
- padding: 12px 16px;
- background-color: #f5f7fa;
- border-radius: 4px;
- text-align: right;
- font-size: 16px;
-}
-.salary-total .total-label {
- color: #606266;
- margin-right: 8px;
-}
-.salary-total .total-value {
- color: #f56c6c;
- font-weight: bold;
- font-size: 18px;
-}
+ .form-dia-body {
+ padding: 0;
+ }
+ .card-title-line {
+ color: #f56c6c;
+ margin-right: 4px;
+ }
+ .form-card {
+ margin-bottom: 16px;
+ }
+ .form-card :deep(.el-card__header) {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 16px;
+ }
+ .card-title {
+ font-weight: 500;
+ }
+ .card-collapse {
+ color: #999;
+ cursor: pointer;
+ }
+ .toolbar {
+ margin-bottom: 16px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ }
+ .employee-table-wrap {
+ position: relative;
+ min-height: 120px;
+ }
+ .table-empty {
+ text-align: center;
+ padding: 24px;
+ color: #999;
+ font-size: 14px;
+ }
+ .add-person-tree {
+ max-height: 360px;
+ overflow-y: auto;
+ padding: 8px 0;
+ }
+ .tax-desc {
+ margin-bottom: 12px;
+ font-size: 14px;
+ color: #606266;
+ }
+ .dialog-footer {
+ text-align: right;
+ }
+ .salary-total {
+ margin-top: 16px;
+ padding: 12px 16px;
+ background-color: #f5f7fa;
+ border-radius: 4px;
+ text-align: right;
+ font-size: 16px;
+ }
+ .salary-total .total-label {
+ color: #606266;
+ margin-right: 8px;
+ }
+ .salary-total .total-value {
+ color: #f56c6c;
+ font-weight: bold;
+ font-size: 18px;
+ }
</style>
diff --git a/src/views/personnelManagement/monthlyStatistics/index.vue b/src/views/personnelManagement/monthlyStatistics/index.vue
index 4ac9e97..f581070 100644
--- a/src/views/personnelManagement/monthlyStatistics/index.vue
+++ b/src/views/personnelManagement/monthlyStatistics/index.vue
@@ -107,7 +107,7 @@
nextTick,
} from "vue";
import { ElMessageBox } from "element-plus";
-import Cookies from "js-cookie";
+import useUserStore from "@/store/modules/user";
import FormDia from "./components/formDia.vue";
import BankSettingDia from "./components/bankSettingDia.vue";
import AuditDia from "./components/auditDia.vue";
@@ -167,7 +167,12 @@
{
name: "瀹℃牳",
type: "text",
- disabled: (row) => Number(row?.status) !== 3,
+ disabled: (row) => {
+ const currentUserId = Number(userStore.id);
+ const auditUserId = Number(row?.auditUserId);
+ // 鐘舵�佷笉鏄緟瀹℃牳 鎴� 褰撳墠鐢ㄦ埛涓嶆槸鎸囧畾鐨勫鏍镐汉锛屽垯绂佺敤瀹℃牳鎸夐挳
+ return Number(row?.status) !== 3 || !currentUserId || currentUserId !== auditUserId;
+ },
clickFun: (row) => openAudit(row),
},
{
@@ -193,6 +198,7 @@
const operationType = ref("add");
const currentRow = ref({});
const { proxy } = getCurrentInstance();
+const userStore = useUserStore();
const bankSetting = ref({});
const bankDialogVisible = ref(false);
const bankDiaRef = ref(null);
@@ -378,8 +384,8 @@
const handleExport = () => {
proxy.download(
- "/compensationPerformance/export",
- { ...searchForm.value, current: page.current, size: page.size },
+ "/staffSalaryMain/export",
+ { ...searchForm.value },
"宸ヨ祫琛�.xlsx"
);
};
@@ -404,4 +410,4 @@
.table_list {
margin-top: 20px;
}
-</style>
+</style>
\ No newline at end of file
diff --git a/src/views/personnelManagement/socialSecuritySet/components/formDia.vue b/src/views/personnelManagement/socialSecuritySet/components/formDia.vue
index 71f5e61..a8cba91 100644
--- a/src/views/personnelManagement/socialSecuritySet/components/formDia.vue
+++ b/src/views/personnelManagement/socialSecuritySet/components/formDia.vue
@@ -1,34 +1,31 @@
<template>
<div>
- <FormDialog
- v-model="dialogFormVisible"
- :operation-type="operationType"
- :title="dialogTitle"
- width="80%"
- @close="closeDia"
- @confirm="submitForm"
- @cancel="closeDia"
- >
- <el-form ref="formRef" :model="form" :rules="rules" label-position="top">
+ <FormDialog v-model="dialogFormVisible"
+ :operation-type="operationType"
+ :title="dialogTitle"
+ width="80%"
+ @close="closeDia"
+ @confirm="submitForm"
+ @cancel="closeDia">
+ <el-form ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-position="top">
<el-row :gutter="24">
<!-- 宸︿晶锛氶�傜敤浜哄憳 -->
<el-col :span="8">
- <el-form-item label="閫傜敤浜哄憳锛�" prop="deptIds">
+ <el-form-item label="閫傜敤浜哄憳锛�"
+ prop="deptIds">
<div class="dept-checkbox-wrap">
- <el-checkbox-group
- v-model="form.deptIds"
- :disabled="isDetail"
- >
- <div
- v-for="dept in deptList"
- :key="dept.deptId"
- class="dept-checkbox-item"
- >
+ <el-checkbox-group v-model="form.deptIds"
+ :disabled="isDetail">
+ <div v-for="dept in deptList"
+ :key="dept.deptId"
+ class="dept-checkbox-item">
<el-checkbox :value="dept.deptId">
{{ dept.deptName }}
- <span v-if="dept.personCount != null" class="dept-count"
- >{{ dept.personCount }}浜�</span
- >
+ <span v-if="dept.personCount != null"
+ class="dept-count">{{ dept.personCount }}浜�</span>
</el-checkbox>
</div>
</el-checkbox-group>
@@ -38,124 +35,117 @@
<!-- 鍙充晶锛氬熀纭�淇℃伅 + 淇濋櫓绫诲瀷 -->
<el-col :span="16">
<!-- 鍩虹淇℃伅 -->
- <el-card class="form-card" shadow="never">
+ <el-card class="form-card"
+ shadow="never">
<template #header>
<span class="card-title"><span class="card-title-line">|</span> 鍩虹淇℃伅</span>
- <el-icon class="card-collapse"><ArrowUp /></el-icon>
+ <el-icon class="card-collapse">
+ <ArrowUp />
+ </el-icon>
</template>
- <el-form-item label="鏂规鏍囬锛�" prop="title">
- <el-input
- v-model="form.title"
- placeholder="璇疯緭鍏�"
- clearable
- :disabled="isDetail"
- />
+ <el-form-item label="鏂规鏍囬锛�"
+ prop="title">
+ <el-input v-model="form.title"
+ placeholder="璇疯緭鍏�"
+ clearable
+ :disabled="isDetail" />
</el-form-item>
- <el-form-item label="澶囨敞锛�" prop="remark">
- <el-input
- v-model="form.remark"
- type="textarea"
- :rows="2"
- placeholder="璇疯緭鍏�"
- clearable
- :disabled="isDetail"
- />
+ <el-form-item label="澶囨敞锛�"
+ prop="remark">
+ <el-input v-model="form.remark"
+ type="textarea"
+ :rows="2"
+ placeholder="璇疯緭鍏�"
+ clearable
+ :disabled="isDetail" />
</el-form-item>
</el-card>
-
<!-- 淇濋櫓绫诲瀷 -->
- <el-card class="form-card" shadow="never">
+ <el-card class="form-card"
+ shadow="never">
<template #header>
<span class="card-title"><span class="card-title-line">|</span> 淇濋櫓绫诲瀷</span>
- <el-button
- v-if="!isDetail"
- type="primary"
- size="small"
- @click="addInsuranceBenefit"
- >
+ <el-button v-if="!isDetail"
+ type="primary"
+ size="small"
+ @click="addInsuranceBenefit">
娣诲姞淇濋櫓绂忓埄
</el-button>
</template>
<el-row :gutter="16">
- <el-col
- v-for="(item, index) in form.insuranceBenefits"
- :key="item._key"
- :span="12"
- >
+ <el-col v-for="(item, index) in form.insuranceBenefits"
+ :key="item._key"
+ :span="12">
<div class="insurance-benefit-card">
<div class="insurance-benefit-title">
淇濋櫓绂忓埄{{ index + 1 }}
- <el-button
- v-if="!isDetail && form.insuranceBenefits.length > 1"
- type="danger"
- link
- size="small"
- class="card-delete-btn"
- @click="removeInsuranceBenefit(index)"
- >
+ <el-button v-if="!isDetail && form.insuranceBenefits.length > 1"
+ type="danger"
+ link
+ size="small"
+ class="card-delete-btn"
+ @click="removeInsuranceBenefit(index)">
鍒犻櫎
</el-button>
</div>
- <el-form-item
- :prop="'insuranceBenefits.' + index + '.insuranceType'"
- label="淇濋櫓绫诲瀷锛�"
- label-width="100px"
+ <el-form-item :prop="'insuranceBenefits.' + index + '.insuranceType'"
+ label="淇濋櫓绫诲瀷锛�"
+ label-width="100px"
+ :rules="[{ required: true, message: '璇烽�夋嫨淇濋櫓绫诲瀷', trigger: 'blur' }]"
>
- <el-select
- v-model="item.insuranceType"
- placeholder="璇烽�夋嫨"
- clearable
- style="width: 100%"
- :disabled="isDetail"
- >
- <el-option
- v-for="opt in insuranceTypeOptions"
- :key="opt.value"
- :label="opt.label"
- :value="opt.value"
- />
+ <el-select v-model="item.insuranceType"
+ placeholder="璇烽�夋嫨"
+ clearable
+ style="width: 100%"
+ :disabled="isDetail">
+ <el-option v-for="opt in insuranceTypeOptions"
+ :key="opt.value"
+ :label="opt.label"
+ :value="opt.value" />
</el-select>
</el-form-item>
- <el-form-item label="缂磋垂鍩烘暟锛�" label-width="100px">
+ <el-form-item label="缂磋垂鍩烘暟锛�"
+ label-width="100px"
+ :prop="'insuranceBenefits.' + index + '.paymentBase'"
+ :rules="[{ validator: validatePaymentBase, trigger: 'blur' }]">
<div class="base-salary-wrap">
- <el-input
- v-model="item.paymentBase"
- placeholder="鏍规嵁鍩烘湰宸ヨ祫缂寸撼"
- clearable
- style="width: 120px"
- type="number"
- :disabled="isDetail || item.useBasicSalary"
- @input="handlePaymentBaseInput(item)"
- />
- <el-checkbox
- v-model="item.useBasicSalary"
- @change="handleUseBasicSalaryChange(item)"
- :disabled="isDetail"
- >
+ <el-input v-model="item.paymentBase"
+ placeholder="鏍规嵁鍩烘湰宸ヨ祫缂寸撼"
+ clearable
+ style="width: 120px"
+ type="number"
+ :disabled="isDetail || item.useBasicSalary"
+ @input="handlePaymentBaseInput(item)" />
+ <el-checkbox v-model="item.useBasicSalary"
+ @change="(val) => handleUseBasicSalaryChange(item, index)"
+ :disabled="isDetail">
璋冪敤鍩烘湰宸ヨ祫
</el-checkbox>
</div>
</el-form-item>
- <el-form-item label="涓汉缂磋垂姣斾緥锛�" label-width="100px">
+ <el-form-item label="涓汉缂磋垂姣斾緥锛�"
+ label-width="100px"
+ :prop="'insuranceBenefits.' + index + '.personalRatio'"
+ :rules="[{ required: true, message: '璇疯緭鍏ヤ釜浜虹即璐规瘮渚�', trigger: 'blur' }]">
<div class="personal-ratio-wrap">
- <el-input
- v-model="item.personalRatio"
- placeholder="璇疯緭鍏�"
- clearable
- style="width: 100px"
- type="number"
- :disabled="isDetail"
- />
+ <el-input v-model="item.personalRatio"
+ placeholder="璇疯緭鍏�"
+ clearable
+ style="width: 100px"
+ type="number"
+ :disabled="isDetail"
+ :min="0"
+ @input="handlePersonalRatioInput(item)" />
<span class="ratio-unit">(%)</span>
<span class="ratio-plus">+</span>
- <el-input
- v-model="item.personalFixed"
- placeholder="璇疯緭鍏�"
- clearable
- style="width: 100px"
- type="number"
- :disabled="isDetail"
- />
+ <el-input v-model="item.personalFixed"
+ placeholder="璇疯緭鍏�"
+ clearable
+ style="width: 100px"
+ type="number"
+ :disabled="isDetail"
+ :min="0"
+ @input="handlePersonalFixedInput(item)" />
</div>
</el-form-item>
</div>
@@ -170,301 +160,345 @@
</template>
<script setup>
-import { ref, reactive, toRefs, getCurrentInstance, nextTick, computed } from "vue";
-import FormDialog from "@/components/Dialog/FormDialog.vue";
-import { ArrowUp } from "@element-plus/icons-vue";
-import { listDept } from "@/api/system/dept.js";
-import { socialSecurityAdd, socialSecurityUpdate } from "@/api/personnelManagement/socialSecuritySet.js";
+ import {
+ ref,
+ reactive,
+ toRefs,
+ getCurrentInstance,
+ nextTick,
+ computed,
+ } from "vue";
+ import FormDialog from "@/components/Dialog/FormDialog.vue";
+ import { ArrowUp } from "@element-plus/icons-vue";
+ import { listDept } from "@/api/system/dept.js";
+ import {
+ socialSecurityAdd,
+ socialSecurityUpdate,
+ } from "@/api/personnelManagement/socialSecuritySet.js";
-const emit = defineEmits(["close"]);
-const { proxy } = getCurrentInstance();
+ const emit = defineEmits(["close"]);
+ const { proxy } = getCurrentInstance();
-const dialogFormVisible = ref(false);
-const operationType = ref("add");
-const formRef = ref(null);
-const deptList = ref([]);
+ const dialogFormVisible = ref(false);
+ const operationType = ref("add");
+ const formRef = ref(null);
+ const deptList = ref([]);
-const isDetail = computed(() => operationType.value === "detail");
+ const isDetail = computed(() => operationType.value === "detail");
-const dialogTitle = () =>
- operationType.value === "add"
- ? "鏂板鏂规"
- : operationType.value === "edit"
- ? "缂栬緫鏂规"
- : "鏂规璇︽儏";
+ const dialogTitle = () =>
+ operationType.value === "add"
+ ? "鏂板鏂规"
+ : operationType.value === "edit"
+ ? "缂栬緫鏂规"
+ : "鏂规璇︽儏";
-// 淇濋櫓绫诲瀷閫夐」锛堝彲鎸夊瓧鍏告浛鎹級
-const insuranceTypeOptions = [
- { label: "鍏昏�佷繚闄�", value: "鍏昏�佷繚闄�" },
- { label: "鍖荤枟淇濋櫓", value: "鍖荤枟淇濋櫓" },
- { label: "澶变笟淇濋櫓", value: "澶变笟淇濋櫓" },
- { label: "宸ヤ激淇濋櫓", value: "宸ヤ激淇濋櫓" },
- { label: "鐢熻偛淇濋櫓", value: "鐢熻偛淇濋櫓" },
- { label: "鍏Н閲�", value: "鍏Н閲�" },
-];
+ // 淇濋櫓绫诲瀷閫夐」锛堝彲鎸夊瓧鍏告浛鎹級
+ const insuranceTypeOptions = [
+ { label: "鍏昏�佷繚闄�", value: "鍏昏�佷繚闄�" },
+ { label: "鍖荤枟淇濋櫓", value: "鍖荤枟淇濋櫓" },
+ { label: "澶变笟淇濋櫓", value: "澶变笟淇濋櫓" },
+ { label: "宸ヤ激淇濋櫓", value: "宸ヤ激淇濋櫓" },
+ { label: "鐢熻偛淇濋櫓", value: "鐢熻偛淇濋櫓" },
+ { label: "鍏Н閲�", value: "鍏Н閲�" },
+ ];
-const defaultBenefit = () => ({
- _key: Math.random().toString(36).slice(2),
- insuranceType: "",
- paymentBase: "",
- useBasicSalary: false,
- personalRatio: "",
- personalFixed: "",
-});
+ const defaultBenefit = () => ({
+ _key: Math.random().toString(36).slice(2),
+ insuranceType: "",
+ paymentBase: "",
+ useBasicSalary: false,
+ personalRatio: "",
+ personalFixed: "",
+ });
-const data = reactive({
- form: {
- id: undefined,
- title: "",
- remark: "",
- deptIds: [],
- insuranceBenefits: [defaultBenefit()],
- },
- rules: {
- title: [{ required: true, message: "璇疯緭鍏ユ柟妗堟爣棰�", trigger: "blur" }],
- deptIds: [
- {
- required: true,
- type: "array",
- min: 1,
- message: "璇疯嚦灏戦�夋嫨涓�涓�傜敤閮ㄩ棬",
- trigger: "change",
- },
- ],
- },
-});
-const { form, rules } = toRefs(data);
+ const data = reactive({
+ form: {
+ id: undefined,
+ title: "",
+ remark: "",
+ deptIds: [],
+ insuranceBenefits: [defaultBenefit()],
+ },
+ rules: {
+ title: [{ required: true, message: "璇疯緭鍏ユ柟妗堟爣棰�", trigger: "blur" }],
+ deptIds: [
+ {
+ required: true,
+ type: "array",
+ min: 1,
+ message: "璇疯嚦灏戦�夋嫨涓�涓�傜敤閮ㄩ棬",
+ trigger: "change",
+ },
+ ],
+ },
+ });
+ const { form, rules } = toRefs(data);
-function flattenDept(tree, list = []) {
- if (!tree || !tree.length) return list;
- tree.forEach((node) => {
- list.push({
- deptId: node.deptId,
- deptName: node.deptName,
- personCount: node.personCount ?? null,
+ function flattenDept(tree, list = []) {
+ if (!tree || !tree.length) return list;
+ tree.forEach(node => {
+ list.push({
+ deptId: node.deptId,
+ deptName: node.deptName,
+ personCount: node.personCount ?? null,
+ });
+ if (node.children && node.children.length) {
+ flattenDept(node.children, list);
+ }
});
- if (node.children && node.children.length) {
- flattenDept(node.children, list);
- }
- });
- return list;
-}
-
-const loadDeptList = () => {
- listDept().then((res) => {
- const tree = res.data ?? [];
- deptList.value = flattenDept(tree);
- });
-};
-
-const addInsuranceBenefit = () => {
- form.value.insuranceBenefits.push(defaultBenefit());
-};
-
-const removeInsuranceBenefit = (index) => {
- form.value.insuranceBenefits.splice(index, 1);
-};
-
-const handleUseBasicSalaryChange = (item) => {
- if (item.useBasicSalary) {
- item.paymentBase = "";
+ return list;
}
-};
-const handlePaymentBaseInput = (item) => {
- if (item.paymentBase !== "" && item.paymentBase != null) {
- item.useBasicSalary = false;
- }
-};
-
-const resetForm = () => {
- form.value = {
- id: undefined,
- title: "",
- remark: "",
- deptIds: [],
- insuranceBenefits: [defaultBenefit()],
+ const loadDeptList = () => {
+ listDept().then(res => {
+ const tree = res.data ?? [];
+ deptList.value = flattenDept(tree);
+ });
};
-};
-const openDialog = (type, row) => {
- operationType.value = type;
- dialogFormVisible.value = true;
- loadDeptList();
- resetForm();
- if ((type === "edit" || type === "detail") && row) {
- const d = row || {};
- form.value.id = d.id;
- form.value.title = d.title;
- form.value.remark = d.remark ?? "";
- // deptIds 鍚庣鍙兘鏄�楀彿鍒嗛殧瀛楃涓叉垨鏁扮粍锛岃繖閲岀粺涓�杞负鏁扮粍骞跺敖閲忚繕鍘熸暟鍊肩被鍨�
- if (d.deptIds) {
- form.value.deptIds = String(d.deptIds)
- .split(",")
- .filter((v) => v !== "")
- .map((v) => {
- const num = Number(v);
- return Number.isNaN(num) ? v : num;
- });
- } else {
- form.value.deptIds = [];
+ const addInsuranceBenefit = () => {
+ form.value.insuranceBenefits.push(defaultBenefit());
+ };
+
+ const removeInsuranceBenefit = index => {
+ form.value.insuranceBenefits.splice(index, 1);
+ };
+
+ const handleUseBasicSalaryChange = (item, index) => {
+ if (item.useBasicSalary) {
+ item.paymentBase = "";
}
- const detailList = d.schemeInsuranceDetailList || [];
- form.value.insuranceBenefits =
- detailList.length > 0
- ? detailList.map((b) => ({
- _key: Math.random().toString(36).slice(2),
- insuranceType: b.insuranceType || "",
- paymentBase: b.paymentBase ?? "",
- useBasicSalary: b.useBasicSalary === 2,
- personalRatio: b.personalRatio ?? "",
- personalFixed: b.personalFixed ?? "",
- }))
- : [defaultBenefit()];
- }
-};
+ nextTick(() => {
+ formRef.value?.validateField(`insuranceBenefits.${index}.paymentBase`);
+ });
+ };
-const submitForm = () => {
- // 璇︽儏妯″紡涓嬩笉鎻愪氦锛屽彧鍏抽棴寮圭獥
- if (operationType.value === "detail") {
- closeDia();
- return;
- }
- formRef.value?.validate((valid) => {
- if (!valid) return;
- const deptIds =
- Array.isArray(form.value.deptIds) && form.value.deptIds.length
- ? form.value.deptIds.join(",")
- : "";
- const schemeInsuranceDetailList = (form.value.insuranceBenefits || []).map(
- ({ _key, ...rest }) => ({
- ...rest,
- useBasicSalary: rest.useBasicSalary ? 2 : 1,
- })
- );
- const insuranceTypes = schemeInsuranceDetailList
- .map((item) => item.insuranceType)
- .filter((v) => v)
- .join(",");
- // 閮ㄩ棬鍚嶇О锛屽涓娇鐢ㄩ�楀彿闅斿紑锛堟牴鎹�変腑鐨� deptIds 涓� deptList 璁$畻锛�
- const deptNames = (deptList.value || [])
- .filter((d) =>
- (form.value.deptIds || []).some(
- (id) => String(id) === String(d.deptId)
- )
- )
- .map((d) => d.deptName)
- .join(",");
- const submitData = {
- id: form.value.id,
- title: form.value.title,
- remark: form.value.remark ?? "",
- deptIds,
- insuranceTypes,
- deptNames,
- schemeInsuranceDetailList,
+ const handlePaymentBaseInput = item => {
+ if (item.paymentBase !== "" && item.paymentBase != null) {
+ item.useBasicSalary = false;
+ }
+ };
+
+ const handlePersonalRatioInput = item => {
+ if (item.personalRatio !== "" && item.personalRatio != null) {
+ const value = Number(item.personalRatio);
+ if (value < 0) {
+ item.personalRatio = "";
+ }
+ }
+ };
+
+ const handlePersonalFixedInput = item => {
+ if (item.personalFixed !== "" && item.personalFixed != null) {
+ const value = Number(item.personalFixed);
+ if (value < 0) {
+ item.personalFixed = "";
+ }
+ }
+ };
+
+ const validatePaymentBase = (rule, value, callback) => {
+ const match = rule.field.match(/insuranceBenefits\.(\d+)\.paymentBase/);
+ if (match) {
+ const index = parseInt(match[1], 10);
+ const item = form.value.insuranceBenefits[index];
+ if (item && !item.useBasicSalary && (!value || value === "")) {
+ callback(new Error("璇疯緭鍏ョ即璐瑰熀鏁�"));
+ } else {
+ callback();
+ }
+ } else {
+ callback();
+ }
+ };
+
+ const resetForm = () => {
+ form.value = {
+ id: undefined,
+ title: "",
+ remark: "",
+ deptIds: [],
+ insuranceBenefits: [defaultBenefit()],
};
- if (operationType.value === "add") {
- socialSecurityAdd(submitData).then(() => {
- proxy.$modal.msgSuccess("鏂板鎴愬姛");
- closeDia();
- });
- } else {
- socialSecurityUpdate(submitData).then(() => {
- proxy.$modal.msgSuccess("淇敼鎴愬姛");
- closeDia();
- });
+ };
+
+ const openDialog = (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ loadDeptList();
+ resetForm();
+ if ((type === "edit" || type === "detail") && row) {
+ const d = row || {};
+ form.value.id = d.id;
+ form.value.title = d.title;
+ form.value.remark = d.remark ?? "";
+ // deptIds 鍚庣鍙兘鏄�楀彿鍒嗛殧瀛楃涓叉垨鏁扮粍锛岃繖閲岀粺涓�杞负鏁扮粍骞跺敖閲忚繕鍘熸暟鍊肩被鍨�
+ if (d.deptIds) {
+ form.value.deptIds = String(d.deptIds)
+ .split(",")
+ .filter(v => v !== "")
+ .map(v => {
+ const num = Number(v);
+ return Number.isNaN(num) ? v : num;
+ });
+ } else {
+ form.value.deptIds = [];
+ }
+ const detailList = d.schemeInsuranceDetailList || [];
+ form.value.insuranceBenefits =
+ detailList.length > 0
+ ? detailList.map(b => ({
+ _key: Math.random().toString(36).slice(2),
+ insuranceType: b.insuranceType || "",
+ paymentBase: b.paymentBase ?? "",
+ useBasicSalary: b.useBasicSalary === 2,
+ personalRatio: b.personalRatio ?? "",
+ personalFixed: b.personalFixed ?? "",
+ }))
+ : [defaultBenefit()];
}
- });
-};
+ };
-const closeDia = () => {
- proxy.resetForm?.("formRef");
- dialogFormVisible.value = false;
- emit("close");
-};
+ const submitForm = () => {
+ // 璇︽儏妯″紡涓嬩笉鎻愪氦锛屽彧鍏抽棴寮圭獥
+ if (operationType.value === "detail") {
+ closeDia();
+ return;
+ }
+ formRef.value?.validate(valid => {
+ if (!valid) return;
+ const deptIds =
+ Array.isArray(form.value.deptIds) && form.value.deptIds.length
+ ? form.value.deptIds.join(",")
+ : "";
+ const schemeInsuranceDetailList = (form.value.insuranceBenefits || []).map(
+ ({ _key, ...rest }) => ({
+ ...rest,
+ useBasicSalary: rest.useBasicSalary ? 2 : 1,
+ })
+ );
+ const insuranceTypes = schemeInsuranceDetailList
+ .map(item => item.insuranceType)
+ .filter(v => v)
+ .join(",");
+ // 閮ㄩ棬鍚嶇О锛屽涓娇鐢ㄩ�楀彿闅斿紑锛堟牴鎹�変腑鐨� deptIds 涓� deptList 璁$畻锛�
+ const deptNames = (deptList.value || [])
+ .filter(d =>
+ (form.value.deptIds || []).some(id => String(id) === String(d.deptId))
+ )
+ .map(d => d.deptName)
+ .join(",");
+ const submitData = {
+ id: form.value.id,
+ title: form.value.title,
+ remark: form.value.remark ?? "",
+ deptIds,
+ insuranceTypes,
+ deptNames,
+ schemeInsuranceDetailList,
+ };
+ if (operationType.value === "add") {
+ socialSecurityAdd(submitData).then(() => {
+ proxy.$modal.msgSuccess("鏂板鎴愬姛");
+ closeDia();
+ });
+ } else {
+ socialSecurityUpdate(submitData).then(() => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛");
+ closeDia();
+ });
+ }
+ });
+ };
-defineExpose({ openDialog });
+ const closeDia = () => {
+ proxy.resetForm?.("formRef");
+ dialogFormVisible.value = false;
+ emit("close");
+ };
+
+ defineExpose({ openDialog });
</script>
<style scoped>
-.card-title-line {
- color: #f56c6c;
- margin-right: 4px;
-}
-.form-card {
- margin-bottom: 16px;
-}
-.form-card :deep(.el-card__header) {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 12px 16px;
-}
-.card-title {
- font-weight: 500;
-}
-.card-collapse {
- color: #999;
- cursor: pointer;
-}
-.dept-checkbox-wrap {
- max-height: 320px;
- overflow-y: auto;
- padding: 8px 0;
- border: 1px solid var(--el-border-color);
- border-radius: 4px;
- background: #fff;
-}
-.dept-checkbox-item {
- padding: 6px 12px;
-}
-.dept-count {
- color: #909399;
- font-size: 12px;
- margin-left: 4px;
-}
-.insurance-benefit-card {
- border: 1px solid var(--el-border-color-lighter);
- border-radius: 4px;
- padding: 12px 16px;
- margin-bottom: 12px;
- background: #fafafa;
-}
-.insurance-benefit-title {
- font-size: 14px;
- margin-bottom: 12px;
- font-weight: 500;
- display: flex;
- align-items: center;
- justify-content: space-between;
-}
-.card-delete-btn {
- margin-left: auto;
-}
-.checkbox-group-inline {
- display: flex;
- flex-wrap: wrap;
- gap: 16px;
-}
-.base-salary-wrap {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: 8px;
-}
-.base-salary-text {
- color: #606266;
- font-size: 14px;
-}
-.personal-ratio-wrap {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-.ratio-unit,
-.ratio-plus {
- color: #606266;
- font-size: 14px;
-}
-</style>
+ .card-title-line {
+ color: #f56c6c;
+ margin-right: 4px;
+ }
+ .form-card {
+ margin-bottom: 16px;
+ }
+ .form-card :deep(.el-card__header) {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 16px;
+ }
+ .card-title {
+ font-weight: 500;
+ }
+ .card-collapse {
+ color: #999;
+ cursor: pointer;
+ }
+ .dept-checkbox-wrap {
+ max-height: 320px;
+ overflow-y: auto;
+ padding: 8px 0;
+ border: 1px solid var(--el-border-color);
+ border-radius: 4px;
+ background: #fff;
+ }
+ .dept-checkbox-item {
+ padding: 6px 12px;
+ }
+ .dept-count {
+ color: #909399;
+ font-size: 12px;
+ margin-left: 4px;
+ }
+ .insurance-benefit-card {
+ border: 1px solid var(--el-border-color-lighter);
+ border-radius: 4px;
+ padding: 12px 16px;
+ margin-bottom: 12px;
+ background: #fafafa;
+ }
+ .insurance-benefit-title {
+ font-size: 14px;
+ margin-bottom: 12px;
+ font-weight: 500;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+ .card-delete-btn {
+ margin-left: auto;
+ }
+ .checkbox-group-inline {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 16px;
+ }
+ .base-salary-wrap {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 8px;
+ }
+ .base-salary-text {
+ color: #606266;
+ font-size: 14px;
+ }
+ .personal-ratio-wrap {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+ .ratio-unit,
+ .ratio-plus {
+ color: #606266;
+ font-size: 14px;
+ }
+</style>
\ No newline at end of file
diff --git a/src/views/procurementManagement/procurementLedger/index.vue b/src/views/procurementManagement/procurementLedger/index.vue
index cbd6e23..d94b593 100644
--- a/src/views/procurementManagement/procurementLedger/index.vue
+++ b/src/views/procurementManagement/procurementLedger/index.vue
@@ -66,6 +66,9 @@
</div>
<div class="table_list">
<div style="display: flex;justify-content: flex-end;margin-bottom: 20px;">
+ <el-button type="success"
+ plain
+ @click="handleBatchGenerate">鎵归噺鐢熸垚鏁版嵁</el-button>
<el-button type="primary"
@click="openForm('add')">鏂板鍙拌处</el-button>
<el-button type="primary"
@@ -106,7 +109,7 @@
prop="specificationModel" />
<el-table-column label="鍗曚綅"
prop="unit" />
- <el-table-column label="鍏ュ簱瀹℃牳鐘舵��"
+ <el-table-column label="鍏ュ簱瀹℃牳鐘舵��"
prop="stockInApprovalStatus"
width="120">
<template #default="scope">
@@ -121,7 +124,7 @@
<el-table-column label="鍙敤鏁伴噺"
prop="availableQuality" />
<el-table-column label="閫�璐ф暟閲�"
- prop="returnQuality" />
+ prop="returnQuantity" />
<el-table-column label="绋庣巼(%)"
prop="taxRate" />
<el-table-column label="鍚◣鍗曚环(鍏�)"
@@ -721,6 +724,7 @@
getOptions,
getPurchaseTemplateList,
delPurchaseTemplate,
+ batchGeneratePurchaseInboundSteps,
} from "@/api/procurementManagement/procurementLedger.js";
import useFormData from "@/hooks/useFormData.js";
const FileList = defineAsyncComponent(() =>
@@ -776,9 +780,9 @@
// 鑾峰彇鍏ュ簱鐘舵�佹爣绛剧被鍨�
const getStockInStatusType = status => {
const typeMap = {
- "寰呭叆搴�": "info", // 寰呭叆搴� - 鐏拌壊
- "鍏ュ簱涓�": "warning", // 鍏ュ簱涓� - 姗欒壊
- "瀹屽叏鍏ュ簱": "success", // 瀹屽叏鍏ュ簱 - 缁胯壊
+ 寰呭叆搴�: "info", // 寰呭叆搴� - 鐏拌壊
+ 鍏ュ簱涓�: "warning", // 鍏ュ簱涓� - 姗欒壊
+ 瀹屽叏鍏ュ簱: "success", // 瀹屽叏鍏ュ簱 - 缁胯壊
};
return typeMap[status] || "";
};
@@ -786,9 +790,9 @@
// 鑾峰彇鍏ュ簱瀹℃牳鐘舵�佹爣绛剧被鍨�
const getStockInApprovalStatusType = status => {
const typeMap = {
- "寰呭叆搴�": "info", // 寰呭叆搴� - 鐏拌壊
- "鍏ュ簱涓�": "warning", // 鍏ュ簱涓� - 姗欒壊
- "瀹屽叏鍏ュ簱": "success", // 瀹屽叏鍏ュ簱 - 缁胯壊
+ 寰呭叆搴�: "info", // 寰呭叆搴� - 鐏拌壊
+ 鍏ュ簱涓�: "warning", // 鍏ュ簱涓� - 姗欒壊
+ 瀹屽叏鍏ュ簱: "success", // 瀹屽叏鍏ュ簱 - 缁胯壊
};
return typeMap[status] || "";
};
@@ -1274,7 +1278,7 @@
const openForm = async (type, row) => {
// 缂栬緫鏃舵鏌ュ叆搴撶姸鎬侊紝瀹屽叏鍏ュ簱鏃朵笉鑳界紪杈�
if (type === "edit" && row) {
- if (row.stockInStatus === '瀹屽叏鍏ュ簱') {
+ if (row.stockInStatus === "瀹屽叏鍏ュ簱") {
proxy.$modal.msgWarning("瀹屽叏鍏ュ簱鐘舵�佺殑璁板綍涓嶈兘缂栬緫");
return;
}
@@ -1314,7 +1318,10 @@
form.value = { ...purchaseRes, stockInStatus: row.stockInStatus };
fileList.value = purchaseRes.storageBlobVOS || [];
// 浣跨敤 productList 鎺ュ彛鑾峰彇浜у搧鍒楄〃锛屼互鑾峰彇鍏ュ簱瀹℃牳鐘舵��
- const productRes = await productList({ salesLedgerId: row.id, type: 2 });
+ const productRes = await productList({
+ salesLedgerId: row.id,
+ type: 2,
+ });
productData.value = productRes.data || [];
} catch (error) {
console.error("鍔犺浇閲囪喘鍙拌处鏁版嵁澶辫触:", error);
@@ -1447,7 +1454,7 @@
// 鎵撳紑浜у搧寮规
const openProductForm = async (type, row, index) => {
// 缂栬緫鏃舵鏌ヤ骇鍝佸叆搴撳鏍哥姸鎬侊紝瀹屽叏鍏ュ簱鏃朵笉鑳界紪杈�
- if (type === "edit" && row && row.stockInApprovalStatus === '瀹屽叏鍏ュ簱') {
+ if (type === "edit" && row && row.stockInApprovalStatus === "瀹屽叏鍏ュ簱") {
proxy.$modal.msgWarning("瀹屽叏鍏ュ簱鐘舵�佺殑浜у搧涓嶈兘缂栬緫");
return;
}
@@ -1636,7 +1643,7 @@
}
// 妫�鏌ラ�変腑鐨勪骇鍝佷腑鏄惁鏈夊畬鍏ㄥ叆搴撶殑
const hasFullyStocked = productSelectedRows.value.some(
- row => row.stockInApprovalStatus === '瀹屽叏鍏ュ簱'
+ row => row.stockInApprovalStatus === "瀹屽叏鍏ュ簱"
);
if (hasFullyStocked) {
proxy.$modal.msgWarning("閫変腑鐨勪骇鍝佷腑鍖呭惈瀹屽叏鍏ュ簱鐨勪骇鍝侊紝鏃犳硶鍒犻櫎");
@@ -1712,6 +1719,37 @@
proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
getList();
});
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+
+ const handleBatchGenerate = async () => {
+ if (selectedRows.value.length === 0) {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ const ids = selectedRows.value.map((item) => item.id);
+
+ ElMessageBox.confirm("纭鎵归噺鐢熸垚鏁版嵁锛�", "鎵归噺鐢熸垚", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "info",
+ })
+ .then(() => {
+ proxy.$modal.loading("姝e湪鎵归噺鐢熸垚鏁版嵁锛岃绋嶅��...");
+ batchGeneratePurchaseInboundSteps({ ids })
+ .then((res) => {
+ proxy.$modal.msgSuccess("鎵归噺鐢熸垚鎴愬姛");
+ getList();
+ })
+ .catch(() => {
+ proxy.$modal.msgError("鎵归噺鐢熸垚澶辫触");
+ })
+ .finally(() => {
+ proxy.$modal.closeLoading();
+ });
})
.catch(() => {
proxy.$modal.msg("宸插彇娑�");
@@ -1855,7 +1893,7 @@
contractNo: form.value.salesLedgerId,
});
if (code == 200) {
- productData.value = data;
+ productData.value = data || [];
}
};
diff --git a/src/views/procurementManagement/procurementPlan/index.vue b/src/views/procurementManagement/procurementPlan/index.vue
index 5be6ffe..ba9a38c 100644
--- a/src/views/procurementManagement/procurementPlan/index.vue
+++ b/src/views/procurementManagement/procurementPlan/index.vue
@@ -183,7 +183,7 @@
</el-col>
<el-col :span="12">
<el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
- <el-date-picker v-model="formData.createTime"
+ <el-date-picker v-model="formCreateTimeDate"
type="date"
placeholder="閫夋嫨鏃ユ湡"
value-format="YYYY-MM-DD"
@@ -377,7 +377,8 @@
<script setup>
import FormDialog from "@/components/Dialog/FormDialog.vue";
- import { ref, reactive, onMounted, getCurrentInstance } from "vue";
+ import { ref, reactive, onMounted, getCurrentInstance, computed } from "vue";
+ import dayjs from "dayjs";
import { ElMessage, ElMessageBox } from "element-plus";
import { Search, Refresh, Plus, Download } from "@element-plus/icons-vue";
import {
@@ -432,6 +433,12 @@
summaryMaterial: false,
summaryAuxAttributes: false,
summaryDemandDate: false,
+ });
+ const formCreateTimeDate = computed({
+ get: () => (formData.createTime ? String(formData.createTime).split(" ")[0] : ""),
+ set: (value) => {
+ formData.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : "";
+ },
});
// 褰撳墠婵�娲荤殑鏍囩椤�
@@ -510,7 +517,7 @@
const handleAdd = () => {
dialogType.value = "add";
resetForm();
- formData.createTime = new Date().toISOString().split("T")[0];
+ formData.createTime = dayjs().format("YYYY-MM-DD HH:mm:ss");
dialogVisible.value = true;
};
diff --git a/src/views/procurementManagement/purchaseOrder/index.vue b/src/views/procurementManagement/purchaseOrder/index.vue
index 71d6caa..4f9812f 100644
--- a/src/views/procurementManagement/purchaseOrder/index.vue
+++ b/src/views/procurementManagement/purchaseOrder/index.vue
@@ -57,7 +57,7 @@
</el-select>
</el-form-item>
<el-form-item label="鍒涘缓鏃堕棿">
- <el-date-picker v-model="formData.createTime"
+ <el-date-picker v-model="formCreateTimeDate"
type="date"
placeholder="閫夋嫨鏃ユ湡"
value-format="YYYY-MM-DD"
@@ -73,7 +73,8 @@
<script setup>
import FormDialog from '@/components/Dialog/FormDialog.vue';
-import { ref, reactive } from 'vue'
+import { ref, reactive, computed } from 'vue'
+import dayjs from 'dayjs'
import { ElMessage, ElMessageBox } from 'element-plus'
const loading = ref(false)
@@ -90,6 +91,12 @@
supplierName: '',
remark: '',
createTime: ''
+})
+const formCreateTimeDate = computed({
+ get: () => (formData.createTime ? String(formData.createTime).split(' ')[0] : ''),
+ set: (value) => {
+ formData.createTime = value ? `${value} ${dayjs().format('HH:mm:ss')}` : ''
+ }
})
const mockData = [
@@ -130,9 +137,9 @@
const openDialog = (type, row = {}) => {
dialogType.value = type
if (type === 'edit' && row.id) {
- Object.assign(formData, { supplierName: row.supplierName, remark: row.remark })
+ Object.assign(formData, { supplierName: row.supplierName, remark: row.remark, createTime: row.createTime || '' })
} else {
- Object.assign(formData, { supplierName: '', remark: '', createTime: new Date().toISOString().split('T')[0] })
+ Object.assign(formData, { supplierName: '', remark: '', createTime: dayjs().format('YYYY-MM-DD HH:mm:ss') })
}
dialogVisible.value = true
}
diff --git a/src/views/procurementManagement/qualityInspection/index.vue b/src/views/procurementManagement/qualityInspection/index.vue
index fde8c68..aee1d99 100644
--- a/src/views/procurementManagement/qualityInspection/index.vue
+++ b/src/views/procurementManagement/qualityInspection/index.vue
@@ -119,7 +119,7 @@
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="鍒涘缓鏃堕棿">
- <el-date-picker v-model="formData.createTime"
+ <el-date-picker v-model="formCreateTimeDate"
type="date"
placeholder="閫夋嫨鏃ユ湡"
value-format="YYYY-MM-DD"
@@ -138,7 +138,8 @@
<script setup>
import FormDialog from '@/components/Dialog/FormDialog.vue';
-import { ref, reactive } from 'vue'
+import { ref, reactive, computed } from 'vue'
+import dayjs from 'dayjs'
import { ElMessage, ElMessageBox } from 'element-plus'
const loading = ref(false)
@@ -158,6 +159,12 @@
inspector: '',
remark: '',
createTime: ''
+})
+const formCreateTimeDate = computed({
+ get: () => (formData.createTime ? String(formData.createTime).split(' ')[0] : ''),
+ set: (value) => {
+ formData.createTime = value ? `${value} ${dayjs().format('HH:mm:ss')}` : ''
+ }
})
const mockData = [
@@ -203,7 +210,8 @@
arrivalNo: row.arrivalNo,
supplierName: row.supplierName,
inspector: row.inspector,
- remark: row.remark
+ remark: row.remark,
+ createTime: row.createTime || ''
})
} else {
Object.assign(formData, {
@@ -212,7 +220,7 @@
products: [],
inspector: '',
remark: '',
- createTime: new Date().toISOString().split('T')[0]
+ createTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
})
}
dialogVisible.value = true
diff --git a/src/views/productManagement/productIdentifier/index.vue b/src/views/productManagement/productIdentifier/index.vue
index 2eceb31..d638e7a 100644
--- a/src/views/productManagement/productIdentifier/index.vue
+++ b/src/views/productManagement/productIdentifier/index.vue
@@ -240,7 +240,7 @@
<span>{{ currentProduct.identifierType }}</span>
</el-form-item>
<el-form-item label="鍒涘缓鏃堕棿">
- <el-date-picker v-model="createTime"
+ <el-date-picker v-model="createTimeDate"
type="date"
placeholder="閫夋嫨鏃ユ湡"
value-format="YYYY-MM-DD"
@@ -363,6 +363,7 @@
<script setup>
import { ref, reactive, computed } from "vue";
+ import dayjs from "dayjs";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus, Search, Loading, Download } from "@element-plus/icons-vue";
import Pagination from "@/components/PIMTable/Pagination.vue";
@@ -449,7 +450,13 @@
const generateQuantity = ref(1);
const codeRule = ref("");
const customPrefix = ref("");
- const createTime = ref(new Date().toISOString().split('T')[0]);
+ const createTime = ref(dayjs().format("YYYY-MM-DD HH:mm:ss"));
+ const createTimeDate = computed({
+ get: () => (createTime.value ? String(createTime.value).split(" ")[0] : ""),
+ set: (value) => {
+ createTime.value = value ? `${value} ${dayjs().format("HH:mm:ss")}` : "";
+ },
+ });
const newBatchNo = ref("");
const reassignReason = ref("");
const formRef = ref();
diff --git a/src/views/productionManagement/productStructure/DetailNew/MaterialCard.vue b/src/views/productionManagement/productStructure/DetailNew/MaterialCard.vue
new file mode 100644
index 0000000..f0ac5a6
--- /dev/null
+++ b/src/views/productionManagement/productStructure/DetailNew/MaterialCard.vue
@@ -0,0 +1,236 @@
+<template>
+ <div class="material-node">
+ <!-- 褰撳墠鑺傜偣鍗$墖 -->
+ <div :class="['node-card', isRoot ? 'root-card' : (row.nodeType === 'semiFinished' ? 'semi-finished-card' : 'child-card')]">
+ <div class="node-header">
+ <div class="node-label">
+ <el-tag :type="isRoot ? '' : (row.nodeType === 'semiFinished' ? 'warning' : 'success')" size="small" effect="dark">
+ {{ isRoot ? '鎴愬搧' : (row.nodeType === 'semiFinished' ? '鍗婃垚鍝�' : '鍘熸枡') }}
+ </el-tag>
+ <span class="node-title">{{ row.productName || '鏈�夋嫨浜у搧' }}</span>
+ <span v-if="row.model" class="node-sub">瑙勬牸: {{ row.model }}</span>
+ <span v-if="row.unit" class="node-sub">鍗曚綅: {{ row.unit }}</span>
+ </div>
+ <div class="node-actions">
+ <template v-if="editable && (isRoot || row.nodeType === 'semiFinished')">
+ <el-button type="primary"
+ text
+ size="small"
+ @click="handleAdd('semiFinished')">
+ + 娣诲姞鍗婃垚鍝�
+ </el-button>
+ <el-button type="primary"
+ text
+ size="small"
+ @click="handleAdd('rawMaterial')">
+ + 娣诲姞鍘熸枡
+ </el-button>
+ </template>
+ <el-button v-if="editable"
+ type="danger"
+ text
+ size="small"
+ @click="$emit('remove', row.tempId)">
+ 鍒犻櫎
+ </el-button>
+ </div>
+ </div>
+
+ <!-- 缂栬緫妯″紡涓嬬殑琛ㄥ崟 -->
+ <div v-if="editable" class="node-body">
+ <el-row :gutter="12">
+ <el-col :span="7">
+ <el-form-item label="浜у搧" :rules="[{ required: true, message: '璇烽�夋嫨浜у搧' }]" style="margin:0">
+ <el-input :model-value="row.productName || ''"
+ readonly
+ placeholder="鐐瑰嚮閫夋嫨浜у搧"
+ @click="openSelect"
+ style="width:100%">
+ <template #suffix>
+ <el-icon><component :is="SearchIcon" /></el-icon>
+ </template>
+ </el-input>
+ </el-form-item>
+ </el-col>
+ <el-col :span="5">
+ <el-form-item label="瑙勬牸" style="margin:0">
+ <el-select v-model="row.model"
+ placeholder="璇烽�夋嫨瑙勬牸"
+ clearable
+ style="width:100%"
+ @visible-change="(v:boolean) => { if (v) openSelect() }">
+ <el-option v-if="row.model" :label="row.model" :value="row.model" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col v-if="!isRoot" :span="5">
+ <el-form-item label="宸ュ簭" :rules="[{ required: true, message: '璇烽�夋嫨宸ュ簭' }]" style="margin:0">
+ <el-select v-model="row.processId"
+ placeholder="璇烽�夋嫨"
+ filterable
+ clearable
+ style="width:100%"
+ @change="(v:any) => $emit('processChange', row, v)">
+ <el-option v-for="item in processOptions"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="鏁伴噺" :rules="[{ required: true, message: '璇峰~鍐欐暟閲�' }]" style="margin:0">
+ <el-input-number v-model="row.unitQuantity"
+ :min="0"
+ :precision="2"
+ :step="1"
+ controls-position="right"
+ style="width:100%"
+ @change="$emit('quantityChange')" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="3">
+ <el-form-item label="鍗曚綅" style="margin:0">
+ <el-input v-model="row.unit" placeholder="鍗曚綅" clearable style="width:100%" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </div>
+
+ <!-- 闈炵紪杈戞ā寮忥細绠�娲佹樉绀� -->
+ <div v-else class="node-view">
+ <span v-if="!isRoot && row.processName">宸ュ簭: {{ row.processName }} | </span>
+ <span>鏁伴噺: {{ row.unitQuantity || '-' }}</span>
+ </div>
+ </div>
+
+ <!-- 閫掑綊娓叉煋瀛愯妭鐐� -->
+ <div v-if="row.children && row.children.length > 0" class="node-children">
+ <MaterialCard
+ v-for="child in row.children"
+ :key="child.tempId"
+ :row="child"
+ :depth="depth + 1"
+ :editable="editable"
+ :process-options="processOptions"
+ @remove="(id:string) => $emit('remove', id)"
+ @add="(id:string, nodeType:string) => $emit('add', id, nodeType)"
+ @select-product="(tempId: string, data: any) => $emit('selectProduct', tempId, data)"
+ @process-change="(row: any, v: any) => $emit('processChange', row, v)"
+ @quantity-change="$emit('quantityChange')"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+import { Search } from '@element-plus/icons-vue'
+
+const SearchIcon = Search
+
+const props = defineProps<{
+ row: any
+ depth: number
+ editable: boolean
+ processOptions: any[]
+}>()
+
+const emit = defineEmits<{
+ remove: [tempId: string]
+ add: [tempId: string, nodeType: string]
+ selectProduct: [tempId: string, data: any]
+ processChange: [row: any, value: any]
+ quantityChange: []
+}>()
+
+const isRoot = computed(() => props.depth === 0)
+
+const openSelect = () => {
+ emit('selectProduct', props.row.tempId, null)
+}
+
+const handleAdd = (nodeType: string) => {
+ emit('add', props.row.tempId, nodeType)
+}
+</script>
+
+<script lang="ts">
+export default { name: 'MaterialCard' }
+</script>
+
+<style scoped>
+.material-node {
+ margin: 4px 0;
+}
+
+.node-card {
+ border: 1px solid #e4e7ed;
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+.root-card {
+ border-color: #409eff;
+ border-left: 4px solid #409eff;
+ background-color: #f0f5ff;
+}
+
+.child-card {
+ border-left: 4px solid #67c23a;
+ background-color: #f0f9eb;
+}
+
+.semi-finished-card {
+ border-left: 4px solid #e6a23c;
+ background-color: #fdf6ec;
+}
+
+.node-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 12px;
+ background-color: rgba(0,0,0,0.03);
+ flex-wrap: wrap;
+ gap: 4px;
+}
+
+.node-label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+.node-title {
+ font-weight: 600;
+ color: #303133;
+}
+
+.node-sub {
+ font-size: 12px;
+ color: #909399;
+}
+
+.node-actions {
+ display: flex;
+ gap: 4px;
+}
+
+.node-body {
+ padding: 10px 12px;
+}
+
+.node-view {
+ padding: 6px 12px;
+ font-size: 13px;
+ color: #606266;
+}
+
+.node-children {
+ margin-left: 36px;
+ padding-left: 16px;
+ border-left: 2px dashed #dcdfe6;
+}
+</style>
diff --git a/src/views/productionManagement/productStructure/DetailNew/index.vue b/src/views/productionManagement/productStructure/DetailNew/index.vue
new file mode 100644
index 0000000..1262c80
--- /dev/null
+++ b/src/views/productionManagement/productStructure/DetailNew/index.vue
@@ -0,0 +1,626 @@
+<template>
+ <div class="app-container">
+ <PageHeader content="浜у搧缁撴瀯璇︽儏">
+ <template #right-button>
+ <el-button v-if="!dataValue.isEdit && !isOrderPage"
+ type="primary"
+ @click="dataValue.isEdit = true">缂栬緫
+ </el-button>
+ <el-button v-if="dataValue.isEdit && !isOrderPage"
+ type="primary"
+ @click="cancelEdit">鍙栨秷
+ </el-button>
+ <el-button v-if="!isOrderPage"
+ type="primary"
+ :loading="dataValue.loading"
+ @click="submit"
+ :disabled="!dataValue.isEdit">纭
+ </el-button>
+ </template>
+ </PageHeader>
+ <el-table :data="tableData"
+ border
+ :preserve-expanded-content="false"
+ :default-expand-all="true"
+ style="width: 100%">
+ <el-table-column type="expand">
+ <template #default="props">
+ <el-form ref="form" :model="dataValue">
+ <div class="tree-container">
+ <div class="tree-legend">
+ <el-tag type="" size="small" effect="dark">鎴愬搧</el-tag>
+ <span style="margin:0 4px">鈫� 鏈�涓婂眰锛堜骇鍑虹墿锛�</span>
+ <el-divider direction="vertical" />
+ <el-tag type="warning" size="small" effect="dark">鍗婃垚鍝�</el-tag>
+ <span style="margin:0 4px">锛堝彲缁х画灞曞紑锛�</span>
+ <el-divider direction="vertical" />
+ <span style="margin:0 4px">鏈�涓嬪眰锛堟姇鍏ョ墿锛夆啋</span>
+ <el-tag type="success" size="small" effect="dark">鍘熸枡</el-tag>
+ </div>
+
+ <div v-if="dataValue.dataList.length === 0 && dataValue.isEdit" class="empty-hint">
+ 璇风偣鍑讳笅鏂规寜閽坊鍔犳垚鍝�
+ </div>
+
+ <MaterialCard
+ v-for="(item, index) in dataValue.dataList"
+ :key="item.tempId"
+ :row="item"
+ :depth="0"
+ :editable="dataValue.isEdit"
+ :process-options="dataValue.processOptions"
+ @remove="(id: string) => removeItem(id)"
+ @add="(id: string, nodeType: string) => addChildItem(id, nodeType)"
+ @select-product="(tempId: string, _data: any) => { dataValue.currentRowName = tempId; dataValue.showProductDialog = true }"
+ @process-change="(row: any, v: any) => handleProcessChange(row, v)"
+ @quantity-change="handleUnitQuantityChange"
+ />
+
+ <el-button v-if="dataValue.isEdit"
+ type="primary"
+ plain
+ style="margin-top:12px"
+ @click="addRootItem">
+ + 娣诲姞鎴愬搧
+ </el-button>
+ </div>
+ </el-form>
+ </template>
+ </el-table-column>
+ <el-table-column label="BOM缂栧彿"
+ prop="bomNo" />
+ <el-table-column label="浜у搧鍚嶇О"
+ prop="productName" />
+ <el-table-column label="瑙勬牸鍨嬪彿"
+ prop="model" />
+ </el-table>
+ <product-select-dialog v-if="dataValue.showProductDialog"
+ v-model:model-value="dataValue.showProductDialog"
+ :single="true"
+ @confirm="handleProduct" />
+ </div>
+</template>
+
+<script setup lang="ts">
+ import {
+ computed,
+ defineAsyncComponent,
+ defineComponent,
+ onMounted,
+ reactive,
+ ref,
+ } from "vue";
+ import {
+ queryList,
+ addBomDetail,
+ } from "@/api/productionManagement/productStructure.js";
+ import { listProcessBom } from "@/api/productionManagement/productionOrder.js";
+ import { list } from "@/api/productionManagement/productionProcess";
+ import { ElMessage } from "element-plus";
+ import { useRoute, useRouter } from "vue-router";
+
+ defineComponent({
+ name: "StructureEdit",
+ });
+
+ const ProductSelectDialog = defineAsyncComponent(
+ () => import("@/views/basicData/product/ProductSelectDialog.vue")
+ );
+ import MaterialCard from "./MaterialCard.vue";
+ const emit = defineEmits(["update:router"]);
+ const form = ref();
+
+ const route = useRoute();
+ const router = useRouter();
+ const routeId = computed({
+ get() {
+ return route.query.id;
+ },
+
+ set(val) {
+ emit("update:router", val);
+ },
+ });
+
+ // 浠庤矾鐢卞弬鏁拌幏鍙栦骇鍝佷俊鎭�
+ const routeBomNo = computed(() => route.query.bomNo || "");
+ const routeProductName = computed(() => route.query.productName || "");
+ const routeProductModelName = computed(
+ () => route.query.productModelName || ""
+ );
+ const routeOrderId = computed(() => route.query.orderId);
+ const pageType = computed(() => route.query.type);
+ const isOrderPage = computed(
+ () => pageType.value === "order" && routeOrderId.value
+ );
+
+ const dataValue = reactive({
+ dataList: [],
+ productOptions: [],
+ processOptions: [],
+ showProductDialog: false,
+ currentRowIndex: null,
+ currentRowName: null,
+ loading: false,
+ isEdit: false,
+ });
+
+ const normalizeListData = (source: any) => {
+ if (Array.isArray(source)) {
+ return source;
+ }
+ if (Array.isArray(source?.records)) {
+ return source.records;
+ }
+ return [];
+ };
+
+ const getProcessOptionById = (id: any) => {
+ if (id === undefined || id === null || id === "") {
+ return null;
+ }
+ return (
+ normalizeListData(dataValue.processOptions).find(
+ option => String(option.id) === String(id)
+ ) || null
+ );
+ };
+
+ const syncProcessOperationFields = (item: any) => {
+ const processId = item.processId ?? item.operationId ?? "";
+ if (!processId) {
+ item.processId = "";
+ item.operationId = "";
+ item.processName = "";
+ item.operationName = "";
+ return;
+ }
+
+ const option = getProcessOptionById(processId);
+ const processName =
+ option?.name || item.processName || item.operationName || "";
+
+ item.processId = processId;
+ item.operationId = processId;
+ item.processName = processName;
+ item.operationName = processName;
+ };
+
+ const normalizeTreeData = (items: any[], depth: number = 0) => {
+ items.forEach((item: any) => {
+ item.tempId = item.tempId || item.id || `${Date.now()}_${Math.random()}`;
+ syncProcessOperationFields(item);
+ if (depth > 0 && !item.nodeType) {
+ item.nodeType = Array.isArray(item.children) && item.children.length > 0
+ ? 'semiFinished'
+ : 'rawMaterial';
+ }
+ if (Array.isArray(item.children) && item.children.length > 0) {
+ normalizeTreeData(item.children, depth + 1);
+ }
+ });
+ };
+
+ const toQuantityNumber = (value: any) => {
+ const numberValue = Number(value);
+ if (!Number.isFinite(numberValue)) {
+ return 0;
+ }
+ return Number(numberValue.toFixed(2));
+ };
+
+ const syncDemandedQuantityTree = (
+ items: any[],
+ parentDemandedQuantity: number | null = null
+ ) => {
+ items.forEach((item: any) => {
+ if (parentDemandedQuantity !== null) {
+ item.demandedQuantity = toQuantityNumber(
+ parentDemandedQuantity * toQuantityNumber(item.unitQuantity)
+ );
+ }
+
+ if (Array.isArray(item.children) && item.children.length > 0) {
+ syncDemandedQuantityTree(
+ item.children,
+ toQuantityNumber(item.demandedQuantity)
+ );
+ }
+ });
+ };
+
+ const recalculateDemandedQuantities = () => {
+ if (!isOrderPage.value) {
+ return;
+ }
+
+ syncDemandedQuantityTree(dataValue.dataList);
+ };
+
+ const buildSubmitTree = (items: any[]) => {
+ return items.map((item: any) => {
+ const current = { ...item };
+ syncProcessOperationFields(current);
+ current.children = Array.isArray(current.children)
+ ? buildSubmitTree(current.children)
+ : [];
+ return current;
+ });
+ };
+
+ const findSiblings = (items: any[], tempId: string): any[] | null => {
+ if (!items || items.length === 0) return null;
+ // 妫�鏌ュ綋鍓嶅眰绾�
+ if (items.some(item => item.tempId === tempId)) {
+ return items;
+ }
+ // 閫掑綊鏌ユ壘瀛愮骇
+ for (const item of items) {
+ if (item.children && item.children.length > 0) {
+ const result = findSiblings(item.children, tempId);
+ if (result) return result;
+ }
+ }
+ return null;
+ };
+
+ const handleProcessChange = (row: any, value: any) => {
+ row.processId = value || "";
+ syncProcessOperationFields(row);
+
+ // 妫�鏌ュ悓涓�灞傜骇鏄惁宸茬粡鏈夊叾浠栦笉鍚岀殑宸ュ簭琚�変腑
+ const siblings = findSiblings(dataValue.dataList, row.tempId);
+ if (siblings && value) {
+ const hasDifferentProcess = siblings.some(sibling => {
+ return sibling.tempId !== row.tempId && sibling.processId && sibling.processId !== value;
+ });
+ if (hasDifferentProcess) {
+ ElMessage.warning("鍚屼竴灞傜骇宸插瓨鍦ㄤ笉鍚岀殑宸ュ簭锛岃鍏堢粺涓�宸ュ簭鍚庡啀杩涜淇敼");
+ }
+ }
+ };
+
+ const handleUnitQuantityChange = () => {
+ recalculateDemandedQuantities();
+ };
+
+ const tableData = reactive([
+ {
+ productName: "",
+ model: "",
+ bomNo: "",
+ },
+ ]);
+
+ const openDialog = (tempId: any) => {
+ console.log(tempId, "tempId");
+ dataValue.currentRowName = tempId;
+ dataValue.showProductDialog = true;
+ };
+
+ const fetchData = async () => {
+ if (isOrderPage.value) {
+ // 璁㈠崟鎯呭喌锛氫娇鐢ㄨ鍗曠殑浜у搧缁撴瀯鎺ュ彛
+ const { data } = await listProcessBom({ orderId: routeOrderId.value });
+ dataValue.dataList = (data as any) || [];
+ normalizeTreeData(dataValue.dataList);
+ recalculateDemandedQuantities();
+ } else {
+ // 闈炶鍗曟儏鍐碉細浣跨敤鍘熸潵鐨勬帴鍙�
+ const { data } = await queryList(routeId.value);
+ dataValue.dataList = (data as any) || [];
+ console.log(dataValue);
+ normalizeTreeData(dataValue.dataList);
+ console.log(dataValue.dataList, "dataValue.dataList");
+ }
+ };
+
+ const fetchProcessOptions = async () => {
+ const { data } = await list({});
+ console.log(data, "dataValue.dataList");
+ dataValue.processOptions = normalizeListData(data);
+ };
+
+ const handleProduct = (row: any) => {
+ if (!Array.isArray(row) || row.length === 0) {
+ ElMessage.warning("璇烽�夋嫨涓�涓骇鍝�");
+ return;
+ }
+ // 鍙厑璁镐竴涓細濡傛灉涓婃父杩斿洖浜嗗涓紝榛樿浣跨敤鏈�鍚庝竴娆¢�夋嫨骞惰鐩栧綋鍓嶅��
+ const productData = row[row.length - 1];
+
+ // 鏈�澶栧眰缁勪欢涓紝涓庡綋鍓嶄骇鍝佺浉鍚岀殑浜у搧鍙兘鏈変竴涓�
+ const isTopLevel = dataValue.dataList.some(
+ item => (item as any).tempId === dataValue.currentRowName
+ );
+ if (isTopLevel) {
+ if (
+ productData.productName === tableData[0].productName &&
+ productData.model === tableData[0].model
+ ) {
+ // 鏌ユ壘鏄惁宸茬粡鏈夊叾浠栭《灞傝宸茬粡鏄繖涓骇鍝�
+ const hasOther = dataValue.dataList.some(
+ item =>
+ (item as any).tempId !== dataValue.currentRowName &&
+ (item as any).productName === tableData[0].productName &&
+ (item as any).model === tableData[0].model
+ );
+ if (hasOther) {
+ ElMessage.warning("鏈�澶栧眰鍜屽綋鍓嶄骇鍝佷竴鏍风殑涓�绾у彧鑳芥湁涓�涓�");
+ return;
+ }
+ }
+ }
+ // dataValue.dataList[dataValue.currentRowIndex].productName =
+ // row[0].productName;
+ // dataValue.dataList[dataValue.currentRowIndex].model = row[0].model;
+ // dataValue.dataList[dataValue.currentRowIndex].productModelId = row[0].id;
+ // dataValue.dataList[dataValue.currentRowIndex].unit = row[0].unit || "";
+ dataValue.dataList.map(item => {
+ if (item.tempId === dataValue.currentRowName) {
+ item.productName = productData.productName;
+ item.model = productData.model;
+ item.productModelId = productData.id;
+ item.unit = productData.unit || "";
+ return;
+ }
+ childItem(item, dataValue.currentRowName, productData);
+ });
+ dataValue.showProductDialog = false;
+ };
+ const childItem = (item: any, tempId: any, productData: any) => {
+ if (item.tempId === tempId) {
+ item.productName = productData.productName;
+ item.model = productData.model;
+ item.productModelId = productData.id;
+ item.unit = productData.unit || "";
+ return true;
+ }
+ if (item.children && item.children.length > 0) {
+ for (let child of item.children) {
+ if (childItem(child, tempId, productData)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ };
+
+ // 閫掑綊鏍¢獙鎵�鏈夊眰绾х殑琛ㄥ崟鏁版嵁
+ const validateAll = () => {
+ let isValid = true;
+
+ // 鏍¢獙涓�缁勫厔寮熻妭鐐圭殑宸ュ簭鏄惁閮界浉鍚�
+ const checkProcessUniqueness = (items: any[]) => {
+ if (!items || items.length === 0 || !isValid) return;
+
+ // 鑾峰彇绗竴涓潪绌虹殑宸ュ簭ID浣滀负鍙傝��
+ const firstProcessId = items.find(item => item.processId)?.processId;
+
+ // 濡傛灉鏈夊伐搴廔D锛屾鏌ユ墍鏈夐」鏄惁閮戒娇鐢ㄧ浉鍚岀殑宸ュ簭
+ if (firstProcessId) {
+ for (const item of items) {
+ if (item.processId && item.processId !== firstProcessId) {
+ const option1 = getProcessOptionById(firstProcessId);
+ const option2 = getProcessOptionById(item.processId);
+ const processName1 = option1?.name || "鏈煡宸ュ簭";
+ const processName2 = option2?.name || "鏈煡宸ュ簭";
+ ElMessage.error(
+ `褰撳墠灞傜骇涓嬪伐搴忎笉涓�鑷达紝璇蜂娇鐢ㄧ浉鍚岀殑宸ュ簭銆傚瓨鍦ㄣ��${processName1}銆嶅拰銆�${processName2}銆峘
+ );
+ isValid = false;
+ return;
+ }
+ }
+ }
+
+ // 閫掑綊鏍¢獙瀛愮骇鐨勫厔寮熻妭鐐�
+ for (const item of items) {
+ if (item.children && item.children.length > 0) {
+ checkProcessUniqueness(item.children);
+ }
+ }
+ };
+
+ // 鏍¢獙鍑芥暟
+ const validateItem = (item: any, isTopLevel = false) => {
+ if (!isValid) return;
+ // 鏍¢獙褰撳墠椤圭殑蹇呭~瀛楁
+ if (!item.model) {
+ ElMessage.error("璇烽�夋嫨瑙勬牸");
+ isValid = false;
+ return;
+ }
+ if (!isTopLevel && !item.processId) {
+ ElMessage.error("璇烽�夋嫨娑堣�楀伐搴�");
+ isValid = false;
+ return;
+ }
+ if (!item.unitQuantity) {
+ ElMessage.error("璇疯緭鍏ュ崟浣嶄骇鍑烘墍闇�鏁伴噺");
+ isValid = false;
+ return;
+ }
+ if (isOrderPage.value && !item.demandedQuantity) {
+ ElMessage.error("璇疯緭鍏ラ渶姹傛�婚噺");
+ isValid = false;
+ return;
+ }
+ // if (!item.unit) {
+ // ElMessage.error("璇疯緭鍏ュ崟浣�");
+ // isValid = false;
+ // return;
+ // }
+
+ // 閫掑綊鏍¢獙瀛愰」瀛楁
+ if (item.children && item.children.length > 0) {
+ item.children.forEach(child => {
+ validateItem(child, false);
+ });
+ }
+ };
+
+ // 1. 棣栧厛鏍¢獙鍚屼竴鐖剁骇涓嬬殑鍚屽眰娑堣�楀伐搴忔槸鍚﹀敮涓�
+ checkProcessUniqueness(dataValue.dataList);
+ if (!isValid) return false;
+
+ // 2. 鐒跺悗閬嶅巻鏍¢獙鎵�鏈夐《灞傞」鐨勫瓧娈靛繀濉儏鍐�
+ dataValue.dataList.forEach(item => {
+ validateItem(item, true);
+ });
+
+ return isValid;
+ };
+
+ const submit = () => {
+ dataValue.loading = true;
+ normalizeTreeData(dataValue.dataList);
+ recalculateDemandedQuantities();
+
+ // 鍏堣繘琛岃〃鍗曟牎楠�
+ const valid = validateAll();
+ console.log(dataValue.dataList, "dataValue.dataList");
+ if (valid) {
+ addBomDetail({
+ bomId: routeId.value,
+ children: buildSubmitTree(dataValue.dataList || []),
+ })
+ .then(res => {
+ router.go(-1);
+ ElMessage.success("淇濆瓨鎴愬姛");
+ dataValue.loading = false;
+ })
+ .catch(() => {
+ dataValue.loading = false;
+ });
+ } else {
+ dataValue.loading = false;
+ }
+ };
+
+ const removeItem = (tempId: string) => {
+ const topIndex = dataValue.dataList.findIndex(item => item.tempId === tempId);
+ if (topIndex !== -1) {
+ dataValue.dataList.splice(topIndex, 1);
+ return;
+ }
+
+ const delchildItem = (items: any[], tempId: any) => {
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ if (item.tempId === tempId) {
+ items.splice(i, 1);
+ return true;
+ }
+ if (item.children && item.children.length > 0) {
+ if (delchildItem(item.children, tempId)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ };
+
+ dataValue.dataList.forEach(item => {
+ if (item.children && item.children.length > 0) {
+ delchildItem(item.children, tempId);
+ }
+ });
+ };
+
+ const newChildNode = (parentItem: any, nodeType: string = 'rawMaterial') => ({
+ parentId: parentItem.id || "",
+ parentTempId: parentItem.tempId || "",
+ productName: "",
+ productId: "",
+ model: undefined,
+ productModelId: undefined,
+ processId: "",
+ processName: "",
+ operationId: "",
+ operationName: "",
+ unitQuantity: 1,
+ demandedQuantity: 0,
+ unit: "",
+ nodeType,
+ children: [],
+ tempId: new Date().getTime(),
+ });
+
+ const addRootItem = () => {
+ dataValue.dataList.push(newChildNode({ id: "", tempId: "" }));
+ };
+
+ const addChildItem = (parentTempId: string, nodeType: string = 'rawMaterial') => {
+ const addToItem = (items: any[]): boolean => {
+ for (const item of items) {
+ if (item.tempId === parentTempId) {
+ if (!item.children) item.children = [];
+ item.children.push(newChildNode(item, nodeType));
+ recalculateDemandedQuantities();
+ return true;
+ }
+ if (item.children?.length > 0) {
+ if (addToItem(item.children)) return true;
+ }
+ }
+ return false;
+ };
+ addToItem(dataValue.dataList);
+ };
+
+ const getPropPath = (row, field) => {
+ // 涓烘瘡涓猺ow鐢熸垚鍞竴鐨勮矾寰�
+ // 浣跨敤row.id鎴栫储寮曚綔涓哄敮涓�鏍囪瘑
+ let path = "dataList";
+
+ // 绠�鍗曞疄鐜帮細浣跨敤row鐨刬d鎴栦竴涓敮涓�鏍囪瘑
+ const uniqueId = row.id || Math.floor(Math.random() * 10000);
+ path += `.${uniqueId}`;
+
+ return path + `.${field}`;
+ };
+
+ const cancelEdit = () => {
+ dataValue.isEdit = false;
+ // dataValue.dataList = dataValue.dataList.filter(item => item.id !== undefined);
+ fetchData();
+ };
+
+ onMounted(async () => {
+ // 浠庤矾鐢卞弬鏁板洖鏄炬暟鎹�
+ tableData[0].productName = routeProductName.value as string;
+ tableData[0].model = routeProductModelName.value as string;
+ tableData[0].bomNo = routeBomNo.value as string;
+
+ // 璁㈠崟鎯呭喌涓嬬鐢ㄧ紪杈�
+ if (isOrderPage.value) {
+ dataValue.isEdit = false;
+ }
+
+ // 鍏堝姞杞藉伐搴忛�夐」锛屽啀鍔犺浇鏁版嵁锛岀‘淇漞l-select鑳藉姝g‘鍥炴樉
+ await fetchProcessOptions();
+ await fetchData();
+ });
+</script>
+
+<style scoped>
+.tree-container {
+ padding: 8px 0;
+}
+.tree-legend {
+ display: flex;
+ align-items: center;
+ margin-bottom: 12px;
+ padding: 8px 12px;
+ background: #f5f7fa;
+ border-radius: 6px;
+ font-size: 13px;
+ color: #606266;
+}
+.empty-hint {
+ text-align: center;
+ color: #909399;
+ padding: 24px 0;
+}
+</style>
\ No newline at end of file
diff --git a/src/views/productionManagement/productionOrder/index.vue b/src/views/productionManagement/productionOrder/index.vue
index 9c7682b..4d71881 100644
--- a/src/views/productionManagement/productionOrder/index.vue
+++ b/src/views/productionManagement/productionOrder/index.vue
@@ -487,7 +487,7 @@
if (!Number.isFinite(n)) return 0;
if (n <= 0) return 0;
if (n >= 100) return 100;
- return Math.round(n);
+ return parseFloat(n.toFixed(2));
};
// 30/50/80/100 鍒嗘棰滆壊锛氱孩/姗�/钃�/缁�
diff --git a/src/views/productionManagement/teachingDemo/BomDemo.vue b/src/views/productionManagement/teachingDemo/BomDemo.vue
new file mode 100644
index 0000000..c6f9897
--- /dev/null
+++ b/src/views/productionManagement/teachingDemo/BomDemo.vue
@@ -0,0 +1,430 @@
+<template>
+ <div class="app-container bom-demo">
+ <PageHeader :content="`BOM缁撴瀯婕旂ず - ${bomInfo.productName || ''}`">
+ <template #right-button>
+ <el-button @click="goBack">杩斿洖</el-button>
+ </template>
+ </PageHeader>
+
+ <DemoControls
+ ref="controlsRef"
+ :steps="demoSteps"
+ :explanations="demoExplanations"
+ @play="handlePlay"
+ @pause="handlePause"
+ @reset="handleReset"
+ @stepChange="handleStepChange"
+ @speedChange="handleSpeedChange"
+ />
+
+ <div class="demo-container" v-loading="loading">
+ <div class="bom-visualization">
+ <div class="tree-container" v-if="bomData.length > 0">
+ <TreeNode
+ v-for="(item, index) in bomData"
+ :key="item.tempId || item.id || index"
+ :node="item"
+ :level="0"
+ :delay="0"
+ :animation-speed="animationSpeed"
+ :auto-expand="autoExpand"
+ :show-lines="true"
+ :highlight-ids="highlightIds"
+ :active-id="activeNodeId"
+ :animating="isAnimating"
+ @node-click="handleNodeClick"
+ @expand="handleExpand"
+ />
+ </div>
+
+ <el-empty v-else description="鏆傛棤BOM鏁版嵁" />
+
+ <div class="visualization-sidebar">
+ <el-card class="info-card" shadow="hover">
+ <template #header>
+ <span>褰撳墠鑺傜偣淇℃伅</span>
+ </template>
+ <el-descriptions :column="1" border size="small" v-if="activeNode">
+ <el-descriptions-item label="浜у搧鍚嶇О">{{ activeNode.productName }}</el-descriptions-item>
+ <el-descriptions-item label="瑙勬牸鍨嬪彿">{{ activeNode.model || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鍗曚綅鐢ㄩ噺">{{ activeNode.unitQuantity || 1 }}</el-descriptions-item>
+ <el-descriptions-item label="鍗曚綅">{{ activeNode.unit || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="娑堣�楀伐搴�">{{ activeNode.processName || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="灞傜骇娣卞害">{{ getNodeDepth(activeNode) }}</el-descriptions-item>
+ <el-descriptions-item label="瀛愰」鏁伴噺">{{ activeNode.children?.length || 0 }}</el-descriptions-item>
+ </el-descriptions>
+ <div v-else class="empty-info">鐐瑰嚮鑺傜偣鏌ョ湅璇︾粏淇℃伅</div>
+ </el-card>
+
+ <el-card class="legend-card" shadow="hover">
+ <template #header>
+ <span>鍥句緥璇存槑</span>
+ </template>
+ <div class="legend-item">
+ <span class="legend-icon root"></span>
+ <span class="legend-text">鏍硅妭鐐癸紙鎴愬搧锛�</span>
+ </div>
+ <div class="legend-item">
+ <span class="legend-icon child"></span>
+ <span class="legend-text">瀛愯妭鐐癸紙闆堕儴浠讹級</span>
+ </div>
+ <div class="legend-item">
+ <span class="legend-icon leaf"></span>
+ <span class="legend-text">鍙跺瓙鑺傜偣锛堝師鏉愭枡锛�</span>
+ </div>
+ <div class="legend-item">
+ <span class="legend-icon highlight"></span>
+ <span class="legend-text">楂樹寒鐘舵��</span>
+ </div>
+ <div class="legend-item">
+ <span class="legend-icon active"></span>
+ <span class="legend-text">閫変腑鐘舵��</span>
+ </div>
+ </el-card>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted, onUnmounted, nextTick } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import TreeNode from './components/TreeNode.vue'
+import DemoControls from './components/DemoControls.vue'
+import { queryList } from '@/api/productionManagement/productStructure.js'
+
+const router = useRouter()
+const route = useRoute()
+
+const loading = ref(false)
+const bomData = ref([])
+const controlsRef = ref(null)
+const animationSpeed = ref(1)
+const autoExpand = ref(false)
+const isAnimating = ref(false)
+const highlightIds = ref([])
+const activeNodeId = ref(null)
+const activeNode = ref(null)
+const playTimer = ref(null)
+
+const bomId = computed(() => route.query.id)
+const bomInfo = computed(() => ({
+ bomNo: route.query.bomNo || '',
+ productName: route.query.productName || '',
+ productModelName: route.query.productModelName || ''
+}))
+
+const demoSteps = [
+ { title: '鏍硅妭鐐�', description: '灞曠ず鎴愬搧' },
+ { title: '灞曞紑瀛愰」', description: '閫愬眰灞曞紑' },
+ { title: '鐗╂枡缁勬垚', description: '鏄剧ず鍏崇郴' },
+ { title: '鐢ㄩ噺淇℃伅', description: '鏁伴噺鍏崇郴' },
+ { title: '宸ュ簭鍏宠仈', description: '娑堣�楀伐搴�' }
+]
+
+const demoExplanations = [
+ {
+ title: '绗�1姝ワ細璁よ瘑鎴愬搧鏍硅妭鐐�',
+ content: 'BOM鐨勬牴鑺傜偣浠h〃鏈�缁堟垚鍝併�傝繖閲屾槸鎴戜滑瑕佺敓浜х殑浜у搧锛屾墍鏈夊瓙鑺傜偣閮芥槸涓轰簡鐢熶骇杩欎釜鎴愬搧鎵�闇�瑕佺殑鐗╂枡銆�'
+ },
+ {
+ title: '绗�2姝ワ細灞曞紑鏌ョ湅瀛愰」',
+ content: '鐐瑰嚮鑺傜偣鍙互灞曞紑鏌ョ湅鍏跺瓙椤广�傚瓙椤逛唬琛ㄧ粍鎴愮埗椤圭殑闆堕儴浠舵垨鍘熸潗鏂欍�侭OM鏄竴涓爲褰㈢粨鏋勶紝浣撶幇浜嗕骇鍝佺殑缁勬垚灞傛銆�'
+ },
+ {
+ title: '绗�3姝ワ細鐞嗚В鐗╂枡缁勬垚鍏崇郴',
+ content: '杩炵嚎琛ㄧず鐖跺瓙鍏崇郴銆傛瘡涓瓙鑺傜偣閮芥槸鍏剁埗鑺傜偣鐨勭粍鎴愰儴鍒嗐�備緥濡傦紝涓�涓數鑴戠敱涓绘澘銆丆PU銆佸唴瀛樼瓑缁勬垚銆�'
+ },
+ {
+ title: '绗�4姝ワ細鏌ョ湅鐢ㄩ噺淇℃伅',
+ content: '姣忎釜鑺傜偣涓婄殑鏁板瓧琛ㄧず"鍗曚綅浜у嚭鎵�闇�鏁伴噺"锛屽嵆鐢熶骇涓�涓埗椤归渶瑕佸灏戝瓙椤广�備緥濡傦紝鐢熶骇涓�鍙扮數鑴戦渶瑕�1鍧椾富鏉裤��'
+ },
+ {
+ title: '绗�5姝ワ細浜嗚В宸ュ簭鍏宠仈',
+ content: '瀛愯妭鐐逛笂鐨勫伐搴忓悕绉拌〃绀鸿鐗╂枡鍦ㄥ摢閬撳伐搴忎腑琚秷鑰椼�傝繖杩炴帴浜咮OM涓庡伐鑹鸿矾绾匡紝璇存槑鐗╂枡鍦ㄤ綍鏃惰浣跨敤銆�'
+ }
+]
+
+const fetchBomData = async () => {
+ if (!bomId.value) return
+ loading.value = true
+ try {
+ const res = await queryList(bomId.value)
+ bomData.value = res?.data || []
+ normalizeBomData(bomData.value)
+ } catch (error) {
+ console.error('鑾峰彇BOM鏁版嵁澶辫触:', error)
+ } finally {
+ loading.value = false
+ }
+}
+
+const normalizeBomData = (data) => {
+ data.forEach(item => {
+ item.tempId = item.tempId || item.id || `${Date.now()}_${Math.random()}`
+ if (item.children && item.children.length > 0) {
+ normalizeBomData(item.children)
+ }
+ })
+}
+
+const goBack = () => {
+ router.push('/productionManagement/teachingDemo')
+}
+
+const handleNodeClick = (node) => {
+ activeNode.value = node
+ activeNodeId.value = node.tempId || node.id
+ highlightIds.value = getRelatedIds(node)
+}
+
+const handleExpand = (node, expanded) => {
+ // 澶勭悊灞曞紑浜嬩欢
+}
+
+const getRelatedIds = (node) => {
+ const ids = []
+ // 娣诲姞鎵�鏈夌埗鑺傜偣ID
+ const addParentIds = (currentNode, targetNode) => {
+ if (currentNode.children) {
+ for (const child of currentNode.children) {
+ if (child.tempId === targetNode.tempId || child.id === targetNode.id) {
+ ids.push(currentNode.tempId || currentNode.id)
+ return true
+ }
+ if (addParentIds(child, targetNode)) {
+ ids.push(currentNode.tempId || currentNode.id)
+ return true
+ }
+ }
+ }
+ return false
+ }
+
+ for (const root of bomData.value) {
+ addParentIds(root, node)
+ }
+
+ // 娣诲姞鎵�鏈夊瓙鑺傜偣ID
+ const addChildIds = (currentNode) => {
+ ids.push(currentNode.tempId || currentNode.id)
+ if (currentNode.children) {
+ for (const child of currentNode.children) {
+ addChildIds(child)
+ }
+ }
+ }
+
+ if (node.children) {
+ for (const child of node.children) {
+ addChildIds(child)
+ }
+ }
+
+ return ids
+}
+
+const getNodeDepth = (node) => {
+ let depth = 0
+ const findDepth = (currentNode, targetNode, currentDepth) => {
+ if (currentNode.tempId === targetNode.tempId || currentNode.id === targetNode.id) {
+ depth = currentDepth
+ return true
+ }
+ if (currentNode.children) {
+ for (const child of currentNode.children) {
+ if (findDepth(child, targetNode, currentDepth + 1)) {
+ return true
+ }
+ }
+ }
+ return false
+ }
+
+ for (const root of bomData.value) {
+ findDepth(root, node, 0)
+ }
+
+ return depth
+}
+
+const handlePlay = () => {
+ isAnimating.value = true
+ autoExpand.value = true
+ runAnimation()
+}
+
+const handlePause = () => {
+ isAnimating.value = false
+ autoExpand.value = false
+ if (playTimer.value) {
+ clearTimeout(playTimer.value)
+ }
+}
+
+const handleReset = () => {
+ isAnimating.value = false
+ autoExpand.value = false
+ activeNodeId.value = null
+ activeNode.value = null
+ highlightIds.value = []
+ if (playTimer.value) {
+ clearTimeout(playTimer.value)
+ }
+}
+
+const handleStepChange = (step) => {
+ // 鏍规嵁姝ラ鎵ц鐩稿簲鎿嶄綔
+ switch (step) {
+ case 0:
+ // 灞曠ず鏍硅妭鐐�
+ if (bomData.value.length > 0) {
+ activeNode.value = bomData.value[0]
+ activeNodeId.value = bomData.value[0].tempId || bomData.value[0].id
+ }
+ break
+ case 1:
+ // 灞曞紑瀛愰」
+ autoExpand.value = true
+ break
+ case 2:
+ // 鏄剧ず鍏崇郴
+ if (activeNode.value) {
+ highlightIds.value = getRelatedIds(activeNode.value)
+ }
+ break
+ case 3:
+ // 鐢ㄩ噺淇℃伅
+ break
+ case 4:
+ // 宸ュ簭鍏宠仈
+ break
+ }
+}
+
+const handleSpeedChange = (speed) => {
+ animationSpeed.value = speed
+}
+
+const runAnimation = () => {
+ if (!isAnimating.value) return
+
+ const totalSteps = demoSteps.length
+ let currentStep = controlsRef.value?.currentStep || 0
+
+ const animate = () => {
+ if (!isAnimating.value) return
+ if (currentStep < totalSteps) {
+ controlsRef.value?.setStep(currentStep)
+ handleStepChange(currentStep)
+ currentStep++
+ playTimer.value = setTimeout(animate, 3000 / animationSpeed.value)
+ } else {
+ isAnimating.value = false
+ controlsRef.value?.setIsPlaying(false)
+ }
+ }
+
+ animate()
+}
+
+onMounted(() => {
+ fetchBomData()
+})
+
+onUnmounted(() => {
+ if (playTimer.value) {
+ clearTimeout(playTimer.value)
+ }
+})
+</script>
+
+<style scoped lang="scss">
+.bom-demo {
+ .demo-container {
+ background: #f5f7fa;
+ padding: 20px;
+ border-radius: 12px;
+ min-height: 400px;
+
+ .bom-visualization {
+ display: flex;
+ gap: 20px;
+
+ .tree-container {
+ flex: 1;
+ background: #fff;
+ padding: 30px;
+ border-radius: 12px;
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+ overflow: auto;
+ max-height: 600px;
+ }
+
+ .visualization-sidebar {
+ width: 300px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ .info-card {
+ .empty-info {
+ color: #909399;
+ text-align: center;
+ padding: 20px;
+ }
+ }
+
+ .legend-card {
+ .legend-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 8px;
+
+ .legend-icon {
+ width: 16px;
+ height: 16px;
+ border-radius: 4px;
+ border: 2px solid;
+
+ &.root {
+ background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
+ border-color: #409eff;
+ }
+
+ &.child {
+ background: #fff;
+ border-color: #e4e7ed;
+ }
+
+ &.leaf {
+ background: #fff;
+ border-color: #67c23a;
+ }
+
+ &.highlight {
+ background: #fff;
+ border-color: #e6a23c;
+ box-shadow: 0 0 8px rgba(230, 162, 60, 0.4);
+ }
+
+ &.active {
+ background: #fff;
+ border-color: #409eff;
+ box-shadow: 0 0 12px rgba(64, 158, 255, 0.5);
+ }
+ }
+
+ .legend-text {
+ font-size: 12px;
+ color: #666;
+ }
+ }
+ }
+ }
+ }
+ }
+}
+</style>
\ No newline at end of file
diff --git a/src/views/productionManagement/teachingDemo/CombinedDemo.vue b/src/views/productionManagement/teachingDemo/CombinedDemo.vue
new file mode 100644
index 0000000..92c9ba8
--- /dev/null
+++ b/src/views/productionManagement/teachingDemo/CombinedDemo.vue
@@ -0,0 +1,518 @@
+<template>
+ <div class="app-container combined-demo">
+ <PageHeader content="BOM涓庡伐鑹鸿矾绾胯仈鍔ㄦ紨绀�">
+ <template #right-button>
+ <el-button @click="goBack">杩斿洖</el-button>
+ </template>
+ </PageHeader>
+
+ <DemoControls
+ ref="controlsRef"
+ :steps="demoSteps"
+ :explanations="demoExplanations"
+ @play="handlePlay"
+ @pause="handlePause"
+ @reset="handleReset"
+ @stepChange="handleStepChange"
+ @speedChange="handleSpeedChange"
+ />
+
+ <div class="selection-panel" v-if="!showDemo">
+ <el-card shadow="hover">
+ <el-form label-width="100px">
+ <el-form-item label="閫夋嫨BOM">
+ <el-select v-model="selectedBomId" placeholder="璇烽�夋嫨BOM" filterable style="width: 100%">
+ <el-option v-for="item in bomList" :key="item.id" :label="`${item.bomNo} - ${item.productName}`" :value="item.id" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="閫夋嫨宸ヨ壓璺嚎">
+ <el-select v-model="selectedRouteId" placeholder="璇烽�夋嫨宸ヨ壓璺嚎" filterable style="width: 100%">
+ <el-option v-for="item in routeList" :key="item.id" :label="`${item.processRouteCode} - ${item.productName}`" :value="item.id" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" :disabled="!selectedBomId || !selectedRouteId" @click="startDemo">
+ 寮�濮嬫紨绀�
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+ </div>
+
+ <div class="demo-container" v-loading="loading" v-if="showDemo">
+ <div class="combined-visualization">
+ <div class="bom-panel">
+ <div class="panel-header">
+ <h4>BOM 鐗╂枡缁撴瀯</h4>
+ <el-button size="small" @click="toggleBomExpand">{{ bomExpanded ? '鍏ㄩ儴鎶樺彔' : '鍏ㄩ儴灞曞紑' }}</el-button>
+ </div>
+ <div class="tree-container">
+ <TreeNode
+ v-for="(item, index) in bomData"
+ :key="item.tempId || item.id || index"
+ :node="item"
+ :level="0"
+ :delay="0"
+ :animation-speed="animationSpeed"
+ :auto-expand="bomExpanded"
+ :show-lines="true"
+ :highlight-ids="highlightBomIds"
+ :active-id="activeBomId"
+ :animating="isAnimating"
+ @node-click="handleBomClick"
+ />
+ </div>
+ </div>
+
+ <div class="connection-panel">
+ <svg class="connection-svg" ref="connectionSvg">
+ <!-- 鍔ㄦ�佺粯鍒惰繛鎺ョ嚎 -->
+ </svg>
+ <div class="connection-info" v-if="connectionInfo">
+ <el-tag type="success" effect="plain">
+ {{ connectionInfo }}
+ </el-tag>
+ </div>
+ </div>
+
+ <div class="route-panel">
+ <div class="panel-header">
+ <h4>宸ヨ壓璺嚎娴佺▼</h4>
+ </div>
+ <div class="process-container">
+ <ProcessNode
+ v-for="(process, index) in processList"
+ :key="process.id || index"
+ :process="process"
+ :index="index"
+ :delay="index * 200"
+ :animation-speed="animationSpeed"
+ :active="activeProcessId === process.id"
+ :completed="completedProcesses.includes(process.id)"
+ :current="currentProcessId === process.id"
+ :animating="isAnimating && currentProcessId === process.id"
+ :show-arrow="index < processList.length - 1"
+ @click="handleProcessClick"
+ />
+ </div>
+ </div>
+ </div>
+
+ <div class="demo-explanation">
+ <el-card shadow="hover">
+ <div class="explanation-content">
+ <h5>鑱斿姩璇存槑</h5>
+ <p>鐐瑰嚮鍙充晶鐨勫伐搴忚妭鐐癸紝宸︿晶浼氶珮浜樉绀鸿宸ュ簭娑堣�楃殑鐗╂枡銆�</p>
+ <p>鐐瑰嚮宸︿晶鐨勭墿鏂欒妭鐐癸紝鍙充晶浼氶珮浜樉绀烘秷鑰楄鐗╂枡鐨勫伐搴忋��</p>
+ <p>缁胯壊杩炵嚎琛ㄧず鐗╂枡鍦ㄥ搴斿伐搴忎腑琚秷鑰楃殑鍏崇郴銆�</p>
+ </div>
+ </el-card>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import TreeNode from './components/TreeNode.vue'
+import ProcessNode from './components/ProcessNode.vue'
+import DemoControls from './components/DemoControls.vue'
+import { listPage as getBomList } from '@/api/productionManagement/productBom.js'
+import { listPage as getRouteList } from '@/api/productionManagement/processRoute.js'
+import { queryList } from '@/api/productionManagement/productStructure.js'
+import { findProcessRouteItemList } from '@/api/productionManagement/processRouteItem.js'
+
+const router = useRouter()
+const route = useRoute()
+
+const loading = ref(false)
+const showDemo = ref(false)
+const bomList = ref([])
+const routeList = ref([])
+const selectedBomId = ref(null)
+const selectedRouteId = ref(null)
+const bomData = ref([])
+const processList = ref([])
+const controlsRef = ref(null)
+const animationSpeed = ref(1)
+const isAnimating = ref(false)
+const bomExpanded = ref(true)
+const highlightBomIds = ref([])
+const activeBomId = ref(null)
+const activeProcessId = ref(null)
+const activeProcess = ref(null)
+const completedProcesses = ref([])
+const currentProcessId = ref(null)
+const connectionInfo = ref('')
+const playTimer = ref(null)
+
+const demoSteps = [
+ { title: 'BOM缁撴瀯', description: '鏌ョ湅鐗╂枡缁勬垚' },
+ { title: '宸ヨ壓璺嚎', description: '鏌ョ湅宸ュ簭娴佺▼' },
+ { title: '鐗╂枡娑堣��', description: '鍏宠仈鍏崇郴' },
+ { title: '鐢熶骇杩囩▼', description: '妯℃嫙娴佺▼' },
+ { title: '鑱斿姩婕旂ず', description: '浜や簰浣撻獙' }
+]
+
+const demoExplanations = [
+ {
+ title: '绗�1姝ワ細璁よ瘑BOM缁撴瀯',
+ content: '宸︿晶灞曠ず浜嗕骇鍝佺殑BOM缁撴瀯锛屾爲褰㈠浘鏄剧ず浜嗕骇鍝佺敱鍝簺鐗╂枡缁勬垚銆傛牴鑺傜偣鏄垚鍝侊紝瀛愯妭鐐规槸缁勬垚鎴愬搧鐨勯浂閮ㄤ欢鍜屽師鏉愭枡銆�'
+ },
+ {
+ title: '绗�2姝ワ細浜嗚В宸ヨ壓璺嚎',
+ content: '鍙充晶灞曠ず浜嗗伐鑹鸿矾绾匡紝妯悜娴佺▼鍥炬樉绀轰簡浜у搧闇�瑕佺粡杩囧摢浜涘伐搴忋�傛瘡涓伐搴忚妭鐐逛唬琛ㄤ竴涓嫭绔嬬殑鍔犲伐姝ラ銆�'
+ },
+ {
+ title: '绗�3姝ワ細鐞嗚В鐗╂枡娑堣�楀叧绯�',
+ content: 'BOM涓殑姣忎釜鐗╂枡鑺傜偣閮芥湁涓�涓�"娑堣�楀伐搴�"灞炴�э紝琛ㄧず璇ョ墿鏂欏湪鍝亾宸ュ簭涓浣跨敤銆傝繖鏄疊OM涓庡伐鑹鸿矾绾跨殑鏍稿績鍏宠仈銆�'
+ },
+ {
+ title: '绗�4姝ワ細瑙傜湅鐢熶骇杩囩▼妯℃嫙',
+ content: '鐐瑰嚮鎾斁鍙互瑙傜湅浜у搧鍦ㄥ悇宸ュ簭闂存祦杞殑杩囩▼銆傞殢鐫�宸ュ簭鐨勮繘琛岋紝BOM涓殑鐗╂枡琚�愭娑堣�椼��'
+ },
+ {
+ title: '绗�5姝ワ細浣撻獙鑱斿姩浜や簰',
+ content: '鐐瑰嚮宸ュ簭鑺傜偣锛孊OM涓搴旂墿鏂欎細楂樹寒锛涚偣鍑荤墿鏂欒妭鐐癸紝瀵瑰簲宸ュ簭浼氶珮浜�傝繖绉嶈仈鍔ㄥ府鍔╃悊瑙g墿鏂欏湪浣曟椂浣曞湴琚秷鑰椼��'
+ }
+]
+
+const fetchBomList = async () => {
+ try {
+ const res = await getBomList({ current: 1, size: 100 })
+ bomList.value = res?.data?.records || []
+ // 濡傛灉璺敱鏈夐璁綽omId锛岃嚜鍔ㄩ�夋嫨
+ if (route.query.bomId) {
+ selectedBomId.value = Number(route.query.bomId)
+ }
+ } catch (error) {
+ console.error('鑾峰彇BOM鍒楄〃澶辫触:', error)
+ }
+}
+
+const fetchRouteList = async () => {
+ try {
+ const res = await getRouteList({ current: 1, size: 100 })
+ routeList.value = res?.data?.records || []
+ // 濡傛灉璺敱鏈夐璁緍outeId锛岃嚜鍔ㄩ�夋嫨
+ if (route.query.routeId) {
+ selectedRouteId.value = Number(route.query.routeId)
+ }
+ } catch (error) {
+ console.error('鑾峰彇宸ヨ壓璺嚎鍒楄〃澶辫触:', error)
+ }
+}
+
+const fetchBomData = async () => {
+ if (!selectedBomId.value) return
+ try {
+ const res = await queryList(selectedBomId.value)
+ bomData.value = res?.data || []
+ normalizeBomData(bomData.value)
+ } catch (error) {
+ console.error('鑾峰彇BOM鏁版嵁澶辫触:', error)
+ }
+}
+
+const fetchProcessList = async () => {
+ if (!selectedRouteId.value) return
+ try {
+ const res = await findProcessRouteItemList({ routeId: selectedRouteId.value })
+ processList.value = res?.data || []
+ processList.value.sort((a, b) => (a.dragSort || 0) - (b.dragSort || 0))
+ } catch (error) {
+ console.error('鑾峰彇宸ヨ壓璺嚎鏁版嵁澶辫触:', error)
+ }
+}
+
+const normalizeBomData = (data) => {
+ data.forEach(item => {
+ item.tempId = item.tempId || item.id || `${Date.now()}_${Math.random()}`
+ if (item.children && item.children.length > 0) {
+ normalizeBomData(item.children)
+ }
+ })
+}
+
+const startDemo = async () => {
+ loading.value = true
+ try {
+ await Promise.all([fetchBomData(), fetchProcessList()])
+ showDemo.value = true
+ } finally {
+ loading.value = false
+ }
+}
+
+const goBack = () => {
+ router.push('/productionManagement/teachingDemo')
+}
+
+const toggleBomExpand = () => {
+ bomExpanded.value = !bomExpanded.value
+}
+
+const handleBomClick = (node) => {
+ activeBomId.value = node.tempId || node.id
+ // 鎵惧埌娑堣�楄鐗╂枡鐨勫伐搴�
+ const processName = node.processName
+ if (processName) {
+ const matchingProcess = processList.value.find(p =>
+ p.technologyOperationName === processName || p.operationName === processName
+ )
+ if (matchingProcess) {
+ activeProcessId.value = matchingProcess.id
+ connectionInfo.value = `${node.productName} 鍦� "${processName}" 宸ュ簭涓秷鑰梎
+ }
+ }
+}
+
+const handleProcessClick = (process) => {
+ activeProcess.value = process
+ activeProcessId.value = process.id
+ // 鎵惧埌璇ュ伐搴忔秷鑰楃殑鎵�鏈夌墿鏂�
+ const processName = process.technologyOperationName || process.operationName
+ highlightBomIds.value = findBomIdsByProcess(processName)
+ if (highlightBomIds.value.length > 0) {
+ connectionInfo.value = `"${processName}" 宸ュ簭娑堣�椾簡 ${highlightBomIds.value.length} 绉嶇墿鏂檂
+ }
+}
+
+const findBomIdsByProcess = (processName) => {
+ const ids = []
+ const traverse = (nodes) => {
+ for (const node of nodes) {
+ if (node.processName === processName) {
+ ids.push(node.tempId || node.id)
+ }
+ if (node.children) {
+ traverse(node.children)
+ }
+ }
+ }
+ traverse(bomData.value)
+ return ids
+}
+
+const handlePlay = () => {
+ isAnimating.value = true
+ bomExpanded.value = true
+ runAnimation()
+}
+
+const handlePause = () => {
+ isAnimating.value = false
+ if (playTimer.value) {
+ clearTimeout(playTimer.value)
+ }
+}
+
+const handleReset = () => {
+ isAnimating.value = false
+ highlightBomIds.value = []
+ activeBomId.value = null
+ activeProcessId.value = null
+ activeProcess.value = null
+ currentProcessId.value = null
+ completedProcesses.value = []
+ connectionInfo.value = ''
+ if (playTimer.value) {
+ clearTimeout(playTimer.value)
+ }
+}
+
+const handleStepChange = (step) => {
+ switch (step) {
+ case 0:
+ // BOM缁撴瀯
+ break
+ case 1:
+ // 宸ヨ壓璺嚎
+ break
+ case 2:
+ // 鐗╂枡娑堣��
+ if (processList.value.length > 0) {
+ const firstProcess = processList.value[0]
+ handleProcessClick(firstProcess)
+ }
+ break
+ case 3:
+ // 鐢熶骇杩囩▼妯℃嫙
+ simulateProduction()
+ break
+ case 4:
+ // 鑱斿姩婕旂ず
+ break
+ }
+}
+
+const handleSpeedChange = (speed) => {
+ animationSpeed.value = speed
+}
+
+const simulateProduction = () => {
+ completedProcesses.value = []
+ currentProcessId.value = null
+ let index = 0
+ const interval = 1500 / animationSpeed.value
+
+ playTimer.value = setInterval(() => {
+ if (index >= processList.value.length) {
+ clearInterval(playTimer.value)
+ currentProcessId.value = null
+ isAnimating.value = false
+ return
+ }
+
+ if (index > 0) {
+ completedProcesses.value.push(processList.value[index - 1].id)
+ }
+
+ currentProcessId.value = processList.value[index].id
+ activeProcessId.value = processList.value[index].id
+
+ // 楂樹寒瀵瑰簲鐗╂枡
+ const processName = processList.value[index].technologyOperationName || processList.value[index].operationName
+ highlightBomIds.value = findBomIdsByProcess(processName)
+ connectionInfo.value = `姝e湪鎵ц "${processName}" 宸ュ簭`
+
+ index++
+ }, interval)
+}
+
+const runAnimation = () => {
+ if (!isAnimating.value) return
+
+ const totalSteps = demoSteps.length
+ let currentStep = controlsRef.value?.currentStep || 0
+
+ const animate = () => {
+ if (!isAnimating.value) return
+ if (currentStep < totalSteps) {
+ controlsRef.value?.setStep(currentStep)
+ handleStepChange(currentStep)
+ currentStep++
+ playTimer.value = setTimeout(animate, 4000 / animationSpeed.value)
+ } else {
+ isAnimating.value = false
+ controlsRef.value?.setIsPlaying(false)
+ }
+ }
+
+ animate()
+}
+
+onMounted(() => {
+ fetchBomList()
+ fetchRouteList()
+ // 濡傛灉棰勮浜嗘暟鎹紝鑷姩寮�濮嬫紨绀�
+ if (route.query.bomId && route.query.routeId) {
+ nextTick(() => {
+ startDemo()
+ })
+ }
+})
+
+onUnmounted(() => {
+ if (playTimer.value) {
+ clearTimeout(playTimer.value)
+ }
+})
+</script>
+
+<style scoped lang="scss">
+.combined-demo {
+ .selection-panel {
+ margin-bottom: 20px;
+ }
+
+ .demo-container {
+ background: #f5f7fa;
+ padding: 20px;
+ border-radius: 12px;
+ min-height: 400px;
+
+ .combined-visualization {
+ display: flex;
+ gap: 16px;
+ min-height: 300px;
+
+ .bom-panel,
+ .route-panel {
+ flex: 1;
+ background: #fff;
+ padding: 20px;
+ border-radius: 12px;
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+
+ .panel-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 16px;
+
+ h4 {
+ color: #303133;
+ font-size: 16px;
+ font-weight: 600;
+ margin: 0;
+ }
+ }
+
+ .tree-container,
+ .process-container {
+ overflow: auto;
+ max-height: 400px;
+ }
+
+ .process-container {
+ display: flex;
+ align-items: center;
+ gap: 0;
+ padding: 10px 0;
+ }
+ }
+
+ .connection-panel {
+ width: 200px;
+ background: #fff;
+ padding: 20px;
+ border-radius: 12px;
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+
+ .connection-svg {
+ width: 100%;
+ height: 100px;
+ }
+
+ .connection-info {
+ text-align: center;
+ margin-top: 16px;
+ }
+ }
+ }
+
+ .demo-explanation {
+ margin-top: 20px;
+
+ .explanation-content {
+ h5 {
+ color: #409eff;
+ margin-bottom: 12px;
+ }
+
+ p {
+ color: #666;
+ line-height: 1.8;
+ margin-bottom: 8px;
+ }
+ }
+ }
+ }
+}
+</style>
\ No newline at end of file
diff --git a/src/views/productionManagement/teachingDemo/FactoryDemo.vue b/src/views/productionManagement/teachingDemo/FactoryDemo.vue
new file mode 100644
index 0000000..c7227e2
--- /dev/null
+++ b/src/views/productionManagement/teachingDemo/FactoryDemo.vue
@@ -0,0 +1,1153 @@
+<template>
+ <div class="app-container factory-demo">
+ <PageHeader content="鍔ㄦ�佸伐鍘傜敓浜х嚎婕旂ず">
+ <template #right-button>
+ <el-button @click="goBack">杩斿洖</el-button>
+ </template>
+ </PageHeader>
+
+ <div class="control-panel">
+ <el-row :gutter="20" align="middle">
+ <el-col :span="6">
+ <el-select v-model="selectedRouteId" placeholder="閫夋嫨宸ヨ壓璺嚎" filterable style="width: 100%" @change="handleRouteChange">
+ <el-option v-for="item in routeList" :key="item.id" :label="`${item.processRouteCode} - ${item.productName}`" :value="item.id" />
+ </el-select>
+ </el-col>
+ <el-col :span="6">
+ <el-button-group>
+ <el-button :type="isPlaying ? 'warning' : 'primary'" @click="togglePlay">
+ <el-icon v-if="isPlaying"><VideoPause /></el-icon>
+ <el-icon v-else><VideoPlay /></el-icon>
+ {{ isPlaying ? '鏆傚仠' : '鎾斁' }}
+ </el-button>
+ <el-button @click="handleReset">
+ <el-icon><RefreshRight /></el-icon>
+ 閲嶇疆
+ </el-button>
+ </el-button-group>
+ </el-col>
+ <el-col :span="6">
+ <div class="speed-control">
+ <span>閫熷害锛�</span>
+ <el-radio-group v-model="speed" size="small">
+ <el-radio-button label="0.5">鎱�</el-radio-button>
+ <el-radio-button label="1">姝e父</el-radio-button>
+ <el-radio-button label="2">蹇�</el-radio-button>
+ </el-radio-group>
+ </div>
+ </el-col>
+ <el-col :span="6">
+ <div class="progress-info">
+ <span>杩涘害锛�</span>
+ <el-progress :percentage="productionProgress" :stroke-width="8" style="width: 120px" />
+ </div>
+ </el-col>
+ </el-row>
+ </div>
+
+ <div class="factory-container" v-loading="loading">
+ <div class="factory-scene">
+ <svg class="factory-svg" viewBox="0 0 1100 500" preserveAspectRatio="xMidYMid meet">
+ <defs>
+ <!-- 娓愬彉瀹氫箟 -->
+ <linearGradient id="beltGradient" x1="0%" y1="0%" x2="100%" y2="0%">
+ <stop offset="0%" stop-color="#409eff" stop-opacity="0.8" />
+ <stop offset="50%" stop-color="#66b1ff" stop-opacity="0.6" />
+ <stop offset="100%" stop-color="#409eff" stop-opacity="0.8" />
+ </linearGradient>
+ <linearGradient id="stationGradient" x1="0%" y1="0%" x2="0%" y2="100%">
+ <stop offset="0%" stop-color="#f8f9fa" />
+ <stop offset="100%" stop-color="#e9ecef" />
+ </linearGradient>
+ <linearGradient id="activeGradient" x1="0%" y1="0%" x2="0%" y2="100%">
+ <stop offset="0%" stop-color="#ecf5ff" />
+ <stop offset="100%" stop-color="#d9ecff" />
+ </linearGradient>
+ <filter id="stationShadow" x="-20%" y="-20%" width="140%" height="140%">
+ <feDropShadow dx="0" dy="4" stdDeviation="6" flood-color="#000" flood-opacity="0.15" />
+ </filter>
+ <filter id="glowEffect" x="-50%" y="-50%" width="200%" height="200%">
+ <feGaussianBlur stdDeviation="4" result="blur" />
+ <feMerge>
+ <feMergeNode in="blur" />
+ <feMergeNode in="SourceGraphic" />
+ </feMerge>
+ </filter>
+ </defs>
+
+ <!-- 鑳屾櫙鍦版澘 -->
+ <rect x="0" y="380" width="1100" height="120" fill="#f5f7fa" />
+ <rect x="0" y="380" width="1100" height="2" fill="#e4e7ed" />
+
+ <!-- 灞ュ甫 -->
+ <g class="conveyor-system">
+ <!-- 灞ュ甫涓讳綋 -->
+ <rect x="40" y="320" width="1020" height="60" rx="8" fill="url(#beltGradient)" opacity="0.3" />
+ <!-- 灞ュ甫杈圭紭 -->
+ <rect x="40" y="320" width="1020" height="4" rx="2" fill="#409eff" opacity="0.5" />
+ <rect x="40" y="376" width="1020" height="4" rx="2" fill="#409eff" opacity="0.5" />
+ <!-- 娴佸姩绾挎潯 -->
+ <path class="belt-flow" d="M50 350 H1050" stroke="#fff" stroke-width="3" stroke-dasharray="15 20" stroke-linecap="round" opacity="0.8" />
+ <!-- 灞ュ甫婊氳疆 -->
+ <circle v-for="i in 12" :key="'roller'+i" :cx="50 + i * 85" cy="350" r="8" fill="#fff" opacity="0.6" />
+ </g>
+
+ <!-- 鍏ュ簱鍖� -->
+ <g class="input-area" @click="showInputDialog">
+ <rect x="20" y="200" width="80" height="80" rx="12" fill="#f0f9eb" stroke="#67c23a" stroke-width="2" filter="url(#stationShadow)" />
+ <text x="60" y="245" text-anchor="middle" font-size="14" fill="#67c23a" font-weight="600">鍘熸潗鏂�</text>
+ <text x="60" y="265" text-anchor="middle" font-size="12" fill="#67c23a">鍏ュ簱</text>
+ <!-- 鍏ュ簱鍥炬爣 -->
+ <g transform="translate(45, 215)">
+ <rect width="30" height="20" rx="4" fill="#67c23a" opacity="0.8" />
+ <path d="M15 -8 L15 24" stroke="#67c23a" stroke-width="3" stroke-dasharray="4 4" />
+ </g>
+ </g>
+
+ <!-- 鍑哄簱鍖� -->
+ <g class="output-area" @click="showOutputDialog">
+ <rect x="1000" y="200" width="80" height="80" rx="12" fill="#ecf5ff" stroke="#409eff" stroke-width="2" filter="url(#stationShadow)" />
+ <text x="1040" y="245" text-anchor="middle" font-size="14" fill="#409eff" font-weight="600">鎴愬搧</text>
+ <text x="1040" y="265" text-anchor="middle" font-size="12" fill="#409eff">鍑哄簱</text>
+ <!-- 鍑哄簱鍥炬爣 -->
+ <g transform="translate(1015, 215)">
+ <rect width="30" height="20" rx="4" fill="#409eff" opacity="0.8" />
+ <path d="M15 28 L15 0" stroke="#409eff" stroke-width="3" stroke-dasharray="4 4" />
+ <path d="M8 -5 L15 -12 L22 -5" stroke="#409eff" stroke-width="2" fill="none" />
+ </g>
+ </g>
+
+ <!-- 宸ュ簭宸ヤ綔绔� -->
+ <g
+ v-for="(process, index) in processList"
+ :key="process.id || index"
+ class="work-station"
+ :class="{ 'is-active': currentProcessIndex === index, 'is-completed': completedIndex > index }"
+ :transform="`translate(${getStationX(index)}, 0)`"
+ @click="showProcessDetail(process)"
+ >
+ <!-- 鏈哄櫒澶栧3 -->
+ <rect x="0" y="80" width="120" height="140" rx="16" :fill="currentProcessIndex === index ? 'url(#activeGradient)' : 'url(#stationGradient)'" :stroke="completedIndex > index ? '#67c23a' : currentProcessIndex === index ? '#409eff' : '#e4e7ed'" stroke-width="2" filter="url(#stationShadow)" />
+
+ <!-- 宸ュ簭搴忓彿 -->
+ <circle cx="60" cy="60" r="24" :fill="completedIndex > index ? '#67c23a' : currentProcessIndex === index ? '#409eff' : '#909399'" />
+ <text x="60" y="65" text-anchor="middle" font-size="14" fill="#fff" font-weight="bold">{{ index + 1 }}</text>
+
+ <!-- 宸ュ簭鍚嶇О -->
+ <text x="60" y="110" text-anchor="middle" font-size="13" :fill="currentProcessIndex === index ? '#409eff' : '#303133'" font-weight="600" class="station-name">
+ {{ truncateText(process.technologyOperationName || process.operationName, 8) }}
+ </text>
+
+ <!-- 浜у搧淇℃伅 -->
+ <text x="60" y="130" text-anchor="middle" font-size="10" fill="#909399">
+ {{ truncateText(process.productName, 10) || '鏃犱骇鍝�' }}
+ </text>
+
+ <!-- 鐘舵�佹爣绛� -->
+ <g transform="translate(20, 145)">
+ <rect width="80" height="20" rx="4" :fill="process.isQuality ? '#fdf6ec' : '#f5f7fa'" />
+ <text x="40" y="14" text-anchor="middle" font-size="10" :fill="process.isQuality ? '#e6a23c' : '#909399'">
+ {{ process.isQuality ? '璐ㄦ鐐�' : '鍔犲伐' }}
+ </text>
+ </g>
+
+ <!-- 鎶曟枡鍙� -->
+ <g class="feed-point" :transform="`translate(50, 218)`">
+ <circle r="10" fill="#fff" stroke="#409eff" stroke-width="2" :class="{ 'is-feeding': feedingIndex === index }" />
+ <text y="4" text-anchor="middle" font-size="10" fill="#409eff">鎶�</text>
+ </g>
+
+ <!-- 鐘舵�佹寚绀虹伅 -->
+ <circle cx="110" cy="90" r="6" :fill="completedIndex > index ? '#67c23a' : currentProcessIndex === index ? '#e6a23c' : '#c0c4cc'" :class="{ 'is-pulsing': currentProcessIndex === index }" />
+
+ <!-- 鎶曟枡鍔ㄧ敾灏忕悆 -->
+ <g v-if="feedingIndex === index" class="feed-ball">
+ <circle cx="60" cy="200" r="8" fill="#67c23a">
+ <animate attributeName="cy" values="200;280" dur="0.3s" fill="freeze" />
+ <animate attributeName="opacity" values="1;0" dur="0.3s" fill="freeze" />
+ </circle>
+ </g>
+ </g>
+
+ <!-- 绉诲姩鐨勪骇鍝佺瀛� -->
+ <g class="product-boxes">
+ <g v-for="(box, bIndex) in visibleBoxes" :key="bIndex" class="product-box" :transform="`translate(${box.x}, ${box.y})`">
+ <!-- 绠卞瓙涓讳綋 - 鏀惧ぇ灏哄 -->
+ <rect x="0" y="0" width="70" height="45" rx="8" :fill="box.color" opacity="0.95" />
+ <rect x="0" y="15" width="70" height="3" fill="#fff" opacity="0.3" />
+ <!-- 浜у搧鍚嶇О -->
+ <text x="35" y="35" text-anchor="middle" font-size="12" fill="#fff" font-weight="600">{{ box.label }}</text>
+ <!-- 鎮仠鎻愮ず妗� -->
+ <title>{{ box.fullName || box.label }}</title>
+ </g>
+ </g>
+
+ <!-- 鎶曟枡鍔ㄧ敾锛氬師鏂欎粠宸ュ簭鑺傜偣钀戒笅鍒颁骇鍝佷笂 -->
+ <g v-if="showFeedingAnimation && feedingProcessIndex >= 0" class="feeding-animation">
+ <!-- 鎶曞叆鐨勫師鏂欏皬鐞� -->
+ <circle
+ :cx="getStationX(feedingProcessIndex) + 60"
+ :cy="feedingAnimY"
+ r="12"
+ fill="#67c23a"
+ opacity="0.9"
+ >
+ <animate
+ attributeName="cy"
+ :values="`${feedingStartY};${feedingEndY}`"
+ :dur="`${0.4 / parseFloat(speed)}s`"
+ fill="freeze"
+ />
+ <animate
+ attributeName="opacity"
+ values="1;0.6"
+ :dur="`${0.4 / parseFloat(speed)}s`"
+ fill="freeze"
+ />
+ </circle>
+ <!-- 鍘熸枡鍚嶇О -->
+ <text
+ :x="getStationX(feedingProcessIndex) + 60"
+ :y="feedingAnimY - 18"
+ text-anchor="middle"
+ font-size="10"
+ fill="#67c23a"
+ font-weight="600"
+ opacity="0.9"
+ >
+ {{ truncateText(feedingMaterialName, 5) || '鍘熸枡' }}
+ </text>
+ </g>
+
+ <!-- 浜у嚭鍔ㄧ敾锛氬姞宸ュ畬鎴愬悗鐨勯棯鍏夋晥鏋� -->
+ <g v-if="showOutputAnimation && outputProcessIndex >= 0" class="output-animation">
+ <circle
+ :cx="getStationX(outputProcessIndex) + 60"
+ cy="285"
+ r="30"
+ fill="none"
+ stroke="#409eff"
+ stroke-width="3"
+ opacity="0.8"
+ >
+ <animate
+ attributeName="r"
+ values="20;40"
+ dur="0.5s"
+ fill="freeze"
+ />
+ <animate
+ attributeName="opacity"
+ values="0.8;0"
+ dur="0.5s"
+ fill="freeze"
+ />
+ </circle>
+ <!-- 浜у嚭鍚嶇О鎻愮ず -->
+ <text
+ :x="getStationX(outputProcessIndex) + 60"
+ y="340"
+ text-anchor="middle"
+ font-size="11"
+ fill="#409eff"
+ font-weight="600"
+ >
+ 浜у嚭: {{ truncateText(outputProductName, 6) || '浜у搧' }}
+ </text>
+ </g>
+
+ <!-- 杩涘害鎸囩ず绾� -->
+ <path class="progress-line" :d="getProgressPath()" stroke="#67c23a" stroke-width="3" fill="none" opacity="0.6" />
+
+ <!-- 璇存槑鏂囧瓧鍖哄煙 -->
+ <g class="info-text" v-if="currentProcess">
+ <rect x="200" y="10" width="300" height="50" rx="8" fill="#ecf5ff" stroke="#409eff" stroke-width="1" opacity="0.9" />
+ <text x="350" y="35" text-anchor="middle" font-size="14" fill="#409eff" font-weight="600">
+ 褰撳墠宸ュ簭锛歿{ currentProcess.technologyOperationName || currentProcess.operationName }}
+ </text>
+ <text x="350" y="50" text-anchor="middle" font-size="11" fill="#606266">
+ {{ feedingIndex >= 0 ? '姝e湪鎶曟枡...' : '浜у搧娴佽浆涓�...' }}
+ </text>
+ </g>
+ </svg>
+ </div>
+
+ <div class="factory-sidebar">
+ <el-card class="info-card" shadow="hover">
+ <template #header>
+ <span>鐢熶骇绾跨姸鎬�</span>
+ </template>
+ <div class="status-info">
+ <div class="status-item">
+ <span class="status-label">宸ュ簭鎬绘暟</span>
+ <span class="status-value">{{ processList.length }}</span>
+ </div>
+ <div class="status-item">
+ <span class="status-label">宸插畬鎴�</span>
+ <span class="status-value">{{ completedIndex }} / {{ processList.length }}</span>
+ </div>
+ <div class="status-item">
+ <span class="status-label">褰撳墠宸ュ簭</span>
+ <span class="status-value highlight">{{ currentProcess?.technologyOperationName || currentProcess?.operationName || '-' }}</span>
+ </div>
+ <div class="status-item">
+ <span class="status-label">鐢熶骇鐘舵��</span>
+ <el-tag :type="isPlaying ? 'success' : 'info'" size="small">{{ isPlaying ? '杩愯涓�' : '宸叉殏鍋�' }}</el-tag>
+ </div>
+ </div>
+ </el-card>
+
+ <el-card class="legend-card" shadow="hover">
+ <template #header>
+ <span>鐘舵�佽鏄�</span>
+ </template>
+ <div class="legend-item">
+ <span class="legend-dot completed"></span>
+ <span class="legend-text">宸插畬鎴�</span>
+ </div>
+ <div class="legend-item">
+ <span class="legend-dot current"></span>
+ <span class="legend-text">褰撳墠宸ュ簭</span>
+ </div>
+ <div class="legend-item">
+ <span class="legend-dot pending"></span>
+ <span class="legend-text">寰呭姞宸�</span>
+ </div>
+ <div class="legend-item">
+ <span class="legend-dot quality"></span>
+ <span class="legend-text">璐ㄦ鐐�</span>
+ </div>
+ </el-card>
+
+ <el-card class="tips-card" shadow="hover">
+ <template #header>
+ <span>鎿嶄綔鎻愮ず</span>
+ </template>
+ <div class="tips-content">
+ <p>鐐瑰嚮宸ュ簭鑺傜偣鏌ョ湅璇︾粏淇℃伅</p>
+ <p>鐐瑰嚮"鍘熸潗鏂欏叆搴�"鏌ョ湅鎶曟枡娓呭崟</p>
+ <p>鐐瑰嚮"鎴愬搧鍑哄簱"鏌ョ湅浜у嚭淇℃伅</p>
+ <p>浣跨敤鎺у埗闈㈡澘璋冭妭婕旂ず閫熷害</p>
+ </div>
+ </el-card>
+ </div>
+ </div>
+
+ <!-- 宸ュ簭璇︽儏瀵硅瘽妗� -->
+ <el-dialog v-model="processDialogVisible" :title="`宸ュ簭璇︽儏 - ${selectedProcess?.technologyOperationName || selectedProcess?.operationName}`" width="650px">
+ <el-descriptions :column="2" border size="small" v-if="selectedProcess">
+ <el-descriptions-item label="宸ュ簭鍚嶇О">{{ selectedProcess.technologyOperationName || selectedProcess.operationName }}</el-descriptions-item>
+ <el-descriptions-item label="宸ュ簭搴忓彿">{{ selectedProcess.dragSort || selectedProcessIndex + 1 }}</el-descriptions-item>
+ <el-descriptions-item label="浜у搧鍚嶇О">{{ selectedProcess.productName || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="瑙勬牸鍨嬪彿">{{ selectedProcess.model || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鍗曚綅">{{ selectedProcess.unit || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="璁¤垂绫诲瀷">
+ <el-tag :type="selectedProcess.type === 1 ? 'primary' : 'success'" size="small">
+ {{ selectedProcess.type === 1 ? '璁′欢' : '璁℃椂' }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="鏄惁璐ㄦ">
+ <el-tag :type="selectedProcess.isQuality ? 'warning' : 'info'" size="small">{{ selectedProcess.isQuality ? '鏄�' : '鍚�' }}</el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="鏄惁鐢熶骇">
+ <el-tag :type="selectedProcess.isProduction ? 'success' : 'info'" size="small">{{ selectedProcess.isProduction ? '鏄�' : '鍚�' }}</el-tag>
+ </el-descriptions-item>
+ </el-descriptions>
+
+ <!-- 鎶曞叆鐗╂枡 -->
+ <div class="material-section" v-if="inputMaterials.length > 0">
+ <div class="section-header">
+ <el-icon style="color: #67c23a;"><Bottom /></el-icon>
+ <h4>闇�瑕佹姇鍏ョ殑鐗╂枡锛坽{ inputMaterials.length }} 绉嶏級</h4>
+ </div>
+ <el-table :data="inputMaterials" border size="small" max-height="200">
+ <el-table-column prop="productName" label="鐗╂枡鍚嶇О" min-width="120" />
+ <el-table-column prop="model" label="瑙勬牸鍨嬪彿" min-width="100" />
+ <el-table-column prop="unitQuantity" label="鍗曚綅鐢ㄩ噺" width="90" align="right">
+ <template #default="{ row }">
+ <span class="quantity-value">{{ row.unitQuantity }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="unit" label="鍗曚綅" width="70" align="center" />
+ </el-table>
+ <div class="material-summary">
+ <el-tag type="success" size="small" effect="plain">
+ 鐢熶骇1涓崟浣嶄骇鍝侀渶瑕佹姇鍏ヤ互涓� {{ inputMaterials.length }} 绉嶇墿鏂�
+ </el-tag>
+ </div>
+ </div>
+ <div class="material-section empty-section" v-else>
+ <el-empty description="璇ュ伐搴忔殏鏃犻渶瑕佹姇鍏ョ殑鐗╂枡" :image-size="60" />
+ </div>
+
+ <!-- 浜у嚭浜у搧 -->
+ <div class="material-section" v-if="outputProduct">
+ <div class="section-header">
+ <el-icon style="color: #409eff;"><Top /></el-icon>
+ <h4>浜у嚭浜у搧锛堟湰宸ュ簭浜у嚭锛�</h4>
+ </div>
+ <el-descriptions :column="2" border size="small">
+ <el-descriptions-item label="浜у搧鍚嶇О">{{ outputProduct.productName || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="瑙勬牸鍨嬪彿">{{ outputProduct.model || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鍗曚綅">{{ outputProduct.unit || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="浜у嚭绫诲瀷">{{ selectedProcess?.isProduction ? '鐢熶骇浜у嚭' : '鍔犲伐浜у嚭' }}</el-descriptions-item>
+ </el-descriptions>
+ </div>
+
+ <!-- 涓婁笅娓稿伐搴� -->
+ <div class="material-section" v-if="selectedProcessIndex >= 0">
+ <div class="section-header">
+ <el-icon style="color: #909399;"><Sort /></el-icon>
+ <h4>宸ュ簭娴佽浆</h4>
+ </div>
+ <div class="process-flow">
+ <div class="flow-item" v-if="prevProcess">
+ <span class="flow-label">涓婁竴閬撳伐搴�</span>
+ <el-tag type="info" size="small">{{ prevProcess.technologyOperationName || prevProcess.operationName }}</el-tag>
+ </div>
+ <div class="flow-item current">
+ <span class="flow-label">褰撳墠宸ュ簭</span>
+ <el-tag type="primary">{{ selectedProcess?.technologyOperationName || selectedProcess?.operationName }}</el-tag>
+ </div>
+ <div class="flow-item" v-if="nextProcess">
+ <span class="flow-label">涓嬩竴閬撳伐搴�</span>
+ <el-tag type="info" size="small">{{ nextProcess.technologyOperationName || nextProcess.operationName }}</el-tag>
+ </div>
+ </div>
+ </div>
+
+ <template #footer>
+ <el-button @click="processDialogVisible = false">鍏抽棴</el-button>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
+import { useRouter } from 'vue-router'
+import { VideoPlay, VideoPause, RefreshRight, Bottom, Top, Sort } from '@element-plus/icons-vue'
+import { listPage as getRouteList } from '@/api/productionManagement/processRoute.js'
+import { findProcessRouteItemList } from '@/api/productionManagement/processRouteItem.js'
+import { queryList } from '@/api/productionManagement/productStructure.js'
+
+const router = useRouter()
+
+const loading = ref(false)
+const routeList = ref([])
+const selectedRouteId = ref(null)
+const selectedBomId = ref(null)
+const processList = ref([])
+const bomData = ref([])
+const isPlaying = ref(false)
+const speed = ref('1')
+const currentProcessIndex = ref(-1)
+const completedIndex = ref(0)
+const feedingIndex = ref(-1)
+const processDialogVisible = ref(false)
+const selectedProcess = ref(null)
+const selectedProcessIndex = ref(-1)
+const boxPosition = ref(0)
+const animationTimer = ref(null)
+const boxColors = ['#ffb15f', '#28d9cd', '#8b5cf6', '#409eff', '#67c23a']
+
+// 鎶曟枡鍔ㄧ敾鐩稿叧
+const showFeedingAnimation = ref(false)
+const feedingProcessIndex = ref(-1)
+const feedingMaterialName = ref('')
+const feedingAnimY = ref(200)
+const feedingStartY = 200
+const feedingEndY = 285
+
+// 浜у嚭鍔ㄧ敾鐩稿叧
+const showOutputAnimation = ref(false)
+const outputProcessIndex = ref(-1)
+const outputProductName = ref('')
+
+// 涓婁竴娆″畬鎴愮殑宸ュ簭绱㈠紩锛堢敤浜庡垽鏂槸鍚﹂渶瑕佽Е鍙戜骇鍑哄姩鐢伙級
+const lastCompletedIndex = ref(-1)
+
+const productionProgress = computed(() => {
+ if (processList.value.length === 0) return 0
+ return Math.round((completedIndex.value / processList.value.length) * 100)
+})
+
+const currentProcess = computed(() => {
+ if (currentProcessIndex.value >= 0 && currentProcessIndex.value < processList.value.length) {
+ return processList.value[currentProcessIndex.value]
+ }
+ return null
+})
+
+const processMaterials = computed(() => {
+ if (!selectedProcess.value) return []
+ const processName = selectedProcess.value.technologyOperationName || selectedProcess.value.operationName
+ const materials = []
+ const traverse = (nodes) => {
+ for (const node of nodes) {
+ if (node.processName === processName) {
+ materials.push(node)
+ }
+ if (node.children) traverse(node.children)
+ }
+ }
+ traverse(bomData.value)
+ return materials
+})
+
+// 鎶曞叆鐗╂枡锛堥渶瑕佹姇鍏ョ殑鐗╂枡 - 浠嶣OM涓尮閰嶅伐搴忥級
+const inputMaterials = computed(() => {
+ if (!selectedProcess.value) return []
+ const processId = selectedProcess.value.technologyOperationId || selectedProcess.value.operationId
+ const processName = selectedProcess.value.technologyOperationName || selectedProcess.value.operationName
+
+ const materials = []
+ const traverse = (nodes) => {
+ for (const node of nodes) {
+ // 浼樺厛鐢↖D鍖归厤锛屽叾娆$敤鍚嶇О鍖归厤
+ const nodeProcessId = node.processId || node.operationId || node.technologyOperationId
+ const nodeProcessName = node.processName || node.operationName || node.technologyOperationName
+
+ const isMatch = (processId && nodeProcessId === processId) ||
+ (processName && nodeProcessName === processName)
+
+ if (isMatch) {
+ materials.push({
+ productName: node.productName || '鏈懡鍚嶇墿鏂�',
+ model: node.model || '-',
+ unitQuantity: node.unitQuantity || 1,
+ unit: node.unit || '-',
+ processName: nodeProcessName || '-'
+ })
+ }
+ if (node.children && node.children.length > 0) {
+ traverse(node.children)
+ }
+ }
+ }
+ traverse(bomData.value)
+ return materials
+})
+
+// 浜у嚭浜у搧锛堝綋鍓嶅伐搴忓叧鑱旂殑浜у搧淇℃伅锛�
+const outputProduct = computed(() => {
+ if (!selectedProcess.value) return null
+ return {
+ productName: selectedProcess.value.productName || null,
+ model: selectedProcess.value.model || null,
+ unit: selectedProcess.value.unit || null
+ }
+})
+
+// 涓婁竴閬撳伐搴�
+const prevProcess = computed(() => {
+ if (selectedProcessIndex.value <= 0) return null
+ return processList.value[selectedProcessIndex.value - 1]
+})
+
+// 涓嬩竴閬撳伐搴�
+const nextProcess = computed(() => {
+ if (selectedProcessIndex.value < 0 || selectedProcessIndex.value >= processList.value.length - 1) return null
+ return processList.value[selectedProcessIndex.value + 1]
+})
+
+const visibleBoxes = computed(() => {
+ const boxes = []
+ const baseX = 60 + boxPosition.value
+
+ // 璁$畻浜у搧褰撳墠鎵�鍦ㄧ殑宸ュ簭绱㈠紩
+ const totalWidth = 900
+ const progressRatio = Math.min(1, boxPosition.value / totalWidth)
+ const currentStep = Math.floor(progressRatio * processList.value.length)
+
+ // 褰撳墠宸ュ簭
+ const currentProc = processList.value[currentStep] || null
+ // 涓婁竴閬撳伐搴忥紙宸插畬鎴愶級
+ const prevProc = processList.value[currentStep - 1] || null
+
+ // 纭畾浜у搧鍚嶇О
+ let productLabel = '鍘熸枡'
+ let fullName = '鍘熸潗鏂欙紙寰呭姞宸ワ級'
+ if (currentStep > 0 && prevProc) {
+ // 宸茬粡杩囦簡涓�浜涘伐搴忥紝鏄剧ず涓婁竴涓伐搴忕殑浜у嚭
+ productLabel = truncateText(prevProc.productName, 5) || '鍗婃垚鍝�'
+ fullName = prevProc.productName || '鍗婃垚鍝�'
+ }
+ if (currentStep >= processList.value.length) {
+ // 鍏ㄩ儴瀹屾垚
+ const lastProc = processList.value[processList.value.length - 1]
+ productLabel = truncateText(lastProc?.productName, 5) || '鎴愬搧'
+ fullName = lastProc?.productName || '鎴愬搧'
+ }
+
+ if (baseX > 40) {
+ boxes.push({
+ x: baseX,
+ y: 275, // 璋冩暣浣嶇疆锛岃绠卞瓙鍦ㄥ饱甯︿笂
+ color: '#409eff',
+ label: productLabel,
+ fullName: fullName,
+ processIndex: currentStep,
+ isFeeding: feedingIndex.value === currentStep && currentStep < processList.value.length
+ })
+ }
+
+ return boxes
+})
+
+// 褰撳墠宸ュ簭鐨勬姇鍏ョ墿鏂欏悕绉�
+const currentInputMaterial = computed(() => {
+ if (currentProcessIndex.value < 0 || currentProcessIndex.value >= processList.value.length) return null
+ const process = processList.value[currentProcessIndex.value]
+ if (!process) return null
+
+ const processId = process.technologyOperationId || process.operationId
+ const processName = process.technologyOperationName || process.operationName
+
+ // 浠嶣OM涓壘绗竴涓尮閰嶇殑鐗╂枡
+ let material = null
+ const traverse = (nodes) => {
+ for (const node of nodes) {
+ const nodeProcessId = node.processId || node.operationId || node.technologyOperationId
+ const nodeProcessName = node.processName || node.operationName || node.technologyOperationName
+ if ((processId && nodeProcessId === processId) || (processName && nodeProcessName === processName)) {
+ material = node
+ return true
+ }
+ if (node.children && node.children.length > 0) {
+ if (traverse(node.children)) return true
+ }
+ }
+ return false
+ }
+ traverse(bomData.value)
+ return material
+})
+
+const getStationX = (index) => {
+ const totalStations = processList.value.length
+ if (totalStations === 0) return 0
+ const startX = 140
+ const spacing = Math.min(180, (900 - startX) / totalStations)
+ return startX + index * spacing
+}
+
+const truncateText = (text, maxLen) => {
+ if (!text) return ''
+ return text.length > maxLen ? text.substring(0, maxLen) + '..' : text
+}
+
+const getProgressPath = () => {
+ if (completedIndex.value === 0) return ''
+ const endX = getStationX(Math.min(completedIndex.value, processList.value.length) - 1) + 60
+ return `M60 350 L${endX} 350`
+}
+
+const fetchRouteList = async () => {
+ try {
+ const res = await getRouteList({ current: 1, size: 100 })
+ routeList.value = res?.data?.records || []
+ } catch (error) {
+ console.error('鑾峰彇宸ヨ壓璺嚎鍒楄〃澶辫触:', error)
+ }
+}
+
+const fetchProcessList = async () => {
+ if (!selectedRouteId.value) return
+ loading.value = true
+ try {
+ const res = await findProcessRouteItemList({ routeId: selectedRouteId.value })
+ processList.value = res?.data || []
+ processList.value.sort((a, b) => (a.dragSort || 0) - (b.dragSort || 0))
+
+ // 鑾峰彇鍏宠仈鐨凚OM鏁版嵁
+ const routeInfo = routeList.value.find(r => r.id === selectedRouteId.value)
+ if (routeInfo?.bomId) {
+ selectedBomId.value = routeInfo.bomId
+ const bomRes = await queryList(routeInfo.bomId)
+ bomData.value = bomRes?.data || []
+ }
+ } catch (error) {
+ console.error('鑾峰彇宸ヨ壓璺嚎鏁版嵁澶辫触:', error)
+ } finally {
+ loading.value = false
+ }
+}
+
+const handleRouteChange = () => {
+ handleReset()
+ fetchProcessList()
+}
+
+const togglePlay = () => {
+ isPlaying.value = !isPlaying.value
+ if (isPlaying.value) {
+ startAnimation()
+ } else {
+ stopAnimation()
+ }
+}
+
+const handleReset = () => {
+ stopAnimation()
+ isPlaying.value = false
+ currentProcessIndex.value = -1
+ completedIndex.value = 0
+ feedingIndex.value = -1
+ boxPosition.value = 0
+ showFeedingAnimation.value = false
+ showOutputAnimation.value = false
+ feedingProcessIndex.value = -1
+ outputProcessIndex.value = -1
+ lastCompletedIndex.value = -1
+}
+
+const startAnimation = () => {
+ const speedFactor = parseFloat(speed.value)
+ const interval = 2000 / speedFactor
+ const moveStep = 20 / speedFactor
+
+ animationTimer.value = setInterval(() => {
+ if (!isPlaying.value) return
+
+ // 鏇存柊绠卞瓙浣嶇疆
+ boxPosition.value += moveStep
+
+ // 璁$畻褰撳墠搴旇鍦ㄥ摢涓伐搴�
+ const totalWidth = 900
+ const progressRatio = boxPosition.value / totalWidth
+ const newIndex = Math.floor(progressRatio * processList.value.length)
+
+ // 杩涘叆鏂板伐搴忔椂瑙﹀彂鎶曟枡鍔ㄧ敾
+ if (newIndex !== currentProcessIndex.value && newIndex < processList.value.length) {
+ // 鍏堣Е鍙戜骇鍑哄姩鐢伙紙涓婁竴閬撳伐搴忓畬鎴愶級
+ if (currentProcessIndex.value >= 0 && currentProcessIndex.value < processList.value.length) {
+ const prevProcess = processList.value[currentProcessIndex.value]
+ triggerOutputAnimation(currentProcessIndex.value, prevProcess?.productName || '浜у搧')
+ }
+
+ currentProcessIndex.value = newIndex
+
+ // 瑙﹀彂鎶曟枡鍔ㄧ敾
+ const process = processList.value[newIndex]
+ const inputMaterial = getInputMaterialForProcess(process)
+ triggerFeedingAnimation(newIndex, inputMaterial?.productName || '鍘熸枡')
+
+ feedingIndex.value = newIndex
+ setTimeout(() => {
+ feedingIndex.value = -1
+ }, 500 / speedFactor)
+ }
+
+ // 鍒ゆ柇鏄惁瀹屾垚
+ if (boxPosition.value >= totalWidth) {
+ // 鏈�鍚庝竴涓伐搴忓畬鎴愶紝瑙﹀彂浜у嚭鍔ㄧ敾
+ if (currentProcessIndex.value >= 0 && currentProcessIndex.value < processList.value.length) {
+ const lastProcess = processList.value[currentProcessIndex.value]
+ triggerOutputAnimation(currentProcessIndex.value, lastProcess?.productName || '鎴愬搧')
+ }
+
+ completedIndex.value = processList.value.length
+ currentProcessIndex.value = -1
+ // 寰幆鎾斁
+ setTimeout(() => {
+ boxPosition.value = 0
+ completedIndex.value = 0
+ currentProcessIndex.value = -1
+ lastCompletedIndex.value = -1
+ showFeedingAnimation.value = false
+ showOutputAnimation.value = false
+ }, 1500 / speedFactor)
+ } else if (newIndex >= 0) {
+ completedIndex.value = newIndex
+ }
+ }, interval)
+}
+
+// 鑾峰彇宸ュ簭闇�瑕佹姇鍏ョ殑鐗╂枡
+const getInputMaterialForProcess = (process) => {
+ if (!process) return null
+ const processId = process.technologyOperationId || process.operationId
+ const processName = process.technologyOperationName || process.operationName
+
+ let material = null
+ const traverse = (nodes) => {
+ for (const node of nodes) {
+ const nodeProcessId = node.processId || node.operationId || node.technologyOperationId
+ const nodeProcessName = node.processName || node.operationName || node.technologyOperationName
+ if ((processId && nodeProcessId === processId) || (processName && nodeProcessName === processName)) {
+ material = node
+ return true
+ }
+ if (node.children && node.children.length > 0) {
+ if (traverse(node.children)) return true
+ }
+ }
+ return false
+ }
+ traverse(bomData.value)
+ return material
+}
+
+// 瑙﹀彂鎶曟枡鍔ㄧ敾
+const triggerFeedingAnimation = (processIndex, materialName) => {
+ showFeedingAnimation.value = true
+ feedingProcessIndex.value = processIndex
+ feedingMaterialName.value = materialName
+ feedingAnimY.value = feedingStartY
+
+ setTimeout(() => {
+ showFeedingAnimation.value = false
+ }, 600 / parseFloat(speed.value))
+}
+
+// 瑙﹀彂浜у嚭鍔ㄧ敾
+const triggerOutputAnimation = (processIndex, productName) => {
+ showOutputAnimation.value = true
+ outputProcessIndex.value = processIndex
+ outputProductName.value = productName
+
+ setTimeout(() => {
+ showOutputAnimation.value = false
+ }, 800 / parseFloat(speed.value))
+}
+
+const stopAnimation = () => {
+ if (animationTimer.value) {
+ clearInterval(animationTimer.value)
+ animationTimer.value = null
+ }
+}
+
+const showProcessDetail = (process) => {
+ selectedProcess.value = process
+ // 鏌ユ壘宸ュ簭绱㈠紩
+ selectedProcessIndex.value = processList.value.findIndex(p =>
+ p.id === process.id ||
+ (p.technologyOperationName === process.technologyOperationName && p.productName === process.productName)
+ )
+ processDialogVisible.value = true
+}
+
+const showInputDialog = () => {
+ // 鍙互灞曠ず鍘熸潗鏂欏叆搴撲俊鎭�
+}
+
+const showOutputDialog = () => {
+ // 鍙互灞曠ず鎴愬搧鍑哄簱淇℃伅
+}
+
+const goBack = () => {
+ router.push('/productionManagement/teachingDemo')
+}
+
+watch(speed, () => {
+ if (isPlaying.value) {
+ stopAnimation()
+ startAnimation()
+ }
+})
+
+onMounted(() => {
+ fetchRouteList()
+})
+
+onUnmounted(() => {
+ stopAnimation()
+})
+</script>
+
+<style scoped lang="scss">
+.factory-demo {
+ .control-panel {
+ background: #fff;
+ padding: 20px;
+ border-radius: 12px;
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+ margin-bottom: 20px;
+
+ .speed-control {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .progress-info {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+ }
+
+ .factory-container {
+ display: flex;
+ gap: 20px;
+ min-height: 500px;
+
+ .factory-scene {
+ flex: 1;
+ background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
+ border-radius: 16px;
+ padding: 20px;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
+ overflow: hidden;
+
+ .factory-svg {
+ width: 100%;
+ height: auto;
+ min-height: 450px;
+
+ .conveyor-system {
+ .belt-flow {
+ animation: beltFlow 0.8s linear infinite;
+ }
+ }
+
+ .work-station {
+ cursor: pointer;
+
+ &:hover {
+ filter: brightness(1.1) drop-shadow(0 0 12px rgba(64, 158, 255, 0.4));
+ }
+
+ &.is-active {
+ .station-name {
+ animation: textGlow 1s ease-in-out infinite;
+ }
+ }
+
+ .feed-point {
+ transition: all 0.3s ease;
+
+ &.is-feeding {
+ animation: feedPulse 0.3s ease-out;
+ }
+ }
+
+ .is-pulsing {
+ animation: statusPulse 1s ease-in-out infinite;
+ }
+ }
+
+ .product-box {
+ transition: transform 0.1s linear;
+ }
+
+ .product-boxes {
+ animation: boxesMove calc(8s / var(--speed, 1)) linear infinite;
+ }
+
+ .info-text {
+ animation: fadeInOut 0.5s ease;
+ }
+ }
+ }
+
+ .factory-sidebar {
+ width: 280px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ .info-card {
+ .status-info {
+ .status-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px 0;
+ border-bottom: 1px solid #f0f0f0;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ .status-label {
+ color: #909399;
+ font-size: 13px;
+ }
+
+ .status-value {
+ color: #303133;
+ font-weight: 600;
+
+ &.highlight {
+ color: #409eff;
+ }
+ }
+ }
+ }
+ }
+
+ .legend-card {
+ .legend-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 8px;
+
+ .legend-dot {
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+
+ &.completed {
+ background: #67c23a;
+ }
+
+ &.current {
+ background: #e6a23c;
+ animation: legendPulse 1s ease-in-out infinite;
+ }
+
+ &.pending {
+ background: #c0c4cc;
+ }
+
+ &.quality {
+ background: #fdf6ec;
+ border: 2px solid #e6a23c;
+ }
+ }
+
+ .legend-text {
+ font-size: 12px;
+ color: #666;
+ }
+ }
+ }
+
+ .tips-card {
+ .tips-content {
+ p {
+ font-size: 12px;
+ color: #909399;
+ margin-bottom: 8px;
+ line-height: 1.6;
+
+ &:before {
+ content: '鈥� ';
+ color: #409eff;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ .material-section {
+ margin-top: 16px;
+
+ .section-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 12px;
+
+ h4 {
+ color: #303133;
+ font-size: 14px;
+ margin: 0;
+ font-weight: 600;
+ }
+ }
+
+ &.empty-section {
+ margin-top: 12px;
+ }
+
+ .quantity-value {
+ font-weight: 600;
+ color: #67c23a;
+ }
+
+ .material-summary {
+ margin-top: 8px;
+ }
+
+ .process-flow {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ flex-wrap: wrap;
+
+ .flow-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ .flow-label {
+ font-size: 12px;
+ color: #909399;
+ }
+
+ &.current {
+ .flow-label {
+ color: #409eff;
+ font-weight: 600;
+ }
+ }
+ }
+ }
+ }
+}
+
+@keyframes beltFlow {
+ to {
+ stroke-dashoffset: -35;
+ }
+}
+
+@keyframes statusPulse {
+ 0%, 100% {
+ opacity: 0.6;
+ transform: scale(1);
+ }
+ 50% {
+ opacity: 1;
+ transform: scale(1.2);
+ }
+}
+
+@keyframes feedPulse {
+ 0% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.3);
+ fill: #67c23a;
+ }
+ 100% {
+ transform: scale(1);
+ }
+}
+
+@keyframes textGlow {
+ 0%, 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.8;
+ }
+}
+
+@keyframes legendPulse {
+ 0%, 100% {
+ box-shadow: 0 0 4px rgba(230, 162, 60, 0.4);
+ }
+ 50% {
+ box-shadow: 0 0 8px rgba(230, 162, 60, 0.6);
+ }
+}
+
+@keyframes fadeInOut {
+ from {
+ opacity: 0;
+ transform: translateY(-10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+</style>
\ No newline at end of file
diff --git a/src/views/productionManagement/teachingDemo/ProcessRouteDemo.vue b/src/views/productionManagement/teachingDemo/ProcessRouteDemo.vue
new file mode 100644
index 0000000..29f16a6
--- /dev/null
+++ b/src/views/productionManagement/teachingDemo/ProcessRouteDemo.vue
@@ -0,0 +1,535 @@
+<template>
+ <div class="app-container process-route-demo">
+ <PageHeader :content="`宸ヨ壓璺嚎婕旂ず - ${routeInfo.productName || ''}`">
+ <template #right-button>
+ <el-button @click="goBack">杩斿洖</el-button>
+ </template>
+ </PageHeader>
+
+ <DemoControls
+ ref="controlsRef"
+ :steps="demoSteps"
+ :explanations="demoExplanations"
+ @play="handlePlay"
+ @pause="handlePause"
+ @reset="handleReset"
+ @stepChange="handleStepChange"
+ @speedChange="handleSpeedChange"
+ />
+
+ <div class="demo-container" v-loading="loading">
+ <div class="route-visualization">
+ <div class="process-flow-container" v-if="processList.length > 0">
+ <div class="flow-wrapper">
+ <ProcessNode
+ v-for="(process, index) in processList"
+ :key="process.id || index"
+ :process="process"
+ :index="index"
+ :delay="index * 200"
+ :animation-speed="animationSpeed"
+ :active="activeProcessId === process.id"
+ :completed="completedProcesses.includes(process.id)"
+ :current="currentProcessId === process.id"
+ :animating="isAnimating && currentProcessId === process.id"
+ :show-arrow="index < processList.length - 1"
+ :show-progress="showProgress"
+ :progress="getProcessProgress(process)"
+ @click="handleProcessClick"
+ />
+ </div>
+
+ <div class="flow-start-end">
+ <div class="flow-node start">
+ <el-icon><Goods /></el-icon>
+ <span>鍘熸潗鏂�</span>
+ </div>
+ <div class="flow-connector">
+ <svg width="60" height="24">
+ <line x1="0" y1="12" x2="50" y2="12" stroke="#67c23a" stroke-width="2" stroke-dasharray="5,5" />
+ <polygon points="50,8 60,12 50,16" fill="#67c23a" />
+ </svg>
+ </div>
+ <div class="flow-node end">
+ <el-icon><Box /></el-icon>
+ <span>鎴愬搧</span>
+ </div>
+ </div>
+ </div>
+
+ <el-empty v-else description="鏆傛棤宸ヨ壓璺嚎鏁版嵁" />
+
+ <div class="visualization-sidebar">
+ <el-card class="info-card" shadow="hover">
+ <template #header>
+ <span>褰撳墠宸ュ簭淇℃伅</span>
+ </template>
+ <el-descriptions :column="1" border size="small" v-if="activeProcess">
+ <el-descriptions-item label="宸ュ簭鍚嶇О">{{ activeProcess.technologyOperationName || activeProcess.operationName }}</el-descriptions-item>
+ <el-descriptions-item label="浜у搧鍚嶇О">{{ activeProcess.productName || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="瑙勬牸鍨嬪彿">{{ activeProcess.model || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鍗曚綅">{{ activeProcess.unit || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="璁¤垂绫诲瀷">{{ activeProcess.type === 1 ? '璁′欢' : '璁℃椂' }}</el-descriptions-item>
+ <el-descriptions-item label="鏄惁璐ㄦ">{{ activeProcess.isQuality ? '鏄�' : '鍚�' }}</el-descriptions-item>
+ <el-descriptions-item label="鏄惁鐢熶骇">{{ activeProcess.isProduction ? '鏄�' : '鍚�' }}</el-descriptions-item>
+ <el-descriptions-item label="宸ュ簭搴忓彿">{{ activeProcess.dragSort || getProcessIndex(activeProcess) + 1 }}</el-descriptions-item>
+ </el-descriptions>
+ <div v-else class="empty-info">鐐瑰嚮宸ュ簭鑺傜偣鏌ョ湅璇︾粏淇℃伅</div>
+ </el-card>
+
+ <el-card class="progress-card" shadow="hover">
+ <template #header>
+ <span>鐢熶骇杩涘害妯℃嫙</span>
+ </template>
+ <div class="progress-info">
+ <div class="progress-stat">
+ <span class="stat-label">宸插畬鎴愬伐搴�</span>
+ <span class="stat-value">{{ completedProcesses.length }} / {{ processList.length }}</span>
+ </div>
+ <el-progress
+ :percentage="processList.length > 0 ? (completedProcesses.length / processList.length) * 100 : 0"
+ :stroke-width="10"
+ status="success"
+ />
+ </div>
+ <div class="progress-actions">
+ <el-button type="primary" size="small" @click="simulateProgress">
+ 妯℃嫙杩涘害
+ </el-button>
+ <el-button size="small" @click="resetProgress">
+ 閲嶇疆
+ </el-button>
+ </div>
+ </el-card>
+
+ <el-card class="legend-card" shadow="hover">
+ <template #header>
+ <span>鐘舵�佽鏄�</span>
+ </template>
+ <div class="legend-item">
+ <span class="legend-icon default"></span>
+ <span class="legend-text">寰呭姞宸�</span>
+ </div>
+ <div class="legend-item">
+ <span class="legend-icon current"></span>
+ <span class="legend-text">褰撳墠宸ュ簭</span>
+ </div>
+ <div class="legend-item">
+ <span class="legend-icon completed"></span>
+ <span class="legend-text">宸插畬鎴�</span>
+ </div>
+ <div class="legend-item">
+ <span class="legend-icon active"></span>
+ <span class="legend-text">閫変腑鐘舵��</span>
+ </div>
+ </el-card>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted, onUnmounted } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { Goods, Box } from '@element-plus/icons-vue'
+import ProcessNode from './components/ProcessNode.vue'
+import DemoControls from './components/DemoControls.vue'
+import { findProcessRouteItemList } from '@/api/productionManagement/processRouteItem.js'
+
+const router = useRouter()
+const route = useRoute()
+
+const loading = ref(false)
+const processList = ref([])
+const controlsRef = ref(null)
+const animationSpeed = ref(1)
+const isAnimating = ref(false)
+const activeProcessId = ref(null)
+const activeProcess = ref(null)
+const currentProcessId = ref(null)
+const completedProcesses = ref([])
+const showProgress = ref(false)
+const playTimer = ref(null)
+const progressTimer = ref(null)
+
+const routeId = computed(() => route.query.id)
+const routeInfo = computed(() => ({
+ processRouteCode: route.query.processRouteCode || '',
+ productName: route.query.productName || '',
+ model: route.query.model || '',
+ bomId: route.query.bomId || ''
+}))
+
+const demoSteps = [
+ { title: '宸ュ簭姒傝', description: '鏌ョ湅鍏ㄩ儴宸ュ簭' },
+ { title: '宸ュ簭椤哄簭', description: '浜嗚В娴佺▼' },
+ { title: '宸ュ簭璇︽儏', description: '鏌ョ湅鍙傛暟' },
+ { title: '杩涘害妯℃嫙', description: '鐢熶骇娴佺▼' },
+ { title: '瀹屾垚鐘舵��', description: '鐢熶骇瀹屾垚' }
+]
+
+const demoExplanations = [
+ {
+ title: '绗�1姝ワ細宸ュ簭姒傝',
+ content: '宸ヨ壓璺嚎灞曠ず浜嗕骇鍝佷粠鍘熸潗鏂欏埌鎴愬搧闇�瑕佺粡杩囩殑鎵�鏈夊伐搴忋�傛瘡涓妭鐐逛唬琛ㄤ竴閬撶嫭绔嬬殑鍔犲伐姝ラ銆�'
+ },
+ {
+ title: '绗�2姝ワ細宸ュ簭椤哄簭',
+ content: '绠ご琛ㄧず宸ュ簭鐨勬墽琛岄『搴忋�備骇鍝佹寜鐓х澶存柟鍚戜緷娆$粡杩囨瘡閬撳伐搴忥紝鐩村埌鏈�缁堝畬鎴愩��'
+ },
+ {
+ title: '绗�3姝ワ細宸ュ簭璇︽儏',
+ content: '鐐瑰嚮宸ュ簭鑺傜偣鍙互鏌ョ湅璇︾粏淇℃伅锛屽寘鎷骇鍝佸悕绉般�佽鏍笺�佽璐圭被鍨嬨�佹槸鍚﹂渶瑕佽川妫�绛夈��'
+ },
+ {
+ title: '绗�4姝ワ細杩涘害妯℃嫙',
+ content: '鐐瑰嚮"妯℃嫙杩涘害"鍙互瑙傜湅浜у搧鍦ㄥ悇宸ュ簭闂存祦杞殑杩囩▼銆傜豢鑹茶〃绀哄凡瀹屾垚锛岄粍鑹茶〃绀哄綋鍓嶅伐搴忋��'
+ },
+ {
+ title: '绗�5姝ワ細鐢熶骇瀹屾垚',
+ content: '褰撴墍鏈夊伐搴忛兘瀹屾垚鍚庯紝浜у搧灏辩敓浜у嚭鏉ヤ簡銆傚疄闄呯敓浜т腑锛屾瘡閬撳伐搴忓彲鑳戒細鏈夋洿璇︾粏鐨勫弬鏁板拰鎿嶄綔瑕佹眰銆�'
+ }
+]
+
+const fetchProcessList = async () => {
+ if (!routeId.value) return
+ loading.value = true
+ try {
+ const res = await findProcessRouteItemList({ routeId: routeId.value })
+ processList.value = res?.data || []
+ // 鎸夊簭鍙锋帓搴�
+ processList.value.sort((a, b) => (a.dragSort || 0) - (b.dragSort || 0))
+ } catch (error) {
+ console.error('鑾峰彇宸ヨ壓璺嚎鏁版嵁澶辫触:', error)
+ } finally {
+ loading.value = false
+ }
+}
+
+const goBack = () => {
+ router.push('/productionManagement/teachingDemo')
+}
+
+const handleProcessClick = (process) => {
+ activeProcess.value = process
+ activeProcessId.value = process.id
+}
+
+const getProcessIndex = (process) => {
+ return processList.value.findIndex(p => p.id === process.id)
+}
+
+const getProcessProgress = (process) => {
+ if (completedProcesses.value.includes(process.id)) {
+ return 100
+ }
+ if (currentProcessId.value === process.id) {
+ return 50
+ }
+ return 0
+}
+
+const handlePlay = () => {
+ isAnimating.value = true
+ runAnimation()
+}
+
+const handlePause = () => {
+ isAnimating.value = false
+ if (playTimer.value) {
+ clearTimeout(playTimer.value)
+ }
+}
+
+const handleReset = () => {
+ isAnimating.value = false
+ activeProcessId.value = null
+ activeProcess.value = null
+ currentProcessId.value = null
+ completedProcesses.value = []
+ showProgress.value = false
+ if (playTimer.value) {
+ clearTimeout(playTimer.value)
+ }
+ if (progressTimer.value) {
+ clearInterval(progressTimer.value)
+ }
+}
+
+const handleStepChange = (step) => {
+ switch (step) {
+ case 0:
+ // 宸ュ簭姒傝
+ break
+ case 1:
+ // 宸ュ簭椤哄簭
+ if (processList.value.length > 0) {
+ activeProcess.value = processList.value[0]
+ activeProcessId.value = processList.value[0].id
+ }
+ break
+ case 2:
+ // 宸ュ簭璇︽儏
+ break
+ case 3:
+ // 杩涘害妯℃嫙
+ simulateProgress()
+ break
+ case 4:
+ // 瀹屾垚鐘舵��
+ completedProcesses.value = processList.value.map(p => p.id)
+ currentProcessId.value = null
+ break
+ }
+}
+
+const handleSpeedChange = (speed) => {
+ animationSpeed.value = speed
+}
+
+const simulateProgress = () => {
+ showProgress.value = true
+ completedProcesses.value = []
+ currentProcessId.value = null
+
+ let index = 0
+ const interval = 1500 / animationSpeed.value
+
+ progressTimer.value = setInterval(() => {
+ if (index >= processList.value.length) {
+ clearInterval(progressTimer.value)
+ currentProcessId.value = null
+ isAnimating.value = false
+ controlsRef.value?.setIsPlaying(false)
+ return
+ }
+
+ if (index > 0) {
+ completedProcesses.value.push(processList.value[index - 1].id)
+ }
+
+ currentProcessId.value = processList.value[index].id
+ activeProcess.value = processList.value[index]
+ activeProcessId.value = processList.value[index].id
+
+ index++
+ }, interval)
+}
+
+const resetProgress = () => {
+ if (progressTimer.value) {
+ clearInterval(progressTimer.value)
+ }
+ completedProcesses.value = []
+ currentProcessId.value = null
+ showProgress.value = false
+}
+
+const runAnimation = () => {
+ if (!isAnimating.value) return
+
+ const totalSteps = demoSteps.length
+ let currentStep = controlsRef.value?.currentStep || 0
+
+ const animate = () => {
+ if (!isAnimating.value) return
+ if (currentStep < totalSteps) {
+ controlsRef.value?.setStep(currentStep)
+ handleStepChange(currentStep)
+ currentStep++
+ playTimer.value = setTimeout(animate, 4000 / animationSpeed.value)
+ } else {
+ isAnimating.value = false
+ controlsRef.value?.setIsPlaying(false)
+ }
+ }
+
+ animate()
+}
+
+onMounted(() => {
+ fetchProcessList()
+})
+
+onUnmounted(() => {
+ if (playTimer.value) {
+ clearTimeout(playTimer.value)
+ }
+ if (progressTimer.value) {
+ clearInterval(progressTimer.value)
+ }
+})
+</script>
+
+<style scoped lang="scss">
+.process-route-demo {
+ .demo-container {
+ background: #f5f7fa;
+ padding: 20px;
+ border-radius: 12px;
+ min-height: 400px;
+
+ .route-visualization {
+ display: flex;
+ gap: 20px;
+
+ .process-flow-container {
+ flex: 1;
+ background: #fff;
+ padding: 30px;
+ border-radius: 12px;
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+
+ .flow-wrapper {
+ display: flex;
+ align-items: center;
+ gap: 0;
+ padding: 20px 0;
+ overflow-x: auto;
+ min-height: 200px;
+ }
+
+ .flow-start-end {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-top: 30px;
+ padding-top: 20px;
+ border-top: 1px dashed #e4e7ed;
+
+ .flow-node {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+ padding: 16px 24px;
+ border-radius: 12px;
+
+ &.start {
+ background: linear-gradient(135deg, #f0f9eb 0%, #e1f3d8 100%);
+ border: 2px solid #67c23a;
+
+ .el-icon {
+ color: #67c23a;
+ font-size: 24px;
+ }
+
+ span {
+ color: #67c23a;
+ font-weight: 600;
+ }
+ }
+
+ &.end {
+ background: linear-gradient(135deg, #ecf5ff 0%, #d9ecff 100%);
+ border: 2px solid #409eff;
+
+ .el-icon {
+ color: #409eff;
+ font-size: 24px;
+ }
+
+ span {
+ color: #409eff;
+ font-weight: 600;
+ }
+ }
+ }
+
+ .flow-connector {
+ margin: 0 20px;
+ }
+ }
+ }
+
+ .visualization-sidebar {
+ width: 300px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ .info-card,
+ .progress-card,
+ .legend-card {
+ .empty-info {
+ color: #909399;
+ text-align: center;
+ padding: 20px;
+ }
+ }
+
+ .progress-card {
+ .progress-info {
+ margin-bottom: 16px;
+
+ .progress-stat {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 8px;
+
+ .stat-label {
+ color: #666;
+ font-size: 14px;
+ }
+
+ .stat-value {
+ color: #409eff;
+ font-weight: 600;
+ }
+ }
+ }
+
+ .progress-actions {
+ display: flex;
+ gap: 8px;
+ }
+ }
+
+ .legend-card {
+ .legend-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 8px;
+
+ .legend-icon {
+ width: 20px;
+ height: 20px;
+ border-radius: 4px;
+ border: 2px solid;
+
+ &.default {
+ background: #fff;
+ border-color: #e4e7ed;
+ }
+
+ &.current {
+ background: linear-gradient(135deg, #fdf6ec 0%, #fff 100%);
+ border-color: #e6a23c;
+ box-shadow: 0 0 8px rgba(230, 162, 60, 0.4);
+ }
+
+ &.completed {
+ background: linear-gradient(135deg, #f0f9eb 0%, #fff 100%);
+ border-color: #67c23a;
+ }
+
+ &.active {
+ background: #fff;
+ border-color: #409eff;
+ box-shadow: 0 0 12px rgba(64, 158, 255, 0.5);
+ }
+ }
+
+ .legend-text {
+ font-size: 12px;
+ color: #666;
+ }
+ }
+ }
+ }
+ }
+ }
+}
+</style>
\ No newline at end of file
diff --git a/src/views/productionManagement/teachingDemo/components/DemoControls.vue b/src/views/productionManagement/teachingDemo/components/DemoControls.vue
new file mode 100644
index 0000000..373c1b8
--- /dev/null
+++ b/src/views/productionManagement/teachingDemo/components/DemoControls.vue
@@ -0,0 +1,186 @@
+<template>
+ <div class="demo-controls">
+ <div class="control-buttons">
+ <el-button-group>
+ <el-button :type="isPlaying ? 'warning' : 'primary'" @click="togglePlay">
+ <el-icon v-if="isPlaying"><VideoPause /></el-icon>
+ <el-icon v-else><VideoPlay /></el-icon>
+ {{ isPlaying ? '鏆傚仠' : '鎾斁' }}
+ </el-button>
+ <el-button @click="handleReset">
+ <el-icon><RefreshRight /></el-icon>
+ 閲嶇疆
+ </el-button>
+ </el-button-group>
+
+ <div class="speed-control">
+ <span class="speed-label">閫熷害锛�</span>
+ <el-radio-group v-model="speed" size="small" @change="handleSpeedChange">
+ <el-radio-button label="0.5">鎱㈤��</el-radio-button>
+ <el-radio-button label="1">姝e父</el-radio-button>
+ <el-radio-button label="2">蹇��</el-radio-button>
+ </el-radio-group>
+ </div>
+ </div>
+
+ <div class="step-control" v-if="steps.length > 0">
+ <span class="step-label">姝ラ锛�</span>
+ <el-steps :active="currentStep" align-center>
+ <el-step
+ v-for="(step, index) in steps"
+ :key="index"
+ :title="step.title"
+ :description="step.description"
+ @click.native="handleStepClick(index)"
+ class="clickable-step"
+ />
+ </el-steps>
+ </div>
+
+ <div class="explanation-box" v-if="currentExplanation">
+ <el-alert
+ :title="currentExplanation.title"
+ type="success"
+ :closable="false"
+ show-icon
+ >
+ <template #default>
+ {{ currentExplanation.content }}
+ </template>
+ </el-alert>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, computed, watch } from 'vue'
+import { VideoPlay, VideoPause, RefreshRight } from '@element-plus/icons-vue'
+
+const props = defineProps({
+ steps: {
+ type: Array,
+ default: () => []
+ },
+ explanations: {
+ type: Array,
+ default: () => []
+ }
+})
+
+const emit = defineEmits(['play', 'pause', 'reset', 'stepChange', 'speedChange'])
+
+const isPlaying = ref(false)
+const speed = ref('1')
+const currentStep = ref(0)
+
+const currentExplanation = computed(() => {
+ if (props.explanations && props.explanations[currentStep.value]) {
+ return props.explanations[currentStep.value]
+ }
+ return null
+})
+
+const togglePlay = () => {
+ isPlaying.value = !isPlaying.value
+ if (isPlaying.value) {
+ emit('play')
+ } else {
+ emit('pause')
+ }
+}
+
+const handleReset = () => {
+ isPlaying.value = false
+ currentStep.value = 0
+ emit('reset')
+}
+
+const handleStepClick = (index) => {
+ currentStep.value = index
+ emit('stepChange', index)
+}
+
+const handleSpeedChange = (val) => {
+ emit('speedChange', parseFloat(val))
+}
+
+const nextStep = () => {
+ if (currentStep.value < props.steps.length - 1) {
+ currentStep.value++
+ emit('stepChange', currentStep.value)
+ }
+}
+
+const setStep = (index) => {
+ currentStep.value = index
+}
+
+const setIsPlaying = (val) => {
+ isPlaying.value = val
+}
+
+defineExpose({
+ nextStep,
+ setStep,
+ setIsPlaying,
+ currentStep
+})
+</script>
+
+<style scoped lang="scss">
+.demo-controls {
+ background: #fff;
+ padding: 20px;
+ border-radius: 12px;
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+ margin-bottom: 20px;
+
+ .control-buttons {
+ display: flex;
+ align-items: center;
+ gap: 24px;
+ margin-bottom: 20px;
+
+ .speed-control {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ .speed-label {
+ color: #666;
+ font-size: 14px;
+ }
+ }
+ }
+
+ .step-control {
+ margin-bottom: 20px;
+
+ .step-label {
+ display: block;
+ color: #666;
+ font-size: 14px;
+ margin-bottom: 12px;
+ }
+
+ .clickable-step {
+ cursor: pointer;
+ }
+ }
+
+ .explanation-box {
+ animation: fadeIn 0.3s ease;
+ }
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(-10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+</style>
diff --git a/src/views/productionManagement/teachingDemo/components/ProcessNode.vue b/src/views/productionManagement/teachingDemo/components/ProcessNode.vue
new file mode 100644
index 0000000..674a763
--- /dev/null
+++ b/src/views/productionManagement/teachingDemo/components/ProcessNode.vue
@@ -0,0 +1,354 @@
+<template>
+ <div
+ class="process-node"
+ :class="{
+ 'is-active': active,
+ 'is-completed': completed,
+ 'is-current': current,
+ 'is-animating': animating
+ }"
+ :style="{ '--delay': delay + 'ms', '--speed': animationSpeed }"
+ >
+ <div class="node-badge">
+ <span class="badge-number">{{ index + 1 }}</span>
+ </div>
+
+ <div class="node-card" @click="handleClick">
+ <div class="node-header">
+ <div class="node-icon">
+ <el-icon><SetUp /></el-icon>
+ </div>
+ <div class="node-title">{{ process.technologyOperationName || process.operationName || '鏈懡鍚嶅伐搴�' }}</div>
+ </div>
+
+ <div class="node-body">
+ <div class="node-info-item" v-if="process.productName">
+ <span class="info-label">浜у搧锛�</span>
+ <span class="info-value">{{ process.productName }}</span>
+ </div>
+ <div class="node-info-item" v-if="process.model">
+ <span class="info-label">瑙勬牸锛�</span>
+ <span class="info-value">{{ process.model }}</span>
+ </div>
+ <div class="node-tags">
+ <el-tag :type="process.type === 1 ? 'primary' : 'success'" size="small">
+ {{ process.type === 1 ? '璁′欢' : '璁℃椂' }}
+ </el-tag>
+ <el-tag v-if="process.isQuality" type="warning" size="small">璐ㄦ</el-tag>
+ <el-tag v-if="process.isProduction" type="info" size="small">鐢熶骇</el-tag>
+ </div>
+ </div>
+
+ <div class="node-progress" v-if="showProgress">
+ <el-progress
+ :percentage="progress"
+ :status="completed ? 'success' : current ? '' : ''"
+ :stroke-width="6"
+ />
+ </div>
+ </div>
+
+ <div class="node-arrow" v-if="showArrow">
+ <svg width="40" height="24" viewBox="0 0 40 24">
+ <defs>
+ <marker
+ id="arrowhead"
+ markerWidth="10"
+ markerHeight="7"
+ refX="9"
+ refY="3.5"
+ orient="auto"
+ >
+ <polygon points="0 0, 10 3.5, 0 7" fill="#409eff" />
+ </marker>
+ </defs>
+ <line
+ x1="0"
+ y1="12"
+ x2="30"
+ y2="12"
+ stroke="#409eff"
+ stroke-width="2"
+ marker-end="url(#arrowhead)"
+ :class="{ 'is-animating': animating }"
+ />
+ <circle
+ v-if="animating"
+ r="4"
+ fill="#67c23a"
+ class="flow-dot"
+ >
+ <animate
+ attributeName="cx"
+ values="0;30"
+ :dur="`${1 / animationSpeed}s`"
+ repeatCount="indefinite"
+ />
+ <animate
+ attributeName="cy"
+ values="12;12"
+ dur="1s"
+ repeatCount="indefinite"
+ />
+ </circle>
+ </svg>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { computed } from 'vue'
+import { SetUp } from '@element-plus/icons-vue'
+
+const props = defineProps({
+ process: {
+ type: Object,
+ required: true
+ },
+ index: {
+ type: Number,
+ default: 0
+ },
+ delay: {
+ type: Number,
+ default: 0
+ },
+ animationSpeed: {
+ type: Number,
+ default: 1
+ },
+ active: {
+ type: Boolean,
+ default: false
+ },
+ completed: {
+ type: Boolean,
+ default: false
+ },
+ current: {
+ type: Boolean,
+ default: false
+ },
+ animating: {
+ type: Boolean,
+ default: false
+ },
+ showArrow: {
+ type: Boolean,
+ default: true
+ },
+ showProgress: {
+ type: Boolean,
+ default: false
+ },
+ progress: {
+ type: Number,
+ default: 0
+ }
+})
+
+const emit = defineEmits(['click'])
+
+const handleClick = () => {
+ emit('click', props.process)
+}
+</script>
+
+<style scoped lang="scss">
+.process-node {
+ display: flex;
+ align-items: center;
+ animation: nodeSlideIn calc(var(--delay) * var(--speed)) ease-out both;
+
+ .node-badge {
+ position: absolute;
+ top: -12px;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 1;
+
+ .badge-number {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 28px;
+ height: 28px;
+ background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
+ color: #fff;
+ font-weight: bold;
+ font-size: 14px;
+ border-radius: 50%;
+ box-shadow: 0 2px 8px rgba(64, 158, 255, 0.4);
+ }
+ }
+
+ .node-card {
+ position: relative;
+ width: 180px;
+ background: #fff;
+ border-radius: 12px;
+ border: 2px solid #e4e7ed;
+ padding: 20px 16px 16px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+
+ &:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
+ border-color: #409eff;
+ }
+
+ .node-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 12px;
+
+ .node-icon {
+ width: 28px;
+ height: 28px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
+ border-radius: 6px;
+
+ .el-icon {
+ color: #fff;
+ font-size: 16px;
+ }
+ }
+
+ .node-title {
+ font-weight: 600;
+ color: #303133;
+ font-size: 13px;
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+
+ .node-body {
+ .node-info-item {
+ display: flex;
+ font-size: 12px;
+ margin-bottom: 4px;
+
+ .info-label {
+ color: #909399;
+ margin-right: 4px;
+ }
+
+ .info-value {
+ color: #606266;
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+
+ .node-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ margin-top: 8px;
+ }
+ }
+
+ .node-progress {
+ margin-top: 12px;
+ }
+ }
+
+ .node-arrow {
+ margin: 0 8px;
+
+ line {
+ transition: stroke 0.3s;
+
+ &.is-animating {
+ animation: arrowGlow 0.5s ease-in-out infinite alternate;
+ }
+ }
+
+ .flow-dot {
+ filter: drop-shadow(0 0 4px rgba(103, 194, 58, 0.6));
+ }
+ }
+
+ &.is-active {
+ .node-card {
+ border-color: #409eff;
+ box-shadow: 0 4px 16px rgba(64, 158, 255, 0.3);
+ }
+ }
+
+ &.is-completed {
+ .node-card {
+ border-color: #67c23a;
+ background: linear-gradient(135deg, #f0f9eb 0%, #fff 100%);
+
+ .node-badge .badge-number {
+ background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%);
+ }
+ }
+ }
+
+ &.is-current {
+ .node-card {
+ border-color: #e6a23c;
+ animation: currentPulse 1.5s ease-in-out infinite;
+ }
+ }
+
+ &.is-animating {
+ .node-card {
+ animation: cardPulse calc(1s * var(--speed)) ease-in-out infinite;
+ }
+ }
+}
+
+@keyframes nodeSlideIn {
+ from {
+ opacity: 0;
+ transform: translateX(-30px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+@keyframes currentPulse {
+ 0%, 100% {
+ box-shadow: 0 4px 16px rgba(230, 162, 60, 0.3);
+ }
+ 50% {
+ box-shadow: 0 4px 24px rgba(230, 162, 60, 0.5);
+ }
+}
+
+@keyframes cardPulse {
+ 0%, 100% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.02);
+ }
+}
+
+@keyframes arrowGlow {
+ from {
+ stroke: #409eff;
+ filter: drop-shadow(0 0 2px rgba(64, 158, 255, 0.4));
+ }
+ to {
+ stroke: #66b1ff;
+ filter: drop-shadow(0 0 6px rgba(64, 158, 255, 0.6));
+ }
+}
+</style>
diff --git a/src/views/productionManagement/teachingDemo/components/TreeNode.vue b/src/views/productionManagement/teachingDemo/components/TreeNode.vue
new file mode 100644
index 0000000..54b0afb
--- /dev/null
+++ b/src/views/productionManagement/teachingDemo/components/TreeNode.vue
@@ -0,0 +1,423 @@
+<template>
+ <div
+ class="tree-node"
+ :class="{
+ 'is-root': isRoot,
+ 'is-expanded': expanded,
+ 'is-highlighted': highlighted,
+ 'is-active': active,
+ 'is-animating': animating
+ }"
+ :style="{ '--delay': delay + 'ms', '--speed': animationSpeed }"
+ >
+ <div class="node-content" @click="handleClick">
+ <div class="node-icon">
+ <el-icon v-if="hasChildren"><FolderOpened v-if="expanded" /><Folder v-else /></el-icon>
+ <el-icon v-else><Document /></el-icon>
+ </div>
+
+ <div class="node-info">
+ <div class="node-name">{{ node.productName || '鏈懡鍚嶄骇鍝�' }}</div>
+ <div class="node-model" v-if="node.model">{{ node.model }}</div>
+ <div class="node-meta">
+ <span class="node-quantity">
+ <el-icon><Goods /></el-icon>
+ {{ node.unitQuantity || 1 }} {{ node.unit || '' }}
+ </span>
+ <span class="node-process" v-if="node.processName && !isRoot">
+ <el-icon><Setting /></el-icon>
+ {{ node.processName }}
+ </span>
+ </div>
+ </div>
+
+ <div class="node-expand-btn" v-if="hasChildren" @click.stop="toggleExpand">
+ <el-icon>
+ <ArrowDown v-if="expanded" />
+ <ArrowRight v-else />
+ </el-icon>
+ </div>
+ </div>
+
+ <div class="node-children" v-if="hasChildren && expanded">
+ <div class="children-container">
+ <svg class="connection-lines" v-if="showLines">
+ <path
+ v-for="(child, index) in node.children"
+ :key="child.tempId || child.id || index"
+ :d="getLinePath(index)"
+ class="connection-line"
+ :class="{ 'is-animating': animating }"
+ :style="{ '--line-delay': (delay + index * 100) + 'ms' }"
+ />
+ </svg>
+
+ <TreeNode
+ v-for="(child, index) in node.children"
+ :key="child.tempId || child.id || index"
+ :node="child"
+ :level="level + 1"
+ :delay="delay + 200 + index * 100"
+ :animation-speed="animationSpeed"
+ :auto-expand="autoExpand"
+ :show-lines="showLines"
+ :highlight-ids="highlightIds"
+ :active-id="activeId"
+ :animating="animating"
+ @node-click="(n) => emit('node-click', n)"
+ @expand="(n, exp) => emit('expand', n, exp)"
+ />
+ </div>
+ </div>
+
+ <div class="material-flow-ball" v-if="animating && !hasChildren">
+ <div class="ball"></div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, computed, watch, onMounted, nextTick } from 'vue'
+import {
+ Folder,
+ FolderOpened,
+ Document,
+ ArrowDown,
+ ArrowRight,
+ Goods,
+ Setting
+} from '@element-plus/icons-vue'
+
+const props = defineProps({
+ node: {
+ type: Object,
+ required: true
+ },
+ level: {
+ type: Number,
+ default: 0
+ },
+ delay: {
+ type: Number,
+ default: 0
+ },
+ animationSpeed: {
+ type: Number,
+ default: 1
+ },
+ autoExpand: {
+ type: Boolean,
+ default: false
+ },
+ showLines: {
+ type: Boolean,
+ default: true
+ },
+ highlightIds: {
+ type: Array,
+ default: () => []
+ },
+ activeId: {
+ type: [String, Number],
+ default: null
+ },
+ animating: {
+ type: Boolean,
+ default: false
+ }
+})
+
+const emit = defineEmits(['node-click', 'expand'])
+
+const expanded = ref(false)
+const nodeWidth = ref(200)
+const nodeHeight = ref(60)
+
+const isRoot = computed(() => props.level === 0)
+const hasChildren = computed(() => props.node.children && props.node.children.length > 0)
+
+const highlighted = computed(() => {
+ const nodeId = props.node.tempId || props.node.id
+ return props.highlightIds.includes(nodeId)
+})
+
+const active = computed(() => {
+ const nodeId = props.node.tempId || props.node.id
+ return props.activeId === nodeId
+})
+
+const toggleExpand = () => {
+ expanded.value = !expanded.value
+ emit('expand', props.node, expanded.value)
+}
+
+const handleClick = () => {
+ emit('node-click', props.node)
+}
+
+const getLinePath = (index) => {
+ const startX = 20
+ const startY = 0
+ const endX = 20
+ const endY = 30 + index * 70
+ return `M ${startX} ${startY} L ${startX} ${endY}`
+}
+
+watch(() => props.autoExpand, (val) => {
+ if (val && hasChildren.value) {
+ setTimeout(() => {
+ expanded.value = true
+ }, props.delay)
+ }
+})
+
+onMounted(() => {
+ if (props.autoExpand && isRoot.value && hasChildren.value) {
+ expanded.value = true
+ }
+})
+</script>
+
+<style scoped lang="scss">
+.tree-node {
+ position: relative;
+ margin-left: 20px;
+ animation: nodeAppear calc(var(--delay) * var(--speed)) ease-out both;
+
+ &.is-root {
+ margin-left: 0;
+
+ .node-content {
+ background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
+ color: #fff;
+
+ .node-name {
+ color: #fff;
+ }
+
+ .node-model {
+ color: rgba(255, 255, 255, 0.85);
+ }
+
+ .node-meta {
+ color: rgba(255, 255, 255, 0.9);
+
+ .node-quantity,
+ .node-process {
+ background: rgba(255, 255, 255, 0.2);
+ padding: 2px 8px;
+ border-radius: 4px;
+ }
+ }
+ }
+ }
+
+ &.is-highlighted {
+ .node-content {
+ border-color: #e6a23c;
+ box-shadow: 0 0 12px rgba(230, 162, 60, 0.4);
+ }
+ }
+
+ &.is-active {
+ .node-content {
+ border-color: #409eff;
+ box-shadow: 0 0 16px rgba(64, 158, 255, 0.5);
+ animation: activePulse 1.5s ease-in-out infinite;
+ }
+ }
+
+ &.is-animating {
+ .node-content {
+ animation: nodeFlow calc(2s * var(--speed)) ease-in-out infinite;
+ }
+ }
+
+ .node-content {
+ display: flex;
+ align-items: center;
+ padding: 12px 16px;
+ background: #fff;
+ border-radius: 12px;
+ border: 2px solid #e4e7ed;
+ min-width: 200px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+
+ &:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
+ border-color: #409eff;
+ }
+
+ .node-icon {
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #f5f7fa;
+ border-radius: 8px;
+ margin-right: 12px;
+
+ .el-icon {
+ color: #409eff;
+ font-size: 18px;
+ }
+ }
+
+ .node-info {
+ flex: 1;
+
+ .node-name {
+ font-weight: 600;
+ color: #303133;
+ font-size: 14px;
+ }
+
+ .node-model {
+ color: #909399;
+ font-size: 12px;
+ margin-top: 2px;
+ }
+
+ .node-meta {
+ display: flex;
+ gap: 8px;
+ margin-top: 6px;
+ font-size: 12px;
+ color: #606266;
+
+ .node-quantity,
+ .node-process {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ }
+ }
+ }
+
+ .node-expand-btn {
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #f5f7fa;
+ border-radius: 4px;
+ transition: background 0.2s;
+
+ &:hover {
+ background: #409eff;
+ .el-icon {
+ color: #fff;
+ }
+ }
+
+ .el-icon {
+ font-size: 14px;
+ color: #909399;
+ }
+ }
+ }
+
+ .node-children {
+ margin-top: 8px;
+
+ .children-container {
+ position: relative;
+ padding-left: 20px;
+
+ .connection-lines {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 40px;
+ height: 100%;
+ overflow: visible;
+
+ .connection-line {
+ stroke: #c0c4cc;
+ stroke-width: 2;
+ fill: none;
+ stroke-dasharray: 100;
+ stroke-dashoffset: 100;
+ transition: stroke-dashoffset calc(var(--line-delay) * var(--speed)) ease-out;
+
+ &.is-animating {
+ stroke-dashoffset: 0;
+ stroke: #409eff;
+ animation: lineFlow 1s ease-in-out infinite;
+ }
+ }
+ }
+ }
+ }
+
+ .material-flow-ball {
+ position: absolute;
+ right: -20px;
+ top: 50%;
+ transform: translateY(-50%);
+
+ .ball {
+ width: 12px;
+ height: 12px;
+ background: #67c23a;
+ border-radius: 50%;
+ box-shadow: 0 0 8px rgba(103, 194, 58, 0.6);
+ animation: ballPulse 0.8s ease-in-out infinite;
+ }
+ }
+}
+
+@keyframes nodeAppear {
+ from {
+ opacity: 0;
+ transform: translateY(-20px) scale(0.9);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+}
+
+@keyframes activePulse {
+ 0%, 100% {
+ box-shadow: 0 0 16px rgba(64, 158, 255, 0.5);
+ }
+ 50% {
+ box-shadow: 0 0 24px rgba(64, 158, 255, 0.8);
+ }
+}
+
+@keyframes nodeFlow {
+ 0%, 100% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.02);
+ }
+}
+
+@keyframes lineFlow {
+ 0% {
+ stroke-dashoffset: 100;
+ }
+ 100% {
+ stroke-dashoffset: 0;
+ }
+}
+
+@keyframes ballPulse {
+ 0%, 100% {
+ transform: scale(1);
+ opacity: 1;
+ }
+ 50% {
+ transform: scale(1.3);
+ opacity: 0.8;
+ }
+}
+</style>
\ No newline at end of file
diff --git a/src/views/productionManagement/teachingDemo/index.vue b/src/views/productionManagement/teachingDemo/index.vue
new file mode 100644
index 0000000..ad633ec
--- /dev/null
+++ b/src/views/productionManagement/teachingDemo/index.vue
@@ -0,0 +1,333 @@
+<template>
+ <div class="app-container teaching-demo-index">
+ <PageHeader content="宸ヨ壓璺嚎涓嶣OM鍔ㄧ敾鏁欏婕旂ず">
+ <template #right-button>
+ <el-button type="success" @click="goToFactoryDemo">
+ <el-icon><Monitor /></el-icon>
+ 宸ュ巶婕旂ず
+ </el-button>
+ <el-button type="primary" @click="goToCombinedDemo">
+ 鑱斿姩婕旂ず
+ </el-button>
+ </template>
+ </PageHeader>
+
+ <div class="demo-intro">
+ <el-alert
+ title="鏁欏婕旂ず璇存槑"
+ type="info"
+ :closable="false"
+ show-icon
+ >
+ <template #default>
+ 鏈ā鍧楅�氳繃鍔ㄧ敾婕旂ず甯姪鎮ㄧ悊瑙� <strong>BOM锛堢墿鏂欐竻鍗曪級</strong> 鍜� <strong>宸ヨ壓璺嚎</strong> 鐨勬蹇点��
+ 閫夋嫨涓嬫柟鐨勬暟鎹悗鐐瑰嚮杩涘叆婕旂ず锛屽嵆鍙鐪嬪姩鐢昏瑙c��
+ </template>
+ </el-alert>
+ </div>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-card class="demo-card" shadow="hover">
+ <template #header>
+ <div class="card-header">
+ <span class="card-title">
+ <el-icon><List /></el-icon>
+ BOM 缁撴瀯婕旂ず
+ </span>
+ <el-button type="primary" :disabled="!selectedBom" @click="goToBomDemo">
+ 杩涘叆婕旂ず
+ </el-button>
+ </div>
+ </template>
+
+ <div class="card-content">
+ <p class="card-desc">灞曠ず鐗╂枡娓呭崟鐨勬爲褰㈠眰绾х粨鏋勶紝鐞嗚В浜у搧鐢卞摢浜涢浂閮ㄤ欢缁勬垚銆�</p>
+
+ <el-select
+ v-model="selectedBom"
+ placeholder="璇烽�夋嫨涓�涓狟OM"
+ filterable
+ style="width: 100%"
+ @change="handleBomChange"
+ >
+ <el-option
+ v-for="item in bomList"
+ :key="item.id"
+ :label="`${item.bomNo} - ${item.productName}`"
+ :value="item.id"
+ />
+ </el-select>
+
+ <div v-if="selectedBomInfo" class="selected-info">
+ <el-descriptions :column="1" border size="small">
+ <el-descriptions-item label="BOM缂栧彿">{{ selectedBomInfo.bomNo }}</el-descriptions-item>
+ <el-descriptions-item label="浜у搧鍚嶇О">{{ selectedBomInfo.productName }}</el-descriptions-item>
+ <el-descriptions-item label="瑙勬牸鍨嬪彿">{{ selectedBomInfo.productModelName }}</el-descriptions-item>
+ <el-descriptions-item label="鐗堟湰鍙�">{{ selectedBomInfo.version }}</el-descriptions-item>
+ </el-descriptions>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+
+ <el-col :span="12">
+ <el-card class="demo-card" shadow="hover">
+ <template #header>
+ <div class="card-header">
+ <span class="card-title">
+ <el-icon><Connection /></el-icon>
+ 宸ヨ壓璺嚎婕旂ず
+ </span>
+ <el-button type="primary" :disabled="!selectedRoute" @click="goToRouteDemo">
+ 杩涘叆婕旂ず
+ </el-button>
+ </div>
+ </template>
+
+ <div class="card-content">
+ <p class="card-desc">灞曠ず浜у搧鍔犲伐鐨勫伐搴忔祦绋嬶紝鐞嗚В浜у搧鏄浣曚竴姝ユ鐢熶骇鍑烘潵鐨勩��</p>
+
+ <el-select
+ v-model="selectedRoute"
+ placeholder="璇烽�夋嫨涓�涓伐鑹鸿矾绾�"
+ filterable
+ style="width: 100%"
+ @change="handleRouteChange"
+ >
+ <el-option
+ v-for="item in routeList"
+ :key="item.id"
+ :label="`${item.processRouteCode} - ${item.productName}`"
+ :value="item.id"
+ />
+ </el-select>
+
+ <div v-if="selectedRouteInfo" class="selected-info">
+ <el-descriptions :column="1" border size="small">
+ <el-descriptions-item label="璺嚎缂栧彿">{{ selectedRouteInfo.processRouteCode }}</el-descriptions-item>
+ <el-descriptions-item label="浜у搧鍚嶇О">{{ selectedRouteInfo.productName }}</el-descriptions-item>
+ <el-descriptions-item label="瑙勬牸鍚嶇О">{{ selectedRouteInfo.model }}</el-descriptions-item>
+ <el-descriptions-item label="BOM缂栧彿">{{ selectedRouteInfo.bomNo }}</el-descriptions-item>
+ </el-descriptions>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+
+ <el-card class="concept-card" shadow="hover">
+ <template #header>
+ <span class="card-title">
+ <el-icon><Reading /></el-icon>
+ 姒傚康璇存槑
+ </span>
+ </template>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <div class="concept-item">
+ <h4>BOM锛堢墿鏂欐竻鍗曪級</h4>
+ <p>
+ BOM锛圔ill of Materials锛夋槸浜у搧缁撴瀯鐨勬妧鏈枃浠讹紝瀹冭缁嗚褰曚簡浜у搧鐢卞摢浜涢浂閮ㄤ欢銆佸師鏉愭枡缁勬垚锛�
+ 浠ュ強鍚勭粍鎴愰儴鍒嗕箣闂寸殑灞傜骇鍏崇郴鍜屾暟閲忓叧绯汇��
+ </p>
+ <ul>
+ <li><strong>鐖堕」锛�</strong>涓婂眰浜у搧鎴栭儴浠�</li>
+ <li><strong>瀛愰」锛�</strong>缁勬垚鐖堕」鐨勯浂閮ㄤ欢鎴栧師鏉愭枡</li>
+ <li><strong>鐢ㄩ噺锛�</strong>鐢熶骇涓�涓埗椤归渶瑕佸灏戝瓙椤�</li>
+ </ul>
+ </div>
+ </el-col>
+ <el-col :span="12">
+ <div class="concept-item">
+ <h4>宸ヨ壓璺嚎</h4>
+ <p>
+ 宸ヨ壓璺嚎鏄弿杩颁骇鍝佸姞宸ヨ繃绋嬬殑鏂囦欢锛屽畠瑙勫畾浜嗕骇鍝佷粠鍘熸潗鏂欏埌鎴愬搧闇�瑕佺粡杩囧摢浜涘伐搴忥紝
+ 浠ュ強鍚勫伐搴忕殑鍏堝悗椤哄簭鍜屾搷浣滆姹傘��
+ </p>
+ <ul>
+ <li><strong>宸ュ簭锛�</strong>涓�涓嫭绔嬬殑鍔犲伐姝ラ</li>
+ <li><strong>椤哄簭锛�</strong>宸ュ簭鎵ц鐨勫厛鍚庢搴�</li>
+ <li><strong>鍙傛暟锛�</strong>宸ュ簭鐨勫叿浣撴搷浣滆姹�</li>
+ </ul>
+ </div>
+ </el-col>
+ </el-row>
+ </el-card>
+ </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+import { List, Connection, Reading, Monitor } from '@element-plus/icons-vue'
+import { listPage as getBomList } from '@/api/productionManagement/productBom.js'
+import { listPage as getRouteList } from '@/api/productionManagement/processRoute.js'
+
+const router = useRouter()
+
+const bomList = ref([])
+const routeList = ref([])
+const selectedBom = ref(null)
+const selectedRoute = ref(null)
+
+const selectedBomInfo = computed(() => {
+ return bomList.value.find(item => item.id === selectedBom.value)
+})
+
+const selectedRouteInfo = computed(() => {
+ return routeList.value.find(item => item.id === selectedRoute.value)
+})
+
+const fetchBomList = async () => {
+ try {
+ const res = await getBomList({ current: 1, size: 100 })
+ bomList.value = res?.data?.records || []
+ } catch (error) {
+ console.error('鑾峰彇BOM鍒楄〃澶辫触:', error)
+ }
+}
+
+const fetchRouteList = async () => {
+ try {
+ const res = await getRouteList({ current: 1, size: 100 })
+ routeList.value = res?.data?.records || []
+ } catch (error) {
+ console.error('鑾峰彇宸ヨ壓璺嚎鍒楄〃澶辫触:', error)
+ }
+}
+
+const handleBomChange = (val) => {
+ selectedBom.value = val
+}
+
+const handleRouteChange = (val) => {
+ selectedRoute.value = val
+}
+
+const goToBomDemo = () => {
+ if (!selectedBom.value) return
+ const info = selectedBomInfo.value
+ router.push({
+ path: '/productionManagement/teachingDemo/bom',
+ query: {
+ id: selectedBom.value,
+ bomNo: info?.bomNo || '',
+ productName: info?.productName || '',
+ productModelName: info?.productModelName || ''
+ }
+ })
+}
+
+const goToRouteDemo = () => {
+ if (!selectedRoute.value) return
+ const info = selectedRouteInfo.value
+ router.push({
+ path: '/productionManagement/teachingDemo/processRoute',
+ query: {
+ id: selectedRoute.value,
+ processRouteCode: info?.processRouteCode || '',
+ productName: info?.productName || '',
+ model: info?.model || '',
+ bomId: info?.bomId || ''
+ }
+ })
+}
+
+const goToCombinedDemo = () => {
+ router.push({
+ path: '/productionManagement/teachingDemo/combined',
+ query: {
+ bomId: selectedBom.value || '',
+ routeId: selectedRoute.value || ''
+ }
+ })
+}
+
+const goToFactoryDemo = () => {
+ router.push({
+ path: '/productionManagement/teachingDemo/factory',
+ query: {
+ routeId: selectedRoute.value || ''
+ }
+ })
+}
+
+onMounted(() => {
+ fetchBomList()
+ fetchRouteList()
+})
+</script>
+
+<style scoped lang="scss">
+.teaching-demo-index {
+ .demo-intro {
+ margin-bottom: 20px;
+ }
+
+ .demo-card {
+ margin-bottom: 20px;
+
+ .card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .card-title {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-weight: 600;
+ font-size: 16px;
+ }
+ }
+
+ .card-content {
+ .card-desc {
+ color: #666;
+ margin-bottom: 16px;
+ line-height: 1.6;
+ }
+
+ .selected-info {
+ margin-top: 16px;
+ }
+ }
+ }
+
+ .concept-card {
+ .card-title {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-weight: 600;
+ font-size: 16px;
+ }
+
+ .concept-item {
+ h4 {
+ color: #409eff;
+ margin-bottom: 12px;
+ font-size: 15px;
+ }
+
+ p {
+ color: #666;
+ line-height: 1.8;
+ margin-bottom: 12px;
+ }
+
+ ul {
+ padding-left: 20px;
+ color: #888;
+ line-height: 1.8;
+
+ li {
+ margin-bottom: 4px;
+ }
+ }
+ }
+ }
+}
+</style>
diff --git a/src/views/productionManagement/workOrder/index.vue b/src/views/productionManagement/workOrder/index.vue
index 8c76b2e..d3b0f93 100644
--- a/src/views/productionManagement/workOrder/index.vue
+++ b/src/views/productionManagement/workOrder/index.vue
@@ -475,7 +475,7 @@
if (!Number.isFinite(n)) return 0;
if (n <= 0) return 0;
if (n >= 100) return 100;
- return Math.round(n);
+ return parseFloat(n.toFixed(2));
};
const progressColor = percentage => {
const p = toProgressPercentage(percentage);
diff --git a/src/views/productionManagement/workOrderEdit/index.vue b/src/views/productionManagement/workOrderEdit/index.vue
index 37cbb4e..98d29f5 100644
--- a/src/views/productionManagement/workOrderEdit/index.vue
+++ b/src/views/productionManagement/workOrderEdit/index.vue
@@ -297,7 +297,7 @@
if (!Number.isFinite(n)) return 0;
if (n <= 0) return 0;
if (n >= 100) return 100;
- return Math.round(n);
+ return parseFloat(n.toFixed(2));
};
const progressColor = percentage => {
diff --git a/src/views/productionManagement/workOrderManagement/index.vue b/src/views/productionManagement/workOrderManagement/index.vue
index 8bc6dc2..b721de6 100644
--- a/src/views/productionManagement/workOrderManagement/index.vue
+++ b/src/views/productionManagement/workOrderManagement/index.vue
@@ -549,7 +549,7 @@
if (!Number.isFinite(n)) return 0;
if (n <= 0) return 0;
if (n >= 100) return 100;
- return Math.round(n);
+ return parseFloat(n.toFixed(2));
};
const progressColor = percentage => {
const p = toProgressPercentage(percentage);
diff --git a/src/views/productionPlan/productionPlan/index.vue b/src/views/productionPlan/productionPlan/index.vue
index 78d9870..fd6c1a3 100644
--- a/src/views/productionPlan/productionPlan/index.vue
+++ b/src/views/productionPlan/productionPlan/index.vue
@@ -140,7 +140,7 @@
style="width: 100%" />
</el-form-item>
<el-form-item label="鍒涘缓鏃堕棿">
- <el-date-picker v-model="mergeForm.createTime"
+ <el-date-picker v-model="mergeCreateTimeDate"
type="date"
value-format="YYYY-MM-DD"
style="width: 100%" />
@@ -235,7 +235,7 @@
</el-form-item>
<el-form-item label="鍒涘缓鏃堕棿"
prop="createTime">
- <el-date-picker v-model="form.createTime"
+ <el-date-picker v-model="formCreateTimeDate"
type="date"
value-format="YYYY-MM-DD"
style="width: 100%"
@@ -444,7 +444,7 @@
Number(row.qtyRequired || 0) - Number(row.quantityIssued || 0);
mergeForm.planCompleteTime = row.requiredDate || "";
mergeForm.productId = row.productId || "";
- mergeForm.createTime = new Date().toISOString().split("T")[0];
+ mergeForm.createTime = dayjs().format("YYYY-MM-DD HH:mm:ss");
mergeForm.ids = [row.id];
sumAssignedQuantity.value =
Number(row.qtyRequired || 0) - Number(row.quantityIssued || 0);
@@ -490,6 +490,12 @@
productId: "",
createTime: "",
});
+ const mergeCreateTimeDate = computed({
+ get: () => (mergeForm.createTime ? String(mergeForm.createTime).split(" ")[0] : ""),
+ set: (value) => {
+ mergeForm.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : "";
+ },
+ });
// 瀵煎叆鐩稿叧
const importDialogRef = ref(null);
@@ -520,6 +526,12 @@
promisedDeliveryDate: "",
remark: "",
createTime: "",
+ });
+ const formCreateTimeDate = computed({
+ get: () => (form.createTime ? String(form.createTime).split(" ")[0] : ""),
+ set: (value) => {
+ form.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : "";
+ },
});
const rules = reactive({
productId: [{ required: true, message: "璇烽�夋嫨浜у搧", trigger: "change" }],
@@ -763,6 +775,7 @@
mergeForm.totalAssignedQuantity = totalAssignedQuantity;
mergeForm.planCompleteTime = firstRow.requiredDate || "";
mergeForm.productId = firstRow.productId || "";
+ mergeForm.createTime = dayjs().format("YYYY-MM-DD HH:mm:ss");
mergeForm.ids = selectedRows.value.map(row => row.id);
// 鎵撳紑寮圭獥
@@ -906,7 +919,7 @@
requiredDate: "",
promisedDeliveryDate: "",
remark: "",
- createTime: new Date().toISOString().split("T")[0],
+ createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
});
dialogVisible.value = true;
fetchProductOptions();
@@ -927,6 +940,7 @@
requiredDate: row.requiredDate || "",
promisedDeliveryDate: row.promisedDeliveryDate || "",
remark: row.remark || "",
+ createTime: row.createTime || "",
});
dialogVisible.value = true;
fetchProductOptions();
diff --git a/src/views/qualityManagement/metricBinding/index.vue b/src/views/qualityManagement/metricBinding/index.vue
index 1ac268a..b5d2a8f 100644
--- a/src/views/qualityManagement/metricBinding/index.vue
+++ b/src/views/qualityManagement/metricBinding/index.vue
@@ -128,6 +128,7 @@
<el-tree-select
v-model="selectedProductIds"
multiple
+ filterable
collapse-tags
collapse-tags-tooltip
placeholder="璇烽�夋嫨浜у搧锛堝彲澶氶�夛級"
diff --git a/src/views/safeProduction/safetyTrainingAssessment/index.vue b/src/views/safeProduction/safetyTrainingAssessment/index.vue
index ff09589..38c2172 100644
--- a/src/views/safeProduction/safetyTrainingAssessment/index.vue
+++ b/src/views/safeProduction/safetyTrainingAssessment/index.vue
@@ -85,7 +85,7 @@
<el-form-item label="鍒涘缓鏃堕棿"
prop="createTime">
<el-date-picker style="width: 100%"
- v-model="form.createTime"
+ v-model="formCreateTimeDate"
value-format="YYYY-MM-DD"
format="YYYY-MM-DD"
type="date"
@@ -466,6 +466,12 @@
dialogType: "add",
viewDialogVisible: false,
currentKnowledge: {},
+ });
+ const formCreateTimeDate = computed({
+ get: () => (form.value.createTime ? String(form.value.createTime).split(" ")[0] : ""),
+ set: (value) => {
+ form.value.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : "";
+ },
});
const {
@@ -1040,7 +1046,7 @@
trainingMode: "", // 鍩硅鏂瑰紡
placeTraining: "", // 鍩硅鍦扮偣
classHour: "", // 璇炬椂
- createTime: new Date().toISOString().split("T")[0], // 鍒涘缓鏃堕棿
+ createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), // 鍒涘缓鏃堕棿
});
} else if (type === "edit" && row) {
dialogTitle.value = "缂栬緫鍩硅";
@@ -1058,6 +1064,7 @@
trainingMode: row.trainingMode, // 鍩硅鏂瑰紡
placeTraining: row.placeTraining, // 鍩硅鍦扮偣
classHour: row.classHour, // 璇炬椂
+ createTime: row.createTime || "", // 鍒涘缓鏃堕棿
});
}
dialogVisible.value = true;
diff --git a/src/views/salesManagement/deliveryLedger/index.vue b/src/views/salesManagement/deliveryLedger/index.vue
index c8890cc..33fa45f 100644
--- a/src/views/salesManagement/deliveryLedger/index.vue
+++ b/src/views/salesManagement/deliveryLedger/index.vue
@@ -139,13 +139,13 @@
@click="openDetail(scope.row)"
>璇︽儏
</el-button>
- <el-button
- link
- type="danger"
- :disabled="isApproving(scope.row.status)"
- @click="handleDeleteSingle(scope.row)"
- >鍒犻櫎
- </el-button>
+ <!-- <el-button-->
+ <!-- link-->
+ <!-- type="danger"-->
+ <!-- :disabled="isApproving(scope.row.status)"-->
+ <!-- @click="handleDeleteSingle(scope.row)"-->
+ <!-- >鍒犻櫎-->
+ <!-- </el-button>-->
</template>
</el-table-column>
</el-table>
diff --git a/src/views/salesManagement/returnOrder/components/formDia.vue b/src/views/salesManagement/returnOrder/components/formDia.vue
index 6a6d756..81d9487 100644
--- a/src/views/salesManagement/returnOrder/components/formDia.vue
+++ b/src/views/salesManagement/returnOrder/components/formDia.vue
@@ -511,6 +511,22 @@
const submitForm = () => {
proxy.$refs["formRef"].validate(valid => {
if (!valid) return;
+ if (!tableData.value || tableData.value.length === 0) {
+ proxy.$modal.msgWarning("璇疯嚦灏戞坊鍔犱竴涓��璐т骇鍝�");
+ return;
+ }
+ for (let i = 0; i < tableData.value.length; i++) {
+ const row = tableData.value[i];
+ const qty = Number(row.num ?? row.returnQuantity ?? 0);
+ if (!qty || qty <= 0) {
+ proxy.$modal.msgWarning(`绗�${i + 1}琛岄��璐ф暟閲忓繀椤诲ぇ浜�0`);
+ return;
+ }
+ if (row.price === "" || row.price === null || row.price === undefined || Number(row.price) < 0) {
+ proxy.$modal.msgWarning(`绗�${i + 1}琛岄��璐у崟浠蜂笉鑳戒负璐熸暟`);
+ return;
+ }
+ }
const returnSaleProducts = (tableData.value || []).map(el => ({
stockOutRecordId: el.returnSaleLedgerProductId ?? el.id,
productModelId: el.productModelId,
diff --git a/src/views/salesManagement/salesLedger/index.vue b/src/views/salesManagement/salesLedger/index.vue
index 4c13d3d..d2e82b5 100644
--- a/src/views/salesManagement/salesLedger/index.vue
+++ b/src/views/salesManagement/salesLedger/index.vue
@@ -117,6 +117,11 @@
</el-tag>
</template>
</el-table-column>
+ <el-table-column label="瀹℃壒涓暟閲�" width="120" align="center">
+ <template #default="scope">
+ <span>{{ scope.row.pendingApprovalQuantity || 0 }}</span>
+ </template>
+ </el-table-column>
<el-table-column
label="蹇�掑叕鍙�"
prop="expressCompany"
@@ -148,6 +153,7 @@
</template>
</el-table-column>
<el-table-column label="鏁伴噺" prop="quantity" />
+ <el-table-column label="閫�璐ф暟閲�" prop="returnQuantity" />
<el-table-column label="寰呭彂璐ф暟閲�" prop="noQuantity" />
<el-table-column label="绋庣巼(%)" prop="taxRate" />
<el-table-column
@@ -214,6 +220,13 @@
<el-table-column
label="鍚堝悓閲戦(鍏�)"
prop="contractAmount"
+ width="220"
+ show-overflow-tooltip
+ :formatter="formattedNumber"
+ />
+ <el-table-column
+ label="瀹為檯鍚堝悓閲戦(鍏�)"
+ prop="netContractAmount"
width="220"
show-overflow-tooltip
:formatter="formattedNumber"
@@ -1749,7 +1762,7 @@
// 绛捐鏃ユ湡榛樿涓哄綋澶�
form.value.executionDate = getCurrentDate();
// 鍒涘缓鏃堕棿榛樿涓哄綋澶�
- form.value.createTime = getCurrentDate();
+ form.value.createTime = dayjs().format("YYYY-MM-DD HH:mm:ss");
// 榛樿鑷姩鐢熸垚閿�鍞悎鍚屽彿
form.value.autoGenerateContractNo = true;
} else {
@@ -2007,6 +2020,12 @@
return;
}
+ // 妫�鏌ユ槸鍚﹀彧鍓╀竴鏉℃暟鎹紝閿�鍞彴璐﹁嚦灏戜繚鐣欎竴鏉′骇鍝�
+ if (productData.value.length <= 1) {
+ proxy.$modal.msgWarning("閿�鍞彴璐﹁嚦灏戦渶瑕佷繚鐣欎竴鏉′骇鍝佹暟鎹�");
+ return;
+ }
+
if (operationType.value === "add") {
productData.value = productData.value.filter(
(item) => !productSelectedRows.value.includes(item)
@@ -2155,7 +2174,7 @@
};
// 鎵撳嵃鍔熻兘
-const handlePrint = async () => {
+ const handlePrint = async () => {
if (selectedRows.value.length === 0) {
proxy.$modal.msgWarning("璇烽�夋嫨瑕佹墦鍗扮殑鏁版嵁");
return;
@@ -2512,11 +2531,7 @@
// 鏍煎紡鍖栨棩鏈�
const formatDate = (dateString) => {
if (!dateString) return getCurrentDate();
- const date = new Date(dateString);
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, "0");
- const day = String(date.getDate()).padStart(2, "0");
- return `${year}/${month}/${day}`;
+ return dayjs(dateString).format("YYYY/MM/DD HH:mm:ss");
};
// 鏍煎紡鍖栨棩鏈熸椂闂�
const formatDateTime = (date) => {
@@ -2803,7 +2818,7 @@
/**
* 鍒ゆ柇鏄惁鍙互鍙戣揣
- * 鍙湁鍦ㄤ骇鍝佺姸鎬佹槸鍏呰冻锛屽彂璐х姸鎬佹槸寰呭彂璐у拰瀹℃牳鎷掔粷鐨勬椂鍊欐墠鍙互鍙戣揣
+ * 鍙湁鍦ㄤ骇鍝佺姸鎬佹槸鍏呰冻锛屽彂璐х姸鎬佹槸寰呭彂璐с�佸鎵逛腑銆佸鏍告嫆缁濄�侀儴鍒嗗彂璐х殑鏃跺�欐墠鍙互鍙戣揣
* @param row 琛屾暟鎹�
*/
const canShip = (row) => {
@@ -2815,15 +2830,16 @@
// 鑾峰彇鍙戣揣鐘舵��
const shippingStatus = row.shippingStatus;
- // 濡傛灉宸插彂璐э紙鏈夊彂璐ф棩鏈熸垨杞︾墝鍙凤級锛屼笉鑳藉啀娆″彂璐�
+ // 濡傛灉宸插彂璐э紝涓嶈兘鍐嶆鍙戣揣
if (shippingStatus === "宸插彂璐�") {
return false;
}
- // 鍙戣揣鐘舵�佸繀椤绘槸"寰呭彂璐�"鎴�"瀹℃牳鎷掔粷"
+ // 鍙戣揣鐘舵�佸繀椤绘槸"寰呭彂璐�"銆�"瀹℃壒涓�"銆�"瀹℃牳鎷掔粷"鎴�"閮ㄥ垎鍙戣揣"
const statusStr = shippingStatus ? String(shippingStatus).trim() : "";
return (
statusStr === "寰呭彂璐�" ||
+ statusStr === "瀹℃壒涓�" ||
statusStr === "瀹℃牳鎷掔粷" ||
statusStr === "閮ㄥ垎鍙戣揣"
);
@@ -2844,11 +2860,17 @@
// 妫�鏌ユ槸鍚﹀彲浠ュ彂璐�
if (!canShip(row)) {
proxy.$modal.msgWarning(
- "鍙湁鍦ㄤ骇鍝佺姸鎬佹槸鍏呰冻锛屽彂璐х姸鎬佹槸寰呭彂璐ф垨瀹℃牳鎷掔粷鐨勬椂鍊欐墠鍙互鍙戣揣"
+ "鍙湁鍦ㄤ骇鍝佺姸鎬佹槸鍏呰冻锛屽彂璐х姸鎬佹槸寰呭彂璐с�佸鎵逛腑銆佸鏍告嫆缁濇垨閮ㄥ垎鍙戣揣鐨勬椂鍊欐墠鍙互鍙戣揣"
);
return;
}
+ // 瀹℃壒涓椂鎻愮ず鐢ㄦ埛宸叉湁寰呭鎵圭殑鍙戣揣璁板綍
+ const statusStr = row.shippingStatus ? String(row.shippingStatus).trim() : "";
+ if (statusStr === "瀹℃壒涓�") {
+ proxy.$modal.msgWarning("璇ヤ骇鍝佸瓨鍦ㄥ緟瀹℃壒鐨勫彂璐ц褰曪紝璇锋敞鎰忛伩鍏嶉噸澶嶆彁浜�");
+ }
+
currentDeliveryRow.value = row;
const batchNoList = await getDeliveryBatchNoList(
row.productModelId || row.modelId
diff --git a/vite.config.js b/vite.config.js
index fcc019d..6b267a7 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -8,11 +8,11 @@
const { VITE_APP_ENV } = env;
const baseUrl =
env.VITE_APP_ENV === "development"
- ? "http://1.15.17.182:9048"
+ ? "http://localhost:7005"
: env.VITE_BASE_API;
const javaUrl =
env.VITE_APP_ENV === "development"
- ? "http://1.15.17.182:9048"
+ ? "http://1.15.17.182:9049"
: env.VITE_JAVA_API;
return {
define:{
--
Gitblit v1.9.3