859 lines
35 KiB
HTML
859 lines
35 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;
|
||
--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>
|