04-quick-add.html 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  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>04 检索优先 · 登记未用完 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. .add-zone {
  11. padding: 14px 16px;
  12. background: linear-gradient(135deg, var(--primary-bg) 0%, #f0fdf4 100%);
  13. border: 1px solid #a7f3d0;
  14. border-radius: var(--radius-sm);
  15. margin-bottom: 16px;
  16. }
  17. .add-zone__title {
  18. font-size: 13px;
  19. font-weight: 600;
  20. color: var(--primary-dark);
  21. margin-bottom: 10px;
  22. }
  23. .add-row {
  24. display: grid;
  25. grid-template-columns: 1fr auto auto;
  26. gap: 10px;
  27. align-items: end;
  28. }
  29. .batch-search-wrap {
  30. position: relative;
  31. }
  32. .batch-search-wrap .input {
  33. height: var(--control-h);
  34. padding-left: 36px;
  35. font-size: 14px;
  36. }
  37. .search-icon {
  38. position: absolute;
  39. left: 12px;
  40. top: 50%;
  41. transform: translateY(-50%);
  42. color: var(--text-muted);
  43. pointer-events: none;
  44. }
  45. .recent-chips {
  46. display: flex;
  47. gap: 6px;
  48. flex-wrap: wrap;
  49. margin-top: 10px;
  50. }
  51. .chip {
  52. padding: 4px 10px;
  53. border-radius: 999px;
  54. font-size: 12px;
  55. border: 1px solid var(--border);
  56. background: var(--surface);
  57. color: var(--text-muted);
  58. cursor: pointer;
  59. }
  60. .chip:hover {
  61. border-color: var(--primary);
  62. color: var(--primary-dark);
  63. background: var(--primary-bg);
  64. }
  65. .compact-table .data-table th,
  66. .compact-table .data-table td {
  67. padding: 6px 8px;
  68. }
  69. .compact-table .input {
  70. height: 28px;
  71. font-size: 12px;
  72. padding: 0 8px;
  73. }
  74. .material-cell {
  75. text-align: left !important;
  76. max-width: 200px;
  77. }
  78. .material-cell__name {
  79. font-weight: 600;
  80. font-size: 13px;
  81. overflow: hidden;
  82. text-overflow: ellipsis;
  83. white-space: nowrap;
  84. }
  85. .material-cell__sub {
  86. font-size: 11px;
  87. color: var(--text-muted);
  88. margin-top: 2px;
  89. }
  90. .drawer-hint {
  91. font-size: 11px;
  92. color: var(--primary-dark);
  93. cursor: pointer;
  94. text-decoration: underline;
  95. text-underline-offset: 2px;
  96. }
  97. /* 模拟侧滑层 */
  98. .side-drawer {
  99. display: none;
  100. position: fixed;
  101. inset: 0;
  102. z-index: 400;
  103. }
  104. .side-drawer.is-open {
  105. display: block;
  106. }
  107. .side-drawer__mask {
  108. position: absolute;
  109. inset: 0;
  110. background: rgba(15, 23, 42, 0.35);
  111. }
  112. .side-drawer__panel {
  113. position: absolute;
  114. top: 0;
  115. right: 0;
  116. width: min(420px, 100%);
  117. height: 100%;
  118. background: var(--surface);
  119. box-shadow: -8px 0 32px rgba(15, 23, 42, 0.12);
  120. display: flex;
  121. flex-direction: column;
  122. }
  123. .side-drawer__head {
  124. padding: 16px 18px;
  125. border-bottom: 1px solid var(--border);
  126. display: flex;
  127. justify-content: space-between;
  128. align-items: center;
  129. }
  130. .side-drawer__body {
  131. flex: 1;
  132. overflow-y: auto;
  133. padding: 16px 18px;
  134. }
  135. .side-drawer__foot {
  136. padding: 12px 18px;
  137. border-top: 1px solid var(--border);
  138. display: flex;
  139. justify-content: flex-end;
  140. gap: 8px;
  141. }
  142. .drawer-form {
  143. display: flex;
  144. flex-direction: column;
  145. gap: 12px;
  146. }
  147. </style>
  148. </head>
  149. <body>
  150. <header class="demo-banner">
  151. <div>
  152. <h1><span class="demo-badge">04 检索优先</span> 登记未用完物料</h1>
  153. <p>顶部批次检索添加入口 · 精简 7 列表格 · 手工登记走侧滑</p>
  154. </div>
  155. <div class="demo-banner__actions">
  156. <a href="index.html">← 选型索引</a>
  157. <a href="03-collapsible.html">03 折叠</a>
  158. <a class="is-current" href="04-quick-add.html">04 检索</a>
  159. <a href="05-current-baseline.html">05 现状</a>
  160. </div>
  161. </header>
  162. <div class="page-wrap">
  163. <div class="dialog-backdrop">
  164. <div class="dialog dialog--wide" role="dialog">
  165. <div class="dialog__head">
  166. <h2 class="dialog__title">登记未用完物料</h2>
  167. <button type="button" class="dialog__close">×</button>
  168. </div>
  169. <div class="dialog__body">
  170. <div class="add-zone">
  171. <div class="add-zone__title">从库存批次添加</div>
  172. <div class="add-row">
  173. <div class="batch-search-wrap">
  174. <span class="search-icon">🔍</span>
  175. <input type="search" class="input" id="batch-search" placeholder="输入入库编号、物料名称、批号、货号检索…" />
  176. </div>
  177. <button type="button" class="btn btn-primary" id="mock-add">添加选中批次</button>
  178. <button type="button" class="btn" id="open-drawer">手工登记</button>
  179. </div>
  180. <div class="recent-chips">
  181. <span style="font-size: 11px; color: var(--text-muted); align-self: center">最近:</span>
  182. <button type="button" class="chip">RK202506001 · 胎牛血清</button>
  183. <button type="button" class="chip">RK202506008 · DMEM</button>
  184. <button type="button" class="chip">RK202506015 · 双抗</button>
  185. </div>
  186. </div>
  187. <div class="section-head">
  188. <div class="section-head__left">
  189. <div class="section-title">已登记明细 <span class="stat-pill" style="margin-left: 8px">共 <strong id="line-count">0</strong> 项</span></div>
  190. <div class="section-hint">表格仅保留登记所需核心列;完整批次信息点击「详情」查看。</div>
  191. </div>
  192. </div>
  193. <div class="table-scroll compact-table">
  194. <table class="data-table">
  195. <thead>
  196. <tr>
  197. <th style="min-width: 40px">#</th>
  198. <th class="material-cell" style="min-width: 180px">物料</th>
  199. <th style="min-width: 88px">未用完数量</th>
  200. <th style="min-width: 72px">单位</th>
  201. <th style="min-width: 96px">剩余金额</th>
  202. <th style="min-width: 88px">持有人</th>
  203. <th style="min-width: 100px">备注</th>
  204. <th style="min-width: 72px">详情</th>
  205. <th style="min-width: 44px">删</th>
  206. </tr>
  207. </thead>
  208. <tbody id="tbody"></tbody>
  209. </table>
  210. </div>
  211. </div>
  212. <div class="dialog__foot">
  213. <button type="button" class="btn btn-ghost">取消</button>
  214. <button type="button" class="btn btn-primary">保存</button>
  215. </div>
  216. </div>
  217. </div>
  218. <div class="side-drawer" id="drawer">
  219. <div class="side-drawer__mask" id="drawer-close"></div>
  220. <div class="side-drawer__panel">
  221. <div class="side-drawer__head">
  222. <strong>手工登记</strong>
  223. <button type="button" class="dialog__close" id="drawer-x">×</button>
  224. </div>
  225. <div class="side-drawer__body">
  226. <div class="drawer-form">
  227. <div><label class="field-label">物料名称 <span style="color: var(--danger)">*</span></label><input class="input" placeholder="必填" /></div>
  228. <div><label class="field-label">批号</label><input class="input" placeholder="可选" /></div>
  229. <div><label class="field-label">规格型号</label><input class="input" /></div>
  230. <div><label class="field-label">单位</label><input class="input" /></div>
  231. <div><label class="field-label">单价(元)</label><input class="input input-num" type="number" /></div>
  232. <div><label class="field-label">未用完数量 <span style="color: var(--danger)">*</span></label><input class="input input-num" type="number" value="1" /></div>
  233. <div><label class="field-label">持有人</label><input class="input" /></div>
  234. <div><label class="field-label">备注</label><input class="input" /></div>
  235. </div>
  236. </div>
  237. <div class="side-drawer__foot">
  238. <button type="button" class="btn btn-ghost" id="drawer-cancel">取消</button>
  239. <button type="button" class="btn btn-primary">加入列表</button>
  240. </div>
  241. </div>
  242. </div>
  243. <p class="style-note">
  244. <strong>适用场景:</strong>绝大多数行来自批次检索,手工登记是少数。
  245. <ul>
  246. <li>检索区视觉权重最高,减少「先点新增再找批次」的步骤</li>
  247. <li>表格 7 列 + 操作,20 条仍可在 520px 高度内滚动浏览</li>
  248. <li>手工登记字段多,放侧滑不打断主列表节奏(Demo 可点「手工登记」体验)</li>
  249. </ul>
  250. </p>
  251. </div>
  252. <script src="_demo-data.js"></script>
  253. <script>
  254. let rows = [...DEMO_UNUSED_LINES]
  255. const tbody = document.getElementById('tbody')
  256. const drawer = document.getElementById('drawer')
  257. function openDrawer() {
  258. drawer.classList.add('is-open')
  259. }
  260. function closeDrawer() {
  261. drawer.classList.remove('is-open')
  262. }
  263. document.getElementById('open-drawer').addEventListener('click', openDrawer)
  264. document.getElementById('drawer-close').addEventListener('click', closeDrawer)
  265. document.getElementById('drawer-x').addEventListener('click', closeDrawer)
  266. document.getElementById('drawer-cancel').addEventListener('click', closeDrawer)
  267. document.getElementById('mock-add').addEventListener('click', () => {
  268. alert('Demo:选中批次后会插入表格首行,并自动聚焦「未用完数量」')
  269. })
  270. function render() {
  271. tbody.innerHTML = rows
  272. .map((row, i) => {
  273. const amt = formatMoney(calcAmount(row.unitPrice, row.unusedQty))
  274. return `<tr>
  275. <td><span class="row-index">${i + 1}</span></td>
  276. <td class="material-cell">
  277. <div class="material-cell__name">${row.materialName}</div>
  278. <div class="material-cell__sub">${row.stockInNo || '手工'} · ${row.batchNo || '—'} · ${row.manufacturer || '—'}</div>
  279. </td>
  280. <td><input class="input input-num qty" data-id="${row.id}" type="number" value="${row.unusedQty}" min="0.01" step="0.01" /></td>
  281. <td>${row.unit || '—'}</td>
  282. <td class="money">${amt}</td>
  283. <td><input class="input holder" data-id="${row.id}" value="${row.holder || ''}" placeholder="持有人" /></td>
  284. <td><input class="input remark" data-id="${row.id}" value="${row.remark || ''}" placeholder="可选" /></td>
  285. <td><span class="drawer-hint detail-link" data-id="${row.id}">查看</span></td>
  286. <td><button type="button" class="btn btn-danger-link btn-sm del" data-id="${row.id}">×</button></td>
  287. </tr>`
  288. })
  289. .join('')
  290. document.getElementById('line-count').textContent = rows.length
  291. }
  292. tbody.addEventListener('input', (e) => {
  293. const row = rows.find((r) => r.id === Number(e.target.dataset.id))
  294. if (!row) return
  295. if (e.target.classList.contains('qty')) {
  296. row.unusedQty = Number(e.target.value) || 0
  297. render()
  298. } else if (e.target.classList.contains('holder')) row.holder = e.target.value
  299. else if (e.target.classList.contains('remark')) row.remark = e.target.value
  300. })
  301. tbody.addEventListener('click', (e) => {
  302. if (e.target.classList.contains('del')) {
  303. rows = rows.filter((r) => r.id !== Number(e.target.dataset.id))
  304. render()
  305. }
  306. if (e.target.classList.contains('detail-link')) {
  307. alert('Demo:打开只读详情 Popover / Drawer,展示完整批次字段')
  308. }
  309. })
  310. render()
  311. </script>
  312. </body>
  313. </html>