pl-site-driver-brand-qty-demo.html 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764
  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</title>
  7. <style>
  8. :root {
  9. --mint: #059669;
  10. --mint-soft: #ecfdf5;
  11. --mint-line: #a7f3d0;
  12. --slate: #64748b;
  13. --ink: #0f172a;
  14. --warn: #d97706;
  15. --danger: #dc2626;
  16. --bg: #f1f5f9;
  17. }
  18. * { box-sizing: border-box; }
  19. body {
  20. margin: 0;
  21. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
  22. background: linear-gradient(165deg, #ecfdf5 0%, #f8fafc 42%, #e2e8f0 100%);
  23. color: var(--ink);
  24. min-height: 100vh;
  25. }
  26. .page {
  27. max-width: 1180px;
  28. margin: 0 auto;
  29. padding: 28px 20px 48px;
  30. }
  31. h1 {
  32. margin: 0 0 6px;
  33. font-size: 22px;
  34. font-weight: 700;
  35. }
  36. .lead {
  37. margin: 0 0 20px;
  38. color: var(--slate);
  39. font-size: 14px;
  40. line-height: 1.6;
  41. }
  42. .layout-picker {
  43. display: grid;
  44. grid-template-columns: repeat(3, 1fr);
  45. gap: 12px;
  46. margin-bottom: 24px;
  47. }
  48. .layout-opt {
  49. border: 2px solid #e2e8f0;
  50. background: #fff;
  51. border-radius: 12px;
  52. padding: 14px 14px 12px;
  53. cursor: pointer;
  54. text-align: left;
  55. transition: border-color .15s, box-shadow .15s;
  56. }
  57. .layout-opt:hover { border-color: var(--mint-line); }
  58. .layout-opt.active {
  59. border-color: var(--mint);
  60. box-shadow: 0 0 0 3px rgb(16 185 129 / 18%);
  61. }
  62. .layout-opt .tag {
  63. display: inline-block;
  64. font-size: 11px;
  65. font-weight: 700;
  66. color: var(--mint);
  67. background: var(--mint-soft);
  68. padding: 2px 8px;
  69. border-radius: 999px;
  70. margin-bottom: 8px;
  71. }
  72. .layout-opt h3 {
  73. margin: 0 0 4px;
  74. font-size: 15px;
  75. }
  76. .layout-opt p {
  77. margin: 0;
  78. font-size: 12px;
  79. color: var(--slate);
  80. line-height: 1.5;
  81. }
  82. .phones {
  83. display: grid;
  84. grid-template-columns: 1fr 1fr;
  85. gap: 28px;
  86. align-items: start;
  87. }
  88. .phone-wrap h2 {
  89. margin: 0 0 10px;
  90. font-size: 15px;
  91. color: var(--slate);
  92. font-weight: 600;
  93. }
  94. .phone {
  95. width: 100%;
  96. max-width: 390px;
  97. margin: 0 auto;
  98. background: #0f172a;
  99. border-radius: 36px;
  100. padding: 12px;
  101. box-shadow: 0 24px 48px rgb(15 23 42 / 18%);
  102. }
  103. .phone-screen {
  104. background: var(--bg);
  105. border-radius: 28px;
  106. overflow: hidden;
  107. min-height: 640px;
  108. display: flex;
  109. flex-direction: column;
  110. }
  111. .nav {
  112. background: var(--mint);
  113. color: #fff;
  114. text-align: center;
  115. font-weight: 600;
  116. font-size: 16px;
  117. padding: 14px 16px 16px;
  118. border-radius: 0 0 14px 14px;
  119. }
  120. .scroll {
  121. flex: 1;
  122. overflow: auto;
  123. padding: 12px 0 80px;
  124. }
  125. .card {
  126. margin: 0 12px 10px;
  127. background: #fff;
  128. border-radius: 12px;
  129. overflow: hidden;
  130. border: 1px solid #e2e8f0;
  131. }
  132. .card-title {
  133. display: flex;
  134. align-items: center;
  135. gap: 6px;
  136. padding: 6px 12px;
  137. font-size: 12px;
  138. font-weight: 600;
  139. color: #047857;
  140. background: linear-gradient(90deg, #ecfdf5 0%, transparent 72%);
  141. }
  142. .card-title::before {
  143. content: "";
  144. width: 3px;
  145. height: 12px;
  146. border-radius: 2px;
  147. background: var(--mint);
  148. }
  149. .cell {
  150. display: flex;
  151. justify-content: space-between;
  152. gap: 12px;
  153. padding: 10px 14px;
  154. font-size: 14px;
  155. border-top: 1px solid #f1f5f9;
  156. }
  157. .cell:first-child { border-top: 0; }
  158. .cell .k { color: var(--slate); }
  159. .cell .v { font-weight: 600; text-align: right; }
  160. .product {
  161. margin: 10px 12px;
  162. padding: 14px;
  163. border-radius: 12px;
  164. background: linear-gradient(180deg, #f0fdf4 0%, #fff 48%);
  165. border: 1px solid #d1fae5;
  166. }
  167. .product-head {
  168. display: flex;
  169. justify-content: space-between;
  170. gap: 8px;
  171. margin-bottom: 10px;
  172. }
  173. .product-name { font-size: 15px; font-weight: 700; }
  174. .badge {
  175. flex-shrink: 0;
  176. font-size: 10px;
  177. font-weight: 600;
  178. padding: 2px 8px;
  179. border-radius: 999px;
  180. background: #d1fae5;
  181. color: #047857;
  182. height: fit-content;
  183. }
  184. .badge.warn { background: #fef3c7; color: #b45309; }
  185. .meta {
  186. display: grid;
  187. grid-template-columns: 1fr 1fr;
  188. gap: 8px;
  189. margin-bottom: 12px;
  190. }
  191. .meta-item {
  192. background: rgb(255 255 255 / 85%);
  193. border: 1px solid #e2e8f0;
  194. border-radius: 8px;
  195. padding: 8px 10px;
  196. }
  197. .meta-item.span { grid-column: 1 / -1; }
  198. .meta-label { font-size: 11px; color: var(--slate); margin-bottom: 2px; }
  199. .meta-value { font-size: 13px; font-weight: 600; }
  200. .meta-value.plan { color: var(--mint); font-size: 16px; }
  201. .actual-box {
  202. background: #fff;
  203. border: 1px solid var(--mint-line);
  204. border-radius: 10px;
  205. padding: 10px 12px;
  206. }
  207. .actual-box__label {
  208. display: flex;
  209. justify-content: space-between;
  210. align-items: baseline;
  211. gap: 8px;
  212. margin-bottom: 6px;
  213. }
  214. .actual-box__label strong { font-size: 13px; }
  215. .actual-box__label span { font-size: 11px; color: var(--slate); }
  216. .actual-row {
  217. display: flex;
  218. align-items: center;
  219. gap: 8px;
  220. }
  221. .actual-row input {
  222. flex: 1;
  223. border: 0;
  224. outline: none;
  225. font-size: 22px;
  226. font-weight: 700;
  227. color: var(--ink);
  228. width: 0;
  229. background: transparent;
  230. }
  231. .unit { color: var(--slate); font-size: 13px; font-weight: 600; }
  232. .brand-block {
  233. margin-top: 10px;
  234. border-top: 1px dashed #bbf7d0;
  235. padding-top: 10px;
  236. }
  237. .brand-head {
  238. display: flex;
  239. align-items: center;
  240. justify-content: space-between;
  241. gap: 8px;
  242. margin-bottom: 8px;
  243. }
  244. .brand-head h4 {
  245. margin: 0;
  246. font-size: 13px;
  247. color: #047857;
  248. }
  249. .brand-sum {
  250. font-size: 11px;
  251. color: var(--slate);
  252. }
  253. .brand-sum.bad { color: var(--danger); font-weight: 600; }
  254. .brand-sum.ok { color: var(--mint); font-weight: 600; }
  255. .brand-row {
  256. display: grid;
  257. grid-template-columns: 1fr 88px 28px;
  258. gap: 6px;
  259. align-items: center;
  260. margin-bottom: 6px;
  261. }
  262. .brand-row select,
  263. .brand-row input {
  264. height: 36px;
  265. border: 1px solid #cbd5e1;
  266. border-radius: 8px;
  267. padding: 0 8px;
  268. font-size: 13px;
  269. background: #fff;
  270. width: 100%;
  271. }
  272. .brand-row input { text-align: right; font-weight: 600; }
  273. .icon-btn {
  274. width: 28px;
  275. height: 28px;
  276. border: 0;
  277. border-radius: 8px;
  278. background: #fee2e2;
  279. color: var(--danger);
  280. font-size: 16px;
  281. cursor: pointer;
  282. line-height: 1;
  283. }
  284. .add-brand {
  285. width: 100%;
  286. margin-top: 4px;
  287. height: 34px;
  288. border: 1px dashed var(--mint);
  289. border-radius: 8px;
  290. background: var(--mint-soft);
  291. color: var(--mint);
  292. font-size: 13px;
  293. font-weight: 600;
  294. cursor: pointer;
  295. }
  296. .chips {
  297. display: flex;
  298. flex-wrap: wrap;
  299. gap: 6px;
  300. margin-bottom: 8px;
  301. }
  302. .chip {
  303. border: 1px solid #cbd5e1;
  304. background: #fff;
  305. border-radius: 999px;
  306. padding: 6px 10px;
  307. font-size: 12px;
  308. cursor: pointer;
  309. }
  310. .chip.on {
  311. border-color: var(--mint);
  312. background: var(--mint-soft);
  313. color: #047857;
  314. font-weight: 600;
  315. }
  316. .chip .q {
  317. margin-left: 4px;
  318. color: var(--slate);
  319. font-weight: 500;
  320. }
  321. .footer {
  322. position: sticky;
  323. bottom: 0;
  324. padding: 12px 16px 16px;
  325. background: linear-gradient(180deg, transparent, #f1f5f9 28%);
  326. }
  327. .submit {
  328. width: 100%;
  329. height: 44px;
  330. border: 0;
  331. border-radius: 999px;
  332. background: var(--mint);
  333. color: #fff;
  334. font-size: 16px;
  335. font-weight: 600;
  336. cursor: pointer;
  337. }
  338. .note {
  339. margin-top: 20px;
  340. padding: 14px 16px;
  341. background: #fff;
  342. border-radius: 12px;
  343. border: 1px solid #e2e8f0;
  344. font-size: 13px;
  345. color: var(--slate);
  346. line-height: 1.6;
  347. }
  348. .note strong { color: var(--ink); }
  349. @media (max-width: 900px) {
  350. .phones, .layout-picker { grid-template-columns: 1fr; }
  351. }
  352. </style>
  353. </head>
  354. <body>
  355. <div class="page">
  356. <h1>运输计划回传 · 按品牌填量 Demo</h1>
  357. <p class="lead">
  358. 左侧模拟<strong>现场链接</strong>(入/出库单回执),右侧模拟<strong>司机链接</strong>。
  359. 有品牌的产品(烧碱)可填 1~N 条「品牌 + 数量」;无品牌产品(盐酸)只填总实际数量。
  360. 请先选择下方交互方案,再在手机框内试用。
  361. </p>
  362. <div class="layout-picker" id="layoutPicker">
  363. <button type="button" class="layout-opt active" data-layout="a" onclick="setLayout('a')">
  364. <span class="tag">方案 A · 推荐</span>
  365. <h3>下拉选品牌 + 数量行</h3>
  366. <p>可增减行;合计自动回写实际数量。与归档弹窗逻辑一致,适合现场快速录入。</p>
  367. </button>
  368. <button type="button" class="layout-opt" data-layout="b" onclick="setLayout('b')">
  369. <span class="tag">方案 B</span>
  370. <h3>品牌芯片 + 点选填量</h3>
  371. <p>先点选参与品牌,再为每个已选品牌填数量。品牌少时更直观。</p>
  372. </button>
  373. <button type="button" class="layout-opt" data-layout="c" onclick="setLayout('c')">
  374. <span class="tag">方案 C</span>
  375. <h3>仅现场填品牌;司机只填总量</h3>
  376. <p>司机链接保持现状(一个总实际数量);品牌明细只在现场链接填写。</p>
  377. </button>
  378. </div>
  379. <div class="phones">
  380. <!-- 现场 -->
  381. <div class="phone-wrap">
  382. <h2>现场链接 · 入库单回执</h2>
  383. <div class="phone">
  384. <div class="phone-screen">
  385. <div class="nav">入库单 · 现场回执</div>
  386. <div class="scroll">
  387. <div class="card">
  388. <div class="cell"><span class="k">业务类型</span><span class="v">采购入库</span></div>
  389. <div class="cell"><span class="k">往来单位</span><span class="v">君正化工</span></div>
  390. <div class="cell"><span class="k">车牌</span><span class="v">皖A·12X34</span></div>
  391. <div class="cell"><span class="k">计划号</span><span class="v">JH20260731001</span></div>
  392. </div>
  393. <div class="card">
  394. <div class="card-title">产品实际数量</div>
  395. </div>
  396. <!-- 有品牌:烧碱 -->
  397. <div class="product" id="siteBranded">
  398. <div class="product-head">
  399. <div class="product-name">烧碱</div>
  400. <span class="badge warn">不限品牌</span>
  401. </div>
  402. <div class="meta">
  403. <div class="meta-item">
  404. <div class="meta-label">规格型号</div>
  405. <div class="meta-value">32%</div>
  406. </div>
  407. <div class="meta-item">
  408. <div class="meta-label">包装规格</div>
  409. <div class="meta-value">散装</div>
  410. </div>
  411. <div class="meta-item span">
  412. <div class="meta-label">计划数量</div>
  413. <div class="meta-value plan">30 吨</div>
  414. </div>
  415. </div>
  416. <div class="actual-box">
  417. <div class="actual-box__label">
  418. <strong>实际数量</strong>
  419. <span class="layout-a-only layout-b-only">由品牌明细合计</span>
  420. <span class="layout-c-only" style="display:none">请按现场实装填写</span>
  421. </div>
  422. <div class="actual-row">
  423. <input id="siteActualCaustic" type="number" value="28" step="0.001" readonly />
  424. <span class="unit">吨</span>
  425. </div>
  426. </div>
  427. <div class="brand-block layout-a-only" id="siteBrandRowsA">
  428. <div class="brand-head">
  429. <h4>按品牌填量</h4>
  430. <span class="brand-sum ok" id="siteSumA">合计 28 = 实际数量 ✓</span>
  431. </div>
  432. <div id="siteRowsA"></div>
  433. <button type="button" class="add-brand" onclick="addSiteRowA()">+ 添加品牌</button>
  434. </div>
  435. <div class="brand-block layout-b-only" id="siteBrandRowsB" style="display:none">
  436. <div class="brand-head">
  437. <h4>选择品牌并填量</h4>
  438. <span class="brand-sum ok" id="siteSumB">合计 28 = 实际数量 ✓</span>
  439. </div>
  440. <div class="chips" id="siteChips"></div>
  441. <div id="siteRowsB"></div>
  442. </div>
  443. </div>
  444. <!-- 无品牌:盐酸 -->
  445. <div class="product">
  446. <div class="product-head">
  447. <div class="product-name">盐酸</div>
  448. <span class="badge">产品 2/2</span>
  449. </div>
  450. <div class="meta">
  451. <div class="meta-item">
  452. <div class="meta-label">规格型号</div>
  453. <div class="meta-value">31%</div>
  454. </div>
  455. <div class="meta-item">
  456. <div class="meta-label">包装规格</div>
  457. <div class="meta-value">吨袋</div>
  458. </div>
  459. <div class="meta-item span">
  460. <div class="meta-label">计划数量</div>
  461. <div class="meta-value plan">10 吨</div>
  462. </div>
  463. </div>
  464. <div class="actual-box">
  465. <div class="actual-box__label">
  466. <strong>实际数量</strong>
  467. <span>无品牌产品,直接填写总量</span>
  468. </div>
  469. <div class="actual-row">
  470. <input type="number" value="9.8" step="0.001" />
  471. <span class="unit">吨</span>
  472. </div>
  473. </div>
  474. </div>
  475. </div>
  476. <div class="footer">
  477. <button type="button" class="submit" onclick="alert('Demo:现场回执已提交(含品牌明细)')">提交</button>
  478. </div>
  479. </div>
  480. </div>
  481. </div>
  482. <!-- 司机 -->
  483. <div class="phone-wrap">
  484. <h2>司机链接 · 运输回传</h2>
  485. <div class="phone">
  486. <div class="phone-screen">
  487. <div class="nav">运输计划</div>
  488. <div class="scroll">
  489. <div class="card">
  490. <div class="cell"><span class="k">计划编号</span><span class="v">JH20260731001</span></div>
  491. <div class="cell"><span class="k">车牌</span><span class="v">皖A·12X34</span></div>
  492. <div class="cell"><span class="k">司机</span><span class="v">张师傅 138****0001</span></div>
  493. </div>
  494. <div class="card">
  495. <div class="card-title">产品明细</div>
  496. <div class="cell"><span class="k">烧碱 32% · 散装</span><span class="v">计划 30 吨</span></div>
  497. <div class="cell"><span class="k">盐酸 31% · 吨袋</span><span class="v">计划 10 吨</span></div>
  498. </div>
  499. <div class="card layout-c-only" style="display:none">
  500. <div class="card-title">回传数量</div>
  501. <div style="padding:12px 14px 14px">
  502. <div class="actual-box">
  503. <div class="actual-box__label">
  504. <strong>实际数量</strong>
  505. <span>方案 C:司机只填总量</span>
  506. </div>
  507. <div class="actual-row">
  508. <input type="number" value="37.8" step="0.001" />
  509. <span class="unit">吨</span>
  510. </div>
  511. </div>
  512. </div>
  513. </div>
  514. <div class="layout-a-only layout-b-only" id="driverProducts">
  515. <div class="card"><div class="card-title">回传数量(按产品)</div></div>
  516. <div class="product">
  517. <div class="product-head">
  518. <div class="product-name">烧碱</div>
  519. <span class="badge warn">不限品牌</span>
  520. </div>
  521. <div class="actual-box">
  522. <div class="actual-box__label">
  523. <strong>实际数量</strong>
  524. <span>由品牌明细合计</span>
  525. </div>
  526. <div class="actual-row">
  527. <input id="driverActualCaustic" type="number" value="28" step="0.001" readonly />
  528. <span class="unit">吨</span>
  529. </div>
  530. </div>
  531. <div class="brand-block">
  532. <div class="brand-head">
  533. <h4>按品牌填量</h4>
  534. <span class="brand-sum ok" id="driverSum">合计 28 = 实际数量 ✓</span>
  535. </div>
  536. <div id="driverRows"></div>
  537. <button type="button" class="add-brand" onclick="addDriverRow()">+ 添加品牌</button>
  538. </div>
  539. </div>
  540. <div class="product">
  541. <div class="product-head">
  542. <div class="product-name">盐酸</div>
  543. <span class="badge">无品牌</span>
  544. </div>
  545. <div class="actual-box">
  546. <div class="actual-box__label">
  547. <strong>实际数量</strong>
  548. <span>无品牌,直接填总量</span>
  549. </div>
  550. <div class="actual-row">
  551. <input type="number" value="9.8" step="0.001" />
  552. <span class="unit">吨</span>
  553. </div>
  554. </div>
  555. </div>
  556. </div>
  557. </div>
  558. <div class="footer">
  559. <button type="button" class="submit" onclick="alert('Demo:司机回执已保存')">保存回执</button>
  560. </div>
  561. </div>
  562. </div>
  563. </div>
  564. </div>
  565. <div class="note">
  566. <strong>规则预览:</strong>
  567. ① 产品有品牌概念(brandMode = any/required)时展示品牌明细;无品牌则隐藏。
  568. ② 可填 1 个或多个品牌行,数量 &gt; 0 才计入。
  569. ③ 各品牌数量合计 = 该产品实际数量;不限品牌与指定品牌(偏好)均可多选品牌,指定品牌默认预选且可取消。
  570. ④ 现场回传写入后,归档弹窗可预填品牌明细(与现有归档按品牌库存衔接)。
  571. <br /><br />
  572. 点击上方方案卡片选择偏好(会写入选择事件)。本地文件也可直接打开:
  573. <code>.superpowers/brainstorm/.../content/site-driver-brand-qty-demo.html</code>
  574. </div>
  575. </div>
  576. <script>
  577. const BRANDS = [
  578. { id: 'b1', name: '君正' },
  579. { id: 'b2', name: '红四方' },
  580. { id: 'b3', name: '中盐' }
  581. ]
  582. let layout = 'a'
  583. let siteRows = [
  584. { brandId: 'b1', qty: 16 },
  585. { brandId: 'b2', qty: 12 }
  586. ]
  587. let driverRows = [
  588. { brandId: 'b1', qty: 16 },
  589. { brandId: 'b2', qty: 12 }
  590. ]
  591. let selectedChips = new Set(['b1', 'b2'])
  592. function setLayout(key) {
  593. layout = key
  594. document.querySelectorAll('.layout-opt').forEach(el => {
  595. el.classList.toggle('active', el.dataset.layout === key)
  596. })
  597. document.querySelectorAll('.layout-a-only').forEach(el => {
  598. el.style.display = key === 'a' || (el.classList.contains('layout-b-only') && key === 'b') ? '' : 'none'
  599. })
  600. document.querySelectorAll('.layout-b-only').forEach(el => {
  601. if (el.classList.contains('layout-a-only')) {
  602. el.style.display = (key === 'a' || key === 'b') ? '' : 'none'
  603. } else {
  604. el.style.display = key === 'b' ? '' : 'none'
  605. }
  606. })
  607. document.querySelectorAll('.layout-c-only').forEach(el => {
  608. el.style.display = key === 'c' ? '' : 'none'
  609. })
  610. // fix dual-class elements
  611. document.querySelectorAll('.layout-a-only.layout-b-only').forEach(el => {
  612. el.style.display = (key === 'a' || key === 'b') ? '' : 'none'
  613. })
  614. document.getElementById('siteBrandRowsA').style.display = key === 'a' ? '' : 'none'
  615. document.getElementById('siteBrandRowsB').style.display = key === 'b' ? '' : 'none'
  616. const actual = document.getElementById('siteActualCaustic')
  617. actual.readOnly = key !== 'c'
  618. if (key === 'c') actual.value = '28'
  619. renderAll()
  620. if (typeof toggleSelect === 'function') {
  621. const opt = document.querySelector(`.layout-opt[data-layout="${key}"]`)
  622. if (opt) toggleSelect(opt)
  623. }
  624. }
  625. function sumRows(rows) {
  626. return rows.reduce((s, r) => s + (Number(r.qty) || 0), 0)
  627. }
  628. function brandOptionsHtml(selectedId) {
  629. return BRANDS.map(b =>
  630. `<option value="${b.id}" ${b.id === selectedId ? 'selected' : ''}>${b.name}</option>`
  631. ).join('')
  632. }
  633. function updateSumLabel(el, total, actual) {
  634. const ok = Math.abs(total - actual) < 0.0005
  635. el.className = 'brand-sum ' + (ok ? 'ok' : 'bad')
  636. el.textContent = ok
  637. ? `合计 ${total} = 实际数量 ✓`
  638. : `合计 ${total} ≠ 实际数量 ${actual}`
  639. }
  640. function renderSiteA() {
  641. const box = document.getElementById('siteRowsA')
  642. box.innerHTML = siteRows.map((r, i) => `
  643. <div class="brand-row">
  644. <select onchange="siteRows[${i}].brandId=this.value; renderAll()">
  645. ${brandOptionsHtml(r.brandId)}
  646. </select>
  647. <input type="number" step="0.001" min="0" value="${r.qty}"
  648. oninput="siteRows[${i}].qty=Number(this.value)||0; syncSiteActual()" />
  649. <button type="button" class="icon-btn" onclick="siteRows.splice(${i},1); renderAll()" title="删除">×</button>
  650. </div>
  651. `).join('')
  652. syncSiteActual()
  653. }
  654. function syncSiteActual() {
  655. const total = Number(sumRows(siteRows).toFixed(3))
  656. document.getElementById('siteActualCaustic').value = total
  657. updateSumLabel(document.getElementById('siteSumA'), total, total)
  658. updateSumLabel(document.getElementById('siteSumB'), total, total)
  659. }
  660. function addSiteRowA() {
  661. const used = new Set(siteRows.map(r => r.brandId))
  662. const next = BRANDS.find(b => !used.has(b.id)) || BRANDS[0]
  663. siteRows.push({ brandId: next.id, qty: 0 })
  664. renderAll()
  665. }
  666. function renderSiteB() {
  667. const chips = document.getElementById('siteChips')
  668. chips.innerHTML = BRANDS.map(b => {
  669. const on = selectedChips.has(b.id)
  670. const row = siteRows.find(r => r.brandId === b.id)
  671. const q = row ? row.qty : 0
  672. return `<button type="button" class="chip ${on ? 'on' : ''}" onclick="toggleChip('${b.id}')">
  673. ${b.name}${on ? `<span class="q">${q || ''}</span>` : ''}
  674. </button>`
  675. }).join('')
  676. const box = document.getElementById('siteRowsB')
  677. const active = BRANDS.filter(b => selectedChips.has(b.id))
  678. box.innerHTML = active.map(b => {
  679. let row = siteRows.find(r => r.brandId === b.id)
  680. if (!row) {
  681. row = { brandId: b.id, qty: 0 }
  682. siteRows.push(row)
  683. }
  684. const i = siteRows.indexOf(row)
  685. return `<div class="brand-row" style="grid-template-columns: 72px 1fr">
  686. <div style="font-size:13px;font-weight:600;padding-left:4px">${b.name}</div>
  687. <input type="number" step="0.001" min="0" value="${row.qty}"
  688. oninput="siteRows[${i}].qty=Number(this.value)||0; syncSiteActual(); renderSiteB()" />
  689. </div>`
  690. }).join('')
  691. siteRows = siteRows.filter(r => selectedChips.has(r.brandId))
  692. syncSiteActual()
  693. }
  694. function toggleChip(id) {
  695. if (selectedChips.has(id)) selectedChips.delete(id)
  696. else selectedChips.add(id)
  697. renderAll()
  698. }
  699. function renderDriver() {
  700. const box = document.getElementById('driverRows')
  701. box.innerHTML = driverRows.map((r, i) => `
  702. <div class="brand-row">
  703. <select onchange="driverRows[${i}].brandId=this.value; renderAll()">
  704. ${brandOptionsHtml(r.brandId)}
  705. </select>
  706. <input type="number" step="0.001" min="0" value="${r.qty}"
  707. oninput="driverRows[${i}].qty=Number(this.value)||0; syncDriverActual()" />
  708. <button type="button" class="icon-btn" onclick="driverRows.splice(${i},1); renderAll()">×</button>
  709. </div>
  710. `).join('')
  711. syncDriverActual()
  712. }
  713. function syncDriverActual() {
  714. const total = Number(sumRows(driverRows).toFixed(3))
  715. document.getElementById('driverActualCaustic').value = total
  716. updateSumLabel(document.getElementById('driverSum'), total, total)
  717. }
  718. function addDriverRow() {
  719. const used = new Set(driverRows.map(r => r.brandId))
  720. const next = BRANDS.find(b => !used.has(b.id)) || BRANDS[0]
  721. driverRows.push({ brandId: next.id, qty: 0 })
  722. renderAll()
  723. }
  724. function renderAll() {
  725. renderSiteA()
  726. renderSiteB()
  727. renderDriver()
  728. }
  729. renderAll()
  730. setLayout('a')
  731. </script>
  732. </body>
  733. </html>