diff --git a/src/api/device.ts b/src/api/device.ts index d9a1956..f2d4ccf 100644 --- a/src/api/device.ts +++ b/src/api/device.ts @@ -11,13 +11,21 @@ export interface Device{ sectionName?:string, spec:string, manufacturer:string, - manufactureDate:string, + manufactureDate?: string | number | null, workshopId:number, remark:string, status:number, deviceFlag?:string, /** 新代 SynFactory equipID */ synEquipId?: number | null, + /** 系统品牌 */ + systemBrand?: string | null, + /** 是否有网口 0否 1是 */ + hasNetworkPort?: number | null, + /** 位置 */ + location?: string | null, + /** IP */ + ip?: string | null, createTime:string, updateTime:string abnormalRecordList:[], @@ -78,7 +86,47 @@ export interface DeviceRealtimeVO { workshopName?: string } +export interface DeviceScheduleItemVO { + id: number + deviceCode?: string + deviceName?: string + sectionName?: string + deviceFlag?: string + deviceFlagLabel?: string + runStatus?: 'busy' | 'idle' | string + runStatusLabel?: string + loadPercent?: number + taskCount?: number +} +export interface DeviceScheduleCapableVO { + sectionId?: number | null + sectionName?: string | null + matchMode?: string + matchHint?: string + devices?: DeviceScheduleItemVO[] +} + +export interface DeviceScheduleTaskVO { + id?: number | string + deviceId?: string + orderNo?: string + materialCode?: string + materialName?: string + processName?: string + operNumber?: number | string | null + quantity?: number | null + assingStatus?: number | null + pauseStatus?: number | null + statusKey?: string + statusText?: string + statusLabel?: string + planStartTime?: string | null + planFinishTime?: string | null + locked?: boolean + isCurrent?: boolean + durationMs?: number +} export interface DocumentEntity { id?:number, @@ -192,5 +240,23 @@ export const deviceApi = { url:`/biz/device/getByid/${deviceId}`, method:"get" }) + }, + + /** 按工序工作中心匹配可执行设备/工位 */ + scheduleCapable(params: { workCenterName?: string | null; departmentName?: string | null }) { + return request({ + url: '/biz/device/schedule-capable', + method: 'get', + params + }) + }, + + /** 指定设备当前未完成任务 */ + scheduleTasks(deviceId: number) { + return request({ + url: '/biz/device/schedule-tasks', + method: 'get', + params: { deviceId } + }) } } \ No newline at end of file diff --git a/src/api/nccode.ts b/src/api/nccode.ts index f433e3b..d34fb1e 100644 --- a/src/api/nccode.ts +++ b/src/api/nccode.ts @@ -102,10 +102,11 @@ export function exportNccode(params:any) { }) } -//加载全部NC代码 -export function loadAllNcCode(id:number) { +// 加载 NC:传设备 id 时按设备查绑定文档;不传则拉全部 NC 代码库 +export function loadAllNcCode(deviceId?: string | number) { + const id = deviceId != null && deviceId !== '' ? `/${deviceId}` : '' return request({ - url: `biz/ncCode/loadAllNcCode/${id}`, + url: `/biz/ncCode/loadAllNcCode${id}`, method: 'get' }) } diff --git a/src/api/orderProject.ts b/src/api/orderProject.ts index d33cf52..988b845 100644 --- a/src/api/orderProject.ts +++ b/src/api/orderProject.ts @@ -93,6 +93,10 @@ export interface KingdeeProcessRoute { qualityInspection?: number | null /** 是否入库 1是 0否(后端:仅末序为 1) */ storageEntry?: number | null + /** 排产指定设备/工位 */ + deviceId?: number | string | null + deviceCode?: string | null + deviceName?: string | null } /** 解析工序名称,兼容旧字段及后端误映射 */ diff --git a/src/views/biz/device/index.vue b/src/views/biz/device/index.vue index cf2b782..e378c40 100644 --- a/src/views/biz/device/index.vue +++ b/src/views/biz/device/index.vue @@ -73,7 +73,7 @@ :data="tableData" :loading="loading" :row-key="(row) => row.id" - :scroll-x="1200" + :scroll-x="1800" v-model:checked-row-keys="selectedIds" />
@@ -96,8 +96,8 @@ - - + + @@ -143,8 +143,23 @@ + + + + + + + + + - + + + + + + + @@ -445,6 +460,10 @@ const defaultFormData: Device = { spec: '', manufacturer: '', manufactureDate: undefined, + systemBrand: '', + hasNetworkPort: 0, + location: '', + ip: '', workshopId: '', remark: '', status: 0, @@ -497,7 +516,24 @@ const columns: DataTableColumns = [ {title: '所属工段', key: 'sectionName'}, {title: '设备型号', key: 'spec'}, {title: '生产厂家', key: 'manufacturer'}, - {title: '出厂日期', key: 'manufactureDate'}, + {title: '系统品牌', key: 'systemBrand', width: 110, + render: (row) => row.systemBrand || '-' + }, + {title: '网口', key: 'hasNetworkPort', width: 80, + render: (row) => { + const yes = Number(row.hasNetworkPort) === 1 + return h(NTag, {type: yes ? 'success' : 'default', size: 'small'}, {default: () => yes ? '是' : '否'}) + } + }, + {title: '出厂日期', key: 'manufactureDate', width: 110, + render: (row) => formatYearMonth(row.manufactureDate) + }, + {title: '位置', key: 'location', width: 120, + render: (row) => row.location || '-' + }, + {title: 'IP', key: 'ip', width: 130, + render: (row) => row.ip || '-' + }, { title: '状态', key: 'status', render: (row) => { @@ -650,11 +686,29 @@ function goRealtimeBoard() { router.push('/biz/device/board') } +function formatYearMonth(val?: string | number | null) { + if (val == null || val === '') return '-' + const s = String(val) + if (/^\d{4}-\d{2}/.test(s)) return s.slice(0, 7) + const d = new Date(typeof val === 'number' ? val : s) + if (Number.isNaN(d.getTime())) return '-' + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}` +} + +function toMonthTimestamp(val: any): number | null { + if (val == null || val === '') return null + if (typeof val === 'number') return val + const d = new Date(String(val).replace(' ', 'T')) + return Number.isNaN(d.getTime()) ? null : d.getTime() +} + // 编辑 function handleEdit(row: Device) { Object.assign(formData, defaultFormData) modalTitle.value = '编辑设备表' Object.assign(formData, row) + formData.manufactureDate = toMonthTimestamp(row.manufactureDate) as any + formData.hasNetworkPort = Number(row.hasNetworkPort) === 1 ? 1 : 0 const userIdList = [] if (row.deviceManager) { let manager = row.deviceManager.split(','); @@ -674,7 +728,9 @@ async function handleSubmit() { const submitData = {...formData} as Device if (typeof submitData.manufactureDate === 'number') { - submitData.manufactureDate = new Date(submitData.manufactureDate).toISOString().slice(0, 19).replace('T', ' ') + const d = new Date(submitData.manufactureDate) + const month = String(d.getMonth() + 1).padStart(2, '0') + submitData.manufactureDate = `${d.getFullYear()}-${month}-01 00:00:00` } if (submitData.id) { await deviceApi.update(submitData) diff --git a/src/views/biz/orderProcessPlan/board.vue b/src/views/biz/orderProcessPlan/board.vue index e84bb04..449d5da 100644 --- a/src/views/biz/orderProcessPlan/board.vue +++ b/src/views/biz/orderProcessPlan/board.vue @@ -378,10 +378,19 @@ 已完成: {{dispatchform.completedQty ?? 0}} + + 已派工: + {{dispatchform.assignedQty ?? 0}} + + + 本次待派: {{dispatchform.remainQty}} + + 待派 = 计划 − 已派工 + @@ -758,6 +767,7 @@ const dispatchLoading = ref(false) const dispatchformRef = ref() const dispatchform = reactive({ id: '', name: '', beginTime: '', endTime: '', quantity: '', sectionId: null as number | null, workshopId: null as number | null, + completedQty: 0, assignedQty: 0, remainQty: 0, list: [{ sectionId:'',deviceId:'', quantity: '', ncId:'' }], }) const dispatchrules = {} @@ -1326,7 +1336,12 @@ function disphand(process: ProcessPlanItemVO, order?: OrderProcessPlanVO) { const planQty = Number(entity.quantity) || 0 const completedQty = Number(process.completedQty) || 0 - const remainQty = Math.max(0, planQty - completedQty) + // 与后端 checkOverProcessNum 一致:可派 = 计划 − 已派工数量(不是计划 − 已完成) + const assignedQty = (process.assignWorkList ?? []).reduce( + (s, a) => s + (Number(a.quantity) || 0), + 0, + ) + const remainQty = Math.max(0, planQty - assignedQty) dispatchmodal.value = true dispatchformRef.value?.restoreValidation() dispatchform.id = entity.id @@ -1337,12 +1352,13 @@ function disphand(process: ProcessPlanItemVO, order?: OrderProcessPlanVO) { // quantity 保持工序计划总数,提交时不能改掉计划数量 dispatchform.quantity = entity.quantity dispatchform.completedQty = completedQty + dispatchform.assignedQty = assignedQty dispatchform.remainQty = remainQty dispatchform.orderItemId = entity.orderItemId dispatchform.sort = entity.sort dispatchform.sectionId = entity.sectionId ?? process.sectionId ?? null dispatchform.workshopId = entity.workshopId ?? process.workshopId ?? order?.workshopId ?? null - dispatchform.list = [{ sectionId: '', deviceId: '', quantity: remainQty+'' || '',ncId:'' }] + dispatchform.list = [{ sectionId: '', deviceId: '', quantity: remainQty > 0 ? String(remainQty) : '', ncId: '' }] } function addpgnum() { @@ -1360,17 +1376,19 @@ function dispatchSubmit() { }) const remainQty = Number(dispatchform.remainQty) if (remainQty <= 0) { - message.warning('当前工序已全部完成,无需再派工') - return + // 计划已派满时仍可能因补料继续派工,超量由后端校验 + message.warning('当前工序计划数量已派满,若因补料继续派工请确认数量') } dispatchformRef.value?.validate((errors: any) => { // Naive UI:有 errors 表示校验失败 if (errors) return + // 补料后可超过「计划−已派」待派数量,超量由后端校验 // if (total !== remainQty) { - // message.error(`派工总数量需等于待派数量 ${remainQty}(计划 ${dispatchform.quantity},已完成 ${dispatchform.completedQty ?? 0})`, { - // duration: 4000, - // }) + // message.error( + // `派工总数量需等于待派数量 ${remainQty}(计划 ${dispatchform.quantity},已派 ${dispatchform.assignedQty ?? 0},已完成 ${dispatchform.completedQty ?? 0})`, + // { duration: 4000 }, + // ) // return // } diff --git a/src/views/biz/orderProcessPlan/components/SplitProcessDialog.vue b/src/views/biz/orderProcessPlan/components/SplitProcessDialog.vue index e35dc05..dc7b25e 100644 --- a/src/views/biz/orderProcessPlan/components/SplitProcessDialog.vue +++ b/src/views/biz/orderProcessPlan/components/SplitProcessDialog.vue @@ -183,7 +183,8 @@ \ No newline at end of file + diff --git a/src/views/biz/orderProject/components/GanttSchedule.vue b/src/views/biz/orderProject/components/GanttSchedule.vue index 71c6ade..ab6b98f 100644 --- a/src/views/biz/orderProject/components/GanttSchedule.vue +++ b/src/views/biz/orderProject/components/GanttSchedule.vue @@ -16,6 +16,8 @@ | 工序 {{ scheduleStats.routeCount }} 道 | + 已分配设备 {{ scheduleStats.assignedCount }} + | 今日 {{ todayLabel }} | 今日时间轴 @@ -24,10 +26,29 @@
排产明细 - 主产品行展示物料编码与名称,子行展示工序号与工序名称 + 点击工序条块打开右侧排产抽屉,选择可执行设备/工位
+ +
+ 未分配 + 待排产 + 已排产 + 生产中 + 暂停 + 已完成 +
+ +
@@ -37,16 +58,23 @@ import { useMessage } from 'naive-ui' import { useRouter } from 'vue-router' import { gantt } from 'dhtmlx-gantt' import 'dhtmlx-gantt/codebase/dhtmlxgantt.css' -import { orderProjectApi, normalizeKingdeeProcessRoute, resolveProcessName, type KingdeePrdMo } from '@/api/orderProject' +import { orderProjectApi, normalizeKingdeeProcessRoute, resolveProcessName, type KingdeePrdMo, type KingdeeProcessRoute } from '@/api/orderProject' +import ProcessScheduleDrawer, { type DraftAssignedItem, type ScheduleSavePayload } from './ProcessScheduleDrawer.vue' const router = useRouter() const ganttContainer = ref(null) const message = useMessage() let ganttEventIds: string[] = [] +let dragEventIds: string[] = [] const tableData = ref([]) const projectId = ref(null) const saving = ref(false) +const drawerShow = ref(false) +const selectedProcess = ref(null) +const selectedOrder = ref(null) +const selectedMoIndex = ref(0) +const selectedRouteIndex = ref(0) const PROCESS_COLORS = [ '#409eff', '#67c23a', '#e6a23c', '#909399', '#f56c6c', @@ -69,15 +97,39 @@ function formatDayScale(date: Date) { const scheduleStats = computed(() => { let routeCount = 0 + let assignedCount = 0 tableData.value.forEach((mo) => { - routeCount += mo.processRoutes?.length ?? 0 + const routes = mo.processRoutes ?? [] + routeCount += routes.length + assignedCount += routes.filter((r: any) => r.deviceId).length }) return { moCount: tableData.value.length, routeCount, + assignedCount, } }) +const draftAssigned = computed(() => { + const list: DraftAssignedItem[] = [] + tableData.value.forEach((mo, moIndex) => { + (mo.processRoutes ?? []).forEach((route: any, routeIndex: number) => { + if (!route.deviceId) return + list.push({ + moIndex, + routeIndex, + orderNo: mo.billNo || mo.productionOrderNo || '', + processName: resolveProcessName(route), + quantity: mo.quantity, + deviceId: route.deviceId, + planStartTime: route.planStartTime, + planFinishTime: route.planFinishTime, + }) + }) + }) + return list +}) + const todayLabel = computed(() => { const d = new Date() const pad = (n: number) => String(n).padStart(2, '0') @@ -88,6 +140,58 @@ function handleBack() { router.back() } +function openProcessDrawer(task: any) { + selectedProcess.value = task?._raw || null + selectedMoIndex.value = Number(task?._moIndex ?? 0) + selectedRouteIndex.value = Number(task?._routeIndex ?? 0) + const parent = task?.parent ? gantt.getTask(task.parent) : null + selectedOrder.value = parent?._raw || null + drawerShow.value = true +} + +function persistDraftLocal() { + sessionStorage.setItem('gantt_schedule_data', JSON.stringify(tableData.value)) +} + +function handleScheduleSave(payload: ScheduleSavePayload) { + const mo = tableData.value[payload.moIndex] + const route = mo?.processRoutes?.[payload.routeIndex] + if (!route) { + message.warning('未找到当前工序,无法保存排产') + return + } + route.deviceId = payload.deviceId + route.deviceCode = payload.deviceCode + route.deviceName = payload.deviceName + route.planStartTime = payload.planStartTime + route.planFinishTime = payload.planFinishTime + + payload.draftUpdates.forEach((item) => { + const other = tableData.value[item.moIndex]?.processRoutes?.[item.routeIndex] + if (!other) return + other.planStartTime = item.planStartTime + other.planFinishTime = item.planFinishTime + }) + + if (mo.processRoutes?.length) { + const starts = mo.processRoutes + .map((r: any) => r.planStartTime) + .filter(Boolean) + .sort() + const ends = mo.processRoutes + .map((r: any) => r.planFinishTime) + .filter(Boolean) + .sort() + if (starts.length) mo.planStartTime = starts[0] + if (ends.length) mo.planFinishTime = ends[ends.length - 1] + } + + drawerShow.value = false + persistDraftLocal() + renderGanttData() + message.success(`已将工序排到 ${payload.deviceName || payload.deviceCode},请再点击「保存排产草稿」落库`) +} + async function handleSaveDraft() { if (!projectId.value) { message.warning('缺少项目ID,无法保存') @@ -213,15 +317,35 @@ function initGantt() { return formatGridDate(task.end_date) }, }, + { + name: 'scheduleStatus', + label: '状态', + align: 'center', + width: 80, + template(task: any) { + if (task._type !== 'route') return '' + return task._raw?.deviceId ? '已排产' : '待排产' + }, + }, + { + name: 'deviceName', + label: '设备/工位', + align: 'center', + width: 120, + template(task: any) { + if (task._type !== 'route') return '' + return task._raw?.deviceName || '未分配' + }, + }, ] gantt.config.layout = { css: 'gantt_container', cols: [ { - width: 620, - minWidth: 420, - maxWidth: 680, + width: 820, + minWidth: 520, + maxWidth: 960, rows: [ { view: 'grid', scrollX: 'gridScroll', scrollable: true, scrollY: 'scrollVer' }, { view: 'scrollbar', id: 'gridScroll', group: 'horizontal' }, @@ -240,6 +364,8 @@ function initGantt() { gantt.config.drag_progress = false gantt.config.drag_links = false + gantt.config.details_on_dblclick = false + gantt.config.details_on_create = false // 使用自然日历(含周六日),不按工作日历自动顺延 gantt.config.work_time = false gantt.config.correct_work_time = false @@ -260,7 +386,8 @@ function initGantt() { gantt.templates.task_class = function(_start, _end, task) { if (task._type === 'mo') return 'gantt-task-mo' if (task._type === 'route') { - return `gantt-task-route gantt-route-c-${task.colorIndex ?? 0}` + const assigned = !!task._raw?.deviceId + return `gantt-task-route gantt-route-c-${task.colorIndex ?? 0}${assigned ? '' : ' gantt-task-unassigned'}` } return '' } @@ -280,6 +407,18 @@ function initGantt() { } gantt.init(ganttContainer.value) + + const evClick = gantt.attachEvent('onTaskClick', (id: string | number) => { + const task = gantt.getTask(id) + if (task?._type === 'route') { + openProcessDrawer(task) + } + return true + }) + ganttEventIds.push(evClick) + + const evDbl = gantt.attachEvent('onTaskDblClick', () => false) + ganttEventIds.push(evDbl) } /** 今日时间轴标记(数据刷新后需重新添加) */ @@ -357,6 +496,8 @@ function renderGanttData() { end_date: formatGanttDate(rEnd), parent: moId, _type: 'route', + _moIndex: moIndex, + _routeIndex: index, progress: 0, _raw: route, }) @@ -367,11 +508,14 @@ function renderGanttData() { gantt.parse({ data: tasks, links: [] }) addTodayMarker() + dragEventIds.forEach(id => gantt.detachEvent(id)) + dragEventIds = [] + const evDragId = gantt.attachEvent('onBeforeTaskDrag', (id) => { const task = gantt.getTask(id) return task.type !== gantt.config.types.project }) - ganttEventIds.push(evDragId) + dragEventIds.push(evDragId) const evId = gantt.attachEvent('onAfterTaskDrag', (id, mode) => { const task = gantt.getTask(id) @@ -417,11 +561,12 @@ function renderGanttData() { message.success(`任务 [${label}] 计划时间已更新`) } }) - ganttEventIds.push(evId) + dragEventIds.push(evId) } onUnmounted(() => { ganttEventIds.forEach(id => gantt.detachEvent(id)) + dragEventIds.forEach(id => gantt.detachEvent(id)) gantt.clearAll() }) @@ -604,10 +749,53 @@ onUnmounted(() => { } .gantt-container :deep(.gantt_task_line.gantt-task-route) { - border: none !important; border-radius: 3px; } +.gantt-container :deep(.gantt_task_line.gantt-task-unassigned) { + background: #fff !important; + border: 1px dashed #c0c4cc !important; +} + +.gantt-container :deep(.gantt_task_line.gantt-task-unassigned .gantt_task_content) { + color: #86909c !important; +} + +.gantt-legend { + flex-shrink: 0; + display: flex; + flex-wrap: wrap; + gap: 14px; + margin-top: 10px; + padding: 0 4px; + font-size: 12px; + color: #606266; +} + +.legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.legend-item .lg { + display: inline-block; + width: 14px; + height: 10px; + border-radius: 2px; +} + +.legend-item .lg.unassigned { + background: #fff; + border: 1px dashed #c0c4cc; +} + +.legend-item .lg.pending { background: #c0c4cc; } +.legend-item .lg.assigned { background: #2080f0; } +.legend-item .lg.producing { background: #18a058; } +.legend-item .lg.paused { background: #f0a020; } +.legend-item .lg.done { background: #8b5cf6; } + .gantt-container :deep(.gantt_grid_data .gantt_cell) { font-size: 13px; font-weight: 400; diff --git a/src/views/biz/orderProject/components/ProcessScheduleDrawer.vue b/src/views/biz/orderProject/components/ProcessScheduleDrawer.vue new file mode 100644 index 0000000..586b2f9 --- /dev/null +++ b/src/views/biz/orderProject/components/ProcessScheduleDrawer.vue @@ -0,0 +1,783 @@ + + + + + diff --git a/vite.config.ts b/vite.config.ts index 3dbf376..c9f96ee 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -16,7 +16,7 @@ export default defineConfig({ emptyOutDir: true }, server: { - //host:'192.168.12.4', //true, + host: true, // 监听 0.0.0.0,避免本机 IP 变化导致 EADDRNOTAVAIL port: 3000, proxy: { '/api': {