含易制毒计划安排后进入已安排待确认,确认通过后再占合同并生成链接/地磅;新增购买证/运输证主数据、产品行关联、余量校验与 OCR 配置,并停止跟踪 api_key/app.pid。 Co-authored-by: Cursor <cursoragent@cursor.com>
667 lines
27 KiB
HTML
667 lines
27 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>Fresh Mint · 购买证关联运输计划 · 校验与比对 Demo</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||
<style>
|
||
:root {
|
||
--primary: #10b981;
|
||
--primary-dark: #059669;
|
||
--primary-bg: #ecfdf5;
|
||
--text: #1e293b;
|
||
--text-muted: #64748b;
|
||
--border: #d9e8e2;
|
||
--surface: #ffffff;
|
||
--bg-page: #f1f5f9;
|
||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||
--shadow-lg: 0 12px 40px rgba(15, 23, 42, 0.14);
|
||
--radius: 10px;
|
||
--warn: #d97706;
|
||
--warn-bg: #fffbeb;
|
||
--warn-border: #fde68a;
|
||
--danger: #dc2626;
|
||
--danger-bg: #fef2f2;
|
||
--danger-border: #fecaca;
|
||
--ok: #059669;
|
||
--ok-bg: #ecfdf5;
|
||
--ok-border: #a7f3d0;
|
||
}
|
||
|
||
* { box-sizing: border-box; }
|
||
body {
|
||
margin: 0;
|
||
min-height: 100vh;
|
||
font-family: Inter, -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
color: var(--text);
|
||
background: var(--bg-page);
|
||
}
|
||
|
||
.demo-page { max-width: 1100px; margin: 0 auto; padding: 28px 24px 80px; }
|
||
.demo-head { margin-bottom: 18px; }
|
||
.demo-head h1 { margin: 0 0 8px; font-size: 22px; font-weight: 700; letter-spacing: -0.02em; }
|
||
.demo-head h1 span { color: var(--primary); }
|
||
.demo-head p { margin: 0; color: var(--text-muted); max-width: 920px; }
|
||
.meta { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
|
||
.pill {
|
||
display: inline-flex; align-items: center; gap: 6px;
|
||
padding: 4px 10px; border-radius: 999px; font-size: 12px; font-weight: 500;
|
||
background: var(--primary-bg); color: var(--primary-dark); border: 1px solid #a7f3d0;
|
||
}
|
||
.pill.muted { background: #f1f5f9; color: var(--text-muted); border-color: #e2e8f0; }
|
||
.pill.warn { background: var(--warn-bg); color: var(--warn); border-color: var(--warn-border); }
|
||
.pill.danger { background: var(--danger-bg); color: var(--danger); border-color: var(--danger-border); }
|
||
|
||
.brief {
|
||
display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 16px;
|
||
}
|
||
@media (max-width: 900px) { .brief { grid-template-columns: 1fr; } }
|
||
.card {
|
||
background: var(--surface); border: 1px solid var(--border);
|
||
border-radius: var(--radius); padding: 14px 16px; box-shadow: var(--shadow);
|
||
}
|
||
.card h2 {
|
||
margin: 0 0 8px; font-size: 14px; font-weight: 600;
|
||
padding-left: 10px; border-left: 3px solid var(--primary);
|
||
color: var(--primary-dark);
|
||
}
|
||
.card.danger-card h2 { border-left-color: var(--danger); color: var(--danger); }
|
||
.card.warn-card h2 { border-left-color: var(--warn); color: var(--warn); }
|
||
.card ul { margin: 0; padding-left: 18px; color: var(--text-muted); font-size: 13px; }
|
||
.card li { margin: 4px 0; }
|
||
.card li strong { color: var(--text); }
|
||
.formula {
|
||
margin-top: 8px; padding: 8px 10px; border-radius: 8px;
|
||
background: var(--primary-bg); color: var(--primary-dark);
|
||
font-size: 12px; font-weight: 500;
|
||
}
|
||
.formula.warn { background: var(--warn-bg); color: #92400e; }
|
||
.formula.danger { background: var(--danger-bg); color: #991b1b; }
|
||
|
||
.scenario-tabs {
|
||
display: flex; gap: 8px; margin-bottom: 14px; flex-wrap: wrap;
|
||
}
|
||
.scenario-tab {
|
||
appearance: none; border: 1px solid var(--border); background: #fff;
|
||
height: 36px; padding: 0 14px; border-radius: 8px; cursor: pointer;
|
||
font-size: 13px; font-weight: 500; color: var(--text);
|
||
}
|
||
.scenario-tab:hover { border-color: #a7f3d0; color: var(--primary-dark); }
|
||
.scenario-tab.active {
|
||
background: var(--primary-bg); border-color: #a7f3d0; color: var(--primary-dark);
|
||
}
|
||
|
||
/* Dialog shell */
|
||
.stage {
|
||
background: rgba(15, 23, 42, 0.35);
|
||
border-radius: 14px;
|
||
padding: 28px 20px 36px;
|
||
min-height: 640px;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: center;
|
||
}
|
||
.dialog {
|
||
width: min(920px, 100%);
|
||
background: var(--surface);
|
||
border-radius: 12px;
|
||
box-shadow: var(--shadow-lg);
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
max-height: calc(100vh - 120px);
|
||
}
|
||
.dialog-hd {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
padding: 16px 20px; border-bottom: 1px solid var(--border);
|
||
}
|
||
.dialog-hd h3 { margin: 0; font-size: 16px; font-weight: 600; }
|
||
.dialog-hd .x {
|
||
width: 28px; height: 28px; border: none; background: transparent;
|
||
color: var(--text-muted); font-size: 18px; cursor: pointer; border-radius: 6px;
|
||
}
|
||
.dialog-hd .x:hover { background: #f1f5f9; }
|
||
.dialog-bd {
|
||
padding: 16px 20px;
|
||
overflow-y: auto;
|
||
flex: 1;
|
||
}
|
||
.dialog-ft {
|
||
display: flex; justify-content: flex-end; gap: 10px;
|
||
padding: 12px 20px; border-top: 1px solid var(--border);
|
||
background: #fafafa;
|
||
}
|
||
|
||
.btn {
|
||
display: inline-flex; align-items: center; gap: 6px;
|
||
height: 36px; padding: 0 14px; border-radius: 8px;
|
||
font-size: 14px; font-weight: 500; cursor: pointer;
|
||
border: 1px solid var(--border); background: #fff; color: var(--text);
|
||
}
|
||
.btn:hover { border-color: #a7f3d0; }
|
||
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
|
||
.btn-primary {
|
||
background: var(--primary); border-color: var(--primary); color: #fff;
|
||
}
|
||
.btn-primary:hover { background: var(--primary-dark); border-color: var(--primary-dark); }
|
||
.btn-primary:disabled { background: #94a3b8; border-color: #94a3b8; }
|
||
|
||
.field-label {
|
||
display: block; font-size: 11px; color: var(--text-muted);
|
||
margin-bottom: 6px; font-weight: 500;
|
||
}
|
||
.select-like {
|
||
height: 36px; border: 1px solid var(--border); border-radius: 8px;
|
||
padding: 0 12px; display: flex; align-items: center; justify-content: space-between;
|
||
background: #fff; color: var(--text); font-size: 14px;
|
||
}
|
||
.select-like .chev { color: var(--text-muted); font-size: 12px; }
|
||
|
||
.plan-meta {
|
||
margin: 12px 0 14px; padding: 10px 12px; border-radius: 8px;
|
||
background: #f8fafc; border: 1px solid #e2e8f0;
|
||
font-size: 13px; color: var(--text-muted);
|
||
}
|
||
.plan-meta strong { color: var(--text); }
|
||
|
||
.link-row {
|
||
border: 1px solid var(--border);
|
||
border-radius: 10px;
|
||
padding: 12px 14px;
|
||
margin-bottom: 12px;
|
||
background: #fff;
|
||
}
|
||
.link-row-title {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
gap: 10px; margin-bottom: 10px; flex-wrap: wrap;
|
||
}
|
||
.link-row-title .name { font-weight: 600; }
|
||
.tag {
|
||
display: inline-flex; align-items: center;
|
||
height: 22px; padding: 0 8px; border-radius: 4px;
|
||
font-size: 12px; font-weight: 500;
|
||
background: transparent; color: var(--text); border: 1px solid #e2e8f0;
|
||
}
|
||
.tag.ok { background: var(--ok); color: #fff; border-color: var(--ok); }
|
||
.tag.warn { background: var(--warn); color: #fff; border-color: var(--warn); }
|
||
.tag.danger { background: var(--danger); color: #fff; border-color: var(--danger); }
|
||
|
||
.link-row-fields {
|
||
display: grid; grid-template-columns: 1fr 1fr; gap: 10px;
|
||
}
|
||
@media (max-width: 700px) { .link-row-fields { grid-template-columns: 1fr; } }
|
||
|
||
/* Hard gate / soft compare panels */
|
||
.gate-panel {
|
||
margin-top: 12px;
|
||
border-radius: 10px;
|
||
border: 1px solid var(--border);
|
||
overflow: hidden;
|
||
}
|
||
.gate-panel.ok { border-color: var(--ok-border); }
|
||
.gate-panel.warn { border-color: var(--warn-border); }
|
||
.gate-panel.danger { border-color: var(--danger-border); }
|
||
.gate-hd {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
padding: 10px 12px; font-weight: 600; font-size: 13px;
|
||
}
|
||
.gate-panel.ok .gate-hd { background: var(--ok-bg); color: var(--ok); }
|
||
.gate-panel.warn .gate-hd { background: var(--warn-bg); color: var(--warn); }
|
||
.gate-panel.danger .gate-hd { background: var(--danger-bg); color: var(--danger); }
|
||
.gate-bd { padding: 0; }
|
||
.gate-row {
|
||
display: grid;
|
||
grid-template-columns: 110px 1fr 1fr 88px;
|
||
gap: 8px;
|
||
padding: 10px 12px;
|
||
border-top: 1px solid #f1f5f9;
|
||
font-size: 13px;
|
||
align-items: start;
|
||
}
|
||
.gate-row.header {
|
||
background: #f8fafc;
|
||
color: var(--text-muted);
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
text-transform: none;
|
||
border-top: none;
|
||
}
|
||
@media (max-width: 700px) {
|
||
.gate-row { grid-template-columns: 1fr; gap: 4px; }
|
||
.gate-row.header { display: none; }
|
||
}
|
||
.gate-row .col-label { color: var(--text-muted); font-size: 12px; }
|
||
.gate-row .col-plan, .gate-row .col-cert { word-break: break-all; }
|
||
.gate-row.mismatch .col-plan,
|
||
.gate-row.mismatch .col-cert { color: var(--warn); font-weight: 500; }
|
||
.gate-row.block .col-plan,
|
||
.gate-row.block .col-cert { color: var(--danger); font-weight: 500; }
|
||
.gate-row.match .result { color: var(--ok); }
|
||
.gate-row.mismatch .result { color: var(--warn); }
|
||
.gate-row.block .result { color: var(--danger); }
|
||
|
||
.qty-breakdown {
|
||
margin: 10px 12px 12px;
|
||
padding: 10px 12px;
|
||
border-radius: 8px;
|
||
background: #f8fafc;
|
||
border: 1px solid #e2e8f0;
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
}
|
||
.qty-breakdown .line {
|
||
display: flex; justify-content: space-between; gap: 12px; margin: 3px 0;
|
||
}
|
||
.qty-breakdown .line.total {
|
||
margin-top: 8px; padding-top: 8px; border-top: 1px dashed #cbd5e1;
|
||
font-weight: 600; color: var(--text); font-size: 13px;
|
||
}
|
||
.qty-breakdown .line.over { color: var(--danger); }
|
||
.qty-breakdown .num { font-variant-numeric: tabular-nums; color: var(--text); font-weight: 500; }
|
||
|
||
.toast {
|
||
position: fixed; left: 50%; bottom: 28px; transform: translateX(-50%);
|
||
background: #1e293b; color: #fff; padding: 10px 16px; border-radius: 8px;
|
||
font-size: 13px; box-shadow: var(--shadow-lg); opacity: 0; pointer-events: none;
|
||
transition: opacity 0.2s;
|
||
z-index: 50;
|
||
}
|
||
.toast.show { opacity: 1; }
|
||
|
||
.note {
|
||
margin-top: 16px; font-size: 12px; color: var(--text-muted);
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="demo-page">
|
||
<header class="demo-head">
|
||
<h1>购买证关联运输计划 · <span>校验与比对</span> Demo</h1>
|
||
<p>
|
||
关联「已安排待确认」计划时:有效期与数量为硬拦截;销售单位 / 购买单位 / 购买物品与计划字段比对后展示差异,
|
||
由用户确认或取消。切换下方场景查看不同结果态。
|
||
</p>
|
||
<div class="meta">
|
||
<span class="pill">Fresh Mint</span>
|
||
<span class="pill muted">关联弹窗</span>
|
||
<span class="pill warn">软比对 · 可确认</span>
|
||
<span class="pill danger">硬门禁 · 不可确认</span>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="brief">
|
||
<div class="card danger-card">
|
||
<h2>硬门禁(不通过则禁用「确定」)</h2>
|
||
<ul>
|
||
<li><strong>有效期</strong>:本次计划送货/装货日期须落在购买证有效期内</li>
|
||
<li><strong>数量</strong>:已归档实际量 + 已确认未归档计划量 + 本次计划量 ≤ 购买证数量</li>
|
||
</ul>
|
||
<div class="formula danger">
|
||
占用合计 = Σ归档实际量 + Σ已安排已确认未归档计划量 + 本次计划量 ≤ 证面数量
|
||
</div>
|
||
</div>
|
||
<div class="card warn-card">
|
||
<h2>软比对(展示差异,点确定 / 取消)</h2>
|
||
<ul>
|
||
<li><strong>销售单位</strong> ↔ 计划装货客户(示意)</li>
|
||
<li><strong>购买单位或个人</strong> ↔ 计划送货客户(示意)</li>
|
||
<li><strong>购买物品</strong> ↔ 计划产品品名</li>
|
||
</ul>
|
||
<div class="formula warn">
|
||
有出入时仍可点「确定」完成关联(Demo 映射待业务确认)
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="scenario-tabs" role="tablist">
|
||
<button type="button" class="scenario-tab active" data-scene="ok">场景 A · 全部通过</button>
|
||
<button type="button" class="scenario-tab" data-scene="soft">场景 B · 单位/品名有出入</button>
|
||
<button type="button" class="scenario-tab" data-scene="qty">场景 C · 数量超限</button>
|
||
<button type="button" class="scenario-tab" data-scene="date">场景 D · 超出有效期</button>
|
||
</div>
|
||
|
||
<div class="stage">
|
||
<div class="dialog" id="dialog">
|
||
<div class="dialog-hd">
|
||
<h3>关联待确认计划</h3>
|
||
<button type="button" class="x" aria-label="关闭">×</button>
|
||
</div>
|
||
<div class="dialog-bd">
|
||
<label class="field-label">待确认计划</label>
|
||
<div class="select-like">
|
||
<span id="planLabel">YS-20261009-018 · 陕A·D8821</span>
|
||
<span class="chev">▾</span>
|
||
</div>
|
||
|
||
<div class="plan-meta" id="planMeta">
|
||
车牌 <strong>陕A·D8821</strong>;送货日期 <strong>2026-10-12</strong>;
|
||
每行易制毒产品须绑定 1 张购买证 + 1 张运输证。
|
||
</div>
|
||
|
||
<div class="link-row">
|
||
<div class="link-row-title">
|
||
<span class="name" id="prodName">丙酮</span>
|
||
<span class="tag" id="prodQtyTag">计划量 32 吨</span>
|
||
</div>
|
||
<div class="link-row-fields">
|
||
<div>
|
||
<label class="field-label">购买证</label>
|
||
<div class="select-like" id="purchaseSelect">
|
||
<span>购备〔2026〕0831 · 丙酮 · 余 118</span>
|
||
<span class="chev">▾</span>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label class="field-label">运输证</label>
|
||
<div class="select-like" id="transportSelect">
|
||
<span>运备〔2026〕2104 · 车陕A·D8821 · 余 40</span>
|
||
<span class="chev">▾</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 硬门禁 -->
|
||
<div class="gate-panel" id="hardGate">
|
||
<div class="gate-hd">
|
||
<span id="hardGateTitle">硬门禁校验</span>
|
||
<span class="tag" id="hardGateTag">通过</span>
|
||
</div>
|
||
<div class="gate-bd">
|
||
<div class="gate-row header">
|
||
<div>校验项</div>
|
||
<div>计划侧</div>
|
||
<div>购买证侧</div>
|
||
<div>结果</div>
|
||
</div>
|
||
<div class="gate-row" id="rowDate">
|
||
<div class="col-label">有效期限</div>
|
||
<div class="col-plan" id="datePlan">送货日 2026-10-12</div>
|
||
<div class="col-cert" id="dateCert">2026-09-01 ~ 2026-12-31</div>
|
||
<div class="result" id="dateResult">通过</div>
|
||
</div>
|
||
<div class="gate-row" id="rowQty">
|
||
<div class="col-label">数量占用</div>
|
||
<div class="col-plan" id="qtyPlan">本次 32 吨</div>
|
||
<div class="col-cert" id="qtyCert">证面 170 吨</div>
|
||
<div class="result" id="qtyResult">通过</div>
|
||
</div>
|
||
<div class="qty-breakdown" id="qtyBreakdown"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 软比对 -->
|
||
<div class="gate-panel" id="softGate">
|
||
<div class="gate-hd">
|
||
<span id="softGateTitle">单位 / 品名比对</span>
|
||
<span class="tag" id="softGateTag">一致</span>
|
||
</div>
|
||
<div class="gate-bd">
|
||
<div class="gate-row header">
|
||
<div>比对项</div>
|
||
<div>计划侧</div>
|
||
<div>购买证侧</div>
|
||
<div>结果</div>
|
||
</div>
|
||
<div class="gate-row" id="rowSeller">
|
||
<div class="col-label">销售单位</div>
|
||
<div class="col-plan" id="sellerPlan">—</div>
|
||
<div class="col-cert" id="sellerCert">—</div>
|
||
<div class="result" id="sellerResult">—</div>
|
||
</div>
|
||
<div class="gate-row" id="rowBuyer">
|
||
<div class="col-label">购买单位</div>
|
||
<div class="col-plan" id="buyerPlan">—</div>
|
||
<div class="col-cert" id="buyerCert">—</div>
|
||
<div class="result" id="buyerResult">—</div>
|
||
</div>
|
||
<div class="gate-row" id="rowProduct">
|
||
<div class="col-label">购买物品</div>
|
||
<div class="col-plan" id="productPlan">—</div>
|
||
<div class="col-cert" id="productCert">—</div>
|
||
<div class="result" id="productResult">—</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="dialog-ft">
|
||
<button type="button" class="btn" id="btnCancel">取消</button>
|
||
<button type="button" class="btn btn-primary" id="btnOk">确定</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<p class="note">
|
||
Demo 说明:装货客户 ↔ 销售单位、送货客户 ↔ 购买单位为示意映射,落地前需业务确认。
|
||
文件路径:<code>docs/fresh-mint-pl-purchase-cert-link-compare-demo.html</code>
|
||
</p>
|
||
</div>
|
||
|
||
<div class="toast" id="toast"></div>
|
||
|
||
<script>
|
||
const SCENES = {
|
||
ok: {
|
||
label: '全部通过',
|
||
planNo: 'YS-20261009-018 · 陕A·D8821',
|
||
deliveryDate: '2026-10-12',
|
||
product: '丙酮',
|
||
planQty: 32,
|
||
purchaseLabel: '购备〔2026〕0831 · 丙酮 · 余 118',
|
||
transportLabel: '运备〔2026〕2104 · 车陕A·D8821 · 余 40',
|
||
validFrom: '2026-09-01',
|
||
validTo: '2026-12-31',
|
||
certTotal: 170,
|
||
archivedActual: 20,
|
||
occupiedPlan: 32,
|
||
loadCustomer: '陕西某某化工有限公司',
|
||
deliveryCustomer: '西安某某制药有限公司',
|
||
sellerName: '陕西某某化工有限公司',
|
||
buyerName: '西安某某制药有限公司',
|
||
certProduct: '丙酮',
|
||
},
|
||
soft: {
|
||
label: '单位/品名有出入',
|
||
planNo: 'YS-20261009-022 · 陕A·D8821',
|
||
deliveryDate: '2026-10-12',
|
||
product: '丙酮(工业级)',
|
||
planQty: 28,
|
||
purchaseLabel: '购备〔2026〕0831 · 丙酮 · 余 118',
|
||
transportLabel: '运备〔2026〕2104 · 车陕A·D8821 · 余 40',
|
||
validFrom: '2026-09-01',
|
||
validTo: '2026-12-31',
|
||
certTotal: 170,
|
||
archivedActual: 20,
|
||
occupiedPlan: 32,
|
||
loadCustomer: '陕西某某化工(销售)有限公司',
|
||
deliveryCustomer: '西安某某制药股份有限公司',
|
||
sellerName: '陕西某某化工有限公司',
|
||
buyerName: '西安某某制药有限公司',
|
||
certProduct: '丙酮',
|
||
},
|
||
qty: {
|
||
label: '数量超限',
|
||
planNo: 'YS-20261009-031 · 陕B·F1908',
|
||
deliveryDate: '2026-10-15',
|
||
product: '丙酮',
|
||
planQty: 90,
|
||
purchaseLabel: '购备〔2026〕0831 · 丙酮 · 余 118',
|
||
transportLabel: '运备〔2026〕2110 · 车陕B·F1908 · 余 100',
|
||
validFrom: '2026-09-01',
|
||
validTo: '2026-12-31',
|
||
certTotal: 170,
|
||
archivedActual: 52,
|
||
occupiedPlan: 40,
|
||
loadCustomer: '陕西某某化工有限公司',
|
||
deliveryCustomer: '西安某某制药有限公司',
|
||
sellerName: '陕西某某化工有限公司',
|
||
buyerName: '西安某某制药有限公司',
|
||
certProduct: '丙酮',
|
||
},
|
||
date: {
|
||
label: '超出有效期',
|
||
planNo: 'YS-20261009-040 · 陕A·D8821',
|
||
deliveryDate: '2027-01-05',
|
||
product: '丙酮',
|
||
planQty: 20,
|
||
purchaseLabel: '购备〔2026〕0831 · 丙酮 · 余 118',
|
||
transportLabel: '运备〔2026〕2104 · 车陕A·D8821 · 余 40',
|
||
validFrom: '2026-09-01',
|
||
validTo: '2026-12-31',
|
||
certTotal: 170,
|
||
archivedActual: 20,
|
||
occupiedPlan: 32,
|
||
loadCustomer: '陕西某某化工有限公司',
|
||
deliveryCustomer: '西安某某制药有限公司',
|
||
sellerName: '陕西某某化工有限公司',
|
||
buyerName: '西安某某制药有限公司',
|
||
certProduct: '丙酮',
|
||
},
|
||
};
|
||
|
||
function norm(s) {
|
||
return String(s || '')
|
||
.replace(/\s+/g, '')
|
||
.replace(/(/g, '(')
|
||
.replace(/)/g, ')')
|
||
.toLowerCase();
|
||
}
|
||
|
||
function softMatch(a, b) {
|
||
const na = norm(a);
|
||
const nb = norm(b);
|
||
if (!na || !nb) return false;
|
||
if (na === nb) return true;
|
||
return na.includes(nb) || nb.includes(na);
|
||
}
|
||
|
||
function setRow(el, state) {
|
||
el.classList.remove('match', 'mismatch', 'block');
|
||
if (state) el.classList.add(state);
|
||
}
|
||
|
||
function setGate(panel, tag, level, title) {
|
||
panel.classList.remove('ok', 'warn', 'danger');
|
||
panel.classList.add(level);
|
||
tag.className = 'tag ' + (level === 'ok' ? 'ok' : level === 'warn' ? 'warn' : 'danger');
|
||
tag.textContent = title;
|
||
}
|
||
|
||
function render(sceneKey) {
|
||
const s = SCENES[sceneKey];
|
||
const used = s.archivedActual + s.occupiedPlan + s.planQty;
|
||
const remainAfter = s.certTotal - used;
|
||
const dateOk = s.deliveryDate >= s.validFrom && s.deliveryDate <= s.validTo;
|
||
const qtyOk = used <= s.certTotal + 1e-9;
|
||
const hardOk = dateOk && qtyOk;
|
||
|
||
const sellerOk = softMatch(s.loadCustomer, s.sellerName);
|
||
const buyerOk = softMatch(s.deliveryCustomer, s.buyerName);
|
||
const productOk = softMatch(s.product, s.certProduct);
|
||
const softOk = sellerOk && buyerOk && productOk;
|
||
|
||
document.getElementById('planLabel').textContent = s.planNo;
|
||
document.getElementById('planMeta').innerHTML =
|
||
'车牌 <strong>' + s.planNo.split('·').slice(1).join('·').trim() + '</strong>;送货日期 <strong>' +
|
||
s.deliveryDate + '</strong>;每行易制毒产品须绑定 1 张购买证 + 1 张运输证。';
|
||
document.getElementById('prodName').textContent = s.product;
|
||
document.getElementById('prodQtyTag').textContent = '计划量 ' + s.planQty + ' 吨';
|
||
document.getElementById('purchaseSelect').querySelector('span').textContent = s.purchaseLabel;
|
||
document.getElementById('transportSelect').querySelector('span').textContent = s.transportLabel;
|
||
|
||
// hard
|
||
document.getElementById('datePlan').textContent = '送货日 ' + s.deliveryDate;
|
||
document.getElementById('dateCert').textContent = s.validFrom + ' ~ ' + s.validTo;
|
||
document.getElementById('dateResult').textContent = dateOk ? '通过' : '超出有效期';
|
||
setRow(document.getElementById('rowDate'), dateOk ? 'match' : 'block');
|
||
|
||
document.getElementById('qtyPlan').textContent = '本次 ' + s.planQty + ' 吨';
|
||
document.getElementById('qtyCert').textContent = '证面 ' + s.certTotal + ' 吨';
|
||
document.getElementById('qtyResult').textContent = qtyOk ? '通过' : '超限';
|
||
setRow(document.getElementById('rowQty'), qtyOk ? 'match' : 'block');
|
||
|
||
document.getElementById('qtyBreakdown').innerHTML =
|
||
'<div class="line"><span>已归档实际数量</span><span class="num">' + s.archivedActual + ' 吨</span></div>' +
|
||
'<div class="line"><span>已安排已确认未归档计划量</span><span class="num">' + s.occupiedPlan + ' 吨</span></div>' +
|
||
'<div class="line"><span>本次计划数量</span><span class="num">' + s.planQty + ' 吨</span></div>' +
|
||
'<div class="line total' + (qtyOk ? '' : ' over') + '"><span>合计占用 / 证面</span><span class="num">' +
|
||
used + ' / ' + s.certTotal + ' 吨' + (qtyOk ? '(剩余 ' + remainAfter + ')' : '(超 ' + Math.abs(remainAfter) + ')') +
|
||
'</span></div>';
|
||
|
||
setGate(
|
||
document.getElementById('hardGate'),
|
||
document.getElementById('hardGateTag'),
|
||
hardOk ? 'ok' : 'danger',
|
||
hardOk ? '通过' : '拦截'
|
||
);
|
||
document.getElementById('hardGateTitle').textContent = hardOk ? '硬门禁校验 · 通过' : '硬门禁校验 · 不可确认';
|
||
|
||
// soft
|
||
function fillSoft(rowId, planId, certId, resultId, planVal, certVal, ok) {
|
||
document.getElementById(planId).textContent = planVal;
|
||
document.getElementById(certId).textContent = certVal;
|
||
document.getElementById(resultId).textContent = ok ? '一致' : '有出入';
|
||
setRow(document.getElementById(rowId), ok ? 'match' : 'mismatch');
|
||
}
|
||
fillSoft('rowSeller', 'sellerPlan', 'sellerCert', 'sellerResult', s.loadCustomer, s.sellerName, sellerOk);
|
||
fillSoft('rowBuyer', 'buyerPlan', 'buyerCert', 'buyerResult', s.deliveryCustomer, s.buyerName, buyerOk);
|
||
fillSoft('rowProduct', 'productPlan', 'productCert', 'productResult', s.product, s.certProduct, productOk);
|
||
|
||
setGate(
|
||
document.getElementById('softGate'),
|
||
document.getElementById('softGateTag'),
|
||
softOk ? 'ok' : 'warn',
|
||
softOk ? '一致' : '有出入'
|
||
);
|
||
document.getElementById('softGateTitle').textContent = softOk
|
||
? '单位 / 品名比对 · 一致'
|
||
: '单位 / 品名比对 · 请确认后继续';
|
||
|
||
const btnOk = document.getElementById('btnOk');
|
||
btnOk.disabled = !hardOk;
|
||
btnOk.textContent = hardOk
|
||
? (softOk ? '确定' : '确认仍要关联')
|
||
: '确定(已拦截)';
|
||
btnOk.dataset.softWarn = softOk ? '0' : '1';
|
||
btnOk.dataset.hardOk = hardOk ? '1' : '0';
|
||
}
|
||
|
||
function toast(msg) {
|
||
const el = document.getElementById('toast');
|
||
el.textContent = msg;
|
||
el.classList.add('show');
|
||
clearTimeout(toast._t);
|
||
toast._t = setTimeout(() => el.classList.remove('show'), 2200);
|
||
}
|
||
|
||
document.querySelectorAll('.scenario-tab').forEach((btn) => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.scenario-tab').forEach((b) => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
render(btn.dataset.scene);
|
||
});
|
||
});
|
||
|
||
document.getElementById('btnCancel').addEventListener('click', () => {
|
||
toast('已取消,未保存关联');
|
||
});
|
||
document.getElementById('btnOk').addEventListener('click', (e) => {
|
||
const btn = e.currentTarget;
|
||
if (btn.disabled) return;
|
||
if (btn.dataset.softWarn === '1') {
|
||
toast('已确认差异并保存关联(Demo)');
|
||
} else {
|
||
toast('关联已保存(Demo)');
|
||
}
|
||
});
|
||
|
||
render('ok');
|
||
</script>
|
||
</body>
|
||
</html>
|