Files
zkxw/docs/mockups/oa-approval-start-flow-preview-demo.html
T
2026-07-15 16:20:48 +08:00

615 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>发起审批 · 表单底部审批流预览 Demo(Fresh Mint)</title>
<style>
:root {
--primary: #10b981;
--primary-dark: #059669;
--primary-bg: #ecfdf5;
--text: #0f172a;
--text-2: #334155;
--muted: #94a3b8;
--border: #e2e8f0;
--bg: #f1f5f9;
--card: #fff;
--warn: #d97706;
--warn-bg: #fff7ed;
--cc: #0891b2;
--cc-bg: #ecfeff;
--radius: 10px;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font: 14px/1.5 Inter, system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
color: var(--text);
background:
linear-gradient(180deg, #eefcf6 0%, var(--bg) 160px) fixed;
}
.page {
max-width: 960px;
margin: 0 auto;
padding: 24px 20px 72px;
}
.demo-banner {
display: flex;
flex-wrap: wrap;
gap: 10px 16px;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
padding: 12px 16px;
background: #fff;
border: 1px solid var(--border);
border-radius: var(--radius);
}
.demo-banner b { color: var(--primary-dark); }
.demo-banner span { color: var(--muted); font-size: 13px; }
.tag {
display: inline-block;
padding: 2px 8px;
border-radius: 999px;
background: var(--primary-bg);
color: var(--primary-dark);
font-size: 12px;
font-weight: 600;
}
.mode-switch {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 20px;
}
.mode-btn {
border: 1px solid var(--border);
background: #fff;
border-radius: 8px;
padding: 8px 14px;
font: inherit;
font-size: 13px;
cursor: pointer;
color: var(--text-2);
}
.mode-btn:hover { border-color: #a7f3d0; background: var(--primary-bg); }
.mode-btn.on {
border-color: var(--primary);
background: var(--primary-bg);
color: var(--primary-dark);
font-weight: 600;
}
/* 遮罩 + 弹窗 */
.mask {
display: flex;
align-items: flex-start;
justify-content: center;
padding: 24px 12px 40px;
min-height: 70vh;
background: rgba(15, 23, 42, 0.35);
border-radius: 12px;
}
.dialog {
width: min(720px, 100%);
max-height: calc(100vh - 80px);
display: flex;
flex-direction: column;
background: var(--card);
border-radius: 12px;
box-shadow: 0 16px 48px rgba(15, 23, 42, 0.18);
overflow: hidden;
}
.dialog__hd {
flex-shrink: 0;
padding: 16px 20px;
border-bottom: 1px solid var(--border);
font-size: 16px;
font-weight: 600;
}
.dialog__bd {
flex: 1;
overflow: auto;
padding: 16px 20px 8px;
}
.dialog__ft {
flex-shrink: 0;
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 12px 20px 16px;
border-top: 1px solid var(--border);
background: #fafbfc;
}
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
height: 36px;
padding: 0 16px;
border-radius: 8px;
border: 1px solid var(--border);
background: #fff;
font: inherit;
font-size: 13px;
cursor: pointer;
}
.btn.primary {
background: var(--primary);
border-color: var(--primary);
color: #fff;
}
/* 表单区 */
.form-stack label {
display: block;
margin: 0 0 6px;
font-size: 12px;
color: var(--muted);
font-weight: 500;
}
.form-stack .field { margin-bottom: 14px; }
.form-stack .req::after {
content: " *";
color: #dc2626;
}
.form-stack input,
.form-stack textarea,
.form-stack select {
width: 100%;
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px 12px;
font: inherit;
background: #fff;
}
.form-stack input:focus,
.form-stack textarea:focus,
.form-stack select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px var(--primary-bg);
}
.form-grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0 16px;
}
@media (max-width: 560px) {
.form-grid-2 { grid-template-columns: 1fr; }
}
/* —— 钉钉式审批流预览(表单底部) —— */
.flow-preview {
margin: 8px 0 12px;
padding: 12px 12px 8px;
background: #f8fafc;
border: 1px solid var(--border);
border-radius: 10px;
}
.flow-preview__title {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
font-size: 13px;
font-weight: 600;
color: var(--text-2);
}
.flow-preview__title .hint {
font-weight: 400;
font-size: 12px;
color: var(--muted);
}
.flow-chain {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 0;
}
.flow-step {
display: flex;
gap: 8px;
align-items: stretch;
}
.flow-rail {
display: flex;
flex-direction: column;
align-items: center;
width: 16px;
flex-shrink: 0;
}
/* 统一灰色圆点,不区分节点类型配色 */
.flow-dot {
width: 10px;
height: 10px;
margin-top: 12px;
border-radius: 50%;
background: #94a3b8;
flex-shrink: 0;
box-shadow: 0 0 0 3px #e2e8f0;
}
.flow-dot.end {
background: #cbd5e1;
box-shadow: 0 0 0 3px #f1f5f9;
}
.flow-line {
width: 2px;
flex: 1;
min-height: 10px;
margin: 4px 0;
background: #e2e8f0;
}
.flow-step:last-child .flow-line { display: none; }
/* 单行紧凑卡片 · 方案 B */
.flow-card {
flex: 1;
min-width: 0;
margin-bottom: 6px;
padding: 9px 12px;
background: #fff;
border: 1px solid var(--border);
border-radius: 6px;
}
.flow-card__row {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 6px 8px;
min-width: 0;
overflow: hidden;
}
.flow-card__name {
flex-shrink: 0;
font-size: 12px;
font-weight: 600;
color: var(--text);
line-height: 1.2;
white-space: nowrap;
}
.flow-card__meta {
flex-shrink: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 11px;
color: var(--muted);
line-height: 1.2;
}
.flow-card__sep {
flex-shrink: 0;
color: #cbd5e1;
font-size: 11px;
}
.flow-card__people {
display: inline-flex;
flex-wrap: nowrap;
align-items: center;
gap: 4px;
margin-left: auto;
flex-shrink: 0;
max-width: 45%;
overflow: hidden;
}
.person-chip {
display: inline-flex;
align-items: center;
gap: 3px;
padding: 1px 6px 1px 2px;
background: #f1f5f9;
border-radius: 999px;
font-size: 11px;
color: var(--text-2);
line-height: 1.2;
white-space: nowrap;
}
.person-chip .dot {
width: 16px;
height: 16px;
border-radius: 50%;
display: grid;
place-items: center;
font-size: 9px;
font-weight: 700;
color: #fff;
background: var(--primary);
}
.person-chip.pick {
background: var(--warn-bg);
color: var(--warn);
border: 1px dashed #fdba74;
cursor: pointer;
padding: 2px 8px;
}
.person-chip.pick:hover { background: #ffedd5; }
.person-chip.role {
background: var(--primary-bg);
color: var(--primary-dark);
}
.badge-mini {
display: inline-block;
flex-shrink: 0;
padding: 0 5px;
border-radius: 4px;
font-size: 10px;
font-weight: 500;
line-height: 16px;
white-space: nowrap;
}
.badge-mini.or { background: #fff7ed; color: var(--warn); }
.badge-mini.and { background: #eff6ff; color: #2563eb; }
.badge-mini.cc { background: var(--cc-bg); color: var(--cc); }
.branch-box {
margin: 2px 0 8px 24px;
padding: 6px 10px;
border: 1px dashed #cbd5e1;
border-radius: 6px;
background: #fff;
font-size: 11px;
color: var(--muted);
}
.branch-box b { color: var(--text-2); }
.branch-box.active {
border-color: #86efac;
background: var(--primary-bg);
color: var(--primary-dark);
}
.note {
margin-top: 16px;
padding: 12px 14px;
background: #fffbeb;
border: 1px solid #fde68a;
border-radius: 8px;
font-size: 13px;
color: #92400e;
}
.note code {
font-size: 12px;
background: #fff;
padding: 1px 5px;
border-radius: 4px;
}
</style>
</head>
<body>
<div class="page">
<div class="demo-banner">
<div>
<b>发起审批 · 方案 B(紧凑卡片)</b>
<span>解析到具体人名;节点卡片缩小以适配弹窗滚动区</span>
</div>
<span class="tag">Fresh Mint · Demo</span>
</div>
<div class="mode-switch" role="tablist">
<button type="button" class="mode-btn on" data-mode="B" id="btn-b">方案 B · 解析到具体人名(推荐)</button>
<button type="button" class="mode-btn" data-mode="A" id="btn-a">方案 A · 仅配置摘要</button>
<button type="button" class="mode-btn" data-mode="C" id="btn-c">方案 C · 含自选 + 条件提示</button>
</div>
<div class="mask">
<div class="dialog" role="dialog" aria-label="发起审批">
<div class="dialog__hd">员工请假申请</div>
<div class="dialog__bd">
<!-- 业务表单 -->
<div class="form-stack">
<div class="form-grid-2">
<div class="field">
<label class="req">请假类型</label>
<select id="leaveType">
<option value="annual">年假</option>
<option value="sick">病假</option>
<option value="personal">事假</option>
</select>
</div>
<div class="field">
<label class="req">请假天数</label>
<input type="number" id="days" value="2" min="0.5" step="0.5" />
</div>
</div>
<div class="field">
<label class="req">请假事由</label>
<textarea rows="2" placeholder="请输入事由">家中有事</textarea>
</div>
</div>
<!-- 审批流预览 -->
<div class="flow-preview">
<div class="flow-preview__title">
<span>审批流程</span>
<span class="hint" id="flowHint">按当前发起人解析审批人</span>
</div>
<div class="flow-chain" id="flowChain"></div>
</div>
</div>
<div class="dialog__ft">
<button type="button" class="btn">取消</button>
<button type="button" class="btn primary">提交</button>
</div>
</div>
</div>
<div class="note">
文件路径:<code>docs/mockups/oa-approval-start-flow-preview-demo.html</code><br />
落地建议:在 <code>oa-approval/start/index.vue</code> 弹窗内、表单下方增加只读流程预览;自选审批人可内嵌到对应节点,替代现在顶部独立字段。
用上方三个按钮切换 A / B / C 方案对比。
</div>
</div>
<script>
const modes = {
A: {
hint: '仅展示节点配置类型(不解析人名)',
render() {
return [
step('start', '发起人', '当前登录用户', null),
step('approval', '直属主管', '发起人的主管', null, 'or'),
step('approval', '一级审批', '工作流角色 · 总经理', null, 'or'),
step('cc', '抄送人', '发起人自选', null, 'cc'),
step('end', '结束', '', null)
].join('')
}
},
B: {
hint: '按当前发起人解析审批人(推荐首版)',
render() {
return [
step('start', '发起人', '', people([{ n: '张', name: '张三' }])),
step(
'approval',
'直属主管',
'发起人的主管',
people([{ n: '李', name: '李主管' }]),
'or'
),
step(
'approval',
'一级审批',
'工作流角色 · 总经理',
people([
{ n: '王', name: '王总' },
{ n: '赵', name: '赵总' }
]),
'or'
),
step(
'cc',
'抄送人',
'人事专员',
people([{ n: '陈', name: '陈人事' }]),
'cc'
),
step('end', '结束', '', null)
].join('')
}
},
C: {
hint: '自选在节点内点选;条件分支随表单天数高亮',
render() {
const days = Number(document.getElementById('days').value) || 0
const needCeo = days > 3
return [
step('start', '发起人', '', people([{ n: '张', name: '张三' }])),
step(
'approval',
'直属主管',
'发起人的主管',
people([{ n: '李', name: '李主管' }]),
'or'
),
`<div class="branch-box ${needCeo ? '' : 'active'}">
<b>条件分支</b> · 请假天数 ≤ 3 天 → 走人事抄送(当前 ${days} 天)
</div>`,
needCeo
? step(
'approval',
'总经理审批',
'发起人自选',
`<span class="person-chip pick" title="点击选择审批人">+ 选择审批人</span>`,
'and'
)
: '',
`<div class="branch-box ${needCeo ? 'active' : ''}">
<b>条件分支</b> · 请假天数 &gt; 3 天 → 需总经理审批
</div>`,
step(
'cc',
'抄送人',
'发起人自选',
`<span class="person-chip pick">+ 选择抄送人</span>`,
'cc'
),
step('end', '结束', '', null)
].join('')
}
}
}
function people(list) {
return list
.map(
(p) =>
`<span class="person-chip"><span class="dot">${p.n}</span>${p.name}</span>`
)
.join('')
}
/** type, nodeName, meta, peopleHtml, badge — 左侧统一灰色圆 */
function step(type, name, meta, peopleHtml, badge) {
const badgeHtml = badge
? `<span class="badge-mini ${badge}">${
badge === 'or' ? '或签' : badge === 'and' ? '会签' : '抄送'
}</span>`
: ''
const metaPart = meta
? `<span class="flow-card__sep">·</span><span class="flow-card__meta" title="${meta}">${meta}</span>`
: ''
const peoplePart = peopleHtml
? `<span class="flow-card__people">${peopleHtml}</span>`
: ''
return `
<div class="flow-step">
<div class="flow-rail">
<div class="flow-dot ${type === 'end' ? 'end' : ''}"></div>
<div class="flow-line"></div>
</div>
<div class="flow-card">
<div class="flow-card__row">
<span class="flow-card__name">${name}</span>
${badgeHtml}
${metaPart}
${peoplePart}
</div>
</div>
</div>`
}
let current = 'B'
function render() {
const mode = modes[current]
document.getElementById('flowHint').textContent = mode.hint
document.getElementById('flowChain').innerHTML = mode.render()
document.querySelectorAll('.mode-btn').forEach((btn) => {
btn.classList.toggle('on', btn.dataset.mode === current)
})
}
document.querySelectorAll('.mode-btn').forEach((btn) => {
btn.addEventListener('click', () => {
current = btn.dataset.mode
render()
})
})
document.getElementById('days').addEventListener('input', () => {
if (current === 'C') render()
})
document.getElementById('leaveType').addEventListener('change', () => {
if (current === 'C') render()
})
render()
</script>
</body>
</html>