fresh-mint-pl-plan-contract-complete-demo.html 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803
  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. --danger-bg: #fef2f2;
  30. }
  31. * { box-sizing: border-box; }
  32. body {
  33. margin: 0;
  34. min-height: 100vh;
  35. font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;
  36. font-size: 14px;
  37. line-height: 1.5;
  38. color: var(--text);
  39. background: var(--bg-page);
  40. }
  41. .demo-page { max-width: 1280px; margin: 0 auto; padding: 28px 24px 100px; }
  42. .demo-head { margin-bottom: 24px; }
  43. .demo-head h1 { margin: 0 0 8px; font-size: 24px; font-weight: 700; letter-spacing: -0.02em; }
  44. .demo-head h1 span { color: var(--primary); }
  45. .demo-head p { margin: 0 0 8px; color: var(--text-muted); max-width: 880px; }
  46. .meta { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
  47. .pill {
  48. display: inline-flex; align-items: center; gap: 6px;
  49. padding: 4px 10px; border-radius: 999px; font-size: 12px; font-weight: 500;
  50. background: var(--primary-bg); color: var(--primary-dark); border: 1px solid #a7f3d0;
  51. }
  52. .pill.muted { background: #f1f5f9; color: var(--text-muted); border-color: #e2e8f0; }
  53. .pill.warn { background: var(--warn-bg); color: var(--warn); border-color: rgba(217,119,6,.25); }
  54. .brief {
  55. display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 22px;
  56. }
  57. @media (max-width: 900px) { .brief { grid-template-columns: 1fr; } }
  58. .card {
  59. background: var(--surface); border: 1px solid var(--border);
  60. border-radius: var(--radius); padding: 16px 18px; box-shadow: var(--shadow);
  61. }
  62. .card h2 {
  63. margin: 0 0 10px; font-size: 15px; font-weight: 600;
  64. padding-left: 10px; border-left: 3px solid var(--primary);
  65. color: var(--primary-dark); line-height: 1.2;
  66. }
  67. .card ul { margin: 0; padding-left: 18px; color: var(--text-muted); }
  68. .card li { margin: 5px 0; }
  69. .card li strong { color: var(--text); }
  70. .scheme-tabs {
  71. display: flex; gap: 8px; margin-bottom: 14px; flex-wrap: wrap;
  72. }
  73. .scheme-tab {
  74. appearance: none; border: 1px solid var(--border); background: #fff;
  75. padding: 8px 14px; border-radius: 8px; font-size: 13px; font-weight: 500;
  76. color: var(--text-muted); cursor: pointer;
  77. }
  78. .scheme-tab.active {
  79. color: var(--primary-dark); border-color: #6ee7b7; background: var(--primary-bg);
  80. }
  81. .scheme-pane { display: none; }
  82. .scheme-pane.active { display: block; }
  83. .approach-note {
  84. margin: 0 0 14px; padding: 10px 14px; border-radius: 8px;
  85. background: #f8fafc; border: 1px dashed #cbd5e1; color: var(--text-muted); font-size: 13px;
  86. }
  87. .approach-note strong { color: var(--text); }
  88. .approach-note.rec {
  89. background: var(--primary-bg); border-color: #6ee7b7; border-style: solid;
  90. color: var(--primary-dark);
  91. }
  92. /* ── 列表壳(对齐 web-admin panel) ── */
  93. .panel {
  94. background: var(--surface); border: 1px solid var(--border);
  95. border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden;
  96. }
  97. .panel-top {
  98. display: flex; align-items: center; justify-content: space-between;
  99. gap: 12px; padding: 16px 20px 12px; border-bottom: 1px solid #eef2f6;
  100. }
  101. .panel-top h2 {
  102. margin: 0; font-size: 16px; font-weight: 600;
  103. display: flex; align-items: center; gap: 0;
  104. }
  105. .panel-top h2::before {
  106. content: ""; display: inline-block; width: 3px; height: 16px;
  107. background: var(--primary); border-radius: 2px; margin-right: 10px;
  108. }
  109. .stat-chip {
  110. display: inline-flex; align-items: center; gap: 6px;
  111. padding: 4px 10px; border-radius: 8px; font-size: 12px; font-weight: 600;
  112. background: var(--warn-bg); color: var(--warn); border: 1px solid rgba(217,119,6,.2);
  113. }
  114. .filter-block { padding: 14px 20px 6px; }
  115. .filter-grid {
  116. display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px 16px;
  117. }
  118. @media (max-width: 1100px) { .filter-grid { grid-template-columns: repeat(2, 1fr); } }
  119. .field label {
  120. display: block; font-size: 11px; color: var(--text-muted); margin-bottom: 4px;
  121. }
  122. .field input, .field select {
  123. width: 100%; height: 36px; border: 1px solid var(--border); border-radius: 8px;
  124. padding: 0 10px; font-size: 14px; color: var(--text); background: #fff;
  125. }
  126. .filter-actions {
  127. display: flex; justify-content: flex-end; gap: 8px; padding: 10px 0 4px;
  128. }
  129. .list-toolbar {
  130. display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  131. padding: 10px 20px; border-top: 1px solid #eef2f6;
  132. }
  133. .list-toolbar .hint { color: var(--text-muted); font-size: 13px; margin-right: auto; }
  134. .btn {
  135. appearance: none; display: inline-flex; align-items: center; gap: 6px;
  136. height: 36px; padding: 0 14px; border-radius: 8px; font-size: 13px; font-weight: 500;
  137. cursor: pointer; border: 1px solid var(--border); background: #fff; color: var(--text);
  138. }
  139. .btn:hover { background: #f8fafc; }
  140. .btn-main {
  141. background: var(--primary); border-color: var(--primary); color: #fff;
  142. }
  143. .btn-main:hover { background: var(--primary-dark); }
  144. .btn-ghost { background: #fff; }
  145. .btn-link {
  146. height: auto; padding: 0; border: none; background: transparent;
  147. color: var(--primary-dark); font-weight: 500;
  148. }
  149. .btn-link:hover { text-decoration: underline; background: transparent; }
  150. .btn-sm { height: 30px; padding: 0 10px; font-size: 12px; }
  151. .btn.warn-outline {
  152. border-color: rgba(217,119,6,.35); color: var(--warn); background: var(--warn-bg);
  153. }
  154. .list-body { border-top: 1px solid #eef2f6; }
  155. table.list-table {
  156. width: 100%; border-collapse: collapse; table-layout: fixed;
  157. }
  158. .list-table th {
  159. background: var(--table-head); text-align: left; font-weight: 600; font-size: 12px;
  160. color: var(--text-muted); padding: 10px 12px; border-bottom: 1px solid #eef2f6;
  161. white-space: nowrap;
  162. }
  163. .list-table td {
  164. padding: 12px; border-bottom: 1px solid #f1f5f9; vertical-align: top;
  165. font-size: 13px;
  166. }
  167. .list-table tbody tr:nth-child(even) { background: var(--row-alt); }
  168. .list-table tbody tr:hover { background: var(--row-hover); }
  169. .list-table .mono { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--primary-dark); }
  170. .list-table .muted { color: var(--text-muted); }
  171. .list-table .party { display: flex; flex-direction: column; gap: 2px; }
  172. .list-table .party .name { font-weight: 500; }
  173. .list-table .party .sub { font-size: 12px; color: var(--text-muted); }
  174. .tag {
  175. display: inline-flex; align-items: center; padding: 2px 8px;
  176. border-radius: 6px; font-size: 12px; font-weight: 500; white-space: nowrap;
  177. }
  178. .tag.success { background: #059669; color: #fff; }
  179. .tag.warn { background: #d97706; color: #fff; }
  180. .tag.danger { background: #dc2626; color: #fff; }
  181. .tag.info { background: transparent; color: #1e293b; border: 1px solid #e2e8f0; }
  182. .tag.muted { background: #f1f5f9; color: var(--text-muted); }
  183. .gap-tags { display: flex; flex-wrap: wrap; gap: 6px; }
  184. .ops { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
  185. .pager {
  186. display: flex; align-items: center; justify-content: flex-end; gap: 12px;
  187. padding: 12px 20px; border-top: 1px solid #eef2f6; color: var(--text-muted); font-size: 13px;
  188. }
  189. /* 方案 B:行内展开补合同 */
  190. .expand-row td { background: #f8fffb !important; padding: 0 !important; }
  191. .expand-panel { padding: 14px 16px 16px; border-top: 1px dashed #a7f3d0; }
  192. .expand-grid {
  193. display: grid; grid-template-columns: 1fr 1fr; gap: 14px;
  194. }
  195. @media (max-width: 900px) { .expand-grid { grid-template-columns: 1fr; } }
  196. .side-box {
  197. background: #fff; border: 1px solid var(--border); border-radius: 8px; padding: 12px;
  198. }
  199. .side-box.need { border-color: rgba(217,119,6,.35); background: #fffdf8; }
  200. .side-box.ok { border-color: #a7f3d0; background: #f6fffb; }
  201. .side-box.exempt { border-color: #e2e8f0; background: #f8fafc; }
  202. .side-box h4 {
  203. margin: 0 0 8px; font-size: 13px; display: flex; align-items: center;
  204. justify-content: space-between; gap: 8px;
  205. }
  206. .side-box .customer { font-size: 13px; margin-bottom: 10px; }
  207. .contract-chip {
  208. display: flex; align-items: center; justify-content: space-between; gap: 8px;
  209. padding: 8px 10px; border: 1px solid #e2e8f0; border-radius: 8px;
  210. background: #fff; margin-bottom: 8px; font-size: 12px;
  211. }
  212. .contract-chip .no { font-weight: 600; color: var(--primary-dark); }
  213. .empty-slot {
  214. padding: 14px; border: 1px dashed #cbd5e1; border-radius: 8px;
  215. text-align: center; color: var(--text-muted); font-size: 12px; margin-bottom: 8px;
  216. }
  217. .side-actions { display: flex; gap: 8px; flex-wrap: wrap; }
  218. /* 弹窗 */
  219. .modal-mask {
  220. display: none; position: fixed; inset: 0; background: rgba(15, 23, 42, 0.45);
  221. z-index: 1000; align-items: center; justify-content: center; padding: 24px;
  222. backdrop-filter: none;
  223. }
  224. .modal-mask.open { display: flex; }
  225. .dialog {
  226. width: min(920px, 100%); max-height: calc(100vh - 100px);
  227. background: #fff; border-radius: 12px; box-shadow: var(--shadow-lg);
  228. display: flex; flex-direction: column; overflow: hidden;
  229. }
  230. .dialog-head {
  231. display: flex; align-items: center; justify-content: space-between;
  232. padding: 14px 20px; border-bottom: 1px solid #eef2f6; flex-shrink: 0;
  233. }
  234. .dialog-head h3 { margin: 0; font-size: 16px; font-weight: 600; }
  235. .dialog-body { padding: 18px 20px; overflow: auto; flex: 1; }
  236. .dialog-foot {
  237. display: flex; justify-content: flex-end; gap: 8px;
  238. padding: 12px 20px; border-top: 1px solid #eef2f6; flex-shrink: 0;
  239. }
  240. .plan-summary {
  241. display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;
  242. margin-bottom: 16px; padding: 12px; background: #f8fafc; border-radius: 8px;
  243. border: 1px solid #eef2f6;
  244. }
  245. @media (max-width: 800px) { .plan-summary { grid-template-columns: 1fr 1fr; } }
  246. .kv .k { font-size: 11px; color: var(--text-muted); }
  247. .kv .v { font-size: 13px; font-weight: 500; margin-top: 2px; }
  248. .party-sections { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
  249. @media (max-width: 800px) { .party-sections { grid-template-columns: 1fr; } }
  250. .selector-list { max-height: 220px; overflow: auto; border: 1px solid var(--border); border-radius: 8px; }
  251. .selector-item {
  252. display: flex; align-items: flex-start; gap: 10px;
  253. padding: 10px 12px; border-bottom: 1px solid #f1f5f9; cursor: pointer;
  254. }
  255. .selector-item:last-child { border-bottom: none; }
  256. .selector-item:hover { background: var(--primary-bg); }
  257. .selector-item.selected { background: var(--primary-bg); }
  258. .selector-item .meta-line { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
  259. .selector-item input { margin-top: 3px; }
  260. .legend {
  261. margin-top: 22px; display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px;
  262. }
  263. @media (max-width: 900px) { .legend { grid-template-columns: 1fr; } }
  264. .legend .card h3 { margin: 0 0 8px; font-size: 14px; }
  265. .legend code {
  266. font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  267. font-size: 12px; background: #f1f5f9; padding: 1px 6px; border-radius: 4px;
  268. }
  269. .toast {
  270. position: fixed; top: 20px; left: 50%; transform: translateX(-50%) translateY(-120%);
  271. background: #059669; color: #fff; padding: 10px 18px; border-radius: 8px;
  272. font-size: 13px; font-weight: 500; box-shadow: var(--shadow-lg);
  273. transition: transform .25s ease; z-index: 2000;
  274. }
  275. .toast.show { transform: translateX(-50%) translateY(0); }
  276. </style>
  277. </head>
  278. <body>
  279. <div class="demo-page">
  280. <header class="demo-head">
  281. <h1>运输计划 · <span>合同完善</span> 界面 Demo</h1>
  282. <p>
  283. 仅展示「缺合同」的运输计划。判定规则:供需双方若非内置公司
  284. (安庆市山峡化工有限公司 / 安徽云栋化工有限公司),该侧至少需关联 1 份合同;
  285. 内置公司该侧合同可免。本页只做<strong>关联已有合同</strong>,不支持新增/编辑合同。
  286. </p>
  287. <div class="meta">
  288. <span class="pill">Fresh Mint · web-admin</span>
  289. <span class="pill muted">参考模式:P02 复杂列表 + 关联弹窗</span>
  290. <span class="pill warn">可点击交互 Demo</span>
  291. <span class="pill muted">菜单建议:运输计划模块下</span>
  292. </div>
  293. </header>
  294. <section class="brief">
  295. <div class="card">
  296. <h2>业务目标</h2>
  297. <ul>
  298. <li>创建计划时合同改为非必填,事后用本页<strong>快速补齐</strong></li>
  299. <li>列表默认只显示缺口:缺供方 / 缺需方 / 两侧都缺</li>
  300. <li>行内操作「补充合同」→ 打开选择器关联 CRM 已有合同</li>
  301. <li>补齐后自动从本列表消失(或刷新后消失)</li>
  302. </ul>
  303. </div>
  304. <div class="card">
  305. <h2>本 Demo 两种交互</h2>
  306. <ul>
  307. <li><strong>方案 A(推荐)</strong>:列表 +「补充合同」弹窗,两侧分区关联</li>
  308. <li><strong>方案 B</strong>:列表行展开,直接在行内补两侧合同</li>
  309. <li>下方可切换方案,并点击按钮体验弹窗</li>
  310. </ul>
  311. </div>
  312. </section>
  313. <div class="scheme-tabs" role="tablist">
  314. <button type="button" class="scheme-tab active" data-scheme="a">方案 A · 列表 + 弹窗(推荐)</button>
  315. <button type="button" class="scheme-tab" data-scheme="b">方案 B · 行内展开</button>
  316. </div>
  317. <!-- ═══ 方案 A ═══ -->
  318. <div class="scheme-pane active" id="scheme-a">
  319. <p class="approach-note rec">
  320. <strong>推荐:</strong>与现有「危化品证上传」「运输计划」列表一致;弹窗内可专注选合同,
  321. 不打断表格滚动;便于后续加菜单角标(待完善数量)。
  322. </p>
  323. <section class="panel">
  324. <div class="panel-top">
  325. <h2>运输计划合同完善</h2>
  326. <span class="stat-chip">待完善 3 条</span>
  327. </div>
  328. <div class="filter-block">
  329. <div class="filter-grid">
  330. <div class="field">
  331. <label>计划编号</label>
  332. <input placeholder="搜索计划编号" />
  333. </div>
  334. <div class="field">
  335. <label>缺口类型</label>
  336. <select>
  337. <option>全部缺口</option>
  338. <option>仅缺供方合同</option>
  339. <option>仅缺需方合同</option>
  340. <option>两侧均缺</option>
  341. </select>
  342. </div>
  343. <div class="field">
  344. <label>供方 / 需方</label>
  345. <input placeholder="客户名称" />
  346. </div>
  347. <div class="field">
  348. <label>计划日期</label>
  349. <input type="text" placeholder="开始日期 - 结束日期" />
  350. </div>
  351. </div>
  352. <div class="filter-actions">
  353. <button type="button" class="btn btn-ghost">重置</button>
  354. <button type="button" class="btn btn-main">搜索</button>
  355. </div>
  356. </div>
  357. <div class="list-toolbar">
  358. <span class="hint">仅展示仍缺合同的运输计划 · 内置公司侧已自动豁免</span>
  359. <button type="button" class="btn btn-ghost">刷新</button>
  360. </div>
  361. <div class="list-body">
  362. <table class="list-table">
  363. <colgroup>
  364. <col style="width: 12%" />
  365. <col style="width: 9%" />
  366. <col style="width: 18%" />
  367. <col style="width: 18%" />
  368. <col style="width: 14%" />
  369. <col style="width: 12%" />
  370. <col style="width: 17%" />
  371. </colgroup>
  372. <thead>
  373. <tr>
  374. <th>计划编号</th>
  375. <th>计划日期</th>
  376. <th>供方</th>
  377. <th>需方</th>
  378. <th>合同缺口</th>
  379. <th>跟进状态</th>
  380. <th>操作</th>
  381. </tr>
  382. </thead>
  383. <tbody>
  384. <tr>
  385. <td><a class="mono btn-link" href="javascript:void(0)">YP20260820001</a></td>
  386. <td>2026-08-20</td>
  387. <td>
  388. <div class="party">
  389. <span class="name">合肥某某商贸有限公司</span>
  390. <span class="sub">供方合同:未关联</span>
  391. </div>
  392. </td>
  393. <td>
  394. <div class="party">
  395. <span class="name">安庆市山峡化工有限公司</span>
  396. <span class="sub">内置 · 合同可免</span>
  397. </div>
  398. </td>
  399. <td>
  400. <div class="gap-tags">
  401. <span class="tag warn">缺供方合同</span>
  402. </div>
  403. </td>
  404. <td><span class="tag success">已安排</span></td>
  405. <td>
  406. <div class="ops">
  407. <button type="button" class="btn btn-sm warn-outline" data-open-dialog="row1">补充合同</button>
  408. <button type="button" class="btn-link btn-sm">查看计划</button>
  409. </div>
  410. </td>
  411. </tr>
  412. <tr>
  413. <td><a class="mono btn-link" href="javascript:void(0)">YP20260819088</a></td>
  414. <td>2026-08-19</td>
  415. <td>
  416. <div class="party">
  417. <span class="name">安庆市山峡化工有限公司</span>
  418. <span class="sub">内置 · 合同可免</span>
  419. </div>
  420. </td>
  421. <td>
  422. <div class="party">
  423. <span class="name">芜湖某某材料有限公司</span>
  424. <span class="sub">需方合同:未关联</span>
  425. </div>
  426. </td>
  427. <td>
  428. <div class="gap-tags">
  429. <span class="tag warn">缺需方合同</span>
  430. </div>
  431. </td>
  432. <td><span class="tag info">待安排</span></td>
  433. <td>
  434. <div class="ops">
  435. <button type="button" class="btn btn-sm warn-outline" data-open-dialog="row2">补充合同</button>
  436. <button type="button" class="btn-link btn-sm">查看计划</button>
  437. </div>
  438. </td>
  439. </tr>
  440. <tr>
  441. <td><a class="mono btn-link" href="javascript:void(0)">YP20260818042</a></td>
  442. <td>2026-08-18</td>
  443. <td>
  444. <div class="party">
  445. <span class="name">马鞍山某某化工有限公司</span>
  446. <span class="sub">供方合同:未关联</span>
  447. </div>
  448. </td>
  449. <td>
  450. <div class="party">
  451. <span class="name">铜陵某某实业有限公司</span>
  452. <span class="sub">需方合同:未关联</span>
  453. </div>
  454. </td>
  455. <td>
  456. <div class="gap-tags">
  457. <span class="tag danger">两侧均缺</span>
  458. </div>
  459. </td>
  460. <td><span class="tag success">运输中</span></td>
  461. <td>
  462. <div class="ops">
  463. <button type="button" class="btn btn-sm warn-outline" data-open-dialog="row3">补充合同</button>
  464. <button type="button" class="btn-link btn-sm">查看计划</button>
  465. </div>
  466. </td>
  467. </tr>
  468. </tbody>
  469. </table>
  470. </div>
  471. <div class="pager">共 3 条 · 每页 20 条 · 1 / 1</div>
  472. </section>
  473. </div>
  474. <!-- ═══ 方案 B ═══ -->
  475. <div class="scheme-pane" id="scheme-b">
  476. <p class="approach-note">
  477. <strong>备选:</strong>适合一次补几条、不想频繁开弹窗的场景;表格变高,窄屏体验略差。
  478. </p>
  479. <section class="panel">
  480. <div class="panel-top">
  481. <h2>运输计划合同完善</h2>
  482. <span class="stat-chip">待完善 2 条(示例)</span>
  483. </div>
  484. <div class="list-toolbar">
  485. <span class="hint">点击「展开补充」在行内关联合同</span>
  486. </div>
  487. <div class="list-body">
  488. <table class="list-table">
  489. <thead>
  490. <tr>
  491. <th style="width:12%">计划编号</th>
  492. <th style="width:10%">计划日期</th>
  493. <th style="width:22%">供方</th>
  494. <th style="width:22%">需方</th>
  495. <th style="width:14%">合同缺口</th>
  496. <th style="width:20%">操作</th>
  497. </tr>
  498. </thead>
  499. <tbody>
  500. <tr>
  501. <td class="mono">YP20260820001</td>
  502. <td>2026-08-20</td>
  503. <td>合肥某某商贸有限公司</td>
  504. <td>安庆市山峡化工有限公司</td>
  505. <td><span class="tag warn">缺供方合同</span></td>
  506. <td><button type="button" class="btn btn-sm warn-outline" id="toggle-expand">收起补充</button></td>
  507. </tr>
  508. <tr class="expand-row" id="expand-demo">
  509. <td colspan="6">
  510. <div class="expand-panel">
  511. <div class="expand-grid">
  512. <div class="side-box need">
  513. <h4>
  514. 供方合同
  515. <span class="tag warn">待补充</span>
  516. </h4>
  517. <div class="customer">合肥某某商贸有限公司</div>
  518. <div class="empty-slot">暂未关联合同</div>
  519. <div class="side-actions">
  520. <button type="button" class="btn btn-sm btn-main" data-open-dialog="row1">选择合同</button>
  521. </div>
  522. </div>
  523. <div class="side-box exempt">
  524. <h4>
  525. 需方合同
  526. <span class="tag muted">可免</span>
  527. </h4>
  528. <div class="customer">安庆市山峡化工有限公司(内置)</div>
  529. <div class="empty-slot" style="border-style:solid;background:#f8fafc;">
  530. 内置公司,无需关联合同
  531. </div>
  532. </div>
  533. </div>
  534. </div>
  535. </td>
  536. </tr>
  537. <tr>
  538. <td class="mono">YP20260818042</td>
  539. <td>2026-08-18</td>
  540. <td>马鞍山某某化工有限公司</td>
  541. <td>铜陵某某实业有限公司</td>
  542. <td><span class="tag danger">两侧均缺</span></td>
  543. <td><button type="button" class="btn btn-sm warn-outline" data-open-dialog="row3">展开补充</button></td>
  544. </tr>
  545. </tbody>
  546. </table>
  547. </div>
  548. </section>
  549. </div>
  550. <section class="legend">
  551. <div class="card">
  552. <h3>缺口判定(与内置名单一致)</h3>
  553. <ul>
  554. <li>名称 ∈ {山峡, 云栋} → 该侧<strong>不要求</strong>合同</li>
  555. <li>否则 → 该侧 <code>contracts.length ≥ 1</code></li>
  556. <li>任一侧不满足 → 进入本列表</li>
  557. </ul>
  558. </div>
  559. <div class="card">
  560. <h3>本页能力边界</h3>
  561. <ul>
  562. <li>✅ 筛选、列表、关联已有合同、保存</li>
  563. <li>✅ 跳转查看运输计划(只读)</li>
  564. <li>❌ 不在此页新增 / 编辑 / 审批合同</li>
  565. </ul>
  566. </div>
  567. <div class="card">
  568. <h3>待你确认(下一步提问)</h3>
  569. <ul>
  570. <li>是否含已作废计划?归档计划?</li>
  571. <li>菜单角标是否显示待完善数量?</li>
  572. <li>关联时是否允许多份合同?</li>
  573. </ul>
  574. </div>
  575. </section>
  576. </div>
  577. <!-- 补充合同弹窗 -->
  578. <div class="modal-mask" id="dialog-mask" role="dialog" aria-modal="true">
  579. <div class="dialog">
  580. <div class="dialog-head">
  581. <h3>补充合同 · <span id="dlg-plan-no">YP20260820001</span></h3>
  582. <button type="button" class="btn-link" id="dlg-close" aria-label="关闭">✕</button>
  583. </div>
  584. <div class="dialog-body">
  585. <div class="plan-summary">
  586. <div class="kv"><div class="k">计划日期</div><div class="v" id="dlg-date">2026-08-20</div></div>
  587. <div class="kv"><div class="k">跟进状态</div><div class="v"><span class="tag success">已安排</span></div></div>
  588. <div class="kv"><div class="k">供方</div><div class="v" id="dlg-supplier">合肥某某商贸有限公司</div></div>
  589. <div class="kv"><div class="k">需方</div><div class="v" id="dlg-demander">安庆市山峡化工有限公司</div></div>
  590. </div>
  591. <div class="party-sections">
  592. <div class="side-box need" id="dlg-supplier-box">
  593. <h4>供方合同 <span class="tag warn" id="dlg-supplier-tag">待补充</span></h4>
  594. <div class="customer" id="dlg-supplier-name">合肥某某商贸有限公司</div>
  595. <div id="dlg-supplier-selected" class="empty-slot">暂未选择 · 下方勾选后确认</div>
  596. <div class="selector-list" id="dlg-supplier-list">
  597. <label class="selector-item">
  598. <input type="checkbox" name="sc" value="c1" />
  599. <div>
  600. <div class="no" style="font-weight:600;color:var(--primary-dark)">HT-2026-0312</div>
  601. <div class="meta-line">销售合同 · 执行中 · 液碱 32%</div>
  602. </div>
  603. </label>
  604. <label class="selector-item">
  605. <input type="checkbox" name="sc" value="c2" />
  606. <div>
  607. <div class="no" style="font-weight:600;color:var(--primary-dark)">HT-2026-0288</div>
  608. <div class="meta-line">框架合同 · 已审批 · 盐酸</div>
  609. </div>
  610. </label>
  611. </div>
  612. </div>
  613. <div class="side-box exempt" id="dlg-demander-box">
  614. <h4>需方合同 <span class="tag muted" id="dlg-demander-tag">可免</span></h4>
  615. <div class="customer" id="dlg-demander-name">安庆市山峡化工有限公司(内置)</div>
  616. <div class="empty-slot" style="border-style:solid;">内置公司,无需选择合同</div>
  617. <div class="selector-list" id="dlg-demander-list" style="display:none;"></div>
  618. </div>
  619. </div>
  620. </div>
  621. <div class="dialog-foot">
  622. <button type="button" class="btn btn-ghost" id="dlg-cancel">取消</button>
  623. <button type="button" class="btn btn-main" id="dlg-save">保存关联</button>
  624. </div>
  625. </div>
  626. </div>
  627. <div class="toast" id="toast">已保存合同关联(Demo)</div>
  628. <script>
  629. const scenarios = {
  630. row1: {
  631. planNo: 'YP20260820001',
  632. date: '2026-08-20',
  633. supplier: '合肥某某商贸有限公司',
  634. demander: '安庆市山峡化工有限公司',
  635. supplierNeed: true,
  636. demanderNeed: false,
  637. supplierExempt: false,
  638. demanderExempt: true
  639. },
  640. row2: {
  641. planNo: 'YP20260819088',
  642. date: '2026-08-19',
  643. supplier: '安庆市山峡化工有限公司',
  644. demander: '芜湖某某材料有限公司',
  645. supplierNeed: false,
  646. demanderNeed: true,
  647. supplierExempt: true,
  648. demanderExempt: false
  649. },
  650. row3: {
  651. planNo: 'YP20260818042',
  652. date: '2026-08-18',
  653. supplier: '马鞍山某某化工有限公司',
  654. demander: '铜陵某某实业有限公司',
  655. supplierNeed: true,
  656. demanderNeed: true,
  657. supplierExempt: false,
  658. demanderExempt: false
  659. }
  660. }
  661. const mask = document.getElementById('dialog-mask')
  662. function paintSide(prefix, need, exempt, name) {
  663. const box = document.getElementById('dlg-' + prefix + '-box')
  664. const tag = document.getElementById('dlg-' + prefix + '-tag')
  665. const list = document.getElementById('dlg-' + prefix + '-list')
  666. const selected = document.getElementById('dlg-' + prefix + '-selected')
  667. document.getElementById('dlg-' + prefix + '-name').textContent =
  668. exempt ? name + '(内置)' : name
  669. box.classList.remove('need', 'ok', 'exempt')
  670. if (exempt) {
  671. box.classList.add('exempt')
  672. tag.className = 'tag muted'
  673. tag.textContent = '可免'
  674. if (selected) {
  675. selected.className = 'empty-slot'
  676. selected.style.borderStyle = 'solid'
  677. selected.textContent = '内置公司,无需选择合同'
  678. }
  679. if (list) list.style.display = 'none'
  680. } else if (need) {
  681. box.classList.add('need')
  682. tag.className = 'tag warn'
  683. tag.textContent = '待补充'
  684. if (selected) {
  685. selected.className = 'empty-slot'
  686. selected.style.borderStyle = ''
  687. selected.textContent = '暂未选择 · 下方勾选后确认'
  688. }
  689. if (list) {
  690. list.style.display = ''
  691. list.innerHTML = `
  692. <label class="selector-item">
  693. <input type="checkbox" name="${prefix}" value="a" />
  694. <div>
  695. <div style="font-weight:600;color:var(--primary-dark)">HT-2026-0312</div>
  696. <div class="meta-line">销售合同 · 执行中 · 示例产品 A</div>
  697. </div>
  698. </label>
  699. <label class="selector-item">
  700. <input type="checkbox" name="${prefix}" value="b" />
  701. <div>
  702. <div style="font-weight:600;color:var(--primary-dark)">HT-2026-0288</div>
  703. <div class="meta-line">框架合同 · 已审批 · 示例产品 B</div>
  704. </div>
  705. </label>`
  706. }
  707. } else {
  708. box.classList.add('ok')
  709. tag.className = 'tag success'
  710. tag.textContent = '已齐'
  711. if (list) list.style.display = 'none'
  712. }
  713. }
  714. function openDialog(key) {
  715. const s = scenarios[key] || scenarios.row1
  716. document.getElementById('dlg-plan-no').textContent = s.planNo
  717. document.getElementById('dlg-date').textContent = s.date
  718. document.getElementById('dlg-supplier').textContent = s.supplier
  719. document.getElementById('dlg-demander').textContent = s.demander
  720. paintSide('supplier', s.supplierNeed, s.supplierExempt, s.supplier)
  721. paintSide('demander', s.demanderNeed, s.demanderExempt, s.demander)
  722. mask.classList.add('open')
  723. }
  724. function closeDialog() {
  725. mask.classList.remove('open')
  726. }
  727. document.querySelectorAll('.scheme-tab').forEach(tab => {
  728. tab.addEventListener('click', () => {
  729. document.querySelectorAll('.scheme-tab').forEach(t => t.classList.remove('active'))
  730. document.querySelectorAll('.scheme-pane').forEach(p => p.classList.remove('active'))
  731. tab.classList.add('active')
  732. document.getElementById('scheme-' + tab.dataset.scheme).classList.add('active')
  733. })
  734. })
  735. document.querySelectorAll('[data-open-dialog]').forEach(btn => {
  736. btn.addEventListener('click', () => openDialog(btn.dataset.openDialog))
  737. })
  738. document.getElementById('dlg-close').addEventListener('click', closeDialog)
  739. document.getElementById('dlg-cancel').addEventListener('click', closeDialog)
  740. mask.addEventListener('click', e => { if (e.target === mask) closeDialog() })
  741. document.getElementById('dlg-save').addEventListener('click', () => {
  742. closeDialog()
  743. const toast = document.getElementById('toast')
  744. toast.classList.add('show')
  745. setTimeout(() => toast.classList.remove('show'), 1800)
  746. })
  747. const expandBtn = document.getElementById('toggle-expand')
  748. const expandRow = document.getElementById('expand-demo')
  749. if (expandBtn && expandRow) {
  750. expandBtn.addEventListener('click', () => {
  751. const hidden = expandRow.style.display === 'none'
  752. expandRow.style.display = hidden ? '' : 'none'
  753. expandBtn.textContent = hidden ? '收起补充' : '展开补充'
  754. })
  755. }
  756. </script>
  757. </body>
  758. </html>