Merge branch 'dev_pro2.0' into dev_南通_苏珩
# Conflicts:
# src/layouts/auth-scene.vue
# src/layouts/auth.vue
# src/main.ts
# src/preferences.ts
# src/styles/herbal-theme.css
# src/views/_core/authentication/login.vue
| | |
| | | # ææ¡£å°åçå¼å
³ |
| | | VITE_APP_DOCALERT_ENABLE=true |
| | | |
| | | # AI åè½æ»å¼å
³ï¼false æ¶éèææ AI å
¥å£ï¼AI 模åèåãOCRã颿µãæºè½å¤å®ãæºè½ä½å¯¼å
¥ï¼ |
| | | VITE_APP_AI_ENABLE=false |
| | | |
| | | # ç¾åº¦ç»è®¡ |
| | | VITE_APP_BAIDU_CODE = e98f2eab6ceb8688bc6d8fc5332ff093 |
| | | |
| | |
| | | # VITE_APP_API_ENCRYPT_RESPONSE_KEY = MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAOH8IfIFxL/MR9XIg1UDv5z1fGXQI93E8wrU4iPFovL/sEt9uSgSkjyidC2O7N+m7EKtoN6b1u7cEwXSkwf3kfK0jdWLSQaNpX5YshqXCBzbDfugDaxuyYrNA4/tIMs7mzZAk0APuRXB35Dmupou7Yw7TFW/7QhQmGfzeEKULQvnAgMBAAECgYAw8LqlQGyQoPv5p3gRxEMOCfgL0JzD3XBJKztiRd35RDh40Nx1ejgjW4dPioFwGiVWd2W8cAGHLzALdcQT2KDJh+T/tsd4SPmI6uSBBK6Ff2DkO+kFFcuYvfclQQKqxma5CaZOSqhgenacmgTMFeg2eKlY3symV6JlFNu/IKU42QJBAOhxAK/Eq3e61aYQV2JSguhMR3b8NXJJRroRs/QHEanksJtl+M+2qhkC9nQVXBmBkndnkU/l2tYcHfSBlAyFySMCQQD445tgm/J2b6qMQmuUGQAYDN8FIkHjeKmha+l/fv0igWm8NDlBAem91lNDIPBUzHL1X1+pcts5bjmq99YdOnhtAkAg2J8dN3B3idpZDiQbC8fd5bGPmdI/pSUudAP27uzLEjr2qrE/QPPGdwm2m7IZFJtK7kK1hKio6u48t/bg0iL7AkEAuUUs94h+v702Fnym+jJ2CHEkXvz2US8UDs52nWrZYiM1o1y4tfSHm8H8bv8JCAa9GHyriEawfBraILOmllFdLQJAQSRZy4wmlaG48MhVXodB85X+VZ9krGXZ2TLhz7kz9iuToy53l9jTkESt6L5BfBDCVdIwcXLYgK+8KFdHN5W7HQ== |
| | | |
| | | # ç¾åº¦å°å¾ |
| | | VITE_BAIDU_MAP_KEY=Y2aJXiswwPxy6mwFs1z9c7U5gwX9WfUN |
| | | VITE_BAIDU_MAP_KEY=5YhFUgsoi8KMm6tafbSljgz9NNpOYP1Z |
| | |
| | | VITE_UPLOAD_TYPE=server |
| | | # æ¯å¦æå¼ devtoolsï¼true 为æå¼ï¼false 为å
³é |
| | | VITE_DEVTOOLS=false |
| | | # AI åè½æ»å¼å
³ï¼false æ¶éèææ AI å
¥å£ï¼AI 模åèåãOCRã颿µãæºè½å¤å®ãæºè½ä½å¯¼å
¥ï¼ |
| | | VITE_APP_AI_ENABLE=true |
| | | |
| | | |
| | | # æ¯å¦æ³¨å
¥å
¨å±loading |
| | | VITE_INJECT_APP_LOADING=true |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | @echo off |
| | | setlocal EnableExtensions EnableDelayedExpansion |
| | | |
| | | REM ============================================================ |
| | | REM Vue Frontend Deploy Script |
| | | REM ============================================================ |
| | | |
| | | REM ========================= |
| | | REM Configuration |
| | | REM ========================= |
| | | |
| | | set "COMPANY=HSC" |
| | | |
| | | set "REMOTE_USER=root" |
| | | set "REMOTE_HOST=36.213.150.237" |
| | | set "REMOTE_PORT=65022" |
| | | |
| | | set "PASSWORD=Sh@1008600" |
| | | |
| | | set "ZIP_FILE=dist.zip" |
| | | set "REMOTE_DIR=/data/vue/mom/hsc/dist" |
| | | |
| | | set "SSH_OPTS=-o StrictHostKeyChecking=no -o ConnectTimeout=10 -o ServerAliveInterval=15 -o ServerAliveCountMax=3" |
| | | |
| | | set "RETRY_COUNT=3" |
| | | |
| | | set "SSHPASS_DIR=%~dp0bin" |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Start |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo ============================================================ |
| | | echo Vue Dist Auto Deploy |
| | | echo ============================================================ |
| | | echo. |
| | | echo Company : %COMPANY% |
| | | echo Server : %REMOTE_USER%@%REMOTE_HOST% |
| | | echo Port : %REMOTE_PORT% |
| | | echo Remote Dir : %REMOTE_DIR% |
| | | echo Package : %ZIP_FILE% |
| | | echo. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check sshpass |
| | | REM ============================================================ |
| | | |
| | | echo [1/7] Checking sshpass... |
| | | |
| | | if exist "%SSHPASS_DIR%\sshpass.exe" ( |
| | | set "PATH=%SSHPASS_DIR%;%PATH%" |
| | | ) |
| | | |
| | | where sshpass >nul 2>&1 |
| | | |
| | | if not errorlevel 1 ( |
| | | echo [OK] sshpass found. |
| | | goto CHECK_PNPM |
| | | ) |
| | | |
| | | echo [WARN] sshpass not found. |
| | | |
| | | if not exist "%SSHPASS_DIR%" ( |
| | | mkdir "%SSHPASS_DIR%" >nul 2>&1 |
| | | ) |
| | | |
| | | if exist "%SSHPASS_DIR%\sshpass.exe" ( |
| | | set "PATH=%SSHPASS_DIR%;%PATH%" |
| | | echo [OK] sshpass found. |
| | | goto CHECK_PNPM |
| | | ) |
| | | |
| | | echo [INFO] Downloading sshpass... |
| | | |
| | | powershell -NoProfile -ExecutionPolicy Bypass -Command "$url='https://github.com/xhcoding/sshpass-win32/releases/download/v1.0.3/sshpass.exe'; $out='%SSHPASS_DIR%\sshpass.exe'; Invoke-WebRequest -Uri $url -OutFile $out" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Failed to download sshpass. |
| | | echo [ERROR] Please install sshpass manually. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | set "PATH=%SSHPASS_DIR%;%PATH%" |
| | | |
| | | echo [OK] sshpass installed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check pnpm |
| | | REM ============================================================ |
| | | |
| | | :CHECK_PNPM |
| | | |
| | | echo. |
| | | echo [2/7] Checking pnpm... |
| | | |
| | | where pnpm >nul 2>&1 |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] pnpm not found. |
| | | echo [HINT] npm install -g pnpm |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | call pnpm --version |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] pnpm execution failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check node |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [3/7] Checking Node.js... |
| | | |
| | | where node >nul 2>&1 |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Node.js not found. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | call node --version |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Node.js execution failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check node_modules |
| | | REM ============================================================ |
| | | |
| | | if not exist "node_modules" ( |
| | | echo. |
| | | echo [INFO] node_modules not found. |
| | | echo [INFO] Running pnpm install... |
| | | |
| | | call pnpm install |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] pnpm install failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] pnpm install completed. |
| | | ) else ( |
| | | echo [OK] node_modules exists. |
| | | ) |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Build |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [4/7] Building project... |
| | | |
| | | if exist "%ZIP_FILE%" ( |
| | | echo. |
| | | echo [WARN] %ZIP_FILE% already exists. |
| | | set "REBUILD=" |
| | | set /p "REBUILD=Rebuild? Y/N (default Y): " |
| | | |
| | | if /I "!REBUILD!"=="N" ( |
| | | echo [INFO] Skip build. |
| | | goto CHECK_ZIP |
| | | ) |
| | | ) |
| | | |
| | | echo [INFO] Company: %COMPANY% |
| | | echo [INFO] Command: pnpm run build |
| | | echo. |
| | | |
| | | set "VITE_COMPANY=%COMPANY%" |
| | | |
| | | call pnpm run build |
| | | |
| | | if errorlevel 1 ( |
| | | echo. |
| | | echo [ERROR] Build failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo. |
| | | echo [OK] Build completed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check ZIP |
| | | REM ============================================================ |
| | | |
| | | :CHECK_ZIP |
| | | |
| | | echo. |
| | | echo [5/7] Checking package... |
| | | |
| | | if not exist "%ZIP_FILE%" ( |
| | | echo [ERROR] %ZIP_FILE% not found. |
| | | echo [HINT] Please check whether your build generates dist.zip. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | for %%F in ("%ZIP_FILE%") do set "ZIP_BYTES=%%~zF" |
| | | |
| | | set /a ZIP_MB=ZIP_BYTES/1024/1024 |
| | | |
| | | echo [OK] Package found. |
| | | echo [INFO] File size: !ZIP_BYTES! bytes |
| | | echo [INFO] File size: !ZIP_MB! MB |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Set sshpass password |
| | | REM ============================================================ |
| | | |
| | | set "SSHPASS=%PASSWORD%" |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Test SSH |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [6/7] Testing SSH connection... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "echo SSH_OK" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] SSH connection failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] SSH connection successful. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Create remote directory |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Creating remote directory... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mkdir -p '%REMOTE_DIR%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Failed to create remote directory. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] Remote directory ready. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Upload temporary file |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Uploading package... |
| | | |
| | | set "REMOTE_TMP=%REMOTE_DIR%/.dist.zip.uploading" |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_TMP%'" |
| | | |
| | | sshpass -e scp -P %REMOTE_PORT% %SSH_OPTS% "%ZIP_FILE%" %REMOTE_USER%@%REMOTE_HOST%:"%REMOTE_TMP%" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Upload failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] Upload completed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check remote file |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Checking remote package... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "test -f '%REMOTE_TMP%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Remote package does not exist. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] Remote package exists. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Compare file size |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Checking package size... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "stat -c %%s '%REMOTE_TMP%'" |
| | | |
| | | echo. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Rename temporary file |
| | | REM ============================================================ |
| | | |
| | | echo [INFO] Activating package... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mv -f '%REMOTE_TMP%' '%REMOTE_DIR%/%ZIP_FILE%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Failed to activate package. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] Package activated. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check unzip |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Checking unzip... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "command -v unzip" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] unzip is not installed on server. |
| | | echo. |
| | | echo CentOS/RHEL: |
| | | echo yum install -y unzip |
| | | echo. |
| | | echo Ubuntu/Debian: |
| | | echo apt install -y unzip |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] unzip found. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Test ZIP |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Testing remote ZIP... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -t '%ZIP_FILE%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo. |
| | | echo [ERROR] ZIP test failed. |
| | | echo [INFO] Remote ZIP has been kept. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] ZIP test passed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Extract |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Extracting package... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -o '%ZIP_FILE%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo. |
| | | echo [ERROR] Remote unzip failed. |
| | | echo [INFO] Remote ZIP has been kept. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo. |
| | | echo [OK] Extract completed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Remove ZIP |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Removing remote ZIP... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_DIR%/%ZIP_FILE%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [WARN] Failed to remove remote ZIP. |
| | | ) else ( |
| | | echo [OK] Remote ZIP removed. |
| | | ) |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Success |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo ============================================================ |
| | | echo DEPLOY SUCCESS |
| | | echo ============================================================ |
| | | echo. |
| | | echo Company : %COMPANY% |
| | | echo Server : %REMOTE_HOST%:%REMOTE_PORT% |
| | | echo Remote Dir : %REMOTE_DIR% |
| | | echo Package : %ZIP_FILE% |
| | | echo Size : !ZIP_MB! MB |
| | | echo. |
| | | echo Deployment completed successfully. |
| | | echo. |
| | | |
| | | goto END |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Error |
| | | REM ============================================================ |
| | | |
| | | :ERROR_EXIT |
| | | |
| | | echo. |
| | | echo ============================================================ |
| | | echo DEPLOY FAILED |
| | | echo ============================================================ |
| | | echo. |
| | | echo Please check the error message above. |
| | | echo. |
| | | |
| | | goto END |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM End |
| | | REM ============================================================ |
| | | |
| | | :END |
| | | |
| | | pause |
| | | endlocal |
| | |
| | | name: 'åº·ææºè½', |
| | | logo: '/logos/YZFXLogo.png', |
| | | favicon: '/logos/YZFXLogo.ico', |
| | | apiUrl: 'http://36.213.150.237:9004/admin-api', |
| | | apiUrl: 'http://36.213.150.237:9006/admin-api', |
| | | }, |
| | | CHDL: { |
| | | name: 'é¿è¾çµå', |
| | | logo: '/logos/YZFXLogo.png', |
| | | favicon: '/logos/YZFXLogo.ico', |
| | | apiUrl: 'http://36.213.150.237:9006/admin-api', |
| | | apiUrl: 'http://36.213.150.237:9004/admin-api', |
| | | }, |
| | | HM: { |
| | | name: 'æ ç§', |
| | |
| | | favicon: '/logos/YZFXLogo.ico', |
| | | apiUrl: 'http://36.213.150.237:9015/admin-api', |
| | | }, |
| | | HSC: { |
| | | name: '好水å·é£åæéå
¬å¸', |
| | | logo: '/logos/YZFXLogo.png', |
| | | favicon: '/logos/YZFXLogo.ico', |
| | | apiUrl: 'http://36.213.150.237:9540/admin-api', |
| | | }, |
| | | }; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { InternalAxiosRequestConfig } from '@vben/request'; |
| | | |
| | | import { |
| | | defaultResponseInterceptor, |
| | | RequestClient, |
| | | } from '@vben/request'; |
| | | import { useAccessStore } from '@vben/stores'; |
| | | |
| | | import { |
| | | bindImportAttachment, |
| | | createImportSession, |
| | | } from '#/api/mdm/item/agent'; |
| | | import { uploadFile } from '#/api/system/storage'; |
| | | |
| | | /** |
| | | * æ¨¡åæ°æ®è¡¥å½ä¼è¯æå¡ï¼Pythonï¼ä¸å¨ /admin-api ä¸ï¼ |
| | | * å¼åæç± Vite 代ç /agent-api 转åå° 127.0.0.1:8091ï¼ä¸ä¸è¿å CommonResult å
è£
ã |
| | | */ |
| | | const agentClient = new RequestClient({ |
| | | baseURL: '/agent-api', |
| | | responseReturn: 'body', |
| | | timeout: 120_000, |
| | | }); |
| | | |
| | | agentClient.addRequestInterceptor({ |
| | | fulfilled: (config: InternalAxiosRequestConfig) => { |
| | | const token = useAccessStore().accessToken; |
| | | if (token) { |
| | | config.headers.Authorization = `Bearer ${token}`; |
| | | } |
| | | return config; |
| | | }, |
| | | }); |
| | | |
| | | // Python æå¡ç´æ¥è¿åä¸å¡ JSONï¼ä¸å¥ CommonResultï¼responseReturn: 'body' æ¶åæ ·è¿åååºä½ã |
| | | agentClient.addResponseInterceptor( |
| | | defaultResponseInterceptor({ |
| | | codeField: 'code', |
| | | dataField: 'data', |
| | | successCode: 0, |
| | | }), |
| | | ); |
| | | |
| | | export namespace AgentConversationApi { |
| | | export interface FieldSpec { |
| | | name: string; |
| | | label: string; |
| | | required: boolean; |
| | | kind: string; |
| | | } |
| | | |
| | | export interface ModuleSpec { |
| | | module_id: string; |
| | | title: string; |
| | | capability: string; |
| | | bridge: string; |
| | | permission: string; |
| | | description: string; |
| | | required_fields: string[]; |
| | | fields: FieldSpec[]; |
| | | } |
| | | |
| | | export interface Capabilities { |
| | | version: string; |
| | | formats: string[]; |
| | | ocr: boolean; |
| | | cross_file_dedupe: boolean; |
| | | import_modes: string[]; |
| | | notes: string[]; |
| | | modules: ModuleSpec[]; |
| | | durable_tasks?: { |
| | | enabled: boolean; |
| | | events: string; |
| | | fact_source: string; |
| | | transport: string; |
| | | }; |
| | | } |
| | | |
| | | export interface AuthorizedCapability { |
| | | tool: string; |
| | | module: string; |
| | | operation: string; |
| | | allowed_fields: string[]; |
| | | scope_summary: string; |
| | | can_execute: boolean; |
| | | } |
| | | |
| | | export interface AuthorizedCapabilityCatalog { |
| | | policy_revision: string; |
| | | authorization_scope: string; |
| | | scope_summary: string; |
| | | capabilities: AuthorizedCapability[]; |
| | | } |
| | | |
| | | export interface BusinessModelInfo { |
| | | id: number; |
| | | name: string; |
| | | model: string; |
| | | platform: string; |
| | | temperature?: number | null; |
| | | max_tokens?: number | null; |
| | | max_contexts?: number | null; |
| | | } |
| | | |
| | | export interface Issue { |
| | | code?: string; |
| | | field?: string; |
| | | message: string; |
| | | candidate_id?: string; |
| | | } |
| | | |
| | | export interface Summary { |
| | | total?: number; |
| | | create?: number; |
| | | skip?: number; |
| | | blocked?: number; |
| | | success?: number; |
| | | failed?: number; |
| | | skipped?: number; |
| | | unknown?: number; |
| | | } |
| | | |
| | | export interface ReceiptRow { |
| | | candidate_id: string; |
| | | row_no?: number; |
| | | code?: string; |
| | | name?: string; |
| | | action: string; |
| | | status: string; |
| | | target_item_id?: number; |
| | | error_code?: string; |
| | | error_message?: string; |
| | | } |
| | | |
| | | export interface Receipt { |
| | | command_id: string; |
| | | idempotency_key: string; |
| | | status: string; |
| | | attachment_id: number; |
| | | source_sha256: string; |
| | | preflight_hash: string; |
| | | summary: Summary; |
| | | rows: ReceiptRow[]; |
| | | create_time?: string; |
| | | completed_time?: string; |
| | | error_code?: string; |
| | | error_message?: string; |
| | | } |
| | | |
| | | export interface ModuleCandidate { |
| | | module_id: string; |
| | | title: string; |
| | | capability: string; |
| | | description: string; |
| | | } |
| | | |
| | | export interface MissingField { |
| | | name: string; |
| | | label: string; |
| | | } |
| | | |
| | | /** å¡çåæ®µæ kind å¤ç¨ï¼æªä½¿ç¨çåæ®µä¸ºç©ºã */ |
| | | export interface Card { |
| | | kind: string; |
| | | title?: string; |
| | | body?: string; |
| | | module_id?: string; |
| | | module_title?: string; |
| | | capability?: string; |
| | | revision?: number; |
| | | issues?: Issue[]; |
| | | warnings?: Issue[]; |
| | | repair_guidance?: Array<{ field?: string; guidance: string }>; |
| | | summary?: Summary; |
| | | can_commit?: boolean; |
| | | side_effects?: string[]; |
| | | sync_mes?: boolean | null; |
| | | total?: number; |
| | | actions?: string[]; |
| | | notice?: string; |
| | | candidates?: ModuleCandidate[]; |
| | | missing_fields?: MissingField[]; |
| | | columns?: string[]; |
| | | command_id?: string; |
| | | status?: string; |
| | | rows?: ReceiptRow[]; |
| | | } |
| | | |
| | | export interface Message { |
| | | message_id: string; |
| | | role: string; |
| | | type: string; |
| | | text: string; |
| | | cards?: Card[]; |
| | | } |
| | | |
| | | export interface Interrupt { |
| | | interrupt_id: string; |
| | | kind: string; |
| | | revision: number; |
| | | validation_revision?: number | null; |
| | | card: Card; |
| | | } |
| | | |
| | | export interface RowView { |
| | | candidate_id: string; |
| | | values: Record<string, string>; |
| | | issues: unknown[]; |
| | | warnings: unknown[]; |
| | | } |
| | | |
| | | export interface ConversationAttachment { |
| | | attachment_id: string; |
| | | /** ä»
ææ°éä»¶è§£æåºæä»¶åï¼åå²éä»¶å¯è½ä¸ºç©ºã */ |
| | | filename: string; |
| | | } |
| | | |
| | | export interface Conversation { |
| | | conversation_id: string; |
| | | title: string; |
| | | status: string; |
| | | module_id: string; |
| | | module_title: string; |
| | | capability: string; |
| | | data_scope_summary: string; |
| | | model_id?: number | null; |
| | | policy_revision: string; |
| | | revision: number; |
| | | validation_revision?: number | null; |
| | | sync_mes?: boolean; |
| | | total: number; |
| | | summary: Summary; |
| | | issues: Issue[]; |
| | | warnings: Issue[]; |
| | | missing_fields: string[]; |
| | | columns: string[]; |
| | | /** åéè¡å®é
æå¼çåæ®µï¼ç¨äºæ¸²ææç»è¡¨ã */ |
| | | row_fields: Array<{ name: string; label: string }>; |
| | | attachments: ConversationAttachment[]; |
| | | messages: Message[]; |
| | | interrupts: Interrupt[]; |
| | | /** æªæäº¤æ¶å端è¿å空对象 {}ï¼æäº¤åææå®æ´åæ§ã */ |
| | | receipt?: null | Partial<Receipt>; |
| | | last_error: string; |
| | | rows: RowView[]; |
| | | created_at: string; |
| | | updated_at: string; |
| | | finished: boolean; |
| | | commit_enabled: boolean; |
| | | mes_sync_default: boolean; |
| | | } |
| | | |
| | | export interface ConversationSummary { |
| | | conversation_id: string; |
| | | title: string; |
| | | status: string; |
| | | module_id: string; |
| | | module_title: string; |
| | | revision: number; |
| | | created_at: string; |
| | | updated_at: string; |
| | | } |
| | | |
| | | export interface PageResult { |
| | | items: ConversationSummary[]; |
| | | total: number; |
| | | page: number; |
| | | page_size: number; |
| | | } |
| | | |
| | | export interface ConversationEvent { |
| | | conversation_id: string; |
| | | payload: Record<string, unknown>; |
| | | seq: number; |
| | | type: |
| | | | 'card' |
| | | | 'error' |
| | | | 'message' |
| | | | 'progress' |
| | | | 'receipt' |
| | | | 'resync' |
| | | | 'status'; |
| | | } |
| | | |
| | | export interface CreateReq { |
| | | title?: string; |
| | | text?: string; |
| | | attachment_id?: string; |
| | | module_id?: string; |
| | | model_id?: number; |
| | | constraints?: ConversationConstraints; |
| | | } |
| | | |
| | | export interface ConversationConstraints { |
| | | scenario_paths: string[]; |
| | | time_start?: string; |
| | | time_end?: string; |
| | | record_limit?: number; |
| | | industry?: string; |
| | | skip_rest_days?: boolean; |
| | | rest_day_description?: string; |
| | | region?: string; |
| | | instruction?: string; |
| | | priority_fields: string[]; |
| | | } |
| | | |
| | | export interface ActionReq { |
| | | interrupt_id: string; |
| | | revision: number; |
| | | action: string; |
| | | payload: Record<string, unknown>; |
| | | model_id?: number; |
| | | } |
| | | |
| | | export interface MessageReq { |
| | | text?: string; |
| | | attachment_id?: string; |
| | | model_id?: number; |
| | | } |
| | | } |
| | | |
| | | export namespace AgentTaskApi { |
| | | export interface Progress { |
| | | mode: 'determinate' | 'indeterminate'; |
| | | completed: number; |
| | | total: null | number; |
| | | percent: null | number; |
| | | unit?: string; |
| | | } |
| | | |
| | | export interface Result { |
| | | attachment_id?: number; |
| | | attachment_ids?: number[]; |
| | | candidate_count?: number; |
| | | continuation_supported?: boolean; |
| | | evidence_ref?: { |
| | | backend: string; |
| | | candidate_count?: number; |
| | | revision: string; |
| | | task_id: string; |
| | | } | null; |
| | | filename?: string; |
| | | filenames?: string[]; |
| | | module_id?: string; |
| | | module_title?: string; |
| | | result_kind?: string; |
| | | source_sha256?: string; |
| | | source_sha256s?: string[]; |
| | | review_summary?: { |
| | | conflict_groups: number; |
| | | exact_duplicates_merged: number; |
| | | incomplete_keys: number; |
| | | requires_review: boolean; |
| | | }; |
| | | warnings?: string[]; |
| | | } |
| | | |
| | | export interface Task { |
| | | task_id: string; |
| | | owner_id: string; |
| | | kind: string; |
| | | status: string; |
| | | stage: string; |
| | | attempt: number; |
| | | sequence: number; |
| | | progress: Progress; |
| | | message: string; |
| | | result: null | Result; |
| | | error: null | { code?: string; message?: string }; |
| | | allowed_actions: string[]; |
| | | created_at: null | string; |
| | | updated_at: null | string; |
| | | started_at: null | string; |
| | | finished_at: null | string; |
| | | } |
| | | |
| | | export interface PageResult { |
| | | items: Task[]; |
| | | total: number; |
| | | page: number; |
| | | page_size: number; |
| | | } |
| | | |
| | | export interface CreateReq { |
| | | attachment_id?: number; |
| | | attachment_ids?: number[]; |
| | | module_id?: string; |
| | | constraints?: AgentConversationApi.ConversationConstraints; |
| | | } |
| | | } |
| | | |
| | | /** è½å声æï¼æ¯æçæ ¼å¼ã模åä¸éå¶ */ |
| | | export function getAgentCapabilities() { |
| | | return agentClient.get<AgentConversationApi.Capabilities>('/v1/capabilities'); |
| | | } |
| | | |
| | | /** 读åå½åç»å½ç¨æ·ç» Java å端è£åªåç坿¥è¯¢ã坿ä½è½åã */ |
| | | export function getAuthorizedAgentCapabilities() { |
| | | return agentClient.get<AgentConversationApi.AuthorizedCapabilityCatalog>( |
| | | '/v1/authorization/capabilities', |
| | | ); |
| | | } |
| | | |
| | | /** å½åæ°æ®è¡¥å½é¾è·¯å®é
使ç¨çä¸å¡æ¨¡åï¼ä¸è¿å API å¯é¥ææå¡å°åã */ |
| | | export function getActiveBusinessModel() { |
| | | return agentClient.get<AgentConversationApi.BusinessModelInfo>('/v1/model'); |
| | | } |
| | | |
| | | /** å¯åæ¢çä¸å¡æ¨¡ååè¡¨ï¼æå端æåºè¿åï¼é»è®¤ä½¿ç¨ç¬¬ä¸ä¸ªã */ |
| | | export function getAgentModels() { |
| | | return agentClient.get<AgentConversationApi.BusinessModelInfo[]>('/v1/models'); |
| | | } |
| | | |
| | | /** å页读åå½åç¨æ·çæä¹
åå°ä»»å¡ã */ |
| | | export function listAgentTasks(params?: { |
| | | page?: number; |
| | | page_size?: number; |
| | | status?: string; |
| | | }) { |
| | | return agentClient.get<AgentTaskApi.PageResult>('/v1/tasks', { params }); |
| | | } |
| | | |
| | | /** åå»ºææ¡£åå°åæä»»å¡ï¼æå¡ç«¯ç«å³è¿å HTTP 202 å¿«ç
§ã */ |
| | | export function createDocumentTask(data: AgentTaskApi.CreateReq) { |
| | | return agentClient.post<AgentTaskApi.Task>('/v1/tasks/document-onboarding', data); |
| | | } |
| | | |
| | | /** 读åä»»å¡äºå®å¿«ç
§ã */ |
| | | export function getAgentTask(taskId: string) { |
| | | return agentClient.get<AgentTaskApi.Task>(`/v1/tasks/${taskId}`); |
| | | } |
| | | |
| | | /** åæ¶ä»»å¡å¹¶æ¤éçæå§æç¥¨æ®ã */ |
| | | export function cancelAgentTask(taskId: string) { |
| | | return agentClient.delete<AgentTaskApi.Task>(`/v1/tasks/${taskId}`); |
| | | } |
| | | |
| | | /** 使ç¨è®¤è¯è¯·æ±è¯»åä»»å¡ SSEï¼æ¸¸æ ç¨äºæçº¿ç»ä¼ ã */ |
| | | export async function streamAgentTaskEvents( |
| | | taskId: string, |
| | | since: number, |
| | | signal: AbortSignal, |
| | | onEvent: (event: AgentTaskApi.Task) => void, |
| | | ) { |
| | | const token = useAccessStore().accessToken; |
| | | const response = await fetch( |
| | | `/agent-api/v1/tasks/${encodeURIComponent(taskId)}/events?since=${since}`, |
| | | { |
| | | credentials: 'same-origin', |
| | | headers: { |
| | | Accept: 'text/event-stream', |
| | | ...(token ? { Authorization: `Bearer ${token}` } : {}), |
| | | }, |
| | | signal, |
| | | }, |
| | | ); |
| | | if (!response.ok) throw new Error(`ä»»å¡äºä»¶è¿æ¥å¤±è´¥ï¼HTTP ${response.status}ï¼`); |
| | | if (!response.body) throw new Error('å½åæµè§å¨ä¸æ¯ææµå¼äºä»¶'); |
| | | const reader = response.body.getReader(); |
| | | const decoder = new TextDecoder(); |
| | | let buffer = ''; |
| | | let cursor = since; |
| | | while (true) { |
| | | const { done, value } = await reader.read(); |
| | | buffer += decoder.decode(value, { stream: !done }).replaceAll('\r\n', '\n'); |
| | | const frames = buffer.split('\n\n'); |
| | | buffer = frames.pop() ?? ''; |
| | | for (const frame of frames) { |
| | | const data = frame |
| | | .split('\n') |
| | | .filter((line) => line.startsWith('data:')) |
| | | .map((line) => line.slice(5).trimStart()) |
| | | .join('\n'); |
| | | if (!data) continue; |
| | | const event = JSON.parse(data) as AgentTaskApi.Task; |
| | | if (!Number.isSafeInteger(event.sequence) || event.sequence <= cursor) continue; |
| | | cursor = event.sequence; |
| | | onEvent(event); |
| | | } |
| | | if (done) break; |
| | | } |
| | | return cursor; |
| | | } |
| | | |
| | | /** ä¼è¯å表 */ |
| | | export function listAgentConversations(params?: { |
| | | page?: number; |
| | | page_size?: number; |
| | | status?: string; |
| | | }) { |
| | | return agentClient.get<AgentConversationApi.PageResult>( |
| | | '/v1/conversations', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å»ºè¡¥å½ä¼è¯ */ |
| | | export function createAgentConversation(data: AgentConversationApi.CreateReq) { |
| | | return agentClient.post<AgentConversationApi.Conversation>( |
| | | '/v1/conversations', |
| | | data, |
| | | ); |
| | | } |
| | | |
| | | /** 读åä¼è¯ */ |
| | | export function getAgentConversation(conversationId: string) { |
| | | return agentClient.get<AgentConversationApi.Conversation>( |
| | | `/v1/conversations/${conversationId}`, |
| | | ); |
| | | } |
| | | |
| | | /** å é¤ä¼è¯ï¼åæ¶æ¸
çæ£æ¥ç¹ãäºä»¶ä¸ Java ä¾§è¡¥å½éä»¶ */ |
| | | export function deleteAgentConversation(conversationId: string) { |
| | | return agentClient.delete<{ |
| | | attachments_deleted: number; |
| | | conversation_id: string; |
| | | deleted: boolean; |
| | | }>(`/v1/conversations/${conversationId}`); |
| | | } |
| | | |
| | | /** |
| | | * 读åä¼è¯éä»¶ãmode 为 inline æ¶ PDF/å¾çç±æµè§å¨ç´æ¥é¢è§ï¼ |
| | | * å
¶ä½ç±»åæå¡ç«¯ä¼èªå¨é级为ä¸è½½ã |
| | | */ |
| | | export function downloadAgentConversationAttachment( |
| | | conversationId: string, |
| | | attachmentId: string, |
| | | mode: 'download' | 'inline', |
| | | ) { |
| | | return agentClient.download<{ |
| | | data: Blob; |
| | | headers: Record<string, string>; |
| | | }>(`/v1/conversations/${conversationId}/attachment`, { |
| | | params: { attachment_id: attachmentId, mode }, |
| | | responseReturn: 'raw', |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * ä½¿ç¨ fetch 读å带认è¯å¤´ç SSEãEventSource ä¸è½è®¾ç½® Authorizationï¼ä¸è½ç¨äºæ¬æ¥å£ã |
| | | * æå¡ç«¯ä¼ä¸»å¨ç»æçè¿æ¥ï¼è°ç¨æ¹è´è´£æºå¸¦è¿å游æ éè¿ã |
| | | */ |
| | | export async function streamAgentConversationEvents( |
| | | conversationId: string, |
| | | since: number, |
| | | signal: AbortSignal, |
| | | onEvent: (event: AgentConversationApi.ConversationEvent) => void, |
| | | onOpen?: () => void, |
| | | ) { |
| | | const token = useAccessStore().accessToken; |
| | | const response = await fetch( |
| | | `/agent-api/v1/conversations/${encodeURIComponent(conversationId)}/events?since=${since}`, |
| | | { |
| | | credentials: 'same-origin', |
| | | headers: { |
| | | Accept: 'text/event-stream', |
| | | ...(token ? { Authorization: `Bearer ${token}` } : {}), |
| | | }, |
| | | signal, |
| | | }, |
| | | ); |
| | | if (!response.ok) { |
| | | throw new Error(`äºä»¶è¿æ¥å¤±è´¥ï¼HTTP ${response.status}ï¼`); |
| | | } |
| | | if (!response.body) { |
| | | throw new Error('å½åæµè§å¨ä¸æ¯ææµå¼äºä»¶'); |
| | | } |
| | | onOpen?.(); |
| | | const reader = response.body.getReader(); |
| | | const decoder = new TextDecoder(); |
| | | let buffer = ''; |
| | | let cursor = since; |
| | | while (true) { |
| | | const { done, value } = await reader.read(); |
| | | buffer += decoder.decode(value, { stream: !done }).replaceAll('\r\n', '\n'); |
| | | const frames = buffer.split('\n\n'); |
| | | buffer = frames.pop() ?? ''; |
| | | for (const frame of frames) { |
| | | const data = frame |
| | | .split('\n') |
| | | .filter((line) => line.startsWith('data:')) |
| | | .map((line) => line.slice(5).trimStart()) |
| | | .join('\n'); |
| | | if (!data) continue; |
| | | const event = JSON.parse(data) as AgentConversationApi.ConversationEvent; |
| | | if (!Number.isSafeInteger(event.seq) || event.seq <= cursor) continue; |
| | | cursor = event.seq; |
| | | onEvent(event); |
| | | } |
| | | if (done) break; |
| | | } |
| | | return cursor; |
| | | } |
| | | |
| | | /** åéææ¬æéä»¶ï¼ç¨äºè¡¥å
ä¸ä¼ æä»¶ï¼ */ |
| | | export function sendAgentMessage( |
| | | conversationId: string, |
| | | data: AgentConversationApi.MessageReq, |
| | | ) { |
| | | return agentClient.post<AgentConversationApi.Conversation>( |
| | | `/v1/conversations/${conversationId}/messages`, |
| | | data, |
| | | ); |
| | | } |
| | | |
| | | /** å¡çæä½ï¼éæ©åºæ¯ã确认æ å°ã修订ã确认æåæ¶ */ |
| | | export function sendAgentAction( |
| | | conversationId: string, |
| | | data: AgentConversationApi.ActionReq, |
| | | ) { |
| | | return agentClient.post<AgentConversationApi.Conversation>( |
| | | `/v1/conversations/${conversationId}/actions`, |
| | | data, |
| | | ); |
| | | } |
| | | |
| | | /** ä¸è½½å¯¼å
¥æ¥è¡¨ï¼xlsxï¼ */ |
| | | export function downloadAgentReport(conversationId: string) { |
| | | return agentClient.download<{ |
| | | data: Blob; |
| | | headers: Record<string, string>; |
| | | }>(`/v1/conversations/${conversationId}/report`, { |
| | | responseReturn: 'raw', |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * ä¸ä¼ éä»¶å° Java åå¨å¹¶ç»å®è¡¥å½ä¼è¯ï¼è¿å attachmentIdã |
| | | * Python ä¾§éè¿ Java Bridge æ attachmentId 读åæä»¶ã |
| | | */ |
| | | export async function uploadAgentAttachment(file: File) { |
| | | const sessionId = await createImportSession(); |
| | | const blobs = await uploadFile([file]); |
| | | const blobId = blobs?.[0]?.id; |
| | | if (!blobId) { |
| | | throw new Error('æä»¶ä¸ä¼ å¤±è´¥ï¼æªåå¾ blobId'); |
| | | } |
| | | return await bindImportAttachment(sessionId, blobId); |
| | | } |
| | | |
| | | export { agentImportErrorMessage } from '#/api/mdm/item/agent'; |
| | |
| | | model: string; // æ¨¡åæ è¯ |
| | | platform: string; // 模åå¹³å° |
| | | type: number; // 模åç±»å |
| | | capability?: string; // 模åè½åï¼text-ææ¬ï¼vision-è§è§ |
| | | sort: number; // æåº |
| | | status: number; // ç¶æ |
| | | temperature?: number; // æ¸©åº¦åæ° |
| | |
| | | } |
| | | |
| | | /** è·å¾æ¨¡åå表 */ |
| | | export function getModelSimpleList(type?: number) { |
| | | export function getModelSimpleList(type?: number, capability?: string) { |
| | | return requestClient.get<AiModelModelApi.Model[]>('/ai/model/simple-list', { |
| | | params: { |
| | | type, |
| | | capability, |
| | | }, |
| | | }); |
| | | } |
| | |
| | | return requestClient.put('/ai/model/update', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹æ¨¡åç¶æï¼å¯ç¨æ¶ä¼èªå¨å
³éåç±»ååè½åçå
¶ä»æ¨¡å */ |
| | | export function updateModelStatus(id: number, status: number) { |
| | | return requestClient.put('/ai/model/update-status', { id, status }); |
| | | } |
| | | |
| | | /** å 餿¨¡å */ |
| | | export function deleteModel(id: number) { |
| | | return requestClient.delete(`/ai/model/delete?id=${id}`); |
| | |
| | | workHours?: number; |
| | | overtimeHours?: number; |
| | | location?: string; |
| | | /** ä¸çæå¡ç»åº¦ï¼WGS84ï¼ */ |
| | | clockInLongitude?: number; |
| | | /** ä¸çæå¡çº¬åº¦ï¼WGS84ï¼ */ |
| | | clockInLatitude?: number; |
| | | /** ä¸çå®ä½ç²¾åº¦ï¼ç±³ */ |
| | | clockInAccuracy?: number; |
| | | /** ä¸çè·æå¡å°ç¹è·ç¦»ï¼ç±³ */ |
| | | clockInDistance?: number; |
| | | /** ä¸çæå¡å°ç¹å°åå¿«ç
§ */ |
| | | clockInAddress?: string; |
| | | /** ä¸çæå¡ç»åº¦ï¼WGS84ï¼ */ |
| | | clockOutLongitude?: number; |
| | | /** ä¸çæå¡çº¬åº¦ï¼WGS84ï¼ */ |
| | | clockOutLatitude?: number; |
| | | /** ä¸çå®ä½ç²¾åº¦ï¼ç±³ */ |
| | | clockOutAccuracy?: number; |
| | | /** ä¸çè·æå¡å°ç¹è·ç¦»ï¼ç±³ */ |
| | | clockOutDistance?: number; |
| | | /** ä¸çæå¡å°ç¹å°åå¿«ç
§ */ |
| | | clockOutAddress?: string; |
| | | dataSource?: number; |
| | | dataSourceName?: string; |
| | | remark?: string; |
| | |
| | | overtimeHours?: number; |
| | | totalWorkHours?: number; |
| | | } |
| | | } |
| | | |
| | | /** å®ä½æå¡åæ ï¼å端ä»
æ¥å WGS84 */ |
| | | export interface AttendanceClockLocation { |
| | | longitude: number; |
| | | latitude: number; |
| | | accuracy: number; |
| | | coordinateSystem: 'WGS84'; |
| | | } |
| | | |
| | | /** å®ä½æå¡ç»æï¼æªå¯ç¨å®ä½èå´æ ¡éªæ¶ï¼è·ç¦»ä¸åå¾ä¸ºç©º */ |
| | | export interface AttendanceClockResponse { |
| | | record?: HrmAttendanceRecordApi.AttendanceRecord; |
| | | locationName?: string; |
| | | distanceMeters?: number; |
| | | radiusMeters?: number; |
| | | withinRange?: boolean; |
| | | } |
| | | |
| | | /** æ¥è¯¢èå¤è®°å½å页å表 */ |
| | |
| | | return requestClient.download('/hrm/attendance/record/export-excel', { params }); |
| | | } |
| | | |
| | | /** ä¸çæå¡ */ |
| | | export function clockIn(params: { location: string }) { |
| | | return requestClient.post<HrmAttendanceRecordApi.AttendanceRecord>( |
| | | /** |
| | | * ä¸çæå¡ |
| | | * |
| | | * åæ¶æäº¤å®ä½ï¼JSON bodyï¼ä¸æ§çå°ç¹ææ¬ï¼query åæ°ï¼ï¼ |
| | | * - æ°å端æå®ä½æ ¡éªæå¡èå´ï¼æªå¯ç¨å®ä½æ¶å¿½ç¥å®ä½ï¼ |
| | | * - æ§å端åªè¯»å location åæ°ï¼ä¿è¯å级ååé½è½æ£å¸¸æå¡ã |
| | | */ |
| | | export function clockIn( |
| | | data?: AttendanceClockLocation | null, |
| | | location?: string, |
| | | ) { |
| | | return requestClient.post<AttendanceClockResponse>( |
| | | '/hrm/attendance/record/clock-in', |
| | | null, |
| | | { params }, |
| | | data ?? null, |
| | | location ? { params: { location } } : undefined, |
| | | ); |
| | | } |
| | | |
| | | /** ä¸çæå¡ */ |
| | | export function clockOut(params: { location: string }) { |
| | | return requestClient.post<HrmAttendanceRecordApi.AttendanceRecord>( |
| | | /** ä¸çæå¡ï¼åæ°è¯´æå clockIn */ |
| | | export function clockOut( |
| | | data?: AttendanceClockLocation | null, |
| | | location?: string, |
| | | ) { |
| | | return requestClient.post<AttendanceClockResponse>( |
| | | '/hrm/attendance/record/clock-out', |
| | | null, |
| | | { params }, |
| | | data ?? null, |
| | | location ? { params: { location } } : undefined, |
| | | ); |
| | | } |
| | | } |
| | |
| | | lateAllowMinutes?: number; |
| | | earlyAllowMinutes?: number; |
| | | overtimeThreshold?: number; |
| | | /** æå¡å°ç¹åç§° */ |
| | | locationName?: string; |
| | | /** æå¡å°ç¹å°å */ |
| | | locationAddress?: string; |
| | | /** æå¡ä¸å¿ç»åº¦ï¼WGS84ï¼ */ |
| | | longitude?: number; |
| | | /** æå¡ä¸å¿çº¬åº¦ï¼WGS84ï¼ */ |
| | | latitude?: number; |
| | | /** å
许æå¡åå¾ï¼åä½ç±³ */ |
| | | radiusMeters?: number; |
| | | /** æ¯å¦å¯ç¨å®ä½èå´æ ¡éª */ |
| | | locationRequired?: boolean; |
| | | /** åæ ç³»ï¼ä»
æ¯æ WGS84 */ |
| | | coordinateSystem?: string; |
| | | status?: number; |
| | | remark?: string; |
| | | createTime?: string; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { InternalAxiosRequestConfig } from '@vben/request'; |
| | | |
| | | import { RequestClient } from '@vben/request'; |
| | | import { useAccessStore } from '@vben/stores'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | /** |
| | | * Python æºè½ä½æå¡ä¸å¨ /admin-api ä¸ï¼å¼åæç± Vite 代ç /agent-api 转åå° 127.0.0.1:8091ã |
| | | * 该æå¡ç´æ¥è¿åä¸å¡ JSONï¼ä¸ä½¿ç¨ CommonResult å
è£
ï¼å æ¤åç¬å»ºå®¢æ·ç«¯ã |
| | | */ |
| | | const agentRequestClient = new RequestClient({ |
| | | baseURL: '/agent-api', |
| | | responseReturn: 'body', |
| | | timeout: 120_000, |
| | | }); |
| | | |
| | | agentRequestClient.addRequestInterceptor({ |
| | | fulfilled: (config: InternalAxiosRequestConfig) => { |
| | | const token = useAccessStore().accessToken; |
| | | if (token) { |
| | | config.headers.Authorization = `Bearer ${token}`; |
| | | } |
| | | return config; |
| | | }, |
| | | }); |
| | | |
| | | export namespace MdmAgentImportApi { |
| | | export type RowAction = 'CREATE' | 'SKIP' | 'BLOCKED'; |
| | | export type RowStatus = 'SUCCESS' | 'FAILED' | 'SKIPPED' | 'UNKNOWN'; |
| | | |
| | | export interface Evidence { |
| | | sheet: string; |
| | | row: number; |
| | | column: string; |
| | | raw: string; |
| | | } |
| | | |
| | | export interface Candidate { |
| | | candidate_id: string; |
| | | source_sha256: string; |
| | | values: Record<string, string>; |
| | | evidence: Record<string, Evidence>; |
| | | issues: string[]; |
| | | } |
| | | |
| | | export interface Issue { |
| | | code?: string; |
| | | field?: string; |
| | | message: string; |
| | | retryable?: boolean; |
| | | } |
| | | |
| | | export interface Resolved { |
| | | category_id?: number; |
| | | category_name?: string; |
| | | unit_measure_id?: number; |
| | | unit_measure_name?: string; |
| | | } |
| | | |
| | | export interface PreflightRow { |
| | | candidate_id: string; |
| | | row_no?: number; |
| | | code?: string; |
| | | name?: string; |
| | | action: RowAction; |
| | | resolved?: Resolved; |
| | | blockers?: Issue[]; |
| | | warnings?: Issue[]; |
| | | target_item_id?: number; |
| | | } |
| | | |
| | | export interface Preflight { |
| | | preflight_hash: string; |
| | | source: { |
| | | attachment_id: number; |
| | | session_id?: number; |
| | | source_sha256: string; |
| | | proposal_revision?: string; |
| | | }; |
| | | policy: { |
| | | update_allowed: boolean; |
| | | mes_sync_allowed: boolean; |
| | | max_rows: number; |
| | | }; |
| | | summary: { |
| | | total: number; |
| | | create: number; |
| | | skip: number; |
| | | blocked: number; |
| | | }; |
| | | can_commit: boolean; |
| | | side_effects: string[]; |
| | | rows: PreflightRow[]; |
| | | blocking_items: Issue[]; |
| | | } |
| | | |
| | | export interface Correction { |
| | | candidate_id: string; |
| | | field: 'categoryCode' | 'code' | 'name' | 'specification' | 'unitMeasureCode'; |
| | | value: string; |
| | | reason: string; |
| | | } |
| | | |
| | | export interface ReceiptRow { |
| | | candidate_id: string; |
| | | row_no?: number; |
| | | code?: string; |
| | | name?: string; |
| | | action: 'CREATE' | 'SKIP'; |
| | | status: RowStatus; |
| | | target_item_id?: number; |
| | | target_version?: string; |
| | | error_code?: string; |
| | | error_message?: string; |
| | | retryable?: boolean; |
| | | } |
| | | |
| | | export interface Receipt { |
| | | command_id: string; |
| | | idempotency_key: string; |
| | | status: 'FAILED' | 'PARTIAL_FAILED' | 'RUNNING' | 'SUCCEEDED' | 'UNKNOWN'; |
| | | attachment_id: number; |
| | | session_id?: number; |
| | | source_sha256: string; |
| | | preflight_hash: string; |
| | | proposal_hash?: string; |
| | | summary: { |
| | | total: number; |
| | | success: number; |
| | | failed: number; |
| | | skipped: number; |
| | | unknown: number; |
| | | }; |
| | | error_code?: string; |
| | | error_message?: string; |
| | | rows: ReceiptRow[]; |
| | | create_time?: string; |
| | | completed_time?: string; |
| | | } |
| | | |
| | | export interface ImportState { |
| | | task_id: string; |
| | | attachment_id: string; |
| | | filename: string; |
| | | source_sha256: string; |
| | | candidates: Candidate[]; |
| | | issues: Array<Issue & { candidate_id?: string; candidate_ids?: string[] }>; |
| | | revision: number; |
| | | status: string; |
| | | preflight?: Preflight; |
| | | preflight_hash?: string; |
| | | proposal?: Record<string, unknown>; |
| | | proposal_hash?: string; |
| | | command_id?: string; |
| | | receipt?: Receipt; |
| | | last_error?: string; |
| | | } |
| | | |
| | | export interface ReviewInterrupt { |
| | | type: string; |
| | | revision: number; |
| | | issues: Array<Issue & { candidate_id?: string; candidate_ids?: string[] }>; |
| | | candidates: Candidate[]; |
| | | preflight: { |
| | | hash?: string; |
| | | can_commit: boolean; |
| | | summary?: Preflight['summary']; |
| | | blocking_items: Issue[]; |
| | | needs_attention: PreflightRow[]; |
| | | }; |
| | | last_error?: string; |
| | | notice?: string; |
| | | } |
| | | |
| | | export interface PublicState { |
| | | state: ImportState; |
| | | interrupts: ReviewInterrupt[]; |
| | | } |
| | | |
| | | export interface ReviewReq { |
| | | expected_revision: number; |
| | | action: 'cancel' | 'confirm' | 'revise'; |
| | | corrections?: Correction[]; |
| | | expected_preflight_hash?: string; |
| | | expected_proposal_hash?: string; |
| | | } |
| | | } |
| | | |
| | | /** å建补å½ä¼è¯ï¼Javaï¼ */ |
| | | export function createImportSession() { |
| | | return requestClient.post<number>('/agent/bridge/import-sessions'); |
| | | } |
| | | |
| | | /** ç»å®éä»¶å°è¡¥å½ä¼è¯ï¼è¿å attachmentIdï¼Javaï¼ */ |
| | | export function bindImportAttachment(sessionId: number, blobId: number) { |
| | | return requestClient.post<number>( |
| | | `/agent/bridge/import-sessions/${sessionId}/attachments`, |
| | | { blobId }, |
| | | ); |
| | | } |
| | | |
| | | /** å建æºè½ä½å¯¼å
¥ä»»å¡å¹¶è§¦åè§£æä¸é¢æ£ï¼Pythonï¼ */ |
| | | export function startMaterialImport(attachmentId: number) { |
| | | return agentRequestClient.post<MdmAgentImportApi.PublicState>( |
| | | '/v1/material-imports', |
| | | { attachment_id: String(attachmentId) }, |
| | | ); |
| | | } |
| | | |
| | | /** 读åä»»å¡å½åç¶æï¼Pythonï¼ */ |
| | | export function getMaterialImportTask(taskId: string) { |
| | | return agentRequestClient.get<MdmAgentImportApi.PublicState>( |
| | | `/v1/material-imports/${taskId}`, |
| | | ); |
| | | } |
| | | |
| | | /** 修订æç¡®è®¤ï¼Pythonï¼ */ |
| | | export function reviewMaterialImport( |
| | | taskId: string, |
| | | data: MdmAgentImportApi.ReviewReq, |
| | | ) { |
| | | return agentRequestClient.post<MdmAgentImportApi.PublicState>( |
| | | `/v1/material-imports/${taskId}/review`, |
| | | data, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢æäº¤åæ§ï¼Pythonï¼ */ |
| | | export function getMaterialImportReceipt(taskId: string) { |
| | | return agentRequestClient.get<MdmAgentImportApi.Receipt>( |
| | | `/v1/material-imports/${taskId}/receipt`, |
| | | ); |
| | | } |
| | | |
| | | interface AgentErrorDetail { |
| | | error_code?: string; |
| | | message?: string; |
| | | } |
| | | |
| | | /** ä» Python é误ååºä¸æåå¯è¯»ææ¡ */ |
| | | export function agentImportErrorMessage(error: unknown): string { |
| | | const err = error as |
| | | | { data?: unknown; message?: string; response?: { data?: unknown } } |
| | | | undefined; |
| | | const body = (err?.response?.data ?? err?.data) as |
| | | | AgentErrorDetail |
| | | | string |
| | | | undefined; |
| | | const detail = |
| | | typeof body === 'string' |
| | | ? body |
| | | : ((body as { detail?: unknown } | undefined)?.detail ?? body); |
| | | if (typeof detail === 'string') { |
| | | return detail; |
| | | } |
| | | const info = detail as AgentErrorDetail | undefined; |
| | | if (info?.message) { |
| | | return info.error_code |
| | | ? `${info.message}ï¼${info.error_code}ï¼` |
| | | : info.message; |
| | | } |
| | | return err?.message || 'æºè½ä½æå¡è¯·æ±å¤±è´¥'; |
| | | } |
| | |
| | | export function exportRoute(params: any) { |
| | | return requestClient.download('/mes/pro/route/export-excel', { params }); |
| | | } |
| | | |
| | | /** ä¸è½½å·¥èºè·¯çº¿å¯¼å
¥æ¨¡æ¿ */ |
| | | export function importRouteTemplate() { |
| | | return requestClient.download('/mes/pro/route/get-import-template'); |
| | | } |
| | | |
| | | /** 导å
¥å·¥èºè·¯çº¿ */ |
| | | export function importRoute(file: File, updateSupport: boolean) { |
| | | return requestClient.upload( |
| | | `/mes/pro/route/import?updateSupport=${updateSupport}`, |
| | | { file }, |
| | | ); |
| | | } |
| | | |
| | | /** ä¸è½½å
å«è·¯çº¿ã产åãå·¥åºãå·¥åºäº§åºãå·¥åºææäºå¼ å·¥ä½è¡¨çæ¨¡æ¿ */ |
| | | export function importCompleteRouteTemplate() { |
| | | return requestClient.download('/mes/pro/route/get-complete-import-template'); |
| | | } |
| | | |
| | | /** æ¹é导å
¥å®æ´å·¥èºè·¯çº¿ */ |
| | | export function importCompleteRoute(file: File) { |
| | | return requestClient.upload('/mes/pro/route/import-complete', { file }); |
| | | } |
| | |
| | | remark?: string; |
| | | } |
| | | |
| | | /** 产åºç©æå½å± */ |
| | | export interface OutputItem { |
| | | id?: number; |
| | | routeProcessId?: number; |
| | | productId?: number; // å½å±äº§åç¼å·ï¼ä¸ºç©ºè¡¨ç¤ºå¤äº§åéç¨ |
| | | productName?: string; |
| | | itemId?: number; |
| | | itemCode?: string; |
| | | itemName?: string; |
| | | sort?: number; |
| | | remark?: string; |
| | | } |
| | | |
| | | /** MES å·¥èºè·¯çº¿å·¥åº */ |
| | | export interface RouteProcess { |
| | | id?: number; |
| | | routeId?: number; |
| | | processId?: number; |
| | | paramTemplateId?: number; |
| | | /** å·¥åºæ¶çï¼äº§åº/æå
¥ï¼0~1ï¼ */ |
| | | yieldRate?: number; |
| | | processCode?: string; |
| | | processName?: string; |
| | |
| | | outputItemName?: string; |
| | | outputItemIds?: number[]; |
| | | outputItemNames?: string; |
| | | outputs?: OutputItem[]; // 产åºç©æå½å±é
ç½® |
| | | warehouseId?: number; // é»è®¤æ¥å·¥ä»åºç¼å· |
| | | warehouseName?: string; // é»è®¤æ¥å·¥ä»åºåç§° |
| | | locationId?: number; // é»è®¤æ¥å·¥åºåºç¼å· |
| | | locationName?: string; // é»è®¤æ¥å·¥åºåºåç§° |
| | | areaId?: number; // é»è®¤æ¥å·¥åºä½ç¼å· |
| | | areaName?: string; // é»è®¤æ¥å·¥åºä½åç§° |
| | | remark?: string; |
| | | bomItems?: ProcessBomItem[]; |
| | | createTime?: Date; |
| | |
| | | itemId?: number; |
| | | itemCode?: string; |
| | | itemName?: string; |
| | | warehouseId?: number; |
| | | warehouseName?: string; |
| | | locationId?: number; |
| | | locationName?: string; |
| | | areaId?: number; |
| | | areaName?: string; |
| | | specification?: string; |
| | | unitName?: string; |
| | | quantity?: number; |
| | |
| | | import type { MesProWorkOrderProcessApi } from './index'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace MesProWorkOrderProcessApi { |
| | |
| | | outputItemName?: string; // 产åºäº§åç©æåç§°ï¼ç¬¬ä¸ä¸ªï¼ |
| | | outputItemIds?: number[]; // 产åºäº§åç©æç¼å·å表 |
| | | outputItemNames?: string; // 产åºäº§åç©æåç§°ï¼éå·æ¼æ¥ï¼ |
| | | warehouseId?: number; // æ¥å·¥ä»åºç¼å· |
| | | warehouseName?: string; // æ¥å·¥ä»åºåç§° |
| | | locationId?: number; // æ¥å·¥åºåºç¼å· |
| | | locationName?: string; // æ¥å·¥åºåºåç§° |
| | | areaId?: number; // æ¥å·¥åºä½ç¼å· |
| | | areaName?: string; // æ¥å·¥åºä½åç§° |
| | | bomItems?: ProcessBomItem[]; // å·¥åºææBOMæç» |
| | | remark?: string; // 夿³¨ |
| | | } |
| | |
| | | /** å é¤å·¥åå·¥åº */ |
| | | export function deleteWorkOrderProcess(id: number) { |
| | | return requestClient.delete(`/mes/pro/work-order-process/delete?id=${id}`); |
| | | } |
| | | } |
| | |
| | | import { initStores } from './packages/stores/src'; |
| | | import './packages/styles/src'; |
| | | import './packages/styles/src/antd/index.css'; |
| | | import './styles/herbal-theme.css'; |
| | | |
| | | import { useTitle } from '@vueuse/core'; |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FileType } from 'ant-design-vue/es/upload/interface'; |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | |
| | | import { h, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button, message, Modal as AntModal, Upload } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm, z } from '#/adapter/form'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | export interface ImportModalProps { |
| | | /** å¼¹çªæ é¢ï¼å¦ã导å
¥ç¨æ·ã */ |
| | | title: string; |
| | | /** ä¸è½½çæ¨¡æ¿æä»¶åï¼å¦ãç¨æ·å¯¼å
¥æ¨¡æ¿.xlsã */ |
| | | fileName: string; |
| | | /** æ§è¡å¯¼å
¥çæ¥å£å½æ° (file, updateSupport) => Promise */ |
| | | importFn: (file: File, updateSupport: boolean) => Promise<unknown>; |
| | | /** ä¸è½½æ¨¡æ¿çæ¥å£å½æ° () => Promise(blob) */ |
| | | templateFn: () => Promise<BlobPart>; |
| | | /** 宿´è·¯çº¿åªæ¯ææ°å¢ï¼ä¸å±ç¤ºè¦çå¼å
³ */ |
| | | showUpdateSupport?: boolean; |
| | | } |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const config = ref<ImportModalProps>({ |
| | | title: '', |
| | | fileName: '', |
| | | importFn: async () => {}, |
| | | templateFn: () => Promise.resolve(new Blob()), |
| | | }); |
| | | |
| | | function createSchema(showUpdateSupport = true): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'file', |
| | | label: 'æ°æ®æä»¶', |
| | | component: 'Upload' as const, |
| | | rules: 'required', |
| | | help: 'ä»
å
许导å
¥ xlsãxlsx æ ¼å¼æä»¶', |
| | | }, |
| | | ...(showUpdateSupport ? [{ |
| | | fieldName: 'updateSupport', |
| | | label: 'æ¯å¦è¦ç', |
| | | component: 'Switch' as const, |
| | | componentProps: { |
| | | checkedChildren: 'æ¯', |
| | | unCheckedChildren: 'å¦', |
| | | }, |
| | | rules: z.boolean().default(false), |
| | | help: 'æ¯å¦æ´æ°å·²ç»åå¨çæ°æ®', |
| | | }] : []), |
| | | ]; |
| | | } |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | formItemClass: 'col-span-2', |
| | | labelWidth: 120, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: createSchema(), |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (isOpen) { |
| | | config.value = modalApi.getData<ImportModalProps>() ?? config.value; |
| | | await formApi.updateSchema(createSchema(config.value.showUpdateSupport !== false)); |
| | | await formApi.resetForm(); |
| | | } |
| | | }, |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | // æäº¤è¡¨å |
| | | const data = await formApi.getValues(); |
| | | try { |
| | | const result = await config.value.importFn(data.file, data.updateSupport ?? false) as { |
| | | createCodes?: string[]; |
| | | updateCodes?: string[]; |
| | | failureCodes?: Record<string, string>; |
| | | } | undefined; |
| | | // å
³éå¹¶æç¤º |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | const failures = Object.entries(result?.failureCodes ?? {}); |
| | | if (failures.length > 0) { |
| | | AntModal.warning({ |
| | | title: `导å
¥å®æï¼æå ${(result?.createCodes?.length ?? 0) + (result?.updateCodes?.length ?? 0)} æ¡ï¼å¤±è´¥ ${failures.length} æ¡`, |
| | | content: h('div', { style: { whiteSpace: 'pre-wrap', maxHeight: '50vh', overflowY: 'auto' } }, |
| | | failures.map(([code, reason]) => `${code}ï¼${reason}`).join('\n')), |
| | | }); |
| | | } else { |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | /** ä¸ä¼ å */ |
| | | function beforeUpload(file: FileType) { |
| | | formApi.setFieldValue('file', file); |
| | | return false; |
| | | } |
| | | |
| | | /** ä¸è½½æ¨¡ç */ |
| | | async function handleDownload() { |
| | | const data = await config.value.templateFn(); |
| | | downloadFileFromBlobPart({ fileName: config.value.fileName, source: data }); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="config.title" class="w-1/3"> |
| | | <Form class="mx-4"> |
| | | <template #file> |
| | | <div class="w-full"> |
| | | <Upload |
| | | :max-count="1" |
| | | accept=".xls,.xlsx" |
| | | :before-upload="beforeUpload" |
| | | > |
| | | <Button type="primary"> éæ© Excel æä»¶ </Button> |
| | | </Upload> |
| | | </div> |
| | | </template> |
| | | </Form> |
| | | <template #prepend-footer> |
| | | <div class="flex flex-auto items-center"> |
| | | <Button @click="handleDownload"> ä¸è½½å¯¼å
¥æ¨¡æ¿ </Button> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | export { default } from './import-modal.vue'; |
| | |
| | | RERANK: 6, // éæ |
| | | }; |
| | | |
| | | /** AI 模åè½åï¼ä¸æ¨¡åç±»åä¸èµ·å³å®ãå¯ç¨å¯ä¸ãåç» */ |
| | | export const AiModelCapabilityEnum = { |
| | | TEXT: 'text', // ææ¬ |
| | | VISION: 'vision', // è§è§ |
| | | }; |
| | | |
| | | export interface ImageModel { |
| | | image?: string; |
| | | key: string; |
| | |
| | | const AI_DICT = { |
| | | AI_GENERATE_MODE: 'ai_generate_mode', // AI çææ¨¡å¼ |
| | | AI_IMAGE_STATUS: 'ai_image_status', // AI å¾çç¶æ |
| | | AI_MODEL_CAPABILITY: 'ai_model_capability', // AI 模åè½å |
| | | AI_MODEL_TYPE: 'ai_model_type', // AI 模åç±»å |
| | | AI_MUSIC_STATUS: 'ai_music_status', // AI é³ä¹ç¶æ |
| | | AI_PLATFORM: 'ai_platform', // AI å¹³å° |
| | |
| | | export function isDocAlertEnable(): boolean { |
| | | return import.meta.env.VITE_APP_DOCALERT_ENABLE !== 'false'; |
| | | } |
| | | |
| | | /** AI åè½æ»å¼å
³ï¼å
³éåéèææ AI å
¥å£ï¼æ¨¡åèåãOCRã颿µãæºè½å¤å®ãæºè½ä½å¯¼å
¥çï¼ */ |
| | | export function isAiEnable(): boolean { |
| | | return import.meta.env.VITE_APP_AI_ENABLE !== 'false'; |
| | | } |
| | |
| | | import type { RouteRecordRaw } from 'vue-router'; |
| | | |
| | | const routes: RouteRecordRaw[] = [ |
| | | import { isAiEnable } from '@vben/hooks'; |
| | | |
| | | /** AI æ»å¼å
³å
³éæ¶æ´ç»è·¯ç±ä¸æ³¨åï¼é¿å
éè¿å°åæ ç´æ¥è®¿é® */ |
| | | const routes: RouteRecordRaw[] = isAiEnable() ? [ |
| | | { |
| | | path: '/ai', |
| | | name: 'Ai', |
| | |
| | | }, |
| | | ], |
| | | }, |
| | | ]; |
| | | ] : []; |
| | | |
| | | export default routes; |
| | |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { LOGIN_PATH } from '@vben/constants'; |
| | | import { isAiEnable } from '@vben/hooks'; |
| | | import { preferences } from '@vben/preferences'; |
| | | import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores'; |
| | | |
| | |
| | | } from '#/api'; |
| | | import { $t } from '#/locales'; |
| | | import { automaticApproval, loadAuditPolicy } from '#/store/audit-policy'; |
| | | |
| | | /** AI æ»å¼å
³å
³éæ¶ï¼è¿å /ai 模åèåä¸å¹¶éè */ |
| | | function filterAiMenus<T extends { children?: T[]; path?: string }>( |
| | | menus: T[], |
| | | ): T[] { |
| | | return menus |
| | | .filter((menu) => { |
| | | const path = menu.path?.replace(/^\//, ''); |
| | | return !(path === 'ai' || path?.startsWith('ai/')); |
| | | }) |
| | | .map((menu) => |
| | | menu.children?.length |
| | | ? { ...menu, children: filterAiMenus(menu.children) } |
| | | : menu, |
| | | ); |
| | | } |
| | | |
| | | export const useAuthStore = defineStore('auth', () => { |
| | | const accessStore = useAccessStore(); |
| | |
| | | userStore.setUserInfo(authPermissionInfo.user); |
| | | userStore.setUserRoles(authPermissionInfo.roles); |
| | | // accessStore |
| | | accessStore.setAccessMenus(authPermissionInfo.menus); |
| | | accessStore.setAccessMenus( |
| | | isAiEnable() ? authPermissionInfo.menus : filterAiMenus(authPermissionInfo.menus), |
| | | ); |
| | | accessStore.setAccessCodes(authPermissionInfo.permissions); |
| | | return authPermissionInfo; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { AgentTaskApi } from '#/api/agent/conversation'; |
| | | |
| | | import { computed, onBeforeUnmount, ref } from 'vue'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | agentImportErrorMessage, |
| | | cancelAgentTask, |
| | | listAgentTasks, |
| | | streamAgentTaskEvents, |
| | | } from '#/api/agent/conversation'; |
| | | |
| | | defineOptions({ name: 'AgentTaskCenterDrawer' }); |
| | | |
| | | interface Emits { |
| | | (event: 'continue', task: AgentTaskApi.Task): void; |
| | | } |
| | | |
| | | const props = defineProps<{ enabled: boolean }>(); |
| | | const emit = defineEmits<Emits>(); |
| | | |
| | | const visible = ref(false); |
| | | const loading = ref(false); |
| | | const tasks = ref<AgentTaskApi.Task[]>([]); |
| | | let pollTimer: ReturnType<typeof setTimeout> | undefined; |
| | | let eventAbort: AbortController | undefined; |
| | | |
| | | const activeCount = computed( |
| | | () => tasks.value.filter((task) => !isTerminal(task.status)).length, |
| | | ); |
| | | |
| | | function isTerminal(status: string) { |
| | | return ['cancelled', 'failed', 'partially_succeeded', 'succeeded'].includes( |
| | | status, |
| | | ); |
| | | } |
| | | |
| | | function statusText(status: string) { |
| | | const values: Record<string, string> = { |
| | | cancelled: '已忶', |
| | | failed: '失败', |
| | | partially_succeeded: 'é¨å宿', |
| | | queued: 'æéä¸', |
| | | retry_wait: 'çå¾
éè¯', |
| | | running: 'æ§è¡ä¸', |
| | | succeeded: '已宿', |
| | | waiting_approval: 'å¾
审æ¹', |
| | | waiting_input: 'å¾
è¡¥å
', |
| | | }; |
| | | return values[status] ?? status; |
| | | } |
| | | |
| | | function statusColor(status: string) { |
| | | if (status === 'succeeded') return 'green'; |
| | | if (status === 'failed' || status === 'cancelled') return 'red'; |
| | | if (status.startsWith('waiting_') || status === 'retry_wait') return 'orange'; |
| | | return 'blue'; |
| | | } |
| | | |
| | | function stageText(stage: string) { |
| | | const values: Record<string, string> = { |
| | | attachment_download: '读åä¼ä¸éä»¶', |
| | | cancelled: 'ä»»å¡å·²åæ¶', |
| | | document_invalid: 'ææ¡£æ ¡éªå¤±è´¥', |
| | | document_parse: 'è§£æä¸ç»æè¯å«', |
| | | evidence_persist: 'ä¿åè§£æè¯æ®', |
| | | failed: 'ä»»å¡å¤±è´¥', |
| | | queued: 'çå¾
Worker', |
| | | reauth_required: 'éè¦éæ°ææ', |
| | | result_ready: 'åæç»æå·²çæ', |
| | | retry_wait: 'çå¾
èªå¨éè¯', |
| | | starting: 'å夿§è¡', |
| | | worker_lost: 'Worker 夿¬¡å¤±è', |
| | | }; |
| | | return values[stage] ?? stage; |
| | | } |
| | | |
| | | function stopPolling() { |
| | | if (pollTimer) clearTimeout(pollTimer); |
| | | pollTimer = undefined; |
| | | eventAbort?.abort(); |
| | | eventAbort = undefined; |
| | | } |
| | | |
| | | async function loadTasks(silent = false) { |
| | | if (!props.enabled) return; |
| | | if (!silent) loading.value = true; |
| | | try { |
| | | const result = await listAgentTasks({ page_size: 50 }); |
| | | tasks.value = result.items ?? []; |
| | | } catch (error) { |
| | | if (!silent) message.error(agentImportErrorMessage(error)); |
| | | } finally { |
| | | if (!silent) loading.value = false; |
| | | } |
| | | } |
| | | |
| | | function schedulePolling() { |
| | | if (!visible.value || !props.enabled) return; |
| | | pollTimer = setTimeout(async () => { |
| | | await loadTasks(true); |
| | | schedulePolling(); |
| | | }, 3000); |
| | | } |
| | | |
| | | async function watchTask(taskId: string, since = 0) { |
| | | eventAbort?.abort(); |
| | | const controller = new AbortController(); |
| | | eventAbort = controller; |
| | | let cursor = since; |
| | | let retryDelay = 1000; |
| | | while (visible.value && !controller.signal.aborted) { |
| | | try { |
| | | cursor = await streamAgentTaskEvents( |
| | | taskId, |
| | | cursor, |
| | | controller.signal, |
| | | (event) => { |
| | | const index = tasks.value.findIndex((task) => task.task_id === taskId); |
| | | if (index >= 0) tasks.value[index] = { ...tasks.value[index], ...event }; |
| | | }, |
| | | ); |
| | | retryDelay = 1000; |
| | | } catch { |
| | | if (controller.signal.aborted) return; |
| | | } |
| | | await loadTasks(true); |
| | | const latest = tasks.value.find((task) => task.task_id === taskId); |
| | | if (!latest || isTerminal(latest.status) || controller.signal.aborted) return; |
| | | cursor = Math.max(cursor, latest.sequence); |
| | | await new Promise<void>((resolve) => { |
| | | const timer = setTimeout(resolve, retryDelay); |
| | | controller.signal.addEventListener( |
| | | 'abort', |
| | | () => { |
| | | clearTimeout(timer); |
| | | resolve(); |
| | | }, |
| | | { once: true }, |
| | | ); |
| | | }); |
| | | retryDelay = Math.min(retryDelay * 2, 30_000); |
| | | } |
| | | } |
| | | |
| | | async function open(taskId?: string) { |
| | | stopPolling(); |
| | | visible.value = true; |
| | | await loadTasks(); |
| | | schedulePolling(); |
| | | const target = taskId |
| | | ? tasks.value.find((task) => task.task_id === taskId) |
| | | : tasks.value.find((task) => !isTerminal(task.status)); |
| | | if (target && !isTerminal(target.status)) void watchTask(target.task_id, target.sequence); |
| | | } |
| | | |
| | | function close() { |
| | | visible.value = false; |
| | | stopPolling(); |
| | | } |
| | | |
| | | async function cancel(task: AgentTaskApi.Task) { |
| | | try { |
| | | await cancelAgentTask(task.task_id); |
| | | message.success('ä»»å¡å·²åæ¶'); |
| | | await loadTasks(true); |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } |
| | | } |
| | | |
| | | function continueTask(task: AgentTaskApi.Task) { |
| | | close(); |
| | | emit('continue', task); |
| | | } |
| | | |
| | | defineExpose({ open, activeCount }); |
| | | onBeforeUnmount(stopPolling); |
| | | </script> |
| | | |
| | | <template> |
| | | <a-drawer |
| | | :open="visible" |
| | | title="åå°ä»»å¡ä¸å¿" |
| | | width="min(560px, 100vw)" |
| | | :body-style="{ padding: '16px' }" |
| | | @close="close" |
| | | > |
| | | <a-alert |
| | | v-if="!enabled" |
| | | type="info" |
| | | show-icon |
| | | message="æä¹
任塿å¡å°æªå¯ç¨" |
| | | description="é
ç½® PostgreSQLãRedis å¹¶å¯ç¨ MOM_TASK_BACKEND åï¼å¯å¨è¿éæ¥çè·¨é¡µé¢æ§è¡çææ¡£ä»»å¡ã" |
| | | /> |
| | | |
| | | <a-spin v-else :spinning="loading"> |
| | | <a-empty v-if="!tasks.length" description="ææ åå°ä»»å¡" /> |
| | | <div v-else class="space-y-3"> |
| | | <article |
| | | v-for="task in tasks" |
| | | :key="task.task_id" |
| | | class="rounded-lg border border-border bg-card p-4 transition-colors duration-200 hover:border-blue-300" |
| | | > |
| | | <div class="flex items-start justify-between gap-3"> |
| | | <div class="min-w-0"> |
| | | <div class="font-medium text-gray-900 dark:text-gray-100"> |
| | | {{ task.result?.filename || 'ä¼ä¸ææ¡£åæ' }} |
| | | </div> |
| | | <div class="mt-1 text-xs text-gray-500"> |
| | | {{ stageText(task.stage) }} · 第 {{ task.attempt }} 次æ§è¡ |
| | | </div> |
| | | </div> |
| | | <a-tag :color="statusColor(task.status)" class="!mr-0 shrink-0"> |
| | | {{ statusText(task.status) }} |
| | | </a-tag> |
| | | </div> |
| | | |
| | | <div class="mt-3" aria-live="polite"> |
| | | <a-progress |
| | | v-if="task.progress.mode === 'determinate'" |
| | | :percent="task.progress.percent ?? 0" |
| | | :status="task.status === 'failed' ? 'exception' : undefined" |
| | | /> |
| | | <div |
| | | v-else |
| | | class="rounded-md border border-dashed border-border bg-gray-50 px-3 py-2 text-xs text-gray-600 dark:bg-gray-800 dark:text-gray-300" |
| | | role="status" |
| | | > |
| | | å½åé¶æ®µå·¥ä½éå¾
ç¡®å® |
| | | </div> |
| | | <div class="mt-1 flex justify-between gap-3 text-xs text-gray-500"> |
| | | <span>{{ task.message }}</span> |
| | | <span v-if="task.progress.total !== null" class="shrink-0"> |
| | | {{ task.progress.completed }} / {{ task.progress.total }} |
| | | {{ task.progress.unit === 'row' ? 'è¡' : '' }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | |
| | | <a-alert |
| | | v-if="task.error?.message" |
| | | class="mt-3" |
| | | type="error" |
| | | :message="task.error.message" |
| | | /> |
| | | |
| | | <div |
| | | v-if="task.result?.candidate_count !== undefined" |
| | | class="mt-3 rounded-md bg-gray-50 px-3 py-2 text-sm dark:bg-gray-800" |
| | | > |
| | | å·²è¯å« {{ task.result.candidate_count }} æ¡åéæ°æ® |
| | | <span v-if="task.result.warnings?.length" class="ml-2 text-orange-600"> |
| | | {{ task.result.warnings.length }} æ¡æç¤º |
| | | </span> |
| | | </div> |
| | | <a-alert |
| | | v-if="task.result?.review_summary?.requires_review" |
| | | class="mt-2" |
| | | type="warning" |
| | | show-icon |
| | | :message="`éè¦äººå·¥å¤æ ¸ï¼${task.result.review_summary.conflict_groups} ç»å²çªï¼${task.result.review_summary.incomplete_keys} æ¡ç¼ºå°å½å¹¶é®`" |
| | | /> |
| | | <div |
| | | v-else-if="task.result?.review_summary?.exact_duplicates_merged" |
| | | class="mt-2 text-xs text-gray-500" |
| | | > |
| | | å·²èªå¨åå¹¶ |
| | | {{ task.result.review_summary.exact_duplicates_merged }} æ¡å®å
¨éå¤è®°å½ |
| | | </div> |
| | | |
| | | <div class="mt-3 flex flex-wrap justify-end gap-2"> |
| | | <a-button |
| | | v-if="task.allowed_actions.includes('cancel')" |
| | | danger |
| | | @click="cancel(task)" |
| | | > |
| | | åæ¶ä»»å¡ |
| | | </a-button> |
| | | <a-button |
| | | v-if=" |
| | | task.status === 'succeeded' && |
| | | task.result?.attachment_id && |
| | | task.result?.continuation_supported !== false |
| | | " |
| | | type="primary" |
| | | @click="continueTask(task)" |
| | | > |
| | | è¿å
¥è¡¥å½ç¡®è®¤ |
| | | </a-button> |
| | | </div> |
| | | </article> |
| | | </div> |
| | | </a-spin> |
| | | </a-drawer> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FileType } from 'ant-design-vue/es/upload/interface'; |
| | | import type { MenuRecordRaw } from '@vben/types'; |
| | | |
| | | import type { AgentConversationApi } from '#/api/agent/conversation'; |
| | | import type { AgentTaskApi } from '#/api/agent/conversation'; |
| | | |
| | | import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | import { useAccessStore } from '@vben/stores'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | agentImportErrorMessage, |
| | | createDocumentTask, |
| | | createAgentConversation, |
| | | deleteAgentConversation, |
| | | downloadAgentConversationAttachment, |
| | | downloadAgentReport, |
| | | getAgentCapabilities, |
| | | getAgentModels, |
| | | getAuthorizedAgentCapabilities, |
| | | getAgentConversation, |
| | | listAgentConversations, |
| | | sendAgentAction, |
| | | sendAgentMessage, |
| | | streamAgentConversationEvents, |
| | | uploadAgentAttachment, |
| | | } from '#/api/agent/conversation'; |
| | | |
| | | import TaskCenterDrawer from './components/TaskCenterDrawer.vue'; |
| | | |
| | | defineOptions({ name: 'AgentDataEntry' }); |
| | | |
| | | const accessStore = useAccessStore(); |
| | | |
| | | const MAX_BYTES = 8 * 1024 * 1024; |
| | | const ACCEPT = '.csv,.xlsx,.docx,.pdf'; |
| | | |
| | | const authorizationScope = ref(''); |
| | | const models = ref<AgentConversationApi.BusinessModelInfo[]>([]); |
| | | const selectedModelId = ref<number>(); |
| | | const conversations = ref<AgentConversationApi.ConversationSummary[]>([]); |
| | | const active = ref<AgentConversationApi.Conversation>(); |
| | | const loading = ref(false); |
| | | const busy = ref(false); |
| | | const creating = ref(false); |
| | | const newOpen = ref(false); |
| | | const newScenarios = ref<string[]>([]); |
| | | const newText = ref(''); |
| | | const newTimeRange = ref<string[]>([]); |
| | | const newRecordLimit = ref<number>(); |
| | | const newIndustry = ref(''); |
| | | const newSkipRestDays = ref(false); |
| | | const newRestDayDescription = ref(''); |
| | | const newRegion = ref(''); |
| | | type ConstraintKey = |
| | | | 'industry' |
| | | | 'instruction' |
| | | | 'record_limit' |
| | | | 'region' |
| | | | 'rest_day_description' |
| | | | 'scenario_paths' |
| | | | 'skip_rest_days' |
| | | | 'time_range'; |
| | | const newPriorities = ref<Record<ConstraintKey, boolean>>({ |
| | | industry: false, |
| | | instruction: false, |
| | | record_limit: false, |
| | | region: false, |
| | | rest_day_description: false, |
| | | scenario_paths: false, |
| | | skip_rest_days: false, |
| | | time_range: false, |
| | | }); |
| | | const newFiles = ref<File[]>([]); |
| | | const newAttachmentIds = ref<number[]>([]); |
| | | const durableTasksEnabled = ref(false); |
| | | const taskCenter = ref<{ open: (taskId?: string) => Promise<void> }>(); |
| | | const interruptFile = ref<File>(); |
| | | const mappingDraft = ref<Record<string, string>>({}); |
| | | const reviseText = ref(''); |
| | | const syncMes = ref(true); |
| | | const eventState = ref<'connected' | 'connecting' | 'idle' | 'retrying'>('idle'); |
| | | interface ExecutionProgress { |
| | | candidateCount: number; |
| | | issueCount: number; |
| | | label: string; |
| | | modelId?: number; |
| | | percent: number; |
| | | stage: string; |
| | | } |
| | | const executionProgress = ref<ExecutionProgress>(); |
| | | let eventAbort: AbortController | undefined; |
| | | let eventGeneration = 0; |
| | | let eventRefreshTimer: ReturnType<typeof setTimeout> | undefined; |
| | | |
| | | const attachmentPreview = ref<{ |
| | | filename: string; |
| | | kind: 'image' | 'pdf'; |
| | | open: boolean; |
| | | url: string; |
| | | }>({ filename: '', kind: 'image', open: false, url: '' }); |
| | | |
| | | const modelOptions = computed(() => |
| | | models.value.map((model) => ({ |
| | | label: `${model.name || model.model} · ${model.platform}`, |
| | | value: model.id, |
| | | })), |
| | | ); |
| | | const interrupt = computed(() => active.value?.interrupts?.at(-1)); |
| | | const card = computed(() => interrupt.value?.card); |
| | | const actions = computed(() => { |
| | | const list = [...(card.value?.actions ?? [])]; |
| | | // å¡çå¯è½å¨æäº¤å¼å
³å
³éæ¶çæï¼ä»¥æå¡ç«¯å½åå¼å
³ä¸ºåè¡¥å确认å¨ä½ |
| | | if ( |
| | | card.value?.kind === 'preview' && |
| | | active.value?.commit_enabled && |
| | | !list.includes('confirm') |
| | | ) { |
| | | list.push('confirm'); |
| | | } |
| | | return list; |
| | | }); |
| | | const isGatewayModule = computed(() => active.value?.module_id === 'mdm.item'); |
| | | const previewValueLabels: Record<string, Record<string, string>> = { |
| | | status: { '0': 'å¯ç¨', '1': 'åç¨' }, |
| | | types: { |
| | | '1': 'IQCï¼æ¥ææ£éªï¼', |
| | | '2': 'IPQCï¼è¿ç¨æ£éªï¼', |
| | | '3': 'OQCï¼åºè´§æ£éªï¼', |
| | | '4': 'RQCï¼éææ£éªï¼', |
| | | }, |
| | | }; |
| | | const previewColumns = computed(() => [ |
| | | { title: 'è¡å·', dataIndex: 'row_index', key: 'row_index', width: 60 }, |
| | | ...(active.value?.row_fields ?? []).map((field) => ({ |
| | | title: field.label, |
| | | dataIndex: field.name, |
| | | key: field.name, |
| | | width: 140, |
| | | customRender: ({ text }: { text: unknown }) => |
| | | previewValueLabels[field.name]?.[String(text ?? '')] ?? text, |
| | | })), |
| | | ]); |
| | | const previewRows = computed(() => |
| | | (active.value?.rows ?? []).map((row, index) => ({ |
| | | candidate_id: row.candidate_id, |
| | | row_index: index + 1, |
| | | ...row.values, |
| | | })), |
| | | ); |
| | | const eventStateText = computed(() => { |
| | | if (active.value?.finished) return '已忥'; |
| | | return { |
| | | connected: '宿¶æ´æ°', |
| | | connecting: 'æ£å¨è¿æ¥', |
| | | idle: 'æªè¿æ¥', |
| | | retrying: 'æçº¿éè¿', |
| | | }[eventState.value]; |
| | | }); |
| | | const eventStateColor = computed(() => { |
| | | if (active.value?.finished || eventState.value === 'connected') return 'green'; |
| | | if (eventState.value === 'retrying') return 'orange'; |
| | | return 'blue'; |
| | | }); |
| | | |
| | | interface ScenarioOption { |
| | | description: string; |
| | | label: string; |
| | | name: string; |
| | | value: string; |
| | | } |
| | | |
| | | function flattenMenuScenarios( |
| | | menus: MenuRecordRaw[], |
| | | parentNames: string[] = [], |
| | | ): ScenarioOption[] { |
| | | return menus.flatMap((menu) => { |
| | | const names = [...parentNames, menu.name].filter(Boolean); |
| | | const current = menu.show === false |
| | | ? [] |
| | | : [ |
| | | { |
| | | description: menu.path, |
| | | label: names.join(' / '), |
| | | name: menu.name, |
| | | value: menu.path, |
| | | }, |
| | | ]; |
| | | return [ |
| | | ...current, |
| | | ...flattenMenuScenarios(menu.children ?? [], names), |
| | | ]; |
| | | }); |
| | | } |
| | | |
| | | const scenarioOptions = computed(() => { |
| | | const seen = new Set<string>(); |
| | | return flattenMenuScenarios(accessStore.accessMenus).filter((item) => { |
| | | if (!item.value || seen.has(item.value)) return false; |
| | | seen.add(item.value); |
| | | return true; |
| | | }); |
| | | }); |
| | | |
| | | function togglePriority(key: ConstraintKey) { |
| | | newPriorities.value[key] = !newPriorities.value[key]; |
| | | } |
| | | |
| | | function buildConstraints(): AgentConversationApi.ConversationConstraints { |
| | | const scenarioByPath = new Map( |
| | | scenarioOptions.value.map((item) => [item.value, item.label]), |
| | | ); |
| | | return { |
| | | industry: newIndustry.value.trim() || undefined, |
| | | instruction: newText.value.trim() || undefined, |
| | | priority_fields: Object.entries(newPriorities.value) |
| | | .filter(([, enabled]) => enabled) |
| | | .map(([key]) => key), |
| | | record_limit: newRecordLimit.value, |
| | | region: newRegion.value.trim() || undefined, |
| | | rest_day_description: newRestDayDescription.value.trim() || undefined, |
| | | scenario_paths: newScenarios.value.map((path) => |
| | | `${scenarioByPath.get(path) ?? path} (${path})`, |
| | | ), |
| | | skip_rest_days: |
| | | newSkipRestDays.value || newPriorities.value.skip_rest_days |
| | | ? newSkipRestDays.value |
| | | : undefined, |
| | | time_end: newTimeRange.value[1], |
| | | time_start: newTimeRange.value[0], |
| | | }; |
| | | } |
| | | |
| | | function hasNewConditions() { |
| | | return Boolean( |
| | | newScenarios.value.length || |
| | | newTimeRange.value.length || |
| | | newRecordLimit.value || |
| | | newIndustry.value.trim() || |
| | | newSkipRestDays.value || |
| | | newRestDayDescription.value.trim() || |
| | | newRegion.value.trim(), |
| | | ); |
| | | } |
| | | |
| | | function resetNewConditions() { |
| | | newScenarios.value = []; |
| | | newText.value = ''; |
| | | newTimeRange.value = []; |
| | | newRecordLimit.value = undefined; |
| | | newIndustry.value = ''; |
| | | newSkipRestDays.value = false; |
| | | newRestDayDescription.value = ''; |
| | | newRegion.value = ''; |
| | | for (const key of Object.keys(newPriorities.value) as ConstraintKey[]) { |
| | | newPriorities.value[key] = false; |
| | | } |
| | | } |
| | | |
| | | watch( |
| | | () => interrupt.value?.interrupt_id, |
| | | () => { |
| | | reviseText.value = ''; |
| | | mappingDraft.value = {}; |
| | | interruptFile.value = undefined; |
| | | const value = card.value?.sync_mes; |
| | | syncMes.value = |
| | | typeof value === 'boolean' |
| | | ? value |
| | | : (active.value?.sync_mes ?? active.value?.mes_sync_default ?? true); |
| | | }, |
| | | ); |
| | | |
| | | function statusText(status?: string) { |
| | | const map: Record<string, string> = { |
| | | SUCCEEDED: '导å
¥æå', |
| | | PARTIAL_FAILED: 'é¨å失败', |
| | | FAILED: '失败', |
| | | UNKNOWN: 'ç»ææªç¥', |
| | | RUNNING: 'æäº¤ä¸', |
| | | attached: 'å·²ä¸ä¼ ', |
| | | blocked: '已黿', |
| | | cancelled: '已忶', |
| | | classified: 'å·²è¯å«åºæ¯', |
| | | commit_disabled: 'æªæäº¤', |
| | | confirmed: '已确认', |
| | | created: 'å¾
ä¸ä¼ æä»¶', |
| | | queued: 'æéå¤çä¸', |
| | | error: 'åºé', |
| | | failed: '失败', |
| | | mapping_applied: 'æ å°å·²æ´æ°', |
| | | needs_review: 'å¾
确认', |
| | | parsed: '已解æ', |
| | | preview_ready: 'å¾
确认', |
| | | revise: 'å¾
éæ°æ ¡éª', |
| | | stale: 'å·²è¿æï¼é鿣', |
| | | validated: 'å¾
确认', |
| | | }; |
| | | return map[status ?? ''] ?? status ?? 'è¿è¡ä¸'; |
| | | } |
| | | |
| | | function statusColor(status?: string) { |
| | | if (status === 'SUCCEEDED' || status === 'confirmed') return 'green'; |
| | | if ( |
| | | status === 'PARTIAL_FAILED' || |
| | | status === 'revise' || |
| | | status === 'stale' |
| | | ) { |
| | | return 'orange'; |
| | | } |
| | | if ( |
| | | status === 'FAILED' || |
| | | status === 'failed' || |
| | | status === 'error' || |
| | | status === 'blocked' || |
| | | status === 'cancelled' |
| | | ) { |
| | | return 'red'; |
| | | } |
| | | return 'blue'; |
| | | } |
| | | |
| | | function checkFile(raw: File) { |
| | | const name = raw.name.toLowerCase(); |
| | | if (!ACCEPT.split(',').some((ext) => name.endsWith(ext))) { |
| | | message.error('ä»
æ¯æ CSVãXLSXãDOCX æ PDF æä»¶'); |
| | | return false; |
| | | } |
| | | if (raw.size > MAX_BYTES) { |
| | | message.error('æä»¶ä¸è½è¶
è¿ 8 MiB'); |
| | | return false; |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | function beforeNewUpload(fileInfo: FileType) { |
| | | const raw = fileInfo as unknown as File; |
| | | if (!checkFile(raw)) return false; |
| | | const duplicate = newFiles.value.some( |
| | | (item) => |
| | | item.name === raw.name && item.size === raw.size && item.lastModified === raw.lastModified, |
| | | ); |
| | | if (!duplicate) newFiles.value = [...newFiles.value, raw].slice(0, 20); |
| | | newAttachmentIds.value = []; |
| | | return false; |
| | | } |
| | | |
| | | function clearNewFiles() { |
| | | newFiles.value = []; |
| | | newAttachmentIds.value = []; |
| | | } |
| | | |
| | | function beforeInterruptUpload(fileInfo: FileType) { |
| | | const raw = fileInfo as unknown as File; |
| | | if (!checkFile(raw)) return false; |
| | | interruptFile.value = raw; |
| | | return false; |
| | | } |
| | | |
| | | async function boot() { |
| | | loading.value = true; |
| | | try { |
| | | const [capabilities, authorization, list, modelList] = await Promise.all([ |
| | | getAgentCapabilities(), |
| | | getAuthorizedAgentCapabilities(), |
| | | listAgentConversations({ page_size: 50 }), |
| | | getAgentModels().catch(() => []), |
| | | ]); |
| | | authorizationScope.value = authorization.scope_summary; |
| | | models.value = modelList; |
| | | if (!selectedModelId.value) { |
| | | selectedModelId.value = active.value?.model_id ?? modelList[0]?.id; |
| | | } |
| | | const visibleScenarioPaths = new Set(scenarioOptions.value.map((item) => item.value)); |
| | | newScenarios.value = newScenarios.value.filter((path) => |
| | | visibleScenarioPaths.has(path), |
| | | ); |
| | | durableTasksEnabled.value = capabilities.durable_tasks?.enabled ?? false; |
| | | conversations.value = list.items ?? []; |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | async function ensureNewAttachments() { |
| | | if (newAttachmentIds.value.length === newFiles.value.length) { |
| | | return newAttachmentIds.value; |
| | | } |
| | | newAttachmentIds.value = []; |
| | | for (const file of newFiles.value) { |
| | | newAttachmentIds.value.push(await uploadAgentAttachment(file)); |
| | | } |
| | | return newAttachmentIds.value; |
| | | } |
| | | |
| | | async function reloadConversations() { |
| | | try { |
| | | const list = await listAgentConversations({ page_size: 50 }); |
| | | conversations.value = list.items ?? []; |
| | | } catch { |
| | | // åè¡¨å·æ°å¤±è´¥ä¸é»å¡ä¸»æµç¨ |
| | | } |
| | | } |
| | | |
| | | function abortEventStream() { |
| | | eventGeneration += 1; |
| | | eventAbort?.abort(); |
| | | eventAbort = undefined; |
| | | if (eventRefreshTimer) clearTimeout(eventRefreshTimer); |
| | | eventRefreshTimer = undefined; |
| | | eventState.value = 'idle'; |
| | | } |
| | | |
| | | async function refreshActive(conversationId: string) { |
| | | const snapshot = await getAgentConversation(conversationId); |
| | | if (active.value?.conversation_id !== conversationId) return; |
| | | active.value = snapshot; |
| | | await reloadConversations(); |
| | | if (snapshot.finished) abortEventStream(); |
| | | } |
| | | |
| | | function queueEventRefresh(conversationId: string) { |
| | | if (eventRefreshTimer) clearTimeout(eventRefreshTimer); |
| | | eventRefreshTimer = setTimeout(() => { |
| | | eventRefreshTimer = undefined; |
| | | void refreshActive(conversationId).catch(() => { |
| | | // SSE ä¼ç»§ç»éè¿ï¼å¿«ç
§è¯»å失败ä¸éå¤å¼¹çªææ°æä½ã |
| | | }); |
| | | }, 120); |
| | | } |
| | | |
| | | function handleConversationEvent( |
| | | conversationId: string, |
| | | event: AgentConversationApi.ConversationEvent, |
| | | ) { |
| | | if (active.value?.conversation_id !== conversationId) return; |
| | | if (event.type === 'progress') { |
| | | const payload = event.payload; |
| | | executionProgress.value = { |
| | | candidateCount: Number(payload.candidate_count ?? 0), |
| | | issueCount: Number(payload.issue_count ?? 0), |
| | | label: String(payload.label ?? 'æ£å¨å¤ç'), |
| | | modelId: |
| | | typeof payload.model_id === 'number' ? payload.model_id : undefined, |
| | | percent: Math.min(100, Math.max(0, Number(payload.percent ?? 0))), |
| | | stage: String(payload.stage ?? 'processing'), |
| | | }; |
| | | return; |
| | | } |
| | | queueEventRefresh(conversationId); |
| | | } |
| | | |
| | | function waitForRetry(delay: number, signal: AbortSignal) { |
| | | return new Promise<void>((resolve) => { |
| | | const timer = setTimeout(resolve, delay); |
| | | signal.addEventListener( |
| | | 'abort', |
| | | () => { |
| | | clearTimeout(timer); |
| | | resolve(); |
| | | }, |
| | | { once: true }, |
| | | ); |
| | | }); |
| | | } |
| | | |
| | | function startEventStream(conversationId: string) { |
| | | abortEventStream(); |
| | | const generation = eventGeneration; |
| | | const controller = new AbortController(); |
| | | eventAbort = controller; |
| | | eventState.value = 'connecting'; |
| | | void (async () => { |
| | | let cursor = 0; |
| | | let retryDelay = 1000; |
| | | while (!controller.signal.aborted && generation === eventGeneration) { |
| | | try { |
| | | cursor = await streamAgentConversationEvents( |
| | | conversationId, |
| | | cursor, |
| | | controller.signal, |
| | | (event) => handleConversationEvent(conversationId, event), |
| | | () => { |
| | | eventState.value = 'connected'; |
| | | retryDelay = 1000; |
| | | }, |
| | | ); |
| | | if (!controller.signal.aborted) eventState.value = 'connecting'; |
| | | } catch { |
| | | if (controller.signal.aborted) return; |
| | | eventState.value = 'retrying'; |
| | | try { |
| | | await refreshActive(conversationId); |
| | | } catch { |
| | | // 轮询å
åºå¤±è´¥åç»§ç»ææ°éé¿ã |
| | | } |
| | | await waitForRetry(retryDelay, controller.signal); |
| | | retryDelay = Math.min(retryDelay * 2, 30_000); |
| | | } |
| | | } |
| | | })(); |
| | | } |
| | | |
| | | async function openConversation(conversationId: string) { |
| | | if (busy.value) return; |
| | | busy.value = true; |
| | | try { |
| | | const conversation = await getAgentConversation(conversationId); |
| | | active.value = conversation; |
| | | selectedModelId.value = conversation.model_id ?? models.value[0]?.id; |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | function changeModel(value: number) { |
| | | selectedModelId.value = value; |
| | | } |
| | | |
| | | async function createConversation() { |
| | | if (!newFiles.value.length && !newText.value.trim() && !hasNewConditions()) { |
| | | message.warning('请ä¸ä¼ æä»¶ãå¡«åè¡¥å½è¯´ææè®¾ç½®è¡¥å½æ¡ä»¶'); |
| | | return; |
| | | } |
| | | creating.value = true; |
| | | try { |
| | | if (newFiles.value.length > 1) { |
| | | message.warning('å¤æä»¶è¯·ä½¿ç¨åå°åæï¼å½å¹¶æ£æ¥å®æååè¿å
¥è¡¥å½'); |
| | | return; |
| | | } |
| | | const [attachmentId] = await ensureNewAttachments(); |
| | | const description = newText.value.trim(); |
| | | const baseTitle = |
| | | newFiles.value[0]?.name || description.slice(0, 60) || 'æ°å»ºè¡¥å½'; |
| | | active.value = await createAgentConversation({ |
| | | attachment_id: attachmentId ? String(attachmentId) : undefined, |
| | | constraints: buildConstraints(), |
| | | model_id: selectedModelId.value, |
| | | text: description || undefined, |
| | | title: baseTitle, |
| | | }); |
| | | newOpen.value = false; |
| | | clearNewFiles(); |
| | | resetNewConditions(); |
| | | await reloadConversations(); |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } finally { |
| | | creating.value = false; |
| | | } |
| | | } |
| | | |
| | | async function startBackgroundAnalysis() { |
| | | if (!durableTasksEnabled.value) { |
| | | message.info('æä¹
任塿å¡å°æªå¯ç¨'); |
| | | return; |
| | | } |
| | | if (!newFiles.value.length) { |
| | | message.warning('åå°åæéè¦å
éæ©æ°æ®æä»¶'); |
| | | return; |
| | | } |
| | | creating.value = true; |
| | | try { |
| | | const attachmentIds = await ensureNewAttachments(); |
| | | if (!attachmentIds.length) return; |
| | | const task = await createDocumentTask({ |
| | | attachment_ids: attachmentIds, |
| | | constraints: buildConstraints(), |
| | | }); |
| | | newOpen.value = false; |
| | | resetNewConditions(); |
| | | message.success('åå°åæä»»å¡å·²å建ï¼å¯å
³é页é¢åç»§ç»æ§è¡'); |
| | | await taskCenter.value?.open(task.task_id); |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } finally { |
| | | creating.value = false; |
| | | } |
| | | } |
| | | |
| | | async function continueFromTask(task: AgentTaskApi.Task) { |
| | | if (task.result?.continuation_supported === false) { |
| | | message.info('å¤æä»¶ç»æéå
å¨è¯æ®æç»ä¸å¤çå²çªï¼å½åæä¸è¿å
¥åæä»¶æäº¤'); |
| | | return; |
| | | } |
| | | const attachmentId = task.result?.attachment_id; |
| | | if (!attachmentId) { |
| | | message.error('ä»»å¡ç»æç¼ºå°éä»¶å¼ç¨ï¼æ æ³è¿å
¥è¡¥å½'); |
| | | return; |
| | | } |
| | | busy.value = true; |
| | | try { |
| | | active.value = await createAgentConversation({ |
| | | attachment_id: String(attachmentId), |
| | | model_id: selectedModelId.value, |
| | | module_id: task.result?.module_id || undefined, |
| | | text: 'åå°ææ¡£åæå·²å®æï¼è¯·ç»§ç»è¿è¡åæ®µæ ¸éªåè¡¥å½ç¡®è®¤ã', |
| | | title: task.result?.filename || 'åå°åæè¡¥å½', |
| | | }); |
| | | await reloadConversations(); |
| | | message.success('å·²è¿å
¥è¡¥å½ç¡®è®¤æµç¨'); |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | async function apply(result: AgentConversationApi.Conversation, tip?: string) { |
| | | active.value = result; |
| | | selectedModelId.value = result.model_id ?? selectedModelId.value; |
| | | if (tip) message.success(tip); |
| | | await reloadConversations(); |
| | | } |
| | | |
| | | async function act( |
| | | action: string, |
| | | payload: Record<string, unknown> = {}, |
| | | tip?: string, |
| | | ) { |
| | | const current = active.value; |
| | | const pending = interrupt.value; |
| | | if (!current || !pending) return; |
| | | busy.value = true; |
| | | try { |
| | | const result = await sendAgentAction(current.conversation_id, { |
| | | action, |
| | | interrupt_id: pending.interrupt_id, |
| | | model_id: selectedModelId.value, |
| | | payload, |
| | | revision: pending.revision, |
| | | }); |
| | | await apply(result, tip); |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | function chooseModule(moduleId: string) { |
| | | return act('choose_scenario', { module_id: moduleId }, '已鿩ä¸å¡åºæ¯'); |
| | | } |
| | | |
| | | async function confirmMapping() { |
| | | const missing = card.value?.missing_fields ?? []; |
| | | const mapping: Record<string, string> = {}; |
| | | for (const field of missing) { |
| | | const column = mappingDraft.value[field.name]; |
| | | if (!column) { |
| | | message.warning(`请为ã${field.label}ã鿩坹åºå`); |
| | | return; |
| | | } |
| | | mapping[field.name] = column; |
| | | } |
| | | await act('confirm_mapping', { mapping }, 'æ å°å·²æäº¤ï¼éæ°æ ¡éª'); |
| | | } |
| | | |
| | | async function uploadForFileInterrupt() { |
| | | const current = active.value; |
| | | if (!current) return; |
| | | if (!interruptFile.value) { |
| | | message.warning('请å
éæ©æä»¶'); |
| | | return; |
| | | } |
| | | busy.value = true; |
| | | try { |
| | | const attachmentId = await uploadAgentAttachment(interruptFile.value); |
| | | const result = await sendAgentMessage(current.conversation_id, { |
| | | attachment_id: String(attachmentId), |
| | | model_id: selectedModelId.value, |
| | | text: 'å·²ä¸ä¼ è¦è¡¥å½çæä»¶ã', |
| | | }); |
| | | interruptFile.value = undefined; |
| | | await apply(result); |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | async function revise() { |
| | | const text = reviseText.value.trim(); |
| | | const payload: Record<string, unknown> = {}; |
| | | if (text) payload.text = text; |
| | | if (isGatewayModule.value) payload.sync_mes = syncMes.value; |
| | | if (!text && !isGatewayModule.value) { |
| | | message.warning('请è¾å
¥ä¿®è®¢å
容ï¼ä¾å¦â第2è¡ åç§° æ¹ä¸º XXXâ'); |
| | | return; |
| | | } |
| | | await act('revise', payload, 'å·²æäº¤ä¿®è®¢ï¼éæ°æ ¡éª'); |
| | | reviseText.value = ''; |
| | | } |
| | | |
| | | async function toggleSync(value: boolean) { |
| | | syncMes.value = value; |
| | | await act('revise', { sync_mes: value }, 'å·²æ´æ°åæ¥é项ï¼éæ°æ ¡éª'); |
| | | } |
| | | |
| | | function confirmImport() { |
| | | return act('confirm', {}, 'å·²ç¡®è®¤ï¼æ£å¨æäº¤'); |
| | | } |
| | | |
| | | function cancelConversation() { |
| | | return act('cancel', {}, 'å·²åæ¶æ¬æ¬¡è¡¥å½'); |
| | | } |
| | | |
| | | async function downloadReport() { |
| | | const current = active.value; |
| | | if (!current) return; |
| | | try { |
| | | const response = await downloadAgentReport(current.conversation_id); |
| | | const disposition = String( |
| | | response.headers?.['content-disposition'] ?? '', |
| | | ); |
| | | const match = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(disposition); |
| | | downloadFileFromBlobPart({ |
| | | fileName: match?.[1] || `${current.title || 'è¡¥å½åæ§'}.xlsx`, |
| | | source: response.data, |
| | | }); |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } |
| | | } |
| | | |
| | | function decodeAttachmentFilename( |
| | | headers: Record<string, string> | undefined, |
| | | fallback: string, |
| | | ) { |
| | | const encoded = headers?.['x-agent-filename-b64']; |
| | | if (!encoded) return fallback; |
| | | try { |
| | | const binary = atob(encoded); |
| | | const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0)); |
| | | return new TextDecoder().decode(bytes); |
| | | } catch { |
| | | return fallback; |
| | | } |
| | | } |
| | | |
| | | function canPreview(filename?: string) { |
| | | return /\.(pdf|png|jpe?g|gif|webp|bmp)$/i.test(filename || ''); |
| | | } |
| | | |
| | | async function previewAttachment( |
| | | attachment: AgentConversationApi.ConversationAttachment, |
| | | ) { |
| | | const current = active.value; |
| | | if (!current) return; |
| | | try { |
| | | const response = await downloadAgentConversationAttachment( |
| | | current.conversation_id, |
| | | attachment.attachment_id, |
| | | 'inline', |
| | | ); |
| | | const filename = decodeAttachmentFilename( |
| | | response.headers, |
| | | attachment.filename || `éä»¶-${attachment.attachment_id}`, |
| | | ); |
| | | closeAttachmentPreview(); |
| | | attachmentPreview.value = { |
| | | filename, |
| | | kind: /\.pdf$/i.test(filename) ? 'pdf' : 'image', |
| | | open: true, |
| | | url: URL.createObjectURL(response.data), |
| | | }; |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } |
| | | } |
| | | |
| | | function closeAttachmentPreview() { |
| | | if (attachmentPreview.value.url) URL.revokeObjectURL(attachmentPreview.value.url); |
| | | attachmentPreview.value = { filename: '', kind: 'image', open: false, url: '' }; |
| | | } |
| | | |
| | | async function downloadAttachment( |
| | | attachment: AgentConversationApi.ConversationAttachment, |
| | | ) { |
| | | const current = active.value; |
| | | if (!current) return; |
| | | try { |
| | | const response = await downloadAgentConversationAttachment( |
| | | current.conversation_id, |
| | | attachment.attachment_id, |
| | | 'download', |
| | | ); |
| | | downloadFileFromBlobPart({ |
| | | fileName: decodeAttachmentFilename( |
| | | response.headers, |
| | | attachment.filename || `éä»¶-${attachment.attachment_id}`, |
| | | ), |
| | | source: response.data, |
| | | }); |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } |
| | | } |
| | | |
| | | async function removeConversation( |
| | | item: AgentConversationApi.ConversationSummary, |
| | | ) { |
| | | try { |
| | | await deleteAgentConversation(item.conversation_id); |
| | | if (active.value?.conversation_id === item.conversation_id) { |
| | | abortEventStream(); |
| | | active.value = undefined; |
| | | } |
| | | await reloadConversations(); |
| | | message.success('ä¼è¯åéä»¶å·²å é¤'); |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } |
| | | } |
| | | |
| | | onMounted(boot); |
| | | onBeforeUnmount(() => { |
| | | abortEventStream(); |
| | | closeAttachmentPreview(); |
| | | }); |
| | | |
| | | watch( |
| | | () => active.value?.conversation_id, |
| | | (conversationId) => { |
| | | executionProgress.value = undefined; |
| | | if (conversationId && !active.value?.finished) startEventStream(conversationId); |
| | | else abortEventStream(); |
| | | }, |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <div class="flex h-full gap-3 p-3"> |
| | | <aside |
| | | class="flex w-64 shrink-0 flex-col overflow-hidden rounded-lg border border-border bg-card" |
| | | > |
| | | <div class="border-b border-border p-3"> |
| | | <a-button type="primary" block @click="newOpen = true"> |
| | | æ°å»ºè¡¥å½ |
| | | </a-button> |
| | | <div class="mt-2 text-xs text-gray-500"> |
| | | ä¸ä¼ æä»¶åèªå¨è¯å«ä¸å¡åºæ¯ï¼ç¡®è®¤åä¼å®æ´æ ¡éªã |
| | | </div> |
| | | </div> |
| | | <div class="flex-1 overflow-y-auto p-2"> |
| | | <a-spin :spinning="loading"> |
| | | <a-empty |
| | | v-if="!conversations.length" |
| | | description="ææ è¡¥å½ä¼è¯" |
| | | /> |
| | | <div |
| | | v-for="item in conversations" |
| | | :key="item.conversation_id" |
| | | class="mb-1 flex w-full cursor-pointer items-start gap-1 rounded-md p-2 text-left transition-colors hover:bg-gray-100 dark:hover:bg-gray-800" |
| | | :class="{ |
| | | 'bg-blue-50 dark:bg-blue-900/30': |
| | | item.conversation_id === active?.conversation_id, |
| | | }" |
| | | role="button" |
| | | tabindex="0" |
| | | @click="openConversation(item.conversation_id)" |
| | | @keydown.enter="openConversation(item.conversation_id)" |
| | | > |
| | | <div class="min-w-0 flex-1"> |
| | | <div class="truncate text-sm font-medium"> |
| | | {{ item.title || 'æªå½åè¡¥å½' }} |
| | | </div> |
| | | <div class="mt-1 flex items-center gap-2 text-xs text-gray-500"> |
| | | <a-tag :color="statusColor(item.status)" class="!mr-0"> |
| | | {{ statusText(item.status) }} |
| | | </a-tag> |
| | | <span class="truncate"> |
| | | {{ item.module_title || 'å¾
è¯å«' }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | <a-popconfirm |
| | | cancel-text="åæ¶" |
| | | ok-text="å é¤" |
| | | title="å é¤ä¼è¯åå·²ä¸ä¼ éä»¶ï¼" |
| | | @confirm="removeConversation(item)" |
| | | > |
| | | <a-button |
| | | class="shrink-0" |
| | | danger |
| | | size="small" |
| | | type="text" |
| | | @click.stop |
| | | > |
| | | å é¤ |
| | | </a-button> |
| | | </a-popconfirm> |
| | | </div> |
| | | </a-spin> |
| | | </div> |
| | | </aside> |
| | | |
| | | <section |
| | | class="flex min-w-0 flex-1 flex-col overflow-hidden rounded-lg border border-border bg-card" |
| | | > |
| | | <header |
| | | class="flex items-center justify-between border-b border-border px-4 py-3" |
| | | > |
| | | <div class="min-w-0"> |
| | | <div class="truncate text-base font-semibold"> |
| | | {{ active?.title || 'æ¨¡åæ°æ®è¡¥å½' }} |
| | | </div> |
| | | <div class="mt-1 flex items-center gap-2 text-xs text-gray-500"> |
| | | <a-tag :color="statusColor(active?.status)" class="!mr-0"> |
| | | {{ statusText(active?.status) }} |
| | | </a-tag> |
| | | <span v-if="active?.module_title">{{ active.module_title }}</span> |
| | | <span v-if="active?.total">å
± {{ active.total }} è¡</span> |
| | | <span v-if="active?.data_scope_summary || authorizationScope"> |
| | | æ°æ®èå´ï¼{{ active?.data_scope_summary || authorizationScope }} |
| | | </span> |
| | | <span |
| | | v-if="modelOptions.length" |
| | | class="flex items-center gap-1" |
| | | title="忢åä»ä¸ä¸æ¬¡åæå¼å§çæ" |
| | | > |
| | | 模åï¼ |
| | | <a-select |
| | | :disabled="busy" |
| | | :options="modelOptions" |
| | | :value="selectedModelId" |
| | | class="w-56" |
| | | size="small" |
| | | @change="changeModel" |
| | | /> |
| | | </span> |
| | | <a-tag |
| | | v-if="active" |
| | | :color="eventStateColor" |
| | | class="!mr-0" |
| | | aria-live="polite" |
| | | > |
| | | {{ eventStateText }} |
| | | </a-tag> |
| | | </div> |
| | | <div |
| | | v-if="active?.attachments?.length" |
| | | class="mt-1 flex flex-wrap items-center gap-2 text-xs text-gray-500" |
| | | > |
| | | <span>éä»¶ï¼</span> |
| | | <span |
| | | v-for="attachment in active.attachments" |
| | | :key="attachment.attachment_id" |
| | | class="flex items-center gap-1 rounded border border-border px-2 py-0.5" |
| | | > |
| | | <span class="max-w-60 truncate"> |
| | | {{ attachment.filename || `éä»¶ ${attachment.attachment_id}` }} |
| | | </span> |
| | | <a-button |
| | | v-if="canPreview(attachment.filename)" |
| | | class="!h-5 !px-1" |
| | | size="small" |
| | | type="link" |
| | | @click="previewAttachment(attachment)" |
| | | > |
| | | é¢è§ |
| | | </a-button> |
| | | <a-button |
| | | class="!h-5 !px-1" |
| | | size="small" |
| | | type="link" |
| | | @click="downloadAttachment(attachment)" |
| | | > |
| | | ä¸è½½ |
| | | </a-button> |
| | | </span> |
| | | </div> |
| | | </div> |
| | | <div class="flex shrink-0 items-center gap-2"> |
| | | <a-tooltip |
| | | :title=" |
| | | durableTasksEnabled |
| | | ? 'æ¥çè·¨é¡µé¢æ§è¡çææ¡£ä»»å¡' |
| | | : 'æä¹
任塿å¡å°æªå¯ç¨' |
| | | " |
| | | > |
| | | <a-button @click="taskCenter?.open()">åå°ä»»å¡</a-button> |
| | | </a-tooltip> |
| | | <a-button |
| | | v-if="active?.receipt?.command_id" |
| | | :disabled="busy" |
| | | @click="downloadReport" |
| | | > |
| | | ä¸è½½åæ§ |
| | | </a-button> |
| | | </div> |
| | | </header> |
| | | |
| | | <div |
| | | v-if="executionProgress && active" |
| | | class="border-b border-border bg-blue-50/70 px-4 py-2 dark:bg-blue-950/20" |
| | | aria-live="polite" |
| | | > |
| | | <div class="mb-1 flex items-center justify-between gap-4 text-xs"> |
| | | <span class="font-medium text-blue-700 dark:text-blue-300"> |
| | | {{ executionProgress.label }} |
| | | </span> |
| | | <span class="text-gray-500"> |
| | | å·²åç° {{ executionProgress.candidateCount }} æ¡åé |
| | | <template v-if="executionProgress.issueCount"> |
| | | · {{ executionProgress.issueCount }} 项å¾
æ ¸å¯¹ |
| | | </template> |
| | | </span> |
| | | </div> |
| | | <a-progress |
| | | :percent="executionProgress.percent" |
| | | :status="executionProgress.stage === 'error' ? 'exception' : 'active'" |
| | | size="small" |
| | | /> |
| | | </div> |
| | | |
| | | <div class="flex-1 overflow-y-auto p-4"> |
| | | <a-empty |
| | | v-if="!active" |
| | | description="ä»å·¦ä¾§éæ©ä¼è¯ï¼æç¹å»ãæ°å»ºè¡¥å½ãå¼å§" |
| | | /> |
| | | <div v-else class="space-y-3"> |
| | | <div |
| | | v-for="item in active.messages" |
| | | :key="item.message_id" |
| | | class="flex" |
| | | :class="item.role === 'user' ? 'justify-end' : 'justify-start'" |
| | | > |
| | | <div |
| | | class="max-w-[80%] rounded-lg px-3 py-2 text-sm" |
| | | :class=" |
| | | item.role === 'user' |
| | | ? 'bg-blue-500 text-white' |
| | | : 'bg-gray-100 dark:bg-gray-800' |
| | | " |
| | | > |
| | | <div class="whitespace-pre-wrap">{{ item.text }}</div> |
| | | <div |
| | | v-for="(msgCard, index) in item.cards || []" |
| | | :key="`${item.message_id}-${index}`" |
| | | class="mt-2 rounded-md border border-border bg-card p-2 text-gray-800 dark:text-gray-100" |
| | | > |
| | | <template v-if="msgCard.kind === 'receipt'"> |
| | | <div class="mb-1 font-semibold"> |
| | | {{ msgCard.title || '导å
¥åæ§' }} |
| | | </div> |
| | | <div class="text-xs text-gray-500"> |
| | | æå {{ msgCard.summary?.success ?? 0 }}ï¼è·³è¿ |
| | | {{ msgCard.summary?.skipped ?? 0 }}ï¼å¤±è´¥ |
| | | {{ msgCard.summary?.failed ?? 0 }} |
| | | </div> |
| | | <a-table |
| | | class="mt-2" |
| | | :columns="[ |
| | | { title: 'ç¼ç ', dataIndex: 'code', width: 160 }, |
| | | { title: 'åç§°', dataIndex: 'name' }, |
| | | { title: 'ç»æ', dataIndex: 'status', width: 90 }, |
| | | ]" |
| | | :data-source="msgCard.rows || []" |
| | | :pagination="false" |
| | | row-key="candidate_id" |
| | | size="small" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <a-tag |
| | | v-if="column.dataIndex === 'status'" |
| | | :color="record.status === 'SUCCESS' ? 'green' : 'red'" |
| | | > |
| | | {{ record.status }} |
| | | </a-tag> |
| | | </template> |
| | | </a-table> |
| | | <a-button |
| | | class="mt-2" |
| | | size="small" |
| | | type="link" |
| | | @click="downloadReport" |
| | | > |
| | | ä¸è½½æ¥è¡¨ |
| | | </a-button> |
| | | </template> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <footer v-if="active && interrupt" class="border-t border-border p-3"> |
| | | <a-spin :spinning="busy"> |
| | | <!-- åºæ¯éæ© --> |
| | | <div v-if="interrupt.kind === 'scenario'"> |
| | | <div class="mb-2 font-medium">{{ card?.title }}</div> |
| | | <div class="mb-2 text-xs text-gray-500">{{ card?.body }}</div> |
| | | <div class="flex flex-wrap gap-2"> |
| | | <a-button |
| | | v-for="candidate in card?.candidates || []" |
| | | :key="candidate.module_id" |
| | | :disabled="busy" |
| | | @click="chooseModule(candidate.module_id)" |
| | | > |
| | | {{ candidate.title }} |
| | | <a-tag class="ml-1" color="blue"> |
| | | {{ candidate.capability }} |
| | | </a-tag> |
| | | </a-button> |
| | | <a-button |
| | | v-if="actions.includes('cancel')" |
| | | danger |
| | | :disabled="busy" |
| | | @click="cancelConversation" |
| | | > |
| | | åæ¶ |
| | | </a-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- åæ®µæ å° --> |
| | | <div v-else-if="interrupt.kind === 'mapping'"> |
| | | <div class="mb-2 font-medium">{{ card?.title }}</div> |
| | | <div class="mb-2 text-xs text-gray-500">{{ card?.body }}</div> |
| | | <div class="space-y-2"> |
| | | <div |
| | | v-for="field in card?.missing_fields || []" |
| | | :key="field.name" |
| | | class="flex items-center gap-2" |
| | | > |
| | | <span class="w-28 text-sm">{{ field.label }}</span> |
| | | <a-select |
| | | v-model:value="mappingDraft[field.name]" |
| | | class="min-w-56" |
| | | placeholder="éæ©æä»¶ä¸å¯¹åºçå" |
| | | :options=" |
| | | (card?.columns || []).map((column) => ({ |
| | | label: column, |
| | | value: column, |
| | | })) |
| | | " |
| | | /> |
| | | </div> |
| | | </div> |
| | | <div class="mt-3 flex gap-2"> |
| | | <a-button |
| | | type="primary" |
| | | :loading="busy" |
| | | @click="confirmMapping" |
| | | > |
| | | 确认æ å° |
| | | </a-button> |
| | | <a-button danger :disabled="busy" @click="cancelConversation"> |
| | | åæ¶ |
| | | </a-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- è¡¥ä¼ æä»¶ --> |
| | | <div v-else-if="interrupt.kind === 'file'"> |
| | | <div class="mb-2 font-medium">{{ card?.title }}</div> |
| | | <div class="mb-2 text-xs text-gray-500">{{ card?.body }}</div> |
| | | <a-upload |
| | | accept=".csv,.xlsx,.docx,.pdf" |
| | | :before-upload="beforeInterruptUpload" |
| | | :max-count="1" |
| | | :show-upload-list="false" |
| | | > |
| | | <a-button>{{ interruptFile ? 'éæ°éæ©' : 'éæ©æä»¶' }}</a-button> |
| | | </a-upload> |
| | | <span v-if="interruptFile" class="ml-2 text-xs text-gray-500"> |
| | | {{ interruptFile.name }} |
| | | </span> |
| | | <div class="mt-3"> |
| | | <a-button |
| | | type="primary" |
| | | :disabled="!interruptFile" |
| | | :loading="busy" |
| | | @click="uploadForFileInterrupt" |
| | | > |
| | | ä¸ä¼ å¹¶ç»§ç» |
| | | </a-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- æ ¡éª / é¢è§ç¡®è®¤ --> |
| | | <div v-else-if="['preview', 'validation'].includes(interrupt.kind)"> |
| | | <div class="mb-2 flex items-center justify-between"> |
| | | <span class="font-medium">{{ card?.title }}</span> |
| | | <span class="text-xs text-gray-500"> |
| | | {{ card?.notice }} |
| | | </span> |
| | | </div> |
| | | |
| | | <a-alert |
| | | v-if="card?.issues?.length" |
| | | class="mb-2" |
| | | type="error" |
| | | show-icon |
| | | > |
| | | <template #message> |
| | | 黿项 {{ card.issues.length }} æ¡ï¼é修订åéæ°æ ¡éª |
| | | </template> |
| | | <ul class="mt-1 list-decimal pl-4 text-xs"> |
| | | <li v-for="(issue, index) in card.issues" :key="index"> |
| | | {{ issue.message }} |
| | | <span v-if="issue.code" class="text-gray-400"> |
| | | ï¼{{ issue.code }}ï¼ |
| | | </span> |
| | | </li> |
| | | </ul> |
| | | </a-alert> |
| | | |
| | | <a-alert |
| | | v-if="card?.warnings?.length" |
| | | class="mb-2" |
| | | type="warning" |
| | | show-icon |
| | | > |
| | | <template #message> |
| | | æç¤º {{ card.warnings.length }} æ¡ï¼ä¸é»ææäº¤ |
| | | </template> |
| | | <ul class="mt-1 list-decimal pl-4 text-xs"> |
| | | <li v-for="(item, index) in card.warnings" :key="index"> |
| | | {{ item.message }} |
| | | </li> |
| | | </ul> |
| | | </a-alert> |
| | | |
| | | <a-alert |
| | | v-if="card?.repair_guidance?.length" |
| | | class="mb-2" |
| | | type="info" |
| | | show-icon |
| | | > |
| | | <template #message>ä¸å¡å¤§æ¨¡åæ ¸å¯¹å»ºè®®</template> |
| | | <ul class="mt-1 list-disc pl-4 text-xs"> |
| | | <li v-for="(item, index) in card.repair_guidance" :key="index"> |
| | | <span v-if="item.field" class="font-medium">{{ item.field }}ï¼</span> |
| | | {{ item.guidance }} |
| | | </li> |
| | | </ul> |
| | | </a-alert> |
| | | |
| | | <div |
| | | v-if="card?.summary" |
| | | class="mb-2 flex flex-wrap gap-3 text-xs text-gray-600 dark:text-gray-300" |
| | | > |
| | | <span>å
± {{ card.summary.total ?? 0 }} è¡</span> |
| | | <span class="text-green-600"> |
| | | æ°å»º {{ card.summary.create ?? 0 }} |
| | | </span> |
| | | <span class="text-orange-500"> |
| | | è·³è¿ {{ card.summary.skip ?? 0 }} |
| | | </span> |
| | | <span class="text-red-500"> |
| | | 黿 {{ card.summary.blocked ?? 0 }} |
| | | </span> |
| | | </div> |
| | | |
| | | <div v-if="card?.side_effects?.length" class="mb-2 text-xs text-gray-500"> |
| | | 颿影åï¼{{ card.side_effects.join('ï¼') }} |
| | | </div> |
| | | |
| | | <div |
| | | v-if="isGatewayModule" |
| | | class="mb-2 flex items-center gap-2 text-sm" |
| | | > |
| | | <span>åæ¥å° MES</span> |
| | | <a-switch |
| | | :checked="syncMes" |
| | | :disabled="busy" |
| | | @change="toggleSync" |
| | | /> |
| | | <span class="text-xs text-gray-500"> |
| | | å
³éåä»
å建 ERP ç©æ |
| | | </span> |
| | | </div> |
| | | |
| | | <a-table |
| | | v-if="previewRows.length" |
| | | class="mb-2" |
| | | :columns="previewColumns" |
| | | :data-source="previewRows" |
| | | :pagination="false" |
| | | :scroll="{ x: 'max-content', y: 320 }" |
| | | row-key="candidate_id" |
| | | size="small" |
| | | bordered |
| | | /> |
| | | |
| | | <div class="flex gap-2"> |
| | | <a-input |
| | | v-model:value="reviseText" |
| | | class="flex-1" |
| | | placeholder="å¦éä¿®æ¹ï¼è¾å
¥ä¿®è®¢è¯´æï¼ä¾å¦ï¼ç¬¬2è¡ åç§° æ¹ä¸º XXX" |
| | | :disabled="busy" |
| | | @press-enter="revise" |
| | | /> |
| | | <a-button :disabled="busy" @click="revise">ä¿®è®¢å¹¶éæ£</a-button> |
| | | <a-button |
| | | v-if="actions.includes('confirm')" |
| | | type="primary" |
| | | :disabled="busy || card?.can_commit === false" |
| | | :loading="busy" |
| | | @click="confirmImport" |
| | | > |
| | | 确认导å
¥ |
| | | </a-button> |
| | | <a-button |
| | | v-if="actions.includes('cancel')" |
| | | danger |
| | | :disabled="busy" |
| | | @click="cancelConversation" |
| | | > |
| | | åæ¶ |
| | | </a-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <a-alert |
| | | v-else |
| | | type="info" |
| | | show-icon |
| | | :message="card?.title || 'å½åæ¥éª¤éè¦å¤ç'" |
| | | :description="card?.body" |
| | | /> |
| | | </a-spin> |
| | | </footer> |
| | | </section> |
| | | |
| | | <TaskCenterDrawer |
| | | ref="taskCenter" |
| | | :enabled="durableTasksEnabled" |
| | | @continue="continueFromTask" |
| | | /> |
| | | |
| | | <a-modal |
| | | v-model:open="attachmentPreview.open" |
| | | :footer="null" |
| | | :title="attachmentPreview.filename" |
| | | width="80%" |
| | | @cancel="closeAttachmentPreview" |
| | | > |
| | | <iframe |
| | | v-if="attachmentPreview.kind === 'pdf'" |
| | | :src="attachmentPreview.url" |
| | | class="h-[70vh] w-full border-0" |
| | | title="éä»¶é¢è§" |
| | | /> |
| | | <img |
| | | v-else |
| | | :src="attachmentPreview.url" |
| | | alt="éä»¶é¢è§" |
| | | class="max-h-[70vh] w-full object-contain" |
| | | /> |
| | | </a-modal> |
| | | |
| | | <a-modal |
| | | v-model:open="newOpen" |
| | | title="æ°å»ºè¡¥å½" |
| | | :confirm-loading="creating" |
| | | :width="760" |
| | | > |
| | | <div class="space-y-3"> |
| | | <div> |
| | | <div class="mb-1 flex items-center justify-between text-sm"> |
| | | <span>ä¸å¡åºæ¯ï¼å¯å¤éï¼</span> |
| | | <a-button |
| | | size="small" |
| | | :type="newPriorities.scenario_paths ? 'primary' : 'default'" |
| | | @click="togglePriority('scenario_paths')" |
| | | >ä¼å
</a-button> |
| | | </div> |
| | | <a-select |
| | | v-model:value="newScenarios" |
| | | class="w-full" |
| | | mode="multiple" |
| | | :max-tag-count="3" |
| | | :options="scenarioOptions" |
| | | option-filter-prop="label" |
| | | placeholder="è¯·éæ©å½åè¡¥å½æ¶åçè忍¡å" |
| | | show-search |
| | | > |
| | | <template #option="option"> |
| | | <div class="py-1"> |
| | | <div class="font-medium">{{ option.label }}</div> |
| | | <div v-if="option.description" class="mt-0.5 text-xs text-gray-500"> |
| | | {{ option.description }} |
| | | </div> |
| | | </div> |
| | | </template> |
| | | </a-select> |
| | | <div class="mt-1 text-xs text-gray-500"> |
| | | å¼å¯âä¼å
âåæç¡¬çº¦æå¤çï¼æç»æ¨¡åä»å¿
é¡»éè¿è½å䏿°æ®æéæ ¡éªã |
| | | </div> |
| | | </div> |
| | | <div class="grid grid-cols-1 gap-3 md:grid-cols-2"> |
| | | <div> |
| | | <div class="mb-1 flex items-center justify-between text-sm"> |
| | | <span>æ¶é´èå´</span> |
| | | <a-button size="small" :type="newPriorities.time_range ? 'primary' : 'default'" @click="togglePriority('time_range')">ä¼å
</a-button> |
| | | </div> |
| | | <a-range-picker v-model:value="newTimeRange" class="w-full" value-format="YYYY-MM-DD" /> |
| | | </div> |
| | | <div> |
| | | <div class="mb-1 flex items-center justify-between text-sm"> |
| | | <span>æ°æ®æ¡æ°</span> |
| | | <a-button size="small" :type="newPriorities.record_limit ? 'primary' : 'default'" @click="togglePriority('record_limit')">ä¼å
</a-button> |
| | | </div> |
| | | <a-input-number v-model:value="newRecordLimit" class="w-full" :min="1" :max="100000" placeholder="ä¾å¦ï¼500" /> |
| | | </div> |
| | | <div> |
| | | <div class="mb-1 flex items-center justify-between text-sm"> |
| | | <span>è¡ä¸</span> |
| | | <a-button size="small" :type="newPriorities.industry ? 'primary' : 'default'" @click="togglePriority('industry')">ä¼å
</a-button> |
| | | </div> |
| | | <a-input v-model:value="newIndustry" maxlength="80" placeholder="ä¾å¦ï¼æ±½è½¦é¶é¨ä»¶" /> |
| | | </div> |
| | | <div> |
| | | <div class="mb-1 flex items-center justify-between text-sm"> |
| | | <span>åºå</span> |
| | | <a-button size="small" :type="newPriorities.region ? 'primary' : 'default'" @click="togglePriority('region')">ä¼å
</a-button> |
| | | </div> |
| | | <a-input v-model:value="newRegion" maxlength="120" placeholder="ä¾å¦ï¼åä¸ / æ±èåé" /> |
| | | </div> |
| | | <div> |
| | | <div class="mb-1 flex items-center justify-between text-sm"> |
| | | <span>è·³è¿ä¼æ¯æ¥</span> |
| | | <a-button size="small" :type="newPriorities.skip_rest_days ? 'primary' : 'default'" @click="togglePriority('skip_rest_days')">ä¼å
</a-button> |
| | | </div> |
| | | <div class="flex h-8 items-center gap-2"> |
| | | <a-switch v-model:checked="newSkipRestDays" /> |
| | | <span class="text-xs text-gray-500">{{ newSkipRestDays ? 'è·³è¿' : 'ä¸è·³è¿' }}</span> |
| | | </div> |
| | | </div> |
| | | <div> |
| | | <div class="mb-1 flex items-center justify-between text-sm"> |
| | | <span>伿¯æ¥æè¿°</span> |
| | | <a-button size="small" :type="newPriorities.rest_day_description ? 'primary' : 'default'" @click="togglePriority('rest_day_description')">ä¼å
</a-button> |
| | | </div> |
| | | <a-input v-model:value="newRestDayDescription" maxlength="300" placeholder="ä¾å¦ï¼å¨æ¥åæ³å®è忥" /> |
| | | </div> |
| | | </div> |
| | | <div> |
| | | <div class="mb-1 flex items-center justify-between text-sm"> |
| | | <span>è¡¥å½è¯´æï¼å¯éï¼</span> |
| | | <a-button size="small" :type="newPriorities.instruction ? 'primary' : 'default'" @click="togglePriority('instruction')">ä¼å
</a-button> |
| | | </div> |
| | | <a-textarea |
| | | v-model:value="newText" |
| | | :rows="2" |
| | | placeholder="ä¾å¦ï¼å¯¼å
¥ä¸æ¹ä¾åºåæ¡£æ¡" |
| | | /> |
| | | </div> |
| | | <div> |
| | | <div class="mb-1 text-sm">æ°æ®æä»¶</div> |
| | | <a-upload |
| | | accept=".csv,.xlsx,.docx,.pdf" |
| | | :before-upload="beforeNewUpload" |
| | | :max-count="20" |
| | | multiple |
| | | :show-upload-list="false" |
| | | > |
| | | <a-button>éæ©æä»¶</a-button> |
| | | </a-upload> |
| | | <span v-if="newFiles.length" class="ml-2 text-xs text-gray-500"> |
| | | 已鿩 {{ newFiles.length }} 个æä»¶ |
| | | </span> |
| | | <a-button v-if="newFiles.length" class="ml-2" size="small" type="link" @click="clearNewFiles"> |
| | | æ¸
空 |
| | | </a-button> |
| | | <div v-if="newFiles.length" class="mt-2 max-h-20 overflow-auto text-xs text-gray-500"> |
| | | <div v-for="file in newFiles" :key="`${file.name}:${file.size}:${file.lastModified}`"> |
| | | {{ file.name }} |
| | | </div> |
| | | </div> |
| | | <div class="mt-1 text-xs text-gray-500"> |
| | | æ¯æä¸æ¬¡éæ©æå¤ 20 个 CSVãXLSXãDOCX ä¸å¯å¤å¶ææ¬ç PDFï¼å个æä»¶ä¸è¶
è¿ 8 |
| | | MiBã夿件ä¼å¨åå°åè·¨æä»¶éå¤åå²çªæ£æ¥ï¼æ«æä»¶æä¸æ¯æã |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <template #footer> |
| | | <div class="flex flex-wrap justify-end gap-2"> |
| | | <a-button :disabled="creating" @click="newOpen = false"> |
| | | åæ¶ |
| | | </a-button> |
| | | <a-tooltip |
| | | :title=" |
| | | !durableTasksEnabled |
| | | ? 'éå
å¯ç¨ PostgreSQL ä¸ Redis æä¹
任塿å¡' |
| | | : 'ä»»å¡ä¼æ ¹æ®æä»¶å
容èªå¨è¯å«ï¼å
³é页é¢åä»å¯ä»ä»»å¡ä¸å¿æ¢å¤' |
| | | " |
| | | > |
| | | <a-button |
| | | :disabled="!durableTasksEnabled || !newFiles.length" |
| | | :loading="creating" |
| | | @click="startBackgroundAnalysis" |
| | | > |
| | | åå°åæ |
| | | </a-button> |
| | | </a-tooltip> |
| | | <a-button |
| | | type="primary" |
| | | :loading="creating" |
| | | @click="createConversation" |
| | | > |
| | | ç´æ¥å¼å§è¡¥å½ |
| | | </a-button> |
| | | </div> |
| | | </template> |
| | | </a-modal> |
| | | </div> |
| | | </Page> |
| | | </template> |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | |
| | | import { AiModelTypeEnum, CommonStatusEnum, DICT_TYPE } from '@vben/constants'; |
| | | import type { AiModelModelApi } from '#/api/ai/model/model'; |
| | | |
| | | import { AiModelCapabilityEnum, AiModelTypeEnum, CommonStatusEnum, DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { z } from '#/adapter/form'; |
| | |
| | | }; |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'capability', |
| | | label: '模åè½å', |
| | | component: 'Select', |
| | | componentProps: (values) => { |
| | | return { |
| | | placeholder: 'è¯·éæ©æ¨¡åè½å', |
| | | // åªæå¯¹è¯ç±»ååºåææ¬/è§è§ï¼å
¶ä½ç±»åç»ä¸æææ¬è½åå¤ç |
| | | disabled: !!values.id || values.type !== AiModelTypeEnum.CHAT, |
| | | options: getDictOptions(DICT_TYPE.AI_MODEL_CAPABILITY, 'string'), |
| | | allowClear: true, |
| | | }; |
| | | }, |
| | | help: 'åä¸ç±»ååä¸è½ååªè½å¯ç¨ä¸ä¸ªæ¨¡åï¼è§è§è½åç¨äºå¾çè¯å«', |
| | | dependencies: { |
| | | triggerFields: ['type'], |
| | | show: (values) => values.type === AiModelTypeEnum.CHAT, |
| | | }, |
| | | rules: z.string().default(AiModelCapabilityEnum.TEXT), |
| | | }, |
| | | { |
| | | fieldName: 'keyId', |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'type', |
| | | label: '模åç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©æ¨¡åç±»å', |
| | | options: getDictOptions(DICT_TYPE.AI_MODEL_TYPE, 'number'), |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'model', |
| | | label: 'æ¨¡åæ è¯', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥æ¨¡åæ è¯', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'capability', |
| | | label: '模åè½å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©æ¨¡åè½å', |
| | | options: getDictOptions(DICT_TYPE.AI_MODEL_CAPABILITY, 'string'), |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | |
| | | } |
| | | |
| | | /** å表çåæ®µ */ |
| | | export function useGridColumns(): VxeTableGridOptions['columns'] { |
| | | export function useGridColumns( |
| | | onStatusChange?: (newStatus: number, row: AiModelModelApi.Model) => Promise<boolean | undefined>, |
| | | ): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { |
| | | field: 'platform', |
| | | title: 'æå±å¹³å°', |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.AI_PLATFORM }, |
| | | }, |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'type', |
| | | title: '模åç±»å', |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.AI_MODEL_TYPE }, |
| | | }, |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'capability', |
| | | title: '模åè½å', |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.AI_MODEL_CAPABILITY }, |
| | | }, |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'platform', |
| | | title: 'æå±å¹³å°', |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.AI_PLATFORM }, |
| | | }, |
| | | minWidth: 100, |
| | | }, |
| | |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | title: 'å¯ç¨', |
| | | minWidth: 100, |
| | | align: 'center', |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.COMMON_STATUS }, |
| | | attrs: { beforeChange: onStatusChange }, |
| | | name: 'CellSwitch', |
| | | props: { |
| | | checkedValue: CommonStatusEnum.ENABLE, |
| | | unCheckedValue: CommonStatusEnum.DISABLE, |
| | | }, |
| | | }, |
| | | minWidth: 80, |
| | | }, |
| | | { |
| | | field: 'temperature', |
| | |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { AiModelModelApi } from '#/api/ai/model/model'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { ref } from 'vue'; |
| | | |
| | | import { confirm, Page, useVbenModal } from '@vben/common-ui'; |
| | | import { CommonStatusEnum } from '@vben/constants'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { deleteModel, getModelPage } from '#/api/ai/model/model'; |
| | | import { deleteModel, getModelPage, updateModelStatus } from '#/api/ai/model/model'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** å½åé¡µæ°æ®ï¼spanMethod ä¾èµå®è®¡ç®ç±»åååå¹¶è¡æ° */ |
| | | const currentRows = ref<AiModelModelApi.Model[]>([]); |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | |
| | | } |
| | | } |
| | | |
| | | /** æ´æ°æ¨¡åå¯ç¨ç¶æï¼å¯ç¨ä¼èªå¨å
³éåç±»ååè½åçå
¶ä»æ¨¡å */ |
| | | async function handleStatusChange( |
| | | newStatus: number, |
| | | row: AiModelModelApi.Model, |
| | | ): Promise<boolean | undefined> { |
| | | try { |
| | | await confirm( |
| | | newStatus === CommonStatusEnum.ENABLE |
| | | ? `å¯ç¨ã${row.name}ãåï¼åç±»ååè½åä¸å·²å¯ç¨çå
¶ä»æ¨¡åä¼èªå¨å
³éï¼ç¡®å®ç»§ç»åï¼` |
| | | : `ä½ è¦åç¨ã${row.name}ãåï¼åç¨å该类å该è½åå°æ²¡æå¯ç¨æ¨¡åã`, |
| | | ); |
| | | } catch { |
| | | return false; |
| | | } |
| | | await updateModelStatus(row.id!, newStatus); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | // åç»å
¶ä»æ¨¡åçå¯ç¨ç¶æå·²è¢«å端æ¹åï¼å¿
é¡»æ´é¡µå·æ° |
| | | handleRefresh(); |
| | | return true; |
| | | } |
| | | |
| | | /** 模åç±»ååæç±»å纵ååå¹¶ï¼å½¢æåç±»å±ç¤º */ |
| | | function handleSpanMethod({ |
| | | column, |
| | | rowIndex, |
| | | }: { |
| | | column: { field?: string; property?: string }; |
| | | rowIndex: number; |
| | | }) { |
| | | const field = column.field || column.property; |
| | | if (field !== 'type') { |
| | | return { colspan: 1, rowspan: 1 }; |
| | | } |
| | | const row = currentRows.value[rowIndex]; |
| | | if (rowIndex > 0 && currentRows.value[rowIndex - 1]?.type === row?.type) { |
| | | return { colspan: 0, rowspan: 0 }; |
| | | } |
| | | let rowspan = 1; |
| | | for (let index = rowIndex + 1; index < currentRows.value.length; index += 1) { |
| | | if (currentRows.value[index]?.type !== row?.type) { |
| | | break; |
| | | } |
| | | rowspan += 1; |
| | | } |
| | | return { colspan: 1, rowspan }; |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | columns: useGridColumns(handleStatusChange), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getModelPage({ |
| | | const data = await getModelPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | currentRows.value = data.list || []; |
| | | return data; |
| | | }, |
| | | }, |
| | | }, |
| | |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | spanMethod: handleSpanMethod, |
| | | toolbarConfig: { |
| | | refresh: true, |
| | | search: true, |
| | |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { isAiEnable } from '@vben/hooks'; |
| | | |
| | | import { confirm, Page, useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | import { downloadFileFromBlobPart, isEmpty } from '#/packages/utils/src'; |
| | | |
| | |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import AgentImport from './modules/agent-import.vue'; |
| | | import Form from './modules/form.vue'; |
| | | import ImportForm from './modules/import-form.vue'; |
| | | |
| | |
| | | |
| | | const [ImportModal, importModalApi] = useVbenModal({ |
| | | connectedComponent: ImportForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [AgentImportModal, agentImportModalApi] = useVbenModal({ |
| | | connectedComponent: AgentImport, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | |
| | | function handleImport() { |
| | | importModalApi.open(); |
| | | } |
| | | |
| | | /** æºè½ä½å¯¼å
¥ç©æ */ |
| | | function handleAgentImport() { |
| | | agentImportModalApi.open(); |
| | | } |
| | | |
| | | /** å·¥å
·æ æé®ï¼æºè½ä½å¯¼å
¥å AI æ»å¼å
³æ§å¶ */ |
| | | const toolbarActions = computed(() => [ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['ç©æ']), |
| | | type: 'primary' as const, |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['mdm:item:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.import', ['ç©æ']), |
| | | type: 'primary' as const, |
| | | icon: ACTION_ICON.UPLOAD, |
| | | auth: ['mdm:item:import'], |
| | | onClick: handleImport, |
| | | }, |
| | | ...(isAiEnable() |
| | | ? [ |
| | | { |
| | | label: 'æºè½ä½å¯¼å
¥', |
| | | type: 'primary' as const, |
| | | icon: ACTION_ICON.UPLOAD, |
| | | auth: ['mdm:item:import'], |
| | | onClick: handleAgentImport, |
| | | }, |
| | | ] |
| | | : []), |
| | | { |
| | | label: $t('ui.actionTitle.export'), |
| | | type: 'primary' as const, |
| | | icon: ACTION_ICON.DOWNLOAD, |
| | | auth: ['mdm:item:export'], |
| | | onClick: handleExport, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.deleteBatch'), |
| | | type: 'primary' as const, |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['mdm:item:delete'], |
| | | disabled: isEmpty(checkedIds), |
| | | onClick: handleDeleteBatch, |
| | | }, |
| | | ]); |
| | | |
| | | /** ç¼è¾ç©æ */ |
| | | function handleEdit(row: MdmItemApi.Item) { |
| | |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <ImportModal @success="handleRefresh" /> |
| | | <AgentImportModal @success="handleRefresh" /> |
| | | <Grid table-title="ç©æå表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['ç©æ']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['mdm:item:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.import', ['ç©æ']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.UPLOAD, |
| | | auth: ['mdm:item:import'], |
| | | onClick: handleImport, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.export'), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.DOWNLOAD, |
| | | auth: ['mdm:item:export'], |
| | | onClick: handleExport, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.deleteBatch'), |
| | | type: 'primary', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['mdm:item:delete'], |
| | | disabled: isEmpty(checkedIds), |
| | | onClick: handleDeleteBatch, |
| | | }, |
| | | ]" |
| | | /> |
| | | <TableAction :actions="toolbarActions" /> |
| | | </template> |
| | | <template #categoryName="{ row }"> |
| | | <Tag v-if="getCategoryColor(row.categoryId)" :color="getCategoryColor(row.categoryId)">{{ getCategoryName(row.categoryId) }}</Tag> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FileType } from 'ant-design-vue/es/upload/interface'; |
| | | |
| | | import type { MdmAgentImportApi } from '#/api/mdm/item/agent'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { |
| | | Alert, |
| | | Button, |
| | | Input, |
| | | message, |
| | | Steps, |
| | | Table, |
| | | Tag, |
| | | Upload, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | agentImportErrorMessage, |
| | | bindImportAttachment, |
| | | createImportSession, |
| | | getMaterialImportReceipt, |
| | | reviewMaterialImport, |
| | | startMaterialImport, |
| | | } from '#/api/mdm/item/agent'; |
| | | import { uploadFile } from '#/api/system/storage'; |
| | | |
| | | defineOptions({ name: 'MdmAgentImport' }); |
| | | |
| | | const emit = defineEmits<{ success: [] }>(); |
| | | |
| | | const MAX_BYTES = 8 * 1024 * 1024; |
| | | const FIELD_KEYS = [ |
| | | 'code', |
| | | 'name', |
| | | 'specification', |
| | | 'categoryCode', |
| | | 'unitMeasureCode', |
| | | ] as const; |
| | | type FieldKey = (typeof FIELD_KEYS)[number]; |
| | | |
| | | interface EditableRow { |
| | | candidate_id: string; |
| | | row_no?: number; |
| | | code: string; |
| | | name: string; |
| | | specification: string; |
| | | categoryCode: string; |
| | | unitMeasureCode: string; |
| | | original: Record<string, string>; |
| | | preflight?: MdmAgentImportApi.PreflightRow; |
| | | issues: string[]; |
| | | } |
| | | |
| | | interface RowMessage { |
| | | type: 'error' | 'warning'; |
| | | text: string; |
| | | } |
| | | |
| | | const step = ref(1); |
| | | const file = ref<File>(); |
| | | const fileName = ref(''); |
| | | const processing = ref(false); |
| | | const taskId = ref(''); |
| | | const state = ref<MdmAgentImportApi.ImportState>(); |
| | | const receipt = ref<MdmAgentImportApi.Receipt>(); |
| | | const rows = ref<EditableRow[]>([]); |
| | | const lastError = ref(''); |
| | | let sessionId: number | undefined; |
| | | let attachmentId: number | undefined; |
| | | |
| | | const preflight = computed(() => state.value?.preflight); |
| | | const issues = computed(() => state.value?.issues ?? []); |
| | | const canCommit = computed( |
| | | () => !!preflight.value?.can_commit && issues.value.length === 0, |
| | | ); |
| | | const hasChanges = computed(() => corrections.value.length > 0); |
| | | |
| | | const corrections = computed<MdmAgentImportApi.Correction[]>(() => { |
| | | const result: MdmAgentImportApi.Correction[] = []; |
| | | for (const row of rows.value) { |
| | | for (const field of FIELD_KEYS) { |
| | | const before = (row.original[field] ?? '').trim(); |
| | | const after = (row[field] ?? '').trim(); |
| | | if (before !== after) { |
| | | result.push({ |
| | | candidate_id: row.candidate_id, |
| | | field, |
| | | value: after, |
| | | reason: '人工修订', |
| | | }); |
| | | } |
| | | } |
| | | } |
| | | return result; |
| | | }); |
| | | |
| | | const ACTION_LABEL: Record<MdmAgentImportApi.RowAction, string> = { |
| | | BLOCKED: '黿', |
| | | CREATE: 'æ°å»º', |
| | | SKIP: 'å·²åå¨', |
| | | }; |
| | | const ACTION_COLOR: Record<MdmAgentImportApi.RowAction, string> = { |
| | | BLOCKED: 'red', |
| | | CREATE: 'green', |
| | | SKIP: 'orange', |
| | | }; |
| | | const STATUS_LABEL: Record<MdmAgentImportApi.RowStatus, string> = { |
| | | FAILED: '失败', |
| | | SKIPPED: '已跳è¿', |
| | | SUCCESS: 'æå', |
| | | UNKNOWN: 'æªç¥', |
| | | }; |
| | | const STATUS_COLOR: Record<MdmAgentImportApi.RowStatus, string> = { |
| | | FAILED: 'red', |
| | | SKIPPED: 'orange', |
| | | SUCCESS: 'green', |
| | | UNKNOWN: 'default', |
| | | }; |
| | | |
| | | const reviewColumns = [ |
| | | { dataIndex: 'row_no', key: 'row_no', title: 'è¡å·', width: 64 }, |
| | | { dataIndex: 'code', key: 'code', title: 'ç©æç¼ç ', width: 150 }, |
| | | { dataIndex: 'name', key: 'name', title: 'ç©æåç§°', width: 180 }, |
| | | { dataIndex: 'specification', key: 'specification', title: 'è§æ ¼åå·', width: 150 }, |
| | | { dataIndex: 'categoryCode', key: 'categoryCode', title: 'åç±»ç¼ç ', width: 140 }, |
| | | { |
| | | dataIndex: 'unitMeasureCode', |
| | | key: 'unitMeasureCode', |
| | | title: '计éåä½', |
| | | width: 130, |
| | | }, |
| | | { key: 'action', title: '颿£å¨ä½', width: 100 }, |
| | | { key: 'message', title: 'æ ¡éªä¿¡æ¯', width: 260 }, |
| | | ]; |
| | | |
| | | const receiptColumns = [ |
| | | { dataIndex: 'row_no', key: 'row_no', title: 'è¡å·', width: 64 }, |
| | | { dataIndex: 'code', key: 'code', title: 'ç©æç¼ç ', width: 150 }, |
| | | { dataIndex: 'name', key: 'name', title: 'ç©æåç§°', width: 180 }, |
| | | { key: 'action', title: 'å¨ä½', width: 90 }, |
| | | { key: 'status', title: 'ç»æ', width: 90 }, |
| | | { dataIndex: 'target_item_id', key: 'target_item_id', title: 'ç©æ ID', width: 100 }, |
| | | { key: 'error', title: '失败åå ', width: 220 }, |
| | | ]; |
| | | |
| | | function isEditableColumn(key: unknown): key is FieldKey { |
| | | return typeof key === 'string' && (FIELD_KEYS as readonly string[]).includes(key); |
| | | } |
| | | |
| | | function rowMessages(row: EditableRow): RowMessage[] { |
| | | return [ |
| | | ...(row.preflight?.blockers ?? []).map((item) => ({ |
| | | type: 'error' as const, |
| | | text: item.message, |
| | | })), |
| | | ...(row.preflight?.warnings ?? []).map((item) => ({ |
| | | type: 'warning' as const, |
| | | text: item.message, |
| | | })), |
| | | ...row.issues.map((text) => ({ type: 'warning' as const, text })), |
| | | ]; |
| | | } |
| | | |
| | | function buildRows(data: MdmAgentImportApi.ImportState): EditableRow[] { |
| | | const plannedRows = data.preflight?.rows ?? []; |
| | | return (data.candidates ?? []) |
| | | .map((candidate) => { |
| | | const planned = plannedRows.find( |
| | | (item) => item.candidate_id === candidate.candidate_id, |
| | | ); |
| | | const values = candidate.values ?? {}; |
| | | return { |
| | | candidate_id: candidate.candidate_id, |
| | | row_no: planned?.row_no ?? candidate.evidence?.code?.row, |
| | | code: values.code ?? '', |
| | | name: values.name ?? '', |
| | | specification: values.specification ?? '', |
| | | categoryCode: values.categoryCode ?? '', |
| | | unitMeasureCode: values.unitMeasureCode ?? '', |
| | | original: { ...values }, |
| | | preflight: planned, |
| | | issues: candidate.issues ?? [], |
| | | }; |
| | | }) |
| | | .sort((a, b) => (a.row_no ?? 0) - (b.row_no ?? 0)); |
| | | } |
| | | |
| | | function applyState(payload: MdmAgentImportApi.PublicState) { |
| | | state.value = payload.state; |
| | | taskId.value = payload.state.task_id; |
| | | lastError.value = |
| | | payload.state.last_error || payload.interrupts?.[0]?.last_error || ''; |
| | | receipt.value = payload.state.receipt; |
| | | rows.value = buildRows(payload.state); |
| | | step.value = receipt.value ? 3 : 2; |
| | | syncFooter(); |
| | | } |
| | | |
| | | function syncFooter() { |
| | | void modalApi.setState({ |
| | | cancelText: step.value === 2 ? 'åæ¶ä»»å¡' : 'åæ¶', |
| | | confirmText: |
| | | step.value === 1 ? 'å¼å§è§£æ' : step.value === 2 ? '确认æäº¤' : '宿', |
| | | showCancelButton: step.value !== 3, |
| | | }); |
| | | } |
| | | |
| | | function reset() { |
| | | step.value = 1; |
| | | file.value = undefined; |
| | | fileName.value = ''; |
| | | processing.value = false; |
| | | taskId.value = ''; |
| | | state.value = undefined; |
| | | receipt.value = undefined; |
| | | rows.value = []; |
| | | lastError.value = ''; |
| | | sessionId = undefined; |
| | | attachmentId = undefined; |
| | | } |
| | | |
| | | function beforeUpload(fileInfo: FileType) { |
| | | const raw = fileInfo as unknown as File; |
| | | const name = raw.name.toLowerCase(); |
| | | if (!name.endsWith('.csv') && !name.endsWith('.xlsx')) { |
| | | message.error('ä»
æ¯æ CSV æ XLSX æä»¶'); |
| | | return false; |
| | | } |
| | | if (raw.size > MAX_BYTES) { |
| | | message.error('æä»¶ä¸è½è¶
è¿ 8 MiB'); |
| | | return false; |
| | | } |
| | | file.value = raw; |
| | | fileName.value = raw.name; |
| | | return false; |
| | | } |
| | | |
| | | async function startImport() { |
| | | if (!file.value) { |
| | | message.warning('请å
éæ© CSV/XLSX æä»¶'); |
| | | return; |
| | | } |
| | | processing.value = true; |
| | | modalApi.lock(); |
| | | try { |
| | | sessionId = await createImportSession(); |
| | | const blobs = await uploadFile([file.value]); |
| | | const blobId = blobs?.[0]?.id; |
| | | if (!blobId) { |
| | | throw new Error('æä»¶ä¸ä¼ å¤±è´¥ï¼æªåå¾ blobId'); |
| | | } |
| | | attachmentId = await bindImportAttachment(sessionId, blobId); |
| | | const payload = await startMaterialImport(attachmentId); |
| | | applyState(payload); |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } finally { |
| | | processing.value = false; |
| | | modalApi.unlock(); |
| | | } |
| | | } |
| | | |
| | | async function revise() { |
| | | if (!state.value) return; |
| | | if (!hasChanges.value) { |
| | | message.info('æªæ£æµå°ä¿®æ¹'); |
| | | return; |
| | | } |
| | | processing.value = true; |
| | | modalApi.lock(); |
| | | try { |
| | | const payload = await reviewMaterialImport(taskId.value, { |
| | | action: 'revise', |
| | | corrections: corrections.value, |
| | | expected_revision: state.value.revision, |
| | | }); |
| | | applyState(payload); |
| | | message.success('å·²æä¿®æ¹éæ°é¢æ£ï¼è¯·æ ¸å¯¹ç»æ'); |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } finally { |
| | | processing.value = false; |
| | | modalApi.unlock(); |
| | | } |
| | | } |
| | | |
| | | async function confirmSubmit() { |
| | | if (!state.value) return; |
| | | if (hasChanges.value) { |
| | | message.warning('å卿ªæäº¤çä¿®æ¹ï¼è¯·å
ç¹ãéæ°æ£æ¥ãå¹¶ææ°ç颿£ç»æç¡®è®¤'); |
| | | return; |
| | | } |
| | | if (!canCommit.value) { |
| | | message.warning(lastError.value || '颿£ä»æé»æé¡¹ï¼è¯·ä¿®è®¢åéæ°æ£æ¥'); |
| | | return; |
| | | } |
| | | processing.value = true; |
| | | modalApi.lock(); |
| | | try { |
| | | const payload = await reviewMaterialImport(taskId.value, { |
| | | action: 'confirm', |
| | | expected_preflight_hash: state.value.preflight_hash, |
| | | expected_revision: state.value.revision, |
| | | }); |
| | | applyState(payload); |
| | | if (receipt.value) { |
| | | message.success('æäº¤å®æï¼è¯·æ¥çåæ§'); |
| | | } else if (lastError.value) { |
| | | message.error(lastError.value); |
| | | } |
| | | } catch (error) { |
| | | const text = agentImportErrorMessage(error); |
| | | message.error(text); |
| | | // æäº¤è¶
æ¶/ç½å
³ä¸ææ¶å½ä»¤å¯è½å·²æ§è¡ï¼æç¤ºç¨æ·æ¥åæ§ï¼é¿å
éå¤æäº¤ |
| | | lastError.value = text; |
| | | } finally { |
| | | processing.value = false; |
| | | modalApi.unlock(); |
| | | } |
| | | } |
| | | |
| | | async function fetchReceipt() { |
| | | if (!taskId.value) return; |
| | | processing.value = true; |
| | | modalApi.lock(); |
| | | try { |
| | | receipt.value = await getMaterialImportReceipt(taskId.value); |
| | | step.value = 3; |
| | | syncFooter(); |
| | | } catch (error) { |
| | | message.error(agentImportErrorMessage(error)); |
| | | } finally { |
| | | processing.value = false; |
| | | modalApi.unlock(); |
| | | } |
| | | } |
| | | |
| | | async function handleCancel() { |
| | | if (step.value === 2 && taskId.value) { |
| | | try { |
| | | await reviewMaterialImport(taskId.value, { |
| | | action: 'cancel', |
| | | expected_revision: state.value?.revision ?? 0, |
| | | }); |
| | | } catch { |
| | | // åæ¶å¤±è´¥ä¸å½±åå
³éçªå£ |
| | | } |
| | | } |
| | | modalApi.close(); |
| | | } |
| | | |
| | | function finish() { |
| | | if ((receipt.value?.summary.success ?? 0) > 0) { |
| | | emit('success'); |
| | | } |
| | | modalApi.close(); |
| | | } |
| | | |
| | | async function handlePrimary() { |
| | | if (processing.value) return; |
| | | if (step.value === 1) { |
| | | await startImport(); |
| | | return; |
| | | } |
| | | if (step.value === 2) { |
| | | await confirmSubmit(); |
| | | return; |
| | | } |
| | | finish(); |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | onCancel() { |
| | | void handleCancel(); |
| | | }, |
| | | onClosed() { |
| | | reset(); |
| | | }, |
| | | onConfirm() { |
| | | void handlePrimary(); |
| | | }, |
| | | onOpenChange(isOpen) { |
| | | if (isOpen) { |
| | | reset(); |
| | | syncFooter(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="æºè½ä½å¯¼å
¥ç©æ" class="w-[1080px]"> |
| | | <Steps |
| | | :current="step - 1" |
| | | class="mx-4 mb-4" |
| | | size="small" |
| | | :items="[ |
| | | { title: 'ä¸ä¼ æä»¶' }, |
| | | { title: 'å®¡æ ¸ç¡®è®¤' }, |
| | | { title: 'æäº¤åæ§' }, |
| | | ]" |
| | | /> |
| | | |
| | | <div v-if="step === 1" class="mx-4"> |
| | | <Upload |
| | | accept=".csv,.xlsx" |
| | | :max-count="1" |
| | | :show-upload-list="false" |
| | | :before-upload="beforeUpload" |
| | | > |
| | | <Button type="primary">éæ© CSV / XLSX æä»¶</Button> |
| | | </Upload> |
| | | <div v-if="fileName" class="mt-2 text-sm text-gray-600"> |
| | | 已鿩ï¼{{ fileName }} |
| | | </div> |
| | | <div class="mt-3 text-xs text-gray-500 leading-relaxed"> |
| | | <div>æä»¶ä¸é 8 MiBï¼ä»
æ¯æ CSV / XLSXã</div> |
| | | <div> |
| | | æµç¨ï¼è§£ææ¸
å â Java æ ¡éªåç±»/åä½/ç¼ç â äººå·¥æ ¸å¯¹ â 确认æäº¤ã |
| | | </div> |
| | | <div> |
| | | æ¬é¶æ®µåªæ°å»ºç©æï¼ç¼ç å·²åå¨ä¼è·³è¿ï¼ä¸æ´æ°åç©æï¼ä¹ä¸åæ¥ MESã |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-else-if="step === 2" class="mx-4"> |
| | | <div class="mb-3 flex flex-wrap items-center gap-x-5 gap-y-1 text-sm"> |
| | | <span>æ»è®¡ï¼{{ preflight?.summary.total ?? rows.length }}</span> |
| | | <span class="text-green-600"> |
| | | æ°å»ºï¼{{ preflight?.summary.create ?? 0 }} |
| | | </span> |
| | | <span class="text-orange-500"> |
| | | è·³è¿ï¼{{ preflight?.summary.skip ?? 0 }} |
| | | </span> |
| | | <span class="text-red-500"> |
| | | 黿ï¼{{ preflight?.summary.blocked ?? 0 }} |
| | | </span> |
| | | <span class="text-gray-500">æ¹æ¡çæ¬ï¼{{ state?.revision ?? 0 }}</span> |
| | | </div> |
| | | |
| | | <Alert |
| | | v-if="lastError" |
| | | type="error" |
| | | :message="lastError" |
| | | show-icon |
| | | class="mb-3" |
| | | /> |
| | | |
| | | <Alert |
| | | v-if="issues.length > 0" |
| | | type="warning" |
| | | show-icon |
| | | class="mb-3" |
| | | message="åå¨éè¦å¤ççé®é¢" |
| | | > |
| | | <template #description> |
| | | <div v-for="(issue, index) in issues" :key="index"> |
| | | {{ issue.message }} |
| | | </div> |
| | | </template> |
| | | </Alert> |
| | | |
| | | <Alert |
| | | v-else-if="preflight && !preflight.can_commit" |
| | | type="warning" |
| | | show-icon |
| | | class="mb-3" |
| | | message="颿£ä»æé»æé¡¹ï¼ä¿®æ£å请ç¹ãéæ°æ£æ¥ãï¼éè¿åæè½ç¡®è®¤æäº¤" |
| | | /> |
| | | |
| | | <Table |
| | | bordered |
| | | size="small" |
| | | :columns="reviewColumns" |
| | | :data-source="rows" |
| | | :pagination="false" |
| | | :row-key="(record: EditableRow) => record.candidate_id" |
| | | :scroll="{ y: 360 }" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="isEditableColumn(column.key)"> |
| | | <Input |
| | | v-model:value="record[column.key]" |
| | | size="small" |
| | | :status=" |
| | | record.preflight?.blockers?.some( |
| | | (item: MdmAgentImportApi.Issue) => item.field === column.key, |
| | | ) |
| | | ? 'error' |
| | | : undefined |
| | | " |
| | | /> |
| | | <div |
| | | v-if=" |
| | | column.key === 'categoryCode' && |
| | | record.preflight?.resolved?.category_name |
| | | " |
| | | class="mt-1 text-xs text-gray-400" |
| | | > |
| | | è§£æä¸ºï¼{{ record.preflight.resolved.category_name }} |
| | | </div> |
| | | <div |
| | | v-if=" |
| | | column.key === 'unitMeasureCode' && |
| | | record.preflight?.resolved?.unit_measure_name |
| | | " |
| | | class="mt-1 text-xs text-gray-400" |
| | | > |
| | | è§£æä¸ºï¼{{ record.preflight.resolved.unit_measure_name }} |
| | | </div> |
| | | </template> |
| | | <template v-else-if="column.key === 'action'"> |
| | | <Tag |
| | | v-if="record.preflight" |
| | | :color=" |
| | | ACTION_COLOR[record.preflight.action as MdmAgentImportApi.RowAction] |
| | | " |
| | | > |
| | | {{ ACTION_LABEL[record.preflight.action as MdmAgentImportApi.RowAction] }} |
| | | </Tag> |
| | | </template> |
| | | <template v-else-if="column.key === 'message'"> |
| | | <div |
| | | v-for="(item, index) in rowMessages(record as EditableRow)" |
| | | :key="index" |
| | | class="text-xs leading-relaxed" |
| | | :class="item.type === 'error' ? 'text-red-500' : 'text-orange-500'" |
| | | > |
| | | {{ item.text }} |
| | | </div> |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | |
| | | <div class="mt-3 flex flex-wrap items-center gap-2"> |
| | | <Button :disabled="processing || !hasChanges" @click="revise"> |
| | | éæ°æ£æ¥ |
| | | </Button> |
| | | <Button :disabled="processing || !taskId" @click="fetchReceipt"> |
| | | æ¥è¯¢åæ§ |
| | | </Button> |
| | | <span class="text-xs text-gray-400"> |
| | | {{ hasChanges ? '已修æ¹ï¼ééæ°æ£æ¥åæè½ç¡®è®¤' : '确认åæäº¤ï¼å·²åå¨ç¼ç ä¼è·³è¿' }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-else class="mx-4"> |
| | | <div class="mb-3 flex flex-wrap items-center gap-x-5 gap-y-1 text-sm"> |
| | | <Tag :color="receipt?.status === 'SUCCEEDED' ? 'green' : 'orange'"> |
| | | {{ receipt?.status ?? 'UNKNOWN' }} |
| | | </Tag> |
| | | <span>æ»è®¡ï¼{{ receipt?.summary.total ?? 0 }}</span> |
| | | <span class="text-green-600"> |
| | | æåï¼{{ receipt?.summary.success ?? 0 }} |
| | | </span> |
| | | <span class="text-orange-500"> |
| | | è·³è¿ï¼{{ receipt?.summary.skipped ?? 0 }} |
| | | </span> |
| | | <span class="text-red-500"> |
| | | 失败ï¼{{ receipt?.summary.failed ?? 0 }} |
| | | </span> |
| | | <span class="text-gray-500"> |
| | | æªç¥ï¼{{ receipt?.summary.unknown ?? 0 }} |
| | | </span> |
| | | </div> |
| | | |
| | | <Alert |
| | | v-if="receipt?.error_message" |
| | | type="error" |
| | | :message="receipt.error_message" |
| | | show-icon |
| | | class="mb-3" |
| | | /> |
| | | |
| | | <Table |
| | | bordered |
| | | size="small" |
| | | :columns="receiptColumns" |
| | | :data-source="receipt?.rows ?? []" |
| | | :pagination="false" |
| | | :row-key="(record: MdmAgentImportApi.ReceiptRow) => record.candidate_id" |
| | | :scroll="{ y: 360 }" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.key === 'action'"> |
| | | <Tag :color="record.action === 'CREATE' ? 'green' : 'orange'"> |
| | | {{ record.action === 'CREATE' ? 'æ°å»º' : 'è·³è¿' }} |
| | | </Tag> |
| | | </template> |
| | | <template v-else-if="column.key === 'status'"> |
| | | <Tag :color="STATUS_COLOR[record.status as MdmAgentImportApi.RowStatus]"> |
| | | {{ STATUS_LABEL[record.status as MdmAgentImportApi.RowStatus] }} |
| | | </Tag> |
| | | </template> |
| | | <template v-else-if="column.key === 'error'"> |
| | | <span class="text-xs text-red-500"> |
| | | {{ record.error_message ?? '' }} |
| | | </span> |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | </div> |
| | | </Modal> |
| | | </template> |
| | |
| | | |
| | | import type { CrmSaleQuotationAiApi } from '#/api/crm/saleQuotation/ai'; |
| | | |
| | | import { nextTick, ref } from 'vue'; |
| | | import { computed, nextTick, ref } from 'vue'; |
| | | |
| | | import { isAiEnable } from '@vben/hooks'; |
| | | |
| | | import { QUOTATION_STATUS, useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | |
| | | }); |
| | | } |
| | | |
| | | /** å·¥å
·æ æé®ï¼è¯å«å½å
¥å AI æ»å¼å
³æ§å¶ */ |
| | | const toolbarActions = computed(() => [ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['æ¥ä»·å']), |
| | | type: 'primary' as const, |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['crm:sale-quotation:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | ...(isAiEnable() |
| | | ? [ |
| | | { |
| | | label: 'è¯å«å½å
¥', |
| | | type: 'default' as const, |
| | | auth: ['crm:sale-quotation:create'], |
| | | onClick: handleOcrCreate, |
| | | }, |
| | | ] |
| | | : []), |
| | | ]); |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | |
| | | <OcrUpload ref="ocrUploadRef" @success="handleOcrSuccess" /> |
| | | <Grid> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['æ¥ä»·å']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['crm:sale-quotation:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: 'è¯å«å½å
¥', |
| | | type: 'default', |
| | | auth: ['crm:sale-quotation:create'], |
| | | onClick: handleOcrCreate, |
| | | }, |
| | | ]" |
| | | /> |
| | | <TableAction :actions="toolbarActions" /> |
| | | </template> |
| | | <template #name="{ row }"> |
| | | <Button type="link" @click="handleDetail(row)"> |
| | |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | import { isAiEnable } from '@vben/hooks'; |
| | | |
| | | import { Col, Row } from 'ant-design-vue'; |
| | | |
| | |
| | | |
| | | <!-- AI æºè½åæ --> |
| | | <AiInsights |
| | | v-if="canViewHome && canNavigate('MesProWorkOrder')" |
| | | v-if="canViewHome && canNavigate('MesProWorkOrder') && isAiEnable()" |
| | | :active-order-count="summary.workOrderActiveCount" |
| | | @navigate="handleNavigate" |
| | | /> |
| | |
| | | import type { ErpPurchaseInvoiceAiApi } from '#/api/erp/purchase/invoice/ai'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { isAiEnable } from '@vben/hooks'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button, message, Tag } from 'ant-design-vue'; |
| | | import { nextTick, ref } from 'vue'; |
| | | import { computed, nextTick, ref } from 'vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | |
| | | } |
| | | } |
| | | |
| | | /** å·¥å
·æ æé®ï¼è¯å«å½å
¥å AI æ»å¼å
³æ§å¶ */ |
| | | const toolbarActions = computed(() => [ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['æ¥ç¥¨']), |
| | | type: 'primary' as const, |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['erp:purchase-invoice:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | ...(isAiEnable() |
| | | ? [ |
| | | { |
| | | label: 'è¯å«å½å
¥', |
| | | type: 'primary' as const, |
| | | auth: ['erp:purchase-invoice:create'], |
| | | onClick: handleOcrCreate, |
| | | }, |
| | | ] |
| | | : []), |
| | | { |
| | | label: $t('ui.actionTitle.export'), |
| | | type: 'primary' as const, |
| | | icon: ACTION_ICON.DOWNLOAD, |
| | | auth: ['erp:purchase-invoice:export'], |
| | | onClick: handleExport, |
| | | }, |
| | | ]); |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | |
| | | <OcrUpload ref="ocrUploadRef" @success="handleOcrSuccess" /> |
| | | <Grid table-title="æ¥ç¥¨å表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['æ¥ç¥¨']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['erp:purchase-invoice:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: 'è¯å«å½å
¥', |
| | | type: 'primary', |
| | | auth: ['erp:purchase-invoice:create'], |
| | | onClick: handleOcrCreate, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.export'), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.DOWNLOAD, |
| | | auth: ['erp:purchase-invoice:export'], |
| | | onClick: handleExport, |
| | | }, |
| | | ]" |
| | | /> |
| | | <TableAction :actions="toolbarActions" /> |
| | | </template> |
| | | <template #no="{ row }"> |
| | | <Button type="link" @click="handleDetail(row)"> |
| | |
| | | <script lang="ts" setup> |
| | | import type { AttendanceClockLocation, AttendanceClockResponse } from '#/api/hrm/attendance/record'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '#/packages/effects/common-ui/src'; |
| | |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const location = ref('å
¬å¸åå°'); |
| | | const loading = ref(false); |
| | | const DEFAULT_LOCATION = 'å
¬å¸åå°'; |
| | | |
| | | /** ä¸çæå¡ */ |
| | | async function handleClockIn() { |
| | | const location = ref(DEFAULT_LOCATION); |
| | | const loading = ref(false); |
| | | const locating = ref(false); |
| | | |
| | | /** |
| | | * è·åæµè§å¨å®ä½å¹¶ç»ä¸ä¸º WGS84 交ç»åç«¯æ ¡éªã |
| | | * å®ä½ä¸å¯ç¨ï¼ä¸æ¯æãæç»ææãé HTTPSï¼æ¶è¿å nullï¼ |
| | | * ç±å端æèå¤è§åå³å®æ¯å¦å
许æ å®ä½æå¡ã |
| | | */ |
| | | function getCurrentLocation(): Promise<AttendanceClockLocation | null> { |
| | | if (!navigator.geolocation) { |
| | | return Promise.resolve(null); |
| | | } |
| | | return new Promise((resolve) => { |
| | | navigator.geolocation.getCurrentPosition( |
| | | (position) => |
| | | resolve({ |
| | | longitude: Number(position.coords.longitude.toFixed(7)), |
| | | latitude: Number(position.coords.latitude.toFixed(7)), |
| | | accuracy: Number(position.coords.accuracy.toFixed(2)), |
| | | coordinateSystem: 'WGS84', |
| | | }), |
| | | () => resolve(null), |
| | | { enableHighAccuracy: true, maximumAge: 0, timeout: 15_000 }, |
| | | ); |
| | | }); |
| | | } |
| | | |
| | | /** æªå¯ç¨å®ä½èå´æ ¡éªæ¶å端ä¸è¿åè·ç¦»ï¼ç´æ¥æç¤ºæå */ |
| | | function buildSuccessMessage( |
| | | action: string, |
| | | result?: AttendanceClockResponse, |
| | | ) { |
| | | if (result?.distanceMeters === null || result?.distanceMeters === undefined) { |
| | | return `${action}æå`; |
| | | } |
| | | const place = result.locationName ? `è·${result.locationName}` : 'è·æå¡å°ç¹'; |
| | | return `${action}æåï¼${place} ${Number(result.distanceMeters).toFixed(1)} ç±³`; |
| | | } |
| | | |
| | | async function submitClock(action: 'in' | 'out') { |
| | | if (loading.value) { |
| | | return; |
| | | } |
| | | loading.value = true; |
| | | try { |
| | | await clockIn({ location: location.value }); |
| | | message.success('ä¸çæå¡æå'); |
| | | locating.value = true; |
| | | const position = await getCurrentLocation(); |
| | | locating.value = false; |
| | | if (!position) { |
| | | message.warning( |
| | | 'æªè·åå°å®ä½ï¼å°ææå¡å°ç¹ææ¬æäº¤ï¼è¥èå¤è§åè¦æ±å®ä½ï¼è¯·å¼å¯å®ä½æéåéè¯ï¼é HTTPS ç¯å¢æµè§å¨ä¼ç¦ç¨å®ä½ï¼', |
| | | ); |
| | | } |
| | | const locationText = location.value.trim() || undefined; |
| | | const result = |
| | | action === 'in' |
| | | ? await clockIn(position, locationText) |
| | | : await clockOut(position, locationText); |
| | | message.success( |
| | | buildSuccessMessage(action === 'in' ? 'ä¸çæå¡' : 'ä¸çæå¡', result), |
| | | ); |
| | | emit('success'); |
| | | } catch (e: any) { |
| | | message.error(e.message || 'æå¡å¤±è´¥'); |
| | | } catch (error: any) { |
| | | // æ¥å£é误已ç±è¯·æ±æ¦æªå¨ç»ä¸æç¤ºï¼è¿éåªå
åºæ¬å°å¼å¸¸ï¼é¿å
éå¤å¼¹åº |
| | | const handledByInterceptor = Boolean( |
| | | error?.response || error?.data || error?.isAxiosError || error?.code, |
| | | ); |
| | | if (!handledByInterceptor) { |
| | | message.error(error?.message || 'æå¡å¤±è´¥'); |
| | | } |
| | | } finally { |
| | | locating.value = false; |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | /** ä¸çæå¡ */ |
| | | function handleClockIn() { |
| | | return submitClock('in'); |
| | | } |
| | | |
| | | /** ä¸çæå¡ */ |
| | | async function handleClockOut() { |
| | | loading.value = true; |
| | | try { |
| | | await clockOut({ location: location.value }); |
| | | message.success('ä¸çæå¡æå'); |
| | | emit('success'); |
| | | } catch (e: any) { |
| | | message.error(e.message || 'æå¡å¤±è´¥'); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | function handleClockOut() { |
| | | return submitClock('out'); |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | const [Modal] = useVbenModal({ |
| | | onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | location.value = 'å
¬å¸åå°'; |
| | | location.value = DEFAULT_LOCATION; |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="æå¡" class="w-[400px]"> |
| | | <Modal title="æå¡" class="w-[420px]"> |
| | | <div class="p-4"> |
| | | <div class="mb-4"> |
| | | <label class="mr-2">æå¡å°ç¹ï¼</label> |
| | | <a-input |
| | | v-model:value="location" |
| | | placeholder="请è¾å
¥æå¡å°ç¹" |
| | | style="width: 200px" |
| | | style="width: 220px" |
| | | /> |
| | | </div> |
| | | <div class="mb-4 text-xs text-gray-500"> |
| | | ç¹å»æå¡åä¼èªå¨è·åå½å GPS ä½ç½®ï¼å¹¶æèå¤è§åä¸çå°ç¹ä¸å
许å徿 ¡éªï¼æµè§å¨å®ä½ä¸å¯ç¨æ¶ï¼å°æä¸æ¹å°ç¹ææ¬æäº¤ã |
| | | </div> |
| | | <div class="flex gap-4"> |
| | | <a-button |
| | | type="primary" |
| | | size="large" |
| | | :loading="loading" |
| | | :disabled="loading" |
| | | @click="handleClockIn" |
| | | > |
| | | ä¸çæå¡ |
| | |
| | | <a-button |
| | | size="large" |
| | | :loading="loading" |
| | | :disabled="loading" |
| | | @click="handleClockOut" |
| | | > |
| | | ä¸çæå¡ |
| | | </a-button> |
| | | </div> |
| | | <div v-if="locating" class="mt-2 text-xs text-gray-400"> |
| | | æ£å¨è·åå®ä½â¦ |
| | | </div> |
| | | </div> |
| | | </Modal> |
| | | </template> |
| | | </template> |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'locationRequired', |
| | | label: 'å®ä½æå¡', |
| | | component: 'Switch', |
| | | defaultValue: false, |
| | | componentProps: { |
| | | checkedChildren: 'å¯ç¨', |
| | | unCheckedChildren: 'å
³é', |
| | | }, |
| | | help: 'å¯ç¨åï¼åå·¥æå¡å¿
é¡»å¨ä¸æ¹è®¾å®å°ç¹çå
许åå¾å
', |
| | | }, |
| | | { |
| | | fieldName: 'locationName', |
| | | label: 'æå¡å°ç¹', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: 'ä¾å¦ï¼æ°é«çµåååº', |
| | | maxlength: 100, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['locationRequired'], |
| | | show: (values) => Boolean(values.locationRequired), |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'locationAddress', |
| | | label: 'å°ç¹å°å', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥è¯¦ç»å°åï¼ä¾¿äºåå·¥è¯å«', |
| | | maxlength: 255, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['locationRequired'], |
| | | show: (values) => Boolean(values.locationRequired), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'longitude', |
| | | label: 'ç»åº¦(WGS84)', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | placeholder: 'å°å¾éç¹åèªå¨å¡«å
', |
| | | min: -180, |
| | | max: 180, |
| | | precision: 7, |
| | | style: { width: '100%' }, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['locationRequired'], |
| | | show: (values) => Boolean(values.locationRequired), |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'latitude', |
| | | label: '纬度(WGS84)', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | placeholder: 'å°å¾éç¹åèªå¨å¡«å
', |
| | | min: -90, |
| | | max: 90, |
| | | precision: 7, |
| | | style: { width: '100%' }, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['locationRequired'], |
| | | show: (values) => Boolean(values.locationRequired), |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'radiusMeters', |
| | | label: 'æå¡åå¾(ç±³)', |
| | | component: 'InputNumber', |
| | | defaultValue: 10, |
| | | componentProps: { |
| | | min: 10, |
| | | max: 5000, |
| | | precision: 0, |
| | | style: { width: '100%' }, |
| | | }, |
| | | help: 'å
许èå´ 10~5000 ç±³ï¼å®é
精度åå³äºææº GPS ä¸ç¯å¢', |
| | | dependencies: { |
| | | triggerFields: ['locationRequired'], |
| | | show: (values) => Boolean(values.locationRequired), |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'coordinateSystem', |
| | | component: 'Input', |
| | | defaultValue: 'WGS84', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => false, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'RadioGroup', |
| | |
| | | { field: 'earlyAllowMinutes', title: 'æ©éå
许(åé)', width: 120 }, |
| | | { field: 'overtimeThreshold', title: 'å çèµ·ç®(å°æ¶)', width: 120 }, |
| | | { |
| | | field: 'locationName', |
| | | title: 'æå¡èå´', |
| | | minWidth: 180, |
| | | formatter: ({ row }) => |
| | | row.locationRequired |
| | | ? `${row.locationName || '-'}ï¼${row.radiusMeters ?? 0}ç±³ï¼` |
| | | : 'ä¸éå¶ä½ç½®', |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | width: 80, |
| | |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | import LocationMapPicker from './location-map-picker.vue'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formType = ref<FormType>('create'); |
| | | |
| | | /** å°å¾éæ©å¨æéçå®ä½ç¶æï¼é表åå¼åå忥 */ |
| | | const locationRequired = ref(false); |
| | | const longitude = ref<number>(); |
| | | const latitude = ref<number>(); |
| | | const radiusMeters = ref(10); |
| | | |
| | | const isDetail = computed(() => formType.value === 'detail'); |
| | | const modalWidth = computed(() => |
| | | locationRequired.value ? 'w-[1000px]' : 'w-[800px]', |
| | | ); |
| | | const getTitle = computed(() => { |
| | | if (formType.value === 'detail') { |
| | | return 'æ¥çèå¤è§å'; |
| | |
| | | } |
| | | return 'æ°å¢èå¤è§å'; |
| | | }); |
| | | |
| | | function toNumber(value: unknown): number | undefined { |
| | | return value === null || value === undefined || value === '' |
| | | ? undefined |
| | | : Number(value); |
| | | } |
| | | |
| | | /** 表åå¼å忶忥å®ä½ç¶æï¼ç¨äºæ§å¶å°å¾åºåä¸å¼¹çªå®½åº¦ */ |
| | | function syncLocationState( |
| | | values: Partial<HrmAttendanceRuleApi.AttendanceRule>, |
| | | ) { |
| | | locationRequired.value = Boolean(values.locationRequired); |
| | | longitude.value = toNumber(values.longitude); |
| | | latitude.value = toNumber(values.latitude); |
| | | radiusMeters.value = toNumber(values.radiusMeters) ?? 10; |
| | | } |
| | | |
| | | /** å°å¾éç¹ååå¡«ç»çº¬åº¦ï¼å¹¶å°½éè¡¥å
¨å°ç¹åç§°ä¸å°å */ |
| | | async function handleMapSelect(location: { |
| | | address?: string; |
| | | latitude: number; |
| | | locationName?: string; |
| | | longitude: number; |
| | | }) { |
| | | const currentValues = await formApi.getValues(); |
| | | longitude.value = location.longitude; |
| | | latitude.value = location.latitude; |
| | | await formApi.setFieldValue('longitude', location.longitude); |
| | | await formApi.setFieldValue('latitude', location.latitude); |
| | | await formApi.setFieldValue('coordinateSystem', 'WGS84'); |
| | | if (location.address) { |
| | | await formApi.setFieldValue('locationAddress', location.address); |
| | | } |
| | | if (!currentValues.locationName && location.locationName) { |
| | | await formApi.setFieldValue('locationName', location.locationName); |
| | | } |
| | | } |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | |
| | | layout: 'horizontal', |
| | | schema: [], |
| | | showDefaultActions: false, |
| | | handleValuesChange: (values) => { |
| | | syncLocationState(values); |
| | | }, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | |
| | | } |
| | | modalApi.lock(); |
| | | const data = (await formApi.getValues()) as HrmAttendanceRuleApi.AttendanceRule; |
| | | // å端ä»
æ¥å WGS84ï¼ä¸å¯ç¨å®ä½æ¶è¦æ±åæ ç³»å¿
å¡« |
| | | if (data.locationRequired) { |
| | | data.coordinateSystem = 'WGS84'; |
| | | } |
| | | try { |
| | | if (formType.value === 'update') { |
| | | await updateAttendanceRule(data); |
| | |
| | | } |
| | | const data = modalApi.getData<{ formType: FormType; id?: number }>(); |
| | | formType.value = data.formType; |
| | | locationRequired.value = false; |
| | | longitude.value = undefined; |
| | | latitude.value = undefined; |
| | | radiusMeters.value = 10; |
| | | formApi.setState({ schema: useFormSchema(data.formType) }); |
| | | formApi.setDisabled(data.formType === 'detail'); |
| | | modalApi.setState({ showConfirmButton: data.formType !== 'detail' }); |
| | | if (!data?.id) { |
| | | await formApi.setValues({ |
| | | coordinateSystem: 'WGS84', |
| | | locationRequired: false, |
| | | radiusMeters: 10, |
| | | }); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | await formApi.setValues(await getAttendanceRule(data.id)); |
| | | const rule = await getAttendanceRule(data.id); |
| | | await formApi.setValues(rule); |
| | | syncLocationState(rule); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-[800px]"> |
| | | <Modal :title="getTitle" :class="modalWidth"> |
| | | <Form class="mx-4" /> |
| | | <section v-if="locationRequired" class="mx-4 mt-2"> |
| | | <div class="mb-2 text-sm font-medium">å°å¾éæ©æå¡èå´</div> |
| | | <LocationMapPicker |
| | | :disabled="isDetail" |
| | | :latitude="latitude" |
| | | :longitude="longitude" |
| | | :radius-meters="radiusMeters" |
| | | @select="handleMapSelect" |
| | | /> |
| | | </section> |
| | | </Modal> |
| | | </template> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- èå¤å°ç¹å°å¾éæ©å¨ï¼ç¾åº¦å°å¾å±ç¤ºï¼å¯¹å¤ç»ä¸è¾åº WGS84 åæ --> |
| | | <script lang="ts" setup> |
| | | import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'; |
| | | |
| | | import { Alert, Button, Input, Spin, message } from 'ant-design-vue'; |
| | | |
| | | import { loadBaiduMapSdk } from '#/components/map'; |
| | | |
| | | /** 对å¤è¾åºï¼WGS84 åæ + 忥å°çå°å/å°ç¹å */ |
| | | interface LocationSelection { |
| | | address?: string; |
| | | latitude: number; |
| | | locationName?: string; |
| | | longitude: number; |
| | | } |
| | | |
| | | interface Props { |
| | | disabled?: boolean; |
| | | latitude?: number; |
| | | longitude?: number; |
| | | radiusMeters?: number; |
| | | } |
| | | |
| | | const props = withDefaults(defineProps<Props>(), { |
| | | disabled: false, |
| | | latitude: undefined, |
| | | longitude: undefined, |
| | | radiusMeters: 10, |
| | | }); |
| | | |
| | | const emit = defineEmits<{ |
| | | select: [location: LocationSelection]; |
| | | }>(); |
| | | |
| | | /** é»è®¤ä¸å¿ç¹ï¼æ°çä¹é²æ¨é½ï¼æªé
ç½®åæ æ¶ä½¿ç¨ */ |
| | | const DEFAULT_CENTER: [number, number] = [87.625, 43.835]; |
| | | const PI = Math.PI; |
| | | const X_PI = (PI * 3000) / 180; |
| | | const EARTH_SEMI_MAJOR_AXIS = 6_378_245; |
| | | const EARTH_ECCENTRICITY_SQUARED = 0.006_693_421_622_965_943; |
| | | |
| | | /* eslint-disable @typescript-eslint/no-explicit-any */ |
| | | type AnyMap = any; |
| | | |
| | | const mapContainer = ref<HTMLElement>(); |
| | | const loading = ref(false); |
| | | const searchKeyword = ref(''); |
| | | const selectedAddress = ref(''); |
| | | |
| | | let map: AnyMap; |
| | | let marker: AnyMap; |
| | | let rangeCircle: AnyMap; |
| | | let geocoder: AnyMap; |
| | | |
| | | const baiduMapKey = String(import.meta.env.VITE_BAIDU_MAP_KEY ?? '').trim(); |
| | | const hasMapConfig = computed(() => Boolean(baiduMapKey)); |
| | | |
| | | function outOfChina(longitude: number, latitude: number) { |
| | | return ( |
| | | longitude < 72.004 || |
| | | longitude > 137.8347 || |
| | | latitude < 0.8293 || |
| | | latitude > 55.8271 |
| | | ); |
| | | } |
| | | |
| | | function transformLatitude(longitude: number, latitude: number) { |
| | | let result = |
| | | -100 + |
| | | 2 * longitude + |
| | | 3 * latitude + |
| | | 0.2 * latitude * latitude + |
| | | 0.1 * longitude * latitude + |
| | | 0.2 * Math.sqrt(Math.abs(longitude)); |
| | | result += |
| | | ((20 * Math.sin(6 * longitude * PI) + 20 * Math.sin(2 * longitude * PI)) * 2) / 3; |
| | | result += |
| | | ((20 * Math.sin(latitude * PI) + 40 * Math.sin((latitude / 3) * PI)) * 2) / 3; |
| | | result += |
| | | ((160 * Math.sin((latitude / 12) * PI) + |
| | | 320 * Math.sin((latitude * PI) / 30)) * |
| | | 2) / |
| | | 3; |
| | | return result; |
| | | } |
| | | |
| | | function transformLongitude(longitude: number, latitude: number) { |
| | | let result = |
| | | 300 + |
| | | longitude + |
| | | 2 * latitude + |
| | | 0.1 * longitude * longitude + |
| | | 0.1 * longitude * latitude + |
| | | 0.1 * Math.sqrt(Math.abs(longitude)); |
| | | result += |
| | | ((20 * Math.sin(6 * longitude * PI) + 20 * Math.sin(2 * longitude * PI)) * 2) / 3; |
| | | result += |
| | | ((20 * Math.sin(longitude * PI) + 40 * Math.sin((longitude / 3) * PI)) * 2) / 3; |
| | | result += |
| | | ((150 * Math.sin((longitude / 12) * PI) + |
| | | 300 * Math.sin((longitude / 30) * PI)) * |
| | | 2) / |
| | | 3; |
| | | return result; |
| | | } |
| | | |
| | | /** WGS84 â GCJ02 */ |
| | | function wgs84ToGcj02(longitude: number, latitude: number): [number, number] { |
| | | if (outOfChina(longitude, latitude)) { |
| | | return [longitude, latitude]; |
| | | } |
| | | let deltaLatitude = transformLatitude(longitude - 105, latitude - 35); |
| | | let deltaLongitude = transformLongitude(longitude - 105, latitude - 35); |
| | | const radianLatitude = (latitude / 180) * PI; |
| | | let magic = Math.sin(radianLatitude); |
| | | magic = 1 - EARTH_ECCENTRICITY_SQUARED * magic * magic; |
| | | const sqrtMagic = Math.sqrt(magic); |
| | | deltaLatitude = |
| | | (deltaLatitude * 180) / |
| | | (((EARTH_SEMI_MAJOR_AXIS * (1 - EARTH_ECCENTRICITY_SQUARED)) / |
| | | (magic * sqrtMagic)) * |
| | | PI); |
| | | deltaLongitude = |
| | | (deltaLongitude * 180) / |
| | | ((EARTH_SEMI_MAJOR_AXIS / sqrtMagic) * Math.cos(radianLatitude) * PI); |
| | | return [longitude + deltaLongitude, latitude + deltaLatitude]; |
| | | } |
| | | |
| | | /** GCJ02 â WGS84ï¼è¿ä»£é¼è¿ï¼è¯¯å·®å¯æ§å¶å¨ 0.5 ç±³å
*/ |
| | | function gcj02ToWgs84(longitude: number, latitude: number): [number, number] { |
| | | if (outOfChina(longitude, latitude)) { |
| | | return [longitude, latitude]; |
| | | } |
| | | let wgsLongitude = longitude; |
| | | let wgsLatitude = latitude; |
| | | for (let index = 0; index < 5; index++) { |
| | | const [gcjLongitude, gcjLatitude] = wgs84ToGcj02(wgsLongitude, wgsLatitude); |
| | | wgsLongitude += longitude - gcjLongitude; |
| | | wgsLatitude += latitude - gcjLatitude; |
| | | } |
| | | return [wgsLongitude, wgsLatitude]; |
| | | } |
| | | |
| | | /** GCJ02 â BD09 */ |
| | | function gcj02ToBd09(longitude: number, latitude: number): [number, number] { |
| | | const z = |
| | | Math.sqrt(longitude * longitude + latitude * latitude) + |
| | | 0.000_02 * Math.sin(latitude * X_PI); |
| | | const theta = |
| | | Math.atan2(latitude, longitude) + 0.000_003 * Math.cos(longitude * X_PI); |
| | | return [z * Math.cos(theta) + 0.0065, z * Math.sin(theta) + 0.006]; |
| | | } |
| | | |
| | | /** BD09 â GCJ02 */ |
| | | function bd09ToGcj02(longitude: number, latitude: number): [number, number] { |
| | | const x = longitude - 0.0065; |
| | | const y = latitude - 0.006; |
| | | const z = Math.sqrt(x * x + y * y) - 0.000_02 * Math.sin(y * X_PI); |
| | | const theta = Math.atan2(y, x) - 0.000_003 * Math.cos(x * X_PI); |
| | | return [z * Math.cos(theta), z * Math.sin(theta)]; |
| | | } |
| | | |
| | | function wgs84ToBd09(longitude: number, latitude: number): [number, number] { |
| | | return gcj02ToBd09(...wgs84ToGcj02(longitude, latitude)); |
| | | } |
| | | |
| | | function bd09ToWgs84(longitude: number, latitude: number): [number, number] { |
| | | return gcj02ToWgs84(...bd09ToGcj02(longitude, latitude)); |
| | | } |
| | | |
| | | function updateOverlay( |
| | | longitude: number, |
| | | latitude: number, |
| | | centerMap = false, |
| | | ) { |
| | | if (!map || !window.BMapGL) { |
| | | return; |
| | | } |
| | | const point = new window.BMapGL.Point(longitude, latitude); |
| | | if (!marker) { |
| | | marker = new window.BMapGL.Marker(point); |
| | | map.addOverlay(marker); |
| | | } else { |
| | | marker.setPosition(point); |
| | | } |
| | | if (!rangeCircle) { |
| | | rangeCircle = new window.BMapGL.Circle(point, props.radiusMeters, { |
| | | fillColor: '#1677ff', |
| | | fillOpacity: 0.16, |
| | | strokeColor: '#1677ff', |
| | | strokeOpacity: 0.8, |
| | | strokeWeight: 2, |
| | | }); |
| | | map.addOverlay(rangeCircle); |
| | | } else { |
| | | rangeCircle.setCenter(point); |
| | | rangeCircle.setRadius(props.radiusMeters); |
| | | } |
| | | if (centerMap) { |
| | | map.centerAndZoom(point, 19); |
| | | } |
| | | } |
| | | |
| | | function reverseGeocode(longitude: number, latitude: number) { |
| | | return new Promise<{ address?: string; name?: string }>((resolve) => { |
| | | if (!geocoder) { |
| | | resolve({}); |
| | | return; |
| | | } |
| | | geocoder.getLocation( |
| | | new window.BMapGL.Point(longitude, latitude), |
| | | (result: any) => { |
| | | const components = result?.addressComponents ?? {}; |
| | | const nearestPoi = result?.surroundingPois?.[0]?.title; |
| | | resolve({ |
| | | address: result?.address, |
| | | name: |
| | | nearestPoi || |
| | | components.street || |
| | | components.district || |
| | | components.city, |
| | | }); |
| | | }, |
| | | ); |
| | | }); |
| | | } |
| | | |
| | | /** å°å¾åæ ï¼BD09ï¼è½ç¹åï¼åä¼ WGS84 ç»è¡¨å */ |
| | | async function selectBaiduLocation( |
| | | longitude: number, |
| | | latitude: number, |
| | | centerMap = true, |
| | | ) { |
| | | updateOverlay(longitude, latitude, centerMap); |
| | | const [wgsLongitude, wgsLatitude] = bd09ToWgs84(longitude, latitude); |
| | | const location = await reverseGeocode(longitude, latitude); |
| | | selectedAddress.value = location.address ?? ''; |
| | | emit('select', { |
| | | address: location.address, |
| | | latitude: Number(wgsLatitude.toFixed(7)), |
| | | locationName: location.name, |
| | | longitude: Number(wgsLongitude.toFixed(7)), |
| | | }); |
| | | } |
| | | |
| | | async function handleSearch() { |
| | | const keyword = searchKeyword.value.trim(); |
| | | if (!keyword || !geocoder) { |
| | | return; |
| | | } |
| | | loading.value = true; |
| | | try { |
| | | const point = await new Promise<any>((resolve, reject) => { |
| | | geocoder.getPoint(keyword, (result: any) => { |
| | | result |
| | | ? resolve(result) |
| | | : reject( |
| | | new Error('没ææç´¢å°è¯¥å°åï¼è¯·è¾å
¥æ´å®æ´ççå¸åºåå°ç¹åç§°'), |
| | | ); |
| | | }); |
| | | }); |
| | | await selectBaiduLocation(point.lng, point.lat); |
| | | } catch (error: any) { |
| | | message.warning(error?.message || 'å°åæç´¢å¤±è´¥'); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | function handleCurrentLocation() { |
| | | if (!navigator.geolocation) { |
| | | message.warning('å½åæµè§å¨ä¸æ¯æå®ä½'); |
| | | return; |
| | | } |
| | | loading.value = true; |
| | | navigator.geolocation.getCurrentPosition( |
| | | async (position) => { |
| | | const [longitude, latitude] = wgs84ToBd09( |
| | | position.coords.longitude, |
| | | position.coords.latitude, |
| | | ); |
| | | await selectBaiduLocation(longitude, latitude); |
| | | loading.value = false; |
| | | }, |
| | | () => { |
| | | message.warning( |
| | | 'å®ä½å¤±è´¥ï¼è¯·å¼å¯æµè§å¨å®ä½æéï¼é localhost ç¯å¢éä½¿ç¨ HTTPSï¼', |
| | | ); |
| | | loading.value = false; |
| | | }, |
| | | { enableHighAccuracy: true, maximumAge: 0, timeout: 15_000 }, |
| | | ); |
| | | } |
| | | |
| | | async function initializeMap() { |
| | | if (!hasMapConfig.value || !mapContainer.value) { |
| | | return; |
| | | } |
| | | loading.value = true; |
| | | try { |
| | | await loadBaiduMapSdk(15_000); |
| | | await nextTick(); |
| | | const hasCoordinates = |
| | | Number.isFinite(props.longitude) && Number.isFinite(props.latitude); |
| | | const center = hasCoordinates |
| | | ? wgs84ToBd09(Number(props.longitude), Number(props.latitude)) |
| | | : DEFAULT_CENTER; |
| | | map = new window.BMapGL.Map(mapContainer.value); |
| | | map.centerAndZoom( |
| | | new window.BMapGL.Point(center[0], center[1]), |
| | | hasCoordinates ? 19 : 12, |
| | | ); |
| | | map.enableScrollWheelZoom(true); |
| | | map.addControl(new window.BMapGL.ZoomControl()); |
| | | map.addControl(new window.BMapGL.ScaleControl()); |
| | | geocoder = new window.BMapGL.Geocoder(); |
| | | map.addEventListener('click', (event: any) => { |
| | | if (!props.disabled) { |
| | | void selectBaiduLocation(event.latlng.lng, event.latlng.lat, false); |
| | | } |
| | | }); |
| | | if (hasCoordinates) { |
| | | updateOverlay(center[0], center[1]); |
| | | const location = await reverseGeocode(center[0], center[1]); |
| | | selectedAddress.value = location.address ?? ''; |
| | | } |
| | | } catch (error: any) { |
| | | message.error(error?.message || 'å°å¾åå§å失败'); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | watch( |
| | | () => [props.longitude, props.latitude] as const, |
| | | ([longitude, latitude]) => { |
| | | if ( |
| | | !map || |
| | | !Number.isFinite(longitude) || |
| | | !Number.isFinite(latitude) |
| | | ) { |
| | | return; |
| | | } |
| | | const point = wgs84ToBd09(Number(longitude), Number(latitude)); |
| | | updateOverlay(point[0], point[1]); |
| | | }, |
| | | ); |
| | | |
| | | watch( |
| | | () => props.radiusMeters, |
| | | (radius) => { |
| | | if (Number.isFinite(radius)) { |
| | | rangeCircle?.setRadius(Number(radius)); |
| | | } |
| | | }, |
| | | ); |
| | | |
| | | onMounted(initializeMap); |
| | | |
| | | onBeforeUnmount(() => { |
| | | map?.clearOverlays?.(); |
| | | map?.destroy?.(); |
| | | map = undefined; |
| | | marker = undefined; |
| | | rangeCircle = undefined; |
| | | geocoder = undefined; |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Alert |
| | | v-if="!hasMapConfig" |
| | | type="warning" |
| | | show-icon |
| | | message="ç¾åº¦å°å¾å°æªé
ç½®" |
| | | description="请é
ç½® VITE_BAIDU_MAP_KEY åéæ°æå»ºåç«¯ï¼æªé
ç½®æ¶å¯æå·¥å¡«åç»çº¬åº¦ã" |
| | | /> |
| | | <Spin v-else :spinning="loading"> |
| | | <div v-if="!disabled" class="mb-2 flex gap-2"> |
| | | <Input |
| | | v-model:value="searchKeyword" |
| | | allow-clear |
| | | placeholder="è¾å
¥çãå¸ãåºåå
·ä½å°ç¹ï¼ä¾å¦ï¼ä¹é²æ¨é½å¸ååº" |
| | | @press-enter="handleSearch" |
| | | /> |
| | | <Button :loading="loading" @click="handleSearch">æç´¢å°å</Button> |
| | | <Button @click="handleCurrentLocation" v-if="false">å®ä½å°å½åä½ç½®</Button> |
| | | </div> |
| | | <div |
| | | ref="mapContainer" |
| | | class="h-[360px] w-full overflow-hidden rounded border border-gray-200" |
| | | ></div> |
| | | <div |
| | | class="mt-2 flex flex-wrap justify-between gap-2 text-xs text-gray-500" |
| | | > |
| | | <span> |
| | | {{ |
| | | disabled |
| | | ? 'æå¡ä½ç½®åå
许èå´' |
| | | : 'ç¹å»å°å¾éæ©æå¡ä¸å¿ç¹ï¼èè²åå表示å
许æå¡èå´' |
| | | }} |
| | | </span> |
| | | <span v-if="selectedAddress">{{ selectedAddress }}</span> |
| | | </div> |
| | | </Spin> |
| | | </template> |
| | |
| | | import type { MesMdItemApi } from "#/api/mes/md/item"; |
| | | import type { MesMdItemTypeApi } from "#/api/mes/md/item/type"; |
| | | |
| | | import { nextTick, ref } from "vue"; |
| | | import { computed, nextTick, ref } from "vue"; |
| | | |
| | | import { CommonStatusEnum } from "@vben/constants"; |
| | | |
| | | import { Card, message, Modal } from "ant-design-vue"; |
| | | import { Alert, Card, message, Modal, Tag } from "ant-design-vue"; |
| | | |
| | | import { useVbenVxeGrid } from "#/adapter/vxe-table"; |
| | | import { getItemPage } from "#/api/mes/md/item"; |
| | |
| | | const selectedItemTypeId = ref<number>(); // å½åçéåç±»ç¼å· |
| | | const preSelectedIds = ref<number[]>([]); // é¢éç©æç¼å·å表 |
| | | const productIds = ref<number[]>([]); // éå®å¯éçç©æç¼å·å表ï¼ä¸ºç©ºåä¸éå¶ï¼ |
| | | const limitedToProducts = computed(() => productIds.value.length > 0); |
| | | const dialogTitle = computed(() => limitedToProducts.value ? "路线产åºéæ©" : "ç©æäº§åéæ©"); |
| | | const tableTitle = computed(() => limitedToProducts.value ? "å·²é
ç½®ç产åºäº§å" : "ç©æäº§åå表"); |
| | | const typeTreeRef = ref<InstanceType<typeof MdItemTypeTree>>(); // ç©æåç±»æ |
| | | |
| | | /** è·åå½åè¡¨æ ¼æ°æ® */ |
| | |
| | | }, |
| | | gridOptions: { |
| | | columns: useItemSelectGridColumns(true), |
| | | height: 560, |
| | | height: '100%', |
| | | keepSource: true, |
| | | checkboxConfig: { |
| | | highlight: true, |
| | |
| | | |
| | | <template> |
| | | <Modal v-model:open="open" |
| | | title="ç©æäº§åéæ©" |
| | | :title="dialogTitle" |
| | | width="80%" |
| | | :destroy-on-close="true" |
| | | @ok="handleConfirm" |
| | | @cancel="closeModal"> |
| | | <div class="flex h-full w-full"> |
| | | <Card class="mr-4 h-full w-1/5"> |
| | | <MdItemTypeTree ref="typeTreeRef" |
| | | @node-click="handleItemTypeNodeClick" /> |
| | | </Card> |
| | | <div class="w-4/5"> |
| | | <Grid table-title="ç©æäº§åå表" /> |
| | | <div class="flex h-[calc(100vh-300px)] min-h-[380px] w-full flex-col"> |
| | | <Alert |
| | | v-if="limitedToProducts" |
| | | class="mb-3 shrink-0" |
| | | message="ä»
å¯éæ©æ¬äº§åå¨å·¥èºè·¯çº¿ä¸å·²é
ç½®ç产åºç©æ" |
| | | description="å¦éå¢å å¯é项ï¼è¯·å
å¨å·¥èºè·¯çº¿å¯¹åºå·¥åºä¸é
置产åºå¹¶ä¿åã" |
| | | show-icon |
| | | type="info" |
| | | > |
| | | <template #action><Tag color="blue">{{ productIds.length }} 个产åº</Tag></template> |
| | | </Alert> |
| | | <div class="flex min-h-0 w-full flex-1 gap-4"> |
| | | <Card v-if="!limitedToProducts" class="h-full w-1/5 shrink-0" :body-style="{ height: '100%', overflow: 'hidden' }"> |
| | | <MdItemTypeTree ref="typeTreeRef" @node-click="handleItemTypeNodeClick" /> |
| | | </Card> |
| | | <div class="h-full min-h-0 min-w-0 flex-1"> |
| | | <Grid class="h-full" :table-title="tableTitle" /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </Modal> |
| | |
| | | export { default as RouteColorPicker } from './color-picker.vue'; |
| | | export { default as RouteOutputAttribution } from './output-attribution.vue'; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process'; |
| | | import type { MesProRouteProductApi } from '#/api/mes/pro/route/product'; |
| | | |
| | | import { computed } from 'vue'; |
| | | |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { Button, Select } from 'ant-design-vue'; |
| | | |
| | | defineOptions({ name: 'RouteOutputAttribution' }); |
| | | |
| | | /** å½å±éæ©ä¸ä»£è¡¨âå¤äº§åéç¨âçå¨å
µå¼ï¼äº§åç¼å·åä¸ºæ£æ°ï¼ */ |
| | | const GENERAL_PRODUCT = 0; |
| | | |
| | | const props = withDefaults( |
| | | defineProps<{ |
| | | disabled?: boolean; |
| | | modelValue?: MesProRouteProcessApi.OutputItem[]; |
| | | products?: MesProRouteProductApi.RouteProduct[]; |
| | | }>(), |
| | | { |
| | | disabled: false, |
| | | modelValue: () => [], |
| | | products: () => [], |
| | | }, |
| | | ); |
| | | |
| | | const emit = defineEmits<{ |
| | | 'update:modelValue': [value: MesProRouteProcessApi.OutputItem[]]; |
| | | }>(); |
| | | |
| | | const rows = computed(() => props.modelValue ?? []); |
| | | const productOptions = computed(() => |
| | | props.products |
| | | .filter((product) => product.itemId != null) |
| | | .map((product) => ({ |
| | | label: product.itemName ?? product.itemCode ?? String(product.itemId), |
| | | value: product.itemId!, |
| | | })), |
| | | ); |
| | | const attributionOptions = computed(() => [ |
| | | { label: 'éç¨ï¼ææäº§åï¼', value: GENERAL_PRODUCT }, |
| | | ...productOptions.value, |
| | | ]); |
| | | |
| | | function commit(next: MesProRouteProcessApi.OutputItem[]) { |
| | | emit('update:modelValue', next); |
| | | } |
| | | |
| | | function updateRow( |
| | | index: number, |
| | | patch: Partial<MesProRouteProcessApi.OutputItem>, |
| | | ) { |
| | | commit(rows.value.map((row, i) => (i === index ? { ...row, ...patch } : row))); |
| | | } |
| | | |
| | | /** éæ©äº§åºç©ææ¶ï¼é»è®¤å½å±å°èªèº«äº§åï¼åæåå
±ç¨æ¶åæå¨æ¹æâéç¨âï¼ */ |
| | | function handleItemChange(index: number, itemId: number | undefined) { |
| | | const current = rows.value[index]; |
| | | const patch: Partial<MesProRouteProcessApi.OutputItem> = { itemId }; |
| | | // 忥å½å±èªèº«æ¶ï¼åæ¢äº§åºç©æä¹è¦åæ¥å½å±ï¼é¿å
ç䏿§äº§åç¼å·ã |
| | | if ((current?.productId == null || current.productId === current.itemId) && itemId != null) { |
| | | patch.productId = itemId; |
| | | } |
| | | updateRow(index, patch); |
| | | } |
| | | |
| | | function handleAttributionChange(index: number, value: number | undefined) { |
| | | updateRow(index, { |
| | | productId: value === GENERAL_PRODUCT || value == null ? undefined : value, |
| | | }); |
| | | } |
| | | |
| | | function addRow() { |
| | | commit([...rows.value, { itemId: undefined, productId: undefined }]); |
| | | } |
| | | |
| | | function removeRow(index: number) { |
| | | commit(rows.value.filter((_, i) => i !== index)); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="w-full"> |
| | | <div class="flex flex-col gap-2"> |
| | | <div |
| | | v-for="(row, index) in rows" |
| | | :key="index" |
| | | class="flex items-center gap-2" |
| | | > |
| | | <Select |
| | | :disabled="disabled" |
| | | :options="productOptions" |
| | | :placeholder="'éæ©äº§åºç©æ'" |
| | | :value="row.itemId" |
| | | class="min-w-0 flex-1" |
| | | option-filter-prop="label" |
| | | show-search |
| | | @change="(value) => handleItemChange(index, value as number | undefined)" |
| | | /> |
| | | <Select |
| | | :disabled="disabled || row.itemId == null" |
| | | :options="attributionOptions" |
| | | :value="row.productId ?? GENERAL_PRODUCT" |
| | | class="min-w-0 flex-1" |
| | | @change="(value) => handleAttributionChange(index, value as number | undefined)" |
| | | /> |
| | | <Button |
| | | :aria-label="`å é¤ç¬¬ ${index + 1} æ¡äº§åºé
ç½®`" |
| | | :disabled="disabled" |
| | | danger |
| | | size="small" |
| | | type="text" |
| | | @click="removeRow(index)" |
| | | > |
| | | <IconifyIcon class="size-4" icon="lucide:trash-2" /> |
| | | </Button> |
| | | </div> |
| | | <div v-if="rows.length === 0" class="text-sm text-gray-500"> |
| | | ææ äº§åºï¼å½¢æåæåææåæ¶æ·»å |
| | | </div> |
| | | </div> |
| | | <Button |
| | | v-if="!disabled" |
| | | class="mt-2" |
| | | size="small" |
| | | type="dashed" |
| | | @click="addRow" |
| | | > |
| | | <IconifyIcon class="mr-1 size-4" icon="lucide:plus" /> |
| | | æ·»å äº§åº |
| | | </Button> |
| | | </div> |
| | | </template> |
| | |
| | | import { z } from '#/adapter/form'; |
| | | import { generateAutoCode } from '#/api/mes/md/autocode/record'; |
| | | import { getTemplatePage } from '#/api/mes/pro/processdesign/template'; |
| | | import { getWarehouseSimpleList } from '#/api/mes/wm/warehouse'; |
| | | import { getWarehouseAreaSimpleList } from '#/api/mes/wm/warehouse/area'; |
| | | import { getWarehouseLocationSimpleList } from '#/api/mes/wm/warehouse/location'; |
| | | import { |
| | | MdItemSelect, |
| | | MdProductBomSelect, |
| | | } from '#/views/mes/md/item/components'; |
| | | |
| | | import { RouteColorPicker } from './components'; |
| | | import { RouteColorPicker, RouteOutputAttribution } from './components'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'update'; |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDate', |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | /** å·¥èºè·¯çº¿å·¥åºæç»è¡¨å */ |
| | | export function useRouteProcessFormSchema( |
| | | processOptions: Array<{ label: string; value: number }>, |
| | | products: MesProRouteProductApi.RouteProduct[] = [], |
| | | ): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | |
| | | }, |
| | | { |
| | | fieldName: 'yieldRate', |
| | | label: 'æ¶ç', |
| | | label: 'å·¥åºæ¶ç', |
| | | component: 'InputNumber', |
| | | help: '产åºé ÷ æå
¥éï¼æªå¡«åæ 100% åä¸æäº§è®¡ç®ã', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | min: 0.0001, |
| | | max: 1, |
| | | step: 0.01, |
| | | precision: 4, |
| | | placeholder: '产åº/æå
¥ï¼å¦ 0.98', |
| | | placeholder: 'å¦ 0.98ï¼98%ï¼', |
| | | }, |
| | | rules: z.number().min(0.0001).max(1).nullish(), |
| | | }, |
| | | { |
| | | fieldName: 'outputs', |
| | | label: 'æ¬å·¥åºäº§åº', |
| | | component: markRaw(RouteOutputAttribution), |
| | | componentProps: { |
| | | products, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'linkType', |
| | | label: 'ä¸ä¸éå·¥åºå
³ç³»', |
| | | component: 'Select', |
| | | help: () => |
| | | h( |
| | | 'div', |
| | | { |
| | | style: |
| | | 'white-space: pre-line; max-width: 320px; line-height: 1.6;', |
| | | }, |
| | | '0=å¼å§-å¼å§(SS)ï¼æ¬å·¥åºå¼å§åï¼ä¸éå·¥åºæè½å¼å§ï¼å¯éå å¹¶è¡å¼å·¥ï¼\n1=ç»æ-ç»æ(FF)ï¼ä¸éå·¥åºå¿
é¡»çå°æ¬å·¥åºç»ææè½ç»æï¼å¹¶è¡ä½åæ¥å®å·¥ï¼\n2=å¼å§-ç»æ(SF)ï¼æ¬å·¥åºå¼å§åï¼ä¸éå·¥åºæè½ç»æ\n3=ç»æ-å¼å§(FS)ï¼æ¬å·¥åºç»æåï¼ä¸éå·¥åºæè½å¼å§ï¼é»è®¤å¼ï¼', |
| | | ), |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.MES_PRO_LINK_TYPE, 'number'), |
| | | placeholder: 'è¯·éæ©', |
| | | }, |
| | | rules: z.number().optional(), |
| | | rules: z.number().default(3), |
| | | }, |
| | | { |
| | | fieldName: 'colorCode', |
| | |
| | | componentProps: { |
| | | checkedChildren: 'æ¯', |
| | | unCheckedChildren: 'å¦', |
| | | }, |
| | | rules: z.boolean().default(false), |
| | | }, |
| | | { |
| | | fieldName: 'backflushFlag', |
| | | label: 'æ¥å·¥èªå¨æ£æ', |
| | | component: 'Switch', |
| | | componentProps: { |
| | | checkedChildren: 'èªå¨', |
| | | unCheckedChildren: 'æå¨', |
| | | }, |
| | | rules: z.boolean().default(false), |
| | | }, |
| | |
| | | { field: 'sort', title: 'åºå·', width: 70, align: 'center', fixed: 'left' }, |
| | | { field: 'processCode', title: 'å·¥åºç¼ç ', width: 140, fixed: 'left' }, |
| | | { field: 'processName', title: 'å·¥åºåç§°', width: 140, fixed: 'left' }, |
| | | { |
| | | field: 'outputItemNames', |
| | | title: 'æ¬å·¥åºäº§åº', |
| | | minWidth: 240, |
| | | formatter: ({ cellValue, row }) => { |
| | | const outputs = row.outputs ?? []; |
| | | if (!outputs.length) { |
| | | return cellValue ?? ''; |
| | | } |
| | | return outputs |
| | | .map((output) => { |
| | | const name = output.itemName ?? output.itemCode ?? output.itemId; |
| | | return output.productId == null |
| | | ? `${name}ï¼éç¨ï¼` |
| | | : `${name}ï¼${output.productName ?? output.productId}ï¼`; |
| | | }) |
| | | .join('ã'); |
| | | }, |
| | | }, |
| | | { field: 'nextProcessName', title: 'ä¸ä¸éå·¥åº', width: 140 }, |
| | | { |
| | | field: 'linkType', |
| | |
| | | props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'backflushFlag', |
| | | title: 'èªå¨æ£æ', |
| | | width: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING }, |
| | | }, |
| | | }, |
| | | { field: 'prepareTime', title: 'å夿¶é´(å)', width: 110 }, |
| | | { field: 'waitTime', title: 'çå¾
æ¶é´(å)', width: 110 }, |
| | | { |
| | |
| | | /** å·¥èºè·¯çº¿äº§ååè¡¨åæ®µ */ |
| | | export function useRouteProductGridColumns(): VxeTableGridOptions<MesProRouteProductApi.RouteProduct>['columns'] { |
| | | return [ |
| | | { field: 'itemCode', title: '产åç©æç¼ç ', width: 150 }, |
| | | { field: 'itemName', title: '产åç©æåç§°', width: 150 }, |
| | | { field: 'itemCode', title: '产åºç¼ç ', width: 150 }, |
| | | { field: 'itemName', title: '产åºåç§°', width: 150 }, |
| | | { field: 'specification', title: 'è§æ ¼åå·', width: 150 }, |
| | | { field: 'unitName', title: 'åä½', width: 80 }, |
| | | { field: 'quantity', title: 'ç产æ°é', width: 100 }, |
| | | { field: 'warehouseName', title: 'æ¥å·¥å
¥åºä»', width: 140 }, |
| | | { field: 'locationName', title: 'å
¥åºåºåº', width: 120 }, |
| | | { field: 'areaName', title: 'å
¥åºåºä½', width: 120 }, |
| | | { |
| | | field: 'productionTime', |
| | | title: 'çäº§ç¨æ¶', |
| | |
| | | /** å·¥èºè·¯çº¿äº§å BOM åè¡¨åæ®µ */ |
| | | export function useRouteProductBomGridColumns(): VxeTableGridOptions<MesProRouteProductBomApi.RouteProductBom>['columns'] { |
| | | return [ |
| | | { field: 'itemCode', title: 'BOM ç©æç¼ç ', width: 150 }, |
| | | { field: 'itemName', title: 'BOM ç©æåç§°', width: 150 }, |
| | | { field: 'itemCode', title: 'ææç¼ç ', width: 150 }, |
| | | { field: 'itemName', title: 'ææåç§°', width: 150 }, |
| | | { field: 'specification', title: 'è§æ ¼åå·', width: 150 }, |
| | | { field: 'unitName', title: 'åä½', width: 80 }, |
| | | { field: 'quantity', title: 'ç¨ææ¯ä¾', width: 100 }, |
| | |
| | | /** å·¥èºè·¯çº¿äº§å表å */ |
| | | export function useRouteProductFormSchema( |
| | | onItemChange?: (item: any) => void, |
| | | getFormApi?: () => VbenFormApi, |
| | | ): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | |
| | | // 产åç©æï¼ä½¿ç¨ä¸å¡èªå®ä¹éæ©å¨ï¼change åå¡«ç¼ç /åç§°/è§æ ¼/åä½ |
| | | { |
| | | fieldName: 'itemId', |
| | | label: '产å', |
| | | label: '产åºç©æ', |
| | | component: markRaw(MdItemSelect), |
| | | componentProps: { |
| | | onChange: onItemChange, |
| | | }, |
| | | help: 'å¯éæ©ä¸»æ°æ®ç±»å为æåæåæåçç©æï¼ä¹å
¼å®¹ MES æå/åæååç±»ï¼å·²æè·¯çº¿äº§åå¯ç»§ç»ä¿®æ¹ä»ä½ã', |
| | | formItemClass: 'col-span-2', |
| | | rules: 'selectRequired', |
| | | }, |
| | |
| | | rules: z.number().default(1), |
| | | }, |
| | | { |
| | | fieldName: 'warehouseId', |
| | | label: 'æ¥å·¥ä»åº', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | api: getWarehouseSimpleList, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | placeholder: 'è¯·éæ©æ¥å·¥äº§åºä»åº', |
| | | onChange: async () => { |
| | | await getFormApi?.().setFieldValue('locationId', undefined); |
| | | await getFormApi?.().setFieldValue('areaId', undefined); |
| | | }, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'locationId', |
| | | label: 'æ¥å·¥åºåº', |
| | | component: 'Select', |
| | | dependencies: { |
| | | triggerFields: ['warehouseId'], |
| | | disabled: (values) => !values.warehouseId, |
| | | async componentProps(values) { |
| | | const options = values.warehouseId |
| | | ? await getWarehouseLocationSimpleList(values.warehouseId) |
| | | : []; |
| | | return { |
| | | fieldNames: { label: 'name', value: 'id' }, |
| | | options, |
| | | placeholder: 'è¯·éæ©æ¥å·¥äº§åºåºåº', |
| | | onChange: async () => getFormApi?.().setFieldValue('areaId', undefined), |
| | | }; |
| | | }, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'areaId', |
| | | label: 'æ¥å·¥åºä½', |
| | | component: 'Select', |
| | | dependencies: { |
| | | triggerFields: ['locationId'], |
| | | disabled: (values) => !values.locationId, |
| | | async componentProps(values) { |
| | | const options = values.locationId |
| | | ? await getWarehouseAreaSimpleList(values.locationId) |
| | | : []; |
| | | return { |
| | | fieldNames: { label: 'name', value: 'id' }, |
| | | options, |
| | | placeholder: 'è¯·éæ©æ¥å·¥äº§åºåºä½', |
| | | }; |
| | | }, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'productionTime', |
| | | label: 'çäº§ç¨æ¶', |
| | | component: 'InputNumber', |
| | |
| | | import { getDictLabel } from '@vben/hooks'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button, message, Tooltip } from 'ant-design-vue'; |
| | | import { Alert, Button, message, Tooltip } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteRoute, |
| | | exportRoute, |
| | | getRoutePage, |
| | | importRoute, |
| | | importRouteTemplate, |
| | | updateRouteStatus, |
| | | } from '#/api/mes/pro/route'; |
| | | import ImportModalComponent from '#/components/import-modal'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | import Form from './modules/route-configurator.vue'; |
| | | |
| | | const { hasAccessByCodes } = useAccess(); |
| | | const statusEditable = hasAccessByCodes(['mes:pro-route:update']); // æ¯å¦å¯åæ¢ç¶æ |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ImportModal, importModalApi] = useVbenModal({ |
| | | connectedComponent: ImportModalComponent, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | |
| | | /** å建工èºè·¯çº¿ */ |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | /** 导å
¥å·¥èºè·¯çº¿ */ |
| | | function handleImport() { |
| | | importModalApi |
| | | .setData({ |
| | | title: $t('ui.actionTitle.import', ['å·¥èºè·¯çº¿']), |
| | | fileName: 'å·¥èºè·¯çº¿å¯¼å
¥æ¨¡æ¿.xls', |
| | | importFn: importRoute, |
| | | templateFn: importRouteTemplate, |
| | | }) |
| | | .open(); |
| | | } |
| | | |
| | | /** ç¼è¾å·¥èºè·¯çº¿ï¼ä»
åç¨ç¶æå¯ç¼è¾ï¼ */ |
| | |
| | | ): Promise<boolean | undefined> { |
| | | try { |
| | | await confirm( |
| | | `确认è¦å°"${row.name}"å·¥èºè·¯çº¿åæ¢ä¸ºã${getDictLabel(DICT_TYPE.COMMON_STATUS, newStatus)}ãåï¼`, |
| | | newStatus === CommonStatusEnum.ENABLE |
| | | ? `确认å¯ç¨â${row.name}âåï¼ç³»ç»å°æ£æ¥äº§åºå
¥åºä½ç½®ãå·¥åºäº§åºåå·¥åºæææ¯å¦å®æ´ã` |
| | | : `确认è¦å°â${row.name}âå·¥èºè·¯çº¿åæ¢ä¸ºã${getDictLabel(DICT_TYPE.COMMON_STATUS, newStatus)}ãåï¼`, |
| | | ); |
| | | } catch { |
| | | return false; |
| | |
| | | |
| | | <template> |
| | | <Page auto-content-height><FormModal @success="handleRefresh" /> |
| | | <ImportModal @success="handleRefresh" /> |
| | | <Alert |
| | | class="mb-3" |
| | | message="路线é
置顺åº" |
| | | description="åºæ¬ä¿¡æ¯ â 产åºäº§ååå
¥åºä» â å·¥åºååå·¥åºäº§åº â å·¥åºææ â æ ¡éªå¯ç¨" |
| | | show-icon |
| | | type="info" |
| | | /> |
| | | <Grid table-title="å·¥èºè·¯çº¿å表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.import', ['å·¥èºè·¯çº¿']), |
| | | icon: ACTION_ICON.UPLOAD, |
| | | auth: ['mes:pro-route:import'], |
| | | onClick: handleImport, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.export'), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.DOWNLOAD, |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormType } from '../data'; |
| | | import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process'; |
| | | import type { MesProRouteProductApi } from '#/api/mes/pro/route/product'; |
| | | import type { MesProRouteProductBomApi } from '#/api/mes/pro/route/productbom'; |
| | | |
| | | import { computed, ref, watch } from 'vue'; |
| | | |
| | | import { Alert, Button, Empty, Tag } from 'ant-design-vue'; |
| | | |
| | | import { getRouteProductBomList } from '#/api/mes/pro/route/productbom'; |
| | | |
| | | import ProductBomList from './product-bom-list.vue'; |
| | | |
| | | const props = defineProps<{ |
| | | formType: FormType; |
| | | processes: MesProRouteProcessApi.RouteProcess[]; |
| | | products: MesProRouteProductApi.RouteProduct[]; |
| | | routeId: number; |
| | | }>(); |
| | | |
| | | const emit = defineEmits<{ |
| | | change: [list: MesProRouteProductBomApi.RouteProductBom[]]; |
| | | }>(); |
| | | |
| | | const activeProductId = ref<number>(); |
| | | const allBomList = ref<MesProRouteProductBomApi.RouteProductBom[]>([]); |
| | | |
| | | const activeProduct = computed(() => |
| | | props.products.find((item) => item.itemId === activeProductId.value), |
| | | ); |
| | | |
| | | function materialCount(productId?: number) { |
| | | if (!productId) return 0; |
| | | return allBomList.value.filter((item) => item.productId === productId).length; |
| | | } |
| | | |
| | | async function loadAll() { |
| | | allBomList.value = (await getRouteProductBomList({ routeId: props.routeId })) || []; |
| | | emit('change', allBomList.value); |
| | | } |
| | | |
| | | watch( |
| | | () => props.products.map((item) => item.itemId).filter(Boolean), |
| | | (productIds) => { |
| | | if (!productIds.length) { |
| | | activeProductId.value = undefined; |
| | | return; |
| | | } |
| | | if (!activeProductId.value || !productIds.includes(activeProductId.value)) { |
| | | activeProductId.value = productIds[0]; |
| | | } |
| | | }, |
| | | { immediate: true }, |
| | | ); |
| | | |
| | | watch( |
| | | () => props.routeId, |
| | | (value) => value && loadAll(), |
| | | { immediate: true }, |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | | <Alert |
| | | class="mb-4" |
| | | message="æäº§åºäº§åé
ç½®æ¯éå·¥åºéè¦æå
¥çç©æ" |
| | | description="å
éæ©äº§åºäº§åï¼å忢å°å
·ä½å·¥åºæ·»å ææç©æåç¨ææ¯ä¾ãå¼å¯âæ¥å·¥èªå¨æ£æâçå·¥åºï¼ä¼å¨æ¥å·¥æ¶æè¿éçé
ç½®èªå¨æ£ååºåã" |
| | | show-icon |
| | | type="info" |
| | | /> |
| | | |
| | | <Empty |
| | | v-if="!products.length || !processes.length" |
| | | :description="!products.length ? '请å
å®æäº§åºäº§åé
ç½®' : '请å
宿工åºé
ç½®'" |
| | | /> |
| | | |
| | | <div v-else class="material-layout"> |
| | | <aside class="product-selector" aria-label="éæ©è¦é
ç½®ææç产åºäº§å"> |
| | | <div class="selector-heading"> |
| | | <strong>产åºäº§å</strong> |
| | | <span>{{ products.length }} 个</span> |
| | | </div> |
| | | <Button |
| | | v-for="product in products" |
| | | :key="product.itemId" |
| | | :class="['product-button', { active: product.itemId === activeProductId }]" |
| | | type="text" |
| | | @click="activeProductId = product.itemId" |
| | | > |
| | | <span class="product-copy"> |
| | | <strong>{{ product.itemName }}</strong> |
| | | <small>{{ product.itemCode }}</small> |
| | | </span> |
| | | <Tag :color="materialCount(product.itemId) ? 'success' : 'warning'"> |
| | | {{ materialCount(product.itemId) ? `${materialCount(product.itemId)} æ¡` : 'æªé
ç½®' }} |
| | | </Tag> |
| | | </Button> |
| | | </aside> |
| | | |
| | | <section v-if="activeProduct" class="material-detail"> |
| | | <header class="material-detail-header"> |
| | | <div> |
| | | <h3>{{ activeProduct.itemName }} · å·¥åºææ</h3> |
| | | <p>便¬¡åæ¢å·¥åºï¼ç»´æ¤è¯¥äº§åå¨è¯¥å·¥åºå®é
æ¶èçç©æã</p> |
| | | </div> |
| | | <Tag color="blue">{{ processes.length }} éå·¥åº</Tag> |
| | | </header> |
| | | <ProductBomList |
| | | :form-type="formType" |
| | | :product-id="activeProduct.itemId!" |
| | | :product-name="activeProduct.itemName" |
| | | :route-id="routeId" |
| | | @change="loadAll" |
| | | /> |
| | | </section> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .material-layout { display: grid; grid-template-columns: minmax(210px, 260px) minmax(0, 1fr); gap: 18px; } |
| | | .product-selector { overflow: hidden; align-self: start; border: 1px solid rgb(226 232 240); border-radius: 12px; background: rgb(248 250 252); } |
| | | .selector-heading { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; border-bottom: 1px solid rgb(226 232 240); color: rgb(15 23 42); } |
| | | .selector-heading span { color: rgb(100 116 139); font-size: 12px; } |
| | | .product-button { display: flex; width: 100%; height: auto; min-height: 62px; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 12px; border-radius: 0; text-align: left; } |
| | | .product-button + .product-button { border-top: 1px solid rgb(226 232 240); } |
| | | .product-button.active { background: rgb(219 234 254); color: rgb(29 78 216); } |
| | | .product-copy { display: grid; min-width: 0; gap: 2px; } |
| | | .product-copy strong, .product-copy small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
| | | .product-copy small { color: rgb(100 116 139); } |
| | | .material-detail { min-width: 0; padding: 18px; border: 1px solid rgb(226 232 240); border-radius: 12px; } |
| | | .material-detail-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; } |
| | | .material-detail-header h3 { margin: 0; color: rgb(15 23 42); font-size: 16px; font-weight: 650; } |
| | | .material-detail-header p { margin: 4px 0 0; color: rgb(100 116 139); font-size: 13px; } |
| | | :global(.dark) .product-selector, :global(.dark) .material-detail { border-color: rgb(51 65 85); background: rgb(15 23 42); } |
| | | :global(.dark) .selector-heading, :global(.dark) .product-button + .product-button { border-color: rgb(51 65 85); color: rgb(241 245 249); } |
| | | :global(.dark) .product-button.active { background: rgb(30 64 175 / 35%); color: rgb(147 197 253); } |
| | | :global(.dark) .material-detail-header h3 { color: rgb(241 245 249); } |
| | | @media (max-width: 900px) { |
| | | .material-layout { grid-template-columns: 1fr; } |
| | | .product-selector { display: flex; overflow-x: auto; } |
| | | .selector-heading { display: none; } |
| | | .product-button { min-width: 210px; border-top: 0 !important; border-right: 1px solid rgb(226 232 240); } |
| | | } |
| | | </style> |
| | |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | import { Alert, message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { getProcessSimpleList } from '#/api/mes/pro/process'; |
| | |
| | | getRouteProcess, |
| | | updateRouteProcess, |
| | | } from '#/api/mes/pro/route/process'; |
| | | import { getRouteProductListByRoute } from '#/api/mes/pro/route/product'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useRouteProcessFormSchema } from '../data'; |
| | |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | | formItemClass: 'col-span-1', |
| | | labelWidth: 150, |
| | | labelWidth: 130, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: [], |
| | |
| | | }); |
| | | |
| | | /** å 载工åºé项ååçæ schemaï¼é¿å
ä¸æç©ºéé¡¹ï¼ */ |
| | | async function loadSchema(): Promise<VbenFormSchema[]> { |
| | | async function loadSchema(routeId: number): Promise<VbenFormSchema[]> { |
| | | const list = await getProcessSimpleList(); |
| | | const options = (list || []).map((item) => ({ |
| | | label: item.name ?? '', |
| | | value: item.id!, |
| | | })); |
| | | return useRouteProcessFormSchema(options); |
| | | const products = routeId ? await getRouteProductListByRoute(routeId) : []; |
| | | return useRouteProcessFormSchema(options, products); |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | |
| | | formData.value = undefined; |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | // å·¥åºä¸æä¾èµè¿ç¨æ°æ®ï¼schema å¨å¼¹çªæå¼æ¶åçæ |
| | | const schema = await loadSchema(); |
| | | formApi.setState({ schema }); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | // å è½½æ°æ® |
| | | const data = modalApi.getData<{ |
| | | id?: number; |
| | | maxSort?: number; |
| | |
| | | if (!data) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | // å·¥åºä¸æä¾èµè¿ç¨æ°æ®ï¼schema å¨å¼¹çªæå¼æ¶åçæ |
| | | const schema = await loadSchema(data.routeId); |
| | | formApi.setState({ schema }); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | // å è½½æ°æ® |
| | | if (!data.id) { |
| | | // æ°å¢æ¶ï¼é»è®¤åºå· = maxSort + 1ï¼å¹¶ç»ä¸ä¸ªé»è®¤çç¹å¾é¢è² |
| | | await formApi.setValues({ |
| | |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-3/5"> |
| | | <Alert |
| | | class="mx-4 mb-4" |
| | | message="æå®é
ç产èç¹é
置产åº" |
| | | description="ä¸é´å·¥åºæ²¡æç¬ç«äº§åºæ¶å¯ç空ï¼å½¢æåæåæ¶éæ©å¯¹åºåæåï¼æ«éå·¥åºå¿
须鿩å
¨é¨æåãå½å±è¡¨ç¤ºè¯¥äº§åºå±äºåªä¸ªäº§åï¼å¤äº§åå
±ç¨çåæåéâéç¨âï¼äº§åä¸å±ï¼å°¤å
¶æ¯æåï¼å½å±å°èªèº«äº§åãå¦å¼å¯æ¥å·¥èªå¨æ£æï¼æ¥å·¥æ¶ä¼æå·¥åºææé
ç½®æ£ååºåã" |
| | | show-icon |
| | | type="info" |
| | | /> |
| | | <Form class="mx-4" /> |
| | | </Modal> |
| | | </template> |
| | |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process'; |
| | | |
| | | import { ref, watch } from 'vue'; |
| | | import { computed, ref, watch } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | |
| | | routeId: number; |
| | | }>(); |
| | | |
| | | const isEditable = ref(props.formType !== 'detail'); // æ¯å¦å¯ç¼è¾ |
| | | const emit = defineEmits<{ |
| | | change: [list: MesProRouteProcessApi.RouteProcess[]]; |
| | | }>(); |
| | | |
| | | const isEditable = computed(() => props.formType !== 'detail'); // æ¯å¦å¯ç¼è¾ |
| | | const list = ref<MesProRouteProcessApi.RouteProcess[]>([]); // å·¥èºè·¯çº¿å·¥åºå表 |
| | | const sortedList = computed(() => |
| | | [...list.value].sort((a, b) => (a.sort || 0) - (b.sort || 0)), |
| | | ); |
| | | |
| | | const [ProcessFormModal, processFormModalApi] = useVbenModal({ |
| | | connectedComponent: ProcessForm, |
| | |
| | | try { |
| | | list.value = await getRouteProcessListByRoute(props.routeId); |
| | | gridApi.setGridOptions({ data: list.value }); |
| | | emit('change', list.value); |
| | | } finally { |
| | | gridApi.setLoading(false); |
| | | } |
| | |
| | | |
| | | <template> |
| | | <ProcessFormModal @success="getList" /> |
| | | <div v-if="sortedList.length" class="mb-4 flex flex-wrap items-center gap-2 rounded-lg bg-slate-50 p-3 dark:bg-slate-900"> |
| | | <template v-for="(item, index) in sortedList" :key="item.id"> |
| | | <div class="flex items-center gap-2 rounded-md border border-slate-200 bg-white px-3 py-2 dark:border-slate-700 dark:bg-slate-800"> |
| | | <span class="text-xs font-semibold text-blue-600">{{ item.sort }}</span> |
| | | <span class="font-medium">{{ item.processName }}</span> |
| | | <Tag v-if="index === sortedList.length - 1" color="blue">æ«é</Tag> |
| | | </div> |
| | | <span v-if="index < sortedList.length - 1" class="text-slate-400">â</span> |
| | | </template> |
| | | </div> |
| | | <div v-if="isEditable" class="mb-3 flex items-center justify-start"> |
| | | <TableAction |
| | | :actions="[ |
| | |
| | | ]" |
| | | /> |
| | | </div> |
| | | <Grid class="w-full" table-title="ç»æå·¥åº"> |
| | | <Grid class="w-full" table-title="å·¥åºé¡ºåºååå·¥åºäº§åº"> |
| | | <template #colorCode="{ row }"> |
| | | <div v-if="row.colorCode" class="flex items-center justify-center gap-1"> |
| | | <div |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormType } from '../data'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process'; |
| | | import type { MesProRouteProductBomApi } from '#/api/mes/pro/route/productbom'; |
| | | |
| | | import { ref, watch } from 'vue'; |
| | | import { computed, ref, watch } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | |
| | | import BomForm from './bom-form.vue'; |
| | | |
| | | const props = defineProps<{ |
| | | formType: FormType; |
| | | productId: number; |
| | | productName?: string; |
| | | routeId: number; |
| | | }>(); |
| | | |
| | | const emit = defineEmits<{ |
| | | change: []; |
| | | }>(); |
| | | |
| | | const isEditable = computed(() => props.formType !== 'detail'); |
| | | |
| | | const processList = ref<MesProRouteProcessApi.RouteProcess[]>([]); // å·¥åºå表ï¼ç¨äº Tabï¼ |
| | | const activeProcessId = ref<number>(); // å½åéä¸çå·¥åº Tab |
| | |
| | | await deleteRouteProductBom(row.id!); |
| | | message.success($t('ui.actionMessage.deleteSuccess', ['BOM ç©æ'])); |
| | | await getList(); |
| | | emit('change'); |
| | | } |
| | | |
| | | async function handleSaved() { |
| | | await getList(); |
| | | emit('change'); |
| | | } |
| | | |
| | | watch( |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <BomFormModal @success="getList" /> |
| | | <BomFormModal @success="handleSaved" /> |
| | | <Tabs v-model:active-key="activeProcessId" @change="getList"> |
| | | <TabPane |
| | | v-for="item in processList" |
| | |
| | | :tab="item.processName" |
| | | /> |
| | | </Tabs> |
| | | <div class="mb-3 flex items-center justify-start"> |
| | | <div v-if="isEditable" class="mb-3 flex items-center justify-start"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['BOM ç©æ']), |
| | | label: $t('ui.actionTitle.create', ['ææç©æ']), |
| | | type: 'primary', |
| | | disabled: !activeProcessId, |
| | | onClick: handleCreate, |
| | |
| | | ]" |
| | | /> |
| | | </div> |
| | | <Grid class="w-full" table-title="产å BOM"> |
| | | <Grid class="w-full" table-title="å½åå·¥åºææ"> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | ifShow: () => isEditable, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | ifShow: () => isEditable, |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', ['BOM ç©æ']), |
| | | title: $t('ui.actionMessage.deleteConfirm', ['ææç©æ']), |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { Divider, message } from 'ant-design-vue'; |
| | | import { Alert, message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useRouteProductFormSchema } from '../data'; |
| | | import ProductBomList from './product-bom-list.vue'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formData = ref<MesProRouteProductApi.RouteProduct>(); |
| | |
| | | labelWidth: 100, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: useRouteProductFormSchema(handleItemChange), |
| | | schema: useRouteProductFormSchema(handleItemChange, () => formApi), |
| | | showDefaultActions: false, |
| | | wrapperClass: 'grid-cols-2', |
| | | }); |
| | |
| | | try { |
| | | if (formData.value?.id) { |
| | | await updateRouteProduct(data); |
| | | // ç¨ææ°è¡¨åå¼åæ¥ formDataï¼ç¡®ä¿äº§å BOM å表ç»å®ç itemId ä¸è¡¨åä¸è´ |
| | | formData.value = { ...formData.value, ...data }; |
| | | } else { |
| | | // æ°å¢æåå忢å°ç¼è¾æ¨¡å¼ï¼æ¹ä¾¿ç»§ç»ç»´æ¤äº§å BOM |
| | | const id = await createRouteProduct(data); |
| | | formData.value = { ...data, id }; |
| | | await formApi.setFieldValue('id', id); |
| | | await createRouteProduct(data); |
| | | } |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-3/5"> |
| | | <Alert |
| | | class="mx-4 mb-4" |
| | | message="è¿éç»´æ¤äº§åºåå
¶æ¥å·¥å
¥åºä½ç½®" |
| | | description="ææç©æå°å¨è·¯çº¿é
ç½®çâå·¥åºææâæ¥éª¤ä¸æäº§åãæå·¥åºç»ä¸ç»´æ¤ã" |
| | | show-icon |
| | | type="info" |
| | | /> |
| | | <Form class="mx-4" /> |
| | | <!-- ç¼è¾æ¨¡å¼ä¸å±ç¤ºäº§å BOM åè¡¨ï¼æ°å¢æ¨¡å¼ä¸éè --> |
| | | <template v-if="formData?.id && formData?.itemId"> |
| | | <Divider class="!my-3" orientation="left">产å BOM é
ç½®</Divider> |
| | | <div class="mx-4"> |
| | | <ProductBomList |
| | | :route-id="formData.routeId!" |
| | | :product-id="formData.itemId" |
| | | :product-name="formData.itemName" |
| | | /> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| | |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesProRouteProductApi } from '#/api/mes/pro/route/product'; |
| | | |
| | | import { ref, watch } from 'vue'; |
| | | import { computed, ref, watch } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | |
| | | routeId: number; |
| | | }>(); |
| | | |
| | | const isEditable = ref(props.formType !== 'detail'); // æ¯å¦å¯ç¼è¾ |
| | | const emit = defineEmits<{ |
| | | change: [list: MesProRouteProductApi.RouteProduct[]]; |
| | | }>(); |
| | | |
| | | const isEditable = computed(() => props.formType !== 'detail'); // æ¯å¦å¯ç¼è¾ |
| | | const list = ref<MesProRouteProductApi.RouteProduct[]>([]); // å·¥èºè·¯çº¿äº§åå表 |
| | | |
| | | const [ProductFormModal, productFormModalApi] = useVbenModal({ |
| | |
| | | try { |
| | | list.value = await getRouteProductListByRoute(props.routeId); |
| | | gridApi.setGridOptions({ data: list.value }); |
| | | emit('change', list.value); |
| | | } finally { |
| | | gridApi.setLoading(false); |
| | | } |
| | |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['å
³è产å']), |
| | | label: $t('ui.actionTitle.create', ['产åºäº§å']), |
| | | type: 'primary', |
| | | onClick: handleCreate, |
| | | }, |
| | | ]" |
| | | /> |
| | | </div> |
| | | <Grid class="w-full" table-title="å
³è产å"> |
| | | <Grid class="w-full" table-title="产åºäº§å忥工å
¥åºä½ç½®"> |
| | | <template #productionTime="{ row }"> |
| | | <span v-if="row.productionTime"> |
| | | {{ row.productionTime }} |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormType } from '../data'; |
| | | import type { MesProRouteApi } from '#/api/mes/pro/route'; |
| | | import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process'; |
| | | import type { MesProRouteProductApi } from '#/api/mes/pro/route/product'; |
| | | import type { MesProRouteProductBomApi } from '#/api/mes/pro/route/productbom'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | import { Alert, Button, Empty, message, Steps, Tag } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { createRoute, getRoute, updateRoute } from '#/api/mes/pro/route'; |
| | | import { getRouteProcessListByRoute } from '#/api/mes/pro/route/process'; |
| | | import { getRouteProductListByRoute } from '#/api/mes/pro/route/product'; |
| | | import { getRouteProductBomList } from '#/api/mes/pro/route/productbom'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | import MaterialInputList from './material-input-list.vue'; |
| | | import ProcessList from './process-list.vue'; |
| | | import ProductList from './product-list.vue'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formType = ref<FormType>('create'); |
| | | const formData = ref<MesProRouteApi.Route>(); |
| | | const activeStep = ref(0); |
| | | const productList = ref<MesProRouteProductApi.RouteProduct[]>([]); |
| | | const processList = ref<MesProRouteProcessApi.RouteProcess[]>([]); |
| | | const bomList = ref<MesProRouteProductBomApi.RouteProductBom[]>([]); |
| | | |
| | | const isDetail = computed(() => formType.value === 'detail'); |
| | | const isNew = computed(() => !formData.value?.id); |
| | | const routeId = computed(() => formData.value?.id); |
| | | const sortedProcessList = computed(() => |
| | | [...processList.value].sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0)), |
| | | ); |
| | | const title = computed(() => |
| | | isDetail.value ? 'æ¥çå·¥èºè·¯çº¿' : isNew.value ? 'æ°å»ºå·¥èºè·¯çº¿' : 'ç¼è¾å·¥èºè·¯çº¿', |
| | | ); |
| | | |
| | | const stepItems = [ |
| | | { title: 'åºæ¬ä¿¡æ¯', description: '路线åç§°ä¸è¯´æ' }, |
| | | { title: '产åºä¸å
¥åº', description: '产å忥工ä»ä½' }, |
| | | { title: 'å·¥åºä¸äº§åº', description: '顺åºååæå/æå' }, |
| | | { title: 'å·¥åºææ', description: 'æäº§åä¸å·¥åºé
ç½®' }, |
| | | { title: 'å¯ç¨åæ£æ¥', description: 'é䏿£æ¥ç¼ºå¤±é¡¹' }, |
| | | ]; |
| | | |
| | | const productsWithWarehouse = computed(() => |
| | | productList.value.filter((item) => item.warehouseId && item.locationId && item.areaId).length, |
| | | ); |
| | | const processesWithOutput = computed(() => |
| | | processList.value.filter((item) => item.outputItemIds?.length).length, |
| | | ); |
| | | const lastProcess = computed(() => sortedProcessList.value.at(-1)); |
| | | const configuredProductIds = computed( |
| | | () => new Set(bomList.value.map((item) => item.productId).filter(Boolean)), |
| | | ); |
| | | const productsWithBom = computed(() => configuredProductIds.value.size); |
| | | const reviewIssues = computed(() => { |
| | | const issues: Array<{ step: number; text: string }> = []; |
| | | if (!productList.value.length) issues.push({ step: 1, text: 'è¿æ²¡æé
置产åºäº§å' }); |
| | | const missingWarehouse = productList.value.filter( |
| | | (item) => !item.warehouseId || !item.locationId || !item.areaId, |
| | | ); |
| | | if (missingWarehouse.length) { |
| | | issues.push({ |
| | | step: 1, |
| | | text: `${missingWarehouse.length} 个产åºäº§åæªé
ç½®å®æ´ä»åºãåºåºååºä½`, |
| | | }); |
| | | } |
| | | if (!processList.value.length) issues.push({ step: 2, text: 'è¿æ²¡æé
ç½®ç产工åº' }); |
| | | if (processList.value.length && !processList.value.some((item) => item.keyFlag)) { |
| | | issues.push({ step: 2, text: 'æªè®¾ç½®å
³é®å·¥åº' }); |
| | | } |
| | | if (lastProcess.value && !lastProcess.value.outputItemIds?.length) { |
| | | issues.push({ |
| | | step: 2, |
| | | text: `æ«éå·¥åºâ${lastProcess.value.processName || lastProcess.value.processCode || lastProcess.value.sort}âå¿
é¡»é
ç½®æå产åº`, |
| | | }); |
| | | } |
| | | const lastOutputs = lastProcess.value?.outputs ?? []; |
| | | if ( |
| | | lastOutputs.length && |
| | | lastOutputs.some( |
| | | (output) => output.productId == null || output.productId !== output.itemId, |
| | | ) |
| | | ) { |
| | | issues.push({ |
| | | step: 2, |
| | | text: 'æ«éå·¥åºçæå产åºå¿
é¡»å½å±å°èªèº«äº§åï¼ä¸è½ä½¿ç¨âéç¨â', |
| | | }); |
| | | } |
| | | const productIds = new Set(productList.value.map((item) => item.itemId)); |
| | | const invalidOutputCount = processList.value.filter((process) => |
| | | process.outputItemIds?.some((itemId) => !productIds.has(itemId)), |
| | | ).length; |
| | | if (invalidOutputCount) { |
| | | issues.push({ |
| | | step: 2, |
| | | text: `${invalidOutputCount} éå·¥åºçäº§åºæªå
å«å¨è·¯çº¿äº§åºäº§åä¸`, |
| | | }); |
| | | } |
| | | const noBomProducts = productList.value.filter( |
| | | (item) => item.itemId && !configuredProductIds.value.has(item.itemId), |
| | | ); |
| | | if (noBomProducts.length) { |
| | | issues.push({ step: 3, text: `${noBomProducts.length} 个产åè¿æ²¡æé
置任ä½å·¥åºææ` }); |
| | | } |
| | | return issues; |
| | | }); |
| | | const primaryText = computed(() => { |
| | | if (activeStep.value === stepItems.length - 1) return isDetail.value ? 'å
³é' : '宿é
ç½®'; |
| | | if (activeStep.value === 0 && isNew.value) return 'ä¿åå¹¶é
置产åº'; |
| | | return 'ä¸ä¸æ¥'; |
| | | }); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | | formItemClass: 'col-span-1', |
| | | labelWidth: 100, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: [], |
| | | showDefaultActions: false, |
| | | wrapperClass: 'grid-cols-2', |
| | | }); |
| | | |
| | | async function loadConfiguration() { |
| | | if (!routeId.value) return; |
| | | const [products, processes, boms] = await Promise.all([ |
| | | getRouteProductListByRoute(routeId.value), |
| | | getRouteProcessListByRoute(routeId.value), |
| | | getRouteProductBomList({ routeId: routeId.value }), |
| | | ]); |
| | | productList.value = products || []; |
| | | processList.value = processes || []; |
| | | bomList.value = boms || []; |
| | | } |
| | | |
| | | async function saveBaseInformation() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) return false; |
| | | modalApi.lock(); |
| | | try { |
| | | const data = (await formApi.getValues()) as MesProRouteApi.Route; |
| | | if (!routeId.value) { |
| | | const id = await createRoute(data); |
| | | formData.value = { ...data, id }; |
| | | await formApi.setFieldValue('id', id); |
| | | formType.value = 'update'; |
| | | message.success('åºæ¬ä¿¡æ¯å·²ä¿åï¼è¯·ç»§ç»é
置产åºäº§ååå
¥åºä½ç½®'); |
| | | } else { |
| | | await updateRoute({ ...data, id: routeId.value }); |
| | | formData.value = { ...formData.value, ...data, id: routeId.value }; |
| | | } |
| | | emit('success'); |
| | | return true; |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | } |
| | | |
| | | function validateProducts() { |
| | | if (!productList.value.length) { |
| | | message.warning('请è³å°æ·»å ä¸ä¸ªäº§åºäº§åï¼åé
置工åº'); |
| | | return false; |
| | | } |
| | | if (productsWithWarehouse.value !== productList.value.length) { |
| | | message.warning('请为æ¯ä¸ªäº§åºäº§åé
ç½®å®æ´çä»åºãåºåºååºä½'); |
| | | return false; |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | function validateProcesses() { |
| | | if (!processList.value.length) { |
| | | message.warning('请è³å°æ·»å ä¸éç产工åº'); |
| | | return false; |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | function validateBom() { |
| | | if (productsWithBom.value !== productList.value.length) { |
| | | message.warning('æ¯ä¸ªäº§åºäº§åè³å°è¦é
ç½®ä¸æ¡å·¥åºææ'); |
| | | return false; |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | async function goToStep(target: number) { |
| | | if (target < 0 || target >= stepItems.length) return; |
| | | if (target <= activeStep.value || isDetail.value) { |
| | | activeStep.value = target; |
| | | return; |
| | | } |
| | | if (!(await saveBaseInformation())) return; |
| | | await loadConfiguration(); |
| | | if (target >= 2 && !validateProducts()) return void (activeStep.value = 1); |
| | | if (target >= 3 && !validateProcesses()) return void (activeStep.value = 2); |
| | | if (target >= 4 && !validateBom()) return void (activeStep.value = 3); |
| | | activeStep.value = target; |
| | | } |
| | | |
| | | async function handlePrimaryAction() { |
| | | if (activeStep.value === stepItems.length - 1) { |
| | | if (!isDetail.value && reviewIssues.value.length) { |
| | | message.warning('ä»æç¼ºå¤±é
ç½®ï¼è¯·æ ¹æ®æ£æ¥ç»æè¡¥å
åå宿'); |
| | | return; |
| | | } |
| | | await modalApi.close(); |
| | | return; |
| | | } |
| | | await goToStep(activeStep.value + 1); |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | showConfirmButton: false, |
| | | cancelText: 'å
³é', |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | formData.value = undefined; |
| | | productList.value = []; |
| | | processList.value = []; |
| | | bomList.value = []; |
| | | activeStep.value = 0; |
| | | return; |
| | | } |
| | | formApi.setState({ schema: useFormSchema(formApi) }); |
| | | const data = modalApi.getData<{ formType: FormType; id?: number }>(); |
| | | formType.value = data.formType; |
| | | formApi.setDisabled(isDetail.value); |
| | | if (!data.id) return; |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getRoute(data.id); |
| | | await formApi.setValues(formData.value); |
| | | await loadConfiguration(); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="title" class="w-[94vw] max-w-[1560px]"> |
| | | <div class="route-config-shell"> |
| | | <header class="route-config-header"> |
| | | <div class="min-w-0"> |
| | | <div class="flex flex-wrap items-center gap-2"> |
| | | <h2 class="m-0 text-lg font-semibold text-gray-900 dark:text-gray-100"> |
| | | {{ formData?.name || 'æ°å·¥èºè·¯çº¿' }} |
| | | </h2> |
| | | <Tag :color="isDetail ? 'default' : 'processing'"> |
| | | {{ isDetail ? 'åªè¯»æ¥ç' : 'é
ç½®ä¸' }} |
| | | </Tag> |
| | | </div> |
| | | <p class="mb-0 mt-1 text-sm text-gray-600 dark:text-gray-300"> |
| | | å·¥åºææä¾èµäº§åºäº§ååå·¥åºï¼å æ¤å¨å·¥åºç¼æå®æåéä¸é
ç½®ã |
| | | </p> |
| | | </div> |
| | | <div class="route-config-summary" aria-label="å½åé
ç½®æ¦è§"> |
| | | <span>äº§åº {{ productList.length }}</span> |
| | | <span>å·¥åº {{ processList.length }}</span> |
| | | <span>ææ {{ bomList.length }}</span> |
| | | </div> |
| | | </header> |
| | | |
| | | <Steps |
| | | :current="activeStep" |
| | | :items="stepItems" |
| | | class="route-config-steps" |
| | | responsive |
| | | size="small" |
| | | type="navigation" |
| | | @change="goToStep" |
| | | /> |
| | | |
| | | <main class="route-config-content"> |
| | | <section v-show="activeStep === 0" class="route-step-panel"> |
| | | <div class="route-step-copy"> |
| | | <IconifyIcon icon="lucide:route" /> |
| | | <div> |
| | | <h3>å
å®ä¹è¿æ¡è·¯çº¿æ¯ä»ä¹</h3> |
| | | <p>ç¼ç ç¨äºç³»ç»è¯å«ï¼åç§°å说æå¸®å©ç°åºäººåå¿«ééæ©ã</p> |
| | | </div> |
| | | </div> |
| | | <Form class="route-base-form" /> |
| | | </section> |
| | | |
| | | <section v-if="routeId" v-show="activeStep === 1" class="route-step-panel"> |
| | | <Alert |
| | | class="mb-4" |
| | | message="å
ç»´æ¤ææå¯è½çå·¥åºäº§åº" |
| | | description="å®é
å½¢æçä¸é´åæååæ«éå·¥åºçæåé½è¦å å
¥ï¼å¹¶å嫿宿¥å·¥åè¿å
¥çä»åºãåºåºååºä½ã" |
| | | show-icon |
| | | type="info" |
| | | /> |
| | | <ProductList |
| | | :form-type="formType" |
| | | :route-id="routeId" |
| | | @change="productList = $event" |
| | | /> |
| | | </section> |
| | | |
| | | <section v-if="routeId" v-show="activeStep === 2" class="route-step-panel"> |
| | | <Alert |
| | | class="mb-4" |
| | | message="æå®é
ç产顺åºç¼æå·¥åº" |
| | | description="ä¸é´å·¥åºæ²¡æç¬ç«äº§åºæ¶å¯ç空ï¼å½¢æåæåæ¶éæ©å¯¹åºåæåï¼æåä¸éå·¥åºå¿
é¡»éæ©è·¯çº¿ç»å®çå
¨é¨æåãå¤äº§åå
±ç¨çåæåå½å±éâéç¨âï¼æåå¿
é¡»å½å±å°èªèº«äº§åã" |
| | | show-icon |
| | | type="info" |
| | | /> |
| | | <ProcessList |
| | | :form-type="formType" |
| | | :route-id="routeId" |
| | | @change="processList = $event" |
| | | /> |
| | | </section> |
| | | |
| | | <section v-if="routeId" v-show="activeStep === 3" class="route-step-panel"> |
| | | <MaterialInputList |
| | | :form-type="formType" |
| | | :processes="sortedProcessList" |
| | | :products="productList" |
| | | :route-id="routeId" |
| | | @change="bomList = $event" |
| | | /> |
| | | </section> |
| | | |
| | | <section v-show="activeStep === 4" class="route-step-panel"> |
| | | <div class="review-heading"> |
| | | <div> |
| | | <h3>å¯ç¨åæ£æ¥</h3> |
| | | <p>确认产åºãä»ä½ãå·¥åºåææå½¢æéç¯åï¼ååå°å表å¯ç¨è·¯çº¿ã</p> |
| | | </div> |
| | | <Tag :color="reviewIssues.length ? 'warning' : 'success'"> |
| | | {{ reviewIssues.length ? `${reviewIssues.length} 项å¾
å¤ç` : 'é
ç½®å®æ´' }} |
| | | </Tag> |
| | | </div> |
| | | |
| | | <div class="review-metrics"> |
| | | <div><strong>{{ productsWithWarehouse }}/{{ productList.length }}</strong><span>产åä»ä½å®æ´</span></div> |
| | | <div><strong>{{ processesWithOutput }}</strong><span>é
置产åºçå·¥åº</span></div> |
| | | <div><strong>{{ productsWithBom }}/{{ productList.length }}</strong><span>产åå·²é
ç½®ææ</span></div> |
| | | </div> |
| | | |
| | | <div v-if="reviewIssues.length" class="issue-list" role="status"> |
| | | <button |
| | | v-for="issue in reviewIssues" |
| | | :key="issue.text" |
| | | class="issue-item" |
| | | type="button" |
| | | @click="activeStep = issue.step" |
| | | > |
| | | <IconifyIcon icon="lucide:circle-alert" /> |
| | | <span>{{ issue.text }}</span> |
| | | <IconifyIcon class="ml-auto" icon="lucide:chevron-right" /> |
| | | </button> |
| | | </div> |
| | | <Empty v-else description="ææå¿
éé
ç½®å已宿"> |
| | | <template #image> |
| | | <div class="review-success-icon"><IconifyIcon icon="lucide:badge-check" /></div> |
| | | </template> |
| | | </Empty> |
| | | </section> |
| | | </main> |
| | | </div> |
| | | |
| | | <template #prepend-footer> |
| | | <div class="flex flex-auto items-center justify-between gap-3"> |
| | | <Button v-if="activeStep > 0" @click="activeStep--"> |
| | | <IconifyIcon class="mr-1" icon="lucide:arrow-left" />ä¸ä¸æ¥ |
| | | </Button> |
| | | <span v-else class="text-sm text-gray-500">æ°å»ºè·¯çº¿é»è®¤åç¨ï¼é
ç½®å®æ´ååå¯ç¨</span> |
| | | <Button type="primary" @click="handlePrimaryAction"> |
| | | {{ primaryText }} |
| | | <IconifyIcon |
| | | v-if="activeStep < stepItems.length - 1" |
| | | class="ml-1" |
| | | icon="lucide:arrow-right" |
| | | /> |
| | | </Button> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .route-config-shell { min-height: min(720px, 72vh); background: rgb(248 250 252); color: rgb(15 23 42); } |
| | | .route-config-header { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 18px 24px 14px; background: white; border-bottom: 1px solid rgb(226 232 240); } |
| | | .route-config-summary { display: flex; flex-shrink: 0; gap: 8px; } |
| | | .route-config-summary span { padding: 5px 10px; border-radius: 8px; background: rgb(239 246 255); color: rgb(29 78 216); font-size: 12px; font-weight: 600; } |
| | | .route-config-steps { padding: 12px 20px 0; background: white; } |
| | | .route-config-content { padding: 20px 24px 28px; } |
| | | .route-step-panel { min-height: 460px; padding: 22px; border-radius: 14px; background: white; box-shadow: 0 10px 28px rgb(15 23 42 / 6%); } |
| | | .route-step-copy, .review-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; margin-bottom: 22px; } |
| | | .route-step-copy { justify-content: flex-start; } |
| | | .route-step-copy > svg { width: 38px; height: 38px; padding: 9px; border-radius: 10px; background: rgb(219 234 254); color: rgb(29 78 216); } |
| | | .route-step-copy h3, .review-heading h3 { margin: 0; color: rgb(15 23 42); font-size: 17px; font-weight: 650; } |
| | | .route-step-copy p, .review-heading p { margin: 4px 0 0; color: rgb(71 85 105); font-size: 14px; } |
| | | .route-base-form { max-width: 1040px; margin: 34px auto 0; } |
| | | .review-metrics { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; margin: 24px 0; } |
| | | .review-metrics > div { display: flex; align-items: baseline; gap: 10px; padding: 18px; border-radius: 12px; background: rgb(248 250 252); } |
| | | .review-metrics strong { color: rgb(29 78 216); font-size: 24px; } |
| | | .review-metrics span { color: rgb(71 85 105); font-size: 13px; } |
| | | .issue-list { display: grid; gap: 10px; } |
| | | .issue-item { display: flex; width: 100%; min-height: 48px; align-items: center; gap: 10px; padding: 10px 14px; border: 1px solid rgb(253 230 138); border-radius: 10px; background: rgb(255 251 235); color: rgb(146 64 14); cursor: pointer; text-align: left; transition: background-color 180ms ease, border-color 180ms ease; } |
| | | .issue-item:hover, .issue-item:focus-visible { border-color: rgb(245 158 11); background: rgb(254 243 199); outline: none; } |
| | | .review-success-icon { display: grid; width: 72px; height: 72px; place-items: center; margin: 0 auto; border-radius: 50%; background: rgb(220 252 231); color: rgb(22 163 74); font-size: 38px; } |
| | | :global(.dark) .route-config-shell { background: rgb(15 23 42); color: rgb(226 232 240); } |
| | | :global(.dark) .route-config-header, :global(.dark) .route-config-steps, :global(.dark) .route-step-panel { background: rgb(30 41 59); border-color: rgb(51 65 85); } |
| | | :global(.dark) .route-step-copy h3, :global(.dark) .review-heading h3 { color: rgb(241 245 249); } |
| | | :global(.dark) .route-step-copy p, :global(.dark) .review-heading p, :global(.dark) .review-metrics span { color: rgb(203 213 225); } |
| | | :global(.dark) .review-metrics > div { background: rgb(15 23 42); } |
| | | @media (max-width: 900px) { |
| | | .route-config-header { align-items: flex-start; flex-direction: column; } |
| | | .review-metrics { grid-template-columns: 1fr; } |
| | | .route-step-panel { padding: 16px; } |
| | | } |
| | | </style> |
| | |
| | | width: 160, |
| | | }, |
| | | { |
| | | field: 'warehouseName', |
| | | title: 'æ¥å·¥ä»åº', |
| | | width: 130, |
| | | formatter: ({ cellValue, row }) => { |
| | | if (!cellValue) return '-'; |
| | | return row.areaName |
| | | ? `${cellValue}/${row.locationName ?? ''}/${row.areaName}` |
| | | : cellValue; |
| | | }, |
| | | }, |
| | | { |
| | | field: 'remark', |
| | | title: '夿³¨', |
| | | minWidth: 120, |
| | |
| | | import { |
| | | MesProWorkOrderStatusEnum, |
| | | } from "@vben/constants"; |
| | | import { isAiEnable } from "@vben/hooks"; |
| | | |
| | | import { Button, message, Tabs } from "ant-design-vue"; |
| | | |
| | |
| | | ); |
| | | const canUseAi = computed( |
| | | () => |
| | | isAiEnable() && |
| | | formData.value?.id && |
| | | (formData.value?.status === MesProWorkOrderStatusEnum.PREPARE || |
| | | formData.value?.status === MesProWorkOrderStatusEnum.CONFIRMED) |
| | |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { getProcessSimpleList } from '#/api/mes/pro/process'; |
| | | import { getRouteProcessListByProduct } from '#/api/mes/pro/route/process'; |
| | | import { |
| | | createWorkOrderProcess, |
| | | getWorkOrderProcess, |
| | | updateWorkOrderProcess, |
| | | } from '#/api/mes/pro/workorder/process'; |
| | | import { getWarehouseSimpleList } from '#/api/mes/wm/warehouse'; |
| | | import { getWarehouseAreaSimpleList } from '#/api/mes/wm/warehouse/area'; |
| | | import { getWarehouseLocationSimpleList } from '#/api/mes/wm/warehouse/location'; |
| | | import { $t } from '#/locales'; |
| | | import { MdItemSelect } from '#/views/mes/md/item/components'; |
| | | |
| | |
| | | const workOrderId = ref<number>(); |
| | | const processList = ref<MesProWorkOrderProcessApi.WorkOrderProcess[]>([]); |
| | | const isCreateMode = ref(false); |
| | | const allowedOutputItemIds = ref<number[]>([]); |
| | | |
| | | const getTitle = computed(() => { |
| | | return formData.value?.id |
| | |
| | | })); |
| | | } |
| | | |
| | | /** |
| | | * è½½å
¥æ¬å·¥å产åå¯éç产åºç©æèå´ |
| | | * |
| | | * åå·¥èºè·¯çº¿æäº§åºå½å±è¿æ»¤åç产åºï¼éç¨åæå + æ¬äº§åä¸å±æåï¼ï¼ |
| | | * å¹¶å¹¶å
¥è¯¥å·¥åºå·²é
ç½®ç产åºï¼é¿å
å岿°æ®å¨éæ°éæ©æ¶è¢«è¯¯æ¸
空ã |
| | | */ |
| | | async function loadAllowedOutputItemIds( |
| | | productId?: number, |
| | | currentOutputItemIds: number[] = [], |
| | | ): Promise<number[]> { |
| | | if (!productId) { |
| | | return []; |
| | | } |
| | | const ids = new Set<number>([productId, ...currentOutputItemIds]); |
| | | const routeProcessList = await getRouteProcessListByProduct(productId); |
| | | (routeProcessList || []).forEach((process) => { |
| | | (process.outputItemIds || []).forEach((id) => { |
| | | if (id) { |
| | | ids.add(id); |
| | | } |
| | | }); |
| | | }); |
| | | return [...ids]; |
| | | } |
| | | |
| | | /** 表å schema */ |
| | | async function loadSchema(): Promise<VbenFormSchema[]> { |
| | | const processOptions = await loadProcessOptions(); |
| | | const [processOptions, warehouseList] = await Promise.all([ |
| | | loadProcessOptions(), |
| | | getWarehouseSimpleList(), |
| | | ]); |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | |
| | | }, |
| | | { |
| | | fieldName: 'workOrderId', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | | { |
| | | fieldName: 'warehouseName', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | | { |
| | | fieldName: 'locationName', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | | { |
| | | fieldName: 'areaName', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | |
| | | componentProps: { |
| | | multiple: true, |
| | | placeholder: 'è¯·éæ©äº§åºç©æ', |
| | | productIds: allowedOutputItemIds.value, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'warehouseId', |
| | | label: 'æ¥å·¥ä»åº', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: (warehouseList || []).map((item) => ({ |
| | | label: item.name ?? '', |
| | | value: item.id!, |
| | | })), |
| | | placeholder: 'é»è®¤åå·¥èºè·¯çº¿é
ç½®ï¼å¯ä¿®æ¹', |
| | | onChange: async (value: number) => { |
| | | const warehouse = (warehouseList || []).find( |
| | | (item) => item.id === value, |
| | | ); |
| | | await formApi.setFieldValue('warehouseName', warehouse?.name); |
| | | await formApi.setFieldValue('locationId', undefined); |
| | | await formApi.setFieldValue('locationName', undefined); |
| | | await formApi.setFieldValue('areaId', undefined); |
| | | await formApi.setFieldValue('areaName', undefined); |
| | | }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'locationId', |
| | | label: 'æ¥å·¥åºåº', |
| | | component: 'Select', |
| | | dependencies: { |
| | | triggerFields: ['warehouseId'], |
| | | disabled: (values) => !values.warehouseId, |
| | | async componentProps(values) { |
| | | const options = values.warehouseId |
| | | ? await getWarehouseLocationSimpleList(values.warehouseId) |
| | | : []; |
| | | return { |
| | | allowClear: true, |
| | | fieldNames: { label: 'name', value: 'id' }, |
| | | options, |
| | | placeholder: 'è¯·éæ©æ¥å·¥åºåº', |
| | | onChange: async (value: number) => { |
| | | const location = options.find((item) => item.id === value); |
| | | await formApi.setFieldValue('locationName', location?.name); |
| | | await formApi.setFieldValue('areaId', undefined); |
| | | await formApi.setFieldValue('areaName', undefined); |
| | | }, |
| | | }; |
| | | }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'areaId', |
| | | label: 'æ¥å·¥åºä½', |
| | | component: 'Select', |
| | | dependencies: { |
| | | triggerFields: ['locationId'], |
| | | disabled: (values) => !values.locationId, |
| | | async componentProps(values) { |
| | | const options = values.locationId |
| | | ? await getWarehouseAreaSimpleList(values.locationId) |
| | | : []; |
| | | return { |
| | | allowClear: true, |
| | | fieldNames: { label: 'name', value: 'id' }, |
| | | options, |
| | | placeholder: 'è¯·éæ©æ¥å·¥åºä½', |
| | | onChange: async (value: number) => { |
| | | const area = options.find((item) => item.id === value); |
| | | await formApi.setFieldValue('areaName', area?.name); |
| | | }, |
| | | }; |
| | | }, |
| | | }, |
| | | }, |
| | | { |
| | |
| | | formData.value = undefined; |
| | | processList.value = []; |
| | | isCreateMode.value = false; |
| | | allowedOutputItemIds.value = []; |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | const schema = await loadSchema(); |
| | | formApi.setState({ schema }); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | |
| | | const data = modalApi.getData<{ |
| | | id?: number; |
| | | productId?: number; |
| | |
| | | processList.value = data.processList || []; |
| | | isCreateMode.value = !data.workOrderId; |
| | | |
| | | if (data.row) { |
| | | // ç¼è¾ä¸´æ¶å表ä¸çå·¥åº |
| | | formData.value = data.row; |
| | | await formApi.setValues(data.row); |
| | | } else if (data.id) { |
| | | // ç¼è¾å·²ä¿åçå·¥åº |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getWorkOrderProcess(data.id); |
| | | modalApi.lock(); |
| | | try { |
| | | // å
åå·¥åºæ°æ®ï¼ç¼è¾å·²ä¿åå·¥åºæ¶ç¨äºåæ¾ï¼ä¹ç¨äºä¿çå
¶å·²æäº§åº |
| | | const savedProcess = !data.row && data.id |
| | | ? await getWorkOrderProcess(data.id) |
| | | : undefined; |
| | | formData.value = data.row ?? savedProcess; |
| | | |
| | | allowedOutputItemIds.value = await loadAllowedOutputItemIds( |
| | | data.productId, |
| | | formData.value?.outputItemIds || [], |
| | | ); |
| | | formApi.setState({ schema: await loadSchema() }); |
| | | |
| | | if (formData.value) { |
| | | // ç¼è¾ä¸´æ¶å表ä¸çå·¥åº / ç¼è¾å·²ä¿åçå·¥åº |
| | | await formApi.setValues(formData.value); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } else { |
| | | // æ°å¢ |
| | | const maxSort = Math.max(0, ...processList.value.map((item) => item.sort || 0)); |
| | | await formApi.setValues({ |
| | | colorCode: '#00AEF3', |
| | | sort: maxSort + 1, |
| | | }); |
| | | } |
| | | } else { |
| | | // æ°å¢ |
| | | formData.value = undefined; |
| | | const maxSort = Math.max(0, ...processList.value.map((item) => item.sort || 0)); |
| | | await formApi.setValues({ |
| | | colorCode: '#00AEF3', |
| | | sort: maxSort + 1, |
| | | }); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | |
| | | backflushFlag: item.backflushFlag, |
| | | outputItemIds: item.outputItemIds, |
| | | outputItemNames: item.outputItemNames, |
| | | warehouseId: item.warehouseId, |
| | | warehouseName: item.warehouseName, |
| | | locationId: item.locationId, |
| | | locationName: item.locationName, |
| | | areaId: item.areaId, |
| | | areaName: item.areaName, |
| | | remark: item.remark, |
| | | bomItems: item.bomItems, |
| | | })); |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: "productKeyword", |
| | | label: "产å", |
| | | component: "Input", |
| | | componentProps: { |
| | | allowClear: true, |
| | | maxLength: 100, |
| | | placeholder: "è¾å
¥äº§ååç§°æç¼ç ", |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: "status", |
| | | label: "ç¶æ", |
| | | component: "Select", |
| | |
| | | field: "createTime", |
| | | title: "å建æ¶é´", |
| | | width: 180, |
| | | formatter: 'formatDate', |
| | | formatter: "formatDateTime", |
| | | }, |
| | | { |
| | | title: "æä½", |
| | |
| | | fieldName: "yieldRate", |
| | | label: "å·¥åºæ¶ç", |
| | | component: "InputNumber", |
| | | help: "产åºé ÷ æå
¥éãå¡«å 0.98 表示 98%ï¼1 è¡¨ç¤ºæ æèï¼æªå¡«åæ 100% åä¸æäº§è®¡ç®ã", |
| | | help: "产åºé ÷ æå
¥éãå¡«å 0.98 表示 98%ï¼æªå¡«åæ 100% åä¸æäº§è®¡ç®ã", |
| | | componentProps: { |
| | | class: "!w-full", |
| | | min: 0.0001, |
| | |
| | | fieldName: "linkType", |
| | | label: "ä¸ä¸éå·¥åºå
³ç³»", |
| | | component: "Select", |
| | | help: () => |
| | | h( |
| | | "div", |
| | | { |
| | | style: |
| | | "white-space: pre-line; max-width: 320px; line-height: 1.6;", |
| | | }, |
| | | "0=å¼å§-å¼å§(SS)ï¼æ¬å·¥åºå¼å§åï¼ä¸éå·¥åºæè½å¼å§ï¼å¯éå å¹¶è¡å¼å·¥ï¼\n1=ç»æ-ç»æ(FF)ï¼ä¸éå·¥åºå¿
é¡»çå°æ¬å·¥åºç»ææè½ç»æï¼å¹¶è¡ä½åæ¥å®å·¥ï¼\n2=å¼å§-ç»æ(SF)ï¼æ¬å·¥åºå¼å§åï¼ä¸éå·¥åºæè½ç»æ\n3=ç»æ-å¼å§(FS)ï¼æ¬å·¥åºç»æåï¼ä¸éå·¥åºæè½å¼å§ï¼é»è®¤å¼ï¼", |
| | | ), |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.MES_PRO_LINK_TYPE, "number"), |
| | | placeholder: "è¯·éæ©", |
| | | }, |
| | | rules: z.number().optional(), |
| | | rules: z.number().default(3), |
| | | }, |
| | | { |
| | | fieldName: "colorCode", |
| | |
| | | ]; |
| | | } |
| | | |
| | | /** å·¥èºè·¯çº¿äº§å表åï¼multiple = trueï¼æ°å¢æ¨¡å¼ï¼æ¶äº§åå¯å¤éï¼ä¸æ¬¡æ§ç»å®å¤ä¸ª */ |
| | | export function useRouteProductFormSchema( |
| | | onItemChange?: (item: any) => void, |
| | | multiple = false, |
| | | ): VbenFormSchema[] { |
| | | /** å·¥èºè·¯çº¿äº§å表å */ |
| | | export function useRouteProductFormSchema(onItemChange?: (item: any) => void): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: "id", |
| | |
| | | component: "Input", |
| | | dependencies: { triggerFields: [""], show: () => false }, |
| | | }, |
| | | // æ°å¢ï¼å¤éï¼ï¼ä¸æ¬¡ç»å®å¤ä¸ªäº§åï¼ç¼è¾ï¼åéï¼ï¼change åå¡«ç¼ç /åç§°/è§æ ¼/åä½ |
| | | multiple |
| | | ? { |
| | | fieldName: "itemIds", |
| | | label: "产åï¼å¯å¤éï¼", |
| | | component: markRaw(MdItemSelect), |
| | | componentProps: { |
| | | multiple: true, |
| | | placeholder: "è¯·éæ©è¦ç»å®ç产åï¼å¯å¤éï¼", |
| | | }, |
| | | formItemClass: "col-span-2", |
| | | rules: "selectRequired", |
| | | } |
| | | : { |
| | | fieldName: "itemId", |
| | | label: "产å", |
| | | component: markRaw(MdItemSelect), |
| | | componentProps: { |
| | | onChange: onItemChange, |
| | | }, |
| | | formItemClass: "col-span-2", |
| | | rules: "selectRequired", |
| | | }, |
| | | // 产åç©æï¼ä½¿ç¨ä¸å¡èªå®ä¹éæ©å¨ï¼change åå¡«ç¼ç /åç§°/è§æ ¼/åä½ |
| | | { |
| | | fieldName: "itemId", |
| | | label: "产å", |
| | | component: markRaw(MdItemSelect), |
| | | componentProps: { |
| | | onChange: onItemChange, |
| | | }, |
| | | formItemClass: "col-span-2", |
| | | rules: "selectRequired", |
| | | }, |
| | | { |
| | | fieldName: "quantity", |
| | | label: "ç产æ°é", |
| | |
| | | import { getDictLabel } from '@vben/hooks'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button, message, Tooltip } from 'ant-design-vue'; |
| | | import { Alert, Button, message, Tooltip } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteRoute, |
| | | exportRoute, |
| | | getRoutePage, |
| | | importRoute, |
| | | importRouteTemplate, |
| | | importCompleteRoute, |
| | | importCompleteRouteTemplate, |
| | | updateRouteStatus, |
| | | } from '#/api/mes/pro/route'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import ImportModalComponent from '#/components/import-modal'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | // å·¥èºè®¾è®¡ä¸ç产åºç¡æ°æ®å
±ç¨åä¸ä¸ªé
ç½®å¨ï¼é¿å
两å¥é¡µé¢è½åä¸ä¸è´ã |
| | | import Form from '#/views/mes/pro/route/modules/route-configurator.vue'; |
| | | |
| | | const { hasAccessByCodes } = useAccess(); |
| | | const statusEditable = hasAccessByCodes(['mes:pro-route:update']); // æ¯å¦å¯åæ¢ç¶æ |
| | |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ImportModal, importModalApi] = useVbenModal({ |
| | | connectedComponent: ImportModalComponent, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** 导å
¥å·¥èºè·¯çº¿ */ |
| | | function handleImport() { |
| | | importModalApi |
| | | .setData({ |
| | | title: $t('ui.actionTitle.import', ['å·¥èºè·¯çº¿']), |
| | | fileName: 'å·¥èºè·¯çº¿å¯¼å
¥æ¨¡æ¿.xls', |
| | | importFn: importRoute, |
| | | templateFn: importRouteTemplate, |
| | | }) |
| | | .open(); |
| | | } |
| | | |
| | | function handleCompleteImport() { |
| | | importModalApi |
| | | .setData({ |
| | | title: 'æ¹é导å
¥å®æ´å·¥èºè·¯çº¿', |
| | | fileName: '宿´å·¥èºè·¯çº¿å¯¼å
¥æ¨¡æ¿.xlsx', |
| | | importFn: importCompleteRoute, |
| | | templateFn: importCompleteRouteTemplate, |
| | | showUpdateSupport: false, |
| | | }) |
| | | .open(); |
| | | } |
| | | |
| | | /** å建工èºè·¯çº¿ */ |
| | |
| | | ): Promise<boolean | undefined> { |
| | | try { |
| | | await confirm( |
| | | `确认è¦å°"${row.name}"å·¥èºè·¯çº¿åæ¢ä¸ºã${getDictLabel(DICT_TYPE.COMMON_STATUS, newStatus)}ãåï¼`, |
| | | newStatus === CommonStatusEnum.ENABLE |
| | | ? `确认å¯ç¨â${row.name}âåï¼ç³»ç»å°æ£æ¥äº§åºå
¥åºä½ç½®ãå·¥åºäº§åºåå·¥åºæææ¯å¦å®æ´ã` |
| | | : `确认è¦å°â${row.name}âå·¥èºè·¯çº¿åæ¢ä¸ºã${getDictLabel(DICT_TYPE.COMMON_STATUS, newStatus)}ãåï¼`, |
| | | ); |
| | | } catch { |
| | | return false; |
| | |
| | | |
| | | <template> |
| | | <Page auto-content-height><FormModal @success="handleRefresh" /> |
| | | <ImportModal @success="handleRefresh" /> |
| | | <Alert |
| | | class="mb-3" |
| | | message="路线é
置顺åº" |
| | | description="åºæ¬ä¿¡æ¯ â 产åºäº§ååå
¥åºä» â å·¥åºååå·¥åºäº§åº â å·¥åºææ â æ ¡éªå¯ç¨" |
| | | show-icon |
| | | type="info" |
| | | /> |
| | | <Grid table-title="å·¥èºè·¯çº¿å表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | |
| | | auth: ['mes:pro-route:export'], |
| | | onClick: handleExport, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.import', ['å·¥èºè·¯çº¿']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.UPLOAD, |
| | | auth: ['mes:pro-route:import'], |
| | | onClick: handleImport, |
| | | }, |
| | | { |
| | | label: 'æ¹é导å
¥å®æ´è·¯çº¿', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.UPLOAD, |
| | | auth: ['mes:pro-route:import'], |
| | | onClick: handleCompleteImport, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | |
| | | import type { FormType } from '../data'; |
| | | |
| | | import type { MesProRouteApi } from '#/api/mes/pro/route'; |
| | | import type { MesProRouteProductApi } from '#/api/mes/pro/route/product'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { confirm, useVbenModal } from '@vben/common-ui'; |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { Button, Divider, message, Select } from 'ant-design-vue'; |
| | | import { message, TabPane, Tabs } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { createRoute, getRoute, updateRoute } from '#/api/mes/pro/route'; |
| | | import { |
| | | deleteRouteProduct, |
| | | getRouteProductListByRoute, |
| | | } from '#/api/mes/pro/route/product'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | import ProductForm from './product-form.vue'; |
| | | import Workbench from './workbench.vue'; |
| | | import ProcessList from './process-list.vue'; |
| | | import ProductList from './product-list.vue'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formType = ref<FormType>('create'); // è¡¨åæ¨¡å¼ |
| | | const subTab = ref('process'); // å½åæ¿æ´»çå表 Tab |
| | | const formData = ref<MesProRouteApi.Route>(); |
| | | |
| | | const isDetail = computed(() => formType.value === 'detail'); // æ¯å¦æ¥çæ¨¡å¼ |
| | |
| | | : $t('ui.actionTitle.create', ['å·¥èºè·¯çº¿']); |
| | | }); |
| | | |
| | | // ==================== 产åç»å® ==================== |
| | | const products = ref<MesProRouteProductApi.RouteProduct[]>([]); // å
³è产åå表 |
| | | const selectedProductItemId = ref<number>(); // å½åéä¸ç产åç©æç¼å· |
| | | const selectedProduct = computed(() => |
| | | products.value.find((item) => item.itemId === selectedProductItemId.value), |
| | | ); |
| | | const productOptions = computed(() => |
| | | products.value.map((item) => ({ label: item.itemName, value: item.itemId })), |
| | | ); |
| | | |
| | | async function loadProducts() { |
| | | const routeId = formData.value?.id; |
| | | if (!routeId) { |
| | | products.value = []; |
| | | return; |
| | | } |
| | | products.value = (await getRouteProductListByRoute(routeId)) || []; |
| | | if ( |
| | | !products.value.some((item) => item.itemId === selectedProductItemId.value) |
| | | ) { |
| | | selectedProductItemId.value = products.value[0]?.itemId; |
| | | } |
| | | } |
| | | |
| | | const [ProductFormModal, productFormModalApi] = useVbenModal({ |
| | | connectedComponent: ProductForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** æå¼äº§åå¼¹çªï¼ç»å® / ç¼è¾ï¼ */ |
| | | function openProductForm(row?: MesProRouteProductApi.RouteProduct) { |
| | | if (!formData.value?.id) { |
| | | return; |
| | | } |
| | | productFormModalApi |
| | | .setData( |
| | | row |
| | | ? { id: row.id, routeId: formData.value.id } |
| | | : { |
| | | routeId: formData.value.id, |
| | | boundItemIds: products.value |
| | | .map((item) => item.itemId) |
| | | .filter((id): id is number => id != null), |
| | | }, |
| | | ) |
| | | .open(); |
| | | } |
| | | |
| | | /** ç§»é¤äº§åï¼ä¼çº§èå é¤è¯¥äº§åçæå
¥ç©æé
ç½®ï¼ */ |
| | | async function handleRemoveProduct() { |
| | | if (!selectedProduct.value?.id) { |
| | | return; |
| | | } |
| | | try { |
| | | await confirm( |
| | | `确认å°äº§åã${selectedProduct.value.itemName}ãä»è¯¥å·¥èºè·¯çº¿ç§»é¤ï¼å
¶å
¨é¨æå
¥ç©æé
ç½®å°ä¸å¹¶å é¤ã`, |
| | | ); |
| | | } catch { |
| | | return; |
| | | } |
| | | await deleteRouteProduct(selectedProduct.value.id); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | await loadProducts(); |
| | | } |
| | | |
| | | // ==================== åºæ¬ä¿¡æ¯è¡¨å ==================== |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | |
| | | const data = (await formApi.getValues()) as MesProRouteApi.Route; |
| | | try { |
| | | if (formType.value === 'create') { |
| | | // æ°å¢æåååå°ç¼è¾æ¨¡å¼ï¼è¿å
¥ã产åä¸å·¥åºé
ç½®ãå·¥ä½å° |
| | | // æ°å¢æåååå°ç¼è¾æ¨¡å¼ï¼æ¹ä¾¿ç»§ç»ç»´æ¤ç»æå·¥åºãå
³è产å |
| | | const id = await createRoute(data); |
| | | formData.value = { ...data, id }; |
| | | await formApi.setFieldValue('id', id); |
| | | formType.value = 'update'; |
| | | await loadProducts(); |
| | | } else { |
| | | await updateRoute(data); |
| | | formData.value = { ...formData.value, ...data }; |
| | |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | formData.value = undefined; |
| | | products.value = []; |
| | | selectedProductItemId.value = undefined; |
| | | return; |
| | | } |
| | | formApi.setState({ schema: useFormSchema(formApi) }); |
| | | subTab.value = 'process'; |
| | | // å è½½æ°æ® |
| | | const data = modalApi.getData<{ formType: FormType; id?: number }>(); |
| | | formType.value = data.formType; |
| | |
| | | formData.value = await getRoute(data.id); |
| | | // è®¾ç½®å° values |
| | | await formApi.setValues(formData.value); |
| | | await loadProducts(); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-11/12"> |
| | | <Modal :title="getTitle" class="w-4/5"> |
| | | <Form class="mx-4" /> |
| | | <!-- ç¼è¾/详æ
模å¼ä¸å±ç¤ºã产åä¸å·¥åºé
ç½®ãå·¥ä½å°ï¼æ°å¢æ¨¡å¼ä¸éè --> |
| | | <template v-if="formData?.id"> |
| | | <div class="mx-4 mt-4"> |
| | | <Divider class="!my-2" orientation="left"> |
| | | <span class="text-sm">产åä¸å·¥åºé
ç½®</span> |
| | | </Divider> |
| | | <div class="flex flex-wrap items-center gap-3"> |
| | | <span class="shrink-0 text-sm font-medium">ç产产å</span> |
| | | <Select |
| | | v-model:value="selectedProductItemId" |
| | | class="!w-64" |
| | | allow-clear |
| | | :disabled="isDetail" |
| | | placeholder="è¯·éæ©æç»å®äº§å" |
| | | :options="productOptions" |
| | | /> |
| | | <template v-if="!isDetail"> |
| | | <Button type="primary" @click="openProductForm()"> ç»å®äº§å </Button> |
| | | <Button |
| | | :disabled="!selectedProduct" |
| | | @click="openProductForm(selectedProduct)" |
| | | > |
| | | ç¼è¾äº§å |
| | | </Button> |
| | | <Button danger :disabled="!selectedProduct" @click="handleRemoveProduct"> |
| | | ç§»é¤äº§å |
| | | </Button> |
| | | </template> |
| | | </div> |
| | | <p v-if="products.length === 0" class="mt-2 text-xs text-gray-400"> |
| | | å
ç»å®è¦ç产çç©æï¼å为æ¯éå·¥åºé
ç½®ãæå
¥ + 产åºã |
| | | </p> |
| | | <div class="mt-3"> |
| | | <Workbench |
| | | :form-type="formType" |
| | | :product="selectedProduct" |
| | | :route-id="formData.id" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <!-- ç¼è¾/详æ
模å¼ä¸å±ç¤ºå表 Tabï¼æ°å¢æ¨¡å¼ä¸éè --> |
| | | <Tabs |
| | | v-if="formType !== 'create' && formData?.id" |
| | | v-model:active-key="subTab" |
| | | class="mx-4 mt-4" |
| | | > |
| | | <TabPane key="process" tab="ç»æå·¥åº"> |
| | | <ProcessList :form-type="formType" :route-id="formData.id" /> |
| | | </TabPane> |
| | | <TabPane key="product" tab="å
³è产å"> |
| | | <ProductList :form-type="formType" :route-id="formData.id" /> |
| | | </TabPane> |
| | | </Tabs> |
| | | </Modal> |
| | | <ProductFormModal @success="loadProducts" /> |
| | | </template> |
| | | </template> |
| | |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | | formItemClass: 'col-span-1', |
| | | labelWidth: 150, |
| | | labelWidth: 130, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: [], |
| | |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | import { Divider, message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useRouteProductFormSchema } from '../data'; |
| | | import ProductBomList from './product-bom-list.vue'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formData = ref<MesProRouteProductApi.RouteProduct>(); |
| | | const boundItemIds = ref<number[]>([]); // å·²ç»å®æ¬è·¯çº¿ç产åç©æç¼å·ï¼æ°å¢å¤éæ¶è·³è¿ï¼ |
| | | const getTitle = computed(() => |
| | | formData.value?.id |
| | | ? $t('ui.actionTitle.edit', ['å·¥èºè·¯çº¿äº§å']) |
| | | : $t('ui.actionTitle.create', ['å·¥èºè·¯çº¿äº§å']), |
| | | ); |
| | | |
| | | /** MdItemSelect åé change åè°ï¼æç©æç¼ç /åç§°/è§æ ¼/åä½åå¡«å° form */ |
| | | /** MdItemSelect change åè°ï¼æç©æç¼ç /åç§°/è§æ ¼/åä½åå¡«å° form */ |
| | | async function handleItemChange(item?: any) { |
| | | if (!item) { |
| | | return; |
| | |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | const data = (await formApi.getValues()) as MesProRouteProductApi.RouteProduct & { |
| | | itemIds?: number[]; |
| | | }; |
| | | // æäº¤è¡¨å |
| | | const data = |
| | | (await formApi.getValues()) as MesProRouteProductApi.RouteProduct; |
| | | try { |
| | | // ç¼è¾æ¨¡å¼ï¼æ´æ°å个产å |
| | | if (formData.value?.id) { |
| | | await updateRouteProduct(data); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | return; |
| | | // ç¨ææ°è¡¨åå¼åæ¥ formDataï¼ç¡®ä¿äº§å BOM å表ç»å®ç itemId ä¸è¡¨åä¸è´ |
| | | formData.value = { ...formData.value, ...data }; |
| | | } else { |
| | | // æ°å¢æåå忢å°ç¼è¾æ¨¡å¼ï¼æ¹ä¾¿ç»§ç»ç»´æ¤äº§å BOM |
| | | const id = await createRouteProduct(data); |
| | | formData.value = { ...data, id }; |
| | | await formApi.setFieldValue('id', id); |
| | | } |
| | | // æ°å¢æ¨¡å¼ï¼å¤é产å䏿¬¡æ§ç»å®ï¼è·³è¿å·²å¨è·¯çº¿ä¸ç产å |
| | | const itemIds = Array.isArray(data.itemIds) ? data.itemIds : []; |
| | | if (itemIds.length === 0) { |
| | | message.warning('请è³å°éæ©ä¸ä¸ªäº§å'); |
| | | return; |
| | | } |
| | | let created = 0; |
| | | let skipped = 0; |
| | | const failedIds: number[] = []; |
| | | for (const itemId of itemIds) { |
| | | if (itemId == null) { |
| | | continue; |
| | | } |
| | | if (boundItemIds.value.includes(itemId)) { |
| | | skipped++; |
| | | continue; |
| | | } |
| | | try { |
| | | await createRouteProduct({ |
| | | routeId: data.routeId, |
| | | itemId, |
| | | quantity: data.quantity, |
| | | productionTime: data.productionTime, |
| | | timeUnitType: data.timeUnitType, |
| | | remark: data.remark, |
| | | }); |
| | | created++; |
| | | } catch { |
| | | failedIds.push(itemId); |
| | | } |
| | | } |
| | | if (skipped > 0) { |
| | | message.warning(`å·²è·³è¿ ${skipped} 个已ç»å®ç产å`); |
| | | } |
| | | if (failedIds.length > 0) { |
| | | message.error( |
| | | `æ ${failedIds.length} 个产åç»å®å¤±è´¥ï¼å¯è½å·²è¢«å
¶å®å·¥èºè·¯çº¿å ç¨ï¼`, |
| | | ); |
| | | } |
| | | if (created > 0) { |
| | | message.success(`æåç»å® ${created} 个产å`); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | } |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | formData.value = undefined; |
| | | boundItemIds.value = []; |
| | | return; |
| | | } |
| | | // å è½½æ°æ® |
| | | const data = modalApi.getData<{ |
| | | id?: number; |
| | | routeId: number; |
| | | boundItemIds?: number[]; |
| | | }>(); |
| | | if (!data) { |
| | | return; |
| | | } |
| | | boundItemIds.value = data.boundItemIds || []; |
| | | if (!data.id) { |
| | | // æ°å¢æ¨¡å¼ï¼å¤é产åç»å® |
| | | formData.value = undefined; |
| | | await formApi.setState({ |
| | | schema: useRouteProductFormSchema(handleItemChange, true), |
| | | }); |
| | | await formApi.setValues({ |
| | | routeId: data.routeId, |
| | | itemIds: [], |
| | | quantity: 1, |
| | | productionTime: 1, |
| | | timeUnitType: 'MINUTE', |
| | | remark: undefined, |
| | | }); |
| | | await formApi.setValues({ routeId: data.routeId }); |
| | | return; |
| | | } |
| | | // ç¼è¾æ¨¡å¼ï¼å è½½å个产ååå¡« |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getRouteProduct(data.id); |
| | | await formApi.setState({ |
| | | schema: useRouteProductFormSchema(handleItemChange, false), |
| | | }); |
| | | // è®¾ç½®å° values |
| | | await formApi.setValues(formData.value); |
| | | } finally { |
| | | modalApi.unlock(); |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-2/5"> |
| | | <Modal :title="getTitle" class="w-3/5"> |
| | | <Form class="mx-4" /> |
| | | <!-- ç¼è¾æ¨¡å¼ä¸å±ç¤ºäº§å BOM åè¡¨ï¼æ°å¢æ¨¡å¼ä¸éè --> |
| | | <template v-if="formData?.id && formData?.itemId"> |
| | | <Divider class="!my-3" orientation="left">产å BOM é
ç½®</Divider> |
| | | <div class="mx-4"> |
| | | <ProductBomList |
| | | :route-id="formData.routeId!" |
| | | :product-id="formData.itemId" |
| | | :product-name="formData.itemName" |
| | | /> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| | | </template> |
| | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { isAiEnable } from '@vben/hooks'; |
| | | |
| | | import { Empty, Input, Spin } from 'ant-design-vue'; |
| | | |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <a-button type="default" @click="modalApi.open()"> |
| | | <a-button v-if="isAiEnable()" type="default" @click="modalApi.open()"> |
| | | è´¨æ£å©æ |
| | | </a-button> |
| | | |
| | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { isAiEnable } from '@vben/hooks'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { Alert, Button, message, Tag } from 'ant-design-vue'; |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <Button type="default" :loading="loading" @click="handleSuggest"> |
| | | <Button |
| | | v-if="isAiEnable()" |
| | | type="default" |
| | | :loading="loading" |
| | | @click="handleSuggest" |
| | | > |
| | | <template #icon> |
| | | <IconifyIcon icon="ant-design:robot-outlined" /> |
| | | </template> |
| | |
| | | |
| | | import { useAccess } from '@vben/access'; |
| | | import { Page } from '@vben/common-ui'; |
| | | import { useTabs } from '@vben/hooks'; |
| | | import { isAiEnable, useTabs } from '@vben/hooks'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { |
| | |
| | | const { hasAccessByCodes } = useAccess(); |
| | | |
| | | /** AI 导å
¥æ¯ç¬ç«æéç ï¼è½æ¹æ¨¡æ¿ä¸çäºè½ç¨å¤§æ¨¡åè¯å«æä»¶ï¼æ¯æ¬¡è°ç¨é½è¦è±é±ï¼ */ |
| | | const canAiImport = computed(() => |
| | | hasAccessByCodes(['qc-report:template:ai-import']), |
| | | const canAiImport = computed( |
| | | () => isAiEnable() && hasAccessByCodes(['qc-report:template:ai-import']), |
| | | ); |
| | | |
| | | const loading = ref(true); |
| | |
| | | target: "http://127.0.0.1:48080", |
| | | ws: true, |
| | | }, |
| | | "/agent-api": { |
| | | changeOrigin: true, |
| | | target: "http://127.0.0.1:8091", |
| | | rewrite: (path) => path.replace(/^\/agent-api/, ""), |
| | | ws: true, |
| | | }, |
| | | }, |
| | | }, |
| | | }, |