03-collapsible.html 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  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>03 折叠摘要行 · 登记未用完 Demo</title>
  7. <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
  8. <link rel="stylesheet" href="_shared.css" />
  9. <style>
  10. .accordion-list {
  11. display: flex;
  12. flex-direction: column;
  13. gap: 8px;
  14. max-height: 520px;
  15. overflow-y: auto;
  16. padding-right: 2px;
  17. }
  18. .acc-item {
  19. border: 1px solid var(--border);
  20. border-radius: var(--radius-sm);
  21. background: var(--surface);
  22. overflow: hidden;
  23. }
  24. .acc-item.is-expanded {
  25. border-color: #a7f3d0;
  26. box-shadow: 0 2px 8px rgba(16, 185, 129, 0.08);
  27. }
  28. .acc-summary {
  29. display: grid;
  30. grid-template-columns: 32px 1fr auto auto auto 72px 32px;
  31. gap: 12px;
  32. align-items: center;
  33. padding: 10px 12px;
  34. cursor: pointer;
  35. user-select: none;
  36. }
  37. .acc-summary:hover {
  38. background: #fafafa;
  39. }
  40. .acc-item.is-expanded .acc-summary {
  41. background: var(--primary-bg);
  42. border-bottom: 1px solid #d1fae5;
  43. }
  44. .acc-chevron {
  45. width: 20px;
  46. height: 20px;
  47. display: flex;
  48. align-items: center;
  49. justify-content: center;
  50. color: var(--text-muted);
  51. transition: transform 0.2s;
  52. font-size: 12px;
  53. }
  54. .acc-item.is-expanded .acc-chevron {
  55. transform: rotate(90deg);
  56. color: var(--primary-dark);
  57. }
  58. .acc-name {
  59. font-weight: 600;
  60. font-size: 13px;
  61. overflow: hidden;
  62. text-overflow: ellipsis;
  63. white-space: nowrap;
  64. }
  65. .acc-meta {
  66. font-size: 12px;
  67. color: var(--text-muted);
  68. }
  69. .acc-inline-qty {
  70. width: 72px;
  71. }
  72. .acc-body {
  73. display: none;
  74. padding: 12px 14px 14px;
  75. background: #fafafa;
  76. }
  77. .acc-item.is-expanded .acc-body {
  78. display: block;
  79. }
  80. .acc-grid {
  81. display: grid;
  82. grid-template-columns: repeat(4, minmax(0, 1fr));
  83. gap: 10px 14px;
  84. }
  85. .acc-col-span-2 {
  86. grid-column: span 2;
  87. }
  88. .acc-col-span-4 {
  89. grid-column: 1 / -1;
  90. }
  91. .bulk-bar {
  92. display: flex;
  93. gap: 8px;
  94. align-items: center;
  95. margin-bottom: 12px;
  96. flex-wrap: wrap;
  97. }
  98. .bulk-bar .input {
  99. width: 140px;
  100. }
  101. @media (max-width: 960px) {
  102. .acc-summary {
  103. grid-template-columns: 28px 1fr auto 72px 28px;
  104. }
  105. .acc-summary .acc-meta:nth-child(4),
  106. .acc-summary .acc-meta:nth-child(5) {
  107. display: none;
  108. }
  109. .acc-grid {
  110. grid-template-columns: repeat(2, 1fr);
  111. }
  112. }
  113. </style>
  114. </head>
  115. <body>
  116. <header class="demo-banner">
  117. <div>
  118. <h1><span class="demo-badge">03 折叠摘要行</span> 登记未用完物料</h1>
  119. <p>默认一行摘要 + 行内改数量 · 点击展开完整字段</p>
  120. </div>
  121. <div class="demo-banner__actions">
  122. <a href="index.html">← 选型索引</a>
  123. <a href="01-inline-table.html">01 表格</a>
  124. <a class="is-current" href="03-collapsible.html">03 折叠</a>
  125. <a href="04-quick-add.html">04 检索</a>
  126. </div>
  127. </header>
  128. <div class="page-wrap">
  129. <div class="dialog-backdrop">
  130. <div class="dialog dialog--wide" role="dialog">
  131. <div class="dialog__head">
  132. <h2 class="dialog__title">登记未用完物料</h2>
  133. <button type="button" class="dialog__close">×</button>
  134. </div>
  135. <div class="dialog__body">
  136. <div class="section-head">
  137. <div class="section-head__left">
  138. <div class="section-title">未用完明细</div>
  139. <div class="section-hint">折叠态可快速改数量;需改批次或手工字段时再展开。支持全部展开/收起。</div>
  140. </div>
  141. <div class="section-tools">
  142. <span class="stat-pill">共 <strong id="line-count">0</strong> 项</span>
  143. <button type="button" class="btn btn-sm" id="expand-all">全部展开</button>
  144. <button type="button" class="btn btn-sm" id="collapse-all">全部收起</button>
  145. <button type="button" class="btn btn-sm">+ 新增一行</button>
  146. </div>
  147. </div>
  148. <div class="bulk-bar">
  149. <span style="font-size: 12px; color: var(--text-muted)">批量填充持有人:</span>
  150. <input type="text" class="input" id="bulk-holder" placeholder="输入姓名" />
  151. <button type="button" class="btn btn-sm" id="apply-holder">应用到全部</button>
  152. <input type="search" class="input" style="width: 200px; margin-left: auto" placeholder="过滤…" id="filter" />
  153. </div>
  154. <div class="accordion-list" id="accordion"></div>
  155. </div>
  156. <div class="dialog__foot">
  157. <button type="button" class="btn btn-ghost">取消</button>
  158. <button type="button" class="btn btn-primary">保存</button>
  159. </div>
  160. </div>
  161. </div>
  162. <p class="style-note">
  163. <strong>适用场景:</strong>多数行只需改数量/持有人,少数行需展开核对批次。
  164. <ul>
  165. <li>比现状卡片省约 70% 纵向空间,10 条折叠态一屏可见</li>
  166. <li>摘要行右侧 inline 数量框,无需展开即可改</li>
  167. <li>展开区 4 列栅格,比现状 5~6 列略疏,控件 32px</li>
  168. </ul>
  169. </p>
  170. </div>
  171. <script src="_demo-data.js"></script>
  172. <script>
  173. let rows = [...DEMO_UNUSED_LINES]
  174. const expanded = new Set([rows[0]?.id])
  175. const accordion = document.getElementById('accordion')
  176. function render() {
  177. const kw = (document.getElementById('filter').value || '').trim().toLowerCase()
  178. const filtered = rows.filter((r) => !kw || materialSummary(r).toLowerCase().includes(kw))
  179. accordion.innerHTML = filtered
  180. .map((row, i) => {
  181. const isExp = expanded.has(row.id)
  182. const amt = formatMoney(calcAmount(row.unitPrice, row.unusedQty))
  183. return `<div class="acc-item ${isExp ? 'is-expanded' : ''}" data-id="${row.id}">
  184. <div class="acc-summary" data-toggle="${row.id}">
  185. <span class="acc-chevron">▸</span>
  186. <span class="row-index">${rows.indexOf(row) + 1}</span>
  187. <span class="acc-name" title="${row.materialName}">${row.materialName}</span>
  188. <span class="acc-meta">${row.batchNo || '—'}</span>
  189. <span class="acc-meta">${row.holder || '—'}</span>
  190. <span class="acc-meta money">${amt} 元</span>
  191. <input class="input acc-inline-qty qty-inline" type="number" min="0.01" step="0.01" value="${row.unusedQty}" data-id="${row.id}" onclick="event.stopPropagation()" />
  192. <button type="button" class="btn btn-danger-link btn-sm del-btn" data-id="${row.id}" onclick="event.stopPropagation()">×</button>
  193. </div>
  194. <div class="acc-body">
  195. <div class="acc-grid">
  196. <div class="acc-col-span-2">
  197. <label class="field-label">批次检索</label>
  198. <select class="select" ${row.fromBatch ? 'disabled' : ''}><option>${row.fromBatch ? row.stockInNo + ' · ' + row.materialName : '检索或手工'}</option></select>
  199. </div>
  200. <div><label class="field-label">入库编号</label><input class="input input--readonly" readonly value="${row.stockInNo || ''}" /></div>
  201. <div><label class="field-label">批号</label><input class="input input--readonly" readonly value="${row.batchNo || ''}" /></div>
  202. <div><label class="field-label">物料编码</label><input class="input input--readonly" readonly value="${row.materialCode || ''}" /></div>
  203. <div><label class="field-label">货号</label><input class="input input--readonly" readonly value="${row.catalogNo || ''}" /></div>
  204. <div><label class="field-label">规格</label><input class="input input--readonly" readonly value="${row.specModel || ''}" /></div>
  205. <div><label class="field-label">单位</label><input class="input input--readonly" readonly value="${row.unit || ''}" /></div>
  206. <div><label class="field-label">厂家</label><input class="input input--readonly" readonly value="${row.manufacturer || ''}" /></div>
  207. <div><label class="field-label">单价</label><input class="input input--readonly" readonly value="${formatMoney(row.unitPrice)}" /></div>
  208. <div><label class="field-label">持有人</label><input class="input holder-field" data-id="${row.id}" value="${row.holder || ''}" /></div>
  209. <div class="acc-col-span-4"><label class="field-label">备注</label><input class="input remark-field" data-id="${row.id}" value="${row.remark || ''}" /></div>
  210. </div>
  211. </div>
  212. </div>`
  213. })
  214. .join('')
  215. document.getElementById('line-count').textContent = rows.length
  216. }
  217. accordion.addEventListener('click', (e) => {
  218. if (e.target.classList.contains('del-btn')) {
  219. const id = Number(e.target.dataset.id)
  220. rows = rows.filter((r) => r.id !== id)
  221. expanded.delete(id)
  222. render()
  223. return
  224. }
  225. const toggle = e.target.closest('[data-toggle]')
  226. if (toggle && !e.target.classList.contains('qty-inline')) {
  227. const id = Number(toggle.dataset.toggle)
  228. if (expanded.has(id)) expanded.delete(id)
  229. else expanded.add(id)
  230. render()
  231. }
  232. })
  233. accordion.addEventListener('input', (e) => {
  234. const id = Number(e.target.dataset.id)
  235. const row = rows.find((r) => r.id === id)
  236. if (!row) return
  237. if (e.target.classList.contains('qty-inline')) row.unusedQty = Number(e.target.value) || 0
  238. if (e.target.classList.contains('holder-field')) row.holder = e.target.value
  239. if (e.target.classList.contains('remark-field')) row.remark = e.target.value
  240. if (e.target.classList.contains('qty-inline')) render()
  241. })
  242. document.getElementById('expand-all').addEventListener('click', () => {
  243. rows.forEach((r) => expanded.add(r.id))
  244. render()
  245. })
  246. document.getElementById('collapse-all').addEventListener('click', () => {
  247. expanded.clear()
  248. render()
  249. })
  250. document.getElementById('apply-holder').addEventListener('click', () => {
  251. const v = document.getElementById('bulk-holder').value.trim()
  252. if (!v) return
  253. rows.forEach((r) => (r.holder = v))
  254. render()
  255. })
  256. document.getElementById('filter').addEventListener('input', render)
  257. render()
  258. </script>
  259. </body>
  260. </html>