Merge remote-tracking branch 'refs/remotes/origin/dev_pro2.0' into dev_宁夏_九泓化工
# Conflicts:
# company.config.ts
# deploy.bat
# vite.config.ts
已添加122个文件
已删除2个文件
已修改254个文件
| | |
| | | VITE_BASE=/ |
| | | |
| | | # 请æ±è·¯å¾ |
| | | VITE_BASE_URL=http://36.213.180.217:9500 |
| | | VITE_BASE_URL=http://36.213.90.123:9501 |
| | | # æ¥å£å°å |
| | | VITE_GLOB_API_URL=http://36.213.180.217:9500/admin-api |
| | | VITE_GLOB_API_URL=http://36.213.90.123:9501/admin-api |
| | | # æä»¶ä¸ä¼ ç±»åï¼server - å端ä¸ä¼ ï¼ client - å端ç´è¿ä¸ä¼ ï¼ä»
æ¯æS3æå¡ |
| | | VITE_UPLOAD_TYPE=server |
| | | |
| | |
| | | <excludeFolder url="file://$MODULE_DIR$/.cache" /> |
| | | <excludeFolder url="file://$MODULE_DIR$/dist" /> |
| | | <excludeFolder url="file://$MODULE_DIR$/build" /> |
| | | <excludeFolder url="file://$MODULE_DIR$/.agents" /> |
| | | <excludeFolder url="file://$MODULE_DIR$/.codex" /> |
| | | </content> |
| | | <orderEntry type="inheritedJdk" /> |
| | | <orderEntry type="sourceFolder" forTests="false" /> |
| | |
| | | |
| | | --- |
| | | |
| | | ## åãç¸å
³èµæº |
| | | ## åãGit æäº¤è§è |
| | | |
| | | ### 10.1 æäº¤æ ¼å¼ |
| | | |
| | | ``` |
| | | <type>: <subject> |
| | | |
| | | <body> |
| | | ``` |
| | | |
| | | - **type** å **subject** ä¹é´å¿
é¡»æä¸ä¸ªç©ºæ ¼ï¼`: `ï¼ |
| | | - **subject** å¿
å¡«ï¼ç®çæè¿°åæ´å
容 |
| | | - **body** å¯éï¼ä¸ºç©ºæ¶è·³è¿å³å¯ |
| | | |
| | | ### 10.2 æ¯æç Type |
| | | |
| | | | type | 说æ | |
| | | | ---------- | ------------------ | |
| | | | `feat` | æ°åè½ | |
| | | | `fix` | ä¿®å¤ Bug | |
| | | | `docs` | ææ¡£ä¿®æ¹ | |
| | | | `style` | ä»£ç æ ¼å¼ï¼ä¸å½±åé»è¾ï¼ | |
| | | | `refactor` | 代ç éæ | |
| | | | `perf` | æ§è½ä¼å | |
| | | | `test` | æ·»å æµè¯ä»£ç | |
| | | | `build` | ä¾èµ/æå»ºä¿®æ¹ | |
| | | | `ci` | CI/CD éæ | |
| | | | `chore` | æ¥å¸¸äºå¡ | |
| | | | `revert` | çæ¬åé | |
| | | | `types` | ç±»åå®ä¹ä¿®æ¹ | |
| | | |
| | | ### 10.3 ç¤ºä¾ |
| | | |
| | | ```bash |
| | | # æç®åï¼åªæä¸»é¢ï¼ |
| | | feat: æ·»å ç©æç±»å管çé¡µé¢ |
| | | |
| | | # 带详ç»è¯´æ |
| | | fix: ä¿®å¤å®¡æ¹æµç¨æ°æ®ä¸¢å¤± |
| | | |
| | | åå æ¯å端æ¥å£è¿åæ ¼å¼ååï¼è°æ´äºæ°æ®è§£æé»è¾ã |
| | | |
| | | # 带åºå·å表 |
| | | refactor: éæè¯·æ±æ¦æªå¨ |
| | | |
| | | 1. æåå
Œ
±é误å¤çé»è¾ |
| | | 2. ç»ä¸ Token å·æ°æºå¶ |
| | | ``` |
| | | |
| | | ### 10.4 èªå¨æ ¡éª |
| | | |
| | | 项ç®å·²é
ç½® commitlintï¼æ¯æ¬¡æäº¤æ¶ä¼èªå¨æ ¡éªæ ¼å¼ãå¦ææ ¼å¼ä¸ç¬¦åè§èï¼Git ä¼æç»æäº¤å¹¶æç¤ºé误ã |
| | | |
| | | ä¹å¯ä»¥ä½¿ç¨äº¤äºå¼æäº¤ï¼ |
| | | |
| | | ```bash |
| | | pnpm commit |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## åä¸ãç¸å
³èµæº |
| | | |
| | | - [Vue 3 ææ¡£](https://vuejs.org/) |
| | | - [Ant Design Vue ææ¡£](https://antdv.com/) |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | @echo off |
| | | setlocal EnableExtensions EnableDelayedExpansion |
| | | |
| | | REM ============================================================ |
| | | REM Vue Frontend Deploy Script |
| | | REM ============================================================ |
| | | |
| | | REM ========================= |
| | | REM Configuration |
| | | REM ========================= |
| | | |
| | | set "COMPANY=JHHG" |
| | | |
| | | set "REMOTE_USER=root" |
| | | set "REMOTE_HOST=42.63.70.90" |
| | | set "REMOTE_PORT=22" |
| | | |
| | | set "PASSWORD=Nxjhhg!@##@!" |
| | | |
| | | set "ZIP_FILE=dist.zip" |
| | | set "REMOTE_DIR=/home/vue/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 |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | @echo off |
| | | setlocal EnableExtensions EnableDelayedExpansion |
| | | |
| | | REM ============================================================ |
| | | REM Vue Frontend Deploy Script |
| | | REM ============================================================ |
| | | |
| | | REM ========================= |
| | | REM Configuration |
| | | REM ========================= |
| | | |
| | | set "COMPANY=WTXC" |
| | | |
| | | set "REMOTE_USER=root" |
| | | set "REMOTE_HOST=42.63.71.140" |
| | | set "REMOTE_PORT=22" |
| | | |
| | | set "PASSWORD=Nxwtxc123." |
| | | |
| | | set "ZIP_FILE=dist.zip" |
| | | set "REMOTE_DIR=/home/vue/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 |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | @echo off |
| | | setlocal EnableExtensions EnableDelayedExpansion |
| | | |
| | | REM ============================================================ |
| | | REM Vue Frontend Deploy Script |
| | | REM ============================================================ |
| | | |
| | | REM ========================= |
| | | REM Configuration |
| | | REM ========================= |
| | | |
| | | set "COMPANY=YZFX" |
| | | |
| | | set "REMOTE_USER=root" |
| | | set "REMOTE_HOST=42.63.70.231" |
| | | set "REMOTE_PORT=22" |
| | | |
| | | set "PASSWORD=Nxyzfxxcl1@" |
| | | |
| | | set "ZIP_FILE=dist.zip" |
| | | set "REMOTE_DIR=/home/vue/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 |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | @echo off |
| | | setlocal EnableExtensions EnableDelayedExpansion |
| | | |
| | | REM ============================================================ |
| | | REM Vue Frontend Deploy Script |
| | | REM ============================================================ |
| | | |
| | | REM ========================= |
| | | REM Configuration |
| | | REM ========================= |
| | | |
| | | set "COMPANY=ZSJC" |
| | | |
| | | set "REMOTE_USER=root" |
| | | set "REMOTE_HOST=116.172.73.239" |
| | | set "REMOTE_PORT=22" |
| | | |
| | | set "PASSWORD=ecs-QGY8ecs-QGY8" |
| | | |
| | | set "ZIP_FILE=dist.zip" |
| | | set "REMOTE_DIR=/home/vue/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 |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // å¼ç¨å
é¨ commitlint é
ç½® |
| | | export { default } from './internal/lint-configs/commitlint-config/index.mjs'; |
| | |
| | | logo: string; |
| | | /** ç½é¡µæ é¢å¾æ è·¯å¾ï¼ç¸å¯¹äº public ç®å½ */ |
| | | favicon: string; |
| | | /** API æ¥å£å°å */ |
| | | apiUrl: string; |
| | | } |
| | | |
| | | /** |
| | |
| | | * 示ä¾ï¼set "COMPANY=TEST" |
| | | */ |
| | | export const companies: Record<string, CompanyConfig> = { |
| | | JHHG: { name: '乿³åå·¥', logo: '/logos/TEST.png', favicon: '/logos/TEST.ico' }, |
| | | TEST: { |
| | | name: 'æµè¯å
¬å¸', |
| | | logo: '/logos/TEST.png', |
| | | favicon: '/logos/TEST.ico', |
| | | apiUrl: 'http://36.213.90.123:9501/admin-api', |
| | | }, |
| | | JHHG: { |
| | | name: 'å®å¤ä¹æ³åå·¥', |
| | | logo: '/logos/TEST.png', |
| | | favicon: '/logos/TEST.ico', |
| | | apiUrl: 'http://42.63.70.90:9001/admin-api', |
| | | }, |
| | | YZFX: { |
| | | name: 'å®å¤è±æ³½é²é', |
| | | logo: '/logos/YZFXLogo.png', |
| | | favicon: '/logos/YZFXLogo.ico', |
| | | apiUrl: 'http://42.63.70.231:9001/admin-api', |
| | | }, |
| | | WTXC: { |
| | | name: 'å®å¤ä¸éæ°æ', |
| | | logo: '/logos/YZFXLogo.png', |
| | | favicon: '/logos/YZFXLogo.ico', |
| | | apiUrl: 'http://42.63.71.140:9001/admin-api', |
| | | }, |
| | | YSJX: { |
| | | name: 'å®å¤çå±±æºæ¢°', |
| | | logo: '/logos/YZFXLogo.png', |
| | | favicon: '/logos/YZFXLogo.ico', |
| | | apiUrl: 'http://42.63.71.140:9004/admin-api', |
| | | }, |
| | | XLZB: { |
| | | name: 'å®å¤ååè£
å¤', |
| | | logo: '/logos/YZFXLogo.png', |
| | | favicon: '/logos/YZFXLogo.ico', |
| | | apiUrl: 'http://42.63.71.140:9004/admin-api', |
| | | }, |
| | | ZSJC: { |
| | | name: 'å®å¤ä¸ç建æç§ææéå
¬å¸', |
| | | logo: '/logos/YZFXLogo.png', |
| | | favicon: '/logos/YZFXLogo.ico', |
| | | apiUrl: 'http://116.172.73.239:9001/admin-api', |
| | | }, |
| | | KYZN: { |
| | | name: 'åº·ææºè½', |
| | | logo: '/logos/YZFXLogo.png', |
| | | favicon: '/logos/YZFXLogo.ico', |
| | | apiUrl: 'http://36.213.150.237:9004/admin-api', |
| | | }, |
| | | CHDL: { |
| | | name: 'é¿è¾çµå', |
| | | logo: '/logos/YZFXLogo.png', |
| | | favicon: '/logos/YZFXLogo.ico', |
| | | apiUrl: 'http://36.213.150.237:9006/admin-api', |
| | | }, |
| | | }; |
| | |
| | | @echo off |
| | | chcp 65001 >nul |
| | | setlocal enabledelayedexpansion |
| | | setlocal EnableExtensions EnableDelayedExpansion |
| | | |
| | | :: ================= Config ================= |
| | | set "COMPANY=JHHG" |
| | | set "USER=root" |
| | | set "PASS=Xdrjkj@10086#" |
| | | set "IP=36.213.180.217" |
| | | set "PORT=65022" |
| | | set "DIST_DIR=dist" |
| | | set "REMOTE_DIR=/data/project/mom/vue/test" |
| | | set "SSH_OPTS=-o StrictHostKeyChecking=no -o ConnectTimeout=10" |
| | | :: =========================================== |
| | | REM ============================================================ |
| | | REM Vue Frontend Deploy Script |
| | | REM ============================================================ |
| | | |
| | | echo ========================================== |
| | | echo Vue Dist Auto Deploy Script |
| | | echo ========================================== |
| | | REM ========================= |
| | | REM Configuration |
| | | REM ========================= |
| | | |
| | | set "COMPANY=TEST" |
| | | |
| | | set "REMOTE_USER=root" |
| | | set "REMOTE_HOST=36.213.90.123" |
| | | set "REMOTE_PORT=65022" |
| | | |
| | | set "PASSWORD=Xdrjkj@10086#" |
| | | |
| | | set "ZIP_FILE=dist.zip" |
| | | set "REMOTE_DIR=/data/project/mom/vue/test/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. |
| | | |
| | | :: Check if sshpass is available |
| | | set "SSHPASS_DIR=%~dp0bin" |
| | | if exist "!SSHPASS_DIR!\sshpass.exe" ( |
| | | set "PATH=!SSHPASS_DIR!;%PATH%" |
| | | |
| | | 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 %errorlevel% equ 0 goto :check_node |
| | | |
| | | echo [WARN] sshpass not found, trying to install... |
| | | echo. |
| | | |
| | | where bash >nul 2>&1 |
| | | if %errorlevel% equ 0 ( |
| | | echo [INFO] Git Bash detected, trying pacman install... |
| | | bash -c "pacman -S --noconfirm sshpass" 2>nul |
| | | if !errorlevel! equ 0 ( |
| | | echo [OK] sshpass installed via pacman! |
| | | goto :check_node |
| | | ) |
| | | if not errorlevel 1 ( |
| | | echo [OK] sshpass found. |
| | | goto CHECK_PNPM |
| | | ) |
| | | |
| | | if not exist "!SSHPASS_DIR!" mkdir "!SSHPASS_DIR!" |
| | | echo [WARN] sshpass not found. |
| | | |
| | | if exist "!SSHPASS_DIR!\sshpass.exe" ( |
| | | set "PATH=!SSHPASS_DIR!;%PATH%" |
| | | echo [OK] sshpass found at: !SSHPASS_DIR! |
| | | goto :check_node |
| | | 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 -Command "$url='https://github.com/xhcoding/sshpass-win32/releases/download/v1.0.3/sshpass.exe'; $out='!SSHPASS_DIR!\sshpass.exe'; Write-Host \"Downloading to: $out\"; try { Invoke-WebRequest -Uri $url -OutFile $out -UseBasicParsing; exit 0 } catch { Write-Host $_.Exception.Message; exit 1 }" |
| | | if !errorlevel! neq 0 ( |
| | | echo [ERROR] Failed to download sshpass! |
| | | echo [HINT] Please install sshpass manually or use Git Bash: pacman -S sshpass |
| | | goto :error_exit |
| | | |
| | | 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 downloaded to: !SSHPASS_DIR! |
| | | echo. |
| | | set "PATH=%SSHPASS_DIR%;%PATH%" |
| | | |
| | | :check_node |
| | | :: Check if pnpm is available |
| | | echo [OK] sshpass installed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check pnpm |
| | | REM ============================================================ |
| | | |
| | | :CHECK_PNPM |
| | | |
| | | echo. |
| | | echo [2/7] Checking pnpm... |
| | | |
| | | where pnpm >nul 2>&1 |
| | | if %errorlevel% neq 0 ( |
| | | echo [ERROR] pnpm not found! Please install pnpm first. |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] pnpm not found. |
| | | echo [HINT] npm install -g pnpm |
| | | goto :error_exit |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | :: Check if node_modules exists |
| | | 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 [INFO] node_modules not found, running pnpm install... |
| | | echo. |
| | | echo [INFO] node_modules not found. |
| | | echo [INFO] Running pnpm install... |
| | | |
| | | call pnpm install |
| | | if !errorlevel! neq 0 ( |
| | | echo [ERROR] pnpm install failed! |
| | | goto :error_exit |
| | | |
| | | 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): " |
| | | |
| | | :: Check if dist exists and ask user |
| | | if exist "%DIST_DIR%" ( |
| | | echo [WARN] dist directory already exists. |
| | | set /p "REBUILD=Do you want to rebuild? (Y/N, default Y): " |
| | | if /i "!REBUILD!"=="N" ( |
| | | echo [INFO] Skip build, using existing dist |
| | | goto :upload |
| | | if /I "!REBUILD!"=="N" ( |
| | | echo [INFO] Skip build. |
| | | goto CHECK_ZIP |
| | | ) |
| | | ) |
| | | |
| | | :: Build the project |
| | | echo [STEP 1/3] Building project for %COMPANY%... |
| | | echo [INFO] Running: pnpm run build |
| | | echo. |
| | | set "COMPANY=!COMPANY!" && call pnpm run build |
| | | if !errorlevel! neq 0 ( |
| | | echo [ERROR] Build failed! Check your project configuration. |
| | | goto :error_exit |
| | | ) |
| | | echo [OK] Build completed! |
| | | echo [INFO] Company: %COMPANY% |
| | | echo [INFO] Command: pnpm run build |
| | | echo. |
| | | |
| | | :upload |
| | | if not exist "%DIST_DIR%" ( |
| | | echo [ERROR] Dist directory not found: %DIST_DIR% |
| | | echo [HINT] Build may have failed or output directory is incorrect. |
| | | goto :error_exit |
| | | set "VITE_COMPANY=%COMPANY%" |
| | | |
| | | call pnpm run build |
| | | |
| | | if errorlevel 1 ( |
| | | echo. |
| | | echo [ERROR] Build failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | :: Count files and calculate size |
| | | set "FILE_COUNT=0" |
| | | set "TOTAL_SIZE=0" |
| | | for /r "%DIST_DIR%" %%F in (*) do ( |
| | | set /a "FILE_COUNT+=1" |
| | | set /a "TOTAL_SIZE+=%%~zF" |
| | | ) |
| | | set /a "TOTAL_SIZE_MB=TOTAL_SIZE/1024/1024" |
| | | |
| | | echo [INFO] Dist directory: %DIST_DIR% |
| | | echo [INFO] Files count: %FILE_COUNT% |
| | | echo [INFO] Total size: %TOTAL_SIZE_MB% MB |
| | | echo [INFO] Target: %USER%@%IP%:%PORT% |
| | | echo [INFO] Remote dir: %REMOTE_DIR% |
| | | echo. |
| | | echo [OK] Build completed. |
| | | |
| | | :: Optional: Ask for confirmation before upload |
| | | set /p "CONFIRM=Proceed with upload? (Y/N, default Y): " |
| | | if /i "!CONFIRM!"=="N" ( |
| | | echo [INFO] Upload cancelled by user. |
| | | goto :end |
| | | |
| | | 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 |
| | | ) |
| | | |
| | | :: Upload dist directory |
| | | echo [STEP 2/3] Uploading dist directory... |
| | | sshpass -p "%PASS%" scp -P %PORT% %SSH_OPTS% -r "%DIST_DIR%" %USER%@%IP%:%REMOTE_DIR%/ |
| | | if %errorlevel% neq 0 ( |
| | | echo [ERROR] Upload failed! Check network or credentials. |
| | | goto :error_exit |
| | | 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] Upload completed! |
| | | echo. |
| | | |
| | | :: Optional: Remote restart/cleanup (uncomment if needed) |
| | | :: echo [STEP 3/3] Restarting remote service... |
| | | :: sshpass -p "%PASS%" ssh -p %PORT% %SSH_OPTS% %USER%@%IP% "cd %REMOTE_DIR% && npm run restart" |
| | | echo [OK] SSH connection successful. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Create remote directory |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo ========================================== |
| | | echo [SUCCESS] Deploy completed! |
| | | 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 Summary: |
| | | echo - Build: Completed |
| | | echo - Upload: %FILE_COUNT% files (%TOTAL_SIZE_MB% MB) |
| | | echo - Target: %USER%@%IP%:%PORT%%REMOTE_DIR% |
| | | 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. |
| | | |
| | | goto :end |
| | | |
| | | :error_exit |
| | | 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 ========================================== |
| | | echo [FAILED] Deploy failed! |
| | | echo ========================================== |
| | | echo [INFO] Checking unzip... |
| | | |
| | | :end |
| | | pause |
| | | 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 |
| | |
| | | * ^^^^^^^^^^^^^^ empty line. |
| | | * - Something here |
| | | */ |
| | | 'body-leading-blank': [2, 'always'], |
| | | 'body-leading-blank': [0], |
| | | /** |
| | | * type[scope]: [function] description |
| | | * |
| | |
| | | match = 'VITE_GLOB_', |
| | | confFiles = getConfFiles(), |
| | | ) { |
| | | let envConfig = {}; |
| | | let envConfig: Record<string, string> = {}; |
| | | |
| | | for (const confFile of confFiles) { |
| | | try { |
| | |
| | | Reflect.deleteProperty(envConfig, key); |
| | | } |
| | | }); |
| | | // åå¹¶è¿ç¨ç¯å¢åéï¼ä½¿ vite.config.ts æå
¬å¸æ³¨å
¥ç VITE_GLOB_API_URL è½è¿å
¥ _app-config æä»¶ |
| | | Object.keys(process.env).forEach((key) => { |
| | | const value = process.env[key]; |
| | | if (reg.test(key) && value) { |
| | | envConfig[key] = value; |
| | | } |
| | | }); |
| | | return envConfig as T; |
| | | } |
| | | |
| | |
| | | "scripts": { |
| | | "build": "pnpm vite build --mode production", |
| | | "build:analyze": "pnpm vite build --mode analyze", |
| | | "commit": "czg", |
| | | "dev": "pnpm vite --mode development", |
| | | "preview": "vite preview", |
| | | "typecheck": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vue-tsc --noEmit --skipLibCheck --incremental --tsBuildInfoFile node_modules/.cache/vue-tsc/tsconfig.tsbuildinfo" |
| | |
| | | "@vben/tailwind-config": "workspace:*", |
| | | "@vben/tsconfig": "workspace:*", |
| | | "@vben/vite-config": "workspace:*", |
| | | "cross-env": "catalog:", |
| | | "jiti": "^2.7.0", |
| | | "tsdown": "^0.22.3", |
| | | "tsx": "^4.22.4", |
| | | "typescript": "catalog:", |
| | | "vite": "catalog:" |
| | | "vite": "catalog:", |
| | | "vue-tsc": "catalog:" |
| | | }, |
| | | "packageManager": "pnpm@11.0.9+sha512.34ce82e6780233cf9cad8685029a8f81d2e06196c5a9bad98879f7424940c6817c4e4524fb7d38b8553ceed48b9758b8ebaf1abd3600c232c4c8cf7366086f38" |
| | | } |
| | |
| | | cropperjs: |
| | | specifier: ^1.6.2 |
| | | version: 1.6.2 |
| | | cross-env: |
| | | specifier: ^10.1.0 |
| | | version: 10.1.0 |
| | | crypto-js: |
| | | specifier: ^4.2.0 |
| | | version: 4.2.0 |
| | |
| | | vue-tippy: |
| | | specifier: ^6.7.1 |
| | | version: 6.7.1 |
| | | vue-tsc: |
| | | specifier: ^3.3.9 |
| | | version: 3.3.9 |
| | | vue3-print-nb: |
| | | specifier: ^0.1.4 |
| | | version: 0.1.4 |
| | |
| | | '@vben/vite-config': |
| | | specifier: workspace:* |
| | | version: link:internal/vite-config |
| | | cross-env: |
| | | specifier: 'catalog:' |
| | | version: 10.1.0 |
| | | jiti: |
| | | specifier: ^2.7.0 |
| | | version: 2.7.0 |
| | | tsdown: |
| | | specifier: ^0.22.3 |
| | | version: 0.22.3(tsx@4.22.4)(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13)) |
| | | version: 0.22.3(tsx@4.22.4)(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13))(vue-tsc@3.3.9(typescript@6.0.3)) |
| | | tsx: |
| | | specifier: ^4.22.4 |
| | | version: 4.22.4 |
| | |
| | | vite: |
| | | specifier: 'catalog:' |
| | | version: 8.0.10(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) |
| | | vue-tsc: |
| | | specifier: 'catalog:' |
| | | version: 3.3.9(typescript@6.0.3) |
| | | |
| | | internal/lint-configs/commitlint-config: |
| | | dependencies: |
| | |
| | | |
| | | '@emotion/unitless@0.8.1': |
| | | resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==} |
| | | |
| | | '@epic-web/invariant@1.0.0': |
| | | resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} |
| | | |
| | | '@es-joy/jsdoccomment@0.84.0': |
| | | resolution: {integrity: sha512-0xew1CxOam0gV5OMjh2KjFQZsKL2bByX1+q4j3E73MpYIdyUxcZb/xQct9ccUb+ve5KGUYbCUxyPnYB7RbuP+w==} |
| | |
| | | '@vue/devtools-shared@8.1.3': |
| | | resolution: {integrity: sha512-CM3uIPL+v+lrJUk33+pxspYo0MhuMWlCvf7zC9fybifvCPyM2jUbYRPwoYEJgYbwRqPikm5HozbUhp60MF2QuA==} |
| | | |
| | | '@vue/language-core@3.3.9': |
| | | resolution: {integrity: sha512-in/68oAa4BCtVY6n/nkuhLIkV8DHYd2UivedJ6cMZ6UYtlq9jaoaSNUBHYCVO44z3nKg7MdE5OBoHKt5SxeBKQ==} |
| | | |
| | | '@vue/reactivity@3.5.38': |
| | | resolution: {integrity: sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==} |
| | | |
| | |
| | | |
| | | ajv@8.20.0: |
| | | resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} |
| | | |
| | | alien-signals@3.2.1: |
| | | resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==} |
| | | |
| | | ansi-regex@5.0.1: |
| | | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} |
| | |
| | | |
| | | cropperjs@1.6.2: |
| | | resolution: {integrity: sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==} |
| | | |
| | | cross-env@10.1.0: |
| | | resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==} |
| | | engines: {node: '>=20'} |
| | | hasBin: true |
| | | |
| | | cross-spawn@7.0.6: |
| | | resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} |
| | |
| | | peerDependencies: |
| | | vue: ^3.2.0 |
| | | |
| | | vue-tsc@3.3.9: |
| | | resolution: {integrity: sha512-TS3Y1ux/IRoE8OCP2PpACAeOseuIs0UvWrcr7u+w3PmfY+SlCfEf8zjrBgnQksHUgLpthi5vHlffcQTQTdPBZA==} |
| | | hasBin: true |
| | | peerDependencies: |
| | | typescript: '>=5.0.0' |
| | | |
| | | vue-types@3.0.0: |
| | | resolution: {integrity: sha512-xzq2D7zLGbzn3bPQ0CWWYx4W78JWdGYZAFfRkzxz1aTm9DaNoH0qxYDiLXBofBEQ5X78l32uIq/G9INQ2E8X/Q==} |
| | | engines: {node: '>=10.15.0'} |
| | |
| | | '@emotion/hash@0.9.2': {} |
| | | |
| | | '@emotion/unitless@0.8.1': {} |
| | | |
| | | '@epic-web/invariant@1.0.0': {} |
| | | |
| | | '@es-joy/jsdoccomment@0.84.0': |
| | | dependencies: |
| | |
| | | |
| | | '@vue/devtools-shared@8.1.3': {} |
| | | |
| | | '@vue/language-core@3.3.9': |
| | | dependencies: |
| | | '@volar/language-core': 2.4.28 |
| | | '@vue/compiler-dom': 3.5.38 |
| | | '@vue/shared': 3.5.38 |
| | | alien-signals: 3.2.1 |
| | | muggle-string: 0.4.1 |
| | | path-browserify: 1.0.1 |
| | | picomatch: 4.0.4 |
| | | |
| | | '@vue/reactivity@3.5.38': |
| | | dependencies: |
| | | '@vue/shared': 3.5.38 |
| | |
| | | fast-uri: 3.1.2 |
| | | json-schema-traverse: 1.0.0 |
| | | require-from-string: 2.0.2 |
| | | |
| | | alien-signals@3.2.1: {} |
| | | |
| | | ansi-regex@5.0.1: {} |
| | | |
| | |
| | | croner@10.0.1: {} |
| | | |
| | | cropperjs@1.6.2: {} |
| | | |
| | | cross-env@10.1.0: |
| | | dependencies: |
| | | '@epic-web/invariant': 1.0.0 |
| | | cross-spawn: 7.0.6 |
| | | |
| | | cross-spawn@7.0.6: |
| | | dependencies: |
| | |
| | | |
| | | robust-predicates@3.0.3: {} |
| | | |
| | | rolldown-plugin-dts@0.26.0(rolldown@1.1.2)(typescript@6.0.3): |
| | | rolldown-plugin-dts@0.26.0(rolldown@1.1.2)(typescript@6.0.3)(vue-tsc@3.3.9(typescript@6.0.3)): |
| | | dependencies: |
| | | '@babel/generator': 8.0.0 |
| | | '@babel/helper-validator-identifier': 8.0.2 |
| | |
| | | rolldown: 1.1.2 |
| | | optionalDependencies: |
| | | typescript: 6.0.3 |
| | | vue-tsc: 3.3.9(typescript@6.0.3) |
| | | transitivePeerDependencies: |
| | | - oxc-resolver |
| | | |
| | |
| | | minimist: 1.2.8 |
| | | strip-bom: 3.0.0 |
| | | |
| | | tsdown@0.22.3(tsx@4.22.4)(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13)): |
| | | tsdown@0.22.3(tsx@4.22.4)(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13))(vue-tsc@3.3.9(typescript@6.0.3)): |
| | | dependencies: |
| | | ansis: 4.3.1 |
| | | cac: 7.0.0 |
| | |
| | | obug: 2.1.3 |
| | | picomatch: 4.0.4 |
| | | rolldown: 1.1.2 |
| | | rolldown-plugin-dts: 0.26.0(rolldown@1.1.2)(typescript@6.0.3) |
| | | rolldown-plugin-dts: 0.26.0(rolldown@1.1.2)(typescript@6.0.3)(vue-tsc@3.3.9(typescript@6.0.3)) |
| | | semver: 7.8.5 |
| | | tinyexec: 1.2.4 |
| | | tinyglobby: 0.2.17 |
| | |
| | | tippy.js: 6.3.7 |
| | | vue: 3.5.38(typescript@6.0.3) |
| | | |
| | | vue-tsc@3.3.9(typescript@6.0.3): |
| | | dependencies: |
| | | '@volar/typescript': 2.4.28 |
| | | '@vue/language-core': 3.3.9 |
| | | typescript: 6.0.3 |
| | | |
| | | vue-types@3.0.0(vue@3.5.38(typescript@6.0.3)): |
| | | dependencies: |
| | | is-plain-object: 3.0.1 |
| | |
| | | vue-json-pretty: ^2.6.0 |
| | | vue-router: ^5.1.0 |
| | | vue-tippy: ^6.7.1 |
| | | vue-tsc: ^3.3.3 |
| | | vue-tsc: ^3.3.9 |
| | | vue3-print-nb: ^0.1.4 |
| | | vue3-signature: ^0.4.4 |
| | | vuedraggable: ^4.1.0 |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace AftersalesProblemApi { |
| | | /** å®åé®é¢ */ |
| | | export interface Problem { |
| | | id?: number; |
| | | no?: string; |
| | | title?: string; |
| | | sourceTicketId?: number; |
| | | sourceTicketNo?: string; |
| | | customerId?: number; |
| | | customerName?: string; |
| | | itemId?: number; |
| | | itemCode?: string; |
| | | itemName?: string; |
| | | batchId?: number; |
| | | batchCode?: string; |
| | | issueType?: number; |
| | | issueCategory?: string; |
| | | severityLevel?: number; |
| | | description?: string; |
| | | /** æ ¹å åç±»: 1-设计 2-ç©æ 3-å·¥èº 4-è£
é
5-è¿è¾ 6-å
è£
7-ä½¿ç¨ 8-软件 9-å
¶ä» */ |
| | | rootCauseCategory?: number; |
| | | rootCause?: string; |
| | | solution?: string; |
| | | /** ç¶æ: 0-è稿 1-å·²åå¸ 2-å·²åç¨ */ |
| | | status?: number; |
| | | ownerUserId?: number; |
| | | ownerUserName?: string; |
| | | publishTime?: string; |
| | | remark?: string; |
| | | creator?: string; |
| | | createTime?: string; |
| | | updater?: string; |
| | | updateTime?: string; |
| | | /** éä»¶ blobId å表ï¼ä¿åæ¶ä¼ å
¥ï¼ */ |
| | | blobIds?: number[]; |
| | | /** éä»¶å表ï¼è¯¦æ
è¿åï¼ */ |
| | | attachmentList?: { id: number; name?: string; url?: string }[]; |
| | | } |
| | | } |
| | | |
| | | /** æ°å¢å®åé®é¢ï¼èç¨¿ï¼ */ |
| | | export function createProblem(data: AftersalesProblemApi.Problem) { |
| | | return requestClient.post<number>('/aftersales/problem/create', data); |
| | | } |
| | | |
| | | /** æ´æ°å®åé®é¢ï¼ä»
èç¨¿ï¼ */ |
| | | export function updateProblem(data: AftersalesProblemApi.Problem) { |
| | | return requestClient.put<boolean>('/aftersales/problem/update', data); |
| | | } |
| | | |
| | | /** å é¤å®åé®é¢ï¼ä»
èç¨¿ï¼ */ |
| | | export function deleteProblem(id: number) { |
| | | return requestClient.delete<boolean>('/aftersales/problem/delete', { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** è·åå®åé®é¢è¯¦æ
*/ |
| | | export function getProblem(id: number) { |
| | | return requestClient.get<AftersalesProblemApi.Problem>( |
| | | `/aftersales/problem/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** å页æ¥è¯¢å®åé®é¢ */ |
| | | export function getProblemPage(params: PageParam) { |
| | | return requestClient.get<PageResult<AftersalesProblemApi.Problem>>( |
| | | '/aftersales/problem/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** åå¸é®é¢ï¼è稿/åç¨ -> å·²åå¸ï¼ */ |
| | | export function publishProblem(id: number) { |
| | | return requestClient.put<boolean>('/aftersales/problem/publish', null, { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** åç¨é®é¢ï¼å·²åå¸ -> åç¨ï¼ */ |
| | | export function disableProblem(id: number) { |
| | | return requestClient.put<boolean>('/aftersales/problem/disable', null, { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** åèæ£ç´¢ï¼æ°å·¥ååçï¼åªè¿åå·²åå¸ï¼ */ |
| | | export function referenceProblem(params: { |
| | | itemId?: number; |
| | | issueCategory?: string; |
| | | issueType?: number; |
| | | rootCauseCategory?: number; |
| | | keyword?: string; |
| | | }) { |
| | | return requestClient.get<AftersalesProblemApi.Problem[]>( |
| | | '/aftersales/problem/reference', |
| | | { params }, |
| | | ); |
| | | } |
| | |
| | | repairId?: number; |
| | | resolvedTime?: string; |
| | | closedTime?: string; |
| | | /** åè®¿ç¶æ: 0-å¾
å访 1-å·²å访éè¿ */ |
| | | visitStatus?: number; |
| | | lastVisitTime?: string; |
| | | lastVisitSatisfied?: boolean; |
| | | lastVisitRecurred?: boolean; |
| | | /** 彿¡£æ²æ·çé®é¢ */ |
| | | problemId?: number; |
| | | problemNo?: string; |
| | | remark?: string; |
| | | creator?: string; |
| | | createTime?: string; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace AftersalesVisitApi { |
| | | /** åè®¿è®°å½ */ |
| | | export interface Visit { |
| | | id?: number; |
| | | ticketId?: number; |
| | | visitType?: number; |
| | | visitorUserId?: number; |
| | | visitorUserName?: string; |
| | | content?: string; |
| | | satisfied?: boolean; |
| | | recurred?: boolean; |
| | | visitTime?: string; |
| | | remark?: string; |
| | | creator?: string; |
| | | createTime?: string; |
| | | /** éä»¶å表ï¼è¯¦æ
è¿åï¼ */ |
| | | attachmentList?: { id: number; name?: string; url?: string }[]; |
| | | } |
| | | |
| | | /** ç»è®°å访å
¥å */ |
| | | export interface CreateVisit { |
| | | ticketId: number; |
| | | visitType: number; |
| | | content?: string; |
| | | satisfied: boolean; |
| | | recurred: boolean; |
| | | remark?: string; |
| | | /** éä»¶ blobId å表ï¼ä¿åæ¶ä¼ å
¥ï¼ */ |
| | | blobIds?: number[]; |
| | | } |
| | | } |
| | | |
| | | /** ç»è®°å®åå访 */ |
| | | export function createVisit(data: AftersalesVisitApi.CreateVisit) { |
| | | return requestClient.post<number>('/aftersales/visit/create', data); |
| | | } |
| | | |
| | | /** è·åå访详æ
*/ |
| | | export function getVisit(id: number) { |
| | | return requestClient.get<AftersalesVisitApi.Visit>( |
| | | `/aftersales/visit/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** è·åæå·¥åçå访记å½å表 */ |
| | | export function getVisitListByTicketId(ticketId: number) { |
| | | return requestClient.get<AftersalesVisitApi.Visit[]>( |
| | | `/aftersales/visit/list?ticketId=${ticketId}`, |
| | | ); |
| | | } |
| | | |
| | | /** å页æ¥è¯¢åè®¿è®°å½ */ |
| | | export function getVisitPage(params: PageParam) { |
| | | return requestClient.get<PageResult<AftersalesVisitApi.Visit>>( |
| | | '/aftersales/visit/page', |
| | | { params }, |
| | | ); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace BpmAutoApproveApi { |
| | | /** æµç¨å
审æ¹é
ç½® */ |
| | | export interface AutoApproveConfig { |
| | | id: number; |
| | | /** æµç¨å®ä¹ç Key */ |
| | | processDefinitionKey: string; |
| | | /** æ¯å¦å
审æ¹ï¼true-æäº¤åç³»ç»èªå¨éè¿ï¼false-æ£å¸¸äººå·¥å®¡æ¹ */ |
| | | autoApprove: boolean; |
| | | /** 夿³¨ */ |
| | | remark?: string; |
| | | createTime: string; |
| | | } |
| | | } |
| | | |
| | | /** è·åå
¨é¨æµç¨å
审æ¹é
ç½® */ |
| | | export async function getAutoApproveList() { |
| | | return requestClient.get<BpmAutoApproveApi.AutoApproveConfig[]>( |
| | | '/bpm/process-auto-approve/list', |
| | | ); |
| | | } |
| | | |
| | | /** è·å¾å
¨å±å
å®¡æ¹æ»å¼å
³ç¶æ */ |
| | | export async function getGlobalAutoApprove() { |
| | | return requestClient.get<boolean>('/bpm/process-auto-approve/global'); |
| | | } |
| | | |
| | | /** 设置å
¨å±å
å®¡æ¹æ»å¼å
³ï¼å¼å¯åæææµç¨ä¸å¾å
å®¡æ¹ */ |
| | | export async function updateGlobalAutoApprove( |
| | | autoApprove: boolean, |
| | | remark?: string, |
| | | ) { |
| | | const params = new URLSearchParams({ autoApprove: String(autoApprove) }); |
| | | if (remark) { |
| | | params.append('remark', remark); |
| | | } |
| | | return requestClient.put<boolean>( |
| | | `/bpm/process-auto-approve/update-global?${params.toString()}`, |
| | | ); |
| | | } |
| | | |
| | | /** ææµç¨å®ä¹ç Key 设置å
审æ¹å¼å
³ï¼ä¸åå¨åæ°å¢ãåå¨åæ´æ°ï¼ */ |
| | | export async function updateAutoApproveByKey( |
| | | processDefinitionKey: string, |
| | | autoApprove: boolean, |
| | | remark?: string, |
| | | ) { |
| | | const params = new URLSearchParams({ |
| | | processDefinitionKey, |
| | | autoApprove: String(autoApprove), |
| | | }); |
| | | if (remark) { |
| | | params.append('remark', remark); |
| | | } |
| | | return requestClient.put<boolean>( |
| | | `/bpm/process-auto-approve/update-by-key?${params.toString()}`, |
| | | ); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace BpmBusinessAttachApi { |
| | | /** ä¸å¡éä»¶é
ç½® */ |
| | | export interface BusinessAttach { |
| | | id: number; |
| | | processDefinitionKey: string; |
| | | businessType?: string; |
| | | componentPath: string; |
| | | status: number; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢ä¸å¡éä»¶é
ç½®å页 */ |
| | | export async function getBusinessAttachPage(params: PageParam) { |
| | | return requestClient.get<PageResult<BpmBusinessAttachApi.BusinessAttach>>( |
| | | '/bpm/business-attach/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢ä¸å¡éä»¶é
置详æ
*/ |
| | | export async function getBusinessAttach(id: number) { |
| | | return requestClient.get<BpmBusinessAttachApi.BusinessAttach>( |
| | | `/bpm/business-attach/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢ä¸å¡éä»¶é
ç½® */ |
| | | export async function createBusinessAttach( |
| | | data: BpmBusinessAttachApi.BusinessAttach, |
| | | ) { |
| | | return requestClient.post<number>('/bpm/business-attach/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹ä¸å¡éä»¶é
ç½® */ |
| | | export async function updateBusinessAttach( |
| | | data: BpmBusinessAttachApi.BusinessAttach, |
| | | ) { |
| | | return requestClient.put<boolean>('/bpm/business-attach/update', data); |
| | | } |
| | | |
| | | /** å é¤ä¸å¡éä»¶é
ç½® */ |
| | | export async function deleteBusinessAttach(id: number) { |
| | | return requestClient.delete<boolean>( |
| | | `/bpm/business-attach/delete?id=${id}`, |
| | | ); |
| | | } |
| | |
| | | createTime: string; |
| | | bpmnXml: string; |
| | | startUsers?: UserInfo[]; |
| | | /** æ¯å¦å
审æ¹ï¼ç±é¡µé¢æ ¹æ®æµç¨å
审æ¹é
ç½®åå¹¶ï¼éå端è¿ååæ®µï¼ */ |
| | | autoApprove?: boolean; |
| | | } |
| | | |
| | | /** æµç¨å®ä¹ */ |
| | |
| | | totalPrice: number; |
| | | depositPrice?: number; // å®ééé¢ï¼åä½ï¼å
|
| | | totalReceivablePrice: number; |
| | | totalInvoicePrice?: number; // å·²å¼ç¥¨éé¢ï¼å«å®¡æ¹ä¸ï¼ï¼åä½ï¼å
|
| | | unInvoicePrice?: number; // æªå¼ç¥¨éé¢ï¼åä½ï¼å
|
| | | signContactId: number; |
| | | signContactName?: string; |
| | | signUserId: number; |
| | |
| | | export function generateSaleOrder(id: number) { |
| | | return requestClient.post<number>('/crm/contract/generate-sale-order', null, { params: { id } }); |
| | | } |
| | | |
| | | /** åå导å
¥ç»æ */ |
| | | export interface ImportResult { |
| | | /** å建æåçååç¼å· */ |
| | | createList?: string[]; |
| | | /** æ´æ°æåçååç¼å· */ |
| | | updateList?: string[]; |
| | | /** 导å
¥å¤±è´¥éåï¼key 为ååç¼å·æè¡å·ï¼value 为失败åå */ |
| | | failureList?: Record<string, string>; |
| | | } |
| | | |
| | | /** ä¸è½½åå导å
¥æ¨¡æ¿ */ |
| | | export function importContractTemplate() { |
| | | return requestClient.download('/crm/contract/get-import-template'); |
| | | } |
| | | |
| | | /** 导å
¥åå */ |
| | | export function importContract(file: File) { |
| | | return requestClient.upload<ImportResult>('/crm/contract/import', { file }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace CrmInvoiceApi { |
| | | /** ååä¿¡æ¯ */ |
| | | export interface Contract { |
| | | id?: number; |
| | | name?: string; |
| | | no: string; |
| | | } |
| | | |
| | | /** éä»¶ä¿¡æ¯ */ |
| | | export interface Attachment { |
| | | id: number; |
| | | url?: string; |
| | | name?: string; |
| | | } |
| | | |
| | | /** å¼ç¥¨ä¿¡æ¯ */ |
| | | export interface Invoice { |
| | | id?: number; |
| | | no?: string; |
| | | contractId?: number; |
| | | contract?: Contract; |
| | | customerId?: number; |
| | | customerName?: string; |
| | | invoiceType?: number; |
| | | invoiceNo?: string; |
| | | invoiceTitle?: string; |
| | | price?: number; |
| | | invoiceTime?: string; |
| | | auditStatus?: number; |
| | | remark?: string; |
| | | ownerUserId?: number; |
| | | ownerUserName?: string; |
| | | ownerUserDeptName?: string; |
| | | processInstanceId?: string; |
| | | attachmentList?: Attachment[]; |
| | | blobIds?: number[]; |
| | | hasAttachment?: boolean; |
| | | remainingInvoicePrice?: number; |
| | | creator?: string; |
| | | creatorName?: string; |
| | | createTime?: string; |
| | | updateTime?: string; |
| | | } |
| | | |
| | | /** å¼ç¥¨ç®è¦ä¿¡æ¯ï¼ç¨äºå款å
³èéæ©ï¼ */ |
| | | export interface InvoiceSimple { |
| | | id: number; |
| | | no: string; |
| | | invoiceNo?: string; |
| | | invoiceTitle?: string; |
| | | price?: number; |
| | | customerId?: number; |
| | | contractId?: number; |
| | | auditStatus?: number; |
| | | hasAttachment?: boolean; |
| | | remainingInvoicePrice?: number; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢å¼ç¥¨å页 */ |
| | | export function getInvoicePage(params: PageParam) { |
| | | return requestClient.get<PageResult<CrmInvoiceApi.Invoice>>( |
| | | '/crm/invoice/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢å¼ç¥¨å页ï¼åºäºæå®å®¢æ· */ |
| | | export function getInvoicePageByCustomer(params: PageParam) { |
| | | return requestClient.get<PageResult<CrmInvoiceApi.Invoice>>( |
| | | '/crm/invoice/page-by-customer', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢å¼ç¥¨è¯¦æ
*/ |
| | | export function getInvoice(id: number) { |
| | | return requestClient.get<CrmInvoiceApi.Invoice>( |
| | | `/crm/invoice/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢å¼ç¥¨ */ |
| | | export function createInvoice(data: CrmInvoiceApi.Invoice) { |
| | | return requestClient.post('/crm/invoice/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹å¼ç¥¨ */ |
| | | export function updateInvoice(data: CrmInvoiceApi.Invoice) { |
| | | return requestClient.put('/crm/invoice/update', data); |
| | | } |
| | | |
| | | /** å é¤å¼ç¥¨ */ |
| | | export function deleteInvoice(id: number) { |
| | | return requestClient.delete(`/crm/invoice/delete?id=${id}`); |
| | | } |
| | | |
| | | /** 导åºå¼ç¥¨ */ |
| | | export function exportInvoice(params: Record<string, unknown>) { |
| | | return requestClient.download('/crm/invoice/export-excel', { params }); |
| | | } |
| | | |
| | | /** æäº¤å®¡æ ¸ */ |
| | | export function submitInvoice(id: number) { |
| | | return requestClient.put(`/crm/invoice/submit?id=${id}`); |
| | | } |
| | | |
| | | /** è·å¾å¾
å®¡æ ¸å¼ç¥¨æ°é */ |
| | | export function getAuditInvoiceCount() { |
| | | return requestClient.get<number>('/crm/invoice/audit-count'); |
| | | } |
| | | |
| | | /** æ¥è¯¢å¼ç¥¨ä¸æåè¡¨ï¼æå忥å¯éå¼ç¥¨ï¼ */ |
| | | export function getInvoiceSimpleList(contractId: number) { |
| | | return requestClient.get<CrmInvoiceApi.InvoiceSimple[]>( |
| | | `/crm/invoice/simple-list?contractId=${contractId}`, |
| | | ); |
| | | } |
| | |
| | | CRM_PRODUCT = 6, // 产å |
| | | CRM_RECEIVABLE = 7, // 忬¾ |
| | | CRM_RECEIVABLE_PLAN = 8, // 忬¾è®¡å |
| | | CRM_INVOICE = 9, // å¼ç¥¨ |
| | | } |
| | | |
| | | /** |
| | |
| | | contractId?: number; |
| | | contractNo?: string; |
| | | contract?: Contract; |
| | | invoiceId?: number; |
| | | invoice?: InvoiceInfo; |
| | | auditStatus: number; |
| | | processInstanceId: number; |
| | | returnTime: Date; |
| | |
| | | name?: string; |
| | | no: string; |
| | | totalPrice: number; |
| | | } |
| | | |
| | | /** å¼ç¥¨ä¿¡æ¯ */ |
| | | export interface InvoiceInfo { |
| | | id?: number; |
| | | no?: string; |
| | | invoiceNo?: string; |
| | | invoiceTitle?: string; |
| | | price?: number; |
| | | } |
| | | |
| | | export interface ReceivablePageParam extends PageParam { |
| | |
| | | return requestClient.download('/crm/receivable/export-excel', { params }); |
| | | } |
| | | |
| | | /** æäº¤å®¡æ ¸ */ |
| | | export function submitReceivable(id: number) { |
| | | return requestClient.put(`/crm/receivable/submit?id=${id}`); |
| | | /** æ´æ°å款çç¶æï¼å®¡æ ¸/åå®¡æ ¸ï¼ */ |
| | | export function updateReceivableStatus(id: number, status: number) { |
| | | return requestClient.put('/crm/receivable/update-status', null, { |
| | | params: { id, status }, |
| | | }); |
| | | } |
| | | |
| | | /** è·å¾å¾
å®¡æ ¸åæ¬¾æ°é */ |
| | |
| | | quotationPrice?: number; |
| | | } |
| | | |
| | | /** AI OCR è¯å«æ¥ä»·æä»¶ååº */ |
| | | /** è¯å«æ¥ä»·æä»¶ååº */ |
| | | export interface OcrResultVO { |
| | | name?: string; |
| | | customerName?: string; |
| | |
| | | } |
| | | } |
| | | |
| | | /** AI OCR è¯å«æ¥ä»·æä»¶ */ |
| | | /** è¯å«æ¥ä»·æä»¶ */ |
| | | export function ocrSaleQuotation(blobId: number) { |
| | | return requestClient.post<CrmSaleQuotationAiApi.OcrResultVO>( |
| | | '/crm/sale-quotation/ai/ocr', |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace CrmSalesTargetApi { |
| | | /** éå®ç®æ ä¿¡æ¯ */ |
| | | export interface SalesTarget { |
| | | id?: number; |
| | | name?: string; |
| | | periodType?: number; |
| | | periodValue?: string; |
| | | startTime?: string; |
| | | endTime?: string; |
| | | targetDimension?: number; |
| | | targetUserId?: number; |
| | | targetUserName?: string; |
| | | targetDeptId?: number; |
| | | targetDeptName?: string; |
| | | targetItemId?: number; |
| | | targetItemName?: string; |
| | | contractTarget?: number; |
| | | receivableTarget?: number; |
| | | businessTarget?: number; |
| | | customerTarget?: number; |
| | | contractCountTarget?: number; |
| | | actualContractPrice?: number; |
| | | actualReceivablePrice?: number; |
| | | actualBusinessPrice?: number; |
| | | actualCustomerCount?: number; |
| | | actualContractCount?: number; |
| | | contractRate?: number; |
| | | receivableRate?: number; |
| | | businessRate?: number; |
| | | customerRate?: number; |
| | | contractCountRate?: number; |
| | | status?: number; |
| | | remark?: string; |
| | | creator?: string; |
| | | creatorName?: string; |
| | | createTime?: string; |
| | | updateTime?: string; |
| | | } |
| | | |
| | | /** éå®ç®æ å页æ¥è¯¢åæ° */ |
| | | export interface PageParams extends PageParam { |
| | | name?: string; |
| | | periodType?: number; |
| | | periodValue?: string; |
| | | targetDimension?: number; |
| | | status?: number; |
| | | } |
| | | |
| | | /** éå®ç®æ è¾¾æåææ¥è¯¢åæ° */ |
| | | export interface AnalysisParams { |
| | | periodType?: number; |
| | | periodValue?: string; |
| | | targetDimension?: number; |
| | | status?: number; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢éå®ç®æ å页 */ |
| | | export function getSalesTargetPage(params: CrmSalesTargetApi.PageParams) { |
| | | return requestClient.get<PageResult<CrmSalesTargetApi.SalesTarget>>( |
| | | '/crm/sales-target/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢éå®ç®æ 详æ
*/ |
| | | export function getSalesTarget(id: number) { |
| | | return requestClient.get<CrmSalesTargetApi.SalesTarget>( |
| | | `/crm/sales-target/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢éå®ç®æ */ |
| | | export function createSalesTarget(data: CrmSalesTargetApi.SalesTarget) { |
| | | return requestClient.post('/crm/sales-target/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹éå®ç®æ */ |
| | | export function updateSalesTarget(data: CrmSalesTargetApi.SalesTarget) { |
| | | return requestClient.put('/crm/sales-target/update', data); |
| | | } |
| | | |
| | | /** åæ´éå®ç®æ ç¶æï¼ä½åº/æ¢å¤çï¼ */ |
| | | export function changeSalesTargetStatus(data: { |
| | | id: number; |
| | | status: number; |
| | | }) { |
| | | return requestClient.put('/crm/sales-target/change-status', data); |
| | | } |
| | | |
| | | /** å é¤éå®ç®æ */ |
| | | export function deleteSalesTarget(id: number) { |
| | | return requestClient.delete(`/crm/sales-target/delete?id=${id}`); |
| | | } |
| | | |
| | | /** 导åºéå®ç®æ */ |
| | | export function exportSalesTarget(params: Record<string, unknown>) { |
| | | return requestClient.download('/crm/sales-target/export-excel', { params }); |
| | | } |
| | | |
| | | /** æ¥è¯¢éå®ç®æ è¾¾æåæ */ |
| | | export function getSalesTargetAnalysis( |
| | | params: CrmSalesTargetApi.AnalysisParams, |
| | | ) { |
| | | return requestClient.get<CrmSalesTargetApi.SalesTarget[]>( |
| | | '/crm/sales-target/get-achievement-analysis', |
| | | { params }, |
| | | ); |
| | | } |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import type { ErpPurchaseInvoiceApi } from '#/api/erp/purchase/invoice'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace ErpFinancePaymentApi { |
| | |
| | | no: string; // 仿¬¾åå· |
| | | supplierId?: number; // ä¾åºåç¼å· |
| | | supplierName?: string; // ä¾åºååç§° |
| | | invoiceId?: number; // å
³èæ¥ç¥¨ç¼å· |
| | | invoice?: ErpPurchaseInvoiceApi.PurchaseInvoice; // å
³èæ¥ç¥¨ä¿¡æ¯ |
| | | paymentTime?: Date; // 仿¬¾æ¶é´ |
| | | totalPrice: number; // å计éé¢ï¼åä½ï¼å
|
| | | discountPrice: number; // 伿 éé¢ |
| | | paymentPrice: number; // å®é
仿¬¾éé¢ |
| | | status: number; // ç¶æ |
| | | processInstanceId?: string; // 工使µç¼å· |
| | | remark: string; // 夿³¨ |
| | | attachmentList?: { id: number; name?: string; url?: string }[]; // éä»¶å表 |
| | | accountId?: number; // 仿¬¾è´¦æ· |
| | |
| | | return requestClient.put('/erp/finance-payment/update', data); |
| | | } |
| | | |
| | | /** æ´æ°ä»æ¬¾åçç¶æ */ |
| | | export function updateFinancePaymentStatus(id: number, status: number) { |
| | | return requestClient.put('/erp/finance-payment/update-status', null, { |
| | | params: { id, status }, |
| | | /** æäº¤ä»æ¬¾åå®¡æ¹ */ |
| | | export function submitFinancePayment(id: number, processDefinitionKey: string) { |
| | | return requestClient.post('/erp/finance-payment/submit', null, { |
| | | params: { id, processDefinitionKey }, |
| | | }); |
| | | } |
| | | |
| | | /** è·å仿¬¾åå®¡æ¹æµç¨å表 */ |
| | | export function getFinancePaymentApproveProcessList() { |
| | | return requestClient.get('/erp/finance-payment/approve-process-list'); |
| | | } |
| | | |
| | | /** å é¤ä»æ¬¾å */ |
| | | export function deleteFinancePayment(ids: number[]) { |
| | | return requestClient.delete('/erp/finance-payment/delete', { |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace ErpPurchaseInvoiceAiApi { |
| | | /** è¯å«å票ååº */ |
| | | export interface OcrResultVO { |
| | | /** å票å·ç */ |
| | | invoiceNo?: string; |
| | | /** å票æ¬å¤´ï¼è´ä¹°æ¹åç§°ï¼ */ |
| | | invoiceTitle?: string; |
| | | /** é宿¹åç§°ï¼ææ¬ï¼ä¾æ ¸å¯¹ä¾åºåï¼ */ |
| | | supplierName?: string; |
| | | /** æ¥ç¥¨éé¢ï¼ä»·ç¨å计ï¼ï¼åä½ï¼å
*/ |
| | | price?: number; |
| | | /** å¼ç¥¨æ¥æï¼æ ¼å¼ yyyy-MM-dd */ |
| | | invoiceTime?: string; |
| | | /** å票类å */ |
| | | invoiceType?: string; |
| | | /** ç¨çï¼ç¾åæ¯ï¼ */ |
| | | taxRate?: number; |
| | | /** 夿³¨ */ |
| | | remark?: string; |
| | | /** åå§è¯å«ææ¬ï¼è¯å«å¤±è´¥æ¶ä¸ºé误æç¤ºï¼ */ |
| | | rawText?: string; |
| | | } |
| | | } |
| | | |
| | | /** è¯å«å票æä»¶ */ |
| | | export function ocrPurchaseInvoice(blobId: number) { |
| | | return requestClient.post<ErpPurchaseInvoiceAiApi.OcrResultVO>( |
| | | '/erp/purchase-invoice/ai/ocr', |
| | | { blobId }, |
| | | ); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace ErpPurchaseInvoiceApi { |
| | | /** éè´è®¢åä¿¡æ¯ï¼æ¥ç¥¨å
³èï¼ */ |
| | | export interface PurchaseOrder { |
| | | id?: number; |
| | | no?: string; |
| | | supplierId?: number; |
| | | supplierName?: string; |
| | | orderTime?: Date | string; |
| | | totalPrice?: number; |
| | | status?: number; |
| | | } |
| | | |
| | | /** æ¥ç¥¨ä¿¡æ¯ */ |
| | | export interface PurchaseInvoice { |
| | | id?: number; |
| | | no?: string; |
| | | supplierId?: number; |
| | | supplierName?: string; |
| | | purchaseOrderId?: number; |
| | | order?: PurchaseOrder; |
| | | invoiceNo?: string; |
| | | invoiceTitle?: string; |
| | | price?: number; |
| | | invoiceTime?: string; |
| | | remark?: string; |
| | | attachmentList?: { id: number; name?: string; url?: string }[]; |
| | | blobIds?: number[]; |
| | | hasAttachment?: boolean; |
| | | remainingInvoicePrice?: number; |
| | | creator?: string; |
| | | creatorName?: string; |
| | | createTime?: string; |
| | | updateTime?: string; |
| | | } |
| | | |
| | | /** æ¥ç¥¨ç®è¦ä¿¡æ¯ï¼ç¨äºä»æ¬¾å
³èéæ©ï¼ */ |
| | | export interface PurchaseInvoiceSimple { |
| | | id: number; |
| | | no: string; |
| | | invoiceNo?: string; |
| | | invoiceTitle?: string; |
| | | price?: number; |
| | | supplierId?: number; |
| | | purchaseOrderId?: number; |
| | | hasAttachment?: boolean; |
| | | remainingInvoicePrice?: number; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢æ¥ç¥¨å页 */ |
| | | export function getPurchaseInvoicePage(params: PageParam) { |
| | | return requestClient.get<PageResult<ErpPurchaseInvoiceApi.PurchaseInvoice>>( |
| | | '/erp/purchase-invoice/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢æ¥ç¥¨è¯¦æ
*/ |
| | | export function getPurchaseInvoice(id: number) { |
| | | return requestClient.get<ErpPurchaseInvoiceApi.PurchaseInvoice>( |
| | | `/erp/purchase-invoice/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢æ¥ç¥¨ */ |
| | | export function createPurchaseInvoice(data: ErpPurchaseInvoiceApi.PurchaseInvoice) { |
| | | return requestClient.post('/erp/purchase-invoice/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹æ¥ç¥¨ */ |
| | | export function updatePurchaseInvoice(data: ErpPurchaseInvoiceApi.PurchaseInvoice) { |
| | | return requestClient.put('/erp/purchase-invoice/update', data); |
| | | } |
| | | |
| | | /** å 餿¥ç¥¨ */ |
| | | export function deletePurchaseInvoice(id: number) { |
| | | return requestClient.delete(`/erp/purchase-invoice/delete?id=${id}`); |
| | | } |
| | | |
| | | /** å¯¼åºæ¥ç¥¨ */ |
| | | export function exportPurchaseInvoice(params: Record<string, unknown>) { |
| | | return requestClient.download('/erp/purchase-invoice/export-excel', { params }); |
| | | } |
| | | |
| | | /** æ¥è¯¢æ¥ç¥¨ä¸æåè¡¨ï¼æä¾åºåå¯éæ¥ç¥¨ï¼å·²ä¸ä¼ å票éä»¶ä¸å©ä½å¯ä»éé¢å¤§äº 0ï¼ */ |
| | | export function getPurchaseInvoiceSimpleList( |
| | | supplierId: number, |
| | | purchaseOrderId?: number, |
| | | ) { |
| | | const params = `supplierId=${supplierId}${ |
| | | purchaseOrderId ? `&purchaseOrderId=${purchaseOrderId}` : '' |
| | | }`; |
| | | return requestClient.get<ErpPurchaseInvoiceApi.PurchaseInvoiceSimple[]>( |
| | | `/erp/purchase-invoice/simple-list?${params}`, |
| | | ); |
| | | } |
| | |
| | | totalCount?: number; // å计æ°é |
| | | totalPrice?: number; // å计éé¢ï¼åä½ï¼å
|
| | | totalProductPrice?: number; // 产åéé¢ï¼åä½ï¼å
|
| | | totalTaxPrice?: number; // å计ç¨é¢ï¼åä½ï¼å
|
| | | remainInvoicePrice?: number; // å©ä½å¯å¼ç¥¨éé¢ï¼è®¢åæ»éé¢-å·²æ¥ç¥¨éé¢ï¼ï¼åä½ï¼å
|
| | | discountPercent?: number; // 伿 çï¼ç¾åæ¯ |
| | | discountPrice?: number; // 伿 éé¢ï¼åä½ï¼å
|
| | | depositPrice?: number; // å®ééé¢ï¼åä½ï¼å
|
| | |
| | | return requestClient.get<any[]>('/erp/purchase-order/approve-process-list'); |
| | | } |
| | | |
| | | /** å审æ¹ï¼åååå
¬å·²åä»»å¡ä¸æ¤åå·²å®¡æ ¸è®¢åï¼åé为æªå®¡æ ¸ï¼ */ |
| | | export function reverseApprove(processInstanceId: string) { |
| | | return requestClient.post('/erp/purchase-order/reverse-approve', null, { |
| | | params: { processInstanceId }, |
| | | }); |
| | | } |
| | | |
| | | /** éè´è®¢å导å
¥ç»æ */ |
| | | export interface ImportResult { |
| | | /** å建æåçéè´åç¼å· */ |
| | | createList?: string[]; |
| | | /** æ´æ°æåçéè´åç¼å· */ |
| | | updateList?: string[]; |
| | | /** 导å
¥å¤±è´¥éåï¼key 为éè´åç¼å·æè¡å·ï¼value 为失败åå */ |
| | | failureList?: Record<string, string>; |
| | | } |
| | | |
| | | /** è·åéè´å®¡æ ¸æµç¨å表ååº */ |
| | | export interface ProcessDefinition { |
| | | id: string; // æµç¨å®ä¹ID |
| | |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** ä¸è½½éè´è®¢å导å
¥æ¨¡æ¿ */ |
| | | export function importPurchaseOrderTemplate() { |
| | | return requestClient.download('/erp/purchase-order/get-import-template'); |
| | | } |
| | | |
| | | /** 导å
¥éè´è®¢å */ |
| | | export function importPurchaseOrder(file: File, directApprove: boolean) { |
| | | return requestClient.upload<ImportResult>( |
| | | '/erp/purchase-order/import', |
| | | { file, directApprove }, |
| | | ); |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | /** éè´ç³è¯·å¯¼å
¥ç»æ */ |
| | | export interface ImportResult { |
| | | /** å建æåçéè´ç³è¯·åå· */ |
| | | createList?: string[]; |
| | | /** æ´æ°æåçéè´ç³è¯·åå· */ |
| | | updateList?: string[]; |
| | | /** 导å
¥å¤±è´¥éåï¼key 为éè´ç³è¯·åå·æè¡å·ï¼value 为失败åå */ |
| | | failureList?: Record<string, string>; |
| | | } |
| | | |
| | | /** å建éè´ç³è¯· */ |
| | | export function createPurchaseRequest(data: ErpPurchaseRequestApi.PurchaseRequest) { |
| | | return requestClient.post('/erp/purchase-request/create', data); |
| | |
| | | /** æäº¤éè´ç³è¯·å®¡æ¹ */ |
| | | export function submitPurchaseRequest(id: number, processDefinitionKey: string) { |
| | | return requestClient.put('/erp/purchase-request/submit', null, { params: { id, processDefinitionKey } }); |
| | | } |
| | | |
| | | /** é置被驳åçéè´ç³è¯·ä¸ºèç¨¿ç¶æ */ |
| | | export function resetPurchaseRequestStatus(id: number) { |
| | | return requestClient.put('/erp/purchase-request/reset-status', null, { params: { id } }); |
| | | } |
| | | |
| | | /** çæéè´è®¢å */ |
| | |
| | | export function getApproveProcessList(id: number) { |
| | | return requestClient.get(`/erp/purchase-request/approve-process-list`, { params: { id } }); |
| | | } |
| | | |
| | | /** BPM æµç¨å®ä¾ç¶æ â ä¸å¡ç¶ææ å° */ |
| | | export const BPM_STATUS_TO_BIZ: Record<number, number> = { |
| | | 1: 10, // RUNNING â 审æ¹ä¸ |
| | | 2: 20, // APPROVE â å®¡æ ¸éè¿ |
| | | 3: 30, // REJECT â å®¡æ ¸ä¸éè¿ |
| | | 4: 40, // CANCEL â 已忶 |
| | | }; |
| | | |
| | | /** |
| | | * 忥éè´ç³è¯·çä¸å¡ç¶æï¼æ ¹æ® BPM æµç¨å®ä¾å®é
ç¶æï¼ |
| | | * ç¨äºä¿®å¤å端çå¬å¨æªæ£ç¡®åå导è´çä¸å¡ç¶æä¸ä¸è´é®é¢ |
| | | */ |
| | | export async function syncPurchaseRequestStatus(items: ErpPurchaseRequestApi.PurchaseRequest[]) { |
| | | const processingItems = items.filter( |
| | | (item) => item.status === 10 && item.processInstanceId, |
| | | ); |
| | | if (processingItems.length === 0) return; |
| | | |
| | | const { getApprovalDetail } = await import('#/api/bpm/processInstance'); |
| | | for (const item of processingItems) { |
| | | try { |
| | | const detail = await getApprovalDetail({ |
| | | processInstanceId: item.processInstanceId, |
| | | }); |
| | | const bizStatus = BPM_STATUS_TO_BIZ[detail.processInstance?.status]; |
| | | if (bizStatus && bizStatus !== 10) { |
| | | item.status = bizStatus; |
| | | } |
| | | } catch { |
| | | // æ¥è¯¢å¤±è´¥æ¶ä¿æåæç¶æ |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** ä¸è½½éè´ç³è¯·å¯¼å
¥æ¨¡æ¿ */ |
| | | export function importPurchaseRequestTemplate() { |
| | | return requestClient.download('/erp/purchase-request/get-import-template'); |
| | | } |
| | | |
| | | /** 导å
¥éè´ç³è¯· */ |
| | | export function importPurchaseRequest(file: File) { |
| | | return requestClient.upload<ImportResult>('/erp/purchase-request/import', { file }); |
| | | } |
| | |
| | | customerId: number; // 客æ·ç¼å· |
| | | customerName?: string; // 客æ·åç§° |
| | | accountId?: number; // æ¶æ¬¾è´¦æ·ç¼å· |
| | | saleUserId?: number; // éå®äººåç¼å· |
| | | orderTime: Date; // è®¢åæ¶é´ |
| | | totalCount: number; // å计æ°é |
| | | totalPrice: number; // å计éé¢ï¼åä½ï¼å
|
| | | status: number; // ç¶æ |
| | | remark: string; // 夿³¨ |
| | | processInstanceId?: string; // BPM æµç¨å®ä¾ç¼å·ï¼æäº¤åååå
¬å®¡æ¹åçæï¼ |
| | | outCount: number; // éå®åºåºæ°é |
| | | outStatus?: number; // åºåºç¶æï¼0-æªåºåºï¼1-é¨ååºåºï¼2-å
¨é¨åºåº |
| | | attachmentList?: { id: number; name?: string; url?: string }[]; // éä»¶å表 |
| | |
| | | productionFinishTime?: Date; // çäº§å®ææ¶é´ |
| | | hasSalesNotice?: boolean; // æ¯å¦å·²çæåè´§éç¥å |
| | | canCreateSalesNotice?: boolean; // æ¯å¦å¯ä»¥çæåè´§éç¥å |
| | | creator?: string; // å建人ç¼å·ï¼åç¬¦ä¸²ï¼ |
| | | creatorName?: string; // å建人åç§° |
| | | createTime?: Date; // å建æ¶é´ |
| | | items?: SaleOrderItem[]; // éå®è®¢å产åæç»å表 |
| | | } |
| | | |
| | |
| | | totalTaxPrice?: number; // å«ç¨æ»ä»·ï¼åä½ï¼å
|
| | | remark?: string; // 夿³¨ |
| | | stockCount?: number; // åºåæ°éï¼æ¾ç¤ºåæ®µï¼ |
| | | productSpecification?: string; // è§æ ¼åå·ï¼æ¾ç¤ºåæ®µï¼ |
| | | needProduction?: number; // æ¯å¦éè¦ç产ï¼0-ä¸éè¦ï¼1-éè¦ |
| | | mpsId?: number; // MPSç¼å· |
| | | batchCode?: string; // æ¹æ¬¡å· |
| | | } |
| | | } |
| | | |
| | | /** éå®è®¢å导å
¥ç»æ */ |
| | | export interface ImportResult { |
| | | /** å建æåçéå®åç¼å· */ |
| | | createList?: string[]; |
| | | /** æ´æ°æåçéå®åç¼å· */ |
| | | updateList?: string[]; |
| | | /** 导å
¥å¤±è´¥éåï¼key 为éå®åç¼å·æè¡å·ï¼value 为失败åå */ |
| | | failureList?: Record<string, string>; |
| | | } |
| | | |
| | | /** æ¥è¯¢éå®è®¢åå页 */ |
| | |
| | | return requestClient.put('/erp/sale-order/update', data); |
| | | } |
| | | |
| | | /** æ´æ°éå®è®¢åçç¶æ */ |
| | | export function updateSaleOrderStatus(id: number, status: number) { |
| | | return requestClient.put('/erp/sale-order/update-status', null, { |
| | | params: { id, status }, |
| | | /** æäº¤éå®è®¢å审æ¹ï¼åååå
¬ï¼ */ |
| | | export function submitSaleOrder(id: number, processDefinitionKey: string) { |
| | | return requestClient.post('/erp/sale-order/submit', null, { |
| | | params: { id, processDefinitionKey }, |
| | | }); |
| | | } |
| | | |
| | | /** è·åéå®è®¢åå®¡æ¹æµç¨å®ä¹å表 */ |
| | | export function getSaleOrderApproveProcessList() { |
| | | return requestClient.get< |
| | | { id: string; key: string; name: string }[] |
| | | >('/erp/sale-order/approve-process-list'); |
| | | } |
| | | |
| | | /** å é¤éå®è®¢å */ |
| | |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** ä¸è½½éå®è®¢å导å
¥æ¨¡æ¿ */ |
| | | export function importSaleOrderTemplate() { |
| | | return requestClient.download('/erp/sale-order/get-import-template'); |
| | | } |
| | | |
| | | /** 导å
¥éå®è®¢å */ |
| | | export function importSaleOrder(file: File) { |
| | | return requestClient.upload<ImportResult>('/erp/sale-order/import', { file }); |
| | | } |
| | |
| | | userName?: string; |
| | | deptId?: number; |
| | | deptName?: string; |
| | | phone?: string; |
| | | date?: string; |
| | | clockInTime?: string; |
| | | clockOutTime?: string; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '#/packages/effects/request/src'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace HrmEmployeeContractApi { |
| | | /** åå·¥åå */ |
| | | export interface EmployeeContract { |
| | | id?: number; |
| | | /** ç»ç¾ä¸ä¸ä»½ååID */ |
| | | parentId?: number; |
| | | /** ç»ç¾ä¸ä¸ä»½ååç¼å· */ |
| | | parentNo?: string; |
| | | /** æ¯å¦å½åçæåå */ |
| | | isCurrent?: boolean; |
| | | /** ååç¼å·ï¼å端èªå¨çæï¼ */ |
| | | contractNo?: string; |
| | | /** åå·¥ID */ |
| | | employeeId?: number; |
| | | /** åå·¥å§å */ |
| | | employeeName?: string; |
| | | /** åå·¥å·¥å· */ |
| | | employeeNo?: string; |
| | | /** é¨é¨åç§° */ |
| | | deptName?: string; |
| | | /** ååç±»åï¼1-å³å¨åå 2-å³å¡åå 3-å®ä¹ åè®® 4-å³å¡æ´¾é£ 5-å
¶ä» */ |
| | | contractType?: number; |
| | | /** ååæéç±»åï¼1-åºå®æé 2-æ åºå®æé 3-以宿ä¸å®å·¥ä½ä»»å¡ä¸ºæé */ |
| | | contractTermType?: number; |
| | | /** ç¾çº¦ä¸»ä½ */ |
| | | signCompany?: string; |
| | | /** ç¾è®¢æ¥æ */ |
| | | signDate?: string; |
| | | /** ååå¼å§æ¥æ */ |
| | | startDate?: string; |
| | | /** ååç»ææ¥æ */ |
| | | endDate?: string; |
| | | /** è¯ç¨æå¼å§æ¥æ */ |
| | | probationStartDate?: string; |
| | | /** è¯ç¨æç»ææ¥æ */ |
| | | probationEndDate?: string; |
| | | /** è¯ç¨æå·¥èµ */ |
| | | probationSalary?: number; |
| | | /** 转æ£å·¥èµ */ |
| | | regularSalary?: number; |
| | | /** è§£é¤ç»æ¢ç¶æï¼0-æ£å¸¸ 1-å·²è§£é¤ 2-å·²ç»æ¢ */ |
| | | terminateStatus?: number; |
| | | /** è§£é¤/ç»æ¢æ¥æ */ |
| | | terminateDate?: string; |
| | | /** è§£é¤/ç»æ¢åå */ |
| | | terminateReason?: string; |
| | | /** ååç¶æï¼å¨æè®¡ç®ï¼ï¼1-å¾
çæ 2-çæä¸ 3-å³å°å°æ 4-已尿 5-å·²è§£é¤ 6-å·²ç»æ¢ */ |
| | | status?: number; |
| | | /** éä»¶ */ |
| | | attachmentList?: AttachmentItem[]; |
| | | /** éä»¶ blobId å表 */ |
| | | blobIds?: number[]; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | /** é件项 */ |
| | | export interface AttachmentItem { |
| | | id: number; |
| | | name?: string; |
| | | url?: string; |
| | | uid?: string; |
| | | } |
| | | |
| | | /** åå·¥ååæ¥è¯¢åæ° */ |
| | | export interface EmployeeContractPageParam extends PageParam { |
| | | contractNo?: string; |
| | | employeeId?: number; |
| | | contractType?: number; |
| | | contractTermType?: number; |
| | | terminateStatus?: number; |
| | | status?: number; |
| | | /** å°æç±»åï¼1-å³å°å°æ 2-已尿 */ |
| | | expiryType?: number; |
| | | } |
| | | |
| | | /** è§£é¤/ç»æ¢ååè¯·æ± */ |
| | | export interface TerminateReq { |
| | | id: number; |
| | | terminateStatus: number; |
| | | terminateDate: string; |
| | | terminateReason: string; |
| | | } |
| | | |
| | | /** åå导å
¥ç»æ */ |
| | | export interface ImportResult { |
| | | /** æ°å¢æåçåå·¥åæ°ç» */ |
| | | createNames?: string[]; |
| | | /** è¦çæ´æ°æåçåå·¥åæ°ç» */ |
| | | updateNames?: string[]; |
| | | /** 导å
¥å¤±è´¥éåï¼key 为åå·¥åï¼value 为失败åå */ |
| | | failureNames?: Record<string, string>; |
| | | } |
| | | } |
| | | |
| | | /** è·ååå·¥ååå页å表 */ |
| | | export function getEmployeeContractPage( |
| | | params: HrmEmployeeContractApi.EmployeeContractPageParam, |
| | | ) { |
| | | return requestClient.get<PageResult<HrmEmployeeContractApi.EmployeeContract>>( |
| | | '/hrm/employee-contract/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** è·ååå·¥åå详æ
*/ |
| | | export function getEmployeeContract(id: number) { |
| | | return requestClient.get<HrmEmployeeContractApi.EmployeeContract>( |
| | | `/hrm/employee-contract/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** å建åå·¥åå */ |
| | | export function createEmployeeContract(data: HrmEmployeeContractApi.EmployeeContract) { |
| | | return requestClient.post('/hrm/employee-contract/create', data); |
| | | } |
| | | |
| | | /** æ´æ°åå·¥åå */ |
| | | export function updateEmployeeContract(data: HrmEmployeeContractApi.EmployeeContract) { |
| | | return requestClient.put('/hrm/employee-contract/update', data); |
| | | } |
| | | |
| | | /** è§£é¤/ç»æ¢åå·¥åå */ |
| | | export function terminateEmployeeContract(data: HrmEmployeeContractApi.TerminateReq) { |
| | | return requestClient.put('/hrm/employee-contract/terminate', data); |
| | | } |
| | | |
| | | /** å é¤åå·¥åå */ |
| | | export function deleteEmployeeContract(id: number) { |
| | | return requestClient.delete(`/hrm/employee-contract/delete?id=${id}`); |
| | | } |
| | | |
| | | /** è·åå³å°å°æååæ°é */ |
| | | export function getExpiringContractCount() { |
| | | return requestClient.get<number>('/hrm/employee-contract/expiring-count'); |
| | | } |
| | | |
| | | /** ä¸è½½åå·¥åå导å
¥æ¨¡æ¿ */ |
| | | export function importEmployeeContractTemplate() { |
| | | return requestClient.download('/hrm/employee-contract/import-template'); |
| | | } |
| | | |
| | | /** 导å
¥åå·¥åå */ |
| | | export function importEmployeeContract( |
| | | file: File, |
| | | updateSupport: boolean, |
| | | ) { |
| | | return requestClient.upload<HrmEmployeeContractApi.ImportResult>( |
| | | '/hrm/employee-contract/import', |
| | | { file, updateSupport }, |
| | | ); |
| | | } |
| | |
| | | userId?: number; |
| | | deptId?: number; |
| | | deptName?: string; |
| | | postId?: number; |
| | | postName?: string; |
| | | postIds?: number[]; |
| | | postNames?: string; |
| | | employeeNo?: string; |
| | | name?: string; |
| | | gender?: number; |
| | |
| | | phone?: string; |
| | | email?: string; |
| | | idCard?: string; |
| | | nation?: string; |
| | | marriageStatus?: string; |
| | | age?: number; |
| | | education?: string; |
| | | politicalStatus?: string; |
| | | hireDate?: string; |
| | | regularDate?: string; |
| | | leaveDate?: string; |
| | | employeeStatus?: number; |
| | | annualLeaveBalance?: number; |
| | | sickLeaveBalance?: number; |
| | | salaryStructureId?: number; |
| | | baseSalary?: number; |
| | | roleIds?: number[]; |
| | | roleNames?: string[]; |
| | |
| | | deptName?: string; |
| | | postName?: string; |
| | | } |
| | | |
| | | /** å工导å
¥ç»æ */ |
| | | export interface ImportResult { |
| | | /** å建æåçåå·¥åæ°ç» */ |
| | | createNames?: string[]; |
| | | /** æ´æ°æåçåå·¥åæ°ç» */ |
| | | updateNames?: string[]; |
| | | /** 导å
¥å¤±è´¥éåï¼key 为åå·¥åï¼value 为失败åå */ |
| | | failureNames?: Record<string, string>; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢åå·¥å页å表 */ |
| | |
| | | return requestClient.download('/hrm/employee/export-excel', { params }); |
| | | } |
| | | |
| | | /** ä¸è½½å工导å
¥æ¨¡æ¿ */ |
| | | export function importEmployeeTemplate() { |
| | | return requestClient.download('/hrm/employee/import-template'); |
| | | } |
| | | |
| | | /** 导å
¥åå·¥ */ |
| | | export function importEmployee(file: File, updateSupport: boolean) { |
| | | return requestClient.upload<HrmEmployeeApi.ImportResult>( |
| | | '/hrm/employee/import', |
| | | { file, updateSupport }, |
| | | ); |
| | | } |
| | | |
| | | /** å 餿è²ç»å */ |
| | | export function deleteEducation(id: number) { |
| | | return requestClient.delete(`/hrm/employee/education/delete?id=${id}`); |
| | |
| | | /** å é¤ç´§æ¥è系人 */ |
| | | export function deleteEmergencyContact(id: number) { |
| | | return requestClient.delete(`/hrm/employee/emergency-contact/delete?id=${id}`); |
| | | } |
| | | } |
| | |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢è¯·åç±»åé
ç½® */ |
| | | export function createLeaveTypeConfig(data: HrmLeaveTypeConfigApi.LeaveTypeConfig) { |
| | | return requestClient.post<number>('/hrm/leave-type-config/create', data); |
| | | } |
| | | |
| | | /** æ´æ°è¯·åç±»åé
ç½® */ |
| | | export function updateLeaveTypeConfig(data: HrmLeaveTypeConfigApi.LeaveTypeConfig) { |
| | | return requestClient.put('/hrm/leave-type-config/update', data); |
| | | } |
| | | |
| | | /** å é¤è¯·åç±»åé
ç½® */ |
| | | export function deleteLeaveTypeConfig(id: number) { |
| | | return requestClient.delete(`/hrm/leave-type-config/delete?id=${id}`); |
| | | } |
| | |
| | | /** 请åç³è¯· */ |
| | | export interface Leave { |
| | | id?: number; |
| | | employeeId?: number; |
| | | employeeName?: string; |
| | | userId?: number; |
| | | userName?: string; |
| | | deptName?: string; |
| | | leaveType?: number; |
| | | startTime?: string; |
| | |
| | | processInstanceId?: string; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | /** å®¡æ¹æµç¨ */ |
| | | export interface ApproveProcess { |
| | | key: string; |
| | | name: string; |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | /** æäº¤è¯·åç³è¯·ï¼å起审æ¹ï¼ */ |
| | | export function submitLeave(id: number) { |
| | | return requestClient.post(`/hrm/leave/submit?id=${id}`); |
| | | export function submitLeave(id: number, processDefinitionKey: string) { |
| | | return requestClient.post('/hrm/leave/submit', null, { |
| | | params: { id, processDefinitionKey }, |
| | | }); |
| | | } |
| | | |
| | | /** æ¤é请åç³è¯· */ |
| | |
| | | |
| | | /** è·åå¯ç¨æµç¨å®ä¹å表 */ |
| | | export function getLeaveApproveProcessDefinitionList() { |
| | | return requestClient.get<any[]>('/hrm/leave/approve-process-definition-list'); |
| | | return requestClient.get<HrmLeaveApi.ApproveProcess[]>( |
| | | '/hrm/leave/approve-process-definition-list', |
| | | ); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '#/packages/effects/request/src'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace HrmOvertimeApi { |
| | | /** å çç³è¯· */ |
| | | export interface Overtime { |
| | | id?: number; |
| | | employeeId?: number; |
| | | employeeName?: string; |
| | | employeeNo?: string; |
| | | deptName?: string; |
| | | postName?: string; |
| | | overtimeDate?: string; |
| | | startTime?: string; |
| | | endTime?: string; |
| | | duration?: number; |
| | | overtimeType?: number; |
| | | crossDay?: boolean; |
| | | mealDeduction?: boolean; |
| | | mealDeductionHours?: number; |
| | | reason?: string; |
| | | relatedType?: number; |
| | | relatedCode?: string; |
| | | workContent?: string; |
| | | status?: number; |
| | | processInstanceId?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | export interface ApproveProcess { |
| | | key: string; |
| | | name: string; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢å çç³è¯·å页å表 */ |
| | | export function getOvertimePage(params: PageParam) { |
| | | return requestClient.get<PageResult<HrmOvertimeApi.Overtime>>( |
| | | '/hrm/overtime/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢å çç³è¯·è¯¦æ
*/ |
| | | export function getOvertime(id: number) { |
| | | return requestClient.get<HrmOvertimeApi.Overtime>( |
| | | `/hrm/overtime/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** è·åå½åç³è¯·äººå
³èçåå·¥æ¡£æ¡ */ |
| | | export function getCurrentOvertimeApplicant() { |
| | | return requestClient.get<HrmOvertimeApi.Overtime>('/hrm/overtime/current-applicant'); |
| | | } |
| | | |
| | | /** æ°å¢å çç³è¯·ï¼èç¨¿ï¼ */ |
| | | export function createOvertime(data: HrmOvertimeApi.Overtime) { |
| | | return requestClient.post('/hrm/overtime/create', data); |
| | | } |
| | | |
| | | /** æ´æ°å çç³è¯· */ |
| | | export function updateOvertime(data: HrmOvertimeApi.Overtime) { |
| | | return requestClient.put('/hrm/overtime/update', data); |
| | | } |
| | | |
| | | /** å é¤å çç³è¯· */ |
| | | export function deleteOvertime(id: number) { |
| | | return requestClient.delete(`/hrm/overtime/delete?id=${id}`); |
| | | } |
| | | |
| | | /** æäº¤å çç³è¯·ï¼å起审æ¹ï¼ */ |
| | | export function submitOvertime(id: number, processDefinitionKey: string) { |
| | | return requestClient.post('/hrm/overtime/submit', null, { |
| | | params: { id, processDefinitionKey }, |
| | | }); |
| | | } |
| | | |
| | | /** æ¤éå çç³è¯· */ |
| | | export function cancelOvertime(id: number) { |
| | | return requestClient.post(`/hrm/overtime/cancel?id=${id}`); |
| | | } |
| | | |
| | | /** è·ååååå
¬ä¸çå¯ç¨å çå®¡æ¹æµç¨ */ |
| | | export function getOvertimeApproveProcessDefinitionList() { |
| | | return requestClient.get<HrmOvertimeApi.ApproveProcess[]>( |
| | | '/hrm/overtime/approve-process-definition-list', |
| | | ); |
| | | } |
| | |
| | | actualSalary?: number; |
| | | workDays?: number; |
| | | overtimeHours?: number; |
| | | /** æçæ»å·¥æ¶ï¼å°æ¶ï¼ */ |
| | | totalWorkHours?: number; |
| | | status?: number; |
| | | statusName?: string; |
| | | remark?: string; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace HrmSalaryStructureApi { |
| | | export interface SalaryStructure { |
| | | id?: number; |
| | | name?: string; |
| | | overtimePayRatio?: number; |
| | | restDayOvertimePayRatio?: number; |
| | | legalHolidayOvertimePayRatio?: number; |
| | | status?: number; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | export interface SalaryStructureQueryParam { |
| | | name?: string; |
| | | status?: number; |
| | | } |
| | | } |
| | | |
| | | export function getSalaryStructureList( |
| | | params?: HrmSalaryStructureApi.SalaryStructureQueryParam, |
| | | ) { |
| | | return requestClient.get<HrmSalaryStructureApi.SalaryStructure[]>( |
| | | '/hrm/salary/structure/list', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | export function getSalaryStructure(id: number) { |
| | | return requestClient.get<HrmSalaryStructureApi.SalaryStructure>( |
| | | `/hrm/salary/structure/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | export function createSalaryStructure( |
| | | data: HrmSalaryStructureApi.SalaryStructure, |
| | | ) { |
| | | return requestClient.post('/hrm/salary/structure/create', data); |
| | | } |
| | | |
| | | export function updateSalaryStructure( |
| | | data: HrmSalaryStructureApi.SalaryStructure, |
| | | ) { |
| | | return requestClient.put('/hrm/salary/structure/update', data); |
| | | } |
| | | |
| | | export function deleteSalaryStructure(id: number) { |
| | | return requestClient.delete(`/hrm/salary/structure/delete?id=${id}`); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace HrmEmployeeScheduleApi { |
| | | /** æçè®°å½ */ |
| | | export interface EmployeeSchedule { |
| | | id?: number; |
| | | employeeId?: number; |
| | | employeeName?: string; |
| | | employeeNo?: string; |
| | | /** æçæ¥æ yyyy-MM-dd */ |
| | | date?: string; |
| | | shiftId?: number; |
| | | shiftName?: string; |
| | | shiftColor?: string; |
| | | remark?: string; |
| | | } |
| | | |
| | | /** ä¿åé¡¹ï¼æ´æå
¨éæäº¤ï¼shiftId ä¸ºç©ºè¡¨ç¤ºä¼æ¯/æ¸
é¤ï¼ */ |
| | | export interface ScheduleItem { |
| | | employeeId: number; |
| | | /** æçæ¥æ yyyy-MM-dd */ |
| | | date: string; |
| | | shiftId?: number | null; |
| | | remark?: string; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢æææçå表 */ |
| | | export function getEmployeeScheduleList(params: { |
| | | yearMonth: string; |
| | | deptId?: number; |
| | | employeeId?: number; |
| | | }) { |
| | | return requestClient.get<HrmEmployeeScheduleApi.EmployeeSchedule[]>( |
| | | '/hrm/employee-schedule/list', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¹éä¿åæçï¼æ´æè¦çå¼ï¼æç次 upsertï¼æ çæ¬¡å é¤ï¼ */ |
| | | export function saveEmployeeSchedule(data: HrmEmployeeScheduleApi.ScheduleItem[]) { |
| | | return requestClient.post('/hrm/employee-schedule/save', data); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '#/packages/effects/request/src'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace HrmShiftApi { |
| | | /** çæ¬¡å®ä¹ */ |
| | | export interface Shift { |
| | | id?: number; |
| | | name?: string; |
| | | /** ä¸çæ¶é´ HH:mm:ss */ |
| | | startTime?: string; |
| | | /** ä¸çæ¶é´ HH:mm:ss */ |
| | | endTime?: string; |
| | | /** æ åå·¥æ¶ï¼å°æ¶ï¼ */ |
| | | workHours?: number; |
| | | /** æ¯å¦è·¨å¤© */ |
| | | crossDay?: boolean; |
| | | /** æ ç¾é¢è²ï¼åå
è¿å¶ï¼ */ |
| | | color?: string; |
| | | /** ç¶æï¼0-å¯ç¨ï¼1-ç¦ç¨ */ |
| | | status?: number; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢ç次å页å表 */ |
| | | export function getShiftPage(params: PageParam) { |
| | | return requestClient.get<PageResult<HrmShiftApi.Shift>>('/hrm/shift/page', { |
| | | params, |
| | | }); |
| | | } |
| | | |
| | | /** æ¥è¯¢ç次å表ï¼å¯ç¨ç¶æï¼ä¾æçé¡µéæ©ï¼ */ |
| | | export function getShiftList() { |
| | | return requestClient.get<HrmShiftApi.Shift[]>('/hrm/shift/list'); |
| | | } |
| | | |
| | | /** æ¥è¯¢ç次详æ
*/ |
| | | export function getShift(id: number) { |
| | | return requestClient.get<HrmShiftApi.Shift>(`/hrm/shift/get?id=${id}`); |
| | | } |
| | | |
| | | /** æ°å¢ç次 */ |
| | | export function createShift(data: HrmShiftApi.Shift) { |
| | | return requestClient.post('/hrm/shift/create', data); |
| | | } |
| | | |
| | | /** æ´æ°ç次 */ |
| | | export function updateShift(data: HrmShiftApi.Shift) { |
| | | return requestClient.put('/hrm/shift/update', data); |
| | | } |
| | | |
| | | /** å é¤ç次 */ |
| | | export function deleteShift(id: number) { |
| | | return requestClient.delete(`/hrm/shift/delete?id=${id}`); |
| | | } |
| | | |
| | | /** 导åºç次 Excel */ |
| | | export function exportShift(params: any) { |
| | | return requestClient.download('/hrm/shift/export-excel', { params }); |
| | | } |
| | |
| | | return requestClient.put('/mdm/item/update-status', { id, status }); |
| | | } |
| | | |
| | | /** æ´æ°ç©æåæ¥ç¶æ */ |
| | | export function updateItemSync(id: number, sync: boolean) { |
| | | return requestClient.put('/mdm/item/update-sync', { id, sync }); |
| | | } |
| | | |
| | | /** 导åºç©æ */ |
| | | export function exportItem(params: Partial<MdmItemApi.Item>) { |
| | | return requestClient.download('/mdm/item/export-excel', { params }); |
| | | } |
| | | |
| | | /** ä¸è½½ç©æå¯¼å
¥æ¨¡æ¿ */ |
| | | export function importItemTemplate() { |
| | | return requestClient.download('/mdm/item/get-import-template'); |
| | | } |
| | | |
| | | /** 导å
¥ç©æ */ |
| | | export function importItem(file: File, updateSupport: boolean) { |
| | | return requestClient.upload('/mdm/item/import', { |
| | | file, |
| | | updateSupport, |
| | | }); |
| | | } |
| | |
| | | export function deleteUnit(id: number) { |
| | | return requestClient.delete(`/mdm/unit/delete?id=${id}`); |
| | | } |
| | | |
| | | /** 导åºè®¡éåä½ */ |
| | | export function exportUnit(params: Record<string, any>) { |
| | | return requestClient.download('/mdm/unit/export-excel', { params }); |
| | | } |
| | | |
| | | /** ä¸è½½è®¡éåä½å¯¼å
¥æ¨¡æ¿ */ |
| | | export function importUnitTemplate() { |
| | | return requestClient.download('/mdm/unit/get-import-template'); |
| | | } |
| | | |
| | | /** 导å
¥è®¡éåä½ */ |
| | | export function importUnit(file: File, updateSupport: boolean) { |
| | | return requestClient.upload('/mdm/unit/import', { |
| | | file, |
| | | updateSupport, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace MesDvMeteringApi { |
| | | /** MES 计éå¨å
· */ |
| | | export interface Metering { |
| | | id?: number; // 主é®ID |
| | | code?: string; // 计éå¨å
·ç¼ç |
| | | name?: string; // 计éå¨å
·åç§° |
| | | specification?: string; // è§æ ¼åå· |
| | | brand?: string; // åç/ç产åå®¶ |
| | | category?: number; // å¨å
·ç±»å«ï¼åå
¸ï¼ |
| | | precision?: string; // 精度 |
| | | range?: string; // éç¨ |
| | | deptId?: number; // 使ç¨é¨é¨ç¼å· |
| | | deptName?: string; // 使ç¨é¨é¨åç§° |
| | | chargeUserId?: number; // 责任人ç¼å· |
| | | chargeUserNickname?: string; // 责任人æµç§° |
| | | checkCycleCount?: number; // æ ¡åå¨æï¼æï¼ |
| | | useDate?: string; // æå
¥ä½¿ç¨æ¥æ |
| | | lastCheckDate?: string; // 䏿¬¡æ£æµæ¥æ |
| | | nextCheckDate?: string; // 䏿¬¡æ£æµæ¥æï¼å°ææ¥ï¼ |
| | | status?: number; // ç¶æï¼åå
¸ï¼ |
| | | expireStatus?: number; // å°æç¶æï¼1 å·²è¿æ / 2 å³å°å°æ / 3 æ£å¸¸ |
| | | expireDays?: number; // è·å°æå¤©æ° |
| | | remark?: string; // 夿³¨ |
| | | createTime?: Date; // å建æ¶é´ |
| | | } |
| | | } |
| | | |
| | | /** 计éå¨å
·å¯¼å
¥ç»æ */ |
| | | export interface ImportResult { |
| | | /** å建æåç计éå¨å
·ç¼ç */ |
| | | createList?: string[]; |
| | | /** æ´æ°æåç计éå¨å
·ç¼ç */ |
| | | updateList?: string[]; |
| | | /** 导å
¥å¤±è´¥éåï¼key 为计éå¨å
·ç¼ç æè¡å·ï¼value 为失败åå */ |
| | | failureList?: Record<string, string>; |
| | | } |
| | | |
| | | /** æ¥è¯¢è®¡éå¨å
·å页 */ |
| | | export function getMeteringPage(params: PageParam) { |
| | | return requestClient.get<PageResult<MesDvMeteringApi.Metering>>( |
| | | '/mes/dv/metering/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢è®¡éå¨å
·è¯¦æ
*/ |
| | | export function getMetering(id: number) { |
| | | return requestClient.get<MesDvMeteringApi.Metering>( |
| | | `/mes/dv/metering/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢è®¡éå¨å
· */ |
| | | export function createMetering(data: MesDvMeteringApi.Metering) { |
| | | return requestClient.post<number>('/mes/dv/metering/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹è®¡éå¨å
· */ |
| | | export function updateMetering(data: MesDvMeteringApi.Metering) { |
| | | return requestClient.put('/mes/dv/metering/update', data); |
| | | } |
| | | |
| | | /** å é¤è®¡éå¨å
· */ |
| | | export function deleteMetering(id: number) { |
| | | return requestClient.delete(`/mes/dv/metering/delete?id=${id}`); |
| | | } |
| | | |
| | | /** 导åºè®¡éå¨å
· */ |
| | | export function exportMetering(params: any) { |
| | | return requestClient.download('/mes/dv/metering/export-excel', { params }); |
| | | } |
| | | |
| | | /** æ¥è¯¢è®¡éå¨å
·ç²¾ç®å表ï¼ä¸æéé¡¹ï¼ */ |
| | | export function getMeteringSimpleList() { |
| | | return requestClient.get<MesDvMeteringApi.Metering[]>( |
| | | '/mes/dv/metering/simple-list', |
| | | ); |
| | | } |
| | | |
| | | /** ä¸è½½è®¡éå¨å
·å¯¼å
¥æ¨¡æ¿ */ |
| | | export function importMeteringTemplate() { |
| | | return requestClient.download('/mes/dv/metering/get-import-template'); |
| | | } |
| | | |
| | | /** 导å
¥è®¡éå¨å
· */ |
| | | export function importMetering(file: File) { |
| | | return requestClient.upload<ImportResult>('/mes/dv/metering/import', { file }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace MesDvMeteringCheckApi { |
| | | /** MES è®¡éæ£æµè®°å½ */ |
| | | export interface MeteringCheck { |
| | | id?: number; // 主é®ID |
| | | meteringId?: number; // 计éå¨å
·ID |
| | | meteringCode?: string; // 计éå¨å
·ç¼ç |
| | | meteringName?: string; // 计éå¨å
·åç§° |
| | | checkCode?: string; // æ£æµç¼å· |
| | | checkDate?: string; // æ£æµæ¥æ |
| | | checkOrg?: string; // æ£æµæºæ |
| | | checkResult?: number; // æ£æµç»è®ºï¼åå
¸ï¼ |
| | | certNo?: string; // è¯ä¹¦ç¼å· |
| | | checkUserId?: number; // æ£æµäººç¼å· |
| | | checkUserNickname?: string; // æ£æµäººæµç§° |
| | | nextCheckDate?: string; // 䏿¬¡æ£æµæ¥æ |
| | | remark?: string; // 夿³¨ |
| | | createTime?: Date; // å建æ¶é´ |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢è®¡éæ£æµè®°å½å页 */ |
| | | export function getMeteringCheckPage(params: PageParam) { |
| | | return requestClient.get<PageResult<MesDvMeteringCheckApi.MeteringCheck>>( |
| | | '/mes/dv/metering-check/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢è®¡éæ£æµè®°å½è¯¦æ
*/ |
| | | export function getMeteringCheck(id: number) { |
| | | return requestClient.get<MesDvMeteringCheckApi.MeteringCheck>( |
| | | `/mes/dv/metering-check/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢è®¡éæ£æµè®°å½ */ |
| | | export function createMeteringCheck(data: MesDvMeteringCheckApi.MeteringCheck) { |
| | | return requestClient.post<number>('/mes/dv/metering-check/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹è®¡éæ£æµè®°å½ */ |
| | | export function updateMeteringCheck(data: MesDvMeteringCheckApi.MeteringCheck) { |
| | | return requestClient.put('/mes/dv/metering-check/update', data); |
| | | } |
| | | |
| | | /** å é¤è®¡éæ£æµè®°å½ */ |
| | | export function deleteMeteringCheck(id: number) { |
| | | return requestClient.delete(`/mes/dv/metering-check/delete?id=${id}`); |
| | | } |
| | | |
| | | /** 导åºè®¡éæ£æµè®°å½ */ |
| | | export function exportMeteringCheck(params: any) { |
| | | return requestClient.download('/mes/dv/metering-check/export-excel', { |
| | | params, |
| | | }); |
| | | } |
| | |
| | | return requestClient.put('/mes/md/item/update', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹ç©æäº§åç¶æ */ |
| | | export function updateItemStatus(id: number, status: number) { |
| | | return requestClient.put('/mes/md/item/update-status', undefined, { |
| | | params: { id, status }, |
| | | }); |
| | | } |
| | | |
| | | /** å é¤ç©æäº§å */ |
| | | export function deleteItem(id: number) { |
| | | return requestClient.delete(`/mes/md/item/delete?id=${id}`); |
| | |
| | | export function deleteItemType(id: number) { |
| | | return requestClient.delete(`/mes/md/item-type/delete?id=${id}`); |
| | | } |
| | | |
| | | /** 导åºç©æäº§ååç±» */ |
| | | export function exportItemType(params: Record<string, any>) { |
| | | return requestClient.download('/mes/md/item-type/export-excel', { params }); |
| | | } |
| | | |
| | | /** ä¸è½½ç©æäº§åå类导å
¥æ¨¡æ¿ */ |
| | | export function importItemTypeTemplate() { |
| | | return requestClient.download('/mes/md/item-type/get-import-template'); |
| | | } |
| | | |
| | | /** 导å
¥ç©æäº§ååç±» */ |
| | | export function importItemType(file: File, updateSupport: boolean) { |
| | | return requestClient.upload('/mes/md/item-type/import', { |
| | | file, |
| | | updateSupport, |
| | | }); |
| | | } |
| | |
| | | import { requestClient } from '#/api/request'; |
| | | import type { SystemUserApi } from '#/api/system/user'; |
| | | |
| | | export namespace MesMdWorkstationWorkerApi { |
| | | /** MES å·¥ä½ç«äººåèµæº */ |
| | |
| | | ); |
| | | } |
| | | |
| | | /** è·å¾å·¥ä½ç«å¯¹åºå²ä½çåå·¥å表 */ |
| | | export function getUserListByWorkstationId(params: { workstationId: number }) { |
| | | if (!params || !params.workstationId) { |
| | | return Promise.resolve([]); |
| | | } |
| | | return requestClient.get<SystemUserApi.UserSimple[]>( |
| | | '/mes/md-workstation-worker/list-user-by-workstation', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢å·¥ä½ç«äººåèµæº */ |
| | | export function createWorkstationWorker( |
| | | data: MesMdWorkstationWorkerApi.WorkstationWorker, |
| | |
| | | documentVersion?: string; |
| | | /** æä»¶å */ |
| | | fileName?: string; |
| | | /** æä»¶å°å */ |
| | | fileUrl?: string; |
| | | /** ä¸ä¼ 人ï¼ç¨æ·IDï¼ */ |
| | | uploadUserId?: number; |
| | | /** ä¸ä¼ 人åç§° */ |
| | |
| | | remark?: string; |
| | | /** å建æ¶é´ */ |
| | | createTime?: number; |
| | | /** BPM æµç¨å®ä¾ç¼å· */ |
| | | processInstanceId?: string; |
| | | /** å¾
å®¡æ ¸æä»¶æ°ï¼auditStatus=0 çå®¡æ ¸è®°å½æ°ï¼ */ |
| | | pendingAuditCount?: number; |
| | | } |
| | | |
| | | export interface PageParams extends PageParam { |
| | |
| | | }); |
| | | } |
| | | |
| | | /** å®¡æ ¸ï¼å¾
å®¡æ ¸âå¾
彿¡£ï¼ */ |
| | | export function auditPdProject(id: number) { |
| | | return requestClient.put('/mes/pd/project/audit', undefined, { |
| | | params: { id }, |
| | | /** æäº¤å®¡æ¹ï¼å¾
å®¡æ ¸â审æ¹ä¸ï¼ */ |
| | | export function submitPdProjectApproval(id: number, processDefinitionKey: string) { |
| | | return requestClient.put('/mes/pd/project/submit-approval', undefined, { |
| | | params: { id, processDefinitionKey }, |
| | | }); |
| | | } |
| | | |
| | | /** è·å¾å®¡æ¹æµç¨å表 */ |
| | | export function getPdProjectApproveProcessList() { |
| | | return requestClient.get<Array<{ id: string; name: string; key: string }>>( |
| | | '/mes/pd/project/approve-process-list', |
| | | ); |
| | | } |
| | | |
| | | /** 彿¡£ï¼å¾
彿¡£â已彿¡£ï¼ï¼å端èªå¨åå»ºå½æ¡£å°è´¦è®°å½ */ |
| | | export function archivePdProject(id: number) { |
| | | return requestClient.put('/mes/pd/project/archive', undefined, { |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace MesProFeedbackApi { |
| | | export interface FeedbackSubmitReqVO { |
| | | id: number; |
| | | produceDate?: string; |
| | | expireDate?: string; |
| | | receiptDate?: string; |
| | | vendorId?: number; |
| | | clientId?: number; |
| | | purchaseOrderCode?: string; |
| | | salesOrderCode?: string; |
| | | toolId?: number; |
| | | moldId?: number; |
| | | lotNumber?: string; |
| | | qualityStatus?: number; |
| | | } |
| | | |
| | | /** MES ç产æ¥å·¥ */ |
| | | export interface Feedback { |
| | | id?: number; |
| | |
| | | remark?: string; // 夿³¨ |
| | | creator?: string; // å建人 |
| | | createTime?: number; // å建æ¶é´ |
| | | processInstanceId?: string; // BPM æµç¨å®ä¾ç¼å· |
| | | processDefinitionKey?: string; // BPM æµç¨å®ä¹ Key |
| | | paramValues?: ParamValue[]; |
| | | } |
| | | |
| | | export interface ParamValue { |
| | | detailId: number; |
| | | value?: string; |
| | | // ---- 忾忮µï¼æ¥å·¥è¯¦æ
/å表è¿åï¼ ---- |
| | | paramCode?: string; |
| | | paramName?: string; |
| | | paramType?: number; |
| | | unitMeasureName?: string; |
| | | paramValue?: string; |
| | | required?: boolean; |
| | | } |
| | | |
| | | /** MES ç产æ¥å·¥å页æ¥è¯¢åæ° */ |
| | |
| | | } |
| | | |
| | | /** æäº¤ç产æ¥å·¥ */ |
| | | export function submitFeedback(id: number) { |
| | | return requestClient.put(`/mes/pro/feedback/submit?id=${id}`); |
| | | export function submitFeedback(data: MesProFeedbackApi.FeedbackSubmitReqVO) { |
| | | return requestClient.put(`/mes/pro/feedback/submit`, data); |
| | | } |
| | | |
| | | /** 驳åç产æ¥å·¥ */ |
| | |
| | | /** å¿«éæ¥å·¥è¯·æ± */ |
| | | export interface QuickCreateReq { |
| | | taskId: number; |
| | | paramValues?: MesProFeedbackApi.ParamValue[]; |
| | | feedbackQuantity: number; |
| | | qualifiedQuantity?: number; |
| | | unqualifiedQuantity?: number; |
| | | remark?: string; |
| | | } |
| | | |
| | |
| | | feedbackId?: number; |
| | | taskStatus?: number; |
| | | workOrderStatus?: number; |
| | | /** æ¥å·¥åå©ä½å¯æ¥å·¥æ°é */ |
| | | reportableQuantity?: number; |
| | | } |
| | | |
| | | /** æ¹éæ¥å·¥è¯·æ± */ |
| | | export interface BatchCreateReq { |
| | | taskIds: number[]; |
| | | feedbackQuantity: number; |
| | | qualifiedQuantity?: number; |
| | | unqualifiedQuantity?: number; |
| | | remark?: string; |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | /** å é¤ MPS */ |
| | | export function deleteMps(ids: number[]) { |
| | | export function deleteMps(id: number) { |
| | | return requestClient.delete('/mes/pro-mps/delete', { |
| | | params: { ids: ids.join(',') }, |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace MesProProcessParamApi { |
| | | /** å·¥åº-å·¥èºåæ°ç»å® */ |
| | | export interface ProcessParam { |
| | | id?: number; |
| | | processId?: number; |
| | | processParamId?: number; |
| | | paramCode?: string; |
| | | paramName?: string; |
| | | paramType?: number; |
| | | unitMeasureId?: number; |
| | | unitMeasureName?: string; |
| | | required?: boolean; |
| | | sort?: number; |
| | | remark?: string; |
| | | } |
| | | |
| | | /** ä¿åç»å®é¡¹ */ |
| | | export interface SaveItem { |
| | | processParamId: number; |
| | | required?: boolean; |
| | | sort?: number; |
| | | remark?: string; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢å·¥åºç»å®çå·¥èºåæ°å表 */ |
| | | export function getProcessParamListByProcess(processId: number) { |
| | | return requestClient.get<MesProProcessParamApi.ProcessParam[]>( |
| | | '/mes/pro/process-param/list-by-process', |
| | | { params: { processId } }, |
| | | ); |
| | | } |
| | | |
| | | /** å
¨éè¦çä¿åå·¥åºç»å®çå·¥èºåæ° */ |
| | | export function updateProcessParams( |
| | | processId: number, |
| | | items: MesProProcessParamApi.SaveItem[], |
| | | ) { |
| | | return requestClient.post('/mes/pro/process-param/update-bindings', { |
| | | processId, |
| | | items, |
| | | }); |
| | | } |
| | | |
| | | /** æ¥è¯¢æå·¥èºåæ°è¢«åªäºå·¥åºç»å® */ |
| | | export function getProcessListByParam(paramId: number) { |
| | | return requestClient.get< |
| | | { id: number; code?: string; name?: string; attention?: string }[] |
| | | >('/mes/pro/process-param/list-by-param', { params: { paramId } }); |
| | | } |
| | |
| | | id?: number; |
| | | routeId?: number; |
| | | processId?: number; |
| | | paramTemplateId?: number; |
| | | processCode?: string; |
| | | processName?: string; |
| | | sort?: number; |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace MesProTaskApi { |
| | | export interface PauseRecord { |
| | | id: number; |
| | | pauseTime: string; |
| | | pauseUserId: number; |
| | | pauseUserName: string; |
| | | reason: string; |
| | | remark?: string; |
| | | resumeTime?: string; |
| | | resumeUserId?: number; |
| | | resumeUserName?: string; |
| | | taskId: number; |
| | | } |
| | | |
| | | /** MES çäº§ä»»å¡ */ |
| | | export interface Task { |
| | | id?: number; |
| | |
| | | workstationName?: string; // å·¥ä½ç«åç§° |
| | | routeId?: number; // å·¥èºè·¯çº¿ç¼å· |
| | | processId?: number; // å·¥åºç¼å· |
| | | paramTemplateId?: number; // å·¥èºåæ°æ¨¡æ¿ç¼å· |
| | | processName?: string; // å·¥åºåç§° |
| | | itemId?: number; // 产åç©æç¼å· |
| | | itemCode?: string; // 产åç¼ç |
| | |
| | | checkFlag?: boolean; // æ¯å¦è´¨æ£ï¼æ´¾çèªå·¥èºè·¯çº¿å·¥åºï¼ |
| | | backflushFlag?: boolean; // æ¯å¦åå²ï¼æ´¾çèªå·¥èºè·¯çº¿å·¥åºï¼ |
| | | remark?: string; // 夿³¨ |
| | | pauseRecords?: PauseRecord[]; // åå·¥è®°å½ |
| | | reportableQuantity?: number; // 坿¥å·¥æ°é |
| | | assigneeUserIds?: number[]; // æ¥å·¥äºº |
| | | feedbackApprove?: boolean; // æ¯å¦æ¥å·¥å®¡æ¹ï¼æ´¾çèªå·¥åï¼ |
| | | } |
| | | |
| | | /** MES ç产任å¡å页æ¥è¯¢åæ° */ |
| | |
| | | { params: { workstationId, quantity } }, |
| | | ); |
| | | } |
| | | |
| | | /** ç产任å¡å工请æ±åæ° */ |
| | | export interface MesProTaskPauseReqVO { |
| | | id: number; |
| | | reason: string; |
| | | remark?: string; |
| | | } |
| | | |
| | | /** ç产任å¡åå·¥ */ |
| | | export function pauseTask(data: MesProTaskPauseReqVO) { |
| | | return requestClient.put('/mes/pro/task/pause', data); |
| | | } |
| | | |
| | | /** ç产任å¡å¤å·¥ */ |
| | | export function resumeTask(data: { id: number }) { |
| | | return requestClient.put('/mes/pro/task/resume', data); |
| | | } |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | |
| | | finishDate?: number; // 宿æ¶é´ |
| | | cancelDate?: number; // åæ¶æ¶é´ |
| | | status?: number; // 订åç¶æ |
| | | scheduleStatus?: number; // æäº§ç¶æ |
| | | productionStatus?: number; // çäº§ç¶æ |
| | | processProgressList?: ProcessProgress[]; // å·¥åºç产è¿åº¦ |
| | | completionProgress?: number; // 宿è¿åº¦ |
| | | remark?: string; // 夿³¨ |
| | | createTime?: number; // å建æ¶é´ |
| | | feedbackApprove?: boolean; // æ¯å¦æ¥å·¥å®¡æ¹ |
| | | processDefinitionKey?: string; // æ¥å·¥å®¡æ¹æµç¨å®ä¹ Key |
| | | feedbackRole?: string; // æ¥å·¥è§è² |
| | | } |
| | | |
| | | /** MES ç产订åå页æ¥è¯¢åæ° */ |
| | |
| | | return requestClient.get<PageResult<MesProWorkOrderApi.WorkOrder>>( |
| | | '/mes/pro/work-order/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** è·å¾ç产工åè°åº¦ç¶æå页 */ |
| | | export function getWorkOrderSchedulePage(params: MesProWorkOrderApi.PageParams) { |
| | | return requestClient.get<PageResult<MesProWorkOrderApi.WorkOrder>>( |
| | | '/mes/pro/work-order/schedule-page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | |
| | | `/mes/pro/work-order/check-auto-finish/${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** è·å¾ç产订åå®¡æ¹æµç¨å表 */ |
| | | export function getWorkOrderApproveProcessList() { |
| | | return requestClient.get<Array<{ id: string; name: string; key: string }>>( |
| | | '/mes/pro/work-order/approve-process-list', |
| | | ); |
| | | } |
| | |
| | | processId?: number; // å·¥åºç¼å· |
| | | processCode?: string; // å·¥åºç¼ç |
| | | processName?: string; // å·¥åºåç§° |
| | | paramTemplateId?: number; // å·¥èºåæ°æ¨¡æ¿ç¼å· |
| | | sort?: number; // åºå· |
| | | nextProcessId?: number; // ä¸ä¸éå·¥åºç¼å· |
| | | nextProcessName?: string; // ä¸ä¸éå·¥åºåç§° |
| | |
| | | unitMeasureId?: number; |
| | | unitMeasureName?: string; |
| | | paramValue?: string; |
| | | required?: boolean; |
| | | remark?: string; |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | /** è´¨æ£æ¹æ¡éé¡¹ï¼æ¹æ¡-产åå
³èï¼ */ |
| | | export namespace MesQcTemplateItemOptionApi { |
| | | export interface Option { |
| | | id?: number; // 产åå
³èç¼å· |
| | | templateId?: number; // è´¨æ£æ¹æ¡ ID |
| | | templateCode?: string; // è´¨æ£æ¹æ¡ç¼å· |
| | | templateName?: string; // è´¨æ£æ¹æ¡åç§° |
| | | quantityCheck?: number; // æä½æ£æµæ° |
| | | quantityUnqualified?: number; // æå¤§ä¸åæ ¼æ° |
| | | criticalRate?: number; // æå¤§è´å½ç¼ºé·çï¼%ï¼ |
| | | majorRate?: number; // æå¤§ä¸¥é缺é·çï¼%ï¼ |
| | | minorRate?: number; // æå¤§è½»å¾®ç¼ºé·çï¼%ï¼ |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢äº§åå
³èå页 */ |
| | | export function getTemplateItemPage( |
| | | params: PageParam & { templateId?: number }, |
| | |
| | | export function deleteTemplateItem(id: number) { |
| | | return requestClient.delete(`/mes/qc/template/item/delete?id=${id}`); |
| | | } |
| | | |
| | | /** æ ¹æ®ç©ææå·¥å/ä»»å¡åæ£æµç§ç±»è·åå¯ç¨è´¨æ£æ¹æ¡å表 */ |
| | | export function getTemplateItemOptionList(params: { |
| | | itemId?: number; |
| | | qcType: number; |
| | | taskId?: number; |
| | | workOrderId?: number; |
| | | }) { |
| | | return requestClient.get<MesQcTemplateItemOptionApi.Option[]>( |
| | | '/mes/qc/template/item/list-by-item-and-type', |
| | | { params }, |
| | | ); |
| | | } |
| | |
| | | expireDate?: string[]; // è¿ææ¥æ |
| | | receiptDate?: string[]; // å
¥åºæ¥æ |
| | | } |
| | | |
| | | /** MES æ¹æ¬¡éè´æ¥æºï¼éè´å
¥åºå + IQCï¼ */ |
| | | export interface PurchaseSource { |
| | | receiptCode?: string; // éè´å
¥åºåå· |
| | | receiptDate?: Date; // å
¥åºæ¥æ |
| | | purchaseOrderCode?: string; // éè´è®¢åç¼å· |
| | | vendorId?: number; // ä¾åºå ID |
| | | vendorCode?: string; // ä¾åºåç¼ç |
| | | vendorName?: string; // ä¾åºååç§° |
| | | iqcCode?: string; // IQC æ£éªåå· |
| | | checkResult?: number; // IQC æ£éªç»è®ºï¼åå
¸ mes_qc_check_resultï¼ |
| | | itemId?: number; // ç©æ ID |
| | | itemCode?: string; // ç©æç¼ç |
| | | itemName?: string; // ç©æåç§° |
| | | batchCode?: string; // æ¹æ¬¡å· |
| | | receivedQuantity?: number; // å
¥åºæ°é |
| | | } |
| | | |
| | | /** MES æ¹æ¬¡éå®å»åï¼éå®åºåºå + OQCï¼ */ |
| | | export interface SalesTarget { |
| | | salesCode?: string; // éå®åºåºåå· |
| | | salesDate?: Date; // åºåºæ¥æ |
| | | salesOrderCode?: string; // éå®è®¢åç¼å· |
| | | clientId?: number; // å®¢æ· ID |
| | | clientCode?: string; // 客æ·ç¼ç |
| | | clientName?: string; // 客æ·åç§° |
| | | oqcCode?: string; // OQC æ£éªåå· |
| | | checkResult?: number; // OQC æ£éªç»è®ºï¼åå
¸ mes_qc_check_resultï¼ |
| | | itemId?: number; // ç©æ ID |
| | | itemCode?: string; // ç©æç¼ç |
| | | itemName?: string; // ç©æåç§° |
| | | batchCode?: string; // æ¹æ¬¡å· |
| | | quantity?: number; // åºåºæ°é |
| | | qualityStatus?: number; // è´¨éç¶æï¼åå
¸ mes_wm_quality_statusï¼ |
| | | } |
| | | |
| | | /** MES æ¹æ¬¡å·¥åºæµè½¬ï¼äº§åº/æå
¥äºä»¶ï¼ */ |
| | | export interface ProcessTrace { |
| | | eventType?: 'OUT' | 'IN'; // äºä»¶ç±»åï¼OUT-äº§åº / IN-æå
¥ |
| | | workOrderId?: number; // ç产工å ID |
| | | workOrderCode?: string; // ç产工åç¼å· |
| | | processId?: number; // å·¥åº ID |
| | | processCode?: string; // å·¥åºç¼ç |
| | | processName?: string; // å·¥åºåç§° |
| | | batchCode?: string; // æ¹æ¬¡å· |
| | | quantity?: number; // æ°é |
| | | qualityStatus?: number; // è´¨éç¶æï¼åå
¸ mes_wm_quality_statusï¼ï¼ä»
产åºä¾§æå¼ |
| | | eventTime?: Date; // äºä»¶æ¶é´ |
| | | processSort?: number; // å·¥åºåºå· |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢æ¹æ¬¡è¯¦æ
*/ |
| | |
| | | { params: { code } }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¹æ¬¡éè´æ¥æºï¼éè´å
¥åºå + IQCï¼ */ |
| | | export function getPurchaseSourceList(code: string) { |
| | | return requestClient.get<MesWmBatchApi.PurchaseSource[]>( |
| | | '/mes/wm/batch/purchase-sources', |
| | | { params: { code } }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¹æ¬¡éå®å»åï¼éå®åºåºå + OQCï¼ */ |
| | | export function getSalesTargetList(code: string) { |
| | | return requestClient.get<MesWmBatchApi.SalesTarget[]>( |
| | | '/mes/wm/batch/sales-targets', |
| | | { params: { code } }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¹æ¬¡å·¥åºæµè½¬ï¼äº§åº/æå
¥äºä»¶ï¼ */ |
| | | export function getProcessTraceList(code: string) { |
| | | return requestClient.get<MesWmBatchApi.ProcessTrace[]>( |
| | | '/mes/wm/batch/process-trace', |
| | | { params: { code } }, |
| | | ); |
| | | } |
| | | |
| | | /** æ ¹æ® SN ç åæ¥æ¹æ¬¡ */ |
| | | export function getBatchBySn(sn: string) { |
| | | return requestClient.get<MesWmBatchApi.Batch | null>('/mes/wm/batch/get-by-sn', { |
| | | params: { sn }, |
| | | }); |
| | | } |
| | |
| | | }); |
| | | } |
| | | |
| | | /** åºåç°æé导å
¥ç»æ */ |
| | | export interface ImportResult { |
| | | /** æ°å»ºåºåè®°å½æåçè¡ï¼å
ç´ æ ¼å¼ãç©æç¼ç -ä»åºã */ |
| | | createNames?: string[]; |
| | | /** å·²åå¨åºåè®°å½è°æ´æåçè¡ï¼å
ç´ æ ¼å¼ãç©æç¼ç -ä»åºã */ |
| | | updateNames?: string[]; |
| | | /** 导å
¥å¤±è´¥çè®°å½ï¼key 为ãç©æç¼ç -ä»åºãï¼value 为失败åå */ |
| | | failureNames?: Record<string, string>; |
| | | } |
| | | |
| | | /** ä¸è½½åºåç°æé导å
¥æ¨¡æ¿ */ |
| | | export function importMaterialStockTemplate() { |
| | | return requestClient.download('/mes/wm/material-stock/import-template'); |
| | | } |
| | | |
| | | /** 导å
¥åºåç°æé */ |
| | | export function importMaterialStock(file: File) { |
| | | return requestClient.upload<MesWmMaterialStockApi.ImportResult>( |
| | | '/mes/wm/material-stock/import', |
| | | { file }, |
| | | ); |
| | | } |
| | | |
| | | /** åºåè°æ´ï¼èªå®ä¹åºå
¥åºï¼- å
¥åº */ |
| | | export interface MaterialStockAdjustInParams { |
| | | itemId: number; // ç©æID |
| | |
| | | batchId?: number; // æ¹æ¬¡ç¼å· |
| | | remark?: string; // 夿³¨ |
| | | } |
| | | |
| | | /** 颿åºåºåå¯é¢ç©æï¼å½åå·¥å BOM ç©æï¼ */ |
| | | export interface BomItem { |
| | | itemId?: number; // ç©æç¼å· |
| | | itemCode?: string; // ç©æç¼ç |
| | | itemName?: string; // ç©æåç§° |
| | | itemSpecification?: string; // è§æ ¼åå· |
| | | unitMeasureName?: string; // åä½åç§° |
| | | requiredQuantity?: number; // å·¥å BOM éæ±é |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢é¢æåºåºåè¡å页 */ |
| | |
| | | export function deleteProductIssueLine(id: number) { |
| | | return requestClient.delete(`/mes/wm/product-issue-line/delete?id=${id}`); |
| | | } |
| | | |
| | | /** æ¥è¯¢é¢æåºåºåå¯é¢ç©æå表ï¼å½åå·¥å BOM ç©æï¼ */ |
| | | export function getBomItemList(issueId: number) { |
| | | return requestClient.get<MesWmProductIssueLineApi.BomItem[]>( |
| | | '/mes/wm/product-issue-line/bom-item-list', |
| | | { params: { issueId } }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢å¯éæç颿åè¡åè¡¨ï¼æç产订åè¿æ»¤ï¼ä»
è¿å已宿颿çè®°å½ï¼ */ |
| | | export function getReturnableIssueLines(workOrderId: number) { |
| | | return requestClient.get<MesWmProductIssueLineApi.ProductIssueLine[]>( |
| | | '/mes/wm/product-issue-line/returnable-list', |
| | | { params: { workOrderId } }, |
| | | ); |
| | | } |
| | |
| | | export interface ReturnIssueLine { |
| | | id?: number; // è¡ç¼å· |
| | | issueId?: number; // éæåç¼å· |
| | | issueLineId?: number; // 颿åè¡ç¼å·ï¼å
³èæ¥æºé¢æè®°å½ï¼ |
| | | itemId?: number; // ç©æç¼å· |
| | | itemCode?: string; // ç©æç¼ç |
| | | itemName?: string; // ç©æåç§° |
| | |
| | | vendorCode?: string; // ä¾åºåç¼ç |
| | | vendorName?: string; // ä¾åºååç§° |
| | | vendorNickname?: string; // ä¾åºåç®ç§° |
| | | returnDate?: number; // éè´§æ¥æ |
| | | returnDate?: number | string; // éè´§æ¥æ |
| | | returnReason?: string; // éè´§åå |
| | | transportCode?: string; // è¿åå· |
| | | transportTelephone?: string; // èç³»çµè¯ |
| | |
| | | return requestClient.get<SrmSupplierApi.SupplierVO[]>('/srm/supplier/simple-list'); |
| | | } |
| | | |
| | | /** è·åä¾åºåæ¡£æ¡å表ï¼ä¸è¿æ»¤åå
¥ç¶æï¼ */ |
| | | export function getSupplierList() { |
| | | return requestClient.get<SrmSupplierApi.SupplierVO[]>('/srm/supplier/list'); |
| | | } |
| | | |
| | | /** è·åä¾åºå详æ
*/ |
| | | export function getSupplier(id: number) { |
| | | return requestClient.get<SrmSupplierApi.SupplierVO>(`/srm/supplier/get?id=${id}`); |
| | |
| | | export function deleteSupplier(id: number) { |
| | | return requestClient.delete(`/srm/supplier/delete?id=${id}`); |
| | | } |
| | | |
| | | /** 导åºä¾åºåæ¡£æ¡ Excelï¼æå表æ¥è¯¢æ¡ä»¶ï¼ */ |
| | | export function exportSupplierExcel(params: any) { |
| | | return requestClient.download('/srm/supplier/export-excel', { params }); |
| | | } |
| | | |
| | | /** ä¸è½½ä¾åºåæ¡£æ¡å¯¼å
¥æ¨¡æ¿ */ |
| | | export function getSupplierImportTemplate() { |
| | | return requestClient.download('/srm/supplier/get-import-template'); |
| | | } |
| | | |
| | | /** 导å
¥ä¾åºåæ¡£æ¡ Excel */ |
| | | export function importSupplierExcel(data: { |
| | | file: File; |
| | | updateSupport: boolean; |
| | | }) { |
| | | return requestClient.upload('/srm/supplier/import-excel', data); |
| | | } |
| | |
| | | applyNo?: string; |
| | | supplierId?: number; |
| | | supplierName: string; |
| | | contactName?: string; |
| | | contactMobile?: string; |
| | | contactEmail?: string; |
| | | supplierType?: string; |
| | | companyNature?: string; |
| | | registeredCapital?: number; |
| | | businessLicenseNo?: string; |
| | | taxNo?: string; |
| | | address?: string; |
| | | bankName?: string; |
| | | bankAccount?: string; |
| | | taxNo?: string; |
| | | businessLicense?: string; |
| | | taxCertificate?: string; |
| | | orgCodeCertificate?: string; |
| | | contactName?: string; |
| | | contactMobile?: string; |
| | | contactEmail?: string; |
| | | applyUserName?: string; |
| | | applyTime?: string; |
| | | applyStatus?: number; |
| | | applyStatusLabel?: string; |
| | | auditComment?: string; |
| | | processInstanceId?: string; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | export interface ReviewVO { |
| | | id: number; |
| | | passed: boolean; |
| | | comment?: string; |
| | | /** åååå
¬å®¡æ¹æµç¨å®ä¹ */ |
| | | export interface ProcessDefinition { |
| | | id: string; |
| | | key: string; |
| | | name: string; |
| | | } |
| | | } |
| | | |
| | |
| | | return requestClient.delete(`/srm/supplier-apply/delete?id=${id}`); |
| | | } |
| | | |
| | | /** æäº¤å®¡æ ¸ */ |
| | | export function submitApply(id: number) { |
| | | return requestClient.put(`/srm/supplier-apply/submit?id=${id}`); |
| | | /** è·åå®¡æ¹æµç¨å表ï¼åååå
¬å·²å叿µç¨ï¼ */ |
| | | export function getApproveProcessList() { |
| | | return requestClient.get<SrmSupplierApplyApi.ProcessDefinition[]>( |
| | | '/srm/supplier-apply/approve-process-list', |
| | | ); |
| | | } |
| | | |
| | | /** éè´å®¡æ ¸ */ |
| | | export function reviewPurchase(data: SrmSupplierApplyApi.ReviewVO) { |
| | | return requestClient.put('/srm/supplier-apply/review-purchase', data); |
| | | } |
| | | |
| | | /** è´¨éå®¡æ ¸ */ |
| | | export function reviewQuality(data: SrmSupplierApplyApi.ReviewVO) { |
| | | return requestClient.put('/srm/supplier-apply/review-quality', data); |
| | | } |
| | | |
| | | /** è´¢å¡å®¡æ ¸ */ |
| | | export function reviewFinance(data: SrmSupplierApplyApi.ReviewVO) { |
| | | return requestClient.put('/srm/supplier-apply/review-finance', data); |
| | | /** æäº¤åå
¥ç³è¯·å®¡æ¹ï¼å¯å¨åååå
¬æµç¨ï¼ */ |
| | | export function submitApply(id: number, processDefinitionKey: string) { |
| | | return requestClient.put('/srm/supplier-apply/submit', null, { |
| | | params: { id, processDefinitionKey }, |
| | | }); |
| | | } |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import type { SystemStorageApi } from '#/api/system/storage'; |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace SrmSupplierCertificateApi { |
| | |
| | | status?: number; |
| | | remark?: string; |
| | | createTime?: string; |
| | | blobIds?: number[]; |
| | | attachmentList?: SystemStorageApi.StorageBlob[]; |
| | | } |
| | | } |
| | | |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import type { SystemStorageApi } from '#/api/system/storage'; |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace SrmTenderApi { |
| | |
| | | purchaseOrderNo?: string; |
| | | biddingMode?: string; |
| | | createTime?: string; |
| | | blobIds?: number[]; |
| | | attachmentList?: SystemStorageApi.StorageBlob[]; |
| | | } |
| | | |
| | | export interface TenderMaterialVO { |
| | |
| | | id: number; |
| | | tenderDesc?: string; |
| | | qualificationRequirements?: string; |
| | | blobIds?: number[]; |
| | | } |
| | | } |
| | | |
| | |
| | | return requestClient.put(`/srm/tender-bid/withdraw?id=${id}`); |
| | | } |
| | | |
| | | export function updateBidTotal(id: number, bidTotalAmount?: number | null) { |
| | | const params = bidTotalAmount == null ? '' : `&bidTotalAmount=${bidTotalAmount}`; |
| | | return requestClient.put(`/srm/tender-bid/update-total?id=${id}${params}`); |
| | | } |
| | | |
| | | // ========== æ¥ä»·ç®¡ç ========== |
| | | |
| | | export function getQuoteList(bidId: number) { |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace SystemCompanyConfigApi { |
| | | /** å
¬å¸åçä¿¡æ¯ */ |
| | | export interface CompanyConfig { |
| | | id?: number; |
| | | companyName?: string; |
| | | logoUrl?: string; |
| | | logoBlobId?: number; |
| | | loginTitle?: string; |
| | | loginDescription?: string; |
| | | } |
| | | } |
| | | |
| | | /** è·å¾å
¬å¸åçä¿¡æ¯ï¼å
ç»å½å
¬å¼ï¼ç»å½é¡µ / åºç¨å¯å¨æ¶ä½¿ç¨ï¼ */ |
| | | export async function getCompanyConfig() { |
| | | return requestClient.get<SystemCompanyConfigApi.CompanyConfig | null>( |
| | | '/system/company-config/get', |
| | | ); |
| | | } |
| | | |
| | | /** ä¿åå
¬å¸åçä¿¡æ¯ */ |
| | | export async function saveCompanyConfig( |
| | | data: SystemCompanyConfigApi.CompanyConfig, |
| | | ) { |
| | | return requestClient.post<number>('/system/company-config/save', data); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | function downloadBlob(blob: Blob, headers?: any) { |
| | | let filename = 'backup.sql'; |
| | | if (headers) { |
| | | const disposition = headers['content-disposition'] || headers['Content-Disposition']; |
| | | if (disposition && disposition.indexOf('filename=') !== -1) { |
| | | const matches = disposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/); |
| | | if (matches != null && matches[1]) { |
| | | filename = matches[1].replace(/['"]/g, ''); |
| | | } |
| | | } |
| | | } |
| | | |
| | | const url = window.URL.createObjectURL(blob); |
| | | const link = document.createElement('a'); |
| | | link.style.display = 'none'; |
| | | link.href = url; |
| | | link.setAttribute('download', decodeURIComponent(filename)); |
| | | document.body.append(link); |
| | | link.click(); |
| | | link.remove(); |
| | | window.URL.revokeObjectURL(url); |
| | | } |
| | | |
| | | /** ä¸è½½æ°æ®åºå¤ä»½æä»¶ */ |
| | | export function backupDatabase(onDownloadProgress?: (progressEvent: any) => void) { |
| | | return requestClient.request('/system/database/backup', { |
| | | method: 'GET', |
| | | responseType: 'blob', |
| | | timeout: 300000, // 5åé |
| | | onDownloadProgress, |
| | | responseReturn: 'raw', |
| | | }).then((res: any) => { |
| | | const blob = res.data || res; |
| | | if (blob instanceof Blob) { |
| | | downloadBlob(blob, res.headers); |
| | | } |
| | | return res; |
| | | }).catch((err: any) => { |
| | | if (err instanceof Blob) { |
| | | downloadBlob(err); |
| | | return err; |
| | | } else if (err?.response?.data instanceof Blob) { |
| | | downloadBlob(err.response.data, err.response.headers); |
| | | return err; |
| | | } |
| | | throw err; |
| | | }); |
| | | } |
| | |
| | | export async function deleteDeptList(ids: number[]) { |
| | | return requestClient.delete(`/system/dept/delete-list?ids=${ids.join(',')}`); |
| | | } |
| | | |
| | | /** ä¸è½½é¨é¨å¯¼å
¥æ¨¡æ¿ */ |
| | | export function importDeptTemplate() { |
| | | return requestClient.download('/system/dept/get-import-template'); |
| | | } |
| | | |
| | | /** 导å
¥é¨é¨ */ |
| | | export function importDept(file: File, updateSupport: boolean) { |
| | | return requestClient.upload('/system/dept/import', { |
| | | file, |
| | | updateSupport, |
| | | }); |
| | | } |
| | |
| | | params, |
| | | }); |
| | | } |
| | | |
| | | /** ä¸è½½å²ä½å¯¼å
¥æ¨¡æ¿ */ |
| | | export function importPostTemplate() { |
| | | return requestClient.download('/system/post/get-import-template'); |
| | | } |
| | | |
| | | /** 导å
¥å²ä½ */ |
| | | export function importPost(file: File, updateSupport: boolean) { |
| | | return requestClient.upload('/system/post/import', { |
| | | file, |
| | | updateSupport, |
| | | }); |
| | | } |
| | |
| | | ); |
| | | } |
| | | |
| | | /** å
Œ
±æä»¶ä¸ä¼ ï¼æ°¸ä¹
ææï¼è¿åæ°¸ä¹
å
¬å¼ previewURLï¼ç¨äºç»å½é¡µçå
ç»å½å±ç¤ºçåºæ¯ï¼ */ |
| | | export function publicUploadFile(files: globalThis.File[]) { |
| | | const formData = new FormData(); |
| | | files.forEach((file) => formData.append('files', file)); |
| | | return requestClient.post<SystemStorageApi.StorageBlob[]>( |
| | | '/system/storage-blob/public-upload', |
| | | formData, |
| | | { headers: { 'Content-Type': 'multipart/form-data' } }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢éä»¶å表 */ |
| | | export function listAttachments(params: SystemStorageApi.AttachmentListParams) { |
| | | return requestClient.get<SystemStorageApi.StorageBlob[]>( |
| | |
| | | params: { nickname }, |
| | | }); |
| | | } |
| | | |
| | | /** éè¿è§è²æ è¯è·åç¨æ·ç²¾ç®ä¿¡æ¯å表 */ |
| | | export function getSimpleUserListByRoleCode(code: Object) { |
| | | return requestClient.get<SystemUserApi.UserSimple[]>('/system/user/list-by-role-code', { |
| | | params: code , |
| | | }); |
| | | } |
| | |
| | | import App from './app.vue'; |
| | | import { router } from './router'; |
| | | |
| | | import { getCompanyConfig } from '#/api/system/company-config'; |
| | | import { applyCompanyConfig } from '#/utils/company-config'; |
| | | |
| | | async function bootstrap(namespace: string) { |
| | | // åå§åç»ä»¶éé
å¨ |
| | | await initComponentAdapter(); |
| | |
| | | // é
ç½® pinia-store |
| | | await initStores(app, { namespace }); |
| | | |
| | | // å è½½å
¬å¸åçä¿¡æ¯ï¼ä»å端ï¼å
ç»å½æ¥å£ï¼ï¼èå¨ç»å½é¡µ/èåæ /ç½é¡µæ é¢ã |
| | | // æ³¨ï¼æ¤æ¶ pinia å°æªå¨ç»ä»¶å¤æ¿æ´»ï¼è¿éç¨çº¯å½æ°ï¼ä¸ä¾èµ storeã |
| | | try { |
| | | const companyConfig = await getCompanyConfig(); |
| | | if (companyConfig) { |
| | | applyCompanyConfig(companyConfig); |
| | | } |
| | | } catch { |
| | | // å端ä¸å¯ç¨ææªé
ç½®ï¼ä¿æç¼è¯æé»è®¤ |
| | | } |
| | | |
| | | // å®è£
æéæä»¤ |
| | | registerAccessDirective(app); |
| | | |
| | |
| | | if (preferences.app.dynamicTitle) { |
| | | const routeTitle = router.currentRoute.value.meta?.title; |
| | | const pageTitle = |
| | | (routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name; |
| | | (routeTitle ? `${$t(routeTitle)} - ` : '') + |
| | | preferences.copyright.companyName; |
| | | useTitle(pageTitle); |
| | | } |
| | | }); |
| | |
| | | } |
| | | |
| | | /** è§£ç 表å Conf */ |
| | | export function decodeConf(conf: string) { |
| | | export function decodeConf(conf?: string | null) { |
| | | if (!conf) { |
| | | return {}; |
| | | } |
| | | return formCreate.parseJson(conf); |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | /** è§£ç 表å Fields */ |
| | | export function decodeFields(fields: string[]) { |
| | | export function decodeFields(fields?: string[] | null) { |
| | | const rule: Rule[] = []; |
| | | fields.forEach((item) => { |
| | | (fields || []).forEach((item) => { |
| | | rule.push(formCreate.parseJson(item)); |
| | | }); |
| | | return rule; |
| | |
| | | disabled: false, |
| | | drag: false, |
| | | helpText: '', |
| | | maxSize: 2, |
| | | maxSize: 50, |
| | | maxNumber: 1, |
| | | accept: () => [], |
| | | multiple: false, |
| | |
| | | :progress="{ showInfo: true }" |
| | | :show-upload-list="{ |
| | | showPreviewIcon: true, |
| | | showRemoveIcon: true, |
| | | showRemoveIcon: !disabled, |
| | | showDownloadIcon, |
| | | }" |
| | | @remove="handleRemove" |
| | | @preview="handlePreview" |
| | | @reject="handleExceed" |
| | | > |
| | | <div v-if="drag" class="upload-drag-area"> |
| | | <div v-if="drag && !disabled" class="upload-drag-area"> |
| | | <p class="ant-upload-drag-icon"> |
| | | <IconifyIcon icon="lucide:cloud-upload" /> |
| | | </p> |
| | |
| | | æ¯æ{{ accept.join('/') }}æ ¼å¼æä»¶ï¼ä¸è¶
è¿{{ maxSize }}MB |
| | | </p> |
| | | </div> |
| | | <div v-else-if="fileList && fileList.length < maxNumber"> |
| | | <div |
| | | v-else-if="!disabled && fileList && fileList.length < maxNumber" |
| | | > |
| | | <Button> |
| | | <IconifyIcon icon="lucide:cloud-upload" /> |
| | | {{ $t('ui.upload.upload') }} |
| | |
| | | disabled: false, |
| | | listType: 'picture-card', |
| | | helpText: '', |
| | | maxSize: 2, |
| | | maxSize: 50, |
| | | maxNumber: 1, |
| | | accept: () => defaultImageAccepts, |
| | | multiple: false, |
| | |
| | | ) { |
| | | const formData = new FormData(); |
| | | formData.append('files', file); |
| | | const blobs = await requestClient.post('/system/storage-blob/upload', formData, { |
| | | // 使ç¨å
Œ
±ä¸ä¼ æ¥å£ï¼è¿åæ°¸ä¹
å
¬å¼ previewURL |
| | | const blobs = await requestClient.post('/system/storage-blob/public-upload', formData, { |
| | | onUploadProgress, |
| | | headers: { 'Content-Type': 'multipart/form-data' }, |
| | | }); |
| | | const blob = Array.isArray(blobs) ? blobs[0] : blobs; |
| | | return blob ? { url: blob.url || blob.previewURL, name: blob.name, id: blob.id } : {}; |
| | | // åå¨å®æ´ previewURLï¼å« publicKeyï¼ï¼ç¡®ä¿å¯è®¿é® |
| | | return blob ? { |
| | | url: blob.previewURL || blob.url, |
| | | name: blob.name || blob.originalFilename, |
| | | id: blob.id |
| | | } : {}; |
| | | } |
| | | |
| | | return { |
| | |
| | | <script lang="ts" setup> |
| | | import { computed } from 'vue'; |
| | | import { computed, onMounted } from 'vue'; |
| | | |
| | | import { AuthPageLayout } from '@vben/layouts'; |
| | | import { preferences } from '@vben/preferences'; |
| | | |
| | | import { $t } from '#/locales'; |
| | | import { useCompanyConfigStore } from '#/store/company-config'; |
| | | |
| | | declare const __APP_LOGO__: string; |
| | | |
| | | const appName = computed(() => preferences.app.name); |
| | | const logo = computed(() => __APP_LOGO__ || preferences.logo.source); |
| | | const companyConfigStore = useCompanyConfigStore(); |
| | | |
| | | onMounted(() => { |
| | | // ç»å½é¡µå±ç¤º loginTitleï¼å¯å¨å è½½èµ°çæ¯çº¯å½æ°ï¼ä¸å¡« storeï¼ï¼è¿å
¥ç»å½é¡µæ¶æå䏿¬¡ |
| | | companyConfigStore.loadFromServer(); |
| | | }); |
| | | |
| | | const appName = computed(() => preferences.copyright.companyName); |
| | | // ç»å½é¡µ logo ä¼å
åæ°æ®åºé
ç½®ï¼preferences.logo.sourceï¼ï¼æªé
ç½®æ¶åéç¼è¯æé»è®¤ __APP_LOGO__ |
| | | const logo = computed(() => preferences.logo.source || __APP_LOGO__); |
| | | const logoDark = computed(() => preferences.logo.sourceDark); |
| | | // ç»å½é¡µæ é¢/æè¿°ä½¿ç¨ i18n åºå®ææ¡ï¼ä¸éå
¬å¸é
ç½®åå |
| | | const pageTitle = computed(() => $t('authentication.pageTitle')); |
| | | const pageDescription = computed(() => $t('authentication.pageDesc')); |
| | | </script> |
| | | |
| | | <template> |
| | |
| | | :app-name="appName" |
| | | :logo="logo" |
| | | :logo-dark="logoDark" |
| | | :page-description="$t('authentication.pageDesc')" |
| | | :page-title="$t('authentication.pageTitle')" |
| | | :page-description="pageDescription" |
| | | :page-title="pageTitle" |
| | | > |
| | | <!-- èªå®ä¹å·¥å
·æ --> |
| | | <!-- <template #toolbar></template> --> |
| | |
| | | |
| | | declare const __APP_LOGO__: string; |
| | | declare const __APP_FAVICON__: string; |
| | | declare const __APP_NAME__: string; |
| | | |
| | | /** |
| | | * åºç¨åå§å宿ä¹ååè¿è¡é¡µé¢å 载渲æ |
| | |
| | | if (__APP_LOGO__) { |
| | | preferencesManager.updatePreferences({ logo: { source: __APP_LOGO__ } }); |
| | | } |
| | | // 强å¶è¦çå
¬å¸åç§°ï¼ç»è¿ localStorage ç¼å |
| | | if (__APP_NAME__) { |
| | | preferencesManager.updatePreferences({ |
| | | app: { name: __APP_NAME__ }, |
| | | copyright: { companyName: __APP_NAME__ }, |
| | | }); |
| | | } |
| | | if (__APP_FAVICON__) { |
| | | const link = document.querySelector('link[rel="icon"]'); |
| | | if (link) { |
| | |
| | | CloudUpload, |
| | | Copy, |
| | | CornerDownLeft, |
| | | DatabaseBackup, |
| | | Download, |
| | | Ellipsis, |
| | | Eraser, |
| | |
| | | |
| | | import { TabsTrigger } from 'reka-ui'; |
| | | |
| | | import { cn } from '../../../../../base/shared/src/utils'; |
| | | |
| | | import { Tabs, TabsContent, TabsList } from '../../ui'; |
| | | import TabsIndicator from './tabs-indicator.vue'; |
| | | |
| | | interface Props { |
| | | defaultValue?: string; |
| | | tabs?: SegmentedItem[]; |
| | | /** æ¯å¦å
è®¸æ¨ªåæ»å¨ï¼tab è¾å¤æ¶å¯ç¨ï¼å tab æå
容宽度æåï¼è¶
åºé¨å坿»å¨ */ |
| | | scrollable?: boolean; |
| | | } |
| | | |
| | | const props = withDefaults(defineProps<Props>(), { |
| | | defaultValue: '', |
| | | tabs: () => [], |
| | | scrollable: false, |
| | | }); |
| | | |
| | | const activeTab = defineModel<string>(); |
| | |
| | | }); |
| | | |
| | | const tabsStyle = computed(() => { |
| | | if (props.scrollable) { |
| | | return undefined; |
| | | } |
| | | return { |
| | | 'grid-template-columns': `repeat(${props.tabs.length}, minmax(0, 1fr))`, |
| | | }; |
| | | }); |
| | | |
| | | const tabsIndicatorStyle = computed(() => { |
| | | if (props.scrollable) { |
| | | return undefined; |
| | | } |
| | | return { |
| | | width: `${(100 / props.tabs.length).toFixed(0)}%`, |
| | | }; |
| | |
| | | <Tabs v-model="activeTab" :default-value="getDefaultValue"> |
| | | <TabsList |
| | | :style="tabsStyle" |
| | | class="bg-accent outline-heavy! relative grid w-full outline-2!" |
| | | :class=" |
| | | cn( |
| | | 'bg-accent outline-heavy! relative outline-2!', |
| | | // 坿»å¨æ¨¡å¼ï¼å·¦å¯¹é½ï¼è¦ç reka-ui ç justify-centerï¼å¦åå
容è¶
宽æ¶å·¦ä¾§è¢«æ¤åºä¸æ æ³æ»å¨ï¼ãå
¨å®½ãæ¨ªåæ»å¨ |
| | | scrollable |
| | | ? 'flex! w-full! overflow-x-auto justify-start!' |
| | | : 'grid w-full', |
| | | ) |
| | | " |
| | | > |
| | | <TabsIndicator :style="tabsIndicatorStyle" /> |
| | | <TabsIndicator v-if="!scrollable" :style="tabsIndicatorStyle" /> |
| | | <template v-for="tab in tabs" :key="tab.value"> |
| | | <TabsTrigger |
| | | :value="tab.value" |
| | | :class="activeClass(tab.value)" |
| | | class="hover:text-primary z-20 inline-flex items-center justify-center rounded-md px-3 py-1 text-sm font-medium whitespace-nowrap disabled:pointer-events-none disabled:opacity-50" |
| | | class="hover:text-primary z-20 inline-flex shrink-0 items-center justify-center rounded-md px-2 py-1 text-sm font-medium whitespace-nowrap disabled:pointer-events-none disabled:opacity-50" |
| | | > |
| | | {{ tab.label }} |
| | | </TabsTrigger> |
| | |
| | | PURCHASE_ORDER: 10, |
| | | PURCHASE_IN: 11, |
| | | PURCHASE_RETURN: 12, |
| | | PURCHASE_INVOICE: 13, |
| | | SALE_ORDER: 20, |
| | | SALE_OUT: 21, |
| | | SALE_RETURN: 22, |
| | |
| | | ABNORMAL: 2, |
| | | } as const; |
| | | |
| | | /** MES 计éå¨å
·ç¶ææä¸¾ */ |
| | | export const MesDvMeteringStatusEnum = { |
| | | IN_USE: 1, // å¨ç¨ |
| | | STOPPED: 2, // åç¨ |
| | | SCRAPPED: 3, // æ¥åº |
| | | } as const; |
| | | |
| | | /** MES è®¡éæ£æµç»è®ºæä¸¾ */ |
| | | export const MesDvMeteringCheckResultEnum = { |
| | | QUALIFIED: 1, // åæ ¼ |
| | | UNQUALIFIED: 2, // ä¸åæ ¼ |
| | | } as const; |
| | | |
| | | /** MES 计éå¨å
·å°æç¶ææä¸¾ */ |
| | | export const MesDvMeteringExpireStatusEnum = { |
| | | OVERDUE: 1, // å·²è¿æ |
| | | WITHIN: 2, // å³å°å°æ |
| | | NORMAL: 3, // æ£å¸¸ |
| | | } as const; |
| | | |
| | | /** MES 维修工åç¶ææä¸¾ */ |
| | | export const MesDvRepairStatusEnum = { |
| | | PREPARE: MesOrderStatusConstants.DRAFT, |
| | |
| | | CONFIRMED: 1, // 已确认 |
| | | FINISHED: 2, // 已宿 |
| | | CANCELED: 3, // 已忶 |
| | | } as const; |
| | | |
| | | /** MES ç产工åçäº§ç¶ææä¸¾ï¼å¯¹é½å端 MesProWorkOrderProductionStatusEnumï¼ */ |
| | | export const MesProWorkOrderProductionStatusEnum = { |
| | | WAITING: 0, // å¾
ç产 |
| | | PRODUCING: 1, // çäº§ä¸ |
| | | FINISHED: 2, // çäº§å®æ |
| | | } as const; |
| | | |
| | | /** MES å·¥åç±»åæä¸¾ */ |
| | |
| | | RIGHT: 2, // å³è¡¥é½ |
| | | } as const; |
| | | |
| | | /** MES 产å设计项ç®ç¶ææä¸¾ï¼çº¿æ§æµè½¬ï¼å¾
åå¸âå¾
ä¸ä¼ âå¾
å®¡æ ¸âå¾
彿¡£â已彿¡£ï¼ */ |
| | | /** MES 产å设计项ç®ç¶ææä¸¾ï¼çº¿æ§æµè½¬ï¼å¾
åå¸âå¾
ä¸ä¼ âå¾
å®¡æ ¸â审æ¹ä¸âå¾
彿¡£â已彿¡£ï¼ */ |
| | | export const MesPdProjectStatusEnum = { |
| | | PENDING_PUBLISH: 0, // å¾
åå¸ |
| | | PENDING_UPLOAD: 1, // å¾
ä¸ä¼ |
| | | PENDING_AUDIT: 2, // å¾
å®¡æ ¸ |
| | | APPROVING: 5, // 审æ¹ä¸ |
| | | PENDING_ARCHIVE: 3, // å¾
彿¡£ |
| | | ARCHIVED: 4, // 已彿¡£ |
| | | } as const; |
| | |
| | | return prefix + result; |
| | | } |
| | | |
| | | /** çç¹æ¡ä»¶ç±»åæä¸¾ */ |
| | | /** çç¹æ¡ä»¶ç±»åæä¸¾ï¼ä¸å端 MesBizTypeConstants / åå
¸å¼ä¿æä¸è´ï¼ */ |
| | | export const WlsStockTakingParamTypeEnum = { |
| | | WAREHOUSE: 1, // ä»åº |
| | | LOCATION: 2, // åºåº |
| | | AREA: 3, // åºä½ |
| | | ITEM: 4, // ç©æ |
| | | BATCH: 5, // æ¹æ¬¡ |
| | | QUALITY_STATUS: 6, // è´¨éç¶æ |
| | | WAREHOUSE: 102, // ä»åº |
| | | LOCATION: 103, // åºåº |
| | | AREA: 104, // åºä½ |
| | | ITEM: 600, // ç©æ |
| | | BATCH: 107, // æ¹æ¬¡ |
| | | QUALITY_STATUS: 900, // è´¨éç¶æ |
| | | } as const; |
| | | |
| | | /** çç¹ç±»åæä¸¾ */ |
| | |
| | | CRM_PRODUCT_STATUS: 'crm_product_status', // CRM ååç¶æ |
| | | CRM_PRODUCT_UNIT: 'crm_product_unit', // CRM 产ååä½ |
| | | CRM_RECEIVABLE_RETURN_TYPE: 'crm_receivable_return_type', // CRM 忬¾çè¿æ¬¾æ¹å¼ |
| | | CRM_INVOICE_TYPE: 'crm_invoice_type', // CRM å票类å |
| | | CRM_SALES_TARGET_PERIOD_TYPE: 'crm_sales_target_period_type', // CRM éå®ç®æ å¨æç±»å |
| | | CRM_SALES_TARGET_DIMENSION: 'crm_sales_target_dimension', // CRM éå®ç®æ 维度 |
| | | CRM_SALES_TARGET_STATUS: 'crm_sales_target_status', // CRM éå®ç®æ ç¶æ |
| | | } as const; |
| | | |
| | | /** ========== AFTERSALES - å®åæå¡æ¨¡å ========== */ |
| | |
| | | MES_DV_REPAIR_RESULT: 'mes_dv_repair_result', // MES ç»´ä¿®ç»æ |
| | | MES_DV_CHECK_RECORD_STATUS: 'mes_dv_check_record_status', // MES ç¹æ£è®°å½ç¶æ |
| | | MES_DV_CHECK_RESULT: 'mes_dv_check_result', // MES ç¹æ£ç»æ |
| | | MES_DV_METERING_CATEGORY: 'mes_dv_metering_category', // MES 计éå¨å
·ç±»å« |
| | | MES_DV_METERING_STATUS: 'mes_dv_metering_status', // MES 计éå¨å
·ç¶æ |
| | | MES_DV_METERING_CHECK_RESULT: 'mes_dv_metering_check_result', // MES è®¡éæ£æµç»è®º |
| | | MES_PRO_LINK_TYPE: 'mes_pro_link_type', // MES å·¥åºå
³ç³»ç±»å |
| | | MES_PRO_WORK_ORDER_STATUS: 'mes_pro_work_order_status', // MES ç产工åç¶æ |
| | | ORDER_PRODUCTION_STATUS: 'order_production_status', // ç产订åç¶æ |
| | | MES_PRO_WORK_ORDER_TYPE: 'mes_pro_work_order_type', // MES å·¥åç±»å |
| | | MES_PRO_WORK_ORDER_SOURCE_TYPE: 'mes_pro_work_order_source_type', // MES 工忥æºç±»å |
| | | MES_PRO_TASK_STATUS: 'mes_pro_task_status', // MES ç产任å¡ç¶æ |
| | |
| | | HRM_SALARY_STATUS: 'hrm_salary_status', // HRM èªèµç¶æ |
| | | HRM_HANDOVER_STATUS: 'hrm_handover_status', // HRM 交æ¥ç¶æ |
| | | HRM_PAYMENT_METHOD: 'hrm_payment_method', // HRM åæ¾æ¹å¼ |
| | | HRM_CONTRACT_TYPE: 'hrm_contract_type', // HRM ååç±»å |
| | | HRM_CONTRACT_TERM_TYPE: 'hrm_contract_term_type', // HRM ååæéç±»å |
| | | HRM_CONTRACT_STATUS: 'hrm_contract_status', // HRM ååç¶æ |
| | | HRM_CONTRACT_TERMINATE_STATUS: 'hrm_contract_terminate_status', // HRM ååè§£é¤ç»æ¢ç¶æ |
| | | } as const; |
| | | |
| | | /** åå
¸ç±»åæä¸¾ - ç»ä¸å¯¼åº */ |
| | |
| | | import { VbenFullScreen, VbenIconButton } from '../../../../../@core/ui-kit/shadcn-ui/src'; |
| | | |
| | | import { |
| | | DatabaseBackupButton, |
| | | GlobalSearch, |
| | | LanguageToggle, |
| | | PreferencesButton, |
| | |
| | | } |
| | | // å好设置快æ·åè½ |
| | | if (preferencesButtonPosition.value.header) { |
| | | // æ°æ®åºå¤ä»½æé® |
| | | list.push({ |
| | | index: REFERENCE_VALUE + 5, |
| | | name: 'database-backup', |
| | | }); |
| | | |
| | | list.push({ |
| | | index: REFERENCE_VALUE + 10, |
| | | name: 'preferences', |
| | |
| | | /> |
| | | </template> |
| | | |
| | | <template v-else-if="slot.name === 'database-backup'"> |
| | | <DatabaseBackupButton class="mr-1" /> |
| | | </template> |
| | | <template v-else-if="slot.name === 'preferences'"> |
| | | <PreferencesButton |
| | | class="mr-1" |
| | |
| | | :collapsed="logoCollapsed" |
| | | :src="preferences.logo.source" |
| | | :src-dark="preferences.logo.sourceDark" |
| | | :text="preferences.app.name" |
| | | :text="preferences.copyright.companyName" |
| | | :theme="logoTheme" |
| | | @click="clickLogo" |
| | | > |
| | |
| | | <VbenLogo |
| | | v-if="preferences.logo.enable" |
| | | :fit="preferences.logo.fit" |
| | | :text="preferences.app.name" |
| | | :text="preferences.copyright.companyName" |
| | | :theme="sidebarThemeSub" |
| | | > |
| | | <template v-if="$slots['logo-text']" #text> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import { DatabaseBackup } from '../../../../../icons/src'; |
| | | import { VbenIconButton } from '../../../../../@core/ui-kit/shadcn-ui/src'; |
| | | import { useVbenModal } from '../../../../../@core/ui-kit/popup-ui/src'; |
| | | import { backupDatabase } from '../../../../../../api/system/database'; |
| | | import { ref } from 'vue'; |
| | | |
| | | const isDownloading = ref(false); |
| | | const downloadProgress = ref(0); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | onConfirm: async () => { |
| | | modalApi.close(); |
| | | try { |
| | | isDownloading.value = true; |
| | | downloadProgress.value = 0; |
| | | await backupDatabase((progressEvent) => { |
| | | if (progressEvent.total) { |
| | | downloadProgress.value = Math.round((progressEvent.loaded * 100) / progressEvent.total); |
| | | } |
| | | }); |
| | | } catch (error: any) { |
| | | // Ignore if it's the Blob being thrown by the response interceptor |
| | | if (!(error instanceof Blob) && !(error?.response?.data instanceof Blob)) { |
| | | console.error('Backup failed:', error); |
| | | } |
| | | } finally { |
| | | isDownloading.value = false; |
| | | downloadProgress.value = 0; |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | function handleOpen() { |
| | | modalApi.open(); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="relative"> |
| | | <VbenIconButton |
| | | v-access:code="['system:database:backup']" |
| | | class="hover:animate-[shrink_0.3s_ease-in-out]" |
| | | @click="handleOpen" |
| | | > |
| | | <DatabaseBackup class="size-4 text-foreground" /> |
| | | </VbenIconButton> |
| | | |
| | | <Modal |
| | | title="æç¤º" |
| | | content-class="px-8 min-h-10" |
| | | footer-class="border-none mb-3 mr-3" |
| | | header-class="border-none" |
| | | > |
| | | 确认è¦ä¸è½½æ°æ®åºçå¤ä»½æä»¶åï¼ |
| | | </Modal> |
| | | |
| | | <teleport to="body"> |
| | | <div |
| | | v-if="isDownloading" |
| | | class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 backdrop-blur-sm" |
| | | > |
| | | <div class="flex flex-col items-center space-y-4 rounded-lg bg-background p-6 shadow-lg"> |
| | | <div class="size-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div> |
| | | <p class="text-lg font-medium text-foreground">æ£å¨ä¸è½½å¤ä»½: {{ downloadProgress }}%</p> |
| | | </div> |
| | | </div> |
| | | </teleport> |
| | | </div> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | export { default as DatabaseBackupButton } from './database-backup.vue'; |
| | |
| | | export * from './theme-toggle'; |
| | | export * from './timezone'; |
| | | export * from './user-dropdown'; |
| | | export * from './database-backup'; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup lang="ts"> |
| | | import { message } from 'ant-design-vue'; |
| | | import { Button, Input, Upload } from 'ant-design-vue'; |
| | | import { onMounted, ref } from 'vue'; |
| | | |
| | | // 注æï¼æ¬æä»¶ä½äº @vben ç packages å
é¨ï¼æ æ³ä½¿ç¨åºç¨å±å«å #/ï¼éç¨ç¸å¯¹è·¯å¾ï¼ä¸åç®å½ custom.vue ä¿æä¸è´ï¼ |
| | | import { publicUploadFile } from '../../../../../../../../api/system/storage'; |
| | | import { $t } from '../../../../../../../locales/src'; |
| | | import { useCompanyConfigStore } from '../../../../../../../../store/company-config'; |
| | | |
| | | defineOptions({ |
| | | name: 'PreferenceCompanyConfig', |
| | | }); |
| | | |
| | | const companyConfigStore = useCompanyConfigStore(); |
| | | |
| | | const companyName = ref(companyConfigStore.companyName); |
| | | const loginTitle = ref(companyConfigStore.loginTitle); |
| | | const loginDescription = ref(companyConfigStore.loginDescription); |
| | | const logoUrl = ref(companyConfigStore.logoUrl); |
| | | const uploading = ref(false); |
| | | |
| | | // æå¼åºåæ¶ä»å端æåææ°å
¬å¸é
置并å填表åï¼store åå¼å¯è½ä¸ºç©ºï¼å å¯å¨å è½½èµ°çæ¯çº¯å½æ°ï¼ |
| | | onMounted(async () => { |
| | | await companyConfigStore.loadFromServer(); |
| | | companyName.value = companyConfigStore.companyName; |
| | | loginTitle.value = companyConfigStore.loginTitle; |
| | | loginDescription.value = companyConfigStore.loginDescription; |
| | | logoUrl.value = companyConfigStore.logoUrl; |
| | | }); |
| | | |
| | | /** ä¸ä¼ logoï¼æ°¸ä¹
å
¬å¼ï¼ä¾ç»å½é¡µå
ç»å½å±ç¤ºï¼ */ |
| | | async function handleBeforeUpload(file: File) { |
| | | uploading.value = true; |
| | | try { |
| | | const blobs = await publicUploadFile([file]); |
| | | const blob = blobs?.[0]; |
| | | if (blob?.previewURL || blob?.url) { |
| | | logoUrl.value = blob.previewURL || blob.url || ''; |
| | | message.success($t('preferences.company.uploadSuccess')); |
| | | } else { |
| | | message.error($t('preferences.company.uploadFailed')); |
| | | } |
| | | } catch { |
| | | message.error($t('preferences.company.uploadFailed')); |
| | | } finally { |
| | | uploading.value = false; |
| | | } |
| | | // è¿å falseï¼é»æ¢ç»ä»¶é»è®¤ä¸ä¼ ï¼å·²éè¿æ¥å£ä¸ä¼ ï¼ |
| | | return false; |
| | | } |
| | | |
| | | async function handleSave() { |
| | | if (!companyName.value.trim()) { |
| | | message.warning($t('preferences.company.companyNameRequired')); |
| | | return; |
| | | } |
| | | try { |
| | | await companyConfigStore.save({ |
| | | companyName: companyName.value.trim(), |
| | | logoUrl: logoUrl.value || undefined, |
| | | loginTitle: loginTitle.value, |
| | | loginDescription: loginDescription.value, |
| | | }); |
| | | message.success($t('preferences.company.saveSuccess')); |
| | | } catch { |
| | | message.error($t('preferences.company.saveFailed')); |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="flex flex-col"> |
| | | <!-- å
¬å¸åç§° --> |
| | | <div class="my-2 flex w-full items-center justify-between gap-4 rounded-md px-2 py-2"> |
| | | <span class="shrink-0 text-sm text-foreground/80"> |
| | | {{ $t('preferences.company.companyName') }} |
| | | </span> |
| | | <Input |
| | | v-model:value="companyName" |
| | | :placeholder="$t('preferences.company.companyNamePlaceholder')" |
| | | class="w-41.25" |
| | | allow-clear |
| | | /> |
| | | </div> |
| | | |
| | | <!-- ç»å½é¡µæ é¢ --> |
| | | <div class="my-2 flex w-full items-center justify-between gap-4 rounded-md px-2 py-2"> |
| | | <span class="shrink-0 text-sm text-foreground/80"> |
| | | {{ $t('preferences.company.loginTitle') }} |
| | | </span> |
| | | <Input |
| | | v-model:value="loginTitle" |
| | | :placeholder="$t('preferences.company.loginTitlePlaceholder')" |
| | | class="w-41.25" |
| | | allow-clear |
| | | /> |
| | | </div> |
| | | |
| | | <!-- ç»å½é¡µæè¿° --> |
| | | <div class="my-2 flex w-full items-center justify-between gap-4 rounded-md px-2 py-2"> |
| | | <span class="shrink-0 text-sm text-foreground/80"> |
| | | {{ $t('preferences.company.loginDescription') }} |
| | | </span> |
| | | <Input |
| | | v-model:value="loginDescription" |
| | | :placeholder="$t('preferences.company.loginDescriptionPlaceholder')" |
| | | class="w-41.25" |
| | | allow-clear |
| | | /> |
| | | </div> |
| | | |
| | | <!-- å
¬å¸ Logo --> |
| | | <div class="my-2 flex w-full items-center justify-between gap-4 rounded-md px-2 py-2"> |
| | | <span class="shrink-0 text-sm text-foreground/80"> |
| | | {{ $t('preferences.company.logo') }} |
| | | </span> |
| | | <div class="flex items-center gap-2"> |
| | | <Upload |
| | | :show-upload-list="false" |
| | | accept="image/*" |
| | | :before-upload="handleBeforeUpload" |
| | | > |
| | | <Button size="small" :loading="uploading"> |
| | | {{ $t('preferences.company.uploadLogo') }} |
| | | </Button> |
| | | </Upload> |
| | | <img |
| | | v-if="logoUrl" |
| | | :src="logoUrl" |
| | | alt="company-logo" |
| | | class="h-9 w-9 rounded border border-border object-contain" |
| | | /> |
| | | <span v-else class="text-xs text-muted-foreground"> |
| | | {{ $t('preferences.company.logoEmpty') }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="uploading" class="px-2 pb-1 text-xs text-muted-foreground"> |
| | | {{ $t('preferences.company.uploading') }} |
| | | </div> |
| | | |
| | | <!-- ä¿å --> |
| | | <div class="mt-3 px-2"> |
| | | <Button type="primary" class="w-full" @click="handleSave"> |
| | | {{ $t('preferences.company.save') }} |
| | | </Button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | |
| | | export { default as Block } from './block.vue'; |
| | | export { default as Company } from './company/company.vue'; |
| | | export { default as Custom } from './custom/custom.vue'; |
| | | export { default as Animation } from './general/animation.vue'; |
| | | export { default as General } from './general/general.vue'; |
| | |
| | | Breadcrumb, |
| | | BuiltinTheme, |
| | | ColorMode, |
| | | Company, |
| | | Content, |
| | | Copyright, |
| | | Custom, |
| | |
| | | label: $t('preferences.general'), |
| | | value: 'general', |
| | | }, |
| | | { |
| | | label: $t('preferences.company.title'), |
| | | value: 'company', |
| | | }, |
| | | ]; |
| | | |
| | | if (showCustomTab.value) { |
| | |
| | | <VbenSegmented |
| | | v-model="activeTab" |
| | | :tabs="tabs" |
| | | scrollable |
| | | :class="{ |
| | | 'sticky-tabs-header': appEnableStickyPreferencesNavigationBar, |
| | | }" |
| | |
| | | /> |
| | | </Block> |
| | | </template> |
| | | <template #company> |
| | | <Block :title="$t('preferences.company.title')"> |
| | | <Company /> |
| | | </Block> |
| | | </template> |
| | | <template #custom> |
| | | <Block :title="customTabTitle"> |
| | | <Custom |
| | |
| | | "placeholder": "Please enter the default report title" |
| | | } |
| | | } |
| | | }, |
| | | "company": { |
| | | "title": "Company Info", |
| | | "companyName": "Company Name", |
| | | "companyNamePlaceholder": "Please enter company name", |
| | | "companyNameRequired": "Company name is required", |
| | | "loginTitle": "Login Page Title", |
| | | "loginTitlePlaceholder": "Enter login page title (optional)", |
| | | "loginDescription": "Login Page Description", |
| | | "loginDescriptionPlaceholder": "Enter login page description (optional)", |
| | | "logo": "Company Logo", |
| | | "uploadLogo": "Upload Logo", |
| | | "uploadSuccess": "Logo uploaded successfully", |
| | | "uploadFailed": "Logo upload failed", |
| | | "uploading": "Uploading...", |
| | | "logoEmpty": "No logo set", |
| | | "save": "Save", |
| | | "saveSuccess": "Company info saved successfully", |
| | | "saveFailed": "Failed to save company info" |
| | | } |
| | | } |
| | |
| | | "placeholder": "请è¾å
¥æ¥è¡¨é»è®¤æ é¢" |
| | | } |
| | | } |
| | | }, |
| | | "company": { |
| | | "title": "å
¬å¸ä¿¡æ¯", |
| | | "companyName": "å
¬å¸åç§°", |
| | | "companyNamePlaceholder": "请è¾å
¥å
¬å¸åç§°", |
| | | "companyNameRequired": "å
¬å¸åç§°ä¸è½ä¸ºç©º", |
| | | "loginTitle": "ç»å½é¡µæ é¢", |
| | | "loginTitlePlaceholder": "请è¾å
¥ç»å½é¡µæ é¢ï¼å¯éï¼", |
| | | "loginDescription": "ç»å½é¡µæè¿°", |
| | | "loginDescriptionPlaceholder": "请è¾å
¥ç»å½é¡µæè¿°ï¼å¯éï¼", |
| | | "logo": "å
¬å¸ Logo", |
| | | "uploadLogo": "ä¸ä¼ Logo", |
| | | "uploadSuccess": "Logo ä¸ä¼ æå", |
| | | "uploadFailed": "Logo ä¸ä¼ 失败", |
| | | "uploading": "ä¸ä¼ ä¸...", |
| | | "logoEmpty": "æªè®¾ç½® Logo", |
| | | "save": "ä¿å", |
| | | "saveSuccess": "å
¬å¸ä¿¡æ¯ä¿åæå", |
| | | "saveFailed": "å
¬å¸ä¿¡æ¯ä¿å失败" |
| | | } |
| | | } |
| | |
| | | } from './packages/preferences/src'; |
| | | |
| | | declare const __APP_LOGO__: string; |
| | | declare const __APP_NAME__: string; |
| | | |
| | | interface WebAntdPreferencesExtension { |
| | | defaultTableSize: number; |
| | |
| | | app: { |
| | | /** åç«¯è·¯ç±æ¨¡å¼ */ |
| | | accessMode: 'backend', |
| | | name: import.meta.env.VITE_APP_TITLE, |
| | | name: __APP_NAME__ || import.meta.env.VITE_APP_TITLE, |
| | | enableRefreshToken: true, |
| | | }, |
| | | footer: { |
| | |
| | | fixed: false, |
| | | }, |
| | | copyright: { |
| | | companyName: import.meta.env.VITE_APP_TITLE, |
| | | companyName: __APP_NAME__ || import.meta.env.VITE_APP_TITLE, |
| | | companySiteLink: '#', |
| | | }, |
| | | logo: { |
| | |
| | | if (to.path === LOGIN_PATH && accessStore.accessToken) { |
| | | return decodeURIComponent( |
| | | (to.query?.redirect as string) || |
| | | userStore.userInfo?.homePath || |
| | | preferences.app.defaultHomePath, |
| | | '/analytics', |
| | | ); |
| | | } |
| | | return true; |
| | |
| | | userStore.setUserRoles(userRoles); |
| | | const redirectPath = (from.query.redirect ?? |
| | | (to.path === preferences.app.defaultHomePath |
| | | ? userInfo?.homePath || preferences.app.defaultHomePath |
| | | ? '/analytics' |
| | | : to.fullPath)) as string; |
| | | |
| | | return { |
| | |
| | | }, |
| | | component: () => import('#/views/crm/product/detail/index.vue'), |
| | | }, |
| | | { |
| | | path: 'invoice/detail/:id', |
| | | name: 'CrmInvoiceDetail', |
| | | meta: { |
| | | title: 'å¼ç¥¨è¯¦æ
', |
| | | activePath: '/crm/invoice', |
| | | }, |
| | | component: () => import('#/views/crm/invoice/detail/index.vue'), |
| | | }, |
| | | ], |
| | | }, |
| | | ]; |
| | |
| | | }, |
| | | component: () => import('#/views/mes/pd/project/detail/index.vue'), |
| | | }, |
| | | { |
| | | path: 'pro/feedback/detail/:id', |
| | | name: 'MesProFeedbackDetail', |
| | | meta: { |
| | | title: 'ç产æ¥å·¥è¯¦æ
', |
| | | activePath: '/mes/pro/feedback', |
| | | }, |
| | | props: true, |
| | | component: () => import('#/views/mes/pro/feedback/detail/index.vue'), |
| | | }, |
| | | ], |
| | | }, |
| | | ]; |
| | |
| | | // oxlint-disable-next-line no-unused-expressions |
| | | onSuccess |
| | | ? await onSuccess?.() |
| | | : await router.push( |
| | | userInfo.homePath || preferences.app.defaultHomePath, |
| | | ); |
| | | : await router.push('/analytics'); |
| | | } |
| | | |
| | | if (userInfo?.nickname) { |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { ref } from 'vue'; |
| | | import { defineStore } from 'pinia'; |
| | | |
| | | import { |
| | | getCompanyConfig, |
| | | saveCompanyConfig as saveCompanyConfigApi, |
| | | } from '#/api/system/company-config'; |
| | | import { applyCompanyConfig } from '#/utils/company-config'; |
| | | |
| | | /** |
| | | * å
¬å¸åçä¿¡æ¯å
¨å±é
ç½® |
| | | * |
| | | * å¯å¨æ¶ä»å端å è½½ï¼ä¿åæ¶ååå端ï¼å¹¶å°å
¬å¸å/logo åæ¥å° preferencesï¼ |
| | | * ä»èèå¨ç»å½é¡µãèåæ ãç½é¡µæ é¢çæææ¶è´¹ç¹ã |
| | | */ |
| | | export const useCompanyConfigStore = defineStore('companyConfig', () => { |
| | | const companyName = ref(''); |
| | | const logoUrl = ref(''); |
| | | const loginTitle = ref(''); |
| | | const loginDescription = ref(''); |
| | | |
| | | /** å°å
¬å¸ä¿¡æ¯åºç¨å° preferencesï¼èå¨ç»å½é¡µ/èåæ /æ é¢ */ |
| | | function applyToPreferences() { |
| | | applyCompanyConfig({ |
| | | companyName: companyName.value, |
| | | logoUrl: logoUrl.value, |
| | | }); |
| | | } |
| | | |
| | | /** ä»å端å è½½å
¬å¸é
置并åºç¨ */ |
| | | async function loadFromServer() { |
| | | try { |
| | | const config = await getCompanyConfig(); |
| | | if (config) { |
| | | companyName.value = config.companyName ?? ''; |
| | | logoUrl.value = config.logoUrl ?? ''; |
| | | loginTitle.value = config.loginTitle ?? ''; |
| | | loginDescription.value = config.loginDescription ?? ''; |
| | | applyToPreferences(); |
| | | } |
| | | } catch { |
| | | // å端ä¸å¯ç¨ææªé
ç½®ï¼ä¿æç¼è¯æé»è®¤ |
| | | } |
| | | } |
| | | |
| | | /** ä¿åå
¬å¸é
ç½®å°å端并åºç¨ */ |
| | | async function save(data: { |
| | | companyName: string; |
| | | logoUrl?: string; |
| | | logoBlobId?: number; |
| | | loginTitle?: string; |
| | | loginDescription?: string; |
| | | }) { |
| | | await saveCompanyConfigApi(data); |
| | | companyName.value = data.companyName; |
| | | logoUrl.value = data.logoUrl ?? ''; |
| | | loginTitle.value = data.loginTitle ?? ''; |
| | | loginDescription.value = data.loginDescription ?? ''; |
| | | applyToPreferences(); |
| | | } |
| | | |
| | | return { |
| | | companyName, |
| | | logoUrl, |
| | | loginTitle, |
| | | loginDescription, |
| | | applyToPreferences, |
| | | loadFromServer, |
| | | save, |
| | | }; |
| | | }); |
| | |
| | | export * from './auth'; |
| | | export * from './company-config'; |
| | |
| | | declare const __APP_LOGO__: string; |
| | | declare const __APP_FAVICON__: string; |
| | | declare const __APP_NAME__: string; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { preferencesManager } from '@vben/preferences'; |
| | | |
| | | /** å
¬å¸åçä¿¡æ¯ï¼ä¸å端 system_company_config è¡¨åæ®µå¯¹åºï¼ */ |
| | | export interface CompanyConfigData { |
| | | companyName?: string; |
| | | logoUrl?: string; |
| | | loginTitle?: string; |
| | | loginDescription?: string; |
| | | } |
| | | |
| | | /** |
| | | * å°å
¬å¸åçä¿¡æ¯åºç¨å°å
¨å±ï¼ |
| | | * - å
¬å¸å â preferences.app.name / copyright.companyNameï¼èå¨èåæ ãç»å½é¡µãç½é¡µæ é¢ãçæï¼ |
| | | * - logo â preferences.logo.sourceï¼èå¨ç»å½é¡µãä¾§è¾¹æ logoï¼ |
| | | * - æå
¬å¸åæ¶ï¼è®¾ç½® document.titleï¼åºç¡æ é¢ï¼dynamicTitle å¼å¯æ¶è·¯ç±æ é¢å¨æ¤åºç¡ä¸æ¼æ¥ï¼ |
| | | */ |
| | | export function applyCompanyConfig(config: CompanyConfigData) { |
| | | const updates: Record<string, any> = {}; |
| | | if (config.logoUrl) { |
| | | updates.logo = { source: config.logoUrl }; |
| | | // 忥ç½é¡µæ é¢å¾æ ï¼faviconï¼ä¸ºä¸ä¼ ç logo |
| | | const faviconLink = document.querySelector('link[rel="icon"]'); |
| | | faviconLink?.setAttribute('href', config.logoUrl); |
| | | } |
| | | if (config.companyName) { |
| | | updates.app = { name: config.companyName }; |
| | | updates.copyright = { companyName: config.companyName }; |
| | | } |
| | | if (Object.keys(updates).length > 0) { |
| | | preferencesManager.updatePreferences(updates); |
| | | } |
| | | if (config.companyName) { |
| | | document.title = config.companyName; |
| | | } |
| | | } |
| | |
| | | }); |
| | | return index; |
| | | }; |
| | | |
| | | /** |
| | | * å»æè¡¨æ ¼åçåºå®ï¼fixedï¼é
ç½® |
| | | * |
| | | * ç¨äºãæ¥çå®¡æ¹æç» / 详æ
ãçåªè¯»åºæ¯ï¼æ¤æ¶è¡¨æ ¼å®¹å¨è¾çªï¼ |
| | | * åºå®å伿¤å å¯è§å®½åº¦ï¼æäº§ååç§°ãåä½ã夿³¨çåé®ä½ã |
| | | * |
| | | * @param columns åå§è¡¨æ ¼åé
ç½® |
| | | */ |
| | | export function withoutFixedColumns<T>(columns?: null | T[]): T[] { |
| | | return (columns ?? []).map((column) => { |
| | | const { fixed: _fixed, ...rest } = column as Recordable<any>; |
| | | return rest as unknown as T; |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { AftersalesProblemApi } from '#/api/aftersales/problem'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type ProblemFormType = 'create' | 'update' | 'detail'; |
| | | |
| | | /** é®é¢ç±»åé项ï¼å¯¹é½ AfterSaleIssueTypeEnumï¼ */ |
| | | export const ISSUE_TYPE_OPTIONS = [ |
| | | { label: 'ä¸è¬é®é¢', value: 1 }, |
| | | { label: 'ç»´ä¿®é®é¢', value: 2 }, |
| | | { label: 'éè´§é®é¢', value: 3 }, |
| | | ]; |
| | | |
| | | /** 严éç¨åº¦é项 */ |
| | | export const SEVERITY_LEVEL_OPTIONS = [ |
| | | { label: '轻微', value: 0 }, |
| | | { label: 'ä¸è¬', value: 1 }, |
| | | { label: '严é', value: 2 }, |
| | | { label: 'è´å½', value: 3 }, |
| | | ]; |
| | | |
| | | /** æ ¹å åç±»é项ï¼å¯¹é½ AfterSaleRootCauseCategoryEnumï¼ */ |
| | | export const ROOT_CAUSE_CATEGORY_OPTIONS = [ |
| | | { label: '设计缺é·', value: 1 }, |
| | | { label: 'ç©æç¼ºé·', value: 2 }, |
| | | { label: 'å·¥èºç¼ºé·', value: 3 }, |
| | | { label: 'è£
é
缺é·', value: 4 }, |
| | | { label: 'è¿è¾æå', value: 5 }, |
| | | { label: 'å
è£
缺é·', value: 6 }, |
| | | { label: '使ç¨ä¸å½', value: 7 }, |
| | | { label: '软件缺é·', value: 8 }, |
| | | { label: 'å
¶ä»', value: 9 }, |
| | | ]; |
| | | |
| | | /** é®é¢ç¶æé项ï¼å¯¹é½ AfterSaleProblemStatusEnumï¼ */ |
| | | export const PROBLEM_STATUS_OPTIONS = [ |
| | | { label: 'è稿', value: 0 }, |
| | | { label: 'å·²åå¸', value: 1 }, |
| | | { label: 'å·²åç¨', value: 2 }, |
| | | ]; |
| | | |
| | | /** é®é¢åºè¡¨å */ |
| | | export function useProblemFormSchema( |
| | | formType: ProblemFormType, |
| | | ): VbenFormSchema[] { |
| | | const readonly = formType === 'detail'; |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | | { |
| | | fieldName: 'title', |
| | | label: 'é®é¢æ é¢', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥é®é¢æ é¢' }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'issueType', |
| | | label: 'é®é¢ç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: ISSUE_TYPE_OPTIONS, |
| | | placeholder: 'è¯·éæ©é®é¢ç±»å', |
| | | disabled: readonly, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'issueCategory', |
| | | label: 'é®é¢åç±»', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: 'å¦åè½æ
éãå¤è§ç¼ºé·', |
| | | disabled: readonly, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'severityLevel', |
| | | label: '严éç¨åº¦', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: SEVERITY_LEVEL_OPTIONS, |
| | | placeholder: 'è¯·éæ©', |
| | | disabled: readonly, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'rootCauseCategory', |
| | | label: 'æ ¹å åç±»', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: ROOT_CAUSE_CATEGORY_OPTIONS, |
| | | placeholder: 'è¯·éæ©æ ¹å åç±»', |
| | | disabled: readonly, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'itemCode', |
| | | label: 'ç©æç¼ç ', |
| | | component: 'Input', |
| | | componentProps: { placeholder: 'æ¥æºäºå·¥åæç»', disabled: readonly }, |
| | | }, |
| | | { |
| | | fieldName: 'itemName', |
| | | label: 'ç©æåç§°', |
| | | component: 'Input', |
| | | componentProps: { placeholder: 'æ¥æºäºå·¥åæç»', disabled: readonly }, |
| | | }, |
| | | { |
| | | fieldName: 'batchCode', |
| | | label: 'æ¹æ¬¡å·', |
| | | component: 'Input', |
| | | componentProps: { placeholder: 'æ¥æºäºå·¥åæç»', disabled: readonly }, |
| | | }, |
| | | { |
| | | fieldName: 'description', |
| | | label: 'é®é¢æè¿°', |
| | | component: 'Textarea', |
| | | formItemClass: 'col-span-2', |
| | | componentProps: { |
| | | rows: 3, |
| | | placeholder: 'é®é¢ç°è±¡ä¸èæ¯', |
| | | disabled: readonly, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'rootCause', |
| | | label: 'æ ¹å åæ', |
| | | component: 'Textarea', |
| | | formItemClass: 'col-span-2', |
| | | componentProps: { |
| | | rows: 3, |
| | | placeholder: '缺é·äº§ççæ ¹æ¬åå ', |
| | | disabled: readonly, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'solution', |
| | | label: 'è§£å³æ¹æ¡', |
| | | component: 'Textarea', |
| | | formItemClass: 'col-span-2', |
| | | componentProps: { |
| | | rows: 3, |
| | | placeholder: 'å¤çæªæ½/SOPï¼ä¾åç»å·¥ååè', |
| | | disabled: readonly, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | formItemClass: 'col-span-2', |
| | | componentProps: { rows: 2, placeholder: '夿³¨', disabled: readonly }, |
| | | }, |
| | | { |
| | | fieldName: 'blobIds', |
| | | label: 'éä»¶', |
| | | component: 'FileUpload', |
| | | componentProps: { |
| | | valueKey: 'id', |
| | | maxNumber: 10, |
| | | multiple: true, |
| | | disabled: readonly, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** é®é¢åºæç´¢è¡¨å */ |
| | | export function useProblemSearchFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'keyword', |
| | | label: 'å
³é®å', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: 'åå·/æ é¢/æè¿°/æ¹æ¡', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'itemCode', |
| | | label: 'ç©æç¼ç ', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥ç©æç¼ç ', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'issueCategory', |
| | | label: 'é®é¢åç±»', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥é®é¢åç±»', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'issueType', |
| | | label: 'é®é¢ç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: ISSUE_TYPE_OPTIONS, |
| | | placeholder: 'è¯·éæ©', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'rootCauseCategory', |
| | | label: 'æ ¹å åç±»', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: ROOT_CAUSE_CATEGORY_OPTIONS, |
| | | placeholder: 'è¯·éæ©', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: PROBLEM_STATUS_OPTIONS, |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** é®é¢åºåè¡¨åæ®µ */ |
| | | export function useProblemGridColumns(): VxeTableGridOptions<AftersalesProblemApi.Problem>['columns'] { |
| | | return [ |
| | | { field: 'no', title: 'é®é¢åå·', minWidth: 170, slots: { default: 'no' } }, |
| | | { field: 'title', title: 'é®é¢æ é¢', minWidth: 180 }, |
| | | { field: 'sourceTicketNo', title: 'æ¥æºå·¥å', minWidth: 150 }, |
| | | { field: 'itemName', title: 'ç©æåç§°', minWidth: 120 }, |
| | | { field: 'itemCode', title: 'ç©æç¼ç ', minWidth: 120 }, |
| | | { field: 'issueType', title: 'é®é¢ç±»å', width: 100, slots: { default: 'issueType' } }, |
| | | { field: 'severityLevel', title: '严éç¨åº¦', width: 100, slots: { default: 'severityLevel' } }, |
| | | { field: 'rootCauseCategory', title: 'æ ¹å åç±»', width: 100, slots: { default: 'rootCauseCategory' } }, |
| | | { field: 'status', title: 'ç¶æ', width: 90, slots: { default: 'status' } }, |
| | | { field: 'ownerUserName', title: 'æ²æ·äºº', minWidth: 100 }, |
| | | { field: 'updateTime', title: 'æ´æ°æ¶é´', width: 170, formatter: 'formatDate' }, |
| | | { title: 'æä½', width: 220, fixed: 'right', slots: { default: 'actions' } }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { AftersalesProblemApi } from '#/api/aftersales/problem'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { Button, message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteProblem, |
| | | disableProblem, |
| | | getProblemPage, |
| | | publishProblem, |
| | | } from '#/api/aftersales/problem'; |
| | | |
| | | import { |
| | | PROBLEM_STATUS_OPTIONS, |
| | | ROOT_CAUSE_CATEGORY_OPTIONS, |
| | | useProblemGridColumns, |
| | | useProblemSearchFormSchema, |
| | | } from './data'; |
| | | import ProblemForm from './modules/form.vue'; |
| | | |
| | | const statusLabelMap = Object.fromEntries( |
| | | PROBLEM_STATUS_OPTIONS.map((o) => [o.value, o.label]), |
| | | ); |
| | | const rootCauseLabelMap = Object.fromEntries( |
| | | ROOT_CAUSE_CATEGORY_OPTIONS.map((o) => [o.value, o.label]), |
| | | ); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: ProblemForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | function handleDetail(row: AftersalesProblemApi.Problem) { |
| | | formModalApi.setData({ formType: 'detail', id: row.id }).open(); |
| | | } |
| | | |
| | | function handleEdit(row: AftersalesProblemApi.Problem) { |
| | | formModalApi.setData({ formType: 'update', id: row.id }).open(); |
| | | } |
| | | |
| | | async function handleDelete(row: AftersalesProblemApi.Problem) { |
| | | await deleteProblem(row.id!); |
| | | message.success('å 餿å'); |
| | | handleRefresh(); |
| | | } |
| | | |
| | | async function handlePublish(row: AftersalesProblemApi.Problem) { |
| | | await publishProblem(row.id!); |
| | | message.success('å叿å'); |
| | | handleRefresh(); |
| | | } |
| | | |
| | | async function handleDisable(row: AftersalesProblemApi.Problem) { |
| | | await disableProblem(row.id!); |
| | | message.success('åç¨æå'); |
| | | handleRefresh(); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useProblemSearchFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useProblemGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getProblemPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | toolbarConfig: { |
| | | refresh: true, |
| | | search: true, |
| | | }, |
| | | } as VxeTableGridOptions<AftersalesProblemApi.Problem>, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | |
| | | <Grid table-title="å®åé®é¢åºå表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'æ°å¢é®é¢', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['aftersales:problem:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | |
| | | <template #no="{ row }"> |
| | | <Button type="link" @click="handleDetail(row)"> |
| | | {{ row.no }} |
| | | </Button> |
| | | </template> |
| | | |
| | | <template #issueType="{ row }"> |
| | | <Tag v-if="row.issueType === 1" color="blue">ä¸è¬é®é¢</Tag> |
| | | <Tag v-else-if="row.issueType === 2" color="orange">ç»´ä¿®é®é¢</Tag> |
| | | <Tag v-else-if="row.issueType === 3" color="red">éè´§é®é¢</Tag> |
| | | <span v-else>-</span> |
| | | </template> |
| | | |
| | | <template #severityLevel="{ row }"> |
| | | <Tag v-if="row.severityLevel === 0" color="green">轻微</Tag> |
| | | <Tag v-else-if="row.severityLevel === 1" color="blue">ä¸è¬</Tag> |
| | | <Tag v-else-if="row.severityLevel === 2" color="orange">严é</Tag> |
| | | <Tag v-else-if="row.severityLevel === 3" color="red">è´å½</Tag> |
| | | <span v-else>-</span> |
| | | </template> |
| | | |
| | | <template #rootCauseCategory="{ row }"> |
| | | <span>{{ rootCauseLabelMap[row.rootCauseCategory!] || '-' }}</span> |
| | | </template> |
| | | |
| | | <template #status="{ row }"> |
| | | <Tag |
| | | :color=" |
| | | row.status === 1 |
| | | ? 'green' |
| | | : row.status === 0 |
| | | ? 'default' |
| | | : 'orange' |
| | | " |
| | | > |
| | | {{ statusLabelMap[row.status!] || '-' }} |
| | | </Tag> |
| | | </template> |
| | | |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'æ¥ç', |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | onClick: handleDetail.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'ç¼è¾', |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['aftersales:problem:update'], |
| | | ifShow: row.status === 0, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'åå¸', |
| | | type: 'link', |
| | | auth: ['aftersales:problem:publish'], |
| | | ifShow: row.status === 0 || row.status === 2, |
| | | popConfirm: { |
| | | title: '确认åå¸è¯¥é®é¢ï¼', |
| | | confirm: handlePublish.bind(null, row), |
| | | }, |
| | | }, |
| | | { |
| | | label: 'åç¨', |
| | | type: 'link', |
| | | auth: ['aftersales:problem:disable'], |
| | | ifShow: row.status === 1, |
| | | popConfirm: { |
| | | title: '确认åç¨è¯¥é®é¢ï¼', |
| | | confirm: handleDisable.bind(null, row), |
| | | }, |
| | | }, |
| | | { |
| | | label: 'å é¤', |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['aftersales:problem:delete'], |
| | | ifShow: row.status === 0, |
| | | popConfirm: { |
| | | title: `确认å é¤é®é¢ã${row.no}ãï¼`, |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { ProblemFormType } from '../data'; |
| | | |
| | | import type { AftersalesProblemApi } from '#/api/aftersales/problem'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { Button, message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createProblem, |
| | | getProblem, |
| | | updateProblem, |
| | | } from '#/api/aftersales/problem'; |
| | | |
| | | import { useProblemFormSchema } from '../data'; |
| | | |
| | | defineOptions({ name: 'AftersalesProblemForm' }); |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const formType = ref<ProblemFormType>('create'); |
| | | const id = ref<number>(); |
| | | const submitting = ref(false); |
| | | |
| | | const isEdit = computed(() => formType.value !== 'detail'); |
| | | const title = computed(() => { |
| | | if (formType.value === 'detail') return 'æ¥çå®åé®é¢'; |
| | | if (id.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', |
| | | }); |
| | | |
| | | /** æé®é¢å¯¹è±¡æ å°æè¡¨åå¼ï¼é件转为 FileUpload ç»æï¼ */ |
| | | function problemToForm(p: AftersalesProblemApi.Problem): Record<string, unknown> { |
| | | const form: Record<string, unknown> = { ...p }; |
| | | if (p.attachmentList?.length) { |
| | | form.blobIds = p.attachmentList.map((item) => ({ |
| | | uid: String(item.id), |
| | | name: item.name || '', |
| | | url: item.url || '', |
| | | status: 'done', |
| | | id: item.id, |
| | | })); |
| | | } |
| | | return form; |
| | | } |
| | | |
| | | /** æè¡¨åéçéä»¶å¼è§æ´æ blobId æ°ç» */ |
| | | function normalizeBlobIds(value: unknown): number[] { |
| | | const arr = Array.isArray(value) ? value : []; |
| | | return arr |
| | | .map((x) => |
| | | typeof x === 'number' |
| | | ? x |
| | | : x && typeof x === 'object' && 'id' in (x as Record<string, unknown>) |
| | | ? Number((x as Record<string, unknown>).id) |
| | | : Number(x), |
| | | ) |
| | | .filter((n) => Number.isFinite(n)); |
| | | } |
| | | |
| | | // Modal |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | id.value = undefined; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ formType: ProblemFormType; id?: number }>(); |
| | | formType.value = data.formType; |
| | | id.value = data.id; |
| | | formApi.setState({ schema: useProblemFormSchema(formType.value) }); |
| | | formApi.setDisabled(formType.value === 'detail'); |
| | | if (data.id) { |
| | | modalApi.lock(); |
| | | try { |
| | | const detail = await getProblem(data.id); |
| | | await formApi.setValues(problemToForm(detail)); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | } else { |
| | | await formApi.resetForm(); |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | async function handleSubmit() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | submitting.value = true; |
| | | try { |
| | | const data = (await formApi.getValues()) as Record<string, unknown>; |
| | | const blobIds = normalizeBlobIds(data.blobIds); |
| | | const payload = { ...data, blobIds } as AftersalesProblemApi.Problem; |
| | | if (id.value) { |
| | | await updateProblem({ ...payload, id: id.value }); |
| | | } else { |
| | | await createProblem(payload); |
| | | } |
| | | message.success('æä½æå'); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | } catch (e: unknown) { |
| | | message.error((e as Error).message || 'æäº¤å¤±è´¥'); |
| | | } finally { |
| | | submitting.value = false; |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="title" class="w-2/3"> |
| | | <Form class="mx-4" /> |
| | | |
| | | <template #footer> |
| | | <div class="flex justify-end gap-2"> |
| | | <Button @click="modalApi.close()">åæ¶</Button> |
| | | <Button v-if="isEdit" type="primary" :loading="submitting" @click="handleSubmit"> |
| | | 确认 |
| | | </Button> |
| | | <Button v-else type="primary" @click="modalApi.close()">å
³é</Button> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| | |
| | | import { Page } from '@vben/common-ui'; |
| | | import { AftersaleIssueTypeEnum, AftersaleTicketStatusEnum } from '@vben/constants'; |
| | | |
| | | import { Button, Card, Descriptions, Input, message, Popconfirm, Select, Tag } from 'ant-design-vue'; |
| | | import { |
| | | Button, |
| | | Card, |
| | | Descriptions, |
| | | Input, |
| | | message, |
| | | Popconfirm, |
| | | Select, |
| | | Switch, |
| | | Tag, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | assignTicket, |
| | |
| | | getTicket, |
| | | resolveTicket, |
| | | } from '#/api/aftersales/ticket'; |
| | | import type { AftersalesVisitApi } from '#/api/aftersales/visit'; |
| | | import { |
| | | createVisit, |
| | | getVisitListByTicketId, |
| | | } from '#/api/aftersales/visit'; |
| | | |
| | | defineOptions({ name: 'AftersalesTicketDetail' }); |
| | | |
| | |
| | | const isPendingReturn = computed(() => ticket.value?.status === AftersaleTicketStatusEnum.PENDING_RETURN); |
| | | const isPendingRefund = computed(() => ticket.value?.status === AftersaleTicketStatusEnum.PENDING_REFUND); |
| | | const isResolved = computed(() => ticket.value?.status === AftersaleTicketStatusEnum.RESOLVED); |
| | | /** å访æ¯å¦å·²éè¿ï¼æ»¡æä¸æªå¤åï¼ */ |
| | | const isVisitPassed = computed(() => ticket.value?.visitStatus === 1); |
| | | |
| | | // åè®¿è®°å½ |
| | | const visits = ref<AftersalesVisitApi.Visit[]>([]); |
| | | const showVisitForm = ref(false); |
| | | const visitForm = ref<AftersalesVisitApi.CreateVisit>({ |
| | | ticketId: 0, |
| | | visitType: 0, |
| | | content: '', |
| | | satisfied: true, |
| | | recurred: false, |
| | | }); |
| | | |
| | | const visitTypeOptions = [ |
| | | { label: 'çµè¯å访', value: 0 }, |
| | | { label: 'ä¸é¨å访', value: 1 }, |
| | | { label: 'è¿ç¨å访', value: 2 }, |
| | | { label: 'å
¶ä»', value: 3 }, |
| | | ]; |
| | | const visitTypeMap: Record<number, string> = { |
| | | 0: 'çµè¯', |
| | | 1: 'ä¸é¨', |
| | | 2: 'è¿ç¨', |
| | | 3: 'å
¶ä»', |
| | | }; |
| | | |
| | | // åé
å¤ç人 |
| | | const showAssignInput = ref(false); |
| | |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | async function loadVisits() { |
| | | visits.value = await getVisitListByTicketId(ticketId.value); |
| | | } |
| | | |
| | | function toggleVisitForm() { |
| | | showVisitForm.value = !showVisitForm.value; |
| | | if (showVisitForm.value) { |
| | | visitForm.value = { |
| | | ticketId: ticketId.value, |
| | | visitType: 0, |
| | | content: '', |
| | | satisfied: true, |
| | | recurred: false, |
| | | }; |
| | | } |
| | | } |
| | | |
| | | async function handleCreateVisit() { |
| | | await createVisit({ ...visitForm.value, ticketId: ticketId.value }); |
| | | message.success('å访ç»è®°æå'); |
| | | showVisitForm.value = false; |
| | | await Promise.all([loadVisits(), loadTicket()]); |
| | | } |
| | | |
| | | function toggleAssignInput() { |
| | |
| | | |
| | | onMounted(() => { |
| | | loadTicket(); |
| | | loadVisits(); |
| | | }); |
| | | </script> |
| | | |
| | |
| | | </Descriptions.Item> |
| | | <Descriptions.Item label="è´è´£äºº">{{ ticket.ownerUserName || '-' }}</Descriptions.Item> |
| | | <Descriptions.Item label="å¤ç人">{{ ticket.handlerUserName || '-' }}</Descriptions.Item> |
| | | <Descriptions.Item label="åè®¿ç¶æ"> |
| | | <Tag v-if="ticket.visitStatus === 1" color="green">å·²å访éè¿</Tag> |
| | | <Tag v-else color="orange">å¾
å访</Tag> |
| | | </Descriptions.Item> |
| | | <Descriptions.Item label="é®é¢æ²æ·" :span="2"> |
| | | <template v-if="ticket.problemNo">{{ ticket.problemNo }}</template> |
| | | <template v-else>-</template> |
| | | </Descriptions.Item> |
| | | <Descriptions.Item label="é®é¢æè¿°" :span="3"> |
| | | {{ ticket.issueDescription || '-' }} |
| | | </Descriptions.Item> |
| | |
| | | > |
| | | {{ file.name || 'éä»¶-' + file.id }} |
| | | </a> |
| | | </div> |
| | | </Card> |
| | | |
| | | <!-- åè®¿è®°å½ --> |
| | | <Card v-if="ticket" title="å访记å½" class="mb-4"> |
| | | <template #extra> |
| | | <Button |
| | | v-if="isResolved && !showVisitForm" |
| | | type="primary" |
| | | size="small" |
| | | @click="toggleVisitForm" |
| | | > |
| | | ç»è®°å访 |
| | | </Button> |
| | | </template> |
| | | |
| | | <div v-if="isResolved" class="mb-3 text-sm text-gray-500"> |
| | | ç»è®°ã满æ ä¸ æªå¤åãçå访åï¼å·¥åæå¯å½æ¡£ï¼ç»è®°ã䏿»¡ææå¤åãä¼èªå¨éåå¤çä¸ã |
| | | </div> |
| | | |
| | | <!-- ç»è®°å访表å --> |
| | | <div |
| | | v-if="showVisitForm" |
| | | class="mb-4 flex flex-wrap items-center gap-3 rounded bg-gray-50 p-3" |
| | | > |
| | | <div class="flex items-center gap-1"> |
| | | <span class="text-sm text-gray-500">å访æ¹å¼ï¼</span> |
| | | <Select |
| | | v-model:value="visitForm.visitType" |
| | | :options="visitTypeOptions" |
| | | style="width: 120px" |
| | | /> |
| | | </div> |
| | | <div class="flex items-center gap-1"> |
| | | <span class="text-sm text-gray-500">å访å
容ï¼</span> |
| | | <Input |
| | | v-model:value="visitForm.content" |
| | | placeholder="客æ·åé¦å
容" |
| | | style="width: 260px" |
| | | /> |
| | | </div> |
| | | <div class="flex items-center gap-1"> |
| | | <span class="text-sm text-gray-500">æ¯å¦æ»¡æï¼</span> |
| | | <Switch |
| | | v-model:checked="visitForm.satisfied" |
| | | checked-children="满æ" |
| | | un-checked-children="䏿»¡æ" |
| | | /> |
| | | </div> |
| | | <div class="flex items-center gap-1"> |
| | | <span class="text-sm text-gray-500">æ¯å¦å¤åï¼</span> |
| | | <Switch |
| | | v-model:checked="visitForm.recurred" |
| | | checked-children="å¤å" |
| | | un-checked-children="æªå¤å" |
| | | /> |
| | | </div> |
| | | <Button type="primary" size="small" @click="handleCreateVisit">æäº¤</Button> |
| | | <Button size="small" @click="toggleVisitForm">åæ¶</Button> |
| | | </div> |
| | | |
| | | <!-- å访记å½å表 --> |
| | | <div v-if="visits.length === 0" class="text-sm text-gray-400">ææ å访记å½</div> |
| | | <div v-else class="overflow-x-auto"> |
| | | <table class="w-full border text-sm"> |
| | | <thead class="bg-gray-50"> |
| | | <tr> |
| | | <th class="border p-2 text-left">å访æ¶é´</th> |
| | | <th class="border p-2 text-left">æ¹å¼</th> |
| | | <th class="border p-2 text-left">å访人</th> |
| | | <th class="border p-2 text-center">满æ</th> |
| | | <th class="border p-2 text-center">å¤å</th> |
| | | <th class="border p-2 text-left">å
容</th> |
| | | <th class="border p-2 text-left">夿³¨</th> |
| | | </tr> |
| | | </thead> |
| | | <tbody> |
| | | <tr v-for="visit in visits" :key="visit.id"> |
| | | <td class="border p-2">{{ visit.visitTime }}</td> |
| | | <td class="border p-2">{{ visitTypeMap[visit.visitType!] ?? '-' }}</td> |
| | | <td class="border p-2">{{ visit.visitorUserName || '-' }}</td> |
| | | <td class="border p-2 text-center"> |
| | | <Tag :color="visit.satisfied ? 'green' : 'red'"> |
| | | {{ visit.satisfied ? '满æ' : '䏿»¡æ' }} |
| | | </Tag> |
| | | </td> |
| | | <td class="border p-2 text-center"> |
| | | <Tag v-if="visit.recurred" color="orange">å¤å</Tag> |
| | | <Tag v-else color="default">æªå¤å</Tag> |
| | | </td> |
| | | <td class="border p-2">{{ visit.content || '-' }}</td> |
| | | <td class="border p-2">{{ visit.remark || '-' }}</td> |
| | | </tr> |
| | | </tbody> |
| | | </table> |
| | | </div> |
| | | </Card> |
| | | |
| | |
| | | <Button type="primary">å®ç»</Button> |
| | | </Popconfirm> |
| | | |
| | | <!-- å
³é --> |
| | | <!-- 彿¡£ï¼ä»
å访éè¿åå¯å½æ¡£ï¼æªå访请å¨âå访记å½âéç»è®°ï¼ --> |
| | | <Popconfirm |
| | | v-if="isResolved" |
| | | title="确认å
³é该工åï¼å
³éåä¸å¯æ¢å¤ã" |
| | | v-if="isResolved && isVisitPassed" |
| | | title="ç¡®è®¤å½æ¡£è¯¥å·¥åï¼å½æ¡£åèªå¨æ²æ·ä¸æ¡é®é¢ï¼å·¥ååªè¯»ä¸å¯æ¢å¤ã" |
| | | @confirm="handleClose" |
| | | > |
| | | <Button type="primary">å
³é</Button> |
| | | <Button type="primary">彿¡£</Button> |
| | | </Popconfirm> |
| | | |
| | | <!-- åæ¶ --> |
| | |
| | | import type { AftersalesTicketApi } from '#/api/aftersales/ticket'; |
| | | |
| | | import { ref } from 'vue'; |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { AftersaleTicketStatusEnum } from '@vben/constants'; |
| | |
| | | |
| | | import { useTicketGridColumns, useTicketGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | |
| | | const router = useRouter(); |
| | | |
| | | /** æ¥çå·¥åï¼è¿å
¥è·¯ç±è¯¦æ
页ï¼å¯ç»è®°å访/彿¡£ï¼ */ |
| | | function handleDetail(row: AftersalesTicketApi.Ticket) { |
| | | router.push({ |
| | | name: 'AftersalesTicketDetail', |
| | | params: { id: row.id }, |
| | | }); |
| | | } |
| | | |
| | | const sceneType = ref('1'); |
| | | |
| | |
| | | /** å建工å */ |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | /** æ¥çå·¥å */ |
| | | function handleDetail(row: AftersalesTicketApi.Ticket) { |
| | | formModalApi.setData({ formType: 'detail', id: row.id }).open(); |
| | | } |
| | | |
| | | /** ç¼è¾å·¥å */ |
| | |
| | | }, |
| | | }, |
| | | { |
| | | component: 'Switch', |
| | | fieldName: 'syncMes', |
| | | label: '忥å°MES', |
| | | defaultValue: true, |
| | | componentProps: { |
| | | checkedChildren: 'æ¯', |
| | | unCheckedChildren: 'å¦', |
| | | }, |
| | | rules: z.boolean().default(true), |
| | | }, |
| | | { |
| | | component: "RadioGroup", |
| | | fieldName: "status", |
| | | label: "ç¶æ", |
| | |
| | | { |
| | | field: "syncedMes", |
| | | title: "åæ¥ç¶æ", |
| | | width: 90, |
| | | width: 100, |
| | | fixed: "right", |
| | | slots: { default: "syncedMes" }, |
| | | }, |
| | |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** ç©æå¯¼å
¥ç表å */ |
| | | export function useImportFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'file', |
| | | label: 'ç©ææ°æ®', |
| | | component: 'Upload', |
| | | rules: 'required', |
| | | help: 'ä»
å
许导å
¥ xlsãxlsx æ ¼å¼æä»¶', |
| | | }, |
| | | { |
| | | fieldName: 'updateSupport', |
| | | label: 'æ¯å¦è¦ç', |
| | | component: 'Switch', |
| | | componentProps: { |
| | | checkedChildren: 'æ¯', |
| | | unCheckedChildren: 'å¦', |
| | | }, |
| | | rules: z.boolean().default(false), |
| | | help: 'æ¯å¦æ´æ°å·²ç»åå¨çç©ææ°æ®', |
| | | }, |
| | | ]; |
| | | } |
| | |
| | | import { confirm, Page, useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | import { downloadFileFromBlobPart, isEmpty } from '#/packages/utils/src'; |
| | | |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | import { message, Switch, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteItem, |
| | | deleteItemList, |
| | | // exportItem, |
| | | exportItem, |
| | | getItemPage, |
| | | updateItemStatus, |
| | | updateItemSync, |
| | | } from '#/api/mdm/item'; |
| | | import { getItemTypeSimpleList } from '#/api/mes/md/item/type'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | import ImportForm from './modules/import-form.vue'; |
| | | |
| | | defineOptions({ name: 'MdmItem' }); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ImportModal, importModalApi] = useVbenModal({ |
| | | connectedComponent: ImportForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** 导åºè¡¨æ ¼ */ |
| | | // async function handleExport() { |
| | | // const data = await exportItem(await gridApi.formApi.getValues()); |
| | | // downloadFileFromBlobPart({ fileName: 'ç©æ.xls', source: data }); |
| | | // } |
| | | /** 导åºè¡¨æ ¼ï¼æå½åå表æ¥è¯¢æ¡ä»¶å¯¼åºå
¨é¨å½ä¸æ°æ®ï¼ */ |
| | | async function handleExport() { |
| | | const data = await exportItem(await gridApi.formApi.getValues()); |
| | | downloadFileFromBlobPart({ fileName: 'ç©ææ°æ®.xls', source: data }); |
| | | } |
| | | |
| | | /** åå»ºç©æ */ |
| | | function handleCreate() { |
| | | formModalApi.setData(null).open(); |
| | | } |
| | | |
| | | /** 导å
¥ç©æ */ |
| | | function handleImport() { |
| | | importModalApi.open(); |
| | | } |
| | | |
| | | /** ç¼è¾ç©æ */ |
| | |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | const syncingId = ref<number | null>(null); |
| | | |
| | | /** åæ¢åæ¥ç¶æï¼å
³éå¼å
³ä¼ä» MES å é¤ç©æï¼é确认 */ |
| | | async function handleSyncChange(row: MdmItemApi.Item, checked: boolean) { |
| | | if (!checked) { |
| | | await confirm('å
³é忥å°ä» MES å é¤è¯¥ç©æåå
³èé
ç½®ï¼BOM/SOP/SIP/æ¹æ¬¡é
ç½®ï¼ï¼ç¡®è®¤ç»§ç»ï¼'); |
| | | } |
| | | syncingId.value = row.id!; |
| | | try { |
| | | await updateItemSync(row.id!, checked); |
| | | message.success(checked ? 'å·²åæ¥å° MES' : '已忶忥'); |
| | | handleRefresh(); |
| | | } finally { |
| | | syncingId.value = null; |
| | | } |
| | | } |
| | | |
| | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <ImportModal @success="handleRefresh" /> |
| | | <Grid table-title="ç©æå表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | |
| | | auth: ['mdm:item:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | // { |
| | | // label: $t('ui.actionTitle.export'), |
| | | // type: 'primary', |
| | | // icon: ACTION_ICON.DOWNLOAD, |
| | | // auth: ['mdm:item:export'], |
| | | // onClick: handleExport, |
| | | // }, |
| | | { |
| | | 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', |
| | |
| | | <Tag v-else>å¦</Tag> |
| | | </template> |
| | | <template #syncedMes="{ row }"> |
| | | <Tag v-if="row.syncedMes" color="success">已忥</Tag> |
| | | <Tag v-else color="default">æªåæ¥</Tag> |
| | | <Switch |
| | | :checked="row.syncedMes" |
| | | :loading="syncingId === row.id" |
| | | checkedChildren="已忥" |
| | | unCheckedChildren="æªåæ¥" |
| | | @change="handleSyncChange(row, $event)" |
| | | /> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getItem(data.id); |
| | | // åæ¥ç¶ææ å°ï¼è¯¦æ
æ¥å£è¿å syncedMesï¼è¡¨ååæ®µä¸º syncMes |
| | | formData.value.syncMes = formData.value.syncedMes ?? true; |
| | | await formApi.setValues(formData.value); |
| | | } finally { |
| | | modalApi.unlock(); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FileType } from 'ant-design-vue/es/upload/interface'; |
| | | |
| | | import { h } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button, message, notification, 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'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | formItemClass: 'col-span-2', |
| | | labelWidth: 120, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: useImportFormSchema(), |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | // æäº¤è¡¨å |
| | | const data = await formApi.getValues(); |
| | | try { |
| | | const response = (await importItem(data.file, data.updateSupport)) as any; |
| | | // å
³éå¹¶æç¤º |
| | | await modalApi.close(); |
| | | 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, |
| | | }); |
| | | } 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 importItemTemplate(); |
| | | downloadFileFromBlobPart({ fileName: 'ç©æå¯¼å
¥æ¨¡æ¿.xls', source: data }); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal 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> |
| | | <div class="mx-4 mt-2 mb-4 text-gray-500 text-xs"> |
| | | æç¤ºï¼æ¨¡æ¿ä¸âç©æåç±»ç¼ç âãâ计éåä½ç¼ç âãâè¾
åä½ç¼ç â请填å对åºä¸»æ°æ®çç¼ç ï¼å¯ç空ã |
| | | <div>âæ¯å¦æ¹æ¬¡ç®¡çâãâæ¯å¦åæ¥å°MESâå¡« true/falseã</div> |
| | | <div>âç¶æâå¡« 0ï¼å¯ç¨ï¼/1ï¼ç¦ç¨ï¼ã</div> |
| | | </div> |
| | | <template #prepend-footer> |
| | | <div class="flex flex-auto items-center"> |
| | | <Button @click="handleDownload"> ä¸è½½å¯¼å
¥æ¨¡æ¿ </Button> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| | |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** 计éåä½å¯¼å
¥ç表å */ |
| | | export function useImportFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'file', |
| | | label: 'å使°æ®', |
| | | component: 'Upload', |
| | | rules: 'required', |
| | | help: 'ä»
å
许导å
¥ xlsãxlsx æ ¼å¼æä»¶', |
| | | }, |
| | | { |
| | | fieldName: 'updateSupport', |
| | | label: 'æ¯å¦è¦ç', |
| | | component: 'Switch', |
| | | componentProps: { |
| | | checkedChildren: 'æ¯', |
| | | unCheckedChildren: 'å¦', |
| | | }, |
| | | rules: z.boolean().default(false), |
| | | help: 'æ¯å¦æ´æ°å·²ç»åå¨çå使°æ®', |
| | | }, |
| | | ]; |
| | | } |
| | |
| | | import type { MdmUnitApi } from '#/api/mdm/unit'; |
| | | |
| | | import { Page, useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | import { downloadFileFromBlobPart } from '#/packages/utils/src'; |
| | | |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteUnit, |
| | | exportUnit, |
| | | getUnitPage, |
| | | } from '#/api/mdm/unit'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | import ImportForm from './modules/import-form.vue'; |
| | | |
| | | defineOptions({ name: 'MdmUnit' }); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ImportModal, importModalApi] = useVbenModal({ |
| | | connectedComponent: ImportForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | |
| | | /** å建计éåä½ */ |
| | | function handleCreate() { |
| | | formModalApi.setData(null).open(); |
| | | } |
| | | |
| | | /** 导å
¥è®¡éåä½ */ |
| | | function handleImport() { |
| | | importModalApi.open(); |
| | | } |
| | | |
| | | /** 导åºè®¡éåä½ï¼æå½åå表æ¥è¯¢æ¡ä»¶å¯¼åºå
¨é¨å½ä¸æ°æ®ï¼ */ |
| | | async function handleExport() { |
| | | const data = await exportUnit(await gridApi.formApi.getValues()); |
| | | downloadFileFromBlobPart({ fileName: '计éå使°æ®.xls', source: data }); |
| | | } |
| | | |
| | | /** ç¼è¾è®¡éåä½ */ |
| | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <ImportModal @success="handleRefresh" /> |
| | | <Grid table-title="计éåä½å表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | |
| | | auth: ['mdm:unit:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.import', ['计éåä½']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.UPLOAD, |
| | | auth: ['mdm:unit-measure:import'], |
| | | onClick: handleImport, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.export'), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.DOWNLOAD, |
| | | auth: ['mdm:unit-measure:export'], |
| | | onClick: handleExport, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FileType } from 'ant-design-vue/es/upload/interface'; |
| | | |
| | | import { h } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button, message, notification, Upload } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { importUnit, importUnitTemplate } from '#/api/mdm/unit'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useImportFormSchema } from '../data'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | formItemClass: 'col-span-2', |
| | | labelWidth: 120, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: useImportFormSchema(), |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | // æäº¤è¡¨å |
| | | const data = await formApi.getValues(); |
| | | try { |
| | | const response = (await importUnit(data.file, data.updateSupport)) as any; |
| | | // å
³éå¹¶æç¤º |
| | | await modalApi.close(); |
| | | 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, |
| | | }); |
| | | } 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 importUnitTemplate(); |
| | | downloadFileFromBlobPart({ fileName: '计éåä½å¯¼å
¥æ¨¡æ¿.xls', source: data }); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal 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> |
| | | <div class="mx-4 mt-2 mb-4 text-gray-500 text-xs"> |
| | | æç¤ºï¼æ¨¡æ¿ä¸çâ主åä½ç¼ç â请填å主åä½çç¼ç ï¼å¦ PCï¼ï¼ä¸»å使¬èº«è¯¥åç空å³å¯ã |
| | | <div>âæ¯å¦ä¸»åä½âå¡« true/falseã</div> |
| | | <div>âç¶æâå¡« 0ï¼å¯ç¨ï¼/1ï¼ç¦ç¨ï¼ã</div> |
| | | </div> |
| | | <template #prepend-footer> |
| | | <div class="flex flex-auto items-center"> |
| | | <Button @click="handleDownload"> ä¸è½½å¯¼å
¥æ¨¡æ¿ </Button> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | |
| | | import { CommonStatusEnum, DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { z } from '#/adapter/form'; |
| | | import { getRangePickerDefaultProps } from '#/utils'; |
| | | |
| | | /** æ°å¢/ä¿®æ¹ç表å */ |
| | | export function useFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => false, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'processDefinitionKey', |
| | | label: 'æµç¨å®ä¹ Key', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥æµç¨å®ä¹ Keyï¼å¦ purchase_approve', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'businessType', |
| | | label: 'ä¸å¡ç±»å', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥ä¸å¡ç±»åï¼å¦ erp_purchase_order', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'componentPath', |
| | | label: 'ä¸å¡è¯¦æ
ç»ä»¶è·¯å¾', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: |
| | | '请è¾å
¥ä¸å¡è¯¦æ
ç»ä»¶è·¯å¾ï¼å¦ /erp/purchase/order/detail/index.vue', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'RadioGroup', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), |
| | | buttonStyle: 'solid', |
| | | optionType: 'button', |
| | | }, |
| | | rules: z.number().default(CommonStatusEnum.ENABLE), |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å¤æ³¨', |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表çæç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'processDefinitionKey', |
| | | label: 'æµç¨å®ä¹ Key', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥æµç¨å®ä¹ Key', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'businessType', |
| | | label: 'ä¸å¡ç±»å', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥ä¸å¡ç±»å', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'createTime', |
| | | label: 'å建æ¶é´', |
| | | component: 'RangePicker', |
| | | componentProps: { |
| | | ...getRangePickerDefaultProps(), |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表çåæ®µ */ |
| | | export function useGridColumns(): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { |
| | | field: 'id', |
| | | title: 'ç¼å·', |
| | | minWidth: 80, |
| | | }, |
| | | { |
| | | field: 'processDefinitionKey', |
| | | title: 'æµç¨å®ä¹ Key', |
| | | minWidth: 180, |
| | | }, |
| | | { |
| | | field: 'businessType', |
| | | title: 'ä¸å¡ç±»å', |
| | | minWidth: 160, |
| | | }, |
| | | { |
| | | field: 'componentPath', |
| | | title: 'ä¸å¡è¯¦æ
ç»ä»¶è·¯å¾', |
| | | minWidth: 260, |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | minWidth: 80, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.COMMON_STATUS }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'remark', |
| | | title: '夿³¨', |
| | | minWidth: 160, |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 160, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { BpmBusinessAttachApi } from '#/api/bpm/businessAttach'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteBusinessAttach, |
| | | getBusinessAttachPage, |
| | | } from '#/api/bpm/businessAttach'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** å建ä¸å¡éä»¶é
ç½® */ |
| | | function handleCreate() { |
| | | formModalApi.setData(null).open(); |
| | | } |
| | | |
| | | /** ç¼è¾ä¸å¡éä»¶é
ç½® */ |
| | | function handleEdit(row: BpmBusinessAttachApi.BusinessAttach) { |
| | | formModalApi.setData(row).open(); |
| | | } |
| | | |
| | | /** å é¤ä¸å¡éä»¶é
ç½® */ |
| | | async function handleDelete(row: BpmBusinessAttachApi.BusinessAttach) { |
| | | const hideLoading = message.loading({ |
| | | content: $t('ui.actionMessage.deleting', [row.processDefinitionKey]), |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await deleteBusinessAttach(row.id as number); |
| | | message.success($t('ui.actionMessage.deleteSuccess')); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getBusinessAttachPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | toolbarConfig: { |
| | | refresh: true, |
| | | search: true, |
| | | }, |
| | | } as VxeTableGridOptions<BpmBusinessAttachApi.BusinessAttach>, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height><FormModal @success="handleRefresh" /> |
| | | <Grid table-title="ä¸å¡éä»¶é
ç½®"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['ä¸å¡éä»¶é
ç½®']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['bpm:business-attach:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['bpm:business-attach:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['bpm:business-attach:delete'], |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [ |
| | | row.processDefinitionKey, |
| | | ]), |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { BpmBusinessAttachApi } from '#/api/bpm/businessAttach'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createBusinessAttach, |
| | | getBusinessAttach, |
| | | updateBusinessAttach, |
| | | } from '#/api/bpm/businessAttach'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formData = ref<BpmBusinessAttachApi.BusinessAttach>(); |
| | | const getTitle = computed(() => { |
| | | return formData.value?.id |
| | | ? $t('ui.actionTitle.edit', ['ä¸å¡éä»¶é
ç½®']) |
| | | : $t('ui.actionTitle.create', ['ä¸å¡éä»¶é
ç½®']); |
| | | }); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { |
| | | class: 'w-full', |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | labelWidth: 140, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: useFormSchema(), |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | // æäº¤è¡¨å |
| | | const data = (await formApi.getValues()) as BpmBusinessAttachApi.BusinessAttach; |
| | | try { |
| | | await (formData.value?.id |
| | | ? updateBusinessAttach(data) |
| | | : createBusinessAttach(data)); |
| | | // å
³éå¹¶æç¤º |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | formData.value = undefined; |
| | | return; |
| | | } |
| | | // å è½½æ°æ® |
| | | const data = modalApi.getData<BpmBusinessAttachApi.BusinessAttach>(); |
| | | if (!data || !data.id) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getBusinessAttach(data.id); |
| | | // è®¾ç½®å° values |
| | | await formApi.setValues(formData.value); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-2/3"> |
| | | <Form class="mx-4" /> |
| | | </Modal> |
| | | </template> |
| | |
| | | slots: { default: 'deploymentTime' }, |
| | | }, |
| | | { |
| | | field: 'autoApprove', |
| | | title: 'å
审æ¹', |
| | | minWidth: 110, |
| | | slots: { default: 'autoApprove' }, |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 150, |
| | | fixed: 'right', |
| | |
| | | <script lang="ts" setup> |
| | | import type { ModelCategoryInfo } from '#/api/bpm/model'; |
| | | |
| | | import { onActivated, reactive, ref, useTemplateRef, watch } from 'vue'; |
| | | import { |
| | | computed, |
| | | onActivated, |
| | | onMounted, |
| | | reactive, |
| | | ref, |
| | | useTemplateRef, |
| | | watch, |
| | | } from 'vue'; |
| | | |
| | | import { useAccess } from '@vben/access'; |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | import { cloneDeep } from '@vben/utils'; |
| | | |
| | | import { useSortable } from '@vueuse/integrations/useSortable'; |
| | | import { Button, Card, Dropdown, Input, Menu, message } from 'ant-design-vue'; |
| | | import { |
| | | Button, |
| | | Card, |
| | | Dropdown, |
| | | Input, |
| | | Menu, |
| | | message, |
| | | Switch, |
| | | Tooltip, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | getAutoApproveList, |
| | | updateGlobalAutoApprove, |
| | | } from '#/api/bpm/autoApprove'; |
| | | import { |
| | | getCategorySimpleList, |
| | | updateCategorySortBatch, |
| | |
| | | }); |
| | | |
| | | const modelListSpinning = ref(false); // 模åå表å è½½ç¶æ |
| | | |
| | | /** å
¨å±å
å®¡æ¹æ»å¼å
³å ç¨çæµç¨ Keyï¼ä¸å端 BpmProcessAutoApproveService.GLOBAL_KEY ä¿æä¸è´ï¼ */ |
| | | const GLOBAL_AUTO_APPROVE_KEY = '*'; |
| | | const globalAutoApprove = ref(false); // å
¨å±å
å®¡æ¹æ»å¼å
³ |
| | | const globalAutoApproveLoading = ref(false); // æ»å¼å
³æäº¤ç¶æ |
| | | |
| | | const { hasAccessByCodes } = useAccess(); |
| | | /** å
审æ¹å¼å
³çæéï¼æ æéæ¶å¼å
³ç½®ç°ï¼é¿å
ç¹äºææ¥é */ |
| | | const hasPermiAutoApprove = computed(() => { |
| | | return hasAccessByCodes(['bpm:process-auto-approve:update']); |
| | | }); |
| | | |
| | | const saveSortLoading = ref(false); // ä¿åæåºç¶æ |
| | | const categoryGroup = ref<ModelCategoryInfo[]>([]); // æç
§ category åç»çæ°æ® |
| | |
| | | async function getList() { |
| | | modelListSpinning.value = true; |
| | | try { |
| | | const modelList = await getModelList(queryParams.name); |
| | | const categoryList = await getCategorySimpleList(); |
| | | const [modelList, categoryList, autoApproveList] = await Promise.all([ |
| | | getModelList(queryParams.name), |
| | | getCategorySimpleList(), |
| | | getAutoApproveList(), |
| | | ]); |
| | | // å
审æ¹é
ç½®æãæµç¨ Keyãå»ºç«æ å°ï¼åå¹¶å°æ¨¡åè¡æ°æ®ä¸ï¼ä¾å表å
çå
审æ¹å¼å
³åç´æ¥è¯»å |
| | | const autoApproveMap = new Map<string, boolean>(); |
| | | autoApproveList.forEach((config) => { |
| | | autoApproveMap.set(config.processDefinitionKey, config.autoApprove); |
| | | }); |
| | | modelList.forEach((model: any) => { |
| | | model.autoApprove = autoApproveMap.get(model.key) ?? false; |
| | | }); |
| | | // å
¨å±æ»å¼å
³ä»¥ç¹æ® Key åæ¾å¨å䏿¡é
ç½®é |
| | | globalAutoApprove.value = |
| | | autoApproveMap.get(GLOBAL_AUTO_APPROVE_KEY) ?? false; |
| | | // æç
§ category èå |
| | | categoryGroup.value = categoryList.map((category: any) => ({ |
| | | ...category, |
| | |
| | | } |
| | | |
| | | /** åå§å */ |
| | | onMounted(() => { |
| | | getList(); |
| | | }); |
| | | |
| | | /** æ¿æ´»æ¶å·æ°ï¼keep-alive ç¼ååºæ¯ä¸ä»ç¼è¾é¡µè¿åæ¶è§¦åï¼ */ |
| | | onActivated(() => { |
| | | getList(); |
| | | }); |
| | |
| | | sortableInstance.value.option('disabled', true); |
| | | } |
| | | } |
| | | |
| | | /** 忢å
¨å±å
å®¡æ¹æ»å¼å
³ï¼å¼å¯åæææµç¨ä¸å¾å
å®¡æ¹ */ |
| | | async function handleGlobalAutoApproveChange(checked: boolean) { |
| | | globalAutoApprove.value = checked; |
| | | globalAutoApproveLoading.value = true; |
| | | try { |
| | | await updateGlobalAutoApprove(checked); |
| | | message.success( |
| | | checked |
| | | ? 'å·²å¼å¯å
¨å±å
审æ¹ï¼æææµç¨æäº¤åå°ç´æ¥éè¿' |
| | | : 'å·²å
³éå
¨å±å
审æ¹ï¼æ¢å¤ä¸ºæåæµç¨å¼å
³å¤å®', |
| | | ); |
| | | // éæ°æåå表ï¼åæ¥åæµç¨å¼å
³çå±ç¤º |
| | | await getList(); |
| | | } catch (error) { |
| | | // å¤±è´¥åæ» |
| | | globalAutoApprove.value = !checked; |
| | | console.error('设置å
¨å±å
审æ¹å¼å
³å¤±è´¥', error); |
| | | } finally { |
| | | globalAutoApproveLoading.value = false; |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | |
| | | > |
| | | <template #extra> |
| | | <div v-if="!isCategorySorting"> |
| | | <Tooltip title="å¼å¯åæææµç¨ä¸å¾å
审æ¹ï¼ä¼å
äºåæµç¨èªå·±çå¼å
³ï¼"> |
| | | <span class="mr-1">å
¨é¨å
审æ¹</span> |
| | | </Tooltip> |
| | | <Switch |
| | | :checked="globalAutoApprove" |
| | | :loading="globalAutoApproveLoading" |
| | | :disabled="!hasPermiAutoApprove" |
| | | class="mr-3" |
| | | @change="(checked: any) => handleGlobalAutoApproveChange(checked)" |
| | | /> |
| | | <Input |
| | | v-model:value="queryParams.name" |
| | | placeholder="æç´¢æµç¨" |
| | |
| | | Dropdown, |
| | | Menu, |
| | | message, |
| | | Switch, |
| | | Tag, |
| | | Tooltip, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { updateAutoApproveByKey } from '#/api/bpm/autoApprove'; |
| | | import { deleteCategory } from '#/api/bpm/category'; |
| | | import { |
| | | cleanModel, |
| | |
| | | const isModelSorting = ref(false); |
| | | const originalData = ref<BpmModelApi.Model[]>([]); |
| | | const modelList = ref<BpmModelApi.Model[]>([]); |
| | | const autoApproveLoadingId = ref<null | number>(null); // å
审æ¹å¼å
³ï¼æ£å¨æäº¤ç模åç¼å· |
| | | // TODO @jasonï¼å¯ä»¥å
¨é¨å±å¼ä¹ï¼ @èè¿ ä¸æ¬¡è®¨è®ºã好忝å 为æ§è½é®é¢æåªå±å¼ç¬¬ä¸ä¸ªåç±» |
| | | const isExpand = ref(props.isFirst); // æ ¹æ®æ¯å¦ä¸ºç¬¬ä¸ä¸ªåç±», æ¥è®¾ç½®åå§å±å¼ç¶æ |
| | | |
| | |
| | | }); |
| | | const hasPermiDeploy = computed(() => { |
| | | return hasAccessByCodes(['bpm:model:deploy']); |
| | | }); |
| | | /** å
审æ¹å¼å
³çæéï¼æ æéæ¶å¼å
³ç½®ç°ï¼é¿å
ç¹äºææ¥é */ |
| | | const hasPermiAutoApprove = computed(() => { |
| | | return hasAccessByCodes(['bpm:process-auto-approve:update']); |
| | | }); |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | |
| | | message.success(`åå¸[${row.name}]æµç¨æå`); |
| | | // å·æ°å表 |
| | | emit('success'); |
| | | } |
| | | |
| | | /** 忢å
审æ¹å¼å
³ï¼å¼å¯å该æµç¨æäº¤æ¶ä¸åè¿å
¥äººå·¥å®¡æ¹ï¼ç±ç³»ç»èªå¨éè¿ */ |
| | | async function handleAutoApproveChange(checked: boolean, row: any) { |
| | | const oldValue = row.autoApprove; |
| | | // ä¹è§æ´æ°ï¼é¿å
çå¾
æ¥å£æé´å¼å
³ç¶æåè·³ |
| | | row.autoApprove = checked; |
| | | gridApi.setGridOptions({ data: modelList.value }); |
| | | autoApproveLoadingId.value = row.id; |
| | | try { |
| | | await updateAutoApproveByKey(row.key, checked); |
| | | message.success( |
| | | checked |
| | | ? `å·²å¼å¯[${row.name}]å
审æ¹ï¼è¯¥æµç¨æäº¤åå°ç±ç³»ç»èªå¨éè¿` |
| | | : `å·²å
³é[${row.name}]å
审æ¹ï¼æ¢å¤ä¸ºæ£å¸¸äººå·¥å®¡æ¹`, |
| | | ); |
| | | // éæ°æåå表ï¼ä¿è¯åç«¯ç¶æä¸å端ä¸è´ |
| | | emit('success'); |
| | | } catch (error) { |
| | | // å¤±è´¥åæ» |
| | | row.autoApprove = oldValue; |
| | | gridApi.setGridOptions({ data: modelList.value }); |
| | | console.error('设置å
审æ¹å¼å
³å¤±è´¥', error); |
| | | } finally { |
| | | autoApproveLoadingId.value = null; |
| | | } |
| | | } |
| | | |
| | | /** 'æ´å¤'æä½æé® */ |
| | |
| | | </Tag> |
| | | </div> |
| | | </template> |
| | | <template #autoApprove="{ row }"> |
| | | <div class="flex items-center gap-1"> |
| | | <Switch |
| | | :checked="row.autoApprove" |
| | | :loading="autoApproveLoadingId === row.id" |
| | | :disabled="!hasPermiAutoApprove" |
| | | size="small" |
| | | @change=" |
| | | (checked: any) => handleAutoApproveChange(checked, row) |
| | | " |
| | | /> |
| | | <Tooltip |
| | | v-if="!row.processDefinition" |
| | | title="æµç¨å°æªåå¸ï¼å
审æ¹å¼å
³ä¸ä¼çæï¼è¯·å
å叿µç¨" |
| | | > |
| | | <IconifyIcon |
| | | icon="lucide:alert-circle" |
| | | class="text-base text-orange-400" |
| | | /> |
| | | </Tooltip> |
| | | </div> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <div class="flex items-center space-x-0"> |
| | | <Button |
| | |
| | | |
| | | import { nextTick, onMounted, ref, shallowRef, watch } from 'vue'; |
| | | |
| | | import { useRoute } from 'vue-router'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { |
| | | BpmFieldPermissionType, |
| | |
| | | |
| | | defineOptions({ name: 'BpmProcessInstanceDetail' }); |
| | | |
| | | const route = useRoute(); |
| | | |
| | | const props = defineProps<{ |
| | | activityId?: string; // æµç¨æ´»å¨ç¼å·ï¼ç¨äºæéæ¥ç |
| | | id: string; // æµç¨å®ä¾çç¼å· |
| | | taskId?: string; // ä»»å¡ç¼å· |
| | | }>(); |
| | | |
| | | /** åªè¯»æ¥ç模å¼ï¼ä»ä¸å¡æ¨¡åï¼å¦éå®è®¢å审æ¹è¿åº¦ï¼è·³è½¬æ¶éèå®¡æ¹æä½æé®ï¼ä»
æ¥ç */ |
| | | const viewOnly = route.query.viewOnly === '1'; |
| | | |
| | | const processInstanceLoading = ref(false); // æµç¨å®ä¾çå è½½ä¸ |
| | | const processInstance = ref<BpmProcessInstanceApi.ProcessInstance>(); // æµç¨å®ä¾ |
| | |
| | | </div> |
| | | |
| | | <template #actions> |
| | | <div class="px-4"> |
| | | <div v-if="!viewOnly" class="px-4"> |
| | | <ProcessInstanceOperationButton |
| | | ref="operationButtonRef" |
| | | :process-instance="processInstance" |
| | |
| | | @success="refresh" |
| | | /> |
| | | </div> |
| | | <div v-else class="px-4 py-2 text-sm text-muted-foreground"> |
| | | <span>åªè¯»æ¥ç模å¼ï¼å®¡æ¹æä½è¯·å¨ãåååå
¬ â å¾
åä»»å¡ãä¸å¤ç</span> |
| | | </div> |
| | | </template> |
| | | </Card> |
| | | <!-- æå°å¯¹è¯æ¡ --> |
| | |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 120, |
| | | width: 180, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { reverseApprove } from '#/api/erp/purchase/order'; |
| | | import { getTaskDonePage, withdrawTask } from '#/api/bpm/task'; |
| | | import { router } from '#/router'; |
| | | |
| | |
| | | try { |
| | | await withdrawTask(row.id); |
| | | message.success('æ¤åæå'); |
| | | await gridApi.query(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** æ¯å¦éè´è®¢å审æ¹ä»»å¡ï¼ç¨äºæ¾ç¤º"å审æ¹"ãéèéç¨"æ¤å"ï¼ */ |
| | | function isPurchaseOrderApprove(row: BpmTaskApi.Task) { |
| | | return (row.processInstance?.processDefinitionId || '').startsWith( |
| | | 'purchase_order_approve', |
| | | ); |
| | | } |
| | | |
| | | /** å审æ¹éè´è®¢åï¼å·²å®¡æ ¸ -> æªå®¡æ ¸ï¼å¨åååå
¬å·²åä»»å¡ä¸æä½ï¼ */ |
| | | async function handleReverseApprove(row: BpmTaskApi.Task) { |
| | | const hideLoading = message.loading({ |
| | | content: 'å审æ¹ä¸...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await reverseApprove(row.processInstance.id); |
| | | message.success('åå®¡æ¹æåï¼è®¢å已鿰è¿å
¥å¾
å审æ¹'); |
| | | await gridApi.query(); |
| | | } finally { |
| | | hideLoading(); |
| | |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'å审æ¹', |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.AUDIT, |
| | | ifShow: () => isPurchaseOrderApprove(row), |
| | | popConfirm: { |
| | | title: 'ç¡®å®è¦å审æ¹è¯¥éè´è®¢ååï¼è®¢åå°éæ°è¿å
¥å¾
å审æ¹', |
| | | confirm: handleReverseApprove.bind(null, row), |
| | | }, |
| | | }, |
| | | { |
| | | label: 'æ¤å', |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | ifShow: () => !isPurchaseOrderApprove(row), |
| | | popConfirm: { |
| | | title: 'ç¡®å®è¦æ¤å该任å¡åï¼', |
| | | confirm: handleWithdraw.bind(null, row), |
| | |
| | | |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { Button } from 'ant-design-vue'; |
| | | import { Button, message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getReceivablePage } from '#/api/crm/receivable'; |
| | | import { getReceivablePage, updateReceivableStatus } from '#/api/crm/receivable'; |
| | | import { useGridColumns } from '#/views/crm/receivable/data'; |
| | | |
| | | import { AUDIT_STATUS } from '../data'; |
| | | |
| | | const { push } = useRouter(); |
| | | |
| | | /** æ¥çå®¡æ¹ */ |
| | | function handleProcessDetail(row: CrmReceivableApi.Receivable) { |
| | | push({ |
| | | name: 'BpmProcessInstanceDetail', |
| | | query: { id: row.processInstanceId }, |
| | | /** å®¡æ ¸ */ |
| | | async function handleAudit(row: CrmReceivableApi.Receivable) { |
| | | const hideLoading = message.loading({ |
| | | content: 'å®¡æ ¸ä¸...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await updateReceivableStatus(row.id!, 20); |
| | | message.success('å®¡æ ¸æå'); |
| | | gridApi.query(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** æå¼å款详æ
*/ |
| | |
| | | push({ name: 'CrmContractDetail', params: { id: row.contractId } }); |
| | | } |
| | | |
| | | const [Grid] = useVbenVxeGrid({ |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: [ |
| | | { |
| | |
| | | </Button> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <Button type="link" @click="handleProcessDetail(row)">æ¥ç审æ¹</Button> |
| | | <Button |
| | | v-if="row.auditStatus === 10" |
| | | type="link" |
| | | @click="handleAudit(row)" |
| | | > |
| | | å®¡æ ¸ |
| | | </Button> |
| | | </template> |
| | | </Grid> |
| | | </template> |
| | |
| | | |
| | | /** è½¬ç§»åæº */ |
| | | function handleTransfer() { |
| | | transferModalApi.setData({ bizType: BizTypeEnum.CRM_BUSINESS }).open(); |
| | | transferModalApi |
| | | .setData({ id: businessId.value, bizType: BizTypeEnum.CRM_BUSINESS }) |
| | | .open(); |
| | | } |
| | | |
| | | /** æ´æ°åæºç¶ææä½ */ |
| | |
| | | |
| | | /** 转移线索 */ |
| | | function handleTransfer() { |
| | | transferModalApi.setData({ bizType: BizTypeEnum.CRM_CLUE }).open(); |
| | | transferModalApi |
| | | .setData({ id: clueId.value, bizType: BizTypeEnum.CRM_CLUE }) |
| | | .open(); |
| | | } |
| | | |
| | | /** 转åä¸ºå®¢æ· */ |
| | |
| | | |
| | | /** 转移è系人 */ |
| | | function handleTransfer() { |
| | | transferModalApi.setData({ id: contactId.value }).open(); |
| | | transferModalApi |
| | | .setData({ id: contactId.value, bizType: BizTypeEnum.CRM_CONTACT }) |
| | | .open(); |
| | | } |
| | | |
| | | /** å è½½æ°æ® */ |
| | |
| | | }, |
| | | }, |
| | | { |
| | | title: 'å·²å¼ç¥¨éé¢ï¼å
ï¼', |
| | | field: 'totalInvoicePrice', |
| | | minWidth: 150, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | title: 'æªå¼ç¥¨éé¢ï¼å
ï¼', |
| | | field: 'unInvoicePrice', |
| | | minWidth: 150, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | title: 'è´è´£äºº', |
| | | field: 'ownerUserName', |
| | | minWidth: 150, |
| | |
| | | }, |
| | | }, |
| | | { |
| | | title: 'å·²å¼ç¥¨éé¢ï¼å
ï¼', |
| | | field: 'totalInvoicePrice', |
| | | minWidth: 150, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | title: 'æªå¼ç¥¨éé¢ï¼å
ï¼', |
| | | field: 'unInvoicePrice', |
| | | minWidth: 140, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | title: 'æåè·è¿æ¶é´', |
| | | field: 'contactLastTime', |
| | | minWidth: 180, |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { CrmContractApi } from '#/api/crm/contract'; |
| | | import type { DescriptionItemSchema } from '#/components/description'; |
| | | |
| | | import { h, nextTick, onMounted, ref } from 'vue'; |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { |
| | | erpCountInputFormatter, |
| | | erpPriceInputFormatter, |
| | | formatDateTime, |
| | | } from '@vben/utils'; |
| | | |
| | | import { Spin } from 'ant-design-vue'; |
| | | |
| | | import { useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getContract } from '#/api/crm/contract'; |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | import { useDescription } from '#/components/description'; |
| | | |
| | | defineOptions({ name: 'CrmContractApproveDetail' }); |
| | | |
| | | const props = defineProps<{ id: string }>(); |
| | | |
| | | const loading = ref(false); |
| | | const formData = ref<CrmContractApi.Contract>(); |
| | | |
| | | /** ææ¬åæ®µç©ºå¼å ä½ */ |
| | | function textRender(val: unknown) { |
| | | return val === 0 || val ? String(val) : '-'; |
| | | } |
| | | |
| | | /** åå产åæç»ï¼å®¡æ¹è¯¦æ
åªè¯»åï¼ */ |
| | | function useProductColumns(): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { type: 'seq', title: 'åºå·', width: 60 }, |
| | | { field: 'itemName', title: '产ååç§°', minWidth: 180 }, |
| | | { field: 'itemCode', title: 'ç©æç¼ç ', minWidth: 120 }, |
| | | { field: 'itemUnitName', title: 'åä½', minWidth: 80, formatter: ({ cellValue }) => textRender(cellValue) }, |
| | | { |
| | | field: 'contractPrice', |
| | | title: 'ååä»·æ ¼ï¼å
ï¼', |
| | | minWidth: 120, |
| | | formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-', |
| | | }, |
| | | { |
| | | field: 'count', |
| | | title: 'æ°é', |
| | | minWidth: 100, |
| | | formatter: ({ cellValue }) => erpCountInputFormatter(cellValue) || '-', |
| | | }, |
| | | { |
| | | field: 'totalPrice', |
| | | title: 'å计éé¢ï¼å
ï¼', |
| | | minWidth: 120, |
| | | formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-', |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | gridOptions: { |
| | | columns: useProductColumns(), |
| | | data: [], |
| | | minHeight: 200, |
| | | autoResize: true, |
| | | border: true, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | pagerConfig: { |
| | | enabled: false, |
| | | }, |
| | | toolbarConfig: { |
| | | enabled: false, |
| | | }, |
| | | }, |
| | | }); |
| | | |
| | | const [Descriptions] = useDescription({ |
| | | bordered: true, |
| | | column: 2, |
| | | schema: [ |
| | | { label: 'ååç¼å·', field: 'no', render: textRender }, |
| | | { label: 'åååç§°', field: 'name', render: textRender }, |
| | | { label: '客æ·åç§°', field: 'customerName', render: textRender }, |
| | | { label: 'åæºåç§°', field: 'businessName', render: textRender }, |
| | | { |
| | | label: 'ååéé¢ï¼å
ï¼', |
| | | field: 'totalPrice', |
| | | render: (val) => erpPriceInputFormatter(val) || '-', |
| | | }, |
| | | { |
| | | label: 'å®ééé¢ï¼å
ï¼', |
| | | field: 'depositPrice', |
| | | render: (val) => erpPriceInputFormatter(val) || '-', |
| | | }, |
| | | { |
| | | label: '忬¾éé¢ï¼å
ï¼', |
| | | field: 'totalReceivablePrice', |
| | | render: (val) => erpPriceInputFormatter(val) || '-', |
| | | }, |
| | | { |
| | | label: 'å·²å¼ç¥¨éé¢ï¼å
ï¼', |
| | | field: 'totalInvoicePrice', |
| | | render: (val) => erpPriceInputFormatter(val) || '-', |
| | | }, |
| | | { |
| | | label: 'æªå¼ç¥¨éé¢ï¼å
ï¼', |
| | | field: 'unInvoicePrice', |
| | | render: (val) => erpPriceInputFormatter(val) || '-', |
| | | }, |
| | | { |
| | | label: 'ä¸åæ¶é´', |
| | | field: 'orderDate', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { |
| | | label: 'ååå¼å§æ¶é´', |
| | | field: 'startTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { |
| | | label: 'ååç»ææ¶é´', |
| | | field: 'endTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { label: '客æ·ç¾çº¦äºº', field: 'signContactName', render: textRender }, |
| | | { label: 'å
¬å¸ç¾çº¦äºº', field: 'signUserName', render: textRender }, |
| | | { label: 'éå®è®¢å', field: 'orderNo', render: textRender }, |
| | | { label: 'å建人', field: 'creatorName', render: textRender }, |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { |
| | | label: 'ååç¶æ', |
| | | field: 'auditStatus', |
| | | render: (val) => |
| | | h(DictTag, { type: DICT_TYPE.CRM_AUDIT_STATUS, value: val }), |
| | | }, |
| | | { label: '夿³¨', field: 'remark', render: textRender }, |
| | | ] as DescriptionItemSchema[], |
| | | }); |
| | | |
| | | onMounted(async () => { |
| | | try { |
| | | loading.value = true; |
| | | formData.value = await getContract(Number(props.id)); |
| | | if (formData.value?.products?.length) { |
| | | await nextTick(); |
| | | await gridApi.grid.reloadData(formData.value.products); |
| | | } |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <ContentWrap class="m-2"> |
| | | <Spin :spinning="loading" tip="å è½½ä¸..."> |
| | | <div class="space-y-4"> |
| | | <!-- åååºæ¬ä¿¡æ¯ --> |
| | | <div class="rounded-lg border border-border bg-background p-4"> |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">åååºæ¬ä¿¡æ¯</span> |
| | | </div> |
| | | <Descriptions :data="formData" /> |
| | | </div> |
| | | <!-- 产åæç» --> |
| | | <div |
| | | v-if="formData?.products?.length" |
| | | class="rounded-lg border border-border bg-background p-4" |
| | | > |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">产åæç»</span> |
| | | </div> |
| | | <Grid /> |
| | | </div> |
| | | </div> |
| | | </Spin> |
| | | </ContentWrap> |
| | | </template> |
| | |
| | | render: (val) => erpPriceInputFormatter(val) as string, |
| | | }, |
| | | { |
| | | field: 'totalInvoicePrice', |
| | | label: 'å·²å¼ç¥¨éé¢ï¼å«å®¡æ¹ä¸ï¼', |
| | | render: (val) => erpPriceInputFormatter(val) as string, |
| | | }, |
| | | { |
| | | field: 'ownerUserName', |
| | | label: 'è´è´£äºº', |
| | | }, |
| | |
| | | render: (val) => erpPriceInputFormatter(val) as string, |
| | | }, |
| | | { |
| | | field: 'totalInvoicePrice', |
| | | label: 'å·²å¼ç¥¨éé¢ï¼å«å®¡æ¹ä¸ï¼', |
| | | render: (val) => erpPriceInputFormatter(val) as string, |
| | | }, |
| | | { |
| | | field: 'unInvoicePrice', |
| | | label: 'æªå¼ç¥¨éé¢ï¼å
ï¼', |
| | | render: (val) => erpPriceInputFormatter(val) as string, |
| | | }, |
| | | { |
| | | field: 'depositPrice', |
| | | label: 'å®ééé¢ï¼å
ï¼', |
| | | render: (val) => erpPriceInputFormatter(val) as string, |
| | |
| | | |
| | | /** 转移åå */ |
| | | function handleTransfer() { |
| | | transferModalApi.setData({ bizType: BizTypeEnum.CRM_CONTRACT }).open(); |
| | | transferModalApi |
| | | .setData({ id: contractId.value, bizType: BizTypeEnum.CRM_CONTRACT }) |
| | | .open(); |
| | | } |
| | | |
| | | /** å è½½æ°æ® */ |
| | |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | import ImportForm from './modules/import-form.vue'; |
| | | |
| | | const { push } = useRouter(); |
| | | const sceneType = ref('1'); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ImportModal, importModalApi] = useVbenModal({ |
| | | connectedComponent: ImportForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | |
| | | ...formValues, |
| | | }); |
| | | downloadFileFromBlobPart({ fileName: 'åå.xls', source: data }); |
| | | } |
| | | |
| | | /** 导å
¥åå */ |
| | | function handleImport() { |
| | | importModalApi.open(); |
| | | } |
| | | |
| | | /** å建åå */ |
| | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <ImportModal @success="handleRefresh" /> |
| | | <Grid> |
| | | <template #toolbar-actions> |
| | | <Tabs class="w-full" @change="handleChangeSceneType"> |
| | |
| | | auth: ['crm:contract:export'], |
| | | onClick: handleExport, |
| | | }, |
| | | { |
| | | label: '导å
¥', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.UPLOAD, |
| | | auth: ['crm:contract:import'], |
| | | onClick: handleImport, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FileType } from 'ant-design-vue/es/upload/interface'; |
| | | import type { ImportResult } from '#/api/crm/contract'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | import { downloadFileFromBlobPart } from '#/packages/utils/src'; |
| | | |
| | | import { Button, message, Progress, Upload } from 'ant-design-vue'; |
| | | |
| | | import { importContract, importContractTemplate } from '#/api/crm/contract'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const fileList = ref<FileType[]>([]); |
| | | const importResult = ref<ImportResult>(); |
| | | const importing = ref(false); |
| | | const progressPercent = ref(0); |
| | | let progressTimer: ReturnType<typeof setInterval> | undefined; |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | let shouldClose = false; |
| | | if (fileList.value.length === 0) { |
| | | message.warning('è¯·éæ©è¦å¯¼å
¥çæä»¶'); |
| | | return; |
| | | } |
| | | if (importing.value) return; |
| | | modalApi.lock(); |
| | | importing.value = true; |
| | | startMockProgress(); |
| | | try { |
| | | importResult.value = await importContract(fileList.value[0] as File); |
| | | // 宿ï¼è¿åº¦æ¡èµ°æ»¡ |
| | | stopMockProgress(); |
| | | progressPercent.value = 100; |
| | | await delay(300); |
| | | emit('success'); |
| | | const failedCount = Object.keys(importResult.value?.failureList ?? {}).length; |
| | | if (failedCount > 0) { |
| | | message.warning( |
| | | `导å
¥å®æï¼æ°å¢ ${createCount.value} æ¡ï¼å¤±è´¥ ${failedCount} æ¡ï¼è¯·æ¥ç䏿¹å¤±è´¥æç»`, |
| | | ); |
| | | } else { |
| | | message.success('导å
¥å®æ'); |
| | | shouldClose = true; |
| | | } |
| | | } finally { |
| | | stopMockProgress(); |
| | | importing.value = false; |
| | | modalApi.unlock(); |
| | | if (shouldClose) { |
| | | await modalApi.close(); |
| | | } |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | /** 模æè¿åº¦æ¡ï¼è¯·æ±æé´ä» 0 èµ°å° 90% */ |
| | | function startMockProgress() { |
| | | progressPercent.value = 0; |
| | | progressTimer = setInterval(() => { |
| | | if (progressPercent.value < 90) { |
| | | progressPercent.value = Math.min( |
| | | 90, |
| | | progressPercent.value + Math.random() * 12, |
| | | ); |
| | | } |
| | | }, 300); |
| | | } |
| | | |
| | | function stopMockProgress() { |
| | | if (progressTimer) { |
| | | clearInterval(progressTimer); |
| | | progressTimer = undefined; |
| | | } |
| | | } |
| | | |
| | | function delay(ms: number) { |
| | | return new Promise((resolve) => setTimeout(resolve, ms)); |
| | | } |
| | | |
| | | function beforeUpload(file: FileType) { |
| | | fileList.value = [file]; |
| | | importResult.value = undefined; |
| | | return false; |
| | | } |
| | | |
| | | function handleRemove() { |
| | | fileList.value = []; |
| | | importResult.value = undefined; |
| | | } |
| | | |
| | | async function handleDownloadTemplate() { |
| | | const data = await importContractTemplate(); |
| | | downloadFileFromBlobPart({ fileName: 'åå导å
¥æ¨¡æ¿.xls', source: data }); |
| | | } |
| | | |
| | | const createCount = computed(() => importResult.value?.createList?.length ?? 0); |
| | | const updateCount = computed(() => importResult.value?.updateList?.length ?? 0); |
| | | const failureCount = computed( |
| | | () => Object.keys(importResult.value?.failureList ?? {}).length, |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="导å
¥åå" class="w-[720px]"> |
| | | <div class="mx-4"> |
| | | <Upload |
| | | :max-count="1" |
| | | accept=".xls,.xlsx" |
| | | :file-list="fileList" |
| | | :before-upload="beforeUpload" |
| | | @remove="handleRemove" |
| | | > |
| | | <Button type="primary">éæ© Excel æä»¶</Button> |
| | | </Upload> |
| | | <div class="mt-2 rounded-md bg-gray-50 p-2 text-xs leading-5 text-gray-500"> |
| | | <p>ä»
å
许导å
¥ xlsãxlsx æ ¼å¼æä»¶ï¼å¿
å¡«åï¼åååç§°ã客æ·åç§°ã产åç¼ç ãæ°éãâ客æ·åç§°âéä¸ CRM 客æ·ä¸è´ï¼â产åç¼ç âæç©æç¼ç å¡«åï¼â产ååç§°âåä»
ä¾å¯¹ç
§ï¼ï¼âåååä»·(å
)âç空ååç©æéå®ä»·ã</p> |
| | | <p>ä¸å¼ ååå¯å«å¤æ¡æç»ï¼ç¸åâååç¼å·âçå¤è¡ä¼å并为ä¸å¼ ååï¼ä¸»è¡¨å段å该ç»é¦è¡ï¼ï¼âååç¼å·âçç©ºæ¶æ¯ä¸è¡åçæä¸å¼ æ°ååãååç¼å·å·²å卿¶ä¼å¯¼å
¥å¤±è´¥ã</p> |
| | | <p>ååé颿âæç»åä»·Ãæ°é â 伿 çâèªå¨è®¡ç®ï¼å®ééé¢å¯å¡«ï¼è´è´£äºº = å½åç»å½ç¨æ·ï¼ç¶æä¸ºâæªæäº¤âï¼å¯å¨å表ä¸ç»§ç»ç¼è¾ææäº¤å®¡æ¹ã</p> |
| | | </div> |
| | | <!-- 导å
¥è¿åº¦æ¡ --> |
| | | <div v-if="importing" class="mt-4"> |
| | | <div class="mb-2 text-sm text-gray-600">æ£å¨å¯¼å
¥ååæ°æ®ï¼è¯·ç¨å...</div> |
| | | <Progress :percent="progressPercent" status="active" /> |
| | | </div> |
| | | <!-- 导å
¥ç»æ --> |
| | | <div |
| | | v-if="importResult" |
| | | class="mt-4 rounded-md border border-gray-200 bg-gray-50 p-3" |
| | | > |
| | | <div class="mb-2 font-medium">导å
¥ç»æ</div> |
| | | <div class="mb-2 flex gap-4"> |
| | | <span class="text-green-600">æ°å¢ï¼{{ createCount }} æ¡</span> |
| | | <span class="text-blue-600">è¦çæ´æ°ï¼{{ updateCount }} æ¡</span> |
| | | <span class="text-red-600">失败ï¼{{ failureCount }} æ¡</span> |
| | | </div> |
| | | <div v-if="failureCount > 0" class="max-h-40 overflow-y-auto"> |
| | | <div |
| | | v-for="(reason, name) in importResult.failureList" |
| | | :key="name" |
| | | class="text-sm leading-6 text-red-500" |
| | | > |
| | | {{ name }}ï¼{{ reason }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <template #prepend-footer> |
| | | <Button @click="handleDownloadTemplate">ä¸è½½å¯¼å
¥æ¨¡æ¿</Button> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| | |
| | | formModalApi.setData({ id: customerId.value }).open(); |
| | | } |
| | | |
| | | /** 转移线索 */ |
| | | /** è½¬ç§»å®¢æ· */ |
| | | function handleTransfer() { |
| | | transferModalApi.setData({ id: customerId.value }).open(); |
| | | transferModalApi |
| | | .setData({ id: customerId.value, bizType: BizTypeEnum.CRM_CUSTOMER }) |
| | | .open(); |
| | | } |
| | | |
| | | /** éå®å®¢æ· */ |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | import { useUserStore } from '@vben/stores'; |
| | | |
| | | import { getContractSimpleList } from '#/api/crm/contract'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import CrmCustomerSelect from '#/components/crm-customer-select.vue'; |
| | | |
| | | import { markRaw } from 'vue'; |
| | | |
| | | /** æ°å¢/ä¿®æ¹ç表å */ |
| | | export function useFormSchema(): VbenFormSchema[] { |
| | | const userStore = useUserStore(); |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => false, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'no', |
| | | label: 'å¼ç¥¨ç¼å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: 'ä¿åæ¶èªå¨çæ', |
| | | disabled: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'ownerUserId', |
| | | label: 'è´è´£äºº', |
| | | component: 'ApiSelect', |
| | | rules: 'required', |
| | | dependencies: { |
| | | triggerFields: ['id'], |
| | | disabled: (values) => !!values.id, |
| | | }, |
| | | componentProps: { |
| | | api: getSimpleUserList, |
| | | labelField: 'nickname', |
| | | valueField: 'id', |
| | | placeholder: 'è¯·éæ©è´è´£äºº', |
| | | allowClear: true, |
| | | }, |
| | | defaultValue: userStore.userInfo?.id, |
| | | }, |
| | | { |
| | | fieldName: 'customerId', |
| | | label: '客æ·åç§°', |
| | | component: markRaw(CrmCustomerSelect), |
| | | rules: 'required', |
| | | componentProps: (_values, form) => ({ |
| | | placeholder: 'è¯·éæ©å®¢æ·', |
| | | onChange: () => { |
| | | form.setFieldValue('contractId', undefined); |
| | | form.setFieldValue('price', undefined); |
| | | }, |
| | | }), |
| | | dependencies: { |
| | | triggerFields: ['id'], |
| | | disabled: (values) => !!values.id, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'contractId', |
| | | label: 'åååç§°', |
| | | component: 'Select', |
| | | rules: 'required', |
| | | dependencies: { |
| | | triggerFields: ['customerId'], |
| | | disabled: (values) => !values.customerId || !!values.id, |
| | | async componentProps(values, form) { |
| | | if (!values.customerId) { |
| | | return { |
| | | options: [], |
| | | placeholder: '请å
鿩客æ·', |
| | | }; |
| | | } |
| | | const contracts = await getContractSimpleList(values.customerId); |
| | | const approvedContracts = contracts.filter( |
| | | (item) => item.auditStatus === 20, |
| | | ); |
| | | return { |
| | | options: approvedContracts.map((item) => { |
| | | const remain = |
| | | (item.totalPrice ?? 0) - (item.totalInvoicePrice ?? 0); |
| | | return { |
| | | label: `${item.name}ï¼Â¥${item.totalPrice ?? 0}ï¼å¯å¼ç¥¨ Â¥${remain.toFixed(2)}ï¼`, |
| | | value: item.id, |
| | | }; |
| | | }), |
| | | placeholder: 'è¯·éæ©åå', |
| | | onChange: () => { |
| | | form.setFieldValue('price', undefined); |
| | | }, |
| | | } as any; |
| | | }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'invoiceType', |
| | | label: 'å票类å', |
| | | component: 'Select', |
| | | rules: 'required', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.CRM_INVOICE_TYPE, 'number'), |
| | | placeholder: 'è¯·éæ©å票类å', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'invoiceNo', |
| | | label: 'å票å·ç ', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å票å·ç ', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'invoiceTitle', |
| | | label: 'å票æ¬å¤´', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å票æ¬å¤´', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'price', |
| | | label: 'å¼ç¥¨éé¢ï¼å
ï¼', |
| | | component: 'InputNumber', |
| | | rules: 'required', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | placeholder: '请è¾å
¥å¼ç¥¨éé¢', |
| | | min: 0, |
| | | precision: 2, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'invoiceTime', |
| | | label: 'å¼ç¥¨æ¥æ', |
| | | component: 'DatePicker', |
| | | rules: 'required', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©å¼ç¥¨æ¥æ', |
| | | showTime: false, |
| | | valueFormat: 'YYYY-MM-DD', |
| | | format: 'YYYY-MM-DD', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'blobIds', |
| | | label: 'å票éä»¶', |
| | | component: 'FileUpload', |
| | | componentProps: { |
| | | valueKey: 'id', |
| | | maxNumber: 10, |
| | | multiple: true, |
| | | accept: ['jpg', 'jpeg', 'png', 'pdf'], |
| | | helpText: 'æ¯æjpgãjpegãpngãpdfæ ¼å¼ï¼å个æä»¶ä¸è¶
è¿50MB', |
| | | }, |
| | | formItemClass: 'md:col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å¤æ³¨', |
| | | rows: 4, |
| | | }, |
| | | formItemClass: 'md:col-span-2', |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表çæç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'no', |
| | | label: 'å¼ç¥¨ç¼å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å¼ç¥¨ç¼å·', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'customerId', |
| | | label: '客æ·', |
| | | component: markRaw(CrmCustomerSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©å®¢æ·', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | export function useGridColumns(): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { |
| | | title: 'å¼ç¥¨ç¼å·', |
| | | field: 'no', |
| | | minWidth: 160, |
| | | fixed: 'left', |
| | | slots: { default: 'no' }, |
| | | }, |
| | | { |
| | | title: '客æ·åç§°', |
| | | field: 'customerName', |
| | | minWidth: 150, |
| | | slots: { default: 'customerName' }, |
| | | }, |
| | | { |
| | | title: 'ååç¼å·', |
| | | field: 'contract', |
| | | minWidth: 160, |
| | | slots: { default: 'contractNo' }, |
| | | }, |
| | | { |
| | | title: 'å票类å', |
| | | field: 'invoiceType', |
| | | minWidth: 120, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.CRM_INVOICE_TYPE }, |
| | | }, |
| | | }, |
| | | { |
| | | title: 'å票å·ç ', |
| | | field: 'invoiceNo', |
| | | minWidth: 150, |
| | | }, |
| | | { |
| | | title: 'å¼ç¥¨éé¢ï¼å
ï¼', |
| | | field: 'price', |
| | | minWidth: 150, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | title: 'å©ä½å¯å款éé¢ï¼å«å®¡æ¹ä¸ï¼', |
| | | field: 'remainingInvoicePrice', |
| | | minWidth: 180, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | title: 'å¼ç¥¨æ¥æ', |
| | | field: 'invoiceTime', |
| | | minWidth: 150, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: '夿³¨', |
| | | field: 'remark', |
| | | minWidth: 150, |
| | | }, |
| | | { |
| | | title: 'è´è´£äºº', |
| | | field: 'ownerUserName', |
| | | minWidth: 150, |
| | | }, |
| | | { |
| | | title: 'æå±é¨é¨', |
| | | field: 'ownerUserDeptName', |
| | | minWidth: 150, |
| | | }, |
| | | { |
| | | title: 'æ´æ°æ¶é´', |
| | | field: 'updateTime', |
| | | minWidth: 150, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | minWidth: 150, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: 'å建人', |
| | | field: 'creatorName', |
| | | minWidth: 150, |
| | | }, |
| | | { |
| | | title: 'éä»¶', |
| | | field: 'attachmentList', |
| | | minWidth: 100, |
| | | fixed: 'right', |
| | | slots: { default: 'attachment' }, |
| | | }, |
| | | { |
| | | title: '审æ¹ç¶æ', |
| | | field: 'auditStatus', |
| | | minWidth: 100, |
| | | fixed: 'right', |
| | | slots: { default: 'auditStatus' }, |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | field: 'actions', |
| | | minWidth: 200, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { CrmInvoiceApi } from '#/api/crm/invoice'; |
| | | import type { DescriptionItemSchema } from '#/components/description'; |
| | | |
| | | import { h, onMounted, ref } from 'vue'; |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { erpPriceInputFormatter, formatDateTime } from '@vben/utils'; |
| | | |
| | | import { Spin } from 'ant-design-vue'; |
| | | |
| | | import { getInvoice } from '#/api/crm/invoice'; |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | import { useDescription } from '#/components/description'; |
| | | |
| | | defineOptions({ name: 'CrmInvoiceApproveDetail' }); |
| | | |
| | | const props = defineProps<{ id: string }>(); |
| | | |
| | | const loading = ref(false); |
| | | const formData = ref<CrmInvoiceApi.Invoice>(); |
| | | |
| | | /** ææ¬åæ®µç©ºå¼å ä½ */ |
| | | function textRender(val: unknown) { |
| | | return val === 0 || val ? String(val) : '-'; |
| | | } |
| | | |
| | | const [Descriptions] = useDescription({ |
| | | bordered: true, |
| | | column: 2, |
| | | schema: [ |
| | | { label: 'å¼ç¥¨ç¼å·', field: 'no', render: textRender }, |
| | | { label: '客æ·åç§°', field: 'customerName', render: textRender }, |
| | | { |
| | | label: 'ååç¼å·', |
| | | field: 'contract', |
| | | render: (_val, data) => (data?.contract?.no ? data.contract.no : '-'), |
| | | }, |
| | | { |
| | | label: 'å票类å', |
| | | field: 'invoiceType', |
| | | render: (val) => |
| | | h(DictTag, { type: DICT_TYPE.CRM_INVOICE_TYPE, value: val }), |
| | | }, |
| | | { label: 'å票å·ç ', field: 'invoiceNo', render: textRender }, |
| | | { label: 'å票æ¬å¤´', field: 'invoiceTitle', render: textRender }, |
| | | { |
| | | label: 'å¼ç¥¨éé¢ï¼å
ï¼', |
| | | field: 'price', |
| | | render: (val) => erpPriceInputFormatter(val) || '-', |
| | | }, |
| | | { |
| | | label: 'å©ä½å¯å款éé¢ï¼å
ï¼', |
| | | field: 'remainingInvoicePrice', |
| | | render: (val) => erpPriceInputFormatter(val ?? 0) || '-', |
| | | }, |
| | | { |
| | | label: 'å¼ç¥¨æ¥æ', |
| | | field: 'invoiceTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { |
| | | label: '审æ¹ç¶æ', |
| | | field: 'auditStatus', |
| | | render: (val) => |
| | | h(DictTag, { type: DICT_TYPE.CRM_AUDIT_STATUS, value: val }), |
| | | }, |
| | | { label: 'è´è´£äºº', field: 'ownerUserName', render: textRender }, |
| | | { label: 'å建人', field: 'creatorName', render: textRender }, |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { label: '夿³¨', field: 'remark', render: textRender }, |
| | | ] as DescriptionItemSchema[], |
| | | }); |
| | | |
| | | onMounted(async () => { |
| | | try { |
| | | loading.value = true; |
| | | formData.value = await getInvoice(Number(props.id)); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <ContentWrap class="m-2"> |
| | | <Spin :spinning="loading" tip="å è½½ä¸..."> |
| | | <div class="space-y-4"> |
| | | <!-- å¼ç¥¨åºæ¬ä¿¡æ¯ --> |
| | | <div class="rounded-lg border border-border bg-background p-4"> |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">å¼ç¥¨åºæ¬ä¿¡æ¯</span> |
| | | </div> |
| | | <Descriptions :data="formData" /> |
| | | </div> |
| | | </div> |
| | | </Spin> |
| | | </ContentWrap> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { DescriptionItemSchema } from '#/components/description'; |
| | | |
| | | import { h } from 'vue'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { erpPriceInputFormatter, formatDateTime } from '@vben/utils'; |
| | | |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | |
| | | /** 详æ
页çåæ®µ */ |
| | | export function useDetailSchema(): DescriptionItemSchema[] { |
| | | return [ |
| | | { |
| | | field: 'customerName', |
| | | label: '客æ·åç§°', |
| | | }, |
| | | { |
| | | field: 'price', |
| | | label: 'å¼ç¥¨éé¢ï¼å
ï¼', |
| | | render: (val) => erpPriceInputFormatter(val), |
| | | }, |
| | | { |
| | | field: 'remainingInvoicePrice', |
| | | label: 'å©ä½å¯å款éé¢ï¼å«å®¡æ¹ä¸ï¼', |
| | | render: (val) => erpPriceInputFormatter(val ?? 0), |
| | | }, |
| | | { |
| | | field: 'invoiceTime', |
| | | label: 'å¼ç¥¨æ¥æ', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { |
| | | field: 'invoiceType', |
| | | label: 'å票类å', |
| | | render: (val) => |
| | | h(DictTag, { |
| | | type: DICT_TYPE.CRM_INVOICE_TYPE, |
| | | value: val, |
| | | }), |
| | | }, |
| | | { |
| | | field: 'invoiceTitle', |
| | | label: 'å票æ¬å¤´', |
| | | }, |
| | | { |
| | | field: 'invoiceNo', |
| | | label: 'å票å·ç ', |
| | | }, |
| | | { |
| | | field: 'ownerUserName', |
| | | label: 'è´è´£äºº', |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** 详æ
页çåºç¡å段 */ |
| | | export function useDetailBaseSchema(): DescriptionItemSchema[] { |
| | | return [ |
| | | { |
| | | field: 'no', |
| | | label: 'å¼ç¥¨ç¼å·', |
| | | }, |
| | | { |
| | | field: 'customerName', |
| | | label: '客æ·åç§°', |
| | | }, |
| | | { |
| | | field: 'contract', |
| | | label: 'ååç¼å·', |
| | | render: (val, data) => |
| | | val && data?.contract?.no ? data?.contract?.no : '', |
| | | }, |
| | | { |
| | | field: 'invoiceType', |
| | | label: 'å票类å', |
| | | render: (val) => |
| | | h(DictTag, { |
| | | type: DICT_TYPE.CRM_INVOICE_TYPE, |
| | | value: val, |
| | | }), |
| | | }, |
| | | { |
| | | field: 'invoiceNo', |
| | | label: 'å票å·ç ', |
| | | }, |
| | | { |
| | | field: 'invoiceTitle', |
| | | label: 'å票æ¬å¤´', |
| | | }, |
| | | { |
| | | field: 'price', |
| | | label: 'å¼ç¥¨éé¢', |
| | | render: (val) => erpPriceInputFormatter(val), |
| | | }, |
| | | { |
| | | field: 'remainingInvoicePrice', |
| | | label: 'å©ä½å¯å款éé¢ï¼å«å®¡æ¹ä¸ï¼', |
| | | render: (val) => erpPriceInputFormatter(val ?? 0), |
| | | }, |
| | | { |
| | | field: 'invoiceTime', |
| | | label: 'å¼ç¥¨æ¥æ', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { |
| | | field: 'remark', |
| | | label: '夿³¨', |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** ç³»ç»ä¿¡æ¯å段 */ |
| | | export function useDetailSystemSchema(): DescriptionItemSchema[] { |
| | | return [ |
| | | { |
| | | field: 'ownerUserName', |
| | | label: 'è´è´£äºº', |
| | | }, |
| | | { |
| | | field: 'creatorName', |
| | | label: 'å建人', |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | label: 'å建æ¶é´', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { |
| | | field: 'updateTime', |
| | | label: 'æ´æ°æ¶é´', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup lang="ts"> |
| | | import type { CrmInvoiceApi } from '#/api/crm/invoice'; |
| | | import type { SystemOperateLogApi } from '#/api/system/operate-log'; |
| | | |
| | | import { onMounted, ref } from 'vue'; |
| | | import { useRoute, useRouter } from 'vue-router'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { useTabs } from '@vben/hooks'; |
| | | |
| | | import { Card, Tabs } from 'ant-design-vue'; |
| | | |
| | | import { getInvoice } from '#/api/crm/invoice'; |
| | | import { getOperateLogPage } from '#/api/crm/operateLog'; |
| | | import { BizTypeEnum } from '#/api/crm/permission'; |
| | | import { useDescription } from '#/components/description'; |
| | | import { OperateLog } from '#/components/operate-log'; |
| | | import { ACTION_ICON, TableAction } from '#/components/table-action'; |
| | | import { $t } from '#/locales'; |
| | | import { PermissionList } from '#/views/crm/permission'; |
| | | |
| | | import InvoiceForm from '../modules/form.vue'; |
| | | import { useDetailSchema } from './data'; |
| | | import Info from './modules/info.vue'; |
| | | |
| | | const props = defineProps<{ id?: number }>(); |
| | | |
| | | const route = useRoute(); |
| | | const router = useRouter(); |
| | | const tabs = useTabs(); |
| | | |
| | | const loading = ref(false); |
| | | const invoiceId = ref(0); |
| | | const invoice = ref<CrmInvoiceApi.Invoice>({} as CrmInvoiceApi.Invoice); |
| | | const logList = ref<SystemOperateLogApi.OperateLog[]>([]); |
| | | const permissionListRef = ref<InstanceType<typeof PermissionList>>(); |
| | | |
| | | const [Descriptions] = useDescription({ |
| | | bordered: false, |
| | | column: 4, |
| | | schema: useDetailSchema(), |
| | | }); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: InvoiceForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** å è½½å¼ç¥¨è¯¦æ
*/ |
| | | async function loadInvoiceDetail() { |
| | | loading.value = true; |
| | | try { |
| | | invoice.value = await getInvoice(invoiceId.value); |
| | | const res = await getOperateLogPage({ |
| | | bizType: BizTypeEnum.CRM_INVOICE, |
| | | bizId: invoiceId.value, |
| | | }); |
| | | logList.value = res.list; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | /** è¿åå表页 */ |
| | | function handleBack() { |
| | | tabs.closeCurrentTab(); |
| | | router.push({ name: 'CrmInvoice' }); |
| | | } |
| | | |
| | | /** ç¼è¾å¼ç¥¨ */ |
| | | function handleEdit() { |
| | | formModalApi.setData({ invoice: { id: invoiceId.value } }).open(); |
| | | } |
| | | |
| | | /** å è½½æ°æ® */ |
| | | onMounted(() => { |
| | | invoiceId.value = Number(props.id || route.params.id); |
| | | loadInvoiceDetail(); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height :title="invoice?.no" :loading="loading"> |
| | | <FormModal @success="loadInvoiceDetail" /> |
| | | <template #extra> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'è¿å', |
| | | type: 'default', |
| | | icon: 'lucide:arrow-left', |
| | | onClick: handleBack, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.edit'), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['crm:invoice:update'], |
| | | ifShow: permissionListRef?.validateWrite, |
| | | onClick: handleEdit, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <Card class="min-h-[10%]"> |
| | | <Descriptions :data="invoice" /> |
| | | </Card> |
| | | <Card class="mt-4 min-h-[60%]"> |
| | | <Tabs> |
| | | <Tabs.TabPane tab="详ç»èµæ" key="1" :force-render="true"> |
| | | <Info :invoice="invoice" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane tab="æä½æ¥å¿" key="2" :force-render="true"> |
| | | <OperateLog :log-list="logList" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane tab="å¢éæå" key="3" :force-render="true"> |
| | | <PermissionList |
| | | ref="permissionListRef" |
| | | :biz-id="invoiceId" |
| | | :biz-type="BizTypeEnum.CRM_INVOICE" |
| | | :show-action="true" |
| | | @quit-team="handleBack" |
| | | /> |
| | | </Tabs.TabPane> |
| | | </Tabs> |
| | | </Card> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { CrmInvoiceApi } from '#/api/crm/invoice'; |
| | | |
| | | import { Divider } from 'ant-design-vue'; |
| | | |
| | | import { useDescription } from '#/components/description'; |
| | | |
| | | import { useDetailBaseSchema, useDetailSystemSchema } from '../data'; |
| | | |
| | | const props = defineProps<{ |
| | | invoice: CrmInvoiceApi.Invoice; |
| | | }>(); |
| | | |
| | | const [BaseDescriptions] = useDescription({ |
| | | title: 'åºæ¬ä¿¡æ¯', |
| | | bordered: false, |
| | | column: 4, |
| | | class: 'mx-4', |
| | | schema: useDetailBaseSchema(), |
| | | }); |
| | | |
| | | const [SystemDescriptions] = useDescription({ |
| | | title: 'ç³»ç»ä¿¡æ¯', |
| | | bordered: false, |
| | | column: 3, |
| | | class: 'mx-4', |
| | | schema: useDetailSystemSchema(), |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <div> |
| | | <BaseDescriptions :data="invoice" /> |
| | | <Divider /> |
| | | <div v-if="props.invoice?.attachmentList?.length" class="mx-4 mb-4"> |
| | | <h4 class="mb-2 text-base font-medium">å票éä»¶</h4> |
| | | <div |
| | | v-for="file in props.invoice.attachmentList" |
| | | :key="file.id" |
| | | class="flex items-center gap-2 py-1" |
| | | > |
| | | <a :href="file.url" target="_blank" rel="noopener noreferrer"> |
| | | {{ file.name || 'éä»¶' }} |
| | | </a> |
| | | </div> |
| | | </div> |
| | | <SystemDescriptions :data="invoice" /> |
| | | </div> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { CrmInvoiceApi } from '#/api/crm/invoice'; |
| | | |
| | | import { ref } from 'vue'; |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button, message, Tag, Tabs } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteInvoice, |
| | | exportInvoice, |
| | | getInvoicePage, |
| | | submitInvoice, |
| | | } from '#/api/crm/invoice'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | |
| | | const { push } = useRouter(); |
| | | const sceneType = ref('1'); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** å¤çåºæ¯ç±»åç忢 */ |
| | | function handleChangeSceneType(key: number | string) { |
| | | sceneType.value = key.toString(); |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** 导åºè¡¨æ ¼ */ |
| | | async function handleExport() { |
| | | const formValues = await gridApi.formApi.getValues(); |
| | | const data = await exportInvoice({ |
| | | sceneType: sceneType.value, |
| | | ...formValues, |
| | | }); |
| | | downloadFileFromBlobPart({ fileName: 'å¼ç¥¨.xls', source: data }); |
| | | } |
| | | |
| | | /** å建å¼ç¥¨ */ |
| | | function handleCreate() { |
| | | formModalApi.setData(null).open(); |
| | | } |
| | | |
| | | /** ç¼è¾å¼ç¥¨ */ |
| | | function handleEdit(row: CrmInvoiceApi.Invoice) { |
| | | formModalApi.setData({ invoice: row }).open(); |
| | | } |
| | | |
| | | /** å é¤å¼ç¥¨ */ |
| | | async function handleDelete(row: CrmInvoiceApi.Invoice) { |
| | | const hideLoading = message.loading({ |
| | | content: $t('ui.actionMessage.deleting', [row.no]), |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await deleteInvoice(row.id!); |
| | | message.success($t('ui.actionMessage.deleteSuccess', [row.no])); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** æäº¤å®¡æ ¸ */ |
| | | async function handleSubmit(row: CrmInvoiceApi.Invoice) { |
| | | const hideLoading = message.loading({ |
| | | content: 'æäº¤å®¡æ ¸ä¸...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await submitInvoice(row.id!); |
| | | message.success('æäº¤å®¡æ ¸æå'); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** æ¥çå¼ç¥¨è¯¦æ
*/ |
| | | function handleDetail(row: CrmInvoiceApi.Invoice) { |
| | | push({ name: 'CrmInvoiceDetail', params: { id: row.id } }); |
| | | } |
| | | |
| | | /** æ¥ç客æ·è¯¦æ
*/ |
| | | function handleCustomerDetail(row: CrmInvoiceApi.Invoice) { |
| | | push({ name: 'CrmCustomerDetail', params: { id: row.customerId } }); |
| | | } |
| | | |
| | | /** æ¥çåå详æ
*/ |
| | | function handleContractDetail(row: CrmInvoiceApi.Invoice) { |
| | | push({ name: 'CrmContractDetail', params: { id: row.contractId } }); |
| | | } |
| | | |
| | | /** æ¥ç审æ¹è¯¦æ
*/ |
| | | function handleProcessDetail(row: CrmInvoiceApi.Invoice) { |
| | | push({ |
| | | name: 'BpmProcessInstanceDetail', |
| | | query: { id: row.processInstanceId }, |
| | | }); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getInvoicePage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | sceneType: sceneType.value, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | toolbarConfig: { |
| | | refresh: true, |
| | | search: true, |
| | | }, |
| | | } as VxeTableGridOptions<CrmInvoiceApi.Invoice>, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <Grid> |
| | | <template #toolbar-actions> |
| | | <Tabs class="w-full" @change="handleChangeSceneType"> |
| | | <Tabs.TabPane tab="æè´è´£ç" key="1" /> |
| | | <Tabs.TabPane tab="æåä¸ç" key="2" /> |
| | | <Tabs.TabPane tab="ä¸å±è´è´£ç" key="3" /> |
| | | </Tabs> |
| | | </template> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'æ°å¢å¼ç¥¨ç³è¯·', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['crm:invoice:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.export'), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.DOWNLOAD, |
| | | auth: ['crm:invoice:export'], |
| | | onClick: handleExport, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #no="{ row }"> |
| | | <Button type="link" @click="handleDetail(row)"> |
| | | {{ row.no }} |
| | | </Button> |
| | | </template> |
| | | <template #customerName="{ row }"> |
| | | <Button type="link" @click="handleCustomerDetail(row)"> |
| | | {{ row.customerName }} |
| | | </Button> |
| | | </template> |
| | | <template #contractNo="{ row }"> |
| | | <Button |
| | | v-if="row.contract" |
| | | type="link" |
| | | @click="handleContractDetail(row)" |
| | | > |
| | | {{ row.contract.no }} |
| | | </Button> |
| | | <span v-else>--</span> |
| | | </template> |
| | | <template #attachment="{ row }"> |
| | | <Tag v-if="!row.hasAttachment" color="warning"> |
| | | æªä¸ä¼ |
| | | </Tag> |
| | | <Tag |
| | | v-else |
| | | color="success" |
| | | style="cursor: pointer" |
| | | @click="handleDetail(row)" |
| | | > |
| | | å·²ä¸ä¼ |
| | | </Tag> |
| | | </template> |
| | | <template #auditStatus="{ row }"> |
| | | <Tag v-if="row.auditStatus === 0" color="default">æªæäº¤</Tag> |
| | | <Tag v-if="row.auditStatus === 10" color="warning">审æ¹ä¸</Tag> |
| | | <Tag v-if="row.auditStatus === 20" color="success">å®¡æ ¸éè¿</Tag> |
| | | <Tag v-if="row.auditStatus === 30" color="error">å®¡æ ¸ä¸éè¿</Tag> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['crm:invoice:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'æäº¤å®¡æ ¸', |
| | | type: 'link', |
| | | auth: ['crm:invoice:update'], |
| | | onClick: handleSubmit.bind(null, row), |
| | | ifShow: row.auditStatus === 0, |
| | | }, |
| | | { |
| | | label: 'æ¥ç审æ¹', |
| | | type: 'link', |
| | | auth: ['crm:invoice:update'], |
| | | onClick: handleProcessDetail.bind(null, row), |
| | | ifShow: row.auditStatus !== 0 && !!row.processInstanceId, |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['crm:invoice:delete'], |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.no]), |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { CrmInvoiceApi } from '#/api/crm/invoice'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenForm, useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { getContractSimpleList } from '#/api/crm/contract'; |
| | | import { |
| | | createInvoice, |
| | | getInvoice, |
| | | updateInvoice, |
| | | } from '#/api/crm/invoice'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | |
| | | type InvoicePrefillData = Partial< |
| | | Pick<CrmInvoiceApi.Invoice, 'contractId' | 'customerId'> |
| | | >; |
| | | |
| | | type InvoiceFormModalData = InvoicePrefillData & { |
| | | invoice?: Pick<CrmInvoiceApi.Invoice, 'id'>; |
| | | }; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formData = ref<Partial<CrmInvoiceApi.Invoice>>(); |
| | | const getTitle = computed(() => { |
| | | return formData.value?.id |
| | | ? $t('ui.actionTitle.edit', ['å¼ç¥¨']) |
| | | : $t('ui.actionTitle.create', ['å¼ç¥¨']); |
| | | }); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { |
| | | class: 'w-full', |
| | | }, |
| | | labelWidth: 120, |
| | | }, |
| | | wrapperClass: 'grid-cols-2', |
| | | layout: 'horizontal', |
| | | schema: useFormSchema(), |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | /** æå»ºæ°å¢å¼ç¥¨çé¢å¡«è¡¨å */ |
| | | async function buildCreateFormData( |
| | | prefill: InvoicePrefillData, |
| | | ): Promise<Partial<CrmInvoiceApi.Invoice>> { |
| | | const values: Partial<CrmInvoiceApi.Invoice> = { |
| | | contractId: prefill?.contractId, |
| | | customerId: prefill?.customerId, |
| | | }; |
| | | if (values.customerId && values.contractId) { |
| | | const contracts = await getContractSimpleList(values.customerId); |
| | | const contract = contracts.find((item) => item.id === values.contractId); |
| | | if (contract) { |
| | | values.invoiceTitle = contract.customerName; |
| | | } |
| | | } |
| | | return values; |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | const data = (await formApi.getValues()) as CrmInvoiceApi.Invoice; |
| | | if (formData.value?.id) { |
| | | data.id = formData.value.id; |
| | | } |
| | | try { |
| | | await (formData.value?.id |
| | | ? updateInvoice(data) |
| | | : createInvoice(data)); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | formData.value = undefined; |
| | | return; |
| | | } |
| | | formData.value = undefined; |
| | | await formApi.resetForm(); |
| | | const data = modalApi.getData() as null | InvoiceFormModalData; |
| | | if (!data) { |
| | | return; |
| | | } |
| | | const { invoice } = data; |
| | | const prefill = data.customerId || data.contractId ? data : undefined; |
| | | modalApi.lock(); |
| | | try { |
| | | if (invoice?.id) { |
| | | formData.value = await getInvoice(invoice.id!); |
| | | // ç¨ attachmentList ä»£æ¿ blobIdsï¼è®©ä¸ä¼ ç»ä»¶è½å±ç¤ºæä»¶ååé¢è§é¾æ¥ |
| | | if (formData.value?.attachmentList?.length) { |
| | | (formData.value as Record<string, unknown>).blobIds = |
| | | formData.value.attachmentList; |
| | | } |
| | | } else if (prefill) { |
| | | formData.value = await buildCreateFormData(prefill); |
| | | } |
| | | if (formData.value) { |
| | | await formApi.setValues(formData.value as any); |
| | | } |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-3/5"> |
| | | <Form class="mx-4" /> |
| | | </Modal> |
| | | </template> |
| | |
| | | }) { |
| | | if (records.some((item) => item.level === PermissionLevelEnum.OWNER)) { |
| | | message.warning('ä¸è½éæ©è´è´£äººï¼'); |
| | | checkedRows.value = []; |
| | | gridApi.grid.setAllCheckboxRow(false); |
| | | return; |
| | | } |
| | |
| | | } |
| | | // æç¤ºå¹¶è¿åæå |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | checkedRows.value = []; |
| | | gridApi.grid.setAllCheckboxRow(false); |
| | | handleRefresh(); |
| | | return true; |
| | | } |
| | | |
| | | /** éåºå¢é */ |
| | | async function handleQuit() { |
| | | if (checkedRows.value.length > 0) { |
| | | return handleDelete(); |
| | | } |
| | | |
| | | const permission = gridApi.grid |
| | | .getData() |
| | | .find( |
| | | (item) => |
| | | item.id === userStore.userInfo?.id && |
| | | item.userId === userStore.userInfo?.id && |
| | | item.level === PermissionLevelEnum.OWNER, |
| | | ); |
| | | if (permission) { |
| | |
| | | |
| | | const userPermission = gridApi.grid |
| | | .getData() |
| | | .find((item) => item.id === userStore.userInfo?.id); |
| | | .find((item) => item.userId === userStore.userInfo?.id); |
| | | if (!userPermission) { |
| | | message.warning('ä½ ä¸æ¯å¢éæåï¼'); |
| | | return; |
| | |
| | | return; |
| | | } |
| | | // å è½½æ°æ® |
| | | const data = modalApi.getData<{ bizType: number }>(); |
| | | if (!data || !data.bizType) { |
| | | const data = modalApi.getData<{ bizType: number; id: number }>(); |
| | | if (!data?.bizType || !data.id) { |
| | | return; |
| | | } |
| | | bizType.value = data.bizType; |
| | | await formApi.setFieldValue('id', data.bizType); |
| | | await formApi.setFieldValue('id', data.id); |
| | | }, |
| | | }); |
| | | </script> |
| | |
| | | minWidth: 150, |
| | | }, |
| | | { |
| | | title: '忬¾ç¶æ', |
| | | field: 'auditStatus', |
| | | minWidth: 100, |
| | | fixed: 'right', |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.CRM_AUDIT_STATUS }, |
| | | }, |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | field: 'actions', |
| | | width: 130, |
| | |
| | | import { useUserStore } from '@vben/stores'; |
| | | |
| | | import { getContractSimpleList } from '#/api/crm/contract'; |
| | | import { getInvoiceSimpleList } from '#/api/crm/invoice'; |
| | | import { |
| | | getReceivablePlan, |
| | | getReceivablePlanSimpleList, |
| | |
| | | placeholder: 'è¯·éæ©å®¢æ·', |
| | | onChange: () => { |
| | | form.setFieldValue('contractId', undefined); |
| | | form.setFieldValue('invoiceId', undefined); |
| | | form.setFieldValue('planId', undefined); |
| | | form.setFieldValue('price', undefined); |
| | | form.setFieldValue('returnTime', undefined); |
| | |
| | | })), |
| | | placeholder: 'è¯·éæ©åå', |
| | | onChange: (value: number) => { |
| | | form.setFieldValue('invoiceId', undefined); |
| | | form.setFieldValue('planId', undefined); |
| | | form.setFieldValue('returnTime', undefined); |
| | | form.setFieldValue('returnType', undefined); |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'invoiceId', |
| | | label: 'å
³èå¼ç¥¨', |
| | | component: 'Select', |
| | | rules: 'required', |
| | | dependencies: { |
| | | triggerFields: ['contractId', 'invoiceId'], |
| | | disabled: (values) => !values.contractId || !!values.id, |
| | | async componentProps(values, form) { |
| | | if (!values.contractId) { |
| | | return { |
| | | options: [], |
| | | placeholder: '请å
éæ©åå', |
| | | }; |
| | | } |
| | | const invoices = await getInvoiceSimpleList(values.contractId); |
| | | return { |
| | | options: invoices.map((item) => { |
| | | const remain = item.remainingInvoicePrice ?? item.price ?? 0; |
| | | return { |
| | | label: `${item.no} / ${item.invoiceNo ?? '-'} / Â¥${item.price ?? 0} / å©ä½å¯å款 Â¥${remain.toFixed(2)}`, |
| | | value: item.id, |
| | | }; |
| | | }), |
| | | placeholder: 'è¯·éæ©å
³èå¼ç¥¨', |
| | | onChange: (value: number) => { |
| | | form.setFieldValue('planId', undefined); |
| | | form.setFieldValue('returnTime', undefined); |
| | | form.setFieldValue('returnType', undefined); |
| | | const invoice = invoices.find((item) => item.id === value); |
| | | if (invoice) { |
| | | form.setFieldValue( |
| | | 'price', |
| | | invoice.remainingInvoicePrice ?? invoice.price ?? undefined, |
| | | ); |
| | | form.setFieldValue('customerId', invoice.customerId); |
| | | } |
| | | }, |
| | | } as any; |
| | | }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'planId', |
| | | label: '忬¾ææ°', |
| | | component: 'Select', |
| | | rules: 'required', |
| | | help: 'æ¤åæ°å¯å¨å款计åé
ç½®', |
| | | dependencies: { |
| | | triggerFields: ['contractId'], |
| | | disabled: (values) => !values.contractId || values.id, |
| | |
| | | slots: { default: 'contractNo' }, |
| | | }, |
| | | { |
| | | title: 'å
³èå¼ç¥¨', |
| | | field: 'invoice', |
| | | minWidth: 200, |
| | | slots: { default: 'invoiceNo' }, |
| | | }, |
| | | { |
| | | title: '忬¾æ¥æ', |
| | | field: 'returnTime', |
| | | minWidth: 150, |
| | |
| | | title: 'å建人', |
| | | field: 'creatorName', |
| | | minWidth: 150, |
| | | }, |
| | | { |
| | | title: '忬¾ç¶æ', |
| | | field: 'auditStatus', |
| | | minWidth: 100, |
| | | fixed: 'right', |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.CRM_AUDIT_STATUS }, |
| | | }, |
| | | }, |
| | | { |
| | | title: 'æä½', |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { CrmReceivableApi } from '#/api/crm/receivable'; |
| | | import type { DescriptionItemSchema } from '#/components/description'; |
| | | |
| | | import { h, onMounted, ref } from 'vue'; |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { erpPriceInputFormatter, formatDateTime } from '@vben/utils'; |
| | | |
| | | import { Spin } from 'ant-design-vue'; |
| | | |
| | | import { getReceivable } from '#/api/crm/receivable'; |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | import { useDescription } from '#/components/description'; |
| | | |
| | | defineOptions({ name: 'CrmReceivableApproveDetail' }); |
| | | |
| | | const props = defineProps<{ id: string }>(); |
| | | |
| | | const loading = ref(false); |
| | | const formData = ref<CrmReceivableApi.Receivable>(); |
| | | |
| | | /** ææ¬åæ®µç©ºå¼å ä½ */ |
| | | function textRender(val: unknown) { |
| | | return val === 0 || val ? String(val) : '-'; |
| | | } |
| | | |
| | | /** å
³èå¼ç¥¨å±ç¤º */ |
| | | function invoiceRender(_val: unknown, data?: CrmReceivableApi.Receivable) { |
| | | if (!data?.invoice?.no) { |
| | | return '-'; |
| | | } |
| | | return `${data.invoice.no} / ${data.invoice.invoiceNo ?? '-'} / ¥${ |
| | | data.invoice.price ?? 0 |
| | | }`; |
| | | } |
| | | |
| | | const [Descriptions] = useDescription({ |
| | | bordered: true, |
| | | column: 2, |
| | | schema: [ |
| | | { label: '忬¾ç¼å·', field: 'no', render: textRender }, |
| | | { label: '客æ·åç§°', field: 'customerName', render: textRender }, |
| | | { |
| | | label: 'ååç¼å·', |
| | | field: 'contract', |
| | | render: (_val, data) => (data?.contract?.no ? data.contract.no : '-'), |
| | | }, |
| | | { label: 'å
³èå¼ç¥¨', field: 'invoice', render: invoiceRender }, |
| | | { |
| | | label: '忬¾æ¥æ', |
| | | field: 'returnTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { |
| | | label: '忬¾éé¢ï¼å
ï¼', |
| | | field: 'price', |
| | | render: (val) => erpPriceInputFormatter(val) || '-', |
| | | }, |
| | | { |
| | | label: '忬¾æ¹å¼', |
| | | field: 'returnType', |
| | | render: (val) => |
| | | h(DictTag, { |
| | | type: DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE, |
| | | value: val, |
| | | }), |
| | | }, |
| | | { |
| | | label: '审æ¹ç¶æ', |
| | | field: 'auditStatus', |
| | | render: (val) => |
| | | h(DictTag, { type: DICT_TYPE.CRM_AUDIT_STATUS, value: val }), |
| | | }, |
| | | { label: 'è´è´£äºº', field: 'ownerUserName', render: textRender }, |
| | | { label: 'å建人', field: 'creatorName', render: textRender }, |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { label: '夿³¨', field: 'remark', render: textRender }, |
| | | ] as DescriptionItemSchema[], |
| | | }); |
| | | |
| | | onMounted(async () => { |
| | | try { |
| | | loading.value = true; |
| | | formData.value = await getReceivable(Number(props.id)); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <ContentWrap class="m-2"> |
| | | <Spin :spinning="loading" tip="å è½½ä¸..."> |
| | | <div class="space-y-4"> |
| | | <!-- 忬¾åºæ¬ä¿¡æ¯ --> |
| | | <div class="rounded-lg border border-border bg-background p-4"> |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">忬¾åºæ¬ä¿¡æ¯</span> |
| | | </div> |
| | | <Descriptions :data="formData" /> |
| | | </div> |
| | | </div> |
| | | </Spin> |
| | | </ContentWrap> |
| | | </template> |
| | |
| | | val && data?.contract?.no ? data?.contract?.no : '', |
| | | }, |
| | | { |
| | | field: 'invoice', |
| | | label: 'å
³èå¼ç¥¨', |
| | | render: (val, data) => |
| | | val && data?.invoice?.no |
| | | ? `${data.invoice.no} / ${data.invoice.invoiceNo ?? '-'} / ¥${data.invoice.price ?? 0}` |
| | | : '', |
| | | }, |
| | | { |
| | | field: 'returnTime', |
| | | label: '忬¾æ¥æ', |
| | | render: (val) => formatDateTime(val) as string, |
| | |
| | | deleteReceivable, |
| | | exportReceivable, |
| | | getReceivablePage, |
| | | submitReceivable, |
| | | updateReceivableStatus, |
| | | } from '#/api/crm/receivable'; |
| | | import { $t } from '#/locales'; |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | /** æäº¤å®¡æ ¸ */ |
| | | async function handleSubmit(row: CrmReceivableApi.Receivable) { |
| | | /** æ´æ°åæ¬¾ç¶æï¼å®¡æ ¸/åå®¡æ ¸ï¼ */ |
| | | async function handleUpdateStatus( |
| | | row: CrmReceivableApi.Receivable, |
| | | status: number, |
| | | ) { |
| | | const hideLoading = message.loading({ |
| | | content: 'æäº¤å®¡æ ¸ä¸...', |
| | | content: 'æ´æ°ç¶æä¸...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await submitReceivable(row.id!); |
| | | message.success('æäº¤å®¡æ ¸æå'); |
| | | await updateReceivableStatus(row.id!, status); |
| | | message.success('æ´æ°ç¶ææå'); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | |
| | | push({ name: 'CrmContractDetail', params: { id: row.contractId } }); |
| | | } |
| | | |
| | | /** æ¥ç审æ¹è¯¦æ
*/ |
| | | function handleProcessDetail(row: CrmReceivableApi.Receivable) { |
| | | push({ |
| | | name: 'BpmProcessInstanceDetail', |
| | | query: { id: row.processInstanceId }, |
| | | }); |
| | | /** æ¥çå¼ç¥¨è¯¦æ
*/ |
| | | function handleInvoiceDetail(row: CrmReceivableApi.Receivable) { |
| | | push({ name: 'CrmInvoiceDetail', params: { id: row.invoiceId } }); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | |
| | | {{ row.customerName }} |
| | | </Button> |
| | | </template> |
| | | <template #invoiceNo="{ row }"> |
| | | <template v-if="row.invoice"> |
| | | <Button type="link" @click="handleInvoiceDetail(row)"> |
| | | {{ row.invoice.no }} |
| | | </Button> |
| | | <span class="mx-1">/</span> |
| | | <span>{{ row.invoice.invoiceNo ?? '-' }}</span> |
| | | <span class="mx-1">/</span> |
| | | <span>Â¥{{ row.invoice.price ?? 0 }}</span> |
| | | </template> |
| | | <span v-else>--</span> |
| | | </template> |
| | | <template #contractNo="{ row }"> |
| | | <Button |
| | | v-if="row.contract" |
| | |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['crm:receivable:update'], |
| | | ifShow: row.auditStatus === 10, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'æäº¤å®¡æ ¸', |
| | | label: 'å®¡æ ¸', |
| | | type: 'link', |
| | | icon: ACTION_ICON.AUDIT, |
| | | auth: ['crm:receivable:update'], |
| | | onClick: handleSubmit.bind(null, row), |
| | | ifShow: row.auditStatus === 0, |
| | | ifShow: row.auditStatus === 10, |
| | | onClick: handleUpdateStatus.bind(null, row, 20), |
| | | }, |
| | | { |
| | | label: 'æ¥ç审æ¹', |
| | | label: 'åå®¡æ ¸', |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['crm:receivable:update'], |
| | | onClick: handleProcessDetail.bind(null, row), |
| | | ifShow: row.auditStatus !== 0, |
| | | ifShow: row.auditStatus === 20, |
| | | onClick: handleUpdateStatus.bind(null, row, 10), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | |
| | | <script lang="ts" setup> |
| | | import type { CrmReceivableApi } from '#/api/crm/receivable'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | import { computed, ref, watch } from 'vue'; |
| | | |
| | | import { useVbenForm, useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | import { Alert, message } from 'ant-design-vue'; |
| | | |
| | | import { getContractSimpleList } from '#/api/crm/contract'; |
| | | import { |
| | |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formData = ref<Partial<CrmReceivableApi.Receivable>>(); |
| | | const showInvoiceHint = ref(false); |
| | | const getTitle = computed(() => { |
| | | return formData.value?.id |
| | | ? $t('ui.actionTitle.edit', ['忬¾']) |
| | |
| | | schema: useFormSchema(), |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | /** çå¬ååéæ©ï¼æç¤ºå
å¼ç¥¨å忬¾ */ |
| | | watch( |
| | | () => formApi.form?.values?.contractId, |
| | | (val) => { |
| | | showInvoiceHint.value = !!val; |
| | | }, |
| | | ); |
| | | |
| | | /** æå»ºæ°å¢å款çé¢å¡«è¡¨å */ |
| | | async function buildCreateFormData( |
| | |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-2/5"> |
| | | <Alert |
| | | v-if="showInvoiceHint" |
| | | type="warning" |
| | | message="请å
宿å¼ç¥¨å¹¶ä¸ä¼ å票éä»¶ï¼åè¿è¡å款æä½" |
| | | show-icon |
| | | class="mb-4" |
| | | /> |
| | | <Form class="mx-4" /> |
| | | </Modal> |
| | | </template> |
| | |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import CrmCustomerSelect from '#/components/crm-customer-select.vue'; |
| | | |
| | | import dayjs from 'dayjs'; |
| | | import { markRaw } from 'vue'; |
| | | |
| | | /** æ°å¢/ä¿®æ¹ç表å */ |
| | |
| | | valueFormat: 'YYYY-MM-DD', |
| | | format: 'YYYY-MM-DD', |
| | | }, |
| | | defaultValue: new Date(), |
| | | defaultValue: dayjs().format('YYYY-MM-DD'), |
| | | }, |
| | | { |
| | | fieldName: 'remindDays', |
| | |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | labelWidth: 120, |
| | | componentProps: { |
| | | class: 'w-full', |
| | | }, |
| | |
| | | return; |
| | | } |
| | | // å è½½æ°æ® |
| | | formData.value = undefined; |
| | | await formApi.resetForm(); |
| | | const data = modalApi.getData<CrmReceivablePlanApi.Plan>(); |
| | | if (!data || !data.id) { |
| | | // è®¾ç½®å° values |
| | | await formApi.setValues(data); |
| | | if (!data) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getReceivablePlan(data.id); |
| | | if (data.id) { |
| | | formData.value = await getReceivablePlan(data.id); |
| | | } |
| | | // è®¾ç½®å° values |
| | | await formApi.setValues(formData.value); |
| | | await formApi.setValues(formData.value ?? data); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-2/5"> |
| | | <Modal :title="getTitle" class="w-3/5"> |
| | | <Form class="mx-4" /> |
| | | </Modal> |
| | | </template> |
| | |
| | | formModalApi.setData(null).open(); |
| | | } |
| | | |
| | | /** AI è¯å«å½å
¥ */ |
| | | /** è¯å«å½å
¥ */ |
| | | function handleOcrCreate() { |
| | | ocrUploadRef.value?.open(); |
| | | } |
| | |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: 'AI è¯å«å½å
¥', |
| | | label: 'è¯å«å½å
¥', |
| | | type: 'default', |
| | | auth: ['crm:sale-quotation:create'], |
| | | onClick: handleOcrCreate, |
| | |
| | | type="info" |
| | | show-icon |
| | | class="mx-4 mb-4" |
| | | message="AI è¯å«å°å®¢æ·åç§°ï¼è¯·å¨å®¢æ·éæ©å¨ä¸æå¨å¹é
CRM 客æ·" |
| | | message="è¯å«å°å®¢æ·åç§°ï¼è¯·å¨å®¢æ·éæ©å¨ä¸æå¨å¹é
CRM 客æ·" |
| | | :description="`è¯å«ç»æï¼${ocrCustomerName}`" |
| | | /> |
| | | <Form class="mx-4"> |
| | |
| | | onSuccess({ blobId }, file); |
| | | // å¼å§ OCR è¯å« |
| | | step.value = 1; |
| | | processingTip.value = 'æ£å¨è¿è¡ AI OCR è¯å«ï¼è¯·ç¨å...'; |
| | | processingTip.value = 'æ£å¨è¿è¡æ¥ä»·åè¯å«ï¼è¯·ç¨å...'; |
| | | const ocrData = await ocrSaleQuotation(blobId); |
| | | // æ£æ¥é误 |
| | | if (ocrData.rawText && !ocrData.name && !ocrData.customerName && ocrData.items?.length === 0) { |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal v-model:open="open" title="AI è¯å«å½å
¥æ¥ä»·å" width="680px" :footer="null" @cancel="handleClose"> |
| | | <Modal v-model:open="open" title="è¯å«å½å
¥æ¥ä»·å" width="680px" :footer="null" @cancel="handleClose"> |
| | | <!-- Step 0: ä¸ä¼ --> |
| | | <template v-if="step === 0"> |
| | | <Upload.Dragger |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { EChartsOption } from '@vben/plugins/echarts'; |
| | | |
| | | import type { CrmSalesTargetApi } from '#/api/crm/salesTarget'; |
| | | |
| | | /** è¾¾æåæææ Tab å®ä¹ */ |
| | | export const ANALYSIS_TABS = [ |
| | | { key: 'contract', label: 'ååéé¢', unit: 'å
' }, |
| | | { key: 'receivable', label: '忬¾éé¢', unit: 'å
' }, |
| | | { key: 'business', label: 'åæºéé¢', unit: 'å
' }, |
| | | { key: 'customer', label: 'æ°å¢å®¢æ·', unit: '个' }, |
| | | { key: 'contractCount', label: 'æ°å¢åå', unit: '个' }, |
| | | ] as const; |
| | | |
| | | export type AnalysisTabKey = (typeof ANALYSIS_TABS)[number]['key']; |
| | | |
| | | type MetricValue = { |
| | | target: number; |
| | | actual: number; |
| | | rate: number | null; |
| | | }; |
| | | |
| | | const METRICS: Record< |
| | | AnalysisTabKey, |
| | | { label: string; unit: string; pick: (item: CrmSalesTargetApi.SalesTarget) => MetricValue } |
| | | > = { |
| | | contract: { |
| | | label: 'ååéé¢', |
| | | unit: 'å
', |
| | | pick: (item) => ({ |
| | | target: item.contractTarget ?? 0, |
| | | actual: item.actualContractPrice ?? 0, |
| | | rate: item.contractRate ?? null, |
| | | }), |
| | | }, |
| | | receivable: { |
| | | label: '忬¾éé¢', |
| | | unit: 'å
', |
| | | pick: (item) => ({ |
| | | target: item.receivableTarget ?? 0, |
| | | actual: item.actualReceivablePrice ?? 0, |
| | | rate: item.receivableRate ?? null, |
| | | }), |
| | | }, |
| | | business: { |
| | | label: 'åæºéé¢', |
| | | unit: 'å
', |
| | | pick: (item) => ({ |
| | | target: item.businessTarget ?? 0, |
| | | actual: item.actualBusinessPrice ?? 0, |
| | | rate: item.businessRate ?? null, |
| | | }), |
| | | }, |
| | | customer: { |
| | | label: 'æ°å¢å®¢æ·', |
| | | unit: '个', |
| | | pick: (item) => ({ |
| | | target: item.customerTarget ?? 0, |
| | | actual: item.actualCustomerCount ?? 0, |
| | | rate: item.customerRate ?? null, |
| | | }), |
| | | }, |
| | | contractCount: { |
| | | label: 'æ°å¢åå', |
| | | unit: '个', |
| | | pick: (item) => ({ |
| | | target: item.contractCountTarget ?? 0, |
| | | actual: item.actualContractCount ?? 0, |
| | | rate: item.contractCountRate ?? null, |
| | | }), |
| | | }, |
| | | }; |
| | | |
| | | function formatNumber(value: number | null | undefined): string { |
| | | return value == null ? '-' : value.toLocaleString('zh-CN'); |
| | | } |
| | | |
| | | /** çæææ å¯¹æ¯æ±ç¶å¾ï¼ç®æ vs å®é
ï¼é
ç½® */ |
| | | export function getChartOptions( |
| | | tabKey: AnalysisTabKey, |
| | | list: CrmSalesTargetApi.SalesTarget[], |
| | | ): EChartsOption { |
| | | const metric = METRICS[tabKey]; |
| | | const valueName = `${metric.label}ï¼${metric.unit}ï¼`; |
| | | return { |
| | | dataset: { |
| | | dimensions: ['name', 'target', 'actual', 'rate'], |
| | | source: list.map((item) => { |
| | | const value = metric.pick(item); |
| | | return { |
| | | name: item.name ?? '--', |
| | | target: value.target, |
| | | actual: value.actual, |
| | | rate: value.rate, |
| | | }; |
| | | }), |
| | | }, |
| | | grid: { left: 20, right: 20, bottom: 20, containLabel: true }, |
| | | legend: { top: 10 }, |
| | | series: [ |
| | | { name: 'ç®æ ', type: 'bar', barMaxWidth: 24 }, |
| | | { name: 'å®é
', type: 'bar', barMaxWidth: 24 }, |
| | | ], |
| | | toolbox: { |
| | | feature: { |
| | | saveAsImage: { show: true, name: `${metric.label}è¾¾æå¯¹æ¯` }, |
| | | }, |
| | | }, |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { type: 'shadow' }, |
| | | formatter: (params: unknown) => { |
| | | const list = params as Array<{ |
| | | data: { name: string; target: number; actual: number; rate: number | null }; |
| | | }>; |
| | | const row = list[0]?.data; |
| | | if (!row) { |
| | | return ''; |
| | | } |
| | | return [ |
| | | `<b>${row.name}</b>`, |
| | | `ç®æ ï¼${formatNumber(row.target)}`, |
| | | `å®é
ï¼${formatNumber(row.actual)}`, |
| | | `è¾¾æçï¼${row.rate == null ? '-' : `${row.rate}%`}`, |
| | | ].join('<br/>'); |
| | | }, |
| | | }, |
| | | xAxis: { type: 'category', name: 'ç®æ åç§°' }, |
| | | yAxis: { type: 'value', name: valueName }, |
| | | } as EChartsOption; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | /** è¾¾æåæçæ¥è¯¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'periodType', |
| | | label: 'å¨æç±»å', |
| | | component: 'Select', |
| | | rules: 'required', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.CRM_SALES_TARGET_PERIOD_TYPE, 'number'), |
| | | placeholder: 'è¯·éæ©å¨æç±»å', |
| | | }, |
| | | defaultValue: 1, |
| | | }, |
| | | { |
| | | fieldName: 'periodValue', |
| | | label: '卿å¼', |
| | | component: 'Input', |
| | | rules: 'required', |
| | | componentProps: { |
| | | placeholder: 'å¦ 2026 / 2026-Q1 / 2026-07', |
| | | }, |
| | | defaultValue: new Date().getFullYear().toString(), |
| | | }, |
| | | { |
| | | fieldName: 'targetDimension', |
| | | label: 'ç®æ 维度', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.CRM_SALES_TARGET_DIMENSION, 'number'), |
| | | placeholder: 'å
¨é¨', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.CRM_SALES_TARGET_STATUS, 'number'), |
| | | placeholder: 'å
¨é¨', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** è¾¾æåæçè¡¨æ ¼åæ®µ */ |
| | | export function useGridColumns(): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { |
| | | field: 'name', |
| | | title: 'ç®æ åç§°', |
| | | minWidth: 200, |
| | | fixed: 'left', |
| | | }, |
| | | { |
| | | field: 'targetUserName', |
| | | title: 'ç®æ 对象', |
| | | minWidth: 160, |
| | | slots: { default: 'target' }, |
| | | }, |
| | | { |
| | | field: 'contractTarget', |
| | | title: 'ååéé¢ç®æ ', |
| | | minWidth: 130, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | field: 'actualContractPrice', |
| | | title: 'å®é
ååéé¢', |
| | | minWidth: 130, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | field: 'contractRate', |
| | | title: 'ååè¾¾æç(%)', |
| | | minWidth: 120, |
| | | slots: { default: 'contractRate' }, |
| | | }, |
| | | { |
| | | field: 'receivableTarget', |
| | | title: '忬¾éé¢ç®æ ', |
| | | minWidth: 130, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | field: 'actualReceivablePrice', |
| | | title: 'å®é
忬¾éé¢', |
| | | minWidth: 130, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | field: 'receivableRate', |
| | | title: '忬¾è¾¾æç(%)', |
| | | minWidth: 120, |
| | | slots: { default: 'receivableRate' }, |
| | | }, |
| | | { |
| | | field: 'businessTarget', |
| | | title: 'åæºéé¢ç®æ ', |
| | | minWidth: 130, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | field: 'actualBusinessPrice', |
| | | title: 'å®é
åæºéé¢', |
| | | minWidth: 130, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | field: 'businessRate', |
| | | title: 'åæºè¾¾æç(%)', |
| | | minWidth: 120, |
| | | slots: { default: 'businessRate' }, |
| | | }, |
| | | { |
| | | field: 'customerTarget', |
| | | title: 'æ°å¢å®¢æ·ç®æ ', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'actualCustomerCount', |
| | | title: 'å®é
æ°å¢å®¢æ·', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'customerRate', |
| | | title: '客æ·è¾¾æç(%)', |
| | | minWidth: 120, |
| | | slots: { default: 'customerRate' }, |
| | | }, |
| | | { |
| | | field: 'contractCountTarget', |
| | | title: 'æ°å¢ååç®æ ', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'actualContractCount', |
| | | title: 'å®é
æ°å¢åå', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'contractCountRate', |
| | | title: 'ååæ°è¾¾æç(%)', |
| | | minWidth: 120, |
| | | slots: { default: 'contractCountRate' }, |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | minWidth: 100, |
| | | fixed: 'right', |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.CRM_SALES_TARGET_STATUS }, |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { EchartsUIType } from '@vben/plugins/echarts'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { CrmSalesTargetApi } from '#/api/crm/salesTarget'; |
| | | |
| | | import { onMounted, ref } from 'vue'; |
| | | |
| | | import { ContentWrap, Page } from '@vben/common-ui'; |
| | | import { EchartsUI, useEcharts } from '@vben/plugins/echarts'; |
| | | |
| | | import { Tabs } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getSalesTargetAnalysis } from '#/api/crm/salesTarget'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { |
| | | ANALYSIS_TABS, |
| | | getChartOptions, |
| | | type AnalysisTabKey, |
| | | } from './chart-options'; |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | |
| | | const activeTab = ref<string>('contract'); |
| | | const chartRef = ref<EchartsUIType>(); |
| | | const { renderEcharts } = useEcharts(chartRef); |
| | | |
| | | /** æå½å表åå¼ + å½åææ Tab å è½½å¾è¡¨ä¸è¡¨æ ¼ */ |
| | | async function handleLoad() { |
| | | const formValues = await formApi.getValues(); |
| | | const res = await getSalesTargetAnalysis(formValues); |
| | | await renderEcharts(getChartOptions(activeTab.value as AnalysisTabKey, res)); |
| | | await gridApi.grid.reloadData(res); |
| | | } |
| | | |
| | | const [QueryForm, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { |
| | | class: 'w-full', |
| | | }, |
| | | }, |
| | | schema: useGridFormSchema(), |
| | | showCollapseButton: true, |
| | | submitButtonOptions: { |
| | | content: $t('common.query'), |
| | | }, |
| | | wrapperClass: 'grid-cols-1 md:grid-cols-4', |
| | | handleSubmit: handleLoad, |
| | | }); |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | pagerConfig: { |
| | | enabled: false, |
| | | }, |
| | | proxyConfig: { |
| | | autoLoad: false, // å¾è¡¨ä¸è¡¨æ ¼ç»ä¸èµ° handleLoad æå¨å è½½ï¼é¿å
ç©ºåæ°è¯·æ± |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | toolbarConfig: { |
| | | enabled: false, |
| | | }, |
| | | } as VxeTableGridOptions<CrmSalesTargetApi.SalesTarget>, |
| | | }); |
| | | |
| | | /** ææ Tab 忢 */ |
| | | function handleTabChange(key: string | number) { |
| | | activeTab.value = String(key); |
| | | handleLoad(); |
| | | } |
| | | |
| | | onMounted(async () => { |
| | | // é置表å为é»è®¤å¨æï¼å½å年份ï¼ï¼é»è®¤å è½½ä¸æ¬¡è¾¾æåæ |
| | | await formApi.resetForm(); |
| | | await handleLoad(); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <ContentWrap> |
| | | <QueryForm /> |
| | | <Tabs |
| | | v-model:active-key="activeTab" |
| | | class="w-full" |
| | | @change="handleTabChange" |
| | | > |
| | | <Tabs.TabPane |
| | | v-for="item in ANALYSIS_TABS" |
| | | :key="item.key" |
| | | :tab="item.label" |
| | | :force-render="true" |
| | | /> |
| | | </Tabs> |
| | | <EchartsUI class="mb-4 w-full" height="380px" ref="chartRef" /> |
| | | <Grid> |
| | | <template #target="{ row }"> |
| | | <span> |
| | | {{ |
| | | row.targetUserName || |
| | | row.targetDeptName || |
| | | row.targetItemName || |
| | | '--' |
| | | }} |
| | | </span> |
| | | </template> |
| | | <template #contractRate="{ row }"> |
| | | <span :class="row.contractRate == null ? '' : 'font-medium'"> |
| | | {{ row.contractRate == null ? '--' : `${row.contractRate}%` }} |
| | | </span> |
| | | </template> |
| | | <template #receivableRate="{ row }"> |
| | | <span :class="row.receivableRate == null ? '' : 'font-medium'"> |
| | | {{ row.receivableRate == null ? '--' : `${row.receivableRate}%` }} |
| | | </span> |
| | | </template> |
| | | <template #businessRate="{ row }"> |
| | | <span :class="row.businessRate == null ? '' : 'font-medium'"> |
| | | {{ row.businessRate == null ? '--' : `${row.businessRate}%` }} |
| | | </span> |
| | | </template> |
| | | <template #customerRate="{ row }"> |
| | | <span :class="row.customerRate == null ? '' : 'font-medium'"> |
| | | {{ row.customerRate == null ? '--' : `${row.customerRate}%` }} |
| | | </span> |
| | | </template> |
| | | <template #contractCountRate="{ row }"> |
| | | <span :class="row.contractCountRate == null ? '' : 'font-medium'"> |
| | | {{ |
| | | row.contractCountRate == null |
| | | ? '--' |
| | | : `${row.contractCountRate}%` |
| | | }} |
| | | </span> |
| | | </template> |
| | | </Grid> |
| | | </ContentWrap> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | import { handleTree } from '@vben/utils'; |
| | | |
| | | import { getSimpleDeptList } from '#/api/system/dept'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import { MdmItemSelect } from '#/views/basicData/mdm/components'; |
| | | |
| | | import { markRaw } from 'vue'; |
| | | |
| | | /** ç®æ 维度æä¸¾ */ |
| | | export const SALES_TARGET_DIMENSION = { |
| | | USER: 1, // 个人 |
| | | DEPT: 2, // é¨é¨ |
| | | ITEM: 3, // 产å |
| | | } as const; |
| | | |
| | | /** å¨æç±»åæä¸¾ */ |
| | | export const SALES_TARGET_PERIOD_TYPE = { |
| | | YEAR: 1, // å¹´ |
| | | QUARTER: 2, // å£åº¦ |
| | | MONTH: 3, // æ |
| | | } as const; |
| | | |
| | | /** 卿å¼è¾å
¥å ä½æç¤º */ |
| | | const PERIOD_VALUE_PLACEHOLDER: Record<number, string> = { |
| | | [SALES_TARGET_PERIOD_TYPE.YEAR]: '请è¾å
¥å¹´ä»½ï¼å¦ 2026', |
| | | [SALES_TARGET_PERIOD_TYPE.QUARTER]: '请è¾å
¥å£åº¦ï¼å¦ 2026-Q1', |
| | | [SALES_TARGET_PERIOD_TYPE.MONTH]: '请è¾å
¥æä»½ï¼å¦ 2026-07', |
| | | }; |
| | | |
| | | /** æ°å¢/ä¿®æ¹ç表å */ |
| | | export function useFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => false, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'name', |
| | | label: 'ç®æ åç§°', |
| | | component: 'Input', |
| | | rules: 'required', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥ç®æ åç§°ï¼å¦ 2026 年度éå®ç®æ ', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'periodType', |
| | | label: 'å¨æç±»å', |
| | | component: 'Select', |
| | | rules: 'required', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.CRM_SALES_TARGET_PERIOD_TYPE, 'number'), |
| | | placeholder: 'è¯·éæ©å¨æç±»å', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'periodValue', |
| | | label: '卿å¼', |
| | | component: 'Input', |
| | | rules: 'required', |
| | | dependencies: { |
| | | triggerFields: ['periodType'], |
| | | async componentProps(values) { |
| | | const placeholder = |
| | | PERIOD_VALUE_PLACEHOLDER[values.periodType] ?? '请è¾å
¥å¨æå¼'; |
| | | return { placeholder }; |
| | | }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'targetDimension', |
| | | label: 'ç®æ 维度', |
| | | component: 'Select', |
| | | rules: 'required', |
| | | componentProps: (_values, form) => ({ |
| | | options: getDictOptions(DICT_TYPE.CRM_SALES_TARGET_DIMENSION, 'number'), |
| | | placeholder: 'è¯·éæ©ç®æ 维度', |
| | | onChange: () => { |
| | | form.setFieldValue('targetUserId', undefined); |
| | | form.setFieldValue('targetDeptId', undefined); |
| | | form.setFieldValue('targetItemId', undefined); |
| | | }, |
| | | }), |
| | | }, |
| | | { |
| | | fieldName: 'targetUserId', |
| | | label: 'ç®æ ç¨æ·', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | api: getSimpleUserList, |
| | | labelField: 'nickname', |
| | | valueField: 'id', |
| | | placeholder: 'è¯·éæ©ç®æ ç¨æ·', |
| | | allowClear: true, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['targetDimension'], |
| | | show: (values) => values.targetDimension === SALES_TARGET_DIMENSION.USER, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'targetDeptId', |
| | | label: 'ç®æ é¨é¨', |
| | | component: 'ApiTreeSelect', |
| | | componentProps: { |
| | | api: async () => { |
| | | const data = await getSimpleDeptList(); |
| | | return handleTree(data); |
| | | }, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | childrenField: 'children', |
| | | treeDefaultExpandAll: true, |
| | | placeholder: 'è¯·éæ©ç®æ é¨é¨', |
| | | allowClear: true, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['targetDimension'], |
| | | show: (values) => values.targetDimension === SALES_TARGET_DIMENSION.DEPT, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'targetItemId', |
| | | label: 'ç®æ 产å', |
| | | component: markRaw(MdmItemSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©ç®æ 产å', |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['targetDimension'], |
| | | show: (values) => values.targetDimension === SALES_TARGET_DIMENSION.ITEM, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'contractTarget', |
| | | label: 'ååéé¢ç®æ ï¼å
ï¼', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | min: 0, |
| | | precision: 2, |
| | | placeholder: '请è¾å
¥ååéé¢ç®æ ', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'receivableTarget', |
| | | label: '忬¾éé¢ç®æ ï¼å
ï¼', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | min: 0, |
| | | precision: 2, |
| | | placeholder: '请è¾å
¥å款éé¢ç®æ ', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'businessTarget', |
| | | label: 'åæºéé¢ç®æ ï¼å
ï¼', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | min: 0, |
| | | precision: 2, |
| | | placeholder: '请è¾å
¥åæºéé¢ç®æ ', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'customerTarget', |
| | | label: 'æ°å¢å®¢æ·æ°ç®æ ', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | min: 0, |
| | | precision: 0, |
| | | placeholder: '请è¾å
¥æ°å¢å®¢æ·æ°ç®æ ', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'contractCountTarget', |
| | | label: 'æ°å¢ååæ°ç®æ ', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | min: 0, |
| | | precision: 0, |
| | | placeholder: '请è¾å
¥æ°å¢ååæ°ç®æ ', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å¤æ³¨', |
| | | rows: 3, |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表çæç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'name', |
| | | label: 'ç®æ åç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥ç®æ åç§°', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'periodType', |
| | | label: 'å¨æç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.CRM_SALES_TARGET_PERIOD_TYPE, 'number'), |
| | | placeholder: 'è¯·éæ©å¨æç±»å', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'periodValue', |
| | | label: '卿å¼', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: 'å¦ 2026 / 2026-Q1 / 2026-07', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'targetDimension', |
| | | label: 'ç®æ 维度', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.CRM_SALES_TARGET_DIMENSION, 'number'), |
| | | placeholder: 'è¯·éæ©ç®æ 维度', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.CRM_SALES_TARGET_STATUS, 'number'), |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表çåæ®µ */ |
| | | export function useGridColumns(): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { |
| | | field: 'name', |
| | | title: 'ç®æ åç§°', |
| | | minWidth: 200, |
| | | fixed: 'left', |
| | | }, |
| | | { |
| | | field: 'periodType', |
| | | title: 'å¨æç±»å', |
| | | minWidth: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.CRM_SALES_TARGET_PERIOD_TYPE }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'periodValue', |
| | | title: '卿å¼', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'targetUserName', |
| | | title: 'ç®æ 对象', |
| | | minWidth: 160, |
| | | slots: { default: 'target' }, |
| | | }, |
| | | { |
| | | field: 'contractTarget', |
| | | title: 'ååéé¢', |
| | | minWidth: 200, |
| | | slots: { default: 'contract' }, |
| | | }, |
| | | { |
| | | field: 'receivableTarget', |
| | | title: '忬¾éé¢', |
| | | minWidth: 200, |
| | | slots: { default: 'receivable' }, |
| | | }, |
| | | { |
| | | field: 'businessTarget', |
| | | title: 'åæºéé¢', |
| | | minWidth: 200, |
| | | slots: { default: 'business' }, |
| | | }, |
| | | { |
| | | field: 'customerTarget', |
| | | title: 'æ°å¢å®¢æ·', |
| | | minWidth: 170, |
| | | slots: { default: 'customer' }, |
| | | }, |
| | | { |
| | | field: 'contractCountTarget', |
| | | title: 'æ°å¢åå', |
| | | minWidth: 170, |
| | | slots: { default: 'contractCount' }, |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | minWidth: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.CRM_SALES_TARGET_STATUS }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | minWidth: 160, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | field: 'creatorName', |
| | | title: 'å建人', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 180, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { CrmSalesTargetApi } from '#/api/crm/salesTarget'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | changeSalesTargetStatus, |
| | | deleteSalesTarget, |
| | | exportSalesTarget, |
| | | getSalesTargetPage, |
| | | } from '#/api/crm/salesTarget'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** 导åºè¡¨æ ¼ */ |
| | | async function handleExport() { |
| | | const formValues = await gridApi.formApi.getValues(); |
| | | const data = await exportSalesTarget(formValues); |
| | | downloadFileFromBlobPart({ fileName: 'éå®ç®æ .xls', source: data }); |
| | | } |
| | | |
| | | /** æ°å¢éå®ç®æ */ |
| | | function handleCreate() { |
| | | formModalApi.setData(null).open(); |
| | | } |
| | | |
| | | /** ç¼è¾éå®ç®æ */ |
| | | function handleEdit(row: CrmSalesTargetApi.SalesTarget) { |
| | | formModalApi.setData({ salesTarget: row }).open(); |
| | | } |
| | | |
| | | /** ä½åº/æ¢å¤éå®ç®æ */ |
| | | async function handleChangeStatus(row: CrmSalesTargetApi.SalesTarget) { |
| | | const nextStatus = row.status === 3 ? 0 : 3; // ä½åº <-> æ¢å¤ |
| | | const actionText = row.status === 3 ? 'æ¢å¤' : 'ä½åº'; |
| | | const hideLoading = message.loading({ |
| | | content: `${actionText}ä¸...`, |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await changeSalesTargetStatus({ id: row.id!, status: nextStatus }); |
| | | message.success(`${actionText}æå`); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** å é¤éå®ç®æ */ |
| | | async function handleDelete(row: CrmSalesTargetApi.SalesTarget) { |
| | | const hideLoading = message.loading({ |
| | | content: $t('ui.actionMessage.deleting', [row.name!]), |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await deleteSalesTarget(row.id!); |
| | | message.success($t('ui.actionMessage.deleteSuccess', [row.name!])); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getSalesTargetPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | height: 64, // æ¾å¤§è¡é«ï¼å®¹çº³ç®æ /å®é
/è¾¾æçä¸è¡å
容 |
| | | }, |
| | | toolbarConfig: { |
| | | refresh: true, |
| | | search: true, |
| | | }, |
| | | } as VxeTableGridOptions<CrmSalesTargetApi.SalesTarget>, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <Grid> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'æ°å¢éå®ç®æ ', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['crm:sales-target:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.export'), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.DOWNLOAD, |
| | | auth: ['crm:sales-target:export'], |
| | | onClick: handleExport, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #target="{ row }"> |
| | | <span> |
| | | {{ |
| | | row.targetUserName || |
| | | row.targetDeptName || |
| | | row.targetItemName || |
| | | '--' |
| | | }} |
| | | </span> |
| | | </template> |
| | | <template #contract="{ row }"> |
| | | <div class="text-xs leading-5"> |
| | | <div>ç®æ ï¼{{ row.contractTarget ?? '-' }}</div> |
| | | <div>å®é
ï¼{{ row.actualContractPrice ?? '-' }}</div> |
| | | <div>è¾¾æçï¼{{ row.contractRate ?? '-' }}%</div> |
| | | </div> |
| | | </template> |
| | | <template #receivable="{ row }"> |
| | | <div class="text-xs leading-5"> |
| | | <div>ç®æ ï¼{{ row.receivableTarget ?? '-' }}</div> |
| | | <div>å®é
ï¼{{ row.actualReceivablePrice ?? '-' }}</div> |
| | | <div>è¾¾æçï¼{{ row.receivableRate ?? '-' }}%</div> |
| | | </div> |
| | | </template> |
| | | <template #business="{ row }"> |
| | | <div class="text-xs leading-5"> |
| | | <div>ç®æ ï¼{{ row.businessTarget ?? '-' }}</div> |
| | | <div>å®é
ï¼{{ row.actualBusinessPrice ?? '-' }}</div> |
| | | <div>è¾¾æçï¼{{ row.businessRate ?? '-' }}%</div> |
| | | </div> |
| | | </template> |
| | | <template #customer="{ row }"> |
| | | <div class="text-xs leading-5"> |
| | | <div>ç®æ ï¼{{ row.customerTarget ?? '-' }}</div> |
| | | <div>å®é
ï¼{{ row.actualCustomerCount ?? '-' }}</div> |
| | | <div>è¾¾æçï¼{{ row.customerRate ?? '-' }}%</div> |
| | | </div> |
| | | </template> |
| | | <template #contractCount="{ row }"> |
| | | <div class="text-xs leading-5"> |
| | | <div>ç®æ ï¼{{ row.contractCountTarget ?? '-' }}</div> |
| | | <div>å®é
ï¼{{ row.actualContractCount ?? '-' }}</div> |
| | | <div>è¾¾æçï¼{{ row.contractCountRate ?? '-' }}%</div> |
| | | </div> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['crm:sales-target:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | ifShow: row.status !== 3, |
| | | }, |
| | | { |
| | | label: row.status === 3 ? 'æ¢å¤' : 'ä½åº', |
| | | type: 'link', |
| | | auth: ['crm:sales-target:update'], |
| | | popConfirm: { |
| | | title: |
| | | row.status === 3 ? '确认æ¢å¤è¯¥ç®æ ï¼' : '确认ä½åºè¯¥ç®æ ï¼', |
| | | confirm: handleChangeStatus.bind(null, row), |
| | | }, |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['crm:sales-target:delete'], |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.name!]), |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { CrmSalesTargetApi } from '#/api/crm/salesTarget'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenForm, useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | createSalesTarget, |
| | | getSalesTarget, |
| | | updateSalesTarget, |
| | | } from '#/api/crm/salesTarget'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | |
| | | type SalesTargetFormModalData = { |
| | | salesTarget?: Pick<CrmSalesTargetApi.SalesTarget, 'id'>; |
| | | }; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formData = ref<Partial<CrmSalesTargetApi.SalesTarget>>(); |
| | | const getTitle = computed(() => { |
| | | return formData.value?.id |
| | | ? $t('ui.actionTitle.edit', ['éå®ç®æ ']) |
| | | : $t('ui.actionTitle.create', ['éå®ç®æ ']); |
| | | }); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { |
| | | class: 'w-full', |
| | | }, |
| | | labelWidth: 150, |
| | | }, |
| | | wrapperClass: 'grid-cols-2', |
| | | layout: 'horizontal', |
| | | schema: useFormSchema(), |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | const data = (await formApi.getValues()) as CrmSalesTargetApi.SalesTarget; |
| | | if (formData.value?.id) { |
| | | data.id = formData.value.id; |
| | | } |
| | | try { |
| | | await (formData.value?.id |
| | | ? updateSalesTarget(data) |
| | | : createSalesTarget(data)); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | formData.value = undefined; |
| | | return; |
| | | } |
| | | formData.value = undefined; |
| | | await formApi.resetForm(); |
| | | const data = modalApi.getData() as null | SalesTargetFormModalData; |
| | | if (!data?.salesTarget?.id) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getSalesTarget(data.salesTarget.id); |
| | | if (formData.value) { |
| | | await formApi.setValues(formData.value as any); |
| | | } |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-3/5"> |
| | | <Form class="mx-4" /> |
| | | </Modal> |
| | | </template> |
| | |
| | | yAxisIndex: 1, |
| | | data: res.map((s: any) => |
| | | s.lastMonthCount === 0 |
| | | ? 'NULL' |
| | | ? (s.currentMonthCount > 0 ? 100 : 0).toFixed(2) |
| | | : ( |
| | | ((s.currentMonthCount - s.lastMonthCount) / |
| | | s.lastMonthCount) * |
| | |
| | | yAxisIndex: 1, |
| | | data: res.map((s: any) => |
| | | s.lastYearCount === 0 |
| | | ? 'NULL' |
| | | ? (s.currentMonthCount > 0 ? 100 : 0).toFixed(2) |
| | | : ( |
| | | ((s.currentMonthCount - s.lastYearCount) / |
| | | s.lastYearCount) * |
| | |
| | | yAxisIndex: 1, |
| | | data: res.map((s: any) => |
| | | s.lastMonthCount === 0 |
| | | ? 'NULL' |
| | | ? (s.currentMonthCount > 0 ? 100 : 0).toFixed(2) |
| | | : ( |
| | | ((s.currentMonthCount - s.lastMonthCount) / |
| | | s.lastMonthCount) * |
| | |
| | | yAxisIndex: 1, |
| | | data: res.map((s: any) => |
| | | s.lastYearCount === 0 |
| | | ? 'NULL' |
| | | ? (s.currentMonthCount > 0 ? 100 : 0).toFixed(2) |
| | | : ( |
| | | ((s.currentMonthCount - s.lastYearCount) / |
| | | s.lastYearCount) * |
| | |
| | | yAxisIndex: 1, |
| | | data: res.map((s: any) => |
| | | s.lastMonthCount === 0 |
| | | ? 'NULL' |
| | | ? (s.currentMonthCount > 0 ? 100 : 0).toFixed(2) |
| | | : ( |
| | | ((s.currentMonthCount - s.lastMonthCount) / |
| | | s.lastMonthCount) * |
| | |
| | | yAxisIndex: 1, |
| | | data: res.map((s: any) => |
| | | s.lastYearCount === 0 |
| | | ? 'NULL' |
| | | ? (s.currentMonthCount > 0 ? 100 : 0).toFixed(2) |
| | | : ( |
| | | ((s.currentMonthCount - s.lastYearCount) / |
| | | s.lastYearCount) * |
| | |
| | | tableData[2][`field${index}`] = item.lastYearCount; |
| | | tableData[3][`field${index}`] = |
| | | item.lastMonthCount === 0 |
| | | ? 'NULL' |
| | | ? (item.currentMonthCount > 0 ? 100 : 0).toFixed(2) |
| | | : ( |
| | | ((item.currentMonthCount - item.lastMonthCount) / |
| | | item.lastMonthCount) * |
| | |
| | | ).toFixed(2); |
| | | tableData[4][`field${index}`] = |
| | | item.lastYearCount === 0 |
| | | ? 'NULL' |
| | | ? (item.currentMonthCount > 0 ? 100 : 0).toFixed(2) |
| | | : ( |
| | | ((item.currentMonthCount - item.lastYearCount) / |
| | | item.lastYearCount) * |
| | |
| | | </Col> |
| | | <Col :lg="10" :md="24" :sm="24" :xl="10" :xs="24" class="mb-4"> |
| | | <AlertPanel |
| | | :andon-active-count="summary.andonActiveCount" |
| | | :repair-active-count="summary.repairActiveCount" |
| | | @navigate="handleNavigate" |
| | | /> |
| | |
| | | defineOptions({ name: 'DashboardAlertPanel' }); |
| | | |
| | | const props = defineProps<{ |
| | | andonActiveCount: number; |
| | | repairActiveCount: number; |
| | | }>(); |
| | | |
| | |
| | | <template> |
| | | <div class="space-y-4"> |
| | | <!-- é¢è¦å¡çè¡ --> |
| | | <div class="grid grid-cols-2 gap-3"> |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0 p-4" |
| | | @click="emit('navigate', 'MesProAndon')" |
| | | > |
| | | <div class="relative z-10 flex items-center gap-3"> |
| | | <div class="flex size-10 items-center justify-center rounded-lg bg-red-50 text-red-600 shadow-sm dark:bg-red-500/15 dark:text-red-400"> |
| | | <IconifyIcon class="size-5" icon="lucide:alert-triangle" /> |
| | | </div> |
| | | <div> |
| | | <div class="text-xs text-gray-500 dark:text-gray-400">å®ç¯å¼å«</div> |
| | | <div class="flex items-baseline gap-1"> |
| | | <CountTo class="text-xl font-bold text-red-600 dark:text-red-400" :end-val="andonActiveCount" :duration="1000" /> |
| | | <span class="text-xs text-gray-400">æªå¤ç½®</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="grid grid-cols-1 gap-3"> |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0 p-4" |
| | | @click="emit('navigate', 'MesProRepair')" |
| | |
| | | { title: 'è´¨æ£ç®¡ç', icon: 'lucide:clipboard-check', route: 'MesQcRecord', gradient: 'from-orange-500 to-amber-500', shadow: 'shadow-orange-500/15' }, |
| | | { title: '设å¤ç¹æ£', icon: 'lucide:settings', route: 'MesDvMachinery', gradient: 'from-violet-500 to-purple-500', shadow: 'shadow-violet-500/15' }, |
| | | { title: 'ç产æç¨', icon: 'lucide:calendar-range', route: 'MesProScheduling', gradient: 'from-cyan-500 to-teal-500', shadow: 'shadow-cyan-500/15' }, |
| | | { title: 'å®ç¯å¼å«', icon: 'lucide:bell-ring', route: 'MesProAndon', gradient: 'from-red-500 to-rose-500', shadow: 'shadow-red-500/15' }, |
| | | { title: 'ç©æåºå', icon: 'lucide:package-search', route: 'WlsMaterialStock', gradient: 'from-indigo-500 to-blue-500', shadow: 'shadow-indigo-500/15' }, |
| | | { 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' }, |
| | |
| | | <template> |
| | | <div 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-3 gap-3 sm:grid-cols-9"> |
| | | <div class="grid grid-cols-4 gap-3 sm:grid-cols-8"> |
| | | <div |
| | | v-for="action in actions" |
| | | :key="action.route" |
| | |
| | | import { erpPriceInputFormatter } from '@vben/utils'; |
| | | |
| | | import { getAccountSimpleList } from '#/api/erp/finance/account'; |
| | | import { getPurchaseInvoiceSimpleList } from '#/api/erp/purchase/invoice'; |
| | | import { getSupplierSimpleList } from '#/api/srm/supplier'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import { getRangePickerDefaultProps } from '#/utils'; |
| | |
| | | fieldName: 'supplierId', |
| | | label: 'ä¾åºå', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | componentProps: (_values, form) => ({ |
| | | disabled: formType === 'detail', |
| | | placeholder: 'è¯·éæ©ä¾åºå', |
| | | allowClear: true, |
| | |
| | | api: getSupplierSimpleList, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | }, |
| | | onChange: () => { |
| | | // ä¾åºååååï¼æ¸
空已éçå
³èæ¥ç¥¨ |
| | | form.setFieldValue('invoiceId', undefined); |
| | | }, |
| | | }), |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'invoiceId', |
| | | label: 'å
³èæ¥ç¥¨', |
| | | component: formType === 'detail' ? 'Input' : 'Select', |
| | | componentProps: |
| | | formType === 'detail' |
| | | ? { |
| | | disabled: true, |
| | | placeholder: '--', |
| | | } |
| | | : undefined, |
| | | rules: 'required', |
| | | dependencies: { |
| | | // ä¾èµ invoiceId èªèº«ï¼ç¡®ä¿ç¼è¾åæ¾æ¶éæ°å è½½é项以å¹é
å·²éæ¥ç¥¨ |
| | | triggerFields: ['supplierId', 'invoiceId'], |
| | | disabled: (values) => formType === 'detail' || !values.supplierId, |
| | | async componentProps(values) { |
| | | if (formType === 'detail') { |
| | | return {}; |
| | | } |
| | | if (!values.supplierId) { |
| | | return { |
| | | options: [], |
| | | placeholder: '请å
éæ©ä¾åºå', |
| | | }; |
| | | } |
| | | const invoices = await getPurchaseInvoiceSimpleList( |
| | | values.supplierId, |
| | | ); |
| | | return { |
| | | options: invoices.map((item) => { |
| | | const remain = item.remainingInvoicePrice ?? item.price ?? 0; |
| | | return { |
| | | label: `${item.no} / ${item.invoiceNo ?? '-'} / Â¥${item.price ?? 0} / å©ä½å¯ä» Â¥${remain.toFixed(2)}`, |
| | | value: item.id, |
| | | }; |
| | | }), |
| | | placeholder: 'è¯·éæ©å
³èæ¥ç¥¨', |
| | | } as any; |
| | | }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'financeUserId', |
| | |
| | | formItemClass: 'col-span-3', |
| | | }, |
| | | { |
| | | fieldName: 'accountId', |
| | | fieldName: 'accountName', |
| | | label: '仿¬¾è´¦æ·', |
| | | component: 'ApiSelect', |
| | | component: 'Input', |
| | | rules: 'required', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©ä»æ¬¾è´¦æ·', |
| | | placeholder: '请è¾å
¥ä»æ¬¾è´¦æ·', |
| | | allowClear: true, |
| | | showSearch: true, |
| | | api: getAccountSimpleList, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | }, |
| | | }, |
| | | { |
| | |
| | | fieldName: 'discountPrice', |
| | | label: '伿 éé¢', |
| | | component: 'InputNumber', |
| | | defaultValue: 0, |
| | | componentProps: { |
| | | class: '!w-full', |
| | | disabled: formType === 'detail', |
| | |
| | | ]; |
| | | } |
| | | |
| | | /** 表åçæç»è¡¨æ ¼å */ |
| | | export function useFormItemColumns( |
| | | disabled: boolean, |
| | | ): VxeTableGridOptions['columns'] { |
| | | /** 表åçæç»è¡¨æ ¼åï¼ä»æ¬¾åºäºæ¥ç¥¨ï¼æç»ç±æ¥ç¥¨èªå¨çæï¼ä¸å¯å é¤ï¼ */ |
| | | export function useFormItemColumns(): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { type: 'seq', title: 'åºå·', minWidth: 50, fixed: 'left' }, |
| | | { |
| | | field: 'bizNo', |
| | | title: 'éè´åæ®ç¼å·', |
| | | title: 'æ¥ç¥¨ç¼å·', |
| | | minWidth: 200, |
| | | }, |
| | | { |
| | |
| | | title: '夿³¨', |
| | | minWidth: 150, |
| | | slots: { default: 'remark' }, |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 50, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | visible: !disabled, |
| | | }, |
| | | ]; |
| | | } |
| | |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'invoice', |
| | | title: 'å
³èæ¥ç¥¨', |
| | | minWidth: 200, |
| | | slots: { default: 'invoice' }, |
| | | }, |
| | | { |
| | | field: 'paymentTime', |
| | | title: '仿¬¾æ¶é´', |
| | | width: 160, |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { ErpFinancePaymentApi } from '#/api/erp/finance/payment'; |
| | | import type { DescriptionItemSchema } from '#/components/description'; |
| | | |
| | | import { h, nextTick, onMounted, ref } from 'vue'; |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { erpPriceInputFormatter, formatDateTime } from '@vben/utils'; |
| | | |
| | | import { Spin } from 'ant-design-vue'; |
| | | |
| | | import { useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getFinancePayment } from '#/api/erp/finance/payment'; |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | import { useDescription } from '#/components/description'; |
| | | |
| | | defineOptions({ name: 'ErpFinancePaymentApproveDetail' }); |
| | | |
| | | const props = defineProps<{ id: string }>(); |
| | | |
| | | const loading = ref(false); |
| | | const formData = ref<ErpFinancePaymentApi.FinancePayment>(); |
| | | |
| | | /** ææ¬åæ®µç©ºå¼å ä½ */ |
| | | function textRender(val: unknown) { |
| | | return val === 0 || val ? String(val) : '-'; |
| | | } |
| | | |
| | | /** 仿¬¾æç»ï¼å®¡æ¹è¯¦æ
åªè¯»åï¼ */ |
| | | function usePaymentItemColumns(): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { type: 'seq', title: 'åºå·', width: 60 }, |
| | | { field: 'bizNo', title: 'æ¥ç¥¨ç¼å·', minWidth: 200 }, |
| | | { |
| | | field: 'totalPrice', |
| | | title: 'åºä»éé¢', |
| | | minWidth: 120, |
| | | formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-', |
| | | }, |
| | | { |
| | | field: 'paidPrice', |
| | | title: 'å·²ä»éé¢', |
| | | minWidth: 120, |
| | | formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-', |
| | | }, |
| | | { |
| | | field: 'paymentPrice', |
| | | title: 'æ¬æ¬¡ä»æ¬¾', |
| | | minWidth: 120, |
| | | formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-', |
| | | }, |
| | | { |
| | | field: 'remark', |
| | | title: '夿³¨', |
| | | minWidth: 150, |
| | | formatter: ({ cellValue }) => textRender(cellValue), |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | gridOptions: { |
| | | columns: usePaymentItemColumns(), |
| | | data: [], |
| | | minHeight: 200, |
| | | autoResize: true, |
| | | border: true, |
| | | rowConfig: { |
| | | keyField: 'bizId', |
| | | isHover: true, |
| | | }, |
| | | pagerConfig: { |
| | | enabled: false, |
| | | }, |
| | | toolbarConfig: { |
| | | enabled: false, |
| | | }, |
| | | }, |
| | | }); |
| | | |
| | | const [Descriptions] = useDescription({ |
| | | bordered: true, |
| | | column: 2, |
| | | schema: [ |
| | | { label: '仿¬¾åå·', field: 'no', render: textRender }, |
| | | { label: 'ä¾åºååç§°', field: 'supplierName', render: textRender }, |
| | | { |
| | | label: 'å
³èæ¥ç¥¨', |
| | | field: 'invoice', |
| | | render: (_val, data) => (data?.invoice?.no ? data.invoice.no : '-'), |
| | | }, |
| | | { |
| | | label: '仿¬¾æ¶é´', |
| | | field: 'paymentTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { |
| | | label: 'å计éé¢ï¼å
ï¼', |
| | | field: 'totalPrice', |
| | | render: (val) => erpPriceInputFormatter(val) || '-', |
| | | }, |
| | | { |
| | | label: '伿 éé¢ï¼å
ï¼', |
| | | field: 'discountPrice', |
| | | render: (val) => erpPriceInputFormatter(val) || '-', |
| | | }, |
| | | { |
| | | label: 'å®é
仿¬¾éé¢ï¼å
ï¼', |
| | | field: 'paymentPrice', |
| | | render: (val) => erpPriceInputFormatter(val) || '-', |
| | | }, |
| | | { label: '仿¬¾è´¦æ·', field: 'accountName', render: textRender }, |
| | | { label: 'è´¢å¡äººå', field: 'financeUserName', render: textRender }, |
| | | { |
| | | label: '审æ¹ç¶æ', |
| | | field: 'status', |
| | | render: (val) => |
| | | h(DictTag, { type: DICT_TYPE.ERP_AUDIT_STATUS, value: val }), |
| | | }, |
| | | { label: 'å建人', field: 'creatorName', render: textRender }, |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { label: '夿³¨', field: 'remark', render: textRender }, |
| | | ] as DescriptionItemSchema[], |
| | | }); |
| | | |
| | | onMounted(async () => { |
| | | try { |
| | | loading.value = true; |
| | | formData.value = await getFinancePayment(Number(props.id)); |
| | | if (formData.value?.items?.length) { |
| | | await nextTick(); |
| | | await gridApi.grid.reloadData(formData.value.items); |
| | | } |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <ContentWrap class="m-2"> |
| | | <Spin :spinning="loading" tip="å è½½ä¸..."> |
| | | <div class="space-y-4"> |
| | | <!-- 仿¬¾ååºæ¬ä¿¡æ¯ --> |
| | | <div class="rounded-lg border border-border bg-background p-4"> |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">仿¬¾ååºæ¬ä¿¡æ¯</span> |
| | | </div> |
| | | <Descriptions :data="formData" /> |
| | | </div> |
| | | <!-- 仿¬¾æç» --> |
| | | <div |
| | | v-if="formData?.items?.length" |
| | | class="rounded-lg border border-border bg-background p-4" |
| | | > |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">仿¬¾æç»</span> |
| | | </div> |
| | | <Grid /> |
| | | </div> |
| | | </div> |
| | | </Spin> |
| | | </ContentWrap> |
| | | </template> |
| | |
| | | import type { ErpFinancePaymentApi } from '#/api/erp/finance/payment'; |
| | | |
| | | import { ref } from 'vue'; |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { downloadFileFromBlobPart, isEmpty } from '@vben/utils'; |
| | |
| | | import { |
| | | deleteFinancePayment, |
| | | exportFinancePayment, |
| | | getFinancePaymentApproveProcessList, |
| | | getFinancePaymentPage, |
| | | updateFinancePaymentStatus, |
| | | } from '#/api/erp/finance/payment'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | import ProcessSelectModal from './modules/process-select-modal.vue'; |
| | | |
| | | /** ERP 仿¬¾åå表 */ |
| | | defineOptions({ name: 'ErpFinancePayment' }); |
| | | |
| | | const router = useRouter(); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ProcessModal, processModalApi] = useVbenModal({ |
| | | connectedComponent: ProcessSelectModal, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | /** 审æ¹/åå®¡æ¹æä½ */ |
| | | async function handleUpdateStatus( |
| | | row: ErpFinancePaymentApi.FinancePayment, |
| | | status: number, |
| | | ) { |
| | | /** æäº¤å®¡æ¹ - å
è·åæµç¨å表 */ |
| | | async function handleSubmit(row: ErpFinancePaymentApi.FinancePayment) { |
| | | const hideLoading = message.loading({ |
| | | content: `ç¡®å®${status === 20 ? '审æ¹' : 'å审æ¹'}è¯¥ä»æ¬¾ååï¼`, |
| | | content: 'è·åå®¡æ¹æµç¨...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await updateFinancePaymentStatus(row.id!, status); |
| | | message.success(`${status === 20 ? '审æ¹' : 'å审æ¹'}æå`); |
| | | handleRefresh(); |
| | | } finally { |
| | | const processList = await getFinancePaymentApproveProcessList(); |
| | | hideLoading(); |
| | | if (!processList || processList.length === 0) { |
| | | message.warning('ææ å¯ç¨å®¡æ¹æµç¨ï¼è¯·å
é
ç½® BPM æµç¨'); |
| | | return; |
| | | } |
| | | // æå¼æµç¨éæ©å¼¹çª |
| | | processModalApi.setData({ id: row.id, processList }).open(); |
| | | } catch { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** æ¥ç审æ¹è¯¦æ
*/ |
| | | function handleProcessDetail(row: ErpFinancePaymentApi.FinancePayment) { |
| | | if (!row.processInstanceId) { |
| | | message.warning('è¯¥ä»æ¬¾åå°æªæäº¤å®¡æ¹'); |
| | | return; |
| | | } |
| | | router.push({ |
| | | name: 'BpmProcessInstanceDetail', |
| | | query: { id: row.processInstanceId }, |
| | | }); |
| | | } |
| | | |
| | | const checkedIds = ref<number[]>([]); |
| | |
| | | |
| | | <template> |
| | | <Page auto-content-height><FormModal @success="handleRefresh" /> |
| | | <ProcessModal @success="handleRefresh" /> |
| | | <Grid table-title="仿¬¾åå表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #invoice="{ row }"> |
| | | <span v-if="row.invoice"> |
| | | {{ row.invoice.no |
| | | }}{{ row.invoice.invoiceNo ? ` / ${row.invoice.invoiceNo}` : '' }} |
| | | </span> |
| | | <span v-else>--</span> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['erp:finance-payment:update'], |
| | | ifShow: () => row.status !== 20, |
| | | ifShow: () => row.status !== 20 && row.status !== 15, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: row.status === 10 ? '审æ¹' : 'å审æ¹', |
| | | label: 'æäº¤å®¡æ ¸', |
| | | type: 'link', |
| | | icon: ACTION_ICON.AUDIT, |
| | | auth: ['erp:finance-payment:update-status'], |
| | | popConfirm: { |
| | | title: `确认${row.status === 10 ? '审æ¹' : 'å审æ¹'}${row.no}åï¼`, |
| | | confirm: handleUpdateStatus.bind( |
| | | null, |
| | | row, |
| | | row.status === 10 ? 20 : 10, |
| | | ), |
| | | }, |
| | | auth: ['erp:finance-payment:update'], |
| | | ifShow: () => row.status === 10, |
| | | onClick: handleSubmit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'æ¥ç审æ¹', |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['erp:finance-payment:query'], |
| | | ifShow: () => row.status !== 10 && !!row.processInstanceId, |
| | | onClick: handleProcessDetail.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['erp:finance-payment:delete'], |
| | | ifShow: () => row.status !== 15, |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.no]), |
| | | confirm: handleDelete.bind(null, [row.id!]), |
| | |
| | | getFinancePayment, |
| | | updateFinancePayment, |
| | | } from '#/api/erp/finance/payment'; |
| | | import { getPurchaseInvoiceSimpleList } from '#/api/erp/purchase/invoice'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | import ItemForm from './item-form.vue'; |
| | |
| | | id: undefined, |
| | | no: '', |
| | | supplierId: undefined, |
| | | accountId: undefined, |
| | | accountName: undefined, |
| | | financeUserId: undefined, |
| | | paymentTime: undefined, |
| | | remark: '', |
| | |
| | | if (changedFields.includes('supplierId')) { |
| | | formData.value.supplierId = values.supplierId; |
| | | } |
| | | // å
³èæ¥ç¥¨ååæ¶ï¼å è½½æ¥ç¥¨ä¿¡æ¯ä¾æç»èªå¨çæ |
| | | if (changedFields.includes('invoiceId')) { |
| | | formData.value.invoiceId = values.invoiceId; |
| | | handleInvoiceChange(values.invoiceId); |
| | | } |
| | | // ç®çï¼åæ¥å° item-form ç»ä»¶ï¼è§¦åæ´ä½çä»·æ ¼è®¡ç® |
| | | if (changedFields.includes('discountPrice')) { |
| | | formData.value.discountPrice = values.discountPrice; |
| | |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | /** å
³èæ¥ç¥¨ååæ¶ï¼å è½½æ¥ç¥¨ä¿¡æ¯ï¼ä¾ä»æ¬¾æç»èªå¨çæ */ |
| | | async function handleInvoiceChange(invoiceId: number | undefined) { |
| | | if (formType.value === 'detail' || !invoiceId) { |
| | | if (!invoiceId) { |
| | | formData.value.invoice = undefined; |
| | | } |
| | | return; |
| | | } |
| | | const supplierId = formData.value.supplierId; |
| | | if (!supplierId) { |
| | | return; |
| | | } |
| | | const invoices = await getPurchaseInvoiceSimpleList(supplierId); |
| | | // è¯·æ±æé´æ¥ç¥¨å·²åæ¬¡åæ¢æ¶ï¼ä¸¢å¼è¿æç»æï¼é¿å
åæ¾éä¹± |
| | | if (formData.value.invoiceId !== invoiceId) { |
| | | return; |
| | | } |
| | | const invoice = invoices.find((item) => item.id === invoiceId); |
| | | formData.value.invoice = invoice; |
| | | } |
| | | |
| | | /** æ´æ°ä»æ¬¾é¡¹ */ |
| | | function handleUpdateItems(items: ErpFinancePaymentApi.FinancePaymentItem[]) { |
| | |
| | | formApi.setDisabled(formType.value === 'detail'); |
| | | formApi.updateSchema(useFormSchema(formType.value)); |
| | | if (!data || !data.id) { |
| | | // æ°å¢æ¶ï¼é»è®¤éä¸è´¦æ· |
| | | // æ°å¢æ¶ï¼é»è®¤å¡«å
¥é»è®¤ç»ç®è´¦æ·åç§° |
| | | const accountList = await getAccountSimpleList(); |
| | | const defaultAccount = accountList.find((item) => item.defaultStatus); |
| | | if (defaultAccount) { |
| | | await formApi.setValues({ accountId: defaultAccount.id }); |
| | | await formApi.setValues({ accountName: defaultAccount.name }); |
| | | } |
| | | return; |
| | | } |
| | |
| | | formData.value = await getFinancePayment(data.id); |
| | | // è®¾ç½®å° values |
| | | await formApi.setValues(formData.value, false); |
| | | // 详æ
模å¼ä¸ï¼å
³èæ¥ç¥¨åæ®µç¨ Input å±ç¤ºæ¥ç¥¨ç¼å· |
| | | if (formType.value === 'detail' && formData.value?.invoice) { |
| | | await formApi.setFieldValue('invoiceId', formData.value.invoice.no); |
| | | } |
| | | if (formData.value?.attachmentList?.length) { |
| | | const blobIds = formData.value.attachmentList.map((item) => ({ |
| | | uid: String(item.id), |
| | |
| | | :supplier-id="formData?.supplierId" |
| | | :disabled="formType === 'detail'" |
| | | :discount-price="formData?.discountPrice ?? 0" |
| | | :invoice="formData?.invoice" |
| | | @update:items="handleUpdateItems" |
| | | @update:total-price="handleUpdateTotalPrice" |
| | | @update:payment-price="handleUpdatePaymentPrice" |
| | |
| | | <script lang="ts" setup> |
| | | import type { ErpFinancePaymentApi } from '#/api/erp/finance/payment'; |
| | | import type { ErpPurchaseInApi } from '#/api/erp/purchase/in'; |
| | | import type { ErpPurchaseReturnApi } from '#/api/erp/purchase/return'; |
| | | |
| | | import { computed, nextTick, ref, watch } from 'vue'; |
| | | |
| | | import { ErpBizType } from '@vben/constants'; |
| | | import { erpPriceInputFormatter } from '@vben/utils'; |
| | | |
| | | import { Input, InputNumber, message } from 'ant-design-vue'; |
| | | import { Input, InputNumber } from 'ant-design-vue'; |
| | | |
| | | import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | |
| | | import { useFormItemColumns } from '../data'; |
| | | import PurchaseInSelect from './purchase-in-select.vue'; |
| | | import SaleReturnSelect from './sale-return-select.vue'; |
| | | |
| | | interface InvoiceForItem { |
| | | id?: number; |
| | | no?: string; |
| | | price?: number; |
| | | remainingInvoicePrice?: number; |
| | | } |
| | | |
| | | interface Props { |
| | | items?: ErpFinancePaymentApi.FinancePaymentItem[]; |
| | | supplierId?: number; |
| | | disabled?: boolean; |
| | | discountPrice?: number; |
| | | invoice?: InvoiceForItem; |
| | | } |
| | | |
| | | const props = withDefaults(defineProps<Props>(), { |
| | |
| | | supplierId: undefined, |
| | | disabled: false, |
| | | discountPrice: 0, |
| | | invoice: undefined, |
| | | }); |
| | | |
| | | const emit = defineEmits([ |
| | |
| | | 'update:paymentPrice', |
| | | ]); |
| | | |
| | | /** è¾å
¥æ¶ä¸è¡¥é¶ï¼å¤±ç¦å仿é颿 ¼å¼ä¿ç两ä½å°æ° */ |
| | | const paymentPriceFormatter = ( |
| | | value: number | string | undefined, |
| | | info: { input: string; userTyping: boolean }, |
| | | ) => { |
| | | return info.userTyping ? info.input : erpPriceInputFormatter(value); |
| | | }; |
| | | |
| | | const tableData = ref<ErpFinancePaymentApi.FinancePaymentItem[]>([]); // è¡¨æ ¼æ°æ® |
| | | |
| | | /** å·²æ ¹æ®è¯¥æ¥ç¥¨çæçæç»ï¼é¿å
éå¤çæ */ |
| | | const generatedInvoiceId = ref<number | undefined>(); |
| | | |
| | | /** è·åè¡¨æ ¼åè®¡æ°æ® */ |
| | | const summaries = computed(() => { |
| | |
| | | /** è¡¨æ ¼é
ç½® */ |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | gridOptions: { |
| | | columns: useFormItemColumns(props.disabled), |
| | | columns: useFormItemColumns(), |
| | | data: tableData.value, |
| | | minHeight: 250, |
| | | autoResize: true, |
| | |
| | | return; |
| | | } |
| | | tableData.value = [...items]; |
| | | // ç¼è¾åæ¾æ¶ï¼æ è®°å·²æ ¹æ®å½åæ¥ç¥¨çæè¿ï¼é¿å
éå¤çæ |
| | | generatedInvoiceId.value = props.invoice?.id; |
| | | await nextTick(); // ç¹æ®ï¼ä¿è¯ gridApi å·²ç»åå§å |
| | | await gridApi.grid.reloadData(tableData.value); |
| | | }, |
| | | { |
| | | immediate: true, |
| | | }, |
| | | ); |
| | | |
| | | /** å
³èæ¥ç¥¨ååæ¶ï¼èªå¨çæå¯ä¸ç仿¬¾æç»ï¼ä»æ¬¾åºäºæ¥ç¥¨ï¼ */ |
| | | watch( |
| | | () => props.invoice?.id, |
| | | async (newId) => { |
| | | if (!newId || !props.invoice) { |
| | | return; |
| | | } |
| | | if (generatedInvoiceId.value === newId) { |
| | | return; |
| | | } |
| | | generatedInvoiceId.value = newId; |
| | | const invoice = props.invoice; |
| | | const price = invoice.price ?? 0; |
| | | const remaining = invoice.remainingInvoicePrice ?? price; |
| | | tableData.value = [ |
| | | { |
| | | bizId: invoice.id ?? 0, |
| | | bizType: ErpBizType.PURCHASE_INVOICE, |
| | | bizNo: invoice.no ?? '', |
| | | totalPrice: price, |
| | | paidPrice: price - remaining, |
| | | paymentPrice: remaining, |
| | | remark: '', |
| | | }, |
| | | ]; |
| | | await nextTick(); |
| | | await gridApi.grid.reloadData(tableData.value); |
| | | emit('update:items', [...tableData.value]); |
| | | }, |
| | | ); |
| | | |
| | |
| | | { deep: true }, |
| | | ); |
| | | |
| | | /** æ·»å éè´å
¥åºå */ |
| | | const purchaseInSelectRef = ref(); |
| | | const handleOpenPurchaseIn = () => { |
| | | if (!props.supplierId) { |
| | | message.error('è¯·éæ©ä¾åºå'); |
| | | return; |
| | | } |
| | | purchaseInSelectRef.value?.open(props.supplierId); |
| | | }; |
| | | |
| | | const handleAddPurchaseIn = (rows: ErpPurchaseInApi.PurchaseIn[]) => { |
| | | rows.forEach((row) => { |
| | | const totalPrice = row.totalPrice ?? 0; |
| | | const paidPrice = row.paymentPrice ?? 0; |
| | | const newItem: ErpFinancePaymentApi.FinancePaymentItem = { |
| | | bizId: row.id ?? 0, |
| | | bizType: ErpBizType.PURCHASE_IN, |
| | | bizNo: row.no ?? '', |
| | | totalPrice, |
| | | paidPrice, |
| | | paymentPrice: totalPrice - paidPrice, |
| | | remark: undefined, |
| | | }; |
| | | tableData.value.push(newItem); |
| | | }); |
| | | emit('update:items', [...tableData.value]); |
| | | }; |
| | | |
| | | /** æ·»å éè´éè´§å */ |
| | | const saleReturnSelectRef = ref(); |
| | | const handleOpenSaleReturn = () => { |
| | | if (!props.supplierId) { |
| | | message.error('è¯·éæ©ä¾åºå'); |
| | | return; |
| | | } |
| | | saleReturnSelectRef.value?.open(props.supplierId); |
| | | }; |
| | | |
| | | const handleAddSaleReturn = (rows: ErpPurchaseReturnApi.PurchaseReturn[]) => { |
| | | rows.forEach((row) => { |
| | | const totalPrice = row.totalPrice ?? 0; |
| | | const refundPrice = row.refundPrice ?? 0; |
| | | const newItem: ErpFinancePaymentApi.FinancePaymentItem = { |
| | | bizId: row.id ?? 0, |
| | | bizType: ErpBizType.PURCHASE_RETURN, |
| | | bizNo: row.no ?? '', |
| | | totalPrice: -totalPrice, |
| | | paidPrice: -refundPrice, |
| | | paymentPrice: -totalPrice + refundPrice, |
| | | remark: undefined, |
| | | }; |
| | | tableData.value.push(newItem); |
| | | }); |
| | | emit('update:items', [...tableData.value]); |
| | | }; |
| | | |
| | | /** å é¤è¡ */ |
| | | const handleDelete = async (row: any) => { |
| | | const index = tableData.value.findIndex( |
| | | (item) => item.bizId === row.bizId && item.bizType === row.bizType, |
| | | ); |
| | | if (index !== -1) { |
| | | tableData.value.splice(index, 1); |
| | | } |
| | | // éç¥ç¶ç»ä»¶æ´æ° |
| | | emit('update:items', [...tableData.value]); |
| | | }; |
| | | |
| | | /** å¤çè¡æ°æ®åæ´ */ |
| | | const handleRowChange = (row: any) => { |
| | | const index = tableData.value.findIndex( |
| | |
| | | const validate = () => { |
| | | // æ£æ¥æ¯å¦ææç» |
| | | if (tableData.value.length === 0) { |
| | | throw new Error('请添å 仿¬¾æç»'); |
| | | throw new Error('请å
éæ©å
³èæ¥ç¥¨'); |
| | | } |
| | | // æ£æ¥æ¯è¡ç仿¬¾éé¢ |
| | | for (let i = 0; i < tableData.value.length; i++) { |
| | |
| | | v-model:value="row.paymentPrice" |
| | | :precision="2" |
| | | :disabled="disabled" |
| | | :formatter="erpPriceInputFormatter" |
| | | :formatter="paymentPriceFormatter" |
| | | placeholder="请è¾å
¥æ¬æ¬¡ä»æ¬¾" |
| | | @change="handleRowChange(row)" |
| | | /> |
| | |
| | | @change="handleRowChange(row)" |
| | | /> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'å é¤', |
| | | type: 'link', |
| | | danger: true, |
| | | popConfirm: { |
| | | title: '确认å é¤è¯¥ä»æ¬¾æç»åï¼', |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | |
| | | <template #bottom> |
| | | <div class="mt-2 rounded border border-border bg-muted p-2"> |
| | |
| | | <span class="font-medium text-foreground">å计ï¼</span> |
| | | <div class="flex space-x-4"> |
| | | <span> |
| | | åè®¡ä»æ¬¾ï¼{{ erpPriceInputFormatter(summaries.totalPrice) }} |
| | | åºä»éé¢ï¼{{ erpPriceInputFormatter(summaries.totalPrice) }} |
| | | </span> |
| | | <span> |
| | | å·²ä»éé¢ï¼{{ erpPriceInputFormatter(summaries.paidPrice) }} |
| | |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <TableAction |
| | | v-if="!disabled" |
| | | class="mt-2 flex justify-center" |
| | | :actions="[ |
| | | { |
| | | label: 'æ·»å éè´å
¥åºå', |
| | | type: 'default', |
| | | onClick: handleOpenPurchaseIn, |
| | | }, |
| | | { |
| | | label: 'æ·»å éè´éè´§å', |
| | | type: 'default', |
| | | onClick: handleOpenSaleReturn, |
| | | }, |
| | | ]" |
| | | /> |
| | | <div |
| | | v-if="!invoice && !disabled" |
| | | class="mt-2 flex justify-center text-sm text-muted-foreground" |
| | | > |
| | | 请å
å¨ä¸æ¹è¡¨åéæ©å
³èæ¥ç¥¨ï¼ç³»ç»å°èªå¨çæä»æ¬¾æç» |
| | | </div> |
| | | </template> |
| | | </Grid> |
| | | |
| | | <!-- éè´å
¥åºåéæ©ç»ä»¶ --> |
| | | <PurchaseInSelect |
| | | ref="purchaseInSelectRef" |
| | | @success="handleAddPurchaseIn" |
| | | /> |
| | | <!-- éè´éè´§åéæ©ç»ä»¶ --> |
| | | <SaleReturnSelect |
| | | ref="saleReturnSelectRef" |
| | | @success="handleAddSaleReturn" |
| | | /> |
| | | </div> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import { ref, computed } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message, Select } from 'ant-design-vue'; |
| | | |
| | | import { submitFinancePayment } from '#/api/erp/finance/payment'; |
| | | |
| | | interface ProcessDefinition { |
| | | id: string; |
| | | name: string; |
| | | version: number; |
| | | key: string; |
| | | } |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const paymentId = ref<number>(); |
| | | const processList = ref<ProcessDefinition[]>([]); |
| | | const selectedProcessKey = ref<string>(''); |
| | | |
| | | const processOptions = computed(() => { |
| | | return processList.value.map((p) => ({ |
| | | label: `${p.name} (V${p.version})`, |
| | | value: p.key, |
| | | })); |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | if (!selectedProcessKey.value) { |
| | | message.warning('è¯·éæ©å®¡æ¹æµç¨'); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | await submitFinancePayment(paymentId.value!, selectedProcessKey.value); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success('æäº¤æå'); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | paymentId.value = undefined; |
| | | selectedProcessKey.value = ''; |
| | | processList.value = []; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ id: number; processList: ProcessDefinition[] }>(); |
| | | paymentId.value = data.id; |
| | | processList.value = data.processList || []; |
| | | // é»è®¤éæ©ç¬¬ä¸ä¸ªæµç¨ |
| | | if (processList.value.length > 0) { |
| | | selectedProcessKey.value = processList.value[0].key; |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="éæ©å®¡æ¹æµç¨" class="w-1/3"> |
| | | <div class="mb-4"> |
| | | <span class="mr-2">å®¡æ¹æµç¨ï¼</span> |
| | | <Select |
| | | v-model:value="selectedProcessKey" |
| | | :options="processOptions" |
| | | style="width: 200px" |
| | | placeholder="è¯·éæ©å®¡æ¹æµç¨" |
| | | /> |
| | | </div> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { Ref } from 'vue'; |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | |
| | | import { getSupplierSimpleList } from '#/api/srm/supplier'; |
| | | |
| | | import { z } from '#/adapter/form'; |
| | | |
| | | import { markRaw } from 'vue'; |
| | | |
| | | import PurchaseOrderSelect from './modules/purchase-order-select.vue'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'edit'; |
| | | |
| | | /** æ°å¢/ä¿®æ¹ç表å */ |
| | | export function useFormSchema( |
| | | orderRemainInvoicePrice?: Ref<number | undefined>, |
| | | ): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => false, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'no', |
| | | label: 'æ¥ç¥¨ç¼å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: 'ä¿åæ¶èªå¨çæ', |
| | | disabled: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'purchaseOrderId', |
| | | label: 'éè´è®¢å', |
| | | component: markRaw(PurchaseOrderSelect), |
| | | rules: 'required', |
| | | dependencies: { |
| | | triggerFields: ['id'], |
| | | disabled: (values) => !!values.id, |
| | | }, |
| | | componentProps: (_values, form) => ({ |
| | | placeholder: 'è¯·éæ©éè´è®¢å', |
| | | onChange: ( |
| | | order: |
| | | | { supplierName?: string; remainInvoicePrice?: number } |
| | | | undefined, |
| | | ) => { |
| | | form.setFieldValue('supplierName', order?.supplierName); |
| | | if (orderRemainInvoicePrice) { |
| | | orderRemainInvoicePrice.value = order?.remainInvoicePrice; |
| | | } |
| | | }, |
| | | }), |
| | | }, |
| | | { |
| | | fieldName: 'supplierName', |
| | | label: 'ä¾åºå', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: 'éæ©éè´è®¢ååèªå¨å¸¦åº', |
| | | disabled: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'invoiceNo', |
| | | label: 'å票å·ç ', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å票å·ç ', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'invoiceTitle', |
| | | label: 'å票æ¬å¤´', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å票æ¬å¤´', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'price', |
| | | label: 'æ¥ç¥¨éé¢ï¼å
ï¼', |
| | | component: 'InputNumber', |
| | | rules: 'required', |
| | | description: () => |
| | | orderRemainInvoicePrice?.value != null |
| | | ? `éè´è®¢åæªå¼ç¥¨éé¢ï¼${Number(orderRemainInvoicePrice.value).toFixed(2)} å
` |
| | | : '', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | placeholder: '请è¾å
¥æ¥ç¥¨éé¢', |
| | | min: 0, |
| | | precision: 2, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['purchaseOrderId'], |
| | | componentProps: () => ({ |
| | | class: '!w-full', |
| | | placeholder: '请è¾å
¥æ¥ç¥¨éé¢', |
| | | min: 0, |
| | | max: orderRemainInvoicePrice?.value ?? undefined, |
| | | precision: 2, |
| | | }), |
| | | rules: () => { |
| | | const max = orderRemainInvoicePrice?.value; |
| | | if (max != null) { |
| | | return z |
| | | .number({ message: '请è¾å
¥æ¥ç¥¨éé¢' }) |
| | | .min(0, 'æ¥ç¥¨éé¢ä¸è½å°äº 0') |
| | | .max(max, `æ¥ç¥¨éé¢ä¸è½å¤§äºæªå¼ç¥¨éé¢ ${Number(max).toFixed(2)}`); |
| | | } |
| | | return z |
| | | .number({ message: '请è¾å
¥æ¥ç¥¨éé¢' }) |
| | | .min(0, 'æ¥ç¥¨éé¢ä¸è½å°äº 0'); |
| | | }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'invoiceTime', |
| | | label: 'æ¥ç¥¨æ¥æ', |
| | | component: 'DatePicker', |
| | | rules: 'required', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©æ¥ç¥¨æ¥æ', |
| | | showTime: false, |
| | | valueFormat: 'YYYY-MM-DD', |
| | | format: 'YYYY-MM-DD', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'blobIds', |
| | | label: 'å票éä»¶', |
| | | component: 'FileUpload', |
| | | componentProps: { |
| | | valueKey: 'id', |
| | | maxNumber: 10, |
| | | multiple: true, |
| | | accept: ['jpg', 'jpeg', 'png', 'pdf'], |
| | | helpText: 'æ¯æjpgãjpegãpngãpdfæ ¼å¼ï¼å个æä»¶ä¸è¶
è¿50MB', |
| | | }, |
| | | formItemClass: 'md:col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å¤æ³¨', |
| | | rows: 4, |
| | | }, |
| | | formItemClass: 'md:col-span-2', |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表çæç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'no', |
| | | label: 'æ¥ç¥¨ç¼å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥æ¥ç¥¨ç¼å·', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'supplierId', |
| | | label: 'ä¾åºå', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©ä¾åºå', |
| | | allowClear: true, |
| | | showSearch: true, |
| | | api: getSupplierSimpleList, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表çåæ®µ */ |
| | | export function useGridColumns(): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { |
| | | title: 'æ¥ç¥¨ç¼å·', |
| | | field: 'no', |
| | | minWidth: 160, |
| | | fixed: 'left', |
| | | slots: { default: 'no' }, |
| | | }, |
| | | { |
| | | title: 'ä¾åºå', |
| | | field: 'supplierName', |
| | | minWidth: 150, |
| | | }, |
| | | { |
| | | title: 'éè´è®¢åç¼å·', |
| | | field: 'order', |
| | | minWidth: 160, |
| | | slots: { default: 'orderNo' }, |
| | | }, |
| | | { |
| | | title: 'å票å·ç ', |
| | | field: 'invoiceNo', |
| | | minWidth: 150, |
| | | }, |
| | | { |
| | | title: 'æ¥ç¥¨éé¢ï¼å
ï¼', |
| | | field: 'price', |
| | | minWidth: 150, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | title: 'æ¥ç¥¨æ¥æ', |
| | | field: 'invoiceTime', |
| | | minWidth: 150, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: '夿³¨', |
| | | field: 'remark', |
| | | minWidth: 150, |
| | | }, |
| | | { |
| | | title: 'éä»¶', |
| | | field: 'attachmentList', |
| | | minWidth: 100, |
| | | fixed: 'right', |
| | | slots: { default: 'attachment' }, |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | field: 'actions', |
| | | minWidth: 200, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { ErpPurchaseInvoiceApi } from '#/api/erp/purchase/invoice'; |
| | | import type { DescriptionItemSchema } from '#/components/description'; |
| | | |
| | | import { onMounted, ref } from 'vue'; |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { erpPriceInputFormatter, formatDateTime } from '@vben/utils'; |
| | | |
| | | import { Spin } from 'ant-design-vue'; |
| | | |
| | | import { getPurchaseInvoice } from '#/api/erp/purchase/invoice'; |
| | | import { useDescription } from '#/components/description'; |
| | | |
| | | defineOptions({ name: 'ErpPurchaseInvoiceApproveDetail' }); |
| | | |
| | | const props = defineProps<{ id: string }>(); |
| | | |
| | | const loading = ref(false); |
| | | const formData = ref<ErpPurchaseInvoiceApi.PurchaseInvoice>(); |
| | | |
| | | /** ææ¬åæ®µç©ºå¼å ä½ */ |
| | | function textRender(val: unknown) { |
| | | return val === 0 || val ? String(val) : '-'; |
| | | } |
| | | |
| | | const [Descriptions] = useDescription({ |
| | | bordered: true, |
| | | column: 2, |
| | | schema: [ |
| | | { label: 'æ¥ç¥¨ç¼å·', field: 'no', render: textRender }, |
| | | { label: 'ä¾åºååç§°', field: 'supplierName', render: textRender }, |
| | | { |
| | | label: 'éè´è®¢åå·', |
| | | field: 'order', |
| | | render: (_val, data) => (data?.order?.no ? data.order.no : '-'), |
| | | }, |
| | | { label: 'å票å·ç ', field: 'invoiceNo', render: textRender }, |
| | | { label: 'å票æ¬å¤´', field: 'invoiceTitle', render: textRender }, |
| | | { |
| | | label: 'æ¥ç¥¨éé¢ï¼å
ï¼', |
| | | field: 'price', |
| | | render: (val) => erpPriceInputFormatter(val) || '-', |
| | | }, |
| | | { |
| | | label: 'å©ä½å¯ä»éé¢ï¼å
ï¼', |
| | | field: 'remainingInvoicePrice', |
| | | render: (val) => erpPriceInputFormatter(val ?? 0) || '-', |
| | | }, |
| | | { |
| | | label: 'å¼ç¥¨æ¥æ', |
| | | field: 'invoiceTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { label: 'å建人', field: 'creatorName', render: textRender }, |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { label: '夿³¨', field: 'remark', render: textRender }, |
| | | ] as DescriptionItemSchema[], |
| | | }); |
| | | |
| | | onMounted(async () => { |
| | | try { |
| | | loading.value = true; |
| | | formData.value = await getPurchaseInvoice(Number(props.id)); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <ContentWrap class="m-2"> |
| | | <Spin :spinning="loading" tip="å è½½ä¸..."> |
| | | <div class="space-y-4"> |
| | | <!-- æ¥ç¥¨åºæ¬ä¿¡æ¯ --> |
| | | <div class="rounded-lg border border-border bg-background p-4"> |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">æ¥ç¥¨åºæ¬ä¿¡æ¯</span> |
| | | </div> |
| | | <Descriptions :data="formData" /> |
| | | </div> |
| | | </div> |
| | | </Spin> |
| | | </ContentWrap> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { ErpPurchaseInvoiceApi } from '#/api/erp/purchase/invoice'; |
| | | import type { ErpPurchaseInvoiceAiApi } from '#/api/erp/purchase/invoice/ai'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button, message, Tag } from 'ant-design-vue'; |
| | | import { nextTick, ref } from 'vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deletePurchaseInvoice, |
| | | exportPurchaseInvoice, |
| | | getPurchaseInvoicePage, |
| | | } from '#/api/erp/purchase/invoice'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | import OcrUpload from './modules/ocr-upload.vue'; |
| | | |
| | | defineOptions({ name: 'ErpPurchaseInvoice' }); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const ocrUploadRef = ref<InstanceType<typeof OcrUpload>>(); |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** 导åºè¡¨æ ¼ */ |
| | | async function handleExport() { |
| | | const formValues = await gridApi.formApi.getValues(); |
| | | const data = await exportPurchaseInvoice(formValues); |
| | | downloadFileFromBlobPart({ fileName: 'æ¥ç¥¨.xls', source: data }); |
| | | } |
| | | |
| | | /** å建æ¥ç¥¨ */ |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | /** è¯å«å½å
¥ */ |
| | | function handleOcrCreate() { |
| | | ocrUploadRef.value?.open(); |
| | | } |
| | | |
| | | /** OCR è¯å«ç»æç¡®è®¤ */ |
| | | function handleOcrSuccess(data: ErpPurchaseInvoiceAiApi.OcrResultVO) { |
| | | nextTick(() => { |
| | | formModalApi.setData({ formType: 'create', ocrData: data } as any).open(); |
| | | }); |
| | | } |
| | | |
| | | /** ç¼è¾æ¥ç¥¨ */ |
| | | function handleEdit(row: ErpPurchaseInvoiceApi.PurchaseInvoice) { |
| | | formModalApi.setData({ formType: 'edit', id: row.id }).open(); |
| | | } |
| | | |
| | | /** æ¥çæ¥ç¥¨è¯¦æ
*/ |
| | | function handleDetail(row: ErpPurchaseInvoiceApi.PurchaseInvoice) { |
| | | formModalApi.setData({ formType: 'detail', id: row.id }).open(); |
| | | } |
| | | |
| | | /** å 餿¥ç¥¨ */ |
| | | async function handleDelete(row: ErpPurchaseInvoiceApi.PurchaseInvoice) { |
| | | const hideLoading = message.loading({ |
| | | content: $t('ui.actionMessage.deleting', [row.no]), |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await deletePurchaseInvoice(row.id!); |
| | | message.success($t('ui.actionMessage.deleteSuccess', [row.no])); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getPurchaseInvoicePage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | toolbarConfig: { |
| | | refresh: true, |
| | | search: true, |
| | | }, |
| | | } as VxeTableGridOptions<ErpPurchaseInvoiceApi.PurchaseInvoice>, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <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, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #no="{ row }"> |
| | | <Button type="link" @click="handleDetail(row)"> |
| | | {{ row.no }} |
| | | </Button> |
| | | </template> |
| | | <template #orderNo="{ row }"> |
| | | <span>{{ row.order?.no ?? '--' }}</span> |
| | | </template> |
| | | <template #attachment="{ row }"> |
| | | <Tag v-if="!row.hasAttachment" color="warning">æªä¸ä¼ </Tag> |
| | | <Tag v-else color="success">å·²ä¸ä¼ </Tag> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('common.detail'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['erp:purchase-invoice:query'], |
| | | onClick: handleDetail.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['erp:purchase-invoice:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['erp:purchase-invoice:delete'], |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.no]), |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormType } from '../data'; |
| | | |
| | | import type { ErpPurchaseInvoiceApi } from '#/api/erp/purchase/invoice'; |
| | | import type { ErpPurchaseInvoiceAiApi } from '#/api/erp/purchase/invoice/ai'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { Alert, message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createPurchaseInvoice, |
| | | getPurchaseInvoice, |
| | | updatePurchaseInvoice, |
| | | } from '#/api/erp/purchase/invoice'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formData = ref<ErpPurchaseInvoiceApi.PurchaseInvoice>(); |
| | | const formType = ref<FormType>('create'); |
| | | const ocrSupplierName = ref(''); |
| | | const orderRemainInvoicePrice = ref<number>(); // æééè´è®¢åå©ä½å¯å¼ç¥¨éé¢ï¼æªå¼ç¥¨éé¢ï¼ç¨äºæç¤ºä¸æ ¡éªï¼ |
| | | |
| | | const getTitle = computed(() => { |
| | | if (formType.value === 'create') { |
| | | return $t('ui.actionTitle.create', ['æ¥ç¥¨']); |
| | | } else if (formType.value === 'edit') { |
| | | return $t('ui.actionTitle.edit', ['æ¥ç¥¨']); |
| | | } else { |
| | | return 'æ¥ç¥¨è¯¦æ
'; |
| | | } |
| | | }); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { |
| | | class: 'w-full', |
| | | }, |
| | | labelWidth: 120, |
| | | }, |
| | | wrapperClass: 'grid-cols-2', |
| | | layout: 'horizontal', |
| | | schema: useFormSchema(orderRemainInvoicePrice), |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | const data = (await formApi.getValues()) as ErpPurchaseInvoiceApi.PurchaseInvoice; |
| | | if (formData.value?.id) { |
| | | data.id = formData.value.id; |
| | | } |
| | | try { |
| | | await (formData.value?.id |
| | | ? updatePurchaseInvoice(data) |
| | | : createPurchaseInvoice(data)); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | formData.value = undefined; |
| | | ocrSupplierName.value = ''; |
| | | orderRemainInvoicePrice.value = undefined; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ |
| | | formType: FormType; |
| | | id?: number; |
| | | ocrData?: ErpPurchaseInvoiceAiApi.OcrResultVO; |
| | | }>(); |
| | | formType.value = data.formType; |
| | | formApi.setDisabled(formType.value === 'detail'); |
| | | formApi.updateSchema(useFormSchema(orderRemainInvoicePrice)); |
| | | await formApi.resetForm(); |
| | | // OCR è¯å«æ°æ®é¢å¡«ï¼æ°å»ºåºæ¯ï¼ç¼è¾/详æ
ä¸å¤çï¼ |
| | | const ocr = data?.ocrData; |
| | | ocrSupplierName.value = ocr?.supplierName || ''; |
| | | if (ocr) { |
| | | await formApi.setValues({ |
| | | invoiceNo: ocr.invoiceNo, |
| | | invoiceTitle: ocr.invoiceTitle, |
| | | price: ocr.price, |
| | | invoiceTime: ocr.invoiceTime, |
| | | remark: ocr.remark, |
| | | }); |
| | | } |
| | | if (!data || !data.id) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getPurchaseInvoice(data.id); |
| | | if (formData.value?.attachmentList?.length) { |
| | | // 转为ä¸ä¼ ç»ä»¶çæä»¶å¯¹è±¡ï¼æäº¤æ¶ formApi 伿 valueKey æåéä»¶ ID |
| | | (formData.value as Record<string, unknown>).blobIds = |
| | | formData.value.attachmentList.map((item) => ({ |
| | | uid: String(item.id), |
| | | name: item.name || '', |
| | | url: item.url || '', |
| | | status: 'done', |
| | | id: item.id, |
| | | })); |
| | | } |
| | | await formApi.setValues(formData.value as any); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-3/5"> |
| | | <Alert |
| | | v-if="ocrSupplierName" |
| | | type="info" |
| | | show-icon |
| | | class="mx-4 mb-4" |
| | | message="è¯å«å°é宿¹åç§°ï¼è¯·éæ©éè´è®¢åå¹¶æ ¸å¯¹è®¢åä¾åºåæ¯å¦ä¸è´" |
| | | :description="`è¯å«ç»æï¼${ocrSupplierName}`" |
| | | /> |
| | | <Form class="mx-4" /> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { ErpPurchaseInvoiceAiApi } from '#/api/erp/purchase/invoice/ai'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { |
| | | Alert, |
| | | Button, |
| | | Descriptions, |
| | | Modal, |
| | | Tag, |
| | | Upload, |
| | | message, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { ocrPurchaseInvoice } from '#/api/erp/purchase/invoice/ai'; |
| | | import { uploadFile } from '#/api/system/storage'; |
| | | |
| | | defineOptions({ name: 'ErpPurchaseInvoiceOcrUpload' }); |
| | | |
| | | const emit = defineEmits<{ |
| | | success: [data: ErpPurchaseInvoiceAiApi.OcrResultVO]; |
| | | }>(); |
| | | |
| | | const open = ref(false); |
| | | const step = ref(0); // 0=upload, 1=processing, 2=preview |
| | | const fileList = ref<any[]>([]); |
| | | const ocrResult = ref<ErpPurchaseInvoiceAiApi.OcrResultVO>(); |
| | | const processingTip = ref(''); |
| | | |
| | | function handleOpen() { |
| | | open.value = true; |
| | | step.value = 0; |
| | | fileList.value = []; |
| | | ocrResult.value = undefined; |
| | | } |
| | | |
| | | /** èªå®ä¹ä¸ä¼ */ |
| | | async function handleUpload(options: any) { |
| | | const { file, onSuccess, onError } = options; |
| | | try { |
| | | const result = await uploadFile([file as File]); |
| | | const blobId = result?.[0]?.id; |
| | | if (!blobId) { |
| | | onError(new Error('ä¸ä¼ 失败')); |
| | | return; |
| | | } |
| | | onSuccess({ blobId }, file); |
| | | // å¼å§ OCR è¯å« |
| | | step.value = 1; |
| | | processingTip.value = 'æ£å¨è¿è¡å票è¯å«ï¼è¯·ç¨å...'; |
| | | const ocrData = await ocrPurchaseInvoice(blobId); |
| | | // æ£æ¥é误ï¼rawText 为é误æç¤ºï¼ä¸ä¸å¡å段å
¨ç©º |
| | | const hasData = !!( |
| | | ocrData.invoiceNo || |
| | | ocrData.invoiceTitle || |
| | | ocrData.supplierName || |
| | | ocrData.price != null || |
| | | ocrData.invoiceTime || |
| | | ocrData.invoiceType || |
| | | ocrData.taxRate != null || |
| | | ocrData.remark |
| | | ); |
| | | if (ocrData.rawText && !hasData) { |
| | | processingTip.value = ''; |
| | | step.value = 0; |
| | | message.error(ocrData.rawText); |
| | | return; |
| | | } |
| | | if (!hasData) { |
| | | processingTip.value = ''; |
| | | step.value = 0; |
| | | message.warning('æªè¯å«å°åç¥¨æ°æ®ï¼è¯·æå¨å½å
¥æéæ°ä¸ä¼ '); |
| | | return; |
| | | } |
| | | ocrResult.value = ocrData; |
| | | step.value = 2; |
| | | } catch { |
| | | processingTip.value = ''; |
| | | step.value = 0; |
| | | onError(new Error('ä¸ä¼ 失败')); |
| | | } |
| | | } |
| | | |
| | | function handleConfirm() { |
| | | if (ocrResult.value) { |
| | | emit('success', ocrResult.value); |
| | | } |
| | | handleClose(); |
| | | } |
| | | |
| | | function handleReset() { |
| | | step.value = 0; |
| | | fileList.value = []; |
| | | ocrResult.value = undefined; |
| | | processingTip.value = ''; |
| | | } |
| | | |
| | | function handleClose() { |
| | | open.value = false; |
| | | handleReset(); |
| | | } |
| | | |
| | | defineExpose({ open: handleOpen }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal |
| | | v-model:open="open" |
| | | title="è¯å«å½å
¥å票" |
| | | width="680px" |
| | | :footer="null" |
| | | @cancel="handleClose" |
| | | > |
| | | <!-- Step 0: ä¸ä¼ --> |
| | | <template v-if="step === 0"> |
| | | <Upload.Dragger |
| | | v-model:file-list="fileList" |
| | | :max-count="1" |
| | | :custom-request="handleUpload" |
| | | accept=".pdf,.png,.jpg,.jpeg" |
| | | @remove="handleReset" |
| | | > |
| | | <p class="text-4xl text-gray-400"> |
| | | <IconifyIcon icon="ant-design:inbox-outlined" /> |
| | | </p> |
| | | <p class="text-base text-gray-500">ç¹å»æææ½å票æä»¶å°æ¤åºåä¸ä¼ </p> |
| | | <p class="text-sm text-gray-400">æ¯æ PDFãJPGãPNG æ ¼å¼ï¼ä¸æ¥ç¥¨éä»¶ä¸ä¼ æ ¼å¼ä¸è´</p> |
| | | </Upload.Dragger> |
| | | </template> |
| | | |
| | | <!-- Step 1: è¯å«ä¸ --> |
| | | <template v-else-if="step === 1"> |
| | | <div class="flex flex-col items-center gap-4 py-12"> |
| | | <a-spin size="large" /> |
| | | <span class="text-base text-gray-500">{{ processingTip }}</span> |
| | | </div> |
| | | </template> |
| | | |
| | | <!-- Step 2: é¢è§ç»æ --> |
| | | <template v-else-if="step === 2 && ocrResult"> |
| | | <div class="mb-4"> |
| | | <Descriptions :column="2" bordered size="small"> |
| | | <Descriptions.Item v-if="ocrResult.invoiceNo" label="å票å·ç "> |
| | | {{ ocrResult.invoiceNo }} |
| | | </Descriptions.Item> |
| | | <Descriptions.Item v-if="ocrResult.invoiceType" label="å票类å"> |
| | | {{ ocrResult.invoiceType }} |
| | | </Descriptions.Item> |
| | | <Descriptions.Item v-if="ocrResult.invoiceTitle" label="å票æ¬å¤´"> |
| | | {{ ocrResult.invoiceTitle }} |
| | | </Descriptions.Item> |
| | | <Descriptions.Item v-if="ocrResult.supplierName" label="é宿¹ï¼éæ ¸å¯¹ï¼"> |
| | | <Tag color="orange">{{ ocrResult.supplierName }}</Tag> |
| | | </Descriptions.Item> |
| | | <Descriptions.Item v-if="ocrResult.price != null" label="æ¥ç¥¨éé¢ï¼å
ï¼"> |
| | | {{ ocrResult.price }} |
| | | </Descriptions.Item> |
| | | <Descriptions.Item v-if="ocrResult.invoiceTime" label="å¼ç¥¨æ¥æ"> |
| | | {{ ocrResult.invoiceTime }} |
| | | </Descriptions.Item> |
| | | <Descriptions.Item v-if="ocrResult.taxRate != null" label="ç¨ç(%)"> |
| | | {{ ocrResult.taxRate }} |
| | | </Descriptions.Item> |
| | | <Descriptions.Item v-if="ocrResult.remark" label="夿³¨" :span="2"> |
| | | {{ ocrResult.remark }} |
| | | </Descriptions.Item> |
| | | </Descriptions> |
| | | </div> |
| | | |
| | | <Alert |
| | | message="é宿¹åç§°ä»
为åç¥¨ææ¬è¯å«ç»æï¼è¯·å¨è¡¨åä¸éæ©éè´è®¢ååæ ¸å¯¹è®¢åä¾åºåä¸é宿¹æ¯å¦ä¸è´ï¼è¯å«æ°æ®å¯æå¨ä¿®æ¹ã" |
| | | type="info" |
| | | show-icon |
| | | class="mb-4" |
| | | /> |
| | | |
| | | <div class="flex justify-end gap-2"> |
| | | <Button @click="handleReset">éæ°ä¸ä¼ </Button> |
| | | <Button type="primary" @click="handleConfirm">确认并填å
¥è¡¨å</Button> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { ErpPurchaseOrderApi } from '#/api/erp/purchase/order'; |
| | | |
| | | import { computed, nextTick, ref, watch } from 'vue'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { Input, message, Modal } from 'ant-design-vue'; |
| | | |
| | | import { useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | getPurchaseOrder, |
| | | getPurchaseOrderPage, |
| | | } from '#/api/erp/purchase/order'; |
| | | |
| | | defineOptions({ name: 'ErpPurchaseInvoiceOrderSelect' }); |
| | | |
| | | const props = withDefaults( |
| | | defineProps<{ |
| | | disabled?: boolean; |
| | | modelValue?: number; |
| | | placeholder?: string; |
| | | }>(), |
| | | { |
| | | disabled: false, |
| | | modelValue: undefined, |
| | | placeholder: 'è¯·éæ©éè´è®¢å', |
| | | }, |
| | | ); |
| | | const emit = defineEmits<{ |
| | | change: [order: ErpPurchaseOrderApi.PurchaseOrder | undefined]; |
| | | 'update:modelValue': [value: number | undefined]; |
| | | }>(); |
| | | |
| | | const open = ref(false); // éæ©å¼¹çªæ¯å¦æå¼ |
| | | const order = ref<ErpPurchaseOrderApi.PurchaseOrder>(); // å½åéä¸éè´è®¢å |
| | | |
| | | const displayNo = computed(() => order.value?.no ?? ''); |
| | | |
| | | /** æ ¹æ®è®¢åç¼å·åæ¾éæ©å¨ */ |
| | | async function resolveOrderById(id: number | undefined) { |
| | | if (!id) { |
| | | order.value = undefined; |
| | | return; |
| | | } |
| | | if (order.value?.id === id) { |
| | | return; |
| | | } |
| | | order.value = await getPurchaseOrder(id); |
| | | } |
| | | |
| | | watch( |
| | | () => props.modelValue, |
| | | (value) => { |
| | | resolveOrderById(value); |
| | | }, |
| | | { immediate: true }, |
| | | ); |
| | | |
| | | /** è¡¨æ ¼é
ç½® */ |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: [ |
| | | { |
| | | fieldName: 'no', |
| | | label: '订ååå·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥è®¢ååå·', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | ], |
| | | }, |
| | | gridOptions: { |
| | | columns: [ |
| | | { type: 'radio', width: 50, fixed: 'left' }, |
| | | { field: 'no', title: '订ååå·', width: 200, fixed: 'left' }, |
| | | { field: 'supplierName', title: 'ä¾åºå', minWidth: 120 }, |
| | | { |
| | | field: 'orderTime', |
| | | title: 'è®¢åæ¶é´', |
| | | width: 160, |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'totalPrice', |
| | | title: 'å«ç¨éé¢', |
| | | formatter: 'formatAmount2', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'remainInvoicePrice', |
| | | title: 'æªå¼ç¥¨éé¢', |
| | | formatter: 'formatAmount2', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | minWidth: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.ERP_AUDIT_STATUS }, |
| | | }, |
| | | }, |
| | | ], |
| | | height: 520, |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getPurchaseOrderPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | status: 20, // ä»
å±ç¤ºå·²å®¡æ ¸éè¿çéè´è®¢å |
| | | remainPriceEnable: true, // ä»
å±ç¤ºå©ä½å¯å¼ç¥¨éé¢å¤§äº 0 ç订åï¼éé¢å·²å¼å®çä¸å±ç¤ºï¼ |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | radioConfig: { |
| | | trigger: 'row', |
| | | highlight: true, |
| | | }, |
| | | toolbarConfig: { |
| | | refresh: true, |
| | | search: true, |
| | | }, |
| | | } as VxeTableGridOptions<ErpPurchaseOrderApi.PurchaseOrder>, |
| | | gridEvents: { |
| | | radioChange: ({ row }: { row: ErpPurchaseOrderApi.PurchaseOrder }) => { |
| | | order.value = row; |
| | | }, |
| | | }, |
| | | }); |
| | | |
| | | /** æå¼éæ©å¼¹çª */ |
| | | async function handleOpen() { |
| | | if (props.disabled) { |
| | | return; |
| | | } |
| | | open.value = true; |
| | | await nextTick(); |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** ç¡®è®¤éæ©éè´è®¢å */ |
| | | function handleOk() { |
| | | if (!order.value?.id) { |
| | | message.warning('è¯·éæ©éè´è®¢å'); |
| | | return; |
| | | } |
| | | emit('update:modelValue', order.value.id); |
| | | emit('change', order.value); |
| | | open.value = false; |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <div> |
| | | <Input |
| | | readonly |
| | | :disabled="disabled" |
| | | :placeholder="placeholder" |
| | | :value="displayNo" |
| | | @click="handleOpen" |
| | | > |
| | | <template #suffix> |
| | | <IconifyIcon |
| | | class="size-4 cursor-pointer" |
| | | icon="lucide:search" |
| | | :style="{ cursor: disabled ? 'not-allowed' : 'pointer' }" |
| | | /> |
| | | </template> |
| | | </Input> |
| | | <Modal |
| | | v-model:open="open" |
| | | title="éæ©éè´è®¢åï¼ä»
å·²å®¡æ ¸éè¿ï¼" |
| | | width="80%" |
| | | @cancel.stop="open = false" |
| | | @ok.stop="handleOk" |
| | | > |
| | | <Grid table-title="éè´è®¢åå表" /> |
| | | </Modal> |
| | | </div> |
| | | </template> |
| | |
| | | import { getSupplierSimpleList } from '#/api/srm/supplier'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import { MdmItemSelect } from '#/views/basicData/mdm/components'; |
| | | import { getRangePickerDefaultProps } from '#/utils'; |
| | | import { getRangePickerDefaultProps, withoutFixedColumns } from '#/utils'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'edit'; |
| | |
| | | }, |
| | | { |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | componentProps: (values) => ({ |
| | | class: '!w-full', |
| | | placeholder: '请è¾å
¥æ¯ä»è®¢é', |
| | | precision: 2, |
| | | min: 0, |
| | | }, |
| | | max: values.totalPrice ?? undefined, |
| | | }), |
| | | fieldName: 'depositPrice', |
| | | label: 'æ¯ä»è®¢é', |
| | | dependencies: { |
| | | triggerFields: ['totalPrice'], |
| | | rules(values) { |
| | | if (values.totalPrice != null) { |
| | | return z |
| | | .number() |
| | | .min(0, '订éä¸è½å°äº0') |
| | | .max(values.totalPrice, '订éä¸è½å¤§äºä¼æ åéé¢'); |
| | | } |
| | | return z.number().min(0, '订éä¸è½å°äº0'); |
| | | }, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | |
| | | export function useFormItemColumns( |
| | | disabled: boolean, |
| | | ): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | const columns: VxeTableGridOptions['columns'] = [ |
| | | { type: 'seq', title: 'åºå·', minWidth: 50, fixed: 'left' }, |
| | | { |
| | | field: 'productId', |
| | |
| | | visible: !disabled, |
| | | }, |
| | | ]; |
| | | // æ¥çå®¡æ¹æç»ï¼åªè¯»ï¼æ¶åæ¶åºå®åï¼é¿å
çªå¼¹çªéæ¤æäº§ååç§°ãåä½ã夿³¨çå |
| | | return disabled ? withoutFixedColumns(columns) : columns; |
| | | } |
| | | |
| | | /** å表çæç´¢è¡¨å */ |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { ErpPurchaseOrderApi } from '#/api/erp/purchase/order'; |
| | | import type { DescriptionItemSchema } from '#/components/description'; |
| | | |
| | | import { h, onMounted, ref } from 'vue'; |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { |
| | | erpCountInputFormatter, |
| | | erpPriceInputFormatter, |
| | | formatDateTime, |
| | | } from '@vben/utils'; |
| | | |
| | | import { Spin, Tag } from 'ant-design-vue'; |
| | | |
| | | import { getPurchaseOrder } from '#/api/erp/purchase/order'; |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | import { useDescription } from '#/components/description'; |
| | | |
| | | import ItemForm from '../modules/item-form.vue'; |
| | | |
| | | defineOptions({ name: 'ErpPurchaseOrderDetail' }); |
| | | |
| | | const props = defineProps<{ id: string }>(); |
| | | |
| | | const loading = ref(false); |
| | | const formData = ref<ErpPurchaseOrderApi.PurchaseOrder>(); |
| | | |
| | | /** ææ¬åæ®µç©ºå¼å ä½ */ |
| | | function textRender(val: unknown) { |
| | | return val === 0 || val ? String(val) : '-'; |
| | | } |
| | | |
| | | /** å
¥åºç¶ææ¸²æ */ |
| | | function renderInStatus(val?: number) { |
| | | const map: Record<number, { text: string; color: string }> = { |
| | | 0: { text: 'æªå
¥åº', color: 'default' }, |
| | | 1: { text: 'é¨åå
¥åº', color: 'warning' }, |
| | | 2: { text: 'å
¨é¨å
¥åº', color: 'success' }, |
| | | }; |
| | | const item = map[val ?? 0]!; |
| | | return h(Tag, { color: item.color }, () => item.text); |
| | | } |
| | | |
| | | const [Descriptions] = useDescription({ |
| | | bordered: true, |
| | | column: 2, |
| | | schema: [ |
| | | { label: 'éè´è®¢åå·', field: 'no' }, |
| | | { label: 'ä¾åºååç§°', field: 'supplierName', render: textRender }, |
| | | { |
| | | label: 'è®¢åæ¶é´', |
| | | field: 'orderTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { |
| | | label: 'å计æ°é', |
| | | field: 'totalCount', |
| | | render: (val) => erpCountInputFormatter(val) || '-', |
| | | }, |
| | | { |
| | | label: 'å计éé¢', |
| | | field: 'totalPrice', |
| | | render: (val) => erpPriceInputFormatter(val) || '-', |
| | | }, |
| | | { |
| | | label: '伿 éé¢', |
| | | field: 'discountPrice', |
| | | render: (val) => erpPriceInputFormatter(val) || '-', |
| | | }, |
| | | { |
| | | label: 'å®ééé¢', |
| | | field: 'depositPrice', |
| | | render: (val) => erpPriceInputFormatter(val) || '-', |
| | | }, |
| | | { |
| | | label: '审æ¹ç¶æ', |
| | | field: 'status', |
| | | render: (val) => |
| | | h(DictTag, { type: DICT_TYPE.ERP_AUDIT_STATUS, value: val }), |
| | | }, |
| | | { |
| | | label: 'å
¥åºç¶æ', |
| | | field: 'inStatus', |
| | | render: (val) => renderInStatus(val as number), |
| | | }, |
| | | { label: 'å建人', field: 'creatorName', render: textRender }, |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { label: '夿³¨', field: 'remark', render: textRender }, |
| | | ] as DescriptionItemSchema[], |
| | | }); |
| | | |
| | | onMounted(async () => { |
| | | try { |
| | | loading.value = true; |
| | | formData.value = await getPurchaseOrder(Number(props.id)); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <ContentWrap class="m-2"> |
| | | <Spin :spinning="loading" tip="å è½½ä¸..."> |
| | | <div class="space-y-4"> |
| | | <!-- åºæ¬ä¿¡æ¯ --> |
| | | <div class="rounded-lg border border-border bg-background p-4"> |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">åºæ¬ä¿¡æ¯</span> |
| | | </div> |
| | | <Descriptions :data="formData" /> |
| | | </div> |
| | | <!-- éè´æç» --> |
| | | <div |
| | | v-if="formData?.items?.length" |
| | | class="rounded-lg border border-border bg-background p-4" |
| | | > |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">éè´æç»</span> |
| | | </div> |
| | | <ItemForm :items="formData.items" :disabled="true" /> |
| | | </div> |
| | | </div> |
| | | </Spin> |
| | | </ContentWrap> |
| | | </template> |
| | |
| | | deletePurchaseOrder, |
| | | exportPurchaseOrder, |
| | | getPurchaseOrderPage, |
| | | updatePurchaseOrderStatus, |
| | | } from '#/api/erp/purchase/order'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | import ImportForm from './modules/import-form.vue'; |
| | | |
| | | /** ERP éè´è®¢åå表 */ |
| | | defineOptions({ name: 'ErpPurchaseOrder' }); |
| | |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ImportModal, importModalApi] = useVbenModal({ |
| | | connectedComponent: ImportForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | |
| | | async function handleExport() { |
| | | const data = await exportPurchaseOrder(await gridApi.formApi.getValues()); |
| | | downloadFileFromBlobPart({ fileName: 'éè´è®¢å.xls', source: data }); |
| | | } |
| | | |
| | | /** 导å
¥éè´è®¢å */ |
| | | function handleImport() { |
| | | importModalApi.open(); |
| | | } |
| | | |
| | | /** æ°å¢éè´è®¢å */ |
| | |
| | | try { |
| | | await deletePurchaseOrder(ids); |
| | | message.success($t('ui.actionMessage.deleteSuccess')); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** 审æ¹/åå®¡æ¹æä½ */ |
| | | async function handleUpdateStatus( |
| | | row: ErpPurchaseOrderApi.PurchaseOrder, |
| | | status: number, |
| | | ) { |
| | | const hideLoading = message.loading({ |
| | | content: `ç¡®å®${status === 20 ? '审æ¹' : 'å审æ¹'}该订ååï¼`, |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await updatePurchaseOrderStatus(row.id!, status); |
| | | message.success(`${status === 20 ? '审æ¹' : 'å审æ¹'}æå`); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height><FormModal @success="handleRefresh" /> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <ImportModal @success="handleRefresh" /> |
| | | <Grid table-title="éè´è®¢åå表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | |
| | | icon: ACTION_ICON.DOWNLOAD, |
| | | auth: ['erp:purchase-order:export'], |
| | | onClick: handleExport, |
| | | }, |
| | | { |
| | | label: '导å
¥', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.UPLOAD, |
| | | auth: ['erp:purchase-order:import'], |
| | | onClick: handleImport, |
| | | }, |
| | | { |
| | | label: 'æ¹éå é¤', |
| | |
| | | auth: ['erp:purchase-order:update'], |
| | | ifShow: () => row.status !== 20, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: row.status === 10 ? '审æ¹' : 'å审æ¹', |
| | | type: 'link', |
| | | icon: ACTION_ICON.AUDIT, |
| | | auth: ['erp:purchase-order:update-status'], |
| | | popConfirm: { |
| | | title: `确认${row.status === 10 ? '审æ¹' : 'å审æ¹'}${row.no}åï¼`, |
| | | confirm: handleUpdateStatus.bind( |
| | | null, |
| | | row, |
| | | row.status === 10 ? 20 : 10, |
| | | ), |
| | | }, |
| | | }, |
| | | { |
| | | label: '确认æ¶è´§', |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FileType } from 'ant-design-vue/es/upload/interface'; |
| | | import type { ImportResult } from '#/api/erp/purchase/order'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | import { downloadFileFromBlobPart } from '#/packages/utils/src'; |
| | | |
| | | import { Button, message, Progress, Switch, Upload } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | importPurchaseOrder, |
| | | importPurchaseOrderTemplate, |
| | | } from '#/api/erp/purchase/order'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const fileList = ref<FileType[]>([]); |
| | | /** æ¯å¦å°å¯¼å
¥ç订åç´æ¥ç½®ä¸ºâå·²å®¡æ ¸â */ |
| | | const directApprove = ref(false); |
| | | const importResult = ref<ImportResult>(); |
| | | const importing = ref(false); |
| | | const progressPercent = ref(0); |
| | | let progressTimer: ReturnType<typeof setInterval> | undefined; |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | let shouldClose = false; |
| | | if (fileList.value.length === 0) { |
| | | message.warning('è¯·éæ©è¦å¯¼å
¥çæä»¶'); |
| | | return; |
| | | } |
| | | if (importing.value) return; |
| | | modalApi.lock(); |
| | | importing.value = true; |
| | | startMockProgress(); |
| | | try { |
| | | importResult.value = await importPurchaseOrder( |
| | | fileList.value[0] as File, |
| | | directApprove.value, |
| | | ); |
| | | // 宿ï¼è¿åº¦æ¡èµ°æ»¡ |
| | | stopMockProgress(); |
| | | progressPercent.value = 100; |
| | | await delay(300); |
| | | emit('success'); |
| | | const failedCount = Object.keys(importResult.value?.failureList ?? {}).length; |
| | | if (failedCount > 0) { |
| | | message.warning( |
| | | `导å
¥å®æï¼æ°å¢ ${createCount.value} æ¡ï¼å¤±è´¥ ${failedCount} æ¡ï¼è¯·æ¥ç䏿¹å¤±è´¥æç»`, |
| | | ); |
| | | } else { |
| | | message.success('导å
¥å®æ'); |
| | | shouldClose = true; |
| | | } |
| | | } finally { |
| | | stopMockProgress(); |
| | | importing.value = false; |
| | | modalApi.unlock(); |
| | | if (shouldClose) { |
| | | await modalApi.close(); |
| | | } |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | /** 模æè¿åº¦æ¡ï¼è¯·æ±æé´ä» 0 èµ°å° 90% */ |
| | | function startMockProgress() { |
| | | progressPercent.value = 0; |
| | | progressTimer = setInterval(() => { |
| | | if (progressPercent.value < 90) { |
| | | progressPercent.value = Math.min( |
| | | 90, |
| | | progressPercent.value + Math.random() * 12, |
| | | ); |
| | | } |
| | | }, 300); |
| | | } |
| | | |
| | | function stopMockProgress() { |
| | | if (progressTimer) { |
| | | clearInterval(progressTimer); |
| | | progressTimer = undefined; |
| | | } |
| | | } |
| | | |
| | | function delay(ms: number) { |
| | | return new Promise((resolve) => setTimeout(resolve, ms)); |
| | | } |
| | | |
| | | function beforeUpload(file: FileType) { |
| | | fileList.value = [file]; |
| | | importResult.value = undefined; |
| | | return false; |
| | | } |
| | | |
| | | function handleRemove() { |
| | | fileList.value = []; |
| | | importResult.value = undefined; |
| | | } |
| | | |
| | | async function handleDownloadTemplate() { |
| | | const data = await importPurchaseOrderTemplate(); |
| | | downloadFileFromBlobPart({ fileName: 'éè´è®¢å导å
¥æ¨¡æ¿.xls', source: data }); |
| | | } |
| | | |
| | | const createCount = computed(() => importResult.value?.createList?.length ?? 0); |
| | | const updateCount = computed(() => importResult.value?.updateList?.length ?? 0); |
| | | const failureCount = computed( |
| | | () => Object.keys(importResult.value?.failureList ?? {}).length, |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="导å
¥éè´è®¢å" class="w-[720px]"> |
| | | <div class="mx-4"> |
| | | <Upload |
| | | :max-count="1" |
| | | accept=".xls,.xlsx" |
| | | :file-list="fileList" |
| | | :before-upload="beforeUpload" |
| | | @remove="handleRemove" |
| | | > |
| | | <Button type="primary">éæ© Excel æä»¶</Button> |
| | | </Upload> |
| | | <div class="mt-4"> |
| | | <span class="mr-2">æ¯å¦å®¡æ ¸</span> |
| | | <Switch v-model:checked="directApprove" /> |
| | | </div> |
| | | <div class="mt-2 rounded-md bg-gray-50 p-2 text-xs leading-5 text-gray-500"> |
| | | <p>ä»
å
许导å
¥ xlsãxlsx æ ¼å¼æä»¶ï¼æ¨¡æ¿ä¸âä¾åºååç§°âæåç§°ï¼æç¼ç ï¼å¡«åï¼éä¸ç³»ç»å
ä¾åºåä¸è´ï¼â产åç¼ç âæç©æç¼ç å¡«åï¼â产ååç§°âåä»
ä¾å¯¹ç
§ï¼å¹é
以产åç¼ç 为åï¼ï¼âéè´æ¶é´âæ ¼å¼å¦ 2026-01-01 10:00:00ï¼ç空åå½åæ¶é´ã</p> |
| | | <p>ä¸å¼ éè´åå¯å«å¤æ¡æç»ï¼ç¸åâéè´åç¼å·âçå¤è¡ä¼å并为ä¸å¼ 订åï¼ä¸»è¡¨å段å该ç»é¦è¡ï¼ï¼âéè´åç¼å·âçç©ºæ¶æ¯ä¸è¡åçæä¸å¼ æ°è®¢åãéè´åç¼å·å·²å卿¶ä¼å¯¼å
¥å¤±è´¥ã</p> |
| | | <p>订åç¶æï¼å¾é䏿¹å¼å
³ = ç´æ¥ç½®ä¸ºâå·²å®¡æ ¸âï¼ä¸å¾é = âæªå®¡æ ¸âã导å
¥ä¸ä¼èªå¨æäº¤å®¡æ¹æµç¨ã</p> |
| | | </div> |
| | | <!-- 导å
¥è¿åº¦æ¡ --> |
| | | <div v-if="importing" class="mt-4"> |
| | | <div class="mb-2 text-sm text-gray-600">æ£å¨å¯¼å
¥éè´è®¢åæ°æ®ï¼è¯·ç¨å...</div> |
| | | <Progress :percent="progressPercent" status="active" /> |
| | | </div> |
| | | <!-- 导å
¥ç»æ --> |
| | | <div |
| | | v-if="importResult" |
| | | class="mt-4 rounded-md border border-gray-200 bg-gray-50 p-3" |
| | | > |
| | | <div class="mb-2 font-medium">导å
¥ç»æ</div> |
| | | <div class="mb-2 flex gap-4"> |
| | | <span class="text-green-600">æ°å¢ï¼{{ createCount }} å¼ </span> |
| | | <span class="text-blue-600">è¦çæ´æ°ï¼{{ updateCount }} å¼ </span> |
| | | <span class="text-red-600">失败ï¼{{ failureCount }} å¼ </span> |
| | | </div> |
| | | <div v-if="failureCount > 0" class="max-h-40 overflow-y-auto"> |
| | | <div |
| | | v-for="(reason, name) in importResult.failureList" |
| | | :key="name" |
| | | class="text-sm leading-6 text-red-500" |
| | | > |
| | | {{ name }}ï¼{{ reason }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <template #prepend-footer> |
| | | <Button @click="handleDownloadTemplate">ä¸è½½å¯¼å
¥æ¨¡æ¿</Button> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| | |
| | | options: [ |
| | | { label: 'æªæäº¤', value: 0 }, |
| | | { label: '审æ¹ä¸', value: 10 }, |
| | | { label: 'å®¡æ ¸éè¿', value: 20 }, |
| | | { label: 'å®¡æ ¸ä¸éè¿', value: 30 }, |
| | | { label: '审æ¹éè¿', value: 20 }, |
| | | { label: '审æ¹ä¸éè¿', value: 30 }, |
| | | { label: '已忶', value: 40 }, |
| | | ], |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { ErpPurchaseRequestApi } from '#/api/erp/purchase/request'; |
| | | import type { DescriptionItemSchema } from '#/components/description'; |
| | | |
| | | import { h, nextTick, onMounted, ref } from 'vue'; |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { |
| | | erpCountInputFormatter, |
| | | erpPriceInputFormatter, |
| | | formatDateTime, |
| | | } from '@vben/utils'; |
| | | |
| | | import { Spin } from 'ant-design-vue'; |
| | | |
| | | import { useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getPurchaseRequest } from '#/api/erp/purchase/request'; |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | import { useDescription } from '#/components/description'; |
| | | |
| | | defineOptions({ name: 'ErpPurchaseRequestDetail' }); |
| | | |
| | | const props = defineProps<{ id: string }>(); |
| | | |
| | | const loading = ref(false); |
| | | const formData = ref<ErpPurchaseRequestApi.PurchaseRequest>(); |
| | | |
| | | /** ææ¬åæ®µç©ºå¼å ä½ */ |
| | | function textRender(val: unknown) { |
| | | return val === 0 || val ? String(val) : '-'; |
| | | } |
| | | |
| | | /** éè´ç³è¯·æç»ï¼å®¡æ¹è¯¦æ
åªè¯»åï¼ */ |
| | | function useDetailItemColumns(): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { type: 'seq', title: 'åºå·', width: 60 }, |
| | | { field: 'productName', title: '产ååç§°', minWidth: 180 }, |
| | | { field: 'productUnitName', title: 'åä½', minWidth: 80 }, |
| | | { |
| | | field: 'count', |
| | | title: 'æ°é', |
| | | minWidth: 100, |
| | | formatter: ({ cellValue }) => erpCountInputFormatter(cellValue) || '-', |
| | | }, |
| | | { |
| | | field: 'productPrice', |
| | | title: 'åä»·ï¼åèä»·ï¼', |
| | | minWidth: 120, |
| | | formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-', |
| | | }, |
| | | { |
| | | field: 'totalPrice', |
| | | title: 'é¢ä¼°éé¢', |
| | | minWidth: 120, |
| | | formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-', |
| | | }, |
| | | { |
| | | field: 'taxPercent', |
| | | title: 'ç¨ç(%)', |
| | | minWidth: 90, |
| | | formatter: ({ cellValue }) => (cellValue ? `${cellValue}%` : '-'), |
| | | }, |
| | | { field: 'demandTime', title: 'éæ±æ¥æ', minWidth: 110 }, |
| | | { |
| | | field: 'remark', |
| | | title: '夿³¨', |
| | | minWidth: 160, |
| | | showOverflow: 'tooltip', |
| | | }, |
| | | { |
| | | field: 'qcCheckFlag', |
| | | title: 'æ¯å¦è´¨æ£', |
| | | minWidth: 80, |
| | | formatter: ({ cellValue }) => (cellValue ? 'æ¯' : 'å¦'), |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | gridOptions: { |
| | | columns: useDetailItemColumns(), |
| | | data: [], |
| | | minHeight: 200, |
| | | autoResize: true, |
| | | border: true, |
| | | rowConfig: { |
| | | keyField: 'seq', |
| | | isHover: true, |
| | | }, |
| | | pagerConfig: { |
| | | enabled: false, |
| | | }, |
| | | toolbarConfig: { |
| | | enabled: false, |
| | | }, |
| | | }, |
| | | }); |
| | | |
| | | const [Descriptions] = useDescription({ |
| | | bordered: true, |
| | | column: 2, |
| | | schema: [ |
| | | { label: 'ç³è¯·ç¼å·', field: 'no' }, |
| | | { |
| | | label: '审æ¹ç¶æ', |
| | | field: 'status', |
| | | render: (val) => |
| | | h(DictTag, { type: DICT_TYPE.ERP_AUDIT_STATUS, value: val }), |
| | | }, |
| | | { label: 'ç³è¯·äºº', field: 'requestUserName', render: textRender }, |
| | | { label: 'ç³è¯·é¨é¨', field: 'requestDeptName', render: textRender }, |
| | | { |
| | | label: 'ç³è¯·æ¶é´', |
| | | field: 'requestTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { label: 'ä¾åºååç§°', field: 'supplierName', render: textRender }, |
| | | { |
| | | label: 'å计æ°é', |
| | | field: 'totalCount', |
| | | render: (val) => erpCountInputFormatter(val) || '-', |
| | | }, |
| | | { |
| | | label: 'å计éé¢', |
| | | field: 'totalPrice', |
| | | render: (val) => erpPriceInputFormatter(val) || '-', |
| | | }, |
| | | { label: 'ç³è¯·çç±', field: 'requestReason', render: textRender }, |
| | | { label: 'çæéè´è®¢å', field: 'orderNo', render: textRender }, |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { label: '夿³¨', field: 'remark', render: textRender }, |
| | | ] as DescriptionItemSchema[], |
| | | }); |
| | | |
| | | onMounted(async () => { |
| | | try { |
| | | loading.value = true; |
| | | formData.value = await getPurchaseRequest(Number(props.id)); |
| | | if (formData.value?.items?.length) { |
| | | await nextTick(); |
| | | await gridApi.grid.reloadData(formData.value.items); |
| | | } |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <ContentWrap class="m-2"> |
| | | <Spin :spinning="loading" tip="å è½½ä¸..."> |
| | | <div class="space-y-4"> |
| | | <!-- åºæ¬ä¿¡æ¯ --> |
| | | <div class="rounded-lg border border-border bg-background p-4"> |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">åºæ¬ä¿¡æ¯</span> |
| | | </div> |
| | | <Descriptions :data="formData" /> |
| | | </div> |
| | | <!-- ç³è¯·æç» --> |
| | | <div |
| | | v-if="formData?.items?.length" |
| | | class="rounded-lg border border-border bg-background p-4" |
| | | > |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">ç³è¯·æç»</span> |
| | | </div> |
| | | <Grid /> |
| | | </div> |
| | | </div> |
| | | </Spin> |
| | | </ContentWrap> |
| | | </template> |
| | |
| | | getApproveProcessList, |
| | | getPurchaseRequestPage, |
| | | submitPurchaseRequest, |
| | | syncPurchaseRequestStatus, |
| | | } from '#/api/erp/purchase/request'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | import ImportForm from './modules/import-form.vue'; |
| | | import OrderForm from './modules/order-form.vue'; |
| | | import ProcessSelectModal from './modules/process-select-modal.vue'; |
| | | |
| | |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ImportModal, importModalApi] = useVbenModal({ |
| | | connectedComponent: ImportForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | |
| | | async function handleExport() { |
| | | const data = await exportPurchaseRequest(await gridApi.formApi.getValues()); |
| | | downloadFileFromBlobPart({ fileName: 'éè´ç³è¯·.xls', source: data }); |
| | | } |
| | | |
| | | /** 导å
¥éè´ç³è¯· */ |
| | | function handleImport() { |
| | | importModalApi.open(); |
| | | } |
| | | |
| | | /** æ°å¢éè´ç³è¯· */ |
| | |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getPurchaseRequestPage({ |
| | | const result = await getPurchaseRequestPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | const processingItems = (result.list || []).filter( |
| | | (item) => item.status === 10 && item.processInstanceId, |
| | | ); |
| | | if (processingItems.length > 0) { |
| | | await syncPurchaseRequestStatus(processingItems); |
| | | } |
| | | return result; |
| | | }, |
| | | }, |
| | | }, |
| | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <ImportModal @success="handleRefresh" /> |
| | | <OrderFormModal @success="handleRefresh" /> |
| | | <ProcessModal @success="handleRefresh" /> |
| | | <Grid table-title="éè´ç³è¯·å表"> |
| | |
| | | onClick: handleExport, |
| | | }, |
| | | { |
| | | label: '导å
¥', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.UPLOAD, |
| | | auth: ['erp:purchase-request:import'], |
| | | onClick: handleImport, |
| | | }, |
| | | { |
| | | label: 'æ¹éå é¤', |
| | | type: 'primary', |
| | | danger: true, |
| | |
| | | <template #status="{ row }"> |
| | | <Tag v-if="row.status === 0" color="default">æªæäº¤</Tag> |
| | | <Tag v-else-if="row.status === 10" color="processing">审æ¹ä¸</Tag> |
| | | <Tag v-else-if="row.status === 20" color="success">å®¡æ ¸éè¿</Tag> |
| | | <Tag v-else-if="row.status === 30" color="error">å®¡æ ¸ä¸éè¿</Tag> |
| | | <Tag v-else-if="row.status === 20" color="success">审æ¹éè¿</Tag> |
| | | <Tag v-else-if="row.status === 30" color="error">审æ¹ä¸éè¿</Tag> |
| | | <Tag v-else-if="row.status === 40" color="warning">已忶</Tag> |
| | | </template> |
| | | <template #inStatus="{ row }"> |
| | |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['erp:purchase-request:update'], |
| | | ifShow: () => row.status === 0, |
| | | ifShow: () => row.status === 0 || Number(row.status) === 30, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | |
| | | import { |
| | | createPurchaseRequest, |
| | | getPurchaseRequest, |
| | | resetPurchaseRequestStatus, |
| | | syncPurchaseRequestStatus, |
| | | updatePurchaseRequest, |
| | | } from '#/api/erp/purchase/request'; |
| | | import { $t } from '#/locales'; |
| | |
| | | remark: item.remark, |
| | | })); |
| | | try { |
| | | console.log('[DEBUG] formType:', formType.value, 'status:', formData.value?.status, 'type:', typeof formData.value?.status); |
| | | if (formType.value === 'edit' && Number(formData.value?.status) !== 0) { |
| | | if (Number(formData.value?.status) !== 30) { |
| | | await syncPurchaseRequestStatus([formData.value!]); |
| | | } |
| | | await resetPurchaseRequestStatus(formData.value!.id!); |
| | | } |
| | | await (formType.value === 'create' |
| | | ? createPurchaseRequest(data) |
| | | : updatePurchaseRequest(data)); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FileType } from 'ant-design-vue/es/upload/interface'; |
| | | import type { ImportResult } from '#/api/erp/purchase/request'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | import { downloadFileFromBlobPart } from '#/packages/utils/src'; |
| | | |
| | | import { Button, message, Progress, Upload } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | importPurchaseRequest, |
| | | importPurchaseRequestTemplate, |
| | | } from '#/api/erp/purchase/request'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const fileList = ref<FileType[]>([]); |
| | | const importResult = ref<ImportResult>(); |
| | | const importing = ref(false); |
| | | const progressPercent = ref(0); |
| | | let progressTimer: ReturnType<typeof setInterval> | undefined; |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | let shouldClose = false; |
| | | if (fileList.value.length === 0) { |
| | | message.warning('è¯·éæ©è¦å¯¼å
¥çæä»¶'); |
| | | return; |
| | | } |
| | | if (importing.value) return; |
| | | modalApi.lock(); |
| | | importing.value = true; |
| | | startMockProgress(); |
| | | try { |
| | | importResult.value = await importPurchaseRequest(fileList.value[0] as File); |
| | | // 宿ï¼è¿åº¦æ¡èµ°æ»¡ |
| | | stopMockProgress(); |
| | | progressPercent.value = 100; |
| | | await delay(300); |
| | | emit('success'); |
| | | const failedCount = Object.keys(importResult.value?.failureList ?? {}).length; |
| | | if (failedCount > 0) { |
| | | message.warning( |
| | | `导å
¥å®æï¼æ°å¢ ${createCount.value} æ¡ï¼å¤±è´¥ ${failedCount} æ¡ï¼è¯·æ¥ç䏿¹å¤±è´¥æç»`, |
| | | ); |
| | | } else { |
| | | message.success('导å
¥å®æ'); |
| | | shouldClose = true; |
| | | } |
| | | } finally { |
| | | stopMockProgress(); |
| | | importing.value = false; |
| | | modalApi.unlock(); |
| | | if (shouldClose) { |
| | | await modalApi.close(); |
| | | } |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | /** 模æè¿åº¦æ¡ï¼è¯·æ±æé´ä» 0 èµ°å° 90% */ |
| | | function startMockProgress() { |
| | | progressPercent.value = 0; |
| | | progressTimer = setInterval(() => { |
| | | if (progressPercent.value < 90) { |
| | | progressPercent.value = Math.min( |
| | | 90, |
| | | progressPercent.value + Math.random() * 12, |
| | | ); |
| | | } |
| | | }, 300); |
| | | } |
| | | |
| | | function stopMockProgress() { |
| | | if (progressTimer) { |
| | | clearInterval(progressTimer); |
| | | progressTimer = undefined; |
| | | } |
| | | } |
| | | |
| | | function delay(ms: number) { |
| | | return new Promise((resolve) => setTimeout(resolve, ms)); |
| | | } |
| | | |
| | | function beforeUpload(file: FileType) { |
| | | fileList.value = [file]; |
| | | importResult.value = undefined; |
| | | return false; |
| | | } |
| | | |
| | | function handleRemove() { |
| | | fileList.value = []; |
| | | importResult.value = undefined; |
| | | } |
| | | |
| | | async function handleDownloadTemplate() { |
| | | const data = await importPurchaseRequestTemplate(); |
| | | downloadFileFromBlobPart({ fileName: 'éè´ç³è¯·å¯¼å
¥æ¨¡æ¿.xls', source: data }); |
| | | } |
| | | |
| | | const createCount = computed(() => importResult.value?.createList?.length ?? 0); |
| | | const updateCount = computed(() => importResult.value?.updateList?.length ?? 0); |
| | | const failureCount = computed( |
| | | () => Object.keys(importResult.value?.failureList ?? {}).length, |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="导å
¥éè´ç³è¯·" class="w-[720px]"> |
| | | <div class="mx-4"> |
| | | <Upload |
| | | :max-count="1" |
| | | accept=".xls,.xlsx" |
| | | :file-list="fileList" |
| | | :before-upload="beforeUpload" |
| | | @remove="handleRemove" |
| | | > |
| | | <Button type="primary">éæ© Excel æä»¶</Button> |
| | | </Upload> |
| | | <div class="mt-2 rounded-md bg-gray-50 p-2 text-xs leading-5 text-gray-500"> |
| | | <p>ä»
å
许导å
¥ xlsãxlsx æ ¼å¼æä»¶ï¼å¿
å¡«åï¼äº§åç¼ç ãæ°éãâ产åç¼ç âæç©æç¼ç å¡«åï¼â产ååç§°âåä»
ä¾å¯¹ç
§ï¼ï¼ä¾åºåå¯ç©ºï¼å¡«äºéä¸ç³»ç»ä¾åºåä¸è´ï¼ï¼âéæ±æ¥æâæ ¼å¼å¦ 2026-01-01ï¼âæ¯å¦æ¥ææ£éªâå¡«âæ¯/å¦âã</p> |
| | | <p>ä¸å¼ éè´ç³è¯·å¯å«å¤æ¡æç»ï¼ç¸åâéè´ç³è¯·åå·âçå¤è¡ä¼å并为ä¸å¼ ç³è¯·ï¼ä¸»è¡¨å段å该ç»é¦è¡ï¼ï¼âéè´ç³è¯·åå·âçç©ºæ¶æ¯ä¸è¡åçæä¸å¼ æ°ç³è¯·ãéè´ç³è¯·åå·å·²å卿¶ä¼å¯¼å
¥å¤±è´¥ã</p> |
| | | <p>导å
¥çç³è¯·ï¼å
¶ãç³è¯·äºº / ç³è¯·é¨é¨ãæå½åç»å½ç¨æ·èªå¨å¸¦åºï¼ç¶æä¸ºâæªæäº¤âï¼å¯å¨å表ä¸ç»§ç»ç¼è¾ææäº¤å®¡æ¹ã</p> |
| | | </div> |
| | | <!-- 导å
¥è¿åº¦æ¡ --> |
| | | <div v-if="importing" class="mt-4"> |
| | | <div class="mb-2 text-sm text-gray-600">æ£å¨å¯¼å
¥éè´ç³è¯·æ°æ®ï¼è¯·ç¨å...</div> |
| | | <Progress :percent="progressPercent" status="active" /> |
| | | </div> |
| | | <!-- 导å
¥ç»æ --> |
| | | <div |
| | | v-if="importResult" |
| | | class="mt-4 rounded-md border border-gray-200 bg-gray-50 p-3" |
| | | > |
| | | <div class="mb-2 font-medium">导å
¥ç»æ</div> |
| | | <div class="mb-2 flex gap-4"> |
| | | <span class="text-green-600">æ°å¢ï¼{{ createCount }} æ¡</span> |
| | | <span class="text-blue-600">è¦çæ´æ°ï¼{{ updateCount }} æ¡</span> |
| | | <span class="text-red-600">失败ï¼{{ failureCount }} æ¡</span> |
| | | </div> |
| | | <div v-if="failureCount > 0" class="max-h-40 overflow-y-auto"> |
| | | <div |
| | | v-for="(reason, name) in importResult.failureList" |
| | | :key="name" |
| | | class="text-sm leading-6 text-red-500" |
| | | > |
| | | {{ name }}ï¼{{ reason }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <template #prepend-footer> |
| | | <Button @click="handleDownloadTemplate">ä¸è½½å¯¼å
¥æ¨¡æ¿</Button> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| | |
| | | import { getAccountSimpleList } from '#/api/erp/finance/account'; |
| | | import { getItemPage } from '#/api/mdm/item'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import { getRangePickerDefaultProps } from '#/utils'; |
| | | import { getRangePickerDefaultProps, withoutFixedColumns } from '#/utils'; |
| | | import CrmCustomerSelect from '#/components/crm-customer-select.vue'; |
| | | |
| | | import { markRaw } from 'vue'; |
| | |
| | | }, |
| | | { |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | componentProps: (values) => ({ |
| | | class: '!w-full', |
| | | placeholder: '请è¾å
¥æ¶å订é', |
| | | precision: 2, |
| | | min: 0, |
| | | }, |
| | | max: values.totalPrice ?? undefined, |
| | | }), |
| | | fieldName: 'depositPrice', |
| | | label: 'æ¶å订é', |
| | | dependencies: { |
| | | triggerFields: ['totalPrice'], |
| | | rules(values) { |
| | | if (values.totalPrice != null) { |
| | | return z |
| | | .number() |
| | | .min(0, '订éä¸è½å°äº0') |
| | | .max(values.totalPrice, 'æ¶å订éä¸è½å¤§äºä¼æ åéé¢'); |
| | | } |
| | | return z.number().min(0, '订éä¸è½å°äº0').optional(); |
| | | }, |
| | | }, |
| | | rules: z.number().min(0).optional(), |
| | | }, |
| | | ]; |
| | |
| | | export function useFormItemColumns( |
| | | disabled: boolean, |
| | | ): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | const columns: VxeTableGridOptions['columns'] = [ |
| | | { type: 'seq', title: 'åºå·', minWidth: 50, fixed: 'left' }, |
| | | { |
| | | field: 'productId', |
| | |
| | | visible: !disabled, |
| | | }, |
| | | ]; |
| | | // æ¥çå®¡æ¹æç»ï¼åªè¯»ï¼æ¶åæ¶åºå®åï¼é¿å
çªå¼¹çªéæ¤æäº§ååç§°ãåä½ã夿³¨çå |
| | | return disabled ? withoutFixedColumns(columns) : columns; |
| | | } |
| | | |
| | | /** å表çæç´¢è¡¨å */ |
| | |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: [ |
| | | { label: 'æªå®¡æ ¸', value: 10 }, |
| | | { label: 'æªæäº¤', value: 0 }, |
| | | { label: '审æ¹ä¸', value: 15 }, |
| | | { label: 'å·²å®¡æ ¸', value: 20 }, |
| | | { label: '已驳å', value: 35 }, |
| | | { label: 'å·²ä½åº', value: 30 }, |
| | | ], |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { ErpSaleOrderApi } from '#/api/erp/sale/order'; |
| | | import type { DescriptionItemSchema } from '#/components/description'; |
| | | |
| | | import { h, onMounted, ref } from 'vue'; |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { |
| | | erpCountInputFormatter, |
| | | erpPriceInputFormatter, |
| | | formatDateTime, |
| | | } from '@vben/utils'; |
| | | |
| | | import { Spin, Tag } from 'ant-design-vue'; |
| | | |
| | | import { getSaleOrder } from '#/api/erp/sale/order'; |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | import { useDescription } from '#/components/description'; |
| | | |
| | | import ItemForm from '../modules/item-form.vue'; |
| | | |
| | | defineOptions({ name: 'ErpSaleOrderDetail' }); |
| | | |
| | | const props = defineProps<{ id: string }>(); |
| | | |
| | | const loading = ref(false); |
| | | const formData = ref<ErpSaleOrderApi.SaleOrder>(); |
| | | |
| | | /** ææ¬åæ®µç©ºå¼å ä½ */ |
| | | function textRender(val: unknown) { |
| | | return val === 0 || val ? String(val) : '-'; |
| | | } |
| | | |
| | | /** åºåºç¶ææ¸²æ */ |
| | | function renderOutStatus(val?: number) { |
| | | const map: Record<number, { text: string; color: string }> = { |
| | | 0: { text: 'æªåºåº', color: 'default' }, |
| | | 1: { text: 'å·²é¢ç', color: 'processing' }, |
| | | 2: { text: 'é¨ååºåº', color: 'warning' }, |
| | | 3: { text: 'å
¨é¨åºåº', color: 'success' }, |
| | | }; |
| | | const item = map[val ?? 0]!; |
| | | return h(Tag, { color: item.color }, () => item.text); |
| | | } |
| | | |
| | | /** çäº§ç¶ææ¸²æ */ |
| | | function renderProductionStatus(val?: number) { |
| | | if (formData.value?.needProduction !== 1) { |
| | | return h(Tag, { color: 'default' }, () => 'æ éç产'); |
| | | } |
| | | const map: Record<number, { text: string; color: string }> = { |
| | | 0: { text: 'æªå®æ', color: 'warning' }, |
| | | 1: { text: '已宿', color: 'success' }, |
| | | }; |
| | | const item = map[val ?? 0]!; |
| | | return h(Tag, { color: item.color }, () => item.text); |
| | | } |
| | | |
| | | const [Descriptions] = useDescription({ |
| | | bordered: true, |
| | | column: 2, |
| | | schema: [ |
| | | { label: '订ååå·', field: 'no', render: textRender }, |
| | | { label: '客æ·åç§°', field: 'customerName', render: textRender }, |
| | | { label: 'å
³èåå', field: 'contractNo', render: textRender }, |
| | | { |
| | | label: 'è®¢åæ¶é´', |
| | | field: 'orderTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { |
| | | label: 'å计æ°é', |
| | | field: 'totalCount', |
| | | render: (val) => erpCountInputFormatter(val) || '-', |
| | | }, |
| | | { |
| | | label: 'å计éé¢', |
| | | field: 'totalPrice', |
| | | render: (val) => erpPriceInputFormatter(val) || '-', |
| | | }, |
| | | { |
| | | label: '伿 éé¢', |
| | | field: 'discountPrice', |
| | | render: (val) => erpPriceInputFormatter(val) || '-', |
| | | }, |
| | | { |
| | | label: 'å®ééé¢', |
| | | field: 'depositPrice', |
| | | render: (val) => erpPriceInputFormatter(val) || '-', |
| | | }, |
| | | { |
| | | label: '审æ¹ç¶æ', |
| | | field: 'status', |
| | | render: (val) => |
| | | h(DictTag, { type: DICT_TYPE.ERP_AUDIT_STATUS, value: val }), |
| | | }, |
| | | { |
| | | label: 'åºåºç¶æ', |
| | | field: 'outStatus', |
| | | render: (val) => renderOutStatus(val as number), |
| | | }, |
| | | { |
| | | label: 'çäº§ç¶æ', |
| | | field: 'productionStatus', |
| | | render: (val) => renderProductionStatus(val as number), |
| | | }, |
| | | { label: 'å建人', field: 'creatorName', render: textRender }, |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { label: '夿³¨', field: 'remark', render: textRender }, |
| | | ] as DescriptionItemSchema[], |
| | | }); |
| | | |
| | | onMounted(async () => { |
| | | try { |
| | | loading.value = true; |
| | | formData.value = await getSaleOrder(Number(props.id)); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <ContentWrap class="m-2"> |
| | | <Spin :spinning="loading" tip="å è½½ä¸..."> |
| | | <div class="space-y-4"> |
| | | <!-- åºæ¬ä¿¡æ¯ --> |
| | | <div class="rounded-lg border border-border bg-background p-4"> |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">åºæ¬ä¿¡æ¯</span> |
| | | </div> |
| | | <Descriptions :data="formData" /> |
| | | </div> |
| | | <!-- éå®æç» --> |
| | | <div |
| | | v-if="formData?.items?.length" |
| | | class="rounded-lg border border-border bg-background p-4" |
| | | > |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">é宿ç»</span> |
| | | </div> |
| | | <ItemForm :items="formData.items" :disabled="true" /> |
| | | </div> |
| | | </div> |
| | | </Spin> |
| | | </ContentWrap> |
| | | </template> |
| | |
| | | deleteSaleOrder, |
| | | enableSaleOrder, |
| | | exportSaleOrder, |
| | | getSaleOrderApproveProcessList, |
| | | getSaleOrderPage, |
| | | updateSaleOrderStatus, |
| | | submitSaleOrder, |
| | | } from '#/api/erp/sale/order'; |
| | | import { generateFromSaleOrder } from '#/api/mes/wm/salesnotice'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | import ImportForm from './modules/import-form.vue'; |
| | | import SaleOrderPrint from './modules/print.vue'; |
| | | import SalesNoticeForm from '#/views/wls/salesnotice/modules/form.vue'; |
| | | |
| | | /** ERP éå®è®¢åå表 */ |
| | |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ImportModal, importModalApi] = useVbenModal({ |
| | | connectedComponent: ImportForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | |
| | | async function handleExport() { |
| | | const data = await exportSaleOrder(await gridApi.formApi.getValues()); |
| | | downloadFileFromBlobPart({ fileName: 'éå®è®¢å.xls', source: data }); |
| | | } |
| | | |
| | | /** 导å
¥éå®è®¢å */ |
| | | function handleImport() { |
| | | importModalApi.open(); |
| | | } |
| | | |
| | | /** æ°å¢éå®è®¢å */ |
| | |
| | | } |
| | | } |
| | | |
| | | /** 审æ¹/åå®¡æ¹æä½ */ |
| | | async function handleUpdateStatus( |
| | | row: ErpSaleOrderApi.SaleOrder, |
| | | status: number, |
| | | ) { |
| | | /** æäº¤éå®è®¢å审æ¹ï¼åååå
¬ï¼ */ |
| | | async function handleSubmit(row: ErpSaleOrderApi.SaleOrder) { |
| | | // è·åå¯ç¨çå®¡æ¹æµç¨å®ä¹ |
| | | const processList = await getSaleOrderApproveProcessList(); |
| | | if (isEmpty(processList)) { |
| | | message.error('éå®è®¢åå®¡æ¹æµç¨æªé
ç½®ï¼è¯·å
å¨ãåååå
¬âæµç¨ç®¡çãä¸å建并åå¸éå®è®¢åå®¡æ¹æµç¨'); |
| | | return; |
| | | } |
| | | const hideLoading = message.loading({ |
| | | content: `ç¡®å®${status === 20 ? '审æ¹' : 'å审æ¹'}该订ååï¼`, |
| | | content: 'æ£å¨æäº¤å®¡æ¹...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await updateSaleOrderStatus(row.id!, status); |
| | | message.success(`${status === 20 ? '审æ¹' : 'å审æ¹'}æå`); |
| | | await submitSaleOrder(row.id!, processList[0].key); |
| | | message.success('æäº¤å®¡æ¹æåï¼è¯·å°åååå
¬ä¸å¤ç'); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** æ¥ç审æ¹è¿åº¦ï¼è·³è½¬åååå
¬æµç¨è¯¦æ
ï¼åªè¯»æ¨¡å¼ï¼ä¸æ¾ç¤ºå®¡æ¹æä½ï¼ */ |
| | | function handleViewApproval(row: ErpSaleOrderApi.SaleOrder) { |
| | | if (row.processInstanceId) { |
| | | router.push({ |
| | | name: 'BpmProcessInstanceDetail', |
| | | query: { id: row.processInstanceId, viewOnly: '1' }, |
| | | }); |
| | | } |
| | | } |
| | | |
| | |
| | | /** æ¥ç详æ
*/ |
| | | function handleDetail(row: ErpSaleOrderApi.SaleOrder) { |
| | | formModalApi.setData({ formType: 'detail', id: row.id }).open(); |
| | | } |
| | | |
| | | /** æå°/导åºéå®è®¢å详æ
ï¼ä¾ä¼ä¸çç« çå */ |
| | | const printRef = ref<InstanceType<typeof SaleOrderPrint>>(); |
| | | function handlePrint(row: ErpSaleOrderApi.SaleOrder) { |
| | | printRef.value?.print(row.id!); |
| | | } |
| | | |
| | | /** æ¥çå
³èåå */ |
| | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <ImportModal @success="handleRefresh" /> |
| | | <SalesNoticeFormModal /> |
| | | <SaleOrderPrint ref="printRef" /> |
| | | <Grid table-title="éå®è®¢åå表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | |
| | | icon: ACTION_ICON.DOWNLOAD, |
| | | auth: ['erp:sale-order:export'], |
| | | onClick: handleExport, |
| | | }, |
| | | { |
| | | label: '导å
¥', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.UPLOAD, |
| | | auth: ['erp:sale-order:import'], |
| | | onClick: handleImport, |
| | | }, |
| | | { |
| | | label: 'æ¹éå é¤', |
| | |
| | | <span v-else>-</span> |
| | | </template> |
| | | <template #status="{ row }"> |
| | | <Tag v-if="row.status === 10" color="warning">æªå®¡æ ¸</Tag> |
| | | <Tag v-if="row.status === 0" color="default">æªæäº¤</Tag> |
| | | <Tag v-if="row.status === 15" color="processing">审æ¹ä¸</Tag> |
| | | <Tag v-if="row.status === 20" color="success">å·²å®¡æ ¸</Tag> |
| | | <Tag v-if="row.status === 35" color="warning">已驳å</Tag> |
| | | <Tag v-if="row.status === 30" color="error">å·²ä½åº</Tag> |
| | | </template> |
| | | <template #needProduction="{ row }"> |
| | |
| | | onClick: handleDetail.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'æå°', |
| | | type: 'link', |
| | | icon: 'lucide:printer', |
| | | auth: ['erp:sale-order:query'], |
| | | onClick: handlePrint.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['erp:sale-order:update'], |
| | | ifShow: () => row.status === 10, |
| | | ifShow: () => row.status === 0 || row.status === 35, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: '审æ¹', |
| | | label: 'æäº¤å®¡æ¹', |
| | | type: 'link', |
| | | icon: ACTION_ICON.AUDIT, |
| | | auth: ['erp:sale-order:update-status'], |
| | | ifShow: () => row.status === 10, |
| | | ifShow: () => row.status === 0 || row.status === 35, |
| | | popConfirm: { |
| | | title: `确认审æ¹${row.no}åï¼`, |
| | | confirm: handleUpdateStatus.bind(null, row, 20), |
| | | title: `确认æäº¤${row.no}ç审æ¹åï¼æäº¤å审æ¹å°å¨åååå
¬ä¸å®æ`, |
| | | confirm: handleSubmit.bind(null, row), |
| | | }, |
| | | }, |
| | | { |
| | | label: 'å审æ¹', |
| | | label: '审æ¹è¿åº¦', |
| | | type: 'link', |
| | | icon: ACTION_ICON.AUDIT, |
| | | auth: ['erp:sale-order:update-status'], |
| | | ifShow: () => row.status === 20, |
| | | popConfirm: { |
| | | title: `确认å审æ¹${row.no}åï¼`, |
| | | confirm: handleUpdateStatus.bind(null, row, 10), |
| | | }, |
| | | icon: 'lucide:file-clock', |
| | | auth: ['erp:sale-order:query'], |
| | | ifShow: () => row.status === 15 && !!row.processInstanceId, |
| | | onClick: handleViewApproval.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'ä½åº', |
| | |
| | | type: 'link', |
| | | icon: 'ant-design:file-protect-outlined', |
| | | auth: ['mes:wm-sales-notice:create'], |
| | | ifShow: () => row.status === 20 && row.canCreateSalesNotice, |
| | | ifShow: () => row.status === 20 && !!row.canCreateSalesNotice, |
| | | onClick: handleGenerateNotice.bind(null, row), |
| | | }, |
| | | { |
| | |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['erp:sale-order:delete'], |
| | | ifShow: () => row.status === 10, |
| | | ifShow: () => row.status === 0 || row.status === 35, |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.no]), |
| | | confirm: handleDelete.bind(null, [row.id!]), |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FileType } from 'ant-design-vue/es/upload/interface'; |
| | | import type { ImportResult } from '#/api/erp/sale/order'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | import { downloadFileFromBlobPart } from '#/packages/utils/src'; |
| | | |
| | | import { Button, message, Progress, Upload } from 'ant-design-vue'; |
| | | |
| | | import { importSaleOrder, importSaleOrderTemplate } from '#/api/erp/sale/order'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const fileList = ref<FileType[]>([]); |
| | | const importResult = ref<ImportResult>(); |
| | | const importing = ref(false); |
| | | const progressPercent = ref(0); |
| | | let progressTimer: ReturnType<typeof setInterval> | undefined; |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | let shouldClose = false; |
| | | if (fileList.value.length === 0) { |
| | | message.warning('è¯·éæ©è¦å¯¼å
¥çæä»¶'); |
| | | return; |
| | | } |
| | | if (importing.value) return; |
| | | modalApi.lock(); |
| | | importing.value = true; |
| | | startMockProgress(); |
| | | try { |
| | | importResult.value = await importSaleOrder(fileList.value[0] as File); |
| | | // 宿ï¼è¿åº¦æ¡èµ°æ»¡ |
| | | stopMockProgress(); |
| | | progressPercent.value = 100; |
| | | await delay(300); |
| | | emit('success'); |
| | | const failedCount = Object.keys(importResult.value?.failureList ?? {}).length; |
| | | if (failedCount > 0) { |
| | | message.warning( |
| | | `导å
¥å®æï¼æ°å¢ ${createCount.value} æ¡ï¼å¤±è´¥ ${failedCount} æ¡ï¼è¯·æ¥ç䏿¹å¤±è´¥æç»`, |
| | | ); |
| | | } else { |
| | | message.success('导å
¥å®æ'); |
| | | shouldClose = true; |
| | | } |
| | | } finally { |
| | | stopMockProgress(); |
| | | importing.value = false; |
| | | modalApi.unlock(); |
| | | if (shouldClose) { |
| | | await modalApi.close(); |
| | | } |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | /** 模æè¿åº¦æ¡ï¼è¯·æ±æé´ä» 0 èµ°å° 90% */ |
| | | function startMockProgress() { |
| | | progressPercent.value = 0; |
| | | progressTimer = setInterval(() => { |
| | | if (progressPercent.value < 90) { |
| | | progressPercent.value = Math.min( |
| | | 90, |
| | | progressPercent.value + Math.random() * 12, |
| | | ); |
| | | } |
| | | }, 300); |
| | | } |
| | | |
| | | function stopMockProgress() { |
| | | if (progressTimer) { |
| | | clearInterval(progressTimer); |
| | | progressTimer = undefined; |
| | | } |
| | | } |
| | | |
| | | function delay(ms: number) { |
| | | return new Promise((resolve) => setTimeout(resolve, ms)); |
| | | } |
| | | |
| | | function beforeUpload(file: FileType) { |
| | | fileList.value = [file]; |
| | | importResult.value = undefined; |
| | | return false; |
| | | } |
| | | |
| | | function handleRemove() { |
| | | fileList.value = []; |
| | | importResult.value = undefined; |
| | | } |
| | | |
| | | async function handleDownloadTemplate() { |
| | | const data = await importSaleOrderTemplate(); |
| | | downloadFileFromBlobPart({ fileName: 'éå®è®¢å导å
¥æ¨¡æ¿.xls', source: data }); |
| | | } |
| | | |
| | | const createCount = computed(() => importResult.value?.createList?.length ?? 0); |
| | | const updateCount = computed(() => importResult.value?.updateList?.length ?? 0); |
| | | const failureCount = computed( |
| | | () => Object.keys(importResult.value?.failureList ?? {}).length, |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="导å
¥éå®è®¢å" class="w-[720px]"> |
| | | <div class="mx-4"> |
| | | <Upload |
| | | :max-count="1" |
| | | accept=".xls,.xlsx" |
| | | :file-list="fileList" |
| | | :before-upload="beforeUpload" |
| | | @remove="handleRemove" |
| | | > |
| | | <Button type="primary">éæ© Excel æä»¶</Button> |
| | | </Upload> |
| | | <div class="mt-2 rounded-md bg-gray-50 p-2 text-xs leading-5 text-gray-500"> |
| | | <p>ä»
å
许导å
¥ xlsãxlsx æ ¼å¼æä»¶ï¼æ¨¡æ¿ä¸â客æ·åç§°âæåç§°å¡«åï¼éä¸ CRM 客æ·ä¸è´ï¼â产åç¼ç âæç©æç¼ç å¡«åï¼â产ååç§°âåä»
ä¾å¯¹ç
§ï¼å¹é
以产åç¼ç 为åï¼ï¼âä¸åæ¶é´âæ ¼å¼å¦ 2026-01-01 10:00:00ï¼ç空åå½åæ¶é´ï¼âåä»·âç空åç©æéå®ä»·ã</p> |
| | | <p>ä¸å¼ éå®åå¯å«å¤æ¡æç»ï¼ç¸åâéå®åç¼å·âçå¤è¡ä¼å并为ä¸å¼ 订åï¼ä¸»è¡¨å段å该ç»é¦è¡ï¼ï¼âéå®åç¼å·âçç©ºæ¶æ¯ä¸è¡åçæä¸å¼ æ°è®¢åãéå®åç¼å·å·²å卿¶ä¼å¯¼å
¥å¤±è´¥ã</p> |
| | | <p>导å
¥åç订åä¸ºâæªæäº¤âç¶æï¼å¯å¨å表ä¸ç»§ç»ç¼è¾ææäº¤å®¡æ¹ï¼å¯¼å
¥ä¸ä¼èªå¨æäº¤å®¡æ¹æµç¨ã</p> |
| | | </div> |
| | | <!-- 导å
¥è¿åº¦æ¡ --> |
| | | <div v-if="importing" class="mt-4"> |
| | | <div class="mb-2 text-sm text-gray-600">æ£å¨å¯¼å
¥éå®è®¢åæ°æ®ï¼è¯·ç¨å...</div> |
| | | <Progress :percent="progressPercent" status="active" /> |
| | | </div> |
| | | <!-- 导å
¥ç»æ --> |
| | | <div |
| | | v-if="importResult" |
| | | class="mt-4 rounded-md border border-gray-200 bg-gray-50 p-3" |
| | | > |
| | | <div class="mb-2 font-medium">导å
¥ç»æ</div> |
| | | <div class="mb-2 flex gap-4"> |
| | | <span class="text-green-600">æ°å¢ï¼{{ createCount }} å¼ </span> |
| | | <span class="text-blue-600">è¦çæ´æ°ï¼{{ updateCount }} å¼ </span> |
| | | <span class="text-red-600">失败ï¼{{ failureCount }} å¼ </span> |
| | | </div> |
| | | <div v-if="failureCount > 0" class="max-h-40 overflow-y-auto"> |
| | | <div |
| | | v-for="(reason, name) in importResult.failureList" |
| | | :key="name" |
| | | class="text-sm leading-6 text-red-500" |
| | | > |
| | | {{ name }}ï¼{{ reason }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <template #prepend-footer> |
| | | <Button @click="handleDownloadTemplate">ä¸è½½å¯¼å
¥æ¨¡æ¿</Button> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| | |
| | | minHeight: 250, |
| | | autoResize: true, |
| | | border: true, |
| | | // æç»åè¾å¤ï¼å¯ç¨æ¨ªåæ»å¨ï¼é¿å
å被åç¼©å¯¼è´æ¾ç¤ºä¸å®æ´ï¼åååå
¬å®¡æ¹è¯¦æ
ççªå®¹å¨åºæ¯ï¼ |
| | | scroll: { |
| | | x: 'auto', |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'seq', |
| | | isHover: true, |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { ErpSaleOrderApi } from '#/api/erp/sale/order'; |
| | | |
| | | import { computed, nextTick, ref } from 'vue'; |
| | | |
| | | import { Barcode, BarcodeFormatEnum } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictLabel } from '@vben/hooks'; |
| | | import { useUserStore } from '@vben/stores'; |
| | | import { erpPriceMultiply, formatDateTime } from '@vben/utils'; |
| | | |
| | | import { getCustomerSimpleList } from '#/api/crm/customer'; |
| | | import { getAccountSimpleList } from '#/api/erp/finance/account'; |
| | | import { getSaleOrder } from '#/api/erp/sale/order'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | |
| | | /** |
| | | * éå®è®¢åæå°ç»ä»¶ |
| | | * |
| | | * ç¨äºå¯¼åº/æå°éå®è®¢å详ç»ä¿¡æ¯ï¼ä¾ä¼ä¸çç« çåã |
| | | * æå°æ ·å¼åè WMS åºåºåæå°ï¼wms/order/shipment/modules/print.vueï¼ã |
| | | */ |
| | | defineOptions({ name: 'ErpSaleOrderPrint' }); |
| | | |
| | | const userStore = useUserStore(); |
| | | |
| | | const printData = ref<Partial<ErpSaleOrderApi.SaleOrder>>({}); |
| | | /** éå®äººåæµç§°æ å° */ |
| | | const saleUserMap = ref<Map<number, string>>(new Map()); |
| | | /** ç»ç®è´¦æ·åç§°æ å° */ |
| | | const accountMap = ref<Map<number, string>>(new Map()); |
| | | /** 客æ·åç§°æ å°ï¼è¯¦æ
æ¥å£ä¸è¿å客æ·åç§°ï¼éæå®¢æ·ç¼å·è§£æï¼ */ |
| | | const customerMap = ref<Map<number, string>>(new Map()); |
| | | /** æå°æ¶é´ */ |
| | | const printTime = ref(''); |
| | | |
| | | /** 订åç¶æææ¡ï¼status 为空æ¶ä¸è°ç¨ getDictLabelï¼é¿å
å
é¨å¯¹ undefined æ§è¡ toString æ¥éï¼ */ |
| | | const statusText = computed(() => { |
| | | if (printData.value.status == null) { |
| | | return '-'; |
| | | } |
| | | const label = getDictLabel(DICT_TYPE.ERP_AUDIT_STATUS, printData.value.status); |
| | | return label || '-'; |
| | | }); |
| | | |
| | | /** åºåºç¶æææ¡ */ |
| | | const outStatusText = computed(() => { |
| | | switch (printData.value.outStatus) { |
| | | case 0: |
| | | return 'æªåºåº'; |
| | | case 1: |
| | | return 'å·²é¢ç'; |
| | | case 2: |
| | | return 'é¨ååºåº'; |
| | | case 3: |
| | | return 'å
¨é¨åºåº'; |
| | | default: |
| | | return '-'; |
| | | } |
| | | }); |
| | | |
| | | /** çäº§ç¶æææ¡ */ |
| | | const productionStatusText = computed(() => { |
| | | if (printData.value.needProduction !== 1) { |
| | | return 'æ éç产'; |
| | | } |
| | | return printData.value.productionStatus === 1 ? '已宿' : 'æªå®æ'; |
| | | }); |
| | | |
| | | /** éå®äººååç§° */ |
| | | const saleUserName = computed( |
| | | () => saleUserMap.value.get(printData.value.saleUserId!) || '-', |
| | | ); |
| | | |
| | | /** ç»ç®è´¦æ·åç§° */ |
| | | const accountName = computed( |
| | | () => accountMap.value.get(printData.value.accountId!) || '-', |
| | | ); |
| | | |
| | | /** 客æ·åç§°ï¼ä¼å
å详æ
è¿åç客æ·åç§°ï¼å¦åæå®¢æ·ç¼å·è§£æ */ |
| | | const customerName = computed( |
| | | () => |
| | | printData.value.customerName || |
| | | customerMap.value.get(printData.value.customerId!) || |
| | | '-', |
| | | ); |
| | | |
| | | /** å建人åç§°ï¼è¯¦æ
æ¥å£ä¸è¿åå建人åç§°ï¼æå建人ç¼å·è§£æ */ |
| | | const creatorName = computed( |
| | | () => |
| | | printData.value.creatorName || |
| | | saleUserMap.value.get(Number(printData.value.creator)) || |
| | | '-', |
| | | ); |
| | | |
| | | /** åä»· à æ°éï¼ä»»ä¸ä¸ºç©ºè¿å undefinedï¼ */ |
| | | function multiplyPrice(price?: number, count?: number) { |
| | | return price == null || count == null ? undefined : erpPriceMultiply(price, count); |
| | | } |
| | | |
| | | /** |
| | | * æç»è¡ï¼å端ä¸è¿åæç»è¡ç totalProductPrice/totalPriceï¼éæåä»·Ãæ°é计ç®ï¼ |
| | | * éé¢ = åä»· à æ°éï¼ç¨é¢ = éé¢ Ã ç¨çï¼å«ç¨éé¢ = éé¢ + ç¨é¢ï¼ |
| | | */ |
| | | const printRows = computed(() => |
| | | (printData.value.items || []).map((item) => { |
| | | const totalProductPrice = |
| | | item.totalProductPrice ?? multiplyPrice(item.productPrice, item.count) ?? 0; |
| | | const taxPrice = |
| | | item.taxPrice ?? multiplyPrice(totalProductPrice, (item.taxPercent ?? 0) / 100) ?? 0; |
| | | const totalPrice = item.totalPrice ?? totalProductPrice + taxPrice; |
| | | return { ...item, totalProductPrice, taxPrice, totalPrice }; |
| | | }), |
| | | ); |
| | | |
| | | /** æç»åè®¡ï¼æ°é/éé¢ä¼å
å订å头ï¼å端æå¨å¼ï¼ï¼ç¨é¢ä¸å«ç¨é颿æç»ç´¯å ï¼è®¢å头 totalPrice ä¸ºä¼æ åéé¢ï¼å¦å¨éé¢ä¿¡æ¯åºå±ç¤ºï¼ */ |
| | | const summaries = computed(() => { |
| | | const rows = printRows.value; |
| | | const count = rows.reduce((sum, item) => sum + (item.count || 0), 0); |
| | | const totalProductPrice = rows.reduce( |
| | | (sum, item) => sum + (item.totalProductPrice || 0), |
| | | 0, |
| | | ); |
| | | const taxPrice = rows.reduce((sum, item) => sum + (item.taxPrice || 0), 0); |
| | | const totalPrice = rows.reduce((sum, item) => sum + (item.totalPrice || 0), 0); |
| | | return { |
| | | count: printData.value.totalCount ?? count, |
| | | totalProductPrice: printData.value.totalProductPrice ?? totalProductPrice, |
| | | taxPrice, |
| | | totalPrice, |
| | | }; |
| | | }); |
| | | |
| | | /** ä¿ç两ä½å°æ°ï¼éé¢å±ç¤ºï¼ */ |
| | | function formatPrice(value?: number | null) { |
| | | return value === undefined || value === null || Number.isNaN(Number(value)) |
| | | ? '-' |
| | | : Number(value).toFixed(2); |
| | | } |
| | | |
| | | /** ä¿çä¸ä½å°æ°ï¼æ°éå±ç¤ºï¼ */ |
| | | function formatCount(value?: number | null) { |
| | | return value === undefined || value === null || Number.isNaN(Number(value)) |
| | | ? '-' |
| | | : Number(value).toFixed(3); |
| | | } |
| | | |
| | | /** çå¾
æ¡ç åæå° DOM 宿ç»å¶ï¼é¿å
æµè§å¨æå°å°æ§å
容 */ |
| | | function waitForPaint() { |
| | | return new Promise<void>((resolve) => { |
| | | requestAnimationFrame(() => { |
| | | requestAnimationFrame(() => resolve()); |
| | | }); |
| | | }); |
| | | } |
| | | |
| | | /** éåºæå°æ¨¡å¼ï¼æ¢å¤å½å页颿¾ç¤º */ |
| | | function removePrintMode() { |
| | | document.body.classList.remove('erp-sale-order-printing'); |
| | | } |
| | | |
| | | /** æå°éå®è®¢åï¼å è½½æ°æ®ååªå±ç¤ºæå°åºåï¼åè°ç¨æµè§å¨æå° */ |
| | | async function print(id: number) { |
| | | const [order, userList, accountList, customerList] = await Promise.all([ |
| | | getSaleOrder(id), |
| | | getSimpleUserList(), |
| | | getAccountSimpleList(), |
| | | getCustomerSimpleList(), |
| | | ]); |
| | | printData.value = order; |
| | | saleUserMap.value = new Map( |
| | | userList.map((item) => [Number(item.id), item.nickname || item.username]), |
| | | ); |
| | | accountMap.value = new Map( |
| | | accountList.map((item) => [Number(item.id), item.name]), |
| | | ); |
| | | customerMap.value = new Map( |
| | | customerList.map((item) => [Number(item.id), item.name]), |
| | | ); |
| | | printTime.value = formatDateTime(new Date()); |
| | | await nextTick(); |
| | | await waitForPaint(); |
| | | document.body.classList.add('erp-sale-order-printing'); |
| | | window.addEventListener('afterprint', removePrintMode, { once: true }); |
| | | window.print(); |
| | | } |
| | | |
| | | defineExpose({ print }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Teleport to="body"> |
| | | <div |
| | | id="erpSaleOrderPrint" |
| | | class="erp-sale-order-print pointer-events-none fixed left-0 top-0 z-[-1] w-full bg-white text-[#303133] opacity-0" |
| | | > |
| | | <div class="relative mb-2"> |
| | | <h2 class="m-0 text-center text-[1.6em] font-bold leading-[1.2]"> |
| | | éå®è®¢å |
| | | </h2> |
| | | <div v-if="printData.no" class="absolute right-0 top-0"> |
| | | <Barcode |
| | | :content="printData.no" |
| | | :display-value="false" |
| | | :format="BarcodeFormatEnum.CODE39" |
| | | :height="40" |
| | | :width="180" |
| | | /> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- åºæ¬ä¿¡æ¯ --> |
| | | <div |
| | | class="mb-3 grid grid-cols-3 gap-x-6 gap-y-2 border border-solid border-[#dcdfe6] p-3 text-sm leading-[1.6]" |
| | | > |
| | | <div>订ååå·ï¼{{ printData.no || '-' }}</div> |
| | | <div> |
| | | è®¢åæ¶é´ï¼{{ |
| | | formatDateTime(printData.orderTime) || '-' |
| | | }} |
| | | </div> |
| | | <div>客æ·ï¼{{ customerName }}</div> |
| | | <div>éå®äººåï¼{{ saleUserName }}</div> |
| | | <div>å
³èååï¼{{ printData.contractNo || '-' }}</div> |
| | | <div>ç»ç®è´¦æ·ï¼{{ accountName }}</div> |
| | | <div>订åç¶æï¼{{ statusText }}</div> |
| | | <div>åºåºç¶æï¼{{ outStatusText }}</div> |
| | | <div>çäº§ç¶æï¼{{ productionStatusText }}</div> |
| | | <div>å建人ï¼{{ creatorName }}</div> |
| | | <div> |
| | | å建æ¶é´ï¼{{ |
| | | formatDateTime(printData.createTime) || '-' |
| | | }} |
| | | </div> |
| | | <div class="col-span-3">夿³¨ï¼{{ printData.remark || '-' }}</div> |
| | | </div> |
| | | |
| | | <!-- 产åæç» --> |
| | | <table class="w-full border-collapse text-[13px] leading-[1.5]"> |
| | | <thead> |
| | | <tr> |
| | | <th |
| | | class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-center font-bold" |
| | | > |
| | | åºå· |
| | | </th> |
| | | <th |
| | | class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold" |
| | | > |
| | | 产ååç§°åè§æ ¼ |
| | | </th> |
| | | <th |
| | | class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-center font-bold" |
| | | > |
| | | åä½ |
| | | </th> |
| | | <th |
| | | class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right font-bold" |
| | | > |
| | | æ°é |
| | | </th> |
| | | <th |
| | | class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right font-bold" |
| | | > |
| | | 产ååä»·(å
) |
| | | </th> |
| | | <th |
| | | class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right font-bold" |
| | | > |
| | | éé¢(å
) |
| | | </th> |
| | | <th |
| | | class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right font-bold" |
| | | > |
| | | ç¨ç(%) |
| | | </th> |
| | | <th |
| | | class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right font-bold" |
| | | > |
| | | ç¨é¢(å
) |
| | | </th> |
| | | <th |
| | | class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right font-bold" |
| | | > |
| | | å«ç¨éé¢(å
) |
| | | </th> |
| | | <th |
| | | class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold" |
| | | > |
| | | 夿³¨ |
| | | </th> |
| | | </tr> |
| | | </thead> |
| | | <tbody> |
| | | <tr v-for="(item, index) in printRows" :key="item.id || index"> |
| | | <td class="border border-solid border-[#dcdfe6] p-2 text-center"> |
| | | {{ index + 1 }} |
| | | </td> |
| | | <td class="border border-solid border-[#dcdfe6] p-2"> |
| | | <div>{{ item.productName || '-' }}</div> |
| | | <div v-if="item.productSpecification" class="text-xs"> |
| | | {{ item.productSpecification }} |
| | | </div> |
| | | </td> |
| | | <td class="border border-solid border-[#dcdfe6] p-2 text-center"> |
| | | {{ item.productUnitName || '-' }} |
| | | </td> |
| | | <td class="border border-solid border-[#dcdfe6] p-2 text-right"> |
| | | {{ formatCount(item.count) }} |
| | | </td> |
| | | <td class="border border-solid border-[#dcdfe6] p-2 text-right"> |
| | | {{ formatPrice(item.productPrice) }} |
| | | </td> |
| | | <td class="border border-solid border-[#dcdfe6] p-2 text-right"> |
| | | {{ formatPrice(item.totalProductPrice) }} |
| | | </td> |
| | | <td class="border border-solid border-[#dcdfe6] p-2 text-right"> |
| | | {{ formatPrice(item.taxPercent) }} |
| | | </td> |
| | | <td class="border border-solid border-[#dcdfe6] p-2 text-right"> |
| | | {{ formatPrice(item.taxPrice) }} |
| | | </td> |
| | | <td class="border border-solid border-[#dcdfe6] p-2 text-right"> |
| | | {{ formatPrice(item.totalPrice) }} |
| | | </td> |
| | | <td class="border border-solid border-[#dcdfe6] p-2"> |
| | | {{ item.remark || '-' }} |
| | | </td> |
| | | </tr> |
| | | <tr v-if="printData.items && printData.items.length > 0"> |
| | | <td |
| | | class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-center" |
| | | > |
| | | å计 |
| | | </td> |
| | | <td |
| | | class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2" |
| | | ></td> |
| | | <td |
| | | class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2" |
| | | ></td> |
| | | <td |
| | | class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right" |
| | | > |
| | | {{ formatCount(summaries.count) }} |
| | | </td> |
| | | <td |
| | | class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2" |
| | | ></td> |
| | | <td |
| | | class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right" |
| | | > |
| | | {{ formatPrice(summaries.totalProductPrice) }} |
| | | </td> |
| | | <td |
| | | class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2" |
| | | ></td> |
| | | <td |
| | | class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right" |
| | | > |
| | | {{ formatPrice(summaries.taxPrice) }} |
| | | </td> |
| | | <td |
| | | class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right" |
| | | > |
| | | {{ formatPrice(summaries.totalPrice) }} |
| | | </td> |
| | | <td |
| | | class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2" |
| | | ></td> |
| | | </tr> |
| | | <tr v-if="printRows.length === 0"> |
| | | <td |
| | | class="border border-solid border-[#dcdfe6] p-2 text-center" |
| | | colspan="10" |
| | | > |
| | | ææ æç» |
| | | </td> |
| | | </tr> |
| | | </tbody> |
| | | </table> |
| | | |
| | | <!-- éé¢ä¿¡æ¯ --> |
| | | <div |
| | | class="mt-3 grid grid-cols-4 gap-x-6 gap-y-2 border border-solid border-[#dcdfe6] p-3 text-sm leading-[1.6]" |
| | | > |
| | | <div>伿 ç(%)ï¼{{ formatPrice(printData.discountPercent) }}</div> |
| | | <div>仿¬¾ä¼æ (å
)ï¼{{ formatPrice(printData.discountPrice) }}</div> |
| | | <div>伿 åéé¢(å
)ï¼{{ formatPrice(printData.totalPrice) }}</div> |
| | | <div>æ¶å订é(å
)ï¼{{ formatPrice(printData.depositPrice) }}</div> |
| | | </div> |
| | | |
| | | <!-- çç« æ --> |
| | | <div |
| | | class="mt-6 grid grid-cols-2 gap-x-10 gap-y-4 border border-dashed border-[#dcdfe6] p-4 text-sm leading-[1.8]" |
| | | > |
| | | <div> |
| | | 客æ·çç« /ç¾åï¼<span class="inline-block w-36 border-b border-solid border-[#303133]"></span> |
| | | </div> |
| | | <div> |
| | | å
¬å¸çç« /ç¾åï¼<span class="inline-block w-36 border-b border-solid border-[#303133]"></span> |
| | | </div> |
| | | <div> |
| | | æ¥æï¼<span class="inline-block w-36 border-b border-solid border-[#303133]"></span> |
| | | </div> |
| | | <div> |
| | | æ¥æï¼<span class="inline-block w-36 border-b border-solid border-[#303133]"></span> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- æå°ä¿¡æ¯ --> |
| | | <div |
| | | class="mt-4 flex justify-between text-xs text-[#909399]" |
| | | > |
| | | <div> |
| | | æå°äººåï¼{{ userStore.userInfo?.nickname || '-' }} |
| | | </div> |
| | | <div>æå°æ¶é´ï¼{{ printTime || '-' }}</div> |
| | | </div> |
| | | </div> |
| | | </Teleport> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | @page { |
| | | margin: 8mm 10mm; |
| | | } |
| | | |
| | | @media print { |
| | | :global(body.erp-sale-order-printing) { |
| | | padding: 0 !important; |
| | | margin: 0 !important; |
| | | -webkit-print-color-adjust: exact; |
| | | print-color-adjust: exact; |
| | | } |
| | | |
| | | :global(body.erp-sale-order-printing *) { |
| | | visibility: hidden !important; |
| | | } |
| | | |
| | | :global(body.erp-sale-order-printing .erp-sale-order-print), |
| | | :global(body.erp-sale-order-printing .erp-sale-order-print *) { |
| | | visibility: visible !important; |
| | | } |
| | | |
| | | :global(body.erp-sale-order-printing .erp-sale-order-print) { |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | z-index: auto; |
| | | box-sizing: border-box; |
| | | width: 100%; |
| | | padding: 0 !important; |
| | | margin: 0 !important; |
| | | pointer-events: auto; |
| | | opacity: 1; |
| | | } |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { HrmAttendanceExceptionApi } from '#/api/hrm/attendance/exception'; |
| | | import type { DescriptionItemSchema } from '#/components/description'; |
| | | |
| | | import { h, onMounted, ref } from 'vue'; |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { formatDateTime } from '@vben/utils'; |
| | | |
| | | import { Spin } from 'ant-design-vue'; |
| | | |
| | | import { getAttendanceException } from '#/api/hrm/attendance/exception'; |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | import { useDescription } from '#/components/description'; |
| | | |
| | | defineOptions({ name: 'HrmAttendanceExceptionApproveDetail' }); |
| | | |
| | | const props = defineProps<{ id: string }>(); |
| | | |
| | | const loading = ref(false); |
| | | const formData = ref<HrmAttendanceExceptionApi.AttendanceException>(); |
| | | |
| | | /** ææ¬åæ®µç©ºå¼å ä½ */ |
| | | function textRender(val: unknown) { |
| | | return val === 0 || val ? String(val) : '-'; |
| | | } |
| | | |
| | | const [Descriptions] = useDescription({ |
| | | bordered: true, |
| | | column: 2, |
| | | schema: [ |
| | | { label: 'åå·¥å§å', field: 'userName', render: textRender }, |
| | | { label: 'æå±é¨é¨', field: 'deptName', render: textRender }, |
| | | { |
| | | label: 'å¼å¸¸æ¥æ', |
| | | field: 'date', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { label: 'å¼å¸¸ç±»å', field: 'exceptionTypeName', render: textRender }, |
| | | { label: 'å¼å¸¸è¯´æ', field: 'reason', render: textRender }, |
| | | { |
| | | label: '审æ¹ç¶æ', |
| | | field: 'status', |
| | | render: (val) => |
| | | h(DictTag, { type: DICT_TYPE.HRM_AUDIT_STATUS, value: val }), |
| | | }, |
| | | { label: '夿³¨', field: 'remark', render: textRender }, |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | ] as DescriptionItemSchema[], |
| | | }); |
| | | |
| | | onMounted(async () => { |
| | | try { |
| | | loading.value = true; |
| | | formData.value = await getAttendanceException(Number(props.id)); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <ContentWrap class="m-2"> |
| | | <Spin :spinning="loading" tip="å è½½ä¸..."> |
| | | <div class="space-y-4"> |
| | | <!-- èå¤å¼å¸¸åºæ¬ä¿¡æ¯ --> |
| | | <div class="rounded-lg border border-border bg-background p-4"> |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">èå¤å¼å¸¸åºæ¬ä¿¡æ¯</span> |
| | | </div> |
| | | <Descriptions :data="formData" /> |
| | | </div> |
| | | </div> |
| | | </Spin> |
| | | </ContentWrap> |
| | | </template> |
| | |
| | | return [ |
| | | { field: 'userName', title: 'åå·¥å§å', minWidth: 100 }, |
| | | { field: 'deptName', title: 'é¨é¨', minWidth: 120 }, |
| | | { field: 'phone', title: 'ææºå·', width: 130 }, |
| | | { field: 'date', title: 'è夿¥æ', width: 110 }, |
| | | { field: 'clockInTime', title: 'ä¸çæå¡æ¶é´', width: 160 }, |
| | | { |
| | |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | await importAttendanceRecord(fileList.value[0] as File, updateSupport.value); |
| | | const res: string = await importAttendanceRecord( |
| | | fileList.value[0] as File, |
| | | updateSupport.value, |
| | | ); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success('导å
¥æå'); |
| | | // å端è¿å导å
¥ç»æï¼å«å¤±è´¥åå ï¼ä¾å¦â以ä¸åå·¥ä¸åå¨...ï¼éè¡¥å
ææºå·...âï¼ |
| | | if (res && res.length > 0) { |
| | | message[res.includes('æª') || res.includes('ä¸å®æ´') ? 'warning' : 'success'](res); |
| | | } else { |
| | | message.success('导å
¥æå'); |
| | | } |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | <div>åå·¥ç¼å·ï¼{{ selectedItem.employeeNo || '-' }}</div> |
| | | <div>åå·¥å§åï¼{{ selectedItem.name || '-' }}</div> |
| | | <div v-if="selectedItem.deptName">é¨é¨ï¼{{ selectedItem.deptName }}</div> |
| | | <div v-if="selectedItem.postName">å²ä½ï¼{{ selectedItem.postName }}</div> |
| | | <div v-if="selectedItem.postNames">å²ä½ï¼{{ selectedItem.postNames }}</div> |
| | | </div> |
| | | </template> |
| | | <Input |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { HrmEmployeeContractApi } from '#/api/hrm/employee/contract'; |
| | | |
| | | import { DICT_TYPE } from '#/packages/constants/src'; |
| | | import { getDictOptions } from '#/packages/effects/hooks/src'; |
| | | |
| | | import { getEmployeeSimpleList } from '#/api/hrm/employee'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'update'; |
| | | |
| | | /** æ°å¢/ä¿®æ¹åå·¥ååç表å */ |
| | | export function useFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => false, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'employeeId', |
| | | label: 'åå·¥', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | api: getEmployeeSimpleList, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | showSearch: true, |
| | | filterOption: (input: string, option: any) => |
| | | option.label?.toLowerCase().includes(input.toLowerCase()), |
| | | placeholder: 'è¯·éæ©åå·¥', |
| | | }, |
| | | rules: 'selectRequired', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'contractType', |
| | | label: 'ååç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©ååç±»å', |
| | | options: getDictOptions(DICT_TYPE.HRM_CONTRACT_TYPE, 'number'), |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'contractTermType', |
| | | label: 'æéç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©æéç±»å', |
| | | options: getDictOptions(DICT_TYPE.HRM_CONTRACT_TERM_TYPE, 'number'), |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'signCompany', |
| | | label: 'ç¾çº¦ä¸»ä½', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥ç¾çº¦ä¸»ä½ï¼å
¬å¸å
¨ç§°ï¼', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'signDate', |
| | | label: 'ç¾è®¢æ¥æ', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©ç¾è®¢æ¥æ', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | style: { width: '100%' }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'startDate', |
| | | label: 'å¼å§æ¥æ', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©å¼å§æ¥æ', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | style: { width: '100%' }, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'endDate', |
| | | label: 'ç»ææ¥æ', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | placeholder: 'æ åºå®æéå¯ä¸å¡«', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | style: { width: '100%' }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'probationStartDate', |
| | | label: 'è¯ç¨æå¼å§', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©è¯ç¨æå¼å§æ¥æ', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | style: { width: '100%' }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'probationEndDate', |
| | | label: 'è¯ç¨æç»æ', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©è¯ç¨æç»ææ¥æ', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | style: { width: '100%' }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'probationSalary', |
| | | label: 'è¯ç¨æå·¥èµ', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥è¯ç¨æå·¥èµ', |
| | | min: 0, |
| | | precision: 2, |
| | | style: { width: '100%' }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'regularSalary', |
| | | label: '转æ£å·¥èµ', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥è½¬æ£å·¥èµ', |
| | | min: 0, |
| | | precision: 2, |
| | | style: { width: '100%' }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'blobIds', |
| | | label: 'éä»¶', |
| | | component: 'FileUpload', |
| | | formItemClass: 'col-span-2', |
| | | componentProps: { |
| | | valueKey: 'id', |
| | | maxNumber: 10, |
| | | multiple: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | formItemClass: 'col-span-2', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å¤æ³¨', |
| | | rows: 2, |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表çæç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'contractNo', |
| | | label: 'ååç¼å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥ååç¼å·', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'employeeId', |
| | | label: 'åå·¥', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | api: getEmployeeSimpleList, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | showSearch: true, |
| | | filterOption: (input: string, option: any) => |
| | | option.label?.toLowerCase().includes(input.toLowerCase()), |
| | | allowClear: true, |
| | | placeholder: 'è¯·éæ©åå·¥', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'contractType', |
| | | label: 'ååç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.HRM_CONTRACT_TYPE, 'number'), |
| | | placeholder: 'è¯·éæ©ååç±»å', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'contractTermType', |
| | | label: 'æéç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.HRM_CONTRACT_TERM_TYPE, 'number'), |
| | | placeholder: 'è¯·éæ©æéç±»å', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'terminateStatus', |
| | | label: 'è§£é¤ç»æ¢ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.HRM_CONTRACT_TERMINATE_STATUS, 'number'), |
| | | placeholder: 'è¯·éæ©è§£é¤ç»æ¢ç¶æ', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ååç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.HRM_CONTRACT_STATUS, 'number'), |
| | | placeholder: 'è¯·éæ©ååç¶æ', |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表çåæ®µ */ |
| | | export function useGridColumns(): VxeTableGridOptions<HrmEmployeeContractApi.EmployeeContract>['columns'] { |
| | | return [ |
| | | { field: 'contractNo', title: 'ååç¼å·', minWidth: 160 }, |
| | | { |
| | | field: 'parentNo', |
| | | title: 'ç»ç¾èª', |
| | | minWidth: 140, |
| | | slots: { default: 'parentNo' }, |
| | | }, |
| | | { field: 'employeeName', title: 'åå·¥å§å', minWidth: 100 }, |
| | | { field: 'employeeNo', title: 'å工工å·', minWidth: 130 }, |
| | | { field: 'deptName', title: 'é¨é¨', minWidth: 120 }, |
| | | { |
| | | field: 'contractType', |
| | | title: 'ååç±»å', |
| | | width: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.HRM_CONTRACT_TYPE }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'contractTermType', |
| | | title: 'æéç±»å', |
| | | width: 130, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.HRM_CONTRACT_TERM_TYPE }, |
| | | }, |
| | | }, |
| | | { field: 'signDate', title: 'ç¾è®¢æ¥æ', width: 110 }, |
| | | { field: 'startDate', title: 'å¼å§æ¥æ', width: 110 }, |
| | | { field: 'endDate', title: 'ç»ææ¥æ', width: 110 }, |
| | | { |
| | | field: 'status', |
| | | title: 'ååç¶æ', |
| | | width: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.HRM_CONTRACT_STATUS }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'isCurrent', |
| | | title: 'å½ååå', |
| | | width: 90, |
| | | slots: { default: 'isCurrent' }, |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 260, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { HrmEmployeeContractApi } from '#/api/hrm/employee/contract'; |
| | | |
| | | import { Page, useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteEmployeeContract, |
| | | getEmployeeContractPage, |
| | | } from '#/api/hrm/employee/contract'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | import ImportForm from './modules/import-form.vue'; |
| | | import Terminate from './modules/terminate.vue'; |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ImportModal, importModalApi] = useVbenModal({ |
| | | connectedComponent: ImportForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [TerminateModal, terminateModalApi] = useVbenModal({ |
| | | connectedComponent: Terminate, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** æ°å¢åå */ |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | /** 导å
¥åå */ |
| | | function handleImport() { |
| | | importModalApi.open(); |
| | | } |
| | | |
| | | /** ç¼è¾åå */ |
| | | function handleEdit(row: HrmEmployeeContractApi.EmployeeContract) { |
| | | formModalApi.setData({ id: row.id, formType: 'update' }).open(); |
| | | } |
| | | |
| | | /** ç»ç¾ååï¼å¸¦å
¥ä¸ä»½åååºç¡ä¿¡æ¯ï¼å建æ°ååï¼å端èªå¨å
³è parentIdï¼ */ |
| | | function handleRenew(row: HrmEmployeeContractApi.EmployeeContract) { |
| | | formModalApi.setData({ formType: 'create', renewFrom: row }).open(); |
| | | } |
| | | |
| | | /** è§£é¤/ç»æ¢åå */ |
| | | function handleTerminate(row: HrmEmployeeContractApi.EmployeeContract) { |
| | | terminateModalApi.setData({ id: row.id }).open(); |
| | | } |
| | | |
| | | /** å é¤åå */ |
| | | async function handleDelete(row: HrmEmployeeContractApi.EmployeeContract) { |
| | | await deleteEmployeeContract(row.id!); |
| | | message.success($t('ui.actionMessage.deleteSuccess', ['åå·¥åå'])); |
| | | handleRefresh(); |
| | | } |
| | | |
| | | /** æ¯å¦å¯è§£é¤/ç»æ¢ï¼ä»
æªè§£é¤/æªç»æ¢çååå¯æä½ */ |
| | | function isTerminable(row: HrmEmployeeContractApi.EmployeeContract) { |
| | | return row.terminateStatus === 0; |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => |
| | | await getEmployeeContractPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }), |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | toolbarConfig: { |
| | | refresh: true, |
| | | search: true, |
| | | }, |
| | | } as VxeTableGridOptions<HrmEmployeeContractApi.EmployeeContract>, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <ImportModal @success="handleRefresh" /> |
| | | <TerminateModal @success="handleRefresh" /> |
| | | <Grid table-title="åå·¥ååå表"> |
| | | <template #parentNo="{ row }"> |
| | | <span>{{ row.parentNo || '-' }}</span> |
| | | </template> |
| | | <template #isCurrent="{ row }"> |
| | | <Tag v-if="row.isCurrent" color="processing">å½å</Tag> |
| | | <span v-else>-</span> |
| | | </template> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['åå·¥åå']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['hrm:employee-contract:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: '导å
¥', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.UPLOAD, |
| | | auth: ['hrm:employee-contract:import'], |
| | | onClick: handleImport, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'ç»ç¾', |
| | | type: 'link', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['hrm:employee-contract:create'], |
| | | ifShow: isTerminable(row), |
| | | onClick: handleRenew.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['hrm:employee-contract:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'è§£é¤/ç»æ¢', |
| | | type: 'link', |
| | | icon: ACTION_ICON.CLOSE, |
| | | auth: ['hrm:employee-contract:update'], |
| | | ifShow: isTerminable(row), |
| | | onClick: handleTerminate.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['hrm:employee-contract:delete'], |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', ['åå·¥åå']), |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormType } from '../data'; |
| | | import type { HrmEmployeeContractApi } from '#/api/hrm/employee/contract'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createEmployeeContract, |
| | | getEmployeeContract, |
| | | updateEmployeeContract, |
| | | } from '#/api/hrm/employee/contract'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formType = ref<FormType>('create'); |
| | | const isRenew = ref(false); |
| | | |
| | | const getTitle = computed(() => { |
| | | if (formType.value === 'update') { |
| | | return 'ä¿®æ¹åå·¥åå'; |
| | | } |
| | | return isRenew.value ? 'ç»ç¾åå·¥åå' : 'æ°å¢åå·¥åå'; |
| | | }); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { |
| | | class: 'w-full', |
| | | }, |
| | | formItemClass: 'col-span-1', |
| | | labelWidth: 100, |
| | | }, |
| | | wrapperClass: 'grid-cols-2', |
| | | layout: 'horizontal', |
| | | schema: useFormSchema(), |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | const data = |
| | | (await formApi.getValues()) as HrmEmployeeContractApi.EmployeeContract; |
| | | try { |
| | | // FileUpload ç»ä»¶çå¼é转æ¢ä¸º blobId æ°ç»æäº¤ |
| | | data.blobIds = (data.blobIds ?? []).map((item: any) => item.id ?? item); |
| | | if (data.id) { |
| | | await updateEmployeeContract(data); |
| | | } else { |
| | | await createEmployeeContract(data); |
| | | } |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ |
| | | formType: FormType; |
| | | id?: number; |
| | | renewFrom?: HrmEmployeeContractApi.EmployeeContract; |
| | | }>(); |
| | | formType.value = data.formType; |
| | | isRenew.value = !data?.id && !!data?.renewFrom; |
| | | // ç»ç¾ï¼å¸¦å
¥ä¸ä»½ååçåºç¡ä¿¡æ¯ï¼åå·¥ãç±»åãç¾çº¦ä¸»ä½ãå·¥èµãç»ç¾æ¥æºï¼ï¼æ¥æééæ°å¡«å |
| | | if (data?.renewFrom) { |
| | | await formApi.resetForm(); |
| | | await formApi.setValues({ |
| | | parentId: data.renewFrom.id, |
| | | employeeId: data.renewFrom.employeeId, |
| | | contractType: data.renewFrom.contractType, |
| | | contractTermType: data.renewFrom.contractTermType, |
| | | signCompany: data.renewFrom.signCompany, |
| | | probationSalary: data.renewFrom.probationSalary, |
| | | regularSalary: data.renewFrom.regularSalary, |
| | | }); |
| | | return; |
| | | } |
| | | if (!data?.id) { |
| | | await formApi.resetForm(); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | const detail = await getEmployeeContract(data.id); |
| | | await formApi.setValues(detail); |
| | | if (detail.attachmentList?.length) { |
| | | await formApi.setFieldValue( |
| | | 'blobIds', |
| | | detail.attachmentList.map((item) => ({ |
| | | uid: String(item.id), |
| | | name: item.name || '', |
| | | url: item.url || '', |
| | | status: 'done', |
| | | id: item.id, |
| | | })), |
| | | ); |
| | | } |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-[720px]"> |
| | | <Form class="mx-4" /> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FileType } from 'ant-design-vue/es/upload/interface'; |
| | | import type { HrmEmployeeContractApi } from '#/api/hrm/employee/contract'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | import { downloadFileFromBlobPart } from '#/packages/utils/src'; |
| | | |
| | | import { Button, message, Progress, Switch, Upload } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | importEmployeeContract, |
| | | importEmployeeContractTemplate, |
| | | } from '#/api/hrm/employee/contract'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const fileList = ref<FileType[]>([]); |
| | | const updateSupport = ref(false); |
| | | const importResult = ref<HrmEmployeeContractApi.ImportResult>(); |
| | | const importing = ref(false); |
| | | const progressPercent = ref(0); |
| | | let progressTimer: ReturnType<typeof setInterval> | undefined; |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | let shouldClose = false; |
| | | if (fileList.value.length === 0) { |
| | | message.warning('è¯·éæ©è¦å¯¼å
¥çæä»¶'); |
| | | return; |
| | | } |
| | | if (importing.value) return; |
| | | modalApi.lock(); |
| | | importing.value = true; |
| | | startMockProgress(); |
| | | try { |
| | | importResult.value = await importEmployeeContract( |
| | | fileList.value[0] as File, |
| | | updateSupport.value, |
| | | ); |
| | | // 宿ï¼è¿åº¦æ¡èµ°æ»¡ |
| | | stopMockProgress(); |
| | | progressPercent.value = 100; |
| | | await delay(300); |
| | | emit('success'); |
| | | const failedCount = Object.keys(importResult.value?.failureNames ?? {}).length; |
| | | if (failedCount > 0) { |
| | | message.warning( |
| | | `导å
¥å®æï¼æ°å¢ ${createCount.value} æ¡ï¼è¦ç ${updateCount.value} æ¡ï¼å¤±è´¥ ${failedCount} æ¡ï¼è¯·æ¥ç䏿¹å¤±è´¥æç»`, |
| | | ); |
| | | } else { |
| | | message.success('导å
¥å®æ'); |
| | | shouldClose = true; |
| | | } |
| | | } finally { |
| | | stopMockProgress(); |
| | | importing.value = false; |
| | | modalApi.unlock(); |
| | | if (shouldClose) { |
| | | await modalApi.close(); |
| | | } |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | /** 模æè¿åº¦æ¡ï¼è¯·æ±æé´ä» 0 èµ°å° 90% */ |
| | | function startMockProgress() { |
| | | progressPercent.value = 0; |
| | | progressTimer = setInterval(() => { |
| | | if (progressPercent.value < 90) { |
| | | progressPercent.value = Math.min( |
| | | 90, |
| | | progressPercent.value + Math.random() * 12, |
| | | ); |
| | | } |
| | | }, 300); |
| | | } |
| | | |
| | | function stopMockProgress() { |
| | | if (progressTimer) { |
| | | clearInterval(progressTimer); |
| | | progressTimer = undefined; |
| | | } |
| | | } |
| | | |
| | | function delay(ms: number) { |
| | | return new Promise((resolve) => setTimeout(resolve, ms)); |
| | | } |
| | | |
| | | function beforeUpload(file: FileType) { |
| | | fileList.value = [file]; |
| | | importResult.value = undefined; |
| | | return false; |
| | | } |
| | | |
| | | function handleRemove() { |
| | | fileList.value = []; |
| | | importResult.value = undefined; |
| | | } |
| | | |
| | | async function handleDownloadTemplate() { |
| | | const data = await importEmployeeContractTemplate(); |
| | | downloadFileFromBlobPart({ fileName: 'åå·¥åå导å
¥æ¨¡æ¿.xls', source: data }); |
| | | } |
| | | |
| | | const createCount = computed(() => importResult.value?.createNames?.length ?? 0); |
| | | const updateCount = computed(() => importResult.value?.updateNames?.length ?? 0); |
| | | const failureCount = computed( |
| | | () => Object.keys(importResult.value?.failureNames ?? {}).length, |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="导å
¥åå·¥åå" class="w-[720px]"> |
| | | <div class="mx-4"> |
| | | <Upload |
| | | :max-count="1" |
| | | accept=".xls,.xlsx" |
| | | :file-list="fileList" |
| | | :before-upload="beforeUpload" |
| | | @remove="handleRemove" |
| | | > |
| | | <Button type="primary">éæ© Excel æä»¶</Button> |
| | | </Upload> |
| | | <div class="mt-4"> |
| | | <span class="mr-2">ååå·¥ãåååç±»åãåå¼å§æ¥æçååå·²å卿¶è¦çæ´æ°è¯¥è¡ï¼</span> |
| | | <Switch v-model:checked="updateSupport" /> |
| | | </div> |
| | | <div class="mt-2 rounded-md bg-gray-50 p-2 text-xs leading-5 text-gray-500"> |
| | | <p>ä»
å
许导å
¥ xlsãxlsx æ ¼å¼æä»¶ï¼æ¨¡æ¿ä¸âåå·¥å§å/ååç±»å/ååæéç±»åâæåç§°æä¸æå¡«åï¼ååç±»åå¦ï¼å³å¨ååãå³å¡ååï¼ã</p> |
| | | <p>导å
¥å¤å®ï¼æâåå·¥å§å + ååç±»å + ååå¼å§æ¥æâè¯å«ãæªå¾éâå·²å卿¶æ´æ°âæ¶ï¼å½ä¸å·²åå¨ååçè¡ä¼å¯¼å
¥å¤±è´¥å¹¶æç¤ºï¼å¾éååè¦çæ´æ°è¯¥è¡ï¼ä¿çåååç¼å·ï¼ï¼æªå½ä¸çè¡ä¸å¾æ°å¢ã</p> |
| | | </div> |
| | | <!-- 导å
¥è¿åº¦æ¡ --> |
| | | <div v-if="importing" class="mt-4"> |
| | | <div class="mb-2 text-sm text-gray-600">æ£å¨å¯¼å
¥åå·¥ååæ°æ®ï¼è¯·ç¨å...</div> |
| | | <Progress :percent="progressPercent" status="active" /> |
| | | </div> |
| | | <!-- 导å
¥ç»æ --> |
| | | <div |
| | | v-if="importResult" |
| | | class="mt-4 rounded-md border border-gray-200 bg-gray-50 p-3" |
| | | > |
| | | <div class="mb-2 font-medium">导å
¥ç»æ</div> |
| | | <div class="mb-2 flex gap-4"> |
| | | <span class="text-green-600">æ°å¢ï¼{{ createCount }} 份</span> |
| | | <span class="text-blue-600">è¦çæ´æ°ï¼{{ updateCount }} 份</span> |
| | | <span class="text-red-600">失败ï¼{{ failureCount }} 份</span> |
| | | </div> |
| | | <div v-if="failureCount > 0" class="max-h-40 overflow-y-auto"> |
| | | <div |
| | | v-for="(reason, name) in importResult.failureNames" |
| | | :key="name" |
| | | class="text-sm leading-6 text-red-500" |
| | | > |
| | | {{ name }}ï¼{{ reason }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <template #prepend-footer> |
| | | <Button @click="handleDownloadTemplate">ä¸è½½å¯¼å
¥æ¨¡æ¿</Button> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { terminateEmployeeContract } from '#/api/hrm/employee/contract'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const contractId = ref<number>(); |
| | | |
| | | /** è§£é¤/ç»æ¢è¡¨å */ |
| | | const schema: VbenFormSchema[] = [ |
| | | { |
| | | fieldName: 'terminateStatus', |
| | | label: 'æä½ç±»å', |
| | | component: 'RadioGroup', |
| | | componentProps: { |
| | | options: [ |
| | | { label: 'è§£é¤', value: 1 }, |
| | | { label: 'ç»æ¢', value: 2 }, |
| | | ], |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'terminateDate', |
| | | label: 'è§£é¤/ç»æ¢æ¥æ', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©æ¥æ', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | style: { width: '100%' }, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'terminateReason', |
| | | label: 'åå ', |
| | | component: 'Textarea', |
| | | formItemClass: 'col-span-2', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥è§£é¤/ç»æ¢åå ', |
| | | rows: 3, |
| | | maxlength: 255, |
| | | showCount: true, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | ]; |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { |
| | | class: 'w-full', |
| | | }, |
| | | formItemClass: 'col-span-1', |
| | | labelWidth: 110, |
| | | }, |
| | | wrapperClass: 'grid-cols-2', |
| | | layout: 'horizontal', |
| | | schema, |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | const data = await formApi.getValues(); |
| | | try { |
| | | await terminateEmployeeContract({ id: contractId.value!, ...data }); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ id: number }>(); |
| | | contractId.value = data.id; |
| | | await formApi.resetForm(); |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="è§£é¤/ç»æ¢åå" class="w-[520px]"> |
| | | <Form class="mx-4" /> |
| | | </Modal> |
| | | </template> |
| | |
| | | import { Button } from 'ant-design-vue'; |
| | | |
| | | import { z } from '#/adapter/form'; |
| | | import { getSalaryStructureList } from '#/api/hrm/salary/structure'; |
| | | import { getDeptList } from '#/api/system/dept'; |
| | | import { getSimplePostList } from '#/api/system/post'; |
| | | import { getSimpleRoleList } from '#/api/system/role'; |
| | |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'postId', |
| | | fieldName: 'postIds', |
| | | label: 'å²ä½', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | api: getSimplePostList, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | placeholder: 'è¯·éæ©å²ä½', |
| | | mode: 'multiple', |
| | | placeholder: 'è¯·éæ©å²ä½ï¼å¯å¤éï¼', |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'nation', |
| | | label: 'æ°æ', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥æ°æ', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'marriageStatus', |
| | | label: 'å©å§»ç¶æ', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å©å§»ç¶æ', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'age', |
| | | label: 'å¹´é¾', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å¹´é¾', |
| | | min: 0, |
| | | max: 150, |
| | | style: { width: '100%' }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'education', |
| | | label: 'å¦å', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å¦å', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'politicalStatus', |
| | | label: 'æ¿æ²»é¢è²', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥æ¿æ²»é¢è²', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'annualLeaveBalance', |
| | | label: 'å¹´åä½é¢', |
| | | component: 'InputNumber', |
| | |
| | | precision: 1, |
| | | style: { width: '100%' }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'salaryStructureId', |
| | | label: 'èªé
Ⱦ', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | api: () => getSalaryStructureList({ status: 0 }), |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | allowClear: true, |
| | | placeholder: 'è¯·éæ©èªé
Ⱦ', |
| | | }, |
| | | help: 'å³å®å·¥ä½æ¥å çåçï¼æªéæ©æ¶ä½¿ç¨ç³»ç»é»è®¤ 1.5 å', |
| | | }, |
| | | { |
| | | fieldName: 'baseSalary', |
| | |
| | | }, |
| | | { field: 'phone', title: 'ææºå·ç ', minWidth: 120 }, |
| | | { field: 'deptName', title: 'é¨é¨', minWidth: 120 }, |
| | | { field: 'postName', title: 'å²ä½', minWidth: 120 }, |
| | | { field: 'postNames', title: 'å²ä½', minWidth: 120 }, |
| | | { field: 'nation', title: 'æ°æ', width: 90 }, |
| | | { field: 'age', title: 'å¹´é¾', width: 70 }, |
| | | { field: 'education', title: 'å¦å', width: 90 }, |
| | | { |
| | | field: 'employeeStatus', |
| | | title: 'åå·¥ç¶æ', |
| | |
| | | { field: 'employeeNo', title: 'åå·¥ç¼å·', minWidth: 120 }, |
| | | { field: 'name', title: 'åå·¥å§å', minWidth: 100 }, |
| | | { field: 'deptName', title: 'é¨é¨', minWidth: 120 }, |
| | | { field: 'postName', title: 'å²ä½', minWidth: 120 }, |
| | | { field: 'postNames', title: 'å²ä½', minWidth: 120 }, |
| | | { field: 'phone', title: 'ææºå·ç ', minWidth: 120 }, |
| | | ]; |
| | | } |
| | |
| | | import { Page, useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | import { downloadFileFromBlobPart } from '#/packages/utils/src'; |
| | | |
| | | import { Button, message, Tag } from 'ant-design-vue'; |
| | | import { Button, message, Progress, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { DICT_TYPE } from '#/packages/constants/src'; |
| | | import { getDictLabel } from '#/packages/effects/hooks/src'; |
| | |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | import ImportForm from './modules/import-form.vue'; |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ImportModal, importModalApi] = useVbenModal({ |
| | | connectedComponent: ImportForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | const exporting = ref(false); |
| | | const exportPercent = ref(0); |
| | | let exportTimer: ReturnType<typeof setInterval> | undefined; |
| | | |
| | | /** 模æè¿åº¦æ¡ï¼è¯·æ±æé´ä» 0 èµ°å° 90% */ |
| | | function startExportProgress() { |
| | | exportPercent.value = 0; |
| | | exportTimer = setInterval(() => { |
| | | if (exportPercent.value < 90) { |
| | | exportPercent.value = Math.min( |
| | | 90, |
| | | exportPercent.value + Math.random() * 12, |
| | | ); |
| | | } |
| | | }, 300); |
| | | } |
| | | |
| | | function stopExportProgress() { |
| | | if (exportTimer) { |
| | | clearInterval(exportTimer); |
| | | exportTimer = undefined; |
| | | } |
| | | } |
| | | |
| | | function delay(ms: number) { |
| | | return new Promise((resolve) => setTimeout(resolve, ms)); |
| | | } |
| | | |
| | | /** 导åºåå·¥ */ |
| | | async function handleExport() { |
| | | const data = await exportEmployee(await gridApi.formApi.getValues()); |
| | | downloadFileFromBlobPart({ fileName: 'åå·¥å表.xls', source: data }); |
| | | if (exporting.value) return; |
| | | exporting.value = true; |
| | | startExportProgress(); |
| | | try { |
| | | const data = await exportEmployee(await gridApi.formApi.getValues()); |
| | | // 宿ï¼è¿åº¦æ¡èµ°æ»¡å触åä¸è½½ |
| | | stopExportProgress(); |
| | | exportPercent.value = 100; |
| | | await delay(500); |
| | | downloadFileFromBlobPart({ fileName: 'åå·¥å表.xls', source: data }); |
| | | } finally { |
| | | stopExportProgress(); |
| | | exporting.value = false; |
| | | } |
| | | } |
| | | |
| | | /** 导å
¥åå·¥ */ |
| | | function handleImport() { |
| | | importModalApi.open(); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <ImportModal @success="handleRefresh" /> |
| | | <FormModal @success="handleRefresh" /> |
| | | <Grid table-title="åå·¥å表"> |
| | | <template #toolbar-tools> |
| | |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['hrm:employee:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: '导å
¥', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.UPLOAD, |
| | | auth: ['hrm:employee:import'], |
| | | onClick: handleImport, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.export'), |
| | |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | |
| | | <!-- 导åºè¿åº¦æ¡é®ç½© --> |
| | | <div |
| | | v-if="exporting" |
| | | class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/40" |
| | | > |
| | | <div class="w-80 rounded-lg bg-white p-6 shadow-lg"> |
| | | <div class="mb-3 text-center text-sm text-gray-600"> |
| | | æ£å¨å¯¼åºåå·¥å表... |
| | | </div> |
| | | <Progress :percent="exportPercent" status="active" /> |
| | | </div> |
| | | </div> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { HrmEmployeeContractApi } from '#/api/hrm/employee/contract'; |
| | | |
| | | import { ref, watch } from 'vue'; |
| | | |
| | | import { Table, Tag } from 'ant-design-vue'; |
| | | |
| | | import { getEmployeeContractPage } from '#/api/hrm/employee/contract'; |
| | | import { DICT_TYPE } from '#/packages/constants/src'; |
| | | import { getDictLabel } from '#/packages/effects/hooks/src'; |
| | | |
| | | const props = defineProps<{ |
| | | employeeId?: number; |
| | | }>(); |
| | | |
| | | const list = ref<HrmEmployeeContractApi.EmployeeContract[]>([]); |
| | | const loading = ref(false); |
| | | |
| | | /** ååç¶æé¢è²ï¼1-å¾
çæ 2-çæä¸ 3-å³å°å°æ 4-已尿 5-å·²è§£é¤ 6-å·²ç»æ¢ */ |
| | | const statusColorMap: Record<number, string> = { |
| | | 1: 'processing', |
| | | 2: 'success', |
| | | 3: 'warning', |
| | | 4: 'default', |
| | | 5: 'error', |
| | | 6: 'error', |
| | | }; |
| | | |
| | | /** å·æ°å表 */ |
| | | async function handleRefresh() { |
| | | if (!props.employeeId) return; |
| | | loading.value = true; |
| | | try { |
| | | const res = await getEmployeeContractPage({ |
| | | pageNo: 1, |
| | | pageSize: 100, |
| | | employeeId: props.employeeId, |
| | | }); |
| | | list.value = res.list; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | /** çå¬ employeeId åå */ |
| | | watch( |
| | | () => props.employeeId, |
| | | (val) => { |
| | | if (val) { |
| | | handleRefresh(); |
| | | } |
| | | }, |
| | | { immediate: true }, |
| | | ); |
| | | |
| | | const columns = [ |
| | | { title: 'ååç¼å·', dataIndex: 'contractNo', width: 150 }, |
| | | { title: 'ç»ç¾èª', dataIndex: 'parentNo', width: 130, key: 'parentNo' }, |
| | | { title: 'ååç±»å', dataIndex: 'contractType', width: 100, key: 'contractType' }, |
| | | { title: 'æéç±»å', dataIndex: 'contractTermType', width: 110, key: 'contractTermType' }, |
| | | { title: 'ç¾è®¢æ¥æ', dataIndex: 'signDate', width: 110 }, |
| | | { title: 'å¼å§æ¥æ', dataIndex: 'startDate', width: 110 }, |
| | | { title: 'ç»ææ¥æ', dataIndex: 'endDate', width: 110 }, |
| | | { title: 'ååç¶æ', dataIndex: 'status', width: 100, key: 'status' }, |
| | | { title: 'å½ååå', dataIndex: 'isCurrent', width: 90, key: 'isCurrent' }, |
| | | ]; |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="contract-list"> |
| | | <Table |
| | | :columns="columns" |
| | | :data-source="list" |
| | | :loading="loading" |
| | | :pagination="false" |
| | | size="small" |
| | | row-key="id" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.key === 'parentNo'"> |
| | | {{ record.parentNo || '-' }} |
| | | </template> |
| | | <template v-else-if="column.key === 'contractType'"> |
| | | {{ getDictLabel(DICT_TYPE.HRM_CONTRACT_TYPE, record.contractType) }} |
| | | </template> |
| | | <template v-else-if="column.key === 'contractTermType'"> |
| | | {{ getDictLabel(DICT_TYPE.HRM_CONTRACT_TERM_TYPE, record.contractTermType) }} |
| | | </template> |
| | | <template v-else-if="column.key === 'status'"> |
| | | <Tag :color="statusColorMap[record.status!] || 'default'"> |
| | | {{ getDictLabel(DICT_TYPE.HRM_CONTRACT_STATUS, record.status) }} |
| | | </Tag> |
| | | </template> |
| | | <template v-else-if="column.key === 'isCurrent'"> |
| | | <Tag v-if="record.isCurrent" color="processing">å½å</Tag> |
| | | <span v-else>-</span> |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .contract-list { |
| | | padding: 0; |
| | | } |
| | | </style> |
| | |
| | | import WorkHistoryList from './work-history-list.vue'; |
| | | import EmergencyContactList from './emergency-contact-list.vue'; |
| | | import SocialSecurityList from './social-security-list.vue'; |
| | | import ContractList from './contract-list.vue'; |
| | | |
| | | const props = defineProps<{ |
| | | employee: HrmEmployeeApi.Employee; |
| | |
| | | <DescriptionsItem label="åå·¥ç¶æ"> |
| | | <Tag :color="employeeStatusColor">{{ employeeStatusLabel }}</Tag> |
| | | </DescriptionsItem> |
| | | <DescriptionsItem label="æ°æ">{{ employee.nation || '-' }}</DescriptionsItem> |
| | | <DescriptionsItem label="å©å§»ç¶æ">{{ employee.marriageStatus || '-' }}</DescriptionsItem> |
| | | <DescriptionsItem label="å¹´é¾">{{ employee.age ?? '-' }}</DescriptionsItem> |
| | | <DescriptionsItem label="å¦å">{{ employee.education || '-' }}</DescriptionsItem> |
| | | <DescriptionsItem label="æ¿æ²»é¢è²">{{ employee.politicalStatus || '-' }}</DescriptionsItem> |
| | | </Descriptions> |
| | | |
| | | <!-- å·¥ä½ä¿¡æ¯ --> |
| | | <Descriptions title="å·¥ä½ä¿¡æ¯" bordered :column="2" class="mb-4"> |
| | | <DescriptionsItem label="å½å±é¨é¨">{{ employee.deptName || '-' }}</DescriptionsItem> |
| | | <DescriptionsItem label="å²ä½">{{ employee.postName || '-' }}</DescriptionsItem> |
| | | <DescriptionsItem label="å²ä½">{{ employee.postNames || '-' }}</DescriptionsItem> |
| | | <DescriptionsItem label="å
¥èæ¥æ">{{ employee.hireDate }}</DescriptionsItem> |
| | | <DescriptionsItem label="è½¬æ£æ¥æ">{{ employee.regularDate || '-' }}</DescriptionsItem> |
| | | <DescriptionsItem label="ç¦»èæ¥æ">{{ employee.leaveDate || '-' }}</DescriptionsItem> |
| | |
| | | <Tabs.TabPane key="socialSecurity" tab="社ä¿å
¬ç§¯éæ¡£æ¡"> |
| | | <SocialSecurityList form-type="detail" :employee-id="employee.id" :user-id="employee.userId" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane key="contract" tab="åå·¥åå"> |
| | | <ContractList :employee-id="employee.id" /> |
| | | </Tabs.TabPane> |
| | | </Tabs> |
| | | </div> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FileType } from 'ant-design-vue/es/upload/interface'; |
| | | import type { HrmEmployeeApi } from '#/api/hrm/employee'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | import { downloadFileFromBlobPart } from '#/packages/utils/src'; |
| | | |
| | | import { Button, message, Progress, Switch, Upload } from 'ant-design-vue'; |
| | | |
| | | import { importEmployee, importEmployeeTemplate } from '#/api/hrm/employee'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const fileList = ref<FileType[]>([]); |
| | | const updateSupport = ref(false); |
| | | const importResult = ref<HrmEmployeeApi.ImportResult>(); |
| | | const importing = ref(false); |
| | | const progressPercent = ref(0); |
| | | let progressTimer: ReturnType<typeof setInterval> | undefined; |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | let shouldClose = false; |
| | | if (fileList.value.length === 0) { |
| | | message.warning('è¯·éæ©è¦å¯¼å
¥çæä»¶'); |
| | | return; |
| | | } |
| | | if (importing.value) return; |
| | | modalApi.lock(); |
| | | importing.value = true; |
| | | startMockProgress(); |
| | | try { |
| | | importResult.value = await importEmployee( |
| | | fileList.value[0] as File, |
| | | updateSupport.value, |
| | | ); |
| | | // 宿ï¼è¿åº¦æ¡èµ°æ»¡ |
| | | stopMockProgress(); |
| | | progressPercent.value = 100; |
| | | await delay(300); |
| | | emit('success'); |
| | | const failedCount = Object.keys(importResult.value?.failureNames ?? {}).length; |
| | | if (failedCount > 0) { |
| | | message.warning( |
| | | `导å
¥å®æï¼æå ${createCount.value} æ¡ãè¦ç ${updateCount.value} æ¡ï¼å¤±è´¥ ${failedCount} æ¡ï¼è¯·æ¥ç䏿¹å¤±è´¥æç»`, |
| | | ); |
| | | } else { |
| | | message.success('导å
¥å®æ'); |
| | | shouldClose = true; |
| | | } |
| | | } finally { |
| | | stopMockProgress(); |
| | | importing.value = false; |
| | | modalApi.unlock(); |
| | | if (shouldClose) { |
| | | await modalApi.close(); |
| | | } |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | /** 模æè¿åº¦æ¡ï¼è¯·æ±æé´ä» 0 èµ°å° 90% */ |
| | | function startMockProgress() { |
| | | progressPercent.value = 0; |
| | | progressTimer = setInterval(() => { |
| | | if (progressPercent.value < 90) { |
| | | progressPercent.value = Math.min( |
| | | 90, |
| | | progressPercent.value + Math.random() * 12, |
| | | ); |
| | | } |
| | | }, 300); |
| | | } |
| | | |
| | | function stopMockProgress() { |
| | | if (progressTimer) { |
| | | clearInterval(progressTimer); |
| | | progressTimer = undefined; |
| | | } |
| | | } |
| | | |
| | | function delay(ms: number) { |
| | | return new Promise((resolve) => setTimeout(resolve, ms)); |
| | | } |
| | | |
| | | function beforeUpload(file: FileType) { |
| | | fileList.value = [file]; |
| | | importResult.value = undefined; |
| | | return false; |
| | | } |
| | | |
| | | function handleRemove() { |
| | | fileList.value = []; |
| | | importResult.value = undefined; |
| | | } |
| | | |
| | | async function handleDownloadTemplate() { |
| | | const data = await importEmployeeTemplate(); |
| | | downloadFileFromBlobPart({ fileName: 'å工导å
¥æ¨¡æ¿.xls', source: data }); |
| | | } |
| | | |
| | | const createCount = computed(() => importResult.value?.createNames?.length ?? 0); |
| | | const updateCount = computed(() => importResult.value?.updateNames?.length ?? 0); |
| | | const failureCount = computed( |
| | | () => Object.keys(importResult.value?.failureNames ?? {}).length, |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="导å
¥åå·¥" class="w-[640px]"> |
| | | <div class="mx-4"> |
| | | <Upload |
| | | :max-count="1" |
| | | accept=".xls,.xlsx" |
| | | :file-list="fileList" |
| | | :before-upload="beforeUpload" |
| | | @remove="handleRemove" |
| | | > |
| | | <Button type="primary">éæ© Excel æä»¶</Button> |
| | | </Upload> |
| | | <div class="mt-4"> |
| | | <span class="mr-2">ææºå·å·²å卿¶æ´æ°å工信æ¯ï¼</span> |
| | | <Switch v-model:checked="updateSupport" /> |
| | | </div> |
| | | <div class="mt-2 text-gray-500"> |
| | | ä»
å
许导å
¥ xlsãxlsx æ ¼å¼æä»¶ï¼æ¨¡æ¿ä¸"é¨é¨åç§°/å²ä½åç§°/ç¨æ·è§è²"å¡«ååç§°ï¼å²ä½åè§è²å¤ä¸ªç¨éå·åé |
| | | </div> |
| | | <!-- 导å
¥è¿åº¦æ¡ --> |
| | | <div v-if="importing" class="mt-4"> |
| | | <div class="mb-2 text-sm text-gray-600">æ£å¨å¯¼å
¥åå·¥æ°æ®ï¼è¯·ç¨å...</div> |
| | | <Progress :percent="progressPercent" status="active" /> |
| | | </div> |
| | | <!-- 导å
¥ç»æ --> |
| | | <div |
| | | v-if="importResult" |
| | | class="mt-4 rounded-md border border-gray-200 bg-gray-50 p-3" |
| | | > |
| | | <div class="mb-2 font-medium">导å
¥ç»æ</div> |
| | | <div class="mb-2 flex gap-4"> |
| | | <span class="text-green-600">æåå建ï¼{{ createCount }} 人</span> |
| | | <span class="text-blue-600">æåæ´æ°ï¼{{ updateCount }} 人</span> |
| | | <span class="text-red-600">失败ï¼{{ failureCount }} 人</span> |
| | | </div> |
| | | <div v-if="failureCount > 0" class="max-h-40 overflow-y-auto"> |
| | | <div |
| | | v-for="(reason, name) in importResult.failureNames" |
| | | :key="name" |
| | | class="text-sm leading-6 text-red-500" |
| | | > |
| | | {{ name }}ï¼{{ reason }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <template #prepend-footer> |
| | | <Button @click="handleDownloadTemplate">ä¸è½½å¯¼å
¥æ¨¡æ¿</Button> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| | |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { HrmLeaveApi } from '#/api/hrm/leave'; |
| | | |
| | | import { markRaw } from 'vue'; |
| | | |
| | | import { DICT_TYPE } from '#/packages/constants/src'; |
| | | import { getDictOptions } from '#/packages/effects/hooks/src'; |
| | | |
| | | import { UserSelect } from '#/views/system/user/components'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'update'; |
| | |
| | | triggerFields: [''], |
| | | show: () => false, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'userId', |
| | | label: 'ç³è¯·äºº', |
| | | component: markRaw(UserSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©ç³è¯·äºº', |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'leaveType', |
| | |
| | | export function useGridColumns(): VxeTableGridOptions<HrmLeaveApi.Leave>['columns'] { |
| | | return [ |
| | | { field: 'id', title: 'ç³è¯·ç¼å·', width: 80 }, |
| | | { field: 'employeeName', title: 'ç³è¯·äºº', minWidth: 100 }, |
| | | { field: 'userName', title: 'ç³è¯·äºº', minWidth: 100 }, |
| | | { field: 'deptName', title: 'é¨é¨', minWidth: 120 }, |
| | | { |
| | | field: 'leaveType', |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { HrmLeaveApi } from '#/api/hrm/leave'; |
| | | import type { DescriptionItemSchema } from '#/components/description'; |
| | | |
| | | import { h, onMounted, ref } from 'vue'; |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { formatDateTime } from '@vben/utils'; |
| | | |
| | | import { Spin } from 'ant-design-vue'; |
| | | |
| | | import { getLeave } from '#/api/hrm/leave'; |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | import { useDescription } from '#/components/description'; |
| | | |
| | | defineOptions({ name: 'HrmLeaveApproveDetail' }); |
| | | |
| | | const props = defineProps<{ id: string }>(); |
| | | |
| | | const loading = ref(false); |
| | | const formData = ref<HrmLeaveApi.Leave>(); |
| | | |
| | | /** ææ¬åæ®µç©ºå¼å ä½ */ |
| | | function textRender(val: unknown) { |
| | | return val === 0 || val ? String(val) : '-'; |
| | | } |
| | | |
| | | const [Descriptions] = useDescription({ |
| | | bordered: true, |
| | | column: 2, |
| | | schema: [ |
| | | { label: 'ç³è¯·äºº', field: 'userName', render: textRender }, |
| | | { label: 'æå±é¨é¨', field: 'deptName', render: textRender }, |
| | | { |
| | | label: '请åç±»å', |
| | | field: 'leaveType', |
| | | render: (val) => |
| | | h(DictTag, { type: DICT_TYPE.HRM_LEAVE_TYPE, value: val }), |
| | | }, |
| | | { |
| | | label: 'å¼å§æ¶é´', |
| | | field: 'startTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { |
| | | label: 'ç»ææ¶é´', |
| | | field: 'endTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { label: 'è¯·åæ¶é¿', field: 'duration', render: textRender }, |
| | | { label: '请åäºç±', field: 'reason', render: textRender }, |
| | | { |
| | | label: '审æ¹ç¶æ', |
| | | field: 'status', |
| | | render: (val) => |
| | | h(DictTag, { type: DICT_TYPE.HRM_AUDIT_STATUS, value: val }), |
| | | }, |
| | | { label: '夿³¨', field: 'remark', render: textRender }, |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | ] as DescriptionItemSchema[], |
| | | }); |
| | | |
| | | onMounted(async () => { |
| | | try { |
| | | loading.value = true; |
| | | formData.value = await getLeave(Number(props.id)); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <ContentWrap class="m-2"> |
| | | <Spin :spinning="loading" tip="å è½½ä¸..."> |
| | | <div class="space-y-4"> |
| | | <!-- 请ååºæ¬ä¿¡æ¯ --> |
| | | <div class="rounded-lg border border-border bg-background p-4"> |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">请ååºæ¬ä¿¡æ¯</span> |
| | | </div> |
| | | <Descriptions :data="formData" /> |
| | | </div> |
| | | </div> |
| | | </Spin> |
| | | </ContentWrap> |
| | | </template> |
| | |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { HrmLeaveApi } from '#/api/hrm/leave'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { Page, useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | import { message, Modal, Select, Tag } from 'ant-design-vue'; |
| | | |
| | | import { DICT_TYPE } from '#/packages/constants/src'; |
| | | import { getDictLabel } from '#/packages/effects/hooks/src'; |
| | |
| | | import { |
| | | cancelLeave, |
| | | deleteLeave, |
| | | getLeaveApproveProcessDefinitionList, |
| | | getLeavePage, |
| | | submitLeave, |
| | | } from '#/api/hrm/leave'; |
| | |
| | | } |
| | | } |
| | | |
| | | /** æäº¤è¯·åç³è¯· */ |
| | | const submitModalVisible = ref(false); |
| | | const currentRow = ref<HrmLeaveApi.Leave>(); |
| | | const processList = ref<HrmLeaveApi.ApproveProcess[]>([]); |
| | | const selectedProcessKey = ref<string>(); |
| | | const submitLoading = ref(false); |
| | | |
| | | /** æäº¤è¯·åç³è¯·ï¼å
éæ©å®¡æ¹æµç¨ï¼ */ |
| | | async function handleSubmit(row: HrmLeaveApi.Leave) { |
| | | await submitLeave(row.id!); |
| | | message.success('æäº¤æå'); |
| | | handleRefresh(); |
| | | processList.value = await getLeaveApproveProcessDefinitionList(); |
| | | currentRow.value = row; |
| | | selectedProcessKey.value = undefined; |
| | | submitModalVisible.value = true; |
| | | } |
| | | |
| | | async function confirmSubmit() { |
| | | if (!selectedProcessKey.value) { |
| | | message.warning('è¯·éæ©è¯·åå®¡æ¹æµç¨'); |
| | | return; |
| | | } |
| | | submitLoading.value = true; |
| | | try { |
| | | await submitLeave(currentRow.value!.id!, selectedProcessKey.value); |
| | | message.success('æäº¤æå'); |
| | | submitModalVisible.value = false; |
| | | handleRefresh(); |
| | | } finally { |
| | | submitLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | /** æ¤é请åç³è¯· */ |
| | |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | <Modal |
| | | v-model:open="submitModalVisible" |
| | | title="æäº¤è³åååå
¬" |
| | | width="420px" |
| | | :confirm-loading="submitLoading" |
| | | @ok="confirmSubmit" |
| | | > |
| | | <Select |
| | | v-model:value="selectedProcessKey" |
| | | class="w-full" |
| | | placeholder="è¯·éæ©è¯·åå®¡æ¹æµç¨" |
| | | :options="processList.map((item) => ({ label: item.name, value: item.key }))" |
| | | /> |
| | | </Modal> |
| | | </Page> |
| | | </template> |
| | |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useUserStore } from '@vben/stores'; |
| | | |
| | | import { useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formType = ref<FormType>('create'); |
| | | |
| | | const userStore = useUserStore(); |
| | | |
| | | const isDetail = computed(() => formType.value === 'detail'); |
| | | const getTitle = computed(() => { |
| | |
| | | formApi.setDisabled(data.formType === 'detail'); |
| | | modalApi.setState({ showConfirmButton: data.formType !== 'detail' }); |
| | | if (!data?.id) { |
| | | // æ°å¢æ¶é»è®¤ç³è¯·äººä¸ºå½åç»å½ç¨æ·ï¼å¯ä¿®æ¹ |
| | | await formApi.setFieldValue('userId', userStore.userInfo?.id); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { HrmOvertimeApi } from '#/api/hrm/overtime'; |
| | | |
| | | import { DICT_TYPE } from '#/packages/constants/src'; |
| | | import { getDictOptions } from '#/packages/effects/hooks/src'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'update'; |
| | | |
| | | export const OVERTIME_TYPE_OPTIONS = [ |
| | | { label: '工使¥å ç', value: 1 }, |
| | | { label: '伿¯æ¥å ç', value: 2 }, |
| | | { label: 'æ³å®è忥å ç', value: 3 }, |
| | | ]; |
| | | |
| | | export const RELATED_TYPE_OPTIONS = [ |
| | | { label: '项ç®', value: 1 }, |
| | | { label: 'å·¥å', value: 2 }, |
| | | ]; |
| | | |
| | | export function getOvertimeTypeLabel(type?: number) { |
| | | return OVERTIME_TYPE_OPTIONS.find((item) => item.value === type)?.label || '-'; |
| | | } |
| | | |
| | | /** æ°å¢/ä¿®æ¹å çç³è¯·ç表å */ |
| | | export function useFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | | { |
| | | fieldName: 'employeeId', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | | { |
| | | fieldName: 'baseInfoDivider', |
| | | label: 'ç³è¯·äººåºç¡ä¿¡æ¯', |
| | | component: 'Divider', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'employeeName', |
| | | label: 'ç³è¯·äºº', |
| | | component: 'Input', |
| | | componentProps: { disabled: true, placeholder: 'ç³»ç»èªå¨å¸¦åº' }, |
| | | }, |
| | | { |
| | | fieldName: 'employeeNo', |
| | | label: 'å·¥å·', |
| | | component: 'Input', |
| | | componentProps: { disabled: true, placeholder: 'ç³»ç»èªå¨å¸¦åº' }, |
| | | }, |
| | | { |
| | | fieldName: 'deptName', |
| | | label: 'æå±é¨é¨', |
| | | component: 'Input', |
| | | componentProps: { disabled: true, placeholder: 'ç³»ç»èªå¨å¸¦åº' }, |
| | | }, |
| | | { |
| | | fieldName: 'postName', |
| | | label: 'å²ä½/è级', |
| | | component: 'Input', |
| | | componentProps: { disabled: true, placeholder: 'ç³»ç»èªå¨å¸¦åº' }, |
| | | }, |
| | | { |
| | | fieldName: 'overtimeInfoDivider', |
| | | label: 'å çæ ¸å¿ä¿¡æ¯', |
| | | component: 'Divider', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'overtimeDate', |
| | | label: 'å çæ¥æ', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©å çæ¥æ', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | style: { width: '100%' }, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'overtimeType', |
| | | label: 'å çç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: OVERTIME_TYPE_OPTIONS, |
| | | placeholder: 'è¯·éæ©å çç±»å', |
| | | }, |
| | | help: 'ä¸åç±»å对åºä¸åçèªèµè®¡ç®è§å', |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'startTime', |
| | | label: 'å¼å§æ¶é´', |
| | | component: 'TimePicker', |
| | | componentProps: { |
| | | format: 'HH:mm', |
| | | minuteStep: 30, |
| | | placeholder: 'è¯·éæ©å¼å§æ¶é´', |
| | | valueFormat: 'HH:mm:ss', |
| | | style: { width: '100%' }, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'endTime', |
| | | label: 'ç»ææ¶é´', |
| | | component: 'TimePicker', |
| | | componentProps: { |
| | | format: 'HH:mm', |
| | | minuteStep: 30, |
| | | placeholder: 'è¯·éæ©ç»ææ¶é´', |
| | | valueFormat: 'HH:mm:ss', |
| | | style: { width: '100%' }, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'crossDay', |
| | | label: 'æ¯å¦è·¨å¤©', |
| | | component: 'Switch', |
| | | componentProps: { |
| | | checkedChildren: 'æ¯', |
| | | disabled: true, |
| | | unCheckedChildren: 'å¦', |
| | | }, |
| | | defaultValue: false, |
| | | help: 'ç»ææ¶é´æ©äºæçäºå¼å§æ¶é´æ¶ï¼ç³»ç»èªå¨æ 记为跨天', |
| | | }, |
| | | { |
| | | fieldName: 'mealDeduction', |
| | | label: 'æ£é¤ç¨é¤æ¶é´', |
| | | component: 'Switch', |
| | | componentProps: { |
| | | checkedChildren: 'æ£é¤', |
| | | unCheckedChildren: '䏿£é¤', |
| | | }, |
| | | defaultValue: false, |
| | | }, |
| | | { |
| | | fieldName: 'mealDeductionHours', |
| | | label: 'ç¨é¤æ£é¤æ¶é¿', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | min: 0.5, |
| | | max: 4, |
| | | precision: 1, |
| | | step: 0.5, |
| | | style: { width: '100%' }, |
| | | }, |
| | | defaultValue: 1, |
| | | suffix: 'å°æ¶', |
| | | dependencies: { |
| | | triggerFields: ['mealDeduction'], |
| | | show: (values) => Boolean(values.mealDeduction), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'duration', |
| | | label: 'å çæ¶é¿', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | disabled: true, |
| | | min: 0, |
| | | precision: 1, |
| | | step: 0.5, |
| | | style: { width: '100%' }, |
| | | }, |
| | | help: 'æå®é
æ¶æ®µæ£é¤ç¨é¤æ¶é´åï¼èªå¨åæ´å° 0.5 å°æ¶', |
| | | suffix: 'å°æ¶', |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'reasonInfoDivider', |
| | | label: 'å çäºç±', |
| | | component: 'Divider', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'reason', |
| | | label: 'å çåå ', |
| | | component: 'Textarea', |
| | | formItemClass: 'col-span-2', |
| | | componentProps: { |
| | | maxlength: 500, |
| | | placeholder: '请说æå ççå¿
è¦æ§ååå ', |
| | | rows: 3, |
| | | showCount: true, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'relatedType', |
| | | label: 'å
³èç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: RELATED_TYPE_OPTIONS, |
| | | placeholder: 'å¯éï¼é¡¹ç®æå·¥å', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'relatedCode', |
| | | label: '项ç®/å·¥åç¼å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | maxlength: 100, |
| | | placeholder: '请è¾å
¥å
³èé¡¹ç®æå·¥åç¼å·', |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['relatedType'], |
| | | componentProps: (values) => ({ |
| | | allowClear: true, |
| | | maxlength: 100, |
| | | placeholder: values.relatedType === 1 |
| | | ? '请è¾å
¥é¡¹ç®ç¼å·' |
| | | : values.relatedType === 2 |
| | | ? '请è¾å
¥å·¥åç¼å·' |
| | | : '请å
éæ©å
³èç±»å', |
| | | disabled: !values.relatedType, |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'workContent', |
| | | label: 'å·¥ä½å
容ç®è¿°', |
| | | component: 'Textarea', |
| | | formItemClass: 'col-span-2', |
| | | componentProps: { |
| | | maxlength: 1000, |
| | | placeholder: '请ç®è¦æè¿°å çæé´è®¡å宿çå
·ä½å·¥ä½', |
| | | rows: 4, |
| | | showCount: true, |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表æç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'overtimeDate', |
| | | label: 'å çæ¥æ', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: 'è¯·éæ©å çæ¥æ', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'overtimeType', |
| | | label: 'å çç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: OVERTIME_TYPE_OPTIONS, |
| | | placeholder: 'è¯·éæ©å çç±»å', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: '审æ¹ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.HRM_AUDIT_STATUS, 'number'), |
| | | placeholder: 'è¯·éæ©å®¡æ¹ç¶æ', |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** åè¡¨åæ®µ */ |
| | | export function useGridColumns(): VxeTableGridOptions<HrmOvertimeApi.Overtime>['columns'] { |
| | | return [ |
| | | { field: 'id', title: 'ç³è¯·ç¼å·', width: 90 }, |
| | | { field: 'employeeName', title: 'ç³è¯·äºº', minWidth: 100 }, |
| | | { field: 'employeeNo', title: 'å·¥å·', minWidth: 110 }, |
| | | { field: 'deptName', title: 'æå±é¨é¨', minWidth: 120 }, |
| | | { field: 'overtimeDate', title: 'å çæ¥æ', width: 120 }, |
| | | { |
| | | field: 'overtimeType', |
| | | title: 'å çç±»å', |
| | | width: 130, |
| | | slots: { default: 'overtimeType' }, |
| | | }, |
| | | { field: 'startTime', title: 'å¼å§æ¶é´', width: 100 }, |
| | | { field: 'endTime', title: 'ç»ææ¶é´', width: 100 }, |
| | | { field: 'duration', title: 'æ¶é¿(å°æ¶)', width: 100 }, |
| | | { |
| | | field: 'status', |
| | | title: '审æ¹ç¶æ', |
| | | width: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.HRM_AUDIT_STATUS }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 200, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { HrmOvertimeApi } from '#/api/hrm/overtime'; |
| | | import type { DescriptionItemSchema } from '#/components/description'; |
| | | |
| | | import { h, onMounted, ref } from 'vue'; |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { formatDateTime } from '@vben/utils'; |
| | | |
| | | import { Spin, Tag } from 'ant-design-vue'; |
| | | |
| | | import { getOvertime } from '#/api/hrm/overtime'; |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | import { useDescription } from '#/components/description'; |
| | | import { getOvertimeTypeLabel } from '#/views/hrm/overtime/data'; |
| | | |
| | | defineOptions({ name: 'HrmOvertimeApproveDetail' }); |
| | | |
| | | const props = defineProps<{ id: string }>(); |
| | | const loading = ref(false); |
| | | const formData = ref<HrmOvertimeApi.Overtime>(); |
| | | |
| | | function textRender(value: unknown) { |
| | | return value === 0 || value ? String(value) : '-'; |
| | | } |
| | | |
| | | const [Descriptions] = useDescription({ |
| | | bordered: true, |
| | | column: 2, |
| | | schema: [ |
| | | { label: 'ç³è¯·äºº', field: 'employeeName', render: textRender }, |
| | | { label: 'å·¥å·', field: 'employeeNo', render: textRender }, |
| | | { label: 'æå±é¨é¨', field: 'deptName', render: textRender }, |
| | | { label: 'å²ä½/è级', field: 'postName', render: textRender }, |
| | | { label: 'å çæ¥æ', field: 'overtimeDate', render: textRender }, |
| | | { |
| | | label: 'å çç±»å', |
| | | field: 'overtimeType', |
| | | render: (value) => h(Tag, { color: value === 3 ? 'red' : value === 2 ? 'orange' : 'blue' }, () => getOvertimeTypeLabel(Number(value))), |
| | | }, |
| | | { label: 'å¼å§æ¶é´', field: 'startTime', render: textRender }, |
| | | { label: 'ç»ææ¶é´', field: 'endTime', render: textRender }, |
| | | { label: 'å çæ¶é¿', field: 'duration', render: (value) => `${textRender(value)} å°æ¶` }, |
| | | { label: 'æ¯å¦è·¨å¤©', field: 'crossDay', render: (value) => value ? 'æ¯' : 'å¦' }, |
| | | { label: 'ç¨é¤æ£é¤', field: 'mealDeduction', render: (value) => value ? 'æ¯' : 'å¦' }, |
| | | { label: 'æ£é¤æ¶é¿', field: 'mealDeductionHours', render: (value) => value ? `${value} å°æ¶` : '-' }, |
| | | { label: 'å çåå ', field: 'reason', render: textRender }, |
| | | { label: 'å
³èç±»å', field: 'relatedType', render: (value) => value === 1 ? '项ç®' : value === 2 ? 'å·¥å' : '-' }, |
| | | { label: '项ç®/å·¥åç¼å·', field: 'relatedCode', render: textRender }, |
| | | { label: 'å·¥ä½å
容ç®è¿°', field: 'workContent', render: textRender }, |
| | | { |
| | | label: '审æ¹ç¶æ', |
| | | field: 'status', |
| | | render: (value) => h(DictTag, { type: DICT_TYPE.HRM_AUDIT_STATUS, value }), |
| | | }, |
| | | { label: 'å建æ¶é´', field: 'createTime', render: (value) => formatDateTime(value) as string }, |
| | | ] as DescriptionItemSchema[], |
| | | }); |
| | | |
| | | onMounted(async () => { |
| | | loading.value = true; |
| | | try { |
| | | formData.value = await getOvertime(Number(props.id)); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <ContentWrap class="m-2"> |
| | | <Spin :spinning="loading" tip="å è½½ä¸..."> |
| | | <div class="rounded-lg border border-border bg-background p-4"> |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">å çç³è¯·ä¿¡æ¯</span> |
| | | </div> |
| | | <Descriptions :data="formData" /> |
| | | </div> |
| | | </Spin> |
| | | </ContentWrap> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { HrmOvertimeApi } from '#/api/hrm/overtime'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { Page, useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | |
| | | import { message, Modal, Select, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | cancelOvertime, |
| | | deleteOvertime, |
| | | getOvertimeApproveProcessDefinitionList, |
| | | getOvertimePage, |
| | | submitOvertime, |
| | | } from '#/api/hrm/overtime'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { |
| | | getOvertimeTypeLabel, |
| | | useGridColumns, |
| | | useGridFormSchema, |
| | | } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | function handleDetail(row: HrmOvertimeApi.Overtime) { |
| | | formModalApi.setData({ id: row.id, formType: 'detail' }).open(); |
| | | } |
| | | |
| | | function handleEdit(row: HrmOvertimeApi.Overtime) { |
| | | formModalApi.setData({ id: row.id, formType: 'update' }).open(); |
| | | } |
| | | |
| | | async function handleDelete(row: HrmOvertimeApi.Overtime) { |
| | | const hideLoading = message.loading({ content: 'æ£å¨å é¤å çç³è¯·...', duration: 0 }); |
| | | try { |
| | | await deleteOvertime(row.id!); |
| | | message.success('å çç³è¯·å 餿å'); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | const submitModalVisible = ref(false); |
| | | const currentRow = ref<HrmOvertimeApi.Overtime>(); |
| | | const processList = ref<HrmOvertimeApi.ApproveProcess[]>([]); |
| | | const selectedProcessKey = ref<string>(); |
| | | const submitLoading = ref(false); |
| | | |
| | | async function handleSubmit(row: HrmOvertimeApi.Overtime) { |
| | | processList.value = await getOvertimeApproveProcessDefinitionList(); |
| | | currentRow.value = row; |
| | | selectedProcessKey.value = undefined; |
| | | submitModalVisible.value = true; |
| | | } |
| | | |
| | | async function confirmSubmit() { |
| | | if (!selectedProcessKey.value) { |
| | | message.warning('è¯·éæ©åååå
¬å®¡æ¹æµç¨'); |
| | | return; |
| | | } |
| | | submitLoading.value = true; |
| | | try { |
| | | await submitOvertime(currentRow.value!.id!, selectedProcessKey.value); |
| | | message.success('å·²æäº¤è³åååå
¬å®¡æ¹'); |
| | | submitModalVisible.value = false; |
| | | handleRefresh(); |
| | | } finally { |
| | | submitLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | async function handleCancel(row: HrmOvertimeApi.Overtime) { |
| | | await cancelOvertime(row.id!); |
| | | message.success('æ¤éæå'); |
| | | handleRefresh(); |
| | | } |
| | | |
| | | function getOvertimeTypeColor(type?: number) { |
| | | return type === 1 ? 'blue' : type === 2 ? 'orange' : type === 3 ? 'red' : 'default'; |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { schema: useGridFormSchema() }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => |
| | | await getOvertimePage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }), |
| | | }, |
| | | }, |
| | | rowConfig: { keyField: 'id', isHover: true }, |
| | | toolbarConfig: { refresh: true, search: true }, |
| | | } as VxeTableGridOptions<HrmOvertimeApi.Overtime>, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <Grid table-title="å çç³è¯·å表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['å çç³è¯·']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['hrm:overtime:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #overtimeType="{ row }"> |
| | | <Tag :color="getOvertimeTypeColor(row.overtimeType)"> |
| | | {{ getOvertimeTypeLabel(row.overtimeType) }} |
| | | </Tag> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { label: 'æ¥ç', type: 'link', onClick: handleDetail.bind(null, row) }, |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['hrm:overtime:update'], |
| | | ifShow: row.status === 0, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'æäº¤', |
| | | type: 'link', |
| | | auth: ['hrm:overtime:submit'], |
| | | ifShow: row.status === 0, |
| | | onClick: handleSubmit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'æ¤é', |
| | | type: 'link', |
| | | auth: ['hrm:overtime:cancel'], |
| | | ifShow: row.status === 10, |
| | | onClick: handleCancel.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['hrm:overtime:delete'], |
| | | ifShow: row.status === 0 || row.status === 40, |
| | | popConfirm: { |
| | | title: '确认å é¤è¯¥å çç³è¯·åï¼', |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | <Modal |
| | | v-model:open="submitModalVisible" |
| | | title="æäº¤è³åååå
¬" |
| | | width="420px" |
| | | :confirm-loading="submitLoading" |
| | | @ok="confirmSubmit" |
| | | > |
| | | <Select |
| | | v-model:value="selectedProcessKey" |
| | | class="w-full" |
| | | placeholder="è¯·éæ©å çå®¡æ¹æµç¨" |
| | | :options="processList.map((item) => ({ label: item.name, value: item.key }))" |
| | | /> |
| | | </Modal> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { HrmOvertimeApi } from '#/api/hrm/overtime'; |
| | | import type { FormType } from '../data'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createOvertime, |
| | | getCurrentOvertimeApplicant, |
| | | getOvertime, |
| | | updateOvertime, |
| | | } from '#/api/hrm/overtime'; |
| | | import { $t } from '#/locales'; |
| | | import { useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formType = ref<FormType>('create'); |
| | | |
| | | const isDetail = computed(() => formType.value === 'detail'); |
| | | const getTitle = computed(() => { |
| | | if (isDetail.value) return 'æ¥çå çç³è¯·'; |
| | | return formType.value === 'update' ? 'ä¿®æ¹å çç³è¯·' : 'æ°å¢å çç³è¯·'; |
| | | }); |
| | | |
| | | /** æ ¹æ®æ¶æ®µã跨天æ è¯åç¨é¤æ£é¤è®¡ç®å çæ¶é¿ï¼å¹¶æ 0.5 å°æ¶åæ´ */ |
| | | function calculateDuration(values: Record<string, any>) { |
| | | const { startTime, endTime } = values; |
| | | if (!startTime || !endTime) return { crossDay: false, duration: 0 }; |
| | | |
| | | const toMinutes = (time: string) => { |
| | | const [hour = 0, minute = 0] = time.split(':').map(Number); |
| | | return hour * 60 + minute; |
| | | }; |
| | | const startMinutes = toMinutes(startTime); |
| | | let endMinutes = toMinutes(endTime); |
| | | const crossDay = endMinutes <= startMinutes; |
| | | if (crossDay) endMinutes += 24 * 60; |
| | | |
| | | const deductionMinutes = values.mealDeduction |
| | | ? Number(values.mealDeductionHours || 0) * 60 |
| | | : 0; |
| | | const actualHours = Math.max(0, (endMinutes - startMinutes - deductionMinutes) / 60); |
| | | return { |
| | | crossDay, |
| | | duration: Math.round(actualHours * 2) / 2, |
| | | }; |
| | | } |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | | formItemClass: 'col-span-1', |
| | | labelWidth: 120, |
| | | }, |
| | | wrapperClass: 'grid-cols-2', |
| | | layout: 'horizontal', |
| | | schema: [], |
| | | scrollToFirstError: true, |
| | | showDefaultActions: false, |
| | | handleValuesChange: (values, changedFields) => { |
| | | const durationFields = [ |
| | | 'startTime', |
| | | 'endTime', |
| | | 'mealDeduction', |
| | | 'mealDeductionHours', |
| | | ]; |
| | | if (changedFields.some((field) => durationFields.includes(field))) { |
| | | const result = calculateDuration(values); |
| | | void formApi.setValues(result); |
| | | } |
| | | if (changedFields.includes('relatedType') && !values.relatedType) { |
| | | void formApi.setFieldValue('relatedCode', undefined); |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | async function setCurrentEmployee() { |
| | | const employee = await getCurrentOvertimeApplicant(); |
| | | await formApi.setValues({ |
| | | employeeId: employee.employeeId, |
| | | employeeName: employee.employeeName, |
| | | employeeNo: employee.employeeNo, |
| | | deptName: employee.deptName, |
| | | postName: employee.postName, |
| | | mealDeduction: false, |
| | | mealDeductionHours: 1, |
| | | crossDay: false, |
| | | duration: 0, |
| | | }); |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | if (isDetail.value) { |
| | | await modalApi.close(); |
| | | return; |
| | | } |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) return; |
| | | |
| | | const data = (await formApi.getValues()) as HrmOvertimeApi.Overtime; |
| | | if (!data.employeeId) { |
| | | message.warning('å½åè´¦å·æªå
³èå工档æ¡ï¼ææ¶æ æ³æäº¤å çç³è¯·'); |
| | | return; |
| | | } |
| | | if (!data.duration || data.duration <= 0) { |
| | | message.warning('å çæ¶é¿å¿
é¡»å¤§äº 0 å°æ¶ï¼è¯·æ£æ¥æ¶æ®µæç¨é¤æ£é¤'); |
| | | return; |
| | | } |
| | | if (data.relatedType && !data.relatedCode?.trim()) { |
| | | message.warning('è¯·éæ©å
³èç±»ååå¡«å对åºçé¡¹ç®æå·¥åç¼å·'); |
| | | return; |
| | | } |
| | | |
| | | modalApi.lock(); |
| | | try { |
| | | if (data.id) await updateOvertime(data); |
| | | else await createOvertime(data); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) return; |
| | | |
| | | const data = modalApi.getData<{ formType: FormType; id?: number }>(); |
| | | formType.value = data.formType; |
| | | formApi.setState({ schema: useFormSchema() }); |
| | | formApi.setDisabled(isDetail.value); |
| | | modalApi.setState({ showConfirmButton: !isDetail.value }); |
| | | |
| | | if (!data.id) { |
| | | await setCurrentEmployee(); |
| | | return; |
| | | } |
| | | |
| | | modalApi.lock(); |
| | | try { |
| | | await formApi.setValues(await getOvertime(data.id)); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-[900px]"> |
| | | <Form class="mx-4" /> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { HrmResignationApi } from '#/api/hrm/resignation'; |
| | | import type { DescriptionItemSchema } from '#/components/description'; |
| | | |
| | | import { h, onMounted, ref } from 'vue'; |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { formatDateTime } from '@vben/utils'; |
| | | |
| | | import { Spin } from 'ant-design-vue'; |
| | | |
| | | import { getResignation } from '#/api/hrm/resignation'; |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | import { useDescription } from '#/components/description'; |
| | | |
| | | defineOptions({ name: 'HrmResignationApproveDetail' }); |
| | | |
| | | const props = defineProps<{ id: string }>(); |
| | | |
| | | const loading = ref(false); |
| | | const formData = ref<HrmResignationApi.Resignation>(); |
| | | |
| | | /** ææ¬åæ®µç©ºå¼å ä½ */ |
| | | function textRender(val: unknown) { |
| | | return val === 0 || val ? String(val) : '-'; |
| | | } |
| | | |
| | | const [Descriptions] = useDescription({ |
| | | bordered: true, |
| | | column: 2, |
| | | schema: [ |
| | | { label: 'åå·¥å§å', field: 'employeeName', render: textRender }, |
| | | { label: 'æå±é¨é¨', field: 'deptName', render: textRender }, |
| | | { label: 'èä½', field: 'postName', render: textRender }, |
| | | { |
| | | label: '离èç±»å', |
| | | field: 'resignationType', |
| | | render: (val) => |
| | | h(DictTag, { type: DICT_TYPE.HRM_RESIGNATION_TYPE, value: val }), |
| | | }, |
| | | { |
| | | label: 'é¢è®¡ç¦»èæ¥æ', |
| | | field: 'expectedDate', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { label: '离èåå ', field: 'reason', render: textRender }, |
| | | { label: '交æ¥äºº', field: 'handoverUserName', render: textRender }, |
| | | { |
| | | label: '交æ¥ç¶æ', |
| | | field: 'handoverStatus', |
| | | render: (val) => |
| | | h(DictTag, { type: DICT_TYPE.HRM_HANDOVER_STATUS, value: val }), |
| | | }, |
| | | { |
| | | label: '审æ¹ç¶æ', |
| | | field: 'status', |
| | | render: (val) => |
| | | h(DictTag, { type: DICT_TYPE.HRM_AUDIT_STATUS, value: val }), |
| | | }, |
| | | { label: '夿³¨', field: 'remark', render: textRender }, |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | ] as DescriptionItemSchema[], |
| | | }); |
| | | |
| | | onMounted(async () => { |
| | | try { |
| | | loading.value = true; |
| | | formData.value = await getResignation(Number(props.id)); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <ContentWrap class="m-2"> |
| | | <Spin :spinning="loading" tip="å è½½ä¸..."> |
| | | <div class="space-y-4"> |
| | | <!-- 离èåºæ¬ä¿¡æ¯ --> |
| | | <div class="rounded-lg border border-border bg-background p-4"> |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">离èåºæ¬ä¿¡æ¯</span> |
| | | </div> |
| | | <Descriptions :data="formData" /> |
| | | </div> |
| | | </div> |
| | | </Spin> |
| | | </ContentWrap> |
| | | </template> |
| | |
| | | { field: 'actualSalary', title: 'å®åå·¥èµ', width: 100, formatter: 'formatAmount2' }, |
| | | { field: 'workDays', title: 'åºå¤å¤©æ°', width: 80 }, |
| | | { field: 'overtimeHours', title: 'å çæ¶é¿(h)', width: 100 }, |
| | | { field: 'totalWorkHours', title: 'æçå·¥æ¶(h)', width: 100 }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | |
| | | { title: 'è¯·åæ£æ¬¾', dataIndex: 'leaveDeduction', width: 110 }, |
| | | { title: 'åºå¤å¤©æ°', dataIndex: 'workDays', width: 90 }, |
| | | { title: 'å çæ¶é¿', dataIndex: 'overtimeHours', width: 100 }, |
| | | { title: 'æçå·¥æ¶(h)', dataIndex: 'totalWorkHours', width: 100 }, |
| | | { title: 'å®åå·¥èµ', dataIndex: 'actualSalary', width: 100, fixed: 'right' }, |
| | | ]; |
| | | } |
| | |
| | | import { |
| | | calculateSalary, |
| | | confirmSalary, |
| | | deleteSalaryCalculation, |
| | | exportSalaryCalculation, |
| | | getSalaryCalculationPage, |
| | | previewSalaryCalculation, |
| | | revokeSalaryCalculation, |
| | | saveSalaryCalculation, |
| | | } from '#/api/hrm/salary/calculation'; |
| | | import { getDeptList } from '#/api/system/dept'; |
| | |
| | | handleRefresh(); |
| | | } |
| | | |
| | | /** æ¹é确认 */ |
| | | /** æ¹é确认éä¸çè®°å½ */ |
| | | async function handleBatchConfirm() { |
| | | const records = gridApi.grid.getCheckboxRecords() as HrmSalaryCalculationApi.SalaryCalculation[]; |
| | | if (records.length === 0) { |
| | |
| | | await confirmSalary(ids); |
| | | message.success('确认æå'); |
| | | handleRefresh(); |
| | | } |
| | | |
| | | /** æ¹éä¿åéä¸çæ ¸ç®è®°å½ï¼ä»
å¾
ç¡®è®¤ç¶æï¼ */ |
| | | async function handleBatchSave() { |
| | | const records = gridApi.grid.getCheckboxRecords() as HrmSalaryCalculationApi.SalaryCalculation[]; |
| | | if (records.length === 0) { |
| | | message.warning('è¯·éæ©è¦ä¿åçè®°å½'); |
| | | return; |
| | | } |
| | | const pending = records.filter((r) => r.status === 0); |
| | | if (pending.length === 0) { |
| | | message.warning('éä¸çè®°å½åéå¾
ç¡®è®¤ç¶æï¼æ éä¿å'); |
| | | return; |
| | | } |
| | | const first = pending[0]; |
| | | if (!first?.period) { |
| | | message.warning('ç¼ºå°æ ¸ç®å¨æ'); |
| | | return; |
| | | } |
| | | await saveSalaryCalculation({ period: first.period, list: pending }); |
| | | message.success('æ¹éä¿åæå'); |
| | | handleRefresh(); |
| | | } |
| | | |
| | | /** æ¤é确认ï¼å·²ç¡®è®¤ â å¾
确认ï¼å¯éæ°æ ¸ç®ï¼ */ |
| | | async function handleRevoke(row: HrmSalaryCalculationApi.SalaryCalculation) { |
| | | Modal.confirm({ |
| | | title: 'æ¤é确认', |
| | | content: `ç¡®å®æ¤éã${row.no}ãç确认åï¼æ¤éåå¯éæ°æ ¸ç®ã`, |
| | | okText: 'ç¡®å®', |
| | | cancelText: 'åæ¶', |
| | | onOk: async () => { |
| | | await revokeSalaryCalculation([row.id!]); |
| | | message.success('æ¤éæå'); |
| | | handleRefresh(); |
| | | }, |
| | | }); |
| | | } |
| | | |
| | | /** å é¤å¾
ç¡®è®¤çæ ¸ç®è®°å½ */ |
| | | async function handleDelete(row: HrmSalaryCalculationApi.SalaryCalculation) { |
| | | Modal.confirm({ |
| | | title: '确认å é¤', |
| | | content: `ç¡®å®å é¤ã${row.no}ãçæ ¸ç®è®°å½åï¼å é¤åééæ°æ ¸ç®ã`, |
| | | okText: 'ç¡®å®', |
| | | cancelText: 'åæ¶', |
| | | onOk: async () => { |
| | | await deleteSalaryCalculation(row.id!); |
| | | message.success('å 餿å'); |
| | | handleRefresh(); |
| | | }, |
| | | }); |
| | | } |
| | | |
| | | /** 导åºèªé
¬æ ¸ç® */ |
| | |
| | | keepSource: true, |
| | | checkboxConfig: { |
| | | highlight: true, |
| | | reserve: true, |
| | | }, |
| | | proxyConfig: { |
| | | ajax: { |
| | |
| | | ...formValues, |
| | | }), |
| | | }, |
| | | }, |
| | | pagingConfig: { |
| | | pageSize: 20, |
| | | pageSizes: [20, 50, 100], |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | |
| | | v-if="previewData.length > 0" |
| | | :columns="usePreviewColumns()" |
| | | :data-source="previewData" |
| | | :pagination="false" |
| | | :pagination="{ pageSize: 20, showSizeChanger: true, showTotal: (t) => `å
± ${t} 人` }" |
| | | :scroll="{ x: 1800, y: 400 }" |
| | | size="small" |
| | | row-key="userId" |
| | |
| | | onClick: handleBatchConfirm, |
| | | }, |
| | | { |
| | | label: 'æ¹éä¿å', |
| | | type: 'primary', |
| | | auth: ['hrm:salary-calculation:save'], |
| | | onClick: handleBatchSave, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.export'), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.DOWNLOAD, |
| | |
| | | ifShow: row.status === 0, |
| | | onClick: handleConfirm.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'å é¤', |
| | | type: 'link', |
| | | danger: true, |
| | | auth: ['hrm:salary-calculation:delete'], |
| | | ifShow: row.status === 0, |
| | | onClick: handleDelete.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'æ¤é确认', |
| | | type: 'link', |
| | | auth: ['hrm:salary-calculation:confirm'], |
| | | ifShow: row.status === 10, |
| | | onClick: handleRevoke.bind(null, row), |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | |
| | | import type { HrmLeaveTypeConfigApi } from '#/api/hrm/leave-type-config'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'update'; |
| | | export type FormType = 'create' | 'update'; |
| | | |
| | | /** æ°å¢/ä¿®æ¹è¯·åæ£æ¬¾é
ç½®ç表å */ |
| | | export function useFormSchema(): VbenFormSchema[] { |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'name', |
| | | fieldName: 'leaveType', |
| | | label: '请åç±»å', |
| | | component: 'Input', |
| | | component: 'Select', |
| | | componentProps: { |
| | | disabled: true, |
| | | placeholder: '', |
| | | options: [ |
| | | { label: 'äºå', value: 1 }, |
| | | { label: 'ç
å', value: 2 }, |
| | | { label: 'å¹´å', value: 3 }, |
| | | { label: 'å©å', value: 4 }, |
| | | { label: '产å', value: 5 }, |
| | | { label: '丧å', value: 6 }, |
| | | ], |
| | | placeholder: 'è¯·éæ©è¯·åç±»å', |
| | | style: { width: '100%' }, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['id'], |
| | | // ç¼è¾æ¶ä¸å
许修æ¹è¯·åç±»å |
| | | componentProps: (values: Record<string, unknown>) => ({ |
| | | disabled: Boolean(values.id), |
| | | }), |
| | | }, |
| | | rules: 'required', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'RadioGroup', |
| | | defaultValue: 0, |
| | | componentProps: { |
| | | options: [ |
| | | { label: 'å¯ç¨', value: 0 }, |
| | |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getLeaveTypeConfigList } from '#/api/hrm/leave-type-config'; |
| | | import { |
| | | deleteLeaveTypeConfig, |
| | | getLeaveTypeConfigList, |
| | | } from '#/api/hrm/leave-type-config'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns } from './data'; |
| | |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** æ°å¢é
ç½® */ |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | /** ç¼è¾é
ç½® */ |
| | | function handleEdit(row: HrmLeaveTypeConfigApi.LeaveTypeConfig) { |
| | | formModalApi.setData({ id: row.id, row }).open(); |
| | | formModalApi.setData({ formType: 'update', row }).open(); |
| | | } |
| | | |
| | | /** å é¤é
ç½® */ |
| | | async function handleDelete(row: HrmLeaveTypeConfigApi.LeaveTypeConfig) { |
| | | await deleteLeaveTypeConfig(row.id!); |
| | | message.success($t('ui.actionMessage.deleteSuccess', ['è¯·åæ£æ¬¾é
ç½®'])); |
| | | handleRefresh(); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <Grid table-title="è¯·åæ£æ¬¾é
ç½®å表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['è¯·åæ£æ¬¾é
ç½®']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['hrm:leave-type-config:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #deductionRatio="{ row }"> |
| | | <Tag :color="row.deductionRatio === 0 ? 'success' : row.deductionRatio === 1 ? 'error' : 'warning'"> |
| | | {{ row.deductionRatio === 0 ? '䏿£æ¬¾' : `${(row.deductionRatio * 100).toFixed(0)}%` }} |
| | |
| | | auth: ['hrm:leave-type-config:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['hrm:leave-type-config:delete'], |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', ['è¯·åæ£æ¬¾é
ç½®']), |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormType } from '../data'; |
| | | import type { HrmLeaveTypeConfigApi } from '#/api/hrm/leave-type-config'; |
| | | |
| | | import { ref } from 'vue'; |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { updateLeaveTypeConfig } from '#/api/hrm/leave-type-config'; |
| | | import { |
| | | createLeaveTypeConfig, |
| | | updateLeaveTypeConfig, |
| | | } from '#/api/hrm/leave-type-config'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formType = ref<FormType>('create'); |
| | | |
| | | const getTitle = computed(() => |
| | | formType.value === 'update' ? 'ç¼è¾æ£æ¬¾æ¯ä¾' : 'æ°å¢è¯·åæ£æ¬¾é
ç½®', |
| | | ); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | const data = await formApi.getValues(); |
| | | const data = (await formApi.getValues()) as HrmLeaveTypeConfigApi.LeaveTypeConfig; |
| | | // è½¬æ¢æ¯ä¾ä¸ºå°æ° |
| | | data.deductionRatio = data.deductionRatio / 100; |
| | | const deductionRatio = data.deductionRatio! / 100; |
| | | try { |
| | | await updateLeaveTypeConfig(data); |
| | | if (formType.value === 'update') { |
| | | await updateLeaveTypeConfig({ |
| | | id: data.id, |
| | | deductionRatio, |
| | | remark: data.remark, |
| | | status: data.status, |
| | | }); |
| | | } else { |
| | | // 请åç±»ååç§°ç±æéç±»åèªå¨å¸¦åºï¼æ éæå¨å¡«å |
| | | const NAME_MAP: Record<number, string> = { |
| | | 1: 'äºå', |
| | | 2: 'ç
å', |
| | | 3: 'å¹´å', |
| | | 4: 'å©å', |
| | | 5: '产å', |
| | | 6: '丧å', |
| | | }; |
| | | await createLeaveTypeConfig({ |
| | | leaveType: data.leaveType, |
| | | name: NAME_MAP[data.leaveType!] ?? '', |
| | | deductionRatio, |
| | | remark: data.remark, |
| | | status: data.status, |
| | | }); |
| | | } |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | |
| | | if (!isOpen) { |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ id: number; row: HrmLeaveTypeConfigApi.LeaveTypeConfig }>(); |
| | | const data = modalApi.getData<{ formType: FormType; row: HrmLeaveTypeConfigApi.LeaveTypeConfig }>(); |
| | | formType.value = data.formType; |
| | | if (data.formType === 'create' || !data.row) { |
| | | await formApi.resetForm(); |
| | | return; |
| | | } |
| | | // è½¬æ¢æ¯ä¾ä¸ºç¾åæ¯æ¾ç¤º |
| | | const row = { ...data.row, deductionRatio: (data.row.deductionRatio || 0) * 100 }; |
| | | const row = { |
| | | ...data.row, |
| | | deductionRatio: (data.row.deductionRatio || 0) * 100, |
| | | }; |
| | | await formApi.setValues(row); |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="ç¼è¾æ£æ¬¾æ¯ä¾" class="w-[600px]"> |
| | | <Modal :title="getTitle" class="w-[600px]"> |
| | | <Form class="mx-4" /> |
| | | </Modal> |
| | | </template> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { HrmSalaryStructureApi } from '#/api/hrm/salary/structure'; |
| | | |
| | | export type FormType = 'create' | 'update'; |
| | | |
| | | const statusOptions = [ |
| | | { label: 'å¯ç¨', value: 0 }, |
| | | { label: 'ç¦ç¨', value: 1 }, |
| | | ]; |
| | | |
| | | export function useFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | | { |
| | | fieldName: 'name', |
| | | label: 'ç»æåç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | maxlength: 100, |
| | | placeholder: 'ä¾å¦ï¼æ ååå·¥èªé
¬ã管çå±èªé
¬', |
| | | showCount: true, |
| | | }, |
| | | rules: 'required', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'overtimePayRatio', |
| | | label: '工使¥åç', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | addonAfter: 'å', |
| | | max: 10, |
| | | min: 0, |
| | | precision: 2, |
| | | step: 0.1, |
| | | placeholder: '0 表示ä¸è®¡å çå·¥èµ', |
| | | style: { width: '100%' }, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'restDayOvertimePayRatio', |
| | | label: '伿¯æ¥åç', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | addonAfter: 'å', |
| | | max: 10, |
| | | min: 0, |
| | | precision: 2, |
| | | step: 0.1, |
| | | placeholder: '0 表示ä¸è®¡è¯¥ç±»åå çå·¥èµ', |
| | | style: { width: '100%' }, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'legalHolidayOvertimePayRatio', |
| | | label: 'æ³å®è忥åç', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | addonAfter: 'å', |
| | | max: 10, |
| | | min: 0, |
| | | precision: 2, |
| | | step: 0.1, |
| | | placeholder: '0 表示ä¸è®¡è¯¥ç±»åå çå·¥èµ', |
| | | style: { width: '100%' }, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'RadioGroup', |
| | | componentProps: { options: statusOptions }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | maxlength: 500, |
| | | placeholder: 'å¡«åéç¨å²ä½ãè级æç¹æ®è®¡èªè¯´æ', |
| | | rows: 3, |
| | | showCount: true, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'name', |
| | | label: 'ç»æåç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥ç»æåç§°', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: statusOptions, |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | export function useGridColumns(): VxeTableGridOptions<HrmSalaryStructureApi.SalaryStructure>['columns'] { |
| | | return [ |
| | | { field: 'name', title: 'ç»æåç§°', minWidth: 180 }, |
| | | { |
| | | field: 'overtimePayRatio', |
| | | title: '工使¥å çåç', |
| | | width: 145, |
| | | slots: { default: 'overtimePayRatio' }, |
| | | }, |
| | | { |
| | | field: 'restDayOvertimePayRatio', |
| | | title: '伿¯æ¥å çåç', |
| | | width: 145, |
| | | slots: { default: 'restDayOvertimePayRatio' }, |
| | | }, |
| | | { |
| | | field: 'legalHolidayOvertimePayRatio', |
| | | title: 'æ³å®è忥åç', |
| | | width: 165, |
| | | slots: { default: 'legalHolidayOvertimePayRatio' }, |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | width: 90, |
| | | slots: { default: 'status' }, |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | field: 'actions', |
| | | title: 'æä½', |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | width: 150, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { HrmSalaryStructureApi } from '#/api/hrm/salary/structure'; |
| | | |
| | | import { Page, useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | |
| | | import { Alert, message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteSalaryStructure, |
| | | getSalaryStructureList, |
| | | } from '#/api/hrm/salary/structure'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | |
| | | defineOptions({ name: 'HrmSalaryStructure' }); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | function handleEdit(row: HrmSalaryStructureApi.SalaryStructure) { |
| | | formModalApi.setData({ id: row.id, formType: 'update' }).open(); |
| | | } |
| | | |
| | | async function handleDelete(row: HrmSalaryStructureApi.SalaryStructure) { |
| | | await deleteSalaryStructure(row.id!); |
| | | message.success($t('ui.actionMessage.deleteSuccess', ['èªé
Ⱦ'])); |
| | | handleRefresh(); |
| | | } |
| | | |
| | | function formatRatio(value?: number) { |
| | | return value === undefined || value === null ? '-' : `${value.toFixed(2)} å`; |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { schema: useGridFormSchema() }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | pagerConfig: { enabled: false }, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async (_params, formValues) => |
| | | await getSalaryStructureList(formValues), |
| | | }, |
| | | }, |
| | | rowConfig: { keyField: 'id', isHover: true }, |
| | | toolbarConfig: { refresh: true, search: true }, |
| | | } as VxeTableGridOptions<HrmSalaryStructureApi.SalaryStructure>, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <Alert |
| | | class="mb-4" |
| | | description="åºæ¬å·¥èµåå工档æ¡ï¼ç¤¾ä¿åå
¬ç§¯éåºæ°ååå·¥ç¤¾ä¿æ¡£æ¡ãè¿éä»
ç»´æ¤å·¥ä½æ¥ã伿¯æ¥åæ³å®è忥å çåçã" |
| | | message="èªé
¬ç»æç»´æ¤å ç计èªè§å" |
| | | show-icon |
| | | type="info" |
| | | /> |
| | | <Grid table-title="èªé
¬ç»æå表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['èªé
Ⱦ']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['hrm:salary-structure:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #overtimePayRatio="{ row }"> |
| | | <Tag :color="(row.overtimePayRatio ?? 0) > 0 ? 'blue' : 'default'"> |
| | | {{ (row.overtimePayRatio ?? 0) > 0 ? formatRatio(row.overtimePayRatio) : 'ä¸è®¡å çå·¥èµ' }} |
| | | </Tag> |
| | | </template> |
| | | <template #restDayOvertimePayRatio="{ row }"> |
| | | <Tag :color="(row.restDayOvertimePayRatio ?? 0) > 0 ? 'blue' : 'default'"> |
| | | {{ (row.restDayOvertimePayRatio ?? 0) > 0 ? formatRatio(row.restDayOvertimePayRatio) : 'ä¸è®¡å çå·¥èµ' }} |
| | | </Tag> |
| | | </template> |
| | | <template #legalHolidayOvertimePayRatio="{ row }"> |
| | | <Tag :color="(row.legalHolidayOvertimePayRatio ?? 0) > 0 ? 'blue' : 'default'"> |
| | | {{ (row.legalHolidayOvertimePayRatio ?? 0) > 0 ? formatRatio(row.legalHolidayOvertimePayRatio) : 'ä¸è®¡å çå·¥èµ' }} |
| | | </Tag> |
| | | </template> |
| | | <template #status="{ row }"> |
| | | <Tag :color="row.status === 0 ? 'success' : 'default'"> |
| | | {{ row.status === 0 ? 'å¯ç¨' : 'ç¦ç¨' }} |
| | | </Tag> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['hrm:salary-structure:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['hrm:salary-structure:delete'], |
| | | popConfirm: { |
| | | title: '确认å é¤è¯¥èªé
¬ç»æåï¼å·²è¢«å工使ç¨çç»æä¸å¯å é¤ã', |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { HrmSalaryStructureApi } from '#/api/hrm/salary/structure'; |
| | | import type { FormType } from '../data'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | |
| | | import { Alert, message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createSalaryStructure, |
| | | getSalaryStructure, |
| | | updateSalaryStructure, |
| | | } from '#/api/hrm/salary/structure'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formType = ref<FormType>('create'); |
| | | |
| | | const getTitle = computed(() => |
| | | formType.value === 'update' ? 'ä¿®æ¹èªé
¬ç»æ' : 'æ°å¢èªé
Ⱦ', |
| | | ); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | | formItemClass: 'col-span-1', |
| | | labelWidth: 110, |
| | | }, |
| | | wrapperClass: 'grid-cols-2', |
| | | layout: 'horizontal', |
| | | schema: useFormSchema(), |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) return; |
| | | modalApi.lock(); |
| | | try { |
| | | const data = |
| | | (await formApi.getValues()) as HrmSalaryStructureApi.SalaryStructure; |
| | | if (data.id) { |
| | | await updateSalaryStructure(data); |
| | | } else { |
| | | await createSalaryStructure(data); |
| | | } |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) return; |
| | | const data = modalApi.getData<{ formType: FormType; id?: number }>(); |
| | | formType.value = data.formType; |
| | | await formApi.resetForm(); |
| | | if (!data.id) { |
| | | await formApi.setValues({ |
| | | legalHolidayOvertimePayRatio: 3, |
| | | overtimePayRatio: 1.5, |
| | | restDayOvertimePayRatio: 2, |
| | | status: 0, |
| | | }); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | await formApi.setValues(await getSalaryStructure(data.id)); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-[680px]"> |
| | | <Alert |
| | | class="mx-4 mb-4" |
| | | description="ä¸ç§å çç±»ååå«è®¾ç½®åçï¼æä¸é¡¹è®¾ä¸º 0 æ¶ï¼ä»
该类åä¸è®¡ç®å çå·¥èµï¼å®¡æ¹éè¿çå çæ¶é¿ä»æ£å¸¸ç»è®¡ã" |
| | | message="å çå·¥èµè®¡ç®è¯´æ" |
| | | show-icon |
| | | type="info" |
| | | /> |
| | | <Form class="mx-4" /> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** æçç½æ ¼çæ¥æå·¥å
· */ |
| | | |
| | | const WEEK_LABELS = ['æ¥', 'ä¸', 'äº', 'ä¸', 'å', 'äº', 'å
']; |
| | | |
| | | /** è¿åææå¤©æ°æ°ç»ï¼å¦ 2026-08 â [1..31] */ |
| | | export function getDaysOfMonth(yearMonth: string): number[] { |
| | | const [yearStr, monthStr] = yearMonth.split('-'); |
| | | const days = new Date(Number(yearStr), Number(monthStr), 0).getDate(); |
| | | return Array.from({ length: days }, (_, i) => i + 1); |
| | | } |
| | | |
| | | /** è¿åææ¥çææä¸ææ ç¾ï¼æ¥/ä¸/äºâ¦ï¼ */ |
| | | export function getWeekLabel(yearMonth: string, day: number): string { |
| | | const date = new Date(`${yearMonth}-${String(day).padStart(2, '0')}`); |
| | | return WEEK_LABELS[date.getDay()] ?? ''; |
| | | } |
| | | |
| | | /** å¤æææ¥æ¯å¦ä¸ºå¨æ« */ |
| | | export function isWeekend(yearMonth: string, day: number): boolean { |
| | | const date = new Date(`${yearMonth}-${String(day).padStart(2, '0')}`); |
| | | const week = date.getDay(); |
| | | return week === 0 || week === 6; |
| | | } |
| | | |
| | | /** çæææææ¥çæ¥æå符串 yyyy-MM-dd */ |
| | | export function formatDate(yearMonth: string, day: number): string { |
| | | return `${yearMonth}-${String(day).padStart(2, '0')}`; |
| | | } |
| | | |
| | | /** èªå¨æç伿¯æ¨¡å¼ */ |
| | | export const REST_MODE = { |
| | | /** åä¼ï¼å¨å
ã卿¥ä¼æ¯ */ |
| | | DOUBLE: 'double', |
| | | /** åä¼ï¼å¨æ¥ä¼æ¯ */ |
| | | SINGLE: 'single', |
| | | /** ä¸ä¼æ¯ï¼æ¯å¤©æç */ |
| | | NONE: 'none', |
| | | } as const; |
| | | |
| | | export type RestMode = (typeof REST_MODE)[keyof typeof REST_MODE]; |
| | | |
| | | export const REST_MODE_OPTIONS: { label: string; value: RestMode }[] = [ |
| | | { label: 'åä¼ï¼å
æ¥ä¼ï¼', value: REST_MODE.DOUBLE }, |
| | | { label: 'åä¼ï¼å¨æ¥ä¼ï¼', value: REST_MODE.SINGLE }, |
| | | { label: 'ä¸ä¼æ¯', value: REST_MODE.NONE }, |
| | | ]; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { HrmEmployeeApi } from '#/api/hrm/employee'; |
| | | import type { HrmEmployeeScheduleApi } from '#/api/hrm/schedule'; |
| | | import type { HrmShiftApi } from '#/api/hrm/shift'; |
| | | |
| | | import { computed, onMounted, ref, watch } from 'vue'; |
| | | |
| | | import dayjs from 'dayjs'; |
| | | |
| | | import { |
| | | Button, |
| | | Checkbox, |
| | | DatePicker, |
| | | Empty, |
| | | Input, |
| | | message, |
| | | Modal, |
| | | Pagination, |
| | | Radio, |
| | | Segmented, |
| | | Select, |
| | | TreeSelect, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { Page } from '#/packages/effects/common-ui/src'; |
| | | |
| | | import { getEmployeePage } from '#/api/hrm/employee'; |
| | | import { getShiftList } from '#/api/hrm/shift'; |
| | | import { getEmployeeScheduleList, saveEmployeeSchedule } from '#/api/hrm/schedule'; |
| | | import { getDeptList } from '#/api/system/dept'; |
| | | import { handleTree } from '#/packages/utils/src'; |
| | | |
| | | import { |
| | | REST_MODE, |
| | | REST_MODE_OPTIONS, |
| | | formatDate, |
| | | getDaysOfMonth, |
| | | } from './data'; |
| | | import type { RestMode } from './data'; |
| | | |
| | | defineOptions({ name: 'HrmSchedule' }); |
| | | |
| | | // ========== æ¥è¯¢æ¡ä»¶ ========== |
| | | const month = ref<string>(''); |
| | | const deptId = ref<number>(); |
| | | const deptTree = ref<any[]>([]); |
| | | |
| | | /** åå·¥åé¡µï¼æçç½æ ¼æ¯æ¬¡åªæ¾ç¤ºä¸é¡µåå·¥ï¼ */ |
| | | const employeePageNo = ref(1); |
| | | const employeePageSize = ref(20); |
| | | const employeeTotal = ref(0); |
| | | |
| | | /** æåå·¥ç¼å·/å§å/ææºå·æç´¢å个åå·¥ */ |
| | | const keyword = ref(''); |
| | | |
| | | /** ä¿åæ¶å次请æ±çæçæ¡ç®æ°ï¼é²æ¢å¤§æ¹é请æ±è¿å¤§ï¼ */ |
| | | const SAVE_BATCH_SIZE = 1000; |
| | | |
| | | // ========== æ°æ® ========== |
| | | const employees = ref<HrmEmployeeApi.Employee[]>([]); |
| | | const shiftList = ref<HrmShiftApi.Shift[]>([]); |
| | | const loading = ref(false); |
| | | const saving = ref(false); |
| | | /** æ¯å¦å·²æ§è¡è¿æ¥è¯¢ */ |
| | | const loaded = ref(false); |
| | | |
| | | // ========== åå·¥å¾é ========== |
| | | /** Checkbox åæ´äºä»¶ï¼ä»
å checked åæ®µï¼ */ |
| | | interface CheckChangeEvent { |
| | | target: { checked: boolean }; |
| | | } |
| | | |
| | | /** å¾éçåå·¥IDéåï¼ä¸ºç©ºæ¶èªå¨æçä½ç¨äºå
¨é¨åå·¥ */ |
| | | const checkedEmployeeIds = ref<Set<number>>(new Set()); |
| | | |
| | | const isAllChecked = computed( |
| | | () => |
| | | employees.value.length > 0 && |
| | | checkedEmployeeIds.value.size === employees.value.length, |
| | | ); |
| | | |
| | | function isChecked(id: number): boolean { |
| | | return checkedEmployeeIds.value.has(id); |
| | | } |
| | | |
| | | function handleCheck(id: number, e: CheckChangeEvent) { |
| | | const set = new Set(checkedEmployeeIds.value); |
| | | if (e.target.checked) { |
| | | set.add(id); |
| | | } else { |
| | | set.delete(id); |
| | | } |
| | | checkedEmployeeIds.value = set; |
| | | } |
| | | |
| | | function handleCheckAll(e: CheckChangeEvent) { |
| | | if (e.target.checked) { |
| | | checkedEmployeeIds.value = new Set( |
| | | employees.value |
| | | .map((item) => item.id) |
| | | .filter((id): id is number => id != null), |
| | | ); |
| | | } else { |
| | | checkedEmployeeIds.value = new Set(); |
| | | } |
| | | } |
| | | |
| | | // ========== èªå¨æç ========== |
| | | const autoOpen = ref(false); |
| | | const autoShiftId = ref<number>(); |
| | | const autoRestMode = ref<RestMode>(REST_MODE.DOUBLE); |
| | | |
| | | /** èªå¨æççæ¬¡éæ©é项 */ |
| | | const autoShiftOptions = computed(() => |
| | | shiftList.value.map((shift) => ({ |
| | | label: `${shift.name} (${shift.startTime}~${shift.endTime})`, |
| | | value: shift.id, |
| | | })), |
| | | ); |
| | | |
| | | /** æä¼æ¯æ¨¡å¼ä¸ºå½ååå·¥çææ´ææçï¼ä¼æ¯æ¥æ¸
é¤ï¼å·¥ä½æ¥å¡«çæ¬¡ï¼ */ |
| | | function applyAutoSchedule() { |
| | | if (!employees.value.length || !days.value.length) { |
| | | message.warning('请å
æ¥è¯¢åºåå·¥'); |
| | | return; |
| | | } |
| | | if (autoShiftId.value == null) { |
| | | message.warning('è¯·éæ©æççæ¬¡'); |
| | | return; |
| | | } |
| | | // å¾éäºåå·¥ååªå¯¹å¾é人çæï¼æªå¾éåä½ç¨äºå
¨é¨åå·¥ |
| | | const targets = |
| | | checkedEmployeeIds.value.size > 0 |
| | | ? employees.value.filter( |
| | | (item) => |
| | | item.id != null && checkedEmployeeIds.value.has(item.id), |
| | | ) |
| | | : employees.value; |
| | | if (!targets.length) { |
| | | message.warning('请å
å¾éåå·¥'); |
| | | return; |
| | | } |
| | | const map = new Map(scheduleMap.value); |
| | | for (const emp of targets) { |
| | | if (emp.id == null) { |
| | | continue; |
| | | } |
| | | for (const day of days.value) { |
| | | const date = formatDate(month.value, day); |
| | | const week = new Date( |
| | | `${month.value}-${String(day).padStart(2, '0')}`, |
| | | ).getDay(); |
| | | let rest = false; |
| | | if (autoRestMode.value === REST_MODE.DOUBLE) { |
| | | rest = week === 0 || week === 6; |
| | | } else if (autoRestMode.value === REST_MODE.SINGLE) { |
| | | rest = week === 0; |
| | | } |
| | | map.set(`${emp.id}_${date}`, rest ? null : autoShiftId.value); |
| | | } |
| | | } |
| | | scheduleMap.value = map; |
| | | autoOpen.value = false; |
| | | message.success('èªå¨æçå·²çæï¼å¯æå¨å¾®è°åä¿å'); |
| | | } |
| | | |
| | | /** çæ¬¡ id â çæ¬¡ æ å° */ |
| | | const shiftMap = computed(() => { |
| | | const map = new Map<number, HrmShiftApi.Shift>(); |
| | | for (const shift of shiftList.value) { |
| | | if (shift.id != null) { |
| | | map.set(shift.id, shift); |
| | | } |
| | | } |
| | | return map; |
| | | }); |
| | | |
| | | /** |
| | | * æçæ°æ®ãkey = `${employeeId}_${date}`ï¼value = shiftIdï¼null è¡¨ç¤ºä¼æ¯ï¼ |
| | | */ |
| | | const scheduleMap = ref(new Map<string, number | null>()); |
| | | |
| | | /** å½æå¤©æ°æ°ç» */ |
| | | const days = computed(() => (month.value ? getDaysOfMonth(month.value) : [])); |
| | | |
| | | // ========== è§å¾åæ¢ï¼å¨/æï¼========== |
| | | const WEEK_LABELS = ['æ¥', 'ä¸', 'äº', 'ä¸', 'å', 'äº', 'å
']; |
| | | |
| | | interface ViewDay { |
| | | day: number; |
| | | date: string; |
| | | weekLabel: string; |
| | | isWeekend: boolean; |
| | | } |
| | | |
| | | const viewMode = ref<'week' | 'month'>('week'); |
| | | /** æè§å¾æ¯å¦æ¾ç¤ºå¨æ«åï¼é»è®¤éèï¼ååç¼è§£æ¥æ¤ï¼ */ |
| | | const showWeekend = ref(false); |
| | | /** å¨è§å¾éç¹æ¥æ */ |
| | | const viewAnchor = ref<Date>(new Date()); |
| | | |
| | | /** å½åè§å¾è¦å±ç¤ºçæ¥æå表 */ |
| | | const viewDays = computed<ViewDay[]>(() => { |
| | | if (viewMode.value === 'week') { |
| | | // 以å¨ä¸ä¸ºä¸å¨èµ·ç¹ |
| | | const base = dayjs(viewAnchor.value).day(1); |
| | | return Array.from({ length: 7 }, (_, i) => { |
| | | const date = base.add(i, 'day'); |
| | | const week = date.day(); |
| | | return { |
| | | day: date.date(), |
| | | date: date.format('YYYY-MM-DD'), |
| | | weekLabel: WEEK_LABELS[week] ?? '', |
| | | isWeekend: week === 0 || week === 6, |
| | | }; |
| | | }); |
| | | } |
| | | return days.value |
| | | .filter((day) => { |
| | | if (showWeekend.value) { |
| | | return true; |
| | | } |
| | | const week = new Date( |
| | | `${month.value}-${String(day).padStart(2, '0')}`, |
| | | ).getDay(); |
| | | return week !== 0 && week !== 6; |
| | | }) |
| | | .map((day) => { |
| | | const week = new Date( |
| | | `${month.value}-${String(day).padStart(2, '0')}`, |
| | | ).getDay(); |
| | | return { |
| | | day, |
| | | date: formatDate(month.value, day), |
| | | weekLabel: WEEK_LABELS[week] ?? '', |
| | | isWeekend: week === 0 || week === 6, |
| | | }; |
| | | }); |
| | | }); |
| | | |
| | | /** å¨è§å¾å½åæ¾ç¤ºèå´æ ç¾ */ |
| | | const weekRangeLabel = computed(() => { |
| | | const list = viewDays.value; |
| | | const first = list[0]; |
| | | const last = list[list.length - 1]; |
| | | if (!first || !last) { |
| | | return ''; |
| | | } |
| | | return `${first.date} ~ ${last.date}`; |
| | | }); |
| | | |
| | | function shiftWeek(delta: number) { |
| | | viewAnchor.value = dayjs(viewAnchor.value).add(delta, 'week').toDate(); |
| | | } |
| | | |
| | | // æä»½ååæ¶ï¼å¨è§å¾éç¹è½å°è¯¥æ 1 å·æå¨å¨ |
| | | watch(month, (value) => { |
| | | if (value) { |
| | | viewAnchor.value = new Date(`${value}-01`); |
| | | } |
| | | }); |
| | | |
| | | function getShiftByCell(employeeId: number, date: string): HrmShiftApi.Shift | undefined { |
| | | const shiftId = scheduleMap.value.get(`${employeeId}_${date}`); |
| | | if (shiftId == null) { |
| | | return undefined; |
| | | } |
| | | return shiftMap.value.get(shiftId); |
| | | } |
| | | |
| | | // ========== çæ¬¡éæ©å¼¹çª ========== |
| | | const pickerOpen = ref(false); |
| | | const pickerEmployee = ref<HrmEmployeeApi.Employee>(); |
| | | const pickerDate = ref<string>(''); |
| | | |
| | | const pickerTitle = computed(() => { |
| | | if (!pickerEmployee.value || !pickerDate.value) { |
| | | return 'éæ©çæ¬¡'; |
| | | } |
| | | return `éæ©çæ¬¡ · ${pickerEmployee.value.name} · ${pickerDate.value}`; |
| | | }); |
| | | |
| | | function openPicker(emp: HrmEmployeeApi.Employee, date: string) { |
| | | pickerEmployee.value = emp; |
| | | pickerDate.value = date; |
| | | pickerOpen.value = true; |
| | | } |
| | | |
| | | /** å½åå¼¹çªæ ¼åæ¯å¦å·²éè¯¥çæ¬¡ */ |
| | | function isCurrentPick(shiftId: number): boolean { |
| | | if (!pickerEmployee.value || !pickerDate.value) { |
| | | return false; |
| | | } |
| | | return ( |
| | | scheduleMap.value.get(`${pickerEmployee.value.id}_${pickerDate.value}`) === |
| | | shiftId |
| | | ); |
| | | } |
| | | |
| | | function pickShift(shiftId: number | null) { |
| | | if (!pickerEmployee.value || !pickerDate.value) { |
| | | return; |
| | | } |
| | | scheduleMap.value.set(`${pickerEmployee.value.id}_${pickerDate.value}`, shiftId); |
| | | pickerOpen.value = false; |
| | | } |
| | | |
| | | // ========== å è½½ ========== |
| | | async function loadDeptTree() { |
| | | const data = await getDeptList(); |
| | | deptTree.value = handleTree(data); |
| | | } |
| | | |
| | | /** |
| | | * å页æåé¨é¨ä¸çå
¨é¨åå·¥ï¼å页ä¸é 200ï¼å¾ªç¯æ¼å
¨ï¼ï¼ä¾æçç½æ ¼æ´è¡¨å±ç¤º |
| | | */ |
| | | /** å è½½å½å页åå·¥ï¼ç¿»é¡µæ¶åªéæ°æåå·¥ï¼æçæ°æ®ä¿çï¼ */ |
| | | async function loadEmployeePage() { |
| | | loading.value = true; |
| | | try { |
| | | const res = await getEmployeePage({ |
| | | pageNo: employeePageNo.value, |
| | | pageSize: employeePageSize.value, |
| | | deptId: deptId.value, |
| | | keyword: keyword.value || undefined, |
| | | }); |
| | | employees.value = res.list ?? []; |
| | | employeeTotal.value = res.total ?? 0; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | /** å工翻页 */ |
| | | function handlePageChange(page: number, pageSize: number) { |
| | | if (pageSize !== employeePageSize.value) { |
| | | employeePageSize.value = pageSize; |
| | | employeePageNo.value = 1; |
| | | } else { |
| | | employeePageNo.value = page; |
| | | } |
| | | loadEmployeePage(); |
| | | } |
| | | |
| | | /** æå
³é®è¯æç´¢åå·¥ */ |
| | | function handleSearch() { |
| | | employeePageNo.value = 1; |
| | | loadEmployeePage(); |
| | | } |
| | | |
| | | async function loadData() { |
| | | if (!month.value) { |
| | | message.warning('è¯·éæ©æä»½'); |
| | | return; |
| | | } |
| | | if (!deptId.value) { |
| | | message.warning('请å
éæ©é¨é¨'); |
| | | return; |
| | | } |
| | | loading.value = true; |
| | | try { |
| | | const [shifts, schedules] = await Promise.all([ |
| | | getShiftList(), |
| | | getEmployeeScheduleList({ |
| | | yearMonth: month.value, |
| | | deptId: deptId.value, |
| | | }), |
| | | ]); |
| | | shiftList.value = shifts; |
| | | const map = new Map<string, number | null>(); |
| | | for (const item of schedules) { |
| | | if (item.employeeId != null && item.date) { |
| | | map.set(`${item.employeeId}_${item.date}`, item.shiftId ?? null); |
| | | } |
| | | } |
| | | scheduleMap.value = map; |
| | | loaded.value = true; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | // å 载第ä¸é¡µåå·¥ |
| | | employeePageNo.value = 1; |
| | | await loadEmployeePage(); |
| | | } |
| | | |
| | | // ========== ä¿åï¼æ´æå
¨éï¼ ========== |
| | | async function handleSave() { |
| | | if (!month.value || !employees.value.length) { |
| | | return; |
| | | } |
| | | Modal.confirm({ |
| | | title: '确认ä¿å', |
| | | content: `å°ä¿åå½å页 ${employees.value.length} ååå·¥å¨ ${month.value} çå
¨é¨æçï¼ç¡®å®ç»§ç»åï¼`, |
| | | okText: 'ç¡®å®', |
| | | cancelText: 'åæ¶', |
| | | onOk: async () => { |
| | | saving.value = true; |
| | | try { |
| | | const items: HrmEmployeeScheduleApi.ScheduleItem[] = []; |
| | | for (const emp of employees.value) { |
| | | if (emp.id == null) { |
| | | continue; |
| | | } |
| | | for (const day of days.value) { |
| | | const date = formatDate(month.value, day); |
| | | items.push({ |
| | | employeeId: emp.id, |
| | | date, |
| | | shiftId: scheduleMap.value.get(`${emp.id}_${date}`) ?? null, |
| | | }); |
| | | } |
| | | } |
| | | // 大æ¹éæ¶åæ¹æäº¤ï¼é¿å
忬¡è¯·æ±è¿å¤§ |
| | | for (let i = 0; i < items.length; i += SAVE_BATCH_SIZE) { |
| | | await saveEmployeeSchedule(items.slice(i, i + SAVE_BATCH_SIZE)); |
| | | } |
| | | message.success('æçä¿åæå'); |
| | | } finally { |
| | | saving.value = false; |
| | | } |
| | | }, |
| | | }); |
| | | } |
| | | |
| | | onMounted(() => { |
| | | month.value = dayjs().format('YYYY-MM'); |
| | | loadDeptTree(); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <!-- æ¥è¯¢å·¥å
·æ --> |
| | | <div |
| | | class="mb-4 flex flex-wrap items-center gap-3 rounded-lg bg-white p-4" |
| | | > |
| | | <DatePicker |
| | | v-model:value="month" |
| | | picker="month" |
| | | :allow-clear="false" |
| | | value-format="YYYY-MM" |
| | | format="YYYY-MM" |
| | | style="width: 140px" |
| | | /> |
| | | <TreeSelect |
| | | v-model:value="deptId" |
| | | :tree-data="deptTree" |
| | | :field-names="{ label: 'name', value: 'id', children: 'children' }" |
| | | allow-clear |
| | | placeholder="è¯·éæ©é¨é¨" |
| | | tree-default-expand-all |
| | | style="width: 200px" |
| | | /> |
| | | <Input |
| | | v-model:value="keyword" |
| | | allow-clear |
| | | placeholder="åå·¥ç¼å·/å§å/ææºå·" |
| | | style="width: 200px" |
| | | @pressEnter="handleSearch" |
| | | /> |
| | | <Button type="primary" :loading="loading" @click="loadData"> |
| | | æ¥è¯¢æç |
| | | </Button> |
| | | <Button :disabled="!employees.length" @click="autoOpen = true"> |
| | | èªå¨æç |
| | | </Button> |
| | | <Button |
| | | type="primary" |
| | | :loading="saving" |
| | | :disabled="!employees.length" |
| | | @click="handleSave" |
| | | > |
| | | ä¿åæç |
| | | </Button> |
| | | <Segmented |
| | | v-model:value="viewMode" |
| | | :options="[ |
| | | { label: 'å¨è§å¾', value: 'week' }, |
| | | { label: 'æè§å¾', value: 'month' }, |
| | | ]" |
| | | /> |
| | | <template v-if="viewMode === 'week'"> |
| | | <Button @click="shiftWeek(-1)">ä¸ä¸å¨</Button> |
| | | <span class="text-sm text-gray-500">{{ weekRangeLabel }}</span> |
| | | <Button @click="shiftWeek(1)">ä¸ä¸å¨</Button> |
| | | </template> |
| | | <template v-else> |
| | | <Checkbox v-model:checked="showWeekend">æ¾ç¤ºå¨æ«</Checkbox> |
| | | </template> |
| | | </div> |
| | | |
| | | <!-- çæ¬¡å¾ä¾ --> |
| | | <div |
| | | v-if="shiftList.length" |
| | | class="mb-4 flex flex-wrap items-center gap-4 rounded-lg bg-white px-4 py-3" |
| | | > |
| | | <span class="text-sm text-gray-500">çæ¬¡å¾ä¾ï¼</span> |
| | | <span |
| | | v-for="shift in shiftList" |
| | | :key="shift.id" |
| | | class="inline-flex items-center gap-1.5 text-sm" |
| | | > |
| | | <span |
| | | class="inline-block h-3 w-3 rounded" |
| | | :style="{ backgroundColor: shift.color || '#1677ff' }" |
| | | /> |
| | | <span>{{ shift.name }}</span> |
| | | <span class="text-xs text-gray-400" |
| | | >{{ shift.startTime }}~{{ shift.endTime }}</span |
| | | > |
| | | </span> |
| | | </div> |
| | | |
| | | <!-- æçç½æ ¼ --> |
| | | <div class="overflow-auto rounded-lg bg-white"> |
| | | <Empty |
| | | v-if="!employees.length && !loading" |
| | | :description=" |
| | | loaded |
| | | ? '该é¨é¨ææ ç¬¦åæ¡ä»¶çåå·¥ï¼è¯·è°æ´é¨é¨æåå·¥ç¶æ' |
| | | : 'è¯·éæ©æä»½ãé¨é¨åï¼ç¹å»ãæ¥è¯¢æçã' |
| | | " |
| | | class="py-20" |
| | | /> |
| | | <table v-else class="schedule-table w-full text-sm"> |
| | | <thead> |
| | | <tr> |
| | | <th class="schedule-check-col bg-gray-100"> |
| | | <Checkbox :checked="isAllChecked" @change="handleCheckAll" /> |
| | | </th> |
| | | <th class="schedule-name-col bg-gray-100 text-left">åå·¥</th> |
| | | <th |
| | | v-for="vd in viewDays" |
| | | :key="vd.date" |
| | | :class="[ |
| | | 'schedule-date-col text-center', |
| | | vd.isWeekend ? 'bg-red-50' : 'bg-gray-50', |
| | | ]" |
| | | > |
| | | <div class="font-medium">{{ vd.day }}</div> |
| | | <div class="text-xs text-gray-400">{{ vd.weekLabel }}</div> |
| | | </th> |
| | | </tr> |
| | | </thead> |
| | | <tbody> |
| | | <tr v-for="emp in employees" :key="emp.id"> |
| | | <td class="schedule-check-col" @click.stop> |
| | | <Checkbox |
| | | :checked="isChecked(emp.id!)" |
| | | @change="handleCheck(emp.id!, $event)" |
| | | /> |
| | | </td> |
| | | <td class="schedule-name-col"> |
| | | <div class="font-medium">{{ emp.name }}</div> |
| | | <div class="text-xs text-gray-400">{{ emp.employeeNo }}</div> |
| | | </td> |
| | | <td |
| | | v-for="vd in viewDays" |
| | | :key="vd.date" |
| | | class="schedule-cell" |
| | | :class="[vd.isWeekend ? 'bg-red-50/40' : '']" |
| | | @click="openPicker(emp, vd.date)" |
| | | > |
| | | <span |
| | | v-if="getShiftByCell(emp.id!, vd.date)" |
| | | class="shift-chip" |
| | | :title="getShiftByCell(emp.id!, vd.date)!.name" |
| | | :style="{ |
| | | backgroundColor: |
| | | getShiftByCell(emp.id!, vd.date)!.color || '#1677ff', |
| | | }" |
| | | > |
| | | {{ getShiftByCell(emp.id!, vd.date)!.name }} |
| | | </span> |
| | | </td> |
| | | </tr> |
| | | </tbody> |
| | | </table> |
| | | </div> |
| | | |
| | | <!-- åå·¥å页 --> |
| | | <div |
| | | v-if="loaded && employees.length" |
| | | class="mt-4 flex items-center justify-end rounded-lg bg-white p-3" |
| | | > |
| | | <Pagination |
| | | :current="employeePageNo" |
| | | :page-size="employeePageSize" |
| | | :total="employeeTotal" |
| | | show-size-changer |
| | | show-quick-jumper |
| | | :show-total="(total: number) => `å
± ${total} 人`" |
| | | @change="handlePageChange" |
| | | /> |
| | | </div> |
| | | |
| | | <!-- çæ¬¡éæ©å¼¹çª --> |
| | | <Modal |
| | | v-model:open="pickerOpen" |
| | | :title="pickerTitle" |
| | | :footer="null" |
| | | :width="440" |
| | | > |
| | | <div class="grid grid-cols-2 gap-3"> |
| | | <button |
| | | v-for="shift in shiftList" |
| | | :key="shift.id" |
| | | type="button" |
| | | class="flex cursor-pointer flex-col items-center gap-1 rounded-lg border px-2 py-3 transition-colors hover:border-blue-500" |
| | | :class="[ |
| | | isCurrentPick(shift.id!) |
| | | ? 'border-blue-500 bg-blue-50' |
| | | : 'border-gray-200', |
| | | ]" |
| | | @click="pickShift(shift.id!)" |
| | | > |
| | | <span class="inline-flex items-center gap-2 text-sm"> |
| | | <span |
| | | class="inline-block h-3 w-3 rounded" |
| | | :style="{ backgroundColor: shift.color || '#1677ff' }" |
| | | /> |
| | | <span class="font-medium">{{ shift.name }}</span> |
| | | </span> |
| | | <span class="text-xs text-gray-400" |
| | | >{{ shift.startTime }} ~ {{ shift.endTime }}</span |
| | | > |
| | | </button> |
| | | </div> |
| | | <div class="mt-4 flex items-center justify-between"> |
| | | <Button danger @click="pickShift(null)">æ¸
餿çï¼ä¼æ¯ï¼</Button> |
| | | <Button @click="pickerOpen = false">åæ¶</Button> |
| | | </div> |
| | | </Modal> |
| | | |
| | | <!-- èªå¨æçå¼¹çª --> |
| | | <Modal v-model:open="autoOpen" title="èªå¨æç" :footer="null" :width="440"> |
| | | <div class="space-y-4"> |
| | | <div> |
| | | <div class="mb-1.5 text-sm text-gray-500">æççæ¬¡</div> |
| | | <Select |
| | | v-model:value="autoShiftId" |
| | | :options="autoShiftOptions" |
| | | placeholder="è¯·éæ©æççæ¬¡" |
| | | style="width: 100%" |
| | | /> |
| | | </div> |
| | | <div> |
| | | <div class="mb-1.5 text-sm text-gray-500">伿¯æ¨¡å¼</div> |
| | | <Radio.Group |
| | | v-model:value="autoRestMode" |
| | | :options="REST_MODE_OPTIONS" |
| | | option-type="button" |
| | | /> |
| | | </div> |
| | | <div class="text-xs text-gray-400"> |
| | | å°ææé模å¼ä¸º{{ |
| | | checkedEmployeeIds.size > 0 |
| | | ? checkedEmployeeIds.size |
| | | : employees.length |
| | | }} |
| | | ååå·¥çææ´ææç{{ |
| | | checkedEmployeeIds.size > 0 |
| | | ? 'ï¼ä»
å¾éçåå·¥ï¼' |
| | | : 'ï¼æªå¾éæ¶ä¸ºå½å页å
¨é¨åå·¥ï¼' |
| | | }}ï¼çæåå¯åæå¨è°æ´ã |
| | | </div> |
| | | <div class="flex justify-end gap-2"> |
| | | <Button @click="autoOpen = false">åæ¶</Button> |
| | | <Button type="primary" @click="applyAutoSchedule">çææç</Button> |
| | | </div> |
| | | </div> |
| | | </Modal> |
| | | </Page> |
| | | </template> |
| | | |
| | | <style lang="scss" scoped> |
| | | .schedule-table { |
| | | width: 100%; |
| | | min-width: max-content; |
| | | border-collapse: separate; |
| | | border-spacing: 0; |
| | | |
| | | th, |
| | | td { |
| | | height: 44px; |
| | | border: 1px solid #f0f0f0; |
| | | } |
| | | |
| | | .schedule-date-col { |
| | | width: 52px; |
| | | min-width: 52px; |
| | | } |
| | | |
| | | .schedule-check-col, |
| | | .schedule-name-col { |
| | | position: sticky; |
| | | z-index: 10; |
| | | background-color: #fafafa; |
| | | } |
| | | |
| | | .schedule-check-col { |
| | | left: 0; |
| | | width: 40px; |
| | | min-width: 40px; |
| | | padding: 0 8px; |
| | | text-align: center; |
| | | } |
| | | |
| | | .schedule-name-col { |
| | | left: 40px; |
| | | min-width: 180px; |
| | | padding: 0 12px; |
| | | line-height: 1.3; |
| | | } |
| | | |
| | | thead .schedule-check-col, |
| | | thead .schedule-name-col { |
| | | z-index: 20; |
| | | } |
| | | |
| | | .schedule-cell { |
| | | padding: 2px; |
| | | cursor: pointer; |
| | | text-align: center; |
| | | |
| | | &:hover { |
| | | background-color: #e6f4ff; |
| | | } |
| | | } |
| | | |
| | | .shift-chip { |
| | | display: inline-block; |
| | | max-width: 100%; |
| | | padding: 1px 6px; |
| | | overflow: hidden; |
| | | color: #fff; |
| | | font-size: 11px; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | border-radius: 3px; |
| | | } |
| | | } |
| | | </style> |
| src/views/hrm/shift/data.ts
src/views/hrm/shift/index.vue
src/views/hrm/shift/modules/form.vue
src/views/hrm/transfer/detail/approve/index.vue
src/views/im/home/index.vue
src/views/mes/dv/machinery/type/components/tree.vue
src/views/mes/dv/machinery/type/index.vue
src/views/mes/dv/metering/components/index.ts
src/views/mes/dv/metering/components/select-dialog.vue
src/views/mes/dv/metering/components/select.vue
src/views/mes/dv/metering/data.ts
src/views/mes/dv/metering/index.vue
src/views/mes/dv/metering/modules/form.vue
src/views/mes/dv/metering/modules/import-form.vue
src/views/mes/dv/meteringcheck/data.ts
src/views/mes/dv/meteringcheck/index.vue
src/views/mes/dv/meteringcheck/modules/form.vue
src/views/mes/md/item/components/product-bom-select-dialog.vue
src/views/mes/md/item/components/product-bom-select.vue
src/views/mes/md/item/components/select-dialog.vue
src/views/mes/md/item/data.ts
src/views/mes/md/item/index.vue
src/views/mes/md/item/modules/form.vue
src/views/mes/md/item/modules/product-media-list.vue
src/views/mes/md/item/type/data.ts
src/views/mes/md/item/type/index.vue
src/views/mes/md/item/type/modules/import-form.vue
src/views/mes/md/workstation/index.vue
src/views/mes/md/workstation/modules/form.vue
src/views/mes/md/workstation/workshop/modules/form.vue
src/views/mes/pd/archive/modules/detail.vue
src/views/mes/pd/project/detail/index.vue
src/views/mes/pd/project/index.vue
src/views/mes/pd/project/modules/document-audit-form.vue
src/views/mes/pd/project/modules/process-select-modal.vue
src/views/mes/pd/project/modules/workflow-timeline.vue
src/views/mes/pro/feedback/data.ts
src/views/mes/pro/feedback/detail/index.vue
src/views/mes/pro/feedback/index.vue
src/views/mes/pro/feedback/modules/batch-feedback.vue
src/views/mes/pro/feedback/modules/form.vue
src/views/mes/pro/feedback/param-state.ts
src/views/mes/pro/mps/index.vue
src/views/mes/pro/route/data.ts
src/views/mes/pro/route/modules/process-form.vue
src/views/mes/pro/task/data.ts
src/views/mes/pro/task/edit/index.vue
src/views/mes/pro/task/index.vue
src/views/mes/pro/task/modules/schedule-form.vue
src/views/mes/pro/task/modules/task-form.vue
src/views/mes/pro/task/modules/task-list.vue
src/views/mes/pro/workorder-center/modules/quick-feedback.vue
src/views/mes/pro/workorder/components/select-dialog.vue
src/views/mes/pro/workorder/components/select.vue
src/views/mes/pro/workorder/data.ts
src/views/mes/pro/workorder/index.vue
src/views/mes/pro/workorder/modules/form.vue
src/views/mes/pro/workorder/modules/process-form.vue
src/views/mes/pro/workorder/modules/process-list.vue
src/views/mes/process-design/parameter/index.vue
src/views/mes/process-design/parameter/modules/process-link-dialog.vue
src/views/mes/process-design/process/index.vue
src/views/mes/process-design/process/modules/param-manage.vue
src/views/mes/process-design/route/data.ts
src/views/mes/process-design/route/modules/form.vue
src/views/mes/process-design/route/modules/process-form.vue
src/views/mes/process-design/route/modules/product-form.vue
src/views/mes/process-design/route/modules/workbench.vue
src/views/mes/qc/batchtrace/data.ts
src/views/mes/qc/batchtrace/index.vue
src/views/mes/qc/batchtrace/modules/process-trace-list.vue
src/views/mes/qc/batchtrace/modules/purchase-source-list.vue
src/views/mes/qc/batchtrace/modules/sales-target-list.vue
src/views/mes/qc/batchtrace/modules/trace-detail.vue
src/views/mes/qc/components/export-range-modal.vue
src/views/mes/qc/ipqc/data.ts
src/views/mes/qc/ipqc/index.vue
src/views/mes/qc/iqc/data.ts
src/views/mes/qc/iqc/index.vue
src/views/mes/qc/oqc/data.ts
src/views/mes/qc/oqc/index.vue
src/views/mes/qc/rqc/data.ts
src/views/mes/qc/rqc/index.vue
src/views/mes/qc/template/data.ts
src/views/mes/workbench/components/TaskListPanel.vue
src/views/mes/workbench/components/tabs/FeedbackHistoryTab.vue
src/views/mes/workbench/components/tabs/FeedbackTab.vue
src/views/mes/workbench/components/tabs/ProductIssueLineFormModal.vue
src/views/mes/workbench/components/tabs/TaskDetailTab.vue
src/views/mes/workbench/index.vue
src/views/mes/workbench/types.ts
src/views/srm/enums.ts
src/views/srm/statistics/index.vue
src/views/srm/supplier/data.ts
src/views/srm/supplier/index.vue
src/views/srm/supplier/modules/import-form.vue
src/views/srm/supplierApply/data.ts
src/views/srm/supplierApply/detail/index.vue
src/views/srm/supplierApply/index.vue
src/views/srm/supplierApply/modules/form.vue
src/views/srm/supplierApply/modules/process-select-modal.vue
src/views/srm/supplierCertificate/data.ts
src/views/srm/supplierCertificate/index.vue
src/views/srm/supplierCertificate/modules/form.vue
src/views/srm/tender/data.ts
src/views/srm/tender/detail.vue
src/views/srm/tender/modules/form.vue
src/views/system/dept/data.ts
src/views/system/dept/index.vue
src/views/system/dept/modules/import-form.vue
src/views/system/post/data.ts
src/views/system/post/index.vue
src/views/system/post/modules/import-form.vue
src/views/system/user/data.ts
src/views/wls/arrivalnotice/index.vue
src/views/wls/arrivalnotice/modules/po-select-dialog.vue
src/views/wls/barcode/config/index.vue
src/views/wls/barcode/data.ts
src/views/wls/itemreceipt/modules/an-select-dialog.vue
src/views/wls/materialstock/components/import-form.vue
src/views/wls/materialstock/components/index.ts
src/views/wls/materialstock/components/stock-adjust-modal.vue
src/views/wls/materialstock/index.vue
src/views/wls/productissue/components/index.ts
src/views/wls/productissue/components/select-dialog.vue
src/views/wls/productissue/components/select.vue
src/views/wls/productissue/data.ts
src/views/wls/productreceipt/data.ts
src/views/wls/returnissue/data.ts
src/views/wls/returnissue/modules/form.vue
src/views/wls/returnissue/modules/line-form.vue
src/views/wls/returnissue/modules/line-list.vue
src/views/wls/returnvendor/data.ts
src/views/wls/returnvendor/modules/detail-form.vue
src/views/wls/returnvendor/modules/form.vue
src/views/wls/stocktaking/plan/data.ts
src/views/wls/stocktaking/plan/modules/param-form.vue
src/views/wls/warehouse/area/data.ts
src/views/wls/warehouse/data.ts
src/views/wls/warehouse/location/data.ts
vite.config.ts |