| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858 |
- <!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>
|