list-table-scroll-demo.html 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1" />
  6. <title>列表滚动体验 Demo · Fresh Mint</title>
  7. <style>
  8. :root {
  9. --primary: #10b981;
  10. --primary-dark: #059669;
  11. --primary-bg: #ecfdf5;
  12. --text: #1e293b;
  13. --text-muted: #64748b;
  14. --border: #e2e8f0;
  15. --surface: #fff;
  16. --bg: #f1f5f9;
  17. --table-head: #f8fafc;
  18. --row-alt: #f8fafc;
  19. --row-hover: #f0fdf4;
  20. --radius: 10px;
  21. --danger: #dc2626;
  22. --warning: #d97706;
  23. --shadow: 0 1px 2px rgba(15, 23, 42, 0.06);
  24. font-family: Inter, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
  25. }
  26. * { box-sizing: border-box; }
  27. body {
  28. margin: 0;
  29. background: var(--bg);
  30. color: var(--text);
  31. font-size: 14px;
  32. line-height: 1.5;
  33. }
  34. .page {
  35. max-width: 1180px;
  36. margin: 0 auto;
  37. padding: 24px 20px 48px;
  38. }
  39. .hero {
  40. background: linear-gradient(135deg, #ecfdf5 0%, #fff 55%, #f8fafc 100%);
  41. border: 1px solid var(--border);
  42. border-radius: 14px;
  43. padding: 22px 24px;
  44. margin-bottom: 18px;
  45. box-shadow: var(--shadow);
  46. }
  47. .hero h1 {
  48. margin: 0 0 8px;
  49. font-size: 22px;
  50. font-weight: 700;
  51. color: var(--text);
  52. }
  53. .hero h1::before {
  54. content: "";
  55. display: inline-block;
  56. width: 4px;
  57. height: 1.05em;
  58. margin-right: 10px;
  59. border-radius: 2px;
  60. background: var(--primary);
  61. vertical-align: -0.12em;
  62. }
  63. .hero p {
  64. margin: 0;
  65. color: var(--text-muted);
  66. max-width: 780px;
  67. }
  68. .problem-list {
  69. display: flex;
  70. flex-wrap: wrap;
  71. gap: 8px;
  72. margin-top: 14px;
  73. }
  74. .pill {
  75. display: inline-flex;
  76. align-items: center;
  77. gap: 6px;
  78. padding: 4px 10px;
  79. border-radius: 999px;
  80. font-size: 12px;
  81. font-weight: 600;
  82. border: 1px solid transparent;
  83. }
  84. .pill.warn {
  85. background: #fffbeb;
  86. color: #b45309;
  87. border-color: #fde68a;
  88. }
  89. .pill.ok {
  90. background: var(--primary-bg);
  91. color: var(--primary-dark);
  92. border-color: #a7f3d0;
  93. }
  94. .tabs {
  95. display: flex;
  96. flex-wrap: wrap;
  97. gap: 8px;
  98. margin-bottom: 14px;
  99. }
  100. .tab {
  101. appearance: none;
  102. border: 1px solid var(--border);
  103. background: var(--surface);
  104. color: var(--text);
  105. border-radius: 8px;
  106. padding: 8px 14px;
  107. font-size: 13px;
  108. font-weight: 600;
  109. cursor: pointer;
  110. transition: 0.15s ease;
  111. }
  112. .tab:hover { border-color: #a7f3d0; background: var(--primary-bg); }
  113. .tab.active {
  114. background: var(--primary);
  115. border-color: var(--primary);
  116. color: #fff;
  117. }
  118. .tab .badge {
  119. display: inline-block;
  120. margin-left: 6px;
  121. padding: 1px 6px;
  122. border-radius: 999px;
  123. font-size: 11px;
  124. font-weight: 700;
  125. background: rgba(255,255,255,0.22);
  126. }
  127. .tab:not(.active) .badge {
  128. background: #fee2e2;
  129. color: #b91c1c;
  130. }
  131. .tab:not(.active).recommend .badge {
  132. background: var(--primary-bg);
  133. color: var(--primary-dark);
  134. }
  135. .panel {
  136. background: var(--surface);
  137. border: 1px solid var(--border);
  138. border-radius: var(--radius);
  139. box-shadow: var(--shadow);
  140. overflow: hidden;
  141. }
  142. .panel-top {
  143. display: flex;
  144. align-items: flex-start;
  145. justify-content: space-between;
  146. gap: 16px;
  147. padding: 14px 18px;
  148. border-bottom: 1px solid var(--border);
  149. background: var(--table-head);
  150. }
  151. .panel-top h2 {
  152. margin: 0 0 4px;
  153. font-size: 16px;
  154. font-weight: 700;
  155. }
  156. .panel-top .desc {
  157. margin: 0;
  158. color: var(--text-muted);
  159. font-size: 13px;
  160. max-width: 720px;
  161. }
  162. .meta {
  163. flex-shrink: 0;
  164. font-size: 12px;
  165. color: var(--text-muted);
  166. text-align: right;
  167. }
  168. .meta strong { color: var(--primary-dark); }
  169. .callout {
  170. margin: 0;
  171. padding: 10px 18px;
  172. font-size: 13px;
  173. border-bottom: 1px solid var(--border);
  174. }
  175. .callout.bad { background: #fef2f2; color: #991b1b; }
  176. .callout.good { background: var(--primary-bg); color: #065f46; }
  177. .callout code {
  178. font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  179. font-size: 12px;
  180. background: rgba(0,0,0,0.05);
  181. padding: 1px 5px;
  182. border-radius: 4px;
  183. }
  184. .toolbar {
  185. display: flex;
  186. align-items: center;
  187. gap: 8px;
  188. flex-wrap: wrap;
  189. padding: 8px 16px;
  190. background: var(--table-head);
  191. border-bottom: 1px solid var(--border);
  192. }
  193. .sel {
  194. font-size: 12px;
  195. font-weight: 600;
  196. color: var(--text-muted);
  197. padding-right: 10px;
  198. margin-right: 4px;
  199. border-right: 1px solid var(--border);
  200. min-height: 36px;
  201. display: inline-flex;
  202. align-items: center;
  203. }
  204. .sel b { color: var(--primary-dark); margin: 0 2px; }
  205. .btn {
  206. height: 36px;
  207. padding: 0 14px;
  208. border-radius: 8px;
  209. border: 1px solid #d9e8e2;
  210. background: #fff;
  211. color: var(--text);
  212. font-size: 13px;
  213. font-weight: 600;
  214. cursor: default;
  215. }
  216. .btn.main {
  217. background: var(--primary);
  218. border-color: var(--primary);
  219. color: #fff;
  220. }
  221. .viewport {
  222. /* 模拟管理端内容区可视高度 */
  223. height: min(420px, 58vh);
  224. overflow: auto;
  225. background: #fff;
  226. position: relative;
  227. }
  228. .viewport-label {
  229. position: sticky;
  230. top: 0;
  231. z-index: 20;
  232. pointer-events: none;
  233. display: flex;
  234. justify-content: flex-end;
  235. padding: 6px 10px 0 0;
  236. height: 0;
  237. }
  238. .viewport-label span {
  239. pointer-events: none;
  240. font-size: 11px;
  241. font-weight: 700;
  242. color: #0369a1;
  243. background: #e0f2fe;
  244. border: 1px solid #7dd3fc;
  245. border-radius: 999px;
  246. padding: 2px 8px;
  247. }
  248. table.data {
  249. width: max-content;
  250. min-width: 1400px;
  251. border-collapse: collapse;
  252. font-size: 13px;
  253. }
  254. table.data th,
  255. table.data td {
  256. padding: 10px 14px;
  257. border-bottom: 1px solid var(--border);
  258. text-align: center;
  259. white-space: nowrap;
  260. }
  261. table.data th {
  262. font-size: 12px;
  263. font-weight: 600;
  264. color: var(--text-muted);
  265. background: var(--table-head);
  266. }
  267. table.data tbody tr:nth-child(even) td { background: var(--row-alt); }
  268. table.data tbody tr:hover td { background: var(--row-hover); }
  269. table.data .left { text-align: left; }
  270. table.data .op { min-width: 180px; }
  271. .link { color: var(--primary-dark); font-weight: 600; text-decoration: none; }
  272. .tag {
  273. display: inline-block;
  274. padding: 2px 8px;
  275. border-radius: 6px;
  276. font-size: 12px;
  277. font-weight: 600;
  278. color: #fff;
  279. background: var(--primary-dark);
  280. }
  281. .tag.warn { background: var(--warning); }
  282. .pager {
  283. display: flex;
  284. align-items: center;
  285. justify-content: space-between;
  286. padding: 10px 16px;
  287. border-top: 1px solid var(--border);
  288. color: var(--text-muted);
  289. font-size: 13px;
  290. background: #fff;
  291. }
  292. /* —— 方案 A:表体内嵌滚动(推荐) —— */
  293. .scheme-a .table-frame {
  294. height: 320px;
  295. overflow: auto;
  296. border-top: 1px solid var(--border);
  297. }
  298. .scheme-a thead th {
  299. position: sticky;
  300. top: 0;
  301. z-index: 3;
  302. box-shadow: 0 1px 0 rgba(0,0,0,0.06);
  303. }
  304. .scheme-a .th-check,
  305. .scheme-a .td-check {
  306. position: sticky;
  307. left: 0;
  308. z-index: 2;
  309. background: #fff;
  310. box-shadow: 6px 0 10px -6px rgba(0,0,0,0.12);
  311. }
  312. .scheme-a thead .th-check {
  313. z-index: 6;
  314. background: var(--table-head);
  315. }
  316. .scheme-a tbody tr:nth-child(even) .td-check { background: var(--row-alt); }
  317. .scheme-a tbody tr:hover .td-check { background: var(--row-hover); }
  318. .scheme-a .th-op,
  319. .scheme-a .td-op {
  320. position: sticky;
  321. right: 0;
  322. z-index: 2;
  323. background: #fff;
  324. box-shadow: -6px 0 10px -6px rgba(0,0,0,0.12);
  325. }
  326. .scheme-a thead .th-op {
  327. z-index: 5;
  328. background: var(--table-head);
  329. }
  330. .scheme-a tbody tr:nth-child(even) .td-op { background: var(--row-alt); }
  331. .scheme-a tbody tr:hover .td-op { background: var(--row-hover); }
  332. /* —— 方案 B:页面滚动 + 表头 sticky + 底部悬浮横滚条 —— */
  333. .scheme-b .page-scroll {
  334. height: 320px;
  335. overflow: auto;
  336. border-top: 1px solid var(--border);
  337. position: relative;
  338. }
  339. .scheme-b thead th {
  340. position: sticky;
  341. top: 0;
  342. z-index: 3;
  343. box-shadow: 0 1px 0 rgba(0,0,0,0.06);
  344. }
  345. .scheme-b .float-hbar-wrap {
  346. position: sticky;
  347. bottom: 0;
  348. z-index: 8;
  349. height: 14px;
  350. background: rgba(248, 250, 252, 0.92);
  351. border-top: 1px solid var(--border);
  352. backdrop-filter: blur(2px);
  353. }
  354. .scheme-b .float-hbar {
  355. overflow-x: auto;
  356. overflow-y: hidden;
  357. height: 14px;
  358. }
  359. .scheme-b .float-hbar-inner {
  360. height: 1px;
  361. }
  362. /* —— 方案 C:顶部镜像横滚条 + 表体内嵌滚动 —— */
  363. .scheme-c .top-hbar {
  364. overflow-x: auto;
  365. overflow-y: hidden;
  366. height: 14px;
  367. border-top: 1px solid var(--border);
  368. background: var(--table-head);
  369. }
  370. .scheme-c .top-hbar-inner { height: 1px; }
  371. .scheme-c .table-frame {
  372. height: 306px;
  373. overflow: auto;
  374. }
  375. .scheme-c thead th {
  376. position: sticky;
  377. top: 0;
  378. z-index: 3;
  379. box-shadow: 0 1px 0 rgba(0,0,0,0.06);
  380. }
  381. .hint-row {
  382. display: grid;
  383. grid-template-columns: repeat(3, 1fr);
  384. gap: 10px;
  385. margin-top: 14px;
  386. }
  387. @media (max-width: 900px) {
  388. .hint-row { grid-template-columns: 1fr; }
  389. }
  390. .hint-card {
  391. background: #fff;
  392. border: 1px solid var(--border);
  393. border-radius: 10px;
  394. padding: 12px 14px;
  395. box-shadow: var(--shadow);
  396. }
  397. .hint-card h3 {
  398. margin: 0 0 6px;
  399. font-size: 13px;
  400. font-weight: 700;
  401. }
  402. .hint-card p {
  403. margin: 0;
  404. font-size: 12px;
  405. color: var(--text-muted);
  406. }
  407. .hidden { display: none !important; }
  408. .choose {
  409. margin-top: 18px;
  410. padding: 16px 18px;
  411. background: #fff;
  412. border: 1px solid var(--border);
  413. border-radius: 12px;
  414. }
  415. .choose h3 { margin: 0 0 10px; font-size: 15px; }
  416. .choose-options {
  417. display: grid;
  418. grid-template-columns: repeat(3, 1fr);
  419. gap: 10px;
  420. }
  421. @media (max-width: 900px) {
  422. .choose-options { grid-template-columns: 1fr; }
  423. }
  424. .choose-opt {
  425. border: 1px solid var(--border);
  426. border-radius: 10px;
  427. padding: 12px;
  428. cursor: pointer;
  429. transition: 0.15s ease;
  430. background: #fff;
  431. }
  432. .choose-opt:hover { border-color: #a7f3d0; background: var(--primary-bg); }
  433. .choose-opt.selected {
  434. border-color: var(--primary);
  435. box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.25);
  436. background: var(--primary-bg);
  437. }
  438. .choose-opt .letter {
  439. display: inline-flex;
  440. width: 22px;
  441. height: 22px;
  442. align-items: center;
  443. justify-content: center;
  444. border-radius: 6px;
  445. background: var(--primary);
  446. color: #fff;
  447. font-size: 12px;
  448. font-weight: 700;
  449. margin-right: 6px;
  450. }
  451. .choose-opt strong { font-size: 13px; }
  452. .choose-opt p {
  453. margin: 8px 0 0;
  454. font-size: 12px;
  455. color: var(--text-muted);
  456. }
  457. </style>
  458. </head>
  459. <body>
  460. <div class="page">
  461. <div class="hero">
  462. <h1>列表横向 / 纵向滚动体验 Demo</h1>
  463. <p>
  464. 对齐 web-admin Fresh Mint。用下方 Tab 切换「现状问题」与三种改法,
  465. 请在灰色模拟视口内<strong>纵向滚动到中部</strong>、再试<strong>横向滚动</strong>,感受差异。
  466. </p>
  467. <div class="problem-list">
  468. <span class="pill warn">问题 1:横滚条在表最底部,行多时够不着</span>
  469. <span class="pill warn">问题 2:纵滚到中部后看不到表头</span>
  470. <span class="pill ok">目标:横滚条常在可视区底部 · 表头始终可见</span>
  471. </div>
  472. </div>
  473. <div class="tabs" role="tablist">
  474. <button class="tab active" data-tab="now" type="button">现状(问题复现)<span class="badge">Bug</span></button>
  475. <button class="tab recommend" data-tab="a" type="button">方案 A · 表体内嵌滚动<span class="badge">推荐</span></button>
  476. <button class="tab" data-tab="b" type="button">方案 B · 表头 sticky + 悬浮横条</button>
  477. <button class="tab" data-tab="c" type="button">方案 C · 顶部镜像横条</button>
  478. </div>
  479. <!-- 现状 -->
  480. <section class="panel scheme-now" data-panel="now">
  481. <div class="panel-top">
  482. <div>
  483. <h2>现状:整页拉长,表跟着页面滚</h2>
  484. <p class="desc">与多数业务列表一致:表格无固定高度,横向滚动发生在整张表底部。</p>
  485. </div>
  486. <div class="meta">模拟视口<br /><strong>请先向下滚半屏</strong></div>
  487. </div>
  488. <p class="callout bad">
  489. 复现:① 纵向滚到行 20+ → 表头消失;② 想横看「操作」列 → 必须滚到最底才能碰到横滚条。
  490. </p>
  491. <div class="toolbar">
  492. <span class="sel">已选 <b>0</b> 项</span>
  493. <button class="btn main" type="button">新增</button>
  494. <button class="btn" type="button">批量导出</button>
  495. </div>
  496. <div class="viewport" id="vp-now">
  497. <div class="viewport-label"><span>内容区可视范围(向下滚)</span></div>
  498. <div class="table-scroll-full" style="overflow-x:auto;">
  499. <table class="data" id="table-now"></table>
  500. </div>
  501. <div class="pager"><span>共 60 条</span><span>1 / 3 页(示意)</span></div>
  502. </div>
  503. </section>
  504. <!-- 方案 A -->
  505. <section class="panel scheme-a hidden" data-panel="a">
  506. <div class="panel-top">
  507. <div>
  508. <h2>方案 A:表格视口填满高度(内嵌滚动)</h2>
  509. <p class="desc">
  510. 对齐规范:动态 <code>height</code> 使表体尽量撑满,
  511. <strong>分页始终可见且下方留 10px</strong>;
  512. 纵滚在表体内,表头 sticky,横滚条贴表体可视区底。
  513. </p>
  514. </div>
  515. <div class="meta">实现成本低<br /><strong>与 EP 官方行为一致</strong></div>
  516. </div>
  517. <p class="callout good">
  518. 一次解决两个问题:表头始终在表体顶部;横滚条始终在表体可视区底部(不必滚到第 60 行);分页下约 10px。
  519. </p>
  520. <div class="toolbar">
  521. <span class="sel">已选 <b>0</b> 项</span>
  522. <button class="btn main" type="button">新增</button>
  523. <button class="btn" type="button">批量导出</button>
  524. </div>
  525. <div class="viewport" style="height:auto; overflow:visible;">
  526. <div class="table-frame" id="vp-a">
  527. <table class="data" id="table-a"></table>
  528. </div>
  529. <div class="pager"><span>共 60 条 · 分页固定在表外</span><span>1 / 3 页</span></div>
  530. </div>
  531. </section>
  532. <!-- 方案 B -->
  533. <section class="panel scheme-b hidden" data-panel="b">
  534. <div class="panel-top">
  535. <div>
  536. <h2>方案 B:保留整页滚动 + sticky 表头 + 底部悬浮横滚条</h2>
  537. <p class="desc">
  538. 表仍可随页面变高;用 sticky 钉住表头;在滚动容器底部再放一条「镜像」横滚条,始终可见。
  539. </p>
  540. </div>
  541. <div class="meta">交互灵活<br /><strong>实现与同步成本更高</strong></div>
  542. </div>
  543. <p class="callout good">
  544. 适合「不想裁切表体高度、希望一屏看更多行」的场景;需维护真实横滚与悬浮条 scrollLeft 双向同步。
  545. </p>
  546. <div class="toolbar">
  547. <span class="sel">已选 <b>0</b> 项</span>
  548. <button class="btn main" type="button">新增</button>
  549. <button class="btn" type="button">批量导出</button>
  550. </div>
  551. <div class="viewport" style="height:auto; overflow:visible;">
  552. <div class="page-scroll" id="vp-b">
  553. <div class="table-x" id="b-table-x" style="overflow-x:auto;">
  554. <table class="data" id="table-b"></table>
  555. </div>
  556. <div class="float-hbar-wrap">
  557. <div class="float-hbar" id="b-float-hbar">
  558. <div class="float-hbar-inner" id="b-float-inner"></div>
  559. </div>
  560. </div>
  561. </div>
  562. <div class="pager"><span>共 60 条</span><span>试拖底部细条或表内横滚</span></div>
  563. </div>
  564. </section>
  565. <!-- 方案 C -->
  566. <section class="panel scheme-c hidden" data-panel="c">
  567. <div class="panel-top">
  568. <div>
  569. <h2>方案 C:表体内嵌滚动 + 顶部镜像横滚条</h2>
  570. <p class="desc">
  571. 在方案 A 基础上,表头上方再放一条横滚条:人眼在看标题附近时也能横移,无需移到表底。
  572. </p>
  573. </div>
  574. <div class="meta">A 的增强<br /><strong>双条同步,略占高度</strong></div>
  575. </div>
  576. <p class="callout good">
  577. 横滚可达性最好(顶/底都能拖)。若采用 A,可按「列特别多」的页面再叠加顶部条。
  578. </p>
  579. <div class="toolbar">
  580. <span class="sel">已选 <b>0</b> 项</span>
  581. <button class="btn main" type="button">新增</button>
  582. <button class="btn" type="button">批量导出</button>
  583. </div>
  584. <div class="viewport" style="height:auto; overflow:visible;">
  585. <div class="top-hbar" id="c-top-hbar">
  586. <div class="top-hbar-inner" id="c-top-inner"></div>
  587. </div>
  588. <div class="table-frame" id="vp-c">
  589. <table class="data" id="table-c"></table>
  590. </div>
  591. <div class="pager"><span>共 60 条</span><span>顶条与表体横滚已同步</span></div>
  592. </div>
  593. </section>
  594. <div class="hint-row">
  595. <div class="hint-card">
  596. <h3>与现有代码的关系</h3>
  597. <p>
  598. 规范 §7.6 已写 <code>max-height + overflow</code>;HTML demo 也有 sticky 表头。
  599. 业务页多数未给 <code>el-table</code> 设高度,bx 的 <code>.table-scroll</code> 仅 <code>overflow-x</code>,故问题仍在。
  600. </p>
  601. </div>
  602. <div class="hint-card">
  603. <h3>推荐落地路径</h3>
  604. <p>
  605. 优先方案 A:公共列表用 <code>useListTableViewportHeight</code> 填满视口(分页下 10px),分页留在表外。
  606. 列极多的页可再加方案 C 顶条。
  607. </p>
  608. </div>
  609. <div class="hint-card">
  610. <h3>注意点</h3>
  611. <p>
  612. 固定高度后筛选区展开会挤表体,需用「视口剩余高度」动态计算;
  613. 展开行 / 树表要回归测 <code>doLayout</code>。
  614. </p>
  615. </div>
  616. </div>
  617. <div class="choose">
  618. <h3>看完后请选择倾向方案(可点选,并在对话里告诉我)</h3>
  619. <div class="choose-options">
  620. <div class="choose-opt" data-choice="a" tabindex="0">
  621. <span class="letter">A</span><strong>表体内嵌滚动</strong>
  622. <p>成本最低,一次修两个问题;与 Element Plus / 规范最贴合。</p>
  623. </div>
  624. <div class="choose-opt" data-choice="b" tabindex="0">
  625. <span class="letter">B</span><strong>sticky + 悬浮横条</strong>
  626. <p>保留「整表随页面变高」;同步逻辑与兼容成本更高。</p>
  627. </div>
  628. <div class="choose-opt" data-choice="c" tabindex="0">
  629. <span class="letter">C</span><strong>A + 顶部镜像条</strong>
  630. <p>在 A 上增强横滚可达性;适合列特别宽的列表。</p>
  631. </div>
  632. </div>
  633. </div>
  634. </div>
  635. <script>
  636. const COLS = [
  637. { key: "check", label: "", cls: "th-check" },
  638. { key: "idx", label: "序号" },
  639. { key: "code", label: "单据编号", left: true },
  640. { key: "name", label: "名称", left: true },
  641. { key: "customer", label: "客户" },
  642. { key: "region", label: "区域路径" },
  643. { key: "amount", label: "金额" },
  644. { key: "status", label: "状态" },
  645. { key: "owner", label: "负责人" },
  646. { key: "dept", label: "所属部门" },
  647. { key: "created", label: "创建时间" },
  648. { key: "updated", label: "更新时间" },
  649. { key: "remark", label: "备注", left: true },
  650. { key: "op", label: "操作", cls: "th-op op" }
  651. ];
  652. function buildTable(tableEl, rows) {
  653. const thead = document.createElement("thead");
  654. const hr = document.createElement("tr");
  655. COLS.forEach((c) => {
  656. const th = document.createElement("th");
  657. th.textContent = c.label;
  658. if (c.cls) th.className = c.cls;
  659. if (c.left) th.classList.add("left");
  660. hr.appendChild(th);
  661. });
  662. thead.appendChild(hr);
  663. const tbody = document.createElement("tbody");
  664. for (let i = 1; i <= rows; i++) {
  665. const tr = document.createElement("tr");
  666. const cells = [
  667. { html: '<input type="checkbox" />', cls: "td-check" },
  668. { text: String(i) },
  669. { html: `<a class="link" href="#">SO-2026-${String(i).padStart(4, "0")}</a>`, left: true },
  670. { text: `示例业务名称 ${i}`, left: true },
  671. { text: i % 3 === 0 ? "华东化工" : i % 3 === 1 ? "山峡贸易" : "三墙供应链" },
  672. { text: "华东 / 上海 / 浦东" },
  673. { text: (1200 + i * 37).toLocaleString("zh-CN") },
  674. { html: i % 5 === 0 ? '<span class="tag warn">待处理</span>' : '<span class="tag">进行中</span>' },
  675. { text: i % 2 ? "张三" : "李四" },
  676. { text: "销售一部" },
  677. { text: "2026-08-01 09:12" },
  678. { text: "2026-09-01 18:00" },
  679. { text: i % 4 === 0 ? "需要横向滚动才能看到本列与操作列" : "—", left: true },
  680. { html: '<a class="link" href="#">查看</a> · 编辑', cls: "td-op op" }
  681. ];
  682. cells.forEach((cell) => {
  683. const td = document.createElement("td");
  684. if (cell.html) td.innerHTML = cell.html;
  685. else td.textContent = cell.text;
  686. if (cell.cls) td.className = cell.cls;
  687. if (cell.left) td.classList.add("left");
  688. tr.appendChild(td);
  689. });
  690. tbody.appendChild(tr);
  691. }
  692. tableEl.innerHTML = "";
  693. tableEl.appendChild(thead);
  694. tableEl.appendChild(tbody);
  695. }
  696. ["table-now", "table-a", "table-b", "table-c"].forEach((id) => {
  697. buildTable(document.getElementById(id), 60);
  698. });
  699. // Tabs
  700. const tabs = document.querySelectorAll(".tab");
  701. const panels = document.querySelectorAll("[data-panel]");
  702. tabs.forEach((tab) => {
  703. tab.addEventListener("click", () => {
  704. tabs.forEach((t) => t.classList.remove("active"));
  705. tab.classList.add("active");
  706. const key = tab.dataset.tab;
  707. panels.forEach((p) => {
  708. p.classList.toggle("hidden", p.dataset.panel !== key);
  709. });
  710. if (key === "b") syncB();
  711. if (key === "c") syncC();
  712. });
  713. });
  714. // Choice
  715. document.querySelectorAll(".choose-opt").forEach((el) => {
  716. el.addEventListener("click", () => {
  717. document.querySelectorAll(".choose-opt").forEach((x) => x.classList.remove("selected"));
  718. el.classList.add("selected");
  719. });
  720. });
  721. // Scheme B sync
  722. function syncB() {
  723. const x = document.getElementById("b-table-x");
  724. const bar = document.getElementById("b-float-hbar");
  725. const inner = document.getElementById("b-float-inner");
  726. const table = document.getElementById("table-b");
  727. inner.style.width = table.scrollWidth + "px";
  728. let lock = false;
  729. const bind = (from, to) => {
  730. from.onscroll = () => {
  731. if (lock) return;
  732. lock = true;
  733. to.scrollLeft = from.scrollLeft;
  734. lock = false;
  735. };
  736. };
  737. bind(x, bar);
  738. bind(bar, x);
  739. }
  740. // Scheme C sync
  741. function syncC() {
  742. const frame = document.getElementById("vp-c");
  743. const top = document.getElementById("c-top-hbar");
  744. const inner = document.getElementById("c-top-inner");
  745. const table = document.getElementById("table-c");
  746. inner.style.width = table.scrollWidth + "px";
  747. let lock = false;
  748. frame.onscroll = () => {
  749. if (lock) return;
  750. lock = true;
  751. top.scrollLeft = frame.scrollLeft;
  752. lock = false;
  753. };
  754. top.onscroll = () => {
  755. if (lock) return;
  756. lock = true;
  757. frame.scrollLeft = top.scrollLeft;
  758. lock = false;
  759. };
  760. }
  761. // init sync widths after paint
  762. requestAnimationFrame(() => {
  763. syncB();
  764. syncC();
  765. });
  766. window.addEventListener("resize", () => {
  767. syncB();
  768. syncC();
  769. });
  770. </script>
  771. </body>
  772. </html>