fresh-mint-view-wide-dialog-demos.html 65 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860
  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. --shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
  21. --shadow-lg: 0 8px 24px rgba(15, 23, 42, 0.08);
  22. --radius: 10px;
  23. --danger: #dc2626;
  24. }
  25. * { box-sizing: border-box; }
  26. body {
  27. margin: 0;
  28. min-height: 100vh;
  29. font-family: Inter, -apple-system, sans-serif;
  30. font-size: 14px;
  31. line-height: 1.5;
  32. color: var(--text);
  33. background: #f8fafc;
  34. }
  35. .page { max-width: 1320px; margin: 0 auto; padding: 32px 24px 80px; }
  36. .page-head { margin-bottom: 24px; }
  37. .page-head h1 { margin: 0 0 8px; font-size: 24px; font-weight: 700; }
  38. .page-head h1 span { color: var(--primary); }
  39. .page-head p { margin: 0 0 12px; color: var(--text-muted); max-width: 780px; line-height: 1.6; }
  40. .page-head a { color: var(--primary-dark); }
  41. .width-bar {
  42. display: flex;
  43. flex-wrap: wrap;
  44. gap: 8px;
  45. margin-bottom: 24px;
  46. }
  47. .width-pill {
  48. font-size: 12px;
  49. font-weight: 600;
  50. padding: 4px 10px;
  51. border-radius: 6px;
  52. background: var(--surface);
  53. border: 1px solid var(--border);
  54. color: var(--text-muted);
  55. }
  56. .width-pill b { color: var(--text); }
  57. .compare-grid {
  58. display: grid;
  59. grid-template-columns: repeat(2, 1fr);
  60. gap: 20px;
  61. }
  62. @media (max-width: 1000px) { .compare-grid { grid-template-columns: 1fr; } }
  63. .layout-card {
  64. background: var(--surface);
  65. border: 1px solid var(--border);
  66. border-radius: var(--radius);
  67. overflow: hidden;
  68. box-shadow: var(--shadow);
  69. display: flex;
  70. flex-direction: column;
  71. }
  72. .layout-card.rec { border-color: rgba(16, 185, 129, 0.4); }
  73. .layout-card-head {
  74. padding: 14px 16px;
  75. border-bottom: 1px solid var(--border);
  76. display: flex;
  77. justify-content: space-between;
  78. gap: 12px;
  79. }
  80. .layout-card-head h2 { margin: 0 0 4px; font-size: 15px; font-weight: 600; }
  81. .layout-card-head p { margin: 0; font-size: 12px; color: var(--text-muted); line-height: 1.45; }
  82. .badge {
  83. flex-shrink: 0;
  84. font-size: 11px;
  85. font-weight: 700;
  86. padding: 3px 8px;
  87. border-radius: 6px;
  88. background: var(--primary-bg);
  89. color: var(--primary-dark);
  90. white-space: nowrap;
  91. }
  92. .badge.blue { background: #eff6ff; color: #2563eb; }
  93. .badge.amber { background: #fffbeb; color: #b45309; }
  94. .layout-preview {
  95. flex: 1;
  96. padding: 12px;
  97. background: #f1f5f9;
  98. min-height: 260px;
  99. overflow: auto;
  100. }
  101. .mini-wrap {
  102. transform: scale(0.92);
  103. transform-origin: top center;
  104. }
  105. .mini-dialog {
  106. background: #fff;
  107. border-radius: 8px;
  108. border: 1px solid var(--border);
  109. overflow: hidden;
  110. font-size: 11px;
  111. margin: 0 auto;
  112. }
  113. .mini-dialog.w920 { max-width: 420px; }
  114. .mini-dialog.w960 { max-width: 440px; }
  115. .mini-dialog.w1040 { max-width: 480px; }
  116. .mini-dialog.w1120 { max-width: 520px; }
  117. .layout-card-foot {
  118. padding: 12px 16px;
  119. border-top: 1px solid var(--border);
  120. display: flex;
  121. justify-content: space-between;
  122. align-items: center;
  123. background: var(--table-head);
  124. }
  125. .layout-card-foot .meta { font-size: 11px; color: var(--text-muted); }
  126. .btn {
  127. height: 32px;
  128. padding: 0 12px;
  129. border-radius: 8px;
  130. font-size: 12px;
  131. font-weight: 600;
  132. font-family: inherit;
  133. cursor: pointer;
  134. border: 1px solid var(--primary);
  135. background: var(--primary);
  136. color: #fff;
  137. }
  138. /* ── 弹窗通用 ── */
  139. .backdrop {
  140. position: fixed;
  141. inset: 0;
  142. background: rgba(15, 23, 42, 0.45);
  143. display: none;
  144. align-items: center;
  145. justify-content: center;
  146. padding: 20px;
  147. z-index: 1000;
  148. backdrop-filter: none;
  149. }
  150. .backdrop.open { display: flex; }
  151. .dialog {
  152. background: #fff;
  153. border-radius: var(--radius);
  154. box-shadow: var(--shadow-lg);
  155. width: 100%;
  156. max-height: calc(100vh - 40px);
  157. display: flex;
  158. flex-direction: column;
  159. overflow: hidden;
  160. animation: pop 0.2s ease;
  161. }
  162. @keyframes pop {
  163. from { opacity: 0; transform: scale(0.97); }
  164. to { opacity: 1; transform: none; }
  165. }
  166. .dialog.w920 { max-width: 920px; }
  167. .dialog.w720 { max-width: 720px; }
  168. .dialog.w960 { max-width: 960px; }
  169. .dialog.w1040 { max-width: 1040px; }
  170. .dialog.w1120 { max-width: 1120px; }
  171. .dialog-hero {
  172. padding: 16px 22px;
  173. background: linear-gradient(135deg, var(--primary-bg), #fff);
  174. border-bottom: 1px solid var(--border);
  175. display: flex;
  176. align-items: flex-start;
  177. justify-content: space-between;
  178. gap: 16px;
  179. }
  180. .dialog-hero h2 { margin: 0 0 4px; font-size: 18px; font-weight: 700; }
  181. .dialog-hero p { margin: 0; font-size: 13px; color: var(--text-muted); }
  182. .width-tag {
  183. flex-shrink: 0;
  184. font-size: 11px;
  185. font-weight: 700;
  186. padding: 4px 10px;
  187. border-radius: 6px;
  188. background: #eff6ff;
  189. color: #2563eb;
  190. border: 1px solid rgba(37, 99, 235, 0.2);
  191. }
  192. .dialog-scroll { overflow-y: auto; flex: 1; }
  193. .dialog-foot {
  194. padding: 12px 22px;
  195. border-top: 1px solid var(--border);
  196. display: flex;
  197. justify-content: flex-end;
  198. background: var(--table-head);
  199. }
  200. .dialog-foot .btn-ghost {
  201. height: 36px;
  202. padding: 0 14px;
  203. border-radius: 8px;
  204. font-size: 13px;
  205. font-weight: 600;
  206. font-family: inherit;
  207. cursor: pointer;
  208. border: 1px solid var(--border);
  209. background: #fff;
  210. color: var(--text);
  211. }
  212. .view-section {
  213. padding: 18px 22px;
  214. border-bottom: 1px solid #ebeef5;
  215. }
  216. .view-section:last-child { border-bottom: none; }
  217. .view-section h3 {
  218. margin: 0 0 14px;
  219. padding-left: 10px;
  220. font-size: 14px;
  221. font-weight: 600;
  222. border-left: 3px solid var(--primary);
  223. line-height: 1;
  224. }
  225. /* KV 上下堆叠 + 多列 grid */
  226. .kv-grid {
  227. display: grid;
  228. gap: 12px 18px;
  229. }
  230. .kv-grid.cols-2 { grid-template-columns: repeat(2, 1fr); }
  231. .kv-grid.cols-3 { grid-template-columns: repeat(3, 1fr); }
  232. .kv-grid.cols-4 { grid-template-columns: repeat(4, 1fr); }
  233. .kv-item { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
  234. .kv-item dt { margin: 0; font-size: 11px; color: var(--text-muted); }
  235. .kv-item dd { margin: 0; font-size: 13px; font-weight: 600; word-break: break-word; line-height: 1.45; }
  236. .kv-item.span-2 { grid-column: span 2; }
  237. .kv-item.span-3 { grid-column: span 3; }
  238. .kv-item.span-4 { grid-column: span 4; }
  239. .kv-item.span-all { grid-column: 1 / -1; }
  240. .tag {
  241. display: inline-block;
  242. padding: 1px 8px;
  243. border-radius: 5px;
  244. font-size: 12px;
  245. font-weight: 600;
  246. background: var(--primary-bg);
  247. color: var(--primary-dark);
  248. border: 1px solid rgba(16, 185, 129, 0.25);
  249. }
  250. /* 双区块并排 */
  251. .section-cols-2 {
  252. display: grid;
  253. grid-template-columns: 1fr 1fr;
  254. gap: 0;
  255. }
  256. .section-cols-2 .view-section {
  257. border-bottom: none;
  258. border-right: 1px solid #ebeef5;
  259. }
  260. .section-cols-2 .view-section:last-child { border-right: none; }
  261. @media (max-width: 800px) {
  262. .section-cols-2 { grid-template-columns: 1fr; }
  263. .section-cols-2 .view-section { border-right: none; border-bottom: 1px solid #ebeef5; }
  264. }
  265. /* 左右分栏 */
  266. .view-split {
  267. display: flex;
  268. min-height: 400px;
  269. }
  270. .view-split-left {
  271. flex: 0 0 340px;
  272. border-right: 1px solid var(--border);
  273. overflow-y: auto;
  274. }
  275. .view-split-right {
  276. flex: 1;
  277. overflow-y: auto;
  278. background: var(--table-head);
  279. }
  280. .view-split-left .view-section,
  281. .view-split-right .view-section { border-bottom: none; }
  282. @media (max-width: 860px) {
  283. .view-split { flex-direction: column; }
  284. .view-split-left { flex: none; border-right: none; border-bottom: 1px solid var(--border); }
  285. }
  286. /* 2×2 卡片宫格 */
  287. .card-grid-2x2 {
  288. display: grid;
  289. grid-template-columns: 1fr 1fr;
  290. gap: 14px;
  291. }
  292. @media (max-width: 720px) { .card-grid-2x2 { grid-template-columns: 1fr; } }
  293. .info-card {
  294. border: 1px solid var(--border);
  295. border-radius: var(--radius);
  296. padding: 14px 16px;
  297. background: #fff;
  298. }
  299. .info-card h4 {
  300. margin: 0 0 12px;
  301. font-size: 13px;
  302. font-weight: 600;
  303. color: var(--primary-dark);
  304. display: flex;
  305. align-items: center;
  306. gap: 6px;
  307. }
  308. .info-card h4::before {
  309. content: "";
  310. width: 6px;
  311. height: 6px;
  312. border-radius: 50%;
  313. background: var(--primary);
  314. }
  315. .info-card .kv-grid { gap: 10px 12px; }
  316. /* 宽表双组(一行两组 label-value) */
  317. .kv-wide-table {
  318. width: 100%;
  319. border-collapse: collapse;
  320. font-size: 13px;
  321. }
  322. .kv-wide-table td {
  323. padding: 9px 12px;
  324. border-bottom: 1px solid #f1f5f9;
  325. vertical-align: top;
  326. }
  327. .kv-wide-table .lbl {
  328. width: 11%;
  329. color: var(--text-muted);
  330. font-size: 11px;
  331. font-weight: 500;
  332. white-space: nowrap;
  333. }
  334. .kv-wide-table .val {
  335. width: 39%;
  336. font-weight: 600;
  337. word-break: break-word;
  338. padding-right: 20px;
  339. }
  340. .kv-wide-table tr:last-child td { border-bottom: none; }
  341. /* ── KV 表格模式(左右 label | value)── */
  342. .demo-section { margin-top: 32px; }
  343. .demo-section h2 {
  344. margin: 0 0 8px;
  345. font-size: 18px;
  346. font-weight: 700;
  347. }
  348. .demo-section > p {
  349. margin: 0 0 16px;
  350. color: var(--text-muted);
  351. max-width: 820px;
  352. line-height: 1.6;
  353. font-size: 13px;
  354. }
  355. .kv-table-wrap {
  356. border: 1px solid var(--border);
  357. border-radius: 8px;
  358. overflow: hidden;
  359. background: #fff;
  360. }
  361. .kv-table {
  362. width: 100%;
  363. border-collapse: collapse;
  364. font-size: 13px;
  365. table-layout: fixed;
  366. }
  367. .kv-table th {
  368. padding: 10px 12px;
  369. background: var(--table-head);
  370. color: var(--text-muted);
  371. font-size: 11px;
  372. font-weight: 500;
  373. text-align: left;
  374. vertical-align: top;
  375. border-bottom: 1px solid #eef2f6;
  376. border-right: 1px solid #eef2f6;
  377. white-space: nowrap;
  378. }
  379. .kv-table td {
  380. padding: 10px 14px;
  381. font-weight: 600;
  382. color: var(--text);
  383. border-bottom: 1px solid #eef2f6;
  384. vertical-align: top;
  385. word-break: break-word;
  386. line-height: 1.45;
  387. }
  388. .kv-table tr:last-child th,
  389. .kv-table tr:last-child td { border-bottom: none; }
  390. .kv-table .money-val { color: var(--danger); }
  391. .kv-table .muted-val { color: var(--text-muted); font-weight: 600; }
  392. /* 每行一组 label | value */
  393. .kv-table--pair th { width: 128px; }
  394. .kv-table--pair tr.row-long th { width: 128px; }
  395. /* 每行两组 label | value | label | value */
  396. .kv-table--dual th { width: 12%; }
  397. .kv-table--dual td { width: 38%; }
  398. .kv-table--dual tr.row-long th { width: 12%; }
  399. .kv-table--dual tr.row-long td { width: auto; }
  400. /* 斑马纹:无竖线、行间浅底 */
  401. .kv-table--zebra { table-layout: auto; }
  402. .kv-table--zebra th {
  403. border-right: none;
  404. background: transparent;
  405. color: var(--text-muted);
  406. width: 110px;
  407. padding-left: 14px;
  408. }
  409. .kv-table--zebra td {
  410. border-bottom: 1px solid #f1f5f9;
  411. padding-right: 14px;
  412. }
  413. .kv-table--zebra tbody tr:nth-child(even) { background: #f8fafc; }
  414. .kv-table--zebra tr.row-long th { vertical-align: top; padding-top: 12px; }
  415. .kv-table--zebra.kv-table--dual th { width: 11%; }
  416. .kv-table--zebra.kv-table--dual td { width: 39%; }
  417. /* 紧凑:更小行高 */
  418. .kv-table--compact th,
  419. .kv-table--compact td { padding: 8px 10px; }
  420. .kv-table--compact th { font-size: 10px; }
  421. .kv-table--compact td { font-size: 12px; }
  422. .mini-dialog .kv-table th { font-size: 8px; padding: 4px 5px; }
  423. .mini-dialog .kv-table td { font-size: 9px; padding: 4px 6px; }
  424. .mini-dialog .kv-table .tag { font-size: 8px; padding: 0 4px; }
  425. /* ── 其他展示模式(非 KV 栅格 / 非 KV 表)── */
  426. .stat-row {
  427. display: grid;
  428. grid-template-columns: repeat(4, 1fr);
  429. gap: 10px;
  430. }
  431. @media (max-width: 720px) { .stat-row { grid-template-columns: repeat(2, 1fr); } }
  432. .stat-cell {
  433. padding: 12px 14px;
  434. border: 1px solid var(--border);
  435. border-radius: 8px;
  436. background: #fff;
  437. box-shadow: var(--shadow);
  438. }
  439. .stat-cell .lbl { font-size: 11px; color: var(--text-muted); margin-bottom: 4px; }
  440. .stat-cell .val { font-size: 15px; font-weight: 700; line-height: 1.3; font-variant-numeric: tabular-nums; }
  441. .stat-cell .val.money { color: var(--danger); }
  442. .mini-dialog .stat-row { grid-template-columns: repeat(2, 1fr); gap: 6px; }
  443. .mini-dialog .stat-cell { padding: 6px 8px; }
  444. .mini-dialog .stat-cell .val { font-size: 10px; }
  445. .prop-list {
  446. border: 1px solid var(--border);
  447. border-radius: 8px;
  448. overflow: hidden;
  449. background: #fff;
  450. }
  451. .prop-row {
  452. display: flex;
  453. align-items: flex-start;
  454. gap: 12px;
  455. padding: 10px 14px;
  456. border-bottom: 1px solid #f1f5f9;
  457. font-size: 13px;
  458. }
  459. .prop-row:last-child { border-bottom: none; }
  460. .prop-row .lbl {
  461. flex: 0 0 108px;
  462. font-size: 11px;
  463. color: var(--text-muted);
  464. font-weight: 500;
  465. line-height: 1.45;
  466. }
  467. .prop-row .val { flex: 1; font-weight: 600; word-break: break-word; line-height: 1.45; }
  468. .prop-row .val.money { color: var(--danger); }
  469. .mini-dialog .prop-row { padding: 5px 8px; }
  470. .mini-dialog .prop-row .lbl { flex-basis: 56px; font-size: 8px; }
  471. .mini-dialog .prop-row .val { font-size: 9px; }
  472. .view-tabs {
  473. display: flex;
  474. flex-wrap: wrap;
  475. gap: 8px;
  476. padding: 0 0 14px;
  477. margin-bottom: 4px;
  478. }
  479. .tab-chip {
  480. height: 32px;
  481. padding: 0 14px;
  482. border-radius: 8px;
  483. font-size: 12px;
  484. font-weight: 600;
  485. font-family: inherit;
  486. cursor: pointer;
  487. border: 1px solid var(--border);
  488. background: #fff;
  489. color: var(--text-muted);
  490. transition: background 0.15s, color 0.15s, border-color 0.15s;
  491. }
  492. .tab-chip.is-active {
  493. background: var(--primary);
  494. border-color: var(--primary);
  495. color: #fff;
  496. }
  497. .tab-panel { display: none; }
  498. .tab-panel.is-active { display: block; }
  499. .mini-dialog .tab-chip { height: 20px; padding: 0 8px; font-size: 8px; }
  500. .view-timeline {
  501. padding-left: 18px;
  502. border-left: 2px solid var(--border);
  503. margin-left: 8px;
  504. }
  505. .view-tl-item {
  506. position: relative;
  507. padding: 0 0 16px 14px;
  508. font-size: 13px;
  509. }
  510. .view-tl-item:last-child { padding-bottom: 0; }
  511. .view-tl-item::before {
  512. content: "";
  513. position: absolute;
  514. left: -25px;
  515. top: 4px;
  516. width: 10px;
  517. height: 10px;
  518. border-radius: 50%;
  519. background: var(--primary);
  520. border: 2px solid #fff;
  521. box-shadow: 0 0 0 1px var(--border);
  522. }
  523. .view-tl-item.muted::before { background: #cbd5e1; }
  524. .view-tl-item.future::before { background: #fff; border-color: var(--primary); }
  525. .view-tl-time { font-size: 11px; color: var(--text-muted); margin-bottom: 2px; }
  526. .view-tl-title { font-weight: 600; margin-bottom: 2px; }
  527. .view-tl-desc { font-size: 12px; color: var(--text-muted); line-height: 1.45; }
  528. .mini-dialog .view-tl-item { padding-bottom: 10px; font-size: 9px; }
  529. .mini-dialog .view-tl-time { font-size: 8px; }
  530. .acc-list { display: flex; flex-direction: column; gap: 8px; }
  531. .acc-item {
  532. border: 1px solid var(--border);
  533. border-radius: 8px;
  534. overflow: hidden;
  535. background: #fff;
  536. }
  537. .acc-hd {
  538. width: 100%;
  539. padding: 11px 14px;
  540. border: none;
  541. background: var(--table-head);
  542. font-size: 13px;
  543. font-weight: 600;
  544. font-family: inherit;
  545. color: var(--text);
  546. cursor: pointer;
  547. display: flex;
  548. align-items: center;
  549. justify-content: space-between;
  550. gap: 8px;
  551. text-align: left;
  552. }
  553. .acc-hd::after {
  554. content: "▾";
  555. font-size: 12px;
  556. color: var(--text-muted);
  557. transition: transform 0.15s;
  558. }
  559. .acc-item.is-open .acc-hd::after { transform: rotate(180deg); }
  560. .acc-bd { display: none; padding: 12px 14px 14px; border-top: 1px solid #f1f5f9; }
  561. .acc-item.is-open .acc-bd { display: block; }
  562. .mini-dialog .acc-hd { padding: 6px 8px; font-size: 9px; }
  563. .mini-dialog .acc-bd { padding: 6px 8px; }
  564. .meta-chips {
  565. display: flex;
  566. flex-wrap: wrap;
  567. gap: 8px;
  568. }
  569. .meta-chip {
  570. font-size: 12px;
  571. padding: 6px 10px;
  572. border-radius: 6px;
  573. background: var(--table-head);
  574. border: 1px solid var(--border);
  575. color: var(--text);
  576. font-weight: 600;
  577. }
  578. .meta-chip b {
  579. font-weight: 500;
  580. color: var(--text-muted);
  581. margin-right: 4px;
  582. }
  583. .prose-block {
  584. padding: 14px 16px;
  585. background: var(--table-head);
  586. border: 1px solid var(--border);
  587. border-radius: 8px;
  588. font-size: 13px;
  589. line-height: 1.65;
  590. color: var(--text);
  591. word-break: break-word;
  592. }
  593. .prose-block h4 {
  594. margin: 0 0 8px;
  595. font-size: 12px;
  596. font-weight: 600;
  597. color: var(--text-muted);
  598. }
  599. .mini-dialog .meta-chip { font-size: 8px; padding: 3px 6px; }
  600. .mini-dialog .prose-block { font-size: 8px; padding: 6px 8px; }
  601. .view-route-bar {
  602. display: flex;
  603. align-items: center;
  604. min-height: 88px;
  605. padding: 16px 20px;
  606. border-radius: var(--radius);
  607. border: 1px solid var(--border);
  608. background: linear-gradient(90deg, #ecfdf5 0%, #d1fae5 40%, #a7f3d0 100%);
  609. }
  610. .route-node {
  611. flex: 0 0 auto;
  612. text-align: center;
  613. min-width: 64px;
  614. }
  615. .route-node .dot {
  616. width: 12px;
  617. height: 12px;
  618. border-radius: 50%;
  619. background: #fff;
  620. border: 2px solid var(--primary);
  621. margin: 0 auto 6px;
  622. opacity: 0.55;
  623. }
  624. .route-node.is-done .dot,
  625. .route-node.is-active .dot {
  626. background: var(--primary);
  627. opacity: 1;
  628. box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.2);
  629. }
  630. .route-node .lbl { font-size: 11px; font-weight: 600; color: var(--text); }
  631. .route-node .sub { font-size: 10px; color: var(--text-muted); margin-top: 2px; }
  632. .view-route-line {
  633. flex: 1;
  634. height: 3px;
  635. background: var(--primary);
  636. opacity: 0.35;
  637. border-radius: 2px;
  638. margin: 0 8px 18px;
  639. min-width: 12px;
  640. }
  641. .view-route-line.is-done { opacity: 0.75; }
  642. .mini-dialog .view-route-bar { min-height: 48px; padding: 8px 10px; }
  643. .mini-dialog .route-node .lbl { font-size: 7px; }
  644. .mini-dialog .route-node .dot { width: 8px; height: 8px; }
  645. .view-bento {
  646. display: grid;
  647. grid-template-columns: 1.2fr 0.8fr;
  648. gap: 12px;
  649. }
  650. @media (max-width: 720px) { .view-bento { grid-template-columns: 1fr; } }
  651. .view-card {
  652. border: 1px solid var(--border);
  653. border-radius: var(--radius);
  654. padding: 14px 16px;
  655. background: #fff;
  656. }
  657. .view-card h4 {
  658. margin: 0 0 10px;
  659. font-size: 12px;
  660. font-weight: 700;
  661. color: var(--text-muted);
  662. letter-spacing: 0.04em;
  663. }
  664. .view-card.span-full { grid-column: 1 / -1; }
  665. .mini-dialog .view-bento { grid-template-columns: 1fr 1fr; gap: 6px; }
  666. .mini-dialog .view-card { padding: 6px 8px; }
  667. .mini-dialog .view-card h4 { font-size: 8px; margin-bottom: 4px; }
  668. .note-bar {
  669. margin-top: 24px;
  670. padding: 14px 16px;
  671. background: var(--primary-bg);
  672. border: 1px solid rgba(16, 185, 129, 0.25);
  673. border-radius: var(--radius);
  674. font-size: 13px;
  675. color: var(--primary-dark);
  676. line-height: 1.6;
  677. }
  678. /* mini 预览专用缩小 */
  679. .mini-dialog .kv-grid.cols-3 { grid-template-columns: repeat(3, 1fr); gap: 6px 8px; }
  680. .mini-dialog .kv-grid.cols-4 { grid-template-columns: repeat(4, 1fr); gap: 5px 6px; }
  681. .mini-dialog .kv-item dt { font-size: 9px; }
  682. .mini-dialog .kv-item dd { font-size: 10px; font-weight: 600; }
  683. .mini-dialog .view-section { padding: 10px 12px; }
  684. .mini-dialog .view-section h3 { font-size: 11px; margin-bottom: 8px; }
  685. .mini-dialog .section-cols-2 { grid-template-columns: 1fr 1fr; }
  686. .mini-dialog .card-grid-2x2 { gap: 8px; }
  687. .mini-dialog .info-card { padding: 8px 10px; }
  688. .mini-dialog .info-card h4 { font-size: 10px; margin-bottom: 6px; }
  689. /* ── 少字段稀疏场景 ── */
  690. .sparse-section { margin-top: 32px; }
  691. .kpi-strip {
  692. display: flex;
  693. flex-wrap: wrap;
  694. gap: 12px;
  695. padding: 14px 22px;
  696. background: var(--table-head);
  697. border-bottom: 1px solid var(--border);
  698. }
  699. .kpi-chip {
  700. flex: 1;
  701. min-width: 140px;
  702. padding: 12px 14px;
  703. background: #fff;
  704. border: 1px solid var(--border);
  705. border-radius: 8px;
  706. box-shadow: var(--shadow);
  707. }
  708. .kpi-chip .lbl { font-size: 11px; color: var(--text-muted); margin-bottom: 4px; }
  709. .kpi-chip .val { font-size: 16px; font-weight: 700; color: var(--text); line-height: 1.3; }
  710. .kpi-chip .val.money { color: var(--danger); }
  711. .kpi-chip .val.muted { font-size: 14px; font-weight: 600; color: var(--text-muted); }
  712. .hero-chips {
  713. display: flex;
  714. flex-wrap: wrap;
  715. gap: 8px;
  716. margin-top: 10px;
  717. }
  718. .hero-chip {
  719. font-size: 12px;
  720. padding: 4px 10px;
  721. border-radius: 6px;
  722. background: rgba(255, 255, 255, 0.75);
  723. border: 1px solid rgba(16, 185, 129, 0.2);
  724. color: var(--text);
  725. font-weight: 500;
  726. }
  727. .hero-chip b { font-weight: 600; color: var(--text-muted); margin-right: 4px; }
  728. .kv-panel-centered {
  729. max-width: 680px;
  730. margin: 0 auto;
  731. padding: 20px 22px 24px;
  732. }
  733. .kv-panel-centered .kv-grid { gap: 14px 20px; }
  734. .bento-single {
  735. max-width: 760px;
  736. margin: 0 auto;
  737. padding: 18px 22px 22px;
  738. }
  739. .bento-single .info-card {
  740. background: linear-gradient(180deg, #fff 0%, var(--primary-bg) 100%);
  741. border-color: rgba(16, 185, 129, 0.25);
  742. padding: 18px 20px;
  743. }
  744. .sparse-empty-hint {
  745. margin-top: 14px;
  746. padding: 12px 14px;
  747. border-radius: 8px;
  748. background: var(--table-head);
  749. border: 1px dashed var(--border);
  750. font-size: 12px;
  751. color: var(--text-muted);
  752. text-align: center;
  753. line-height: 1.5;
  754. }
  755. .dialog-compact.w720 { max-width: 720px; }
  756. .dialog-compact .view-section { padding: 16px 20px; }
  757. </style>
  758. </head>
  759. <body>
  760. <div class="page">
  761. <header class="page-head">
  762. <h1><span>宽弹窗</span> · 多列 KV 布局 Demo</h1>
  763. <p>
  764. 弹窗宽度 ≥920px 时,如何在<strong>不牺牲长值可读性</strong>的前提下提高信息密度。
  765. 以下 7 种方案使用同一组 18 个字段(含路径、地址、备注等长值),默认 KV 为<strong>上下堆叠</strong>。
  766. 若偏好<strong>左右表格</strong>(label | value),见「KV 表格模式」;
  767. 指标卡、Tab、时间轴、手风琴等<strong>其他展示形态</strong>见「其他信息展示模式」。
  768. 相关:<a href="fresh-mint-view-kv-layout-demos.html">窄弹窗 KV 对比</a> ·
  769. <a href="fresh-mint-view-dialog-demos.html">弹窗风格 Demo</a> ·
  770. <a href="fresh-mint-view-sparse-kv-demos.html">少字段稀疏场景</a>
  771. </p>
  772. <div class="width-bar">
  773. <span class="width-pill"><b>720px</b> 双列(基准,不在此页)</span>
  774. <span class="width-pill"><b>920px</b> 三列起点</span>
  775. <span class="width-pill"><b>960px</b> 分栏 / 双区块</span>
  776. <span class="width-pill"><b>1040px</b> 四列 / 2×2 卡片</span>
  777. <span class="width-pill"><b>1120px</b> 宽表双组</span>
  778. </div>
  779. </header>
  780. <div class="compare-grid" id="grid"></div>
  781. <section class="demo-section" id="table-section">
  782. <h2>KV 表格模式(左右 label | value)</h2>
  783. <p>
  784. 与上下堆叠栅格相对:每行(或每半行)为<strong>左 label、右 value</strong> 的表格行,类似 <code>el-descriptions</code> 但样式对齐 Fresh Mint。
  785. 短字段可<strong>一行两组</strong>(四列);长字段<strong>独占整行</strong>。少字段时表格更饱满,不易显空。
  786. </p>
  787. <div class="compare-grid" id="table-grid"></div>
  788. </section>
  789. <section class="demo-section" id="alt-section">
  790. <h2>其他信息展示模式</h2>
  791. <p>
  792. 除<strong>上下 KV 栅格</strong>与<strong>左右 KV 表格</strong>外,查看弹窗还可采用指标卡、属性行、Tab 分段、时间轴、手风琴、芯片摘要、路线进度、Bento 卡片等形态。
  793. 样式对齐规范 <code>§9.6</code>(<code>view-timeline</code> / <code>view-route-bar</code> / <code>view-bento</code> 等)。Tab 与手风琴在全屏预览中可点击交互。
  794. </p>
  795. <div class="compare-grid" id="alt-grid"></div>
  796. </section>
  797. <div class="note-bar">
  798. <strong>宽弹窗选型建议:</strong>
  799. 短字段(编码/状态/日期)≥9 个 → 可用 <strong>3~4 列</strong>;
  800. 含长路径/备注 → 长字段必须 <strong>span 跨整行</strong>(方案 C / G);
  801. 字段可自然分组 → 优先 <strong>双区块并排</strong>(D)或 <strong>2×2 卡片</strong>(F),比单纯加列更易扫读;
  802. 需「摘要 + 明细/轨迹」→ 用 <strong>左右分栏</strong>(E),左侧 1 列、右侧 2~3 列。
  803. 偏好<strong>左右对齐、表格感</strong> → 见 <a href="#table-section" style="color:inherit;font-weight:600">KV 表格模式</a>(M~R,推荐 N 双组四列)。
  804. 需<strong>流程/进度/折叠/分段</strong> → 见 <a href="#alt-section" style="color:inherit;font-weight:600">其他展示模式</a>(S~Y)。
  805. <strong>字段 ≤8 个</strong> → 见下方「少字段稀疏场景」或 <a href="fresh-mint-view-sparse-kv-demos.html" style="color:inherit;font-weight:600">独立 Demo 页</a>;表格方案见 Q / R。
  806. </div>
  807. <section class="demo-section sparse-section" id="sparse-section">
  808. <h2>少字段稀疏场景(≤8 个 KV)</h2>
  809. <p>
  810. 1040px 四列在字段少时容易「空荡荡」。常用手段:<strong>① 自适应降列</strong> ② <strong>Hero 下沉 KPI</strong>
  811. ③ <strong>Hero 芯片摘要</strong> ④ <strong>单卡片 Bento</strong> ⑤ <strong>窄弹窗 720px</strong>。
  812. 实现上可在 <code>SystemViewDialog</code> 按 <code>fields.length</code> 动态设 <code>gridColumns</code> 与 <code>width</code>。
  813. </p>
  814. <div class="compare-grid" id="sparse-grid"></div>
  815. </section>
  816. </div>
  817. <div class="backdrop" id="backdrop">
  818. <div class="dialog w920" id="dialog"></div>
  819. </div>
  820. <script>
  821. var FIELDS = {
  822. short: [
  823. { k: "合同编号", v: "HT-2026-SX-00428" },
  824. { k: "合同类型", v: "年度框架" },
  825. { k: "签订日期", v: "2026-06-01" },
  826. { k: "生效日期", v: "2026-06-01" },
  827. { k: "到期日期", v: "2027-05-31" },
  828. { k: "合同状态", v: "已生效", tag: true },
  829. { k: "付款方式", v: "月结 30 天" },
  830. { k: "交货方式", v: "卖方送货" },
  831. { k: "销售负责人", v: "陈经理" },
  832. { k: "归属部门", v: "华东销售一部" },
  833. { k: "币种", v: "CNY" },
  834. { k: "税率", v: "13%" }
  835. ],
  836. long: [
  837. { k: "甲方", v: "山峡化工有限公司" },
  838. { k: "乙方", v: "华东精细化工贸易公司" },
  839. { k: "交货地址", v: "浙江省杭州市滨江区长河街道江南大道 3588 号 · 山峡化工华东仓储中心(北门)" },
  840. { k: "质量标准", v: "GB/T 标准 · 每批次附第三方质检报告 · 不合格品 24 小时内退换" },
  841. { k: "合同金额", v: "¥ 2,580,000.00", money: true },
  842. { k: "备注", v: "本合同为年度框架,具体订单以 PO 为准;价格锁定至 2026-12-31,逾期重新询价。紧急联系人:刘总 139****8821。" }
  843. ]
  844. };
  845. var ALL_SHORT = FIELDS.short;
  846. var ALL_LONG = FIELDS.long;
  847. /** 备用金申请等少字段场景(7 个) */
  848. var SPARSE_FIELDS = [
  849. { k: "申请人", v: "张三" },
  850. { k: "申请原因", v: "项目现场零星采购备用" },
  851. { k: "申请金额(元)", v: "¥ 5,000.00", money: true },
  852. { k: "实际发放(元)", v: "—", muted: true },
  853. { k: "当前节点", v: "部门审批", tag: true },
  854. { k: "申请时间", v: "2026-06-18 14:32" },
  855. { k: "冲抵总额(元)", v: "¥ 0.00", money: true }
  856. ];
  857. function suggestCols(n) {
  858. if (n <= 4) return 2;
  859. if (n <= 8) return 3;
  860. return 4;
  861. }
  862. function suggestWidth(n) {
  863. if (n <= 6) return "w720";
  864. if (n <= 10) return "w920";
  865. return "w1040";
  866. }
  867. function esc(s) {
  868. var d = document.createElement("div");
  869. d.textContent = s;
  870. return d.innerHTML;
  871. }
  872. function kvItem(f, spanClass) {
  873. var val = f.tag ? '<span class="tag">' + esc(f.v) + "</span>" : esc(f.v);
  874. if (f.money) val = '<span style="color:var(--danger)">' + val + "</span>";
  875. if (f.muted) val = '<span style="color:var(--text-muted);font-weight:600">' + val + "</span>";
  876. var span = spanClass ? " " + spanClass : "";
  877. return '<div class="kv-item' + span + '"><dt>' + esc(f.k) + "</dt><dd>" + val + "</dd></div>";
  878. }
  879. function kvGrid(items, cols, spanFn) {
  880. var html = '<dl class="kv-grid cols-' + cols + '">';
  881. items.forEach(function (f) {
  882. html += kvItem(f, spanFn ? spanFn(f) : "");
  883. });
  884. return html + "</dl>";
  885. }
  886. function section(title, body) {
  887. return '<div class="view-section"><h3>' + esc(title) + "</h3>" + body + "</div>";
  888. }
  889. var LONG_LABEL_RE = /备注|地址|描述|说明|条款|原因|摘要|内容|标准|路径|意向/;
  890. function isLongField(f) {
  891. var text = f.v == null ? "" : String(f.v);
  892. if (text.length >= 17) return true;
  893. if (LONG_LABEL_RE.test(f.k)) return true;
  894. return false;
  895. }
  896. function formatFieldVal(f) {
  897. if (f.tag) return '<span class="tag">' + esc(f.v) + "</span>";
  898. if (f.money) return '<span class="money-val">' + esc(f.v) + "</span>";
  899. if (f.muted) return '<span class="muted-val">' + esc(f.v) + "</span>";
  900. return esc(f.v);
  901. }
  902. function kvTableWrap(inner) {
  903. return '<div class="kv-table-wrap">' + inner + "</div>";
  904. }
  905. /** 每行一组:label | value */
  906. function kvTablePair(items) {
  907. var html = '<table class="kv-table kv-table--pair"><tbody>';
  908. items.forEach(function (f) {
  909. html +=
  910. '<tr' +
  911. (isLongField(f) ? ' class="row-long"' : "") +
  912. "><th>" +
  913. esc(f.k) +
  914. "</th><td>" +
  915. formatFieldVal(f) +
  916. "</td></tr>";
  917. });
  918. return kvTableWrap(html + "</tbody></table>");
  919. }
  920. /** 每行两组短字段;长字段独占一行(value 跨 3 列) */
  921. function kvTableDual(items) {
  922. var html = '<table class="kv-table kv-table--dual"><tbody>';
  923. var i = 0;
  924. while (i < items.length) {
  925. var f = items[i];
  926. if (isLongField(f)) {
  927. html +=
  928. '<tr class="row-long"><th>' +
  929. esc(f.k) +
  930. '</th><td colspan="3">' +
  931. formatFieldVal(f) +
  932. "</td></tr>";
  933. i += 1;
  934. continue;
  935. }
  936. html += "<tr><th>" + esc(f.k) + "</th><td>" + formatFieldVal(f) + "</td>";
  937. i += 1;
  938. if (i < items.length && !isLongField(items[i])) {
  939. html += "<th>" + esc(items[i].k) + "</th><td>" + formatFieldVal(items[i]) + "</td>";
  940. i += 1;
  941. } else {
  942. html += "<th></th><td></td>";
  943. }
  944. html += "</tr>";
  945. }
  946. return kvTableWrap(html + "</tbody></table>");
  947. }
  948. /** 斑马纹:无竖线,偶数行浅底(两列) */
  949. function kvTableZebraPair(items) {
  950. var html = '<table class="kv-table kv-table--zebra kv-table--pair"><tbody>';
  951. items.forEach(function (f) {
  952. html +=
  953. '<tr' +
  954. (isLongField(f) ? ' class="row-long"' : "") +
  955. "><th>" +
  956. esc(f.k) +
  957. "</th><td>" +
  958. formatFieldVal(f) +
  959. "</td></tr>";
  960. });
  961. return kvTableWrap(html + "</tbody></table>");
  962. }
  963. /** 斑马纹 + 双组四列 */
  964. function kvTableZebraDual(items) {
  965. var html = '<table class="kv-table kv-table--zebra kv-table--dual"><tbody>';
  966. var i = 0;
  967. while (i < items.length) {
  968. var f = items[i];
  969. if (isLongField(f)) {
  970. html +=
  971. '<tr class="row-long"><th>' +
  972. esc(f.k) +
  973. '</th><td colspan="3">' +
  974. formatFieldVal(f) +
  975. "</td></tr>";
  976. i += 1;
  977. continue;
  978. }
  979. html += "<tr><th>" + esc(f.k) + "</th><td>" + formatFieldVal(f) + "</td>";
  980. i += 1;
  981. if (i < items.length && !isLongField(items[i])) {
  982. html += "<th>" + esc(items[i].k) + "</th><td>" + formatFieldVal(items[i]) + "</td>";
  983. i += 1;
  984. } else {
  985. html += "<th></th><td></td>";
  986. }
  987. html += "</tr>";
  988. }
  989. return kvTableWrap(html + "</tbody></table>");
  990. }
  991. var CONTRACT_TIMELINE = [
  992. { time: "2026-05-20 10:00", title: "合同起草", desc: "陈经理创建年度框架合同草稿" },
  993. { time: "2026-05-28 16:30", title: "审批通过", desc: "法务、财务会签完成" },
  994. { time: "2026-06-01 09:00", title: "签订生效", desc: "双方盖章,合同状态变更为已生效", active: true },
  995. { time: "2027-05-31", title: "合同到期", desc: "到期前 30 天触发续签提醒", future: true }
  996. ];
  997. function renderStatRow(items) {
  998. var html = '<div class="stat-row">';
  999. items.forEach(function (f) {
  1000. html +=
  1001. '<div class="stat-cell"><div class="lbl">' +
  1002. esc(f.k) +
  1003. '</div><div class="val' +
  1004. (f.money ? " money" : "") +
  1005. '">' +
  1006. (f.tag ? '<span class="tag">' + esc(f.v) + "</span>" : esc(f.v)) +
  1007. "</div></div>";
  1008. });
  1009. return html + "</div>";
  1010. }
  1011. function renderPropList(items) {
  1012. var html = '<div class="prop-list">';
  1013. items.forEach(function (f) {
  1014. html +=
  1015. '<div class="prop-row"><span class="lbl">' +
  1016. esc(f.k) +
  1017. '</span><span class="val' +
  1018. (f.money ? " money" : "") +
  1019. '">' +
  1020. formatFieldVal(f) +
  1021. "</span></div>";
  1022. });
  1023. return html + "</div>";
  1024. }
  1025. function renderTabs(panels, groupId) {
  1026. var gid = groupId || "tabs";
  1027. var html = '<div class="view-tabs" role="tablist">';
  1028. panels.forEach(function (p, idx) {
  1029. html +=
  1030. '<button type="button" class="tab-chip' +
  1031. (idx === 0 ? " is-active" : "") +
  1032. '" role="tab" data-tab-group="' +
  1033. gid +
  1034. '" data-tab="' +
  1035. idx +
  1036. '">' +
  1037. esc(p.title) +
  1038. "</button>";
  1039. });
  1040. html += "</div>";
  1041. panels.forEach(function (p, idx) {
  1042. html +=
  1043. '<div class="tab-panel' +
  1044. (idx === 0 ? " is-active" : "") +
  1045. '" role="tabpanel" data-tab-group="' +
  1046. gid +
  1047. '" data-tab-panel="' +
  1048. idx +
  1049. '">' +
  1050. p.body +
  1051. "</div>";
  1052. });
  1053. return html;
  1054. }
  1055. function renderTimeline(items) {
  1056. var html = '<div class="view-timeline">';
  1057. items.forEach(function (it) {
  1058. var cls = it.future ? " future" : it.muted ? " muted" : it.active ? " active" : "";
  1059. html +=
  1060. '<div class="view-tl-item' +
  1061. cls +
  1062. '"><div class="view-tl-time">' +
  1063. esc(it.time) +
  1064. '</div><div class="view-tl-title">' +
  1065. esc(it.title) +
  1066. '</div><div class="view-tl-desc">' +
  1067. esc(it.desc) +
  1068. "</div></div>";
  1069. });
  1070. return html + "</div>";
  1071. }
  1072. function renderAccordion(sections) {
  1073. var html = '<div class="acc-list">';
  1074. sections.forEach(function (sec, idx) {
  1075. html +=
  1076. '<div class="acc-item' +
  1077. (idx === 0 ? " is-open" : "") +
  1078. '"><button type="button" class="acc-hd" data-acc-toggle>' +
  1079. esc(sec.title) +
  1080. '</button><div class="acc-bd">' +
  1081. sec.body +
  1082. "</div></div>";
  1083. });
  1084. return html + "</div>";
  1085. }
  1086. function renderMetaChips(items) {
  1087. var html = '<div class="meta-chips">';
  1088. items.forEach(function (f) {
  1089. html += '<span class="meta-chip"><b>' + esc(f.k) + "</b>" + formatFieldVal(f) + "</span>";
  1090. });
  1091. return html + "</div>";
  1092. }
  1093. function renderProseBlock(title, text) {
  1094. return '<div class="prose-block"><h4>' + esc(title) + "</h4>" + esc(text) + "</div>";
  1095. }
  1096. function renderRouteBar(nodes) {
  1097. var html = '<div class="view-route-bar">';
  1098. nodes.forEach(function (node, idx) {
  1099. if (idx > 0) {
  1100. html += '<div class="view-route-line' + (node.done || node.active ? " is-done" : "") + '"></div>';
  1101. }
  1102. html +=
  1103. '<div class="route-node' +
  1104. (node.done ? " is-done" : "") +
  1105. (node.active ? " is-active" : "") +
  1106. '"><div class="dot"></div><div class="lbl">' +
  1107. esc(node.label) +
  1108. '</div><div class="sub">' +
  1109. esc(node.sub || "") +
  1110. "</div></div>";
  1111. });
  1112. return html + "</div>";
  1113. }
  1114. function renderBento(cards) {
  1115. var html = '<div class="view-bento">';
  1116. cards.forEach(function (c) {
  1117. html +=
  1118. '<div class="view-card' +
  1119. (c.spanFull ? " span-full" : "") +
  1120. '"><h4>' +
  1121. esc(c.title) +
  1122. "</h4>" +
  1123. c.body +
  1124. "</div>";
  1125. });
  1126. return html + "</div>";
  1127. }
  1128. var LAYOUTS = [
  1129. {
  1130. id: "cols3",
  1131. name: "A · 三列上下 KV",
  1132. width: "w920",
  1133. widthLabel: "920px",
  1134. badge: "推荐",
  1135. desc: "宽弹窗最常用。18 个短字段 3 列 × 6 行;长字段另开区块 span 整行。",
  1136. meta: "字段 12~24 · 920px",
  1137. previewClass: "w920",
  1138. render: function () {
  1139. var s = kvGrid(ALL_SHORT, 3);
  1140. var l = kvGrid(ALL_LONG, 3, function () { return "span-all"; });
  1141. return section("合同要素", s) + section("主体与条款(长字段整行)", l);
  1142. }
  1143. },
  1144. {
  1145. id: "cols4",
  1146. name: "B · 四列上下 KV",
  1147. width: "w1040",
  1148. widthLabel: "1040px",
  1149. badge: "高密度",
  1150. desc: "仅当字段几乎全是短值(编码/日期/枚举)时使用。1040px 下每格 ~240px。",
  1151. meta: "短字段 ≥16 · 1040px",
  1152. previewClass: "w1040",
  1153. render: function () {
  1154. var mix = ALL_SHORT.concat(ALL_LONG.filter(function (f) { return f.k !== "备注"; }));
  1155. var html = kvGrid(mix, 4);
  1156. var remark = ALL_LONG.find(function (f) { return f.k === "备注"; });
  1157. html = html.replace("</dl>", kvItem(remark, "span-all") + "</dl>");
  1158. return section("合同全量字段", html);
  1159. }
  1160. },
  1161. {
  1162. id: "cols3-span2",
  1163. name: "C · 三列 + 长值跨 2 列",
  1164. width: "w920",
  1165. widthLabel: "920px",
  1166. badge: "折中",
  1167. desc: "中等长度(甲方/乙方/地址)占 2 列宽;超长备注仍整行。比全 span 省高度。",
  1168. meta: "长短混合 · 920px",
  1169. previewClass: "w920",
  1170. render: function () {
  1171. var html = '<dl class="kv-grid cols-3">';
  1172. ALL_SHORT.forEach(function (f) { html += kvItem(f); });
  1173. ALL_LONG.forEach(function (f) {
  1174. var span = f.k === "备注" ? "span-all" : "span-2";
  1175. html += kvItem(f, span);
  1176. });
  1177. html += "</dl>";
  1178. return section("合同信息", html);
  1179. }
  1180. },
  1181. {
  1182. id: "section-2col",
  1183. name: "D · 双区块并排(各 2 列 KV)",
  1184. width: "w960",
  1185. widthLabel: "960px",
  1186. badge: "推荐",
  1187. desc: "按业务语义拆成左右两栏(如「基本信息|商务条款」),每栏内部 2 列 KV。比 4 列更易分组扫读。",
  1188. meta: "可自然分组 · 960px",
  1189. previewClass: "w960",
  1190. render: function () {
  1191. var left = ALL_SHORT.slice(0, 6).concat(ALL_LONG.filter(function (f) { return f.k === "甲方" || f.k === "乙方"; }));
  1192. var right = ALL_SHORT.slice(6).concat(ALL_LONG.filter(function (f) { return f.k !== "甲方" && f.k !== "乙方"; }));
  1193. return (
  1194. '<div class="section-cols-2">' +
  1195. section("基本信息", kvGrid(left, 2, function (f) { return f.k === "备注" || f.k === "交货地址" || f.k === "质量标准" ? "span-2" : ""; })) +
  1196. section("商务与条款", kvGrid(right, 2, function (f) { return f.k === "备注" || f.k === "交货地址" || f.k === "质量标准" ? "span-2" : ""; })) +
  1197. "</div>"
  1198. );
  1199. }
  1200. },
  1201. {
  1202. id: "split",
  1203. name: "E · 左摘要 + 右三列",
  1204. width: "w960",
  1205. widthLabel: "960px",
  1206. badge: "分栏",
  1207. desc: "左 340px 放核心摘要(1 列 KV),右栏 3 列放其余。对应 SystemViewDialog split 布局。",
  1208. meta: "摘要+明细 · 960px",
  1209. previewClass: "w960",
  1210. render: function () {
  1211. var leftFields = [
  1212. { k: "合同编号", v: "HT-2026-SX-00428" },
  1213. { k: "合同状态", v: "已生效", tag: true },
  1214. { k: "合同金额", v: "¥ 2,580,000.00", money: true },
  1215. { k: "签订日期", v: "2026-06-01" },
  1216. { k: "销售负责人", v: "陈经理" },
  1217. { k: "甲方", v: "山峡化工有限公司" },
  1218. { k: "乙方", v: "华东精细化工贸易公司" }
  1219. ];
  1220. var rightFields = ALL_SHORT.slice(1).filter(function (f) { return f.k !== "合同状态"; })
  1221. .concat(ALL_LONG.filter(function (f) { return f.k !== "甲方" && f.k !== "乙方" && f.k !== "合同金额"; }));
  1222. return (
  1223. '<div class="view-split">' +
  1224. '<div class="view-split-left">' + section("核心摘要", kvGrid(leftFields, 1)) + "</div>" +
  1225. '<div class="view-split-right">' + section("完整要素", kvGrid(rightFields, 3, function (f) {
  1226. return f.k === "备注" || f.k === "交货地址" ? "span-all" : f.k === "质量标准" ? "span-2" : "";
  1227. })) + "</div>" +
  1228. "</div>"
  1229. );
  1230. }
  1231. },
  1232. {
  1233. id: "card-2x2",
  1234. name: "F · 2×2 卡片宫格",
  1235. width: "w1040",
  1236. widthLabel: "1040px",
  1237. badge: "分组",
  1238. desc: "4 张主题卡片(身份/时间/商务/条款),卡内 2 列 KV。适合 CRM 客户/合同等多维度档案。",
  1239. meta: "多维度 · 1040px",
  1240. previewClass: "w1040",
  1241. render: function () {
  1242. var groups = [
  1243. { title: "身份标识", items: [ALL_SHORT[0], ALL_SHORT[1], ALL_SHORT[5], ALL_LONG[0], ALL_LONG[1]] },
  1244. { title: "时间周期", items: ALL_SHORT.slice(2, 5) },
  1245. { title: "商务条款", items: [ALL_SHORT[6], ALL_SHORT[7], ALL_SHORT[10], ALL_SHORT[11], ALL_LONG[4]] },
  1246. { title: "交付与备注", items: [ALL_SHORT[8], ALL_SHORT[9], ALL_LONG[2], ALL_LONG[3], ALL_LONG[5]] }
  1247. ];
  1248. var html = '<div class="view-section"><div class="card-grid-2x2">';
  1249. groups.forEach(function (g) {
  1250. html += '<div class="info-card"><h4>' + esc(g.title) + "</h4>" +
  1251. kvGrid(g.items, 2, function (f) {
  1252. return f.k === "备注" || f.k === "交货地址" ? "span-2" : "";
  1253. }) + "</div>";
  1254. });
  1255. return html + "</div></div>";
  1256. }
  1257. },
  1258. {
  1259. id: "wide-table",
  1260. name: "G · 宽表双组(一行 4 格)",
  1261. width: "w1120",
  1262. widthLabel: "1120px",
  1263. badge: "表格感",
  1264. desc: "1120px 下一行展示两组 label-value(共 4 列),label 窄列对齐。同质字段极多时的替代方案。",
  1265. meta: "18+ 同质字段 · 1120px",
  1266. previewClass: "w1120",
  1267. render: function () {
  1268. var all = ALL_SHORT.concat(ALL_LONG);
  1269. var html = '<div class="view-section"><table class="kv-wide-table"><tbody>';
  1270. for (var i = 0; i < all.length; i += 2) {
  1271. html += "<tr>";
  1272. html += '<td class="lbl">' + esc(all[i].k) + '</td><td class="val">' + (all[i].tag ? '<span class="tag">' + esc(all[i].v) + "</span>" : esc(all[i].v)) + "</td>";
  1273. if (all[i + 1]) {
  1274. html += '<td class="lbl">' + esc(all[i + 1].k) + '</td><td class="val">' + (all[i + 1].tag ? '<span class="tag">' + esc(all[i + 1].v) + "</span>" : esc(all[i + 1].v)) + "</td>";
  1275. } else {
  1276. html += '<td class="lbl"></td><td class="val"></td>';
  1277. }
  1278. html += "</tr>";
  1279. }
  1280. return html + "</tbody></table></div>";
  1281. }
  1282. }
  1283. ];
  1284. var TABLE_LAYOUTS = [
  1285. {
  1286. id: "table-pair",
  1287. name: "M · 标准两列表格",
  1288. width: "w920",
  1289. widthLabel: "920px",
  1290. badge: "表格",
  1291. desc: "每行一组:左 label(灰底固定宽)| 右 value。长字段仍独占一行,适合字段不多或需逐行阅读。",
  1292. meta: "18 字段 · 1 组/行",
  1293. previewClass: "w920",
  1294. tableMode: true,
  1295. render: function () {
  1296. return section("合同信息", kvTablePair(ALL_SHORT.concat(ALL_LONG)));
  1297. }
  1298. },
  1299. {
  1300. id: "table-dual",
  1301. name: "N · 双组四列表格",
  1302. width: "w1040",
  1303. widthLabel: "1040px",
  1304. badge: "推荐",
  1305. desc: "一行展示两组 label|value(共四列);备注/地址等长字段自动独占整行。信息密度高且对齐清晰。",
  1306. meta: "18 字段 · 2 组/行",
  1307. previewClass: "w1040",
  1308. tableMode: true,
  1309. render: function () {
  1310. return section("合同全量字段", kvTableDual(ALL_SHORT.concat(ALL_LONG)));
  1311. }
  1312. },
  1313. {
  1314. id: "table-split-section",
  1315. name: "O · 分区 + 双组表",
  1316. width: "w1040",
  1317. widthLabel: "1040px",
  1318. badge: "分组",
  1319. desc: "「合同要素」「主体与条款」各一张表,双组四列。比单表更易扫读分组。",
  1320. meta: "分组表格 · 1040px",
  1321. previewClass: "w1040",
  1322. tableMode: true,
  1323. render: function () {
  1324. return (
  1325. section("合同要素", kvTableDual(ALL_SHORT)) +
  1326. section("主体与条款", kvTableDual(ALL_LONG))
  1327. );
  1328. }
  1329. },
  1330. {
  1331. id: "table-zebra-dual",
  1332. name: "P · 斑马纹双组表",
  1333. width: "w1040",
  1334. widthLabel: "1040px",
  1335. badge: "轻量",
  1336. desc: "无双竖线,偶数行浅灰底;仍为一行两组。视觉比全边框表更轻。",
  1337. meta: "斑马纹 · 1040px",
  1338. previewClass: "w1040",
  1339. tableMode: true,
  1340. render: function () {
  1341. return section("合同全量字段", kvTableZebraDual(ALL_SHORT.concat(ALL_LONG)));
  1342. }
  1343. },
  1344. {
  1345. id: "table-pair-sparse",
  1346. name: "Q · 少字段两列表格",
  1347. width: "w720",
  1348. widthLabel: "720px",
  1349. badge: "推荐",
  1350. desc: "7 个字段用标准两列表格 + 720px 弹窗,行数适中、不空荡,报销/审批简详情首选表格方案。",
  1351. meta: "7 字段 · 720px",
  1352. previewClass: "w920",
  1353. sparse: true,
  1354. tableMode: true,
  1355. render: function () {
  1356. return section("申请信息", kvTablePair(SPARSE_FIELDS));
  1357. }
  1358. },
  1359. {
  1360. id: "table-dual-sparse",
  1361. name: "R · 少字段双组表",
  1362. width: "w920",
  1363. widthLabel: "920px",
  1364. badge: "表格",
  1365. desc: "少字段时双组四列(4 行左右),比四列上下 KV 更饱满。",
  1366. meta: "7 字段 · 双组",
  1367. previewClass: "w920",
  1368. sparse: true,
  1369. tableMode: true,
  1370. render: function () {
  1371. return section("申请信息", kvTableDual(SPARSE_FIELDS));
  1372. }
  1373. }
  1374. ];
  1375. var ALT_LAYOUTS = [
  1376. {
  1377. id: "alt-stat-props",
  1378. name: "S · 指标卡 + 属性行",
  1379. width: "w1040",
  1380. widthLabel: "1040px",
  1381. badge: "推荐",
  1382. desc: "顶部 4 格核心指标(金额/状态/日期),下方属性行列表展示其余字段。适合「先抓重点、再扫明细」。",
  1383. meta: "指标 + 列表 · 1040px",
  1384. previewClass: "w1040",
  1385. altMode: true,
  1386. render: function () {
  1387. var stats = [
  1388. ALL_LONG.find(function (f) { return f.k === "合同金额"; }),
  1389. ALL_SHORT.find(function (f) { return f.k === "合同状态"; }),
  1390. ALL_SHORT.find(function (f) { return f.k === "签订日期"; }),
  1391. ALL_SHORT.find(function (f) { return f.k === "到期日期"; })
  1392. ];
  1393. var rest = ALL_SHORT.filter(function (f) {
  1394. return f.k !== "合同状态" && f.k !== "签订日期" && f.k !== "到期日期";
  1395. }).concat(ALL_LONG.filter(function (f) { return f.k !== "合同金额"; }));
  1396. return section("核心指标", renderStatRow(stats)) + section("合同明细", renderPropList(rest));
  1397. }
  1398. },
  1399. {
  1400. id: "alt-tabs",
  1401. name: "T · Tab 分段切换",
  1402. width: "w1040",
  1403. widthLabel: "1040px",
  1404. badge: "分段",
  1405. desc: "字段按业务语义拆成 Tab(基本信息 / 商务条款 / 主体备注),单屏信息量可控,全屏可点击切换。",
  1406. meta: "3 Tab · 1040px",
  1407. previewClass: "w1040",
  1408. altMode: true,
  1409. interactive: true,
  1410. render: function () {
  1411. return section(
  1412. "合同信息",
  1413. renderTabs(
  1414. [
  1415. { title: "基本信息", body: kvGrid(ALL_SHORT.slice(0, 6), 2) },
  1416. { title: "商务条款", body: kvGrid(ALL_SHORT.slice(6).concat(ALL_LONG.filter(function (f) { return f.k === "合同金额" || f.k === "质量标准"; })), 2, function (f) { return f.k === "质量标准" ? "span-2" : ""; }) },
  1417. { title: "主体备注", body: renderPropList(ALL_LONG.filter(function (f) { return f.k !== "合同金额" && f.k !== "质量标准"; })) }
  1418. ],
  1419. "contract"
  1420. )
  1421. );
  1422. }
  1423. },
  1424. {
  1425. id: "alt-timeline",
  1426. name: "U · 时间轴 + 精简属性",
  1427. width: "w920",
  1428. widthLabel: "920px",
  1429. badge: "流程",
  1430. desc: "左侧时间轴展示合同生命周期节点,右侧或下方属性行补充当前态字段。适合审批/履约类详情。",
  1431. meta: "时间轴 · 920px",
  1432. previewClass: "w920",
  1433. altMode: true,
  1434. render: function () {
  1435. var brief = [
  1436. ALL_SHORT[0],
  1437. ALL_SHORT[5],
  1438. ALL_LONG[0],
  1439. ALL_LONG[1]
  1440. ];
  1441. return (
  1442. section("履约进度", renderTimeline(CONTRACT_TIMELINE)) +
  1443. section("当前摘要", renderPropList(brief))
  1444. );
  1445. }
  1446. },
  1447. {
  1448. id: "alt-accordion",
  1449. name: "V · 手风琴折叠",
  1450. width: "w1040",
  1451. widthLabel: "1040px",
  1452. badge: "折叠",
  1453. desc: "按分组折叠次要信息,默认展开第一组。字段多、分组清晰时减少首屏高度,全屏可展开/收起。",
  1454. meta: "3 组折叠 · 1040px",
  1455. previewClass: "w1040",
  1456. altMode: true,
  1457. interactive: true,
  1458. render: function () {
  1459. return section(
  1460. "合同信息",
  1461. renderAccordion([
  1462. { title: "身份与时间(6 项)", body: kvGrid(ALL_SHORT.slice(0, 6), 2) },
  1463. { title: "商务与交付(6 项)", body: kvGrid(ALL_SHORT.slice(6).concat(ALL_LONG.filter(function (f) { return f.k === "交货地址"; })), 2, function (f) { return f.k === "交货地址" ? "span-2" : ""; }) },
  1464. { title: "主体、金额与备注(6 项)", body: renderPropList(ALL_LONG.filter(function (f) { return f.k !== "交货地址"; })) }
  1465. ])
  1466. );
  1467. }
  1468. },
  1469. {
  1470. id: "alt-chips-prose",
  1471. name: "W · 芯片摘要 + 正文块",
  1472. width: "w920",
  1473. widthLabel: "920px",
  1474. badge: "轻量",
  1475. desc: "短字段收成芯片行(类 Hero 下沉),长文本(备注/条款)用独立正文块展示,阅读感更接近文档。",
  1476. meta: "芯片 + 正文 · 920px",
  1477. previewClass: "w920",
  1478. altMode: true,
  1479. render: function () {
  1480. var chips = ALL_SHORT.filter(function (f) {
  1481. return f.k !== "合同状态";
  1482. }).slice(0, 8);
  1483. return (
  1484. section("关键要素", renderMetaChips(chips.concat([ALL_SHORT[5]]))) +
  1485. section("质量标准", renderProseBlock("质量标准", ALL_LONG.find(function (f) { return f.k === "质量标准"; }).v)) +
  1486. section("备注说明", renderProseBlock("备注", ALL_LONG.find(function (f) { return f.k === "备注"; }).v))
  1487. );
  1488. }
  1489. },
  1490. {
  1491. id: "alt-route",
  1492. name: "X · 路线进度条 + 属性行",
  1493. width: "w1040",
  1494. widthLabel: "1040px",
  1495. badge: "进度",
  1496. desc: "顶部路线条展示合同阶段(起草→审批→签订→履行→到期),下方属性行列关键字段。对齐 view-route-bar。",
  1497. meta: "路线条 · 1040px",
  1498. previewClass: "w1040",
  1499. altMode: true,
  1500. render: function () {
  1501. var route = [
  1502. { label: "起草", sub: "05-20", done: true },
  1503. { label: "审批", sub: "05-28", done: true },
  1504. { label: "签订", sub: "06-01", done: true, active: true },
  1505. { label: "履行", sub: "进行中" },
  1506. { label: "到期", sub: "2027-05" }
  1507. ];
  1508. var props = [
  1509. ALL_SHORT[0],
  1510. ALL_SHORT[5],
  1511. ALL_LONG[4],
  1512. ALL_SHORT[2],
  1513. ALL_SHORT[4],
  1514. ALL_LONG[0],
  1515. ALL_LONG[1]
  1516. ];
  1517. return section("合同阶段", renderRouteBar(route)) + section("合同要素", renderPropList(props));
  1518. }
  1519. },
  1520. {
  1521. id: "alt-bento",
  1522. name: "Y · Bento 不均等卡片",
  1523. width: "w1040",
  1524. widthLabel: "1040px",
  1525. badge: "卡片",
  1526. desc: "左大右小 Bento 布局:左侧主体信息占宽列,右侧堆叠时间/金额小卡;底部长备注横跨整行。",
  1527. meta: "Bento · 1040px",
  1528. previewClass: "w1040",
  1529. altMode: true,
  1530. render: function () {
  1531. return section(
  1532. "合同档案",
  1533. renderBento([
  1534. {
  1535. title: "主体信息",
  1536. body: renderPropList(ALL_LONG.filter(function (f) { return f.k === "甲方" || f.k === "乙方" || f.k === "交货地址"; }))
  1537. },
  1538. {
  1539. title: "金额与周期",
  1540. body:
  1541. renderStatRow([
  1542. ALL_LONG.find(function (f) { return f.k === "合同金额"; }),
  1543. ALL_SHORT.find(function (f) { return f.k === "合同状态"; }),
  1544. ALL_SHORT.find(function (f) { return f.k === "签订日期"; }),
  1545. ALL_SHORT.find(function (f) { return f.k === "到期日期"; })
  1546. ])
  1547. },
  1548. {
  1549. title: "备注",
  1550. spanFull: true,
  1551. body: renderProseBlock("备注", ALL_LONG.find(function (f) { return f.k === "备注"; }).v)
  1552. }
  1553. ])
  1554. );
  1555. }
  1556. },
  1557. {
  1558. id: "alt-stat-sparse",
  1559. name: "Z · 少字段指标 + 属性行",
  1560. width: "w720",
  1561. widthLabel: "720px",
  1562. badge: "推荐",
  1563. desc: "备用金等少字段场景:顶部 3 指标 + 下方属性行,比四列 KV 更饱满,比表格更轻。",
  1564. meta: "7 字段 · 720px",
  1565. previewClass: "w920",
  1566. sparse: true,
  1567. altMode: true,
  1568. render: function () {
  1569. var stats = SPARSE_FIELDS.filter(function (f) {
  1570. return f.k === "申请金额(元)" || f.k === "当前节点" || f.k === "冲抵总额(元)";
  1571. });
  1572. var rest = SPARSE_FIELDS.filter(function (f) {
  1573. return stats.indexOf(f) === -1;
  1574. });
  1575. return section("申请概览", renderStatRow(stats)) + section("申请明细", renderPropList(rest));
  1576. }
  1577. }
  1578. ];
  1579. var SPARSE_LAYOUTS = [
  1580. {
  1581. id: "sparse-auto-cols",
  1582. name: "H · 自适应降列 + 居中窄区",
  1583. width: "w1040",
  1584. widthLabel: "1040 壳 / 2列 KV",
  1585. badge: "推荐",
  1586. desc: "弹窗仍 1040,KV 区 max-width 680 居中,字段 ≤8 时自动 2 列(非 4 列)。视觉不「铺满空格子」。",
  1587. meta: "7 字段 · 2 列",
  1588. previewClass: "w920",
  1589. sparse: true,
  1590. render: function () {
  1591. var cols = suggestCols(SPARSE_FIELDS.length);
  1592. var html = kvGrid(SPARSE_FIELDS, cols, function (f) {
  1593. return f.k === "申请原因" ? "span-all" : "";
  1594. });
  1595. return '<div class="kv-panel-centered">' + section("申请信息", html) + "</div>";
  1596. }
  1597. },
  1598. {
  1599. id: "sparse-kpi",
  1600. name: "I · Hero KPI 条 + 精简 KV",
  1601. width: "w1040",
  1602. widthLabel: "1040px",
  1603. badge: "推荐",
  1604. desc: "金额/状态/节点提到 Hero 下 KPI 三格;正文只留 3~4 个补充字段,2 列 KV。",
  1605. meta: "7 字段 · KPI 下沉",
  1606. previewClass: "w920",
  1607. sparse: true,
  1608. render: function () {
  1609. var kpi =
  1610. '<div class="kpi-strip">' +
  1611. '<div class="kpi-chip"><div class="lbl">申请金额(元)</div><div class="val money">¥ 5,000.00</div></div>' +
  1612. '<div class="kpi-chip"><div class="lbl">当前节点</div><div class="val"><span class="tag">部门审批</span></div></div>' +
  1613. '<div class="kpi-chip"><div class="lbl">冲抵总额(元)</div><div class="val money">¥ 0.00</div></div>' +
  1614. "</div>";
  1615. var rest = SPARSE_FIELDS.filter(function (f) {
  1616. return f.k !== "申请金额(元)" && f.k !== "当前节点" && f.k !== "冲抵总额(元)";
  1617. });
  1618. return kpi + section("其他信息", kvGrid(rest, 2, function (f) {
  1619. return f.k === "申请原因" ? "span-all" : "";
  1620. }));
  1621. }
  1622. },
  1623. {
  1624. id: "sparse-chips",
  1625. name: "J · Hero 芯片摘要",
  1626. width: "w1040",
  1627. widthLabel: "1040px",
  1628. badge: "轻量",
  1629. desc: "次要短字段(时间/节点/冲抵)收成 Hero 下 chip 行;正文仅保留人、原因等 2~3 项。",
  1630. meta: "7 字段 · chip",
  1631. previewClass: "w920",
  1632. sparse: true,
  1633. renderHeroExtra: function () {
  1634. return (
  1635. '<div class="hero-chips">' +
  1636. '<span class="hero-chip"><b>节点</b>部门审批</span>' +
  1637. '<span class="hero-chip"><b>时间</b>2026-06-18 14:32</span>' +
  1638. '<span class="hero-chip"><b>冲抵</b>¥ 0.00</span>' +
  1639. "</div>"
  1640. );
  1641. },
  1642. render: function () {
  1643. var core = SPARSE_FIELDS.filter(function (f) {
  1644. return f.k === "申请人" || f.k === "申请原因" || f.k === "实际发放(元)";
  1645. });
  1646. return section("申请信息", kvGrid(core, 2, function (f) {
  1647. return f.k === "申请原因" ? "span-all" : "";
  1648. }));
  1649. }
  1650. },
  1651. {
  1652. id: "sparse-bento",
  1653. name: "K · 单卡片 Bento",
  1654. width: "w920",
  1655. widthLabel: "920px",
  1656. badge: "卡片",
  1657. desc: "少字段时一张主题卡片包裹全部 KV(2 列),比大面积空白 grid 更有「档案感」。",
  1658. meta: "7 字段 · 单卡",
  1659. previewClass: "w920",
  1660. sparse: true,
  1661. render: function () {
  1662. return (
  1663. '<div class="bento-single">' +
  1664. '<div class="info-card"><h4>备用金申请</h4>' +
  1665. kvGrid(SPARSE_FIELDS, 2, function (f) { return f.k === "申请原因" ? "span-all" : ""; }) +
  1666. "</div></div>"
  1667. );
  1668. }
  1669. },
  1670. {
  1671. id: "sparse-compact",
  1672. name: "L · 窄弹窗 720px",
  1673. width: "w720",
  1674. widthLabel: "720px",
  1675. badge: "紧凑",
  1676. desc: "字段 ≤6 时直接缩弹窗宽度 + 2 列 KV,不强行 1040。报销/审批类简详情最常用。",
  1677. meta: "7 字段 · 720px",
  1678. previewClass: "w920",
  1679. sparse: true,
  1680. dialogClass: "dialog-compact",
  1681. render: function () {
  1682. return section("申请信息", kvGrid(SPARSE_FIELDS, 2, function (f) {
  1683. return f.k === "申请原因" ? "span-all" : "";
  1684. }));
  1685. }
  1686. }
  1687. ];
  1688. var ALL_LAYOUTS = LAYOUTS.concat(TABLE_LAYOUTS).concat(ALT_LAYOUTS).concat(SPARSE_LAYOUTS);
  1689. function renderPreview(layout) {
  1690. return '<div class="mini-wrap"><div class="mini-dialog ' + layout.previewClass + '">' + layout.render() + "</div></div>";
  1691. }
  1692. function renderFull(layout) {
  1693. var title = layout.sparse ? "备用金申请 · BYJ-2026-0618" : "产品销售合同 · 查看";
  1694. var sub = layout.sparse ? "张三 · 申请 ¥5,000.00" : "HT-2026-SX-00428 · 华东精细化工贸易公司";
  1695. var heroExtra = layout.renderHeroExtra ? layout.renderHeroExtra() : "";
  1696. return (
  1697. '<div class="dialog-hero">' +
  1698. "<div><h2>" + title + "</h2><p>" + sub + "</p>" + heroExtra + "</div>" +
  1699. '<span class="width-tag">' + esc(layout.widthLabel) + "</span>" +
  1700. "</div>" +
  1701. '<div class="dialog-scroll">' + layout.render() + "</div>" +
  1702. '<div class="dialog-foot"><button type="button" class="btn-ghost" data-close>关闭</button></div>'
  1703. );
  1704. }
  1705. function mountLayoutCards(targetId, layouts, recBadge) {
  1706. var target = document.getElementById(targetId);
  1707. if (!target) return;
  1708. layouts.forEach(function (layout) {
  1709. var card = document.createElement("article");
  1710. var isRec = layout.badge === recBadge || layout.badge === "推荐";
  1711. card.className = "layout-card" + (isRec ? " rec" : "");
  1712. card.innerHTML =
  1713. '<div class="layout-card-head">' +
  1714. "<div><h2>" + esc(layout.name) + "</h2><p>" + esc(layout.desc) + "</p></div>" +
  1715. '<span class="badge' +
  1716. (layout.badge === "表格感" ? " blue" : layout.altMode ? " amber" : "") +
  1717. '">' +
  1718. esc(layout.widthLabel) +
  1719. "</span>" +
  1720. "</div>" +
  1721. '<div class="layout-preview">' + renderPreview(layout) + "</div>" +
  1722. '<div class="layout-card-foot">' +
  1723. '<span class="meta">' + esc(layout.meta) + "</span>" +
  1724. '<button type="button" class="btn" data-open="' + layout.id + '">全屏预览</button>' +
  1725. "</div>";
  1726. target.appendChild(card);
  1727. });
  1728. }
  1729. function initDialogInteractions(layout) {
  1730. if (!layout || !layout.interactive) return;
  1731. dialog.querySelectorAll("[data-tab]").forEach(function (btn) {
  1732. btn.addEventListener("click", function () {
  1733. var group = btn.getAttribute("data-tab-group");
  1734. var idx = btn.getAttribute("data-tab");
  1735. dialog.querySelectorAll('[data-tab-group="' + group + '"][data-tab]').forEach(function (b) {
  1736. b.classList.toggle("is-active", b === btn);
  1737. });
  1738. dialog.querySelectorAll('[data-tab-group="' + group + '"][data-tab-panel]').forEach(function (p) {
  1739. p.classList.toggle("is-active", p.getAttribute("data-tab-panel") === idx);
  1740. });
  1741. });
  1742. });
  1743. dialog.querySelectorAll("[data-acc-toggle]").forEach(function (btn) {
  1744. btn.addEventListener("click", function () {
  1745. var item = btn.closest(".acc-item");
  1746. if (item) item.classList.toggle("is-open");
  1747. });
  1748. });
  1749. }
  1750. mountLayoutCards("grid", LAYOUTS, "推荐");
  1751. mountLayoutCards("table-grid", TABLE_LAYOUTS, "推荐");
  1752. mountLayoutCards("alt-grid", ALT_LAYOUTS, "推荐");
  1753. mountLayoutCards("sparse-grid", SPARSE_LAYOUTS, "推荐");
  1754. var backdrop = document.getElementById("backdrop");
  1755. var dialog = document.getElementById("dialog");
  1756. document.addEventListener("click", function (e) {
  1757. var btn = e.target.closest("[data-open]");
  1758. if (!btn) return;
  1759. var layout = ALL_LAYOUTS.find(function (l) { return l.id === btn.getAttribute("data-open"); });
  1760. if (!layout) return;
  1761. dialog.className = "dialog " + layout.width + (layout.dialogClass ? " " + layout.dialogClass : "");
  1762. dialog.innerHTML = renderFull(layout);
  1763. initDialogInteractions(layout);
  1764. backdrop.classList.add("open");
  1765. });
  1766. backdrop.addEventListener("click", function (e) {
  1767. if (e.target === backdrop || e.target.closest("[data-close]")) backdrop.classList.remove("open");
  1768. });
  1769. document.addEventListener("keydown", function (e) {
  1770. if (e.key === "Escape") backdrop.classList.remove("open");
  1771. });
  1772. </script>
  1773. </body>
  1774. </html>