chore: sync remaining local plan, form mapping and graphify cache
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
44a57b86ca
commit
dd4aa734ee
28 files changed
+3215
-5
No files matched your search
@@ -0,0 +1,586 @@
|
||||
<!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;
|
||||
}
|
||||
.slip h3 {
|
||||
margin: 0 0 10px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
.slip .meta {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px 12px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.slip .meta span { color: var(--muted); }
|
||||
.slip table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
.slip th, .slip td {
|
||||
border: 1px solid #94a3b8;
|
||||
padding: 6px 4px;
|
||||
text-align: center;
|
||||
}
|
||||
.slip th { background: #f1f5f9; }
|
||||
.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">
|
||||
演示数据已覆盖:同客户同日可合打、跨客户/跨日拦截、分装单放行。另按现有能力:入库类计划只能打入库单,出库类只能打送(提)货单/分装单。
|
||||
</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', 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',
|
||||
products: [
|
||||
{ name: '亚硫酸钠', model: '液体', qty: 12, unit: '吨' },
|
||||
{ name: '试剂硝酸', model: 'AR', qty: 2, unit: '吨' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '2', planNo: 'PO20260727002', planDate: '2026-07-27', customerName: '安徽云栋化工有限公司',
|
||||
planType: 'inbound',
|
||||
products: [
|
||||
{ name: '次氯酸钠', model: '固体', qty: 8, unit: '吨' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '3', planNo: 'PO20260727003', planDate: '2026-07-27', customerName: '合肥市皖和化工有限公司',
|
||||
planType: 'inbound',
|
||||
products: [
|
||||
{ name: '98% 工业硫酸', model: 'AR', qty: 20, unit: '吨' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '4', planNo: 'PO20260728001', planDate: '2026-07-28', customerName: '安徽云栋化工有限公司',
|
||||
planType: 'inbound',
|
||||
products: [
|
||||
{ name: '试剂氨水', model: 'AR', qty: 5, unit: '吨' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '5', planNo: 'PO20260727011', planDate: '2026-07-27', customerName: '中国长江实业',
|
||||
planType: 'outbound',
|
||||
products: [
|
||||
{ name: 'TEST液碱', model: '31%', qty: 30, unit: '吨' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '6', planNo: 'PO20260727012', planDate: '2026-07-27', customerName: '中国长江实业',
|
||||
planType: 'outbound',
|
||||
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',
|
||||
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 renderPreview(cmd, plans) {
|
||||
const title =
|
||||
cmd === 'inbound' ? '入库单(合打预览)' :
|
||||
cmd === 'delivery' ? '送(提)货单(合打预览)' : '分装单(合打预览)'
|
||||
const customer = plans[0].customerName
|
||||
const date = plans[0].planDate
|
||||
const planNos = plans.map(p => p.planNo).join('、')
|
||||
const rows = []
|
||||
plans.forEach(p => {
|
||||
p.products.forEach(prod => {
|
||||
rows.push({
|
||||
planNo: p.planNo,
|
||||
name: prod.name,
|
||||
model: prod.model,
|
||||
qty: prod.qty,
|
||||
unit: prod.unit
|
||||
})
|
||||
})
|
||||
})
|
||||
const metaExtra = cmd === 'subpack' && !sameCustomerAndDate(plans)
|
||||
? `<div><span>说明:</span>分装单允许跨客户/跨日期合打</div>
|
||||
<div><span>客户(多值):</span>${[...new Set(plans.map(p => p.customerName))].join('、')}</div>
|
||||
<div><span>计划日期(多值):</span>${[...new Set(plans.map(p => p.planDate))].join('、')}</div>`
|
||||
: `<div><span>客户名称:</span>${customer}</div>
|
||||
<div><span>计划日期:</span>${date}</div>`
|
||||
|
||||
document.getElementById('preview').innerHTML = `
|
||||
<h3>${title}</h3>
|
||||
<div class="meta">
|
||||
${metaExtra}
|
||||
<div><span>来源计划:</span>${planNos}</div>
|
||||
<div><span>产品行数:</span>${rows.length}</div>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>单据编号</th>
|
||||
<th>物料名称</th>
|
||||
<th>规格</th>
|
||||
<th>单位</th>
|
||||
<th>数量</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${rows.map(r => `<tr>
|
||||
<td>${r.planNo}</td>
|
||||
<td>${r.name}</td>
|
||||
<td>${r.model}</td>
|
||||
<td>${r.unit}</td>
|
||||
<td>${r.qty}</td>
|
||||
</tr>`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`
|
||||
}
|
||||
|
||||
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()
|
||||
showToast('已预勾选 PO…001 + PO…002(同客户、同计划日期)。可点「打印单据 → 入库单」看合打预览;再勾选 003/004 可看拦截提示。', 'warn')
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+748
@@ -0,0 +1,748 @@
|
||||
<!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 {
|
||||
font-family: SimSun, "Songti SC", serif;
|
||||
color: #111;
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 8px 10px 24px;
|
||||
}
|
||||
.inbound-title {
|
||||
position: relative;
|
||||
min-height: 56px;
|
||||
margin-bottom: 14px;
|
||||
text-align: center;
|
||||
padding: 4px 40px 0;
|
||||
}
|
||||
.inbound-title .co {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 6px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.inbound-title .doc {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 10px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.inbound-info {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 10px;
|
||||
font-size: 15px;
|
||||
}
|
||||
.inbound-info td { padding: 4px 0; vertical-align: middle; }
|
||||
.inbound-info .lab { white-space: nowrap; }
|
||||
.inbound-product {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
font-size: 13px;
|
||||
}
|
||||
.inbound-product th,
|
||||
.inbound-product td {
|
||||
border: 1px solid #333 !important;
|
||||
padding: 8px 4px;
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
}
|
||||
.inbound-product th {
|
||||
font-weight: bold;
|
||||
background: #fff;
|
||||
}
|
||||
.inbound-product .remark {
|
||||
text-align: left;
|
||||
padding-left: 8px;
|
||||
}
|
||||
.inbound-sign {
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
font-size: 13px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.inbound-sign td {
|
||||
padding: 8px 4px;
|
||||
vertical-align: top;
|
||||
}
|
||||
.slip h3 {
|
||||
margin: 0 0 10px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
.slip .meta {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px 12px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.slip .meta span { color: var(--muted); }
|
||||
.slip table.simple {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
.slip table.simple th, .slip table.simple td {
|
||||
border: 1px solid #94a3b8;
|
||||
padding: 6px 4px;
|
||||
text-align: center;
|
||||
}
|
||||
.slip table.simple th { background: #f1f5f9; }
|
||||
.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">
|
||||
演示数据已覆盖:同客户同日可合打、跨客户/跨日拦截、分装单放行。另按现有能力:入库类计划只能打入库单,出库类只能打送(提)货单/分装单。
|
||||
</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', 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',
|
||||
products: [
|
||||
{ name: '亚硫酸钠', model: '液体', qty: 12, unit: '吨' },
|
||||
{ name: '试剂硝酸', model: 'AR', qty: 2, unit: '吨' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '2', planNo: 'PO20260727002', planDate: '2026-07-27', customerName: '安徽云栋化工有限公司',
|
||||
planType: 'inbound',
|
||||
products: [
|
||||
{ name: '次氯酸钠', model: '固体', qty: 8, unit: '吨' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '3', planNo: 'PO20260727003', planDate: '2026-07-27', customerName: '合肥市皖和化工有限公司',
|
||||
planType: 'inbound',
|
||||
products: [
|
||||
{ name: '98% 工业硫酸', model: 'AR', qty: 20, unit: '吨' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '4', planNo: 'PO20260728001', planDate: '2026-07-28', customerName: '安徽云栋化工有限公司',
|
||||
planType: 'inbound',
|
||||
products: [
|
||||
{ name: '试剂氨水', model: 'AR', qty: 5, unit: '吨' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '5', planNo: 'PO20260727011', planDate: '2026-07-27', customerName: '中国长江实业',
|
||||
planType: 'outbound',
|
||||
products: [
|
||||
{ name: 'TEST液碱', model: '31%', qty: 30, unit: '吨' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '6', planNo: 'PO20260727012', planDate: '2026-07-27', customerName: '中国长江实业',
|
||||
planType: 'outbound',
|
||||
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',
|
||||
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 renderInboundSlip(plans) {
|
||||
const customer = plans[0].customerName
|
||||
const dateCn = formatPlanDateChinese(plans[0].planDate)
|
||||
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
|
||||
})
|
||||
})
|
||||
})
|
||||
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="inbound-title">
|
||||
<div class="co">安庆市山峡化工有限公司</div>
|
||||
<div class="doc">入 库 单</div>
|
||||
</div>
|
||||
<table class="inbound-info">
|
||||
<tr>
|
||||
<td style="width:58%"><span class="lab">送货单位:</span>${customer}</td>
|
||||
<td style="width:42%;text-align:right"><span class="lab">日 期:</span>${dateCn}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table class="inbound-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">备注:</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="inbound-sign">
|
||||
<tr>
|
||||
<td>仓库:</td>
|
||||
<td>审核:</td>
|
||||
<td>客户签收:</td>
|
||||
<td>司机/送货人(附车牌):</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function renderPreview(cmd, plans) {
|
||||
if (cmd === 'inbound') {
|
||||
renderInboundSlip(plans)
|
||||
return
|
||||
}
|
||||
|
||||
const title =
|
||||
cmd === 'delivery' ? '送(提)货单(合打预览)' : '分装单(合打预览)'
|
||||
const customer = plans[0].customerName
|
||||
const date = plans[0].planDate
|
||||
const rows = []
|
||||
plans.forEach(p => {
|
||||
p.products.forEach(prod => {
|
||||
rows.push({
|
||||
planNo: p.planNo,
|
||||
name: prod.name,
|
||||
model: prod.model,
|
||||
qty: prod.qty,
|
||||
unit: prod.unit
|
||||
})
|
||||
})
|
||||
})
|
||||
const metaExtra = cmd === 'subpack' && !sameCustomerAndDate(plans)
|
||||
? `<div><span>说明:</span>分装单允许跨客户/跨日期合打</div>
|
||||
<div><span>客户(多值):</span>${[...new Set(plans.map(p => p.customerName))].join('、')}</div>
|
||||
<div><span>计划日期(多值):</span>${[...new Set(plans.map(p => p.planDate))].join('、')}</div>`
|
||||
: `<div><span>客户名称:</span>${customer}</div>
|
||||
<div><span>计划日期:</span>${date}</div>`
|
||||
|
||||
document.getElementById('preview').innerHTML = `
|
||||
<h3>${title}</h3>
|
||||
<div class="meta">
|
||||
${metaExtra}
|
||||
</div>
|
||||
<table class="simple">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>单据编号</th>
|
||||
<th>物料名称</th>
|
||||
<th>规格</th>
|
||||
<th>单位</th>
|
||||
<th>数量</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${rows.map(r => `<tr>
|
||||
<td>${r.planNo}</td>
|
||||
<td>${r.name}</td>
|
||||
<td>${r.model}</td>
|
||||
<td>${r.unit}</td>
|
||||
<td>${r.qty}</td>
|
||||
</tr>`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -0,0 +1,865 @@
|
||||
<!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>(不合并数量);入库单/送(提)货单第一列为「单据编号」填各计划号;分装单第一列为「序号」按行递增。
|
||||
演示数据已覆盖:同客户同日可合打、跨客户/跨日拦截、分装单放行。另按现有能力:入库类计划只能打入库单,出库类只能打送(提)货单/分装单。
|
||||
</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', 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',
|
||||
products: [
|
||||
{ name: '亚硫酸钠', model: '液体', qty: 12, unit: '吨' },
|
||||
{ name: '试剂硝酸', model: 'AR', qty: 2, unit: '吨' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '2', planNo: 'PO20260727002', planDate: '2026-07-27', customerName: '安徽云栋化工有限公司',
|
||||
planType: 'inbound',
|
||||
products: [
|
||||
{ name: '次氯酸钠', model: '固体', qty: 8, unit: '吨' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '3', planNo: 'PO20260727003', planDate: '2026-07-27', customerName: '合肥市皖和化工有限公司',
|
||||
planType: 'inbound',
|
||||
products: [
|
||||
{ name: '98% 工业硫酸', model: 'AR', qty: 20, unit: '吨' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '4', planNo: 'PO20260728001', planDate: '2026-07-28', customerName: '安徽云栋化工有限公司',
|
||||
planType: 'inbound',
|
||||
products: [
|
||||
{ name: '试剂氨水', model: 'AR', qty: 5, unit: '吨' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '5', planNo: 'PO20260727011', planDate: '2026-07-27', customerName: '中国长江实业',
|
||||
planType: 'outbound',
|
||||
products: [
|
||||
{ name: 'TEST液碱', model: '31%', qty: 30, unit: '吨' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '6', planNo: 'PO20260727012', planDate: '2026-07-27', customerName: '中国长江实业',
|
||||
planType: 'outbound',
|
||||
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',
|
||||
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 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">入 库 单</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">日 期:</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">备注:</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 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">送 (提) 货 单</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">日 期:</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">备注:</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 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">分 装 单</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">
|
||||
备注:
|
||||
<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>
|
||||
@@ -0,0 +1,881 @@
|
||||
<!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">入 库 单</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">日 期:</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">送 (提) 货 单</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">日 期:</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">分 装 单</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>
|
||||
Reference in new issue
Block a user