independent-pills.html 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  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.0" />
  6. <title>质检工单 · 独立胶囊 Tab Demo</title>
  7. <link
  8. href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
  9. rel="stylesheet"
  10. />
  11. <link rel="stylesheet" href="_shared.css" />
  12. <style>
  13. /* ── 共享:胶囊按钮内容(对齐现网 QcWorkOrderSampleGroup) ── */
  14. .pill-btn {
  15. display: inline-flex;
  16. align-items: center;
  17. gap: 6px;
  18. min-height: 34px;
  19. padding: 8px 16px;
  20. border-radius: 10px;
  21. font-size: 12px;
  22. font-family: inherit;
  23. color: #1e293b;
  24. cursor: pointer;
  25. white-space: nowrap;
  26. transition:
  27. background 0.15s,
  28. color 0.15s,
  29. border-color 0.15s,
  30. box-shadow 0.15s;
  31. }
  32. .pill-btn:focus-visible {
  33. outline: 2px solid #10b981;
  34. outline-offset: 2px;
  35. }
  36. .pill-index {
  37. flex-shrink: 0;
  38. width: 18px;
  39. height: 18px;
  40. border-radius: 50%;
  41. display: inline-flex;
  42. align-items: center;
  43. justify-content: center;
  44. font-size: 11px;
  45. font-weight: 700;
  46. background: #e2e8f0;
  47. }
  48. .pill-source {
  49. display: inline-flex;
  50. align-items: center;
  51. gap: 4px;
  52. padding: 1px 6px;
  53. border-radius: 4px;
  54. font-size: 11px;
  55. font-weight: 600;
  56. background: #f1f5f9;
  57. }
  58. .pill-source--reception {
  59. color: var(--reception);
  60. background: var(--reception-bg);
  61. }
  62. .pill-source--production {
  63. color: var(--production);
  64. background: var(--production-bg);
  65. }
  66. .pill-source__dot {
  67. width: 6px;
  68. height: 6px;
  69. border-radius: 50%;
  70. background: currentColor;
  71. }
  72. .pill-no {
  73. font-family: ui-monospace, Menlo, monospace;
  74. }
  75. .pill-btn.is-active {
  76. color: #fff;
  77. background: var(--primary);
  78. border-color: var(--primary);
  79. box-shadow: 0 2px 6px rgba(16, 185, 129, 0.28);
  80. }
  81. .pill-btn.is-active .pill-index {
  82. color: #fff;
  83. background: rgb(255 255 255 / 28%);
  84. }
  85. .pill-btn.is-active .pill-source,
  86. .pill-btn.is-active .pill-source--reception,
  87. .pill-btn.is-active .pill-source--production {
  88. color: #fff;
  89. background: rgb(255 255 255 / 22%);
  90. }
  91. .pill-btn.is-active .tag {
  92. background: rgb(255 255 255 / 22%);
  93. color: #fff;
  94. border-color: rgb(255 255 255 / 35%);
  95. }
  96. .pill-btn:not(.is-active) .tag {
  97. background: #f1f5f9;
  98. color: #1e293b;
  99. border-color: #e2e8f0;
  100. }
  101. /* ── A · 现网:轨道内连体 ── */
  102. .grouped-zone {
  103. padding: 8px 16px;
  104. border-bottom: 1px solid var(--border);
  105. background: #f8fafc;
  106. }
  107. .grouped-track {
  108. display: inline-flex;
  109. flex-wrap: wrap;
  110. gap: 10px;
  111. padding: 6px;
  112. background: linear-gradient(180deg, var(--primary-bg) 0%, #fff 100%);
  113. border: 1px solid #d1fae5;
  114. border-radius: 10px;
  115. }
  116. .grouped-track .pill-btn {
  117. border: none;
  118. border-radius: 8px;
  119. background: transparent;
  120. }
  121. .grouped-track .pill-btn:hover:not(.is-active) {
  122. background: #f1f5f9;
  123. }
  124. /* ── B/C/D · 独立胶囊 ── */
  125. .indep-zone {
  126. padding: 12px 16px;
  127. border-bottom: 1px solid var(--border);
  128. background: #f8fafc;
  129. }
  130. .indep-zone__hint {
  131. margin-bottom: 10px;
  132. font-size: 12px;
  133. color: var(--text-muted);
  134. }
  135. .indep-row {
  136. display: flex;
  137. flex-wrap: wrap;
  138. align-items: center;
  139. }
  140. .indep-row--gap-md {
  141. gap: 16px;
  142. }
  143. .indep-row--gap-lg {
  144. gap: 24px;
  145. }
  146. .indep-row--gap-xl {
  147. gap: 32px;
  148. }
  149. /* 每个胶囊独立边框 + 白底 */
  150. .indep-row .pill-btn {
  151. border: 2px solid #e2e8f0;
  152. background: #fff;
  153. box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
  154. }
  155. .indep-row .pill-btn:hover:not(.is-active) {
  156. border-color: #cbd5e1;
  157. box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
  158. }
  159. /* 未选中时按来源保留边框色 */
  160. .indep-row .pill-btn[data-source='reception']:not(.is-active) {
  161. border-color: var(--reception-border);
  162. }
  163. .indep-row .pill-btn[data-source='production']:not(.is-active) {
  164. border-color: var(--production-border);
  165. }
  166. .indep-row .pill-btn[data-source='reception']:not(.is-active):hover {
  167. border-color: var(--reception);
  168. background: var(--reception-bg);
  169. }
  170. .indep-row .pill-btn[data-source='production']:not(.is-active):hover {
  171. border-color: var(--production);
  172. background: var(--production-bg);
  173. }
  174. /* D · 独立 + 更大胶囊 */
  175. .indep-row--large .pill-btn {
  176. min-height: 40px;
  177. padding: 10px 18px;
  178. font-size: 13px;
  179. border-radius: 12px;
  180. }
  181. .indep-row--large .pill-source {
  182. font-size: 12px;
  183. padding: 2px 8px;
  184. }
  185. .gap-label {
  186. display: inline-flex;
  187. align-items: center;
  188. height: 22px;
  189. padding: 0 8px;
  190. margin-left: 8px;
  191. border-radius: 6px;
  192. font-size: 11px;
  193. font-weight: 600;
  194. background: #f1f5f9;
  195. color: var(--text-muted);
  196. }
  197. </style>
  198. </head>
  199. <body>
  200. <header class="demo-banner">
  201. <div>
  202. <h1>独立胶囊 Tab · 对比 Demo</h1>
  203. <p>
  204. 在现网胶囊内容结构不变的前提下,将每个 Tab 从<strong>轨道连体</strong>改为<strong>独立胶囊</strong>,并加大间距。
  205. 下方 A 为现网对照,B/C/D 为独立方案(间距 16 / 24 / 32px,均可点击切换)。
  206. </p>
  207. </div>
  208. <div class="demo-banner__actions">
  209. <a class="demo-link" href="index.html">← 索引</a>
  210. <a class="demo-link" href="compare-all-v2.html">第二组方案</a>
  211. </div>
  212. </header>
  213. <div class="page-container">
  214. <div class="compare-stack">
  215. <section class="compare-block">
  216. <div class="compare-block__label">
  217. <h2>
  218. <span class="variant-tag variant-tag--baseline">A</span>
  219. 现网 · 轨道内连体(对照)
  220. </h2>
  221. <p>多个 Tab 包在同一 <code>panel-pill-tabs__track</code> 绿底容器内,胶囊之间 gap 10px,视觉上像一组。</p>
  222. </div>
  223. <div class="compare-block__body" id="block-a"></div>
  224. </section>
  225. <section class="compare-block">
  226. <div class="compare-block__label">
  227. <h2>
  228. <span class="variant-tag variant-tag--rec">推荐</span>
  229. <span class="variant-tag">B</span>
  230. 独立胶囊 · 间距 16px
  231. </h2>
  232. <p>去掉外层轨道,每个 Tab 自带白底 + 2px 边框 + 轻阴影;未选中按来源保留绿/琥珀边框色。</p>
  233. </div>
  234. <div class="compare-block__body" id="block-b"></div>
  235. </section>
  236. <section class="compare-block">
  237. <div class="compare-block__label">
  238. <h2><span class="variant-tag">C</span> 独立胶囊 · 间距 24px</h2>
  239. <p>同 B,胶囊间距加大到 24px,分离感更强。</p>
  240. </div>
  241. <div class="compare-block__body" id="block-c"></div>
  242. </section>
  243. <section class="compare-block">
  244. <div class="compare-block__label">
  245. <h2><span class="variant-tag">D</span> 独立胶囊 · 间距 32px + 略放大</h2>
  246. <p>间距 32px,胶囊 min-height 40px、圆角 12px,点击区域更大。</p>
  247. </div>
  248. <div class="compare-block__body" id="block-d"></div>
  249. </section>
  250. </div>
  251. </div>
  252. <script>
  253. const MOCK = {
  254. sampleCode: 'YB-2025-0618-0042',
  255. subtitle: '张三 · 间充质干细胞 · 5×10⁶/支',
  256. workOrders: [
  257. {
  258. id: 'wo-reception',
  259. source: 'reception',
  260. label: '样本接收',
  261. no: 'QC-IN-20250618-0012',
  262. statusLabel: '已完成',
  263. statusClass: 'tag--success'
  264. },
  265. {
  266. id: 'wo-production',
  267. source: 'production',
  268. label: '生产过程',
  269. no: 'QC-IN-20250620-0038',
  270. statusLabel: '已配置',
  271. statusClass: 'tag--warning'
  272. }
  273. ]
  274. }
  275. function ctxHeader() {
  276. return `
  277. <div class="qc-sample-row__ctx">
  278. <span class="qc-sample-row__idx">1</span>
  279. <span class="qc-sample-row__code">${MOCK.sampleCode}</span>
  280. <span class="qc-sample-row__sub">${MOCK.subtitle}</span>
  281. <span class="qc-sample-row__badge">${MOCK.workOrders.length} 条质检</span>
  282. </div>`
  283. }
  284. function pillBtn(wo, i, activeId) {
  285. const active = wo.id === activeId
  286. return `
  287. <button type="button" class="pill-btn tab-btn ${active ? 'is-active' : ''}"
  288. data-id="${wo.id}" data-source="${wo.source}" role="tab" aria-selected="${active}">
  289. <span class="pill-index">${i + 1}</span>
  290. <span class="pill-source pill-source--${wo.source}">
  291. <span class="pill-source__dot"></span>${wo.label}
  292. </span>
  293. <span class="pill-no">${wo.no}</span>
  294. <span class="tag ${wo.statusClass}">${wo.statusLabel}</span>
  295. </button>`
  296. }
  297. function cardPlaceholder(wo) {
  298. return `
  299. <div class="qc-card-placeholder">
  300. <div class="qc-card-placeholder__head">
  301. <span class="qc-card-placeholder__no">${wo.no}</span>
  302. <span class="tag ${wo.statusClass}">${wo.statusLabel}</span>
  303. <span class="tag tag--${wo.source}">${wo.label}</span>
  304. </div>
  305. <div class="qc-card-placeholder__tasks">▼ 质检任务列表(${wo.label})</div>
  306. </div>`
  307. }
  308. function wrap(tabsHtml, activeId) {
  309. const active = MOCK.workOrders.find((w) => w.id === activeId)
  310. return `
  311. <article class="qc-sample-row">
  312. ${ctxHeader()}
  313. ${tabsHtml}
  314. ${cardPlaceholder(active)}
  315. </article>`
  316. }
  317. function renderGrouped(activeId) {
  318. const pills = MOCK.workOrders.map((wo, i) => pillBtn(wo, i, activeId)).join('')
  319. return wrap(
  320. `<div class="grouped-zone">
  321. <div class="grouped-track" role="tablist" aria-label="同样本质检工单">${pills}</div>
  322. </div>`,
  323. activeId
  324. )
  325. }
  326. function renderIndependent(activeId, gapClass, extraRowClass, hint) {
  327. const pills = MOCK.workOrders.map((wo, i) => pillBtn(wo, i, activeId)).join('')
  328. const gapLabel = gapClass.replace('indep-row--gap-', '') + 'px'
  329. return wrap(
  330. `<div class="indep-zone">
  331. ${hint ? `<div class="indep-zone__hint">${hint}</div>` : ''}
  332. <div class="indep-row ${gapClass} ${extraRowClass || ''}" role="tablist" aria-label="同样本质检工单">
  333. ${pills}
  334. </div>
  335. </div>`,
  336. activeId
  337. )
  338. }
  339. const BLOCKS = {
  340. a: { render: (id) => renderGrouped(id), default: 'wo-reception' },
  341. b: {
  342. render: (id) =>
  343. renderIndependent(id, 'indep-row--gap-md', '', '每个胶囊独立展示,间距 16px'),
  344. default: 'wo-reception'
  345. },
  346. c: {
  347. render: (id) =>
  348. renderIndependent(id, 'indep-row--gap-lg', '', '每个胶囊独立展示,间距 24px'),
  349. default: 'wo-reception'
  350. },
  351. d: {
  352. render: (id) =>
  353. renderIndependent(
  354. id,
  355. 'indep-row--gap-xl',
  356. 'indep-row--large',
  357. '每个胶囊独立展示,间距 32px,尺寸略放大'
  358. ),
  359. default: 'wo-reception'
  360. }
  361. }
  362. const state = Object.fromEntries(Object.entries(BLOCKS).map(([k, v]) => [k, v.default]))
  363. function mount(key) {
  364. const el = document.getElementById(`block-${key}`)
  365. el.innerHTML = BLOCKS[key].render(state[key])
  366. el.querySelectorAll('[data-id]').forEach((btn) => {
  367. btn.addEventListener('click', () => {
  368. state[key] = btn.dataset.id
  369. mount(key)
  370. })
  371. })
  372. }
  373. Object.keys(BLOCKS).forEach(mount)
  374. </script>
  375. </body>
  376. </html>