aps-agent/aps-frontend/pages/calendar.html

177 lines
9.3 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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">
<style>
.gantt-wrap { min-height: 320px; }
.gantt-header { display: flex; border-bottom: 1px solid var(--border); background: #fafafa; }
.gantt-header .gantt-label { border-right: 1px solid var(--border); font-weight: 700; }
.gantt-header .gantt-track { display: flex; }
.day-slot { flex: 1; border-right: 1px dashed #e5e7eb; padding: 6px; font-size: 12px; color: var(--text-muted); text-align: center; }
.drag-tip { position: fixed; background: rgba(31,41,55,0.9); color: #fff; padding: 4px 8px; border-radius: 4px; font-size: 12px; pointer-events: none; z-index: 100; display: none; }
</style>
</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" onclick="changeDate(-7)">← 上一周</button>
<button class="btn" onclick="changeDate(7)">下一周 →</button>
<span id="rangeLabel" class="text-muted"></span>
<select id="viewLevel" class="btn" style="padding:7px 10px;margin-left:auto" onchange="render()">
<option value="LINE">产线视图</option>
<option value="WORKSTATION">工位视图</option>
</select>
<button class="btn btn-primary" onclick="location.href='scheduling.html'">重新排产</button>
</div>
<div class="card gantt-wrap" id="ganttCard">
<div class="gantt-header">
<div class="gantt-label">资源</div>
<div class="gantt-track" id="dayHeader"></div>
</div>
<div id="ganttBody"></div>
</div>
<div class="card">
<div class="card-header">图例</div>
<div class="card-body flex gap-3">
<span class="badge badge-primary">正常</span>
<span class="badge badge-danger">冲突</span>
<span class="badge" style="background:#9ca3af;color:#fff">已下发冻结</span>
<span class="badge badge-success">已完成</span>
<span class="text-muted">提示:拖拽工单条可调整时间,红色表示不可放置</span>
</div>
</div>
</main>
</div>
</div>
<div class="drag-tip" id="dragTip"></div>
<script src="../js/data.js"></script>
<script src="../js/common.js"></script>
<script>
initPage('calendar');
let viewStart = apsData.$utils.today();
const days = 7;
function changeDate(offset) {
viewStart = apsData.$utils.addMinutes(viewStart, offset * 24 * 60);
render();
}
function render() {
const level = document.getElementById('viewLevel').value;
const end = apsData.$utils.addMinutes(viewStart, (days - 1) * 24 * 60);
document.getElementById('rangeLabel').textContent = `${apsData.$utils.fmtDate(viewStart)} ~ ${apsData.$utils.fmtDate(end)}`;
const header = document.getElementById('dayHeader');
header.innerHTML = '';
for (let i = 0; i < days; i++) {
const d = apsData.$utils.addMinutes(viewStart, i * 24 * 60);
const div = document.createElement('div'); div.className = 'day-slot'; div.textContent = apsData.$utils.fmtDate(d).slice(5) + ' ' + ['日','一','二','三','四','五','六'][d.getDay()];
header.appendChild(div);
}
const body = document.getElementById('ganttBody');
body.innerHTML = '';
const resources = level === 'WORKSTATION' ? apsData.workstations : apsData.lines;
const trackWidth = header.clientWidth || 800;
const pxPerHour = trackWidth / (days * 24);
const viewStartMs = viewStart.getTime();
resources.forEach(res => {
const row = document.createElement('div'); row.className = 'gantt-row';
const label = document.createElement('div'); label.className = 'gantt-label'; label.textContent = res.name;
const track = document.createElement('div'); track.className = 'gantt-track';
track.style.width = trackWidth + 'px';
for (let i = 0; i < days; i++) {
const slot = document.createElement('div'); slot.className = 'slot'; track.appendChild(slot);
}
const wos = apsData.workOrders.filter(wo => {
if (level === 'WORKSTATION') return wo.workstationId == res.id;
return wo.lineId == res.id;
});
wos.forEach(wo => {
const s = apsData.$utils.parseDate(wo.plannedStartTime);
const e = apsData.$utils.parseDate(wo.plannedEndTime);
const left = ((s.getTime() - viewStartMs) / 3600000) * pxPerHour;
const width = Math.max(((e - s) / 3600000) * pxPerHour, 20);
if (left + width < 0 || left > trackWidth) return;
const bar = document.createElement('div');
bar.className = 'gantt-bar' + (wo.conflictCount ? ' conflict' : '') + (wo.isFrozen ? ' frozen' : '') + (wo.status === 'COMPLETED' ? ' completed' : '');
bar.style.left = Math.max(0, left) + 'px';
bar.style.width = Math.max(width, 20) + 'px';
bar.textContent = `${wo.productName} #${wo.quantity}`;
bar.title = `${wo.orderNo}\n${wo.operationName}\n${wo.plannedStartTime} ~ ${wo.plannedEndTime}`;
bar.onclick = (ev) => { ev.stopPropagation(); openWoDetail(wo); };
makeDraggable(bar, wo, pxPerHour, viewStartMs);
track.appendChild(bar);
});
row.appendChild(label); row.appendChild(track); body.appendChild(row);
});
if (!resources.length) body.innerHTML = '<div class="empty-state">暂无资源数据</div>';
}
function makeDraggable(bar, wo, pxPerHour, viewStartMs) {
let startX, startLeft, dragging = false;
bar.addEventListener('mousedown', e => {
if (wo.isFrozen || wo.status === 'COMPLETED') return;
dragging = true; startX = e.clientX; startLeft = parseFloat(bar.style.left) || 0;
bar.style.zIndex = 10; bar.style.opacity = 0.8;
document.body.style.userSelect = 'none';
});
window.addEventListener('mousemove', e => {
if (!dragging) return;
const dx = e.clientX - startX;
const newLeft = startLeft + dx;
bar.style.left = Math.max(0, newLeft) + 'px';
const newStart = new Date(viewStartMs + (newLeft / pxPerHour) * 3600000);
const check = checkMoveConflict(wo, apsData.$utils.fmtDateTime(newStart));
const tip = document.getElementById('dragTip');
tip.style.display = 'block';
tip.style.left = (e.clientX + 12) + 'px';
tip.style.top = (e.clientY - 30) + 'px';
tip.style.background = check.canApply ? '#16a34a' : '#dc2626';
tip.textContent = check.canApply ? '可放置' : check.conflicts.map(c => c.message).join('; ');
});
window.addEventListener('mouseup', e => {
if (!dragging) return;
dragging = false; bar.style.zIndex = ''; bar.style.opacity = 1; document.body.style.userSelect = '';
document.getElementById('dragTip').style.display = 'none';
const newLeft = parseFloat(bar.style.left) || 0;
const newStart = new Date(viewStartMs + (newLeft / pxPerHour) * 3600000);
const check = checkMoveConflict(wo, apsData.$utils.fmtDateTime(newStart));
if (check.canApply) {
const dur = apsData.$utils.parseDate(wo.plannedEndTime) - apsData.$utils.parseDate(wo.plannedStartTime);
wo.plannedStartTime = apsData.$utils.fmtDateTime(newStart);
wo.plannedEndTime = apsData.$utils.fmtDateTime(new Date(newStart.getTime() + dur));
apsData.$utils.save(apsData);
showToast('调整成功', `工单 ${wo.orderNo} 已更新`);
render();
} else {
showToast('无法放置', check.conflicts.map(c => c.message).join('; '), 'error');
render();
}
});
}
function openWoDetail(wo) {
const po = findById(apsData.productionOrders, wo.productionOrderId);
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.lineName} / ${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>
<div class="form-row"><div class="form-group"><label>状态</label><input value="${STATUS_LABELS[wo.status] || wo.status}" readonly></div><div class="form-group"><label>齐套</label><input value="${STATUS_LABELS[wo.kitStatus] || wo.kitStatus}" readonly></div></div>
<p><strong>所属生产订单:</strong>${po ? po.orderNo : '-'}</p>
`);
}
window.addEventListener('resize', render);
setTimeout(render, 50);
</script>
</body>
</html>