Files
shanxia2.0/docs/fresh-mint-quote-approver-setting-demo.html
T
2026-08-01 17:29:37 +08:00

787 lines
24 KiB
HTML

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Fresh Mint · 报价单审批人设置 Demo</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<style>
:root {
--primary: #10b981;
--primary-dark: #059669;
--primary-bg: #ecfdf5;
--text: #1e293b;
--text-muted: #64748b;
--border: #d9e8e2;
--surface: #ffffff;
--table-head: #f8fafc;
--bg-page: #f8fafc;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
--radius: 10px;
--warn-bg: #fffbeb;
--warn: #d97706;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 14px;
line-height: 1.5;
color: var(--text);
background: var(--bg-page);
}
.demo-page { max-width: 1180px; margin: 0 auto; padding: 32px 24px 96px; }
.demo-head { margin-bottom: 28px; }
.demo-head h1 {
margin: 0 0 8px;
font-size: 24px;
font-weight: 700;
letter-spacing: -0.02em;
}
.demo-head h1 span { color: var(--primary); }
.demo-head p { margin: 0; color: var(--text-muted); max-width: 820px; }
.toc {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 20px 0 0;
}
.toc a {
display: inline-flex;
align-items: center;
height: 32px;
padding: 0 12px;
border-radius: 8px;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text);
text-decoration: none;
font-size: 13px;
font-weight: 500;
}
.toc a:hover,
.toc a.rec {
border-color: var(--primary);
color: var(--primary-dark);
background: var(--primary-bg);
}
.section { margin-top: 40px; }
.section-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 14px;
}
.section-head h2 {
margin: 0 0 4px;
font-size: 18px;
font-weight: 700;
}
.section-head p { margin: 0; color: var(--text-muted); font-size: 13px; max-width: 720px; }
.badge {
flex-shrink: 0;
display: inline-flex;
align-items: center;
height: 26px;
padding: 0 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
}
.badge-ok { background: var(--primary-bg); color: var(--primary-dark); }
.badge-bad { background: #fef2f2; color: #b91c1c; }
.badge-mid { background: #eff6ff; color: #1d4ed8; }
.badge-warn { background: var(--warn-bg); color: var(--warn); }
.split {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
@media (max-width: 900px) {
.split { grid-template-columns: 1fr; }
}
.panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
}
.panel-top {
padding: 14px 16px;
border-bottom: 1px solid var(--border);
background: var(--surface);
}
.panel-top h3 {
margin: 0;
font-size: 15px;
font-weight: 700;
position: relative;
padding-left: 10px;
}
.panel-top h3::before {
content: "";
position: absolute;
left: 0;
top: 2px;
bottom: 2px;
width: 3px;
border-radius: 2px;
background: var(--primary);
}
.panel-top .hint {
margin: 6px 0 0;
padding-left: 10px;
font-size: 12px;
color: var(--text-muted);
}
.panel-body { padding: 16px; }
.layout-mock {
display: grid;
grid-template-columns: 180px 1fr;
min-height: 420px;
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
background: var(--surface);
box-shadow: var(--shadow);
}
.side {
background: var(--surface);
border-right: 1px solid var(--border);
padding: 16px 12px;
}
.side .logo {
font-weight: 700;
font-size: 13px;
margin-bottom: 16px;
padding: 0 8px;
}
.side .logo span { color: var(--primary); }
.nav-item {
display: block;
padding: 8px 10px;
border-radius: 8px;
font-size: 13px;
color: var(--text-muted);
margin-bottom: 2px;
}
.nav-item.active {
background: var(--primary-bg);
color: var(--primary-dark);
font-weight: 600;
}
.main { padding: 16px; background: var(--bg-page); }
.crumb {
font-size: 12px;
color: var(--text-muted);
margin-bottom: 12px;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px 16px;
}
.form-item { display: flex; flex-direction: column; gap: 6px; }
.form-item.full { grid-column: 1 / -1; }
.form-item label {
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
letter-spacing: 0.02em;
}
.form-item label .tip {
margin-left: 6px;
font-weight: 400;
color: #94a3b8;
}
.ctrl {
height: 36px;
border: 1px solid var(--border);
border-radius: 8px;
padding: 0 12px;
display: flex;
align-items: center;
background: #fff;
color: var(--text);
font-size: 14px;
}
.ctrl.multi {
height: auto;
min-height: 36px;
flex-wrap: wrap;
gap: 6px;
padding: 6px 10px;
}
.ctrl.textarea {
height: auto;
min-height: 88px;
align-items: flex-start;
padding: 10px 12px;
color: var(--text-muted);
}
.ctrl.muted { color: var(--text-muted); }
.tag {
display: inline-flex;
align-items: center;
height: 24px;
padding: 0 8px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
background: var(--primary-bg);
color: var(--primary-dark);
}
.tag.muted {
background: var(--table-head);
color: var(--text-muted);
}
.tag.warn {
background: var(--warn-bg);
color: var(--warn);
}
.btn-row {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 16px;
padding-top: 14px;
border-top: 1px solid var(--border);
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
height: 36px;
padding: 0 14px;
border-radius: 8px;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text);
font-size: 14px;
font-weight: 600;
font-family: inherit;
cursor: pointer;
}
.btn-main {
background: var(--primary);
border-color: var(--primary);
color: #fff;
}
.dialog {
border: 1px solid var(--border);
border-radius: var(--radius);
background: #fff;
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.12);
overflow: hidden;
max-width: 520px;
margin: 0 auto;
}
.dialog-hd {
padding: 14px 16px;
border-bottom: 1px solid var(--border);
font-weight: 700;
font-size: 15px;
}
.dialog-bd { padding: 16px; }
.dialog-ft {
padding: 12px 16px;
border-top: 1px solid var(--border);
background: var(--table-head);
display: flex;
justify-content: flex-end;
gap: 8px;
}
.callout {
margin-bottom: 12px;
padding: 10px 12px;
border-radius: 8px;
font-size: 12px;
line-height: 1.45;
}
.callout.ok {
background: var(--primary-bg);
color: var(--primary-dark);
border: 1px solid #a7f3d0;
}
.callout.bad {
background: #fef2f2;
color: #b91c1c;
border: 1px solid #fecaca;
}
.flow {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
padding: 14px 16px;
background: var(--table-head);
border-bottom: 1px solid var(--border);
font-size: 13px;
}
.flow-step {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 999px;
background: #fff;
border: 1px solid var(--border);
color: var(--text-muted);
font-weight: 500;
}
.flow-step.on {
background: var(--primary-bg);
border-color: #a7f3d0;
color: var(--primary-dark);
font-weight: 600;
}
.flow-arrow { color: #94a3b8; }
.options {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
@media (max-width: 900px) {
.options { grid-template-columns: 1fr; }
}
.option {
border: 1px solid var(--border);
border-radius: var(--radius);
background: #fff;
padding: 14px;
cursor: pointer;
transition: border-color .15s, box-shadow .15s, background .15s;
text-align: left;
font: inherit;
color: inherit;
position: relative;
}
.option:hover {
border-color: #a7f3d0;
background: #f8fffc;
}
.option.selected {
border-color: var(--primary);
background: var(--primary-bg);
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.18);
}
.option .letter {
display: inline-flex;
width: 24px;
height: 24px;
align-items: center;
justify-content: center;
border-radius: 6px;
background: var(--table-head);
color: var(--text-muted);
font-size: 12px;
font-weight: 700;
margin-bottom: 10px;
}
.option.selected .letter {
background: var(--primary);
color: #fff;
}
.option h3 {
margin: 0 0 6px;
font-size: 14px;
font-weight: 700;
}
.option p {
margin: 0;
font-size: 12px;
color: var(--text-muted);
line-height: 1.5;
}
.option .rec-flag {
position: absolute;
top: 12px;
right: 12px;
font-size: 11px;
font-weight: 700;
color: var(--primary-dark);
background: #fff;
border: 1px solid #a7f3d0;
border-radius: 999px;
padding: 2px 8px;
}
.option ul {
margin: 10px 0 0;
padding-left: 16px;
font-size: 12px;
color: var(--text-muted);
}
.option ul li { margin-bottom: 4px; }
.pick-bar {
position: sticky;
bottom: 0;
margin-top: 20px;
padding: 12px 16px;
border: 1px solid var(--border);
border-radius: var(--radius);
background: #fff;
box-shadow: 0 -4px 20px rgba(15, 23, 42, 0.06);
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.pick-bar strong { color: var(--primary-dark); }
.table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.table th {
text-align: left;
padding: 10px 12px;
background: var(--table-head);
color: var(--text-muted);
font-weight: 600;
border-bottom: 1px solid var(--border);
}
.table td {
padding: 10px 12px;
border-bottom: 1px solid var(--border);
}
.table tr:last-child td { border-bottom: none; }
.note {
margin-top: 12px;
font-size: 12px;
color: var(--text-muted);
}
</style>
</head>
<body>
<div class="demo-page">
<header class="demo-head">
<h1>报价单审批人设置 <span>Demo</span></h1>
<p>
客户痛点:每次「提交审批」都要手选审批人,且每次选的人几乎相同。
本 Demo 展示:① 新增设置页如何配置默认审批人;② 提交弹窗如何自动带出;③ 三种实现路径供选型。
</p>
<nav class="toc">
<a href="#pain">① 现状痛点</a>
<a href="#setting" class="rec">② 设置页方案</a>
<a href="#submit">③ 提交弹窗改造</a>
<a href="#approaches">④ 三种路径选型</a>
</nav>
</header>
<!-- ① 现状 -->
<section class="section" id="pain">
<div class="section-head">
<div>
<h2>① 现状:每次都要手选</h2>
<p>打开「提交审批」弹窗时,审批人为空,必须重新选择;备注也常重复填写。</p>
</div>
<span class="badge badge-bad">当前体验</span>
</div>
<div class="panel">
<div class="flow">
<span class="flow-step">选中报价单</span>
<span class="flow-arrow">→</span>
<span class="flow-step">点击提交审批</span>
<span class="flow-arrow">→</span>
<span class="flow-step on">手选审批人(重复操作)</span>
<span class="flow-arrow">→</span>
<span class="flow-step">填写备注</span>
<span class="flow-arrow">→</span>
<span class="flow-step">确定</span>
</div>
<div class="panel-body">
<div class="dialog">
<div class="dialog-hd">提交审批</div>
<div class="dialog-bd">
<div class="callout bad">审批人为空,每次都要重新点选;用户反馈「系统不好用」。</div>
<div class="form-item" style="margin-bottom:12px">
<label>选择审批人 <span class="tip">可选择多个审批人,按顺序依次审批</span></label>
<div class="ctrl muted">请选择审批人</div>
</div>
<div class="form-item">
<label>备注说明</label>
<div class="ctrl textarea">请输入备注说明</div>
</div>
</div>
<div class="dialog-ft">
<button class="btn" type="button">取消</button>
<button class="btn btn-main" type="button">确定</button>
</div>
</div>
</div>
</div>
</section>
<!-- ② 设置页 -->
<section class="section" id="setting">
<div class="section-head">
<div>
<h2>② 新增模块:报价单审批人设置</h2>
<p>建议挂在 CRM → 报价相关菜单下(如「报价单审批人设置」)。管理员配置一次,提交时默认带出。</p>
</div>
<span class="badge badge-ok">推荐示意</span>
</div>
<div class="layout-mock">
<aside class="side">
<div class="logo">山峡 <span>CRM</span></div>
<div class="nav-item">报价单管理</div>
<div class="nav-item active">报价单审批人设置</div>
<div class="nav-item">合同管理</div>
<div class="nav-item">客户管理</div>
</aside>
<div class="main">
<div class="crumb">CRM / 报价 / 报价单审批人设置</div>
<div class="panel">
<div class="panel-top">
<h3>报价单审批人设置</h3>
<p class="hint">配置租户级默认审批人;提交报价单审批时自动预填,仍可临时调整。</p>
</div>
<div class="panel-body">
<div class="form-grid">
<div class="form-item full">
<label>
默认审批人
<span class="tip">支持多人,按选择顺序依次审批</span>
</label>
<div class="ctrl multi">
<span class="tag">1. 张经理</span>
<span class="tag">2. 李总</span>
<span class="tag muted">+ 添加</span>
</div>
</div>
<div class="form-item">
<label>生效范围 <span class="tip">本 Demo 默认租户级</span></label>
<div class="ctrl">全租户统一</div>
</div>
<div class="form-item">
<label>状态</label>
<div class="ctrl"><span class="tag">启用</span></div>
</div>
<div class="form-item full">
<label>默认备注模板 <span class="tip">可选;提交时可改</span></label>
<div class="ctrl textarea" style="color:var(--text)">请按常规折扣政策审批。</div>
</div>
</div>
<div class="btn-row">
<button class="btn" type="button">重置</button>
<button class="btn btn-main" type="button">保存设置</button>
</div>
<p class="note">说明:本示意采用「租户级一份配置」。若后续要按部门/金额分岔,可再扩展,不必首版做满。</p>
</div>
</div>
</div>
</div>
</section>
<!-- ③ 提交弹窗改造 -->
<section class="section" id="submit">
<div class="section-head">
<div>
<h2>③ 提交审批:自动带出默认审批人</h2>
<p>打开弹窗即预填设置中的审批人;用户多数情况只需点「确定」,特殊单仍可改人。</p>
</div>
<span class="badge badge-ok">改造后</span>
</div>
<div class="split">
<div class="panel">
<div class="panel-top">
<h3>改造前</h3>
<p class="hint">每次空白,强制手选</p>
</div>
<div class="panel-body">
<div class="dialog">
<div class="dialog-hd">提交审批</div>
<div class="dialog-bd">
<div class="callout bad">必须重新选择审批人</div>
<div class="form-item" style="margin-bottom:12px">
<label>选择审批人</label>
<div class="ctrl muted">请选择审批人</div>
</div>
<div class="form-item">
<label>备注说明</label>
<div class="ctrl textarea">(空)</div>
</div>
</div>
<div class="dialog-ft">
<button class="btn" type="button">取消</button>
<button class="btn btn-main" type="button">确定</button>
</div>
</div>
</div>
</div>
<div class="panel">
<div class="panel-top">
<h3>改造后</h3>
<p class="hint">默认带出,可改可确认</p>
</div>
<div class="panel-body">
<div class="dialog">
<div class="dialog-hd">提交审批</div>
<div class="dialog-bd">
<div class="callout ok">已按「报价单审批人设置」预填:张经理 → 李总。可修改后提交。</div>
<div class="form-item" style="margin-bottom:12px">
<label>选择审批人 <span class="tip">已预填,可调整</span></label>
<div class="ctrl multi">
<span class="tag">1. 张经理</span>
<span class="tag">2. 李总</span>
</div>
</div>
<div class="form-item">
<label>备注说明</label>
<div class="ctrl textarea" style="color:var(--text)">请按常规折扣政策审批。</div>
</div>
</div>
<div class="dialog-ft">
<button class="btn" type="button">取消</button>
<button class="btn btn-main" type="button">确定</button>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- ④ 三种路径 -->
<section class="section" id="approaches">
<div class="section-head">
<div>
<h2>④ 实现路径选型(请点击选择)</h2>
<p>三种都能解决「重复选人」;范围与复杂度不同。点击卡片选型,底部会显示当前选择。</p>
</div>
<span class="badge badge-mid">待确认</span>
</div>
<div class="options" id="approachOptions">
<button type="button" class="option selected" data-choice="a" onclick="pickApproach(this)">
<span class="rec-flag">推荐</span>
<div class="letter">A</div>
<h3>租户级默认审批人</h3>
<p>新增「报价单审批人设置」页,存一份有序审批人列表;提交弹窗自动预填,仍可改。</p>
<ul>
<li>改动小,最快落地</li>
<li>贴合「每次都选同一批人」</li>
<li>不引入完整工作流引擎</li>
</ul>
</button>
<button type="button" class="option" data-choice="b" onclick="pickApproach(this)">
<div class="letter">B</div>
<h3>记住「上次选的人」</h3>
<p>不做设置页;按当前用户缓存最近一次提交的审批人,下次自动回填。</p>
<ul>
<li>开发更快</li>
<li>无统一管控,新人无默认</li>
<li>换人时可能误用旧配置</li>
</ul>
</button>
<button type="button" class="option" data-choice="c" onclick="pickApproach(this)">
<div class="letter">C</div>
<h3>接入通用审批流</h3>
<p>走 OpenSpec 工作流引擎(指定角色/条件分支)。长期正确,但本期过重。</p>
<ul>
<li>能力完整、可复用合同等业务</li>
<li>依赖尚未落地的工作流子系统</li>
<li>交付周期长,超出当前痛点</li>
</ul>
</button>
</div>
<div class="pick-bar">
<div>
当前选择:<strong id="pickLabel">A · 租户级默认审批人</strong>
<div class="note" style="margin:4px 0 0">看完后请在对话里回复选型(如「选 A」),我们再继续澄清细节。</div>
</div>
<span class="badge badge-ok" id="pickBadge">方案 A</span>
</div>
<div class="panel" style="margin-top:16px">
<div class="panel-top">
<h3>方案 A 若落地:涉及面速览</h3>
</div>
<table class="table">
<thead>
<tr>
<th>层</th>
<th>内容</th>
</tr>
</thead>
<tbody>
<tr>
<td>菜单 / 页面</td>
<td>CRM 下新增「报价单审批人设置」(表单页,非复杂列表)</td>
</tr>
<tr>
<td>数据</td>
<td>租户维度配置表:有序 approver_ids、可选默认备注、启用状态</td>
</tr>
<tr>
<td>提交审批</td>
<td>打开弹窗时读取配置并预填;未配置则保持现状必选</td>
</tr>
<tr>
<td>权限</td>
<td>设置页建议管理员可改;普通业务员只读使用默认值</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
<script>
const labels = {
a: 'A · 租户级默认审批人',
b: 'B · 记住上次选的人',
c: 'C · 接入通用审批流'
};
const badges = { a: '方案 A', b: '方案 B', c: '方案 C' };
function pickApproach(el) {
document.querySelectorAll('#approachOptions .option').forEach((n) => n.classList.remove('selected'));
el.classList.add('selected');
const choice = el.getAttribute('data-choice');
document.getElementById('pickLabel').textContent = labels[choice] || choice;
document.getElementById('pickBadge').textContent = badges[choice] || choice;
try {
localStorage.setItem('quote-approver-demo-choice', choice);
} catch (e) {}
}
try {
const saved = localStorage.getItem('quote-approver-demo-choice');
if (saved) {
const el = document.querySelector('#approachOptions .option[data-choice="' + saved + '"]');
if (el) pickApproach(el);
}
} catch (e) {}
</script>
</body>
</html>