/** * APS 公共脚本:布局、交互、图表、模拟排产引擎 */ (function () { const $ = (sel, ctx) => (ctx || document).querySelector(sel); const $$ = (sel, ctx) => Array.from((ctx || document).querySelectorAll(sel)); window.$ = $; window.$$ = $$; // -------------- 页面初始化 -------------- window.initPage = function (key) { renderHeader(); renderSidebar(key); ensureModal(); ensureToast(); }; function renderHeader() { const header = $('#header'); if (!header) return; header.innerHTML = `
${msg}
`, ` `); }; // -------------- 状态标签 -------------- window.statusBadge = function (status, map) { const cls = (map && map[status]) || 'default'; const label = (window.STATUS_LABELS && window.STATUS_LABELS[status]) || status; return `${label}`; }; window.STATUS_LABELS = { DRAFT: '草稿', SUBMITTED: '待审核', CONFIRMED: '已确认', REJECTED: '已退回', IN_PRODUCTION: '生产中', COMPLETED: '已完成', CANCELLED: '已取消', PENDING: '待排产', ISSUED: '已下发', PAUSED: '已暂停', CLOSED: '已关闭', PASSED: '通过', PARTIAL: '部分', FAILED: '失败', FULFILLED: '齐套', SHORTAGE: '缺料', CRITICAL: '致命', MAJOR: '严重', MINOR: '警告', RESOLVED: '已解决', RULE: '规则引擎', CP: '约束规划', GA: '遗传算法', HYBRID: '混合引擎', RUNNING: '运行中', IDLE: '待机', FAULT: '故障', MAINTENANCE: '保养', OFFLINE: '离线' }; // -------------- 查找器 -------------- window.findById = function (arr, id) { return arr.find(x => String(x.id) === String(id)); }; window.findLineWorkstations = function (lineId) { return apsData.workstations.filter(w => w.lineId == lineId).sort((a, b) => a.sequenceNo - b.sequenceNo); }; window.findRoutingSteps = function (productId) { const routing = apsData.routings.find(r => r.productId == productId && r.isDefault); if (!routing) return []; return apsData.routingSteps.filter(s => s.routingId == routing.id).sort((a, b) => a.sequenceNo - b.sequenceNo); }; window.findBomItems = function (productId) { const bom = apsData.boms.find(b => b.productId == productId && b.isDefault); if (!bom) return []; return apsData.bomItems.filter(i => i.bomId == bom.id); }; window.findProductLines = function (productId) { return apsData.lineProducts.filter(lp => lp.productId == productId).sort((a, b) => a.priority - b.priority); }; window.findWorkstationForOperation = function (lineId, operationId) { const wsIds = apsData.workstationOperations.filter(wo => wo.operationId == operationId).map(wo => wo.workstationId); return apsData.workstations.find(ws => ws.lineId == lineId && wsIds.includes(ws.id)); }; window.getShiftMinutes = function (shiftId, dateStr) { const shift = apsData.shifts.find(s => s.id == shiftId); if (!shift) return 0; const [sh, sm] = shift.startTime.split(':').map(Number); const [eh, em] = shift.endTime.split(':').map(Number); let total = (eh * 60 + em) - (sh * 60 + sm); (shift.breakPeriods || []).forEach(bp => { const [bsh, bsm] = bp.start.split(':').map(Number); const [beh, bem] = bp.end.split(':').map(Number); total -= (beh * 60 + bem) - (bsh * 60 + bsm); }); return Math.max(total, 0); }; window.getLineShifts = function (lineId, dateStr) { return apsData.shiftCalendar.filter(sc => sc.lineId == lineId && sc.date == dateStr && sc.isWorking).map(sc => apsData.shifts.find(s => s.id == sc.shiftId)).filter(Boolean); }; window.getAvailableMinutes = function (lineId, dateStr) { return getLineShifts(lineId, dateStr).reduce((sum, s) => sum + getShiftMinutes(s.id, dateStr), 0); }; // -------------- 图表 -------------- function setupCanvas(canvasId) { const c = document.getElementById(canvasId); if (!c) return null; const rect = c.getBoundingClientRect(); c.width = rect.width * 2; c.height = rect.height * 2; const ctx = c.getContext('2d'); ctx.scale(2, 2); return { c, ctx, w: rect.width, h: rect.height }; } window.drawBarChart = function (canvasId, labels, data, color = '#f97316') { const o = setupCanvas(canvasId); if (!o) return; const { ctx, w, h } = o; ctx.clearRect(0, 0, w, h); const pad = 28, bh = h - pad - 10, bw = (w - pad * 2) / labels.length * 0.6; const max = Math.max(...data, 1); labels.forEach((l, i) => { const val = data[i]; const barH = (val / max) * (bh - 20); const x = pad + i * ((w - pad * 2) / labels.length) + bw * 0.33; const y = bh - barH; ctx.fillStyle = color; ctx.fillRect(x, y, bw, barH); ctx.fillStyle = '#374151'; ctx.font = '11px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(val, x + bw / 2, y - 5); ctx.fillStyle = '#6b7280'; ctx.fillText(l, x + bw / 2, h - 6); }); }; window.drawLineChart = function (canvasId, labels, series) { const o = setupCanvas(canvasId); if (!o) return; const { ctx, w, h } = o; ctx.clearRect(0, 0, w, h); const pad = 28, max = Math.max(...series.flat(), 1); const stepX = (w - pad * 2) / (labels.length - 1 || 1); ctx.strokeStyle = '#e5e7eb'; for (let i = 0; i < 5; i++) { const y = pad + i * ((h - pad * 2) / 4); ctx.beginPath(); ctx.moveTo(pad, y); ctx.lineTo(w - pad, y); ctx.stroke(); } const colors = ['#f97316', '#16a34a', '#0ea5e9', '#eab308']; series.forEach((data, idx) => { ctx.strokeStyle = colors[idx % colors.length]; ctx.lineWidth = 2; ctx.beginPath(); data.forEach((v, i) => { const x = pad + i * stepX; const y = h - pad - (v / max) * (h - pad * 2); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); }); ctx.fillStyle = '#6b7280'; ctx.textAlign = 'center'; labels.forEach((l, i) => ctx.fillText(l, pad + i * stepX, h - 8)); }; window.drawPieChart = function (canvasId, data) { const o = setupCanvas(canvasId); if (!o) return; const { ctx, w, h } = o; ctx.clearRect(0, 0, w, h); const total = data.reduce((a, b) => a + b.value, 0); const cx = w / 2 - 60, cy = h / 2, r = Math.min(w, h) / 2 - 30; let start = -Math.PI / 2; data.forEach(d => { const ang = (d.value / total) * Math.PI * 2; ctx.fillStyle = d.color; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.arc(cx, cy, r, start, start + ang); ctx.closePath(); ctx.fill(); start += ang; }); let ly = 30; ctx.textAlign = 'left'; data.forEach(d => { ctx.fillStyle = d.color; ctx.fillRect(cx + r + 30, ly - 8, 12, 12); ctx.fillStyle = '#374151'; ctx.font = '12px sans-serif'; ctx.fillText(`${d.label} ${d.value}`, cx + r + 48, ly + 2); ly += 22; }); }; // -------------- 排产引擎 -------------- /** * params: { orderIds, engineType, strategyTemplate, planningHorizonDays, startDate, constraints, onProgress } */ window.runScheduling = function (params, callback) { const { orderIds, engineType, strategyTemplate, planningHorizonDays, startDate, constraints, onProgress } = params; const allItems = []; apsData.salesOrders.forEach(so => { if (orderIds && orderIds.length && !orderIds.includes(so.id)) return; if (so.status === 'CANCELLED' || so.status === 'COMPLETED') return; so.items.forEach(item => { if (item.status === 'COMPLETED') return; allItems.push({ so, item }); }); }); // 排序规则 const sortFn = (a, b) => { if (strategyTemplate === 'DELIVERY_FIRST') { return a.so.deliveryDate.localeCompare(b.so.deliveryDate) || a.so.priority - b.so.priority; } if (strategyTemplate === 'FIFO') return a.so.orderDate.localeCompare(b.so.orderDate); if (strategyTemplate === 'CAPACITY_BALANCE') return a.so.priority - b.so.priority; if (strategyTemplate === 'COST_FIRST') return a.so.priority - b.so.priority; return a.so.priority - b.so.priority || a.so.deliveryDate.localeCompare(b.so.deliveryDate); }; allItems.sort(sortFn); const version = { id: apsData.$utils.nextId('scheduleVersion'), versionNo: 'V' + apsData.$utils.fmtDate(new Date()).replace(/-/g, '') + '-' + String(apsData.scheduleVersions.length + 1).padStart(3, '0'), versionName: params.name || ('手动排产 ' + apsData.$utils.fmtDateTime(new Date())), triggerType: params.triggerType || 'MANUAL', engineType: engineType || 'RULE', status: 'DRAFT', parentVersionId: apsData.scheduleVersions.length ? apsData.scheduleVersions[apsData.scheduleVersions.length - 1].id : null, orderCount: allItems.length, poCount: 0, woCount: 0, totalTardiness: 0, totalCost: 0, avgUtilization: 0, conflictCount: 0, resolvedCount: 0, createdBy: 'admin', createdAt: apsData.$utils.fmtDateTime(new Date()), publishedAt: null, note: params.note || '' }; apsData.scheduleVersions.push(version); // 生成生产订单与工单 const horizon = planningHorizonDays || 30; const baseStart = startDate ? apsData.$utils.parseDate(startDate + ' 08:00') : apsData.$utils.addMinutes(apsData.$utils.today(), 24 * 60); const usedWorkstations = {}; // wsId -> [{start,end}] const usedLineMinutes = {}; // lineId_date -> minutes const conflicts = []; allItems.forEach(({ so, item }) => { const product = apsData.materials.find(m => m.id == item.productId); const lineOptions = findProductLines(item.productId); if (!lineOptions.length) { conflicts.push({ conflictType: 'NO_LINE', severity: 'CRITICAL', productionOrderId: null, resourceType: 'LINE', description: `产品 ${product.name} 无可用产线配置`, suggestedSolution: '维护产线产品配置' }); return; } // 选择产线 let chosenLineId = lineOptions[0].lineId; if (strategyTemplate === 'CAPACITY_BALANCE') { chosenLineId = lineOptions.sort((a, b) => (usedLineMinutes[a.lineId] || 0) - (usedLineMinutes[b.lineId] || 0))[0].lineId; } const line = apsData.lines.find(l => l.id == chosenLineId); const wsList = findRoutingSteps(item.productId).map(step => { const ws = findWorkstationForOperation(chosenLineId, step.operationId); return { step, ws }; }); if (wsList.some(x => !x.ws)) { conflicts.push({ conflictType: 'NO_WORKSTATION', severity: 'CRITICAL', productionOrderId: null, resourceType: 'WORKSTATION', description: `产品 ${product.name} 在产线 ${line.name} 缺少可用工位`, suggestedSolution: '维护工位工序配置' }); return; } // 物料齐套时间 let materialReady = baseStart; if (constraints && constraints.materialKit !== false) { const bomItems = findBomItems(item.productId); bomItems.forEach(bi => { const mat = apsData.materials.find(m => m.id == bi.materialId); const need = bi.quantity * item.quantity; if (mat.stock < need && mat.stock + mat.inTransit >= need) { const eta = apsData.$utils.addMinutes(apsData.$utils.today(), 3 * 24 * 60); if (eta > materialReady) materialReady = eta; } else if (mat.stock + mat.inTransit < need) { conflicts.push({ conflictType: 'MATERIAL_SHORTAGE', severity: 'MAJOR', resourceType: 'MATERIAL', resourceName: mat.name, description: `${so.orderNo} ${mat.name} 缺料 ${Math.ceil(need - mat.stock - mat.inTransit)} ${mat.unit}`, suggestedSolution: '紧急采购或启用替代物料' }); } }); } // 逐个工序排产 let cursor = new Date(Math.max(baseStart.getTime(), materialReady.getTime())); const poStart = cursor; let poEnd = cursor; const productionOrder = { id: apsData.$utils.nextId('productionOrder'), orderNo: 'PO' + so.orderNo.replace(/SO/, ''), salesOrderId: so.id, salesOrderNo: so.orderNo, salesOrderItemId: item.id, productId: item.productId, productName: product.name, productCode: product.code, quantity: item.quantity, unit: item.unit, plannedStartDate: null, plannedEndDate: null, lineId: line.id, lineName: line.name, status: 'DRAFT', priority: so.priority, schedulingEngine: engineType || 'RULE', schedulingVersionId: version.id, optimizationScore: 0, materialKitStatus: 'PASSED', constraintCheckStatus: 'PASSED', conflictCount: 0, isRushOrder: so.isRush, rushStrategy: so.rushStrategy }; apsData.productionOrders.push(productionOrder); version.poCount++; wsList.forEach(({ step, ws }, idx) => { const op = apsData.operations.find(o => o.id == step.operationId); const durationMin = step.setupTime + (item.quantity * step.runTimePerUnit) / (line.efficiencyFactor || 1); const placed = placeWorkOrder(cursor, chosenLineId, ws, durationMin, usedWorkstations, usedLineMinutes, productionOrder, apsData.$utils, constraints); const wo = { id: apsData.$utils.nextId('workOrder'), orderNo: productionOrder.orderNo + '-' + String(idx + 1).padStart(2, '0'), productionOrderId: productionOrder.id, productionOrderNo: productionOrder.orderNo, operationId: op.id, operationName: op.name, sequenceNo: step.sequenceNo, productId: item.productId, productName: product.name, quantity: item.quantity, unit: item.unit, completedQuantity: 0, lineId: line.id, lineName: line.name, workstationId: ws.id, workstationName: ws.name, plannedStartTime: apsData.$utils.fmtDateTime(placed.start), plannedEndTime: apsData.$utils.fmtDateTime(placed.end), status: 'PENDING', teamId: null, teamName: null, requiredMaterials: [], requiredEquipment: [], priority: so.priority, isFrozen: false, kitStatus: 'PASSED', progressPercent: 0, conflictCount: 0, note: '' }; // 物料需求 const bomItems = findBomItems(item.productId).filter(bi => bi.operationId == op.id); bomItems.forEach(bi => { const mat = apsData.materials.find(m => m.id == bi.materialId); const need = bi.quantity * item.quantity; let ks = 'PASSED'; if (mat.stock < need) { if (mat.stock + mat.inTransit >= need) ks = 'PARTIAL'; else ks = 'FAILED'; } if (ks !== 'PASSED' && wo.kitStatus === 'PASSED') wo.kitStatus = ks; else if (ks === 'FAILED') wo.kitStatus = 'FAILED'; wo.requiredMaterials.push({ materialId: mat.id, name: mat.name, required: need, allocated: Math.min(need, mat.stock), available: mat.stock, status: ks }); }); if (wo.kitStatus !== 'PASSED') productionOrder.materialKitStatus = wo.kitStatus; apsData.workOrders.push(wo); version.woCount++; cursor = apsData.$utils.addMinutes(placed.end, step.transferTime + step.waitTime); poEnd = placed.end; }); productionOrder.plannedStartDate = apsData.$utils.fmtDateTime(poStart); productionOrder.plannedEndDate = apsData.$utils.fmtDateTime(poEnd); const due = apsData.$utils.parseDate(so.deliveryDate + ' 18:00'); if (poEnd > due) { const hoursLate = (poEnd - due) / 3600000; version.totalTardiness += hoursLate; conflicts.push({ conflictType: 'DELAY', severity: 'MAJOR', productionOrderId: productionOrder.id, resourceType: 'TIME', description: `${productionOrder.orderNo} 完成时间晚于交期 ${hoursLate.toFixed(1)} 小时`, suggestedSolution: '启用加班/替代产线/压缩准备时间' }); } }); // 容量冲突检测 apsData.lines.forEach(line => { const loads = {}; apsData.workOrders.filter(wo => wo.lineId == line.id && !wo.isFrozen).forEach(wo => { const d = apsData.$utils.fmtDate(wo.plannedStartTime); const dur = (apsData.$utils.parseDate(wo.plannedEndTime) - apsData.$utils.parseDate(wo.plannedStartTime)) / 60000; loads[d] = (loads[d] || 0) + dur; }); Object.entries(loads).forEach(([d, min]) => { const avail = getAvailableMinutes(line.id, d); if (min > avail * 1.05) { conflicts.push({ conflictType: 'CAPACITY', severity: 'CRITICAL', resourceType: 'LINE', resourceName: line.name, conflictTimeStart: d + ' 08:00', description: `${line.name} ${d} 负荷 ${Math.round(min)} 分钟,超出可用 ${Math.round(avail)} 分钟`, suggestedSolution: '分流至替代产线或启用加班' }); } }); }); // 设备维保冲突 apsData.maintenance.forEach(m => { apsData.workOrders.forEach(wo => { const eq = apsData.equipment.find(e => e.workstationId == wo.workstationId); if (!eq || eq.id != m.equipmentId) return; const ws = apsData.$utils.parseDate(wo.plannedStartTime); const we = apsData.$utils.parseDate(wo.plannedEndTime); const ms = apsData.$utils.parseDate(m.plannedStart); const me = apsData.$utils.parseDate(m.plannedEnd); if (ws < me && we > ms) { conflicts.push({ conflictType: 'EQUIPMENT', severity: 'CRITICAL', workOrderId: wo.id, resourceType: 'EQUIPMENT', resourceName: eq.name, description: `${wo.orderNo} 与设备 ${eq.name} 维保时间冲突`, suggestedSolution: '调整工单时间或启用替代设备' }); } }); }); // 冲突入库 conflicts.forEach(cf => { cf.id = apsData.$utils.nextId('conflict'); cf.versionId = version.id; cf.isResolved = false; cf.resolutionAction = ''; cf.resolvedBy = ''; apsData.conflicts.push(cf); }); version.conflictCount = conflicts.length; // 更新关联工单冲突计数与 PO 状态 apsData.productionOrders.filter(po => po.schedulingVersionId == version.id).forEach(po => { po.conflictCount = apsData.conflicts.filter(c => c.productionOrderId == po.id || (c.workOrderId && apsData.workOrders.find(wo => wo.id == c.workOrderId)?.productionOrderId == po.id)).length; if (po.conflictCount > 0) po.constraintCheckStatus = 'FAILED'; else if (po.materialKitStatus !== 'PASSED') po.constraintCheckStatus = 'WARNING'; }); // 统计利用率 const lineUtil = {}; apsData.workOrders.filter(wo => !wo.isFrozen).forEach(wo => { const d = apsData.$utils.fmtDate(wo.plannedStartTime); const dur = (apsData.$utils.parseDate(wo.plannedEndTime) - apsData.$utils.parseDate(wo.plannedStartTime)) / 60000; lineUtil[wo.lineId] = lineUtil[wo.lineId] || { used: 0, avail: 0 }; lineUtil[wo.lineId].used += dur; }); apsData.lines.forEach(line => { for (let i = 0; i < horizon; i++) { const d = apsData.$utils.fmtDate(apsData.$utils.addMinutes(baseStart, i * 24 * 60)); if (!lineUtil[line.id]) lineUtil[line.id] = { used: 0, avail: 0 }; lineUtil[line.id].avail += getAvailableMinutes(line.id, d); } }); const utilVals = Object.values(lineUtil).map(x => x.avail ? x.used / x.avail : 0); version.avgUtilization = utilVals.length ? utilVals.reduce((a, b) => a + b, 0) / utilVals.length : 0; version.totalCost = version.woCount * 120 + version.totalTardiness * 50; // 日志 apsData.logs.unshift({ id: apsData.$utils.nextId('log'), type: 'INFO', category: 'SCHEDULE', operationType: 'SCHEDULE', targetType: 'SCHEDULE_VERSION', targetId: version.id, action: '执行排产', description: `版本 ${version.versionNo} 生成 ${version.poCount} 个生产订单 / ${version.woCount} 个工单,冲突 ${version.conflictCount}`, operator: 'admin', createdAt: apsData.$utils.fmtDateTime(new Date()) }); apsData.$utils.save(apsData); if (onProgress) onProgress(100, 'COMPLETED'); if (callback) callback(version); return version; }; function placeWorkOrder(cursor, lineId, ws, durationMin, usedWorkstations, usedLineMinutes, productionOrder, utils, constraints) { const maxDays = 30; for (let dayOffset = 0; dayOffset < maxDays; dayOffset++) { const day = utils.addMinutes(cursor, dayOffset * 24 * 60); const dateStr = utils.fmtDate(day); const shifts = getLineShifts(lineId, dateStr).filter(s => s); if (!shifts.length) continue; for (const shift of shifts) { const [sh, sm] = shift.startTime.split(':').map(Number); let candidate = utils.parseDate(dateStr + ' ' + String(sh).padStart(2, '0') + ':' + String(sm).padStart(2, '0')); if (candidate < cursor && dayOffset === 0) candidate = new Date(cursor); const [eh, em] = shift.endTime.split(':').map(Number); const shiftEnd = utils.parseDate(dateStr + ' ' + String(eh).padStart(2, '0') + ':' + String(em).padStart(2, '0')); const availableEnd = new Date(Math.min(shiftEnd.getTime(), candidate.getTime() + durationMin * 60000 + 24 * 60 * 60000)); const start = findSlot(candidate, availableEnd, ws.id, durationMin, usedWorkstations); if (start) { const end = utils.addMinutes(start, durationMin); if (end > shiftEnd) continue; pushInterval(usedWorkstations, ws.id, start, end); const key = `${lineId}_${dateStr}`; usedLineMinutes[key] = (usedLineMinutes[key] || 0) + durationMin; return { start, end }; } } } // fallback: 放在 cursor return { start: cursor, end: utils.addMinutes(cursor, durationMin) }; } function findSlot(from, toEnd, wsId, durationMin, usedWorkstations) { let t = new Date(from); const intervals = usedWorkstations[wsId] || []; while (t <= toEnd) { const end = apsData.$utils.addMinutes(t, durationMin); const overlap = intervals.some(iv => t < iv.end && end > iv.start); if (!overlap) return t; t = apsData.$utils.addMinutes(t, 15); } return null; } function pushInterval(map, wsId, start, end) { map[wsId] = map[wsId] || []; map[wsId].push({ start, end }); } // -------------- 拖拽校验 -------------- window.checkMoveConflict = function (workOrder, newStartStr) { const newStart = apsData.$utils.parseDate(newStartStr); const dur = (apsData.$utils.parseDate(workOrder.plannedEndTime) - apsData.$utils.parseDate(workOrder.plannedStartTime)) / 60000; const newEnd = apsData.$utils.addMinutes(newStart, dur); const conflicts = []; const dateStr = apsData.$utils.fmtDate(newStart); const shifts = getLineShifts(workOrder.lineId, dateStr); if (!shifts.length) conflicts.push({ type: 'NO_SHIFT', severity: 'CRITICAL', message: '目标日期无工作班次' }); // 设备维保 const eq = apsData.equipment.find(e => e.workstationId == workOrder.workstationId); if (eq) { apsData.maintenance.filter(m => m.equipmentId == eq.id).forEach(m => { const ms = apsData.$utils.parseDate(m.plannedStart), me = apsData.$utils.parseDate(m.plannedEnd); if (newStart < me && newEnd > ms) conflicts.push({ type: 'EQUIPMENT', severity: 'CRITICAL', message: `与设备维保 ${m.description} 冲突` }); }); } // 时间重叠 const overlaps = apsData.workOrders.filter(wo => wo.id != workOrder.id && wo.workstationId == workOrder.workstationId && wo.status !== 'CANCELLED') .some(wo => { const ws = apsData.$utils.parseDate(wo.plannedStartTime), we = apsData.$utils.parseDate(wo.plannedEndTime); return newStart < we && newEnd > ws; }); if (overlaps) conflicts.push({ type: 'OVERLAP', severity: 'CRITICAL', message: '目标时段与其他工单时间重叠' }); // 容量 const line = apsData.lines.find(l => l.id == workOrder.lineId); const avail = getAvailableMinutes(line.id, dateStr); if (dur > avail) conflicts.push({ type: 'CAPACITY', severity: 'MAJOR', message: `${line.name} 当日可用产能不足` }); return { canApply: !conflicts.length, conflicts, newEnd: apsData.$utils.fmtDateTime(newEnd) }; }; // -------------- 全局工具 -------------- window.fmtDuration = function (min) { if (min < 60) return Math.round(min) + '分钟'; const h = Math.floor(min / 60); const m = Math.round(min % 60); return m ? `${h}小时${m}分` : `${h}小时`; }; window.renderPagination = function (containerId, page, total, pageSize, onClick) { const c = $('#' + containerId); if (!c) return; const pages = Math.max(1, Math.ceil(total / pageSize)); let html = ``; for (let i = 1; i <= pages; i++) { if (i === 1 || i === pages || Math.abs(i - page) <= 1) { html += ``; } else if (Math.abs(i - page) === 2) html += `…`; } html += ``; c.innerHTML = html; }; // 首次访问时自动执行一次排产,生成初始测试效果 if (apsData.scheduleVersions.length === 0) { const tomorrow = apsData.$utils.fmtDate(apsData.$utils.addMinutes(apsData.$utils.today(), 24 * 60)); runScheduling({ orderIds: [], engineType: 'HYBRID', strategyTemplate: 'COMPREHENSIVE', planningHorizonDays: 14, startDate: tomorrow, constraints: { materialKit: true, equipment: true, personnel: true, changeover: true }, triggerType: 'AUTO_ROLLING', name: '初始自动排产' }); } })();