Files
shanxia2.0/docs/pl-plan-batch-print-merge-demo.html
T

882 lines
29 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>运输计划 · 多选合打单据 Demo</title>
<style>
:root {
--primary: #10b981;
--primary-dark: #059669;
--primary-bg: #ecfdf5;
--text: #0f172a;
--muted: #64748b;
--border: #e2e8f0;
--warn: #d97706;
--danger: #dc2626;
--card: #fff;
--bg: #f1f5f9;
}
* { box-sizing: border-box; }
body {
margin: 0;
font: 14px/1.5 Inter, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
color: var(--text);
background: var(--bg);
}
.wrap { max-width: 1180px; margin: 0 auto; padding: 24px; }
h1 { font-size: 20px; margin: 0 0 6px; }
.sub { color: var(--muted); margin-bottom: 20px; }
.panel {
background: var(--card);
border: 1px solid var(--border);
border-radius: 10px;
padding: 16px 20px 20px;
margin-bottom: 16px;
}
.panel h2 {
margin: 0 0 12px;
font-size: 15px;
display: flex;
align-items: center;
gap: 8px;
}
.panel h2::before {
content: "";
width: 3px;
height: 14px;
background: var(--primary);
border-radius: 2px;
}
.rules {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 8px;
}
.rule {
border: 1px solid var(--border);
border-radius: 8px;
padding: 10px 12px;
background: #fafafa;
}
.rule strong { display: block; margin-bottom: 4px; }
.rule.ok { border-color: #a7f3d0; background: var(--primary-bg); }
.rule.loose { border-color: #fde68a; background: #fffbeb; }
.toolbar {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border);
}
.selected-tip { color: var(--muted); min-width: 72px; }
.btn {
height: 36px;
padding: 0 14px;
border-radius: 8px;
border: 1px solid var(--border);
background: #fff;
cursor: pointer;
font-size: 14px;
display: inline-flex;
align-items: center;
gap: 6px;
}
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
.btn-main {
background: var(--primary);
border-color: var(--primary);
color: #fff;
}
.dropdown { position: relative; }
.menu {
display: none;
position: absolute;
top: 40px;
left: 0;
z-index: 20;
min-width: 180px;
background: #fff;
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);
padding: 6px;
}
.menu.open { display: block; }
.menu button {
width: 100%;
text-align: left;
border: 0;
background: transparent;
padding: 8px 10px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
}
.menu button:hover:not(:disabled) { background: var(--primary-bg); color: var(--primary-dark); }
.menu button:disabled { color: #94a3b8; cursor: not-allowed; }
.menu .hint {
font-size: 11px;
color: var(--muted);
padding: 0 10px 6px;
line-height: 1.35;
}
table.list {
width: 100%;
border-collapse: collapse;
}
table.list th, table.list td {
padding: 10px 8px;
border-bottom: 1px solid var(--border);
text-align: left;
font-size: 13px;
}
table.list th { color: var(--muted); font-weight: 600; background: #f8fafc; }
table.list tr:hover td { background: #f8fafc; }
table.list tr.picked td { background: var(--primary-bg); }
.tag {
display: inline-block;
padding: 1px 8px;
border-radius: 999px;
font-size: 12px;
background: #e2e8f0;
color: #334155;
}
.tag.in { background: #d1fae5; color: #065f46; }
.tag.out { background: #dbeafe; color: #1e40af; }
.msg {
margin-top: 12px;
padding: 10px 12px;
border-radius: 8px;
font-size: 13px;
display: none;
}
.msg.show { display: block; }
.msg.err { background: #fef2f2; color: var(--danger); border: 1px solid #fecaca; }
.msg.warn { background: #fffbeb; color: #92400e; border: 1px solid #fde68a; }
.msg.ok { background: var(--primary-bg); color: #065f46; border: 1px solid #a7f3d0; }
.grid2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
@media (max-width: 900px) {
.rules, .grid2 { grid-template-columns: 1fr; }
}
.slip {
border: 1px solid #cbd5e1;
border-radius: 8px;
padding: 14px;
background: #fff;
min-height: 280px;
overflow: auto;
}
.slip .inbound-like,
.delivery-like,
.subpack-like {
font-family: SimSun, "Songti SC", serif;
color: #111;
max-width: 960px;
margin: 0 auto;
padding: 8px 10px 24px;
}
.slip-title {
position: relative;
min-height: 56px;
margin-bottom: 14px;
text-align: center;
padding: 4px 40px 0;
}
.slip-title .co {
font-size: 22px;
font-weight: 700;
letter-spacing: 6px;
line-height: 1.35;
}
.slip-title .doc {
font-size: 22px;
font-weight: 700;
letter-spacing: 10px;
margin-top: 2px;
}
.slip-info {
width: 100%;
border-collapse: collapse;
margin-bottom: 10px;
font-size: 14px;
}
.slip-info td { padding: 4px 0; vertical-align: middle; }
.slip-info .lab { white-space: nowrap; }
.type-checks {
display: flex;
gap: 12px;
font-size: 14px;
margin-bottom: 4px;
justify-content: flex-start;
}
.type-checks label { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; }
.slip-product {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
font-size: 13px;
}
.slip-product th,
.slip-product td {
border: 1px solid #333 !important;
padding: 8px 4px;
text-align: center;
word-break: break-all;
}
.slip-product th {
font-weight: bold;
background: #fff;
}
.slip-product .remark,
.slip-product .terms {
text-align: left;
padding-left: 8px;
font-size: 12px;
line-height: 1.45;
}
.slip-sign {
width: 100%;
margin-top: 16px;
font-size: 13px;
border-collapse: collapse;
}
.slip-sign td {
padding: 8px 4px;
vertical-align: top;
}
.bucket-checks {
display: inline-flex;
gap: 14px;
margin-left: 12px;
flex-wrap: wrap;
font-size: 13px;
}
.subpack-no {
text-align: right;
font-size: 13px;
margin-bottom: 6px;
color: #333;
}
.empty {
color: var(--muted);
text-align: center;
padding: 48px 12px;
}
.legend { font-size: 12px; color: var(--muted); margin-top: 8px; }
code { background: #f1f5f9; padding: 1px 5px; border-radius: 4px; font-size: 12px; }
</style>
</head>
<body>
<div class="wrap">
<h1>运输计划 · 多选合打单据 Demo</h1>
<p class="sub">
勾选多条计划 →「打印单据」→ 按规则校验 → 右侧预览「产品合并到一张单据」。
本页仅演示交互与合打规则,非最终生产版式。
</p>
<section class="panel">
<h2>合打规则(客户确认稿)</h2>
<div class="rules">
<div class="rule ok">
<strong>入库单</strong>
勾选计划的<strong>客户名称一致</strong>且<strong>计划日期一致</strong>才可合打;产品行合并到一张入库单。
</div>
<div class="rule ok">
<strong>送(提)货单</strong>
同入库单:客户名称 + 计划日期均须一致。
</div>
<div class="rule loose">
<strong>分装单</strong>
<strong>不校验</strong>客户名称、计划日期;勾选即可合打产品到一张分装单。
</div>
</div>
<p class="legend">
合打策略:沿用现网三类单据模版,<strong>只合并物料表格行</strong>(分行列出,不合并数量);
入库/送货「单据编号」= 各计划号;分装「序号」递增。
表头:客户/日期取共有值(入库、送货须一致);<strong>备注</strong>= 各计划备注换行拼接(一行一计划);
<strong>收货联系方式留空</strong>;不填车辆信息。
</p>
</section>
<div class="grid2">
<section class="panel">
<h2>运输计划列表(多选)</h2>
<div class="toolbar">
<span class="selected-tip" id="selCount">已选 0 项</span>
<div class="dropdown">
<button class="btn" id="printBtn" type="button" disabled>
🖨 打印单据 ▾
</button>
<div class="menu" id="printMenu">
<div class="hint" id="menuHint">请先勾选计划</div>
<button type="button" data-cmd="inbound" disabled>入库单</button>
<button type="button" data-cmd="delivery" disabled>送(提)货单</button>
<button type="button" data-cmd="subpack" disabled>分装单</button>
</div>
</div>
<button class="btn" type="button" id="clearBtn">清空选择</button>
</div>
<table class="list">
<thead>
<tr>
<th style="width:36px"><input type="checkbox" id="checkAll" title="全选当前页" /></th>
<th>计划号</th>
<th>计划日期</th>
<th>客户名称</th>
<th>类型</th>
<th>产品摘要</th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>
<div class="msg" id="toast"></div>
</section>
<section class="panel">
<h2>合打预览</h2>
<div class="slip" id="preview">
<div class="empty">勾选计划并选择打印类型后,在此预览合并产品明细</div>
</div>
</section>
</div>
</div>
<script>
/** @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 */
/** @type {Plan[]} */
const PLANS = [
{
id: '1', planNo: 'PO20260727001', planDate: '2026-07-27', customerName: '安徽云栋化工有限公司',
planType: 'inbound', remark: '上午到厂,注意卸货安全',
products: [
{ name: '亚硫酸钠', model: '液体', qty: 12, unit: '吨' },
{ name: '试剂硝酸', model: 'AR', qty: 2, unit: '吨' }
]
},
{
id: '2', planNo: 'PO20260727002', planDate: '2026-07-27', customerName: '安徽云栋化工有限公司',
planType: 'inbound', remark: '附带质检报告',
products: [
{ name: '次氯酸钠', model: '固体', qty: 8, unit: '吨' }
]
},
{
id: '3', planNo: 'PO20260727003', planDate: '2026-07-27', customerName: '合肥市皖和化工有限公司',
planType: 'inbound', remark: '',
products: [
{ name: '98% 工业硫酸', model: 'AR', qty: 20, unit: '吨' }
]
},
{
id: '4', planNo: 'PO20260728001', planDate: '2026-07-28', customerName: '安徽云栋化工有限公司',
planType: 'inbound', remark: '改期后计划',
products: [
{ name: '试剂氨水', model: 'AR', qty: 5, unit: '吨' }
]
},
{
id: '5', planNo: 'PO20260727011', planDate: '2026-07-27', customerName: '中国长江实业',
planType: 'outbound', remark: '客户自提点卸货',
products: [
{ name: 'TEST液碱', model: '31%', qty: 30, unit: '吨' }
]
},
{
id: '6', planNo: 'PO20260727012', planDate: '2026-07-27', customerName: '中国长江实业',
planType: 'outbound', remark: '需带防护用品',
products: [
{ name: 'TEST盐酸', model: '30%', qty: 15, unit: '吨' },
{ name: 'TEST液碱', model: '31%', qty: 10, unit: '吨' }
]
},
{
id: '7', planNo: 'PO20260729001', planDate: '2026-07-29', customerName: '中国长江实业',
planType: 'outbound', remark: '',
products: [
{ name: 'TEST液碱', model: '31%', qty: 6, unit: '吨' }
]
}
]
const selected = new Set()
let menuOpen = false
function typeLabel(t) {
return t === 'inbound' ? '入库单' : t === 'outbound' ? '出库单' : '直销单'
}
function typeClass(t) {
return t === 'inbound' ? 'in' : t === 'outbound' ? 'out' : ''
}
function printableCommands(planType) {
if (planType === 'inbound') return ['inbound']
if (planType === 'outbound') return ['delivery', 'subpack']
return []
}
function selectedPlans() {
return PLANS.filter(p => selected.has(p.id))
}
function sameCustomerAndDate(plans) {
if (!plans.length) return false
const c = plans[0].customerName
const d = plans[0].planDate
return plans.every(p => p.customerName === c && p.planDate === d)
}
function canMerge(cmd, plans) {
if (plans.length < 1) return { ok: false, reason: '请至少勾选 1 条运输计划' }
if (plans.length === 1) {
const allow = printableCommands(plans[0].planType)
if (!allow.includes(cmd)) {
return { ok: false, reason: `当前单据类型(${typeLabel(plans[0].planType)})不支持打印该单据` }
}
return { ok: true, reason: '' }
}
// multi
if (cmd === 'subpack') {
const bad = plans.filter(p => !printableCommands(p.planType).includes('subpack'))
if (bad.length) {
return { ok: false, reason: `分装单仅支持出库类计划;以下不可打:${bad.map(p => p.planNo).join('、')}` }
}
return { ok: true, reason: '' }
}
if (cmd === 'inbound') {
const bad = plans.filter(p => p.planType !== 'inbound')
if (bad.length) {
return { ok: false, reason: `入库单仅支持入库类计划;冲突:${bad.map(p => p.planNo).join('、')}` }
}
if (!sameCustomerAndDate(plans)) {
return { ok: false, reason: '合打入库单要求:客户名称一致 且 计划日期一致' }
}
return { ok: true, reason: '' }
}
if (cmd === 'delivery') {
const bad = plans.filter(p => p.planType !== 'outbound')
if (bad.length) {
return { ok: false, reason: `送(提)货单仅支持出库类计划;冲突:${bad.map(p => p.planNo).join('、')}` }
}
if (!sameCustomerAndDate(plans)) {
return { ok: false, reason: '合打送(提)货单要求:客户名称一致 且 计划日期一致' }
}
return { ok: true, reason: '' }
}
return { ok: false, reason: '未知单据类型' }
}
function renderList() {
const tb = document.getElementById('tbody')
tb.innerHTML = PLANS.map(p => {
const checked = selected.has(p.id) ? 'checked' : ''
const picked = selected.has(p.id) ? 'picked' : ''
const prod = p.products.map(x => `${x.name}×${x.qty}`).join(',')
return `<tr class="${picked}" data-id="${p.id}">
<td><input type="checkbox" class="row-check" data-id="${p.id}" ${checked} /></td>
<td>${p.planNo}</td>
<td>${p.planDate}</td>
<td>${p.customerName}</td>
<td><span class="tag ${typeClass(p.planType)}">${typeLabel(p.planType)}</span></td>
<td>${prod}</td>
</tr>`
}).join('')
document.getElementById('selCount').textContent = `已选 ${selected.size} 项`
document.getElementById('printBtn').disabled = selected.size === 0
document.getElementById('checkAll').checked = selected.size === PLANS.length && PLANS.length > 0
updateMenuState()
}
function updateMenuState() {
const plans = selectedPlans()
const hint = document.getElementById('menuHint')
const cmds = [
['inbound', '入库单'],
['delivery', '送(提)货单'],
['subpack', '分装单']
]
cmds.forEach(([cmd]) => {
const btn = document.querySelector(`#printMenu button[data-cmd="${cmd}"]`)
const r = canMerge(cmd, plans)
btn.disabled = !r.ok
btn.title = r.ok ? '可合打' : r.reason
})
if (!plans.length) {
hint.textContent = '请先勾选计划'
} else if (plans.length === 1) {
hint.textContent = '单条:沿用原打印能力'
} else {
const okAny = cmds.some(([c]) => canMerge(c, plans).ok)
hint.textContent = okAny
? `已选 ${plans.length} 条 · 灰色项表示不满足合打条件`
: `已选 ${plans.length} 条 · 当前选择不满足任一合打条件`
}
}
function showToast(text, type) {
const el = document.getElementById('toast')
el.className = 'msg show ' + type
el.textContent = text
}
function formatPlanDateChinese(ymd) {
if (!ymd) return ''
const [y, m, d] = ymd.split('-')
return `${Number(y)}年${Number(m)}月${Number(d)}日`
}
function productNoStub(name) {
// demo only
const map = {
'亚硫酸钠': 'WL-001',
'试剂硝酸': 'WL-002',
'次氯酸钠': 'WL-003',
'98% 工业硫酸': 'WL-004',
'试剂氨水': 'WL-005',
'TEST液碱': 'WL-101',
'TEST盐酸': 'WL-102'
}
return map[name] || '—'
}
function mergePlanRemarks(plans) {
// 一行一个计划备注;空备注跳过;前缀计划号便于对照
return plans
.map(p => {
const text = (p.remark || '').trim()
if (!text) return ''
return `${p.planNo}:${text}`
})
.filter(Boolean)
.join('<br />')
}
function collectProductRows(plans) {
const rows = []
plans.forEach(p => {
p.products.forEach(prod => {
rows.push({
planNo: p.planNo,
productNo: productNoStub(prod.name),
name: prod.name,
model: prod.model,
qty: prod.qty,
unit: prod.unit
})
})
})
return rows
}
function renderInboundSlip(plans) {
const customer = plans[0].customerName
const dateCn = formatPlanDateChinese(plans[0].planDate)
const rows = collectProductRows(plans)
const body = rows.map(r => `<tr>
<td>${r.planNo}</td>
<td>${r.productNo}</td>
<td>${r.name}</td>
<td>${r.model}</td>
<td>${r.unit}</td>
<td>${r.qty}</td>
<td></td>
<td></td>
</tr>`).join('')
document.getElementById('preview').innerHTML = `
<div class="inbound-like">
<div class="slip-title">
<div class="co">安庆市山峡化工有限公司</div>
<div class="doc">入&nbsp;&nbsp;库&nbsp;&nbsp;单</div>
</div>
<table class="slip-info">
<tr>
<td style="width:58%"><span class="lab">送货单位:</span>${customer}</td>
<td style="width:42%;text-align:right"><span class="lab">日&nbsp;期:</span>${dateCn}</td>
</tr>
</table>
<table class="slip-product">
<colgroup>
<col style="width:14%" /><col style="width:14%" /><col style="width:20%" />
<col style="width:8%" /><col style="width:6%" /><col style="width:10%" />
<col style="width:14%" /><col style="width:14%" />
</colgroup>
<thead>
<tr>
<th>单据编号</th>
<th>物料编号</th>
<th>物料名称</th>
<th>规格</th>
<th>单位</th>
<th>数量</th>
<th>产品规格</th>
<th>实收数量</th>
</tr>
</thead>
<tbody>
${body}
<tr><td colspan="8" class="remark">备注:${mergePlanRemarks(plans) || ''}</td></tr>
</tbody>
</table>
<table class="slip-sign">
<tr>
<td>仓库:</td>
<td>审核:</td>
<td>客户签收:</td>
<td>司机/送货人(附车牌):</td>
</tr>
</table>
</div>
`
}
function renderDeliverySlip(plans) {
const customer = plans[0].customerName
const dateCn = formatPlanDateChinese(plans[0].planDate)
const rows = collectProductRows(plans)
const remarkHtml = mergePlanRemarks(plans)
const body = rows.map(r => `<tr>
<td>${r.planNo}</td>
<td>${r.productNo}</td>
<td>${r.name}</td>
<td>${r.model}</td>
<td>${r.unit}</td>
<td>${r.qty}</td>
<td></td>
<td></td>
</tr>`).join('')
document.getElementById('preview').innerHTML = `
<div class="delivery-like">
<div class="slip-title">
<div class="co">安庆市山峡化工有限公司</div>
<div class="doc">送&nbsp;(提)&nbsp;货&nbsp;单</div>
</div>
<table class="slip-info">
<tr>
<td style="width:34%"><span class="lab">收货单位:</span>${customer}</td>
<td style="width:33%"><span class="lab">收货联系方式:</span></td>
<td style="width:33%;text-align:right;vertical-align:top;">
<div class="type-checks">
<label><input type="checkbox" />自提</label>
<label><input type="checkbox" checked />出库</label>
<label><input type="checkbox" />直销</label>
</div>
<div><span class="lab">日&nbsp;期:</span>${dateCn}</div>
</td>
</tr>
</table>
<table class="slip-product">
<colgroup>
<col style="width:14%" /><col style="width:14%" /><col style="width:20%" />
<col style="width:8%" /><col style="width:6%" /><col style="width:10%" />
<col style="width:14%" /><col style="width:14%" />
</colgroup>
<thead>
<tr>
<th>单据编号</th>
<th>物料编号</th>
<th>物料名称</th>
<th>规格</th>
<th>单位</th>
<th>数量</th>
<th>产品规格</th>
<th>实收数量</th>
</tr>
</thead>
<tbody>
${body}
<tr><td colspan="8" class="remark">备注:${remarkHtml || ''}</td></tr>
<tr>
<td colspan="5" class="terms">
1.本送货单如购销双方未签订合同,视为有效;<br />
2.本次送货含桶____个,回收____个,结存____个;
</td>
<td colspan="3" class="remark" style="font-size:11px;line-height:1.5;">
地址:安庆市高新区霞虹路5号<br />
送货单位固话:0556-5999710<br />
联系人电话:方总13395560066
</td>
</tr>
</tbody>
</table>
<table class="slip-sign">
<tr>
<td>仓库:</td>
<td>审核:</td>
<td>客户签收:</td>
<td>司机/送货人(附车牌):</td>
</tr>
</table>
</div>
`
}
function renderSubpackSlip(plans) {
// 分装单允许跨客户/跨日期:沿用现网模版字段;仅扩展物料表行
const first = plans[0]
const orderDate = formatPlanDateChinese(first.planDate)
const rows = collectProductRows(plans)
const remarkHtml = mergePlanRemarks(plans)
const body = rows.map((r, idx) => `<tr>
<td>${idx + 1}</td>
<td>${r.name}</td>
<td>${r.model}</td>
<td></td>
<td>${r.unit}</td>
<td>${r.qty}</td>
<td><input type="checkbox" /></td>
</tr>`).join('')
const slipNo = `SXFZ${first.planDate.replace(/-/g, '')}-001`
document.getElementById('preview').innerHTML = `
<div class="subpack-like">
<div class="slip-title">
<div class="co">安庆市山峡化工有限公司</div>
<div class="doc">分&nbsp;装&nbsp;单</div>
</div>
<div class="subpack-no">${slipNo}</div>
<table class="slip-info">
<tr>
<td><span class="lab">下单日期:</span>${orderDate}</td>
<td><span class="lab">交货日期:</span>${orderDate}</td>
<td><span class="lab">下单人:</span></td>
<td><span class="lab">经手人:</span></td>
</tr>
</table>
<table class="slip-product">
<colgroup>
<col style="width:8%" /><col style="width:22%" /><col style="width:16%" />
<col style="width:16%" /><col style="width:10%" /><col style="width:12%" />
<col style="width:16%" />
</colgroup>
<thead>
<tr>
<th>序号</th>
<th>产品名称</th>
<th>产品型号</th>
<th>包装规格</th>
<th>单位</th>
<th>数量</th>
<th>是否完成</th>
</tr>
</thead>
<tbody>
${body}
<tr>
<td colspan="7" class="remark">
备注:${remarkHtml || ''}
<span class="bucket-checks">
<label><input type="checkbox" />全新桶</label>
<label><input type="checkbox" />是否回收桶</label>
<label><input type="checkbox" />桶循环</label>
</span>
</td>
</tr>
</tbody>
</table>
<table class="slip-sign">
<tr>
<td>仓库:</td>
<td>审核:</td>
<td>客户签收:</td>
<td>司机/送货人(附车牌):</td>
</tr>
</table>
</div>
`
}
function renderPreview(cmd, plans) {
if (cmd === 'inbound') {
renderInboundSlip(plans)
return
}
if (cmd === 'delivery') {
renderDeliverySlip(plans)
return
}
renderSubpackSlip(plans)
}
function tryPrint(cmd) {
const plans = selectedPlans()
const r = canMerge(cmd, plans)
menuOpen = false
document.getElementById('printMenu').classList.remove('open')
if (!r.ok) {
showToast(r.reason, 'err')
document.getElementById('preview').innerHTML = `<div class="empty">${r.reason}</div>`
return
}
const label = cmd === 'inbound' ? '入库单' : cmd === 'delivery' ? '送(提)货单' : '分装单'
showToast(
plans.length === 1
? `将打开单计划「${label}」打印(与现网一致)`
: `将合并 ${plans.length} 条计划的产品到一张「${label}」`,
'ok'
)
renderPreview(cmd, plans)
}
document.getElementById('tbody').addEventListener('change', (e) => {
const t = e.target
if (!(t instanceof HTMLInputElement) || !t.classList.contains('row-check')) return
if (t.checked) selected.add(t.dataset.id)
else selected.delete(t.dataset.id)
renderList()
document.getElementById('toast').classList.remove('show')
})
document.getElementById('checkAll').addEventListener('change', (e) => {
const on = e.target.checked
selected.clear()
if (on) PLANS.forEach(p => selected.add(p.id))
renderList()
})
document.getElementById('clearBtn').addEventListener('click', () => {
selected.clear()
renderList()
document.getElementById('preview').innerHTML = '<div class="empty">勾选计划并选择打印类型后,在此预览合并产品明细</div>'
document.getElementById('toast').classList.remove('show')
})
document.getElementById('printBtn').addEventListener('click', (e) => {
e.stopPropagation()
menuOpen = !menuOpen
document.getElementById('printMenu').classList.toggle('open', menuOpen)
updateMenuState()
})
document.getElementById('printMenu').addEventListener('click', (e) => {
const btn = e.target.closest('button[data-cmd]')
if (!btn || btn.disabled) return
tryPrint(btn.dataset.cmd)
})
document.addEventListener('click', () => {
if (!menuOpen) return
menuOpen = false
document.getElementById('printMenu').classList.remove('open')
})
// 预设:勾选同客户同日的两条入库计划,方便演示合打
selected.add('1')
selected.add('2')
renderList()
tryPrint('inbound')
showToast('已预勾选同客户同日两条入库计划,并按现网入库单版式展示合打预览(无「来源计划」「产品行数」)。', 'warn')
</script>
</body>
</html>