zhizhi/modules/orders/public/track.html

173 lines
7.6 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>__SITE_NAME__ · 我的订单</title><link rel="stylesheet" href="style.css">
<style>
.dashboard-header { text-align:center;padding:24px 0 16px; }
.dashboard-header h1 { font-size:20px;font-weight:500;margin:0; }
.dashboard-header p { color:#888780;font-size:13px;margin:6px 0 0; }
.order-info { background:#f1efe8;border-radius:8px;padding:14px;margin:12px 0;text-align:center; }
.order-info .oid { font-family:monospace;font-size:15px;color:#534ab7;font-weight:500; }
.progress-wrap { margin:12px 0; }
.progress-bar { height:8px;background:#d3d1c7;border-radius:4px;overflow:hidden; }
.progress-fill { height:100%;background:#534ab7;border-radius:4px;transition:width .5s; }
.progress-label { display:flex;justify-content:space-between;font-size:12px;color:#888780;margin-top:4px; }
.status-grid { display:grid;grid-template-columns:repeat(auto-fit,minmax(100px,1fr));gap:8px;margin:12px 0; }
.status-step { text-align:center;padding:8px;border-radius:6px;font-size:12px;background:#f1efe8;color:#b4b2a9; }
.status-step.done { background:#eaf3de;color:#3b6d11; }
.status-step.active { background:#534ab7;color:#fff; }
.contact-box { border:0.5px solid #d3d1c7;border-radius:8px;padding:14px;margin:12px 0;text-align:center; }
.contact-box p { font-size:13px;color:#5f5e5a;margin:0 0 8px; }
.search-area { border:0.5px solid #d3d1c7;border-radius:8px;padding:20px;margin-top:16px;text-align:center; }
.search-area h3 { font-size:14px;font-weight:500;margin:0 0 8px;color:#3c3489; }
.search-row { display:flex;gap:8px;max-width:400px;margin:0 auto; }
.search-row input { flex:1;font-family:monospace; }
</style></head>
<body>
<div class="container">
<div class="nav">
<a href="/submit">提交需求</a>
<a href="/track" class="active">我的订单</a>
<a href="/admin">管理面板</a>
</div>
<!-- 查看已有订单 -->
<div id="search-view">
<div class="dashboard-header">
<h1>🔍 我的订单</h1>
<p>输入订单编号查看进度</p>
</div>
<div class="search-area">
<h3>查询订单</h3>
<div class="search-row">
<input id="track-q" placeholder="如: ZT-20260606-001" onkeydown="if(event.key==='Enter')trackOrder()">
<button onclick="trackOrder()">查询</button>
</div>
<p style="font-size:12px;color:#b4b2a9;margin-top:6px;">提交需求后会自动获得订单编号</p>
</div>
</div>
<!-- 订单详情面板 -->
<div id="dashboard-view" style="display:none;">
<div class="dashboard-header">
<h1>📋 订单详情</h1>
<button class="small secondary" onclick="showSearch()" style="margin-top:8px;">← 查询其他订单</button>
</div>
<div id="dashboard-content"></div>
</div>
</div>
<script>
const STATUS_LABEL = { submitted:'已提交', reviewing:'审核中', accepted:'已接单', 'in-progress':'制作中', testing:'测试中', completed:'已完成', delivered:'已交付' };
const STATUS_STEPS = ['submitted','reviewing','accepted','in-progress','testing','completed','delivered'];
function showSearch() {
document.getElementById('search-view').style.display = 'block';
document.getElementById('dashboard-view').style.display = 'none';
}
async function trackOrder() {
const q = document.getElementById('track-q').value.trim();
if (!q) return;
await loadOrder(q);
}
async function loadOrder(id) {
const dv = document.getElementById('dashboard-view');
const dc = document.getElementById('dashboard-content');
dv.style.display = 'block';
document.getElementById('search-view').style.display = 'none';
dc.innerHTML = '<div class="loading"><p>查询中...</p></div>';
try {
const r = await fetch(`api/orders/search?q=${encodeURIComponent(id)}`);
const orders = await r.json();
const o = orders.find(x => x.id.toLowerCase() === id.toLowerCase());
if (!o) {
dc.innerHTML = `<div class="card"><p style="color:#e24b4a;text-align:center;">未找到订单 <strong>${id}</strong></p>
<p style="text-align:center;font-size:13px;color:#888780;margin-top:8px;"><a href="/submit">提交新需求</a></p></div>`;
return;
}
renderDashboard(o, dc);
} catch(e) {
dc.innerHTML = `<p class="msg error">${e.message}</p>`;
}
}
function renderDashboard(o, el) {
const idx = STATUS_STEPS.indexOf(o.status);
const pct = o.progress || (idx >= 0 ? Math.round((idx / (STATUS_STEPS.length-1)) * 100) : 0);
const exp = o.expectedDelivery ? new Date(o.expectedDelivery).toLocaleDateString() : '待确认';
let h = `<div class="card"><div class="order-info"><div class="oid">${o.id}</div>`;
h += `<span class="status status-${o.status}" style="margin-top:6px;display:inline-block;">${STATUS_LABEL[o.status]||o.status}</span></div>`;
// 进度条
h += `<div class="progress-wrap"><div class="progress-bar"><div class="progress-fill" style="width:${pct}%"></div></div>`;
h += `<div class="progress-label"><span>${o.progressNote||STATUS_LABEL[o.status]}</span><span>${pct}%</span></div></div>`;
// 状态步骤
h += `<div class="status-grid">`;
STATUS_STEPS.forEach((s, i) => {
const cls = i <= idx ? 'done' : (i === idx+1 && idx >= 0 ? 'active' : '');
h += `<div class="status-step ${cls}">${i <= idx ? '✅' : (i === idx+1 ? '⏳' : '○')}<br>${STATUS_LABEL[s]}</div>`;
});
h += `</div>`;
// 预期交付
h += `<p style="font-size:13px;color:#5f5e5a;text-align:center;">📅 预计交付: ${exp}</p>`;
// 需求
h += `<h3>📝 需求</h3><p style="font-size:13px;color:#5f5e5a;white-space:pre-wrap;">${o.requirements||''}</p>`;
if (o.budget) h += `<p style="font-size:12px;color:#888780;">💰 预算: ${o.budget}</p>`;
// 子任务
if (o.subs?.length) {
h += `<h3>📋 任务清单</h3><div class="sub-list">`;
o.subs.forEach(s => {
h += `<div class="sub-item"><span class="sid">${s.subId}</span><span class="sub-status sub-${s.status==='done'?'done':'pending'}">${s.status==='done'?'✅':'⏳'}</span><span>${s.title}</span></div>`;
});
h += `</div>`;
}
// 沟通记录
if (o.comments?.length) {
h += `<h3>💬 沟通记录</h3>`;
o.comments.slice().reverse().forEach(c => {
h += `<div class="comment ${c.side}"><span class="time">${new Date(c.createdAt).toLocaleString()}</span>`;
h += `<span class="author">${c.author} (${c.side==='dev'?'开发者':'你'}):</span><br>${c.content}</div>`;
});
}
// 主动联系入口
h += `<div class="contact-box"><p>有问题需要联系开发者?</p>`;
h += `<textarea id="client-msg" rows="2" placeholder="留言给开发者..." style="margin-bottom:6px;"></textarea>`;
h += `<button class="small" onclick="sendMsg('${o.id}')">📤 发送留言</button></div>`;
// 时间线
if (o.timeline?.length) {
h += `<h3>⏱ 更新记录</h3><div style="font-size:12px;color:#888780;">`;
o.timeline.slice().reverse().forEach(t => {
h += `<div style="padding:2px 0;">${new Date(t.at).toLocaleString()} · ${t.detail}</div>`;
});
h += `</div>`;
}
h += `</div>`;
el.innerHTML = h;
}
async function sendMsg(id) {
const content = document.getElementById('client-msg').value.trim();
if (!content) return;
await fetch(`api/orders/${id}`, { method:'PUT', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ action:'comment', author:'客户', content, side:'client' }) });
document.getElementById('client-msg').value = '';
loadOrder(id);
}
// URL 参数自动查询
const params = new URLSearchParams(location.search);
if (params.get('id')) loadOrder(params.get('id'));
else document.getElementById('track-q').focus();
</script>
</body></html>