143 lines
9.8 KiB
HTML
143 lines
9.8 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="toolbar">
|
||
<button class="btn btn-primary" id="batchIssueBtn" onclick="batchIssue()" disabled>批量下发</button>
|
||
<select id="statusFilter" class="btn" style="padding:7px 10px" onchange="render()">
|
||
<option value="">全部状态</option>
|
||
<option value="PENDING">待下发</option>
|
||
<option value="ISSUED">已下发</option>
|
||
<option value="IN_PROGRESS">执行中</option>
|
||
<option value="PAUSED">已暂停</option>
|
||
<option value="COMPLETED">已完成</option>
|
||
<option value="CANCELLED">已取消</option>
|
||
</select>
|
||
<div class="search-box"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg><input id="searchInput" placeholder="搜索工单号/产品/工序" oninput="render()"></div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-body table-wrap">
|
||
<table id="woTable">
|
||
<thead><tr><th><input type="checkbox" id="checkAll" onclick="toggleAll()"></th><th>工单号</th><th>工序</th><th>产品/数量</th><th>产线/工位</th><th>计划时间</th><th>进度</th><th>齐套</th><th>状态</th><th>操作</th></tr></thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
</div>
|
||
<div class="card-footer flex justify-between">
|
||
<span id="totalInfo" class="text-muted"></span>
|
||
<div id="pagination" class="pagination"></div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
</div>
|
||
<script src="../js/data.js"></script>
|
||
<script src="../js/common.js"></script>
|
||
<script>
|
||
initPage('work-orders');
|
||
let page = 1, pageSize = 12, selectedIds = [];
|
||
function render(pg) {
|
||
if (pg) page = pg;
|
||
const st = document.getElementById('statusFilter').value;
|
||
const kw = document.getElementById('searchInput').value.toLowerCase();
|
||
let rows = apsData.workOrders.filter(wo => {
|
||
if (st && wo.status !== st) return false;
|
||
const text = (wo.orderNo + ' ' + wo.productName + ' ' + wo.operationName + ' ' + wo.lineName).toLowerCase();
|
||
return !kw || text.includes(kw);
|
||
}).sort((a, b) => apsData.$utils.parseDate(a.plannedStartTime) - apsData.$utils.parseDate(b.plannedStartTime));
|
||
const total = rows.length;
|
||
rows = rows.slice((page - 1) * pageSize, page * pageSize);
|
||
const tbody = document.querySelector('#woTable tbody');
|
||
tbody.innerHTML = rows.map(wo => `
|
||
<tr>
|
||
<td><input type="checkbox" class="row-check" value="${wo.id}" ${selectedIds.includes(wo.id)?'checked':''} onchange="toggleRow(${wo.id})"></td>
|
||
<td>${wo.orderNo}</td>
|
||
<td>${wo.operationName}</td>
|
||
<td>${wo.productName} × ${wo.quantity}</td>
|
||
<td>${wo.lineName} / ${wo.workstationName}</td>
|
||
<td>${wo.plannedStartTime.slice(0,16)}<br><span class="text-muted">${wo.plannedEndTime.slice(0,16)}</span></td>
|
||
<td><div class="progress"><div class="progress-bar ${wo.progressPercent>=100?'success':(wo.progressPercent>0?'warning':'')}" style="width:${wo.progressPercent}%"></div></div>${wo.progressPercent}%</td>
|
||
<td>${statusBadge(wo.kitStatus, { PASSED:'success', PARTIAL:'warning', FAILED:'danger', PENDING:'default' })}</td>
|
||
<td>${statusBadge(wo.status, { PENDING:'default', ISSUED:'primary', IN_PROGRESS:'warning', PAUSED:'warning', COMPLETED:'success', CANCELLED:'danger' })}</td>
|
||
<td class="actions">
|
||
<button class="btn btn-sm" onclick="viewWo(${wo.id})">详情</button>
|
||
${wo.status === 'PENDING' ? `<button class="btn btn-sm btn-success" onclick="issueWo(${wo.id})">下发</button>` : ''}
|
||
${wo.status === 'IN_PROGRESS' || wo.status === 'ISSUED' ? `<button class="btn btn-sm" onclick="feedbackWo(${wo.id})">反馈</button>` : ''}
|
||
${wo.status === 'IN_PROGRESS' ? `<button class="btn btn-sm btn-warning" onclick="pauseWo(${wo.id})">暂停</button>` : ''}
|
||
${wo.status === 'PAUSED' ? `<button class="btn btn-sm btn-success" onclick="resumeWo(${wo.id})">恢复</button>` : ''}
|
||
</td>
|
||
</tr>`).join('') || '<tr><td colspan="10" class="text-center text-muted">无数据</td></tr>';
|
||
document.getElementById('totalInfo').textContent = `共 ${total} 条`;
|
||
document.getElementById('batchIssueBtn').disabled = !selectedIds.length;
|
||
renderPagination('pagination', page, total, pageSize, 'render');
|
||
}
|
||
function toggleRow(id) { selectedIds = selectedIds.includes(id) ? selectedIds.filter(x=>x!==id) : [...selectedIds, id]; render(); }
|
||
function toggleAll() { const checked = document.getElementById('checkAll').checked; selectedIds = checked ? $$('.row-check').map(cb=>Number(cb.value)) : []; render(); }
|
||
|
||
function viewWo(id) {
|
||
const wo = findById(apsData.workOrders, id);
|
||
const matList = wo.requiredMaterials.map(m => `<li>${m.name}: 需求 ${m.required}, 可用 ${m.available}, ${statusBadge(m.status, { PASSED:'success', PARTIAL:'warning', FAILED:'danger' })}</li>`).join('') || '<li class="text-muted">无物料需求</li>';
|
||
openModal('工单详情', `
|
||
<div class="form-row"><div class="form-group"><label>工单号</label><input value="${wo.orderNo}" readonly></div><div class="form-group"><label>工序</label><input value="${wo.operationName}" readonly></div></div>
|
||
<div class="form-row"><div class="form-group"><label>产品</label><input value="${wo.productName} × ${wo.quantity}" readonly></div><div class="form-group"><label>工位</label><input value="${wo.workstationName}" readonly></div></div>
|
||
<div class="form-row"><div class="form-group"><label>计划开始</label><input value="${wo.plannedStartTime}" readonly></div><div class="form-group"><label>计划结束</label><input value="${wo.plannedEndTime}" readonly></div></div>
|
||
<p><strong>物料齐套:</strong></p><ul>${matList}</ul>
|
||
`, '', true);
|
||
}
|
||
function issueWo(id) {
|
||
const wo = findById(apsData.workOrders, id);
|
||
if (wo.kitStatus === 'FAILED') return showToast('无法下发', '物料不齐套', 'error');
|
||
wo.status = 'ISSUED'; wo.isFrozen = true;
|
||
apsData.logs.unshift({ id: apsData.$utils.nextId('log'), type: 'INFO', category: 'WO', operationType: 'ISSUE', targetType: 'WORK_ORDER', targetId: id, action: '下发工单', description: `工单 ${wo.orderNo} 已下发至 MES`, operator: 'admin', createdAt: apsData.$utils.fmtDateTime(new Date()) });
|
||
apsData.$utils.save(apsData); showToast('下发成功', wo.orderNo); render();
|
||
}
|
||
function batchIssue() {
|
||
let count = 0;
|
||
selectedIds.forEach(id => {
|
||
const wo = findById(apsData.workOrders, id);
|
||
if (wo.status === 'PENDING' && wo.kitStatus !== 'FAILED') { wo.status = 'ISSUED'; wo.isFrozen = true; count++; }
|
||
});
|
||
apsData.$utils.save(apsData); selectedIds = []; render();
|
||
showToast('批量下发完成', `成功下发 ${count} 个工单`);
|
||
}
|
||
function feedbackWo(id) {
|
||
const wo = findById(apsData.workOrders, id);
|
||
openModal('工单执行反馈', `
|
||
<div class="form-row"><div class="form-group"><label>累计完成数量</label><input id="f_qty" type="number" value="${wo.completedQuantity || 0}"></div><div class="form-group"><label>报废数量</label><input id="f_scrap" type="number" value="0"></div></div>
|
||
<div class="form-row"><div class="form-group"><label>反馈类型</label><select id="f_type"><option value="PROGRESS">进度更新</option><option value="COMPLETION">完工</option><option value="EXCEPTION">异常</option></select></div></div>
|
||
<div class="form-row"><div class="form-group"><label>异常说明</label><textarea id="f_exc" rows="2" placeholder="选异常时填写"></textarea></div></div>
|
||
`, `
|
||
<button class="btn" onclick="closeModal()">取消</button>
|
||
<button class="btn btn-primary" onclick="doFeedback(${id})">提交</button>
|
||
`);
|
||
}
|
||
function doFeedback(id) {
|
||
const wo = findById(apsData.workOrders, id);
|
||
const qty = Number(document.getElementById('f_qty').value);
|
||
const scrap = Number(document.getElementById('f_scrap').value);
|
||
const type = document.getElementById('f_type').value;
|
||
wo.completedQuantity = qty;
|
||
wo.progressPercent = Math.min(100, Math.round(qty / wo.quantity * 100));
|
||
if (type === 'COMPLETION' || wo.progressPercent >= 100) { wo.status = 'COMPLETED'; wo.actualEndTime = apsData.$utils.fmtDateTime(new Date()); }
|
||
else if (type === 'EXCEPTION') { wo.status = 'PAUSED'; }
|
||
else if (wo.status === 'ISSUED' && qty > 0) wo.status = 'IN_PROGRESS';
|
||
apsData.logs.unshift({ id: apsData.$utils.nextId('log'), type: type==='EXCEPTION'?'ERROR':'INFO', category: 'WO', operationType: 'FEEDBACK', targetType: 'WORK_ORDER', targetId: id, action: '工单反馈', description: `${wo.orderNo} 进度 ${wo.progressPercent}%`, operator: 'admin', createdAt: apsData.$utils.fmtDateTime(new Date()) });
|
||
apsData.$utils.save(apsData); closeModal(); showToast('反馈已提交', wo.orderNo); render();
|
||
}
|
||
function pauseWo(id) { const wo = findById(apsData.workOrders, id); wo.status='PAUSED'; apsData.$utils.save(apsData); showToast('已暂停', wo.orderNo); render(); }
|
||
function resumeWo(id) { const wo = findById(apsData.workOrders, id); wo.status='IN_PROGRESS'; apsData.$utils.save(apsData); showToast('已恢复', wo.orderNo); render(); }
|
||
render();
|
||
</script>
|
||
</body>
|
||
</html>
|