166 lines
10 KiB
HTML
166 lines
10 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>排产策略 - 智能排产系统</title>
|
||
<link rel="stylesheet" href="../css/common.css">
|
||
</head>
|
||
<body>
|
||
<div id="app">
|
||
<header class="header" id="header"></header>
|
||
<div class="body">
|
||
<aside class="sidebar" id="sidebar"></aside>
|
||
<main class="main">
|
||
<h1 class="page-title">排产策略</h1>
|
||
<div class="row">
|
||
<div class="col-4">
|
||
<div class="card">
|
||
<div class="card-header">排产参数</div>
|
||
<div class="card-body">
|
||
<div class="form-group"><label>排产引擎</label><select id="engineType"><option value="RULE">规则引擎(快速)</option><option value="CP">约束规划 CP-SAT</option><option value="GA">遗传算法 GA</option><option value="HYBRID" selected>混合模式</option></select></div>
|
||
<div class="form-group"><label>策略模板</label><select id="strategyTemplate"><option value="DELIVERY_FIRST">交期优先</option><option value="CAPACITY_BALANCE">产能均衡</option><option value="COST_FIRST">成本最优</option><option value="FAST_RESPONSE">快速响应</option><option value="COMPREHENSIVE" selected>综合优化</option></select></div>
|
||
<div class="form-group"><label>计划展望期(天)</label><input id="horizon" type="number" value="14"></div>
|
||
<div class="form-group"><label>排产起始日</label><input id="startDate" type="date"></div>
|
||
<div class="form-group"><label>目标订单</label><select id="orderScope"><option value="">全部待排产订单</option><option value="selected">仅已选订单</option></select></div>
|
||
<div class="form-group"><label>约束检查</label><div><label style="font-weight:400"><input type="checkbox" id="ckMaterial" checked> 物料齐套</label></div><div><label style="font-weight:400"><input type="checkbox" id="ckEquipment" checked> 设备可用</label></div><div><label style="font-weight:400"><input type="checkbox" id="ckPersonnel" checked> 人员班次</label></div><div><label style="font-weight:400"><input type="checkbox" id="ckChangeover" checked> 换线时间</label></div></div>
|
||
<button class="btn btn-primary w-full" onclick="startScheduling()" id="runBtn">开始排产</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-8">
|
||
<div class="card" id="progressCard" style="display:none">
|
||
<div class="card-header">排产进度</div>
|
||
<div class="card-body">
|
||
<div class="flex justify-between mb-2"><span id="phaseText">初始化...</span><span id="progressText">0%</span></div>
|
||
<div class="progress"><div class="progress-bar" id="progressBar" style="width:0%"></div></div>
|
||
<div class="mt-3" id="engineLog" style="font-family:monospace;font-size:12px;color:#6b7280;max-height:160px;overflow:auto"></div>
|
||
</div>
|
||
</div>
|
||
<div class="card" id="resultCard" style="display:none">
|
||
<div class="card-header">排产结果</div>
|
||
<div class="card-body">
|
||
<div class="row">
|
||
<div class="col-3"><div class="kpi-value" id="r_orders">0</div><div class="kpi-label">订单数</div></div>
|
||
<div class="col-3"><div class="kpi-value" id="r_pos">0</div><div class="kpi-label">生产订单</div></div>
|
||
<div class="col-3"><div class="kpi-value" id="r_wos">0</div><div class="kpi-label">工单数</div></div>
|
||
<div class="col-3"><div class="kpi-value" id="r_conflicts">0</div><div class="kpi-label">冲突数</div></div>
|
||
</div>
|
||
<div class="row mt-3">
|
||
<div class="col-4"><div class="kpi-value" id="r_tardiness">0h</div><div class="kpi-label">总延迟</div></div>
|
||
<div class="col-4"><div class="kpi-value" id="r_util">0%</div><div class="kpi-label">平均利用率</div></div>
|
||
<div class="col-4"><div class="kpi-value" id="r_cost">0</div><div class="kpi-label">预估成本</div></div>
|
||
</div>
|
||
<div class="mt-3 table-wrap">
|
||
<table id="resultTable"><thead><tr><th>生产订单</th><th>产品</th><th>产线</th><th>计划起止</th><th>齐套</th><th>冲突</th></tr></thead><tbody></tbody></table>
|
||
</div>
|
||
</div>
|
||
<div class="card-footer">
|
||
<button class="btn" onclick="location.href='calendar.html'">查看日历</button>
|
||
<button class="btn btn-success ml-auto" id="publishBtn" onclick="publishVersion()">发布版本</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
</div>
|
||
<script src="../js/data.js"></script>
|
||
<script src="../js/common.js"></script>
|
||
<script>
|
||
initPage('scheduling');
|
||
const params = new URLSearchParams(location.search);
|
||
const selectedOrderIds = params.get('orderIds') ? params.get('orderIds').split(',').map(Number) : [];
|
||
if (selectedOrderIds.length) document.getElementById('orderScope').value = 'selected';
|
||
document.getElementById('startDate').value = apsData.$utils.fmtDate(apsData.$utils.addMinutes(apsData.$utils.today(), 24 * 60));
|
||
|
||
let currentVersion = null;
|
||
|
||
function startScheduling() {
|
||
const engine = document.getElementById('engineType').value;
|
||
const strategy = document.getElementById('strategyTemplate').value;
|
||
const horizon = Number(document.getElementById('horizon').value);
|
||
const startDate = document.getElementById('startDate').value;
|
||
const scope = document.getElementById('orderScope').value;
|
||
const orderIds = scope === 'selected' ? selectedOrderIds : [];
|
||
const constraints = {
|
||
materialKit: document.getElementById('ckMaterial').checked,
|
||
equipment: document.getElementById('ckEquipment').checked,
|
||
personnel: document.getElementById('ckPersonnel').checked,
|
||
changeover: document.getElementById('ckChangeover').checked
|
||
};
|
||
|
||
document.getElementById('progressCard').style.display = 'block';
|
||
document.getElementById('resultCard').style.display = 'none';
|
||
document.getElementById('runBtn').disabled = true;
|
||
const logEl = document.getElementById('engineLog');
|
||
logEl.innerHTML = '';
|
||
const phases = [
|
||
{ p: 10, t: '加载订单与主数据...' },
|
||
{ p: 25, t: '规则引擎生成初始解...' },
|
||
{ p: 55, t: 'CP-SAT 局部优化...' },
|
||
{ p: 80, t: 'GA 全局优化...' },
|
||
{ p: 95, t: '冲突检测与统计...' },
|
||
{ p: 100, t: '排产完成' }
|
||
];
|
||
let idx = 0;
|
||
function tick() {
|
||
if (idx >= phases.length) return finalize();
|
||
const ph = phases[idx++];
|
||
document.getElementById('progressBar').style.width = ph.p + '%';
|
||
document.getElementById('progressText').textContent = ph.p + '%';
|
||
document.getElementById('phaseText').textContent = ph.t;
|
||
logEl.innerHTML += `> ${ph.t}<br>`;
|
||
logEl.scrollTop = logEl.scrollHeight;
|
||
if (engine === 'RULE' && ph.p > 25) return finalize();
|
||
setTimeout(tick, 400 + Math.random() * 300);
|
||
}
|
||
function finalize() {
|
||
currentVersion = runScheduling({ orderIds, engineType: engine, strategyTemplate: strategy, planningHorizonDays: horizon, startDate, constraints });
|
||
showResult(currentVersion);
|
||
document.getElementById('runBtn').disabled = false;
|
||
}
|
||
tick();
|
||
}
|
||
|
||
function showResult(version) {
|
||
document.getElementById('resultCard').style.display = 'block';
|
||
document.getElementById('r_orders').textContent = version.orderCount;
|
||
document.getElementById('r_pos').textContent = version.poCount;
|
||
document.getElementById('r_wos').textContent = version.woCount;
|
||
document.getElementById('r_conflicts').textContent = version.conflictCount;
|
||
document.getElementById('r_tardiness').textContent = Math.round(version.totalTardiness) + 'h';
|
||
document.getElementById('r_util').textContent = Math.round(version.avgUtilization * 100) + '%';
|
||
document.getElementById('r_cost').textContent = '¥' + Math.round(version.totalCost).toLocaleString();
|
||
const pos = apsData.productionOrders.filter(po => po.schedulingVersionId == version.id);
|
||
const tbody = document.querySelector('#resultTable tbody');
|
||
tbody.innerHTML = pos.map(po => `
|
||
<tr>
|
||
<td>${po.orderNo}</td>
|
||
<td>${po.productName} × ${po.quantity}</td>
|
||
<td>${po.lineName}</td>
|
||
<td>${po.plannedStartDate ? po.plannedStartDate.slice(0,16) : ''} ~ ${po.plannedEndDate ? po.plannedEndDate.slice(0,16) : ''}</td>
|
||
<td>${statusBadge(po.materialKitStatus, { PASSED:'success', PARTIAL:'warning', FAILED:'danger', PENDING:'default' })}</td>
|
||
<td>${po.conflictCount}</td>
|
||
</tr>`).join('') || '<tr><td colspan="6" class="text-center text-muted">无数据</td></tr>';
|
||
document.getElementById('publishBtn').disabled = version.status === 'PUBLISHED';
|
||
}
|
||
|
||
function publishVersion() {
|
||
if (!currentVersion) return;
|
||
currentVersion.status = 'PUBLISHED';
|
||
currentVersion.publishedAt = apsData.$utils.fmtDateTime(new Date());
|
||
apsData.productionOrders.filter(po => po.schedulingVersionId == currentVersion.id).forEach(po => po.status = 'CONFIRMED');
|
||
apsData.workOrders.filter(wo => {
|
||
const po = apsData.productionOrders.find(p => p.id == wo.productionOrderId);
|
||
return po && po.schedulingVersionId == currentVersion.id;
|
||
}).forEach(wo => wo.status = 'PENDING');
|
||
apsData.logs.unshift({ id: apsData.$utils.nextId('log'), type: 'INFO', category: 'SCHEDULE', operationType: 'PUBLISH', targetType: 'SCHEDULE_VERSION', targetId: currentVersion.id, action: '发布排产版本', description: `版本 ${currentVersion.versionNo} 已发布`, operator: 'admin', createdAt: apsData.$utils.fmtDateTime(new Date()) });
|
||
apsData.$utils.save(apsData);
|
||
showToast('发布成功', `版本 ${currentVersion.versionNo} 已发布为执行基准`);
|
||
showResult(currentVersion);
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|