zhizhi/modules/orders/public/admin.html

297 lines
15 KiB
HTML
Raw Permalink Normal View History

<!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>