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