fresh-mint-pl-plan-contract-supplement-dialog-demo.html 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858
  1. <!doctype html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  6. <title>Fresh Mint · 运输计划合同完善 · 补充弹窗 Demo</title>
  7. <link rel="preconnect" href="https://fonts.googleapis.com" />
  8. <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
  9. <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
  10. <style>
  11. :root {
  12. --primary: #10b981;
  13. --primary-dark: #059669;
  14. --primary-bg: #ecfdf5;
  15. --text: #1e293b;
  16. --text-muted: #64748b;
  17. --border: #d9e8e2;
  18. --surface: #ffffff;
  19. --table-head: #f8fafc;
  20. --bg-page: #f8fafc;
  21. --row-alt: #fafafa;
  22. --row-hover: #f1f5f9;
  23. --shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
  24. --shadow-lg: 0 12px 40px rgba(15, 23, 42, 0.12);
  25. --radius: 10px;
  26. --warn: #d97706;
  27. --warn-bg: #fffbeb;
  28. --danger: #dc2626;
  29. }
  30. * { box-sizing: border-box; }
  31. body {
  32. margin: 0;
  33. min-height: 100vh;
  34. font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;
  35. font-size: 14px;
  36. line-height: 1.5;
  37. color: var(--text);
  38. background: var(--bg-page);
  39. }
  40. .demo-page { max-width: 1360px; margin: 0 auto; padding: 28px 24px 80px; }
  41. .demo-head { margin-bottom: 20px; }
  42. .demo-head h1 { margin: 0 0 8px; font-size: 22px; font-weight: 700; letter-spacing: -0.02em; }
  43. .demo-head h1 span { color: var(--primary); }
  44. .demo-head p { margin: 0; color: var(--text-muted); max-width: 920px; }
  45. .meta { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
  46. .pill {
  47. display: inline-flex; align-items: center;
  48. padding: 4px 10px; border-radius: 999px; font-size: 12px; font-weight: 500;
  49. background: var(--primary-bg); color: var(--primary-dark); border: 1px solid #a7f3d0;
  50. }
  51. .pill.muted { background: #f1f5f9; color: var(--text-muted); border-color: #e2e8f0; }
  52. .pill.warn { background: var(--warn-bg); color: var(--warn); border-color: rgba(217,119,6,.25); }
  53. .note {
  54. margin: 0 0 16px; padding: 12px 14px; border-radius: 8px;
  55. background: var(--primary-bg); border: 1px solid #6ee7b7; color: var(--primary-dark);
  56. font-size: 13px;
  57. }
  58. .note strong { color: var(--text); }
  59. .panel {
  60. background: var(--surface); border: 1px solid var(--border);
  61. border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden;
  62. }
  63. .panel-top {
  64. display: flex; align-items: center; justify-content: space-between;
  65. gap: 12px; padding: 16px 20px 12px; border-bottom: 1px solid #eef2f6;
  66. }
  67. .panel-top h2 {
  68. margin: 0; font-size: 16px; font-weight: 600;
  69. display: flex; align-items: center;
  70. }
  71. .panel-top h2::before {
  72. content: ""; display: inline-block; width: 3px; height: 16px;
  73. background: var(--primary); border-radius: 2px; margin-right: 10px;
  74. }
  75. .stat-chip {
  76. display: inline-flex; align-items: center;
  77. padding: 4px 10px; border-radius: 8px; font-size: 12px; font-weight: 600;
  78. background: var(--warn-bg); color: var(--warn); border: 1px solid rgba(217,119,6,.2);
  79. }
  80. .filter-block { padding: 14px 20px 6px; }
  81. .filter-grid {
  82. display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px 16px;
  83. }
  84. @media (max-width: 1100px) { .filter-grid { grid-template-columns: repeat(2, 1fr); } }
  85. .field label {
  86. display: block; font-size: 11px; color: var(--text-muted); margin-bottom: 4px;
  87. }
  88. .field input, .field select {
  89. width: 100%; height: 36px; border: 1px solid var(--border); border-radius: 8px;
  90. padding: 0 10px; font-size: 14px; color: var(--text); background: #fff;
  91. }
  92. .filter-actions {
  93. display: flex; justify-content: flex-end; gap: 8px; padding: 10px 0 4px;
  94. }
  95. .list-toolbar {
  96. display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  97. padding: 10px 20px; border-top: 1px solid #eef2f6;
  98. }
  99. .date-pills { display: flex; gap: 6px; flex-wrap: wrap; }
  100. .date-pill {
  101. height: 30px; padding: 0 10px; border-radius: 999px; border: 1px solid var(--border);
  102. background: #fff; font-size: 12px; color: var(--text-muted); cursor: pointer;
  103. }
  104. .date-pill.active {
  105. background: var(--primary-bg); border-color: #6ee7b7; color: var(--primary-dark); font-weight: 600;
  106. }
  107. .toolbar-spacer { flex: 1; }
  108. .quick-search {
  109. width: 220px; height: 36px; border: 1px solid var(--border); border-radius: 8px;
  110. padding: 0 12px; font-size: 13px;
  111. }
  112. .btn {
  113. appearance: none; display: inline-flex; align-items: center; gap: 6px;
  114. height: 36px; padding: 0 14px; border-radius: 8px; font-size: 13px; font-weight: 500;
  115. cursor: pointer; border: 1px solid var(--border); background: #fff; color: var(--text);
  116. }
  117. .btn:hover { background: #f8fafc; }
  118. .btn-main { background: var(--primary); border-color: var(--primary); color: #fff; }
  119. .btn-main:hover { background: var(--primary-dark); }
  120. .btn-ghost { background: #fff; }
  121. .btn-link {
  122. height: auto; padding: 0; border: none; background: transparent;
  123. color: var(--primary-dark); font-weight: 500; cursor: pointer; font-size: 13px;
  124. }
  125. .btn-link:hover { text-decoration: underline; }
  126. .btn-link.warn { color: var(--warn); }
  127. .btn-sm { height: 30px; padding: 0 10px; font-size: 12px; }
  128. .btn-icon {
  129. width: 28px; height: 28px; padding: 0; justify-content: center;
  130. border: none; background: transparent; color: var(--text-muted); cursor: pointer;
  131. border-radius: 6px;
  132. }
  133. .btn-icon:hover { background: #f1f5f9; color: var(--primary-dark); }
  134. .btn-icon.danger:hover { color: var(--danger); background: #fef2f2; }
  135. .list-body { border-top: 1px solid #eef2f6; overflow-x: auto; }
  136. table.list-table {
  137. width: 100%; border-collapse: collapse; min-width: 1100px;
  138. }
  139. .list-table th {
  140. background: var(--table-head); text-align: left; font-weight: 600; font-size: 12px;
  141. color: var(--text-muted); padding: 10px 12px; border-bottom: 1px solid #eef2f6;
  142. white-space: nowrap;
  143. }
  144. .list-table td {
  145. padding: 12px; border-bottom: 1px solid #f1f5f9; vertical-align: middle;
  146. font-size: 13px;
  147. }
  148. .list-table tbody tr:hover { background: var(--row-hover); }
  149. .list-table .mono { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--primary-dark); }
  150. .list-table .muted { color: var(--text-muted); }
  151. .list-table .merge-cell { background: #fff; }
  152. .addr-time { display: flex; flex-direction: column; gap: 2px; }
  153. .addr-time .addr { color: var(--text); }
  154. .addr-time .time { font-size: 12px; color: var(--text-muted); }
  155. .tag {
  156. display: inline-flex; align-items: center; padding: 2px 8px;
  157. border-radius: 6px; font-size: 12px; font-weight: 500; white-space: nowrap;
  158. }
  159. .tag.success { background: #059669; color: #fff; }
  160. .tag.warn { background: #d97706; color: #fff; }
  161. .tag.danger { background: #dc2626; color: #fff; }
  162. .tag.info { background: transparent; color: #1e293b; border: 1px solid #e2e8f0; }
  163. .tag.muted { background: #f1f5f9; color: var(--text-muted); }
  164. .tag.internal { background: #059669; color: #fff; margin-left: 6px; }
  165. .gap-cell {
  166. display: flex; flex-direction: column; align-items: flex-start; gap: 6px;
  167. }
  168. .gap-line {
  169. display: flex; align-items: center; gap: 6px; font-size: 12px;
  170. }
  171. .gap-line .side { color: var(--text-muted); min-width: 2.5em; }
  172. .gap-line .pending { color: var(--warn); font-weight: 500; }
  173. .gap-line .ok { color: var(--primary-dark); font-weight: 500; }
  174. .pager {
  175. display: flex; align-items: center; justify-content: flex-end; gap: 12px;
  176. padding: 12px 20px; border-top: 1px solid #eef2f6; color: var(--text-muted); font-size: 13px;
  177. }
  178. /* 弹窗 */
  179. .modal-mask {
  180. display: none; position: fixed; inset: 0; background: rgba(15, 23, 42, 0.45);
  181. z-index: 1000; align-items: center; justify-content: center; padding: 24px;
  182. backdrop-filter: none;
  183. }
  184. .modal-mask.open { display: flex; }
  185. .dialog {
  186. width: min(760px, 100%); max-height: calc(100vh - 100px);
  187. background: #fff; border-radius: 12px; box-shadow: var(--shadow-lg);
  188. display: flex; flex-direction: column; overflow: hidden;
  189. }
  190. .dialog-head {
  191. display: flex; align-items: center; justify-content: space-between;
  192. padding: 14px 20px; border-bottom: 1px solid #eef2f6; flex-shrink: 0;
  193. }
  194. .dialog-head h3 { margin: 0; font-size: 16px; font-weight: 600; }
  195. .dialog-body { padding: 18px 20px; overflow: auto; flex: 1; }
  196. .dialog-foot {
  197. display: flex; justify-content: flex-end; gap: 8px;
  198. padding: 12px 20px; border-top: 1px solid #eef2f6; flex-shrink: 0;
  199. }
  200. .stop-summary {
  201. display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;
  202. margin-bottom: 16px; padding: 12px 14px; background: #f8fafc; border-radius: 8px;
  203. border: 1px solid #eef2f6;
  204. }
  205. @media (max-width: 700px) { .stop-summary { grid-template-columns: 1fr 1fr; } }
  206. .kv .k { font-size: 11px; color: var(--text-muted); }
  207. .kv .v { font-size: 13px; font-weight: 500; margin-top: 2px; }
  208. .side-box {
  209. background: #fff; border: 1px solid var(--border); border-radius: 8px; padding: 14px;
  210. }
  211. .side-box.need { border-color: rgba(217,119,6,.35); background: #fffdf8; }
  212. .side-box.ok { border-color: #a7f3d0; background: #f6fffb; }
  213. .side-box.exempt { border-color: #e2e8f0; background: #f8fafc; }
  214. .side-box__head {
  215. display: flex; align-items: center; justify-content: space-between;
  216. gap: 8px; margin-bottom: 6px;
  217. }
  218. .side-box__head h4 { margin: 0; font-size: 14px; font-weight: 600; }
  219. .side-box__customer { font-size: 13px; margin-bottom: 12px; color: var(--text); }
  220. .side-box__exempt {
  221. margin: 0; padding: 14px; border: 1px solid #e2e8f0; border-radius: 8px;
  222. background: #fff; color: var(--text-muted); font-size: 13px; text-align: center;
  223. }
  224. .contract-toolbar {
  225. display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px;
  226. }
  227. .contract-hint { font-size: 12px; color: var(--text-muted); }
  228. .contract-hint.is-warn { color: var(--warn); }
  229. .contract-cards { display: flex; flex-direction: column; gap: 10px; }
  230. .contract-card {
  231. border: 1px solid var(--border); border-radius: 8px; background: #fff; overflow: hidden;
  232. }
  233. .contract-card__head {
  234. display: flex; align-items: center; justify-content: space-between; gap: 8px;
  235. padding: 10px 12px; background: #f8fafc; border-bottom: 1px solid #eef2f6;
  236. }
  237. .contract-card__meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
  238. .contract-card__no { font-weight: 600; color: var(--primary-dark); font-size: 13px; }
  239. .contract-card__count { font-size: 12px; color: var(--text-muted); }
  240. .contract-card__actions { display: flex; align-items: center; gap: 2px; }
  241. .contract-card__body { padding: 0; }
  242. .mini-table { width: 100%; border-collapse: collapse; font-size: 12px; }
  243. .mini-table th {
  244. background: #fafafa; color: var(--text-muted); font-weight: 500;
  245. padding: 6px 10px; text-align: left; border-bottom: 1px solid #f1f5f9;
  246. }
  247. .mini-table td {
  248. padding: 6px 10px; border-bottom: 1px solid #f8fafc; color: var(--text);
  249. }
  250. .empty-hint {
  251. margin: 0; padding: 18px; text-align: center; color: var(--text-muted);
  252. font-size: 13px; border: 1px dashed #cbd5e1; border-radius: 8px; background: #fff;
  253. }
  254. /* 合同选择器(二级弹层) */
  255. .selector-mask {
  256. display: none; position: fixed; inset: 0; background: rgba(15, 23, 42, 0.4);
  257. z-index: 1100; align-items: center; justify-content: center; padding: 24px;
  258. }
  259. .selector-mask.open { display: flex; }
  260. .selector-dialog {
  261. width: min(640px, 100%); max-height: calc(100vh - 120px);
  262. background: #fff; border-radius: 12px; box-shadow: var(--shadow-lg);
  263. display: flex; flex-direction: column; overflow: hidden;
  264. }
  265. .selector-list { overflow: auto; flex: 1; border-top: 1px solid #eef2f6; }
  266. .selector-item {
  267. display: flex; align-items: flex-start; gap: 10px;
  268. padding: 12px 16px; border-bottom: 1px solid #f1f5f9; cursor: pointer;
  269. }
  270. .selector-item:hover, .selector-item.selected { background: var(--primary-bg); }
  271. .selector-item .meta-line { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
  272. .selector-item input { margin-top: 3px; }
  273. .toast {
  274. position: fixed; top: 20px; left: 50%; transform: translateX(-50%) translateY(-120%);
  275. background: #059669; color: #fff; padding: 10px 18px; border-radius: 8px;
  276. font-size: 13px; font-weight: 500; box-shadow: var(--shadow-lg);
  277. transition: transform .25s ease; z-index: 2000;
  278. }
  279. .toast.show { transform: translateX(-50%) translateY(0); }
  280. .legend {
  281. margin-top: 20px; display: grid; grid-template-columns: 1fr 1fr; gap: 12px;
  282. }
  283. @media (max-width: 800px) { .legend { grid-template-columns: 1fr; } }
  284. .card {
  285. background: var(--surface); border: 1px solid var(--border);
  286. border-radius: var(--radius); padding: 14px 16px; box-shadow: var(--shadow);
  287. }
  288. .card h3 {
  289. margin: 0 0 8px; font-size: 14px; font-weight: 600;
  290. padding-left: 10px; border-left: 3px solid var(--primary); color: var(--primary-dark);
  291. }
  292. .card ul { margin: 0; padding-left: 18px; color: var(--text-muted); font-size: 13px; }
  293. .card li { margin: 4px 0; }
  294. .card li strong { color: var(--text); }
  295. </style>
  296. </head>
  297. <body>
  298. <div class="demo-page">
  299. <header class="demo-head">
  300. <h1>运输计划合同完善 · <span>补充弹窗</span></h1>
  301. <p>
  302. 对齐当前线上列表(段次行合并):点击合同缺口旁的「补充」打开弹窗,
  303. 仅针对<strong>本段客户</strong>关联已有 CRM 合同并保存。不再使用行内展开。
  304. </p>
  305. <div class="meta">
  306. <span class="pill">Fresh Mint · web-admin</span>
  307. <span class="pill warn">点击「补充」体验弹窗</span>
  308. <span class="pill muted">路径 /#/pl/plan-module/plan-contract</span>
  309. </div>
  310. </header>
  311. <p class="note">
  312. <strong>交互变更:</strong>原「行内展开 + 保存本段合同」→ 改为「弹窗补充」。
  313. 弹窗内复用合同多选卡片(选择合同 / 查看 / 移除),底栏「取消 / 保存本段合同」。
  314. 未保存关闭时二次确认(Demo 用 confirm)。
  315. </p>
  316. <section class="panel">
  317. <div class="panel-top">
  318. <h2>运输计划合同完善</h2>
  319. <span class="stat-chip">待完善 3 条</span>
  320. </div>
  321. <div class="filter-block">
  322. <div class="filter-grid">
  323. <div class="field">
  324. <label>计划编号</label>
  325. <input placeholder="搜索计划编号" />
  326. </div>
  327. <div class="field">
  328. <label>缺口类型</label>
  329. <select>
  330. <option>全部缺口</option>
  331. <option>装货段缺合同</option>
  332. <option>送货段缺合同</option>
  333. <option>装货与送货均缺</option>
  334. </select>
  335. </div>
  336. <div class="field">
  337. <label>客户名称</label>
  338. <input placeholder="段次客户名称" />
  339. </div>
  340. <div class="field">
  341. <label>计划日期</label>
  342. <input type="text" placeholder="开始日期 - 结束日期" />
  343. </div>
  344. </div>
  345. <div class="filter-actions">
  346. <button type="button" class="btn btn-ghost">重置</button>
  347. <button type="button" class="btn btn-main">搜索</button>
  348. </div>
  349. </div>
  350. <div class="list-toolbar">
  351. <div class="date-pills">
  352. <button type="button" class="date-pill active">全部</button>
  353. <button type="button" class="date-pill">今天</button>
  354. <button type="button" class="date-pill">近 7 天</button>
  355. <button type="button" class="date-pill">本月</button>
  356. </div>
  357. <div class="toolbar-spacer"></div>
  358. <input class="quick-search" placeholder="计划编号/客户名称" />
  359. <button type="button" class="btn btn-ghost">导出</button>
  360. <button type="button" class="btn btn-ghost">刷新</button>
  361. <span class="muted" style="font-size:13px">待完善 3 条</span>
  362. </div>
  363. <div class="list-body">
  364. <table class="list-table">
  365. <thead>
  366. <tr>
  367. <th style="width:52px">序号</th>
  368. <th style="width:130px">计划编号</th>
  369. <th style="width:100px">计划日期</th>
  370. <th style="width:80px">单据类型</th>
  371. <th style="width:88px">跟进状态</th>
  372. <th style="width:72px">段次</th>
  373. <th style="width:160px">客户</th>
  374. <th style="width:160px">地址 / 时间</th>
  375. <th style="width:200px">合同缺口</th>
  376. <th>产品名称</th>
  377. </tr>
  378. </thead>
  379. <tbody>
  380. <!-- 计划 1:装货缺合同,送货内置可免 -->
  381. <tr>
  382. <td>1</td>
  383. <td rowspan="2" class="merge-cell">
  384. <button type="button" class="btn-link mono">YP20260820001</button>
  385. </td>
  386. <td rowspan="2" class="merge-cell">2026-08-20</td>
  387. <td rowspan="2" class="merge-cell">汽运</td>
  388. <td rowspan="2" class="merge-cell"><span class="tag success">已安排</span></td>
  389. <td>装货</td>
  390. <td>合肥某某商贸有限公司</td>
  391. <td>
  392. <div class="addr-time">
  393. <span class="addr">合肥市经开区某某路 88 号</span>
  394. <span class="time">08:30</span>
  395. </div>
  396. </td>
  397. <td>
  398. <div class="gap-cell">
  399. <div class="gap-line">
  400. <span class="side">装货</span>
  401. <span class="pending">待补充</span>
  402. </div>
  403. <button type="button" class="btn-link warn" data-open="load1">✎ 补充</button>
  404. </div>
  405. </td>
  406. <td>液碱 32%</td>
  407. </tr>
  408. <tr>
  409. <td>2</td>
  410. <td>送货</td>
  411. <td>
  412. 安庆市山峡化工有限公司
  413. <span class="tag internal">内部</span>
  414. </td>
  415. <td>
  416. <div class="addr-time">
  417. <span class="addr">安庆市迎江区某某大道</span>
  418. <span class="time">14:00</span>
  419. </div>
  420. </td>
  421. <td>
  422. <div class="gap-cell">
  423. <div class="gap-line">
  424. <span class="side">送货</span>
  425. <span class="ok">内置可免</span>
  426. </div>
  427. </div>
  428. </td>
  429. <td>液碱 32%</td>
  430. </tr>
  431. <!-- 计划 2:送货缺合同 -->
  432. <tr>
  433. <td>3</td>
  434. <td rowspan="2" class="merge-cell">
  435. <button type="button" class="btn-link mono">YP20260819088</button>
  436. </td>
  437. <td rowspan="2" class="merge-cell">2026-08-19</td>
  438. <td rowspan="2" class="merge-cell">汽运</td>
  439. <td rowspan="2" class="merge-cell"><span class="tag info">待安排</span></td>
  440. <td>装货</td>
  441. <td>
  442. 安徽云栋化工有限公司
  443. <span class="tag internal">内部</span>
  444. </td>
  445. <td>
  446. <div class="addr-time">
  447. <span class="addr">安庆市大观区某某工业园</span>
  448. <span class="time">09:00</span>
  449. </div>
  450. </td>
  451. <td>
  452. <div class="gap-cell">
  453. <div class="gap-line">
  454. <span class="side">装货</span>
  455. <span class="ok">内置可免</span>
  456. </div>
  457. </div>
  458. </td>
  459. <td>盐酸</td>
  460. </tr>
  461. <tr>
  462. <td>4</td>
  463. <td>送货</td>
  464. <td>芜湖某某材料有限公司</td>
  465. <td>
  466. <div class="addr-time">
  467. <span class="addr">芜湖市弋江区某某路</span>
  468. <span class="time">16:00</span>
  469. </div>
  470. </td>
  471. <td>
  472. <div class="gap-cell">
  473. <div class="gap-line">
  474. <span class="side">送货</span>
  475. <span class="pending">待补充</span>
  476. </div>
  477. <button type="button" class="btn-link warn" data-open="delivery2">✎ 补充</button>
  478. </div>
  479. </td>
  480. <td>盐酸</td>
  481. </tr>
  482. <!-- 计划 3:两侧均缺,仅示装货行补充入口 -->
  483. <tr>
  484. <td>5</td>
  485. <td class="merge-cell">
  486. <button type="button" class="btn-link mono">YP20260818042</button>
  487. </td>
  488. <td class="merge-cell">2026-08-18</td>
  489. <td class="merge-cell">汽运</td>
  490. <td class="merge-cell"><span class="tag success">运输中</span></td>
  491. <td>装货</td>
  492. <td>马鞍山某某化工有限公司</td>
  493. <td>
  494. <div class="addr-time">
  495. <span class="addr">马鞍山市雨山区某某路</span>
  496. <span class="time">07:30</span>
  497. </div>
  498. </td>
  499. <td>
  500. <div class="gap-cell">
  501. <div class="gap-line">
  502. <span class="side">装货</span>
  503. <span class="pending">待补充</span>
  504. </div>
  505. <button type="button" class="btn-link warn" data-open="load3">✎ 补充</button>
  506. </div>
  507. </td>
  508. <td>片碱</td>
  509. </tr>
  510. </tbody>
  511. </table>
  512. </div>
  513. <div class="pager">共 3 条 · 每页 20 条 · 1 / 1</div>
  514. </section>
  515. <section class="legend">
  516. <div class="card">
  517. <h3>弹窗行为</h3>
  518. <ul>
  519. <li>标题:<strong>补充合同 · 装货/送货</strong> + 计划编号</li>
  520. <li>摘要:计划日期、跟进状态、本段客户、地址时间</li>
  521. <li>主体:复用合同多选卡片(选择 / 查看 / 移除)</li>
  522. <li>保存:仅提交本段合同;成功后刷新列表与角标</li>
  523. </ul>
  524. </div>
  525. <div class="card">
  526. <h3>与现网差异</h3>
  527. <ul>
  528. <li>❌ 去掉表格 <code>type="expand"</code> 行内展开区</li>
  529. <li>✅ 「补充」改为打开弹窗(本 Demo)</li>
  530. <li>✅ 内置客户行仍不展示「补充」入口</li>
  531. <li>✅ 不在此页新建 / 编辑合同</li>
  532. </ul>
  533. </div>
  534. </section>
  535. </div>
  536. <!-- 补充合同弹窗 -->
  537. <div class="modal-mask" id="dialog-mask" role="dialog" aria-modal="true" aria-labelledby="dlg-title">
  538. <div class="dialog">
  539. <div class="dialog-head">
  540. <h3 id="dlg-title">补充合同 · <span id="dlg-stop-kind">装货</span> · <span id="dlg-plan-no">YP20260820001</span></h3>
  541. <button type="button" class="btn-link" id="dlg-close" aria-label="关闭">✕</button>
  542. </div>
  543. <div class="dialog-body">
  544. <div class="stop-summary">
  545. <div class="kv"><div class="k">计划日期</div><div class="v" id="dlg-date">2026-08-20</div></div>
  546. <div class="kv"><div class="k">跟进状态</div><div class="v" id="dlg-status"><span class="tag success">已安排</span></div></div>
  547. <div class="kv"><div class="k">本段客户</div><div class="v" id="dlg-customer">合肥某某商贸有限公司</div></div>
  548. <div class="kv"><div class="k">地址 / 时间</div><div class="v" id="dlg-addr">合肥市经开区某某路 88 号 · 08:30</div></div>
  549. </div>
  550. <div class="side-box need" id="dlg-side-box">
  551. <div class="side-box__head">
  552. <h4 id="dlg-side-title">装货合同</h4>
  553. <span class="tag warn" id="dlg-side-tag">待补充</span>
  554. </div>
  555. <p class="side-box__customer" id="dlg-side-customer">合肥某某商贸有限公司</p>
  556. <div id="dlg-exempt" class="side-box__exempt" hidden>
  557. 内置公司(山峡/云栋),无需补充合同
  558. </div>
  559. <div id="dlg-editor">
  560. <div class="contract-toolbar">
  561. <button type="button" class="btn btn-ghost btn-sm" id="btn-pick">🔍 选择合同</button>
  562. <span class="contract-hint is-warn" id="dlg-hint">请至少选择 1 份合同后保存</span>
  563. </div>
  564. <div class="contract-cards" id="dlg-cards">
  565. <p class="empty-hint">暂未选择合同</p>
  566. </div>
  567. </div>
  568. </div>
  569. </div>
  570. <div class="dialog-foot">
  571. <button type="button" class="btn btn-ghost" id="dlg-cancel">✕ 取消</button>
  572. <button type="button" class="btn btn-main" id="dlg-save">✓ 保存本段合同</button>
  573. </div>
  574. </div>
  575. </div>
  576. <!-- 选择合同(二级) -->
  577. <div class="selector-mask" id="selector-mask" role="dialog" aria-modal="true">
  578. <div class="selector-dialog">
  579. <div class="dialog-head">
  580. <h3>选择合同 · <span id="sel-customer">客户</span></h3>
  581. <button type="button" class="btn-link" id="sel-close">✕</button>
  582. </div>
  583. <div style="padding:12px 16px;border-bottom:1px solid #eef2f6">
  584. <input style="width:100%;height:36px;border:1px solid var(--border);border-radius:8px;padding:0 12px"
  585. placeholder="搜索合同编号 / 产品" id="sel-search" />
  586. </div>
  587. <div class="selector-list" id="sel-list"></div>
  588. <div class="dialog-foot">
  589. <button type="button" class="btn btn-ghost" id="sel-cancel">取消</button>
  590. <button type="button" class="btn btn-main" id="sel-confirm">确定</button>
  591. </div>
  592. </div>
  593. </div>
  594. <div class="toast" id="toast">本段合同已保存(Demo)</div>
  595. <script>
  596. const catalog = [
  597. { id: 'c1', no: 'HT-2026-0312', type: '销售合同', status: '执行中', product: '液碱 32%', qty: '30 吨' },
  598. { id: 'c2', no: 'HT-2026-0288', type: '框架合同', status: '已审批', product: '盐酸', qty: '20 吨' },
  599. { id: 'c3', no: 'HT-2026-0199', type: '销售合同', status: '执行中', product: '片碱', qty: '15 吨' }
  600. ]
  601. const scenarios = {
  602. load1: {
  603. planNo: 'YP20260820001',
  604. stopKind: '装货',
  605. date: '2026-08-20',
  606. statusHtml: '<span class="tag success">已安排</span>',
  607. customer: '合肥某某商贸有限公司',
  608. addr: '合肥市经开区某某路 88 号 · 08:30',
  609. exempt: false,
  610. selected: []
  611. },
  612. delivery2: {
  613. planNo: 'YP20260819088',
  614. stopKind: '送货',
  615. date: '2026-08-19',
  616. statusHtml: '<span class="tag info">待安排</span>',
  617. customer: '芜湖某某材料有限公司',
  618. addr: '芜湖市弋江区某某路 · 16:00',
  619. exempt: false,
  620. selected: []
  621. },
  622. load3: {
  623. planNo: 'YP20260818042',
  624. stopKind: '装货',
  625. date: '2026-08-18',
  626. statusHtml: '<span class="tag success">运输中</span>',
  627. customer: '马鞍山某某化工有限公司',
  628. addr: '马鞍山市雨山区某某路 · 07:30',
  629. exempt: false,
  630. selected: []
  631. }
  632. }
  633. let currentKey = 'load1'
  634. let draftIds = []
  635. let baselineIds = []
  636. let pendingPick = []
  637. const mask = document.getElementById('dialog-mask')
  638. const selMask = document.getElementById('selector-mask')
  639. function isDirty() {
  640. const a = [...draftIds].sort().join(',')
  641. const b = [...baselineIds].sort().join(',')
  642. return a !== b
  643. }
  644. function renderCards() {
  645. const el = document.getElementById('dlg-cards')
  646. const hint = document.getElementById('dlg-hint')
  647. const box = document.getElementById('dlg-side-box')
  648. const tag = document.getElementById('dlg-side-tag')
  649. if (!draftIds.length) {
  650. el.innerHTML = '<p class="empty-hint">暂未选择合同</p>'
  651. hint.textContent = '请至少选择 1 份合同后保存'
  652. hint.classList.add('is-warn')
  653. box.className = 'side-box need'
  654. tag.className = 'tag warn'
  655. tag.textContent = '待补充'
  656. return
  657. }
  658. hint.textContent = '已选 ' + draftIds.length + ' 份合同'
  659. hint.classList.remove('is-warn')
  660. box.className = 'side-box ok'
  661. tag.className = 'tag success'
  662. tag.textContent = '已选'
  663. el.innerHTML = draftIds.map(id => {
  664. const c = catalog.find(x => x.id === id)
  665. if (!c) return ''
  666. return `
  667. <article class="contract-card" data-id="${c.id}">
  668. <div class="contract-card__head">
  669. <div class="contract-card__meta">
  670. <span class="contract-card__no">${c.no}</span>
  671. <span class="tag info">${c.type}</span>
  672. <span class="contract-card__count">1 个产品</span>
  673. </div>
  674. <div class="contract-card__actions">
  675. <button type="button" class="btn-icon" title="查看" data-view="${c.id}">👁</button>
  676. <button type="button" class="btn-icon danger" title="移除" data-remove="${c.id}">🗑</button>
  677. </div>
  678. </div>
  679. <div class="contract-card__body">
  680. <table class="mini-table">
  681. <thead>
  682. <tr>
  683. <th>产品名称</th>
  684. <th>数量</th>
  685. <th>状态</th>
  686. </tr>
  687. </thead>
  688. <tbody>
  689. <tr>
  690. <td>${c.product}</td>
  691. <td>${c.qty}</td>
  692. <td>${c.status}</td>
  693. </tr>
  694. </tbody>
  695. </table>
  696. </div>
  697. </article>`
  698. }).join('')
  699. el.querySelectorAll('[data-remove]').forEach(btn => {
  700. btn.addEventListener('click', () => {
  701. draftIds = draftIds.filter(id => id !== btn.dataset.remove)
  702. renderCards()
  703. })
  704. })
  705. el.querySelectorAll('[data-view]').forEach(btn => {
  706. btn.addEventListener('click', () => {
  707. const c = catalog.find(x => x.id === btn.dataset.view)
  708. alert('Demo:查看合同详情\\n' + (c ? c.no : ''))
  709. })
  710. })
  711. }
  712. function openDialog(key) {
  713. currentKey = key
  714. const s = scenarios[key] || scenarios.load1
  715. draftIds = [...(s.selected || [])]
  716. baselineIds = [...draftIds]
  717. document.getElementById('dlg-stop-kind').textContent = s.stopKind
  718. document.getElementById('dlg-plan-no').textContent = s.planNo
  719. document.getElementById('dlg-date').textContent = s.date
  720. document.getElementById('dlg-status').innerHTML = s.statusHtml
  721. document.getElementById('dlg-customer').textContent = s.customer
  722. document.getElementById('dlg-addr').textContent = s.addr
  723. document.getElementById('dlg-side-title').textContent = s.stopKind + '合同'
  724. document.getElementById('dlg-side-customer').textContent = s.customer
  725. document.getElementById('sel-customer').textContent = s.customer
  726. const exempt = !!s.exempt
  727. document.getElementById('dlg-exempt').hidden = !exempt
  728. document.getElementById('dlg-editor').hidden = exempt
  729. document.getElementById('dlg-save').style.display = exempt ? 'none' : ''
  730. if (exempt) {
  731. document.getElementById('dlg-side-box').className = 'side-box exempt'
  732. document.getElementById('dlg-side-tag').className = 'tag muted'
  733. document.getElementById('dlg-side-tag').textContent = '可免'
  734. } else {
  735. renderCards()
  736. }
  737. mask.classList.add('open')
  738. }
  739. function tryCloseDialog() {
  740. if (isDirty() && !confirm('当前有未保存的合同变更,确认关闭吗?')) return
  741. mask.classList.remove('open')
  742. }
  743. function openSelector() {
  744. pendingPick = [...draftIds]
  745. const list = document.getElementById('sel-list')
  746. list.innerHTML = catalog.map(c => {
  747. const checked = pendingPick.includes(c.id) ? 'checked' : ''
  748. const selected = pendingPick.includes(c.id) ? 'selected' : ''
  749. return `
  750. <label class="selector-item ${selected}">
  751. <input type="checkbox" value="${c.id}" ${checked} />
  752. <div>
  753. <div style="font-weight:600;color:var(--primary-dark)">${c.no}</div>
  754. <div class="meta-line">${c.type} · ${c.status} · ${c.product}</div>
  755. </div>
  756. </label>`
  757. }).join('')
  758. list.querySelectorAll('input').forEach(input => {
  759. input.addEventListener('change', () => {
  760. pendingPick = [...list.querySelectorAll('input:checked')].map(i => i.value)
  761. list.querySelectorAll('.selector-item').forEach(item => {
  762. const cb = item.querySelector('input')
  763. item.classList.toggle('selected', cb.checked)
  764. })
  765. })
  766. })
  767. selMask.classList.add('open')
  768. }
  769. function closeSelector() {
  770. selMask.classList.remove('open')
  771. }
  772. document.querySelectorAll('[data-open]').forEach(btn => {
  773. btn.addEventListener('click', () => openDialog(btn.dataset.open))
  774. })
  775. document.getElementById('dlg-close').addEventListener('click', tryCloseDialog)
  776. document.getElementById('dlg-cancel').addEventListener('click', tryCloseDialog)
  777. mask.addEventListener('click', e => { if (e.target === mask) tryCloseDialog() })
  778. document.getElementById('btn-pick').addEventListener('click', openSelector)
  779. document.getElementById('sel-close').addEventListener('click', closeSelector)
  780. document.getElementById('sel-cancel').addEventListener('click', closeSelector)
  781. selMask.addEventListener('click', e => { if (e.target === selMask) closeSelector() })
  782. document.getElementById('sel-confirm').addEventListener('click', () => {
  783. draftIds = [...pendingPick]
  784. closeSelector()
  785. renderCards()
  786. })
  787. document.getElementById('dlg-save').addEventListener('click', () => {
  788. if (!draftIds.length) {
  789. alert('请先选择至少 1 份合同')
  790. return
  791. }
  792. scenarios[currentKey].selected = [...draftIds]
  793. baselineIds = [...draftIds]
  794. mask.classList.remove('open')
  795. const toast = document.getElementById('toast')
  796. toast.classList.add('show')
  797. setTimeout(() => toast.classList.remove('show'), 1800)
  798. })
  799. </script>
  800. </body>
  801. </html>