Files
shanxia2.0/docs/fresh-mint-pl-plan-contract-supplement-dialog-demo.html
T
2026-08-27 19:38:12 +08:00

859 lines
35 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.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;
--table-head: #f8fafc;
--bg-page: #f8fafc;
--row-alt: #fafafa;
--row-hover: #f1f5f9;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
--shadow-lg: 0 12px 40px rgba(15, 23, 42, 0.12);
--radius: 10px;
--warn: #d97706;
--warn-bg: #fffbeb;
--danger: #dc2626;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 14px;
line-height: 1.5;
color: var(--text);
background: var(--bg-page);
}
.demo-page { max-width: 1360px; margin: 0 auto; padding: 28px 24px 80px; }
.demo-head { margin-bottom: 20px; }
.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;
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: rgba(217,119,6,.25); }
.note {
margin: 0 0 16px; padding: 12px 14px; border-radius: 8px;
background: var(--primary-bg); border: 1px solid #6ee7b7; color: var(--primary-dark);
font-size: 13px;
}
.note strong { color: var(--text); }
.panel {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden;
}
.panel-top {
display: flex; align-items: center; justify-content: space-between;
gap: 12px; padding: 16px 20px 12px; border-bottom: 1px solid #eef2f6;
}
.panel-top h2 {
margin: 0; font-size: 16px; font-weight: 600;
display: flex; align-items: center;
}
.panel-top h2::before {
content: ""; display: inline-block; width: 3px; height: 16px;
background: var(--primary); border-radius: 2px; margin-right: 10px;
}
.stat-chip {
display: inline-flex; align-items: center;
padding: 4px 10px; border-radius: 8px; font-size: 12px; font-weight: 600;
background: var(--warn-bg); color: var(--warn); border: 1px solid rgba(217,119,6,.2);
}
.filter-block { padding: 14px 20px 6px; }
.filter-grid {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px 16px;
}
@media (max-width: 1100px) { .filter-grid { grid-template-columns: repeat(2, 1fr); } }
.field label {
display: block; font-size: 11px; color: var(--text-muted); margin-bottom: 4px;
}
.field input, .field select {
width: 100%; height: 36px; border: 1px solid var(--border); border-radius: 8px;
padding: 0 10px; font-size: 14px; color: var(--text); background: #fff;
}
.filter-actions {
display: flex; justify-content: flex-end; gap: 8px; padding: 10px 0 4px;
}
.list-toolbar {
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
padding: 10px 20px; border-top: 1px solid #eef2f6;
}
.date-pills { display: flex; gap: 6px; flex-wrap: wrap; }
.date-pill {
height: 30px; padding: 0 10px; border-radius: 999px; border: 1px solid var(--border);
background: #fff; font-size: 12px; color: var(--text-muted); cursor: pointer;
}
.date-pill.active {
background: var(--primary-bg); border-color: #6ee7b7; color: var(--primary-dark); font-weight: 600;
}
.toolbar-spacer { flex: 1; }
.quick-search {
width: 220px; height: 36px; border: 1px solid var(--border); border-radius: 8px;
padding: 0 12px; font-size: 13px;
}
.btn {
appearance: none; display: inline-flex; align-items: center; gap: 6px;
height: 36px; padding: 0 14px; border-radius: 8px; font-size: 13px; font-weight: 500;
cursor: pointer; border: 1px solid var(--border); background: #fff; color: var(--text);
}
.btn:hover { background: #f8fafc; }
.btn-main { background: var(--primary); border-color: var(--primary); color: #fff; }
.btn-main:hover { background: var(--primary-dark); }
.btn-ghost { background: #fff; }
.btn-link {
height: auto; padding: 0; border: none; background: transparent;
color: var(--primary-dark); font-weight: 500; cursor: pointer; font-size: 13px;
}
.btn-link:hover { text-decoration: underline; }
.btn-link.warn { color: var(--warn); }
.btn-sm { height: 30px; padding: 0 10px; font-size: 12px; }
.btn-icon {
width: 28px; height: 28px; padding: 0; justify-content: center;
border: none; background: transparent; color: var(--text-muted); cursor: pointer;
border-radius: 6px;
}
.btn-icon:hover { background: #f1f5f9; color: var(--primary-dark); }
.btn-icon.danger:hover { color: var(--danger); background: #fef2f2; }
.list-body { border-top: 1px solid #eef2f6; overflow-x: auto; }
table.list-table {
width: 100%; border-collapse: collapse; min-width: 1100px;
}
.list-table th {
background: var(--table-head); text-align: left; font-weight: 600; font-size: 12px;
color: var(--text-muted); padding: 10px 12px; border-bottom: 1px solid #eef2f6;
white-space: nowrap;
}
.list-table td {
padding: 12px; border-bottom: 1px solid #f1f5f9; vertical-align: middle;
font-size: 13px;
}
.list-table tbody tr:hover { background: var(--row-hover); }
.list-table .mono { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--primary-dark); }
.list-table .muted { color: var(--text-muted); }
.list-table .merge-cell { background: #fff; }
.addr-time { display: flex; flex-direction: column; gap: 2px; }
.addr-time .addr { color: var(--text); }
.addr-time .time { font-size: 12px; color: var(--text-muted); }
.tag {
display: inline-flex; align-items: center; padding: 2px 8px;
border-radius: 6px; font-size: 12px; font-weight: 500; white-space: nowrap;
}
.tag.success { background: #059669; color: #fff; }
.tag.warn { background: #d97706; color: #fff; }
.tag.danger { background: #dc2626; color: #fff; }
.tag.info { background: transparent; color: #1e293b; border: 1px solid #e2e8f0; }
.tag.muted { background: #f1f5f9; color: var(--text-muted); }
.tag.internal { background: #059669; color: #fff; margin-left: 6px; }
.gap-cell {
display: flex; flex-direction: column; align-items: flex-start; gap: 6px;
}
.gap-line {
display: flex; align-items: center; gap: 6px; font-size: 12px;
}
.gap-line .side { color: var(--text-muted); min-width: 2.5em; }
.gap-line .pending { color: var(--warn); font-weight: 500; }
.gap-line .ok { color: var(--primary-dark); font-weight: 500; }
.pager {
display: flex; align-items: center; justify-content: flex-end; gap: 12px;
padding: 12px 20px; border-top: 1px solid #eef2f6; color: var(--text-muted); font-size: 13px;
}
/* 弹窗 */
.modal-mask {
display: none; position: fixed; inset: 0; background: rgba(15, 23, 42, 0.45);
z-index: 1000; align-items: center; justify-content: center; padding: 24px;
backdrop-filter: none;
}
.modal-mask.open { display: flex; }
.dialog {
width: min(760px, 100%); max-height: calc(100vh - 100px);
background: #fff; border-radius: 12px; box-shadow: var(--shadow-lg);
display: flex; flex-direction: column; overflow: hidden;
}
.dialog-head {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 20px; border-bottom: 1px solid #eef2f6; flex-shrink: 0;
}
.dialog-head h3 { margin: 0; font-size: 16px; font-weight: 600; }
.dialog-body { padding: 18px 20px; overflow: auto; flex: 1; }
.dialog-foot {
display: flex; justify-content: flex-end; gap: 8px;
padding: 12px 20px; border-top: 1px solid #eef2f6; flex-shrink: 0;
}
.stop-summary {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;
margin-bottom: 16px; padding: 12px 14px; background: #f8fafc; border-radius: 8px;
border: 1px solid #eef2f6;
}
@media (max-width: 700px) { .stop-summary { grid-template-columns: 1fr 1fr; } }
.kv .k { font-size: 11px; color: var(--text-muted); }
.kv .v { font-size: 13px; font-weight: 500; margin-top: 2px; }
.side-box {
background: #fff; border: 1px solid var(--border); border-radius: 8px; padding: 14px;
}
.side-box.need { border-color: rgba(217,119,6,.35); background: #fffdf8; }
.side-box.ok { border-color: #a7f3d0; background: #f6fffb; }
.side-box.exempt { border-color: #e2e8f0; background: #f8fafc; }
.side-box__head {
display: flex; align-items: center; justify-content: space-between;
gap: 8px; margin-bottom: 6px;
}
.side-box__head h4 { margin: 0; font-size: 14px; font-weight: 600; }
.side-box__customer { font-size: 13px; margin-bottom: 12px; color: var(--text); }
.side-box__exempt {
margin: 0; padding: 14px; border: 1px solid #e2e8f0; border-radius: 8px;
background: #fff; color: var(--text-muted); font-size: 13px; text-align: center;
}
.contract-toolbar {
display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px;
}
.contract-hint { font-size: 12px; color: var(--text-muted); }
.contract-hint.is-warn { color: var(--warn); }
.contract-cards { display: flex; flex-direction: column; gap: 10px; }
.contract-card {
border: 1px solid var(--border); border-radius: 8px; background: #fff; overflow: hidden;
}
.contract-card__head {
display: flex; align-items: center; justify-content: space-between; gap: 8px;
padding: 10px 12px; background: #f8fafc; border-bottom: 1px solid #eef2f6;
}
.contract-card__meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.contract-card__no { font-weight: 600; color: var(--primary-dark); font-size: 13px; }
.contract-card__count { font-size: 12px; color: var(--text-muted); }
.contract-card__actions { display: flex; align-items: center; gap: 2px; }
.contract-card__body { padding: 0; }
.mini-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.mini-table th {
background: #fafafa; color: var(--text-muted); font-weight: 500;
padding: 6px 10px; text-align: left; border-bottom: 1px solid #f1f5f9;
}
.mini-table td {
padding: 6px 10px; border-bottom: 1px solid #f8fafc; color: var(--text);
}
.empty-hint {
margin: 0; padding: 18px; text-align: center; color: var(--text-muted);
font-size: 13px; border: 1px dashed #cbd5e1; border-radius: 8px; background: #fff;
}
/* 合同选择器(二级弹层) */
.selector-mask {
display: none; position: fixed; inset: 0; background: rgba(15, 23, 42, 0.4);
z-index: 1100; align-items: center; justify-content: center; padding: 24px;
}
.selector-mask.open { display: flex; }
.selector-dialog {
width: min(640px, 100%); max-height: calc(100vh - 120px);
background: #fff; border-radius: 12px; box-shadow: var(--shadow-lg);
display: flex; flex-direction: column; overflow: hidden;
}
.selector-list { overflow: auto; flex: 1; border-top: 1px solid #eef2f6; }
.selector-item {
display: flex; align-items: flex-start; gap: 10px;
padding: 12px 16px; border-bottom: 1px solid #f1f5f9; cursor: pointer;
}
.selector-item:hover, .selector-item.selected { background: var(--primary-bg); }
.selector-item .meta-line { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
.selector-item input { margin-top: 3px; }
.toast {
position: fixed; top: 20px; left: 50%; transform: translateX(-50%) translateY(-120%);
background: #059669; color: #fff; padding: 10px 18px; border-radius: 8px;
font-size: 13px; font-weight: 500; box-shadow: var(--shadow-lg);
transition: transform .25s ease; z-index: 2000;
}
.toast.show { transform: translateX(-50%) translateY(0); }
.legend {
margin-top: 20px; display: grid; grid-template-columns: 1fr 1fr; gap: 12px;
}
@media (max-width: 800px) { .legend { 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 h3 {
margin: 0 0 8px; font-size: 14px; font-weight: 600;
padding-left: 10px; border-left: 3px solid var(--primary); color: var(--primary-dark);
}
.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); }
</style>
</head>
<body>
<div class="demo-page">
<header class="demo-head">
<h1>运输计划合同完善 · <span>补充弹窗</span></h1>
<p>
对齐当前线上列表(段次行合并):点击合同缺口旁的「补充」打开弹窗,
仅针对<strong>本段客户</strong>关联已有 CRM 合同并保存。不再使用行内展开。
</p>
<div class="meta">
<span class="pill">Fresh Mint · web-admin</span>
<span class="pill warn">点击「补充」体验弹窗</span>
<span class="pill muted">路径 /#/pl/plan-module/plan-contract</span>
</div>
</header>
<p class="note">
<strong>交互变更:</strong>原「行内展开 + 保存本段合同」→ 改为「弹窗补充」。
弹窗内复用合同多选卡片(选择合同 / 查看 / 移除),底栏「取消 / 保存本段合同」。
未保存关闭时二次确认(Demo 用 confirm)。
</p>
<section class="panel">
<div class="panel-top">
<h2>运输计划合同完善</h2>
<span class="stat-chip">待完善 3 条</span>
</div>
<div class="filter-block">
<div class="filter-grid">
<div class="field">
<label>计划编号</label>
<input placeholder="搜索计划编号" />
</div>
<div class="field">
<label>缺口类型</label>
<select>
<option>全部缺口</option>
<option>装货段缺合同</option>
<option>送货段缺合同</option>
<option>装货与送货均缺</option>
</select>
</div>
<div class="field">
<label>客户名称</label>
<input placeholder="段次客户名称" />
</div>
<div class="field">
<label>计划日期</label>
<input type="text" placeholder="开始日期 - 结束日期" />
</div>
</div>
<div class="filter-actions">
<button type="button" class="btn btn-ghost">重置</button>
<button type="button" class="btn btn-main">搜索</button>
</div>
</div>
<div class="list-toolbar">
<div class="date-pills">
<button type="button" class="date-pill active">全部</button>
<button type="button" class="date-pill">今天</button>
<button type="button" class="date-pill">近 7 天</button>
<button type="button" class="date-pill">本月</button>
</div>
<div class="toolbar-spacer"></div>
<input class="quick-search" placeholder="计划编号/客户名称" />
<button type="button" class="btn btn-ghost">导出</button>
<button type="button" class="btn btn-ghost">刷新</button>
<span class="muted" style="font-size:13px">待完善 3 条</span>
</div>
<div class="list-body">
<table class="list-table">
<thead>
<tr>
<th style="width:52px">序号</th>
<th style="width:130px">计划编号</th>
<th style="width:100px">计划日期</th>
<th style="width:80px">单据类型</th>
<th style="width:88px">跟进状态</th>
<th style="width:72px">段次</th>
<th style="width:160px">客户</th>
<th style="width:160px">地址 / 时间</th>
<th style="width:200px">合同缺口</th>
<th>产品名称</th>
</tr>
</thead>
<tbody>
<!-- 计划 1:装货缺合同,送货内置可免 -->
<tr>
<td>1</td>
<td rowspan="2" class="merge-cell">
<button type="button" class="btn-link mono">YP20260820001</button>
</td>
<td rowspan="2" class="merge-cell">2026-08-20</td>
<td rowspan="2" class="merge-cell">汽运</td>
<td rowspan="2" class="merge-cell"><span class="tag success">已安排</span></td>
<td>装货</td>
<td>合肥某某商贸有限公司</td>
<td>
<div class="addr-time">
<span class="addr">合肥市经开区某某路 88 号</span>
<span class="time">08:30</span>
</div>
</td>
<td>
<div class="gap-cell">
<div class="gap-line">
<span class="side">装货</span>
<span class="pending">待补充</span>
</div>
<button type="button" class="btn-link warn" data-open="load1">✎ 补充</button>
</div>
</td>
<td>液碱 32%</td>
</tr>
<tr>
<td>2</td>
<td>送货</td>
<td>
安庆市山峡化工有限公司
<span class="tag internal">内部</span>
</td>
<td>
<div class="addr-time">
<span class="addr">安庆市迎江区某某大道</span>
<span class="time">14:00</span>
</div>
</td>
<td>
<div class="gap-cell">
<div class="gap-line">
<span class="side">送货</span>
<span class="ok">内置可免</span>
</div>
</div>
</td>
<td>液碱 32%</td>
</tr>
<!-- 计划 2:送货缺合同 -->
<tr>
<td>3</td>
<td rowspan="2" class="merge-cell">
<button type="button" class="btn-link mono">YP20260819088</button>
</td>
<td rowspan="2" class="merge-cell">2026-08-19</td>
<td rowspan="2" class="merge-cell">汽运</td>
<td rowspan="2" class="merge-cell"><span class="tag info">待安排</span></td>
<td>装货</td>
<td>
安徽云栋化工有限公司
<span class="tag internal">内部</span>
</td>
<td>
<div class="addr-time">
<span class="addr">安庆市大观区某某工业园</span>
<span class="time">09:00</span>
</div>
</td>
<td>
<div class="gap-cell">
<div class="gap-line">
<span class="side">装货</span>
<span class="ok">内置可免</span>
</div>
</div>
</td>
<td>盐酸</td>
</tr>
<tr>
<td>4</td>
<td>送货</td>
<td>芜湖某某材料有限公司</td>
<td>
<div class="addr-time">
<span class="addr">芜湖市弋江区某某路</span>
<span class="time">16:00</span>
</div>
</td>
<td>
<div class="gap-cell">
<div class="gap-line">
<span class="side">送货</span>
<span class="pending">待补充</span>
</div>
<button type="button" class="btn-link warn" data-open="delivery2">✎ 补充</button>
</div>
</td>
<td>盐酸</td>
</tr>
<!-- 计划 3:两侧均缺,仅示装货行补充入口 -->
<tr>
<td>5</td>
<td class="merge-cell">
<button type="button" class="btn-link mono">YP20260818042</button>
</td>
<td class="merge-cell">2026-08-18</td>
<td class="merge-cell">汽运</td>
<td class="merge-cell"><span class="tag success">运输中</span></td>
<td>装货</td>
<td>马鞍山某某化工有限公司</td>
<td>
<div class="addr-time">
<span class="addr">马鞍山市雨山区某某路</span>
<span class="time">07:30</span>
</div>
</td>
<td>
<div class="gap-cell">
<div class="gap-line">
<span class="side">装货</span>
<span class="pending">待补充</span>
</div>
<button type="button" class="btn-link warn" data-open="load3">✎ 补充</button>
</div>
</td>
<td>片碱</td>
</tr>
</tbody>
</table>
</div>
<div class="pager">共 3 条 · 每页 20 条 · 1 / 1</div>
</section>
<section class="legend">
<div class="card">
<h3>弹窗行为</h3>
<ul>
<li>标题:<strong>补充合同 · 装货/送货</strong> + 计划编号</li>
<li>摘要:计划日期、跟进状态、本段客户、地址时间</li>
<li>主体:复用合同多选卡片(选择 / 查看 / 移除)</li>
<li>保存:仅提交本段合同;成功后刷新列表与角标</li>
</ul>
</div>
<div class="card">
<h3>与现网差异</h3>
<ul>
<li>❌ 去掉表格 <code>type="expand"</code> 行内展开区</li>
<li>✅ 「补充」改为打开弹窗(本 Demo)</li>
<li>✅ 内置客户行仍不展示「补充」入口</li>
<li>✅ 不在此页新建 / 编辑合同</li>
</ul>
</div>
</section>
</div>
<!-- 补充合同弹窗 -->
<div class="modal-mask" id="dialog-mask" role="dialog" aria-modal="true" aria-labelledby="dlg-title">
<div class="dialog">
<div class="dialog-head">
<h3 id="dlg-title">补充合同 · <span id="dlg-stop-kind">装货</span> · <span id="dlg-plan-no">YP20260820001</span></h3>
<button type="button" class="btn-link" id="dlg-close" aria-label="关闭">✕</button>
</div>
<div class="dialog-body">
<div class="stop-summary">
<div class="kv"><div class="k">计划日期</div><div class="v" id="dlg-date">2026-08-20</div></div>
<div class="kv"><div class="k">跟进状态</div><div class="v" id="dlg-status"><span class="tag success">已安排</span></div></div>
<div class="kv"><div class="k">本段客户</div><div class="v" id="dlg-customer">合肥某某商贸有限公司</div></div>
<div class="kv"><div class="k">地址 / 时间</div><div class="v" id="dlg-addr">合肥市经开区某某路 88 号 · 08:30</div></div>
</div>
<div class="side-box need" id="dlg-side-box">
<div class="side-box__head">
<h4 id="dlg-side-title">装货合同</h4>
<span class="tag warn" id="dlg-side-tag">待补充</span>
</div>
<p class="side-box__customer" id="dlg-side-customer">合肥某某商贸有限公司</p>
<div id="dlg-exempt" class="side-box__exempt" hidden>
内置公司(山峡/云栋),无需补充合同
</div>
<div id="dlg-editor">
<div class="contract-toolbar">
<button type="button" class="btn btn-ghost btn-sm" id="btn-pick">🔍 选择合同</button>
<span class="contract-hint is-warn" id="dlg-hint">请至少选择 1 份合同后保存</span>
</div>
<div class="contract-cards" id="dlg-cards">
<p class="empty-hint">暂未选择合同</p>
</div>
</div>
</div>
</div>
<div class="dialog-foot">
<button type="button" class="btn btn-ghost" id="dlg-cancel">✕ 取消</button>
<button type="button" class="btn btn-main" id="dlg-save">✓ 保存本段合同</button>
</div>
</div>
</div>
<!-- 选择合同(二级) -->
<div class="selector-mask" id="selector-mask" role="dialog" aria-modal="true">
<div class="selector-dialog">
<div class="dialog-head">
<h3>选择合同 · <span id="sel-customer">客户</span></h3>
<button type="button" class="btn-link" id="sel-close">✕</button>
</div>
<div style="padding:12px 16px;border-bottom:1px solid #eef2f6">
<input style="width:100%;height:36px;border:1px solid var(--border);border-radius:8px;padding:0 12px"
placeholder="搜索合同编号 / 产品" id="sel-search" />
</div>
<div class="selector-list" id="sel-list"></div>
<div class="dialog-foot">
<button type="button" class="btn btn-ghost" id="sel-cancel">取消</button>
<button type="button" class="btn btn-main" id="sel-confirm">确定</button>
</div>
</div>
</div>
<div class="toast" id="toast">本段合同已保存(Demo)</div>
<script>
const catalog = [
{ id: 'c1', no: 'HT-2026-0312', type: '销售合同', status: '执行中', product: '液碱 32%', qty: '30 吨' },
{ id: 'c2', no: 'HT-2026-0288', type: '框架合同', status: '已审批', product: '盐酸', qty: '20 吨' },
{ id: 'c3', no: 'HT-2026-0199', type: '销售合同', status: '执行中', product: '片碱', qty: '15 吨' }
]
const scenarios = {
load1: {
planNo: 'YP20260820001',
stopKind: '装货',
date: '2026-08-20',
statusHtml: '<span class="tag success">已安排</span>',
customer: '合肥某某商贸有限公司',
addr: '合肥市经开区某某路 88 号 · 08:30',
exempt: false,
selected: []
},
delivery2: {
planNo: 'YP20260819088',
stopKind: '送货',
date: '2026-08-19',
statusHtml: '<span class="tag info">待安排</span>',
customer: '芜湖某某材料有限公司',
addr: '芜湖市弋江区某某路 · 16:00',
exempt: false,
selected: []
},
load3: {
planNo: 'YP20260818042',
stopKind: '装货',
date: '2026-08-18',
statusHtml: '<span class="tag success">运输中</span>',
customer: '马鞍山某某化工有限公司',
addr: '马鞍山市雨山区某某路 · 07:30',
exempt: false,
selected: []
}
}
let currentKey = 'load1'
let draftIds = []
let baselineIds = []
let pendingPick = []
const mask = document.getElementById('dialog-mask')
const selMask = document.getElementById('selector-mask')
function isDirty() {
const a = [...draftIds].sort().join(',')
const b = [...baselineIds].sort().join(',')
return a !== b
}
function renderCards() {
const el = document.getElementById('dlg-cards')
const hint = document.getElementById('dlg-hint')
const box = document.getElementById('dlg-side-box')
const tag = document.getElementById('dlg-side-tag')
if (!draftIds.length) {
el.innerHTML = '<p class="empty-hint">暂未选择合同</p>'
hint.textContent = '请至少选择 1 份合同后保存'
hint.classList.add('is-warn')
box.className = 'side-box need'
tag.className = 'tag warn'
tag.textContent = '待补充'
return
}
hint.textContent = '已选 ' + draftIds.length + ' 份合同'
hint.classList.remove('is-warn')
box.className = 'side-box ok'
tag.className = 'tag success'
tag.textContent = '已选'
el.innerHTML = draftIds.map(id => {
const c = catalog.find(x => x.id === id)
if (!c) return ''
return `
<article class="contract-card" data-id="${c.id}">
<div class="contract-card__head">
<div class="contract-card__meta">
<span class="contract-card__no">${c.no}</span>
<span class="tag info">${c.type}</span>
<span class="contract-card__count">1 个产品</span>
</div>
<div class="contract-card__actions">
<button type="button" class="btn-icon" title="查看" data-view="${c.id}">👁</button>
<button type="button" class="btn-icon danger" title="移除" data-remove="${c.id}">🗑</button>
</div>
</div>
<div class="contract-card__body">
<table class="mini-table">
<thead>
<tr>
<th>产品名称</th>
<th>数量</th>
<th>状态</th>
</tr>
</thead>
<tbody>
<tr>
<td>${c.product}</td>
<td>${c.qty}</td>
<td>${c.status}</td>
</tr>
</tbody>
</table>
</div>
</article>`
}).join('')
el.querySelectorAll('[data-remove]').forEach(btn => {
btn.addEventListener('click', () => {
draftIds = draftIds.filter(id => id !== btn.dataset.remove)
renderCards()
})
})
el.querySelectorAll('[data-view]').forEach(btn => {
btn.addEventListener('click', () => {
const c = catalog.find(x => x.id === btn.dataset.view)
alert('Demo:查看合同详情\\n' + (c ? c.no : ''))
})
})
}
function openDialog(key) {
currentKey = key
const s = scenarios[key] || scenarios.load1
draftIds = [...(s.selected || [])]
baselineIds = [...draftIds]
document.getElementById('dlg-stop-kind').textContent = s.stopKind
document.getElementById('dlg-plan-no').textContent = s.planNo
document.getElementById('dlg-date').textContent = s.date
document.getElementById('dlg-status').innerHTML = s.statusHtml
document.getElementById('dlg-customer').textContent = s.customer
document.getElementById('dlg-addr').textContent = s.addr
document.getElementById('dlg-side-title').textContent = s.stopKind + '合同'
document.getElementById('dlg-side-customer').textContent = s.customer
document.getElementById('sel-customer').textContent = s.customer
const exempt = !!s.exempt
document.getElementById('dlg-exempt').hidden = !exempt
document.getElementById('dlg-editor').hidden = exempt
document.getElementById('dlg-save').style.display = exempt ? 'none' : ''
if (exempt) {
document.getElementById('dlg-side-box').className = 'side-box exempt'
document.getElementById('dlg-side-tag').className = 'tag muted'
document.getElementById('dlg-side-tag').textContent = '可免'
} else {
renderCards()
}
mask.classList.add('open')
}
function tryCloseDialog() {
if (isDirty() && !confirm('当前有未保存的合同变更,确认关闭吗?')) return
mask.classList.remove('open')
}
function openSelector() {
pendingPick = [...draftIds]
const list = document.getElementById('sel-list')
list.innerHTML = catalog.map(c => {
const checked = pendingPick.includes(c.id) ? 'checked' : ''
const selected = pendingPick.includes(c.id) ? 'selected' : ''
return `
<label class="selector-item ${selected}">
<input type="checkbox" value="${c.id}" ${checked} />
<div>
<div style="font-weight:600;color:var(--primary-dark)">${c.no}</div>
<div class="meta-line">${c.type} · ${c.status} · ${c.product}</div>
</div>
</label>`
}).join('')
list.querySelectorAll('input').forEach(input => {
input.addEventListener('change', () => {
pendingPick = [...list.querySelectorAll('input:checked')].map(i => i.value)
list.querySelectorAll('.selector-item').forEach(item => {
const cb = item.querySelector('input')
item.classList.toggle('selected', cb.checked)
})
})
})
selMask.classList.add('open')
}
function closeSelector() {
selMask.classList.remove('open')
}
document.querySelectorAll('[data-open]').forEach(btn => {
btn.addEventListener('click', () => openDialog(btn.dataset.open))
})
document.getElementById('dlg-close').addEventListener('click', tryCloseDialog)
document.getElementById('dlg-cancel').addEventListener('click', tryCloseDialog)
mask.addEventListener('click', e => { if (e.target === mask) tryCloseDialog() })
document.getElementById('btn-pick').addEventListener('click', openSelector)
document.getElementById('sel-close').addEventListener('click', closeSelector)
document.getElementById('sel-cancel').addEventListener('click', closeSelector)
selMask.addEventListener('click', e => { if (e.target === selMask) closeSelector() })
document.getElementById('sel-confirm').addEventListener('click', () => {
draftIds = [...pendingPick]
closeSelector()
renderCards()
})
document.getElementById('dlg-save').addEventListener('click', () => {
if (!draftIds.length) {
alert('请先选择至少 1 份合同')
return
}
scenarios[currentKey].selected = [...draftIds]
baselineIds = [...draftIds]
mask.classList.remove('open')
const toast = document.getElementById('toast')
toast.classList.add('show')
setTimeout(() => toast.classList.remove('show'), 1800)
})
</script>
</body>
</html>