fresh-mint-pl-purchase-cert-link-compare-demo.html 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666
  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>Fresh Mint · 购买证关联运输计划 · 校验与比对 Demo</title>
  7. <link rel="preconnect" href="https://fonts.googleapis.com" />
  8. <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
  9. <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
  10. <style>
  11. :root {
  12. --primary: #10b981;
  13. --primary-dark: #059669;
  14. --primary-bg: #ecfdf5;
  15. --text: #1e293b;
  16. --text-muted: #64748b;
  17. --border: #d9e8e2;
  18. --surface: #ffffff;
  19. --bg-page: #f1f5f9;
  20. --shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
  21. --shadow-lg: 0 12px 40px rgba(15, 23, 42, 0.14);
  22. --radius: 10px;
  23. --warn: #d97706;
  24. --warn-bg: #fffbeb;
  25. --warn-border: #fde68a;
  26. --danger: #dc2626;
  27. --danger-bg: #fef2f2;
  28. --danger-border: #fecaca;
  29. --ok: #059669;
  30. --ok-bg: #ecfdf5;
  31. --ok-border: #a7f3d0;
  32. }
  33. * { box-sizing: border-box; }
  34. body {
  35. margin: 0;
  36. min-height: 100vh;
  37. font-family: Inter, -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
  38. font-size: 14px;
  39. line-height: 1.5;
  40. color: var(--text);
  41. background: var(--bg-page);
  42. }
  43. .demo-page { max-width: 1100px; margin: 0 auto; padding: 28px 24px 80px; }
  44. .demo-head { margin-bottom: 18px; }
  45. .demo-head h1 { margin: 0 0 8px; font-size: 22px; font-weight: 700; letter-spacing: -0.02em; }
  46. .demo-head h1 span { color: var(--primary); }
  47. .demo-head p { margin: 0; color: var(--text-muted); max-width: 920px; }
  48. .meta { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
  49. .pill {
  50. display: inline-flex; align-items: center; gap: 6px;
  51. padding: 4px 10px; border-radius: 999px; font-size: 12px; font-weight: 500;
  52. background: var(--primary-bg); color: var(--primary-dark); border: 1px solid #a7f3d0;
  53. }
  54. .pill.muted { background: #f1f5f9; color: var(--text-muted); border-color: #e2e8f0; }
  55. .pill.warn { background: var(--warn-bg); color: var(--warn); border-color: var(--warn-border); }
  56. .pill.danger { background: var(--danger-bg); color: var(--danger); border-color: var(--danger-border); }
  57. .brief {
  58. display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 16px;
  59. }
  60. @media (max-width: 900px) { .brief { grid-template-columns: 1fr; } }
  61. .card {
  62. background: var(--surface); border: 1px solid var(--border);
  63. border-radius: var(--radius); padding: 14px 16px; box-shadow: var(--shadow);
  64. }
  65. .card h2 {
  66. margin: 0 0 8px; font-size: 14px; font-weight: 600;
  67. padding-left: 10px; border-left: 3px solid var(--primary);
  68. color: var(--primary-dark);
  69. }
  70. .card.danger-card h2 { border-left-color: var(--danger); color: var(--danger); }
  71. .card.warn-card h2 { border-left-color: var(--warn); color: var(--warn); }
  72. .card ul { margin: 0; padding-left: 18px; color: var(--text-muted); font-size: 13px; }
  73. .card li { margin: 4px 0; }
  74. .card li strong { color: var(--text); }
  75. .formula {
  76. margin-top: 8px; padding: 8px 10px; border-radius: 8px;
  77. background: var(--primary-bg); color: var(--primary-dark);
  78. font-size: 12px; font-weight: 500;
  79. }
  80. .formula.warn { background: var(--warn-bg); color: #92400e; }
  81. .formula.danger { background: var(--danger-bg); color: #991b1b; }
  82. .scenario-tabs {
  83. display: flex; gap: 8px; margin-bottom: 14px; flex-wrap: wrap;
  84. }
  85. .scenario-tab {
  86. appearance: none; border: 1px solid var(--border); background: #fff;
  87. height: 36px; padding: 0 14px; border-radius: 8px; cursor: pointer;
  88. font-size: 13px; font-weight: 500; color: var(--text);
  89. }
  90. .scenario-tab:hover { border-color: #a7f3d0; color: var(--primary-dark); }
  91. .scenario-tab.active {
  92. background: var(--primary-bg); border-color: #a7f3d0; color: var(--primary-dark);
  93. }
  94. /* Dialog shell */
  95. .stage {
  96. background: rgba(15, 23, 42, 0.35);
  97. border-radius: 14px;
  98. padding: 28px 20px 36px;
  99. min-height: 640px;
  100. display: flex;
  101. align-items: flex-start;
  102. justify-content: center;
  103. }
  104. .dialog {
  105. width: min(920px, 100%);
  106. background: var(--surface);
  107. border-radius: 12px;
  108. box-shadow: var(--shadow-lg);
  109. overflow: hidden;
  110. display: flex;
  111. flex-direction: column;
  112. max-height: calc(100vh - 120px);
  113. }
  114. .dialog-hd {
  115. display: flex; align-items: center; justify-content: space-between;
  116. padding: 16px 20px; border-bottom: 1px solid var(--border);
  117. }
  118. .dialog-hd h3 { margin: 0; font-size: 16px; font-weight: 600; }
  119. .dialog-hd .x {
  120. width: 28px; height: 28px; border: none; background: transparent;
  121. color: var(--text-muted); font-size: 18px; cursor: pointer; border-radius: 6px;
  122. }
  123. .dialog-hd .x:hover { background: #f1f5f9; }
  124. .dialog-bd {
  125. padding: 16px 20px;
  126. overflow-y: auto;
  127. flex: 1;
  128. }
  129. .dialog-ft {
  130. display: flex; justify-content: flex-end; gap: 10px;
  131. padding: 12px 20px; border-top: 1px solid var(--border);
  132. background: #fafafa;
  133. }
  134. .btn {
  135. display: inline-flex; align-items: center; gap: 6px;
  136. height: 36px; padding: 0 14px; border-radius: 8px;
  137. font-size: 14px; font-weight: 500; cursor: pointer;
  138. border: 1px solid var(--border); background: #fff; color: var(--text);
  139. }
  140. .btn:hover { border-color: #a7f3d0; }
  141. .btn:disabled { opacity: 0.45; cursor: not-allowed; }
  142. .btn-primary {
  143. background: var(--primary); border-color: var(--primary); color: #fff;
  144. }
  145. .btn-primary:hover { background: var(--primary-dark); border-color: var(--primary-dark); }
  146. .btn-primary:disabled { background: #94a3b8; border-color: #94a3b8; }
  147. .field-label {
  148. display: block; font-size: 11px; color: var(--text-muted);
  149. margin-bottom: 6px; font-weight: 500;
  150. }
  151. .select-like {
  152. height: 36px; border: 1px solid var(--border); border-radius: 8px;
  153. padding: 0 12px; display: flex; align-items: center; justify-content: space-between;
  154. background: #fff; color: var(--text); font-size: 14px;
  155. }
  156. .select-like .chev { color: var(--text-muted); font-size: 12px; }
  157. .plan-meta {
  158. margin: 12px 0 14px; padding: 10px 12px; border-radius: 8px;
  159. background: #f8fafc; border: 1px solid #e2e8f0;
  160. font-size: 13px; color: var(--text-muted);
  161. }
  162. .plan-meta strong { color: var(--text); }
  163. .link-row {
  164. border: 1px solid var(--border);
  165. border-radius: 10px;
  166. padding: 12px 14px;
  167. margin-bottom: 12px;
  168. background: #fff;
  169. }
  170. .link-row-title {
  171. display: flex; align-items: center; justify-content: space-between;
  172. gap: 10px; margin-bottom: 10px; flex-wrap: wrap;
  173. }
  174. .link-row-title .name { font-weight: 600; }
  175. .tag {
  176. display: inline-flex; align-items: center;
  177. height: 22px; padding: 0 8px; border-radius: 4px;
  178. font-size: 12px; font-weight: 500;
  179. background: transparent; color: var(--text); border: 1px solid #e2e8f0;
  180. }
  181. .tag.ok { background: var(--ok); color: #fff; border-color: var(--ok); }
  182. .tag.warn { background: var(--warn); color: #fff; border-color: var(--warn); }
  183. .tag.danger { background: var(--danger); color: #fff; border-color: var(--danger); }
  184. .link-row-fields {
  185. display: grid; grid-template-columns: 1fr 1fr; gap: 10px;
  186. }
  187. @media (max-width: 700px) { .link-row-fields { grid-template-columns: 1fr; } }
  188. /* Hard gate / soft compare panels */
  189. .gate-panel {
  190. margin-top: 12px;
  191. border-radius: 10px;
  192. border: 1px solid var(--border);
  193. overflow: hidden;
  194. }
  195. .gate-panel.ok { border-color: var(--ok-border); }
  196. .gate-panel.warn { border-color: var(--warn-border); }
  197. .gate-panel.danger { border-color: var(--danger-border); }
  198. .gate-hd {
  199. display: flex; align-items: center; justify-content: space-between;
  200. padding: 10px 12px; font-weight: 600; font-size: 13px;
  201. }
  202. .gate-panel.ok .gate-hd { background: var(--ok-bg); color: var(--ok); }
  203. .gate-panel.warn .gate-hd { background: var(--warn-bg); color: var(--warn); }
  204. .gate-panel.danger .gate-hd { background: var(--danger-bg); color: var(--danger); }
  205. .gate-bd { padding: 0; }
  206. .gate-row {
  207. display: grid;
  208. grid-template-columns: 110px 1fr 1fr 88px;
  209. gap: 8px;
  210. padding: 10px 12px;
  211. border-top: 1px solid #f1f5f9;
  212. font-size: 13px;
  213. align-items: start;
  214. }
  215. .gate-row.header {
  216. background: #f8fafc;
  217. color: var(--text-muted);
  218. font-size: 11px;
  219. font-weight: 600;
  220. text-transform: none;
  221. border-top: none;
  222. }
  223. @media (max-width: 700px) {
  224. .gate-row { grid-template-columns: 1fr; gap: 4px; }
  225. .gate-row.header { display: none; }
  226. }
  227. .gate-row .col-label { color: var(--text-muted); font-size: 12px; }
  228. .gate-row .col-plan, .gate-row .col-cert { word-break: break-all; }
  229. .gate-row.mismatch .col-plan,
  230. .gate-row.mismatch .col-cert { color: var(--warn); font-weight: 500; }
  231. .gate-row.block .col-plan,
  232. .gate-row.block .col-cert { color: var(--danger); font-weight: 500; }
  233. .gate-row.match .result { color: var(--ok); }
  234. .gate-row.mismatch .result { color: var(--warn); }
  235. .gate-row.block .result { color: var(--danger); }
  236. .qty-breakdown {
  237. margin: 10px 12px 12px;
  238. padding: 10px 12px;
  239. border-radius: 8px;
  240. background: #f8fafc;
  241. border: 1px solid #e2e8f0;
  242. font-size: 12px;
  243. color: var(--text-muted);
  244. }
  245. .qty-breakdown .line {
  246. display: flex; justify-content: space-between; gap: 12px; margin: 3px 0;
  247. }
  248. .qty-breakdown .line.total {
  249. margin-top: 8px; padding-top: 8px; border-top: 1px dashed #cbd5e1;
  250. font-weight: 600; color: var(--text); font-size: 13px;
  251. }
  252. .qty-breakdown .line.over { color: var(--danger); }
  253. .qty-breakdown .num { font-variant-numeric: tabular-nums; color: var(--text); font-weight: 500; }
  254. .toast {
  255. position: fixed; left: 50%; bottom: 28px; transform: translateX(-50%);
  256. background: #1e293b; color: #fff; padding: 10px 16px; border-radius: 8px;
  257. font-size: 13px; box-shadow: var(--shadow-lg); opacity: 0; pointer-events: none;
  258. transition: opacity 0.2s;
  259. z-index: 50;
  260. }
  261. .toast.show { opacity: 1; }
  262. .note {
  263. margin-top: 16px; font-size: 12px; color: var(--text-muted);
  264. }
  265. </style>
  266. </head>
  267. <body>
  268. <div class="demo-page">
  269. <header class="demo-head">
  270. <h1>购买证关联运输计划 · <span>校验与比对</span> Demo</h1>
  271. <p>
  272. 关联「已安排待确认」计划时:有效期与数量为硬拦截;销售单位 / 购买单位 / 购买物品与计划字段比对后展示差异,
  273. 由用户确认或取消。切换下方场景查看不同结果态。
  274. </p>
  275. <div class="meta">
  276. <span class="pill">Fresh Mint</span>
  277. <span class="pill muted">关联弹窗</span>
  278. <span class="pill warn">软比对 · 可确认</span>
  279. <span class="pill danger">硬门禁 · 不可确认</span>
  280. </div>
  281. </header>
  282. <div class="brief">
  283. <div class="card danger-card">
  284. <h2>硬门禁(不通过则禁用「确定」)</h2>
  285. <ul>
  286. <li><strong>有效期</strong>:本次计划送货/装货日期须落在购买证有效期内</li>
  287. <li><strong>数量</strong>:已归档实际量 + 已确认未归档计划量 + 本次计划量 ≤ 购买证数量</li>
  288. </ul>
  289. <div class="formula danger">
  290. 占用合计 = Σ归档实际量 + Σ已安排已确认未归档计划量 + 本次计划量 ≤ 证面数量
  291. </div>
  292. </div>
  293. <div class="card warn-card">
  294. <h2>软比对(展示差异,点确定 / 取消)</h2>
  295. <ul>
  296. <li><strong>销售单位</strong> ↔ 计划装货客户(示意)</li>
  297. <li><strong>购买单位或个人</strong> ↔ 计划送货客户(示意)</li>
  298. <li><strong>购买物品</strong> ↔ 计划产品品名</li>
  299. </ul>
  300. <div class="formula warn">
  301. 有出入时仍可点「确定」完成关联(Demo 映射待业务确认)
  302. </div>
  303. </div>
  304. </div>
  305. <div class="scenario-tabs" role="tablist">
  306. <button type="button" class="scenario-tab active" data-scene="ok">场景 A · 全部通过</button>
  307. <button type="button" class="scenario-tab" data-scene="soft">场景 B · 单位/品名有出入</button>
  308. <button type="button" class="scenario-tab" data-scene="qty">场景 C · 数量超限</button>
  309. <button type="button" class="scenario-tab" data-scene="date">场景 D · 超出有效期</button>
  310. </div>
  311. <div class="stage">
  312. <div class="dialog" id="dialog">
  313. <div class="dialog-hd">
  314. <h3>关联待确认计划</h3>
  315. <button type="button" class="x" aria-label="关闭">×</button>
  316. </div>
  317. <div class="dialog-bd">
  318. <label class="field-label">待确认计划</label>
  319. <div class="select-like">
  320. <span id="planLabel">YS-20261009-018 · 陕A·D8821</span>
  321. <span class="chev">▾</span>
  322. </div>
  323. <div class="plan-meta" id="planMeta">
  324. 车牌 <strong>陕A·D8821</strong>;送货日期 <strong>2026-10-12</strong>;
  325. 每行易制毒产品须绑定 1 张购买证 + 1 张运输证。
  326. </div>
  327. <div class="link-row">
  328. <div class="link-row-title">
  329. <span class="name" id="prodName">丙酮</span>
  330. <span class="tag" id="prodQtyTag">计划量 32 吨</span>
  331. </div>
  332. <div class="link-row-fields">
  333. <div>
  334. <label class="field-label">购买证</label>
  335. <div class="select-like" id="purchaseSelect">
  336. <span>购备〔2026〕0831 · 丙酮 · 余 118</span>
  337. <span class="chev">▾</span>
  338. </div>
  339. </div>
  340. <div>
  341. <label class="field-label">运输证</label>
  342. <div class="select-like" id="transportSelect">
  343. <span>运备〔2026〕2104 · 车陕A·D8821 · 余 40</span>
  344. <span class="chev">▾</span>
  345. </div>
  346. </div>
  347. </div>
  348. <!-- 硬门禁 -->
  349. <div class="gate-panel" id="hardGate">
  350. <div class="gate-hd">
  351. <span id="hardGateTitle">硬门禁校验</span>
  352. <span class="tag" id="hardGateTag">通过</span>
  353. </div>
  354. <div class="gate-bd">
  355. <div class="gate-row header">
  356. <div>校验项</div>
  357. <div>计划侧</div>
  358. <div>购买证侧</div>
  359. <div>结果</div>
  360. </div>
  361. <div class="gate-row" id="rowDate">
  362. <div class="col-label">有效期限</div>
  363. <div class="col-plan" id="datePlan">送货日 2026-10-12</div>
  364. <div class="col-cert" id="dateCert">2026-09-01 ~ 2026-12-31</div>
  365. <div class="result" id="dateResult">通过</div>
  366. </div>
  367. <div class="gate-row" id="rowQty">
  368. <div class="col-label">数量占用</div>
  369. <div class="col-plan" id="qtyPlan">本次 32 吨</div>
  370. <div class="col-cert" id="qtyCert">证面 170 吨</div>
  371. <div class="result" id="qtyResult">通过</div>
  372. </div>
  373. <div class="qty-breakdown" id="qtyBreakdown"></div>
  374. </div>
  375. </div>
  376. <!-- 软比对 -->
  377. <div class="gate-panel" id="softGate">
  378. <div class="gate-hd">
  379. <span id="softGateTitle">单位 / 品名比对</span>
  380. <span class="tag" id="softGateTag">一致</span>
  381. </div>
  382. <div class="gate-bd">
  383. <div class="gate-row header">
  384. <div>比对项</div>
  385. <div>计划侧</div>
  386. <div>购买证侧</div>
  387. <div>结果</div>
  388. </div>
  389. <div class="gate-row" id="rowSeller">
  390. <div class="col-label">销售单位</div>
  391. <div class="col-plan" id="sellerPlan">—</div>
  392. <div class="col-cert" id="sellerCert">—</div>
  393. <div class="result" id="sellerResult">—</div>
  394. </div>
  395. <div class="gate-row" id="rowBuyer">
  396. <div class="col-label">购买单位</div>
  397. <div class="col-plan" id="buyerPlan">—</div>
  398. <div class="col-cert" id="buyerCert">—</div>
  399. <div class="result" id="buyerResult">—</div>
  400. </div>
  401. <div class="gate-row" id="rowProduct">
  402. <div class="col-label">购买物品</div>
  403. <div class="col-plan" id="productPlan">—</div>
  404. <div class="col-cert" id="productCert">—</div>
  405. <div class="result" id="productResult">—</div>
  406. </div>
  407. </div>
  408. </div>
  409. </div>
  410. </div>
  411. <div class="dialog-ft">
  412. <button type="button" class="btn" id="btnCancel">取消</button>
  413. <button type="button" class="btn btn-primary" id="btnOk">确定</button>
  414. </div>
  415. </div>
  416. </div>
  417. <p class="note">
  418. Demo 说明:装货客户 ↔ 销售单位、送货客户 ↔ 购买单位为示意映射,落地前需业务确认。
  419. 文件路径:<code>docs/fresh-mint-pl-purchase-cert-link-compare-demo.html</code>
  420. </p>
  421. </div>
  422. <div class="toast" id="toast"></div>
  423. <script>
  424. const SCENES = {
  425. ok: {
  426. label: '全部通过',
  427. planNo: 'YS-20261009-018 · 陕A·D8821',
  428. deliveryDate: '2026-10-12',
  429. product: '丙酮',
  430. planQty: 32,
  431. purchaseLabel: '购备〔2026〕0831 · 丙酮 · 余 118',
  432. transportLabel: '运备〔2026〕2104 · 车陕A·D8821 · 余 40',
  433. validFrom: '2026-09-01',
  434. validTo: '2026-12-31',
  435. certTotal: 170,
  436. archivedActual: 20,
  437. occupiedPlan: 32,
  438. loadCustomer: '陕西某某化工有限公司',
  439. deliveryCustomer: '西安某某制药有限公司',
  440. sellerName: '陕西某某化工有限公司',
  441. buyerName: '西安某某制药有限公司',
  442. certProduct: '丙酮',
  443. },
  444. soft: {
  445. label: '单位/品名有出入',
  446. planNo: 'YS-20261009-022 · 陕A·D8821',
  447. deliveryDate: '2026-10-12',
  448. product: '丙酮(工业级)',
  449. planQty: 28,
  450. purchaseLabel: '购备〔2026〕0831 · 丙酮 · 余 118',
  451. transportLabel: '运备〔2026〕2104 · 车陕A·D8821 · 余 40',
  452. validFrom: '2026-09-01',
  453. validTo: '2026-12-31',
  454. certTotal: 170,
  455. archivedActual: 20,
  456. occupiedPlan: 32,
  457. loadCustomer: '陕西某某化工(销售)有限公司',
  458. deliveryCustomer: '西安某某制药股份有限公司',
  459. sellerName: '陕西某某化工有限公司',
  460. buyerName: '西安某某制药有限公司',
  461. certProduct: '丙酮',
  462. },
  463. qty: {
  464. label: '数量超限',
  465. planNo: 'YS-20261009-031 · 陕B·F1908',
  466. deliveryDate: '2026-10-15',
  467. product: '丙酮',
  468. planQty: 90,
  469. purchaseLabel: '购备〔2026〕0831 · 丙酮 · 余 118',
  470. transportLabel: '运备〔2026〕2110 · 车陕B·F1908 · 余 100',
  471. validFrom: '2026-09-01',
  472. validTo: '2026-12-31',
  473. certTotal: 170,
  474. archivedActual: 52,
  475. occupiedPlan: 40,
  476. loadCustomer: '陕西某某化工有限公司',
  477. deliveryCustomer: '西安某某制药有限公司',
  478. sellerName: '陕西某某化工有限公司',
  479. buyerName: '西安某某制药有限公司',
  480. certProduct: '丙酮',
  481. },
  482. date: {
  483. label: '超出有效期',
  484. planNo: 'YS-20261009-040 · 陕A·D8821',
  485. deliveryDate: '2027-01-05',
  486. product: '丙酮',
  487. planQty: 20,
  488. purchaseLabel: '购备〔2026〕0831 · 丙酮 · 余 118',
  489. transportLabel: '运备〔2026〕2104 · 车陕A·D8821 · 余 40',
  490. validFrom: '2026-09-01',
  491. validTo: '2026-12-31',
  492. certTotal: 170,
  493. archivedActual: 20,
  494. occupiedPlan: 32,
  495. loadCustomer: '陕西某某化工有限公司',
  496. deliveryCustomer: '西安某某制药有限公司',
  497. sellerName: '陕西某某化工有限公司',
  498. buyerName: '西安某某制药有限公司',
  499. certProduct: '丙酮',
  500. },
  501. };
  502. function norm(s) {
  503. return String(s || '')
  504. .replace(/\s+/g, '')
  505. .replace(/(/g, '(')
  506. .replace(/)/g, ')')
  507. .toLowerCase();
  508. }
  509. function softMatch(a, b) {
  510. const na = norm(a);
  511. const nb = norm(b);
  512. if (!na || !nb) return false;
  513. if (na === nb) return true;
  514. return na.includes(nb) || nb.includes(na);
  515. }
  516. function setRow(el, state) {
  517. el.classList.remove('match', 'mismatch', 'block');
  518. if (state) el.classList.add(state);
  519. }
  520. function setGate(panel, tag, level, title) {
  521. panel.classList.remove('ok', 'warn', 'danger');
  522. panel.classList.add(level);
  523. tag.className = 'tag ' + (level === 'ok' ? 'ok' : level === 'warn' ? 'warn' : 'danger');
  524. tag.textContent = title;
  525. }
  526. function render(sceneKey) {
  527. const s = SCENES[sceneKey];
  528. const used = s.archivedActual + s.occupiedPlan + s.planQty;
  529. const remainAfter = s.certTotal - used;
  530. const dateOk = s.deliveryDate >= s.validFrom && s.deliveryDate <= s.validTo;
  531. const qtyOk = used <= s.certTotal + 1e-9;
  532. const hardOk = dateOk && qtyOk;
  533. const sellerOk = softMatch(s.loadCustomer, s.sellerName);
  534. const buyerOk = softMatch(s.deliveryCustomer, s.buyerName);
  535. const productOk = softMatch(s.product, s.certProduct);
  536. const softOk = sellerOk && buyerOk && productOk;
  537. document.getElementById('planLabel').textContent = s.planNo;
  538. document.getElementById('planMeta').innerHTML =
  539. '车牌 <strong>' + s.planNo.split('·').slice(1).join('·').trim() + '</strong>;送货日期 <strong>' +
  540. s.deliveryDate + '</strong>;每行易制毒产品须绑定 1 张购买证 + 1 张运输证。';
  541. document.getElementById('prodName').textContent = s.product;
  542. document.getElementById('prodQtyTag').textContent = '计划量 ' + s.planQty + ' 吨';
  543. document.getElementById('purchaseSelect').querySelector('span').textContent = s.purchaseLabel;
  544. document.getElementById('transportSelect').querySelector('span').textContent = s.transportLabel;
  545. // hard
  546. document.getElementById('datePlan').textContent = '送货日 ' + s.deliveryDate;
  547. document.getElementById('dateCert').textContent = s.validFrom + ' ~ ' + s.validTo;
  548. document.getElementById('dateResult').textContent = dateOk ? '通过' : '超出有效期';
  549. setRow(document.getElementById('rowDate'), dateOk ? 'match' : 'block');
  550. document.getElementById('qtyPlan').textContent = '本次 ' + s.planQty + ' 吨';
  551. document.getElementById('qtyCert').textContent = '证面 ' + s.certTotal + ' 吨';
  552. document.getElementById('qtyResult').textContent = qtyOk ? '通过' : '超限';
  553. setRow(document.getElementById('rowQty'), qtyOk ? 'match' : 'block');
  554. document.getElementById('qtyBreakdown').innerHTML =
  555. '<div class="line"><span>已归档实际数量</span><span class="num">' + s.archivedActual + ' 吨</span></div>' +
  556. '<div class="line"><span>已安排已确认未归档计划量</span><span class="num">' + s.occupiedPlan + ' 吨</span></div>' +
  557. '<div class="line"><span>本次计划数量</span><span class="num">' + s.planQty + ' 吨</span></div>' +
  558. '<div class="line total' + (qtyOk ? '' : ' over') + '"><span>合计占用 / 证面</span><span class="num">' +
  559. used + ' / ' + s.certTotal + ' 吨' + (qtyOk ? '(剩余 ' + remainAfter + ')' : '(超 ' + Math.abs(remainAfter) + ')') +
  560. '</span></div>';
  561. setGate(
  562. document.getElementById('hardGate'),
  563. document.getElementById('hardGateTag'),
  564. hardOk ? 'ok' : 'danger',
  565. hardOk ? '通过' : '拦截'
  566. );
  567. document.getElementById('hardGateTitle').textContent = hardOk ? '硬门禁校验 · 通过' : '硬门禁校验 · 不可确认';
  568. // soft
  569. function fillSoft(rowId, planId, certId, resultId, planVal, certVal, ok) {
  570. document.getElementById(planId).textContent = planVal;
  571. document.getElementById(certId).textContent = certVal;
  572. document.getElementById(resultId).textContent = ok ? '一致' : '有出入';
  573. setRow(document.getElementById(rowId), ok ? 'match' : 'mismatch');
  574. }
  575. fillSoft('rowSeller', 'sellerPlan', 'sellerCert', 'sellerResult', s.loadCustomer, s.sellerName, sellerOk);
  576. fillSoft('rowBuyer', 'buyerPlan', 'buyerCert', 'buyerResult', s.deliveryCustomer, s.buyerName, buyerOk);
  577. fillSoft('rowProduct', 'productPlan', 'productCert', 'productResult', s.product, s.certProduct, productOk);
  578. setGate(
  579. document.getElementById('softGate'),
  580. document.getElementById('softGateTag'),
  581. softOk ? 'ok' : 'warn',
  582. softOk ? '一致' : '有出入'
  583. );
  584. document.getElementById('softGateTitle').textContent = softOk
  585. ? '单位 / 品名比对 · 一致'
  586. : '单位 / 品名比对 · 请确认后继续';
  587. const btnOk = document.getElementById('btnOk');
  588. btnOk.disabled = !hardOk;
  589. btnOk.textContent = hardOk
  590. ? (softOk ? '确定' : '确认仍要关联')
  591. : '确定(已拦截)';
  592. btnOk.dataset.softWarn = softOk ? '0' : '1';
  593. btnOk.dataset.hardOk = hardOk ? '1' : '0';
  594. }
  595. function toast(msg) {
  596. const el = document.getElementById('toast');
  597. el.textContent = msg;
  598. el.classList.add('show');
  599. clearTimeout(toast._t);
  600. toast._t = setTimeout(() => el.classList.remove('show'), 2200);
  601. }
  602. document.querySelectorAll('.scenario-tab').forEach((btn) => {
  603. btn.addEventListener('click', () => {
  604. document.querySelectorAll('.scenario-tab').forEach((b) => b.classList.remove('active'));
  605. btn.classList.add('active');
  606. render(btn.dataset.scene);
  607. });
  608. });
  609. document.getElementById('btnCancel').addEventListener('click', () => {
  610. toast('已取消,未保存关联');
  611. });
  612. document.getElementById('btnOk').addEventListener('click', (e) => {
  613. const btn = e.currentTarget;
  614. if (btn.disabled) return;
  615. if (btn.dataset.softWarn === '1') {
  616. toast('已确认差异并保存关联(Demo)');
  617. } else {
  618. toast('关联已保存(Demo)');
  619. }
  620. });
  621. render('ok');
  622. </script>
  623. </body>
  624. </html>