Records screen count+jump vs print multi-line recursion (max 3 levels) and links the V1 related-approval spec to the revision. Co-authored-by: Cursor <cursoragent@cursor.com>
418 lines
16 KiB
HTML
418 lines
16 KiB
HTML
<!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>
|