| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786 |
- <!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>
|