oa-related-approval-nested-demo.html 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  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" />
  6. <title>关联审批单 · 嵌套展示方案 Demo</title>
  7. <style>
  8. :root {
  9. --primary: #10b981;
  10. --primary-dark: #059669;
  11. --primary-bg: #ecfdf5;
  12. --primary-border: #a7f3d0;
  13. --text: #334155;
  14. --text-muted: #94a3b8;
  15. --text-strong: #0f172a;
  16. --border: #e2e8f0;
  17. --bg: #f8fafc;
  18. --panel: #ffffff;
  19. }
  20. * { box-sizing: border-box; }
  21. body {
  22. margin: 0;
  23. font-family: Inter, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", system-ui, sans-serif;
  24. font-size: 14px;
  25. line-height: 1.5;
  26. color: var(--text);
  27. background: var(--bg);
  28. }
  29. .page {
  30. max-width: 1080px;
  31. margin: 0 auto;
  32. padding: 28px 24px 64px;
  33. }
  34. h1 {
  35. margin: 0 0 8px;
  36. font-size: 22px;
  37. color: var(--text-strong);
  38. }
  39. .lead {
  40. margin: 0 0 24px;
  41. color: var(--text-muted);
  42. max-width: 720px;
  43. }
  44. .scene {
  45. margin-bottom: 12px;
  46. padding: 12px 14px;
  47. border: 1px dashed var(--primary-border);
  48. border-radius: 10px;
  49. background: #fff;
  50. color: var(--text);
  51. font-size: 13px;
  52. }
  53. .scene strong { color: var(--primary-dark); }
  54. .options {
  55. display: grid;
  56. gap: 16px;
  57. }
  58. @media (min-width: 900px) {
  59. .options { grid-template-columns: 1fr 1fr; }
  60. }
  61. .option {
  62. border: 1px solid var(--border);
  63. border-radius: 12px;
  64. background: var(--panel);
  65. overflow: hidden;
  66. box-shadow: 0 1px 3px rgb(15 23 42 / 6%);
  67. cursor: pointer;
  68. transition: border-color .15s, box-shadow .15s;
  69. }
  70. .option:hover { border-color: var(--primary-border); }
  71. .option.is-selected {
  72. border-color: var(--primary);
  73. box-shadow: 0 0 0 2px rgb(16 185 129 / 25%);
  74. }
  75. .option-head {
  76. display: flex;
  77. gap: 10px;
  78. align-items: flex-start;
  79. padding: 14px 16px 10px;
  80. border-bottom: 1px solid var(--border);
  81. background: #fafafa;
  82. }
  83. .badge {
  84. flex-shrink: 0;
  85. width: 28px;
  86. height: 28px;
  87. border-radius: 8px;
  88. background: var(--primary-bg);
  89. color: var(--primary-dark);
  90. font-weight: 700;
  91. font-size: 13px;
  92. display: grid;
  93. place-items: center;
  94. }
  95. .option.is-current .badge {
  96. background: #fef3c7;
  97. color: #b45309;
  98. }
  99. .option-meta h3 {
  100. margin: 0 0 4px;
  101. font-size: 15px;
  102. color: var(--text-strong);
  103. }
  104. .option-meta p {
  105. margin: 0;
  106. font-size: 12.5px;
  107. color: var(--text-muted);
  108. }
  109. .tag-now {
  110. display: inline-block;
  111. margin-left: 6px;
  112. padding: 1px 6px;
  113. border-radius: 4px;
  114. background: #fef3c7;
  115. color: #b45309;
  116. font-size: 11px;
  117. font-weight: 600;
  118. vertical-align: middle;
  119. }
  120. .mock-body { padding: 14px 16px 16px; }
  121. .field-label {
  122. margin-bottom: 8px;
  123. font-size: 12px;
  124. color: var(--text-muted);
  125. }
  126. /* Related approval card (shared) */
  127. .ra-card {
  128. padding: 10px 12px;
  129. border: 1px solid var(--primary-border);
  130. border-radius: 8px;
  131. background: var(--primary-bg);
  132. }
  133. .ra-head {
  134. display: flex;
  135. gap: 8px;
  136. align-items: center;
  137. justify-content: space-between;
  138. }
  139. .ra-title {
  140. padding: 0;
  141. border: 0;
  142. background: transparent;
  143. color: var(--primary);
  144. font-weight: 600;
  145. font-size: 13px;
  146. text-decoration: underline;
  147. cursor: pointer;
  148. text-align: left;
  149. }
  150. .ra-toggle {
  151. padding: 0;
  152. border: 0;
  153. background: transparent;
  154. color: var(--primary);
  155. font-size: 13px;
  156. cursor: pointer;
  157. }
  158. .ra-toggle:hover, .ra-title:hover { color: var(--primary-dark); }
  159. .ra-fields {
  160. display: flex;
  161. flex-wrap: wrap;
  162. gap: 6px 16px;
  163. margin-top: 8px;
  164. font-size: 12.5px;
  165. }
  166. .ra-fields.collapsed .ra-f:nth-child(n+4) { display: none; }
  167. .ra-f { display: inline-flex; gap: 6px; max-width: 100%; }
  168. .ra-f-label { color: var(--text-muted); }
  169. .ra-f-value { color: var(--text); word-break: break-all; }
  170. .ra-f.muted .ra-f-value { color: var(--text-muted); font-style: italic; }
  171. .ra-nested {
  172. margin-top: 8px;
  173. padding-top: 8px;
  174. border-top: 1px dashed var(--primary-border);
  175. display: none;
  176. flex-direction: column;
  177. gap: 8px;
  178. }
  179. .ra-nested.open { display: flex; }
  180. .ra-card--nested {
  181. background: #fff;
  182. border-color: #d1fae5;
  183. }
  184. .ra-hint {
  185. margin-top: 8px;
  186. font-size: 12px;
  187. color: var(--text-muted);
  188. }
  189. .pros {
  190. margin: 12px 16px 16px;
  191. padding: 10px 12px;
  192. border-radius: 8px;
  193. background: #f1f5f9;
  194. font-size: 12.5px;
  195. color: var(--text);
  196. }
  197. .pros strong { color: var(--primary-dark); }
  198. .footer-note {
  199. margin-top: 28px;
  200. padding: 14px 16px;
  201. border-radius: 10px;
  202. background: #fff;
  203. border: 1px solid var(--border);
  204. font-size: 13px;
  205. color: var(--text-muted);
  206. }
  207. .pick-bar {
  208. position: sticky;
  209. bottom: 0;
  210. margin: 24px -24px -64px;
  211. padding: 14px 24px;
  212. background: rgb(255 255 255 / 92%);
  213. backdrop-filter: blur(8px);
  214. border-top: 1px solid var(--border);
  215. display: flex;
  216. gap: 12px;
  217. align-items: center;
  218. justify-content: space-between;
  219. }
  220. .pick-bar span { color: var(--text-muted); font-size: 13px; }
  221. .pick-bar em { color: var(--primary-dark); font-style: normal; font-weight: 600; }
  222. </style>
  223. </head>
  224. <body>
  225. <div class="page">
  226. <h1>关联审批单 · 嵌套展示方案</h1>
  227. <p class="lead">
  228. 场景:发起/详情里选中一张历史审批单 A;A 自己的表单里也有「关联审批单」字段(指向 B)。
  229. 当前 V1 规格把嵌套字段压成「已关联 M 条」,导致看不到缩略信息与展开。请点选你更倾向的方案(可点卡片内「展开」试交互)。
  230. </p>
  231. <div class="scene">
  232. <strong>数据链:</strong>当前单 → 关联 <u>差旅报销 · 07-18</u>(单 A)→ 单 A 内又关联了 <u>外出申请 · 07-10</u>(单 B)
  233. </div>
  234. <div class="options" id="options">
  235. <!-- A: current -->
  236. <article class="option is-current" data-choice="a-status-quo" onclick="selectOption(this)">
  237. <div class="option-head">
  238. <div class="badge">A</div>
  239. <div class="option-meta">
  240. <h3>现状(V1 已定)<span class="tag-now">当前实现</span></h3>
  241. <p>嵌套 relatedApproval 只显示「已关联 M 条」,不递归、无子卡片缩略</p>
  242. </div>
  243. </div>
  244. <div class="mock-body">
  245. <div class="field-label">关联审批单</div>
  246. <div class="ra-card" data-stop>
  247. <div class="ra-head">
  248. <button type="button" class="ra-title">差旅报销 · 07-18</button>
  249. <button type="button" class="ra-toggle" data-expand="a1">展开</button>
  250. </div>
  251. <div class="ra-fields collapsed" id="a1">
  252. <span class="ra-f"><span class="ra-f-label">事由</span><span class="ra-f-value">客户拜访</span></span>
  253. <span class="ra-f"><span class="ra-f-label">金额</span><span class="ra-f-value">¥1,280.00</span></span>
  254. <span class="ra-f muted"><span class="ra-f-label">关联审批单</span><span class="ra-f-value">已关联 1 条</span></span>
  255. <span class="ra-f"><span class="ra-f-label">备注</span><span class="ra-f-value">含打车费</span></span>
  256. </div>
  257. <div class="ra-hint">↑ 嵌套字段只有计数文案;即使点「展开」也看不到单 B 的业务字段</div>
  258. </div>
  259. </div>
  260. <div class="pros"><strong>优点</strong>:实现简单、无递归性能风险。<strong>缺点</strong>:信息不足,正是你遇到的问题。</div>
  261. </article>
  262. <!-- B: jump only -->
  263. <article class="option" data-choice="b-jump" onclick="selectOption(this)">
  264. <div class="option-head">
  265. <div class="badge">B</div>
  266. <div class="option-meta">
  267. <h3>计数 + 可跳转详情</h3>
  268. <p>仍显示「已关联 M 条」,但做成可点链接,跳进被关联单详情查看</p>
  269. </div>
  270. </div>
  271. <div class="mock-body">
  272. <div class="field-label">关联审批单</div>
  273. <div class="ra-card" data-stop>
  274. <div class="ra-head">
  275. <button type="button" class="ra-title">差旅报销 · 07-18</button>
  276. <button type="button" class="ra-toggle" data-expand="b1">展开</button>
  277. </div>
  278. <div class="ra-fields collapsed" id="b1">
  279. <span class="ra-f"><span class="ra-f-label">事由</span><span class="ra-f-value">客户拜访</span></span>
  280. <span class="ra-f"><span class="ra-f-label">金额</span><span class="ra-f-value">¥1,280.00</span></span>
  281. <span class="ra-f"><span class="ra-f-label">关联审批单</span><span class="ra-f-value"><a href="#" style="color:var(--primary)" onclick="event.preventDefault();alert('跳转到:外出申请详情')">已关联 1 条 → 查看</a></span></span>
  282. <span class="ra-f"><span class="ra-f-label">备注</span><span class="ra-f-value">含打车费</span></span>
  283. </div>
  284. </div>
  285. </div>
  286. <div class="pros"><strong>优点</strong>:改动小、无嵌套 UI。<strong>缺点</strong>:仍无缩略预览,要跳页才能看清。</div>
  287. </article>
  288. <!-- C: one-level nested cards -->
  289. <article class="option" data-choice="c-one-level" onclick="selectOption(this)">
  290. <div class="option-head">
  291. <div class="badge">C</div>
  292. <div class="option-meta">
  293. <h3>一层嵌套卡片(推荐倾向)</h3>
  294. <p>展开单 A 后,嵌套关联展示为子卡片(应用名·时间 + 前 3 字段);再往下仍用计数,不无限递归</p>
  295. </div>
  296. </div>
  297. <div class="mock-body">
  298. <div class="field-label">关联审批单</div>
  299. <div class="ra-card" data-stop>
  300. <div class="ra-head">
  301. <button type="button" class="ra-title">差旅报销 · 07-18</button>
  302. <button type="button" class="ra-toggle" data-expand="c1" data-nested="c1n">展开</button>
  303. </div>
  304. <div class="ra-fields collapsed" id="c1">
  305. <span class="ra-f"><span class="ra-f-label">事由</span><span class="ra-f-value">客户拜访</span></span>
  306. <span class="ra-f"><span class="ra-f-label">金额</span><span class="ra-f-value">¥1,280.00</span></span>
  307. <span class="ra-f"><span class="ra-f-label">备注</span><span class="ra-f-value">含打车费</span></span>
  308. </div>
  309. <div class="ra-nested" id="c1n">
  310. <div class="field-label" style="margin:0">内含关联审批单 · 1 条</div>
  311. <div class="ra-card ra-card--nested">
  312. <div class="ra-head">
  313. <button type="button" class="ra-title">外出申请 · 07-10</button>
  314. <button type="button" class="ra-toggle" data-expand="c1b">展开</button>
  315. </div>
  316. <div class="ra-fields collapsed" id="c1b">
  317. <span class="ra-f"><span class="ra-f-label">目的地</span><span class="ra-f-value">上海</span></span>
  318. <span class="ra-f"><span class="ra-f-label">起止</span><span class="ra-f-value">07-10 ~ 07-12</span></span>
  319. <span class="ra-f"><span class="ra-f-label">同行人</span><span class="ra-f-value">张三</span></span>
  320. <span class="ra-f muted"><span class="ra-f-label">关联审批单</span><span class="ra-f-value">已关联 0 条</span></span>
  321. </div>
  322. </div>
  323. </div>
  324. </div>
  325. </div>
  326. <div class="pros"><strong>优点</strong>:有缩略与展开,信息完整可控。<strong>缺点</strong>:enrichment 需多取一层;UI 略复杂。</div>
  327. </article>
  328. <!-- D: deep recursive -->
  329. <article class="option" data-choice="d-recursive" onclick="selectOption(this)">
  330. <div class="option-head">
  331. <div class="badge">D</div>
  332. <div class="option-meta">
  333. <h3>无限递归嵌套</h3>
  334. <p>每一层关联都可以再展开子关联,形成树状</p>
  335. </div>
  336. </div>
  337. <div class="mock-body">
  338. <div class="field-label">关联审批单</div>
  339. <div class="ra-card" data-stop>
  340. <div class="ra-head">
  341. <button type="button" class="ra-title">差旅报销 · 07-18</button>
  342. <span class="ra-toggle" style="cursor:default;color:var(--text-muted)">已展开</span>
  343. </div>
  344. <div class="ra-fields">
  345. <span class="ra-f"><span class="ra-f-label">事由</span><span class="ra-f-value">客户拜访</span></span>
  346. </div>
  347. <div class="ra-nested open">
  348. <div class="ra-card ra-card--nested">
  349. <div class="ra-head">
  350. <button type="button" class="ra-title">外出申请 · 07-10</button>
  351. </div>
  352. <div class="ra-nested open" style="border-top:1px dashed #d1fae5;margin-top:8px;padding-top:8px">
  353. <div class="ra-card ra-card--nested" style="opacity:.85">
  354. <div class="ra-head">
  355. <button type="button" class="ra-title">立项申请 · 06-01</button>
  356. </div>
  357. <div class="ra-hint">…可能继续嵌套</div>
  358. </div>
  359. </div>
  360. </div>
  361. </div>
  362. </div>
  363. </div>
  364. <div class="pros"><strong>优点</strong>:最完整。<strong>缺点</strong>:性能、权限、循环引用、UI 噪声都更大,一般不建议 V1.x。</div>
  365. </article>
  366. </div>
  367. <div class="footer-note">
  368. 说明:V1 设计文档 §5 明确写了「嵌套 relatedApproval → 已关联 M 条(不递归展开)」。
  369. 你遇到的现象符合现规格,不是前端展开按钮单独坏了。接下来要决定是否修订规格(例如采用方案 C)。
  370. 文件路径:<code>docs/mockups/oa-related-approval-nested-demo.html</code>
  371. </div>
  372. <div class="pick-bar">
  373. <span>当前选择:<em id="picked">尚未选择</em></span>
  374. <span>也可直接在对话里回复 A / B / C / D</span>
  375. </div>
  376. </div>
  377. <script>
  378. function selectOption(el) {
  379. document.querySelectorAll('.option').forEach((n) => n.classList.remove('is-selected'))
  380. el.classList.add('is-selected')
  381. const choice = el.getAttribute('data-choice')
  382. const title = el.querySelector('h3')?.childNodes[0]?.textContent?.trim() || choice
  383. document.getElementById('picked').textContent = title
  384. window.__nestedChoice = choice
  385. console.log('[demo] selected', choice)
  386. }
  387. document.querySelectorAll('[data-stop]').forEach((el) => {
  388. el.addEventListener('click', (e) => e.stopPropagation())
  389. })
  390. document.querySelectorAll('[data-expand]').forEach((btn) => {
  391. btn.addEventListener('click', (e) => {
  392. e.stopPropagation()
  393. const id = btn.getAttribute('data-expand')
  394. const fields = document.getElementById(id)
  395. if (!fields) return
  396. const collapsed = fields.classList.toggle('collapsed')
  397. btn.textContent = collapsed ? '展开' : '收起'
  398. const nestedId = btn.getAttribute('data-nested')
  399. if (nestedId) {
  400. document.getElementById(nestedId)?.classList.toggle('open', !collapsed)
  401. }
  402. })
  403. })
  404. </script>
  405. </body>
  406. </html>