615 lines
16 KiB
HTML
615 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(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> · 请假天数 > 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>
|