hsy
5 天以前 fee2f2532144fca6c81b1523aeef54165c5263b2
Merge branch 'refs/heads/dev_pro2.0' into fix/hsy/20260812
已添加18个文件
已修改48个文件
3492 ■■■■■ 文件已修改
WTXC.bat 451 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
YZFX.bat 451 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
company.config.ts 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
internal/vite-config/src/utils/env.ts 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/erp/purchase/order/index.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/attendance/record/index.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/employee/index.ts 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/leave-type-config/index.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/overtime/index.ts 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/salary/structure/index.ts 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mdm/item/index.ts 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mdm/unit/index.ts 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/md/item/type/index.ts 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pd/document/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/company-config/index.ts 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/storage/index.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bootstrap.ts 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/auth.vue 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/@core/ui-kit/shadcn-ui/src/components/segmented/segmented.vue 25 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/constants/src/biz-wms-enum.ts 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/effects/layouts/src/widgets/preferences/blocks/company/company.vue 151 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/effects/layouts/src/widgets/preferences/blocks/index.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/locales/src/langs/en-US/preferences.json 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/locales/src/langs/zh-CN/preferences.json 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/company-config.ts 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/index.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/company-config.ts 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/data.ts 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/index.vue 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/modules/import-form.vue 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/unit/data.ts 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/unit/index.vue 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/unit/modules/import-form.vue 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/task/done/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/task/done/index.vue 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/finance/payment/data.ts 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/finance/payment/modules/form.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/invoice/data.ts 48 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/invoice/modules/form.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/invoice/modules/purchase-order-select.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/index.vue 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/attendance/record/data.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/attendance/record/modules/import-form.vue 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/data.ts 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/overtime/data.ts 320 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/overtime/detail/approve/index.vue 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/overtime/index.vue 200 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/overtime/modules/form.vue 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/leave-type-config/data.ts 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/leave-type-config/index.vue 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/leave-type-config/modules/form.vue 59 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/structure/data.ts 163 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/structure/index.vue 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/structure/modules/form.vue 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/item/type/data.ts 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/item/type/index.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/item/type/modules/import-form.vue 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/route/data.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/route/modules/process-form.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder/modules/process-form.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/route/data.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/route/modules/process-form.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/materialstock/components/stock-adjust-modal.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/stocktaking/plan/modules/param-form.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.ts 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WTXC.bat
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,451 @@
@echo off
setlocal EnableExtensions EnableDelayedExpansion
REM ============================================================
REM Vue Frontend Deploy Script
REM ============================================================
REM =========================
REM Configuration
REM =========================
set "COMPANY=WTXC"
set "REMOTE_USER=root"
set "REMOTE_HOST=42.63.71.140"
set "REMOTE_PORT=22"
set "PASSWORD=Nxwtxc123."
set "ZIP_FILE=dist.zip"
set "REMOTE_DIR=/home/vue/dist"
set "SSH_OPTS=-o StrictHostKeyChecking=no -o ConnectTimeout=10 -o ServerAliveInterval=15 -o ServerAliveCountMax=3"
set "RETRY_COUNT=3"
set "SSHPASS_DIR=%~dp0bin"
REM ============================================================
REM Start
REM ============================================================
echo.
echo ============================================================
echo              Vue Dist Auto Deploy
echo ============================================================
echo.
echo Company    : %COMPANY%
echo Server     : %REMOTE_USER%@%REMOTE_HOST%
echo Port       : %REMOTE_PORT%
echo Remote Dir : %REMOTE_DIR%
echo Package    : %ZIP_FILE%
echo.
REM ============================================================
REM Check sshpass
REM ============================================================
echo [1/7] Checking sshpass...
if exist "%SSHPASS_DIR%\sshpass.exe" (
    set "PATH=%SSHPASS_DIR%;%PATH%"
)
where sshpass >nul 2>&1
if not errorlevel 1 (
    echo [OK] sshpass found.
    goto CHECK_PNPM
)
echo [WARN] sshpass not found.
if not exist "%SSHPASS_DIR%" (
    mkdir "%SSHPASS_DIR%" >nul 2>&1
)
if exist "%SSHPASS_DIR%\sshpass.exe" (
    set "PATH=%SSHPASS_DIR%;%PATH%"
    echo [OK] sshpass found.
    goto CHECK_PNPM
)
echo [INFO] Downloading sshpass...
powershell -NoProfile -ExecutionPolicy Bypass -Command "$url='https://github.com/xhcoding/sshpass-win32/releases/download/v1.0.3/sshpass.exe'; $out='%SSHPASS_DIR%\sshpass.exe'; Invoke-WebRequest -Uri $url -OutFile $out"
if errorlevel 1 (
    echo [ERROR] Failed to download sshpass.
    echo [ERROR] Please install sshpass manually.
    goto ERROR_EXIT
)
set "PATH=%SSHPASS_DIR%;%PATH%"
echo [OK] sshpass installed.
REM ============================================================
REM Check pnpm
REM ============================================================
:CHECK_PNPM
echo.
echo [2/7] Checking pnpm...
where pnpm >nul 2>&1
if errorlevel 1 (
    echo [ERROR] pnpm not found.
    echo [HINT] npm install -g pnpm
    goto ERROR_EXIT
)
call pnpm --version
if errorlevel 1 (
    echo [ERROR] pnpm execution failed.
    goto ERROR_EXIT
)
REM ============================================================
REM Check node
REM ============================================================
echo.
echo [3/7] Checking Node.js...
where node >nul 2>&1
if errorlevel 1 (
    echo [ERROR] Node.js not found.
    goto ERROR_EXIT
)
call node --version
if errorlevel 1 (
    echo [ERROR] Node.js execution failed.
    goto ERROR_EXIT
)
REM ============================================================
REM Check node_modules
REM ============================================================
if not exist "node_modules" (
    echo.
    echo [INFO] node_modules not found.
    echo [INFO] Running pnpm install...
    call pnpm install
    if errorlevel 1 (
        echo [ERROR] pnpm install failed.
        goto ERROR_EXIT
    )
    echo [OK] pnpm install completed.
) else (
    echo [OK] node_modules exists.
)
REM ============================================================
REM Build
REM ============================================================
echo.
echo [4/7] Building project...
if exist "%ZIP_FILE%" (
    echo.
    echo [WARN] %ZIP_FILE% already exists.
    set "REBUILD="
    set /p "REBUILD=Rebuild? Y/N (default Y): "
    if /I "!REBUILD!"=="N" (
        echo [INFO] Skip build.
        goto CHECK_ZIP
    )
)
echo [INFO] Company: %COMPANY%
echo [INFO] Command: pnpm run build
echo.
set "VITE_COMPANY=%COMPANY%"
call pnpm run build
if errorlevel 1 (
    echo.
    echo [ERROR] Build failed.
    goto ERROR_EXIT
)
echo.
echo [OK] Build completed.
REM ============================================================
REM Check ZIP
REM ============================================================
:CHECK_ZIP
echo.
echo [5/7] Checking package...
if not exist "%ZIP_FILE%" (
    echo [ERROR] %ZIP_FILE% not found.
    echo [HINT] Please check whether your build generates dist.zip.
    goto ERROR_EXIT
)
for %%F in ("%ZIP_FILE%") do set "ZIP_BYTES=%%~zF"
set /a ZIP_MB=ZIP_BYTES/1024/1024
echo [OK] Package found.
echo [INFO] File size: !ZIP_BYTES! bytes
echo [INFO] File size: !ZIP_MB! MB
REM ============================================================
REM Set sshpass password
REM ============================================================
set "SSHPASS=%PASSWORD%"
REM ============================================================
REM Test SSH
REM ============================================================
echo.
echo [6/7] Testing SSH connection...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "echo SSH_OK"
if errorlevel 1 (
    echo [ERROR] SSH connection failed.
    goto ERROR_EXIT
)
echo [OK] SSH connection successful.
REM ============================================================
REM Create remote directory
REM ============================================================
echo.
echo [INFO] Creating remote directory...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mkdir -p '%REMOTE_DIR%'"
if errorlevel 1 (
    echo [ERROR] Failed to create remote directory.
    goto ERROR_EXIT
)
echo [OK] Remote directory ready.
REM ============================================================
REM Upload temporary file
REM ============================================================
echo.
echo [INFO] Uploading package...
set "REMOTE_TMP=%REMOTE_DIR%/.dist.zip.uploading"
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_TMP%'"
sshpass -e scp -P %REMOTE_PORT% %SSH_OPTS% "%ZIP_FILE%" %REMOTE_USER%@%REMOTE_HOST%:"%REMOTE_TMP%"
if errorlevel 1 (
    echo [ERROR] Upload failed.
    goto ERROR_EXIT
)
echo [OK] Upload completed.
REM ============================================================
REM Check remote file
REM ============================================================
echo.
echo [INFO] Checking remote package...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "test -f '%REMOTE_TMP%'"
if errorlevel 1 (
    echo [ERROR] Remote package does not exist.
    goto ERROR_EXIT
)
echo [OK] Remote package exists.
REM ============================================================
REM Compare file size
REM ============================================================
echo.
echo [INFO] Checking package size...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "stat -c %%s '%REMOTE_TMP%'"
echo.
REM ============================================================
REM Rename temporary file
REM ============================================================
echo [INFO] Activating package...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mv -f '%REMOTE_TMP%' '%REMOTE_DIR%/%ZIP_FILE%'"
if errorlevel 1 (
    echo [ERROR] Failed to activate package.
    goto ERROR_EXIT
)
echo [OK] Package activated.
REM ============================================================
REM Check unzip
REM ============================================================
echo.
echo [INFO] Checking unzip...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "command -v unzip"
if errorlevel 1 (
    echo [ERROR] unzip is not installed on server.
    echo.
    echo CentOS/RHEL:
    echo yum install -y unzip
    echo.
    echo Ubuntu/Debian:
    echo apt install -y unzip
    goto ERROR_EXIT
)
echo [OK] unzip found.
REM ============================================================
REM Test ZIP
REM ============================================================
echo.
echo [INFO] Testing remote ZIP...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -t '%ZIP_FILE%'"
if errorlevel 1 (
    echo.
    echo [ERROR] ZIP test failed.
    echo [INFO] Remote ZIP has been kept.
    goto ERROR_EXIT
)
echo [OK] ZIP test passed.
REM ============================================================
REM Extract
REM ============================================================
echo.
echo [INFO] Extracting package...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -o '%ZIP_FILE%'"
if errorlevel 1 (
    echo.
    echo [ERROR] Remote unzip failed.
    echo [INFO] Remote ZIP has been kept.
    goto ERROR_EXIT
)
echo.
echo [OK] Extract completed.
REM ============================================================
REM Remove ZIP
REM ============================================================
echo.
echo [INFO] Removing remote ZIP...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_DIR%/%ZIP_FILE%'"
if errorlevel 1 (
    echo [WARN] Failed to remove remote ZIP.
) else (
    echo [OK] Remote ZIP removed.
)
REM ============================================================
REM Success
REM ============================================================
echo.
echo ============================================================
echo                 DEPLOY SUCCESS
echo ============================================================
echo.
echo Company    : %COMPANY%
echo Server     : %REMOTE_HOST%:%REMOTE_PORT%
echo Remote Dir : %REMOTE_DIR%
echo Package    : %ZIP_FILE%
echo Size       : !ZIP_MB! MB
echo.
echo Deployment completed successfully.
echo.
goto END
REM ============================================================
REM Error
REM ============================================================
:ERROR_EXIT
echo.
echo ============================================================
echo                  DEPLOY FAILED
echo ============================================================
echo.
echo Please check the error message above.
echo.
goto END
REM ============================================================
REM End
REM ============================================================
:END
pause
endlocal
YZFX.bat
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,451 @@
@echo off
setlocal EnableExtensions EnableDelayedExpansion
REM ============================================================
REM Vue Frontend Deploy Script
REM ============================================================
REM =========================
REM Configuration
REM =========================
set "COMPANY=YZFX"
set "REMOTE_USER=root"
set "REMOTE_HOST=42.63.70.231"
set "REMOTE_PORT=22"
set "PASSWORD=Nxyzfxxcl1@"
set "ZIP_FILE=dist.zip"
set "REMOTE_DIR=/home/vue/dist"
set "SSH_OPTS=-o StrictHostKeyChecking=no -o ConnectTimeout=10 -o ServerAliveInterval=15 -o ServerAliveCountMax=3"
set "RETRY_COUNT=3"
set "SSHPASS_DIR=%~dp0bin"
REM ============================================================
REM Start
REM ============================================================
echo.
echo ============================================================
echo              Vue Dist Auto Deploy
echo ============================================================
echo.
echo Company    : %COMPANY%
echo Server     : %REMOTE_USER%@%REMOTE_HOST%
echo Port       : %REMOTE_PORT%
echo Remote Dir : %REMOTE_DIR%
echo Package    : %ZIP_FILE%
echo.
REM ============================================================
REM Check sshpass
REM ============================================================
echo [1/7] Checking sshpass...
if exist "%SSHPASS_DIR%\sshpass.exe" (
    set "PATH=%SSHPASS_DIR%;%PATH%"
)
where sshpass >nul 2>&1
if not errorlevel 1 (
    echo [OK] sshpass found.
    goto CHECK_PNPM
)
echo [WARN] sshpass not found.
if not exist "%SSHPASS_DIR%" (
    mkdir "%SSHPASS_DIR%" >nul 2>&1
)
if exist "%SSHPASS_DIR%\sshpass.exe" (
    set "PATH=%SSHPASS_DIR%;%PATH%"
    echo [OK] sshpass found.
    goto CHECK_PNPM
)
echo [INFO] Downloading sshpass...
powershell -NoProfile -ExecutionPolicy Bypass -Command "$url='https://github.com/xhcoding/sshpass-win32/releases/download/v1.0.3/sshpass.exe'; $out='%SSHPASS_DIR%\sshpass.exe'; Invoke-WebRequest -Uri $url -OutFile $out"
if errorlevel 1 (
    echo [ERROR] Failed to download sshpass.
    echo [ERROR] Please install sshpass manually.
    goto ERROR_EXIT
)
set "PATH=%SSHPASS_DIR%;%PATH%"
echo [OK] sshpass installed.
REM ============================================================
REM Check pnpm
REM ============================================================
:CHECK_PNPM
echo.
echo [2/7] Checking pnpm...
where pnpm >nul 2>&1
if errorlevel 1 (
    echo [ERROR] pnpm not found.
    echo [HINT] npm install -g pnpm
    goto ERROR_EXIT
)
call pnpm --version
if errorlevel 1 (
    echo [ERROR] pnpm execution failed.
    goto ERROR_EXIT
)
REM ============================================================
REM Check node
REM ============================================================
echo.
echo [3/7] Checking Node.js...
where node >nul 2>&1
if errorlevel 1 (
    echo [ERROR] Node.js not found.
    goto ERROR_EXIT
)
call node --version
if errorlevel 1 (
    echo [ERROR] Node.js execution failed.
    goto ERROR_EXIT
)
REM ============================================================
REM Check node_modules
REM ============================================================
if not exist "node_modules" (
    echo.
    echo [INFO] node_modules not found.
    echo [INFO] Running pnpm install...
    call pnpm install
    if errorlevel 1 (
        echo [ERROR] pnpm install failed.
        goto ERROR_EXIT
    )
    echo [OK] pnpm install completed.
) else (
    echo [OK] node_modules exists.
)
REM ============================================================
REM Build
REM ============================================================
echo.
echo [4/7] Building project...
if exist "%ZIP_FILE%" (
    echo.
    echo [WARN] %ZIP_FILE% already exists.
    set "REBUILD="
    set /p "REBUILD=Rebuild? Y/N (default Y): "
    if /I "!REBUILD!"=="N" (
        echo [INFO] Skip build.
        goto CHECK_ZIP
    )
)
echo [INFO] Company: %COMPANY%
echo [INFO] Command: pnpm run build
echo.
set "VITE_COMPANY=%COMPANY%"
call pnpm run build
if errorlevel 1 (
    echo.
    echo [ERROR] Build failed.
    goto ERROR_EXIT
)
echo.
echo [OK] Build completed.
REM ============================================================
REM Check ZIP
REM ============================================================
:CHECK_ZIP
echo.
echo [5/7] Checking package...
if not exist "%ZIP_FILE%" (
    echo [ERROR] %ZIP_FILE% not found.
    echo [HINT] Please check whether your build generates dist.zip.
    goto ERROR_EXIT
)
for %%F in ("%ZIP_FILE%") do set "ZIP_BYTES=%%~zF"
set /a ZIP_MB=ZIP_BYTES/1024/1024
echo [OK] Package found.
echo [INFO] File size: !ZIP_BYTES! bytes
echo [INFO] File size: !ZIP_MB! MB
REM ============================================================
REM Set sshpass password
REM ============================================================
set "SSHPASS=%PASSWORD%"
REM ============================================================
REM Test SSH
REM ============================================================
echo.
echo [6/7] Testing SSH connection...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "echo SSH_OK"
if errorlevel 1 (
    echo [ERROR] SSH connection failed.
    goto ERROR_EXIT
)
echo [OK] SSH connection successful.
REM ============================================================
REM Create remote directory
REM ============================================================
echo.
echo [INFO] Creating remote directory...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mkdir -p '%REMOTE_DIR%'"
if errorlevel 1 (
    echo [ERROR] Failed to create remote directory.
    goto ERROR_EXIT
)
echo [OK] Remote directory ready.
REM ============================================================
REM Upload temporary file
REM ============================================================
echo.
echo [INFO] Uploading package...
set "REMOTE_TMP=%REMOTE_DIR%/.dist.zip.uploading"
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_TMP%'"
sshpass -e scp -P %REMOTE_PORT% %SSH_OPTS% "%ZIP_FILE%" %REMOTE_USER%@%REMOTE_HOST%:"%REMOTE_TMP%"
if errorlevel 1 (
    echo [ERROR] Upload failed.
    goto ERROR_EXIT
)
echo [OK] Upload completed.
REM ============================================================
REM Check remote file
REM ============================================================
echo.
echo [INFO] Checking remote package...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "test -f '%REMOTE_TMP%'"
if errorlevel 1 (
    echo [ERROR] Remote package does not exist.
    goto ERROR_EXIT
)
echo [OK] Remote package exists.
REM ============================================================
REM Compare file size
REM ============================================================
echo.
echo [INFO] Checking package size...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "stat -c %%s '%REMOTE_TMP%'"
echo.
REM ============================================================
REM Rename temporary file
REM ============================================================
echo [INFO] Activating package...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mv -f '%REMOTE_TMP%' '%REMOTE_DIR%/%ZIP_FILE%'"
if errorlevel 1 (
    echo [ERROR] Failed to activate package.
    goto ERROR_EXIT
)
echo [OK] Package activated.
REM ============================================================
REM Check unzip
REM ============================================================
echo.
echo [INFO] Checking unzip...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "command -v unzip"
if errorlevel 1 (
    echo [ERROR] unzip is not installed on server.
    echo.
    echo CentOS/RHEL:
    echo yum install -y unzip
    echo.
    echo Ubuntu/Debian:
    echo apt install -y unzip
    goto ERROR_EXIT
)
echo [OK] unzip found.
REM ============================================================
REM Test ZIP
REM ============================================================
echo.
echo [INFO] Testing remote ZIP...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -t '%ZIP_FILE%'"
if errorlevel 1 (
    echo.
    echo [ERROR] ZIP test failed.
    echo [INFO] Remote ZIP has been kept.
    goto ERROR_EXIT
)
echo [OK] ZIP test passed.
REM ============================================================
REM Extract
REM ============================================================
echo.
echo [INFO] Extracting package...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -o '%ZIP_FILE%'"
if errorlevel 1 (
    echo.
    echo [ERROR] Remote unzip failed.
    echo [INFO] Remote ZIP has been kept.
    goto ERROR_EXIT
)
echo.
echo [OK] Extract completed.
REM ============================================================
REM Remove ZIP
REM ============================================================
echo.
echo [INFO] Removing remote ZIP...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_DIR%/%ZIP_FILE%'"
if errorlevel 1 (
    echo [WARN] Failed to remove remote ZIP.
) else (
    echo [OK] Remote ZIP removed.
)
REM ============================================================
REM Success
REM ============================================================
echo.
echo ============================================================
echo                 DEPLOY SUCCESS
echo ============================================================
echo.
echo Company    : %COMPANY%
echo Server     : %REMOTE_HOST%:%REMOTE_PORT%
echo Remote Dir : %REMOTE_DIR%
echo Package    : %ZIP_FILE%
echo Size       : !ZIP_MB! MB
echo.
echo Deployment completed successfully.
echo.
goto END
REM ============================================================
REM Error
REM ============================================================
:ERROR_EXIT
echo.
echo ============================================================
echo                  DEPLOY FAILED
echo ============================================================
echo.
echo Please check the error message above.
echo.
goto END
REM ============================================================
REM End
REM ============================================================
:END
pause
endlocal
company.config.ts
@@ -30,9 +30,15 @@
    apiUrl: 'http://42.63.70.90:9001/admin-api',
  },
  YZFX: {
    name: '宁夏英泽除锈',
    name: '宁夏英泽防锈',
    logo: '/logos/YZFXLogo.png',
    favicon: '/logos/YZFXLogo.ico',
    apiUrl: 'http://42.63.70.231:9001/admin-api',
  },
  WTXC: {
    name: '宁夏万通新材',
    logo: '/logos/YZFXLogo.png',
    favicon: '/logos/YZFXLogo.ico',
    apiUrl: 'http://42.63.71.140:9001/admin-api',
  },
};
internal/vite-config/src/utils/env.ts
@@ -38,7 +38,7 @@
  match = 'VITE_GLOB_',
  confFiles = getConfFiles(),
) {
  let envConfig = {};
  let envConfig: Record<string, string> = {};
  for (const confFile of confFiles) {
    try {
@@ -60,6 +60,13 @@
      Reflect.deleteProperty(envConfig, key);
    }
  });
  // åˆå¹¶è¿›ç¨‹çŽ¯å¢ƒå˜é‡ï¼Œä½¿ vite.config.ts æŒ‰å…¬å¸æ³¨å…¥çš„ VITE_GLOB_API_URL èƒ½è¿›å…¥ _app-config æ–‡ä»¶
  Object.keys(process.env).forEach((key) => {
    const value = process.env[key];
    if (reg.test(key) && value) {
      envConfig[key] = value;
    }
  });
  return envConfig as T;
}
src/api/erp/purchase/order/index.ts
@@ -13,6 +13,7 @@
    totalCount?: number; // åˆè®¡æ•°é‡
    totalPrice?: number; // åˆè®¡é‡‘额,单位:元
    totalProductPrice?: number; // äº§å“é‡‘额,单位:元
    totalTaxPrice?: number; // åˆè®¡ç¨Žé¢ï¼Œå•位:元
    discountPercent?: number; // ä¼˜æƒ çŽ‡ï¼Œç™¾åˆ†æ¯”
    discountPrice?: number; // ä¼˜æƒ é‡‘额,单位:元
    depositPrice?: number; // å®šé‡‘金额,单位:元
@@ -103,6 +104,13 @@
  return requestClient.get<any[]>('/erp/purchase-order/approve-process-list');
}
/** åå®¡æ‰¹ï¼ˆååŒåŠžå…¬å·²åŠžä»»åŠ¡ä¸­æ’¤å›žå·²å®¡æ ¸è®¢å•ï¼Œå›žé€€ä¸ºæœªå®¡æ ¸ï¼‰ */
export function reverseApprove(processInstanceId: string) {
  return requestClient.post('/erp/purchase-order/reverse-approve', null, {
    params: { processInstanceId },
  });
}
/** èŽ·å–é‡‡è´­å®¡æ ¸æµç¨‹åˆ—è¡¨å“åº” */
export interface ProcessDefinition {
  id: string; // æµç¨‹å®šä¹‰ID
src/api/hrm/attendance/record/index.ts
@@ -10,6 +10,7 @@
    userName?: string;
    deptId?: number;
    deptName?: string;
    phone?: string;
    date?: string;
    clockInTime?: string;
    clockOutTime?: string;
src/api/hrm/employee/index.ts
@@ -29,6 +29,7 @@
    employeeStatus?: number;
    annualLeaveBalance?: number;
    sickLeaveBalance?: number;
    salaryStructureId?: number;
    baseSalary?: number;
    roleIds?: number[];
    roleNames?: string[];
@@ -167,4 +168,4 @@
/** åˆ é™¤ç´§æ€¥è”系人 */
export function deleteEmergencyContact(id: number) {
  return requestClient.delete(`/hrm/employee/emergency-contact/delete?id=${id}`);
}
}
src/api/hrm/leave-type-config/index.ts
@@ -22,7 +22,17 @@
  );
}
/** æ–°å¢žè¯·å‡ç±»åž‹é…ç½® */
export function createLeaveTypeConfig(data: HrmLeaveTypeConfigApi.LeaveTypeConfig) {
  return requestClient.post<number>('/hrm/leave-type-config/create', data);
}
/** æ›´æ–°è¯·å‡ç±»åž‹é…ç½® */
export function updateLeaveTypeConfig(data: HrmLeaveTypeConfigApi.LeaveTypeConfig) {
  return requestClient.put('/hrm/leave-type-config/update', data);
}
/** åˆ é™¤è¯·å‡ç±»åž‹é…ç½® */
export function deleteLeaveTypeConfig(id: number) {
  return requestClient.delete(`/hrm/leave-type-config/delete?id=${id}`);
}
src/api/hrm/overtime/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,89 @@
import type { PageParam, PageResult } from '#/packages/effects/request/src';
import { requestClient } from '#/api/request';
export namespace HrmOvertimeApi {
  /** åŠ ç­ç”³è¯· */
  export interface Overtime {
    id?: number;
    employeeId?: number;
    employeeName?: string;
    employeeNo?: string;
    deptName?: string;
    postName?: string;
    overtimeDate?: string;
    startTime?: string;
    endTime?: string;
    duration?: number;
    overtimeType?: number;
    crossDay?: boolean;
    mealDeduction?: boolean;
    mealDeductionHours?: number;
    reason?: string;
    relatedType?: number;
    relatedCode?: string;
    workContent?: string;
    status?: number;
    processInstanceId?: string;
    createTime?: string;
  }
  export interface ApproveProcess {
    key: string;
    name: string;
  }
}
/** æŸ¥è¯¢åŠ ç­ç”³è¯·åˆ†é¡µåˆ—è¡¨ */
export function getOvertimePage(params: PageParam) {
  return requestClient.get<PageResult<HrmOvertimeApi.Overtime>>(
    '/hrm/overtime/page',
    { params },
  );
}
/** æŸ¥è¯¢åŠ ç­ç”³è¯·è¯¦æƒ… */
export function getOvertime(id: number) {
  return requestClient.get<HrmOvertimeApi.Overtime>(
    `/hrm/overtime/get?id=${id}`,
  );
}
/** èŽ·å–å½“å‰ç”³è¯·äººå…³è”çš„å‘˜å·¥æ¡£æ¡ˆ */
export function getCurrentOvertimeApplicant() {
  return requestClient.get<HrmOvertimeApi.Overtime>('/hrm/overtime/current-applicant');
}
/** æ–°å¢žåŠ ç­ç”³è¯·ï¼ˆè‰ç¨¿ï¼‰ */
export function createOvertime(data: HrmOvertimeApi.Overtime) {
  return requestClient.post('/hrm/overtime/create', data);
}
/** æ›´æ–°åŠ ç­ç”³è¯· */
export function updateOvertime(data: HrmOvertimeApi.Overtime) {
  return requestClient.put('/hrm/overtime/update', data);
}
/** åˆ é™¤åŠ ç­ç”³è¯· */
export function deleteOvertime(id: number) {
  return requestClient.delete(`/hrm/overtime/delete?id=${id}`);
}
/** æäº¤åŠ ç­ç”³è¯·ï¼ˆå‘èµ·å®¡æ‰¹ï¼‰ */
export function submitOvertime(id: number, processDefinitionKey: string) {
  return requestClient.post('/hrm/overtime/submit', null, {
    params: { id, processDefinitionKey },
  });
}
/** æ’¤é”€åŠ ç­ç”³è¯· */
export function cancelOvertime(id: number) {
  return requestClient.post(`/hrm/overtime/cancel?id=${id}`);
}
/** èŽ·å–ååŒåŠžå…¬ä¸­çš„å¯ç”¨åŠ ç­å®¡æ‰¹æµç¨‹ */
export function getOvertimeApproveProcessDefinitionList() {
  return requestClient.get<HrmOvertimeApi.ApproveProcess[]>(
    '/hrm/overtime/approve-process-definition-list',
  );
}
src/api/hrm/salary/structure/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
import { requestClient } from '#/api/request';
export namespace HrmSalaryStructureApi {
  export interface SalaryStructure {
    id?: number;
    name?: string;
    overtimePayRatio?: number;
    restDayOvertimePayRatio?: number;
    legalHolidayOvertimePayRatio?: number;
    status?: number;
    remark?: string;
    createTime?: string;
  }
  export interface SalaryStructureQueryParam {
    name?: string;
    status?: number;
  }
}
export function getSalaryStructureList(
  params?: HrmSalaryStructureApi.SalaryStructureQueryParam,
) {
  return requestClient.get<HrmSalaryStructureApi.SalaryStructure[]>(
    '/hrm/salary/structure/list',
    { params },
  );
}
export function getSalaryStructure(id: number) {
  return requestClient.get<HrmSalaryStructureApi.SalaryStructure>(
    `/hrm/salary/structure/get?id=${id}`,
  );
}
export function createSalaryStructure(
  data: HrmSalaryStructureApi.SalaryStructure,
) {
  return requestClient.post('/hrm/salary/structure/create', data);
}
export function updateSalaryStructure(
  data: HrmSalaryStructureApi.SalaryStructure,
) {
  return requestClient.put('/hrm/salary/structure/update', data);
}
export function deleteSalaryStructure(id: number) {
  return requestClient.delete(`/hrm/salary/structure/delete?id=${id}`);
}
src/api/mdm/item/index.ts
@@ -116,3 +116,16 @@
export function exportItem(params: Partial<MdmItemApi.Item>) {
  return requestClient.download('/mdm/item/export-excel', { params });
}
/** ä¸‹è½½ç‰©æ–™å¯¼å…¥æ¨¡æ¿ */
export function importItemTemplate() {
  return requestClient.download('/mdm/item/get-import-template');
}
/** å¯¼å…¥ç‰©æ–™ */
export function importItem(file: File, updateSupport: boolean) {
  return requestClient.upload('/mdm/item/import', {
    file,
    updateSupport,
  });
}
src/api/mdm/unit/index.ts
@@ -47,3 +47,16 @@
export function deleteUnit(id: number) {
  return requestClient.delete(`/mdm/unit/delete?id=${id}`);
}
/** ä¸‹è½½è®¡é‡å•位导入模板 */
export function importUnitTemplate() {
  return requestClient.download('/mdm/unit/get-import-template');
}
/** å¯¼å…¥è®¡é‡å•位 */
export function importUnit(file: File, updateSupport: boolean) {
  return requestClient.upload('/mdm/unit/import', {
    file,
    updateSupport,
  });
}
src/api/mes/md/item/type/index.ts
@@ -52,3 +52,16 @@
export function deleteItemType(id: number) {
  return requestClient.delete(`/mes/md/item-type/delete?id=${id}`);
}
/** ä¸‹è½½ç‰©æ–™äº§å“åˆ†ç±»å¯¼å…¥æ¨¡æ¿ */
export function importItemTypeTemplate() {
  return requestClient.download('/mes/md/item-type/get-import-template');
}
/** å¯¼å…¥ç‰©æ–™äº§å“åˆ†ç±» */
export function importItemType(file: File, updateSupport: boolean) {
  return requestClient.upload('/mes/md/item-type/import', {
    file,
    updateSupport,
  });
}
src/api/mes/pd/document/index.ts
@@ -17,6 +17,8 @@
    documentVersion?: string;
    /** æ–‡ä»¶å */
    fileName?: string;
    /** æ–‡ä»¶åœ°å€ */
    fileUrl?: string;
    /** ä¸Šä¼ äººï¼ˆç”¨æˆ·ID) */
    uploadUserId?: number;
    /** ä¸Šä¼ äººåç§° */
src/api/system/company-config/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
import { requestClient } from '#/api/request';
export namespace SystemCompanyConfigApi {
  /** å…¬å¸å“ç‰Œä¿¡æ¯ */
  export interface CompanyConfig {
    id?: number;
    companyName?: string;
    logoUrl?: string;
    logoBlobId?: number;
    loginTitle?: string;
    loginDescription?: string;
  }
}
/** èŽ·å¾—å…¬å¸å“ç‰Œä¿¡æ¯ï¼ˆå…ç™»å½•å…¬å¼€ï¼Œç™»å½•é¡µ / åº”用启动时使用) */
export async function getCompanyConfig() {
  return requestClient.get<SystemCompanyConfigApi.CompanyConfig | null>(
    '/system/company-config/get',
  );
}
/** ä¿å­˜å…¬å¸å“ç‰Œä¿¡æ¯ */
export async function saveCompanyConfig(
  data: SystemCompanyConfigApi.CompanyConfig,
) {
  return requestClient.post<number>('/system/company-config/save', data);
}
src/api/system/storage/index.ts
@@ -45,6 +45,17 @@
  );
}
/** å…¬å…±æ–‡ä»¶ä¸Šä¼ ï¼ˆæ°¸ä¹…有效,返回永久公开 previewURL,用于登录页等免登录展示的场景) */
export function publicUploadFile(files: globalThis.File[]) {
  const formData = new FormData();
  files.forEach((file) => formData.append('files', file));
  return requestClient.post<SystemStorageApi.StorageBlob[]>(
    '/system/storage-blob/public-upload',
    formData,
    { headers: { 'Content-Type': 'multipart/form-data' } },
  );
}
/** æŸ¥è¯¢é™„件列表 */
export function listAttachments(params: SystemStorageApi.AttachmentListParams) {
  return requestClient.get<SystemStorageApi.StorageBlob[]>(
src/bootstrap.ts
@@ -18,6 +18,9 @@
import App from './app.vue';
import { router } from './router';
import { getCompanyConfig } from '#/api/system/company-config';
import { applyCompanyConfig } from '#/utils/company-config';
async function bootstrap(namespace: string) {
  // åˆå§‹åŒ–组件适配器
  await initComponentAdapter();
@@ -49,6 +52,17 @@
  // é…ç½® pinia-store
  await initStores(app, { namespace });
  // åŠ è½½å…¬å¸å“ç‰Œä¿¡æ¯ï¼ˆä»ŽåŽç«¯ï¼Œå…ç™»å½•æŽ¥å£ï¼‰ï¼Œè”åŠ¨ç™»å½•é¡µ/菜单栏/网页标题。
  // æ³¨ï¼šæ­¤æ—¶ pinia å°šæœªåœ¨ç»„件外激活,这里用纯函数,不依赖 store。
  try {
    const companyConfig = await getCompanyConfig();
    if (companyConfig) {
      applyCompanyConfig(companyConfig);
    }
  } catch {
    // åŽç«¯ä¸å¯ç”¨æˆ–未配置,保持编译期默认
  }
  // å®‰è£…权限指令
  registerAccessDirective(app);
src/layouts/auth.vue
@@ -1,16 +1,32 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { computed, onMounted } from 'vue';
import { AuthPageLayout } from '@vben/layouts';
import { preferences } from '@vben/preferences';
import { $t } from '#/locales';
import { useCompanyConfigStore } from '#/store/company-config';
declare const __APP_LOGO__: string;
const companyConfigStore = useCompanyConfigStore();
onMounted(() => {
  // ç™»å½•页展示 loginTitle:启动加载走的是纯函数(不填 store),进入登录页时拉取一次
  companyConfigStore.loadFromServer();
});
const appName = computed(() => preferences.copyright.companyName);
const logo = computed(() => __APP_LOGO__ || preferences.logo.source);
// ç™»å½•页 logo ä¼˜å…ˆå–数据库配置(preferences.logo.source),未配置时回退编译期默认 __APP_LOGO__
const logo = computed(() => preferences.logo.source || __APP_LOGO__);
const logoDark = computed(() => preferences.logo.sourceDark);
// ç™»å½•页标题/描述优先取后端配置,未配置时回退到 i18n é»˜è®¤
const pageTitle = computed(
  () => companyConfigStore.loginTitle || $t('authentication.pageTitle'),
);
const pageDescription = computed(
  () => companyConfigStore.loginDescription || $t('authentication.pageDesc'),
);
</script>
<template>
@@ -18,8 +34,8 @@
    :app-name="appName"
    :logo="logo"
    :logo-dark="logoDark"
    :page-description="$t('authentication.pageDesc')"
    :page-title="$t('authentication.pageTitle')"
    :page-description="pageDescription"
    :page-title="pageTitle"
  >
    <!-- è‡ªå®šä¹‰å·¥å…·æ  -->
    <!-- <template #toolbar></template> -->
src/packages/@core/ui-kit/shadcn-ui/src/components/segmented/segmented.vue
@@ -5,17 +5,22 @@
import { TabsTrigger } from 'reka-ui';
import { cn } from '../../../../../base/shared/src/utils';
import { Tabs, TabsContent, TabsList } from '../../ui';
import TabsIndicator from './tabs-indicator.vue';
interface Props {
  defaultValue?: string;
  tabs?: SegmentedItem[];
  /** æ˜¯å¦å…è®¸æ¨ªå‘滚动:tab è¾ƒå¤šæ—¶å¯ç”¨ï¼Œå„ tab æŒ‰å†…容宽度排列,超出部分可滑动 */
  scrollable?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
  defaultValue: '',
  tabs: () => [],
  scrollable: false,
});
const activeTab = defineModel<string>();
@@ -25,12 +30,18 @@
});
const tabsStyle = computed(() => {
  if (props.scrollable) {
    return undefined;
  }
  return {
    'grid-template-columns': `repeat(${props.tabs.length}, minmax(0, 1fr))`,
  };
});
const tabsIndicatorStyle = computed(() => {
  if (props.scrollable) {
    return undefined;
  }
  return {
    width: `${(100 / props.tabs.length).toFixed(0)}%`,
  };
@@ -45,14 +56,22 @@
  <Tabs v-model="activeTab" :default-value="getDefaultValue">
    <TabsList
      :style="tabsStyle"
      class="bg-accent outline-heavy! relative grid w-full outline-2!"
      :class="
        cn(
          'bg-accent outline-heavy! relative outline-2!',
          // å¯æ»šåŠ¨æ¨¡å¼ï¼šå·¦å¯¹é½ï¼ˆè¦†ç›– reka-ui çš„ justify-center,否则内容超宽时左侧被挤出且无法滚动)、全宽、横向滚动
          scrollable
            ? 'flex! w-full! overflow-x-auto justify-start!'
            : 'grid w-full',
        )
      "
    >
      <TabsIndicator :style="tabsIndicatorStyle" />
      <TabsIndicator v-if="!scrollable" :style="tabsIndicatorStyle" />
      <template v-for="tab in tabs" :key="tab.value">
        <TabsTrigger
          :value="tab.value"
          :class="activeClass(tab.value)"
          class="hover:text-primary z-20 inline-flex items-center justify-center rounded-md px-3 py-1 text-sm font-medium whitespace-nowrap disabled:pointer-events-none disabled:opacity-50"
          class="hover:text-primary z-20 inline-flex shrink-0 items-center justify-center rounded-md px-2 py-1 text-sm font-medium whitespace-nowrap disabled:pointer-events-none disabled:opacity-50"
        >
          {{ tab.label }}
        </TabsTrigger>
src/packages/constants/src/biz-wms-enum.ts
@@ -57,14 +57,14 @@
  return prefix + result;
}
/** ç›˜ç‚¹æ¡ä»¶ç±»åž‹æžšä¸¾ */
/** ç›˜ç‚¹æ¡ä»¶ç±»åž‹æžšä¸¾ï¼ˆä¸ŽåŽç«¯ MesBizTypeConstants / å­—典值保持一致) */
export const WlsStockTakingParamTypeEnum = {
  WAREHOUSE: 1, // ä»“库
  LOCATION: 2, // åº“区
  AREA: 3, // åº“位
  ITEM: 4, // ç‰©æ–™
  BATCH: 5, // æ‰¹æ¬¡
  QUALITY_STATUS: 6, // è´¨é‡çŠ¶æ€
  WAREHOUSE: 102, // ä»“库
  LOCATION: 103, // åº“区
  AREA: 104, // åº“位
  ITEM: 600, // ç‰©æ–™
  BATCH: 107, // æ‰¹æ¬¡
  QUALITY_STATUS: 900, // è´¨é‡çŠ¶æ€
} as const;
/** ç›˜ç‚¹ç±»åž‹æžšä¸¾ */
src/packages/effects/layouts/src/widgets/preferences/blocks/company/company.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,151 @@
<script setup lang="ts">
import { message } from 'ant-design-vue';
import { Button, Input, Upload } from 'ant-design-vue';
import { onMounted, ref } from 'vue';
// æ³¨æ„ï¼šæœ¬æ–‡ä»¶ä½äºŽ @vben çš„ packages å†…部,无法使用应用层别名 #/,需用相对路径(与同目录 custom.vue ä¿æŒä¸€è‡´ï¼‰
import { publicUploadFile } from '../../../../../../../../api/system/storage';
import { $t } from '../../../../../../../locales/src';
import { useCompanyConfigStore } from '../../../../../../../../store/company-config';
defineOptions({
  name: 'PreferenceCompanyConfig',
});
const companyConfigStore = useCompanyConfigStore();
const companyName = ref(companyConfigStore.companyName);
const loginTitle = ref(companyConfigStore.loginTitle);
const loginDescription = ref(companyConfigStore.loginDescription);
const logoUrl = ref(companyConfigStore.logoUrl);
const uploading = ref(false);
// æ‰“开区块时从后端拉取最新公司配置并回填表单(store åˆå€¼å¯èƒ½ä¸ºç©ºï¼Œå› å¯åŠ¨åŠ è½½èµ°çš„æ˜¯çº¯å‡½æ•°ï¼‰
onMounted(async () => {
  await companyConfigStore.loadFromServer();
  companyName.value = companyConfigStore.companyName;
  loginTitle.value = companyConfigStore.loginTitle;
  loginDescription.value = companyConfigStore.loginDescription;
  logoUrl.value = companyConfigStore.logoUrl;
});
/** ä¸Šä¼  logo(永久公开,供登录页免登录展示) */
async function handleBeforeUpload(file: File) {
  uploading.value = true;
  try {
    const blobs = await publicUploadFile([file]);
    const blob = blobs?.[0];
    if (blob?.previewURL || blob?.url) {
      logoUrl.value = blob.previewURL || blob.url || '';
      message.success($t('preferences.company.uploadSuccess'));
    } else {
      message.error($t('preferences.company.uploadFailed'));
    }
  } catch {
    message.error($t('preferences.company.uploadFailed'));
  } finally {
    uploading.value = false;
  }
  // è¿”回 false,阻止组件默认上传(已通过接口上传)
  return false;
}
async function handleSave() {
  if (!companyName.value.trim()) {
    message.warning($t('preferences.company.companyNameRequired'));
    return;
  }
  try {
    await companyConfigStore.save({
      companyName: companyName.value.trim(),
      logoUrl: logoUrl.value || undefined,
      loginTitle: loginTitle.value,
      loginDescription: loginDescription.value,
    });
    message.success($t('preferences.company.saveSuccess'));
  } catch {
    message.error($t('preferences.company.saveFailed'));
  }
}
</script>
<template>
  <div class="flex flex-col">
    <!-- å…¬å¸åç§° -->
    <div class="my-2 flex w-full items-center justify-between gap-4 rounded-md px-2 py-2">
      <span class="shrink-0 text-sm text-foreground/80">
        {{ $t('preferences.company.companyName') }}
      </span>
      <Input
        v-model:value="companyName"
        :placeholder="$t('preferences.company.companyNamePlaceholder')"
        class="w-41.25"
        allow-clear
      />
    </div>
    <!-- ç™»å½•页标题 -->
    <div class="my-2 flex w-full items-center justify-between gap-4 rounded-md px-2 py-2">
      <span class="shrink-0 text-sm text-foreground/80">
        {{ $t('preferences.company.loginTitle') }}
      </span>
      <Input
        v-model:value="loginTitle"
        :placeholder="$t('preferences.company.loginTitlePlaceholder')"
        class="w-41.25"
        allow-clear
      />
    </div>
    <!-- ç™»å½•页描述 -->
    <div class="my-2 flex w-full items-center justify-between gap-4 rounded-md px-2 py-2">
      <span class="shrink-0 text-sm text-foreground/80">
        {{ $t('preferences.company.loginDescription') }}
      </span>
      <Input
        v-model:value="loginDescription"
        :placeholder="$t('preferences.company.loginDescriptionPlaceholder')"
        class="w-41.25"
        allow-clear
      />
    </div>
    <!-- å…¬å¸ Logo -->
    <div class="my-2 flex w-full items-center justify-between gap-4 rounded-md px-2 py-2">
      <span class="shrink-0 text-sm text-foreground/80">
        {{ $t('preferences.company.logo') }}
      </span>
      <div class="flex items-center gap-2">
        <Upload
          :show-upload-list="false"
          accept="image/*"
          :before-upload="handleBeforeUpload"
        >
          <Button size="small" :loading="uploading">
            {{ $t('preferences.company.uploadLogo') }}
          </Button>
        </Upload>
        <img
          v-if="logoUrl"
          :src="logoUrl"
          alt="company-logo"
          class="h-9 w-9 rounded border border-border object-contain"
        />
        <span v-else class="text-xs text-muted-foreground">
          {{ $t('preferences.company.logoEmpty') }}
        </span>
      </div>
    </div>
    <div v-if="uploading" class="px-2 pb-1 text-xs text-muted-foreground">
      {{ $t('preferences.company.uploading') }}
    </div>
    <!-- ä¿å­˜ -->
    <div class="mt-3 px-2">
      <Button type="primary" class="w-full" @click="handleSave">
        {{ $t('preferences.company.save') }}
      </Button>
    </div>
  </div>
</template>
src/packages/effects/layouts/src/widgets/preferences/blocks/index.ts
@@ -1,4 +1,5 @@
export { default as Block } from './block.vue';
export { default as Company } from './company/company.vue';
export { default as Custom } from './custom/custom.vue';
export { default as Animation } from './general/animation.vue';
export { default as General } from './general/general.vue';
src/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue
@@ -43,6 +43,7 @@
  Breadcrumb,
  BuiltinTheme,
  ColorMode,
  Company,
  Content,
  Copyright,
  Custom,
@@ -256,6 +257,10 @@
      label: $t('preferences.general'),
      value: 'general',
    },
    {
      label: $t('preferences.company.title'),
      value: 'company',
    },
  ];
  if (showCustomTab.value) {
@@ -352,6 +357,7 @@
        <VbenSegmented
          v-model="activeTab"
          :tabs="tabs"
          scrollable
          :class="{
            'sticky-tabs-header': appEnableStickyPreferencesNavigationBar,
          }"
@@ -526,6 +532,11 @@
              />
            </Block>
          </template>
          <template #company>
            <Block :title="$t('preferences.company.title')">
              <Company />
            </Block>
          </template>
          <template #custom>
            <Block :title="customTabTitle">
              <Custom
src/packages/locales/src/langs/en-US/preferences.json
@@ -229,5 +229,24 @@
        "placeholder": "Please enter the default report title"
      }
    }
  },
  "company": {
    "title": "Company Info",
    "companyName": "Company Name",
    "companyNamePlaceholder": "Please enter company name",
    "companyNameRequired": "Company name is required",
    "loginTitle": "Login Page Title",
    "loginTitlePlaceholder": "Enter login page title (optional)",
    "loginDescription": "Login Page Description",
    "loginDescriptionPlaceholder": "Enter login page description (optional)",
    "logo": "Company Logo",
    "uploadLogo": "Upload Logo",
    "uploadSuccess": "Logo uploaded successfully",
    "uploadFailed": "Logo upload failed",
    "uploading": "Uploading...",
    "logoEmpty": "No logo set",
    "save": "Save",
    "saveSuccess": "Company info saved successfully",
    "saveFailed": "Failed to save company info"
  }
}
src/packages/locales/src/langs/zh-CN/preferences.json
@@ -229,5 +229,24 @@
        "placeholder": "请输入报表默认标题"
      }
    }
  },
  "company": {
    "title": "公司信息",
    "companyName": "公司名称",
    "companyNamePlaceholder": "请输入公司名称",
    "companyNameRequired": "公司名称不能为空",
    "loginTitle": "登录页标题",
    "loginTitlePlaceholder": "请输入登录页标题(可选)",
    "loginDescription": "登录页描述",
    "loginDescriptionPlaceholder": "请输入登录页描述(可选)",
    "logo": "公司 Logo",
    "uploadLogo": "上传 Logo",
    "uploadSuccess": "Logo ä¸Šä¼ æˆåŠŸ",
    "uploadFailed": "Logo ä¸Šä¼ å¤±è´¥",
    "uploading": "上传中...",
    "logoEmpty": "未设置 Logo",
    "save": "保存",
    "saveSuccess": "公司信息保存成功",
    "saveFailed": "公司信息保存失败"
  }
}
src/store/company-config.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,71 @@
import { ref } from 'vue';
import { defineStore } from 'pinia';
import {
  getCompanyConfig,
  saveCompanyConfig as saveCompanyConfigApi,
} from '#/api/system/company-config';
import { applyCompanyConfig } from '#/utils/company-config';
/**
 * å…¬å¸å“ç‰Œä¿¡æ¯å…¨å±€é…ç½®
 *
 * å¯åŠ¨æ—¶ä»ŽåŽç«¯åŠ è½½ï¼Œä¿å­˜æ—¶å†™å›žåŽç«¯ï¼›å¹¶å°†å…¬å¸å/logo åŒæ­¥åˆ° preferences,
 * ä»Žè€Œè”动登录页、菜单栏、网页标题等所有消费点。
 */
export const useCompanyConfigStore = defineStore('companyConfig', () => {
  const companyName = ref('');
  const logoUrl = ref('');
  const loginTitle = ref('');
  const loginDescription = ref('');
  /** å°†å…¬å¸ä¿¡æ¯åº”用到 preferences,联动登录页/菜单栏/标题 */
  function applyToPreferences() {
    applyCompanyConfig({
      companyName: companyName.value,
      logoUrl: logoUrl.value,
    });
  }
  /** ä»ŽåŽç«¯åŠ è½½å…¬å¸é…ç½®å¹¶åº”ç”¨ */
  async function loadFromServer() {
    try {
      const config = await getCompanyConfig();
      if (config) {
        companyName.value = config.companyName ?? '';
        logoUrl.value = config.logoUrl ?? '';
        loginTitle.value = config.loginTitle ?? '';
        loginDescription.value = config.loginDescription ?? '';
        applyToPreferences();
      }
    } catch {
      // åŽç«¯ä¸å¯ç”¨æˆ–未配置,保持编译期默认
    }
  }
  /** ä¿å­˜å…¬å¸é…ç½®åˆ°åŽç«¯å¹¶åº”用 */
  async function save(data: {
    companyName: string;
    logoUrl?: string;
    logoBlobId?: number;
    loginTitle?: string;
    loginDescription?: string;
  }) {
    await saveCompanyConfigApi(data);
    companyName.value = data.companyName;
    logoUrl.value = data.logoUrl ?? '';
    loginTitle.value = data.loginTitle ?? '';
    loginDescription.value = data.loginDescription ?? '';
    applyToPreferences();
  }
  return {
    companyName,
    logoUrl,
    loginTitle,
    loginDescription,
    applyToPreferences,
    loadFromServer,
    save,
  };
});
src/store/index.ts
@@ -1 +1,2 @@
export * from './auth';
export * from './company-config';
src/utils/company-config.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
import { preferencesManager } from '@vben/preferences';
/** å…¬å¸å“ç‰Œä¿¡æ¯ï¼ˆä¸ŽåŽç«¯ system_company_config è¡¨å­—段对应) */
export interface CompanyConfigData {
  companyName?: string;
  logoUrl?: string;
  loginTitle?: string;
  loginDescription?: string;
}
/**
 * å°†å…¬å¸å“ç‰Œä¿¡æ¯åº”用到全局:
 * - å…¬å¸å â†’ preferences.app.name / copyright.companyName(联动菜单栏、登录页、网页标题、版权)
 * - logo â†’ preferences.logo.source(联动登录页、侧边栏 logo)
 * - æœ‰å…¬å¸åæ—¶ï¼Œè®¾ç½® document.title(基础标题,dynamicTitle å¼€å¯æ—¶è·¯ç”±æ ‡é¢˜åœ¨æ­¤åŸºç¡€ä¸Šæ‹¼æŽ¥ï¼‰
 */
export function applyCompanyConfig(config: CompanyConfigData) {
  const updates: Record<string, any> = {};
  if (config.logoUrl) {
    updates.logo = { source: config.logoUrl };
  }
  if (config.companyName) {
    updates.app = { name: config.companyName };
    updates.copyright = { companyName: config.companyName };
  }
  if (Object.keys(updates).length > 0) {
    preferencesManager.updatePreferences(updates);
  }
  if (config.companyName) {
    document.title = config.companyName;
  }
}
src/views/basicData/mdm/data.ts
@@ -630,3 +630,27 @@
    },
  ];
}
/** ç‰©æ–™å¯¼å…¥çš„表单 */
export function useImportFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'file',
      label: '物料数据',
      component: 'Upload',
      rules: 'required',
      help: '仅允许导入 xls、xlsx æ ¼å¼æ–‡ä»¶',
    },
    {
      fieldName: 'updateSupport',
      label: '是否覆盖',
      component: 'Switch',
      componentProps: {
        checkedChildren: '是',
        unCheckedChildren: '否',
      },
      rules: z.boolean().default(false),
      help: '是否更新已经存在的物料数据',
    },
  ];
}
src/views/basicData/mdm/index.vue
@@ -24,11 +24,17 @@
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
import ImportForm from './modules/import-form.vue';
defineOptions({ name: 'MdmItem' });
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [ImportModal, importModalApi] = useVbenModal({
  connectedComponent: ImportForm,
  destroyOnClose: true,
});
@@ -84,6 +90,11 @@
/** åˆ›å»ºç‰©æ–™ */
function handleCreate() {
  formModalApi.setData(null).open();
}
/** å¯¼å…¥ç‰©æ–™ */
function handleImport() {
  importModalApi.open();
}
/** ç¼–辑物料 */
@@ -211,6 +222,7 @@
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <ImportModal @success="handleRefresh" />
    <Grid table-title="物料列表">
      <template #toolbar-tools>
        <TableAction
@@ -222,6 +234,13 @@
              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',
src/views/basicData/mdm/modules/import-form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,105 @@
<script lang="ts" setup>
import type { FileType } from 'ant-design-vue/es/upload/interface';
import { h } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message, notification, Upload } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { importItem, importItemTemplate } from '#/api/mdm/item';
import { $t } from '#/locales';
import { useImportFormSchema } from '../data';
const emit = defineEmits(['success']);
const [Form, formApi] = useVbenForm({
  commonConfig: {
    formItemClass: 'col-span-2',
    labelWidth: 120,
  },
  layout: 'horizontal',
  schema: useImportFormSchema(),
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    // æäº¤è¡¨å•
    const data = await formApi.getValues();
    try {
      const response = (await importItem(data.file, data.updateSupport)) as any;
      // å…³é—­å¹¶æç¤º
      await modalApi.close();
      emit('success');
      const resultData = response?.data || response || {};
      const failureList = resultData.failureList || {};
      if (Object.keys(failureList).length > 0) {
        const errorNodes = Object.entries(failureList).map(([name, reason]) => {
          return h('div', `${name}: ${reason}`);
        });
        notification.warning({
          message: '导入完成,部分物料导入失败',
          description: () => h('div', errorNodes),
          duration: null,
        });
      } else {
        message.success($t('ui.actionMessage.operationSuccess'));
      }
    } finally {
      modalApi.unlock();
    }
  },
});
/** ä¸Šä¼ å‰ */
function beforeUpload(file: FileType) {
  formApi.setFieldValue('file', file);
  return false;
}
/** ä¸‹è½½æ¨¡ç‰ˆ */
async function handleDownload() {
  const data = await importItemTemplate();
  downloadFileFromBlobPart({ fileName: '物料导入模板.xls', source: data });
}
</script>
<template>
  <Modal title="导入物料" class="w-1/3">
    <Form class="mx-4">
      <template #file>
        <div class="w-full">
          <Upload
            :max-count="1"
            accept=".xls,.xlsx"
            :before-upload="beforeUpload"
          >
            <Button type="primary"> é€‰æ‹© Excel æ–‡ä»¶ </Button>
          </Upload>
        </div>
      </template>
    </Form>
    <div class="mx-4 mt-2 mb-4 text-gray-500 text-xs">
      æç¤ºï¼šæ¨¡æ¿ä¸­â€œç‰©æ–™åˆ†ç±»ç¼–码”、“计量单位编码”、“辅单位编码”请填写对应主数据的编码,可留空。
      <div>“是否批次管理”、“是否同步到MES”填 true/false。</div>
      <div>“状态”填 0(启用)/1(禁用)。</div>
    </div>
    <template #prepend-footer>
      <div class="flex flex-auto items-center">
        <Button @click="handleDownload"> ä¸‹è½½å¯¼å…¥æ¨¡æ¿ </Button>
      </div>
    </template>
  </Modal>
</template>
src/views/basicData/mdm/unit/data.ts
@@ -128,4 +128,28 @@
      slots: { default: 'actions' },
    },
  ];
}
/** è®¡é‡å•位导入的表单 */
export function useImportFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'file',
      label: '单位数据',
      component: 'Upload',
      rules: 'required',
      help: '仅允许导入 xls、xlsx æ ¼å¼æ–‡ä»¶',
    },
    {
      fieldName: 'updateSupport',
      label: '是否覆盖',
      component: 'Switch',
      componentProps: {
        checkedChildren: '是',
        unCheckedChildren: '否',
      },
      rules: z.boolean().default(false),
      help: '是否更新已经存在的单位数据',
    },
  ];
}
src/views/basicData/mdm/unit/index.vue
@@ -15,11 +15,17 @@
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
import ImportForm from './modules/import-form.vue';
defineOptions({ name: 'MdmUnit' });
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [ImportModal, importModalApi] = useVbenModal({
  connectedComponent: ImportForm,
  destroyOnClose: true,
});
@@ -31,6 +37,11 @@
/** åˆ›å»ºè®¡é‡å•位 */
function handleCreate() {
  formModalApi.setData(null).open();
}
/** å¯¼å…¥è®¡é‡å•位 */
function handleImport() {
  importModalApi.open();
}
/** ç¼–辑计量单位 */
@@ -87,6 +98,7 @@
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <ImportModal @success="handleRefresh" />
    <Grid table-title="计量单位列表">
      <template #toolbar-tools>
        <TableAction
@@ -98,6 +110,13 @@
              auth: ['mdm:unit:create'],
              onClick: handleCreate,
            },
            {
              label: $t('ui.actionTitle.import', ['计量单位']),
              type: 'primary',
              icon: ACTION_ICON.UPLOAD,
              auth: ['mdm:unit-measure:import'],
              onClick: handleImport,
            },
          ]"
        />
      </template>
src/views/basicData/mdm/unit/modules/import-form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,105 @@
<script lang="ts" setup>
import type { FileType } from 'ant-design-vue/es/upload/interface';
import { h } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message, notification, Upload } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { importUnit, importUnitTemplate } from '#/api/mdm/unit';
import { $t } from '#/locales';
import { useImportFormSchema } from '../data';
const emit = defineEmits(['success']);
const [Form, formApi] = useVbenForm({
  commonConfig: {
    formItemClass: 'col-span-2',
    labelWidth: 120,
  },
  layout: 'horizontal',
  schema: useImportFormSchema(),
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    // æäº¤è¡¨å•
    const data = await formApi.getValues();
    try {
      const response = (await importUnit(data.file, data.updateSupport)) as any;
      // å…³é—­å¹¶æç¤º
      await modalApi.close();
      emit('success');
      const resultData = response?.data || response || {};
      const failureList = resultData.failureList || {};
      if (Object.keys(failureList).length > 0) {
        const errorNodes = Object.entries(failureList).map(([name, reason]) => {
          return h('div', `${name}: ${reason}`);
        });
        notification.warning({
          message: '导入完成,部分计量单位导入失败',
          description: () => h('div', errorNodes),
          duration: null,
        });
      } else {
        message.success($t('ui.actionMessage.operationSuccess'));
      }
    } finally {
      modalApi.unlock();
    }
  },
});
/** ä¸Šä¼ å‰ */
function beforeUpload(file: FileType) {
  formApi.setFieldValue('file', file);
  return false;
}
/** ä¸‹è½½æ¨¡ç‰ˆ */
async function handleDownload() {
  const data = await importUnitTemplate();
  downloadFileFromBlobPart({ fileName: '计量单位导入模板.xls', source: data });
}
</script>
<template>
  <Modal title="导入计量单位" class="w-1/3">
    <Form class="mx-4">
      <template #file>
        <div class="w-full">
          <Upload
            :max-count="1"
            accept=".xls,.xlsx"
            :before-upload="beforeUpload"
          >
            <Button type="primary"> é€‰æ‹© Excel æ–‡ä»¶ </Button>
          </Upload>
        </div>
      </template>
    </Form>
    <div class="mx-4 mt-2 mb-4 text-gray-500 text-xs">
      æç¤ºï¼šæ¨¡æ¿ä¸­çš„“主单位编码”请填写主单位的编码(如 PC),主单位本身该列留空即可。
      <div>“是否主单位”填 true/false。</div>
      <div>“状态”填 0(启用)/1(禁用)。</div>
    </div>
    <template #prepend-footer>
      <div class="flex flex-auto items-center">
        <Button @click="handleDownload"> ä¸‹è½½å¯¼å…¥æ¨¡æ¿ </Button>
      </div>
    </template>
  </Modal>
</template>
src/views/bpm/task/done/data.ts
@@ -146,7 +146,7 @@
    },
    {
      title: '操作',
      width: 120,
      width: 180,
      fixed: 'right',
      slots: { default: 'actions' },
    },
src/views/bpm/task/done/index.vue
@@ -7,6 +7,7 @@
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { reverseApprove } from '#/api/erp/purchase/order';
import { getTaskDonePage, withdrawTask } from '#/api/bpm/task';
import { router } from '#/router';
@@ -34,6 +35,28 @@
  try {
    await withdrawTask(row.id);
    message.success('撤回成功');
    await gridApi.query();
  } finally {
    hideLoading();
  }
}
/** æ˜¯å¦é‡‡è´­è®¢å•审批任务(用于显示"反审批"、隐藏通用"撤回") */
function isPurchaseOrderApprove(row: BpmTaskApi.Task) {
  return (row.processInstance?.processDefinitionId || '').startsWith(
    'purchase_order_approve',
  );
}
/** åå®¡æ‰¹é‡‡è´­è®¢å•(已审核 -> æœªå®¡æ ¸ï¼Œåœ¨ååŒåŠžå…¬å·²åŠžä»»åŠ¡ä¸­æ“ä½œï¼‰ */
async function handleReverseApprove(row: BpmTaskApi.Task) {
  const hideLoading = message.loading({
    content: '反审批中...',
    duration: 0,
  });
  try {
    await reverseApprove(row.processInstance.id);
    message.success('反审批成功,订单已重新进入待办审批');
    await gridApi.query();
  } finally {
    hideLoading();
@@ -77,10 +100,22 @@
        <TableAction
          :actions="[
            {
              label: '反审批',
              type: 'link',
              danger: true,
              icon: ACTION_ICON.AUDIT,
              ifShow: () => isPurchaseOrderApprove(row),
              popConfirm: {
                title: '确定要反审批该采购订单吗?订单将重新进入待办审批',
                confirm: handleReverseApprove.bind(null, row),
              },
            },
            {
              label: '撤回',
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              ifShow: () => !isPurchaseOrderApprove(row),
              popConfirm: {
                title: '确定要撤回该任务吗?',
                confirm: handleWithdraw.bind(null, row),
src/views/erp/finance/payment/data.ts
@@ -152,18 +152,14 @@
      formItemClass: 'col-span-3',
    },
    {
      fieldName: 'accountId',
      fieldName: 'accountName',
      label: '付款账户',
      component: 'ApiSelect',
      component: 'Input',
      rules: 'required',
      componentProps: {
        placeholder: '请选择付款账户',
        placeholder: '请输入付款账户',
        allowClear: true,
        showSearch: true,
        api: getAccountSimpleList,
        labelField: 'name',
        valueField: 'id',
      },
      help: '付款账户数据来自【财务管理 â†’ ç»“算账户】菜单维护的开启状态账户,无选项时请先在结算账户页面新增。',
    },
    {
      fieldName: 'totalPrice',
src/views/erp/finance/payment/modules/form.vue
@@ -29,7 +29,7 @@
  id: undefined,
  no: '',
  supplierId: undefined,
  accountId: undefined,
  accountName: undefined,
  financeUserId: undefined,
  paymentTime: undefined,
  remark: '',
@@ -176,11 +176,11 @@
    formApi.setDisabled(formType.value === 'detail');
    formApi.updateSchema(useFormSchema(formType.value));
    if (!data || !data.id) {
      // æ–°å¢žæ—¶ï¼Œé»˜è®¤é€‰ä¸­è´¦æˆ·
      // æ–°å¢žæ—¶ï¼Œé»˜è®¤å¡«å…¥é»˜è®¤ç»“算账户名称
      const accountList = await getAccountSimpleList();
      const defaultAccount = accountList.find((item) => item.defaultStatus);
      if (defaultAccount) {
        await formApi.setValues({ accountId: defaultAccount.id });
        await formApi.setValues({ accountName: defaultAccount.name });
      }
      return;
    }
src/views/erp/purchase/invoice/data.ts
@@ -1,7 +1,10 @@
import type { Ref } from 'vue';
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { getSupplierSimpleList } from '#/api/srm/supplier';
import { z } from '#/adapter/form';
import { markRaw } from 'vue';
@@ -11,7 +14,9 @@
export type FormType = 'create' | 'detail' | 'edit';
/** æ–°å¢ž/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
export function useFormSchema(
  orderTotalTaxPrice?: Ref<number | undefined>,
): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
@@ -41,8 +46,15 @@
      },
      componentProps: (_values, form) => ({
        placeholder: '请选择采购订单',
        onChange: (order: { supplierName?: string } | undefined) => {
        onChange: (
          order:
            | { supplierName?: string; totalTaxPrice?: number }
            | undefined,
        ) => {
          form.setFieldValue('supplierName', order?.supplierName);
          if (orderTotalTaxPrice) {
            orderTotalTaxPrice.value = order?.totalTaxPrice;
          }
        },
      }),
    },
@@ -78,11 +90,37 @@
      label: '来票金额(元)',
      component: 'InputNumber',
      rules: 'required',
      description: () =>
        orderTotalTaxPrice?.value != null
          ? `采购订单未开票金额:${Number(orderTotalTaxPrice.value).toFixed(2)} å…ƒ`
          : '',
      componentProps: {
        class: '!w-full',
        placeholder: '请输入来票金额',
        min: 0,
        precision: 2,
      },
      dependencies: {
        triggerFields: ['purchaseOrderId'],
        componentProps: () => ({
          class: '!w-full',
          placeholder: '请输入来票金额',
          min: 0,
          max: orderTotalTaxPrice?.value ?? undefined,
          precision: 2,
        }),
        rules: () => {
          const max = orderTotalTaxPrice?.value;
          if (max != null) {
            return z
              .number({ message: '请输入来票金额' })
              .min(0, '来票金额不能小于 0')
              .max(max, `来票金额不能大于未开票金额 ${Number(max).toFixed(2)}`);
          }
          return z
            .number({ message: '请输入来票金额' })
            .min(0, '来票金额不能小于 0');
        },
      },
    },
    {
@@ -180,12 +218,6 @@
    {
      title: '来票金额(元)',
      field: 'price',
      minWidth: 150,
      formatter: 'formatAmount2',
    },
    {
      title: '剩余可付金额',
      field: 'remainingInvoicePrice',
      minWidth: 150,
      formatter: 'formatAmount2',
    },
src/views/erp/purchase/invoice/modules/form.vue
@@ -22,6 +22,7 @@
const emit = defineEmits(['success']);
const formData = ref<ErpPurchaseInvoiceApi.PurchaseInvoice>();
const formType = ref<FormType>('create');
const orderTotalTaxPrice = ref<number>(); // æ‰€é€‰é‡‡è´­è®¢å•合计税额(未开票金额,用于提示与校验)
const getTitle = computed(() => {
  if (formType.value === 'create') {
@@ -42,7 +43,7 @@
  },
  wrapperClass: 'grid-cols-2',
  layout: 'horizontal',
  schema: useFormSchema(),
  schema: useFormSchema(orderTotalTaxPrice),
  showDefaultActions: false,
});
@@ -71,12 +72,13 @@
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      orderTotalTaxPrice.value = undefined;
      return;
    }
    const data = modalApi.getData<{ formType: FormType; id?: number }>();
    formType.value = data.formType;
    formApi.setDisabled(formType.value === 'detail');
    formApi.updateSchema(useFormSchema());
    formApi.updateSchema(useFormSchema(orderTotalTaxPrice));
    await formApi.resetForm();
    if (!data || !data.id) {
      return;
src/views/erp/purchase/invoice/modules/purchase-order-select.vue
@@ -92,6 +92,12 @@
        minWidth: 120,
      },
      {
        field: 'totalTaxPrice',
        title: '未开票金额',
        formatter: 'formatAmount2',
        minWidth: 120,
      },
      {
        field: 'status',
        title: '状态',
        minWidth: 100,
src/views/erp/purchase/order/index.vue
@@ -16,7 +16,6 @@
  deletePurchaseOrder,
  exportPurchaseOrder,
  getPurchaseOrderPage,
  updatePurchaseOrderStatus,
} from '#/api/erp/purchase/order';
import { $t } from '#/locales';
@@ -63,24 +62,6 @@
  try {
    await deletePurchaseOrder(ids);
    message.success($t('ui.actionMessage.deleteSuccess'));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** å®¡æ‰¹/反审批操作 */
async function handleUpdateStatus(
  row: ErpPurchaseOrderApi.PurchaseOrder,
  status: number,
) {
  const hideLoading = message.loading({
    content: `确定${status === 20 ? '审批' : '反审批'}该订单吗?`,
    duration: 0,
  });
  try {
    await updatePurchaseOrderStatus(row.id!, status);
    message.success(`${status === 20 ? '审批' : '反审批'}成功`);
    handleRefresh();
  } finally {
    hideLoading();
@@ -159,7 +140,8 @@
</script>
<template>
  <Page auto-content-height><FormModal @success="handleRefresh" />
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="采购订单列表">
      <template #toolbar-tools>
        <TableAction
@@ -215,20 +197,6 @@
              auth: ['erp:purchase-order:update'],
              ifShow: () => row.status !== 20,
              onClick: handleEdit.bind(null, row),
            },
            {
              label: row.status === 10 ? '审批' : '反审批',
              type: 'link',
              icon: ACTION_ICON.AUDIT,
              auth: ['erp:purchase-order:update-status'],
              popConfirm: {
                title: `确认${row.status === 10 ? '审批' : '反审批'}${row.no}吗?`,
                confirm: handleUpdateStatus.bind(
                  null,
                  row,
                  row.status === 10 ? 20 : 10,
                ),
              },
            },
            {
              label: '确认收货',
src/views/hrm/attendance/record/data.ts
@@ -75,6 +75,7 @@
  return [
    { field: 'userName', title: '员工姓名', minWidth: 100 },
    { field: 'deptName', title: '部门', minWidth: 120 },
    { field: 'phone', title: '手机号', width: 130 },
    { field: 'date', title: '考勤日期', width: 110 },
    { field: 'clockInTime', title: '上班打卡时间', width: 160 },
    {
src/views/hrm/attendance/record/modules/import-form.vue
@@ -23,10 +23,18 @@
    }
    modalApi.lock();
    try {
      await importAttendanceRecord(fileList.value[0] as File, updateSupport.value);
      const res: string = await importAttendanceRecord(
        fileList.value[0] as File,
        updateSupport.value,
      );
      await modalApi.close();
      emit('success');
      message.success('导入成功');
      // åŽç«¯è¿”回导入结果(含失败原因,例如“以下员工不存在...;需补充手机号...”)
      if (res && res.length > 0) {
        message[res.includes('未') || res.includes('不完整') ? 'warning' : 'success'](res);
      } else {
        message.success('导入成功');
      }
    } finally {
      modalApi.unlock();
    }
src/views/hrm/employee/data.ts
@@ -10,6 +10,7 @@
import { Button } from 'ant-design-vue';
import { z } from '#/adapter/form';
import { getSalaryStructureList } from '#/api/hrm/salary/structure';
import { getDeptList } from '#/api/system/dept';
import { getSimplePostList } from '#/api/system/post';
import { getSimpleRoleList } from '#/api/system/role';
@@ -242,6 +243,19 @@
      },
    },
    {
      fieldName: 'salaryStructureId',
      label: '薪酬结构',
      component: 'ApiSelect',
      componentProps: {
        api: () => getSalaryStructureList({ status: 0 }),
        labelField: 'name',
        valueField: 'id',
        allowClear: true,
        placeholder: '请选择薪酬结构',
      },
      help: '决定工作日加班倍率;未选择时使用系统默认 1.5 å€',
    },
    {
      fieldName: 'baseSalary',
      label: '基本工资',
      component: 'InputNumber',
src/views/hrm/overtime/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,320 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmOvertimeApi } from '#/api/hrm/overtime';
import { DICT_TYPE } from '#/packages/constants/src';
import { getDictOptions } from '#/packages/effects/hooks/src';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'update';
export const OVERTIME_TYPE_OPTIONS = [
  { label: '工作日加班', value: 1 },
  { label: '休息日加班', value: 2 },
  { label: '法定节假日加班', value: 3 },
];
export const RELATED_TYPE_OPTIONS = [
  { label: '项目', value: 1 },
  { label: '工单', value: 2 },
];
export function getOvertimeTypeLabel(type?: number) {
  return OVERTIME_TYPE_OPTIONS.find((item) => item.value === type)?.label || '-';
}
/** æ–°å¢ž/修改加班申请的表单 */
export function useFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: { triggerFields: [''], show: () => false },
    },
    {
      fieldName: 'employeeId',
      component: 'Input',
      dependencies: { triggerFields: [''], show: () => false },
    },
    {
      fieldName: 'baseInfoDivider',
      label: '申请人基础信息',
      component: 'Divider',
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'employeeName',
      label: '申请人',
      component: 'Input',
      componentProps: { disabled: true, placeholder: '系统自动带出' },
    },
    {
      fieldName: 'employeeNo',
      label: '工号',
      component: 'Input',
      componentProps: { disabled: true, placeholder: '系统自动带出' },
    },
    {
      fieldName: 'deptName',
      label: '所属部门',
      component: 'Input',
      componentProps: { disabled: true, placeholder: '系统自动带出' },
    },
    {
      fieldName: 'postName',
      label: '岗位/职级',
      component: 'Input',
      componentProps: { disabled: true, placeholder: '系统自动带出' },
    },
    {
      fieldName: 'overtimeInfoDivider',
      label: '加班核心信息',
      component: 'Divider',
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'overtimeDate',
      label: '加班日期',
      component: 'DatePicker',
      componentProps: {
        placeholder: '请选择加班日期',
        valueFormat: 'YYYY-MM-DD',
        style: { width: '100%' },
      },
      rules: 'required',
    },
    {
      fieldName: 'overtimeType',
      label: '加班类型',
      component: 'Select',
      componentProps: {
        options: OVERTIME_TYPE_OPTIONS,
        placeholder: '请选择加班类型',
      },
      help: '不同类型对应不同的薪资计算规则',
      rules: 'selectRequired',
    },
    {
      fieldName: 'startTime',
      label: '开始时间',
      component: 'TimePicker',
      componentProps: {
        format: 'HH:mm',
        minuteStep: 30,
        placeholder: '请选择开始时间',
        valueFormat: 'HH:mm:ss',
        style: { width: '100%' },
      },
      rules: 'required',
    },
    {
      fieldName: 'endTime',
      label: '结束时间',
      component: 'TimePicker',
      componentProps: {
        format: 'HH:mm',
        minuteStep: 30,
        placeholder: '请选择结束时间',
        valueFormat: 'HH:mm:ss',
        style: { width: '100%' },
      },
      rules: 'required',
    },
    {
      fieldName: 'crossDay',
      label: '是否跨天',
      component: 'Switch',
      componentProps: {
        checkedChildren: '是',
        disabled: true,
        unCheckedChildren: '否',
      },
      defaultValue: false,
      help: '结束时间早于或等于开始时间时,系统自动标记为跨天',
    },
    {
      fieldName: 'mealDeduction',
      label: '扣除用餐时间',
      component: 'Switch',
      componentProps: {
        checkedChildren: '扣除',
        unCheckedChildren: '不扣除',
      },
      defaultValue: false,
    },
    {
      fieldName: 'mealDeductionHours',
      label: '用餐扣除时长',
      component: 'InputNumber',
      componentProps: {
        min: 0.5,
        max: 4,
        precision: 1,
        step: 0.5,
        style: { width: '100%' },
      },
      defaultValue: 1,
      suffix: '小时',
      dependencies: {
        triggerFields: ['mealDeduction'],
        show: (values) => Boolean(values.mealDeduction),
      },
    },
    {
      fieldName: 'duration',
      label: '加班时长',
      component: 'InputNumber',
      componentProps: {
        disabled: true,
        min: 0,
        precision: 1,
        step: 0.5,
        style: { width: '100%' },
      },
      help: '按实际时段扣除用餐时间后,自动取整到 0.5 å°æ—¶',
      suffix: '小时',
      rules: 'required',
    },
    {
      fieldName: 'reasonInfoDivider',
      label: '加班事由',
      component: 'Divider',
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'reason',
      label: '加班原因',
      component: 'Textarea',
      formItemClass: 'col-span-2',
      componentProps: {
        maxlength: 500,
        placeholder: '请说明加班的必要性和原因',
        rows: 3,
        showCount: true,
      },
      rules: 'required',
    },
    {
      fieldName: 'relatedType',
      label: '关联类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: RELATED_TYPE_OPTIONS,
        placeholder: '可选:项目或工单',
      },
    },
    {
      fieldName: 'relatedCode',
      label: '项目/工单编号',
      component: 'Input',
      componentProps: {
        allowClear: true,
        maxlength: 100,
        placeholder: '请输入关联项目或工单编号',
      },
      dependencies: {
        triggerFields: ['relatedType'],
        componentProps: (values) => ({
          allowClear: true,
          maxlength: 100,
          placeholder: values.relatedType === 1
            ? '请输入项目编号'
            : values.relatedType === 2
              ? '请输入工单编号'
              : '请先选择关联类型',
          disabled: !values.relatedType,
        }),
      },
    },
    {
      fieldName: 'workContent',
      label: '工作内容简述',
      component: 'Textarea',
      formItemClass: 'col-span-2',
      componentProps: {
        maxlength: 1000,
        placeholder: '请简要描述加班期间计划完成的具体工作',
        rows: 4,
        showCount: true,
      },
    },
  ];
}
/** åˆ—表搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'overtimeDate',
      label: '加班日期',
      component: 'DatePicker',
      componentProps: {
        allowClear: true,
        placeholder: '请选择加班日期',
        valueFormat: 'YYYY-MM-DD',
      },
    },
    {
      fieldName: 'overtimeType',
      label: '加班类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: OVERTIME_TYPE_OPTIONS,
        placeholder: '请选择加班类型',
      },
    },
    {
      fieldName: 'status',
      label: '审批状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.HRM_AUDIT_STATUS, 'number'),
        placeholder: '请选择审批状态',
      },
    },
  ];
}
/** åˆ—表字段 */
export function useGridColumns(): VxeTableGridOptions<HrmOvertimeApi.Overtime>['columns'] {
  return [
    { field: 'id', title: '申请编号', width: 90 },
    { field: 'employeeName', title: '申请人', minWidth: 100 },
    { field: 'employeeNo', title: '工号', minWidth: 110 },
    { field: 'deptName', title: '所属部门', minWidth: 120 },
    { field: 'overtimeDate', title: '加班日期', width: 120 },
    {
      field: 'overtimeType',
      title: '加班类型',
      width: 130,
      slots: { default: 'overtimeType' },
    },
    { field: 'startTime', title: '开始时间', width: 100 },
    { field: 'endTime', title: '结束时间', width: 100 },
    { field: 'duration', title: '时长(小时)', width: 100 },
    {
      field: 'status',
      title: '审批状态',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.HRM_AUDIT_STATUS },
      },
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 200,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/hrm/overtime/detail/approve/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,83 @@
<script lang="ts" setup>
import type { HrmOvertimeApi } from '#/api/hrm/overtime';
import type { DescriptionItemSchema } from '#/components/description';
import { h, onMounted, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { formatDateTime } from '@vben/utils';
import { Spin, Tag } from 'ant-design-vue';
import { getOvertime } from '#/api/hrm/overtime';
import { DictTag } from '#/components/dict-tag';
import { useDescription } from '#/components/description';
import { getOvertimeTypeLabel } from '#/views/hrm/overtime/data';
defineOptions({ name: 'HrmOvertimeApproveDetail' });
const props = defineProps<{ id: string }>();
const loading = ref(false);
const formData = ref<HrmOvertimeApi.Overtime>();
function textRender(value: unknown) {
  return value === 0 || value ? String(value) : '-';
}
const [Descriptions] = useDescription({
  bordered: true,
  column: 2,
  schema: [
    { label: '申请人', field: 'employeeName', render: textRender },
    { label: '工号', field: 'employeeNo', render: textRender },
    { label: '所属部门', field: 'deptName', render: textRender },
    { label: '岗位/职级', field: 'postName', render: textRender },
    { label: '加班日期', field: 'overtimeDate', render: textRender },
    {
      label: '加班类型',
      field: 'overtimeType',
      render: (value) => h(Tag, { color: value === 3 ? 'red' : value === 2 ? 'orange' : 'blue' }, () => getOvertimeTypeLabel(Number(value))),
    },
    { label: '开始时间', field: 'startTime', render: textRender },
    { label: '结束时间', field: 'endTime', render: textRender },
    { label: '加班时长', field: 'duration', render: (value) => `${textRender(value)} å°æ—¶` },
    { label: '是否跨天', field: 'crossDay', render: (value) => value ? '是' : '否' },
    { label: '用餐扣除', field: 'mealDeduction', render: (value) => value ? '是' : '否' },
    { label: '扣除时长', field: 'mealDeductionHours', render: (value) => value ? `${value} å°æ—¶` : '-' },
    { label: '加班原因', field: 'reason', render: textRender },
    { label: '关联类型', field: 'relatedType', render: (value) => value === 1 ? '项目' : value === 2 ? '工单' : '-' },
    { label: '项目/工单编号', field: 'relatedCode', render: textRender },
    { label: '工作内容简述', field: 'workContent', render: textRender },
    {
      label: '审批状态',
      field: 'status',
      render: (value) => h(DictTag, { type: DICT_TYPE.HRM_AUDIT_STATUS, value }),
    },
    { label: '创建时间', field: 'createTime', render: (value) => formatDateTime(value) as string },
  ] as DescriptionItemSchema[],
});
onMounted(async () => {
  loading.value = true;
  try {
    formData.value = await getOvertime(Number(props.id));
  } finally {
    loading.value = false;
  }
});
</script>
<template>
  <ContentWrap class="m-2">
    <Spin :spinning="loading" tip="加载中...">
      <div class="rounded-lg border border-border bg-background p-4">
        <div class="mb-4 flex items-center gap-2">
          <span class="inline-block h-4 w-1 rounded bg-primary" />
          <span class="text-base font-semibold">加班申请信息</span>
        </div>
        <Descriptions :data="formData" />
      </div>
    </Spin>
  </ContentWrap>
</template>
src/views/hrm/overtime/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,200 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmOvertimeApi } from '#/api/hrm/overtime';
import { ref } from 'vue';
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
import { message, Modal, Select, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  cancelOvertime,
  deleteOvertime,
  getOvertimeApproveProcessDefinitionList,
  getOvertimePage,
  submitOvertime,
} from '#/api/hrm/overtime';
import { $t } from '#/locales';
import {
  getOvertimeTypeLabel,
  useGridColumns,
  useGridFormSchema,
} from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
function handleRefresh() {
  gridApi.query();
}
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
function handleDetail(row: HrmOvertimeApi.Overtime) {
  formModalApi.setData({ id: row.id, formType: 'detail' }).open();
}
function handleEdit(row: HrmOvertimeApi.Overtime) {
  formModalApi.setData({ id: row.id, formType: 'update' }).open();
}
async function handleDelete(row: HrmOvertimeApi.Overtime) {
  const hideLoading = message.loading({ content: '正在删除加班申请...', duration: 0 });
  try {
    await deleteOvertime(row.id!);
    message.success('加班申请删除成功');
    handleRefresh();
  } finally {
    hideLoading();
  }
}
const submitModalVisible = ref(false);
const currentRow = ref<HrmOvertimeApi.Overtime>();
const processList = ref<HrmOvertimeApi.ApproveProcess[]>([]);
const selectedProcessKey = ref<string>();
const submitLoading = ref(false);
async function handleSubmit(row: HrmOvertimeApi.Overtime) {
  processList.value = await getOvertimeApproveProcessDefinitionList();
  currentRow.value = row;
  selectedProcessKey.value = undefined;
  submitModalVisible.value = true;
}
async function confirmSubmit() {
  if (!selectedProcessKey.value) {
    message.warning('请选择协同办公审批流程');
    return;
  }
  submitLoading.value = true;
  try {
    await submitOvertime(currentRow.value!.id!, selectedProcessKey.value);
    message.success('已提交至协同办公审批');
    submitModalVisible.value = false;
    handleRefresh();
  } finally {
    submitLoading.value = false;
  }
}
async function handleCancel(row: HrmOvertimeApi.Overtime) {
  await cancelOvertime(row.id!);
  message.success('撤销成功');
  handleRefresh();
}
function getOvertimeTypeColor(type?: number) {
  return type === 1 ? 'blue' : type === 2 ? 'orange' : type === 3 ? 'red' : 'default';
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: { schema: useGridFormSchema() },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) =>
          await getOvertimePage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          }),
      },
    },
    rowConfig: { keyField: 'id', isHover: true },
    toolbarConfig: { refresh: true, search: true },
  } as VxeTableGridOptions<HrmOvertimeApi.Overtime>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="加班申请列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['加班申请']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['hrm:overtime:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #overtimeType="{ row }">
        <Tag :color="getOvertimeTypeColor(row.overtimeType)">
          {{ getOvertimeTypeLabel(row.overtimeType) }}
        </Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            { label: '查看', type: 'link', onClick: handleDetail.bind(null, row) },
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['hrm:overtime:update'],
              ifShow: row.status === 0,
              onClick: handleEdit.bind(null, row),
            },
            {
              label: '提交',
              type: 'link',
              auth: ['hrm:overtime:submit'],
              ifShow: row.status === 0,
              onClick: handleSubmit.bind(null, row),
            },
            {
              label: '撤销',
              type: 'link',
              auth: ['hrm:overtime:cancel'],
              ifShow: row.status === 10,
              onClick: handleCancel.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['hrm:overtime:delete'],
              ifShow: row.status === 0 || row.status === 40,
              popConfirm: {
                title: '确认删除该加班申请吗?',
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
    <Modal
      v-model:open="submitModalVisible"
      title="提交至协同办公"
      width="420px"
      :confirm-loading="submitLoading"
      @ok="confirmSubmit"
    >
      <Select
        v-model:value="selectedProcessKey"
        class="w-full"
        placeholder="请选择加班审批流程"
        :options="processList.map((item) => ({ label: item.name, value: item.key }))"
      />
    </Modal>
  </Page>
</template>
src/views/hrm/overtime/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,159 @@
<script lang="ts" setup>
import type { HrmOvertimeApi } from '#/api/hrm/overtime';
import type { FormType } from '../data';
import { computed, ref } from 'vue';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
  createOvertime,
  getCurrentOvertimeApplicant,
  getOvertime,
  updateOvertime,
} from '#/api/hrm/overtime';
import { $t } from '#/locales';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const isDetail = computed(() => formType.value === 'detail');
const getTitle = computed(() => {
  if (isDetail.value) return '查看加班申请';
  return formType.value === 'update' ? '修改加班申请' : '新增加班申请';
});
/** æ ¹æ®æ—¶æ®µã€è·¨å¤©æ ‡è¯†å’Œç”¨é¤æ‰£é™¤è®¡ç®—加班时长,并按 0.5 å°æ—¶å–æ•´ */
function calculateDuration(values: Record<string, any>) {
  const { startTime, endTime } = values;
  if (!startTime || !endTime) return { crossDay: false, duration: 0 };
  const toMinutes = (time: string) => {
    const [hour = 0, minute = 0] = time.split(':').map(Number);
    return hour * 60 + minute;
  };
  const startMinutes = toMinutes(startTime);
  let endMinutes = toMinutes(endTime);
  const crossDay = endMinutes <= startMinutes;
  if (crossDay) endMinutes += 24 * 60;
  const deductionMinutes = values.mealDeduction
    ? Number(values.mealDeductionHours || 0) * 60
    : 0;
  const actualHours = Math.max(0, (endMinutes - startMinutes - deductionMinutes) / 60);
  return {
    crossDay,
    duration: Math.round(actualHours * 2) / 2,
  };
}
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: { class: 'w-full' },
    formItemClass: 'col-span-1',
    labelWidth: 120,
  },
  wrapperClass: 'grid-cols-2',
  layout: 'horizontal',
  schema: [],
  scrollToFirstError: true,
  showDefaultActions: false,
  handleValuesChange: (values, changedFields) => {
    const durationFields = [
      'startTime',
      'endTime',
      'mealDeduction',
      'mealDeductionHours',
    ];
    if (changedFields.some((field) => durationFields.includes(field))) {
      const result = calculateDuration(values);
      void formApi.setValues(result);
    }
    if (changedFields.includes('relatedType') && !values.relatedType) {
      void formApi.setFieldValue('relatedCode', undefined);
    }
  },
});
async function setCurrentEmployee() {
  const employee = await getCurrentOvertimeApplicant();
  await formApi.setValues({
    employeeId: employee.employeeId,
    employeeName: employee.employeeName,
    employeeNo: employee.employeeNo,
    deptName: employee.deptName,
    postName: employee.postName,
    mealDeduction: false,
    mealDeductionHours: 1,
    crossDay: false,
    duration: 0,
  });
}
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    if (isDetail.value) {
      await modalApi.close();
      return;
    }
    const { valid } = await formApi.validate();
    if (!valid) return;
    const data = (await formApi.getValues()) as HrmOvertimeApi.Overtime;
    if (!data.employeeId) {
      message.warning('当前账号未关联员工档案,暂时无法提交加班申请');
      return;
    }
    if (!data.duration || data.duration <= 0) {
      message.warning('加班时长必须大于 0 å°æ—¶ï¼Œè¯·æ£€æŸ¥æ—¶æ®µæˆ–用餐扣除');
      return;
    }
    if (data.relatedType && !data.relatedCode?.trim()) {
      message.warning('请选择关联类型后填写对应的项目或工单编号');
      return;
    }
    modalApi.lock();
    try {
      if (data.id) await updateOvertime(data);
      else await createOvertime(data);
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) return;
    const data = modalApi.getData<{ formType: FormType; id?: number }>();
    formType.value = data.formType;
    formApi.setState({ schema: useFormSchema() });
    formApi.setDisabled(isDetail.value);
    modalApi.setState({ showConfirmButton: !isDetail.value });
    if (!data.id) {
      await setCurrentEmployee();
      return;
    }
    modalApi.lock();
    try {
      await formApi.setValues(await getOvertime(data.id));
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-[900px]">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/hrm/salary/leave-type-config/data.ts
@@ -3,7 +3,7 @@
import type { HrmLeaveTypeConfigApi } from '#/api/hrm/leave-type-config';
/** è¡¨å•类型 */
export type FormType = 'update';
export type FormType = 'create' | 'update';
/** æ–°å¢ž/修改请假扣款配置的表单 */
export function useFormSchema(): VbenFormSchema[] {
@@ -17,13 +17,29 @@
      },
    },
    {
      fieldName: 'name',
      fieldName: 'leaveType',
      label: '请假类型',
      component: 'Input',
      component: 'Select',
      componentProps: {
        disabled: true,
        placeholder: '',
        options: [
          { label: '事假', value: 1 },
          { label: '病假', value: 2 },
          { label: '年假', value: 3 },
          { label: '婚假', value: 4 },
          { label: '产假', value: 5 },
          { label: '丧假', value: 6 },
        ],
        placeholder: '请选择请假类型',
        style: { width: '100%' },
      },
      dependencies: {
        triggerFields: ['id'],
        // ç¼–辑时不允许修改请假类型
        componentProps: (values: Record<string, unknown>) => ({
          disabled: Boolean(values.id),
        }),
      },
      rules: 'required',
      formItemClass: 'col-span-2',
    },
    {
@@ -47,6 +63,7 @@
      fieldName: 'status',
      label: '状态',
      component: 'RadioGroup',
      defaultValue: 0,
      componentProps: {
        options: [
          { label: '启用', value: 0 },
src/views/hrm/salary/leave-type-config/index.vue
@@ -7,7 +7,10 @@
import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getLeaveTypeConfigList } from '#/api/hrm/leave-type-config';
import {
  deleteLeaveTypeConfig,
  getLeaveTypeConfigList,
} from '#/api/hrm/leave-type-config';
import { $t } from '#/locales';
import { useGridColumns } from './data';
@@ -23,9 +26,21 @@
  gridApi.query();
}
/** æ–°å¢žé…ç½® */
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
/** ç¼–辑配置 */
function handleEdit(row: HrmLeaveTypeConfigApi.LeaveTypeConfig) {
  formModalApi.setData({ id: row.id, row }).open();
  formModalApi.setData({ formType: 'update', row }).open();
}
/** åˆ é™¤é…ç½® */
async function handleDelete(row: HrmLeaveTypeConfigApi.LeaveTypeConfig) {
  await deleteLeaveTypeConfig(row.id!);
  message.success($t('ui.actionMessage.deleteSuccess', ['请假扣款配置']));
  handleRefresh();
}
const [Grid, gridApi] = useVbenVxeGrid({
@@ -56,6 +71,19 @@
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="请假扣款配置列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['请假扣款配置']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['hrm:leave-type-config:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #deductionRatio="{ row }">
        <Tag :color="row.deductionRatio === 0 ? 'success' : row.deductionRatio === 1 ? 'error' : 'warning'">
          {{ row.deductionRatio === 0 ? '不扣款' : `${(row.deductionRatio * 100).toFixed(0)}%` }}
@@ -76,6 +104,17 @@
              auth: ['hrm:leave-type-config:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['hrm:leave-type-config:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', ['请假扣款配置']),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
src/views/hrm/salary/leave-type-config/modules/form.vue
@@ -1,19 +1,28 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { HrmLeaveTypeConfigApi } from '#/api/hrm/leave-type-config';
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { updateLeaveTypeConfig } from '#/api/hrm/leave-type-config';
import {
  createLeaveTypeConfig,
  updateLeaveTypeConfig,
} from '#/api/hrm/leave-type-config';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const getTitle = computed(() =>
  formType.value === 'update' ? '编辑扣款比例' : '新增请假扣款配置',
);
const [Form, formApi] = useVbenForm({
  commonConfig: {
@@ -36,11 +45,35 @@
      return;
    }
    modalApi.lock();
    const data = await formApi.getValues();
    const data = (await formApi.getValues()) as HrmLeaveTypeConfigApi.LeaveTypeConfig;
    // è½¬æ¢æ¯”例为小数
    data.deductionRatio = data.deductionRatio / 100;
    const deductionRatio = data.deductionRatio! / 100;
    try {
      await updateLeaveTypeConfig(data);
      if (formType.value === 'update') {
        await updateLeaveTypeConfig({
          id: data.id,
          deductionRatio,
          remark: data.remark,
          status: data.status,
        });
      } else {
        // è¯·å‡ç±»åž‹åç§°ç”±æ‰€é€‰ç±»åž‹è‡ªåŠ¨å¸¦å‡ºï¼Œæ— éœ€æ‰‹åŠ¨å¡«å†™
        const NAME_MAP: Record<number, string> = {
          1: '事假',
          2: '病假',
          3: '年假',
          4: '婚假',
          5: '产假',
          6: '丧假',
        };
        await createLeaveTypeConfig({
          leaveType: data.leaveType,
          name: NAME_MAP[data.leaveType!] ?? '',
          deductionRatio,
          remark: data.remark,
          status: data.status,
        });
      }
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
@@ -52,16 +85,24 @@
    if (!isOpen) {
      return;
    }
    const data = modalApi.getData<{ id: number; row: HrmLeaveTypeConfigApi.LeaveTypeConfig }>();
    const data = modalApi.getData<{ formType: FormType; row: HrmLeaveTypeConfigApi.LeaveTypeConfig }>();
    formType.value = data.formType;
    if (data.formType === 'create' || !data.row) {
      await formApi.resetForm();
      return;
    }
    // è½¬æ¢æ¯”例为百分比显示
    const row = { ...data.row, deductionRatio: (data.row.deductionRatio || 0) * 100 };
    const row = {
      ...data.row,
      deductionRatio: (data.row.deductionRatio || 0) * 100,
    };
    await formApi.setValues(row);
  },
});
</script>
<template>
  <Modal title="编辑扣款比例" class="w-[600px]">
  <Modal :title="getTitle" class="w-[600px]">
    <Form class="mx-4" />
  </Modal>
</template>
</template>
src/views/hrm/salary/structure/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,163 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmSalaryStructureApi } from '#/api/hrm/salary/structure';
export type FormType = 'create' | 'update';
const statusOptions = [
  { label: '启用', value: 0 },
  { label: '禁用', value: 1 },
];
export function useFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: { triggerFields: [''], show: () => false },
    },
    {
      fieldName: 'name',
      label: '结构名称',
      component: 'Input',
      componentProps: {
        maxlength: 100,
        placeholder: '例如:标准员工薪酬、管理层薪酬',
        showCount: true,
      },
      rules: 'required',
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'overtimePayRatio',
      label: '工作日倍率',
      component: 'InputNumber',
      componentProps: {
        addonAfter: '倍',
        max: 10,
        min: 0,
        precision: 2,
        step: 0.1,
        placeholder: '0 è¡¨ç¤ºä¸è®¡åŠ ç­å·¥èµ„',
        style: { width: '100%' },
      },
      rules: 'required',
    },
    {
      fieldName: 'restDayOvertimePayRatio',
      label: '休息日倍率',
      component: 'InputNumber',
      componentProps: {
        addonAfter: '倍',
        max: 10,
        min: 0,
        precision: 2,
        step: 0.1,
        placeholder: '0 è¡¨ç¤ºä¸è®¡è¯¥ç±»åž‹åŠ ç­å·¥èµ„',
        style: { width: '100%' },
      },
      rules: 'required',
    },
    {
      fieldName: 'legalHolidayOvertimePayRatio',
      label: '法定节假日倍率',
      component: 'InputNumber',
      componentProps: {
        addonAfter: '倍',
        max: 10,
        min: 0,
        precision: 2,
        step: 0.1,
        placeholder: '0 è¡¨ç¤ºä¸è®¡è¯¥ç±»åž‹åŠ ç­å·¥èµ„',
        style: { width: '100%' },
      },
      rules: 'required',
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'RadioGroup',
      componentProps: { options: statusOptions },
      rules: 'required',
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      componentProps: {
        maxlength: 500,
        placeholder: '填写适用岗位、职级或特殊计薪说明',
        rows: 3,
        showCount: true,
      },
      formItemClass: 'col-span-2',
    },
  ];
}
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'name',
      label: '结构名称',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入结构名称',
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: statusOptions,
        placeholder: '请选择状态',
      },
    },
  ];
}
export function useGridColumns(): VxeTableGridOptions<HrmSalaryStructureApi.SalaryStructure>['columns'] {
  return [
    { field: 'name', title: '结构名称', minWidth: 180 },
    {
      field: 'overtimePayRatio',
      title: '工作日加班倍率',
      width: 145,
      slots: { default: 'overtimePayRatio' },
    },
    {
      field: 'restDayOvertimePayRatio',
      title: '休息日加班倍率',
      width: 145,
      slots: { default: 'restDayOvertimePayRatio' },
    },
    {
      field: 'legalHolidayOvertimePayRatio',
      title: '法定节假日倍率',
      width: 165,
      slots: { default: 'legalHolidayOvertimePayRatio' },
    },
    {
      field: 'status',
      title: '状态',
      width: 90,
      slots: { default: 'status' },
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDateTime',
    },
    {
      field: 'actions',
      title: '操作',
      fixed: 'right',
      slots: { default: 'actions' },
      width: 150,
    },
  ];
}
src/views/hrm/salary/structure/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,137 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmSalaryStructureApi } from '#/api/hrm/salary/structure';
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
import { Alert, message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteSalaryStructure,
  getSalaryStructureList,
} from '#/api/hrm/salary/structure';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
defineOptions({ name: 'HrmSalaryStructure' });
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
function handleRefresh() {
  gridApi.query();
}
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
function handleEdit(row: HrmSalaryStructureApi.SalaryStructure) {
  formModalApi.setData({ id: row.id, formType: 'update' }).open();
}
async function handleDelete(row: HrmSalaryStructureApi.SalaryStructure) {
  await deleteSalaryStructure(row.id!);
  message.success($t('ui.actionMessage.deleteSuccess', ['薪酬结构']));
  handleRefresh();
}
function formatRatio(value?: number) {
  return value === undefined || value === null ? '-' : `${value.toFixed(2)} å€`;
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: { schema: useGridFormSchema() },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    pagerConfig: { enabled: false },
    proxyConfig: {
      ajax: {
        query: async (_params, formValues) =>
          await getSalaryStructureList(formValues),
      },
    },
    rowConfig: { keyField: 'id', isHover: true },
    toolbarConfig: { refresh: true, search: true },
  } as VxeTableGridOptions<HrmSalaryStructureApi.SalaryStructure>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Alert
      class="mb-4"
      description="基本工资取员工档案;社保和公积金基数取员工社保档案。这里仅维护工作日、休息日和法定节假日加班倍率。"
      message="薪酬结构维护加班计薪规则"
      show-icon
      type="info"
    />
    <Grid table-title="薪酬结构列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['薪酬结构']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['hrm:salary-structure:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #overtimePayRatio="{ row }">
        <Tag :color="(row.overtimePayRatio ?? 0) > 0 ? 'blue' : 'default'">
          {{ (row.overtimePayRatio ?? 0) > 0 ? formatRatio(row.overtimePayRatio) : '不计加班工资' }}
        </Tag>
      </template>
      <template #restDayOvertimePayRatio="{ row }">
        <Tag :color="(row.restDayOvertimePayRatio ?? 0) > 0 ? 'blue' : 'default'">
          {{ (row.restDayOvertimePayRatio ?? 0) > 0 ? formatRatio(row.restDayOvertimePayRatio) : '不计加班工资' }}
        </Tag>
      </template>
      <template #legalHolidayOvertimePayRatio="{ row }">
        <Tag :color="(row.legalHolidayOvertimePayRatio ?? 0) > 0 ? 'blue' : 'default'">
          {{ (row.legalHolidayOvertimePayRatio ?? 0) > 0 ? formatRatio(row.legalHolidayOvertimePayRatio) : '不计加班工资' }}
        </Tag>
      </template>
      <template #status="{ row }">
        <Tag :color="row.status === 0 ? 'success' : 'default'">
          {{ row.status === 0 ? '启用' : '禁用' }}
        </Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['hrm:salary-structure:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['hrm:salary-structure:delete'],
              popConfirm: {
                title: '确认删除该薪酬结构吗?已被员工使用的结构不可删除。',
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/hrm/salary/structure/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,95 @@
<script lang="ts" setup>
import type { HrmSalaryStructureApi } from '#/api/hrm/salary/structure';
import type { FormType } from '../data';
import { computed, ref } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { Alert, message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
  createSalaryStructure,
  getSalaryStructure,
  updateSalaryStructure,
} from '#/api/hrm/salary/structure';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const getTitle = computed(() =>
  formType.value === 'update' ? '修改薪酬结构' : '新增薪酬结构',
);
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: { class: 'w-full' },
    formItemClass: 'col-span-1',
    labelWidth: 110,
  },
  wrapperClass: 'grid-cols-2',
  layout: 'horizontal',
  schema: useFormSchema(),
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) return;
    modalApi.lock();
    try {
      const data =
        (await formApi.getValues()) as HrmSalaryStructureApi.SalaryStructure;
      if (data.id) {
        await updateSalaryStructure(data);
      } else {
        await createSalaryStructure(data);
      }
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) return;
    const data = modalApi.getData<{ formType: FormType; id?: number }>();
    formType.value = data.formType;
    await formApi.resetForm();
    if (!data.id) {
      await formApi.setValues({
        legalHolidayOvertimePayRatio: 3,
        overtimePayRatio: 1.5,
        restDayOvertimePayRatio: 2,
        status: 0,
      });
      return;
    }
    modalApi.lock();
    try {
      await formApi.setValues(await getSalaryStructure(data.id));
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-[680px]">
    <Alert
      class="mx-4 mb-4"
      description="三种加班类型分别设置倍率;某一项设为 0 æ—¶ï¼Œä»…该类型不计算加班工资,审批通过的加班时长仍正常统计。"
      message="加班工资计算说明"
      show-icon
      type="info"
    />
    <Form class="mx-4" />
  </Modal>
</template>
src/views/mes/md/item/type/data.ts
@@ -223,3 +223,27 @@
    },
  ];
}
/** ç‰©æ–™åˆ†ç±»å¯¼å…¥çš„表单 */
export function useImportFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'file',
      label: '分类数据',
      component: 'Upload',
      rules: 'required',
      help: '仅允许导入 xls、xlsx æ ¼å¼æ–‡ä»¶',
    },
    {
      fieldName: 'updateSupport',
      label: '是否覆盖',
      component: 'Switch',
      componentProps: {
        checkedChildren: '是',
        unCheckedChildren: '否',
      },
      rules: z.boolean().default(false),
      help: '是否更新已经存在的分类数据',
    },
  ];
}
src/views/mes/md/item/type/index.vue
@@ -14,9 +14,15 @@
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
import ImportForm from './modules/import-form.vue';
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [ImportModal, importModalApi] = useVbenModal({
  connectedComponent: ImportForm,
  destroyOnClose: true,
});
@@ -36,6 +42,11 @@
/** åˆ›å»ºåˆ†ç±» */
function handleCreate() {
  formModalApi.setData(null).open();
}
/** å¯¼å…¥åˆ†ç±» */
function handleImport() {
  importModalApi.open();
}
/** æ·»åŠ ä¸‹çº§åˆ†ç±» */
@@ -101,7 +112,7 @@
</script>
<template>
  <Page auto-content-height><FormModal @success="handleRefresh" />
  <Page auto-content-height><FormModal @success="handleRefresh" /><ImportModal @success="handleRefresh" />
    <Grid table-title="物料分类列表">
      <template #toolbar-tools>
        <TableAction
@@ -114,6 +125,13 @@
              onClick: handleCreate,
            },
            {
              label: $t('ui.actionTitle.import', ['物料分类']),
              type: 'primary',
              icon: ACTION_ICON.UPLOAD,
              auth: ['mes:md-item-type:import'],
              onClick: handleImport,
            },
            {
              label: isExpanded ? '收缩' : '展开',
              type: 'primary',
              onClick: handleExpand,
src/views/mes/md/item/type/modules/import-form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,105 @@
<script lang="ts" setup>
import type { FileType } from 'ant-design-vue/es/upload/interface';
import { h } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message, notification, Upload } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { importItemType, importItemTypeTemplate } from '#/api/mes/md/item/type';
import { $t } from '#/locales';
import { useImportFormSchema } from '../data';
const emit = defineEmits(['success']);
const [Form, formApi] = useVbenForm({
  commonConfig: {
    formItemClass: 'col-span-2',
    labelWidth: 120,
  },
  layout: 'horizontal',
  schema: useImportFormSchema(),
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    // æäº¤è¡¨å•
    const data = await formApi.getValues();
    try {
      const response = (await importItemType(data.file, data.updateSupport)) as any;
      // å…³é—­å¹¶æç¤º
      await modalApi.close();
      emit('success');
      const resultData = response?.data || response || {};
      const failureList = resultData.failureList || {};
      if (Object.keys(failureList).length > 0) {
        const errorNodes = Object.entries(failureList).map(([name, reason]) => {
          return h('div', `${name}: ${reason}`);
        });
        notification.warning({
          message: '导入完成,部分物料分类导入失败',
          description: () => h('div', errorNodes),
          duration: null,
        });
      } else {
        message.success($t('ui.actionMessage.operationSuccess'));
      }
    } finally {
      modalApi.unlock();
    }
  },
});
/** ä¸Šä¼ å‰ */
function beforeUpload(file: FileType) {
  formApi.setFieldValue('file', file);
  return false;
}
/** ä¸‹è½½æ¨¡ç‰ˆ */
async function handleDownload() {
  const data = await importItemTypeTemplate();
  downloadFileFromBlobPart({ fileName: '物料分类导入模板.xls', source: data });
}
</script>
<template>
  <Modal title="导入物料分类" class="w-1/3">
    <Form class="mx-4">
      <template #file>
        <div class="w-full">
          <Upload
            :max-count="1"
            accept=".xls,.xlsx"
            :before-upload="beforeUpload"
          >
            <Button type="primary"> é€‰æ‹© Excel æ–‡ä»¶ </Button>
          </Upload>
        </div>
      </template>
    </Form>
    <div class="mx-4 mt-2 mb-4 text-gray-500 text-xs">
      æç¤ºï¼šæ¨¡æ¿ä¸­çš„“父分类编码”请填写父分类的编码(如 RAW),顶级分类该列留空即可。
      <div>“物料/产品标识”填 ITEM(物料)或 PRODUCT(产品)。</div>
      <div>“状态”填 0(启用)/1(禁用)。</div>
    </div>
    <template #prepend-footer>
      <div class="flex flex-auto items-center">
        <Button @click="handleDownload"> ä¸‹è½½å¯¼å…¥æ¨¡æ¿ </Button>
      </div>
    </template>
  </Modal>
</template>
src/views/mes/pro/route/data.ts
@@ -219,6 +219,15 @@
      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'),
src/views/mes/pro/route/modules/process-form.vue
@@ -31,7 +31,7 @@
  commonConfig: {
    componentProps: { class: 'w-full' },
    formItemClass: 'col-span-1',
    labelWidth: 130,
    labelWidth: 150,
  },
  layout: 'horizontal',
  schema: [],
src/views/mes/pro/workorder/modules/process-form.vue
@@ -2,7 +2,7 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { MesProWorkOrderProcessApi } from '#/api/mes/pro/workorder/process';
import { computed, ref } from 'vue';
import { computed, h, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
@@ -91,6 +91,15 @@
      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: [
src/views/mes/process-design/route/data.ts
@@ -205,6 +205,15 @@
      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"),
src/views/mes/process-design/route/modules/process-form.vue
@@ -31,7 +31,7 @@
  commonConfig: {
    componentProps: { class: 'w-full' },
    formItemClass: 'col-span-1',
    labelWidth: 130,
    labelWidth: 150,
  },
  layout: 'horizontal',
  schema: [],
src/views/wls/materialstock/components/stock-adjust-modal.vue
@@ -20,7 +20,7 @@
} from '#/api/mes/wm/materialstock';
import { getItem } from '#/api/mdm/item';
import { MdmItemSelect } from '#/views/basicData/mdm/components';
import { ErpSupplierSelect } from '#/views/erp/purchase/supplier/components';
import { SrmSupplierSelect } from '#/views/srm/supplier/components';
import BatchConfigDisplay from './batch-config-display.vue';
import {
  WmWarehouseAreaSelect,
@@ -297,7 +297,7 @@
    {
      fieldName: 'vendorId',
      label: '供应商',
      component: markRaw(ErpSupplierSelect),
      component: markRaw(SrmSupplierSelect),
      componentProps: {
        placeholder: '请选择供应商',
      },
src/views/wls/stocktaking/plan/modules/param-form.vue
@@ -52,7 +52,7 @@
    const valueValid =
      values.type === WlsStockTakingParamTypeEnum.QUALITY_STATUS
        ? !!values.valueCode
        : values.valueId !== null;
        : values.valueId !== undefined && values.valueId !== null;
    if (!valueValid) {
      message.warning('请选择条件值');
      return;
vite.config.ts
@@ -25,6 +25,7 @@
    __APP_NAME__: JSON.stringify(appName),
  };
  if (!isDev && appApiUrl) {
    process.env.VITE_GLOB_API_URL = appApiUrl;
    defineVars['import.meta.env.VITE_GLOB_API_URL'] = JSON.stringify(appApiUrl);
    defineVars['import.meta.env.VITE_BASE_URL'] = JSON.stringify(new URL(appApiUrl).origin);
  }
@@ -82,7 +83,7 @@
        proxy: {
          "/admin-api": {
            changeOrigin: true,
            target: "http://192.168.0.10:48080",
            target: "http://36.213.90.123:9500",
            ws: true,
          },
        },