Merge branch 'refs/heads/dev_NEW_pro' into dev_宁夏_铭远环保
# Conflicts:
# multiple/config.json
已复制1个文件
已修改72个文件
已重命名1个文件
已添加51个文件
| | |
| | | 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=dell" |
| | | set "PASS=xd123456@.." |
| | | set "IP=1.15.17.182" |
| | | set "PORT=10000" |
| | | set "DIST_DIR=dist" |
| | | set "REMOTE_DIR=/home/vue/newpro-product" |
| | | set "SSH_OPTS=-o StrictHostKeyChecking=no -o ConnectTimeout=10" |
| | | :: =========================================== |
| | | |
| | | echo ========================================== |
| | | echo Vue Dist Auto Deploy Script |
| | | echo ========================================== |
| | | echo. |
| | | |
| | | :: Check if sshpass is available |
| | | where sshpass >nul 2>&1 |
| | | if %errorlevel% neq 0 ( |
| | | echo [WARN] sshpass not found, trying to install... |
| | | echo. |
| | | |
| | | where bash >nul 2>&1 |
| | | if %errorlevel% equ 0 ( |
| | | echo [INFO] Git Bash detected, trying pacman install... |
| | | bash -c "pacman -S --noconfirm sshpass" 2>nul |
| | | if !errorlevel! equ 0 ( |
| | | echo [OK] sshpass installed via pacman! |
| | | goto :check_dist |
| | | ) |
| | | ) |
| | | |
| | | set "SSHPASS_DIR=%~dp0bin" |
| | | if not exist "!SSHPASS_DIR!" mkdir "!SSHPASS_DIR!" |
| | | |
| | | echo [INFO] Downloading sshpass... |
| | | powershell -NoProfile -Command "$url='https://github.com/xhcoding/sshpass-win32/releases/download/v1.0.3/sshpass.exe'; $out='!SSHPASS_DIR!\sshpass.exe'; Write-Host \"Downloading to: $out\"; try { Invoke-WebRequest -Uri $url -OutFile $out -UseBasicParsing; exit 0 } catch { Write-Host $_.Exception.Message; exit 1 }" |
| | | if !errorlevel! neq 0 ( |
| | | echo [ERROR] Failed to download sshpass! |
| | | echo [HINT] Please install sshpass manually or use Git Bash: pacman -S sshpass |
| | | goto :error_exit |
| | | ) |
| | | |
| | | set "PATH=!SSHPASS_DIR!;%PATH%" |
| | | echo [OK] sshpass downloaded to: !SSHPASS_DIR! |
| | | echo. |
| | | ) |
| | | |
| | | :check_dist |
| | | if not exist "%DIST_DIR%" ( |
| | | echo [ERROR] Dist directory not found: %DIST_DIR% |
| | | echo [HINT] Please run 'npm run build' first! |
| | | goto :error_exit |
| | | ) |
| | | |
| | | :: Count files and calculate size |
| | | set "FILE_COUNT=0" |
| | | set "TOTAL_SIZE=0" |
| | | for /r "%DIST_DIR%" %%F in (*) do ( |
| | | set /a "FILE_COUNT+=1" |
| | | set /a "TOTAL_SIZE+=%%~zF" |
| | | ) |
| | | set /a "TOTAL_SIZE_MB=TOTAL_SIZE/1024/1024" |
| | | |
| | | echo [INFO] Dist directory: %DIST_DIR% |
| | | echo [INFO] Files count: %FILE_COUNT% |
| | | echo [INFO] Total size: %TOTAL_SIZE_MB% MB |
| | | echo [INFO] Target: %USER%@%IP%:%PORT% |
| | | echo [INFO] Remote dir: %REMOTE_DIR% |
| | | echo. |
| | | |
| | | :: Compress dist to zip |
| | | set "ZIP_FILE=dist.zip" |
| | | echo [STEP 1/3] Compressing dist to %ZIP_FILE%... |
| | | if exist "%ZIP_FILE%" del "%ZIP_FILE%" |
| | | powershell -NoProfile -Command "Compress-Archive -Path '%DIST_DIR%\*' -DestinationPath '%ZIP_FILE%' -CompressionLevel Optimal" |
| | | if %errorlevel% neq 0 ( |
| | | echo [ERROR] Compression failed! |
| | | goto :error_exit |
| | | ) |
| | | for %%F in ("%ZIP_FILE%") do set "ZIP_SIZE_MB=%%~zF" |
| | | set /a "ZIP_SIZE_MB=ZIP_SIZE_MB/1024/1024" |
| | | echo [OK] Compressed: %ZIP_SIZE_MB% MB |
| | | echo. |
| | | |
| | | :: Upload zip file |
| | | echo [STEP 2/3] Uploading %ZIP_FILE%... |
| | | sshpass -p "%PASS%" scp -P %PORT% %SSH_OPTS% "%ZIP_FILE%" %USER%@%IP%:%REMOTE_DIR%/ |
| | | if %errorlevel% neq 0 ( |
| | | echo [ERROR] Upload failed! Check network or credentials. |
| | | goto :error_exit |
| | | ) |
| | | echo [OK] Upload completed! |
| | | echo. |
| | | |
| | | :: Extract on remote server |
| | | echo [STEP 3/3] Extracting on remote server... |
| | | sshpass -p "%PASS%" ssh -p %PORT% %SSH_OPTS% %USER%@%IP% "cd '%REMOTE_DIR%' && rm -rf dist && unzip -o '%ZIP_FILE%' -d dist && rm -f '%ZIP_FILE%'" |
| | | if %errorlevel% neq 0 ( |
| | | echo [ERROR] Extraction failed! |
| | | goto :error_exit |
| | | ) |
| | | echo [OK] Extraction completed! |
| | | |
| | | :: Cleanup local zip |
| | | del "%ZIP_FILE%" |
| | | |
| | | echo. |
| | | echo ========================================== |
| | | echo [SUCCESS] Deploy completed! |
| | | echo ========================================== |
| | | goto :end |
| | | |
| | | :error_exit |
| | | echo. |
| | | echo ========================================== |
| | | echo [FAILED] Deploy failed! |
| | | echo ========================================== |
| | | |
| | | :end |
| | | pause |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | # æ°æ®æ¨¡æ - åºç¡æ°æ®æ£æµ |
| | | |
| | | ## æ¶åé¡µé¢ |
| | | |
| | | - æ°æ®æ¨¡ææä½é¡µé¢ï¼æ°å¢ï¼ |
| | | |
| | | ## 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 (计åä¸) |
| | | - ð² æ·»å åå²è®°å½æ¥è¯¢ |
| | | - ð² æ·»å æ¹éæä½åè½ |
| | | - ð² æ·»å æä»¶é¢è§åè½ |
| | | - ð² ä¼ååéåè¿åº¦æ¾ç¤º |
| | |
| | | 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", |
| | | }); |
| | | } |
| | |
| | | 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, |
| | | }); |
| | | } |
| | |
| | | }); |
| | | } |
| | | export function updateApprovalStatus(query) { |
| | | return request({ |
| | | url: "/purchase/ledger/updateApprovalStatus", |
| | | method: "post", |
| | | data: query, |
| | | }); |
| | | return request({ |
| | | url: "/purchase/ledger/updateApprovalStatus", |
| | | method: "post", |
| | | data: query, |
| | | }); |
| | | } |
| | | |
| | | // ä¿åéè´æ¨¡æ¿ |
| | | export function addPurchaseTemplate(data) { |
| | | return request({ |
| | | url: "/purchaseLedgerTemplate/add", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | return request({ |
| | | url: "/purchaseLedgerTemplate/add", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // ä¿®æ¹éè´æ¨¡æ¿ |
| | | export function updatePurchaseTemplate(data) { |
| | | return request({ |
| | | url: "/purchaseLedgerTemplate/update", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | return request({ |
| | | url: "/purchaseLedgerTemplate/update", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // æ¥è¯¢éè´æ¨¡æ¿ |
| | | export function getPurchaseTemplateList(query) { |
| | | return request({ |
| | | url: "/purchase/ledger/getPurchaseTemplateList", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | return request({ |
| | | url: "/purchase/ledger/getPurchaseTemplateList", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // å é¤éè´æ¨¡æ¿ |
| | | export function delPurchaseTemplate(id) { |
| | | return request({ |
| | | url: "/purchaseLedgerTemplate/delete", |
| | | method: "delete", |
| | | data: id, |
| | | }); |
| | | return request({ |
| | | url: "/purchaseLedgerTemplate/delete", |
| | | method: "delete", |
| | | data: id, |
| | | }); |
| | | } |
| | | // æ¥è¯¢éè´è¯¦æ
|
| | | export function getPurchaseByCode(id) { |
| | | return request({ |
| | | url: "/purchase/ledger/getPurchaseByCode", |
| | | method: "get", |
| | | params: id, |
| | | }); |
| | | } |
| | | return request({ |
| | | url: "/purchase/ledger/getPurchaseByCode", |
| | | method: "get", |
| | | params: id, |
| | | }); |
| | | } |
| | | |
| | | export function batchGeneratePurchaseInboundSteps(query) { |
| | | return request({ |
| | | url: "/purchase/ledger/batchInsertPurchaseSteps", |
| | | method: "post", |
| | | data: query, |
| | | }); |
| | | } |
| | |
| | | |
| | | export const productionOrderProgress = (params = {}) => { |
| | | const safePageNum = Math.max(1, Number(params.pageNum || 1)); |
| | | const safePageSize = Math.min(50, Math.max(1, Number(params.pageSize || 10))); |
| | | const safeTab = ["all", "inProgress", "completed", "paused"].includes(params.tab) |
| | | ? params.tab |
| | | : "all"; |
| | |
| | | ...params, |
| | | tab: safeTab, |
| | | pageNum: safePageNum, |
| | | pageSize: safePageSize, |
| | | pageSize: params.pageSize, |
| | | }, |
| | | headers: { |
| | | handleAuthError: false, |
| | |
| | | & .nest-menu .el-sub-menu > .el-sub-menu__title, |
| | | & .el-sub-menu .el-menu-item { |
| | | min-width: 0 !important; |
| | | width: calc(100% - 24px) !important; |
| | | margin: 0 12px 8px !important; |
| | | height: 46px; |
| | | line-height: 46px; |
| | | padding-left: 14px !important; |
| | | padding-right: 14px !important; |
| | | border-radius: 12px; |
| | | width: calc(100% - 32px) !important; |
| | | margin: 0 16px 6px !important; |
| | | height: 40px; |
| | | line-height: 40px; |
| | | padding-left: 12px !important; |
| | | padding-right: 12px !important; |
| | | border-radius: 8px; |
| | | transition: all 0.24s ease; |
| | | color: var(--sidebar-text); |
| | | border: 1px solid rgba(255, 255, 255, 0.06) !important; |
| | | background: linear-gradient(128deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.01)); |
| | | border: none !important; |
| | | background: transparent; |
| | | font-size: 13px; |
| | | |
| | | &:hover { |
| | | background: linear-gradient(128deg, rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.24), rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.07)) !important; |
| | | border-color: rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.3) !important; |
| | | transform: translateX(2px); |
| | | background: rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.12) !important; |
| | | transform: translateX(4px); |
| | | } |
| | | |
| | | &.is-active { |
| | | background: var(--menu-active-bg, linear-gradient(135deg, var(--el-color-primary), var(--el-color-primary-light-3))) !important; |
| | | background-size: 180% 180%; |
| | | background: rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.85) !important; |
| | | color: #fff !important; |
| | | font-weight: 500; |
| | | box-shadow: var(--menu-active-glow, 0 10px 24px rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.34)); |
| | | animation: sidebarActiveFlow 4.6s ease infinite; |
| | | box-shadow: 0 4px 12px rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.25); |
| | | } |
| | | } |
| | | |
| | | // åèå容卿 ·å¼ - å¢å 缩è¿åè§è§å±æ¬¡ |
| | | & .el-sub-menu .el-menu { |
| | | padding: 4px 0 8px; |
| | | margin-left: 8px; |
| | | border-left: 2px solid rgba(255, 255, 255, 0.08); |
| | | } |
| | | } |
| | | |
| | | .hideSidebar { |
| | |
| | | :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" |
| | |
| | | </script>
|
| | |
|
| | | <style lang="scss" scoped>
|
| | | .sidebar-item-wrapper { |
| | | :deep(.menu-icon) { |
| | | width: 26px; |
| | | height: 26px; |
| | | margin-right: 12px; |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | flex-shrink: 0; |
| | | transition: color 0.2s ease; |
| | | color: var(--sidebar-text); |
| | | opacity: 0.88; |
| | | } |
| | | |
| | | :deep(.el-menu-item:hover .menu-icon), |
| | | :deep(.el-sub-menu__title:hover .menu-icon) { |
| | | color: #ffffff; |
| | | opacity: 1; |
| | | } |
| | | .sidebar-item-wrapper {
|
| | | :deep(.menu-icon) {
|
| | | width: 26px;
|
| | | height: 26px;
|
| | | margin-right: 12px;
|
| | | display: inline-flex;
|
| | | align-items: center;
|
| | | justify-content: center;
|
| | | flex-shrink: 0;
|
| | | transition: color 0.2s ease;
|
| | | color: var(--sidebar-text);
|
| | | opacity: 0.88;
|
| | | }
|
| | | |
| | | :deep(.el-menu-item:hover .menu-icon),
|
| | | :deep(.el-sub-menu__title:hover .menu-icon) {
|
| | | color: #ffffff;
|
| | | opacity: 1;
|
| | | }
|
| | |
|
| | | :deep(.el-menu-item.is-active .menu-icon) {
|
| | | color: var(--menu-active-text) !important;
|
| | | opacity: 1;
|
| | | }
|
| | |
|
| | | :deep(.menu-title) { |
| | | font-weight: 500; |
| | | transition: color 0.2s ease; |
| | | color: var(--sidebar-text); |
| | | opacity: 0.82; |
| | | } |
| | | |
| | | :deep(.el-menu-item:hover .menu-title), |
| | | :deep(.el-sub-menu__title:hover .menu-title) { |
| | | color: #ffffff; |
| | | opacity: 1; |
| | | } |
| | | |
| | | :deep(.el-menu-item.is-active .menu-title) { |
| | | color: var(--menu-active-text) !important; |
| | | opacity: 1; |
| | | } |
| | | |
| | | :deep(.nest-menu) { |
| | | .menu-icon { |
| | | width: 22px; |
| | | height: 22px; |
| | | margin-right: 10px; |
| | | } |
| | | |
| | | .menu-title { font-size: 13px; } |
| | | } |
| | | } |
| | | :deep(.menu-title) {
|
| | | font-weight: 500;
|
| | | transition: color 0.2s ease;
|
| | | color: var(--sidebar-text);
|
| | | opacity: 0.82;
|
| | | }
|
| | | |
| | | :deep(.el-menu-item:hover .menu-title),
|
| | | :deep(.el-sub-menu__title:hover .menu-title) {
|
| | | color: #ffffff;
|
| | | opacity: 1;
|
| | | }
|
| | | |
| | | :deep(.el-menu-item.is-active .menu-title) {
|
| | | color: var(--menu-active-text) !important;
|
| | | opacity: 1;
|
| | | }
|
| | | |
| | | :deep(.nest-menu) {
|
| | | .menu-icon {
|
| | | width: 18px;
|
| | | height: 18px;
|
| | | margin-right: 8px;
|
| | | opacity: 0.7;
|
| | | }
|
| | | |
| | | .menu-title { |
| | | font-size: 13px;
|
| | | opacity: 0.85;
|
| | | }
|
| | | |
| | | .el-menu-item.is-active {
|
| | | .menu-icon {
|
| | | opacity: 1;
|
| | | }
|
| | | .menu-title {
|
| | | opacity: 1;
|
| | | }
|
| | | }
|
| | | }
|
| | | }
|
| | | </style>
|
| | |
| | | }, |
| | | ], |
| | | }, |
| | | // æå¦æ¼ç¤ºæ¨¡å |
| | | { |
| | | 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", |
| | | clickFun: row => { |
| | | 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 => { |
| | |
| | | </el-form-item> --> |
| | | <el-form-item label="å建æ¶é´" |
| | | prop="createTime"> |
| | | <el-date-picker v-model="formData.createTime" |
| | | <el-date-picker v-model="formCreateTimeDate" |
| | | type="date" |
| | | placeholder="éæ©æ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { onMounted, ref, reactive } from "vue"; |
| | | import { onMounted, ref, reactive, computed } from "vue"; |
| | | import dayjs from "dayjs"; |
| | | import { |
| | | parameterListPage, |
| | | addParameter, |
| | |
| | | }, |
| | | }, |
| | | ], |
| | | }); |
| | | const formCreateTimeDate = computed({ |
| | | get: () => (formData.createTime ? String(formData.createTime).split(" ")[0] : ""), |
| | | set: (value) => { |
| | | formData.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : ""; |
| | | }, |
| | | }); |
| | | // const productTypes = ref([]); |
| | | const isEdit = ref(false); |
| | |
| | | formData.unit = ""; |
| | | formData.remark = ""; |
| | | formData.isRequired = 0; |
| | | formData.createTime = new Date().toISOString().split("T")[0]; |
| | | formData.createTime = dayjs().format("YYYY-MM-DD HH:mm:ss"); |
| | | dialogVisible.value = true; |
| | | }; |
| | | |
| | |
| | | formData.remark = row.remark || ""; |
| | | formData.paramFormat = row.paramFormat || ""; |
| | | formData.isRequired = row.isRequired || 0; |
| | | formData.createTime = row.createTime || ""; |
| | | dialogVisible.value = true; |
| | | }; |
| | | |
| | |
| | | </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> |
| | |
| | | <template> |
| | | <div> |
| | | |
| | | <!-- ç³è¯·ç±»åéæ© --> |
| | | <el-card class="type-card"> |
| | | <div class="type-selector"> |
| | | <div |
| | | v-for="type in applicationTypes" |
| | | :key="type.value" |
| | | class="type-item" |
| | | :class="{ active: currentType === type.value }" |
| | | @click="changeType(type.value)" |
| | | > |
| | | <div class="type-icon"> |
| | | <el-icon :size="24"><component :is="type.icon"/></el-icon> |
| | | </div> |
| | | <div class="type-info"> |
| | | <div class="type-name">{{ type.name }}</div> |
| | | <div class="type-desc">{{ type.desc }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <!-- ä¼è®®ç³è¯·è¡¨å --> |
| | | <el-card> |
| | | <div class="form-header"> |
| | | <h3>{{ getCurrentTypeName() }}ç³è¯·</h3> |
| | | </div> |
| | | |
| | | <el-form |
| | | ref="meetingFormRef" |
| | | :model="meetingForm" |
| | | :rules="rules" |
| | | label-width="100px" |
| | | > |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ä¼è®®ä¸»é¢" prop="title"> |
| | | <el-input v-model="meetingForm.title" placeholder="请è¾å
¥ä¼è®®ä¸»é¢"/> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ä¼è®®å®¤" prop="roomId"> |
| | | <el-select v-model="meetingForm.roomId" placeholder="è¯·éæ©ä¼è®®å®¤" style="width: 100%"> |
| | | <el-option |
| | | v-for="room in meetingRooms" |
| | | :key="room.id" |
| | | :label="`${room.name} (${room.location})`" |
| | | :value="room.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="主æäºº" prop="host"> |
| | | <el-input v-model="meetingForm.host" placeholder="请è¾å
¥ä¸»æäººå§å"/> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ä¼è®®æ¥æ" prop="meetingDate"> |
| | | <el-date-picker |
| | | v-model="meetingForm.meetingDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©ä¼è®®æ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | :disabled-date="disabledDate" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <!-- 空åï¼ä¿æå¸å± --> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å¼å§æ¶é´" prop="startTime"> |
| | | <el-select |
| | | v-model="meetingForm.startTime" |
| | | placeholder="è¯·éæ©å¼å§æ¶é´" |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="time in startTimeOptions" |
| | | :key="time.value" |
| | | :label="time.label" |
| | | :value="time.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç»ææ¶é´" prop="endTime"> |
| | | <el-select |
| | | v-model="meetingForm.endTime" |
| | | placeholder="è¯·éæ©ç»ææ¶é´" |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="time in endTimeOptions" |
| | | :key="time.value" |
| | | :label="time.label" |
| | | :value="time.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-form-item label="åä¼äººå" prop="participants"> |
| | | <el-select |
| | | v-model="meetingForm.participants" |
| | | multiple |
| | | filterable |
| | | placeholder="è¯·éæ©åä¼äººå" |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="person in employees" |
| | | :key="person.id" |
| | | :label="`${person.staffName}${person.postName ? ` (${person.postName})` : ''}`" |
| | | :value="person.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="ä¼è®®è¯´æ" prop="description"> |
| | | <el-input |
| | | v-model="meetingForm.description" |
| | | type="textarea" |
| | | :rows="4" |
| | | placeholder="请è¾å
¥ä¼è®®è¯´æ" |
| | | /> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <div class="form-footer"> |
| | | <el-button @click="resetForm">éç½®</el-button> |
| | | <el-button type="primary" @click="submitForm">æäº¤</el-button> |
| | | </div> |
| | | </el-card> |
| | | </div> |
| | | <div> |
| | | |
| | | <!-- ç³è¯·ç±»åéæ© --> |
| | | <el-card class="type-card"> |
| | | <div class="type-selector"> |
| | | <div |
| | | v-for="type in applicationTypes" |
| | | :key="type.value" |
| | | class="type-item" |
| | | :class="{ active: currentType === type.value }" |
| | | @click="changeType(type.value)" |
| | | > |
| | | <div class="type-icon"> |
| | | <el-icon :size="24"><component :is="type.icon"/></el-icon> |
| | | </div> |
| | | <div class="type-info"> |
| | | <div class="type-name">{{ type.name }}</div> |
| | | <div class="type-desc">{{ type.desc }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <!-- ä¼è®®ç³è¯·è¡¨å --> |
| | | <el-card> |
| | | <div class="form-header"> |
| | | <h3>{{ getCurrentTypeName() }}ç³è¯·</h3> |
| | | </div> |
| | | |
| | | <el-form |
| | | ref="meetingFormRef" |
| | | :model="meetingForm" |
| | | :rules="rules" |
| | | label-width="100px" |
| | | > |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ä¼è®®ä¸»é¢" prop="title"> |
| | | <el-input v-model="meetingForm.title" placeholder="请è¾å
¥ä¼è®®ä¸»é¢"/> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ä¼è®®å®¤" prop="roomId"> |
| | | <el-select v-model="meetingForm.roomId" placeholder="è¯·éæ©ä¼è®®å®¤" style="width: 100%"> |
| | | <el-option |
| | | v-for="room in meetingRooms" |
| | | :key="room.id" |
| | | :label="`${room.name} (${room.location})`" |
| | | :value="room.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="主æäºº" prop="host"> |
| | | <el-input v-model="meetingForm.host" placeholder="请è¾å
¥ä¸»æäººå§å"/> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ä¼è®®æ¥æ" prop="meetingDate"> |
| | | <el-date-picker |
| | | v-model="meetingForm.meetingDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©ä¼è®®æ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | :disabled-date="disabledDate" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <!-- 空åï¼ä¿æå¸å± --> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å¼å§æ¶é´" prop="startTime"> |
| | | <el-time-picker |
| | | v-model="meetingForm.startTime" |
| | | placeholder="è¯·éæ©å¼å§æ¶é´" |
| | | format="HH:mm" |
| | | value-format="HH:mm" |
| | | :disabled-hours="disabledStartHours" |
| | | :disabled-minutes="disabledStartMinutes" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç»ææ¶é´" prop="endTime"> |
| | | <el-time-picker |
| | | v-model="meetingForm.endTime" |
| | | placeholder="è¯·éæ©ç»ææ¶é´" |
| | | format="HH:mm" |
| | | value-format="HH:mm" |
| | | :disabled-hours="disabledEndHours" |
| | | :disabled-minutes="disabledEndMinutes" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-form-item label="åä¼äººå" prop="participants"> |
| | | <el-select |
| | | v-model="meetingForm.participants" |
| | | multiple |
| | | filterable |
| | | placeholder="è¯·éæ©åä¼äººå" |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="person in employees" |
| | | :key="person.id" |
| | | :label="`${person.staffName}${person.postName ? ` (${person.postName})` : ''}`" |
| | | :value="person.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="ä¼è®®è¯´æ" prop="description"> |
| | | <el-input |
| | | v-model="meetingForm.description" |
| | | type="textarea" |
| | | :rows="4" |
| | | placeholder="请è¾å
¥ä¼è®®è¯´æ" |
| | | /> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <div class="form-footer"> |
| | | <el-button @click="resetForm">éç½®</el-button> |
| | | <el-button type="primary" @click="submitForm">æäº¤</el-button> |
| | | </div> |
| | | </el-card> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | |
| | | |
| | | // ç³è¯·ç±»åé项 |
| | | const applicationTypes = ref([ |
| | | { |
| | | value: 'approval', |
| | | name: 'å®¡æ¹æµç¨ä¼è®®', |
| | | desc: 'éè¦ç»è¿å¤çº§å®¡æ¹çä¼è®®ç³è¯·', |
| | | icon: Document |
| | | }, |
| | | { |
| | | value: 'department', |
| | | name: 'é¨é¨çº§ä¼è®®', |
| | | desc: 'é¨é¨å
é¨ä¼è®®ç³è¯·æµç¨', |
| | | icon: Promotion |
| | | }, |
| | | { |
| | | value: 'notification', |
| | | name: 'ä¼è®®éç¥', |
| | | desc: 'æ é审æ¹ç´æ¥åå¸çä¼è®®éç¥', |
| | | icon: Bell |
| | | } |
| | | { |
| | | value: 'approval', |
| | | name: 'å®¡æ¹æµç¨ä¼è®®', |
| | | desc: 'éè¦ç»è¿å¤çº§å®¡æ¹çä¼è®®ç³è¯·', |
| | | icon: Document |
| | | }, |
| | | { |
| | | value: 'department', |
| | | name: 'é¨é¨çº§ä¼è®®', |
| | | desc: 'é¨é¨å
é¨ä¼è®®ç³è¯·æµç¨', |
| | | icon: Promotion |
| | | }, |
| | | { |
| | | value: 'notification', |
| | | name: 'ä¼è®®éç¥', |
| | | desc: 'æ é审æ¹ç´æ¥åå¸çä¼è®®éç¥', |
| | | icon: Bell |
| | | } |
| | | ]) |
| | | |
| | | // è¡¨åæ°æ® |
| | | const meetingForm = reactive({ |
| | | title: '', |
| | | type: '', |
| | | roomId: '', |
| | | host: '', |
| | | meetingDate: '', |
| | | startTime: '', |
| | | endTime: '', |
| | | participants: [], |
| | | description: '' |
| | | title: '', |
| | | type: '', |
| | | roomId: '', |
| | | host: '', |
| | | meetingDate: '', |
| | | startTime: '', |
| | | endTime: '', |
| | | participants: [], |
| | | description: '' |
| | | }) |
| | | |
| | | // 表åå¼ç¨ |
| | |
| | | // åå·¥å表 |
| | | const employees = ref([]) |
| | | |
| | | // æ¶é´é项ï¼ä»¥åå°æ¶ä¸ºé´éï¼ |
| | | const timeOptions = ref([]) |
| | | |
| | | const getTimeInMinutes = (time) => { |
| | | if (!time) return -1 |
| | | const [hour, minute] = time.split(':').map(Number) |
| | | return hour * 60 + minute |
| | | if (!time) return -1 |
| | | const [hour, minute] = time.split(':').map(Number) |
| | | return hour * 60 + minute |
| | | } |
| | | |
| | | const isToday = (dateText) => { |
| | | if (!dateText) return false |
| | | const [year, month, day] = dateText.split('-').map(Number) |
| | | const now = new Date() |
| | | return year === now.getFullYear() && month === now.getMonth() + 1 && day === now.getDate() |
| | | if (!dateText) return false |
| | | const [year, month, day] = dateText.split('-').map(Number) |
| | | const now = new Date() |
| | | return year === now.getFullYear() && month === now.getMonth() + 1 && day === now.getDate() |
| | | } |
| | | |
| | | const validateStartTime = (_rule, value, callback) => { |
| | | if (!value) { |
| | | callback() |
| | | return |
| | | } |
| | | |
| | | if (isToday(meetingForm.meetingDate)) { |
| | | const now = new Date() |
| | | const currentMinutes = now.getHours() * 60 + now.getMinutes() |
| | | if (getTimeInMinutes(value) > currentMinutes) { |
| | | callback(new Error('å½å¤©å¼å§æ¶é´ä¸è½æäºå½åæ¶é´')) |
| | | return |
| | | } |
| | | } |
| | | |
| | | callback() |
| | | if (!value) { |
| | | callback() |
| | | return |
| | | } |
| | | callback() |
| | | } |
| | | |
| | | const validateEndTime = (_rule, value, callback) => { |
| | | if (!value || !meetingForm.startTime) { |
| | | callback() |
| | | return |
| | | } |
| | | |
| | | if (getTimeInMinutes(value) <= getTimeInMinutes(meetingForm.startTime)) { |
| | | callback(new Error('ç»ææ¶é´å¿
须大äºå¼å§æ¶é´')) |
| | | return |
| | | } |
| | | |
| | | callback() |
| | | if (!value || !meetingForm.startTime) { |
| | | callback() |
| | | return |
| | | } |
| | | if (getTimeInMinutes(value) <= getTimeInMinutes(meetingForm.startTime)) { |
| | | callback(new Error('ç»ææ¶é´å¿
须大äºå¼å§æ¶é´')) |
| | | return |
| | | } |
| | | callback() |
| | | } |
| | | |
| | | // è¡¨åæ ¡éªè§å |
| | | const rules = { |
| | | title: [{required: true, message: '请è¾å
¥ä¼è®®ä¸»é¢', trigger: 'blur'}], |
| | | roomId: [{required: true, message: 'è¯·éæ©ä¼è®®å®¤', trigger: 'change'}], |
| | | host: [{required: true, message: '请è¾å
¥ä¸»æäºº', trigger: 'blur'}], |
| | | meetingDate: [{required: true, message: 'è¯·éæ©ä¼è®®æ¥æ', trigger: 'change'}], |
| | | startTime: [ |
| | | {required: true, message: 'è¯·éæ©å¼å§æ¶é´', trigger: 'change'}, |
| | | {validator: validateStartTime, trigger: 'change'} |
| | | ], |
| | | endTime: [ |
| | | {required: true, message: 'è¯·éæ©ç»ææ¶é´', trigger: 'change'}, |
| | | {validator: validateEndTime, trigger: 'change'} |
| | | ], |
| | | participants: [{required: true, message: 'è¯·éæ©åä¼äººå', trigger: 'change'}] |
| | | title: [{required: true, message: '请è¾å
¥ä¼è®®ä¸»é¢', trigger: 'blur'}], |
| | | roomId: [{required: true, message: 'è¯·éæ©ä¼è®®å®¤', trigger: 'change'}], |
| | | host: [{required: true, message: '请è¾å
¥ä¸»æäºº', trigger: 'blur'}], |
| | | meetingDate: [{required: true, message: 'è¯·éæ©ä¼è®®æ¥æ', trigger: 'change'}], |
| | | startTime: [ |
| | | {required: true, message: 'è¯·éæ©å¼å§æ¶é´', trigger: 'change'}, |
| | | {validator: validateStartTime, trigger: 'change'} |
| | | ], |
| | | endTime: [ |
| | | {required: true, message: 'è¯·éæ©ç»ææ¶é´', trigger: 'change'}, |
| | | {validator: validateEndTime, trigger: 'change'} |
| | | ], |
| | | participants: [{required: true, message: 'è¯·éæ©åä¼äººå', trigger: 'change'}] |
| | | } |
| | | |
| | | const startTimeOptions = computed(() => { |
| | | if (!isToday(meetingForm.meetingDate)) { |
| | | return timeOptions.value |
| | | } |
| | | const now = new Date() |
| | | const currentMinutes = now.getHours() * 60 + now.getMinutes() |
| | | return timeOptions.value.filter(item => getTimeInMinutes(item.value) <= currentMinutes) |
| | | }) |
| | | |
| | | const endTimeOptions = computed(() => { |
| | | if (!meetingForm.startTime) { |
| | | return timeOptions.value |
| | | } |
| | | const startMinutes = getTimeInMinutes(meetingForm.startTime) |
| | | return timeOptions.value.filter(item => getTimeInMinutes(item.value) > startMinutes) |
| | | }) |
| | | |
| | | // åå§åæ¶é´é项 |
| | | const initTimeOptions = () => { |
| | | const options = [] |
| | | const now = new Date() |
| | | const currentHour = now.getHours() |
| | | const currentMinute = now.getMinutes() |
| | | // meetingDate æ¯ "yyyy-MM-dd" |
| | | const meetingDate = new Date(meetingForm.meetingDate) |
| | | |
| | | const isSameDay = |
| | | now.getFullYear() === meetingDate.getFullYear() && |
| | | now.getMonth() === meetingDate.getMonth() && |
| | | now.getDate() === meetingDate.getDate() |
| | | |
| | | console.log('æ¯å¦åä¸å¤©:', isSameDay) |
| | | for (let hour = 8; hour <= 18; hour++) { |
| | | // å¼å§æ¶é´å¿
é¡»æäºå½åæ¶é´ |
| | | if (hour < currentHour && isSameDay) { |
| | | continue |
| | | } |
| | | if (hour === currentHour && currentMinute > 30 && isSameDay) { |
| | | continue |
| | | } |
| | | // æ¯ä¸ªå°æ¶æ·»å 两个éé¡¹ï¼æ´ç¹ååç¹ |
| | | options.push({ |
| | | value: `${hour.toString().padStart(2, '0')}:00`, |
| | | label: `${hour.toString().padStart(2, '0')}:00` |
| | | }) |
| | | |
| | | if (hour < 18) { // 18:00ä¹å没æåç¹é项 |
| | | options.push({ |
| | | value: `${hour.toString().padStart(2, '0')}:30`, |
| | | label: `${hour.toString().padStart(2, '0')}:30` |
| | | }) |
| | | } |
| | | } |
| | | timeOptions.value = options |
| | | // æ¶é´éæ©å¨ç¦ç¨é»è¾ |
| | | const disabledStartHours = () => { |
| | | const hours = [] |
| | | for (let h = 0; h < 24; h++) { |
| | | if (h < 8 || h > 18) hours.push(h) |
| | | } |
| | | if (isToday(meetingForm.meetingDate)) { |
| | | const now = new Date() |
| | | for (let h = 8; h < now.getHours(); h++) { |
| | | if (!hours.includes(h)) hours.push(h) |
| | | } |
| | | } |
| | | return hours |
| | | } |
| | | |
| | | watch(() => meetingForm.meetingDate, () => { |
| | | if (meetingForm.startTime && !startTimeOptions.value.some(item => item.value === meetingForm.startTime)) { |
| | | meetingForm.startTime = '' |
| | | } |
| | | if (meetingForm.endTime && !endTimeOptions.value.some(item => item.value === meetingForm.endTime)) { |
| | | meetingForm.endTime = '' |
| | | } |
| | | if (meetingForm.startTime) { |
| | | meetingFormRef.value?.validateField('startTime') |
| | | } |
| | | if (meetingForm.endTime) { |
| | | meetingFormRef.value?.validateField('endTime') |
| | | } |
| | | initTimeOptions() |
| | | }) |
| | | const disabledStartMinutes = (hour) => { |
| | | const minutes = [] |
| | | for (let m = 0; m < 60; m++) { |
| | | if (m !== 0 && m !== 30) minutes.push(m) |
| | | } |
| | | if (isToday(meetingForm.meetingDate)) { |
| | | const now = new Date() |
| | | if (hour === now.getHours()) { |
| | | if (now.getMinutes() >= 30) { |
| | | minutes.push(0) |
| | | } |
| | | } |
| | | } |
| | | return minutes |
| | | } |
| | | |
| | | const disabledEndHours = () => { |
| | | const hours = [] |
| | | for (let h = 0; h < 24; h++) { |
| | | if (h < 8 || h > 18) hours.push(h) |
| | | } |
| | | if (meetingForm.startTime) { |
| | | const startHour = parseInt(meetingForm.startTime.split(':')[0]) |
| | | for (let h = 8; h < startHour; h++) { |
| | | if (!hours.includes(h)) hours.push(h) |
| | | } |
| | | } |
| | | return hours |
| | | } |
| | | |
| | | const disabledEndMinutes = (hour) => { |
| | | const minutes = [] |
| | | for (let m = 0; m < 60; m++) { |
| | | if (m !== 0 && m !== 30) minutes.push(m) |
| | | } |
| | | if (meetingForm.startTime) { |
| | | const startHour = parseInt(meetingForm.startTime.split(':')[0]) |
| | | const startMinute = parseInt(meetingForm.startTime.split(':')[1]) |
| | | if (hour === startHour) { |
| | | if (startMinute >= 0) minutes.push(0) |
| | | if (startMinute >= 30) minutes.push(30) |
| | | // only keep minutes > startMinute |
| | | for (let m = 0; m <= startMinute; m++) { |
| | | if (!minutes.includes(m)) minutes.push(m) |
| | | } |
| | | } |
| | | } |
| | | return minutes |
| | | } |
| | | |
| | | watch(() => meetingForm.startTime, () => { |
| | | if (meetingForm.endTime && getTimeInMinutes(meetingForm.endTime) <= getTimeInMinutes(meetingForm.startTime)) { |
| | | meetingForm.endTime = '' |
| | | } |
| | | if (meetingForm.endTime) { |
| | | meetingFormRef.value?.validateField('endTime') |
| | | } |
| | | |
| | | if (meetingForm.endTime && getTimeInMinutes(meetingForm.endTime) <= getTimeInMinutes(meetingForm.startTime)) { |
| | | meetingForm.endTime = '' |
| | | } |
| | | if (meetingForm.endTime) { |
| | | meetingFormRef.value?.validateField('endTime') |
| | | } |
| | | |
| | | }) |
| | | |
| | | // ç¦ç¨æ¥æï¼ç¦ç¨ä»å¤©ä¹åçæ¥æï¼ |
| | | const disabledDate = (time) => { |
| | | // ç¦ç¨ä»å¤©ä¹åçæ¥æ |
| | | return time.getTime() < Date.now() - 86400000 |
| | | // ç¦ç¨ä»å¤©ä¹åçæ¥æ |
| | | return time.getTime() < Date.now() - 86400000 |
| | | } |
| | | |
| | | // 忢ç³è¯·ç±»å |
| | | const changeType = (type) => { |
| | | currentType.value = type |
| | | currentType.value = type |
| | | } |
| | | |
| | | // è·åå½åç±»ååç§° |
| | | const getCurrentTypeName = () => { |
| | | const type = applicationTypes.value.find(t => t.value === currentType.value) |
| | | return type ? type.name : '' |
| | | const type = applicationTypes.value.find(t => t.value === currentType.value) |
| | | return type ? type.name : '' |
| | | } |
| | | |
| | | // é置表å |
| | | const resetForm = () => { |
| | | meetingFormRef.value?.resetFields() |
| | | meetingFormRef.value?.resetFields() |
| | | } |
| | | |
| | | // æäº¤è¡¨å |
| | | const submitForm = () => { |
| | | meetingFormRef.value?.validate((valid) => { |
| | | if (valid) { |
| | | |
| | | let formData = {...meetingForm} |
| | | formData.applicationType = currentType.value |
| | | formData.startTime = `${meetingForm.meetingDate} ${meetingForm.startTime}:00` |
| | | formData.endTime = `${meetingForm.meetingDate} ${meetingForm.endTime}:00` |
| | | formData.participants = JSON.stringify(formData.participants) |
| | | console.log(formData) |
| | | saveMeetingApplication(formData).then(() => { |
| | | |
| | | // 模ææäº¤æä½ |
| | | ElMessage.success(`${getCurrentTypeName()}æäº¤æå`) |
| | | |
| | | // æ ¹æ®ä¸åç±»åæ§è¡ä¸åæä½ |
| | | switch (currentType.value) { |
| | | case 'approval': |
| | | ElMessage.info('ä¼è®®å·²æäº¤å®¡æ¹æµç¨') |
| | | break |
| | | case 'department': |
| | | ElMessage.info('é¨é¨çº§ä¼è®®ç³è¯·å·²æäº¤') |
| | | break |
| | | case 'notification': |
| | | ElMessage.info('ä¼è®®éç¥å·²åå¸') |
| | | break |
| | | } |
| | | resetForm() |
| | | }) |
| | | |
| | | } |
| | | }) |
| | | meetingFormRef.value?.validate((valid) => { |
| | | if (valid) { |
| | | |
| | | let formData = {...meetingForm} |
| | | formData.applicationType = currentType.value |
| | | formData.startTime = `${meetingForm.meetingDate} ${meetingForm.startTime}:00` |
| | | formData.endTime = `${meetingForm.meetingDate} ${meetingForm.endTime}:00` |
| | | formData.participants = JSON.stringify(formData.participants) |
| | | console.log(formData) |
| | | saveMeetingApplication(formData).then(() => { |
| | | |
| | | // 模ææäº¤æä½ |
| | | ElMessage.success(`${getCurrentTypeName()}æäº¤æå`) |
| | | resetForm() |
| | | }) |
| | | |
| | | } |
| | | }) |
| | | } |
| | | |
| | | // 页é¢å è½½æ¶åå§å |
| | | onMounted(() => { |
| | | initTimeOptions() |
| | | getRoomEnum().then(res => { |
| | | meetingRooms.value = res.data |
| | | }) |
| | | staffOnJobListPage({ |
| | | current: -1, |
| | | size: -1, |
| | | staffState: 1 |
| | | }).then(res => { |
| | | employees.value = res.data.records.sort((a, b) => (a.postName || '').localeCompare(b.postName || '')) |
| | | }) |
| | | getRoomEnum().then(res => { |
| | | meetingRooms.value = res.data |
| | | }) |
| | | staffOnJobListPage({ |
| | | current: -1, |
| | | size: -1, |
| | | staffState: 1 |
| | | }).then(res => { |
| | | employees.value = res.data.records.sort((a, b) => (a.postName || '').localeCompare(b.postName || '')) |
| | | }) |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .app-container { |
| | | padding: 20px; |
| | | padding: 20px; |
| | | } |
| | | |
| | | .page-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 20px; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .page-header h2 { |
| | | margin: 0; |
| | | color: #303133; |
| | | margin: 0; |
| | | color: #303133; |
| | | } |
| | | |
| | | .type-card { |
| | | margin-bottom: 20px; |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .type-selector { |
| | | display: flex; |
| | | gap: 20px; |
| | | display: flex; |
| | | gap: 20px; |
| | | } |
| | | |
| | | .type-item { |
| | | flex: 1; |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 20px; |
| | | border: 1px solid #ebeef5; |
| | | border-radius: 8px; |
| | | cursor: pointer; |
| | | transition: all 0.3s; |
| | | flex: 1; |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 20px; |
| | | border: 1px solid #ebeef5; |
| | | border-radius: 8px; |
| | | cursor: pointer; |
| | | transition: all 0.3s; |
| | | } |
| | | |
| | | .type-item:hover { |
| | | border-color: #409eff; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); |
| | | border-color: #409eff; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .type-item.active { |
| | | border-color: #409eff; |
| | | background-color: #ecf5ff; |
| | | border-color: #409eff; |
| | | background-color: #ecf5ff; |
| | | } |
| | | |
| | | .type-icon { |
| | | margin-right: 15px; |
| | | color: #409eff; |
| | | margin-right: 15px; |
| | | color: #409eff; |
| | | } |
| | | |
| | | .type-name { |
| | | font-size: 16px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | margin-bottom: 5px; |
| | | font-size: 16px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | margin-bottom: 5px; |
| | | } |
| | | |
| | | .type-desc { |
| | | font-size: 14px; |
| | | color: #909399; |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .form-header { |
| | | margin-bottom: 20px; |
| | | padding-bottom: 15px; |
| | | border-bottom: 1px solid #ebeef5; |
| | | margin-bottom: 20px; |
| | | padding-bottom: 15px; |
| | | border-bottom: 1px solid #ebeef5; |
| | | } |
| | | |
| | | .form-header h3 { |
| | | margin: 0; |
| | | color: #303133; |
| | | margin: 0; |
| | | color: #303133; |
| | | } |
| | | |
| | | .form-footer { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | gap: 10px; |
| | | margin-top: 30px; |
| | | padding-top: 20px; |
| | | border-top: 1px solid #ebeef5; |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | gap: 10px; |
| | | margin-top: 30px; |
| | | padding-top: 20px; |
| | | border-top: 1px solid #ebeef5; |
| | | } |
| | | </style> |
| | |
| | | <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> |
| | |
| | | </div> |
| | | </template> |
| | | |
| | | |
| | | <!-- ç¨å°ç³è¯·ç®¡ç --> |
| | | <div class="tab-content"> |
| | | <el-row :gutter="20" class="mb-20 "> |
| | |
| | | :isShowPagination="true" |
| | | @pagination="paginationChange" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <!-- ç¨å°ç³è¯·å¯¹è¯æ¡ --> |
| | |
| | | const sealFormRef = ref() |
| | | const userList = ref([]) |
| | | const sealForm = reactive({ |
| | | id: null, |
| | | applicationNum: '', |
| | | title: '', |
| | | sealType: '', |
| | |
| | | |
| | | // ç¨å°ç³è¯·è¡¨æ ¼åé
ç½®ï¼éå¨ getStatusText/getSealTypeText çä¹åå®ä¹ï¼ |
| | | const sealTableColumn = ref([ |
| | | { label: 'ç³è¯·ç¼å·', prop: 'applicationNum',}, |
| | | { label: 'ç³è¯·ç¼å·', prop: 'applicationNum' }, |
| | | { label: 'ç³è¯·æ é¢', prop: 'title', showOverflowTooltip: true }, |
| | | { label: 'ç³è¯·äºº', prop: 'createUserName', }, |
| | | { label: 'ç³è¯·äºº', prop: 'createUserName' }, |
| | | { label: 'æå±é¨é¨', prop: 'department', width: 150 }, |
| | | { |
| | | label: 'ç¨å°ç±»å', |
| | |
| | | formatData: (v) => getStatusText(v), |
| | | formatType: (v) => getStatusType(v) |
| | | }, |
| | | { label: '审æ¹äºº', prop: 'approveUserName', width: 100 }, |
| | | { |
| | | dataType: 'action', |
| | | label: 'æä½', |
| | | width: 200, |
| | | width: 250, |
| | | fixed: 'right', |
| | | align: 'center', |
| | | operation: [ |
| | | { |
| | | name: '审æ¹', |
| | | clickFun: (row) => approveSeal(row), |
| | | showHide: (row) => row.status === 'pending' |
| | | showHide: (row) => row.status === 'pending' && Number(userStore.id) === row.approveUserId |
| | | }, |
| | | { |
| | | name: 'æç»', |
| | | clickFun: (row) => rejectSeal(row), |
| | | showHide: (row) => row.status === 'pending' |
| | | showHide: (row) => row.status === 'pending' && Number(userStore.id) === row.approveUserId |
| | | }, |
| | | { name: '详æ
', clickFun: (row) => viewSealDetail(row) } |
| | | { |
| | | name: 'éæ°ç³è¯·', |
| | | clickFun: (row) => reapplySeal(row), |
| | | showHide: (row) => row.status === 'rejected' && Number(userStore.id) === row.createUser |
| | | }, |
| | | { name: '详æ
', clickFun: (row) => viewSealDetail(row) } |
| | | ] |
| | | } |
| | | ]) |
| | | |
| | | // æç´¢å°ç« ç³è¯· |
| | | const searchSealApplications = () => { |
| | | page.current=1 |
| | | page.current = 1 |
| | | getSealApplicationList() |
| | | |
| | | // ElMessage.success('æç´¢å®æ') |
| | | } |
| | | // éç½®å°ç« ç³è¯·æç´¢ |
| | | const resetSealSearch = () => { |
| | |
| | | sealSearchForm.applicationNum = '' |
| | | searchSealApplications() |
| | | } |
| | | |
| | | // éæ°ç³è¯·ç¨å° |
| | | const reapplySeal = (row) => { |
| | | // é¢å¡«è¡¨åæ°æ® |
| | | Object.assign(sealForm, { |
| | | id: row.id, |
| | | applicationNum: row.applicationNum, |
| | | title: row.title, |
| | | sealType: row.sealType, |
| | | reason: row.reason, |
| | | approveUserId: row.approveUserId, |
| | | urgency: row.urgency || 'normal', |
| | | status: 'pending', |
| | | storageBlobDTOs: row.storageBlobVOList || [] |
| | | }) |
| | | showSealApplyDialog.value = true |
| | | } |
| | | |
| | | // æäº¤ç¨å°ç³è¯· |
| | | const submitSealApplication = async () => { |
| | | try { |
| | | await sealFormRef.value.validate() |
| | | addSealApplication(sealForm).then(res => { |
| | | if(res.code == 200){ |
| | | ElMessage.success('ç³è¯·æäº¤æå') |
| | | const request = sealForm.id ? updateSealApplication : addSealApplication |
| | | request(sealForm).then(res => { |
| | | if (res.code == 200) { |
| | | ElMessage.success(sealForm.id ? 'éæ°ç³è¯·æå' : 'ç³è¯·æäº¤æå') |
| | | closeSealApplyDialog() |
| | | getSealApplicationList() |
| | | Object.assign(sealForm, { |
| | | applicationNum: '', |
| | | title: '', |
| | | sealType: '', |
| | | reason: '', |
| | | approveUserId: '', |
| | | urgency: 'normal', |
| | | status: 'pending', |
| | | storageBlobDTOs: [] |
| | | }) |
| | | id: null, |
| | | applicationNum: '', |
| | | title: '', |
| | | sealType: '', |
| | | reason: '', |
| | | approveUserId: '', |
| | | urgency: 'normal', |
| | | status: 'pending', |
| | | storageBlobDTOs: [] |
| | | }) |
| | | } |
| | | }).catch(err => { |
| | | console.log(err.msg) |
| | | }) |
| | | |
| | | } catch (error) { |
| | | } |
| | | } |
| | | |
| | | // å
³éç¨å°ç³è¯·å¯¹è¯æ¡ |
| | | const closeSealApplyDialog = () => { |
| | | // æ¸
ç©ºè¡¨åæ°æ® |
| | | Object.assign(sealForm, { |
| | | id: null, |
| | | applicationNum: '', |
| | | title: '', |
| | | sealType: '', |
| | |
| | | }).then(() => { |
| | | row.status = 'approved' |
| | | updateSealApplication(row).then(res => { |
| | | if(res.code == 200){ |
| | | if (res.code == 200) { |
| | | ElMessage.success('审æ¹éè¿') |
| | | getSealApplicationList() |
| | | } |
| | |
| | | inputErrorMessage: 'æç»åå ä¸è½ä¸ºç©º' |
| | | }).then(({ value }) => { |
| | | row.status = 'rejected' |
| | | row.reason = value |
| | | updateSealApplication(row).then(res => { |
| | | if(res.code == 200){ |
| | | if (res.code == 200) { |
| | | ElMessage.success('å·²æç»ç³è¯·') |
| | | getSealApplicationList() |
| | | } |
| | |
| | | const getSealApplicationList = async () => { |
| | | tableLoading.value = true |
| | | listSealApplication(page, sealSearchForm) |
| | | .then(res => { |
| | | sealApplications.value = res.data.records |
| | | page.total = res.data.total |
| | | tableLoading.value = false |
| | | }).catch(err => { |
| | | tableLoading.value = false |
| | | }) |
| | | .then(res => { |
| | | sealApplications.value = res.data.records |
| | | page.total = res.data.total |
| | | tableLoading.value = false |
| | | }).catch(err => { |
| | | tableLoading.value = false |
| | | }) |
| | | } |
| | | // å页ååå¤ç |
| | | const paginationChange = (obj) => { |
| | |
| | | .attachment-table { |
| | | border-radius: 4px; |
| | | } |
| | | </style> |
| | | </style> |
| | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å建æ¶é´" prop="createTime"> |
| | | <el-date-picker v-model="form.createTime" type="date" placeholder="éæ©æ¥æ" value-format="YYYY-MM-DD" style="width: 100%;" /> |
| | | <el-date-picker v-model="createTimeDate" type="date" placeholder="éæ©æ¥æ" value-format="YYYY-MM-DD" style="width: 100%;" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted, computed } from "vue"; |
| | | import dayjs from "dayjs"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import FormDialog from "@/components/Dialog/FormDialog.vue"; |
| | | import { |
| | |
| | | |
| | | const form = reactive({ |
| | | ...createDefaultForm(), |
| | | }); |
| | | const createTimeDate = computed({ |
| | | get: () => (form.createTime ? String(form.createTime).split(" ")[0] : ""), |
| | | set: (value) => { |
| | | form.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : ""; |
| | | }, |
| | | }); |
| | | |
| | | const rules = { |
| | |
| | | dialogTitle.value = "æ°å¢åºå®èµäº§"; |
| | | Object.assign(form, createDefaultForm(), { |
| | | purchaseDate: new Date().toISOString().split('T')[0], |
| | | createTime: new Date().toISOString().split('T')[0], |
| | | createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), |
| | | }); |
| | | dialogVisible.value = true; |
| | | }; |
| | |
| | | </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> |
| | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å建æ¶é´" prop="createTime"> |
| | | <el-date-picker v-model="form.createTime" type="date" placeholder="éæ©æ¥æ" value-format="YYYY-MM-DD" style="width: 100%;" /> |
| | | <el-date-picker v-model="createTimeDate" type="date" placeholder="éæ©æ¥æ" value-format="YYYY-MM-DD" style="width: 100%;" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted, computed } from "vue"; |
| | | import dayjs from "dayjs"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import FormDialog from "@/components/Dialog/FormDialog.vue"; |
| | | import { |
| | |
| | | ); |
| | | |
| | | const createDefaultForm = () => ({ |
| | | id: null, |
| | | assetCode: "", |
| | | assetName: "", |
| | | category: "", |
| | |
| | | |
| | | const form = reactive({ |
| | | ...createDefaultForm(), |
| | | }); |
| | | const createTimeDate = computed({ |
| | | get: () => (form.createTime ? String(form.createTime).split(" ")[0] : ""), |
| | | set: (value) => { |
| | | form.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : ""; |
| | | }, |
| | | }); |
| | | |
| | | const rules = { |
| | |
| | | dialogTitle.value = "æ°å¢æ å½¢èµäº§"; |
| | | Object.assign(form, createDefaultForm(), { |
| | | acquisitionDate: new Date().toISOString().split('T')[0], |
| | | createTime: new Date().toISOString().split('T')[0], |
| | | createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), |
| | | }); |
| | | dialogVisible.value = true; |
| | | }; |
| | |
| | | :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 formatMoney = value => { |
| | | const getSummaries = ({ columns, data }) => { |
| | | const sums = []; |
| | | columns.forEach((col, index) => { |
| | | if (index === 0) { |
| | | sums[index] = "å计"; |
| | | } else if (col.property === "amount") { |
| | | const total = data.reduce((prev, cur) => { |
| | | const v = Number(cur.amount); |
| | | return prev + (isNaN(v) ? 0 : v); |
| | | }, 0); |
| | | sums[index] = total.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); |
| | | } else { |
| | | sums[index] = ""; |
| | | } |
| | | }); |
| | | return sums; |
| | | }; |
| | | |
| | | const formatMoney = value => { |
| | | if (value === undefined || value === null) return "0.00"; |
| | | return Number(value) |
| | | .toFixed(2) |
| | |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | :tableLoading="tableLoading" |
| | | isShowSummary |
| | | :summaryMethod="getSummaries" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | |
| | | <el-col :span="12"> |
| | | <el-form-item label="å建æ¶é´" prop="createTime"> |
| | | <el-date-picker |
| | | v-model="form.createTime" |
| | | v-model="formCreateTimeDate" |
| | | type="date" |
| | | placeholder="éæ©æ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | |
| | | <el-col :span="12"> |
| | | <el-form-item label="å建æ¶é´" prop="createTime"> |
| | | <el-date-picker |
| | | v-model="paymentForm.createTime" |
| | | v-model="paymentFormCreateTimeDate" |
| | | type="date" |
| | | placeholder="éæ©æ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | |
| | | |
| | | <script setup> |
| | | import { ref, reactive, computed, onMounted, nextTick, getCurrentInstance } from "vue"; |
| | | import dayjs from "dayjs"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import FormDialog from "@/components/Dialog/FormDialog.vue"; |
| | | import { getOptions } from "@/api/procurementManagement/procurementLedger.js"; |
| | |
| | | status: 0, |
| | | createTime: "", |
| | | }); |
| | | const formCreateTimeDate = computed({ |
| | | get: () => (form.createTime ? String(form.createTime).split(" ")[0] : ""), |
| | | set: (value) => { |
| | | form.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : ""; |
| | | }, |
| | | }); |
| | | const paymentFormCreateTimeDate = computed({ |
| | | get: () => (paymentForm.createTime ? String(paymentForm.createTime).split(" ")[0] : ""), |
| | | set: (value) => { |
| | | paymentForm.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : ""; |
| | | }, |
| | | }); |
| | | |
| | | const rules = { |
| | | supplierId: [{ required: true, message: "è¯·éæ©ä¾åºå", trigger: "change" }], |
| | |
| | | paymentAmount: [{ required: true, message: "请è¾å
¥ä»æ¬¾éé¢", trigger: "blur" }], |
| | | paymentMethod: [{ required: true, message: "è¯·éæ©ä»æ¬¾æ¹å¼", trigger: "change" }], |
| | | applyDate: [{ required: true, message: "è¯·éæ©ç³è¯·æ¥æ", trigger: "change" }], |
| | | }; |
| | | |
| | | const getSummaries = ({ columns, data }) => { |
| | | const sums = []; |
| | | columns.forEach((col, index) => { |
| | | if (index === 0) { |
| | | sums[index] = "å计"; |
| | | } else if (col.property === "amount") { |
| | | const total = data.reduce((prev, cur) => { |
| | | const v = Number(cur.amount); |
| | | return prev + (isNaN(v) ? 0 : v); |
| | | }, 0); |
| | | sums[index] = total.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); |
| | | } else { |
| | | sums[index] = ""; |
| | | } |
| | | }); |
| | | return sums; |
| | | }; |
| | | |
| | | const formatMoney = (value) => { |
| | |
| | | remark: form.remark || "", |
| | | status: 0, |
| | | paymentAmount: form.paymentAmount, |
| | | createTime: form.createTime, |
| | | }; |
| | | if (forUpdate) { |
| | | payload.id = currentId.value; |
| | |
| | | stockInRecordIds: [], |
| | | inboundBatches: "", |
| | | status: 0, |
| | | createTime: new Date().toISOString().split("T")[0], |
| | | createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), |
| | | }); |
| | | inboundBatchList.value = []; |
| | | inboundBatchOptions.value = []; |
| | |
| | | bankAccount: row.bankAccountNum ?? row.bankAccount ?? "", |
| | | bankName: row.bankAccountName ?? row.bankName ?? "", |
| | | remark: "", |
| | | createTime: new Date().toISOString().split("T")[0], |
| | | createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), |
| | | }); |
| | | paymentDialogVisible.value = true; |
| | | nextTick(() => { |
| | |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | :tableLoading="tableLoading" |
| | | isShowSummary |
| | | :summaryMethod="getSummaries" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | |
| | | getTableData(); |
| | | }; |
| | | |
| | | const handleOut = () => { |
| | | const getSummaries = ({ columns, data }) => { |
| | | const sums = []; |
| | | columns.forEach((col, index) => { |
| | | if (index === 0) { |
| | | sums[index] = "å计"; |
| | | } else if (col.property === "inboundAmount") { |
| | | const total = data.reduce((prev, cur) => { |
| | | const v = Number(cur.inboundAmount); |
| | | return prev + (isNaN(v) ? 0 : v); |
| | | }, 0); |
| | | sums[index] = total.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); |
| | | } else { |
| | | sums[index] = ""; |
| | | } |
| | | }); |
| | | return sums; |
| | | }; |
| | | |
| | | const handleOut = () => { |
| | | proxy.download( |
| | | "/accountPurchase/exportAccountPurchaseInbound", |
| | | buildFilterParams(), |
| | |
| | | :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 === "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, |
| | |
| | | <el-col :span="12"> |
| | | <el-form-item label="å建æ¶é´" prop="createTime"> |
| | | <el-date-picker |
| | | v-model="form.createTime" |
| | | v-model="formCreateTimeDate" |
| | | type="date" |
| | | placeholder="éæ©æ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | |
| | | |
| | | <script setup> |
| | | import { ref, reactive, computed, onMounted, nextTick, getCurrentInstance, defineAsyncComponent } from "vue"; |
| | | import dayjs from "dayjs"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import FormDialog from "@/components/Dialog/FormDialog.vue"; |
| | | import { listCustomer } from "@/api/basicData/customer.js"; |
| | |
| | | remark: "", |
| | | createTime: "", |
| | | }); |
| | | const formCreateTimeDate = computed({ |
| | | get: () => (form.createTime ? String(form.createTime).split(" ")[0] : ""), |
| | | set: (value) => { |
| | | form.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : ""; |
| | | }, |
| | | }); |
| | | |
| | | const rules = { |
| | | customerId: [{ required: true, message: "è¯·éæ©å®¢æ·", trigger: "change" }], |
| | |
| | | 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, ","); |
| | |
| | | status: normalizeStatus(row.status ?? row.auditStatus), |
| | | outboundBatchNos, |
| | | outboundBatches: formatOutboundBatches(row.outboundBatches), |
| | | createTime: row.createTime ?? "", |
| | | }); |
| | | }; |
| | | |
| | |
| | | applyDate: new Date().toISOString().split("T")[0], |
| | | content: "", |
| | | remark: "", |
| | | createTime: new Date().toISOString().split("T")[0], |
| | | createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), |
| | | }); |
| | | outboundBatchList.value = []; |
| | | outboundBatchOptions.value = []; |
| | |
| | | v-loading="tableLoading" |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | isShowSummary |
| | | :summaryMethod="getSummaries" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | |
| | | amount: [{ required: true, message: "请è¾å
¥éé¢", trigger: "blur" }], |
| | | }; |
| | | |
| | | const getSummaries = ({ columns, data }) => { |
| | | const sums = []; |
| | | columns.forEach((col, index) => { |
| | | if (index === 0) { |
| | | sums[index] = "å计"; |
| | | } else if (col.property === "amount") { |
| | | const total = data.reduce((prev, cur) => { |
| | | const v = Number(cur.amount); |
| | | return prev + (isNaN(v) ? 0 : v); |
| | | }, 0); |
| | | sums[index] = total.toLocaleString("zh-CN", { |
| | | minimumFractionDigits: 2, |
| | | maximumFractionDigits: 2, |
| | | }); |
| | | } else { |
| | | sums[index] = ""; |
| | | } |
| | | }); |
| | | return sums; |
| | | }; |
| | | |
| | | const formatMoney = (value) => { |
| | | if (value === undefined || value === null) return "0.00"; |
| | | return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ","); |
| | |
| | | <el-col :span="12"> |
| | | <el-form-item label="å建æ¶é´" |
| | | prop="createTime"> |
| | | <el-date-picker v-model="form.createTime" |
| | | <el-date-picker v-model="formCreateTimeDate" |
| | | type="date" |
| | | placeholder="éæ©æ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | |
| | | nextTick, |
| | | getCurrentInstance, |
| | | } from "vue"; |
| | | import dayjs from "dayjs"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import FormDialog from "@/components/Dialog/FormDialog.vue"; |
| | | import { listCustomer } from "@/api/basicData/customer.js"; |
| | |
| | | outboundBatches: "", |
| | | remark: "", |
| | | createTime: "", |
| | | }); |
| | | const formCreateTimeDate = computed({ |
| | | get: () => (form.createTime ? String(form.createTime).split(" ")[0] : ""), |
| | | set: (value) => { |
| | | form.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : ""; |
| | | }, |
| | | }); |
| | | |
| | | const rules = { |
| | |
| | | stockOutRecordIds, |
| | | outboundBatches: formatOutboundBatches(row.outboundBatches), |
| | | remark: row.remark ?? "", |
| | | createTime: row.createTime ?? "", |
| | | }); |
| | | }; |
| | | |
| | |
| | | stockOutRecordIds: [], |
| | | outboundBatches: "", |
| | | remark: "", |
| | | createTime: new Date().toISOString().split("T")[0], |
| | | createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), |
| | | }); |
| | | outboundBatchList.value = []; |
| | | outboundBatchOptions.value = []; |
| | |
| | | :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: "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="'paused'">å·²æå({{ orderProgressMeta.pausedCount }})</el-radio-button> |
| | | </el-radio-group> |
| | | </div> |
| | | <el-table :data="filteredOrders" stripe> |
| | | <el-table-column prop="orderNo" label="订åç¼å·" min-width="150" /> |
| | | <el-table-column prop="productName" label="产ååç§°" min-width="120" /> |
| | | <el-table-column prop="planQty" label="è®¡åæ°é" min-width="90" /> |
| | | <el-table-column prop="completedQty" label="已宿" min-width="90" /> |
| | | <el-table-column label="宿ç" min-width="180"> |
| | | <template #default="{ row }"> |
| | | <div class="table-progress"> |
| | | <el-progress |
| | | :stroke-width="8" |
| | | :percentage="row.completionRate" |
| | | :show-text="false" |
| | | status="success" |
| | | /> |
| | | <span>{{ row.completionRate }}%</span> |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="deliveryDate" label="交æ" min-width="110" /> |
| | | <el-table-column label="ç¶æ" min-width="90"> |
| | | <template #default="{ row }"> |
| | | <el-tag :type="getOrderStatusType(row.status)" effect="light"> |
| | | {{ row.statusLabel || getOrderStatusText(row.status) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div class="order-table-wrap"> |
| | | <el-table :data="filteredOrders" stripe max-height="440"> |
| | | <el-table-column prop="orderNo" label="订åç¼å·" min-width="150" /> |
| | | <el-table-column prop="productName" label="产ååç§°" min-width="120" /> |
| | | <el-table-column prop="planQty" label="è®¡åæ°é" min-width="90" /> |
| | | <el-table-column prop="completedQty" label="已宿" min-width="90" /> |
| | | <el-table-column label="宿ç" min-width="180"> |
| | | <template #default="{ row }"> |
| | | <div class="table-progress"> |
| | | <el-progress |
| | | :stroke-width="8" |
| | | :percentage="row.completionRate" |
| | | :show-text="false" |
| | | status="success" |
| | | /> |
| | | <span>{{ row.completionRate }}%</span> |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="deliveryDate" label="交æ" min-width="110" /> |
| | | <el-table-column label="ç¶æ" min-width="90"> |
| | | <template #default="{ row }"> |
| | | <el-tag :type="getOrderStatusType(row.status)" effect="light"> |
| | | {{ row.statusLabel || getOrderStatusText(row.status) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="visiblePanels.contract" class="cockpit-panel contract-panel"> |
| | |
| | | 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); |
| | |
| | | |
| | | .order-panel { |
| | | min-height: 0; |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .order-table-wrap { |
| | | flex: 1; |
| | | min-height: 0; |
| | | } |
| | | |
| | | .quick-panel { |
| | |
| | | <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="夿³¨" |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| | |
| | | currentNodeIndex: 0, |
| | | approvalRecords, |
| | | rejectReason: approvalRecords.find(r => r.result === "rejected")?.opinion || "", |
| | | purchaseContractNumber: row.purchaseContractNumber || "", |
| | | quotationNo: row.quotationNo || "", |
| | | shippingNo: row.shippingNo || "", |
| | | }; |
| | | } |
| | | |
| | |
| | | {{ 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"> |
| | |
| | | </div> |
| | | <div class="detail-block"> |
| | | <div class="detail-block-title">å¡«æ¥å
容</div> |
| | | <FormPayloadFields :fields="formResolved.fields" |
| | | <!-- é»è®¤è¡¨åå±ç¤º --> |
| | | <FormPayloadFields v-if="!isSpecialApprovalType" |
| | | :fields="formResolved.fields" |
| | | :form-payload="formResolved.formPayload" |
| | | readonly /> |
| | | |
| | | <!-- å货审æ¹è¯¦æ
--> |
| | | <template v-else-if="row.businessType === 7"> |
| | | <div v-if="detailData.shippingInfo" class="shipment-detail"> |
| | | <el-divider content-position="left">å货详æ
</el-divider> |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions-item label="éå®è®¢å">{{ detailData.shippingInfo.salesContractNo || "--" }}</el-descriptions-item> |
| | | <el-descriptions-item label="å货订åå·">{{ detailData.shippingInfo.shippingNo || "--" }}</el-descriptions-item> |
| | | <el-descriptions-item label="客æ·åç§°">{{ detailData.shippingInfo.customerName || "--" }}</el-descriptions-item> |
| | | <el-descriptions-item label="åè´§ç±»å">{{ detailData.shippingInfo.type || "--" }}</el-descriptions-item> |
| | | <el-descriptions-item label="åè´§æ¥æ">{{ detailData.shippingInfo.shippingDateDate || "--" }}</el-descriptions-item> |
| | | <el-descriptions-item label="å®¡æ ¸ç¶æ">{{ detailData.shippingInfo.status || "--" }}</el-descriptions-item> |
| | | <el-descriptions-item label="å货车çå·">{{ detailData.shippingInfo.shippingCarNumber || "--" }}</el-descriptions-item> |
| | | <el-descriptions-item label="å¿«éå
¬å¸">{{ detailData.shippingInfo.expressCompany || "--" }}</el-descriptions-item> |
| | | <el-descriptions-item label="å¿«éåå·" :span="2">{{ detailData.shippingInfo.expressNumber || "--" }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | <div v-if="detailData.shippingProductDetailDtoList.length" style="margin-top: 20px;"> |
| | | <h4>产åæç»</h4> |
| | | <el-table :data="detailData.shippingProductDetailDtoList" |
| | | border |
| | | size="small" |
| | | style="width: 100%"> |
| | | <el-table-column label="æ¹å·" prop="batchNo" min-width="160" show-overflow-tooltip /> |
| | | <el-table-column label="产ååç§°" prop="productName" min-width="160" show-overflow-tooltip /> |
| | | <el-table-column label="è§æ ¼åå·" prop="specificationModel" min-width="160" show-overflow-tooltip /> |
| | | <el-table-column label="åè´§æ°é" prop="deliveryQuantity" min-width="120" align="center" /> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <!-- éè´å®¡æ¹è¯¦æ
--> |
| | | <template v-else-if="row.businessType === 5"> |
| | | <div v-if="detailData" class="procurement-detail"> |
| | | <el-divider content-position="left">éè´è¯¦æ
</el-divider> |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions-item label="éè´ååå·">{{ detailData.purchaseContractNumber || "--" }}</el-descriptions-item> |
| | | <el-descriptions-item label="ä¾åºååç§°">{{ detailData.supplierName || "--" }}</el-descriptions-item> |
| | | <el-descriptions-item label="项ç®åç§°">{{ detailData.projectName || "--" }}</el-descriptions-item> |
| | | <el-descriptions-item label="éå®ååå·">{{ detailData.salesContractNo || "--" }}</el-descriptions-item> |
| | | <el-descriptions-item label="ç¾è®¢æ¥æ">{{ detailData.executionDate || "--" }}</el-descriptions-item> |
| | | <el-descriptions-item label="å½å
¥æ¥æ">{{ detailData.entryDate || "--" }}</el-descriptions-item> |
| | | <el-descriptions-item label="仿¬¾æ¹å¼">{{ detailData.paymentMethod || "--" }}</el-descriptions-item> |
| | | <el-descriptions-item label="ååéé¢" :span="2"> |
| | | <span style="font-size: 18px; color: #e6a23c; font-weight: bold;"> |
| | | ¥{{ Number(detailData.contractAmount ?? 0).toFixed(2) }} |
| | | </span> |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | <div v-if="detailData.productData.length" style="margin-top: 20px;"> |
| | | <h4>产åæç»</h4> |
| | | <el-table :data="detailData.productData" |
| | | border |
| | | style="width: 100%"> |
| | | <el-table-column prop="productCategory" label="产ååç§°" /> |
| | | <el-table-column prop="specificationModel" label="è§æ ¼åå·" /> |
| | | <el-table-column prop="unit" label="åä½" /> |
| | | <el-table-column prop="quantity" label="æ°é" /> |
| | | <el-table-column prop="taxInclusiveUnitPrice" label="å«ç¨åä»·"> |
| | | <template #default="scope">Â¥{{ Number(scope.row.taxInclusiveUnitPrice ?? 0).toFixed(2) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="taxInclusiveTotalPrice" label="å«ç¨æ»ä»·"> |
| | | <template #default="scope">Â¥{{ Number(scope.row.taxInclusiveTotalPrice ?? 0).toFixed(2) }}</template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <!-- æ¥ä»·å®¡æ¹è¯¦æ
--> |
| | | <template v-else-if="row.businessType === 6"> |
| | | <div v-if="detailData" class="quotation-detail"> |
| | | <el-divider content-position="left">æ¥ä»·è¯¦æ
</el-divider> |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions-item label="æ¥ä»·åå·">{{ detailData.quotationNo || "--" }}</el-descriptions-item> |
| | | <el-descriptions-item label="客æ·åç§°">{{ detailData.customer || "--" }}</el-descriptions-item> |
| | | <el-descriptions-item label="ä¸å¡å">{{ detailData.salesperson || "--" }}</el-descriptions-item> |
| | | <el-descriptions-item label="æ¥ä»·æ¥æ">{{ detailData.quotationDate || "--" }}</el-descriptions-item> |
| | | <el-descriptions-item label="æææè³">{{ detailData.validDate || "--" }}</el-descriptions-item> |
| | | <el-descriptions-item label="仿¬¾æ¹å¼">{{ detailData.paymentMethod || "--" }}</el-descriptions-item> |
| | | <el-descriptions-item label="æ¥ä»·æ»é¢" :span="2"> |
| | | <span style="font-size: 18px; color: #e6a23c; font-weight: bold;"> |
| | | ¥{{ Number(detailData.totalAmount ?? 0).toFixed(2) }} |
| | | </span> |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | <div v-if="detailData.products.length" style="margin-top: 20px;"> |
| | | <h4>产åæç»</h4> |
| | | <el-table :data="detailData.products" |
| | | border |
| | | style="width: 100%"> |
| | | <el-table-column prop="product" label="产ååç§°" /> |
| | | <el-table-column prop="specification" label="è§æ ¼åå·" /> |
| | | <el-table-column prop="unit" label="åä½" /> |
| | | <el-table-column prop="unitPrice" label="åä»·"> |
| | | <template #default="scope">Â¥{{ Number(scope.row.unitPrice ?? 0).toFixed(2) }}</template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | <div v-if="detailData.remark" style="margin-top: 20px;"> |
| | | <h4>夿³¨</h4> |
| | | <p>{{ detailData.remark }}</p> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | </div> |
| | | <div v-if="attachmentList.length" |
| | | class="detail-block"> |
| | |
| | | |
| | | const props = defineProps({ |
| | | row: { type: Object, default: () => ({}) }, |
| | | detailData: { type: Object, default: () => ({}) }, |
| | | }); |
| | | |
| | | const formResolved = computed(() => resolveInstanceFormFields(props.row)); |
| | | |
| | | |
| | | // æ¯å¦ä¸ºç¹æ®å®¡æ¹ç±»åï¼éè´ãåè´§ãæ¥ä»·ï¼ |
| | | const isSpecialApprovalType = computed(() => { |
| | | return [5, 7, 6].includes(props.row.businessType); |
| | | }); |
| | | |
| | | // 详æ
æ°æ®ï¼ç´æ¥ä½¿ç¨ä¼ å
¥ç detail-data åæ°ï¼ |
| | | const detailData = computed(() => { |
| | | return props.detailData || {}; |
| | | }); |
| | | |
| | | const attachmentList = computed(() => { |
| | | const list = props.row.storageBlobVOList || props.row.storageBlobDTOs || []; |
| | |
| | | align-items: center; |
| | | flex-shrink: 0; |
| | | } |
| | | </style> |
| | | </style> |
| | |
| | | class="approve-detail-dialog" |
| | | > |
| | | <div class="approve-detail-body"> |
| | | <ApproveDetailPanel :row="detailRow" /> |
| | | <ApproveDetailPanel :row="detailRow" :detail-data="detailData" /> |
| | | <div class="detail-block"> |
| | | <div class="detail-block-title"> |
| | | å®¡æ¹æµç¨ï¼{{ detailRow.tasks?.length || detailRow.flowNodes?.length || 0 }} é¡¹ï¼ |
| | |
| | | destroy-on-close |
| | | @closed="approveOpinion = ''" |
| | | > |
| | | <ApproveDetailPanel :row="approveDialog.row" /> |
| | | <ApproveDetailPanel :row="approveDialog.row" :detail-data="detailData" /> |
| | | <div class="detail-block mt16"> |
| | | <div class="detail-block-title"> |
| | | å®¡æ¹æµç¨ï¼{{ approveDialog.row?.tasks?.length || approveDialog.row?.flowNodes?.length || 0 }} é¡¹ï¼ |
| | |
| | | tableColumn, |
| | | detailDialog, |
| | | detailRow, |
| | | detailData, |
| | | reimburseDialog, |
| | | approveDialog, |
| | | approveOpinion, |
| | |
| | | border-left: 3px solid var(--el-color-primary); |
| | | line-height: 1.4; |
| | | } |
| | | </style> |
| | | </style> |
| | |
| | | saveApprovalInstance, |
| | | updateApprovalInstance, |
| | | } from "@/api/officeProcessAutomation/approvalInstance.js"; |
| | | import { getQuotationList } from "@/api/salesManagement/salesQuotation.js"; |
| | | import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger.js"; |
| | | import { getDeliveryDetailByShippingNo } from "@/api/salesManagement/deliveryLedger.js"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | |
| | | |
| | | const detailDialog = reactive({ visible: false }); |
| | | const detailRow = ref({}); |
| | | const detailData = ref({}); |
| | | |
| | | const approveDialog = reactive({ visible: false, row: null }); |
| | | const approveOpinion = ref(""); |
| | |
| | | })); |
| | | |
| | | const tableColumn = ref([ |
| | | { label: "ç³è¯·äººç¼å·", prop: "applicantNo", width: 110 }, |
| | | { label: "ç³è¯·äººåç§°", prop: "applicantName", minWidth: 100 }, |
| | | // { label: "ç³è¯·äººç¼å·", prop: "applicantNo", width: 110 }, |
| | | { label: "å起人", prop: "applicantName", minWidth: 100 }, |
| | | { label: "模æ¿ç±»å", prop: "businessName", minWidth: 120 }, |
| | | { |
| | | label: "审æ¹ç±»å", |
| | |
| | | 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), |
| | | // }, |
| | | ], |
| | | }, |
| | | ]); |
| | |
| | | fetchBusinessTypeOptions(), |
| | | listApprovalTemplate(TEMPLATE_TYPE_CUSTOM), |
| | | ]); |
| | | submitBusinessTypeOptions.value = typeOptions; |
| | | submitBusinessTypeOptions.value = typeOptions.filter((x) => ![5, 6, 7].includes(x.value)); |
| | | allSubmitTemplates.value = unwrapTemplateList(customRes) |
| | | .filter((row) => mapEnabledFromApi(row.enabled)) |
| | | .map(mapSubmitTemplateCard); |
| | |
| | | await openReimburseDetail(row, "detail"); |
| | | return; |
| | | } |
| | | |
| | | detailRow.value = { ...row }; |
| | | // æ¥ä»·å®¡æ¹ï¼ç¨å®¡æ¹äºç±å段æ¿è½½ç"æ¥ä»·åå·"廿¥æ¥ä»·å表 |
| | | if (row.businessType === 6) { |
| | | const quotationNo = row?.quotationNo; |
| | | if (quotationNo) { |
| | | const res = await getQuotationList({ quotationNo }); |
| | | const records = res?.data?.records || []; |
| | | detailData.value = records[0] || {}; |
| | | } |
| | | } |
| | | |
| | | // éè´å®¡æ¹ï¼ç¨å®¡æ¹äºç±å段æ¿è½½ç"éè´ååå·"廿¥éè´è¯¦æ
|
| | | else if (row.businessType === 5) { |
| | | const purchaseContractNumber = row?.purchaseContractNumber; |
| | | if (purchaseContractNumber) { |
| | | const res = await getPurchaseByCode({ purchaseContractNumber }); |
| | | detailData.value = res || {}; |
| | | } |
| | | } |
| | | |
| | | // å货审æ¹ï¼ç¨å®¡æ¹äºç±å段æ¿è½½ç"åè´§åå·"廿¥å货详æ
|
| | | else if (row.businessType === 7) { |
| | | const shippingNo = row?.shippingNo; |
| | | if (shippingNo) { |
| | | const res = await getDeliveryDetailByShippingNo({ shippingNo }); |
| | | detailData.value = res?.data || res || {}; |
| | | } |
| | | } |
| | | |
| | | // å
¶ä»å®¡æ¹ç±»å |
| | | else { |
| | | detailData.value = {}; |
| | | } |
| | | |
| | | detailDialog.visible = true; |
| | | } |
| | | |
| | | async function openApprove(row) { |
| | | |
| | | if (inferReimburseModuleKeyFromInstance(row)) { |
| | | approveOpinion.value = ""; |
| | | await openReimburseDetail(row, "approve"); |
| | |
| | | } |
| | | approveDialog.row = { ...row }; |
| | | approveOpinion.value = ""; |
| | | // æ¥ä»·å®¡æ¹ï¼ç¨å®¡æ¹äºç±å段æ¿è½½ç"æ¥ä»·åå·"廿¥æ¥ä»·å表 |
| | | if (row.businessType === 6) { |
| | | const quotationNo = row?.quotationNo; |
| | | if (quotationNo) { |
| | | const res = await getQuotationList({ quotationNo }); |
| | | const records = res?.data?.records || []; |
| | | detailData.value = records[0] || {}; |
| | | } |
| | | } |
| | | |
| | | // éè´å®¡æ¹ï¼ç¨å®¡æ¹äºç±å段æ¿è½½ç"éè´ååå·"廿¥éè´è¯¦æ
|
| | | else if (row.businessType === 5) { |
| | | const purchaseContractNumber = row?.purchaseContractNumber; |
| | | if (purchaseContractNumber) { |
| | | const res = await getPurchaseByCode({ purchaseContractNumber }); |
| | | detailData.value = res || {}; |
| | | } |
| | | } |
| | | |
| | | // å货审æ¹ï¼ç¨å®¡æ¹äºç±å段æ¿è½½ç"åè´§åå·"廿¥å货详æ
|
| | | else if (row.businessType === 7) { |
| | | const shippingNo = row?.shippingNo; |
| | | if (shippingNo) { |
| | | const res = await getDeliveryDetailByShippingNo({ shippingNo }); |
| | | detailData.value = res?.data || res || {}; |
| | | } |
| | | } |
| | | |
| | | // å
¶ä»å®¡æ¹ç±»å |
| | | else { |
| | | detailData.value = {}; |
| | | } |
| | | approveDialog.visible = true; |
| | | } |
| | | |
| | |
| | | tableColumn, |
| | | detailDialog, |
| | | detailRow, |
| | | detailData, |
| | | reimburseDialog, |
| | | approveDialog, |
| | | approveOpinion, |
| | |
| | | openApprove, |
| | | fetchApprovalList, |
| | | }; |
| | | } |
| | | } |
| | |
| | | disabled: (row) => !canEditBusinessInstanceRow(row), |
| | | clickFun: (row) => openEdit(row), |
| | | }, |
| | | { |
| | | name: "å é¤", |
| | | type: "danger", |
| | | clickFun: (row) => removeInstance(row), |
| | | }, |
| | | // { |
| | | // name: "å é¤", |
| | | // type: "danger", |
| | | // clickFun: (row) => removeInstance(row), |
| | | // }, |
| | | ...extraOperations, |
| | | ]; |
| | | } |
| | |
| | | |
| | | <template v-if="detailAttachments.length"> |
| | | |
| | | <el-tag |
| | | |
| | | v-for="(f, i) in detailAttachments" |
| | | |
| | | :key="i" |
| | | |
| | | class="detail-attachment-tag" |
| | | |
| | | type="info" |
| | | |
| | | effect="plain" |
| | | |
| | | > |
| | | |
| | | {{ attachmentDisplayName(f) }} |
| | | |
| | | </el-tag> |
| | | <div class="detail-attachment-list"> |
| | | <div |
| | | v-for="(f, i) in detailAttachments" |
| | | :key="i" |
| | | class="detail-attachment-item" |
| | | @click="openAttachmentFile(f)" |
| | | > |
| | | <el-icon class="attachment-icon"><Document /></el-icon> |
| | | <span class="attachment-name">{{ attachmentDisplayName(f) }}</span> |
| | | <el-icon class="attachment-download"><Download /></el-icon> |
| | | </div> |
| | | </div> |
| | | |
| | | </template> |
| | | |
| | |
| | | |
| | | <script setup> |
| | | |
| | | import { ArrowRight, Plus, RefreshRight } from "@element-plus/icons-vue"; |
| | | import { ArrowRight, Document, Download, Plus, RefreshRight } from "@element-plus/icons-vue"; |
| | | |
| | | import { ElMessage } from "element-plus"; |
| | | |
| | |
| | | |
| | | } |
| | | |
| | | function openAttachmentFile(file) { |
| | | const url = file?.url || file?.previewURL || file?.downloadURL || file?.previewUrl || ""; |
| | | if (url) { |
| | | window.open(url, "_blank"); |
| | | } else { |
| | | ElMessage.warning("æ æ³æå¼è¯¥éä»¶"); |
| | | } |
| | | } |
| | | |
| | | |
| | | function unwrapArray(payload) { |
| | |
| | | |
| | | } |
| | | |
| | | .detail-attachment-tag { |
| | | .detail-attachment-list { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 8px; |
| | | } |
| | | |
| | | margin: 0 8px 8px 0; |
| | | .detail-attachment-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | padding: 8px 12px; |
| | | background: var(--el-fill-color-light); |
| | | border-radius: 6px; |
| | | cursor: pointer; |
| | | transition: background 0.2s; |
| | | } |
| | | |
| | | .detail-attachment-item:hover { |
| | | background: var(--el-fill-color); |
| | | } |
| | | |
| | | .attachment-icon { |
| | | font-size: 16px; |
| | | color: var(--el-text-color-regular); |
| | | } |
| | | |
| | | .attachment-name { |
| | | font-size: 14px; |
| | | color: var(--el-text-color-primary); |
| | | } |
| | | |
| | | .attachment-download { |
| | | font-size: 14px; |
| | | color: var(--el-text-color-secondary); |
| | | } |
| | | |
| | | .text-muted { |
| | |
| | | }); |
| | | } |
| | | |
| | | function openFormDialog(mode, row) { |
| | | async function openFormDialog(mode, row) { |
| | | formDialog.mode = mode; |
| | | formDialog.title = mode === "add" ? "æ°å»ºå®¡æ¹æ¨¡æ¿" : "ç¼è¾å®¡æ¹æ¨¡æ¿"; |
| | | resetForm(mode === "edit" ? row : null); |
| | | if (mode === "edit" && row?.id) { |
| | | // ç¼è¾æ¶å
æ¥è¯¢è¯¦æ
è·å宿´æ°æ®ï¼å
å«éä»¶ï¼ |
| | | try { |
| | | const res = await getApprovalTemplateDetail(row.id); |
| | | const detailData = mapTemplateFromApi(unwrapTemplateDetail(res)); |
| | | resetForm(detailData); |
| | | } catch { |
| | | resetForm(row); |
| | | } |
| | | } else { |
| | | resetForm(mode === "edit" ? row : null); |
| | | } |
| | | formDialog.visible = true; |
| | | } |
| | | |
| | |
| | | <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> |
| | |
| | | <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> |
| | |
| | | { 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(":"); |
| | |
| | | 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" |
| | | :title="operationType === 'add' ? 'æ°å»ºå·¥èµè¡¨' : 'ç¼è¾å·¥èµè¡¨'" |
| | | width="90%" |
| | | @close="closeDia" |
| | | > |
| | | <FormDialog v-model="dialogVisible" |
| | | :title="operationType === 'add' ? 'æ°å»ºå·¥èµè¡¨' : 'ç¼è¾å·¥èµè¡¨'" |
| | | width="90%" |
| | | @close="closeDia"> |
| | | <template #footer> |
| | | <el-button type="info" @click="saveDraft">ä¿åè稿</el-button> |
| | | <el-button type="primary" @click="submitForm">确认æäº¤</el-button> |
| | | <el-button type="info" |
| | | @click="saveDraft">ä¿åè稿</el-button> |
| | | <el-button type="primary" |
| | | @click="submitForm">确认æäº¤</el-button> |
| | | <el-button @click="closeDia">åæ¶</el-button> |
| | | </template> |
| | | <div class="form-dia-body"> |
| | | <!-- åºç¡èµæ --> |
| | | <el-card class="form-card" shadow="never"> |
| | | <el-card class="form-card" |
| | | shadow="never"> |
| | | <template #header> |
| | | <span class="card-title"><span class="card-title-line">|</span> åºç¡èµæ</span> |
| | | <el-icon class="card-collapse"><ArrowUp /></el-icon> |
| | | <el-icon class="card-collapse"> |
| | | <ArrowUp /> |
| | | </el-icon> |
| | | </template> |
| | | <el-form ref="formRef" :model="form" :rules="rules" label-position="top"> |
| | | <el-form ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-position="top"> |
| | | <el-row :gutter="24"> |
| | | <el-col :span="6"> |
| | | <el-form-item label="å·¥èµä¸»é¢" prop="salaryTitle"> |
| | | <el-input |
| | | v-model="form.salaryTitle" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="20" |
| | | show-word-limit |
| | | /> |
| | | <el-form-item label="å·¥èµä¸»é¢" |
| | | prop="salaryTitle"> |
| | | <el-input v-model="form.salaryTitle" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="20" |
| | | show-word-limit /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="éæ©é¨é¨" prop="deptIds"> |
| | | <el-select |
| | | v-model="form.deptIds" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | multiple |
| | | collapse-tags-tooltip |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="item in deptOptions" |
| | | :key="item.deptId" |
| | | :label="item.deptName" |
| | | :value="item.deptId" |
| | | /> |
| | | <el-form-item label="éæ©å·¥èµæä»½" |
| | | prop="salaryMonth"> |
| | | <el-date-picker v-model="form.salaryMonth" |
| | | type="month" |
| | | value-format="YYYY-MM" |
| | | format="YYYY-MM" |
| | | placeholder="è¯·éæ©å·¥èµæä»½" |
| | | style="width: 100%" |
| | | clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="éæ©é¨é¨" |
| | | prop="deptIds"> |
| | | <el-select v-model="form.deptIds" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | multiple |
| | | collapse-tags-tooltip |
| | | style="width: 100%"> |
| | | <el-option v-for="item in deptOptions" |
| | | :key="item.deptId" |
| | | :label="item.deptName" |
| | | :value="item.deptId" |
| | | :disabled="isDeptDisabled(item.deptId)" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="éæ©å·¥èµæä»½" prop="salaryMonth"> |
| | | <el-date-picker |
| | | v-model="form.salaryMonth" |
| | | type="month" |
| | | value-format="YYYY-MM" |
| | | format="YYYY-MM" |
| | | placeholder="è¯·éæ©å·¥èµæä»½" |
| | | style="width: 100%" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="夿³¨" prop="remark"> |
| | | <el-input |
| | | v-model="form.remark" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | /> |
| | | <el-form-item label="夿³¨" |
| | | prop="remark"> |
| | | <el-input v-model="form.remark" |
| | | placeholder="请è¾å
¥" |
| | | clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="24"> |
| | | <el-col :span="6"> |
| | | <el-form-item label="æ¯ä»é¶è¡" prop="payBank"> |
| | | <el-select |
| | | v-model="form.payBank" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | filterable |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="b in bankOptions" |
| | | :key="b" |
| | | :label="b" |
| | | :value="b" |
| | | /> |
| | | <el-form-item label="æ¯ä»é¶è¡" |
| | | prop="payBank"> |
| | | <el-select v-model="form.payBank" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | filterable |
| | | style="width: 100%"> |
| | | <el-option v-for="b in bankOptions" |
| | | :key="b" |
| | | :label="b" |
| | | :value="b" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="å®¡æ ¸äºº" prop="auditUserId"> |
| | | <el-select |
| | | v-model="form.auditUserId" |
| | | placeholder="è¯·éæ©å®¡æ ¸äºº" |
| | | clearable |
| | | filterable |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="item in userList" |
| | | :key="item.userId" |
| | | :label="item.nickName" |
| | | :value="item.userId" |
| | | /> |
| | | <el-form-item label="å®¡æ ¸äºº" |
| | | prop="auditUserId"> |
| | | <el-select v-model="form.auditUserId" |
| | | placeholder="è¯·éæ©å®¡æ ¸äºº" |
| | | clearable |
| | | filterable |
| | | style="width: 100%"> |
| | | <el-option v-for="item in userList" |
| | | :key="item.userId" |
| | | :label="item.nickName" |
| | | :value="item.userId" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | </el-card> |
| | | |
| | | <!-- æä½æé® --> |
| | | <div class="toolbar"> |
| | | <el-button type="primary" @click="handleGenerate">çæå·¥èµè¡¨</el-button> |
| | | <el-button type="primary" |
| | | @click="handleGenerate">çæå·¥èµè¡¨</el-button> |
| | | <el-button @click="handleClear">æ¸
空</el-button> |
| | | <el-button @click="handleBatchDelete">å é¤</el-button> |
| | | <el-button @click="handleTaxForm">个ç¨è¡¨</el-button> |
| | | </div> |
| | | |
| | | <!-- å工工èµè¯¦æ
è¡¨æ ¼ --> |
| | | <div class="employee-table-wrap"> |
| | | <el-table |
| | | ref="employeeTableRef" |
| | | :data="employeeList" |
| | | border |
| | | max-height="400" |
| | | @selection-change="onEmployeeSelectionChange" |
| | | > |
| | | <el-table-column type="selection" width="55" align="center" /> |
| | | <el-table-column label="åå·¥å§å" prop="staffName" minWidth="100" /> |
| | | <el-table-column label="é¨é¨" prop="deptName" minWidth="100" /> |
| | | <el-table-column label="åºæ¬å·¥èµ" minWidth="110"> |
| | | <el-table ref="employeeTableRef" |
| | | :data="employeeList" |
| | | border |
| | | max-height="400" |
| | | @selection-change="onEmployeeSelectionChange"> |
| | | <el-table-column type="selection" |
| | | width="55" |
| | | align="center" /> |
| | | <el-table-column label="åå·¥å§å" |
| | | prop="staffName" |
| | | minWidth="100" /> |
| | | <el-table-column label="é¨é¨" |
| | | prop="deptName" |
| | | minWidth="100" /> |
| | | <el-table-column label="åºæ¬å·¥èµ" |
| | | minWidth="110"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model.number="row.basicSalary" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.basicSalary = parseNum(row.basicSalary)" |
| | | /> |
| | | <el-input v-model.number="row.basicSalary" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="handleSalaryInput(row)" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="计件工èµ" minWidth="110"> |
| | | <el-table-column label="计件工èµ" |
| | | minWidth="110"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model.number="row.pieceSalary" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.pieceSalary = parseNum(row.pieceSalary)" |
| | | /> |
| | | <el-input v-model.number="row.pieceSalary" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | disabled |
| | | @input="row.pieceSalary = parseNum(row.pieceSalary)" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="计æ¶å·¥èµ" minWidth="110"> |
| | | <el-table-column label="计æ¶å·¥èµ" |
| | | minWidth="110"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model.number="row.hourlySalary" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.hourlySalary = parseNum(row.hourlySalary)" |
| | | /> |
| | | <el-input v-model.number="row.hourlySalary" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | disabled |
| | | @input="row.hourlySalary = parseNum(row.hourlySalary)" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å
¶ä»æ¶å
¥" minWidth="110"> |
| | | <el-table-column label="å
¶ä»æ¶å
¥" |
| | | minWidth="110"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model.number="row.otherIncome" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.otherIncome = parseNum(row.otherIncome)" |
| | | /> |
| | | <el-input v-model.number="row.otherIncome" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="handleSalaryInput(row)" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="社ä¿ä¸ªäºº" minWidth="110"> |
| | | <el-table-column label="社ä¿ä¸ªäºº" |
| | | minWidth="110"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model.number="row.socialPersonal" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.socialPersonal = parseNum(row.socialPersonal)" |
| | | /> |
| | | <el-input v-model.number="row.socialPersonal" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | disabled |
| | | @input="row.socialPersonal = parseNum(row.socialPersonal)" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å
¬ç§¯é个人" minWidth="120"> |
| | | <el-table-column label="å
¬ç§¯é个人" |
| | | minWidth="120"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model.number="row.fundPersonal" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.fundPersonal = parseNum(row.fundPersonal)" |
| | | /> |
| | | <el-input v-model.number="row.fundPersonal" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | disabled |
| | | @input="row.fundPersonal = parseNum(row.fundPersonal)" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å
¶ä»æ¯åº" minWidth="110"> |
| | | <el-table-column label="å
¶ä»æ¯åº" |
| | | minWidth="110"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model.number="row.otherDeduct" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.otherDeduct = parseNum(row.otherDeduct)" |
| | | /> |
| | | <el-input v-model.number="row.otherDeduct" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | disabled |
| | | @input="row.otherDeduct = parseNum(row.otherDeduct)" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å·¥èµä¸ªç¨" minWidth="110"> |
| | | <el-table-column label="社ä¿è¡¥ç¼´" |
| | | minWidth="120"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model.number="row.salaryTax" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.salaryTax = parseNum(row.salaryTax)" |
| | | /> |
| | | <el-input v-model.number="row.socialSupplementAmount" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="handleSalaryInput(row)" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="åºåå·¥èµ" minWidth="110"> |
| | | <el-table-column label="å·¥èµä¸ªç¨" |
| | | minWidth="110"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model.number="row.grossSalary" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.grossSalary = parseNum(row.grossSalary)" |
| | | /> |
| | | <el-input v-model.number="row.salaryTax" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | disabled |
| | | @input="row.salaryTax = parseNum(row.salaryTax)" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="åºæ£å·¥èµ" minWidth="110"> |
| | | <el-table-column label="åºåå·¥èµ" |
| | | minWidth="110"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model.number="row.deductSalary" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.deductSalary = parseNum(row.deductSalary)" |
| | | /> |
| | | <el-input v-model.number="row.grossSalary" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | disabled |
| | | @input="row.grossSalary = parseNum(row.grossSalary)" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å®åå·¥èµ" minWidth="110"> |
| | | <el-table-column label="åºæ£å·¥èµ" |
| | | minWidth="110"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model.number="row.netSalary" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.netSalary = parseNum(row.netSalary)" |
| | | /> |
| | | <el-input v-model.number="row.deductSalary" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | disabled |
| | | @input="row.deductSalary = parseNum(row.deductSalary)" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="夿³¨" minWidth="120"> |
| | | <el-table-column label="å®åå·¥èµ" |
| | | minWidth="110"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model="row.remark" |
| | | placeholder="请è¾å
¥" |
| | | size="small" |
| | | /> |
| | | <el-input v-model.number="row.netSalary" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | disabled |
| | | @input="row.netSalary = parseNum(row.netSalary)" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="80" align="center" fixed="right"> |
| | | <el-table-column label="夿³¨" |
| | | minWidth="120"> |
| | | <template #default="{ row }"> |
| | | <el-button type="primary" link @click="removeEmployee(row)">å é¤</el-button> |
| | | <el-input v-model="row.remark" |
| | | placeholder="请è¾å
¥" |
| | | size="small" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" |
| | | width="80" |
| | | align="center" |
| | | fixed="right"> |
| | | <template #default="{ row }"> |
| | | <el-button type="primary" |
| | | link |
| | | @click="removeEmployee(row)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div v-if="!employeeList.length" class="table-empty">ææ æ°æ®</div> |
| | | <div v-else class="salary-total"> |
| | | <div v-if="!employeeList.length" |
| | | class="table-empty">ææ æ°æ®</div> |
| | | <div v-else |
| | | class="salary-total"> |
| | | <span class="total-label">å·¥èµæ»é¢ï¼</span> |
| | | <span class="total-value">Â¥ {{ totalSalary.toFixed(2) }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | |
| | | |
| | | <!-- æ°å¢äººåå¼¹çª --> |
| | | <el-dialog |
| | | v-model="addPersonVisible" |
| | | title="æ°å¢äººå" |
| | | width="400px" |
| | | append-to-body |
| | | @close="addPersonClose" |
| | | > |
| | | <el-dialog v-model="addPersonVisible" |
| | | title="æ°å¢äººå" |
| | | width="400px" |
| | | append-to-body |
| | | @close="addPersonClose"> |
| | | <div class="add-person-tree"> |
| | | <el-tree |
| | | ref="personTreeRef" |
| | | :data="deptStaffTree" |
| | | show-checkbox |
| | | node-key="id" |
| | | :props="{ label: 'label', children: 'children' }" |
| | | default-expand-all |
| | | /> |
| | | <el-tree ref="personTreeRef" |
| | | :data="deptStaffTree" |
| | | show-checkbox |
| | | node-key="id" |
| | | :props="{ label: 'label', children: 'children' }" |
| | | default-expand-all /> |
| | | </div> |
| | | <template #footer> |
| | | <el-button @click="addPersonVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" @click="confirmAddPerson">ç¡®å®</el-button> |
| | | <el-button type="primary" |
| | | @click="confirmAddPerson">ç¡®å®</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- 个ç¨è¡¨å¼¹çª --> |
| | | <el-dialog |
| | | v-model="taxDialogVisible" |
| | | title="个ç¨è¡¨" |
| | | width="700px" |
| | | append-to-body |
| | | > |
| | | <el-dialog v-model="taxDialogVisible" |
| | | title="个ç¨è¡¨" |
| | | width="700px" |
| | | append-to-body> |
| | | <div class="tax-desc">个人æå¾ç¨å
å¾é¢ï¼5000å
</div> |
| | | <el-table :data="taxTableData" border style="width: 100%;margin-bottom: 20px;"> |
| | | <el-table-column prop="level" label="级æ°" width="80" align="center" /> |
| | | <el-table-column |
| | | prop="range" |
| | | label="å
¨å¹´åºçº³ç¨æå¾é¢/å
" |
| | | min-width="220" |
| | | /> |
| | | <el-table-column |
| | | prop="rate" |
| | | label="ç¨ç(%)" |
| | | width="100" |
| | | align="center" |
| | | /> |
| | | <el-table-column |
| | | prop="quickDeduction" |
| | | label="éç®æ£é¤æ°/å
" |
| | | width="160" |
| | | align="center" |
| | | /> |
| | | <el-table :data="taxTableData" |
| | | border |
| | | style="width: 100%;margin-bottom: 20px;"> |
| | | <el-table-column prop="level" |
| | | label="级æ°" |
| | | width="80" |
| | | align="center" /> |
| | | <el-table-column prop="range" |
| | | label="å
¨å¹´åºçº³ç¨æå¾é¢/å
" |
| | | min-width="220" /> |
| | | <el-table-column prop="rate" |
| | | label="ç¨ç(%)" |
| | | width="100" |
| | | align="center" /> |
| | | <el-table-column prop="quickDeduction" |
| | | label="éç®æ£é¤æ°/å
" |
| | | width="160" |
| | | align="center" /> |
| | | </el-table> |
| | | </el-dialog> |
| | | </FormDialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, toRefs, computed, getCurrentInstance, nextTick } from "vue"; |
| | | import { ArrowUp } from "@element-plus/icons-vue"; |
| | | import FormDialog from "@/components/Dialog/FormDialog.vue"; |
| | | import { listDept } from "@/api/system/dept.js"; |
| | | import { staffOnJobList } from "@/api/personnelManagement/monthlyStatistics.js"; |
| | | import { |
| | | ref, |
| | | reactive, |
| | | toRefs, |
| | | computed, |
| | | getCurrentInstance, |
| | | nextTick, |
| | | watch, |
| | | } from "vue"; |
| | | import { ArrowUp } from "@element-plus/icons-vue"; |
| | | import FormDialog from "@/components/Dialog/FormDialog.vue"; |
| | | import { listDept } from "@/api/system/dept.js"; |
| | | import { staffOnJobList } from "@/api/personnelManagement/monthlyStatistics.js"; |
| | | import { bankList } from "@/api/personnelManagement/bank.js"; |
| | | import { |
| | | staffSalaryMainAdd, |
| | | staffSalaryMainUpdate, |
| | | staffSalaryMainCalculateSalary, |
| | | staffSalaryMainCalculateByEmployeeId, |
| | | staffSalaryMainAvailableDepts, |
| | | } from "@/api/personnelManagement/staffSalaryMain.js"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user.js"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user.js"; |
| | | |
| | | const emit = defineEmits(["update:modelValue", "close"]); |
| | | const props = defineProps({ |
| | | modelValue: { type: Boolean, default: false }, |
| | | operationType: { type: String, default: "add" }, |
| | | row: { type: Object, default: () => ({}) }, |
| | | }); |
| | | |
| | | const emit = defineEmits(["update:modelValue", "close"]); |
| | | const props = defineProps({ |
| | | modelValue: { type: Boolean, default: false }, |
| | | operationType: { type: String, default: "add" }, |
| | | row: { type: Object, default: () => ({}) }, |
| | | }); |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | const dialogVisible = computed({ |
| | | get: () => props.modelValue, |
| | | set: val => emit("update:modelValue", val), |
| | | }); |
| | | |
| | | const dialogVisible = computed({ |
| | | get: () => props.modelValue, |
| | | set: (val) => emit("update:modelValue", val), |
| | | }); |
| | | const formRef = ref(null); |
| | | const employeeTableRef = ref(null); |
| | | const personTreeRef = ref(null); |
| | | const addPersonVisible = ref(false); |
| | | const taxDialogVisible = ref(false); |
| | | const deptOptions = ref([]); |
| | | const availableDeptIds = ref(new Set()); |
| | | const deptStaffTree = ref([]); |
| | | const employeeList = ref([]); |
| | | const selectedEmployees = ref([]); |
| | | const bankOptions = ref([]); |
| | | const userList = ref([]); |
| | | const recalcTimers = new Map(); |
| | | const recalcVersions = new Map(); |
| | | const taxTableData = ref([ |
| | | { level: 1, range: "ä¸è¶
è¿36000å
", rate: 3, quickDeduction: 0 }, |
| | | { level: 2, range: "è¶
è¿36000-144000å
", rate: 10, quickDeduction: 2520 }, |
| | | { level: 3, range: "è¶
è¿144000-300000å
", rate: 20, quickDeduction: 16920 }, |
| | | { level: 4, range: "è¶
è¿300000-420000å
", rate: 25, quickDeduction: 31920 }, |
| | | { level: 5, range: "è¶
è¿420000-660000å
", rate: 30, quickDeduction: 52920 }, |
| | | { level: 6, range: "è¶
è¿660000-960000å
", rate: 35, quickDeduction: 85920 }, |
| | | { level: 7, range: "è¶
è¿960000å
", rate: 45, quickDeduction: 181920 }, |
| | | ]); |
| | | |
| | | const formRef = ref(null); |
| | | const employeeTableRef = ref(null); |
| | | const personTreeRef = ref(null); |
| | | const addPersonVisible = ref(false); |
| | | const taxDialogVisible = ref(false); |
| | | const deptOptions = ref([]); |
| | | const deptStaffTree = ref([]); |
| | | const employeeList = ref([]); |
| | | const selectedEmployees = ref([]); |
| | | const bankOptions = ref([]); |
| | | const userList = ref([]); |
| | | const taxTableData = ref([ |
| | | { level: 1, range: "ä¸è¶
è¿36000å
", rate: 3, quickDeduction: 0 }, |
| | | { level: 2, range: "è¶
è¿36000-144000å
", rate: 10, quickDeduction: 2520 }, |
| | | { level: 3, range: "è¶
è¿144000-300000å
", rate: 20, quickDeduction: 16920 }, |
| | | { level: 4, range: "è¶
è¿300000-420000å
", rate: 25, quickDeduction: 31920 }, |
| | | { level: 5, range: "è¶
è¿420000-660000å
", rate: 30, quickDeduction: 52920 }, |
| | | { level: 6, range: "è¶
è¿660000-960000å
", rate: 35, quickDeduction: 85920 }, |
| | | { level: 7, range: "è¶
è¿960000å
", rate: 45, quickDeduction: 181920 }, |
| | | ]); |
| | | function parseNum(v) { |
| | | if (v === "" || v == null) return 0; |
| | | const n = Number(v); |
| | | return isNaN(n) ? 0 : n; |
| | | } |
| | | |
| | | function parseNum(v) { |
| | | if (v === "" || v == null) return 0; |
| | | const n = Number(v); |
| | | return isNaN(n) ? 0 : n; |
| | | } |
| | | function parseNullableNum(v) { |
| | | if (v === "" || v == null) return null; |
| | | const n = Number(v); |
| | | return isNaN(n) ? null : n; |
| | | } |
| | | |
| | | // åºç¡èµæè¡¨å |
| | | const data = reactive({ |
| | | form: { |
| | | const createEmployeeRow = () => ({ |
| | | staffOnJobId: undefined, |
| | | id: undefined, |
| | | salaryTitle: "", |
| | | deptIds: [], |
| | | salaryMonth: "", |
| | | staffName: "", |
| | | postName: "", |
| | | deptName: "", |
| | | basicSalary: 0, |
| | | pieceSalary: 0, |
| | | hourlySalary: 0, |
| | | otherIncome: 0, |
| | | socialPersonal: 0, |
| | | fundPersonal: 0, |
| | | otherDeduct: 0, |
| | | socialSupplementAmount: null, |
| | | salaryTax: 0, |
| | | grossSalary: 0, |
| | | deductSalary: 0, |
| | | netSalary: 0, |
| | | remark: "", |
| | | payBank: "", |
| | | auditUserId: undefined, |
| | | }, |
| | | rules: { |
| | | salaryTitle: [{ required: true, message: "请è¾å
¥å·¥èµä¸»é¢", trigger: "blur" }], |
| | | deptIds: [{ required: true, message: "è¯·éæ©é¨é¨", trigger: "change" }], |
| | | salaryMonth: [{ required: true, message: "è¯·éæ©å·¥èµæä»½", trigger: "change" }], |
| | | auditUserId: [{ required: true, message: "è¯·éæ©å®¡æ ¸äºº", trigger: "change" }], |
| | | }, |
| | | }); |
| | | const { form, rules } = toRefs(data); |
| | | |
| | | // 计ç®å·¥èµæ»é¢ï¼ææåå·¥å®åå·¥èµä¹åï¼ |
| | | const totalSalary = computed(() => { |
| | | return employeeList.value.reduce((sum, e) => sum + parseNum(e.netSalary), 0); |
| | | }); |
| | | |
| | | // æ ¹æ®å®¡æ ¸äººIDè·åå®¡æ ¸äººåç§° |
| | | const auditUserName = computed(() => { |
| | | if (!form.value.auditUserId) return ""; |
| | | const user = userList.value.find(u => u.userId === form.value.auditUserId); |
| | | return user ? user.nickName : ""; |
| | | }); |
| | | |
| | | const loadBankOptions = () => { |
| | | return bankList().then((res) => { |
| | | const list = Array.isArray(res?.data) ? res.data : []; |
| | | bankOptions.value = list |
| | | .map((b) => (b?.bankName == null ? "" : String(b.bankName).trim())) |
| | | .filter((v) => v !== ""); |
| | | }); |
| | | }; |
| | | |
| | | const loadUserList = () => { |
| | | return userListNoPageByTenantId().then((res) => { |
| | | userList.value = res.data || []; |
| | | const normalizeEmployeeRow = (source, fallback = {}) => ({ |
| | | ...createEmployeeRow(), |
| | | ...fallback, |
| | | ...source, |
| | | staffOnJobId: source?.staffOnJobId ?? source?.staffId ?? source?.id ?? fallback.staffOnJobId ?? fallback.id, |
| | | id: source?.staffOnJobId ?? source?.staffId ?? source?.id ?? fallback.staffOnJobId ?? fallback.id, |
| | | staffName: source?.staffName ?? fallback.staffName ?? "", |
| | | postName: source?.postName ?? fallback.postName ?? "", |
| | | deptName: source?.deptName ?? fallback.deptName ?? "", |
| | | basicSalary: parseNum(source?.basicSalary ?? fallback.basicSalary), |
| | | pieceSalary: parseNum(source?.pieceSalary ?? fallback.pieceSalary), |
| | | hourlySalary: parseNum(source?.hourlySalary ?? fallback.hourlySalary), |
| | | otherIncome: parseNum(source?.otherIncome ?? fallback.otherIncome), |
| | | socialPersonal: parseNum(source?.socialPersonal ?? fallback.socialPersonal), |
| | | fundPersonal: parseNum(source?.fundPersonal ?? fallback.fundPersonal), |
| | | otherDeduct: parseNum(source?.otherDeduct ?? fallback.otherDeduct), |
| | | socialSupplementAmount: |
| | | source?.socialSupplementAmount ?? fallback.socialSupplementAmount ?? null, |
| | | salaryTax: parseNum(source?.salaryTax ?? fallback.salaryTax), |
| | | grossSalary: parseNum(source?.grossSalary ?? fallback.grossSalary), |
| | | deductSalary: parseNum(source?.deductSalary ?? fallback.deductSalary), |
| | | netSalary: parseNum(source?.netSalary ?? fallback.netSalary), |
| | | remark: source?.remark ?? fallback.remark ?? "", |
| | | }); |
| | | }; |
| | | |
| | | // æå¹³åé¨é¨æ ä¾ä¸æä½¿ç¨ |
| | | function flattenDept(tree, list = []) { |
| | | if (!tree?.length) return list; |
| | | tree.forEach((node) => { |
| | | list.push({ deptId: node.deptId, deptName: node.deptName }); |
| | | if (node.children?.length) flattenDept(node.children, list); |
| | | }); |
| | | return list; |
| | | } |
| | | const applyBackendRow = (targetRow, sourceRow) => { |
| | | Object.assign(targetRow, normalizeEmployeeRow(sourceRow, targetRow)); |
| | | }; |
| | | |
| | | const loadDeptOptions = () => { |
| | | listDept().then((res) => { |
| | | const tree = res.data ?? []; |
| | | deptOptions.value = flattenDept(tree); |
| | | }); |
| | | }; |
| | | const clearRowRecalcState = row => { |
| | | const key = String(row?.staffOnJobId ?? row?.id ?? ""); |
| | | if (!key) return; |
| | | clearTimeout(recalcTimers.get(key)); |
| | | recalcTimers.delete(key); |
| | | recalcVersions.delete(key); |
| | | }; |
| | | |
| | | // æå»º é¨é¨-äººå æ ï¼ç¨äºæ°å¢äººåå¼¹çªï¼ |
| | | const loadDeptStaffTree = () => { |
| | | Promise.all([listDept(), staffOnJobList()]).then(([deptRes, staffRes]) => { |
| | | const tree = deptRes.data ?? []; |
| | | const staffList = staffRes.data ?? []; |
| | | const deptMap = new Map(); |
| | | function walk(nodes) { |
| | | nodes.forEach((node) => { |
| | | deptMap.set(node.deptId, { |
| | | id: "dept_" + node.deptId, |
| | | deptId: node.deptId, |
| | | label: node.deptName, |
| | | type: "dept", |
| | | children: [], |
| | | const resetRecalcState = () => { |
| | | recalcTimers.forEach(timer => clearTimeout(timer)); |
| | | recalcTimers.clear(); |
| | | recalcVersions.clear(); |
| | | }; |
| | | |
| | | const requestEmployeeRecalculate = row => { |
| | | const key = String(row?.staffOnJobId ?? row?.id ?? ""); |
| | | if (!key || !form.value.salaryMonth) return; |
| | | const version = (recalcVersions.get(key) || 0) + 1; |
| | | recalcVersions.set(key, version); |
| | | clearTimeout(recalcTimers.get(key)); |
| | | const payload = { |
| | | staffOnJobId: row.staffOnJobId ?? row.id, |
| | | salaryMonth: form.value.salaryMonth, |
| | | staffName: row.staffName ?? "", |
| | | postName: row.postName ?? "", |
| | | deptName: row.deptName ?? "", |
| | | basicSalary: parseNum(row.basicSalary), |
| | | pieceSalary: parseNum(row.pieceSalary), |
| | | hourlySalary: parseNum(row.hourlySalary), |
| | | otherIncome: parseNum(row.otherIncome), |
| | | otherDeduct: parseNum(row.otherDeduct), |
| | | socialSupplementAmount: parseNullableNum(row.socialSupplementAmount), |
| | | remark: row.remark ?? "", |
| | | }; |
| | | recalcTimers.set( |
| | | key, |
| | | setTimeout(() => { |
| | | staffSalaryMainCalculateByEmployeeId(payload).then(res => { |
| | | if (recalcVersions.get(key) !== version) return; |
| | | applyBackendRow(row, res?.data || {}); |
| | | }); |
| | | if (node.children?.length) walk(node.children); |
| | | }); |
| | | } |
| | | walk(tree); |
| | | staffList.forEach((s) => { |
| | | const deptId = s.deptId ?? s.dept_id; |
| | | const node = deptMap.get(deptId); |
| | | if (node) { |
| | | node.children.push({ |
| | | id: s.id ?? s.staffId, |
| | | staffId: s.id ?? s.staffId, |
| | | label: s.staffName ?? s.name, |
| | | type: "staff", |
| | | ...s, |
| | | }); |
| | | } |
| | | }); |
| | | deptStaffTree.value = Array.from(deptMap.values()).filter( |
| | | (n) => n.children && n.children.length > 0 |
| | | }, 250) |
| | | ); |
| | | }); |
| | | }; |
| | | }; |
| | | |
| | | const openDialog = (type, row) => { |
| | | nextTick(() => { |
| | | loadDeptOptions(); |
| | | loadBankOptions(); |
| | | loadUserList(); |
| | | employeeList.value = []; |
| | | Object.assign(form.value, { |
| | | const handleSalaryInput = row => { |
| | | row.basicSalary = parseNum(row.basicSalary); |
| | | row.otherIncome = parseNum(row.otherIncome); |
| | | requestEmployeeRecalculate(row); |
| | | }; |
| | | |
| | | // åºç¡èµæè¡¨å |
| | | const data = reactive({ |
| | | form: { |
| | | id: undefined, |
| | | salaryTitle: "", |
| | | deptIds: [], |
| | |
| | | remark: "", |
| | | payBank: "", |
| | | auditUserId: undefined, |
| | | }, |
| | | rules: { |
| | | salaryTitle: [ |
| | | { required: true, message: "请è¾å
¥å·¥èµä¸»é¢", trigger: "blur" }, |
| | | ], |
| | | deptIds: [{ required: true, message: "è¯·éæ©é¨é¨", trigger: "change" }], |
| | | salaryMonth: [ |
| | | { required: true, message: "è¯·éæ©å·¥èµæä»½", trigger: "change" }, |
| | | ], |
| | | auditUserId: [ |
| | | { required: true, message: "è¯·éæ©å®¡æ ¸äºº", trigger: "change" }, |
| | | ], |
| | | }, |
| | | }); |
| | | const { form, rules } = toRefs(data); |
| | | |
| | | // 计ç®å·¥èµæ»é¢ï¼ææåå·¥å®åå·¥èµä¹åï¼ |
| | | const totalSalary = computed(() => { |
| | | return employeeList.value.reduce((sum, e) => sum + parseNum(e.netSalary), 0); |
| | | }); |
| | | |
| | | // æ ¹æ®å®¡æ ¸äººIDè·åå®¡æ ¸äººåç§° |
| | | const auditUserName = computed(() => { |
| | | if (!form.value.auditUserId) return ""; |
| | | const user = userList.value.find(u => u.userId === form.value.auditUserId); |
| | | return user ? user.nickName : ""; |
| | | }); |
| | | |
| | | const loadBankOptions = () => { |
| | | return bankList().then(res => { |
| | | const list = Array.isArray(res?.data) ? res.data : []; |
| | | bankOptions.value = list |
| | | .map(b => (b?.bankName == null ? "" : String(b.bankName).trim())) |
| | | .filter(v => v !== ""); |
| | | }); |
| | | // ç¼è¾ï¼å表页已è¿åä¸»è¡¨åæ®µï¼è¿éåªååæ¾ï¼æç»ç±âçæå·¥èµè¡¨/计ç®å·¥èµâå¾å°ï¼ |
| | | if (type === "edit" && row?.id) { |
| | | form.value.id = row.id; |
| | | form.value.salaryTitle = row.salaryTitle ?? ""; |
| | | // deptIds å端æ¯å符串ï¼å¤ä¸ªç¨éå·åéï¼ï¼å½å表å仿¯åé deptId |
| | | form.value.deptIds = row.deptIds |
| | | ? String(row.deptIds).split(",").map((id) => Number(id.trim())).filter(Boolean) |
| | | : []; |
| | | form.value.salaryMonth = row.salaryMonth ?? ""; |
| | | form.value.remark = row.remark ?? ""; |
| | | form.value.payBank = row.payBank ?? ""; |
| | | form.value.auditUserId = row.auditUserId ?? undefined; |
| | | |
| | | // 妿æåå·¥æç»æ°æ®ï¼ç´æ¥åæ¾ |
| | | if (row.staffSalaryDetailList && row.staffSalaryDetailList.length > 0) { |
| | | employeeList.value = row.staffSalaryDetailList.map((e) => ({ |
| | | staffOnJobId: e.staffOnJobId ?? e.staffId ?? e.id, |
| | | id: e.staffOnJobId ?? e.staffId ?? e.id, |
| | | staffName: e.staffName ?? "", |
| | | postName: e.postName ?? "", |
| | | deptName: e.deptName ?? "", |
| | | basicSalary: parseNum(e.basicSalary), |
| | | pieceSalary: parseNum(e.pieceSalary), |
| | | hourlySalary: parseNum(e.hourlySalary), |
| | | otherIncome: parseNum(e.otherIncome), |
| | | socialPersonal: parseNum(e.socialPersonal), |
| | | fundPersonal: parseNum(e.fundPersonal), |
| | | otherDeduct: parseNum(e.otherDeduct), |
| | | salaryTax: parseNum(e.salaryTax), |
| | | grossSalary: parseNum(e.grossSalary), |
| | | deductSalary: parseNum(e.deductSalary), |
| | | netSalary: parseNum(e.netSalary), |
| | | remark: e.remark ?? "", |
| | | })); |
| | | } |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const openAddPerson = () => { |
| | | loadDeptStaffTree(); |
| | | addPersonVisible.value = true; |
| | | nextTick(() => { |
| | | personTreeRef.value?.setCheckedKeys([]); |
| | | }); |
| | | }; |
| | | |
| | | const addPersonClose = () => {}; |
| | | |
| | | const confirmAddPerson = () => { |
| | | const tree = personTreeRef.value; |
| | | if (!tree) { |
| | | addPersonVisible.value = false; |
| | | return; |
| | | } |
| | | const checked = tree.getCheckedNodes(); |
| | | const staffNodes = checked.filter((n) => n.type === "staff"); |
| | | const existIds = new Set(employeeList.value.map((e) => e.staffId || e.id)); |
| | | staffNodes.forEach((node) => { |
| | | const id = node.staffId ?? node.id; |
| | | if (existIds.has(id)) return; |
| | | existIds.add(id); |
| | | employeeList.value.push({ |
| | | staffOnJobId: id, |
| | | id, |
| | | staffName: node.label, |
| | | postName: node.postName ?? node.post ?? "", |
| | | deptName: node.deptName ?? "", |
| | | basicSalary: 0, |
| | | pieceSalary: 0, |
| | | hourlySalary: 0, |
| | | otherIncome: 0, |
| | | socialPersonal: 0, |
| | | fundPersonal: 0, |
| | | otherDeduct: 0, |
| | | salaryTax: 0, |
| | | grossSalary: 0, |
| | | deductSalary: 0, |
| | | netSalary: 0, |
| | | remark: "", |
| | | }); |
| | | }); |
| | | addPersonVisible.value = false; |
| | | }; |
| | | |
| | | const removeEmployee = (row) => { |
| | | employeeList.value = employeeList.value.filter( |
| | | (e) => (e.staffOnJobId || e.id) !== (row.staffOnJobId || row.id) |
| | | ); |
| | | }; |
| | | |
| | | const onEmployeeSelectionChange = (selection) => { |
| | | selectedEmployees.value = selection; |
| | | }; |
| | | |
| | | const handleBatchDelete = () => { |
| | | if (!selectedEmployees.value?.length) { |
| | | proxy.$modal.msgWarning("请å
å¾éè¦å é¤çåå·¥"); |
| | | return; |
| | | } |
| | | const ids = new Set(selectedEmployees.value.map((e) => e.staffOnJobId || e.id)); |
| | | employeeList.value = employeeList.value.filter( |
| | | (e) => !ids.has(e.staffOnJobId || e.id) |
| | | ); |
| | | }; |
| | | |
| | | const handleGenerate = () => { |
| | | if (!form.value.deptIds?.length) { |
| | | proxy.$modal.msgWarning("请å
éæ©é¨é¨"); |
| | | return; |
| | | } |
| | | if (!form.value.salaryMonth) { |
| | | proxy.$modal.msgWarning("请å
éæ©å·¥èµæä»½"); |
| | | return; |
| | | } |
| | | const payload = { |
| | | ids: form.value.deptIds, |
| | | date: form.value.salaryMonth, |
| | | }; |
| | | staffSalaryMainCalculateSalary(payload).then((res) => { |
| | | const list = Array.isArray(res?.data) ? res.data : []; |
| | | if (!list.length) { |
| | | proxy.$modal.msgWarning("æªè®¡ç®å°å·¥èµæ°æ®"); |
| | | |
| | | const loadUserList = () => { |
| | | return userListNoPageByTenantId().then(res => { |
| | | userList.value = res.data || []; |
| | | }); |
| | | }; |
| | | |
| | | // æå¹³åé¨é¨æ ä¾ä¸æä½¿ç¨ |
| | | function flattenDept(tree, list = []) { |
| | | if (!tree?.length) return list; |
| | | tree.forEach(node => { |
| | | list.push({ deptId: node.deptId, deptName: node.deptName }); |
| | | if (node.children?.length) flattenDept(node.children, list); |
| | | }); |
| | | return list; |
| | | } |
| | | |
| | | // å è½½ææé¨é¨ |
| | | const loadAllDeptOptions = () => { |
| | | listDept().then(res => { |
| | | const tree = res.data ?? []; |
| | | deptOptions.value = flattenDept(tree); |
| | | }); |
| | | }; |
| | | |
| | | // å è½½å¯ç¨é¨é¨å表ï¼ç¨äºå¤æé¨é¨æ¯å¦å¯éï¼ |
| | | const loadAvailableDepts = (salaryMonth) => { |
| | | if (!salaryMonth) { |
| | | availableDeptIds.value = new Set(); |
| | | return; |
| | | } |
| | | employeeList.value = list.map((e) => ({ |
| | | ...e, |
| | | staffOnJobId: e.staffOnJobId ?? e.staffId ?? e.id, |
| | | staffName: e.staffName, |
| | | postName: e.postName, |
| | | deptName: e.deptName, |
| | | basicSalary: parseNum(e.basicSalary), |
| | | pieceSalary: parseNum(e.pieceSalary), |
| | | hourlySalary: parseNum(e.hourlySalary), |
| | | otherIncome: parseNum(e.otherIncome), |
| | | socialPersonal: parseNum(e.socialPersonal), |
| | | fundPersonal: parseNum(e.fundPersonal), |
| | | otherDeduct: parseNum(e.otherDeduct), |
| | | salaryTax: parseNum(e.salaryTax), |
| | | grossSalary: parseNum(e.grossSalary), |
| | | deductSalary: parseNum(e.deductSalary), |
| | | netSalary: parseNum(e.netSalary), |
| | | remark: e.remark ?? "", |
| | | })); |
| | | proxy.$modal.msgSuccess("çææå"); |
| | | }); |
| | | }; |
| | | |
| | | const handleClear = () => { |
| | | proxy.$modal.confirm("ç¡®å®æ¸
空å½ååå·¥å表åï¼").then(() => { |
| | | employeeList.value = []; |
| | | }).catch(() => {}); |
| | | }; |
| | | |
| | | const handleTaxForm = () => { |
| | | taxDialogVisible.value = true; |
| | | }; |
| | | |
| | | const submitForm = () => { |
| | | formRef.value?.validate((valid) => { |
| | | if (!valid) return; |
| | | saveData(3); // 确认æäº¤ï¼ç¶æä¸º3ï¼å¾
å®¡æ ¸ï¼ |
| | | }); |
| | | }; |
| | | |
| | | const saveDraft = () => { |
| | | formRef.value?.validate((valid) => { |
| | | if (!valid) return; |
| | | saveData(1); // ä¿åè稿ï¼ç¶æä¸º1ï¼èç¨¿ï¼ |
| | | }); |
| | | }; |
| | | |
| | | const saveData = (status) => { |
| | | const payload = { |
| | | id: form.value.id, |
| | | salaryTitle: form.value.salaryTitle, |
| | | deptIds: form.value.deptIds?.length ? form.value.deptIds.join(",") : "", |
| | | salaryMonth: form.value.salaryMonth, |
| | | remark: form.value.remark, |
| | | payBank: form.value.payBank, |
| | | auditUserId: form.value.auditUserId, |
| | | auditUserName: auditUserName.value, |
| | | totalSalary: totalSalary.value, |
| | | staffSalaryDetailList: employeeList.value.map((e) => ({ |
| | | staffOnJobId: e.staffOnJobId ?? e.staffId ?? e.id, |
| | | staffName: e.staffName, |
| | | postName: e.postName ?? "", |
| | | deptName: e.deptName ?? "", |
| | | basicSalary: parseNum(e.basicSalary), |
| | | pieceSalary: parseNum(e.pieceSalary), |
| | | hourlySalary: parseNum(e.hourlySalary), |
| | | otherIncome: parseNum(e.otherIncome), |
| | | socialPersonal: parseNum(e.socialPersonal), |
| | | fundPersonal: parseNum(e.fundPersonal), |
| | | otherDeduct: parseNum(e.otherDeduct), |
| | | salaryTax: parseNum(e.salaryTax), |
| | | grossSalary: parseNum(e.grossSalary), |
| | | deductSalary: parseNum(e.deductSalary), |
| | | netSalary: parseNum(e.netSalary), |
| | | remark: e.remark ?? "", |
| | | })), |
| | | staffSalaryMainAvailableDepts(salaryMonth).then(res => { |
| | | const availableDepts = Array.isArray(res.data) ? res.data : []; |
| | | availableDeptIds.value = new Set(availableDepts.map(d => d.deptId)); |
| | | }); |
| | | }; |
| | | if (props.operationType === "add") { |
| | | staffSalaryMainAdd({ ...payload, status }).then(() => { |
| | | proxy.$modal.msgSuccess(status === 1 ? "è稿ä¿åæå" : "æäº¤æå"); |
| | | closeDia(); |
| | | }); |
| | | } else { |
| | | staffSalaryMainUpdate({ ...payload, status }).then(() => { |
| | | proxy.$modal.msgSuccess(status === 1 ? "è稿ä¿åæå" : "æäº¤æå"); |
| | | closeDia(); |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | const closeDia = () => { |
| | | dialogVisible.value = false; |
| | | emit("close"); |
| | | }; |
| | | // 夿é¨é¨æ¯å¦ç¦ç¨ |
| | | const isDeptDisabled = (deptId) => { |
| | | // å¦ææ²¡æéæ©æä»½ï¼ææé¨é¨é½å¯é |
| | | if (!form.value.salaryMonth) return false; |
| | | // 妿已鿩æä»½ï¼åªæå¨å¯ç¨é¨é¨å表ä¸çé¨é¨æå¯é |
| | | return !availableDeptIds.value.has(deptId); |
| | | }; |
| | | |
| | | defineExpose({ openDialog }); |
| | | // æå»º é¨é¨-äººå æ ï¼ç¨äºæ°å¢äººåå¼¹çªï¼ |
| | | const loadDeptStaffTree = () => { |
| | | Promise.all([listDept(), staffOnJobList()]).then(([deptRes, staffRes]) => { |
| | | const tree = deptRes.data ?? []; |
| | | const staffList = staffRes.data ?? []; |
| | | const deptMap = new Map(); |
| | | function walk(nodes) { |
| | | nodes.forEach(node => { |
| | | deptMap.set(node.deptId, { |
| | | id: "dept_" + node.deptId, |
| | | deptId: node.deptId, |
| | | label: node.deptName, |
| | | type: "dept", |
| | | children: [], |
| | | }); |
| | | if (node.children?.length) walk(node.children); |
| | | }); |
| | | } |
| | | walk(tree); |
| | | staffList.forEach(s => { |
| | | const deptId = s.deptId ?? s.dept_id; |
| | | const node = deptMap.get(deptId); |
| | | if (node) { |
| | | node.children.push({ |
| | | id: s.id ?? s.staffId, |
| | | staffId: s.id ?? s.staffId, |
| | | label: s.staffName ?? s.name, |
| | | type: "staff", |
| | | ...s, |
| | | }); |
| | | } |
| | | }); |
| | | deptStaffTree.value = Array.from(deptMap.values()).filter( |
| | | n => n.children && n.children.length > 0 |
| | | ); |
| | | }); |
| | | }; |
| | | |
| | | const openDialog = (type, row) => { |
| | | nextTick(() => { |
| | | resetRecalcState(); |
| | | loadBankOptions(); |
| | | loadUserList(); |
| | | loadAllDeptOptions(); |
| | | employeeList.value = []; |
| | | Object.assign(form.value, { |
| | | id: undefined, |
| | | salaryTitle: "", |
| | | deptIds: [], |
| | | salaryMonth: "", |
| | | remark: "", |
| | | payBank: "", |
| | | auditUserId: undefined, |
| | | }); |
| | | // ç¼è¾ï¼å表页已è¿åä¸»è¡¨åæ®µï¼è¿éåªååæ¾ï¼æç»ç±âçæå·¥èµè¡¨/计ç®å·¥èµâå¾å°ï¼ |
| | | if (type === "edit" && row?.id) { |
| | | form.value.id = row.id; |
| | | form.value.salaryTitle = row.salaryTitle ?? ""; |
| | | // deptIds å端æ¯å符串ï¼å¤ä¸ªç¨éå·åéï¼ï¼å½å表å仿¯åé deptId |
| | | form.value.deptIds = row.deptIds |
| | | ? String(row.deptIds) |
| | | .split(",") |
| | | .map(id => Number(id.trim())) |
| | | .filter(Boolean) |
| | | : []; |
| | | form.value.salaryMonth = row.salaryMonth ?? ""; |
| | | form.value.remark = row.remark ?? ""; |
| | | form.value.payBank = row.payBank ?? ""; |
| | | form.value.auditUserId = row.auditUserId ?? undefined; |
| | | |
| | | // 妿æåå·¥æç»æ°æ®ï¼ç´æ¥åæ¾ |
| | | if (row.staffSalaryDetailList && row.staffSalaryDetailList.length > 0) { |
| | | employeeList.value = row.staffSalaryDetailList.map(e => normalizeEmployeeRow(e)); |
| | | } |
| | | } |
| | | // ç¼è¾ç¶æä¸ææä»½æ¶ï¼å è½½å¯ç¨é¨é¨å表 |
| | | if (type === "edit" && form.value.salaryMonth) { |
| | | loadAvailableDepts(form.value.salaryMonth); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // çå¬å·¥èµæä»½ååï¼æ¸
空é¨é¨éæ©å¹¶å è½½å¯ç¨é¨é¨å表 |
| | | watch(() => form.value.salaryMonth, (newMonth, oldMonth) => { |
| | | // 忢æä»½æ¶æ¸
ç©ºå·²éæ©çé¨é¨ |
| | | if (newMonth !== oldMonth) { |
| | | form.value.deptIds = []; |
| | | } |
| | | loadAvailableDepts(newMonth); |
| | | }); |
| | | |
| | | const openAddPerson = () => { |
| | | loadDeptStaffTree(); |
| | | addPersonVisible.value = true; |
| | | nextTick(() => { |
| | | personTreeRef.value?.setCheckedKeys([]); |
| | | }); |
| | | }; |
| | | |
| | | const addPersonClose = () => {}; |
| | | |
| | | const confirmAddPerson = () => { |
| | | const tree = personTreeRef.value; |
| | | if (!tree) { |
| | | addPersonVisible.value = false; |
| | | return; |
| | | } |
| | | const checked = tree.getCheckedNodes(); |
| | | const staffNodes = checked.filter(n => n.type === "staff"); |
| | | const existIds = new Set(employeeList.value.map(e => e.staffId || e.id)); |
| | | staffNodes.forEach(node => { |
| | | const id = node.staffId ?? node.id; |
| | | if (existIds.has(id)) return; |
| | | existIds.add(id); |
| | | const newRow = normalizeEmployeeRow({ |
| | | staffOnJobId: id, |
| | | id, |
| | | staffName: node.label, |
| | | postName: node.postName ?? node.post ?? "", |
| | | deptName: node.deptName ?? "", |
| | | remark: "", |
| | | socialSupplementAmount: null, |
| | | }); |
| | | employeeList.value.push(newRow); |
| | | requestEmployeeRecalculate(newRow); |
| | | }); |
| | | addPersonVisible.value = false; |
| | | }; |
| | | |
| | | const removeEmployee = row => { |
| | | clearRowRecalcState(row); |
| | | employeeList.value = employeeList.value.filter( |
| | | e => (e.staffOnJobId || e.id) !== (row.staffOnJobId || row.id) |
| | | ); |
| | | }; |
| | | |
| | | const onEmployeeSelectionChange = selection => { |
| | | selectedEmployees.value = selection; |
| | | }; |
| | | |
| | | const handleBatchDelete = () => { |
| | | if (!selectedEmployees.value?.length) { |
| | | proxy.$modal.msgWarning("请å
å¾éè¦å é¤çåå·¥"); |
| | | return; |
| | | } |
| | | const ids = new Set(selectedEmployees.value.map(e => e.staffOnJobId || e.id)); |
| | | selectedEmployees.value.forEach(item => clearRowRecalcState(item)); |
| | | employeeList.value = employeeList.value.filter( |
| | | e => !ids.has(e.staffOnJobId || e.id) |
| | | ); |
| | | }; |
| | | |
| | | const handleGenerate = () => { |
| | | if (!form.value.deptIds?.length) { |
| | | proxy.$modal.msgWarning("请å
éæ©é¨é¨"); |
| | | return; |
| | | } |
| | | if (!form.value.salaryMonth) { |
| | | proxy.$modal.msgWarning("请å
éæ©å·¥èµæä»½"); |
| | | return; |
| | | } |
| | | const payload = { |
| | | ids: form.value.deptIds, |
| | | date: form.value.salaryMonth, |
| | | }; |
| | | staffSalaryMainCalculateSalary(payload).then(res => { |
| | | const list = Array.isArray(res?.data) ? res.data : []; |
| | | if (!list.length) { |
| | | proxy.$modal.msgWarning("æªè®¡ç®å°å·¥èµæ°æ®"); |
| | | return; |
| | | } |
| | | resetRecalcState(); |
| | | employeeList.value = list.map(e => normalizeEmployeeRow(e)); |
| | | proxy.$modal.msgSuccess("çææå"); |
| | | }); |
| | | }; |
| | | |
| | | const handleClear = () => { |
| | | proxy.$modal |
| | | .confirm("ç¡®å®æ¸
空å½ååå·¥å表åï¼") |
| | | .then(() => { |
| | | resetRecalcState(); |
| | | employeeList.value = []; |
| | | }) |
| | | .catch(() => {}); |
| | | }; |
| | | |
| | | const handleTaxForm = () => { |
| | | taxDialogVisible.value = true; |
| | | }; |
| | | |
| | | const submitForm = () => { |
| | | formRef.value?.validate(valid => { |
| | | if (!valid) return; |
| | | saveData(3); // 确认æäº¤ï¼ç¶æä¸º3ï¼å¾
å®¡æ ¸ï¼ |
| | | }); |
| | | }; |
| | | |
| | | const saveDraft = () => { |
| | | formRef.value?.validate(valid => { |
| | | if (!valid) return; |
| | | saveData(1); // ä¿åè稿ï¼ç¶æä¸º1ï¼èç¨¿ï¼ |
| | | }); |
| | | }; |
| | | |
| | | const saveData = status => { |
| | | const payload = { |
| | | id: form.value.id, |
| | | salaryTitle: form.value.salaryTitle, |
| | | deptIds: form.value.deptIds?.length ? form.value.deptIds.join(",") : "", |
| | | salaryMonth: form.value.salaryMonth, |
| | | remark: form.value.remark, |
| | | payBank: form.value.payBank, |
| | | auditUserId: form.value.auditUserId, |
| | | auditUserName: auditUserName.value, |
| | | totalSalary: totalSalary.value, |
| | | staffSalaryDetailList: employeeList.value.map(e => ({ |
| | | staffOnJobId: e.staffOnJobId ?? e.staffId ?? e.id, |
| | | staffName: e.staffName, |
| | | postName: e.postName ?? "", |
| | | deptName: e.deptName ?? "", |
| | | basicSalary: parseNum(e.basicSalary), |
| | | pieceSalary: parseNum(e.pieceSalary), |
| | | hourlySalary: parseNum(e.hourlySalary), |
| | | otherIncome: parseNum(e.otherIncome), |
| | | socialPersonal: parseNum(e.socialPersonal), |
| | | fundPersonal: parseNum(e.fundPersonal), |
| | | otherDeduct: parseNum(e.otherDeduct), |
| | | socialSupplementAmount: parseNum(e.socialSupplementAmount), |
| | | salaryTax: parseNum(e.salaryTax), |
| | | grossSalary: parseNum(e.grossSalary), |
| | | deductSalary: parseNum(e.deductSalary), |
| | | netSalary: parseNum(e.netSalary), |
| | | remark: e.remark ?? "", |
| | | })), |
| | | }; |
| | | if (props.operationType === "add") { |
| | | staffSalaryMainAdd({ ...payload, status }).then(() => { |
| | | proxy.$modal.msgSuccess(status === 1 ? "è稿ä¿åæå" : "æäº¤æå"); |
| | | closeDia(); |
| | | }); |
| | | } else { |
| | | staffSalaryMainUpdate({ ...payload, status }).then(() => { |
| | | proxy.$modal.msgSuccess(status === 1 ? "è稿ä¿åæå" : "æäº¤æå"); |
| | | closeDia(); |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | const closeDia = () => { |
| | | resetRecalcState(); |
| | | dialogVisible.value = false; |
| | | emit("close"); |
| | | }; |
| | | |
| | | defineExpose({ openDialog }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .form-dia-body { |
| | | padding: 0; |
| | | } |
| | | .card-title-line { |
| | | color: #f56c6c; |
| | | margin-right: 4px; |
| | | } |
| | | .form-card { |
| | | margin-bottom: 16px; |
| | | } |
| | | .form-card :deep(.el-card__header) { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 12px 16px; |
| | | } |
| | | .card-title { |
| | | font-weight: 500; |
| | | } |
| | | .card-collapse { |
| | | color: #999; |
| | | cursor: pointer; |
| | | } |
| | | .toolbar { |
| | | margin-bottom: 16px; |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 10px; |
| | | } |
| | | .employee-table-wrap { |
| | | position: relative; |
| | | min-height: 120px; |
| | | } |
| | | .table-empty { |
| | | text-align: center; |
| | | padding: 24px; |
| | | color: #999; |
| | | font-size: 14px; |
| | | } |
| | | .add-person-tree { |
| | | max-height: 360px; |
| | | overflow-y: auto; |
| | | padding: 8px 0; |
| | | } |
| | | .tax-desc { |
| | | margin-bottom: 12px; |
| | | font-size: 14px; |
| | | color: #606266; |
| | | } |
| | | .dialog-footer { |
| | | text-align: right; |
| | | } |
| | | .salary-total { |
| | | margin-top: 16px; |
| | | padding: 12px 16px; |
| | | background-color: #f5f7fa; |
| | | border-radius: 4px; |
| | | text-align: right; |
| | | font-size: 16px; |
| | | } |
| | | .salary-total .total-label { |
| | | color: #606266; |
| | | margin-right: 8px; |
| | | } |
| | | .salary-total .total-value { |
| | | color: #f56c6c; |
| | | font-weight: bold; |
| | | font-size: 18px; |
| | | } |
| | | .form-dia-body { |
| | | padding: 0; |
| | | } |
| | | .card-title-line { |
| | | color: #f56c6c; |
| | | margin-right: 4px; |
| | | } |
| | | .form-card { |
| | | margin-bottom: 16px; |
| | | } |
| | | .form-card :deep(.el-card__header) { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 12px 16px; |
| | | } |
| | | .card-title { |
| | | font-weight: 500; |
| | | } |
| | | .card-collapse { |
| | | color: #999; |
| | | cursor: pointer; |
| | | } |
| | | .toolbar { |
| | | margin-bottom: 16px; |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 10px; |
| | | } |
| | | .employee-table-wrap { |
| | | position: relative; |
| | | min-height: 120px; |
| | | } |
| | | .table-empty { |
| | | text-align: center; |
| | | padding: 24px; |
| | | color: #999; |
| | | font-size: 14px; |
| | | } |
| | | .add-person-tree { |
| | | max-height: 360px; |
| | | overflow-y: auto; |
| | | padding: 8px 0; |
| | | } |
| | | .tax-desc { |
| | | margin-bottom: 12px; |
| | | font-size: 14px; |
| | | color: #606266; |
| | | } |
| | | .dialog-footer { |
| | | text-align: right; |
| | | } |
| | | .salary-total { |
| | | margin-top: 16px; |
| | | padding: 12px 16px; |
| | | background-color: #f5f7fa; |
| | | border-radius: 4px; |
| | | text-align: right; |
| | | font-size: 16px; |
| | | } |
| | | .salary-total .total-label { |
| | | color: #606266; |
| | | margin-right: 8px; |
| | | } |
| | | .salary-total .total-value { |
| | | color: #f56c6c; |
| | | font-weight: bold; |
| | | font-size: 18px; |
| | | } |
| | | </style> |
| | |
| | | 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" |
| | | ); |
| | | }; |
| | |
| | | .table_list { |
| | | margin-top: 20px; |
| | | } |
| | | </style> |
| | | </style> |
| | |
| | | <template> |
| | | <div> |
| | | <FormDialog |
| | | v-model="dialogFormVisible" |
| | | :operation-type="operationType" |
| | | :title="dialogTitle" |
| | | width="80%" |
| | | @close="closeDia" |
| | | @confirm="submitForm" |
| | | @cancel="closeDia" |
| | | > |
| | | <el-form ref="formRef" :model="form" :rules="rules" label-position="top"> |
| | | <FormDialog v-model="dialogFormVisible" |
| | | :operation-type="operationType" |
| | | :title="dialogTitle" |
| | | width="80%" |
| | | @close="closeDia" |
| | | @confirm="submitForm" |
| | | @cancel="closeDia"> |
| | | <el-form ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-position="top"> |
| | | <el-row :gutter="24"> |
| | | <!-- 左侧ï¼éç¨äººå --> |
| | | <el-col :span="8"> |
| | | <el-form-item label="éç¨äººåï¼" prop="deptIds"> |
| | | <el-form-item label="éç¨äººåï¼" |
| | | prop="deptIds"> |
| | | <div class="dept-checkbox-wrap"> |
| | | <el-checkbox-group |
| | | v-model="form.deptIds" |
| | | :disabled="isDetail" |
| | | > |
| | | <div |
| | | v-for="dept in deptList" |
| | | :key="dept.deptId" |
| | | class="dept-checkbox-item" |
| | | > |
| | | <el-checkbox-group v-model="form.deptIds" |
| | | :disabled="isDetail"> |
| | | <div v-for="dept in deptList" |
| | | :key="dept.deptId" |
| | | class="dept-checkbox-item"> |
| | | <el-checkbox :value="dept.deptId"> |
| | | {{ dept.deptName }} |
| | | <span v-if="dept.personCount != null" class="dept-count" |
| | | >{{ dept.personCount }}人</span |
| | | > |
| | | <span v-if="dept.personCount != null" |
| | | class="dept-count">{{ dept.personCount }}人</span> |
| | | </el-checkbox> |
| | | </div> |
| | | </el-checkbox-group> |
| | |
| | | <!-- å³ä¾§ï¼åºç¡ä¿¡æ¯ + ä¿é©ç±»å --> |
| | | <el-col :span="16"> |
| | | <!-- åºç¡ä¿¡æ¯ --> |
| | | <el-card class="form-card" shadow="never"> |
| | | <el-card class="form-card" |
| | | shadow="never"> |
| | | <template #header> |
| | | <span class="card-title"><span class="card-title-line">|</span> åºç¡ä¿¡æ¯</span> |
| | | <el-icon class="card-collapse"><ArrowUp /></el-icon> |
| | | <el-icon class="card-collapse"> |
| | | <ArrowUp /> |
| | | </el-icon> |
| | | </template> |
| | | <el-form-item label="æ¹æ¡æ é¢ï¼" prop="title"> |
| | | <el-input |
| | | v-model="form.title" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | :disabled="isDetail" |
| | | /> |
| | | <el-form-item label="æ¹æ¡æ é¢ï¼" |
| | | prop="title"> |
| | | <el-input v-model="form.title" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | :disabled="isDetail" /> |
| | | </el-form-item> |
| | | <el-form-item label="夿³¨ï¼" prop="remark"> |
| | | <el-input |
| | | v-model="form.remark" |
| | | type="textarea" |
| | | :rows="2" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | :disabled="isDetail" |
| | | /> |
| | | <el-form-item label="夿³¨ï¼" |
| | | prop="remark"> |
| | | <el-input v-model="form.remark" |
| | | type="textarea" |
| | | :rows="2" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | :disabled="isDetail" /> |
| | | </el-form-item> |
| | | </el-card> |
| | | |
| | | <!-- ä¿é©ç±»å --> |
| | | <el-card class="form-card" shadow="never"> |
| | | <el-card class="form-card" |
| | | shadow="never"> |
| | | <template #header> |
| | | <span class="card-title"><span class="card-title-line">|</span> ä¿é©ç±»å</span> |
| | | <el-button |
| | | v-if="!isDetail" |
| | | type="primary" |
| | | size="small" |
| | | @click="addInsuranceBenefit" |
| | | > |
| | | <el-button v-if="!isDetail" |
| | | type="primary" |
| | | size="small" |
| | | @click="addInsuranceBenefit"> |
| | | æ·»å ä¿é©ç¦å© |
| | | </el-button> |
| | | </template> |
| | | <el-row :gutter="16"> |
| | | <el-col |
| | | v-for="(item, index) in form.insuranceBenefits" |
| | | :key="item._key" |
| | | :span="12" |
| | | > |
| | | <el-col v-for="(item, index) in form.insuranceBenefits" |
| | | :key="item._key" |
| | | :span="12"> |
| | | <div class="insurance-benefit-card"> |
| | | <div class="insurance-benefit-title"> |
| | | ä¿é©ç¦å©{{ index + 1 }} |
| | | <el-button |
| | | v-if="!isDetail && form.insuranceBenefits.length > 1" |
| | | type="danger" |
| | | link |
| | | size="small" |
| | | class="card-delete-btn" |
| | | @click="removeInsuranceBenefit(index)" |
| | | > |
| | | <el-button v-if="!isDetail && form.insuranceBenefits.length > 1" |
| | | type="danger" |
| | | link |
| | | size="small" |
| | | class="card-delete-btn" |
| | | @click="removeInsuranceBenefit(index)"> |
| | | å é¤ |
| | | </el-button> |
| | | </div> |
| | | <el-form-item |
| | | :prop="'insuranceBenefits.' + index + '.insuranceType'" |
| | | label="ä¿é©ç±»åï¼" |
| | | label-width="100px" |
| | | <el-form-item :prop="'insuranceBenefits.' + index + '.insuranceType'" |
| | | label="ä¿é©ç±»åï¼" |
| | | label-width="100px" |
| | | :rules="[{ required: true, message: 'è¯·éæ©ä¿é©ç±»å', trigger: 'blur' }]" |
| | | > |
| | | <el-select |
| | | v-model="item.insuranceType" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | style="width: 100%" |
| | | :disabled="isDetail" |
| | | > |
| | | <el-option |
| | | v-for="opt in insuranceTypeOptions" |
| | | :key="opt.value" |
| | | :label="opt.label" |
| | | :value="opt.value" |
| | | /> |
| | | <el-select v-model="item.insuranceType" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | style="width: 100%" |
| | | :disabled="isDetail"> |
| | | <el-option v-for="opt in insuranceTypeOptions" |
| | | :key="opt.value" |
| | | :label="opt.label" |
| | | :value="opt.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="ç¼´è´¹åºæ°ï¼" label-width="100px"> |
| | | <el-form-item label="ç¼´è´¹åºæ°ï¼" |
| | | label-width="100px" |
| | | :prop="'insuranceBenefits.' + index + '.paymentBase'" |
| | | :rules="[{ validator: validatePaymentBase, trigger: 'blur' }]"> |
| | | <div class="base-salary-wrap"> |
| | | <el-input |
| | | v-model="item.paymentBase" |
| | | placeholder="æ ¹æ®åºæ¬å·¥èµç¼´çº³" |
| | | clearable |
| | | style="width: 120px" |
| | | type="number" |
| | | :disabled="isDetail || item.useBasicSalary" |
| | | @input="handlePaymentBaseInput(item)" |
| | | /> |
| | | <el-checkbox |
| | | v-model="item.useBasicSalary" |
| | | @change="handleUseBasicSalaryChange(item)" |
| | | :disabled="isDetail" |
| | | > |
| | | <el-input v-model="item.paymentBase" |
| | | placeholder="æ ¹æ®åºæ¬å·¥èµç¼´çº³" |
| | | clearable |
| | | style="width: 120px" |
| | | type="number" |
| | | :disabled="isDetail || item.useBasicSalary" |
| | | @input="handlePaymentBaseInput(item)" /> |
| | | <el-checkbox v-model="item.useBasicSalary" |
| | | @change="(val) => handleUseBasicSalaryChange(item, index)" |
| | | :disabled="isDetail"> |
| | | è°ç¨åºæ¬å·¥èµ |
| | | </el-checkbox> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="个人缴费æ¯ä¾ï¼" label-width="100px"> |
| | | <el-form-item label="个人缴费æ¯ä¾ï¼" |
| | | label-width="100px" |
| | | :prop="'insuranceBenefits.' + index + '.personalRatio'" |
| | | :rules="[{ required: true, message: '请è¾å
¥ä¸ªäººç¼´è´¹æ¯ä¾', trigger: 'blur' }]"> |
| | | <div class="personal-ratio-wrap"> |
| | | <el-input |
| | | v-model="item.personalRatio" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | style="width: 100px" |
| | | type="number" |
| | | :disabled="isDetail" |
| | | /> |
| | | <el-input v-model="item.personalRatio" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | style="width: 100px" |
| | | type="number" |
| | | :disabled="isDetail" |
| | | :min="0" |
| | | @input="handlePersonalRatioInput(item)" /> |
| | | <span class="ratio-unit">(%)</span> |
| | | <span class="ratio-plus">+</span> |
| | | <el-input |
| | | v-model="item.personalFixed" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | style="width: 100px" |
| | | type="number" |
| | | :disabled="isDetail" |
| | | /> |
| | | <el-input v-model="item.personalFixed" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | style="width: 100px" |
| | | type="number" |
| | | :disabled="isDetail" |
| | | :min="0" |
| | | @input="handlePersonalFixedInput(item)" /> |
| | | </div> |
| | | </el-form-item> |
| | | </div> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, toRefs, getCurrentInstance, nextTick, computed } from "vue"; |
| | | import FormDialog from "@/components/Dialog/FormDialog.vue"; |
| | | import { ArrowUp } from "@element-plus/icons-vue"; |
| | | import { listDept } from "@/api/system/dept.js"; |
| | | import { socialSecurityAdd, socialSecurityUpdate } from "@/api/personnelManagement/socialSecuritySet.js"; |
| | | import { |
| | | ref, |
| | | reactive, |
| | | toRefs, |
| | | getCurrentInstance, |
| | | nextTick, |
| | | computed, |
| | | } from "vue"; |
| | | import FormDialog from "@/components/Dialog/FormDialog.vue"; |
| | | import { ArrowUp } from "@element-plus/icons-vue"; |
| | | import { listDept } from "@/api/system/dept.js"; |
| | | import { |
| | | socialSecurityAdd, |
| | | socialSecurityUpdate, |
| | | } from "@/api/personnelManagement/socialSecuritySet.js"; |
| | | |
| | | const emit = defineEmits(["close"]); |
| | | const { proxy } = getCurrentInstance(); |
| | | const emit = defineEmits(["close"]); |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | const dialogFormVisible = ref(false); |
| | | const operationType = ref("add"); |
| | | const formRef = ref(null); |
| | | const deptList = ref([]); |
| | | const dialogFormVisible = ref(false); |
| | | const operationType = ref("add"); |
| | | const formRef = ref(null); |
| | | const deptList = ref([]); |
| | | |
| | | const isDetail = computed(() => operationType.value === "detail"); |
| | | const isDetail = computed(() => operationType.value === "detail"); |
| | | |
| | | const dialogTitle = () => |
| | | operationType.value === "add" |
| | | ? "æ°å¢æ¹æ¡" |
| | | : operationType.value === "edit" |
| | | ? "ç¼è¾æ¹æ¡" |
| | | : "æ¹æ¡è¯¦æ
"; |
| | | const dialogTitle = () => |
| | | operationType.value === "add" |
| | | ? "æ°å¢æ¹æ¡" |
| | | : operationType.value === "edit" |
| | | ? "ç¼è¾æ¹æ¡" |
| | | : "æ¹æ¡è¯¦æ
"; |
| | | |
| | | // ä¿é©ç±»åé项ï¼å¯æåå
¸æ¿æ¢ï¼ |
| | | const insuranceTypeOptions = [ |
| | | { label: "å
»èä¿é©", value: "å
»èä¿é©" }, |
| | | { label: "å»çä¿é©", value: "å»çä¿é©" }, |
| | | { label: "失ä¸ä¿é©", value: "失ä¸ä¿é©" }, |
| | | { label: "工伤ä¿é©", value: "工伤ä¿é©" }, |
| | | { label: "çè²ä¿é©", value: "çè²ä¿é©" }, |
| | | { label: "å
¬ç§¯é", value: "å
¬ç§¯é" }, |
| | | ]; |
| | | // ä¿é©ç±»åé项ï¼å¯æåå
¸æ¿æ¢ï¼ |
| | | const insuranceTypeOptions = [ |
| | | { label: "å
»èä¿é©", value: "å
»èä¿é©" }, |
| | | { label: "å»çä¿é©", value: "å»çä¿é©" }, |
| | | { label: "失ä¸ä¿é©", value: "失ä¸ä¿é©" }, |
| | | { label: "工伤ä¿é©", value: "工伤ä¿é©" }, |
| | | { label: "çè²ä¿é©", value: "çè²ä¿é©" }, |
| | | { label: "å
¬ç§¯é", value: "å
¬ç§¯é" }, |
| | | ]; |
| | | |
| | | const defaultBenefit = () => ({ |
| | | _key: Math.random().toString(36).slice(2), |
| | | insuranceType: "", |
| | | paymentBase: "", |
| | | useBasicSalary: false, |
| | | personalRatio: "", |
| | | personalFixed: "", |
| | | }); |
| | | const defaultBenefit = () => ({ |
| | | _key: Math.random().toString(36).slice(2), |
| | | insuranceType: "", |
| | | paymentBase: "", |
| | | useBasicSalary: false, |
| | | personalRatio: "", |
| | | personalFixed: "", |
| | | }); |
| | | |
| | | const data = reactive({ |
| | | form: { |
| | | id: undefined, |
| | | title: "", |
| | | remark: "", |
| | | deptIds: [], |
| | | insuranceBenefits: [defaultBenefit()], |
| | | }, |
| | | rules: { |
| | | title: [{ required: true, message: "请è¾å
¥æ¹æ¡æ é¢", trigger: "blur" }], |
| | | deptIds: [ |
| | | { |
| | | required: true, |
| | | type: "array", |
| | | min: 1, |
| | | message: "请è³å°éæ©ä¸ä¸ªéç¨é¨é¨", |
| | | trigger: "change", |
| | | }, |
| | | ], |
| | | }, |
| | | }); |
| | | const { form, rules } = toRefs(data); |
| | | const data = reactive({ |
| | | form: { |
| | | id: undefined, |
| | | title: "", |
| | | remark: "", |
| | | deptIds: [], |
| | | insuranceBenefits: [defaultBenefit()], |
| | | }, |
| | | rules: { |
| | | title: [{ required: true, message: "请è¾å
¥æ¹æ¡æ é¢", trigger: "blur" }], |
| | | deptIds: [ |
| | | { |
| | | required: true, |
| | | type: "array", |
| | | min: 1, |
| | | message: "请è³å°éæ©ä¸ä¸ªéç¨é¨é¨", |
| | | trigger: "change", |
| | | }, |
| | | ], |
| | | }, |
| | | }); |
| | | const { form, rules } = toRefs(data); |
| | | |
| | | function flattenDept(tree, list = []) { |
| | | if (!tree || !tree.length) return list; |
| | | tree.forEach((node) => { |
| | | list.push({ |
| | | deptId: node.deptId, |
| | | deptName: node.deptName, |
| | | personCount: node.personCount ?? null, |
| | | function flattenDept(tree, list = []) { |
| | | if (!tree || !tree.length) return list; |
| | | tree.forEach(node => { |
| | | list.push({ |
| | | deptId: node.deptId, |
| | | deptName: node.deptName, |
| | | personCount: node.personCount ?? null, |
| | | }); |
| | | if (node.children && node.children.length) { |
| | | flattenDept(node.children, list); |
| | | } |
| | | }); |
| | | if (node.children && node.children.length) { |
| | | flattenDept(node.children, list); |
| | | } |
| | | }); |
| | | return list; |
| | | } |
| | | |
| | | const loadDeptList = () => { |
| | | listDept().then((res) => { |
| | | const tree = res.data ?? []; |
| | | deptList.value = flattenDept(tree); |
| | | }); |
| | | }; |
| | | |
| | | const addInsuranceBenefit = () => { |
| | | form.value.insuranceBenefits.push(defaultBenefit()); |
| | | }; |
| | | |
| | | const removeInsuranceBenefit = (index) => { |
| | | form.value.insuranceBenefits.splice(index, 1); |
| | | }; |
| | | |
| | | const handleUseBasicSalaryChange = (item) => { |
| | | if (item.useBasicSalary) { |
| | | item.paymentBase = ""; |
| | | return list; |
| | | } |
| | | }; |
| | | |
| | | const handlePaymentBaseInput = (item) => { |
| | | if (item.paymentBase !== "" && item.paymentBase != null) { |
| | | item.useBasicSalary = false; |
| | | } |
| | | }; |
| | | |
| | | const resetForm = () => { |
| | | form.value = { |
| | | id: undefined, |
| | | title: "", |
| | | remark: "", |
| | | deptIds: [], |
| | | insuranceBenefits: [defaultBenefit()], |
| | | const loadDeptList = () => { |
| | | listDept().then(res => { |
| | | const tree = res.data ?? []; |
| | | deptList.value = flattenDept(tree); |
| | | }); |
| | | }; |
| | | }; |
| | | |
| | | const openDialog = (type, row) => { |
| | | operationType.value = type; |
| | | dialogFormVisible.value = true; |
| | | loadDeptList(); |
| | | resetForm(); |
| | | if ((type === "edit" || type === "detail") && row) { |
| | | const d = row || {}; |
| | | form.value.id = d.id; |
| | | form.value.title = d.title; |
| | | form.value.remark = d.remark ?? ""; |
| | | // deptIds å端å¯è½æ¯éå·åéåç¬¦ä¸²ææ°ç»ï¼è¿éç»ä¸è½¬ä¸ºæ°ç»å¹¶å°½éè¿åæ°å¼ç±»å |
| | | if (d.deptIds) { |
| | | form.value.deptIds = String(d.deptIds) |
| | | .split(",") |
| | | .filter((v) => v !== "") |
| | | .map((v) => { |
| | | const num = Number(v); |
| | | return Number.isNaN(num) ? v : num; |
| | | }); |
| | | } else { |
| | | form.value.deptIds = []; |
| | | const addInsuranceBenefit = () => { |
| | | form.value.insuranceBenefits.push(defaultBenefit()); |
| | | }; |
| | | |
| | | const removeInsuranceBenefit = index => { |
| | | form.value.insuranceBenefits.splice(index, 1); |
| | | }; |
| | | |
| | | const handleUseBasicSalaryChange = (item, index) => { |
| | | if (item.useBasicSalary) { |
| | | item.paymentBase = ""; |
| | | } |
| | | const detailList = d.schemeInsuranceDetailList || []; |
| | | form.value.insuranceBenefits = |
| | | detailList.length > 0 |
| | | ? detailList.map((b) => ({ |
| | | _key: Math.random().toString(36).slice(2), |
| | | insuranceType: b.insuranceType || "", |
| | | paymentBase: b.paymentBase ?? "", |
| | | useBasicSalary: b.useBasicSalary === 2, |
| | | personalRatio: b.personalRatio ?? "", |
| | | personalFixed: b.personalFixed ?? "", |
| | | })) |
| | | : [defaultBenefit()]; |
| | | } |
| | | }; |
| | | nextTick(() => { |
| | | formRef.value?.validateField(`insuranceBenefits.${index}.paymentBase`); |
| | | }); |
| | | }; |
| | | |
| | | const submitForm = () => { |
| | | // 详æ
模å¼ä¸ä¸æäº¤ï¼åªå
³éå¼¹çª |
| | | if (operationType.value === "detail") { |
| | | closeDia(); |
| | | return; |
| | | } |
| | | formRef.value?.validate((valid) => { |
| | | if (!valid) return; |
| | | const deptIds = |
| | | Array.isArray(form.value.deptIds) && form.value.deptIds.length |
| | | ? form.value.deptIds.join(",") |
| | | : ""; |
| | | const schemeInsuranceDetailList = (form.value.insuranceBenefits || []).map( |
| | | ({ _key, ...rest }) => ({ |
| | | ...rest, |
| | | useBasicSalary: rest.useBasicSalary ? 2 : 1, |
| | | }) |
| | | ); |
| | | const insuranceTypes = schemeInsuranceDetailList |
| | | .map((item) => item.insuranceType) |
| | | .filter((v) => v) |
| | | .join(","); |
| | | // é¨é¨åç§°ï¼å¤ä¸ªä½¿ç¨éå·éå¼ï¼æ ¹æ®éä¸ç deptIds ä¸ deptList 计ç®ï¼ |
| | | const deptNames = (deptList.value || []) |
| | | .filter((d) => |
| | | (form.value.deptIds || []).some( |
| | | (id) => String(id) === String(d.deptId) |
| | | ) |
| | | ) |
| | | .map((d) => d.deptName) |
| | | .join(","); |
| | | const submitData = { |
| | | id: form.value.id, |
| | | title: form.value.title, |
| | | remark: form.value.remark ?? "", |
| | | deptIds, |
| | | insuranceTypes, |
| | | deptNames, |
| | | schemeInsuranceDetailList, |
| | | const handlePaymentBaseInput = item => { |
| | | if (item.paymentBase !== "" && item.paymentBase != null) { |
| | | item.useBasicSalary = false; |
| | | } |
| | | }; |
| | | |
| | | const handlePersonalRatioInput = item => { |
| | | if (item.personalRatio !== "" && item.personalRatio != null) { |
| | | const value = Number(item.personalRatio); |
| | | if (value < 0) { |
| | | item.personalRatio = ""; |
| | | } |
| | | } |
| | | }; |
| | | |
| | | const handlePersonalFixedInput = item => { |
| | | if (item.personalFixed !== "" && item.personalFixed != null) { |
| | | const value = Number(item.personalFixed); |
| | | if (value < 0) { |
| | | item.personalFixed = ""; |
| | | } |
| | | } |
| | | }; |
| | | |
| | | const validatePaymentBase = (rule, value, callback) => { |
| | | const match = rule.field.match(/insuranceBenefits\.(\d+)\.paymentBase/); |
| | | if (match) { |
| | | const index = parseInt(match[1], 10); |
| | | const item = form.value.insuranceBenefits[index]; |
| | | if (item && !item.useBasicSalary && (!value || value === "")) { |
| | | callback(new Error("请è¾å
¥ç¼´è´¹åºæ°")); |
| | | } else { |
| | | callback(); |
| | | } |
| | | } else { |
| | | callback(); |
| | | } |
| | | }; |
| | | |
| | | const resetForm = () => { |
| | | form.value = { |
| | | id: undefined, |
| | | title: "", |
| | | remark: "", |
| | | deptIds: [], |
| | | insuranceBenefits: [defaultBenefit()], |
| | | }; |
| | | if (operationType.value === "add") { |
| | | socialSecurityAdd(submitData).then(() => { |
| | | proxy.$modal.msgSuccess("æ°å¢æå"); |
| | | closeDia(); |
| | | }); |
| | | } else { |
| | | socialSecurityUpdate(submitData).then(() => { |
| | | proxy.$modal.msgSuccess("ä¿®æ¹æå"); |
| | | closeDia(); |
| | | }); |
| | | }; |
| | | |
| | | const openDialog = (type, row) => { |
| | | operationType.value = type; |
| | | dialogFormVisible.value = true; |
| | | loadDeptList(); |
| | | resetForm(); |
| | | if ((type === "edit" || type === "detail") && row) { |
| | | const d = row || {}; |
| | | form.value.id = d.id; |
| | | form.value.title = d.title; |
| | | form.value.remark = d.remark ?? ""; |
| | | // deptIds å端å¯è½æ¯éå·åéåç¬¦ä¸²ææ°ç»ï¼è¿éç»ä¸è½¬ä¸ºæ°ç»å¹¶å°½éè¿åæ°å¼ç±»å |
| | | if (d.deptIds) { |
| | | form.value.deptIds = String(d.deptIds) |
| | | .split(",") |
| | | .filter(v => v !== "") |
| | | .map(v => { |
| | | const num = Number(v); |
| | | return Number.isNaN(num) ? v : num; |
| | | }); |
| | | } else { |
| | | form.value.deptIds = []; |
| | | } |
| | | const detailList = d.schemeInsuranceDetailList || []; |
| | | form.value.insuranceBenefits = |
| | | detailList.length > 0 |
| | | ? detailList.map(b => ({ |
| | | _key: Math.random().toString(36).slice(2), |
| | | insuranceType: b.insuranceType || "", |
| | | paymentBase: b.paymentBase ?? "", |
| | | useBasicSalary: b.useBasicSalary === 2, |
| | | personalRatio: b.personalRatio ?? "", |
| | | personalFixed: b.personalFixed ?? "", |
| | | })) |
| | | : [defaultBenefit()]; |
| | | } |
| | | }); |
| | | }; |
| | | }; |
| | | |
| | | const closeDia = () => { |
| | | proxy.resetForm?.("formRef"); |
| | | dialogFormVisible.value = false; |
| | | emit("close"); |
| | | }; |
| | | const submitForm = () => { |
| | | // 详æ
模å¼ä¸ä¸æäº¤ï¼åªå
³éå¼¹çª |
| | | if (operationType.value === "detail") { |
| | | closeDia(); |
| | | return; |
| | | } |
| | | formRef.value?.validate(valid => { |
| | | if (!valid) return; |
| | | const deptIds = |
| | | Array.isArray(form.value.deptIds) && form.value.deptIds.length |
| | | ? form.value.deptIds.join(",") |
| | | : ""; |
| | | const schemeInsuranceDetailList = (form.value.insuranceBenefits || []).map( |
| | | ({ _key, ...rest }) => ({ |
| | | ...rest, |
| | | useBasicSalary: rest.useBasicSalary ? 2 : 1, |
| | | }) |
| | | ); |
| | | const insuranceTypes = schemeInsuranceDetailList |
| | | .map(item => item.insuranceType) |
| | | .filter(v => v) |
| | | .join(","); |
| | | // é¨é¨åç§°ï¼å¤ä¸ªä½¿ç¨éå·éå¼ï¼æ ¹æ®éä¸ç deptIds ä¸ deptList 计ç®ï¼ |
| | | const deptNames = (deptList.value || []) |
| | | .filter(d => |
| | | (form.value.deptIds || []).some(id => String(id) === String(d.deptId)) |
| | | ) |
| | | .map(d => d.deptName) |
| | | .join(","); |
| | | const submitData = { |
| | | id: form.value.id, |
| | | title: form.value.title, |
| | | remark: form.value.remark ?? "", |
| | | deptIds, |
| | | insuranceTypes, |
| | | deptNames, |
| | | schemeInsuranceDetailList, |
| | | }; |
| | | if (operationType.value === "add") { |
| | | socialSecurityAdd(submitData).then(() => { |
| | | proxy.$modal.msgSuccess("æ°å¢æå"); |
| | | closeDia(); |
| | | }); |
| | | } else { |
| | | socialSecurityUpdate(submitData).then(() => { |
| | | proxy.$modal.msgSuccess("ä¿®æ¹æå"); |
| | | closeDia(); |
| | | }); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | defineExpose({ openDialog }); |
| | | const closeDia = () => { |
| | | proxy.resetForm?.("formRef"); |
| | | dialogFormVisible.value = false; |
| | | emit("close"); |
| | | }; |
| | | |
| | | defineExpose({ openDialog }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .card-title-line { |
| | | color: #f56c6c; |
| | | margin-right: 4px; |
| | | } |
| | | .form-card { |
| | | margin-bottom: 16px; |
| | | } |
| | | .form-card :deep(.el-card__header) { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 12px 16px; |
| | | } |
| | | .card-title { |
| | | font-weight: 500; |
| | | } |
| | | .card-collapse { |
| | | color: #999; |
| | | cursor: pointer; |
| | | } |
| | | .dept-checkbox-wrap { |
| | | max-height: 320px; |
| | | overflow-y: auto; |
| | | padding: 8px 0; |
| | | border: 1px solid var(--el-border-color); |
| | | border-radius: 4px; |
| | | background: #fff; |
| | | } |
| | | .dept-checkbox-item { |
| | | padding: 6px 12px; |
| | | } |
| | | .dept-count { |
| | | color: #909399; |
| | | font-size: 12px; |
| | | margin-left: 4px; |
| | | } |
| | | .insurance-benefit-card { |
| | | border: 1px solid var(--el-border-color-lighter); |
| | | border-radius: 4px; |
| | | padding: 12px 16px; |
| | | margin-bottom: 12px; |
| | | background: #fafafa; |
| | | } |
| | | .insurance-benefit-title { |
| | | font-size: 14px; |
| | | margin-bottom: 12px; |
| | | font-weight: 500; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | } |
| | | .card-delete-btn { |
| | | margin-left: auto; |
| | | } |
| | | .checkbox-group-inline { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 16px; |
| | | } |
| | | .base-salary-wrap { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | .base-salary-text { |
| | | color: #606266; |
| | | font-size: 14px; |
| | | } |
| | | .personal-ratio-wrap { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | .ratio-unit, |
| | | .ratio-plus { |
| | | color: #606266; |
| | | font-size: 14px; |
| | | } |
| | | </style> |
| | | .card-title-line { |
| | | color: #f56c6c; |
| | | margin-right: 4px; |
| | | } |
| | | .form-card { |
| | | margin-bottom: 16px; |
| | | } |
| | | .form-card :deep(.el-card__header) { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 12px 16px; |
| | | } |
| | | .card-title { |
| | | font-weight: 500; |
| | | } |
| | | .card-collapse { |
| | | color: #999; |
| | | cursor: pointer; |
| | | } |
| | | .dept-checkbox-wrap { |
| | | max-height: 320px; |
| | | overflow-y: auto; |
| | | padding: 8px 0; |
| | | border: 1px solid var(--el-border-color); |
| | | border-radius: 4px; |
| | | background: #fff; |
| | | } |
| | | .dept-checkbox-item { |
| | | padding: 6px 12px; |
| | | } |
| | | .dept-count { |
| | | color: #909399; |
| | | font-size: 12px; |
| | | margin-left: 4px; |
| | | } |
| | | .insurance-benefit-card { |
| | | border: 1px solid var(--el-border-color-lighter); |
| | | border-radius: 4px; |
| | | padding: 12px 16px; |
| | | margin-bottom: 12px; |
| | | background: #fafafa; |
| | | } |
| | | .insurance-benefit-title { |
| | | font-size: 14px; |
| | | margin-bottom: 12px; |
| | | font-weight: 500; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | } |
| | | .card-delete-btn { |
| | | margin-left: auto; |
| | | } |
| | | .checkbox-group-inline { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 16px; |
| | | } |
| | | .base-salary-wrap { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | .base-salary-text { |
| | | color: #606266; |
| | | font-size: 14px; |
| | | } |
| | | .personal-ratio-wrap { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | .ratio-unit, |
| | | .ratio-plus { |
| | | color: #606266; |
| | | font-size: 14px; |
| | | } |
| | | </style> |
| | |
| | | </div> |
| | | <div class="table_list"> |
| | | <div style="display: flex;justify-content: flex-end;margin-bottom: 20px;"> |
| | | <el-button type="success" |
| | | plain |
| | | @click="handleBatchGenerate">æ¹éçææ°æ®</el-button> |
| | | <el-button type="primary" |
| | | @click="openForm('add')">æ°å¢å°è´¦</el-button> |
| | | <el-button type="primary" |
| | |
| | | prop="specificationModel" /> |
| | | <el-table-column label="åä½" |
| | | prop="unit" /> |
| | | <el-table-column label="å
¥åºå®¡æ ¸ç¶æ" |
| | | <el-table-column label="å
¥åºå®¡æ ¸ç¶æ" |
| | | prop="stockInApprovalStatus" |
| | | width="120"> |
| | | <template #default="scope"> |
| | |
| | | <el-table-column label="å¯ç¨æ°é" |
| | | prop="availableQuality" /> |
| | | <el-table-column label="éè´§æ°é" |
| | | prop="returnQuality" /> |
| | | prop="returnQuantity" /> |
| | | <el-table-column label="ç¨ç(%)" |
| | | prop="taxRate" /> |
| | | <el-table-column label="å«ç¨åä»·(å
)" |
| | |
| | | getOptions, |
| | | getPurchaseTemplateList, |
| | | delPurchaseTemplate, |
| | | batchGeneratePurchaseInboundSteps, |
| | | } from "@/api/procurementManagement/procurementLedger.js"; |
| | | import useFormData from "@/hooks/useFormData.js"; |
| | | const FileList = defineAsyncComponent(() => |
| | |
| | | // è·åå
¥åºç¶ææ ç¾ç±»å |
| | | const getStockInStatusType = status => { |
| | | const typeMap = { |
| | | "å¾
å
¥åº": "info", // å¾
å
¥åº - ç°è² |
| | | "å
¥åºä¸": "warning", // å
¥åºä¸ - æ©è² |
| | | "å®å
¨å
¥åº": "success", // å®å
¨å
¥åº - ç»¿è² |
| | | å¾
å
¥åº: "info", // å¾
å
¥åº - ç°è² |
| | | å
¥åºä¸: "warning", // å
¥åºä¸ - æ©è² |
| | | å®å
¨å
¥åº: "success", // å®å
¨å
¥åº - ç»¿è² |
| | | }; |
| | | return typeMap[status] || ""; |
| | | }; |
| | |
| | | // è·åå
¥åºå®¡æ ¸ç¶ææ ç¾ç±»å |
| | | const getStockInApprovalStatusType = status => { |
| | | const typeMap = { |
| | | "å¾
å
¥åº": "info", // å¾
å
¥åº - ç°è² |
| | | "å
¥åºä¸": "warning", // å
¥åºä¸ - æ©è² |
| | | "å®å
¨å
¥åº": "success", // å®å
¨å
¥åº - ç»¿è² |
| | | å¾
å
¥åº: "info", // å¾
å
¥åº - ç°è² |
| | | å
¥åºä¸: "warning", // å
¥åºä¸ - æ©è² |
| | | å®å
¨å
¥åº: "success", // å®å
¨å
¥åº - ç»¿è² |
| | | }; |
| | | return typeMap[status] || ""; |
| | | }; |
| | |
| | | const openForm = async (type, row) => { |
| | | // ç¼è¾æ¶æ£æ¥å
¥åºç¶æï¼å®å
¨å
¥åºæ¶ä¸è½ç¼è¾ |
| | | if (type === "edit" && row) { |
| | | if (row.stockInStatus === 'å®å
¨å
¥åº') { |
| | | if (row.stockInStatus === "å®å
¨å
¥åº") { |
| | | proxy.$modal.msgWarning("å®å
¨å
¥åºç¶æçè®°å½ä¸è½ç¼è¾"); |
| | | return; |
| | | } |
| | |
| | | form.value = { ...purchaseRes, stockInStatus: row.stockInStatus }; |
| | | fileList.value = purchaseRes.storageBlobVOS || []; |
| | | // ä½¿ç¨ productList æ¥å£è·å产åå表ï¼ä»¥è·åå
¥åºå®¡æ ¸ç¶æ |
| | | const productRes = await productList({ salesLedgerId: row.id, type: 2 }); |
| | | const productRes = await productList({ |
| | | salesLedgerId: row.id, |
| | | type: 2, |
| | | }); |
| | | productData.value = productRes.data || []; |
| | | } catch (error) { |
| | | console.error("å è½½éè´å°è´¦æ°æ®å¤±è´¥:", error); |
| | |
| | | // æå¼äº§åå¼¹æ¡ |
| | | const openProductForm = async (type, row, index) => { |
| | | // ç¼è¾æ¶æ£æ¥äº§åå
¥åºå®¡æ ¸ç¶æï¼å®å
¨å
¥åºæ¶ä¸è½ç¼è¾ |
| | | if (type === "edit" && row && row.stockInApprovalStatus === 'å®å
¨å
¥åº') { |
| | | if (type === "edit" && row && row.stockInApprovalStatus === "å®å
¨å
¥åº") { |
| | | proxy.$modal.msgWarning("å®å
¨å
¥åºç¶æç产åä¸è½ç¼è¾"); |
| | | return; |
| | | } |
| | |
| | | } |
| | | // æ£æ¥éä¸ç产å䏿¯å¦æå®å
¨å
¥åºç |
| | | const hasFullyStocked = productSelectedRows.value.some( |
| | | row => row.stockInApprovalStatus === 'å®å
¨å
¥åº' |
| | | row => row.stockInApprovalStatus === "å®å
¨å
¥åº" |
| | | ); |
| | | if (hasFullyStocked) { |
| | | proxy.$modal.msgWarning("éä¸ç产åä¸å
å«å®å
¨å
¥åºç产åï¼æ æ³å é¤"); |
| | |
| | | proxy.$modal.msgSuccess("å 餿å"); |
| | | getList(); |
| | | }); |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msg("已忶"); |
| | | }); |
| | | }; |
| | | |
| | | const handleBatchGenerate = async () => { |
| | | if (selectedRows.value.length === 0) { |
| | | proxy.$modal.msgWarning("è¯·éæ©æ°æ®"); |
| | | return; |
| | | } |
| | | const ids = selectedRows.value.map((item) => item.id); |
| | | |
| | | ElMessageBox.confirm("确认æ¹éçææ°æ®ï¼", "æ¹éçæ", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "info", |
| | | }) |
| | | .then(() => { |
| | | proxy.$modal.loading("æ£å¨æ¹éçææ°æ®ï¼è¯·ç¨å..."); |
| | | batchGeneratePurchaseInboundSteps({ ids }) |
| | | .then((res) => { |
| | | proxy.$modal.msgSuccess("æ¹éçææå"); |
| | | getList(); |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msgError("æ¹éçæå¤±è´¥"); |
| | | }) |
| | | .finally(() => { |
| | | proxy.$modal.closeLoading(); |
| | | }); |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msg("已忶"); |
| | |
| | | contractNo: form.value.salesLedgerId, |
| | | }); |
| | | if (code == 200) { |
| | | productData.value = data; |
| | | productData.value = data || []; |
| | | } |
| | | }; |
| | | |
| | |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å建æ¶é´" prop="createTime"> |
| | | <el-date-picker v-model="formData.createTime" |
| | | <el-date-picker v-model="formCreateTimeDate" |
| | | type="date" |
| | | placeholder="éæ©æ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | |
| | | |
| | | <script setup> |
| | | import FormDialog from "@/components/Dialog/FormDialog.vue"; |
| | | import { ref, reactive, onMounted, getCurrentInstance } from "vue"; |
| | | import { ref, reactive, onMounted, getCurrentInstance, computed } from "vue"; |
| | | import dayjs from "dayjs"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import { Search, Refresh, Plus, Download } from "@element-plus/icons-vue"; |
| | | import { |
| | |
| | | summaryMaterial: false, |
| | | summaryAuxAttributes: false, |
| | | summaryDemandDate: false, |
| | | }); |
| | | const formCreateTimeDate = computed({ |
| | | get: () => (formData.createTime ? String(formData.createTime).split(" ")[0] : ""), |
| | | set: (value) => { |
| | | formData.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : ""; |
| | | }, |
| | | }); |
| | | |
| | | // å½åæ¿æ´»çæ ç¾é¡µ |
| | |
| | | const handleAdd = () => { |
| | | dialogType.value = "add"; |
| | | resetForm(); |
| | | formData.createTime = new Date().toISOString().split("T")[0]; |
| | | formData.createTime = dayjs().format("YYYY-MM-DD HH:mm:ss"); |
| | | dialogVisible.value = true; |
| | | }; |
| | | |
| | |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="å建æ¶é´"> |
| | | <el-date-picker v-model="formData.createTime" |
| | | <el-date-picker v-model="formCreateTimeDate" |
| | | type="date" |
| | | placeholder="éæ©æ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | |
| | | |
| | | <script setup> |
| | | import FormDialog from '@/components/Dialog/FormDialog.vue'; |
| | | import { ref, reactive } from 'vue' |
| | | import { ref, reactive, computed } from 'vue' |
| | | import dayjs from 'dayjs' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | |
| | | const loading = ref(false) |
| | |
| | | supplierName: '', |
| | | remark: '', |
| | | createTime: '' |
| | | }) |
| | | const formCreateTimeDate = computed({ |
| | | get: () => (formData.createTime ? String(formData.createTime).split(' ')[0] : ''), |
| | | set: (value) => { |
| | | formData.createTime = value ? `${value} ${dayjs().format('HH:mm:ss')}` : '' |
| | | } |
| | | }) |
| | | |
| | | const mockData = [ |
| | |
| | | const openDialog = (type, row = {}) => { |
| | | dialogType.value = type |
| | | if (type === 'edit' && row.id) { |
| | | Object.assign(formData, { supplierName: row.supplierName, remark: row.remark }) |
| | | Object.assign(formData, { supplierName: row.supplierName, remark: row.remark, createTime: row.createTime || '' }) |
| | | } else { |
| | | Object.assign(formData, { supplierName: '', remark: '', createTime: new Date().toISOString().split('T')[0] }) |
| | | Object.assign(formData, { supplierName: '', remark: '', createTime: dayjs().format('YYYY-MM-DD HH:mm:ss') }) |
| | | } |
| | | dialogVisible.value = true |
| | | } |
| | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å建æ¶é´"> |
| | | <el-date-picker v-model="formData.createTime" |
| | | <el-date-picker v-model="formCreateTimeDate" |
| | | type="date" |
| | | placeholder="éæ©æ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | |
| | | |
| | | <script setup> |
| | | import FormDialog from '@/components/Dialog/FormDialog.vue'; |
| | | import { ref, reactive } from 'vue' |
| | | import { ref, reactive, computed } from 'vue' |
| | | import dayjs from 'dayjs' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | |
| | | const loading = ref(false) |
| | |
| | | inspector: '', |
| | | remark: '', |
| | | createTime: '' |
| | | }) |
| | | const formCreateTimeDate = computed({ |
| | | get: () => (formData.createTime ? String(formData.createTime).split(' ')[0] : ''), |
| | | set: (value) => { |
| | | formData.createTime = value ? `${value} ${dayjs().format('HH:mm:ss')}` : '' |
| | | } |
| | | }) |
| | | |
| | | const mockData = [ |
| | |
| | | arrivalNo: row.arrivalNo, |
| | | supplierName: row.supplierName, |
| | | inspector: row.inspector, |
| | | remark: row.remark |
| | | remark: row.remark, |
| | | createTime: row.createTime || '' |
| | | }) |
| | | } else { |
| | | Object.assign(formData, { |
| | |
| | | products: [], |
| | | inspector: '', |
| | | remark: '', |
| | | createTime: new Date().toISOString().split('T')[0] |
| | | createTime: dayjs().format('YYYY-MM-DD HH:mm:ss') |
| | | }) |
| | | } |
| | | dialogVisible.value = true |
| | |
| | | <span>{{ currentProduct.identifierType }}</span> |
| | | </el-form-item> |
| | | <el-form-item label="å建æ¶é´"> |
| | | <el-date-picker v-model="createTime" |
| | | <el-date-picker v-model="createTimeDate" |
| | | type="date" |
| | | placeholder="éæ©æ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | |
| | | |
| | | <script setup> |
| | | import { ref, reactive, computed } from "vue"; |
| | | import dayjs from "dayjs"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import { Plus, Search, Loading, Download } from "@element-plus/icons-vue"; |
| | | import Pagination from "@/components/PIMTable/Pagination.vue"; |
| | |
| | | const generateQuantity = ref(1); |
| | | const codeRule = ref(""); |
| | | const customPrefix = ref(""); |
| | | const createTime = ref(new Date().toISOString().split('T')[0]); |
| | | const createTime = ref(dayjs().format("YYYY-MM-DD HH:mm:ss")); |
| | | const createTimeDate = computed({ |
| | | get: () => (createTime.value ? String(createTime.value).split(" ")[0] : ""), |
| | | set: (value) => { |
| | | createTime.value = value ? `${value} ${dayjs().format("HH:mm:ss")}` : ""; |
| | | }, |
| | | }); |
| | | const newBatchNo = ref(""); |
| | | const reassignReason = ref(""); |
| | | const formRef = ref(); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| | |
| | | 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 åæ®µé¢è²ï¼çº¢/æ©/è/绿 |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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); |
| | |
| | | style="width: 100%" /> |
| | | </el-form-item> |
| | | <el-form-item label="å建æ¶é´"> |
| | | <el-date-picker v-model="mergeForm.createTime" |
| | | <el-date-picker v-model="mergeCreateTimeDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 100%" /> |
| | |
| | | </el-form-item> |
| | | <el-form-item label="å建æ¶é´" |
| | | prop="createTime"> |
| | | <el-date-picker v-model="form.createTime" |
| | | <el-date-picker v-model="formCreateTimeDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 100%" |
| | |
| | | Number(row.qtyRequired || 0) - Number(row.quantityIssued || 0); |
| | | mergeForm.planCompleteTime = row.requiredDate || ""; |
| | | mergeForm.productId = row.productId || ""; |
| | | mergeForm.createTime = new Date().toISOString().split("T")[0]; |
| | | mergeForm.createTime = dayjs().format("YYYY-MM-DD HH:mm:ss"); |
| | | mergeForm.ids = [row.id]; |
| | | sumAssignedQuantity.value = |
| | | Number(row.qtyRequired || 0) - Number(row.quantityIssued || 0); |
| | |
| | | productId: "", |
| | | createTime: "", |
| | | }); |
| | | const mergeCreateTimeDate = computed({ |
| | | get: () => (mergeForm.createTime ? String(mergeForm.createTime).split(" ")[0] : ""), |
| | | set: (value) => { |
| | | mergeForm.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : ""; |
| | | }, |
| | | }); |
| | | |
| | | // 导å
¥ç¸å
³ |
| | | const importDialogRef = ref(null); |
| | |
| | | promisedDeliveryDate: "", |
| | | remark: "", |
| | | createTime: "", |
| | | }); |
| | | const formCreateTimeDate = computed({ |
| | | get: () => (form.createTime ? String(form.createTime).split(" ")[0] : ""), |
| | | set: (value) => { |
| | | form.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : ""; |
| | | }, |
| | | }); |
| | | const rules = reactive({ |
| | | productId: [{ required: true, message: "è¯·éæ©äº§å", trigger: "change" }], |
| | |
| | | mergeForm.totalAssignedQuantity = totalAssignedQuantity; |
| | | mergeForm.planCompleteTime = firstRow.requiredDate || ""; |
| | | mergeForm.productId = firstRow.productId || ""; |
| | | mergeForm.createTime = dayjs().format("YYYY-MM-DD HH:mm:ss"); |
| | | mergeForm.ids = selectedRows.value.map(row => row.id); |
| | | |
| | | // æå¼å¼¹çª |
| | |
| | | requiredDate: "", |
| | | promisedDeliveryDate: "", |
| | | remark: "", |
| | | createTime: new Date().toISOString().split("T")[0], |
| | | createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), |
| | | }); |
| | | dialogVisible.value = true; |
| | | fetchProductOptions(); |
| | |
| | | requiredDate: row.requiredDate || "", |
| | | promisedDeliveryDate: row.promisedDeliveryDate || "", |
| | | remark: row.remark || "", |
| | | createTime: row.createTime || "", |
| | | }); |
| | | dialogVisible.value = true; |
| | | fetchProductOptions(); |
| | |
| | | <el-tree-select |
| | | v-model="selectedProductIds" |
| | | multiple |
| | | filterable |
| | | collapse-tags |
| | | collapse-tags-tooltip |
| | | placeholder="è¯·éæ©äº§åï¼å¯å¤éï¼" |
| | |
| | | <el-form-item label="å建æ¶é´" |
| | | prop="createTime"> |
| | | <el-date-picker style="width: 100%" |
| | | v-model="form.createTime" |
| | | v-model="formCreateTimeDate" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | type="date" |
| | |
| | | dialogType: "add", |
| | | viewDialogVisible: false, |
| | | currentKnowledge: {}, |
| | | }); |
| | | const formCreateTimeDate = computed({ |
| | | get: () => (form.value.createTime ? String(form.value.createTime).split(" ")[0] : ""), |
| | | set: (value) => { |
| | | form.value.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : ""; |
| | | }, |
| | | }); |
| | | |
| | | const { |
| | |
| | | trainingMode: "", // å¹è®æ¹å¼ |
| | | placeTraining: "", // å¹è®å°ç¹ |
| | | classHour: "", // è¯¾æ¶ |
| | | createTime: new Date().toISOString().split("T")[0], // å建æ¶é´ |
| | | createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), // å建æ¶é´ |
| | | }); |
| | | } else if (type === "edit" && row) { |
| | | dialogTitle.value = "ç¼è¾å¹è®"; |
| | |
| | | trainingMode: row.trainingMode, // å¹è®æ¹å¼ |
| | | placeTraining: row.placeTraining, // å¹è®å°ç¹ |
| | | classHour: row.classHour, // è¯¾æ¶ |
| | | createTime: row.createTime || "", // å建æ¶é´ |
| | | }); |
| | | } |
| | | dialogVisible.value = true; |
| | |
| | | @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> |
| | |
| | | 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, |
| | |
| | | </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" |
| | |
| | | // ç¾è®¢æ¥æé»è®¤ä¸ºå½å¤© |
| | | form.value.executionDate = getCurrentDate(); |
| | | // å建æ¶é´é»è®¤ä¸ºå½å¤© |
| | | form.value.createTime = getCurrentDate(); |
| | | form.value.createTime = dayjs().format("YYYY-MM-DD HH:mm:ss"); |
| | | // é»è®¤èªå¨çæéå®ååå· |
| | | form.value.autoGenerateContractNo = true; |
| | | } else { |
| | |
| | | return; |
| | | } |
| | | |
| | | // æ£æ¥æ¯å¦åªå©ä¸æ¡æ°æ®ï¼éå®å°è´¦è³å°ä¿ç䏿¡äº§å |
| | | if (productData.value.length <= 1) { |
| | | proxy.$modal.msgWarning("éå®å°è´¦è³å°éè¦ä¿ç䏿¡äº§åæ°æ®"); |
| | | return; |
| | | } |
| | | |
| | | if (operationType.value === "add") { |
| | | productData.value = productData.value.filter( |
| | | (item) => !productSelectedRows.value.includes(item) |
| | |
| | | }; |
| | | |
| | | // æå°åè½ |
| | | const handlePrint = async () => { |
| | | const handlePrint = async () => { |
| | | if (selectedRows.value.length === 0) { |
| | | proxy.$modal.msgWarning("è¯·éæ©è¦æå°çæ°æ®"); |
| | | return; |
| | |
| | | // æ ¼å¼åæ¥æ |
| | | const formatDate = (dateString) => { |
| | | if (!dateString) return getCurrentDate(); |
| | | const date = new Date(dateString); |
| | | const year = date.getFullYear(); |
| | | const month = String(date.getMonth() + 1).padStart(2, "0"); |
| | | const day = String(date.getDate()).padStart(2, "0"); |
| | | return `${year}/${month}/${day}`; |
| | | return dayjs(dateString).format("YYYY/MM/DD HH:mm:ss"); |
| | | }; |
| | | // æ ¼å¼åæ¥ææ¶é´ |
| | | const formatDateTime = (date) => { |
| | |
| | | |
| | | /** |
| | | * 夿æ¯å¦å¯ä»¥åè´§ |
| | | * åªæå¨äº§åç¶ææ¯å
è¶³ï¼åè´§ç¶ææ¯å¾
åè´§åå®¡æ ¸æç»çæ¶åæå¯ä»¥åè´§ |
| | | * åªæå¨äº§åç¶ææ¯å
è¶³ï¼åè´§ç¶ææ¯å¾
åè´§ã审æ¹ä¸ãå®¡æ ¸æç»ãé¨ååè´§çæ¶åæå¯ä»¥åè´§ |
| | | * @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:{ |