.env.production
@@ -3,9 +3,9 @@ VITE_BASE=/ # 请求路径 VITE_BASE_URL=http://36.213.180.217:9501 VITE_BASE_URL=http://36.213.90.123:9501 # 接口地址 VITE_GLOB_API_URL=http://36.213.180.217:9501/admin-api VITE_GLOB_API_URL=http://36.213.90.123:9501/admin-api # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务 VITE_UPLOAD_TYPE=server company.config.ts
@@ -21,7 +21,7 @@ name: '测试公司', logo: '/logos/TEST.png', favicon: '/logos/TEST.ico', apiUrl: 'http://36.213.180.217:9501/admin-api', apiUrl: 'http://36.213.90.123:9501/admin-api', }, JHHG: { name: '宁夏九泓化工', deploy.bat
@@ -1,169 +1,451 @@ @echo off setlocal enabledelayedexpansion :: ================= Config ================= set "COMPANY=TEST" set "USER=root" set "PASS=Xdrjkj@10086#" set "IP=36.213.180.217" set "PORT=65022" set "ZIP_FILE=dist.zip" set "REMOTE_DIR=/data/project/mom/vue/test/dist" set "SSH_OPTS=-o StrictHostKeyChecking=no -o ConnectTimeout=10" :: =========================================== echo ========================================== echo Vue Dist Auto Deploy Script echo ========================================== echo. :: Check if sshpass is available set "SSHPASS_DIR=%~dp0bin" if exist "!SSHPASS_DIR!\sshpass.exe" ( set "PATH=!SSHPASS_DIR!;%PATH%" ) where sshpass >nul 2>&1 if %errorlevel% equ 0 goto :check_node echo [WARN] sshpass not found, trying to install... echo. where bash >nul 2>&1 if %errorlevel% equ 0 ( echo [INFO] Git Bash detected, trying pacman install... bash -c "pacman -S --noconfirm sshpass" 2>nul if !errorlevel! equ 0 ( echo [OK] sshpass installed via pacman! goto :check_node ) ) if not exist "!SSHPASS_DIR!" mkdir "!SSHPASS_DIR!" if exist "!SSHPASS_DIR!\sshpass.exe" ( set "PATH=!SSHPASS_DIR!;%PATH%" echo [OK] sshpass found at: !SSHPASS_DIR! goto :check_node ) echo [INFO] Downloading sshpass... powershell -NoProfile -Command "$url='https://github.com/xhcoding/sshpass-win32/releases/download/v1.0.3/sshpass.exe'; $out='!SSHPASS_DIR!\sshpass.exe'; Write-Host \"Downloading to: $out\"; try { Invoke-WebRequest -Uri $url -OutFile $out -UseBasicParsing; exit 0 } catch { Write-Host $_.Exception.Message; exit 1 }" if !errorlevel! neq 0 ( echo [ERROR] Failed to download sshpass! echo [HINT] Please install sshpass manually or use Git Bash: pacman -S sshpass goto :error_exit ) set "PATH=!SSHPASS_DIR!;%PATH%" echo [OK] sshpass downloaded to: !SSHPASS_DIR! echo. :check_node :: Check if pnpm is available where pnpm >nul 2>&1 if %errorlevel% neq 0 ( echo [ERROR] pnpm not found! Please install pnpm first. echo [HINT] npm install -g pnpm goto :error_exit ) :: Check if node_modules exists if not exist "node_modules" ( echo [INFO] node_modules not found, running pnpm install... echo. call pnpm install if !errorlevel! neq 0 ( echo [ERROR] pnpm install failed! goto :error_exit ) echo. ) :: Check if dist.zip exists and ask user if exist "%ZIP_FILE%" ( echo [WARN] %ZIP_FILE% already exists. set /p "REBUILD=Do you want to rebuild? (Y/N, default Y): " if /i "!REBUILD!"=="N" ( echo [INFO] Skip build, using existing %ZIP_FILE% goto :upload ) ) :: Build the project echo [STEP 1/3] Building project for %COMPANY%... echo [INFO] Running: pnpm run build echo. :: Pass VITE_COMPANY so vite.config.ts picks the right company (vite reads VITE_ prefixed env vars) set "VITE_COMPANY=!COMPANY!" && call pnpm run build if !errorlevel! neq 0 ( echo [ERROR] Build failed! Check your project configuration. goto :error_exit ) echo [OK] Build completed! echo. :upload if not exist "%ZIP_FILE%" ( echo [ERROR] %ZIP_FILE% not found. echo [HINT] Build may have failed or VITE_ARCHIVER is not enabled in .env.production. goto :error_exit ) :: Calculate zip size for %%F in ("%ZIP_FILE%") do set "ZIP_BYTES=%%~zF" set /a "ZIP_SIZE_MB=ZIP_BYTES/1024/1024" echo [INFO] Zip file: %ZIP_FILE% echo [INFO] Size: %ZIP_SIZE_MB% MB echo [INFO] Target: %USER%@%IP%:%PORT% echo [INFO] Remote dir: %REMOTE_DIR% echo. :: Optional: Ask for confirmation before upload set /p "CONFIRM=Proceed with upload? (Y/N, default Y): " if /i "!CONFIRM!"=="N" ( echo [INFO] Upload cancelled by user. goto :end ) :: Upload dist.zip echo [STEP 2/3] Uploading %ZIP_FILE%... sshpass -p "%PASS%" scp -P %PORT% %SSH_OPTS% "%ZIP_FILE%" %USER%@%IP%:%REMOTE_DIR%/ if %errorlevel% neq 0 ( echo [ERROR] Upload failed! Check network or credentials. goto :error_exit ) echo [OK] Upload completed! echo. :: Remote unzip: cd to %REMOTE_DIR% then unzip -o ./dist.zip echo [STEP 3/3] Running remote: cd %REMOTE_DIR% && unzip -o ./%ZIP_FILE% sshpass -p "%PASS%" ssh -P %PORT% %SSH_OPTS% %USER%@%IP% "cd %REMOTE_DIR% && unzip -o ./%ZIP_FILE% && rm -f ./%ZIP_FILE%" if %errorlevel% neq 0 ( echo [ERROR] Remote unzip failed! Check if 'unzip' is installed on server. goto :error_exit ) echo [OK] Unzip completed! %ZIP_FILE% removed from remote. echo. echo ========================================== echo [SUCCESS] Deploy completed! echo ========================================== echo. echo Summary: echo - Build: Completed echo - Upload: %ZIP_FILE% (%ZIP_SIZE_MB% MB) echo - Target: %USER%@%IP%:%PORT%%REMOTE_DIR% echo. goto :end :error_exit echo. echo ========================================== echo [FAILED] Deploy failed! echo ========================================== :end pause @echo off setlocal EnableExtensions EnableDelayedExpansion REM ============================================================ REM Vue Frontend Deploy Script REM ============================================================ REM ========================= REM Configuration REM ========================= set "COMPANY=TEST" set "REMOTE_USER=root" set "REMOTE_HOST=36.213.90.123" set "REMOTE_PORT=65022" set "PASSWORD=Xdrjkj@10086#" set "ZIP_FILE=dist.zip" set "REMOTE_DIR=/data/project/mom/vue/test/dist" set "SSH_OPTS=-o StrictHostKeyChecking=no -o ConnectTimeout=10 -o ServerAliveInterval=15 -o ServerAliveCountMax=3" set "RETRY_COUNT=3" set "SSHPASS_DIR=%~dp0bin" REM ============================================================ REM Start REM ============================================================ echo. echo ============================================================ echo Vue Dist Auto Deploy echo ============================================================ echo. echo Company : %COMPANY% echo Server : %REMOTE_USER%@%REMOTE_HOST% echo Port : %REMOTE_PORT% echo Remote Dir : %REMOTE_DIR% echo Package : %ZIP_FILE% echo. 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 src/api/mes/pro/task/index.ts
@@ -1,8 +1,21 @@ import type { PageParam, PageResult } from '@vben/request'; import type { PageParam, PageResult } from '@vben/request'; import { requestClient } from '#/api/request'; export namespace MesProTaskApi { export interface PauseRecord { id: number; pauseTime: string; pauseUserId: number; pauseUserName: string; reason: string; remark?: string; resumeTime?: string; resumeUserId?: number; resumeUserName?: string; taskId: number; } /** MES 生产任务 */ export interface Task { id?: number; @@ -41,6 +54,9 @@ checkFlag?: boolean; // 是否质检(派生自工艺路线工序) backflushFlag?: boolean; // 是否倒冲(派生自工艺路线工序) remark?: string; // 备注 pauseRecords?: PauseRecord[]; // 停工记录 reportableQuantity?: number; // 可报工数量 assigneeUserIds?: number[]; // 报工人 } /** MES 生产任务分页查询参数 */ @@ -174,3 +190,20 @@ { params: { workstationId, quantity } }, ); } /** 生产任务停工请求参数 */ export interface MesProTaskPauseReqVO { id: number; reason: string; remark?: string; } /** 生产任务停工 */ export function pauseTask(data: MesProTaskPauseReqVO) { return requestClient.put('/mes/pro/task/pause', data); } /** 生产任务复工 */ export function resumeTask(data: { id: number }) { return requestClient.put('/mes/pro/task/resume', data); } src/api/mes/pro/workorder/index.ts
@@ -1,4 +1,4 @@ import type { PageParam, PageResult } from '@vben/request'; import type { PageParam, PageResult } from '@vben/request'; import { requestClient } from '#/api/request'; @@ -34,6 +34,10 @@ finishDate?: number; // 完成时间 cancelDate?: number; // 取消时间 status?: number; // 订单状态 scheduleStatus?: number; // 排产状态 productionStatus?: number; // 生产状态 processProgressList?: ProcessProgress[]; // 工序生产进度 completionProgress?: number; // 完成进度 remark?: string; // 备注 createTime?: number; // 创建时间 } @@ -59,6 +63,14 @@ ); } /** 获得生产工单调度状态分页 */ export function getWorkOrderSchedulePage(params: MesProWorkOrderApi.PageParams) { return requestClient.get<PageResult<MesProWorkOrderApi.WorkOrder>>( '/mes/pro/work-order/schedule-page', { params }, ); } /** 查询生产订单详情 */ export function getWorkOrder(id: number) { return requestClient.get<MesProWorkOrderApi.WorkOrder>( src/api/system/user/index.ts
@@ -114,3 +114,10 @@ params: { nickname }, }); } /** 通过角色标识获取用户精简信息列表 */ export function getSimpleUserListByRoleCode(code: string) { return requestClient.get<SystemUserApi.UserSimple[]>('/system/user/list-by-role-code', { params: { code }, }); } src/packages/constants/src/dict-enum.ts
@@ -239,6 +239,7 @@ MES_DV_METERING_CHECK_RESULT: 'mes_dv_metering_check_result', // MES 计量检测结论 MES_PRO_LINK_TYPE: 'mes_pro_link_type', // MES 工序关系类型 MES_PRO_WORK_ORDER_STATUS: 'mes_pro_work_order_status', // MES 生产工单状态 ORDER_PRODUCTION_STATUS: 'order_production_status', // 生产订单状态 MES_PRO_WORK_ORDER_TYPE: 'mes_pro_work_order_type', // MES 工单类型 MES_PRO_WORK_ORDER_SOURCE_TYPE: 'mes_pro_work_order_source_type', // MES 工单来源类型 MES_PRO_TASK_STATUS: 'mes_pro_task_status', // MES 生产任务状态 src/views/mes/pro/feedback/data.ts
@@ -1,4 +1,4 @@ import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { MesProFeedbackApi } from '#/api/mes/pro/feedback'; import type { MesProTaskApi } from '#/api/mes/pro/task'; @@ -264,6 +264,7 @@ taskId: undefined, unitMeasureName: undefined, workstationId: undefined, reportableQuantity: undefined, }); }, }, @@ -298,6 +299,7 @@ routeId: task.routeId, unitMeasureName: task.unitMeasureName, workstationId: task.workstationId, reportableQuantity: task.reportableQuantity, }); // 工艺路线工序的 checkFlag 决定数量区域展示 if (task.routeId && task.processId) { @@ -377,6 +379,20 @@ }, }, { fieldName: 'reportableQuantity', label: '可报工数量', component: 'InputNumber', componentProps: { class: 'w-full', disabled: true, precision: 2, }, dependencies: { triggerFields: ['taskId'], show: (values) => !!values.taskId && ['create', 'update'].includes(formType), }, }, { fieldName: 'feedbackQuantity', label: '报工数量', component: 'InputNumber', src/views/mes/pro/task/data.ts
@@ -1,5 +1,7 @@ import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import { z } from '#/adapter/form'; import { getSimpleUserListByRoleCode } from '#/api/system/user'; import type { MesProTaskApi } from '#/api/mes/pro/task'; import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder'; @@ -130,15 +132,33 @@ width: 80, }, { field: 'status', title: '订单状态', width: 100, cellRender: { name: 'CellDict', props: { type: DICT_TYPE.MES_PRO_WORK_ORDER_STATUS }, }, }, { field: 'productionStatus', title: '生产状态', width: 100, cellRender: { name: 'CellDict', props: { type: DICT_TYPE.ORDER_PRODUCTION_STATUS }, }, }, { field: 'quantity', title: '订单数量', width: 100, }, { field: 'quantityChanged', title: '调整数量', width: 100, }, // { // field: 'quantityChanged', // title: '调整数量', // width: 100, // }, { field: 'quantityProduced', title: '已生产数量', @@ -160,15 +180,7 @@ width: 120, formatter: 'formatDate', }, { field: 'status', title: '排产状态', width: 100, cellRender: { name: 'CellDict', props: { type: DICT_TYPE.MES_PRO_WORK_ORDER_STATUS }, }, }, { title: '操作', width: 160, @@ -458,6 +470,20 @@ }, }, { fieldName: 'assigneeUserIds', label: '报工人', component: 'ApiSelect', componentProps: { placeholder: '请选择报工人', api: getSimpleUserListByRoleCode, params: { code: 'production_submit_work' }, labelField: 'nickname', valueField: 'id', mode: 'multiple', }, rules: z.array(z.number()).min(1, '请选择报工人').default([]), }, { fieldName: 'remark', label: '备注', component: 'Textarea', src/views/mes/pro/task/index.vue
@@ -1,6 +1,6 @@ <script lang="ts" setup> <script lang="ts" setup> import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder'; import {getWorkOrderSchedulePage, type MesProWorkOrderApi} from '#/api/mes/pro/workorder'; import { onMounted, ref } from 'vue'; import { useRouter } from 'vue-router'; @@ -11,7 +11,7 @@ MesProWorkOrderTypeEnum, } from '@vben/constants'; import { Button, Card, message } from 'ant-design-vue'; import { Button, Card, message, Tabs } from 'ant-design-vue'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; import { getGanttTaskList, previewAutoGenerate, autoGenerateTasks } from '#/api/mes/pro/task'; @@ -27,6 +27,8 @@ const ganttTasks = ref<any[]>([]); // 甘特图预览数据 const archiveVisible = ref(false); const archiveProductCode = ref(''); const activeTab = ref('PENDING'); // 默认选中待排产 const [ScheduleModal, scheduleModalApi] = useVbenModal({ connectedComponent: ScheduleForm, @@ -106,10 +108,19 @@ proxyConfig: { ajax: { query: async ({ page }, formValues) => { return await getWorkOrderPage({ let scheduleStatus: number | undefined = undefined; if (activeTab.value === 'PENDING') { scheduleStatus = 0; // 待排产 } else if (activeTab.value === 'FINISHED') { scheduleStatus = 1; // 已完成 } // activeTab === 'ALL' 时不传 return await getWorkOrderSchedulePage({ pageNo: page.currentPage, pageSize: page.pageSize, status: MesProWorkOrderStatusEnum.CONFIRMED, scheduleStatus, type: MesProWorkOrderTypeEnum.SELF, ...formValues, }); @@ -147,7 +158,17 @@ <GanttChart :height="350" :readonly="true" :tasks="ganttTasks" /> </Card> <Grid table-title="待排产订单"> <Grid> <template #table-title> <div class="flex flex-col gap-2"> <span class="text-base font-medium">生产订单列表</span> <Tabs v-model:activeKey="activeTab" @change="handleRefresh" :tabBarStyle="{ margin: 0, borderBottom: 0 }"> <Tabs.TabPane key="ALL" tab="全部" /> <Tabs.TabPane key="PENDING" tab="未完成" /> <Tabs.TabPane key="FINISHED" tab="已完成" /> </Tabs> </div> </template> <template #toolbar-tools> <TableAction :actions="[ src/views/mes/pro/task/modules/schedule-form.vue
@@ -1,4 +1,4 @@ <script lang="ts" setup> <script lang="ts" setup> import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder'; import type { MesProWorkOrderProcessApi } from '#/api/mes/pro/workorder/process'; @@ -50,16 +50,19 @@ message.warning('当前订单未配置工序,请先在工单中维护工序信息'); return; } // 确定工艺路线ID:工单路线 → 产品路线 let tempRouteId = 0; if (workOrder.value?.routeId) { tempRouteId = workOrder.value.routeId; } else { const processes = await getRouteProcessListByProduct(productId); tempRouteId = processes?.[0]?.routeId ?? 0; } currentRouteId.value = tempRouteId; routeProcessList.value = [...woProcesses].toSorted( (a, b) => (a.sort ?? 0) - (b.sort ?? 0), ); // 确定工艺路线ID:工单路线 → 产品路线 if (workOrder.value?.routeId) { currentRouteId.value = workOrder.value.routeId; } else { const processes = await getRouteProcessListByProduct(productId); currentRouteId.value = processes?.[0]?.routeId ?? 0; } } /** 完成订单 */ @@ -131,22 +134,22 @@ :title="rp.processName" /> </Steps> <Card v-for="(rp, index) in routeProcessList" v-show="activeProcessStep === index" :key="rp.processId" class="mx-4" > <TaskList :color-code="rp.colorCode" :disabled="isReadonly" :item-id="rp.outputItemId || workOrder.productId" :process-id="rp.processId!" :route-id="currentRouteId" :work-order-id="workOrder.id!" :work-order-quantity="workOrder.quantity" /> </Card> <template v-for="(rp, index) in routeProcessList" :key="rp.processId"> <Card v-if="activeProcessStep === index" class="mx-4" > <TaskList :color-code="rp.colorCode" :disabled="isReadonly" :item-id="rp.outputItemId || workOrder.productId" :process-id="rp.processId!" :route-id="currentRouteId" :work-order-id="workOrder.id!" :work-order-quantity="workOrder.quantity" /> </Card> </template> </template> <template #prepend-footer> <div class="flex flex-auto items-center gap-2"> src/views/mes/pro/task/modules/task-list.vue
@@ -110,6 +110,8 @@ () => props.processId, () => handleRefresh(), ); </script> <template> src/views/mes/pro/workorder/data.ts
@@ -1,4 +1,4 @@ import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder'; import type { MesProWorkOrderBomApi } from '#/api/mes/pro/workorder/bom'; @@ -413,6 +413,27 @@ }, }, { field: 'productionStatus', title: '生产状态', width: 100, cellRender: { name: 'CellDict', props: { type: DICT_TYPE.ORDER_PRODUCTION_STATUS }, }, }, { field: 'processProgressList', title: '工序生产进度', width: 350, slots: { default: 'processProgressList' }, }, { field: 'completionProgress', title: '完成进度', width: 200, slots: { default: 'completionProgress' }, }, { field: 'createTime', title: '创建时间', width: 180, src/views/mes/pro/workorder/index.vue
@@ -11,7 +11,7 @@ } from "@vben/constants"; import { downloadFileFromBlobPart } from "@vben/utils"; import { Button, message } from "ant-design-vue"; import { Button, message, Progress, Steps, Popover, Modal } from "ant-design-vue"; import { ACTION_ICON, TableAction, useVbenVxeGrid } from "#/adapter/vxe-table"; import { @@ -63,6 +63,21 @@ /** 完成订单 */ function handleFinish(row: MesProWorkOrderApi.WorkOrder) { formModalApi.setData({ formType: "finish", id: row.id }).open(); } /** 完成订单前校验 */ function handleCheckFinish(row: MesProWorkOrderApi.WorkOrder) { if (row.productionStatus !== 2) { Modal.confirm({ title: '操作确认', content: '该订单未完成,是否确认要继续完成?', onOk() { handleFinish(row); }, }); } else { handleFinish(row); } } /** 删除订单 */ @@ -139,6 +154,13 @@ }, } as VxeTableGridOptions<MesProWorkOrderApi.WorkOrder>, }); /** 获取工序节点状态样式类 */ function getProcessNodeStatus(percent: number) { if (percent >= 100) return 'completed'; if (percent > 0) return 'in-progress'; return 'pending'; } </script> <template> @@ -168,6 +190,29 @@ @click="handleDetail(row)"> {{ row.code }} </Button> </template> <template #processProgressList="{ row }"> <div v-if="row.processProgressList && row.processProgressList.length > 0" class="process-node-list"> <div v-for="(item, index) in row.processProgressList" :key="index" class="process-node-item" > <div class="process-node-dot" :class="getProcessNodeStatus(item.progressPercent || item.progress || 0)"></div> <div class="process-node-info"> <span class="process-node-name" :title="item.processName ">{{ item.processName }}</span> <span class="process-node-percent">{{ item.progress || 0 }}%</span> </div> <div v-if="index !== row.processProgressList.length - 1" class="process-node-line"></div> </div> </div> <span v-else>-</span> </template> <template #completionProgress="{ row }"> <div style="display: flex; align-items: center; gap: 8px; width: 100%;"> <Progress :percent="row.completionProgress || 0" :show-info="false" size="small" style="flex: 1; margin: 0;" /> <span style="white-space: nowrap; min-width: 40px; text-align: left;">{{ row.completionProgress || 0 }}%</span> </div> </template> <template #actions="{ row }"> <TableAction :actions="[ @@ -207,7 +252,7 @@ ifShow: row.status === MesProWorkOrderStatusEnum.CONFIRMED && !!row.quantityProduced, onClick: handleFinish.bind(null, row), onClick: handleCheckFinish.bind(null, row), }, { label: '取消', @@ -238,3 +283,79 @@ <ArchiveDetail ref="archiveDetailRef" /> </Page> </template> <style scoped> .process-node-list { display: flex; align-items: center; overflow-x: auto; overflow-y: hidden; max-width: 100%; padding-bottom: 4px; } /* Custom scrollbar to make it sleek */ .process-node-list::-webkit-scrollbar { height: 4px; } .process-node-list::-webkit-scrollbar-thumb { background-color: #d9d9d9; border-radius: 4px; } .process-node-list::-webkit-scrollbar-track { background: transparent; } .process-node-item { display: flex; align-items: center; flex-shrink: 0; } .process-node-dot { width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; } .process-node-dot.completed { background-color: #52c41a; } .process-node-dot.in-progress { background-color: #1890ff; } .process-node-dot.pending { background-color: #d9d9d9; } .process-node-info { display: flex; flex-direction: column; justify-content: center; margin-right: 12px; font-size: 12px; line-height: 1.2; } .process-node-name { color: #333; max-width: 80px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .process-node-percent { color: #888; font-size: 11px; } .process-node-line { width: 20px; height: 1px; background-color: #e8e8e8; margin-right: 12px; } </style> src/views/mes/workbench/components/TaskListPanel.vue
@@ -1,5 +1,5 @@ <script lang="ts" setup> import type { PendingTask } from '../../types'; import { computed, ref } from 'vue'; @@ -15,7 +15,13 @@ const emit = defineEmits<{ select: [task: PendingTask]; refresh: []; }>(); import { pauseTask, resumeTask } from '#/api/mes/pro/task'; import { Form, FormItem, Input, Modal, message, Button } from 'ant-design-vue'; import { confirm } from '@vben/common-ui'; import type {PendingTask} from "#/views/mes/workbench/types"; const searchKeyword = ref(''); @@ -41,6 +47,50 @@ if (rate >= 100) return '#52c41a'; if (rate >= 60) return '#1890ff'; return '#faad14'; } const pauseModalVisible = ref(false); const pauseFormRef = ref(); const pauseFormState = ref({ id: 0, reason: '', remark: '' }); const pauseLoading = ref(false); const pauseRules = { reason: [{ required: true, message: '请输入停工原因', trigger: 'blur' }], }; function handlePause(item: PendingTask, event: Event) { event.stopPropagation(); pauseFormState.value = { id: item.id!, reason: '', remark: '' }; pauseModalVisible.value = true; } async function submitPause() { await pauseFormRef.value?.validate(); pauseLoading.value = true; try { await pauseTask(pauseFormState.value); message.success('停工成功'); pauseModalVisible.value = false; emit('refresh'); } finally { pauseLoading.value = false; } } async function handleResume(item: PendingTask, event: Event) { event.stopPropagation(); try { await confirm('确认要复工该任务吗?'); } catch { return; } try { await resumeTask({ id: item.id! }); message.success('复工成功'); emit('refresh'); } catch (error: any) { // Error is handled by request interceptor } } </script> @@ -99,6 +149,27 @@ > {{ item.feedbackRate ?? 0 }}% </span> <div class="task-card__actions"> <Button v-if="item.status === 9" type="primary" size="small" class="task-card__action-btn resume-btn" @click="handleResume(item, $event)" > 复工 </Button> <Button v-else type="primary" danger size="small" class="task-card__action-btn pause-btn" @click="handlePause(item, $event)" > 停工 </Button> </div> </div> <div class="task-card__progress-row"> @@ -144,6 +215,42 @@ </div> </div> </div> <Modal v-model:open="pauseModalVisible" title="生产任务停工" @ok="submitPause" :confirm-loading="pauseLoading" destroy-on-close > <Form ref="pauseFormRef" :model="pauseFormState" :rules="pauseRules" :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }" style="margin-top: 20px" > <FormItem label="停工原因" name="reason"> <Input.TextArea v-model:value="pauseFormState.reason" placeholder="请输入停工原因(如:设备故障、缺料等)" :rows="3" :maxlength="100" show-count /> </FormItem> <FormItem label="备注" name="remark"> <Input.TextArea v-model:value="pauseFormState.remark" placeholder="请输入备注说明" :rows="2" :maxlength="200" show-count /> </FormItem> </Form> </Modal> </section> </template> @@ -290,6 +397,32 @@ font-variant-numeric: tabular-nums; } .task-card__actions { display: flex; align-items: center; margin-left: auto; } .task-card__action-btn { font-size: 13px; border-radius: 4px; } .pause-btn { box-shadow: 0 2px 4px rgb(255 77 79 / 20%); } .resume-btn { background-color: #52c41a; border-color: #52c41a; box-shadow: 0 2px 4px rgb(82 196 26 / 20%); } .resume-btn:hover, .resume-btn:focus { background-color: #73d13d; border-color: #73d13d; } .task-card__progress-row { display: flex; align-items: baseline; src/views/mes/workbench/components/tabs/TaskDetailTab.vue
@@ -1,7 +1,7 @@ <script lang="ts" setup> import type { PendingTask } from '../../../types'; import { Descriptions, Progress, Tag } from 'ant-design-vue'; import { Descriptions, Progress, Tag, Timeline, TimelineItem } from 'ant-design-vue'; defineOptions({ name: 'WorkbenchTaskDetailTab' }); @@ -91,6 +91,48 @@ </Descriptions.Item> <Descriptions.Item label="备注" :span="2">{{ task.remark ?? '—' }}</Descriptions.Item> </Descriptions> <!-- 停工记录 --> <div v-if="task.pauseRecords && task.pauseRecords.length > 0" class="detail-pause-records"> <div class="detail-pause-records__title">停工历史记录</div> <Timeline class="pause-timeline"> <TimelineItem v-for="record in task.pauseRecords" :key="record.id" :color="record.resumeTime ? 'green' : 'red'" > <div class="pause-timeline-item"> <div class="pause-timeline-time"> <span class="time-label">{{ record.pauseTime }}</span> <span class="time-separator">至</span> <span class="time-label" :class="{ 'is-ongoing': !record.resumeTime }"> {{ record.resumeTime || '正在停工' }} </span> </div> <div class="pause-timeline-content"> <div class="pause-info-row"> <span class="info-label">操作人员:</span> <span class="info-value"> {{ record.pauseUserName }} (停工) <template v-if="record.resumeTime"> <span style="margin: 0 4px; color: #d9d9d9">|</span> {{ record.resumeUserName }} (复工) </template> </span> </div> <div class="pause-info-row"> <span class="info-label">停工原因:</span> <span class="info-value">{{ record.reason }}</span> </div> <div class="pause-info-row" v-if="record.remark"> <span class="info-label">备注:</span> <span class="info-value">{{ record.remark }}</span> </div> </div> </div> </TimelineItem> </Timeline> </div> </template> </div> </template> @@ -187,4 +229,86 @@ font-size: 14px; } } .detail-pause-records { margin-top: 24px; } .detail-pause-records__title { font-size: 16px; font-weight: 600; color: #1a1a1a; margin-bottom: 20px; } .pause-timeline { padding-top: 6px; padding-left: 4px; } .pause-timeline-item { background: #fafafa; border: 1px solid #f0f0f0; border-radius: 8px; padding: 14px 18px; margin-top: -6px; box-shadow: 0 1px 2px rgb(0 0 0 / 2%); transition: all 0.3s ease; &:hover { box-shadow: 0 4px 12px rgb(0 0 0 / 5%); border-color: #e6d8d8; } } .pause-timeline-time { font-size: 13px; color: #595959; font-weight: 500; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; } .time-label { background: #e6f7ff; color: #1890ff; padding: 2px 8px; border-radius: 4px; font-variant-numeric: tabular-nums; } .time-label.is-ongoing { background: #fff1f0; color: #f5222d; } .time-separator { color: #bfbfbf; font-size: 12px; } .pause-timeline-content { display: flex; flex-direction: column; gap: 6px; } .pause-info-row { font-size: 13px; display: flex; line-height: 1.5; } .pause-info-row .info-label { color: #8c8c8c; width: 70px; flex-shrink: 0; } .pause-info-row .info-value { color: #262626; flex: 1; } </style> src/views/mes/workbench/index.vue
@@ -215,6 +215,7 @@ :selected-id="selectedTask?.id" :loading="taskLoading" @select="handleSelectTask" @refresh="loadTaskList" /> </aside> src/views/wls/arrivalnotice/index.vue
@@ -155,13 +155,14 @@ <template #toolbar-tools> <TableAction :actions="[ { label: $t('ui.actionTitle.create', ['到货通知单']), type: 'primary', icon: ACTION_ICON.ADD, auth: ['mes:wm-arrival-notice:create'], onClick: handleCreate, }, // 注释:隐藏新增到货通知单按钮 // { // label: $t('ui.actionTitle.create', ['到货通知单']), // type: 'primary', // icon: ACTION_ICON.ADD, // auth: ['mes:wm-arrival-notice:create'], // onClick: handleCreate, // }, { label: '从采购订单生成', type: 'primary', src/views/wls/arrivalnotice/modules/po-select-dialog.vue
@@ -117,7 +117,7 @@ </script> <template> <Modal title="选择采购订单" class="w-2/3"> <Modal title="选择采购订单" class="w-[1000px]"> <Grid table-title="采购订单列表" /> </Modal> </template> vite.config.ts
@@ -5,18 +5,24 @@ export default defineConfig(async ({ mode }) => { const env = loadEnv(mode, process.cwd(), 'VITE_'); const companyKey = env.VITE_COMPANY || ''; // deploy.bat 通过 set "VITE_COMPANY=xx" 传入,是进程环境变量,loadEnv 读不到,需优先取 process.env const companyKey = process.env.VITE_COMPANY || env.VITE_COMPANY || ''; const company = companies[companyKey]; const appLogo = company?.logo ?? ''; const appFavicon = company?.favicon ?? ''; const appApiUrl = company?.apiUrl ?? ''; const appName = company?.name ?? ''; const isDev = mode === 'development'; console.log('[company]', companyKey, '-> name:', company?.name, 'logo:', appLogo, 'api:', appApiUrl || '(from env)'); if (appName) { // 覆盖浏览器标签页标题:Vite 的 resolveEnv 优先读 process.env,从而替换 index.html 里的 %VITE_APP_TITLE% process.env.VITE_APP_TITLE = appName; } console.log('[company]', companyKey, '-> name:', appName, 'logo:', appLogo, 'api:', appApiUrl || '(from env)'); const defineVars: Record<string, string> = { __APP_LOGO__: JSON.stringify(appLogo), __APP_FAVICON__: JSON.stringify(appFavicon), __APP_NAME__: JSON.stringify(company?.name ?? ''), __APP_NAME__: JSON.stringify(appName), }; if (!isDev && appApiUrl) { defineVars['import.meta.env.VITE_GLOB_API_URL'] = JSON.stringify(appApiUrl);