Align Fresh Mint §7.6 and OpenSpec ui-conventions with the confirmed UX; include design and plan. Co-authored-by: Cursor <cursoragent@cursor.com>
805 lines
25 KiB
HTML
805 lines
25 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>列表滚动体验 Demo · Fresh Mint</title>
|
||
<style>
|
||
:root {
|
||
--primary: #10b981;
|
||
--primary-dark: #059669;
|
||
--primary-bg: #ecfdf5;
|
||
--text: #1e293b;
|
||
--text-muted: #64748b;
|
||
--border: #e2e8f0;
|
||
--surface: #fff;
|
||
--bg: #f1f5f9;
|
||
--table-head: #f8fafc;
|
||
--row-alt: #f8fafc;
|
||
--row-hover: #f0fdf4;
|
||
--radius: 10px;
|
||
--danger: #dc2626;
|
||
--warning: #d97706;
|
||
--shadow: 0 1px 2px rgba(15, 23, 42, 0.06);
|
||
font-family: Inter, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
|
||
}
|
||
|
||
* { box-sizing: border-box; }
|
||
body {
|
||
margin: 0;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.page {
|
||
max-width: 1180px;
|
||
margin: 0 auto;
|
||
padding: 24px 20px 48px;
|
||
}
|
||
|
||
.hero {
|
||
background: linear-gradient(135deg, #ecfdf5 0%, #fff 55%, #f8fafc 100%);
|
||
border: 1px solid var(--border);
|
||
border-radius: 14px;
|
||
padding: 22px 24px;
|
||
margin-bottom: 18px;
|
||
box-shadow: var(--shadow);
|
||
}
|
||
.hero h1 {
|
||
margin: 0 0 8px;
|
||
font-size: 22px;
|
||
font-weight: 700;
|
||
color: var(--text);
|
||
}
|
||
.hero h1::before {
|
||
content: "";
|
||
display: inline-block;
|
||
width: 4px;
|
||
height: 1.05em;
|
||
margin-right: 10px;
|
||
border-radius: 2px;
|
||
background: var(--primary);
|
||
vertical-align: -0.12em;
|
||
}
|
||
.hero p {
|
||
margin: 0;
|
||
color: var(--text-muted);
|
||
max-width: 780px;
|
||
}
|
||
.problem-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-top: 14px;
|
||
}
|
||
.pill {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 4px 10px;
|
||
border-radius: 999px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
border: 1px solid transparent;
|
||
}
|
||
.pill.warn {
|
||
background: #fffbeb;
|
||
color: #b45309;
|
||
border-color: #fde68a;
|
||
}
|
||
.pill.ok {
|
||
background: var(--primary-bg);
|
||
color: var(--primary-dark);
|
||
border-color: #a7f3d0;
|
||
}
|
||
|
||
.tabs {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-bottom: 14px;
|
||
}
|
||
.tab {
|
||
appearance: none;
|
||
border: 1px solid var(--border);
|
||
background: var(--surface);
|
||
color: var(--text);
|
||
border-radius: 8px;
|
||
padding: 8px 14px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: 0.15s ease;
|
||
}
|
||
.tab:hover { border-color: #a7f3d0; background: var(--primary-bg); }
|
||
.tab.active {
|
||
background: var(--primary);
|
||
border-color: var(--primary);
|
||
color: #fff;
|
||
}
|
||
.tab .badge {
|
||
display: inline-block;
|
||
margin-left: 6px;
|
||
padding: 1px 6px;
|
||
border-radius: 999px;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
background: rgba(255,255,255,0.22);
|
||
}
|
||
.tab:not(.active) .badge {
|
||
background: #fee2e2;
|
||
color: #b91c1c;
|
||
}
|
||
.tab:not(.active).recommend .badge {
|
||
background: var(--primary-bg);
|
||
color: var(--primary-dark);
|
||
}
|
||
|
||
.panel {
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
box-shadow: var(--shadow);
|
||
overflow: hidden;
|
||
}
|
||
.panel-top {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
padding: 14px 18px;
|
||
border-bottom: 1px solid var(--border);
|
||
background: var(--table-head);
|
||
}
|
||
.panel-top h2 {
|
||
margin: 0 0 4px;
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
}
|
||
.panel-top .desc {
|
||
margin: 0;
|
||
color: var(--text-muted);
|
||
font-size: 13px;
|
||
max-width: 720px;
|
||
}
|
||
.meta {
|
||
flex-shrink: 0;
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
text-align: right;
|
||
}
|
||
.meta strong { color: var(--primary-dark); }
|
||
|
||
.callout {
|
||
margin: 0;
|
||
padding: 10px 18px;
|
||
font-size: 13px;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
.callout.bad { background: #fef2f2; color: #991b1b; }
|
||
.callout.good { background: var(--primary-bg); color: #065f46; }
|
||
.callout code {
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
font-size: 12px;
|
||
background: rgba(0,0,0,0.05);
|
||
padding: 1px 5px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
padding: 8px 16px;
|
||
background: var(--table-head);
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
.sel {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--text-muted);
|
||
padding-right: 10px;
|
||
margin-right: 4px;
|
||
border-right: 1px solid var(--border);
|
||
min-height: 36px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
}
|
||
.sel b { color: var(--primary-dark); margin: 0 2px; }
|
||
.btn {
|
||
height: 36px;
|
||
padding: 0 14px;
|
||
border-radius: 8px;
|
||
border: 1px solid #d9e8e2;
|
||
background: #fff;
|
||
color: var(--text);
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
cursor: default;
|
||
}
|
||
.btn.main {
|
||
background: var(--primary);
|
||
border-color: var(--primary);
|
||
color: #fff;
|
||
}
|
||
|
||
.viewport {
|
||
/* 模拟管理端内容区可视高度 */
|
||
height: min(420px, 58vh);
|
||
overflow: auto;
|
||
background: #fff;
|
||
position: relative;
|
||
}
|
||
.viewport-label {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 20;
|
||
pointer-events: none;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
padding: 6px 10px 0 0;
|
||
height: 0;
|
||
}
|
||
.viewport-label span {
|
||
pointer-events: none;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
color: #0369a1;
|
||
background: #e0f2fe;
|
||
border: 1px solid #7dd3fc;
|
||
border-radius: 999px;
|
||
padding: 2px 8px;
|
||
}
|
||
|
||
table.data {
|
||
width: max-content;
|
||
min-width: 1400px;
|
||
border-collapse: collapse;
|
||
font-size: 13px;
|
||
}
|
||
table.data th,
|
||
table.data td {
|
||
padding: 10px 14px;
|
||
border-bottom: 1px solid var(--border);
|
||
text-align: center;
|
||
white-space: nowrap;
|
||
}
|
||
table.data th {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--text-muted);
|
||
background: var(--table-head);
|
||
}
|
||
table.data tbody tr:nth-child(even) td { background: var(--row-alt); }
|
||
table.data tbody tr:hover td { background: var(--row-hover); }
|
||
table.data .left { text-align: left; }
|
||
table.data .op { min-width: 180px; }
|
||
.link { color: var(--primary-dark); font-weight: 600; text-decoration: none; }
|
||
.tag {
|
||
display: inline-block;
|
||
padding: 2px 8px;
|
||
border-radius: 6px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #fff;
|
||
background: var(--primary-dark);
|
||
}
|
||
.tag.warn { background: var(--warning); }
|
||
|
||
.pager {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 10px 16px;
|
||
border-top: 1px solid var(--border);
|
||
color: var(--text-muted);
|
||
font-size: 13px;
|
||
background: #fff;
|
||
}
|
||
|
||
/* —— 方案 A:表体内嵌滚动(推荐) —— */
|
||
.scheme-a .table-frame {
|
||
height: 320px;
|
||
overflow: auto;
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
.scheme-a thead th {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 3;
|
||
box-shadow: 0 1px 0 rgba(0,0,0,0.06);
|
||
}
|
||
.scheme-a .th-check,
|
||
.scheme-a .td-check {
|
||
position: sticky;
|
||
left: 0;
|
||
z-index: 2;
|
||
background: #fff;
|
||
box-shadow: 6px 0 10px -6px rgba(0,0,0,0.12);
|
||
}
|
||
.scheme-a thead .th-check {
|
||
z-index: 6;
|
||
background: var(--table-head);
|
||
}
|
||
.scheme-a tbody tr:nth-child(even) .td-check { background: var(--row-alt); }
|
||
.scheme-a tbody tr:hover .td-check { background: var(--row-hover); }
|
||
.scheme-a .th-op,
|
||
.scheme-a .td-op {
|
||
position: sticky;
|
||
right: 0;
|
||
z-index: 2;
|
||
background: #fff;
|
||
box-shadow: -6px 0 10px -6px rgba(0,0,0,0.12);
|
||
}
|
||
.scheme-a thead .th-op {
|
||
z-index: 5;
|
||
background: var(--table-head);
|
||
}
|
||
.scheme-a tbody tr:nth-child(even) .td-op { background: var(--row-alt); }
|
||
.scheme-a tbody tr:hover .td-op { background: var(--row-hover); }
|
||
|
||
/* —— 方案 B:页面滚动 + 表头 sticky + 底部悬浮横滚条 —— */
|
||
.scheme-b .page-scroll {
|
||
height: 320px;
|
||
overflow: auto;
|
||
border-top: 1px solid var(--border);
|
||
position: relative;
|
||
}
|
||
.scheme-b thead th {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 3;
|
||
box-shadow: 0 1px 0 rgba(0,0,0,0.06);
|
||
}
|
||
.scheme-b .float-hbar-wrap {
|
||
position: sticky;
|
||
bottom: 0;
|
||
z-index: 8;
|
||
height: 14px;
|
||
background: rgba(248, 250, 252, 0.92);
|
||
border-top: 1px solid var(--border);
|
||
backdrop-filter: blur(2px);
|
||
}
|
||
.scheme-b .float-hbar {
|
||
overflow-x: auto;
|
||
overflow-y: hidden;
|
||
height: 14px;
|
||
}
|
||
.scheme-b .float-hbar-inner {
|
||
height: 1px;
|
||
}
|
||
|
||
/* —— 方案 C:顶部镜像横滚条 + 表体内嵌滚动 —— */
|
||
.scheme-c .top-hbar {
|
||
overflow-x: auto;
|
||
overflow-y: hidden;
|
||
height: 14px;
|
||
border-top: 1px solid var(--border);
|
||
background: var(--table-head);
|
||
}
|
||
.scheme-c .top-hbar-inner { height: 1px; }
|
||
.scheme-c .table-frame {
|
||
height: 306px;
|
||
overflow: auto;
|
||
}
|
||
.scheme-c thead th {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 3;
|
||
box-shadow: 0 1px 0 rgba(0,0,0,0.06);
|
||
}
|
||
|
||
.hint-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 10px;
|
||
margin-top: 14px;
|
||
}
|
||
@media (max-width: 900px) {
|
||
.hint-row { grid-template-columns: 1fr; }
|
||
}
|
||
.hint-card {
|
||
background: #fff;
|
||
border: 1px solid var(--border);
|
||
border-radius: 10px;
|
||
padding: 12px 14px;
|
||
box-shadow: var(--shadow);
|
||
}
|
||
.hint-card h3 {
|
||
margin: 0 0 6px;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
}
|
||
.hint-card p {
|
||
margin: 0;
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
}
|
||
.hidden { display: none !important; }
|
||
|
||
.choose {
|
||
margin-top: 18px;
|
||
padding: 16px 18px;
|
||
background: #fff;
|
||
border: 1px solid var(--border);
|
||
border-radius: 12px;
|
||
}
|
||
.choose h3 { margin: 0 0 10px; font-size: 15px; }
|
||
.choose-options {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 10px;
|
||
}
|
||
@media (max-width: 900px) {
|
||
.choose-options { grid-template-columns: 1fr; }
|
||
}
|
||
.choose-opt {
|
||
border: 1px solid var(--border);
|
||
border-radius: 10px;
|
||
padding: 12px;
|
||
cursor: pointer;
|
||
transition: 0.15s ease;
|
||
background: #fff;
|
||
}
|
||
.choose-opt:hover { border-color: #a7f3d0; background: var(--primary-bg); }
|
||
.choose-opt.selected {
|
||
border-color: var(--primary);
|
||
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.25);
|
||
background: var(--primary-bg);
|
||
}
|
||
.choose-opt .letter {
|
||
display: inline-flex;
|
||
width: 22px;
|
||
height: 22px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 6px;
|
||
background: var(--primary);
|
||
color: #fff;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
margin-right: 6px;
|
||
}
|
||
.choose-opt strong { font-size: 13px; }
|
||
.choose-opt p {
|
||
margin: 8px 0 0;
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="page">
|
||
<div class="hero">
|
||
<h1>列表横向 / 纵向滚动体验 Demo</h1>
|
||
<p>
|
||
对齐 web-admin Fresh Mint。用下方 Tab 切换「现状问题」与三种改法,
|
||
请在灰色模拟视口内<strong>纵向滚动到中部</strong>、再试<strong>横向滚动</strong>,感受差异。
|
||
</p>
|
||
<div class="problem-list">
|
||
<span class="pill warn">问题 1:横滚条在表最底部,行多时够不着</span>
|
||
<span class="pill warn">问题 2:纵滚到中部后看不到表头</span>
|
||
<span class="pill ok">目标:横滚条常在可视区底部 · 表头始终可见</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tabs" role="tablist">
|
||
<button class="tab active" data-tab="now" type="button">现状(问题复现)<span class="badge">Bug</span></button>
|
||
<button class="tab recommend" data-tab="a" type="button">方案 A · 表体内嵌滚动<span class="badge">推荐</span></button>
|
||
<button class="tab" data-tab="b" type="button">方案 B · 表头 sticky + 悬浮横条</button>
|
||
<button class="tab" data-tab="c" type="button">方案 C · 顶部镜像横条</button>
|
||
</div>
|
||
|
||
<!-- 现状 -->
|
||
<section class="panel scheme-now" data-panel="now">
|
||
<div class="panel-top">
|
||
<div>
|
||
<h2>现状:整页拉长,表跟着页面滚</h2>
|
||
<p class="desc">与多数业务列表一致:表格无固定高度,横向滚动发生在整张表底部。</p>
|
||
</div>
|
||
<div class="meta">模拟视口<br /><strong>请先向下滚半屏</strong></div>
|
||
</div>
|
||
<p class="callout bad">
|
||
复现:① 纵向滚到行 20+ → 表头消失;② 想横看「操作」列 → 必须滚到最底才能碰到横滚条。
|
||
</p>
|
||
<div class="toolbar">
|
||
<span class="sel">已选 <b>0</b> 项</span>
|
||
<button class="btn main" type="button">新增</button>
|
||
<button class="btn" type="button">批量导出</button>
|
||
</div>
|
||
<div class="viewport" id="vp-now">
|
||
<div class="viewport-label"><span>内容区可视范围(向下滚)</span></div>
|
||
<div class="table-scroll-full" style="overflow-x:auto;">
|
||
<table class="data" id="table-now"></table>
|
||
</div>
|
||
<div class="pager"><span>共 60 条</span><span>1 / 3 页(示意)</span></div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 方案 A -->
|
||
<section class="panel scheme-a hidden" data-panel="a">
|
||
<div class="panel-top">
|
||
<div>
|
||
<h2>方案 A:表格视口填满高度(内嵌滚动)</h2>
|
||
<p class="desc">
|
||
对齐规范:动态 <code>height</code> 使表体尽量撑满,
|
||
<strong>分页始终可见且下方留 10px</strong>;
|
||
纵滚在表体内,表头 sticky,横滚条贴表体可视区底。
|
||
</p>
|
||
</div>
|
||
<div class="meta">实现成本低<br /><strong>与 EP 官方行为一致</strong></div>
|
||
</div>
|
||
<p class="callout good">
|
||
一次解决两个问题:表头始终在表体顶部;横滚条始终在表体可视区底部(不必滚到第 60 行);分页下约 10px。
|
||
</p>
|
||
<div class="toolbar">
|
||
<span class="sel">已选 <b>0</b> 项</span>
|
||
<button class="btn main" type="button">新增</button>
|
||
<button class="btn" type="button">批量导出</button>
|
||
</div>
|
||
<div class="viewport" style="height:auto; overflow:visible;">
|
||
<div class="table-frame" id="vp-a">
|
||
<table class="data" id="table-a"></table>
|
||
</div>
|
||
<div class="pager"><span>共 60 条 · 分页固定在表外</span><span>1 / 3 页</span></div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 方案 B -->
|
||
<section class="panel scheme-b hidden" data-panel="b">
|
||
<div class="panel-top">
|
||
<div>
|
||
<h2>方案 B:保留整页滚动 + sticky 表头 + 底部悬浮横滚条</h2>
|
||
<p class="desc">
|
||
表仍可随页面变高;用 sticky 钉住表头;在滚动容器底部再放一条「镜像」横滚条,始终可见。
|
||
</p>
|
||
</div>
|
||
<div class="meta">交互灵活<br /><strong>实现与同步成本更高</strong></div>
|
||
</div>
|
||
<p class="callout good">
|
||
适合「不想裁切表体高度、希望一屏看更多行」的场景;需维护真实横滚与悬浮条 scrollLeft 双向同步。
|
||
</p>
|
||
<div class="toolbar">
|
||
<span class="sel">已选 <b>0</b> 项</span>
|
||
<button class="btn main" type="button">新增</button>
|
||
<button class="btn" type="button">批量导出</button>
|
||
</div>
|
||
<div class="viewport" style="height:auto; overflow:visible;">
|
||
<div class="page-scroll" id="vp-b">
|
||
<div class="table-x" id="b-table-x" style="overflow-x:auto;">
|
||
<table class="data" id="table-b"></table>
|
||
</div>
|
||
<div class="float-hbar-wrap">
|
||
<div class="float-hbar" id="b-float-hbar">
|
||
<div class="float-hbar-inner" id="b-float-inner"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="pager"><span>共 60 条</span><span>试拖底部细条或表内横滚</span></div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 方案 C -->
|
||
<section class="panel scheme-c hidden" data-panel="c">
|
||
<div class="panel-top">
|
||
<div>
|
||
<h2>方案 C:表体内嵌滚动 + 顶部镜像横滚条</h2>
|
||
<p class="desc">
|
||
在方案 A 基础上,表头上方再放一条横滚条:人眼在看标题附近时也能横移,无需移到表底。
|
||
</p>
|
||
</div>
|
||
<div class="meta">A 的增强<br /><strong>双条同步,略占高度</strong></div>
|
||
</div>
|
||
<p class="callout good">
|
||
横滚可达性最好(顶/底都能拖)。若采用 A,可按「列特别多」的页面再叠加顶部条。
|
||
</p>
|
||
<div class="toolbar">
|
||
<span class="sel">已选 <b>0</b> 项</span>
|
||
<button class="btn main" type="button">新增</button>
|
||
<button class="btn" type="button">批量导出</button>
|
||
</div>
|
||
<div class="viewport" style="height:auto; overflow:visible;">
|
||
<div class="top-hbar" id="c-top-hbar">
|
||
<div class="top-hbar-inner" id="c-top-inner"></div>
|
||
</div>
|
||
<div class="table-frame" id="vp-c">
|
||
<table class="data" id="table-c"></table>
|
||
</div>
|
||
<div class="pager"><span>共 60 条</span><span>顶条与表体横滚已同步</span></div>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="hint-row">
|
||
<div class="hint-card">
|
||
<h3>与现有代码的关系</h3>
|
||
<p>
|
||
规范 §7.6 已写 <code>max-height + overflow</code>;HTML demo 也有 sticky 表头。
|
||
业务页多数未给 <code>el-table</code> 设高度,bx 的 <code>.table-scroll</code> 仅 <code>overflow-x</code>,故问题仍在。
|
||
</p>
|
||
</div>
|
||
<div class="hint-card">
|
||
<h3>推荐落地路径</h3>
|
||
<p>
|
||
优先方案 A:公共列表用 <code>useListTableViewportHeight</code> 填满视口(分页下 10px),分页留在表外。
|
||
列极多的页可再加方案 C 顶条。
|
||
</p>
|
||
</div>
|
||
<div class="hint-card">
|
||
<h3>注意点</h3>
|
||
<p>
|
||
固定高度后筛选区展开会挤表体,需用「视口剩余高度」动态计算;
|
||
展开行 / 树表要回归测 <code>doLayout</code>。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="choose">
|
||
<h3>看完后请选择倾向方案(可点选,并在对话里告诉我)</h3>
|
||
<div class="choose-options">
|
||
<div class="choose-opt" data-choice="a" tabindex="0">
|
||
<span class="letter">A</span><strong>表体内嵌滚动</strong>
|
||
<p>成本最低,一次修两个问题;与 Element Plus / 规范最贴合。</p>
|
||
</div>
|
||
<div class="choose-opt" data-choice="b" tabindex="0">
|
||
<span class="letter">B</span><strong>sticky + 悬浮横条</strong>
|
||
<p>保留「整表随页面变高」;同步逻辑与兼容成本更高。</p>
|
||
</div>
|
||
<div class="choose-opt" data-choice="c" tabindex="0">
|
||
<span class="letter">C</span><strong>A + 顶部镜像条</strong>
|
||
<p>在 A 上增强横滚可达性;适合列特别宽的列表。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const COLS = [
|
||
{ key: "check", label: "", cls: "th-check" },
|
||
{ key: "idx", label: "序号" },
|
||
{ key: "code", label: "单据编号", left: true },
|
||
{ key: "name", label: "名称", left: true },
|
||
{ key: "customer", label: "客户" },
|
||
{ key: "region", label: "区域路径" },
|
||
{ key: "amount", label: "金额" },
|
||
{ key: "status", label: "状态" },
|
||
{ key: "owner", label: "负责人" },
|
||
{ key: "dept", label: "所属部门" },
|
||
{ key: "created", label: "创建时间" },
|
||
{ key: "updated", label: "更新时间" },
|
||
{ key: "remark", label: "备注", left: true },
|
||
{ key: "op", label: "操作", cls: "th-op op" }
|
||
];
|
||
|
||
function buildTable(tableEl, rows) {
|
||
const thead = document.createElement("thead");
|
||
const hr = document.createElement("tr");
|
||
COLS.forEach((c) => {
|
||
const th = document.createElement("th");
|
||
th.textContent = c.label;
|
||
if (c.cls) th.className = c.cls;
|
||
if (c.left) th.classList.add("left");
|
||
hr.appendChild(th);
|
||
});
|
||
thead.appendChild(hr);
|
||
|
||
const tbody = document.createElement("tbody");
|
||
for (let i = 1; i <= rows; i++) {
|
||
const tr = document.createElement("tr");
|
||
const cells = [
|
||
{ html: '<input type="checkbox" />', cls: "td-check" },
|
||
{ text: String(i) },
|
||
{ html: `<a class="link" href="#">SO-2026-${String(i).padStart(4, "0")}</a>`, left: true },
|
||
{ text: `示例业务名称 ${i}`, left: true },
|
||
{ text: i % 3 === 0 ? "华东化工" : i % 3 === 1 ? "山峡贸易" : "三墙供应链" },
|
||
{ text: "华东 / 上海 / 浦东" },
|
||
{ text: (1200 + i * 37).toLocaleString("zh-CN") },
|
||
{ html: i % 5 === 0 ? '<span class="tag warn">待处理</span>' : '<span class="tag">进行中</span>' },
|
||
{ text: i % 2 ? "张三" : "李四" },
|
||
{ text: "销售一部" },
|
||
{ text: "2026-08-01 09:12" },
|
||
{ text: "2026-09-01 18:00" },
|
||
{ text: i % 4 === 0 ? "需要横向滚动才能看到本列与操作列" : "—", left: true },
|
||
{ html: '<a class="link" href="#">查看</a> · 编辑', cls: "td-op op" }
|
||
];
|
||
cells.forEach((cell) => {
|
||
const td = document.createElement("td");
|
||
if (cell.html) td.innerHTML = cell.html;
|
||
else td.textContent = cell.text;
|
||
if (cell.cls) td.className = cell.cls;
|
||
if (cell.left) td.classList.add("left");
|
||
tr.appendChild(td);
|
||
});
|
||
tbody.appendChild(tr);
|
||
}
|
||
|
||
tableEl.innerHTML = "";
|
||
tableEl.appendChild(thead);
|
||
tableEl.appendChild(tbody);
|
||
}
|
||
|
||
["table-now", "table-a", "table-b", "table-c"].forEach((id) => {
|
||
buildTable(document.getElementById(id), 60);
|
||
});
|
||
|
||
// Tabs
|
||
const tabs = document.querySelectorAll(".tab");
|
||
const panels = document.querySelectorAll("[data-panel]");
|
||
tabs.forEach((tab) => {
|
||
tab.addEventListener("click", () => {
|
||
tabs.forEach((t) => t.classList.remove("active"));
|
||
tab.classList.add("active");
|
||
const key = tab.dataset.tab;
|
||
panels.forEach((p) => {
|
||
p.classList.toggle("hidden", p.dataset.panel !== key);
|
||
});
|
||
if (key === "b") syncB();
|
||
if (key === "c") syncC();
|
||
});
|
||
});
|
||
|
||
// Choice
|
||
document.querySelectorAll(".choose-opt").forEach((el) => {
|
||
el.addEventListener("click", () => {
|
||
document.querySelectorAll(".choose-opt").forEach((x) => x.classList.remove("selected"));
|
||
el.classList.add("selected");
|
||
});
|
||
});
|
||
|
||
// Scheme B sync
|
||
function syncB() {
|
||
const x = document.getElementById("b-table-x");
|
||
const bar = document.getElementById("b-float-hbar");
|
||
const inner = document.getElementById("b-float-inner");
|
||
const table = document.getElementById("table-b");
|
||
inner.style.width = table.scrollWidth + "px";
|
||
let lock = false;
|
||
const bind = (from, to) => {
|
||
from.onscroll = () => {
|
||
if (lock) return;
|
||
lock = true;
|
||
to.scrollLeft = from.scrollLeft;
|
||
lock = false;
|
||
};
|
||
};
|
||
bind(x, bar);
|
||
bind(bar, x);
|
||
}
|
||
|
||
// Scheme C sync
|
||
function syncC() {
|
||
const frame = document.getElementById("vp-c");
|
||
const top = document.getElementById("c-top-hbar");
|
||
const inner = document.getElementById("c-top-inner");
|
||
const table = document.getElementById("table-c");
|
||
inner.style.width = table.scrollWidth + "px";
|
||
let lock = false;
|
||
frame.onscroll = () => {
|
||
if (lock) return;
|
||
lock = true;
|
||
top.scrollLeft = frame.scrollLeft;
|
||
lock = false;
|
||
};
|
||
top.onscroll = () => {
|
||
if (lock) return;
|
||
lock = true;
|
||
frame.scrollLeft = top.scrollLeft;
|
||
lock = false;
|
||
};
|
||
}
|
||
|
||
// init sync widths after paint
|
||
requestAnimationFrame(() => {
|
||
syncB();
|
||
syncC();
|
||
});
|
||
window.addEventListener("resize", () => {
|
||
syncB();
|
||
syncC();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|