zhizhi/modules/orders/public/admin.html

297 lines
15 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>__SITE_NAME__ · 管理</title><link rel="stylesheet" href="style.css">
<style>
.tab-bar { display:flex;gap:4px;margin-bottom:16px; }
.tab-bar button { flex:1;padding:10px;border:0.5px solid #d3d1c7;background:#fff;border-radius:8px;cursor:pointer;font-size:13px;color:#5f5e5a; }
.tab-bar button.active { background:#534ab7;color:#fff;border-color:#534ab7; }
.filter-bar { display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap;align-items:center; }
.filter-bar input { width:200px; }
.detail-panel { display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.3);z-index:100;overflow-y:auto; }
.detail-panel .card { max-width:700px;margin:40px auto;position:relative; }
.close-btn { position:absolute;top:12px;right:16px;background:none;border:none;font-size:22px;cursor:pointer;color:#888780; }
.dev-stage { display:flex;gap:8px;margin:12px 0;flex-wrap:wrap; }
.dev-stage button { flex:1;min-width:80px;padding:8px;border:0.5px solid #d3d1c7;background:#fff;border-radius:6px;cursor:pointer;font-size:12px;text-align:center; }
.dev-stage button.active { background:#534ab7;color:#fff;border-color:#534ab7; }
.dev-stage button.done { background:#eaf3de;color:#3b6d11; }
.comment-input { display:flex;gap:8px;margin-top:8px; }
.comment-input input { flex:1; }
.progress-edit { display:flex;gap:8px;align-items:center;margin:8px 0; }
.progress-edit input[type=range] { flex:1; }
</style></head>
<body>
<div class="container">
<div class="nav">
<a href="/submit">提交需求</a>
<a href="/track">我的订单</a>
<a href="/admin" class="active">管理面板</a>
</div>
<div class="stats" id="stats-row">
<div class="stat-card"><div class="num" id="s-total">0</div><div class="label">当前订单</div></div>
<div class="stat-card"><div class="num" id="s-pending">0</div><div class="label">待处理</div></div>
<div class="stat-card"><div class="num" id="s-active">0</div><div class="label">制作中</div></div>
<div class="stat-card"><div class="num" id="s-history">0</div><div class="label">历史</div></div>
</div>
<div class="tab-bar">
<button id="tab-active" class="active" onclick="switchTab('active')">📋 当前订单</button>
<button id="tab-history" onclick="switchTab('history')">📦 历史订单</button>
</div>
<div class="filter-bar">
<input id="filter-q" placeholder="搜索编号/姓名/联系方式" oninput="loadOrders()">
<select id="filter-status" onchange="loadOrders()">
<option value="">全部状态</option>
<option value="submitted">已提交</option>
<option value="reviewing">审核中</option>
<option value="accepted">已接单</option>
<option value="in-progress">制作中</option>
<option value="testing">测试中</option>
<option value="completed">已完成</option>
<option value="delivered">已交付</option>
<option value="flagged">⚠️ 可疑</option>
</select>
<button class="small" onclick="loadOrders()">🔄 刷新</button>
</div>
<div id="order-list"></div>
</div>
<div class="detail-panel" id="detail-panel" onclick="if(event.target===this)closeDetail()">
<div class="card" id="detail-card"></div>
</div>
<script>
const STATUS_LABEL = { submitted:'已提交', reviewing:'审核中', accepted:'已接单', 'in-progress':'制作中', testing:'测试中', completed:'已完成', delivered:'已交付' };
const DEV_STAGES = [
{ key:'analysis', label:'需求分析', pct:10 },
{ key:'design', label:'方案设计', pct:25 },
{ key:'backend', label:'后端开发', pct:45 },
{ key:'frontend', label:'前端开发', pct:65 },
{ key:'testing', label:'测试', pct:85 },
{ key:'done', label:'完成', pct:100 },
];
let currentTab = 'active';
function switchTab(tab) {
currentTab = tab;
document.querySelectorAll('.tab-bar button').forEach(b => b.classList.remove('active'));
document.getElementById('tab-'+tab).classList.add('active');
loadOrders();
}
async function loadOrders() {
const el = document.getElementById('order-list');
try {
const url = currentTab === 'history' ? 'api/orders/archived' : 'api/orders/active';
const r = await fetch(url);
let orders = await r.json();
const q = document.getElementById('filter-q').value.trim().toLowerCase();
const st = document.getElementById('filter-status').value;
if (q) orders = orders.filter(o => o.id.toLowerCase().includes(q) || (o.name||'').includes(q) || (o.contact||'').includes(q));
if (st === 'flagged') orders = orders.filter(o => o.flagged);
else if (st) orders = orders.filter(o => o.status === st);
renderList(orders, el);
updateStats();
} catch(e) { el.innerHTML = `<p class="msg error">加载失败: ${e.message}</p>`; }
}
function renderList(orders, el) {
if (!orders.length) { el.innerHTML = '<p style="color:#888780;text-align:center;padding:40px;">暂无订单</p>'; return; }
el.innerHTML = orders.map(o => `
<div class="order-card" onclick="openDetail('${o.id}')">
<div class="oh">
<div><span class="id">${o.id}</span> <strong>${o.name||''}</strong>
${o.flagged ? '<span style="color:#e24b4a;font-size:11px;">⚠️</span>' : ''}
${o.archived ? '<span style="color:#888780;font-size:11px;">📦</span>' : ''}
</div>
<div><span class="status status-${o.status}">${STATUS_LABEL[o.status]||o.status}</span></div>
</div>
<div style="font-size:12px;color:#5f5e5a;margin-top:4px;">${(o.requirements||'').slice(0,80)}${(o.requirements||'').length>80?'...':''}</div>
<div style="font-size:11px;color:#888780;margin-top:4px;display:flex;gap:12px;">
<span>📞 ${o.contactType||''}: ${o.contact||''}</span>
<span>📅 ${new Date(o.createdAt).toLocaleDateString()}</span>
${o.expectedDays ? `<span>⏱ ${o.expectedDays}天</span>` : ''}
<span>📊 ${o.progress||0}%</span>
</div>
</div>
`).join('');
}
async function updateStats() {
const r = await fetch('api/orders/active');
const active = await r.json();
const rh = await fetch('api/orders/archived');
const history = await rh.json();
document.getElementById('s-total').textContent = active.length;
document.getElementById('s-pending').textContent = active.filter(o => ['submitted','reviewing'].includes(o.status)).length;
document.getElementById('s-active').textContent = active.filter(o => ['accepted','in-progress','testing'].includes(o.status)).length;
document.getElementById('s-history').textContent = history.length;
}
async function openDetail(id) {
const r = await fetch(`api/orders/search?q=${id}`);
const orders = await r.json();
const o = orders.find(x => x.id === id);
if (!o) return;
document.getElementById('detail-panel').style.display = 'block';
document.getElementById('detail-card').innerHTML = buildDetail(o);
}
function closeDetail() {
document.getElementById('detail-panel').style.display = 'none';
}
function buildDetail(o) {
const idx = DEV_STAGES.findIndex(s => s.key === (o.devStage || ''));
const exp = o.expectedDelivery ? new Date(o.expectedDelivery).toLocaleDateString() : '未设定';
let h = `<button class="close-btn" onclick="closeDetail()">✕</button>`;
h += `<h2>${o.id}</h2>`;
h += `<p style="color:#5f5e5a;font-size:13px;"><strong>${o.name||''}</strong> · 📞 ${o.contactType||''}: ${o.contact||''}</p>`;
h += `<span class="status status-${o.status}" style="font-size:13px;padding:4px 12px;">${STATUS_LABEL[o.status]||o.status}</span>`;
if (o.flagged) h += ` <span style="color:#e24b4a;font-size:12px;">⚠️ ${o.flagReason||'可疑'}</span>`;
// --- 开发工作流(仅活跃订单)---
if (!o.archived) {
h += `<div style="margin-top:12px;"><h3>🔧 开发进度</h3>`;
h += `<div class="dev-stage">`;
DEV_STAGES.forEach((s, i) => {
const cls = i <= idx ? 'done' : (s.key === o.devStage ? 'active' : '');
h += `<button class="${cls}" onclick="setStage('${o.id}','${s.key}',${s.pct})">${i <= idx ? '✅' : ''} ${s.label}</button>`;
});
h += `</div>`;
// 进度条
h += `<div style="margin:8px 0;"><div style="display:flex;justify-content:space-between;font-size:12px;color:#888780;">
<span>${o.progressNote||''}</span><span>${o.progress||0}%</span></div>
<div class="progress-bar"><div class="progress-fill" style="width:${o.progress||0}%;background:#1d9e75;"></div></div>
</div>`;
// 手动调整进度
h += `<div class="progress-edit"><span style="font-size:12px;color:#888780;">进度:</span>
<input type="range" min="0" max="100" value="${o.progress||0}" oninput="previewProgress(this.value)" onchange="setProgress('${o.id}',this.value)">
<span id="prog-val" style="font-size:12px;font-weight:500;width:30px;">${o.progress||0}%</span></div>
<input id="prog-note" placeholder="进度说明后端API已完成" style="margin-bottom:6px;">
<button class="small" onclick="setProgressWithNote('${o.id}')">更新进度</button>`;
// 预期时间
h += `<div style="margin-top:8px;display:flex;gap:8px;align-items:center;">
<span style="font-size:12px;color:#888780;">⏱ 预期 ${o.expectedDays||'?'} 天</span>
<span style="font-size:12px;color:#888780;">📅 ${exp}</span></div>`;
// 接单/推进/交付/归档按钮
h += `<div style="margin-top:12px;display:flex;gap:6px;flex-wrap:wrap;">`;
const flow = ['submitted','reviewing','accepted','in-progress','testing','completed','delivered'];
const ci = flow.indexOf(o.status);
if (ci < flow.length-1) h += `<button class="small" onclick="updateStatus('${o.id}','${flow[ci+1]}')">→ ${STATUS_LABEL[flow[ci+1]]}</button>`;
if (ci > 0) h += `<button class="small secondary" onclick="updateStatus('${o.id}','${flow[ci-1]}')">← 回退</button>`;
if (o.status === 'completed' || o.status === 'testing') h += `<button class="small" style="background:#1d9e75;" onclick="deliver('${o.id}')">📤 交付给客户</button>`;
if (o.status === 'delivered') h += `<button class="small" style="background:#639922;" onclick="doArchive('${o.id}')">📦 归档</button>`;
h += `<button class="small secondary" onclick="if(confirm('确定删除?'))deleteOrder('${o.id}')">🗑</button>`;
if (o.flagged) h += `<button class="small" onclick="dismissFlag('${o.id}')">解除可疑</button>`;
h += `</div>`;
}
// 需求
h += `<div style="margin-top:12px;"><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></div>`;
// 子任务
if (o.subs?.length) {
h += `<h3>📋 子任务 (${o.subs.length})</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>`;
}
h += `<div style="margin-top:8px;display:flex;gap:4px;">
<input id="sub-title" placeholder="子任务" style="flex:1;">
<button class="small" onclick="addSub('${o.id}')">+</button>
</div>`;
// 沟通记录
if (o.comments?.length) {
h += `<h3>💬 沟通记录 (${o.comments.length})</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="comment-input"><input id="comment-text" placeholder="留言..." onkeydown="if(event.key==='Enter')addComment('${o.id}')"><button class="small" onclick="addComment('${o.id}')">发送</button></div>`;
// 时间线
if (o.timeline?.length) {
h += `<h3>⏱ 时间线</h3><div style="font-size:12px;color:#888780;max-height:200px;overflow-y:auto;">`;
o.timeline.slice().reverse().forEach(t => h += `<div style="padding:2px 0;">${new Date(t.at).toLocaleString()} · ${t.detail}</div>`);
h += `</div>`;
}
return h;
}
// 开发阶段
async function setStage(id, stage, pct) {
await fetch(`api/orders/${id}`, { method:'PUT', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ action:'progress', progress:pct, note:`${stage}阶段` }) });
// 同步推进状态
if (pct >= 100 && stage === 'done') {
await fetch(`api/orders/${id}`, { method:'PUT', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ action:'status', status:'completed' }) });
} else if (pct >= 25) {
await fetch(`api/orders/${id}`, { method:'PUT', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ action:'status', status:'in-progress' }) });
}
openDetail(id); loadOrders();
}
function previewProgress(v) { document.getElementById('prog-val').textContent = v + '%'; }
async function setProgress(id, v) {
await fetch(`api/orders/${id}`, { method:'PUT', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ action:'progress', progress:parseInt(v) }) });
openDetail(id); loadOrders();
}
async function setProgressWithNote(id) {
const p = document.getElementById('prog-val').textContent.replace('%','');
const n = document.getElementById('prog-note').value;
await fetch(`api/orders/${id}`, { method:'PUT', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ action:'progress', progress:parseInt(p), note:n }) });
document.getElementById('prog-note').value = '';
openDetail(id); loadOrders();
}
// 状态/操作
async function updateStatus(id, s) {
await fetch(`api/orders/${id}`, { method:'PUT', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ action:'status', status:s }) });
openDetail(id); loadOrders();
}
async function deliver(id) {
await fetch(`api/orders/${id}`, { method:'PUT', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ action:'deliver' }) });
openDetail(id); loadOrders();
}
async function doArchive(id) {
await fetch(`api/orders/${id}`, { method:'PUT', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ action:'archive' }) });
closeDetail(); loadOrders();
}
async function addSub(id) {
const t = document.getElementById('sub-title').value.trim();
if (!t) return;
await fetch(`api/orders/${id}`, { method:'PUT', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ action:'subtask', title:t }) });
document.getElementById('sub-title').value = '';
openDetail(id); loadOrders();
}
async function addComment(id) {
const c = document.getElementById('comment-text').value.trim();
if (!c) return;
await fetch(`api/orders/${id}`, { method:'PUT', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ action:'comment', author:'之之', content:c, side:'dev' }) });
document.getElementById('comment-text').value = '';
openDetail(id); loadOrders();
}
async function dismissFlag(id) {
await fetch(`api/orders/${id}`, { method:'PUT', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ action:'flag', flagged:false }) });
openDetail(id); loadOrders();
}
async function deleteOrder(id) {
await fetch(`api/orders/${id}`, { method:'DELETE' });
closeDetail(); loadOrders();
}
loadOrders();
</script>
</body></html>