Files
shanxia2.0/docs/demo-driver-transport-history.html
T
zlwuandCursor ab46a8c999 docs: add driver transport history design and HTML demo
Capture the approved design for same-main-plate public driver history
(list by pickup/delivery day, today shortcuts, cross-plan feedback).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-21 11:12:35 +08:00

658 lines
19 KiB
HTML
Raw 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, maximum-scale=1, viewport-fit=cover" />
<title>司机链接 · 运输记录 Demo</title>
<style>
:root {
--green: #059669;
--green-deep: #047857;
--green-soft: #ecfdf5;
--text: #1e293b;
--muted: #64748b;
--line: #e2e8f0;
--bg: #f1f5f9;
--warn: #d97706;
--danger: #dc2626;
--card: #fff;
--radius: 12px;
--safe-bottom: env(safe-area-inset-bottom, 0px);
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Segoe UI", sans-serif;
background: #0f172a;
color: var(--text);
min-height: 100vh;
}
.stage {
max-width: 1100px;
margin: 0 auto;
padding: 24px 16px 48px;
}
.stage-head {
color: #e2e8f0;
margin-bottom: 20px;
}
.stage-head h1 {
margin: 0 0 8px;
font-size: 22px;
font-weight: 700;
}
.stage-head p {
margin: 0;
color: #94a3b8;
font-size: 14px;
line-height: 1.6;
}
.phones {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 20px;
}
.phone-wrap {
background: #1e293b;
border-radius: 20px;
padding: 12px;
border: 1px solid #334155;
}
.phone-label {
color: #cbd5e1;
font-size: 13px;
font-weight: 600;
margin: 0 0 10px 4px;
}
.phone {
width: 100%;
max-width: 390px;
margin: 0 auto;
background: var(--bg);
border-radius: 18px;
overflow: hidden;
min-height: 720px;
display: flex;
flex-direction: column;
box-shadow: 0 20px 40px rgba(0,0,0,.35);
position: relative;
}
.nav {
background: var(--green);
color: #fff;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: 600;
position: relative;
flex-shrink: 0;
}
.nav .back {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
background: transparent;
border: 0;
color: #fff;
font-size: 14px;
cursor: pointer;
padding: 8px;
}
.scroll {
flex: 1;
overflow: auto;
padding: 12px 12px 96px;
}
.scroll.no-footer-pad { padding-bottom: 12px; }
.card {
background: var(--card);
border-radius: var(--radius);
margin-bottom: 12px;
overflow: hidden;
border: 1px solid #eef2f7;
}
.card-title {
padding: 10px 14px 0;
font-size: 13px;
color: var(--muted);
font-weight: 600;
}
.row {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 12px 14px;
border-bottom: 1px solid #f1f5f9;
font-size: 14px;
}
.row:last-child { border-bottom: 0; }
.row .k { color: var(--muted); flex-shrink: 0; }
.row .v { color: var(--text); text-align: right; word-break: break-all; }
.empty {
padding: 64px 24px;
text-align: center;
color: var(--muted);
}
.empty .icon {
width: 64px;
height: 64px;
margin: 0 auto 16px;
border-radius: 50%;
background: #e2e8f0;
display: grid;
place-items: center;
font-size: 28px;
color: #94a3b8;
}
.empty .title {
font-size: 15px;
color: #475569;
margin-bottom: 6px;
}
.footer {
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 10px 12px calc(10px + var(--safe-bottom));
background: rgba(255,255,255,.96);
border-top: 1px solid var(--line);
display: flex;
flex-direction: column;
gap: 8px;
backdrop-filter: blur(8px);
}
.btn {
height: 44px;
border-radius: 999px;
border: 0;
font-size: 15px;
font-weight: 600;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.btn-primary {
background: var(--green);
color: #fff;
}
.btn-primary:active { background: var(--green-deep); }
.btn-ghost {
background: #fff;
color: var(--green);
border: 1px solid #a7f3d0;
}
.btn-ghost:active { background: var(--green-soft); }
.btn-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.filters {
background: var(--card);
border-radius: var(--radius);
padding: 12px;
margin-bottom: 12px;
border: 1px solid #eef2f7;
}
.filter-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
flex-wrap: wrap;
}
.filter-row:last-child { margin-bottom: 0; }
.chip {
display: inline-flex;
align-items: center;
gap: 6px;
height: 32px;
padding: 0 12px;
border-radius: 999px;
border: 1px solid var(--line);
background: #fff;
font-size: 13px;
color: var(--text);
cursor: pointer;
user-select: none;
}
.chip.on {
background: var(--green-soft);
border-color: #6ee7b7;
color: var(--green-deep);
font-weight: 600;
}
.chip input { display: none; }
.date-inputs {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 8px;
align-items: center;
width: 100%;
}
.date-inputs input {
height: 36px;
border: 1px solid var(--line);
border-radius: 8px;
padding: 0 10px;
font-size: 13px;
color: var(--text);
background: #fff;
width: 100%;
}
.date-inputs span { color: var(--muted); font-size: 12px; }
.hint {
font-size: 12px;
color: var(--muted);
margin-top: 8px;
}
.record {
background: var(--card);
border-radius: var(--radius);
border: 1px solid #eef2f7;
padding: 14px;
margin-bottom: 10px;
cursor: pointer;
transition: border-color .15s, box-shadow .15s;
}
.record:active, .record:hover {
border-color: #6ee7b7;
box-shadow: 0 0 0 3px rgba(16,185,129,.12);
}
.record-top {
display: flex;
justify-content: space-between;
gap: 8px;
align-items: flex-start;
margin-bottom: 8px;
}
.record-no {
font-size: 15px;
font-weight: 700;
}
.tag {
display: inline-flex;
align-items: center;
height: 22px;
padding: 0 8px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
color: #fff;
flex-shrink: 0;
}
.tag.ok { background: var(--green); }
.tag.arch { background: #64748b; }
.tag.void { background: var(--danger); }
.tag.warn { background: var(--warn); }
.record-meta {
font-size: 13px;
color: var(--muted);
line-height: 1.55;
}
.record-meta strong { color: var(--text); font-weight: 500; }
.mini-tabs {
display: flex;
gap: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.mini-tabs button {
border: 1px solid #334155;
background: #0f172a;
color: #94a3b8;
border-radius: 999px;
padding: 8px 12px;
font-size: 12px;
cursor: pointer;
}
.mini-tabs button.active {
background: var(--green);
border-color: var(--green);
color: #fff;
font-weight: 600;
}
.note {
margin-top: 18px;
padding: 14px 16px;
background: #1e293b;
border: 1px solid #334155;
border-radius: 12px;
color: #cbd5e1;
font-size: 13px;
line-height: 1.7;
}
.note strong { color: #f8fafc; }
.screen { display: none; flex: 1; flex-direction: column; min-height: 0; }
.screen.active { display: flex; }
.product {
padding: 12px 14px;
border-top: 1px solid #f1f5f9;
}
.product-name { font-weight: 600; margin-bottom: 6px; }
.product-meta { font-size: 13px; color: var(--muted); }
</style>
</head>
<body>
<div class="stage">
<div class="stage-head">
<h1>司机链接 ·「运输记录」交互 Demo</h1>
<p>
列表默认<strong>今天</strong>。筛选为:单日日期框 +「今天 / 明天 / 后天」快捷按钮。
命中:取货日或送货日等于所选日。按<strong>主车车牌</strong>聚合。
</p>
<div class="mini-tabs" id="jumpTabs">
<button type="button" class="active" data-jump="A">A · 正常详情</button>
<button type="button" data-jump="B">B · 已归档详情</button>
<button type="button" data-jump="C">C · 运输记录列表</button>
<button type="button" data-jump="D">D · 作废详情</button>
</div>
</div>
<div class="phones">
<!-- Phone interactive -->
<div class="phone-wrap">
<div class="phone-label">可交互 Demo(点底部按钮 / 点记录)</div>
<div class="phone" id="phone">
<!-- Screen: active detail -->
<div class="screen active" data-screen="detail-active">
<div class="nav">运输计划</div>
<div class="scroll">
<div class="card">
<div class="row"><span class="k">计划编号</span><span class="v">YS20260821001</span></div>
<div class="row"><span class="k">计划日期</span><span class="v">2026-08-21</span></div>
<div class="row"><span class="k">车牌</span><span class="v">苏A·D12345</span></div>
<div class="row"><span class="k">司机</span><span class="v">张师傅</span></div>
</div>
<div class="card">
<div class="card-title">取货信息</div>
<div class="row"><span class="k">取货地址</span><span class="v">南京市江宁区××仓库</span></div>
<div class="row"><span class="k">取货时间</span><span class="v">2026-08-21 09:00</span></div>
</div>
<div class="card">
<div class="card-title">产品回传数量</div>
<div class="product">
<div class="product-name">硫酸</div>
<div class="product-meta">计划 28 吨 · 实际待填</div>
</div>
</div>
</div>
<div class="footer">
<div class="btn-row">
<button class="btn btn-ghost" type="button" data-go="list">运输记录</button>
<button class="btn btn-primary" type="button">保存回执</button>
</div>
</div>
</div>
<!-- Screen: archived detail (from link or list) -->
<div class="screen" data-screen="detail-archived">
<div class="nav">运输计划</div>
<div class="scroll no-footer-pad">
<div class="empty">
<div class="icon">📦</div>
<div class="title">运输计划已归档完成</div>
<div>业务详情不可查看</div>
</div>
</div>
<div class="footer">
<button class="btn btn-ghost" type="button" data-go="list">运输记录</button>
</div>
</div>
<!-- Screen: voided detail -->
<div class="screen" data-screen="detail-voided">
<div class="nav">运输计划</div>
<div class="scroll no-footer-pad">
<div class="empty">
<div class="icon">⛔</div>
<div class="title">运输计划已作废</div>
<div>业务详情不可查看</div>
</div>
</div>
<div class="footer">
<button class="btn btn-ghost" type="button" data-go="list">运输记录</button>
</div>
</div>
<!-- Screen: history list -->
<div class="screen" data-screen="list">
<div class="nav">
<button class="back" type="button" data-go="detail-active">‹ 返回</button>
运输记录
</div>
<div class="scroll" style="padding-bottom:16px">
<div class="filters">
<div class="filter-row" style="gap:8px">
<button type="button" class="chip on" data-quick="0">今天</button>
<button type="button" class="chip" data-quick="1">明天</button>
<button type="button" class="chip" data-quick="2">后天</button>
</div>
<div class="filter-row">
<input type="date" id="pickDate" value="2026-08-21" style="height:36px;border:1px solid var(--line);border-radius:8px;padding:0 10px;font-size:13px;width:100%;background:#fff" />
</div>
<div class="hint" id="filterHint">当前:今天 2026-08-21 · 取货或送货命中</div>
</div>
<div id="recordList"></div>
</div>
</div>
</div>
</div>
<!-- Static reference: footer always -->
<div class="phone-wrap">
<div class="phone-label">对照:任意状态底部都有「运输记录」</div>
<div class="phone">
<div class="nav">运输计划</div>
<div class="scroll no-footer-pad">
<div class="empty">
<div class="icon">📦</div>
<div class="title">运输计划已归档完成</div>
<div>与现状一致,详情不展示</div>
</div>
</div>
<div class="footer">
<button class="btn btn-ghost" type="button">运输记录</button>
</div>
</div>
</div>
</div>
<div class="note">
<strong>Demo 说明(已确认部分):</strong><br />
1)「运输记录」按<strong>主车车牌号</strong>聚合历史与后续安排。<br />
2)任意状态底部仅保留「运输记录」;不再提供「返回列表」。<br />
3)筛选:单日日期框 +「今天/明天/后天」快捷;<strong>默认今天</strong>。<br />
4)命中:取货日或送货日<strong>任一等于</strong>所选日。
</div>
</div>
<script>
const records = [
{
id: "r1",
planNo: "YS20260821001",
pickupDate: "2026-08-21",
deliveryDate: "2026-08-21",
route: "江宁仓库 → 镇江厂区",
status: "arranged",
statusLabel: "进行中",
tag: "ok",
screen: "detail-active"
},
{
id: "r2",
planNo: "YS20260821002",
pickupDate: "2026-08-21",
deliveryDate: "2026-08-22",
route: "仪征码头 → 南京港",
status: "arranged",
statusLabel: "进行中",
tag: "ok",
screen: "detail-active"
},
{
id: "r3",
planNo: "YS20260818008",
pickupDate: "2026-08-18",
deliveryDate: "2026-08-18",
route: "徐州仓 → 宿迁站",
status: "archived",
statusLabel: "已归档",
tag: "arch",
screen: "detail-archived"
},
{
id: "r4",
planNo: "YS20260815003",
pickupDate: "2026-08-15",
deliveryDate: "2026-08-16",
route: "盐城库 → 南通厂",
status: "voided",
statusLabel: "已作废",
tag: "void",
screen: "detail-voided"
},
{
id: "r5",
planNo: "YS20260822001",
pickupDate: "2026-08-22",
deliveryDate: "2026-08-22",
route: "连云港 → 淮安仓",
status: "arranged",
statusLabel: "后续安排",
tag: "warn",
screen: "detail-active"
}
];
const todayBase = new Date(2026, 7, 21); // demo fixed "today": 2026-08-21
function fmt(d) {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
}
function addDays(base, n) {
const d = new Date(base.getTime());
d.setDate(d.getDate() + n);
return d;
}
const today = fmt(todayBase);
const phone = document.getElementById("phone");
const listEl = document.getElementById("recordList");
const pickDate = document.getElementById("pickDate");
const filterHint = document.getElementById("filterHint");
const quickBtns = document.querySelectorAll("[data-quick]");
pickDate.value = today;
function showScreen(name) {
phone.querySelectorAll(".screen").forEach((el) => {
el.classList.toggle("active", el.dataset.screen === name);
});
document.querySelectorAll("#jumpTabs button").forEach((btn) => {
const map = {
A: "detail-active",
B: "detail-archived",
C: "list",
D: "detail-voided"
};
btn.classList.toggle("active", map[btn.dataset.jump] === name);
});
}
function matchesDate(r, day) {
return r.pickupDate === day || r.deliveryDate === day;
}
function syncQuickActive(day) {
quickBtns.forEach((btn) => {
const offset = Number(btn.dataset.quick);
const target = fmt(addDays(todayBase, offset));
btn.classList.toggle("on", target === day);
});
}
function labelFor(day) {
if (day === fmt(addDays(todayBase, 0))) return `今天 ${day}`;
if (day === fmt(addDays(todayBase, 1))) return `明天 ${day}`;
if (day === fmt(addDays(todayBase, 2))) return `后天 ${day}`;
return day;
}
function renderList() {
const day = (pickDate.value || today).trim();
syncQuickActive(day);
const filtered = records.filter((r) => matchesDate(r, day));
filterHint.textContent = `当前:${labelFor(day)} · 取货或送货命中 · 共 ${filtered.length} 条`;
if (!filtered.length) {
listEl.innerHTML = `<div class="empty"><div class="icon">🗂️</div><div class="title">暂无运输记录</div><div>可点「明天/后天」或另选日期</div></div>`;
return;
}
listEl.innerHTML = filtered
.map(
(r) => `
<div class="record" data-screen="${r.screen}" role="button" tabindex="0">
<div class="record-top">
<div class="record-no">${r.planNo}</div>
<span class="tag ${r.tag}">${r.statusLabel}</span>
</div>
<div class="record-meta">
<div>取货:<strong>${r.pickupDate}</strong> · 送货:<strong>${r.deliveryDate}</strong></div>
<div>线路:${r.route}</div>
</div>
</div>`
)
.join("");
listEl.querySelectorAll(".record").forEach((el) => {
el.addEventListener("click", () => showScreen(el.dataset.screen));
});
}
phone.addEventListener("click", (e) => {
const btn = e.target.closest("[data-go]");
if (!btn) return;
showScreen(btn.dataset.go);
if (btn.dataset.go === "list") renderList();
});
quickBtns.forEach((btn) => {
btn.addEventListener("click", () => {
pickDate.value = fmt(addDays(todayBase, Number(btn.dataset.quick)));
renderList();
});
});
pickDate.addEventListener("change", renderList);
document.getElementById("jumpTabs").addEventListener("click", (e) => {
const btn = e.target.closest("button[data-jump]");
if (!btn) return;
const map = {
A: "detail-active",
B: "detail-archived",
C: "list",
D: "detail-voided"
};
showScreen(map[btn.dataset.jump]);
if (btn.dataset.jump === "C") renderList();
});
renderList();
</script>
</body>
</html>