Merge branch 'dev_NEW_pro' into dev_pro_山西_晋和园
# Conflicts:
# multiple/config.json
| | |
| | | yarn-error.log* |
| | | **/*.log |
| | | |
| | | .claude/ |
| | | |
| | | tests/**/coverage/ |
| | | tests/e2e/reports |
| | | selenium-debug.log |
| | |
| | | *.sln |
| | | *.local |
| | | |
| | | # Claude Code |
| | | .claude/ |
| | | |
| | | package-lock.json |
| | | yarn.lock |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | @echo off |
| | | chcp 65001 >nul |
| | | setlocal enabledelayedexpansion |
| | | |
| | | :: ================= Config ================= |
| | | set "USER=root" |
| | | set "PASS=Xdrjkj@10086#" |
| | | set "IP=36.213.180.217 |
| | | set "PORT=65022" |
| | | set "DIST_DIR=dist" |
| | | set "REMOTE_DIR=/data/project/pim/vue/xjgm" |
| | | 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 |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | # æ°æ®æ¨¡æ - åºç¡æ°æ®æ£æµ |
| | | |
| | | ## æ¶åé¡µé¢ |
| | | |
| | | - æ°æ®æ¨¡ææä½é¡µé¢ï¼æ°å¢ï¼ |
| | | |
| | | ## API |
| | | |
| | | | æ¹æ³ | è·¯å¾ | 说æ | |
| | | |------|------|------| |
| | | | POST | /mock/dataCheck | æ£æµæå®æ¨¡åçåºç¡æ°æ®æ¯å¦å°±ç»ª | |
| | | |
| | | **请æ±åæ°ï¼** |
| | | |
| | | | åæ° | ç±»å | å¿
å¡« | 说æ | |
| | | |------|------|------|------| |
| | | | modules | List\<String\> | æ¯ | è¦æ£æµç模åå表ï¼å¯éå¼ï¼salesï¼éå®ï¼ãpurchaseï¼éè´ï¼ãqualityï¼è´¨éï¼ãproductionï¼çäº§ï¼ | |
| | | |
| | | 请æ±ä½ç¤ºä¾ï¼ |
| | | ```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管çãä¸ä¸ºBOMæ·»å 产åç»æèç¹ | |
| | | | å·¥èºè·¯çº¿ | 1 | 缺å°å·¥èºè·¯çº¿ï¼è¯·å
å¨ãå·¥èºè®¾è®¡-å·¥èºè·¯çº¿ãä¸å建工èºè·¯çº¿ | |
| | | | å·¥èºè·¯çº¿å·¥åº | 1 | 缺å°å·¥èºè·¯çº¿å·¥åºï¼è¯·å
å¨ãå·¥èºè®¾è®¡-å·¥èºè·¯çº¿ãä¸ä¸ºå·¥èºè·¯çº¿æ·»å å·¥åº | |
| | | | 产å-å·¥èºè·¯çº¿å
³è | â¥1 | é¨å产åè§æ ¼æªå
³èå·¥èºè·¯çº¿ï¼è¯·å
å¨ãå·¥èºè®¾è®¡-å·¥èºè·¯çº¿ãä¸ä¸ºäº§åè§æ ¼å建工èºè·¯çº¿ | |
| | | | å·¥èºè·¯çº¿-å·¥åºå
³è | â¥1 | é¨åå·¥èºè·¯çº¿æªæ·»å å·¥åºï¼è¯·å
å¨ãå·¥èºè®¾è®¡-å·¥èºè·¯çº¿ãä¸ä¸ºå·¥èºè·¯çº¿æ·»å å·¥åº | |
| | | | BOM-产åç»æå
³è | â¥1 | é¨åBOMæªæ·»å 产åç»æï¼è¯·å
å¨ãå·¥èºè®¾è®¡-BOM管çãä¸ä¸ºBOMæ·»å 产åç»æèç¹ | |
| | | |
| | | ### è´¨éæ¨¡å (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 |
| | | } |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | ## 注æäºé¡¹ |
| | | |
| | | - äº§åæ°æ®ï¼å
Œ
±åºç¡ï¼å¨å¤ä¸ªæ¨¡åé´å
±äº«ï¼æ£æµç»æä¸åªåºç°ä¸æ¬¡ï¼ä¸ä¼éå¤å±ç¤º |
| | | - æ£æµä»
ååªè¯»æ¥è¯¢ï¼ä¸åå
¥ä»»ä½æ°æ® |
| | | - 模ååæ°ä¸ºç©ºæ°ç»æ¶è¿åç©ºæ£æµå表 |
| | | - å»ºè®®å¨æ°æ®æ¨¡æå¼å§åå
è°ç¨æ¤æ¥å£ç¡®è®¤åºç¡æ°æ®å°±ç»ª |
| | | - ç产模åé¤äºæ£æµæ°æ®éï¼è¿ä¼æ£æµå
³èæ§ï¼äº§å-å·¥èºè·¯çº¿ãå·¥èºè·¯çº¿-å·¥åºãBOM-产åç»æï¼ï¼ç¡®ä¿å·¥èºè®¾è®¡æ°æ®å®æ´ |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | # æ°æ®æ¨¡æ - AI æ°æ®çæ |
| | | |
| | | ## æ¶åé¡µé¢ |
| | | |
| | | - æ°æ®æ¨¡ææä½é¡µé¢ï¼æ°å¢/ä¿®æ¹ï¼ |
| | | |
| | | ## API |
| | | |
| | | | æ¹æ³ | è·¯å¾ | 说æ | |
| | | |------|------|------| |
| | | | POST | /mock/generate | AI çææ¨¡ææ°æ®ï¼å«åç½®åºç¡æ°æ®æ£æµï¼ | |
| | | |
| | | **请æ±åæ°ï¼** |
| | | |
| | | | åæ° | ç±»å | å¿
å¡« | 说æ | |
| | | |------|------|------|------| |
| | | | modules | List\<String\> | æ¯ | è¦çææ°æ®ç模åï¼salesï¼éå®ï¼ãpurchaseï¼éè´ï¼ãqualityï¼è´¨éï¼ãproductionï¼ç产ï¼ãstockï¼åºåï¼ | |
| | | | industries | List\<String\> | æ¯ | è¡ä¸ï¼å¦ï¼["æºæ¢°å¶é ", "é£åå å·¥", "çµåè£
é
"] | |
| | | | countMin | int | å¦ | æ¯ä¸ªå®ä½çææ¡æ°æå°å¼ï¼é»è®¤ 3 | |
| | | | countMax | int | å¦ | æ¯ä¸ªå®ä½çææ¡æ°æå¤§å¼ï¼é»è®¤ 10 | |
| | | | dateStart | String | å¦ | æ¶é´èå´å¼å§ï¼yyyy-MM-ddï¼ | |
| | | | dateEnd | String | å¦ | æ¶é´èå´ç»æï¼yyyy-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 çåæ®µ |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | # ç¥è¯åºRAGåéæ£ç´¢åè½å®ç°ææ¡£ |
| | | |
| | | ## ä¸ãåè½æ¦è¿° |
| | | |
| | | åºäº RAGï¼Retrieval-Augmented Generationï¼ææ¯å®ç°ç¥è¯åºé®çåè½ï¼æ¯æï¼ |
| | | - ç¥è¯åºç®¡çï¼CRUDï¼ |
| | | - æä»¶ä¸ä¼ ä¸åéåå¤ç |
| | | - åºäºåéæ£ç´¢çæºè½é®ç |
| | | - å¤ç§æä»¶æ ¼å¼æ¯æï¼txtãmdãdocxãxlsxãxlsãpdfï¼ |
| | | |
| | | ## äºãææ¯æ¶æ |
| | | |
| | | ### 2.1 ææ¯æ |
| | | | ç»ä»¶ | ææ¯ | |
| | | |------|------| |
| | | | åéæ°æ®åº | Pinecone | |
| | | | Embedding模å | é¿éäº DashScope text-embedding-v3 | |
| | | | LLM | é¿éäºéä¹åé® qwen-max | |
| | | | æ¡æ¶ | langchain4j | |
| | | | ORM | MyBatis-Plus | |
| | | |
| | | ### 2.2 æ¶æå¾ |
| | | |
| | | ``` |
| | | âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ |
| | | â å端åºç¨ â |
| | | âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ |
| | | â |
| | | â¼ |
| | | âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ |
| | | â Controller Layer â |
| | | â âââââââââââââââââââââââ âââââââââââââââââââââââââââââââ â |
| | | â â KnowledgeBaseCtrl â â KnowledgeChatController â â |
| | | â â (ç¥è¯åºç®¡ç) â â (ç¥è¯åºé®ç) â â |
| | | â âââââââââââââââââââââââ âââââââââââââââââââââââââââââââ â |
| | | âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ |
| | | â |
| | | â¼ |
| | | âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ |
| | | â Service Layer â |
| | | â âââââââââââââââââââââââ âââââââââââââââââââââââââââââââ â |
| | | â âKnowledgeBaseService â â KnowledgeRagService â â |
| | | â â (ç¥è¯åºCRUD) â â (åéå/æ£ç´¢) â â |
| | | â âââââââââââââââââââââââ âââââââââââââââââââââââââââââââ â |
| | | âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ |
| | | â |
| | | â¼ |
| | | âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ |
| | | â AI Layer â |
| | | â âââââââââââââââââââââââ âââââââââââââââââââââââââââââââ â |
| | | â â KnowledgeChatAgent â â EmbeddingStore (Pinecone) â â |
| | | â â (é®çAgent) â â (åéåå¨) â â |
| | | â âââââââââââââââââââââââ âââââââââââââââââââââââââââââââ â |
| | | âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## ä¸ãå端å®ç° |
| | | |
| | | ### 3.1 æ°æ®åºè®¾è®¡ |
| | | |
| | | #### 3.1.1 ç¥è¯åºè¡¨ï¼knowledge_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 'è§£å³æç', |
| | | problem TEXT COMMENT 'é®é¢æè¿°', |
| | | solution TEXT COMMENT 'è§£å³æ¹æ¡', |
| | | 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 ç¥è¯åºåéè®°å½è¡¨ï¼knowledge_base_vectorï¼ |
| | | |
| | | ```sql |
| | | CREATE TABLE knowledge_base_vector ( |
| | | id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主é®ID', |
| | | knowledge_base_id BIGINT NOT NULL COMMENT 'å
³èç¥è¯åºID', |
| | | 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 é
ç½®æä»¶ï¼application.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 æ ¸å¿ä»£ç å®ç° |
| | | |
| | | #### 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 ç¥è¯åºé®çAgent |
| | | |
| | | **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()); |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## åãAPIæ¥å£ææ¡£ |
| | | |
| | | ### 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": "å¦ä½å¤çåºåçç¹å·®å¼ï¼" |
| | | } |
| | | ``` |
| | | |
| | | **ååº**ï¼SSEæµå¼ï¼ |
| | | ``` |
| | | æ ¹æ®ç¥è¯åºå
容ï¼åºåçç¹å·®å¼çå¤çæµç¨å¦ä¸ï¼ |
| | | |
| | | 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ï¼æµå¼æ¥å£ï¼ |
| | | 3. å端å¤çï¼ |
| | | âââ æå»ºå½å空é´ï¼kb-{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å°è£
|
| | | ``` |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | # ç¥è¯åºæ¨¡åä¼ åæ¹å¼ååæ°å½åè§èææ¡£ |
| | | |
| | | ## ä¸ãæ¦è¿° |
| | | |
| | | æ¬ææ¡£è¯¦ç»è¯´æç¥è¯åºæ¨¡åä¸æææ¥å£ãç»ä»¶ãæ¹æ³çä¼ åæ¹å¼ååæ°å½åè§è,æ¨å¨: |
| | | - ç»ä¸ååç«¯åæ°å½åè§è |
| | | - æç¡®åæ°ç±»ååå¿
å¡«æ§ |
| | | - è§èä¼ åæ¹å¼(GET paramsãPOST bodyãDELETE data) |
| | | - æä¾æ¸
æ°çåæ°æ å°å
³ç³» |
| | | |
| | | --- |
| | | |
| | | ## äºãåæ°å½åè§è |
| | | |
| | | ### 2.1 åºæ¬è§è |
| | | |
| | | #### å½å飿 ¼ |
| | | - **åç«¯åæ°**: éµå¾ª Java 驼峰å½åæ³ (camelCase) |
| | | - **åç«¯åæ°**: éµå¾ª JavaScript 驼峰å½åæ³ (camelCase) |
| | | - **æ°æ®åºå段**: éµå¾ª MySQL ä¸å线å½åæ³ (snake_case) |
| | | - **æ¥å£URL**: éµå¾ª RESTful 飿 ¼,使ç¨å°ååè¿å符 |
| | | |
| | | #### å½åçº¦å® |
| | | 1. **IDç¸å
³**: ç»ä¸ä½¿ç¨ `id`ã`Id` åç¼ |
| | | - `knowledgeBaseId` - ç¥è¯åºID |
| | | - `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` - æ´æ°æ¶é´ |
| | | |
| | | --- |
| | | |
| | | ## ä¸ãæ¥å£ä¼ åæ¹å¼è§è |
| | | |
| | | ### 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` ä¸ |
| | | - â
åæ°åä¸å端å®ä½åæ®µä¿æä¸è´ |
| | | - â
å¿
å¡«åæ°éè¦å¨è¡¨åéªè¯è§åä¸å£°æ |
| | | - â
æ°å¼ç±»ååæ°éæå®é»è®¤å¼ |
| | | |
| | | --- |
| | | |
| | | ### 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]); // â
ç´æ¥ä¼ éIDæ°ç» |
| | | ``` |
| | | |
| | | **请æ±ä½æ ¼å¼**: JSON æ°ç» `[1, 2, 3]` |
| | | |
| | | **è§èè¦ç¹**: |
| | | - â
DELETE请æ±çåæ°æ¾å¨ `data` ä¸ |
| | | - â
æ¹éå é¤ä¼ éIDæ°ç» |
| | | - â
å个å é¤ä¹å¯ä»¥ä¼ éæ°ç» `[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 ç¥è¯åºç®¡çæ¥å£åæ° |
| | | |
| | | #### æ¥è¯¢å表 (`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 | è§£å³æç | "high" | |
| | | | problem | String | æ¯ | data | é®é¢æè¿° | "ç¨æ·ä¸ä¼æä½" | |
| | | | solution | String | æ¯ | data | è§£å³æ¹æ¡ | "åèæå" | |
| | | | 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 | ç¥è¯åºID | 10 | |
| | | | *(å
¶ä»åæ°åæ°å¢)* | - | - | data | - | - | |
| | | |
| | | **å端è°ç¨**: |
| | | ```javascript |
| | | updateKnowledgeBase({ |
| | | id: 10, |
| | | title: "æä½æå(æ´æ°)", |
| | | type: "guide", |
| | | // ...å
¶ä»åæ° |
| | | }); |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | #### å é¤ç¥è¯åº (`DELETE /knowledgeBase/delete`) |
| | | |
| | | | åæ°å | ç±»å | å¿
å¡« | ä¼ åä½ç½® | 说æ | 示ä¾å¼ | |
| | | |--------|------|------|----------|------|--------| |
| | | | ids | Long[] | æ¯ | data | ç¥è¯åºIDæ°ç» | [1, 2, 3] | |
| | | |
| | | **å端è°ç¨**: |
| | | ```javascript |
| | | delKnowledgeBase([1, 2, 3]); |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 4.2 æä»¶ç®¡çæ¥å£åæ° |
| | | |
| | | #### æ¥è¯¢åéåç¶æ (`GET /knowledgeBase/vector/status/{knowledgeBaseId}`) |
| | | |
| | | | åæ°å | ç±»å | å¿
å¡« | ä¼ åä½ç½® | 说æ | 示ä¾å¼ | |
| | | |--------|------|------|----------|------|--------| |
| | | | knowledgeBaseId | Long | æ¯ | URLè·¯å¾ | ç¥è¯åºID | 10 | |
| | | |
| | | **å端è°ç¨**: |
| | | ```javascript |
| | | getVectorStatus(10); |
| | | ``` |
| | | |
| | | **URL**: `/knowledgeBase/vector/status/10` |
| | | |
| | | --- |
| | | |
| | | #### ä¿åæä»¶å
³è (`POST /knowledgeBase/file/save`) |
| | | |
| | | | åæ°å | ç±»å | å¿
å¡« | ä¼ åä½ç½® | 说æ | 示ä¾å¼ | |
| | | |--------|------|------|----------|------|--------| |
| | | | knowledgeBaseId | Long | æ¯ | data | ç¥è¯åºID | 10 | |
| | | | storageBlobIds | Long[] | æ¯ | data | æä»¶blob IDæ°ç» | [123, 124] | |
| | | |
| | | **å端è°ç¨**: |
| | | ```javascript |
| | | saveKnowledgeBaseFiles({ |
| | | knowledgeBaseId: 10, |
| | | storageBlobIds: [123, 124] |
| | | }); |
| | | ``` |
| | | |
| | | **éè¦è¯´æ**: |
| | | - â ï¸ **å¿
é¡»å
è°ç¨ `/common/upload` ä¸ä¼ æä»¶** |
| | | - â ï¸ **è·åè¿åç `data.id` ä½ä¸º `storageBlobId`** |
| | | - â ï¸ **æ¤æ¥å£è§¦å弿¥åéåå¤ç** |
| | | |
| | | --- |
| | | |
| | | #### å é¤ç¥è¯åºæä»¶ (`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 | ç¥è¯åºID | 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, // ç¥è¯åºID |
| | | title: "æä½æå", // ç¥è¯æ é¢ |
| | | type: "guide", // ç¥è¯ç±»å |
| | | scenario: "ç³»ç»æä½", // éç¨åºæ¯ |
| | | efficiency: "high", // è§£å³æç |
| | | problem: "...", // é®é¢æè¿° |
| | | solution: "...", // è§£å³æ¹æ¡ |
| | | 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ãmdãdocxãxlsxãxlsãpdf æ ¼å¼çæä»¶'); |
| | | 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; // 鲿¢éå¤åé |
| | | } |
| | | |
| | | // ...åéè¯·æ± |
| | | }; |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## ä¹ã常è§é误åè§£å³æ¹æ¡ |
| | | |
| | | ### 9.1 åæ°åä¸å¹é
|
| | | |
| | | **é误示ä¾**: |
| | | ```javascript |
| | | // â é误: 使ç¨äºä¸å线å½å |
| | | saveKnowledgeBaseFiles({ |
| | | knowledge_base_id: 10, |
| | | storage_blob_ids: [123] |
| | | }); |
| | | |
| | | // â
æ£ç¡®: 使ç¨é©¼å³°å½å |
| | | saveKnowledgeBaseFiles({ |
| | | knowledgeBaseId: 10, |
| | | storageBlobIds: [123] |
| | | }); |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 9.2 ä¼ åä½ç½®é误 |
| | | |
| | | **é误示ä¾**: |
| | | ```javascript |
| | | // â é误: POST请æ±ä½¿ç¨ params |
| | | export function addKnowledgeBase(data) { |
| | | return request({ |
| | | url: "/knowledgeBase/add", |
| | | method: "post", |
| | | params: data // â é误 |
| | | }); |
| | | } |
| | | |
| | | // â
æ£ç¡®: POST请æ±ä½¿ç¨ data |
| | | export function addKnowledgeBase(data) { |
| | | return request({ |
| | | url: "/knowledgeBase/add", |
| | | method: "post", |
| | | data: data // â
æ£ç¡® |
| | | }); |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 9.3 DELETE请æ±åæ°é误 |
| | | |
| | | **é误示ä¾**: |
| | | ```javascript |
| | | // â é误: DELETEä½¿ç¨ params ä¼ éæ°ç» |
| | | export function delKnowledgeBase(ids) { |
| | | return request({ |
| | | url: "/knowledgeBase/delete", |
| | | method: "delete", |
| | | params: ids // â é误 |
| | | }); |
| | | } |
| | | |
| | | // â
æ£ç¡®: DELETEä½¿ç¨ data ä¼ éæ°ç» |
| | | export function delKnowledgeBase(ids) { |
| | | return request({ |
| | | url: "/knowledgeBase/delete", |
| | | method: "delete", |
| | | data: ids // â
æ£ç¡® |
| | | }); |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 9.4 æµå¼æ¥å£é误 |
| | | |
| | | **é误示ä¾**: |
| | | ```javascript |
| | | // â é误: æµå¼æ¥å£ä½¿ç¨ axios |
| | | export function knowledgeChat(data) { |
| | | return request({ |
| | | url: "/ai/knowledge/chat", |
| | | method: "post", |
| | | data: data // â axios䏿¯ææµå¼ |
| | | }); |
| | | } |
| | | |
| | | // â
æ£ç¡®: æµå¼æ¥å£ä½¿ç¨ 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) // â
æ£ç¡® |
| | | }); |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 9.5 æä»¶ä¸ä¼ IDè·åé误 |
| | | |
| | | **é误示ä¾**: |
| | | ```javascript |
| | | // â é误: ä¸ä¼ æååæ²¡æä¿å storageBlobId |
| | | const handleUploadSuccess = (response) => { |
| | | if (response.code === 200) { |
| | | ElMessage.success("ä¸ä¼ æå"); |
| | | // â æ²¡æä¿å response.data.id |
| | | } |
| | | }; |
| | | |
| | | // â
æ£ç¡®: ä¿å 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 å¿
å¡«åæ°æ ¡éª |
| | | |
| | | â
**å¨è°ç¨æ¥å£åæ ¡éªå¿
å¡«åæ°** |
| | | ```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}`; |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## åä¸ãåæ°æ å°å
³ç³»æ»ç» |
| | | |
| | | ### ç¥è¯åºIDç¸å
³ |
| | | | åºæ¯ | åæ°å | ç±»å | æ¥æº | |
| | | |------|--------|------|------| |
| | | | ç¥è¯åºå表æ¥è¯¢ | - | - | 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 (红è²) | |
| | | |
| | | --- |
| | | |
| | | ## åä¸ãæ»ç» |
| | | |
| | | æ¬ææ¡£è¯¦ç»è§èäºç¥è¯åºæ¨¡åçåæ°å½ååä¼ åæ¹å¼,éµå¾ªä»¥ä¸åå: |
| | | |
| | | 1. â
**å½åä¸è´æ§**: ååç«¯åæ°åä¿æä¸è´(驼峰å½å) |
| | | 2. â
**ä¼ åè§èæ§**: GETç¨paramsãPOSTç¨dataãDELETEç¨data |
| | | 3. â
**ç±»åæç¡®æ§**: æç¡®åæ°ç±»å,åç设置é»è®¤å¼ |
| | | 4. â
**æ ¡éªå®æ´æ§**: å¿
å¡«åæ°éæ ¡éª,å¯éåæ°æé»è®¤å¼ |
| | | 5. â
**é误é¿å
**: éµå¾ªè§èé¿å
常è§é误 |
| | | |
| | | 建议å¢éæåä¸¥æ ¼éµå¾ªæ¬è§è,ç¡®ä¿ååç«¯åæ°ä¼ éçä¸è´æ§åå¯é æ§ã |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | # ç¥è¯åºæ¨¡å宿´å端å®ç°ææ¡£ |
| | | |
| | | ## ä¸ãæ¨¡åæ¦è¿° |
| | | |
| | | ç¥è¯åºæ¨¡åæ¯ä¸ä¸ªéæäºRAG(æ£ç´¢å¢å¼ºçæ)ææ¯çæºè½ç¥è¯ç®¡çç³»ç»,æ¯æ: |
| | | - **ç¥è¯åºCRUD管ç** - å建ãç¼è¾ãå é¤ãæ¥è¯¢ç¥è¯åº |
| | | - **æä»¶ä¸ä¼ ä¸åéå** - æ¯æå¤ç§æä»¶æ ¼å¼,èªå¨è¿è¡åéåçå¤ç |
| | | - **æºè½é®ç** - åºäºä¸ä¼ æä»¶å
容è¿è¡AIé®ç |
| | | - **æä»¶ç®¡ç** - æ¥çæä»¶åéåç¶æ,æ¯æéæ°å¤çåå é¤ |
| | | |
| | | ### ææ¯æ¶æ |
| | | - **åç«¯æ¡æ¶**: Vue 3 + Composition API |
| | | - **UIç»ä»¶åº**: Element Plus |
| | | - **åéæ°æ®åº**: Pinecone |
| | | - **AI模å**: é¿éäºéä¹åé® |
| | | - **æä»¶å¤ç**: æ¯ædocxãxlsxãpdfãtxtãmdçæ ¼å¼ |
| | | |
| | | --- |
| | | |
| | | ## äºãæä»¶ç»æ |
| | | |
| | | ``` |
| | | src/ |
| | | âââ api/ |
| | | â âââ collaborativeApproval/ |
| | | â âââ knowledgeBase.js # APIæ¥å£å°è£
|
| | | âââ views/ |
| | | â âââ collaborativeApproval/ |
| | | â âââ knowledgeBase/ |
| | | â âââ index.vue # 主页é¢ç»ä»¶ |
| | | âââ components/ |
| | | âââ PIMTable/ |
| | | â âââ PIMTable.vue # è¡¨æ ¼ç»ä»¶ |
| | | âââ Dialog/ |
| | | âââ FormDialog.vue # å¼¹çªç»ä»¶ |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## ä¸ãAPIæ¥å£å®ä¹ |
| | | |
| | | ### 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, // ç¥è¯åºID |
| | | storageBlobIds: [123, 124] // ä¸ä¼ æä»¶è¿åçblob 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页ç¾å¸å±,å
å«ä¸¤ä¸ªä¸»è¦åè½æ¨¡å: |
| | | - **ç¥è¯åºç®¡ç** - ç¥è¯åºCRUDæä½ |
| | | - **ç¥è¯åºé®ç** - åºäº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: "è§£å³æç", |
| | | 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ç§å·æ°æä»¶å表 |
| | | â |
| | | æ¾ç¤ºåéåç¶æ(å¾
å¤çâå¤çä¸â已宿) |
| | | ``` |
| | | |
| | | #### 代ç å®ç° |
| | | |
| | | ```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ãmdãdocxãxlsxãxlsãpdf æ ¼å¼çæä»¶'); |
| | | 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("æä»¶å
³èä¿åæå,æ£å¨åå°å¤çåéå"); |
| | | 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 (æµå¼æ¥å£) |
| | | â |
| | | å端å¤ç: |
| | | - 对é®é¢è¿è¡åéå |
| | | - å¨Pinecone䏿£ç´¢ç¸å
³åç |
| | | - æå»ºä¸ä¸æPrompt |
| | | - è°ç¨LLMçæåç |
| | | â |
| | | æµå¼è¿åAIåç |
| | | â |
| | | åç«¯å®æ¶æ¾ç¤ºåçå
容 |
| | | â |
| | | èªå¨æ»å¨å°åºé¨ |
| | | ``` |
| | | |
| | | #### 代ç å®ç° |
| | | |
| | | ```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">æ£å¨æèä¸...</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 | å¤çä¸ | æ£å¨è¿è¡åéåçå¤ç | 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: 'è§£å³æ¹æ¡', elTagType: 'primary' }, |
| | | { value: 'experience', label: 'ç»éªå享', elTagType: 'info' }, |
| | | { value: 'guide', label: 'æä½æå', elTagType: 'danger' } |
| | | ]; |
| | | ``` |
| | | |
| | | ### 7.3 è§£å³æçæ å° |
| | | |
| | | ```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ãmdãdocxãxlsxãxlsãpdf æ ¼å¼ |
| | | 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 ç¥è¯åºç®¡ç |
| | | |
| | | - [ ] æ°å¢ç¥è¯åºæå |
| | | - [ ] ç¼è¾ç¥è¯åºæå |
| | | - [ ] å é¤ç¥è¯åºæå(å个/æ¹é) |
| | | - [ ] æç´¢åè½æ£å¸¸(ææ é¢ãç±»å) |
| | | - [ ] å页åè½æ£å¸¸ |
| | | - [ ] 导åºåè½æ£å¸¸ |
| | | |
| | | ### 10.2 æä»¶ä¸ä¼ |
| | | |
| | | - [ ] åæä»¶ä¸ä¼ æå |
| | | - [ ] 夿件ä¸ä¼ æå |
| | | - [ ] æä»¶ç±»åæ ¡éªæ£å¸¸ |
| | | - [ ] æä»¶å¤§å°æ ¡éªæ£å¸¸ |
| | | - [ ] ä¿åæä»¶å
³èæå |
| | | - [ ] åéåç¶ææ£ç¡®æ¾ç¤º |
| | | |
| | | ### 10.3 æä»¶ç®¡ç |
| | | |
| | | - [ ] æ¥çæä»¶å表æ£å¸¸ |
| | | - [ ] åéåç¶æè½®è¯¢æ£å¸¸ |
| | | - [ ] éæ°å¤ç失败æä»¶æå |
| | | - [ ] å 餿件æå |
| | | - [ ] æä»¶é¢è§/ä¸è½½æ£å¸¸ |
| | | |
| | | ### 10.4 ç¥è¯åºé®ç |
| | | |
| | | - [ ] é®çå¼¹çªæå¼æ£å¸¸ |
| | | - [ ] åéé®é¢æå |
| | | - [ ] æµå¼ååºæ¾ç¤ºæ£å¸¸ |
| | | - [ ] å¤è½®å¯¹è¯æ£å¸¸ |
| | | - [ ] èªå¨æ»å¨å°åºé¨ |
| | | - [ ] é误å¤çæ£å¸¸ |
| | | |
| | | --- |
| | | |
| | | ## åä¸ã常è§é®é¢ |
| | | |
| | | ### Q1: æä»¶ä¸ä¼ åæ²¡æè§¦ååéå? |
| | | |
| | | **A**: æ£æ¥æ¯å¦è°ç¨äº `/knowledgeBase/file/save` æ¥å£ãä¸ä¼ æååå¿
é¡»è°ç¨æ¤æ¥å£æè½è§¦ååéåã |
| | | |
| | | ### 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) |
| | | - â
宿ç¥è¯åºCRUDåè½ |
| | | - â
宿æä»¶ä¸ä¼ ä¸åéååè½ |
| | | - â
宿ç¥è¯åºé®çåè½ |
| | | - â
宿æä»¶ç®¡çåè½ |
| | | - â
宿åéåç¶ææ¾ç¤º |
| | | |
| | | ### v1.1.0 (计åä¸) |
| | | - ð² æ·»å åå²è®°å½æ¥è¯¢ |
| | | - ð² æ·»å æ¹éæä½åè½ |
| | | - ð² æ·»å æä»¶é¢è§åè½ |
| | | - ð² ä¼ååéåè¿åº¦æ¾ç¤º |
| | |
| | | const backupFile = path.join(replacePath, config[key]); |
| | | const replaceFile = path.join(resourcePath, companyMap[key]); |
| | | |
| | | if (fsSync.existsSync(originFile)) { |
| | | await copyFileWithOverwrite(originFile, backupFile); |
| | | } |
| | | await copyFileWithOverwrite(replaceFile, originFile); |
| | | } |
| | | |
| | |
| | | |
| | | const originFile = path.join(rootPath, config[key]); |
| | | const backupFile = path.join(replacePath, config[key]); |
| | | if (fsSync.existsSync(backupFile)) { |
| | | await copyFileWithOverwrite(backupFile, originFile); |
| | | } |
| | | } |
| | | await fs.rm(replacePath, { recursive: true, force: true }); |
| | | console.log(`ðï¸ å·²å é¤ ${replacePath}`); |
| | | } |
| | |
| | | import request from "@/utils/request"; |
| | | import { getToken } from '@/utils/auth'; |
| | | |
| | | // æ¥è¯¢ç¥è¯åºå表 |
| | | /** |
| | | * ç¥è¯åºç®¡çæ¥å£ |
| | | * ä¼ åè§è: |
| | | * - GET请æ±: ä½¿ç¨ params |
| | | * - POST请æ±: ä½¿ç¨ data |
| | | * - DELETE请æ±: ä½¿ç¨ data |
| | | * - æµå¼è¯·æ±: ä½¿ç¨ Fetch API |
| | | */ |
| | | |
| | | // ==================== ç¥è¯åºCRUDæ¥å£ ==================== |
| | | |
| | | /** |
| | | * æ¥è¯¢ç¥è¯åºå表 |
| | | * @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] - è§£å³æç(å¯é) |
| | | * @param {string} data.problem - é®é¢æè¿°(å¿
å¡«) |
| | | * @param {string} data.solution - è§£å³æ¹æ¡(å¿
å¡«) |
| | | * @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 - ç¥è¯åºID(å¿
å¡«) |
| | | * @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 - ç¥è¯åºIDæ°ç» |
| | | * @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 - ç¥è¯åºID |
| | | * @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 - ç¥è¯åºID(å¿
å¡«) |
| | | * @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 - ç¥è¯åºID(å¿
å¡«) |
| | | * @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", |
| | | }); |
| | | } |
| | |
| | | method: "delete", |
| | | }); |
| | | } |
| | | |
| | | /** æ ¹æ®å®¢æ·idæ¥è¯¢å¼ç¥¨ç³è¯·å°è´¦ */ |
| | | export function getAccountInvoiceApplicationList(query) { |
| | | return request({ |
| | | url: '/accountInvoiceApplication/getAccountInvoiceApplicationList', |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | // æ¹éå审åºåºè®°å½ |
| | | export const batchUnapproveStockOutRecords = (data) => { |
| | | return request({ |
| | | url: "/stockOutRecord/reAudit", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // æ£æµæå®æ¨¡åçåºç¡æ°æ®æ¯å¦å°±ç»ª |
| | | export function dataCheck(modules) { |
| | | return request({ |
| | | url: "/mock/dataCheck", |
| | | method: "post", |
| | | data: { modules }, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // AI çææ¨¡ææ°æ®ï¼å«åç½®åºç¡æ°æ®æ£æµï¼ |
| | | export function mockGenerate(data) { |
| | | return request({ |
| | | url: "/mock/generate", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | |
| | | }); |
| | | } |
| | | |
| | | export function staffSalaryMainCalculateByEmployeeId(data) { |
| | | return request({ |
| | | url: "/staffSalaryMain/calculateByEmployeeId", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | export function staffSalaryMainAdd(data) { |
| | | return request({ |
| | | url: "/staffSalaryMain/add", |
| | |
| | | }); |
| | | } |
| | | |
| | | // è·åå¯ç¨é¨é¨åè¡¨ï¼æ ¹æ®æä»½æ¥è¯¢ï¼æé¤å·²åæ¾å·¥èµè¡¨çé¨é¨ï¼ |
| | | export function staffSalaryMainAvailableDepts(salaryMonth) { |
| | | return request({ |
| | | url: "/staffSalaryMain/availableDepts", |
| | | method: "get", |
| | | params: { salaryMonth }, |
| | | }); |
| | | } |
| | |
| | | params, |
| | | }); |
| | | } |
| | | |
| | | /** 仿¬¾å°è´¦ - 仿¬¾ç»è®°å表 */ |
| | | export function registrationList(params) { |
| | | return request({ |
| | | url: "/purchase/report/registration", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | } |
| | |
| | | params: query, |
| | | }); |
| | | } |
| | | // éè´éè´§å¯ééè´å°è´¦ |
| | | export function purchaseReturnableList(query) { |
| | | return request({ |
| | | url: "/purchase/ledger/returnableList", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | // æ¥è¯¢ååå· |
| | | export function getSalesNo(query) { |
| | | return request({ |
| | |
| | | |
| | | 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) |
| | | const safeTab = ["all", "inProgress", "completed", "end"].includes(params.tab) |
| | | ? params.tab |
| | | : "all"; |
| | | return request({ |
| | |
| | | ...params, |
| | | tab: safeTab, |
| | | pageNum: safePageNum, |
| | | pageSize: safePageSize, |
| | | pageSize: params.pageSize, |
| | | }, |
| | | headers: { |
| | | handleAuthError: false, |
| | |
| | | :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" |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-tree-select |
| | | v-model="innerValue" |
| | | :data="data" |
| | | :multiple="multiple" |
| | | :filterable="filterable" |
| | | :collapse-tags="collapseTags" |
| | | :collapse-tags-tooltip="collapseTagsTooltip" |
| | | :placeholder="placeholder" |
| | | :clearable="clearable" |
| | | :check-strictly="checkStrictly" |
| | | :render-after-expand="renderAfterExpand" |
| | | :disabled="disabled" |
| | | v-bind="$attrs" |
| | | @change="handleChange" |
| | | /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed } from "vue"; |
| | | |
| | | const props = defineProps({ |
| | | modelValue: { |
| | | type: [String, Number, Array, Object], |
| | | default: undefined, |
| | | }, |
| | | data: { |
| | | type: Array, |
| | | default: () => [], |
| | | }, |
| | | multiple: { |
| | | type: Boolean, |
| | | default: false, |
| | | }, |
| | | filterable: { |
| | | type: Boolean, |
| | | default: true, |
| | | }, |
| | | collapseTags: { |
| | | type: Boolean, |
| | | default: false, |
| | | }, |
| | | collapseTagsTooltip: { |
| | | type: Boolean, |
| | | default: false, |
| | | }, |
| | | placeholder: { |
| | | type: String, |
| | | default: "è¯·éæ©", |
| | | }, |
| | | clearable: { |
| | | type: Boolean, |
| | | default: true, |
| | | }, |
| | | checkStrictly: { |
| | | type: Boolean, |
| | | default: true, |
| | | }, |
| | | renderAfterExpand: { |
| | | type: Boolean, |
| | | default: false, |
| | | }, |
| | | disabled: { |
| | | type: Boolean, |
| | | default: false, |
| | | }, |
| | | }); |
| | | |
| | | const emit = defineEmits(["update:modelValue", "change"]); |
| | | |
| | | const innerValue = computed({ |
| | | get() { |
| | | return props.modelValue; |
| | | }, |
| | | set(val) { |
| | | emit("update:modelValue", val); |
| | | }, |
| | | }); |
| | | |
| | | const handleChange = val => { |
| | | emit("change", val); |
| | | }; |
| | | </script> |
| | | |
| | |
| | | size: paramPage.value.size, |
| | | }).then(res => { |
| | | if (res.code === 200) { |
| | | filteredParamList.value = res.data?.records || []; |
| | | const existingParamIds = new Set( |
| | | (props.paramList || []) |
| | | .map(item => Number(item.paramId || item.technologyOperationParamId)) |
| | | .filter(id => Number.isFinite(id) && id > 0) |
| | | ); |
| | | filteredParamList.value = (res.data?.records || []).filter( |
| | | item => !existingParamIds.has(Number(item.id)) |
| | | ); |
| | | paramPage.value.total = res.data.total || 0; |
| | | } else { |
| | | ElMessage.error(res.msg || "æ¥è¯¢å¤±è´¥"); |
| | |
| | | addProcessRouteItemParamOrder({ |
| | | productionOrderId: Number(props.orderId), |
| | | productionOrderRoutingOperationId: props.process.id, |
| | | technologyRoutingOperationParamId: props.process.id, |
| | | paramId: selectedParam.value.id, |
| | | standardValue: selectedParam.value.standardValue || "", |
| | | isRequired: selectedParam.value.isRequired || 0, |
| | |
| | | }, |
| | | ], |
| | | }, |
| | | // æå¦æ¼ç¤ºæ¨¡å |
| | | { |
| | | path: "/productionManagement/teachingDemo", |
| | | component: Layout, |
| | | hidden: true, |
| | | children: [ |
| | | { |
| | | path: "", |
| | | component: () => import("@/views/productionManagement/teachingDemo/index.vue"), |
| | | name: "TeachingDemo", |
| | | meta: { title: "å·¥èºè·¯çº¿ä¸BOMæå¦æ¼ç¤º", 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, |
| | |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "text", |
| | | showHide: row => !isCustomerEditLocked(row), |
| | | clickFun: row => { |
| | | if (isCustomerEditLocked(row)) { |
| | | proxy.$modal.msgWarning("已被é宿¨¡åå¼ç¨çç§æµ·å®¢æ·ä¸è½ç¼è¾"); |
| | | return; |
| | | } |
| | | openForm("edit", row); |
| | | }, |
| | | }, |
| | |
| | | } |
| | | // æå¼å¼¹æ¡ |
| | | const openForm = (type, row) => { |
| | | if (type === "edit" && isCustomerEditLocked(row)) { |
| | | proxy.$modal.msgWarning("已被é宿¨¡åå¼ç¨çç§æµ·å®¢æ·ä¸è½ç¼è¾"); |
| | | return; |
| | | } |
| | | operationType.value = type; |
| | | form.value = {}; |
| | | form.value.maintainer = userStore.nickName; |
| | |
| | | } |
| | | 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 => { |
| | |
| | | { |
| | | name: "åæ¶", |
| | | type: "text", |
| | | disabled: row => isCustomerRecoveryLocked(row), |
| | | showHide: row => row.usageStatus == 1, |
| | | clickFun: row => { |
| | | if (isCustomerRecoveryLocked(row)) { |
| | | proxy.$modal.msgWarning("å·²é¢ç¨å¹¶ä¸è¢«é宿¨¡åå¼ç¨çå
¬æµ·å®¢æ·ä¸è½åæ¶"); |
| | | return; |
| | | } |
| | | recycle(row); |
| | | }, |
| | | }, |
| | |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "text", |
| | | disabled: row => isCustomerEditLocked(row), |
| | | clickFun: row => { |
| | | if (isCustomerEditLocked(row)) { |
| | | proxy.$modal.msgWarning("å·²é¢ç¨å¹¶ä¸è¢«é宿¨¡åå¼ç¨çå
¬æµ·å®¢æ·ä¸è½ç¼è¾"); |
| | | return; |
| | | } |
| | | openForm("edit", row); |
| | | }, |
| | | }, |
| | |
| | | } |
| | | // æå¼å¼¹æ¡ |
| | | const openForm = (type, row) => { |
| | | if (type === "edit" && isCustomerEditLocked(row)) { |
| | | proxy.$modal.msgWarning("å·²é¢ç¨å¹¶ä¸è¢«é宿¨¡åå¼ç¨çå
¬æµ·å®¢æ·ä¸è½ç¼è¾"); |
| | | return; |
| | | } |
| | | operationType.value = type; |
| | | form.value = {}; |
| | | form.value.maintainer = userStore.nickName; |
| | |
| | | } |
| | | 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 => { |
| | |
| | | </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">æ£å¨æèä¸...</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 = { |
| | |
| | | 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 { |
| | |
| | | dialogTitle, |
| | | dialogType, |
| | | viewDialogVisible, |
| | | currentKnowledge |
| | | currentKnowledge, |
| | | filesDialogVisible, |
| | | currentKnowledgeBase, |
| | | fileList, |
| | | uploadedBlobIds, |
| | | savingFiles, |
| | | vectorStatusTimer, |
| | | chatDialogVisible, |
| | | messages, |
| | | inputQuestion, |
| | | chatLoading, |
| | | memoryId |
| | | } = toRefs(data); |
| | | |
| | | // 表åå¼ç¨ |
| | |
| | | // ç¨æ·ç¸å
³ |
| | | 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([ |
| | |
| | | } |
| | | }, |
| | | { |
| | | label: "æä»¶æ°é", |
| | | prop: "fileCount", |
| | | width: 100, |
| | | align: "center" |
| | | }, |
| | | { |
| | | label: "åçæ°é", |
| | | prop: "totalChunkCount", |
| | | width: 100, |
| | | align: "center" |
| | | }, |
| | | { |
| | | label: "ä½¿ç¨æ¬¡æ°", |
| | | prop: "usageCount", |
| | | width: 100, |
| | |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openForm("edit", row); |
| | | } |
| | | }, |
| | | { |
| | | name: "æä»¶", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openFilesDialog(row); |
| | | } |
| | | }, |
| | | { |
| | | name: "é®ç", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openChatDialog(row); |
| | | } |
| | | }, |
| | | { |
| | |
| | | |
| | | 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); |
| | | }); |
| | | }; |
| | | |
| | | // å页å¤ç |
| | |
| | | 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); |
| | |
| | | return; |
| | | } |
| | | |
| | | ElMessageBox.confirm("éä¸çå
容å°è¢«å é¤ï¼æ¯å¦ç¡®è®¤å é¤ï¼", "å é¤", { |
| | | ElMessageBox.confirm("éä¸çå
容å°è¢«å é¤,æ¯å¦ç¡®è®¤å é¤?", "å é¤", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }).then(() => { |
| | | // console.log(selectedIds.value); |
| | | // â
DELETE请æ±ä½¿ç¨dataä¼ éIDæ°ç» |
| | | delKnowledgeBase(selectedIds.value).then(res => { |
| | | if(res.code == 200){ |
| | | ElMessage.success("å 餿å"); |
| | | selectedIds.value = []; |
| | | getList(); |
| | | } |
| | | }) |
| | | }).catch(err => { |
| | | console.error("å é¤ç¥è¯åºå¤±è´¥:", err); |
| | | ElMessage.error(err.msg || "å é¤å¤±è´¥"); |
| | | }); |
| | | }).catch(() => { |
| | | // ç¨æ·åæ¶ |
| | | }); |
| | |
| | | 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ãmdãdocxãxlsxãxlsãpdf æ ¼å¼çæä»¶'); |
| | | 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>,æä»¥dataæ¯æ°ç» |
| | | 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, // ç¥è¯åºID |
| | | storageBlobIds: uploadedBlobIds.value // æä»¶blob IDæ°ç» |
| | | }); |
| | | |
| | | ElMessage.success("æä»¶å
³èä¿åæå,æ£å¨åå°å¤çåéå"); |
| | | 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ä¼ éIDæ°ç» |
| | | await deleteKnowledgeBaseFile([row.id]); // 注æ: row.idæ¯åéè®°å½ID,䏿¯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çfallbackæ¹æ¡ |
| | | 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, // ç¥è¯åºID |
| | | 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> |
| | |
| | | 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> |
| | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å¼å§æ¶é´" prop="startTime"> |
| | | <el-select |
| | | <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-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 |
| | | <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-option |
| | | v-for="time in endTimeOptions" |
| | | :key="time.value" |
| | | :label="time.label" |
| | | :value="time.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | |
| | | // åå·¥å表 |
| | | const employees = ref([]) |
| | | |
| | | // æ¶é´é项ï¼ä»¥åå°æ¶ä¸ºé´éï¼ |
| | | const timeOptions = ref([]) |
| | | |
| | | const getTimeInMinutes = (time) => { |
| | | if (!time) return -1 |
| | | const [hour, minute] = time.split(':').map(Number) |
| | |
| | | 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() |
| | | } |
| | | |
| | |
| | | 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'}], |
| | |
| | | participants: [{required: true, message: 'è¯·éæ©åä¼äººå', trigger: 'change'}] |
| | | } |
| | | |
| | | const startTimeOptions = computed(() => { |
| | | if (!isToday(meetingForm.meetingDate)) { |
| | | return timeOptions.value |
| | | // æ¶é´éæ©å¨ç¦ç¨é»è¾ |
| | | 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() |
| | | 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 |
| | | for (let h = 8; h < now.getHours(); h++) { |
| | | if (!hours.includes(h)) hours.push(h) |
| | | } |
| | | const startMinutes = getTimeInMinutes(meetingForm.startTime) |
| | | return timeOptions.value.filter(item => getTimeInMinutes(item.value) > startMinutes) |
| | | }) |
| | | } |
| | | return hours |
| | | } |
| | | |
| | | // åå§åæ¶é´é项 |
| | | const initTimeOptions = () => { |
| | | const options = [] |
| | | 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() |
| | | 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` |
| | | }) |
| | | if (hour === now.getHours()) { |
| | | if (now.getMinutes() >= 30) { |
| | | minutes.push(0) |
| | | } |
| | | } |
| | | timeOptions.value = options |
| | | } |
| | | return minutes |
| | | } |
| | | |
| | | 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 = '' |
| | | const disabledEndHours = () => { |
| | | const hours = [] |
| | | for (let h = 0; h < 24; h++) { |
| | | if (h < 8 || h > 18) hours.push(h) |
| | | } |
| | | if (meetingForm.startTime) { |
| | | meetingFormRef.value?.validateField('startTime') |
| | | const startHour = parseInt(meetingForm.startTime.split(':')[0]) |
| | | for (let h = 8; h < startHour; h++) { |
| | | if (!hours.includes(h)) hours.push(h) |
| | | } |
| | | if (meetingForm.endTime) { |
| | | meetingFormRef.value?.validateField('endTime') |
| | | } |
| | | initTimeOptions() |
| | | }) |
| | | 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)) { |
| | |
| | | |
| | | // 模ææäº¤æä½ |
| | | ElMessage.success(`${getCurrentTypeName()}æäº¤æå`) |
| | | |
| | | // æ ¹æ®ä¸åç±»åæ§è¡ä¸åæä½ |
| | | switch (currentType.value) { |
| | | case 'approval': |
| | | ElMessage.info('ä¼è®®å·²æäº¤å®¡æ¹æµç¨') |
| | | break |
| | | case 'department': |
| | | ElMessage.info('é¨é¨çº§ä¼è®®ç³è¯·å·²æäº¤') |
| | | break |
| | | case 'notification': |
| | | ElMessage.info('ä¼è®®éç¥å·²åå¸') |
| | | break |
| | | } |
| | | resetForm() |
| | | }) |
| | | |
| | |
| | | |
| | | // 页é¢å è½½æ¶åå§å |
| | | onMounted(() => { |
| | | initTimeOptions() |
| | | getRoomEnum().then(res => { |
| | | meetingRooms.value = res.data |
| | | }) |
| | |
| | | <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"> |
| | |
| | | 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 { |
| | |
| | | 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) |
| | | } |
| | | |
| | |
| | | cells.push({ |
| | | type: 'meeting', |
| | | meeting: meeting, |
| | | span: endIdx - startIdx, |
| | | span: displayEndIdx - startIdx, |
| | | startTime: meeting.startTime, |
| | | endTime: meeting.endTime |
| | | }) |
| | |
| | | <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"> |
| | |
| | | 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 => { |
| | |
| | | <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"> |
| | |
| | | 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 { |
| | |
| | | <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> |
| | |
| | | const resetQuery = () => { |
| | | queryParams.name = ''; |
| | | pagination.current = 1; |
| | | pagination.size = 10; |
| | | fetchListData(); |
| | | } |
| | | |
| | | // å页大尿¹å |
| | | const handleSizeChange = (size) => { |
| | | pagination.size = size; |
| | | pagination.current = 1; |
| | | const handleSizeChange = ({ page, limit }) => { |
| | | pagination.current = page; |
| | | pagination.size = limit; |
| | | fetchListData(); |
| | | } |
| | | |
| | |
| | | </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> |
| | |
| | | <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> |
| | |
| | | ); |
| | | |
| | | const createDefaultForm = () => ({ |
| | | id: null, |
| | | assetCode: "", |
| | | assetName: "", |
| | | category: "", |
| | |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | :tableLoading="tableLoading" |
| | | isShowSummary |
| | | :summaryMethod="getSummaries" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | |
| | | 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, ","); |
| | |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | :tableLoading="tableLoading" |
| | | isShowSummary |
| | | :summaryMethod="getSummaries" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | |
| | | dataList.value.reduce((sum, item) => sum + Number(item.amount ?? 0), 0) |
| | | ); |
| | | |
| | | 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) |
| | |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | :tableLoading="tableLoading" |
| | | isShowSummary |
| | | :summaryMethod="getSummaries" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | |
| | | 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) => { |
| | | if (value === undefined || value === null) return "0.00"; |
| | | return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ","); |
| | |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | :tableLoading="tableLoading" |
| | | isShowSummary |
| | | :summaryMethod="getSummaries" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | |
| | | getTableData(); |
| | | }; |
| | | |
| | | 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", |
| | |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | :tableLoading="tableLoading" |
| | | isShowSummary |
| | | :summaryMethod="getSummaries" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | |
| | | { label: "éè´§åå·", prop: "returnNo", minWidth: "150" }, |
| | | { label: "ä¾åºå", prop: "supplierName", minWidth: "180" }, |
| | | { label: "å
³èå
¥åºåå·", prop: "inboundBatches", minWidth: "150" }, |
| | | { |
| | | label: "åè´§ç±»å", |
| | | prop: "shippingType", |
| | | minWidth: "110", |
| | | formatData: (val) => ({ 1: "货车", 2: "å¿«é" }[String(val)] || "--"), |
| | | }, |
| | | { |
| | | label: "å货车çå·", |
| | | prop: "truckPlateNo", |
| | | minWidth: "140", |
| | | formatData: (_val, row) => (String(row?.shippingType) === "1" ? row?.truckPlateNo || "--" : "--"), |
| | | }, |
| | | { |
| | | label: "å¿«éå
¬å¸", |
| | | prop: "expressCompany", |
| | | minWidth: "140", |
| | | formatData: (_val, row) => (String(row?.shippingType) === "2" ? row?.expressCompany || "--" : "--"), |
| | | }, |
| | | { |
| | | label: "å¿«éåå·", |
| | | prop: "expressNo", |
| | | minWidth: "150", |
| | | formatData: (_val, row) => (String(row?.shippingType) === "2" ? row?.expressNo || "--" : "--"), |
| | | }, |
| | | { label: "éè´§æ¥æ", prop: "preparedAt", minWidth: "170" }, |
| | | { |
| | | label: "鿬¾æ»é¢", |
| | |
| | | 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", |
| | |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | :tableLoading="tableLoading" |
| | | isShowSummary |
| | | :summaryMethod="getSummaries" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | |
| | | 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, ","); |
| | |
| | | v-loading="tableLoading" |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | isShowSummary |
| | | :summaryMethod="getSummaries" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | |
| | | 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, ","); |
| | |
| | | <div class="actions"> |
| | | <div></div> |
| | | <div> |
| | | <!-- <el-button type="primary" @click="add" icon="Plus">å½å
¥å票</el-button> --> |
| | | <el-button type="primary" @click="add" icon="Plus">å½å
¥å票</el-button> |
| | | <el-button type="success" @click="handleExport" icon="Download">导åº</el-button> |
| | | </div> |
| | | </div> |
| | |
| | | v-loading="tableLoading" |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | isShowSummary |
| | | :summaryMethod="getSummaries" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | |
| | | @confirm="submitForm" |
| | | @cancel="closeDialog" |
| | | > |
| | | <el-form :model="form" :rules="rules" ref="formRef" label-width="120px"> |
| | | <el-form :model="form" :rules="rules" ref="formRef" label-width="100px"> |
| | | <el-row v-if="isView" :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç¶æ"> |
| | |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="客æ·" prop="customerId"> |
| | | <el-select v-model="form.customerId" placeholder="è¯·éæ©å®¢æ·" style="width: 100%;" :disabled="isView"> |
| | | <el-select @change="checkCustomer" v-model="form.customerId" placeholder="è¯·éæ©å®¢æ·" style="width: 100%;" :disabled="isView"> |
| | | <el-option v-for="item in customerList" :key="item.id" :label="item.customerName" :value="item.id" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å
³èå¼ç¥¨ç³è¯·" prop="accountInvoiceApplicationId"> |
| | | <el-select v-model="form.accountInvoiceApplicationId" |
| | | placeholder="请å
鿩客æ·" |
| | | readonly |
| | | @change="selectionApply" |
| | | :disabled="!form.customerId || isView" |
| | | class="outbound-batch-select"> |
| | | <el-option v-for="(item,index) in applyList" :key="index" :label="item.invoiceApplicationNo" :value="item.id"></el-option> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å¼ç¥¨æ¥æ" prop="invoiceDate"> |
| | | <el-date-picker |
| | |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å票类å" prop="invoiceType"> |
| | | <el-select |
| | |
| | | placeholder="è¯·éæ©å票类å" |
| | | style="width: 100%;" |
| | | :disabled="isView" |
| | | @change="handleInvoiceTypeChange" |
| | | > |
| | | <el-option label="å¢å¼ç¨ä¸ç¨å票" value="å¢å¼ç¨ä¸ç¨å票" /> |
| | | <el-option label="å¢å¼ç¨æ®éå票" value="å¢å¼ç¨æ®éå票" /> |
| | |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç¨ç" prop="taxRate"> |
| | | <el-select |
| | |
| | | placeholder="è¯·éæ©ç¨ç" |
| | | style="width: 100%;" |
| | | :disabled="isView" |
| | | @change="calculateTax" |
| | | @change="handleTaxRateChange" |
| | | > |
| | | <el-option |
| | | v-for="dict in tax_rate" |
| | |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="8"> |
| | | <el-col :span="10"> |
| | | <el-form-item label="éé¢(ä¸å«ç¨)" prop="amount"> |
| | | <el-input-number |
| | | v-model="form.amount" |
| | |
| | | :precision="2" |
| | | style="width: 100%;" |
| | | :disabled="isView" |
| | | @change="calculateTax" |
| | | @change="handleTaxRateChange" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-col :span="7"> |
| | | <el-form-item label="ç¨é¢"> |
| | | <el-input v-model="form.taxAmount" disabled /> |
| | | <el-input v-model="form.taxAmount" style="width: 100%;" disabled /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-col :span="7"> |
| | | <el-form-item label="ä»·ç¨å计"> |
| | | <el-input v-model="form.totalAmount" disabled /> |
| | | <el-input v-model="form.totalAmount" style="width: 100%;" disabled /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | |
| | | cancelAccountSalesInvoice, |
| | | deleteAccountSalesInvoice, |
| | | } from "@/api/financialManagement/accountSalesInvoice.js"; |
| | | import {getAccountInvoiceApplicationList} from "@/api/financialManagement/invoiceApply.js"; |
| | | |
| | | const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue")); |
| | | |
| | |
| | | const fileDialogVisible = ref(false); |
| | | const currentRecordId = ref(0); |
| | | |
| | | const applySelectVisible = ref(false); |
| | | const applyList = ref([]); |
| | | |
| | | const openFileDialog = (row) => { |
| | | if (!row.accountInvoiceApplicationId) { |
| | | ElMessage.warning("æªå
³èå¼ç¥¨ç³è¯·ï¼æ æ³æ¥çéä»¶"); |
| | |
| | | totalAmount: 0, |
| | | content: "", |
| | | remark: "", |
| | | deptId: undefined, |
| | | accountInvoiceApplicationId: undefined, |
| | | storageAttachmentId: undefined, |
| | | }); |
| | | |
| | | const rules = { |
| | | invoiceNo: [{ required: true, message: "请è¾å
¥å票å·ç ", trigger: "blur" }], |
| | | accountInvoiceApplicationId: [{ required: true, message: "è¯·éæ©å¼ç¥¨ç³è¯·", trigger: "change" }], |
| | | customerId: [{ required: true, message: "è¯·éæ©å®¢æ·", trigger: "change" }], |
| | | invoiceDate: [{ required: true, message: "è¯·éæ©å¼ç¥¨æ¥æ", trigger: "change" }], |
| | | invoiceType: [{ required: true, message: "è¯·éæ©å票类å", trigger: "change" }], |
| | |
| | | amount: [{ required: true, message: "请è¾å
¥éé¢", trigger: "blur" }], |
| | | }; |
| | | |
| | | const formatMoney = (value) => { |
| | | if (value === undefined || value === null) return "0.00"; |
| | | return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ","); |
| | | // éæ©å®¢æ· |
| | | const checkCustomer = (val) =>{ |
| | | loadApplyList(val); |
| | | } |
| | | |
| | | //æ¥è¯¢å¼ç¥¨ç³è¯·å表 |
| | | const loadApplyList = (customerId) => { |
| | | if (!customerId) { |
| | | applyList.value = []; |
| | | return Promise.resolve(); |
| | | } |
| | | return getAccountInvoiceApplicationList({ customerId }) |
| | | .then(res => { |
| | | if (res.code === 200) { |
| | | const list = res.data?.records ?? res.data ?? []; |
| | | applyList.value = Array.isArray(list) ? list : []; |
| | | } else { |
| | | applyList.value = []; |
| | | } |
| | | }) |
| | | .catch(() => { |
| | | applyList.value = []; |
| | | }) |
| | | }; |
| | | |
| | | const calculateTax = () => { |
| | | const selectionApply = (val)=>{ |
| | | const applyObj = applyList.value.find(item=>item.id === val); |
| | | if(!applyObj){ |
| | | ElMessage.warning("è¯·éæ©å
³èçå¼ç¥¨ç³è¯·"); |
| | | return; |
| | | } |
| | | form.remark = applyObj.remark; |
| | | form.content = applyObj.invoiceContent; |
| | | form.invoiceDate = applyObj.applyDate; |
| | | form.invoiceType = applyObj.invoiceType; |
| | | form.deptId = applyObj.deptId; |
| | | form.taxRate = applyObj.taxRate; |
| | | form.totalAmount = applyObj.invoiceAmount |
| | | handleTaxRateChange(); |
| | | } |
| | | |
| | | /** ä»·ç¨åè®¡åæ´ï¼æç¨çåç®ä¸å«ç¨éé¢ãç¨é¢ */ |
| | | const calculateTaxFromInclusive = (inclusiveTotal) => { |
| | | const total = Number(inclusiveTotal ?? form.totalAmount ?? 0); |
| | | if (total <= 0) { |
| | | form.amount = 0; |
| | | form.taxAmount = 0; |
| | | form.totalAmount = 0; |
| | | return; |
| | | } |
| | | const rate = Number(form.taxRate) / 100; |
| | | form.totalAmount = Number(total.toFixed(2)); |
| | | form.amount = Number((form.totalAmount / (1 + rate)).toFixed(2)); |
| | | form.taxAmount = Number((form.totalAmount - form.amount).toFixed(2)); |
| | | }; |
| | | |
| | | const calculateTaxFromExclusive = () => { |
| | | form.taxAmount = Number((form.amount * form.taxRate / 100).toFixed(2)); |
| | | form.totalAmount = Number((form.amount + form.taxAmount).toFixed(2)); |
| | | }; |
| | | |
| | | const handleInvoiceTypeChange = () => { |
| | | calculateTax(); |
| | | const handleTaxRateChange = () => { |
| | | if (form.totalAmount > 0) { |
| | | calculateTaxFromInclusive(form.totalAmount); |
| | | } else { |
| | | calculateTaxFromExclusive(); |
| | | } |
| | | }; |
| | | |
| | | 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, ","); |
| | | }; |
| | | |
| | | const normalizeTableRow = (row) => ({ |
| | |
| | | |
| | | const closeDialog = () => { |
| | | dialogVisible.value = false; |
| | | applySelectVisible.value = false; |
| | | isView.value = false; |
| | | isView.value = false; |
| | | }; |
| | | |
| | |
| | | prop="amount"> |
| | | <el-input-number v-model="form.amount" |
| | | :min="0" |
| | | :max="collectionAmountInputMax" |
| | | :precision="2" |
| | | style="width: 100%;" |
| | | :disabled="isView" |
| | | placeholder="æ ¹æ®å
³èåæ®èªå¨æ±æ»ï¼å¯ä¿®æ¹" /> |
| | | :disabled="isView" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | |
| | | align="right"> |
| | | <template #default="{ row }">Â¥{{ formatMoney(row.outboundAmount) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="amountReceived" |
| | | label="å·²æ¶æ¬¾éé¢" |
| | | width="110" |
| | | align="right"> |
| | | <template #default="{ row }">Â¥{{ formatMoney(row.amountReceived) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="taxRate" |
| | | label="ç¨ç" |
| | | width="80" |
| | |
| | | const isEdit = ref(false); |
| | | const isView = ref(false); |
| | | const currentId = ref(null); |
| | | const originalReceiptAmount = ref(0); |
| | | const submitLoading = ref(false); |
| | | |
| | | const customerList = ref([]); |
| | |
| | | }, |
| | | }); |
| | | |
| | | const maxCollectionAmount = computed(() => { |
| | | const selected = form.stockOutRecordIds || []; |
| | | const editAmount = isEdit.value ? Number(originalReceiptAmount.value) || 0 : 0; |
| | | if (!selected.length) return isEdit.value ? editAmount : undefined; |
| | | const selectedValueSet = new Set(selected.map(id => String(id))); |
| | | const selectedOptions = outboundBatchOptions.value.filter( |
| | | opt => opt.amountLimitAvailable && selectedValueSet.has(String(opt.value)) |
| | | ); |
| | | if (selectedOptions.length !== selectedValueSet.size) { |
| | | return isEdit.value ? Number(editAmount.toFixed(2)) : undefined; |
| | | } |
| | | const sum = selectedOptions.reduce( |
| | | (acc, opt) => acc + (Number(opt.outboundAmount) || 0), |
| | | 0 |
| | | ); |
| | | return Number((sum + editAmount).toFixed(2)); |
| | | }); |
| | | |
| | | const collectionAmountInputMax = computed( |
| | | () => maxCollectionAmount.value ?? Number.MAX_SAFE_INTEGER |
| | | ); |
| | | |
| | | const validateCollectionAmount = (rule, value, callback) => { |
| | | if (value === undefined || value === null || value === "") { |
| | | callback(); |
| | | return; |
| | | } |
| | | const amount = Number(value); |
| | | if (Number.isNaN(amount)) { |
| | | callback(new Error("请è¾å
¥æ¶æ¬¾éé¢")); |
| | | return; |
| | | } |
| | | const max = maxCollectionAmount.value; |
| | | if (max !== undefined && amount - max > 0.000001) { |
| | | callback(new Error(`æ¶æ¬¾éé¢ä¸è½è¶
è¿${max.toFixed(2)}`)); |
| | | return; |
| | | } |
| | | callback(); |
| | | }; |
| | | |
| | | const rules = { |
| | | customerId: [{ required: true, message: "è¯·éæ©å®¢æ·", trigger: "change" }], |
| | | stockOutRecordIds: [ |
| | |
| | | receiptDate: [ |
| | | { required: true, message: "è¯·éæ©æ¶æ¬¾æ¥æ", trigger: "change" }, |
| | | ], |
| | | amount: [{ required: true, message: "请è¾å
¥æ¶æ¬¾éé¢", trigger: "blur" }], |
| | | amount: [ |
| | | { required: true, message: "请è¾å
¥æ¶æ¬¾éé¢", trigger: "blur" }, |
| | | { validator: validateCollectionAmount, trigger: ["blur", "change"] }, |
| | | ], |
| | | receiptMethod: [ |
| | | { required: true, message: "è¯·éæ©æ¶æ¬¾æ¹å¼", trigger: "change" }, |
| | | ], |
| | |
| | | return list.map((item, index) => { |
| | | if (typeof item === "string" || typeof item === "number") { |
| | | const text = String(item); |
| | | return { label: text, value: text, outboundAmount: 0 }; |
| | | return { |
| | | label: text, |
| | | value: text, |
| | | outboundAmount: 0, |
| | | amountLimitAvailable: false, |
| | | }; |
| | | } |
| | | const label = |
| | | item.outboundBatches ?? |
| | |
| | | item.label ?? |
| | | `åºåºå${index + 1}`; |
| | | const value = item.id ?? item.stockOutRecordId ?? label; |
| | | const outboundAmount = Number(item.outboundAmount) || 0; |
| | | const amountReceived = Number(item.amountReceived) || 0; |
| | | const availableAmount = outboundAmount - amountReceived; |
| | | return { |
| | | label: String(label), |
| | | value, |
| | | outboundAmount: Number(item.outboundAmount) || 0, |
| | | outboundAmount: |
| | | availableAmount > 0 ? Number(availableAmount.toFixed(2)) : 0, |
| | | amountLimitAvailable: true, |
| | | }; |
| | | }); |
| | | }; |
| | |
| | | label: String(id), |
| | | value: id, |
| | | outboundAmount: 0, |
| | | amountLimitAvailable: false, |
| | | }); |
| | | }); |
| | | }; |
| | |
| | | outboundSelectVisible.value = false; |
| | | syncCollectionAmount(); |
| | | formRef.value?.validateField("stockOutRecordIds"); |
| | | formRef.value?.validateField("amount"); |
| | | }; |
| | | |
| | | const handleOutboundDialogClosed = () => { |
| | |
| | | outboundSelectVisible.value = false; |
| | | isView.value = false; |
| | | isEdit.value = false; |
| | | originalReceiptAmount.value = 0; |
| | | }; |
| | | |
| | | const handleExport = () => { |
| | |
| | | isEdit.value = false; |
| | | isView.value = false; |
| | | dialogTitle.value = "æ°å¢æ¶æ¬¾"; |
| | | originalReceiptAmount.value = 0; |
| | | Object.assign(form, { |
| | | receiptCode: "", |
| | | customerId: "", |
| | |
| | | currentId.value = row.id; |
| | | dialogTitle.value = "ç¼è¾æ¶æ¬¾"; |
| | | fillFormFromRow(row); |
| | | originalReceiptAmount.value = Number(form.amount || 0); |
| | | dialogVisible.value = true; |
| | | loadOutboundBatches(form.customerId, true); |
| | | }; |
| | | |
| | | const view = row => { |
| | | isView.value = true; |
| | | isEdit.value = false; |
| | | dialogTitle.value = "æ¥çæ¶æ¬¾"; |
| | | originalReceiptAmount.value = 0; |
| | | fillFormFromRow(row); |
| | | dialogVisible.value = true; |
| | | }; |
| | |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | :tableLoading="tableLoading" |
| | | isShowSummary |
| | | :summaryMethod="getSummaries" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | |
| | | 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, ","); |
| | |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | :tableLoading="tableLoading" |
| | | isShowSummary |
| | | :summaryMethod="getSummaries" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | |
| | | label: "éé¢", |
| | | prop: "outboundAmount", |
| | | minWidth: "120", |
| | | align: "right", |
| | | formatData: val => |
| | | val === null || val === undefined || val === "" |
| | | ? "" |
| | |
| | | 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", |
| | |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | :tableLoading="tableLoading" |
| | | isShowSummary |
| | | :summaryMethod="getSummaries" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | |
| | | { label: "éè´§åå·", prop: "returnNo", minWidth: "150" }, |
| | | { label: "客æ·åç§°", prop: "customerName", minWidth: "180" }, |
| | | { label: "å
³èåè´§åå·", prop: "shippingNo", minWidth: "150" }, |
| | | { |
| | | label: "åè´§ç±»å", |
| | | prop: "shippingType", |
| | | minWidth: "110", |
| | | formatData: (val) => ({ 1: "货车", 2: "å¿«é" }[String(val)] || "--"), |
| | | }, |
| | | { |
| | | label: "å货车çå·", |
| | | prop: "truckPlateNo", |
| | | minWidth: "140", |
| | | formatData: (_val, row) => (String(row?.shippingType) === "1" ? row?.truckPlateNo || "--" : "--"), |
| | | }, |
| | | { |
| | | label: "å¿«éå
¬å¸", |
| | | prop: "expressCompany", |
| | | minWidth: "140", |
| | | formatData: (_val, row) => (String(row?.shippingType) === "2" ? row?.expressCompany || "--" : "--"), |
| | | }, |
| | | { |
| | | label: "å¿«éåå·", |
| | | prop: "expressNo", |
| | | minWidth: "150", |
| | | formatData: (_val, row) => (String(row?.shippingType) === "2" ? row?.expressNo || "--" : "--"), |
| | | }, |
| | | { label: "éè´§æ¥æ", prop: "makeTime", minWidth: "170" }, |
| | | { |
| | | label: "鿬¾æ»é¢", |
| | | prop: "refundAmount", |
| | | minWidth: "120", |
| | | align: "right", |
| | | formatData: (val) => |
| | | val === null || val === undefined || val === "" |
| | | ? "" |
| | |
| | | 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", |
| | |
| | | </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> |
| | |
| | | return map[key] || ""; |
| | | }; |
| | | |
| | | const onSearch = () => { |
| | | pagination.currentPage = 1; |
| | | getTableData(); |
| | | }; |
| | | |
| | | // èè°çº¦å®ï¼åé¡µåæ°ä½¿ç¨ current/sizeï¼æ¥æèå´æå为 startDate/endDate |
| | | const getTableData = async () => { |
| | | try { |
| | |
| | | getTableData(); |
| | | }; |
| | | |
| | | const changePage = ({ current, size }) => { |
| | | pagination.currentPage = current; |
| | | pagination.pageSize = size; |
| | | const changePage = ({ page, limit }) => { |
| | | pagination.currentPage = page; |
| | | pagination.pageSize = limit; |
| | | getTableData(); |
| | | }; |
| | | |
| | |
| | | <el-radio-button :value="'waiting'">å¾
å¼å§({{ orderProgressMeta.waitingCount }})</el-radio-button> |
| | | <el-radio-button :value="'inProgress'">è¿è¡ä¸({{ orderProgressMeta.inProgressCount }})</el-radio-button> |
| | | <el-radio-button :value="'completed'">已宿({{ orderProgressMeta.completedCount }})</el-radio-button> |
| | | <el-radio-button :value="'paused'">å·²æå({{ orderProgressMeta.pausedCount }})</el-radio-button> |
| | | <el-radio-button :value="'end'">å·²ç»æ({{ orderProgressMeta.endCount }})</el-radio-button> |
| | | </el-radio-group> |
| | | </div> |
| | | <el-table :data="filteredOrders" stripe> |
| | | <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" /> |
| | |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="visiblePanels.contract" class="cockpit-panel contract-panel"> |
| | |
| | | <div v-if="visiblePanels.todo" class="cockpit-panel todo-panel"> |
| | | <div class="panel-title-row"> |
| | | <div class="panel-title">å¾
åäºé¡¹</div> |
| | | <span class="panel-more">æ´å¤</span> |
| | | <span class="panel-more" @click="openTodoDialog">æ´å¤</span> |
| | | </div> |
| | | <ul class="todo-list" v-if="todoList.length > 0"> |
| | | <li v-for="item in todoList" :key="item.id" class="todo-item"> |
| | |
| | | <div v-if="visiblePanels.realtime" class="cockpit-panel realtime-panel"> |
| | | <div class="panel-title-row"> |
| | | <div class="panel-title">çäº§å®æ¶çæ¿</div> |
| | | <span class="panel-more">æ´å¤</span> |
| | | </div> |
| | | <div class="realtime-grid"> |
| | | <div class="realtime-item" v-for="item in realtimeBoard" :key="item.key"> |
| | |
| | | <el-button type="primary" @click="handleProcessDialogConfirm">确认</el-button> |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <el-dialog v-model="todoDialogVisible" title="å
¨é¨å¾
åäºé¡¹" width="900px" append-to-body destroy-on-close> |
| | | <div v-loading="todoDialogLoading" class="todo-dialog-body"> |
| | | <el-table :data="todoDialogList" stripe max-height="520"> |
| | | <el-table-column prop="approveId" label="å¾
åç¼å·" min-width="160" /> |
| | | <el-table-column prop="approveDeptName" label="é¨é¨/模æ¿" min-width="160" /> |
| | | <el-table-column prop="approveReason" label="äºç±" min-width="240" show-overflow-tooltip /> |
| | | <el-table-column prop="approveUserName" label="å起人" min-width="120" /> |
| | | <el-table-column prop="approveTime" label="æ¶é´" min-width="170" /> |
| | | </el-table> |
| | | </div> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | |
| | | waitingCount: 0, |
| | | inProgressCount: 0, |
| | | completedCount: 0, |
| | | pausedCount: 0, |
| | | endCount: 0, |
| | | }); |
| | | |
| | | const todayPlanList = ref([]); |
| | | const todayPlanTotal = ref(0); |
| | | const todoDialogVisible = ref(false); |
| | | const todoDialogLoading = ref(false); |
| | | const todoDialogList = ref([]); |
| | | |
| | | const sum = ref(0); |
| | | const yny = ref(0); |
| | |
| | | |
| | | const productionOrders = ref([]); |
| | | |
| | | const orderFilterOptions = ["all", "waiting", "inProgress", "completed", "paused"]; |
| | | const orderFilterOptions = ["all", "waiting", "inProgress", "completed", "end"]; |
| | | const orderFilterAliasMap = { |
| | | 1: "waiting", |
| | | 2: "inProgress", |
| | | 3: "completed", |
| | | 4: "paused", |
| | | 5: "end", |
| | | }; |
| | | const orderFilter = ref("all"); |
| | | const filteredOrders = computed(() => productionOrders.value); |
| | |
| | | pathPrefixes: ["/inventorymanagement"], |
| | | }, |
| | | production: { |
| | | titles: ["ç产管ç", "主ç产计å", "ç产订å", "ç产æ¥å·¥"], |
| | | pathPrefixes: ["/productionmanagement", "/productionplan"], |
| | | titles: ["ç产管æ§"], |
| | | pathPrefixes: ["/productionManagement", "/productionPlan"], |
| | | }, |
| | | quality: { |
| | | titles: ["è´¨é管ç"], |
| | |
| | | realtime: visibleModules.value.production, |
| | | quick: quickEntries.value.length > 0, |
| | | plan: visibleModules.value.production, |
| | | receipt: visibleModules.value.sales || visibleModules.value.finance, |
| | | receipt: visibleModules.value.finance, |
| | | })); |
| | | |
| | | const hasLeftPanels = computed( |
| | |
| | | 1: "å¾
å¼å§", |
| | | 2: "è¿è¡ä¸", |
| | | 3: "已宿", |
| | | 4: "å·²æå", |
| | | 5: "å·²ç»æ", |
| | | }; |
| | | return mapping[status] || "æªç¥"; |
| | | }; |
| | |
| | | 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); |
| | |
| | | waitingCount: resolveProgressCount(data.waitingCount, statusValue, "waiting", total), |
| | | inProgressCount: resolveProgressCount(data.inProgressCount, statusValue, "inProgress", total), |
| | | completedCount: resolveProgressCount(data.completedCount, statusValue, "completed", total), |
| | | pausedCount: resolveProgressCount(data.pausedCount, statusValue, "paused", total), |
| | | endCount: resolveProgressCount(data.endCount, statusValue, "end", total), |
| | | }; |
| | | productionOrders.value = (data.records || []).map(mapOrderProgressRecord); |
| | | } catch { |
| | |
| | | waitingCount: 0, |
| | | inProgressCount: 0, |
| | | completedCount: 0, |
| | | pausedCount: 0, |
| | | endCount: 0, |
| | | }; |
| | | productionOrders.value = []; |
| | | } |
| | |
| | | const refreshTodayProductionPlan = async () => { |
| | | try { |
| | | const res = await todayProductionPlan({ |
| | | limit: 4, |
| | | limit: 1000, |
| | | planDate: nowDate.value, |
| | | }); |
| | | const data = res?.data || {}; |
| | |
| | | const todoInfoS = async () => { |
| | | const res = await homeTodos(); |
| | | todoList.value = res.data || []; |
| | | }; |
| | | |
| | | const openTodoDialog = async () => { |
| | | todoDialogVisible.value = true; |
| | | todoDialogLoading.value = true; |
| | | try { |
| | | const res = await homeTodos(); |
| | | todoDialogList.value = res.data || []; |
| | | } catch { |
| | | todoDialogList.value = []; |
| | | } finally { |
| | | todoDialogLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | | const statisticsReceivable = async () => { |
| | |
| | | } |
| | | |
| | | .stats-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(4, minmax(0, 1fr)); |
| | | display: flex; |
| | | gap: 14px; |
| | | } |
| | | |
| | | .stats-grid .stat-card { |
| | | flex: 1; |
| | | } |
| | | |
| | | .stat-card { |
| | |
| | | font-size: 14px; |
| | | color: #2563eb; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .todo-dialog-body { |
| | | min-height: 220px; |
| | | } |
| | | |
| | | .plan-panel { |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .plan-list { |
| | | margin: 10px 0 0; |
| | | padding: 0 4px 0 0; |
| | | list-style: none; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | max-height: 300px; |
| | | overflow-y: auto; |
| | | } |
| | | |
| | | .plan-list::-webkit-scrollbar { |
| | | width: 6px; |
| | | } |
| | | |
| | | .plan-list::-webkit-scrollbar-thumb { |
| | | border-radius: 999px; |
| | | background: rgba(37, 99, 235, 0.28); |
| | | } |
| | | |
| | | .todo-list { |
| | |
| | | |
| | | .order-panel { |
| | | min-height: 0; |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .order-table-wrap { |
| | | flex: 1; |
| | | min-height: 0; |
| | | } |
| | | |
| | | .quick-panel { |
| | |
| | | transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; |
| | | } |
| | | |
| | | .plan-panel { |
| | | min-height: 236px; |
| | | } |
| | | |
| | | .quality-panel { |
| | | min-height: 0; |
| | | } |
| | | |
| | | .receipt-panel { |
| | | min-height: 340px; |
| | | } |
| | | |
| | | .plan-list { |
| | | margin: 10px 0 0; |
| | | padding: 0; |
| | | list-style: none; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .plan-item { |
| | |
| | | .main-grid { |
| | | grid-template-columns: minmax(0, 1fr) 340px; |
| | | } |
| | | |
| | | .stats-grid { |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 1200px) { |
| | |
| | | |
| | | .meta-extra { |
| | | justify-content: flex-start; |
| | | } |
| | | |
| | | .stats-grid { |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | } |
| | | |
| | | .right-column { |
| | |
| | | <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 |
| | |
| | | getStockOutPage, |
| | | delPendingStockOut, |
| | | batchApproveStockOutRecords, |
| | | batchUnapproveStockOutRecords, |
| | | } from "@/api/inventoryManagement/stockOut.js"; |
| | | import { |
| | | findAllQualifiedStockOutRecordTypeOptions, |
| | |
| | | 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 = () => { |
| | |
| | | <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="æ¹å·" |
| | |
| | | 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="夿³¨" |
| | |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | <div> |
| | | <div style="display: flex; gap: 10px; flex-wrap: nowrap;"> |
| | | <el-button type="primary" |
| | | @click="isShowNewModal = true">æ°å¢åºå</el-button> |
| | | <el-button type="info" |
| | |
| | | <div class="login-page"> |
| | | <main class="page"> |
| | | <section class="factory"> |
| | | <div class="brand hero-brand"> |
| | | <div class="logo hero-logo"> |
| | | <img |
| | | :src="brandLogoUrl" |
| | | :alt="`${companyName} logo`" |
| | | class="logo-image hero-logo-image" |
| | | @error="handleLogoError" |
| | | /> |
| | | </div> |
| | | </div> |
| | | <!-- <div class="brand hero-brand">--> |
| | | <!-- <div class="logo hero-logo">--> |
| | | <!-- <img--> |
| | | <!-- :src="brandLogoUrl"--> |
| | | <!-- :alt="`${companyName} logo`"--> |
| | | <!-- class="logo-image hero-logo-image"--> |
| | | <!-- @error="handleLogoError"--> |
| | | <!-- />--> |
| | | <!-- </div>--> |
| | | <!-- </div>--> |
| | | |
| | | <div class="hero"> |
| | | <div class="chip">æ°åå·¥å · æºè½æäº§ · 设å¤äºè · è´¨é追溯</div> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| | |
| | | {{ 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"> |
| | |
| | | <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.shippingDate || "--" }}</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> |
| | |
| | | filterable |
| | | > |
| | | <el-option |
| | | v-for="o in getOptions(field)" |
| | | v-for="o in getOptions(field, { moduleKey: props.moduleKey })" |
| | | :key="String(o.value)" |
| | | :label="o.label" |
| | | :value="o.value" |
| | |
| | | fields: { type: Array, default: () => [] }, |
| | | formPayload: { type: Object, default: () => ({}) }, |
| | | readonly: { type: Boolean, default: false }, |
| | | moduleKey: { type: String, default: "" }, |
| | | }); |
| | | |
| | | const { loading: optionSourceLoading, ensureForFields, getOptions, getDisplayLabel } = |
| | | useSelectOptionSources(); |
| | | |
| | | async function loadOptionCaches() { |
| | | await ensureForFields(props.fields); |
| | | await ensureForFields(props.fields, { moduleKey: props.moduleKey }); |
| | | } |
| | | |
| | | onMounted(() => { |
| | |
| | | function displayValue(field) { |
| | | const val = props.formPayload?.[field.key]; |
| | | if (field.type === "select" && field.optionSource && field.optionSource !== "static") { |
| | | return getDisplayLabel(field, val); |
| | | return getDisplayLabel(field, val, { moduleKey: props.moduleKey }); |
| | | } |
| | | return formatFieldDisplayValue(field, val); |
| | | } |
| | |
| | | |
| | | const tableColumn = ref([ |
| | | // { label: "ç³è¯·äººç¼å·", prop: "applicantNo", width: 110 }, |
| | | { label: "ç³è¯·äººåç§°", prop: "applicantName", minWidth: 100 }, |
| | | { label: "å起人", prop: "applicantName", minWidth: 100 }, |
| | | { label: "模æ¿ç±»å", prop: "businessName", minWidth: 120 }, |
| | | { |
| | | label: "审æ¹ç±»å", |
| | |
| | | prop: "unread", |
| | | width: 90, |
| | | align: "center", |
| | | dataType: "tag", |
| | | formatData: (v) => (v ? "æ¯" : "å¦"), |
| | | formatType: (v) => (v ? "success" : "danger"), |
| | | }, |
| | | { |
| | | label: "审æ¹ç¶æ", |
| | |
| | | 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), |
| | | // }, |
| | | ], |
| | | }, |
| | | ]); |
| | |
| | | else if (row.businessType === 5) { |
| | | const purchaseContractNumber = row?.purchaseContractNumber; |
| | | if (purchaseContractNumber) { |
| | | const res = await getPurchaseByCode({ purchaseContractNumber }); |
| | | const res = await getPurchaseByCode({ |
| | | purchaseContractNumber, |
| | | approvalInstanceId: row?.id, |
| | | }); |
| | | detailData.value = res || {}; |
| | | } |
| | | } |
| | |
| | | else if (row.businessType === 5) { |
| | | const purchaseContractNumber = row?.purchaseContractNumber; |
| | | if (purchaseContractNumber) { |
| | | const res = await getPurchaseByCode({ purchaseContractNumber }); |
| | | const res = await getPurchaseByCode({ |
| | | purchaseContractNumber, |
| | | approvalInstanceId: row?.id, |
| | | }); |
| | | detailData.value = res || {}; |
| | | } |
| | | } |
| | |
| | | v-model:attachments="form.storageBlobDTOs" |
| | | :template-attachments="form.templateAttachments" |
| | | :user-options="userOptions" |
| | | :module-key="moduleKey" |
| | | :show-template-name="!isEdit" |
| | | :allow-change-template="false" |
| | | :flow-attachments-only="flowAttachmentsOnly" |
| | |
| | | fields: { type: Array, default: () => [] }, |
| | | activeTemplate: { type: Object, default: null }, |
| | | userOptions: { type: Array, default: () => [] }, |
| | | moduleKey: { type: String, default: "" }, |
| | | isEdit: { type: Boolean, default: false }, |
| | | saving: { type: Boolean, default: false }, |
| | | formRef: { type: Object, default: null }, |
| | |
| | | ç¨æ³ï¼ |
| | | <ApprovalTemplateBindDialog |
| | | v-model:visible="visible" |
| | | module-key="regular" |
| | | :module-key="APPROVAL_MODULE_KEYS.REGULAR" |
| | | @confirm="onTemplateBound" |
| | | /> |
| | | --> |
| | |
| | | v-if="!hideFormFields && !flowAttachmentsOnly && !flowOnly" |
| | | :fields="fields" |
| | | :form-payload="formPayload" |
| | | :module-key="moduleKey" |
| | | /> |
| | | |
| | | <el-form-item label="å®¡æ¹æµç¨" required> |
| | |
| | | uploadLimit: { type: Number, default: 10 }, |
| | | /** 为 true æ¶å¯ç¼è¾æ¨¡æ¿é¢ç½®ç审æ¹äººï¼ä»
å®¡æ¹æ¨¡æ¿ç®¡ç页使ç¨ï¼ */ |
| | | flowEditable: { type: Boolean, default: false }, |
| | | moduleKey: { type: String, default: "" }, |
| | | }); |
| | | |
| | | const emit = defineEmits(["update:flowNodes", "update:attachments", "change-template"]); |
| | |
| | | disabled: (row) => !canEditBusinessInstanceRow(row), |
| | | clickFun: (row) => openEdit(row), |
| | | }, |
| | | { |
| | | name: "å é¤", |
| | | type: "danger", |
| | | clickFun: (row) => removeInstance(row), |
| | | }, |
| | | // { |
| | | // name: "å é¤", |
| | | // type: "danger", |
| | | // clickFun: (row) => removeInstance(row), |
| | | // }, |
| | | ...extraOperations, |
| | | ]; |
| | | } |
| | |
| | | import { deptTreeSelect, userListNoPageByTenantId } from "@/api/system/user.js"; |
| | | import { staffOnJobListPage } from "@/api/personnelManagement/staffOnJob.js"; |
| | | |
| | | /** 䏿éé¡¹æ¥æºï¼åå
¥ formConfigï¼æäº¤é¡µææ¥æºæåæ°æ®ï¼ */ |
| | | export const SELECT_OPTION_SOURCE = { |
| | | STATIC: "static", |
| | | USER: "user", |
| | | DEPT: "dept", |
| | | EMPLOYEE: "employee", |
| | | }; |
| | | |
| | | export const SELECT_OPTION_SOURCE_OPTIONS = [ |
| | | { value: SELECT_OPTION_SOURCE.STATIC, label: "æå¨é
ç½®", desc: "卿¨¡æ¿ä¸èªå®ä¹éé¡¹ææ¬ä¸å¼" }, |
| | | { value: SELECT_OPTION_SOURCE.USER, label: "人åå表", desc: "ä»ç³»ç»ç¨æ·ä¸éæ©ï¼å¼ä¸ºç¨æ· ID" }, |
| | | { value: SELECT_OPTION_SOURCE.DEPT, label: "é¨é¨å表", desc: "ä»ç»ç»æ¶æä¸éæ©ï¼å¼ä¸ºé¨é¨ ID" }, |
| | | { value: SELECT_OPTION_SOURCE.EMPLOYEE, label: "åå·¥å表", desc: "ä»å¨èåå·¥ä¸éæ©ï¼å¼ä¸ºåå·¥ ID" }, |
| | | ]; |
| | | |
| | | const EMPLOYEE_APPLICANT_MODULE_KEYS = new Set(["regular"]); |
| | | |
| | | export function selectOptionSourceLabel(source) { |
| | | return SELECT_OPTION_SOURCE_OPTIONS.find((x) => x.value === source)?.label || "â"; |
| | | } |
| | | |
| | | export function isDynamicOptionSource(source) { |
| | | return source === SELECT_OPTION_SOURCE.USER || source === SELECT_OPTION_SOURCE.DEPT; |
| | | return ( |
| | | source === SELECT_OPTION_SOURCE.USER || |
| | | source === SELECT_OPTION_SOURCE.DEPT || |
| | | source === SELECT_OPTION_SOURCE.EMPLOYEE |
| | | ); |
| | | } |
| | | |
| | | function unwrapArray(payload) { |
| | | if (Array.isArray(payload)) return payload; |
| | | if (Array.isArray(payload?.records)) return payload.records; |
| | | if (payload?.data && Array.isArray(payload.data)) return payload.data; |
| | | if (Array.isArray(payload?.data?.records)) return payload.data.records; |
| | | if (payload?.rows && Array.isArray(payload.rows)) return payload.rows; |
| | | if (Array.isArray(payload?.data?.rows)) return payload.data.rows; |
| | | return []; |
| | | } |
| | | |
| | |
| | | const value = u.userId ?? u.id; |
| | | return { |
| | | label: u.nickName || u.userName || `ç¨æ·${value}`, |
| | | value, |
| | | }; |
| | | } |
| | | |
| | | /** å¨èåå·¥ â 䏿 option */ |
| | | export function mapEmployeeToSelectOption(u) { |
| | | const value = u.id ?? u.staffOnJobId ?? u.staffId; |
| | | return { |
| | | label: u.staffName || u.name || u.userName || `åå·¥${value}`, |
| | | value, |
| | | }; |
| | | } |
| | |
| | | } |
| | | |
| | | /** æå段é
置解æä¸æ optionsï¼éä¼ å
¥å·²å è½½çç¼åï¼ */ |
| | | export function resolveFieldSelectOptions(field, caches = {}) { |
| | | function isApplicantField(field) { |
| | | const key = String(field?.key || "").toLowerCase(); |
| | | const label = String(field?.label || ""); |
| | | return ( |
| | | key === "applicant" || |
| | | key === "applicantname" || |
| | | label.includes("ç³è¯·äºº") |
| | | ); |
| | | } |
| | | |
| | | export function resolveFieldSelectOptions(field, caches = {}, context = {}) { |
| | | const source = field?.optionSource || SELECT_OPTION_SOURCE.STATIC; |
| | | if ( |
| | | source === SELECT_OPTION_SOURCE.USER && |
| | | EMPLOYEE_APPLICANT_MODULE_KEYS.has(context.moduleKey) && |
| | | isApplicantField(field) |
| | | ) { |
| | | return (caches.employees || []).map(mapEmployeeToSelectOption); |
| | | } |
| | | if (source === SELECT_OPTION_SOURCE.USER) { |
| | | return (caches.users || []).map(mapUserToSelectOption); |
| | | } |
| | | if (source === SELECT_OPTION_SOURCE.DEPT) { |
| | | return caches.deptOptions || []; |
| | | } |
| | | if (source === SELECT_OPTION_SOURCE.EMPLOYEE) { |
| | | return (caches.employees || []).map(mapEmployeeToSelectOption); |
| | | } |
| | | return (field?.options || []).filter((o) => o.value !== "" && o.value != null); |
| | | } |
| | | |
| | | /** æ ¹æ®å·²è§£æç options 忥å±ç¤ºææ¬ */ |
| | | export function resolveSelectDisplayLabel(field, val, caches = {}) { |
| | | export function resolveSelectDisplayLabel(field, val, caches = {}, context = {}) { |
| | | if (val == null || val === "") return "â"; |
| | | const options = resolveFieldSelectOptions(field, caches); |
| | | const options = resolveFieldSelectOptions(field, caches, context); |
| | | const hit = options.find((o) => String(o.value) === String(val)); |
| | | return hit?.label || String(val); |
| | | } |
| | |
| | | export async function fetchSelectOptionCaches(sources = []) { |
| | | const needUser = sources.includes(SELECT_OPTION_SOURCE.USER); |
| | | const needDept = sources.includes(SELECT_OPTION_SOURCE.DEPT); |
| | | const caches = { users: [], deptOptions: [] }; |
| | | const needEmployee = sources.includes(SELECT_OPTION_SOURCE.EMPLOYEE); |
| | | const caches = { users: [], deptOptions: [], employees: [] }; |
| | | |
| | | if (!needUser && !needDept) return caches; |
| | | if (!needUser && !needDept && !needEmployee) return caches; |
| | | |
| | | const tasks = []; |
| | | if (needUser) { |
| | |
| | | }) |
| | | ); |
| | | } |
| | | if (needEmployee) { |
| | | tasks.push( |
| | | staffOnJobListPage({ current: -1, size: -1, staffState: 1 }) |
| | | .then((res) => { |
| | | caches.employees = unwrapArray(res); |
| | | }) |
| | | .catch(() => { |
| | | caches.employees = []; |
| | | }) |
| | | ); |
| | | } |
| | | |
| | | await Promise.all(tasks); |
| | | return caches; |
| | | } |
| | | |
| | | /** ä»å段å表æ¶ééè¦é¢å è½½çå¨ææ¥æº */ |
| | | export function collectOptionSourcesFromFields(fields) { |
| | | export function collectOptionSourcesFromFields(fields, context = {}) { |
| | | const set = new Set(); |
| | | (fields || []).forEach((f) => { |
| | | if (f?.type === "select" && isDynamicOptionSource(f.optionSource)) { |
| | | if ( |
| | | EMPLOYEE_APPLICANT_MODULE_KEYS.has(context.moduleKey) && |
| | | f.optionSource === SELECT_OPTION_SOURCE.USER && |
| | | isApplicantField(f) |
| | | ) { |
| | | set.add(SELECT_OPTION_SOURCE.EMPLOYEE); |
| | | } else { |
| | | set.add(f.optionSource); |
| | | } |
| | | } |
| | | }); |
| | | return [...set]; |
| | | } |
| | |
| | | deptOptions: [], |
| | | }); |
| | | |
| | | async function ensureForFields(fields) { |
| | | const sources = collectOptionSourcesFromFields(fields); |
| | | async function ensureForFields(fields, context = {}) { |
| | | const sources = collectOptionSourcesFromFields(fields, context); |
| | | if (!sources.length) return; |
| | | loading.value = true; |
| | | try { |
| | | const next = await fetchSelectOptionCaches(sources); |
| | | caches.users = next.users; |
| | | caches.deptOptions = next.deptOptions; |
| | | caches.employees = next.employees || []; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | function getOptions(field) { |
| | | return resolveFieldSelectOptions(field, caches); |
| | | function getOptions(field, context = {}) { |
| | | return resolveFieldSelectOptions(field, caches, context); |
| | | } |
| | | |
| | | function getDisplayLabel(field, val) { |
| | | return resolveSelectDisplayLabel(field, val, caches); |
| | | function getDisplayLabel(field, val, context = {}) { |
| | | return resolveSelectDisplayLabel(field, val, caches, context); |
| | | } |
| | | |
| | | return { |
| | |
| | | <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> |
| | |
| | | :fields="submitFormFields" |
| | | :active-template="activeTemplate" |
| | | :user-options="flowUserOptions" |
| | | :module-key="APPROVAL_MODULE_KEYS.REGULAR" |
| | | :is-edit="isSubmitEdit" |
| | | :saving="submitSaving" |
| | | :form-ref="submitFormRef" |
| | |
| | | <template> |
| | | <div> |
| | | <el-dialog |
| | | v-model="dialogFormVisible" |
| | | <el-dialog v-model="dialogFormVisible" |
| | | :title="operationType === 'add' ? 'æ°å¢ç¦»è' : 'ç¼è¾ç¦»è'" |
| | | width="70%" |
| | | @close="closeDia" |
| | | > |
| | | @close="closeDia"> |
| | | <!-- å工信æ¯å±ç¤ºåºå --> |
| | | <div class="info-section"> |
| | | <div class="info-title">å工信æ¯</div> |
| | | <el-form :model="form" label-width="200px" label-position="left" :rules="rules" ref="formRef" style="margin-top: 20px"> |
| | | <el-form :model="form" |
| | | label-width="200px" |
| | | label-position="left" |
| | | :rules="rules" |
| | | ref="formRef" |
| | | style="margin-top: 20px"> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å§åï¼" prop="staffOnJobId"> |
| | | <el-form-item label="å§åï¼" |
| | | prop="staffOnJobId"> |
| | | <el-select v-model="form.staffOnJobId" |
| | | placeholder="è¯·éæ©äººå" |
| | | style="width: 100%" |
| | | :disabled="operationType === 'edit'" |
| | | @change="handleSelect"> |
| | | <el-option |
| | | v-for="item in personList" |
| | | <el-option v-for="item in personList" |
| | | :key="item.id" |
| | | :label="item.staffName" |
| | | :value="item.id" |
| | | /> |
| | | :value="item.id" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | |
| | | {{ currentStaffRecord.sex || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ·ç±ä½åï¼"> |
| | | {{ currentStaffRecord.nativePlace || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å²ä½ï¼"> |
| | | {{ currentStaffRecord.postName || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç°ä½åï¼"> |
| | | {{ currentStaffRecord.adress || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç¦»èæ¥æï¼" prop="leaveDate"> |
| | | <el-date-picker |
| | | v-model="form.leaveDate" |
| | | <el-form-item label="ç¦»èæ¥æï¼" |
| | | prop="leaveDate"> |
| | | <el-date-picker v-model="form.leaveDate" |
| | | type="date" |
| | | :disabled="operationType === 'edit'" |
| | | :disabled-date="disabledFutureDate" |
| | | placeholder="è¯·éæ©ç¦»èæ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | style="width: 100%" |
| | | /> |
| | | style="width: 100%" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="离èåå ï¼" prop="reason"> |
| | | <el-select v-model="form.reason" placeholder="è¯·éæ©ç¦»èåå " style="width: 100%" @change="handleSelectDimissionReason"> |
| | | <el-option |
| | | v-for="(item, index) in dimissionReasonOptions" |
| | | <el-form-item label="离èåå ï¼" |
| | | prop="reason"> |
| | | <el-select v-model="form.reason" |
| | | placeholder="è¯·éæ©ç¦»èåå " |
| | | style="width: 100%" |
| | | @change="handleSelectDimissionReason"> |
| | | <el-option v-for="(item, index) in dimissionReasonOptions" |
| | | :key="index" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | :value="item.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="夿³¨ï¼" prop="remark" v-if="form.reason === 'other'"> |
| | | <el-input |
| | | v-model="form.remark" |
| | | <el-form-item label="夿³¨ï¼" |
| | | prop="remark" |
| | | v-if="form.reason === 'other'"> |
| | | <el-input v-model="form.remark" |
| | | type="textarea" |
| | | :rows="3" |
| | | placeholder="夿³¨" |
| | | maxlength="500" |
| | | show-word-limit |
| | | /> |
| | | show-word-limit /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | |
| | | <!-- <el-row :gutter="30">--> |
| | | <!-- <el-col :span="12">--> |
| | | <!-- <div class="info-item">--> |
| | |
| | | <!-- </el-col>--> |
| | | <!-- </el-row>--> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" @click="submitForm">确认</el-button> |
| | | <el-button type="primary" |
| | | @click="submitForm">确认</el-button> |
| | | <el-button @click="closeDia">åæ¶</el-button> |
| | | </div> |
| | | </template> |
| | |
| | | <script setup> |
| | | import {ref, reactive, toRefs, getCurrentInstance} from "vue"; |
| | | import {staffOnJobListPage} from "@/api/personnelManagement/staffOnJob.js"; |
| | | import {createStaffLeave, updateStaffLeave} from "@/api/personnelManagement/staffLeave.js"; |
| | | const { proxy } = getCurrentInstance() |
| | | const emit = defineEmits(['close']) |
| | | import { |
| | | createStaffLeave, |
| | | updateStaffLeave, |
| | | } from "@/api/personnelManagement/staffLeave.js"; |
| | | const { proxy } = getCurrentInstance(); |
| | | const emit = defineEmits(["close"]); |
| | | |
| | | const dialogFormVisible = ref(false); |
| | | const operationType = ref('') |
| | | const operationType = ref(""); |
| | | const getTodayDate = () => { |
| | | const now = new Date(); |
| | | const year = now.getFullYear(); |
| | | const month = `${now.getMonth() + 1}`.padStart(2, '0'); |
| | | const day = `${now.getDate()}`.padStart(2, '0'); |
| | | const month = `${now.getMonth() + 1}`.padStart(2, "0"); |
| | | const day = `${now.getDate()}`.padStart(2, "0"); |
| | | return `${year}-${month}-${day}`; |
| | | }; |
| | | |
| | | const disabledFutureDate = (time) => { |
| | | const disabledFutureDate = time => { |
| | | const todayEnd = new Date(); |
| | | todayEnd.setHours(23, 59, 59, 999); |
| | | return time.getTime() > todayEnd.getTime(); |
| | |
| | | }, |
| | | rules: { |
| | | staffName: [{ required: true, message: "è¯·éæ©äººå" }], |
| | | leaveDate: [{ required: true, message: "è¯·éæ©ç¦»èæ¥æ", trigger: "change" }], |
| | | leaveDate: [ |
| | | { required: true, message: "è¯·éæ©ç¦»èæ¥æ", trigger: "change" }, |
| | | ], |
| | | reason: [{ required: true, message: "è¯·éæ©ç¦»èåå "}], |
| | | }, |
| | | dimissionReasonOptions: [ |
| | | {label: 'èªèµå¾
é', value: 'salary'}, |
| | | {label: 'èä¸åå±', value: 'career_development'}, |
| | | {label: 'å·¥ä½ç¯å¢', value: 'work_environment'}, |
| | | {label: '个人åå ', value: 'personal_reason'}, |
| | | {label: 'å
¶ä»', value: 'other'}, |
| | | { label: "èªèµå¾
é", value: "salary" }, |
| | | { label: "èä¸åå±", value: "career_development" }, |
| | | { label: "å·¥ä½ç¯å¢", value: "work_environment" }, |
| | | { label: "个人åå ", value: "personal_reason" }, |
| | | { label: "å
¶ä»", value: "other" }, |
| | | ], |
| | | currentStaffRecord: {}, |
| | | }); |
| | | const { form, rules, dimissionReasonOptions, currentStaffRecord } = toRefs(data); |
| | | const { form, rules, dimissionReasonOptions, currentStaffRecord } = |
| | | toRefs(data); |
| | | |
| | | // æå¼å¼¹æ¡ |
| | | const openDialog = (type, row) => { |
| | | operationType.value = type; |
| | | dialogFormVisible.value = true; |
| | | if (operationType.value === 'edit') { |
| | | currentStaffRecord.value = row |
| | | form.value.staffOnJobId = row.staffOnJobId |
| | | form.value.leaveDate = row.leaveDate |
| | | form.value.reason = row.reason |
| | | form.value.remark = row.remark |
| | | if (operationType.value === "edit") { |
| | | currentStaffRecord.value = row; |
| | | form.value.staffOnJobId = row.staffOnJobId; |
| | | form.value.leaveDate = row.leaveDate; |
| | | form.value.reason = row.reason; |
| | | form.value.remark = row.remark; |
| | | personList.value = [ |
| | | { |
| | | staffName: row.staffName, |
| | | id: row.staffOnJobId, |
| | | } |
| | | ] |
| | | }, |
| | | ]; |
| | | } else { |
| | | form.value.leaveDate = getTodayDate() |
| | | getList() |
| | | form.value.leaveDate = getTodayDate(); |
| | | getList(); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | const handleSelectDimissionReason = (val) => { |
| | | if (val === 'other') { |
| | | form.value.remark = '' |
| | | const handleSelectDimissionReason = val => { |
| | | if (val === "other") { |
| | | form.value.remark = ""; |
| | | } |
| | | } |
| | | }; |
| | | // æäº¤äº§å表å |
| | | const submitForm = () => { |
| | | form.value.staffState = 0 |
| | | if (form.value.reason !== 'other') { |
| | | form.value.remark = '' |
| | | form.value.staffState = 0; |
| | | if (form.value.reason !== "other") { |
| | | form.value.remark = ""; |
| | | } |
| | | proxy.$refs["formRef"].validate(valid => { |
| | | if (valid) { |
| | |
| | | createStaffLeave(form.value).then(res => { |
| | | proxy.$modal.msgSuccess("æäº¤æå"); |
| | | closeDia(); |
| | | }) |
| | | }); |
| | | } else { |
| | | updateStaffLeave(currentStaffRecord.value.id, form.value).then(res => { |
| | | proxy.$modal.msgSuccess("æäº¤æå"); |
| | | closeDia(); |
| | | }) |
| | | }); |
| | | } |
| | | } |
| | | }) |
| | | |
| | | } |
| | | }); |
| | | }; |
| | | // å
³éå¼¹æ¡ |
| | | const closeDia = () => { |
| | | // 表å已注éï¼æå¨éç½®è¡¨åæ°æ® |
| | |
| | | remark: "", |
| | | }; |
| | | dialogFormVisible.value = false; |
| | | emit('close') |
| | | emit("close"); |
| | | }; |
| | | |
| | | const personList = ref([]); |
| | |
| | | staffOnJobListPage({ |
| | | current: -1, |
| | | size: -1, |
| | | staffState: 1 |
| | | staffState: 1, |
| | | }).then(res => { |
| | | personList.value = res.data.records || [] |
| | | }) |
| | | personList.value = res.data.records || []; |
| | | }); |
| | | }; |
| | | |
| | | const handleSelect = (val) => { |
| | | let obj = personList.value.find(item => item.id === val) |
| | | currentStaffRecord.value = {} |
| | | const handleSelect = val => { |
| | | let obj = personList.value.find(item => item.id === val); |
| | | currentStaffRecord.value = {}; |
| | | if (obj) { |
| | | // ä¿çç¦»èæ¥æå离èåå ï¼åªæ´æ°åå·¥ä¿¡æ¯ |
| | | currentStaffRecord.value = obj |
| | | currentStaffRecord.value = obj; |
| | | } |
| | | } |
| | | }; |
| | | defineExpose({ |
| | | openDialog, |
| | | }); |
| | |
| | | <div class="search_form mb20"> |
| | | <div> |
| | | <span class="search_title">å§åï¼</span> |
| | | <el-input |
| | | v-model="searchForm.staffName" |
| | | <el-input v-model="searchForm.staffName" |
| | | style="width: 240px" |
| | | placeholder="请è¾å
¥å§åæç´¢" |
| | | @change="handleQuery" |
| | | clearable |
| | | :prefix-icon="Search" |
| | | /> |
| | | <el-button type="primary" @click="handleQuery" style="margin-left: 10px" |
| | | >æç´¢</el-button |
| | | > |
| | | :prefix-icon="Search" /> |
| | | <el-button type="primary" |
| | | @click="handleQuery" |
| | | style="margin-left: 10px">æç´¢</el-button> |
| | | </div> |
| | | <div> |
| | | <el-button type="primary" @click="openForm('add')">æ°å¢ç¦»è</el-button> |
| | | <el-button type="primary" |
| | | @click="openForm('add')">æ°å¢ç¦»è</el-button> |
| | | <el-button @click="handleOut">导åº</el-button> |
| | | </div> |
| | | </div> |
| | | <div class="table_list"> |
| | | <PIMTable |
| | | rowKey="id" |
| | | <PIMTable rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :page="page" |
| | |
| | | @selection-change="handleSelectionChange" |
| | | :tableLoading="tableLoading" |
| | | @pagination="pagination" |
| | | :total="page.total" |
| | | ></PIMTable> |
| | | :total="page.total"></PIMTable> |
| | | </div> |
| | | <form-dia ref="formDia" @close="handleQuery"></form-dia> |
| | | <form-dia ref="formDia" |
| | | @close="handleQuery"></form-dia> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | label: "ç¶æ", |
| | | prop: "staffState", |
| | | dataType: "tag", |
| | | formatData: (params) => { |
| | | formatData: params => { |
| | | if (params == 0) { |
| | | return "离è"; |
| | | } else if (params == 1) { |
| | |
| | | return null; |
| | | } |
| | | }, |
| | | formatType: (params) => { |
| | | formatType: params => { |
| | | if (params == 0) { |
| | | return "danger"; |
| | | } else if (params == 1) { |
| | |
| | | prop: "sex", |
| | | }, |
| | | { |
| | | label: "æ·ç±ä½å", |
| | | prop: "nativePlace", |
| | | }, |
| | | { |
| | | label: "é¨é¨", |
| | | prop: "deptName", |
| | | }, |
| | |
| | | prop: "postName", |
| | | }, |
| | | { |
| | | label: "ç°ä½å", |
| | | prop: "adress", |
| | | width:200 |
| | | }, |
| | | { |
| | | label: "第ä¸å¦å", |
| | | prop: "firstStudy", |
| | | }, |
| | | { |
| | | label: "ä¸ä¸", |
| | | prop: "profession", |
| | | width:100 |
| | | width: 100, |
| | | }, |
| | | { |
| | | label: "å¹´é¾", |
| | |
| | | { |
| | | label: "èç³»çµè¯", |
| | | prop: "phone", |
| | | width:150 |
| | | width: 150, |
| | | }, |
| | | { |
| | | label: "ç´§æ¥è系人", |
| | | prop: "emergencyContact", |
| | | width: 120 |
| | | width: 120, |
| | | }, |
| | | { |
| | | label: "ç´§æ¥è系人çµè¯", |
| | | prop: "emergencyContactPhone", |
| | | width:150 |
| | | width: 150, |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: 'right', |
| | | fixed: "right", |
| | | operation: [ |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | clickFun: row => { |
| | | openForm("edit", row); |
| | | }, |
| | | }, |
| | |
| | | size: 100, |
| | | total: 0, |
| | | }); |
| | | const formDia = ref() |
| | | const { proxy } = getCurrentInstance() |
| | | |
| | | const formDia = ref(); |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | // æ¥è¯¢å表 |
| | | /** æç´¢æé®æä½ */ |
| | |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | | const pagination = (obj) => { |
| | | const pagination = obj => { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | getList(); |
| | | }; |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | findStaffLeaveListPage({...page, ...searchForm.value}).then(res => { |
| | | findStaffLeaveListPage({ ...page, ...searchForm.value }) |
| | | .then(res => { |
| | | tableLoading.value = false; |
| | | tableData.value = res.data.records |
| | | tableData.value = res.data.records; |
| | | page.total = res.data.total; |
| | | }).catch(err => { |
| | | tableLoading.value = false; |
| | | }) |
| | | .catch(err => { |
| | | tableLoading.value = false; |
| | | }); |
| | | }; |
| | | // è¡¨æ ¼éæ©æ°æ® |
| | | const handleSelectionChange = (selection) => { |
| | | const handleSelectionChange = selection => { |
| | | selectedRows.value = selection; |
| | | }; |
| | | |
| | | // æå¼å¼¹æ¡ |
| | | const openForm = (type, row) => { |
| | | nextTick(() => { |
| | | formDia.value?.openDialog(type, row) |
| | | }) |
| | | formDia.value?.openDialog(type, row); |
| | | }); |
| | | }; |
| | | |
| | | // å¯¼åº |
| | |
| | | <div class="search_form mb20"> |
| | | <div> |
| | | <span class="search_title">å§åï¼</span> |
| | | <el-input v-model="searchForm.staffName" style="width: 240px" placeholder="请è¾å
¥å§åæç´¢" @change="handleQuery" |
| | | clearable :prefix-icon="Search" /> |
| | | <span style="margin-left: 10px" class="search_title">ååç»ææ¥æï¼</span> |
| | | <el-date-picker v-model="searchForm.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange" |
| | | placeholder="è¯·éæ©" clearable @change="changeDaterange" /> |
| | | <el-button type="primary" @click="handleQuery" style="margin-left: 10px">æç´¢</el-button> |
| | | <el-input v-model="searchForm.staffName" |
| | | style="width: 240px" |
| | | placeholder="请è¾å
¥å§åæç´¢" |
| | | @change="handleQuery" |
| | | clearable |
| | | :prefix-icon="Search" /> |
| | | <span style="margin-left: 10px" |
| | | class="search_title">ååç»ææ¥æï¼</span> |
| | | <el-date-picker v-model="searchForm.entryDate" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | type="daterange" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | @change="changeDaterange" /> |
| | | <el-button type="primary" |
| | | @click="handleQuery" |
| | | style="margin-left: 10px">æç´¢</el-button> |
| | | </div> |
| | | <div> |
| | | <el-button @click="handleOut">导åº</el-button> |
| | | </div> |
| | | </div> |
| | | <div class="table_list"> |
| | | <PIMTable rowKey="id" :column="tableColumn" :tableData="tableData" :page="page" :isSelection="true" |
| | | @selection-change="handleSelectionChange" :tableLoading="tableLoading" @pagination="pagination" |
| | | <PIMTable rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :page="page" |
| | | :isSelection="true" |
| | | @selection-change="handleSelectionChange" |
| | | :tableLoading="tableLoading" |
| | | @pagination="pagination" |
| | | :total="page.total"></PIMTable> |
| | | </div> |
| | | <form-dia ref="formDia" @close="handleQuery"></form-dia> |
| | | |
| | | <form-dia ref="formDia" |
| | | @close="handleQuery"></form-dia> |
| | | <!-- åå导å
¥å¯¹è¯æ¡ --> |
| | | <el-dialog |
| | | :title="upload.title" |
| | | <el-dialog :title="upload.title" |
| | | v-model="upload.open" |
| | | width="400px" |
| | | append-to-body |
| | | > |
| | | <el-upload |
| | | ref="uploadRef" |
| | | append-to-body> |
| | | <el-upload ref="uploadRef" |
| | | :limit="1" |
| | | accept=".xlsx, .xls" |
| | | :headers="upload.headers" |
| | |
| | | :on-progress="handleFileUploadProgress" |
| | | :on-success="handleFileSuccess" |
| | | :auto-upload="false" |
| | | drag |
| | | > |
| | | drag> |
| | | <el-icon class="el-icon--upload"><upload-filled /></el-icon> |
| | | <div class="el-upload__text">å°æä»¶æå°æ¤å¤ï¼æ<em>ç¹å»ä¸ä¼ </em></div> |
| | | <template #tip> |
| | |
| | | </el-upload> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" @click="submitFileForm">ç¡® å®</el-button> |
| | | <el-button type="primary" |
| | | @click="submitFileForm">ç¡® å®</el-button> |
| | | <el-button @click="upload.open = false">å æ¶</el-button> |
| | | </div> |
| | | </template> |
| | |
| | | label: "ç¶æ", |
| | | prop: "staffState", |
| | | dataType: "tag", |
| | | formatData: (params) => { |
| | | formatData: params => { |
| | | if (params == 0) { |
| | | return "离è"; |
| | | } else if (params == 1) { |
| | |
| | | return null; |
| | | } |
| | | }, |
| | | formatType: (params) => { |
| | | formatType: params => { |
| | | if (params == 0) { |
| | | return "danger"; |
| | | } else if (params == 1) { |
| | |
| | | prop: "sex", |
| | | }, |
| | | { |
| | | label: "æ·ç±ä½å", |
| | | prop: "nativePlace", |
| | | }, |
| | | { |
| | | label: "å²ä½", |
| | | prop: "postName", |
| | | }, |
| | | { |
| | | label: "ç°ä½å", |
| | | prop: "adress", |
| | | width: 200 |
| | | }, |
| | | { |
| | | label: "第ä¸å¦å", |
| | |
| | | { |
| | | label: "ä¸ä¸", |
| | | prop: "profession", |
| | | width: 100 |
| | | width: 100, |
| | | }, |
| | | { |
| | | label: "å¹´é¾", |
| | |
| | | { |
| | | label: "èç³»çµè¯", |
| | | prop: "phone", |
| | | width: 150 |
| | | width: 150, |
| | | }, |
| | | { |
| | | label: "ç´§æ¥è系人", |
| | | prop: "emergencyContact", |
| | | width: 120 |
| | | width: 120, |
| | | }, |
| | | { |
| | | label: "ç´§æ¥è系人çµè¯", |
| | | prop: "emergencyContactPhone", |
| | | width: 150 |
| | | width: 150, |
| | | }, |
| | | { |
| | | label: "ååç»ææ¥æ", |
| | | prop: "contractExpireTime", |
| | | width: 120 |
| | | width: 120, |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: 'right', |
| | | fixed: "right", |
| | | width: 120, |
| | | operation: [ |
| | | { |
| | | name: "详æ
", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | clickFun: row => { |
| | | openForm("edit", row); |
| | | }, |
| | | } |
| | | }, |
| | | ], |
| | | }, |
| | | ]); |
| | | const filesDia = ref() |
| | | const filesDia = ref(); |
| | | const tableData = ref([]); |
| | | const selectedRows = ref([]); |
| | | const tableLoading = ref(false); |
| | |
| | | size: 100, |
| | | total: 0, |
| | | }); |
| | | const formDia = ref() |
| | | const { proxy } = getCurrentInstance() |
| | | const formDia = ref(); |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | const changeDaterange = (value) => { |
| | | const changeDaterange = value => { |
| | | searchForm.value.entryDateStart = undefined; |
| | | searchForm.value.entryDateEnd = undefined; |
| | | if (value) { |
| | |
| | | getList(); |
| | | }; |
| | | // æå¼éä»¶å¼¹æ¡ |
| | | const openFilesFormDia = (row) => { |
| | | const openFilesFormDia = row => { |
| | | nextTick(() => { |
| | | filesDia.value?.openDialog( row,'åå') |
| | | }) |
| | | filesDia.value?.openDialog(row, "åå"); |
| | | }); |
| | | }; |
| | | // æ¥è¯¢å表 |
| | | /** æç´¢æé®æä½ */ |
| | |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | | const pagination = (obj) => { |
| | | const pagination = obj => { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | getList(); |
| | |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | const params = { ...searchForm.value, ...page }; |
| | | params.entryDate = undefined |
| | | params.staffState = 1 |
| | | staffOnJobListPage(params).then(res => { |
| | | params.entryDate = undefined; |
| | | params.staffState = 1; |
| | | staffOnJobListPage(params) |
| | | .then(res => { |
| | | tableLoading.value = false; |
| | | tableData.value = res.data.records |
| | | tableData.value = res.data.records; |
| | | page.total = res.data.total; |
| | | }).catch(err => { |
| | | tableLoading.value = false; |
| | | }) |
| | | .catch(err => { |
| | | tableLoading.value = false; |
| | | }); |
| | | }; |
| | | // è¡¨æ ¼éæ©æ°æ® |
| | | const handleSelectionChange = (selection) => { |
| | | const handleSelectionChange = selection => { |
| | | selectedRows.value = selection; |
| | | }; |
| | | |
| | | // æå¼å¼¹æ¡ |
| | | const openForm = (type, row) => { |
| | | nextTick(() => { |
| | | formDia.value?.openDialog(type, row) |
| | | }) |
| | | formDia.value?.openDialog(type, row); |
| | | }); |
| | | }; |
| | | // å¯¼åº |
| | | const handleOut = () => { |
| | |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | proxy.download("/staff/staffOnJob/export", {staffState: 1}, "åå管ç.xlsx"); |
| | | proxy.download( |
| | | "/staff/staffOnJob/export", |
| | | { staffState: 1 }, |
| | | "åå管ç.xlsx" |
| | | ); |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msg("已忶"); |
| | |
| | | :fields="submitFormFields" |
| | | :active-template="activeTemplate" |
| | | :user-options="flowUserOptions" |
| | | :module-key="APPROVAL_MODULE_KEYS.TRANSFER" |
| | | :is-edit="isSubmitEdit" |
| | | :saving="submitSaving" |
| | | :form-ref="submitFormRef" |
| | |
| | | :fields="submitFormFields" |
| | | :active-template="activeTemplate" |
| | | :user-options="flowUserOptions" |
| | | :module-key="APPROVAL_MODULE_KEYS.WORK_HANDOVER" |
| | | :is-edit="isSubmitEdit" |
| | | :saving="submitSaving" |
| | | :form-ref="submitFormRef" |
| | |
| | | <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> |
| | |
| | | <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 /> |
| | |
| | | |
| | | <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: () => ({}) }, |
| | |
| | | <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> |
| | |
| | | <el-option v-for="item in shifts_list" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" /> |
| | | :value="item.value" |
| | | :disabled=" |
| | | operationType === 'add' && |
| | | (String(item.label || '').includes('å') || |
| | | String(item.label || '').includes('ä¼')) |
| | | " /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="æå¡èå´(m)" |
| | | prop="radius"> |
| | | prop="radius" |
| | | v-show="!hideClockFields"> |
| | | <el-input-number v-model="form.radius" |
| | | :min="10" |
| | | :max="1000" |
| | |
| | | </el-form-item> |
| | | <!-- é«å¾·å°å¾éæ© --> |
| | | <el-form-item label="æå¡ä½ç½®" |
| | | prop="longitude"> |
| | | prop="longitude" |
| | | v-show="!hideClockFields"> |
| | | <div class="map-container"> |
| | | <div class="map-header" |
| | | style="margin-bottom: 10px"> |
| | |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="å°ç¹åç§°" |
| | | prop="locationName"> |
| | | prop="locationName" |
| | | v-show="!hideClockFields"> |
| | | <el-input v-model="form.locationName" |
| | | :disabled="operationType === 'view'" |
| | | placeholder="请è¾å
¥å°ç¹åç§°" /> |
| | | </el-form-item> |
| | | <!-- ä¸ä¸çæ¶é´ --> |
| | | <el-form-item label="ä¸çæ¶é´" |
| | | prop="startAt"> |
| | | prop="startAt" |
| | | v-show="!hideClockFields"> |
| | | <el-time-picker v-model="form.startAt" |
| | | format="HH:mm" |
| | | value-format="HH:mm" |
| | |
| | | :disabled="operationType === 'view'" /> |
| | | </el-form-item> |
| | | <el-form-item label="ä¸çæ¶é´" |
| | | prop="endAt"> |
| | | prop="endAt" |
| | | v-show="!hideClockFields"> |
| | | <el-time-picker v-model="form.endAt" |
| | | format="HH:mm" |
| | | value-format="HH:mm" |
| | |
| | | </el-form> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button type="primary" @click="submitForm" v-if="operationType !== 'view'">ç¡®å®</el-button> |
| | | <el-button type="primary" |
| | | @click="submitForm" |
| | | v-if="operationType !== 'view'">ç¡®å®</el-button> |
| | | <el-button @click="dialogVisible = false">åæ¶</el-button> |
| | | </span> |
| | | </template> |
| | |
| | | // è·åçæ¬¡åå
¸å¼ |
| | | const { shifts_list } = useDict("shifts_list"); |
| | | |
| | | const selectedShiftLabel = computed(() => { |
| | | const item = shifts_list.value?.find( |
| | | s => String(s.value) === String(form.shift) |
| | | ); |
| | | return item?.label || ""; |
| | | }); |
| | | |
| | | const hideClockFields = computed(() => { |
| | | if (props.operationType === "view") return false; |
| | | return ( |
| | | selectedShiftLabel.value.includes("å") || |
| | | selectedShiftLabel.value.includes("ä¼") |
| | | ); |
| | | }); |
| | | |
| | | // è¡¨åæ°æ® |
| | | const formRef = ref(); |
| | | const form = reactive({ |
| | |
| | | }); |
| | | |
| | | // 表åéªè¯è§å |
| | | const rules = { |
| | | const rules = computed(() => { |
| | | const base = { |
| | | sysDeptId: [{ required: true, message: "è¯·éæ©é¨é¨", trigger: "change" }], |
| | | shift: [{ required: true, message: "è¯·éæ©çæ¬¡", trigger: "change" }], |
| | | }; |
| | | |
| | | if (hideClockFields.value) { |
| | | return base; |
| | | } |
| | | |
| | | return { |
| | | ...base, |
| | | locationName: [ |
| | | { required: true, message: "请è¾å
¥å°ç¹åç§°", trigger: "blur" }, |
| | | ], |
| | | longitude: [{ required: true, message: "è¯·éæ©æå¡ä½ç½®", trigger: "blur" }], |
| | | latitude: [{ required: true, message: "è¯·éæ©æå¡ä½ç½®", trigger: "blur" }], |
| | | shift: [{ required: true, message: "è¯·éæ©çæ¬¡", trigger: "change" }], |
| | | radius: [{ required: true, message: "请è¾å
¥æå¡èå´", trigger: "blur" }], |
| | | startAt: [{ required: true, message: "è¯·éæ©ä¸çæ¶é´", trigger: "change" }], |
| | | endAt: [ |
| | | { 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(":"); |
| | |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | |
| | | // é¨é¨é项 |
| | | const deptOptions = ref([]); |
| | |
| | | watch( |
| | | () => form.radius, |
| | | newValue => { |
| | | if (circle) { |
| | | circle.setRadius(newValue); |
| | | if (circle && newValue != null && newValue !== "") { |
| | | circle.setRadius(Number(newValue)); |
| | | } |
| | | } |
| | | ); |
| | |
| | | () => { |
| | | if (formRef.value && form.endAt) { |
| | | formRef.value.validateField("endAt"); |
| | | } |
| | | } |
| | | ); |
| | | |
| | | watch( |
| | | () => hideClockFields.value, |
| | | val => { |
| | | if (!val) return; |
| | | form.radius = null; |
| | | form.longitude = ""; |
| | | form.latitude = ""; |
| | | form.locationName = ""; |
| | | form.startAt = ""; |
| | | form.endAt = ""; |
| | | if (formRef.value) { |
| | | formRef.value.clearValidate([ |
| | | "radius", |
| | | "longitude", |
| | | "latitude", |
| | | "locationName", |
| | | "startAt", |
| | | "endAt", |
| | | ]); |
| | | } |
| | | } |
| | | ); |
| | |
| | | |
| | | // åå§åå°å¾ |
| | | setTimeout(() => { |
| | | if (!hideClockFields.value) { |
| | | initMap(); |
| | | } |
| | | }, 100); |
| | | } |
| | | } |
| | |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | <!-- æ¥è¯¢æ¡ä»¶ --> |
| | | <!-- <el-form :model="searchForm" |
| | | <el-form :model="searchForm" |
| | | :inline="true" |
| | | class="search-form mb16"> |
| | | <el-form-item label="é¨é¨ï¼" |
| | | prop="countId"> |
| | | <el-tree-select v-model="searchForm.countId" |
| | | prop="sysDeptId"> |
| | | <el-tree-select v-model="searchForm.sysDeptId" |
| | | :data="deptOptions" |
| | | :props="{ value: 'id', label: 'label', children: 'children' }" |
| | | value-key="id" |
| | | placeholder="è¯·éæ©é¨é¨" |
| | | check-strictly |
| | | style="width: 200px" /> |
| | | </el-form-item> |
| | | <el-form-item label="å°ç¹ï¼" |
| | | prop="locationName"> |
| | | <el-input v-model="searchForm.locationName" |
| | | placeholder="请è¾å
¥å°ç¹åç§°" |
| | | clearable |
| | | style="width: 200px" /> |
| | | </el-form-item> |
| | | <el-form-item label="çæ¬¡ï¼" |
| | | prop="shift"> |
| | | <el-select v-model="searchForm.shift" |
| | | placeholder="è¯·éæ©çæ¬¡" |
| | | clearable |
| | | style="width: 200px"> |
| | | <el-option |
| | | v-for="item in shifts_list" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" |
| | | @click="fetchData"> |
| | | @click="handleSearch"> |
| | | <el-icon> |
| | | <Search /> |
| | | </el-icon> |
| | |
| | | éç½® |
| | | </el-button> |
| | | </el-form-item> |
| | | </el-form> --> |
| | | </el-form> |
| | | <!-- çæ¬¡å表 --> |
| | | <el-card shadow="never" |
| | | class="mb16"> |
| | |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="locationName" |
| | | label="å°ç¹åç§°" /> |
| | | label="å°ç¹åç§°"> |
| | | <template #default="scope"> |
| | | {{ scope.row.locationName || "-" }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="longitude" |
| | | label="ç»åº¦" /> |
| | | label="ç»åº¦"> |
| | | <template #default="scope"> |
| | | {{ scope.row.longitude ?? "-" }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="latitude" |
| | | label="纬度" /> |
| | | label="纬度"> |
| | | <template #default="scope"> |
| | | {{ scope.row.latitude ?? "-" }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="radius" |
| | | label="æå¡èå´(m)" /> |
| | | label="æå¡èå´(m)"> |
| | | <template #default="scope"> |
| | | {{ scope.row.radius ?? "-" }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="startAt" |
| | | label="ä¸çæ¶é´"> |
| | | <template #default="scope"> |
| | | {{ scope.row.startAt }} |
| | | {{ scope.row.startAt || "-" }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="endAt" |
| | | label="ä¸çæ¶é´"> |
| | | <template #default="scope"> |
| | | {{ scope.row.endAt }} |
| | | {{ scope.row.endAt || "-" }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" |
| | |
| | | <template #default="scope"> |
| | | <el-button type="primary" |
| | | link |
| | | :disabled="isRuleLocked(scope.row)" |
| | | @click="openForm('edit', scope.row)">ç¼è¾</el-button> |
| | | <el-button type="danger" |
| | | link |
| | | :disabled="isRuleLocked(scope.row)" |
| | | @click="handleDelete(scope.row.id)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted } from "vue"; |
| | | import { ref, reactive, onMounted, getCurrentInstance } from "vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import { |
| | | Plus, |
| | |
| | | |
| | | // æ¥è¯¢è¡¨å |
| | | const searchForm = reactive({ |
| | | countId: "", |
| | | locationName: "", |
| | | sysDeptId: "", |
| | | shift: "", |
| | | }); |
| | | |
| | | // é¨é¨é项 |
| | |
| | | if (!value) return ""; |
| | | const shift = shifts_list.value.find(item => item.value === value); |
| | | return shift ? shift.label : value; |
| | | }; |
| | | |
| | | const isRuleLocked = row => { |
| | | return Boolean(row?.usageLocked || row?.canEdit === false || row?.canDelete === false); |
| | | }; |
| | | |
| | | // è·åé¨é¨å表 |
| | |
| | | tableLoading.value = true; |
| | | getAttendanceRules({ ...page, ...searchForm }) |
| | | .then(res => { |
| | | tableData.value = res.data.records; |
| | | const records = Array.isArray(res?.data?.records) ? res.data.records : []; |
| | | tableData.value = records.filter(r => String(r?.id) !== "999"); |
| | | page.total = res.data.total; |
| | | }) |
| | | .finally(() => { |
| | | tableLoading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | const handleSearch = () => { |
| | | page.current = 1; |
| | | fetchData(); |
| | | }; |
| | | |
| | | // å页忴 |
| | |
| | | |
| | | // éç½®æç´¢ |
| | | const resetSearch = () => { |
| | | searchForm.countId = ""; |
| | | searchForm.locationName = ""; |
| | | searchForm.sysDeptId = ""; |
| | | searchForm.shift = ""; |
| | | page.current = 1; |
| | | fetchData(); |
| | | }; |
| | | |
| | | // æå¼è¡¨å |
| | | const openForm = (type, row = {}) => { |
| | | if (type === "edit" && isRuleLocked(row)) { |
| | | ElMessage.warning("è¯¥çæ¬¡é
置已被æç并产çæå¡è®°å½ï¼ä¸è½ç¼è¾"); |
| | | return; |
| | | } |
| | | operationType.value = type; |
| | | currentRow.value = row; |
| | | dialogVisible.value = true; |
| | |
| | | |
| | | // å é¤ç次 |
| | | const handleDelete = id => { |
| | | const row = tableData.value.find(item => String(item.id) === String(id)); |
| | | if (isRuleLocked(row)) { |
| | | ElMessage.warning("åå¨å·²æçä¸å·²ææå¡è®°å½ççæ¬¡é
ç½®ï¼ä¸è½å é¤"); |
| | | return; |
| | | } |
| | | ElMessageBox.confirm("ç¡®å®è¦å é¤è¿æ¡ç次åï¼", "å é¤ç¡®è®¤", { |
| | | confirmButtonText: "ç¡®å®", |
| | | cancelButtonText: "åæ¶", |
| | |
| | | :key="'d' + i"> |
| | | <el-dropdown trigger="click" |
| | | placement="bottom" |
| | | @command="(e) => handleCommand(e, m)" |
| | | :disabled="isCellBlockedByLeaveDate(item, m)" |
| | | @command="(e) => handleCommand(e, item, m)" |
| | | class="shift-dropdown"> |
| | | <div class="shift-box" |
| | | :class="{ |
| | | 'shift-box-early': m.shift === 'æ©ç', |
| | | 'shift-box-mid': m.shift === 'ä¸ç', |
| | | 'shift-box-night': m.shift === 'å¤ç', |
| | | 'shift-box-rest': m.shift === '伿¯', |
| | | 'shift-box-rest': !m.shift || m.shift === '伿¯', |
| | | 'shift-box-leave': m.shift === '请å', |
| | | 'shift-box-other': m.shift === 'å¤11', |
| | | 'shift-box-business': m.shift === 'å¤12', |
| | | }"> |
| | | <span class="shift-text">{{ getShiftNameByValue(m.shift) || 'â' }}</span> |
| | | <span class="shift-text">{{ getShiftNameByValue(m.shift) || '伿¯' }}</span> |
| | | </div> |
| | | <template #dropdown> |
| | | <el-dropdown-menu> |
| | |
| | | </div> --> |
| | | <div class="user-total"> |
| | | <span class="total-label">å计åºå¤:</span> |
| | | <span class="total-value">{{ item.work_time }}天</span> |
| | | <span class="total-value">{{ item.year.totalYearAttendance }}天</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | |
| | | <div class="search_thing"> |
| | | <div class="search_label" |
| | | style="width: 90px"> |
| | | <span style="color: red; margin-right: 4px">*</span>é¨é¨ï¼ |
| | | </div> |
| | | <div class="search_input"> |
| | | <el-tree-select v-model="schedulingQuery.deptId" |
| | | :data="deptOptions" |
| | | :props="{ value: 'id', label: 'label', children: 'children' }" |
| | | value-key="id" |
| | | placeholder="è¯·éæ©é¨é¨" |
| | | check-strictly |
| | | clearable |
| | | @change="onSchedulingDeptChange" |
| | | style="width: 100%" /> |
| | | </div> |
| | | </div> |
| | | <div class="search_thing"> |
| | | <div class="search_label" |
| | | style="width: 90px"> |
| | | <span style="color: red; margin-right: 4px">*</span>人ååç§°ï¼ |
| | | </div> |
| | | <div class="search_input"> |
| | |
| | | style="width: 100%" |
| | | multiple |
| | | clearable |
| | | collapse-tags> |
| | | collapse-tags |
| | | :disabled="!schedulingQuery.deptId"> |
| | | <el-option v-for="item in personList" |
| | | :key="item.id" |
| | | :label="item.staffName" |
| | |
| | | <div class="search_input"> |
| | | <el-select v-model="schedulingQuery.shift" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%"> |
| | | <el-option v-for="item in classType" |
| | | style="width: 100%" |
| | | :disabled="!schedulingQuery.deptId"> |
| | | <el-option v-for="item in schedulingClassType" |
| | | :key="item.id" |
| | | :label="getShiftNameByValue(item.shift)" |
| | | :value="item.id"> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted, getCurrentInstance } from "vue"; |
| | | import { |
| | | ref, |
| | | reactive, |
| | | computed, |
| | | watch, |
| | | onMounted, |
| | | getCurrentInstance, |
| | | } from "vue"; |
| | | import { useRouter } from "vue-router"; |
| | | import { |
| | | page, |
| | |
| | | update, |
| | | staffOnJobListPage, |
| | | } from "@/api/personnelManagement/class"; |
| | | import { findStaffLeaveListPage } from "@/api/personnelManagement/staffLeave.js"; |
| | | import { deptTreeSelect } from "@/api/system/user.js"; |
| | | import { getAttendanceRules } from "@/api/personnelManagement/attendanceRules.js"; |
| | | import { useDict } from "@/utils/dict"; |
| | |
| | | |
| | | // 人åå表 |
| | | const personList = ref([]); |
| | | const personListAll = ref([]); |
| | | const leaveDateMap = ref({}); |
| | | |
| | | // å è½½ç¶æ |
| | | const loading = ref(false); |
| | |
| | | // æçæ¥è¯¢æ¡ä»¶ |
| | | const schedulingQuery = reactive({ |
| | | week: "", |
| | | deptId: "", |
| | | userId: null, |
| | | shift: "", |
| | | }); |
| | | |
| | | const schedulingClassType = computed(() => { |
| | | if (!schedulingQuery.deptId) return classType.value || []; |
| | | return (classType.value || []).filter(item => { |
| | | if (["998", "999"].includes(String(item?.id))) return true; |
| | | const itemDeptId = item.deptId ?? item.sysDeptId ?? item.dept_id; |
| | | return String(itemDeptId) === String(schedulingQuery.deptId); |
| | | }); |
| | | }); |
| | | |
| | | // åè¡¨æ°æ® |
| | |
| | | proxy.$modal.msgError("è¯·éæ©å¨æ¬¡"); |
| | | return; |
| | | } |
| | | if (!schedulingQuery.deptId) { |
| | | proxy.$modal.msgError("è¯·éæ©é¨é¨"); |
| | | return; |
| | | } |
| | | let time = schedulingQuery.week.getTime(); |
| | | |
| | | // æ ¼å¼åæ¥æä¸º YYYY-MM-DD æ ¼å¼ |
| | |
| | | proxy.$modal.msgError("è¯·éæ©çæ¬¡"); |
| | | return; |
| | | } |
| | | const blockedEmployees = (personList.value || []).filter(item => { |
| | | const leaveDate = getEmployeeLeaveDate(item?.id); |
| | | const weekEndDate = getSchedulingWeekEndDate(); |
| | | return leaveDate && weekEndDate && weekEndDate.getTime() > leaveDate.getTime(); |
| | | }); |
| | | if (blockedEmployees.length) { |
| | | proxy.$modal.msgError("åå¨å·²è¶
è¿ç¦»èæ¥æç人åï¼ä¸è½ç»§ç»æç"); |
| | | return; |
| | | } |
| | | loading.value = true; |
| | | add({ |
| | | startWeek, |
| | |
| | | proxy.$modal.msgSuccess("æä½æå"); |
| | | schedulingVisible.value = false; |
| | | schedulingQuery.week = ""; |
| | | schedulingQuery.deptId = ""; |
| | | schedulingQuery.userId = null; |
| | | schedulingQuery.shift = ""; |
| | | personList.value = []; |
| | | refresh(); |
| | | }) |
| | | .catch(err => { |
| | |
| | | } |
| | | proxy.$download.saveAs(blob, fileName + ".xlsx"); |
| | | }) |
| | | .catch(err => { |
| | | }) |
| | | .catch(err => {}) |
| | | .finally(() => { |
| | | downLoading.value = false; |
| | | }); |
| | | }; |
| | | // å¤çå½ä»¤ |
| | | const handleCommand = (e, m) => { |
| | | const handleCommand = (e, row, cell) => { |
| | | if (isCellBlockedByLeaveDate(row, cell)) { |
| | | proxy.$modal.msgError("该å工已è¶
åºç¦»èæ¥æï¼ä¸è½ç»§ç»æç"); |
| | | return; |
| | | } |
| | | // if (e != m.shift) { |
| | | update({ |
| | | id: m.id, |
| | | id: cell.id, |
| | | personalAttendanceLocationConfigId: e, |
| | | }).then(res => { |
| | | proxy.$modal.msgSuccess("æä½æå"); |
| | |
| | | // æ¥è¯¢è§åå表 |
| | | const fetchData = () => { |
| | | getAttendanceRules({ current: -1, size: -1 }).then(res => { |
| | | classType.value = res.data.records; |
| | | const records = Array.isArray(res?.data?.records) ? res.data.records : []; |
| | | const appendList = [ |
| | | { id: 998, shift: "伿¯" }, |
| | | { id: 999, shift: "请å" }, |
| | | ]; |
| | | const exists = new Set(records.map(r => String(r?.id))); |
| | | appendList.forEach(item => { |
| | | if (!exists.has(String(item.id))) { |
| | | records.push(item); |
| | | } |
| | | }); |
| | | classType.value = records; |
| | | }); |
| | | }; |
| | | // è·åç¨æ· |
| | |
| | | staffState: 1, |
| | | }).then(res => { |
| | | let arr = res.data.records; |
| | | personList.value = arr; |
| | | personListAll.value = arr; |
| | | updatePersonListByDept(); |
| | | }); |
| | | }; |
| | | |
| | | const getLeaveDates = () => { |
| | | findStaffLeaveListPage({ |
| | | current: -1, |
| | | size: -1, |
| | | }).then(res => { |
| | | const records = Array.isArray(res?.data?.records) ? res.data.records : []; |
| | | const map = {}; |
| | | records.forEach(item => { |
| | | const staffId = item?.staffOnJobId ?? item?.staff_on_job_id; |
| | | const leaveDate = item?.leaveDate ?? item?.leave_date; |
| | | if (!staffId || !leaveDate) return; |
| | | const key = String(staffId); |
| | | const prev = map[key]; |
| | | if (!prev || String(leaveDate) > String(prev)) { |
| | | map[key] = leaveDate; |
| | | } |
| | | }); |
| | | leaveDateMap.value = map; |
| | | updatePersonListByDept(); |
| | | syncPersonListSelection(); |
| | | }); |
| | | }; |
| | | |
| | | const findDeptNodeById = (deptId, deptList = deptOptions.value) => { |
| | | const list = Array.isArray(deptList) ? deptList : []; |
| | | for (const node of list) { |
| | | if (String(node.id) === String(deptId)) return node; |
| | | if (node.children && node.children.length) { |
| | | const found = findDeptNodeById(deptId, node.children); |
| | | if (found) return found; |
| | | } |
| | | } |
| | | return null; |
| | | }; |
| | | |
| | | const collectDeptIds = node => { |
| | | const ids = []; |
| | | if (!node) return ids; |
| | | ids.push(String(node.id)); |
| | | if (node.children && node.children.length) { |
| | | node.children.forEach(child => { |
| | | ids.push(...collectDeptIds(child)); |
| | | }); |
| | | } |
| | | return ids; |
| | | }; |
| | | |
| | | const updatePersonListByDept = () => { |
| | | if (!schedulingQuery.deptId) { |
| | | personList.value = []; |
| | | return; |
| | | } |
| | | const node = findDeptNodeById(schedulingQuery.deptId); |
| | | const deptIds = new Set(collectDeptIds(node)); |
| | | personList.value = (personListAll.value || []).filter(item => { |
| | | const itemDeptId = item.deptId ?? item.sysDeptId ?? item.dept_id; |
| | | return deptIds.has(String(itemDeptId)) && !isEmployeeBlockedByLeaveDate(item); |
| | | }); |
| | | }; |
| | | |
| | | const parseDateOnly = value => { |
| | | if (!value) return null; |
| | | if (value instanceof Date) { |
| | | const d = new Date(value); |
| | | d.setHours(0, 0, 0, 0); |
| | | return d; |
| | | } |
| | | const normalized = String(value).trim().slice(0, 10); |
| | | if (!normalized) return null; |
| | | const d = new Date(normalized.replace(/-/g, "/")); |
| | | if (Number.isNaN(d.getTime())) return null; |
| | | d.setHours(0, 0, 0, 0); |
| | | return d; |
| | | }; |
| | | |
| | | const getSchedulingWeekEndDate = () => { |
| | | if (!schedulingQuery.week) return null; |
| | | const start = new Date(schedulingQuery.week); |
| | | start.setHours(0, 0, 0, 0); |
| | | return new Date(start.getTime() + 24 * 60 * 60 * 1000 * 5); |
| | | }; |
| | | |
| | | const getCellDate = cell => parseDateOnly(cell?.time); |
| | | |
| | | const getEmployeeLeaveDate = staffId => { |
| | | const leaveDate = leaveDateMap.value[String(staffId)]; |
| | | return parseDateOnly(leaveDate); |
| | | }; |
| | | |
| | | const isEmployeeBlockedByLeaveDate = item => { |
| | | const leaveDate = getEmployeeLeaveDate(item?.id); |
| | | if (!leaveDate) return false; |
| | | const weekEndDate = getSchedulingWeekEndDate(); |
| | | if (!weekEndDate) return false; |
| | | return weekEndDate.getTime() > leaveDate.getTime(); |
| | | }; |
| | | |
| | | const isCellBlockedByLeaveDate = (row, cell) => { |
| | | const staffId = row?.userId ?? row?.user_id ?? row?.id; |
| | | const leaveDate = getEmployeeLeaveDate(staffId); |
| | | const cellDate = getCellDate(cell); |
| | | if (!leaveDate || !cellDate) return false; |
| | | return cellDate.getTime() > leaveDate.getTime(); |
| | | }; |
| | | |
| | | const syncPersonListSelection = () => { |
| | | if (!Array.isArray(schedulingQuery.userId) || !schedulingQuery.userId.length) return; |
| | | const validIds = new Set((personList.value || []).map(item => String(item.id))); |
| | | const nextIds = schedulingQuery.userId.filter(id => validIds.has(String(id))); |
| | | if (nextIds.length !== schedulingQuery.userId.length) { |
| | | schedulingQuery.userId = nextIds; |
| | | proxy.$modal.msgWarning("å·²èªå¨åé¤è¶
åºç¦»èæ¥æç人å"); |
| | | } |
| | | }; |
| | | |
| | | const setDefaultShiftByDept = () => { |
| | | if (!schedulingQuery.deptId) { |
| | | schedulingQuery.shift = ""; |
| | | return; |
| | | } |
| | | const first = schedulingClassType.value?.[0]; |
| | | schedulingQuery.shift = first ? first.id : ""; |
| | | }; |
| | | |
| | | const onSchedulingDeptChange = () => { |
| | | schedulingQuery.userId = null; |
| | | updatePersonListByDept(); |
| | | setDefaultShiftByDept(); |
| | | }; |
| | | |
| | | // æ ¹æ®åå
¸è·åæ¥æ |
| | |
| | | onMounted(() => { |
| | | fetchData(); |
| | | getUsers(); |
| | | getLeaveDates(); |
| | | fetchDeptOptions(); |
| | | if (query.month) { |
| | | init(); |
| | |
| | | } |
| | | monthList.value.reverse(); |
| | | }); |
| | | |
| | | watch( |
| | | () => classType.value, |
| | | () => { |
| | | if (schedulingVisible.value && schedulingQuery.deptId) { |
| | | setDefaultShiftByDept(); |
| | | } |
| | | } |
| | | ); |
| | | |
| | | watch( |
| | | () => schedulingQuery.week, |
| | | () => { |
| | | if (!schedulingVisible.value) return; |
| | | updatePersonListByDept(); |
| | | syncPersonListSelection(); |
| | | } |
| | | ); |
| | | |
| | | watch( |
| | | () => schedulingQuery.deptId, |
| | | () => { |
| | | if (!schedulingVisible.value) return; |
| | | updatePersonListByDept(); |
| | | syncPersonListSelection(); |
| | | } |
| | | ); |
| | | |
| | | watch( |
| | | () => schedulingVisible.value, |
| | | val => { |
| | | if (!val) return; |
| | | schedulingQuery.week = ""; |
| | | schedulingQuery.deptId = ""; |
| | | schedulingQuery.userId = null; |
| | | schedulingQuery.shift = ""; |
| | | personList.value = []; |
| | | } |
| | | ); |
| | | </script> |
| | | |
| | | <style scoped> |
| | |
| | | <div class="search_form mb20"> |
| | | <div> |
| | | <span class="search_title">å§åï¼</span> |
| | | <el-input v-model="searchForm.staffName" style="width: 240px" placeholder="请è¾å
¥å§åæç´¢" @change="handleQuery" |
| | | clearable :prefix-icon="Search" /> |
| | | <span style="margin-left: 10px" class="search_title">ååç»ææ¥æï¼</span> |
| | | <el-date-picker v-model="searchForm.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange" |
| | | placeholder="è¯·éæ©" clearable @change="changeDaterange" /> |
| | | <el-button type="primary" @click="handleQuery" style="margin-left: 10px">æç´¢</el-button> |
| | | <el-input v-model="searchForm.staffName" |
| | | style="width: 240px" |
| | | placeholder="请è¾å
¥å§åæç´¢" |
| | | @change="handleQuery" |
| | | clearable |
| | | :prefix-icon="Search" /> |
| | | <span style="margin-left: 10px" |
| | | class="search_title">ååç»ææ¥æï¼</span> |
| | | <el-date-picker v-model="searchForm.entryDate" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | type="daterange" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | @change="changeDaterange" /> |
| | | <el-button type="primary" |
| | | @click="handleQuery" |
| | | style="margin-left: 10px">æç´¢</el-button> |
| | | </div> |
| | | <div> |
| | | <!-- <el-button type="primary" @click="openForm('add')">æ°å¢å
¥è</el-button>--> |
| | |
| | | </div> |
| | | </div> |
| | | <div class="table_list"> |
| | | <PIMTable rowKey="id" :column="tableColumn" :tableData="tableData" :page="page" :isSelection="true" |
| | | @selection-change="handleSelectionChange" :tableLoading="tableLoading" @pagination="pagination" |
| | | <PIMTable rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :page="page" |
| | | :isSelection="true" |
| | | @selection-change="handleSelectionChange" |
| | | :tableLoading="tableLoading" |
| | | @pagination="pagination" |
| | | :total="page.total"></PIMTable> |
| | | </div> |
| | | <form-dia ref="formDia" @close="handleQuery"></form-dia> |
| | | <renew-contract |
| | | v-if="isShowRenewContractModal" |
| | | <form-dia ref="formDia" |
| | | @close="handleQuery"></form-dia> |
| | | <renew-contract v-if="isShowRenewContractModal" |
| | | v-model:visible="isShowRenewContractModal" |
| | | :id="id" |
| | | @completed="handleQuery" |
| | | /> |
| | | |
| | | @completed="handleQuery" /> |
| | | <!-- åå导å
¥å¯¹è¯æ¡ --> |
| | | <el-dialog |
| | | :title="upload.title" |
| | | <el-dialog :title="upload.title" |
| | | v-model="upload.open" |
| | | width="400px" |
| | | append-to-body |
| | | > |
| | | <el-upload |
| | | ref="uploadRef" |
| | | append-to-body> |
| | | <el-upload ref="uploadRef" |
| | | :limit="1" |
| | | accept=".xlsx, .xls" |
| | | :headers="upload.headers" |
| | |
| | | :on-progress="handleFileUploadProgress" |
| | | :on-success="handleFileSuccess" |
| | | :auto-upload="false" |
| | | drag |
| | | > |
| | | drag> |
| | | <el-icon class="el-icon--upload"><upload-filled /></el-icon> |
| | | <div class="el-upload__text">å°æä»¶æå°æ¤å¤ï¼æ<em>ç¹å»ä¸ä¼ </em></div> |
| | | <template #tip> |
| | |
| | | </el-upload> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" @click="submitFileForm">ç¡® å®</el-button> |
| | | <el-button type="primary" |
| | | @click="submitFileForm">ç¡® å®</el-button> |
| | | <el-button @click="upload.open = false">å æ¶</el-button> |
| | | </div> |
| | | </template> |
| | |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import { onMounted, ref, defineAsyncComponent } from "vue"; |
| | | import FormDia from "@/views/personnelManagement/contractManagement/components/formDia.vue"; |
| | | const RenewContract = defineAsyncComponent(() => import("@/views/personnelManagement/employeeRecord/components/RenewContract.vue")); |
| | | const RenewContract = defineAsyncComponent(() => |
| | | import( |
| | | "@/views/personnelManagement/employeeRecord/components/RenewContract.vue" |
| | | ) |
| | | ); |
| | | import { ElMessageBox } from "element-plus"; |
| | | import { staffOnJobListPage } from "@/api/personnelManagement/staffOnJob.js"; |
| | | import dayjs from "dayjs"; |
| | |
| | | label: "ç¶æ", |
| | | prop: "staffState", |
| | | dataType: "tag", |
| | | formatData: (params) => { |
| | | formatData: params => { |
| | | if (params == 0) { |
| | | return "离è"; |
| | | } else if (params == 1) { |
| | |
| | | return null; |
| | | } |
| | | }, |
| | | formatType: (params) => { |
| | | formatType: params => { |
| | | if (params == 0) { |
| | | return "danger"; |
| | | } else if (params == 1) { |
| | |
| | | prop: "sex", |
| | | }, |
| | | { |
| | | label: "æ·ç±ä½å", |
| | | prop: "nativePlace", |
| | | }, |
| | | { |
| | | label: "å²ä½", |
| | | prop: "postName", |
| | | }, |
| | | { |
| | | label: "ç°ä½å", |
| | | prop: "adress", |
| | | width: 200 |
| | | }, |
| | | { |
| | | label: "第ä¸å¦å", |
| | |
| | | { |
| | | label: "ä¸ä¸", |
| | | prop: "profession", |
| | | width: 100 |
| | | width: 100, |
| | | }, |
| | | { |
| | | label: "å¹´é¾", |
| | |
| | | { |
| | | label: "èç³»çµè¯", |
| | | prop: "phone", |
| | | width: 150 |
| | | width: 150, |
| | | }, |
| | | { |
| | | label: "ç´§æ¥è系人", |
| | | prop: "emergencyContact", |
| | | width: 120 |
| | | width: 120, |
| | | }, |
| | | { |
| | | label: "ç´§æ¥è系人çµè¯", |
| | | prop: "emergencyContactPhone", |
| | | width: 150 |
| | | width: 150, |
| | | }, |
| | | // { |
| | | // label: "ååå¹´é", |
| | |
| | | { |
| | | label: "ååç»ææ¥æ", |
| | | prop: "contractExpireTime", |
| | | width: 120 |
| | | width: 120, |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: 'right', |
| | | fixed: "right", |
| | | width: 160, |
| | | operation: [ |
| | | { |
| | | name: "详æ
", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | clickFun: row => { |
| | | openForm("edit", row); |
| | | }, |
| | | }, |
| | |
| | | name: "ç»ç¾åå", |
| | | type: "text", |
| | | showHide: row => row.staffState === 1, |
| | | clickFun: (row) => { |
| | | clickFun: row => { |
| | | isShowRenewContractModal.value = true; |
| | | id.value = row.id; |
| | | }, |
| | | } |
| | | }, |
| | | ], |
| | | }, |
| | | ]); |
| | | const filesDia = ref() |
| | | const filesDia = ref(); |
| | | const isShowRenewContractModal = ref(false); |
| | | const id = ref(0); |
| | | const tableData = ref([]); |
| | |
| | | size: 100, |
| | | total: 0, |
| | | }); |
| | | const formDia = ref() |
| | | const { proxy } = getCurrentInstance() |
| | | const formDia = ref(); |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | const changeDaterange = (value) => { |
| | | const changeDaterange = value => { |
| | | searchForm.value.entryDateStart = undefined; |
| | | searchForm.value.entryDateEnd = undefined; |
| | | if (value) { |
| | |
| | | getList(); |
| | | }; |
| | | // æå¼éä»¶å¼¹æ¡ |
| | | const openFilesFormDia = (row) => { |
| | | console.log(row) |
| | | const openFilesFormDia = row => { |
| | | console.log(row); |
| | | nextTick(() => { |
| | | filesDia.value?.openDialog( row,'åå') |
| | | }) |
| | | filesDia.value?.openDialog(row, "åå"); |
| | | }); |
| | | }; |
| | | // æ¥è¯¢å表 |
| | | /** æç´¢æé®æä½ */ |
| | |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | | const pagination = (obj) => { |
| | | const pagination = obj => { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | getList(); |
| | |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | const params = { ...searchForm.value, ...page }; |
| | | params.entryDate = undefined |
| | | params.staffState = 1 |
| | | staffOnJobListPage(params).then(res => { |
| | | params.entryDate = undefined; |
| | | params.staffState = 1; |
| | | staffOnJobListPage(params) |
| | | .then(res => { |
| | | tableLoading.value = false; |
| | | tableData.value = res.data.records |
| | | tableData.value = res.data.records; |
| | | page.total = res.data.total; |
| | | }).catch(err => { |
| | | tableLoading.value = false; |
| | | }) |
| | | .catch(err => { |
| | | tableLoading.value = false; |
| | | }); |
| | | }; |
| | | // è¡¨æ ¼éæ©æ°æ® |
| | | const handleSelectionChange = (selection) => { |
| | | const handleSelectionChange = selection => { |
| | | selectedRows.value = selection; |
| | | }; |
| | | |
| | | // æå¼å¼¹æ¡ |
| | | const openForm = (type, row) => { |
| | | nextTick(() => { |
| | | formDia.value?.openDialog(type, row) |
| | | }) |
| | | formDia.value?.openDialog(type, row); |
| | | }); |
| | | }; |
| | | // å¯¼åº |
| | | const handleOut = () => { |
| | |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | proxy.download("/staff/staffOnJob/export", {staffState: 1}, "åå管ç.xlsx"); |
| | | proxy.download( |
| | | "/staff/staffOnJob/export", |
| | | { staffState: 1 }, |
| | | "åå管ç.xlsx" |
| | | ); |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msg("已忶"); |
| | |
| | | } |
| | | /** æäº¤ä¸ä¼ æä»¶ */ |
| | | function submitFileForm() { |
| | | console.log(upload.url + '?updateSupport=' + upload.updateSupport) |
| | | console.log(upload.url + "?updateSupport=" + upload.updateSupport); |
| | | proxy.$refs["uploadRef"].submit(); |
| | | } |
| | | /**æä»¶ä¸ä¼ ä¸å¤ç */ |
| | |
| | | <template> |
| | | <div> |
| | | <el-dialog |
| | | v-model="dialogFormVisible" |
| | | <el-dialog v-model="dialogFormVisible" |
| | | :title="operationType === 'add' ? 'æ°å¢ç¦»è' : 'ç¼è¾ç¦»è'" |
| | | width="70%" |
| | | @close="closeDia" |
| | | > |
| | | @close="closeDia"> |
| | | <!-- å工信æ¯å±ç¤ºåºå --> |
| | | <div class="info-section"> |
| | | <div class="info-title">å工信æ¯</div> |
| | | <el-form :model="form" label-width="200px" label-position="left" :rules="rules" ref="formRef" style="margin-top: 20px"> |
| | | <el-form :model="form" |
| | | label-width="200px" |
| | | label-position="left" |
| | | :rules="rules" |
| | | ref="formRef" |
| | | style="margin-top: 20px"> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å§åï¼" prop="staffOnJobId"> |
| | | <el-form-item label="å§åï¼" |
| | | prop="staffOnJobId"> |
| | | <el-select v-model="form.staffOnJobId" |
| | | placeholder="è¯·éæ©äººå" |
| | | style="width: 100%" |
| | | :disabled="operationType === 'edit'" |
| | | @change="handleSelect"> |
| | | <el-option |
| | | v-for="item in personList" |
| | | <el-option v-for="item in personList" |
| | | :key="item.id" |
| | | :label="item.staffName" |
| | | :value="item.id" |
| | | /> |
| | | :value="item.id" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | |
| | | {{ currentStaffRecord.sex || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ·ç±ä½åï¼"> |
| | | {{ currentStaffRecord.nativePlace || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å²ä½ï¼"> |
| | | {{ currentStaffRecord.postName || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç°ä½åï¼"> |
| | | {{ currentStaffRecord.adress || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç¦»èæ¥æï¼" prop="leaveDate"> |
| | | <el-date-picker |
| | | v-model="form.leaveDate" |
| | | <el-form-item label="ç¦»èæ¥æï¼" |
| | | prop="leaveDate"> |
| | | <el-date-picker v-model="form.leaveDate" |
| | | type="date" |
| | | :disabled="operationType === 'edit'" |
| | | :disabled-date="disabledFutureDate" |
| | | placeholder="è¯·éæ©ç¦»èæ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | style="width: 100%" |
| | | /> |
| | | style="width: 100%" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="离èåå ï¼" prop="reason"> |
| | | <el-select v-model="form.reason" placeholder="è¯·éæ©ç¦»èåå " style="width: 100%" @change="handleSelectDimissionReason"> |
| | | <el-option |
| | | v-for="(item, index) in dimissionReasonOptions" |
| | | <el-form-item label="离èåå ï¼" |
| | | prop="reason"> |
| | | <el-select v-model="form.reason" |
| | | placeholder="è¯·éæ©ç¦»èåå " |
| | | style="width: 100%" |
| | | @change="handleSelectDimissionReason"> |
| | | <el-option v-for="(item, index) in dimissionReasonOptions" |
| | | :key="index" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | :value="item.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="夿³¨ï¼" prop="remark" v-if="form.reason === 'other'"> |
| | | <el-input |
| | | v-model="form.remark" |
| | | <el-form-item label="夿³¨ï¼" |
| | | prop="remark" |
| | | v-if="form.reason === 'other'"> |
| | | <el-input v-model="form.remark" |
| | | type="textarea" |
| | | :rows="3" |
| | | placeholder="夿³¨" |
| | | maxlength="500" |
| | | show-word-limit |
| | | /> |
| | | show-word-limit /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | |
| | | <!-- <el-row :gutter="30">--> |
| | | <!-- <el-col :span="12">--> |
| | | <!-- <div class="info-item">--> |
| | |
| | | <!-- </el-col>--> |
| | | <!-- </el-row>--> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" @click="submitForm">确认</el-button> |
| | | <el-button type="primary" |
| | | @click="submitForm">确认</el-button> |
| | | <el-button @click="closeDia">åæ¶</el-button> |
| | | </div> |
| | | </template> |
| | |
| | | <script setup> |
| | | import {ref, reactive, toRefs, getCurrentInstance} from "vue"; |
| | | import {staffOnJobListPage} from "@/api/personnelManagement/staffOnJob.js"; |
| | | import {createStaffLeave, updateStaffLeave} from "@/api/personnelManagement/staffLeave.js"; |
| | | const { proxy } = getCurrentInstance() |
| | | const emit = defineEmits(['close']) |
| | | import { |
| | | createStaffLeave, |
| | | updateStaffLeave, |
| | | } from "@/api/personnelManagement/staffLeave.js"; |
| | | const { proxy } = getCurrentInstance(); |
| | | const emit = defineEmits(["close"]); |
| | | |
| | | const dialogFormVisible = ref(false); |
| | | const operationType = ref('') |
| | | const operationType = ref(""); |
| | | const getTodayDate = () => { |
| | | const now = new Date(); |
| | | const year = now.getFullYear(); |
| | | const month = `${now.getMonth() + 1}`.padStart(2, '0'); |
| | | const day = `${now.getDate()}`.padStart(2, '0'); |
| | | const month = `${now.getMonth() + 1}`.padStart(2, "0"); |
| | | const day = `${now.getDate()}`.padStart(2, "0"); |
| | | return `${year}-${month}-${day}`; |
| | | }; |
| | | |
| | | const disabledFutureDate = (time) => { |
| | | const disabledFutureDate = time => { |
| | | const todayEnd = new Date(); |
| | | todayEnd.setHours(23, 59, 59, 999); |
| | | return time.getTime() > todayEnd.getTime(); |
| | |
| | | }, |
| | | rules: { |
| | | staffName: [{ required: true, message: "è¯·éæ©äººå" }], |
| | | leaveDate: [{ required: true, message: "è¯·éæ©ç¦»èæ¥æ", trigger: "change" }], |
| | | leaveDate: [ |
| | | { required: true, message: "è¯·éæ©ç¦»èæ¥æ", trigger: "change" }, |
| | | ], |
| | | reason: [{ required: true, message: "è¯·éæ©ç¦»èåå "}], |
| | | }, |
| | | dimissionReasonOptions: [ |
| | | {label: 'èªèµå¾
é', value: 'salary'}, |
| | | {label: 'èä¸åå±', value: 'career_development'}, |
| | | {label: 'å·¥ä½ç¯å¢', value: 'work_environment'}, |
| | | {label: '个人åå ', value: 'personal_reason'}, |
| | | {label: 'å
¶ä»', value: 'other'}, |
| | | { label: "èªèµå¾
é", value: "salary" }, |
| | | { label: "èä¸åå±", value: "career_development" }, |
| | | { label: "å·¥ä½ç¯å¢", value: "work_environment" }, |
| | | { label: "个人åå ", value: "personal_reason" }, |
| | | { label: "å
¶ä»", value: "other" }, |
| | | ], |
| | | currentStaffRecord: {}, |
| | | }); |
| | | const { form, rules, dimissionReasonOptions, currentStaffRecord } = toRefs(data); |
| | | const { form, rules, dimissionReasonOptions, currentStaffRecord } = |
| | | toRefs(data); |
| | | |
| | | // æå¼å¼¹æ¡ |
| | | const openDialog = (type, row) => { |
| | | operationType.value = type; |
| | | dialogFormVisible.value = true; |
| | | if (operationType.value === 'edit') { |
| | | currentStaffRecord.value = row |
| | | form.value.staffOnJobId = row.staffOnJobId |
| | | form.value.leaveDate = row.leaveDate |
| | | form.value.reason = row.reason |
| | | form.value.remark = row.remark |
| | | if (operationType.value === "edit") { |
| | | currentStaffRecord.value = row; |
| | | form.value.staffOnJobId = row.staffOnJobId; |
| | | form.value.leaveDate = row.leaveDate; |
| | | form.value.reason = row.reason; |
| | | form.value.remark = row.remark; |
| | | personList.value = [ |
| | | { |
| | | staffName: row.staffName, |
| | | id: row.staffOnJobId, |
| | | } |
| | | ] |
| | | }, |
| | | ]; |
| | | } else { |
| | | form.value.leaveDate = getTodayDate() |
| | | getList() |
| | | form.value.leaveDate = getTodayDate(); |
| | | getList(); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | const handleSelectDimissionReason = (val) => { |
| | | if (val === 'other') { |
| | | form.value.remark = '' |
| | | const handleSelectDimissionReason = val => { |
| | | if (val === "other") { |
| | | form.value.remark = ""; |
| | | } |
| | | } |
| | | }; |
| | | // æäº¤äº§å表å |
| | | const submitForm = () => { |
| | | form.value.staffState = 0 |
| | | if (form.value.reason !== 'other') { |
| | | form.value.remark = '' |
| | | form.value.staffState = 0; |
| | | if (form.value.reason !== "other") { |
| | | form.value.remark = ""; |
| | | } |
| | | proxy.$refs["formRef"].validate(valid => { |
| | | if (valid) { |
| | |
| | | createStaffLeave(form.value).then(res => { |
| | | proxy.$modal.msgSuccess("æäº¤æå"); |
| | | closeDia(); |
| | | }) |
| | | }); |
| | | } else { |
| | | updateStaffLeave(currentStaffRecord.value.id, form.value).then(res => { |
| | | proxy.$modal.msgSuccess("æäº¤æå"); |
| | | closeDia(); |
| | | }) |
| | | }); |
| | | } |
| | | } |
| | | }) |
| | | |
| | | } |
| | | }); |
| | | }; |
| | | // å
³éå¼¹æ¡ |
| | | const closeDia = () => { |
| | | // 表å已注éï¼æå¨éç½®è¡¨åæ°æ® |
| | |
| | | remark: "", |
| | | }; |
| | | dialogFormVisible.value = false; |
| | | emit('close') |
| | | emit("close"); |
| | | }; |
| | | |
| | | const personList = ref([]); |
| | |
| | | staffOnJobListPage({ |
| | | current: -1, |
| | | size: -1, |
| | | staffState: 1 |
| | | staffState: 1, |
| | | }).then(res => { |
| | | personList.value = res.data.records || [] |
| | | }) |
| | | personList.value = res.data.records || []; |
| | | }); |
| | | }; |
| | | |
| | | const handleSelect = (val) => { |
| | | let obj = personList.value.find(item => item.id === val) |
| | | currentStaffRecord.value = {} |
| | | const handleSelect = val => { |
| | | let obj = personList.value.find(item => item.id === val); |
| | | currentStaffRecord.value = {}; |
| | | if (obj) { |
| | | // ä¿çç¦»èæ¥æå离èåå ï¼åªæ´æ°åå·¥ä¿¡æ¯ |
| | | currentStaffRecord.value = obj |
| | | currentStaffRecord.value = obj; |
| | | } |
| | | } |
| | | }; |
| | | defineExpose({ |
| | | openDialog, |
| | | }); |
| | |
| | | <div class="search_form mb20"> |
| | | <div> |
| | | <span class="search_title">å§åï¼</span> |
| | | <el-input |
| | | v-model="searchForm.staffName" |
| | | <el-input v-model="searchForm.staffName" |
| | | style="width: 240px" |
| | | placeholder="请è¾å
¥å§åæç´¢" |
| | | @change="handleQuery" |
| | | clearable |
| | | :prefix-icon="Search" |
| | | /> |
| | | <el-button type="primary" @click="handleQuery" style="margin-left: 10px" |
| | | >æç´¢</el-button |
| | | > |
| | | :prefix-icon="Search" /> |
| | | <el-button type="primary" |
| | | @click="handleQuery" |
| | | style="margin-left: 10px">æç´¢</el-button> |
| | | </div> |
| | | <div> |
| | | <el-button type="primary" @click="openForm('add')">æ°å¢ç¦»è</el-button> |
| | | <el-button type="primary" |
| | | @click="openForm('add')">æ°å¢ç¦»è</el-button> |
| | | <el-button @click="handleOut">导åº</el-button> |
| | | <!-- <el-button type="danger" plain @click="handleDelete">å é¤</el-button> --> |
| | | </div> |
| | | </div> |
| | | <div class="table_list"> |
| | | <PIMTable |
| | | rowKey="id" |
| | | <PIMTable rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :page="page" |
| | |
| | | @selection-change="handleSelectionChange" |
| | | :tableLoading="tableLoading" |
| | | @pagination="pagination" |
| | | :total="page.total" |
| | | ></PIMTable> |
| | | :total="page.total"></PIMTable> |
| | | </div> |
| | | <form-dia ref="formDia" @close="handleQuery"></form-dia> |
| | | <form-dia ref="formDia" |
| | | @close="handleQuery"></form-dia> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import {onMounted, ref} from "vue"; |
| | | import FormDia from "@/views/personnelManagement/dimission/components/formDia.vue"; |
| | | import {findStaffLeaveListPage, batchDeleteStaffLeaves} from "@/api/personnelManagement/staffLeave.js"; |
| | | import { |
| | | findStaffLeaveListPage, |
| | | batchDeleteStaffLeaves, |
| | | } from "@/api/personnelManagement/staffLeave.js"; |
| | | import {ElMessageBox} from "element-plus"; |
| | | |
| | | const data = reactive({ |
| | |
| | | label: "ç¶æ", |
| | | prop: "staffState", |
| | | dataType: "tag", |
| | | formatData: (params) => { |
| | | formatData: params => { |
| | | if (params == 0) { |
| | | return "离è"; |
| | | } else if (params == 1) { |
| | |
| | | return null; |
| | | } |
| | | }, |
| | | formatType: (params) => { |
| | | formatType: params => { |
| | | if (params == 0) { |
| | | return "danger"; |
| | | } else if (params == 1) { |
| | |
| | | prop: "sex", |
| | | }, |
| | | { |
| | | label: "æ·ç±ä½å", |
| | | prop: "nativePlace", |
| | | }, |
| | | { |
| | | label: "é¨é¨", |
| | | prop: "deptName", |
| | | }, |
| | |
| | | prop: "postName", |
| | | }, |
| | | { |
| | | label: "ç°ä½å", |
| | | prop: "adress", |
| | | width:200 |
| | | }, |
| | | { |
| | | label: "第ä¸å¦å", |
| | | prop: "firstStudy", |
| | | }, |
| | | { |
| | | label: "ä¸ä¸", |
| | | prop: "profession", |
| | | width:100 |
| | | width: 100, |
| | | }, |
| | | { |
| | | label: "å¹´é¾", |
| | |
| | | { |
| | | label: "èç³»çµè¯", |
| | | prop: "phone", |
| | | width:150 |
| | | width: 150, |
| | | }, |
| | | { |
| | | label: "ç´§æ¥è系人", |
| | | prop: "emergencyContact", |
| | | width: 120 |
| | | width: 120, |
| | | }, |
| | | { |
| | | label: "ç´§æ¥è系人çµè¯", |
| | | prop: "emergencyContactPhone", |
| | | width:150 |
| | | width: 150, |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: 'right', |
| | | fixed: "right", |
| | | operation: [ |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | clickFun: row => { |
| | | openForm("edit", row); |
| | | }, |
| | | }, |
| | |
| | | size: 100, |
| | | total: 0, |
| | | }); |
| | | const formDia = ref() |
| | | const { proxy } = getCurrentInstance() |
| | | |
| | | const formDia = ref(); |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | // æ¥è¯¢å表 |
| | | /** æç´¢æé®æä½ */ |
| | |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | | const pagination = (obj) => { |
| | | const pagination = obj => { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | getList(); |
| | | }; |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | findStaffLeaveListPage({...page, ...searchForm.value}).then(res => { |
| | | findStaffLeaveListPage({ ...page, ...searchForm.value }) |
| | | .then(res => { |
| | | tableLoading.value = false; |
| | | tableData.value = res.data.records |
| | | tableData.value = res.data.records; |
| | | page.total = res.data.total; |
| | | }).catch(err => { |
| | | tableLoading.value = false; |
| | | }) |
| | | .catch(err => { |
| | | tableLoading.value = false; |
| | | }); |
| | | }; |
| | | // è¡¨æ ¼éæ©æ°æ® |
| | | const handleSelectionChange = (selection) => { |
| | | const handleSelectionChange = selection => { |
| | | selectedRows.value = selection; |
| | | }; |
| | | |
| | | // æå¼å¼¹æ¡ |
| | | const openForm = (type, row) => { |
| | | nextTick(() => { |
| | | formDia.value?.openDialog(type, row) |
| | | }) |
| | | formDia.value?.openDialog(type, row); |
| | | }); |
| | | }; |
| | | |
| | | // å é¤ |
| | | const handleDelete = () => { |
| | | let ids = []; |
| | | if (selectedRows.value.length > 0) { |
| | | ids = selectedRows.value.map((item) => item.id); |
| | | ids = selectedRows.value.map(item => item.id); |
| | | } else { |
| | | proxy.$modal.msgWarning("è¯·éæ©æ°æ®"); |
| | | return; |
| | |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | batchDeleteStaffLeaves(ids).then((res) => { |
| | | batchDeleteStaffLeaves(ids).then(res => { |
| | | proxy.$modal.msgSuccess("å 餿å"); |
| | | getList(); |
| | | }); |
| | |
| | | prop: "staffNo", |
| | | }, |
| | | { |
| | | label: "é¨é¨", |
| | | prop: "deptName", |
| | | }, |
| | | { |
| | | label: "å§å", |
| | | prop: "staffName", |
| | | }, |
| | |
| | | <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"; |
| | | |
| | |
| | | }); |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | const userStore = useUserStore(); |
| | | |
| | | const dialogVisible = computed({ |
| | | get: () => props.modelValue, |
| | |
| | | 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; |
| | |
| | | <template> |
| | | <FormDialog |
| | | v-model="dialogVisible" |
| | | <FormDialog v-model="dialogVisible" |
| | | :title="operationType === 'add' ? 'æ°å»ºå·¥èµè¡¨' : 'ç¼è¾å·¥èµè¡¨'" |
| | | width="90%" |
| | | @close="closeDia" |
| | | > |
| | | @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" |
| | | <el-form-item label="å·¥èµä¸»é¢" |
| | | prop="salaryTitle"> |
| | | <el-input v-model="form.salaryTitle" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="20" |
| | | show-word-limit |
| | | /> |
| | | 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-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="éæ©å·¥èµæä»½" prop="salaryMonth"> |
| | | <el-date-picker |
| | | v-model="form.salaryMonth" |
| | | <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 |
| | | /> |
| | | clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="夿³¨" prop="remark"> |
| | | <el-input |
| | | v-model="form.remark" |
| | | placeholder="请è¾å
¥" |
| | | <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="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" |
| | | <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" |
| | | style="width: 100%"> |
| | | <el-option v-for="b in bankOptions" |
| | | :key="b" |
| | | :label="b" |
| | | :value="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" |
| | | <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" |
| | | style="width: 100%"> |
| | | <el-option v-for="item in userList" |
| | | :key="item.userId" |
| | | :label="item.nickName" |
| | | :value="item.userId" |
| | | /> |
| | | :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" |
| | | <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"> |
| | | @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" |
| | | <el-input v-model.number="row.basicSalary" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.basicSalary = parseNum(row.basicSalary)" |
| | | /> |
| | | @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" |
| | | <el-input v-model.number="row.pieceSalary" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.pieceSalary = parseNum(row.pieceSalary)" |
| | | /> |
| | | 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" |
| | | <el-input v-model.number="row.hourlySalary" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.hourlySalary = parseNum(row.hourlySalary)" |
| | | /> |
| | | 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" |
| | | <el-input v-model.number="row.otherIncome" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.otherIncome = parseNum(row.otherIncome)" |
| | | /> |
| | | @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" |
| | | <el-input v-model.number="row.socialPersonal" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.socialPersonal = parseNum(row.socialPersonal)" |
| | | /> |
| | | 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" |
| | | <el-input v-model.number="row.fundPersonal" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.fundPersonal = parseNum(row.fundPersonal)" |
| | | /> |
| | | 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" |
| | | <el-input v-model.number="row.otherDeduct" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.otherDeduct = parseNum(row.otherDeduct)" |
| | | /> |
| | | 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" |
| | | <el-input v-model.number="row.socialSupplementAmount" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.salaryTax = parseNum(row.salaryTax)" |
| | | /> |
| | | @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" |
| | | <el-input v-model.number="row.salaryTax" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.grossSalary = parseNum(row.grossSalary)" |
| | | /> |
| | | 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" |
| | | <el-input v-model.number="row.grossSalary" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.deductSalary = parseNum(row.deductSalary)" |
| | | /> |
| | | 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" |
| | | <el-input v-model.number="row.deductSalary" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.netSalary = parseNum(row.netSalary)" |
| | | /> |
| | | 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" |
| | | <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="夿³¨" |
| | | minWidth="120"> |
| | | <template #default="{ row }"> |
| | | <el-input v-model="row.remark" |
| | | placeholder="请è¾å
¥" |
| | | size="small" |
| | | /> |
| | | size="small" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="80" align="center" fixed="right"> |
| | | <el-table-column label="æä½" |
| | | width="80" |
| | | align="center" |
| | | fixed="right"> |
| | | <template #default="{ row }"> |
| | | <el-button type="primary" link @click="removeEmployee(row)">å é¤</el-button> |
| | | <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" |
| | | <el-dialog v-model="addPersonVisible" |
| | | title="æ°å¢äººå" |
| | | width="400px" |
| | | append-to-body |
| | | @close="addPersonClose" |
| | | > |
| | | @close="addPersonClose"> |
| | | <div class="add-person-tree"> |
| | | <el-tree |
| | | ref="personTreeRef" |
| | | <el-tree ref="personTreeRef" |
| | | :data="deptStaffTree" |
| | | show-checkbox |
| | | node-key="id" |
| | | :props="{ label: 'label', children: 'children' }" |
| | | default-expand-all |
| | | /> |
| | | 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" |
| | | <el-dialog v-model="taxDialogVisible" |
| | | title="个ç¨è¡¨" |
| | | width="700px" |
| | | append-to-body |
| | | > |
| | | 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" |
| | | <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" |
| | | min-width="220" /> |
| | | <el-table-column prop="rate" |
| | | label="ç¨ç(%)" |
| | | width="100" |
| | | align="center" |
| | | /> |
| | | <el-table-column |
| | | prop="quickDeduction" |
| | | align="center" /> |
| | | <el-table-column prop="quickDeduction" |
| | | label="éç®æ£é¤æ°/å
" |
| | | width="160" |
| | | align="center" |
| | | /> |
| | | align="center" /> |
| | | </el-table> |
| | | </el-dialog> |
| | | </FormDialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, toRefs, computed, getCurrentInstance, nextTick } from "vue"; |
| | | 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"; |
| | |
| | | staffSalaryMainAdd, |
| | | staffSalaryMainUpdate, |
| | | staffSalaryMainCalculateSalary, |
| | | staffSalaryMainCalculateByEmployeeId, |
| | | staffSalaryMainAvailableDepts, |
| | | } from "@/api/personnelManagement/staffSalaryMain.js"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user.js"; |
| | | |
| | | |
| | | const emit = defineEmits(["update:modelValue", "close"]); |
| | | const props = defineProps({ |
| | |
| | | |
| | | const dialogVisible = computed({ |
| | | get: () => props.modelValue, |
| | | set: (val) => emit("update:modelValue", val), |
| | | set: val => emit("update:modelValue", val), |
| | | }); |
| | | |
| | | const formRef = 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 }, |
| | |
| | | return isNaN(n) ? 0 : n; |
| | | } |
| | | |
| | | function parseNullableNum(v) { |
| | | if (v === "" || v == null) return null; |
| | | const n = Number(v); |
| | | return isNaN(n) ? null : n; |
| | | } |
| | | |
| | | const createEmployeeRow = () => ({ |
| | | staffOnJobId: undefined, |
| | | id: undefined, |
| | | 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: "", |
| | | }); |
| | | |
| | | 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 ?? "", |
| | | }); |
| | | |
| | | const applyBackendRow = (targetRow, sourceRow) => { |
| | | Object.assign(targetRow, normalizeEmployeeRow(sourceRow, targetRow)); |
| | | }; |
| | | |
| | | const clearRowRecalcState = row => { |
| | | const key = String(row?.staffOnJobId ?? row?.id ?? ""); |
| | | if (!key) return; |
| | | clearTimeout(recalcTimers.get(key)); |
| | | recalcTimers.delete(key); |
| | | recalcVersions.delete(key); |
| | | }; |
| | | |
| | | 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 || {}); |
| | | }); |
| | | }, 250) |
| | | ); |
| | | }; |
| | | |
| | | const handleSalaryInput = row => { |
| | | row.basicSalary = parseNum(row.basicSalary); |
| | | row.otherIncome = parseNum(row.otherIncome); |
| | | requestEmployeeRecalculate(row); |
| | | }; |
| | | |
| | | // åºç¡èµæè¡¨å |
| | | const data = reactive({ |
| | | form: { |
| | |
| | | auditUserId: undefined, |
| | | }, |
| | | rules: { |
| | | salaryTitle: [{ required: true, message: "请è¾å
¥å·¥èµä¸»é¢", trigger: "blur" }], |
| | | salaryTitle: [ |
| | | { required: true, message: "请è¾å
¥å·¥èµä¸»é¢", trigger: "blur" }, |
| | | ], |
| | | deptIds: [{ required: true, message: "è¯·éæ©é¨é¨", trigger: "change" }], |
| | | salaryMonth: [{ required: true, message: "è¯·éæ©å·¥èµæä»½", trigger: "change" }], |
| | | auditUserId: [{ required: true, message: "è¯·éæ©å®¡æ ¸äºº", trigger: "change" }], |
| | | salaryMonth: [ |
| | | { required: true, message: "è¯·éæ©å·¥èµæä»½", trigger: "change" }, |
| | | ], |
| | | auditUserId: [ |
| | | { required: true, message: "è¯·éæ©å®¡æ ¸äºº", trigger: "change" }, |
| | | ], |
| | | }, |
| | | }); |
| | | const { form, rules } = toRefs(data); |
| | |
| | | }); |
| | | |
| | | const loadBankOptions = () => { |
| | | return bankList().then((res) => { |
| | | 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 !== ""); |
| | | .map(b => (b?.bankName == null ? "" : String(b.bankName).trim())) |
| | | .filter(v => v !== ""); |
| | | }); |
| | | }; |
| | | |
| | | const loadUserList = () => { |
| | | return userListNoPageByTenantId().then((res) => { |
| | | return userListNoPageByTenantId().then(res => { |
| | | userList.value = res.data || []; |
| | | }); |
| | | }; |
| | |
| | | // æå¹³åé¨é¨æ ä¾ä¸æä½¿ç¨ |
| | | function flattenDept(tree, list = []) { |
| | | if (!tree?.length) return list; |
| | | tree.forEach((node) => { |
| | | tree.forEach(node => { |
| | | list.push({ deptId: node.deptId, deptName: node.deptName }); |
| | | if (node.children?.length) flattenDept(node.children, list); |
| | | }); |
| | | return list; |
| | | } |
| | | |
| | | const loadDeptOptions = () => { |
| | | listDept().then((res) => { |
| | | // å è½½ææé¨é¨ |
| | | const loadAllDeptOptions = () => { |
| | | listDept().then(res => { |
| | | const tree = res.data ?? []; |
| | | deptOptions.value = flattenDept(tree); |
| | | }); |
| | | }; |
| | | |
| | | // å è½½å¯ç¨é¨é¨å表ï¼ç¨äºå¤æé¨é¨æ¯å¦å¯éï¼ |
| | | const loadAvailableDepts = (salaryMonth) => { |
| | | if (!salaryMonth) { |
| | | availableDeptIds.value = new Set(); |
| | | return; |
| | | } |
| | | staffSalaryMainAvailableDepts(salaryMonth).then(res => { |
| | | const availableDepts = Array.isArray(res.data) ? res.data : []; |
| | | availableDeptIds.value = new Set(availableDepts.map(d => d.deptId)); |
| | | }); |
| | | }; |
| | | |
| | | // 夿é¨é¨æ¯å¦ç¦ç¨ |
| | | const isDeptDisabled = (deptId) => { |
| | | // å¦ææ²¡æéæ©æä»½ï¼ææé¨é¨é½å¯é |
| | | if (!form.value.salaryMonth) return false; |
| | | // 妿已鿩æä»½ï¼åªæå¨å¯ç¨é¨é¨å表ä¸çé¨é¨æå¯é |
| | | return !availableDeptIds.value.has(deptId); |
| | | }; |
| | | |
| | | // æå»º é¨é¨-äººå æ ï¼ç¨äºæ°å¢äººåå¼¹çªï¼ |
| | |
| | | const staffList = staffRes.data ?? []; |
| | | const deptMap = new Map(); |
| | | function walk(nodes) { |
| | | nodes.forEach((node) => { |
| | | nodes.forEach(node => { |
| | | deptMap.set(node.deptId, { |
| | | id: "dept_" + node.deptId, |
| | | deptId: node.deptId, |
| | |
| | | }); |
| | | } |
| | | walk(tree); |
| | | staffList.forEach((s) => { |
| | | staffList.forEach(s => { |
| | | const deptId = s.deptId ?? s.dept_id; |
| | | const node = deptMap.get(deptId); |
| | | if (node) { |
| | |
| | | } |
| | | }); |
| | | deptStaffTree.value = Array.from(deptMap.values()).filter( |
| | | (n) => n.children && n.children.length > 0 |
| | | n => n.children && n.children.length > 0 |
| | | ); |
| | | }); |
| | | }; |
| | | |
| | | const openDialog = (type, row) => { |
| | | nextTick(() => { |
| | | loadDeptOptions(); |
| | | resetRecalcState(); |
| | | loadBankOptions(); |
| | | loadUserList(); |
| | | loadAllDeptOptions(); |
| | | employeeList.value = []; |
| | | Object.assign(form.value, { |
| | | id: undefined, |
| | |
| | | form.value.salaryTitle = row.salaryTitle ?? ""; |
| | | // deptIds å端æ¯å符串ï¼å¤ä¸ªç¨éå·åéï¼ï¼å½å表å仿¯åé deptId |
| | | form.value.deptIds = row.deptIds |
| | | ? String(row.deptIds).split(",").map((id) => Number(id.trim())).filter(Boolean) |
| | | ? String(row.deptIds) |
| | | .split(",") |
| | | .map(id => Number(id.trim())) |
| | | .filter(Boolean) |
| | | : []; |
| | | form.value.salaryMonth = row.salaryMonth ?? ""; |
| | | form.value.remark = row.remark ?? ""; |
| | |
| | | |
| | | // 妿æåå·¥æç»æ°æ®ï¼ç´æ¥åæ¾ |
| | | 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 ?? "", |
| | | })); |
| | | 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(); |
| | |
| | | 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 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({ |
| | | const newRow = normalizeEmployeeRow({ |
| | | 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: "", |
| | | socialSupplementAmount: null, |
| | | }); |
| | | employeeList.value.push(newRow); |
| | | requestEmployeeRecalculate(newRow); |
| | | }); |
| | | addPersonVisible.value = false; |
| | | }; |
| | | |
| | | const removeEmployee = (row) => { |
| | | const removeEmployee = row => { |
| | | clearRowRecalcState(row); |
| | | employeeList.value = employeeList.value.filter( |
| | | (e) => (e.staffOnJobId || e.id) !== (row.staffOnJobId || row.id) |
| | | e => (e.staffOnJobId || e.id) !== (row.staffOnJobId || row.id) |
| | | ); |
| | | }; |
| | | |
| | | const onEmployeeSelectionChange = (selection) => { |
| | | const onEmployeeSelectionChange = selection => { |
| | | selectedEmployees.value = selection; |
| | | }; |
| | | |
| | |
| | | proxy.$modal.msgWarning("请å
å¾éè¦å é¤çåå·¥"); |
| | | return; |
| | | } |
| | | const ids = new Set(selectedEmployees.value.map((e) => e.staffOnJobId || e.id)); |
| | | 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) |
| | | e => !ids.has(e.staffOnJobId || e.id) |
| | | ); |
| | | }; |
| | | |
| | |
| | | ids: form.value.deptIds, |
| | | date: form.value.salaryMonth, |
| | | }; |
| | | staffSalaryMainCalculateSalary(payload).then((res) => { |
| | | staffSalaryMainCalculateSalary(payload).then(res => { |
| | | const list = Array.isArray(res?.data) ? res.data : []; |
| | | if (!list.length) { |
| | | proxy.$modal.msgWarning("æªè®¡ç®å°å·¥èµæ°æ®"); |
| | | 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 ?? "", |
| | | })); |
| | | resetRecalcState(); |
| | | employeeList.value = list.map(e => normalizeEmployeeRow(e)); |
| | | proxy.$modal.msgSuccess("çææå"); |
| | | }); |
| | | }; |
| | | |
| | | const handleClear = () => { |
| | | proxy.$modal.confirm("ç¡®å®æ¸
空å½ååå·¥å表åï¼").then(() => { |
| | | proxy.$modal |
| | | .confirm("ç¡®å®æ¸
空å½ååå·¥å表åï¼") |
| | | .then(() => { |
| | | resetRecalcState(); |
| | | employeeList.value = []; |
| | | }).catch(() => {}); |
| | | }) |
| | | .catch(() => {}); |
| | | }; |
| | | |
| | | const handleTaxForm = () => { |
| | |
| | | }; |
| | | |
| | | const submitForm = () => { |
| | | formRef.value?.validate((valid) => { |
| | | formRef.value?.validate(valid => { |
| | | if (!valid) return; |
| | | saveData(3); // 确认æäº¤ï¼ç¶æä¸º3ï¼å¾
å®¡æ ¸ï¼ |
| | | }); |
| | | }; |
| | | |
| | | const saveDraft = () => { |
| | | formRef.value?.validate((valid) => { |
| | | formRef.value?.validate(valid => { |
| | | if (!valid) return; |
| | | saveData(1); // ä¿åè稿ï¼ç¶æä¸º1ï¼èç¨¿ï¼ |
| | | }); |
| | | }; |
| | | |
| | | const saveData = (status) => { |
| | | const saveData = status => { |
| | | const payload = { |
| | | id: form.value.id, |
| | | salaryTitle: form.value.salaryTitle, |
| | |
| | | auditUserId: form.value.auditUserId, |
| | | auditUserName: auditUserName.value, |
| | | totalSalary: totalSalary.value, |
| | | staffSalaryDetailList: employeeList.value.map((e) => ({ |
| | | staffSalaryDetailList: employeeList.value.map(e => ({ |
| | | staffOnJobId: e.staffOnJobId ?? e.staffId ?? e.id, |
| | | staffName: e.staffName, |
| | | postName: e.postName ?? "", |
| | |
| | | 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), |
| | |
| | | }; |
| | | |
| | | const closeDia = () => { |
| | | resetRecalcState(); |
| | | dialogVisible.value = false; |
| | | emit("close"); |
| | | }; |
| | |
| | | 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"; |
| | |
| | | { |
| | | 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), |
| | | }, |
| | | { |
| | |
| | | const operationType = ref("add"); |
| | | const currentRow = ref({}); |
| | | const { proxy } = getCurrentInstance(); |
| | | const userStore = useUserStore(); |
| | | const bankSetting = ref({}); |
| | | const bankDialogVisible = ref(false); |
| | | const bankDiaRef = ref(null); |
| | |
| | | |
| | | const handleExport = () => { |
| | | proxy.download( |
| | | "/compensationPerformance/export", |
| | | { ...searchForm.value, current: page.current, size: page.size }, |
| | | "/staffSalaryMain/export", |
| | | { ...searchForm.value }, |
| | | "å·¥èµè¡¨.xlsx" |
| | | ); |
| | | }; |
| | |
| | | <template> |
| | | <div> |
| | | <FormDialog |
| | | v-model="dialogFormVisible" |
| | | <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"> |
| | | @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" |
| | | <el-checkbox-group v-model="form.deptIds" |
| | | :disabled="isDetail"> |
| | | <div v-for="dept in deptList" |
| | | :key="dept.deptId" |
| | | class="dept-checkbox-item" |
| | | > |
| | | 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> |
| | |
| | | <!-- å³ä¾§ï¼åºç¡ä¿¡æ¯ + ä¿é©ç±»å --> |
| | | <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" |
| | | <el-form-item label="æ¹æ¡æ é¢ï¼" |
| | | prop="title"> |
| | | <el-input v-model="form.title" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | :disabled="isDetail" |
| | | /> |
| | | :disabled="isDetail" /> |
| | | </el-form-item> |
| | | <el-form-item label="夿³¨ï¼" prop="remark"> |
| | | <el-input |
| | | v-model="form.remark" |
| | | <el-form-item label="夿³¨ï¼" |
| | | prop="remark"> |
| | | <el-input v-model="form.remark" |
| | | type="textarea" |
| | | :rows="2" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | :disabled="isDetail" |
| | | /> |
| | | :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" |
| | | <el-button v-if="!isDetail" |
| | | type="primary" |
| | | size="small" |
| | | @click="addInsuranceBenefit" |
| | | > |
| | | @click="addInsuranceBenefit"> |
| | | æ·»å ä¿é©ç¦å© |
| | | </el-button> |
| | | </template> |
| | | <el-row :gutter="16"> |
| | | <el-col |
| | | v-for="(item, index) in form.insuranceBenefits" |
| | | <el-col v-for="(item, index) in form.insuranceBenefits" |
| | | :key="item._key" |
| | | :span="12" |
| | | > |
| | | :span="12"> |
| | | <div class="insurance-benefit-card"> |
| | | <div class="insurance-benefit-title"> |
| | | ä¿é©ç¦å©{{ index + 1 }} |
| | | <el-button |
| | | v-if="!isDetail && form.insuranceBenefits.length > 1" |
| | | <el-button v-if="!isDetail && form.insuranceBenefits.length > 1" |
| | | type="danger" |
| | | link |
| | | size="small" |
| | | class="card-delete-btn" |
| | | @click="removeInsuranceBenefit(index)" |
| | | > |
| | | @click="removeInsuranceBenefit(index)"> |
| | | å é¤ |
| | | </el-button> |
| | | </div> |
| | | <el-form-item |
| | | :prop="'insuranceBenefits.' + index + '.insuranceType'" |
| | | <el-form-item :prop="'insuranceBenefits.' + index + '.insuranceType'" |
| | | label="ä¿é©ç±»åï¼" |
| | | label-width="100px" |
| | | :rules="[{ required: true, message: 'è¯·éæ©ä¿é©ç±»å', trigger: 'blur' }]" |
| | | > |
| | | <el-select |
| | | v-model="item.insuranceType" |
| | | <el-select v-model="item.insuranceType" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | style="width: 100%" |
| | | :disabled="isDetail" |
| | | > |
| | | <el-option |
| | | v-for="opt in insuranceTypeOptions" |
| | | :disabled="isDetail"> |
| | | <el-option v-for="opt in insuranceTypeOptions" |
| | | :key="opt.value" |
| | | :label="opt.label" |
| | | :value="opt.value" |
| | | /> |
| | | :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" |
| | | <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" |
| | | > |
| | | @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" |
| | | <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" |
| | | <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> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, toRefs, getCurrentInstance, nextTick, computed } from "vue"; |
| | | 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 { |
| | | socialSecurityAdd, |
| | | socialSecurityUpdate, |
| | | } from "@/api/personnelManagement/socialSecuritySet.js"; |
| | | |
| | | const emit = defineEmits(["close"]); |
| | | const { proxy } = getCurrentInstance(); |
| | |
| | | |
| | | function flattenDept(tree, list = []) { |
| | | if (!tree || !tree.length) return list; |
| | | tree.forEach((node) => { |
| | | tree.forEach(node => { |
| | | list.push({ |
| | | deptId: node.deptId, |
| | | deptName: node.deptName, |
| | |
| | | } |
| | | |
| | | const loadDeptList = () => { |
| | | listDept().then((res) => { |
| | | listDept().then(res => { |
| | | const tree = res.data ?? []; |
| | | deptList.value = flattenDept(tree); |
| | | }); |
| | |
| | | form.value.insuranceBenefits.push(defaultBenefit()); |
| | | }; |
| | | |
| | | const removeInsuranceBenefit = (index) => { |
| | | const removeInsuranceBenefit = index => { |
| | | form.value.insuranceBenefits.splice(index, 1); |
| | | }; |
| | | |
| | | const handleUseBasicSalaryChange = (item) => { |
| | | const handleUseBasicSalaryChange = (item, index) => { |
| | | if (item.useBasicSalary) { |
| | | item.paymentBase = ""; |
| | | } |
| | | nextTick(() => { |
| | | formRef.value?.validateField(`insuranceBenefits.${index}.paymentBase`); |
| | | }); |
| | | }; |
| | | |
| | | const handlePaymentBaseInput = (item) => { |
| | | 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(); |
| | | } |
| | | }; |
| | | |
| | |
| | | if (d.deptIds) { |
| | | form.value.deptIds = String(d.deptIds) |
| | | .split(",") |
| | | .filter((v) => v !== "") |
| | | .map((v) => { |
| | | .filter(v => v !== "") |
| | | .map(v => { |
| | | const num = Number(v); |
| | | return Number.isNaN(num) ? v : num; |
| | | }); |
| | |
| | | const detailList = d.schemeInsuranceDetailList || []; |
| | | form.value.insuranceBenefits = |
| | | detailList.length > 0 |
| | | ? detailList.map((b) => ({ |
| | | ? detailList.map(b => ({ |
| | | _key: Math.random().toString(36).slice(2), |
| | | insuranceType: b.insuranceType || "", |
| | | paymentBase: b.paymentBase ?? "", |
| | |
| | | closeDia(); |
| | | return; |
| | | } |
| | | formRef.value?.validate((valid) => { |
| | | formRef.value?.validate(valid => { |
| | | if (!valid) return; |
| | | const deptIds = |
| | | Array.isArray(form.value.deptIds) && form.value.deptIds.length |
| | |
| | | }) |
| | | ); |
| | | const insuranceTypes = schemeInsuranceDetailList |
| | | .map((item) => item.insuranceType) |
| | | .filter((v) => v) |
| | | .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) |
| | | .filter(d => |
| | | (form.value.deptIds || []).some(id => String(id) === String(d.deptId)) |
| | | ) |
| | | ) |
| | | .map((d) => d.deptName) |
| | | .map(d => d.deptName) |
| | | .join(","); |
| | | const submitData = { |
| | | id: form.value.id, |
| | |
| | | prop="paymentAmount" |
| | | show-overflow-tooltip |
| | | :formatter="formattedNumber" /> |
| | | <el-table-column label="éè´§éé¢(å
)" |
| | | prop="returnAmount" |
| | | show-overflow-tooltip |
| | | :formatter="formattedNumber" /> |
| | | <el-table-column label="åºä»éé¢(å
)" |
| | | prop="payableAmount" |
| | | show-overflow-tooltip> |
| | |
| | | }, |
| | | }, |
| | | { |
| | | label: "éè´§éé¢(å
)", |
| | | prop: "returnAmount", |
| | | width: 200, |
| | | formatData: params => { |
| | | return params ? parseFloat(params).toFixed(2) : 0; |
| | | }, |
| | | }, |
| | | { |
| | | label: "åºä»éé¢(å
)", |
| | | dataType: "slot", |
| | | width: 200, |
| | |
| | | const summarizeMainTable = param => { |
| | | return proxy.summarizeTable( |
| | | param, |
| | | ["contractAmounts", "paymentAmount", "payableAmount"], |
| | | ["contractAmounts", "paymentAmount", "returnAmount", "payableAmount"], |
| | | { |
| | | ticketsNum: { noDecimal: true }, // ä¸ä¿çå°æ° |
| | | futureTickets: { noDecimal: true }, // ä¸ä¿çå°æ° |
| | |
| | | const summarizeMainTable1 = param => { |
| | | let summarizeTable = proxy.summarizeTable( |
| | | param, |
| | | ["contractAmount", "invoiceAmount", "paymentAmount"], |
| | | ["contractAmount", "invoiceAmount", "paymentAmount", "returnAmount"], |
| | | { |
| | | ticketsNum: { noDecimal: true }, // ä¸ä¿çå°æ° |
| | | futureTickets: { noDecimal: true }, // ä¸ä¿çå°æ° |
| | |
| | | </el-table-column> |
| | | <el-table-column label="æ°é" |
| | | prop="quantity" /> |
| | | <el-table-column label="éå®åè´§æ°é" |
| | | prop="shippedQuantity" /> |
| | | <el-table-column label="å¯ç¨æ°é" |
| | | prop="availableQuality" /> |
| | | <el-table-column label="éè´§æ°é" |
| | | prop="returnQuality" /> |
| | | prop="returnQuantity" /> |
| | | <el-table-column label="ç¨ç(%)" |
| | | prop="taxRate" /> |
| | | <el-table-column label="å«ç¨åä»·(å
)" |
| | |
| | | width="200" |
| | | show-overflow-tooltip |
| | | :formatter="formattedNumber" /> |
| | | <el-table-column label="å®é
ååéé¢(å
)" |
| | | prop="netContractAmount" |
| | | width="200" |
| | | show-overflow-tooltip |
| | | :formatter="(_, __, cellValue) => formatMoney(cellValue)" /> |
| | | <el-table-column label="å½å
¥äºº" |
| | | prop="recorderName" |
| | | width="120" |
| | |
| | | <el-button link |
| | | type="primary" |
| | | @click="openForm('edit', scope.row)" |
| | | :disabled="scope.row.stockInStatus === 'å®å
¨å
¥åº'">ç¼è¾ |
| | | :disabled="scope.row.approvalStatus === 3">ç¼è¾ |
| | | </el-button> |
| | | <el-button link |
| | | type="primary" |
| | |
| | | }; |
| | | // 主表åè®¡æ¹æ³ |
| | | const summarizeMainTable = param => { |
| | | return proxy.summarizeTable(param, ["contractAmount"]); |
| | | return proxy.summarizeTable(param, ["contractAmount", "netContractAmount"]); |
| | | }; |
| | | const formatMoney = value => { |
| | | if (value === null || value === undefined || value === "") { |
| | | return "--"; |
| | | } |
| | | const num = Number(value); |
| | | return Number.isFinite(num) ? num.toFixed(2) : "--"; |
| | | }; |
| | | // å表åè®¡æ¹æ³ |
| | | const summarizeProTable = param => { |
| | |
| | | }; |
| | | // å é¤äº§å |
| | | const deleteProduct = () => { |
| | | if (productData.value.length <= 1) { |
| | | proxy.$modal.msgWarning("éè´å°è´¦è³å°éè¦ä¿ç䏿¡äº§åæ°æ®"); |
| | | return; |
| | | } |
| | | if (productSelectedRows.value.length === 0) { |
| | | proxy.$modal.msgWarning("è¯·éæ©æ°æ®"); |
| | | return; |
| | |
| | | </el-form-item> |
| | | |
| | | <el-form-item |
| | | label="åè´§ç±»å" |
| | | prop="shippingType" |
| | | :rules="[ |
| | | { |
| | | required: true, |
| | | message: 'è¯·éæ©åè´§ç±»å', |
| | | trigger: 'change', |
| | | } |
| | | ]" |
| | | > |
| | | <el-select |
| | | v-model="formState.shippingType" |
| | | placeholder="è¯·éæ©åè´§ç±»å" |
| | | style="width: 240px" |
| | | @change="handleShippingTypeChange" |
| | | > |
| | | <el-option label="货车" :value="1" /> |
| | | <el-option label="å¿«é" :value="2" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item |
| | | v-if="formState.shippingType === 1" |
| | | label="å货车çå·" |
| | | prop="truckPlateNo" |
| | | > |
| | | <el-input |
| | | v-model="formState.truckPlateNo" |
| | | placeholder="请è¾å
¥å货车çå·" |
| | | style="width: 240px" |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <template v-else-if="formState.shippingType === 2"> |
| | | <el-form-item |
| | | label="å¿«éå
¬å¸" |
| | | prop="expressCompany" |
| | | > |
| | | <el-input |
| | | v-model="formState.expressCompany" |
| | | placeholder="请è¾å
¥å¿«éå
¬å¸" |
| | | style="width: 240px" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item |
| | | label="å¿«éåå·" |
| | | prop="expressNo" |
| | | > |
| | | <el-input |
| | | v-model="formState.expressNo" |
| | | placeholder="请è¾å
¥å¿«éåå·" |
| | | style="width: 240px" |
| | | /> |
| | | </el-form-item> |
| | | </template> |
| | | |
| | | <el-form-item |
| | | label="ä¾åºååç§°" |
| | | prop="supplierId" |
| | | :rules="[ |
| | |
| | | <el-table-column label="æ°é" |
| | | prop="stockInNum" |
| | | width="100" /> |
| | | <el-table-column label="éå®åè´§æ°é" |
| | | prop="saleOutQuantity" |
| | | width="120" /> |
| | | <el-table-column label="å¯éè´§æ°é" |
| | | prop="unQuantity" |
| | | width="130" /> |
| | | <el-table-column label="å·²éè´§æ°é" |
| | | width="130"> |
| | | <template #default="scope"> |
| | | {{ calcAlreadyReturned(scope.row) }} |
| | | {{ formattedNumber(scope.row, null, scope.row.totalReturnNum || 0) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="éè´§æ°é" |
| | |
| | | <script setup> |
| | | import {ref, computed, getCurrentInstance, watch, defineAsyncComponent} from "vue"; |
| | | import {createPurchaseReturnOrder} from "@/api/procurementManagement/purchase_return_order.js"; |
| | | import {getOptions, purchaseList} from "@/api/procurementManagement/procurementLedger.js"; |
| | | import {getOptions, purchaseReturnableList} from "@/api/procurementManagement/procurementLedger.js"; |
| | | import {userListNoPageByTenantId} from "@/api/system/user.js"; |
| | | const ProductList = defineAsyncComponent(() => import("@/views/procurementManagement/purchaseReturnOrder/ProductList.vue")); |
| | | const props = defineProps({ |
| | |
| | | no: '', |
| | | isDefaultNo: true, |
| | | returnType: 0, |
| | | shippingType: undefined, |
| | | truckPlateNo: '', |
| | | expressCompany: '', |
| | | expressNo: '', |
| | | incomeType: undefined, |
| | | remark: '', |
| | | supplierId: undefined, |
| | |
| | | // æ¯å¦å±ç¤ºäº§ååè¡¨æ°æ® |
| | | const isShowProductsModal = ref(false) |
| | | |
| | | const handleShippingTypeChange = (val) => { |
| | | if (val === 1) { |
| | | formState.value.expressCompany = ''; |
| | | formState.value.expressNo = ''; |
| | | } else if (val === 2) { |
| | | formState.value.truckPlateNo = ''; |
| | | } else { |
| | | formState.value.truckPlateNo = ''; |
| | | formState.value.expressCompany = ''; |
| | | formState.value.expressNo = ''; |
| | | } |
| | | }; |
| | | |
| | | const isShow = computed({ |
| | | get() { |
| | | return props.visible; |
| | |
| | | const toNumber = (val) => { |
| | | const num = Number(val) |
| | | return Number.isNaN(num) ? 0 : num |
| | | } |
| | | |
| | | /** å·²éè´§æ°é = å
¥åºè¡æ»æ°é â å½åå¯éè´§æ°éï¼å©ä½ï¼ */ |
| | | const calcAlreadyReturned = (row) => { |
| | | const total = Number(row?.stockInNum ?? row?.totalQuantity ?? row?.quantity ?? 0) |
| | | const un = Number(row?.unQuantity ?? 0) |
| | | if (!Number.isFinite(total) || !Number.isFinite(un)) return 0 |
| | | return Math.max(total - un, 0) |
| | | } |
| | | |
| | | const getReturnTotal = (row) => { |
| | |
| | | } |
| | | |
| | | const closeModal = () => { |
| | | formState.value.shippingType = undefined; |
| | | formState.value.truckPlateNo = ''; |
| | | formState.value.expressCompany = ''; |
| | | formState.value.expressNo = ''; |
| | | isShow.value = false; |
| | | }; |
| | | |
| | |
| | | const fetchPurchaseLedgerOptions = () => { |
| | | purchaseLedgerOptions.value = [] |
| | | if (formState.value.supplierId) { |
| | | purchaseList({supplierId: formState.value.supplierId,approvalStatus:3}).then((res) => { |
| | | purchaseReturnableList({ supplierId: formState.value.supplierId }).then((res) => { |
| | | purchaseLedgerOptions.value = res.rows; |
| | | }); |
| | | } |
| | |
| | | <el-table-column label="æ°é" |
| | | prop="stockInNum" |
| | | width="70" /> |
| | | <el-table-column label="éå®åè´§æ°é" |
| | | prop="saleOutQuantity" |
| | | width="120" /> |
| | | <el-table-column label="å¯éè´§æ°é" |
| | | prop="unQuantity" |
| | | width="130" /> |
| | | <el-table-column label="å·²éè´§æ°é" |
| | | width="130"> |
| | | <template #default="scope"> |
| | | {{ calcAlreadyReturned(scope.row) }} |
| | | {{ formattedNumber(scope.row, null, scope.row.totalReturnNum || 0) }} |
| | | </template> |
| | | </el-table-column> |
| | | <!-- <el-table-column label="åºåé¢è¦æ°é" |
| | |
| | | const formattedNumber = (row, column, cellValue) => { |
| | | return parseFloat(cellValue).toFixed(2); |
| | | }; |
| | | |
| | | /** å·²éè´§æ°é = å
¥åºè¡æ»æ°é â å½åå¯éè´§æ°éï¼å©ä½ï¼ */ |
| | | const calcAlreadyReturned = (row) => { |
| | | const total = Number(row?.stockInNum ?? row?.totalQuantity ?? row?.quantity ?? 0) |
| | | const un = Number(row?.unQuantity ?? 0) |
| | | if (!Number.isFinite(total) || !Number.isFinite(un)) return 0 |
| | | return Math.max(total - un, 0) |
| | | } |
| | | |
| | | const handleChangeSelection = (val) => { |
| | | selectedRows.value = val; |
| | |
| | | @click="handleDetail(row)" |
| | | >详æ
</el-button |
| | | > |
| | | <el-button link size="small" @click="handleDelete(row)" |
| | | >å é¤</el-button |
| | | <el-button |
| | | link |
| | | size="small" |
| | | :disabled="row.stockOutApproved" |
| | | :style="{ color: row.stockOutApproved ? '#c0c4cc' : undefined }" |
| | | @click="handleDelete(row)" |
| | | >{{ row.stockOutApproved }}</el-button |
| | | > |
| | | </template> |
| | | </PIMTable> |
| | |
| | | <el-descriptions-item label="éæäºº">{{ |
| | | detailData.returnUserName || "--" |
| | | }}</el-descriptions-item> |
| | | <el-descriptions-item label="åè´§ç±»å">{{ |
| | | getShippingTypeLabel(detailData.shippingType) |
| | | }}</el-descriptions-item> |
| | | <el-descriptions-item v-if="String(detailData.shippingType) === '1'" label="å货车çå·">{{ |
| | | detailData.truckPlateNo || "--" |
| | | }}</el-descriptions-item> |
| | | <template v-else-if="String(detailData.shippingType) === '2'"> |
| | | <el-descriptions-item label="å¿«éå
¬å¸">{{ |
| | | detailData.expressCompany || "--" |
| | | }}</el-descriptions-item> |
| | | <el-descriptions-item label="å¿«éåå·">{{ |
| | | detailData.expressNo || "--" |
| | | }}</el-descriptions-item> |
| | | </template> |
| | | <el-descriptions-item label="æ´åææ£é¢">{{ |
| | | formatAmount(detailData.totalDiscountAmount) |
| | | }}</el-descriptions-item> |
| | |
| | | /> |
| | | <el-table-column label="åä½" prop="unit" width="80" /> |
| | | <el-table-column label="æ°é" prop="stockInNum" width="80" /> |
| | | <el-table-column label="éå®åè´§æ°é" prop="saleOutQuantity" width="110" /> |
| | | <el-table-column label="å¯éè´§æ°é" |
| | | prop="unQuantity" |
| | | width="100" /> |
| | | <el-table-column label="å·²éè´§æ°é" |
| | | width="100"> |
| | | <template #default="scope"> |
| | | {{ calcAlreadyReturned(scope.row) }} |
| | | {{ formatAmount(scope.row.totalReturnNum || 0) }} |
| | | </template> |
| | | </el-table-column> |
| | | <!-- <el-table-column label="åºåé¢è¦æ°é" prop="warnNum" width="120" /> --> |
| | |
| | | prop: "returnUserName", |
| | | width: 110, |
| | | }, |
| | | { |
| | | label: "åè´§ç±»å", |
| | | prop: "shippingType", |
| | | width: 100, |
| | | formatData: (val) => getShippingTypeLabel(val), |
| | | }, |
| | | { label: "å货车çå·", prop: "truckPlateDisplay", width: 140 }, |
| | | { label: "å¿«éå
¬å¸", prop: "expressCompanyDisplay", width: 140 }, |
| | | { label: "å¿«éåå·", prop: "expressNoDisplay", width: 150 }, |
| | | |
| | | { |
| | | label: "æ´åææ£é¢", |
| | |
| | | }, |
| | | { |
| | | name: "å é¤", |
| | | disabled: (row) => !!row.stockOutApproved, |
| | | clickFun: (row) => { |
| | | handleDelete(row); |
| | | }, |
| | |
| | | findPurchaseReturnOrderListPage({ ...searchForm.value, ...page }) |
| | | .then((res) => { |
| | | tableLoading.value = false; |
| | | tableData.value = res.data.records; |
| | | tableData.value = (res.data.records || []).map((row) => ({ |
| | | ...row, |
| | | truckPlateDisplay: String(row?.shippingType) === "1" ? (row?.truckPlateNo || "--") : "--", |
| | | expressCompanyDisplay: String(row?.shippingType) === "2" ? (row?.expressCompany || "--") : "--", |
| | | expressNoDisplay: String(row?.shippingType) === "2" ? (row?.expressNo || "--") : "--", |
| | | })); |
| | | page.total = res.data.total; |
| | | }) |
| | | .catch(() => { |
| | |
| | | ); |
| | | }; |
| | | |
| | | const getShippingTypeLabel = (value) => { |
| | | const shippingTypeMap = { |
| | | 1: "货车", |
| | | 2: "å¿«é", |
| | | }; |
| | | return shippingTypeMap[String(value)] || shippingTypeMap[value] || "--"; |
| | | }; |
| | | |
| | | const formatAmount = (value) => { |
| | | if (value === null || value === undefined || value === "") { |
| | | return "--"; |
| | |
| | | return num.toFixed(2); |
| | | }; |
| | | |
| | | /** å·²éè´§æ°é = å
¥åºè¡æ»æ°é â å½åå¯éè´§æ°éï¼å©ä½ï¼ */ |
| | | const calcAlreadyReturned = (row) => { |
| | | const total = Number(row?.stockInNum ?? row?.totalQuantity ?? row?.quantity ?? 0); |
| | | const un = Number(row?.unQuantity ?? 0); |
| | | if (!Number.isFinite(total) || !Number.isFinite(un)) return 0; |
| | | return Math.max(total - un, 0); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getList(); |
| | | }); |
| | |
| | | margin-top: unset; |
| | | } |
| | | </style> |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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; |
| | | |
| | | // 妿æå·¥åºIDï¼æ£æ¥ææé¡¹æ¯å¦é½ä½¿ç¨ç¸åçå·¥åº |
| | | 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) => { |
| | | // 为æ¯ä¸ªrowçæå¯ä¸çè·¯å¾ |
| | | // 使ç¨row.idæç´¢å¼ä½ä¸ºå¯ä¸æ è¯ |
| | | let path = "dataList"; |
| | | |
| | | // ç®åå®ç°ï¼ä½¿ç¨rowçidæä¸ä¸ªå¯ä¸æ è¯ |
| | | 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; |
| | | } |
| | | |
| | | // å
å 载工åºé项ï¼åå è½½æ°æ®ï¼ç¡®ä¿el-selectè½å¤æ£ç¡®åæ¾ |
| | | 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> |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="table_list"> |
| | | <el-form :inline="true" :model="queryParams" class="search-bar" @submit.prevent> |
| | | <el-form-item label="产ååç§°"> |
| | | <el-input |
| | | v-model="queryParams.productName" |
| | | placeholder="请è¾å
¥äº§ååç§°" |
| | | clearable |
| | | style="width: 220px" |
| | | @keyup.enter="handleQuery" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="è§æ ¼åå·"> |
| | | <el-input |
| | | v-model="queryParams.productModelName" |
| | | placeholder="请è¾å
¥è§æ ¼åå·" |
| | | clearable |
| | | style="width: 220px" |
| | | @keyup.enter="handleQuery" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" @click="handleQuery">æ¥è¯¢</el-button> |
| | | <el-button @click="resetQuery">éç½®</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | <div style="text-align: right; margin-bottom: 10px;"> |
| | | <el-button type="primary" |
| | | @click="handleAdd">æ°å¢</el-button> |
| | |
| | | const operationType = ref("add"); // add | edit |
| | | const formRef = ref(null); |
| | | const showProductSelectDialog = ref(false); |
| | | const queryParams = reactive({ |
| | | productName: "", |
| | | productModelName: "", |
| | | }); |
| | | |
| | | // BOM导å
¥åæ° |
| | | const upload = reactive({ |
| | |
| | | getList(); |
| | | }; |
| | | |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | | |
| | | const resetQuery = () => { |
| | | queryParams.productName = ""; |
| | | queryParams.productModelName = ""; |
| | | handleQuery(); |
| | | }; |
| | | |
| | | // æ¥è¯¢å表 |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | listPage({ |
| | | current: page.current, |
| | | size: page.size, |
| | | productName: queryParams.productName || undefined, |
| | | productModelName: queryParams.productModelName || undefined, |
| | | }) |
| | | .then(res => { |
| | | const records = res?.data?.records || []; |
| | |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | prefix-icon="Search" |
| | | style="width: 160px;" |
| | | style="width: 160px" |
| | | @change="handleQuery" /> |
| | | </el-form-item> |
| | | <el-form-item label="产ååç§°:"> |
| | |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | prefix-icon="Search" |
| | | style="width: 160px;" |
| | | style="width: 160px" |
| | | @change="handleQuery" /> |
| | | </el-form-item> |
| | | <el-form-item label="è§æ ¼:"> |
| | |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | prefix-icon="Search" |
| | | style="width: 160px;" |
| | | style="width: 160px" |
| | | @change="handleQuery" /> |
| | | </el-form-item> |
| | | <el-form-item label="ç¶æ:"> |
| | | <el-select v-model="searchForm.status" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 160px;" |
| | | style="width: 160px" |
| | | @change="handleQuery"> |
| | | <el-option label="å¾
å¼å§" |
| | | value="1" /> |
| | |
| | | value="2" /> |
| | | <el-option label="已宿" |
| | | value="3" /> |
| | | <el-option label="已忶" |
| | | value="4" /> |
| | | <!-- <el-option label="已忶"--> |
| | | <!-- value="4" />--> |
| | | <el-option label="å·²ç»æ" |
| | | value="5" /> |
| | | </el-select> |
| | |
| | | :tableLoading="tableLoading" |
| | | :row-class-name="tableRowClassName" |
| | | :isSelection="true" |
| | | :selectable="row => !row.endOrder" |
| | | :selectable="(row) => !row.endOrder" |
| | | @selection-change="handleSelectionChange" |
| | | @pagination="pagination"> |
| | | <template #completionStatus="{ row }"> |
| | | <el-progress :percentage="toProgressPercentage(row?.completionStatus)" |
| | | :color="progressColor(toProgressPercentage(row?.completionStatus))" |
| | | :status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''" /> |
| | | :status=" |
| | | toProgressPercentage(row?.completionStatus) >= 100 |
| | | ? 'success' |
| | | : '' |
| | | " /> |
| | | </template> |
| | | <template #processRouteStatus="{ row }"> |
| | | <div v-if="row.processRouteStatus && row.processRouteStatus.length" |
| | |
| | | <div class="step-circle" |
| | | :class="{ 'is-completed': item.percentage >= 100 }"> |
| | | <span class="step-percentage" |
| | | :style="{ color: item.percentage >= 70 ? item.percentage >= 100 ? '#67c23a' : '#f56c6c' : '#000' }">{{ item.percentage }}%</span> |
| | | :style="{ |
| | | color: |
| | | item.percentage >= 70 |
| | | ? item.percentage >= 100 |
| | | ? '#67c23a' |
| | | : '#f56c6c' |
| | | : '#000', |
| | | }">{{ item.percentage }}%</span> |
| | | </div> |
| | | <div class="step-name">{{ item.name }}</div> |
| | | </div> |
| | |
| | | <el-form-item label="å·¥èºè·¯çº¿"> |
| | | <el-select v-model="bindForm.routeId" |
| | | placeholder="è¯·éæ©å·¥èºè·¯çº¿" |
| | | style="width: 100%;" |
| | | style="width: 100%" |
| | | :loading="bindRouteLoading"> |
| | | <el-option v-for="item in routeOptions" |
| | | :key="item.id" |
| | |
| | | <!-- æ¥æºæ°æ®å¼¹çª --> |
| | | <el-dialog v-model="sourceDataDialogVisible" |
| | | title="æ¥æºæ°æ®" |
| | | width="1200px"> |
| | | width="1200px" |
| | | top="5vh" |
| | | class="source-data-dialog" |
| | | append-to-body> |
| | | <div v-if="sourceRowData" |
| | | class="applyno-summary1"> |
| | | <div class="summary-item"> |
| | | <span class="summary-label">产ååç§°ï¼</span> |
| | | <span class="summary-value"> |
| | | <el-tag type="primary">{{ sourceRowData.productName || '-' }}</el-tag> |
| | | <el-tag type="primary">{{ |
| | | sourceRowData.productName || "-" |
| | | }}</el-tag> |
| | | </span> |
| | | </div> |
| | | <div class="summary-item"> |
| | | <span class="summary-label">è§æ ¼ï¼</span> |
| | | <span class="summary-value">{{ sourceRowData.model || '-' }}</span> |
| | | <span class="summary-value">{{ sourceRowData.model || "-" }}</span> |
| | | </div> |
| | | <div class="summary-item"> |
| | | <span class="summary-label">订åéæ±æ°éï¼</span> |
| | |
| | | <div class="info-grid"> |
| | | <div class="info-item"> |
| | | <div class="info-label">计åå·</div> |
| | | <div class="info-value">{{ item.mpsNo || '-' }}</div> |
| | | <div class="info-value">{{ item.mpsNo || "-" }}</div> |
| | | </div> |
| | | <div class="info-item"> |
| | | <div class="info-label">æ°æ®æ¥æº</div> |
| | | <div class="info-value"> |
| | | <el-tag :type="item.source === 'éå®' ? 'primary' : 'warning'"> |
| | | {{ item.source || 'æªç¥' }} |
| | | {{ item.source || "æªç¥" }} |
| | | </el-tag> |
| | | </div> |
| | | </div> |
| | | <div class="info-item"> |
| | | <div class="info-label">ååå·</div> |
| | | <div class="info-value">{{ item.salesContractNo || '-' }}</div> |
| | | <div class="info-value"> |
| | | {{ item.salesContractNo || "-" }} |
| | | </div> |
| | | </div> |
| | | <div class="info-item"> |
| | | <div class="info-label">客æ·åç§°</div> |
| | | <div class="info-value">{{ item.customerName || '-' }}</div> |
| | | <div class="info-value">{{ item.customerName || "-" }}</div> |
| | | </div> |
| | | <div class="info-item"> |
| | | <div class="info-label">项ç®åç§°</div> |
| | | <div class="info-value">{{ item.projectName || '-' }}</div> |
| | | <div class="info-value">{{ item.projectName || "-" }}</div> |
| | | </div> |
| | | <div class="info-item"> |
| | | <div class="info-label">计åéæ±æ°é</div> |
| | | <div class="info-value">{{ item.qtyRequired || 0 }} {{ item.unit || '' }}</div> |
| | | <div class="info-value"> |
| | | {{ item.qtyRequired || 0 }} {{ item.unit || "" }} |
| | | </div> |
| | | </div> |
| | | <div class="info-item"> |
| | | <div class="info-label">åä½</div> |
| | | <div class="info-value">{{ item.unit || '-' }}</div> |
| | | <div class="info-value">{{ item.unit || "-" }}</div> |
| | | </div> |
| | | <div class="info-item"> |
| | | <div class="info-label">éæ±æ¥æ</div> |
| | | <div class="info-value">{{ item.requiredDate ? dayjs(item.requiredDate).format('YYYY-MM-DD') : '-' }}</div> |
| | | <div class="info-value"> |
| | | {{ |
| | | item.requiredDate |
| | | ? dayjs(item.requiredDate).format("YYYY-MM-DD") |
| | | : "-" |
| | | }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | |
| | | { |
| | | name: "ç»å®å·¥èºè·¯çº¿", |
| | | type: "text", |
| | | showHide: row => !row.processRouteCode && !row.endOrder, |
| | | showHide: row => |
| | | !row.processRouteCode && !row.endOrder && row.status !== 3, |
| | | clickFun: row => { |
| | | openBindRouteDialog(row, "add"); |
| | | }, |
| | |
| | | { |
| | | name: "æ´æ¢å·¥èºè·¯çº¿", |
| | | type: "text", |
| | | showHide: row => row.processRouteCode && !row.endOrder, |
| | | showHide: row => |
| | | row.processRouteCode && !row.endOrder && row.status !== 3, |
| | | clickFun: row => { |
| | | openBindRouteDialog(row, "change"); |
| | | }, |
| | |
| | | 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 åæ®µé¢è²ï¼çº¢/æ©/è/绿 |
| | |
| | | technologyRoutingId: data.technologyRoutingId, |
| | | orderId, |
| | | type: "order", |
| | | editable: !row.endOrder, |
| | | editable: !row.endOrder && row.status !== 3, |
| | | }, |
| | | }); |
| | | } catch (e) { |
| | |
| | | |
| | | .source-table-container { |
| | | margin-top: 20px; |
| | | flex: 1; |
| | | min-height: 0; |
| | | max-height: 500px; |
| | | overflow: auto; |
| | | } |
| | | |
| | | .source-data-cards-container { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 16px; |
| | | max-height: 500px; |
| | | flex: 1; |
| | | min-height: 0; |
| | | max-height: none; |
| | | overflow-y: auto; |
| | | padding: 10px; |
| | | background-color: #f5f7fa; |
| | |
| | | } |
| | | } |
| | | } |
| | | |
| | | .source-data-dialog { |
| | | .el-dialog { |
| | | display: flex; |
| | | flex-direction: column; |
| | | max-height: 90vh; |
| | | } |
| | | |
| | | .el-dialog__body { |
| | | flex: 1; |
| | | min-height: 0; |
| | | display: flex; |
| | | flex-direction: column; |
| | | overflow: hidden; |
| | | } |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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çæ ¹èç¹ä»£è¡¨æç»æåãè¿éæ¯æä»¬è¦ç产ç产åï¼ææåèç¹é½æ¯ä¸ºäºç产è¿ä¸ªæåæéè¦çç©æã' |
| | | }, |
| | | { |
| | | title: '第2æ¥ï¼å±å¼æ¥çå项', |
| | | content: 'ç¹å»èç¹å¯ä»¥å±å¼æ¥çå
¶å项ãåé¡¹ä»£è¡¨ç»æç¶é¡¹çé¶é¨ä»¶æåææãBOMæ¯ä¸ä¸ªæ å½¢ç»æï¼ä½ç°äºäº§åçç»æå±æ¬¡ã' |
| | | }, |
| | | { |
| | | title: '第3æ¥ï¼çè§£ç©æç»æå
³ç³»', |
| | | content: 'è¿çº¿è¡¨ç¤ºç¶åå
³ç³»ãæ¯ä¸ªåèç¹é½æ¯å
¶ç¶èç¹çç»æé¨åãä¾å¦ï¼ä¸ä¸ªçµèç±ä¸»æ¿ãCPUãå
åçç»æã' |
| | | }, |
| | | { |
| | | title: '第4æ¥ï¼æ¥çç¨éä¿¡æ¯', |
| | | content: 'æ¯ä¸ªèç¹ä¸çæ°å表示"åä½äº§åºæéæ°é"ï¼å³ç产ä¸ä¸ªç¶é¡¹éè¦å¤å°å项ãä¾å¦ï¼ç产ä¸å°çµèéè¦1å主æ¿ã' |
| | | }, |
| | | { |
| | | title: '第5æ¥ï¼äºè§£å·¥åºå
³è', |
| | | content: 'åèç¹ä¸çå·¥åºåç§°è¡¨ç¤ºè¯¥ç©æå¨åªéå·¥åºä¸è¢«æ¶èãè¿è¿æ¥äºBOMä¸å·¥èºè·¯çº¿ï¼è¯´æç©æå¨ä½æ¶è¢«ä½¿ç¨ã' |
| | | } |
| | | ] |
| | | |
| | | 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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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ä¸çæ¯ä¸ªç©æèç¹é½æä¸ä¸ª"æ¶èå·¥åº"屿§ï¼è¡¨ç¤ºè¯¥ç©æå¨åªéå·¥åºä¸è¢«ä½¿ç¨ãè¿æ¯BOMä¸å·¥èºè·¯çº¿çæ ¸å¿å
³èã' |
| | | }, |
| | | { |
| | | title: '第4æ¥ï¼è§çç产è¿ç¨æ¨¡æ', |
| | | content: 'ç¹å»ææ¾å¯ä»¥è§ç产åå¨åå·¥åºé´æµè½¬çè¿ç¨ãéçå·¥åºçè¿è¡ï¼BOMä¸çç©æè¢«éæ¥æ¶èã' |
| | | }, |
| | | { |
| | | title: '第5æ¥ï¼ä½éªèå¨äº¤äº', |
| | | content: 'ç¹å»å·¥åºèç¹ï¼BOMä¸å¯¹åºç©æä¼é«äº®ï¼ç¹å»ç©æèç¹ï¼å¯¹åºå·¥åºä¼é«äº®ãè¿ç§èå¨å¸®å©çè§£ç©æå¨ä½æ¶ä½å°è¢«æ¶èã' |
| | | } |
| | | ] |
| | | |
| | | const fetchBomList = async () => { |
| | | try { |
| | | const res = await getBomList({ current: 1, size: 100 }) |
| | | bomList.value = res?.data?.records || [] |
| | | // å¦æè·¯ç±æé¢è®¾bomIdï¼èªå¨éæ© |
| | | 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 || [] |
| | | // å¦æè·¯ç±æé¢è®¾routeIdï¼èªå¨éæ© |
| | | 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 = `æ£å¨æ§è¡ "${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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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">æ£å¸¸</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)" |
| | | > |
| | | <!-- æºå¨å¤å£³ --> |
| | | <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 ? 'æ£å¨ææ...' : 'äº§åæµè½¬ä¸...' }} |
| | | </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 |
| | | }) |
| | | |
| | | // æå
¥ç©æï¼éè¦æå
¥çç©æ - ä»BOMä¸å¹é
å·¥åºï¼ |
| | | 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) { |
| | | // ä¼å
ç¨IDå¹é
ï¼å
¶æ¬¡ç¨åç§°å¹é
|
| | | 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 |
| | | |
| | | // ä»BOM䏿¾ç¬¬ä¸ä¸ªå¹é
çç©æ |
| | | 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)) |
| | | |
| | | // è·åå
³èçBOMæ°æ® |
| | | 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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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">æ£å¸¸</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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container teaching-demo-index"> |
| | | <PageHeader content="å·¥èºè·¯çº¿ä¸BOMå¨ç»æå¦æ¼ç¤º"> |
| | | <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> çæ¦å¿µã |
| | | 鿩䏿¹çæ°æ®åç¹å»è¿å
¥æ¼ç¤ºï¼å³å¯è§çå¨ç»è®²è§£ã |
| | | </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="è¯·éæ©ä¸ä¸ªBOM" |
| | | 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ï¼Bill 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> |
| | |
| | | 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); |
| | |
| | | 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 => { |
| | |
| | | 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); |
| | |
| | | type: "danger", |
| | | link: true, |
| | | showHide: row => { |
| | | return row.status == 0; |
| | | return row.status == 0 && row.source != "éå®"; |
| | | }, |
| | | clickFun: row => { |
| | | handleDelete(row); |
| | |
| | | createTime: "", |
| | | }); |
| | | const mergeCreateTimeDate = computed({ |
| | | get: () => (mergeForm.createTime ? String(mergeForm.createTime).split(" ")[0] : ""), |
| | | set: (value) => { |
| | | mergeForm.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : ""; |
| | | get: () => |
| | | mergeForm.createTime ? String(mergeForm.createTime).split(" ")[0] : "", |
| | | set: value => { |
| | | mergeForm.createTime = value |
| | | ? `${value} ${dayjs().format("HH:mm:ss")}` |
| | | : ""; |
| | | }, |
| | | }); |
| | | |
| | |
| | | }); |
| | | const formCreateTimeDate = computed({ |
| | | get: () => (form.createTime ? String(form.createTime).split(" ")[0] : ""), |
| | | set: (value) => { |
| | | set: value => { |
| | | form.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : ""; |
| | | }, |
| | | }); |
| | |
| | | <template> |
| | | <div> |
| | | <el-dialog |
| | | v-model="dialogFormVisible" |
| | | <el-dialog v-model="dialogFormVisible" |
| | | :title="operationType === 'add' ? 'æ°å¢åºåæ£éª' : operationType === 'view' ? 'æ¥çåºåæ£éª' : 'ç¼è¾åºåæ£éª'" |
| | | width="70%" |
| | | @close="closeDia" |
| | | > |
| | | <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef"> |
| | | @close="closeDia"> |
| | | <el-form :model="form" |
| | | label-width="140px" |
| | | label-position="top" |
| | | :rules="rules" |
| | | ref="formRef"> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="产ååç§°ï¼" prop="productId"> |
| | | <el-tree-select |
| | | v-model="form.productId" |
| | | <el-form-item label="产ååç§°ï¼" |
| | | prop="productId"> |
| | | <el-tree-select v-model="form.productId" |
| | | placeholder="è¯·éæ©" |
| | | filterable |
| | | clearable |
| | | check-strictly |
| | | @change="getModels" |
| | | :data="productOptions" |
| | | :render-after-expand="false" |
| | | :disabled="isViewMode || operationType === 'edit'" |
| | | style="width: 100%" |
| | | /> |
| | | style="width: 100%" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="è§æ ¼åå·ï¼" prop="productModelId"> |
| | | <el-select v-model="form.productModelId" placeholder="è¯·éæ©" clearable :disabled="isViewMode || operationType === 'edit'" |
| | | filterable readonly @change="handleChangeModel"> |
| | | <el-option v-for="item in modelOptions" :key="item.id" :label="item.model" :value="item.id" /> |
| | | <el-form-item label="è§æ ¼åå·ï¼" |
| | | prop="productModelId"> |
| | | <el-select v-model="form.productModelId" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | :disabled="isViewMode || operationType === 'edit'" |
| | | filterable |
| | | readonly |
| | | @change="handleChangeModel"> |
| | | <el-option v-for="item in modelOptions" |
| | | :key="item.id" |
| | | :label="item.model" |
| | | :value="item.id" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ææ éæ©ï¼" prop="testStandardId"> |
| | | <el-select |
| | | v-model="form.testStandardId" |
| | | <el-form-item label="ææ éæ©ï¼" |
| | | prop="testStandardId"> |
| | | <el-select v-model="form.testStandardId" |
| | | placeholder="è¯·éæ©ææ " |
| | | clearable |
| | | @change="handleTestStandardChange" |
| | | style="width: 100%" |
| | | :disabled="isViewMode" |
| | | > |
| | | <el-option |
| | | v-for="item in testStandardOptions" |
| | | :disabled="isViewMode"> |
| | | <el-option v-for="item in testStandardOptions" |
| | | :key="item.id" |
| | | :label="item.standardName || item.standardNo" |
| | | :value="item.id" |
| | | /> |
| | | :value="item.id" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="åä½ï¼" prop="unit"> |
| | | <el-input v-model="form.unit" placeholder="请è¾å
¥" disabled/> |
| | | <el-form-item label="åä½ï¼" |
| | | prop="unit"> |
| | | <el-input v-model="form.unit" |
| | | placeholder="请è¾å
¥" |
| | | disabled /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ°éï¼" prop="quantity"> |
| | | <el-input-number :step="0.01" :min="0" style="width: 100%" v-model="form.quantity" placeholder="请è¾å
¥" clearable :precision="2" :disabled="isViewMode || processQuantityDisabled"/> |
| | | <el-form-item label="æ°éï¼" |
| | | prop="quantity"> |
| | | <el-input-number :step="0.01" |
| | | :min="0" |
| | | style="width: 100%" |
| | | v-model="form.quantity" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | :precision="2" |
| | | :disabled="isViewMode || processQuantityDisabled" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ£æµåä½ï¼" prop="checkCompany"> |
| | | <el-input v-model="form.checkCompany" placeholder="请è¾å
¥" clearable :disabled="isViewMode"/> |
| | | <el-form-item label="æ£æµåä½ï¼" |
| | | prop="checkCompany"> |
| | | <el-input v-model="form.checkCompany" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | :disabled="isViewMode" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ£æµç»æï¼" prop="checkResult"> |
| | | <el-select v-model="form.checkResult" :disabled="isViewMode"> |
| | | <el-option label="åæ ¼" value="åæ ¼" /> |
| | | <el-option label="ä¸åæ ¼" value="ä¸åæ ¼" /> |
| | | <el-option label="é¨ååæ ¼" value="é¨ååæ ¼" /> |
| | | <el-form-item label="æ£æµç»æï¼" |
| | | prop="checkResult"> |
| | | <el-select v-model="form.checkResult" |
| | | :disabled="isViewMode"> |
| | | <el-option label="åæ ¼" |
| | | value="åæ ¼" /> |
| | | <el-option label="ä¸åæ ¼" |
| | | value="ä¸åæ ¼" /> |
| | | <el-option label="é¨ååæ ¼" |
| | | value="é¨ååæ ¼" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ£éªåï¼" prop="checkName"> |
| | | <el-select v-model="form.checkName" placeholder="è¯·éæ©" clearable :disabled="isViewMode"> |
| | | <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName" |
| | | <el-form-item label="æ£éªåï¼" |
| | | prop="checkName"> |
| | | <el-select v-model="form.checkName" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | :disabled="isViewMode"> |
| | | <el-option v-for="item in userList" |
| | | :key="item.nickName" |
| | | :label="item.nickName" |
| | | :value="item.nickName"/> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ£æµæ¥æï¼" prop="checkTime"> |
| | | <el-date-picker |
| | | v-model="form.checkTime" |
| | | <el-form-item label="æ£æµæ¥æï¼" |
| | | prop="checkTime"> |
| | | <el-date-picker v-model="form.checkTime" |
| | | type="date" |
| | | placeholder="è¯·éæ©æ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | clearable |
| | | style="width: 100%" |
| | | :disabled="isViewMode" |
| | | /> |
| | | :disabled="isViewMode" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | <PIMTable |
| | | rowKey="id" |
| | | <PIMTable rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :tableLoading="tableLoading" |
| | | height="400" |
| | | > |
| | | height="400"> |
| | | <template #slot="{ row }"> |
| | | <el-input v-model="row.testValue" clearable :disabled="isViewMode"/> |
| | | <el-input v-model="row.testValue" |
| | | clearable |
| | | :disabled="isViewMode" /> |
| | | </template> |
| | | </PIMTable> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <template v-if="!isViewMode"> |
| | | <el-button type="primary" @click="submitForm">确认</el-button> |
| | | <el-button type="primary" |
| | | @click="submitForm">确认</el-button> |
| | | <el-button @click="closeDia">åæ¶</el-button> |
| | | </template> |
| | | <el-button v-else @click="closeDia">å
³é</el-button> |
| | | <el-button v-else |
| | | @click="closeDia">å
³é</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import {ref, reactive, toRefs, computed, getCurrentInstance, nextTick} from "vue"; |
| | | import { |
| | | ref, |
| | | reactive, |
| | | toRefs, |
| | | computed, |
| | | getCurrentInstance, |
| | | nextTick, |
| | | } from "vue"; |
| | | import {getOptions} from "@/api/procurementManagement/procurementLedger.js"; |
| | | import {modelList, productTreeList} from "@/api/basicData/product.js"; |
| | | import {qualityInspectAdd, qualityInspectUpdate} from "@/api/qualityManagement/rawMaterialInspection.js"; |
| | | import { |
| | | qualityInspectAdd, |
| | | qualityInspectUpdate, |
| | | } from "@/api/qualityManagement/rawMaterialInspection.js"; |
| | | import {userListNoPage} from "@/api/system/user.js"; |
| | | import {qualityInspectDetailByProductId, getQualityTestStandardParamByTestStandardId} from "@/api/qualityManagement/metricMaintenance.js"; |
| | | import { |
| | | qualityInspectDetailByProductId, |
| | | getQualityTestStandardParamByTestStandardId, |
| | | } from "@/api/qualityManagement/metricMaintenance.js"; |
| | | import {qualityInspectParamInfo} from "@/api/qualityManagement/qualityInspectParam.js"; |
| | | const { proxy } = getCurrentInstance() |
| | | const emit = defineEmits(['close']) |
| | | const { proxy } = getCurrentInstance(); |
| | | const emit = defineEmits(["close"]); |
| | | |
| | | const dialogFormVisible = ref(false); |
| | | const operationType = ref(""); |
| | |
| | | checkName: [{ required: false, message: "请è¾å
¥", trigger: "blur" }], |
| | | productId: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | productModelId: [{ required: true, message: "è¯·éæ©", trigger: "change" }], |
| | | testStandardId: [{ required: false, message: "è¯·éæ©ææ ", trigger: "change" }], |
| | | testStandardId: [ |
| | | { required: false, message: "è¯·éæ©ææ ", trigger: "change" }, |
| | | ], |
| | | unit: [{ required: false, message: "请è¾å
¥", trigger: "blur" }], |
| | | quantity: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | qualifiedQuantity: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | unqualifiedQuantity: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | unqualifiedQuantity: [ |
| | | { required: true, message: "请è¾å
¥", trigger: "blur" }, |
| | | ], |
| | | checkCompany: [{ required: false, message: "请è¾å
¥", trigger: "blur" }], |
| | | checkResult: [{ required: true, message: "请è¾å
¥", trigger: "change" }], |
| | | }, |
| | | }); |
| | | const { form, rules } = toRefs(data); |
| | | // æ¯å¦ä¸ºæ¥çæ¨¡å¼ |
| | | const isViewMode = computed(() => operationType.value === 'view'); |
| | | const isViewMode = computed(() => operationType.value === "view"); |
| | | // ç¼è¾æ¶ï¼productMainId æ purchaseLedgerId 任䏿å¼åå·¥åºãæ°éç½®ç° |
| | | const processQuantityDisabled = computed(() => { |
| | | const v = form.value || {}; |
| | |
| | | { |
| | | label: "æ£éªå¼", |
| | | prop: "testValue", |
| | | dataType: 'slot', |
| | | slot: 'slot', |
| | | dataType: "slot", |
| | | slot: "slot", |
| | | }, |
| | | ]); |
| | | const tableData = ref([]); |
| | |
| | | const [userListsRes] = await Promise.all([ |
| | | userListNoPage(), |
| | | getProductOptions(), |
| | | getOptions().then((res) => { |
| | | getOptions().then(res => { |
| | | supplierList.value = res.data; |
| | | }) |
| | | }), |
| | | ]); |
| | | userList.value = userListsRes.data; |
| | | |
| | | form.value = {} |
| | | form.value = {}; |
| | | testStandardOptions.value = []; |
| | | tableData.value = []; |
| | | |
| | | if (operationType.value === 'edit' || operationType.value === 'view') { |
| | | if (operationType.value === "edit" || operationType.value === "view") { |
| | | // å
ä¿å testStandardIdï¼é¿å
被æ¸
空 |
| | | const savedTestStandardId = row.testStandardId; |
| | | // å
è®¾ç½®è¡¨åæ°æ®ï¼ä½ææ¶æ¸
空 testStandardIdï¼çé项å è½½å®æåå设置 |
| | | form.value = {...row, testStandardId: ''} |
| | | currentProductId.value = row.productId || 0 |
| | | form.value = { ...row, testStandardId: "" }; |
| | | currentProductId.value = row.productId || 0; |
| | | // æ¸
空éªè¯ç¶æï¼é¿å
æ°æ®å è½½è¿ç¨ä¸çæ ¡éªéªç |
| | | nextTick(() => { |
| | | proxy.$refs.formRef?.clearValidate(); |
| | |
| | | // ç¼è¾æ¨¡å¼ä¸ï¼å¹¶è¡å è½½è§æ ¼åå·åææ é项 |
| | | if (currentProductId.value) { |
| | | // 设置产ååç§° |
| | | form.value.productName = findNodeById(productOptions.value, currentProductId.value); |
| | | form.value.productName = findNodeById( |
| | | productOptions.value, |
| | | currentProductId.value |
| | | ); |
| | | |
| | | // å¹¶è¡å è½½è§æ ¼åå·åææ é项 |
| | | const params = { |
| | | productId: currentProductId.value, |
| | | inspectType: 2 |
| | | inspectType: 2, |
| | | }; |
| | | |
| | | Promise.all([ |
| | | modelList({ id: currentProductId.value }), |
| | | qualityInspectDetailByProductId(params) |
| | | qualityInspectDetailByProductId(params), |
| | | ]).then(([modelRes, testStandardRes]) => { |
| | | // è®¾ç½®è§æ ¼åå·é项 |
| | | modelOptions.value = modelRes || []; |
| | | // å¦æè¡¨åä¸å·²æ productModelIdï¼è®¾ç½®å¯¹åºç model å unit |
| | | if (form.value.productModelId && modelOptions.value.length > 0) { |
| | | const selectedModel = modelOptions.value.find(item => item.id == form.value.productModelId); |
| | | const selectedModel = modelOptions.value.find( |
| | | item => item.id == form.value.productModelId |
| | | ); |
| | | if (selectedModel) { |
| | | form.value.model = selectedModel.model || ''; |
| | | form.value.unit = selectedModel.unit || ''; |
| | | form.value.model = selectedModel.model || ""; |
| | | form.value.unit = selectedModel.unit || ""; |
| | | } |
| | | } |
| | | |
| | |
| | | nextTick(() => { |
| | | if (savedTestStandardId) { |
| | | // ç¡®ä¿ç±»åå¹é
ï¼item.id å¯è½æ¯æ°åæåç¬¦ä¸²ï¼ |
| | | const matchedOption = testStandardOptions.value.find(item => |
| | | item.id == savedTestStandardId || String(item.id) === String(savedTestStandardId) |
| | | const matchedOption = testStandardOptions.value.find( |
| | | item => |
| | | item.id == savedTestStandardId || |
| | | String(item.id) === String(savedTestStandardId) |
| | | ); |
| | | if (matchedOption) { |
| | | // ç¡®ä¿ä½¿ç¨å¹é
项ç idï¼ä¿æç±»åä¸è´ï¼ |
| | | form.value.testStandardId = matchedOption.id; |
| | | } else { |
| | | // 妿æ¾ä¸å°å¹é
项ï¼å°è¯ç´æ¥ä½¿ç¨åå¼ |
| | | console.warn('æªæ¾å°å¹é
çææ é项ï¼testStandardId:', savedTestStandardId, 'å¯ç¨é项:', testStandardOptions.value); |
| | | console.warn( |
| | | "æªæ¾å°å¹é
çææ é项ï¼testStandardId:", |
| | | savedTestStandardId, |
| | | "å¯ç¨é项:", |
| | | testStandardOptions.value |
| | | ); |
| | | form.value.testStandardId = savedTestStandardId; |
| | | } |
| | | } |
| | |
| | | getQualityInspectParamList(row.id); |
| | | } |
| | | } |
| | | } |
| | | }; |
| | | const getProductOptions = () => { |
| | | return productTreeList().then((res) => { |
| | | return productTreeList().then(res => { |
| | | productOptions.value = convertIdToValue(res); |
| | | }); |
| | | }; |
| | | const getModels = (value) => { |
| | | const getModels = value => { |
| | | form.value.productModelId = undefined; |
| | | form.value.unit = undefined; |
| | | modelOptions.value = []; |
| | | currentProductId.value = value |
| | | currentProductId.value = value; |
| | | form.value.productName = findNodeById(productOptions.value, value); |
| | | modelList({ id: value }).then((res) => { |
| | | modelList({ id: value }).then(res => { |
| | | modelOptions.value = res; |
| | | }) |
| | | }); |
| | | if (currentProductId.value) { |
| | | getList(); |
| | | } |
| | | }; |
| | | |
| | | const handleChangeModel = (value) => { |
| | | form.value.model = modelOptions.value.find(item => item.id == value)?.model || ''; |
| | | form.value.unit = modelOptions.value.find(item => item.id == value)?.unit || ''; |
| | | } |
| | | const handleChangeModel = value => { |
| | | form.value.model = |
| | | modelOptions.value.find(item => item.id == value)?.model || ""; |
| | | form.value.unit = |
| | | modelOptions.value.find(item => item.id == value)?.unit || ""; |
| | | }; |
| | | |
| | | const handleQualifiedQuantityChange = (value) => { |
| | | const handleQualifiedQuantityChange = value => { |
| | | if (value === null || value === undefined) { |
| | | form.value.qualifiedQuantity = 0; |
| | | return; |
| | |
| | | form.value.unqualifiedQuantity = Math.max(0, quantity - qualified); |
| | | }; |
| | | |
| | | const handleUnqualifiedQuantityChange = (value) => { |
| | | const handleUnqualifiedQuantityChange = value => { |
| | | if (value === null || value === undefined) { |
| | | form.value.unqualifiedQuantity = 0; |
| | | return; |
| | | } |
| | | const quantity = parseFloat(form.value.quantity) || 0; |
| | | const unqualified = parseFloat(value) || 0; |
| | | form.value.unqualifiedQuantity = unqualified > quantity?quantity:unqualified; |
| | | form.value.unqualifiedQuantity = |
| | | unqualified > quantity ? quantity : unqualified; |
| | | form.value.qualifiedQuantity = Math.max(0, quantity - unqualified); |
| | | }; |
| | | |
| | |
| | | return null; // æ²¡ææ¾å°èç¹ï¼è¿ånull |
| | | }; |
| | | function convertIdToValue(data) { |
| | | return data.map((item) => { |
| | | return data.map(item => { |
| | | const { id, children, ...rest } = item; |
| | | const newItem = { |
| | | ...rest, |
| | |
| | | if (valid) { |
| | | form.value.inspectType = 2; |
| | | if (operationType.value === "add") { |
| | | tableData.value.forEach((item) => { |
| | | tableData.value.forEach(item => { |
| | | delete item.id; |
| | | }); |
| | | } |
| | |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | }; |
| | | const getList = () => { |
| | | if (!currentProductId.value) { |
| | | testStandardOptions.value = []; |
| | |
| | | } |
| | | let params = { |
| | | productId: currentProductId.value, |
| | | inspectType: 2 |
| | | inspectType: 2, |
| | | }; |
| | | qualityInspectDetailByProductId(params).then(res => { |
| | | // ä¿å䏿æ¡éé¡¹æ°æ® |
| | |
| | | // æ¸
ç©ºè¡¨æ ¼æ°æ®ï¼çå¾
ç¨æ·éæ©ææ |
| | | tableData.value = []; |
| | | // æ¸
ç©ºææ éæ© |
| | | form.value.testStandardId = ''; |
| | | form.value.testStandardId = ""; |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | // ææ éæ©ååå¤ç |
| | | const handleTestStandardChange = (testStandardId) => { |
| | | const handleTestStandardChange = testStandardId => { |
| | | if (!testStandardId) { |
| | | tableData.value = []; |
| | | return; |
| | | } |
| | | tableLoading.value = true; |
| | | getQualityTestStandardParamByTestStandardId(testStandardId).then(res => { |
| | | getQualityTestStandardParamByTestStandardId(testStandardId) |
| | | .then(res => { |
| | | tableData.value = res.data || []; |
| | | tableData.value = tableData.value.map(item => ({ |
| | | ...item, |
| | | id: null |
| | | id: null, |
| | | })); |
| | | }).catch(error => { |
| | | console.error('è·åæ ååæ°å¤±è´¥:', error); |
| | | }) |
| | | .catch(error => { |
| | | console.error("è·åæ ååæ°å¤±è´¥:", error); |
| | | tableData.value = []; |
| | | }).finally(() => { |
| | | }) |
| | | .finally(() => { |
| | | tableLoading.value = false; |
| | | }); |
| | | } |
| | | const getQualityInspectParamList = (id) => { |
| | | }; |
| | | const getQualityInspectParamList = id => { |
| | | qualityInspectParamInfo(id).then(res => { |
| | | tableData.value = res.data; |
| | | }); |
| | | } |
| | | }; |
| | | // å
³éå¼¹æ¡ |
| | | const closeDia = () => { |
| | | proxy.resetForm("formRef"); |
| | | tableData.value = []; |
| | | testStandardOptions.value = []; |
| | | form.value.testStandardId = ''; |
| | | form.value.testStandardId = ""; |
| | | dialogFormVisible.value = false; |
| | | emit('close'); |
| | | } |
| | | emit("close"); |
| | | }; |
| | | defineExpose({ |
| | | openDialog, |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | |
| | | </style> |
| | |
| | | <template> |
| | | <div class="app-container metric-binding"> |
| | | <el-row :gutter="16" class="metric-binding-row"> |
| | | <el-row :gutter="16" |
| | | class="metric-binding-row"> |
| | | <!-- å·¦ä¾§ï¼æ£æµæ åå表 --> |
| | | <el-col :xs="24" :sm="24" :md="12" :lg="14" :xl="14" class="left-col"> |
| | | <el-col :xs="24" |
| | | :sm="24" |
| | | :md="12" |
| | | :lg="14" |
| | | :xl="14" |
| | | class="left-col"> |
| | | <div class="panel left-panel"> |
| | | <PIMTable |
| | | rowKey="id" |
| | | <PIMTable rowKey="id" |
| | | :column="standardColumns" |
| | | :tableData="standardTableData" |
| | | :page="page" |
| | |
| | | :tableLoading="tableLoading" |
| | | :rowClick="handleTableRowClick" |
| | | @pagination="handlePagination" |
| | | :total="page.total" |
| | | > |
| | | :total="page.total"> |
| | | <template #standardNoCell="{ row }"> |
| | | <span class="clickable-link" @click="handleStandardRowClick(row)"> |
| | | <span class="clickable-link" |
| | | @click="handleStandardRowClick(row)"> |
| | | {{ row.standardNo }} |
| | | </span> |
| | | </template> |
| | | |
| | | <!-- 表头æç´¢ --> |
| | | <template #standardNoHeader> |
| | | <el-input |
| | | v-model="searchForm.standardNo" |
| | | <el-input v-model="searchForm.standardNo" |
| | | placeholder="æ åç¼å·" |
| | | clearable |
| | | size="small" |
| | | @change="handleQuery" |
| | | @clear="handleQuery" |
| | | /> |
| | | @clear="handleQuery" /> |
| | | </template> |
| | | <template #standardNameHeader> |
| | | <el-input |
| | | v-model="searchForm.standardName" |
| | | <el-input v-model="searchForm.standardName" |
| | | placeholder="æ ååç§°" |
| | | clearable |
| | | size="small" |
| | | @change="handleQuery" |
| | | @clear="handleQuery" |
| | | /> |
| | | @clear="handleQuery" /> |
| | | </template> |
| | | <template #inspectTypeHeader> |
| | | <el-select |
| | | v-model="searchForm.inspectType" |
| | | <el-select v-model="searchForm.inspectType" |
| | | placeholder="ç±»å«" |
| | | clearable |
| | | size="small" |
| | | style="width: 120px" |
| | | @change="handleQuery" |
| | | @clear="handleQuery" |
| | | > |
| | | <el-option label="åæææ£éª" value="0" /> |
| | | <el-option label="è¿ç¨æ£éª" value="1" /> |
| | | <el-option label="åºåæ£éª" value="2" /> |
| | | @clear="handleQuery"> |
| | | <el-option label="åæææ£éª" |
| | | value="0" /> |
| | | <el-option label="è¿ç¨æ£éª" |
| | | value="1" /> |
| | | <el-option label="åºåæ£éª" |
| | | value="2" /> |
| | | </el-select> |
| | | </template> |
| | | <template #stateHeader> |
| | | <el-select |
| | | v-model="searchForm.state" |
| | | <el-select v-model="searchForm.state" |
| | | placeholder="ç¶æ" |
| | | clearable |
| | | size="small" |
| | | style="width: 110px" |
| | | @change="handleQuery" |
| | | @clear="handleQuery" |
| | | > |
| | | <el-option label="è稿" value="0" /> |
| | | <el-option label="éè¿" value="1" /> |
| | | <el-option label="æ¤é" value="2" /> |
| | | @clear="handleQuery"> |
| | | <el-option label="è稿" |
| | | value="0" /> |
| | | <el-option label="éè¿" |
| | | value="1" /> |
| | | <el-option label="æ¤é" |
| | | value="2" /> |
| | | </el-select> |
| | | </template> |
| | | </PIMTable> |
| | | </div> |
| | | </el-col> |
| | | |
| | | <!-- å³ä¾§ï¼ç»å®å表 --> |
| | | <el-col :xs="24" :sm="24" :md="12" :lg="10" :xl="10" class="right-col"> |
| | | <el-col :xs="24" |
| | | :sm="24" |
| | | :md="12" |
| | | :lg="10" |
| | | :xl="10" |
| | | class="right-col"> |
| | | <div class="panel right-panel"> |
| | | <div class="right-header"> |
| | | <div class="title">ç»å®å
³ç³»</div> |
| | | <div class="desc" v-if="currentStandard"> |
| | | <div class="desc" |
| | | v-if="currentStandard"> |
| | | å½åæ£æµæ åç¼å·ï¼<span class="link-text">{{ currentStandard.standardNo }}</span> |
| | | </div> |
| | | <div class="desc" v-else>è¯·éæ©å·¦ä¾§æ£æµæ å</div> |
| | | <div class="desc" |
| | | v-else>è¯·éæ©å·¦ä¾§æ£æµæ å</div> |
| | | </div> |
| | | |
| | | <div class="right-toolbar"> |
| | | <el-button type="primary" :disabled="!currentStandard" @click="openBindingDialog">æ·»å ç»å®</el-button> |
| | | <el-button type="danger" plain :disabled="!currentStandard" @click="handleBatchUnbind">å é¤</el-button> |
| | | <el-button type="primary" |
| | | :disabled="!currentStandard" |
| | | @click="openBindingDialog">æ·»å ç»å®</el-button> |
| | | <el-button type="danger" |
| | | plain |
| | | :disabled="!currentStandard" |
| | | @click="handleBatchUnbind">å é¤</el-button> |
| | | </div> |
| | | |
| | | <el-table |
| | | v-loading="bindingLoading" |
| | | <el-table v-loading="bindingLoading" |
| | | :data="bindingTableData" |
| | | border |
| | | :row-class-name="() => 'row-center'" |
| | | class="center-table" |
| | | style="width: 100%" |
| | | height="calc(100vh - 220px)" |
| | | @selection-change="handleBindingSelectionChange" |
| | | > |
| | | <el-table-column type="selection" width="48" align="center" /> |
| | | <el-table-column type="index" label="åºå·" width="60" align="center" /> |
| | | <el-table-column prop="productName" label="产ååç§°" min-width="140" /> |
| | | <el-table-column label="æä½" width="120" fixed="right" align="center"> |
| | | @selection-change="handleBindingSelectionChange"> |
| | | <el-table-column type="selection" |
| | | width="48" |
| | | align="center" /> |
| | | <el-table-column type="index" |
| | | label="åºå·" |
| | | width="60" |
| | | align="center" /> |
| | | <el-table-column prop="productName" |
| | | label="产ååç§°" |
| | | min-width="140" /> |
| | | <el-table-column label="æä½" |
| | | width="120" |
| | | fixed="right" |
| | | align="center"> |
| | | <template #default="{ row }"> |
| | | <el-button link type="danger" size="small" @click="handleUnbind(row)">å é¤</el-button> |
| | | <el-button link |
| | | type="danger" |
| | | size="small" |
| | | @click="handleUnbind(row)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- æ·»å ç»å®å¼¹æ¡ --> |
| | | <el-dialog |
| | | v-model="bindingDialogVisible" |
| | | <el-dialog v-model="bindingDialogVisible" |
| | | title="æ·»å ç»å®" |
| | | width="520px" |
| | | @close="closeBindingDialog" |
| | | > |
| | | @close="closeBindingDialog"> |
| | | <el-form label-width="100px"> |
| | | <el-form-item label="产å"> |
| | | <el-tree-select |
| | | v-model="selectedProductIds" |
| | | <PIMTreeSelect v-model="selectedProductIds" |
| | | multiple |
| | | filterable |
| | | collapse-tags |
| | | collapse-tags-tooltip |
| | | placeholder="è¯·éæ©äº§åï¼å¯å¤éï¼" |
| | |
| | | check-strictly |
| | | :data="productOptions" |
| | | :render-after-expand="false" |
| | | style="width: 100%" |
| | | /> |
| | | style="width: 100%" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button @click="closeBindingDialog">åæ¶</el-button> |
| | | <el-button type="primary" @click="submitBinding">ç¡®å®</el-button> |
| | | <el-button type="primary" |
| | | @click="submitBinding">ç¡®å®</el-button> |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { Search } from '@element-plus/icons-vue' |
| | | import { ref, reactive, toRefs, onMounted, getCurrentInstance } from 'vue' |
| | | import { ElMessageBox } from 'element-plus' |
| | | import PIMTable from '@/components/PIMTable/PIMTable.vue' |
| | | import { productTreeList } from '@/api/basicData/product.js' |
| | | import { |
| | | qualityTestStandardListPage |
| | | } from '@/api/qualityManagement/metricMaintenance.js' |
| | | import { productProcessListPage } from '@/api/basicData/productProcess.js' |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import { ref, reactive, toRefs, onMounted, getCurrentInstance } from "vue"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | import PIMTable from "@/components/PIMTable/PIMTable.vue"; |
| | | import PIMTreeSelect from "@/components/PIMTreeSelect/index.vue"; |
| | | import { productTreeList } from "@/api/basicData/product.js"; |
| | | import { qualityTestStandardListPage } from "@/api/qualityManagement/metricMaintenance.js"; |
| | | import { productProcessListPage } from "@/api/basicData/productProcess.js"; |
| | | import { |
| | | qualityTestStandardBindingList, |
| | | qualityTestStandardBindingAdd, |
| | | qualityTestStandardBindingDel |
| | | } from '@/api/qualityManagement/qualityTestStandardBinding.js' |
| | | qualityTestStandardBindingDel, |
| | | } from "@/api/qualityManagement/qualityTestStandardBinding.js"; |
| | | |
| | | const { proxy } = getCurrentInstance() |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | // 左侧æ ååè¡¨ï¼æ´è¡å
容å±
ä¸ï¼é
åæ ·å¼ï¼ |
| | | const rowClassNameCenter = () => 'row-center' |
| | | const rowClassNameCenter = () => "row-center"; |
| | | |
| | | const data = reactive({ |
| | | searchForm: { |
| | | standardNo: '', |
| | | standardName: '', |
| | | state: '', |
| | | inspectType: '' |
| | | } |
| | | }) |
| | | const { searchForm } = toRefs(data) |
| | | standardNo: "", |
| | | standardName: "", |
| | | state: "", |
| | | inspectType: "", |
| | | }, |
| | | }); |
| | | const { searchForm } = toRefs(data); |
| | | |
| | | // 左侧 |
| | | const standardTableData = ref([]) |
| | | const tableLoading = ref(false) |
| | | const page = reactive({ current: 1, size: 10, total: 0 }) |
| | | const standardTableData = ref([]); |
| | | const tableLoading = ref(false); |
| | | const page = reactive({ current: 1, size: 10, total: 0 }); |
| | | |
| | | // å·¥åºä¸æï¼ç¨äºåè¡¨åæ¾ï¼ |
| | | const processOptions = ref([]) |
| | | const processOptions = ref([]); |
| | | |
| | | const getProcessList = async () => { |
| | | try { |
| | | const res = await productProcessListPage({ current: 1, size: 1000 }) |
| | | const res = await productProcessListPage({ current: 1, size: 1000 }); |
| | | if (res?.code === 200) { |
| | | const records = res?.data?.records || [] |
| | | processOptions.value = records.map((item) => ({ |
| | | const records = res?.data?.records || []; |
| | | processOptions.value = records.map(item => ({ |
| | | label: item.processName || item.name || item.label, |
| | | value: item.id || item.processId || item.value |
| | | })) |
| | | value: item.id || item.processId || item.value, |
| | | })); |
| | | } |
| | | } catch (error) { |
| | | console.error('è·åå·¥åºå表失败:', error) |
| | | console.error("è·åå·¥åºå表失败:", error); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | const standardColumns = ref([ |
| | | { label: 'æ åç¼å·', prop: 'standardNo', dataType: 'slot', slot: 'standardNoCell', minWidth: 160, align: 'center', headerSlot: 'standardNoHeader' }, |
| | | { label: 'æ ååç§°', prop: 'standardName', minWidth: 180, align: 'center', headerSlot: 'standardNameHeader' }, |
| | | { |
| | | label: 'ç±»å«', |
| | | prop: 'inspectType', |
| | | headerSlot: 'inspectTypeHeader', |
| | | align: 'center', |
| | | dataType: 'tag', |
| | | formatData: (val) => { |
| | | const map = { 0: 'åæææ£éª', 1: 'è¿ç¨æ£éª', 2: 'åºåæ£éª' } |
| | | return map[val] || val |
| | | } |
| | | }, |
| | | { |
| | | label: 'å·¥åº', |
| | | prop: 'processId', |
| | | align: 'center', |
| | | dataType: 'tag', |
| | | formatData: (val) => { |
| | | const target = processOptions.value.find( |
| | | (item) => String(item.value) === String(val) |
| | | ) |
| | | return target?.label || val |
| | | } |
| | | }, |
| | | { |
| | | label: '夿³¨', |
| | | prop: 'remark', |
| | | label: "æ åç¼å·", |
| | | prop: "standardNo", |
| | | dataType: "slot", |
| | | slot: "standardNoCell", |
| | | minWidth: 160, |
| | | align: 'center' |
| | | } |
| | | align: "center", |
| | | headerSlot: "standardNoHeader", |
| | | }, |
| | | { |
| | | label: "æ ååç§°", |
| | | prop: "standardName", |
| | | minWidth: 180, |
| | | align: "center", |
| | | headerSlot: "standardNameHeader", |
| | | }, |
| | | { |
| | | label: "ç±»å«", |
| | | prop: "inspectType", |
| | | headerSlot: "inspectTypeHeader", |
| | | align: "center", |
| | | dataType: "tag", |
| | | formatData: val => { |
| | | const map = { 0: "åæææ£éª", 1: "è¿ç¨æ£éª", 2: "åºåæ£éª" }; |
| | | return map[val] || val; |
| | | }, |
| | | }, |
| | | { |
| | | label: "å·¥åº", |
| | | prop: "processId", |
| | | align: "center", |
| | | dataType: "tag", |
| | | formatData: val => { |
| | | const target = processOptions.value.find( |
| | | item => String(item.value) === String(val) |
| | | ); |
| | | return target?.label || val; |
| | | }, |
| | | }, |
| | | { |
| | | label: "夿³¨", |
| | | prop: "remark", |
| | | minWidth: 160, |
| | | align: "center", |
| | | }, |
| | | // { |
| | | // label: 'ç¶æ', |
| | | // prop: 'state', |
| | |
| | | // return 'info' |
| | | // } |
| | | // } |
| | | ]) |
| | | ]); |
| | | |
| | | const currentStandard = ref(null) |
| | | const currentStandard = ref(null); |
| | | |
| | | // å³ä¾§ç»å® |
| | | const bindingTableData = ref([]) |
| | | const bindingLoading = ref(false) |
| | | const bindingSelectedRows = ref([]) |
| | | const bindingDialogVisible = ref(false) |
| | | const bindingTableData = ref([]); |
| | | const bindingLoading = ref(false); |
| | | const bindingSelectedRows = ref([]); |
| | | const bindingDialogVisible = ref(false); |
| | | |
| | | // äº§åæ ï¼ç¨äºç»å®éæ©ï¼ |
| | | const productOptions = ref([]) |
| | | const selectedProductIds = ref([]) |
| | | const productOptions = ref([]); |
| | | const selectedProductIds = ref([]); |
| | | |
| | | const getProductOptions = async () => { |
| | | // é¿å
éå¤è¯·æ± |
| | | if (productOptions.value?.length) return |
| | | const res = await productTreeList() |
| | | productOptions.value = convertIdToValue(Array.isArray(res) ? res : []) |
| | | } |
| | | if (productOptions.value?.length) return; |
| | | const res = await productTreeList(); |
| | | productOptions.value = convertIdToValue(Array.isArray(res) ? res : []); |
| | | }; |
| | | |
| | | function convertIdToValue(data) { |
| | | return (data || []).map((item) => { |
| | | const { id, children, ...rest } = item |
| | | return (data || []).map(item => { |
| | | const { id, children, ...rest } = item; |
| | | const newItem = { |
| | | ...rest, |
| | | value: id |
| | | } |
| | | value: id, |
| | | }; |
| | | if (children && children.length > 0) { |
| | | newItem.children = convertIdToValue(children) |
| | | newItem.children = convertIdToValue(children); |
| | | } |
| | | return newItem |
| | | }) |
| | | return newItem; |
| | | }); |
| | | } |
| | | |
| | | const handleQuery = () => { |
| | | page.current = 1 |
| | | getStandardList() |
| | | } |
| | | page.current = 1; |
| | | getStandardList(); |
| | | }; |
| | | |
| | | const handlePagination = (obj) => { |
| | | page.current = obj.page |
| | | page.size = obj.limit |
| | | getStandardList() |
| | | } |
| | | const handlePagination = obj => { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | getStandardList(); |
| | | }; |
| | | |
| | | const getStandardList = () => { |
| | | tableLoading.value = true |
| | | tableLoading.value = true; |
| | | qualityTestStandardListPage({ |
| | | ...searchForm.value, |
| | | current: page.current, |
| | | size: page.size, |
| | | state: 1 |
| | | state: 1, |
| | | }) |
| | | .then((res) => { |
| | | const records = res?.data?.records || [] |
| | | standardTableData.value = records |
| | | page.total = res?.data?.total || records.length |
| | | .then(res => { |
| | | const records = res?.data?.records || []; |
| | | standardTableData.value = records; |
| | | page.total = res?.data?.total || records.length; |
| | | }) |
| | | .finally(() => { |
| | | tableLoading.value = false |
| | | }) |
| | | } |
| | | tableLoading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | // è¡¨æ ¼è¡ç¹å»ï¼å è½½å³ä¾§ç»å®å表 |
| | | const handleTableRowClick = (row) => { |
| | | currentStandard.value = row |
| | | loadBindingList() |
| | | } |
| | | const handleTableRowClick = row => { |
| | | currentStandard.value = row; |
| | | loadBindingList(); |
| | | }; |
| | | |
| | | // 左侧è¡ç¹å»ï¼å è½½å³ä¾§ç»å®å表ï¼ä¿çç¨äºæ åç¼å·åçç¹å»ï¼ |
| | | const handleStandardRowClick = (row) => { |
| | | currentStandard.value = row |
| | | loadBindingList() |
| | | } |
| | | const handleStandardRowClick = row => { |
| | | currentStandard.value = row; |
| | | loadBindingList(); |
| | | }; |
| | | |
| | | const loadBindingList = () => { |
| | | if (!currentStandard.value?.id) { |
| | | bindingTableData.value = [] |
| | | return |
| | | bindingTableData.value = []; |
| | | return; |
| | | } |
| | | bindingLoading.value = true |
| | | bindingLoading.value = true; |
| | | qualityTestStandardBindingList({ testStandardId: currentStandard.value.id }) |
| | | .then((res) => { |
| | | const base = res?.data || [] |
| | | .then(res => { |
| | | const base = res?.data || []; |
| | | // å°å½åæ åçå·¥åºå夿³¨å¸¦å°ç»å®å表ä¸å±ç¤º |
| | | bindingTableData.value = base.map((item) => ({ |
| | | bindingTableData.value = base.map(item => ({ |
| | | ...item, |
| | | processId: currentStandard.value?.processId, |
| | | remark: currentStandard.value?.remark |
| | | })) |
| | | remark: currentStandard.value?.remark, |
| | | })); |
| | | }) |
| | | .finally(() => { |
| | | bindingLoading.value = false |
| | | }) |
| | | } |
| | | bindingLoading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | const handleBindingSelectionChange = (selection) => { |
| | | bindingSelectedRows.value = selection |
| | | } |
| | | const handleBindingSelectionChange = selection => { |
| | | bindingSelectedRows.value = selection; |
| | | }; |
| | | |
| | | const openBindingDialog = () => { |
| | | if (!currentStandard.value?.id) return |
| | | selectedProductIds.value = [] |
| | | getProductOptions() |
| | | bindingDialogVisible.value = true |
| | | } |
| | | if (!currentStandard.value?.id) return; |
| | | selectedProductIds.value = []; |
| | | getProductOptions(); |
| | | bindingDialogVisible.value = true; |
| | | }; |
| | | |
| | | const closeBindingDialog = () => { |
| | | bindingDialogVisible.value = false |
| | | } |
| | | bindingDialogVisible.value = false; |
| | | }; |
| | | |
| | | const submitBinding = async () => { |
| | | const testStandardId = currentStandard.value?.id |
| | | if (!testStandardId) return |
| | | const ids = (selectedProductIds.value || []).filter(Boolean) |
| | | const testStandardId = currentStandard.value?.id; |
| | | if (!testStandardId) return; |
| | | const ids = (selectedProductIds.value || []).filter(Boolean); |
| | | if (!ids.length) { |
| | | proxy.$message.warning('è¯·éæ©äº§å') |
| | | return |
| | | proxy.$message.warning("è¯·éæ©äº§å"); |
| | | return; |
| | | } |
| | | const payload = ids.map((pid) => ({ |
| | | const payload = ids.map(pid => ({ |
| | | productId: pid, |
| | | testStandardId |
| | | })) |
| | | await qualityTestStandardBindingAdd(payload) |
| | | proxy.$message.success('æ·»å æå') |
| | | bindingDialogVisible.value = false |
| | | loadBindingList() |
| | | } |
| | | testStandardId, |
| | | })); |
| | | await qualityTestStandardBindingAdd(payload); |
| | | proxy.$message.success("æ·»å æå"); |
| | | bindingDialogVisible.value = false; |
| | | loadBindingList(); |
| | | }; |
| | | |
| | | const handleUnbind = async (row) => { |
| | | const id = row?.id ?? row?.qualityTestStandardBindingId |
| | | if (id == null || id === '') return |
| | | const handleUnbind = async row => { |
| | | const id = row?.id ?? row?.qualityTestStandardBindingId; |
| | | if (id == null || id === "") return; |
| | | try { |
| | | await ElMessageBox.confirm('确认å é¤è¯¥ç»å®ï¼', 'æç¤º', { type: 'warning' }) |
| | | await ElMessageBox.confirm("确认å é¤è¯¥ç»å®ï¼", "æç¤º", { type: "warning" }); |
| | | } catch { |
| | | return |
| | | return; |
| | | } |
| | | try { |
| | | await qualityTestStandardBindingDel([id]) |
| | | proxy.$message.success('å 餿å') |
| | | loadBindingList() |
| | | await qualityTestStandardBindingDel([id]); |
| | | proxy.$message.success("å 餿å"); |
| | | loadBindingList(); |
| | | } catch (err) { |
| | | console.error('å é¤ç»å®å¤±è´¥:', err) |
| | | proxy.$message?.error(err?.message || 'å é¤å¤±è´¥') |
| | | console.error("å é¤ç»å®å¤±è´¥:", err); |
| | | proxy.$message?.error(err?.message || "å é¤å¤±è´¥"); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | const handleBatchUnbind = async () => { |
| | | if (!bindingSelectedRows.value.length) { |
| | | proxy.$message.warning('è¯·éæ©æ°æ®') |
| | | return |
| | | proxy.$message.warning("è¯·éæ©æ°æ®"); |
| | | return; |
| | | } |
| | | const ids = bindingSelectedRows.value |
| | | .map((i) => i?.id ?? i?.qualityTestStandardBindingId) |
| | | .filter((id) => id != null && id !== '') |
| | | .map(i => i?.id ?? i?.qualityTestStandardBindingId) |
| | | .filter(id => id != null && id !== ""); |
| | | if (!ids.length) { |
| | | proxy.$message.warning('é䏿°æ®ç¼ºå°ææ id') |
| | | return |
| | | proxy.$message.warning("é䏿°æ®ç¼ºå°ææ id"); |
| | | return; |
| | | } |
| | | try { |
| | | await ElMessageBox.confirm('éä¸çå
容å°è¢«å é¤ï¼æ¯å¦ç¡®è®¤å é¤ï¼', 'å é¤æç¤º', { type: 'warning' }) |
| | | await ElMessageBox.confirm( |
| | | "éä¸çå
容å°è¢«å é¤ï¼æ¯å¦ç¡®è®¤å é¤ï¼", |
| | | "å é¤æç¤º", |
| | | { type: "warning" } |
| | | ); |
| | | } catch { |
| | | return |
| | | return; |
| | | } |
| | | try { |
| | | await qualityTestStandardBindingDel(ids) |
| | | proxy.$message.success('å 餿å') |
| | | loadBindingList() |
| | | await qualityTestStandardBindingDel(ids); |
| | | proxy.$message.success("å 餿å"); |
| | | loadBindingList(); |
| | | } catch (err) { |
| | | console.error('æ¹éå é¤ç»å®å¤±è´¥:', err) |
| | | proxy.$message?.error(err?.message || 'å é¤å¤±è´¥') |
| | | console.error("æ¹éå é¤ç»å®å¤±è´¥:", err); |
| | | proxy.$message?.error(err?.message || "å é¤å¤±è´¥"); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getStandardList() |
| | | getProcessList() |
| | | }) |
| | | getStandardList(); |
| | | getProcessList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | |
| | | <template> |
| | | <div> |
| | | <el-dialog |
| | | v-model="dialogFormVisible" |
| | | <el-dialog v-model="dialogFormVisible" |
| | | :title="operationType === 'add' ? 'æ°å¢ä¸åæ ¼ç®¡ç' : 'ç¼è¾ä¸åæ ¼ç®¡ç'" |
| | | width="70%" |
| | | @close="closeDia" |
| | | > |
| | | <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef"> |
| | | @close="closeDia"> |
| | | <el-form :model="form" |
| | | label-width="140px" |
| | | label-position="top" |
| | | :rules="rules" |
| | | ref="formRef"> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç±»å«ï¼" prop="inspectType"> |
| | | <el-form-item label="ç±»å«ï¼" |
| | | prop="inspectType"> |
| | | <el-select v-model="form.inspectType"> |
| | | <el-option label="åæææ£éª" :value="0" /> |
| | | <el-option label="è¿ç¨æ£éª" :value="1" /> |
| | | <el-option label="åºåæ£éª" :value="2" /> |
| | | <el-option label="åæææ£éª" |
| | | :value="0" /> |
| | | <el-option label="è¿ç¨æ£éª" |
| | | :value="1" /> |
| | | <el-option label="åºåæ£éª" |
| | | :value="2" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="产ååç§°ï¼" prop="productId"> |
| | | <el-tree-select |
| | | v-model="form.productId" |
| | | <el-form-item label="产ååç§°ï¼" |
| | | prop="productId"> |
| | | <el-tree-select v-model="form.productId" |
| | | placeholder="è¯·éæ©" |
| | | filterable |
| | | clearable |
| | | check-strictly |
| | | @change="getModels" |
| | | :data="productOptions" |
| | | :render-after-expand="false" |
| | | style="width: 100%" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="è§æ ¼åå·ï¼" |
| | | prop="model"> |
| | | <el-select v-model="form.productModelId" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | :disabled="operationType === 'edit'" |
| | | filterable |
| | | readonly |
| | | @change="handleChangeModel"> |
| | | <el-option v-for="item in modelOptions" |
| | | :key="item.id" |
| | | :label="item.model" |
| | | :value="item.id" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="åä½ï¼" |
| | | prop="unit"> |
| | | <el-input v-model="form.unit" |
| | | placeholder="请è¾å
¥" |
| | | clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ°éï¼" |
| | | prop="quantity"> |
| | | <el-input-number :step="0.01" |
| | | :min="0" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="è§æ ¼åå·ï¼" prop="model"> |
| | | <el-select v-model="form.productModelId" placeholder="è¯·éæ©" clearable :disabled="operationType === 'edit'" |
| | | filterable readonly @change="handleChangeModel"> |
| | | <el-option v-for="item in modelOptions" :key="item.id" :label="item.model" :value="item.id" /> |
| | | </el-select> |
| | | v-model="form.quantity" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | :precision="2" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="åä½ï¼" prop="unit"> |
| | | <el-input v-model="form.unit" placeholder="请è¾å
¥" clearable/> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ°éï¼" prop="quantity"> |
| | | <el-input-number :step="0.01" :min="0" style="width: 100%" v-model="form.quantity" placeholder="请è¾å
¥" clearable :precision="2"/> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ£éªåï¼" prop="checkName"> |
| | | <el-select v-model="form.checkName" placeholder="è¯·éæ©" clearable style="width: 100%"> |
| | | <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName" :value="item.nickName"/> |
| | | <el-form-item label="æ£éªåï¼" |
| | | prop="checkName"> |
| | | <el-select v-model="form.checkName" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | style="width: 100%"> |
| | | <el-option v-for="item in userList" |
| | | :key="item.nickName" |
| | | :label="item.nickName" |
| | | :value="item.nickName" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ£æµæ¥æï¼" prop="checkTime"> |
| | | <el-date-picker |
| | | v-model="form.checkTime" |
| | | <el-form-item label="æ£æµæ¥æï¼" |
| | | prop="checkTime"> |
| | | <el-date-picker v-model="form.checkTime" |
| | | type="date" |
| | | placeholder="è¯·éæ©æ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | clearable |
| | | style="width: 100%" |
| | | /> |
| | | style="width: 100%" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ä¸åæ ¼ç°è±¡ï¼" prop="defectivePhenomena"> |
| | | <el-input v-model="form.defectivePhenomena" placeholder="请è¾å
¥" clearable/> |
| | | <el-form-item label="ä¸åæ ¼ç°è±¡ï¼" |
| | | prop="defectivePhenomena"> |
| | | <el-input v-model="form.defectivePhenomena" |
| | | placeholder="请è¾å
¥" |
| | | clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å¤çç»æï¼" prop="dealResult"> |
| | | <el-select v-model="form.dealResult" placeholder="è¯·éæ©" clearable> |
| | | <el-option :label="item.label" :value="item.value" v-for="item in rejection_handling" :key="item.value" /> |
| | | <el-form-item label="å¤çç»æï¼" |
| | | prop="dealResult"> |
| | | <el-select v-model="form.dealResult" |
| | | placeholder="è¯·éæ©" |
| | | clearable> |
| | | <el-option :label="item.label" |
| | | :value="item.value" |
| | | v-for="item in rejection_handling" |
| | | :key="item.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å¤ç人ï¼" prop="dealName"> |
| | | <el-select v-model="form.dealName" placeholder="è¯·éæ©" clearable style="width: 100%"> |
| | | <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName" :value="item.nickName"/> |
| | | <el-form-item label="å¤ç人ï¼" |
| | | prop="dealName"> |
| | | <el-select v-model="form.dealName" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | style="width: 100%"> |
| | | <el-option v-for="item in userList" |
| | | :key="item.nickName" |
| | | :label="item.nickName" |
| | | :value="item.nickName" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å¤çæ¥æï¼" prop="dealTime"> |
| | | <el-date-picker |
| | | v-model="form.dealTime" |
| | | <el-form-item label="å¤çæ¥æï¼" |
| | | prop="dealTime"> |
| | | <el-date-picker v-model="form.dealTime" |
| | | type="date" |
| | | placeholder="è¯·éæ©æ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | clearable |
| | | style="width: 100%" |
| | | /> |
| | | style="width: 100%" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" @click="submitForm">确认</el-button> |
| | | <el-button type="primary" |
| | | @click="submitForm">确认</el-button> |
| | | <el-button @click="closeDia">åæ¶</el-button> |
| | | </div> |
| | | </template> |
| | |
| | | import { |
| | | getQualityUnqualifiedInfo, |
| | | qualityUnqualifiedAdd, |
| | | qualityUnqualifiedUpdate |
| | | qualityUnqualifiedUpdate, |
| | | } from "@/api/qualityManagement/nonconformingManagement.js"; |
| | | import {userListNoPage} from "@/api/system/user.js"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | const { proxy } = getCurrentInstance() |
| | | const userStore = useUserStore() |
| | | const emit = defineEmits(['close']) |
| | | const { proxy } = getCurrentInstance(); |
| | | const userStore = useUserStore(); |
| | | const emit = defineEmits(["close"]); |
| | | |
| | | const dialogFormVisible = ref(false); |
| | | const operationType = ref('') |
| | | const { rejection_handling } = proxy.useDict("rejection_handling") |
| | | const operationType = ref(""); |
| | | const { rejection_handling } = proxy.useDict("rejection_handling"); |
| | | const data = reactive({ |
| | | form: { |
| | | checkTime: "", |
| | |
| | | quantity: undefined, |
| | | checkCompany: "", |
| | | checkResult: "", |
| | | inspectType: '', |
| | | defectivePhenomena: '', |
| | | dealResult: '', |
| | | dealName: '', |
| | | dealTime: '', |
| | | inspectType: "", |
| | | defectivePhenomena: "", |
| | | dealResult: "", |
| | | dealName: "", |
| | | dealTime: "", |
| | | productModelId: undefined, |
| | | }, |
| | | rules: { |
| | | checkTime: [{ required: false, message: "请è¾å
¥", trigger: "blur" },], |
| | | checkTime: [{ required: false, message: "请è¾å
¥", trigger: "blur" }], |
| | | process: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | checkName: [{ required: true, message: "è¯·éæ©æ£éªå", trigger: "change" }], |
| | | productId: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | |
| | | userList.value = []; |
| | | } |
| | | dialogFormVisible.value = true; |
| | | if (operationType.value === 'add') { |
| | | if (operationType.value === "add") { |
| | | form.value = { |
| | | checkName: userStore.nickName || '', |
| | | dealName: '', |
| | | dealTime: '', |
| | | dealResult: '', |
| | | defectivePhenomena: '', |
| | | inspectType: '', |
| | | checkTime: '', |
| | | productId: '', |
| | | model: '', |
| | | unit: '', |
| | | checkName: userStore.nickName || "", |
| | | dealName: "", |
| | | dealTime: "", |
| | | dealResult: "", |
| | | defectivePhenomena: "", |
| | | inspectType: "", |
| | | checkTime: "", |
| | | productId: "", |
| | | model: "", |
| | | unit: "", |
| | | quantity: undefined, |
| | | productName: '', |
| | | productName: "", |
| | | productModelId: undefined, |
| | | }; |
| | | } else { |
| | | form.value = {}; |
| | | } |
| | | getProductOptions(); |
| | | if (operationType.value === 'edit') { |
| | | if (operationType.value === "edit") { |
| | | getQualityUnqualifiedInfo(row.id).then(res => { |
| | | const { inspectState, ...rest } = (res.data || {}) |
| | | form.value = { ...rest } |
| | | }) |
| | | const { inspectState, ...rest } = res.data || {}; |
| | | form.value = { ...rest }; |
| | | }); |
| | | } |
| | | } |
| | | }; |
| | | const getProductOptions = () => { |
| | | productTreeList().then((res) => { |
| | | productTreeList().then(res => { |
| | | productOptions.value = convertIdToValue(res); |
| | | }); |
| | | }; |
| | | const getModels = (value) => { |
| | | const getModels = value => { |
| | | form.value.productName = findNodeById(productOptions.value, value); |
| | | modelList({ id: value }).then((res) => { |
| | | modelList({ id: value }).then(res => { |
| | | modelOptions.value = res; |
| | | }) |
| | | }); |
| | | }; |
| | | const handleChangeModel = (value) => { |
| | | const handleChangeModel = value => { |
| | | const selectedModel = modelOptions.value.find(item => item.id === value); |
| | | if (selectedModel) { |
| | | form.value.model = selectedModel.model; |
| | |
| | | return null; // æ²¡ææ¾å°èç¹ï¼è¿ånull |
| | | }; |
| | | function convertIdToValue(data) { |
| | | return data.map((item) => { |
| | | return data.map(item => { |
| | | const { id, children, ...rest } = item; |
| | | const newItem = { |
| | | ...rest, |
| | |
| | | proxy.$refs.formRef.validate(valid => { |
| | | if (valid) { |
| | | // ç¶æåæ®µä¸å¨è¡¨åå¡«åï¼ä¹ä¸ä¼ ç»å端 |
| | | const { inspectState, ...payload } = (form.value || {}) |
| | | const { inspectState, ...payload } = form.value || {}; |
| | | if (operationType.value === "add") { |
| | | qualityUnqualifiedAdd(payload).then(res => { |
| | | proxy.$modal.msgSuccess("æäº¤æå"); |
| | | closeDia(); |
| | | }) |
| | | }); |
| | | } else { |
| | | qualityUnqualifiedUpdate(payload).then(res => { |
| | | proxy.$modal.msgSuccess("æäº¤æå"); |
| | | closeDia(); |
| | | }) |
| | | }); |
| | | } |
| | | } |
| | | }) |
| | | } |
| | | }); |
| | | }; |
| | | // å
³éå¼¹æ¡ |
| | | const closeDia = () => { |
| | | proxy.resetForm("formRef"); |
| | | dialogFormVisible.value = false; |
| | | emit('close') |
| | | emit("close"); |
| | | }; |
| | | defineExpose({ |
| | | openDialog, |
| | |
| | | </script> |
| | | |
| | | <style scoped> |
| | | |
| | | </style> |
| | |
| | | prop="productId"> |
| | | <el-tree-select v-model="form.productId" |
| | | placeholder="è¯·éæ©" |
| | | filterable |
| | | clearable |
| | | check-strictly |
| | | @change="getModels" |
| | |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ£æµç»æï¼" |
| | | prop="checkResult"> |
| | | <el-select v-model="form.checkResult" :disabled="isViewMode"> |
| | | <el-select v-model="form.checkResult" |
| | | :disabled="isViewMode"> |
| | | <el-option label="åæ ¼" |
| | | value="åæ ¼" /> |
| | | <el-option label="ä¸åæ ¼" |
| | |
| | | @click="submitForm">确认</el-button> |
| | | <el-button @click="closeDia">åæ¶</el-button> |
| | | </template> |
| | | <el-button v-else @click="closeDia">å
³é</el-button> |
| | | <el-button v-else |
| | | @click="closeDia">å
³é</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | |
| | | checkName: [{ required: false, message: "请è¾å
¥", trigger: "blur" }], |
| | | productId: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | productModelId: [{ required: true, message: "è¯·éæ©", trigger: "change" }], |
| | | testStandardId: [{ required: false, message: "è¯·éæ©ææ ", trigger: "change" }], |
| | | testStandardId: [ |
| | | { required: false, message: "è¯·éæ©ææ ", trigger: "change" }, |
| | | ], |
| | | unit: [{ required: false, message: "请è¾å
¥", trigger: "blur" }], |
| | | quantity: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | qualifiedQuantity: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | unqualifiedQuantity: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | unqualifiedQuantity: [ |
| | | { required: true, message: "请è¾å
¥", trigger: "blur" }, |
| | | ], |
| | | checkCompany: [{ required: false, message: "请è¾å
¥", trigger: "blur" }], |
| | | checkResult: [{ required: true, message: "请è¾å
¥", trigger: "change" }], |
| | | }, |
| | |
| | | const userList = ref([]); |
| | | const { form, rules } = toRefs(data); |
| | | // æ¯å¦ä¸ºæ¥çæ¨¡å¼ |
| | | const isViewMode = computed(() => operationType.value === 'view'); |
| | | const isViewMode = computed(() => operationType.value === "view"); |
| | | // ç¼è¾æ¶ï¼productMainId æ purchaseLedgerId 任䏿å¼åå·¥åºãæ°éç½®ç° |
| | | const processQuantityDisabled = computed(() => { |
| | | const v = form.value || {}; |
| | |
| | | modelOptions.value.find(item => item.id == value)?.unit || ""; |
| | | }; |
| | | |
| | | const handleQualifiedQuantityChange = (value) => { |
| | | const handleQualifiedQuantityChange = value => { |
| | | if (value === null || value === undefined) { |
| | | form.value.qualifiedQuantity = 0; |
| | | return; |
| | |
| | | form.value.unqualifiedQuantity = Math.max(0, quantity - qualified); |
| | | }; |
| | | |
| | | const handleUnqualifiedQuantityChange = (value) => { |
| | | const handleUnqualifiedQuantityChange = value => { |
| | | if (value === null || value === undefined) { |
| | | form.value.unqualifiedQuantity = 0; |
| | | return; |
| | | } |
| | | const quantity = parseFloat(form.value.quantity) || 0; |
| | | const unqualified = parseFloat(value) || 0; |
| | | form.value.unqualifiedQuantity = unqualified > quantity?quantity:unqualified; |
| | | form.value.unqualifiedQuantity = |
| | | unqualified > quantity ? quantity : unqualified; |
| | | form.value.qualifiedQuantity = Math.max(0, quantity - unqualified); |
| | | }; |
| | | |
| | |
| | | tableData.value = res.data || []; |
| | | tableData.value = tableData.value.map(item => ({ |
| | | ...item, |
| | | id: null |
| | | id: null, |
| | | })); |
| | | }) |
| | | .catch(error => { |
| | |
| | | <template> |
| | | <div> |
| | | <el-dialog |
| | | v-model="dialogFormVisible" |
| | | <el-dialog v-model="dialogFormVisible" |
| | | :title="operationType === 'add' ? 'æ°å¢åæææ£éª' : operationType === 'view' ? 'æ¥çåæææ£éª' : 'ç¼è¾åæææ£éª'" |
| | | width="70%" |
| | | @close="closeDia" |
| | | > |
| | | <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef"> |
| | | @close="closeDia"> |
| | | <el-form :model="form" |
| | | label-width="140px" |
| | | label-position="top" |
| | | :rules="rules" |
| | | ref="formRef"> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ä¾åºåï¼" prop="supplier"> |
| | | <el-select |
| | | v-model="form.supplier" |
| | | <el-form-item label="ä¾åºåï¼" |
| | | prop="supplier"> |
| | | <el-select v-model="form.supplier" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | :disabled="isViewMode || supplierQuantityDisabled" |
| | | > |
| | | <el-option |
| | | v-for="item in supplierList" |
| | | :disabled="isViewMode || supplierQuantityDisabled"> |
| | | <el-option v-for="item in supplierList" |
| | | :key="item.id" |
| | | :label="item.supplierName" |
| | | :value="item.supplierName" |
| | | /> |
| | | :value="item.supplierName" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="产ååç§°ï¼" prop="productId"> |
| | | <el-tree-select |
| | | v-model="form.productId" |
| | | <el-form-item label="产ååç§°ï¼" |
| | | prop="productId"> |
| | | <el-tree-select v-model="form.productId" |
| | | placeholder="è¯·éæ©" |
| | | filterable |
| | | clearable |
| | | check-strictly |
| | | @change="getModels" |
| | | :data="productOptions" |
| | | :render-after-expand="false" |
| | | :disabled="isViewMode || operationType === 'edit'" |
| | | style="width: 100%" |
| | | /> |
| | | style="width: 100%" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="è§æ ¼åå·ï¼" prop="productModelId"> |
| | | <el-select v-model="form.productModelId" placeholder="è¯·éæ©" clearable :disabled="isViewMode || operationType === 'edit'" |
| | | filterable readonly @change="handleChangeModel"> |
| | | <el-option v-for="item in modelOptions" :key="item.id" :label="item.model" :value="item.id" /> |
| | | <el-form-item label="è§æ ¼åå·ï¼" |
| | | prop="productModelId"> |
| | | <el-select v-model="form.productModelId" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | :disabled="isViewMode || operationType === 'edit'" |
| | | filterable |
| | | readonly |
| | | @change="handleChangeModel"> |
| | | <el-option v-for="item in modelOptions" |
| | | :key="item.id" |
| | | :label="item.model" |
| | | :value="item.id" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ææ éæ©ï¼" prop="testStandardId"> |
| | | <el-select |
| | | v-model="form.testStandardId" |
| | | <el-form-item label="ææ éæ©ï¼" |
| | | prop="testStandardId"> |
| | | <el-select v-model="form.testStandardId" |
| | | placeholder="è¯·éæ©ææ " |
| | | clearable |
| | | @change="handleTestStandardChange" |
| | | style="width: 100%" |
| | | :disabled="isViewMode" |
| | | > |
| | | <el-option |
| | | v-for="item in testStandardOptions" |
| | | :disabled="isViewMode"> |
| | | <el-option v-for="item in testStandardOptions" |
| | | :key="item.id" |
| | | :label="item.standardName || item.standardNo" |
| | | :value="item.id" |
| | | /> |
| | | :value="item.id" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="åä½ï¼" prop="unit"> |
| | | <el-input v-model="form.unit" disabled/> |
| | | <el-form-item label="åä½ï¼" |
| | | prop="unit"> |
| | | <el-input v-model="form.unit" |
| | | disabled /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ°éï¼" prop="quantity"> |
| | | <el-input-number :step="0.01" :min="0" style="width: 100%" v-model="form.quantity" placeholder="请è¾å
¥" |
| | | clearable :precision="2" :disabled="isViewMode || supplierQuantityDisabled"/> |
| | | <el-form-item label="æ°éï¼" |
| | | prop="quantity"> |
| | | <el-input-number :step="0.01" |
| | | :min="0" |
| | | style="width: 100%" |
| | | v-model="form.quantity" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | :precision="2" |
| | | :disabled="isViewMode || supplierQuantityDisabled" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="åæ ¼æ°éï¼" prop="qualifiedQuantity"> |
| | | <el-input-number :step="0.01" :min="0" :max="form.quantity || 0" style="width: 100%" |
| | | v-model="form.qualifiedQuantity" placeholder="请è¾å
¥" :precision="2" |
| | | @change="onQualifiedChange" :disabled="isViewMode"/> |
| | | <el-form-item label="åæ ¼æ°éï¼" |
| | | prop="qualifiedQuantity"> |
| | | <el-input-number :step="0.01" |
| | | :min="0" |
| | | :max="form.quantity || 0" |
| | | style="width: 100%" |
| | | v-model="form.qualifiedQuantity" |
| | | placeholder="请è¾å
¥" |
| | | :precision="2" |
| | | @change="onQualifiedChange" |
| | | :disabled="isViewMode" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ä¸åæ ¼æ°éï¼" prop="unqualifiedQuantity"> |
| | | <el-input-number :step="0.01" :min="0" :max="form.quantity || 0" style="width: 100%" |
| | | v-model="form.unqualifiedQuantity" placeholder="请è¾å
¥" :precision="2" |
| | | @change="onUnqualifiedChange" :disabled="isViewMode"/> |
| | | <el-form-item label="ä¸åæ ¼æ°éï¼" |
| | | prop="unqualifiedQuantity"> |
| | | <el-input-number :step="0.01" |
| | | :min="0" |
| | | :max="form.quantity || 0" |
| | | style="width: 100%" |
| | | v-model="form.unqualifiedQuantity" |
| | | placeholder="请è¾å
¥" |
| | | :precision="2" |
| | | @change="onUnqualifiedChange" |
| | | :disabled="isViewMode" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ£æµåä½ï¼" prop="checkCompany"> |
| | | <el-input v-model="form.checkCompany" placeholder="请è¾å
¥" clearable :disabled="isViewMode"/> |
| | | <el-form-item label="æ£æµåä½ï¼" |
| | | prop="checkCompany"> |
| | | <el-input v-model="form.checkCompany" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | :disabled="isViewMode" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ£æµç»æï¼" prop="checkResult"> |
| | | <el-select v-model="form.checkResult" :disabled="isViewMode"> |
| | | <el-option label="åæ ¼" value="åæ ¼"/> |
| | | <el-option label="ä¸åæ ¼" value="ä¸åæ ¼"/> |
| | | <el-option label="é¨ååæ ¼" value="é¨ååæ ¼"/> |
| | | <el-form-item label="æ£æµç»æï¼" |
| | | prop="checkResult"> |
| | | <el-select v-model="form.checkResult" |
| | | :disabled="isViewMode"> |
| | | <el-option label="åæ ¼" |
| | | value="åæ ¼" /> |
| | | <el-option label="ä¸åæ ¼" |
| | | value="ä¸åæ ¼" /> |
| | | <el-option label="é¨ååæ ¼" |
| | | value="é¨ååæ ¼" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ£éªåï¼" prop="checkName"> |
| | | <el-select v-model="form.checkName" placeholder="è¯·éæ©" clearable style="width: 100%" :disabled="isViewMode"> |
| | | <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName" :value="item.nickName"/> |
| | | <el-form-item label="æ£éªåï¼" |
| | | prop="checkName"> |
| | | <el-select v-model="form.checkName" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | style="width: 100%" |
| | | :disabled="isViewMode"> |
| | | <el-option v-for="item in userList" |
| | | :key="item.nickName" |
| | | :label="item.nickName" |
| | | :value="item.nickName" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ£æµæ¥æï¼" prop="checkTime"> |
| | | <el-date-picker |
| | | v-model="form.checkTime" |
| | | <el-form-item label="æ£æµæ¥æï¼" |
| | | prop="checkTime"> |
| | | <el-date-picker v-model="form.checkTime" |
| | | type="date" |
| | | placeholder="è¯·éæ©æ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | clearable |
| | | style="width: 100%" |
| | | :disabled="isViewMode" |
| | | /> |
| | | :disabled="isViewMode" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | |
| | | <!-- <div style="margin-bottom: 10px;text-align: right">--> |
| | | <!-- <el-button type="danger" plain @click="handleDelete">å é¤</el-button>--> |
| | | <!-- </div>--> |
| | | <PIMTable |
| | | rowKey="id" |
| | | <PIMTable rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :tableLoading="tableLoading" |
| | | height="400" |
| | | > |
| | | height="400"> |
| | | <template #slot="{ row }"> |
| | | <el-input v-model="row.testValue" clearable :disabled="isViewMode"/> |
| | | <el-input v-model="row.testValue" |
| | | clearable |
| | | :disabled="isViewMode" /> |
| | | </template> |
| | | </PIMTable> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <template v-if="!isViewMode"> |
| | | <el-button type="primary" @click="submitForm">确认</el-button> |
| | | <el-button type="primary" |
| | | @click="submitForm">确认</el-button> |
| | | <el-button @click="closeDia">åæ¶</el-button> |
| | | </template> |
| | | <el-button v-else @click="closeDia">å
³é</el-button> |
| | | <el-button v-else |
| | | @click="closeDia">å
³é</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import {ref, reactive, toRefs, computed, getCurrentInstance, nextTick} from "vue"; |
| | | import { |
| | | ref, |
| | | reactive, |
| | | toRefs, |
| | | computed, |
| | | getCurrentInstance, |
| | | nextTick, |
| | | } from "vue"; |
| | | import {getOptions} from "@/api/procurementManagement/procurementLedger.js"; |
| | | import {modelList, productTreeList} from "@/api/basicData/product.js"; |
| | | import {qualityInspectAdd, qualityInspectUpdate} from "@/api/qualityManagement/rawMaterialInspection.js"; |
| | | import {qualityInspectParamDel, qualityInspectParamInfo} from "@/api/qualityManagement/qualityInspectParam.js"; |
| | | import {qualityInspectDetailByProductId, getQualityTestStandardParamByTestStandardId} from "@/api/qualityManagement/metricMaintenance.js"; |
| | | import { |
| | | qualityInspectAdd, |
| | | qualityInspectUpdate, |
| | | } from "@/api/qualityManagement/rawMaterialInspection.js"; |
| | | import { |
| | | qualityInspectParamDel, |
| | | qualityInspectParamInfo, |
| | | } from "@/api/qualityManagement/qualityInspectParam.js"; |
| | | import { |
| | | qualityInspectDetailByProductId, |
| | | getQualityTestStandardParamByTestStandardId, |
| | | } from "@/api/qualityManagement/metricMaintenance.js"; |
| | | import {userListNoPage} from "@/api/system/user.js"; |
| | | |
| | | const {proxy} = getCurrentInstance() |
| | | const emit = defineEmits(['close']) |
| | | const { proxy } = getCurrentInstance(); |
| | | const emit = defineEmits(["close"]); |
| | | |
| | | const dialogFormVisible = ref(false); |
| | | const operationType = ref('') |
| | | const operationType = ref(""); |
| | | const data = reactive({ |
| | | form: { |
| | | checkTime: "", |
| | |
| | | checkResult: "", |
| | | }, |
| | | rules: { |
| | | checkTime: [{required: true, message: "请è¾å
¥", trigger: "blur"},], |
| | | checkTime: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | supplier: [{required: true, message: "请è¾å
¥", trigger: "blur"}], |
| | | checkName: [{required: false, message: "请è¾å
¥", trigger: "blur"}], |
| | | productId: [{required: true, message: "请è¾å
¥", trigger: "blur"}], |
| | | productModelId: [{required: true, message: "è¯·éæ©äº§ååå·", trigger: "change"}], |
| | | testStandardId: [{required: false, message: "è¯·éæ©ææ ", trigger: "change"}], |
| | | productModelId: [ |
| | | { required: true, message: "è¯·éæ©äº§ååå·", trigger: "change" }, |
| | | ], |
| | | testStandardId: [ |
| | | { required: false, message: "è¯·éæ©ææ ", trigger: "change" }, |
| | | ], |
| | | unit: [{required: false, message: "请è¾å
¥", trigger: "blur"}], |
| | | quantity: [{required: true, message: "请è¾å
¥", trigger: "blur"}], |
| | | qualifiedQuantity: [{required: true, message: "请è¾å
¥", trigger: "blur"}], |
| | | unqualifiedQuantity: [{required: true, message: "请è¾å
¥", trigger: "blur"}], |
| | | unqualifiedQuantity: [ |
| | | { required: true, message: "请è¾å
¥", trigger: "blur" }, |
| | | ], |
| | | checkCompany: [{required: false, message: "请è¾å
¥", trigger: "blur"}], |
| | | checkResult: [{required: true, message: "è¯·éæ©æ£æµç»æ", trigger: "change"}], |
| | | checkResult: [ |
| | | { required: true, message: "è¯·éæ©æ£æµç»æ", trigger: "change" }, |
| | | ], |
| | | }, |
| | | }); |
| | | const tableColumn = ref([ |
| | |
| | | { |
| | | label: "æ£éªå¼", |
| | | prop: "testValue", |
| | | dataType: 'slot', |
| | | slot: 'slot', |
| | | dataType: "slot", |
| | | slot: "slot", |
| | | }, |
| | | ]); |
| | | const tableData = ref([]); |
| | |
| | | const userList = ref([]); // æ£éªå䏿å表 |
| | | |
| | | // æ¯å¦ä¸ºæ¥çæ¨¡å¼ |
| | | const isViewMode = computed(() => operationType.value === 'view'); |
| | | const isViewMode = computed(() => operationType.value === "view"); |
| | | |
| | | // ç¼è¾æ¶ï¼productMainId æ purchaseLedgerId 任䏿å¼åä¾åºåãæ°éç½®ç° |
| | | const supplierQuantityDisabled = computed(() => { |
| | |
| | | // æå¼å¼¹æ¡ |
| | | const openDialog = async (type, row) => { |
| | | operationType.value = type; |
| | | getOptions().then((res) => { |
| | | getOptions().then(res => { |
| | | supplierList.value = res.data; |
| | | }); |
| | | |
| | |
| | | quantity: "", |
| | | checkCompany: "", |
| | | checkResult: "", |
| | | } |
| | | }; |
| | | testStandardOptions.value = []; |
| | | tableData.value = []; |
| | | // å
ç¡®ä¿äº§åæ å·²å è½½ï¼å¦åç¼è¾æ¶äº§å/è§æ ¼åå·æ æ³åæ¾ |
| | | await getProductOptions(); |
| | | if (operationType.value === 'edit' || operationType.value === 'view') { |
| | | if (operationType.value === "edit" || operationType.value === "view") { |
| | | // å
ä¿å testStandardIdï¼é¿å
被æ¸
空 |
| | | const savedTestStandardId = row.testStandardId; |
| | | form.value = {...row} |
| | | currentProductId.value = row.productId || 0 |
| | | form.value = { ...row }; |
| | | currentProductId.value = row.productId || 0; |
| | | // å
³é®ï¼ç¼è¾æ¶å è½½è§æ ¼åå·ä¸æéé¡¹ï¼æè½åæ¾ productModelId |
| | | if (currentProductId.value) { |
| | | try { |
| | |
| | | // å
å è½½ææ é项 |
| | | let params = { |
| | | productId: currentProductId.value, |
| | | inspectType: 0 |
| | | } |
| | | inspectType: 0, |
| | | }; |
| | | qualityInspectDetailByProductId(params).then(res => { |
| | | testStandardOptions.value = res.data || []; |
| | | // ä½¿ç¨ nextTick å setTimeout ç¡®ä¿éé¡¹å·²ç»æ¸²æå° DOM |
| | |
| | | // 妿ç¼è¾æ°æ®ä¸æ testStandardIdï¼å设置并å 载对åºçåæ° |
| | | if (savedTestStandardId) { |
| | | // ç¡®ä¿ç±»åå¹é
ï¼item.id å¯è½æ¯æ°åæåç¬¦ä¸²ï¼ |
| | | const matchedOption = testStandardOptions.value.find(item => |
| | | item.id == savedTestStandardId || String(item.id) === String(savedTestStandardId) |
| | | const matchedOption = testStandardOptions.value.find( |
| | | item => |
| | | item.id == savedTestStandardId || |
| | | String(item.id) === String(savedTestStandardId) |
| | | ); |
| | | if (matchedOption) { |
| | | // ç¡®ä¿ä½¿ç¨å¹é
项ç idï¼ä¿æç±»åä¸è´ï¼ |
| | |
| | | getQualityInspectParamList(row.id); |
| | | } else { |
| | | // 妿æ¾ä¸å°å¹é
项ï¼å°è¯ç´æ¥ä½¿ç¨åå¼ |
| | | console.warn('æªæ¾å°å¹é
çææ é项ï¼testStandardId:', savedTestStandardId, 'å¯ç¨é项:', testStandardOptions.value); |
| | | console.warn( |
| | | "æªæ¾å°å¹é
çææ é项ï¼testStandardId:", |
| | | savedTestStandardId, |
| | | "å¯ç¨é项:", |
| | | testStandardOptions.value |
| | | ); |
| | | form.value.testStandardId = savedTestStandardId; |
| | | getQualityInspectParamList(row.id); |
| | | } |
| | |
| | | nextTick(() => { |
| | | proxy.$refs?.formRef?.clearValidate?.(); |
| | | }); |
| | | } |
| | | }; |
| | | const getProductOptions = () => { |
| | | return productTreeList().then((res) => { |
| | | return productTreeList().then(res => { |
| | | productOptions.value = convertIdToValue(res); |
| | | return productOptions.value; |
| | | }); |
| | | }; |
| | | const getModels = (value) => { |
| | | const getModels = value => { |
| | | form.value.productModelId = undefined; |
| | | form.value.unit = undefined; |
| | | modelOptions.value = []; |
| | | currentProductId.value = value |
| | | currentProductId.value = value; |
| | | form.value.productName = findNodeById(productOptions.value, value); |
| | | modelList({ id: value }).then((res) => { |
| | | modelList({ id: value }).then(res => { |
| | | modelOptions.value = res; |
| | | }) |
| | | }); |
| | | if (currentProductId.value) { |
| | | getList(); |
| | | } |
| | | }; |
| | | |
| | | const handleChangeModel = (value) => { |
| | | form.value.model = modelOptions.value.find(item => item.id == value)?.model || ''; |
| | | form.value.unit = modelOptions.value.find(item => item.id == value)?.unit || ''; |
| | | } |
| | | const handleChangeModel = value => { |
| | | form.value.model = |
| | | modelOptions.value.find(item => item.id == value)?.model || ""; |
| | | form.value.unit = |
| | | modelOptions.value.find(item => item.id == value)?.unit || ""; |
| | | }; |
| | | |
| | | const findNodeById = (nodes, productId) => { |
| | | for (let i = 0; i < nodes.length; i++) { |
| | |
| | | }; |
| | | |
| | | function convertIdToValue(data) { |
| | | return data.map((item) => { |
| | | return data.map(item => { |
| | | const {id, children, ...rest} = item; |
| | | const newItem = { |
| | | ...rest, |
| | |
| | | const submitForm = () => { |
| | | proxy.$refs.formRef.validate(valid => { |
| | | if (valid) { |
| | | form.value.inspectType = 0 |
| | | form.value.inspectType = 0; |
| | | if (operationType.value === "add") { |
| | | tableData.value.forEach((item) => { |
| | | delete item.id |
| | | }) |
| | | tableData.value.forEach(item => { |
| | | delete item.id; |
| | | }); |
| | | } |
| | | const data = {...form.value, qualityInspectParams: tableData.value} |
| | | const data = { ...form.value, qualityInspectParams: tableData.value }; |
| | | if (operationType.value === "add") { |
| | | qualityInspectAdd(data).then(res => { |
| | | proxy.$modal.msgSuccess("æäº¤æå"); |
| | | closeDia(); |
| | | }) |
| | | }); |
| | | } else { |
| | | qualityInspectUpdate(data).then(res => { |
| | | proxy.$modal.msgSuccess("æäº¤æå"); |
| | | closeDia(); |
| | | }) |
| | | }); |
| | | } |
| | | } |
| | | }) |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const getList = () => { |
| | | if (!currentProductId.value) { |
| | |
| | | } |
| | | let params = { |
| | | productId: currentProductId.value, |
| | | inspectType: 0 |
| | | } |
| | | inspectType: 0, |
| | | }; |
| | | qualityInspectDetailByProductId(params).then(res => { |
| | | // ä¿å䏿æ¡éé¡¹æ°æ® |
| | | testStandardOptions.value = res.data || []; |
| | | // æ¸
ç©ºè¡¨æ ¼æ°æ®ï¼çå¾
ç¨æ·éæ©ææ |
| | | tableData.value = []; |
| | | // æ¸
ç©ºææ éæ© |
| | | form.value.testStandardId = ''; |
| | | }) |
| | | } |
| | | form.value.testStandardId = ""; |
| | | }); |
| | | }; |
| | | |
| | | // ææ éæ©ååå¤ç |
| | | const handleTestStandardChange = (testStandardId) => { |
| | | const handleTestStandardChange = testStandardId => { |
| | | if (!testStandardId) { |
| | | tableData.value = []; |
| | | return; |
| | | } |
| | | tableLoading.value = true; |
| | | getQualityTestStandardParamByTestStandardId(testStandardId).then(res => { |
| | | getQualityTestStandardParamByTestStandardId(testStandardId) |
| | | .then(res => { |
| | | tableData.value = res.data || []; |
| | | tableData.value = tableData.value.map(item => ({ |
| | | ...item, |
| | | id: null |
| | | id: null, |
| | | })); |
| | | }).catch(error => { |
| | | console.error('è·åæ ååæ°å¤±è´¥:', error); |
| | | tableData.value = []; |
| | | }).finally(() => { |
| | | tableLoading.value = false; |
| | | }) |
| | | } |
| | | .catch(error => { |
| | | console.error("è·åæ ååæ°å¤±è´¥:", error); |
| | | tableData.value = []; |
| | | }) |
| | | .finally(() => { |
| | | tableLoading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | const getQualityInspectParamList = (id) => { |
| | | const getQualityInspectParamList = id => { |
| | | qualityInspectParamInfo(id).then(res => { |
| | | tableData.value = res.data; |
| | | }) |
| | | } |
| | | }); |
| | | }; |
| | | // èªå¨è®¡ç®åæ ¼æ°éååæ¶çä¸åæ ¼æ°é |
| | | const onQualifiedChange = (value) => { |
| | | const onQualifiedChange = value => { |
| | | if (form.value.quantity !== undefined && form.value.quantity !== null) { |
| | | const maxUnqualified = form.value.quantity - value; |
| | | if (maxUnqualified >= 0) { |
| | |
| | | }; |
| | | |
| | | // èªå¨è®¡ç®ä¸åæ ¼æ°éååæ¶çåæ ¼æ°é |
| | | const onUnqualifiedChange = (value) => { |
| | | const onUnqualifiedChange = value => { |
| | | if (form.value.quantity !== undefined && form.value.quantity !== null) { |
| | | const maxQualified = form.value.quantity - value; |
| | | if (maxQualified >= 0) { |
| | |
| | | proxy.resetForm("formRef"); |
| | | tableData.value = []; |
| | | testStandardOptions.value = []; |
| | | form.value.testStandardId = ''; |
| | | form.value.testStandardId = ""; |
| | | dialogFormVisible.value = false; |
| | | emit('close') |
| | | emit("close"); |
| | | }; |
| | | defineExpose({ |
| | | openDialog, |
| | |
| | | </script> |
| | | |
| | | <style scoped> |
| | | |
| | | </style> |
| | |
| | | @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> |
| | |
| | | <div class="app-container indicator-stats"> |
| | | <!-- KPI æ±æ» --> |
| | | <el-row :gutter="20" class="stats-row"> |
| | | <el-col :xs="24" :sm="12" :md="8"> |
| | | <el-col :xs="24" :sm="12" :md="12"> |
| | | <div class="stat-card stat-card-blue"> |
| | | <div class="stat-icon-wrapper"> |
| | | <div class="stat-icon"> |
| | |
| | | <div class="stat-bg-decoration"></div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :xs="24" :sm="12" :md="8"> |
| | | <el-col :xs="24" :sm="12" :md="12"> |
| | | <div class="stat-card stat-card-green"> |
| | | <div class="stat-icon-wrapper"> |
| | | <div class="stat-icon"> |
| | |
| | | <div class="stat-content"> |
| | | <div class="stat-value">Â¥{{ indicatorKpis.salesAmount.toLocaleString() }}</div> |
| | | <div class="stat-label">éå®é¢</div> |
| | | </div> |
| | | <div class="stat-bg-decoration"></div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :xs="24" :sm="12" :md="8"> |
| | | <div class="stat-card stat-card-orange"> |
| | | <div class="stat-icon-wrapper"> |
| | | <div class="stat-icon"> |
| | | <el-icon :size="32"><Van /></el-icon> |
| | | </div> |
| | | </div> |
| | | <div class="stat-content"> |
| | | <div class="stat-value">{{ indicatorKpis.shipRate }}</div> |
| | | <div class="stat-label">åè´§ç</div> |
| | | </div> |
| | | <div class="stat-bg-decoration"></div> |
| | | </div> |
| | |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue' |
| | | import { Document, Van, Tickets, Search, Refresh } from '@element-plus/icons-vue' |
| | | import { Document, Tickets, Search, Refresh } from '@element-plus/icons-vue' |
| | | import * as echarts from 'echarts' |
| | | import { getTotalStatistics, getStatisticsTable } from '@/api/salesManagement/indicatorStats' |
| | | import { productTreeList } from '@/api/basicData/product.js' |
| | |
| | | show-overflow-tooltip |
| | | :formatter="formattedNumber" |
| | | width="200" /> |
| | | <el-table-column label="éè´§éé¢(å
)" |
| | | prop="returnAmount" |
| | | show-overflow-tooltip |
| | | :formatter="formattedNumber" |
| | | width="200" /> |
| | | <el-table-column label="åºæ¶éé¢(å
)" |
| | | prop="receiptableAmount" |
| | | show-overflow-tooltip |
| | |
| | | width="200" /> |
| | | <el-table-column label="忬¾éé¢(å
)" |
| | | prop="receiptPaymentAmount" |
| | | show-overflow-tooltip |
| | | :formatter="formattedNumber" |
| | | width="200" /> |
| | | <el-table-column label="éè´§éé¢(å
)" |
| | | prop="returnAmount" |
| | | show-overflow-tooltip |
| | | :formatter="formattedNumber" |
| | | width="200" /> |
| | |
| | | const summarizeMainTable = param => { |
| | | return proxy.summarizeTable( |
| | | param, |
| | | ["invoiceTotal", "receiptPaymentAmount", "unReceiptPaymentAmount"], |
| | | [ |
| | | "contractAmounts", |
| | | "receiptPaymentAmount", |
| | | "returnAmount", |
| | | "receiptableAmount", |
| | | ], |
| | | { |
| | | ticketsNum: { noDecimal: true }, // ä¸ä¿çå°æ° |
| | | futureTickets: { noDecimal: true }, // ä¸ä¿çå°æ° |
| | |
| | | const summarizeMainTable1 = param => { |
| | | var summarizeTable = proxy.summarizeTable( |
| | | param, |
| | | ["contractAmount", "receiptPaymentAmount", "receiptableAmount"], |
| | | [ |
| | | "contractAmount", |
| | | "receiptPaymentAmount", |
| | | "returnAmount", |
| | | "receiptableAmount", |
| | | ], |
| | | { |
| | | ticketsNum: { noDecimal: true }, // ä¸ä¿çå°æ° |
| | | futureTickets: { noDecimal: true }, // ä¸ä¿çå°æ° |
| | |
| | | <el-descriptions-item label="éå®åå·">{{ detail.salesContractNo }}</el-descriptions-item> |
| | | <el-descriptions-item label="ä¸å¡å">{{ detail.salesman }}</el-descriptions-item> |
| | | <el-descriptions-item label="å
³èåè´§åå·">{{ detail.shippingNo }}</el-descriptions-item> |
| | | <el-descriptions-item label="åè´§ç±»å">{{ getShippingTypeText(detail.shippingType) }}</el-descriptions-item> |
| | | <el-descriptions-item v-if="String(detail.shippingType) === '1'" label="å货车çå·">{{ detail.truckPlateNo || "--" }}</el-descriptions-item> |
| | | <template v-else-if="String(detail.shippingType) === '2'"> |
| | | <el-descriptions-item label="å¿«éå
¬å¸">{{ detail.expressCompany || "--" }}</el-descriptions-item> |
| | | <el-descriptions-item label="å¿«éåå·">{{ detail.expressNo || "--" }}</el-descriptions-item> |
| | | </template> |
| | | <!-- <el-descriptions-item label="项ç®åç§°">{{ detail.projectName }}</el-descriptions-item> --> |
| | | <el-descriptions-item label="å¶å人">{{ detail.maker }}</el-descriptions-item> |
| | | <el-descriptions-item label="å¶åæ¶é´">{{ detail.makeTime }}</el-descriptions-item> |
| | |
| | | <div style="padding-top: 20px"> |
| | | <span class="descriptions">产åå表</span> |
| | | <PIMTable :isShowPagination="false" rowKey="id" :column="tableColumn" :tableData="tableData"> |
| | | <template #totalReturnNum="{ row }"> |
| | | {{ calcAlreadyReturned(row) }} |
| | | </template> |
| | | </PIMTable> |
| | | </div> |
| | | </div> |
| | |
| | | const detail = ref({}); |
| | | const tableData = ref([]); |
| | | const availableProducts = ref([]); |
| | | const shippingTypeMap = { |
| | | 1: "货车", |
| | | 2: "å¿«é", |
| | | }; |
| | | |
| | | const sameKey = (a, b) => a != null && b != null && String(a) === String(b); |
| | | |
| | |
| | | if (!Number.isFinite(total) || !Number.isFinite(un)) return 0; |
| | | return Math.max(total - un, 0); |
| | | }; |
| | | |
| | | const getShippingTypeText = (value) => shippingTypeMap[String(value)] || shippingTypeMap[value] || "--"; |
| | | |
| | | /** 详æ
è¡¨ç¨ productName / modelï¼åå¹¶æ¶å¿è®©ç©ºä¸²çæåºåºè¡å段 */ |
| | | const mergeDetailProductRow = (product, normalized) => { |
| | |
| | | {align: "center", label: "è§æ ¼åå·", prop: "model"}, |
| | | {align: "center", label: "åä½", prop: "unit", width: 80}, |
| | | {align: "center", label: "æ»æ°é", prop: "stockOutNum", width: 120}, |
| | | {align: "center", label: "å·²éè´§æ°é", prop: "totalReturnNum", width: 120, dataType: "slot", slot: "totalReturnNum"}, |
| | | {align: "center", label: "å·²éè´§æ°é", prop: "totalReturnNum", width: 120}, |
| | | {align: "center", label: "æªéè´§æ°é", prop: "unQuantity", width: 120}, |
| | | {align: "center", label: "å¾
å¤çéè´§æ°é", prop: "pendingReturnNum", width: 120}, |
| | | {align: "center", label: "éè´§æ°é", prop: "returnQuantity", width: 120}, |
| | | {align: "center", label: "é货产ååä»·", prop: "price", width: 120}, |
| | | {align: "center", label: "é货产åéé¢", prop: "amount", width: 120}, |
| | |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="4"> |
| | | <el-form-item label="åè´§ç±»åï¼" prop="shippingType"> |
| | | <el-select v-model="form.shippingType" placeholder="è¯·éæ©åè´§ç±»å" @change="handleShippingTypeChange"> |
| | | <el-option label="货车" :value="1" /> |
| | | <el-option label="å¿«é" :value="2" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="4" v-if="form.shippingType === 1"> |
| | | <el-form-item label="å货车çå·ï¼" prop="truckPlateNo"> |
| | | <el-input v-model="form.truckPlateNo" placeholder="请è¾å
¥å货车çå·" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <template v-else-if="form.shippingType === 2"> |
| | | <el-col :span="4"> |
| | | <el-form-item label="å¿«éå
¬å¸ï¼" prop="expressCompany"> |
| | | <el-input v-model="form.expressCompany" placeholder="请è¾å
¥å¿«éå
¬å¸" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="4"> |
| | | <el-form-item label="å¿«éåå·ï¼" prop="expressNo"> |
| | | <el-input v-model="form.expressNo" placeholder="请è¾å
¥å¿«éåå·" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </template> |
| | | <el-col :span="4"> |
| | | <el-form-item label="鿬¾æ»é¢ï¼" prop="refundAmount"> |
| | | <el-input v-model="form.refundAmount" disabled placeholder="èªå¨è®¡ç®" /> |
| | | </el-form-item> |
| | |
| | | <el-button type="primary" @click="openProductSelection" :disabled="!form.shippingId">æ·»å 产å</el-button> |
| | | </div> |
| | | <PIMTable :isShowPagination="false" rowKey="id" :column="tableColumn" :tableData="tableData"> |
| | | <template #totalReturnNum="{ row }"> |
| | | {{ calcAlreadyReturned(row) }} |
| | | </template> |
| | | <template #returnQuantity="{ row }"> |
| | | <el-input |
| | | v-model="row.returnQuantity" |
| | |
| | | <el-table-column align="center" prop="specificationModel" label="è§æ ¼åå·" /> |
| | | <el-table-column align="center" prop="unit" label="åä½" /> |
| | | <el-table-column align="center" prop="stockOutNum" label="æ»æ°é" /> |
| | | <el-table-column align="center" label="å·²éè´§æ°é" prop="totalReturnNum"></el-table-column> |
| | | <el-table-column align="center" prop="unQuantity" label="æªéè´§æ°é" /> |
| | | <el-table-column align="center" label="å·²éè´§æ°é"> |
| | | <template #default="{ row }">{{ calcAlreadyReturned(row) }}</template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column align="center" prop="pendingReturnNum" label="å¾
å¤çéè´§æ°é" /> |
| | | </el-table> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | |
| | | returnNo: "", |
| | | customerId: "", |
| | | shippingId: "", |
| | | shippingType: "", |
| | | truckPlateNo: "", |
| | | expressCompany: "", |
| | | expressNo: "", |
| | | projectId: "", |
| | | maker: "", |
| | | makeTime: "", |
| | |
| | | }], |
| | | customerId: [{ required: true, message: "è¯·éæ©å®¢æ·", trigger: "change" }], |
| | | shippingId: [{ required: true, message: "è¯·éæ©å
³èåºåºåå·", trigger: "change" }], |
| | | shippingType: [{ required: true, message: "è¯·éæ©åè´§ç±»å", trigger: "change" }], |
| | | } |
| | | }); |
| | | const { form, rules } = toRefs(data); |
| | |
| | | {align: "center", label: "è§æ ¼åå·", prop: "specificationModel" }, |
| | | {align: "center", label: "åä½", prop: "unit", width: 80 }, |
| | | {align: "center", label: "æ»æ°é", prop: "stockOutNum", width: 120 }, |
| | | {align: "center", label: "å·²éè´§æ°é", prop: "totalReturnNum", width: 120, dataType: "slot", slot: "totalReturnNum" }, |
| | | {align: "center", label: "å·²éè´§æ°é", prop: "totalReturnNum", width: 120 }, |
| | | {align: "center", label: "å¾
å¤çéè´§æ°é", prop: "pendingReturnNum", width: 120 }, |
| | | {align: "center", label: "æªéè´§æ°é", prop: "unQuantity", width: 120 }, |
| | | {align: "center", label: "éè´§æ°é", prop: "returnQuantity", dataType: "slot", slot: "returnQuantity", width: 120 }, |
| | | {align: "center", label: "é货产ååä»·", prop: "price", dataType: "slot", slot: "price", width: 120 }, |
| | |
| | | returnNo: "", |
| | | customerId: "", |
| | | shippingId: "", |
| | | shippingType: "", |
| | | truckPlateNo: "", |
| | | expressCompany: "", |
| | | expressNo: "", |
| | | projectId: "", |
| | | maker: "", |
| | | makeTime: "", |
| | |
| | | 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, |
| | |
| | | const payload = { ...form.value, returnSaleProducts }; |
| | | delete payload.returnNoCheckbox; |
| | | if (operationType.value === "add" && form.value.returnNoCheckbox) delete payload.returnNo; |
| | | if (payload.shippingType !== 1) payload.truckPlateNo = ""; |
| | | if (payload.shippingType !== 2) { |
| | | payload.expressCompany = ""; |
| | | payload.expressNo = ""; |
| | | } |
| | | if (operationType.value === "add") { |
| | | returnManagementAdd(payload).then(() => { |
| | | proxy.$modal.msgSuccess("æ°å¢æå"); |
| | |
| | | proxy.resetForm("formRef"); |
| | | dialogFormVisible.value = false; |
| | | emit('close'); |
| | | data.form = { |
| | | returnNoCheckbox: true, |
| | | returnNo: "", |
| | | customerId: "", |
| | | shippingId: "", |
| | | shippingType: "", |
| | | truckPlateNo: "", |
| | | expressCompany: "", |
| | | expressNo: "", |
| | | projectId: "", |
| | | maker: "", |
| | | makeTime: "", |
| | | status: 0, |
| | | returnReason: "", |
| | | refundAmount: "", |
| | | } |
| | | }; |
| | | |
| | | const initCustomers = async () => { |
| | |
| | | const handleReturnNoCheckboxChange = (checked) => { |
| | | if (checked) form.value.returnNo = ""; |
| | | formRef.value?.validateField('returnNo'); |
| | | }; |
| | | |
| | | const handleShippingTypeChange = (val) => { |
| | | if (val === 1) { |
| | | form.value.expressCompany = ""; |
| | | form.value.expressNo = ""; |
| | | } else if (val === 2) { |
| | | form.value.truckPlateNo = ""; |
| | | } else { |
| | | form.value.truckPlateNo = ""; |
| | | form.value.expressCompany = ""; |
| | | form.value.expressNo = ""; |
| | | } |
| | | }; |
| | | |
| | | const customerNameChange = async (val, clearDownstream = true) => { |
| | |
| | | |
| | | const handleReturnQuantityChange = (val, row) => { |
| | | if (val === "" || val === null) return; |
| | | const max = row.unQuantity === undefined || row.unQuantity === null ? Infinity : Number(row.unQuantity || 0); |
| | | const unQuantity = row.unQuantity === undefined || row.unQuantity === null ? Infinity : Number(row.unQuantity || 0);//æªéè´§æ°é |
| | | const pendingReturnNum = row.pendingReturnNum === undefined || row.pendingReturnNum === null ? Infinity : Number(row.pendingReturnNum || 0);//å¾
å¤çéè´§æ°é |
| | | //æå¤§éè´§æ°éï¼å¦æä¸ºç¼è¾ç¶æå ä¸å¾
å¤çéè´§æ°é |
| | | const max = operationType.value === 'edit' ? unQuantity + pendingReturnNum : unQuantity |
| | | const current = Number(val); |
| | | |
| | | if (current > max) { |
| | |
| | | <template #status="{ row }"> |
| | | <el-tag :type="getStatusType(row.status)">{{ getStatusText(row.status) }}</el-tag> |
| | | </template> |
| | | <template #stockInApprovalStatus="{ row }"> |
| | | <el-tag :type="getStockInApprovalStatusType(row.stockInApprovalStatus)">{{ getStockInApprovalStatusText(row.stockInApprovalStatus) }}</el-tag> |
| | | </template> |
| | | </PIMTable> |
| | | </div> |
| | | <form-dia ref="formDia" @close="handleQuery" /> |
| | |
| | | const defaultColumns = [ |
| | | { label: "éè´§åå·", prop: "returnNo", minWidth: 160 }, |
| | | { label: "åæ®ç¶æ", prop: "status", minWidth: 90, dataType: "slot", slot: "status" }, |
| | | { label: "å
¥åºå®¡æ¹ç¶æ", prop: "stockInApprovalStatus", minWidth: 120, dataType: "slot", slot: "stockInApprovalStatus" }, |
| | | { label: "å¶åæ¶é´", prop: "makeTime", minWidth: 170 }, |
| | | { label: "客æ·åç§°", prop: "customerName", minWidth: 220 }, |
| | | { label: "éå®åå·", prop: "salesContractNo", minWidth: 160 }, |
| | | { label: "ä¸å¡å", prop: "salesman", minWidth: 120 }, |
| | | { label: "å
³èåè´§åå·", prop: "shippingNo", minWidth: 170 }, |
| | | { |
| | | label: "åè´§ç±»å", |
| | | prop: "shippingType", |
| | | minWidth: 100, |
| | | formatData: (val) => ({ 1: "货车", 2: "å¿«é" }[String(val)] || "--"), |
| | | }, |
| | | { label: "å货车çå·", prop: "truckPlateDisplay", minWidth: 140 }, |
| | | { label: "å¿«éå
¬å¸", prop: "expressCompanyDisplay", minWidth: 140 }, |
| | | { label: "å¿«éåå·", prop: "expressNoDisplay", minWidth: 150 }, |
| | | { label: "项ç®åç§°", prop: "projectName", minWidth: 180 }, |
| | | { label: "å¶å人", prop: "maker", minWidth: 120 }, |
| | | { |
| | |
| | | tableLoading.value = true; |
| | | returnManagementList({ ...searchForm.value, ...page }).then(res => { |
| | | tableLoading.value = false; |
| | | tableData.value = res?.data?.records || []; |
| | | tableData.value = (res?.data?.records || []).map((row) => ({ |
| | | ...row, |
| | | truckPlateDisplay: String(row?.shippingType) === "1" ? (row?.truckPlateNo || "--") : "--", |
| | | expressCompanyDisplay: String(row?.shippingType) === "2" ? (row?.expressCompany || "--") : "--", |
| | | expressNoDisplay: String(row?.shippingType) === "2" ? (row?.expressNo || "--") : "--", |
| | | })); |
| | | page.total = res?.data?.total || 0; |
| | | }).finally(() => tableLoading.value = false); |
| | | }; |
| | |
| | | return statusMap[status] || "æªç¥"; |
| | | }; |
| | | |
| | | const getStockInApprovalStatusType = (status) => { |
| | | const statusMap = { |
| | | 0: "", |
| | | 1: "success", |
| | | 2: "warning" |
| | | }; |
| | | return statusMap[status] || "info"; |
| | | }; |
| | | |
| | | const getStockInApprovalStatusText = (status) => { |
| | | const statusMap = { |
| | | 0: "æªå®¡æ¹", |
| | | 1: "已审æ¹", |
| | | 2: "审æ¹ä¸" |
| | | }; |
| | | return statusMap[status] || "æªç¥"; |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getList(); |
| | | }); |
| | |
| | | </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" |
| | |
| | | </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 |
| | |
| | | <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" |
| | |
| | | return; |
| | | } |
| | | |
| | | // æ£æ¥æ¯å¦åªå©ä¸æ¡æ°æ®ï¼éå®å°è´¦è³å°ä¿ç䏿¡äº§å |
| | | if (productData.value.length <= 1) { |
| | | proxy.$modal.msgWarning("éå®å°è´¦è³å°éè¦ä¿ç䏿¡äº§åæ°æ®"); |
| | | return; |
| | | } |
| | | |
| | | if (operationType.value === "add") { |
| | | productData.value = productData.value.filter( |
| | | (item) => !productSelectedRows.value.includes(item) |
| | |
| | | |
| | | /** |
| | | * 夿æ¯å¦å¯ä»¥åè´§ |
| | | * åªæå¨äº§åç¶ææ¯å
è¶³ï¼åè´§ç¶ææ¯å¾
åè´§åå®¡æ ¸æç»çæ¶åæå¯ä»¥åè´§ |
| | | * åªæå¨äº§åç¶ææ¯å
è¶³ï¼åè´§ç¶ææ¯å¾
åè´§ã审æ¹ä¸ãå®¡æ ¸æç»ãé¨ååè´§çæ¶åæå¯ä»¥åè´§ |
| | | * @param row è¡æ°æ® |
| | | */ |
| | | const canShip = (row) => { |
| | |
| | | // è·ååè´§ç¶æ |
| | | const shippingStatus = row.shippingStatus; |
| | | |
| | | // 妿已åè´§ï¼æåè´§æ¥ææè½¦çå·ï¼ï¼ä¸è½å次åè´§ |
| | | // 妿已åè´§ï¼ä¸è½å次åè´§ |
| | | if (shippingStatus === "å·²åè´§") { |
| | | return false; |
| | | } |
| | | |
| | | // åè´§ç¶æå¿
é¡»æ¯"å¾
åè´§"æ"å®¡æ ¸æç»" |
| | | // åè´§ç¶æå¿
é¡»æ¯"å¾
åè´§"ã"审æ¹ä¸"ã"å®¡æ ¸æç»"æ"é¨ååè´§" |
| | | const statusStr = shippingStatus ? String(shippingStatus).trim() : ""; |
| | | return ( |
| | | statusStr === "å¾
åè´§" || |
| | | statusStr === "审æ¹ä¸" || |
| | | statusStr === "å®¡æ ¸æç»" || |
| | | statusStr === "é¨ååè´§" |
| | | ); |
| | |
| | | // æ£æ¥æ¯å¦å¯ä»¥åè´§ |
| | | 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 |
| | |
| | | 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:{ |