490 lines
20 KiB
HTML
490 lines
20 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>运输计划列表 · 扁平多列(无合并单元格)Demo</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||
<style>
|
||
:root {
|
||
--primary: #10b981;
|
||
--primary-dark: #059669;
|
||
--primary-bg: #ecfdf5;
|
||
--text: #1e293b;
|
||
--muted: #64748b;
|
||
--border: #d9e8e2;
|
||
--head: #f8fafc;
|
||
--page: #eef2f6;
|
||
--panel: #fff;
|
||
--warn: #d97706;
|
||
--danger: #dc2626;
|
||
--alt: #fafafa;
|
||
--load-bg: #ecfdf5;
|
||
--delivery-bg: #f0f9ff;
|
||
--load-border: #a7f3d0;
|
||
--delivery-border: #bae6fd;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
body {
|
||
margin: 0;
|
||
padding: 24px 20px 80px;
|
||
font-family: Inter, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
color: var(--text);
|
||
background: var(--page);
|
||
}
|
||
.page { max-width: 1760px; margin: 0 auto; }
|
||
|
||
.hero {
|
||
background: linear-gradient(135deg, #ecfdf5 0%, #fff 55%, #f0f9ff 100%);
|
||
border: 1px solid var(--border);
|
||
border-radius: 10px;
|
||
padding: 20px 22px;
|
||
margin-bottom: 16px;
|
||
}
|
||
.hero h1 { margin: 0 0 8px; font-size: 22px; font-weight: 700; }
|
||
.hero h1 em { font-style: normal; color: var(--primary-dark); }
|
||
.hero p { margin: 0; color: var(--muted); max-width: 1080px; }
|
||
.pills { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
|
||
.pill {
|
||
font-size: 12px; font-weight: 600; padding: 4px 10px; border-radius: 999px;
|
||
background: var(--primary-bg); color: var(--primary-dark); border: 1px solid #a7f3d0;
|
||
}
|
||
.pill.muted { background: #f1f5f9; color: var(--muted); border-color: #e2e8f0; }
|
||
.pill.warn { background: #fffbeb; color: var(--warn); border-color: #fde68a; }
|
||
|
||
.tabs {
|
||
display: flex; flex-wrap: wrap; gap: 6px;
|
||
background: var(--panel); border: 1px solid var(--border);
|
||
border-radius: 10px 10px 0 0; padding: 8px 10px 0;
|
||
}
|
||
.tab {
|
||
border: none; background: transparent; cursor: pointer; font-family: inherit;
|
||
padding: 10px 14px; font-size: 13px; font-weight: 600; color: var(--muted);
|
||
border-bottom: 2px solid transparent;
|
||
}
|
||
.tab:hover { color: var(--primary-dark); }
|
||
.tab.active { color: var(--primary-dark); border-bottom-color: var(--primary); }
|
||
.tab .rec {
|
||
margin-left: 6px; font-size: 10px; padding: 1px 6px; border-radius: 999px;
|
||
background: var(--primary-bg); color: var(--primary-dark); border: 1px solid #a7f3d0;
|
||
}
|
||
|
||
.panel {
|
||
display: none;
|
||
background: var(--panel);
|
||
border: 1px solid var(--border);
|
||
border-top: none;
|
||
border-radius: 0 0 10px 10px;
|
||
padding: 16px 18px 22px;
|
||
}
|
||
.panel.active { display: block; }
|
||
|
||
.desc {
|
||
margin: 0 0 12px; padding: 12px 14px; border-radius: 8px;
|
||
background: #f0f9ff; border: 1px solid #bae6fd; color: #0369a1; font-size: 13px;
|
||
}
|
||
.desc strong { color: var(--text); }
|
||
.ok {
|
||
margin: 0 0 14px; padding: 10px 12px; border-radius: 8px;
|
||
background: #ecfdf5; border: 1px solid #a7f3d0; color: #065f46; font-size: 12.5px;
|
||
}
|
||
.pain {
|
||
margin: 0 0 14px; padding: 10px 12px; border-radius: 8px;
|
||
background: #fef2f2; border: 1px solid #fecaca; color: #991b1b; font-size: 12.5px;
|
||
}
|
||
|
||
.table-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: 8px; }
|
||
table.list {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
min-width: 1480px;
|
||
background: #fff;
|
||
}
|
||
table.list th,
|
||
table.list td {
|
||
border-bottom: 1px solid var(--border);
|
||
border-right: 1px solid #eef2f0;
|
||
padding: 10px 12px;
|
||
vertical-align: top;
|
||
font-size: 13px;
|
||
}
|
||
table.list th:last-child,
|
||
table.list td:last-child { border-right: none; }
|
||
table.list thead th {
|
||
position: sticky; top: 0; z-index: 1;
|
||
background: var(--head);
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
text-align: left;
|
||
}
|
||
table.list thead th.num,
|
||
table.list td.num { text-align: right; }
|
||
table.list thead th.center,
|
||
table.list td.center { text-align: center; }
|
||
table.list tbody tr:nth-child(even) { background: var(--alt); }
|
||
table.list tbody tr:hover { background: #f1f5f9; }
|
||
table.list a { color: var(--primary-dark); font-weight: 500; text-decoration: none; }
|
||
table.list a:hover { text-decoration: underline; }
|
||
|
||
.tag {
|
||
display: inline-flex; align-items: center;
|
||
padding: 1px 8px; border-radius: 6px;
|
||
font-size: 12px; font-weight: 600; line-height: 1.45; white-space: nowrap;
|
||
}
|
||
.tag.success { background: #059669; color: #fff; }
|
||
.tag.warn { background: #d97706; color: #fff; }
|
||
.tag.info {
|
||
background: transparent; color: var(--text);
|
||
border: 1px solid #cbd5e1; font-weight: 500;
|
||
}
|
||
.tag.side-load {
|
||
background: var(--load-bg); color: var(--primary-dark);
|
||
border: 1px solid var(--load-border);
|
||
}
|
||
.tag.side-need {
|
||
background: var(--delivery-bg); color: #0369a1;
|
||
border: 1px solid var(--delivery-border);
|
||
}
|
||
.muted { color: var(--muted); }
|
||
.stack { display: flex; flex-direction: column; gap: 6px; }
|
||
.stack .sub { color: var(--muted); font-size: 12px; }
|
||
.name-with-mark {
|
||
display: inline-flex; align-items: center; flex-wrap: wrap; gap: 4px;
|
||
}
|
||
.th-group-load { background: #ecfdf5 !important; color: var(--primary-dark) !important; }
|
||
.th-group-need { background: #e0f2fe !important; color: #0369a1 !important; }
|
||
.cell-load { background: rgba(236, 253, 245, 0.45); }
|
||
.cell-need { background: rgba(240, 249, 255, 0.55); }
|
||
|
||
.meta {
|
||
margin-top: 14px; font-size: 12px; color: var(--muted);
|
||
}
|
||
.meta code {
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
background: #f1f5f9; padding: 1px 6px; border-radius: 4px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="page">
|
||
<header class="hero">
|
||
<h1>运输计划列表 · <em>扁平多列</em>(取消合并单元格)</h1>
|
||
<p>
|
||
案例数据来自测试库运输计划 <strong>PO20261009022</strong>:
|
||
装货客户「安庆市山峡化工有限公司」、送货客户「桐城运城制版有限公司」、
|
||
3 个产品(硫酸 / 聚合氯化铝 / 液碱);需方合同 <code>SX/X-JT20261008006</code>,供方合同为空。
|
||
目标:去掉供/需纵向拆行 + rowspan,改为扁平分列,每一物理行独立完整。
|
||
</p>
|
||
<div class="pills">
|
||
<span class="pill">PO20261009022</span>
|
||
<span class="pill">皖H21522</span>
|
||
<span class="pill">程欢欢</span>
|
||
<span class="pill muted">黄天宝(押)、沈卫斌(驾)</span>
|
||
<span class="pill warn">待安排</span>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="tabs" role="tablist">
|
||
<button class="tab active" data-tab="a" type="button">
|
||
方案 A · 一产品一行 + 供/需分列<span class="rec">推荐</span>
|
||
</button>
|
||
<button class="tab" data-tab="b" type="button">方案 B · 一计划一行(格内多行)</button>
|
||
<button class="tab" data-tab="c" type="button">对照 · 现状(供/需拆行 + 合并)</button>
|
||
</div>
|
||
|
||
<!-- 方案 A -->
|
||
<section class="panel active" id="panel-a">
|
||
<p class="desc">
|
||
<strong>方案 A(推荐):</strong>按计划产品对齐成行(本单 3 行)。
|
||
「合同」拆成 <strong>供合同 / 需合同</strong> 两列;计划量若两侧一致可只保留一列,也可再拆供/需计划量。
|
||
<strong>无 rowspan / colspan</strong>,计划级字段在每行重复展示(或仅首行填、其余留空——本 demo 采用重复,便于导出/筛选)。
|
||
</p>
|
||
<p class="ok">
|
||
能解决:去掉合并单元格后,前端不再依赖 <code>span-method</code>,导出 Excel 也是标准扁平行;
|
||
筛选「需合同」「产品」时行语义清晰。代价是列变宽、计划级信息有重复。
|
||
</p>
|
||
<div class="table-wrap">
|
||
<table class="list">
|
||
<thead>
|
||
<tr>
|
||
<th>计划编号</th>
|
||
<th class="center">跟进状态</th>
|
||
<th>车牌号</th>
|
||
<th>人员</th>
|
||
<th>跟进人</th>
|
||
<th>装货客户</th>
|
||
<th>送货客户</th>
|
||
<th>装货地址/时间</th>
|
||
<th>送货地址/时间</th>
|
||
<th class="th-group-load">供合同</th>
|
||
<th class="th-group-need">需合同</th>
|
||
<th>产品名称</th>
|
||
<th class="center">型号/规格</th>
|
||
<th class="center">包装规格</th>
|
||
<th class="center">单位</th>
|
||
<th class="num">计划量</th>
|
||
<th class="num">实际量</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td><a href="#">PO20261009022</a></td>
|
||
<td class="center"><span class="tag warn">待安排</span></td>
|
||
<td>皖H21522</td>
|
||
<td>黄天宝(押)、沈卫斌(驾)</td>
|
||
<td>程欢欢</td>
|
||
<td>安庆市山峡化工有限公司</td>
|
||
<td>桐城运城制版有限公司</td>
|
||
<td>
|
||
<div class="stack">
|
||
<span class="muted">—</span>
|
||
<span class="sub">2026-10-12</span>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<div class="stack">
|
||
<span class="muted">—</span>
|
||
<span class="sub">2026-10-12</span>
|
||
</div>
|
||
</td>
|
||
<td class="cell-load"><span class="muted">—</span></td>
|
||
<td class="cell-need"><a href="#">SX/X-JT20261008006</a></td>
|
||
<td>
|
||
<span class="name-with-mark">
|
||
98% 工业硫酸
|
||
<span class="tag warn">毒</span>
|
||
</span>
|
||
</td>
|
||
<td class="center">98%</td>
|
||
<td class="center">25kg/桶</td>
|
||
<td class="center">吨</td>
|
||
<td class="num">1</td>
|
||
<td class="num">0</td>
|
||
</tr>
|
||
<tr>
|
||
<td><a href="#">PO20261009022</a></td>
|
||
<td class="center"><span class="tag warn">待安排</span></td>
|
||
<td>皖H21522</td>
|
||
<td>黄天宝(押)、沈卫斌(驾)</td>
|
||
<td>程欢欢</td>
|
||
<td>安庆市山峡化工有限公司</td>
|
||
<td>桐城运城制版有限公司</td>
|
||
<td>
|
||
<div class="stack">
|
||
<span class="muted">—</span>
|
||
<span class="sub">2026-10-12</span>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<div class="stack">
|
||
<span class="muted">—</span>
|
||
<span class="sub">2026-10-12</span>
|
||
</div>
|
||
</td>
|
||
<td class="cell-load"><span class="muted">—</span></td>
|
||
<td class="cell-need"><a href="#">SX/X-JT20261008006</a></td>
|
||
<td>聚合氯化铝</td>
|
||
<td class="center"><span class="muted">—</span></td>
|
||
<td class="center">25Kg/袋</td>
|
||
<td class="center">吨</td>
|
||
<td class="num">1</td>
|
||
<td class="num">0</td>
|
||
</tr>
|
||
<tr>
|
||
<td><a href="#">PO20261009022</a></td>
|
||
<td class="center"><span class="tag warn">待安排</span></td>
|
||
<td>皖H21522</td>
|
||
<td>黄天宝(押)、沈卫斌(驾)</td>
|
||
<td>程欢欢</td>
|
||
<td>安庆市山峡化工有限公司</td>
|
||
<td>桐城运城制版有限公司</td>
|
||
<td>
|
||
<div class="stack">
|
||
<span class="muted">—</span>
|
||
<span class="sub">2026-10-12</span>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<div class="stack">
|
||
<span class="muted">—</span>
|
||
<span class="sub">2026-10-12</span>
|
||
</div>
|
||
</td>
|
||
<td class="cell-load"><span class="muted">—</span></td>
|
||
<td class="cell-need"><a href="#">SX/X-JT20261008006</a></td>
|
||
<td>32%液碱</td>
|
||
<td class="center">32%</td>
|
||
<td class="center">25kg/桶</td>
|
||
<td class="center">吨</td>
|
||
<td class="num">0.25</td>
|
||
<td class="num">0</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<p class="meta">
|
||
行粒度:计划产品(装/送两侧按 <code>plan_product_id</code> 对齐)。本单 load×3 + delivery×3 收敛为 <strong>3 行</strong>,不再拆成 6 行供/需。
|
||
</p>
|
||
</section>
|
||
|
||
<!-- 方案 B -->
|
||
<section class="panel" id="panel-b">
|
||
<p class="desc">
|
||
<strong>方案 B:</strong>整份计划只占 <strong>1 行</strong>。供/需合同、产品明细在单元格内用多行文本堆叠,
|
||
同样 <strong>无合并单元格</strong>。适合「一眼看完整单」;不适合按产品行勾选/导出明细。
|
||
</p>
|
||
<p class="ok">实现成本最低(去掉 flatten + span-method 即可),但表格行高随产品数膨胀,筛选产品时仍是计划粒度。</p>
|
||
<div class="table-wrap">
|
||
<table class="list" style="min-width: 1320px;">
|
||
<thead>
|
||
<tr>
|
||
<th>计划编号</th>
|
||
<th class="center">跟进状态</th>
|
||
<th>车牌号</th>
|
||
<th>人员</th>
|
||
<th>跟进人</th>
|
||
<th>装货客户</th>
|
||
<th>送货客户</th>
|
||
<th>装货地址/时间</th>
|
||
<th>送货地址/时间</th>
|
||
<th class="th-group-load">供合同</th>
|
||
<th class="th-group-need">需合同</th>
|
||
<th>产品明细</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td><a href="#">PO20261009022</a></td>
|
||
<td class="center"><span class="tag warn">待安排</span></td>
|
||
<td>皖H21522</td>
|
||
<td>黄天宝(押)、沈卫斌(驾)</td>
|
||
<td>程欢欢</td>
|
||
<td>安庆市山峡化工有限公司</td>
|
||
<td>桐城运城制版有限公司</td>
|
||
<td>
|
||
<div class="stack">
|
||
<span class="muted">—</span>
|
||
<span class="sub">2026-10-12</span>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<div class="stack">
|
||
<span class="muted">—</span>
|
||
<span class="sub">2026-10-12</span>
|
||
</div>
|
||
</td>
|
||
<td class="cell-load"><span class="muted">—</span></td>
|
||
<td class="cell-need"><a href="#">SX/X-JT20261008006</a></td>
|
||
<td>
|
||
<div class="stack">
|
||
<span class="name-with-mark">98% 工业硫酸 <span class="tag warn">毒</span> · 98% · 25kg/桶 · 1 吨</span>
|
||
<span>聚合氯化铝 · 25Kg/袋 · 1 吨</span>
|
||
<span>32%液碱 · 32% · 25kg/桶 · 0.25 吨</span>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 对照现状 -->
|
||
<section class="panel" id="panel-c">
|
||
<p class="pain">
|
||
<strong>现状对照:</strong>一份计划拆成供行 + 需行(本单 6 行),「合同 / 产品…」共用列头;
|
||
计划编号、装货客户等靠 <code>rowspan</code> 合并。视觉上「从合同起供/需多行」,也是导出与勾选复杂的根源。
|
||
</p>
|
||
<div class="table-wrap">
|
||
<table class="list" style="min-width: 1200px;">
|
||
<thead>
|
||
<tr>
|
||
<th>计划编号</th>
|
||
<th>装货客户</th>
|
||
<th>送货客户</th>
|
||
<th>合同</th>
|
||
<th>产品名称</th>
|
||
<th class="center">型号/规格</th>
|
||
<th class="center">包装规格</th>
|
||
<th class="center">单位</th>
|
||
<th class="num">计划量</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td rowspan="6"><a href="#">PO20261009022</a></td>
|
||
<td rowspan="6">安庆市山峡化工有限公司</td>
|
||
<td rowspan="6">桐城运城制版有限公司</td>
|
||
<td rowspan="3" class="cell-load"><span class="tag side-load">供</span> <span class="muted">—</span></td>
|
||
<td class="cell-load"><span class="name-with-mark">98% 工业硫酸 <span class="tag warn">毒</span></span></td>
|
||
<td class="center cell-load">98%</td>
|
||
<td class="center cell-load">25kg/桶</td>
|
||
<td class="center cell-load">吨</td>
|
||
<td class="num cell-load">1</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="cell-load">聚合氯化铝</td>
|
||
<td class="center cell-load"><span class="muted">—</span></td>
|
||
<td class="center cell-load">25Kg/袋</td>
|
||
<td class="center cell-load">吨</td>
|
||
<td class="num cell-load">1</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="cell-load">32%液碱</td>
|
||
<td class="center cell-load">32%</td>
|
||
<td class="center cell-load">25kg/桶</td>
|
||
<td class="center cell-load">吨</td>
|
||
<td class="num cell-load">0.25</td>
|
||
</tr>
|
||
<tr>
|
||
<td rowspan="3" class="cell-need"><span class="tag side-need">需</span> <a href="#">SX/X-JT20261008006</a></td>
|
||
<td class="cell-need"><span class="name-with-mark">98% 工业硫酸 <span class="tag warn">毒</span></span></td>
|
||
<td class="center cell-need">98%</td>
|
||
<td class="center cell-need">25kg/桶</td>
|
||
<td class="center cell-need">吨</td>
|
||
<td class="num cell-need">1</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="cell-need">聚合氯化铝</td>
|
||
<td class="center cell-need"><span class="muted">—</span></td>
|
||
<td class="center cell-need">25Kg/袋</td>
|
||
<td class="center cell-need">吨</td>
|
||
<td class="num cell-need">1</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="cell-need">32%液碱</td>
|
||
<td class="center cell-need">32%</td>
|
||
<td class="center cell-need">25kg/桶</td>
|
||
<td class="center cell-need">吨</td>
|
||
<td class="num cell-need">0.25</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<p class="meta">本对照刻意保留 rowspan,仅用于对比;方案 A/B 均不使用合并单元格。</p>
|
||
</section>
|
||
</div>
|
||
|
||
<script>
|
||
const tabs = document.querySelectorAll('.tab')
|
||
const panels = {
|
||
a: document.getElementById('panel-a'),
|
||
b: document.getElementById('panel-b'),
|
||
c: document.getElementById('panel-c')
|
||
}
|
||
tabs.forEach((tab) => {
|
||
tab.addEventListener('click', () => {
|
||
const key = tab.getAttribute('data-tab')
|
||
tabs.forEach((t) => t.classList.toggle('active', t === tab))
|
||
Object.entries(panels).forEach(([k, el]) => el.classList.toggle('active', k === key))
|
||
})
|
||
})
|
||
</script>
|
||
</body>
|
||
</html>
|