pl-plan-batch-print-merge-demo.html 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881
  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. --primary: #10b981;
  10. --primary-dark: #059669;
  11. --primary-bg: #ecfdf5;
  12. --text: #0f172a;
  13. --muted: #64748b;
  14. --border: #e2e8f0;
  15. --warn: #d97706;
  16. --danger: #dc2626;
  17. --card: #fff;
  18. --bg: #f1f5f9;
  19. }
  20. * { box-sizing: border-box; }
  21. body {
  22. margin: 0;
  23. font: 14px/1.5 Inter, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
  24. color: var(--text);
  25. background: var(--bg);
  26. }
  27. .wrap { max-width: 1180px; margin: 0 auto; padding: 24px; }
  28. h1 { font-size: 20px; margin: 0 0 6px; }
  29. .sub { color: var(--muted); margin-bottom: 20px; }
  30. .panel {
  31. background: var(--card);
  32. border: 1px solid var(--border);
  33. border-radius: 10px;
  34. padding: 16px 20px 20px;
  35. margin-bottom: 16px;
  36. }
  37. .panel h2 {
  38. margin: 0 0 12px;
  39. font-size: 15px;
  40. display: flex;
  41. align-items: center;
  42. gap: 8px;
  43. }
  44. .panel h2::before {
  45. content: "";
  46. width: 3px;
  47. height: 14px;
  48. background: var(--primary);
  49. border-radius: 2px;
  50. }
  51. .rules {
  52. display: grid;
  53. grid-template-columns: repeat(3, 1fr);
  54. gap: 10px;
  55. margin-bottom: 8px;
  56. }
  57. .rule {
  58. border: 1px solid var(--border);
  59. border-radius: 8px;
  60. padding: 10px 12px;
  61. background: #fafafa;
  62. }
  63. .rule strong { display: block; margin-bottom: 4px; }
  64. .rule.ok { border-color: #a7f3d0; background: var(--primary-bg); }
  65. .rule.loose { border-color: #fde68a; background: #fffbeb; }
  66. .toolbar {
  67. display: flex;
  68. align-items: center;
  69. gap: 10px;
  70. flex-wrap: wrap;
  71. margin-bottom: 12px;
  72. padding-bottom: 12px;
  73. border-bottom: 1px solid var(--border);
  74. }
  75. .selected-tip { color: var(--muted); min-width: 72px; }
  76. .btn {
  77. height: 36px;
  78. padding: 0 14px;
  79. border-radius: 8px;
  80. border: 1px solid var(--border);
  81. background: #fff;
  82. cursor: pointer;
  83. font-size: 14px;
  84. display: inline-flex;
  85. align-items: center;
  86. gap: 6px;
  87. }
  88. .btn:disabled { opacity: 0.45; cursor: not-allowed; }
  89. .btn-main {
  90. background: var(--primary);
  91. border-color: var(--primary);
  92. color: #fff;
  93. }
  94. .dropdown { position: relative; }
  95. .menu {
  96. display: none;
  97. position: absolute;
  98. top: 40px;
  99. left: 0;
  100. z-index: 20;
  101. min-width: 180px;
  102. background: #fff;
  103. border: 1px solid var(--border);
  104. border-radius: 8px;
  105. box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);
  106. padding: 6px;
  107. }
  108. .menu.open { display: block; }
  109. .menu button {
  110. width: 100%;
  111. text-align: left;
  112. border: 0;
  113. background: transparent;
  114. padding: 8px 10px;
  115. border-radius: 6px;
  116. cursor: pointer;
  117. font-size: 13px;
  118. }
  119. .menu button:hover:not(:disabled) { background: var(--primary-bg); color: var(--primary-dark); }
  120. .menu button:disabled { color: #94a3b8; cursor: not-allowed; }
  121. .menu .hint {
  122. font-size: 11px;
  123. color: var(--muted);
  124. padding: 0 10px 6px;
  125. line-height: 1.35;
  126. }
  127. table.list {
  128. width: 100%;
  129. border-collapse: collapse;
  130. }
  131. table.list th, table.list td {
  132. padding: 10px 8px;
  133. border-bottom: 1px solid var(--border);
  134. text-align: left;
  135. font-size: 13px;
  136. }
  137. table.list th { color: var(--muted); font-weight: 600; background: #f8fafc; }
  138. table.list tr:hover td { background: #f8fafc; }
  139. table.list tr.picked td { background: var(--primary-bg); }
  140. .tag {
  141. display: inline-block;
  142. padding: 1px 8px;
  143. border-radius: 999px;
  144. font-size: 12px;
  145. background: #e2e8f0;
  146. color: #334155;
  147. }
  148. .tag.in { background: #d1fae5; color: #065f46; }
  149. .tag.out { background: #dbeafe; color: #1e40af; }
  150. .msg {
  151. margin-top: 12px;
  152. padding: 10px 12px;
  153. border-radius: 8px;
  154. font-size: 13px;
  155. display: none;
  156. }
  157. .msg.show { display: block; }
  158. .msg.err { background: #fef2f2; color: var(--danger); border: 1px solid #fecaca; }
  159. .msg.warn { background: #fffbeb; color: #92400e; border: 1px solid #fde68a; }
  160. .msg.ok { background: var(--primary-bg); color: #065f46; border: 1px solid #a7f3d0; }
  161. .grid2 {
  162. display: grid;
  163. grid-template-columns: 1fr 1fr;
  164. gap: 16px;
  165. }
  166. @media (max-width: 900px) {
  167. .rules, .grid2 { grid-template-columns: 1fr; }
  168. }
  169. .slip {
  170. border: 1px solid #cbd5e1;
  171. border-radius: 8px;
  172. padding: 14px;
  173. background: #fff;
  174. min-height: 280px;
  175. overflow: auto;
  176. }
  177. .slip .inbound-like,
  178. .delivery-like,
  179. .subpack-like {
  180. font-family: SimSun, "Songti SC", serif;
  181. color: #111;
  182. max-width: 960px;
  183. margin: 0 auto;
  184. padding: 8px 10px 24px;
  185. }
  186. .slip-title {
  187. position: relative;
  188. min-height: 56px;
  189. margin-bottom: 14px;
  190. text-align: center;
  191. padding: 4px 40px 0;
  192. }
  193. .slip-title .co {
  194. font-size: 22px;
  195. font-weight: 700;
  196. letter-spacing: 6px;
  197. line-height: 1.35;
  198. }
  199. .slip-title .doc {
  200. font-size: 22px;
  201. font-weight: 700;
  202. letter-spacing: 10px;
  203. margin-top: 2px;
  204. }
  205. .slip-info {
  206. width: 100%;
  207. border-collapse: collapse;
  208. margin-bottom: 10px;
  209. font-size: 14px;
  210. }
  211. .slip-info td { padding: 4px 0; vertical-align: middle; }
  212. .slip-info .lab { white-space: nowrap; }
  213. .type-checks {
  214. display: flex;
  215. gap: 12px;
  216. font-size: 14px;
  217. margin-bottom: 4px;
  218. justify-content: flex-start;
  219. }
  220. .type-checks label { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; }
  221. .slip-product {
  222. width: 100%;
  223. border-collapse: collapse;
  224. table-layout: fixed;
  225. font-size: 13px;
  226. }
  227. .slip-product th,
  228. .slip-product td {
  229. border: 1px solid #333 !important;
  230. padding: 8px 4px;
  231. text-align: center;
  232. word-break: break-all;
  233. }
  234. .slip-product th {
  235. font-weight: bold;
  236. background: #fff;
  237. }
  238. .slip-product .remark,
  239. .slip-product .terms {
  240. text-align: left;
  241. padding-left: 8px;
  242. font-size: 12px;
  243. line-height: 1.45;
  244. }
  245. .slip-sign {
  246. width: 100%;
  247. margin-top: 16px;
  248. font-size: 13px;
  249. border-collapse: collapse;
  250. }
  251. .slip-sign td {
  252. padding: 8px 4px;
  253. vertical-align: top;
  254. }
  255. .bucket-checks {
  256. display: inline-flex;
  257. gap: 14px;
  258. margin-left: 12px;
  259. flex-wrap: wrap;
  260. font-size: 13px;
  261. }
  262. .subpack-no {
  263. text-align: right;
  264. font-size: 13px;
  265. margin-bottom: 6px;
  266. color: #333;
  267. }
  268. .empty {
  269. color: var(--muted);
  270. text-align: center;
  271. padding: 48px 12px;
  272. }
  273. .legend { font-size: 12px; color: var(--muted); margin-top: 8px; }
  274. code { background: #f1f5f9; padding: 1px 5px; border-radius: 4px; font-size: 12px; }
  275. </style>
  276. </head>
  277. <body>
  278. <div class="wrap">
  279. <h1>运输计划 · 多选合打单据 Demo</h1>
  280. <p class="sub">
  281. 勾选多条计划 →「打印单据」→ 按规则校验 → 右侧预览「产品合并到一张单据」。
  282. 本页仅演示交互与合打规则,非最终生产版式。
  283. </p>
  284. <section class="panel">
  285. <h2>合打规则(客户确认稿)</h2>
  286. <div class="rules">
  287. <div class="rule ok">
  288. <strong>入库单</strong>
  289. 勾选计划的<strong>客户名称一致</strong>且<strong>计划日期一致</strong>才可合打;产品行合并到一张入库单。
  290. </div>
  291. <div class="rule ok">
  292. <strong>送(提)货单</strong>
  293. 同入库单:客户名称 + 计划日期均须一致。
  294. </div>
  295. <div class="rule loose">
  296. <strong>分装单</strong>
  297. <strong>不校验</strong>客户名称、计划日期;勾选即可合打产品到一张分装单。
  298. </div>
  299. </div>
  300. <p class="legend">
  301. 合打策略:沿用现网三类单据模版,<strong>只合并物料表格行</strong>(分行列出,不合并数量);
  302. 入库/送货「单据编号」= 各计划号;分装「序号」递增。
  303. 表头:客户/日期取共有值(入库、送货须一致);<strong>备注</strong>= 各计划备注换行拼接(一行一计划);
  304. <strong>收货联系方式留空</strong>;不填车辆信息。
  305. </p>
  306. </section>
  307. <div class="grid2">
  308. <section class="panel">
  309. <h2>运输计划列表(多选)</h2>
  310. <div class="toolbar">
  311. <span class="selected-tip" id="selCount">已选 0 项</span>
  312. <div class="dropdown">
  313. <button class="btn" id="printBtn" type="button" disabled>
  314. 🖨 打印单据 ▾
  315. </button>
  316. <div class="menu" id="printMenu">
  317. <div class="hint" id="menuHint">请先勾选计划</div>
  318. <button type="button" data-cmd="inbound" disabled>入库单</button>
  319. <button type="button" data-cmd="delivery" disabled>送(提)货单</button>
  320. <button type="button" data-cmd="subpack" disabled>分装单</button>
  321. </div>
  322. </div>
  323. <button class="btn" type="button" id="clearBtn">清空选择</button>
  324. </div>
  325. <table class="list">
  326. <thead>
  327. <tr>
  328. <th style="width:36px"><input type="checkbox" id="checkAll" title="全选当前页" /></th>
  329. <th>计划号</th>
  330. <th>计划日期</th>
  331. <th>客户名称</th>
  332. <th>类型</th>
  333. <th>产品摘要</th>
  334. </tr>
  335. </thead>
  336. <tbody id="tbody"></tbody>
  337. </table>
  338. <div class="msg" id="toast"></div>
  339. </section>
  340. <section class="panel">
  341. <h2>合打预览</h2>
  342. <div class="slip" id="preview">
  343. <div class="empty">勾选计划并选择打印类型后,在此预览合并产品明细</div>
  344. </div>
  345. </section>
  346. </div>
  347. </div>
  348. <script>
  349. /** @typedef {{ id: string, planNo: string, planDate: string, customerName: string, planType: 'inbound'|'outbound'|'direct', remark?: string, products: {name:string, model:string, qty:number, unit:string}[] }} Plan */
  350. /** @type {Plan[]} */
  351. const PLANS = [
  352. {
  353. id: '1', planNo: 'PO20260727001', planDate: '2026-07-27', customerName: '安徽云栋化工有限公司',
  354. planType: 'inbound', remark: '上午到厂,注意卸货安全',
  355. products: [
  356. { name: '亚硫酸钠', model: '液体', qty: 12, unit: '吨' },
  357. { name: '试剂硝酸', model: 'AR', qty: 2, unit: '吨' }
  358. ]
  359. },
  360. {
  361. id: '2', planNo: 'PO20260727002', planDate: '2026-07-27', customerName: '安徽云栋化工有限公司',
  362. planType: 'inbound', remark: '附带质检报告',
  363. products: [
  364. { name: '次氯酸钠', model: '固体', qty: 8, unit: '吨' }
  365. ]
  366. },
  367. {
  368. id: '3', planNo: 'PO20260727003', planDate: '2026-07-27', customerName: '合肥市皖和化工有限公司',
  369. planType: 'inbound', remark: '',
  370. products: [
  371. { name: '98% 工业硫酸', model: 'AR', qty: 20, unit: '吨' }
  372. ]
  373. },
  374. {
  375. id: '4', planNo: 'PO20260728001', planDate: '2026-07-28', customerName: '安徽云栋化工有限公司',
  376. planType: 'inbound', remark: '改期后计划',
  377. products: [
  378. { name: '试剂氨水', model: 'AR', qty: 5, unit: '吨' }
  379. ]
  380. },
  381. {
  382. id: '5', planNo: 'PO20260727011', planDate: '2026-07-27', customerName: '中国长江实业',
  383. planType: 'outbound', remark: '客户自提点卸货',
  384. products: [
  385. { name: 'TEST液碱', model: '31%', qty: 30, unit: '吨' }
  386. ]
  387. },
  388. {
  389. id: '6', planNo: 'PO20260727012', planDate: '2026-07-27', customerName: '中国长江实业',
  390. planType: 'outbound', remark: '需带防护用品',
  391. products: [
  392. { name: 'TEST盐酸', model: '30%', qty: 15, unit: '吨' },
  393. { name: 'TEST液碱', model: '31%', qty: 10, unit: '吨' }
  394. ]
  395. },
  396. {
  397. id: '7', planNo: 'PO20260729001', planDate: '2026-07-29', customerName: '中国长江实业',
  398. planType: 'outbound', remark: '',
  399. products: [
  400. { name: 'TEST液碱', model: '31%', qty: 6, unit: '吨' }
  401. ]
  402. }
  403. ]
  404. const selected = new Set()
  405. let menuOpen = false
  406. function typeLabel(t) {
  407. return t === 'inbound' ? '入库单' : t === 'outbound' ? '出库单' : '直销单'
  408. }
  409. function typeClass(t) {
  410. return t === 'inbound' ? 'in' : t === 'outbound' ? 'out' : ''
  411. }
  412. function printableCommands(planType) {
  413. if (planType === 'inbound') return ['inbound']
  414. if (planType === 'outbound') return ['delivery', 'subpack']
  415. return []
  416. }
  417. function selectedPlans() {
  418. return PLANS.filter(p => selected.has(p.id))
  419. }
  420. function sameCustomerAndDate(plans) {
  421. if (!plans.length) return false
  422. const c = plans[0].customerName
  423. const d = plans[0].planDate
  424. return plans.every(p => p.customerName === c && p.planDate === d)
  425. }
  426. function canMerge(cmd, plans) {
  427. if (plans.length < 1) return { ok: false, reason: '请至少勾选 1 条运输计划' }
  428. if (plans.length === 1) {
  429. const allow = printableCommands(plans[0].planType)
  430. if (!allow.includes(cmd)) {
  431. return { ok: false, reason: `当前单据类型(${typeLabel(plans[0].planType)})不支持打印该单据` }
  432. }
  433. return { ok: true, reason: '' }
  434. }
  435. // multi
  436. if (cmd === 'subpack') {
  437. const bad = plans.filter(p => !printableCommands(p.planType).includes('subpack'))
  438. if (bad.length) {
  439. return { ok: false, reason: `分装单仅支持出库类计划;以下不可打:${bad.map(p => p.planNo).join('、')}` }
  440. }
  441. return { ok: true, reason: '' }
  442. }
  443. if (cmd === 'inbound') {
  444. const bad = plans.filter(p => p.planType !== 'inbound')
  445. if (bad.length) {
  446. return { ok: false, reason: `入库单仅支持入库类计划;冲突:${bad.map(p => p.planNo).join('、')}` }
  447. }
  448. if (!sameCustomerAndDate(plans)) {
  449. return { ok: false, reason: '合打入库单要求:客户名称一致 且 计划日期一致' }
  450. }
  451. return { ok: true, reason: '' }
  452. }
  453. if (cmd === 'delivery') {
  454. const bad = plans.filter(p => p.planType !== 'outbound')
  455. if (bad.length) {
  456. return { ok: false, reason: `送(提)货单仅支持出库类计划;冲突:${bad.map(p => p.planNo).join('、')}` }
  457. }
  458. if (!sameCustomerAndDate(plans)) {
  459. return { ok: false, reason: '合打送(提)货单要求:客户名称一致 且 计划日期一致' }
  460. }
  461. return { ok: true, reason: '' }
  462. }
  463. return { ok: false, reason: '未知单据类型' }
  464. }
  465. function renderList() {
  466. const tb = document.getElementById('tbody')
  467. tb.innerHTML = PLANS.map(p => {
  468. const checked = selected.has(p.id) ? 'checked' : ''
  469. const picked = selected.has(p.id) ? 'picked' : ''
  470. const prod = p.products.map(x => `${x.name}×${x.qty}`).join(',')
  471. return `<tr class="${picked}" data-id="${p.id}">
  472. <td><input type="checkbox" class="row-check" data-id="${p.id}" ${checked} /></td>
  473. <td>${p.planNo}</td>
  474. <td>${p.planDate}</td>
  475. <td>${p.customerName}</td>
  476. <td><span class="tag ${typeClass(p.planType)}">${typeLabel(p.planType)}</span></td>
  477. <td>${prod}</td>
  478. </tr>`
  479. }).join('')
  480. document.getElementById('selCount').textContent = `已选 ${selected.size} 项`
  481. document.getElementById('printBtn').disabled = selected.size === 0
  482. document.getElementById('checkAll').checked = selected.size === PLANS.length && PLANS.length > 0
  483. updateMenuState()
  484. }
  485. function updateMenuState() {
  486. const plans = selectedPlans()
  487. const hint = document.getElementById('menuHint')
  488. const cmds = [
  489. ['inbound', '入库单'],
  490. ['delivery', '送(提)货单'],
  491. ['subpack', '分装单']
  492. ]
  493. cmds.forEach(([cmd]) => {
  494. const btn = document.querySelector(`#printMenu button[data-cmd="${cmd}"]`)
  495. const r = canMerge(cmd, plans)
  496. btn.disabled = !r.ok
  497. btn.title = r.ok ? '可合打' : r.reason
  498. })
  499. if (!plans.length) {
  500. hint.textContent = '请先勾选计划'
  501. } else if (plans.length === 1) {
  502. hint.textContent = '单条:沿用原打印能力'
  503. } else {
  504. const okAny = cmds.some(([c]) => canMerge(c, plans).ok)
  505. hint.textContent = okAny
  506. ? `已选 ${plans.length} 条 · 灰色项表示不满足合打条件`
  507. : `已选 ${plans.length} 条 · 当前选择不满足任一合打条件`
  508. }
  509. }
  510. function showToast(text, type) {
  511. const el = document.getElementById('toast')
  512. el.className = 'msg show ' + type
  513. el.textContent = text
  514. }
  515. function formatPlanDateChinese(ymd) {
  516. if (!ymd) return ''
  517. const [y, m, d] = ymd.split('-')
  518. return `${Number(y)}年${Number(m)}月${Number(d)}日`
  519. }
  520. function productNoStub(name) {
  521. // demo only
  522. const map = {
  523. '亚硫酸钠': 'WL-001',
  524. '试剂硝酸': 'WL-002',
  525. '次氯酸钠': 'WL-003',
  526. '98% 工业硫酸': 'WL-004',
  527. '试剂氨水': 'WL-005',
  528. 'TEST液碱': 'WL-101',
  529. 'TEST盐酸': 'WL-102'
  530. }
  531. return map[name] || '—'
  532. }
  533. function mergePlanRemarks(plans) {
  534. // 一行一个计划备注;空备注跳过;前缀计划号便于对照
  535. return plans
  536. .map(p => {
  537. const text = (p.remark || '').trim()
  538. if (!text) return ''
  539. return `${p.planNo}:${text}`
  540. })
  541. .filter(Boolean)
  542. .join('<br />')
  543. }
  544. function collectProductRows(plans) {
  545. const rows = []
  546. plans.forEach(p => {
  547. p.products.forEach(prod => {
  548. rows.push({
  549. planNo: p.planNo,
  550. productNo: productNoStub(prod.name),
  551. name: prod.name,
  552. model: prod.model,
  553. qty: prod.qty,
  554. unit: prod.unit
  555. })
  556. })
  557. })
  558. return rows
  559. }
  560. function renderInboundSlip(plans) {
  561. const customer = plans[0].customerName
  562. const dateCn = formatPlanDateChinese(plans[0].planDate)
  563. const rows = collectProductRows(plans)
  564. const body = rows.map(r => `<tr>
  565. <td>${r.planNo}</td>
  566. <td>${r.productNo}</td>
  567. <td>${r.name}</td>
  568. <td>${r.model}</td>
  569. <td>${r.unit}</td>
  570. <td>${r.qty}</td>
  571. <td></td>
  572. <td></td>
  573. </tr>`).join('')
  574. document.getElementById('preview').innerHTML = `
  575. <div class="inbound-like">
  576. <div class="slip-title">
  577. <div class="co">安庆市山峡化工有限公司</div>
  578. <div class="doc">入&nbsp;&nbsp;库&nbsp;&nbsp;单</div>
  579. </div>
  580. <table class="slip-info">
  581. <tr>
  582. <td style="width:58%"><span class="lab">送货单位:</span>${customer}</td>
  583. <td style="width:42%;text-align:right"><span class="lab">日&nbsp;期:</span>${dateCn}</td>
  584. </tr>
  585. </table>
  586. <table class="slip-product">
  587. <colgroup>
  588. <col style="width:14%" /><col style="width:14%" /><col style="width:20%" />
  589. <col style="width:8%" /><col style="width:6%" /><col style="width:10%" />
  590. <col style="width:14%" /><col style="width:14%" />
  591. </colgroup>
  592. <thead>
  593. <tr>
  594. <th>单据编号</th>
  595. <th>物料编号</th>
  596. <th>物料名称</th>
  597. <th>规格</th>
  598. <th>单位</th>
  599. <th>数量</th>
  600. <th>产品规格</th>
  601. <th>实收数量</th>
  602. </tr>
  603. </thead>
  604. <tbody>
  605. ${body}
  606. <tr><td colspan="8" class="remark">备注:${mergePlanRemarks(plans) || ''}</td></tr>
  607. </tbody>
  608. </table>
  609. <table class="slip-sign">
  610. <tr>
  611. <td>仓库:</td>
  612. <td>审核:</td>
  613. <td>客户签收:</td>
  614. <td>司机/送货人(附车牌):</td>
  615. </tr>
  616. </table>
  617. </div>
  618. `
  619. }
  620. function renderDeliverySlip(plans) {
  621. const customer = plans[0].customerName
  622. const dateCn = formatPlanDateChinese(plans[0].planDate)
  623. const rows = collectProductRows(plans)
  624. const remarkHtml = mergePlanRemarks(plans)
  625. const body = rows.map(r => `<tr>
  626. <td>${r.planNo}</td>
  627. <td>${r.productNo}</td>
  628. <td>${r.name}</td>
  629. <td>${r.model}</td>
  630. <td>${r.unit}</td>
  631. <td>${r.qty}</td>
  632. <td></td>
  633. <td></td>
  634. </tr>`).join('')
  635. document.getElementById('preview').innerHTML = `
  636. <div class="delivery-like">
  637. <div class="slip-title">
  638. <div class="co">安庆市山峡化工有限公司</div>
  639. <div class="doc">送&nbsp;(提)&nbsp;货&nbsp;单</div>
  640. </div>
  641. <table class="slip-info">
  642. <tr>
  643. <td style="width:34%"><span class="lab">收货单位:</span>${customer}</td>
  644. <td style="width:33%"><span class="lab">收货联系方式:</span></td>
  645. <td style="width:33%;text-align:right;vertical-align:top;">
  646. <div class="type-checks">
  647. <label><input type="checkbox" />自提</label>
  648. <label><input type="checkbox" checked />出库</label>
  649. <label><input type="checkbox" />直销</label>
  650. </div>
  651. <div><span class="lab">日&nbsp;期:</span>${dateCn}</div>
  652. </td>
  653. </tr>
  654. </table>
  655. <table class="slip-product">
  656. <colgroup>
  657. <col style="width:14%" /><col style="width:14%" /><col style="width:20%" />
  658. <col style="width:8%" /><col style="width:6%" /><col style="width:10%" />
  659. <col style="width:14%" /><col style="width:14%" />
  660. </colgroup>
  661. <thead>
  662. <tr>
  663. <th>单据编号</th>
  664. <th>物料编号</th>
  665. <th>物料名称</th>
  666. <th>规格</th>
  667. <th>单位</th>
  668. <th>数量</th>
  669. <th>产品规格</th>
  670. <th>实收数量</th>
  671. </tr>
  672. </thead>
  673. <tbody>
  674. ${body}
  675. <tr><td colspan="8" class="remark">备注:${remarkHtml || ''}</td></tr>
  676. <tr>
  677. <td colspan="5" class="terms">
  678. 1.本送货单如购销双方未签订合同,视为有效;<br />
  679. 2.本次送货含桶____个,回收____个,结存____个;
  680. </td>
  681. <td colspan="3" class="remark" style="font-size:11px;line-height:1.5;">
  682. 地址:安庆市高新区霞虹路5号<br />
  683. 送货单位固话:0556-5999710<br />
  684. 联系人电话:方总13395560066
  685. </td>
  686. </tr>
  687. </tbody>
  688. </table>
  689. <table class="slip-sign">
  690. <tr>
  691. <td>仓库:</td>
  692. <td>审核:</td>
  693. <td>客户签收:</td>
  694. <td>司机/送货人(附车牌):</td>
  695. </tr>
  696. </table>
  697. </div>
  698. `
  699. }
  700. function renderSubpackSlip(plans) {
  701. // 分装单允许跨客户/跨日期:沿用现网模版字段;仅扩展物料表行
  702. const first = plans[0]
  703. const orderDate = formatPlanDateChinese(first.planDate)
  704. const rows = collectProductRows(plans)
  705. const remarkHtml = mergePlanRemarks(plans)
  706. const body = rows.map((r, idx) => `<tr>
  707. <td>${idx + 1}</td>
  708. <td>${r.name}</td>
  709. <td>${r.model}</td>
  710. <td></td>
  711. <td>${r.unit}</td>
  712. <td>${r.qty}</td>
  713. <td><input type="checkbox" /></td>
  714. </tr>`).join('')
  715. const slipNo = `SXFZ${first.planDate.replace(/-/g, '')}-001`
  716. document.getElementById('preview').innerHTML = `
  717. <div class="subpack-like">
  718. <div class="slip-title">
  719. <div class="co">安庆市山峡化工有限公司</div>
  720. <div class="doc">分&nbsp;装&nbsp;单</div>
  721. </div>
  722. <div class="subpack-no">${slipNo}</div>
  723. <table class="slip-info">
  724. <tr>
  725. <td><span class="lab">下单日期:</span>${orderDate}</td>
  726. <td><span class="lab">交货日期:</span>${orderDate}</td>
  727. <td><span class="lab">下单人:</span></td>
  728. <td><span class="lab">经手人:</span></td>
  729. </tr>
  730. </table>
  731. <table class="slip-product">
  732. <colgroup>
  733. <col style="width:8%" /><col style="width:22%" /><col style="width:16%" />
  734. <col style="width:16%" /><col style="width:10%" /><col style="width:12%" />
  735. <col style="width:16%" />
  736. </colgroup>
  737. <thead>
  738. <tr>
  739. <th>序号</th>
  740. <th>产品名称</th>
  741. <th>产品型号</th>
  742. <th>包装规格</th>
  743. <th>单位</th>
  744. <th>数量</th>
  745. <th>是否完成</th>
  746. </tr>
  747. </thead>
  748. <tbody>
  749. ${body}
  750. <tr>
  751. <td colspan="7" class="remark">
  752. 备注:${remarkHtml || ''}
  753. <span class="bucket-checks">
  754. <label><input type="checkbox" />全新桶</label>
  755. <label><input type="checkbox" />是否回收桶</label>
  756. <label><input type="checkbox" />桶循环</label>
  757. </span>
  758. </td>
  759. </tr>
  760. </tbody>
  761. </table>
  762. <table class="slip-sign">
  763. <tr>
  764. <td>仓库:</td>
  765. <td>审核:</td>
  766. <td>客户签收:</td>
  767. <td>司机/送货人(附车牌):</td>
  768. </tr>
  769. </table>
  770. </div>
  771. `
  772. }
  773. function renderPreview(cmd, plans) {
  774. if (cmd === 'inbound') {
  775. renderInboundSlip(plans)
  776. return
  777. }
  778. if (cmd === 'delivery') {
  779. renderDeliverySlip(plans)
  780. return
  781. }
  782. renderSubpackSlip(plans)
  783. }
  784. function tryPrint(cmd) {
  785. const plans = selectedPlans()
  786. const r = canMerge(cmd, plans)
  787. menuOpen = false
  788. document.getElementById('printMenu').classList.remove('open')
  789. if (!r.ok) {
  790. showToast(r.reason, 'err')
  791. document.getElementById('preview').innerHTML = `<div class="empty">${r.reason}</div>`
  792. return
  793. }
  794. const label = cmd === 'inbound' ? '入库单' : cmd === 'delivery' ? '送(提)货单' : '分装单'
  795. showToast(
  796. plans.length === 1
  797. ? `将打开单计划「${label}」打印(与现网一致)`
  798. : `将合并 ${plans.length} 条计划的产品到一张「${label}」`,
  799. 'ok'
  800. )
  801. renderPreview(cmd, plans)
  802. }
  803. document.getElementById('tbody').addEventListener('change', (e) => {
  804. const t = e.target
  805. if (!(t instanceof HTMLInputElement) || !t.classList.contains('row-check')) return
  806. if (t.checked) selected.add(t.dataset.id)
  807. else selected.delete(t.dataset.id)
  808. renderList()
  809. document.getElementById('toast').classList.remove('show')
  810. })
  811. document.getElementById('checkAll').addEventListener('change', (e) => {
  812. const on = e.target.checked
  813. selected.clear()
  814. if (on) PLANS.forEach(p => selected.add(p.id))
  815. renderList()
  816. })
  817. document.getElementById('clearBtn').addEventListener('click', () => {
  818. selected.clear()
  819. renderList()
  820. document.getElementById('preview').innerHTML = '<div class="empty">勾选计划并选择打印类型后,在此预览合并产品明细</div>'
  821. document.getElementById('toast').classList.remove('show')
  822. })
  823. document.getElementById('printBtn').addEventListener('click', (e) => {
  824. e.stopPropagation()
  825. menuOpen = !menuOpen
  826. document.getElementById('printMenu').classList.toggle('open', menuOpen)
  827. updateMenuState()
  828. })
  829. document.getElementById('printMenu').addEventListener('click', (e) => {
  830. const btn = e.target.closest('button[data-cmd]')
  831. if (!btn || btn.disabled) return
  832. tryPrint(btn.dataset.cmd)
  833. })
  834. document.addEventListener('click', () => {
  835. if (!menuOpen) return
  836. menuOpen = false
  837. document.getElementById('printMenu').classList.remove('open')
  838. })
  839. // 预设:勾选同客户同日的两条入库计划,方便演示合打
  840. selected.add('1')
  841. selected.add('2')
  842. renderList()
  843. tryPrint('inbound')
  844. showToast('已预勾选同客户同日两条入库计划,并按现网入库单版式展示合打预览(无「来源计划」「产品行数」)。', 'warn')
  845. </script>
  846. </body>
  847. </html>