Merge remote-tracking branch 'origin/dev_pro2.0' into dev_pro2.0
已添加42个文件
已删除12个文件
已修改379个文件
| | |
| | | # ææ¡£å°åçå¼å
³ |
| | | VITE_APP_DOCALERT_ENABLE=true |
| | | |
| | | # AI åè½æ»å¼å
³ï¼false æ¶éèææ AI å
¥å£ï¼AI 模åèåãOCRã颿µãæºè½å¤å®ãæºè½ä½å¯¼å
¥ï¼ |
| | | VITE_APP_AI_ENABLE=false |
| | | |
| | | # ç¾åº¦ç»è®¡ |
| | | VITE_APP_BAIDU_CODE = e98f2eab6ceb8688bc6d8fc5332ff093 |
| | | |
| | |
| | | VITE_UPLOAD_TYPE=server |
| | | # æ¯å¦æå¼ devtoolsï¼true 为æå¼ï¼false 为å
³é |
| | | VITE_DEVTOOLS=false |
| | | # AI åè½æ»å¼å
³ï¼false æ¶éèææ AI å
¥å£ï¼AI 模åèåãOCRã颿µãæºè½å¤å®ãæºè½ä½å¯¼å
¥ï¼ |
| | | VITE_APP_AI_ENABLE=true |
| | | |
| | | |
| | | # æ¯å¦æ³¨å
¥å
¨å±loading |
| | | VITE_INJECT_APP_LOADING=true |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | --- |
| | | name: è¯å ä¸»é¢ |
| | | description: æ±çº¢ãæéä¸å®£çº¸ç½ææçç»å½åºæ¯ååºç¨ä¸»é¢ |
| | | colors: |
| | | primary: "#a51d24" |
| | | primary-dark: "hsl(357 64% 73%)" |
| | | paper: "hsl(40 50% 99%)" |
| | | paper-deep: "hsl(37 33% 95%)" |
| | | ink: "hsl(27 16% 17%)" |
| | | border: "hsl(33 25% 86%)" |
| | | dark-background: "hsl(26 16% 12%)" |
| | | dark-card: "hsl(26 15% 14%)" |
| | | dark-foreground: "hsl(35 30% 90%)" |
| | | frame-gold: "#cda878" |
| | | typography: |
| | | display: |
| | | fontFamily: '"STKaiti", "KaiTi", "FangSong", serif' |
| | | fontWeight: 700 |
| | | body: |
| | | fontFamily: "var(--font-family, system-ui, sans-serif)" |
| | | rounded: |
| | | control: "9px" |
| | | components: |
| | | login-button: |
| | | backgroundColor: "{colors.primary}" |
| | | textColor: "#ffffff" |
| | | rounded: "{rounded.control}" |
| | | --- |
| | | |
| | | # Design System: è¯å ä¸»é¢ |
| | | |
| | | ## Overview |
| | | |
| | | **Creative North Star: "宣纸ä¸çè¯å "** |
| | | |
| | | ä»¥å·²ç¡®è®¤çæ±çº¢ãæéã宣纸ç½ä¸è¯å åºé¢åèä¸ºä¾æ®ãç»å½åºæ¯ç¨è¯æä¸å»ºçæ¿è½½æ°æ°ï¼è¡¨åä¿ææ¸
æ°ï¼åºç¨å
鍿²¿ç¨ç°æç»ä»¶ä¸å¸å±ï¼éè¿å
¨å±è¯ä¹è²ä¼ éåä¸è§è§èº«ä»½ãç»å½é¡µçå è®ãè£
饰æ¡ä¸åºé¢æå¾ä¸æ¨å¹¿å°ä¸å¡é¡µé¢ã |
| | | |
| | | ## Colors |
| | | |
| | | æ±çº¢æ¿æ
ä¸»è¦æä½ä¸éä¸æï¼æéç¨äºç»å½åºæ¯çç»æ¡åè£
é¥°ï¼æç½ä¸å¢¨è²æ¿æ
æ¥å¸¸é
读ã`src/preferences.ts` å®ä¹é»è®¤ä¸»è²ï¼`src/styles/herbal-theme.css` å®ä¹ææä¸æ§è²ï¼ä¸»è²è²é¶ç±å好系ç»çæã |
| | | |
| | | **The ç¨æ·å好 Rule.** 馿¬¡ä¸»é¢å级è¿ç§»å°é»è®¤æ±çº¢ï¼æ¤åä¿çç¨æ·éæ©ç主è²ä¸æææ¨¡å¼ã亮æ±ç æè²ä¸»è²è¦çåªå¨å½å主è²çäºé»è®¤æ±çº¢æ¶å¯ç¨ãè¿ç§»ç»èä¸åå¨éå¶è§[ç»å½ä¸»é¢è®°å½](../docs/design/login-herbal-theme.md)ã |
| | | |
| | | ## Typography |
| | | |
| | | æ£æç»§æ¿åºç¨åä½åéä¸ç³»ç»æ 衬线åéï¼ä¿æè¡¨ååä¸å¡æ°æ®å¯è¯»ãå
¬å¸åç§°ãå è®éç¨æ¬æºæ¥·ä½ï¼ä»¿å®åéï¼ä¸ä¸è½½åä½ï¼ä¸åæä½ç³»ç»çå形伿差å¼ãç»å½æ é¢ä½¿ç¨æ£æä½ç³»çç²ä½ï¼é¿å
æè£
饰åä½ç¨äºè¾å
¥å
容ã |
| | | |
| | | ## Layout |
| | | |
| | | ä¸å¡çé¢ä¿çæ¢æå¸å±å¯åº¦ä¸ç»ä»¶ç»æãç»å½æ¡é¢éç¨å·¦ä¾§å è®ãå³ä¾§è¡¨åï¼å®½å±éå¶è¡¨å宽度ï¼å°äºçäº 900px æ¹ä¸ºåå表åå¹¶éèå è®ï¼å°äºçäº 540px æ¶ç´§è¾¹è·ãå
许åçåºåæ¢è¡ãè¯è¨å主é¢å·¥å
·ä¿çå¨é¡µé¦ãçæ¡é¢è§å£éä½è¡¨åæå°é«åº¦åå
è¾¹è·ã |
| | | |
| | | ## Elevation & Depth |
| | | |
| | | åºé¢å¾çä½äºç»å½å
容ä¸å±ï¼æµ
è²è¡¨åç¨æ¥è¿ä¸éæç纸ç½åºä¸æåæè²é´å½±æä¾é
读å±ãæè²éä½å¾ç亮度ä¸é¥±å度ï¼è¡¨åæ¹ç¨æ·±æ£åºãåºç¨å
é¨ä¾èµç°æå¡çãæµ®å±è¯ä¹è²ï¼ä¸è¿½å åºæ¯è£
饰ã |
| | | |
| | | ## Shapes |
| | | |
| | | ç»å½è¾å
¥æ¡ä¸ä¸»æé®ä½¿ç¨è½»åè§ï¼è¡¨åå¤å£³ä½¿ç¨æ´å®½çåè§ï¼æéç»çº¿ååè§çº¹æ ·ä»
å±äºè®¤è¯åºæ¯ãæ¡çº¿ä¸çº¹æ ·ä¸ºè£
饰ï¼ä¸æ¿è½½äº¤äºã |
| | | |
| | | ## Components |
| | | |
| | | 认è¯å¸å±ç»§ç»æ¸²æçå®è·¯ç±ï¼ä¿çç°æ `AuthenticationLogin`ãè¡¨åæ ¡éªãå è½½æãéªè¯ç ä¸è®¤è¯ store è°ç¨ãè¾å
¥æ¯æè´¦å·ï¼å½åå¯ç èªå¨å¡«å
ï¼å¯è§ç¦ç¹ç¯æç¡®å®ä½é®çæä½ãå¯ç åæ¢ä¸ºææåä»ä¿çæ¾éæé®çå³ä¾§ç©ºé´ï¼44pxï¼ï¼é¿å
æå䏿é®éå ãæè²å·¥å
·æ¡éç¨æ·±è²åºãæ é¢æä½¿ç¨æ æåå ä½ SVG çº¹æ ·ã |
| | | |
| | | **The å
¬å¸èº«ä»½ Rule.** å
¬å¸åç§°ãLogoãç»å½æ é¢åæè¿°ä»è¯»åç°æé
ç½®ãè¯å èæ¯ä¸è£
饰å±äºä¸»é¢ï¼ä¸è½æ¿ä»£å
¬å¸ Logoï¼æªé
ç½®å
¬å¸æ å¿æ¶éèæ¡æ¶ç¤ºä¾ Logoã |
| | | |
| | | ## Do's and Don'ts |
| | | |
| | | - 使ç¨è¯ä¹è²ä¸ç°æåå¥½ç³»ç»æ©å±ä¸å¡é¡µé¢ï¼ä¸å¡ç¶æè²ä¿çåæå«ä¹ã |
| | | - ä¿çå
¬å¸å¨æé
ç½®ãçå®è®¤è¯é¾è·¯ä¸ç¨æ·åç»ä¸»é¢å好ã |
| | | - ä¸æèæ¯ä¸çè¯å åºæ¯æå ä½è£
饰解é为ä¼ä¸è®¤è¯ãåæ æäº§åèµè´¨ã |
| | | - ä¸å¨çæçèæ¯ä¸ç»å¶æåãLogoã表åæå
¶ä» UIã |
| | | - 䏿 mock æªå¾éªè¯æè¿°ä¸ºçå®å端ç»å½æåã |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | @echo off |
| | | setlocal EnableExtensions EnableDelayedExpansion |
| | | |
| | | REM ============================================================ |
| | | REM Vue Frontend Deploy Script |
| | | REM ============================================================ |
| | | |
| | | REM ========================= |
| | | REM Configuration |
| | | REM ========================= |
| | | |
| | | set "COMPANY=HSC" |
| | | |
| | | set "REMOTE_USER=root" |
| | | set "REMOTE_HOST=36.213.150.237" |
| | | set "REMOTE_PORT=65022" |
| | | |
| | | set "PASSWORD=Sh@1008600" |
| | | |
| | | set "ZIP_FILE=dist.zip" |
| | | set "REMOTE_DIR=/data/vue/mom/hsc/dist" |
| | | |
| | | set "SSH_OPTS=-o StrictHostKeyChecking=no -o ConnectTimeout=10 -o ServerAliveInterval=15 -o ServerAliveCountMax=3" |
| | | |
| | | set "RETRY_COUNT=3" |
| | | |
| | | set "SSHPASS_DIR=%~dp0bin" |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Start |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo ============================================================ |
| | | echo Vue Dist Auto Deploy |
| | | echo ============================================================ |
| | | echo. |
| | | echo Company : %COMPANY% |
| | | echo Server : %REMOTE_USER%@%REMOTE_HOST% |
| | | echo Port : %REMOTE_PORT% |
| | | echo Remote Dir : %REMOTE_DIR% |
| | | echo Package : %ZIP_FILE% |
| | | echo. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check sshpass |
| | | REM ============================================================ |
| | | |
| | | echo [1/7] Checking sshpass... |
| | | |
| | | if exist "%SSHPASS_DIR%\sshpass.exe" ( |
| | | set "PATH=%SSHPASS_DIR%;%PATH%" |
| | | ) |
| | | |
| | | where sshpass >nul 2>&1 |
| | | |
| | | if not errorlevel 1 ( |
| | | echo [OK] sshpass found. |
| | | goto CHECK_PNPM |
| | | ) |
| | | |
| | | echo [WARN] sshpass not found. |
| | | |
| | | if not exist "%SSHPASS_DIR%" ( |
| | | mkdir "%SSHPASS_DIR%" >nul 2>&1 |
| | | ) |
| | | |
| | | if exist "%SSHPASS_DIR%\sshpass.exe" ( |
| | | set "PATH=%SSHPASS_DIR%;%PATH%" |
| | | echo [OK] sshpass found. |
| | | goto CHECK_PNPM |
| | | ) |
| | | |
| | | echo [INFO] Downloading sshpass... |
| | | |
| | | powershell -NoProfile -ExecutionPolicy Bypass -Command "$url='https://github.com/xhcoding/sshpass-win32/releases/download/v1.0.3/sshpass.exe'; $out='%SSHPASS_DIR%\sshpass.exe'; Invoke-WebRequest -Uri $url -OutFile $out" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Failed to download sshpass. |
| | | echo [ERROR] Please install sshpass manually. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | set "PATH=%SSHPASS_DIR%;%PATH%" |
| | | |
| | | echo [OK] sshpass installed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check pnpm |
| | | REM ============================================================ |
| | | |
| | | :CHECK_PNPM |
| | | |
| | | echo. |
| | | echo [2/7] Checking pnpm... |
| | | |
| | | where pnpm >nul 2>&1 |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] pnpm not found. |
| | | echo [HINT] npm install -g pnpm |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | call pnpm --version |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] pnpm execution failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check node |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [3/7] Checking Node.js... |
| | | |
| | | where node >nul 2>&1 |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Node.js not found. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | call node --version |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Node.js execution failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check node_modules |
| | | REM ============================================================ |
| | | |
| | | if not exist "node_modules" ( |
| | | echo. |
| | | echo [INFO] node_modules not found. |
| | | echo [INFO] Running pnpm install... |
| | | |
| | | call pnpm install |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] pnpm install failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] pnpm install completed. |
| | | ) else ( |
| | | echo [OK] node_modules exists. |
| | | ) |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Build |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [4/7] Building project... |
| | | |
| | | if exist "%ZIP_FILE%" ( |
| | | echo. |
| | | echo [WARN] %ZIP_FILE% already exists. |
| | | set "REBUILD=" |
| | | set /p "REBUILD=Rebuild? Y/N (default Y): " |
| | | |
| | | if /I "!REBUILD!"=="N" ( |
| | | echo [INFO] Skip build. |
| | | goto CHECK_ZIP |
| | | ) |
| | | ) |
| | | |
| | | echo [INFO] Company: %COMPANY% |
| | | echo [INFO] Command: pnpm run build |
| | | echo. |
| | | |
| | | set "VITE_COMPANY=%COMPANY%" |
| | | |
| | | call pnpm run build |
| | | |
| | | if errorlevel 1 ( |
| | | echo. |
| | | echo [ERROR] Build failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo. |
| | | echo [OK] Build completed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check ZIP |
| | | REM ============================================================ |
| | | |
| | | :CHECK_ZIP |
| | | |
| | | echo. |
| | | echo [5/7] Checking package... |
| | | |
| | | if not exist "%ZIP_FILE%" ( |
| | | echo [ERROR] %ZIP_FILE% not found. |
| | | echo [HINT] Please check whether your build generates dist.zip. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | for %%F in ("%ZIP_FILE%") do set "ZIP_BYTES=%%~zF" |
| | | |
| | | set /a ZIP_MB=ZIP_BYTES/1024/1024 |
| | | |
| | | echo [OK] Package found. |
| | | echo [INFO] File size: !ZIP_BYTES! bytes |
| | | echo [INFO] File size: !ZIP_MB! MB |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Set sshpass password |
| | | REM ============================================================ |
| | | |
| | | set "SSHPASS=%PASSWORD%" |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Test SSH |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [6/7] Testing SSH connection... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "echo SSH_OK" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] SSH connection failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] SSH connection successful. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Create remote directory |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Creating remote directory... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mkdir -p '%REMOTE_DIR%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Failed to create remote directory. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] Remote directory ready. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Upload temporary file |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Uploading package... |
| | | |
| | | set "REMOTE_TMP=%REMOTE_DIR%/.dist.zip.uploading" |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_TMP%'" |
| | | |
| | | sshpass -e scp -P %REMOTE_PORT% %SSH_OPTS% "%ZIP_FILE%" %REMOTE_USER%@%REMOTE_HOST%:"%REMOTE_TMP%" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Upload failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] Upload completed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check remote file |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Checking remote package... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "test -f '%REMOTE_TMP%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Remote package does not exist. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] Remote package exists. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Compare file size |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Checking package size... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "stat -c %%s '%REMOTE_TMP%'" |
| | | |
| | | echo. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Rename temporary file |
| | | REM ============================================================ |
| | | |
| | | echo [INFO] Activating package... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mv -f '%REMOTE_TMP%' '%REMOTE_DIR%/%ZIP_FILE%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Failed to activate package. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] Package activated. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check unzip |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Checking unzip... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "command -v unzip" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] unzip is not installed on server. |
| | | echo. |
| | | echo CentOS/RHEL: |
| | | echo yum install -y unzip |
| | | echo. |
| | | echo Ubuntu/Debian: |
| | | echo apt install -y unzip |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] unzip found. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Test ZIP |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Testing remote ZIP... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -t '%ZIP_FILE%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo. |
| | | echo [ERROR] ZIP test failed. |
| | | echo [INFO] Remote ZIP has been kept. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] ZIP test passed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Extract |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Extracting package... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -o '%ZIP_FILE%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo. |
| | | echo [ERROR] Remote unzip failed. |
| | | echo [INFO] Remote ZIP has been kept. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo. |
| | | echo [OK] Extract completed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Remove ZIP |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Removing remote ZIP... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_DIR%/%ZIP_FILE%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [WARN] Failed to remove remote ZIP. |
| | | ) else ( |
| | | echo [OK] Remote ZIP removed. |
| | | ) |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Success |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo ============================================================ |
| | | echo DEPLOY SUCCESS |
| | | echo ============================================================ |
| | | echo. |
| | | echo Company : %COMPANY% |
| | | echo Server : %REMOTE_HOST%:%REMOTE_PORT% |
| | | echo Remote Dir : %REMOTE_DIR% |
| | | echo Package : %ZIP_FILE% |
| | | echo Size : !ZIP_MB! MB |
| | | echo. |
| | | echo Deployment completed successfully. |
| | | echo. |
| | | |
| | | goto END |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Error |
| | | REM ============================================================ |
| | | |
| | | :ERROR_EXIT |
| | | |
| | | echo. |
| | | echo ============================================================ |
| | | echo DEPLOY FAILED |
| | | echo ============================================================ |
| | | echo. |
| | | echo Please check the error message above. |
| | | echo. |
| | | |
| | | goto END |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM End |
| | | REM ============================================================ |
| | | |
| | | :END |
| | | |
| | | pause |
| | | endlocal |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | # Product |
| | | |
| | | <!-- impeccable:product-schema 1 --> |
| | | |
| | | ## Platform |
| | | |
| | | web |
| | | |
| | | ## Operating Context |
| | | |
| | | ç°æ MOM 管çåå°ãæ¤æ¬¡è½èåè½é¢åéæµ·çä¸å®¶å京åä»å ä¼ä¸ï¼ä¸»è¦äº§åå
å«ççªåæ»è¡¥ååæµï¼ç¨æ·æä¾ï¼ã |
| | | |
| | | ## Capabilities and Constraints |
| | | |
| | | è½èç®¡çæ¯ç¬ç«æ¨¡åï¼æå¨æå¤©å½å
¥æ°´ãçµãè¸æ±½ä½¿ç¨ä¿¡æ¯ï¼æä¾å¢å æ¹æ¥åæ°æ®åæä¸¤ä¸ªåèåãç¨æ·å·²ç¡®è®¤æå¤©å½å
¥ãåºåã产åç±»å«ãç¨éåä¸ºæ¬æ¨¡åä¿¡æ¯ï¼ä¸å
³èå
¶ä»ä¸å¡æ¨¡åãåææ¯æææ¥/ææ±æ»ã |
| | | |
| | | ## Open Decisions |
| | | |
| | | ä¼ä¸æ£å¼åç§°ãå®é
åºååç§°ä¸è½æºåä»·æªæä¾ï¼åºå建议ä»
ç¨äºå½å
¥æç¤ºï¼ä»·æ ¼ç±ç¨æ·å¡«åã |
| | |
| | | name: 'åº·ææºè½', |
| | | logo: '/logos/YZFXLogo.png', |
| | | favicon: '/logos/YZFXLogo.ico', |
| | | apiUrl: 'http://36.213.150.237:9004/admin-api', |
| | | apiUrl: 'http://36.213.150.237:9006/admin-api', |
| | | }, |
| | | CHDL: { |
| | | name: 'é¿è¾çµå', |
| | | logo: '/logos/YZFXLogo.png', |
| | | favicon: '/logos/YZFXLogo.ico', |
| | | apiUrl: 'http://36.213.150.237:9006/admin-api', |
| | | apiUrl: 'http://36.213.150.237:9004/admin-api', |
| | | }, |
| | | HM: { |
| | | name: 'æ ç§', |
| | | logo: '/logos/YZFXLogo.png', |
| | | favicon: '/logos/YZFXLogo.ico', |
| | | apiUrl: 'http://36.213.150.237:9013/admin-api', |
| | | }, |
| | | TRT: { |
| | | name: 'å京åä»å ', |
| | | logo: '/logos/YZFXLogo.png', |
| | | favicon: '/logos/YZFXLogo.ico', |
| | | apiUrl: 'http://36.213.150.237:9015/admin-api', |
| | | }, |
| | | HSC: { |
| | | name: '好水å·é£åæéå
¬å¸', |
| | | logo: '/logos/YZFXLogo.png', |
| | | favicon: '/logos/YZFXLogo.ico', |
| | | apiUrl: 'http://36.213.150.237:9540/admin-api', |
| | | }, |
| | | }; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | @echo off |
| | | setlocal EnableExtensions EnableDelayedExpansion |
| | | |
| | | REM ============================================================ |
| | | REM Vue Frontend Deploy Script |
| | | REM ============================================================ |
| | | |
| | | REM ========================= |
| | | REM Configuration |
| | | REM ========================= |
| | | |
| | | set "COMPANY=HM" |
| | | |
| | | set "REMOTE_USER=root" |
| | | set "REMOTE_HOST=36.213.150.237" |
| | | set "REMOTE_PORT=65022" |
| | | |
| | | set "PASSWORD=Sh@1008600" |
| | | |
| | | set "ZIP_FILE=dist.zip" |
| | | set "REMOTE_DIR=/data/vue/mom/hm/dist" |
| | | |
| | | set "SSH_OPTS=-o StrictHostKeyChecking=no -o ConnectTimeout=10 -o ServerAliveInterval=15 -o ServerAliveCountMax=3" |
| | | |
| | | set "RETRY_COUNT=3" |
| | | |
| | | set "SSHPASS_DIR=%~dp0bin" |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Start |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo ============================================================ |
| | | echo Vue Dist Auto Deploy |
| | | echo ============================================================ |
| | | echo. |
| | | echo Company : %COMPANY% |
| | | echo Server : %REMOTE_USER%@%REMOTE_HOST% |
| | | echo Port : %REMOTE_PORT% |
| | | echo Remote Dir : %REMOTE_DIR% |
| | | echo Package : %ZIP_FILE% |
| | | echo. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check sshpass |
| | | REM ============================================================ |
| | | |
| | | echo [1/7] Checking sshpass... |
| | | |
| | | if exist "%SSHPASS_DIR%\sshpass.exe" ( |
| | | set "PATH=%SSHPASS_DIR%;%PATH%" |
| | | ) |
| | | |
| | | where sshpass >nul 2>&1 |
| | | |
| | | if not errorlevel 1 ( |
| | | echo [OK] sshpass found. |
| | | goto CHECK_PNPM |
| | | ) |
| | | |
| | | echo [WARN] sshpass not found. |
| | | |
| | | if not exist "%SSHPASS_DIR%" ( |
| | | mkdir "%SSHPASS_DIR%" >nul 2>&1 |
| | | ) |
| | | |
| | | if exist "%SSHPASS_DIR%\sshpass.exe" ( |
| | | set "PATH=%SSHPASS_DIR%;%PATH%" |
| | | echo [OK] sshpass found. |
| | | goto CHECK_PNPM |
| | | ) |
| | | |
| | | echo [INFO] Downloading sshpass... |
| | | |
| | | powershell -NoProfile -ExecutionPolicy Bypass -Command "$url='https://github.com/xhcoding/sshpass-win32/releases/download/v1.0.3/sshpass.exe'; $out='%SSHPASS_DIR%\sshpass.exe'; Invoke-WebRequest -Uri $url -OutFile $out" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Failed to download sshpass. |
| | | echo [ERROR] Please install sshpass manually. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | set "PATH=%SSHPASS_DIR%;%PATH%" |
| | | |
| | | echo [OK] sshpass installed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check pnpm |
| | | REM ============================================================ |
| | | |
| | | :CHECK_PNPM |
| | | |
| | | echo. |
| | | echo [2/7] Checking pnpm... |
| | | |
| | | where pnpm >nul 2>&1 |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] pnpm not found. |
| | | echo [HINT] npm install -g pnpm |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | call pnpm --version |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] pnpm execution failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check node |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [3/7] Checking Node.js... |
| | | |
| | | where node >nul 2>&1 |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Node.js not found. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | call node --version |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Node.js execution failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check node_modules |
| | | REM ============================================================ |
| | | |
| | | if not exist "node_modules" ( |
| | | echo. |
| | | echo [INFO] node_modules not found. |
| | | echo [INFO] Running pnpm install... |
| | | |
| | | call pnpm install |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] pnpm install failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] pnpm install completed. |
| | | ) else ( |
| | | echo [OK] node_modules exists. |
| | | ) |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Build |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [4/7] Building project... |
| | | |
| | | if exist "%ZIP_FILE%" ( |
| | | echo. |
| | | echo [WARN] %ZIP_FILE% already exists. |
| | | set "REBUILD=" |
| | | set /p "REBUILD=Rebuild? Y/N (default Y): " |
| | | |
| | | if /I "!REBUILD!"=="N" ( |
| | | echo [INFO] Skip build. |
| | | goto CHECK_ZIP |
| | | ) |
| | | ) |
| | | |
| | | echo [INFO] Company: %COMPANY% |
| | | echo [INFO] Command: pnpm run build |
| | | echo. |
| | | |
| | | set "VITE_COMPANY=%COMPANY%" |
| | | |
| | | call pnpm run build |
| | | |
| | | if errorlevel 1 ( |
| | | echo. |
| | | echo [ERROR] Build failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo. |
| | | echo [OK] Build completed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check ZIP |
| | | REM ============================================================ |
| | | |
| | | :CHECK_ZIP |
| | | |
| | | echo. |
| | | echo [5/7] Checking package... |
| | | |
| | | if not exist "%ZIP_FILE%" ( |
| | | echo [ERROR] %ZIP_FILE% not found. |
| | | echo [HINT] Please check whether your build generates dist.zip. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | for %%F in ("%ZIP_FILE%") do set "ZIP_BYTES=%%~zF" |
| | | |
| | | set /a ZIP_MB=ZIP_BYTES/1024/1024 |
| | | |
| | | echo [OK] Package found. |
| | | echo [INFO] File size: !ZIP_BYTES! bytes |
| | | echo [INFO] File size: !ZIP_MB! MB |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Set sshpass password |
| | | REM ============================================================ |
| | | |
| | | set "SSHPASS=%PASSWORD%" |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Test SSH |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [6/7] Testing SSH connection... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "echo SSH_OK" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] SSH connection failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] SSH connection successful. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Create remote directory |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Creating remote directory... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mkdir -p '%REMOTE_DIR%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Failed to create remote directory. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] Remote directory ready. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Upload temporary file |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Uploading package... |
| | | |
| | | set "REMOTE_TMP=%REMOTE_DIR%/.dist.zip.uploading" |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_TMP%'" |
| | | |
| | | sshpass -e scp -P %REMOTE_PORT% %SSH_OPTS% "%ZIP_FILE%" %REMOTE_USER%@%REMOTE_HOST%:"%REMOTE_TMP%" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Upload failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] Upload completed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check remote file |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Checking remote package... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "test -f '%REMOTE_TMP%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Remote package does not exist. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] Remote package exists. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Compare file size |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Checking package size... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "stat -c %%s '%REMOTE_TMP%'" |
| | | |
| | | echo. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Rename temporary file |
| | | REM ============================================================ |
| | | |
| | | echo [INFO] Activating package... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mv -f '%REMOTE_TMP%' '%REMOTE_DIR%/%ZIP_FILE%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Failed to activate package. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] Package activated. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check unzip |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Checking unzip... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "command -v unzip" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] unzip is not installed on server. |
| | | echo. |
| | | echo CentOS/RHEL: |
| | | echo yum install -y unzip |
| | | echo. |
| | | echo Ubuntu/Debian: |
| | | echo apt install -y unzip |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] unzip found. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Test ZIP |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Testing remote ZIP... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -t '%ZIP_FILE%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo. |
| | | echo [ERROR] ZIP test failed. |
| | | echo [INFO] Remote ZIP has been kept. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] ZIP test passed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Extract |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Extracting package... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -o '%ZIP_FILE%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo. |
| | | echo [ERROR] Remote unzip failed. |
| | | echo [INFO] Remote ZIP has been kept. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo. |
| | | echo [OK] Extract completed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Remove ZIP |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Removing remote ZIP... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_DIR%/%ZIP_FILE%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [WARN] Failed to remove remote ZIP. |
| | | ) else ( |
| | | echo [OK] Remote ZIP removed. |
| | | ) |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Success |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo ============================================================ |
| | | echo DEPLOY SUCCESS |
| | | echo ============================================================ |
| | | echo. |
| | | echo Company : %COMPANY% |
| | | echo Server : %REMOTE_HOST%:%REMOTE_PORT% |
| | | echo Remote Dir : %REMOTE_DIR% |
| | | echo Package : %ZIP_FILE% |
| | | echo Size : !ZIP_MB! MB |
| | | echo. |
| | | echo Deployment completed successfully. |
| | | echo. |
| | | |
| | | goto END |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Error |
| | | REM ============================================================ |
| | | |
| | | :ERROR_EXIT |
| | | |
| | | echo. |
| | | echo ============================================================ |
| | | echo DEPLOY FAILED |
| | | echo ============================================================ |
| | | echo. |
| | | echo Please check the error message above. |
| | | echo. |
| | | |
| | | goto END |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM End |
| | | REM ============================================================ |
| | | |
| | | :END |
| | | |
| | | pause |
| | | endlocal |
| | |
| | | name="viewport" |
| | | content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0" |
| | | /> |
| | | <!-- ç± vite 注å
¥ VITE_APP_TITLE åéï¼å¨ .env æä»¶å
é
ç½® --> |
| | | <title>%VITE_APP_TITLE%</title> |
| | | <!-- é¦å±æ é¢åºå®ä¸ºä¸æ§ææ¡ï¼ä¸è¦æå®¢æ·åçç¼è¯è¿äº§ç©ï¼å¦åæ¢ä¸ªç¯å¢é¨ç½²å°±ä¼"串åç"ï¼ã |
| | | çå®åçç±è¿è¡æ¶ä¸åï¼ç³»ç»ç®¡ç â å好设置 â å
¬å¸ä¿¡æ¯ï¼å端 system_company_configï¼ï¼ |
| | | å¯å¨åè¦ç document.titleï¼è§ src/utils/company-config.ts --> |
| | | <title>å·¥ä¸AIæºé å¹³å°</title> |
| | | <link rel="icon" href="/favicon.ico" /> |
| | | <script> |
| | | var HM_ID = '%VITE_APP_BAIDU_CODE%'; |
| | |
| | | </style> |
| | | <div class="loading" id="__app-loading__"> |
| | | <span class="dot"><i></i><i></i><i></i><i></i></span> |
| | | <div class="title">%VITE_APP_TITLE%</div> |
| | | <!-- 䏿§ææ¡ï¼å 载页æ©äº JS æ§è¡ï¼æ¿ä¸å°è¿è¡æ¶çå
¬å¸ä¿¡æ¯ï¼ä¸è½åæ»å®¢æ·åç --> |
| | | <div class="title">æ£å¨å è½½ï¼è¯·ç¨åâ¦</div> |
| | | </div> |
| | |
| | | </style> |
| | | <div class="loading" id="__app-loading__"> |
| | | <div class="loader"></div> |
| | | <div class="title">%VITE_APP_TITLE%</div> |
| | | <!-- 䏿§ææ¡ï¼å 载页æ©äº JS æ§è¡ï¼æ¿ä¸å°è¿è¡æ¶çå
¬å¸ä¿¡æ¯ï¼ä¸è½åæ»å®¢æ·åç --> |
| | | <div class="title">æ£å¨å è½½ï¼è¯·ç¨åâ¦</div> |
| | | </div> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import assert from 'node:assert/strict'; |
| | | import fs from 'node:fs'; |
| | | import http from 'node:http'; |
| | | import path from 'node:path'; |
| | | import { createRequire } from 'node:module'; |
| | | import { fileURLToPath } from 'node:url'; |
| | | |
| | | const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); |
| | | const require = createRequire(path.join(root, 'package.json')); |
| | | const { parse, compileScript } = require('vue/compiler-sfc'); |
| | | const ts = require('typescript'); |
| | | const { chromium } = require('C:/Users/äº/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright'); |
| | | const source = fs.readFileSync(path.join(root, 'src/views/bi/dashboard/modules/RealtimeTable.vue'), 'utf8'); |
| | | const { descriptor } = parse(source); |
| | | const script = compileScript(descriptor, { id: 'qa', inlineTemplate: true }); |
| | | const module = ts.transpileModule(script.content, { |
| | | compilerOptions: { target: ts.ScriptTarget.ESNext, module: ts.ModuleKind.ESNext }, |
| | | }).outputText.replace(/from ['"]vue['"]/g, 'from "/vue.js"'); |
| | | const html = `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>BI æ»å¨éªè¯</title> |
| | | <style>body{background:#020817;color:#ebf0fc;font-family:Microsoft YaHei,sans-serif;margin:24px}h1{font-size:18px}main{max-width:1100px;margin:auto}.card{padding:16px;background:#0a1834;border:1px solid #153554;border-radius:8px} ${descriptor.styles.map(s => s.content).join('\n')}</style> |
| | | <main><h1>æè¿å
¥åºè®°å½</h1><div class="card"><div id="app"></div></div></main> |
| | | <script type="module"> |
| | | import {createApp, h, ref} from '/vue.js';import Table from '/table.js'; |
| | | const rows=ref([]), height=ref(180), show=ref(true); const makeRows=n=>Array.from({length:n},(_,i)=>({'å
¥åºåå·':'RK-'+String(i+1).padStart(4,'0'),'产ååç§°':'éªè¯äº§å '+(i+1),'ä»åº':'åéä»','æ°é':100+i})); |
| | | rows.value=makeRows(20); window.qa={setRows:n=>rows.value=makeRows(n), refresh:()=>rows.value=[...rows.value], setHeight:n=>height.value=n, hide:()=>show.value=false}; |
| | | createApp({render:()=>show.value?h(Table,{rows:rows.value,maxHeight:height.value,speed:30}):null}).mount('#app'); |
| | | </script></html>`; |
| | | const vueFile = path.join(path.dirname(require.resolve('vue/package.json')), 'dist/vue.esm-browser.js'); |
| | | const server = http.createServer((request, response) => { |
| | | response.setHeader('Content-Type', request.url === '/' ? 'text/html; charset=utf-8' : 'text/javascript; charset=utf-8'); |
| | | response.end(request.url === '/table.js' ? module : request.url === '/vue.js' ? fs.readFileSync(vueFile) : html); |
| | | }); |
| | | await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); |
| | | let browser; |
| | | const errors = []; |
| | | const output = path.join(root, '../scripts/bi-realtime-scroll-20261014'); |
| | | fs.mkdirSync(output, { recursive: true }); |
| | | try { |
| | | browser = await chromium.launch({ headless: true, executablePath: 'C:/Program Files/Google/Chrome/Application/chrome.exe' }); |
| | | const page = await browser.newPage({ viewport: { width: 1440, height: 800 } }); |
| | | page.on('pageerror', error => errors.push(error.message)); |
| | | await page.goto(`http://127.0.0.1:${server.address().port}`); |
| | | await page.waitForFunction(() => document.querySelector('.table-track')?.getAnimations().length === 1); |
| | | const offset = () => page.locator('.table-track').evaluate(el => new DOMMatrix(getComputedStyle(el).transform).m42); |
| | | const top = await page.locator('.header-table').evaluate(el => el.getBoundingClientRect().top); |
| | | const first = await offset(); |
| | | await page.waitForTimeout(500); |
| | | assert.ok((await offset()) < first - 8, 'Rows must move upwards'); |
| | | assert.equal(await page.locator('.header-table').evaluate(el => el.getBoundingClientRect().top), top, 'Header must stay fixed'); |
| | | await page.locator('.realtime-table').hover(); |
| | | const paused = await offset(); |
| | | await page.waitForTimeout(250); |
| | | assert.ok(Math.abs((await offset()) - paused) < 1, 'Hover must pause'); |
| | | await page.mouse.move(0, 0); |
| | | await page.waitForTimeout(200); |
| | | assert.ok((await offset()) < paused - 3, 'Mouse leave must resume'); |
| | | const refreshOffset = await offset(); |
| | | await page.evaluate(() => window.qa.refresh()); |
| | | await page.waitForTimeout(100); |
| | | assert.ok((await offset()) < refreshOffset, 'Refresh must preserve scroll position'); |
| | | await page.locator('.table-track').evaluate(el => { |
| | | const animation = el.getAnimations()[0]; |
| | | animation.currentTime = Number(animation.effect.getTiming().duration) - 200; |
| | | }); |
| | | await page.waitForTimeout(400); |
| | | assert.ok((await offset()) > -20, 'Loop must wrap seamlessly'); |
| | | await page.locator('.realtime-table').focus(); |
| | | const focused = await offset(); |
| | | await page.waitForTimeout(100); |
| | | assert.ok(Math.abs((await offset()) - focused) < 1, 'Focus must pause'); |
| | | await page.keyboard.press('ArrowDown'); |
| | | assert.ok((await offset()) < focused - 35, 'Keyboard must advance rows'); |
| | | await page.evaluate(() => document.activeElement.blur()); |
| | | await page.screenshot({ path: path.join(output, 'desktop.png'), fullPage: true }); |
| | | await page.setViewportSize({ width: 390, height: 844 }); |
| | | await page.waitForTimeout(150); |
| | | assert.ok(await page.locator('.table-track').evaluate(el => el.getAnimations().length === 1), 'Resize must keep one animation'); |
| | | await page.screenshot({ path: path.join(output, 'mobile.png'), fullPage: true }); |
| | | await page.evaluate(() => window.qa.setRows(2)); |
| | | await page.waitForTimeout(150); |
| | | assert.equal(await page.locator('.table-track').evaluate(el => el.getAnimations().length), 0, 'Short table must not scroll'); |
| | | assert.equal(await page.locator('table[aria-hidden="true"]').count(), 0, 'Short table must not duplicate rows'); |
| | | await page.evaluate(() => window.qa.setRows(30)); |
| | | await page.waitForFunction(() => document.querySelector('.table-track').getAnimations().length === 1); |
| | | await page.emulateMedia({ reducedMotion: 'reduce' }); |
| | | await page.waitForTimeout(150); |
| | | assert.equal(await page.locator('.table-track').evaluate(el => el.getAnimations().length), 0, 'Reduced motion must disable autoplay'); |
| | | assert.equal(await page.locator('.table-viewport').evaluate(el => getComputedStyle(el).overflowY), 'auto', 'Reduced motion must allow manual scroll'); |
| | | await page.emulateMedia({ reducedMotion: 'no-preference' }); |
| | | await page.waitForFunction(() => document.querySelector('.table-track').getAnimations().length === 1); |
| | | await page.evaluate(() => window.qa.setHeight(2000)); |
| | | await page.waitForTimeout(150); |
| | | assert.equal(await page.locator('.table-track').evaluate(el => el.getAnimations().length), 0, 'Larger viewport must stop scrolling when rows fit'); |
| | | await page.evaluate(() => window.qa.hide()); |
| | | await page.waitForTimeout(100); |
| | | assert.equal(await page.evaluate(() => document.getAnimations().length), 0, 'Unmount must clean up animations'); |
| | | assert.deepEqual(errors, []); |
| | | const result = { passed: true, checks: ['continuous scroll', 'fixed header', 'hover pause/resume', 'refresh continuity', 'seamless loop', 'keyboard navigation', 'responsive resize', 'short data', 'reduced motion', 'unmount cleanup'], screenshots: ['desktop.png', 'mobile.png'] }; |
| | | fs.writeFileSync(path.join(output, 'result.json'), JSON.stringify(result, null, 2)); |
| | | console.log(JSON.stringify(result)); |
| | | } finally { |
| | | await browser?.close(); |
| | | server.close(); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | const fs = require('node:fs'); |
| | | const path = require('node:path'); |
| | | const ts = require('typescript'); |
| | | const assert = require('node:assert/strict'); |
| | | const { parse, compileTemplate } = require('vue/compiler-sfc'); |
| | | const root = path.join(__dirname, '../src/views/dashboard/analytics'); |
| | | function evaluate(source, mocks) { |
| | | const js = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText; |
| | | const module = { exports: {} }; |
| | | new Function('require', 'module', 'exports', 'defineOptions', 'defineProps', 'defineEmits', js)( |
| | | (id) => mocks[id] || {}, module, module.exports, () => {}, () => ({ repairActiveCount: 0 }), () => () => {}); |
| | | return module.exports; |
| | | } |
| | | const vue = { computed: (getter) => ({ get value() { return getter(); } }), ref: (value) => ({ value }) }; |
| | | async function scenario(codes, names, expected) { |
| | | const store = { accessRoutes: [{ name: 'Parent', children: names.map((name) => ({ name })) }] }; |
| | | const home = evaluate(fs.readFileSync(path.join(root, 'use-home-access.ts'), 'utf8'), { |
| | | vue, '@vben/stores': { useAccessStore: () => store }, |
| | | '@vben/access': { useAccess: () => ({ hasAccessByCodes: (requested) => requested.some((code) => codes.includes(code)) }) }, |
| | | }); |
| | | const calls = []; |
| | | const mocks = { '../use-home-access': home, './use-home-access': home, |
| | | 'vue-router': { useRouter: () => ({ push: () => {} }) }, './data': { defaultSummary: {} } }; |
| | | for (const [id, method] of [ |
| | | ['#/api/mes/home', 'getHomeSummary'], ['#/api/mes/home', 'getProductionTrend'], |
| | | ['#/api/mes/home', 'getWorkOrderStatusDistribution'], ['#/api/erp/purchase/order', 'getPurchaseOrderPage'], |
| | | ['#/api/erp/sale/order', 'getSaleOrderPage'], ['#/api/mes/wm/materialstock', 'getMaterialStockPage'], |
| | | ['#/api/bpm/task', 'getTaskTodoPage'], |
| | | ]) { |
| | | mocks[id] ||= {}; |
| | | mocks[id][method] = async () => { calls.push(method); return method.startsWith('getHome') ? {} : method.endsWith('Page') ? { total: 3 } : []; }; |
| | | } |
| | | mocks['@vben/plugins/echarts'] = { useEcharts: () => ({ renderEcharts: () => {} }) }; |
| | | for (const file of ['index.vue', 'modules/biz-cards.vue', 'modules/production-trend.vue', 'modules/alert-panel.vue']) { |
| | | const mounted = []; |
| | | mocks.vue = { ...vue, onMounted: (fn) => mounted.push(fn) }; |
| | | const { descriptor } = parse(fs.readFileSync(path.join(root, file), 'utf8')); |
| | | evaluate(descriptor.scriptSetup.content, mocks); |
| | | mounted.forEach((fn) => fn()); |
| | | } |
| | | await new Promise((resolve) => setImmediate(resolve)); |
| | | assert.deepEqual(calls.sort(), expected.sort()); |
| | | const access = home.useHomeAccess(); |
| | | store.accessRoutes = [{ name: 'Forbidden', meta: { menuVisibleWithForbidden: true }, children: [{ name: 'Child' }] }]; |
| | | assert.equal(access.canNavigate('Forbidden'), false); |
| | | assert.equal(access.canNavigate('Child'), false); |
| | | } |
| | | (async () => { |
| | | await scenario([], [], []); |
| | | await scenario(['erp:purchase-order:query'], ['ErpPurchaseOrder'], ['getPurchaseOrderPage']); |
| | | await scenario(['erp:purchase-order:query'], [], []); |
| | | await scenario(['mes:home:query'], [], ['getHomeSummary', 'getProductionTrend', 'getWorkOrderStatusDistribution']); |
| | | await scenario(['*:*:*'], ['ErpPurchaseOrder', 'ErpSaleOrder', 'MesWmMaterialStock', 'BpmTaskTodo'], |
| | | ['getHomeSummary', 'getProductionTrend', 'getWorkOrderStatusDistribution', 'getPurchaseOrderPage', 'getSaleOrderPage', 'getMaterialStockPage', 'getTaskTodoPage']); |
| | | for (const file of ['index.vue', ...fs.readdirSync(path.join(root, 'modules')).filter((f) => f.endsWith('.vue')).map((f) => `modules/${f}`)]) { |
| | | const { descriptor, errors } = parse(fs.readFileSync(path.join(root, file), 'utf8')); |
| | | assert.equal(errors.length, 0, file); |
| | | const result = compileTemplate({ source: descriptor.template.content, filename: file, id: file }); |
| | | assert.equal(result.errors.length, 0, `${file}: ${result.errors}`); |
| | | } |
| | | console.log('PASS: 5 permission scenarios, forbidden routes, and all analytics Vue templates'); |
| | | })(); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // Real user dropdown, confirmation and router; all APIs simulated. |
| | | const assert=require('node:assert/strict'),fs=require('fs'),path=require('path'); |
| | | const {chromium}=require('C:/Users/äº/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright'); |
| | | (async()=>{ |
| | | const server=await(await import('vite')).createServer({root:path.resolve(__dirname,'..'),server:{host:'127.0.0.1',port:5183}});await server.listen(); |
| | | const browser=await chromium.launch({headless:true,executablePath:'C:/Program Files/Google/Chrome/Application/chrome.exe'}); |
| | | const page=await browser.newPage({viewport:{width:1440,height:950},reducedMotion:'reduce'});const errors=[];let logoutCalls=0;let releaseLogout;let logoutHeader; |
| | | page.on('pageerror',e=>errors.push(e.message)); |
| | | await page.route('**/admin-api/**',async route=>{ |
| | | const url=route.request().url(); |
| | | if(url.includes('/system/auth/logout')){logoutCalls++;logoutHeader=route.request().headers().authorization;await new Promise(r=>releaseLogout=r);} |
| | | const data=url.includes('company-config')?{companyName:'éåºéªè¯å¹³å°'}:url.includes('count')?0:url.includes('list')?[]:null; |
| | | await route.fulfill({contentType:'application/json',body:JSON.stringify({code:0,data,msg:''})}).catch(()=>{}); |
| | | }); |
| | | try{ |
| | | await page.goto('http://127.0.0.1:5183/auth/login',{waitUntil:'domcontentloaded'});await page.getByRole('heading',{name:'欢è¿åæ¥'}).waitFor(); |
| | | await page.waitForFunction(()=>!document.querySelector('#__app-loading__')||getComputedStyle(document.querySelector('#__app-loading__')).opacity==='0'); |
| | | await page.evaluate(async()=>{ |
| | | const stores=await import('/src/packages/stores/src/index.ts'),{router}=await import('/src/router/index.ts'),{h}=await import('/node_modules/.vite/deps/vue.js'); |
| | | stores.useAccessStore().setAccessToken('qa-session-token');stores.useAccessStore().setRefreshToken('qa-refresh-token');stores.useAccessStore().setIsAccessChecked(true); |
| | | stores.useUserStore().setUserInfo({userId:'1',username:'logout-qa',nickname:'éåºéªè¯ç¨æ·',avatar:''}); |
| | | router.addRoute('Root',{name:'LogoutQa',path:'/qa-logout-session',meta:{title:'éåºéªè¯å·¥ä½åº'},component:{render(){return h('div','éåºéªè¯å·¥ä½åº')}}}); |
| | | await router.push('/qa-logout-session'); |
| | | }); |
| | | await page.locator('[id="__vben_main_content"]').getByText('éåºéªè¯å·¥ä½åº',{exact:true}).waitFor({timeout:60000}); |
| | | const avatars=page.locator('button[aria-haspopup="menu"]').filter({has:page.locator('.size-8')}).first(); |
| | | await avatars.click(); |
| | | await page.getByRole('menuitem',{name:/éåºç»å½/}).click(); |
| | | const dialog=page.getByRole('dialog');await dialog.waitFor(); |
| | | const started=Date.now();await dialog.getByRole('button',{name:/^ç¡®\s*[认å®]$/}).click(); |
| | | await page.waitForURL('**/auth/login',{timeout:5000});await page.getByRole('heading',{name:'欢è¿åæ¥'}).waitFor(); |
| | | const elapsed=Date.now()-started; |
| | | const state=await page.evaluate(async()=>{const s=await import('/src/packages/stores/src/index.ts');return{access:s.useAccessStore().accessToken,refresh:s.useAccessStore().refreshToken,user:s.useUserStore().userInfo}}); |
| | | assert.equal(state.access,null);assert.equal(state.refresh,null);assert.equal(state.user,null); |
| | | assert.equal(logoutCalls,1);assert.equal(logoutHeader,'Bearer qa-session-token');assert.equal(typeof releaseLogout,'function','Server logout must still be pending when login page appears'); |
| | | releaseLogout();await page.waitForTimeout(100); |
| | | const result={passed:true,scope:'Real application user dropdown, confirmation, stores and routing; simulated authenticated session and pending backend, no real account login',checks:['click logout and confirm','navigate without refresh while server pending','access and refresh token cleared','user info cleared','server revoke uses original token'],elapsedMs:elapsed,errors}; |
| | | assert.deepEqual(errors,[]); |
| | | const out=path.resolve(__dirname,'../../scripts/logout-fix-20261015');fs.mkdirSync(out,{recursive:true});fs.writeFileSync(path.join(out,'browser-result.json'),JSON.stringify(result,null,2));console.log(JSON.stringify(result)); |
| | | }catch(error){console.error(JSON.stringify({message:error.message,errors,body:(await page.locator('body').innerText()).slice(0,1800)}));throw error;} |
| | | finally{releaseLogout?.();await browser.close();await server.close();} |
| | | })(); |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | const assert = require('node:assert/strict'); |
| | | const fs = require('node:fs'); |
| | | const path = require('node:path'); |
| | | const ts = require('typescript'); |
| | | const vue = require('vue'); |
| | | const pinia = require('pinia'); |
| | | const root = path.join(__dirname, '..'); |
| | | function load(file, mocks = {}) { |
| | | const source = fs.readFileSync(path.join(root, file), 'utf8') |
| | | .replaceAll('import.meta.hot', 'undefined') |
| | | .replaceAll('import.meta.env', '({ DEV: true })'); |
| | | const js = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, esModuleInterop: true } }).outputText; |
| | | const module = { exports: {} }; |
| | | new Function('require', 'module', 'exports', 'localStorage', js)((id) => { |
| | | if (id in mocks) return mocks[id]; |
| | | if (id === 'vue') return vue; |
| | | if (id === 'pinia') return pinia; |
| | | throw new Error(`Unexpected import ${id} in ${file}`); |
| | | }, module, module.exports, {}); |
| | | return module.exports; |
| | | } |
| | | const constants = load('src/views/im/utils/constants.ts'); |
| | | async function scenario(apiFails, resetFails, redirect) { |
| | | const app = vue.createApp({}); |
| | | const setup = load('src/packages/stores/src/setup.ts', { |
| | | 'secure-ls': class {}, 'pinia-plugin-persistedstate': { createPersistedState: () => () => {} }, |
| | | }); |
| | | const instance = await setup.initStores(app, { namespace: 'logout-test' }); |
| | | pinia.setActivePinia(instance); |
| | | const company = load('src/store/company-config.ts', { |
| | | '#/api/system/company-config': {}, '#/utils/company-config': {}, |
| | | }).useCompanyConfigStore(); |
| | | company.companyName = 'Test company'; |
| | | company.logoUrl = '/test-logo.png'; |
| | | company.loginTitle = 'Test'; |
| | | company.loginDescription = 'Test description'; |
| | | const ui = load('src/views/im/home/store/uiStore.ts', { '../../utils/constants': constants }).useImUiStore(); |
| | | ui.userInfoCard.show = true; |
| | | ui.userInfoCard.user = { id: 1 }; |
| | | ui.contextMenu.items = [{ key: 'private', name: 'private' }]; |
| | | ui.contextMenu.onSelect = () => {}; |
| | | ui.nextUnreadJumpNonce = 3; |
| | | const rtc = load('src/views/im/home/store/rtcStore.ts', { |
| | | '../../utils/constants': constants, '#/views/im/utils/auth': {}, './friendStore': {}, |
| | | './groupStore': { useGroupStore: () => ({ markGroupActiveCallLoaded: () => {} }) }, |
| | | }).useRtcStore(); |
| | | rtc.stage = constants.ImRtcCallStage.RUNNING; |
| | | rtc.call = { id: 1 }; |
| | | rtc.setGroupCall({ groupId: 1, room: 'test', joinedUserIds: [1], inviteeIds: [] }); |
| | | const accessModule = load('src/packages/stores/src/modules/access.ts'); |
| | | const userModule = load('src/packages/stores/src/modules/user.ts'); |
| | | const access = accessModule.useAccessStore(); |
| | | access.setAccessToken('old-access'); |
| | | access.setRefreshToken('old-refresh'); |
| | | access.setIsAccessChecked(true); |
| | | access.setLoginExpired(true); |
| | | userModule.useUserStore().setUserInfo({ userId: 1 }); |
| | | if (resetFails) pinia.defineStore('broken-business-store', () => ({ count: vue.ref(1) }))(); |
| | | const navigations = []; |
| | | const apiCalls = []; |
| | | const auth = load('src/store/auth.ts', { |
| | | 'vue-router': { useRouter: () => ({ currentRoute: vue.ref({ fullPath: '/analytics?tab=1' }), replace: async (to) => navigations.push(to) }) }, |
| | | '@vben/constants': { LOGIN_PATH: '/auth/login' }, '@vben/preferences': {}, |
| | | '@vben/stores': { ...setup, ...accessModule, ...userModule }, 'ant-design-vue': {}, '#/locales': {}, |
| | | '#/store/audit-policy': { automaticApproval: vue.ref(false), loadAuditPolicy: async () => {} }, |
| | | '#/api': { logoutApi: async (token) => { apiCalls.push(token); if (apiFails === 'pending') return new Promise(() => {}); if (apiFails) throw new Error('offline'); } }, |
| | | }).useAuthStore(); |
| | | const errors = []; |
| | | const originalError = console.error; |
| | | console.error = (...args) => errors.push(args); |
| | | try { await auth.logout(redirect); } finally { console.error = originalError; } |
| | | assert.deepEqual(apiCalls, ['old-access']); |
| | | assert.equal(navigations.length, 1); |
| | | assert.deepEqual(navigations[0], { path: '/auth/login', query: redirect ? { redirect: encodeURIComponent('/analytics?tab=1') } : {} }); |
| | | assert.equal(access.accessToken, null); |
| | | assert.equal(access.refreshToken, null); |
| | | assert.equal(access.isAccessChecked, false); |
| | | assert.equal(access.loginExpired, false); |
| | | assert.equal(userModule.useUserStore().userInfo, null); |
| | | assert.equal(company.companyName, ''); |
| | | assert.equal(company.logoUrl, ''); |
| | | assert.equal(company.loginTitle, ''); |
| | | assert.equal(company.loginDescription, ''); |
| | | assert.equal(ui.userInfoCard.show, false); |
| | | assert.equal(ui.userInfoCard.user, null); |
| | | assert.deepEqual(ui.contextMenu.items, []); |
| | | assert.equal(ui.contextMenu.onSelect, null); |
| | | assert.equal(ui.nextUnreadJumpNonce, 0); |
| | | assert.equal(rtc.stage, constants.ImRtcCallStage.IDLE); |
| | | assert.equal(rtc.call, null); |
| | | assert.equal(rtc.getGroupCall(1), undefined); |
| | | assert.equal(errors.length, resetFails ? 1 : 0); |
| | | } |
| | | |
| | | async function sessionFixture(api) { |
| | | const app=vue.createApp({}); |
| | | const setup=load('src/packages/stores/src/setup.ts',{'secure-ls':class{},'pinia-plugin-persistedstate':{createPersistedState:()=>()=>{}}}); |
| | | pinia.setActivePinia(await setup.initStores(app,{namespace:'logout-races'})); |
| | | const accessModule=load('src/packages/stores/src/modules/access.ts'),userModule=load('src/packages/stores/src/modules/user.ts'); |
| | | const access=accessModule.useAccessStore(),user=userModule.useUserStore();const navigations=[]; |
| | | access.setAccessToken('old-access');access.setRefreshToken('old-refresh'); |
| | | const auth=load('src/store/auth.ts',{ |
| | | 'vue-router':{useRouter:()=>({currentRoute:vue.ref({fullPath:'/private'}),replace:async to=>navigations.push(to),push:async to=>navigations.push(to)})}, |
| | | '@vben/constants':{LOGIN_PATH:'/auth/login'},'@vben/preferences':{}, |
| | | '@vben/stores':{...setup,...accessModule,...userModule},'ant-design-vue':{notification:{success(){throw Error('Stale success notification')}}},'#/locales':{$t:v=>v}, |
| | | '#/store/audit-policy':{automaticApproval:vue.ref(false),loadAuditPolicy:async()=>{}}, |
| | | '#/api':{logoutApi:async()=>{},...api}, |
| | | }).useAuthStore(); |
| | | return{access,user,auth,navigations}; |
| | | } |
| | | function deferred(){let resolve,reject;const promise=new Promise((a,b)=>{resolve=a;reject=b});return{promise,resolve,reject};} |
| | | async function pendingLoginCannotRestoreSession(){ |
| | | const pending=deferred();const f=await sessionFixture({loginApi:()=>pending.promise}); |
| | | const login=f.auth.authLogin('username',{});await f.auth.logout(false); |
| | | pending.resolve({accessToken:'late-access',refreshToken:'late-refresh'});await login; |
| | | assert.equal(f.access.accessToken,null);assert.equal(f.access.refreshToken,null);assert.equal(f.navigations.length,1); |
| | | } |
| | | async function pendingUserInfoCannotRestorePrivateData(){ |
| | | const pending=deferred();const f=await sessionFixture({getAuthPermissionInfoApi:()=>pending.promise}); |
| | | const info=f.auth.fetchUserInfo();const rejected=assert.rejects(info,/ç»å½ç¶æå·²åå/); |
| | | await f.auth.logout(false);pending.resolve({user:{nickname:'old-user'},roles:['old-role'],menus:[],permissions:['old-permission']});await rejected; |
| | | assert.equal(f.user.userInfo,null);assert.deepEqual(f.access.accessCodes,[]); |
| | | } |
| | | async function requestRaces(){ |
| | | const f=await sessionFixture({});const pending=deferred();let hooks; |
| | | class Client{responses=[];requests=[];isRefreshing=false;refreshTokenQueue=[];addRequestInterceptor(x){this.requests.push(x)}addResponseInterceptor(x){this.responses.push(x)}} |
| | | const presets=load('src/packages/effects/request/src/request-client/preset-interceptors.ts',{ |
| | | '../../../../locales/src':{$t:v=>v},'../../../../utils/src':{isFunction:v=>typeof v==='function'},axios:{isCancel:()=>false}, |
| | | }); |
| | | const request=load('src/api/request.ts',{ |
| | | '@vben/hooks':{isTenantEnable:()=>true,useAppConfig:()=>({apiURL:'/mock'})}, |
| | | '@vben/preferences':{preferences:{app:{enableRefreshToken:true,loginExpiredMode:'page'}}}, |
| | | '@vben/request':{RequestClient:Client,defaultResponseInterceptor:()=>({}),errorMessageResponseInterceptor:()=>({}),authenticateResponseInterceptor:options=>{hooks=options;return presets.authenticateResponseInterceptor(options)}}, |
| | | '@vben/stores':{useAccessStore:()=>f.access},'@vben/utils':{createApiEncrypt:()=>({})}, |
| | | 'ant-design-vue':{message:{}},'#/store':{useAuthStore:()=>f.auth},'./core':{refreshTokenApi:()=>pending.promise}, |
| | | }); |
| | | const refresh=hooks.doRefreshToken();const rejected=assert.rejects(refresh,/ç»å½ç¶æå·²åå/); |
| | | await f.auth.logout(false);pending.resolve({data:{data:{accessToken:'late-refreshed'}}});await rejected;assert.equal(f.access.accessToken,null); |
| | | f.access.setAccessToken('new-account-token');f.access.setRefreshToken('new-account-refresh'); |
| | | await hooks.doReAuthenticate({config:{headers:{Authorization:'Bearer old-access'}}});assert.equal(f.access.accessToken,'new-account-token'); |
| | | const cfg={headers:{'tenant-id':7,'visit-tenant-id':9}}; |
| | | await request.baseRequestClient.requests[0].fulfilled(cfg);assert.equal(cfg.headers['tenant-id'],7);assert.equal(cfg.headers['visit-tenant-id'],9); |
| | | |
| | | const client={isRefreshing:false,refreshTokenQueue:[],request(){throw Error('Failed refresh must not replay queued requests')}}; |
| | | const failed=deferred();let reauthError;const authError={config:{url:'/private',headers:{Authorization:'Bearer old'}},response:{status:401}}; |
| | | const interceptor=presets.authenticateResponseInterceptor({client,doRefreshToken:()=>failed.promise,doReAuthenticate:async e=>{reauthError=e},enableRefreshToken:true,formatToken:t=>t}); |
| | | const first=interceptor.rejected(authError),firstRejected=assert.rejects(first,/refresh failed/); |
| | | const second=interceptor.rejected({config:{url:'/other',headers:{}},response:{status:401}}),secondRejected=assert.rejects(second,/refresh failed/); |
| | | const originalError=console.error;console.error=()=>{}; |
| | | try{failed.reject(new Error('refresh failed'));await Promise.all([firstRejected,secondRejected]);}finally{console.error=originalError;} |
| | | assert.equal(reauthError,authError);assert.equal(client.isRefreshing,false);assert.equal(client.refreshTokenQueue.length,0); |
| | | } |
| | | |
| | | (async () => { |
| | | await scenario(false, false, false); |
| | | await scenario(true, false, true); |
| | | await scenario(false, true, false); |
| | | await scenario(true, true, false); |
| | | await scenario('pending', false, false); |
| | | await pendingLoginCannotRestoreSession(); |
| | | await pendingUserInfoCannotRestorePrivateData(); |
| | | await requestRaces(); |
| | | console.log('PASS: pending APIs do not block logout, stale login/user/refresh cannot restore sessions, failed refresh queue rejects; logout redirects and clears session with API success/failure and store reset failures; company and IM stores reset'); |
| | | })(); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // Actual application/router/forms, simulated APIs, no real authentication. |
| | | const assert=require('node:assert/strict'); |
| | | const {chromium}=require('C:/Users/äº/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright'); |
| | | const fs=require('fs'),path=require('path'); |
| | | const output=path.resolve(__dirname,'../../scripts/login-theme-20261015');fs.mkdirSync(output,{recursive:true}); |
| | | (async()=>{ |
| | | const server=await(await import('vite')).createServer({root:path.resolve(__dirname,'..'),server:{host:'127.0.0.1',port:5182}});await server.listen(); |
| | | const browser=await chromium.launch({headless:true,executablePath:'C:/Program Files/Google/Chrome/Application/chrome.exe'}); |
| | | const page=await browser.newPage({viewport:{width:1664,height:941},reducedMotion:'reduce'}); |
| | | const errors=[];let authPosts=0;page.on('pageerror',e=>errors.push(e.message)); |
| | | await page.route('**/admin-api/**',async route=>{ |
| | | const url=route.request().url();if(route.request().method()==='POST')authPosts++; |
| | | const data=url.includes('company-config')?{companyName:'æ»è¡¥å管çå¹³å°',loginTitle:'æ»è¡¥åè¡ä¸æ°åå管çå¹³å°',loginDescription:'ä¼ æ¿ä¸å»è¯ç²¾å · æå¡å¥åº·ç¾å¥½çæ´»'}:url.includes('tenant')?[]:null; |
| | | await route.fulfill({contentType:'application/json',body:JSON.stringify({code:0,data,msg:''})}); |
| | | }); |
| | | try { |
| | | await page.goto('http://127.0.0.1:5182/auth/login',{waitUntil:'domcontentloaded',timeout:60000}); |
| | | await page.getByRole('heading',{name:'欢è¿åæ¥'}).waitFor({timeout:60000}); |
| | | await page.waitForFunction(()=>!document.querySelector('#__app-loading__')||getComputedStyle(document.querySelector('#__app-loading__')).opacity==='0'); |
| | | await page.waitForFunction(()=>document.querySelector('.herbal-auth__background')?.complete); |
| | | const user=page.getByRole('textbox',{name:'è´¦å·',exact:true}),password=page.locator('input[autocomplete="current-password"]'); |
| | | await user.fill('æ¼ç¤ºè´¦æ·');await password.fill('not-a-real-password');await page.locator('.herbal-login__heading').click();await page.waitForTimeout(400); |
| | | assert.equal(await user.getAttribute('autocomplete'),'username');assert.equal(await password.getAttribute('type'),'password'); |
| | | await page.getByRole('button',{name:'æ¾ç¤ºå¯ç ',exact:true}).focus();await page.keyboard.press('Space');assert.equal(await password.getAttribute('type'),'text'); |
| | | assert.equal(await password.evaluate(e=>getComputedStyle(e).paddingRight),'44px'); |
| | | await page.getByRole('button',{name:'éèå¯ç ',exact:true}).click();assert.equal(await password.getAttribute('type'),'password'); |
| | | const remember=page.locator('button[role="checkbox"]').first();await remember.click();assert.equal(await remember.getAttribute('aria-checked'),'true'); |
| | | await page.screenshot({path:path.join(output,'desktop-login.png'),fullPage:true}); |
| | | const tokens=await page.evaluate(()=>{const s=getComputedStyle(document.documentElement);return{brand:document.documentElement.dataset.brandTheme,primary:s.getPropertyValue('--primary').trim(),background:s.getPropertyValue('--background-deep').trim()}}); |
| | | assert.equal(tokens.brand,'herbal');assert.equal(tokens.background,'37 33% 95%'); |
| | | await page.locator('.herbal-auth__toolbar button').last().click();await page.waitForFunction(()=>document.documentElement.classList.contains('dark'));await page.waitForTimeout(350); |
| | | assert.equal(await page.evaluate(()=>getComputedStyle(document.documentElement).getPropertyValue('--primary').trim()),'357 64% 73%'); |
| | | assert.equal(await page.locator('.herbal-panel').evaluate(e=>getComputedStyle(e).backgroundColor),'rgba(39, 31, 26, 0.97)'); |
| | | assert.equal(await page.locator('.herbal-auth__toolbar >div').evaluate(e=>getComputedStyle(e).backgroundColor),'rgb(48, 39, 31)'); |
| | | assert.equal(await page.evaluate(()=>getComputedStyle(document.documentElement).filter),'none','Dark styles must not apply a filter to the document'); |
| | | await page.screenshot({path:path.join(output,'desktop-dark.png'),fullPage:true}); |
| | | await page.locator('.herbal-auth__toolbar button').last().click();await page.waitForFunction(()=>!document.documentElement.classList.contains('dark')); |
| | | await page.setViewportSize({width:390,height:844});await page.waitForTimeout(250); |
| | | assert.equal(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),true); |
| | | await page.screenshot({path:path.join(output,'mobile-login.png'),fullPage:true}); |
| | | await user.fill('');await password.fill('');await page.getByRole('button',{name:'login',exact:true}).click(); |
| | | await page.getByText('请è¾å
¥ç¨æ·å',{exact:true}).first().waitFor();assert.equal(authPosts,0); |
| | | assert.deepEqual(errors,[]); |
| | | const result={passed:true,scope:'Real application/router/theme/login components; mocked backend and company APIs, demo account, no real authentication',checks:['dynamic company config','background loaded','autocomplete','keyboard password toggle','remember account','light theme variables','dark theme switching','mobile no overflow','required fields block backend'],tokens,screenshots:['desktop-login.png','desktop-dark.png','mobile-login.png']}; |
| | | fs.writeFileSync(path.join(output,'result.json'),JSON.stringify(result,null,2));console.log(JSON.stringify(result)); |
| | | } catch(error){console.error(JSON.stringify({message:error.message,errors}));throw error;} |
| | | finally{await browser.close();await server.close();} |
| | | })(); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // Component interaction check with simulated API data; never creates production tasks. |
| | | import assert from 'node:assert/strict'; |
| | | import fs from 'node:fs'; |
| | | import path from 'node:path'; |
| | | import { createRequire } from 'node:module'; |
| | | import { fileURLToPath } from 'node:url'; |
| | | import { createServer } from 'vite'; |
| | | |
| | | const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); |
| | | const require = createRequire(path.join(root, 'package.json')); |
| | | const { parse, compileScript } = require('vue/compiler-sfc'); |
| | | const ts = require('typescript'); |
| | | const { chromium } = require('C:/Users/äº/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright'); |
| | | const output = path.resolve(root, '../scripts/one-click-schedule-20261014'); |
| | | fs.mkdirSync(output, { recursive: true }); |
| | | const modal = `import {defineComponent,h,reactive,onMounted} from 'vue'; |
| | | export function useVbenModal(options){const state=reactive({open:true,locked:false,confirmDisabled:true,...options}); |
| | | const api={getData:()=>({workOrderIds:[1,2]}),setState:v=>Object.assign(state,v),lock:()=>state.locked=true,unlock:()=>state.locked=false,close:async()=>{if(options.onBeforeClose&&!options.onBeforeClose())return;state.open=false;await options.onOpenChange(false)}}; |
| | | window.qaModal={state,api,options};onMounted(()=>options.onOpenChange(true)); |
| | | const Modal=defineComponent({props:['title'],setup(props,{slots}){return()=>state.open?h('section',{class:'qa-modal'},[ |
| | | h('header',props.title),h('div',{class:'qa-content'},slots.default?.()),h('footer',[ |
| | | h('button',{class:'qa-close',disabled:state.locked,onClick:()=>api.close()},'åæ¶'), |
| | | h('button',{class:'qa-confirm',disabled:state.locked||state.confirmDisabled,onClick:()=>options.onConfirm()},state.locked?'æ£å¨æäº§â¦':state.confirmText)])]):null}});return[Modal,api]}`; |
| | | const api = `export async function previewBatchSchedule(request){window.qa.previewCalls++;await new Promise(r=>setTimeout(r,120)); |
| | | const blocked=window.qa.mode==='blocked',late=window.qa.mode==='late';return {canExecute:!blocked,previewHash:'test-hash',previewTime:'2026-10-14 15:00:00',warnings:late?['å·¥åWO-001é¢è®¡å®ææ¶é´æäºäº¤æ']:[], |
| | | orders:[{workOrderId:1,workOrderCode:'WO-001',quantity:100,taskCount:blocked?0:2,requestDate:'2026-10-16 00:00:00',endTime:'2026-10-16 12:00:00',late,error:blocked?'WO-001ï¼å·¥åºæå没æå¯ç¨å·¥ä½ç«åææè®¾å¤äº§è½ï¼è¯·å
ç»´æ¤':undefined},{workOrderId:2,workOrderCode:'WO-002',quantity:200,taskCount:blocked?0:2,requestDate:'2026-10-19 00:00:00',endTime:'2026-10-19 13:00:00',late:false,error:blocked?'WO-002ï¼å·¥åºå
è£
缺å°è®¾å¤äº§è½ï¼è¯·å
ç»´æ¤':undefined}], |
| | | tasks:blocked?[]:[{workOrderId:1,workOrderCode:'WO-001',processId:1,processName:'æå',workstationName:'æåå·¥ä½ç«A',machineryNames:'æå设å¤A01',quantity:100,hourlyCapacity:50,duration:2,startTime:'2026-10-16 09:00:00',endTime:'2026-10-16 11:00:00'},{workOrderId:1,workOrderCode:'WO-001',processId:2,processName:'å
è£
',workstationName:'å
è£
å·¥ä½ç«B',machineryNames:'å
è£
设å¤B01',quantity:100,hourlyCapacity:100,duration:1,startTime:'2026-10-16 11:00:00',endTime:'2026-10-16 12:00:00'},{workOrderId:2,workOrderCode:'WO-002',processId:1,processName:'æå',workstationName:'æåå·¥ä½ç«A',machineryNames:'æå设å¤A01',quantity:200,hourlyCapacity:100,duration:2,startTime:'2026-10-19 09:00:00',endTime:'2026-10-19 11:00:00'},{workOrderId:2,workOrderCode:'WO-002',processId:2,processName:'å
è£
',workstationName:'å
è£
å·¥ä½ç«B',machineryNames:'å
è£
设å¤B01',quantity:200,hourlyCapacity:100,duration:2,startTime:'2026-10-19 11:00:00',endTime:'2026-10-19 13:00:00'}]};} |
| | | export async function executeBatchSchedule(request){window.qa.executeCalls++;window.qa.lastRequest=request;await new Promise(r=>setTimeout(r,200));if(window.qa.mode==='executeError')throw new Error('æäº§æ°æ®å·²ååï¼è¯·éæ°çæé¢è§å确认');return{taskIds:[1,2,3,4]}}`; |
| | | const html = `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>ä¸é®æäº§äº¤äºéªè¯</title> |
| | | <style>body{margin:0;background:#f4f6f8;font-family:Microsoft YaHei,sans-serif;color:#172033}.qa-modal{width:calc(100% - 32px);max-width:1200px;margin:20px auto;background:white;border-radius:8px}.qa-modal header{padding:18px 24px;font-size:18px;font-weight:600;border-bottom:1px solid #e8ebee}.qa-content{padding:24px}.qa-modal footer{display:flex;justify-content:flex-end;gap:12px;padding:16px 24px;border-top:1px solid #e8ebee}.qa-modal button{padding:8px 16px;border:1px solid #d9d9d9;border-radius:6px;background:white;cursor:pointer}.qa-modal .qa-confirm{background:#1677ff;color:white;border-color:#1677ff}.qa-modal button:disabled{color:#8c8c8c;background:#f5f5f5;border-color:#d9d9d9;cursor:default}.space-y-5>*+*{margin-top:20px}.flex{display:flex}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-end{align-items:flex-end}.gap-3{gap:12px}.gap-2{gap:8px}.font-medium{font-weight:500}.text-sm{font-size:14px}.text-muted-foreground{color:#657083}.text-destructive{color:#cf1322}.mb-2{margin-bottom:8px}.mt-2{margin-top:8px}h3{font-size:15px;margin-top:0}p{margin:4px 0}@media(max-width:600px){.qa-content{padding:16px}.qa-modal header{padding:16px}.ant-descriptions{overflow-x:auto}}</style> |
| | | <div id="app"></div><script type="module" src="/qa-entry.js"></script></html>`; |
| | | const entry = `import{createApp,h}from'vue';import AutoSchedule from'/src/views/mes/pro/task/modules/auto-schedule.vue';window.qa={mode:new URLSearchParams(location.search).get('mode')||'valid',previewCalls:0,executeCalls:0,success:0};createApp({render:()=>h(AutoSchedule,{onSuccess:()=>window.qa.success++})}).mount('#app')`; |
| | | |
| | | const server = await createServer({ root, configFile: false, appType: 'custom', cacheDir: 'node_modules/.cache/one-click-schedule-qa', server: { host: '127.0.0.1', port: 0 }, |
| | | plugins: [{ name: 'schedule-qa', enforce: 'pre', |
| | | resolveId(id) { if (id === '@vben/common-ui') return '\0qa-modal'; if (id === '#/api/mes/pro/task') return '\0qa-api'; if (id === '/qa-entry.js') return '\0qa-entry'; }, |
| | | load(id) { if (id === '\0qa-modal') return modal; if (id === '\0qa-api') return api; if (id === '\0qa-entry') return entry; }, |
| | | transform(code, id) { if (!id.endsWith('.vue')) return; |
| | | const { descriptor } = parse(code); const script = compileScript(descriptor, { id: 'schedule-qa', inlineTemplate: true }); |
| | | return { code: ts.transpileModule(script.content, { compilerOptions: { target: ts.ScriptTarget.ESNext, module: ts.ModuleKind.ESNext } }).outputText, map: null }; |
| | | }, |
| | | configureServer(server) { server.middlewares.use((request,response,next)=>{if(request.url?.startsWith('/?')||request.url==='/'){response.setHeader('Content-Type','text/html;charset=utf-8');response.end(html)}else next()}); }, |
| | | }], optimizeDeps: { entries: [], noDiscovery: true, include: ['vue', 'ant-design-vue', 'dayjs'] } }); |
| | | await server.listen(); |
| | | let browser; |
| | | const errors=[]; |
| | | try { |
| | | browser=await chromium.launch({headless:true,executablePath:'C:/Program Files/Google/Chrome/Application/chrome.exe'}); |
| | | const page=await browser.newPage({viewport:{width:1440,height:1000}}); |
| | | page.on('pageerror',error=>errors.push(error.message)); |
| | | const url=`http://127.0.0.1:${server.httpServer.address().port}`; |
| | | await page.goto(url);await page.getByText('å·¥åæ ¡éª',{exact:true}).waitFor(); |
| | | assert.equal(await page.locator('.qa-confirm').isEnabled(),true); |
| | | assert.equal(await page.evaluate(()=>window.qa.previewCalls),1,'Opening modal must generate exactly one preview'); |
| | | await page.screenshot({path:path.join(output,'desktop-preview.png'),fullPage:true}); |
| | | await page.locator('.ant-picker').click(); |
| | | await page.locator('td[title="2026-10-20"]').click(); |
| | | await page.locator('.ant-picker-ok button').click(); |
| | | await page.waitForTimeout(150); |
| | | assert.equal(await page.locator('.qa-confirm').isEnabled(),false,'Changed earliest time must invalidate preview'); |
| | | await page.getByText('çææäº§é¢è§',{exact:true}).click();await page.getByText('å·¥åæ ¡éª',{exact:true}).waitFor(); |
| | | await page.locator('.qa-confirm').click(); |
| | | assert.equal(await page.locator('.qa-close').isEnabled(),false,'Cancel must be disabled while executing'); |
| | | await page.waitForFunction(()=>window.qa.success===1); |
| | | assert.equal(await page.evaluate(()=>window.qa.executeCalls),1,'Confirm must execute only once'); |
| | | await page.goto(url+'/?mode=blocked');await page.getByText('å·¥åæ ¡éª',{exact:true}).waitFor(); |
| | | assert.equal(await page.locator('.qa-confirm').isEnabled(),false,'Configuration errors must block submission'); |
| | | await page.setViewportSize({width:390,height:844});await page.screenshot({path:path.join(output,'mobile-blocked.png'),fullPage:true}); |
| | | await page.goto(url+'/?mode=late');await page.getByText('å·¥åæ ¡éª',{exact:true}).waitFor(); |
| | | assert.equal(await page.locator('.qa-confirm').isEnabled(),false,'Late risk requires acknowledgement'); |
| | | await page.getByText('æå·²æ ¸å¯¹å¹¶æ¥å以ä¸äº¤æå»¶æé£é©',{exact:true}).click(); |
| | | assert.equal(await page.locator('.qa-confirm').isEnabled(),true); |
| | | await page.locator('.qa-confirm').click();await page.waitForFunction(()=>window.qa.success===1); |
| | | assert.equal(await page.evaluate(()=>window.qa.lastRequest.allowLate),true); |
| | | await page.goto(url+'/?mode=executeError');await page.getByText('å·¥åæ ¡éª',{exact:true}).waitFor(); |
| | | await page.locator('.qa-confirm').click();await page.getByText('æäº§æ°æ®å·²ååï¼è¯·éæ°çæé¢è§å确认',{exact:true}).waitFor(); |
| | | assert.equal(await page.locator('.qa-confirm').isEnabled(),false,'Failed execution must discard old preview'); |
| | | assert.equal(await page.locator('.qa-close').isEnabled(),true,'Failure must unlock modal'); |
| | | assert.deepEqual(errors,[]); |
| | | const result={passed:true,scope:'Actual scheduling component, simulated APIs and modal adapter; no production writes',checks:['initial preview','time change invalidates preview','single submit','execution locks cancel','missing config blocks','late acknowledgement','server rejection unlocks and invalidates'],screenshots:['desktop-preview.png','mobile-blocked.png']}; |
| | | fs.writeFileSync(path.join(output,'frontend-result.json'),JSON.stringify(result,null,2));console.log(JSON.stringify(result)); |
| | | } catch (error) { |
| | | console.error(JSON.stringify({message:error.message,browserErrors:errors})); |
| | | throw error; |
| | | } finally { await browser?.close();await server.close(); } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // Actual Vue components with simulated API data; no production stock writes. |
| | | import assert from 'node:assert/strict'; |
| | | import fs from 'node:fs'; |
| | | import path from 'node:path'; |
| | | import {createRequire} from 'node:module'; |
| | | import {fileURLToPath} from 'node:url'; |
| | | import {createServer} from 'vite'; |
| | | const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..'); |
| | | const require=createRequire(path.join(root,'package.json')); |
| | | const {parse,compileScript}=require('vue/compiler-sfc'),ts=require('typescript'); |
| | | const {chromium}=require('C:/Users/äº/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright'); |
| | | const output=path.resolve(root,'../scripts/spare-management-20261014');fs.mkdirSync(output,{recursive:true}); |
| | | const modal=`import {defineComponent,h,reactive,onMounted} from 'vue'; |
| | | export const Page=defineComponent({setup(p,{slots}){return()=>h('main',slots.default?.())}}); |
| | | export function useVbenModal(options){ |
| | | if(options.connectedComponent||!options.onOpenChange)return[defineComponent({setup(){return()=>null}}),{setData(){return this},open(){}}]; |
| | | const state=reactive({open:true,locked:false,...options});const api={getData:()=>window.qa.data,setState:v=>Object.assign(state,v),lock:()=>state.locked=true,unlock:()=>state.locked=false,close:async()=>{if(options.onBeforeClose&&!options.onBeforeClose())return;state.open=false;await options.onOpenChange(false)}}; |
| | | window.qaModal={state,api,options};onMounted(()=>options.onOpenChange(true)); |
| | | return[defineComponent({props:['title'],setup(props,{slots}){return()=>state.open?h('section',{class:'qa-modal'},[h('header',props.title),h('div',slots.default?.()),h('footer',[h('button',{class:'qa-close',disabled:state.locked,onClick:()=>api.close()},'åæ¶'),h('button',{class:'qa-confirm',disabled:state.locked,onClick:()=>options.onConfirm().catch(()=>{})},'确认')])]):null}}),api];}`; |
| | | const api=`export const movementLabels={1:'åå§åºå',2:'å
¥åº',3:'ç»´ä¿®é¢ç¨',4:'éå'}; |
| | | export async function getSparePage(){return {list:window.qa.parts,total:window.qa.parts.length}} |
| | | export async function getSpare(){return window.qa.parts[0]} |
| | | export async function getSpareMovements(){return {list:window.qa.movements,total:window.qa.movements.length}} |
| | | export async function spareAction(type,data){window.qa.calls++;window.qa.request={type,...data};await new Promise(r=>setTimeout(r,250));if(window.qa.fail)throw Error('server rejection');return 1} |
| | | export async function createSpare(data){window.qa.saved=data;return 3} |
| | | export async function updateSpare(data){window.qa.saved=data} |
| | | export async function deleteSpare(){} |
| | | `; |
| | | const entry=`import {createApp,h} from 'vue';import 'ant-design-vue/dist/reset.css'; |
| | | import Ledger from '/src/views/mes/dv/spare/index.vue';import Records from '/src/views/mes/dv/spare/modules/records.vue';import Action from '/src/views/mes/dv/spare/modules/action.vue';import Form from '/src/views/mes/dv/spare/modules/form.vue'; |
| | | const mode=new URLSearchParams(location.search).get('mode')||'ledger'; |
| | | const parts=[{id:1,code:'SP-001',name:'æ·±æ²çè½´æ¿',specification:'6205-2RS',unit:'ä»¶',location:'å¤ä»¶åº A-01',stockQuantity:10,safetyQuantity:15,status:0},{id:2,code:'SP-002',name:'ä¼ å¨ç®å¸¦',specification:'B-1250',unit:'æ¡',location:'å¤ä»¶åº B-02',stockQuantity:25,safetyQuantity:5,status:0}]; |
| | | const movement={id:1,sparePartId:1,sparePartCode:'SP-001',sparePartName:'æ·±æ²çè½´æ¿',specification:'6205-2RS',unit:'ä»¶',type:3,quantity:4,returnedQuantity:1,returnableQuantity:3,repairCode:'WX-20261014-001',repairId:1,machineryName:'æ°æ§å å·¥ä¸å¿',receiverName:'ç»´ä¿®åç²',remark:'æ´æ¢ä¸»è½´è½´æ¿',createTime:'2026-10-14 15:30:00'}; |
| | | window.qa={parts,movements:[movement],calls:0,data:mode==='return'?{type:'return',original:movement}:mode==='form'?{}:{type:mode==='receive'?'receive':'issue',part:parts[0],repairId:1,repairCode:'WX-20261014-001'}}; |
| | | const C=mode==='ledger'?Ledger:mode==='records'?Records:mode==='form'?Form:Action; |
| | | const app=createApp({render:()=>mode==='records'?h('main',[h(C)]):h(C)});app.directive('access',{});app.mount('#app');`; |
| | | const html=`<!doctype html><html lang="zh-CN"><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>å¤ä»¶ç»ä»¶éªè¯ · æ¨¡ææ°æ®</title><style>body{margin:0;background:#f5f5f5;color:#1f1f1f;font:14px system-ui}main{padding:24px;background:white;margin:20px}.qa-modal{background:white;max-width:600px;margin:24px auto;padding:24px}header{font-size:20px;font-weight:600;margin-bottom:20px}footer{display:flex;gap:12px;justify-content:flex-end;padding-top:16px}footer button{padding:8px 20px}footer button:disabled{opacity:.5}@media(max-width:640px){main{margin:8px;padding:12px}.qa-modal{margin:8px;padding:16px}} .flex{display:flex}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-3{gap:12px}.gap-y-3{row-gap:12px}.mb-4{margin-bottom:16px}.mb-3{margin-bottom:12px}.mt-4{margin-top:16px}.w-full{width:100%}.w-28{width:112px}.w-36{width:144px}.text-lg{font-size:18px}.text-base{font-size:16px}.font-semibold{font-weight:600}.grid{display:grid}.gap-x-4{column-gap:16px}.ml-2{margin-left:8px}@media(min-width:640px){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:col-span-2{grid-column:span 2}}</style><div id="app"></div><script type="module" src="/qa-entry.js"></script></html>`; |
| | | const server=await createServer({root,configFile:false,appType:'custom',cacheDir:'node_modules/.cache/spare-management-qa',server:{host:'127.0.0.1',port:0},plugins:[{name:'spare-qa',enforce:'pre', |
| | | resolveId(id){if(id==='@vben/common-ui')return '\0qa-modal';if(id==='#/api/mes/dv/spare')return '\0qa-api';if(id==='#/api/mes/dv/repair')return '\0qa-repair';if(id==='#/api/system/user')return '\0qa-user';if(id==='/qa-entry.js')return '\0qa-entry'}, |
| | | load(id){if(id==='\0qa-modal')return modal;if(id==='\0qa-api')return api;if(id==='\0qa-repair')return 'export async function getRepairPage(){return {list:[{id:1,code:"WX-001",status:0,machineryName:"设å¤A"}]}}';if(id==='\0qa-user')return 'export async function getSimpleUserList(){return [{id:1,nickname:"ç»´ä¿®åç²"}]}';if(id==='\0qa-entry')return entry}, |
| | | transform(code,id){if(!id.endsWith('.vue'))return;const {descriptor}=parse(code);const script=compileScript(descriptor,{id,inlineTemplate:true});return{code:ts.transpileModule(script.content,{compilerOptions:{target:ts.ScriptTarget.ESNext,module:ts.ModuleKind.ESNext}}).outputText,map:null}}, |
| | | configureServer(s){s.middlewares.use((req,res,next)=>{if(req.url==='/'||req.url?.startsWith('/?')){res.setHeader('Content-Type','text/html;charset=utf-8');res.end(html)}else next()})} |
| | | }],optimizeDeps:{entries:[],noDiscovery:true,include:['vue','ant-design-vue']}}); |
| | | await server.listen();let browser;const errors=[]; |
| | | try{ |
| | | browser=await chromium.launch({headless:true,executablePath:'C:/Program Files/Google/Chrome/Application/chrome.exe'});const page=await browser.newPage({viewport:{width:1440,height:950}}); |
| | | page.on('pageerror',e=>errors.push(e.message));const url='http://127.0.0.1:'+server.httpServer.address().port; |
| | | await page.goto(url);await page.getByText('æ·±æ²çè½´æ¿',{exact:true}).waitFor();await page.screenshot({path:path.join(output,'desktop-ledger.png'),fullPage:true}); |
| | | await page.goto(url+'/?mode=records');await page.getByText('æ´æ¢ä¸»è½´è½´æ¿',{exact:true}).waitFor();await page.setViewportSize({width:390,height:844});await page.screenshot({path:path.join(output,'mobile-records.png'),fullPage:true}); |
| | | await page.goto(url+'/?mode=issue');await page.locator('.qa-confirm').waitFor();await page.locator('.qa-confirm').click();await page.waitForTimeout(100);assert.equal(await page.evaluate(()=>window.qa.calls),0,'Missing receiver and reason blocks issue'); |
| | | await page.locator('#form_item_receiverUserId').click();await page.locator('.ant-select-item-option-content').filter({hasText:'ç»´ä¿®åç²'}).click(); |
| | | await page.locator('textarea').fill('æ´æ¢ä¸»è½´è½´æ¿');await page.locator('.ant-input-number-input').fill('11');await page.locator('.qa-confirm').click();await page.getByText('è¶
è¿å½åå¯ç¨åºå',{exact:true}).waitFor();assert.equal(await page.evaluate(()=>window.qa.calls),0); |
| | | await page.locator('.ant-input-number-input').fill('2');await page.setViewportSize({width:1440,height:950});await page.screenshot({path:path.join(output,'desktop-issue.png'),fullPage:true}); |
| | | await page.locator('.qa-confirm').click();await page.waitForFunction(()=>window.qa.calls===1);assert.equal(await page.locator('.qa-close').isEnabled(),false);await page.waitForFunction(()=>!window.qaModal.state.open);assert.equal(await page.evaluate(()=>window.qa.calls),1); |
| | | await page.goto(url+'/?mode=return');await page.locator('.qa-confirm').waitFor();await page.locator('textarea').fill('æªä½¿ç¨éå');await page.locator('.ant-input-number-input').fill('4');await page.locator('.qa-confirm').click();await page.getByText('è¶
è¿å¯éåæ°é',{exact:true}).waitFor();assert.equal(await page.evaluate(()=>window.qa.calls),0); |
| | | await page.locator('.ant-input-number-input').fill('2');await page.evaluate(()=>window.qa.fail=true);await page.locator('.qa-confirm').click();await page.waitForFunction(()=>window.qa.calls===1&&!window.qaModal.state.locked);assert.equal(await page.locator('.qa-close').isEnabled(),true); |
| | | const oldKey=await page.evaluate(()=>window.qa.request.requestKey);await page.evaluate(()=>window.qa.fail=false);await page.locator('.qa-confirm').click();await page.waitForFunction(()=>!window.qaModal.state.open);assert.equal(await page.evaluate(()=>window.qa.request.requestKey),oldKey,'Failed submission retry uses same idempotency key'); |
| | | await page.goto(url+'/?mode=form');await page.locator('.qa-confirm').waitFor();await page.locator('.qa-confirm').click();await page.waitForTimeout(80);assert.equal(await page.evaluate(()=>window.qa.saved),undefined,'Required fields block empty part creation'); |
| | | await page.goto(url+'/?mode=issue');await page.locator('.qa-confirm').waitFor();await page.locator('#form_item_receiverUserId').click();await page.locator('.ant-select-item-option-content').filter({hasText:'ç»´ä¿®åç²'}).click();await page.locator('textarea').fill('æ´æ¢ä¸»è½´è½´æ¿');await page.locator('.ant-input-number-input').fill('2');await page.locator('header').click();await page.waitForTimeout(300);await page.screenshot({path:path.join(output,'desktop-issue.png'),fullPage:true}); |
| | | assert.deepEqual(errors,[]);const result={passed:true,scope:'Actual Vue/Ant Design components with mock APIs and modal adapter; no production writes',checks:['ledger display and shortage','records mobile display','required issue fields','issue stock limit','single submit and cancel lock','return limit','failure unlock','retry idempotency key','required part fields'],screenshots:['desktop-ledger.png','mobile-records.png','desktop-issue.png']};fs.writeFileSync(path.join(output,'frontend-result.json'),JSON.stringify(result,null,2));console.log(JSON.stringify(result)); |
| | | }finally{await browser?.close();await server.close()} |
| | | |
| | | |
| | | |
| | | |
| | | |
| | |
| | | import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui'; |
| | | import { $t } from '@vben/locales'; |
| | | import { isMobile } from '@vben/utils'; |
| | | import { watch } from 'vue'; |
| | | |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import { isAuditField } from '#/utils/audit-ui-policy'; |
| | | |
| | | async function initSetupVbenForm() { |
| | | setupVbenForm<ComponentType>({ |
| | |
| | | }); |
| | | } |
| | | |
| | | const useVbenForm = useForm<ComponentType, ComponentPropsMap>; |
| | | const useVbenForm = (options: FormProps<ComponentType, ComponentPropsMap>) => { |
| | | const result = useForm<ComponentType, ComponentPropsMap>(options); |
| | | const schema = options.schema; |
| | | if (schema) { |
| | | watch(automaticApproval, () => { |
| | | result[1].setState({ schema: schema.filter((item) => !automaticApproval.value || !isAuditField(item)) }); |
| | | }, { immediate: true }); |
| | | } |
| | | return result; |
| | | }; |
| | | |
| | | export { initSetupVbenForm, useVbenForm, z }; |
| | | |
| | |
| | | |
| | | import type { ComponentPropsMap, ComponentType } from './component'; |
| | | |
| | | import { h } from 'vue'; |
| | | import { h, watch } from 'vue'; |
| | | |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | import { $te } from '@vben/locales'; |
| | |
| | | |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | import { $t } from '#/locales'; |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import { isAuditField } from '#/utils/audit-ui-policy'; |
| | | |
| | | import { useVbenForm } from './form'; |
| | | |
| | |
| | | export * from '#/components/table-action'; |
| | | export const useVbenVxeGrid = <T extends Record<string, any>>( |
| | | ...rest: Parameters<typeof useGrid<T, ComponentType, ComponentPropsMap>> |
| | | ) => useGrid<T, ComponentType, ComponentPropsMap>(...rest); |
| | | ) => { |
| | | const options = rest[0]; |
| | | const columns = options.gridOptions?.columns; |
| | | const schema = options.formOptions?.schema; |
| | | const applyPolicy = () => ({ |
| | | ...(columns ? { gridOptions: { columns: columns.filter((item) => !automaticApproval.value || !isAuditField(item)) } } : {}), |
| | | ...(schema ? { formOptions: { schema: schema.filter((item) => !automaticApproval.value || !isAuditField(item)) } } : {}), |
| | | }); |
| | | const result = useGrid<T, ComponentType, ComponentPropsMap>(options); |
| | | watch(automaticApproval, () => result[1].setState(applyPolicy()), { immediate: true }); |
| | | return result; |
| | | }; |
| | | |
| | | export type * from '@vben/plugins/vxe-table'; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { InternalAxiosRequestConfig } from '@vben/request'; |
| | | |
| | | import { |
| | | defaultResponseInterceptor, |
| | | RequestClient, |
| | | } from '@vben/request'; |
| | | import { useAccessStore } from '@vben/stores'; |
| | | |
| | | import { |
| | | bindImportAttachment, |
| | | createImportSession, |
| | | } from '#/api/mdm/item/agent'; |
| | | import { uploadFile } from '#/api/system/storage'; |
| | | |
| | | /** |
| | | * æ¨¡åæ°æ®è¡¥å½ä¼è¯æå¡ï¼Pythonï¼ä¸å¨ /admin-api ä¸ï¼ |
| | | * å¼åæç± Vite 代ç /agent-api 转åå° 127.0.0.1:8091ï¼ä¸ä¸è¿å CommonResult å
è£
ã |
| | | */ |
| | | const agentClient = new RequestClient({ |
| | | baseURL: '/agent-api', |
| | | responseReturn: 'body', |
| | | timeout: 120_000, |
| | | }); |
| | | |
| | | agentClient.addRequestInterceptor({ |
| | | fulfilled: (config: InternalAxiosRequestConfig) => { |
| | | const token = useAccessStore().accessToken; |
| | | if (token) { |
| | | config.headers.Authorization = `Bearer ${token}`; |
| | | } |
| | | return config; |
| | | }, |
| | | }); |
| | | |
| | | // Python æå¡ç´æ¥è¿åä¸å¡ JSONï¼ä¸å¥ CommonResultï¼responseReturn: 'body' æ¶åæ ·è¿åååºä½ã |
| | | agentClient.addResponseInterceptor( |
| | | defaultResponseInterceptor({ |
| | | codeField: 'code', |
| | | dataField: 'data', |
| | | successCode: 0, |
| | | }), |
| | | ); |
| | | |
| | | export namespace AgentConversationApi { |
| | | export interface FieldSpec { |
| | | name: string; |
| | | label: string; |
| | | required: boolean; |
| | | kind: string; |
| | | } |
| | | |
| | | export interface ModuleSpec { |
| | | module_id: string; |
| | | title: string; |
| | | capability: string; |
| | | bridge: string; |
| | | permission: string; |
| | | description: string; |
| | | required_fields: string[]; |
| | | fields: FieldSpec[]; |
| | | } |
| | | |
| | | export interface Capabilities { |
| | | version: string; |
| | | formats: string[]; |
| | | ocr: boolean; |
| | | cross_file_dedupe: boolean; |
| | | import_modes: string[]; |
| | | notes: string[]; |
| | | modules: ModuleSpec[]; |
| | | durable_tasks?: { |
| | | enabled: boolean; |
| | | events: string; |
| | | fact_source: string; |
| | | transport: string; |
| | | }; |
| | | } |
| | | |
| | | export interface AuthorizedCapability { |
| | | tool: string; |
| | | module: string; |
| | | operation: string; |
| | | allowed_fields: string[]; |
| | | scope_summary: string; |
| | | can_execute: boolean; |
| | | } |
| | | |
| | | export interface AuthorizedCapabilityCatalog { |
| | | policy_revision: string; |
| | | authorization_scope: string; |
| | | scope_summary: string; |
| | | capabilities: AuthorizedCapability[]; |
| | | } |
| | | |
| | | export interface BusinessModelInfo { |
| | | id: number; |
| | | name: string; |
| | | model: string; |
| | | platform: string; |
| | | temperature?: number | null; |
| | | max_tokens?: number | null; |
| | | max_contexts?: number | null; |
| | | } |
| | | |
| | | export interface Issue { |
| | | code?: string; |
| | | field?: string; |
| | | message: string; |
| | | candidate_id?: string; |
| | | } |
| | | |
| | | export interface Summary { |
| | | total?: number; |
| | | create?: number; |
| | | skip?: number; |
| | | blocked?: number; |
| | | success?: number; |
| | | failed?: number; |
| | | skipped?: number; |
| | | unknown?: number; |
| | | } |
| | | |
| | | export interface ReceiptRow { |
| | | candidate_id: string; |
| | | row_no?: number; |
| | | code?: string; |
| | | name?: string; |
| | | action: string; |
| | | status: string; |
| | | target_item_id?: number; |
| | | error_code?: string; |
| | | error_message?: string; |
| | | } |
| | | |
| | | export interface Receipt { |
| | | command_id: string; |
| | | idempotency_key: string; |
| | | status: string; |
| | | attachment_id: number; |
| | | source_sha256: string; |
| | | preflight_hash: string; |
| | | summary: Summary; |
| | | rows: ReceiptRow[]; |
| | | create_time?: string; |
| | | completed_time?: string; |
| | | error_code?: string; |
| | | error_message?: string; |
| | | } |
| | | |
| | | export interface ModuleCandidate { |
| | | module_id: string; |
| | | title: string; |
| | | capability: string; |
| | | description: string; |
| | | } |
| | | |
| | | export interface MissingField { |
| | | name: string; |
| | | label: string; |
| | | } |
| | | |
| | | /** å¡çåæ®µæ kind å¤ç¨ï¼æªä½¿ç¨çåæ®µä¸ºç©ºã */ |
| | | export interface Card { |
| | | kind: string; |
| | | title?: string; |
| | | body?: string; |
| | | module_id?: string; |
| | | module_title?: string; |
| | | capability?: string; |
| | | revision?: number; |
| | | issues?: Issue[]; |
| | | warnings?: Issue[]; |
| | | repair_guidance?: Array<{ field?: string; guidance: string }>; |
| | | summary?: Summary; |
| | | can_commit?: boolean; |
| | | side_effects?: string[]; |
| | | sync_mes?: boolean | null; |
| | | total?: number; |
| | | actions?: string[]; |
| | | notice?: string; |
| | | candidates?: ModuleCandidate[]; |
| | | missing_fields?: MissingField[]; |
| | | columns?: string[]; |
| | | command_id?: string; |
| | | status?: string; |
| | | rows?: ReceiptRow[]; |
| | | } |
| | | |
| | | export interface Message { |
| | | message_id: string; |
| | | role: string; |
| | | type: string; |
| | | text: string; |
| | | cards?: Card[]; |
| | | } |
| | | |
| | | export interface Interrupt { |
| | | interrupt_id: string; |
| | | kind: string; |
| | | revision: number; |
| | | validation_revision?: number | null; |
| | | card: Card; |
| | | } |
| | | |
| | | export interface RowView { |
| | | candidate_id: string; |
| | | values: Record<string, string>; |
| | | issues: unknown[]; |
| | | warnings: unknown[]; |
| | | } |
| | | |
| | | export interface ConversationAttachment { |
| | | attachment_id: string; |
| | | /** ä»
ææ°éä»¶è§£æåºæä»¶åï¼åå²éä»¶å¯è½ä¸ºç©ºã */ |
| | | filename: string; |
| | | } |
| | | |
| | | export interface Conversation { |
| | | conversation_id: string; |
| | | title: string; |
| | | status: string; |
| | | module_id: string; |
| | | module_title: string; |
| | | capability: string; |
| | | data_scope_summary: string; |
| | | model_id?: number | null; |
| | | policy_revision: string; |
| | | revision: number; |
| | | validation_revision?: number | null; |
| | | sync_mes?: boolean; |
| | | total: number; |
| | | summary: Summary; |
| | | issues: Issue[]; |
| | | warnings: Issue[]; |
| | | missing_fields: string[]; |
| | | columns: string[]; |
| | | /** åéè¡å®é
æå¼çåæ®µï¼ç¨äºæ¸²ææç»è¡¨ã */ |
| | | row_fields: Array<{ name: string; label: string }>; |
| | | attachments: ConversationAttachment[]; |
| | | messages: Message[]; |
| | | interrupts: Interrupt[]; |
| | | /** æªæäº¤æ¶å端è¿å空对象 {}ï¼æäº¤åææå®æ´åæ§ã */ |
| | | receipt?: null | Partial<Receipt>; |
| | | last_error: string; |
| | | rows: RowView[]; |
| | | created_at: string; |
| | | updated_at: string; |
| | | finished: boolean; |
| | | commit_enabled: boolean; |
| | | mes_sync_default: boolean; |
| | | } |
| | | |
| | | export interface ConversationSummary { |
| | | conversation_id: string; |
| | | title: string; |
| | | status: string; |
| | | module_id: string; |
| | | module_title: string; |
| | | revision: number; |
| | | created_at: string; |
| | | updated_at: string; |
| | | } |
| | | |
| | | export interface PageResult { |
| | | items: ConversationSummary[]; |
| | | total: number; |
| | | page: number; |
| | | page_size: number; |
| | | } |
| | | |
| | | export interface ConversationEvent { |
| | | conversation_id: string; |
| | | payload: Record<string, unknown>; |
| | | seq: number; |
| | | type: |
| | | | 'card' |
| | | | 'error' |
| | | | 'message' |
| | | | 'progress' |
| | | | 'receipt' |
| | | | 'resync' |
| | | | 'status'; |
| | | } |
| | | |
| | | export interface CreateReq { |
| | | title?: string; |
| | | text?: string; |
| | | attachment_id?: string; |
| | | module_id?: string; |
| | | model_id?: number; |
| | | constraints?: ConversationConstraints; |
| | | } |
| | | |
| | | export interface ConversationConstraints { |
| | | scenario_paths: string[]; |
| | | time_start?: string; |
| | | time_end?: string; |
| | | record_limit?: number; |
| | | industry?: string; |
| | | skip_rest_days?: boolean; |
| | | rest_day_description?: string; |
| | | region?: string; |
| | | instruction?: string; |
| | | priority_fields: string[]; |
| | | } |
| | | |
| | | export interface ActionReq { |
| | | interrupt_id: string; |
| | | revision: number; |
| | | action: string; |
| | | payload: Record<string, unknown>; |
| | | model_id?: number; |
| | | } |
| | | |
| | | export interface MessageReq { |
| | | text?: string; |
| | | attachment_id?: string; |
| | | model_id?: number; |
| | | } |
| | | } |
| | | |
| | | export namespace AgentTaskApi { |
| | | export interface Progress { |
| | | mode: 'determinate' | 'indeterminate'; |
| | | completed: number; |
| | | total: null | number; |
| | | percent: null | number; |
| | | unit?: string; |
| | | } |
| | | |
| | | export interface Result { |
| | | attachment_id?: number; |
| | | attachment_ids?: number[]; |
| | | candidate_count?: number; |
| | | continuation_supported?: boolean; |
| | | evidence_ref?: { |
| | | backend: string; |
| | | candidate_count?: number; |
| | | revision: string; |
| | | task_id: string; |
| | | } | null; |
| | | filename?: string; |
| | | filenames?: string[]; |
| | | module_id?: string; |
| | | module_title?: string; |
| | | result_kind?: string; |
| | | source_sha256?: string; |
| | | source_sha256s?: string[]; |
| | | review_summary?: { |
| | | conflict_groups: number; |
| | | exact_duplicates_merged: number; |
| | | incomplete_keys: number; |
| | | requires_review: boolean; |
| | | }; |
| | | warnings?: string[]; |
| | | } |
| | | |
| | | export interface Task { |
| | | task_id: string; |
| | | owner_id: string; |
| | | kind: string; |
| | | status: string; |
| | | stage: string; |
| | | attempt: number; |
| | | sequence: number; |
| | | progress: Progress; |
| | | message: string; |
| | | result: null | Result; |
| | | error: null | { code?: string; message?: string }; |
| | | allowed_actions: string[]; |
| | | created_at: null | string; |
| | | updated_at: null | string; |
| | | started_at: null | string; |
| | | finished_at: null | string; |
| | | } |
| | | |
| | | export interface PageResult { |
| | | items: Task[]; |
| | | total: number; |
| | | page: number; |
| | | page_size: number; |
| | | } |
| | | |
| | | export interface CreateReq { |
| | | attachment_id?: number; |
| | | attachment_ids?: number[]; |
| | | module_id?: string; |
| | | constraints?: AgentConversationApi.ConversationConstraints; |
| | | } |
| | | } |
| | | |
| | | /** è½å声æï¼æ¯æçæ ¼å¼ã模åä¸éå¶ */ |
| | | export function getAgentCapabilities() { |
| | | return agentClient.get<AgentConversationApi.Capabilities>('/v1/capabilities'); |
| | | } |
| | | |
| | | /** 读åå½åç»å½ç¨æ·ç» Java å端è£åªåç坿¥è¯¢ã坿ä½è½åã */ |
| | | export function getAuthorizedAgentCapabilities() { |
| | | return agentClient.get<AgentConversationApi.AuthorizedCapabilityCatalog>( |
| | | '/v1/authorization/capabilities', |
| | | ); |
| | | } |
| | | |
| | | /** å½åæ°æ®è¡¥å½é¾è·¯å®é
使ç¨çä¸å¡æ¨¡åï¼ä¸è¿å API å¯é¥ææå¡å°åã */ |
| | | export function getActiveBusinessModel() { |
| | | return agentClient.get<AgentConversationApi.BusinessModelInfo>('/v1/model'); |
| | | } |
| | | |
| | | /** å¯åæ¢çä¸å¡æ¨¡ååè¡¨ï¼æå端æåºè¿åï¼é»è®¤ä½¿ç¨ç¬¬ä¸ä¸ªã */ |
| | | export function getAgentModels() { |
| | | return agentClient.get<AgentConversationApi.BusinessModelInfo[]>('/v1/models'); |
| | | } |
| | | |
| | | /** å页读åå½åç¨æ·çæä¹
åå°ä»»å¡ã */ |
| | | export function listAgentTasks(params?: { |
| | | page?: number; |
| | | page_size?: number; |
| | | status?: string; |
| | | }) { |
| | | return agentClient.get<AgentTaskApi.PageResult>('/v1/tasks', { params }); |
| | | } |
| | | |
| | | /** åå»ºææ¡£åå°åæä»»å¡ï¼æå¡ç«¯ç«å³è¿å HTTP 202 å¿«ç
§ã */ |
| | | export function createDocumentTask(data: AgentTaskApi.CreateReq) { |
| | | return agentClient.post<AgentTaskApi.Task>('/v1/tasks/document-onboarding', data); |
| | | } |
| | | |
| | | /** 读åä»»å¡äºå®å¿«ç
§ã */ |
| | | export function getAgentTask(taskId: string) { |
| | | return agentClient.get<AgentTaskApi.Task>(`/v1/tasks/${taskId}`); |
| | | } |
| | | |
| | | /** åæ¶ä»»å¡å¹¶æ¤éçæå§æç¥¨æ®ã */ |
| | | export function cancelAgentTask(taskId: string) { |
| | | return agentClient.delete<AgentTaskApi.Task>(`/v1/tasks/${taskId}`); |
| | | } |
| | | |
| | | /** 使ç¨è®¤è¯è¯·æ±è¯»åä»»å¡ SSEï¼æ¸¸æ ç¨äºæçº¿ç»ä¼ ã */ |
| | | export async function streamAgentTaskEvents( |
| | | taskId: string, |
| | | since: number, |
| | | signal: AbortSignal, |
| | | onEvent: (event: AgentTaskApi.Task) => void, |
| | | ) { |
| | | const token = useAccessStore().accessToken; |
| | | const response = await fetch( |
| | | `/agent-api/v1/tasks/${encodeURIComponent(taskId)}/events?since=${since}`, |
| | | { |
| | | credentials: 'same-origin', |
| | | headers: { |
| | | Accept: 'text/event-stream', |
| | | ...(token ? { Authorization: `Bearer ${token}` } : {}), |
| | | }, |
| | | signal, |
| | | }, |
| | | ); |
| | | if (!response.ok) throw new Error(`ä»»å¡äºä»¶è¿æ¥å¤±è´¥ï¼HTTP ${response.status}ï¼`); |
| | | if (!response.body) throw new Error('å½åæµè§å¨ä¸æ¯ææµå¼äºä»¶'); |
| | | const reader = response.body.getReader(); |
| | | const decoder = new TextDecoder(); |
| | | let buffer = ''; |
| | | let cursor = since; |
| | | while (true) { |
| | | const { done, value } = await reader.read(); |
| | | buffer += decoder.decode(value, { stream: !done }).replaceAll('\r\n', '\n'); |
| | | const frames = buffer.split('\n\n'); |
| | | buffer = frames.pop() ?? ''; |
| | | for (const frame of frames) { |
| | | const data = frame |
| | | .split('\n') |
| | | .filter((line) => line.startsWith('data:')) |
| | | .map((line) => line.slice(5).trimStart()) |
| | | .join('\n'); |
| | | if (!data) continue; |
| | | const event = JSON.parse(data) as AgentTaskApi.Task; |
| | | if (!Number.isSafeInteger(event.sequence) || event.sequence <= cursor) continue; |
| | | cursor = event.sequence; |
| | | onEvent(event); |
| | | } |
| | | if (done) break; |
| | | } |
| | | return cursor; |
| | | } |
| | | |
| | | /** ä¼è¯å表 */ |
| | | export function listAgentConversations(params?: { |
| | | page?: number; |
| | | page_size?: number; |
| | | status?: string; |
| | | }) { |
| | | return agentClient.get<AgentConversationApi.PageResult>( |
| | | '/v1/conversations', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å»ºè¡¥å½ä¼è¯ */ |
| | | export function createAgentConversation(data: AgentConversationApi.CreateReq) { |
| | | return agentClient.post<AgentConversationApi.Conversation>( |
| | | '/v1/conversations', |
| | | data, |
| | | ); |
| | | } |
| | | |
| | | /** 读åä¼è¯ */ |
| | | export function getAgentConversation(conversationId: string) { |
| | | return agentClient.get<AgentConversationApi.Conversation>( |
| | | `/v1/conversations/${conversationId}`, |
| | | ); |
| | | } |
| | | |
| | | /** å é¤ä¼è¯ï¼åæ¶æ¸
çæ£æ¥ç¹ãäºä»¶ä¸ Java ä¾§è¡¥å½éä»¶ */ |
| | | export function deleteAgentConversation(conversationId: string) { |
| | | return agentClient.delete<{ |
| | | attachments_deleted: number; |
| | | conversation_id: string; |
| | | deleted: boolean; |
| | | }>(`/v1/conversations/${conversationId}`); |
| | | } |
| | | |
| | | /** |
| | | * 读åä¼è¯éä»¶ãmode 为 inline æ¶ PDF/å¾çç±æµè§å¨ç´æ¥é¢è§ï¼ |
| | | * å
¶ä½ç±»åæå¡ç«¯ä¼èªå¨é级为ä¸è½½ã |
| | | */ |
| | | export function downloadAgentConversationAttachment( |
| | | conversationId: string, |
| | | attachmentId: string, |
| | | mode: 'download' | 'inline', |
| | | ) { |
| | | return agentClient.download<{ |
| | | data: Blob; |
| | | headers: Record<string, string>; |
| | | }>(`/v1/conversations/${conversationId}/attachment`, { |
| | | params: { attachment_id: attachmentId, mode }, |
| | | responseReturn: 'raw', |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * ä½¿ç¨ fetch 读å带认è¯å¤´ç SSEãEventSource ä¸è½è®¾ç½® Authorizationï¼ä¸è½ç¨äºæ¬æ¥å£ã |
| | | * æå¡ç«¯ä¼ä¸»å¨ç»æçè¿æ¥ï¼è°ç¨æ¹è´è´£æºå¸¦è¿å游æ éè¿ã |
| | | */ |
| | | export async function streamAgentConversationEvents( |
| | | conversationId: string, |
| | | since: number, |
| | | signal: AbortSignal, |
| | | onEvent: (event: AgentConversationApi.ConversationEvent) => void, |
| | | onOpen?: () => void, |
| | | ) { |
| | | const token = useAccessStore().accessToken; |
| | | const response = await fetch( |
| | | `/agent-api/v1/conversations/${encodeURIComponent(conversationId)}/events?since=${since}`, |
| | | { |
| | | credentials: 'same-origin', |
| | | headers: { |
| | | Accept: 'text/event-stream', |
| | | ...(token ? { Authorization: `Bearer ${token}` } : {}), |
| | | }, |
| | | signal, |
| | | }, |
| | | ); |
| | | if (!response.ok) { |
| | | throw new Error(`äºä»¶è¿æ¥å¤±è´¥ï¼HTTP ${response.status}ï¼`); |
| | | } |
| | | if (!response.body) { |
| | | throw new Error('å½åæµè§å¨ä¸æ¯ææµå¼äºä»¶'); |
| | | } |
| | | onOpen?.(); |
| | | const reader = response.body.getReader(); |
| | | const decoder = new TextDecoder(); |
| | | let buffer = ''; |
| | | let cursor = since; |
| | | while (true) { |
| | | const { done, value } = await reader.read(); |
| | | buffer += decoder.decode(value, { stream: !done }).replaceAll('\r\n', '\n'); |
| | | const frames = buffer.split('\n\n'); |
| | | buffer = frames.pop() ?? ''; |
| | | for (const frame of frames) { |
| | | const data = frame |
| | | .split('\n') |
| | | .filter((line) => line.startsWith('data:')) |
| | | .map((line) => line.slice(5).trimStart()) |
| | | .join('\n'); |
| | | if (!data) continue; |
| | | const event = JSON.parse(data) as AgentConversationApi.ConversationEvent; |
| | | if (!Number.isSafeInteger(event.seq) || event.seq <= cursor) continue; |
| | | cursor = event.seq; |
| | | onEvent(event); |
| | | } |
| | | if (done) break; |
| | | } |
| | | return cursor; |
| | | } |
| | | |
| | | /** åéææ¬æéä»¶ï¼ç¨äºè¡¥å
ä¸ä¼ æä»¶ï¼ */ |
| | | export function sendAgentMessage( |
| | | conversationId: string, |
| | | data: AgentConversationApi.MessageReq, |
| | | ) { |
| | | return agentClient.post<AgentConversationApi.Conversation>( |
| | | `/v1/conversations/${conversationId}/messages`, |
| | | data, |
| | | ); |
| | | } |
| | | |
| | | /** å¡çæä½ï¼éæ©åºæ¯ã确认æ å°ã修订ã确认æåæ¶ */ |
| | | export function sendAgentAction( |
| | | conversationId: string, |
| | | data: AgentConversationApi.ActionReq, |
| | | ) { |
| | | return agentClient.post<AgentConversationApi.Conversation>( |
| | | `/v1/conversations/${conversationId}/actions`, |
| | | data, |
| | | ); |
| | | } |
| | | |
| | | /** ä¸è½½å¯¼å
¥æ¥è¡¨ï¼xlsxï¼ */ |
| | | export function downloadAgentReport(conversationId: string) { |
| | | return agentClient.download<{ |
| | | data: Blob; |
| | | headers: Record<string, string>; |
| | | }>(`/v1/conversations/${conversationId}/report`, { |
| | | responseReturn: 'raw', |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * ä¸ä¼ éä»¶å° Java åå¨å¹¶ç»å®è¡¥å½ä¼è¯ï¼è¿å attachmentIdã |
| | | * Python ä¾§éè¿ Java Bridge æ attachmentId 读åæä»¶ã |
| | | */ |
| | | export async function uploadAgentAttachment(file: File) { |
| | | const sessionId = await createImportSession(); |
| | | const blobs = await uploadFile([file]); |
| | | const blobId = blobs?.[0]?.id; |
| | | if (!blobId) { |
| | | throw new Error('æä»¶ä¸ä¼ å¤±è´¥ï¼æªåå¾ blobId'); |
| | | } |
| | | return await bindImportAttachment(sessionId, blobId); |
| | | } |
| | | |
| | | export { agentImportErrorMessage } from '#/api/mdm/item/agent'; |
| | |
| | | model: string; // æ¨¡åæ è¯ |
| | | platform: string; // 模åå¹³å° |
| | | type: number; // 模åç±»å |
| | | capability?: string; // 模åè½åï¼text-ææ¬ï¼vision-è§è§ |
| | | sort: number; // æåº |
| | | status: number; // ç¶æ |
| | | temperature?: number; // æ¸©åº¦åæ° |
| | |
| | | } |
| | | |
| | | /** è·å¾æ¨¡åå表 */ |
| | | export function getModelSimpleList(type?: number) { |
| | | export function getModelSimpleList(type?: number, capability?: string) { |
| | | return requestClient.get<AiModelModelApi.Model[]>('/ai/model/simple-list', { |
| | | params: { |
| | | type, |
| | | capability, |
| | | }, |
| | | }); |
| | | } |
| | |
| | | return requestClient.put('/ai/model/update', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹æ¨¡åç¶æï¼å¯ç¨æ¶ä¼èªå¨å
³éåç±»ååè½åçå
¶ä»æ¨¡å */ |
| | | export function updateModelStatus(id: number, status: number) { |
| | | return requestClient.put('/ai/model/update-status', { id, status }); |
| | | } |
| | | |
| | | /** å 餿¨¡å */ |
| | | export function deleteModel(id: number) { |
| | | return requestClient.delete(`/ai/model/delete?id=${id}`); |
| | |
| | | |
| | | export interface PageParams { |
| | | dashboardId?: number; |
| | | name?: string; |
| | | pageNo?: number; |
| | | pageSize?: number; |
| | | } |
| | |
| | | import { requestClient } from '#/api/request'; |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | |
| | | export namespace BpmAutoApproveApi { |
| | | /** æµç¨å
审æ¹é
ç½® */ |
| | |
| | | if (remark) { |
| | | params.append('remark', remark); |
| | | } |
| | | return requestClient.put<boolean>( |
| | | const result = await requestClient.put<boolean>( |
| | | `/bpm/process-auto-approve/update-global?${params.toString()}`, |
| | | ); |
| | | automaticApproval.value = autoApprove; |
| | | return result; |
| | | } |
| | | |
| | | /** ææµç¨å®ä¹ç Key 设置å
审æ¹å¼å
³ï¼ä¸åå¨åæ°å¢ãåå¨åæ´æ°ï¼ */ |
| | |
| | | } |
| | | |
| | | /** éåºç»å½ */ |
| | | export async function logoutApi(accessToken: string) { |
| | | export async function logoutApi(accessToken: string, tenant?: { tenantId: number | null; visitTenantId: number | null }) { |
| | | return baseRequestClient.post( |
| | | '/system/auth/logout', |
| | | {}, |
| | | { |
| | | headers: { |
| | | Authorization: `Bearer ${accessToken}`, |
| | | ...(tenant ? { 'tenant-id': tenant.tenantId, 'visit-tenant-id': tenant.visitTenantId } : {}), |
| | | }, |
| | | }, |
| | | ); |
| | |
| | | ); |
| | | } |
| | | |
| | | /** æè´éå忍¡æ¿ä¸è½½ Word æä»¶ */ |
| | | export function downloadContractDocument(id: number) { |
| | | return requestClient.download('/crm/contract/download-document', { params: { id } }); |
| | | } |
| | | |
| | | /** æ¥è¯¢åå详æ
*/ |
| | | export function getContract(id: number) { |
| | | return requestClient.get<CrmContractApi.Contract>( |
| | |
| | | ownerUserDept?: string; // è´è´£äººçé¨é¨åç§° |
| | | ownerUserDeptName?: string; // è´è´£äººçé¨é¨åç§° |
| | | lockStatus?: boolean; |
| | | dealStatus?: boolean; |
| | | autoPoolEnabled?: boolean; // æ¯å¦æµå
¥å
¬æµ·ï¼å
³éæ¶åæ¶å
¬æµ·å¯è§åº¦ |
| | | mobile: string; // ææºå· |
| | | telephone: string; // çµè¯ |
| | | qq: string; // QQ |
| | |
| | | createTime: Date; // å建æ¶é´ |
| | | updateTime: Date; // æ´æ°æ¶é´ |
| | | poolDay?: number; // è·ç¦»è¿å
¥å
¬æµ·å¤©æ° |
| | | contractCount?: number; // å·²ç¾è®¢ååæ°éï¼å®¡æ¹ç¶æä¸ºå®¡æ ¸éè¿ï¼ï¼æ²¡æå为 0 |
| | | contractTotalPrice?: null | number; // å·²ç¾è®¢ååéé¢å计ï¼å®¡æ ¸éè¿ï¼ï¼æ²¡æå为 null |
| | | } |
| | | |
| | | /** 客æ·å¯¼å
¥è¯·æ± */ |
| | |
| | | file: File; |
| | | updateSupport: boolean; |
| | | } |
| | | |
| | | /** 客æ·å¯¼å
¥ç»æ */ |
| | | export interface CustomerImportRespVO { |
| | | createCustomerNames?: string[]; // æ°å¢æåç客æ·å |
| | | updateCustomerNames?: string[]; // æ´æ°æåç客æ·å |
| | | failureCustomerNames?: Record<string, string>; // 导å
¥å¤±è´¥ç客æ·åååå |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢å®¢æ·å表 */ |
| | |
| | | return requestClient.get<PageResult<CrmCustomerApi.Customer>>( |
| | | '/crm/customer/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢ä¸å¡æ¨¡å客æ·å表ï¼ä¸å CRM è´è´£äººæéè¿æ»¤ï¼ */ |
| | | export function getBusinessCustomerPage(params: PageParam) { |
| | | return requestClient.get<PageResult<CrmCustomerApi.Customer>>( |
| | | '/crm/customer/business-page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢ä¸å¡æ¨¡åå
³è客æ·è¯¦æ
ï¼ä¸å CRM è´è´£äººæéè¿æ»¤ï¼ */ |
| | | export function getBusinessCustomer(id: number) { |
| | | return requestClient.get<CrmCustomerApi.Customer>( |
| | | `/crm/customer/business-get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | |
| | | |
| | | /** 导å
¥å®¢æ· */ |
| | | export function importCustomer(data: CrmCustomerApi.CustomerImportReqVO) { |
| | | return requestClient.upload('/crm/customer/import', data); |
| | | return requestClient.upload<CrmCustomerApi.CustomerImportRespVO>( |
| | | '/crm/customer/import', |
| | | data, |
| | | ); |
| | | } |
| | | |
| | | /** è·å客æ·ç²¾ç®ä¿¡æ¯å表 */ |
| | |
| | | /** å®¢æ·æ¾å
¥å
¬æµ· */ |
| | | export function putCustomerPool(id: number) { |
| | | return requestClient.put(`/crm/customer/put-pool?id=${id}`); |
| | | } |
| | | |
| | | /** æ´æ°å®¢æ·çæäº¤ç¶æ */ |
| | | export function updateCustomerDealStatus(id: number, dealStatus: boolean) { |
| | | return requestClient.put( |
| | | `/crm/customer/update-deal-status?id=${id}&dealStatus=${dealStatus}`, |
| | | ); |
| | | } |
| | | |
| | | /** è¿å
¥å
¬æµ·å®¢æ·æéç客æ·å表 */ |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageResult } from '@vben/request'; |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export type EnergyType = 'ELECTRICITY' | 'STEAM' | 'WATER'; |
| | | export interface EnergyRecord { |
| | | id?: number; |
| | | recordDate: string; |
| | | energyType: EnergyType; |
| | | usageArea: string; |
| | | productCategory: string; |
| | | usagePurpose: string; |
| | | quantity: number; |
| | | unitPrice: number; |
| | | amount?: number; |
| | | recorder: string; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | export interface EnergyFilters { |
| | | startDate?: string; |
| | | endDate?: string; |
| | | energyType?: EnergyType; |
| | | usageArea?: string; |
| | | productCategory?: string; |
| | | } |
| | | export interface EnergySummary { |
| | | energyType: EnergyType; |
| | | quantity: number; |
| | | amount: number; |
| | | recordCount: number; |
| | | } |
| | | export interface EnergyBucket extends EnergySummary { label: string } |
| | | export interface EnergyAnalysis { |
| | | totalAmount: number; |
| | | recordCount: number; |
| | | summary: EnergySummary[]; |
| | | trend: EnergyBucket[]; |
| | | areas: EnergyBucket[]; |
| | | } |
| | | export function getEnergyRecordPage(params: EnergyFilters & { pageNo: number; pageSize: number }) { |
| | | return requestClient.get<PageResult<EnergyRecord>>('/energy/record/page', { params }); |
| | | } |
| | | export function getEnergyRecord(id: number) { |
| | | return requestClient.get<EnergyRecord>('/energy/record/get', { params: { id } }); |
| | | } |
| | | export function createEnergyRecord(data: EnergyRecord) { |
| | | return requestClient.post<number>('/energy/record/create', data); |
| | | } |
| | | export function updateEnergyRecord(data: EnergyRecord) { |
| | | return requestClient.put('/energy/record/update', data); |
| | | } |
| | | export function deleteEnergyRecord(id: number) { |
| | | return requestClient.delete('/energy/record/delete', { params: { id } }); |
| | | } |
| | | export function getEnergyAnalysis(params: EnergyFilters & { startDate: string; endDate: string; groupBy: 'DAY' | 'MONTH' }) { |
| | | return requestClient.get<EnergyAnalysis>('/energy/record/analysis', { params }); |
| | | } |
| | | |
| | | export function exportEnergyRecords(params: EnergyFilters) { |
| | | return requestClient.download('/energy/record/export-excel', { params }); |
| | | } |
| | | export function exportEnergyAnalysis(params: EnergyFilters & { startDate: string; endDate: string; groupBy: 'DAY' | 'MONTH' }) { |
| | | return requestClient.download('/energy/record/analysis-export', { params }); |
| | | } |
| | | export type EnergyAlertMetric = 'AMOUNT' | 'QUANTITY' | 'MONTH_AMOUNT' | 'MONTH_QUANTITY'; |
| | | export interface EnergyAlertRule { |
| | | id?: number; |
| | | name: string; |
| | | energyType: EnergyType; |
| | | usageArea?: string; |
| | | productCategory?: string; |
| | | metric: EnergyAlertMetric; |
| | | threshold: number; |
| | | enabled: boolean; |
| | | recipientIds: number[]; |
| | | } |
| | | export interface EnergyAlertLog { |
| | | id: number; ruleName: string; recordDate: string; energyType: EnergyType; |
| | | scope: string; metric: EnergyAlertMetric; periodType: 'DAY' | 'MONTH'; actualValue: number; threshold: number; |
| | | recipientName: string; status: 'FAILED' | 'SENT'; errorMessage: string; createTime: string; |
| | | } |
| | | export async function getEnergyAlertRules(params: { pageNo: number; pageSize: number }) { |
| | | const result = await requestClient.get<PageResult<Omit<EnergyAlertRule, 'recipientIds'> & { recipientIds: string }>>('/energy/alert/page', { params }); |
| | | return { ...result, list: result.list.map(row => ({ ...row, recipientIds: row.recipientIds.split(',').filter(Boolean).map(Number) })) }; |
| | | } |
| | | export function getEnergyAlertLogs(params: { pageNo: number; pageSize: number; date?: string; status?: string }) { |
| | | return requestClient.get<PageResult<EnergyAlertLog>>('/energy/alert/logs', { params }); |
| | | } |
| | | export function getEnergyAlertRecipients() { |
| | | return requestClient.get<{ id: number; nickname: string }[]>('/energy/alert/recipients'); |
| | | } |
| | | export function saveEnergyAlertRule(data: EnergyAlertRule) { |
| | | return data.id ? requestClient.put('/energy/alert/update', data) : requestClient.post('/energy/alert/create', data); |
| | | } |
| | | export function deleteEnergyAlertRule(id: number) { return requestClient.delete('/energy/alert/delete', { params: { id } }); } |
| | | export function checkEnergyAlerts(date: string) { return requestClient.post<{ sent: number; failed: number }>('/energy/alert/check', undefined, { params: { date } }); } |
| | |
| | | |
| | | /** æäº¤ç¦»èç³è¯·ï¼å起审æ¹ï¼ */ |
| | | export function submitResignation(id: number) { |
| | | return requestClient.post(`/hrm/resignation/submit?id=${id}`); |
| | | return requestClient.post(`/hrm/resignation/submit?id=${id}&processDefinitionKey=hrm_resignation_approve`); |
| | | } |
| | | |
| | | /** æ¤é离èç³è¯· */ |
| | |
| | | |
| | | /** æäº¤è°å²ç³è¯·ï¼å起审æ¹ï¼ */ |
| | | export function submitTransfer(id: number) { |
| | | return requestClient.post(`/hrm/transfer/submit?id=${id}`); |
| | | return requestClient.post(`/hrm/transfer/submit?id=${id}&processDefinitionKey=hrm_transfer_approve`); |
| | | } |
| | | |
| | | /** æ¤éè°å²ç³è¯· */ |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { InternalAxiosRequestConfig } from '@vben/request'; |
| | | |
| | | import { RequestClient } from '@vben/request'; |
| | | import { useAccessStore } from '@vben/stores'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | /** |
| | | * Python æºè½ä½æå¡ä¸å¨ /admin-api ä¸ï¼å¼åæç± Vite 代ç /agent-api 转åå° 127.0.0.1:8091ã |
| | | * 该æå¡ç´æ¥è¿åä¸å¡ JSONï¼ä¸ä½¿ç¨ CommonResult å
è£
ï¼å æ¤åç¬å»ºå®¢æ·ç«¯ã |
| | | */ |
| | | const agentRequestClient = new RequestClient({ |
| | | baseURL: '/agent-api', |
| | | responseReturn: 'body', |
| | | timeout: 120_000, |
| | | }); |
| | | |
| | | agentRequestClient.addRequestInterceptor({ |
| | | fulfilled: (config: InternalAxiosRequestConfig) => { |
| | | const token = useAccessStore().accessToken; |
| | | if (token) { |
| | | config.headers.Authorization = `Bearer ${token}`; |
| | | } |
| | | return config; |
| | | }, |
| | | }); |
| | | |
| | | export namespace MdmAgentImportApi { |
| | | export type RowAction = 'CREATE' | 'SKIP' | 'BLOCKED'; |
| | | export type RowStatus = 'SUCCESS' | 'FAILED' | 'SKIPPED' | 'UNKNOWN'; |
| | | |
| | | export interface Evidence { |
| | | sheet: string; |
| | | row: number; |
| | | column: string; |
| | | raw: string; |
| | | } |
| | | |
| | | export interface Candidate { |
| | | candidate_id: string; |
| | | source_sha256: string; |
| | | values: Record<string, string>; |
| | | evidence: Record<string, Evidence>; |
| | | issues: string[]; |
| | | } |
| | | |
| | | export interface Issue { |
| | | code?: string; |
| | | field?: string; |
| | | message: string; |
| | | retryable?: boolean; |
| | | } |
| | | |
| | | export interface Resolved { |
| | | category_id?: number; |
| | | category_name?: string; |
| | | unit_measure_id?: number; |
| | | unit_measure_name?: string; |
| | | } |
| | | |
| | | export interface PreflightRow { |
| | | candidate_id: string; |
| | | row_no?: number; |
| | | code?: string; |
| | | name?: string; |
| | | action: RowAction; |
| | | resolved?: Resolved; |
| | | blockers?: Issue[]; |
| | | warnings?: Issue[]; |
| | | target_item_id?: number; |
| | | } |
| | | |
| | | export interface Preflight { |
| | | preflight_hash: string; |
| | | source: { |
| | | attachment_id: number; |
| | | session_id?: number; |
| | | source_sha256: string; |
| | | proposal_revision?: string; |
| | | }; |
| | | policy: { |
| | | update_allowed: boolean; |
| | | mes_sync_allowed: boolean; |
| | | max_rows: number; |
| | | }; |
| | | summary: { |
| | | total: number; |
| | | create: number; |
| | | skip: number; |
| | | blocked: number; |
| | | }; |
| | | can_commit: boolean; |
| | | side_effects: string[]; |
| | | rows: PreflightRow[]; |
| | | blocking_items: Issue[]; |
| | | } |
| | | |
| | | export interface Correction { |
| | | candidate_id: string; |
| | | field: 'categoryCode' | 'code' | 'name' | 'specification' | 'unitMeasureCode'; |
| | | value: string; |
| | | reason: string; |
| | | } |
| | | |
| | | export interface ReceiptRow { |
| | | candidate_id: string; |
| | | row_no?: number; |
| | | code?: string; |
| | | name?: string; |
| | | action: 'CREATE' | 'SKIP'; |
| | | status: RowStatus; |
| | | target_item_id?: number; |
| | | target_version?: string; |
| | | error_code?: string; |
| | | error_message?: string; |
| | | retryable?: boolean; |
| | | } |
| | | |
| | | export interface Receipt { |
| | | command_id: string; |
| | | idempotency_key: string; |
| | | status: 'FAILED' | 'PARTIAL_FAILED' | 'RUNNING' | 'SUCCEEDED' | 'UNKNOWN'; |
| | | attachment_id: number; |
| | | session_id?: number; |
| | | source_sha256: string; |
| | | preflight_hash: string; |
| | | proposal_hash?: string; |
| | | summary: { |
| | | total: number; |
| | | success: number; |
| | | failed: number; |
| | | skipped: number; |
| | | unknown: number; |
| | | }; |
| | | error_code?: string; |
| | | error_message?: string; |
| | | rows: ReceiptRow[]; |
| | | create_time?: string; |
| | | completed_time?: string; |
| | | } |
| | | |
| | | export interface ImportState { |
| | | task_id: string; |
| | | attachment_id: string; |
| | | filename: string; |
| | | source_sha256: string; |
| | | candidates: Candidate[]; |
| | | issues: Array<Issue & { candidate_id?: string; candidate_ids?: string[] }>; |
| | | revision: number; |
| | | status: string; |
| | | preflight?: Preflight; |
| | | preflight_hash?: string; |
| | | proposal?: Record<string, unknown>; |
| | | proposal_hash?: string; |
| | | command_id?: string; |
| | | receipt?: Receipt; |
| | | last_error?: string; |
| | | } |
| | | |
| | | export interface ReviewInterrupt { |
| | | type: string; |
| | | revision: number; |
| | | issues: Array<Issue & { candidate_id?: string; candidate_ids?: string[] }>; |
| | | candidates: Candidate[]; |
| | | preflight: { |
| | | hash?: string; |
| | | can_commit: boolean; |
| | | summary?: Preflight['summary']; |
| | | blocking_items: Issue[]; |
| | | needs_attention: PreflightRow[]; |
| | | }; |
| | | last_error?: string; |
| | | notice?: string; |
| | | } |
| | | |
| | | export interface PublicState { |
| | | state: ImportState; |
| | | interrupts: ReviewInterrupt[]; |
| | | } |
| | | |
| | | export interface ReviewReq { |
| | | expected_revision: number; |
| | | action: 'cancel' | 'confirm' | 'revise'; |
| | | corrections?: Correction[]; |
| | | expected_preflight_hash?: string; |
| | | expected_proposal_hash?: string; |
| | | } |
| | | } |
| | | |
| | | /** å建补å½ä¼è¯ï¼Javaï¼ */ |
| | | export function createImportSession() { |
| | | return requestClient.post<number>('/agent/bridge/import-sessions'); |
| | | } |
| | | |
| | | /** ç»å®éä»¶å°è¡¥å½ä¼è¯ï¼è¿å attachmentIdï¼Javaï¼ */ |
| | | export function bindImportAttachment(sessionId: number, blobId: number) { |
| | | return requestClient.post<number>( |
| | | `/agent/bridge/import-sessions/${sessionId}/attachments`, |
| | | { blobId }, |
| | | ); |
| | | } |
| | | |
| | | /** å建æºè½ä½å¯¼å
¥ä»»å¡å¹¶è§¦åè§£æä¸é¢æ£ï¼Pythonï¼ */ |
| | | export function startMaterialImport(attachmentId: number) { |
| | | return agentRequestClient.post<MdmAgentImportApi.PublicState>( |
| | | '/v1/material-imports', |
| | | { attachment_id: String(attachmentId) }, |
| | | ); |
| | | } |
| | | |
| | | /** 读åä»»å¡å½åç¶æï¼Pythonï¼ */ |
| | | export function getMaterialImportTask(taskId: string) { |
| | | return agentRequestClient.get<MdmAgentImportApi.PublicState>( |
| | | `/v1/material-imports/${taskId}`, |
| | | ); |
| | | } |
| | | |
| | | /** 修订æç¡®è®¤ï¼Pythonï¼ */ |
| | | export function reviewMaterialImport( |
| | | taskId: string, |
| | | data: MdmAgentImportApi.ReviewReq, |
| | | ) { |
| | | return agentRequestClient.post<MdmAgentImportApi.PublicState>( |
| | | `/v1/material-imports/${taskId}/review`, |
| | | data, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢æäº¤åæ§ï¼Pythonï¼ */ |
| | | export function getMaterialImportReceipt(taskId: string) { |
| | | return agentRequestClient.get<MdmAgentImportApi.Receipt>( |
| | | `/v1/material-imports/${taskId}/receipt`, |
| | | ); |
| | | } |
| | | |
| | | interface AgentErrorDetail { |
| | | error_code?: string; |
| | | message?: string; |
| | | } |
| | | |
| | | /** ä» Python é误ååºä¸æåå¯è¯»ææ¡ */ |
| | | export function agentImportErrorMessage(error: unknown): string { |
| | | const err = error as |
| | | | { data?: unknown; message?: string; response?: { data?: unknown } } |
| | | | undefined; |
| | | const body = (err?.response?.data ?? err?.data) as |
| | | | AgentErrorDetail |
| | | | string |
| | | | undefined; |
| | | const detail = |
| | | typeof body === 'string' |
| | | ? body |
| | | : ((body as { detail?: unknown } | undefined)?.detail ?? body); |
| | | if (typeof detail === 'string') { |
| | | return detail; |
| | | } |
| | | const info = detail as AgentErrorDetail | undefined; |
| | | if (info?.message) { |
| | | return info.error_code |
| | | ? `${info.message}ï¼${info.error_code}ï¼` |
| | | : info.message; |
| | | } |
| | | return err?.message || 'æºè½ä½æå¡è¯·æ±å¤±è´¥'; |
| | | } |
| | |
| | | checkResult?: number; // æ£æµç»è®ºï¼åå
¸ï¼ |
| | | certNo?: string; // è¯ä¹¦ç¼å· |
| | | checkUserId?: number; // æ£æµäººç¼å· |
| | | checkUserName?: string; // æå·¥å¡«åçæ£æµäººå§å |
| | | checkUserNickname?: string; // æ£æµäººæµç§° |
| | | nextCheckDate?: string; // 䏿¬¡æ£æµæ¥æ |
| | | remark?: string; // 夿³¨ |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | import { requestClient } from '#/api/request'; |
| | | export interface SparePart { |
| | | id: number; code: string; name: string; specification?: string; unit: string; |
| | | location?: string; stockQuantity: number; safetyQuantity: number; status: number; remark?: string; |
| | | } |
| | | export interface SpareMovement { |
| | | id: number; sparePartId: number; sparePartCode: string; sparePartName: string; |
| | | specification?: string; unit: string; type: number; quantity: number; |
| | | beforeQuantity: number; afterQuantity: number; repairId?: number; repairCode?: string; |
| | | machineryName?: string; receiverName?: string; receiverUserId?: number; operatorUserId: number; |
| | | originalId?: number; returnedQuantity: number; returnableQuantity: number; remark: string; createTime: string; |
| | | } |
| | | export interface SpareAction { |
| | | requestKey: string; sparePartId?: number; repairId?: number; originalId?: number; |
| | | receiverUserId?: number; quantity: number; remark: string; |
| | | } |
| | | export type SpareSave = Omit<SparePart, 'id' | 'stockQuantity'> & { id?: number; initialQuantity: number }; |
| | | export const getSparePage = (params: PageParam & Record<string, unknown>) => |
| | | requestClient.get<PageResult<SparePart>>('/mes/dv/spare/page', { params }); |
| | | export const getSpare = (id: number) => requestClient.get<SparePart>('/mes/dv/spare/get', { params: { id } }); |
| | | export const createSpare = (data: SpareSave) => requestClient.post<number>('/mes/dv/spare/create', data); |
| | | export const updateSpare = (data: SpareSave) => requestClient.put('/mes/dv/spare/update', data); |
| | | export const deleteSpare = (id: number) => requestClient.delete('/mes/dv/spare/delete', { params: { id } }); |
| | | export const getSpareMovements = (params: PageParam & Record<string, unknown>) => |
| | | requestClient.get<PageResult<SpareMovement>>('/mes/dv/spare/movement-page', { params }); |
| | | export const spareAction = (type: 'issue' | 'receive' | 'return', data: SpareAction) => |
| | | requestClient.post<number>(`/mes/dv/spare/${type}`, data); |
| | | export const movementLabels: Record<number, string> = { 1: 'åå§åºå', 2: 'å
¥åº', 3: 'ç»´ä¿®é¢ç¨', 4: 'éå' }; |
| | | |
| | |
| | | yesterdayOutput: number; // æ¨æ¥äº§é |
| | | todayQualifiedQuantity: number; // 仿¥åæ ¼åæ° |
| | | todayUnqualifiedQuantity: number; // 仿¥ä¸è¯åæ° |
| | | iqcQualifiedRate: null | number; // æ¥ææ£éªåæ ¼ç(%)ï¼æ æ£éªæ°æ®æ¶ä¸º null |
| | | ipqcQualifiedRate: null | number; // è¿ç¨æ£éªåæ ¼ç(%)ï¼æ æ£éªæ°æ®æ¶ä¸º null |
| | | oqcQualifiedRate: null | number; // åºè´§æ£éªåæ ¼ç(%)ï¼æ æ£éªæ°æ®æ¶ä¸º null |
| | | iqcCheckQuantity: number; // æ¥ææ£éªçæ£éªæ°éå计ï¼åæ ¼ç忝ï¼ï¼æ æ°æ®æ¶ä¸º 0 |
| | | ipqcCheckQuantity: number; // è¿ç¨æ£éªçæ£éªæ°éå计ï¼åæ ¼ç忝ï¼ï¼æ æ°æ®æ¶ä¸º 0 |
| | | oqcCheckQuantity: number; // åºè´§æ£éªçæ£éªæ°éå计ï¼åæ ¼ç忝ï¼ï¼æ æ°æ®æ¶ä¸º 0 |
| | | machineryTotal: number; // è®¾å¤æ»æ° |
| | | machineryProducing: number; // ç产ä¸è®¾å¤æ° |
| | | machineryStop: number; // åæºè®¾å¤æ° |
| | |
| | | /** MES å®¢æ· */ |
| | | export interface Client { |
| | | id?: number; // 客æ·ç¼å· |
| | | code?: string; // 客æ·ç¼ç |
| | | name?: string; // 客æ·åç§° |
| | | nickname?: string; // 客æ·ç®ç§° |
| | | englishName?: string; // 客æ·è±æåç§° |
| | |
| | | |
| | | /** 客æ·å¯¼å
¥ç»æ */ |
| | | export interface ClientImportRespVO { |
| | | createCodes?: string[]; // æ°å¢æåç客æ·ç¼ç |
| | | updateCodes?: string[]; // æ´æ°æåç客æ·ç¼ç |
| | | failureCodes?: Record<string, string>; // 导å
¥å¤±è´¥ç客æ·ç¼ç ååå |
| | | createCustomerNames?: string[]; // æ°å¢æåç客æ·åç§° |
| | | updateCustomerNames?: string[]; // æ´æ°æåç客æ·åç§° |
| | | failureCustomerNames?: Record<string, string>; // 导å
¥å¤±è´¥ç客æ·åç§°ååå |
| | | } |
| | | } |
| | | |
| | |
| | | import type { PageParam, PageResult } from '#/packages/effects/request/src'; |
| | | import type { SystemStorageApi } from '#/api/system/storage'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | |
| | | documentName?: string; |
| | | documentVersion?: string; |
| | | fileName?: string; |
| | | fileUrl?: string; |
| | | /** èµæéä»¶ï¼èµ° system 模å storage-attachmentï¼ä¸å¡è¡¨ä¸åæä»¶å°åï¼ */ |
| | | attachmentList?: SystemStorageApi.StorageBlob[]; |
| | | uploadUserId?: number; |
| | | uploadUserNickname?: string; |
| | | uploadTime?: number; |
| | |
| | | documentName?: string; |
| | | /** èµæçæ¬ï¼åä½ï¼ */ |
| | | documentVersion?: string; |
| | | /** èµææä»¶åï¼åèµæç¬¬ä¸ä¸ªéä»¶ï¼ */ |
| | | fileName?: string; |
| | | /** èµææä»¶å°åï¼åèµæç¬¬ä¸ä¸ªéä»¶ï¼ */ |
| | | fileUrl?: string; |
| | | /** å®¡æ ¸äººï¼ç¨æ·IDï¼ */ |
| | | reviewer?: number; |
| | | /** å®¡æ ¸äººåç§° */ |
| | |
| | | quantity?: number; // 计åç产æ°é |
| | | quantityProduced?: number; // å·²ç产æ°é |
| | | clientId?: number; // 客æ·ID |
| | | clientCode?: string; // 客æ·ç¼ç |
| | | clientName?: string; // 客æ·åç§° |
| | | requestDate?: number; // éæ±æ¥æ |
| | | issueDate?: number; // ä¸åæ¥æ |
| | |
| | | export function exportRoute(params: any) { |
| | | return requestClient.download('/mes/pro/route/export-excel', { params }); |
| | | } |
| | | |
| | | /** ä¸è½½å·¥èºè·¯çº¿å¯¼å
¥æ¨¡æ¿ */ |
| | | export function importRouteTemplate() { |
| | | return requestClient.download('/mes/pro/route/get-import-template'); |
| | | } |
| | | |
| | | /** 导å
¥å·¥èºè·¯çº¿ */ |
| | | export function importRoute(file: File, updateSupport: boolean) { |
| | | return requestClient.upload( |
| | | `/mes/pro/route/import?updateSupport=${updateSupport}`, |
| | | { file }, |
| | | ); |
| | | } |
| | | |
| | | /** ä¸è½½å
å«è·¯çº¿ã产åãå·¥åºãå·¥åºäº§åºãå·¥åºææäºå¼ å·¥ä½è¡¨çæ¨¡æ¿ */ |
| | | export function importCompleteRouteTemplate() { |
| | | return requestClient.download('/mes/pro/route/get-complete-import-template'); |
| | | } |
| | | |
| | | /** æ¹é导å
¥å®æ´å·¥èºè·¯çº¿ */ |
| | | export function importCompleteRoute(file: File) { |
| | | return requestClient.upload('/mes/pro/route/import-complete', { file }); |
| | | } |
| | |
| | | remark?: string; |
| | | } |
| | | |
| | | /** 产åºç©æå½å± */ |
| | | export interface OutputItem { |
| | | id?: number; |
| | | routeProcessId?: number; |
| | | productId?: number; // å½å±äº§åç¼å·ï¼ä¸ºç©ºè¡¨ç¤ºå¤äº§åéç¨ |
| | | productName?: string; |
| | | itemId?: number; |
| | | itemCode?: string; |
| | | itemName?: string; |
| | | sort?: number; |
| | | remark?: string; |
| | | } |
| | | |
| | | /** MES å·¥èºè·¯çº¿å·¥åº */ |
| | | export interface RouteProcess { |
| | | id?: number; |
| | | routeId?: number; |
| | | processId?: number; |
| | | paramTemplateId?: number; |
| | | yieldRate?: number; |
| | | processCode?: string; |
| | | processName?: string; |
| | | sort?: number; |
| | |
| | | outputItemName?: string; |
| | | outputItemIds?: number[]; |
| | | outputItemNames?: string; |
| | | outputs?: OutputItem[]; // 产åºç©æå½å±é
ç½® |
| | | warehouseId?: number; // é»è®¤æ¥å·¥ä»åºç¼å· |
| | | warehouseName?: string; // é»è®¤æ¥å·¥ä»åºåç§° |
| | | locationId?: number; // é»è®¤æ¥å·¥åºåºç¼å· |
| | | locationName?: string; // é»è®¤æ¥å·¥åºåºåç§° |
| | | areaId?: number; // é»è®¤æ¥å·¥åºä½ç¼å· |
| | | areaName?: string; // é»è®¤æ¥å·¥åºä½åç§° |
| | | remark?: string; |
| | | bomItems?: ProcessBomItem[]; |
| | | createTime?: Date; |
| | |
| | | itemId?: number; |
| | | itemCode?: string; |
| | | itemName?: string; |
| | | warehouseId?: number; |
| | | warehouseName?: string; |
| | | locationId?: number; |
| | | locationName?: string; |
| | | areaId?: number; |
| | | areaName?: string; |
| | | specification?: string; |
| | | unitName?: string; |
| | | quantity?: number; |
| | |
| | | } |
| | | |
| | | /** æ¥è¯¢å¾
æ¥å·¥å°è´¦ï¼åé¡µï¼ */ |
| | | export interface BatchScheduleRequest { |
| | | workOrderIds: number[]; |
| | | startTime: string; |
| | | previewHash?: string; |
| | | previewTime?: string; |
| | | allowLate?: boolean; |
| | | } |
| | | |
| | | export interface BatchSchedulePreview { |
| | | canExecute: boolean; |
| | | previewHash: string; |
| | | previewTime: string; |
| | | warnings: string[]; |
| | | orders: { |
| | | workOrderId: number; |
| | | workOrderCode?: string; |
| | | workOrderName?: string; |
| | | quantity?: number; |
| | | taskCount: number; |
| | | requestDate?: string; |
| | | endTime?: string; |
| | | late: boolean; |
| | | error?: string; |
| | | }[]; |
| | | tasks: { |
| | | workOrderId: number; |
| | | workOrderCode: string; |
| | | processId: number; |
| | | processName: string; |
| | | workstationId: number; |
| | | workstationName: string; |
| | | machineryNames: string; |
| | | quantity: number; |
| | | hourlyCapacity: number; |
| | | duration: number; |
| | | startTime: string; |
| | | endTime: string; |
| | | }[]; |
| | | } |
| | | |
| | | export function previewBatchSchedule(data: BatchScheduleRequest) { |
| | | return requestClient.post<BatchSchedulePreview>('/mes/pro/task/auto-schedule/preview', data); |
| | | } |
| | | |
| | | export function executeBatchSchedule(data: BatchScheduleRequest) { |
| | | return requestClient.post<{ taskIds: number[] }>('/mes/pro/task/auto-schedule', data); |
| | | } |
| | | |
| | | /** æ¥è¯¢å¾
æ¥å·¥å°è´¦ï¼åé¡µï¼ */ |
| | | export function getPendingFeedbackPage( |
| | | params: MesProTaskApi.PendingFeedbackPageParams, |
| | | ) { |
| | |
| | | quantityChanged?: number; // è°æ´æ°é |
| | | quantityScheduled?: number; // å·²æäº§æ°é |
| | | clientId?: number; // 客æ·ç¼å· |
| | | clientCode?: string; // 客æ·ç¼ç |
| | | clientName?: string; // 客æ·åç§° |
| | | vendorId?: number; // ä¾åºåç¼å· |
| | | vendorName?: string; // ä¾åºååç§° |
| | |
| | | cancelDate?: number; // åæ¶æ¶é´ |
| | | status?: number; // 订åç¶æ |
| | | scheduleStatus?: number; // æäº§ç¶æ |
| | | productionStatus?: number; // çäº§ç¶æ |
| | | processProgressList?: ProcessProgress[]; // å·¥åºç产è¿åº¦ |
| | | completionProgress?: number; // 宿è¿åº¦ |
| | | remark?: string; // 夿³¨ |
| | |
| | | import type { MesProWorkOrderProcessApi } from './index'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace MesProWorkOrderProcessApi { |
| | |
| | | outputItemName?: string; // 产åºäº§åç©æåç§°ï¼ç¬¬ä¸ä¸ªï¼ |
| | | outputItemIds?: number[]; // 产åºäº§åç©æç¼å·å表 |
| | | outputItemNames?: string; // 产åºäº§åç©æåç§°ï¼éå·æ¼æ¥ï¼ |
| | | warehouseId?: number; // æ¥å·¥ä»åºç¼å· |
| | | warehouseName?: string; // æ¥å·¥ä»åºåç§° |
| | | locationId?: number; // æ¥å·¥åºåºç¼å· |
| | | locationName?: string; // æ¥å·¥åºåºåç§° |
| | | areaId?: number; // æ¥å·¥åºä½ç¼å· |
| | | areaName?: string; // æ¥å·¥åºä½åç§° |
| | | bomItems?: ProcessBomItem[]; // å·¥åºææBOMæç» |
| | | remark?: string; // 夿³¨ |
| | | } |
| | |
| | | /** å é¤å·¥åå·¥åº */ |
| | | export function deleteWorkOrderProcess(id: number) { |
| | | return requestClient.delete(`/mes/pro/work-order-process/delete?id=${id}`); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import dayjs from 'dayjs'; |
| | | |
| | | /** æ¥ææ¥è¯¢å
å«ç»ææ¥æå½å¤©çå
¨é¨è´¨æ£è®°å½ï¼å页ä¸å¯¼åºä½¿ç¨ç¸åå£å¾ã */ |
| | | export function normalizeQcDateFilter<T extends object>(params: T): T { |
| | | const normalized: Record<string, string[]> = {}; |
| | | for (const field of ['inspectDate', 'receiveDate'] as const) { |
| | | const range = (params as { inspectDate?: string[]; receiveDate?: string[] })[field]; |
| | | if (!Array.isArray(range) || range.length !== 2) continue; |
| | | const start = dayjs(range[0]); |
| | | const end = dayjs(range[1]); |
| | | if (!start.isValid() || !end.isValid()) continue; |
| | | normalized[field] = [ |
| | | start.startOf('day').format('YYYY-MM-DD HH:mm:ss'), |
| | | end.endOf('day').format('YYYY-MM-DD HH:mm:ss'), |
| | | ]; |
| | | } |
| | | return Object.keys(normalized).length > 0 ? { ...params, ...normalized } : params; |
| | | } |
| | |
| | | qcId?: number; // å
³èè´¨æ£å ID |
| | | qcType?: number; // è´¨æ£ç±»å |
| | | itemId?: number; // 产åç©æ ID |
| | | sn?: string; // ç©èµ SN |
| | | remark?: string; // 夿³¨ |
| | | createTime?: Date; // å建æ¶é´ |
| | | items?: IndicatorResultDetail[]; // æ£éªç»ææç»å表 |
| | |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | import { normalizeQcDateFilter } from '../date-filter'; |
| | | |
| | | export namespace MesQcIpqcApi { |
| | | /** MES è¿ç¨æ£éªå */ |
| | | export interface Ipqc { |
| | |
| | | /** æ¥è¯¢è¿ç¨æ£éªåå页 */ |
| | | export function getIpqcPage(params: PageParam) { |
| | | return requestClient.get<PageResult<MesQcIpqcApi.Ipqc>>('/mes/qc/ipqc/page', { |
| | | params, |
| | | params: normalizeQcDateFilter(params), |
| | | }); |
| | | } |
| | | |
| | |
| | | |
| | | /** 导åºè¿ç¨æ£éªå */ |
| | | export function exportIpqc(params: any) { |
| | | return requestClient.download('/mes/qc/ipqc/export-excel', { params }); |
| | | return requestClient.download('/mes/qc/ipqc/export-excel', { |
| | | params: normalizeQcDateFilter(params), |
| | | }); |
| | | } |
| | | |
| | | /** ä¸è½½è¿ç¨æ£éªå导å
¥æ¨¡æ¿ */ |
| | |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | import { normalizeQcDateFilter } from '../date-filter'; |
| | | |
| | | export namespace MesQcIqcApi { |
| | | /** MES æ¥ææ£éªå */ |
| | | export interface Iqc { |
| | |
| | | sourceLineId?: number; // æ¥æºåæ®è¡ ID |
| | | sourceDocCode?: string; // æ¥æºåæ®ç¼å·ï¼å
³èæ¥è¯¢ï¼ |
| | | vendorId?: number; // ä¾åºå ID |
| | | vendorName?: string; // ä¾åºååç§°ï¼æå¡«ï¼ |
| | | vendorNickname?: string; // ä¾åºåç®ç§°ï¼å
³èæ¥è¯¢ï¼ |
| | | vendorBatch?: string; // ä¾åºåæ¹æ¬¡å· |
| | | itemId?: number; // 产åç©æ ID |
| | |
| | | /** æ¥è¯¢æ¥ææ£éªåå页 */ |
| | | export function getIqcPage(params: PageParam) { |
| | | return requestClient.get<PageResult<MesQcIqcApi.Iqc>>('/mes/qc/iqc/page', { |
| | | params, |
| | | params: normalizeQcDateFilter(params), |
| | | }); |
| | | } |
| | | |
| | |
| | | |
| | | /** å¯¼åºæ¥ææ£éªå */ |
| | | export function exportIqc(params: any) { |
| | | return requestClient.download('/mes/qc/iqc/export-excel', { params }); |
| | | return requestClient.download('/mes/qc/iqc/export-excel', { |
| | | params: normalizeQcDateFilter(params), |
| | | }); |
| | | } |
| | | |
| | | /** ä¸è½½æ¥ææ£éªå导å
¥æ¨¡æ¿ */ |
| | |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | import { normalizeQcDateFilter } from '../date-filter'; |
| | | |
| | | export namespace MesQcInspectionLedgerApi { |
| | | /** MES æ£æµç»æå°è´¦ï¼ä¸è¡ä¸ä¸ªæ£æµé¡¹ç»æï¼æ£éªåæªå½å
¥æ£æµé¡¹æ¶è¯¥ç»åæ®µä¸ºç©ºï¼ */ |
| | | export interface Ledger { |
| | |
| | | export function getInspectionLedgerPage(params: PageParam) { |
| | | return requestClient.get<PageResult<MesQcInspectionLedgerApi.Ledger>>( |
| | | '/mes/qc/inspection-ledger/page', |
| | | { params }, |
| | | { params: normalizeQcDateFilter(params) }, |
| | | ); |
| | | } |
| | | |
| | | /** å¯¼åºæ£æµç»æå°è´¦ */ |
| | | export function exportInspectionLedger(params: Record<string, unknown>) { |
| | | return requestClient.download('/mes/qc/inspection-ledger/export-excel', { |
| | | params, |
| | | params: normalizeQcDateFilter(params), |
| | | }); |
| | | } |
| | |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | import { normalizeQcDateFilter } from '../date-filter'; |
| | | |
| | | export namespace MesQcOqcApi { |
| | | /** MES åºè´§æ£éªå */ |
| | | export interface Oqc { |
| | |
| | | /** æ¥è¯¢åºè´§æ£éªåå页 */ |
| | | export function getOqcPage(params: PageParam) { |
| | | return requestClient.get<PageResult<MesQcOqcApi.Oqc>>('/mes/qc/oqc/page', { |
| | | params, |
| | | params: normalizeQcDateFilter(params), |
| | | }); |
| | | } |
| | | |
| | |
| | | |
| | | /** 导åºåºè´§æ£éªå */ |
| | | export function exportOqc(params: any) { |
| | | return requestClient.download('/mes/qc/oqc/export-excel', { params }); |
| | | return requestClient.download('/mes/qc/oqc/export-excel', { |
| | | params: normalizeQcDateFilter(params), |
| | | }); |
| | | } |
| | | |
| | | /** ä¸è½½åºè´§æ£éªå导å
¥æ¨¡æ¿ */ |
| | |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | import { normalizeQcDateFilter } from '../date-filter'; |
| | | |
| | | export namespace MesQcRqcApi { |
| | | /** MES éè´§æ£éªå */ |
| | | export interface Rqc { |
| | |
| | | /** æ¥è¯¢éè´§æ£éªåå页 */ |
| | | export function getRqcPage(params: PageParam) { |
| | | return requestClient.get<PageResult<MesQcRqcApi.Rqc>>('/mes/qc/rqc/page', { |
| | | params, |
| | | params: normalizeQcDateFilter(params), |
| | | }); |
| | | } |
| | | |
| | |
| | | |
| | | /** 导åºéè´§æ£éªå */ |
| | | export function exportRqc(params: any) { |
| | | return requestClient.download('/mes/qc/rqc/export-excel', { params }); |
| | | return requestClient.download('/mes/qc/rqc/export-excel', { |
| | | params: normalizeQcDateFilter(params), |
| | | }); |
| | | } |
| | |
| | | vendorCode?: string; // ä¾åºåç¼ç |
| | | vendorName?: string; // ä¾åºååç§° |
| | | clientId?: number; // å®¢æ· ID |
| | | clientCode?: string; // 客æ·ç¼ç |
| | | clientName?: string; // 客æ·åç§° |
| | | purchaseOrderCode?: string; // éè´è®¢åç¼å· |
| | | salesOrderCode?: string; // éå®è®¢åç¼å· |
| | |
| | | salesDate?: Date; // åºåºæ¥æ |
| | | salesOrderCode?: string; // éå®è®¢åç¼å· |
| | | clientId?: number; // å®¢æ· ID |
| | | clientCode?: string; // 客æ·ç¼ç |
| | | clientName?: string; // 客æ·åç§° |
| | | oqcCode?: string; // OQC æ£éªåå· |
| | | checkResult?: number; // OQC æ£éªç»è®ºï¼åå
¸ mes_qc_check_resultï¼ |
| | |
| | | salesOrderCode?: string; // éå®è®¢åç¼å· |
| | | invoiceCode?: string; // å票ç¼å· |
| | | clientId?: number; // 客æ·ç¼å· |
| | | clientCode?: string; // 客æ·ç¼ç |
| | | clientName?: string; // 客æ·åç§° |
| | | clientNickname?: string; // 客æ·ç®ç§° |
| | | length?: number; // ç®±é¿åº¦ |
| | |
| | | workstationName?: string; // å·¥ä½ç«åç§° |
| | | workOrderId?: number; // ç产订åç¼å· |
| | | workOrderCode?: string; // ç产订åç¼ç |
| | | clientCode?: string; // 客æ·ç¼ç |
| | | clientName?: string; // 客æ·åç§° |
| | | requiredTime?: number; // éæ±æ¶é´ |
| | | status?: number; // åæ®ç¶æ |
| | |
| | | noticeId?: number; // åè´§éç¥åç¼å· |
| | | noticeCode?: string; // åè´§éç¥åç¼ç |
| | | clientId?: number; // 客æ·ç¼å· |
| | | clientCode?: string; // 客æ·ç¼ç |
| | | clientName?: string; // 客æ·åç§° |
| | | salesOrderCode?: string; // éå®è®¢åç¼å· |
| | | saleOrderId?: number; // éå®è®¢åID |
| | |
| | | name?: string; // éè´§ååç§° |
| | | salesOrderCode?: string; // éå®è®¢åå· |
| | | clientId?: number; // 客æ·ç¼å· |
| | | clientCode?: string; // 客æ·ç¼ç |
| | | clientName?: string; // 客æ·åç§° |
| | | returnDate?: number; // éè´§æ¥æ |
| | | returnReason?: string; // éè´§åå |
| | |
| | | saleOrderId?: number; // éå®è®¢åç¼å· |
| | | saleOrderCode?: string; // éå®è®¢åç¼å· |
| | | clientId?: number; // 客æ·ç¼å· |
| | | clientCode?: string; // 客æ·ç¼ç |
| | | clientName?: string; // 客æ·åç§° |
| | | salesDate?: number; // åè´§æ¥æ |
| | | recipientName?: string; // æ¶è´§äºº |
| | |
| | | /** |
| | | * éæ°è®¤è¯é»è¾ |
| | | */ |
| | | async function doReAuthenticate() { |
| | | async function doReAuthenticate(error?: { config?: { headers?: Record<string, unknown> } }) { |
| | | console.warn('Access token or refresh token is invalid or expired. '); |
| | | const accessStore = useAccessStore(); |
| | | const authStore = useAuthStore(); |
| | | const requestToken = error?.config?.headers?.Authorization; |
| | | // å·²éåºæå·²åæ¢è´¦å·æ¶ï¼æ§è¯·æ±ç 401 ä¸å¾æ¸
餿°ä¼è¯ã |
| | | if (requestToken && requestToken !== formatToken(accessStore.accessToken)) return; |
| | | accessStore.setAccessToken(null); |
| | | if ( |
| | | preferences.app.loginExpiredMode === 'modal' && |
| | |
| | | throw new Error('Refresh token is null!'); |
| | | } |
| | | const resp = await refreshTokenApi(refreshToken); |
| | | if (accessStore.refreshToken !== refreshToken) { |
| | | throw new Error('ç»å½ç¶æå·²ååï¼å¿½ç¥æ§ä¼è¯ç令çå·æ°ç»æ'); |
| | | } |
| | | const newToken = resp?.data?.data?.accessToken; |
| | | // add by èè¿ï¼è¿éä¸å®è¦æåº resp.dataï¼ä»è触å authenticateResponseInterceptor ä¸ï¼å·æ°ä»¤ç失败ï¼ï¼ï¼ |
| | | if (!newToken) { |
| | |
| | | fulfilled: (config) => { |
| | | const accessStore = useAccessStore(); |
| | | // æ·»å ç§æ·ç¼å· |
| | | config.headers['tenant-id'] = tenantEnable |
| | | ? accessStore.tenantId |
| | | : undefined; |
| | | if (!('tenant-id' in config.headers)) { |
| | | config.headers['tenant-id'] = tenantEnable ? accessStore.tenantId : undefined; |
| | | } |
| | | // åªæç»å½æ¶ï¼æè®¾ç½® visit-tenant-id 访é®ç§æ· |
| | | config.headers['visit-tenant-id'] = tenantEnable |
| | | ? accessStore.visitTenantId |
| | | : undefined; |
| | | if (!('visit-tenant-id' in config.headers)) { |
| | | config.headers['visit-tenant-id'] = tenantEnable ? accessStore.visitTenantId : undefined; |
| | | } |
| | | return config; |
| | | }, |
| | | }); |
| | |
| | | remark: string; |
| | | createTime?: Date; |
| | | } |
| | | |
| | | /** å²ä½å¯¼å
¥ç»æ */ |
| | | export interface PostImportRespVO { |
| | | createPostNames?: string[]; // æ°å¢æåçå²ä½å |
| | | updatePostNames?: string[]; // æ´æ°æåçå²ä½å |
| | | failurePostNames?: Record<string, string>; // 导å
¥å¤±è´¥çå²ä½åååå |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢å²ä½å表 */ |
| | |
| | | |
| | | /** 导å
¥å²ä½ */ |
| | | export function importPost(file: File, updateSupport: boolean) { |
| | | return requestClient.upload('/system/post/import', { |
| | | file, |
| | | updateSupport, |
| | | }); |
| | | return requestClient.upload<SystemPostApi.PostImportRespVO>( |
| | | '/system/post/import', |
| | | { |
| | | file, |
| | | updateSupport, |
| | | }, |
| | | ); |
| | | } |
| | |
| | | export interface UserSimple extends User { |
| | | id: number; |
| | | } |
| | | |
| | | /** ç¨æ·å¯¼å
¥ç»æ */ |
| | | export interface UserImportRespVO { |
| | | createUsernames?: string[]; // æ°å¢æåçç¨æ·å |
| | | updateUsernames?: string[]; // æ´æ°æåçç¨æ·å |
| | | failureUsernames?: Record<string, string>; // 导å
¥å¤±è´¥çç¨æ·åååå |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢ç¨æ·ç®¡çå表 */ |
| | |
| | | |
| | | /** 导å
¥ç¨æ· */ |
| | | export function importUser(file: File, updateSupport: boolean) { |
| | | return requestClient.upload('/system/user/import', { |
| | | file, |
| | | updateSupport, |
| | | }); |
| | | return requestClient.upload<SystemUserApi.UserImportRespVO>( |
| | | '/system/user/import', |
| | | { |
| | | file, |
| | | updateSupport, |
| | | }, |
| | | ); |
| | | } |
| | | |
| | | /** ç¨æ·å¯ç éç½® */ |
| | |
| | | vendorCode?: string; // ä¾åºåç¼ç |
| | | vendorName?: string; // ä¾åºååç§° |
| | | clientId?: number; // å®¢æ· ID |
| | | clientCode?: string; // 客æ·ç¼ç |
| | | clientName?: string; // 客æ·åç§° |
| | | purchaseOrderCode?: string; // éè´è®¢åç¼å· |
| | | salesOrderCode?: string; // éå®è®¢åç¼å· |
| | |
| | | salesOrderCode?: string; // éå®è®¢åç¼å· |
| | | invoiceCode?: string; // å票ç¼å· |
| | | clientId?: number; // 客æ·ç¼å· |
| | | clientCode?: string; // 客æ·ç¼ç |
| | | clientName?: string; // 客æ·åç§° |
| | | clientNickname?: string; // 客æ·ç®ç§° |
| | | length?: number; // ç®±é¿åº¦ |
| | |
| | | workstationName?: string; // å·¥ä½ç«åç§° |
| | | workOrderId?: number; // ç产订åç¼å· |
| | | workOrderCode?: string; // ç产订åç¼ç |
| | | clientCode?: string; // 客æ·ç¼ç |
| | | clientName?: string; // 客æ·åç§° |
| | | requiredTime?: number; // éæ±æ¶é´ |
| | | status?: number; // åæ®ç¶æ |
| | |
| | | noticeId?: number; // åè´§éç¥åç¼å· |
| | | noticeCode?: string; // åè´§éç¥åç¼ç |
| | | clientId?: number; // 客æ·ç¼å· |
| | | clientCode?: string; // 客æ·ç¼ç |
| | | clientName?: string; // 客æ·åç§° |
| | | salesOrderCode?: string; // éå®è®¢åç¼å· |
| | | saleOrderId?: number; // éå®è®¢åID |
| | |
| | | name?: string; // éè´§ååç§° |
| | | salesOrderCode?: string; // éå®è®¢åå· |
| | | clientId?: number; // 客æ·ç¼å· |
| | | clientCode?: string; // 客æ·ç¼ç |
| | | clientName?: string; // 客æ·åç§° |
| | | returnDate?: number; // éè´§æ¥æ |
| | | returnReason?: string; // éè´§åå |
| | |
| | | name?: string; // éç¥ååç§° |
| | | salesOrderCode?: string; // éå®è®¢åç¼å· |
| | | clientId?: number; // 客æ·ç¼å· |
| | | clientCode?: string; // 客æ·ç¼ç |
| | | clientName?: string; // 客æ·åç§° |
| | | salesDate?: number; // åè´§æ¥æ |
| | | recipientName?: string; // æ¶è´§äºº |
| | |
| | | import { message, Modal, Tabs } from 'ant-design-vue'; |
| | | |
| | | import { useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getCustomerPage } from '#/api/crm/customer'; |
| | | import { getBusinessCustomerPage, getCustomerPage } from '#/api/crm/customer'; |
| | | |
| | | defineOptions({ name: 'CrmCustomerSelectDialog' }); |
| | | |
| | | const emit = defineEmits<{ |
| | | selected: [rows: CrmCustomerApi.Customer[]]; |
| | | }>(); |
| | | const props = defineProps<{ allCustomers: boolean }>(); |
| | | |
| | | const open = ref(false); |
| | | const multiple = ref(true); |
| | | const selectedRows = ref<CrmCustomerApi.Customer[]>([]); |
| | | const preSelectedIds = ref<number[]>([]); |
| | | const sceneType = ref('1'); |
| | | |
| | | function handleSceneTypeChange(key: number | string) { |
| | | sceneType.value = key.toString(); |
| | | gridApi.query(); |
| | | } |
| | | |
| | | function getTableRows() { |
| | | return gridApi.grid.getTableData().fullData as CrmCustomerApi.Customer[]; |
| | |
| | | } |
| | | } |
| | | |
| | | function handleSceneTypeChange(key: number | string) { |
| | | sceneType.value = key.toString(); |
| | | gridApi.query(); |
| | | } |
| | | |
| | | const gridColumns = [ |
| | | { field: 'name', title: '客æ·åç§°', minWidth: 180 }, |
| | | { field: 'mobile', title: 'ææºå·', width: 140 }, |
| | |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getCustomerPage({ |
| | | const query = { |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | sceneType: sceneType.value, |
| | | ...formValues, |
| | | }); |
| | | }; |
| | | return props.allCustomers |
| | | ? await getBusinessCustomerPage(query) |
| | | : await getCustomerPage({ ...query, sceneType: sceneType.value }); |
| | | }, |
| | | }, |
| | | }, |
| | |
| | | @ok="handleConfirm" |
| | | @cancel="closeModal" |
| | | > |
| | | <Tabs class="mb-2" @change="handleSceneTypeChange"> |
| | | <Tabs v-if="!allCustomers" class="mb-2" @change="handleSceneTypeChange"> |
| | | <Tabs.TabPane key="1" tab="æè´è´£ç" /> |
| | | <Tabs.TabPane key="2" tab="æåä¸ç" /> |
| | | <Tabs.TabPane key="3" tab="ä¸å±è´è´£ç" /> |
| | |
| | | import type { CrmCustomerApi } from '#/api/crm/customer'; |
| | | |
| | | import { computed, ref, useAttrs, watch } from 'vue'; |
| | | import { useRoute } from 'vue-router'; |
| | | |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { Input, Tooltip } from 'ant-design-vue'; |
| | | |
| | | import { getCustomer } from '#/api/crm/customer'; |
| | | import { getBusinessCustomer, getCustomer } from '#/api/crm/customer'; |
| | | |
| | | import CrmCustomerSelectDialog from './crm-customer-select-dialog.vue'; |
| | | |
| | |
| | | const props = withDefaults( |
| | | defineProps<{ |
| | | allowClear?: boolean; |
| | | allCustomers?: boolean; |
| | | disabled?: boolean; |
| | | modelValue?: number; |
| | | placeholder?: string; |
| | |
| | | modelValue: undefined, |
| | | placeholder: 'è¯·éæ©å®¢æ·', |
| | | }, |
| | | ); |
| | | const route = useRoute(); |
| | | const useAllCustomers = computed( |
| | | () => props.allCustomers ?? !String(route.path).startsWith('/crm/'), |
| | | ); |
| | | const emit = defineEmits<{ |
| | | change: [item: CrmCustomerApi.Customer | undefined]; |
| | |
| | | if (selectedItem.value?.id === id) { |
| | | return; |
| | | } |
| | | selectedItem.value = await getCustomer(id); |
| | | selectedItem.value = useAllCustomers.value |
| | | ? await getBusinessCustomer(id) |
| | | : await getCustomer(id); |
| | | } |
| | | |
| | | watch( |
| | |
| | | </Input> |
| | | </Tooltip> |
| | | </div> |
| | | <CrmCustomerSelectDialog ref="dialogRef" @selected="handleSelected" /> |
| | | <CrmCustomerSelectDialog |
| | | ref="dialogRef" |
| | | :all-customers="useAllCustomers" |
| | | @selected="handleSelected" |
| | | /> |
| | | </template> |
| | |
| | | |
| | | import { Card, Descriptions } from 'ant-design-vue'; |
| | | |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import { isAuditField } from '#/utils/audit-ui-policy'; |
| | | |
| | | const props = { |
| | | bordered: { default: true, type: Boolean }, |
| | | column: { |
| | |
| | | const { data, schema } = unref(getProps); |
| | | return unref(schema) |
| | | .map((item) => { |
| | | if (automaticApproval.value && isAuditField(item)) return null; |
| | | const { contentMinWidth, field, render, show, span } = item; |
| | | |
| | | if (show && isFunction(show) && !show(data)) { |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FileType } from 'ant-design-vue/es/upload/interface'; |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | |
| | | import { h, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button, message, Modal as AntModal, Upload } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm, z } from '#/adapter/form'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | export interface ImportModalProps { |
| | | /** å¼¹çªæ é¢ï¼å¦ã导å
¥ç¨æ·ã */ |
| | | title: string; |
| | | /** ä¸è½½çæ¨¡æ¿æä»¶åï¼å¦ãç¨æ·å¯¼å
¥æ¨¡æ¿.xlsã */ |
| | | fileName: string; |
| | | /** æ§è¡å¯¼å
¥çæ¥å£å½æ° (file, updateSupport) => Promise */ |
| | | importFn: (file: File, updateSupport: boolean) => Promise<unknown>; |
| | | /** ä¸è½½æ¨¡æ¿çæ¥å£å½æ° () => Promise(blob) */ |
| | | templateFn: () => Promise<BlobPart>; |
| | | /** 宿´è·¯çº¿åªæ¯ææ°å¢ï¼ä¸å±ç¤ºè¦çå¼å
³ */ |
| | | showUpdateSupport?: boolean; |
| | | } |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const config = ref<ImportModalProps>({ |
| | | title: '', |
| | | fileName: '', |
| | | importFn: async () => {}, |
| | | templateFn: () => Promise.resolve(new Blob()), |
| | | }); |
| | | |
| | | function createSchema(showUpdateSupport = true): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'file', |
| | | label: 'æ°æ®æä»¶', |
| | | component: 'Upload' as const, |
| | | rules: 'required', |
| | | help: 'ä»
å
许导å
¥ xlsãxlsx æ ¼å¼æä»¶', |
| | | }, |
| | | ...(showUpdateSupport ? [{ |
| | | fieldName: 'updateSupport', |
| | | label: 'æ¯å¦è¦ç', |
| | | component: 'Switch' as const, |
| | | componentProps: { |
| | | checkedChildren: 'æ¯', |
| | | unCheckedChildren: 'å¦', |
| | | }, |
| | | rules: z.boolean().default(false), |
| | | help: 'æ¯å¦æ´æ°å·²ç»åå¨çæ°æ®', |
| | | }] : []), |
| | | ]; |
| | | } |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | formItemClass: 'col-span-2', |
| | | labelWidth: 120, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: createSchema(), |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (isOpen) { |
| | | config.value = modalApi.getData<ImportModalProps>() ?? config.value; |
| | | await formApi.updateSchema(createSchema(config.value.showUpdateSupport !== false)); |
| | | await formApi.resetForm(); |
| | | } |
| | | }, |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | // æäº¤è¡¨å |
| | | const data = await formApi.getValues(); |
| | | try { |
| | | const result = await config.value.importFn(data.file, data.updateSupport ?? false) as { |
| | | createCodes?: string[]; |
| | | updateCodes?: string[]; |
| | | failureCodes?: Record<string, string>; |
| | | } | undefined; |
| | | // å
³éå¹¶æç¤º |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | const failures = Object.entries(result?.failureCodes ?? {}); |
| | | if (failures.length > 0) { |
| | | AntModal.warning({ |
| | | title: `导å
¥å®æï¼æå ${(result?.createCodes?.length ?? 0) + (result?.updateCodes?.length ?? 0)} æ¡ï¼å¤±è´¥ ${failures.length} æ¡`, |
| | | content: h('div', { style: { whiteSpace: 'pre-wrap', maxHeight: '50vh', overflowY: 'auto' } }, |
| | | failures.map(([code, reason]) => `${code}ï¼${reason}`).join('\n')), |
| | | }); |
| | | } else { |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | /** ä¸ä¼ å */ |
| | | function beforeUpload(file: FileType) { |
| | | formApi.setFieldValue('file', file); |
| | | return false; |
| | | } |
| | | |
| | | /** ä¸è½½æ¨¡ç */ |
| | | async function handleDownload() { |
| | | const data = await config.value.templateFn(); |
| | | downloadFileFromBlobPart({ fileName: config.value.fileName, source: data }); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="config.title" class="w-1/3"> |
| | | <Form class="mx-4"> |
| | | <template #file> |
| | | <div class="w-full"> |
| | | <Upload |
| | | :max-count="1" |
| | | accept=".xls,.xlsx" |
| | | :before-upload="beforeUpload" |
| | | > |
| | | <Button type="primary"> éæ© Excel æä»¶ </Button> |
| | | </Upload> |
| | | </div> |
| | | </template> |
| | | </Form> |
| | | <template #prepend-footer> |
| | | <div class="flex flex-auto items-center"> |
| | | <Button @click="handleDownload"> ä¸è½½å¯¼å
¥æ¨¡æ¿ </Button> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | export { default } from './import-modal.vue'; |
| | |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictLabel, getDictObj } from '@vben/hooks'; |
| | | import { formatDateTime } from '@vben/utils'; |
| | | import { formatDate } from '@vben/utils'; |
| | | |
| | | import { Tag, Timeline } from 'ant-design-vue'; |
| | | |
| | |
| | | |
| | | withDefaults(defineProps<OperateLogProps>(), { |
| | | logList: () => [], |
| | | dateFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | }); |
| | | |
| | | /** è·å¾ userType é¢è² */ |
| | |
| | | </template> |
| | | <div class="ml-2 flex flex-wrap items-center gap-2 leading-[22px]"> |
| | | <span class="w-[140px] shrink-0 text-[13px] text-gray-400"> |
| | | {{ formatDateTime(log.createTime) }} |
| | | {{ formatDate(log.createTime, dateFormat) }} |
| | | </span> |
| | | <Tag color="success" class="!mr-0">{{ log.userName }}</Tag> |
| | | <span>{{ log.action }}</span> |
| | |
| | | import type { SystemOperateLogApi } from '#/api/system/operate-log'; |
| | | |
| | | export interface OperateLogProps { |
| | | /** ä»
æ§å¶æ¥æå±ç¤ºï¼ä¸æ¹åæ¥å¿åå§æ¶é´ã */ |
| | | dateFormat?: string; |
| | | logList: SystemOperateLogApi.OperateLog[]; // æä½æ¥å¿å表 |
| | | } |
| | |
| | | return; |
| | | } |
| | | |
| | | const hasLimit = item.upperLimit != null || item.lowerLimit != null; |
| | | if ( |
| | | hasLimit && |
| | | (!String(item.actualValue ?? '').trim() || !Number.isFinite(toNumber(item.actualValue))) |
| | | ) { |
| | | applyResult(item, undefined); |
| | | errors.push( |
| | | `第 ${index + 1} 项ã${item.itemName ?? ''}ãç¼ºå°æææ°å¼ç»æï¼å¾
è¡¥æµæç¡®è®¤åå§è®°å½`, |
| | | ); |
| | | return; |
| | | } |
| | | const byLimit = evaluateByLimit(item); |
| | | if (byLimit !== undefined) { |
| | | applyResult(item, byLimit ? QUALITY_RESULT.PASS : QUALITY_RESULT.FAIL); |
| | |
| | | const node = parseRule(expression); |
| | | ruleCache.set(expression, node); |
| | | return node; |
| | | } |
| | | } |
| | |
| | | Tooltip, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import { auditActionMode } from '#/utils/audit-ui-policy'; |
| | | |
| | | const props = defineProps({ |
| | | actions: { |
| | | type: Array as PropType<ActionItem[]>, |
| | |
| | | |
| | | /** æ£æ¥æ¯å¦æ¾ç¤º */ |
| | | function isIfShow(action: ActionItem): boolean { |
| | | if (auditActionMode(action.label, automaticApproval.value) === 'hidden') return false; |
| | | const ifShow = action.ifShow; |
| | | let isIfShow = true; |
| | | if (isBoolean(ifShow)) { |
| | |
| | | /** å¤çæé® actions */ |
| | | const getActions = computed(() => { |
| | | const actions = props.actions || []; |
| | | return actions.filter((action: ActionItem) => isIfShow(action)); |
| | | return actions.filter((action: ActionItem) => isIfShow(action)).map(displayAction); |
| | | }); |
| | | |
| | | /** å¤ç䏿èå actions */ |
| | | const getDropdownList = computed(() => { |
| | | const dropDownActions = props.dropDownActions || []; |
| | | return dropDownActions.filter((action: ActionItem) => isIfShow(action)); |
| | | return dropDownActions.filter((action: ActionItem) => isIfShow(action)).map(displayAction); |
| | | }); |
| | | |
| | | function displayAction(action: ActionItem): ActionItem { |
| | | return auditActionMode(action.label, automaticApproval.value) === 'activate' |
| | | ? { ...action, label: 'çæ', tooltip: 'æäº¤åèªå¨çæ' } |
| | | : action; |
| | | } |
| | | |
| | | /** Space ç»ä»¶ç size */ |
| | | const spaceSize = computed(() => { |
| | | const actions = unref(getActions); |
| | |
| | | |
| | | const appName = computed(() => preferences.copyright.companyName); |
| | | // ç»å½é¡µ logo ä¼å
åæ°æ®åºé
ç½®ï¼preferences.logo.sourceï¼ï¼æªé
ç½®æ¶åéç¼è¯æé»è®¤ __APP_LOGO__ |
| | | const logo = computed(() => preferences.logo.source || __APP_LOGO__); |
| | | const logo = computed(() => { |
| | | const source = preferences.logo.source || __APP_LOGO__; |
| | | // æªé
ç½®å
¬å¸æ å¿æ¶ä¸å±ç¤ºæ¡æ¶æ¼ç¤º Logoã |
| | | return source?.includes('@vbenjs/static-source') ? '' : source; |
| | | }); |
| | | const logoDark = computed(() => preferences.logo.sourceDark); |
| | | // ç»å½é¡µæ é¢/æè¿°ä½¿ç¨ i18n åºå®ææ¡ï¼ä¸éå
¬å¸é
ç½®åå |
| | | const pageTitle = computed(() => $t('authentication.pageTitle')); |
| | | const pageDescription = computed(() => $t('authentication.pageDesc')); |
| | | // æ é¢/æè¿°ä¼å
åå
¬å¸ä¿¡æ¯é
ç½®ï¼æªé
ç½®æ¶åéæ åææ¡ |
| | | const pageTitle = computed( |
| | | () => companyConfigStore.loginTitle || $t('authentication.pageTitle'), |
| | | ); |
| | | const pageDescription = computed( |
| | | () => companyConfigStore.loginDescription || $t('authentication.pageDesc'), |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | |
| | | overrides: overridesPreferences, |
| | | }); |
| | | |
| | | // æ¢å¤æ åé»è®¤èè²ä¸»é¢ï¼æ¸
æä¹åç¼åçæ±ç 红åçè²ã |
| | | // 䏿¬¡æ§è¿ç§»ï¼ä¹åç¨æ·å¨å好设置ä¸éæ©çé¢è²ä»ä¼ä¿çã |
| | | const themeMigrationKey = `${namespace}-default-blue-theme-v1`; |
| | | const defaultTheme = { |
| | | theme: { builtinType: 'default' as const, colorPrimary: 'hsl(212 100% 45%)' }, |
| | | }; |
| | | try { |
| | | if (!localStorage.getItem(themeMigrationKey)) { |
| | | preferencesManager.updatePreferences(defaultTheme); |
| | | localStorage.setItem(themeMigrationKey, '1'); |
| | | localStorage.removeItem(`${namespace}-herbal-theme-v1`); |
| | | } |
| | | } catch { |
| | | preferencesManager.updatePreferences(defaultTheme); |
| | | } |
| | | |
| | | // 强å¶è¦çå
¬å¸ logo å faviconï¼ç»è¿ localStorage ç¼å |
| | | if (__APP_LOGO__) { |
| | | preferencesManager.updatePreferences({ logo: { source: __APP_LOGO__ } }); |
| | |
| | | <slot name="strengthText"> </slot> |
| | | </p> |
| | | </template> |
| | | <div |
| | | <button |
| | | type="button" |
| | | :aria-label="show ? 'éèå¯ç ' : 'æ¾ç¤ºå¯ç '" |
| | | :aria-pressed="show" |
| | | :disabled="!!$attrs.disabled" |
| | | :class="{ |
| | | 'top-3': !!passwordStrength, |
| | | 'top-1/2 -translate-y-1/2 items-center': !passwordStrength, |
| | | }" |
| | | class="text-foreground/60 hover:text-foreground absolute inset-y-0 right-0 flex cursor-pointer pr-3 text-lg leading-5" |
| | | @click="show = !show" |
| | | @keydown.enter.stop |
| | | > |
| | | <Eye v-if="show" class="size-4" /> |
| | | <EyeOff v-else class="size-4" /> |
| | | </div> |
| | | </button> |
| | | </div> |
| | | </template> |
| | |
| | | RERANK: 6, // éæ |
| | | }; |
| | | |
| | | /** AI 模åè½åï¼ä¸æ¨¡åç±»åä¸èµ·å³å®ãå¯ç¨å¯ä¸ãåç» */ |
| | | export const AiModelCapabilityEnum = { |
| | | TEXT: 'text', // ææ¬ |
| | | VISION: 'vision', // è§è§ |
| | | }; |
| | | |
| | | export interface ImageModel { |
| | | image?: string; |
| | | key: string; |
| | |
| | | DV_MACHINERY_CODE: 'DV_MACHINERY_CODE', |
| | | DV_REPAIR_CODE: 'DV_REPAIR_CODE', |
| | | DV_SUBJECT_CODE: 'DV_SUBJECT_CODE', |
| | | MD_CLIENT_CODE: 'MD_CLIENT_CODE', |
| | | MD_ITEM_TYPE_CODE: 'MD_ITEM_TYPE_CODE', |
| | | MD_ITEM_CODE: 'MD_ITEM_CODE', |
| | | MD_VENDOR_CODE: 'MD_VENDOR_CODE', |
| | |
| | | QC_INDICATOR_CODE: 'QC_INDICATOR_CODE', |
| | | QC_INDICATOR_RESULT_CODE: 'QC_INDICATOR_RESULT_CODE', |
| | | QC_IQC_CODE: 'QC_IQC_CODE', |
| | | QC_IQC_VENDOR_BATCH_CODE: 'QC_IQC_VENDOR_BATCH_CODE', |
| | | QC_IPQC_CODE: 'QC_IPQC_CODE', |
| | | QC_OQC_CODE: 'QC_OQC_CODE', |
| | | QC_RQC_CODE: 'QC_RQC_CODE', |
| | |
| | | CONFIRMED: 1, // 已确认 |
| | | FINISHED: 2, // 已宿 |
| | | CANCELED: 3, // 已忶 |
| | | } as const; |
| | | |
| | | /** MES ç产工åçäº§ç¶ææä¸¾ï¼å¯¹é½å端 MesProWorkOrderProductionStatusEnumï¼ */ |
| | | export const MesProWorkOrderProductionStatusEnum = { |
| | | WAITING: 0, // å¾
ç产 |
| | | PRODUCING: 1, // çäº§ä¸ |
| | | FINISHED: 2, // çäº§å®æ |
| | | } as const; |
| | | |
| | | /** MES å·¥åç±»åæä¸¾ */ |
| | |
| | | const AI_DICT = { |
| | | AI_GENERATE_MODE: 'ai_generate_mode', // AI çææ¨¡å¼ |
| | | AI_IMAGE_STATUS: 'ai_image_status', // AI å¾çç¶æ |
| | | AI_MODEL_CAPABILITY: 'ai_model_capability', // AI 模åè½å |
| | | AI_MODEL_TYPE: 'ai_model_type', // AI 模åç±»å |
| | | AI_MUSIC_STATUS: 'ai_music_status', // AI é³ä¹ç¶æ |
| | | AI_PLATFORM: 'ai_platform', // AI å¹³å° |
| | |
| | | const rememberMe = ref(!!localUsername); |
| | | |
| | | async function handleSubmit() { |
| | | if (props.loading) return; |
| | | const { valid } = await formApi.validate(); |
| | | const values = await formApi.getValues(); |
| | | if (valid) { |
| | |
| | | export function isDocAlertEnable(): boolean { |
| | | return import.meta.env.VITE_APP_DOCALERT_ENABLE !== 'false'; |
| | | } |
| | | |
| | | /** AI åè½æ»å¼å
³ï¼å
³éåéèææ AI å
¥å£ï¼æ¨¡åèåãOCRã颿µãæºè½å¤å®ãæºè½ä½å¯¼å
¥çï¼ */ |
| | | export function isAiEnable(): boolean { |
| | | return import.meta.env.VITE_APP_AI_ENABLE !== 'false'; |
| | | } |
| | |
| | | colorPrimary: '', |
| | | colorSuccess: '', |
| | | colorTextBase: '', |
| | | colorTextLightSolid: '', |
| | | colorWarning: '', |
| | | zIndexPopupBase: 2000, // è°æ´åºç¡å¼¹å±å±çº§ï¼é¿å
䏿çç»ä»¶è¢«å¼¹çªæè
æå¤§åç¶æä¸çè¡¨æ ¼é®æ¡ |
| | | }); |
| | |
| | | |
| | | tokens.colorTextBase = getCssVariableValue('--foreground'); |
| | | |
| | | getCssVariableValue('--primary-foreground'); |
| | | tokens.colorTextLightSolid = getCssVariableValue('--primary-foreground'); |
| | | |
| | | tokens.colorBorderSecondary = tokens.colorBorder = |
| | | getCssVariableValue('--border'); |
| | |
| | | formatToken, |
| | | }: { |
| | | client: RequestClient; |
| | | doReAuthenticate: () => Promise<void>; |
| | | doReAuthenticate: (error?: any) => Promise<void>; |
| | | doRefreshToken: () => Promise<string>; |
| | | enableRefreshToken: boolean; |
| | | formatToken: (token: string) => null | string; |
| | |
| | | // 夿æ¯å¦å¯ç¨äº refreshToken åè½ |
| | | // å¦ææ²¡æå¯ç¨æè
å·²ç»æ¯éè¯è¯·æ±äºï¼ç´æ¥è·³è½¬å°éæ°ç»å½ |
| | | if (!enableRefreshToken || config.__isRetryRequest) { |
| | | await doReAuthenticate(); |
| | | await doReAuthenticate(error); |
| | | throw error; |
| | | } |
| | | // 妿æ£å¨å·æ° tokenï¼åå°è¯·æ±å å
¥éåï¼çå¾
å·æ°å®æ |
| | | if (client.isRefreshing) { |
| | | return new Promise((resolve) => { |
| | | client.refreshTokenQueue.push((newToken: string) => { |
| | | return new Promise((resolve, reject) => { |
| | | client.refreshTokenQueue.push((newToken: string, refreshError?: unknown) => { |
| | | if (refreshError) { reject(refreshError); return; } |
| | | config.headers.Authorization = formatToken(newToken); |
| | | resolve(client.request(config.url, { ...config })); |
| | | }); |
| | |
| | | return client.request(error.config.url, { ...error.config }); |
| | | } catch (refreshError) { |
| | | // 妿巿° token 失败ï¼å¤çé误ï¼å¦å¼ºå¶ç»åºæè·³è½¬ç»å½é¡µé¢ï¼ |
| | | client.refreshTokenQueue.forEach((callback) => callback('')); |
| | | client.refreshTokenQueue.forEach((callback) => callback('', refreshError)); |
| | | client.refreshTokenQueue = []; |
| | | console.error('Refresh token failed, please login again.'); |
| | | await doReAuthenticate(); |
| | | await doReAuthenticate(error); |
| | | |
| | | throw refreshError; |
| | | } finally { |
| | |
| | | public isRefreshing = false; |
| | | public postSSE: SSE['postSSE']; |
| | | // å·æ°tokenéå |
| | | public refreshTokenQueue: ((token: string) => void)[] = []; |
| | | public refreshTokenQueue: ((token: string, error?: unknown) => void)[] = []; |
| | | public requestSSE: SSE['requestSSE']; |
| | | public upload: FileUploader['upload']; |
| | | |
| | |
| | | } |
| | | const allStores = (pinia as any)._s; |
| | | for (const [_key, store] of allStores) { |
| | | store.$reset(); |
| | | try { |
| | | store.$reset(); |
| | | } catch (error) { |
| | | // å个ä¸å¡ Store çå¼å¸¸ä¸è½ä¸æè®¤è¯æ¸
çåéåºç»å½è·³è½¬ã |
| | | console.error(`Failed to reset store: ${_key}`, error); |
| | | } |
| | | } |
| | | } |
| | |
| | | import type { RouteRecordRaw } from 'vue-router'; |
| | | |
| | | const routes: RouteRecordRaw[] = [ |
| | | import { isAiEnable } from '@vben/hooks'; |
| | | |
| | | /** AI æ»å¼å
³å
³éæ¶æ´ç»è·¯ç±ä¸æ³¨åï¼é¿å
éè¿å°åæ ç´æ¥è®¿é® */ |
| | | const routes: RouteRecordRaw[] = isAiEnable() ? [ |
| | | { |
| | | path: '/ai', |
| | | name: 'Ai', |
| | |
| | | }, |
| | | ], |
| | | }, |
| | | ]; |
| | | ] : []; |
| | | |
| | | export default routes; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export const automaticApproval = ref(false); |
| | | |
| | | /** Loaded with each authenticated session, before business pages are rendered. */ |
| | | export async function loadAuditPolicy() { |
| | | try { |
| | | automaticApproval.value = await requestClient.get<boolean>('/bpm/process-auto-approve/ui-policy'); |
| | | } catch { |
| | | // A failed policy request must not remove the normal submission path. |
| | | automaticApproval.value = false; |
| | | } |
| | | } |
| | |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { LOGIN_PATH } from '@vben/constants'; |
| | | import { isAiEnable } from '@vben/hooks'; |
| | | import { preferences } from '@vben/preferences'; |
| | | import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores'; |
| | | |
| | |
| | | socialLogin, |
| | | } from '#/api'; |
| | | import { $t } from '#/locales'; |
| | | import { automaticApproval, loadAuditPolicy } from '#/store/audit-policy'; |
| | | |
| | | /** AI æ»å¼å
³å
³éæ¶ï¼è¿å /ai 模åèåä¸å¹¶éè */ |
| | | function filterAiMenus<T extends { children?: T[]; path?: string }>( |
| | | menus: T[], |
| | | ): T[] { |
| | | return menus |
| | | .filter((menu) => { |
| | | const path = menu.path?.replace(/^\//, ''); |
| | | return !(path === 'ai' || path?.startsWith('ai/')); |
| | | }) |
| | | .map((menu) => |
| | | menu.children?.length |
| | | ? { ...menu, children: filterAiMenus(menu.children) } |
| | | : menu, |
| | | ); |
| | | } |
| | | |
| | | export const useAuthStore = defineStore('auth', () => { |
| | | const accessStore = useAccessStore(); |
| | |
| | | const router = useRouter(); |
| | | |
| | | const loginLoading = ref(false); |
| | | let sessionVersion = 0; |
| | | let logoutTask: Promise<void> | null = null; |
| | | |
| | | /** |
| | | * 弿¥å¤çç»å½æä½ |
| | |
| | | ) { |
| | | // 弿¥å¤çç¨æ·ç»å½æä½å¹¶è·å accessToken |
| | | let userInfo: null | UserInfo = null; |
| | | const version = ++sessionVersion; |
| | | try { |
| | | let loginResult: AuthApi.LoginResult; |
| | | loginLoading.value = true; |
| | |
| | | } |
| | | } |
| | | const { accessToken, refreshToken } = loginResult; |
| | | if (version !== sessionVersion) return { userInfo: null }; |
| | | |
| | | // 妿æåè·åå° accessToken |
| | | if (accessToken) { |
| | |
| | | // // getAccessCodesApi(), |
| | | // ]); |
| | | const fetchUserInfoResult = await fetchUserInfo(); |
| | | if (version !== sessionVersion) return { userInfo: null }; |
| | | |
| | | userInfo = fetchUserInfoResult.user; |
| | | |
| | |
| | | }); |
| | | } |
| | | } |
| | | } catch (error) { |
| | | if (version === sessionVersion) throw error; |
| | | return { userInfo: null }; |
| | | } finally { |
| | | loginLoading.value = false; |
| | | if (version === sessionVersion) loginLoading.value = false; |
| | | } |
| | | |
| | | return { |
| | |
| | | }; |
| | | } |
| | | |
| | | async function logout(redirect: boolean = true) { |
| | | function logout(redirect: boolean = true): Promise<void> { |
| | | if (logoutTask) return logoutTask; |
| | | const accessToken = accessStore.accessToken; |
| | | const tenant = { tenantId: accessStore.tenantId, visitTenantId: accessStore.visitTenantId }; |
| | | const fullPath = router.currentRoute.value.fullPath; |
| | | ++sessionVersion; |
| | | // æå¡å¨æ³¨éç¬ç«æ§è¡ï¼æ¬å°æ¸
çä¸è·³è½¬ä¸çå¾
ç½ç»ã |
| | | // 使ç¨éåºåç token åç§æ·å¿«ç
§ï¼é¿å
æ¸
çå丢失注éä¸ä¸æã |
| | | if (accessToken) void logoutApi(accessToken, tenant).catch(() => {}); |
| | | logoutTask = clearSessionAndNavigate(redirect, fullPath).finally(() => { logoutTask = null; }); |
| | | return logoutTask; |
| | | } |
| | | |
| | | async function clearSessionAndNavigate(redirect: boolean, fullPath: string) { |
| | | try { |
| | | const accessToken = accessStore.accessToken as string; |
| | | if (accessToken) { |
| | | await logoutApi(accessToken); |
| | | } |
| | | } catch { |
| | | // ä¸åä»»ä½å¤ç |
| | | resetAllStores(); |
| | | } catch (error) { |
| | | console.error('Failed to reset business stores during logout', error); |
| | | } |
| | | resetAllStores(); |
| | | // è®¤è¯æ¸
çä¸è½ä¾èµä¸å¡ store çéç½®ç»æã |
| | | accessStore.$reset(); |
| | | userStore.$reset(); |
| | | accessStore.setLoginExpired(false); |
| | | |
| | | // åç»å½é¡µå¸¦ä¸å½åè·¯ç±å°å |
| | |
| | | path: LOGIN_PATH, |
| | | query: redirect |
| | | ? { |
| | | redirect: encodeURIComponent(router.currentRoute.value.fullPath), |
| | | redirect: encodeURIComponent(fullPath), |
| | | } |
| | | : {}, |
| | | }); |
| | | } |
| | | |
| | | async function fetchUserInfo() { |
| | | const version = sessionVersion; |
| | | const accessToken = accessStore.accessToken; |
| | | // å è½½ |
| | | // eslint-disable-next-line no-useless-assignment |
| | | let authPermissionInfo: AuthPermissionInfo | null = null; |
| | | authPermissionInfo = await getAuthPermissionInfoApi(); |
| | | if (version !== sessionVersion || accessToken !== accessStore.accessToken) { |
| | | throw new Error('ç»å½ç¶æå·²ååï¼å¿½ç¥æ§ä¼è¯çç¨æ·ä¿¡æ¯'); |
| | | } |
| | | await loadAuditPolicy(); |
| | | if (version !== sessionVersion || accessToken !== accessStore.accessToken) { |
| | | throw new Error('ç»å½ç¶æå·²ååï¼å¿½ç¥æ§ä¼è¯çç¨æ·ä¿¡æ¯'); |
| | | } |
| | | // userStore |
| | | userStore.setUserInfo(authPermissionInfo.user); |
| | | userStore.setUserRoles(authPermissionInfo.roles); |
| | | // accessStore |
| | | accessStore.setAccessMenus(authPermissionInfo.menus); |
| | | accessStore.setAccessMenus( |
| | | isAiEnable() ? authPermissionInfo.menus : filterAiMenus(authPermissionInfo.menus), |
| | | ); |
| | | accessStore.setAccessCodes(authPermissionInfo.permissions); |
| | | return authPermissionInfo; |
| | | } |
| | | |
| | | function $reset() { |
| | | loginLoading.value = false; |
| | | automaticApproval.value = false; |
| | | } |
| | | |
| | | return { |
| | |
| | | applyToPreferences(); |
| | | } |
| | | |
| | | function $reset() { |
| | | companyName.value = ''; |
| | | logoUrl.value = ''; |
| | | loginTitle.value = ''; |
| | | loginDescription.value = ''; |
| | | } |
| | | |
| | | return { |
| | | $reset, |
| | | companyName, |
| | | logoUrl, |
| | | loginTitle, |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** Review-only UI disappears in automatic mode; document status and activation remain. */ |
| | | export function isAuditField(item: { field?: string; fieldName?: string; label?: unknown; title?: unknown }) { |
| | | return /^(auditStatus|auditUserId|auditTime|approvalStatus|approvalUserId|approvalTime|processInstanceId)$/.test(item.field ?? item.fieldName ?? '') |
| | | || /^(审æ¹ç¶æ|å®¡æ ¸ç¶æ|审æ¹äºº|å®¡æ ¸äºº|å®¡æ¹æ¶é´|å®¡æ ¸æ¶é´|æµç¨å®ä¾)$/.test(String(item.title ?? item.label ?? '')); |
| | | } |
| | | |
| | | export function auditActionMode(label: string | undefined, automatic: boolean) { |
| | | if (!automatic) return 'normal'; |
| | | if (/^(æäº¤å®¡æ¹|æäº¤å®¡æ ¸|å®¡æ ¸|审æ¹|éè¿|å®¡æ ¸éè¿)$/.test(label ?? '')) return 'activate'; |
| | | if (/^(æ¥ç审æ¹|æ¥çå®¡æ ¸|审æ¹è¿åº¦|æ¥ç审æ¹è¿åº¦|åå®¡æ ¸|æ¤é审æ¹|忶审æ¹|驳å|æç»)$/.test(label ?? '')) return 'hidden'; |
| | | return 'normal'; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | import { downloadContractDocument } from '#/api/crm/contract'; |
| | | |
| | | export async function downloadContractWord(id: number, no?: string) { |
| | | const source = await downloadContractDocument(id); |
| | | const suffix = (no || String(id)).replaceAll(/[\\/:*?"<>|]/g, '_'); |
| | | downloadFileFromBlobPart({ fileName: `è´éåå-${suffix}.docx`, source }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VNodeChild } from 'vue'; |
| | | |
| | | import { h } from 'vue'; |
| | | |
| | | import { message, notification } from 'ant-design-vue'; |
| | | |
| | | export interface ImportResultNotifyOptions { |
| | | /** ä¸å¡ä¸»ä½ï¼ç¨äºæ¼æ é¢ä¸å»ºè®®ï¼å¦ã设å¤ãã客æ·ã */ |
| | | subject: string; |
| | | /** æ°å¢æåçæ¡æ° */ |
| | | createCount?: number; |
| | | /** æ´æ°æåçæ¡æ° */ |
| | | updateCount?: number; |
| | | /** 失败æç»ï¼key 为ç¼ç /åç§°ï¼value 为å端失败åå */ |
| | | failures?: null | Record<string, string>; |
| | | /** ãä¸åå¨ã类失败çè½ç¹æç¤ºï¼å¦ãã设å¤ç±»åãã车é´ããå·¥ä½ç«ãä¸ç对åºç¼ç ã */ |
| | | masterDataHint?: string; |
| | | } |
| | | |
| | | /** |
| | | * ç»ä¸å±ç¤º Excel 导å
¥ç»æã |
| | | * |
| | | * åªè¦å端è¿åäºå¤±è´¥æç»ï¼å°±ç»ä¸è½åæç¤ºãæä½æåãââæ¹ä¸ºå¸¸é©»éç¥éè¡ååºå¤±è´¥åå ï¼ |
| | | * å¹¶æå¤±è´¥åå ç»åºä¸ä¸æ¥å¨ä½ï¼å
¨é¨æåæ¶ææç¤ºæåã |
| | | * |
| | | * @returns å®é
åå
¥çæ¡æ°ï¼æ°å¢ + æ´æ°ï¼ï¼ä¾è°ç¨æ¹å³å®æ¯å¦éè¦å·æ°å表 |
| | | */ |
| | | export function notifyImportResult( |
| | | options: ImportResultNotifyOptions, |
| | | ): number { |
| | | const { |
| | | subject, |
| | | createCount = 0, |
| | | updateCount = 0, |
| | | failures, |
| | | masterDataHint, |
| | | } = options; |
| | | const failureEntries = Object.entries(failures ?? {}); |
| | | const importedCount = createCount + updateCount; |
| | | |
| | | if (failureEntries.length === 0) { |
| | | message.success(`导å
¥æåï¼æ°å¢ ${createCount} æ¡ï¼æ´æ° ${updateCount} æ¡`); |
| | | return importedCount; |
| | | } |
| | | |
| | | const nodes: VNodeChild[] = [ |
| | | h( |
| | | 'div', |
| | | `æ°å¢æå ${createCount} æ¡ï¼æ´æ°æå ${updateCount} æ¡ï¼å¤±è´¥ ${failureEntries.length} æ¡ï¼`, |
| | | ), |
| | | ...failureEntries.map(([name, reason]) => h('div', `${name}ï¼${reason}`)), |
| | | ...buildFailureHints(failureEntries.map(([, reason]) => reason), { |
| | | subject, |
| | | masterDataHint, |
| | | }).map((hint) => h('div', { class: 'mt-2 text-red-500' }, hint)), |
| | | ]; |
| | | |
| | | const notify = importedCount === 0 ? notification.error : notification.warning; |
| | | notify({ |
| | | message: |
| | | importedCount === 0 |
| | | ? `${subject}导å
¥å¤±è´¥ï¼${failureEntries.length} è¡å
¨é¨æªå¯¼å
¥` |
| | | : `导å
¥å®æï¼é¨å${subject}导å
¥å¤±è´¥ï¼æå ${importedCount} æ¡ / 失败 ${failureEntries.length} æ¡ï¼`, |
| | | description: () => h('div', nodes), |
| | | duration: null, |
| | | }); |
| | | return importedCount; |
| | | } |
| | | |
| | | /** æå¤±è´¥åå å½çº³ä¸ä¸æ¥å¨ä½ï¼é¿å
ç¨æ·åªçå°ã失败ãå´ä¸ç¥éæ¹åªé */ |
| | | function buildFailureHints( |
| | | reasons: string[], |
| | | { subject, masterDataHint }: Pick<ImportResultNotifyOptions, 'masterDataHint' | 'subject'>, |
| | | ): string[] { |
| | | const hints = new Set<string>(); |
| | | for (const reason of reasons) { |
| | | if (reason.includes('ä¸åå¨')) { |
| | | hints.add( |
| | | `æç¤ºãä¸åå¨ãçï¼è¯·å
ç»´æ¤å¥½${masterDataHint ?? '对åºä¸»æ°æ®'}ï¼ææ Excel ä¸ç弿¹æå·²åå¨çï¼å鿰坼å
¥ã`, |
| | | ); |
| | | } else if (reason.includes('å·²ç»åå¨') || reason.includes('å·²åå¨')) { |
| | | hints.add( |
| | | `æç¤ºãå·²åå¨ãçï¼è¯·ä¿®æ¹ Excel ä¸ç${subject}åç§°æç¼ç ï¼æå¾éãæ¯ææ´æ°ãå鿰坼å
¥ä»¥è¦çå·²ææ°æ®ã`, |
| | | ); |
| | | } else if (reason.includes('ä¸è½ä¸ºç©º') || reason.includes('为空')) { |
| | | hints.add('æç¤ºãä¸è½ä¸ºç©ºãçï¼è¯·è¡¥é½ Excel ä¸å¯¹åºåçå
容å鿰坼å
¥ã'); |
| | | } |
| | | } |
| | | return [...hints]; |
| | | } |
| | |
| | | import type { Recordable } from '@vben/types'; |
| | | |
| | | export * from './import-result'; |
| | | export * from './rangePickerProps'; |
| | | export * from './routerHelper'; |
| | | |
| | |
| | | }, |
| | | }; |
| | | } |
| | | |
| | | /** æä¸å¡æ¥æçéï¼æ¥å£ä»æ¥æ¶å®æ´æ¶é´å符串ã */ |
| | | export function getDateRangePickerDefaultProps() { |
| | | return { |
| | | ...getRangePickerDefaultProps(), |
| | | format: 'YYYY-MM-DD', |
| | | placeholder: ['å¼å§æ¥æ', 'ç»ææ¥æ'], |
| | | showTime: false, |
| | | }; |
| | | } |
| | |
| | | component: 'VbenInput', |
| | | componentProps: { |
| | | placeholder: $t('authentication.usernameTip'), |
| | | autocomplete: 'username', |
| | | 'aria-label': $t('authentication.username'), |
| | | }, |
| | | fieldName: 'username', |
| | | label: $t('authentication.username'), |
| | |
| | | component: 'VbenInputPassword', |
| | | componentProps: { |
| | | placeholder: $t('authentication.passwordTip'), |
| | | autocomplete: 'current-password', |
| | | 'aria-label': $t('authentication.password'), |
| | | }, |
| | | fieldName: 'password', |
| | | label: $t('authentication.password'), |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { AgentTaskApi } from '#/api/agent/conversation'; |
| | | |
| | | import { computed, onBeforeUnmount, ref } from 'vue'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | agentImportErrorMessage, |
| | | cancelAgentTask, |
| | | listAgentTasks, |
| | | streamAgentTaskEvents, |
| | | } from '#/api/agent/conversation'; |
| | | |
| | | defineOptions({ name: 'AgentTaskCenterDrawer' }); |
| | | |
| | | interface Emits { |
| | | (event: 'continue', task: AgentTaskApi.Task): void; |
| | | } |
| | | |
| | | const props = defineProps<{ enabled: boolean }>(); |
| | | const emit = defineEmits<Emits>(); |
| | | |
| | | const visible = ref(false); |
| | | const loading = ref(false); |
| | | const tasks = ref<AgentTaskApi.Task[]>([]); |
| | | let pollTimer: ReturnType<typeof setTimeout> | undefined; |
| | | let eventAbort: AbortController | undefined; |
| | | |
| | | const activeCount = computed( |
| | | () => tasks.value.filter((task) => !isTerminal(task.status)).length, |
| | | ); |
| | | |
| | | function isTerminal(status: string) { |
| | | return ['cancelled', 'failed', 'partially_succeeded', 'succeeded'].includes( |
| | | status, |
| | | ); |
| | | } |
| | | |
| | | function statusText(status: string) { |
| | | const values: Record<string, string> = { |
| | | cancelled: '已忶', |
| | | failed: '失败', |
| | | partially_succeeded: 'é¨å宿', |
| | | queued: 'æéä¸', |
| | | retry_wait: 'çå¾
éè¯', |
| | | running: 'æ§è¡ä¸', |
| | | succeeded: '已宿', |
| | | waiting_approval: 'å¾
审æ¹', |
| | | waiting_input: 'å¾
è¡¥å
', |
| | | }; |
| | | return values[status] ?? status; |
| | | } |
| | | |
| | | function statusColor(status: string) { |
| | | if (status === 'succeeded') return 'green'; |
| | | if (status === 'failed' || status === 'cancelled') return 'red'; |
| | | if (status.startsWith('waiting_') || status === 'retry_wait') return 'orange'; |
| | | return 'blue'; |
| | | } |
| | | |
| | | function stageText(stage: string) { |
| | | const values: Record<string, string> = { |
| | | attachment_download: '读åä¼ä¸éä»¶', |
| | | cancelled: 'ä»»å¡å·²åæ¶', |
| | | document_invalid: 'ææ¡£æ ¡éªå¤±è´¥', |
| | | document_parse: 'è§£æä¸ç»æè¯å«', |
| | | evidence_persist: 'ä¿åè§£æè¯æ®', |
| | | failed: 'ä»»å¡å¤±è´¥', |
| | | queued: 'çå¾
Worker', |
| | | reauth_required: 'éè¦éæ°ææ', |
| | | result_ready: 'åæç»æå·²çæ', |
| | | retry_wait: 'çå¾
èªå¨éè¯', |
| | | starting: 'å夿§è¡', |
| | | worker_lost: 'Worker 夿¬¡å¤±è', |
| | | }; |
| | | return values[stage] ?? stage; |
| | | } |
| | | |
| | | function stopPolling() { |
| | | if (pollTimer) clearTimeout(pollTimer); |
| | | pollTimer = undefined; |
| | | eventAbort?.abort(); |
| | | eventAbort = undefined; |
| | | } |
| | | |
| | | async function loadTasks(silent = false) { |
| | | if (!props.enabled) return; |
| | | if (!silent) loading.value = true; |
| | | try { |
| | | const result = await listAgentTasks({ page_size: 50 }); |
| | | tasks.value = result.items ?? []; |
| | | } catch (error) { |
| | | if (!silent) message.error(agentImportErrorMessage(error)); |
| | | } finally { |
| | | if (!silent) loading.value = false; |
| | | } |
| | | } |
| | | |
| | | function schedulePolling() { |
| | | if (!visible.value || !props.enabled) return; |
| | | pollTimer = setTimeout(async () => { |
| | | await loadTasks(true); |
| | | schedulePolling(); |
| | | }, 3000); |
| | | } |
| | | |
| | | async function watchTask(taskId: string, since = 0) { |
| | | eventAbort?.abort(); |
| | | const controller = new AbortController(); |
| | | eventAbort = controller; |
| | | let cursor = since; |
| | | let retryDelay = 1000; |
| | | while (visible.value && !controller.signal.aborted) { |
| | | try { |
| | | cursor = await streamAgentTaskEvents( |
| | | taskId, |
| | | cursor, |
| | | controller.signal, |
| | | (event) => { |
| | | const index = tasks.value.findIndex((task) => task.task_id === taskId); |
| | | if (index >= 0) tasks.value[index] = { ...tasks.value[index], ...event }; |
| | | }, |
| | | ); |
| | | retryDelay = 1000; |
| | | } catch { |
| | | if (controller.signal.aborted) return; |
| | | } |
| | | await loadTasks(true); |
| | | const latest = tasks.value.find((task) => task.task_id === taskId); |
| | | if (!latest || isTerminal(latest.status) || controller.signal.aborted) return; |
| | | cursor = Math.max(cursor, latest.sequence); |
| | | await new Promise<void>((resolve) => { |
| | | const timer = setTimeout(resolve, retryDelay); |
| | | controller.signal.addEventListener( |
| | | 'abort', |
| | | () => { |
| | | clearTimeout(timer); |
| | | resolve(); |
| | | }, |
| | | { once: true }, |
| | | ); |
| | | }); |
| | | retryDelay = Math.min(retryDelay * 2, 30_000); |
| | | } |
| | | } |
| | | |
| | | async function open(taskId?: string) { |
| | | stopPolling(); |
| | | visible.value = true; |
| | | await loadTasks(); |
| | | schedulePolling(); |
| | | const target = taskId |
| | | ? tasks.value.find((task) => task.task_id === taskId) |
| | | : tasks.value.find((task) => !isTerminal(task.status)); |
| | | if (target && !isTerminal(target.status)) void watchTask(target.task_id, target.sequence); |
| | | } |
| | | |
| | | function close() { |
| | | visible.value = false; |
| | | stopPolling(); |
| | | } |
| | | |
| | | async function cancel(task: AgentTaskApi.Task) { |
| | | try { |
| | | await cancelAgentTask(task.task_id); |
| | | message.success('ä»»å¡å·²åæ¶'); |
| | | await loadTasks(true); |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } |
| | | } |
| | | |
| | | function continueTask(task: AgentTaskApi.Task) { |
| | | close(); |
| | | emit('continue', task); |
| | | } |
| | | |
| | | defineExpose({ open, activeCount }); |
| | | onBeforeUnmount(stopPolling); |
| | | </script> |
| | | |
| | | <template> |
| | | <a-drawer |
| | | :open="visible" |
| | | title="åå°ä»»å¡ä¸å¿" |
| | | width="min(560px, 100vw)" |
| | | :body-style="{ padding: '16px' }" |
| | | @close="close" |
| | | > |
| | | <a-alert |
| | | v-if="!enabled" |
| | | type="info" |
| | | show-icon |
| | | message="æä¹
任塿å¡å°æªå¯ç¨" |
| | | description="é
ç½® PostgreSQLãRedis å¹¶å¯ç¨ MOM_TASK_BACKEND åï¼å¯å¨è¿éæ¥çè·¨é¡µé¢æ§è¡çææ¡£ä»»å¡ã" |
| | | /> |
| | | |
| | | <a-spin v-else :spinning="loading"> |
| | | <a-empty v-if="!tasks.length" description="ææ åå°ä»»å¡" /> |
| | | <div v-else class="space-y-3"> |
| | | <article |
| | | v-for="task in tasks" |
| | | :key="task.task_id" |
| | | class="rounded-lg border border-border bg-card p-4 transition-colors duration-200 hover:border-blue-300" |
| | | > |
| | | <div class="flex items-start justify-between gap-3"> |
| | | <div class="min-w-0"> |
| | | <div class="font-medium text-gray-900 dark:text-gray-100"> |
| | | {{ task.result?.filename || 'ä¼ä¸ææ¡£åæ' }} |
| | | </div> |
| | | <div class="mt-1 text-xs text-gray-500"> |
| | | {{ stageText(task.stage) }} · 第 {{ task.attempt }} 次æ§è¡ |
| | | </div> |
| | | </div> |
| | | <a-tag :color="statusColor(task.status)" class="!mr-0 shrink-0"> |
| | | {{ statusText(task.status) }} |
| | | </a-tag> |
| | | </div> |
| | | |
| | | <div class="mt-3" aria-live="polite"> |
| | | <a-progress |
| | | v-if="task.progress.mode === 'determinate'" |
| | | :percent="task.progress.percent ?? 0" |
| | | :status="task.status === 'failed' ? 'exception' : undefined" |
| | | /> |
| | | <div |
| | | v-else |
| | | class="rounded-md border border-dashed border-border bg-gray-50 px-3 py-2 text-xs text-gray-600 dark:bg-gray-800 dark:text-gray-300" |
| | | role="status" |
| | | > |
| | | å½åé¶æ®µå·¥ä½éå¾
ç¡®å® |
| | | </div> |
| | | <div class="mt-1 flex justify-between gap-3 text-xs text-gray-500"> |
| | | <span>{{ task.message }}</span> |
| | | <span v-if="task.progress.total !== null" class="shrink-0"> |
| | | {{ task.progress.completed }} / {{ task.progress.total }} |
| | | {{ task.progress.unit === 'row' ? 'è¡' : '' }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | |
| | | <a-alert |
| | | v-if="task.error?.message" |
| | | class="mt-3" |
| | | type="error" |
| | | :message="task.error.message" |
| | | /> |
| | | |
| | | <div |
| | | v-if="task.result?.candidate_count !== undefined" |
| | | class="mt-3 rounded-md bg-gray-50 px-3 py-2 text-sm dark:bg-gray-800" |
| | | > |
| | | å·²è¯å« {{ task.result.candidate_count }} æ¡åéæ°æ® |
| | | <span v-if="task.result.warnings?.length" class="ml-2 text-orange-600"> |
| | | {{ task.result.warnings.length }} æ¡æç¤º |
| | | </span> |
| | | </div> |
| | | <a-alert |
| | | v-if="task.result?.review_summary?.requires_review" |
| | | class="mt-2" |
| | | type="warning" |
| | | show-icon |
| | | :message="`éè¦äººå·¥å¤æ ¸ï¼${task.result.review_summary.conflict_groups} ç»å²çªï¼${task.result.review_summary.incomplete_keys} æ¡ç¼ºå°å½å¹¶é®`" |
| | | /> |
| | | <div |
| | | v-else-if="task.result?.review_summary?.exact_duplicates_merged" |
| | | class="mt-2 text-xs text-gray-500" |
| | | > |
| | | å·²èªå¨åå¹¶ |
| | | {{ task.result.review_summary.exact_duplicates_merged }} æ¡å®å
¨éå¤è®°å½ |
| | | </div> |
| | | |
| | | <div class="mt-3 flex flex-wrap justify-end gap-2"> |
| | | <a-button |
| | | v-if="task.allowed_actions.includes('cancel')" |
| | | danger |
| | | @click="cancel(task)" |
| | | > |
| | | åæ¶ä»»å¡ |
| | | </a-button> |
| | | <a-button |
| | | v-if=" |
| | | task.status === 'succeeded' && |
| | | task.result?.attachment_id && |
| | | task.result?.continuation_supported !== false |
| | | " |
| | | type="primary" |
| | | @click="continueTask(task)" |
| | | > |
| | | è¿å
¥è¡¥å½ç¡®è®¤ |
| | | </a-button> |
| | | </div> |
| | | </article> |
| | | </div> |
| | | </a-spin> |
| | | </a-drawer> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FileType } from 'ant-design-vue/es/upload/interface'; |
| | | import type { MenuRecordRaw } from '@vben/types'; |
| | | |
| | | import type { AgentConversationApi } from '#/api/agent/conversation'; |
| | | import type { AgentTaskApi } from '#/api/agent/conversation'; |
| | | |
| | | import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | import { useAccessStore } from '@vben/stores'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | agentImportErrorMessage, |
| | | createDocumentTask, |
| | | createAgentConversation, |
| | | deleteAgentConversation, |
| | | downloadAgentConversationAttachment, |
| | | downloadAgentReport, |
| | | getAgentCapabilities, |
| | | getAgentModels, |
| | | getAuthorizedAgentCapabilities, |
| | | getAgentConversation, |
| | | listAgentConversations, |
| | | sendAgentAction, |
| | | sendAgentMessage, |
| | | streamAgentConversationEvents, |
| | | uploadAgentAttachment, |
| | | } from '#/api/agent/conversation'; |
| | | |
| | | import TaskCenterDrawer from './components/TaskCenterDrawer.vue'; |
| | | |
| | | defineOptions({ name: 'AgentDataEntry' }); |
| | | |
| | | const accessStore = useAccessStore(); |
| | | |
| | | const MAX_BYTES = 8 * 1024 * 1024; |
| | | const ACCEPT = '.csv,.xlsx,.docx,.pdf'; |
| | | |
| | | const authorizationScope = ref(''); |
| | | const models = ref<AgentConversationApi.BusinessModelInfo[]>([]); |
| | | const selectedModelId = ref<number>(); |
| | | const conversations = ref<AgentConversationApi.ConversationSummary[]>([]); |
| | | const active = ref<AgentConversationApi.Conversation>(); |
| | | const loading = ref(false); |
| | | const busy = ref(false); |
| | | const creating = ref(false); |
| | | const newOpen = ref(false); |
| | | const newScenarios = ref<string[]>([]); |
| | | const newText = ref(''); |
| | | const newTimeRange = ref<string[]>([]); |
| | | const newRecordLimit = ref<number>(); |
| | | const newIndustry = ref(''); |
| | | const newSkipRestDays = ref(false); |
| | | const newRestDayDescription = ref(''); |
| | | const newRegion = ref(''); |
| | | type ConstraintKey = |
| | | | 'industry' |
| | | | 'instruction' |
| | | | 'record_limit' |
| | | | 'region' |
| | | | 'rest_day_description' |
| | | | 'scenario_paths' |
| | | | 'skip_rest_days' |
| | | | 'time_range'; |
| | | const newPriorities = ref<Record<ConstraintKey, boolean>>({ |
| | | industry: false, |
| | | instruction: false, |
| | | record_limit: false, |
| | | region: false, |
| | | rest_day_description: false, |
| | | scenario_paths: false, |
| | | skip_rest_days: false, |
| | | time_range: false, |
| | | }); |
| | | const newFiles = ref<File[]>([]); |
| | | const newAttachmentIds = ref<number[]>([]); |
| | | const durableTasksEnabled = ref(false); |
| | | const taskCenter = ref<{ open: (taskId?: string) => Promise<void> }>(); |
| | | const interruptFile = ref<File>(); |
| | | const mappingDraft = ref<Record<string, string>>({}); |
| | | const reviseText = ref(''); |
| | | const syncMes = ref(true); |
| | | const eventState = ref<'connected' | 'connecting' | 'idle' | 'retrying'>('idle'); |
| | | interface ExecutionProgress { |
| | | candidateCount: number; |
| | | issueCount: number; |
| | | label: string; |
| | | modelId?: number; |
| | | percent: number; |
| | | stage: string; |
| | | } |
| | | const executionProgress = ref<ExecutionProgress>(); |
| | | let eventAbort: AbortController | undefined; |
| | | let eventGeneration = 0; |
| | | let eventRefreshTimer: ReturnType<typeof setTimeout> | undefined; |
| | | |
| | | const attachmentPreview = ref<{ |
| | | filename: string; |
| | | kind: 'image' | 'pdf'; |
| | | open: boolean; |
| | | url: string; |
| | | }>({ filename: '', kind: 'image', open: false, url: '' }); |
| | | |
| | | const modelOptions = computed(() => |
| | | models.value.map((model) => ({ |
| | | label: `${model.name || model.model} · ${model.platform}`, |
| | | value: model.id, |
| | | })), |
| | | ); |
| | | const interrupt = computed(() => active.value?.interrupts?.at(-1)); |
| | | const card = computed(() => interrupt.value?.card); |
| | | const actions = computed(() => { |
| | | const list = [...(card.value?.actions ?? [])]; |
| | | // å¡çå¯è½å¨æäº¤å¼å
³å
³éæ¶çæï¼ä»¥æå¡ç«¯å½åå¼å
³ä¸ºåè¡¥å确认å¨ä½ |
| | | if ( |
| | | card.value?.kind === 'preview' && |
| | | active.value?.commit_enabled && |
| | | !list.includes('confirm') |
| | | ) { |
| | | list.push('confirm'); |
| | | } |
| | | return list; |
| | | }); |
| | | const isGatewayModule = computed(() => active.value?.module_id === 'mdm.item'); |
| | | const previewValueLabels: Record<string, Record<string, string>> = { |
| | | status: { '0': 'å¯ç¨', '1': 'åç¨' }, |
| | | types: { |
| | | '1': 'IQCï¼æ¥ææ£éªï¼', |
| | | '2': 'IPQCï¼è¿ç¨æ£éªï¼', |
| | | '3': 'OQCï¼åºè´§æ£éªï¼', |
| | | '4': 'RQCï¼éææ£éªï¼', |
| | | }, |
| | | }; |
| | | const previewColumns = computed(() => [ |
| | | { title: 'è¡å·', dataIndex: 'row_index', key: 'row_index', width: 60 }, |
| | | ...(active.value?.row_fields ?? []).map((field) => ({ |
| | | title: field.label, |
| | | dataIndex: field.name, |
| | | key: field.name, |
| | | width: 140, |
| | | customRender: ({ text }: { text: unknown }) => |
| | | previewValueLabels[field.name]?.[String(text ?? '')] ?? text, |
| | | })), |
| | | ]); |
| | | const previewRows = computed(() => |
| | | (active.value?.rows ?? []).map((row, index) => ({ |
| | | candidate_id: row.candidate_id, |
| | | row_index: index + 1, |
| | | ...row.values, |
| | | })), |
| | | ); |
| | | const eventStateText = computed(() => { |
| | | if (active.value?.finished) return '已忥'; |
| | | return { |
| | | connected: '宿¶æ´æ°', |
| | | connecting: 'æ£å¨è¿æ¥', |
| | | idle: 'æªè¿æ¥', |
| | | retrying: 'æçº¿éè¿', |
| | | }[eventState.value]; |
| | | }); |
| | | const eventStateColor = computed(() => { |
| | | if (active.value?.finished || eventState.value === 'connected') return 'green'; |
| | | if (eventState.value === 'retrying') return 'orange'; |
| | | return 'blue'; |
| | | }); |
| | | |
| | | interface ScenarioOption { |
| | | description: string; |
| | | label: string; |
| | | name: string; |
| | | value: string; |
| | | } |
| | | |
| | | function flattenMenuScenarios( |
| | | menus: MenuRecordRaw[], |
| | | parentNames: string[] = [], |
| | | ): ScenarioOption[] { |
| | | return menus.flatMap((menu) => { |
| | | const names = [...parentNames, menu.name].filter(Boolean); |
| | | const current = menu.show === false |
| | | ? [] |
| | | : [ |
| | | { |
| | | description: menu.path, |
| | | label: names.join(' / '), |
| | | name: menu.name, |
| | | value: menu.path, |
| | | }, |
| | | ]; |
| | | return [ |
| | | ...current, |
| | | ...flattenMenuScenarios(menu.children ?? [], names), |
| | | ]; |
| | | }); |
| | | } |
| | | |
| | | const scenarioOptions = computed(() => { |
| | | const seen = new Set<string>(); |
| | | return flattenMenuScenarios(accessStore.accessMenus).filter((item) => { |
| | | if (!item.value || seen.has(item.value)) return false; |
| | | seen.add(item.value); |
| | | return true; |
| | | }); |
| | | }); |
| | | |
| | | function togglePriority(key: ConstraintKey) { |
| | | newPriorities.value[key] = !newPriorities.value[key]; |
| | | } |
| | | |
| | | function buildConstraints(): AgentConversationApi.ConversationConstraints { |
| | | const scenarioByPath = new Map( |
| | | scenarioOptions.value.map((item) => [item.value, item.label]), |
| | | ); |
| | | return { |
| | | industry: newIndustry.value.trim() || undefined, |
| | | instruction: newText.value.trim() || undefined, |
| | | priority_fields: Object.entries(newPriorities.value) |
| | | .filter(([, enabled]) => enabled) |
| | | .map(([key]) => key), |
| | | record_limit: newRecordLimit.value, |
| | | region: newRegion.value.trim() || undefined, |
| | | rest_day_description: newRestDayDescription.value.trim() || undefined, |
| | | scenario_paths: newScenarios.value.map((path) => |
| | | `${scenarioByPath.get(path) ?? path} (${path})`, |
| | | ), |
| | | skip_rest_days: |
| | | newSkipRestDays.value || newPriorities.value.skip_rest_days |
| | | ? newSkipRestDays.value |
| | | : undefined, |
| | | time_end: newTimeRange.value[1], |
| | | time_start: newTimeRange.value[0], |
| | | }; |
| | | } |
| | | |
| | | function hasNewConditions() { |
| | | return Boolean( |
| | | newScenarios.value.length || |
| | | newTimeRange.value.length || |
| | | newRecordLimit.value || |
| | | newIndustry.value.trim() || |
| | | newSkipRestDays.value || |
| | | newRestDayDescription.value.trim() || |
| | | newRegion.value.trim(), |
| | | ); |
| | | } |
| | | |
| | | function resetNewConditions() { |
| | | newScenarios.value = []; |
| | | newText.value = ''; |
| | | newTimeRange.value = []; |
| | | newRecordLimit.value = undefined; |
| | | newIndustry.value = ''; |
| | | newSkipRestDays.value = false; |
| | | newRestDayDescription.value = ''; |
| | | newRegion.value = ''; |
| | | for (const key of Object.keys(newPriorities.value) as ConstraintKey[]) { |
| | | newPriorities.value[key] = false; |
| | | } |
| | | } |
| | | |
| | | watch( |
| | | () => interrupt.value?.interrupt_id, |
| | | () => { |
| | | reviseText.value = ''; |
| | | mappingDraft.value = {}; |
| | | interruptFile.value = undefined; |
| | | const value = card.value?.sync_mes; |
| | | syncMes.value = |
| | | typeof value === 'boolean' |
| | | ? value |
| | | : (active.value?.sync_mes ?? active.value?.mes_sync_default ?? true); |
| | | }, |
| | | ); |
| | | |
| | | function statusText(status?: string) { |
| | | const map: Record<string, string> = { |
| | | SUCCEEDED: '导å
¥æå', |
| | | PARTIAL_FAILED: 'é¨å失败', |
| | | FAILED: '失败', |
| | | UNKNOWN: 'ç»ææªç¥', |
| | | RUNNING: 'æäº¤ä¸', |
| | | attached: 'å·²ä¸ä¼ ', |
| | | blocked: '已黿', |
| | | cancelled: '已忶', |
| | | classified: 'å·²è¯å«åºæ¯', |
| | | commit_disabled: 'æªæäº¤', |
| | | confirmed: '已确认', |
| | | created: 'å¾
ä¸ä¼ æä»¶', |
| | | queued: 'æéå¤çä¸', |
| | | error: 'åºé', |
| | | failed: '失败', |
| | | mapping_applied: 'æ å°å·²æ´æ°', |
| | | needs_review: 'å¾
确认', |
| | | parsed: '已解æ', |
| | | preview_ready: 'å¾
确认', |
| | | revise: 'å¾
éæ°æ ¡éª', |
| | | stale: 'å·²è¿æï¼é鿣', |
| | | validated: 'å¾
确认', |
| | | }; |
| | | return map[status ?? ''] ?? status ?? 'è¿è¡ä¸'; |
| | | } |
| | | |
| | | function statusColor(status?: string) { |
| | | if (status === 'SUCCEEDED' || status === 'confirmed') return 'green'; |
| | | if ( |
| | | status === 'PARTIAL_FAILED' || |
| | | status === 'revise' || |
| | | status === 'stale' |
| | | ) { |
| | | return 'orange'; |
| | | } |
| | | if ( |
| | | status === 'FAILED' || |
| | | status === 'failed' || |
| | | status === 'error' || |
| | | status === 'blocked' || |
| | | status === 'cancelled' |
| | | ) { |
| | | return 'red'; |
| | | } |
| | | return 'blue'; |
| | | } |
| | | |
| | | function checkFile(raw: File) { |
| | | const name = raw.name.toLowerCase(); |
| | | if (!ACCEPT.split(',').some((ext) => name.endsWith(ext))) { |
| | | message.error('ä»
æ¯æ CSVãXLSXãDOCX æ PDF æä»¶'); |
| | | return false; |
| | | } |
| | | if (raw.size > MAX_BYTES) { |
| | | message.error('æä»¶ä¸è½è¶
è¿ 8 MiB'); |
| | | return false; |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | function beforeNewUpload(fileInfo: FileType) { |
| | | const raw = fileInfo as unknown as File; |
| | | if (!checkFile(raw)) return false; |
| | | const duplicate = newFiles.value.some( |
| | | (item) => |
| | | item.name === raw.name && item.size === raw.size && item.lastModified === raw.lastModified, |
| | | ); |
| | | if (!duplicate) newFiles.value = [...newFiles.value, raw].slice(0, 20); |
| | | newAttachmentIds.value = []; |
| | | return false; |
| | | } |
| | | |
| | | function clearNewFiles() { |
| | | newFiles.value = []; |
| | | newAttachmentIds.value = []; |
| | | } |
| | | |
| | | function beforeInterruptUpload(fileInfo: FileType) { |
| | | const raw = fileInfo as unknown as File; |
| | | if (!checkFile(raw)) return false; |
| | | interruptFile.value = raw; |
| | | return false; |
| | | } |
| | | |
| | | async function boot() { |
| | | loading.value = true; |
| | | try { |
| | | const [capabilities, authorization, list, modelList] = await Promise.all([ |
| | | getAgentCapabilities(), |
| | | getAuthorizedAgentCapabilities(), |
| | | listAgentConversations({ page_size: 50 }), |
| | | getAgentModels().catch(() => []), |
| | | ]); |
| | | authorizationScope.value = authorization.scope_summary; |
| | | models.value = modelList; |
| | | if (!selectedModelId.value) { |
| | | selectedModelId.value = active.value?.model_id ?? modelList[0]?.id; |
| | | } |
| | | const visibleScenarioPaths = new Set(scenarioOptions.value.map((item) => item.value)); |
| | | newScenarios.value = newScenarios.value.filter((path) => |
| | | visibleScenarioPaths.has(path), |
| | | ); |
| | | durableTasksEnabled.value = capabilities.durable_tasks?.enabled ?? false; |
| | | conversations.value = list.items ?? []; |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | async function ensureNewAttachments() { |
| | | if (newAttachmentIds.value.length === newFiles.value.length) { |
| | | return newAttachmentIds.value; |
| | | } |
| | | newAttachmentIds.value = []; |
| | | for (const file of newFiles.value) { |
| | | newAttachmentIds.value.push(await uploadAgentAttachment(file)); |
| | | } |
| | | return newAttachmentIds.value; |
| | | } |
| | | |
| | | async function reloadConversations() { |
| | | try { |
| | | const list = await listAgentConversations({ page_size: 50 }); |
| | | conversations.value = list.items ?? []; |
| | | } catch { |
| | | // åè¡¨å·æ°å¤±è´¥ä¸é»å¡ä¸»æµç¨ |
| | | } |
| | | } |
| | | |
| | | function abortEventStream() { |
| | | eventGeneration += 1; |
| | | eventAbort?.abort(); |
| | | eventAbort = undefined; |
| | | if (eventRefreshTimer) clearTimeout(eventRefreshTimer); |
| | | eventRefreshTimer = undefined; |
| | | eventState.value = 'idle'; |
| | | } |
| | | |
| | | async function refreshActive(conversationId: string) { |
| | | const snapshot = await getAgentConversation(conversationId); |
| | | if (active.value?.conversation_id !== conversationId) return; |
| | | active.value = snapshot; |
| | | await reloadConversations(); |
| | | if (snapshot.finished) abortEventStream(); |
| | | } |
| | | |
| | | function queueEventRefresh(conversationId: string) { |
| | | if (eventRefreshTimer) clearTimeout(eventRefreshTimer); |
| | | eventRefreshTimer = setTimeout(() => { |
| | | eventRefreshTimer = undefined; |
| | | void refreshActive(conversationId).catch(() => { |
| | | // SSE ä¼ç»§ç»éè¿ï¼å¿«ç
§è¯»å失败ä¸éå¤å¼¹çªææ°æä½ã |
| | | }); |
| | | }, 120); |
| | | } |
| | | |
| | | function handleConversationEvent( |
| | | conversationId: string, |
| | | event: AgentConversationApi.ConversationEvent, |
| | | ) { |
| | | if (active.value?.conversation_id !== conversationId) return; |
| | | if (event.type === 'progress') { |
| | | const payload = event.payload; |
| | | executionProgress.value = { |
| | | candidateCount: Number(payload.candidate_count ?? 0), |
| | | issueCount: Number(payload.issue_count ?? 0), |
| | | label: String(payload.label ?? 'æ£å¨å¤ç'), |
| | | modelId: |
| | | typeof payload.model_id === 'number' ? payload.model_id : undefined, |
| | | percent: Math.min(100, Math.max(0, Number(payload.percent ?? 0))), |
| | | stage: String(payload.stage ?? 'processing'), |
| | | }; |
| | | return; |
| | | } |
| | | queueEventRefresh(conversationId); |
| | | } |
| | | |
| | | function waitForRetry(delay: number, signal: AbortSignal) { |
| | | return new Promise<void>((resolve) => { |
| | | const timer = setTimeout(resolve, delay); |
| | | signal.addEventListener( |
| | | 'abort', |
| | | () => { |
| | | clearTimeout(timer); |
| | | resolve(); |
| | | }, |
| | | { once: true }, |
| | | ); |
| | | }); |
| | | } |
| | | |
| | | function startEventStream(conversationId: string) { |
| | | abortEventStream(); |
| | | const generation = eventGeneration; |
| | | const controller = new AbortController(); |
| | | eventAbort = controller; |
| | | eventState.value = 'connecting'; |
| | | void (async () => { |
| | | let cursor = 0; |
| | | let retryDelay = 1000; |
| | | while (!controller.signal.aborted && generation === eventGeneration) { |
| | | try { |
| | | cursor = await streamAgentConversationEvents( |
| | | conversationId, |
| | | cursor, |
| | | controller.signal, |
| | | (event) => handleConversationEvent(conversationId, event), |
| | | () => { |
| | | eventState.value = 'connected'; |
| | | retryDelay = 1000; |
| | | }, |
| | | ); |
| | | if (!controller.signal.aborted) eventState.value = 'connecting'; |
| | | } catch { |
| | | if (controller.signal.aborted) return; |
| | | eventState.value = 'retrying'; |
| | | try { |
| | | await refreshActive(conversationId); |
| | | } catch { |
| | | // 轮询å
åºå¤±è´¥åç»§ç»ææ°éé¿ã |
| | | } |
| | | await waitForRetry(retryDelay, controller.signal); |
| | | retryDelay = Math.min(retryDelay * 2, 30_000); |
| | | } |
| | | } |
| | | })(); |
| | | } |
| | | |
| | | async function openConversation(conversationId: string) { |
| | | if (busy.value) return; |
| | | busy.value = true; |
| | | try { |
| | | const conversation = await getAgentConversation(conversationId); |
| | | active.value = conversation; |
| | | selectedModelId.value = conversation.model_id ?? models.value[0]?.id; |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | function changeModel(value: number) { |
| | | selectedModelId.value = value; |
| | | } |
| | | |
| | | async function createConversation() { |
| | | if (!newFiles.value.length && !newText.value.trim() && !hasNewConditions()) { |
| | | message.warning('请ä¸ä¼ æä»¶ãå¡«åè¡¥å½è¯´ææè®¾ç½®è¡¥å½æ¡ä»¶'); |
| | | return; |
| | | } |
| | | creating.value = true; |
| | | try { |
| | | if (newFiles.value.length > 1) { |
| | | message.warning('å¤æä»¶è¯·ä½¿ç¨åå°åæï¼å½å¹¶æ£æ¥å®æååè¿å
¥è¡¥å½'); |
| | | return; |
| | | } |
| | | const [attachmentId] = await ensureNewAttachments(); |
| | | const description = newText.value.trim(); |
| | | const baseTitle = |
| | | newFiles.value[0]?.name || description.slice(0, 60) || 'æ°å»ºè¡¥å½'; |
| | | active.value = await createAgentConversation({ |
| | | attachment_id: attachmentId ? String(attachmentId) : undefined, |
| | | constraints: buildConstraints(), |
| | | model_id: selectedModelId.value, |
| | | text: description || undefined, |
| | | title: baseTitle, |
| | | }); |
| | | newOpen.value = false; |
| | | clearNewFiles(); |
| | | resetNewConditions(); |
| | | await reloadConversations(); |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } finally { |
| | | creating.value = false; |
| | | } |
| | | } |
| | | |
| | | async function startBackgroundAnalysis() { |
| | | if (!durableTasksEnabled.value) { |
| | | message.info('æä¹
任塿å¡å°æªå¯ç¨'); |
| | | return; |
| | | } |
| | | if (!newFiles.value.length) { |
| | | message.warning('åå°åæéè¦å
éæ©æ°æ®æä»¶'); |
| | | return; |
| | | } |
| | | creating.value = true; |
| | | try { |
| | | const attachmentIds = await ensureNewAttachments(); |
| | | if (!attachmentIds.length) return; |
| | | const task = await createDocumentTask({ |
| | | attachment_ids: attachmentIds, |
| | | constraints: buildConstraints(), |
| | | }); |
| | | newOpen.value = false; |
| | | resetNewConditions(); |
| | | message.success('åå°åæä»»å¡å·²å建ï¼å¯å
³é页é¢åç»§ç»æ§è¡'); |
| | | await taskCenter.value?.open(task.task_id); |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } finally { |
| | | creating.value = false; |
| | | } |
| | | } |
| | | |
| | | async function continueFromTask(task: AgentTaskApi.Task) { |
| | | if (task.result?.continuation_supported === false) { |
| | | message.info('å¤æä»¶ç»æéå
å¨è¯æ®æç»ä¸å¤çå²çªï¼å½åæä¸è¿å
¥åæä»¶æäº¤'); |
| | | return; |
| | | } |
| | | const attachmentId = task.result?.attachment_id; |
| | | if (!attachmentId) { |
| | | message.error('ä»»å¡ç»æç¼ºå°éä»¶å¼ç¨ï¼æ æ³è¿å
¥è¡¥å½'); |
| | | return; |
| | | } |
| | | busy.value = true; |
| | | try { |
| | | active.value = await createAgentConversation({ |
| | | attachment_id: String(attachmentId), |
| | | model_id: selectedModelId.value, |
| | | module_id: task.result?.module_id || undefined, |
| | | text: 'åå°ææ¡£åæå·²å®æï¼è¯·ç»§ç»è¿è¡åæ®µæ ¸éªåè¡¥å½ç¡®è®¤ã', |
| | | title: task.result?.filename || 'åå°åæè¡¥å½', |
| | | }); |
| | | await reloadConversations(); |
| | | message.success('å·²è¿å
¥è¡¥å½ç¡®è®¤æµç¨'); |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | async function apply(result: AgentConversationApi.Conversation, tip?: string) { |
| | | active.value = result; |
| | | selectedModelId.value = result.model_id ?? selectedModelId.value; |
| | | if (tip) message.success(tip); |
| | | await reloadConversations(); |
| | | } |
| | | |
| | | async function act( |
| | | action: string, |
| | | payload: Record<string, unknown> = {}, |
| | | tip?: string, |
| | | ) { |
| | | const current = active.value; |
| | | const pending = interrupt.value; |
| | | if (!current || !pending) return; |
| | | busy.value = true; |
| | | try { |
| | | const result = await sendAgentAction(current.conversation_id, { |
| | | action, |
| | | interrupt_id: pending.interrupt_id, |
| | | model_id: selectedModelId.value, |
| | | payload, |
| | | revision: pending.revision, |
| | | }); |
| | | await apply(result, tip); |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | function chooseModule(moduleId: string) { |
| | | return act('choose_scenario', { module_id: moduleId }, '已鿩ä¸å¡åºæ¯'); |
| | | } |
| | | |
| | | async function confirmMapping() { |
| | | const missing = card.value?.missing_fields ?? []; |
| | | const mapping: Record<string, string> = {}; |
| | | for (const field of missing) { |
| | | const column = mappingDraft.value[field.name]; |
| | | if (!column) { |
| | | message.warning(`请为ã${field.label}ã鿩坹åºå`); |
| | | return; |
| | | } |
| | | mapping[field.name] = column; |
| | | } |
| | | await act('confirm_mapping', { mapping }, 'æ å°å·²æäº¤ï¼éæ°æ ¡éª'); |
| | | } |
| | | |
| | | async function uploadForFileInterrupt() { |
| | | const current = active.value; |
| | | if (!current) return; |
| | | if (!interruptFile.value) { |
| | | message.warning('请å
éæ©æä»¶'); |
| | | return; |
| | | } |
| | | busy.value = true; |
| | | try { |
| | | const attachmentId = await uploadAgentAttachment(interruptFile.value); |
| | | const result = await sendAgentMessage(current.conversation_id, { |
| | | attachment_id: String(attachmentId), |
| | | model_id: selectedModelId.value, |
| | | text: 'å·²ä¸ä¼ è¦è¡¥å½çæä»¶ã', |
| | | }); |
| | | interruptFile.value = undefined; |
| | | await apply(result); |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | async function revise() { |
| | | const text = reviseText.value.trim(); |
| | | const payload: Record<string, unknown> = {}; |
| | | if (text) payload.text = text; |
| | | if (isGatewayModule.value) payload.sync_mes = syncMes.value; |
| | | if (!text && !isGatewayModule.value) { |
| | | message.warning('请è¾å
¥ä¿®è®¢å
容ï¼ä¾å¦â第2è¡ åç§° æ¹ä¸º XXXâ'); |
| | | return; |
| | | } |
| | | await act('revise', payload, 'å·²æäº¤ä¿®è®¢ï¼éæ°æ ¡éª'); |
| | | reviseText.value = ''; |
| | | } |
| | | |
| | | async function toggleSync(value: boolean) { |
| | | syncMes.value = value; |
| | | await act('revise', { sync_mes: value }, 'å·²æ´æ°åæ¥é项ï¼éæ°æ ¡éª'); |
| | | } |
| | | |
| | | function confirmImport() { |
| | | return act('confirm', {}, 'å·²ç¡®è®¤ï¼æ£å¨æäº¤'); |
| | | } |
| | | |
| | | function cancelConversation() { |
| | | return act('cancel', {}, 'å·²åæ¶æ¬æ¬¡è¡¥å½'); |
| | | } |
| | | |
| | | async function downloadReport() { |
| | | const current = active.value; |
| | | if (!current) return; |
| | | try { |
| | | const response = await downloadAgentReport(current.conversation_id); |
| | | const disposition = String( |
| | | response.headers?.['content-disposition'] ?? '', |
| | | ); |
| | | const match = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(disposition); |
| | | downloadFileFromBlobPart({ |
| | | fileName: match?.[1] || `${current.title || 'è¡¥å½åæ§'}.xlsx`, |
| | | source: response.data, |
| | | }); |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } |
| | | } |
| | | |
| | | function decodeAttachmentFilename( |
| | | headers: Record<string, string> | undefined, |
| | | fallback: string, |
| | | ) { |
| | | const encoded = headers?.['x-agent-filename-b64']; |
| | | if (!encoded) return fallback; |
| | | try { |
| | | const binary = atob(encoded); |
| | | const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0)); |
| | | return new TextDecoder().decode(bytes); |
| | | } catch { |
| | | return fallback; |
| | | } |
| | | } |
| | | |
| | | function canPreview(filename?: string) { |
| | | return /\.(pdf|png|jpe?g|gif|webp|bmp)$/i.test(filename || ''); |
| | | } |
| | | |
| | | async function previewAttachment( |
| | | attachment: AgentConversationApi.ConversationAttachment, |
| | | ) { |
| | | const current = active.value; |
| | | if (!current) return; |
| | | try { |
| | | const response = await downloadAgentConversationAttachment( |
| | | current.conversation_id, |
| | | attachment.attachment_id, |
| | | 'inline', |
| | | ); |
| | | const filename = decodeAttachmentFilename( |
| | | response.headers, |
| | | attachment.filename || `éä»¶-${attachment.attachment_id}`, |
| | | ); |
| | | closeAttachmentPreview(); |
| | | attachmentPreview.value = { |
| | | filename, |
| | | kind: /\.pdf$/i.test(filename) ? 'pdf' : 'image', |
| | | open: true, |
| | | url: URL.createObjectURL(response.data), |
| | | }; |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } |
| | | } |
| | | |
| | | function closeAttachmentPreview() { |
| | | if (attachmentPreview.value.url) URL.revokeObjectURL(attachmentPreview.value.url); |
| | | attachmentPreview.value = { filename: '', kind: 'image', open: false, url: '' }; |
| | | } |
| | | |
| | | async function downloadAttachment( |
| | | attachment: AgentConversationApi.ConversationAttachment, |
| | | ) { |
| | | const current = active.value; |
| | | if (!current) return; |
| | | try { |
| | | const response = await downloadAgentConversationAttachment( |
| | | current.conversation_id, |
| | | attachment.attachment_id, |
| | | 'download', |
| | | ); |
| | | downloadFileFromBlobPart({ |
| | | fileName: decodeAttachmentFilename( |
| | | response.headers, |
| | | attachment.filename || `éä»¶-${attachment.attachment_id}`, |
| | | ), |
| | | source: response.data, |
| | | }); |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } |
| | | } |
| | | |
| | | async function removeConversation( |
| | | item: AgentConversationApi.ConversationSummary, |
| | | ) { |
| | | try { |
| | | await deleteAgentConversation(item.conversation_id); |
| | | if (active.value?.conversation_id === item.conversation_id) { |
| | | abortEventStream(); |
| | | active.value = undefined; |
| | | } |
| | | await reloadConversations(); |
| | | message.success('ä¼è¯åéä»¶å·²å é¤'); |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } |
| | | } |
| | | |
| | | onMounted(boot); |
| | | onBeforeUnmount(() => { |
| | | abortEventStream(); |
| | | closeAttachmentPreview(); |
| | | }); |
| | | |
| | | watch( |
| | | () => active.value?.conversation_id, |
| | | (conversationId) => { |
| | | executionProgress.value = undefined; |
| | | if (conversationId && !active.value?.finished) startEventStream(conversationId); |
| | | else abortEventStream(); |
| | | }, |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <div class="flex h-full gap-3 p-3"> |
| | | <aside |
| | | class="flex w-64 shrink-0 flex-col overflow-hidden rounded-lg border border-border bg-card" |
| | | > |
| | | <div class="border-b border-border p-3"> |
| | | <a-button type="primary" block @click="newOpen = true"> |
| | | æ°å»ºè¡¥å½ |
| | | </a-button> |
| | | <div class="mt-2 text-xs text-gray-500"> |
| | | ä¸ä¼ æä»¶åèªå¨è¯å«ä¸å¡åºæ¯ï¼ç¡®è®¤åä¼å®æ´æ ¡éªã |
| | | </div> |
| | | </div> |
| | | <div class="flex-1 overflow-y-auto p-2"> |
| | | <a-spin :spinning="loading"> |
| | | <a-empty |
| | | v-if="!conversations.length" |
| | | description="ææ è¡¥å½ä¼è¯" |
| | | /> |
| | | <div |
| | | v-for="item in conversations" |
| | | :key="item.conversation_id" |
| | | class="mb-1 flex w-full cursor-pointer items-start gap-1 rounded-md p-2 text-left transition-colors hover:bg-gray-100 dark:hover:bg-gray-800" |
| | | :class="{ |
| | | 'bg-blue-50 dark:bg-blue-900/30': |
| | | item.conversation_id === active?.conversation_id, |
| | | }" |
| | | role="button" |
| | | tabindex="0" |
| | | @click="openConversation(item.conversation_id)" |
| | | @keydown.enter="openConversation(item.conversation_id)" |
| | | > |
| | | <div class="min-w-0 flex-1"> |
| | | <div class="truncate text-sm font-medium"> |
| | | {{ item.title || 'æªå½åè¡¥å½' }} |
| | | </div> |
| | | <div class="mt-1 flex items-center gap-2 text-xs text-gray-500"> |
| | | <a-tag :color="statusColor(item.status)" class="!mr-0"> |
| | | {{ statusText(item.status) }} |
| | | </a-tag> |
| | | <span class="truncate"> |
| | | {{ item.module_title || 'å¾
è¯å«' }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | <a-popconfirm |
| | | cancel-text="åæ¶" |
| | | ok-text="å é¤" |
| | | title="å é¤ä¼è¯åå·²ä¸ä¼ éä»¶ï¼" |
| | | @confirm="removeConversation(item)" |
| | | > |
| | | <a-button |
| | | class="shrink-0" |
| | | danger |
| | | size="small" |
| | | type="text" |
| | | @click.stop |
| | | > |
| | | å é¤ |
| | | </a-button> |
| | | </a-popconfirm> |
| | | </div> |
| | | </a-spin> |
| | | </div> |
| | | </aside> |
| | | |
| | | <section |
| | | class="flex min-w-0 flex-1 flex-col overflow-hidden rounded-lg border border-border bg-card" |
| | | > |
| | | <header |
| | | class="flex items-center justify-between border-b border-border px-4 py-3" |
| | | > |
| | | <div class="min-w-0"> |
| | | <div class="truncate text-base font-semibold"> |
| | | {{ active?.title || 'æ¨¡åæ°æ®è¡¥å½' }} |
| | | </div> |
| | | <div class="mt-1 flex items-center gap-2 text-xs text-gray-500"> |
| | | <a-tag :color="statusColor(active?.status)" class="!mr-0"> |
| | | {{ statusText(active?.status) }} |
| | | </a-tag> |
| | | <span v-if="active?.module_title">{{ active.module_title }}</span> |
| | | <span v-if="active?.total">å
± {{ active.total }} è¡</span> |
| | | <span v-if="active?.data_scope_summary || authorizationScope"> |
| | | æ°æ®èå´ï¼{{ active?.data_scope_summary || authorizationScope }} |
| | | </span> |
| | | <span |
| | | v-if="modelOptions.length" |
| | | class="flex items-center gap-1" |
| | | title="忢åä»ä¸ä¸æ¬¡åæå¼å§çæ" |
| | | > |
| | | 模åï¼ |
| | | <a-select |
| | | :disabled="busy" |
| | | :options="modelOptions" |
| | | :value="selectedModelId" |
| | | class="w-56" |
| | | size="small" |
| | | @change="changeModel" |
| | | /> |
| | | </span> |
| | | <a-tag |
| | | v-if="active" |
| | | :color="eventStateColor" |
| | | class="!mr-0" |
| | | aria-live="polite" |
| | | > |
| | | {{ eventStateText }} |
| | | </a-tag> |
| | | </div> |
| | | <div |
| | | v-if="active?.attachments?.length" |
| | | class="mt-1 flex flex-wrap items-center gap-2 text-xs text-gray-500" |
| | | > |
| | | <span>éä»¶ï¼</span> |
| | | <span |
| | | v-for="attachment in active.attachments" |
| | | :key="attachment.attachment_id" |
| | | class="flex items-center gap-1 rounded border border-border px-2 py-0.5" |
| | | > |
| | | <span class="max-w-60 truncate"> |
| | | {{ attachment.filename || `éä»¶ ${attachment.attachment_id}` }} |
| | | </span> |
| | | <a-button |
| | | v-if="canPreview(attachment.filename)" |
| | | class="!h-5 !px-1" |
| | | size="small" |
| | | type="link" |
| | | @click="previewAttachment(attachment)" |
| | | > |
| | | é¢è§ |
| | | </a-button> |
| | | <a-button |
| | | class="!h-5 !px-1" |
| | | size="small" |
| | | type="link" |
| | | @click="downloadAttachment(attachment)" |
| | | > |
| | | ä¸è½½ |
| | | </a-button> |
| | | </span> |
| | | </div> |
| | | </div> |
| | | <div class="flex shrink-0 items-center gap-2"> |
| | | <a-tooltip |
| | | :title=" |
| | | durableTasksEnabled |
| | | ? 'æ¥çè·¨é¡µé¢æ§è¡çææ¡£ä»»å¡' |
| | | : 'æä¹
任塿å¡å°æªå¯ç¨' |
| | | " |
| | | > |
| | | <a-button @click="taskCenter?.open()">åå°ä»»å¡</a-button> |
| | | </a-tooltip> |
| | | <a-button |
| | | v-if="active?.receipt?.command_id" |
| | | :disabled="busy" |
| | | @click="downloadReport" |
| | | > |
| | | ä¸è½½åæ§ |
| | | </a-button> |
| | | </div> |
| | | </header> |
| | | |
| | | <div |
| | | v-if="executionProgress && active" |
| | | class="border-b border-border bg-blue-50/70 px-4 py-2 dark:bg-blue-950/20" |
| | | aria-live="polite" |
| | | > |
| | | <div class="mb-1 flex items-center justify-between gap-4 text-xs"> |
| | | <span class="font-medium text-blue-700 dark:text-blue-300"> |
| | | {{ executionProgress.label }} |
| | | </span> |
| | | <span class="text-gray-500"> |
| | | å·²åç° {{ executionProgress.candidateCount }} æ¡åé |
| | | <template v-if="executionProgress.issueCount"> |
| | | · {{ executionProgress.issueCount }} 项å¾
æ ¸å¯¹ |
| | | </template> |
| | | </span> |
| | | </div> |
| | | <a-progress |
| | | :percent="executionProgress.percent" |
| | | :status="executionProgress.stage === 'error' ? 'exception' : 'active'" |
| | | size="small" |
| | | /> |
| | | </div> |
| | | |
| | | <div class="flex-1 overflow-y-auto p-4"> |
| | | <a-empty |
| | | v-if="!active" |
| | | description="ä»å·¦ä¾§éæ©ä¼è¯ï¼æç¹å»ãæ°å»ºè¡¥å½ãå¼å§" |
| | | /> |
| | | <div v-else class="space-y-3"> |
| | | <div |
| | | v-for="item in active.messages" |
| | | :key="item.message_id" |
| | | class="flex" |
| | | :class="item.role === 'user' ? 'justify-end' : 'justify-start'" |
| | | > |
| | | <div |
| | | class="max-w-[80%] rounded-lg px-3 py-2 text-sm" |
| | | :class=" |
| | | item.role === 'user' |
| | | ? 'bg-blue-500 text-white' |
| | | : 'bg-gray-100 dark:bg-gray-800' |
| | | " |
| | | > |
| | | <div class="whitespace-pre-wrap">{{ item.text }}</div> |
| | | <div |
| | | v-for="(msgCard, index) in item.cards || []" |
| | | :key="`${item.message_id}-${index}`" |
| | | class="mt-2 rounded-md border border-border bg-card p-2 text-gray-800 dark:text-gray-100" |
| | | > |
| | | <template v-if="msgCard.kind === 'receipt'"> |
| | | <div class="mb-1 font-semibold"> |
| | | {{ msgCard.title || '导å
¥åæ§' }} |
| | | </div> |
| | | <div class="text-xs text-gray-500"> |
| | | æå {{ msgCard.summary?.success ?? 0 }}ï¼è·³è¿ |
| | | {{ msgCard.summary?.skipped ?? 0 }}ï¼å¤±è´¥ |
| | | {{ msgCard.summary?.failed ?? 0 }} |
| | | </div> |
| | | <a-table |
| | | class="mt-2" |
| | | :columns="[ |
| | | { title: 'ç¼ç ', dataIndex: 'code', width: 160 }, |
| | | { title: 'åç§°', dataIndex: 'name' }, |
| | | { title: 'ç»æ', dataIndex: 'status', width: 90 }, |
| | | ]" |
| | | :data-source="msgCard.rows || []" |
| | | :pagination="false" |
| | | row-key="candidate_id" |
| | | size="small" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <a-tag |
| | | v-if="column.dataIndex === 'status'" |
| | | :color="record.status === 'SUCCESS' ? 'green' : 'red'" |
| | | > |
| | | {{ record.status }} |
| | | </a-tag> |
| | | </template> |
| | | </a-table> |
| | | <a-button |
| | | class="mt-2" |
| | | size="small" |
| | | type="link" |
| | | @click="downloadReport" |
| | | > |
| | | ä¸è½½æ¥è¡¨ |
| | | </a-button> |
| | | </template> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <footer v-if="active && interrupt" class="border-t border-border p-3"> |
| | | <a-spin :spinning="busy"> |
| | | <!-- åºæ¯éæ© --> |
| | | <div v-if="interrupt.kind === 'scenario'"> |
| | | <div class="mb-2 font-medium">{{ card?.title }}</div> |
| | | <div class="mb-2 text-xs text-gray-500">{{ card?.body }}</div> |
| | | <div class="flex flex-wrap gap-2"> |
| | | <a-button |
| | | v-for="candidate in card?.candidates || []" |
| | | :key="candidate.module_id" |
| | | :disabled="busy" |
| | | @click="chooseModule(candidate.module_id)" |
| | | > |
| | | {{ candidate.title }} |
| | | <a-tag class="ml-1" color="blue"> |
| | | {{ candidate.capability }} |
| | | </a-tag> |
| | | </a-button> |
| | | <a-button |
| | | v-if="actions.includes('cancel')" |
| | | danger |
| | | :disabled="busy" |
| | | @click="cancelConversation" |
| | | > |
| | | åæ¶ |
| | | </a-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- åæ®µæ å° --> |
| | | <div v-else-if="interrupt.kind === 'mapping'"> |
| | | <div class="mb-2 font-medium">{{ card?.title }}</div> |
| | | <div class="mb-2 text-xs text-gray-500">{{ card?.body }}</div> |
| | | <div class="space-y-2"> |
| | | <div |
| | | v-for="field in card?.missing_fields || []" |
| | | :key="field.name" |
| | | class="flex items-center gap-2" |
| | | > |
| | | <span class="w-28 text-sm">{{ field.label }}</span> |
| | | <a-select |
| | | v-model:value="mappingDraft[field.name]" |
| | | class="min-w-56" |
| | | placeholder="éæ©æä»¶ä¸å¯¹åºçå" |
| | | :options=" |
| | | (card?.columns || []).map((column) => ({ |
| | | label: column, |
| | | value: column, |
| | | })) |
| | | " |
| | | /> |
| | | </div> |
| | | </div> |
| | | <div class="mt-3 flex gap-2"> |
| | | <a-button |
| | | type="primary" |
| | | :loading="busy" |
| | | @click="confirmMapping" |
| | | > |
| | | 确认æ å° |
| | | </a-button> |
| | | <a-button danger :disabled="busy" @click="cancelConversation"> |
| | | åæ¶ |
| | | </a-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- è¡¥ä¼ æä»¶ --> |
| | | <div v-else-if="interrupt.kind === 'file'"> |
| | | <div class="mb-2 font-medium">{{ card?.title }}</div> |
| | | <div class="mb-2 text-xs text-gray-500">{{ card?.body }}</div> |
| | | <a-upload |
| | | accept=".csv,.xlsx,.docx,.pdf" |
| | | :before-upload="beforeInterruptUpload" |
| | | :max-count="1" |
| | | :show-upload-list="false" |
| | | > |
| | | <a-button>{{ interruptFile ? 'éæ°éæ©' : 'éæ©æä»¶' }}</a-button> |
| | | </a-upload> |
| | | <span v-if="interruptFile" class="ml-2 text-xs text-gray-500"> |
| | | {{ interruptFile.name }} |
| | | </span> |
| | | <div class="mt-3"> |
| | | <a-button |
| | | type="primary" |
| | | :disabled="!interruptFile" |
| | | :loading="busy" |
| | | @click="uploadForFileInterrupt" |
| | | > |
| | | ä¸ä¼ å¹¶ç»§ç» |
| | | </a-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- æ ¡éª / é¢è§ç¡®è®¤ --> |
| | | <div v-else-if="['preview', 'validation'].includes(interrupt.kind)"> |
| | | <div class="mb-2 flex items-center justify-between"> |
| | | <span class="font-medium">{{ card?.title }}</span> |
| | | <span class="text-xs text-gray-500"> |
| | | {{ card?.notice }} |
| | | </span> |
| | | </div> |
| | | |
| | | <a-alert |
| | | v-if="card?.issues?.length" |
| | | class="mb-2" |
| | | type="error" |
| | | show-icon |
| | | > |
| | | <template #message> |
| | | 黿项 {{ card.issues.length }} æ¡ï¼é修订åéæ°æ ¡éª |
| | | </template> |
| | | <ul class="mt-1 list-decimal pl-4 text-xs"> |
| | | <li v-for="(issue, index) in card.issues" :key="index"> |
| | | {{ issue.message }} |
| | | <span v-if="issue.code" class="text-gray-400"> |
| | | ï¼{{ issue.code }}ï¼ |
| | | </span> |
| | | </li> |
| | | </ul> |
| | | </a-alert> |
| | | |
| | | <a-alert |
| | | v-if="card?.warnings?.length" |
| | | class="mb-2" |
| | | type="warning" |
| | | show-icon |
| | | > |
| | | <template #message> |
| | | æç¤º {{ card.warnings.length }} æ¡ï¼ä¸é»ææäº¤ |
| | | </template> |
| | | <ul class="mt-1 list-decimal pl-4 text-xs"> |
| | | <li v-for="(item, index) in card.warnings" :key="index"> |
| | | {{ item.message }} |
| | | </li> |
| | | </ul> |
| | | </a-alert> |
| | | |
| | | <a-alert |
| | | v-if="card?.repair_guidance?.length" |
| | | class="mb-2" |
| | | type="info" |
| | | show-icon |
| | | > |
| | | <template #message>ä¸å¡å¤§æ¨¡åæ ¸å¯¹å»ºè®®</template> |
| | | <ul class="mt-1 list-disc pl-4 text-xs"> |
| | | <li v-for="(item, index) in card.repair_guidance" :key="index"> |
| | | <span v-if="item.field" class="font-medium">{{ item.field }}ï¼</span> |
| | | {{ item.guidance }} |
| | | </li> |
| | | </ul> |
| | | </a-alert> |
| | | |
| | | <div |
| | | v-if="card?.summary" |
| | | class="mb-2 flex flex-wrap gap-3 text-xs text-gray-600 dark:text-gray-300" |
| | | > |
| | | <span>å
± {{ card.summary.total ?? 0 }} è¡</span> |
| | | <span class="text-green-600"> |
| | | æ°å»º {{ card.summary.create ?? 0 }} |
| | | </span> |
| | | <span class="text-orange-500"> |
| | | è·³è¿ {{ card.summary.skip ?? 0 }} |
| | | </span> |
| | | <span class="text-red-500"> |
| | | 黿 {{ card.summary.blocked ?? 0 }} |
| | | </span> |
| | | </div> |
| | | |
| | | <div v-if="card?.side_effects?.length" class="mb-2 text-xs text-gray-500"> |
| | | 颿影åï¼{{ card.side_effects.join('ï¼') }} |
| | | </div> |
| | | |
| | | <div |
| | | v-if="isGatewayModule" |
| | | class="mb-2 flex items-center gap-2 text-sm" |
| | | > |
| | | <span>åæ¥å° MES</span> |
| | | <a-switch |
| | | :checked="syncMes" |
| | | :disabled="busy" |
| | | @change="toggleSync" |
| | | /> |
| | | <span class="text-xs text-gray-500"> |
| | | å
³éåä»
å建 ERP ç©æ |
| | | </span> |
| | | </div> |
| | | |
| | | <a-table |
| | | v-if="previewRows.length" |
| | | class="mb-2" |
| | | :columns="previewColumns" |
| | | :data-source="previewRows" |
| | | :pagination="false" |
| | | :scroll="{ x: 'max-content', y: 320 }" |
| | | row-key="candidate_id" |
| | | size="small" |
| | | bordered |
| | | /> |
| | | |
| | | <div class="flex gap-2"> |
| | | <a-input |
| | | v-model:value="reviseText" |
| | | class="flex-1" |
| | | placeholder="å¦éä¿®æ¹ï¼è¾å
¥ä¿®è®¢è¯´æï¼ä¾å¦ï¼ç¬¬2è¡ åç§° æ¹ä¸º XXX" |
| | | :disabled="busy" |
| | | @press-enter="revise" |
| | | /> |
| | | <a-button :disabled="busy" @click="revise">ä¿®è®¢å¹¶éæ£</a-button> |
| | | <a-button |
| | | v-if="actions.includes('confirm')" |
| | | type="primary" |
| | | :disabled="busy || card?.can_commit === false" |
| | | :loading="busy" |
| | | @click="confirmImport" |
| | | > |
| | | 确认导å
¥ |
| | | </a-button> |
| | | <a-button |
| | | v-if="actions.includes('cancel')" |
| | | danger |
| | | :disabled="busy" |
| | | @click="cancelConversation" |
| | | > |
| | | åæ¶ |
| | | </a-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <a-alert |
| | | v-else |
| | | type="info" |
| | | show-icon |
| | | :message="card?.title || 'å½åæ¥éª¤éè¦å¤ç'" |
| | | :description="card?.body" |
| | | /> |
| | | </a-spin> |
| | | </footer> |
| | | </section> |
| | | |
| | | <TaskCenterDrawer |
| | | ref="taskCenter" |
| | | :enabled="durableTasksEnabled" |
| | | @continue="continueFromTask" |
| | | /> |
| | | |
| | | <a-modal |
| | | v-model:open="attachmentPreview.open" |
| | | :footer="null" |
| | | :title="attachmentPreview.filename" |
| | | width="80%" |
| | | @cancel="closeAttachmentPreview" |
| | | > |
| | | <iframe |
| | | v-if="attachmentPreview.kind === 'pdf'" |
| | | :src="attachmentPreview.url" |
| | | class="h-[70vh] w-full border-0" |
| | | title="éä»¶é¢è§" |
| | | /> |
| | | <img |
| | | v-else |
| | | :src="attachmentPreview.url" |
| | | alt="éä»¶é¢è§" |
| | | class="max-h-[70vh] w-full object-contain" |
| | | /> |
| | | </a-modal> |
| | | |
| | | <a-modal |
| | | v-model:open="newOpen" |
| | | title="æ°å»ºè¡¥å½" |
| | | :confirm-loading="creating" |
| | | :width="760" |
| | | > |
| | | <div class="space-y-3"> |
| | | <div> |
| | | <div class="mb-1 flex items-center justify-between text-sm"> |
| | | <span>ä¸å¡åºæ¯ï¼å¯å¤éï¼</span> |
| | | <a-button |
| | | size="small" |
| | | :type="newPriorities.scenario_paths ? 'primary' : 'default'" |
| | | @click="togglePriority('scenario_paths')" |
| | | >ä¼å
</a-button> |
| | | </div> |
| | | <a-select |
| | | v-model:value="newScenarios" |
| | | class="w-full" |
| | | mode="multiple" |
| | | :max-tag-count="3" |
| | | :options="scenarioOptions" |
| | | option-filter-prop="label" |
| | | placeholder="è¯·éæ©å½åè¡¥å½æ¶åçè忍¡å" |
| | | show-search |
| | | > |
| | | <template #option="option"> |
| | | <div class="py-1"> |
| | | <div class="font-medium">{{ option.label }}</div> |
| | | <div v-if="option.description" class="mt-0.5 text-xs text-gray-500"> |
| | | {{ option.description }} |
| | | </div> |
| | | </div> |
| | | </template> |
| | | </a-select> |
| | | <div class="mt-1 text-xs text-gray-500"> |
| | | å¼å¯âä¼å
âåæç¡¬çº¦æå¤çï¼æç»æ¨¡åä»å¿
é¡»éè¿è½å䏿°æ®æéæ ¡éªã |
| | | </div> |
| | | </div> |
| | | <div class="grid grid-cols-1 gap-3 md:grid-cols-2"> |
| | | <div> |
| | | <div class="mb-1 flex items-center justify-between text-sm"> |
| | | <span>æ¶é´èå´</span> |
| | | <a-button size="small" :type="newPriorities.time_range ? 'primary' : 'default'" @click="togglePriority('time_range')">ä¼å
</a-button> |
| | | </div> |
| | | <a-range-picker v-model:value="newTimeRange" class="w-full" value-format="YYYY-MM-DD" /> |
| | | </div> |
| | | <div> |
| | | <div class="mb-1 flex items-center justify-between text-sm"> |
| | | <span>æ°æ®æ¡æ°</span> |
| | | <a-button size="small" :type="newPriorities.record_limit ? 'primary' : 'default'" @click="togglePriority('record_limit')">ä¼å
</a-button> |
| | | </div> |
| | | <a-input-number v-model:value="newRecordLimit" class="w-full" :min="1" :max="100000" placeholder="ä¾å¦ï¼500" /> |
| | | </div> |
| | | <div> |
| | | <div class="mb-1 flex items-center justify-between text-sm"> |
| | | <span>è¡ä¸</span> |
| | | <a-button size="small" :type="newPriorities.industry ? 'primary' : 'default'" @click="togglePriority('industry')">ä¼å
</a-button> |
| | | </div> |
| | | <a-input v-model:value="newIndustry" maxlength="80" placeholder="ä¾å¦ï¼æ±½è½¦é¶é¨ä»¶" /> |
| | | </div> |
| | | <div> |
| | | <div class="mb-1 flex items-center justify-between text-sm"> |
| | | <span>åºå</span> |
| | | <a-button size="small" :type="newPriorities.region ? 'primary' : 'default'" @click="togglePriority('region')">ä¼å
</a-button> |
| | | </div> |
| | | <a-input v-model:value="newRegion" maxlength="120" placeholder="ä¾å¦ï¼åä¸ / æ±èåé" /> |
| | | </div> |
| | | <div> |
| | | <div class="mb-1 flex items-center justify-between text-sm"> |
| | | <span>è·³è¿ä¼æ¯æ¥</span> |
| | | <a-button size="small" :type="newPriorities.skip_rest_days ? 'primary' : 'default'" @click="togglePriority('skip_rest_days')">ä¼å
</a-button> |
| | | </div> |
| | | <div class="flex h-8 items-center gap-2"> |
| | | <a-switch v-model:checked="newSkipRestDays" /> |
| | | <span class="text-xs text-gray-500">{{ newSkipRestDays ? 'è·³è¿' : 'ä¸è·³è¿' }}</span> |
| | | </div> |
| | | </div> |
| | | <div> |
| | | <div class="mb-1 flex items-center justify-between text-sm"> |
| | | <span>伿¯æ¥æè¿°</span> |
| | | <a-button size="small" :type="newPriorities.rest_day_description ? 'primary' : 'default'" @click="togglePriority('rest_day_description')">ä¼å
</a-button> |
| | | </div> |
| | | <a-input v-model:value="newRestDayDescription" maxlength="300" placeholder="ä¾å¦ï¼å¨æ¥åæ³å®è忥" /> |
| | | </div> |
| | | </div> |
| | | <div> |
| | | <div class="mb-1 flex items-center justify-between text-sm"> |
| | | <span>è¡¥å½è¯´æï¼å¯éï¼</span> |
| | | <a-button size="small" :type="newPriorities.instruction ? 'primary' : 'default'" @click="togglePriority('instruction')">ä¼å
</a-button> |
| | | </div> |
| | | <a-textarea |
| | | v-model:value="newText" |
| | | :rows="2" |
| | | placeholder="ä¾å¦ï¼å¯¼å
¥ä¸æ¹ä¾åºåæ¡£æ¡" |
| | | /> |
| | | </div> |
| | | <div> |
| | | <div class="mb-1 text-sm">æ°æ®æä»¶</div> |
| | | <a-upload |
| | | accept=".csv,.xlsx,.docx,.pdf" |
| | | :before-upload="beforeNewUpload" |
| | | :max-count="20" |
| | | multiple |
| | | :show-upload-list="false" |
| | | > |
| | | <a-button>éæ©æä»¶</a-button> |
| | | </a-upload> |
| | | <span v-if="newFiles.length" class="ml-2 text-xs text-gray-500"> |
| | | 已鿩 {{ newFiles.length }} 个æä»¶ |
| | | </span> |
| | | <a-button v-if="newFiles.length" class="ml-2" size="small" type="link" @click="clearNewFiles"> |
| | | æ¸
空 |
| | | </a-button> |
| | | <div v-if="newFiles.length" class="mt-2 max-h-20 overflow-auto text-xs text-gray-500"> |
| | | <div v-for="file in newFiles" :key="`${file.name}:${file.size}:${file.lastModified}`"> |
| | | {{ file.name }} |
| | | </div> |
| | | </div> |
| | | <div class="mt-1 text-xs text-gray-500"> |
| | | æ¯æä¸æ¬¡éæ©æå¤ 20 个 CSVãXLSXãDOCX ä¸å¯å¤å¶ææ¬ç PDFï¼å个æä»¶ä¸è¶
è¿ 8 |
| | | MiBã夿件ä¼å¨åå°åè·¨æä»¶éå¤åå²çªæ£æ¥ï¼æ«æä»¶æä¸æ¯æã |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <template #footer> |
| | | <div class="flex flex-wrap justify-end gap-2"> |
| | | <a-button :disabled="creating" @click="newOpen = false"> |
| | | åæ¶ |
| | | </a-button> |
| | | <a-tooltip |
| | | :title=" |
| | | !durableTasksEnabled |
| | | ? 'éå
å¯ç¨ PostgreSQL ä¸ Redis æä¹
任塿å¡' |
| | | : 'ä»»å¡ä¼æ ¹æ®æä»¶å
容èªå¨è¯å«ï¼å
³é页é¢åä»å¯ä»ä»»å¡ä¸å¿æ¢å¤' |
| | | " |
| | | > |
| | | <a-button |
| | | :disabled="!durableTasksEnabled || !newFiles.length" |
| | | :loading="creating" |
| | | @click="startBackgroundAnalysis" |
| | | > |
| | | åå°åæ |
| | | </a-button> |
| | | </a-tooltip> |
| | | <a-button |
| | | type="primary" |
| | | :loading="creating" |
| | | @click="createConversation" |
| | | > |
| | | ç´æ¥å¼å§è¡¥å½ |
| | | </a-button> |
| | | </div> |
| | | </template> |
| | | </a-modal> |
| | | </div> |
| | | </Page> |
| | | </template> |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | |
| | | import { AiModelTypeEnum, CommonStatusEnum, DICT_TYPE } from '@vben/constants'; |
| | | import type { AiModelModelApi } from '#/api/ai/model/model'; |
| | | |
| | | import { AiModelCapabilityEnum, AiModelTypeEnum, CommonStatusEnum, DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { z } from '#/adapter/form'; |
| | |
| | | }; |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'capability', |
| | | label: '模åè½å', |
| | | component: 'Select', |
| | | componentProps: (values) => { |
| | | return { |
| | | placeholder: 'è¯·éæ©æ¨¡åè½å', |
| | | // åªæå¯¹è¯ç±»ååºåææ¬/è§è§ï¼å
¶ä½ç±»åç»ä¸æææ¬è½åå¤ç |
| | | disabled: !!values.id || values.type !== AiModelTypeEnum.CHAT, |
| | | options: getDictOptions(DICT_TYPE.AI_MODEL_CAPABILITY, 'string'), |
| | | allowClear: true, |
| | | }; |
| | | }, |
| | | help: 'åä¸ç±»ååä¸è½ååªè½å¯ç¨ä¸ä¸ªæ¨¡åï¼è§è§è½åç¨äºå¾çè¯å«', |
| | | dependencies: { |
| | | triggerFields: ['type'], |
| | | show: (values) => values.type === AiModelTypeEnum.CHAT, |
| | | }, |
| | | rules: z.string().default(AiModelCapabilityEnum.TEXT), |
| | | }, |
| | | { |
| | | fieldName: 'keyId', |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'type', |
| | | label: '模åç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©æ¨¡åç±»å', |
| | | options: getDictOptions(DICT_TYPE.AI_MODEL_TYPE, 'number'), |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'model', |
| | | label: 'æ¨¡åæ è¯', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥æ¨¡åæ è¯', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'capability', |
| | | label: '模åè½å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©æ¨¡åè½å', |
| | | options: getDictOptions(DICT_TYPE.AI_MODEL_CAPABILITY, 'string'), |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | |
| | | } |
| | | |
| | | /** å表çåæ®µ */ |
| | | export function useGridColumns(): VxeTableGridOptions['columns'] { |
| | | export function useGridColumns( |
| | | onStatusChange?: (newStatus: number, row: AiModelModelApi.Model) => Promise<boolean | undefined>, |
| | | ): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { |
| | | field: 'platform', |
| | | title: 'æå±å¹³å°', |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.AI_PLATFORM }, |
| | | }, |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'type', |
| | | title: '模åç±»å', |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.AI_MODEL_TYPE }, |
| | | }, |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'capability', |
| | | title: '模åè½å', |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.AI_MODEL_CAPABILITY }, |
| | | }, |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'platform', |
| | | title: 'æå±å¹³å°', |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.AI_PLATFORM }, |
| | | }, |
| | | minWidth: 100, |
| | | }, |
| | |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | title: 'å¯ç¨', |
| | | minWidth: 100, |
| | | align: 'center', |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.COMMON_STATUS }, |
| | | attrs: { beforeChange: onStatusChange }, |
| | | name: 'CellSwitch', |
| | | props: { |
| | | checkedValue: CommonStatusEnum.ENABLE, |
| | | unCheckedValue: CommonStatusEnum.DISABLE, |
| | | }, |
| | | }, |
| | | minWidth: 80, |
| | | }, |
| | | { |
| | | field: 'temperature', |
| | |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { AiModelModelApi } from '#/api/ai/model/model'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { ref } from 'vue'; |
| | | |
| | | import { confirm, Page, useVbenModal } from '@vben/common-ui'; |
| | | import { CommonStatusEnum } from '@vben/constants'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { deleteModel, getModelPage } from '#/api/ai/model/model'; |
| | | import { deleteModel, getModelPage, updateModelStatus } from '#/api/ai/model/model'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** å½åé¡µæ°æ®ï¼spanMethod ä¾èµå®è®¡ç®ç±»åååå¹¶è¡æ° */ |
| | | const currentRows = ref<AiModelModelApi.Model[]>([]); |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | |
| | | } |
| | | } |
| | | |
| | | /** æ´æ°æ¨¡åå¯ç¨ç¶æï¼å¯ç¨ä¼èªå¨å
³éåç±»ååè½åçå
¶ä»æ¨¡å */ |
| | | async function handleStatusChange( |
| | | newStatus: number, |
| | | row: AiModelModelApi.Model, |
| | | ): Promise<boolean | undefined> { |
| | | try { |
| | | await confirm( |
| | | newStatus === CommonStatusEnum.ENABLE |
| | | ? `å¯ç¨ã${row.name}ãåï¼åç±»ååè½åä¸å·²å¯ç¨çå
¶ä»æ¨¡åä¼èªå¨å
³éï¼ç¡®å®ç»§ç»åï¼` |
| | | : `ä½ è¦åç¨ã${row.name}ãåï¼åç¨å该类å该è½åå°æ²¡æå¯ç¨æ¨¡åã`, |
| | | ); |
| | | } catch { |
| | | return false; |
| | | } |
| | | await updateModelStatus(row.id!, newStatus); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | // åç»å
¶ä»æ¨¡åçå¯ç¨ç¶æå·²è¢«å端æ¹åï¼å¿
é¡»æ´é¡µå·æ° |
| | | handleRefresh(); |
| | | return true; |
| | | } |
| | | |
| | | /** 模åç±»ååæç±»å纵ååå¹¶ï¼å½¢æåç±»å±ç¤º */ |
| | | function handleSpanMethod({ |
| | | column, |
| | | rowIndex, |
| | | }: { |
| | | column: { field?: string; property?: string }; |
| | | rowIndex: number; |
| | | }) { |
| | | const field = column.field || column.property; |
| | | if (field !== 'type') { |
| | | return { colspan: 1, rowspan: 1 }; |
| | | } |
| | | const row = currentRows.value[rowIndex]; |
| | | if (rowIndex > 0 && currentRows.value[rowIndex - 1]?.type === row?.type) { |
| | | return { colspan: 0, rowspan: 0 }; |
| | | } |
| | | let rowspan = 1; |
| | | for (let index = rowIndex + 1; index < currentRows.value.length; index += 1) { |
| | | if (currentRows.value[index]?.type !== row?.type) { |
| | | break; |
| | | } |
| | | rowspan += 1; |
| | | } |
| | | return { colspan: 1, rowspan }; |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | columns: useGridColumns(handleStatusChange), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getModelPage({ |
| | | const data = await getModelPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | currentRows.value = data.list || []; |
| | | return data; |
| | | }, |
| | | }, |
| | | }, |
| | |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | spanMethod: handleSpanMethod, |
| | | toolbarConfig: { |
| | | refresh: true, |
| | | search: true, |
| | |
| | | title: "åä½", |
| | | minWidth: 80, |
| | | }, |
| | | { |
| | | field: "unitMeasureName2", |
| | | title: "è¾
åä½1", |
| | | minWidth: 80, |
| | | }, |
| | | { |
| | | field: "unitMeasureRate1", |
| | | title: "è¾
åä½1æ¢ç®æ¯ç", |
| | | minWidth: 130, |
| | | }, |
| | | { |
| | | field: "unitMeasureName3", |
| | | title: "è¾
åä½2", |
| | | minWidth: 80, |
| | | }, |
| | | { |
| | | field: "unitMeasureRate2", |
| | | title: "è¾
åä½2æ¢ç®æ¯ç", |
| | | minWidth: 130, |
| | | }, |
| | | // è¾
åä½1/è¾
åä½1æ¢ç®æ¯ç/è¾
åä½2/è¾
åä½2æ¢ç®æ¯çï¼ç©æå表ä¸åå±ç¤ºï¼è¡¨åéä»å¯ç»´æ¤ï¼ï¼éè¦æ¢å¤å°±åæ¶æ³¨é |
| | | // { |
| | | // field: "unitMeasureName2", |
| | | // title: "è¾
åä½1", |
| | | // minWidth: 80, |
| | | // }, |
| | | // { |
| | | // field: "unitMeasureRate1", |
| | | // title: "è¾
åä½1æ¢ç®æ¯ç", |
| | | // minWidth: 130, |
| | | // }, |
| | | // { |
| | | // field: "unitMeasureName3", |
| | | // title: "è¾
åä½2", |
| | | // minWidth: 80, |
| | | // }, |
| | | // { |
| | | // field: "unitMeasureRate2", |
| | | // title: "è¾
åä½2æ¢ç®æ¯ç", |
| | | // minWidth: 130, |
| | | // }, |
| | | { |
| | | field: "warehouseName", |
| | | title: "é»è®¤ä»åº", |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: "purchasePrice", |
| | | title: "éè´ä»·", |
| | | minWidth: 100, |
| | | formatter: ({ cellValue }) => { |
| | | return cellValue != null ? `Â¥${Number(cellValue).toFixed(2)}` : ""; |
| | | }, |
| | | }, |
| | | { |
| | | field: "salesPrice", |
| | | title: "éå®ä»·", |
| | | minWidth: 100, |
| | | formatter: ({ cellValue }) => { |
| | | return cellValue != null ? `Â¥${Number(cellValue).toFixed(2)}` : ""; |
| | | }, |
| | | }, |
| | | // éå®ä»·ï¼ç©æå表ä¸åå±ç¤ºï¼éè¦æ¢å¤å°±åæ¶æ³¨é |
| | | // { |
| | | // field: "salesPrice", |
| | | // title: "éå®ä»·", |
| | | // minWidth: 100, |
| | | // formatter: ({ cellValue }) => { |
| | | // return cellValue != null ? `Â¥${Number(cellValue).toFixed(2)}` : ""; |
| | | // }, |
| | | // }, |
| | | { |
| | | field: "createTime", |
| | | title: "å建æ¶é´", |
| | | minWidth: 180, |
| | | formatter: "formatDateTime", |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: "isBatchManaged", |
| | |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { isAiEnable } from '@vben/hooks'; |
| | | |
| | | import { confirm, Page, useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | import { downloadFileFromBlobPart, isEmpty } from '#/packages/utils/src'; |
| | | |
| | |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import AgentImport from './modules/agent-import.vue'; |
| | | import Form from './modules/form.vue'; |
| | | import ImportForm from './modules/import-form.vue'; |
| | | |
| | |
| | | |
| | | const [ImportModal, importModalApi] = useVbenModal({ |
| | | connectedComponent: ImportForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [AgentImportModal, agentImportModalApi] = useVbenModal({ |
| | | connectedComponent: AgentImport, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | |
| | | function handleImport() { |
| | | importModalApi.open(); |
| | | } |
| | | |
| | | /** æºè½ä½å¯¼å
¥ç©æ */ |
| | | function handleAgentImport() { |
| | | agentImportModalApi.open(); |
| | | } |
| | | |
| | | /** å·¥å
·æ æé®ï¼æºè½ä½å¯¼å
¥å AI æ»å¼å
³æ§å¶ */ |
| | | const toolbarActions = computed(() => [ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['ç©æ']), |
| | | type: 'primary' as const, |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['mdm:item:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.import', ['ç©æ']), |
| | | type: 'primary' as const, |
| | | icon: ACTION_ICON.UPLOAD, |
| | | auth: ['mdm:item:import'], |
| | | onClick: handleImport, |
| | | }, |
| | | ...(isAiEnable() |
| | | ? [ |
| | | { |
| | | label: 'æºè½ä½å¯¼å
¥', |
| | | type: 'primary' as const, |
| | | icon: ACTION_ICON.UPLOAD, |
| | | auth: ['mdm:item:import'], |
| | | onClick: handleAgentImport, |
| | | }, |
| | | ] |
| | | : []), |
| | | { |
| | | label: $t('ui.actionTitle.export'), |
| | | type: 'primary' as const, |
| | | icon: ACTION_ICON.DOWNLOAD, |
| | | auth: ['mdm:item:export'], |
| | | onClick: handleExport, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.deleteBatch'), |
| | | type: 'primary' as const, |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['mdm:item:delete'], |
| | | disabled: isEmpty(checkedIds), |
| | | onClick: handleDeleteBatch, |
| | | }, |
| | | ]); |
| | | |
| | | /** ç¼è¾ç©æ */ |
| | | function handleEdit(row: MdmItemApi.Item) { |
| | |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <ImportModal @success="handleRefresh" /> |
| | | <AgentImportModal @success="handleRefresh" /> |
| | | <Grid table-title="ç©æå表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['ç©æ']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['mdm:item:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.import', ['ç©æ']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.UPLOAD, |
| | | auth: ['mdm:item:import'], |
| | | onClick: handleImport, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.export'), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.DOWNLOAD, |
| | | auth: ['mdm:item:export'], |
| | | onClick: handleExport, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.deleteBatch'), |
| | | type: 'primary', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['mdm:item:delete'], |
| | | disabled: isEmpty(checkedIds), |
| | | onClick: handleDeleteBatch, |
| | | }, |
| | | ]" |
| | | /> |
| | | <TableAction :actions="toolbarActions" /> |
| | | </template> |
| | | <template #categoryName="{ row }"> |
| | | <Tag v-if="getCategoryColor(row.categoryId)" :color="getCategoryColor(row.categoryId)">{{ getCategoryName(row.categoryId) }}</Tag> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FileType } from 'ant-design-vue/es/upload/interface'; |
| | | |
| | | import type { MdmAgentImportApi } from '#/api/mdm/item/agent'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { |
| | | Alert, |
| | | Button, |
| | | Input, |
| | | message, |
| | | Steps, |
| | | Table, |
| | | Tag, |
| | | Upload, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | agentImportErrorMessage, |
| | | bindImportAttachment, |
| | | createImportSession, |
| | | getMaterialImportReceipt, |
| | | reviewMaterialImport, |
| | | startMaterialImport, |
| | | } from '#/api/mdm/item/agent'; |
| | | import { uploadFile } from '#/api/system/storage'; |
| | | |
| | | defineOptions({ name: 'MdmAgentImport' }); |
| | | |
| | | const emit = defineEmits<{ success: [] }>(); |
| | | |
| | | const MAX_BYTES = 8 * 1024 * 1024; |
| | | const FIELD_KEYS = [ |
| | | 'code', |
| | | 'name', |
| | | 'specification', |
| | | 'categoryCode', |
| | | 'unitMeasureCode', |
| | | ] as const; |
| | | type FieldKey = (typeof FIELD_KEYS)[number]; |
| | | |
| | | interface EditableRow { |
| | | candidate_id: string; |
| | | row_no?: number; |
| | | code: string; |
| | | name: string; |
| | | specification: string; |
| | | categoryCode: string; |
| | | unitMeasureCode: string; |
| | | original: Record<string, string>; |
| | | preflight?: MdmAgentImportApi.PreflightRow; |
| | | issues: string[]; |
| | | } |
| | | |
| | | interface RowMessage { |
| | | type: 'error' | 'warning'; |
| | | text: string; |
| | | } |
| | | |
| | | const step = ref(1); |
| | | const file = ref<File>(); |
| | | const fileName = ref(''); |
| | | const processing = ref(false); |
| | | const taskId = ref(''); |
| | | const state = ref<MdmAgentImportApi.ImportState>(); |
| | | const receipt = ref<MdmAgentImportApi.Receipt>(); |
| | | const rows = ref<EditableRow[]>([]); |
| | | const lastError = ref(''); |
| | | let sessionId: number | undefined; |
| | | let attachmentId: number | undefined; |
| | | |
| | | const preflight = computed(() => state.value?.preflight); |
| | | const issues = computed(() => state.value?.issues ?? []); |
| | | const canCommit = computed( |
| | | () => !!preflight.value?.can_commit && issues.value.length === 0, |
| | | ); |
| | | const hasChanges = computed(() => corrections.value.length > 0); |
| | | |
| | | const corrections = computed<MdmAgentImportApi.Correction[]>(() => { |
| | | const result: MdmAgentImportApi.Correction[] = []; |
| | | for (const row of rows.value) { |
| | | for (const field of FIELD_KEYS) { |
| | | const before = (row.original[field] ?? '').trim(); |
| | | const after = (row[field] ?? '').trim(); |
| | | if (before !== after) { |
| | | result.push({ |
| | | candidate_id: row.candidate_id, |
| | | field, |
| | | value: after, |
| | | reason: '人工修订', |
| | | }); |
| | | } |
| | | } |
| | | } |
| | | return result; |
| | | }); |
| | | |
| | | const ACTION_LABEL: Record<MdmAgentImportApi.RowAction, string> = { |
| | | BLOCKED: '黿', |
| | | CREATE: 'æ°å»º', |
| | | SKIP: 'å·²åå¨', |
| | | }; |
| | | const ACTION_COLOR: Record<MdmAgentImportApi.RowAction, string> = { |
| | | BLOCKED: 'red', |
| | | CREATE: 'green', |
| | | SKIP: 'orange', |
| | | }; |
| | | const STATUS_LABEL: Record<MdmAgentImportApi.RowStatus, string> = { |
| | | FAILED: '失败', |
| | | SKIPPED: '已跳è¿', |
| | | SUCCESS: 'æå', |
| | | UNKNOWN: 'æªç¥', |
| | | }; |
| | | const STATUS_COLOR: Record<MdmAgentImportApi.RowStatus, string> = { |
| | | FAILED: 'red', |
| | | SKIPPED: 'orange', |
| | | SUCCESS: 'green', |
| | | UNKNOWN: 'default', |
| | | }; |
| | | |
| | | const reviewColumns = [ |
| | | { dataIndex: 'row_no', key: 'row_no', title: 'è¡å·', width: 64 }, |
| | | { dataIndex: 'code', key: 'code', title: 'ç©æç¼ç ', width: 150 }, |
| | | { dataIndex: 'name', key: 'name', title: 'ç©æåç§°', width: 180 }, |
| | | { dataIndex: 'specification', key: 'specification', title: 'è§æ ¼åå·', width: 150 }, |
| | | { dataIndex: 'categoryCode', key: 'categoryCode', title: 'åç±»ç¼ç ', width: 140 }, |
| | | { |
| | | dataIndex: 'unitMeasureCode', |
| | | key: 'unitMeasureCode', |
| | | title: '计éåä½', |
| | | width: 130, |
| | | }, |
| | | { key: 'action', title: '颿£å¨ä½', width: 100 }, |
| | | { key: 'message', title: 'æ ¡éªä¿¡æ¯', width: 260 }, |
| | | ]; |
| | | |
| | | const receiptColumns = [ |
| | | { dataIndex: 'row_no', key: 'row_no', title: 'è¡å·', width: 64 }, |
| | | { dataIndex: 'code', key: 'code', title: 'ç©æç¼ç ', width: 150 }, |
| | | { dataIndex: 'name', key: 'name', title: 'ç©æåç§°', width: 180 }, |
| | | { key: 'action', title: 'å¨ä½', width: 90 }, |
| | | { key: 'status', title: 'ç»æ', width: 90 }, |
| | | { dataIndex: 'target_item_id', key: 'target_item_id', title: 'ç©æ ID', width: 100 }, |
| | | { key: 'error', title: '失败åå ', width: 220 }, |
| | | ]; |
| | | |
| | | function isEditableColumn(key: unknown): key is FieldKey { |
| | | return typeof key === 'string' && (FIELD_KEYS as readonly string[]).includes(key); |
| | | } |
| | | |
| | | function rowMessages(row: EditableRow): RowMessage[] { |
| | | return [ |
| | | ...(row.preflight?.blockers ?? []).map((item) => ({ |
| | | type: 'error' as const, |
| | | text: item.message, |
| | | })), |
| | | ...(row.preflight?.warnings ?? []).map((item) => ({ |
| | | type: 'warning' as const, |
| | | text: item.message, |
| | | })), |
| | | ...row.issues.map((text) => ({ type: 'warning' as const, text })), |
| | | ]; |
| | | } |
| | | |
| | | function buildRows(data: MdmAgentImportApi.ImportState): EditableRow[] { |
| | | const plannedRows = data.preflight?.rows ?? []; |
| | | return (data.candidates ?? []) |
| | | .map((candidate) => { |
| | | const planned = plannedRows.find( |
| | | (item) => item.candidate_id === candidate.candidate_id, |
| | | ); |
| | | const values = candidate.values ?? {}; |
| | | return { |
| | | candidate_id: candidate.candidate_id, |
| | | row_no: planned?.row_no ?? candidate.evidence?.code?.row, |
| | | code: values.code ?? '', |
| | | name: values.name ?? '', |
| | | specification: values.specification ?? '', |
| | | categoryCode: values.categoryCode ?? '', |
| | | unitMeasureCode: values.unitMeasureCode ?? '', |
| | | original: { ...values }, |
| | | preflight: planned, |
| | | issues: candidate.issues ?? [], |
| | | }; |
| | | }) |
| | | .sort((a, b) => (a.row_no ?? 0) - (b.row_no ?? 0)); |
| | | } |
| | | |
| | | function applyState(payload: MdmAgentImportApi.PublicState) { |
| | | state.value = payload.state; |
| | | taskId.value = payload.state.task_id; |
| | | lastError.value = |
| | | payload.state.last_error || payload.interrupts?.[0]?.last_error || ''; |
| | | receipt.value = payload.state.receipt; |
| | | rows.value = buildRows(payload.state); |
| | | step.value = receipt.value ? 3 : 2; |
| | | syncFooter(); |
| | | } |
| | | |
| | | function syncFooter() { |
| | | void modalApi.setState({ |
| | | cancelText: step.value === 2 ? 'åæ¶ä»»å¡' : 'åæ¶', |
| | | confirmText: |
| | | step.value === 1 ? 'å¼å§è§£æ' : step.value === 2 ? '确认æäº¤' : '宿', |
| | | showCancelButton: step.value !== 3, |
| | | }); |
| | | } |
| | | |
| | | function reset() { |
| | | step.value = 1; |
| | | file.value = undefined; |
| | | fileName.value = ''; |
| | | processing.value = false; |
| | | taskId.value = ''; |
| | | state.value = undefined; |
| | | receipt.value = undefined; |
| | | rows.value = []; |
| | | lastError.value = ''; |
| | | sessionId = undefined; |
| | | attachmentId = undefined; |
| | | } |
| | | |
| | | function beforeUpload(fileInfo: FileType) { |
| | | const raw = fileInfo as unknown as File; |
| | | const name = raw.name.toLowerCase(); |
| | | if (!name.endsWith('.csv') && !name.endsWith('.xlsx')) { |
| | | message.error('ä»
æ¯æ CSV æ XLSX æä»¶'); |
| | | return false; |
| | | } |
| | | if (raw.size > MAX_BYTES) { |
| | | message.error('æä»¶ä¸è½è¶
è¿ 8 MiB'); |
| | | return false; |
| | | } |
| | | file.value = raw; |
| | | fileName.value = raw.name; |
| | | return false; |
| | | } |
| | | |
| | | async function startImport() { |
| | | if (!file.value) { |
| | | message.warning('请å
éæ© CSV/XLSX æä»¶'); |
| | | return; |
| | | } |
| | | processing.value = true; |
| | | modalApi.lock(); |
| | | try { |
| | | sessionId = await createImportSession(); |
| | | const blobs = await uploadFile([file.value]); |
| | | const blobId = blobs?.[0]?.id; |
| | | if (!blobId) { |
| | | throw new Error('æä»¶ä¸ä¼ å¤±è´¥ï¼æªåå¾ blobId'); |
| | | } |
| | | attachmentId = await bindImportAttachment(sessionId, blobId); |
| | | const payload = await startMaterialImport(attachmentId); |
| | | applyState(payload); |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } finally { |
| | | processing.value = false; |
| | | modalApi.unlock(); |
| | | } |
| | | } |
| | | |
| | | async function revise() { |
| | | if (!state.value) return; |
| | | if (!hasChanges.value) { |
| | | message.info('æªæ£æµå°ä¿®æ¹'); |
| | | return; |
| | | } |
| | | processing.value = true; |
| | | modalApi.lock(); |
| | | try { |
| | | const payload = await reviewMaterialImport(taskId.value, { |
| | | action: 'revise', |
| | | corrections: corrections.value, |
| | | expected_revision: state.value.revision, |
| | | }); |
| | | applyState(payload); |
| | | message.success('å·²æä¿®æ¹éæ°é¢æ£ï¼è¯·æ ¸å¯¹ç»æ'); |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } finally { |
| | | processing.value = false; |
| | | modalApi.unlock(); |
| | | } |
| | | } |
| | | |
| | | async function confirmSubmit() { |
| | | if (!state.value) return; |
| | | if (hasChanges.value) { |
| | | message.warning('å卿ªæäº¤çä¿®æ¹ï¼è¯·å
ç¹ãéæ°æ£æ¥ãå¹¶ææ°ç颿£ç»æç¡®è®¤'); |
| | | return; |
| | | } |
| | | if (!canCommit.value) { |
| | | message.warning(lastError.value || '颿£ä»æé»æé¡¹ï¼è¯·ä¿®è®¢åéæ°æ£æ¥'); |
| | | return; |
| | | } |
| | | processing.value = true; |
| | | modalApi.lock(); |
| | | try { |
| | | const payload = await reviewMaterialImport(taskId.value, { |
| | | action: 'confirm', |
| | | expected_preflight_hash: state.value.preflight_hash, |
| | | expected_revision: state.value.revision, |
| | | }); |
| | | applyState(payload); |
| | | if (receipt.value) { |
| | | message.success('æäº¤å®æï¼è¯·æ¥çåæ§'); |
| | | } else if (lastError.value) { |
| | | message.error(lastError.value); |
| | | } |
| | | } catch (error) { |
| | | const text = agentImportErrorMessage(error); |
| | | message.error(text); |
| | | // æäº¤è¶
æ¶/ç½å
³ä¸ææ¶å½ä»¤å¯è½å·²æ§è¡ï¼æç¤ºç¨æ·æ¥åæ§ï¼é¿å
éå¤æäº¤ |
| | | lastError.value = text; |
| | | } finally { |
| | | processing.value = false; |
| | | modalApi.unlock(); |
| | | } |
| | | } |
| | | |
| | | async function fetchReceipt() { |
| | | if (!taskId.value) return; |
| | | processing.value = true; |
| | | modalApi.lock(); |
| | | try { |
| | | receipt.value = await getMaterialImportReceipt(taskId.value); |
| | | step.value = 3; |
| | | syncFooter(); |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } finally { |
| | | processing.value = false; |
| | | modalApi.unlock(); |
| | | } |
| | | } |
| | | |
| | | async function handleCancel() { |
| | | if (step.value === 2 && taskId.value) { |
| | | try { |
| | | await reviewMaterialImport(taskId.value, { |
| | | action: 'cancel', |
| | | expected_revision: state.value?.revision ?? 0, |
| | | }); |
| | | } catch { |
| | | // åæ¶å¤±è´¥ä¸å½±åå
³éçªå£ |
| | | } |
| | | } |
| | | modalApi.close(); |
| | | } |
| | | |
| | | function finish() { |
| | | if ((receipt.value?.summary.success ?? 0) > 0) { |
| | | emit('success'); |
| | | } |
| | | modalApi.close(); |
| | | } |
| | | |
| | | async function handlePrimary() { |
| | | if (processing.value) return; |
| | | if (step.value === 1) { |
| | | await startImport(); |
| | | return; |
| | | } |
| | | if (step.value === 2) { |
| | | await confirmSubmit(); |
| | | return; |
| | | } |
| | | finish(); |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | onCancel() { |
| | | void handleCancel(); |
| | | }, |
| | | onClosed() { |
| | | reset(); |
| | | }, |
| | | onConfirm() { |
| | | void handlePrimary(); |
| | | }, |
| | | onOpenChange(isOpen) { |
| | | if (isOpen) { |
| | | reset(); |
| | | syncFooter(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="æºè½ä½å¯¼å
¥ç©æ" class="w-[1080px]"> |
| | | <Steps |
| | | :current="step - 1" |
| | | class="mx-4 mb-4" |
| | | size="small" |
| | | :items="[ |
| | | { title: 'ä¸ä¼ æä»¶' }, |
| | | { title: 'å®¡æ ¸ç¡®è®¤' }, |
| | | { title: 'æäº¤åæ§' }, |
| | | ]" |
| | | /> |
| | | |
| | | <div v-if="step === 1" class="mx-4"> |
| | | <Upload |
| | | accept=".csv,.xlsx" |
| | | :max-count="1" |
| | | :show-upload-list="false" |
| | | :before-upload="beforeUpload" |
| | | > |
| | | <Button type="primary">éæ© CSV / XLSX æä»¶</Button> |
| | | </Upload> |
| | | <div v-if="fileName" class="mt-2 text-sm text-gray-600"> |
| | | 已鿩ï¼{{ fileName }} |
| | | </div> |
| | | <div class="mt-3 text-xs text-gray-500 leading-relaxed"> |
| | | <div>æä»¶ä¸é 8 MiBï¼ä»
æ¯æ CSV / XLSXã</div> |
| | | <div> |
| | | æµç¨ï¼è§£ææ¸
å â Java æ ¡éªåç±»/åä½/ç¼ç â äººå·¥æ ¸å¯¹ â 确认æäº¤ã |
| | | </div> |
| | | <div> |
| | | æ¬é¶æ®µåªæ°å»ºç©æï¼ç¼ç å·²åå¨ä¼è·³è¿ï¼ä¸æ´æ°åç©æï¼ä¹ä¸åæ¥ MESã |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-else-if="step === 2" class="mx-4"> |
| | | <div class="mb-3 flex flex-wrap items-center gap-x-5 gap-y-1 text-sm"> |
| | | <span>æ»è®¡ï¼{{ preflight?.summary.total ?? rows.length }}</span> |
| | | <span class="text-green-600"> |
| | | æ°å»ºï¼{{ preflight?.summary.create ?? 0 }} |
| | | </span> |
| | | <span class="text-orange-500"> |
| | | è·³è¿ï¼{{ preflight?.summary.skip ?? 0 }} |
| | | </span> |
| | | <span class="text-red-500"> |
| | | 黿ï¼{{ preflight?.summary.blocked ?? 0 }} |
| | | </span> |
| | | <span class="text-gray-500">æ¹æ¡çæ¬ï¼{{ state?.revision ?? 0 }}</span> |
| | | </div> |
| | | |
| | | <Alert |
| | | v-if="lastError" |
| | | type="error" |
| | | :message="lastError" |
| | | show-icon |
| | | class="mb-3" |
| | | /> |
| | | |
| | | <Alert |
| | | v-if="issues.length > 0" |
| | | type="warning" |
| | | show-icon |
| | | class="mb-3" |
| | | message="åå¨éè¦å¤ççé®é¢" |
| | | > |
| | | <template #description> |
| | | <div v-for="(issue, index) in issues" :key="index"> |
| | | {{ issue.message }} |
| | | </div> |
| | | </template> |
| | | </Alert> |
| | | |
| | | <Alert |
| | | v-else-if="preflight && !preflight.can_commit" |
| | | type="warning" |
| | | show-icon |
| | | class="mb-3" |
| | | message="颿£ä»æé»æé¡¹ï¼ä¿®æ£å请ç¹ãéæ°æ£æ¥ãï¼éè¿åæè½ç¡®è®¤æäº¤" |
| | | /> |
| | | |
| | | <Table |
| | | bordered |
| | | size="small" |
| | | :columns="reviewColumns" |
| | | :data-source="rows" |
| | | :pagination="false" |
| | | :row-key="(record: EditableRow) => record.candidate_id" |
| | | :scroll="{ y: 360 }" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="isEditableColumn(column.key)"> |
| | | <Input |
| | | v-model:value="record[column.key]" |
| | | size="small" |
| | | :status=" |
| | | record.preflight?.blockers?.some( |
| | | (item: MdmAgentImportApi.Issue) => item.field === column.key, |
| | | ) |
| | | ? 'error' |
| | | : undefined |
| | | " |
| | | /> |
| | | <div |
| | | v-if=" |
| | | column.key === 'categoryCode' && |
| | | record.preflight?.resolved?.category_name |
| | | " |
| | | class="mt-1 text-xs text-gray-400" |
| | | > |
| | | è§£æä¸ºï¼{{ record.preflight.resolved.category_name }} |
| | | </div> |
| | | <div |
| | | v-if=" |
| | | column.key === 'unitMeasureCode' && |
| | | record.preflight?.resolved?.unit_measure_name |
| | | " |
| | | class="mt-1 text-xs text-gray-400" |
| | | > |
| | | è§£æä¸ºï¼{{ record.preflight.resolved.unit_measure_name }} |
| | | </div> |
| | | </template> |
| | | <template v-else-if="column.key === 'action'"> |
| | | <Tag |
| | | v-if="record.preflight" |
| | | :color=" |
| | | ACTION_COLOR[record.preflight.action as MdmAgentImportApi.RowAction] |
| | | " |
| | | > |
| | | {{ ACTION_LABEL[record.preflight.action as MdmAgentImportApi.RowAction] }} |
| | | </Tag> |
| | | </template> |
| | | <template v-else-if="column.key === 'message'"> |
| | | <div |
| | | v-for="(item, index) in rowMessages(record as EditableRow)" |
| | | :key="index" |
| | | class="text-xs leading-relaxed" |
| | | :class="item.type === 'error' ? 'text-red-500' : 'text-orange-500'" |
| | | > |
| | | {{ item.text }} |
| | | </div> |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | |
| | | <div class="mt-3 flex flex-wrap items-center gap-2"> |
| | | <Button :disabled="processing || !hasChanges" @click="revise"> |
| | | éæ°æ£æ¥ |
| | | </Button> |
| | | <Button :disabled="processing || !taskId" @click="fetchReceipt"> |
| | | æ¥è¯¢åæ§ |
| | | </Button> |
| | | <span class="text-xs text-gray-400"> |
| | | {{ hasChanges ? '已修æ¹ï¼ééæ°æ£æ¥åæè½ç¡®è®¤' : '确认åæäº¤ï¼å·²åå¨ç¼ç ä¼è·³è¿' }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-else class="mx-4"> |
| | | <div class="mb-3 flex flex-wrap items-center gap-x-5 gap-y-1 text-sm"> |
| | | <Tag :color="receipt?.status === 'SUCCEEDED' ? 'green' : 'orange'"> |
| | | {{ receipt?.status ?? 'UNKNOWN' }} |
| | | </Tag> |
| | | <span>æ»è®¡ï¼{{ receipt?.summary.total ?? 0 }}</span> |
| | | <span class="text-green-600"> |
| | | æåï¼{{ receipt?.summary.success ?? 0 }} |
| | | </span> |
| | | <span class="text-orange-500"> |
| | | è·³è¿ï¼{{ receipt?.summary.skipped ?? 0 }} |
| | | </span> |
| | | <span class="text-red-500"> |
| | | 失败ï¼{{ receipt?.summary.failed ?? 0 }} |
| | | </span> |
| | | <span class="text-gray-500"> |
| | | æªç¥ï¼{{ receipt?.summary.unknown ?? 0 }} |
| | | </span> |
| | | </div> |
| | | |
| | | <Alert |
| | | v-if="receipt?.error_message" |
| | | type="error" |
| | | :message="receipt.error_message" |
| | | show-icon |
| | | class="mb-3" |
| | | /> |
| | | |
| | | <Table |
| | | bordered |
| | | size="small" |
| | | :columns="receiptColumns" |
| | | :data-source="receipt?.rows ?? []" |
| | | :pagination="false" |
| | | :row-key="(record: MdmAgentImportApi.ReceiptRow) => record.candidate_id" |
| | | :scroll="{ y: 360 }" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.key === 'action'"> |
| | | <Tag :color="record.action === 'CREATE' ? 'green' : 'orange'"> |
| | | {{ record.action === 'CREATE' ? 'æ°å»º' : 'è·³è¿' }} |
| | | </Tag> |
| | | </template> |
| | | <template v-else-if="column.key === 'status'"> |
| | | <Tag :color="STATUS_COLOR[record.status as MdmAgentImportApi.RowStatus]"> |
| | | {{ STATUS_LABEL[record.status as MdmAgentImportApi.RowStatus] }} |
| | | </Tag> |
| | | </template> |
| | | <template v-else-if="column.key === 'error'"> |
| | | <span class="text-xs text-red-500"> |
| | | {{ record.error_message ?? '' }} |
| | | </span> |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | </div> |
| | | </Modal> |
| | | </template> |
| | |
| | | <script lang="ts" setup> |
| | | import type { FileType } from 'ant-design-vue/es/upload/interface'; |
| | | |
| | | import { h } from 'vue'; |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button, message, notification, Upload } from 'ant-design-vue'; |
| | | import { Button, message, Upload } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { importItem, importItemTemplate } from '#/api/mdm/item'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useImportFormSchema } from '../data'; |
| | | |
| | | /** 导å
¥ç»æï¼å¯¹åºå端 MdmItemImportRespVO */ |
| | | interface ImportResult { |
| | | /** æ°å¢æåçç©æ */ |
| | | createList?: string[]; |
| | | /** æ´æ°æåçç©æ */ |
| | | updateList?: string[]; |
| | | /** 失败æç»ï¼key 为ãç©æç¼ç åç§°ãï¼value 为失败åå */ |
| | | failureList?: Record<string, string>; |
| | | } |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | const importResult = ref<ImportResult>({}); |
| | | const createCount = computed(() => importResult.value.createList?.length ?? 0); |
| | | const updateCount = computed(() => importResult.value.updateList?.length ?? 0); |
| | | const failureEntries = computed(() => |
| | | Object.entries(importResult.value.failureList ?? {}), |
| | | ); |
| | | const failureCount = computed(() => failureEntries.value.length); |
| | | |
| | | // 注æï¼æ¬ç»ä»¶éçå¤ä¸ª useVbenModal æå
å顺åºè¦æ±ââ |
| | | // ç¶é¡µé¢ç¨ useVbenModal({ connectedComponent: ImportForm }) éè¿ provide/inject ç»å®ï¼ |
| | | // åªæç¬¬ä¸ä¸ªè°ç¨ä¼æ¥ç®¡è¿ä¸ªç»å®ãæä»¥å¯¼å
¥çªå£å¿
须声æå¨æåï¼å
¶ä½å¼¹æ¡ï¼å¦ç»æå¼¹æ¡ï¼æå¨åé¢ã |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | |
| | | const data = await formApi.getValues(); |
| | | try { |
| | | const response = (await importItem(data.file, data.updateSupport)) as any; |
| | | // å
³éå¹¶æç¤º |
| | | await modalApi.close(); |
| | | const resultData = (response?.data || response || {}) as ImportResult; |
| | | const hasFailure = Object.keys(resultData.failureList ?? {}).length > 0; |
| | | |
| | | emit('success'); |
| | | |
| | | const resultData = response?.data || response || {}; |
| | | const failureList = resultData.failureList || {}; |
| | | |
| | | if (Object.keys(failureList).length > 0) { |
| | | const errorNodes = Object.entries(failureList).map(([name, reason]) => { |
| | | return h('div', `${name}: ${reason}`); |
| | | }); |
| | | |
| | | notification.warning({ |
| | | message: '导å
¥å®æï¼é¨åç©æå¯¼å
¥å¤±è´¥', |
| | | description: () => h('div', errorNodes), |
| | | duration: null, |
| | | }); |
| | | if (hasFailure) { |
| | | // ä¸è½å¨è¿éå
å
³å¯¼å
¥çªå£ï¼ç¶é¡µé¢é
ç½®äº destroyOnCloseï¼ |
| | | // å
³çªä¼ææ¬ç»ä»¶æ´ä¸ªé建ï¼åæå¼çç»æå¼¹æ¡ä¼è¢«è¿å¸¦å¸è½½ï¼éªä¸ä¸å°±æ¶å¤±ï¼ã |
| | | // æ¹ä¸ºæç»æå¼¹æ¡å å¨ä¸é¢ï¼çå®å
³éååå
³å¯¼å
¥çªå£ï¼è§ä¸é¢ onClosedï¼ã |
| | | importResult.value = resultData; |
| | | resultModalApi.open(); |
| | | } else { |
| | | importResult.value = {}; |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | await modalApi.close(); |
| | | } |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | /** 导å
¥ç»æå¼¹æ¡ï¼å¤±è´¥æç»å¯è½å¾é¿ï¼å¼¹æ¡å
åºå®é«åº¦æ»å¨ï¼é¿å
åéç¥æ¡é£æ ·æåºå±å¹ */ |
| | | const [ResultModal, resultModalApi] = useVbenModal({ |
| | | onOpenChange(isOpen: boolean) { |
| | | if (isOpen) { |
| | | resultModalApi.setState({ |
| | | confirmText: 'ç¥éäº', |
| | | showCancelButton: false, |
| | | }); |
| | | } |
| | | }, |
| | | // å¿
é¡»æ¾å¼æä¾ï¼ModalApi.onConfirm æ¯ this.api.onConfirm?.() çå¯éè°ç¨ï¼ä¸ä¼ åç¹æé®æ¯«æ ååº |
| | | onConfirm() { |
| | | resultModalApi.close(); |
| | | }, |
| | | // ç»æå¼¹æ¡å
³éååå
³å¯¼å
¥çªå£ï¼æ¤æ¶æ¬ç»ä»¶è¢«éæ¯æ¯å®å
¨çï¼ |
| | | onClosed() { |
| | | modalApi.close(); |
| | | }, |
| | | }); |
| | | |
| | |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | |
| | | <ResultModal title="导å
¥ç»æ" class="w-[760px]"> |
| | | <div class="mx-4"> |
| | | <div class="mb-3 flex flex-wrap gap-x-6 gap-y-1 text-sm"> |
| | | <span> |
| | | æ°å¢ï¼<span class="font-medium text-green-600">{{ createCount }}</span> |
| | | æ¡ |
| | | </span> |
| | | <span> |
| | | æ´æ°ï¼<span class="font-medium text-blue-600">{{ updateCount }}</span> |
| | | æ¡ |
| | | </span> |
| | | <span> |
| | | 失败ï¼<span class="font-medium text-red-600">{{ failureCount }}</span> |
| | | æ¡ |
| | | </span> |
| | | </div> |
| | | |
| | | <div |
| | | v-if="failureCount > 0" |
| | | class="overflow-hidden rounded-md border border-red-100" |
| | | > |
| | | <div |
| | | class="border-b border-red-100 bg-red-50 px-3 py-2 text-sm font-medium text-red-600" |
| | | > |
| | | 失败æç»ï¼å
± {{ failureCount }} æ¡ï¼ä¸æ¹åºå坿»å¨æ¥çï¼ |
| | | </div> |
| | | <div class="max-h-[60vh] overflow-y-auto"> |
| | | <div |
| | | v-for="[itemKey, itemReason] in failureEntries" |
| | | :key="itemKey" |
| | | class="flex gap-2 border-b border-gray-100 px-3 py-2 text-sm last:border-b-0" |
| | | > |
| | | <span class="w-56 shrink-0 break-all text-gray-700"> |
| | | {{ itemKey }} |
| | | </span> |
| | | <span class="break-all text-red-500">{{ itemReason }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </ResultModal> |
| | | </template> |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | label: $t('ui.actionTitle.import', ['计éåä½']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.UPLOAD, |
| | | auth: ['mdm:unit-measure:import'], |
| | | auth: ['mdm:unit:import'], |
| | | onClick: handleImport, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.export'), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.DOWNLOAD, |
| | | auth: ['mdm:unit-measure:export'], |
| | | auth: ['mdm:unit:export'], |
| | | onClick: handleExport, |
| | | }, |
| | | ]" |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 170, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 170, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | xData: string[], |
| | | series: Array<{ name: string; data: number[] }>, |
| | | colors: string[] = [C.cyan, C.blue, C.purple], |
| | | horizontal = false, |
| | | ): EChartsOption { |
| | | return { |
| | | grid: { bottom: 40, left: 48, right: 16, top: 16 }, |
| | | grid: { bottom: 40, left: horizontal ? 140 : 48, right: 16, top: 16 }, |
| | | legend: { ...darkLegend, bottom: 0 }, |
| | | tooltip: { ...darkTooltip, axisPointer: { type: 'shadow' }, trigger: 'axis' }, |
| | | xAxis: { ...darkAxis, data: xData, type: 'category' }, |
| | | yAxis: { ...darkAxis, type: 'value' }, |
| | | xAxis: horizontal ? { ...darkAxis, type: 'value' } : { ...darkAxis, data: xData, type: 'category' }, |
| | | yAxis: horizontal ? { |
| | | ...darkAxis, data: xData, type: 'category', inverse: true, |
| | | axisLabel: { ...darkAxis.axisLabel, width: 125, overflow: 'truncate', interval: 0 }, |
| | | } : { ...darkAxis, type: 'value' }, |
| | | series: series.map((s, i) => ({ |
| | | barGap: '30%', |
| | | barMaxWidth: 36, |
| | |
| | | 'purchase-sales': { |
| | | code: 'purchase_sales', |
| | | title: 'éè´ Â· éå® Â· å®å', |
| | | subtitle: 'ä¾åºé¾å
¨é¾è·¯ç»è¥å³ççæ¿', |
| | | subtitle: '客æ·ç»è¥ · åæºè·è¿ · åå忬¾ · éè´å®å', |
| | | gradient: 'from-indigo-600 via-blue-600 to-cyan-500', |
| | | icon: 'lucide:shopping-cart', |
| | | }, |
| | |
| | | |
| | | import ChartCard from './modules/ChartCard.vue'; |
| | | import ClockWidget from './modules/ClockWidget.vue'; |
| | | import RealtimeTable from './modules/RealtimeTable.vue'; |
| | | import { DASHBOARD_META, getDashboardCode, getDefaultCharts } from './data'; |
| | | |
| | | defineOptions({ name: 'BiDashboard' }); |
| | |
| | | const charts = ref<BiDashboardApi.ChartItem[]>([]); |
| | | const loading = ref(true); |
| | | const lastUpdateTime = ref(''); |
| | | let dataRefreshTimer: ReturnType<typeof setInterval> | undefined; |
| | | let requestSequence = 0; |
| | | let fetching = false; |
| | | let disposed = false; |
| | | |
| | | async function loadData() { |
| | | if (!meta.value) return; |
| | | async function loadData(background = false) { |
| | | if (!meta.value || disposed || (background && fetching)) return; |
| | | const sequence = ++requestSequence; |
| | | const code = meta.value.code; |
| | | fetching = true; |
| | | if (!background && dataRefreshTimer) clearInterval(dataRefreshTimer); |
| | | try { |
| | | loading.value = true; |
| | | const data = await getDashboardData(meta.value.code); |
| | | if (!background) loading.value = true; |
| | | const data = await getDashboardData(code); |
| | | if (disposed || sequence !== requestSequence) return; |
| | | charts.value = data.length > 0 ? data : getDefaultCharts(); |
| | | lastUpdateTime.value = new Date().toLocaleTimeString('zh-CN', { hour12: false }); |
| | | } catch { |
| | | charts.value = getDefaultCharts(); |
| | | if (!background && !disposed && sequence === requestSequence) charts.value = getDefaultCharts(); |
| | | } finally { |
| | | loading.value = false; |
| | | await nextTick(); |
| | | resizeAllCharts(); |
| | | if (!disposed && sequence === requestSequence) { |
| | | fetching = false; |
| | | loading.value = false; |
| | | await nextTick(); |
| | | if (!disposed && sequence === requestSequence) { |
| | | resizeAllCharts(); |
| | | if (!background) { |
| | | const intervals = charts.value.map((chart) => chart.refreshInterval).filter((interval) => interval > 0); |
| | | if (intervals.length) dataRefreshTimer = setInterval(() => { |
| | | if (!loading.value && document.visibilityState === 'visible') loadData(true); |
| | | }, Math.max(15, Math.min(...intervals)) * 1000); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // ======== åç±» ======== |
| | | const numberCards = computed(() => charts.value.filter((c) => c.chartType === 'number')); |
| | | const tableCharts = computed(() => charts.value.filter((c) => c.chartType === 'table')); |
| | | const echartsCharts = computed(() => charts.value.filter((c) => !['number', 'table'].includes(c.chartType))); |
| | | const displayCharts = computed(() => { |
| | | if (dashboardCode.value !== 'purchase-sales') return charts.value; |
| | | return [...charts.value].sort((a, b) => |
| | | Number(b.chartOptions?.dashboardSection === 'crm') - Number(a.chartOptions?.dashboardSection === 'crm'), |
| | | ); |
| | | }); |
| | | const numberCards = computed(() => displayCharts.value.filter((c) => c.chartType === 'number')); |
| | | const tableCharts = computed(() => displayCharts.value.filter((c) => c.chartType === 'table')); |
| | | const echartsCharts = computed(() => displayCharts.value.filter((c) => !['number', 'table'].includes(c.chartType))); |
| | | |
| | | // ======== å¾è¡¨ Grid å跨度 ======== |
| | | function isFullWidth(chart: BiDashboardApi.ChartItem): boolean { |
| | |
| | | const progress = Math.min((currentTime - startTime) / duration, 1); |
| | | const eased = 1 - (1 - progress) ** 3; |
| | | el.textContent = (end * eased).toFixed(decimals); |
| | | if (progress === 1) el.textContent = formatNumber(end); |
| | | if (progress < 1) requestAnimationFrame(update); |
| | | } |
| | | requestAnimationFrame(update); |
| | |
| | | function maybeAnimate(el: HTMLElement, cardId: number, value: number) { |
| | | if (animatedIds.has(cardId)) return; |
| | | animatedIds.add(cardId); |
| | | animateValue(el, value, 0); |
| | | animateValue(el, value, Number.isInteger(value) ? 0 : 2); |
| | | } |
| | | |
| | | watch(() => charts.value, () => { |
| | |
| | | return formatNumber(v); |
| | | } |
| | | |
| | | // 大å±è¡¨æ ¼éçæ¥æååªå±ç¤ºå°å¤©ï¼å½¢å¦ 2026-09-01 10:23:45 çå¼åæ 2026-09-01ï¼ï¼ |
| | | // å¾è¡¨ SQL éç order_time / receipt_date / create_time / hire_date 齿¯å¸¦æ¶åç§ç |
| | | const DATE_TIME_RE = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?$/; |
| | | |
| | | function getCellValue(row: unknown, col: string): unknown { |
| | | return (row as Record<string, unknown>)[col]; |
| | | const val = (row as Record<string, unknown>)[col]; |
| | | if (typeof val === 'string' && DATE_TIME_RE.test(val)) { |
| | | return val.slice(0, 10); |
| | | } |
| | | return val; |
| | | } |
| | | |
| | | // ======== çæ¿ä¸»é¢è² ======== |
| | |
| | | |
| | | // ======== KPI 徿 /é¢è² ======== |
| | | function getKpiIcon(name: string): string { |
| | | if (name.includes('客æ·')) return 'lucide:users'; |
| | | if (name.includes('åæº')) return 'lucide:target'; |
| | | if (name.includes('忬¾')) return 'lucide:wallet'; |
| | | if (name.includes('æ¥ä»·')) return 'lucide:receipt-text'; |
| | | if (name.includes('éè´')) return 'lucide:shopping-cart'; |
| | | if (name.includes('éå®')) return 'lucide:trending-up'; |
| | | if (name.includes('å®å')) return 'lucide:headphones'; |
| | |
| | | loadData(); |
| | | }); |
| | | onBeforeUnmount(() => { |
| | | disposed = true; |
| | | requestSequence++; |
| | | if (dataRefreshTimer) clearInterval(dataRefreshTimer); |
| | | document.removeEventListener('fullscreenchange', onFullscreenChange); |
| | | resizeObserver?.disconnect(); |
| | | }); |
| | |
| | | <IconifyIcon :icon="getKpiIcon(card.name)" /> |
| | | </div> |
| | | <div class="kpi-body"> |
| | | <span class="kpi-label">{{ card.name }}</span> |
| | | <span class="kpi-label" :title="card.name">{{ card.name }}</span> |
| | | <span |
| | | class="kpi-value" |
| | | :ref="(el: unknown) => { |
| | |
| | | <span class="table-header-title">{{ chart.name }}</span> |
| | | <span class="table-badge">宿¶æ»å¨</span> |
| | | </div> |
| | | <div v-if="chart.data && chart.data.length > 0" class="table-wrap"> |
| | | <table class="data-table"> |
| | | <thead> |
| | | <tr> |
| | | <th |
| | | v-for="key in Object.keys(chart.data[0] || {})" |
| | | :key="key" |
| | | >{{ key }}</th> |
| | | </tr> |
| | | </thead> |
| | | <tbody> |
| | | <tr |
| | | v-for="(row, ri) in chart.data" |
| | | :key="ri" |
| | | :style="{ animationDelay: `${ri * 50}ms` }" |
| | | > |
| | | <td |
| | | v-for="key in Object.keys(chart.data[0] || {})" |
| | | :key="key" |
| | | >{{ getCellValue(row, key) }}</td> |
| | | </tr> |
| | | </tbody> |
| | | </table> |
| | | </div> |
| | | <RealtimeTable |
| | | v-if="chart.data && chart.data.length > 0" |
| | | :rows="chart.data" |
| | | :format-cell="getCellValue" |
| | | /> |
| | | <Empty v-else description="ææ æ°æ®" /> |
| | | </div> |
| | | </template> |
| | |
| | | |
| | | .kpi-label { |
| | | font-size: 11px; color: var(--text-secondary); letter-spacing: 0.3px; |
| | | white-space: nowrap; overflow: hidden; text-overflow: ellipsis; |
| | | white-space: normal; overflow-wrap: anywhere; line-height: 1.4; |
| | | } |
| | | |
| | | .kpi-value { |
| | |
| | | padding: 2px 8px; border-radius: 10px; |
| | | background: rgba(0,255,136,0.08); color: var(--green); |
| | | border: 1px solid rgba(0,255,136,0.15); |
| | | } |
| | | |
| | | .table-wrap { |
| | | overflow-x: auto; border-radius: 8px; |
| | | background: rgba(4, 14, 36, 0.55); |
| | | backdrop-filter: blur(6px); |
| | | -webkit-backdrop-filter: blur(6px); |
| | | border: 1px solid rgba(0, 229, 255, 0.08); |
| | | } |
| | | |
| | | .data-table { |
| | | width: 100%; border-collapse: collapse; font-size: 12px; |
| | | } |
| | | |
| | | .data-table thead th { |
| | | background: rgba(6, 18, 42, 0.8); |
| | | color: var(--text-secondary); |
| | | font-weight: 600; font-size: 10px; |
| | | text-transform: uppercase; letter-spacing: 0.8px; |
| | | padding: 10px 14px; text-align: left; |
| | | border-bottom: 1px solid rgba(0, 229, 255, 0.12); |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .data-table tbody td { |
| | | padding: 9px 14px; color: var(--text-primary); |
| | | border-bottom: 1px solid rgba(0, 229, 255, 0.04); |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .data-table tbody tr { |
| | | transition: all 0.25s; |
| | | opacity: 0; |
| | | animation: row-fade-in 0.45s ease forwards; |
| | | } |
| | | |
| | | .data-table tbody tr:hover { |
| | | background: rgba(0, 229, 255, 0.06); |
| | | box-shadow: inset 0 0 20px rgba(0, 229, 255, 0.03); |
| | | } |
| | | |
| | | @keyframes row-fade-in { |
| | | from { opacity: 0; transform: translateY(6px); } |
| | | to { opacity: 1; transform: translateY(0); } |
| | | } |
| | | |
| | | /* ======== ç©ºç¶æ ======== */ |
| | |
| | | const valueKey = keys[1] || 'value'; |
| | | const labels = chart.data.map((d: Record<string, unknown>) => String(d[labelKey] || '')); |
| | | const values = chart.data.map((d: Record<string, unknown>) => Number(d[valueKey]) || 0); |
| | | const series = keys.slice(1).map((key) => ({ |
| | | name: keys.length > 2 ? key : chart.name, |
| | | data: chart.data!.map((row) => Number(row[key]) || 0), |
| | | })); |
| | | |
| | | switch (chart.chartType) { |
| | | case 'bar': |
| | | return getBarChartOptions(labels, [{ name: chart.name, data: values }]); |
| | | return getBarChartOptions(labels, series, undefined, chart.chartOptions?.horizontal === true); |
| | | case 'line': |
| | | return getAreaLineChartOptions(labels, [{ name: chart.name, data: values }]); |
| | | return getAreaLineChartOptions(labels, series); |
| | | case 'pie': |
| | | return getDonutPieChartOptions(labels.map((l, i) => ({ name: l, value: values[i] || 0 }))); |
| | | case 'gauge': { |
| | |
| | | <div v-if="chart.error" class="flex h-[240px] items-center justify-center"> |
| | | <Empty :description="chart.error" /> |
| | | </div> |
| | | <div v-else-if="!chart.data?.length" class="flex h-[240px] items-center justify-center"> |
| | | <Empty description="ææ ä¸å¡æ°æ®" /> |
| | | </div> |
| | | <EchartsUI |
| | | v-else |
| | | ref="chartRef" |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue'; |
| | | |
| | | const props = withDefaults(defineProps<{ |
| | | rows: Record<string, unknown>[]; |
| | | formatCell?: (row: Record<string, unknown>, column: string) => unknown; |
| | | maxHeight?: number; |
| | | speed?: number; |
| | | }>(), { maxHeight: 260, speed: 24 }); |
| | | |
| | | const columns = computed(() => Object.keys(props.rows[0] ?? {})); |
| | | const viewport = ref<HTMLElement>(); |
| | | const track = ref<HTMLElement>(); |
| | | const original = ref<HTMLElement>(); |
| | | const scrolling = ref(false); |
| | | const hovered = ref(false); |
| | | const focused = ref(false); |
| | | const reducedMotion = ref(false); |
| | | let animation: Animation | undefined; |
| | | let observer: ResizeObserver | undefined; |
| | | let motionQuery: MediaQueryList | undefined; |
| | | let frame = 0; |
| | | let distance = 0; |
| | | let duration = 0; |
| | | let active = true; |
| | | let disposed = false; |
| | | let measuring = false; |
| | | |
| | | function syncPlayback() { |
| | | if (!animation) return; |
| | | if (hovered.value || focused.value || document.hidden || !active) animation.pause(); |
| | | else animation.play(); |
| | | } |
| | | |
| | | async function measure() { |
| | | if (disposed || measuring || !viewport.value || !original.value || !track.value) return; |
| | | measuring = true; |
| | | try { |
| | | const height = original.value.offsetHeight; |
| | | const shouldScroll = !reducedMotion.value && height > viewport.value.clientHeight + 1; |
| | | const nextDuration = height / Math.max(1, props.speed) * 1000; |
| | | if (shouldScroll && animation && height === distance && duration === nextDuration) return; |
| | | const offset = distance && duration |
| | | ? (Number(animation?.currentTime ?? 0) % duration) / duration * distance : 0; |
| | | animation?.cancel(); |
| | | animation = undefined; |
| | | scrolling.value = shouldScroll; |
| | | distance = height; |
| | | duration = nextDuration; |
| | | await nextTick(); |
| | | if (disposed || !shouldScroll || !track.value) return; |
| | | // Animate a normal block, not tbody. A second identical table fills the loop seam. |
| | | animation = track.value.animate([ |
| | | { transform: 'translateY(0)' }, |
| | | { transform: `translateY(-${height}px)` }, |
| | | ], { duration, iterations: Infinity, easing: 'linear' }); |
| | | animation.currentTime = (offset % height) / height * duration; |
| | | syncPlayback(); |
| | | } finally { |
| | | measuring = false; |
| | | } |
| | | } |
| | | |
| | | function scheduleMeasure() { |
| | | cancelAnimationFrame(frame); |
| | | frame = requestAnimationFrame(() => { void measure(); }); |
| | | } |
| | | |
| | | function onMotionChange() { |
| | | reducedMotion.value = motionQuery?.matches ?? false; |
| | | scheduleMeasure(); |
| | | } |
| | | |
| | | function onKeydown(event: KeyboardEvent) { |
| | | if (!animation || !scrolling.value) return; |
| | | const step = event.key === 'ArrowDown' ? 40 : event.key === 'ArrowUp' ? -40 : 0; |
| | | if (!step) return; |
| | | event.preventDefault(); |
| | | const nextTime = Number(animation.currentTime ?? 0) + step / Math.max(1, props.speed) * 1000; |
| | | animation.currentTime = ((nextTime % duration) + duration) % duration; |
| | | } |
| | | |
| | | watch(() => [props.rows, props.maxHeight, props.speed], scheduleMeasure, { flush: 'post' }); |
| | | watch([hovered, focused], syncPlayback); |
| | | onMounted(() => { |
| | | motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); |
| | | motionQuery.addEventListener('change', onMotionChange); |
| | | onMotionChange(); |
| | | observer = new ResizeObserver(scheduleMeasure); |
| | | if (viewport.value) observer.observe(viewport.value); |
| | | if (original.value) observer.observe(original.value); |
| | | document.addEventListener('visibilitychange', syncPlayback); |
| | | }); |
| | | onActivated(() => { active = true; scheduleMeasure(); syncPlayback(); }); |
| | | onDeactivated(() => { active = false; syncPlayback(); }); |
| | | onBeforeUnmount(() => { |
| | | disposed = true; |
| | | cancelAnimationFrame(frame); |
| | | animation?.cancel(); |
| | | observer?.disconnect(); |
| | | motionQuery?.removeEventListener('change', onMotionChange); |
| | | document.removeEventListener('visibilitychange', syncPlayback); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <div |
| | | class="realtime-table" |
| | | role="region" |
| | | aria-label="宿¶æ°æ®è¡¨ï¼èç¦ææ¬åæåæ»å¨ï¼æ¹åé®éè¡æ¥ç" |
| | | tabindex="0" |
| | | :style="{ '--table-height': `${maxHeight}px`, '--table-width': `${columns.length * 120}px` }" |
| | | @mouseenter="hovered = true" |
| | | @mouseleave="hovered = false" |
| | | @focusin="focused = true" |
| | | @focusout="focused = false" |
| | | @keydown="onKeydown" |
| | | > |
| | | <div class="table-content"> |
| | | <table class="header-table"> |
| | | <thead><tr><th v-for="column in columns" :key="column" scope="col">{{ column }}</th></tr></thead> |
| | | </table> |
| | | <div ref="viewport" class="table-viewport" :class="{ 'is-scrolling': scrolling }"> |
| | | <div ref="track" class="table-track" :class="{ 'is-scrolling': scrolling }"> |
| | | <div ref="original"> |
| | | <table> |
| | | <thead class="sr-only"><tr><th v-for="column in columns" :key="column" scope="col">{{ column }}</th></tr></thead> |
| | | <tbody> |
| | | <tr v-for="(row, index) in rows" :key="index"> |
| | | <td v-for="column in columns" :key="column" :title="String(formatCell ? formatCell(row, column) ?? '' : row[column] ?? '')"> |
| | | {{ formatCell ? formatCell(row, column) : row[column] }} |
| | | </td> |
| | | </tr> |
| | | </tbody> |
| | | </table> |
| | | </div> |
| | | <table v-if="scrolling" aria-hidden="true"> |
| | | <tbody> |
| | | <tr v-for="(row, index) in rows" :key="index"> |
| | | <td v-for="column in columns" :key="column"> |
| | | {{ formatCell ? formatCell(row, column) : row[column] }} |
| | | </td> |
| | | </tr> |
| | | </tbody> |
| | | </table> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .realtime-table { overflow-x: auto; border: 1px solid rgba(0, 229, 255, 0.08); border-radius: 8px; background: rgba(4, 14, 36, 0.55); } |
| | | .realtime-table:focus-visible { outline: 2px solid var(--cyan, #00e5ff); outline-offset: 2px; } |
| | | .table-content { min-width: var(--table-width); } |
| | | table { width: 100%; table-layout: fixed; border-collapse: collapse; font-size: 12px; } |
| | | th { padding: 10px 14px; background: rgb(6, 18, 42); color: var(--text-secondary, #b9c4dc); font-size: 10px; font-weight: 600; text-align: left; border-bottom: 1px solid rgba(0, 229, 255, 0.12); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } |
| | | td { padding: 9px 14px; color: var(--text-primary, #ebf0fc); border-bottom: 1px solid rgba(0, 229, 255, 0.04); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } |
| | | tbody tr:hover { background: rgba(0, 229, 255, 0.06); } |
| | | .table-viewport { max-height: var(--table-height); overflow-y: auto; scrollbar-width: thin; } |
| | | .table-viewport.is-scrolling { overflow-y: hidden; } |
| | | .table-track.is-scrolling { will-change: transform; } |
| | | .sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip-path: inset(50%); } |
| | | </style> |
| | |
| | | } from '#/api/bi/warehouseCoordinate'; |
| | | |
| | | import ClockWidget from '../dashboard/modules/ClockWidget.vue'; |
| | | import RealtimeTable from '../dashboard/modules/RealtimeTable.vue'; |
| | | import { |
| | | getChinaMapOptions, |
| | | getWarehouseAreaLineOptions, |
| | |
| | | const charts = ref<BiDashboardApi.ChartItem[]>([]); |
| | | const loading = ref(true); |
| | | const lastUpdateTime = ref(''); |
| | | let fetching = false; |
| | | let disposed = false; |
| | | |
| | | async function loadData() { |
| | | async function loadData(background = false) { |
| | | if (fetching || disposed) return false; |
| | | fetching = true; |
| | | try { |
| | | loading.value = true; |
| | | if (!background) loading.value = true; |
| | | const data = await getDashboardData('warehouse'); |
| | | if (disposed) return false; |
| | | charts.value = data.length > 0 ? data : []; |
| | | lastUpdateTime.value = new Date().toLocaleTimeString('zh-CN', { hour12: false }); |
| | | return true; |
| | | } catch { |
| | | charts.value = []; |
| | | if (!background && !disposed) charts.value = []; |
| | | return false; |
| | | } finally { |
| | | loading.value = false; |
| | | await nextTick(); |
| | | resizeAllCharts(); |
| | | fetching = false; |
| | | if (!disposed) { |
| | | loading.value = false; |
| | | await nextTick(); |
| | | if (!disposed) resizeAllCharts(); |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | // ======== è¡¨æ ¼æ°æ® ======== |
| | | const tableData = computed(() => tableChart.value?.data || []); |
| | | const tableColumns = computed(() => { |
| | | if (tableData.value.length === 0) return []; |
| | | return Object.keys(tableData.value[0] || {}); |
| | | }); |
| | | |
| | | |
| | | // ======== åºåé¢è¦ ======== |
| | | const warningList = computed(() => { |
| | |
| | | function setupTimer() { |
| | | clearTimer(); |
| | | refreshTimer = setInterval(async () => { |
| | | await loadData(); |
| | | if (document.hidden || editMode.value || disposed) return; |
| | | if (!(await loadData(true))) return; |
| | | await nextTick(); |
| | | await renderAllCharts(); |
| | | if (editMode.value) await nextTick().then(() => attachMapListeners()); |
| | |
| | | resizeObserver = new ResizeObserver(() => resizeAllCharts()); |
| | | if (dashboardRef.value) resizeObserver.observe(dashboardRef.value); |
| | | await loadCoordinates(); |
| | | if (disposed) return; |
| | | await loadData(); |
| | | if (disposed) return; |
| | | await nextTick(); |
| | | await renderAllCharts(); |
| | | setupTimer(); |
| | | }); |
| | | |
| | | onBeforeUnmount(() => { |
| | | disposed = true; |
| | | document.removeEventListener('fullscreenchange', onFullscreenChange); |
| | | resizeObserver?.disconnect(); |
| | | clearTimer(); |
| | |
| | | æè¿å
¥åºè®°å½ |
| | | <span class="wh-table-badge">宿¶æ»å¨</span> |
| | | </div> |
| | | <div v-if="tableData.length > 0" class="wh-table-wrap"> |
| | | <table class="wh-table"> |
| | | <thead> |
| | | <tr> |
| | | <th v-for="col in tableColumns" :key="col">{{ col }}</th> |
| | | </tr> |
| | | </thead> |
| | | <tbody> |
| | | <tr |
| | | v-for="(row, ri) in tableData" |
| | | :key="ri" |
| | | :style="{ animationDelay: `${ri * 50}ms` }" |
| | | > |
| | | <td v-for="col in tableColumns" :key="col"> |
| | | {{ getCellValue(row, col) }} |
| | | </td> |
| | | </tr> |
| | | </tbody> |
| | | </table> |
| | | </div> |
| | | <RealtimeTable |
| | | v-if="tableData.length > 0" |
| | | :rows="tableData" |
| | | :format-cell="getCellValue" |
| | | /> |
| | | <Empty v-else description="ææ æ°æ®" /> |
| | | </div> |
| | | </template> |
| | |
| | | padding: 2px 8px; border-radius: 10px; |
| | | background: rgba(0,255,136,0.08); color: var(--green); |
| | | border: 1px solid rgba(0,255,136,0.15); |
| | | } |
| | | |
| | | .wh-table-wrap { |
| | | overflow-x: auto; border-radius: 8px; |
| | | background: rgba(4, 14, 36, 0.55); |
| | | backdrop-filter: blur(6px); |
| | | -webkit-backdrop-filter: blur(6px); |
| | | border: 1px solid rgba(0, 229, 255, 0.08); |
| | | } |
| | | |
| | | .wh-table { |
| | | width: 100%; border-collapse: collapse; font-size: 12px; |
| | | } |
| | | |
| | | .wh-table thead th { |
| | | background: rgba(6, 18, 42, 0.8); |
| | | color: var(--text-secondary); |
| | | font-weight: 600; font-size: 10px; |
| | | text-transform: uppercase; letter-spacing: 0.8px; |
| | | padding: 10px 14px; text-align: left; |
| | | border-bottom: 1px solid rgba(0,229,255,0.12); |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .wh-table tbody td { |
| | | padding: 9px 14px; color: var(--text-primary); |
| | | border-bottom: 1px solid rgba(0, 229, 255, 0.04); |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .wh-table tbody tr { |
| | | transition: all 0.25s; |
| | | opacity: 0; |
| | | animation: row-fade-in 0.45s ease forwards; |
| | | } |
| | | |
| | | .wh-table tbody tr:hover { |
| | | background: rgba(0, 229, 255, 0.06); |
| | | box-shadow: inset 0 0 20px rgba(0, 229, 255, 0.03); |
| | | } |
| | | |
| | | @keyframes row-fade-in { |
| | | from { opacity: 0; transform: translateY(6px); } |
| | | to { opacity: 1; transform: translateY(0); } |
| | | } |
| | | |
| | | /* ======== å
¨å± ======== */ |
| | |
| | | <script lang="ts" setup> |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import type { ModelCategoryInfo } from '#/api/bpm/model'; |
| | | |
| | | import { |
| | |
| | | globalAutoApproveLoading.value = true; |
| | | try { |
| | | await updateGlobalAutoApprove(checked); |
| | | automaticApproval.value = checked; |
| | | message.success( |
| | | checked |
| | | ? 'å·²å¼å¯å
¨å±å
审æ¹ï¼æææµç¨æäº¤åå°ç´æ¥éè¿' |
| | |
| | | import { getApprovalDetail as getApprovalDetailApi } from '#/api/bpm/processInstance'; |
| | | import { $t } from '#/locales'; |
| | | import { router } from '#/router'; |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import ProcessInstanceTimeline from '#/views/bpm/processInstance/detail/modules/time-line.vue'; |
| | | |
| | | import { useFormSchema } from './data'; |
| | |
| | | return; |
| | | } |
| | | // 1.2 审æ¹ç¸å
³ï¼æ ¡éªæå®å®¡æ¹äºº |
| | | if (startUserSelectTasks.value?.length > 0) { |
| | | if (!automaticApproval.value && startUserSelectTasks.value?.length > 0) { |
| | | for (const userTask of startUserSelectTasks.value) { |
| | | const assignees = startUserSelectAssignees.value[userTask.id]; |
| | | if (Array.isArray(assignees) && assignees.length === 0) { |
| | |
| | | // æäº¤è¡¨å |
| | | const data = (await formApi.getValues()) as LeaveCreateData; |
| | | // 审æ¹ç¸å
³ï¼è®¾ç½®æå®å®¡æ¹äºº |
| | | if (startUserSelectTasks.value?.length > 0) { |
| | | if (!automaticApproval.value && startUserSelectTasks.value?.length > 0) { |
| | | data.startUserSelectAssignees = startUserSelectAssignees.value; |
| | | } |
| | | // æ ¼å¼åå¼å§æ¶é´åç»ææ¶é´çå¼ |
| | |
| | | |
| | | /** 审æ¹ç¸å
³ï¼è·å审æ¹è¯¦æ
*/ |
| | | async function getApprovalDetail() { |
| | | if (automaticApproval.value) return; |
| | | processTimeLineLoading.value = true; |
| | | try { |
| | | const data = await getApprovalDetailApi({ |
| | |
| | | BpmCandidateStrategyEnum.START_USER_SELECT === node.candidateStrategy, |
| | | ); |
| | | // æ¢å¤ä¹åç鿩审æ¹äºº |
| | | if (startUserSelectTasks.value?.length > 0) { |
| | | if (!automaticApproval.value && startUserSelectTasks.value?.length > 0) { |
| | | for (const node of startUserSelectTasks.value) { |
| | | const tempAssignees = tempStartUserSelectAssignees.value[node.id]; |
| | | startUserSelectAssignees.value[node.id] = tempAssignees?.length |
| | |
| | | |
| | | /** åå§å */ |
| | | onMounted(async () => { |
| | | if (automaticApproval.value) { |
| | | if (query.id) await getDetail(Number(query.id)); |
| | | return; |
| | | } |
| | | const processDefinitionDetail: any = await getProcessDefinition( |
| | | undefined, |
| | | processDefineKey, |
| | |
| | | <script lang="ts" setup> |
| | | import { computed, onActivated, onMounted, ref } from 'vue'; |
| | | import type { Ref } from 'vue'; |
| | | |
| | | import { computed, onActivated, onMounted, ref, watch } from 'vue'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | |
| | | import { Badge, Card, List } from 'ant-design-vue'; |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | |
| | | import { getFollowClueCount } from '#/api/crm/clue'; |
| | | import { |
| | |
| | | receivableAuditCount, |
| | | receivablePlanRemindCount, |
| | | ); |
| | | const visibleLeftSides = computed(() => leftSides.filter((item) => |
| | | !automaticApproval.value || !['contractAudit', 'receivableAudit'].includes(item.menu), |
| | | )); |
| | | watch(automaticApproval, (automatic) => { |
| | | if (automatic && ['contractAudit', 'receivableAudit'].includes(leftMenu.value)) { |
| | | leftMenu.value = 'customerTodayContact'; |
| | | } |
| | | }); |
| | | |
| | | const currentComponent = computed(() => { |
| | | const components = { |
| | |
| | | leftMenu.value = item.menu; |
| | | } |
| | | |
| | | /** è·åæ°é */ |
| | | async function getCount() { |
| | | customerTodayContactCount.value = await getTodayContactCustomerCount(); |
| | | customerPutPoolRemindCount.value = await getPutPoolRemindCustomerCount(); |
| | | customerFollowCount.value = await getFollowCustomerCount(); |
| | | clueFollowCount.value = await getFollowClueCount(); |
| | | contractAuditCount.value = await getAuditContractCount(); |
| | | contractRemindCount.value = await getRemindContractCount(); |
| | | receivableAuditCount.value = await getAuditReceivableCount(); |
| | | receivablePlanRemindCount.value = await getReceivablePlanRemindCount(); |
| | | /** ç¬ç«å è½½åç±»æ°éï¼å个æ¥å£å¤±è´¥ä¸é»æå
¶ä»å¾
åã */ |
| | | let countRequest: Promise<void> | undefined; |
| | | function getCount() { |
| | | if (countRequest) return countRequest; |
| | | const requests: [Ref<number>, () => Promise<number>][] = [ |
| | | [customerTodayContactCount, getTodayContactCustomerCount], |
| | | [customerPutPoolRemindCount, getPutPoolRemindCustomerCount], |
| | | [customerFollowCount, getFollowCustomerCount], |
| | | [clueFollowCount, getFollowClueCount], |
| | | [contractRemindCount, getRemindContractCount], |
| | | [receivablePlanRemindCount, getReceivablePlanRemindCount], |
| | | ]; |
| | | if (!automaticApproval.value) { |
| | | requests.push( |
| | | [contractAuditCount, getAuditContractCount], |
| | | [receivableAuditCount, getAuditReceivableCount], |
| | | ); |
| | | } |
| | | countRequest = Promise.allSettled( |
| | | requests.map(async ([count, request]) => { |
| | | count.value = await request(); |
| | | }), |
| | | ).then(() => undefined).finally(() => { |
| | | countRequest = undefined; |
| | | }); |
| | | return countRequest; |
| | | } |
| | | |
| | | /** æ¿æ´»æ¶ */ |
| | |
| | | <Page auto-content-height> |
| | | <div class="flex h-full w-full"> |
| | | <Card class="w-1/5"> |
| | | <List item-layout="horizontal" :data-source="leftSides"> |
| | | <List item-layout="horizontal" :data-source="visibleLeftSides"> |
| | | <template #renderItem="{ item }"> |
| | | <List.Item |
| | | @click="sideClick(item)" |
| | |
| | | </template> |
| | | </List> |
| | | </Card> |
| | | <component class="ml-4 w-4/5" :is="currentComponent" /> |
| | | <component class="ml-4 w-4/5" :is="currentComponent" @refresh="getCount" /> |
| | | </div> |
| | | </Page> |
| | | </template> |
| | |
| | | |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { Button } from 'ant-design-vue'; |
| | | import { Button, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getContractPage } from '#/api/crm/contract'; |
| | |
| | | } |
| | | /** æå¼å®¢æ·è¯¦æ
*/ |
| | | function handleCustomerDetail(row: CrmContractApi.Contract) { |
| | | push({ name: 'CrmCustomerDetail', params: { id: row.id } }); |
| | | if (row.customerId) push({ name: 'CrmCustomerDetail', params: { id: row.customerId } }); |
| | | } |
| | | |
| | | /** æå¼è系人详æ
*/ |
| | | function handleContactDetail(row: CrmContractApi.Contract) { |
| | | push({ name: 'CrmContactDetail', params: { id: row.id } }); |
| | | if (row.signContactId) push({ name: 'CrmContactDetail', params: { id: row.signContactId } }); |
| | | } |
| | | |
| | | /** æå¼åæºè¯¦æ
*/ |
| | | function handleBusinessDetail(row: CrmContractApi.Contract) { |
| | | push({ name: 'CrmBusinessDetail', params: { id: row.id } }); |
| | | if (row.businessId) push({ name: 'CrmBusinessDetail', params: { id: row.businessId } }); |
| | | } |
| | | |
| | | function handleViewOrder(row: CrmContractApi.Contract) { |
| | | if (row.orderId) push({ name: 'ErpSaleOrder', query: { id: row.orderId } }); |
| | | } |
| | | |
| | | const [Grid] = useVbenVxeGrid({ |
| | |
| | | |
| | | <template> |
| | | <Grid> |
| | | <template #no="{ row }"> |
| | | <Button type="link" @click="handleContractDetail(row)"> |
| | | {{ row.no }} |
| | | </Button> |
| | | </template> |
| | | <template #name="{ row }"> |
| | | <Button type="link" @click="handleContractDetail(row)"> |
| | | {{ row.name }} |
| | |
| | | {{ row.businessName }} |
| | | </Button> |
| | | </template> |
| | | <template #contactName="{ row }"> |
| | | <template #signContactName="{ row }"> |
| | | <Button type="link" @click="handleContactDetail(row)"> |
| | | {{ row.contactName }} |
| | | {{ row.signContactName }} |
| | | </Button> |
| | | </template> |
| | | <template #auditStatus="{ row }"> |
| | | <Tag v-if="row.auditStatus === 0">æªæäº¤</Tag> |
| | | <Tag v-else-if="row.auditStatus === 10" color="warning">审æ¹ä¸</Tag> |
| | | <Tag v-else-if="row.auditStatus === 20" color="success">å®¡æ ¸éè¿</Tag> |
| | | <Tag v-else-if="row.auditStatus === 30" color="error">å®¡æ ¸ä¸éè¿</Tag> |
| | | <Tag v-else-if="row.auditStatus === 40">已忶</Tag> |
| | | <Tag v-else-if="row.auditStatus === 50" color="error">å·²ä½åº</Tag> |
| | | <span v-else>â</span> |
| | | </template> |
| | | <template #orderNo="{ row }"> |
| | | <Button v-if="row.orderId" type="link" @click="handleViewOrder(row)">{{ row.orderNo }}</Button> |
| | | <span v-else>â</span> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | |
| | | label: 'æ¥ç审æ¹', |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['crm:contract:update'], |
| | | ifShow: !!row.processInstanceId, |
| | | onClick: handleProcessDetail.bind(null, row), |
| | | }, |
| | | ]" |
| | |
| | | |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { Button } from 'ant-design-vue'; |
| | | import { Button, Tag } from 'ant-design-vue'; |
| | | |
| | | import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getContractPage } from '#/api/crm/contract'; |
| | |
| | | |
| | | /** æå¼å®¢æ·è¯¦æ
*/ |
| | | function handleCustomerDetail(row: CrmContractApi.Contract) { |
| | | push({ name: 'CrmCustomerDetail', params: { id: row.id } }); |
| | | if (row.customerId) push({ name: 'CrmCustomerDetail', params: { id: row.customerId } }); |
| | | } |
| | | |
| | | /** æå¼è系人详æ
*/ |
| | | function handleContactDetail(row: CrmContractApi.Contract) { |
| | | push({ name: 'CrmContactDetail', params: { id: row.id } }); |
| | | if (row.signContactId) push({ name: 'CrmContactDetail', params: { id: row.signContactId } }); |
| | | } |
| | | |
| | | /** æå¼åæºè¯¦æ
*/ |
| | | function handleBusinessDetail(row: CrmContractApi.Contract) { |
| | | push({ name: 'CrmBusinessDetail', params: { id: row.id } }); |
| | | if (row.businessId) push({ name: 'CrmBusinessDetail', params: { id: row.businessId } }); |
| | | } |
| | | |
| | | function handleViewOrder(row: CrmContractApi.Contract) { |
| | | if (row.orderId) push({ name: 'ErpSaleOrder', query: { id: row.orderId } }); |
| | | } |
| | | |
| | | const [Grid] = useVbenVxeGrid({ |
| | |
| | | |
| | | <template> |
| | | <Grid> |
| | | <template #no="{ row }"> |
| | | <Button type="link" @click="handleContractDetail(row)"> |
| | | {{ row.no }} |
| | | </Button> |
| | | </template> |
| | | <template #name="{ row }"> |
| | | <Button type="link" @click="handleContractDetail(row)"> |
| | | {{ row.name }} |
| | |
| | | {{ row.signContactName }} |
| | | </Button> |
| | | </template> |
| | | <template #auditStatus="{ row }"> |
| | | <Tag v-if="row.auditStatus === 0">æªæäº¤</Tag> |
| | | <Tag v-else-if="row.auditStatus === 10" color="warning">审æ¹ä¸</Tag> |
| | | <Tag v-else-if="row.auditStatus === 20" color="success">å®¡æ ¸éè¿</Tag> |
| | | <Tag v-else-if="row.auditStatus === 30" color="error">å®¡æ ¸ä¸éè¿</Tag> |
| | | <Tag v-else-if="row.auditStatus === 40">已忶</Tag> |
| | | <Tag v-else-if="row.auditStatus === 50" color="error">å·²ä½åº</Tag> |
| | | <span v-else>â</span> |
| | | </template> |
| | | <template #orderNo="{ row }"> |
| | | <Button v-if="row.orderId" type="link" @click="handleViewOrder(row)">{{ row.orderNo }}</Button> |
| | | <span v-else>â</span> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | |
| | | label: 'æ¥ç审æ¹', |
| | | type: 'link', |
| | | auth: ['crm:contract:update'], |
| | | ifShow: !!row.processInstanceId, |
| | | onClick: handleProcessDetail.bind(null, row), |
| | | }, |
| | | ]" |
| | |
| | | import type { CrmCustomerApi } from '#/api/crm/customer'; |
| | | |
| | | import { useRouter } from 'vue-router'; |
| | | import { erpNumberFormatter } from '@vben/utils'; |
| | | |
| | | import { Button } from 'ant-design-vue'; |
| | | |
| | |
| | | <template #name="{ row }"> |
| | | <Button type="link" @click="handleDetail(row)">{{ row.name }}</Button> |
| | | </template> |
| | | <template #contracts="{ row }"> |
| | | <span v-if="row.contractCount">{{ row.contractCount }} 份 · ¥{{ erpNumberFormatter(row.contractTotalPrice, 2) }}</span> |
| | | <span v-else>â</span> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <Button type="link" @click="handleDetail(row)">æ¥ç详æ
</Button> |
| | | </template> |
| | |
| | | import type { CrmCustomerApi } from '#/api/crm/customer'; |
| | | |
| | | import { useRouter } from 'vue-router'; |
| | | import { erpNumberFormatter } from '@vben/utils'; |
| | | |
| | | import { Button } from 'ant-design-vue'; |
| | | |
| | | import { useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getCustomerPage } from '#/api/crm/customer'; |
| | | import { getPutPoolRemindCustomerPage } from '#/api/crm/customer'; |
| | | import { useGridColumns } from '#/views/crm/customer/data'; |
| | | |
| | | import { SCENE_TYPES } from '../data'; |
| | |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getCustomerPage({ |
| | | return await getPutPoolRemindCustomerPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | pool: true, // åºå® å
¬æµ·åæ°ä¸º true |
| | | ...formValues, |
| | | }); |
| | | }, |
| | |
| | | <template #name="{ row }"> |
| | | <Button type="link" @click="handleDetail(row)">{{ row.name }}</Button> |
| | | </template> |
| | | <template #contracts="{ row }"> |
| | | <span v-if="row.contractCount">{{ row.contractCount }} 份 · ¥{{ erpNumberFormatter(row.contractTotalPrice, 2) }}</span> |
| | | <span v-else>â</span> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <Button type="link" @click="handleDetail(row)">æ¥ç详æ
</Button> |
| | | </template> |
| | |
| | | import type { CrmCustomerApi } from '#/api/crm/customer'; |
| | | |
| | | import { useRouter } from 'vue-router'; |
| | | import { erpNumberFormatter } from '@vben/utils'; |
| | | |
| | | import { Button } from 'ant-design-vue'; |
| | | |
| | |
| | | <template #name="{ row }"> |
| | | <Button type="link" @click="handleDetail(row)">{{ row.name }}</Button> |
| | | </template> |
| | | <template #contracts="{ row }"> |
| | | <span v-if="row.contractCount">{{ row.contractCount }} 份 · ¥{{ erpNumberFormatter(row.contractTotalPrice, 2) }}</span> |
| | | <span v-else>â</span> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <Button type="link" @click="handleDetail(row)">æ¥ç详æ
</Button> |
| | | </template> |
| | |
| | | |
| | | import { Button, message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getReceivablePage, updateReceivableStatus } from '#/api/crm/receivable'; |
| | | import { useGridColumns } from '#/views/crm/receivable/data'; |
| | | |
| | | import { AUDIT_STATUS } from '../data'; |
| | | |
| | | const { push } = useRouter(); |
| | | const emit = defineEmits<{ refresh: [] }>(); |
| | | |
| | | /** å®¡æ ¸ */ |
| | | async function handleAudit(row: CrmReceivableApi.Receivable) { |
| | |
| | | try { |
| | | await updateReceivableStatus(row.id!, 20); |
| | | message.success('å®¡æ ¸æå'); |
| | | gridApi.query(); |
| | | await gridApi.query(); |
| | | emit('refresh'); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | |
| | | |
| | | /** æå¼åå详æ
*/ |
| | | function handleContractDetail(row: CrmReceivableApi.Receivable) { |
| | | push({ name: 'CrmContractDetail', params: { id: row.contractId } }); |
| | | if (row.contractId) push({ name: 'CrmContractDetail', params: { id: row.contractId } }); |
| | | } |
| | | |
| | | function handleInvoiceDetail(row: CrmReceivableApi.Receivable) { |
| | | if (row.invoiceId) push({ name: 'CrmInvoiceDetail', params: { id: row.invoiceId } }); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | |
| | | schema: [ |
| | | { |
| | | fieldName: 'auditStatus', |
| | | label: 'ååç¶æ', |
| | | label: 'å®¡æ ¸ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | |
| | | return await getReceivablePage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | sceneType: 1, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | |
| | | </Button> |
| | | </template> |
| | | <template #contractNo="{ row }"> |
| | | <Button type="link" @click="handleContractDetail(row)"> |
| | | {{ row.contractNo }} |
| | | <Button v-if="row.contractId" type="link" @click="handleContractDetail(row)"> |
| | | {{ row.contract?.no ?? row.contractNo ?? 'â' }} |
| | | </Button> |
| | | <span v-else>â</span> |
| | | </template> |
| | | <template #invoiceNo="{ row }"> |
| | | <template v-if="row.invoice"> |
| | | <Button type="link" @click="handleInvoiceDetail(row)">{{ row.invoice.no }}</Button> |
| | | <span> / {{ row.invoice.invoiceNo ?? 'â' }} / Â¥{{ row.invoice.price ?? 0 }}</span> |
| | | </template> |
| | | <span v-else>â</span> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <Button |
| | | v-if="row.auditStatus === 10" |
| | | type="link" |
| | | @click="handleAudit(row)" |
| | | > |
| | | å®¡æ ¸ |
| | | </Button> |
| | | <TableAction :actions="[{ |
| | | label: 'å®¡æ ¸', |
| | | type: 'link', |
| | | auth: ['crm:receivable:update'], |
| | | ifShow: row.auditStatus === 10, |
| | | onClick: handleAudit.bind(null, row), |
| | | }]" /> |
| | | </template> |
| | | </Grid> |
| | | </template> |
| | |
| | | |
| | | import { Button } from 'ant-design-vue'; |
| | | |
| | | import { useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getReceivablePlanPage } from '#/api/crm/receivable/plan'; |
| | | import Form from '#/views/crm/receivable/modules/form.vue'; |
| | | import { useGridColumns } from '#/views/crm/receivable/plan/data'; |
| | |
| | | import { RECEIVABLE_REMIND_TYPE } from '../data'; |
| | | |
| | | const { push } = useRouter(); |
| | | const emit = defineEmits<{ refresh: [] }>(); |
| | | |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | emit('refresh'); |
| | | } |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** æå¼å款详æ
*/ |
| | | /** æå¼å款计å详æ
*/ |
| | | function handleDetail(row: CrmReceivablePlanApi.Plan) { |
| | | push({ name: 'CrmReceivableDetail', params: { id: row.id } }); |
| | | push({ name: 'CrmReceivablePlanDetail', params: { id: row.id } }); |
| | | } |
| | | |
| | | /** æå¼å®¢æ·è¯¦æ
*/ |
| | |
| | | |
| | | /** åå»ºåæ¬¾ */ |
| | | function handleCreateReceivable(row: CrmReceivablePlanApi.Plan) { |
| | | if (row.receivableId) return; |
| | | formModalApi.setData({ plan: row }).open(); |
| | | } |
| | | |
| | | const [Grid] = useVbenVxeGrid({ |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: [ |
| | | { |
| | | fieldName: 'remindType', |
| | | label: 'ååç¶æ', |
| | | label: '忬¾ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | |
| | | return await getReceivablePlanPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | sceneType: 1, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | |
| | | |
| | | <template> |
| | | <div> |
| | | <FormModal /> |
| | | <FormModal @success="handleRefresh" /> |
| | | <Grid> |
| | | <template #customerName="{ row }"> |
| | | <Button type="link" @click="handleCustomerDetail(row)"> |
| | |
| | | <Button type="link" @click="handleDetail(row)">{{ row.period }}</Button> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <Button type="link" @click="handleCreateReceivable(row)"> |
| | | åå»ºåæ¬¾ |
| | | </Button> |
| | | <TableAction :actions="[{ |
| | | label: 'åå»ºåæ¬¾', |
| | | type: 'link', |
| | | auth: ['crm:receivable:create'], |
| | | ifShow: !row.receivableId, |
| | | onClick: handleCreateReceivable.bind(null, row), |
| | | }]" /> |
| | | </template> |
| | | </Grid> |
| | | </div> |
| | |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | showTime: false, |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | placeholder: 'è¯·éæ©é¢è®¡æäº¤æ¥æ', |
| | | }, |
| | |
| | | { |
| | | field: 'contactNextTime', |
| | | title: '䏿¬¡èç³»æ¶é´', |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | width: 180, |
| | | }, |
| | | { |
| | |
| | | { |
| | | field: 'contactLastTime', |
| | | title: 'æåè·è¿æ¶é´', |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | width: 180, |
| | | }, |
| | | { |
| | | field: 'updateTime', |
| | | title: 'æ´æ°æ¶é´', |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | width: 180, |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | width: 180, |
| | | }, |
| | | { |
| | |
| | | import type { CrmBusinessApi } from '#/api/crm/business'; |
| | | import type { DescriptionItemSchema } from '#/components/description'; |
| | | |
| | | import { erpPriceInputFormatter, formatDateTime } from '@vben/utils'; |
| | | import { erpPriceInputFormatter, formatDate } from '@vben/utils'; |
| | | |
| | | import { |
| | | getBusinessStatusSimpleList, |
| | |
| | | { |
| | | field: 'createTime', |
| | | label: 'å建æ¶é´', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | ]; |
| | | } |
| | |
| | | { |
| | | field: 'dealTime', |
| | | label: 'é¢è®¡æäº¤æ¥æ', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | field: 'contactNextTime', |
| | | label: '䏿¬¡èç³»æ¶é´', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | field: 'statusTypeName', |
| | |
| | | /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane tab="æä½æ¥å¿" key="6" :force-render="true"> |
| | | <OperateLog :log-list="logList" /> |
| | | <OperateLog :log-list="logList" date-format="YYYY-MM-DD" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane tab="å¢éæå" key="7" :force-render="true"> |
| | | <PermissionList |
| | |
| | | { |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | showTime: true, |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | placeholder: 'è¯·éæ©ä¸æ¬¡èç³»æ¶é´', |
| | | }, |
| | |
| | | component: 'RangePicker', |
| | | componentProps: { |
| | | ...getRangePickerDefaultProps(), |
| | | format: 'YYYY-MM-DD', |
| | | allowClear: true, |
| | | placeholder: ['å¼å§æ¥æ', 'ç»ææ¥æ'], |
| | | }, |
| | |
| | | { |
| | | field: 'contactNextTime', |
| | | title: '䏿¬¡èç³»æ¶é´', |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | minWidth: 180, |
| | | }, |
| | | { |
| | |
| | | { |
| | | field: 'contactLastTime', |
| | | title: 'æåè·è¿æ¶é´', |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | minWidth: 180, |
| | | }, |
| | | { |
| | |
| | | { |
| | | field: 'updateTime', |
| | | title: 'æ´æ°æ¶é´', |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | minWidth: 180, |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | minWidth: 180, |
| | | }, |
| | | { |
| | |
| | | import { h } from 'vue'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { formatDateTime } from '@vben/utils'; |
| | | import { formatDate } from '@vben/utils'; |
| | | |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | |
| | |
| | | { |
| | | field: 'createTime', |
| | | label: 'å建æ¶é´', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | ]; |
| | | } |
| | |
| | | { |
| | | field: 'contactNextTime', |
| | | label: '䏿¬¡èç³»æ¶é´', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | field: 'remark', |
| | |
| | | /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane tab="æä½æ¥å¿" key="4" :force-render="true"> |
| | | <OperateLog :log-list="logList" /> |
| | | <OperateLog :log-list="logList" date-format="YYYY-MM-DD" /> |
| | | </Tabs.TabPane> |
| | | </Tabs> |
| | | </Card> |
| | |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | showTime: true, |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | placeholder: 'è¯·éæ©ä¸æ¬¡èç³»æ¶é´', |
| | | }, |
| | |
| | | { |
| | | field: 'contactNextTime', |
| | | title: '䏿¬¡èç³»æ¶é´', |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | minWidth: 180, |
| | | }, |
| | | { |
| | |
| | | { |
| | | field: 'contactLastTime', |
| | | title: 'æåè·è¿æ¶é´', |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | minWidth: 180, |
| | | }, |
| | | { |
| | |
| | | { |
| | | field: 'updateTime', |
| | | title: 'æ´æ°æ¶é´', |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | minWidth: 180, |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | minWidth: 180, |
| | | }, |
| | | { |
| | |
| | | import { h } from 'vue'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { formatDateTime } from '@vben/utils'; |
| | | import { formatDate } from '@vben/utils'; |
| | | |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | |
| | |
| | | { |
| | | field: 'createTime', |
| | | label: 'å建æ¶é´', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | ]; |
| | | } |
| | |
| | | { |
| | | field: 'contactNextTime', |
| | | label: '䏿¬¡èç³»æ¶é´', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | field: 'remark', |
| | |
| | | <Info :contact="contact" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane tab="æä½æ¥å¿" key="3" :force-render="true"> |
| | | <OperateLog :log-list="logList" /> |
| | | <OperateLog :log-list="logList" date-format="YYYY-MM-DD" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane tab="å¢éæå" key="4" :force-render="true"> |
| | | <PermissionList |
| | |
| | | title: 'ååå¼å§æ¶é´', |
| | | field: 'startTime', |
| | | minWidth: 150, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'ååç»ææ¶é´', |
| | | field: 'endTime', |
| | | minWidth: 150, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: '已忬¾éé¢ï¼å
ï¼', |
| | |
| | | title: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | minWidth: 150, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'å建人', |
| | |
| | | |
| | | export function useGridColumns(): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { type: 'checkbox', width: 50, fixed: 'left' }, |
| | | { |
| | | title: 'ååç¼å·', |
| | | field: 'no', |
| | |
| | | title: 'ä¸åæ¶é´', |
| | | field: 'orderDate', |
| | | minWidth: 120, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'ååå¼å§æ¶é´', |
| | | field: 'startTime', |
| | | minWidth: 120, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'ååç»ææ¶é´', |
| | | field: 'endTime', |
| | | minWidth: 120, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: '客æ·ç¾çº¦äºº', |
| | |
| | | title: 'æåè·è¿æ¶é´', |
| | | field: 'contactLastTime', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'è´è´£äºº', |
| | |
| | | title: 'æ´æ°æ¶é´', |
| | | field: 'updateTime', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'å建人', |
| | |
| | | import { |
| | | erpCountInputFormatter, |
| | | erpPriceInputFormatter, |
| | | formatDateTime, |
| | | formatDate, |
| | | } from '@vben/utils'; |
| | | |
| | | import { Spin } from 'ant-design-vue'; |
| | |
| | | { |
| | | label: 'ä¸åæ¶é´', |
| | | field: 'orderDate', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | label: 'ååå¼å§æ¶é´', |
| | | field: 'startTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | label: 'ååç»ææ¶é´', |
| | | field: 'endTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { label: '客æ·ç¾çº¦äºº', field: 'signContactName', render: textRender }, |
| | | { label: 'å
¬å¸ç¾çº¦äºº', field: 'signUserName', render: textRender }, |
| | |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | label: 'ååç¶æ', |
| | |
| | | import { h } from 'vue'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { erpPriceInputFormatter, formatDateTime } from '@vben/utils'; |
| | | import { erpPriceInputFormatter, formatDate } from '@vben/utils'; |
| | | |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | |
| | |
| | | { |
| | | field: 'orderDate', |
| | | label: 'ä¸åæ¶é´', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | field: 'totalReceivablePrice', |
| | |
| | | { |
| | | field: 'orderDate', |
| | | label: 'ä¸åæ¶é´', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | field: 'startTime', |
| | | label: 'ååå¼å§æ¶é´', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | field: 'endTime', |
| | | label: 'ååç»ææ¶é´', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | field: 'signContactName', |
| | |
| | | import { OperateLog } from '#/components/operate-log'; |
| | | import { ACTION_ICON, TableAction } from '#/components/table-action'; |
| | | import { $t } from '#/locales'; |
| | | import { downloadContractWord } from '#/utils/contract-document'; |
| | | import { FollowUp } from '#/views/crm/followup'; |
| | | import { PermissionList, TransferForm } from '#/views/crm/permission'; |
| | | import { ProductDetailsList } from '#/views/crm/product/components'; |
| | |
| | | onClick: handleEdit, |
| | | }, |
| | | { |
| | | label: 'ä¸è½½åå', |
| | | type: 'primary', |
| | | icon: 'lucide:download', |
| | | auth: ['crm:contract:query'], |
| | | onClick: () => downloadContractWord(contractId, contract.no), |
| | | }, |
| | | { |
| | | label: '转移', |
| | | type: 'primary', |
| | | ifShow: permissionListRef?.validateOwnerUser, |
| | |
| | | /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane tab="æä½æ¥å¿" key="6" :force-render="true"> |
| | | <OperateLog :log-list="logList" /> |
| | | <OperateLog :log-list="logList" date-format="YYYY-MM-DD" /> |
| | | </Tabs.TabPane> |
| | | </Tabs> |
| | | </Card> |
| | |
| | | submitContract, |
| | | } from '#/api/crm/contract'; |
| | | import { $t } from '#/locales'; |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import { downloadContractWord } from '#/utils/contract-document'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | | gridApi.grid.clearCheckboxRow(); |
| | | gridApi.query(); |
| | | } |
| | | |
| | |
| | | const formValues = await gridApi.formApi.getValues(); |
| | | const data = await exportContract({ |
| | | sceneType: sceneType.value, |
| | | ...formValues, |
| | | }); |
| | | downloadFileFromBlobPart({ fileName: 'åå.xls', source: data }); |
| | | } |
| | | |
| | | /** 导åºå¾éçåå */ |
| | | async function handleExportSelected() { |
| | | const rows: CrmContractApi.Contract[] = |
| | | gridApi.grid.getCheckboxRecords?.() ?? []; |
| | | const ids = rows.map((row) => row.id!); |
| | | if (ids.length === 0) { |
| | | message.warning('请å
å¾éè¦å¯¼åºçåå'); |
| | | return; |
| | | } |
| | | const formValues = await gridApi.formApi.getValues(); |
| | | const data = await exportContract({ |
| | | sceneType: sceneType.value, |
| | | ids: ids.join(','), |
| | | ...formValues, |
| | | }); |
| | | downloadFileFromBlobPart({ fileName: 'åå.xls', source: data }); |
| | |
| | | const submitLoading = ref(false); |
| | | |
| | | async function handleSubmit(row: CrmContractApi.Contract) { |
| | | const res = await getContractApproveProcessList(); |
| | | const res = automaticApproval.value ? [] : await getContractApproveProcessList(); |
| | | processList.value = res; |
| | | currentRow.value = row; |
| | | selectedProcessKey.value = undefined; |
| | | if (automaticApproval.value) { |
| | | selectedProcessKey.value = 'contract_approve'; |
| | | await confirmSubmit(); |
| | | return; |
| | | } |
| | | submitModalVisible.value = true; |
| | | } |
| | | |
| | |
| | | submitLoading.value = true; |
| | | try { |
| | | await submitContract(currentRow.value!.id!, selectedProcessKey.value); |
| | | message.success('æäº¤å®¡æ¹æå'); |
| | | message.success(automaticApproval.value ? 'å·²æäº¤çæ' : 'æäº¤å®¡æ¹æå'); |
| | | submitModalVisible.value = false; |
| | | handleRefresh(); |
| | | } finally { |
| | |
| | | onClick: handleExport, |
| | | }, |
| | | { |
| | | label: '导åºéä¸', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.DOWNLOAD, |
| | | auth: ['crm:contract:export'], |
| | | onClick: handleExportSelected, |
| | | }, |
| | | { |
| | | label: '导å
¥', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.UPLOAD, |
| | |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'ä¸è½½åå', |
| | | type: 'link', |
| | | icon: 'lucide:download', |
| | | auth: ['crm:contract:query'], |
| | | onClick: () => downloadContractWord(row.id, row.no), |
| | | }, |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | showTime: true, |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | placeholder: 'è¯·éæ©ä¸æ¬¡èç³»æ¶é´', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'autoPoolEnabled', |
| | | label: 'æ¯å¦æµå
¥å
¬æµ·', |
| | | component: 'Switch', |
| | | defaultValue: true, |
| | | componentProps: { |
| | | checkedChildren: 'æ¯', |
| | | unCheckedChildren: 'å¦', |
| | | }, |
| | | help: 'å
³éåä¸èªå¨æµå
¥å
¬æµ·ï¼å¹¶åæ¶æ®éç¨æ·çå
¬æµ·å¯è§åº¦ï¼ä¿çè´è´£äººåå¢éæéï¼è¶
级管çåä»å¯æ¥çã', |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'autoPoolEnabled', |
| | | label: 'æ¯å¦æµå
¥å
¬æµ·', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: [ |
| | | { label: 'æ¯', value: true }, |
| | | { label: 'å¦', value: false }, |
| | | ], |
| | | placeholder: 'è¯·éæ©', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'createTime', |
| | | label: 'å建æ¶é´', |
| | | component: 'RangePicker', |
| | | componentProps: { |
| | | ...getRangePickerDefaultProps(), |
| | | format: 'YYYY-MM-DD', |
| | | placeholder: ['å¼å§æ¥æ', 'ç»ææ¥æ'], |
| | | allowClear: true, |
| | | }, |
| | |
| | | label: 'å®¢æ·æ°æ®', |
| | | component: 'Upload', |
| | | rules: 'required', |
| | | help: 'ä»
å
许导å
¥ xlsãxlsx æ ¼å¼æä»¶', |
| | | help: 'ä»
å
许导å
¥ xlsãxlsx æ ¼å¼æä»¶ãâæ¯å¦æµå
¥å
¬æµ·âå填忝/å¦ï¼çç©ºæ¶æ°å¢é»è®¤æ¯ï¼è¦çä¿çåå¼ãå
³éååæ¶å
¬æµ·å¯è§åº¦ã', |
| | | }, |
| | | { |
| | | fieldName: 'updateSupport', |
| | |
| | | }, |
| | | }, |
| | | { |
| | | field: 'mobile', |
| | | title: 'ææº', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'telephone', |
| | | title: 'çµè¯', |
| | | minWidth: 130, |
| | | }, |
| | | { |
| | | field: 'email', |
| | | title: 'é®ç®±', |
| | | minWidth: 180, |
| | | }, |
| | | { |
| | | field: 'level', |
| | | title: '客æ·çº§å«', |
| | | minWidth: 135, |
| | |
| | | { |
| | | field: 'contactNextTime', |
| | | title: '䏿¬¡èç³»æ¶é´', |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | minWidth: 180, |
| | | }, |
| | | { |
| | |
| | | }, |
| | | }, |
| | | { |
| | | field: 'dealStatus', |
| | | title: 'æäº¤ç¶æ', |
| | | minWidth: 120, |
| | | field: 'autoPoolEnabled', |
| | | title: 'æ¯å¦æµå
¥å
¬æµ·', |
| | | minWidth: 140, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'contractCount', |
| | | title: 'ç¾è®¢åå', |
| | | minWidth: 170, |
| | | slots: { default: 'contracts' }, |
| | | }, |
| | | { |
| | | field: 'contactLastTime', |
| | | title: 'æåè·è¿æ¶é´', |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | minWidth: 180, |
| | | }, |
| | | { |
| | |
| | | field: 'detailAddress', |
| | | title: 'å°å', |
| | | minWidth: 180, |
| | | }, |
| | | { |
| | | field: 'poolDay', |
| | | title: 'è·ç¦»è¿å
¥å
¬æµ·å¤©æ°', |
| | | minWidth: 140, |
| | | formatter: ({ cellValue }) => |
| | | cellValue === null ? '-' : `${cellValue} 天`, |
| | | }, |
| | | { |
| | | field: 'ownerUserName', |
| | |
| | | { |
| | | field: 'updateTime', |
| | | title: 'æ´æ°æ¶é´', |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | minWidth: 180, |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | minWidth: 180, |
| | | }, |
| | | { |
| | |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { useUserStore } from '@vben/stores'; |
| | | import { formatDateTime } from '@vben/utils'; |
| | | import { formatDate } from '@vben/utils'; |
| | | |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import { DictTag } from '#/components/dict-tag'; |
| | |
| | | h(DictTag, { type: DICT_TYPE.CRM_CUSTOMER_LEVEL, value: val }), |
| | | }, |
| | | { |
| | | field: 'dealStatus', |
| | | label: 'æäº¤ç¶æ', |
| | | render: (val) => (val ? 'å·²æäº¤' : 'æªæäº¤'), |
| | | field: 'autoPoolEnabled', |
| | | label: 'æ¯å¦æµå
¥å
¬æµ·', |
| | | render: (val) => (val === false ? 'å¦' : 'æ¯'), |
| | | }, |
| | | { |
| | | field: 'ownerUserName', |
| | |
| | | { |
| | | field: 'createTime', |
| | | label: 'å建æ¶é´', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | ]; |
| | | } |
| | |
| | | { |
| | | field: 'contactNextTime', |
| | | label: '䏿¬¡èç³»æ¶é´', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | field: 'remark', |
| | |
| | | lockCustomer, |
| | | putCustomerPool, |
| | | receiveCustomer, |
| | | updateCustomerDealStatus, |
| | | } from '#/api/crm/customer'; |
| | | import { getOperateLogPage } from '#/api/crm/operateLog'; |
| | | import { BizTypeEnum } from '#/api/crm/permission'; |
| | |
| | | // return true; |
| | | // } |
| | | |
| | | /** æ´æ°æäº¤ç¶ææä½ */ |
| | | async function handleUpdateDealStatus(): Promise<boolean | undefined> { |
| | | const dealStatus = !customer.value.dealStatus; |
| | | try { |
| | | await confirm( |
| | | `ç¡®å®æ´æ°æäº¤ç¶æä¸ºã${dealStatus ? 'å·²æäº¤' : 'æªæäº¤'}ãåï¼`, |
| | | ); |
| | | } catch { |
| | | return false; |
| | | } |
| | | // æ´æ°æäº¤ç¶æ |
| | | await updateCustomerDealStatus(customerId.value, dealStatus); |
| | | // æç¤ºå¹¶è¿åæå |
| | | message.success('æ´æ°æäº¤ç¶ææå'); |
| | | return true; |
| | | } |
| | | |
| | | /** å è½½æ°æ® */ |
| | | onMounted(() => { |
| | | customerId.value = Number(route.params.id); |
| | |
| | | type: 'primary', |
| | | ifShow: permissionListRef?.validateOwnerUser, |
| | | onClick: handleTransfer, |
| | | }, |
| | | { |
| | | label: 'æ´æ¹æäº¤ç¶æ', |
| | | type: 'default', |
| | | ifShow: permissionListRef?.validateWrite, |
| | | onClick: handleUpdateDealStatus, |
| | | }, |
| | | { |
| | | label: 'éå®', |
| | |
| | | <ReceivableDetailsList :customer-id="customerId" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane tab="æä½æ¥å¿" key="8" :force-render="true"> |
| | | <OperateLog :log-list="logList" /> |
| | | <OperateLog :log-list="logList" date-format="YYYY-MM-DD" /> |
| | | </Tabs.TabPane> |
| | | </Tabs> |
| | | </Card> |
| | |
| | | <script lang="ts" setup> |
| | | import { ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | |
| | | import { useDistributeFormSchema } from '../data'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const customerIds = ref<number[]>([]); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | |
| | | // æäº¤è¡¨å |
| | | const data = await formApi.getValues(); |
| | | try { |
| | | await distributeCustomer([data.id], data.ownerUserId); |
| | | await distributeCustomer(customerIds.value, data.ownerUserId); |
| | | // å
³éå¹¶æç¤º |
| | | await modalApi.close(); |
| | | emit('success'); |
| | |
| | | return; |
| | | } |
| | | // å è½½æ°æ® |
| | | const data = modalApi.getData(); |
| | | if (!data || !data.id) { |
| | | const data = modalApi.getData<{ id?: number; ids?: number[] }>(); |
| | | customerIds.value = data?.ids ?? (data?.id ? [data.id] : []); |
| | | if (customerIds.value.length === 0) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | await formApi.setValues(data); |
| | | await formApi.resetForm(); |
| | | await formApi.setValues({ id: customerIds.value[0] }); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | import { downloadFileFromBlobPart, erpNumberFormatter } from '@vben/utils'; |
| | | |
| | | import { Button, message, Tabs } from 'ant-design-vue'; |
| | | |
| | |
| | | {{ row.name }} |
| | | </Button> |
| | | </template> |
| | | <template #contracts="{ row }"> |
| | | <template v-if="row.contractCount"> |
| | | {{ row.contractCount }} 份 · ¥{{ erpNumberFormatter(row.contractTotalPrice, 2) }} |
| | | </template> |
| | | <template v-else>â</template> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | |
| | | { |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button, message, Upload } from 'ant-design-vue'; |
| | | import { Button, Upload } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { importCustomer, importCustomerTemplate } from '#/api/crm/customer'; |
| | | import { $t } from '#/locales'; |
| | | import { notifyImportResult } from '#/utils'; |
| | | |
| | | import { useImportFormSchema } from '../data'; |
| | | |
| | |
| | | // æäº¤è¡¨å |
| | | const data = await formApi.getValues(); |
| | | try { |
| | | await importCustomer({ |
| | | const importData = await importCustomer({ |
| | | ownerUserId: data.ownerUserId, |
| | | file: data.file, |
| | | updateSupport: data.updateSupport, |
| | | }); |
| | | // å
³éå¹¶æç¤º |
| | | const createCount = importData.createCustomerNames?.length || 0; |
| | | const updateCount = importData.updateCustomerNames?.length || 0; |
| | | |
| | | // å
³éå¹¶æå¯¼å
¥ç»ææç¤ºï¼ä»
å¨ç¡®ææ°æ®åå
¥æ¶éç¥åè¡¨å·æ° |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | if (createCount + updateCount > 0) { |
| | | emit('success'); |
| | | } |
| | | notifyImportResult({ |
| | | subject: '客æ·', |
| | | createCount, |
| | | updateCount, |
| | | failures: importData.failureCustomerNames, |
| | | }); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | |
| | | export function useGridColumns(): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { type: 'checkbox', width: 50, fixed: 'left' }, |
| | | { |
| | | title: '客æ·åç§°', |
| | | field: 'name', |
| | |
| | | title: '䏿¬¡èç³»æ¶é´', |
| | | field: 'contactNextTime', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: '夿³¨', |
| | |
| | | minWidth: 200, |
| | | }, |
| | | { |
| | | title: 'æäº¤ç¶æ', |
| | | field: 'dealStatus', |
| | | minWidth: 80, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING }, |
| | | }, |
| | | }, |
| | | { |
| | | title: 'æåè·è¿æ¶é´', |
| | | field: 'contactLastTime', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æåè·è¿è®°å½', |
| | |
| | | minWidth: 200, |
| | | }, |
| | | { |
| | | title: 'è´è´£äºº', |
| | | field: 'ownerUserName', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | title: 'æå±é¨é¨', |
| | | field: 'ownerUserDeptName', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | title: 'æ´æ°æ¶é´', |
| | | field: 'updateTime', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'å建人', |
| | | field: 'creatorName', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 150, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| | |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { CrmCustomerApi } from '#/api/crm/customer'; |
| | | |
| | | import { ref } from 'vue'; |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | import { confirm, Page, useVbenModal } from '@vben/common-ui'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button } from 'ant-design-vue'; |
| | | import { Button, message } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { exportCustomer, getCustomerPage } from '#/api/crm/customer'; |
| | | import { |
| | | exportCustomer, |
| | | getCustomerPage, |
| | | receiveCustomer, |
| | | } from '#/api/crm/customer'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import DistributeForm from '../detail/modules/distribute-form.vue'; |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | |
| | | const { push } = useRouter(); |
| | | const receiving = ref(false); |
| | | const [DistributeModal, distributeModalApi] = useVbenModal({ |
| | | connectedComponent: DistributeForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | function handleRefresh() { |
| | | gridApi.grid.clearCheckboxRow(); |
| | | gridApi.query(); |
| | | } |
| | | |
| | | function getCustomerIds(row?: CrmCustomerApi.Customer): number[] { |
| | | const records: CrmCustomerApi.Customer[] = row |
| | | ? [row] |
| | | : gridApi.grid.getCheckboxRecords(); |
| | | if ( |
| | | records.some( |
| | | (item) => item.ownerUserId != null || item.autoPoolEnabled === false, |
| | | ) |
| | | ) { |
| | | message.warning('ä»
å¯é¢åæåé
æ è´è´£äººä¸å
许æµå
¥å
¬æµ·ç客æ·'); |
| | | return []; |
| | | } |
| | | const ids = records.map((item) => item.id!); |
| | | if (ids.length === 0) { |
| | | message.warning('请å
å¾é客æ·'); |
| | | } |
| | | return ids; |
| | | } |
| | | |
| | | /** é¢ååæ¡æå¾éçå
¬æµ·å®¢æ· */ |
| | | async function handleReceive(row?: CrmCustomerApi.Customer) { |
| | | if (receiving.value) return; |
| | | const ids = getCustomerIds(row); |
| | | if (ids.length === 0) return; |
| | | receiving.value = true; |
| | | try { |
| | | try { |
| | | await confirm( |
| | | row |
| | | ? `ç¡®å®é¢å客æ·ã${row.name}ãåï¼` |
| | | : `ç¡®å®é¢åéä¸ç ${ids.length} 个客æ·åï¼`, |
| | | ); |
| | | } catch { |
| | | return; |
| | | } |
| | | await receiveCustomer(ids); |
| | | message.success('é¢åå®¢æ·æå'); |
| | | handleRefresh(); |
| | | } finally { |
| | | receiving.value = false; |
| | | } |
| | | } |
| | | |
| | | /** åé
åæ¡æå¾éçå
¬æµ·å®¢æ· */ |
| | | function handleDistribute(row?: CrmCustomerApi.Customer) { |
| | | const ids = getCustomerIds(row); |
| | | if (ids.length === 0) return; |
| | | distributeModalApi.setData({ ids }).open(); |
| | | } |
| | | |
| | | /** 导åºè¡¨æ ¼ */ |
| | | async function handleExport() { |
| | | const data = await exportCustomer(await gridApi.formApi.getValues()); |
| | | const data = await exportCustomer({ |
| | | ...(await gridApi.formApi.getValues()), |
| | | pool: true, |
| | | sceneType: 4, |
| | | }); |
| | | downloadFileFromBlobPart({ fileName: '客æ·å
¬æµ·.xls', source: data }); |
| | | } |
| | | |
| | |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | checkboxConfig: { |
| | | checkMethod: ({ row }: { row: CrmCustomerApi.Customer }) => |
| | | row.ownerUserId == null && row.autoPoolEnabled !== false, |
| | | }, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height><Grid> |
| | | <Page auto-content-height> |
| | | <DistributeModal @success="handleRefresh" /> |
| | | <Grid> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'é¢å客æ·', |
| | | type: 'primary', |
| | | auth: ['crm:customer:receive'], |
| | | disabled: receiving, |
| | | onClick: () => handleReceive(), |
| | | }, |
| | | { |
| | | label: 'åé
客æ·', |
| | | auth: ['crm:customer:distribute'], |
| | | disabled: receiving, |
| | | onClick: () => handleDistribute(), |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.export'), |
| | | type: 'primary', |
| | |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'é¢å', |
| | | type: 'link', |
| | | auth: ['crm:customer:receive'], |
| | | disabled: receiving, |
| | | ifShow: row.ownerUserId == null && row.autoPoolEnabled !== false, |
| | | onClick: () => handleReceive(row), |
| | | }, |
| | | { |
| | | label: 'åé
', |
| | | type: 'link', |
| | | auth: ['crm:customer:distribute'], |
| | | disabled: receiving, |
| | | ifShow: row.ownerUserId == null && row.autoPoolEnabled !== false, |
| | | onClick: () => handleDistribute(row), |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #name="{ row }"> |
| | | <Button type="link" @click="handleDetail(row)"> |
| | | {{ row.name }} |
| | |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | import { formatDateTime } from '@vben/utils'; |
| | | import { formatDate } from '@vben/utils'; |
| | | |
| | | import { getBusinessPageByCustomer } from '#/api/crm/business'; |
| | | import { getContactPageByCustomer } from '#/api/crm/contact'; |
| | |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | showTime: true, |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | placeholder: 'è¯·éæ©ä¸æ¬¡èç³»æ¶é´', |
| | | }, |
| | |
| | | { |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { field: 'creatorName', title: 'è·è¿äºº' }, |
| | | { |
| | |
| | | { |
| | | field: 'nextTime', |
| | | title: '䏿¬¡èç³»æ¶é´', |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'contacts', |
| | |
| | | { |
| | | field: 'contactLastTime', |
| | | label: 'æåè·è¿æ¶é´', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | field: 'creatorName', |
| | |
| | | { |
| | | field: 'createTime', |
| | | label: 'å建æ¶é´', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | field: 'updateTime', |
| | | label: 'æ´æ°æ¶é´', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | ]; |
| | | } |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'invoiceNo', |
| | | label: 'å票å·ç ', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å票å·ç ï¼æ¯ææ¨¡ç³æ¥è¯¢', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'customerId', |
| | | label: '客æ·', |
| | | component: markRaw(CrmCustomerSelect), |
| | |
| | | title: 'å¼ç¥¨æ¥æ', |
| | | field: 'invoiceTime', |
| | | minWidth: 150, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: '夿³¨', |
| | |
| | | title: 'æ´æ°æ¶é´', |
| | | field: 'updateTime', |
| | | minWidth: 150, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | minWidth: 150, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'å建人', |
| | |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { erpPriceInputFormatter, formatDateTime } from '@vben/utils'; |
| | | import { erpPriceInputFormatter, formatDate } from '@vben/utils'; |
| | | |
| | | import { Spin } from 'ant-design-vue'; |
| | | |
| | |
| | | { |
| | | label: 'å¼ç¥¨æ¥æ', |
| | | field: 'invoiceTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | label: '审æ¹ç¶æ', |
| | |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { label: '夿³¨', field: 'remark', render: textRender }, |
| | | ] as DescriptionItemSchema[], |
| | |
| | | import { h } from 'vue'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { erpPriceInputFormatter, formatDateTime } from '@vben/utils'; |
| | | import { erpPriceInputFormatter, formatDate } from '@vben/utils'; |
| | | |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | |
| | |
| | | { |
| | | field: 'invoiceTime', |
| | | label: 'å¼ç¥¨æ¥æ', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | field: 'invoiceType', |
| | |
| | | { |
| | | field: 'invoiceTime', |
| | | label: 'å¼ç¥¨æ¥æ', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | field: 'remark', |
| | |
| | | { |
| | | field: 'createTime', |
| | | label: 'å建æ¶é´', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | field: 'updateTime', |
| | | label: 'æ´æ°æ¶é´', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | ]; |
| | | } |
| | |
| | | import { OperateLog } from '#/components/operate-log'; |
| | | import { ACTION_ICON, TableAction } from '#/components/table-action'; |
| | | import { $t } from '#/locales'; |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import { PermissionList } from '#/views/crm/permission'; |
| | | |
| | | import InvoiceForm from '../modules/form.vue'; |
| | |
| | | type: 'primary', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['crm:invoice:update'], |
| | | ifShow: permissionListRef?.validateWrite, |
| | | ifShow: permissionListRef?.validateWrite && (automaticApproval || invoice.auditStatus === 0 || invoice.auditStatus === 10), |
| | | onClick: handleEdit, |
| | | }, |
| | | ]" |
| | |
| | | <Info :invoice="invoice" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane tab="æä½æ¥å¿" key="2" :force-render="true"> |
| | | <OperateLog :log-list="logList" /> |
| | | <OperateLog :log-list="logList" date-format="YYYY-MM-DD" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane tab="å¢éæå" key="3" :force-render="true"> |
| | | <PermissionList |
| | |
| | | submitInvoice, |
| | | } from '#/api/crm/invoice'; |
| | | import { $t } from '#/locales'; |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | |
| | | /** æäº¤å®¡æ ¸ */ |
| | | async function handleSubmit(row: CrmInvoiceApi.Invoice) { |
| | | const hideLoading = message.loading({ |
| | | content: 'æäº¤å®¡æ ¸ä¸...', |
| | | content: automaticApproval.value ? 'æ£å¨æäº¤çæ...' : 'æäº¤å®¡æ ¸ä¸...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await submitInvoice(row.id!); |
| | | message.success('æäº¤å®¡æ ¸æå'); |
| | | message.success(automaticApproval.value ? 'å·²æäº¤çæ' : 'æäº¤å®¡æ ¸æå'); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['crm:invoice:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | ifShow: automaticApproval || row.auditStatus === 0 || row.auditStatus === 10, |
| | | }, |
| | | { |
| | | label: 'æäº¤å®¡æ ¸', |
| | |
| | | { |
| | | field: 'createTime', |
| | | title: 'å å
¥æ¶é´', |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | ]; |
| | | } |
| | |
| | | { |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'actions', |
| | |
| | | { |
| | | field: 'updateTime', |
| | | title: 'æ´æ°æ¶é´', |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | minWidth: 180, |
| | | }, |
| | | { |
| | |
| | | { |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | minWidth: 180, |
| | | }, |
| | | { |
| | |
| | | <Info :product="product" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane tab="æä½æ¥å¿" key="2" :force-render="true"> |
| | | <OperateLog :log-list="logList" /> |
| | | <OperateLog :log-list="logList" date-format="YYYY-MM-DD" /> |
| | | </Tabs.TabPane> |
| | | </Tabs> |
| | | </Card> |
| | |
| | | title: '忬¾æ¥æ', |
| | | field: 'returnTime', |
| | | minWidth: 150, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: '忬¾éé¢ï¼å
ï¼', |
| | |
| | | title: '忬¾æ¥æ', |
| | | field: 'returnTime', |
| | | minWidth: 150, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: '忬¾éé¢ï¼å
ï¼', |
| | |
| | | title: 'æ´æ°æ¶é´', |
| | | field: 'updateTime', |
| | | minWidth: 150, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | minWidth: 150, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'å建人', |
| | |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { erpPriceInputFormatter, formatDateTime } from '@vben/utils'; |
| | | import { erpPriceInputFormatter, formatDate } from '@vben/utils'; |
| | | |
| | | import { Spin } from 'ant-design-vue'; |
| | | |
| | |
| | | { |
| | | label: '忬¾æ¥æ', |
| | | field: 'returnTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | label: '忬¾éé¢ï¼å
ï¼', |
| | |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { label: '夿³¨', field: 'remark', render: textRender }, |
| | | ] as DescriptionItemSchema[], |
| | |
| | | import { h } from 'vue'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { erpPriceInputFormatter, formatDateTime } from '@vben/utils'; |
| | | import { erpPriceInputFormatter, formatDate } from '@vben/utils'; |
| | | |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | |
| | |
| | | { |
| | | field: 'returnTime', |
| | | label: '忬¾æ¥æ', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | field: 'price', |
| | |
| | | { |
| | | field: 'returnTime', |
| | | label: '忬¾æ¥æ', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | field: 'price', |
| | |
| | | { |
| | | field: 'createTime', |
| | | label: 'å建æ¶é´', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | field: 'updateTime', |
| | | label: 'æ´æ°æ¶é´', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | ]; |
| | | } |
| | |
| | | <Info :receivable="receivable" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane tab="æä½æ¥å¿" key="2" :force-render="true"> |
| | | <OperateLog :log-list="logList" /> |
| | | <OperateLog :log-list="logList" date-format="YYYY-MM-DD" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane tab="å¢éæå" key="3" :force-render="true"> |
| | | <PermissionList |
| | |
| | | title: '计å忬¾æ¥æ', |
| | | field: 'returnTime', |
| | | minWidth: 150, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æåå 天æé', |
| | |
| | | title: 'æéæ¥æ', |
| | | field: 'remindTime', |
| | | minWidth: 150, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'è´è´£äºº', |
| | |
| | | title: '计å忬¾æ¥æ', |
| | | field: 'returnTime', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æåå 天æé', |
| | |
| | | title: 'æéæ¥æ', |
| | | field: 'remindTime', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: '忬¾æ¹å¼', |
| | |
| | | title: 'å®é
忬¾æ¥æ', |
| | | field: 'receivable.returnTime', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æªå款éé¢ï¼å
ï¼', |
| | |
| | | title: 'æ´æ°æ¶é´', |
| | | field: 'updateTime', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'å建人', |
| | |
| | | import { h } from 'vue'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { erpPriceInputFormatter, formatDateTime } from '@vben/utils'; |
| | | import { erpPriceInputFormatter, formatDate } from '@vben/utils'; |
| | | |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | |
| | |
| | | { |
| | | field: 'returnTime', |
| | | label: '计å忬¾æ¥æ', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | field: 'receivable', |
| | |
| | | { |
| | | field: 'returnTime', |
| | | label: '计å忬¾æ¥æ', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | field: 'price', |
| | |
| | | { |
| | | field: 'receivable.returnTime', |
| | | label: 'å®é
忬¾æ¥æ', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | field: 'remark', |
| | |
| | | { |
| | | field: 'createTime', |
| | | label: 'å建æ¶é´', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | field: 'updateTime', |
| | | label: 'æ´æ°æ¶é´', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | ]; |
| | | } |
| | |
| | | <ReceivablePlanDetailsInfo :receivable-plan="receivablePlan" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane tab="æä½æ¥å¿" key="2" :force-render="true"> |
| | | <OperateLog :log-list="logList" /> |
| | | <OperateLog :log-list="logList" date-format="YYYY-MM-DD" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane tab="å¢éæå" key="3" :force-render="true"> |
| | | <PermissionList |
| | |
| | | field: 'quotationTime', |
| | | title: 'æ¥ä»·æ¥æ', |
| | | minWidth: 120, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'validUntil', |
| | | title: 'æææè³', |
| | | minWidth: 120, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'status', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | getSaleQuotationPage, |
| | | } from '#/api/crm/saleQuotation'; |
| | | import { $t } from '#/locales'; |
| | | import { downloadContractWord } from '#/utils/contract-document'; |
| | | |
| | | import type { CrmSaleQuotationAiApi } from '#/api/crm/saleQuotation/ai'; |
| | | |
| | | import { nextTick, ref } from 'vue'; |
| | | import { computed, nextTick, ref } from 'vue'; |
| | | |
| | | import { isAiEnable } from '@vben/hooks'; |
| | | |
| | | import { QUOTATION_STATUS, useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | |
| | | async onOk() { |
| | | const contractId = await convertSaleQuotationToContract(row.id!); |
| | | message.success('转ååæå'); |
| | | gridApi.query(); |
| | | try { |
| | | await downloadContractWord(contractId); |
| | | } catch { |
| | | message.warning('ååå·²åå»ºï¼æä»¶ä¸è½½å¤±è´¥ï¼å¯å¨åå详æ
ä¸éæ°ä¸è½½'); |
| | | } |
| | | push({ name: 'CrmContractDetail', params: { id: contractId } }); |
| | | }, |
| | | }); |
| | | } |
| | | |
| | | /** å·¥å
·æ æé®ï¼è¯å«å½å
¥å AI æ»å¼å
³æ§å¶ */ |
| | | const toolbarActions = computed(() => [ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['æ¥ä»·å']), |
| | | type: 'primary' as const, |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['crm:sale-quotation:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | ...(isAiEnable() |
| | | ? [ |
| | | { |
| | | label: 'è¯å«å½å
¥', |
| | | type: 'default' as const, |
| | | auth: ['crm:sale-quotation:create'], |
| | | onClick: handleOcrCreate, |
| | | }, |
| | | ] |
| | | : []), |
| | | ]); |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | |
| | | <OcrUpload ref="ocrUploadRef" @success="handleOcrSuccess" /> |
| | | <Grid> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['æ¥ä»·å']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['crm:sale-quotation:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: 'è¯å«å½å
¥', |
| | | type: 'default', |
| | | auth: ['crm:sale-quotation:create'], |
| | | onClick: handleOcrCreate, |
| | | }, |
| | | ]" |
| | | /> |
| | | <TableAction :actions="toolbarActions" /> |
| | | </template> |
| | | <template #name="{ row }"> |
| | | <Button type="link" @click="handleDetail(row)"> |
| | |
| | | ifShow: !!row.contractId, |
| | | }, |
| | | { |
| | | label: 'ä¸è½½åå', |
| | | type: 'link', |
| | | auth: ['crm:contract:query'], |
| | | onClick: () => downloadContractWord(row.contractId!, row.contractNo), |
| | | ifShow: !!row.contractId, |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| | | </template> |
| | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | import { formatDate } from '@vben/utils'; |
| | | |
| | | import { Modal, Button, Upload, Descriptions, Table, Tag, message, Alert } from 'ant-design-vue'; |
| | | |
| | |
| | | <Tag color="orange">{{ ocrResult.customerName }}</Tag> |
| | | </Descriptions.Item> |
| | | <Descriptions.Item v-if="ocrResult.quotationTime" label="æ¥ä»·æ¥æ"> |
| | | {{ ocrResult.quotationTime }} |
| | | {{ formatDate(ocrResult.quotationTime) }} |
| | | </Descriptions.Item> |
| | | <Descriptions.Item v-if="ocrResult.validUntil" label="æææè³"> |
| | | {{ ocrResult.validUntil }} |
| | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { SALES_TARGET_PERIOD_TYPE } from '../data'; |
| | | |
| | | /** |
| | | * æå¨æç±»åç®å½å卿å¼ï¼å¹´åº¦ 2026 / å£åº¦ 2026-Q4 / æåº¦ 2026-10ã |
| | | * è¾¾æåææå¼æ¶é»è®¤è½å¨ãå½åå£åº¦ãï¼ä¸éå®ç®æ çæ¼ç¤ºæ°æ®å¨æä¸è´ã |
| | | */ |
| | | function currentPeriodValue(periodType?: number): string { |
| | | const now = new Date(); |
| | | const year = now.getFullYear(); |
| | | const month = now.getMonth() + 1; |
| | | if (periodType === SALES_TARGET_PERIOD_TYPE.YEAR) { |
| | | return `${year}`; |
| | | } |
| | | if (periodType === SALES_TARGET_PERIOD_TYPE.MONTH) { |
| | | return `${year}-${String(month).padStart(2, '0')}`; |
| | | } |
| | | return `${year}-Q${Math.ceil(month / 3)}`; |
| | | } |
| | | |
| | | /** è¾¾æåæçæ¥è¯¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | |
| | | label: 'å¨æç±»å', |
| | | component: 'Select', |
| | | rules: 'required', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.CRM_SALES_TARGET_PERIOD_TYPE, 'number'), |
| | | componentProps: (_values, form) => ({ |
| | | options: getDictOptions( |
| | | DICT_TYPE.CRM_SALES_TARGET_PERIOD_TYPE, |
| | | 'number', |
| | | ), |
| | | placeholder: 'è¯·éæ©å¨æç±»å', |
| | | }, |
| | | defaultValue: 1, |
| | | // å¨æç±»åä¸åå°±æå¨æå¼æ¢æå½å对åºå¨æï¼é¿å
çå¨ä¸ä¸ä¸ªç±»åçå¼ä¸æ¥ä¸å°æ°æ® |
| | | onChange: (value: number) => { |
| | | form.setFieldValue('periodValue', currentPeriodValue(value)); |
| | | }, |
| | | }), |
| | | defaultValue: SALES_TARGET_PERIOD_TYPE.QUARTER, |
| | | }, |
| | | { |
| | | fieldName: 'periodValue', |
| | |
| | | componentProps: { |
| | | placeholder: 'å¦ 2026 / 2026-Q1 / 2026-07', |
| | | }, |
| | | defaultValue: new Date().getFullYear().toString(), |
| | | defaultValue: currentPeriodValue(SALES_TARGET_PERIOD_TYPE.QUARTER), |
| | | }, |
| | | { |
| | | fieldName: 'targetDimension', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | minWidth: 160, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'creatorName', |
| | |
| | | beginOfDay, |
| | | endOfDay, |
| | | erpCalculatePercentage, |
| | | erpNumberFormatter, |
| | | formatDateTime, |
| | | handleTree, |
| | | } from '@vben/utils'; |
| | |
| | | component: 'RangePicker', |
| | | componentProps: { |
| | | ...getRangePickerDefaultProps(), |
| | | format: 'YYYY-MM-DD', |
| | | }, |
| | | defaultValue: [ |
| | | formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))), |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | minWidth: 200, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'orderDate', |
| | | title: 'ä¸åæ¥æ', |
| | | minWidth: 200, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | ]; |
| | | } |
| | |
| | | field: 'creceivablePrice', |
| | | title: 'æªå款éé¢', |
| | | minWidth: 200, |
| | | // æªå款éé¢ = ååæ»éé¢ - 忬¾éé¢ã忥è¿é误ç¨äºã忬¾å®æçãçå
¬å¼ï¼éé¢å䏿¾ç¤ºçæ¯ç¾åæ¯ã |
| | | formatter: ({ row }) => { |
| | | return erpCalculatePercentage( |
| | | row.receivablePrice, |
| | | row.contractPrice, |
| | | return erpNumberFormatter( |
| | | (row.contractPrice ?? 0) - (row.receivablePrice ?? 0), |
| | | 2, |
| | | ); |
| | | }, |
| | | }, |
| | |
| | | }, |
| | | { |
| | | field: 'ownerUserName', |
| | | title: 'æ¥æ', |
| | | title: 'åå·¥å§å', |
| | | minWidth: 200, |
| | | }, |
| | | { |
| | |
| | | |
| | | import { ContentWrap, Page } from '@vben/common-ui'; |
| | | import { EchartsUI, useEcharts } from '@vben/plugins/echarts'; |
| | | import { erpCalculatePercentage } from '@vben/utils'; |
| | | |
| | | import { Tabs } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getChartDatas } from '#/api/crm/statistics/customer'; |
| | | import { |
| | | getChartDatas, |
| | | getCustomerDealCycleByDate, |
| | | getCustomerSummaryByDate, |
| | | getDatas, |
| | | } from '#/api/crm/statistics/customer'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { getChartOptions } from './chartOptions'; |
| | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | gridOptions: { |
| | | columns: useGridColumns(activeTabName.value), |
| | | height: 'auto', |
| | | // height: 'auto' æ¶è¡¨æ ¼è·ç¶å®¹å¨é«åº¦èµ°ï¼æ¬é¡µå¾è¡¨å äºä¸é¢ä¸å¤§åï¼è¡¨æ ¼å°±è¢«åå¾å¾ç®ï¼ |
| | | // è¿éç»ä¸ªåºå®é«åº¦ï¼8 个页ç¾å
±ç¨è¿ä¸ä¸ªè¡¨æ ¼ï¼ä¸æ¹å
¨é«ãå«é«/å«ç®æ¹è¿ä¸å¤å³å¯ã |
| | | height: '600px', |
| | | keepSource: true, |
| | | pagerConfig: { |
| | | enabled: false, |
| | |
| | | } as VxeTableGridOptions<CrmStatisticsCustomerApi.CustomerSummaryByUserRespVO>, |
| | | }); |
| | | |
| | | /** è¶
级管çåï¼ç¨æ· ID 1ï¼ä¸æ¯ä¸å¡äººåï¼ç»è®¡è¡¨éä¸è¯¥åºç°ä»åä¸çé£è¡ */ |
| | | const ADMIN_USER_ID = 1; |
| | | |
| | | function excludeAdminRows(rows: unknown[]) { |
| | | return (rows as Array<Record<string, unknown>>).filter( |
| | | (row) => Number(row.ownerUserId) !== ADMIN_USER_ID, |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * æåå·¥ç»è®¡ç页ç¾ï¼åç«¯ä¼æé¨é¨éæ¯ä¸ªåå·¥é½åä¸è¡ï¼æ²¡æ°æ®çè¡¥ 0ï¼ï¼æç¨æ· ID ååºæ â ææ°æ®çåå·¥å¾å¾å¨æåä¸è¡ï¼ |
| | | * 600px ä¸å±åªæ¾ç¤ºåå è¡ï¼ç¿»åå å±çå°çå
¨æ¯ 0ï¼ççå°±å"æ²¡æ°æ®"ãè¿éæä¸»ææ ååºï¼ææ°æ®çæåé¢ï¼0 è¡ä¸å ã |
| | | */ |
| | | const USER_TAB_METRIC: Record<string, string[]> = { |
| | | customerSummary: ['customerDealCount', 'customerCreateCount'], |
| | | dealCycleByUser: ['customerDealCount', 'customerDealCycle'], |
| | | followUpSummary: ['followUpRecordCount', 'followUpCustomerCount'], |
| | | poolSummary: ['customerPutCount', 'customerTakeCount'], |
| | | }; |
| | | |
| | | function sortByMetric(key: string, rows: unknown[]) { |
| | | const fields = USER_TAB_METRIC[key]; |
| | | if (!fields) { |
| | | return rows; |
| | | } |
| | | return [...(rows as Array<Record<string, number>>)].sort((a, b) => { |
| | | for (const field of fields) { |
| | | const diff = (Number(b[field]) || 0) - (Number(a[field]) || 0); |
| | | if (diff !== 0) { |
| | | return diff; |
| | | } |
| | | } |
| | | return 0; |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * è·è¿æ¹å¼åæçâå æ¯(%)âåå¨ data.ts éæ¢æ²¡æ formatterï¼å端 FollowUpSummaryByTypeRespVO ä¹ä¸è¿åè¯¥åæ®µï¼ |
| | | * æä»¥æ´åæ°¸è¿æ¯ç©ºçï¼è¿éæè·è¿æ¬¡æ°å¨æ¬å°è¡¥ç®ï¼å£å¾ä¸å端å
¶å®å æ¯ä¸è´ï¼value / total * 100ï¼ä¿ç两ä½ï¼ã |
| | | */ |
| | | function fillFollowUpPortion(rows: unknown[]) { |
| | | // getDatas çè¿åç±»åæ¯å页ç¾çèåç±»åï¼è¿éæè°ç¨æ¹ä¿è¯çé¡ºåºæ¶çªæè·è¿æ¹å¼çååº |
| | | const list = rows as CrmStatisticsCustomerApi.FollowUpSummaryByTypeRespVO[]; |
| | | const total = list.reduce((sum, row) => sum + (row.followUpRecordCount ?? 0), 0); |
| | | return list.map((row) => ({ |
| | | ...row, |
| | | portion: erpCalculatePercentage(row.followUpRecordCount ?? 0, total), |
| | | })); |
| | | } |
| | | |
| | | /** tab 忢 */ |
| | | async function handleTabChange(key: any) { |
| | | activeTabName.value = key; |
| | |
| | | columns: useGridColumns(key), |
| | | }); |
| | | const queryParams = await formApi.getValues(); |
| | | const res = await getChartDatas(activeTabName.value, queryParams); |
| | | await renderEcharts(getChartOptions(activeTabName.value, res)); |
| | | await gridApi.grid.reloadData(res); |
| | | // å¾è¡¨åè¡¨æ ¼æ¯ä¸¤ä¸ªå£å¾çæ¥å£ï¼å¾è¡¨è¦"ææ¥æ"çæ¶åºæ°æ®ï¼è¡¨æ ¼è¦"æåå·¥/å°åº/产å"çæç»æ°æ®ã |
| | | // 忥åªè° getChartDatas åæå®çè¿åå¼åç»è¡¨æ ¼ï¼å¡æ¯ä¸¤è
å£å¾ä¸åçæ ç¾é¡µï¼å®¢æ·æ±æ»ãè·è¿æ±æ»ãå®¢æ·æ± ã转åç»è®¡ï¼ |
| | | // è¡¨æ ¼éå°±ä¼å¤§çç©ºåæ®µââå¾è¡¨è¿åçæ¥æ/æ°éåæ®µå¨è¡¨æ ¼åéå¹¶ä¸åå¨ã |
| | | const [chartData, tableData] = await Promise.all([ |
| | | getChartDatas(key, queryParams), |
| | | getDatas(key, queryParams), |
| | | ]); |
| | | // ãåå·¥å®¢æ·æäº¤å¨æåæãçå¾è¡¨éé¡¹è¯»çæ¯ res.customerDealCycleByDate / res.customerDealCycleByUser 两个æ°ç» |
| | | // ï¼chartOptions.ts éåªæè¿ä¸ä¸ªé¡µç¾æ¯æå¯¹è±¡åçï¼ï¼ä½ getChartDatas 对è¿ä¸ªé¡µç¾åªè¿åä¸ä¸ªæ°ç»ï¼ |
| | | // res.customerDealCycleByDate æ¯ undefined â å¨å¾è¡¨é .map ç´æ¥æéï¼åé¢çè¡¨æ ¼ reloadData æ°¸è¿ä¸ä¼æ§è¡ï¼ |
| | | // è¡¨æ ¼å°±ä¸ç´åå¨ä¸ä¸ä¸ªé¡µç¾çæ°æ®ä¸ï¼ççå°±æ¯"æäº¤å¨æ/æäº¤å®¢æ·æ°æ²¡ææ°æ®"ï¼ã |
| | | // è¿éæå¾è¡¨è¦çå½¢ç¶è¡¥é½ä¸¤ä¸ª"ææ¥æ"åºåï¼æäº¤å¨æç¨æäº¤å¨æ(ææ¥æ)æ¥å£ï¼æäº¤å®¢æ·æ°ç¨å®¢æ·æ»é(ææ¥æ)æ¥å£ï¼ |
| | | // 两个æ¥å£é½ææ¥è¯¢åºé´åæ¡¶ï¼time ä¸è´ï¼å®æµ 2026-10-08(第 41 å¨) / 2026-10-12(第 42 å¨)ï¼ã |
| | | let chartPayload: any = chartData; |
| | | if (key === 'dealCycleByUser') { |
| | | const params = queryParams as CrmStatisticsCustomerApi.CustomerSummaryReqVO; |
| | | const [dealCycleByDate, summaryByDate] = await Promise.all([ |
| | | getCustomerDealCycleByDate(params), |
| | | getCustomerSummaryByDate(params), |
| | | ]); |
| | | chartPayload = { |
| | | customerDealCycleByDate: dealCycleByDate, |
| | | customerDealCycleByUser: summaryByDate, |
| | | }; |
| | | } |
| | | // å¾è¡¨ç»ä¸åºæ¥ä¸è½è¿ç´¯è¡¨æ ¼ï¼å®¹éå
ä¸å±ï¼å¼å¸¸åªè®°æ¥å¿ |
| | | try { |
| | | await renderEcharts(getChartOptions(key, chartPayload)); |
| | | } catch (error) { |
| | | console.error('[客æ·åæ] å¾è¡¨æ¸²æå¤±è´¥', key, error); |
| | | } |
| | | const rows = sortByMetric( |
| | | key, |
| | | excludeAdminRows(Array.isArray(tableData) ? tableData : []), |
| | | ); |
| | | await gridApi.grid.reloadData( |
| | | key === 'followUpType' ? fillFollowUpPortion(rows) : rows, |
| | | ); |
| | | } |
| | | |
| | | onMounted(() => { |
| | |
| | | component: 'RangePicker', |
| | | componentProps: { |
| | | ...getRangePickerDefaultProps(), |
| | | format: 'YYYY-MM-DD', |
| | | }, |
| | | defaultValue: [ |
| | | formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))), |
| | |
| | | field: 'dealTime', |
| | | title: 'é¢è®¡æäº¤æ¥æ', |
| | | minWidth: 200, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'ownerUserName', |
| | |
| | | field: 'contactLastTime', |
| | | title: 'æåè·è¿æ¶é´', |
| | | minWidth: 200, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'updateTime', |
| | | title: 'æ´æ°æ¶é´', |
| | | minWidth: 200, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | minWidth: 200, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'creatorName', |
| | |
| | | field: 'dealTime', |
| | | title: 'é¢è®¡æäº¤æ¥æ', |
| | | minWidth: 200, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'ownerUserName', |
| | |
| | | field: 'contactLastTime', |
| | | title: 'æåè·è¿æ¶é´', |
| | | minWidth: 200, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'updateTime', |
| | | title: 'æ´æ°æ¶é´', |
| | | minWidth: 200, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | minWidth: 200, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'creatorName', |
| | |
| | | series: [getPieSeries(seriesName, data)], |
| | | }); |
| | | |
| | | /** |
| | | * ä¸å½å°å¾ï¼ECharts é»è®¤åªæå°å¾æå°å®¹å¨é«åº¦ç 80%ï¼å®¹å¨å 宽并ä¸ä¼è®©å°å¾å大ã |
| | | * ç¨ layoutCenter + layoutSize æå°å¾é¡¶å° 92%ï¼å¹¶ç¨å¾®ä¸ç§»ç»æ é¢çä½ç½®ã |
| | | */ |
| | | const getMapSeries = (name: string, data: any[]) => ({ |
| | | name, |
| | | type: 'map' as const, |
| | | map: 'china', |
| | | roam: false, |
| | | selectedMode: false, |
| | | layoutCenter: ['50%', '52%'], |
| | | layoutSize: '92%', |
| | | data, |
| | | }); |
| | | |
| | | export function getChartOptions(activeTabName: any, res: any): any { |
| | | switch (activeTabName) { |
| | | case 'area': { |
| | |
| | | max: leftMax, |
| | | }, |
| | | series: [ |
| | | { |
| | | name: '客æ·å°ååå¸', |
| | | type: 'map', |
| | | map: 'china', |
| | | roam: false, |
| | | selectedMode: false, |
| | | data: data.map((item: any) => { |
| | | getMapSeries( |
| | | '客æ·å°ååå¸', |
| | | data.map((item: any) => { |
| | | return { |
| | | name: item.areaName, |
| | | value: item.customerCount || 0, |
| | | }; |
| | | }), |
| | | }, |
| | | ), |
| | | ], |
| | | }, |
| | | right: { |
| | |
| | | max: rightMax, |
| | | }, |
| | | series: [ |
| | | { |
| | | name: '客æ·å°ååå¸', |
| | | type: 'map', |
| | | map: 'china', |
| | | roam: false, |
| | | selectedMode: false, |
| | | data: data.map((item: any) => { |
| | | getMapSeries( |
| | | '客æ·å°ååå¸', |
| | | data.map((item: any) => { |
| | | return { |
| | | name: item.areaName, |
| | | value: item.dealCount || 0, |
| | | }; |
| | | }), |
| | | }, |
| | | ), |
| | | ], |
| | | }, |
| | | }; |
| | |
| | | import { useUserStore } from '@vben/stores'; |
| | | import { beginOfDay, endOfDay, formatDateTime, handleTree } from '@vben/utils'; |
| | | |
| | | import dayjs from 'dayjs'; |
| | | |
| | | import { getSimpleDeptList } from '#/api/system/dept'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import { getRangePickerDefaultProps } from '#/utils'; |
| | |
| | | component: 'RangePicker', |
| | | componentProps: { |
| | | ...getRangePickerDefaultProps(), |
| | | format: 'YYYY-MM-DD', |
| | | }, |
| | | defaultValue: [ |
| | | formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))), |
| | | formatDateTime(endOfDay(new Date(Date.now() - 3600 * 1000 * 24))), |
| | | formatDateTime(beginOfDay(dayjs().subtract(2, 'month').toDate())), |
| | | formatDateTime(endOfDay(new Date())), |
| | | ], |
| | | }, |
| | | { |
| | |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { CrmStatisticsCustomerApi } from '#/api/crm/statistics/customer'; |
| | | |
| | | import { onMounted, ref } from 'vue'; |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | |
| | | import { ContentWrap, Page } from '@vben/common-ui'; |
| | | import { EchartsUI, useEcharts } from '@vben/plugins/echarts'; |
| | |
| | | const rightChartRef = ref<EchartsUIType>(); |
| | | const { renderEcharts: renderLeftEcharts } = useEcharts(leftChartRef); |
| | | const { renderEcharts: renderRightEcharts } = useEcharts(rightChartRef); |
| | | |
| | | /** |
| | | * ä¸¤å¼ å¾å§ç»ä¸æä¸¤ä¸ªï¼å°å¾ä¹ä¸ä¾å¤ï¼ã |
| | | * å°å¾å¤§å°ç±å®¹å¨çè¾¹ à 0.92 å³å®ï¼ECharts é»è®¤åªç»çè¾¹ç 80%ï¼å 宽容å¨ä¸ä¼æ¾å¤§å¾å½¢ï¼ |
| | | * åªæ¯å¤çç½ï¼ï¼æä»¥å°å¾åªè½é å 髿¥æ¾å¤§ââ700px æ¯è®©è¿ä¸è¡ä¸è³äºæé¡µé¢é¡¶åºå±å¹çé«åº¦ã |
| | | * 饼徿²¿ç¨åæ¥ç 300pxã |
| | | */ |
| | | const isAreaTab = computed(() => activeTabName.value === 'area'); |
| | | const chartHeight = computed(() => (isAreaTab.value ? '700px' : '300px')); |
| | | |
| | | const [QueryForm, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | |
| | | /> |
| | | </Tabs> |
| | | <div class="mt-5 flex"> |
| | | <EchartsUI class="m-4 w-1/2" ref="leftChartRef" /> |
| | | <EchartsUI class="m-4 w-1/2" ref="rightChartRef" /> |
| | | <EchartsUI |
| | | class="m-4 w-1/2" |
| | | :height="chartHeight" |
| | | ref="leftChartRef" |
| | | /> |
| | | <EchartsUI |
| | | class="m-4 w-1/2" |
| | | :height="chartHeight" |
| | | ref="rightChartRef" |
| | | /> |
| | | </div> |
| | | <Grid v-show="activeTabName !== 'area'" /> |
| | | </ContentWrap> |
| | |
| | | import { useUserStore } from '@vben/stores'; |
| | | import { beginOfDay, endOfDay, formatDateTime, handleTree } from '@vben/utils'; |
| | | |
| | | import dayjs from 'dayjs'; |
| | | |
| | | import { getSimpleDeptList } from '#/api/system/dept'; |
| | | import { getRangePickerDefaultProps } from '#/utils'; |
| | | |
| | |
| | | component: 'RangePicker', |
| | | componentProps: { |
| | | ...getRangePickerDefaultProps(), |
| | | format: 'YYYY-MM-DD', |
| | | }, |
| | | defaultValue: [ |
| | | formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))), |
| | | formatDateTime(endOfDay(new Date(Date.now() - 3600 * 1000 * 24))), |
| | | formatDateTime(beginOfDay(dayjs().subtract(2, 'month').toDate())), |
| | | formatDateTime(endOfDay(new Date())), |
| | | ], |
| | | }, |
| | | { |
| | |
| | | import type { EchartsUIType } from '@vben/plugins/echarts'; |
| | | |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { CrmStatisticsCustomerApi } from '#/api/crm/statistics/customer'; |
| | | import type { CrmStatisticsRankApi } from '#/api/crm/statistics/rank'; |
| | | |
| | | import { onMounted, ref } from 'vue'; |
| | | |
| | |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getDatas } from '#/api/crm/statistics/customer'; |
| | | import { getDatas } from '#/api/crm/statistics/rank'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { getChartOptions } from './chartOptions'; |
| | |
| | | pagerConfig: { |
| | | enabled: false, |
| | | }, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async (_, formValues) => { |
| | | const res = await getDatas(activeTabName.value, formValues); |
| | | await renderEcharts(getChartOptions(activeTabName.value, res)); |
| | | return res; |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | |
| | | toolbarConfig: { |
| | | enabled: false, |
| | | }, |
| | | } as VxeTableGridOptions<CrmStatisticsCustomerApi.CustomerSummaryByUserRespVO>, |
| | | } as VxeTableGridOptions<CrmStatisticsRankApi.RankRespVO>, |
| | | }); |
| | | |
| | | /** tab 忢 */ |
| | |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | import { isAiEnable } from '@vben/hooks'; |
| | | |
| | | import { Card, Col, Row } from 'ant-design-vue'; |
| | | import { Col, Row } from 'ant-design-vue'; |
| | | |
| | | import { getHomeSummary } from '#/api/mes/home'; |
| | | |
| | |
| | | import KpiCards from './modules/kpi-cards.vue'; |
| | | import ProductionTrend from './modules/production-trend.vue'; |
| | | import QuickActions from './modules/quick-actions.vue'; |
| | | import { useHomeAccess } from './use-home-access'; |
| | | |
| | | defineOptions({ name: 'DashboardAnalytics' }); |
| | | |
| | | const router = useRouter(); |
| | | const summary = ref(defaultSummary); |
| | | const { canNavigate, canViewHome } = useHomeAccess(); |
| | | |
| | | const greeting = computed(() => { |
| | | const hour = new Date().getHours(); |
| | |
| | | }); |
| | | |
| | | function handleNavigate(name: string) { |
| | | if (!canNavigate(name)) return; |
| | | router.push({ name }); |
| | | } |
| | | |
| | | async function loadSummary() { |
| | | if (!canViewHome.value) return; |
| | | summary.value = await getHomeSummary(); |
| | | } |
| | | |
| | |
| | | </div> |
| | | |
| | | <!-- MES ç产 KPI --> |
| | | <KpiCards :summary="summary" @navigate="handleNavigate" /> |
| | | <KpiCards v-if="canViewHome" :summary="summary" @navigate="handleNavigate" /> |
| | | |
| | | <!-- ERP/WMS/BPM ç»è¥ KPI --> |
| | | <BizCards @navigate="handleNavigate" /> |
| | | |
| | | <!-- AI æºè½åæ --> |
| | | <AiInsights |
| | | v-if="canViewHome && canNavigate('MesProWorkOrder') && isAiEnable()" |
| | | :active-order-count="summary.workOrderActiveCount" |
| | | @navigate="handleNavigate" |
| | | /> |
| | | |
| | | <!-- ç产è¶å¿ + é¢è¦é¢æ¿ --> |
| | | <Row :gutter="16" class="mb-4"> |
| | | <Row v-if="canViewHome" :gutter="16" class="mb-4"> |
| | | <Col :lg="14" :md="24" :sm="24" :xl="14" :xs="24" class="mb-4"> |
| | | <div class="glass-card rounded-2xl p-5"> |
| | | <h3 class="mb-4 text-base font-semibold text-gray-800 dark:text-gray-100"> |
| | |
| | | |
| | | import { getWorkOrderStatusDistribution } from '#/api/mes/home'; |
| | | |
| | | import { useHomeAccess } from '../use-home-access'; |
| | | |
| | | defineOptions({ name: 'DashboardAlertPanel' }); |
| | | |
| | | const props = defineProps<{ |
| | |
| | | const chartRef = ref<EchartsUIType>(); |
| | | const { renderEcharts } = useEcharts(chartRef); |
| | | const loading = ref(false); |
| | | const { canNavigate, canViewHome } = useHomeAccess(); |
| | | |
| | | async function loadStatus() { |
| | | if (!canViewHome.value) return; |
| | | loading.value = true; |
| | | try { |
| | | const data = await getWorkOrderStatusDistribution(); |
| | |
| | | <!-- é¢è¦å¡çè¡ --> |
| | | <div class="grid grid-cols-1 gap-3"> |
| | | <div |
| | | v-if="canNavigate('MesProRepair')" |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0 p-4" |
| | | @click="emit('navigate', 'MesProRepair')" |
| | | > |
| | |
| | | <script lang="ts" setup> |
| | | import { onMounted, ref } from 'vue'; |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | |
| | | import { CountTo } from '@vben/common-ui'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | |
| | | import { getMaterialStockPage } from '#/api/mes/wm/materialstock'; |
| | | import { getTaskTodoPage } from '#/api/bpm/task'; |
| | | |
| | | import { useHomeAccess } from '../use-home-access'; |
| | | |
| | | defineOptions({ name: 'DashboardBizCards' }); |
| | | |
| | | const emit = defineEmits<{ |
| | |
| | | const saleCount = ref(0); |
| | | const stockCount = ref(0); |
| | | const bpmTodoCount = ref(0); |
| | | const { canNavigate, canQuery } = useHomeAccess(); |
| | | const canViewPurchase = computed(() => canQuery('erp:purchase-order:query') && canNavigate('ErpPurchaseOrder')); |
| | | const canViewSale = computed(() => canQuery('erp:sale-order:query') && canNavigate('ErpSaleOrder')); |
| | | const canViewStock = computed(() => canQuery('mes:wm-material-stock:query') && canNavigate('MesWmMaterialStock')); |
| | | const canViewBpmTodo = computed(() => canQuery('bpm:task:query') && canNavigate('BpmTaskTodo')); |
| | | |
| | | async function loadPurchase() { |
| | | if (!canViewPurchase.value) return; |
| | | try { |
| | | const result = await getPurchaseOrderPage({ pageNo: 1, pageSize: 1 }); |
| | | purchaseCount.value = result.total ?? 0; |
| | |
| | | } |
| | | |
| | | async function loadSale() { |
| | | if (!canViewSale.value) return; |
| | | try { |
| | | const result = await getSaleOrderPage({ pageNo: 1, pageSize: 1 }); |
| | | saleCount.value = result.total ?? 0; |
| | |
| | | } |
| | | |
| | | async function loadStock() { |
| | | if (!canViewStock.value) return; |
| | | try { |
| | | const result = await getMaterialStockPage({ pageNo: 1, pageSize: 1 }); |
| | | stockCount.value = result.total ?? 0; |
| | |
| | | } |
| | | |
| | | async function loadBpmTodo() { |
| | | if (!canViewBpmTodo.value) return; |
| | | try { |
| | | const result = await getTaskTodoPage({ pageNo: 1, pageSize: 1 }); |
| | | bpmTodoCount.value = result.total ?? 0; |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <Row :gutter="16"> |
| | | <Row v-if="canViewPurchase || canViewSale || canViewStock || canViewBpmTodo" :gutter="16"> |
| | | <!-- éè´è®¢å --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col v-if="canViewPurchase" :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0" |
| | | :style="tiltStyle('purchase')" |
| | |
| | | </Col> |
| | | |
| | | <!-- éå®è®¢å --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col v-if="canViewSale" :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0" |
| | | :style="tiltStyle('sale')" |
| | |
| | | </Col> |
| | | |
| | | <!-- ç©æåºå --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col v-if="canViewStock" :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0" |
| | | :style="tiltStyle('stock')" |
| | |
| | | </Col> |
| | | |
| | | <!-- BPM å¾
å --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col v-if="canViewBpmTodo" :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0" |
| | | :style="tiltStyle('bpm')" |
| | |
| | | |
| | | import { Col, Row } from 'ant-design-vue'; |
| | | |
| | | import { useHomeAccess } from '../use-home-access'; |
| | | |
| | | defineOptions({ name: 'DashboardKpiCards' }); |
| | | |
| | | const { canNavigate } = useHomeAccess(); |
| | | |
| | | const props = defineProps<{ |
| | | summary: MesHomeApi.Summary; |
| | |
| | | navigate: [name: string]; |
| | | }>(); |
| | | |
| | | const qualityRate = computed(() => { |
| | | const total = |
| | | props.summary.todayQualifiedQuantity + |
| | | props.summary.todayUnqualifiedQuantity; |
| | | return total === 0 ? 0 : (props.summary.todayQualifiedQuantity / total) * 100; |
| | | }); |
| | | /** |
| | | * è´¨éåæ ¼çï¼æ¥æ / è¿ç¨ / åºè´§ä¸ä¸ªæ£éªç¯èï¼åèªä¸å¼ å¡ç |
| | | * |
| | | * ç±å端æãåæ ¼æ°é / æ£éªæ°éã计ç®ï¼ä¸è¿æ»¤åæ®ç¶æï¼æ æ¶é´éå¶ï¼ï¼ |
| | | * 该ç¯èæ²¡ææ£éªæ°æ®æ¶å端è¿å nullã |
| | | */ |
| | | const qualityRates = computed(() => [ |
| | | { |
| | | key: 'quality-iqc', |
| | | label: 'æ¥æåæ ¼ç', |
| | | rate: props.summary.iqcQualifiedRate, |
| | | checkQuantity: props.summary.iqcCheckQuantity, |
| | | routeName: 'MesQcIqc', |
| | | icon: 'lucide:package-check', |
| | | iconClass: 'bg-gradient-to-br from-orange-500 to-amber-400 shadow-lg shadow-orange-500/25', |
| | | textClass: 'text-orange-600 dark:text-orange-400', |
| | | washClass: 'bg-gradient-to-br from-orange-500/5 to-amber-500/5', |
| | | glowClass: 'bg-orange-400/10', |
| | | }, |
| | | { |
| | | key: 'quality-ipqc', |
| | | label: 'è¿ç¨åæ ¼ç', |
| | | rate: props.summary.ipqcQualifiedRate, |
| | | checkQuantity: props.summary.ipqcCheckQuantity, |
| | | routeName: 'MesQcIpqc', |
| | | icon: 'lucide:workflow', |
| | | iconClass: 'bg-gradient-to-br from-cyan-500 to-teal-400 shadow-lg shadow-cyan-500/25', |
| | | textClass: 'text-cyan-600 dark:text-cyan-400', |
| | | washClass: 'bg-gradient-to-br from-cyan-500/5 to-teal-500/5', |
| | | glowClass: 'bg-cyan-400/10', |
| | | }, |
| | | { |
| | | key: 'quality-oqc', |
| | | label: 'åºè´§åæ ¼ç', |
| | | rate: props.summary.oqcQualifiedRate, |
| | | checkQuantity: props.summary.oqcCheckQuantity, |
| | | routeName: 'MesQcOqc', |
| | | icon: 'lucide:truck', |
| | | iconClass: 'bg-gradient-to-br from-pink-500 to-rose-400 shadow-lg shadow-pink-500/25', |
| | | textClass: 'text-pink-600 dark:text-pink-400', |
| | | washClass: 'bg-gradient-to-br from-pink-500/5 to-rose-500/5', |
| | | glowClass: 'bg-pink-400/10', |
| | | }, |
| | | ]); |
| | | |
| | | /** åæ ¼çå±ç¤ºææ¡ï¼ä¸ä½å°æ°ï¼ä¸åãè´¨éåæ ¼çãå¡çä¸è´ï¼ï¼æ æ°æ®æ¶ç¨ãâãè䏿¯ 0% */ |
| | | function formatRate(rate: null | number | undefined) { |
| | | return rate === null || rate === undefined ? 'â' : rate.toFixed(1); |
| | | } |
| | | |
| | | /** æ æ£éªæ°æ®æ¶ä¸æ¸²æç¾åå·ï¼é¿å
åºç°ãâ%ã */ |
| | | function hasRate(rate: null | number | undefined) { |
| | | return rate !== null && rate !== undefined; |
| | | } |
| | | |
| | | const equipmentRate = computed(() => { |
| | | if (props.summary.machineryTotal === 0) return 0; |
| | |
| | | <template> |
| | | <Row :gutter="16"> |
| | | <!-- ç产订å --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="4" :xs="24" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0" |
| | | class="glass-tilt group h-full overflow-hidden rounded-2xl border-0" |
| | | :class="{ 'cursor-pointer': canNavigate('MesProWorkOrder') }" |
| | | :style="tiltStyle('order')" |
| | | @mousemove="(e: MouseEvent) => onMouseMove(e, 'order')" |
| | | @mouseleave="onMouseLeave('order')" |
| | | @click="emit('navigate', 'MesProWorkOrder')" |
| | | @click="canNavigate('MesProWorkOrder') && emit('navigate', 'MesProWorkOrder')" |
| | | > |
| | | <div class="relative z-10 flex items-center gap-4 p-5"> |
| | | <div class="flex size-12 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-blue-500 to-cyan-400 shadow-lg shadow-blue-500/25 transition-transform duration-300 group-hover:scale-110"> |
| | |
| | | </Col> |
| | | |
| | | <!-- 仿¥äº§é --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="4" :xs="24" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0" |
| | | class="glass-tilt group h-full overflow-hidden rounded-2xl border-0" |
| | | :class="{ 'cursor-pointer': canNavigate('MesProFeedback') }" |
| | | :style="tiltStyle('output')" |
| | | @mousemove="(e: MouseEvent) => onMouseMove(e, 'output')" |
| | | @mouseleave="onMouseLeave('output')" |
| | | @click="emit('navigate', 'MesProFeedback')" |
| | | @click="canNavigate('MesProFeedback') && emit('navigate', 'MesProFeedback')" |
| | | > |
| | | <div class="relative z-10 flex items-center gap-4 p-5"> |
| | | <div class="flex size-12 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-emerald-500 to-green-400 shadow-lg shadow-emerald-500/25 transition-transform duration-300 group-hover:scale-110"> |
| | |
| | | </div> |
| | | </Col> |
| | | |
| | | <!-- è´¨éåæ ¼ç --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <!-- è´¨éåæ ¼çï¼æ¥æ / è¿ç¨ / åºè´§ï¼åä¸å¼ å¡ç --> |
| | | <Col |
| | | v-for="item in qualityRates" |
| | | :key="item.key" |
| | | :lg="6" |
| | | :md="12" |
| | | :sm="12" |
| | | :xl="4" |
| | | :xs="24" |
| | | class="mb-4" |
| | | > |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0" |
| | | :style="tiltStyle('quality')" |
| | | @mousemove="(e: MouseEvent) => onMouseMove(e, 'quality')" |
| | | @mouseleave="onMouseLeave('quality')" |
| | | @click="emit('navigate', 'MesQcRecord')" |
| | | class="glass-tilt group h-full overflow-hidden rounded-2xl border-0" |
| | | :class="{ 'cursor-pointer': canNavigate(item.routeName) }" |
| | | :style="tiltStyle(item.key)" |
| | | @mousemove="(e: MouseEvent) => onMouseMove(e, item.key)" |
| | | @mouseleave="onMouseLeave(item.key)" |
| | | @click="canNavigate(item.routeName) && emit('navigate', item.routeName)" |
| | | > |
| | | <div class="relative z-10 flex items-center gap-4 p-5"> |
| | | <div class="flex size-12 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-orange-500 to-amber-400 shadow-lg shadow-orange-500/25 transition-transform duration-300 group-hover:scale-110"> |
| | | <IconifyIcon class="size-6 text-white" icon="lucide:shield-check" /> |
| | | <div |
| | | class="flex size-12 flex-shrink-0 items-center justify-center rounded-xl transition-transform duration-300 group-hover:scale-110" |
| | | :class="item.iconClass" |
| | | > |
| | | <IconifyIcon class="size-6 text-white" :icon="item.icon" /> |
| | | </div> |
| | | <div class="min-w-0 flex-1"> |
| | | <div class="mb-1 text-xs font-medium text-gray-500 dark:text-gray-400">è´¨éåæ ¼ç</div> |
| | | <div class="mb-1 text-xs font-medium text-gray-500 dark:text-gray-400">{{ item.label }}</div> |
| | | <div class="flex items-baseline gap-1"> |
| | | <CountTo |
| | | class="text-2xl font-bold leading-tight" |
| | | :class="qualityRate >= 95 ? 'text-emerald-600 dark:text-emerald-400' : qualityRate >= 80 ? 'text-orange-600 dark:text-orange-400' : 'text-red-600 dark:text-red-400'" |
| | | :decimals="1" :end-val="qualityRate" :duration="1200" |
| | | /> |
| | | <span class="text-xs text-gray-400">%</span> |
| | | <span class="text-2xl font-bold leading-tight" :class="item.textClass"> |
| | | {{ formatRate(item.rate) }} |
| | | </span> |
| | | <span v-if="hasRate(item.rate)" class="text-xs text-gray-400">%</span> |
| | | </div> |
| | | <div class="mt-1 text-xs text-gray-400"> |
| | | åæ ¼ {{ summary.todayQualifiedQuantity }} | ä¸è¯ {{ summary.todayUnqualifiedQuantity }} |
| | | <div class="mt-1 flex items-center gap-1 text-xs text-gray-400"> |
| | | <span>æ£éª {{ item.checkQuantity }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="absolute inset-0 -z-0 bg-gradient-to-br from-orange-500/5 to-amber-500/5 opacity-0 transition-opacity duration-300 group-hover:opacity-100" /> |
| | | <div class="absolute -right-6 -top-6 size-24 rounded-full bg-orange-400/10 blur-xl transition-all duration-300 group-hover:scale-150" /> |
| | | <div |
| | | class="absolute inset-0 -z-0 opacity-0 transition-opacity duration-300 group-hover:opacity-100" |
| | | :class="item.washClass" |
| | | /> |
| | | <div |
| | | class="absolute -right-6 -top-6 size-24 rounded-full blur-xl transition-all duration-300 group-hover:scale-150" |
| | | :class="item.glowClass" |
| | | /> |
| | | </div> |
| | | </Col> |
| | | |
| | | <!-- 设å¤è¿è¡ç --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="4" :xs="24" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0" |
| | | class="glass-tilt group h-full overflow-hidden rounded-2xl border-0" |
| | | :class="{ 'cursor-pointer': canNavigate('MesDvMachinery') }" |
| | | :style="tiltStyle('equip')" |
| | | @mousemove="(e: MouseEvent) => onMouseMove(e, 'equip')" |
| | | @mouseleave="onMouseLeave('equip')" |
| | | @click="emit('navigate', 'MesDvMachinery')" |
| | | @click="canNavigate('MesDvMachinery') && emit('navigate', 'MesDvMachinery')" |
| | | > |
| | | <div class="relative z-10 flex items-center gap-4 p-5"> |
| | | <div class="flex size-12 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-violet-500 to-purple-400 shadow-lg shadow-violet-500/25 transition-transform duration-300 group-hover:scale-110"> |
| | |
| | | import type { MesHomeApi } from '#/api/mes/home'; |
| | | import type { EchartsUIType } from '@vben/plugins/echarts'; |
| | | |
| | | import { onMounted, ref, watch } from 'vue'; |
| | | import { onMounted, ref } from 'vue'; |
| | | |
| | | import { EchartsUI, useEcharts } from '@vben/plugins/echarts'; |
| | | |
| | | import { getProductionTrend } from '#/api/mes/home'; |
| | | |
| | | import { useHomeAccess } from '../use-home-access'; |
| | | |
| | | defineOptions({ name: 'DashboardProductionTrend' }); |
| | | |
| | | const chartRef = ref<EchartsUIType>(); |
| | | const { renderEcharts } = useEcharts(chartRef); |
| | | const { canViewHome } = useHomeAccess(); |
| | | |
| | | async function loadTrend() { |
| | | if (!canViewHome.value) return; |
| | | const data = await getProductionTrend(7); |
| | | const dates = data.map((d: MesHomeApi.ProductionTrend) => d.date.slice(5)); |
| | | const quantities = data.map((d: MesHomeApi.ProductionTrend) => d.quantity); |
| | |
| | | <script lang="ts" setup> |
| | | import { computed } from 'vue'; |
| | | |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { useHomeAccess } from '../use-home-access'; |
| | | |
| | | defineOptions({ name: 'DashboardQuickActions' }); |
| | | |
| | |
| | | { title: 'å·¥èºè·¯çº¿', icon: 'lucide:route', route: 'MesPdRouting', gradient: 'from-teal-500 to-emerald-500', shadow: 'shadow-teal-500/15' }, |
| | | { title: 'æ°æ®æ¥è¡¨', icon: 'lucide:bar-chart-big', route: 'MesHomeIndex', gradient: 'from-pink-500 to-rose-500', shadow: 'shadow-pink-500/15' }, |
| | | ]; |
| | | |
| | | const { canNavigate } = useHomeAccess(); |
| | | const accessibleActions = computed(() => actions.filter((action) => canNavigate(action.route))); |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="glass-card rounded-2xl p-5"> |
| | | <div v-if="accessibleActions.length" class="glass-card rounded-2xl p-5"> |
| | | <h3 class="mb-4 text-sm font-semibold text-gray-800 dark:text-gray-100">å¿«æ·æä½</h3> |
| | | <div class="grid grid-cols-4 gap-3 sm:grid-cols-8"> |
| | | <div |
| | | v-for="action in actions" |
| | | v-for="action in accessibleActions" |
| | | :key="action.route" |
| | | class="group flex cursor-pointer flex-col items-center gap-2 rounded-xl p-3 transition-all duration-300 hover:-translate-y-1" |
| | | @click="emit('navigate', action.route)" |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { RouteRecordRaw } from 'vue-router'; |
| | | |
| | | import { computed } from 'vue'; |
| | | |
| | | import { useAccess } from '@vben/access'; |
| | | import { useAccessStore } from '@vben/stores'; |
| | | |
| | | /** é¦é¡µå¸¸é©»ï¼ä¸å¡æ°æ®åå
¥å£ä»ééµå¾ªå½åç¨æ·çèåæéã */ |
| | | export function useHomeAccess() { |
| | | const accessStore = useAccessStore(); |
| | | const { hasAccessByCodes } = useAccess(); |
| | | |
| | | function canQuery(permission: string) { |
| | | return hasAccessByCodes([permission, '*:*:*']); |
| | | } |
| | | |
| | | function canNavigate(name: string) { |
| | | function contains(routes: RouteRecordRaw[]): boolean { |
| | | return routes.some((route) => { |
| | | if (route.meta?.menuVisibleWithForbidden) return false; |
| | | return route.name === name || contains(route.children ?? []); |
| | | }); |
| | | } |
| | | return contains(accessStore.accessRoutes); |
| | | } |
| | | |
| | | const canViewHome = computed(() => canQuery('mes:home:query')); |
| | | |
| | | return { canNavigate, canQuery, canViewHome }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup lang="ts"> |
| | | import type { FormInstance, TablePaginationConfig } from 'ant-design-vue'; |
| | | import type { EnergyAlertRule, EnergyAlertLog, EnergyType } from '#/api/energy'; |
| | | import { computed, onMounted, reactive, ref } from 'vue'; |
| | | import { Page } from '@vben/common-ui'; |
| | | import { Alert, Button, Card, DatePicker, Form, FormItem, Input, InputNumber, Modal, Popconfirm, Select, Space, Switch, Table, Tabs, TabPane, Tag, message } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | | import { checkEnergyAlerts, deleteEnergyAlertRule, getEnergyAlertLogs, getEnergyAlertRecipients, getEnergyAlertRules, saveEnergyAlertRule } from '#/api/energy'; |
| | | import { energyInfo, energyOptions, numberText, productOptions } from '../shared'; |
| | | |
| | | defineOptions({ name: 'EnergyAlert' }); |
| | | const tab = ref('rules'); |
| | | const rules = ref<EnergyAlertRule[]>([]); |
| | | const logs = ref<EnergyAlertLog[]>([]); |
| | | const recipients = ref<{ label: string; value: number }[]>([]); |
| | | const loading = ref(false), error = ref(false), saving = ref(false), checking = ref(false), open = ref(false); |
| | | const rulePage = reactive({ current: 1, pageSize: 10, total: 0, showSizeChanger: true }); |
| | | const logPage = reactive({ current: 1, pageSize: 10, total: 0, showSizeChanger: true }); |
| | | const logFilters = reactive<{ date?: string; status?: string }>({}); |
| | | const checkDate = ref(dayjs().format('YYYY-MM-DD')); |
| | | const initial = (): EnergyAlertRule => ({ name: '', energyType: 'WATER', metric: 'QUANTITY', threshold: 1, enabled: true, recipientIds: [], usageArea: '', productCategory: undefined }); |
| | | const model = reactive<EnergyAlertRule>(initial()); |
| | | const metricOptions = [ |
| | | { label: 'æ¯æ¥ç¨é', value: 'QUANTITY' }, { label: 'æ¯æ¥è´¹ç¨', value: 'AMOUNT' }, |
| | | { label: 'æ¯æç¨é', value: 'MONTH_QUANTITY' }, { label: 'æ¯æè´¹ç¨', value: 'MONTH_AMOUNT' }, |
| | | ]; |
| | | const metricLabel = (metric: string) => metricOptions.find(option => option.value === metric)?.label ?? metric; |
| | | const isAmount = (metric: string) => metric.endsWith('AMOUNT'); |
| | | const formRef = ref<FormInstance>(); |
| | | const unit = computed(() => isAmount(model.metric) ? 'å
' : energyInfo[model.energyType].unit); |
| | | const validation = { |
| | | name: [{ required: true, whitespace: true, message: '请è¾å
¥è§ååç§°' }], |
| | | threshold: [{ required: true, type: 'number' as const, min: 0.001, message: 'éå¼å¿
须大äºé¶' }], |
| | | recipientIds: [{ required: true, type: 'array' as const, min: 1, max: 50, message: 'è¯·éæ© 1 è³ 50 使¥æ¶äºº' }], |
| | | }; |
| | | const ruleColumns = [ |
| | | { title: 'è§ååç§°', dataIndex: 'name', width: 180 }, { title: 'è½æº', dataIndex: 'energyType', width: 80 }, |
| | | { title: 'ç»è®¡èå´', key: 'scope', width: 200 }, { title: 'ç»è®¡ææ / éå¼', key: 'threshold', width: 190 }, |
| | | { title: 'æ¥æ¶äºº', key: 'recipients', width: 180 }, { title: 'ç¶æ', key: 'enabled', width: 80 }, |
| | | { title: 'æä½', key: 'action', width: 140, fixed: 'right' as const }, |
| | | ]; |
| | | const logColumns = [ |
| | | { title: 'ç»è®¡å¨æ', key: 'period', width: 120 }, { title: 'è§ååç§°', dataIndex: 'ruleName', width: 160 }, |
| | | { title: 'è½æº / èå´', key: 'scope', width: 230 }, { title: 'å®é
å¼ / éå¼', key: 'value', width: 210 }, |
| | | { title: 'æ¥æ¶äºº', dataIndex: 'recipientName', width: 120 }, { title: 'ç»æ', key: 'status', width: 100 }, |
| | | { title: '失败åå ', dataIndex: 'errorMessage', width: 300 }, |
| | | ]; |
| | | let loadId = 0; |
| | | async function load() { |
| | | const id = ++loadId; loading.value = true; error.value = false; |
| | | try { |
| | | if (tab.value === 'rules') { |
| | | const result = await getEnergyAlertRules({ pageNo: rulePage.current, pageSize: rulePage.pageSize }); |
| | | if (id === loadId) { rules.value = result.list; rulePage.total = result.total; } |
| | | } else { |
| | | const result = await getEnergyAlertLogs({ ...logFilters, pageNo: logPage.current, pageSize: logPage.pageSize }); |
| | | if (id === loadId) { logs.value = result.list; logPage.total = result.total; } |
| | | } |
| | | } catch { if (id === loadId) error.value = true; } |
| | | finally { if (id === loadId) loading.value = false; } |
| | | } |
| | | function changePage(page: TablePaginationConfig) { |
| | | Object.assign(tab.value === 'rules' ? rulePage : logPage, { current: page.current ?? 1, pageSize: page.pageSize ?? 10 }); void load(); |
| | | } |
| | | async function showForm(row?: EnergyAlertRule) { |
| | | try { recipients.value = (await getEnergyAlertRecipients()).map(user => ({ label: user.nickname, value: user.id })); } |
| | | catch { message.error('æ æ³å è½½æ¥æ¶äººï¼è¯·éè¯'); return; } |
| | | Object.keys(model).forEach(key => delete (model as unknown as Record<string, unknown>)[key]); |
| | | Object.assign(model, row ? { ...row, recipientIds: [...row.recipientIds] } : initial()); |
| | | open.value = true; formRef.value?.clearValidate(); |
| | | } |
| | | async function save() { |
| | | await formRef.value?.validate(); saving.value = true; |
| | | try { await saveEnergyAlertRule({ ...model }); open.value = false; message.success('è§åå·²ä¿åï¼åç»å½å
¥èªå¨æ£æ¥ï¼å岿¥æå¯æå¨æ£æ¥'); await load(); } |
| | | finally { saving.value = false; } |
| | | } |
| | | async function remove(id: number) { await deleteEnergyAlertRule(id); message.success('å·²å é¤ï¼åå²éç¥è®°å½ä¿ç'); rulePage.current = 1; await load(); } |
| | | async function check() { |
| | | if (!checkDate.value || checkDate.value > dayjs().format('YYYY-MM-DD')) { message.warning('è¯·éæ©ä»å¤©æä¹åçæ¥æ'); return; } |
| | | checking.value = true; |
| | | try { |
| | | const result = await checkEnergyAlerts(checkDate.value); |
| | | message[result.failed ? 'warning' : 'success'](`æ£æ¥å®æï¼æ°åé ${result.sent} æ¡ï¼å¤±è´¥ ${result.failed} æ¡ãå·²åéçéç¥ä¸ä¼éå¤åéã`); |
| | | tab.value = 'logs'; logFilters.date = checkDate.value; logPage.current = 1; await load(); |
| | | } finally { checking.value = false; } |
| | | } |
| | | function recipientNames(ids: number[]) { return ids.map(id => recipients.value.find(user => user.value === id)?.label ?? `ç¨æ· ${id}`).join('ã'); } |
| | | onMounted(async () => { |
| | | await load(); |
| | | try { recipients.value = (await getEnergyAlertRecipients()).map(user => ({ label: user.nickname, value: user.id })); } catch { /* Rules remain viewable when recipient lookup fails. */ } |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page title="è½èé¢è¦" description="é
ç½®æ¯æ¥ææ¯æç¨éãè´¹ç¨éå¼ï¼è¶
è¿é弿¶åæå®äººååéç«å
éç¥ã"> |
| | | <Alert class="mb-4" type="info" show-icon message="æ¯æ¥ææ æå¤©æ±æ»ï¼æ¯æææ æèªç¶ææ±æ»æ¬æå·²å½å
¥æ°æ®ï¼è¶
éå³éç¥ãåä¸è§åãç»è®¡å¨æåæ¥æ¶äººåªæåéç¥ä¸æ¬¡ãæ£æ¥æ¥æåæ¶æ£æ¥å½å¤©åæå±æä»½ãåºåæå称精确å¹é
ã" /> |
| | | <Card> |
| | | <div class="mb-4 flex flex-wrap items-center justify-between gap-3"> |
| | | <Button v-access:code="['energy:alert:create']" type="primary" @click="showForm()">æ°å¢é¢è¦è§å</Button> |
| | | <Space wrap><span>æ£æ¥æ¥æ</span><DatePicker v-model:value="checkDate" value-format="YYYY-MM-DD" :allow-clear="false" /><Button v-access:code="['energy:alert:check']" :loading="checking" @click="check">æ£æ¥é¢è¦ / éè¯å¤±è´¥éç¥</Button></Space> |
| | | </div> |
| | | <Alert v-if="error" class="mb-4" type="error" show-icon message="å 载失败ï¼è¯·ç¹å»å·æ°éè¯ã" /> |
| | | <Tabs v-model:active-key="tab" @change="load"> |
| | | <TabPane key="rules" tab="é¢è¦è§å"> |
| | | <Table :columns="ruleColumns" :data-source="rules" row-key="id" :loading="loading" :pagination="rulePage" :scroll="{ x: 1100 }" :locale="{ emptyText: 'ææ è§åï¼è¯·å
设置éå¼åéç¥æ¥æ¶äºº' }" @change="changePage"> |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.dataIndex === 'energyType'">{{ energyInfo[record.energyType as EnergyType].name }}</template> |
| | | <template v-else-if="column.key === 'scope'">{{ record.usageArea || 'å
¨é¨åºå' }} / {{ record.productCategory || 'å
¨é¨äº§å' }}</template> |
| | | <template v-else-if="column.key === 'threshold'">{{ metricLabel(record.metric) }} > {{ numberText(record.threshold) }} {{ isAmount(record.metric) ? 'å
' : energyInfo[record.energyType as EnergyType].unit }}</template> |
| | | <template v-else-if="column.key === 'recipients'">{{ recipientNames(record.recipientIds) }}</template> |
| | | <template v-else-if="column.key === 'enabled'"><Tag :color="record.enabled ? 'green' : 'default'">{{ record.enabled ? 'å¯ç¨' : 'åç¨' }}</Tag></template> |
| | | <template v-else-if="column.key === 'action'"><Space :size="0"><Button v-access:code="['energy:alert:update']" type="link" @click="showForm(record as EnergyAlertRule)">ç¼è¾</Button><Popconfirm title="å 餿¤è§åï¼åå²éç¥ä»ä¼ä¿çã" @confirm="remove(record.id)"><Button v-access:code="['energy:alert:delete']" type="link" danger>å é¤</Button></Popconfirm></Space></template> |
| | | </template> |
| | | </Table> |
| | | </TabPane> |
| | | <TabPane key="logs" tab="éç¥è®°å½"> |
| | | <Space class="mb-4" wrap><DatePicker v-model:value="logFilters.date" value-format="YYYY-MM-DD" placeholder="ç»è®¡æ¥æ" /><Select v-model:value="logFilters.status" :options="[{ label: 'åéæå', value: 'SENT' }, { label: 'åé失败', value: 'FAILED' }]" allow-clear placeholder="å
¨é¨ç»æ" style="width: 140px" /><Button @click="logPage.current = 1; load()">æ¥è¯¢</Button></Space> |
| | | <Table :columns="logColumns" :data-source="logs" row-key="id" :loading="loading" :pagination="logPage" :scroll="{ x: 1340 }" :locale="{ emptyText: 'ææ é¢è¦éç¥è®°å½' }" @change="changePage"> |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.key === 'period'">{{ record.periodType === 'MONTH' ? record.recordDate.slice(0, 7) : record.recordDate }}<div class="text-xs text-muted-foreground">{{ metricLabel(record.metric) }}</div></template> |
| | | <template v-else-if="column.key === 'scope'">{{ energyInfo[record.energyType as EnergyType].name }} / {{ record.scope }}</template> |
| | | <template v-else-if="column.key === 'value'">{{ numberText(record.actualValue) }} / {{ numberText(record.threshold) }} {{ isAmount(record.metric) ? 'å
' : energyInfo[record.energyType as EnergyType].unit }}</template> |
| | | <template v-else-if="column.key === 'status'"><Tag :color="record.status === 'SENT' ? 'green' : 'red'">{{ record.status === 'SENT' ? 'åéæå' : 'åé失败' }}</Tag></template> |
| | | </template> |
| | | </Table> |
| | | </TabPane> |
| | | <template #rightExtra><Button :loading="loading" @click="load">å·æ°</Button></template> |
| | | </Tabs> |
| | | </Card> |
| | | <Modal v-model:open="open" :title="model.id ? 'ç¼è¾é¢è¦è§å' : 'æ°å¢é¢è¦è§å'" :confirm-loading="saving" :mask-closable="false" :width="640" @ok="save"> |
| | | <Form ref="formRef" :model="model" :rules="validation" layout="vertical" class="mt-4"> |
| | | <FormItem label="è§ååç§°" name="name"><Input v-model:value="model.name" :maxlength="100" placeholder="ä¾å¦ï¼ççªçäº§åºæ¯æ¥ç¨æ°´é¢è¦" /></FormItem> |
| | | <div class="grid grid-cols-1 gap-x-4 sm:grid-cols-2"> |
| | | <FormItem label="è½æº"><Select v-model:value="model.energyType" :options="energyOptions" /></FormItem> |
| | | <FormItem label="ç»è®¡ææ "><Select v-model:value="model.metric" :options="metricOptions" /></FormItem> |
| | | <FormItem label="使ç¨åºåï¼ç²¾ç¡®å¹é
ï¼å¯ç空ï¼"><Input v-model:value="model.usageArea" :maxlength="100" placeholder="ç空ç»è®¡å
¨é¨åºå" /></FormItem> |
| | | <FormItem label="产åç±»å«"><Select v-model:value="model.productCategory" :options="productOptions" allow-clear placeholder="å
¨é¨äº§å" /></FormItem> |
| | | </div> |
| | | <FormItem :label="`${model.metric.startsWith('MONTH_') ? 'æ¯æ' : 'æ¯æ¥'}éå¼ï¼${unit}ï¼å¤§äºæ¤å¼æ¶éç¥ï¼`" name="threshold"><InputNumber v-model:value="model.threshold" :min="0.001" :precision="3" style="width: 100%" /></FormItem> |
| | | <FormItem label="éç¥æ¥æ¶äºº" name="recipientIds"><Select v-model:value="model.recipientIds" mode="multiple" :options="recipients" option-filter-prop="label" placeholder="éæ©æ¬ç«å
ç¨æ·" /></FormItem> |
| | | <FormItem label="å¯ç¨è§å"><Switch v-model:checked="model.enabled" /></FormItem> |
| | | </Form> |
| | | </Modal> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup lang="ts"> |
| | | import type { EchartsUIType } from '@vben/plugins/echarts'; |
| | | import type { EnergyAnalysis, EnergyFilters, EnergyType } from '#/api/energy'; |
| | | import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'; |
| | | import { Page } from '@vben/common-ui'; |
| | | import { EchartsUI, useEcharts } from '@vben/plugins/echarts'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | import { Alert, Button, Card, DatePicker, Empty, Form, FormItem, Input, RadioGroup, Select, Space, Spin, Table, message } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | | import { exportEnergyAnalysis, getEnergyAnalysis } from '#/api/energy'; |
| | | import { energyInfo, energyOptions, numberText, productOptions } from '../shared'; |
| | | |
| | | defineOptions({ name: 'EnergyAnalysis' }); |
| | | const defaults = () => ({ startDate: dayjs().startOf('month').format('YYYY-MM-DD'), endDate: dayjs().format('YYYY-MM-DD'), groupBy: 'DAY' as 'DAY' | 'MONTH' }); |
| | | const filters = reactive<EnergyFilters & ReturnType<typeof defaults>>(defaults()); |
| | | const applied = ref({ ...filters }); |
| | | const data = ref<EnergyAnalysis>(); |
| | | const loading = ref(false); |
| | | const failed = ref(false); |
| | | const exporting = ref(false); |
| | | const selectedEnergy = ref<EnergyType>('WATER'); |
| | | const trendRef = ref<EchartsUIType>(); |
| | | const shareRef = ref<EchartsUIType>(); |
| | | const areaRef = ref<EchartsUIType>(); |
| | | const trendChart = useEcharts(trendRef); |
| | | const shareChart = useEcharts(shareRef); |
| | | const areaChart = useEcharts(areaRef); |
| | | const costRef = ref<EchartsUIType>(); |
| | | const areaShareRef = ref<EchartsUIType>(); |
| | | const usageRef = ref<EchartsUIType>(); |
| | | const costChart = useEcharts(costRef); |
| | | const areaShareChart = useEcharts(areaShareRef); |
| | | const usageChart = useEcharts(usageRef); |
| | | const toolbox = (name: string) => ({ right: 8, feature: { saveAsImage: { title: 'ä¸è½½ PNG å¾ç', name, pixelRatio: 2 } } }); |
| | | async function exportReport() { |
| | | exporting.value = true; |
| | | try { downloadFileFromBlobPart({ fileName: 'è½èåæ.xlsx', source: await exportEnergyAnalysis({ ...applied.value }) }); } |
| | | finally { exporting.value = false; } |
| | | } |
| | | const hasRecords = computed(() => (data.value?.recordCount ?? 0) > 0); |
| | | const summaryColumns = [ |
| | | { title: 'è½æº', dataIndex: 'energyType' }, |
| | | { title: 'æ»ç¨é', dataIndex: 'quantity', align: 'right' as const }, |
| | | { title: 'è´¹ç¨ï¼å
ï¼', dataIndex: 'amount', align: 'right' as const }, |
| | | { title: 'è´¹ç¨å æ¯', key: 'share', align: 'right' as const }, |
| | | { title: 'å½å
¥æ¡æ°', dataIndex: 'recordCount', align: 'right' as const }, |
| | | ]; |
| | | const detailColumns = [ |
| | | { title: 'æ¥æ / æä»½', dataIndex: 'label' }, |
| | | { title: 'è½æº', dataIndex: 'energyType' }, |
| | | { title: 'ç¨é', dataIndex: 'quantity', align: 'right' as const }, |
| | | { title: 'è´¹ç¨ï¼å
ï¼', dataIndex: 'amount', align: 'right' as const }, |
| | | { title: 'å½å
¥æ¡æ°', dataIndex: 'recordCount', align: 'right' as const }, |
| | | ]; |
| | | let requestId = 0; |
| | | async function load() { |
| | | if (!filters.startDate || !filters.endDate || filters.startDate > filters.endDate) { |
| | | message.warning('è¯·éæ©ææçå¼å§åç»ææ¥æ'); return; |
| | | } |
| | | if (dayjs(filters.endDate).isAfter(dayjs(filters.startDate).add(2, 'year'))) { |
| | | message.warning('æ¯æ¬¡åæè¯·éæ©ä¸¤å¹´ä»¥å
çæ¥æèå´'); return; |
| | | } |
| | | const id = ++requestId; |
| | | const params = { ...filters }; |
| | | loading.value = true; |
| | | failed.value = false; |
| | | try { |
| | | const result = await getEnergyAnalysis(params); |
| | | if (id !== requestId) return; |
| | | data.value = result; |
| | | applied.value = params; |
| | | selectedEnergy.value = params.energyType ?? 'WATER'; |
| | | await nextTick(); |
| | | await renderCharts(); |
| | | } catch { if (id === requestId) { data.value = undefined; failed.value = true; } } |
| | | finally { if (id === requestId) loading.value = false; } |
| | | } |
| | | function reset() { |
| | | Object.keys(filters).forEach(key => delete (filters as Record<string, unknown>)[key]); |
| | | Object.assign(filters, defaults()); |
| | | void load(); |
| | | } |
| | | async function renderTrend() { |
| | | if (!hasRecords.value || !data.value) return; |
| | | const info = energyInfo[selectedEnergy.value]; |
| | | const month = applied.value.groupBy === 'MONTH'; |
| | | const periods: string[] = []; |
| | | let cursor = dayjs(applied.value.startDate).startOf(month ? 'month' : 'day'); |
| | | const end = dayjs(applied.value.endDate); |
| | | while (!cursor.isAfter(end)) { |
| | | periods.push(cursor.format(month ? 'YYYY-MM' : 'YYYY-MM-DD')); |
| | | cursor = cursor.add(1, month ? 'month' : 'day'); |
| | | } |
| | | const values = new Map(data.value.trend.filter(row => row.energyType === selectedEnergy.value).map(row => [row.label, Number(row.quantity)])); |
| | | await trendChart.renderEcharts({ |
| | | animation: false, |
| | | toolbox: toolbox(`${info.name}ç¨éè¶å¿`), |
| | | tooltip: { trigger: 'axis', confine: true }, |
| | | grid: { left: 20, right: 24, top: 40, bottom: 70, containLabel: true }, |
| | | xAxis: { type: 'category', data: periods }, |
| | | yAxis: { type: 'value', name: `${info.name}ç¨éï¼${info.unit}ï¼` }, |
| | | dataZoom: [{ type: 'inside' }, { type: 'slider', height: 18, bottom: 12 }], |
| | | series: [{ name: `${info.name}ï¼${info.unit}ï¼`, type: 'line', showSymbol: periods.length < 40, connectNulls: false, itemStyle: { color: info.color }, data: periods.map(period => values.get(period) ?? null) }], |
| | | }); |
| | | const usage = data.value.areas.filter(row => row.energyType === selectedEnergy.value).sort((a, b) => Number(b.quantity) - Number(a.quantity)).slice(0, 10).reverse(); |
| | | await usageChart.renderEcharts({ |
| | | animation: false, toolbox: toolbox(`${info.name}åºåç¨é`), |
| | | tooltip: { trigger: 'axis', confine: true, axisPointer: { type: 'shadow' } }, |
| | | grid: { left: 16, right: 36, top: 40, bottom: 24, containLabel: true }, |
| | | xAxis: { type: 'value', name: info.unit }, |
| | | yAxis: { type: 'category', data: usage.map(row => row.label), axisLabel: { width: 100, overflow: 'truncate' } }, |
| | | series: [{ name: `${info.name}ï¼${info.unit}ï¼`, type: 'bar', itemStyle: { color: info.color }, data: usage.map(row => Number(row.quantity)) }], |
| | | }); |
| | | await costChart.renderEcharts({ |
| | | animation: false, toolbox: toolbox('è½æºè´¹ç¨è¶å¿'), |
| | | tooltip: { trigger: 'axis', confine: true }, legend: { bottom: 0 }, |
| | | grid: { left: 16, right: 24, top: 40, bottom: 70, containLabel: true }, |
| | | xAxis: { type: 'category', data: periods }, yAxis: { type: 'value', name: 'è´¹ç¨ï¼å
ï¼' }, |
| | | dataZoom: [{ type: 'inside' }], |
| | | series: energyOptions.map(option => { |
| | | const costs = new Map(data.value!.trend.filter(row => row.energyType === option.value).map(row => [row.label, Number(row.amount)])); |
| | | return { name: option.label, type: 'line' as const, showSymbol: periods.length < 40, connectNulls: false, |
| | | itemStyle: { color: energyInfo[option.value as EnergyType].color }, data: periods.map(period => costs.get(period) ?? null) }; |
| | | }), |
| | | }); |
| | | } |
| | | async function renderCharts() { |
| | | if (!hasRecords.value || !data.value) return; |
| | | await renderTrend(); |
| | | if (Number(data.value.totalAmount) > 0) await shareChart.renderEcharts({ |
| | | animation: false, |
| | | toolbox: toolbox('è½æºè´¹ç¨å æ¯'), |
| | | tooltip: { trigger: 'item', confine: true, formatter: '{b}ï¼{c} å
ï¼{d}%ï¼' }, |
| | | legend: { bottom: 0 }, |
| | | series: [{ type: 'pie', radius: '65%', center: ['50%', '45%'], label: { formatter: '{b}\n{d}%' }, data: data.value.summary.filter(row => Number(row.amount) > 0).map(row => ({ name: energyInfo[row.energyType].name, value: Number(row.amount), itemStyle: { color: energyInfo[row.energyType].color } })) }], |
| | | }); |
| | | const totals = new Map<string, number>(); |
| | | for (const row of data.value.areas) totals.set(row.label, (totals.get(row.label) ?? 0) + Number(row.amount)); |
| | | if (Number(data.value.totalAmount) > 0) { |
| | | const sorted = [...totals].filter(([, amount]) => amount > 0).sort((a, b) => b[1] - a[1]); |
| | | const shares = sorted.slice(0, 10).map(([name, value]) => ({ name, value })); |
| | | const others = sorted.slice(10).reduce((sum, [, amount]) => sum + amount, 0); |
| | | if (others > 0) shares.push({ name: 'å
¶ä»åºåå计', value: others }); |
| | | await areaShareChart.renderEcharts({ |
| | | animation: false, toolbox: toolbox('åºåè´¹ç¨å æ¯'), |
| | | tooltip: { trigger: 'item', confine: true, formatter: '{b}ï¼{c} å
ï¼{d}%ï¼' }, |
| | | legend: { type: 'scroll', bottom: 0 }, |
| | | series: [{ type: 'pie', radius: ['35%', '60%'], center: ['50%', '44%'], label: { formatter: '{b}\n{d}%', width: 85, overflow: 'truncate' }, data: shares }], |
| | | }); |
| | | } |
| | | const areaLabels = [...totals].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([label]) => label).reverse(); |
| | | await areaChart.renderEcharts({ |
| | | animation: false, |
| | | toolbox: toolbox('åºåè´¹ç¨åå¸'), |
| | | tooltip: { trigger: 'axis', confine: true, axisPointer: { type: 'shadow' } }, |
| | | legend: { bottom: 0 }, |
| | | grid: { left: 16, right: 24, top: 30, bottom: 45, containLabel: true }, |
| | | xAxis: { type: 'value', name: 'å
' }, |
| | | yAxis: { type: 'category', data: areaLabels, axisLabel: { width: 110, overflow: 'truncate' } }, |
| | | series: energyOptions.map(option => ({ |
| | | name: option.label, type: 'bar' as const, stack: 'amount', itemStyle: { color: energyInfo[option.value as EnergyType].color }, |
| | | data: areaLabels.map(label => data.value!.areas.filter(row => row.label === label && row.energyType === option.value).reduce((sum, row) => sum + Number(row.amount), 0)), |
| | | })), |
| | | }); |
| | | } |
| | | watch(selectedEnergy, () => { void renderTrend(); }); |
| | | onMounted(() => load()); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page title="è½èæ°æ®åæ" description="åºäºæå¨å½å
¥çå°è´¦ï¼æ¥çç¨éè¶å¿åè´¹ç¨åå¸ã"> |
| | | <Card class="mb-4"> |
| | | <Form :model="filters" layout="inline" class="analysis-filters" @finish="load"> |
| | | <FormItem label="å¼å§æ¥æ"><DatePicker v-model:value="filters.startDate" value-format="YYYY-MM-DD" :allow-clear="false" /></FormItem> |
| | | <FormItem label="ç»ææ¥æ"><DatePicker v-model:value="filters.endDate" value-format="YYYY-MM-DD" :allow-clear="false" /></FormItem> |
| | | <FormItem label="æ±æ»"><Select v-model:value="filters.groupBy" :options="[{ label: 'ææ¥', value: 'DAY' }, { label: 'ææ', value: 'MONTH' }]" style="width: 100px" /></FormItem> |
| | | <FormItem label="è½æº"><Select v-model:value="filters.energyType" :options="energyOptions" allow-clear placeholder="å
¨é¨è½æº" style="width: 130px" /></FormItem> |
| | | <FormItem label="åºå"><Input v-model:value="filters.usageArea" placeholder="åºåå
³é®è¯" allow-clear style="width: 150px" /></FormItem> |
| | | <FormItem label="产å"><Select v-model:value="filters.productCategory" :options="productOptions" allow-clear placeholder="å
¨é¨ç±»å«" style="width: 150px" /></FormItem> |
| | | <FormItem><Space wrap><Button type="primary" :loading="loading" @click="load">åæ</Button><Button @click="reset">éç½®</Button><Button v-access:code="['energy:analysis:export']" :disabled="!data || loading" :loading="exporting" @click="exportReport">导åºåææ¥è¡¨</Button></Space></FormItem> |
| | | </Form> |
| | | </Card> |
| | | <Alert v-if="failed" type="error" show-icon message="è½èåæå 载失败ï¼è¯·ç¹å»åæéè¯ã" class="mb-4" /> |
| | | <Spin :spinning="loading"> |
| | | <template v-if="data"> |
| | | <Card class="mb-4" title="è½æºæ±æ»"> |
| | | <template #extra>{{ applied.startDate }} è³ {{ applied.endDate }}</template> |
| | | <div class="mb-4 flex flex-wrap gap-x-6 gap-y-2"> |
| | | <span>æ»è´¹ç¨ï¼<strong>{{ numberText(data.totalAmount, 2) }} å
</strong></span><span>å·²å½å
¥ {{ data.recordCount }} æ¡</span> |
| | | </div> |
| | | <Table :columns="summaryColumns" :data-source="data.summary" row-key="energyType" :pagination="false" :scroll="{ x: 600 }" size="small"> |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.dataIndex === 'energyType'">{{ energyInfo[record.energyType as EnergyType].name }}</template> |
| | | <template v-else-if="column.dataIndex === 'quantity'">{{ numberText(record.quantity) }} {{ energyInfo[record.energyType as EnergyType].unit }}</template> |
| | | <template v-else-if="column.dataIndex === 'amount'">{{ numberText(record.amount, 2) }}</template> |
| | | <template v-else-if="column.key === 'share'">{{ Number(data.totalAmount) > 0 ? numberText(Number(record.amount) / Number(data.totalAmount) * 100, 2) + '%' : 'â' }}</template> |
| | | </template> |
| | | </Table> |
| | | <p class="mb-0 mt-3 text-sm text-muted-foreground">æ°´ãçµãè¸æ±½æåèªåä½ç»è®¡ãæªå½å
¥æ¥æå¨è¶å¿ä¸ç空ï¼å价为 0 çè®°å½ä»è®¡å
¥ç¨éã</p> |
| | | </Card> |
| | | <Card v-if="!hasRecords"><Empty description="æéèå´ææ è½èè®°å½ï¼è¯·è°æ´ç鿡件æå
å½å
¥è½èä¿¡æ¯ã" /></Card> |
| | | <template v-else> |
| | | <Card class="mb-4" title="ç¨éè¶å¿"> |
| | | <template #extra><RadioGroup v-model:value="selectedEnergy" :options="energyOptions" option-type="button" size="small" /></template> |
| | | <EchartsUI ref="trendRef" class="h-[340px] w-full" /> |
| | | </Card> |
| | | <div class="mb-4 grid grid-cols-1 gap-4 lg:grid-cols-2"> |
| | | <Card title="è½æºè´¹ç¨å æ¯"><EchartsUI v-if="Number(data.totalAmount) > 0" ref="shareRef" class="h-[330px] w-full" /><Empty v-else description="å°æªå½å
¥è´¹ç¨ï¼å¡«ååä»·åæ¾ç¤ºè´¹ç¨å æ¯ã" /></Card> |
| | | <Card title="åºåè´¹ç¨åå¸ï¼å 10ï¼"><EchartsUI ref="areaRef" class="h-[330px] w-full" /></Card> |
| | | <Card title="è½æºè´¹ç¨è¶å¿"><EchartsUI ref="costRef" class="h-[330px] w-full" /></Card> |
| | | <Card title="åºåè´¹ç¨å æ¯"><EchartsUI v-if="Number(data.totalAmount) > 0" ref="areaShareRef" class="h-[330px] w-full" /><Empty v-else description="å°æ è´¹ç¨æ°æ®" /></Card> |
| | | <Card class="lg:col-span-2" :title="`${energyInfo[selectedEnergy].name} · åºåç¨é对æ¯ï¼å 10ï¼`"><EchartsUI ref="usageRef" class="h-[330px] w-full" /></Card> |
| | | </div> |
| | | <p class="text-sm text-muted-foreground">å¾è¡¨å³ä¸è§å¯ä¸è½½ PNGï¼åææ¥è¡¨å
å«ç»è®¡å£å¾ãè½æºæ±æ»ãæ¥æè¶å¿ãåºåæ±æ»å个工ä½è¡¨ï¼ä½¿ç¨æè¿ä¸æ¬¡åæçç鿡件ã</p> |
| | | <Card title="è¶å¿æç»"> |
| | | <Table :columns="detailColumns" :data-source="data.trend" :row-key="row => `${row.label}-${row.energyType}`" :scroll="{ x: 600 }" :pagination="{ pageSize: 10, showSizeChanger: true }" size="small"> |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.dataIndex === 'energyType'">{{ energyInfo[record.energyType as EnergyType].name }}</template> |
| | | <template v-else-if="column.dataIndex === 'quantity'">{{ numberText(record.quantity) }} {{ energyInfo[record.energyType as EnergyType].unit }}</template> |
| | | <template v-else-if="column.dataIndex === 'amount'">{{ numberText(record.amount, 2) }}</template> |
| | | </template> |
| | | </Table> |
| | | </Card> |
| | | </template> |
| | | </template> |
| | | </Spin> |
| | | </Page> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .analysis-filters { gap: 8px 0; } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup lang="ts"> |
| | | import type { FormInstance, TablePaginationConfig } from 'ant-design-vue'; |
| | | import type { EnergyFilters, EnergyRecord } from '#/api/energy'; |
| | | import { computed, onMounted, reactive, ref } from 'vue'; |
| | | import { Page } from '@vben/common-ui'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | import { Alert, AutoComplete, Button, Card, DatePicker, Form, FormItem, Input, InputNumber, Modal, Popconfirm, Select, Space, Table, Tag, message } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | | import { createEnergyRecord, deleteEnergyRecord, exportEnergyRecords, getEnergyRecord, getEnergyRecordPage, updateEnergyRecord } from '#/api/energy'; |
| | | import { areaOptions, energyInfo, energyOptions, numberText, productOptions, purposeOptions } from '../shared'; |
| | | |
| | | defineOptions({ name: 'EnergyRecord' }); |
| | | const filters = reactive<EnergyFilters>({}); |
| | | const rows = ref<EnergyRecord[]>([]); |
| | | const loading = ref(false); |
| | | const exporting = ref(false); |
| | | async function exportRecords() { |
| | | if (filters.startDate && filters.endDate && filters.startDate > filters.endDate) { message.warning('å¼å§æ¥æä¸è½æäºç»ææ¥æ'); return; } |
| | | exporting.value = true; |
| | | try { downloadFileFromBlobPart({ fileName: 'è½èå°è´¦.xlsx', source: await exportEnergyRecords({ ...filters }) }); } |
| | | finally { exporting.value = false; } |
| | | } |
| | | const loadError = ref(false); |
| | | const pagination = reactive({ current: 1, pageSize: 10, total: 0, showSizeChanger: true }); |
| | | const open = ref(false); |
| | | const saving = ref(false); |
| | | const detail = ref(false); |
| | | const formRef = ref<FormInstance>(); |
| | | const initial = (): EnergyRecord => ({ recordDate: dayjs().format('YYYY-MM-DD'), energyType: 'WATER', usageArea: '', productCategory: 'ççª', usagePurpose: '', quantity: 1, unitPrice: 0, recorder: '', remark: '' }); |
| | | const model = reactive<EnergyRecord>(initial()); |
| | | const unit = computed(() => energyInfo[model.energyType].unit); |
| | | const estimatedAmount = computed(() => numberText(Number(model.quantity || 0) * Number(model.unitPrice || 0), 2)); |
| | | const title = computed(() => detail.value ? 'è½èè®°å½è¯¦æ
' : model.id ? 'ç¼è¾è½èè®°å½' : 'æ°å¢è½èè®°å½'); |
| | | const rules = { |
| | | recordDate: [{ required: true, message: 'è¯·éæ©ç»è®¡æ¥æ' }], |
| | | energyType: [{ required: true, message: 'è¯·éæ©è½æºç±»å' }], |
| | | usageArea: [{ required: true, whitespace: true, message: '请è¾å
¥ä½¿ç¨åºå' }], |
| | | productCategory: [{ required: true, message: 'è¯·éæ©äº§åç±»å«' }], |
| | | usagePurpose: [{ required: true, whitespace: true, message: '请è¾å
¥ç¨é' }], |
| | | quantity: [{ required: true, type: 'number' as const, min: 0.001, message: 'ç¨éå¿
须大äºé¶' }], |
| | | unitPrice: [{ required: true, type: 'number' as const, min: 0, message: 'åä»·ä¸è½ä¸ºè´æ°' }], |
| | | recorder: [{ required: true, whitespace: true, message: '请è¾å
¥å½å
¥äºº' }], |
| | | }; |
| | | const columns = [ |
| | | { title: 'ç»è®¡æ¥æ', dataIndex: 'recordDate', width: 120 }, |
| | | { title: 'è½æº', dataIndex: 'energyType', width: 95 }, |
| | | { title: '使ç¨åºå', dataIndex: 'usageArea', width: 160 }, |
| | | { title: '产åç±»å«', dataIndex: 'productCategory', width: 130 }, |
| | | { title: 'ç¨é', dataIndex: 'usagePurpose', width: 170 }, |
| | | { title: 'ç¨é', dataIndex: 'quantity', width: 150, align: 'right' as const }, |
| | | { title: 'åä»·ï¼å
/åä½ï¼', dataIndex: 'unitPrice', width: 150, align: 'right' as const }, |
| | | { title: 'è´¹ç¨ï¼å
ï¼', dataIndex: 'amount', width: 145, align: 'right' as const }, |
| | | { title: 'å½å
¥äºº', dataIndex: 'recorder', width: 110 }, |
| | | { title: 'æä½', key: 'action', width: 190, fixed: 'right' as const }, |
| | | ]; |
| | | let requestId = 0; |
| | | async function load(reset = false) { |
| | | if (filters.startDate && filters.endDate && filters.startDate > filters.endDate) { |
| | | message.warning('å¼å§æ¥æä¸è½æäºç»ææ¥æ'); return; |
| | | } |
| | | if (reset) pagination.current = 1; |
| | | const currentId = ++requestId; |
| | | loading.value = true; |
| | | loadError.value = false; |
| | | try { |
| | | const data = await getEnergyRecordPage({ ...filters, pageNo: pagination.current, pageSize: pagination.pageSize }); |
| | | if (currentId !== requestId) return; |
| | | rows.value = data.list; |
| | | pagination.total = data.total; |
| | | } catch { |
| | | if (currentId === requestId) { rows.value = []; loadError.value = true; } |
| | | } finally { if (currentId === requestId) loading.value = false; } |
| | | } |
| | | function resetFilters() { |
| | | Object.keys(filters).forEach(key => delete filters[key as keyof EnergyFilters]); |
| | | void load(true); |
| | | } |
| | | function tableChange(page: TablePaginationConfig) { |
| | | pagination.current = page.current ?? 1; |
| | | pagination.pageSize = page.pageSize ?? 10; |
| | | void load(); |
| | | } |
| | | async function showForm(row?: EnergyRecord, readonly = false) { |
| | | const values = row ? await getEnergyRecord(row.id!) : initial(); |
| | | detail.value = readonly; |
| | | // æ¸
é¤å䏿¬¡ç¼è¾çç¼å·å详æ
åæ®µã |
| | | Object.keys(model).forEach(key => delete (model as unknown as Record<string, unknown>)[key]); |
| | | Object.assign(model, values); |
| | | open.value = true; |
| | | formRef.value?.clearValidate(); |
| | | } |
| | | async function save() { |
| | | await formRef.value?.validate(); |
| | | saving.value = true; |
| | | try { |
| | | const data = { ...model, usageArea: model.usageArea.trim(), usagePurpose: model.usagePurpose.trim(), recorder: model.recorder.trim() }; |
| | | await (data.id ? updateEnergyRecord(data) : createEnergyRecord(data)); |
| | | message.success('è½èè®°å½å·²ä¿å'); |
| | | open.value = false; |
| | | await load(); |
| | | } finally { saving.value = false; } |
| | | } |
| | | const deletingId = ref<number>(); |
| | | async function remove(row: EnergyRecord) { |
| | | deletingId.value = row.id; |
| | | try { |
| | | await deleteEnergyRecord(row.id!); |
| | | message.success('è½èè®°å½å·²å é¤'); |
| | | if (rows.value.length === 1 && pagination.current > 1) pagination.current--; |
| | | await load(); |
| | | } finally { deletingId.value = undefined; } |
| | | } |
| | | onMounted(() => load()); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page title="è½èè®°å½" description="æå¤©æå¨è®°å½æ°´ãçµãè¸æ±½ä½¿ç¨æ
åµï¼è´¹ç¨èªå¨è®¡ç®ã"> |
| | | <Card> |
| | | <Form :model="filters" layout="inline" class="energy-filters" @finish="load(true)"> |
| | | <FormItem label="å¼å§æ¥æ"><DatePicker v-model:value="filters.startDate" value-format="YYYY-MM-DD" placeholder="å¼å§æ¥æ" /></FormItem> |
| | | <FormItem label="ç»ææ¥æ"><DatePicker v-model:value="filters.endDate" value-format="YYYY-MM-DD" placeholder="ç»ææ¥æ" /></FormItem> |
| | | <FormItem label="è½æº"><Select v-model:value="filters.energyType" :options="energyOptions" allow-clear placeholder="å
¨é¨è½æº" style="width: 130px" /></FormItem> |
| | | <FormItem label="åºå"><Input v-model:value="filters.usageArea" allow-clear placeholder="è¾å
¥åºåå
³é®è¯" style="width: 160px" /></FormItem> |
| | | <FormItem label="产å"><Select v-model:value="filters.productCategory" :options="productOptions" allow-clear placeholder="å
¨é¨ç±»å«" style="width: 150px" /></FormItem> |
| | | <FormItem><Space><Button type="primary" :loading="loading" @click="load(true)">æ¥è¯¢</Button><Button @click="resetFilters">éç½®</Button></Space></FormItem> |
| | | </Form> |
| | | <div class="mb-4 flex flex-wrap items-center justify-between gap-3"> |
| | | <span class="text-base font-semibold">è½èå°è´¦</span> |
| | | <Space><Button v-access:code="['energy:record:export']" :loading="exporting" @click="exportRecords">导åºå°è´¦</Button><Button v-access:code="['energy:record:create']" type="primary" @click="showForm()">æ°å¢è½èè®°å½</Button></Space> |
| | | </div> |
| | | <Alert v-if="loadError" type="error" message="è½èè®°å½å 载失败ï¼è¯·ç¹å»æ¥è¯¢éè¯ã" show-icon class="mb-4" /> |
| | | <Table :columns="columns" :data-source="rows" row-key="id" :loading="loading" :pagination="pagination" :scroll="{ x: 1420 }" :locale="{ emptyText: 'ææ è½èè®°å½ï¼è¯·æ°å¢æ¯æ¥ç¨è½ä¿¡æ¯' }" @change="tableChange"> |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.dataIndex === 'energyType'"><Tag>{{ energyInfo[record.energyType as keyof typeof energyInfo].name }}</Tag></template> |
| | | <template v-else-if="column.dataIndex === 'quantity'">{{ numberText(record.quantity) }} {{ energyInfo[record.energyType as keyof typeof energyInfo].unit }}</template> |
| | | <template v-else-if="column.dataIndex === 'unitPrice'">{{ numberText(record.unitPrice, 4) }}</template> |
| | | <template v-else-if="column.dataIndex === 'amount'">{{ numberText(record.amount, 2) }}</template> |
| | | <template v-else-if="column.key === 'action'"> |
| | | <Space :size="0"> |
| | | <Button v-access:code="['energy:record:query']" type="link" size="small" @click="showForm(record as EnergyRecord, true)">详æ
</Button> |
| | | <Button v-access:code="['energy:record:update']" type="link" size="small" @click="showForm(record as EnergyRecord)">ç¼è¾</Button> |
| | | <Popconfirm title="ç¡®å®å é¤è¿æ¡è½èè®°å½ï¼å é¤åç»è®¡æ°æ®ä¹ä¼æ´æ°ã" ok-text="ç¡®å®" cancel-text="åæ¶" @confirm="remove(record as EnergyRecord)"> |
| | | <Button v-access:code="['energy:record:delete']" type="link" danger size="small" :loading="deletingId === record.id">å é¤</Button> |
| | | </Popconfirm> |
| | | </Space> |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | </Card> |
| | | <Modal v-model:open="open" :title="title" :width="760" :confirm-loading="saving" :closable="!saving" :mask-closable="!saving" :keyboard="!saving" :cancel-button-props="{ disabled: saving }" :footer="detail ? null : undefined" ok-text="ä¿å" cancel-text="åæ¶" @ok="save"> |
| | | <Form ref="formRef" :model="model" :rules="rules" :disabled="detail || saving" layout="vertical" class="energy-form"> |
| | | <FormItem label="ç»è®¡æ¥æ" name="recordDate"><DatePicker v-model:value="model.recordDate" value-format="YYYY-MM-DD" :disabled-date="date => date.isAfter(dayjs(), 'day')" class="w-full" /></FormItem> |
| | | <FormItem label="è½æºç±»å" name="energyType"><Select v-model:value="model.energyType" :options="energyOptions" /></FormItem> |
| | | <FormItem label="使ç¨åºå" name="usageArea"><AutoComplete v-model:value="model.usageArea" :options="areaOptions" :maxlength="100" placeholder="éæ©å»ºè®®åºåææå¨è¾å
¥" /></FormItem> |
| | | <FormItem label="产åç±»å«" name="productCategory"><Select v-model:value="model.productCategory" :options="productOptions" /></FormItem> |
| | | <FormItem label="ç¨é / å·¥åº" name="usagePurpose"><AutoComplete v-model:value="model.usagePurpose" :options="purposeOptions" :maxlength="100" placeholder="å¦è®¾å¤æ¸
æ´ãè¸ç
®æåãçè" /></FormItem> |
| | | <FormItem label="å½å
¥äºº" name="recorder"><Input v-model:value="model.recorder" :maxlength="50" placeholder="请è¾å
¥å§å" /></FormItem> |
| | | <FormItem :label="`ç¨éï¼${unit}ï¼`" name="quantity"><InputNumber v-model:value="model.quantity" :min="0.001" :max="999999999999999.999" :precision="3" style="width: 100%" /></FormItem> |
| | | <FormItem :label="`åä»·ï¼å
/${unit}ï¼`" name="unitPrice"><InputNumber v-model:value="model.unitPrice" :min="0" :max="99999999.9999" :precision="4" style="width: 100%" /></FormItem> |
| | | <div class="col-span-full mb-4 rounded border border-solid border-[hsl(var(--border))] p-3"> |
| | | è´¹ç¨ï¼<strong>{{ detail ? numberText(model.amount, 2) : estimatedAmount }} å
</strong> |
| | | <div class="mt-1 text-sm text-muted-foreground">æç¨é à å价计ç®ï¼ä¿åæ¶ç²¾ç¡®å°åãä»
è®°å½ç¨éæ¶ï¼åä»·å¯å¡« 0ã</div> |
| | | </div> |
| | | <FormItem label="夿³¨" name="remark" class="col-span-full"><Input.TextArea v-model:value="model.remark" :maxlength="500" show-count :rows="3" placeholder="å¦è®¡éå£å¾ãç¹æ®ç¨è½æ
åµç" /></FormItem> |
| | | </Form> |
| | | </Modal> |
| | | </Page> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .energy-filters { gap: 8px 0; margin-bottom: 24px; } |
| | | .energy-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 20px; margin-top: 20px; } |
| | | @media (max-width: 640px) { .energy-form { grid-template-columns: minmax(0, 1fr); } } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { EnergyType } from '#/api/energy'; |
| | | |
| | | export const energyOptions = [ |
| | | { label: 'æ°´', value: 'WATER' }, |
| | | { label: 'çµ', value: 'ELECTRICITY' }, |
| | | { label: 'è¸æ±½', value: 'STEAM' }, |
| | | ]; |
| | | export const energyInfo: Record<EnergyType, { name: string; unit: string; color: string }> = { |
| | | WATER: { name: '水', unit: 'm³', color: '#1677ff' }, |
| | | ELECTRICITY: { name: 'çµ', unit: 'kWh', color: '#d48806' }, |
| | | STEAM: { name: 'è¸æ±½', unit: 't', color: '#389e0d' }, |
| | | }; |
| | | export const productOptions = ['ççª', 'æ»è¡¥ååæµ', 'å
¬ç¨è½è', 'å
¶ä»'].map(value => ({ label: value, value })); |
| | | export const areaOptions = ['ççªç产åº', 'åæµç产åº', 'æ¸
æ´åº', 'çèåº', 'çè£
å
è£
åº', 'å¨åç«', 'ä»å¨åº', 'åå
¬çæ´»åº'].map(value => ({ value })); |
| | | export const purposeOptions = ['åææ¸
æ´', 'è®¾å¤æ¸
æ´ / CIP', 'è¸ç
®æå', 'çè', 'çè£
å
è£
', 'å¶å·', 'éæ', 'æ¥å¸¸ç¨è½'].map(value => ({ value })); |
| | | export function numberText(value: number | undefined, digits = 3) { |
| | | return Number(value ?? 0).toLocaleString('zh-CN', { maximumFractionDigits: digits, minimumFractionDigits: digits === 2 ? 2 : 0 }); |
| | | } |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | componentProps: { |
| | | disabled: formType === 'detail', |
| | | placeholder: '鿩仿¬¾æ¶é´', |
| | | showTime: true, |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | showTime: false, |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | }, |
| | | rules: 'required', |
| | |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { erpPriceInputFormatter, formatDateTime } from '@vben/utils'; |
| | | import { erpPriceInputFormatter, formatDate } from '@vben/utils'; |
| | | |
| | | import { Spin } from 'ant-design-vue'; |
| | | |
| | |
| | | { |
| | | label: '仿¬¾æ¶é´', |
| | | field: 'paymentTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | label: 'å计éé¢ï¼å
ï¼', |
| | |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { label: '夿³¨', field: 'remark', render: textRender }, |
| | | ] as DescriptionItemSchema[], |
| | |
| | | <script lang="ts" setup> |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import { submitFinancePayment } from '#/api/erp/finance/payment'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { ErpFinancePaymentApi } from '#/api/erp/finance/payment'; |
| | | |
| | |
| | | /** æäº¤å®¡æ¹ - å
è·åæµç¨å表 */ |
| | | async function handleSubmit(row: ErpFinancePaymentApi.FinancePayment) { |
| | | const hideLoading = message.loading({ |
| | | content: 'è·åå®¡æ¹æµç¨...', |
| | | content: automaticApproval.value ? 'æ£å¨æäº¤çæ...' : 'è·åå®¡æ¹æµç¨...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | if (automaticApproval.value) { |
| | | await submitFinancePayment(row.id!, 'finance_payment_approve'); |
| | | hideLoading(); |
| | | message.success('å·²æäº¤çæ'); |
| | | handleRefresh(); |
| | | return; |
| | | } |
| | | const processList = await getFinancePaymentApproveProcessList(); |
| | | hideLoading(); |
| | | if (!processList || processList.length === 0) { |
| | |
| | | return; |
| | | } |
| | | // æå¼æµç¨éæ©å¼¹çª |
| | | |
| | | processModalApi.setData({ id: row.id, processList }).open(); |
| | | } catch { |
| | | hideLoading(); |
| | |
| | | componentProps: { |
| | | disabled: formType === 'detail', |
| | | placeholder: 'éæ©æ¶æ¬¾æ¶é´', |
| | | showTime: true, |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | showTime: false, |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | }, |
| | | rules: 'required', |
| | |
| | | { |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | componentProps: { |
| | | disabled: formType === 'detail', |
| | | placeholder: 'éæ©å
¥åºæ¶é´', |
| | | showTime: true, |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | showTime: false, |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | }, |
| | | rules: 'required', |
| | |
| | | title: 'æ¥ç¥¨æ¥æ', |
| | | field: 'invoiceTime', |
| | | minWidth: 150, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: '夿³¨', |
| | |
| | | import { onMounted, ref } from 'vue'; |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { erpPriceInputFormatter, formatDateTime } from '@vben/utils'; |
| | | import { erpPriceInputFormatter, formatDate } from '@vben/utils'; |
| | | |
| | | import { Spin } from 'ant-design-vue'; |
| | | |
| | |
| | | { |
| | | label: 'å¼ç¥¨æ¥æ', |
| | | field: 'invoiceTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { label: 'å建人', field: 'creatorName', render: textRender }, |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { label: '夿³¨', field: 'remark', render: textRender }, |
| | | ] as DescriptionItemSchema[], |
| | |
| | | import type { ErpPurchaseInvoiceAiApi } from '#/api/erp/purchase/invoice/ai'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { isAiEnable } from '@vben/hooks'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button, message, Tag } from 'ant-design-vue'; |
| | | import { nextTick, ref } from 'vue'; |
| | | import { computed, nextTick, ref } from 'vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | |
| | | } |
| | | } |
| | | |
| | | /** å·¥å
·æ æé®ï¼è¯å«å½å
¥å AI æ»å¼å
³æ§å¶ */ |
| | | const toolbarActions = computed(() => [ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['æ¥ç¥¨']), |
| | | type: 'primary' as const, |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['erp:purchase-invoice:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | ...(isAiEnable() |
| | | ? [ |
| | | { |
| | | label: 'è¯å«å½å
¥', |
| | | type: 'primary' as const, |
| | | auth: ['erp:purchase-invoice:create'], |
| | | onClick: handleOcrCreate, |
| | | }, |
| | | ] |
| | | : []), |
| | | { |
| | | label: $t('ui.actionTitle.export'), |
| | | type: 'primary' as const, |
| | | icon: ACTION_ICON.DOWNLOAD, |
| | | auth: ['erp:purchase-invoice:export'], |
| | | onClick: handleExport, |
| | | }, |
| | | ]); |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | |
| | | <OcrUpload ref="ocrUploadRef" @success="handleOcrSuccess" /> |
| | | <Grid table-title="æ¥ç¥¨å表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['æ¥ç¥¨']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['erp:purchase-invoice:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: 'è¯å«å½å
¥', |
| | | type: 'primary', |
| | | auth: ['erp:purchase-invoice:create'], |
| | | onClick: handleOcrCreate, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.export'), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.DOWNLOAD, |
| | | auth: ['erp:purchase-invoice:export'], |
| | | onClick: handleExport, |
| | | }, |
| | | ]" |
| | | /> |
| | | <TableAction :actions="toolbarActions" /> |
| | | </template> |
| | | <template #no="{ row }"> |
| | | <Button type="link" @click="handleDetail(row)"> |
| | |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | placeholder: 'éæ©è®¢åæ¶é´', |
| | | showTime: true, |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | showTime: false, |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | }, |
| | | rules: 'required', |
| | |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { |
| | | erpCountInputFormatter, |
| | | erpPriceInputFormatter, |
| | | formatDateTime, |
| | | } from '@vben/utils'; |
| | | import { erpCountInputFormatter, erpPriceInputFormatter, formatDate } from '@vben/utils'; |
| | | |
| | | import { Spin, Tag } from 'ant-design-vue'; |
| | | |
| | |
| | | { |
| | | label: 'è®¢åæ¶é´', |
| | | field: 'orderTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | label: 'å计æ°é', |
| | |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { label: '夿³¨', field: 'remark', render: textRender }, |
| | | ] as DescriptionItemSchema[], |
| | |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | placeholder: 'éæ©ç³è¯·æ¶é´', |
| | | showTime: true, |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | showTime: false, |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | disabled: formType === 'detail', |
| | | }, |
| | |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { |
| | | erpCountInputFormatter, |
| | | erpPriceInputFormatter, |
| | | formatDateTime, |
| | | } from '@vben/utils'; |
| | | import { erpCountInputFormatter, erpPriceInputFormatter, formatDate } from '@vben/utils'; |
| | | |
| | | import { Spin } from 'ant-design-vue'; |
| | | |
| | |
| | | { |
| | | label: 'ç³è¯·æ¶é´', |
| | | field: 'requestTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { label: 'ä¾åºååç§°', field: 'supplierName', render: textRender }, |
| | | { |
| | |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { label: '夿³¨', field: 'remark', render: textRender }, |
| | | ] as DescriptionItemSchema[], |
| | |
| | | <script lang="ts" setup> |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { ErpPurchaseRequestApi } from '#/api/erp/purchase/request'; |
| | | |
| | |
| | | /** æäº¤å®¡æ¹ - å
è·åæµç¨å表 */ |
| | | async function handleSubmit(row: ErpPurchaseRequestApi.PurchaseRequest) { |
| | | const hideLoading = message.loading({ |
| | | content: 'è·åå®¡æ¹æµç¨...', |
| | | content: automaticApproval.value ? 'æ£å¨æäº¤çæ...' : 'è·åå®¡æ¹æµç¨...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | if (automaticApproval.value) { |
| | | await submitPurchaseRequest(row.id!, 'purchase_request_approve'); |
| | | hideLoading(); |
| | | message.success('å·²æäº¤çæ'); |
| | | handleRefresh(); |
| | | return; |
| | | } |
| | | const processList = await getApproveProcessList(row.id!); |
| | | hideLoading(); |
| | | if (!processList || processList.length === 0) { |
| | |
| | | return; |
| | | } |
| | | // æå¼æµç¨éæ©å¼¹çª |
| | | |
| | | processModalApi.setData({ id: row.id, processList }).open(); |
| | | } catch { |
| | | hideLoading(); |
| | |
| | | componentProps: { |
| | | disabled: formType === 'detail', |
| | | placeholder: 'éæ©éè´§æ¶é´', |
| | | showTime: true, |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | showTime: false, |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | }, |
| | | rules: 'required', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | placeholder: 'éæ©è®¢åæ¶é´', |
| | | showTime: true, |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | showTime: false, |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | }, |
| | | rules: 'required', |
| | |
| | | component: 'RangePicker', |
| | | componentProps: { |
| | | ...getRangePickerDefaultProps(), |
| | | showTime: false, |
| | | format: 'YYYY-MM-DD', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | |
| | | import { |
| | | erpCountInputFormatter, |
| | | erpPriceInputFormatter, |
| | | formatDateTime, |
| | | formatDate, |
| | | } from '@vben/utils'; |
| | | |
| | | import { Spin, Tag } from 'ant-design-vue'; |
| | |
| | | { |
| | | label: 'è®¢åæ¶é´', |
| | | field: 'orderTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | label: 'å计æ°é', |
| | |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { label: '夿³¨', field: 'remark', render: textRender }, |
| | | ] as DescriptionItemSchema[], |
| | |
| | | import { downloadFileFromBlobPart, isEmpty } from '@vben/utils'; |
| | | |
| | | import { message, Tag, Tooltip } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | |
| | | } from '#/api/erp/sale/order'; |
| | | import { generateFromSaleOrder } from '#/api/mes/wm/salesnotice'; |
| | | import { $t } from '#/locales'; |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | |
| | | |
| | | /** 导åºè¡¨æ ¼ */ |
| | | async function handleExport() { |
| | | const data = await exportSaleOrder(await gridApi.formApi.getValues()); |
| | | const data = await exportSaleOrder( |
| | | normalizeDateRange(await gridApi.formApi.getValues()), |
| | | ); |
| | | downloadFileFromBlobPart({ fileName: 'éå®è®¢å.xls', source: data }); |
| | | } |
| | | |
| | | /** æ¥æçéè¦çæéèµ·æ¢æ¥æçå
¨å¤©ï¼å
¼å®¹å端æ¶é´ç±»åã */ |
| | | function normalizeDateRange(values: Record<string, any>) { |
| | | const range = values.orderTime; |
| | | if (!Array.isArray(range) || range.length !== 2 || !range.every(Boolean)) { |
| | | return values; |
| | | } |
| | | return { |
| | | ...values, |
| | | orderTime: [ |
| | | dayjs(range[0]).startOf('day').format('YYYY-MM-DD HH:mm:ss'), |
| | | dayjs(range[1]).endOf('day').format('YYYY-MM-DD HH:mm:ss'), |
| | | ], |
| | | }; |
| | | } |
| | | |
| | | /** 导å
¥éå®è®¢å */ |
| | |
| | | /** æäº¤éå®è®¢å审æ¹ï¼åååå
¬ï¼ */ |
| | | async function handleSubmit(row: ErpSaleOrderApi.SaleOrder) { |
| | | // è·åå¯ç¨çå®¡æ¹æµç¨å®ä¹ |
| | | const processList = await getSaleOrderApproveProcessList(); |
| | | if (isEmpty(processList)) { |
| | | const processList = automaticApproval.value ? [] : await getSaleOrderApproveProcessList(); |
| | | if (!automaticApproval.value && isEmpty(processList)) { |
| | | message.error('éå®è®¢åå®¡æ¹æµç¨æªé
ç½®ï¼è¯·å
å¨ãåååå
¬âæµç¨ç®¡çãä¸å建并åå¸éå®è®¢åå®¡æ¹æµç¨'); |
| | | return; |
| | | } |
| | | const hideLoading = message.loading({ |
| | | content: 'æ£å¨æäº¤å®¡æ¹...', |
| | | content: automaticApproval.value ? 'æ£å¨æäº¤çæ...' : 'æ£å¨æäº¤å®¡æ¹...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await submitSaleOrder(row.id!, processList[0].key); |
| | | message.success('æäº¤å®¡æ¹æåï¼è¯·å°åååå
¬ä¸å¤ç'); |
| | | await submitSaleOrder(row.id!, automaticApproval.value ? 'sale_order_approve' : processList[0]!.key); |
| | | message.success(automaticApproval.value ? 'å·²æäº¤çæ' : 'æäº¤å®¡æ¹æåï¼è¯·å°åååå
¬ä¸å¤ç'); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | // çäº§ç¶ææç´¢åæ°è½¬æ¢ï¼-1(æ éç产) â needProduction=0 |
| | | const { productionStatus, ...rest } = formValues; |
| | | const { productionStatus, ...rest } = normalizeDateRange(formValues); |
| | | const params: Record<string, any> = { ...rest }; |
| | | if (productionStatus === -1) { |
| | | params.needProduction = 0; |
| | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { formatDate } from '@vben/utils'; |
| | | |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | |
| | |
| | | try { |
| | | formData.value = await getSaleOrder(data.id); |
| | | // è®¾ç½®å° values |
| | | await formApi.setValues(formData.value); |
| | | await formApi.setValues({ |
| | | ...formData.value, |
| | | productionFinishTime: formatDate(formData.value.productionFinishTime), |
| | | }); |
| | | if (formData.value?.attachmentList?.length) { |
| | | const blobIds = formData.value.attachmentList.map((item) => ({ |
| | | uid: String(item.id), |
| | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictLabel } from '@vben/hooks'; |
| | | import { useUserStore } from '@vben/stores'; |
| | | import { erpPriceMultiply, formatDateTime } from '@vben/utils'; |
| | | import { erpPriceMultiply, formatDate, formatDateTime } from '@vben/utils'; |
| | | |
| | | import { getCustomerSimpleList } from '#/api/crm/customer'; |
| | | import { getAccountSimpleList } from '#/api/erp/finance/account'; |
| | |
| | | <div>订ååå·ï¼{{ printData.no || '-' }}</div> |
| | | <div> |
| | | è®¢åæ¶é´ï¼{{ |
| | | formatDateTime(printData.orderTime) || '-' |
| | | formatDate(printData.orderTime) || '-' |
| | | }} |
| | | </div> |
| | | <div>客æ·ï¼{{ customerName }}</div> |
| | |
| | | <div>å建人ï¼{{ creatorName }}</div> |
| | | <div> |
| | | å建æ¶é´ï¼{{ |
| | | formatDateTime(printData.createTime) || '-' |
| | | formatDate(printData.createTime) || '-' |
| | | }} |
| | | </div> |
| | | <div class="col-span-3">夿³¨ï¼{{ printData.remark || '-' }}</div> |
| | |
| | | componentProps: { |
| | | disabled: formType === 'detail', |
| | | placeholder: 'éæ©åºåºæ¶é´', |
| | | showTime: true, |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | showTime: false, |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | }, |
| | | rules: 'required', |
| | |
| | | componentProps: { |
| | | disabled: formType === 'detail', |
| | | placeholder: 'éæ©éè´§æ¶é´', |
| | | showTime: true, |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | showTime: false, |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | }, |
| | | rules: 'required', |
| | |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | placeholder: 'éæ©çç¹æ¶é´', |
| | | showTime: true, |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | showTime: false, |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | }, |
| | | rules: 'required', |
| | |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | placeholder: 'éæ©å
¥åºæ¶é´', |
| | | showTime: true, |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | showTime: false, |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | }, |
| | | rules: 'required', |
| | |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | placeholder: 'éæ©è°åº¦æ¶é´', |
| | | showTime: true, |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | showTime: false, |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | }, |
| | | rules: 'required', |
| | |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | placeholder: 'éæ©åºåºæ¶é´', |
| | | showTime: true, |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | showTime: false, |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | }, |
| | | rules: 'required', |
| | |
| | | field: 'createTime', |
| | | title: 'åºå
¥åºæ¥æ', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'count', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { formatDateTime } from '@vben/utils'; |
| | | import { formatDateTime, formatDate } from '@vben/utils'; |
| | | |
| | | import { Spin } from 'ant-design-vue'; |
| | | |
| | |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | ] as DescriptionItemSchema[], |
| | | }); |
| | |
| | | <script lang="ts" setup> |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { HrmAttendanceExceptionApi } from '#/api/hrm/attendance/exception'; |
| | | |
| | |
| | | |
| | | /** æäº¤èå¤å¼å¸¸ç³è¯· */ |
| | | async function handleSubmit(row: HrmAttendanceExceptionApi.AttendanceException) { |
| | | const res = await getAttendanceExceptionApproveProcessList(); |
| | | const res = automaticApproval.value ? [] : await getAttendanceExceptionApproveProcessList(); |
| | | processList.value = res; |
| | | currentRow.value = row; |
| | | selectedProcessKey.value = undefined; |
| | | if (automaticApproval.value) { |
| | | selectedProcessKey.value = 'hrm_attendance_exception'; |
| | | await confirmSubmit(); |
| | | return; |
| | | } |
| | | submitModalVisible.value = true; |
| | | } |
| | | |
| | |
| | | submitLoading.value = true; |
| | | try { |
| | | await submitAttendanceException(currentRow.value!.id!, selectedProcessKey.value); |
| | | message.success('æäº¤å®¡æ¹æå'); |
| | | message.success(automaticApproval.value ? 'å·²æäº¤çæ' : 'æäº¤å®¡æ¹æå'); |
| | | submitModalVisible.value = false; |
| | | handleRefresh(); |
| | | } finally { |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | emit('success'); |
| | | // å端è¿å导å
¥ç»æï¼å«å¤±è´¥åå ï¼ä¾å¦â以ä¸åå·¥ä¸åå¨...ï¼éè¡¥å
ææºå·...âï¼ |
| | | if (res && res.length > 0) { |
| | | message[res.includes('æª') || res.includes('ä¸å®æ´') ? 'warning' : 'success'](res); |
| | | // 失败æ¶åç«¯ä¼æ¼ä¸ãæªè½å¯¼å
¥ï¼ãæç»æ®µï¼ç¨å®å¤å®ï¼é¿å
æåææ¡éåºç°ãæªã就误æ¥è¦å |
| | | message[res.includes('æªè½å¯¼å
¥') ? 'warning' : 'success'](res); |
| | | } else { |
| | | message.success('导å
¥æå'); |
| | | } |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { formatDateTime } from '@vben/utils'; |
| | | import { formatDateTime, formatDate } from '@vben/utils'; |
| | | |
| | | import { Spin } from 'ant-design-vue'; |
| | | |
| | |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | ] as DescriptionItemSchema[], |
| | | }); |
| | |
| | | <script lang="ts" setup> |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { HrmLeaveApi } from '#/api/hrm/leave'; |
| | | |
| | |
| | | |
| | | /** æäº¤è¯·åç³è¯·ï¼å
éæ©å®¡æ¹æµç¨ï¼ */ |
| | | async function handleSubmit(row: HrmLeaveApi.Leave) { |
| | | processList.value = await getLeaveApproveProcessDefinitionList(); |
| | | processList.value = automaticApproval.value ? [] : await getLeaveApproveProcessDefinitionList(); |
| | | currentRow.value = row; |
| | | selectedProcessKey.value = undefined; |
| | | if (automaticApproval.value) { |
| | | selectedProcessKey.value = 'hrm_leave'; |
| | | await confirmSubmit(); |
| | | return; |
| | | } |
| | | submitModalVisible.value = true; |
| | | } |
| | | |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { formatDateTime } from '@vben/utils'; |
| | | import { formatDate } from '@vben/utils'; |
| | | |
| | | import { Spin, Tag } from 'ant-design-vue'; |
| | | |
| | |
| | | field: 'status', |
| | | render: (value) => h(DictTag, { type: DICT_TYPE.HRM_AUDIT_STATUS, value }), |
| | | }, |
| | | { label: 'å建æ¶é´', field: 'createTime', render: (value) => formatDateTime(value) as string }, |
| | | { label: 'å建æ¶é´', field: 'createTime', render: (value) => formatDate(value) as string }, |
| | | ] as DescriptionItemSchema[], |
| | | }); |
| | | |
| | |
| | | <script lang="ts" setup> |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { HrmOvertimeApi } from '#/api/hrm/overtime'; |
| | | |
| | |
| | | const submitLoading = ref(false); |
| | | |
| | | async function handleSubmit(row: HrmOvertimeApi.Overtime) { |
| | | processList.value = await getOvertimeApproveProcessDefinitionList(); |
| | | processList.value = automaticApproval.value ? [] : await getOvertimeApproveProcessDefinitionList(); |
| | | currentRow.value = row; |
| | | selectedProcessKey.value = undefined; |
| | | if (automaticApproval.value) { |
| | | selectedProcessKey.value = 'hrm_overtime'; |
| | | await confirmSubmit(); |
| | | return; |
| | | } |
| | | submitModalVisible.value = true; |
| | | } |
| | | |
| | |
| | | submitLoading.value = true; |
| | | try { |
| | | await submitOvertime(currentRow.value!.id!, selectedProcessKey.value); |
| | | message.success('å·²æäº¤è³åååå
¬å®¡æ¹'); |
| | | message.success(automaticApproval.value ? 'å·²æäº¤çæ' : 'å·²æäº¤è³åååå
¬å®¡æ¹'); |
| | | submitModalVisible.value = false; |
| | | handleRefresh(); |
| | | } finally { |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { formatDateTime } from '@vben/utils'; |
| | | import { formatDate } from '@vben/utils'; |
| | | |
| | | import { Spin } from 'ant-design-vue'; |
| | | |
| | |
| | | { |
| | | label: 'é¢è®¡ç¦»èæ¥æ', |
| | | field: 'expectedDate', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { label: '离èåå ', field: 'reason', render: textRender }, |
| | | { label: '交æ¥äºº', field: 'handoverUserName', render: textRender }, |
| | |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | ] as DescriptionItemSchema[], |
| | | }); |
| | |
| | | { |
| | | label: '交æ¥', |
| | | type: 'link', |
| | | auth: ['hrm:resignation:handover'], |
| | | auth: ['hrm:resignation:update'], |
| | | ifShow: row.status === 20 && row.handoverStatus !== 20, |
| | | popConfirm: { |
| | | title: row.handoverStatus === 0 ? '确认å¼å§äº¤æ¥åï¼' : 'ç¡®è®¤å®æäº¤æ¥åï¼', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'actions', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { formatDateTime } from '@vben/utils'; |
| | | import { formatDate } from '@vben/utils'; |
| | | |
| | | import { Spin } from 'ant-design-vue'; |
| | | |
| | |
| | | { |
| | | label: 'çææ¥æ', |
| | | field: 'effectiveDate', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { label: 'è°å²åå ', field: 'reason', render: textRender }, |
| | | { |
| | |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | ] as DescriptionItemSchema[], |
| | | }); |
| | |
| | | }) |
| | | } |
| | | |
| | | function $reset() { |
| | | reset() |
| | | clearGroupCallCache() |
| | | } |
| | | |
| | | return { |
| | | $reset, |
| | | stage, |
| | | call, |
| | | incomingPayload, |
| | |
| | | nextUnreadJumpNonce.value++ |
| | | } |
| | | |
| | | function $reset() { |
| | | Object.assign(userInfoCard, { |
| | | show: false, |
| | | user: null, |
| | | position: { x: 0, y: 0 }, |
| | | addSource: ImFriendAddSource.SEARCH, |
| | | addSourceExtra: '' |
| | | }) |
| | | Object.assign(groupInfoCard, { |
| | | show: false, |
| | | group: null, |
| | | position: { x: 0, y: 0 } |
| | | }) |
| | | Object.assign(contextMenu, { |
| | | show: false, |
| | | position: { x: 0, y: 0 }, |
| | | items: [], |
| | | onSelect: null |
| | | }) |
| | | nextUnreadJumpNonce.value = 0 |
| | | } |
| | | |
| | | return { |
| | | $reset, |
| | | userInfoCard, |
| | | openUserInfoCard, |
| | | openUserInfoCardAtEvent, |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | label: 'ç¹æ£æ¶é´', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | format: 'YYYY-MM-DD', |
| | | placeholder: 'è¯·éæ©ç¹æ£æ¶é´', |
| | | showTime: true, |
| | | showTime: false, |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | }, |
| | | rules: 'required', |
| | |
| | | field: 'checkTime', |
| | | title: 'ç¹æ£æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { field: 'nickname', title: 'ç¹æ£äºº', minWidth: 120 }, |
| | | { |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | componentProps: { |
| | | class: '!w-full', |
| | | disabled: true, |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | showTime: true, |
| | | format: 'YYYY-MM-DD', |
| | | showTime: false, |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | }, |
| | | dependencies: { |
| | |
| | | componentProps: { |
| | | class: '!w-full', |
| | | disabled: true, |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | showTime: true, |
| | | format: 'YYYY-MM-DD', |
| | | showTime: false, |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | }, |
| | | dependencies: { |
| | |
| | | field: 'lastCheckTime', |
| | | title: 'æè¿ç¹æ£æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'lastMaintenTime', |
| | | title: 'æè¿ä¿å
»æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 160, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | ]; |
| | | } |
| | |
| | | field: 'checkTime', |
| | | title: 'ç¹æ£æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { field: 'nickname', title: 'æä½äºº', minWidth: 120 }, |
| | | { |
| | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button, message, Upload } from 'ant-design-vue'; |
| | | import { Button, Upload } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | |
| | | importMachineryTemplate, |
| | | } from '#/api/mes/dv/machinery'; |
| | | import { $t } from '#/locales'; |
| | | import { notifyImportResult } from '#/utils'; |
| | | |
| | | import { useImportFormSchema } from '../data'; |
| | | |
| | |
| | | try { |
| | | const result = await importMachinery(data.file, data.updateSupport); |
| | | const importData = result as MesDvMachineryApi.MachineryImportRespVO; |
| | | let text = `ä¸ä¼ æåæ°éï¼${importData.createCodes?.length || 0}ï¼`; |
| | | text += `æ´æ°æåæ°éï¼${importData.updateCodes?.length || 0}ï¼`; |
| | | text += `æ´æ°å¤±è´¥æ°éï¼${Object.keys(importData.failureCodes || {}).length}ï¼`; |
| | | message.info(text); |
| | | // å
³éå¹¶æç¤º |
| | | const createCount = importData.createCodes?.length || 0; |
| | | const updateCount = importData.updateCodes?.length || 0; |
| | | |
| | | // å
³éå¹¶æå¯¼å
¥ç»ææç¤ºï¼ä»
å¨ç¡®ææ°æ®åå
¥æ¶éç¥åè¡¨å·æ° |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | if (createCount + updateCount > 0) { |
| | | emit('success'); |
| | | } |
| | | notifyImportResult({ |
| | | subject: '设å¤', |
| | | createCount, |
| | | updateCount, |
| | | failures: importData.failureCodes, |
| | | masterDataHint: 'ã设å¤ç±»åãã车é´ããå·¥ä½ç«ãä¸ç对åºç¼ç ', |
| | | }); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | field: 'maintenTime', |
| | | title: 'ä¿å
»æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { field: 'nickname', title: 'æä½äºº', minWidth: 120 }, |
| | | { |
| | |
| | | field: 'requireDate', |
| | | title: 'æ¥ä¿®æ¥æ', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { field: 'nickname', title: 'æä½äºº', minWidth: 120 }, |
| | | { |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | label: 'ä¿å
»æ¶é´', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | format: 'YYYY-MM-DD', |
| | | placeholder: 'è¯·éæ©ä¿å
»æ¶é´', |
| | | showTime: true, |
| | | showTime: false, |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | }, |
| | | rules: 'required', |
| | |
| | | field: 'maintenTime', |
| | | title: 'ä¿å
»æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { field: 'nickname', title: 'ä¿å
»äºº', minWidth: 120 }, |
| | | { |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import { DvMeteringSelect } from '#/views/mes/dv/metering/components'; |
| | | |
| | | /** 表åç±»å */ |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'checkUserId', |
| | | fieldName: 'checkUserName', |
| | | label: 'æ£æµäºº', |
| | | component: 'ApiSelect', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | api: getSimpleUserList, |
| | | labelField: 'nickname', |
| | | placeholder: 'è¯·éæ©æ£æµäºº', |
| | | valueField: 'id', |
| | | placeholder: '请è¾å
¥æ£æµäººå§å', |
| | | maxlength: 100, |
| | | }, |
| | | }, |
| | | { |
| | |
| | | }, |
| | | }, |
| | | { field: 'certNo', title: 'è¯ä¹¦ç¼å·', minWidth: 130 }, |
| | | { field: 'checkUserNickname', title: 'æ£æµäºº', minWidth: 100 }, |
| | | { field: 'checkUserName', title: 'æ£æµäºº', minWidth: 100 }, |
| | | { |
| | | field: 'nextCheckDate', |
| | | title: '䏿¬¡æ£æµæ¥æ', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'requireDate', |
| | | title: 'æ¥ä¿®æ¥æ', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'finishDate', |
| | | title: 'å®ææ¥æ', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'result', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | import type { MesDvRepairApi } from '#/api/mes/dv/repair'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | import { useAccess } from '@vben/access'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { MesDvRepairResultEnum, MesDvRepairStatusEnum } from '@vben/constants'; |
| | |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | import LineList from './line-list.vue'; |
| | | import SpareRecords from '../../spare/modules/records.vue'; |
| | | const { hasAccessByCodes } = useAccess(); |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formType = ref<FormType>('create'); |
| | |
| | | :disabled="isLineReadonly" |
| | | :repair-id="formData.id" |
| | | /> |
| | | <SpareRecords |
| | | v-if="formData?.id && hasAccessByCodes(['mes:dv-spare:record'])" |
| | | :key="formData.id" |
| | | :repair-id="formData.id" |
| | | :repair-code="formData.code" |
| | | :can-issue="[0, 1].includes(formData.status ?? -1)" |
| | | /> |
| | | <template #prepend-footer> |
| | | <div class="flex flex-auto items-center gap-2"> |
| | | <Popconfirm |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup lang="ts"> |
| | | import { Page } from '@vben/common-ui'; |
| | | import Records from '../spare/modules/records.vue'; |
| | | </script> |
| | | <template><Page><Records /></Page></template> |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup lang="ts"> |
| | | import type { SparePart } from '#/api/mes/dv/spare'; |
| | | import { onMounted, reactive, ref } from 'vue'; |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { Alert, Button, Checkbox, Form, Input, Popconfirm, Select, Space, Table, Tag } from 'ant-design-vue'; |
| | | import { deleteSpare, getSparePage } from '#/api/mes/dv/spare'; |
| | | import SpareForm from './modules/form.vue'; |
| | | import Action from './modules/action.vue'; |
| | | import Records from './modules/records.vue'; |
| | | const rows=ref<SparePart[]>([]),loading=ref(false),error=ref(''); |
| | | const page=ref(1),size=ref(10),total=ref(0),selected=ref<SparePart>(); |
| | | const filters=reactive<{code?:string;name?:string;status?:number;shortageOnly:boolean}>({shortageOnly:false}); |
| | | const [FormModal,formApi]=useVbenModal({connectedComponent:SpareForm,destroyOnClose:true}); |
| | | const [ActionModal,actionApi]=useVbenModal({connectedComponent:Action,destroyOnClose:true}); |
| | | const [RecordsModal,recordsApi]=useVbenModal({showConfirmButton:false}); |
| | | let seq=0; |
| | | const columns=[{title:'å¤ä»¶ç¼ç ',dataIndex:'code',width:140},{title:'å¤ä»¶åç§°',dataIndex:'name',width:180}, |
| | | {title:'è§æ ¼åå·',dataIndex:'specification',width:160},{title:'åä½',dataIndex:'unit',width:80}, |
| | | {title:'å¯ç¨åºå',dataIndex:'stockQuantity',key:'stock',width:130},{title:'å®å
¨åºå',dataIndex:'safetyQuantity',width:110}, |
| | | {title:'åæ¾ä½ç½®',dataIndex:'location',width:150},{title:'ç¶æ',dataIndex:'status',key:'status',width:90}, |
| | | {title:'æä½',key:'action',width:290,fixed:'right' as const}]; |
| | | async function load(reset=false) { |
| | | if(reset)page.value=1;const current=++seq;loading.value=true;error.value=''; |
| | | try{const result=await getSparePage({pageNo:page.value,pageSize:size.value,...filters});if(current===seq){rows.value=result.list;total.value=result.total;}} |
| | | catch{if(current===seq){rows.value=[];total.value=0;error.value='å¤ä»¶å 载失败ï¼è¯·ç¹å»æ¥è¯¢éè¯';}} |
| | | finally{if(current===seq)loading.value=false;} |
| | | } |
| | | function reset(){Object.assign(filters,{code:undefined,name:undefined,status:undefined,shortageOnly:false});void load(true);} |
| | | function changePage(p:{current?:number;pageSize?:number}){page.value=p.current||1;size.value=p.pageSize||10;void load();} |
| | | function showForm(part?:SparePart){formApi.setData({id:part?.id}).open();} |
| | | function action(type:'issue'|'receive',part:SparePart){actionApi.setData({type,part}).open();} |
| | | function history(part:SparePart){selected.value=part;recordsApi.open();} |
| | | async function remove(part:SparePart){await deleteSpare(part.id);await load(true);} |
| | | onMounted(()=>load()); |
| | | </script> |
| | | <template> |
| | | <Page> |
| | | <FormModal @success="load(true)" /><ActionModal @success="load(true)" /> |
| | | <RecordsModal :title="selected ? selected.name+' · åºåä¸é¢ç¨è®°å½' : 'åºåä¸é¢ç¨è®°å½'" class="w-full lg:w-4/5"><Records v-if="selected" :key="selected.id" :spare-part-id="selected.id" /></RecordsModal> |
| | | <div class="mb-4 flex flex-wrap items-center justify-between gap-3"><h2 class="text-lg font-semibold">å¤ä»¶å°è´¦</h2><Button v-access:code="['mes:dv-spare:create']" type="primary" @click="showForm()">æ°å¢å¤ä»¶</Button></div> |
| | | <Form layout="inline" class="mb-4 flex flex-wrap gap-y-3"> |
| | | <Form.Item label="å¤ä»¶ç¼ç "><Input v-model:value="filters.code" placeholder="宿´å¤ä»¶ç¼ç " allow-clear @press-enter="load(true)" /></Form.Item> |
| | | <Form.Item label="å¤ä»¶åç§°"><Input v-model:value="filters.name" placeholder="è¾å
¥å¤ä»¶åç§°" allow-clear @press-enter="load(true)" /></Form.Item> |
| | | <Form.Item label="ç¶æ"><Select v-model:value="filters.status" class="w-28" allow-clear :options="[{label:'å¯ç¨',value:0},{label:'åç¨',value:1}]" /></Form.Item> |
| | | <Form.Item><Checkbox v-model:checked="filters.shortageOnly">ä»
çä½äºå®å
¨åºå</Checkbox></Form.Item> |
| | | <Form.Item><Space><Button type="primary" :loading="loading" @click="load(true)">æ¥è¯¢</Button><Button @click="reset">éç½®</Button></Space></Form.Item> |
| | | </Form> |
| | | <Alert v-if="error" class="mb-3" type="error" :message="error" /> |
| | | <Table :columns="columns" :data-source="rows" :loading="loading" row-key="id" :scroll="{x:1330}" :pagination="{current:page,pageSize:size,total,showSizeChanger:true}" @change="changePage"> |
| | | <template #bodyCell="{column,record}"> |
| | | <template v-if="column.key==='stock'"><span>{{ record.stockQuantity }}</span><Tag v-if="Number(record.stockQuantity)<Number(record.safetyQuantity)" color="warning" class="ml-2">ä¸è¶³</Tag></template> |
| | | <template v-else-if="column.key==='status'"><Tag :color="record.status===0?'success':'default'">{{ record.status===0?'å¯ç¨':'åç¨' }}</Tag></template> |
| | | <template v-else-if="column.key==='action'"><Space :size="0" wrap> |
| | | <Button v-access:code="['mes:dv-spare:update']" type="link" @click="showForm(record as SparePart)">ç¼è¾</Button> |
| | | <Button v-access:code="['mes:dv-spare:receive']" type="link" :disabled="record.status!==0" @click="action('receive',record as SparePart)">å
¥åº</Button> |
| | | <Button v-access:code="['mes:dv-spare:issue']" type="link" :disabled="record.status!==0 || Number(record.stockQuantity)<=0" @click="action('issue',record as SparePart)">é¢ç¨</Button> |
| | | <Button v-access:code="['mes:dv-spare:record']" type="link" @click="history(record as SparePart)">è®°å½</Button> |
| | | <Popconfirm title="确认å é¤å¤ä»¶ï¼ä»
æ åºå䏿 æµæ°´çå¤ä»¶å¯å é¤ã" @confirm="remove(record as SparePart)"><Button v-access:code="['mes:dv-spare:delete']" type="link" danger>å é¤</Button></Popconfirm> |
| | | </Space></template> |
| | | </template> |
| | | </Table> |
| | | </Page> |
| | | </template> |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup lang="ts"> |
| | | import type { SpareMovement, SparePart, SpareAction } from '#/api/mes/dv/spare'; |
| | | import type { FormInstance } from 'ant-design-vue'; |
| | | import { computed, reactive, ref } from 'vue'; |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { Alert, Form, Input, InputNumber, Select } from 'ant-design-vue'; |
| | | import { getSparePage, spareAction } from '#/api/mes/dv/spare'; |
| | | import { getRepairPage } from '#/api/mes/dv/repair'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | const emit = defineEmits(['success']); |
| | | const formRef = ref<FormInstance>(); |
| | | const type = ref<'issue'|'receive'|'return'>('issue'); |
| | | const saving = ref(false); |
| | | const loadError = ref(''); |
| | | const original = ref<SpareMovement>(); |
| | | const parts = ref<SparePart[]>([]); |
| | | const repairs = ref<{label:string;value:number}[]>([]); |
| | | const users = ref<{label:string;value:number}[]>([]); |
| | | const model = reactive<SpareAction>({ requestKey: '', quantity: 1, remark: '' }); |
| | | const fixedPart = ref(false), fixedRepair = ref(false); |
| | | let partSeq = 0, repairSeq = 0, openSeq = 0; |
| | | const title = computed(() => ({issue:'ç»´ä¿®é¢ç¨å¤ä»¶',receive:'å¤ä»¶å
¥åº',return:'éåæªä½¿ç¨å¤ä»¶'})[type.value]); |
| | | const chosenPart = computed(() => parts.value.find(p => p.id === model.sparePartId)); |
| | | const maxQuantity = computed(() => type.value === 'return' ? Number(original.value?.returnableQuantity ?? 0) : type.value === 'issue' ? Number(chosenPart.value?.stockQuantity ?? 0) : undefined); |
| | | const required = [{required:true,message:'è¯·å¡«åæ¤é¡¹'}]; |
| | | async function searchParts(name='') { |
| | | const seq = ++partSeq; |
| | | try { const result = await getSparePage({pageNo:1,pageSize:50,status:0,name}); if(seq === partSeq) parts.value=result.list; } |
| | | catch { if(seq===partSeq) loadError.value='å¤ä»¶å 载失败ï¼è¯·éæ°æå¼å¼¹çªåéè¯'; } |
| | | } |
| | | async function searchRepairs(code='') { |
| | | const seq=++repairSeq; |
| | | try { const result=await getRepairPage({pageNo:1,pageSize:50,code} as Parameters<typeof getRepairPage>[0]); |
| | | if(seq===repairSeq) repairs.value=result.list.filter(r=>r.status===0||r.status===1).map(r=>({value:r.id!,label:`${r.code} · ${r.machineryName || r.name || ''}`})); |
| | | } catch { if(seq===repairSeq) loadError.value='ç»´ä¿®åå 载失败ï¼è¯·éæ°æå¼å¼¹çªåéè¯'; } |
| | | } |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | onBeforeClose:()=>!saving.value, |
| | | async onOpenChange(open) { |
| | | const seq=++openSeq; |
| | | if(!open) { partSeq++; repairSeq++; return; } |
| | | const data=modalApi.getData<{type:'issue'|'receive'|'return';part?:SparePart;repairId?:number;repairCode?:string;original?:SpareMovement}>(); |
| | | type.value=data.type; original.value=data.original; loadError.value=''; |
| | | fixedPart.value=!!data.part||data.type==='return'; fixedRepair.value=!!data.repairId; |
| | | Object.assign(model,{requestKey:crypto.randomUUID(),sparePartId:data.part?.id,repairId:data.repairId,originalId:data.original?.id,receiverUserId:undefined,quantity:1,remark:''}); |
| | | parts.value=data.part?[data.part]:[]; repairs.value=[]; users.value=[]; formRef.value?.clearValidate(); |
| | | if(data.repairId) repairs.value=[{value:data.repairId,label:data.repairCode || String(data.repairId)}]; |
| | | modalApi.lock(); |
| | | try { |
| | | const jobs:Promise<unknown>[]=[]; |
| | | if(data.type!=='return'&&!data.part) jobs.push(searchParts()); |
| | | if(data.type==='issue'&&!data.repairId) jobs.push(searchRepairs()); |
| | | if(data.type==='issue') jobs.push(getSimpleUserList().then(list=>{if(seq===openSeq)users.value=list.filter(u=>u.id!==undefined).map(u=>({value:u.id!,label:u.nickname}));})); |
| | | await Promise.all(jobs); |
| | | } catch { if(seq===openSeq) loadError.value='åºç¡æ°æ®å 载失败ï¼è¯·éæ°æå¼å¼¹çªåéè¯'; } |
| | | finally { if(seq===openSeq) modalApi.unlock(); } |
| | | }, |
| | | async onConfirm() { |
| | | if(saving.value||loadError.value)return; |
| | | await formRef.value?.validate(); |
| | | saving.value=true;modalApi.lock(); |
| | | try { await spareAction(type.value,{...model}); emit('success');saving.value=false;await modalApi.close(); } |
| | | finally { saving.value=false;modalApi.unlock(); } |
| | | }, |
| | | }); |
| | | const quantityRules = [{required:true,message:'请è¾å
¥æ°é'}, {validator:async (_:unknown,value:number)=>{ |
| | | if(!Number.isFinite(value)||value<=0)throw new Error('æ°éå¿
须大äº0'); |
| | | if(maxQuantity.value!==undefined && value>maxQuantity.value) throw new Error(type.value==='return'?'è¶
è¿å¯éåæ°é':'è¶
è¿å½åå¯ç¨åºå'); |
| | | }}]; |
| | | </script> |
| | | <template> |
| | | <Modal :title="title" class="w-full sm:w-[600px]"> |
| | | <Alert v-if="loadError" :message="loadError" type="error" class="mb-4" /> |
| | | <Alert v-else class="mb-4" type="info" :message="type === 'issue' ? 'é¢ç¨ç«å³æ£ååºåï¼å¹¶å
³èå½åç»´ä¿®åãè¯·æ ¸å¯¹å¤ä»¶ãé¢ç¨äººåæ°éã' : type === 'return' ? 'ä»
éåæªä½¿ç¨çå¤ä»¶ï¼ç´¯è®¡éåä¸è½è¶
è¿åé¢ç¨æ°éã' : 'å
¥åºå¢å åºåå¹¶ä¿çè®°å½ï¼è¯·å¡«åè¡¥è´§æ¥æºæåæ®ç¼å·ã'" /> |
| | | <Form ref="formRef" :model="model" layout="vertical"> |
| | | <Form.Item v-if="type === 'return'" label="åé¢ç¨è®°å½"> |
| | | {{ original?.repairCode }} · {{ original?.sparePartName }} · å¯éå {{ original?.returnableQuantity }} {{ original?.unit }} |
| | | </Form.Item> |
| | | <Form.Item v-else label="å¤ä»¶" name="sparePartId" :rules="required"> |
| | | <Select v-model:value="model.sparePartId" show-search :disabled="fixedPart || saving" :filter-option="false" placeholder="è¾å
¥å¤ä»¶åç§°æ¥æ¾" :options="parts.map(p=>({value:p.id,label:p.code+' · '+p.name+' · åºå '+p.stockQuantity+' '+p.unit}))" @search="searchParts" /> |
| | | </Form.Item> |
| | | <Form.Item v-if="type === 'issue'" label="å
³èç»´ä¿®å" name="repairId" :rules="required"> |
| | | <Select v-model:value="model.repairId" :disabled="fixedRepair || saving" show-search :filter-option="false" :options="repairs" placeholder="è¾å
¥ç»´ä¿®åç¼å·æ¥æ¾ï¼è稿æç»´ä¿®ä¸ï¼" @search="searchRepairs" /> |
| | | </Form.Item> |
| | | <Form.Item v-if="type === 'issue'" label="é¢ç¨äºº" name="receiverUserId" :rules="required"> |
| | | <Select v-model:value="model.receiverUserId" show-search option-filter-prop="label" :options="users" placeholder="è¯·éæ©é¢ç¨äºº" :disabled="saving" /> |
| | | </Form.Item> |
| | | <Form.Item :label="'æ°é'+(chosenPart ? 'ï¼'+chosenPart.unit+'ï¼' : '')" name="quantity" :rules="quantityRules"> |
| | | <InputNumber v-model:value="model.quantity" :min="0.001" :precision="3" class="w-full" :disabled="saving" /> |
| | | </Form.Item> |
| | | <Form.Item label="æä½è¯´æ" name="remark" :rules="required"><Input.TextArea v-model:value="model.remark" :maxlength="500" :rows="3" :disabled="saving" placeholder="å¡«åé¢ç¨ç¨éãå
¥åºæ¥æºæéååå " /></Form.Item> |
| | | </Form> |
| | | </Modal> |
| | | </template> |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup lang="ts"> |
| | | import type { SpareSave } from '#/api/mes/dv/spare'; |
| | | import type { FormInstance } from 'ant-design-vue'; |
| | | import { reactive, ref } from 'vue'; |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { Alert, Form, Input, InputNumber, Select } from 'ant-design-vue'; |
| | | import { createSpare, getSpare, updateSpare } from '#/api/mes/dv/spare'; |
| | | const emit = defineEmits(['success']); |
| | | const formRef = ref<FormInstance>(); |
| | | const saving = ref(false); |
| | | const model = reactive<SpareSave>({ code: '', name: '', unit: 'ä»¶', initialQuantity: 0, safetyQuantity: 0, status: 0 }); |
| | | const required = [{ required: true, message: 'è¯·å¡«åæ¤é¡¹' }]; |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | onBeforeClose: () => !saving.value, |
| | | async onOpenChange(open) { |
| | | if (!open) return; |
| | | Object.assign(model, { id: undefined, code: '', name: '', specification: '', unit: 'ä»¶', location: '', initialQuantity: 0, safetyQuantity: 0, status: 0, remark: '' }); |
| | | formRef.value?.clearValidate(); |
| | | const { id } = modalApi.getData<{ id?: number }>(); |
| | | if (id) { modalApi.lock(); try { Object.assign(model, await getSpare(id)); } finally { modalApi.unlock(); } } |
| | | }, |
| | | async onConfirm() { |
| | | if (saving.value) return; |
| | | await formRef.value?.validate(); |
| | | saving.value = true; modalApi.lock(); |
| | | try { await (model.id ? updateSpare(model) : createSpare(model)); emit('success'); saving.value = false; await modalApi.close(); } |
| | | finally { saving.value = false; modalApi.unlock(); } |
| | | }, |
| | | }); |
| | | </script> |
| | | <template> |
| | | <Modal :title="model.id ? 'ç¼è¾å¤ä»¶' : 'æ°å¢å¤ä»¶'" class="w-full sm:w-[640px]"> |
| | | <Alert class="mb-4" type="info" :message="model.id ? 'åºåéè¿å
¥åºãé¢ç¨åéååæ´ãå·²ææµæ°´åä¸å¯ä¿®æ¹è®¡éåä½ã' : 'åå§åºåç»è®°åä¼ä¿çæµæ°´ï¼åç»è¡¥è´§è¯·ä½¿ç¨å
¥åºã'" /> |
| | | <Form ref="formRef" :model="model" layout="vertical" class="grid grid-cols-1 gap-x-4 sm:grid-cols-2"> |
| | | <Form.Item label="å¤ä»¶ç¼ç " name="code" :rules="required"><Input v-model:value="model.code" :maxlength="64" /></Form.Item> |
| | | <Form.Item label="å¤ä»¶åç§°" name="name" :rules="required"><Input v-model:value="model.name" :maxlength="128" /></Form.Item> |
| | | <Form.Item label="è§æ ¼åå·" name="specification"><Input v-model:value="model.specification" :maxlength="255" /></Form.Item> |
| | | <Form.Item label="计éåä½" name="unit" :rules="required"><Input v-model:value="model.unit" :maxlength="32" /></Form.Item> |
| | | <Form.Item label="åæ¾ä½ç½®" name="location"><Input v-model:value="model.location" :maxlength="128" /></Form.Item> |
| | | <Form.Item label="å®å
¨åºå" name="safetyQuantity" :rules="required"><InputNumber v-model:value="model.safetyQuantity" :min="0" :precision="3" class="w-full" /></Form.Item> |
| | | <Form.Item v-if="!model.id" label="åå§åºå" name="initialQuantity" :rules="required"><InputNumber v-model:value="model.initialQuantity" :min="0" :precision="3" class="w-full" /></Form.Item> |
| | | <Form.Item label="ç¶æ" name="status"><Select v-model:value="model.status" :options="[{label:'å¯ç¨',value:0},{label:'åç¨',value:1}]" /></Form.Item> |
| | | <Form.Item label="夿³¨" name="remark" class="sm:col-span-2"><Input.TextArea v-model:value="model.remark" :maxlength="500" :rows="3" /></Form.Item> |
| | | </Form> |
| | | </Modal> |
| | | </template> |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup lang="ts"> |
| | | import type { SpareMovement } from '#/api/mes/dv/spare'; |
| | | import { onMounted, reactive, ref, watch } from 'vue'; |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { Alert, Button, Form, Input, Select, Space, Table } from 'ant-design-vue'; |
| | | import { getSpareMovements, movementLabels } from '#/api/mes/dv/spare'; |
| | | import Action from './action.vue'; |
| | | const props=defineProps<{repairId?:number;sparePartId?:number;canIssue?:boolean;repairCode?:string}>(); |
| | | const rows=ref<SpareMovement[]>([]),loading=ref(false),error=ref(''); |
| | | const page=ref(1),size=ref(10),total=ref(0); |
| | | const filters=reactive<{repairCode?:string;type?:number}>({type:3}); |
| | | let seq=0; |
| | | const [ActionModal,actionApi]=useVbenModal({connectedComponent:Action,destroyOnClose:true}); |
| | | const columns=[ |
| | | {title:'æä½æ¶é´',dataIndex:'createTime',key:'createTime',width:175}, |
| | | {title:'ç±»å',dataIndex:'type',key:'type',width:95}, |
| | | {title:'å¤ä»¶ç¼ç ',dataIndex:'sparePartCode',width:135}, |
| | | {title:'å¤ä»¶åç§°',dataIndex:'sparePartName',width:150}, |
| | | {title:'è§æ ¼åå·',dataIndex:'specification',width:130}, |
| | | {title:'æ°é / åä½',key:'quantity',width:110}, |
| | | {title:'å·²éå',dataIndex:'returnedQuantity',width:90}, |
| | | {title:'å¯éå',dataIndex:'returnableQuantity',width:90}, |
| | | {title:'ç»´ä¿®åå·',dataIndex:'repairCode',width:145}, |
| | | {title:'设å¤',dataIndex:'machineryName',width:150}, |
| | | {title:'é¢ç¨äºº',dataIndex:'receiverName',width:110}, |
| | | {title:'æä½è¯´æ',dataIndex:'remark',width:220}, |
| | | {title:'æä½',key:'action',width:90,fixed:'right' as const}, |
| | | ]; |
| | | async function load(reset=false) { |
| | | if(reset)page.value=1; |
| | | const current=++seq;loading.value=true;error.value=''; |
| | | try {const result=await getSpareMovements({pageNo:page.value,pageSize:size.value,...filters,repairId:props.repairId,sparePartId:props.sparePartId}); |
| | | if(current===seq){rows.value=result.list;total.value=result.total;}} |
| | | catch {if(current===seq){rows.value=[];total.value=0;error.value='è®°å½å 载失败ï¼è¯·ç¹å»æ¥è¯¢éè¯';}} |
| | | finally {if(current===seq)loading.value=false;} |
| | | } |
| | | function changePage(p:{current?:number;pageSize?:number}){page.value=p.current||1;size.value=p.pageSize||10;void load();} |
| | | function issue(){actionApi.setData({type:'issue',repairId:props.repairId,repairCode:props.repairCode}).open();} |
| | | function returnPart(record:SpareMovement){actionApi.setData({type:'return',original:record}).open();} |
| | | watch(()=>[props.repairId,props.sparePartId],()=>load(true)); |
| | | onMounted(()=>load()); |
| | | defineExpose({refresh:()=>load(true)}); |
| | | </script> |
| | | <template> |
| | | <div class="mt-4"> |
| | | <ActionModal @success="load(true)" /> |
| | | <div class="mb-3 flex flex-wrap items-center justify-between gap-3"><h3 class="text-base font-semibold">å¤ä»¶é¢ç¨è®°å½</h3> |
| | | <Button v-if="canIssue" v-access:code="['mes:dv-spare:issue']" type="primary" @click="issue">é¢ç¨å¤ä»¶</Button> |
| | | </div> |
| | | <Form layout="inline" class="mb-4 flex flex-wrap gap-y-3"> |
| | | <Form.Item v-if="!repairId" label="ç»´ä¿®åå·"><Input v-model:value="filters.repairCode" allow-clear placeholder="宿´ç»´ä¿®åå·" @press-enter="load(true)" /></Form.Item> |
| | | <Form.Item label="è®°å½ç±»å"><Select v-model:value="filters.type" allow-clear class="w-36" :options="Object.entries(movementLabels).map(([value,label])=>({label,value:Number(value)}))" /></Form.Item> |
| | | <Form.Item><Space><Button :loading="loading" @click="load(true)">æ¥è¯¢</Button><Button @click="filters.repairCode=undefined;filters.type=3;load(true)">éç½®</Button></Space></Form.Item> |
| | | </Form> |
| | | <Alert v-if="error" type="error" :message="error" class="mb-3" /> |
| | | <Table :columns="columns" :data-source="rows" :loading="loading" row-key="id" :scroll="{x:1790}" :pagination="{current:page,pageSize:size,total,showSizeChanger:true}" @change="changePage"> |
| | | <template #bodyCell="{column,record}"> |
| | | <template v-if="column.key==='type'">{{ movementLabels[record.type] }}</template> |
| | | <template v-else-if="column.key==='quantity'">{{ record.quantity }} {{ record.unit }}</template> |
| | | <template v-else-if="column.key==='action'"><Button v-if="record.type===3 && Number(record.returnableQuantity)>0" v-access:code="['mes:dv-spare:return']" type="link" @click="returnPart(record as SpareMovement)">éå</Button><span v-else>â</span></template> |
| | | </template> |
| | | </Table> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | /** å¾
åæéåè¡¨ï¼æ ç¾ãæè¿°ã徿 ãç®æ è·¯ç±åç§°ãæ°é */ |
| | | const alertItems = computed(() => [ |
| | | { |
| | | count: props.summary.andonActiveCount, |
| | | desc: 'æªå¤ç½®çå®ç¯å¼å«', |
| | | icon: 'lucide:bell-ring', |
| | | iconClass: 'bg-red-50 text-red-500', |
| | | label: 'å®ç¯æ¥è¦', |
| | | routeName: 'MesProAndonRecord', |
| | | }, |
| | | { |
| | | count: props.summary.repairActiveCount, |
| | | desc: 'å¾
å¤çç维修订å', |
| | | icon: 'lucide:wrench', |
| | |
| | | navigate: [name: string]; |
| | | }>(); |
| | | |
| | | /** æ¯å¦æè´¨éæ°æ®ï¼åæ ¼å + ä¸è¯å > 0ï¼ */ |
| | | const hasQualityData = computed( |
| | | () => |
| | | props.summary.todayQualifiedQuantity + |
| | | props.summary.todayUnqualifiedQuantity > |
| | | 0, |
| | | ); |
| | | /** |
| | | * è´¨éåæ ¼çï¼æ¥æ / è¿ç¨ / åºè´§ä¸ä¸ªæ£éªç¯èï¼åèªä¸å¼ å¡ç |
| | | * |
| | | * ç±å端æãåæ ¼æ°é / æ£éªæ°éã计ç®ï¼ä¸è¿æ»¤åæ®ç¶æï¼æ æ¶é´éå¶ï¼ï¼ |
| | | * 该ç¯èæ²¡ææ£éªæ°æ®æ¶å端è¿å nullã |
| | | */ |
| | | const qualityRates = computed(() => [ |
| | | { |
| | | label: 'æ¥æåæ ¼ç', |
| | | rate: props.summary.iqcQualifiedRate, |
| | | checkQuantity: props.summary.iqcCheckQuantity, |
| | | routeName: 'MesQcIqc', |
| | | icon: 'lucide:package-check', |
| | | color: '#e6a23c', |
| | | colorLight: '#ebb563', |
| | | }, |
| | | { |
| | | label: 'è¿ç¨åæ ¼ç', |
| | | rate: props.summary.ipqcQualifiedRate, |
| | | checkQuantity: props.summary.ipqcCheckQuantity, |
| | | routeName: 'MesQcIpqc', |
| | | icon: 'lucide:workflow', |
| | | color: '#13c2c2', |
| | | colorLight: '#5cdbd3', |
| | | }, |
| | | { |
| | | label: 'åºè´§åæ ¼ç', |
| | | rate: props.summary.oqcQualifiedRate, |
| | | checkQuantity: props.summary.oqcCheckQuantity, |
| | | routeName: 'MesQcOqc', |
| | | icon: 'lucide:truck', |
| | | color: '#eb2f96', |
| | | colorLight: '#f759ab', |
| | | }, |
| | | ]); |
| | | |
| | | /** è´¨éåæ ¼ç = åæ ¼å / (åæ ¼å + ä¸è¯å) * 100ï¼æ æ°æ®æ¶ä¸º 0 */ |
| | | const qualityRate = computed(() => { |
| | | const total = |
| | | props.summary.todayQualifiedQuantity + |
| | | props.summary.todayUnqualifiedQuantity; |
| | | if (total === 0) { |
| | | return 0; |
| | | } |
| | | return (props.summary.todayQualifiedQuantity / total) * 100; |
| | | }); |
| | | /** åæ ¼çå±ç¤ºææ¡ï¼ä¸ä½å°æ°ï¼ä¸åãè´¨éåæ ¼çãå¡çä¸è´ï¼ï¼æ æ°æ®æ¶ç¨ãâãè䏿¯ 0% */ |
| | | function formatRate(rate: null | number | undefined) { |
| | | return rate === null || rate === undefined ? 'â' : rate.toFixed(1); |
| | | } |
| | | |
| | | /** æ æ£éªæ°æ®æ¶ä¸æ¸²æç¾åå·ï¼é¿å
åºç°ãâ%ã */ |
| | | function hasRate(rate: null | number | undefined) { |
| | | return rate !== null && rate !== undefined; |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <Row :gutter="16"> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="4" :xs="24" class="mb-4"> |
| | | <Card |
| | | class="kpi-card cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg" |
| | | class="kpi-card h-full cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg" |
| | | @click="emit('navigate', 'MesProWorkOrder')" |
| | | > |
| | | <div class="flex items-center gap-4"> |
| | |
| | | </div> |
| | | </Card> |
| | | </Col> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="4" :xs="24" class="mb-4"> |
| | | <Card |
| | | class="kpi-card cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg" |
| | | class="kpi-card h-full cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg" |
| | | @click="emit('navigate', 'MesProFeedback')" |
| | | > |
| | | <div class="flex items-center gap-4"> |
| | |
| | | </div> |
| | | </Card> |
| | | </Col> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col |
| | | v-for="item in qualityRates" |
| | | :key="item.label" |
| | | :lg="6" |
| | | :md="12" |
| | | :sm="12" |
| | | :xl="4" |
| | | :xs="24" |
| | | class="mb-4" |
| | | > |
| | | <Card |
| | | class="kpi-card cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg" |
| | | @click="emit('navigate', 'MesProFeedback')" |
| | | class="kpi-card h-full cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg" |
| | | @click="emit('navigate', item.routeName)" |
| | | > |
| | | <div class="flex items-center gap-4"> |
| | | <div |
| | | class="flex size-14 flex-shrink-0 items-center justify-center rounded-xl text-white" |
| | | style="background: linear-gradient(135deg, #e6a23c, #ebb563)" |
| | | :style="{ background: `linear-gradient(135deg, ${item.color}, ${item.colorLight})` }" |
| | | > |
| | | <IconifyIcon class="size-7" icon="lucide:circle-check" /> |
| | | <IconifyIcon class="size-7" :icon="item.icon" /> |
| | | </div> |
| | | <div> |
| | | <div class="text-muted-foreground mb-1 text-sm">è´¨éåæ ¼ç</div> |
| | | <div class="text-muted-foreground mb-1 text-sm">{{ item.label }}</div> |
| | | <div class="flex items-baseline gap-1"> |
| | | <CountTo |
| | | class="text-2xl font-bold leading-tight text-[#e6a23c]" |
| | | :decimals="1" |
| | | :end-val="qualityRate" |
| | | :duration="1500" |
| | | /> |
| | | <span class="text-muted-foreground text-xs">%</span> |
| | | <span |
| | | class="text-2xl font-bold leading-tight" |
| | | :style="{ color: item.color }" |
| | | > |
| | | {{ formatRate(item.rate) }} |
| | | </span> |
| | | <span |
| | | v-if="hasRate(item.rate)" |
| | | class="text-muted-foreground text-xs" |
| | | > |
| | | % |
| | | </span> |
| | | </div> |
| | | <div class="text-muted-foreground mt-1 text-xs"> |
| | | <template v-if="hasQualityData"> |
| | | <span>åæ ¼ {{ summary.todayQualifiedQuantity }}</span> |
| | | <Divider type="vertical" /> |
| | | <span>ä¸è¯ {{ summary.todayUnqualifiedQuantity }}</span> |
| | | </template> |
| | | <span v-else>ææ æ°æ®</span> |
| | | <span>æ£éª {{ item.checkQuantity }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </Card> |
| | | </Col> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="4" :xs="24" class="mb-4"> |
| | | <Card |
| | | class="kpi-card cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg" |
| | | class="kpi-card h-full cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg" |
| | | @click="emit('navigate', 'MesDvMachinery')" |
| | | > |
| | | <div class="flex items-center gap-4"> |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | <Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false"> |
| | | <template #title> |
| | | <div v-if="selectedItem" class="leading-6"> |
| | | <div>ç¼ç ï¼{{ selectedItem.code || '-' }}</div> |
| | | <div>åç§°ï¼{{ selectedItem.name || '-' }}</div> |
| | | <div>ç®ç§°ï¼{{ selectedItem.nickname || '-' }}</div> |
| | | <div>çµè¯ï¼{{ selectedItem.telephone || '-' }}</div> |
| | |
| | | export function useClientSelectGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'code', |
| | | label: '客æ·ç¼ç ', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥å®¢æ·ç¼ç ', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'name', |
| | | label: '客æ·åç§°', |
| | | component: 'Input', |
| | |
| | | ): VxeTableGridOptions<MesMdClientApi.Client>['columns'] { |
| | | return [ |
| | | { type: multiple ? 'checkbox' : 'radio', width: 50 }, |
| | | { |
| | | field: 'code', |
| | | title: '客æ·ç¼ç ', |
| | | minWidth: 160, |
| | | }, |
| | | { |
| | | field: 'name', |
| | | title: '客æ·åç§°', |
| | |
| | | import type { MesMdItemApi } from "#/api/mes/md/item"; |
| | | import type { MesMdItemTypeApi } from "#/api/mes/md/item/type"; |
| | | |
| | | import { nextTick, ref } from "vue"; |
| | | import { computed, nextTick, ref } from "vue"; |
| | | |
| | | import { CommonStatusEnum } from "@vben/constants"; |
| | | |
| | | import { Card, message, Modal } from "ant-design-vue"; |
| | | import { Alert, Card, message, Modal, Tag } from "ant-design-vue"; |
| | | |
| | | import { useVbenVxeGrid } from "#/adapter/vxe-table"; |
| | | import { getItemPage } from "#/api/mes/md/item"; |
| | |
| | | const selectedItemTypeId = ref<number>(); // å½åçéåç±»ç¼å· |
| | | const preSelectedIds = ref<number[]>([]); // é¢éç©æç¼å·å表 |
| | | const productIds = ref<number[]>([]); // éå®å¯éçç©æç¼å·å表ï¼ä¸ºç©ºåä¸éå¶ï¼ |
| | | const limitedToProducts = computed(() => productIds.value.length > 0); |
| | | const dialogTitle = computed(() => limitedToProducts.value ? "路线产åºéæ©" : "ç©æäº§åéæ©"); |
| | | const tableTitle = computed(() => limitedToProducts.value ? "å·²é
ç½®ç产åºäº§å" : "ç©æäº§åå表"); |
| | | const typeTreeRef = ref<InstanceType<typeof MdItemTypeTree>>(); // ç©æåç±»æ |
| | | |
| | | /** è·åå½åè¡¨æ ¼æ°æ® */ |
| | |
| | | }, |
| | | gridOptions: { |
| | | columns: useItemSelectGridColumns(true), |
| | | height: 560, |
| | | height: '100%', |
| | | keepSource: true, |
| | | checkboxConfig: { |
| | | highlight: true, |
| | |
| | | |
| | | <template> |
| | | <Modal v-model:open="open" |
| | | title="ç©æäº§åéæ©" |
| | | :title="dialogTitle" |
| | | width="80%" |
| | | :destroy-on-close="true" |
| | | @ok="handleConfirm" |
| | | @cancel="closeModal"> |
| | | <div class="flex h-full w-full"> |
| | | <Card class="mr-4 h-full w-1/5"> |
| | | <MdItemTypeTree ref="typeTreeRef" |
| | | @node-click="handleItemTypeNodeClick" /> |
| | | </Card> |
| | | <div class="w-4/5"> |
| | | <Grid table-title="ç©æäº§åå表" /> |
| | | <div class="flex h-[calc(100vh-300px)] min-h-[380px] w-full flex-col"> |
| | | <Alert |
| | | v-if="limitedToProducts" |
| | | class="mb-3 shrink-0" |
| | | message="ä»
å¯éæ©æ¬äº§åå¨å·¥èºè·¯çº¿ä¸å·²é
ç½®ç产åºç©æ" |
| | | description="å¦éå¢å å¯é项ï¼è¯·å
å¨å·¥èºè·¯çº¿å¯¹åºå·¥åºä¸é
置产åºå¹¶ä¿åã" |
| | | show-icon |
| | | type="info" |
| | | > |
| | | <template #action><Tag color="blue">{{ productIds.length }} 个产åº</Tag></template> |
| | | </Alert> |
| | | <div class="flex min-h-0 w-full flex-1 gap-4"> |
| | | <Card v-if="!limitedToProducts" class="h-full w-1/5 shrink-0" :body-style="{ height: '100%', overflow: 'hidden' }"> |
| | | <MdItemTypeTree ref="typeTreeRef" @node-click="handleItemTypeNodeClick" /> |
| | | </Card> |
| | | <div class="h-full min-h-0 min-w-0 flex-1"> |
| | | <Grid class="h-full" :table-title="tableTitle" /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </Modal> |
| | |
| | | |
| | | const displayLabel = computed(() => { |
| | | if (selectedItems.value.length === 0) return ''; |
| | | return selectedItems.value.map(item => item.name ?? '').join('ã'); |
| | | return selectedItems.value.map(item => |
| | | [item.name, item.specification, item.code ? `ï¼${item.code}ï¼` : ''].filter(Boolean).join(' '), |
| | | ).join('ã'); |
| | | }); |
| | | const showClear = computed( |
| | | () => |
| | |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥ç©æåç§°', |
| | | placeholder: 'æ¯æåç§°ä¸è§æ ¼ç»åæ¥è¯¢', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'specification', |
| | | label: 'è§æ ¼åå·', |
| | | component: 'Input', |
| | | componentProps: { allowClear: true, placeholder: '请è¾å
¥è§æ ¼åå·' }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥ç©æåç§°', |
| | | placeholder: 'æ¯æåç§°ä¸è§æ ¼ç»åæ¥è¯¢', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'specification', |
| | | label: 'è§æ ¼åå·', |
| | | component: 'Input', |
| | | componentProps: { allowClear: true, placeholder: '请è¾å
¥è§æ ¼åå·' }, |
| | | }, |
| | | ]; |
| | | } |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | ]; |
| | | } |
| | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button, message, Upload } from 'ant-design-vue'; |
| | | import { Button, Upload } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { importItem, importItemTemplate } from '#/api/mes/md/item'; |
| | | import { $t } from '#/locales'; |
| | | import { notifyImportResult } from '#/utils'; |
| | | |
| | | import { useImportFormSchema } from '../data'; |
| | | |
| | |
| | | try { |
| | | const result = await importItem(data.file, data.updateSupport); |
| | | const importData = result as MesMdItemApi.ItemImportRespVO; |
| | | let text = `ä¸ä¼ æåæ°éï¼${importData.createCodes?.length || 0};`; |
| | | for (const code of importData.createCodes || []) { |
| | | text += `< ${code} >`; |
| | | } |
| | | text += `æ´æ°æåæ°éï¼${importData.updateCodes?.length || 0};`; |
| | | for (const code of importData.updateCodes || []) { |
| | | text += `< ${code} >`; |
| | | } |
| | | text += `æ´æ°å¤±è´¥æ°éï¼${Object.keys(importData.failureCodes || {}).length};`; |
| | | for (const code in importData.failureCodes || {}) { |
| | | text += `< ${code}: ${importData.failureCodes?.[code]} >`; |
| | | } |
| | | message.info(text); |
| | | // å
³éå¹¶æç¤º |
| | | const createCount = importData.createCodes?.length || 0; |
| | | const updateCount = importData.updateCodes?.length || 0; |
| | | |
| | | // å
³éå¹¶æå¯¼å
¥ç»ææç¤ºï¼ä»
å¨ç¡®ææ°æ®åå
¥æ¶éç¥åè¡¨å·æ° |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | if (createCount + updateCount > 0) { |
| | | emit('success'); |
| | | } |
| | | notifyImportResult({ |
| | | subject: 'ç©æ', |
| | | createCount, |
| | | updateCount, |
| | | failures: importData.failureCodes, |
| | | masterDataHint: 'ãç©æåç±»ãä¸ç对åºç¼ç ', |
| | | }); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | <div v-if="selectedItem" class="leading-6"> |
| | | <div>ç¼ç ï¼{{ selectedItem.code || '-' }}</div> |
| | | <div>åç§°ï¼{{ selectedItem.name || '-' }}</div> |
| | | <div> |
| | | é¢ç§¯ï¼ |
| | | {{ selectedItem.area !== null ? `${selectedItem.area} ã¡` : '-' }} |
| | | </div> |
| | | <div>è´è´£äººï¼{{ selectedItem.chargeUserName || '-' }}</div> |
| | | </div> |
| | | </template> |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'area', |
| | | label: 'é¢ç§¯', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | min: 0, |
| | | precision: 2, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'chargeUserId', |
| | | label: 'è´è´£äºº', |
| | | component: 'ApiSelect', |
| | |
| | | field: 'name', |
| | | title: '车é´åç§°', |
| | | minWidth: 150, |
| | | }, |
| | | { |
| | | field: 'area', |
| | | title: 'é¢ç§¯', |
| | | width: 120, |
| | | }, |
| | | { |
| | | field: 'chargeUserName', |
| | |
| | | field: 'archiveTime', |
| | | title: '彿¡£æ¶é´', |
| | | minWidth: 160, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { field: 'updaterNickname', title: 'æ´æ°äºº', minWidth: 100 }, |
| | | { |
| | | field: 'updateTime', |
| | | title: 'æ´æ°æ¶é´', |
| | | minWidth: 160, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | |
| | | |
| | |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * èµææä»¶å°åï¼å端åªå attachmentListï¼éä»¶ä¸é´è¡¨ï¼ï¼ä¸å¡è¡¨æ²¡æ fileUrl åæ®µï¼ |
| | | * è¿éå第ä¸ä¸ªéä»¶ç访é®å°åï¼æ²¡æéä»¶å°±è¿åç©ºä¸²ï¼æ¨¡æ¿éæ¾ç¤ºã-ãï¼ã |
| | | */ |
| | | function documentFileUrl(record: MesPdArchiveApi.DocumentInfo) { |
| | | const blob = record.attachmentList?.[0]; |
| | | return blob?.url || blob?.downloadURL || ''; |
| | | } |
| | | |
| | | /** 彿¡£ç¶æä¸æåï¼åå
¸ mes_pd_archive_status */ |
| | | function archiveStatusText(value?: number) { |
| | | if (value === undefined || value === null || value === '') return '-'; |
| | |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.key === 'action'"> |
| | | <template v-if="record.fileUrl"> |
| | | <a class="mr-2" @click="openPreview(record.fileUrl, record.fileName)">é¢è§</a> |
| | | <a :href="record.fileUrl" target="_blank">ä¸è½½</a> |
| | | <template v-if="documentFileUrl(record)"> |
| | | <a class="mr-2" @click="openPreview(documentFileUrl(record), record.fileName)">é¢è§</a> |
| | | <a :href="documentFileUrl(record)" target="_blank">ä¸è½½</a> |
| | | </template> |
| | | <span v-else class="text-gray-400">-</span> |
| | | </template> |
| | |
| | | field: "createTime", |
| | | title: "å建æ¶é´", |
| | | minWidth: 160, |
| | | formatter: "formatDateTime", |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: "action", |
| | |
| | | field: "uploadTime", |
| | | title: "ä¸ä¼ æ¶é´", |
| | | minWidth: 160, |
| | | formatter: "formatDateTime", |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: "action", |
| | |
| | | import { h } from 'vue'; |
| | | |
| | | import { DICT_TYPE } from '#/packages/constants/src'; |
| | | import { formatDate, formatDateTime } from '#/packages/utils/src'; |
| | | import { formatDate } from '#/packages/utils/src'; |
| | | |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | |
| | |
| | | { |
| | | field: 'createTime', |
| | | label: 'å建æ¶é´', |
| | | render: (val) => (val ? formatDateTime(val) : '-'), |
| | | render: (val) => (val ? formatDate(val) : '-'), |
| | | }, |
| | | { field: 'remark', label: '夿³¨' }, |
| | | ]; |
| | |
| | | <script lang="ts" setup> |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import { submitPdProjectApproval } from '#/api/mes/pd/project'; |
| | | import type { FormType } from '../data'; |
| | | import type { MesPdProjectApi } from '#/api/mes/pd/project'; |
| | | |
| | |
| | | |
| | | /** æäº¤å®¡æ¹ - å
è·åæµç¨å表 */ |
| | | async function handleSubmit() { |
| | | const hideLoading = message.loading({ content: 'è·åå®¡æ¹æµç¨...', duration: 0 }); |
| | | const hideLoading = message.loading({ content: automaticApproval.value ? 'æ£å¨æäº¤çæ...' : 'è·åå®¡æ¹æµç¨...', duration: 0 }); |
| | | try { |
| | | if (automaticApproval.value) { |
| | | await submitPdProjectApproval(projectId.value, 'pd_project_approve'); |
| | | hideLoading(); |
| | | message.success('å·²æäº¤çæ'); |
| | | await loadProjectDetail(); |
| | | return; |
| | | } |
| | | const processList = await getPdProjectApproveProcessList(); |
| | | hideLoading(); |
| | | if (!processList || processList.length === 0) { |
| | |
| | | return; |
| | | } |
| | | // æå¼æµç¨éæ©å¼¹çª |
| | | |
| | | processModalApi.setData({ id: projectId.value, processList }).open(); |
| | | } catch { |
| | | hideLoading(); |
| | |
| | | <script lang="ts" setup> |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import { submitPdProjectApproval } from '#/api/mes/pd/project'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesPdProjectApi } from '#/api/mes/pd/project'; |
| | | |
| | |
| | | /** æäº¤å®¡æ¹ - å
è·åæµç¨å表 */ |
| | | async function handleSubmit(row: MesPdProjectApi.Project) { |
| | | const hideLoading = message.loading({ |
| | | content: 'è·åå®¡æ¹æµç¨...', |
| | | content: automaticApproval.value ? 'æ£å¨æäº¤çæ...' : 'è·åå®¡æ¹æµç¨...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | if (automaticApproval.value) { |
| | | await submitPdProjectApproval(row.id!, 'pd_project_approve'); |
| | | hideLoading(); |
| | | message.success('å·²æäº¤çæ'); |
| | | handleRefresh(); |
| | | return; |
| | | } |
| | | const processList = await getPdProjectApproveProcessList(); |
| | | hideLoading(); |
| | | if (!processList || processList.length === 0) { |
| | |
| | | return; |
| | | } |
| | | // æå¼æµç¨éæ©å¼¹çª |
| | | |
| | | processModalApi.setData({ id: row.id, processList }).open(); |
| | | } catch { |
| | | hideLoading(); |
| | |
| | | { |
| | | label: '彿¡£', |
| | | type: 'link', |
| | | auth: ['mes:pd-project:archive'], |
| | | auth: ['mes:pd-project:update'], |
| | | ifShow: row.status === MesPdProjectStatusEnum.PENDING_ARCHIVE, |
| | | popConfirm: { |
| | | title: 'ç¡®è®¤å½æ¡£ï¼å½æ¡£åå°çæå°è´¦è®°å½ï¼ä»»å¡ä¸å¯åä¿®æ¹ã', |
| | |
| | | { fieldName: 'documentVersion', label: 'èµæçæ¬', component: 'Input', componentProps: { disabled: true } }, |
| | | { fieldName: 'reviewerNickname', label: 'å®¡æ ¸äºº', component: 'Input', componentProps: { disabled: true } }, |
| | | { |
| | | fieldName: 'auditStatus', |
| | | // åæ®µåä¸è½ç¨ auditStatusï¼èªå¨å®¡æ¹æ¨¡å¼ä¸ä¼è¢« adapter/form ç isAuditField æ´æ¡è¿æ»¤æï¼å¯¼è´å®¡æ ¸äººæ æ³éæ© |
| | | fieldName: 'auditResult', |
| | | label: 'å®¡æ ¸ç»æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | |
| | | modalApi.lock(); |
| | | try { |
| | | const raw = await formApi.getValues(); |
| | | await updatePdDocumentAudit({ id: raw.id, auditStatus: raw.auditStatus, remark: raw.remark, auditTime: Date.now() }); |
| | | await updatePdDocumentAudit({ id: raw.id, auditStatus: raw.auditResult, remark: raw.remark, auditTime: Date.now() }); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | await modalApi.close(); |
| | |
| | | if (!isOpen) return; |
| | | const data = modalApi.getData<MesPdDocumentAuditApi.DocumentAudit>(); |
| | | modalApi.setState({ title: 'èµæå®¡æ ¸' }); |
| | | const formData = { ...data }; |
| | | delete formData.auditStatus; |
| | | await formApi.setValues(formData); |
| | | // æ¯æ¬¡æå¼é½æ¸
ç©ºå®¡æ ¸ç»æï¼é¿å
å¤ç¨ä¸ä¸è¡å·²éçå¼ |
| | | await formApi.setValues({ ...data, auditResult: undefined }); |
| | | }, |
| | | }); |
| | | </script> |
| | |
| | | { field: 'auditNo', title: 'å®¡æ ¸åå·', minWidth: 140 }, |
| | | { field: 'documentName', title: 'èµæåç§°', minWidth: 160 }, |
| | | { field: 'documentVersion', title: 'èµæçæ¬', minWidth: 90 }, |
| | | { field: 'fileUrl', title: 'æä»¶', minWidth: 140, slots: { default: 'file' } }, |
| | | { field: 'reviewerNickname', title: 'å®¡æ ¸äºº', minWidth: 100 }, |
| | | { field: 'auditStatus', title: 'å®¡æ ¸ç¶æ', minWidth: 100, cellRender: { name: 'CellDict', props: { type: DICT_TYPE.MES_PD_DOCUMENT_AUDIT_STATUS } } }, |
| | | { field: 'auditTime', title: 'å®¡æ ¸æ¶é´', minWidth: 160, formatter: 'formatDateTime' }, |
| | |
| | | <template> |
| | | <AuditFormModal @success="handleRefresh" /> |
| | | <Grid table-title="èµæå®¡æ ¸"> |
| | | <template #file="{ row }"> |
| | | <a v-if="row.fileUrl" :href="row.fileUrl" target="_blank" class="text-primary"> |
| | | {{ row.fileName || 'æ¥çæä»¶' }} |
| | | </a> |
| | | <span v-else class="text-gray-400">-</span> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | minWidth: 160, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'action', |
| | |
| | | field: 'createTime', |
| | | title: 'ç³è¯·æ¶é´', |
| | | minWidth: 160, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | ]; |
| | | |
| | |
| | | |
| | | import { markRaw } from 'vue'; |
| | | |
| | | import { DICT_TYPE, MesProWorkOrderStatusEnum } from '@vben/constants'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | |
| | | field: 'createTime', |
| | | title: 'åèµ·æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { field: 'reason', title: 'å¼å«åå ', minWidth: 160 }, |
| | | { |
| | |
| | | component: markRaw(ProWorkOrderSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©è®¢åï¼å¯éï¼', |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | }, |
| | | } |
| | | : { |
| | |
| | | import { |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | MesProWorkOrderStatusEnum, |
| | | } from '@vben/constants'; |
| | | |
| | | import { Button } from 'ant-design-vue'; |
| | |
| | | componentProps: { |
| | | disabled: headerReadonly, |
| | | placeholder: 'è¯·éæ©ç产订å', |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | |
| | | import type { MesProCardApi } from '#/api/mes/pro/card'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { MesProCardStatusEnum } from '@vben/constants'; |
| | | import { BarcodeBizTypeEnum, MesProCardStatusEnum } from '@vben/constants'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button, message } from 'ant-design-vue'; |
| | |
| | | <PrinterLabel |
| | | :biz-code="row.code" |
| | | :biz-id="row.id" |
| | | biz-type="PROCARD" |
| | | :biz-type="BarcodeBizTypeEnum.PROCARD" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | |
| | | updateCard, |
| | | } from '#/api/mes/pro/card'; |
| | | import { $t } from '#/locales'; |
| | | import { BarcodeDetail } from '#/views/wls/barcode/components'; |
| | | import { |
| | | BarcodeDetail, |
| | | PrinterLabel, |
| | | } from '#/views/wls/barcode/components'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | import ProcessList from './process-list.vue'; |
| | |
| | | formData.value.id, |
| | | BarcodeBizTypeEnum.PROCARD, |
| | | formData.value.code, |
| | | formData.value.itemName || formData.value.code, |
| | | ); |
| | | } |
| | | |
| | |
| | | > |
| | | æ¥çæ¡ç |
| | | </Button> |
| | | <PrinterLabel |
| | | v-if="formType === 'detail' && formData?.id" |
| | | :biz-id="formData.id" |
| | | :biz-type="BarcodeBizTypeEnum.PROCARD" |
| | | :biz-code="formData.code" |
| | | :biz-name="formData.itemName || formData.code" |
| | | title="æµè½¬å¡" |
| | | /> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | MesProTaskStatusEnum, |
| | | MesProWorkOrderStatusEnum, |
| | | } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | import { formatDate } from '@vben/utils'; |
| | | |
| | | import { Button } from 'ant-design-vue'; |
| | | |
| | |
| | | import { getRouteProcessByRouteAndProcess } from '#/api/mes/pro/route/process'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import { loadParamDetails } from './param-state'; |
| | | |
| | | import { getRangePickerDefaultProps } from '#/utils'; |
| | | import { MdItemSelect } from '#/views/mes/md/item/components'; |
| | | import { MdWorkstationSelect } from '#/views/mes/md/workstation/components'; |
| | |
| | | field: 'feedbackTime', |
| | | title: 'æ¥å·¥æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { field: 'approveUserNickname', title: 'å®¡æ ¸äºº', width: 100 }, |
| | | { |
| | |
| | | componentProps: { |
| | | disabled: isHeaderReadonly, |
| | | placeholder: 'è¯·éæ©è®¢å', |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | // 订ååæ´ï¼æ¸
空任å¡åä»»å¡å¸¦åºç产åä¿¡æ¯ãæ°éåºåæ§å¶ä½ |
| | | onChange: async () => { |
| | | await formApi?.setValues({ |
| | |
| | | { field: 'materialScrapQuantity', label: 'æåºæ°é' }, |
| | | { field: 'otherScrapQuantity', label: 'å
¶ä»åºå' }, |
| | | { field: 'feedbackUserNickname', label: 'æ¥å·¥äºº' }, |
| | | { field: 'feedbackTime', label: 'æ¥å·¥æ¶é´' }, |
| | | { |
| | | field: 'feedbackTime', |
| | | label: 'æ¥å·¥æ¶é´', |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { field: 'approveUserNickname', label: 'å®¡æ ¸äºº' }, |
| | | { field: 'remark', label: '夿³¨' }, |
| | | ]; |
| | |
| | | fieldName: 'expireDate', |
| | | label: 'æææ', |
| | | component: 'DatePicker', |
| | | componentProps: { class: 'w-full', format: 'YYYY-MM-DD', valueFormat: 'YYYY-MM-DD HH:mm:ss', showTime: true, placeholder: 'è¯·éæ©æææ' }, |
| | | componentProps: { class: 'w-full', format: 'YYYY-MM-DD', valueFormat: 'YYYY-MM-DD HH:mm:ss', showTime: false, placeholder: 'è¯·éæ©æææ' }, |
| | | rules: 'required', |
| | | }); |
| | | } |
| | |
| | | fieldName: 'receiptDate', |
| | | label: 'å
¥åºæ¥æ', |
| | | component: 'DatePicker', |
| | | componentProps: { class: 'w-full', format: 'YYYY-MM-DD', valueFormat: 'YYYY-MM-DD HH:mm:ss', showTime: true, placeholder: 'è¯·éæ©å
¥åºæ¥æ' }, |
| | | componentProps: { class: 'w-full', format: 'YYYY-MM-DD', valueFormat: 'YYYY-MM-DD HH:mm:ss', showTime: false, placeholder: 'è¯·éæ©å
¥åºæ¥æ' }, |
| | | rules: 'required', |
| | | }); |
| | | } |
| | |
| | | label: '客æ·', |
| | | component: markRaw(CrmCustomerSelect), |
| | | componentProps: { |
| | | allCustomers: true, |
| | | placeholder: 'è¯·éæ©å®¢æ·', |
| | | disabled: !isCreate && !isEdit, |
| | | }, |
| | |
| | | label: '客æ·', |
| | | component: markRaw(CrmCustomerSelect), |
| | | componentProps: { |
| | | allCustomers: true, |
| | | placeholder: 'è¯·éæ©å®¢æ·', |
| | | }, |
| | | }, |
| | |
| | | formatter: 'formatAmount3', |
| | | }, |
| | | { |
| | | field: 'clientCode', |
| | | title: '客æ·ç¼ç ', |
| | | width: 120, |
| | | }, |
| | | { |
| | | field: 'clientName', |
| | | title: '客æ·åç§°', |
| | | minWidth: 120, |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | <Descriptions.Item label="å·²ç产æ°é"> |
| | | {{ formData.quantityProduced }} |
| | | </Descriptions.Item> |
| | | <Descriptions.Item label="客æ·ç¼ç "> |
| | | {{ formData.clientCode }} |
| | | </Descriptions.Item> |
| | | <Descriptions.Item label="客æ·åç§°"> |
| | | {{ formData.clientName }} |
| | | </Descriptions.Item> |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | export { default as RouteColorPicker } from './color-picker.vue'; |
| | | export { default as RouteOutputAttribution } from './output-attribution.vue'; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process'; |
| | | import type { MesProRouteProductApi } from '#/api/mes/pro/route/product'; |
| | | |
| | | import { computed } from 'vue'; |
| | | |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { Button, Select } from 'ant-design-vue'; |
| | | |
| | | defineOptions({ name: 'RouteOutputAttribution' }); |
| | | |
| | | /** å½å±éæ©ä¸ä»£è¡¨âå¤äº§åéç¨âçå¨å
µå¼ï¼äº§åç¼å·åä¸ºæ£æ°ï¼ */ |
| | | const GENERAL_PRODUCT = 0; |
| | | |
| | | const props = withDefaults( |
| | | defineProps<{ |
| | | disabled?: boolean; |
| | | modelValue?: MesProRouteProcessApi.OutputItem[]; |
| | | products?: MesProRouteProductApi.RouteProduct[]; |
| | | }>(), |
| | | { |
| | | disabled: false, |
| | | modelValue: () => [], |
| | | products: () => [], |
| | | }, |
| | | ); |
| | | |
| | | const emit = defineEmits<{ |
| | | 'update:modelValue': [value: MesProRouteProcessApi.OutputItem[]]; |
| | | }>(); |
| | | |
| | | const rows = computed(() => props.modelValue ?? []); |
| | | const productOptions = computed(() => |
| | | props.products |
| | | .filter((product) => product.itemId != null) |
| | | .map((product) => ({ |
| | | label: product.itemName ?? product.itemCode ?? String(product.itemId), |
| | | value: product.itemId!, |
| | | })), |
| | | ); |
| | | const attributionOptions = computed(() => [ |
| | | { label: 'éç¨ï¼ææäº§åï¼', value: GENERAL_PRODUCT }, |
| | | ...productOptions.value, |
| | | ]); |
| | | |
| | | function commit(next: MesProRouteProcessApi.OutputItem[]) { |
| | | emit('update:modelValue', next); |
| | | } |
| | | |
| | | function updateRow( |
| | | index: number, |
| | | patch: Partial<MesProRouteProcessApi.OutputItem>, |
| | | ) { |
| | | commit(rows.value.map((row, i) => (i === index ? { ...row, ...patch } : row))); |
| | | } |
| | | |
| | | /** éæ©äº§åºç©ææ¶ï¼é»è®¤å½å±å°èªèº«äº§åï¼åæåå
±ç¨æ¶åæå¨æ¹æâéç¨âï¼ */ |
| | | function handleItemChange(index: number, itemId: number | undefined) { |
| | | const current = rows.value[index]; |
| | | const patch: Partial<MesProRouteProcessApi.OutputItem> = { itemId }; |
| | | // 忥å½å±èªèº«æ¶ï¼åæ¢äº§åºç©æä¹è¦åæ¥å½å±ï¼é¿å
ç䏿§äº§åç¼å·ã |
| | | if ((current?.productId == null || current.productId === current.itemId) && itemId != null) { |
| | | patch.productId = itemId; |
| | | } |
| | | updateRow(index, patch); |
| | | } |
| | | |
| | | function handleAttributionChange(index: number, value: number | undefined) { |
| | | updateRow(index, { |
| | | productId: value === GENERAL_PRODUCT || value == null ? undefined : value, |
| | | }); |
| | | } |
| | | |
| | | function addRow() { |
| | | commit([...rows.value, { itemId: undefined, productId: undefined }]); |
| | | } |
| | | |
| | | function removeRow(index: number) { |
| | | commit(rows.value.filter((_, i) => i !== index)); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="w-full"> |
| | | <div class="flex flex-col gap-2"> |
| | | <div |
| | | v-for="(row, index) in rows" |
| | | :key="index" |
| | | class="flex items-center gap-2" |
| | | > |
| | | <Select |
| | | :disabled="disabled" |
| | | :options="productOptions" |
| | | :placeholder="'éæ©äº§åºç©æ'" |
| | | :value="row.itemId" |
| | | class="min-w-0 flex-1" |
| | | option-filter-prop="label" |
| | | show-search |
| | | @change="(value) => handleItemChange(index, value as number | undefined)" |
| | | /> |
| | | <Select |
| | | :disabled="disabled || row.itemId == null" |
| | | :options="attributionOptions" |
| | | :value="row.productId ?? GENERAL_PRODUCT" |
| | | class="min-w-0 flex-1" |
| | | @change="(value) => handleAttributionChange(index, value as number | undefined)" |
| | | /> |
| | | <Button |
| | | :aria-label="`å é¤ç¬¬ ${index + 1} æ¡äº§åºé
ç½®`" |
| | | :disabled="disabled" |
| | | danger |
| | | size="small" |
| | | type="text" |
| | | @click="removeRow(index)" |
| | | > |
| | | <IconifyIcon class="size-4" icon="lucide:trash-2" /> |
| | | </Button> |
| | | </div> |
| | | <div v-if="rows.length === 0" class="text-sm text-gray-500"> |
| | | ææ äº§åºï¼å½¢æåæåææåæ¶æ·»å |
| | | </div> |
| | | </div> |
| | | <Button |
| | | v-if="!disabled" |
| | | class="mt-2" |
| | | size="small" |
| | | type="dashed" |
| | | @click="addRow" |
| | | > |
| | | <IconifyIcon class="mr-1 size-4" icon="lucide:plus" /> |
| | | æ·»å äº§åº |
| | | </Button> |
| | | </div> |
| | | </template> |
| | |
| | | import { z } from '#/adapter/form'; |
| | | import { generateAutoCode } from '#/api/mes/md/autocode/record'; |
| | | import { getTemplatePage } from '#/api/mes/pro/processdesign/template'; |
| | | import { getWarehouseSimpleList } from '#/api/mes/wm/warehouse'; |
| | | import { getWarehouseAreaSimpleList } from '#/api/mes/wm/warehouse/area'; |
| | | import { getWarehouseLocationSimpleList } from '#/api/mes/wm/warehouse/location'; |
| | | import { |
| | | MdItemSelect, |
| | | MdProductBomSelect, |
| | | } from '#/views/mes/md/item/components'; |
| | | |
| | | import { RouteColorPicker } from './components'; |
| | | import { RouteColorPicker, RouteOutputAttribution } from './components'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'update'; |
| | |
| | | /** å·¥èºè·¯çº¿å·¥åºæç»è¡¨å */ |
| | | export function useRouteProcessFormSchema( |
| | | processOptions: Array<{ label: string; value: number }>, |
| | | products: MesProRouteProductApi.RouteProduct[] = [], |
| | | ): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'yieldRate', |
| | | label: 'å·¥åºæ¶ç', |
| | | component: 'InputNumber', |
| | | help: '产åºé ÷ æå
¥éï¼æªå¡«åæ 100% åä¸æäº§è®¡ç®ã', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | min: 0.0001, |
| | | max: 1, |
| | | step: 0.01, |
| | | precision: 4, |
| | | placeholder: 'å¦ 0.98ï¼98%ï¼', |
| | | }, |
| | | rules: z.number().min(0.0001).max(1).nullish(), |
| | | }, |
| | | { |
| | | fieldName: 'outputs', |
| | | label: 'æ¬å·¥åºäº§åº', |
| | | component: markRaw(RouteOutputAttribution), |
| | | componentProps: { |
| | | products, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'linkType', |
| | | label: 'ä¸ä¸éå·¥åºå
³ç³»', |
| | | component: 'Select', |
| | | help: () => |
| | | h( |
| | | 'div', |
| | | { |
| | | style: |
| | | 'white-space: pre-line; max-width: 320px; line-height: 1.6;', |
| | | }, |
| | | '0=å¼å§-å¼å§(SS)ï¼æ¬å·¥åºå¼å§åï¼ä¸éå·¥åºæè½å¼å§ï¼å¯éå å¹¶è¡å¼å·¥ï¼\n1=ç»æ-ç»æ(FF)ï¼ä¸éå·¥åºå¿
é¡»çå°æ¬å·¥åºç»ææè½ç»æï¼å¹¶è¡ä½åæ¥å®å·¥ï¼\n2=å¼å§-ç»æ(SF)ï¼æ¬å·¥åºå¼å§åï¼ä¸éå·¥åºæè½ç»æ\n3=ç»æ-å¼å§(FS)ï¼æ¬å·¥åºç»æåï¼ä¸éå·¥åºæè½å¼å§ï¼é»è®¤å¼ï¼', |
| | | ), |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.MES_PRO_LINK_TYPE, 'number'), |
| | | placeholder: 'è¯·éæ©', |
| | | }, |
| | | rules: z.number().optional(), |
| | | rules: z.number().default(3), |
| | | }, |
| | | { |
| | | fieldName: 'colorCode', |
| | |
| | | componentProps: { |
| | | checkedChildren: 'æ¯', |
| | | unCheckedChildren: 'å¦', |
| | | }, |
| | | rules: z.boolean().default(false), |
| | | }, |
| | | { |
| | | fieldName: 'backflushFlag', |
| | | label: 'æ¥å·¥èªå¨æ£æ', |
| | | component: 'Switch', |
| | | componentProps: { |
| | | checkedChildren: 'èªå¨', |
| | | unCheckedChildren: 'æå¨', |
| | | }, |
| | | rules: z.boolean().default(false), |
| | | }, |
| | |
| | | { field: 'sort', title: 'åºå·', width: 70, align: 'center', fixed: 'left' }, |
| | | { field: 'processCode', title: 'å·¥åºç¼ç ', width: 140, fixed: 'left' }, |
| | | { field: 'processName', title: 'å·¥åºåç§°', width: 140, fixed: 'left' }, |
| | | { |
| | | field: 'outputItemNames', |
| | | title: 'æ¬å·¥åºäº§åº', |
| | | minWidth: 240, |
| | | formatter: ({ cellValue, row }) => { |
| | | const outputs = row.outputs ?? []; |
| | | if (!outputs.length) { |
| | | return cellValue ?? ''; |
| | | } |
| | | return outputs |
| | | .map((output) => { |
| | | const name = output.itemName ?? output.itemCode ?? output.itemId; |
| | | return output.productId == null |
| | | ? `${name}ï¼éç¨ï¼` |
| | | : `${name}ï¼${output.productName ?? output.productId}ï¼`; |
| | | }) |
| | | .join('ã'); |
| | | }, |
| | | }, |
| | | { field: 'nextProcessName', title: 'ä¸ä¸éå·¥åº', width: 140 }, |
| | | { |
| | | field: 'linkType', |
| | |
| | | props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'backflushFlag', |
| | | title: 'èªå¨æ£æ', |
| | | width: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING }, |
| | | }, |
| | | }, |
| | | { field: 'prepareTime', title: 'å夿¶é´(å)', width: 110 }, |
| | | { field: 'waitTime', title: 'çå¾
æ¶é´(å)', width: 110 }, |
| | | { |
| | |
| | | /** å·¥èºè·¯çº¿äº§ååè¡¨åæ®µ */ |
| | | export function useRouteProductGridColumns(): VxeTableGridOptions<MesProRouteProductApi.RouteProduct>['columns'] { |
| | | return [ |
| | | { field: 'itemCode', title: '产åç©æç¼ç ', width: 150 }, |
| | | { field: 'itemName', title: '产åç©æåç§°', width: 150 }, |
| | | { field: 'itemCode', title: '产åºç¼ç ', width: 150 }, |
| | | { field: 'itemName', title: '产åºåç§°', width: 150 }, |
| | | { field: 'specification', title: 'è§æ ¼åå·', width: 150 }, |
| | | { field: 'unitName', title: 'åä½', width: 80 }, |
| | | { field: 'quantity', title: 'ç产æ°é', width: 100 }, |
| | | { field: 'warehouseName', title: 'æ¥å·¥å
¥åºä»', width: 140 }, |
| | | { field: 'locationName', title: 'å
¥åºåºåº', width: 120 }, |
| | | { field: 'areaName', title: 'å
¥åºåºä½', width: 120 }, |
| | | { |
| | | field: 'productionTime', |
| | | title: 'çäº§ç¨æ¶', |
| | |
| | | /** å·¥èºè·¯çº¿äº§å BOM åè¡¨åæ®µ */ |
| | | export function useRouteProductBomGridColumns(): VxeTableGridOptions<MesProRouteProductBomApi.RouteProductBom>['columns'] { |
| | | return [ |
| | | { field: 'itemCode', title: 'BOM ç©æç¼ç ', width: 150 }, |
| | | { field: 'itemName', title: 'BOM ç©æåç§°', width: 150 }, |
| | | { field: 'itemCode', title: 'ææç¼ç ', width: 150 }, |
| | | { field: 'itemName', title: 'ææåç§°', width: 150 }, |
| | | { field: 'specification', title: 'è§æ ¼åå·', width: 150 }, |
| | | { field: 'unitName', title: 'åä½', width: 80 }, |
| | | { field: 'quantity', title: 'ç¨ææ¯ä¾', width: 100 }, |
| | |
| | | /** å·¥èºè·¯çº¿äº§å表å */ |
| | | export function useRouteProductFormSchema( |
| | | onItemChange?: (item: any) => void, |
| | | getFormApi?: () => VbenFormApi, |
| | | ): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | |
| | | // 产åç©æï¼ä½¿ç¨ä¸å¡èªå®ä¹éæ©å¨ï¼change åå¡«ç¼ç /åç§°/è§æ ¼/åä½ |
| | | { |
| | | fieldName: 'itemId', |
| | | label: '产å', |
| | | label: '产åºç©æ', |
| | | component: markRaw(MdItemSelect), |
| | | componentProps: { |
| | | onChange: onItemChange, |
| | | }, |
| | | help: 'å¯éæ©ä¸»æ°æ®ç±»å为æåæåæåçç©æï¼ä¹å
¼å®¹ MES æå/åæååç±»ï¼å·²æè·¯çº¿äº§åå¯ç»§ç»ä¿®æ¹ä»ä½ã', |
| | | formItemClass: 'col-span-2', |
| | | rules: 'selectRequired', |
| | | }, |
| | |
| | | rules: z.number().default(1), |
| | | }, |
| | | { |
| | | fieldName: 'warehouseId', |
| | | label: 'æ¥å·¥ä»åº', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | api: getWarehouseSimpleList, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | placeholder: 'è¯·éæ©æ¥å·¥äº§åºä»åº', |
| | | onChange: async () => { |
| | | await getFormApi?.().setFieldValue('locationId', undefined); |
| | | await getFormApi?.().setFieldValue('areaId', undefined); |
| | | }, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'locationId', |
| | | label: 'æ¥å·¥åºåº', |
| | | component: 'Select', |
| | | dependencies: { |
| | | triggerFields: ['warehouseId'], |
| | | disabled: (values) => !values.warehouseId, |
| | | async componentProps(values) { |
| | | const options = values.warehouseId |
| | | ? await getWarehouseLocationSimpleList(values.warehouseId) |
| | | : []; |
| | | return { |
| | | fieldNames: { label: 'name', value: 'id' }, |
| | | options, |
| | | placeholder: 'è¯·éæ©æ¥å·¥äº§åºåºåº', |
| | | onChange: async () => getFormApi?.().setFieldValue('areaId', undefined), |
| | | }; |
| | | }, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'areaId', |
| | | label: 'æ¥å·¥åºä½', |
| | | component: 'Select', |
| | | dependencies: { |
| | | triggerFields: ['locationId'], |
| | | disabled: (values) => !values.locationId, |
| | | async componentProps(values) { |
| | | const options = values.locationId |
| | | ? await getWarehouseAreaSimpleList(values.locationId) |
| | | : []; |
| | | return { |
| | | fieldNames: { label: 'name', value: 'id' }, |
| | | options, |
| | | placeholder: 'è¯·éæ©æ¥å·¥äº§åºåºä½', |
| | | }; |
| | | }, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'productionTime', |
| | | label: 'çäº§ç¨æ¶', |
| | | component: 'InputNumber', |
| | |
| | | import { getDictLabel } from '@vben/hooks'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button, message, Tooltip } from 'ant-design-vue'; |
| | | import { Alert, Button, message, Tooltip } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteRoute, |
| | | exportRoute, |
| | | getRoutePage, |
| | | importRoute, |
| | | importRouteTemplate, |
| | | updateRouteStatus, |
| | | } from '#/api/mes/pro/route'; |
| | | import ImportModalComponent from '#/components/import-modal'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | import Form from './modules/route-configurator.vue'; |
| | | |
| | | const { hasAccessByCodes } = useAccess(); |
| | | const statusEditable = hasAccessByCodes(['mes:pro-route:update']); // æ¯å¦å¯åæ¢ç¶æ |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ImportModal, importModalApi] = useVbenModal({ |
| | | connectedComponent: ImportModalComponent, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | |
| | | /** å建工èºè·¯çº¿ */ |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | /** 导å
¥å·¥èºè·¯çº¿ */ |
| | | function handleImport() { |
| | | importModalApi |
| | | .setData({ |
| | | title: $t('ui.actionTitle.import', ['å·¥èºè·¯çº¿']), |
| | | fileName: 'å·¥èºè·¯çº¿å¯¼å
¥æ¨¡æ¿.xls', |
| | | importFn: importRoute, |
| | | templateFn: importRouteTemplate, |
| | | }) |
| | | .open(); |
| | | } |
| | | |
| | | /** ç¼è¾å·¥èºè·¯çº¿ï¼ä»
åç¨ç¶æå¯ç¼è¾ï¼ */ |
| | |
| | | ): Promise<boolean | undefined> { |
| | | try { |
| | | await confirm( |
| | | `确认è¦å°"${row.name}"å·¥èºè·¯çº¿åæ¢ä¸ºã${getDictLabel(DICT_TYPE.COMMON_STATUS, newStatus)}ãåï¼`, |
| | | newStatus === CommonStatusEnum.ENABLE |
| | | ? `确认å¯ç¨â${row.name}âåï¼ç³»ç»å°æ£æ¥äº§åºå
¥åºä½ç½®ãå·¥åºäº§åºåå·¥åºæææ¯å¦å®æ´ã` |
| | | : `确认è¦å°â${row.name}âå·¥èºè·¯çº¿åæ¢ä¸ºã${getDictLabel(DICT_TYPE.COMMON_STATUS, newStatus)}ãåï¼`, |
| | | ); |
| | | } catch { |
| | | return false; |
| | |
| | | |
| | | <template> |
| | | <Page auto-content-height><FormModal @success="handleRefresh" /> |
| | | <ImportModal @success="handleRefresh" /> |
| | | <Alert |
| | | class="mb-3" |
| | | message="路线é
置顺åº" |
| | | description="åºæ¬ä¿¡æ¯ â 产åºäº§ååå
¥åºä» â å·¥åºååå·¥åºäº§åº â å·¥åºææ â æ ¡éªå¯ç¨" |
| | | show-icon |
| | | type="info" |
| | | /> |
| | | <Grid table-title="å·¥èºè·¯çº¿å表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.import', ['å·¥èºè·¯çº¿']), |
| | | icon: ACTION_ICON.UPLOAD, |
| | | auth: ['mes:pro-route:import'], |
| | | onClick: handleImport, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.export'), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.DOWNLOAD, |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormType } from '../data'; |
| | | import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process'; |
| | | import type { MesProRouteProductApi } from '#/api/mes/pro/route/product'; |
| | | import type { MesProRouteProductBomApi } from '#/api/mes/pro/route/productbom'; |
| | | |
| | | import { computed, ref, watch } from 'vue'; |
| | | |
| | | import { Alert, Button, Empty, Tag } from 'ant-design-vue'; |
| | | |
| | | import { getRouteProductBomList } from '#/api/mes/pro/route/productbom'; |
| | | |
| | | import ProductBomList from './product-bom-list.vue'; |
| | | |
| | | const props = defineProps<{ |
| | | formType: FormType; |
| | | processes: MesProRouteProcessApi.RouteProcess[]; |
| | | products: MesProRouteProductApi.RouteProduct[]; |
| | | routeId: number; |
| | | }>(); |
| | | |
| | | const emit = defineEmits<{ |
| | | change: [list: MesProRouteProductBomApi.RouteProductBom[]]; |
| | | }>(); |
| | | |
| | | const activeProductId = ref<number>(); |
| | | const allBomList = ref<MesProRouteProductBomApi.RouteProductBom[]>([]); |
| | | |
| | | const activeProduct = computed(() => |
| | | props.products.find((item) => item.itemId === activeProductId.value), |
| | | ); |
| | | |
| | | function materialCount(productId?: number) { |
| | | if (!productId) return 0; |
| | | return allBomList.value.filter((item) => item.productId === productId).length; |
| | | } |
| | | |
| | | async function loadAll() { |
| | | allBomList.value = (await getRouteProductBomList({ routeId: props.routeId })) || []; |
| | | emit('change', allBomList.value); |
| | | } |
| | | |
| | | watch( |
| | | () => props.products.map((item) => item.itemId).filter(Boolean), |
| | | (productIds) => { |
| | | if (!productIds.length) { |
| | | activeProductId.value = undefined; |
| | | return; |
| | | } |
| | | if (!activeProductId.value || !productIds.includes(activeProductId.value)) { |
| | | activeProductId.value = productIds[0]; |
| | | } |
| | | }, |
| | | { immediate: true }, |
| | | ); |
| | | |
| | | watch( |
| | | () => props.routeId, |
| | | (value) => value && loadAll(), |
| | | { immediate: true }, |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | | <Alert |
| | | class="mb-4" |
| | | message="æäº§åºäº§åé
ç½®æ¯éå·¥åºéè¦æå
¥çç©æ" |
| | | description="å
éæ©äº§åºäº§åï¼å忢å°å
·ä½å·¥åºæ·»å ææç©æåç¨ææ¯ä¾ãå¼å¯âæ¥å·¥èªå¨æ£æâçå·¥åºï¼ä¼å¨æ¥å·¥æ¶æè¿éçé
ç½®èªå¨æ£ååºåã" |
| | | show-icon |
| | | type="info" |
| | | /> |
| | | |
| | | <Empty |
| | | v-if="!products.length || !processes.length" |
| | | :description="!products.length ? '请å
å®æäº§åºäº§åé
ç½®' : '请å
宿工åºé
ç½®'" |
| | | /> |
| | | |
| | | <div v-else class="material-layout"> |
| | | <aside class="product-selector" aria-label="éæ©è¦é
ç½®ææç产åºäº§å"> |
| | | <div class="selector-heading"> |
| | | <strong>产åºäº§å</strong> |
| | | <span>{{ products.length }} 个</span> |
| | | </div> |
| | | <Button |
| | | v-for="product in products" |
| | | :key="product.itemId" |
| | | :class="['product-button', { active: product.itemId === activeProductId }]" |
| | | type="text" |
| | | @click="activeProductId = product.itemId" |
| | | > |
| | | <span class="product-copy"> |
| | | <strong>{{ product.itemName }}</strong> |
| | | <small>{{ product.itemCode }}</small> |
| | | </span> |
| | | <Tag :color="materialCount(product.itemId) ? 'success' : 'warning'"> |
| | | {{ materialCount(product.itemId) ? `${materialCount(product.itemId)} æ¡` : 'æªé
ç½®' }} |
| | | </Tag> |
| | | </Button> |
| | | </aside> |
| | | |
| | | <section v-if="activeProduct" class="material-detail"> |
| | | <header class="material-detail-header"> |
| | | <div> |
| | | <h3>{{ activeProduct.itemName }} · å·¥åºææ</h3> |
| | | <p>便¬¡åæ¢å·¥åºï¼ç»´æ¤è¯¥äº§åå¨è¯¥å·¥åºå®é
æ¶èçç©æã</p> |
| | | </div> |
| | | <Tag color="blue">{{ processes.length }} éå·¥åº</Tag> |
| | | </header> |
| | | <ProductBomList |
| | | :form-type="formType" |
| | | :product-id="activeProduct.itemId!" |
| | | :product-name="activeProduct.itemName" |
| | | :route-id="routeId" |
| | | @change="loadAll" |
| | | /> |
| | | </section> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .material-layout { display: grid; grid-template-columns: minmax(210px, 260px) minmax(0, 1fr); gap: 18px; } |
| | | .product-selector { overflow: hidden; align-self: start; border: 1px solid rgb(226 232 240); border-radius: 12px; background: rgb(248 250 252); } |
| | | .selector-heading { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; border-bottom: 1px solid rgb(226 232 240); color: rgb(15 23 42); } |
| | | .selector-heading span { color: rgb(100 116 139); font-size: 12px; } |
| | | .product-button { display: flex; width: 100%; height: auto; min-height: 62px; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 12px; border-radius: 0; text-align: left; } |
| | | .product-button + .product-button { border-top: 1px solid rgb(226 232 240); } |
| | | .product-button.active { background: rgb(219 234 254); color: rgb(29 78 216); } |
| | | .product-copy { display: grid; min-width: 0; gap: 2px; } |
| | | .product-copy strong, .product-copy small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
| | | .product-copy small { color: rgb(100 116 139); } |
| | | .material-detail { min-width: 0; padding: 18px; border: 1px solid rgb(226 232 240); border-radius: 12px; } |
| | | .material-detail-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; } |
| | | .material-detail-header h3 { margin: 0; color: rgb(15 23 42); font-size: 16px; font-weight: 650; } |
| | | .material-detail-header p { margin: 4px 0 0; color: rgb(100 116 139); font-size: 13px; } |
| | | :global(.dark) .product-selector, :global(.dark) .material-detail { border-color: rgb(51 65 85); background: rgb(15 23 42); } |
| | | :global(.dark) .selector-heading, :global(.dark) .product-button + .product-button { border-color: rgb(51 65 85); color: rgb(241 245 249); } |
| | | :global(.dark) .product-button.active { background: rgb(30 64 175 / 35%); color: rgb(147 197 253); } |
| | | :global(.dark) .material-detail-header h3 { color: rgb(241 245 249); } |
| | | @media (max-width: 900px) { |
| | | .material-layout { grid-template-columns: 1fr; } |
| | | .product-selector { display: flex; overflow-x: auto; } |
| | | .selector-heading { display: none; } |
| | | .product-button { min-width: 210px; border-top: 0 !important; border-right: 1px solid rgb(226 232 240); } |
| | | } |
| | | </style> |
| | |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | import { Alert, message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { getProcessSimpleList } from '#/api/mes/pro/process'; |
| | |
| | | getRouteProcess, |
| | | updateRouteProcess, |
| | | } from '#/api/mes/pro/route/process'; |
| | | import { getRouteProductListByRoute } from '#/api/mes/pro/route/product'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useRouteProcessFormSchema } from '../data'; |
| | |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | | formItemClass: 'col-span-1', |
| | | labelWidth: 150, |
| | | labelWidth: 130, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: [], |
| | |
| | | }); |
| | | |
| | | /** å 载工åºé项ååçæ schemaï¼é¿å
ä¸æç©ºéé¡¹ï¼ */ |
| | | async function loadSchema(): Promise<VbenFormSchema[]> { |
| | | async function loadSchema(routeId: number): Promise<VbenFormSchema[]> { |
| | | const list = await getProcessSimpleList(); |
| | | const options = (list || []).map((item) => ({ |
| | | label: item.name ?? '', |
| | | value: item.id!, |
| | | })); |
| | | return useRouteProcessFormSchema(options); |
| | | const products = routeId ? await getRouteProductListByRoute(routeId) : []; |
| | | return useRouteProcessFormSchema(options, products); |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | |
| | | formData.value = undefined; |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | // å·¥åºä¸æä¾èµè¿ç¨æ°æ®ï¼schema å¨å¼¹çªæå¼æ¶åçæ |
| | | const schema = await loadSchema(); |
| | | formApi.setState({ schema }); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | // å è½½æ°æ® |
| | | const data = modalApi.getData<{ |
| | | id?: number; |
| | | maxSort?: number; |
| | |
| | | if (!data) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | // å·¥åºä¸æä¾èµè¿ç¨æ°æ®ï¼schema å¨å¼¹çªæå¼æ¶åçæ |
| | | const schema = await loadSchema(data.routeId); |
| | | formApi.setState({ schema }); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | // å è½½æ°æ® |
| | | if (!data.id) { |
| | | // æ°å¢æ¶ï¼é»è®¤åºå· = maxSort + 1ï¼å¹¶ç»ä¸ä¸ªé»è®¤çç¹å¾é¢è² |
| | | await formApi.setValues({ |
| | |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-3/5"> |
| | | <Alert |
| | | class="mx-4 mb-4" |
| | | message="æå®é
ç产èç¹é
置产åº" |
| | | description="ä¸é´å·¥åºæ²¡æç¬ç«äº§åºæ¶å¯ç空ï¼å½¢æåæåæ¶éæ©å¯¹åºåæåï¼æ«éå·¥åºå¿
须鿩å
¨é¨æåãå½å±è¡¨ç¤ºè¯¥äº§åºå±äºåªä¸ªäº§åï¼å¤äº§åå
±ç¨çåæåéâéç¨âï¼äº§åä¸å±ï¼å°¤å
¶æ¯æåï¼å½å±å°èªèº«äº§åãå¦å¼å¯æ¥å·¥èªå¨æ£æï¼æ¥å·¥æ¶ä¼æå·¥åºææé
ç½®æ£ååºåã" |
| | | show-icon |
| | | type="info" |
| | | /> |
| | | <Form class="mx-4" /> |
| | | </Modal> |
| | | </template> |
| | |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process'; |
| | | |
| | | import { ref, watch } from 'vue'; |
| | | import { computed, ref, watch } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | |
| | | routeId: number; |
| | | }>(); |
| | | |
| | | const isEditable = ref(props.formType !== 'detail'); // æ¯å¦å¯ç¼è¾ |
| | | const emit = defineEmits<{ |
| | | change: [list: MesProRouteProcessApi.RouteProcess[]]; |
| | | }>(); |
| | | |
| | | const isEditable = computed(() => props.formType !== 'detail'); // æ¯å¦å¯ç¼è¾ |
| | | const list = ref<MesProRouteProcessApi.RouteProcess[]>([]); // å·¥èºè·¯çº¿å·¥åºå表 |
| | | const sortedList = computed(() => |
| | | [...list.value].sort((a, b) => (a.sort || 0) - (b.sort || 0)), |
| | | ); |
| | | |
| | | const [ProcessFormModal, processFormModalApi] = useVbenModal({ |
| | | connectedComponent: ProcessForm, |
| | |
| | | try { |
| | | list.value = await getRouteProcessListByRoute(props.routeId); |
| | | gridApi.setGridOptions({ data: list.value }); |
| | | emit('change', list.value); |
| | | } finally { |
| | | gridApi.setLoading(false); |
| | | } |
| | |
| | | |
| | | <template> |
| | | <ProcessFormModal @success="getList" /> |
| | | <div v-if="sortedList.length" class="mb-4 flex flex-wrap items-center gap-2 rounded-lg bg-slate-50 p-3 dark:bg-slate-900"> |
| | | <template v-for="(item, index) in sortedList" :key="item.id"> |
| | | <div class="flex items-center gap-2 rounded-md border border-slate-200 bg-white px-3 py-2 dark:border-slate-700 dark:bg-slate-800"> |
| | | <span class="text-xs font-semibold text-blue-600">{{ item.sort }}</span> |
| | | <span class="font-medium">{{ item.processName }}</span> |
| | | <Tag v-if="index === sortedList.length - 1" color="blue">æ«é</Tag> |
| | | </div> |
| | | <span v-if="index < sortedList.length - 1" class="text-slate-400">â</span> |
| | | </template> |
| | | </div> |
| | | <div v-if="isEditable" class="mb-3 flex items-center justify-start"> |
| | | <TableAction |
| | | :actions="[ |
| | |
| | | ]" |
| | | /> |
| | | </div> |
| | | <Grid class="w-full" table-title="ç»æå·¥åº"> |
| | | <Grid class="w-full" table-title="å·¥åºé¡ºåºååå·¥åºäº§åº"> |
| | | <template #colorCode="{ row }"> |
| | | <div v-if="row.colorCode" class="flex items-center justify-center gap-1"> |
| | | <div |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormType } from '../data'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process'; |
| | | import type { MesProRouteProductBomApi } from '#/api/mes/pro/route/productbom'; |
| | | |
| | | import { ref, watch } from 'vue'; |
| | | import { computed, ref, watch } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | |
| | | import BomForm from './bom-form.vue'; |
| | | |
| | | const props = defineProps<{ |
| | | formType: FormType; |
| | | productId: number; |
| | | productName?: string; |
| | | routeId: number; |
| | | }>(); |
| | | |
| | | const emit = defineEmits<{ |
| | | change: []; |
| | | }>(); |
| | | |
| | | const isEditable = computed(() => props.formType !== 'detail'); |
| | | |
| | | const processList = ref<MesProRouteProcessApi.RouteProcess[]>([]); // å·¥åºå表ï¼ç¨äº Tabï¼ |
| | | const activeProcessId = ref<number>(); // å½åéä¸çå·¥åº Tab |
| | |
| | | await deleteRouteProductBom(row.id!); |
| | | message.success($t('ui.actionMessage.deleteSuccess', ['BOM ç©æ'])); |
| | | await getList(); |
| | | emit('change'); |
| | | } |
| | | |
| | | async function handleSaved() { |
| | | await getList(); |
| | | emit('change'); |
| | | } |
| | | |
| | | watch( |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <BomFormModal @success="getList" /> |
| | | <BomFormModal @success="handleSaved" /> |
| | | <Tabs v-model:active-key="activeProcessId" @change="getList"> |
| | | <TabPane |
| | | v-for="item in processList" |
| | |
| | | :tab="item.processName" |
| | | /> |
| | | </Tabs> |
| | | <div class="mb-3 flex items-center justify-start"> |
| | | <div v-if="isEditable" class="mb-3 flex items-center justify-start"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['BOM ç©æ']), |
| | | label: $t('ui.actionTitle.create', ['ææç©æ']), |
| | | type: 'primary', |
| | | disabled: !activeProcessId, |
| | | onClick: handleCreate, |
| | |
| | | ]" |
| | | /> |
| | | </div> |
| | | <Grid class="w-full" table-title="产å BOM"> |
| | | <Grid class="w-full" table-title="å½åå·¥åºææ"> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | ifShow: () => isEditable, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | ifShow: () => isEditable, |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', ['BOM ç©æ']), |
| | | title: $t('ui.actionMessage.deleteConfirm', ['ææç©æ']), |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { Divider, message } from 'ant-design-vue'; |
| | | import { Alert, message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useRouteProductFormSchema } from '../data'; |
| | | import ProductBomList from './product-bom-list.vue'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formData = ref<MesProRouteProductApi.RouteProduct>(); |
| | |
| | | labelWidth: 100, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: useRouteProductFormSchema(handleItemChange), |
| | | schema: useRouteProductFormSchema(handleItemChange, () => formApi), |
| | | showDefaultActions: false, |
| | | wrapperClass: 'grid-cols-2', |
| | | }); |
| | |
| | | try { |
| | | if (formData.value?.id) { |
| | | await updateRouteProduct(data); |
| | | // ç¨ææ°è¡¨åå¼åæ¥ formDataï¼ç¡®ä¿äº§å BOM å表ç»å®ç itemId ä¸è¡¨åä¸è´ |
| | | formData.value = { ...formData.value, ...data }; |
| | | } else { |
| | | // æ°å¢æåå忢å°ç¼è¾æ¨¡å¼ï¼æ¹ä¾¿ç»§ç»ç»´æ¤äº§å BOM |
| | | const id = await createRouteProduct(data); |
| | | formData.value = { ...data, id }; |
| | | await formApi.setFieldValue('id', id); |
| | | await createRouteProduct(data); |
| | | } |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-3/5"> |
| | | <Alert |
| | | class="mx-4 mb-4" |
| | | message="è¿éç»´æ¤äº§åºåå
¶æ¥å·¥å
¥åºä½ç½®" |
| | | description="ææç©æå°å¨è·¯çº¿é
ç½®çâå·¥åºææâæ¥éª¤ä¸æäº§åãæå·¥åºç»ä¸ç»´æ¤ã" |
| | | show-icon |
| | | type="info" |
| | | /> |
| | | <Form class="mx-4" /> |
| | | <!-- ç¼è¾æ¨¡å¼ä¸å±ç¤ºäº§å BOM åè¡¨ï¼æ°å¢æ¨¡å¼ä¸éè --> |
| | | <template v-if="formData?.id && formData?.itemId"> |
| | | <Divider class="!my-3" orientation="left">产å BOM é
ç½®</Divider> |
| | | <div class="mx-4"> |
| | | <ProductBomList |
| | | :route-id="formData.routeId!" |
| | | :product-id="formData.itemId" |
| | | :product-name="formData.itemName" |
| | | /> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| | |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesProRouteProductApi } from '#/api/mes/pro/route/product'; |
| | | |
| | | import { ref, watch } from 'vue'; |
| | | import { computed, ref, watch } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | |
| | | routeId: number; |
| | | }>(); |
| | | |
| | | const isEditable = ref(props.formType !== 'detail'); // æ¯å¦å¯ç¼è¾ |
| | | const emit = defineEmits<{ |
| | | change: [list: MesProRouteProductApi.RouteProduct[]]; |
| | | }>(); |
| | | |
| | | const isEditable = computed(() => props.formType !== 'detail'); // æ¯å¦å¯ç¼è¾ |
| | | const list = ref<MesProRouteProductApi.RouteProduct[]>([]); // å·¥èºè·¯çº¿äº§åå表 |
| | | |
| | | const [ProductFormModal, productFormModalApi] = useVbenModal({ |
| | |
| | | try { |
| | | list.value = await getRouteProductListByRoute(props.routeId); |
| | | gridApi.setGridOptions({ data: list.value }); |
| | | emit('change', list.value); |
| | | } finally { |
| | | gridApi.setLoading(false); |
| | | } |
| | |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['å
³è产å']), |
| | | label: $t('ui.actionTitle.create', ['产åºäº§å']), |
| | | type: 'primary', |
| | | onClick: handleCreate, |
| | | }, |
| | | ]" |
| | | /> |
| | | </div> |
| | | <Grid class="w-full" table-title="å
³è产å"> |
| | | <Grid class="w-full" table-title="产åºäº§å忥工å
¥åºä½ç½®"> |
| | | <template #productionTime="{ row }"> |
| | | <span v-if="row.productionTime"> |
| | | {{ row.productionTime }} |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormType } from '../data'; |
| | | import type { MesProRouteApi } from '#/api/mes/pro/route'; |
| | | import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process'; |
| | | import type { MesProRouteProductApi } from '#/api/mes/pro/route/product'; |
| | | import type { MesProRouteProductBomApi } from '#/api/mes/pro/route/productbom'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | import { Alert, Button, Empty, message, Steps, Tag } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { createRoute, getRoute, updateRoute } from '#/api/mes/pro/route'; |
| | | import { getRouteProcessListByRoute } from '#/api/mes/pro/route/process'; |
| | | import { getRouteProductListByRoute } from '#/api/mes/pro/route/product'; |
| | | import { getRouteProductBomList } from '#/api/mes/pro/route/productbom'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | import MaterialInputList from './material-input-list.vue'; |
| | | import ProcessList from './process-list.vue'; |
| | | import ProductList from './product-list.vue'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formType = ref<FormType>('create'); |
| | | const formData = ref<MesProRouteApi.Route>(); |
| | | const activeStep = ref(0); |
| | | const productList = ref<MesProRouteProductApi.RouteProduct[]>([]); |
| | | const processList = ref<MesProRouteProcessApi.RouteProcess[]>([]); |
| | | const bomList = ref<MesProRouteProductBomApi.RouteProductBom[]>([]); |
| | | |
| | | const isDetail = computed(() => formType.value === 'detail'); |
| | | const isNew = computed(() => !formData.value?.id); |
| | | const routeId = computed(() => formData.value?.id); |
| | | const sortedProcessList = computed(() => |
| | | [...processList.value].sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0)), |
| | | ); |
| | | const title = computed(() => |
| | | isDetail.value ? 'æ¥çå·¥èºè·¯çº¿' : isNew.value ? 'æ°å»ºå·¥èºè·¯çº¿' : 'ç¼è¾å·¥èºè·¯çº¿', |
| | | ); |
| | | |
| | | const stepItems = [ |
| | | { title: 'åºæ¬ä¿¡æ¯', description: '路线åç§°ä¸è¯´æ' }, |
| | | { title: '产åºä¸å
¥åº', description: '产å忥工ä»ä½' }, |
| | | { title: 'å·¥åºä¸äº§åº', description: '顺åºååæå/æå' }, |
| | | { title: 'å·¥åºææ', description: 'æäº§åä¸å·¥åºé
ç½®' }, |
| | | { title: 'å¯ç¨åæ£æ¥', description: 'é䏿£æ¥ç¼ºå¤±é¡¹' }, |
| | | ]; |
| | | |
| | | const productsWithWarehouse = computed(() => |
| | | productList.value.filter((item) => item.warehouseId && item.locationId && item.areaId).length, |
| | | ); |
| | | const processesWithOutput = computed(() => |
| | | processList.value.filter((item) => item.outputItemIds?.length).length, |
| | | ); |
| | | const lastProcess = computed(() => sortedProcessList.value.at(-1)); |
| | | const configuredProductIds = computed( |
| | | () => new Set(bomList.value.map((item) => item.productId).filter(Boolean)), |
| | | ); |
| | | const productsWithBom = computed(() => configuredProductIds.value.size); |
| | | const reviewIssues = computed(() => { |
| | | const issues: Array<{ step: number; text: string }> = []; |
| | | if (!productList.value.length) issues.push({ step: 1, text: 'è¿æ²¡æé
置产åºäº§å' }); |
| | | const missingWarehouse = productList.value.filter( |
| | | (item) => !item.warehouseId || !item.locationId || !item.areaId, |
| | | ); |
| | | if (missingWarehouse.length) { |
| | | issues.push({ |
| | | step: 1, |
| | | text: `${missingWarehouse.length} 个产åºäº§åæªé
ç½®å®æ´ä»åºãåºåºååºä½`, |
| | | }); |
| | | } |
| | | if (!processList.value.length) issues.push({ step: 2, text: 'è¿æ²¡æé
ç½®ç产工åº' }); |
| | | if (processList.value.length && !processList.value.some((item) => item.keyFlag)) { |
| | | issues.push({ step: 2, text: 'æªè®¾ç½®å
³é®å·¥åº' }); |
| | | } |
| | | if (lastProcess.value && !lastProcess.value.outputItemIds?.length) { |
| | | issues.push({ |
| | | step: 2, |
| | | text: `æ«éå·¥åºâ${lastProcess.value.processName || lastProcess.value.processCode || lastProcess.value.sort}âå¿
é¡»é
ç½®æå产åº`, |
| | | }); |
| | | } |
| | | const lastOutputs = lastProcess.value?.outputs ?? []; |
| | | if ( |
| | | lastOutputs.length && |
| | | lastOutputs.some( |
| | | (output) => output.productId == null || output.productId !== output.itemId, |
| | | ) |
| | | ) { |
| | | issues.push({ |
| | | step: 2, |
| | | text: 'æ«éå·¥åºçæå产åºå¿
é¡»å½å±å°èªèº«äº§åï¼ä¸è½ä½¿ç¨âéç¨â', |
| | | }); |
| | | } |
| | | const productIds = new Set(productList.value.map((item) => item.itemId)); |
| | | const invalidOutputCount = processList.value.filter((process) => |
| | | process.outputItemIds?.some((itemId) => !productIds.has(itemId)), |
| | | ).length; |
| | | if (invalidOutputCount) { |
| | | issues.push({ |
| | | step: 2, |
| | | text: `${invalidOutputCount} éå·¥åºçäº§åºæªå
å«å¨è·¯çº¿äº§åºäº§åä¸`, |
| | | }); |
| | | } |
| | | const noBomProducts = productList.value.filter( |
| | | (item) => item.itemId && !configuredProductIds.value.has(item.itemId), |
| | | ); |
| | | if (noBomProducts.length) { |
| | | issues.push({ step: 3, text: `${noBomProducts.length} 个产åè¿æ²¡æé
置任ä½å·¥åºææ` }); |
| | | } |
| | | return issues; |
| | | }); |
| | | const primaryText = computed(() => { |
| | | if (activeStep.value === stepItems.length - 1) return isDetail.value ? 'å
³é' : '宿é
ç½®'; |
| | | if (activeStep.value === 0 && isNew.value) return 'ä¿åå¹¶é
置产åº'; |
| | | return 'ä¸ä¸æ¥'; |
| | | }); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | | formItemClass: 'col-span-1', |
| | | labelWidth: 100, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: [], |
| | | showDefaultActions: false, |
| | | wrapperClass: 'grid-cols-2', |
| | | }); |
| | | |
| | | async function loadConfiguration() { |
| | | if (!routeId.value) return; |
| | | const [products, processes, boms] = await Promise.all([ |
| | | getRouteProductListByRoute(routeId.value), |
| | | getRouteProcessListByRoute(routeId.value), |
| | | getRouteProductBomList({ routeId: routeId.value }), |
| | | ]); |
| | | productList.value = products || []; |
| | | processList.value = processes || []; |
| | | bomList.value = boms || []; |
| | | } |
| | | |
| | | async function saveBaseInformation() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) return false; |
| | | modalApi.lock(); |
| | | try { |
| | | const data = (await formApi.getValues()) as MesProRouteApi.Route; |
| | | if (!routeId.value) { |
| | | const id = await createRoute(data); |
| | | formData.value = { ...data, id }; |
| | | await formApi.setFieldValue('id', id); |
| | | formType.value = 'update'; |
| | | message.success('åºæ¬ä¿¡æ¯å·²ä¿åï¼è¯·ç»§ç»é
置产åºäº§ååå
¥åºä½ç½®'); |
| | | } else { |
| | | await updateRoute({ ...data, id: routeId.value }); |
| | | formData.value = { ...formData.value, ...data, id: routeId.value }; |
| | | } |
| | | emit('success'); |
| | | return true; |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | } |
| | | |
| | | function validateProducts() { |
| | | if (!productList.value.length) { |
| | | message.warning('请è³å°æ·»å ä¸ä¸ªäº§åºäº§åï¼åé
置工åº'); |
| | | return false; |
| | | } |
| | | if (productsWithWarehouse.value !== productList.value.length) { |
| | | message.warning('请为æ¯ä¸ªäº§åºäº§åé
ç½®å®æ´çä»åºãåºåºååºä½'); |
| | | return false; |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | function validateProcesses() { |
| | | if (!processList.value.length) { |
| | | message.warning('请è³å°æ·»å ä¸éç产工åº'); |
| | | return false; |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | function validateBom() { |
| | | if (productsWithBom.value !== productList.value.length) { |
| | | message.warning('æ¯ä¸ªäº§åºäº§åè³å°è¦é
ç½®ä¸æ¡å·¥åºææ'); |
| | | return false; |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | async function goToStep(target: number) { |
| | | if (target < 0 || target >= stepItems.length) return; |
| | | if (target <= activeStep.value || isDetail.value) { |
| | | activeStep.value = target; |
| | | return; |
| | | } |
| | | if (!(await saveBaseInformation())) return; |
| | | await loadConfiguration(); |
| | | if (target >= 2 && !validateProducts()) return void (activeStep.value = 1); |
| | | if (target >= 3 && !validateProcesses()) return void (activeStep.value = 2); |
| | | if (target >= 4 && !validateBom()) return void (activeStep.value = 3); |
| | | activeStep.value = target; |
| | | } |
| | | |
| | | async function handlePrimaryAction() { |
| | | if (activeStep.value === stepItems.length - 1) { |
| | | if (!isDetail.value && reviewIssues.value.length) { |
| | | message.warning('ä»æç¼ºå¤±é
ç½®ï¼è¯·æ ¹æ®æ£æ¥ç»æè¡¥å
åå宿'); |
| | | return; |
| | | } |
| | | await modalApi.close(); |
| | | return; |
| | | } |
| | | await goToStep(activeStep.value + 1); |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | showConfirmButton: false, |
| | | cancelText: 'å
³é', |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | formData.value = undefined; |
| | | productList.value = []; |
| | | processList.value = []; |
| | | bomList.value = []; |
| | | activeStep.value = 0; |
| | | return; |
| | | } |
| | | formApi.setState({ schema: useFormSchema(formApi) }); |
| | | const data = modalApi.getData<{ formType: FormType; id?: number }>(); |
| | | formType.value = data.formType; |
| | | formApi.setDisabled(isDetail.value); |
| | | if (!data.id) return; |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getRoute(data.id); |
| | | await formApi.setValues(formData.value); |
| | | await loadConfiguration(); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="title" class="w-[94vw] max-w-[1560px]"> |
| | | <div class="route-config-shell"> |
| | | <header class="route-config-header"> |
| | | <div class="min-w-0"> |
| | | <div class="flex flex-wrap items-center gap-2"> |
| | | <h2 class="m-0 text-lg font-semibold text-gray-900 dark:text-gray-100"> |
| | | {{ formData?.name || 'æ°å·¥èºè·¯çº¿' }} |
| | | </h2> |
| | | <Tag :color="isDetail ? 'default' : 'processing'"> |
| | | {{ isDetail ? 'åªè¯»æ¥ç' : 'é
ç½®ä¸' }} |
| | | </Tag> |
| | | </div> |
| | | <p class="mb-0 mt-1 text-sm text-gray-600 dark:text-gray-300"> |
| | | å·¥åºææä¾èµäº§åºäº§ååå·¥åºï¼å æ¤å¨å·¥åºç¼æå®æåéä¸é
ç½®ã |
| | | </p> |
| | | </div> |
| | | <div class="route-config-summary" aria-label="å½åé
ç½®æ¦è§"> |
| | | <span>äº§åº {{ productList.length }}</span> |
| | | <span>å·¥åº {{ processList.length }}</span> |
| | | <span>ææ {{ bomList.length }}</span> |
| | | </div> |
| | | </header> |
| | | |
| | | <Steps |
| | | :current="activeStep" |
| | | :items="stepItems" |
| | | class="route-config-steps" |
| | | responsive |
| | | size="small" |
| | | type="navigation" |
| | | @change="goToStep" |
| | | /> |
| | | |
| | | <main class="route-config-content"> |
| | | <section v-show="activeStep === 0" class="route-step-panel"> |
| | | <div class="route-step-copy"> |
| | | <IconifyIcon icon="lucide:route" /> |
| | | <div> |
| | | <h3>å
å®ä¹è¿æ¡è·¯çº¿æ¯ä»ä¹</h3> |
| | | <p>ç¼ç ç¨äºç³»ç»è¯å«ï¼åç§°å说æå¸®å©ç°åºäººåå¿«ééæ©ã</p> |
| | | </div> |
| | | </div> |
| | | <Form class="route-base-form" /> |
| | | </section> |
| | | |
| | | <section v-if="routeId" v-show="activeStep === 1" class="route-step-panel"> |
| | | <Alert |
| | | class="mb-4" |
| | | message="å
ç»´æ¤ææå¯è½çå·¥åºäº§åº" |
| | | description="å®é
å½¢æçä¸é´åæååæ«éå·¥åºçæåé½è¦å å
¥ï¼å¹¶å嫿宿¥å·¥åè¿å
¥çä»åºãåºåºååºä½ã" |
| | | show-icon |
| | | type="info" |
| | | /> |
| | | <ProductList |
| | | :form-type="formType" |
| | | :route-id="routeId" |
| | | @change="productList = $event" |
| | | /> |
| | | </section> |
| | | |
| | | <section v-if="routeId" v-show="activeStep === 2" class="route-step-panel"> |
| | | <Alert |
| | | class="mb-4" |
| | | message="æå®é
ç产顺åºç¼æå·¥åº" |
| | | description="ä¸é´å·¥åºæ²¡æç¬ç«äº§åºæ¶å¯ç空ï¼å½¢æåæåæ¶éæ©å¯¹åºåæåï¼æåä¸éå·¥åºå¿
é¡»éæ©è·¯çº¿ç»å®çå
¨é¨æåãå¤äº§åå
±ç¨çåæåå½å±éâéç¨âï¼æåå¿
é¡»å½å±å°èªèº«äº§åã" |
| | | show-icon |
| | | type="info" |
| | | /> |
| | | <ProcessList |
| | | :form-type="formType" |
| | | :route-id="routeId" |
| | | @change="processList = $event" |
| | | /> |
| | | </section> |
| | | |
| | | <section v-if="routeId" v-show="activeStep === 3" class="route-step-panel"> |
| | | <MaterialInputList |
| | | :form-type="formType" |
| | | :processes="sortedProcessList" |
| | | :products="productList" |
| | | :route-id="routeId" |
| | | @change="bomList = $event" |
| | | /> |
| | | </section> |
| | | |
| | | <section v-show="activeStep === 4" class="route-step-panel"> |
| | | <div class="review-heading"> |
| | | <div> |
| | | <h3>å¯ç¨åæ£æ¥</h3> |
| | | <p>确认产åºãä»ä½ãå·¥åºåææå½¢æéç¯åï¼ååå°å表å¯ç¨è·¯çº¿ã</p> |
| | | </div> |
| | | <Tag :color="reviewIssues.length ? 'warning' : 'success'"> |
| | | {{ reviewIssues.length ? `${reviewIssues.length} 项å¾
å¤ç` : 'é
ç½®å®æ´' }} |
| | | </Tag> |
| | | </div> |
| | | |
| | | <div class="review-metrics"> |
| | | <div><strong>{{ productsWithWarehouse }}/{{ productList.length }}</strong><span>产åä»ä½å®æ´</span></div> |
| | | <div><strong>{{ processesWithOutput }}</strong><span>é
置产åºçå·¥åº</span></div> |
| | | <div><strong>{{ productsWithBom }}/{{ productList.length }}</strong><span>产åå·²é
ç½®ææ</span></div> |
| | | </div> |
| | | |
| | | <div v-if="reviewIssues.length" class="issue-list" role="status"> |
| | | <button |
| | | v-for="issue in reviewIssues" |
| | | :key="issue.text" |
| | | class="issue-item" |
| | | type="button" |
| | | @click="activeStep = issue.step" |
| | | > |
| | | <IconifyIcon icon="lucide:circle-alert" /> |
| | | <span>{{ issue.text }}</span> |
| | | <IconifyIcon class="ml-auto" icon="lucide:chevron-right" /> |
| | | </button> |
| | | </div> |
| | | <Empty v-else description="ææå¿
éé
ç½®å已宿"> |
| | | <template #image> |
| | | <div class="review-success-icon"><IconifyIcon icon="lucide:badge-check" /></div> |
| | | </template> |
| | | </Empty> |
| | | </section> |
| | | </main> |
| | | </div> |
| | | |
| | | <template #prepend-footer> |
| | | <div class="flex flex-auto items-center justify-between gap-3"> |
| | | <Button v-if="activeStep > 0" @click="activeStep--"> |
| | | <IconifyIcon class="mr-1" icon="lucide:arrow-left" />ä¸ä¸æ¥ |
| | | </Button> |
| | | <span v-else class="text-sm text-gray-500">æ°å»ºè·¯çº¿é»è®¤åç¨ï¼é
ç½®å®æ´ååå¯ç¨</span> |
| | | <Button type="primary" @click="handlePrimaryAction"> |
| | | {{ primaryText }} |
| | | <IconifyIcon |
| | | v-if="activeStep < stepItems.length - 1" |
| | | class="ml-1" |
| | | icon="lucide:arrow-right" |
| | | /> |
| | | </Button> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .route-config-shell { min-height: min(720px, 72vh); background: rgb(248 250 252); color: rgb(15 23 42); } |
| | | .route-config-header { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 18px 24px 14px; background: white; border-bottom: 1px solid rgb(226 232 240); } |
| | | .route-config-summary { display: flex; flex-shrink: 0; gap: 8px; } |
| | | .route-config-summary span { padding: 5px 10px; border-radius: 8px; background: rgb(239 246 255); color: rgb(29 78 216); font-size: 12px; font-weight: 600; } |
| | | .route-config-steps { padding: 12px 20px 0; background: white; } |
| | | .route-config-content { padding: 20px 24px 28px; } |
| | | .route-step-panel { min-height: 460px; padding: 22px; border-radius: 14px; background: white; box-shadow: 0 10px 28px rgb(15 23 42 / 6%); } |
| | | .route-step-copy, .review-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; margin-bottom: 22px; } |
| | | .route-step-copy { justify-content: flex-start; } |
| | | .route-step-copy > svg { width: 38px; height: 38px; padding: 9px; border-radius: 10px; background: rgb(219 234 254); color: rgb(29 78 216); } |
| | | .route-step-copy h3, .review-heading h3 { margin: 0; color: rgb(15 23 42); font-size: 17px; font-weight: 650; } |
| | | .route-step-copy p, .review-heading p { margin: 4px 0 0; color: rgb(71 85 105); font-size: 14px; } |
| | | .route-base-form { max-width: 1040px; margin: 34px auto 0; } |
| | | .review-metrics { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; margin: 24px 0; } |
| | | .review-metrics > div { display: flex; align-items: baseline; gap: 10px; padding: 18px; border-radius: 12px; background: rgb(248 250 252); } |
| | | .review-metrics strong { color: rgb(29 78 216); font-size: 24px; } |
| | | .review-metrics span { color: rgb(71 85 105); font-size: 13px; } |
| | | .issue-list { display: grid; gap: 10px; } |
| | | .issue-item { display: flex; width: 100%; min-height: 48px; align-items: center; gap: 10px; padding: 10px 14px; border: 1px solid rgb(253 230 138); border-radius: 10px; background: rgb(255 251 235); color: rgb(146 64 14); cursor: pointer; text-align: left; transition: background-color 180ms ease, border-color 180ms ease; } |
| | | .issue-item:hover, .issue-item:focus-visible { border-color: rgb(245 158 11); background: rgb(254 243 199); outline: none; } |
| | | .review-success-icon { display: grid; width: 72px; height: 72px; place-items: center; margin: 0 auto; border-radius: 50%; background: rgb(220 252 231); color: rgb(22 163 74); font-size: 38px; } |
| | | :global(.dark) .route-config-shell { background: rgb(15 23 42); color: rgb(226 232 240); } |
| | | :global(.dark) .route-config-header, :global(.dark) .route-config-steps, :global(.dark) .route-step-panel { background: rgb(30 41 59); border-color: rgb(51 65 85); } |
| | | :global(.dark) .route-step-copy h3, :global(.dark) .review-heading h3 { color: rgb(241 245 249); } |
| | | :global(.dark) .route-step-copy p, :global(.dark) .review-heading p, :global(.dark) .review-metrics span { color: rgb(203 213 225); } |
| | | :global(.dark) .review-metrics > div { background: rgb(15 23 42); } |
| | | @media (max-width: 900px) { |
| | | .route-config-header { align-items: flex-start; flex-direction: column; } |
| | | .review-metrics { grid-template-columns: 1fr; } |
| | | .route-step-panel { padding: 16px; } |
| | | } |
| | | </style> |
| | |
| | | /** å¾
æäº§è®¢åå表çåæ®µ */ |
| | | export function useGridColumns(): VxeTableGridOptions<MesProWorkOrderApi.WorkOrder>['columns'] { |
| | | return [ |
| | | { type: 'checkbox', width: 48, fixed: 'left' }, |
| | | { |
| | | field: 'code', |
| | | title: '订åç¼ç ', |
| | |
| | | }, |
| | | }, |
| | | { |
| | | field: 'productionStatus', |
| | | title: 'çäº§ç¶æ', |
| | | width: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.ORDER_PRODUCTION_STATUS }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'quantity', |
| | | title: 'è®¢åæ°é', |
| | | width: 100, |
| | |
| | | slots: { default: 'progressPercent' }, |
| | | }, |
| | | { |
| | | field: 'clientCode', |
| | | title: '客æ·ç¼ç ', |
| | | width: 120, |
| | | }, |
| | | { |
| | | field: 'clientName', |
| | | title: '客æ·åç§°', |
| | | width: 120, |
| | |
| | | |
| | | { |
| | | title: 'æä½', |
| | | width: 160, |
| | | width: 240, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { |
| | | MesProWorkOrderProductionStatusEnum, |
| | | MesProWorkOrderStatusEnum, |
| | | MesProWorkOrderTypeEnum, |
| | | } from '@vben/constants'; |
| | | |
| | | import { Button, Card, Progress, Tabs } from 'ant-design-vue'; |
| | | import { Button, Card, Progress, Tabs, message } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getGanttTaskList } from '#/api/mes/pro/task'; |
| | |
| | | import { GanttChart } from './components'; |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import ScheduleForm from './modules/schedule-form.vue'; |
| | | import AutoSchedule from './modules/auto-schedule.vue'; |
| | | |
| | | const router = useRouter(); |
| | | const ganttTasks = ref<any[]>([]); // çç¹å¾é¢è§æ°æ® |
| | |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [AutoScheduleModal, autoScheduleModalApi] = useVbenModal({ |
| | | connectedComponent: AutoSchedule, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | function canAutoSchedule(row: MesProWorkOrderApi.WorkOrder) { |
| | | return row.status === MesProWorkOrderStatusEnum.CONFIRMED |
| | | && row.type === MesProWorkOrderTypeEnum.SELF |
| | | && !isCompletedWorkOrder(row) |
| | | && (row.quantity ?? 0) > 0 |
| | | && (row.quantityScheduled ?? 0) < (row.quantity ?? 0); |
| | | } |
| | | |
| | | function handleAutoSchedule(row?: MesProWorkOrderApi.WorkOrder) { |
| | | const rows: MesProWorkOrderApi.WorkOrder[] = row ? [row] : gridApi.grid.getCheckboxRecords(); |
| | | if (!rows.length) return message.warning('请å
å¾éå¾
æäº§å·¥å'); |
| | | if (rows.length > 30) return message.warning('䏿¬¡æå¤éæ©30个工åï¼è¯·åæ¹æäº§'); |
| | | if (rows.some((order) => !canAutoSchedule(order))) return message.warning('è¯·éæ©å·²ç¡®è®¤ä¸ä»éæäº§çèªè¡ç产工å'); |
| | | autoScheduleModalApi.setData({ workOrderIds: rows.map((order) => order.id!) }).open(); |
| | | } |
| | | |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | | gridApi.grid.clearCheckboxRow(); |
| | | gridApi.query(); |
| | | loadGanttPreview(); |
| | | } |
| | |
| | | router.push({ name: 'MesProTaskGanttEdit' }); |
| | | } |
| | | |
| | | /** å¤æè®¢åæ¯å¦ä¸ºâå·²å®æç±»åâï¼è®¢åç¶æ=已宿 æ çäº§ç¶æ=çäº§å®æ */ |
| | | /** å¤æè®¢åæ¯å¦ä¸ºâå·²å®æç±»åâï¼è®¢åç¶æ=已宿 */ |
| | | function isCompletedWorkOrder(row: MesProWorkOrderApi.WorkOrder): boolean { |
| | | return ( |
| | | row.status === MesProWorkOrderStatusEnum.FINISHED || |
| | | row.productionStatus === MesProWorkOrderProductionStatusEnum.FINISHED |
| | | ); |
| | | return row.status === MesProWorkOrderStatusEnum.FINISHED; |
| | | } |
| | | |
| | | /** |
| | |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | keepSource: true, |
| | | checkboxConfig: { |
| | | checkStrictly: true, |
| | | showHeader: true, |
| | | checkMethod: ({ row }: { row: MesProWorkOrderApi.WorkOrder }) => canAutoSchedule(row), |
| | | }, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <ScheduleModal @success="handleRefresh" /> |
| | | <AutoScheduleModal @success="handleRefresh" /> |
| | | <!-- æäº§çç¹å¾é¢è§ --> |
| | | <Card title="æäº§çç¹å¾" class="mb-4"> |
| | | <template #extra> |
| | |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'çç¹å¾ç¼è¾', |
| | | label: 'ä¸é®æäº§', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['mes:pro-task:create'], |
| | | onClick: () => handleAutoSchedule(), |
| | | }, |
| | | { |
| | | label: 'çç¹å¾ç¼è¾', |
| | | icon: ACTION_ICON.EDIT, |
| | | onClick: handleGanttEdit, |
| | | }, |
| | |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'ä¸é®æäº§', |
| | | type: 'link', |
| | | auth: ['mes:pro-task:create'], |
| | | ifShow: canAutoSchedule(row), |
| | | onClick: () => handleAutoSchedule(row), |
| | | }, |
| | | { |
| | | label: 'æäº§ç»´æ¤', |
| | | type: 'link', |
| | | auth: ['mes:pro-task:create'], |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup lang="ts"> |
| | | import type { BatchSchedulePreview, BatchScheduleRequest } from '#/api/mes/pro/task'; |
| | | import { computed, ref, watch } from 'vue'; |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { Alert, Button, Checkbox, DatePicker, Descriptions, Table, Tag, message } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | | import { executeBatchSchedule, previewBatchSchedule } from '#/api/mes/pro/task'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const ids = ref<number[]>([]); |
| | | function defaultStartTime() { |
| | | let value = dayjs().add(5, 'minute').startOf('minute'); |
| | | if (value.hour() >= 18) value = value.add(1, 'day').hour(9).minute(0).second(0); |
| | | else if (value.hour() < 9) value = value.hour(9).minute(0).second(0); |
| | | while (value.day() === 0 || value.day() === 6) value = value.add(1, 'day').hour(9).minute(0).second(0); |
| | | return value; |
| | | } |
| | | const startTime = ref(defaultStartTime()); |
| | | const preview = ref<BatchSchedulePreview>(); |
| | | const loading = ref(false); |
| | | const executing = ref(false); |
| | | const acceptLate = ref(false); |
| | | const errorText = ref(''); |
| | | let sequence = 0; |
| | | let open = false; |
| | | const canConfirm = computed(() => !!preview.value?.canExecute && !loading.value && !executing.value |
| | | && (!preview.value.warnings.length || acceptLate.value)); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | confirmText: '确认æäº§', |
| | | confirmDisabled: true, |
| | | onBeforeClose: () => !executing.value, |
| | | onConfirm: execute, |
| | | async onOpenChange(isOpen) { |
| | | open = isOpen; |
| | | sequence++; |
| | | if (!isOpen) { |
| | | preview.value = undefined; |
| | | ids.value = []; |
| | | loading.value = false; |
| | | errorText.value = ''; |
| | | return; |
| | | } |
| | | ids.value = modalApi.getData<{ workOrderIds: number[] }>().workOrderIds; |
| | | startTime.value = defaultStartTime(); |
| | | await calculate(); |
| | | }, |
| | | }); |
| | | |
| | | watch(canConfirm, (value) => modalApi.setState({ confirmDisabled: !value })); |
| | | watch(startTime, () => { |
| | | sequence++; |
| | | preview.value = undefined; |
| | | acceptLate.value = false; |
| | | loading.value = false; |
| | | errorText.value = ''; |
| | | }, { flush: 'sync' }); |
| | | |
| | | function request(): BatchScheduleRequest { |
| | | return { workOrderIds: [...ids.value], startTime: startTime.value.format('YYYY-MM-DD HH:mm:ss') }; |
| | | } |
| | | |
| | | async function calculate() { |
| | | if (executing.value || !startTime.value || !ids.value.length) return; |
| | | const current = ++sequence; |
| | | preview.value = undefined; |
| | | acceptLate.value = false; |
| | | errorText.value = ''; |
| | | loading.value = true; |
| | | try { |
| | | const result = await previewBatchSchedule(request()); |
| | | if (open && current === sequence) preview.value = result; |
| | | } catch (error) { |
| | | if (open && current === sequence) errorText.value = error instanceof Error ? error.message : 'é¢è§å¤±è´¥ï¼è¯·ç¨åéè¯'; |
| | | } finally { |
| | | if (current === sequence) loading.value = false; |
| | | } |
| | | } |
| | | |
| | | async function execute() { |
| | | if (!canConfirm.value || !preview.value) return; |
| | | executing.value = true; |
| | | errorText.value = ''; |
| | | modalApi.lock(); |
| | | try { |
| | | const result = await executeBatchSchedule({ ...request(), previewHash: preview.value.previewHash, |
| | | previewTime: preview.value.previewTime, allowLate: acceptLate.value }); |
| | | message.success(`æäº§æåï¼${ids.value.length} 个工åï¼çæ ${result.taskIds.length} 个任å¡`); |
| | | executing.value = false; |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | } catch (error) { |
| | | errorText.value = error instanceof Error ? error.message : 'æ§è¡ç»ææªç¡®è®¤ï¼è¯·å·æ°åè¡¨å¹¶éæ°é¢è§'; |
| | | // 请æ±å¤±è´¥æç»ææªç¡®è®¤åå¿
é¡»éç®ï¼æ§é¢è§ä¸è½ç´æ¥å次æäº¤ã |
| | | preview.value = undefined; |
| | | acceptLate.value = false; |
| | | emit('success'); // å·æ°å·¥ååçç¹å¾ï¼æ ¸å¯¹è¶
æ¶è¯·æ±æ¯å¦å·²ç»æäº¤ã |
| | | } finally { |
| | | executing.value = false; |
| | | modalApi.unlock(); |
| | | } |
| | | } |
| | | |
| | | const orderColumns = [ |
| | | { title: 'å·¥åç¼ç ', dataIndex: 'workOrderCode', width: 170 }, |
| | | { title: 'ç产æ°é', dataIndex: 'quantity', width: 100 }, |
| | | { title: 'æ°å¢ä»»å¡', dataIndex: 'taskCount', width: 90 }, |
| | | { title: 'éæ±æ¥æ', dataIndex: 'requestDate', width: 120 }, |
| | | { title: 'é¢è®¡å®æ', dataIndex: 'endTime', width: 175 }, |
| | | { title: 'æ ¡éªç»æ', dataIndex: 'error', width: 300 }, |
| | | ]; |
| | | const taskColumns = [ |
| | | { title: 'å·¥å', dataIndex: 'workOrderCode', width: 160 }, |
| | | { title: 'å·¥åº', dataIndex: 'processName', width: 130 }, |
| | | { title: 'å·¥ä½ç«', dataIndex: 'workstationName', width: 150 }, |
| | | { title: '设å¤', dataIndex: 'machineryNames', width: 180 }, |
| | | { title: 'æ¬æ¬¡æ°é', dataIndex: 'quantity', width: 100 }, |
| | | { title: 'å°æ¶äº§è½', dataIndex: 'hourlyCapacity', width: 100 }, |
| | | { title: 'å·¥æ¶(h)', dataIndex: 'duration', width: 85 }, |
| | | { title: 'å¼å·¥æ¶é´', dataIndex: 'startTime', width: 180 }, |
| | | { title: '宿æ¶é´', dataIndex: 'endTime', width: 180 }, |
| | | ]; |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="ä¸é®æäº§" class="w-[min(96vw,1200px)]"> |
| | | <div class="space-y-5 px-1"> |
| | | <Alert type="info" show-icon message="å
æ ¡éªä¸é¢è§ï¼å确认æäº§" |
| | | description="æäº¤æä¼å
åå·¥åºé¡ºåºå®æå·¥ä½ç«ä¸è®¾å¤ï¼æ¯å¤©09:00â18:00ï¼æé¤å¨æ«åå·²é
ç½®è忥ï¼é工使¶é´èªå¨é¡ºå»¶ãå·²é¨åæäº§çå·¥ååªè¡¥é½å©ä½æ°éï¼æå¤30个工åã200个æ°ä»»å¡ã" /> |
| | | <div class="flex flex-wrap items-end gap-3"> |
| | | <label class="flex flex-col gap-2"> |
| | | <span class="font-medium">ææ©å¼å·¥æ¶é´</span> |
| | | <DatePicker v-model:value="startTime" show-time :allow-clear="false" :disabled="executing" |
| | | format="YYYY-MM-DD HH:mm:ss" :disabled-date="(date) => date.endOf('day').isBefore(dayjs())" /> |
| | | </label> |
| | | <Button :loading="loading" :disabled="executing" @click="calculate"> |
| | | {{ preview ? 'éæ°è®¡ç®é¢è§' : 'çææäº§é¢è§' }} |
| | | </Button> |
| | | <span class="text-sm text-muted-foreground">已鿩 {{ ids.length }} 个工å</span> |
| | | </div> |
| | | <Alert v-if="errorText" type="error" show-icon :message="errorText" /> |
| | | <template v-if="preview"> |
| | | <Descriptions :column="{ xs: 1, sm: 3 }" size="small" bordered> |
| | | <Descriptions.Item label="工忰">{{ preview.orders.length }}</Descriptions.Item> |
| | | <Descriptions.Item label="æ°å¢ä»»å¡æ°">{{ preview.tasks.length }}</Descriptions.Item> |
| | | <Descriptions.Item label="é¢è§æææ">15åéï¼æäº¤æ¶éæ°æ ¡éª</Descriptions.Item> |
| | | </Descriptions> |
| | | <Alert v-if="!preview.canExecute" type="error" show-icon message="é¨åå·¥åæªéè¿æ ¡éªï¼æ¬æ¹æ¬¡ä¸è½æäº¤" |
| | | description="è¯·æ ¹æ®ä¸æ¹æ ¡éªç»æä¿®æ£å·¥åãè®¾å¤æå·²æä»»å¡çé
ç½®ï¼åéæ°è®¡ç®é¢è§ã" /> |
| | | <div> |
| | | <h3 class="mb-2 font-medium">å·¥åæ ¡éª</h3> |
| | | <Table :columns="orderColumns" :data-source="preview.orders" row-key="workOrderId" |
| | | :pagination="false" size="small" :scroll="{ x: 1055 }"> |
| | | <template #bodyCell="{ column, record, text }"> |
| | | <template v-if="column.dataIndex === 'error'"> |
| | | <span v-if="record.error" class="text-destructive">{{ record.error }}</span> |
| | | <Tag v-else :color="record.late ? 'orange' : 'green'">{{ record.late ? 'æå»¶æé£é©' : 'éè¿' }}</Tag> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'requestDate'">{{ text ? String(text).slice(0, 10) : 'æªæå®' }}</template> |
| | | </template> |
| | | </Table> |
| | | </div> |
| | | <div v-if="preview.tasks.length"> |
| | | <h3 class="mb-2 font-medium">ä»»å¡å®æ</h3> |
| | | <Table :columns="taskColumns" :data-source="preview.tasks" |
| | | :row-key="(row) => `${row.workOrderId}-${row.processId}`" :pagination="{ pageSize: 10, showSizeChanger: false }" |
| | | size="small" :scroll="{ x: 1365 }" /> |
| | | </div> |
| | | <Alert v-if="preview.warnings.length" type="warning" show-icon message="è¯·æ ¸å¯¹äº¤æé£é©"> |
| | | <template #description> |
| | | <p v-for="warning in preview.warnings" :key="warning">{{ warning }}</p> |
| | | <Checkbox v-model:checked="acceptLate" :disabled="executing" class="mt-2"> |
| | | æå·²æ ¸å¯¹å¹¶æ¥å以ä¸äº¤æå»¶æé£é© |
| | | </Checkbox> |
| | | </template> |
| | | </Alert> |
| | | </template> |
| | | </div> |
| | | </Modal> |
| | | </template> |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesProWorkOrderApi, TaskInfo } from '#/api/mes/pro/workorder'; |
| | | import type { MesProWorkOrderApi, TaskInfo, WorkOrderProgress } from '#/api/mes/pro/workorder'; |
| | | |
| | | import { ref } from 'vue'; |
| | | import { useRouter } from 'vue-router'; |
| | |
| | | getWorkOrderProgress, |
| | | checkAndAutoFinishWorkOrder, |
| | | } from '#/api/mes/pro/workorder'; |
| | | import { previewAutoGenerate, autoGenerateTasks } from '#/api/mes/pro/task'; |
| | | |
| | | import ArchiveDetail from '#/views/mes/pd/archive/modules/detail.vue'; |
| | | import { useGridColumns, useGridFormSchema, useTaskGridColumns } from './data'; |
| | | import QuickFeedbackForm from './modules/quick-feedback.vue'; |
| | | import SchedulePreviewModal from './modules/schedule-preview.vue'; |
| | | import AutoSchedule from '../task/modules/auto-schedule.vue'; |
| | | |
| | | defineOptions({ name: 'WorkorderCenter' }); |
| | | const archiveDetailRef = ref<InstanceType<typeof ArchiveDetail>>(); |
| | | const archiveVisible = ref(false); |
| | | const archiveProductCode = ref(''); |
| | | |
| | | function handleArchive(row: MesProWorkOrderApi.WorkOrder) { |
| | | archiveProductCode.value = row.productCode ?? ''; |
| | | archiveVisible.value = true; |
| | | } |
| | | |
| | | const router = useRouter(); |
| | | |
| | | const progressData = ref<MesProWorkOrderApi.WorkOrderProgress | null>(null); |
| | | const progressData = ref<WorkOrderProgress | null>(null); |
| | | const activeProcessKeys = ref<string[]>([]); |
| | | const expandedRowId = ref<number | null>(null); |
| | | |
| | |
| | | }); |
| | | |
| | | const [SchedulePreviewModalWrap, schedulePreviewModalApi] = useVbenModal({ |
| | | connectedComponent: SchedulePreviewModal, |
| | | connectedComponent: AutoSchedule, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | |
| | | .open(); |
| | | } |
| | | |
| | | /** ä¸é®æäº§é¢è§ */ |
| | | async function handleSchedulePreview(row: MesProWorkOrderApi.WorkOrder) { |
| | | try { |
| | | const preview = await previewAutoGenerate({ workOrderId: row.id! }); |
| | | schedulePreviewModalApi |
| | | .setData({ workOrder: row, preview }) |
| | | .open(); |
| | | } catch { |
| | | message.error('è·åæäº§é¢è§å¤±è´¥'); |
| | | } |
| | | } |
| | | |
| | | /** æ§è¡ä¸é®æäº§ */ |
| | | async function handleScheduleExecute(row: MesProWorkOrderApi.WorkOrder) { |
| | | try { |
| | | const result = await autoGenerateTasks({ workOrderId: row.id! }); |
| | | message.success(`æäº§æåï¼çæ ${result.taskIds?.length || 0} 个任å¡`); |
| | | handleRefresh(); |
| | | } catch { |
| | | message.error('æäº§å¤±è´¥'); |
| | | } |
| | | /** ä¸ç产æäº§å
±ç¨æ ¡éªãé¢è§åæ§è¡æµç¨ã */ |
| | | function handleSchedulePreview(row: MesProWorkOrderApi.WorkOrder) { |
| | | schedulePreviewModalApi.setData({ workOrderIds: [row.id!] }).open(); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'æäº§é¢è§', |
| | | type: 'link', |
| | | auth: ['mes:pro-task:create'], |
| | | ifShow: row.quantityScheduled < row.quantity, |
| | | onClick: handleSchedulePreview.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'ä¸é®æäº§', |
| | | type: 'link', |
| | | auth: ['mes:pro-task:create'], |
| | | ifShow: row.quantityScheduled < row.quantity, |
| | | popConfirm: { |
| | | title: '确认ä¸é®æäº§ï¼å°æç
§å·¥èºè·¯çº¿èªå¨çæä»»å¡ã', |
| | | confirm: handleScheduleExecute.bind(null, row), |
| | | }, |
| | | ifShow: (row.quantityScheduled ?? 0) < (row.quantity ?? 0), |
| | | onClick: handleSchedulePreview.bind(null, row), |
| | | }, |
| | | { |
| | | label: '彿¡£', |
| | | type: 'link', |
| | | onClick: () => archiveDetailRef.value?.open(row.productCode), |
| | | onClick: () => handleArchive(row), |
| | | }, |
| | | ]" |
| | | /> |
| | |
| | | </Collapse.Panel> |
| | | </Collapse> |
| | | </Card> |
| | | <ArchiveDetail ref="archiveDetailRef" /> |
| | | <ArchiveDetail :visible="archiveVisible" :product-code="archiveProductCode" @update:visible="archiveVisible = $event" /> |
| | | </Page> |
| | | </template> |
| | |
| | | width: 100, |
| | | }, |
| | | { |
| | | field: 'clientCode', |
| | | title: '客æ·ç¼ç ', |
| | | width: 120, |
| | | }, |
| | | { |
| | | field: 'clientName', |
| | | title: '客æ·åç§°', |
| | | width: 120, |
| | |
| | | }, |
| | | }, |
| | | { |
| | | field: 'productionStatus', |
| | | title: 'çäº§ç¶æ', |
| | | width: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.ORDER_PRODUCTION_STATUS }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'processProgressList', |
| | | title: 'å·¥åºç产è¿åº¦', |
| | | width: 350, |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'quantity', |
| | | title: 'è®¢åæ°é', |
| | | width: 100, |
| | | }, |
| | | { |
| | | field: 'clientCode', |
| | | title: '客æ·ç¼ç ', |
| | | width: 120, |
| | | }, |
| | | { |
| | | field: 'clientName', |
| | |
| | | width: 160, |
| | | }, |
| | | { |
| | | field: 'warehouseName', |
| | | title: 'æ¥å·¥ä»åº', |
| | | width: 130, |
| | | formatter: ({ cellValue, row }) => { |
| | | if (!cellValue) return '-'; |
| | | return row.areaName |
| | | ? `${cellValue}/${row.locationName ?? ''}/${row.areaName}` |
| | | : cellValue; |
| | | }, |
| | | }, |
| | | { |
| | | field: 'remark', |
| | | title: '夿³¨', |
| | | minWidth: 120, |
| | |
| | | |
| | | /** å®æè®¢ååæ ¡éª */ |
| | | function handleCheckFinish(row: MesProWorkOrderApi.WorkOrder) { |
| | | if (row.productionStatus !== 2) { |
| | | if (row.status !== MesProWorkOrderStatusEnum.FINISHED) { |
| | | Modal.confirm({ |
| | | title: 'æä½ç¡®è®¤', |
| | | content: 'è¯¥è®¢åæªå®æï¼æ¯å¦ç¡®è®¤è¦ç»§ç»å®æï¼', |
| | |
| | | import { |
| | | MesProWorkOrderStatusEnum, |
| | | } from "@vben/constants"; |
| | | import { isAiEnable } from "@vben/hooks"; |
| | | |
| | | import { Button, message, Tabs } from "ant-design-vue"; |
| | | |
| | |
| | | ); |
| | | const canUseAi = computed( |
| | | () => |
| | | isAiEnable() && |
| | | formData.value?.id && |
| | | (formData.value?.status === MesProWorkOrderStatusEnum.PREPARE || |
| | | formData.value?.status === MesProWorkOrderStatusEnum.CONFIRMED) |
| | |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { getProcessSimpleList } from '#/api/mes/pro/process'; |
| | | import { getRouteProcessListByProduct } from '#/api/mes/pro/route/process'; |
| | | import { |
| | | createWorkOrderProcess, |
| | | getWorkOrderProcess, |
| | | updateWorkOrderProcess, |
| | | } from '#/api/mes/pro/workorder/process'; |
| | | import { getWarehouseSimpleList } from '#/api/mes/wm/warehouse'; |
| | | import { getWarehouseAreaSimpleList } from '#/api/mes/wm/warehouse/area'; |
| | | import { getWarehouseLocationSimpleList } from '#/api/mes/wm/warehouse/location'; |
| | | import { $t } from '#/locales'; |
| | | import { MdItemSelect } from '#/views/mes/md/item/components'; |
| | | |
| | |
| | | const workOrderId = ref<number>(); |
| | | const processList = ref<MesProWorkOrderProcessApi.WorkOrderProcess[]>([]); |
| | | const isCreateMode = ref(false); |
| | | const allowedOutputItemIds = ref<number[]>([]); |
| | | |
| | | const getTitle = computed(() => { |
| | | return formData.value?.id |
| | |
| | | })); |
| | | } |
| | | |
| | | /** |
| | | * è½½å
¥æ¬å·¥å产åå¯éç产åºç©æèå´ |
| | | * |
| | | * åå·¥èºè·¯çº¿æäº§åºå½å±è¿æ»¤åç产åºï¼éç¨åæå + æ¬äº§åä¸å±æåï¼ï¼ |
| | | * å¹¶å¹¶å
¥è¯¥å·¥åºå·²é
ç½®ç产åºï¼é¿å
å岿°æ®å¨éæ°éæ©æ¶è¢«è¯¯æ¸
空ã |
| | | */ |
| | | async function loadAllowedOutputItemIds( |
| | | productId?: number, |
| | | currentOutputItemIds: number[] = [], |
| | | ): Promise<number[]> { |
| | | if (!productId) { |
| | | return []; |
| | | } |
| | | const ids = new Set<number>([productId, ...currentOutputItemIds]); |
| | | const routeProcessList = await getRouteProcessListByProduct(productId); |
| | | (routeProcessList || []).forEach((process) => { |
| | | (process.outputItemIds || []).forEach((id) => { |
| | | if (id) { |
| | | ids.add(id); |
| | | } |
| | | }); |
| | | }); |
| | | return [...ids]; |
| | | } |
| | | |
| | | /** 表å schema */ |
| | | async function loadSchema(): Promise<VbenFormSchema[]> { |
| | | const processOptions = await loadProcessOptions(); |
| | | const [processOptions, warehouseList] = await Promise.all([ |
| | | loadProcessOptions(), |
| | | getWarehouseSimpleList(), |
| | | ]); |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | |
| | | }, |
| | | { |
| | | fieldName: 'workOrderId', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | | { |
| | | fieldName: 'warehouseName', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | | { |
| | | fieldName: 'locationName', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | | { |
| | | fieldName: 'areaName', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | |
| | | componentProps: { |
| | | multiple: true, |
| | | placeholder: 'è¯·éæ©äº§åºç©æ', |
| | | productIds: allowedOutputItemIds.value, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'warehouseId', |
| | | label: 'æ¥å·¥ä»åº', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: (warehouseList || []).map((item) => ({ |
| | | label: item.name ?? '', |
| | | value: item.id!, |
| | | })), |
| | | placeholder: 'é»è®¤åå·¥èºè·¯çº¿é
ç½®ï¼å¯ä¿®æ¹', |
| | | onChange: async (value: number) => { |
| | | const warehouse = (warehouseList || []).find( |
| | | (item) => item.id === value, |
| | | ); |
| | | await formApi.setFieldValue('warehouseName', warehouse?.name); |
| | | await formApi.setFieldValue('locationId', undefined); |
| | | await formApi.setFieldValue('locationName', undefined); |
| | | await formApi.setFieldValue('areaId', undefined); |
| | | await formApi.setFieldValue('areaName', undefined); |
| | | }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'locationId', |
| | | label: 'æ¥å·¥åºåº', |
| | | component: 'Select', |
| | | dependencies: { |
| | | triggerFields: ['warehouseId'], |
| | | disabled: (values) => !values.warehouseId, |
| | | async componentProps(values) { |
| | | const options = values.warehouseId |
| | | ? await getWarehouseLocationSimpleList(values.warehouseId) |
| | | : []; |
| | | return { |
| | | allowClear: true, |
| | | fieldNames: { label: 'name', value: 'id' }, |
| | | options, |
| | | placeholder: 'è¯·éæ©æ¥å·¥åºåº', |
| | | onChange: async (value: number) => { |
| | | const location = options.find((item) => item.id === value); |
| | | await formApi.setFieldValue('locationName', location?.name); |
| | | await formApi.setFieldValue('areaId', undefined); |
| | | await formApi.setFieldValue('areaName', undefined); |
| | | }, |
| | | }; |
| | | }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'areaId', |
| | | label: 'æ¥å·¥åºä½', |
| | | component: 'Select', |
| | | dependencies: { |
| | | triggerFields: ['locationId'], |
| | | disabled: (values) => !values.locationId, |
| | | async componentProps(values) { |
| | | const options = values.locationId |
| | | ? await getWarehouseAreaSimpleList(values.locationId) |
| | | : []; |
| | | return { |
| | | allowClear: true, |
| | | fieldNames: { label: 'name', value: 'id' }, |
| | | options, |
| | | placeholder: 'è¯·éæ©æ¥å·¥åºä½', |
| | | onChange: async (value: number) => { |
| | | const area = options.find((item) => item.id === value); |
| | | await formApi.setFieldValue('areaName', area?.name); |
| | | }, |
| | | }; |
| | | }, |
| | | }, |
| | | }, |
| | | { |
| | |
| | | formData.value = undefined; |
| | | processList.value = []; |
| | | isCreateMode.value = false; |
| | | allowedOutputItemIds.value = []; |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | const schema = await loadSchema(); |
| | | formApi.setState({ schema }); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | |
| | | const data = modalApi.getData<{ |
| | | id?: number; |
| | | productId?: number; |
| | |
| | | processList.value = data.processList || []; |
| | | isCreateMode.value = !data.workOrderId; |
| | | |
| | | if (data.row) { |
| | | // ç¼è¾ä¸´æ¶å表ä¸çå·¥åº |
| | | formData.value = data.row; |
| | | await formApi.setValues(data.row); |
| | | } else if (data.id) { |
| | | // ç¼è¾å·²ä¿åçå·¥åº |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getWorkOrderProcess(data.id); |
| | | modalApi.lock(); |
| | | try { |
| | | // å
åå·¥åºæ°æ®ï¼ç¼è¾å·²ä¿åå·¥åºæ¶ç¨äºåæ¾ï¼ä¹ç¨äºä¿çå
¶å·²æäº§åº |
| | | const savedProcess = !data.row && data.id |
| | | ? await getWorkOrderProcess(data.id) |
| | | : undefined; |
| | | formData.value = data.row ?? savedProcess; |
| | | |
| | | allowedOutputItemIds.value = await loadAllowedOutputItemIds( |
| | | data.productId, |
| | | formData.value?.outputItemIds || [], |
| | | ); |
| | | formApi.setState({ schema: await loadSchema() }); |
| | | |
| | | if (formData.value) { |
| | | // ç¼è¾ä¸´æ¶å表ä¸çå·¥åº / ç¼è¾å·²ä¿åçå·¥åº |
| | | await formApi.setValues(formData.value); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } else { |
| | | // æ°å¢ |
| | | const maxSort = Math.max(0, ...processList.value.map((item) => item.sort || 0)); |
| | | await formApi.setValues({ |
| | | colorCode: '#00AEF3', |
| | | sort: maxSort + 1, |
| | | }); |
| | | } |
| | | } else { |
| | | // æ°å¢ |
| | | formData.value = undefined; |
| | | const maxSort = Math.max(0, ...processList.value.map((item) => item.sort || 0)); |
| | | await formApi.setValues({ |
| | | colorCode: '#00AEF3', |
| | | sort: maxSort + 1, |
| | | }); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | |
| | | backflushFlag: item.backflushFlag, |
| | | outputItemIds: item.outputItemIds, |
| | | outputItemNames: item.outputItemNames, |
| | | warehouseId: item.warehouseId, |
| | | warehouseName: item.warehouseName, |
| | | locationId: item.locationId, |
| | | locationName: item.locationName, |
| | | areaId: item.areaId, |
| | | areaName: item.areaName, |
| | | remark: item.remark, |
| | | bomItems: item.bomItems, |
| | | })); |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | ]; |
| | | } |
| | |
| | | field: "createTime", |
| | | title: "å建æ¶é´", |
| | | width: 180, |
| | | formatter: "formatDateTime", |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: "æä½", |
| | |
| | | label: $t('ui.actionTitle.create', ['åæ°']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['mes:pd:parameter:create'], |
| | | auth: ['mes:pd-process-param:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.export'), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.DOWNLOAD, |
| | | auth: ['mes:pd:parameter:export'], |
| | | auth: ['mes:pd-process-param:export'], |
| | | onClick: handleExport, |
| | | }, |
| | | ]" |
| | |
| | | label: 'å
³èå·¥åº', |
| | | type: 'link', |
| | | icon: ACTION_ICON.LINK, |
| | | auth: ['mes:pd:parameter:update'], |
| | | auth: ['mes:pd-process-param:update'], |
| | | onClick: handleViewProcessLink.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['mes:pd:parameter:update'], |
| | | auth: ['mes:pd-process-param:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['mes:pd:parameter:delete'], |
| | | auth: ['mes:pd-process-param:delete'], |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.paramName]), |
| | | confirm: handleDelete.bind(null, row), |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: "productKeyword", |
| | | label: "产å", |
| | | component: "Input", |
| | | componentProps: { |
| | | allowClear: true, |
| | | maxLength: 100, |
| | | placeholder: "è¾å
¥äº§ååç§°æç¼ç ", |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: "status", |
| | | label: "ç¶æ", |
| | | component: "Select", |
| | |
| | | rules: "selectRequired", |
| | | }, |
| | | { |
| | | fieldName: "yieldRate", |
| | | label: "å·¥åºæ¶ç", |
| | | component: "InputNumber", |
| | | help: "产åºé ÷ æå
¥éãå¡«å 0.98 表示 98%ï¼æªå¡«åæ 100% åä¸æäº§è®¡ç®ã", |
| | | componentProps: { |
| | | class: "!w-full", |
| | | min: 0.0001, |
| | | max: 1, |
| | | step: 0.01, |
| | | precision: 4, |
| | | placeholder: "å¦ 0.98ï¼98%ï¼", |
| | | }, |
| | | rules: z.number().min(0.0001).max(1).nullish(), |
| | | }, |
| | | { |
| | | fieldName: "linkType", |
| | | label: "ä¸ä¸éå·¥åºå
³ç³»", |
| | | component: "Select", |
| | | help: () => |
| | | h( |
| | | "div", |
| | | { |
| | | style: |
| | | "white-space: pre-line; max-width: 320px; line-height: 1.6;", |
| | | }, |
| | | "0=å¼å§-å¼å§(SS)ï¼æ¬å·¥åºå¼å§åï¼ä¸éå·¥åºæè½å¼å§ï¼å¯éå å¹¶è¡å¼å·¥ï¼\n1=ç»æ-ç»æ(FF)ï¼ä¸éå·¥åºå¿
é¡»çå°æ¬å·¥åºç»ææè½ç»æï¼å¹¶è¡ä½åæ¥å®å·¥ï¼\n2=å¼å§-ç»æ(SF)ï¼æ¬å·¥åºå¼å§åï¼ä¸éå·¥åºæè½ç»æ\n3=ç»æ-å¼å§(FS)ï¼æ¬å·¥åºç»æåï¼ä¸éå·¥åºæè½å¼å§ï¼é»è®¤å¼ï¼", |
| | | ), |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.MES_PRO_LINK_TYPE, "number"), |
| | | placeholder: "è¯·éæ©", |
| | | }, |
| | | rules: z.number().optional(), |
| | | rules: z.number().default(3), |
| | | }, |
| | | { |
| | | fieldName: "colorCode", |
| | |
| | | ]; |
| | | } |
| | | |
| | | /** å·¥èºè·¯çº¿äº§å表åï¼multiple = trueï¼æ°å¢æ¨¡å¼ï¼æ¶äº§åå¯å¤éï¼ä¸æ¬¡æ§ç»å®å¤ä¸ª */ |
| | | export function useRouteProductFormSchema( |
| | | onItemChange?: (item: any) => void, |
| | | multiple = false, |
| | | ): VbenFormSchema[] { |
| | | /** å·¥èºè·¯çº¿äº§å表å */ |
| | | export function useRouteProductFormSchema(onItemChange?: (item: any) => void): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: "id", |
| | |
| | | component: "Input", |
| | | dependencies: { triggerFields: [""], show: () => false }, |
| | | }, |
| | | // æ°å¢ï¼å¤éï¼ï¼ä¸æ¬¡ç»å®å¤ä¸ªäº§åï¼ç¼è¾ï¼åéï¼ï¼change åå¡«ç¼ç /åç§°/è§æ ¼/åä½ |
| | | multiple |
| | | ? { |
| | | fieldName: "itemIds", |
| | | label: "产åï¼å¯å¤éï¼", |
| | | component: markRaw(MdItemSelect), |
| | | componentProps: { |
| | | multiple: true, |
| | | placeholder: "è¯·éæ©è¦ç»å®ç产åï¼å¯å¤éï¼", |
| | | }, |
| | | formItemClass: "col-span-2", |
| | | rules: "selectRequired", |
| | | } |
| | | : { |
| | | fieldName: "itemId", |
| | | label: "产å", |
| | | component: markRaw(MdItemSelect), |
| | | componentProps: { |
| | | onChange: onItemChange, |
| | | }, |
| | | formItemClass: "col-span-2", |
| | | rules: "selectRequired", |
| | | }, |
| | | // 产åç©æï¼ä½¿ç¨ä¸å¡èªå®ä¹éæ©å¨ï¼change åå¡«ç¼ç /åç§°/è§æ ¼/åä½ |
| | | { |
| | | fieldName: "itemId", |
| | | label: "产å", |
| | | component: markRaw(MdItemSelect), |
| | | componentProps: { |
| | | onChange: onItemChange, |
| | | }, |
| | | formItemClass: "col-span-2", |
| | | rules: "selectRequired", |
| | | }, |
| | | { |
| | | fieldName: "quantity", |
| | | label: "ç产æ°é", |
| | |
| | | import { getDictLabel } from '@vben/hooks'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button, message, Tooltip } from 'ant-design-vue'; |
| | | import { Alert, Button, message, Tooltip } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteRoute, |
| | | exportRoute, |
| | | getRoutePage, |
| | | importRoute, |
| | | importRouteTemplate, |
| | | importCompleteRoute, |
| | | importCompleteRouteTemplate, |
| | | updateRouteStatus, |
| | | } from '#/api/mes/pro/route'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import ImportModalComponent from '#/components/import-modal'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | // å·¥èºè®¾è®¡ä¸ç产åºç¡æ°æ®å
±ç¨åä¸ä¸ªé
ç½®å¨ï¼é¿å
两å¥é¡µé¢è½åä¸ä¸è´ã |
| | | import Form from '#/views/mes/pro/route/modules/route-configurator.vue'; |
| | | |
| | | const { hasAccessByCodes } = useAccess(); |
| | | const statusEditable = hasAccessByCodes(['mes:pro-route:update']); // æ¯å¦å¯åæ¢ç¶æ |
| | |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ImportModal, importModalApi] = useVbenModal({ |
| | | connectedComponent: ImportModalComponent, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** 导å
¥å·¥èºè·¯çº¿ */ |
| | | function handleImport() { |
| | | importModalApi |
| | | .setData({ |
| | | title: $t('ui.actionTitle.import', ['å·¥èºè·¯çº¿']), |
| | | fileName: 'å·¥èºè·¯çº¿å¯¼å
¥æ¨¡æ¿.xls', |
| | | importFn: importRoute, |
| | | templateFn: importRouteTemplate, |
| | | }) |
| | | .open(); |
| | | } |
| | | |
| | | function handleCompleteImport() { |
| | | importModalApi |
| | | .setData({ |
| | | title: 'æ¹é导å
¥å®æ´å·¥èºè·¯çº¿', |
| | | fileName: '宿´å·¥èºè·¯çº¿å¯¼å
¥æ¨¡æ¿.xlsx', |
| | | importFn: importCompleteRoute, |
| | | templateFn: importCompleteRouteTemplate, |
| | | showUpdateSupport: false, |
| | | }) |
| | | .open(); |
| | | } |
| | | |
| | | /** å建工èºè·¯çº¿ */ |
| | |
| | | ): Promise<boolean | undefined> { |
| | | try { |
| | | await confirm( |
| | | `确认è¦å°"${row.name}"å·¥èºè·¯çº¿åæ¢ä¸ºã${getDictLabel(DICT_TYPE.COMMON_STATUS, newStatus)}ãåï¼`, |
| | | newStatus === CommonStatusEnum.ENABLE |
| | | ? `确认å¯ç¨â${row.name}âåï¼ç³»ç»å°æ£æ¥äº§åºå
¥åºä½ç½®ãå·¥åºäº§åºåå·¥åºæææ¯å¦å®æ´ã` |
| | | : `确认è¦å°â${row.name}âå·¥èºè·¯çº¿åæ¢ä¸ºã${getDictLabel(DICT_TYPE.COMMON_STATUS, newStatus)}ãåï¼`, |
| | | ); |
| | | } catch { |
| | | return false; |
| | |
| | | |
| | | <template> |
| | | <Page auto-content-height><FormModal @success="handleRefresh" /> |
| | | <ImportModal @success="handleRefresh" /> |
| | | <Alert |
| | | class="mb-3" |
| | | message="路线é
置顺åº" |
| | | description="åºæ¬ä¿¡æ¯ â 产åºäº§ååå
¥åºä» â å·¥åºååå·¥åºäº§åº â å·¥åºææ â æ ¡éªå¯ç¨" |
| | | show-icon |
| | | type="info" |
| | | /> |
| | | <Grid table-title="å·¥èºè·¯çº¿å表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | |
| | | auth: ['mes:pro-route:export'], |
| | | onClick: handleExport, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.import', ['å·¥èºè·¯çº¿']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.UPLOAD, |
| | | auth: ['mes:pro-route:import'], |
| | | onClick: handleImport, |
| | | }, |
| | | { |
| | | label: 'æ¹é导å
¥å®æ´è·¯çº¿', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.UPLOAD, |
| | | auth: ['mes:pro-route:import'], |
| | | onClick: handleCompleteImport, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | |
| | | import type { FormType } from '../data'; |
| | | |
| | | import type { MesProRouteApi } from '#/api/mes/pro/route'; |
| | | import type { MesProRouteProductApi } from '#/api/mes/pro/route/product'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { confirm, useVbenModal } from '@vben/common-ui'; |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { Button, Divider, message, Select } from 'ant-design-vue'; |
| | | import { message, TabPane, Tabs } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { createRoute, getRoute, updateRoute } from '#/api/mes/pro/route'; |
| | | import { |
| | | deleteRouteProduct, |
| | | getRouteProductListByRoute, |
| | | } from '#/api/mes/pro/route/product'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | import ProductForm from './product-form.vue'; |
| | | import Workbench from './workbench.vue'; |
| | | import ProcessList from './process-list.vue'; |
| | | import ProductList from './product-list.vue'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formType = ref<FormType>('create'); // è¡¨åæ¨¡å¼ |
| | | const subTab = ref('process'); // å½åæ¿æ´»çå表 Tab |
| | | const formData = ref<MesProRouteApi.Route>(); |
| | | |
| | | const isDetail = computed(() => formType.value === 'detail'); // æ¯å¦æ¥çæ¨¡å¼ |
| | |
| | | : $t('ui.actionTitle.create', ['å·¥èºè·¯çº¿']); |
| | | }); |
| | | |
| | | // ==================== 产åç»å® ==================== |
| | | const products = ref<MesProRouteProductApi.RouteProduct[]>([]); // å
³è产åå表 |
| | | const selectedProductItemId = ref<number>(); // å½åéä¸ç产åç©æç¼å· |
| | | const selectedProduct = computed(() => |
| | | products.value.find((item) => item.itemId === selectedProductItemId.value), |
| | | ); |
| | | const productOptions = computed(() => |
| | | products.value.map((item) => ({ label: item.itemName, value: item.itemId })), |
| | | ); |
| | | |
| | | async function loadProducts() { |
| | | const routeId = formData.value?.id; |
| | | if (!routeId) { |
| | | products.value = []; |
| | | return; |
| | | } |
| | | products.value = (await getRouteProductListByRoute(routeId)) || []; |
| | | if ( |
| | | !products.value.some((item) => item.itemId === selectedProductItemId.value) |
| | | ) { |
| | | selectedProductItemId.value = products.value[0]?.itemId; |
| | | } |
| | | } |
| | | |
| | | const [ProductFormModal, productFormModalApi] = useVbenModal({ |
| | | connectedComponent: ProductForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** æå¼äº§åå¼¹çªï¼ç»å® / ç¼è¾ï¼ */ |
| | | function openProductForm(row?: MesProRouteProductApi.RouteProduct) { |
| | | if (!formData.value?.id) { |
| | | return; |
| | | } |
| | | productFormModalApi |
| | | .setData( |
| | | row |
| | | ? { id: row.id, routeId: formData.value.id } |
| | | : { |
| | | routeId: formData.value.id, |
| | | boundItemIds: products.value |
| | | .map((item) => item.itemId) |
| | | .filter((id): id is number => id != null), |
| | | }, |
| | | ) |
| | | .open(); |
| | | } |
| | | |
| | | /** ç§»é¤äº§åï¼ä¼çº§èå é¤è¯¥äº§åçæå
¥ç©æé
ç½®ï¼ */ |
| | | async function handleRemoveProduct() { |
| | | if (!selectedProduct.value?.id) { |
| | | return; |
| | | } |
| | | try { |
| | | await confirm( |
| | | `确认å°äº§åã${selectedProduct.value.itemName}ãä»è¯¥å·¥èºè·¯çº¿ç§»é¤ï¼å
¶å
¨é¨æå
¥ç©æé
ç½®å°ä¸å¹¶å é¤ã`, |
| | | ); |
| | | } catch { |
| | | return; |
| | | } |
| | | await deleteRouteProduct(selectedProduct.value.id); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | await loadProducts(); |
| | | } |
| | | |
| | | // ==================== åºæ¬ä¿¡æ¯è¡¨å ==================== |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | |
| | | const data = (await formApi.getValues()) as MesProRouteApi.Route; |
| | | try { |
| | | if (formType.value === 'create') { |
| | | // æ°å¢æåååå°ç¼è¾æ¨¡å¼ï¼è¿å
¥ã产åä¸å·¥åºé
ç½®ãå·¥ä½å° |
| | | // æ°å¢æåååå°ç¼è¾æ¨¡å¼ï¼æ¹ä¾¿ç»§ç»ç»´æ¤ç»æå·¥åºãå
³è产å |
| | | const id = await createRoute(data); |
| | | formData.value = { ...data, id }; |
| | | await formApi.setFieldValue('id', id); |
| | | formType.value = 'update'; |
| | | await loadProducts(); |
| | | } else { |
| | | await updateRoute(data); |
| | | formData.value = { ...formData.value, ...data }; |
| | |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | formData.value = undefined; |
| | | products.value = []; |
| | | selectedProductItemId.value = undefined; |
| | | return; |
| | | } |
| | | formApi.setState({ schema: useFormSchema(formApi) }); |
| | | subTab.value = 'process'; |
| | | // å è½½æ°æ® |
| | | const data = modalApi.getData<{ formType: FormType; id?: number }>(); |
| | | formType.value = data.formType; |
| | |
| | | formData.value = await getRoute(data.id); |
| | | // è®¾ç½®å° values |
| | | await formApi.setValues(formData.value); |
| | | await loadProducts(); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-11/12"> |
| | | <Modal :title="getTitle" class="w-4/5"> |
| | | <Form class="mx-4" /> |
| | | <!-- ç¼è¾/详æ
模å¼ä¸å±ç¤ºã产åä¸å·¥åºé
ç½®ãå·¥ä½å°ï¼æ°å¢æ¨¡å¼ä¸éè --> |
| | | <template v-if="formData?.id"> |
| | | <div class="mx-4 mt-4"> |
| | | <Divider class="!my-2" orientation="left"> |
| | | <span class="text-sm">产åä¸å·¥åºé
ç½®</span> |
| | | </Divider> |
| | | <div class="flex flex-wrap items-center gap-3"> |
| | | <span class="shrink-0 text-sm font-medium">ç产产å</span> |
| | | <Select |
| | | v-model:value="selectedProductItemId" |
| | | class="!w-64" |
| | | allow-clear |
| | | :disabled="isDetail" |
| | | placeholder="è¯·éæ©æç»å®äº§å" |
| | | :options="productOptions" |
| | | /> |
| | | <template v-if="!isDetail"> |
| | | <Button type="primary" @click="openProductForm()"> ç»å®äº§å </Button> |
| | | <Button |
| | | :disabled="!selectedProduct" |
| | | @click="openProductForm(selectedProduct)" |
| | | > |
| | | ç¼è¾äº§å |
| | | </Button> |
| | | <Button danger :disabled="!selectedProduct" @click="handleRemoveProduct"> |
| | | ç§»é¤äº§å |
| | | </Button> |
| | | </template> |
| | | </div> |
| | | <p v-if="products.length === 0" class="mt-2 text-xs text-gray-400"> |
| | | å
ç»å®è¦ç产çç©æï¼å为æ¯éå·¥åºé
ç½®ãæå
¥ + 产åºã |
| | | </p> |
| | | <div class="mt-3"> |
| | | <Workbench |
| | | :form-type="formType" |
| | | :product="selectedProduct" |
| | | :route-id="formData.id" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <!-- ç¼è¾/详æ
模å¼ä¸å±ç¤ºå表 Tabï¼æ°å¢æ¨¡å¼ä¸éè --> |
| | | <Tabs |
| | | v-if="formType !== 'create' && formData?.id" |
| | | v-model:active-key="subTab" |
| | | class="mx-4 mt-4" |
| | | > |
| | | <TabPane key="process" tab="ç»æå·¥åº"> |
| | | <ProcessList :form-type="formType" :route-id="formData.id" /> |
| | | </TabPane> |
| | | <TabPane key="product" tab="å
³è产å"> |
| | | <ProductList :form-type="formType" :route-id="formData.id" /> |
| | | </TabPane> |
| | | </Tabs> |
| | | </Modal> |
| | | <ProductFormModal @success="loadProducts" /> |
| | | </template> |
| | | </template> |
| | |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | | formItemClass: 'col-span-1', |
| | | labelWidth: 150, |
| | | labelWidth: 130, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: [], |
| | |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | import { Divider, message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useRouteProductFormSchema } from '../data'; |
| | | import ProductBomList from './product-bom-list.vue'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formData = ref<MesProRouteProductApi.RouteProduct>(); |
| | | const boundItemIds = ref<number[]>([]); // å·²ç»å®æ¬è·¯çº¿ç产åç©æç¼å·ï¼æ°å¢å¤éæ¶è·³è¿ï¼ |
| | | const getTitle = computed(() => |
| | | formData.value?.id |
| | | ? $t('ui.actionTitle.edit', ['å·¥èºè·¯çº¿äº§å']) |
| | | : $t('ui.actionTitle.create', ['å·¥èºè·¯çº¿äº§å']), |
| | | ); |
| | | |
| | | /** MdItemSelect åé change åè°ï¼æç©æç¼ç /åç§°/è§æ ¼/åä½åå¡«å° form */ |
| | | /** MdItemSelect change åè°ï¼æç©æç¼ç /åç§°/è§æ ¼/åä½åå¡«å° form */ |
| | | async function handleItemChange(item?: any) { |
| | | if (!item) { |
| | | return; |
| | |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | const data = (await formApi.getValues()) as MesProRouteProductApi.RouteProduct & { |
| | | itemIds?: number[]; |
| | | }; |
| | | // æäº¤è¡¨å |
| | | const data = |
| | | (await formApi.getValues()) as MesProRouteProductApi.RouteProduct; |
| | | try { |
| | | // ç¼è¾æ¨¡å¼ï¼æ´æ°å个产å |
| | | if (formData.value?.id) { |
| | | await updateRouteProduct(data); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | return; |
| | | // ç¨ææ°è¡¨åå¼åæ¥ formDataï¼ç¡®ä¿äº§å BOM å表ç»å®ç itemId ä¸è¡¨åä¸è´ |
| | | formData.value = { ...formData.value, ...data }; |
| | | } else { |
| | | // æ°å¢æåå忢å°ç¼è¾æ¨¡å¼ï¼æ¹ä¾¿ç»§ç»ç»´æ¤äº§å BOM |
| | | const id = await createRouteProduct(data); |
| | | formData.value = { ...data, id }; |
| | | await formApi.setFieldValue('id', id); |
| | | } |
| | | // æ°å¢æ¨¡å¼ï¼å¤é产å䏿¬¡æ§ç»å®ï¼è·³è¿å·²å¨è·¯çº¿ä¸ç产å |
| | | const itemIds = Array.isArray(data.itemIds) ? data.itemIds : []; |
| | | if (itemIds.length === 0) { |
| | | message.warning('请è³å°éæ©ä¸ä¸ªäº§å'); |
| | | return; |
| | | } |
| | | let created = 0; |
| | | let skipped = 0; |
| | | const failedIds: number[] = []; |
| | | for (const itemId of itemIds) { |
| | | if (itemId == null) { |
| | | continue; |
| | | } |
| | | if (boundItemIds.value.includes(itemId)) { |
| | | skipped++; |
| | | continue; |
| | | } |
| | | try { |
| | | await createRouteProduct({ |
| | | routeId: data.routeId, |
| | | itemId, |
| | | quantity: data.quantity, |
| | | productionTime: data.productionTime, |
| | | timeUnitType: data.timeUnitType, |
| | | remark: data.remark, |
| | | }); |
| | | created++; |
| | | } catch { |
| | | failedIds.push(itemId); |
| | | } |
| | | } |
| | | if (skipped > 0) { |
| | | message.warning(`å·²è·³è¿ ${skipped} 个已ç»å®ç产å`); |
| | | } |
| | | if (failedIds.length > 0) { |
| | | message.error( |
| | | `æ ${failedIds.length} 个产åç»å®å¤±è´¥ï¼å¯è½å·²è¢«å
¶å®å·¥èºè·¯çº¿å ç¨ï¼`, |
| | | ); |
| | | } |
| | | if (created > 0) { |
| | | message.success(`æåç»å® ${created} 个产å`); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | } |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | formData.value = undefined; |
| | | boundItemIds.value = []; |
| | | return; |
| | | } |
| | | // å è½½æ°æ® |
| | | const data = modalApi.getData<{ |
| | | id?: number; |
| | | routeId: number; |
| | | boundItemIds?: number[]; |
| | | }>(); |
| | | if (!data) { |
| | | return; |
| | | } |
| | | boundItemIds.value = data.boundItemIds || []; |
| | | if (!data.id) { |
| | | // æ°å¢æ¨¡å¼ï¼å¤é产åç»å® |
| | | formData.value = undefined; |
| | | await formApi.setState({ |
| | | schema: useRouteProductFormSchema(handleItemChange, true), |
| | | }); |
| | | await formApi.setValues({ |
| | | routeId: data.routeId, |
| | | itemIds: [], |
| | | quantity: 1, |
| | | productionTime: 1, |
| | | timeUnitType: 'MINUTE', |
| | | remark: undefined, |
| | | }); |
| | | await formApi.setValues({ routeId: data.routeId }); |
| | | return; |
| | | } |
| | | // ç¼è¾æ¨¡å¼ï¼å è½½å个产ååå¡« |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getRouteProduct(data.id); |
| | | await formApi.setState({ |
| | | schema: useRouteProductFormSchema(handleItemChange, false), |
| | | }); |
| | | // è®¾ç½®å° values |
| | | await formApi.setValues(formData.value); |
| | | } finally { |
| | | modalApi.unlock(); |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-2/5"> |
| | | <Modal :title="getTitle" class="w-3/5"> |
| | | <Form class="mx-4" /> |
| | | <!-- ç¼è¾æ¨¡å¼ä¸å±ç¤ºäº§å BOM åè¡¨ï¼æ°å¢æ¨¡å¼ä¸éè --> |
| | | <template v-if="formData?.id && formData?.itemId"> |
| | | <Divider class="!my-3" orientation="left">产å BOM é
ç½®</Divider> |
| | | <div class="mx-4"> |
| | | <ProductBomList |
| | | :route-id="formData.routeId!" |
| | | :product-id="formData.itemId" |
| | | :product-name="formData.itemName" |
| | | /> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| | | </template> |
| | |
| | | </div> |
| | | <div class="mt-1 flex flex-wrap gap-x-5 gap-y-1 text-sm text-gray-500"> |
| | | <span>åºå·ï¼{{ selectedProcess.sort }}</span> |
| | | <span> |
| | | å·¥åºæ¶çï¼{{ selectedProcess.yieldRate == null ? 'æªè®¾ç½®ï¼æ 100%ï¼' : `${Number((selectedProcess.yieldRate * 100).toFixed(2))}%` }} |
| | | </span> |
| | | <span>å夿¶é´ï¼{{ selectedProcess.prepareTime ?? 0 }} å</span> |
| | | <span>çå¾
æ¶é´ï¼{{ selectedProcess.waitTime ?? 0 }} å</span> |
| | | <span v-if="selectedProcess.linkType != null"> |
| | |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | </template> |
| | |
| | | field: "createTime", |
| | | title: "å建æ¶é´", |
| | | width: 180, |
| | | formatter: "formatDateTime", |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: "æä½", |
| | |
| | | label: $t('ui.actionTitle.create', ['模æ¿']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['mes:pd:template:create'], |
| | | auth: ['mes:pd-process-param-template:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.export'), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.DOWNLOAD, |
| | | auth: ['mes:pd:template:export'], |
| | | auth: ['mes:pd-process-param-template:export'], |
| | | onClick: handleExport, |
| | | }, |
| | | ]" /> |
| | |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['mes:pd:template:update'], |
| | | auth: ['mes:pd-process-param-template:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['mes:pd:template:delete'], |
| | | auth: ['mes:pd-process-param-template:delete'], |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.templateName]), |
| | | confirm: handleDelete.bind(null, row), |
| | |
| | | placeholder: '请è¾å
¥éå®è®¢åç¼å·', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'purchaseOrderCode', |
| | | label: 'éè´è®¢åç¼å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥éè´è®¢åç¼å·', |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | |
| | | width: 80, |
| | | }, |
| | | { |
| | | field: 'vendorCode', |
| | | title: 'ä¾åºåç¼ç ', |
| | | minWidth: 140, |
| | | }, |
| | | { |
| | | field: 'vendorName', |
| | | title: 'ä¾åºååç§°', |
| | | minWidth: 160, |
| | | }, |
| | | { |
| | | field: 'clientCode', |
| | | title: '客æ·ç¼ç ', |
| | | minWidth: 140, |
| | | }, |
| | | { |
| | | field: 'clientName', |
| | |
| | | { |
| | | field: 'salesOrderCode', |
| | | title: 'éå®è®¢åç¼å·', |
| | | minWidth: 160, |
| | | }, |
| | | { |
| | | field: 'purchaseOrderCode', |
| | | title: 'éè´è®¢åç¼å·', |
| | | minWidth: 160, |
| | | }, |
| | | { |
| | |
| | | span: 3, |
| | | }, |
| | | { |
| | | field: 'purchaseOrderCode', |
| | | label: 'éè´è®¢åç¼å·', |
| | | }, |
| | | { |
| | | field: 'vendorCode', |
| | | label: 'ä¾åºåç¼ç ', |
| | | }, |
| | | { |
| | | field: 'vendorName', |
| | | label: 'ä¾åºååç§°', |
| | | }, |
| | | { |
| | | field: 'salesOrderCode', |
| | | label: 'éå®è®¢åç¼å·', |
| | | }, |
| | | { |
| | | field: 'clientCode', |
| | | label: '客æ·ç¼ç ', |
| | | }, |
| | | { |
| | | field: 'clientName', |
| | |
| | | field: 'receiptDate', |
| | | title: 'å
¥åºæ¥æ', |
| | | minWidth: 160, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | field: 'purchaseOrderCode', |
| | | title: 'éè´è®¢åç¼å·', |
| | | minWidth: 160, |
| | | }, |
| | | { |
| | | field: 'vendorCode', |
| | | title: 'ä¾åºåç¼ç ', |
| | | minWidth: 140, |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'vendorName', |
| | |
| | | field: 'salesDate', |
| | | title: 'åºåºæ¥æ', |
| | | minWidth: 160, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'salesOrderCode', |
| | |
| | | |
| | | import { useDetailSchema } from '../data'; |
| | | import ProcessTraceList from './process-trace-list.vue'; |
| | | import PurchaseSourceList from './purchase-source-list.vue'; |
| | | import SalesTargetList from './sales-target-list.vue'; |
| | | import TraceList from './trace-list.vue'; |
| | | |
| | | const detailData = ref<MesWmBatchApi.Batch>(); // å½åæ¹æ¬¡è¯¦æ
|
| | | const subTabsName = ref< |
| | | | 'backward' |
| | | | 'forward' |
| | | | 'purchase' |
| | | | 'sales' |
| | | | 'process' |
| | | >('forward'); // å½åæ¿æ´»çè¿½æº¯é¡µç¾ |
| | | >('backward'); // å½åæ¿æ´»çè¿½æº¯é¡µç¾ |
| | | |
| | | const [Descriptions] = useDescription({ |
| | | bordered: true, |
| | |
| | | return; |
| | | } |
| | | detailData.value = modalApi.getData<MesWmBatchApi.Batch>(); |
| | | subTabsName.value = 'forward'; |
| | | subTabsName.value = 'backward'; |
| | | }, |
| | | }); |
| | | </script> |
| | |
| | | class="mx-4 mt-4" |
| | | type="card" |
| | | > |
| | | <Tabs.TabPane key="forward"> |
| | | <template #tab> |
| | | <Tooltip title="顺çç产æµå¾ä¸æ¸¸æ¥ï¼è¯¥æ¹æ¬¡è¢«åªäºå·¥åæ¶èåï¼äº§åºæäºåªäºæåæ¹æ¬¡"> |
| | | <span>åå追溯</span> |
| | | </Tooltip> |
| | | </template> |
| | | <TraceList :batch-code="detailData.code" direction="forward" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane key="backward"> |
| | | <template #tab> |
| | | <Tooltip title="éçç产æµå¾ä¸æ¸¸æ¥ï¼çäº§è¯¥æ¹æ¬¡æ¶ï¼æ¶èäºåªäºåææ¹æ¬¡"> |
| | |
| | | </Tooltip> |
| | | </template> |
| | | <TraceList :batch-code="detailData.code" direction="backward" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane key="purchase"> |
| | | <template #tab> |
| | | <Tooltip title="åæä¾§æ¥æºï¼è¯¥æ¹æ¬¡å¯¹åºçéè´å
¥åºåä¸ IQC æ¥ææ£éªè®°å½"> |
| | | <span>éè´æ¥æº</span> |
| | | </Tooltip> |
| | | </template> |
| | | <PurchaseSourceList :batch-code="detailData.code" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane key="sales"> |
| | | <template #tab> |
| | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { isAiEnable } from '@vben/hooks'; |
| | | |
| | | import { Empty, Input, Spin } from 'ant-design-vue'; |
| | | |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <a-button type="default" @click="modalApi.open()"> |
| | | <a-button v-if="isAiEnable()" type="default" @click="modalApi.open()"> |
| | | è´¨æ£å©æ |
| | | </a-button> |
| | | |
| | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { isAiEnable } from '@vben/hooks'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { Alert, Button, message, Tag } from 'ant-design-vue'; |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <Button type="default" :loading="loading" @click="handleSuggest"> |
| | | <Button |
| | | v-if="isAiEnable()" |
| | | type="default" |
| | | :loading="loading" |
| | | @click="handleSuggest" |
| | | > |
| | | <template #icon> |
| | | <IconifyIcon icon="ant-design:robot-outlined" /> |
| | | </template> |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | import { generateAutoCode } from '#/api/mes/md/autocode/record'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'update'; |
| | | export type FormType = 'create' | 'update' | 'detail'; |
| | | |
| | | /** æ°å¢/ä¿®æ¹æ£æµç»æç表å */ |
| | | export function useQcIndicatorResultFormSchema( |
| | | formApi?: VbenFormApi, |
| | | readonly = false, |
| | | ): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | |
| | | placeholder: '请è¾å
¥æ ·åç¼å·', |
| | | }, |
| | | rules: 'required', |
| | | suffix: () => |
| | | suffix: readonly ? undefined : () => |
| | | h( |
| | | Button, |
| | | { |
| | |
| | | }, |
| | | { default: () => 'çæ' }, |
| | | ), |
| | | }, |
| | | { |
| | | fieldName: 'sn', |
| | | label: 'ç©èµ SN', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥ç©èµ SN', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | |
| | | width: 200, |
| | | }, |
| | | { |
| | | field: 'sn', |
| | | title: 'ç©èµSN', |
| | | minWidth: 200, |
| | | }, |
| | | { |
| | | field: 'remark', |
| | | title: '夿³¨', |
| | | minWidth: 200, |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 150, |
| | | width: 240, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formType = ref<FormType>('create'); |
| | | const isDetail = computed(() => formType.value === 'detail'); |
| | | const items = ref<MesQcIndicatorResultApi.IndicatorResultDetail[]>([]); |
| | | const ctxData = ref<CtxData>(); |
| | | |
| | | const getTitle = computed(() => |
| | | formType.value === 'update' |
| | | isDetail.value |
| | | ? 'æ£æµç»æè¯¦æ
' |
| | | : formType.value === 'update' |
| | | ? $t('ui.actionTitle.edit', ['æ£éªç»æ']) |
| | | : $t('ui.actionTitle.create', ['æ£éªç»æ']), |
| | | ); |
| | |
| | | : []; |
| | | } |
| | | |
| | | function getDisplayValue(item: MesQcIndicatorResultApi.IndicatorResultDetail) { |
| | | if (item.value === undefined || item.value === null || item.value === '') { |
| | | return 'æªå¡«å'; |
| | | } |
| | | if (item.valueType === MesQcResultValueType.DICT) { |
| | | return getValueOptions(item.valueSpecification).find( |
| | | (option) => option.value === String(item.value), |
| | | )?.label ?? item.value; |
| | | } |
| | | return item.value; |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | if (!ctxData.value) { |
| | | if (!ctxData.value || isDetail.value) { |
| | | return; |
| | | } |
| | | const { valid } = await formApi.validate(); |
| | |
| | | items.value = []; |
| | | return; |
| | | } |
| | | formApi.setState({ schema: useQcIndicatorResultFormSchema(formApi) }); |
| | | // å è½½æ°æ® |
| | | const data = modalApi.getData<CtxData>(); |
| | | ctxData.value = data; |
| | | formType.value = data.formType; |
| | | formApi.setState({ |
| | | schema: useQcIndicatorResultFormSchema(formApi, isDetail.value), |
| | | }); |
| | | formApi.setDisabled(isDetail.value); |
| | | modalApi.setState({ |
| | | showConfirmButton: !isDetail.value, |
| | | cancelText: isDetail.value ? 'å
³é' : $t('common.cancel'), |
| | | }); |
| | | modalApi.lock(); |
| | | try { |
| | | const detail = await getIndicatorResultDetail( |
| | |
| | | valueNumber: |
| | | (item.valueType === MesQcResultValueType.FLOAT || |
| | | item.valueType === MesQcResultValueType.INTEGER) && |
| | | item.value !== null |
| | | item.value != null && |
| | | String(item.value).trim() !== '' && |
| | | Number.isFinite(Number(item.value)) |
| | | ? Number(item.value) |
| | | : undefined, |
| | | })); |
| | |
| | | id: detail.id, |
| | | code: detail.code, |
| | | remark: detail.remark, |
| | | sn: detail.sn, |
| | | }); |
| | | } finally { |
| | | modalApi.unlock(); |
| | |
| | | <AForm.Item |
| | | :label="`æ£æµé¡¹${index + 1}ï¼${item.indicatorName ?? ''}`" |
| | | > |
| | | <div v-if="isDetail" class="whitespace-pre-wrap break-words py-1"> |
| | | {{ getDisplayValue(item) }} |
| | | <div v-if="item.remark" class="mt-1 text-sm text-muted-foreground"> |
| | | 夿³¨ï¼{{ item.remark }} |
| | | </div> |
| | | </div> |
| | | <InputNumber |
| | | v-if=" |
| | | v-else-if=" |
| | | item.valueType === MesQcResultValueType.FLOAT || |
| | | item.valueType === MesQcResultValueType.INTEGER |
| | | " |
| | |
| | | .open(); |
| | | } |
| | | |
| | | /** æ¥çæ£æµç»æï¼è´¨æ£å®æåä»å¯æå¼åªè¯»è¯¦æ
ã */ |
| | | function handleDetail(row: MesQcIndicatorResultApi.IndicatorResult) { |
| | | formModalApi |
| | | .setData({ |
| | | formType: 'detail', |
| | | id: row.id, |
| | | qcId: props.qcId, |
| | | qcType: props.qcType, |
| | | }) |
| | | .open(); |
| | | } |
| | | |
| | | /** å 餿£æµç»æ */ |
| | | async function handleDelete(row: MesQcIndicatorResultApi.IndicatorResult) { |
| | | const hideLoading = message.loading({ |
| | |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'æ¥ç详æ
', |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | onClick: handleDetail.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | MesProTaskStatusEnum, |
| | | MesProWorkOrderStatusEnum, |
| | | } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | |
| | | import { generateAutoCode } from '#/api/mes/md/autocode/record'; |
| | | import { getTemplateItemOptionList } from '#/api/mes/qc/template/item'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import { getRangePickerDefaultProps } from '#/utils'; |
| | | import { getDateRangePickerDefaultProps } from '#/utils'; |
| | | import { MdItemSelect } from '#/views/mes/md/item/components'; |
| | | import { MdWorkstationSelect } from '#/views/mes/md/workstation/components'; |
| | | import { ProTaskSelect } from '#/views/mes/pro/task/components'; |
| | |
| | | component: markRaw(ProWorkOrderSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©ç产订å', |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | // 订ååæ´æ¶æ¸
空çäº§ä»»å¡ |
| | | onChange: () => formApi?.setFieldValue('taskId', undefined), |
| | | }, |
| | |
| | | componentProps: (values) => ({ |
| | | disabled: !!values.sourceDocId, |
| | | placeholder: 'è¯·éæ©ç产订å', |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | onChange: () => formApi?.setFieldValue('taskId', undefined), |
| | | }), |
| | | }, |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'inspectorUserId', |
| | | label: 'æ£æµäººå', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | allowClear: true, |
| | | api: getSimpleUserList, |
| | | labelField: 'nickname', |
| | | placeholder: 'è¯·éæ©æ£æµäººå', |
| | | showSearch: true, |
| | | optionFilterProp: 'label', |
| | | valueField: 'id', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'inspectDate', |
| | | label: 'æ£æµæ¥æ', |
| | | component: 'RangePicker', |
| | | componentProps: { |
| | | ...getRangePickerDefaultProps(), |
| | | ...getDateRangePickerDefaultProps(), |
| | | }, |
| | | }, |
| | | ]; |
| | |
| | | |
| | | import { generateAutoCode } from '#/api/mes/md/autocode/record'; |
| | | import { getTemplateItemOptionList } from '#/api/mes/qc/template/item'; |
| | | import { getSupplierSimpleList } from '#/api/srm/supplier'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import { getRangePickerDefaultProps } from '#/utils'; |
| | | import { getDateRangePickerDefaultProps } from '#/utils'; |
| | | import { MdmItemSelect } from '#/views/basicData/mdm/components'; |
| | | |
| | | |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'vendorId', |
| | | fieldName: 'vendorName', |
| | | label: 'ä¾åºå', |
| | | component: 'ApiSelect', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | api: getSupplierSimpleList, |
| | | labelField: 'name', |
| | | placeholder: 'è¯·éæ©ä¾åºå', |
| | | valueField: 'id', |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['sourceDocId'], |
| | | componentProps: (values) => ({ |
| | | disabled: !!values.sourceDocId, |
| | | allowClear: true, |
| | | api: getSupplierSimpleList, |
| | | labelField: 'name', |
| | | placeholder: 'è¯·éæ©ä¾åºå', |
| | | valueField: 'id', |
| | | }), |
| | | maxlength: 255, |
| | | placeholder: '请è¾å
¥ä¾åºååç§°ï¼å¯ç空ï¼', |
| | | }, |
| | | }, |
| | | { |
| | |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥ä¾åºåæ¹æ¬¡å·', |
| | | }, |
| | | suffix: () => |
| | | h( |
| | | Button, |
| | | { |
| | | disabled: formType === 'detail', |
| | | type: 'default', |
| | | onClick: async () => { |
| | | const code = await generateAutoCode( |
| | | MesAutoCodeRuleCode.QC_IQC_VENDOR_BATCH_CODE, |
| | | ); |
| | | await formApi?.setFieldValue('vendorBatch', code); |
| | | }, |
| | | }, |
| | | { default: () => 'çæ' }, |
| | | ), |
| | | }, |
| | | { |
| | | fieldName: 'receivedQuantity', |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'vendorId', |
| | | fieldName: 'vendorName', |
| | | label: 'ä¾åºå', |
| | | component: 'ApiSelect', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | api: getSupplierSimpleList, |
| | | labelField: 'name', |
| | | placeholder: 'è¯·éæ©ä¾åºå', |
| | | valueField: 'id', |
| | | placeholder: '请è¾å
¥ä¾åºååç§°', |
| | | }, |
| | | }, |
| | | { |
| | |
| | | label: 'æ¥ææ¥æ', |
| | | component: 'RangePicker', |
| | | componentProps: { |
| | | ...getRangePickerDefaultProps(), |
| | | ...getDateRangePickerDefaultProps(), |
| | | }, |
| | | }, |
| | | { |
| | |
| | | label: 'æ£æµæ¥æ', |
| | | component: 'RangePicker', |
| | | componentProps: { |
| | | ...getRangePickerDefaultProps(), |
| | | ...getDateRangePickerDefaultProps(), |
| | | }, |
| | | }, |
| | | { |
| | |
| | | minWidth: 180, |
| | | }, |
| | | { |
| | | field: 'vendorNickname', |
| | | title: 'ä¾åºåç®ç§°', |
| | | field: 'vendorName', |
| | | title: 'ä¾åºå', |
| | | width: 120, |
| | | }, |
| | | { |
| | |
| | | fileName: 'æ¥ææ£éªå导å
¥æ¨¡æ¿.xls', |
| | | hints: [ |
| | | 'ä»
å
许导å
¥ xlsãxlsx æ ¼å¼æä»¶ï¼å¿
å¡«åï¼æ£éªååç§°ã产åç©æç¼ç ãåæ ¼åæ°éãä¸åæ ¼åæ°éãæ£æµæ¥æãæ¥ææ¥æã', |
| | | 'ã产åç©æç¼ç ãéä¸ãç©æç®¡çãä¸çç¼ç å®å
¨ä¸è´ï¼ãä¾åºååç§°ãéä¸ãä¾åºå管çãä¸çåç§°ä¸è´ï¼ç空åè¯¥åæ ä¾åºåã', |
| | | 'ã产åç©æç¼ç ãéä¸ãç©æç®¡çãä¸çç¼ç å®å
¨ä¸è´ï¼ãä¾åºååç§°ãéå¿
å¡«ï¼å¯ç´æ¥å¡«ååç§°ï¼æ éå¨ç³»ç»ä¸å»ºç«ä¾åºåæ¡£æ¡ã', |
| | | 'åæ ¼åæ°é + ä¸åæ ¼åæ°é å¿
é¡»çäºãæ¬æ¬¡æ¥æ¶æ°éãï¼ç空æ¶èªå¨å两è
ä¹åï¼ï¼ãæ£æµç»æãæåå
¸åç§°å¡«åï¼æ ¡éªéè¿ / æ ¡éªä¸éè¿ï¼ã', |
| | | 'ãè´¨æ£æ¹æ¡ãå¡«æ¹æ¡åç§°ï¼ç空åæç©æèªå¨å¹é
IQC æ¹æ¡ï¼ãæ£æµäººåãç空åå½åç»å½ç¨æ·ï¼æ¥ææ¯æ 2026-01-01 10:00:00 æ 2026-01-01ã', |
| | | 'éè¡å¯¼å
¥ï¼å¤±è´¥è¡ä¸å½±åæåè¡ï¼å¯¼å
¥åçæ£éªå为ãå¾
æ£éªãç¶æï¼å¯å¨å表ä¸ç»§ç»ç¼è¾ãå½å
¥æ£æµç»æã', |
| | |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import { getRangePickerDefaultProps } from '#/utils'; |
| | | import { getDateRangePickerDefaultProps } from '#/utils'; |
| | | import { MdmItemSelect } from '#/views/basicData/mdm/components'; |
| | | |
| | | /** å表çæç´¢è¡¨å */ |
| | |
| | | label: 'æ£æµæ¥æ', |
| | | component: 'RangePicker', |
| | | componentProps: { |
| | | ...getRangePickerDefaultProps(), |
| | | ...getDateRangePickerDefaultProps(), |
| | | }, |
| | | }, |
| | | { |
| | |
| | | field: 'inspectDate', |
| | | title: 'æ£æµæ¥æ', |
| | | width: 170, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'status', |
| | |
| | | allowClear: true, |
| | | class: '!w-full', |
| | | placeholder: 'è¯·éæ©å建æ¶é´', |
| | | showTime: true, |
| | | showTime: false, |
| | | type: 'date', |
| | | }, |
| | | }, |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | import { generateAutoCode } from '#/api/mes/md/autocode/record'; |
| | | import { getTemplateItemOptionList } from '#/api/mes/qc/template/item'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import { getRangePickerDefaultProps } from '#/utils'; |
| | | import { getDateRangePickerDefaultProps } from '#/utils'; |
| | | import CrmCustomerSelect from '#/components/crm-customer-select.vue'; |
| | | import { MdmItemSelect } from '#/views/basicData/mdm/components'; |
| | | |
| | |
| | | dependencies: { |
| | | triggerFields: ['sourceDocId'], |
| | | componentProps: (values) => ({ |
| | | allCustomers: true, |
| | | disabled: !!values.sourceDocId, |
| | | placeholder: 'è¯·éæ©äº§åç©æ', |
| | | }), |
| | |
| | | label: '客æ·', |
| | | component: markRaw(CrmCustomerSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©å®¢æ·', |
| | | allCustomers: true, |
| | | allowClear: true, |
| | | placeholder: 'è¯·éæ©å®¢æ·ï¼å¯ä¸å¡«ï¼', |
| | | }, |
| | | rules: 'selectRequired', |
| | | dependencies: { |
| | | triggerFields: ['sourceDocId'], |
| | | componentProps: (values) => ({ |
| | | disabled: !!values.sourceDocId, |
| | | placeholder: 'è¯·éæ©å®¢æ·', |
| | | allowClear: true, |
| | | placeholder: 'è¯·éæ©å®¢æ·ï¼å¯ä¸å¡«ï¼', |
| | | }), |
| | | }, |
| | | }, |
| | |
| | | label: '客æ·', |
| | | component: markRaw(CrmCustomerSelect), |
| | | componentProps: { |
| | | allCustomers: true, |
| | | placeholder: 'è¯·éæ©å®¢æ·', |
| | | }, |
| | | }, |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'inspectorUserId', |
| | | label: 'æ£æµäººå', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | allowClear: true, |
| | | api: getSimpleUserList, |
| | | labelField: 'nickname', |
| | | placeholder: 'è¯·éæ©æ£æµäººå', |
| | | showSearch: true, |
| | | optionFilterProp: 'label', |
| | | valueField: 'id', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'inspectDate', |
| | | label: 'æ£æµæ¥æ', |
| | | component: 'RangePicker', |
| | | componentProps: { |
| | | ...getRangePickerDefaultProps(), |
| | | ...getDateRangePickerDefaultProps(), |
| | | }, |
| | | }, |
| | | ]; |
| | |
| | | importApi: importOqc, |
| | | fileName: 'åºè´§æ£éªå导å
¥æ¨¡æ¿.xls', |
| | | hints: [ |
| | | 'ä»
å
许导å
¥ xlsãxlsx æ ¼å¼æä»¶ï¼å¿
å¡«åï¼æ£éªååç§°ã产åç©æç¼ç ã客æ·åç§°ãæ¬æ¬¡åºè´§æ°éãåæ ¼åæ°éãä¸åæ ¼åæ°éãæ£æµæ¥æãåºè´§æ¥æã', |
| | | 'ã产åç©æç¼ç ãéä¸ãç©æç®¡çãä¸çç¼ç å®å
¨ä¸è´ï¼ã客æ·åç§°ãéä¸ãCRM 客æ·ç®¡çãä¸çåç§°ä¸è´ï¼ä¸å¿
é¡»å¨å½åç»å½ç¨æ·çå®¢æ·æ°æ®æéèå´å
ã', |
| | | 'ä»
å
许导å
¥ xlsãxlsx æ ¼å¼æä»¶ï¼å¿
å¡«åï¼æ£éªååç§°ã产åç©æç¼ç ãæ¬æ¬¡åºè´§æ°éãåæ ¼åæ°éãä¸åæ ¼åæ°éãæ£æµæ¥æãåºè´§æ¥æã客æ·åç§°å¯éã', |
| | | 'ã产åç©æç¼ç ãéä¸ãç©æç®¡çãä¸çç¼ç å®å
¨ä¸è´ï¼å¡«å客æ·åç§°æ¶ï¼éä¸ãCRM 客æ·ç®¡çãä¸ç客æ·åç§°ä¸è´ã', |
| | | 'åæ ¼åæ°é + ä¸åæ ¼åæ°é å¿
é¡»çäºãæ£æµæ°éãï¼ç空æ¶èªå¨å两è
ä¹åï¼ï¼ãæ£æµç»æãæåå
¸åç§°å¡«åï¼æ ¡éªéè¿ / æ ¡éªä¸éè¿ï¼ã', |
| | | 'ãè´¨æ£æ¹æ¡ãå¡«æ¹æ¡åç§°ï¼ç空åæç©æèªå¨å¹é
OQC æ¹æ¡ï¼ãæ£æµäººåãç空åå½åç»å½ç¨æ·ï¼æ¥ææ¯æ 2026-01-01 10:00:00 æ 2026-01-01ã', |
| | | 'éè¡å¯¼å
¥ï¼å¤±è´¥è¡ä¸å½±åæåè¡ï¼å¯¼å
¥åçæ£éªå为ãå¾
æ£éªãç¶æï¼å¯å¨å表ä¸ç»§ç»ç¼è¾ãå½å
¥æ£æµç»æã', |
| | |
| | | sourceDocType: row.sourceDocType, |
| | | sourceLineId: row.sourceLineId, |
| | | vendorId: row.vendorId, |
| | | vendorName: row.vendorName, |
| | | }; |
| | | iqcFormModalApi.setData({ formType: 'create', prefill }).open(); |
| | | } |
| | |
| | | field: 'createTime', |
| | | title: 'çææ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | |
| | | import { useAccess } from '@vben/access'; |
| | | import { Page } from '@vben/common-ui'; |
| | | import { useTabs } from '@vben/hooks'; |
| | | import { isAiEnable, useTabs } from '@vben/hooks'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { |
| | |
| | | const { hasAccessByCodes } = useAccess(); |
| | | |
| | | /** AI 导å
¥æ¯ç¬ç«æéç ï¼è½æ¹æ¨¡æ¿ä¸çäºè½ç¨å¤§æ¨¡åè¯å«æä»¶ï¼æ¯æ¬¡è°ç¨é½è¦è±é±ï¼ */ |
| | | const canAiImport = computed(() => |
| | | hasAccessByCodes(['qc-report:template:ai-import']), |
| | | const canAiImport = computed( |
| | | () => isAiEnable() && hasAccessByCodes(['qc-report:template:ai-import']), |
| | | ); |
| | | |
| | | const loading = ref(true); |
| | |
| | | import { generateAutoCode } from '#/api/mes/md/autocode/record'; |
| | | import { getTemplateItemOptionList } from '#/api/mes/qc/template/item'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import { getRangePickerDefaultProps } from '#/utils'; |
| | | import { getDateRangePickerDefaultProps } from '#/utils'; |
| | | import { MdmItemSelect } from '#/views/basicData/mdm/components'; |
| | | |
| | | /** 表åç±»å */ |
| | |
| | | label: 'æ£æµæ¥æ', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | format: 'YYYY-MM-DD', |
| | | placeholder: 'è¯·éæ©æ£æµæ¥æ', |
| | | showTime: true, |
| | | showTime: false, |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | }, |
| | | rules: 'required', |
| | |
| | | label: 'æ£æµæ¥æ', |
| | | component: 'RangePicker', |
| | | componentProps: { |
| | | ...getRangePickerDefaultProps(), |
| | | ...getDateRangePickerDefaultProps(), |
| | | }, |
| | | }, |
| | | ]; |
| | |
| | | field: 'inspectDate', |
| | | title: 'æ£æµæ¥æ', |
| | | width: 160, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'inspectorNickname', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | MesToolStatusEnum, |
| | | } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | import { formatDateTime } from '@vben/utils'; |
| | | import { formatDate } from '@vben/utils'; |
| | | |
| | | import { Button } from 'ant-design-vue'; |
| | | |
| | |
| | | label: '䏿¬¡ä¿å
»æ¥æ', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | showTime: true, |
| | | format: 'YYYY-MM-DD', |
| | | showTime: false, |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | }, |
| | | dependencies: { |
| | |
| | | formatter: ({ row }) => { |
| | | if (row.maintenType === MesMaintenTypeEnum.REGULAR) { |
| | | return row.nextMaintenDate |
| | | ? formatDateTime(row.nextMaintenDate) |
| | | ? formatDate(row.nextMaintenDate) |
| | | : '-'; |
| | | } |
| | | if (row.maintenType === MesMaintenTypeEnum.USAGE) { |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | ]; |
| | | } |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | <script lang="ts" setup> |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesProFeedbackApi } from '#/api/mes/pro/feedback'; |
| | | |
| | |
| | | field: 'feedbackTime', |
| | | title: 'æ¥å·¥æ¶é´', |
| | | width: 170, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | </Popconfirm> |
| | | <!-- 审æ¹ä¸ï¼æ¥çå®¡æ¹ --> |
| | | <Button |
| | | v-if="row.status === STATUS_APPROVING" |
| | | v-if="!automaticApproval && row.status === STATUS_APPROVING" |
| | | type="link" |
| | | size="small" |
| | | @click="handleViewProcess(row)" |
| | |
| | | import { |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | MesProWorkOrderStatusEnum, |
| | | MesWmProductIssueStatusEnum, |
| | | } from '@vben/constants'; |
| | | |
| | |
| | | label: 'ç产订å', |
| | | component: markRaw(ProWorkOrderSelect), |
| | | componentProps: { |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | }, |
| | | }, |
| | | { |
| | |
| | | import { |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | MesProWorkOrderStatusEnum, |
| | | MesWmProductIssueStatusEnum, |
| | | } from '@vben/constants'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | |
| | | label: 'ç产订å', |
| | | component: markRaw(ProWorkOrderSelect), |
| | | componentProps: { |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | disabled: true, |
| | | }, |
| | | }, |
| | |
| | | import { |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | MesProWorkOrderStatusEnum, |
| | | MesWmReturnIssueStatusEnum, |
| | | } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | |
| | | label: 'ç产订å', |
| | | component: markRaw(ProWorkOrderSelect), |
| | | componentProps: { |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | }, |
| | | }, |
| | | { |
| | |
| | | import { |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | MesProWorkOrderStatusEnum, |
| | | MesWmReturnIssueStatusEnum, |
| | | } from '@vben/constants'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | |
| | | label: 'ç产订å', |
| | | component: markRaw(ProWorkOrderSelect), |
| | | componentProps: { |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | disabled: true, |
| | | }, |
| | | }, |
| | |
| | | import { h, onMounted, ref } from 'vue'; |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { formatDateTime } from '@vben/utils'; |
| | | import { formatDate } from '@vben/utils'; |
| | | |
| | | import { Spin, Tag } from 'ant-design-vue'; |
| | | |
| | |
| | | { |
| | | label: 'ç³è¯·æ¶é´', |
| | | field: 'applyTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { label: 'ä¾åºååç§°', field: 'supplierName', render: textRender }, |
| | | { label: 'ä¾åºåç±»å', field: 'supplierType', render: textRender }, |
| | |
| | | <script lang="ts" setup> |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import { submitApply } from '#/api/srm/supplierApply'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { SrmSupplierApplyApi } from '#/api/srm/supplierApply'; |
| | | |
| | |
| | | /** æäº¤å®¡æ¹ - å
è·ååååå
¬å®¡æ¹æµç¨å表 */ |
| | | async function handleSubmit(row: SrmSupplierApplyApi.ApplyVO) { |
| | | const hideLoading = message.loading({ |
| | | content: 'è·åå®¡æ¹æµç¨...', |
| | | content: automaticApproval.value ? 'æ£å¨æäº¤çæ...' : 'è·åå®¡æ¹æµç¨...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | if (automaticApproval.value) { |
| | | await submitApply(row.id!, 'supplier_apply_approve'); |
| | | hideLoading(); |
| | | message.success('å·²æäº¤çæ'); |
| | | handleRefresh(); |
| | | return; |
| | | } |
| | | const processList = await getApproveProcessList(); |
| | | hideLoading(); |
| | | if (!processList || processList.length === 0) { |
| | |
| | | return; |
| | | } |
| | | // æå¼æµç¨éæ©å¼¹çª |
| | | |
| | | processModalApi.setData({ id: row.id, processList }).open(); |
| | | } catch { |
| | | hideLoading(); |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | title: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | import { h } from 'vue'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { formatDateTime } from '@vben/utils'; |
| | | import { formatDate } from '@vben/utils'; |
| | | |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | import { getRangePickerDefaultProps } from '#/utils'; |
| | |
| | | field: 'createTime', |
| | | title: 'ç»å½æ¥æ', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | { |
| | | field: 'createTime', |
| | | label: 'ç»å½æ¥æ', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | ]; |
| | | } |
| | |
| | | import { h } from 'vue'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { formatDateTime } from '@vben/utils'; |
| | | import { formatDate } from '@vben/utils'; |
| | | |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import { DictTag } from '#/components/dict-tag'; |
| | |
| | | field: 'createTime', |
| | | title: 'æä½æ¶é´', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'bizId', |
| | |
| | | { |
| | | field: 'createTime', |
| | | label: 'æä½æ¶é´', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | field: 'bizId', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button, message, Upload } from 'ant-design-vue'; |
| | | import { Button, Upload } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { importPost, importPostTemplate } from '#/api/system/post'; |
| | | import { $t } from '#/locales'; |
| | | import { notifyImportResult } from '#/utils'; |
| | | |
| | | import { useImportFormSchema } from '../data'; |
| | | |
| | |
| | | // æäº¤è¡¨å |
| | | const data = await formApi.getValues(); |
| | | try { |
| | | await importPost(data.file, data.updateSupport); |
| | | // å
³éå¹¶æç¤º |
| | | const importData = await importPost(data.file, data.updateSupport); |
| | | const createCount = importData.createPostNames?.length || 0; |
| | | const updateCount = importData.updatePostNames?.length || 0; |
| | | |
| | | // å
³éå¹¶æå¯¼å
¥ç»ææç¤ºï¼ä»
å¨ç¡®ææ°æ®åå
¥æ¶éç¥åè¡¨å·æ° |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | if (createCount + updateCount > 0) { |
| | | emit('success'); |
| | | } |
| | | notifyImportResult({ |
| | | subject: 'å²ä½', |
| | | createCount, |
| | | updateCount, |
| | | failures: importData.failurePostNames, |
| | | }); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button, message, Upload } from 'ant-design-vue'; |
| | | import { Button, Upload } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { importUser, importUserTemplate } from '#/api/system/user'; |
| | | import { $t } from '#/locales'; |
| | | import { notifyImportResult } from '#/utils'; |
| | | |
| | | import { useImportFormSchema } from '../data'; |
| | | |
| | |
| | | // æäº¤è¡¨å |
| | | const data = await formApi.getValues(); |
| | | try { |
| | | await importUser(data.file, data.updateSupport); |
| | | // å
³éå¹¶æç¤º |
| | | const importData = await importUser(data.file, data.updateSupport); |
| | | const createCount = importData.createUsernames?.length || 0; |
| | | const updateCount = importData.updateUsernames?.length || 0; |
| | | |
| | | // å
³éå¹¶æå¯¼å
¥ç»ææç¤ºï¼ä»
å¨ç¡®ææ°æ®åå
¥æ¶éç¥åè¡¨å·æ° |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | if (createCount + updateCount > 0) { |
| | | emit('success'); |
| | | } |
| | | notifyImportResult({ |
| | | subject: 'ç¨æ·', |
| | | createCount, |
| | | updateCount, |
| | | failures: importData.failureUsernames, |
| | | }); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesWmBarcodeApi } from '#/api/mes/wm/barcode'; |
| | | |
| | | import { nextTick, ref } from 'vue'; |
| | | |
| | | import { Button, message } from 'ant-design-vue'; |
| | | |
| | | import { createBarcode, getBarcodeByBusiness } from '#/api/mes/wm/barcode'; |
| | | |
| | | import WlsBarcode from './barcode.vue'; |
| | | |
| | | defineOptions({ name: 'WlsPrinterLabel' }); |
| | | |
| | | withDefaults( |
| | | /** æ ç¾ä¸éå å±ç¤ºçä¸å¡å段ï¼å¯éï¼é»è®¤ä¸å±ç¤ºï¼ */ |
| | | interface LabelField { |
| | | label: string; |
| | | value?: number | string; |
| | | } |
| | | |
| | | const props = withDefaults( |
| | | defineProps<{ |
| | | bizCode?: string; |
| | | bizId?: number; |
| | | bizType?: string; |
| | | /** ä¸å¡åç§°ï¼ä½ä¸ºæ ç¾ä¸»ä¿¡æ¯å±ç¤ºå¨æ¡ç 䏿¹ */ |
| | | bizName?: string; |
| | | bizType?: number; |
| | | /** æ ç¾æ é¢ */ |
| | | title?: string; |
| | | /** æé®ææ¡ */ |
| | | labelText?: string; |
| | | /** éå åæ®µï¼ä¸è¬ä¸éè¦ä¼ */ |
| | | fields?: LabelField[]; |
| | | }>(), |
| | | { |
| | | bizCode: undefined, |
| | | bizId: undefined, |
| | | bizName: undefined, |
| | | bizType: undefined, |
| | | title: '', |
| | | labelText: 'æ ç¾æå°', |
| | | fields: () => [], |
| | | }, |
| | | ); |
| | | |
| | | function handlePrint() { |
| | | message.warning('æ ç¾æå°åè½ææªå®ç°ï¼æ¬è¯·æå¾
'); |
| | | const barcodeRef = ref<InstanceType<typeof WlsBarcode>>(); |
| | | const barcode = ref<MesWmBarcodeApi.Barcode>(); |
| | | const renderKey = ref(0); |
| | | const printing = ref(false); |
| | | /** æå°çªå£ï¼éå¨ç¨æ·ç¹å»æ¶åæ¥æå¼ï¼å¦åä¼è¢«æµè§å¨æ¦æª */ |
| | | const printWindow = ref<null | Window>(null); |
| | | |
| | | function escapeHtml(value: string) { |
| | | return value |
| | | .replaceAll('&', '&') |
| | | .replaceAll('<', '<') |
| | | .replaceAll('>', '>'); |
| | | } |
| | | |
| | | /** æå»ºæ ç¾ HTMLï¼æ é¢ + æ¡ç å¾ + åç§°ï¼å±
ä¸ï¼ï¼éå åæ®µå¯é */ |
| | | function buildLabelHtml(base64: string) { |
| | | const nameHtml = props.bizName |
| | | ? `<div class="label-name">${escapeHtml(props.bizName)}</div>` |
| | | : ''; |
| | | const infoHtml = props.fields |
| | | .filter((item) => item.value !== undefined && item.value !== '') |
| | | .map( |
| | | (item) => |
| | | `<p><span class="info-label">${escapeHtml(item.label)}</span>${escapeHtml(String(item.value))}</p>`, |
| | | ) |
| | | .join(''); |
| | | return `<!DOCTYPE html> |
| | | <html><head><meta charset="UTF-8"><title>${escapeHtml(props.title || 'æå°æ ç¾')}</title> |
| | | <style> |
| | | *{margin:0;padding:0;box-sizing:border-box} |
| | | body{font-family:Arial,"Microsoft YaHei",sans-serif;display:flex;justify-content:center;padding:12px} |
| | | .label{width:320px;padding:8px;text-align:center} |
| | | .label-title{font-size:13px;color:#333;margin-bottom:6px} |
| | | .label-barcode{width:200px;margin:0 auto} |
| | | .label-barcode img{display:block;width:100%} |
| | | .label-name{margin-top:8px;font-size:16px;font-weight:700;line-height:1.4;word-break:break-all} |
| | | .label-info{margin-top:8px;font-size:12px;line-height:1.6;text-align:left;color:#333} |
| | | .label-info p{margin:2px 0} |
| | | .info-label{color:#666;margin-right:4px} |
| | | .info-label::after{content:"ï¼"} |
| | | @page{margin:5mm} |
| | | @media print{body{padding:0}.label{width:auto;max-width:100%}} |
| | | </style> |
| | | </head><body><div class="label"> |
| | | ${props.title ? `<div class="label-title">${escapeHtml(props.title)}</div>` : ''} |
| | | <div class="label-barcode"><img src="${base64}" alt="æ¡ç " /></div> |
| | | ${nameHtml} |
| | | ${infoHtml ? `<div class="label-info">${infoHtml}</div>` : ''} |
| | | </div></body></html>`; |
| | | } |
| | | |
| | | /** æ¡ç 徿¸²æå®æï¼ææ ç¾å
容åå
¥å·²æå¼çæå°çªå£å¹¶è°èµ·æå° */ |
| | | function handleBarcodeDone() { |
| | | const win = printWindow.value; |
| | | const base64 = barcodeRef.value?.getImageBase64(); |
| | | printWindow.value = null; |
| | | if (!win || win.closed) { |
| | | return; |
| | | } |
| | | if (!base64) { |
| | | message.warning('æ¡ç 渲æå¤±è´¥ï¼æ æ³æå°æ ç¾'); |
| | | win.close(); |
| | | return; |
| | | } |
| | | win.document.open(); |
| | | win.document.write(buildLabelHtml(base64)); |
| | | win.document.close(); |
| | | setTimeout(() => win.print(), 500); |
| | | } |
| | | |
| | | /** æå°æ ç¾ï¼æ æ¡ç æ¶å
çæï¼åæ¸²ææ¡ç å¾åæå° */ |
| | | async function handlePrint() { |
| | | if (!props.bizType || !props.bizId) { |
| | | message.warning('缺å°ä¸å¡ç±»åæä¸å¡ç¼å·ï¼æ æ³æå°æ ç¾'); |
| | | return; |
| | | } |
| | | if (printing.value) { |
| | | return; |
| | | } |
| | | // å¨ç¨æ·æå¿å
忥æå¼çªå£ï¼é¿å
å¼¹çªè¢«æ¦æª |
| | | const win = window.open('', '_blank'); |
| | | if (!win) { |
| | | message.error('æ æ³æå¼æå°çªå£ï¼è¯·æ£æ¥æµè§å¨è®¾ç½®'); |
| | | return; |
| | | } |
| | | win.document.write( |
| | | '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body style="font-family:Arial;padding:20px">æ£å¨çææ ç¾ï¼è¯·ç¨åâ¦</body></html>', |
| | | ); |
| | | printWindow.value = win; |
| | | printing.value = true; |
| | | try { |
| | | let data = await getBarcodeByBusiness(props.bizType, props.bizId); |
| | | if (!data?.content) { |
| | | await createBarcode({ |
| | | bizCode: props.bizCode || '', |
| | | bizId: props.bizId, |
| | | bizName: props.bizName || '', |
| | | bizType: props.bizType, |
| | | }); |
| | | data = await getBarcodeByBusiness(props.bizType, props.bizId); |
| | | } |
| | | if (!data?.content) { |
| | | message.warning('æ¡ç çæå¤±è´¥ï¼æ æ³æå°æ ç¾'); |
| | | win.close(); |
| | | printWindow.value = null; |
| | | return; |
| | | } |
| | | barcode.value = data; |
| | | renderKey.value += 1; |
| | | await nextTick(); |
| | | } catch { |
| | | message.error('æå°æ ç¾å¤±è´¥ï¼è¯·éè¯'); |
| | | win.close(); |
| | | printWindow.value = null; |
| | | } finally { |
| | | printing.value = false; |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <Button type="link" @click="handlePrint"> |
| | | <Button :loading="printing" @click="handlePrint"> |
| | | {{ labelText }} |
| | | </Button> |
| | | <!-- éèçæ¡ç æ¸²æå®¹å¨ï¼æ¸²æå®æåååºå¾çåå
¥æå°çªå£ --> |
| | | <div |
| | | v-if="barcode?.content" |
| | | class="pointer-events-none fixed top-0 -left-[9999px] h-0 w-0 overflow-hidden" |
| | | > |
| | | <WlsBarcode |
| | | :key="renderKey" |
| | | ref="barcodeRef" |
| | | :content="barcode.content" |
| | | :format="barcode.format" |
| | | :height="150" |
| | | :width="280" |
| | | @done="handleBarcodeDone" |
| | | /> |
| | | </div> |
| | | </template> |
| | |
| | | BarcodeBizTypeEnum, |
| | | CommonStatusEnum, |
| | | DICT_TYPE, |
| | | MesProWorkOrderStatusEnum, |
| | | } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | import { formatDateTime } from '@vben/utils'; |
| | | import { formatDate } from '@vben/utils'; |
| | | |
| | | import { z } from '#/adapter/form'; |
| | | import { generateBarcodeContent } from '#/api/mes/wm/barcode'; |
| | |
| | | label: '订å', |
| | | component: markRaw(ProWorkOrderSelect), |
| | | componentProps: { |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | onChange: (item: any) => syncBizDetail(formApi, item), |
| | | }, |
| | | dependencies: { |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | { |
| | | field: 'createTime', |
| | | label: 'å建æ¶é´', |
| | | render: (value) => formatDateTime(value) || '-', |
| | | render: (value) => formatDate(value) || '-', |
| | | }, |
| | | ]; |
| | | } |
| | |
| | | width: 120, |
| | | }, |
| | | { |
| | | field: 'clientCode', |
| | | title: '客æ·ç¼ç ', |
| | | width: 110, |
| | | }, |
| | | { |
| | | field: 'clientName', |
| | | title: '客æ·åç§°', |
| | | width: 110, |
| | |
| | | import { computed, reactive } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { BarcodeBizTypeEnum } from '@vben/constants'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | |
| | | v-if="isStock" |
| | | :biz-code="row.batchCode" |
| | | :biz-id="row.batchId" |
| | | biz-type="ITEM_BATCH" |
| | | :biz-type="BarcodeBizTypeEnum.BATCH" |
| | | /> |
| | | </div> |
| | | </template> |
| | |
| | | field: 'receiptTime', |
| | | title: 'å
¥åºæ¥æ', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'frozen', |
| | |
| | | field: 'receiptTime', |
| | | title: 'å
¥åºæ¥æ', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'frozen', |
| | |
| | | { |
| | | label: 'ç§»åº', |
| | | type: 'link', |
| | | auth: ['mes:wm-material-stock:move'], |
| | | auth: ['mes:wm-material-stock:update'], |
| | | ifShow: !row.frozen, |
| | | onClick: handleOpenStockMove.bind(null, row), |
| | | }, |
| | |
| | | label: $t('ui.actionTitle.create', ['æé¡¹å
¥åºå']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['mes:wm-misc-receipt:create'], |
| | | auth: ['mes:wm:misc-receipt:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.export'), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.DOWNLOAD, |
| | | auth: ['mes:wm-misc-receipt:export'], |
| | | auth: ['mes:wm:misc-receipt:export'], |
| | | onClick: handleExport, |
| | | }, |
| | | ]" |
| | |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['mes:wm-misc-receipt:update'], |
| | | auth: ['mes:wm:misc-receipt:update'], |
| | | ifShow: row.status === MesWmMiscReceiptStatusEnum.PREPARE, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['mes:wm-misc-receipt:delete'], |
| | | auth: ['mes:wm:misc-receipt:delete'], |
| | | ifShow: row.status === MesWmMiscReceiptStatusEnum.PREPARE, |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.code]), |
| | |
| | | { |
| | | label: 'æ§è¡å
¥åº', |
| | | type: 'link', |
| | | auth: ['mes:wm-misc-receipt:finish'], |
| | | auth: ['mes:wm:misc-receipt:finish'], |
| | | ifShow: row.status === MesWmMiscReceiptStatusEnum.APPROVED, |
| | | onClick: handleFinish.bind(null, row), |
| | | }, |
| | |
| | | label: 'åæ¶', |
| | | type: 'link', |
| | | danger: true, |
| | | auth: ['mes:wm-misc-receipt:cancel'], |
| | | auth: ['mes:wm:misc-receipt:cancel'], |
| | | ifShow: row.status === MesWmMiscReceiptStatusEnum.APPROVED, |
| | | popConfirm: { |
| | | title: 'ç¡®è®¤åæ¶è¯¥æé¡¹å
¥åºååï¼', |
| | |
| | | import { |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | MesProWorkOrderStatusEnum, |
| | | MesProWorkOrderTypeEnum, |
| | | } from '@vben/constants'; |
| | | |
| | |
| | | onChange: async (workOrder?: MesProWorkOrderApi.WorkOrder) => { |
| | | await formApi?.setFieldValue('vendorId', workOrder?.vendorId); |
| | | }, |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | type: MesProWorkOrderTypeEnum.OUTSOURCE, |
| | | }, |
| | | rules: 'selectRequired', |
| | |
| | | import { |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | MesProWorkOrderStatusEnum, |
| | | MesProWorkOrderTypeEnum, |
| | | } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | |
| | | onChange: async (workOrder?: MesProWorkOrderApi.WorkOrder) => { |
| | | await formApi?.setFieldValue('vendorId', workOrder?.vendorId); |
| | | }, |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | type: MesProWorkOrderTypeEnum.OUTSOURCE, |
| | | }, |
| | | rules: 'selectRequired', |
| | |
| | | import { computed, reactive } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { BarcodeBizTypeEnum } from '@vben/constants'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | |
| | | v-if="isStock" |
| | | :biz-code="row.batchCode" |
| | | :biz-id="row.batchId" |
| | | biz-type="BATCH" |
| | | :biz-type="BarcodeBizTypeEnum.BATCH" |
| | | /> |
| | | </div> |
| | | </template> |
| | |
| | | import { |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | MesProWorkOrderStatusEnum, |
| | | } from '@vben/constants'; |
| | | |
| | | import { Button } from 'ant-design-vue'; |
| | |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'clientCode', |
| | | title: '客æ·ç¼ç ', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'clientName', |
| | | title: '客æ·åç§°', |
| | | minWidth: 120, |
| | |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'clientCode', |
| | | title: '客æ·ç¼ç ', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'clientName', |
| | | title: '客æ·åç§°', |
| | | minWidth: 120, |
| | |
| | | component: markRaw(ProWorkOrderSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©ç产订å', |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | |
| | | field: 'invoiceCode', |
| | | title: 'å票ç¼å·', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'clientCode', |
| | | title: '客æ·ç¼ç ', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'clientName', |
| | |
| | | import { |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | MesProWorkOrderStatusEnum, |
| | | } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | format: 'YYYY-MM-DD', |
| | | placeholder: 'è¯·éæ©éæ±æ¶é´', |
| | | showTime: true, |
| | | showTime: false, |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | }, |
| | | rules: 'required', |
| | |
| | | component: markRaw(ProWorkOrderSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©ç产订å', |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | // 订ååæ´ï¼æ¸
空已éç产任å¡ï¼é¿å
ä»»å¡ä¸è®¢åä¸ä¸è´ |
| | | onChange: async () => { |
| | | await formApi?.setFieldValue('taskId', undefined); |
| | |
| | | field: 'requiredTime', |
| | | title: 'éæ±æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'status', |
| | |
| | | import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder'; |
| | | import type { MesWmMaterialStockApi } from '#/api/mes/wm/materialstock'; |
| | | import type { MesWmProductReceiptApi } from '#/api/mes/wm/productreceipt'; |
| | | import type { MesWmProductReceiptDetailApi } from '#/api/mes/wm/productreceipt/detail'; |
| | |
| | | label: 'ç产订å', |
| | | component: markRaw(ProWorkOrderSelect), |
| | | componentProps: { |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | checkMethod: (row: any) => row.productionStatus !== 0, |
| | | checkMethod: (row: MesProWorkOrderApi.WorkOrder) => |
| | | row.status === MesProWorkOrderStatusEnum.CONFIRMED || |
| | | row.status === MesProWorkOrderStatusEnum.FINISHED, |
| | | }, |
| | | }, |
| | | { |
| | |
| | | v-if="isStock" |
| | | :biz-code="row.batchCode" |
| | | :biz-id="row.batchId" |
| | | biz-type="BATCH" |
| | | :biz-type="BarcodeBizTypeEnum.BATCH" |
| | | /> |
| | | </div> |
| | | </template> |
| | |
| | | label: '客æ·', |
| | | component: markRaw(CrmCustomerSelect), |
| | | componentProps: { |
| | | allCustomers: true, |
| | | disabled: isHeaderReadonly(formType), |
| | | placeholder: 'è¯·éæ©å®¢æ·', |
| | | }, |
| | |
| | | label: '客æ·', |
| | | component: markRaw(CrmCustomerSelect), |
| | | componentProps: { |
| | | allCustomers: true, |
| | | placeholder: 'è¯·éæ©å®¢æ·', |
| | | }, |
| | | }, |
| | |
| | | field: 'salesOrderCode', |
| | | title: 'éå®è®¢åç¼å·', |
| | | minWidth: 160, |
| | | }, |
| | | { |
| | | field: 'clientCode', |
| | | title: '客æ·ç¼ç ', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'clientName', |
| | |
| | | import { |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | MesProWorkOrderStatusEnum, |
| | | } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | |
| | | label: 'ç产订å', |
| | | component: markRaw(ProWorkOrderSelect), |
| | | componentProps: { |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | format: 'YYYY-MM-DD', |
| | | placeholder: 'è¯·éæ©éææ¥æ', |
| | | showTime: true, |
| | | showTime: false, |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | }, |
| | | }, |
| | |
| | | field: 'returnDate', |
| | | title: 'éææ¥æ', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'status', |
| | |
| | | v-if="isStock" |
| | | :biz-code="row.batchCode" |
| | | :biz-id="row.batchId" |
| | | biz-type="BATCH" |
| | | :biz-type="BarcodeBizTypeEnum.BATCH" |
| | | /> |
| | | </div> |
| | | </template> |
| | |
| | | label: '客æ·', |
| | | component: markRaw(CrmCustomerSelect), |
| | | componentProps: { |
| | | allCustomers: true, |
| | | placeholder: 'è¯·éæ©å®¢æ·', |
| | | }, |
| | | rules: 'selectRequired', |
| | |
| | | label: '客æ·', |
| | | component: markRaw(CrmCustomerSelect), |
| | | componentProps: { |
| | | allCustomers: true, |
| | | placeholder: 'è¯·éæ©å®¢æ·', |
| | | }, |
| | | }, |
| | |
| | | field: 'salesOrderCode', |
| | | title: 'éå®è®¢åå·', |
| | | minWidth: 140, |
| | | }, |
| | | { |
| | | field: 'clientCode', |
| | | title: '客æ·ç¼ç ', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'clientName', |
| | |
| | | import { computed, reactive } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { BarcodeBizTypeEnum } from '@vben/constants'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | |
| | | v-if="isStock" |
| | | :biz-code="row.batchCode" |
| | | :biz-id="row.batchId" |
| | | biz-type="BATCH" |
| | | :biz-type="BarcodeBizTypeEnum.BATCH" |
| | | /> |
| | | </div> |
| | | </template> |
| | |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | format: 'YYYY-MM-DD', |
| | | placeholder: 'è¯·éæ©éè´§æ¥æ', |
| | | showTime: true, |
| | | showTime: false, |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | }, |
| | | rules: 'required', |
| | |
| | | field: 'returnDate', |
| | | title: 'éè´§æ¥æ', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'status', |
| | |
| | | { |
| | | label: '宿éè´§', |
| | | type: 'link', |
| | | auth: ['mes:wm-return-vendor:update-status'], |
| | | auth: ['mes:wm-return-vendor:finish'], |
| | | ifShow: row.status === MesWmReturnVendorStatusEnum.APPROVED, |
| | | onClick: handleFinish.bind(null, row), |
| | | }, |
| | |
| | | import { computed, reactive } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { BarcodeBizTypeEnum } from '@vben/constants'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | |
| | | v-if="isStock" |
| | | :biz-code="row.batchCode" |
| | | :biz-id="row.batchId" |
| | | biz-type="BATCH" |
| | | :biz-type="BarcodeBizTypeEnum.BATCH" |
| | | /> |
| | | </div> |
| | | </template> |
| | |
| | | field: 'createTime', |
| | | title: 'çææ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'createTime', |
| | | title: 'çææ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'transactionTime', |
| | | title: 'äºå¡æ¶é´', |
| | | width: 160, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | ]; |
| | | } |
| | |
| | | import { computed, reactive } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { BarcodeBizTypeEnum } from '@vben/constants'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | |
| | | v-if="isStock" |
| | | :biz-code="row.batchCode" |
| | | :biz-id="row.batchId" |
| | | biz-type="BATCH" |
| | | :biz-type="BarcodeBizTypeEnum.BATCH" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | import { formatDate, formatDateTime } from '@vben/utils'; |
| | | import { formatDate } from '@vben/utils'; |
| | | |
| | | import { z } from '#/adapter/form'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | |
| | | { |
| | | field: 'createTime', |
| | | label: 'å建æ¶é´', |
| | | render: (val) => formatDateTime(val) || '-', |
| | | render: (val) => formatDate(val) || '-', |
| | | }, |
| | | { |
| | | field: 'creatorName', |
| | |
| | | { |
| | | field: 'updateTime', |
| | | label: 'æ´æ°æ¶é´', |
| | | render: (val) => formatDateTime(val) || '-', |
| | | render: (val) => formatDate(val) || '-', |
| | | }, |
| | | { |
| | | field: 'updaterName', |
| | |
| | | OrderStatusEnum, |
| | | OrderUpdateStatusList, |
| | | } from '@vben/constants'; |
| | | import { downloadFileFromBlobPart, formatDateTime } from '@vben/utils'; |
| | | import { downloadFileFromBlobPart, formatDate } from '@vben/utils'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | |
| | | </template> |
| | | <template #operateInfo="{ row }"> |
| | | <div> |
| | | å建ï¼{{ formatDateTime(row.createTime) || '-' }} / |
| | | å建ï¼{{ formatDate(row.createTime) || '-' }} / |
| | | {{ row.creatorName || row.creator || '-' }} |
| | | </div> |
| | | <div> |
| | | æ´æ°ï¼{{ formatDateTime(row.updateTime) || '-' }} / |
| | | æ´æ°ï¼{{ formatDate(row.updateTime) || '-' }} / |
| | | {{ row.updaterName || row.updater || '-' }} |
| | | </div> |
| | | </template> |
| | |
| | | import { Barcode, BarcodeFormatEnum } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictLabel } from '@vben/hooks'; |
| | | import { formatDate, formatDateTime } from '@vben/utils'; |
| | | import { formatDate } from '@vben/utils'; |
| | | |
| | | import { |
| | | getCheckOrder, |
| | |
| | | </div> |
| | | <div class="col-span-3 grid grid-cols-2 gap-x-6"> |
| | | <div> |
| | | å建ï¼{{ formatDateTime(printData.createTime) || '-' }} / |
| | | å建ï¼{{ formatDate(printData.createTime) || '-' }} / |
| | | {{ printData.creatorName || printData.creator || '-' }} |
| | | </div> |
| | | <div> |
| | | æ´æ°ï¼{{ formatDateTime(printData.updateTime) || '-' }} / |
| | | æ´æ°ï¼{{ formatDate(printData.updateTime) || '-' }} / |
| | | {{ printData.updaterName || printData.updater || '-' }} |
| | | </div> |
| | | </div> |
| | |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | import { formatDate, formatDateTime } from '@vben/utils'; |
| | | import { formatDate } from '@vben/utils'; |
| | | |
| | | import { z } from '#/adapter/form'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | |
| | | { |
| | | field: 'createTime', |
| | | label: 'å建æ¶é´', |
| | | render: (val) => formatDateTime(val) || '-', |
| | | render: (val) => formatDate(val) || '-', |
| | | }, |
| | | { |
| | | field: 'creatorName', |
| | |
| | | { |
| | | field: 'updateTime', |
| | | label: 'æ´æ°æ¶é´', |
| | | render: (val) => formatDateTime(val) || '-', |
| | | render: (val) => formatDate(val) || '-', |
| | | }, |
| | | { |
| | | field: 'updaterName', |
| | |
| | | OrderStatusEnum, |
| | | OrderUpdateStatusList, |
| | | } from '@vben/constants'; |
| | | import { downloadFileFromBlobPart, formatDateTime } from '@vben/utils'; |
| | | import { downloadFileFromBlobPart, formatDate } from '@vben/utils'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | |
| | | </template> |
| | | <template #operateInfo="{ row }"> |
| | | <div> |
| | | å建ï¼{{ formatDateTime(row.createTime) || '-' }} / |
| | | å建ï¼{{ formatDate(row.createTime) || '-' }} / |
| | | {{ row.creatorName || row.creator || '-' }} |
| | | </div> |
| | | <div> |
| | | æ´æ°ï¼{{ formatDateTime(row.updateTime) || '-' }} / |
| | | æ´æ°ï¼{{ formatDate(row.updateTime) || '-' }} / |
| | | {{ row.updaterName || row.updater || '-' }} |
| | | </div> |
| | | </template> |
| | |
| | | import { Barcode, BarcodeFormatEnum } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictLabel } from '@vben/hooks'; |
| | | import { formatDate, formatDateTime } from '@vben/utils'; |
| | | import { formatDate } from '@vben/utils'; |
| | | |
| | | import { |
| | | getMovementOrder, |
| | |
| | | <div>æ»éé¢ï¼{{ formatPrice(printData.totalPrice) || '-' }}</div> |
| | | <div class="col-span-3 grid grid-cols-2 gap-x-6"> |
| | | <div> |
| | | å建ï¼{{ formatDateTime(printData.createTime) || '-' }} / |
| | | å建ï¼{{ formatDate(printData.createTime) || '-' }} / |
| | | {{ printData.creatorName || printData.creator || '-' }} |
| | | </div> |
| | | <div> |
| | | æ´æ°ï¼{{ formatDateTime(printData.updateTime) || '-' }} / |
| | | æ´æ°ï¼{{ formatDate(printData.updateTime) || '-' }} / |
| | | {{ printData.updaterName || printData.updater || '-' }} |
| | | </div> |
| | | </div> |
| | |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | import { formatDate, formatDateTime } from '@vben/utils'; |
| | | import { formatDate } from '@vben/utils'; |
| | | |
| | | import { z } from '#/adapter/form'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | |
| | | { |
| | | field: 'createTime', |
| | | label: 'å建æ¶é´', |
| | | render: (val) => formatDateTime(val) || '-', |
| | | render: (val) => formatDate(val) || '-', |
| | | }, |
| | | { |
| | | field: 'creatorName', |
| | |
| | | { |
| | | field: 'updateTime', |
| | | label: 'æ´æ°æ¶é´', |
| | | render: (val) => formatDateTime(val) || '-', |
| | | render: (val) => formatDate(val) || '-', |
| | | }, |
| | | { |
| | | field: 'updaterName', |
| | |
| | | OrderStatusEnum, |
| | | OrderUpdateStatusList, |
| | | } from '@vben/constants'; |
| | | import { downloadFileFromBlobPart, formatDateTime } from '@vben/utils'; |
| | | import { downloadFileFromBlobPart, formatDate } from '@vben/utils'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | |
| | | </template> |
| | | <template #operateInfo="{ row }"> |
| | | <div> |
| | | å建ï¼{{ formatDateTime(row.createTime) || '-' }} / |
| | | å建ï¼{{ formatDate(row.createTime) || '-' }} / |
| | | {{ row.creatorName || row.creator || '-' }} |
| | | </div> |
| | | <div> |
| | | æ´æ°ï¼{{ formatDateTime(row.updateTime) || '-' }} / |
| | | æ´æ°ï¼{{ formatDate(row.updateTime) || '-' }} / |
| | | {{ row.updaterName || row.updater || '-' }} |
| | | </div> |
| | | </template> |
| | |
| | | import { Barcode, BarcodeFormatEnum } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictLabel } from '@vben/hooks'; |
| | | import { formatDate, formatDateTime } from '@vben/utils'; |
| | | import { formatDate } from '@vben/utils'; |
| | | |
| | | import { |
| | | getReceiptOrder, |
| | |
| | | <div>æ»éé¢ï¼{{ formatPrice(printData.totalPrice) || '-' }}</div> |
| | | <div class="col-span-3 grid grid-cols-2 gap-x-6"> |
| | | <div> |
| | | å建ï¼{{ formatDateTime(printData.createTime) || '-' }} / |
| | | å建ï¼{{ formatDate(printData.createTime) || '-' }} / |
| | | {{ printData.creatorName || printData.creator || '-' }} |
| | | </div> |
| | | <div> |
| | | æ´æ°ï¼{{ formatDateTime(printData.updateTime) || '-' }} / |
| | | æ´æ°ï¼{{ formatDate(printData.updateTime) || '-' }} / |
| | | {{ printData.updaterName || printData.updater || '-' }} |
| | | </div> |
| | | </div> |
| | |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | import { formatDate, formatDateTime } from '@vben/utils'; |
| | | import { formatDate } from '@vben/utils'; |
| | | |
| | | import { z } from '#/adapter/form'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | |
| | | { |
| | | field: 'createTime', |
| | | label: 'å建æ¶é´', |
| | | render: (val) => formatDateTime(val) || '-', |
| | | render: (val) => formatDate(val) || '-', |
| | | }, |
| | | { |
| | | field: 'creatorName', |
| | |
| | | { |
| | | field: 'updateTime', |
| | | label: 'æ´æ°æ¶é´', |
| | | render: (val) => formatDateTime(val) || '-', |
| | | render: (val) => formatDate(val) || '-', |
| | | }, |
| | | { |
| | | field: 'updaterName', |
| | |
| | | OrderStatusEnum, |
| | | OrderUpdateStatusList, |
| | | } from '@vben/constants'; |
| | | import { downloadFileFromBlobPart, formatDateTime } from '@vben/utils'; |
| | | import { downloadFileFromBlobPart, formatDate } from '@vben/utils'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | |
| | | </template> |
| | | <template #operateInfo="{ row }"> |
| | | <div> |
| | | å建ï¼{{ formatDateTime(row.createTime) || '-' }} / |
| | | å建ï¼{{ formatDate(row.createTime) || '-' }} / |
| | | {{ row.creatorName || row.creator || '-' }} |
| | | </div> |
| | | <div> |
| | | æ´æ°ï¼{{ formatDateTime(row.updateTime) || '-' }} / |
| | | æ´æ°ï¼{{ formatDate(row.updateTime) || '-' }} / |
| | | {{ row.updaterName || row.updater || '-' }} |
| | | </div> |
| | | </template> |
| | |
| | | import { Barcode, BarcodeFormatEnum } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictLabel } from '@vben/hooks'; |
| | | import { formatDate, formatDateTime } from '@vben/utils'; |
| | | import { formatDate } from '@vben/utils'; |
| | | |
| | | import { |
| | | getShipmentOrder, |
| | |
| | | <div>æ»éé¢ï¼{{ formatPrice(printData.totalPrice) || '-' }}</div> |
| | | <div class="col-span-3 grid grid-cols-2 gap-x-6"> |
| | | <div> |
| | | å建ï¼{{ formatDateTime(printData.createTime) || '-' }} / |
| | | å建ï¼{{ formatDate(printData.createTime) || '-' }} / |
| | | {{ printData.creatorName || printData.creator || '-' }} |
| | | </div> |
| | | <div> |
| | | æ´æ°ï¼{{ formatDateTime(printData.updateTime) || '-' }} / |
| | | æ´æ°ï¼{{ formatDate(printData.updateTime) || '-' }} / |
| | | {{ printData.updaterName || printData.updater || '-' }} |
| | | </div> |
| | | </div> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | @echo off |
| | | setlocal EnableExtensions EnableDelayedExpansion |
| | | |
| | | REM ============================================================ |
| | | REM Vue Frontend Deploy Script |
| | | REM ============================================================ |
| | | |
| | | REM ========================= |
| | | REM Configuration |
| | | REM ========================= |
| | | |
| | | set "COMPANY=TRT" |
| | | |
| | | set "REMOTE_USER=root" |
| | | set "REMOTE_HOST=36.213.150.237" |
| | | set "REMOTE_PORT=65022" |
| | | |
| | | set "PASSWORD=Sh@1008600" |
| | | |
| | | set "ZIP_FILE=dist.zip" |
| | | set "REMOTE_DIR=/data/vue/mom/trt/dist" |
| | | |
| | | set "SSH_OPTS=-o StrictHostKeyChecking=no -o ConnectTimeout=10 -o ServerAliveInterval=15 -o ServerAliveCountMax=3" |
| | | |
| | | set "RETRY_COUNT=3" |
| | | |
| | | set "SSHPASS_DIR=%~dp0bin" |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Start |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo ============================================================ |
| | | echo Vue Dist Auto Deploy |
| | | echo ============================================================ |
| | | echo. |
| | | echo Company : %COMPANY% |
| | | echo Server : %REMOTE_USER%@%REMOTE_HOST% |
| | | echo Port : %REMOTE_PORT% |
| | | echo Remote Dir : %REMOTE_DIR% |
| | | echo Package : %ZIP_FILE% |
| | | echo. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check sshpass |
| | | REM ============================================================ |
| | | |
| | | echo [1/7] Checking sshpass... |
| | | |
| | | if exist "%SSHPASS_DIR%\sshpass.exe" ( |
| | | set "PATH=%SSHPASS_DIR%;%PATH%" |
| | | ) |
| | | |
| | | where sshpass >nul 2>&1 |
| | | |
| | | if not errorlevel 1 ( |
| | | echo [OK] sshpass found. |
| | | goto CHECK_PNPM |
| | | ) |
| | | |
| | | echo [WARN] sshpass not found. |
| | | |
| | | if not exist "%SSHPASS_DIR%" ( |
| | | mkdir "%SSHPASS_DIR%" >nul 2>&1 |
| | | ) |
| | | |
| | | if exist "%SSHPASS_DIR%\sshpass.exe" ( |
| | | set "PATH=%SSHPASS_DIR%;%PATH%" |
| | | echo [OK] sshpass found. |
| | | goto CHECK_PNPM |
| | | ) |
| | | |
| | | echo [INFO] Downloading sshpass... |
| | | |
| | | powershell -NoProfile -ExecutionPolicy Bypass -Command "$url='https://github.com/xhcoding/sshpass-win32/releases/download/v1.0.3/sshpass.exe'; $out='%SSHPASS_DIR%\sshpass.exe'; Invoke-WebRequest -Uri $url -OutFile $out" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Failed to download sshpass. |
| | | echo [ERROR] Please install sshpass manually. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | set "PATH=%SSHPASS_DIR%;%PATH%" |
| | | |
| | | echo [OK] sshpass installed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check pnpm |
| | | REM ============================================================ |
| | | |
| | | :CHECK_PNPM |
| | | |
| | | echo. |
| | | echo [2/7] Checking pnpm... |
| | | |
| | | where pnpm >nul 2>&1 |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] pnpm not found. |
| | | echo [HINT] npm install -g pnpm |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | call pnpm --version |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] pnpm execution failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check node |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [3/7] Checking Node.js... |
| | | |
| | | where node >nul 2>&1 |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Node.js not found. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | call node --version |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Node.js execution failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check node_modules |
| | | REM ============================================================ |
| | | |
| | | if not exist "node_modules" ( |
| | | echo. |
| | | echo [INFO] node_modules not found. |
| | | echo [INFO] Running pnpm install... |
| | | |
| | | call pnpm install |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] pnpm install failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] pnpm install completed. |
| | | ) else ( |
| | | echo [OK] node_modules exists. |
| | | ) |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Build |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [4/7] Building project... |
| | | |
| | | if exist "%ZIP_FILE%" ( |
| | | echo. |
| | | echo [WARN] %ZIP_FILE% already exists. |
| | | set "REBUILD=" |
| | | set /p "REBUILD=Rebuild? Y/N (default Y): " |
| | | |
| | | if /I "!REBUILD!"=="N" ( |
| | | echo [INFO] Skip build. |
| | | goto CHECK_ZIP |
| | | ) |
| | | ) |
| | | |
| | | echo [INFO] Company: %COMPANY% |
| | | echo [INFO] Command: pnpm run build |
| | | echo. |
| | | |
| | | set "VITE_COMPANY=%COMPANY%" |
| | | |
| | | call pnpm run build |
| | | |
| | | if errorlevel 1 ( |
| | | echo. |
| | | echo [ERROR] Build failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo. |
| | | echo [OK] Build completed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check ZIP |
| | | REM ============================================================ |
| | | |
| | | :CHECK_ZIP |
| | | |
| | | echo. |
| | | echo [5/7] Checking package... |
| | | |
| | | if not exist "%ZIP_FILE%" ( |
| | | echo [ERROR] %ZIP_FILE% not found. |
| | | echo [HINT] Please check whether your build generates dist.zip. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | for %%F in ("%ZIP_FILE%") do set "ZIP_BYTES=%%~zF" |
| | | |
| | | set /a ZIP_MB=ZIP_BYTES/1024/1024 |
| | | |
| | | echo [OK] Package found. |
| | | echo [INFO] File size: !ZIP_BYTES! bytes |
| | | echo [INFO] File size: !ZIP_MB! MB |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Set sshpass password |
| | | REM ============================================================ |
| | | |
| | | set "SSHPASS=%PASSWORD%" |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Test SSH |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [6/7] Testing SSH connection... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "echo SSH_OK" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] SSH connection failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] SSH connection successful. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Create remote directory |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Creating remote directory... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mkdir -p '%REMOTE_DIR%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Failed to create remote directory. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] Remote directory ready. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Upload temporary file |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Uploading package... |
| | | |
| | | set "REMOTE_TMP=%REMOTE_DIR%/.dist.zip.uploading" |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_TMP%'" |
| | | |
| | | sshpass -e scp -P %REMOTE_PORT% %SSH_OPTS% "%ZIP_FILE%" %REMOTE_USER%@%REMOTE_HOST%:"%REMOTE_TMP%" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Upload failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] Upload completed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check remote file |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Checking remote package... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "test -f '%REMOTE_TMP%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Remote package does not exist. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] Remote package exists. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Compare file size |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Checking package size... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "stat -c %%s '%REMOTE_TMP%'" |
| | | |
| | | echo. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Rename temporary file |
| | | REM ============================================================ |
| | | |
| | | echo [INFO] Activating package... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mv -f '%REMOTE_TMP%' '%REMOTE_DIR%/%ZIP_FILE%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Failed to activate package. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] Package activated. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check unzip |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Checking unzip... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "command -v unzip" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] unzip is not installed on server. |
| | | echo. |
| | | echo CentOS/RHEL: |
| | | echo yum install -y unzip |
| | | echo. |
| | | echo Ubuntu/Debian: |
| | | echo apt install -y unzip |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] unzip found. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Test ZIP |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Testing remote ZIP... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -t '%ZIP_FILE%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo. |
| | | echo [ERROR] ZIP test failed. |
| | | echo [INFO] Remote ZIP has been kept. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] ZIP test passed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Extract |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Extracting package... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -o '%ZIP_FILE%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo. |
| | | echo [ERROR] Remote unzip failed. |
| | | echo [INFO] Remote ZIP has been kept. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo. |
| | | echo [OK] Extract completed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Remove ZIP |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Removing remote ZIP... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_DIR%/%ZIP_FILE%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [WARN] Failed to remove remote ZIP. |
| | | ) else ( |
| | | echo [OK] Remote ZIP removed. |
| | | ) |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Success |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo ============================================================ |
| | | echo DEPLOY SUCCESS |
| | | echo ============================================================ |
| | | echo. |
| | | echo Company : %COMPANY% |
| | | echo Server : %REMOTE_HOST%:%REMOTE_PORT% |
| | | echo Remote Dir : %REMOTE_DIR% |
| | | echo Package : %ZIP_FILE% |
| | | echo Size : !ZIP_MB! MB |
| | | echo. |
| | | echo Deployment completed successfully. |
| | | echo. |
| | | |
| | | goto END |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Error |
| | | REM ============================================================ |
| | | |
| | | :ERROR_EXIT |
| | | |
| | | echo. |
| | | echo ============================================================ |
| | | echo DEPLOY FAILED |
| | | echo ============================================================ |
| | | echo. |
| | | echo Please check the error message above. |
| | | echo. |
| | | |
| | | goto END |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM End |
| | | REM ============================================================ |
| | | |
| | | :END |
| | | |
| | | pause |
| | | endlocal |