| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1" />
- <title>关联审批单 · 嵌套展示方案 Demo</title>
- <style>
- :root {
- --primary: #10b981;
- --primary-dark: #059669;
- --primary-bg: #ecfdf5;
- --primary-border: #a7f3d0;
- --text: #334155;
- --text-muted: #94a3b8;
- --text-strong: #0f172a;
- --border: #e2e8f0;
- --bg: #f8fafc;
- --panel: #ffffff;
- }
- * { box-sizing: border-box; }
- body {
- margin: 0;
- font-family: Inter, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", system-ui, sans-serif;
- font-size: 14px;
- line-height: 1.5;
- color: var(--text);
- background: var(--bg);
- }
- .page {
- max-width: 1080px;
- margin: 0 auto;
- padding: 28px 24px 64px;
- }
- h1 {
- margin: 0 0 8px;
- font-size: 22px;
- color: var(--text-strong);
- }
- .lead {
- margin: 0 0 24px;
- color: var(--text-muted);
- max-width: 720px;
- }
- .scene {
- margin-bottom: 12px;
- padding: 12px 14px;
- border: 1px dashed var(--primary-border);
- border-radius: 10px;
- background: #fff;
- color: var(--text);
- font-size: 13px;
- }
- .scene strong { color: var(--primary-dark); }
- .options {
- display: grid;
- gap: 16px;
- }
- @media (min-width: 900px) {
- .options { grid-template-columns: 1fr 1fr; }
- }
- .option {
- border: 1px solid var(--border);
- border-radius: 12px;
- background: var(--panel);
- overflow: hidden;
- box-shadow: 0 1px 3px rgb(15 23 42 / 6%);
- cursor: pointer;
- transition: border-color .15s, box-shadow .15s;
- }
- .option:hover { border-color: var(--primary-border); }
- .option.is-selected {
- border-color: var(--primary);
- box-shadow: 0 0 0 2px rgb(16 185 129 / 25%);
- }
- .option-head {
- display: flex;
- gap: 10px;
- align-items: flex-start;
- padding: 14px 16px 10px;
- border-bottom: 1px solid var(--border);
- background: #fafafa;
- }
- .badge {
- flex-shrink: 0;
- width: 28px;
- height: 28px;
- border-radius: 8px;
- background: var(--primary-bg);
- color: var(--primary-dark);
- font-weight: 700;
- font-size: 13px;
- display: grid;
- place-items: center;
- }
- .option.is-current .badge {
- background: #fef3c7;
- color: #b45309;
- }
- .option-meta h3 {
- margin: 0 0 4px;
- font-size: 15px;
- color: var(--text-strong);
- }
- .option-meta p {
- margin: 0;
- font-size: 12.5px;
- color: var(--text-muted);
- }
- .tag-now {
- display: inline-block;
- margin-left: 6px;
- padding: 1px 6px;
- border-radius: 4px;
- background: #fef3c7;
- color: #b45309;
- font-size: 11px;
- font-weight: 600;
- vertical-align: middle;
- }
- .mock-body { padding: 14px 16px 16px; }
- .field-label {
- margin-bottom: 8px;
- font-size: 12px;
- color: var(--text-muted);
- }
- /* Related approval card (shared) */
- .ra-card {
- padding: 10px 12px;
- border: 1px solid var(--primary-border);
- border-radius: 8px;
- background: var(--primary-bg);
- }
- .ra-head {
- display: flex;
- gap: 8px;
- align-items: center;
- justify-content: space-between;
- }
- .ra-title {
- padding: 0;
- border: 0;
- background: transparent;
- color: var(--primary);
- font-weight: 600;
- font-size: 13px;
- text-decoration: underline;
- cursor: pointer;
- text-align: left;
- }
- .ra-toggle {
- padding: 0;
- border: 0;
- background: transparent;
- color: var(--primary);
- font-size: 13px;
- cursor: pointer;
- }
- .ra-toggle:hover, .ra-title:hover { color: var(--primary-dark); }
- .ra-fields {
- display: flex;
- flex-wrap: wrap;
- gap: 6px 16px;
- margin-top: 8px;
- font-size: 12.5px;
- }
- .ra-fields.collapsed .ra-f:nth-child(n+4) { display: none; }
- .ra-f { display: inline-flex; gap: 6px; max-width: 100%; }
- .ra-f-label { color: var(--text-muted); }
- .ra-f-value { color: var(--text); word-break: break-all; }
- .ra-f.muted .ra-f-value { color: var(--text-muted); font-style: italic; }
- .ra-nested {
- margin-top: 8px;
- padding-top: 8px;
- border-top: 1px dashed var(--primary-border);
- display: none;
- flex-direction: column;
- gap: 8px;
- }
- .ra-nested.open { display: flex; }
- .ra-card--nested {
- background: #fff;
- border-color: #d1fae5;
- }
- .ra-hint {
- margin-top: 8px;
- font-size: 12px;
- color: var(--text-muted);
- }
- .pros {
- margin: 12px 16px 16px;
- padding: 10px 12px;
- border-radius: 8px;
- background: #f1f5f9;
- font-size: 12.5px;
- color: var(--text);
- }
- .pros strong { color: var(--primary-dark); }
- .footer-note {
- margin-top: 28px;
- padding: 14px 16px;
- border-radius: 10px;
- background: #fff;
- border: 1px solid var(--border);
- font-size: 13px;
- color: var(--text-muted);
- }
- .pick-bar {
- position: sticky;
- bottom: 0;
- margin: 24px -24px -64px;
- padding: 14px 24px;
- background: rgb(255 255 255 / 92%);
- backdrop-filter: blur(8px);
- border-top: 1px solid var(--border);
- display: flex;
- gap: 12px;
- align-items: center;
- justify-content: space-between;
- }
- .pick-bar span { color: var(--text-muted); font-size: 13px; }
- .pick-bar em { color: var(--primary-dark); font-style: normal; font-weight: 600; }
- </style>
- </head>
- <body>
- <div class="page">
- <h1>关联审批单 · 嵌套展示方案</h1>
- <p class="lead">
- 场景:发起/详情里选中一张历史审批单 A;A 自己的表单里也有「关联审批单」字段(指向 B)。
- 当前 V1 规格把嵌套字段压成「已关联 M 条」,导致看不到缩略信息与展开。请点选你更倾向的方案(可点卡片内「展开」试交互)。
- </p>
- <div class="scene">
- <strong>数据链:</strong>当前单 → 关联 <u>差旅报销 · 07-18</u>(单 A)→ 单 A 内又关联了 <u>外出申请 · 07-10</u>(单 B)
- </div>
- <div class="options" id="options">
- <!-- A: current -->
- <article class="option is-current" data-choice="a-status-quo" onclick="selectOption(this)">
- <div class="option-head">
- <div class="badge">A</div>
- <div class="option-meta">
- <h3>现状(V1 已定)<span class="tag-now">当前实现</span></h3>
- <p>嵌套 relatedApproval 只显示「已关联 M 条」,不递归、无子卡片缩略</p>
- </div>
- </div>
- <div class="mock-body">
- <div class="field-label">关联审批单</div>
- <div class="ra-card" data-stop>
- <div class="ra-head">
- <button type="button" class="ra-title">差旅报销 · 07-18</button>
- <button type="button" class="ra-toggle" data-expand="a1">展开</button>
- </div>
- <div class="ra-fields collapsed" id="a1">
- <span class="ra-f"><span class="ra-f-label">事由</span><span class="ra-f-value">客户拜访</span></span>
- <span class="ra-f"><span class="ra-f-label">金额</span><span class="ra-f-value">¥1,280.00</span></span>
- <span class="ra-f muted"><span class="ra-f-label">关联审批单</span><span class="ra-f-value">已关联 1 条</span></span>
- <span class="ra-f"><span class="ra-f-label">备注</span><span class="ra-f-value">含打车费</span></span>
- </div>
- <div class="ra-hint">↑ 嵌套字段只有计数文案;即使点「展开」也看不到单 B 的业务字段</div>
- </div>
- </div>
- <div class="pros"><strong>优点</strong>:实现简单、无递归性能风险。<strong>缺点</strong>:信息不足,正是你遇到的问题。</div>
- </article>
- <!-- B: jump only -->
- <article class="option" data-choice="b-jump" onclick="selectOption(this)">
- <div class="option-head">
- <div class="badge">B</div>
- <div class="option-meta">
- <h3>计数 + 可跳转详情</h3>
- <p>仍显示「已关联 M 条」,但做成可点链接,跳进被关联单详情查看</p>
- </div>
- </div>
- <div class="mock-body">
- <div class="field-label">关联审批单</div>
- <div class="ra-card" data-stop>
- <div class="ra-head">
- <button type="button" class="ra-title">差旅报销 · 07-18</button>
- <button type="button" class="ra-toggle" data-expand="b1">展开</button>
- </div>
- <div class="ra-fields collapsed" id="b1">
- <span class="ra-f"><span class="ra-f-label">事由</span><span class="ra-f-value">客户拜访</span></span>
- <span class="ra-f"><span class="ra-f-label">金额</span><span class="ra-f-value">¥1,280.00</span></span>
- <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>
- <span class="ra-f"><span class="ra-f-label">备注</span><span class="ra-f-value">含打车费</span></span>
- </div>
- </div>
- </div>
- <div class="pros"><strong>优点</strong>:改动小、无嵌套 UI。<strong>缺点</strong>:仍无缩略预览,要跳页才能看清。</div>
- </article>
- <!-- C: one-level nested cards -->
- <article class="option" data-choice="c-one-level" onclick="selectOption(this)">
- <div class="option-head">
- <div class="badge">C</div>
- <div class="option-meta">
- <h3>一层嵌套卡片(推荐倾向)</h3>
- <p>展开单 A 后,嵌套关联展示为子卡片(应用名·时间 + 前 3 字段);再往下仍用计数,不无限递归</p>
- </div>
- </div>
- <div class="mock-body">
- <div class="field-label">关联审批单</div>
- <div class="ra-card" data-stop>
- <div class="ra-head">
- <button type="button" class="ra-title">差旅报销 · 07-18</button>
- <button type="button" class="ra-toggle" data-expand="c1" data-nested="c1n">展开</button>
- </div>
- <div class="ra-fields collapsed" id="c1">
- <span class="ra-f"><span class="ra-f-label">事由</span><span class="ra-f-value">客户拜访</span></span>
- <span class="ra-f"><span class="ra-f-label">金额</span><span class="ra-f-value">¥1,280.00</span></span>
- <span class="ra-f"><span class="ra-f-label">备注</span><span class="ra-f-value">含打车费</span></span>
- </div>
- <div class="ra-nested" id="c1n">
- <div class="field-label" style="margin:0">内含关联审批单 · 1 条</div>
- <div class="ra-card ra-card--nested">
- <div class="ra-head">
- <button type="button" class="ra-title">外出申请 · 07-10</button>
- <button type="button" class="ra-toggle" data-expand="c1b">展开</button>
- </div>
- <div class="ra-fields collapsed" id="c1b">
- <span class="ra-f"><span class="ra-f-label">目的地</span><span class="ra-f-value">上海</span></span>
- <span class="ra-f"><span class="ra-f-label">起止</span><span class="ra-f-value">07-10 ~ 07-12</span></span>
- <span class="ra-f"><span class="ra-f-label">同行人</span><span class="ra-f-value">张三</span></span>
- <span class="ra-f muted"><span class="ra-f-label">关联审批单</span><span class="ra-f-value">已关联 0 条</span></span>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class="pros"><strong>优点</strong>:有缩略与展开,信息完整可控。<strong>缺点</strong>:enrichment 需多取一层;UI 略复杂。</div>
- </article>
- <!-- D: deep recursive -->
- <article class="option" data-choice="d-recursive" onclick="selectOption(this)">
- <div class="option-head">
- <div class="badge">D</div>
- <div class="option-meta">
- <h3>无限递归嵌套</h3>
- <p>每一层关联都可以再展开子关联,形成树状</p>
- </div>
- </div>
- <div class="mock-body">
- <div class="field-label">关联审批单</div>
- <div class="ra-card" data-stop>
- <div class="ra-head">
- <button type="button" class="ra-title">差旅报销 · 07-18</button>
- <span class="ra-toggle" style="cursor:default;color:var(--text-muted)">已展开</span>
- </div>
- <div class="ra-fields">
- <span class="ra-f"><span class="ra-f-label">事由</span><span class="ra-f-value">客户拜访</span></span>
- </div>
- <div class="ra-nested open">
- <div class="ra-card ra-card--nested">
- <div class="ra-head">
- <button type="button" class="ra-title">外出申请 · 07-10</button>
- </div>
- <div class="ra-nested open" style="border-top:1px dashed #d1fae5;margin-top:8px;padding-top:8px">
- <div class="ra-card ra-card--nested" style="opacity:.85">
- <div class="ra-head">
- <button type="button" class="ra-title">立项申请 · 06-01</button>
- </div>
- <div class="ra-hint">…可能继续嵌套</div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class="pros"><strong>优点</strong>:最完整。<strong>缺点</strong>:性能、权限、循环引用、UI 噪声都更大,一般不建议 V1.x。</div>
- </article>
- </div>
- <div class="footer-note">
- 说明:V1 设计文档 §5 明确写了「嵌套 relatedApproval → 已关联 M 条(不递归展开)」。
- 你遇到的现象符合现规格,不是前端展开按钮单独坏了。接下来要决定是否修订规格(例如采用方案 C)。
- 文件路径:<code>docs/mockups/oa-related-approval-nested-demo.html</code>
- </div>
- <div class="pick-bar">
- <span>当前选择:<em id="picked">尚未选择</em></span>
- <span>也可直接在对话里回复 A / B / C / D</span>
- </div>
- </div>
- <script>
- function selectOption(el) {
- document.querySelectorAll('.option').forEach((n) => n.classList.remove('is-selected'))
- el.classList.add('is-selected')
- const choice = el.getAttribute('data-choice')
- const title = el.querySelector('h3')?.childNodes[0]?.textContent?.trim() || choice
- document.getElementById('picked').textContent = title
- window.__nestedChoice = choice
- console.log('[demo] selected', choice)
- }
- document.querySelectorAll('[data-stop]').forEach((el) => {
- el.addEventListener('click', (e) => e.stopPropagation())
- })
- document.querySelectorAll('[data-expand]').forEach((btn) => {
- btn.addEventListener('click', (e) => {
- e.stopPropagation()
- const id = btn.getAttribute('data-expand')
- const fields = document.getElementById(id)
- if (!fields) return
- const collapsed = fields.classList.toggle('collapsed')
- btn.textContent = collapsed ? '展开' : '收起'
- const nestedId = btn.getAttribute('data-nested')
- if (nestedId) {
- document.getElementById(nestedId)?.classList.toggle('open', !collapsed)
- }
- })
- })
- </script>
- </body>
- </html>
|