提交
This commit is contained in:
1 parent
06956b742b
commit
e59b2aa6cb
104 files changed
+653227
-7621
No files matched your search
@@ -0,0 +1 @@
|
||||
{"reason":"owner process exited","timestamp":1785154530965}
|
||||
@@ -0,0 +1 @@
|
||||
27319
|
||||
@@ -0,0 +1,786 @@
|
||||
<!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>
|
||||
@@ -0,0 +1 @@
|
||||
{"reason":"owner process exited","timestamp":1785559480927}
|
||||
@@ -0,0 +1 @@
|
||||
37382
|
||||
@@ -0,0 +1,786 @@
|
||||
<!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>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
|
||||
<p class="subtitle">设计评审进行中,请在对话里确认各节…</p>
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
|
||||
<p class="subtitle">已确认方案 A。继续在对话里澄清细节…</p>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
{"type":"server-started","port":54761,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:54761","screen_dir":"/Users/wuzilong/work/00code/shanxia2.0/.superpowers/brainstorm/37644-1785559508/content","state_dir":"/Users/wuzilong/work/00code/shanxia2.0/.superpowers/brainstorm/37644-1785559508/state"}
|
||||
@@ -0,0 +1 @@
|
||||
37644
|
||||
@@ -0,0 +1,855 @@
|
||||
<!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;
|
||||
--text: #0f172a;
|
||||
--muted: #64748b;
|
||||
--border: #e2e8f0;
|
||||
--bg: #f1f5f9;
|
||||
--panel: #ffffff;
|
||||
--danger: #ef4444;
|
||||
--warn: #d97706;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Inter, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
color: var(--text);
|
||||
background: #f8fafc;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.page { padding: 20px 24px 40px; max-width: 1280px; margin: 0 auto; }
|
||||
h1 { font-size: 22px; margin: 0 0 6px; }
|
||||
.lead { color: var(--muted); font-size: 14px; margin: 0 0 18px; }
|
||||
.hint {
|
||||
background: var(--primary-bg);
|
||||
border: 1px solid #a7f3d0;
|
||||
color: #065f46;
|
||||
border-radius: 10px;
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.tab {
|
||||
border: 1px solid var(--border);
|
||||
background: #fff;
|
||||
color: var(--text);
|
||||
border-radius: 999px;
|
||||
padding: 7px 12px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: .15s ease;
|
||||
}
|
||||
.tab:hover { border-color: #6ee7b7; }
|
||||
.tab.active {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.tab .w { opacity: .75; margin-left: 4px; font-weight: 500; }
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 320px 1fr;
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
@media (max-width: 960px) {
|
||||
.layout { grid-template-columns: 1fr; }
|
||||
}
|
||||
.meta-card, .preview-shell {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.meta-card { padding: 16px; }
|
||||
.meta-card h2 { margin: 0 0 8px; font-size: 16px; }
|
||||
.meta-card .desc { font-size: 13px; color: var(--muted); margin: 0 0 12px; }
|
||||
.chip-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
|
||||
.chip {
|
||||
font-size: 11px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 999px;
|
||||
background: #f1f5f9;
|
||||
color: #334155;
|
||||
}
|
||||
.chip.good { background: var(--primary-bg); color: #047857; }
|
||||
.chip.warn { background: #fff7ed; color: #c2410c; }
|
||||
.pros-cons { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; font-size: 12px; }
|
||||
.pros, .cons {
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
.pros h4, .cons h4 { margin: 0 0 6px; font-size: 12px; }
|
||||
.pros ul, .cons ul { margin: 0; padding-left: 16px; color: var(--muted); }
|
||||
.select-btn {
|
||||
margin-top: 14px;
|
||||
width: 100%;
|
||||
border: 1px solid var(--primary);
|
||||
background: var(--primary-bg);
|
||||
color: var(--primary-dark);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.select-btn.picked {
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
}
|
||||
.preview-shell .preview-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: #fafafa;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.preview-frame {
|
||||
height: 420px;
|
||||
background: var(--bg);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ========== shared mock chrome ========== */
|
||||
.app {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 12px;
|
||||
}
|
||||
.top {
|
||||
height: 48px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 0 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.brand {
|
||||
font-weight: 700;
|
||||
color: var(--primary-dark);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mod-select {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 5px 10px;
|
||||
background: #fff;
|
||||
color: var(--text);
|
||||
cursor: default;
|
||||
}
|
||||
.mod-select .dot {
|
||||
width: 8px; height: 8px; border-radius: 50%;
|
||||
background: var(--primary);
|
||||
}
|
||||
.spacer { flex: 1; }
|
||||
.top-right { color: var(--muted); display: flex; gap: 10px; align-items: center; }
|
||||
.avatar {
|
||||
width: 24px; height: 24px; border-radius: 50%;
|
||||
background: linear-gradient(135deg, #6ee7b7, #34d399);
|
||||
}
|
||||
.body { flex: 1; display: flex; min-height: 0; }
|
||||
.side {
|
||||
background: #fff;
|
||||
border-right: 1px solid var(--border);
|
||||
overflow: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.main {
|
||||
flex: 1;
|
||||
padding: 14px;
|
||||
min-width: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
.panel {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
height: 100%;
|
||||
padding: 14px;
|
||||
}
|
||||
.panel h3 { margin: 0 0 8px; font-size: 14px; }
|
||||
.panel p { margin: 0; color: var(--muted); font-size: 12px; }
|
||||
.bars {
|
||||
margin-top: 14px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
.bar {
|
||||
height: 10px;
|
||||
border-radius: 6px;
|
||||
background: linear-gradient(90deg, #d1fae5, #f1f5f9);
|
||||
}
|
||||
.bar.w2 { width: 72%; }
|
||||
.bar.w3 { width: 54%; }
|
||||
.menu-item, .menu-group {
|
||||
padding: 8px 12px;
|
||||
color: #334155;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.menu-group { font-weight: 600; color: #0f172a; }
|
||||
.menu-item.active {
|
||||
background: var(--primary-bg);
|
||||
color: var(--primary-dark);
|
||||
border-right: 2px solid var(--primary);
|
||||
}
|
||||
.menu-item.child { padding-left: 28px; font-size: 11px; color: var(--muted); }
|
||||
.ico {
|
||||
width: 16px; height: 16px; border-radius: 4px;
|
||||
background: #e2e8f0; flex-shrink: 0;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
font-size: 9px; color: #64748b;
|
||||
}
|
||||
.menu-item.active .ico { background: #a7f3d0; color: #047857; }
|
||||
.width-tag {
|
||||
position: absolute;
|
||||
left: 8px; bottom: 8px;
|
||||
background: rgba(15,23,42,.78);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.hamburger {
|
||||
width: 28px; height: 28px; border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
background: #fff; cursor: pointer;
|
||||
}
|
||||
.top-nav { display: flex; gap: 2px; align-items: center; }
|
||||
.top-nav a {
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
color: #334155;
|
||||
text-decoration: none;
|
||||
font-size: 12px;
|
||||
}
|
||||
.top-nav a.on { background: var(--primary-bg); color: var(--primary-dark); font-weight: 600; }
|
||||
.mega {
|
||||
position: absolute;
|
||||
top: 48px; left: 120px;
|
||||
width: 420px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 12px 32px rgba(15,23,42,.12);
|
||||
padding: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
z-index: 8;
|
||||
}
|
||||
.mega .col h4 { margin: 0 0 6px; font-size: 11px; color: var(--muted); }
|
||||
.mega .col div {
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
color: #334155;
|
||||
}
|
||||
.mega .col div.on { background: var(--primary-bg); color: var(--primary-dark); }
|
||||
.dual-rail {
|
||||
width: 64px;
|
||||
background: #0f172a;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.dual-rail .ri {
|
||||
width: 40px; height: 40px; border-radius: 10px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 11px; color: #94a3b8; background: transparent;
|
||||
}
|
||||
.dual-rail .ri.on { background: rgba(16,185,129,.25); color: #6ee7b7; }
|
||||
.dual-panel {
|
||||
width: 160px;
|
||||
background: #fff;
|
||||
border-right: 1px solid var(--border);
|
||||
padding: 8px 0;
|
||||
}
|
||||
.icon-rail {
|
||||
width: 64px;
|
||||
background: #fff;
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 8px 0;
|
||||
position: relative;
|
||||
}
|
||||
.icon-rail .ri {
|
||||
width: 40px; height: 40px; border-radius: 10px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
color: #64748b; font-size: 11px;
|
||||
}
|
||||
.icon-rail .ri.on { background: var(--primary-bg); color: var(--primary-dark); }
|
||||
.flyout {
|
||||
position: absolute;
|
||||
left: 64px; top: 56px;
|
||||
width: 180px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0 10px 10px 0;
|
||||
box-shadow: 8px 8px 24px rgba(15,23,42,.08);
|
||||
padding: 8px 0;
|
||||
z-index: 6;
|
||||
}
|
||||
.drawer-mask {
|
||||
position: absolute;
|
||||
inset: 48px 0 0 0;
|
||||
background: rgba(15,23,42,.28);
|
||||
z-index: 4;
|
||||
}
|
||||
.drawer {
|
||||
position: absolute;
|
||||
top: 48px; left: 0; bottom: 0;
|
||||
width: 220px;
|
||||
background: #fff;
|
||||
z-index: 5;
|
||||
box-shadow: 8px 0 24px rgba(15,23,42,.12);
|
||||
overflow: auto;
|
||||
}
|
||||
.cmdk {
|
||||
position: absolute;
|
||||
left: 50%; top: 28%;
|
||||
transform: translateX(-50%);
|
||||
width: min(420px, 86%);
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 48px rgba(15,23,42,.18);
|
||||
z-index: 8;
|
||||
overflow: hidden;
|
||||
}
|
||||
.cmdk input {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 12px 14px;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
}
|
||||
.cmdk .row {
|
||||
padding: 8px 14px;
|
||||
font-size: 12px;
|
||||
color: #334155;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.cmdk .row.on { background: var(--primary-bg); color: var(--primary-dark); }
|
||||
.cmdk .kbd {
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 1px 5px;
|
||||
}
|
||||
.top-tabs {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: #fff;
|
||||
padding: 0 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.top-tabs span {
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
.top-tabs span.on {
|
||||
color: var(--primary-dark);
|
||||
border-bottom-color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
.pills {
|
||||
display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px;
|
||||
}
|
||||
.pill {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
padding: 4px 10px;
|
||||
font-size: 11px;
|
||||
color: #475569;
|
||||
background: #fff;
|
||||
}
|
||||
.pill.on { border-color: #6ee7b7; background: var(--primary-bg); color: var(--primary-dark); }
|
||||
.fav-bar {
|
||||
margin-top: 16px;
|
||||
padding: 12px 14px;
|
||||
border: 1px dashed #a7f3d0;
|
||||
border-radius: 10px;
|
||||
background: #f0fdf4;
|
||||
font-size: 13px;
|
||||
color: #065f46;
|
||||
}
|
||||
.fav-bar strong { font-weight: 700; }
|
||||
.hidden { display: none !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<h1>菜单布局模式对比(省侧栏空间)</h1>
|
||||
<p class="lead">
|
||||
现状门户布局侧栏约 <strong>270px</strong>。下面 8 种模式可切换预览;喜欢的可点「标记为候选」(可多选)。
|
||||
</p>
|
||||
<div class="hint">
|
||||
点击上方标签切换预览 → 右侧为可交互线框 → 左侧看优缺点 → 点「标记为候选」收藏偏好。看完后回终端告诉我你倾向哪些。
|
||||
</div>
|
||||
|
||||
<div class="tabs" id="tabs"></div>
|
||||
|
||||
<div class="layout">
|
||||
<aside class="meta-card" id="meta"></aside>
|
||||
<section class="preview-shell">
|
||||
<div class="preview-bar">
|
||||
<span id="preview-title">预览</span>
|
||||
<span>Fresh Mint · 线框 Demo</span>
|
||||
</div>
|
||||
<div class="preview-frame" id="preview"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="fav-bar" id="fav">尚未标记候选。可多选后告诉我偏好。</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const patterns = [
|
||||
{
|
||||
id: 'a-current',
|
||||
letter: 'A',
|
||||
name: '现状:模块下拉 + 宽侧栏树',
|
||||
width: '270px',
|
||||
desc: '顶栏切换一级模块,左侧展示二/三级菜单树。信息完整,但内容区最窄。',
|
||||
tags: ['现状基线', '信息完整'],
|
||||
pros: ['层级一眼可见', '与现有实现一致', '深层级友好'],
|
||||
cons: ['占宽最大', '列表页横向空间被挤'],
|
||||
render() {
|
||||
return `
|
||||
<div class="app">
|
||||
<div class="top">
|
||||
<div class="brand">山峡</div>
|
||||
<div class="mod-select"><span class="dot"></span> CRM 客户 <span>▾</span></div>
|
||||
<div class="spacer"></div>
|
||||
<div class="top-right"><span>搜索</span><div class="avatar"></div></div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<aside class="side" style="width:270px">
|
||||
<div class="menu-group"><span class="ico">客</span>客户管理</div>
|
||||
<div class="menu-item active"><span class="ico">•</span>客户列表</div>
|
||||
<div class="menu-item child">联系人</div>
|
||||
<div class="menu-item child">跟进记录</div>
|
||||
<div class="menu-group"><span class="ico">商</span>商机合同</div>
|
||||
<div class="menu-item"><span class="ico">•</span>商机</div>
|
||||
<div class="menu-item"><span class="ico">•</span>合同</div>
|
||||
<div class="menu-item"><span class="ico">•</span>报价单</div>
|
||||
</aside>
|
||||
<main class="main"><div class="panel"><h3>客户列表</h3><p>侧栏 270px,内容区最紧。</p><div class="bars"><div class="bar"></div><div class="bar w2"></div><div class="bar w3"></div></div></div></main>
|
||||
</div>
|
||||
<div class="width-tag">侧栏 270px</div>
|
||||
</div>`
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'b-narrow',
|
||||
letter: 'B',
|
||||
name: '窄侧栏文字树',
|
||||
width: '180px',
|
||||
desc: '保留现有门户结构,仅把侧栏从 270 收到 180。改动最小,立刻多出约 90px 内容宽。',
|
||||
tags: ['改动最小', '省 ~90px', '推荐起步'],
|
||||
pros: ['几乎无学习成本', '实现成本最低', '仍保留树形层级'],
|
||||
cons: ['长菜单名易截断', '三级缩进空间更紧'],
|
||||
render() {
|
||||
return `
|
||||
<div class="app">
|
||||
<div class="top">
|
||||
<div class="brand">山峡</div>
|
||||
<div class="mod-select"><span class="dot"></span> CRM 客户 <span>▾</span></div>
|
||||
<div class="spacer"></div>
|
||||
<div class="top-right"><div class="avatar"></div></div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<aside class="side" style="width:180px">
|
||||
<div class="menu-group">客户管理</div>
|
||||
<div class="menu-item active">客户列表</div>
|
||||
<div class="menu-item child">联系人</div>
|
||||
<div class="menu-item child">跟进记录</div>
|
||||
<div class="menu-group">商机合同</div>
|
||||
<div class="menu-item">商机</div>
|
||||
<div class="menu-item">合同</div>
|
||||
</aside>
|
||||
<main class="main"><div class="panel"><h3>客户列表</h3><p>同样门户结构,侧栏更瘦,内容更宽。</p><div class="bars"><div class="bar"></div><div class="bar w2"></div><div class="bar w3"></div></div></div></main>
|
||||
</div>
|
||||
<div class="width-tag">侧栏 180px · 省 ~90px</div>
|
||||
</div>`
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'c-icon-rail',
|
||||
letter: 'C',
|
||||
name: '图标轨道 + 悬停飞出',
|
||||
width: '64px',
|
||||
desc: '默认只留 64px 图标轨;悬停/点击某图标时飞出二级菜单面板。内容区几乎全宽。',
|
||||
tags: ['极省空间', '悬停飞出'],
|
||||
pros: ['内容区最宽之一', '视觉干净', '已有收起态可复用'],
|
||||
cons: ['发现性变弱', '需靠 tooltip/飞出', '深菜单要二次交互'],
|
||||
render() {
|
||||
return `
|
||||
<div class="app">
|
||||
<div class="top">
|
||||
<div class="brand">山峡</div>
|
||||
<div class="mod-select"><span class="dot"></span> CRM <span>▾</span></div>
|
||||
<div class="spacer"></div>
|
||||
<div class="top-right"><div class="avatar"></div></div>
|
||||
</div>
|
||||
<div class="body" style="position:relative">
|
||||
<aside class="icon-rail">
|
||||
<div class="ri on">客</div>
|
||||
<div class="ri">商</div>
|
||||
<div class="ri">合</div>
|
||||
<div class="ri">报</div>
|
||||
<div class="ri">设</div>
|
||||
<div class="flyout">
|
||||
<div class="menu-item active">客户列表</div>
|
||||
<div class="menu-item">联系人</div>
|
||||
<div class="menu-item">跟进记录</div>
|
||||
<div class="menu-item">客户公海</div>
|
||||
</div>
|
||||
</aside>
|
||||
<main class="main"><div class="panel"><h3>客户列表</h3><p>默认只占 64px;子菜单以飞出层展示。</p><div class="bars"><div class="bar"></div><div class="bar w2"></div><div class="bar w3"></div></div></div></main>
|
||||
</div>
|
||||
<div class="width-tag">常驻 64px · 飞出 180px(临时)</div>
|
||||
</div>`
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'd-dual',
|
||||
letter: 'D',
|
||||
name: '双栏菜单(图标列 + 二级面板)',
|
||||
width: '64+160',
|
||||
desc: '左侧深色图标一级,右侧浅色二级列表。总宽约 224px,比现状略省,层级更清晰。对应系统已有 DUAL_MENU。',
|
||||
tags: ['系统已有类型', '层级清晰'],
|
||||
pros: ['一级/二级分离清楚', '可复用现有双栏能力', '适合模块多的门户'],
|
||||
cons: ['仍占约 220px', '三级需要再展开或压进二级'],
|
||||
render() {
|
||||
return `
|
||||
<div class="app">
|
||||
<div class="top">
|
||||
<div class="brand">山峡一体化</div>
|
||||
<div class="spacer"></div>
|
||||
<div class="top-right"><span>管理员</span><div class="avatar"></div></div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<aside class="dual-rail">
|
||||
<div class="ri on">CRM</div>
|
||||
<div class="ri">报销</div>
|
||||
<div class="ri">地磅</div>
|
||||
<div class="ri">计划</div>
|
||||
<div class="ri">系统</div>
|
||||
</aside>
|
||||
<aside class="dual-panel">
|
||||
<div class="menu-group" style="font-size:11px;color:#94a3b8;padding:4px 12px">CRM</div>
|
||||
<div class="menu-item active">客户列表</div>
|
||||
<div class="menu-item">商机</div>
|
||||
<div class="menu-item">合同</div>
|
||||
<div class="menu-item">报价单</div>
|
||||
<div class="menu-item">产品</div>
|
||||
</aside>
|
||||
<main class="main"><div class="panel"><h3>客户列表</h3><p>一级在图标轨,二级在窄面板;顶栏不再放模块下拉。</p><div class="bars"><div class="bar"></div><div class="bar w2"></div></div></div></main>
|
||||
</div>
|
||||
<div class="width-tag">64 + 160 ≈ 224px</div>
|
||||
</div>`
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'e-top-only',
|
||||
letter: 'E',
|
||||
name: '纯顶栏横向菜单',
|
||||
width: '0',
|
||||
desc: '取消左侧栏;一级/二级全部放顶栏。内容区 100% 宽,适合菜单层级浅、条目不多的模块。',
|
||||
tags: ['零侧栏', '内容最宽'],
|
||||
pros: ['内容区最大化', '页面干净', '横向浏览习惯'],
|
||||
cons: ['菜单一多会挤', '三级几乎放不下', '窄屏需折叠'],
|
||||
render() {
|
||||
return `
|
||||
<div class="app">
|
||||
<div class="top">
|
||||
<div class="brand">山峡</div>
|
||||
<nav class="top-nav">
|
||||
<a href="#" class="on">客户</a>
|
||||
<a href="#">商机</a>
|
||||
<a href="#">合同</a>
|
||||
<a href="#">报价</a>
|
||||
<a href="#">产品 ▾</a>
|
||||
<a href="#">更多 ▾</a>
|
||||
</nav>
|
||||
<div class="spacer"></div>
|
||||
<div class="top-right"><div class="avatar"></div></div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<main class="main" style="width:100%"><div class="panel"><h3>客户列表</h3><p>无侧栏,表格可铺满整行。</p><div class="bars"><div class="bar"></div><div class="bar w2"></div><div class="bar w3"></div></div></div></main>
|
||||
</div>
|
||||
<div class="width-tag">侧栏 0 · 内容 100%</div>
|
||||
</div>`
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'f-mega',
|
||||
letter: 'F',
|
||||
name: '顶栏 + Mega Menu',
|
||||
width: '0',
|
||||
desc: '顶栏只放一级;悬停展开大面板展示二/三级分组。兼顾零侧栏与深层级,适合门户多业务线。',
|
||||
tags: ['零侧栏', '深层级友好', '门户感强'],
|
||||
pros: ['内容全宽', '分组信息密度高', '适合多子系统'],
|
||||
cons: ['悬停面板实现成本', '触控设备体验一般', '要控制面板高度'],
|
||||
render() {
|
||||
return `
|
||||
<div class="app" style="position:relative">
|
||||
<div class="top">
|
||||
<div class="brand">山峡</div>
|
||||
<nav class="top-nav">
|
||||
<a href="#" class="on">CRM ▾</a>
|
||||
<a href="#">报销</a>
|
||||
<a href="#">地磅</a>
|
||||
<a href="#">计划</a>
|
||||
<a href="#">系统</a>
|
||||
</nav>
|
||||
<div class="spacer"></div>
|
||||
<div class="top-right"><div class="avatar"></div></div>
|
||||
</div>
|
||||
<div class="mega">
|
||||
<div class="col">
|
||||
<h4>客户</h4>
|
||||
<div class="on">客户列表</div>
|
||||
<div>联系人</div>
|
||||
<div>跟进记录</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h4>交易</h4>
|
||||
<div>商机</div>
|
||||
<div>合同</div>
|
||||
<div>报价单</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<main class="main"><div class="panel"><h3>客户列表</h3><p>顶栏一级 + 大面板二三级,侧栏完全去掉。</p><div class="bars"><div class="bar"></div><div class="bar w2"></div></div></div></main>
|
||||
</div>
|
||||
<div class="width-tag">侧栏 0 · Mega 临时浮层</div>
|
||||
</div>`
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'g-drawer',
|
||||
letter: 'G',
|
||||
name: '抽屉侧栏(默认隐藏)',
|
||||
width: '0→220',
|
||||
desc: '默认无侧栏;点汉堡按钮滑出抽屉菜单。适合偶发切换菜单、长时间盯列表/表格的场景。',
|
||||
tags: ['默认全宽', '按需打开'],
|
||||
pros: ['日常内容全宽', '菜单完整不砍', '实现相对直接'],
|
||||
cons: ['切换菜单多一步', '频繁跳转效率略低'],
|
||||
render() {
|
||||
return `
|
||||
<div class="app" style="position:relative">
|
||||
<div class="top">
|
||||
<button class="hamburger" title="菜单">☰</button>
|
||||
<div class="brand">山峡</div>
|
||||
<div class="mod-select"><span class="dot"></span> CRM <span>▾</span></div>
|
||||
<div class="spacer"></div>
|
||||
<div class="top-right"><div class="avatar"></div></div>
|
||||
</div>
|
||||
<div class="drawer-mask"></div>
|
||||
<aside class="drawer">
|
||||
<div class="menu-group">客户管理</div>
|
||||
<div class="menu-item active">客户列表</div>
|
||||
<div class="menu-item">联系人</div>
|
||||
<div class="menu-group">商机合同</div>
|
||||
<div class="menu-item">商机</div>
|
||||
<div class="menu-item">合同</div>
|
||||
</aside>
|
||||
<div class="body">
|
||||
<main class="main"><div class="panel"><h3>客户列表</h3><p>默认全宽;需要时再拉开抽屉。</p><div class="bars"><div class="bar"></div><div class="bar w2"></div><div class="bar w3"></div></div></div></main>
|
||||
</div>
|
||||
<div class="width-tag">默认 0 · 打开时 220px 遮罩层</div>
|
||||
</div>`
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'h-cmdk',
|
||||
letter: 'H',
|
||||
name: '极简顶栏 + 命令面板',
|
||||
width: '0',
|
||||
desc: '侧栏去掉,顶栏只保留模块与搜索。用 ⌘K / Ctrl+K 打开命令面板跳转页面。高级用户效率最高。',
|
||||
tags: ['极简', '键盘驱动', '进阶'],
|
||||
pros: ['UI 最干净', '跨模块跳转快', '内容全宽'],
|
||||
cons: ['新手发现成本高', '仍需少量可见入口', '不适合完全替代菜单'],
|
||||
render() {
|
||||
return `
|
||||
<div class="app" style="position:relative">
|
||||
<div class="top">
|
||||
<div class="brand">山峡</div>
|
||||
<div class="mod-select"><span class="dot"></span> CRM <span>▾</span></div>
|
||||
<div class="pills" style="margin:0">
|
||||
<span class="pill on">客户</span>
|
||||
<span class="pill">商机</span>
|
||||
<span class="pill">合同</span>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<div class="top-right"><span class="kbd" style="border:1px solid var(--border);border-radius:4px;padding:2px 6px;font-size:10px;color:var(--muted)">⌘K</span><div class="avatar"></div></div>
|
||||
</div>
|
||||
<div class="cmdk">
|
||||
<input value="客户" readonly />
|
||||
<div class="row on"><span>客户列表</span><span class="kbd">↵</span></div>
|
||||
<div class="row"><span>客户公海</span><span class="kbd">↵</span></div>
|
||||
<div class="row"><span>联系人</span><span class="kbd">↵</span></div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<main class="main"><div class="panel"><h3>工作区</h3><p>高频页可用顶栏胶囊;其余靠命令面板直达。</p><div class="bars"><div class="bar"></div><div class="bar w2"></div></div></div></main>
|
||||
</div>
|
||||
<div class="width-tag">侧栏 0 · 命令面板按需</div>
|
||||
</div>`
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'i-module-tabs',
|
||||
letter: 'I',
|
||||
name: '顶栏模块 + 二级横向 Tabs',
|
||||
width: '0',
|
||||
desc: '顶栏下拉选一级模块后,二级以横向 Tabs 展示;三级可进页面内左侧或页内 Tab。适合二级条目较少的模块。',
|
||||
tags: ['零侧栏', '二级横排'],
|
||||
pros: ['内容全宽', '二级切换很直观', '与现有 WorkTab 可协同'],
|
||||
cons: ['二级过多会换行拥挤', '三级仍要另找位置'],
|
||||
render() {
|
||||
return `
|
||||
<div class="app">
|
||||
<div class="top">
|
||||
<div class="brand">山峡</div>
|
||||
<div class="mod-select"><span class="dot"></span> CRM 客户 <span>▾</span></div>
|
||||
<div class="spacer"></div>
|
||||
<div class="top-right"><div class="avatar"></div></div>
|
||||
</div>
|
||||
<div class="top-tabs">
|
||||
<span class="on">客户列表</span>
|
||||
<span>联系人</span>
|
||||
<span>商机</span>
|
||||
<span>合同</span>
|
||||
<span>报价单</span>
|
||||
<span>产品</span>
|
||||
</div>
|
||||
<div class="body">
|
||||
<main class="main"><div class="panel"><h3>客户列表</h3><p>二级在顶栏下方横滑/横排,不再占左侧。</p><div class="bars"><div class="bar"></div><div class="bar w2"></div><div class="bar w3"></div></div></div></main>
|
||||
</div>
|
||||
<div class="width-tag">侧栏 0 · 二级 Tabs ~40px 高</div>
|
||||
</div>`
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const picked = new Set()
|
||||
let current = patterns[1].id // default show B
|
||||
|
||||
const tabsEl = document.getElementById('tabs')
|
||||
const metaEl = document.getElementById('meta')
|
||||
const previewEl = document.getElementById('preview')
|
||||
const previewTitle = document.getElementById('preview-title')
|
||||
const favEl = document.getElementById('fav')
|
||||
|
||||
function renderTabs() {
|
||||
tabsEl.innerHTML = patterns.map(p => `
|
||||
<button type="button" class="tab ${p.id === current ? 'active' : ''}" data-id="${p.id}">
|
||||
${p.letter}. ${p.name.split(':')[0] || p.name}
|
||||
<span class="w">${p.width}</span>
|
||||
</button>
|
||||
`).join('')
|
||||
tabsEl.querySelectorAll('.tab').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
current = btn.getAttribute('data-id')
|
||||
paint()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function paint() {
|
||||
const p = patterns.find(x => x.id === current)
|
||||
renderTabs()
|
||||
previewTitle.textContent = `预览 ${p.letter} · ${p.name}`
|
||||
previewEl.innerHTML = p.render()
|
||||
metaEl.innerHTML = `
|
||||
<h2>${p.letter}. ${p.name}</h2>
|
||||
<p class="desc">${p.desc}</p>
|
||||
<div class="chip-row">
|
||||
${p.tags.map(t => `<span class="chip ${t.includes('推荐') || t.includes('极省') || t.includes('零侧栏') ? 'good' : ''}">${t}</span>`).join('')}
|
||||
<span class="chip warn">占宽 ${p.width}</span>
|
||||
</div>
|
||||
<div class="pros-cons">
|
||||
<div class="pros"><h4>优点</h4><ul>${p.pros.map(i => `<li>${i}</li>`).join('')}</ul></div>
|
||||
<div class="cons"><h4>代价</h4><ul>${p.cons.map(i => `<li>${i}</li>`).join('')}</ul></div>
|
||||
</div>
|
||||
<button type="button" class="select-btn ${picked.has(p.id) ? 'picked' : ''}" id="pick-btn"
|
||||
data-choice="${p.id}" onclick="toggleSelect(this)">
|
||||
${picked.has(p.id) ? '✓ 已标记为候选(再点取消)' : '标记为候选(可多选)'}
|
||||
</button>
|
||||
`
|
||||
document.getElementById('pick-btn').addEventListener('click', () => {
|
||||
if (picked.has(p.id)) picked.delete(p.id)
|
||||
else picked.add(p.id)
|
||||
updateFav()
|
||||
// re-sync button label after companion toggleSelect
|
||||
setTimeout(() => {
|
||||
const btn = document.getElementById('pick-btn')
|
||||
if (!btn) return
|
||||
const on = picked.has(p.id)
|
||||
btn.classList.toggle('picked', on)
|
||||
btn.textContent = on ? '✓ 已标记为候选(再点取消)' : '标记为候选(可多选)'
|
||||
}, 0)
|
||||
})
|
||||
updateFav()
|
||||
}
|
||||
|
||||
function updateFav() {
|
||||
if (!picked.size) {
|
||||
favEl.innerHTML = '尚未标记候选。可多选后告诉我偏好。'
|
||||
return
|
||||
}
|
||||
const names = patterns.filter(p => picked.has(p.id)).map(p => `${p.letter}`)
|
||||
favEl.innerHTML = `已标记候选:<strong>${names.join('、')}</strong>。回终端告诉我最终倾向(可排序)。`
|
||||
}
|
||||
|
||||
// expose for companion helper if present
|
||||
window.__menuPatterns = patterns
|
||||
paint()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
{"reason":"owner process exited","timestamp":1785548434969}
|
||||
@@ -0,0 +1 @@
|
||||
69345
|
||||
@@ -0,0 +1,646 @@
|
||||
<!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;
|
||||
--text: #0f172a;
|
||||
--muted: #64748b;
|
||||
--border: #e2e8f0;
|
||||
--bg: #f1f5f9;
|
||||
--aliyun-dark: #1a1a1a;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Inter, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
color: var(--text);
|
||||
background: #f8fafc;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.page { padding: 20px 24px 36px; max-width: 1280px; margin: 0 auto; }
|
||||
h1 { font-size: 22px; margin: 0 0 6px; }
|
||||
.lead { color: var(--muted); font-size: 14px; margin: 0 0 14px; }
|
||||
.note {
|
||||
background: var(--primary-bg);
|
||||
border: 1px solid #a7f3d0;
|
||||
color: #065f46;
|
||||
border-radius: 10px;
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.mode-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.mode-tab {
|
||||
border: 1px solid var(--border);
|
||||
background: #fff;
|
||||
border-radius: 999px;
|
||||
padding: 7px 12px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.mode-tab.active {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.shell {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.shell-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: #fafafa;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.frame {
|
||||
height: 520px;
|
||||
background: var(--bg);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.explain {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 12px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
@media (max-width: 900px) { .explain { grid-template-columns: 1fr; } }
|
||||
.card {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.card h3 { margin: 0 0 6px; font-size: 13px; }
|
||||
.card p { margin: 0; color: var(--muted); font-size: 12px; }
|
||||
.actions {
|
||||
margin-top: 14px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.btn {
|
||||
border: 1px solid var(--primary);
|
||||
background: var(--primary-bg);
|
||||
color: var(--primary-dark);
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn.primary { background: var(--primary); color: #fff; }
|
||||
.btn.ghost { background: #fff; border-color: var(--border); color: #334155; }
|
||||
|
||||
/* ===== App chrome ===== */
|
||||
.app { height: 100%; display: flex; flex-direction: column; font-size: 13px; }
|
||||
.topbar {
|
||||
height: 52px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 0 14px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
}
|
||||
.topbar.dark {
|
||||
background: #111827;
|
||||
border-bottom-color: #0f172a;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
.logo {
|
||||
font-weight: 700;
|
||||
color: var(--primary-dark);
|
||||
margin-right: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.topbar.dark .logo { color: #6ee7b7; }
|
||||
.l1 {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
height: 52px;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
.l1-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 0 14px;
|
||||
color: #334155;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
border-bottom: 2px solid transparent;
|
||||
user-select: none;
|
||||
}
|
||||
.topbar.dark .l1-item { color: #cbd5e1; }
|
||||
.l1-item:hover { color: var(--primary-dark); background: #f8fafc; }
|
||||
.topbar.dark .l1-item:hover { color: #fff; background: rgba(255,255,255,.06); }
|
||||
.l1-item.active {
|
||||
color: var(--primary-dark);
|
||||
font-weight: 600;
|
||||
border-bottom-color: var(--primary);
|
||||
background: var(--primary-bg);
|
||||
}
|
||||
.topbar.dark .l1-item.active {
|
||||
color: #6ee7b7;
|
||||
background: rgba(16,185,129,.12);
|
||||
border-bottom-color: #34d399;
|
||||
}
|
||||
.l1-item .caret { font-size: 10px; opacity: .7; }
|
||||
.spacer { flex: 1; }
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
.topbar.dark .right { color: #94a3b8; }
|
||||
.avatar {
|
||||
width: 26px; height: 26px; border-radius: 50%;
|
||||
background: linear-gradient(135deg, #6ee7b7, #34d399);
|
||||
}
|
||||
.main {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
}
|
||||
.panel-page {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
min-height: 100%;
|
||||
padding: 16px;
|
||||
}
|
||||
.panel-page h2 { margin: 0 0 8px; font-size: 16px; }
|
||||
.panel-page p { margin: 0; color: var(--muted); font-size: 13px; }
|
||||
.bars { margin-top: 16px; display: grid; gap: 8px; }
|
||||
.bar { height: 10px; border-radius: 6px; background: linear-gradient(90deg, #d1fae5, #f1f5f9); }
|
||||
.bar.w2 { width: 68%; }
|
||||
.bar.w3 { width: 45%; }
|
||||
|
||||
/* mega overlay */
|
||||
.mask {
|
||||
position: absolute;
|
||||
inset: 52px 0 0 0;
|
||||
background: rgba(15, 23, 42, .28);
|
||||
z-index: 15;
|
||||
}
|
||||
.mega {
|
||||
position: absolute;
|
||||
top: 52px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid var(--border);
|
||||
box-shadow: 0 18px 40px rgba(15, 23, 42, .14);
|
||||
z-index: 16;
|
||||
max-height: calc(100% - 52px);
|
||||
overflow: auto;
|
||||
}
|
||||
.mega-inner {
|
||||
padding: 18px 20px 20px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px 24px;
|
||||
}
|
||||
.mega.with-rail .mega-inner {
|
||||
grid-template-columns: 180px 1fr;
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
min-height: 280px;
|
||||
}
|
||||
.mega-col h4 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
.mega-link {
|
||||
display: block;
|
||||
padding: 6px 0;
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.mega-link:hover { color: var(--primary-dark); }
|
||||
.mega-link.on {
|
||||
color: var(--primary-dark);
|
||||
font-weight: 600;
|
||||
}
|
||||
.mega-link .tip {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
font-weight: 400;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.rail {
|
||||
background: #f8fafc;
|
||||
border-right: 1px solid var(--border);
|
||||
padding: 12px 0;
|
||||
}
|
||||
.rail-item {
|
||||
padding: 10px 16px;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
cursor: pointer;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
.rail-item:hover { background: #fff; }
|
||||
.rail-item.on {
|
||||
background: #fff;
|
||||
color: var(--primary-dark);
|
||||
font-weight: 600;
|
||||
border-left-color: var(--primary);
|
||||
}
|
||||
.rail-body {
|
||||
padding: 16px 20px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px 28px;
|
||||
}
|
||||
.mega-foot {
|
||||
border-top: 1px solid #f1f5f9;
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
background: #fafafa;
|
||||
}
|
||||
.mega-foot a { color: var(--primary-dark); text-decoration: none; font-weight: 600; }
|
||||
.hint-float {
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
bottom: 12px;
|
||||
z-index: 5;
|
||||
background: rgba(15,23,42,.8);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 8px;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<h1>阿里云式:顶栏展开一级,点击展开二/三级大面板</h1>
|
||||
<p class="lead">
|
||||
正是你说的模式:一级横排在顶栏;点击某一级后,用大面板一次性铺开二级分组 + 三级入口(无左侧常驻菜单树)。
|
||||
</p>
|
||||
<div class="note">
|
||||
可点击顶栏一级切换面板;再点同一级或点遮罩可关闭。下面两种变体可切换:纯多列大面板 / 面板内左侧二级轨 + 右侧三级(更接近阿里云产品菜单)。
|
||||
</div>
|
||||
|
||||
<div class="mode-tabs">
|
||||
<button type="button" class="mode-tab active" data-mode="columns">变体 A:多列大面板(二/三级同屏)</button>
|
||||
<button type="button" class="mode-tab" data-mode="rail">变体 B:面板内左轨二级 + 右列三级</button>
|
||||
<button type="button" class="mode-tab" data-mode="dark">变体 C:深色顶栏(阿里云控制台感)</button>
|
||||
</div>
|
||||
|
||||
<div class="shell">
|
||||
<div class="shell-bar">
|
||||
<span id="shell-title">变体 A · 点击顶栏「CRM」试一下</span>
|
||||
<span>无侧栏 · 点击展开</span>
|
||||
</div>
|
||||
<div class="frame" id="frame"></div>
|
||||
</div>
|
||||
|
||||
<div class="explain">
|
||||
<div class="card">
|
||||
<h3>交互要点</h3>
|
||||
<p>一级常显 → 点击展开 → 面板内同时看到二级标题与三级链接 → 点击三级进页并关闭面板。</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>相对现状的变化</h3>
|
||||
<p>取消左侧 270px 菜单树;模块切换与页面跳转都在顶部完成;内容区可全宽。</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>落地注意</h3>
|
||||
<p>一级建议控制在 5~8 个;面板内可加搜索;窄屏一级需「更多」折叠。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button type="button" class="btn primary" data-choice="aliyun-mega-yes" onclick="typeof toggleSelect==='function'&&toggleSelect(this)">✓ 就按这个方向(阿里云式大面板)</button>
|
||||
<button type="button" class="btn" data-choice="aliyun-mega-with-search" onclick="typeof toggleSelect==='function'&&toggleSelect(this)">方向对,但面板内要加搜索</button>
|
||||
<button type="button" class="btn ghost" data-choice="aliyun-mega-need-tweak" onclick="typeof toggleSelect==='function'&&toggleSelect(this)">接近,但还想改细节</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const menuTree = {
|
||||
crm: {
|
||||
label: 'CRM',
|
||||
groups: [
|
||||
{
|
||||
title: '客户管理',
|
||||
items: [
|
||||
{ name: '客户列表', tip: '主数据与跟进', page: '客户列表' },
|
||||
{ name: '联系人', tip: '客户联系人', page: '联系人' },
|
||||
{ name: '跟进记录', tip: '拜访与沟通', page: '跟进记录' },
|
||||
{ name: '客户公海', tip: '公海池领取', page: '客户公海' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '商机交易',
|
||||
items: [
|
||||
{ name: '商机', tip: '销售漏斗', page: '商机' },
|
||||
{ name: '合同', tip: '签约与履约', page: '合同' },
|
||||
{ name: '报价单', tip: '报价与版本', page: '报价单' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '产品主数据',
|
||||
items: [
|
||||
{ name: '产品', tip: '商品档案', page: '产品' },
|
||||
{ name: '分类', tip: '树形分类', page: '分类' },
|
||||
{ name: '单位', tip: '计量单位', page: '单位' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '分析',
|
||||
items: [
|
||||
{ name: '商机统计', tip: '漏斗报表', page: '商机统计' },
|
||||
{ name: '业绩看板', tip: '目标达成', page: '业绩看板' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
bx: {
|
||||
label: '报销',
|
||||
groups: [
|
||||
{
|
||||
title: '费用申请',
|
||||
items: [
|
||||
{ name: '报销单', page: '报销单' },
|
||||
{ name: '借款单', page: '借款单' },
|
||||
{ name: '费用项目', page: '费用项目' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '审批与台账',
|
||||
items: [
|
||||
{ name: '待我审批', page: '待我审批' },
|
||||
{ name: '费用台账', page: '费用台账' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
db: {
|
||||
label: '地磅',
|
||||
groups: [
|
||||
{
|
||||
title: '过磅业务',
|
||||
items: [
|
||||
{ name: '过磅单', page: '过磅单' },
|
||||
{ name: '车辆档案', page: '车辆档案' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '统计',
|
||||
items: [
|
||||
{ name: '过磅日报', page: '过磅日报' },
|
||||
{ name: '异常记录', page: '异常记录' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
pl: {
|
||||
label: '计划物流',
|
||||
groups: [
|
||||
{
|
||||
title: '计划',
|
||||
items: [
|
||||
{ name: '发运计划', page: '发运计划' },
|
||||
{ name: '计划产品', page: '计划产品' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '执行',
|
||||
items: [
|
||||
{ name: '运输单', page: '运输单' },
|
||||
{ name: '入出库', page: '入出库' },
|
||||
{ name: '运费结算', page: '运费结算' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
wf: {
|
||||
label: '工作流',
|
||||
groups: [
|
||||
{
|
||||
title: '任务中心',
|
||||
items: [
|
||||
{ name: '待办任务', page: '待办任务' },
|
||||
{ name: '已办任务', page: '已办任务' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '流程定义',
|
||||
items: [
|
||||
{ name: '流程设计', page: '流程设计' },
|
||||
{ name: '流程实例', page: '流程实例' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
sys: {
|
||||
label: '系统',
|
||||
groups: [
|
||||
{
|
||||
title: '组织权限',
|
||||
items: [
|
||||
{ name: '用户管理', page: '用户管理' },
|
||||
{ name: '角色管理', page: '角色管理' },
|
||||
{ name: '菜单管理', page: '菜单管理' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '租户与日志',
|
||||
items: [
|
||||
{ name: '租户', page: '租户' },
|
||||
{ name: '操作日志', page: '操作日志' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const order = ['crm', 'bx', 'db', 'pl', 'wf', 'sys']
|
||||
let mode = 'columns' // columns | rail | dark
|
||||
let openKey = 'crm'
|
||||
let activePage = '客户列表'
|
||||
let railGroupIndex = 0
|
||||
|
||||
const frame = document.getElementById('frame')
|
||||
const shellTitle = document.getElementById('shell-title')
|
||||
|
||||
document.querySelectorAll('.mode-tab').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
mode = btn.dataset.mode
|
||||
document.querySelectorAll('.mode-tab').forEach(b => b.classList.toggle('active', b === btn))
|
||||
if (mode === 'rail') railGroupIndex = 0
|
||||
paint()
|
||||
})
|
||||
})
|
||||
|
||||
function paint() {
|
||||
const dark = mode === 'dark'
|
||||
const withRail = mode === 'rail'
|
||||
const mod = menuTree[openKey]
|
||||
shellTitle.textContent = openKey
|
||||
? `${dark ? '变体 C' : withRail ? '变体 B' : '变体 A'} · 已展开「${mod.label}」大面板`
|
||||
: `${dark ? '变体 C' : withRail ? '变体 B' : '变体 A'} · 点击顶栏一级打开面板`
|
||||
|
||||
const l1Html = order.map(key => {
|
||||
const active = openKey === key
|
||||
return `<div class="l1-item ${active ? 'active' : ''}" data-key="${key}">${menuTree[key].label}<span class="caret">${active ? '▴' : '▾'}</span></div>`
|
||||
}).join('')
|
||||
|
||||
let megaHtml = ''
|
||||
if (openKey) {
|
||||
if (withRail) {
|
||||
const groups = mod.groups
|
||||
const g = groups[railGroupIndex] || groups[0]
|
||||
megaHtml = `
|
||||
<div class="mask" data-close="1"></div>
|
||||
<div class="mega with-rail">
|
||||
<div class="mega-inner">
|
||||
<div class="rail">
|
||||
${groups.map((grp, i) => `
|
||||
<div class="rail-item ${i === railGroupIndex ? 'on' : ''}" data-rail="${i}">${grp.title}</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div class="rail-body">
|
||||
${g.items.map(it => `
|
||||
<div>
|
||||
<div class="mega-link ${activePage === it.page ? 'on' : ''}" data-page="${it.page}">
|
||||
${it.name}
|
||||
${it.tip ? `<span class="tip">${it.tip}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mega-foot">
|
||||
<span>二级在左轨,三级在右侧 — 更接近阿里云「分类 + 产品」面板</span>
|
||||
<a href="#" data-close="1">关闭</a>
|
||||
</div>
|
||||
</div>`
|
||||
} else {
|
||||
megaHtml = `
|
||||
<div class="mask" data-close="1"></div>
|
||||
<div class="mega">
|
||||
<div class="mega-inner">
|
||||
${mod.groups.map(grp => `
|
||||
<div class="mega-col">
|
||||
<h4>${grp.title}</h4>
|
||||
${grp.items.map(it => `
|
||||
<div class="mega-link ${activePage === it.page ? 'on' : ''}" data-page="${it.page}">
|
||||
${it.name}
|
||||
${it.tip ? `<span class="tip">${it.tip}</span>` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div class="mega-foot">
|
||||
<span>二级作列标题,三级作链接 — 一次点开全部看见</span>
|
||||
<a href="#" data-close="1">关闭</a>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
}
|
||||
|
||||
frame.innerHTML = `
|
||||
<div class="app">
|
||||
<div class="topbar ${dark ? 'dark' : ''}">
|
||||
<div class="logo">山峡</div>
|
||||
<div class="l1">${l1Html}</div>
|
||||
<div class="spacer"></div>
|
||||
<div class="right">
|
||||
<span>搜索</span>
|
||||
<span>消息</span>
|
||||
<div class="avatar"></div>
|
||||
</div>
|
||||
</div>
|
||||
${megaHtml}
|
||||
<div class="main">
|
||||
<div class="panel-page">
|
||||
<h2>${activePage}</h2>
|
||||
<p>当前页面内容区全宽。导航不占用左侧。试试切换顶栏「报销 / 地磅 / 系统」。</p>
|
||||
<div class="bars"><div class="bar"></div><div class="bar w2"></div><div class="bar w3"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hint-float">点击一级菜单展开 · 再点同级或遮罩关闭</div>
|
||||
</div>`
|
||||
|
||||
frame.querySelectorAll('.l1-item').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const key = el.dataset.key
|
||||
if (openKey === key) openKey = null
|
||||
else {
|
||||
openKey = key
|
||||
railGroupIndex = 0
|
||||
}
|
||||
paint()
|
||||
})
|
||||
})
|
||||
frame.querySelectorAll('[data-close]').forEach(el => {
|
||||
el.addEventListener('click', (e) => {
|
||||
e.preventDefault()
|
||||
openKey = null
|
||||
paint()
|
||||
})
|
||||
})
|
||||
frame.querySelectorAll('[data-page]').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
activePage = el.dataset.page
|
||||
openKey = null
|
||||
paint()
|
||||
})
|
||||
})
|
||||
frame.querySelectorAll('[data-rail]').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
railGroupIndex = Number(el.dataset.rail)
|
||||
paint()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
paint()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,545 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>二级无三级:二级可直达页面</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary: #10b981;
|
||||
--primary-dark: #059669;
|
||||
--primary-bg: #ecfdf5;
|
||||
--text: #0f172a;
|
||||
--muted: #64748b;
|
||||
--border: #e2e8f0;
|
||||
--bg: #f1f5f9;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Inter, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
color: var(--text);
|
||||
background: #f8fafc;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.page { padding: 20px 24px 40px; max-width: 1280px; margin: 0 auto; }
|
||||
h1 { font-size: 22px; margin: 0 0 6px; }
|
||||
.lead { color: var(--muted); font-size: 14px; margin: 0 0 12px; }
|
||||
.note {
|
||||
background: var(--primary-bg);
|
||||
border: 1px solid #a7f3d0;
|
||||
color: #065f46;
|
||||
border-radius: 10px;
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.tabs { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; }
|
||||
.tab {
|
||||
border: 1px solid var(--border);
|
||||
background: #fff;
|
||||
border-radius: 999px;
|
||||
padding: 7px 12px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tab.active {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr;
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
}
|
||||
@media (max-width: 960px) { .layout { grid-template-columns: 1fr; } }
|
||||
.meta, .shell {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.meta { padding: 16px; }
|
||||
.meta h2 { margin: 0 0 8px; font-size: 16px; }
|
||||
.meta .desc { margin: 0 0 10px; font-size: 13px; color: var(--muted); }
|
||||
.meta ul { margin: 0; padding-left: 18px; font-size: 12px; color: var(--muted); }
|
||||
.rec {
|
||||
display: inline-block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--primary-bg);
|
||||
color: #047857;
|
||||
font-weight: 600;
|
||||
}
|
||||
.pick {
|
||||
margin-top: 14px;
|
||||
width: 100%;
|
||||
border: 1px solid var(--primary);
|
||||
background: var(--primary-bg);
|
||||
color: var(--primary-dark);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.pick.on { background: var(--primary); color: #fff; }
|
||||
.shell-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: #fafafa;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.frame { height: 480px; background: var(--bg); position: relative; overflow: hidden; }
|
||||
.fav {
|
||||
margin-top: 14px;
|
||||
padding: 12px 14px;
|
||||
border: 1px dashed #a7f3d0;
|
||||
border-radius: 10px;
|
||||
background: #f0fdf4;
|
||||
font-size: 13px;
|
||||
color: #065f46;
|
||||
}
|
||||
.rule {
|
||||
margin-top: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
}
|
||||
.rule code {
|
||||
background: #f1f5f9;
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.app { height: 100%; display: flex; flex-direction: column; font-size: 12px; }
|
||||
.top {
|
||||
height: 44px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 0 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.logo { font-weight: 700; color: var(--primary-dark); margin-right: 8px; }
|
||||
.l1 span {
|
||||
padding: 0 10px;
|
||||
color: #94a3b8;
|
||||
height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
.l1 span.on {
|
||||
color: var(--primary-dark);
|
||||
font-weight: 600;
|
||||
background: var(--primary-bg);
|
||||
border-bottom: 2px solid var(--primary);
|
||||
}
|
||||
.spacer { flex: 1; }
|
||||
.avatar {
|
||||
width: 22px; height: 22px; border-radius: 50%;
|
||||
background: linear-gradient(135deg, #6ee7b7, #34d399);
|
||||
}
|
||||
.mega {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 12px 28px rgba(15,23,42,.1);
|
||||
}
|
||||
.tools {
|
||||
padding: 8px 14px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
background: #fafafa;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.body { flex: 1; min-height: 0; overflow: auto; padding: 12px 14px; }
|
||||
.page-panel {
|
||||
flex: 1;
|
||||
margin: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
}
|
||||
.page-panel h2 { margin: 0 0 6px; font-size: 16px; }
|
||||
.page-panel p { margin: 0; color: var(--muted); }
|
||||
.tag {
|
||||
position: absolute; left: 10px; bottom: 10px;
|
||||
background: rgba(15,23,42,.78); color: #fff;
|
||||
font-size: 11px; padding: 4px 8px; border-radius: 6px;
|
||||
pointer-events: none; z-index: 2;
|
||||
}
|
||||
|
||||
.masonry { column-count: 5; column-gap: 14px; }
|
||||
.block {
|
||||
break-inside: avoid;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
border: 1px solid #f1f5f9;
|
||||
background: #fff;
|
||||
}
|
||||
.block.group { background: #f8fafc; }
|
||||
.block h4 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding-bottom: 5px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
color: #334155;
|
||||
}
|
||||
.block h4.static { cursor: default; }
|
||||
.block h4.clickable {
|
||||
cursor: pointer;
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
.block h4.clickable:hover { text-decoration: underline; }
|
||||
.link {
|
||||
display: block;
|
||||
padding: 3px 0;
|
||||
color: #475569;
|
||||
cursor: pointer;
|
||||
}
|
||||
.link:hover { color: var(--primary-dark); }
|
||||
.link.on { color: var(--primary-dark); font-weight: 600; }
|
||||
|
||||
/* leaf styles */
|
||||
.leaf-card {
|
||||
break-inside: avoid;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
background: var(--primary-bg);
|
||||
border: 1px solid #a7f3d0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
font-weight: 600;
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
.leaf-card:hover { background: #d1fae5; }
|
||||
.leaf-card .go { font-size: 11px; font-weight: 500; opacity: .8; }
|
||||
.leaf-card.on {
|
||||
outline: 2px solid var(--primary);
|
||||
background: #d1fae5;
|
||||
}
|
||||
|
||||
.leaf-inline {
|
||||
break-inside: avoid;
|
||||
margin-bottom: 10px;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px dashed #e2e8f0;
|
||||
}
|
||||
.leaf-inline a {
|
||||
color: var(--primary-dark);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.leaf-inline a:hover { text-decoration: underline; }
|
||||
.leaf-inline .hint {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
color: #94a3b8;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.mixed-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 12px 14px;
|
||||
align-items: start;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<h1>二级没有三级时:二级本身可点击进页</h1>
|
||||
<p class="lead">
|
||||
菜单树会出现两种二级:<strong>分组型</strong>(下挂三级)与 <strong>叶子型</strong>(无子节点,自己就是页面)。大面板里需要一眼能区分,且叶子二级可直接跳转。
|
||||
</p>
|
||||
<div class="note">
|
||||
交互规则建议:有子节点 → 二级作分组标题,点三级进页;无子节点 → 二级本身可点进页。下面对比 3 种叶子二级的视觉呈现(均可点击试跳)。
|
||||
</div>
|
||||
|
||||
<div class="tabs" id="tabs"></div>
|
||||
<div class="layout">
|
||||
<aside class="meta" id="meta"></aside>
|
||||
<section class="shell">
|
||||
<div class="shell-bar">
|
||||
<span id="title">预览</span>
|
||||
<span id="status">点击叶子二级或三级链接试跳</span>
|
||||
</div>
|
||||
<div class="frame" id="preview"></div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="fav" id="fav">标记更倾向的叶子二级样式。</div>
|
||||
<div class="rule">
|
||||
<strong>数据层约定:</strong>
|
||||
二级有 <code>children</code> → 渲染为分组;二级无 <code>children</code>(或空数组)且配置了路由/组件 → 渲染为可点击叶子。
|
||||
这与现有路由「非一级且无子则必须有 component」的校验方向一致。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const nodes = [
|
||||
{
|
||||
type: 'group',
|
||||
title: '客户管理',
|
||||
children: [
|
||||
{ name: '客户列表', path: '客户列表' },
|
||||
{ name: '联系人', path: '联系人' },
|
||||
{ name: '跟进记录', path: '跟进记录' },
|
||||
{ name: '客户公海', path: '客户公海' },
|
||||
{ name: '客户标签', path: '客户标签' }
|
||||
]
|
||||
},
|
||||
{ type: 'leaf', title: '商机看板', path: '商机看板' },
|
||||
{
|
||||
type: 'group',
|
||||
title: '合同交易',
|
||||
children: [
|
||||
{ name: '合同', path: '合同' },
|
||||
{ name: '报价单', path: '报价单' },
|
||||
{ name: '回款', path: '回款' }
|
||||
]
|
||||
},
|
||||
{ type: 'leaf', title: '产品目录', path: '产品目录' },
|
||||
{
|
||||
type: 'group',
|
||||
title: '服务',
|
||||
children: [
|
||||
{ name: '工单', path: '工单' },
|
||||
{ name: '知识库', path: '知识库' }
|
||||
]
|
||||
},
|
||||
{ type: 'leaf', title: '业绩报表', path: '业绩报表' },
|
||||
{ type: 'leaf', title: '公海规则', path: '公海规则' },
|
||||
{
|
||||
type: 'group',
|
||||
title: '配置',
|
||||
children: [
|
||||
{ name: '字段配置', path: '字段配置' },
|
||||
{ name: '审批流', path: '审批流' },
|
||||
{ name: '编号规则', path: '编号规则' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
let mode = 'leaf-card'
|
||||
let activePath = '客户列表'
|
||||
let showMega = true
|
||||
const picked = new Set()
|
||||
|
||||
const strategies = [
|
||||
{
|
||||
id: 'leaf-card',
|
||||
letter: 'L1',
|
||||
name: '叶子二级 = 高亮卡片(推荐)',
|
||||
rec: true,
|
||||
desc: '无三级的二级渲染成薄荷绿卡片,带「进入」提示;有三级的仍是分组块。差异一眼可见,点击卡片进页并关闭面板。',
|
||||
points: ['可点击性最强', '与分组块视觉区分清晰', '和 S5 瀑布流兼容', '卡片略占空间']
|
||||
},
|
||||
{
|
||||
id: 'leaf-inline',
|
||||
letter: 'L2',
|
||||
name: '叶子二级 = 加粗直达链接',
|
||||
desc: '叶子二级做成加粗文字链接 + 小字「直达页面」;分组仍保留标题+三级列表。更紧凑,接近普通链接密度。',
|
||||
points: ['更省空间', '仍可区分叶子/分组', '可点击感弱于卡片', '适合叶子很多时']
|
||||
},
|
||||
{
|
||||
id: 'leaf-same',
|
||||
letter: 'L3',
|
||||
name: '叶子与分组标题同级(标题可点)',
|
||||
desc: '统一用「标题」样式:有子则标题不可点(或仅作分组名),无子则标题可点跳转。视觉最统一,但叶子可点性稍弱,需靠 hover/手型提示。',
|
||||
points: ['视觉最整齐', '依赖交互提示', '实现规则简单', '误认「标题不可点」的风险']
|
||||
}
|
||||
]
|
||||
|
||||
const tabsEl = document.getElementById('tabs')
|
||||
const metaEl = document.getElementById('meta')
|
||||
const previewEl = document.getElementById('preview')
|
||||
const titleEl = document.getElementById('title')
|
||||
const statusEl = document.getElementById('status')
|
||||
const favEl = document.getElementById('fav')
|
||||
|
||||
function go(path) {
|
||||
activePath = path
|
||||
showMega = false
|
||||
paint()
|
||||
statusEl.textContent = `已进入「${path}」· 点顶栏 CRM 可再打开面板`
|
||||
}
|
||||
|
||||
function renderMegaBody() {
|
||||
if (mode === 'leaf-card') {
|
||||
return `
|
||||
<div class="masonry">
|
||||
${nodes.map(n => {
|
||||
if (n.type === 'leaf') {
|
||||
return `
|
||||
<div class="leaf-card ${activePath === n.path ? 'on' : ''}" data-go="${n.path}">
|
||||
<span>${n.title}</span>
|
||||
<span class="go">进入 →</span>
|
||||
</div>`
|
||||
}
|
||||
return `
|
||||
<div class="block group">
|
||||
<h4 class="static">${n.title}</h4>
|
||||
${n.children.map(c => `
|
||||
<div class="link ${activePath === c.path ? 'on' : ''}" data-go="${c.path}">${c.name}</div>
|
||||
`).join('')}
|
||||
</div>`
|
||||
}).join('')}
|
||||
</div>`
|
||||
}
|
||||
|
||||
if (mode === 'leaf-inline') {
|
||||
return `
|
||||
<div class="masonry">
|
||||
${nodes.map(n => {
|
||||
if (n.type === 'leaf') {
|
||||
return `
|
||||
<div class="leaf-inline">
|
||||
<a data-go="${n.path}">${n.title}</a>
|
||||
<span class="hint">直达页面 · 无下级</span>
|
||||
</div>`
|
||||
}
|
||||
return `
|
||||
<div class="block group">
|
||||
<h4 class="static">${n.title}</h4>
|
||||
${n.children.map(c => `
|
||||
<div class="link ${activePath === c.path ? 'on' : ''}" data-go="${c.path}">${c.name}</div>
|
||||
`).join('')}
|
||||
</div>`
|
||||
}).join('')}
|
||||
</div>`
|
||||
}
|
||||
|
||||
// leaf-same
|
||||
return `
|
||||
<div class="masonry">
|
||||
${nodes.map(n => {
|
||||
if (n.type === 'leaf') {
|
||||
return `
|
||||
<div class="block">
|
||||
<h4 class="clickable" data-go="${n.path}">${n.title} →</h4>
|
||||
<div style="font-size:10px;color:#94a3b8">无下级 · 点击标题进入</div>
|
||||
</div>`
|
||||
}
|
||||
return `
|
||||
<div class="block group">
|
||||
<h4 class="static">${n.title}</h4>
|
||||
${n.children.map(c => `
|
||||
<div class="link ${activePath === c.path ? 'on' : ''}" data-go="${c.path}">${c.name}</div>
|
||||
`).join('')}
|
||||
</div>`
|
||||
}).join('')}
|
||||
</div>`
|
||||
}
|
||||
|
||||
function paint() {
|
||||
const s = strategies.find(x => x.id === mode)
|
||||
tabsEl.innerHTML = strategies.map(x =>
|
||||
`<button type="button" class="tab ${x.id === mode ? 'active' : ''}" data-id="${x.id}">${x.letter} ${x.name.split('(')[0]}</button>`
|
||||
).join('')
|
||||
tabsEl.querySelectorAll('.tab').forEach(btn => {
|
||||
btn.onclick = () => {
|
||||
mode = btn.dataset.id
|
||||
showMega = true
|
||||
paint()
|
||||
}
|
||||
})
|
||||
|
||||
titleEl.textContent = `预览 ${s.letter} · ${s.name}`
|
||||
metaEl.innerHTML = `
|
||||
${s.rec ? '<div class="rec">推荐</div>' : ''}
|
||||
<h2>${s.letter}. ${s.name}</h2>
|
||||
<p class="desc">${s.desc}</p>
|
||||
<ul>${s.points.map(p => `<li>${p}</li>`).join('')}</ul>
|
||||
<button type="button" class="pick ${picked.has(s.id) ? 'on' : ''}" id="pick"
|
||||
data-choice="${s.id}" onclick="typeof toggleSelect==='function'&&toggleSelect(this)">
|
||||
${picked.has(s.id) ? '✓ 已标记' : '标记该样式'}
|
||||
</button>
|
||||
`
|
||||
document.getElementById('pick').addEventListener('click', () => {
|
||||
if (picked.has(s.id)) picked.delete(s.id)
|
||||
else picked.add(s.id)
|
||||
paint()
|
||||
})
|
||||
|
||||
if (showMega) {
|
||||
previewEl.innerHTML = `
|
||||
<div class="app">
|
||||
<div class="top">
|
||||
<div class="logo">山峡</div>
|
||||
<div class="l1"><span class="on">CRM ▾</span><span>报销</span><span>地磅</span><span>系统</span></div>
|
||||
<div class="spacer"></div>
|
||||
<div class="avatar"></div>
|
||||
</div>
|
||||
<div class="mega">
|
||||
<div class="tools">
|
||||
<span>绿/可点 = 叶子二级(无三级)· 灰块 = 分组二级(有三级)</span>
|
||||
<span>当前页:${activePath}</span>
|
||||
</div>
|
||||
<div class="body">${renderMegaBody()}</div>
|
||||
</div>
|
||||
<div class="tag">点击叶子二级或三级 · 模拟进页</div>
|
||||
</div>`
|
||||
} else {
|
||||
previewEl.innerHTML = `
|
||||
<div class="app">
|
||||
<div class="top">
|
||||
<div class="logo">山峡</div>
|
||||
<div class="l1"><span class="on" id="reopen">CRM ▾</span><span>报销</span><span>地磅</span><span>系统</span></div>
|
||||
<div class="spacer"></div>
|
||||
<div class="avatar"></div>
|
||||
</div>
|
||||
<div class="page-panel">
|
||||
<h2>${activePath}</h2>
|
||||
<p>已从大面板跳入该页。叶子二级与三级叶子使用同一套路由跳转,只是入口层级不同。</p>
|
||||
<p style="margin-top:10px">再点顶栏「CRM」可重新打开菜单面板。</p>
|
||||
</div>
|
||||
<div class="tag">页面已打开 · 侧栏仍为 0</div>
|
||||
</div>`
|
||||
previewEl.querySelector('#reopen')?.addEventListener('click', () => {
|
||||
showMega = true
|
||||
paint()
|
||||
})
|
||||
}
|
||||
|
||||
previewEl.querySelectorAll('[data-go]').forEach(el => {
|
||||
el.addEventListener('click', () => go(el.getAttribute('data-go')))
|
||||
})
|
||||
|
||||
favEl.innerHTML = picked.size
|
||||
? `已标记:<strong>${strategies.filter(x => picked.has(x.id)).map(x => x.letter).join('、')}</strong>`
|
||||
: '标记更倾向的叶子二级样式。'
|
||||
}
|
||||
|
||||
paint()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,568 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>方案A · 二级很多时如何摆放</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary: #10b981;
|
||||
--primary-dark: #059669;
|
||||
--primary-bg: #ecfdf5;
|
||||
--text: #0f172a;
|
||||
--muted: #64748b;
|
||||
--border: #e2e8f0;
|
||||
--bg: #f1f5f9;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Inter, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
color: var(--text);
|
||||
background: #f8fafc;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.page { padding: 20px 24px 40px; max-width: 1280px; margin: 0 auto; }
|
||||
h1 { font-size: 22px; margin: 0 0 6px; }
|
||||
.lead { color: var(--muted); font-size: 14px; margin: 0 0 12px; }
|
||||
.note {
|
||||
background: var(--primary-bg);
|
||||
border: 1px solid #a7f3d0;
|
||||
color: #065f46;
|
||||
border-radius: 10px;
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.tabs { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; }
|
||||
.tab {
|
||||
border: 1px solid var(--border);
|
||||
background: #fff;
|
||||
border-radius: 999px;
|
||||
padding: 7px 12px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tab.active {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr;
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
}
|
||||
@media (max-width: 960px) { .layout { grid-template-columns: 1fr; } }
|
||||
.meta, .shell {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.meta { padding: 16px; }
|
||||
.meta h2 { margin: 0 0 8px; font-size: 16px; }
|
||||
.meta .desc { margin: 0 0 12px; font-size: 13px; color: var(--muted); }
|
||||
.meta ul { margin: 0; padding-left: 18px; font-size: 12px; color: var(--muted); }
|
||||
.meta li { margin-bottom: 4px; }
|
||||
.rec {
|
||||
display: inline-block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--primary-bg);
|
||||
color: #047857;
|
||||
font-weight: 600;
|
||||
}
|
||||
.pick {
|
||||
margin-top: 14px;
|
||||
width: 100%;
|
||||
border: 1px solid var(--primary);
|
||||
background: var(--primary-bg);
|
||||
color: var(--primary-dark);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.pick.on { background: var(--primary); color: #fff; }
|
||||
.shell-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: #fafafa;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.frame { height: 480px; background: var(--bg); position: relative; overflow: hidden; }
|
||||
.fav {
|
||||
margin-top: 14px;
|
||||
padding: 12px 14px;
|
||||
border: 1px dashed #a7f3d0;
|
||||
border-radius: 10px;
|
||||
background: #f0fdf4;
|
||||
font-size: 13px;
|
||||
color: #065f46;
|
||||
}
|
||||
.rule {
|
||||
margin-top: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
}
|
||||
.rule strong { color: var(--text); }
|
||||
|
||||
/* mock */
|
||||
.app { height: 100%; display: flex; flex-direction: column; font-size: 12px; }
|
||||
.top {
|
||||
height: 48px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 0 12px;
|
||||
flex-shrink: 0;
|
||||
z-index: 5;
|
||||
}
|
||||
.logo { font-weight: 700; color: var(--primary-dark); margin-right: 8px; }
|
||||
.l1 {
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 2px;
|
||||
}
|
||||
.l1 span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
color: #64748b;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
.l1 span.on {
|
||||
color: var(--primary-dark);
|
||||
font-weight: 600;
|
||||
background: var(--primary-bg);
|
||||
border-bottom-color: var(--primary);
|
||||
}
|
||||
.spacer { flex: 1; }
|
||||
.avatar {
|
||||
width: 24px; height: 24px; border-radius: 50%;
|
||||
background: linear-gradient(135deg, #6ee7b7, #34d399);
|
||||
}
|
||||
.mega {
|
||||
background: #fff;
|
||||
border-bottom: 1px solid var(--border);
|
||||
box-shadow: 0 14px 32px rgba(15,23,42,.12);
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.mega-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 16px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
background: #fafafa;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.search {
|
||||
flex: 1;
|
||||
max-width: 280px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 6px 10px;
|
||||
color: #94a3b8;
|
||||
background: #fff;
|
||||
}
|
||||
.mega-body { flex: 1; min-height: 0; overflow: auto; padding: 14px 16px; }
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 14px 20px;
|
||||
}
|
||||
.grid.c4 { grid-template-columns: repeat(4, 1fr); }
|
||||
.grid.c5 { grid-template-columns: repeat(5, 1fr); }
|
||||
.grid.c3 { grid-template-columns: repeat(3, 1fr); }
|
||||
.col h4 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
.col h4 .count {
|
||||
margin-left: 4px;
|
||||
color: #94a3b8;
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
}
|
||||
.link {
|
||||
display: block;
|
||||
padding: 4px 0;
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
}
|
||||
.link.on { color: var(--primary-dark); font-weight: 600; }
|
||||
.link.muted { color: #94a3b8; }
|
||||
.hscroll {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.hscroll .col {
|
||||
min-width: 150px;
|
||||
flex: 0 0 150px;
|
||||
}
|
||||
.anchor-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 140px 1fr;
|
||||
gap: 0;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
.anchors {
|
||||
border-right: 1px solid var(--border);
|
||||
background: #f8fafc;
|
||||
overflow: auto;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.anchors a {
|
||||
display: block;
|
||||
padding: 8px 12px;
|
||||
color: #475569;
|
||||
text-decoration: none;
|
||||
font-size: 12px;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
.anchors a.on {
|
||||
background: #fff;
|
||||
color: var(--primary-dark);
|
||||
font-weight: 600;
|
||||
border-left-color: var(--primary);
|
||||
}
|
||||
.anchor-content {
|
||||
overflow: auto;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 16px;
|
||||
scroll-margin-top: 8px;
|
||||
}
|
||||
.section h4 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
}
|
||||
.section .links {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 2px 16px;
|
||||
}
|
||||
.more-block {
|
||||
margin-top: 8px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px dashed var(--border);
|
||||
}
|
||||
.more-block summary {
|
||||
cursor: pointer;
|
||||
color: var(--primary-dark);
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.tag {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
bottom: 10px;
|
||||
background: rgba(15,23,42,.78);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
}
|
||||
.badge {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<h1>方案 A:二级菜单很多时怎么摆?</h1>
|
||||
<p class="lead">
|
||||
已选定「多列大面板」。下面用 <strong>同一份「CRM 下 12 个二级分组」</strong> 数据,对比 5 种承载超量二级的摆法。
|
||||
</p>
|
||||
<div class="note">
|
||||
经验阈值:二级 ≤ 8 且每组三级不多 → 固定 4 列网格即可;二级 > 8 或总链接 > ~30 → 需要换行滚动 / 锚点 / 搜索 / 折叠「更多」。
|
||||
</div>
|
||||
|
||||
<div class="tabs" id="tabs"></div>
|
||||
<div class="layout">
|
||||
<aside class="meta" id="meta"></aside>
|
||||
<section class="shell">
|
||||
<div class="shell-bar">
|
||||
<span id="title">预览</span>
|
||||
<span>模拟:CRM · 12 个二级</span>
|
||||
</div>
|
||||
<div class="frame" id="preview"></div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="fav" id="fav">请标记你更倾向的超量摆法(可多选)。</div>
|
||||
<div class="rule">
|
||||
<strong>建议组合(可落地):</strong>
|
||||
默认用「自动换行网格 + 面板内纵向滚动」;二级很多时加「左侧锚点快速跳转」;面板顶部始终保留搜索(你刚才也点过需要搜索)。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 12 个二级,模拟很多的情况
|
||||
const groups = [
|
||||
{ title: '客户管理', items: ['客户列表', '联系人', '跟进记录', '客户公海'] },
|
||||
{ title: '商机交易', items: ['商机', '合同', '报价单', '回款'] },
|
||||
{ title: '产品主数据', items: ['产品', '分类', '单位', '价格表'] },
|
||||
{ title: '营销活动', items: ['线索', '活动', '短信触达'] },
|
||||
{ title: '服务工单', items: ['工单列表', '知识库', '满意度'] },
|
||||
{ title: '渠道伙伴', items: ['经销商', '渠道政策', '返利'] },
|
||||
{ title: '库存协同', items: ['可用库存', '调拨申请', '安全库存'] },
|
||||
{ title: '财务协同', items: ['对账单', '开票申请', '信用额度'] },
|
||||
{ title: '项目交付', items: ['项目列表', '里程碑', '验收单'] },
|
||||
{ title: '报表分析', items: ['商机漏斗', '业绩看板', '流失分析'] },
|
||||
{ title: '配置中心', items: ['字段配置', '审批流', '编号规则'] },
|
||||
{ title: '集成扩展', items: ['Webhook', '开放接口', '导入导出'] }
|
||||
]
|
||||
|
||||
const strategies = [
|
||||
{
|
||||
id: 'wrap-scroll',
|
||||
letter: 'S1',
|
||||
name: '自动换行网格 + 纵向滚动',
|
||||
rec: true,
|
||||
desc: '固定 4 列(宽屏可 5 列)。二级超出一行就自动换到下一行;面板整体纵向滚动。二级再多也不改变交互模型。',
|
||||
points: ['实现最简单,和方案 A 一致', '二级数量无硬上限', '面板可能变高,需限高滚动', '适合作为默认策略'],
|
||||
render() {
|
||||
return `
|
||||
<div class="app">
|
||||
${topbar()}
|
||||
<div class="mega">
|
||||
<div class="mega-tools">
|
||||
<div class="search">搜索菜单…</div>
|
||||
<span class="badge">12 个二级 · 4 列换行</span>
|
||||
</div>
|
||||
<div class="mega-body">
|
||||
<div class="grid c4">
|
||||
${groups.map((g, i) => col(g, i === 0)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tag">S1 推荐默认:换行 + 面板内滚动</div>
|
||||
</div>`
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'anchor',
|
||||
letter: 'S2',
|
||||
name: '左侧锚点目录 + 右侧分组',
|
||||
rec: true,
|
||||
desc: '面板左侧列出全部二级名称作锚点;右侧按二级分块展示三级。二级很多时可用左侧快速定位,不必全程滚动找。',
|
||||
points: ['二级很多时定位最快', '仍一次看到三级', '略占面板左侧宽度', '和阿里云「分类跳转」类似'],
|
||||
render() {
|
||||
return `
|
||||
<div class="app">
|
||||
${topbar()}
|
||||
<div class="mega">
|
||||
<div class="mega-tools">
|
||||
<div class="search">搜索菜单…</div>
|
||||
<span class="badge">左锚点 · 右内容</span>
|
||||
</div>
|
||||
<div class="anchor-layout" style="flex:1;min-height:0">
|
||||
<div class="anchors">
|
||||
${groups.map((g, i) => `<a class="${i === 0 ? 'on' : ''}" href="#">${g.title}</a>`).join('')}
|
||||
</div>
|
||||
<div class="anchor-content">
|
||||
${groups.slice(0, 4).map((g, i) => `
|
||||
<div class="section">
|
||||
<h4>${g.title}</h4>
|
||||
<div class="links">
|
||||
${g.items.map((it, j) => `<div class="link ${i === 0 && j === 0 ? 'on' : ''}">${it}</div>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
<div class="link muted">…向下滚动还有 8 个二级分组</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tag">S2 二级很多时增强:锚点跳转</div>
|
||||
</div>`
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'h-scroll',
|
||||
letter: 'S3',
|
||||
name: '单行多列 + 横向滚动',
|
||||
desc: '所有二级排成一行很多列,面板横向滑动浏览。二级极多时也能保持「一行看三级」,但横向滚动在桌面端不如纵向自然。',
|
||||
points: ['每列高度一致、视觉整齐', '横向滚动发现成本高', '触控板/鼠标体验一般', '一般不优先推荐'],
|
||||
render() {
|
||||
return `
|
||||
<div class="app">
|
||||
${topbar()}
|
||||
<div class="mega">
|
||||
<div class="mega-tools">
|
||||
<div class="search">搜索菜单…</div>
|
||||
<span class="badge">横向滑动看更多列 →</span>
|
||||
</div>
|
||||
<div class="mega-body">
|
||||
<div class="hscroll">
|
||||
${groups.map((g, i) => col(g, i === 0)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tag">S3 横向滚 · 通常不如纵向</div>
|
||||
</div>`
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'pin-more',
|
||||
letter: 'S4',
|
||||
name: '前 N 个常显 +「更多二级」折叠',
|
||||
desc: '首屏只展示高频/置顶的 8 个二级;其余收进「更多二级」展开区。避免首屏过载,适合有明显高频菜单的业务。',
|
||||
points: ['首屏干净', '需维护置顶/排序', '低频入口多一步', '可和 S1 组合'],
|
||||
render() {
|
||||
const head = groups.slice(0, 8)
|
||||
const rest = groups.slice(8)
|
||||
return `
|
||||
<div class="app">
|
||||
${topbar()}
|
||||
<div class="mega">
|
||||
<div class="mega-tools">
|
||||
<div class="search">搜索菜单…</div>
|
||||
<span class="badge">首屏 8 个 · 其余折叠</span>
|
||||
</div>
|
||||
<div class="mega-body">
|
||||
<div class="grid c4">
|
||||
${head.map((g, i) => col(g, i === 0)).join('')}
|
||||
</div>
|
||||
<div class="more-block">
|
||||
<div style="color:var(--primary-dark);font-weight:600;margin-bottom:10px">更多二级 ▾(${rest.length})</div>
|
||||
<div class="grid c4">
|
||||
${rest.map(g => col(g, false)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tag">S4 高频置顶 · 其余进更多</div>
|
||||
</div>`
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'dense-5col',
|
||||
letter: 'S5',
|
||||
name: '加列加密(5~6 列)',
|
||||
desc: '不改交互,只把列数从 4 提到 5/6,让更多二级挤进首屏。适合二级多但每组三级很少(1~3 个)的情况。',
|
||||
points: ['首屏信息密度高', '列太窄长文案易折行', '三级很多时会显得挤', '可作宽屏增强'],
|
||||
render() {
|
||||
return `
|
||||
<div class="app">
|
||||
${topbar()}
|
||||
<div class="mega">
|
||||
<div class="mega-tools">
|
||||
<div class="search">搜索菜单…</div>
|
||||
<span class="badge">5 列加密 · 仍可滚动</span>
|
||||
</div>
|
||||
<div class="mega-body">
|
||||
<div class="grid c5">
|
||||
${groups.map((g, i) => col(g, i === 0)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tag">S5 加列 · 适合三级很短的模块</div>
|
||||
</div>`
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
function topbar() {
|
||||
return `
|
||||
<div class="top">
|
||||
<div class="logo">山峡</div>
|
||||
<div class="l1">
|
||||
<span class="on">CRM ▾</span>
|
||||
<span>报销</span>
|
||||
<span>地磅</span>
|
||||
<span>计划</span>
|
||||
<span>系统</span>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<div class="avatar"></div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
function col(g, on) {
|
||||
return `
|
||||
<div class="col">
|
||||
<h4>${g.title}<span class="count">${g.items.length}</span></h4>
|
||||
${g.items.map((it, i) => `<div class="link ${on && i === 0 ? 'on' : ''}">${it}</div>`).join('')}
|
||||
</div>`
|
||||
}
|
||||
|
||||
const picked = new Set()
|
||||
let current = 'wrap-scroll'
|
||||
|
||||
const tabsEl = document.getElementById('tabs')
|
||||
const metaEl = document.getElementById('meta')
|
||||
const previewEl = document.getElementById('preview')
|
||||
const titleEl = document.getElementById('title')
|
||||
const favEl = document.getElementById('fav')
|
||||
|
||||
function paint() {
|
||||
const s = strategies.find(x => x.id === current)
|
||||
tabsEl.innerHTML = strategies.map(x =>
|
||||
`<button type="button" class="tab ${x.id === current ? 'active' : ''}" data-id="${x.id}">${x.letter} ${x.name.split('(')[0]}</button>`
|
||||
).join('')
|
||||
tabsEl.querySelectorAll('.tab').forEach(btn => {
|
||||
btn.onclick = () => { current = btn.dataset.id; paint() }
|
||||
})
|
||||
|
||||
titleEl.textContent = `预览 ${s.letter} · ${s.name}`
|
||||
previewEl.innerHTML = s.render()
|
||||
metaEl.innerHTML = `
|
||||
${s.rec ? '<div class="rec">推荐</div>' : ''}
|
||||
<h2>${s.letter}. ${s.name}</h2>
|
||||
<p class="desc">${s.desc}</p>
|
||||
<ul>${s.points.map(p => `<li>${p}</li>`).join('')}</ul>
|
||||
<button type="button" class="pick ${picked.has(s.id) ? 'on' : ''}" id="pick"
|
||||
data-choice="${s.id}" onclick="typeof toggleSelect==='function'&&toggleSelect(this)">
|
||||
${picked.has(s.id) ? '✓ 已标记' : '标记该摆法'}
|
||||
</button>
|
||||
`
|
||||
document.getElementById('pick').addEventListener('click', () => {
|
||||
if (picked.has(s.id)) picked.delete(s.id)
|
||||
else picked.add(s.id)
|
||||
paint()
|
||||
})
|
||||
|
||||
if (!picked.size) {
|
||||
favEl.textContent = '请标记你更倾向的超量摆法(可多选)。'
|
||||
} else {
|
||||
const names = strategies.filter(x => picked.has(x.id)).map(x => x.letter)
|
||||
favEl.innerHTML = `已标记:<strong>${names.join('、')}</strong>。回终端告诉我最终选择。`
|
||||
}
|
||||
}
|
||||
|
||||
paint()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,531 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>一行一个二级 · 同行三级</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary: #10b981;
|
||||
--primary-dark: #059669;
|
||||
--primary-bg: #ecfdf5;
|
||||
--text: #0f172a;
|
||||
--muted: #64748b;
|
||||
--border: #e2e8f0;
|
||||
--bg: #f1f5f9;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Inter, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
color: var(--text);
|
||||
background: #f8fafc;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.page { padding: 20px 24px 40px; max-width: 1280px; margin: 0 auto; }
|
||||
h1 { font-size: 22px; margin: 0 0 6px; }
|
||||
.lead { color: var(--muted); font-size: 14px; margin: 0 0 12px; }
|
||||
.note {
|
||||
background: var(--primary-bg);
|
||||
border: 1px solid #a7f3d0;
|
||||
color: #065f46;
|
||||
border-radius: 10px;
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
@media (max-width: 900px) { .summary { grid-template-columns: 1fr 1fr; } }
|
||||
.summary .card {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.summary .card strong {
|
||||
display: block;
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.tabs { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; }
|
||||
.tab {
|
||||
border: 1px solid var(--border);
|
||||
background: #fff;
|
||||
border-radius: 999px;
|
||||
padding: 7px 12px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tab.active {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.shell {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.shell-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: #fafafa;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.frame { height: 500px; background: var(--bg); position: relative; overflow: hidden; }
|
||||
.actions {
|
||||
margin-top: 14px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
.btn {
|
||||
border: 1px solid var(--primary);
|
||||
background: var(--primary-bg);
|
||||
color: var(--primary-dark);
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn.primary { background: var(--primary); color: #fff; }
|
||||
.btn.ghost { background: #fff; border-color: var(--border); color: #334155; }
|
||||
|
||||
/* app */
|
||||
.app { height: 100%; display: flex; flex-direction: column; font-size: 13px; }
|
||||
.top {
|
||||
height: 48px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 0 14px;
|
||||
flex-shrink: 0;
|
||||
z-index: 5;
|
||||
}
|
||||
.logo { font-weight: 700; color: var(--primary-dark); margin-right: 10px; }
|
||||
.l1 {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
height: 48px;
|
||||
gap: 2px;
|
||||
}
|
||||
.l1-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 14px;
|
||||
color: #475569;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.l1-item:hover { color: var(--primary-dark); background: #f8fafc; }
|
||||
.l1-item.on {
|
||||
color: var(--primary-dark);
|
||||
font-weight: 600;
|
||||
background: var(--primary-bg);
|
||||
border-bottom-color: var(--primary);
|
||||
}
|
||||
.spacer { flex: 1; }
|
||||
.avatar {
|
||||
width: 26px; height: 26px; border-radius: 50%;
|
||||
background: linear-gradient(135deg, #6ee7b7, #34d399);
|
||||
}
|
||||
.mask {
|
||||
position: absolute;
|
||||
inset: 48px 0 0 0;
|
||||
background: rgba(15, 23, 42, .25);
|
||||
z-index: 8;
|
||||
}
|
||||
.mega {
|
||||
position: absolute;
|
||||
top: 48px; left: 0; right: 0;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid var(--border);
|
||||
box-shadow: 0 16px 36px rgba(15,23,42,.12);
|
||||
z-index: 9;
|
||||
max-height: calc(100% - 48px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.mega-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 18px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
background: #fafafa;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.search {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 6px 10px;
|
||||
color: #94a3b8;
|
||||
background: #fff;
|
||||
min-width: 220px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.mega-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 8px 18px 14px;
|
||||
}
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr;
|
||||
gap: 12px 16px;
|
||||
align-items: start;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
.row:last-child { border-bottom: 0; }
|
||||
.l2 {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
padding-top: 4px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.l2 .sub {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.l3s { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.pill {
|
||||
border: 1px solid var(--border);
|
||||
background: #fff;
|
||||
color: #334155;
|
||||
border-radius: 999px;
|
||||
padding: 5px 12px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: .12s ease;
|
||||
}
|
||||
.pill:hover {
|
||||
border-color: #6ee7b7;
|
||||
color: var(--primary-dark);
|
||||
background: var(--primary-bg);
|
||||
}
|
||||
.pill.on {
|
||||
border-color: var(--primary);
|
||||
background: var(--primary-bg);
|
||||
color: var(--primary-dark);
|
||||
font-weight: 600;
|
||||
}
|
||||
.pill.virtual {
|
||||
border-style: dashed;
|
||||
}
|
||||
.text-links { display: flex; flex-wrap: wrap; gap: 4px 18px; padding-top: 2px; }
|
||||
.text-links a {
|
||||
color: #475569;
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.text-links a:hover { color: var(--primary-dark); }
|
||||
.text-links a.on { color: var(--primary-dark); font-weight: 600; }
|
||||
.text-links a.virtual::after {
|
||||
content: '';
|
||||
}
|
||||
.main {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
}
|
||||
.panel {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
min-height: 100%;
|
||||
padding: 16px;
|
||||
}
|
||||
.panel h2 { margin: 0 0 8px; font-size: 16px; }
|
||||
.panel p { margin: 0; color: var(--muted); font-size: 13px; }
|
||||
.bars { margin-top: 16px; display: grid; gap: 8px; }
|
||||
.bar { height: 10px; border-radius: 6px; background: linear-gradient(90deg, #d1fae5, #f1f5f9); }
|
||||
.bar.w2 { width: 70%; }
|
||||
.hint {
|
||||
position: absolute;
|
||||
left: 12px; bottom: 12px;
|
||||
background: rgba(15,23,42,.8);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
padding: 5px 9px;
|
||||
border-radius: 8px;
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
}
|
||||
.compact .row { padding: 8px 0; }
|
||||
.compact .l2 { font-size: 12px; }
|
||||
.wide-l2 .row { grid-template-columns: 150px 1fr; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<h1>定稿方向:一行一个二级,后面跟三级</h1>
|
||||
<p class="lead">
|
||||
顶栏展开一级 → 点击弹出大面板 → 面板内每行:左侧二级标题(不可点)+ 右侧三级入口(可点)。无三级时自动补「与二级同名」的虚拟三级。
|
||||
</p>
|
||||
<div class="note">
|
||||
这比 S5 多列/瀑布流更整齐可控:三级多少只影响该行换行,不会出现列高参差。可点击顶栏切换一级,点三级进页。
|
||||
</div>
|
||||
|
||||
<div class="summary">
|
||||
<div class="card"><strong>整体</strong>阿里云式顶栏大面板,无左侧栏</div>
|
||||
<div class="card"><strong>面板内</strong>一行一个二级 + 同行三级</div>
|
||||
<div class="card"><strong>叶子二级</strong>补虚拟三级(同名)</div>
|
||||
<div class="card"><strong>可点范围</strong>仅三级(含虚拟)</div>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<button type="button" class="tab active" data-style="pill">样式:三级胶囊</button>
|
||||
<button type="button" class="tab" data-style="text">样式:三级文字链</button>
|
||||
<button type="button" class="tab" data-style="compact">样式:更紧凑行距</button>
|
||||
</div>
|
||||
|
||||
<div class="shell">
|
||||
<div class="shell-bar">
|
||||
<span id="shell-title">预览 · 一行一二级</span>
|
||||
<span id="status">点击三级进入页面</span>
|
||||
</div>
|
||||
<div class="frame" id="frame"></div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button type="button" class="btn primary" data-choice="row-layout-confirm" onclick="typeof toggleSelect==='function'&&toggleSelect(this)">✓ 就按这个布局定稿</button>
|
||||
<button type="button" class="btn" data-choice="row-pill" onclick="typeof toggleSelect==='function'&&toggleSelect(this)">三级用胶囊</button>
|
||||
<button type="button" class="btn" data-choice="row-text" onclick="typeof toggleSelect==='function'&&toggleSelect(this)">三级用文字链</button>
|
||||
<button type="button" class="btn ghost" data-choice="row-need-tweak" onclick="typeof toggleSelect==='function'&&toggleSelect(this)">方向对,还要改细节</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const modules = {
|
||||
crm: {
|
||||
label: 'CRM',
|
||||
rows: [
|
||||
{
|
||||
title: '客户管理',
|
||||
children: [
|
||||
{ name: '客户列表', path: '客户列表' },
|
||||
{ name: '联系人', path: '联系人' },
|
||||
{ name: '跟进记录', path: '跟进记录' },
|
||||
{ name: '客户公海', path: '客户公海' },
|
||||
{ name: '客户标签', path: '客户标签' }
|
||||
]
|
||||
},
|
||||
{ title: '商机看板', path: '商机看板', children: [] },
|
||||
{
|
||||
title: '合同交易',
|
||||
children: [
|
||||
{ name: '合同', path: '合同' },
|
||||
{ name: '报价单', path: '报价单' },
|
||||
{ name: '回款', path: '回款' }
|
||||
]
|
||||
},
|
||||
{ title: '产品目录', path: '产品目录', children: [] },
|
||||
{
|
||||
title: '服务支持',
|
||||
children: [
|
||||
{ name: '工单', path: '工单' },
|
||||
{ name: '知识库', path: '知识库' },
|
||||
{ name: '满意度', path: '满意度' }
|
||||
]
|
||||
},
|
||||
{ title: '业绩报表', path: '业绩报表', children: [] },
|
||||
{
|
||||
title: '配置中心',
|
||||
children: [
|
||||
{ name: '字段配置', path: '字段配置' },
|
||||
{ name: '审批流', path: '审批流' },
|
||||
{ name: '编号规则', path: '编号规则' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
bx: {
|
||||
label: '报销',
|
||||
rows: [
|
||||
{
|
||||
title: '费用申请',
|
||||
children: [
|
||||
{ name: '报销单', path: '报销单' },
|
||||
{ name: '借款单', path: '借款单' }
|
||||
]
|
||||
},
|
||||
{ title: '费用台账', path: '费用台账', children: [] },
|
||||
{
|
||||
title: '基础设置',
|
||||
children: [
|
||||
{ name: '费用项目', path: '费用项目' },
|
||||
{ name: '审批规则', path: '审批规则' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
sys: {
|
||||
label: '系统',
|
||||
rows: [
|
||||
{
|
||||
title: '组织权限',
|
||||
children: [
|
||||
{ name: '用户', path: '用户' },
|
||||
{ name: '角色', path: '角色' },
|
||||
{ name: '菜单', path: '菜单' }
|
||||
]
|
||||
},
|
||||
{ title: '租户管理', path: '租户管理', children: [] },
|
||||
{ title: '操作日志', path: '操作日志', children: [] }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const order = ['crm', 'bx', 'sys']
|
||||
let openKey = 'crm'
|
||||
let style = 'pill' // pill | text | compact
|
||||
let activePath = '客户列表'
|
||||
|
||||
function normalizeRows(rows) {
|
||||
return rows.map(r => {
|
||||
const children = r.children || []
|
||||
if (children.length) {
|
||||
return {
|
||||
title: r.title,
|
||||
children: children.map(c => ({ ...c, virtual: false }))
|
||||
}
|
||||
}
|
||||
return {
|
||||
title: r.title,
|
||||
children: [{
|
||||
name: r.title,
|
||||
path: r.path || r.title,
|
||||
virtual: true
|
||||
}]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const frame = document.getElementById('frame')
|
||||
const shellTitle = document.getElementById('shell-title')
|
||||
const statusEl = document.getElementById('status')
|
||||
|
||||
document.querySelectorAll('.tab').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
style = btn.dataset.style
|
||||
document.querySelectorAll('.tab').forEach(b => b.classList.toggle('active', b === btn))
|
||||
if (!openKey) openKey = 'crm'
|
||||
paint()
|
||||
})
|
||||
})
|
||||
|
||||
function paint() {
|
||||
const mod = openKey ? modules[openKey] : null
|
||||
const rows = mod ? normalizeRows(mod.rows) : []
|
||||
shellTitle.textContent = openKey
|
||||
? `预览 · ${mod.label} · 一行一二级`
|
||||
: '预览 · 点击一级打开面板'
|
||||
statusEl.textContent = `当前页:${activePath}`
|
||||
|
||||
const bodyClass = [
|
||||
style === 'compact' ? 'compact' : '',
|
||||
'wide-l2'
|
||||
].filter(Boolean).join(' ')
|
||||
|
||||
const usePill = style === 'pill' || style === 'compact'
|
||||
|
||||
frame.innerHTML = `
|
||||
<div class="app">
|
||||
<div class="top">
|
||||
<div class="logo">山峡</div>
|
||||
<div class="l1">
|
||||
${order.map(k => `
|
||||
<div class="l1-item ${openKey === k ? 'on' : ''}" data-l1="${k}">
|
||||
${modules[k].label}${openKey === k ? ' ▾' : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<div class="avatar"></div>
|
||||
</div>
|
||||
${openKey ? `
|
||||
<div class="mask" data-close="1"></div>
|
||||
<div class="mega">
|
||||
<div class="mega-tools">
|
||||
<div class="search">搜索菜单…</div>
|
||||
<span style="color:#94a3b8;font-size:12px">左侧二级标题不可点 · 右侧三级可点 · 虚线胶囊=虚拟三级</span>
|
||||
</div>
|
||||
<div class="mega-body ${bodyClass}">
|
||||
${rows.map(row => `
|
||||
<div class="row">
|
||||
<div class="l2">
|
||||
${row.title}
|
||||
<span class="sub">${row.children.length} 个入口${row.children.some(c => c.virtual) ? ' · 含虚拟' : ''}</span>
|
||||
</div>
|
||||
<div class="${usePill ? 'l3s' : 'text-links'}">
|
||||
${row.children.map(c => usePill
|
||||
? `<button type="button" class="pill ${activePath === c.path ? 'on' : ''} ${c.virtual ? 'virtual' : ''}" data-go="${c.path}">${c.name}</button>`
|
||||
: `<a class="${activePath === c.path ? 'on' : ''} ${c.virtual ? 'virtual' : ''}" data-go="${c.path}">${c.name}</a>`
|
||||
).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="main">
|
||||
<div class="panel">
|
||||
<h2>${activePath}</h2>
|
||||
<p>内容区全宽。菜单只在顶部大面板中,按「一行二级 + 同行三级」排布。</p>
|
||||
<div class="bars"><div class="bar"></div><div class="bar w2"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hint">一行一个二级 · 三级跟在后面</div>
|
||||
</div>`
|
||||
|
||||
frame.querySelectorAll('[data-l1]').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const key = el.dataset.l1
|
||||
openKey = openKey === key ? null : key
|
||||
paint()
|
||||
})
|
||||
})
|
||||
frame.querySelectorAll('[data-close]').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
openKey = null
|
||||
paint()
|
||||
})
|
||||
})
|
||||
frame.querySelectorAll('[data-go]').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
activePath = el.getAttribute('data-go')
|
||||
openKey = null
|
||||
paint()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
paint()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,535 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>S5 · 三级数量不均时的布局</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary: #10b981;
|
||||
--primary-dark: #059669;
|
||||
--primary-bg: #ecfdf5;
|
||||
--text: #0f172a;
|
||||
--muted: #64748b;
|
||||
--border: #e2e8f0;
|
||||
--bg: #f1f5f9;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Inter, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
color: var(--text);
|
||||
background: #f8fafc;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.page { padding: 20px 24px 40px; max-width: 1280px; margin: 0 auto; }
|
||||
h1 { font-size: 22px; margin: 0 0 6px; }
|
||||
.lead { color: var(--muted); font-size: 14px; margin: 0 0 12px; }
|
||||
.note {
|
||||
background: var(--primary-bg);
|
||||
border: 1px solid #a7f3d0;
|
||||
color: #065f46;
|
||||
border-radius: 10px;
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.tabs { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; }
|
||||
.tab {
|
||||
border: 1px solid var(--border);
|
||||
background: #fff;
|
||||
border-radius: 999px;
|
||||
padding: 7px 12px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tab.active {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr;
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
}
|
||||
@media (max-width: 960px) { .layout { grid-template-columns: 1fr; } }
|
||||
.meta, .shell {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.meta { padding: 16px; }
|
||||
.meta h2 { margin: 0 0 8px; font-size: 16px; }
|
||||
.meta .desc { margin: 0 0 10px; font-size: 13px; color: var(--muted); }
|
||||
.meta ul { margin: 0; padding-left: 18px; font-size: 12px; color: var(--muted); }
|
||||
.rec {
|
||||
display: inline-block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--primary-bg);
|
||||
color: #047857;
|
||||
font-weight: 600;
|
||||
}
|
||||
.pick {
|
||||
margin-top: 14px;
|
||||
width: 100%;
|
||||
border: 1px solid var(--primary);
|
||||
background: var(--primary-bg);
|
||||
color: var(--primary-dark);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.pick.on { background: var(--primary); color: #fff; }
|
||||
.shell-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: #fafafa;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.frame { height: 460px; background: var(--bg); position: relative; overflow: hidden; }
|
||||
.fav {
|
||||
margin-top: 14px;
|
||||
padding: 12px 14px;
|
||||
border: 1px dashed #a7f3d0;
|
||||
border-radius: 10px;
|
||||
background: #f0fdf4;
|
||||
font-size: 13px;
|
||||
color: #065f46;
|
||||
}
|
||||
.legend {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.legend i {
|
||||
display: inline-block;
|
||||
width: 10px; height: 10px;
|
||||
border-radius: 2px;
|
||||
margin-right: 4px;
|
||||
vertical-align: -1px;
|
||||
}
|
||||
.legend .tall { background: #6ee7b7; }
|
||||
.legend .short { background: #fcd34d; }
|
||||
.legend .single { background: #93c5fd; }
|
||||
|
||||
/* mock */
|
||||
.app { height: 100%; display: flex; flex-direction: column; font-size: 12px; }
|
||||
.top {
|
||||
height: 44px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 0 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.logo { font-weight: 700; color: var(--primary-dark); margin-right: 8px; }
|
||||
.l1 span {
|
||||
padding: 0 10px;
|
||||
color: #94a3b8;
|
||||
height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
.l1 span.on {
|
||||
color: var(--primary-dark);
|
||||
font-weight: 600;
|
||||
background: var(--primary-bg);
|
||||
border-bottom: 2px solid var(--primary);
|
||||
}
|
||||
.spacer { flex: 1; }
|
||||
.avatar {
|
||||
width: 22px; height: 22px; border-radius: 50%;
|
||||
background: linear-gradient(135deg, #6ee7b7, #34d399);
|
||||
}
|
||||
.mega {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 12px 28px rgba(15,23,42,.1);
|
||||
}
|
||||
.tools {
|
||||
padding: 8px 14px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
background: #fafafa;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.body { flex: 1; min-height: 0; overflow: auto; padding: 12px 14px; }
|
||||
.tag {
|
||||
position: absolute; left: 10px; bottom: 10px;
|
||||
background: rgba(15,23,42,.78); color: #fff;
|
||||
font-size: 11px; padding: 4px 8px; border-radius: 6px;
|
||||
pointer-events: none; z-index: 2;
|
||||
}
|
||||
|
||||
.col h4 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding-bottom: 5px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
}
|
||||
.col h4 em {
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-size: 10px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.link { display: block; padding: 3px 0; color: #475569; }
|
||||
.link.on { color: var(--primary-dark); font-weight: 600; }
|
||||
|
||||
/* U1 equal grid - ragged bottoms */
|
||||
.grid5 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 12px 14px;
|
||||
align-items: start;
|
||||
}
|
||||
.col.kind-tall { background: #ecfdf5; border-radius: 8px; padding: 8px; }
|
||||
.col.kind-short { background: #fffbeb; border-radius: 8px; padding: 8px; }
|
||||
.col.kind-single { background: #eff6ff; border-radius: 8px; padding: 8px; }
|
||||
|
||||
/* U2 masonry via CSS columns */
|
||||
.masonry {
|
||||
column-count: 5;
|
||||
column-gap: 14px;
|
||||
}
|
||||
.masonry .col {
|
||||
break-inside: avoid;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
border: 1px solid #f1f5f9;
|
||||
}
|
||||
.masonry .col.kind-tall { background: #ecfdf5; }
|
||||
.masonry .col.kind-short { background: #fffbeb; }
|
||||
.masonry .col.kind-single { background: #eff6ff; }
|
||||
|
||||
/* U3 promote singles */
|
||||
.flat-link {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 8px;
|
||||
background: #eff6ff;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid #dbeafe;
|
||||
}
|
||||
.flat-link .l2 { color: #64748b; font-size: 11px; }
|
||||
.flat-link .l3 { color: #1d4ed8; font-weight: 600; }
|
||||
.flat-link .arrow { color: #94a3b8; font-size: 10px; }
|
||||
|
||||
/* U4 merge tiny into 其他 */
|
||||
.other-box {
|
||||
background: #f8fafc;
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
.other-box h4 { border: 0; margin-bottom: 8px; }
|
||||
.chip-row { display: flex; flex-direction: column; gap: 4px; }
|
||||
.chip-row .link { padding: 2px 0; }
|
||||
|
||||
/* U5 horizontal rows */
|
||||
.row-block {
|
||||
display: grid;
|
||||
grid-template-columns: 110px 1fr;
|
||||
gap: 10px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
align-items: start;
|
||||
}
|
||||
.row-block .title {
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
color: var(--text);
|
||||
padding-top: 2px;
|
||||
}
|
||||
.row-block .title em {
|
||||
display: block;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-size: 10px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.pills { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.pill {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
padding: 3px 10px;
|
||||
font-size: 11px;
|
||||
color: #334155;
|
||||
background: #fff;
|
||||
}
|
||||
.pill.on {
|
||||
border-color: #6ee7b7;
|
||||
background: var(--primary-bg);
|
||||
color: var(--primary-dark);
|
||||
font-weight: 600;
|
||||
}
|
||||
.row-block.kind-tall { background: linear-gradient(90deg, #ecfdf5 0, transparent 110px); }
|
||||
.row-block.kind-single { background: linear-gradient(90deg, #eff6ff 0, transparent 110px); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<h1>S5 加密列:三级有多有少,怎么摆?</h1>
|
||||
<p class="lead">
|
||||
同一份数据:有的二级下 <strong>5 个三级</strong>(绿),有的 <strong>2~3 个</strong>(黄),有的只有 <strong>1 个</strong>(蓝)。对比 5 种处理不均衡的方式。
|
||||
</p>
|
||||
<div class="note">
|
||||
核心矛盾:5 列网格按「列」排时,短列下方会留白。可选:接受留白、瀑布流填缝、单三级「提级」、把小二级合并到「其他」、或改成「横向一行一个二级」。
|
||||
</div>
|
||||
|
||||
<div class="tabs" id="tabs"></div>
|
||||
<div class="layout">
|
||||
<aside class="meta" id="meta"></aside>
|
||||
<section class="shell">
|
||||
<div class="shell-bar">
|
||||
<span id="title">预览</span>
|
||||
<span>S5 · 三级数量不均</span>
|
||||
</div>
|
||||
<div class="frame" id="preview"></div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="legend">
|
||||
<span><i class="tall"></i>二级下 5 个三级</span>
|
||||
<span><i class="short"></i>二级下 2~3 个</span>
|
||||
<span><i class="single"></i>二级下仅 1 个</span>
|
||||
</div>
|
||||
<div class="fav" id="fav">标记你更倾向的不均衡处理方式。</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const groups = [
|
||||
{ title: '客户管理', items: ['客户列表', '联系人', '跟进记录', '客户公海', '客户标签'], kind: 'tall' },
|
||||
{ title: '商机', items: ['商机列表'], kind: 'single' },
|
||||
{ title: '合同交易', items: ['合同', '报价单', '回款', '开票', '信用'], kind: 'tall' },
|
||||
{ title: '产品', items: ['产品档案', '分类'], kind: 'short' },
|
||||
{ title: '单位', items: ['计量单位'], kind: 'single' },
|
||||
{ title: '营销', items: ['线索', '活动', '触达'], kind: 'short' },
|
||||
{ title: '服务工单', items: ['工单', '知识库', '满意度', '回访', 'SLA'], kind: 'tall' },
|
||||
{ title: '渠道', items: ['经销商'], kind: 'single' },
|
||||
{ title: '报表', items: ['漏斗', '业绩'], kind: 'short' },
|
||||
{ title: '配置', items: ['字段配置'], kind: 'single' }
|
||||
]
|
||||
|
||||
function kindClass(k) { return `kind-${k}` }
|
||||
function topbar() {
|
||||
return `
|
||||
<div class="top">
|
||||
<div class="logo">山峡</div>
|
||||
<div class="l1"><span class="on">CRM ▾</span><span>报销</span><span>地磅</span><span>系统</span></div>
|
||||
<div class="spacer"></div>
|
||||
<div class="avatar"></div>
|
||||
</div>`
|
||||
}
|
||||
function colHtml(g, highlightFirst) {
|
||||
return `
|
||||
<div class="col ${kindClass(g.kind)}">
|
||||
<h4>${g.title}<em>${g.items.length}项</em></h4>
|
||||
${g.items.map((it, i) => `<div class="link ${highlightFirst && i === 0 ? 'on' : ''}">${it}</div>`).join('')}
|
||||
</div>`
|
||||
}
|
||||
|
||||
const strategies = [
|
||||
{
|
||||
id: 'ragged',
|
||||
letter: 'U1',
|
||||
name: '普通 5 列网格(接受留白)',
|
||||
desc: '仍用 5 列 CSS Grid,每列高度随三级数量自然伸缩。短列下方留白。实现最简单,视觉略「参差」。',
|
||||
points: ['实现成本最低', '二级边界最清晰', '短列下方空白明显', '适合三级数量差异不太大时'],
|
||||
render() {
|
||||
return `
|
||||
<div class="app">
|
||||
${topbar()}
|
||||
<div class="mega">
|
||||
<div class="tools"><span>5 列 Grid · align-items: start</span><span>短列下方留白</span></div>
|
||||
<div class="body"><div class="grid5">${groups.map((g, i) => colHtml(g, i === 0)).join('')}</div></div>
|
||||
</div>
|
||||
<div class="tag">U1 参差留白 · 最简单</div>
|
||||
</div>`
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'masonry',
|
||||
letter: 'U2',
|
||||
name: '瀑布流填缝(推荐与 S5 搭配)',
|
||||
rec: true,
|
||||
desc: '用多列瀑布流(CSS columns / masonry):矮块会上移填补空白。二级块高度仍不同,但整体更紧凑,少大块留白。',
|
||||
points: ['空间利用率高,适合 S5', '仍保留「二级=一块」', '阅读顺序是先上下再左右,需习惯', '块顺序可能与配置顺序视觉不完全同行'],
|
||||
render() {
|
||||
return `
|
||||
<div class="app">
|
||||
${topbar()}
|
||||
<div class="mega">
|
||||
<div class="tools"><span>column-count: 5 · 矮块上移填缝</span><span>更紧凑</span></div>
|
||||
<div class="body"><div class="masonry">${groups.map((g, i) => colHtml(g, i === 0)).join('')}</div></div>
|
||||
</div>
|
||||
<div class="tag">U2 推荐:瀑布流消化高低差</div>
|
||||
</div>`
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'promote',
|
||||
letter: 'U3',
|
||||
name: '仅 1 个三级时「提级」成单行链接',
|
||||
rec: true,
|
||||
desc: '规则:三级 ≥2 仍作二级块;三级 =1 时不占整列,改成「二级名 → 三级名」单行入口,可单独排成一行或插入流中,避免空荡的一列只挂 1 个链接。',
|
||||
points: ['消灭「一列只有一项」的浪费', '信息层级仍可辨认', '需要一条明确的展示规则', '可与 U2 瀑布流叠加'],
|
||||
render() {
|
||||
const blocks = groups.filter(g => g.items.length >= 2)
|
||||
const singles = groups.filter(g => g.items.length === 1)
|
||||
return `
|
||||
<div class="app">
|
||||
${topbar()}
|
||||
<div class="mega">
|
||||
<div class="tools"><span>单三级提级 · 多三级保持块</span><span>蓝条 = 仅 1 个三级</span></div>
|
||||
<div class="body">
|
||||
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:6px;margin-bottom:12px">
|
||||
${singles.map(g => `
|
||||
<div class="flat-link">
|
||||
<span class="l2">${g.title}</span>
|
||||
<span class="arrow">→</span>
|
||||
<span class="l3">${g.items[0]}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div class="grid5" style="grid-template-columns:repeat(4,1fr)">
|
||||
${blocks.map((g, i) => colHtml(g, i === 0)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tag">U3 单三级提级 · 减少空列</div>
|
||||
</div>`
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'merge-other',
|
||||
letter: 'U4',
|
||||
name: '小二级合并进「其他」列',
|
||||
desc: '三级 ≤2 的二级,不再各自占一列,合并到最后一列「其他 / 更多」里用「二级:三级」或扁平链接展示。大二级仍独立成列。',
|
||||
points: ['大块菜单更突出', '需要合并阈值(如 ≤2)', '「其他」可能变长', '适合有明显主干菜单时'],
|
||||
render() {
|
||||
const main = groups.filter(g => g.items.length >= 3)
|
||||
const tiny = groups.filter(g => g.items.length < 3)
|
||||
return `
|
||||
<div class="app">
|
||||
${topbar()}
|
||||
<div class="mega">
|
||||
<div class="tools"><span>主干二级独立列 · 小二级进「其他」</span><span>阈值:三级 < 3</span></div>
|
||||
<div class="body">
|
||||
<div class="grid5" style="grid-template-columns:repeat(${Math.min(main.length + 1, 5)},1fr)">
|
||||
${main.map((g, i) => colHtml(g, i === 0)).join('')}
|
||||
<div class="col other-box">
|
||||
<h4>其他<small style="font-weight:500;color:#94a3b8"> · ${tiny.length}组</small></h4>
|
||||
<div class="chip-row">
|
||||
${tiny.map(g => g.items.map(it =>
|
||||
`<div class="link"><span style="color:#94a3b8">${g.title} · </span>${it}</div>`
|
||||
).join('')).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tag">U4 小二级合并到「其他」</div>
|
||||
</div>`
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'rows',
|
||||
letter: 'U5',
|
||||
name: '改横向:一行一个二级',
|
||||
desc: '放弃「列卡片」:每个二级占一行——左侧二级名,右侧三级以标签/链接横排换行。1 个或 5 个三级都自然,不再有列高不齐问题。',
|
||||
points: ['彻底解决高低不齐', '扫描路径更线性', '二级很多时面板变高', '密度上不如纯多列「一眼扫完」'],
|
||||
render() {
|
||||
return `
|
||||
<div class="app">
|
||||
${topbar()}
|
||||
<div class="mega">
|
||||
<div class="tools"><span>左二级 · 右三级胶囊</span><span>无列高差</span></div>
|
||||
<div class="body">
|
||||
${groups.map((g, gi) => `
|
||||
<div class="row-block ${kindClass(g.kind)}">
|
||||
<div class="title">${g.title}<em>${g.items.length} 项</em></div>
|
||||
<div class="pills">
|
||||
${g.items.map((it, i) => `<span class="pill ${gi === 0 && i === 0 ? 'on' : ''}">${it}</span>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tag">U5 横排行布局 · 无留白问题</div>
|
||||
</div>`
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const picked = new Set()
|
||||
let current = 'masonry'
|
||||
|
||||
const tabsEl = document.getElementById('tabs')
|
||||
const metaEl = document.getElementById('meta')
|
||||
const previewEl = document.getElementById('preview')
|
||||
const titleEl = document.getElementById('title')
|
||||
const favEl = document.getElementById('fav')
|
||||
|
||||
function paint() {
|
||||
const s = strategies.find(x => x.id === current)
|
||||
tabsEl.innerHTML = strategies.map(x =>
|
||||
`<button type="button" class="tab ${x.id === current ? 'active' : ''}" data-id="${x.id}">${x.letter} ${x.name.split('(')[0]}</button>`
|
||||
).join('')
|
||||
tabsEl.querySelectorAll('.tab').forEach(btn => {
|
||||
btn.onclick = () => { current = btn.dataset.id; paint() }
|
||||
})
|
||||
titleEl.textContent = `预览 ${s.letter} · ${s.name}`
|
||||
previewEl.innerHTML = s.render()
|
||||
metaEl.innerHTML = `
|
||||
${s.rec ? '<div class="rec">较推荐</div>' : ''}
|
||||
<h2>${s.letter}. ${s.name}</h2>
|
||||
<p class="desc">${s.desc}</p>
|
||||
<ul>${s.points.map(p => `<li>${p}</li>`).join('')}</ul>
|
||||
<button type="button" class="pick ${picked.has(s.id) ? 'on' : ''}" id="pick"
|
||||
data-choice="${s.id}" onclick="typeof toggleSelect==='function'&&toggleSelect(this)">
|
||||
${picked.has(s.id) ? '✓ 已标记' : '标记该方式'}
|
||||
</button>
|
||||
`
|
||||
document.getElementById('pick').addEventListener('click', () => {
|
||||
if (picked.has(s.id)) picked.delete(s.id)
|
||||
else picked.add(s.id)
|
||||
paint()
|
||||
})
|
||||
favEl.innerHTML = picked.size
|
||||
? `已标记:<strong>${strategies.filter(x => picked.has(x.id)).map(x => x.letter).join('、')}</strong>。回终端确认。`
|
||||
: '标记你更倾向的不均衡处理方式。'
|
||||
}
|
||||
|
||||
paint()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,536 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>统一模式:无三级时生成虚拟三级</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary: #10b981;
|
||||
--primary-dark: #059669;
|
||||
--primary-bg: #ecfdf5;
|
||||
--text: #0f172a;
|
||||
--muted: #64748b;
|
||||
--border: #e2e8f0;
|
||||
--bg: #f1f5f9;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Inter, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
color: var(--text);
|
||||
background: #f8fafc;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.page { padding: 20px 24px 40px; max-width: 1280px; margin: 0 auto; }
|
||||
h1 { font-size: 22px; margin: 0 0 6px; }
|
||||
.lead { color: var(--muted); font-size: 14px; margin: 0 0 12px; }
|
||||
.note {
|
||||
background: var(--primary-bg);
|
||||
border: 1px solid #a7f3d0;
|
||||
color: #065f46;
|
||||
border-radius: 10px;
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.tabs { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; }
|
||||
.tab {
|
||||
border: 1px solid var(--border);
|
||||
background: #fff;
|
||||
border-radius: 999px;
|
||||
padding: 7px 12px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tab.active {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 320px 1fr;
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
}
|
||||
@media (max-width: 960px) { .layout { grid-template-columns: 1fr; } }
|
||||
.meta, .shell {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.meta { padding: 16px; }
|
||||
.meta h2 { margin: 0 0 8px; font-size: 16px; }
|
||||
.meta .desc { margin: 0 0 10px; font-size: 13px; color: var(--muted); }
|
||||
.meta ul { margin: 0; padding-left: 18px; font-size: 12px; color: var(--muted); }
|
||||
.meta pre {
|
||||
margin: 12px 0 0;
|
||||
padding: 10px;
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
border-radius: 8px;
|
||||
font-size: 11px;
|
||||
overflow: auto;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.rec {
|
||||
display: inline-block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--primary-bg);
|
||||
color: #047857;
|
||||
font-weight: 600;
|
||||
}
|
||||
.pick {
|
||||
margin-top: 14px;
|
||||
width: 100%;
|
||||
border: 1px solid var(--primary);
|
||||
background: var(--primary-bg);
|
||||
color: var(--primary-dark);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.pick.on { background: var(--primary); color: #fff; }
|
||||
.shell-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: #fafafa;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.frame { height: 460px; background: var(--bg); position: relative; overflow: hidden; }
|
||||
.fav {
|
||||
margin-top: 14px;
|
||||
padding: 12px 14px;
|
||||
border: 1px dashed #a7f3d0;
|
||||
border-radius: 10px;
|
||||
background: #f0fdf4;
|
||||
font-size: 13px;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.app { height: 100%; display: flex; flex-direction: column; font-size: 12px; }
|
||||
.top {
|
||||
height: 44px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 0 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.logo { font-weight: 700; color: var(--primary-dark); margin-right: 8px; }
|
||||
.l1 span {
|
||||
padding: 0 10px;
|
||||
color: #94a3b8;
|
||||
height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
.l1 span.on {
|
||||
color: var(--primary-dark);
|
||||
font-weight: 600;
|
||||
background: var(--primary-bg);
|
||||
border-bottom: 2px solid var(--primary);
|
||||
}
|
||||
.spacer { flex: 1; }
|
||||
.avatar {
|
||||
width: 22px; height: 22px; border-radius: 50%;
|
||||
background: linear-gradient(135deg, #6ee7b7, #34d399);
|
||||
}
|
||||
.mega {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 12px 28px rgba(15,23,42,.1);
|
||||
}
|
||||
.tools {
|
||||
padding: 8px 14px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
background: #fafafa;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.body { flex: 1; min-height: 0; overflow: auto; padding: 12px 14px; }
|
||||
.masonry { column-count: 5; column-gap: 14px; }
|
||||
.block {
|
||||
break-inside: avoid;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
border: 1px solid #f1f5f9;
|
||||
background: #f8fafc;
|
||||
}
|
||||
.block h4 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding-bottom: 5px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
color: #334155;
|
||||
}
|
||||
.link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
padding: 3px 0;
|
||||
color: #475569;
|
||||
cursor: pointer;
|
||||
}
|
||||
.link:hover { color: var(--primary-dark); }
|
||||
.link.on { color: var(--primary-dark); font-weight: 600; }
|
||||
.badge {
|
||||
font-size: 9px;
|
||||
padding: 1px 5px;
|
||||
border-radius: 999px;
|
||||
background: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.badge.hide { display: none; }
|
||||
.leaf-card {
|
||||
break-inside: avoid;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
background: var(--primary-bg);
|
||||
border: 1px solid #a7f3d0;
|
||||
font-weight: 600;
|
||||
color: var(--primary-dark);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.page-panel {
|
||||
flex: 1;
|
||||
margin: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
}
|
||||
.page-panel h2 { margin: 0 0 6px; font-size: 16px; }
|
||||
.page-panel p { margin: 0; color: var(--muted); }
|
||||
.tag {
|
||||
position: absolute; left: 10px; bottom: 10px;
|
||||
background: rgba(15,23,42,.78); color: #fff;
|
||||
font-size: 11px; padding: 4px 8px; border-radius: 6px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.compare {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.compare .box {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
background: #f8fafc;
|
||||
}
|
||||
.compare .box strong { display: block; color: var(--text); margin-bottom: 4px; font-size: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<h1>统一渲染:无三级时,补一个虚拟三级</h1>
|
||||
<p class="lead">
|
||||
你的想法:面板里<strong>永远同一套结构</strong>——二级只做分组标题(不可点),可点的永远是三级。若源数据二级没有子节点,前端/菜单层自动生成一个虚拟三级,承接原来二级的路由。
|
||||
</p>
|
||||
<div class="note">
|
||||
这样不需要叶子卡片、加粗直达等特殊样式;S5 瀑布流里每一块都是「标题 + 链接列表」。下面可切换虚拟三级的命名,并对比「规范化之前」。
|
||||
</div>
|
||||
|
||||
<div class="tabs" id="tabs"></div>
|
||||
<div class="layout">
|
||||
<aside class="meta" id="meta"></aside>
|
||||
<section class="shell">
|
||||
<div class="shell-bar">
|
||||
<span id="title">预览</span>
|
||||
<span id="status">点击三级链接试跳</span>
|
||||
</div>
|
||||
<div class="frame" id="preview"></div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="fav" id="fav">请确认是否采用「虚拟三级规范化」,并选择虚拟节点命名。</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 原始菜单(含无 children 的二级)
|
||||
const raw = [
|
||||
{
|
||||
title: '客户管理',
|
||||
children: [
|
||||
{ name: '客户列表', path: '客户列表' },
|
||||
{ name: '联系人', path: '联系人' },
|
||||
{ name: '跟进记录', path: '跟进记录' }
|
||||
]
|
||||
},
|
||||
{ title: '商机看板', path: '商机看板', children: [] },
|
||||
{
|
||||
title: '合同交易',
|
||||
children: [
|
||||
{ name: '合同', path: '合同' },
|
||||
{ name: '报价单', path: '报价单' },
|
||||
{ name: '回款', path: '回款' }
|
||||
]
|
||||
},
|
||||
{ title: '产品目录', path: '产品目录', children: [] },
|
||||
{
|
||||
title: '服务',
|
||||
children: [
|
||||
{ name: '工单', path: '工单' },
|
||||
{ name: '知识库', path: '知识库' }
|
||||
]
|
||||
},
|
||||
{ title: '业绩报表', path: '业绩报表', children: [] },
|
||||
{ title: '公海规则', path: '公海规则', children: [] }
|
||||
]
|
||||
|
||||
function normalize(list, naming) {
|
||||
return list.map(item => {
|
||||
const children = item.children || []
|
||||
if (children.length > 0) {
|
||||
return {
|
||||
title: item.title,
|
||||
children: children.map(c => ({ ...c, virtual: false }))
|
||||
}
|
||||
}
|
||||
// 无三级:生成虚拟三级,路由用二级原来的 path
|
||||
let name
|
||||
if (naming === 'same') name = item.title
|
||||
else if (naming === 'overview') name = '概览'
|
||||
else name = `进入${item.title}`
|
||||
return {
|
||||
title: item.title,
|
||||
children: [{
|
||||
name,
|
||||
path: item.path || item.title,
|
||||
virtual: true
|
||||
}]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const modes = [
|
||||
{
|
||||
id: 'before',
|
||||
letter: 'X0',
|
||||
name: '对比:规范化之前(两种样式)',
|
||||
desc: '有三级用分组块,无三级用叶子卡片——结构不统一。',
|
||||
naming: null,
|
||||
showVirtualBadge: false,
|
||||
before: true
|
||||
},
|
||||
{
|
||||
id: 'same',
|
||||
letter: 'V1',
|
||||
name: '虚拟三级文案 = 二级同名(推荐)',
|
||||
rec: true,
|
||||
desc: '例如二级「商机看板」→ 虚拟三级也叫「商机看板」。用户点的还是这个名字,只是从「点标题」变成「点标题下的链接」。',
|
||||
naming: 'same',
|
||||
showVirtualBadge: true
|
||||
},
|
||||
{
|
||||
id: 'overview',
|
||||
letter: 'V2',
|
||||
name: '虚拟三级文案 =「概览」',
|
||||
desc: '二级标题保留分类感,三级统一叫「概览」。适合二级名偏模块、页面是总览的场景。',
|
||||
naming: 'overview',
|
||||
showVirtualBadge: true
|
||||
},
|
||||
{
|
||||
id: 'enter',
|
||||
letter: 'V3',
|
||||
name: '虚拟三级文案 =「进入xxx」',
|
||||
desc: '更强调动作,但文案较长,5 列加密时容易折行。',
|
||||
naming: 'enter',
|
||||
showVirtualBadge: true
|
||||
}
|
||||
]
|
||||
|
||||
let mode = 'same'
|
||||
let activePath = '客户列表'
|
||||
let showMega = true
|
||||
let showBadge = true
|
||||
const picked = new Set()
|
||||
|
||||
const tabsEl = document.getElementById('tabs')
|
||||
const metaEl = document.getElementById('meta')
|
||||
const previewEl = document.getElementById('preview')
|
||||
const titleEl = document.getElementById('title')
|
||||
const statusEl = document.getElementById('status')
|
||||
const favEl = document.getElementById('fav')
|
||||
|
||||
function go(path) {
|
||||
activePath = path
|
||||
showMega = false
|
||||
paint()
|
||||
statusEl.textContent = `已进入「${path}」`
|
||||
}
|
||||
|
||||
function renderBefore() {
|
||||
return `
|
||||
<div class="masonry">
|
||||
${raw.map(item => {
|
||||
const children = item.children || []
|
||||
if (!children.length) {
|
||||
return `<div class="leaf-card"><span>${item.title}</span><span>进入 →</span></div>`
|
||||
}
|
||||
return `
|
||||
<div class="block">
|
||||
<h4>${item.title}</h4>
|
||||
${children.map(c => `
|
||||
<div class="link ${activePath === c.path ? 'on' : ''}" data-go="${c.path}">
|
||||
<span>${c.name}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>`
|
||||
}).join('')}
|
||||
</div>`
|
||||
}
|
||||
|
||||
function renderNormalized(naming) {
|
||||
const tree = normalize(raw, naming)
|
||||
return `
|
||||
<div class="masonry">
|
||||
${tree.map(item => `
|
||||
<div class="block">
|
||||
<h4>${item.title}</h4>
|
||||
${item.children.map(c => `
|
||||
<div class="link ${activePath === c.path ? 'on' : ''}" data-go="${c.path}">
|
||||
<span>${c.name}</span>
|
||||
<span class="badge ${c.virtual && showBadge ? '' : 'hide'}">虚拟</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>`
|
||||
}
|
||||
|
||||
function paint() {
|
||||
const m = modes.find(x => x.id === mode)
|
||||
tabsEl.innerHTML = modes.map(x =>
|
||||
`<button type="button" class="tab ${x.id === mode ? 'active' : ''}" data-id="${x.id}">${x.letter} ${x.name.split('(')[0]}</button>`
|
||||
).join('')
|
||||
tabsEl.querySelectorAll('.tab').forEach(btn => {
|
||||
btn.onclick = () => {
|
||||
mode = btn.dataset.id
|
||||
showMega = true
|
||||
paint()
|
||||
}
|
||||
})
|
||||
|
||||
titleEl.textContent = `预览 ${m.letter} · ${m.name}`
|
||||
const sample = m.naming ? normalize([raw[1]], m.naming)[0] : null
|
||||
|
||||
metaEl.innerHTML = `
|
||||
${m.rec ? '<div class="rec">推荐</div>' : ''}
|
||||
<h2>${m.letter}. ${m.name}</h2>
|
||||
<p class="desc">${m.desc}</p>
|
||||
<ul>
|
||||
<li>二级标题:只展示,不可点击</li>
|
||||
<li>三级链接:唯一可点击入口</li>
|
||||
<li>无子节点时:自动补 1 个虚拟三级,path 继承原二级</li>
|
||||
</ul>
|
||||
${sample ? `<pre>// 规范化后
|
||||
{
|
||||
title: "${sample.title}", // 二级(不可点)
|
||||
children: [{
|
||||
name: "${sample.children[0].name}", // 虚拟三级(可点)
|
||||
path: "${sample.children[0].path}",
|
||||
virtual: true
|
||||
}]
|
||||
}</pre>` : `<div class="compare">
|
||||
<div class="box"><strong>之前</strong>分组块 + 叶子卡片,两套 UI</div>
|
||||
<div class="box"><strong>之后</strong>全部都是「标题 + 三级链接」</div>
|
||||
</div>`}
|
||||
${m.naming ? `<label style="display:flex;gap:6px;align-items:center;margin-top:12px;font-size:12px;color:var(--muted)">
|
||||
<input type="checkbox" id="badge" ${showBadge ? 'checked' : ''}/> Demo 中显示「虚拟」角标(上线可关)
|
||||
</label>` : ''}
|
||||
<button type="button" class="pick ${picked.has(m.id) ? 'on' : ''}" id="pick"
|
||||
data-choice="${m.id}" onclick="typeof toggleSelect==='function'&&toggleSelect(this)">
|
||||
${picked.has(m.id) ? '✓ 已标记' : '标记该方案'}
|
||||
</button>
|
||||
`
|
||||
|
||||
document.getElementById('pick')?.addEventListener('click', () => {
|
||||
if (picked.has(m.id)) picked.delete(m.id)
|
||||
else picked.add(m.id)
|
||||
paint()
|
||||
})
|
||||
document.getElementById('badge')?.addEventListener('change', (e) => {
|
||||
showBadge = e.target.checked
|
||||
paint()
|
||||
})
|
||||
|
||||
if (!showMega) {
|
||||
previewEl.innerHTML = `
|
||||
<div class="app">
|
||||
<div class="top">
|
||||
<div class="logo">山峡</div>
|
||||
<div class="l1"><span class="on" id="reopen">CRM ▾</span><span>报销</span><span>系统</span></div>
|
||||
<div class="spacer"></div>
|
||||
<div class="avatar"></div>
|
||||
</div>
|
||||
<div class="page-panel">
|
||||
<h2>${activePath}</h2>
|
||||
<p>无论真实三级还是虚拟三级,跳转方式相同;虚拟节点只存在于展示层/规范化层。</p>
|
||||
</div>
|
||||
<div class="tag">点 CRM 重新打开面板</div>
|
||||
</div>`
|
||||
previewEl.querySelector('#reopen')?.addEventListener('click', () => {
|
||||
showMega = true
|
||||
paint()
|
||||
})
|
||||
} else {
|
||||
previewEl.innerHTML = `
|
||||
<div class="app">
|
||||
<div class="top">
|
||||
<div class="logo">山峡</div>
|
||||
<div class="l1"><span class="on">CRM ▾</span><span>报销</span><span>系统</span></div>
|
||||
<div class="spacer"></div>
|
||||
<div class="avatar"></div>
|
||||
</div>
|
||||
<div class="mega">
|
||||
<div class="tools">
|
||||
<span>${m.before ? '不统一:叶子卡片 vs 分组' : '统一:全部为二级标题 + 三级链接'}</span>
|
||||
<span>当前:${activePath}</span>
|
||||
</div>
|
||||
<div class="body">${m.before ? renderBefore() : renderNormalized(m.naming)}</div>
|
||||
</div>
|
||||
<div class="tag">${m.before ? 'X0 旧模式' : '虚拟三级承接原二级路由'}</div>
|
||||
</div>`
|
||||
previewEl.querySelectorAll('[data-go]').forEach(el => {
|
||||
el.addEventListener('click', () => go(el.getAttribute('data-go')))
|
||||
})
|
||||
}
|
||||
|
||||
favEl.innerHTML = picked.size
|
||||
? `已标记:<strong>${modes.filter(x => picked.has(x.id)).map(x => x.letter).join('、')}</strong>`
|
||||
: '请确认是否采用「虚拟三级规范化」,并选择虚拟节点命名。'
|
||||
}
|
||||
|
||||
paint()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,446 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>菜单布局对比:一行 vs 5列网格</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary: #10b981;
|
||||
--primary-dark: #059669;
|
||||
--primary-bg: #ecfdf5;
|
||||
--text: #0f172a;
|
||||
--muted: #64748b;
|
||||
--border: #e2e8f0;
|
||||
--bg: #f1f5f9;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Inter, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
color: var(--text);
|
||||
background: #f8fafc;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.page { padding: 20px 24px 40px; max-width: 1280px; margin: 0 auto; }
|
||||
h1 { font-size: 22px; margin: 0 0 6px; }
|
||||
.lead { color: var(--muted); font-size: 14px; margin: 0 0 12px; }
|
||||
.ok {
|
||||
background: var(--primary-bg);
|
||||
border: 1px solid #a7f3d0;
|
||||
color: #065f46;
|
||||
border-radius: 10px;
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.mode-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.mode-tab {
|
||||
border: 1px solid var(--border);
|
||||
background: #fff;
|
||||
border-radius: 999px;
|
||||
padding: 8px 14px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.mode-tab.active {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr;
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
}
|
||||
@media (max-width: 960px) { .layout { grid-template-columns: 1fr; } }
|
||||
.spec, .shell {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.spec { padding: 16px; }
|
||||
.spec h2 { margin: 0 0 10px; font-size: 16px; }
|
||||
.spec p { margin: 0 0 10px; font-size: 13px; color: var(--muted); }
|
||||
.spec ul { margin: 0; padding-left: 18px; font-size: 12px; color: #475569; }
|
||||
.spec li { margin-bottom: 6px; }
|
||||
.shell-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: #fafafa;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.frame { height: 480px; background: var(--bg); position: relative; overflow: hidden; }
|
||||
.actions { margin-top: 14px; display: flex; gap: 10px; flex-wrap: wrap; }
|
||||
.btn {
|
||||
border: 1px solid var(--primary);
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 10px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn.ghost {
|
||||
background: #fff;
|
||||
color: #334155;
|
||||
border-color: var(--border);
|
||||
}
|
||||
.btn.alt {
|
||||
background: var(--primary-bg);
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.app { height: 100%; display: flex; flex-direction: column; font-size: 13px; }
|
||||
.top {
|
||||
height: 48px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 14px;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.logo { font-weight: 700; color: var(--primary-dark); margin-right: 10px; }
|
||||
.l1-item {
|
||||
padding: 0 14px;
|
||||
height: 48px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: #64748b;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
.l1-item.on {
|
||||
color: var(--primary-dark);
|
||||
font-weight: 600;
|
||||
background: var(--primary-bg);
|
||||
border-bottom-color: var(--primary);
|
||||
}
|
||||
.spacer { flex: 1; }
|
||||
.avatar {
|
||||
width: 26px; height: 26px; border-radius: 50%;
|
||||
background: linear-gradient(135deg, #6ee7b7, #34d399);
|
||||
}
|
||||
.mega {
|
||||
background: #fff;
|
||||
border-bottom: 1px solid var(--border);
|
||||
box-shadow: 0 14px 32px rgba(15,23,42,.1);
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.tools {
|
||||
padding: 10px 18px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
background: #fafafa;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.body { flex: 1; overflow: auto; padding: 8px 18px 14px; }
|
||||
|
||||
/* 一行布局:左右两栏,每栏最多 5 行 */
|
||||
.row-split {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0 16px;
|
||||
align-items: start;
|
||||
min-height: 100%;
|
||||
}
|
||||
.row-pane {
|
||||
padding: 0 4px;
|
||||
min-height: 100%;
|
||||
}
|
||||
.row-pane.left {
|
||||
border-right: 1px solid var(--border);
|
||||
padding-right: 16px;
|
||||
}
|
||||
.row-pane.right {
|
||||
padding-left: 12px;
|
||||
}
|
||||
.row-pane-label {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
padding: 4px 0 2px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 96px 1fr;
|
||||
gap: 8px 12px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
|
||||
align-items: start;
|
||||
}
|
||||
.row-pane .row:last-child { border-bottom: 0; }
|
||||
.l2-title { font-weight: 700; font-size: 13px; padding-top: 2px; }
|
||||
.links { display: flex; flex-wrap: wrap; gap: 4px 16px; }
|
||||
.links a {
|
||||
color: #475569;
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
padding: 2px 0;
|
||||
cursor: default;
|
||||
}
|
||||
.links a.on { color: var(--primary-dark); font-weight: 600; }
|
||||
|
||||
/* 5 列网格:每列独立底色 */
|
||||
.body.grid5-body {
|
||||
background: #fff;
|
||||
}
|
||||
.cols5 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 14px 16px;
|
||||
align-items: start;
|
||||
}
|
||||
.col-block {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
}
|
||||
/* 间隔色:奇数列浅绿,偶数列浅蓝(不用灰) */
|
||||
.col-block:nth-child(odd) {
|
||||
background: #ecfdf5;
|
||||
border-color: #a7f3d0;
|
||||
}
|
||||
.col-block:nth-child(even) {
|
||||
background: #eff6ff;
|
||||
border-color: #bfdbfe;
|
||||
}
|
||||
.col-block h4 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid rgba(15, 23, 42, 0.08);
|
||||
color: #0f172a;
|
||||
}
|
||||
.col-block .links {
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.col-block .links a { padding: 2px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<h1>对比:一行二级 vs 普通 5 列网格</h1>
|
||||
<p class="lead">
|
||||
同一份数据:<strong>7 个二级菜单</strong>,三级均为文字链;无三级时补同名虚拟三级。切换上方标签看效果差异。
|
||||
</p>
|
||||
<div class="ok">
|
||||
A = 一行一个二级(你倾向的定稿方向)· B = 普通 5 列网格(每列一个二级块)。请对比后告诉我选哪个。
|
||||
</div>
|
||||
|
||||
<div class="mode-tabs">
|
||||
<button type="button" class="mode-tab active" data-mode="row">A. 一行二级(左右两栏 · 每栏最多5行)</button>
|
||||
<button type="button" class="mode-tab" data-mode="grid5">B. 普通 5 列网格</button>
|
||||
</div>
|
||||
|
||||
<div class="layout">
|
||||
<aside class="spec" id="meta"></aside>
|
||||
<section class="shell">
|
||||
<div class="shell-bar">
|
||||
<span id="shell-title">预览 A · 一行布局</span>
|
||||
<span>7 个二级 · 三级文字链</span>
|
||||
</div>
|
||||
<div class="frame" id="frame"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button type="button" class="btn" data-choice="prefer-row" onclick="typeof toggleSelect==='function'&&toggleSelect(this)">✓ 还是选 A 一行布局</button>
|
||||
<button type="button" class="btn alt" data-choice="prefer-grid5" onclick="typeof toggleSelect==='function'&&toggleSelect(this)">改选 B 5列网格</button>
|
||||
<button type="button" class="btn ghost" data-choice="design-revise" onclick="typeof toggleSelect==='function'&&toggleSelect(this)">还要改一处</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 7 个二级(含若干虚拟三级)
|
||||
const menuRows = [
|
||||
{
|
||||
title: '客户管理',
|
||||
children: [
|
||||
{ name: '客户列表', on: true },
|
||||
{ name: '联系人' },
|
||||
{ name: '跟进记录' },
|
||||
{ name: '客户公海' },
|
||||
{ name: '客户标签' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '商机看板',
|
||||
children: [{ name: '商机看板', virtual: true }]
|
||||
},
|
||||
{
|
||||
title: '合同交易',
|
||||
children: [
|
||||
{ name: '合同' },
|
||||
{ name: '报价单' },
|
||||
{ name: '回款' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '产品目录',
|
||||
children: [{ name: '产品目录', virtual: true }]
|
||||
},
|
||||
{
|
||||
title: '服务支持',
|
||||
children: [
|
||||
{ name: '工单' },
|
||||
{ name: '知识库' },
|
||||
{ name: '满意度' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '业绩报表',
|
||||
children: [{ name: '业绩报表', virtual: true }]
|
||||
},
|
||||
{
|
||||
title: '配置中心',
|
||||
children: [
|
||||
{ name: '字段配置' },
|
||||
{ name: '审批流' },
|
||||
{ name: '编号规则' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
let mode = 'row'
|
||||
const metaEl = document.getElementById('meta')
|
||||
const frameEl = document.getElementById('frame')
|
||||
const titleEl = document.getElementById('shell-title')
|
||||
|
||||
const metaCopy = {
|
||||
row: {
|
||||
title: 'A. 一行二级 · 左右分栏',
|
||||
desc: '仍是「一行一个二级 + 同行三级文字链」,但整块拆成左右两栏;每栏最多 5 行。当前 7 个二级:左 5、右 2。',
|
||||
points: [
|
||||
'单栏高度更可控,面板不易过长',
|
||||
'左右仅用中缝分隔,无底色',
|
||||
'超过 10 个二级时再向下滚动或加页',
|
||||
'二级仍不可点,三级文字链可点'
|
||||
]
|
||||
},
|
||||
grid5: {
|
||||
title: 'B. 普通 5 列网格',
|
||||
desc: '每列一个二级块:上标题、下三级纵向列表。7 个二级在 5 列下会换行成两行(5+2),短列下方可能留白。',
|
||||
points: [
|
||||
'首屏横向信息更密',
|
||||
'三级少的列会显得空',
|
||||
'第二行只有 2 列时右侧空白大',
|
||||
'适合二级块高度较接近时'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
function linkHtml(c) {
|
||||
return `<a class="${c.on ? 'on' : ''} ${c.virtual ? 'virtual' : ''}">${c.name}</a>`
|
||||
}
|
||||
|
||||
function topbar() {
|
||||
return `
|
||||
<div class="top">
|
||||
<div class="logo">山峡</div>
|
||||
<div class="l1-item on">CRM ▾</div>
|
||||
<div class="l1-item">报销</div>
|
||||
<div class="l1-item">地磅</div>
|
||||
<div class="l1-item">系统</div>
|
||||
<div class="spacer"></div>
|
||||
<div class="avatar"></div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
function rowHtml(r) {
|
||||
return `
|
||||
<div class="row">
|
||||
<div class="l2-title">${r.title}</div>
|
||||
<div class="links">${r.children.map(linkHtml).join('')}</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
function renderRow() {
|
||||
const perSide = 5
|
||||
const left = menuRows.slice(0, perSide)
|
||||
const right = menuRows.slice(perSide)
|
||||
return `
|
||||
<div class="app">
|
||||
${topbar()}
|
||||
<div class="mega">
|
||||
<div class="tools">一行布局 · 左右两栏 · 每栏最多 5 行 · 当前左 ${left.length} / 右 ${right.length}</div>
|
||||
<div class="body">
|
||||
<div class="row-split">
|
||||
<div class="row-pane left">
|
||||
<div class="row-pane-label">左栏(最多 5 行)</div>
|
||||
${left.map(rowHtml).join('')}
|
||||
</div>
|
||||
<div class="row-pane right">
|
||||
<div class="row-pane-label">右栏(最多 5 行)</div>
|
||||
${right.length ? right.map(rowHtml).join('') : '<div style="padding:12px 0;color:#94a3b8;font-size:12px">暂无更多二级</div>'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
function renderGrid5() {
|
||||
return `
|
||||
<div class="app">
|
||||
${topbar()}
|
||||
<div class="mega">
|
||||
<div class="tools">5 列网格 · 奇偶间隔色(浅绿 / 浅蓝)</div>
|
||||
<div class="body grid5-body">
|
||||
<div class="cols5">
|
||||
${menuRows.map(r => `
|
||||
<div class="col-block">
|
||||
<h4>${r.title}</h4>
|
||||
<div class="links">${r.children.map(linkHtml).join('')}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
function paint() {
|
||||
const m = metaCopy[mode]
|
||||
titleEl.textContent = mode === 'row' ? '预览 A · 一行布局' : '预览 B · 5 列网格'
|
||||
metaEl.innerHTML = `
|
||||
<h2>${m.title}</h2>
|
||||
<p>${m.desc}</p>
|
||||
<ul>${m.points.map(p => `<li>${p}</li>`).join('')}</ul>
|
||||
<p style="margin-top:12px;font-size:12px;color:#64748b">数据固定:客户管理、商机看板、合同交易、产品目录、服务支持、业绩报表、配置中心(共 7 个)。</p>
|
||||
`
|
||||
frameEl.innerHTML = mode === 'row' ? renderRow() : renderGrid5()
|
||||
}
|
||||
|
||||
document.querySelectorAll('.mode-tab').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
mode = btn.dataset.mode
|
||||
document.querySelectorAll('.mode-tab').forEach(b => b.classList.toggle('active', b === btn))
|
||||
paint()
|
||||
})
|
||||
})
|
||||
|
||||
paint()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,855 @@
|
||||
<!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;
|
||||
--text: #0f172a;
|
||||
--muted: #64748b;
|
||||
--border: #e2e8f0;
|
||||
--bg: #f1f5f9;
|
||||
--panel: #ffffff;
|
||||
--danger: #ef4444;
|
||||
--warn: #d97706;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Inter, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
color: var(--text);
|
||||
background: #f8fafc;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.page { padding: 20px 24px 40px; max-width: 1280px; margin: 0 auto; }
|
||||
h1 { font-size: 22px; margin: 0 0 6px; }
|
||||
.lead { color: var(--muted); font-size: 14px; margin: 0 0 18px; }
|
||||
.hint {
|
||||
background: var(--primary-bg);
|
||||
border: 1px solid #a7f3d0;
|
||||
color: #065f46;
|
||||
border-radius: 10px;
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.tab {
|
||||
border: 1px solid var(--border);
|
||||
background: #fff;
|
||||
color: var(--text);
|
||||
border-radius: 999px;
|
||||
padding: 7px 12px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: .15s ease;
|
||||
}
|
||||
.tab:hover { border-color: #6ee7b7; }
|
||||
.tab.active {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.tab .w { opacity: .75; margin-left: 4px; font-weight: 500; }
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 320px 1fr;
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
@media (max-width: 960px) {
|
||||
.layout { grid-template-columns: 1fr; }
|
||||
}
|
||||
.meta-card, .preview-shell {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.meta-card { padding: 16px; }
|
||||
.meta-card h2 { margin: 0 0 8px; font-size: 16px; }
|
||||
.meta-card .desc { font-size: 13px; color: var(--muted); margin: 0 0 12px; }
|
||||
.chip-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
|
||||
.chip {
|
||||
font-size: 11px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 999px;
|
||||
background: #f1f5f9;
|
||||
color: #334155;
|
||||
}
|
||||
.chip.good { background: var(--primary-bg); color: #047857; }
|
||||
.chip.warn { background: #fff7ed; color: #c2410c; }
|
||||
.pros-cons { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; font-size: 12px; }
|
||||
.pros, .cons {
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
.pros h4, .cons h4 { margin: 0 0 6px; font-size: 12px; }
|
||||
.pros ul, .cons ul { margin: 0; padding-left: 16px; color: var(--muted); }
|
||||
.select-btn {
|
||||
margin-top: 14px;
|
||||
width: 100%;
|
||||
border: 1px solid var(--primary);
|
||||
background: var(--primary-bg);
|
||||
color: var(--primary-dark);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.select-btn.picked {
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
}
|
||||
.preview-shell .preview-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: #fafafa;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.preview-frame {
|
||||
height: 420px;
|
||||
background: var(--bg);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ========== shared mock chrome ========== */
|
||||
.app {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 12px;
|
||||
}
|
||||
.top {
|
||||
height: 48px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 0 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.brand {
|
||||
font-weight: 700;
|
||||
color: var(--primary-dark);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mod-select {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 5px 10px;
|
||||
background: #fff;
|
||||
color: var(--text);
|
||||
cursor: default;
|
||||
}
|
||||
.mod-select .dot {
|
||||
width: 8px; height: 8px; border-radius: 50%;
|
||||
background: var(--primary);
|
||||
}
|
||||
.spacer { flex: 1; }
|
||||
.top-right { color: var(--muted); display: flex; gap: 10px; align-items: center; }
|
||||
.avatar {
|
||||
width: 24px; height: 24px; border-radius: 50%;
|
||||
background: linear-gradient(135deg, #6ee7b7, #34d399);
|
||||
}
|
||||
.body { flex: 1; display: flex; min-height: 0; }
|
||||
.side {
|
||||
background: #fff;
|
||||
border-right: 1px solid var(--border);
|
||||
overflow: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.main {
|
||||
flex: 1;
|
||||
padding: 14px;
|
||||
min-width: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
.panel {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
height: 100%;
|
||||
padding: 14px;
|
||||
}
|
||||
.panel h3 { margin: 0 0 8px; font-size: 14px; }
|
||||
.panel p { margin: 0; color: var(--muted); font-size: 12px; }
|
||||
.bars {
|
||||
margin-top: 14px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
.bar {
|
||||
height: 10px;
|
||||
border-radius: 6px;
|
||||
background: linear-gradient(90deg, #d1fae5, #f1f5f9);
|
||||
}
|
||||
.bar.w2 { width: 72%; }
|
||||
.bar.w3 { width: 54%; }
|
||||
.menu-item, .menu-group {
|
||||
padding: 8px 12px;
|
||||
color: #334155;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.menu-group { font-weight: 600; color: #0f172a; }
|
||||
.menu-item.active {
|
||||
background: var(--primary-bg);
|
||||
color: var(--primary-dark);
|
||||
border-right: 2px solid var(--primary);
|
||||
}
|
||||
.menu-item.child { padding-left: 28px; font-size: 11px; color: var(--muted); }
|
||||
.ico {
|
||||
width: 16px; height: 16px; border-radius: 4px;
|
||||
background: #e2e8f0; flex-shrink: 0;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
font-size: 9px; color: #64748b;
|
||||
}
|
||||
.menu-item.active .ico { background: #a7f3d0; color: #047857; }
|
||||
.width-tag {
|
||||
position: absolute;
|
||||
left: 8px; bottom: 8px;
|
||||
background: rgba(15,23,42,.78);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.hamburger {
|
||||
width: 28px; height: 28px; border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
background: #fff; cursor: pointer;
|
||||
}
|
||||
.top-nav { display: flex; gap: 2px; align-items: center; }
|
||||
.top-nav a {
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
color: #334155;
|
||||
text-decoration: none;
|
||||
font-size: 12px;
|
||||
}
|
||||
.top-nav a.on { background: var(--primary-bg); color: var(--primary-dark); font-weight: 600; }
|
||||
.mega {
|
||||
position: absolute;
|
||||
top: 48px; left: 120px;
|
||||
width: 420px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 12px 32px rgba(15,23,42,.12);
|
||||
padding: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
z-index: 8;
|
||||
}
|
||||
.mega .col h4 { margin: 0 0 6px; font-size: 11px; color: var(--muted); }
|
||||
.mega .col div {
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
color: #334155;
|
||||
}
|
||||
.mega .col div.on { background: var(--primary-bg); color: var(--primary-dark); }
|
||||
.dual-rail {
|
||||
width: 64px;
|
||||
background: #0f172a;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.dual-rail .ri {
|
||||
width: 40px; height: 40px; border-radius: 10px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 11px; color: #94a3b8; background: transparent;
|
||||
}
|
||||
.dual-rail .ri.on { background: rgba(16,185,129,.25); color: #6ee7b7; }
|
||||
.dual-panel {
|
||||
width: 160px;
|
||||
background: #fff;
|
||||
border-right: 1px solid var(--border);
|
||||
padding: 8px 0;
|
||||
}
|
||||
.icon-rail {
|
||||
width: 64px;
|
||||
background: #fff;
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 8px 0;
|
||||
position: relative;
|
||||
}
|
||||
.icon-rail .ri {
|
||||
width: 40px; height: 40px; border-radius: 10px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
color: #64748b; font-size: 11px;
|
||||
}
|
||||
.icon-rail .ri.on { background: var(--primary-bg); color: var(--primary-dark); }
|
||||
.flyout {
|
||||
position: absolute;
|
||||
left: 64px; top: 56px;
|
||||
width: 180px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0 10px 10px 0;
|
||||
box-shadow: 8px 8px 24px rgba(15,23,42,.08);
|
||||
padding: 8px 0;
|
||||
z-index: 6;
|
||||
}
|
||||
.drawer-mask {
|
||||
position: absolute;
|
||||
inset: 48px 0 0 0;
|
||||
background: rgba(15,23,42,.28);
|
||||
z-index: 4;
|
||||
}
|
||||
.drawer {
|
||||
position: absolute;
|
||||
top: 48px; left: 0; bottom: 0;
|
||||
width: 220px;
|
||||
background: #fff;
|
||||
z-index: 5;
|
||||
box-shadow: 8px 0 24px rgba(15,23,42,.12);
|
||||
overflow: auto;
|
||||
}
|
||||
.cmdk {
|
||||
position: absolute;
|
||||
left: 50%; top: 28%;
|
||||
transform: translateX(-50%);
|
||||
width: min(420px, 86%);
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 48px rgba(15,23,42,.18);
|
||||
z-index: 8;
|
||||
overflow: hidden;
|
||||
}
|
||||
.cmdk input {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 12px 14px;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
}
|
||||
.cmdk .row {
|
||||
padding: 8px 14px;
|
||||
font-size: 12px;
|
||||
color: #334155;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.cmdk .row.on { background: var(--primary-bg); color: var(--primary-dark); }
|
||||
.cmdk .kbd {
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 1px 5px;
|
||||
}
|
||||
.top-tabs {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: #fff;
|
||||
padding: 0 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.top-tabs span {
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
.top-tabs span.on {
|
||||
color: var(--primary-dark);
|
||||
border-bottom-color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
.pills {
|
||||
display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px;
|
||||
}
|
||||
.pill {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
padding: 4px 10px;
|
||||
font-size: 11px;
|
||||
color: #475569;
|
||||
background: #fff;
|
||||
}
|
||||
.pill.on { border-color: #6ee7b7; background: var(--primary-bg); color: var(--primary-dark); }
|
||||
.fav-bar {
|
||||
margin-top: 16px;
|
||||
padding: 12px 14px;
|
||||
border: 1px dashed #a7f3d0;
|
||||
border-radius: 10px;
|
||||
background: #f0fdf4;
|
||||
font-size: 13px;
|
||||
color: #065f46;
|
||||
}
|
||||
.fav-bar strong { font-weight: 700; }
|
||||
.hidden { display: none !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<h1>菜单布局模式对比(省侧栏空间)</h1>
|
||||
<p class="lead">
|
||||
现状门户布局侧栏约 <strong>270px</strong>。下面 8 种模式可切换预览;喜欢的可点「标记为候选」(可多选)。
|
||||
</p>
|
||||
<div class="hint">
|
||||
点击上方标签切换预览 → 右侧为可交互线框 → 左侧看优缺点 → 点「标记为候选」收藏偏好。看完后回终端告诉我你倾向哪些。
|
||||
</div>
|
||||
|
||||
<div class="tabs" id="tabs"></div>
|
||||
|
||||
<div class="layout">
|
||||
<aside class="meta-card" id="meta"></aside>
|
||||
<section class="preview-shell">
|
||||
<div class="preview-bar">
|
||||
<span id="preview-title">预览</span>
|
||||
<span>Fresh Mint · 线框 Demo</span>
|
||||
</div>
|
||||
<div class="preview-frame" id="preview"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="fav-bar" id="fav">尚未标记候选。可多选后告诉我偏好。</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const patterns = [
|
||||
{
|
||||
id: 'a-current',
|
||||
letter: 'A',
|
||||
name: '现状:模块下拉 + 宽侧栏树',
|
||||
width: '270px',
|
||||
desc: '顶栏切换一级模块,左侧展示二/三级菜单树。信息完整,但内容区最窄。',
|
||||
tags: ['现状基线', '信息完整'],
|
||||
pros: ['层级一眼可见', '与现有实现一致', '深层级友好'],
|
||||
cons: ['占宽最大', '列表页横向空间被挤'],
|
||||
render() {
|
||||
return `
|
||||
<div class="app">
|
||||
<div class="top">
|
||||
<div class="brand">山峡</div>
|
||||
<div class="mod-select"><span class="dot"></span> CRM 客户 <span>▾</span></div>
|
||||
<div class="spacer"></div>
|
||||
<div class="top-right"><span>搜索</span><div class="avatar"></div></div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<aside class="side" style="width:270px">
|
||||
<div class="menu-group"><span class="ico">客</span>客户管理</div>
|
||||
<div class="menu-item active"><span class="ico">•</span>客户列表</div>
|
||||
<div class="menu-item child">联系人</div>
|
||||
<div class="menu-item child">跟进记录</div>
|
||||
<div class="menu-group"><span class="ico">商</span>商机合同</div>
|
||||
<div class="menu-item"><span class="ico">•</span>商机</div>
|
||||
<div class="menu-item"><span class="ico">•</span>合同</div>
|
||||
<div class="menu-item"><span class="ico">•</span>报价单</div>
|
||||
</aside>
|
||||
<main class="main"><div class="panel"><h3>客户列表</h3><p>侧栏 270px,内容区最紧。</p><div class="bars"><div class="bar"></div><div class="bar w2"></div><div class="bar w3"></div></div></div></main>
|
||||
</div>
|
||||
<div class="width-tag">侧栏 270px</div>
|
||||
</div>`
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'b-narrow',
|
||||
letter: 'B',
|
||||
name: '窄侧栏文字树',
|
||||
width: '180px',
|
||||
desc: '保留现有门户结构,仅把侧栏从 270 收到 180。改动最小,立刻多出约 90px 内容宽。',
|
||||
tags: ['改动最小', '省 ~90px', '推荐起步'],
|
||||
pros: ['几乎无学习成本', '实现成本最低', '仍保留树形层级'],
|
||||
cons: ['长菜单名易截断', '三级缩进空间更紧'],
|
||||
render() {
|
||||
return `
|
||||
<div class="app">
|
||||
<div class="top">
|
||||
<div class="brand">山峡</div>
|
||||
<div class="mod-select"><span class="dot"></span> CRM 客户 <span>▾</span></div>
|
||||
<div class="spacer"></div>
|
||||
<div class="top-right"><div class="avatar"></div></div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<aside class="side" style="width:180px">
|
||||
<div class="menu-group">客户管理</div>
|
||||
<div class="menu-item active">客户列表</div>
|
||||
<div class="menu-item child">联系人</div>
|
||||
<div class="menu-item child">跟进记录</div>
|
||||
<div class="menu-group">商机合同</div>
|
||||
<div class="menu-item">商机</div>
|
||||
<div class="menu-item">合同</div>
|
||||
</aside>
|
||||
<main class="main"><div class="panel"><h3>客户列表</h3><p>同样门户结构,侧栏更瘦,内容更宽。</p><div class="bars"><div class="bar"></div><div class="bar w2"></div><div class="bar w3"></div></div></div></main>
|
||||
</div>
|
||||
<div class="width-tag">侧栏 180px · 省 ~90px</div>
|
||||
</div>`
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'c-icon-rail',
|
||||
letter: 'C',
|
||||
name: '图标轨道 + 悬停飞出',
|
||||
width: '64px',
|
||||
desc: '默认只留 64px 图标轨;悬停/点击某图标时飞出二级菜单面板。内容区几乎全宽。',
|
||||
tags: ['极省空间', '悬停飞出'],
|
||||
pros: ['内容区最宽之一', '视觉干净', '已有收起态可复用'],
|
||||
cons: ['发现性变弱', '需靠 tooltip/飞出', '深菜单要二次交互'],
|
||||
render() {
|
||||
return `
|
||||
<div class="app">
|
||||
<div class="top">
|
||||
<div class="brand">山峡</div>
|
||||
<div class="mod-select"><span class="dot"></span> CRM <span>▾</span></div>
|
||||
<div class="spacer"></div>
|
||||
<div class="top-right"><div class="avatar"></div></div>
|
||||
</div>
|
||||
<div class="body" style="position:relative">
|
||||
<aside class="icon-rail">
|
||||
<div class="ri on">客</div>
|
||||
<div class="ri">商</div>
|
||||
<div class="ri">合</div>
|
||||
<div class="ri">报</div>
|
||||
<div class="ri">设</div>
|
||||
<div class="flyout">
|
||||
<div class="menu-item active">客户列表</div>
|
||||
<div class="menu-item">联系人</div>
|
||||
<div class="menu-item">跟进记录</div>
|
||||
<div class="menu-item">客户公海</div>
|
||||
</div>
|
||||
</aside>
|
||||
<main class="main"><div class="panel"><h3>客户列表</h3><p>默认只占 64px;子菜单以飞出层展示。</p><div class="bars"><div class="bar"></div><div class="bar w2"></div><div class="bar w3"></div></div></div></main>
|
||||
</div>
|
||||
<div class="width-tag">常驻 64px · 飞出 180px(临时)</div>
|
||||
</div>`
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'd-dual',
|
||||
letter: 'D',
|
||||
name: '双栏菜单(图标列 + 二级面板)',
|
||||
width: '64+160',
|
||||
desc: '左侧深色图标一级,右侧浅色二级列表。总宽约 224px,比现状略省,层级更清晰。对应系统已有 DUAL_MENU。',
|
||||
tags: ['系统已有类型', '层级清晰'],
|
||||
pros: ['一级/二级分离清楚', '可复用现有双栏能力', '适合模块多的门户'],
|
||||
cons: ['仍占约 220px', '三级需要再展开或压进二级'],
|
||||
render() {
|
||||
return `
|
||||
<div class="app">
|
||||
<div class="top">
|
||||
<div class="brand">山峡一体化</div>
|
||||
<div class="spacer"></div>
|
||||
<div class="top-right"><span>管理员</span><div class="avatar"></div></div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<aside class="dual-rail">
|
||||
<div class="ri on">CRM</div>
|
||||
<div class="ri">报销</div>
|
||||
<div class="ri">地磅</div>
|
||||
<div class="ri">计划</div>
|
||||
<div class="ri">系统</div>
|
||||
</aside>
|
||||
<aside class="dual-panel">
|
||||
<div class="menu-group" style="font-size:11px;color:#94a3b8;padding:4px 12px">CRM</div>
|
||||
<div class="menu-item active">客户列表</div>
|
||||
<div class="menu-item">商机</div>
|
||||
<div class="menu-item">合同</div>
|
||||
<div class="menu-item">报价单</div>
|
||||
<div class="menu-item">产品</div>
|
||||
</aside>
|
||||
<main class="main"><div class="panel"><h3>客户列表</h3><p>一级在图标轨,二级在窄面板;顶栏不再放模块下拉。</p><div class="bars"><div class="bar"></div><div class="bar w2"></div></div></div></main>
|
||||
</div>
|
||||
<div class="width-tag">64 + 160 ≈ 224px</div>
|
||||
</div>`
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'e-top-only',
|
||||
letter: 'E',
|
||||
name: '纯顶栏横向菜单',
|
||||
width: '0',
|
||||
desc: '取消左侧栏;一级/二级全部放顶栏。内容区 100% 宽,适合菜单层级浅、条目不多的模块。',
|
||||
tags: ['零侧栏', '内容最宽'],
|
||||
pros: ['内容区最大化', '页面干净', '横向浏览习惯'],
|
||||
cons: ['菜单一多会挤', '三级几乎放不下', '窄屏需折叠'],
|
||||
render() {
|
||||
return `
|
||||
<div class="app">
|
||||
<div class="top">
|
||||
<div class="brand">山峡</div>
|
||||
<nav class="top-nav">
|
||||
<a href="#" class="on">客户</a>
|
||||
<a href="#">商机</a>
|
||||
<a href="#">合同</a>
|
||||
<a href="#">报价</a>
|
||||
<a href="#">产品 ▾</a>
|
||||
<a href="#">更多 ▾</a>
|
||||
</nav>
|
||||
<div class="spacer"></div>
|
||||
<div class="top-right"><div class="avatar"></div></div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<main class="main" style="width:100%"><div class="panel"><h3>客户列表</h3><p>无侧栏,表格可铺满整行。</p><div class="bars"><div class="bar"></div><div class="bar w2"></div><div class="bar w3"></div></div></div></main>
|
||||
</div>
|
||||
<div class="width-tag">侧栏 0 · 内容 100%</div>
|
||||
</div>`
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'f-mega',
|
||||
letter: 'F',
|
||||
name: '顶栏 + Mega Menu',
|
||||
width: '0',
|
||||
desc: '顶栏只放一级;悬停展开大面板展示二/三级分组。兼顾零侧栏与深层级,适合门户多业务线。',
|
||||
tags: ['零侧栏', '深层级友好', '门户感强'],
|
||||
pros: ['内容全宽', '分组信息密度高', '适合多子系统'],
|
||||
cons: ['悬停面板实现成本', '触控设备体验一般', '要控制面板高度'],
|
||||
render() {
|
||||
return `
|
||||
<div class="app" style="position:relative">
|
||||
<div class="top">
|
||||
<div class="brand">山峡</div>
|
||||
<nav class="top-nav">
|
||||
<a href="#" class="on">CRM ▾</a>
|
||||
<a href="#">报销</a>
|
||||
<a href="#">地磅</a>
|
||||
<a href="#">计划</a>
|
||||
<a href="#">系统</a>
|
||||
</nav>
|
||||
<div class="spacer"></div>
|
||||
<div class="top-right"><div class="avatar"></div></div>
|
||||
</div>
|
||||
<div class="mega">
|
||||
<div class="col">
|
||||
<h4>客户</h4>
|
||||
<div class="on">客户列表</div>
|
||||
<div>联系人</div>
|
||||
<div>跟进记录</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h4>交易</h4>
|
||||
<div>商机</div>
|
||||
<div>合同</div>
|
||||
<div>报价单</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<main class="main"><div class="panel"><h3>客户列表</h3><p>顶栏一级 + 大面板二三级,侧栏完全去掉。</p><div class="bars"><div class="bar"></div><div class="bar w2"></div></div></div></main>
|
||||
</div>
|
||||
<div class="width-tag">侧栏 0 · Mega 临时浮层</div>
|
||||
</div>`
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'g-drawer',
|
||||
letter: 'G',
|
||||
name: '抽屉侧栏(默认隐藏)',
|
||||
width: '0→220',
|
||||
desc: '默认无侧栏;点汉堡按钮滑出抽屉菜单。适合偶发切换菜单、长时间盯列表/表格的场景。',
|
||||
tags: ['默认全宽', '按需打开'],
|
||||
pros: ['日常内容全宽', '菜单完整不砍', '实现相对直接'],
|
||||
cons: ['切换菜单多一步', '频繁跳转效率略低'],
|
||||
render() {
|
||||
return `
|
||||
<div class="app" style="position:relative">
|
||||
<div class="top">
|
||||
<button class="hamburger" title="菜单">☰</button>
|
||||
<div class="brand">山峡</div>
|
||||
<div class="mod-select"><span class="dot"></span> CRM <span>▾</span></div>
|
||||
<div class="spacer"></div>
|
||||
<div class="top-right"><div class="avatar"></div></div>
|
||||
</div>
|
||||
<div class="drawer-mask"></div>
|
||||
<aside class="drawer">
|
||||
<div class="menu-group">客户管理</div>
|
||||
<div class="menu-item active">客户列表</div>
|
||||
<div class="menu-item">联系人</div>
|
||||
<div class="menu-group">商机合同</div>
|
||||
<div class="menu-item">商机</div>
|
||||
<div class="menu-item">合同</div>
|
||||
</aside>
|
||||
<div class="body">
|
||||
<main class="main"><div class="panel"><h3>客户列表</h3><p>默认全宽;需要时再拉开抽屉。</p><div class="bars"><div class="bar"></div><div class="bar w2"></div><div class="bar w3"></div></div></div></main>
|
||||
</div>
|
||||
<div class="width-tag">默认 0 · 打开时 220px 遮罩层</div>
|
||||
</div>`
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'h-cmdk',
|
||||
letter: 'H',
|
||||
name: '极简顶栏 + 命令面板',
|
||||
width: '0',
|
||||
desc: '侧栏去掉,顶栏只保留模块与搜索。用 ⌘K / Ctrl+K 打开命令面板跳转页面。高级用户效率最高。',
|
||||
tags: ['极简', '键盘驱动', '进阶'],
|
||||
pros: ['UI 最干净', '跨模块跳转快', '内容全宽'],
|
||||
cons: ['新手发现成本高', '仍需少量可见入口', '不适合完全替代菜单'],
|
||||
render() {
|
||||
return `
|
||||
<div class="app" style="position:relative">
|
||||
<div class="top">
|
||||
<div class="brand">山峡</div>
|
||||
<div class="mod-select"><span class="dot"></span> CRM <span>▾</span></div>
|
||||
<div class="pills" style="margin:0">
|
||||
<span class="pill on">客户</span>
|
||||
<span class="pill">商机</span>
|
||||
<span class="pill">合同</span>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<div class="top-right"><span class="kbd" style="border:1px solid var(--border);border-radius:4px;padding:2px 6px;font-size:10px;color:var(--muted)">⌘K</span><div class="avatar"></div></div>
|
||||
</div>
|
||||
<div class="cmdk">
|
||||
<input value="客户" readonly />
|
||||
<div class="row on"><span>客户列表</span><span class="kbd">↵</span></div>
|
||||
<div class="row"><span>客户公海</span><span class="kbd">↵</span></div>
|
||||
<div class="row"><span>联系人</span><span class="kbd">↵</span></div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<main class="main"><div class="panel"><h3>工作区</h3><p>高频页可用顶栏胶囊;其余靠命令面板直达。</p><div class="bars"><div class="bar"></div><div class="bar w2"></div></div></div></main>
|
||||
</div>
|
||||
<div class="width-tag">侧栏 0 · 命令面板按需</div>
|
||||
</div>`
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'i-module-tabs',
|
||||
letter: 'I',
|
||||
name: '顶栏模块 + 二级横向 Tabs',
|
||||
width: '0',
|
||||
desc: '顶栏下拉选一级模块后,二级以横向 Tabs 展示;三级可进页面内左侧或页内 Tab。适合二级条目较少的模块。',
|
||||
tags: ['零侧栏', '二级横排'],
|
||||
pros: ['内容全宽', '二级切换很直观', '与现有 WorkTab 可协同'],
|
||||
cons: ['二级过多会换行拥挤', '三级仍要另找位置'],
|
||||
render() {
|
||||
return `
|
||||
<div class="app">
|
||||
<div class="top">
|
||||
<div class="brand">山峡</div>
|
||||
<div class="mod-select"><span class="dot"></span> CRM 客户 <span>▾</span></div>
|
||||
<div class="spacer"></div>
|
||||
<div class="top-right"><div class="avatar"></div></div>
|
||||
</div>
|
||||
<div class="top-tabs">
|
||||
<span class="on">客户列表</span>
|
||||
<span>联系人</span>
|
||||
<span>商机</span>
|
||||
<span>合同</span>
|
||||
<span>报价单</span>
|
||||
<span>产品</span>
|
||||
</div>
|
||||
<div class="body">
|
||||
<main class="main"><div class="panel"><h3>客户列表</h3><p>二级在顶栏下方横滑/横排,不再占左侧。</p><div class="bars"><div class="bar"></div><div class="bar w2"></div><div class="bar w3"></div></div></div></main>
|
||||
</div>
|
||||
<div class="width-tag">侧栏 0 · 二级 Tabs ~40px 高</div>
|
||||
</div>`
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const picked = new Set()
|
||||
let current = patterns[1].id // default show B
|
||||
|
||||
const tabsEl = document.getElementById('tabs')
|
||||
const metaEl = document.getElementById('meta')
|
||||
const previewEl = document.getElementById('preview')
|
||||
const previewTitle = document.getElementById('preview-title')
|
||||
const favEl = document.getElementById('fav')
|
||||
|
||||
function renderTabs() {
|
||||
tabsEl.innerHTML = patterns.map(p => `
|
||||
<button type="button" class="tab ${p.id === current ? 'active' : ''}" data-id="${p.id}">
|
||||
${p.letter}. ${p.name.split(':')[0] || p.name}
|
||||
<span class="w">${p.width}</span>
|
||||
</button>
|
||||
`).join('')
|
||||
tabsEl.querySelectorAll('.tab').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
current = btn.getAttribute('data-id')
|
||||
paint()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function paint() {
|
||||
const p = patterns.find(x => x.id === current)
|
||||
renderTabs()
|
||||
previewTitle.textContent = `预览 ${p.letter} · ${p.name}`
|
||||
previewEl.innerHTML = p.render()
|
||||
metaEl.innerHTML = `
|
||||
<h2>${p.letter}. ${p.name}</h2>
|
||||
<p class="desc">${p.desc}</p>
|
||||
<div class="chip-row">
|
||||
${p.tags.map(t => `<span class="chip ${t.includes('推荐') || t.includes('极省') || t.includes('零侧栏') ? 'good' : ''}">${t}</span>`).join('')}
|
||||
<span class="chip warn">占宽 ${p.width}</span>
|
||||
</div>
|
||||
<div class="pros-cons">
|
||||
<div class="pros"><h4>优点</h4><ul>${p.pros.map(i => `<li>${i}</li>`).join('')}</ul></div>
|
||||
<div class="cons"><h4>代价</h4><ul>${p.cons.map(i => `<li>${i}</li>`).join('')}</ul></div>
|
||||
</div>
|
||||
<button type="button" class="select-btn ${picked.has(p.id) ? 'picked' : ''}" id="pick-btn"
|
||||
data-choice="${p.id}" onclick="toggleSelect(this)">
|
||||
${picked.has(p.id) ? '✓ 已标记为候选(再点取消)' : '标记为候选(可多选)'}
|
||||
</button>
|
||||
`
|
||||
document.getElementById('pick-btn').addEventListener('click', () => {
|
||||
if (picked.has(p.id)) picked.delete(p.id)
|
||||
else picked.add(p.id)
|
||||
updateFav()
|
||||
// re-sync button label after companion toggleSelect
|
||||
setTimeout(() => {
|
||||
const btn = document.getElementById('pick-btn')
|
||||
if (!btn) return
|
||||
const on = picked.has(p.id)
|
||||
btn.classList.toggle('picked', on)
|
||||
btn.textContent = on ? '✓ 已标记为候选(再点取消)' : '标记为候选(可多选)'
|
||||
}, 0)
|
||||
})
|
||||
updateFav()
|
||||
}
|
||||
|
||||
function updateFav() {
|
||||
if (!picked.size) {
|
||||
favEl.innerHTML = '尚未标记候选。可多选后告诉我偏好。'
|
||||
return
|
||||
}
|
||||
const names = patterns.filter(p => picked.has(p.id)).map(p => `${p.letter}`)
|
||||
favEl.innerHTML = `已标记候选:<strong>${names.join('、')}</strong>。回终端告诉我最终倾向(可排序)。`
|
||||
}
|
||||
|
||||
// expose for companion helper if present
|
||||
window.__menuPatterns = patterns
|
||||
paint()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,638 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>顶栏菜单模式变体</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary: #10b981;
|
||||
--primary-dark: #059669;
|
||||
--primary-bg: #ecfdf5;
|
||||
--text: #0f172a;
|
||||
--muted: #64748b;
|
||||
--border: #e2e8f0;
|
||||
--bg: #f1f5f9;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Inter, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
color: var(--text);
|
||||
background: #f8fafc;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.page { padding: 20px 24px 40px; max-width: 1280px; margin: 0 auto; }
|
||||
h1 { font-size: 22px; margin: 0 0 6px; }
|
||||
.lead { color: var(--muted); font-size: 14px; margin: 0 0 14px; }
|
||||
.constraint {
|
||||
background: #ecfdf5;
|
||||
border: 1px solid #a7f3d0;
|
||||
color: #065f46;
|
||||
border-radius: 10px;
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.tabs { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
|
||||
.tab {
|
||||
border: 1px solid var(--border);
|
||||
background: #fff;
|
||||
border-radius: 999px;
|
||||
padding: 7px 12px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tab.active {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr;
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
@media (max-width: 960px) { .layout { grid-template-columns: 1fr; } }
|
||||
.meta, .shell {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.meta { padding: 16px; }
|
||||
.meta h2 { margin: 0 0 8px; font-size: 16px; }
|
||||
.meta .desc { font-size: 13px; color: var(--muted); margin: 0 0 12px; }
|
||||
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
|
||||
.chip {
|
||||
font-size: 11px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 999px;
|
||||
background: #f1f5f9;
|
||||
color: #334155;
|
||||
}
|
||||
.chip.good { background: var(--primary-bg); color: #047857; }
|
||||
.pc { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; font-size: 12px; }
|
||||
.pc > div { background: #f8fafc; border-radius: 8px; padding: 10px; }
|
||||
.pc h4 { margin: 0 0 6px; font-size: 12px; }
|
||||
.pc ul { margin: 0; padding-left: 16px; color: var(--muted); }
|
||||
.pick {
|
||||
margin-top: 14px;
|
||||
width: 100%;
|
||||
border: 1px solid var(--primary);
|
||||
background: var(--primary-bg);
|
||||
color: var(--primary-dark);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.pick.on { background: var(--primary); color: #fff; }
|
||||
.shell-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: #fafafa;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.frame { height: 430px; background: var(--bg); position: relative; overflow: hidden; }
|
||||
.fav {
|
||||
margin-top: 16px;
|
||||
padding: 12px 14px;
|
||||
border: 1px dashed #a7f3d0;
|
||||
border-radius: 10px;
|
||||
background: #f0fdf4;
|
||||
font-size: 13px;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
/* mock app */
|
||||
.app { height: 100%; display: flex; flex-direction: column; font-size: 12px; position: relative; }
|
||||
.top {
|
||||
background: #fff;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 12px;
|
||||
min-height: 48px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.top.tall { min-height: 88px; flex-direction: column; align-items: stretch; padding: 0; gap: 0; }
|
||||
.top-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 12px;
|
||||
height: 44px;
|
||||
}
|
||||
.top-row.sub {
|
||||
height: 40px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
background: #fafafa;
|
||||
}
|
||||
.brand { font-weight: 700; color: var(--primary-dark); white-space: nowrap; }
|
||||
.spacer { flex: 1; }
|
||||
.avatar {
|
||||
width: 24px; height: 24px; border-radius: 50%;
|
||||
background: linear-gradient(135deg, #6ee7b7, #34d399);
|
||||
}
|
||||
.nav { display: flex; align-items: center; gap: 2px; flex-wrap: nowrap; min-width: 0; }
|
||||
.nav a, .nav .item {
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
color: #334155;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
cursor: default;
|
||||
}
|
||||
.nav a.on, .nav .item.on {
|
||||
background: var(--primary-bg);
|
||||
color: var(--primary-dark);
|
||||
font-weight: 600;
|
||||
}
|
||||
.nav .more { color: var(--muted); }
|
||||
.main { flex: 1; padding: 14px; min-height: 0; overflow: auto; }
|
||||
.panel {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
height: 100%;
|
||||
padding: 14px;
|
||||
}
|
||||
.panel h3 { margin: 0 0 8px; font-size: 14px; }
|
||||
.panel p { margin: 0; color: var(--muted); }
|
||||
.bars { margin-top: 14px; display: grid; gap: 8px; }
|
||||
.bar { height: 10px; border-radius: 6px; background: linear-gradient(90deg, #d1fae5, #f1f5f9); }
|
||||
.bar.w2 { width: 70%; }
|
||||
.bar.w3 { width: 48%; }
|
||||
.tag {
|
||||
position: absolute; left: 8px; bottom: 8px; z-index: 6;
|
||||
background: rgba(15,23,42,.78); color: #fff;
|
||||
font-size: 11px; padding: 4px 8px; border-radius: 6px;
|
||||
}
|
||||
.dd {
|
||||
position: absolute;
|
||||
top: 44px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 12px 28px rgba(15,23,42,.12);
|
||||
min-width: 160px;
|
||||
padding: 6px;
|
||||
z-index: 8;
|
||||
}
|
||||
.dd div {
|
||||
padding: 7px 10px;
|
||||
border-radius: 6px;
|
||||
color: #334155;
|
||||
}
|
||||
.dd div.on { background: var(--primary-bg); color: var(--primary-dark); }
|
||||
.mega {
|
||||
position: absolute;
|
||||
top: 48px; left: 96px;
|
||||
width: 520px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 16px 36px rgba(15,23,42,.14);
|
||||
padding: 14px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 10px;
|
||||
z-index: 8;
|
||||
}
|
||||
.mega h4 { margin: 0 0 8px; font-size: 11px; color: var(--muted); letter-spacing: .04em; }
|
||||
.mega .link {
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
color: #334155;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.mega .link.on { background: var(--primary-bg); color: var(--primary-dark); }
|
||||
.switcher {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 5px 10px;
|
||||
background: #fff;
|
||||
}
|
||||
.switcher .dot {
|
||||
width: 8px; height: 8px; border-radius: 50%; background: var(--primary);
|
||||
}
|
||||
.casc {
|
||||
position: absolute;
|
||||
top: 44px; left: 280px;
|
||||
display: flex;
|
||||
z-index: 8;
|
||||
}
|
||||
.casc .col {
|
||||
width: 150px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 12px 28px rgba(15,23,42,.12);
|
||||
padding: 6px;
|
||||
}
|
||||
.casc .col + .col { margin-left: -4px; }
|
||||
.casc .col div {
|
||||
padding: 7px 10px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: #334155;
|
||||
}
|
||||
.casc .col div.on { background: var(--primary-bg); color: var(--primary-dark); }
|
||||
.search {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 5px 10px;
|
||||
color: var(--muted);
|
||||
background: #f8fafc;
|
||||
min-width: 140px;
|
||||
}
|
||||
.kbd {
|
||||
font-size: 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 1px 5px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.cmdk {
|
||||
position: absolute;
|
||||
left: 50%; top: 22%;
|
||||
transform: translateX(-50%);
|
||||
width: min(440px, 88%);
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 48px rgba(15,23,42,.18);
|
||||
z-index: 9;
|
||||
overflow: hidden;
|
||||
}
|
||||
.cmdk input {
|
||||
width: 100%; border: 0; border-bottom: 1px solid var(--border);
|
||||
padding: 12px 14px; font-size: 13px; outline: none;
|
||||
}
|
||||
.cmdk .row {
|
||||
padding: 8px 14px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: #334155;
|
||||
}
|
||||
.cmdk .row.on { background: var(--primary-bg); color: var(--primary-dark); }
|
||||
.pill {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
padding: 4px 10px;
|
||||
font-size: 11px;
|
||||
color: #475569;
|
||||
background: #fff;
|
||||
}
|
||||
.pill.on { border-color: #6ee7b7; background: var(--primary-bg); color: var(--primary-dark); font-weight: 600; }
|
||||
.bread { color: var(--muted); font-size: 11px; }
|
||||
.bread b { color: var(--text); font-weight: 600; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<h1>客户约束:菜单全部放在顶部</h1>
|
||||
<p class="lead">左侧栏取消。下面是「纯顶栏」的 7 种常见做法(系统已有 <code>MenuTypeEnum.TOP</code> 可作基础)。</p>
|
||||
<div class="constraint">
|
||||
硬约束:<strong>无左侧菜单树</strong>。差异主要在:一级怎么摆、二/三级用下拉 / 大面板 / 第二行 Tabs / 命令面板 承接。
|
||||
</div>
|
||||
|
||||
<div class="tabs" id="tabs"></div>
|
||||
<div class="layout">
|
||||
<aside class="meta" id="meta"></aside>
|
||||
<section class="shell">
|
||||
<div class="shell-bar">
|
||||
<span id="title">预览</span>
|
||||
<span>零侧栏 · Fresh Mint</span>
|
||||
</div>
|
||||
<div class="frame" id="preview"></div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="fav" id="fav">尚未标记。可多选后回终端说偏好排序。</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const patterns = [
|
||||
{
|
||||
id: 't1-dropdown',
|
||||
letter: 'T1',
|
||||
name: '经典顶栏 + 下拉二级',
|
||||
rec: true,
|
||||
desc: '顶栏横排一级(或模块);悬停/点击下拉出二级。最常见的「菜单都在顶部」形态,也最接近现有 TOP 菜单类型。',
|
||||
tags: ['推荐起步', '实现成本低', '对应 TOP'],
|
||||
pros: ['用户熟悉', '无侧栏占宽', '改造路径清晰'],
|
||||
cons: ['三级要再嵌套', '一级过多会挤,需「更多」'],
|
||||
render: () => `
|
||||
<div class="app">
|
||||
<div class="top">
|
||||
<div class="brand">山峡</div>
|
||||
<nav class="nav">
|
||||
<a class="on">客户 ▾</a>
|
||||
<a>商机</a>
|
||||
<a>合同</a>
|
||||
<a>报价</a>
|
||||
<a>产品 ▾</a>
|
||||
<a class="more">更多 ▾</a>
|
||||
</nav>
|
||||
<div class="spacer"></div>
|
||||
<div class="avatar"></div>
|
||||
</div>
|
||||
<div class="dd" style="left:72px">
|
||||
<div class="on">客户列表</div>
|
||||
<div>联系人</div>
|
||||
<div>跟进记录</div>
|
||||
<div>客户公海</div>
|
||||
</div>
|
||||
<div class="main"><div class="panel"><h3>客户列表</h3><p>一级横排,二级下拉。内容区全宽。</p><div class="bars"><div class="bar"></div><div class="bar w2"></div><div class="bar w3"></div></div></div></div>
|
||||
<div class="tag">单行顶栏 · 下拉二级</div>
|
||||
</div>`
|
||||
},
|
||||
{
|
||||
id: 't2-mega',
|
||||
letter: 'T2',
|
||||
name: '顶栏一级 + Mega 大面板',
|
||||
desc: '顶栏只放业务线/一级模块;悬停展开多列大面板,把二、三级分组展示。适合 CRM/报销/地磅等多子系统门户。',
|
||||
tags: ['深层级友好', '门户感强'],
|
||||
pros: ['信息密度高', '分组清晰', '适合模块多'],
|
||||
cons: ['面板实现与响应式成本', '触控设备不如点击稳'],
|
||||
render: () => `
|
||||
<div class="app">
|
||||
<div class="top">
|
||||
<div class="brand">山峡</div>
|
||||
<nav class="nav">
|
||||
<a class="on">CRM ▾</a>
|
||||
<a>报销</a>
|
||||
<a>地磅</a>
|
||||
<a>计划物流</a>
|
||||
<a>工作流</a>
|
||||
<a>系统</a>
|
||||
</nav>
|
||||
<div class="spacer"></div>
|
||||
<div class="avatar"></div>
|
||||
</div>
|
||||
<div class="mega">
|
||||
<div>
|
||||
<h4>客户</h4>
|
||||
<div class="link on">客户列表</div>
|
||||
<div class="link">联系人</div>
|
||||
<div class="link">跟进记录</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4>交易</h4>
|
||||
<div class="link">商机</div>
|
||||
<div class="link">合同</div>
|
||||
<div class="link">报价单</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4>主数据</h4>
|
||||
<div class="link">产品</div>
|
||||
<div class="link">分类</div>
|
||||
<div class="link">单位</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main"><div class="panel"><h3>客户列表</h3><p>一级在顶,二三級进 Mega。无侧栏。</p><div class="bars"><div class="bar"></div><div class="bar w2"></div></div></div></div>
|
||||
<div class="tag">顶栏模块 · Mega 承接深层级</div>
|
||||
</div>`
|
||||
},
|
||||
{
|
||||
id: 't3-two-row',
|
||||
letter: 'T3',
|
||||
name: '双行顶栏(模块行 + 二级 Tabs)',
|
||||
rec: true,
|
||||
desc: '第一行切换一级模块(CRM/报销…);第二行展示该模块下的二级菜单 Tabs。三级可进页面内 Tab 或面包屑下钻。',
|
||||
tags: ['推荐', '与现状模块感接近', '二级可见'],
|
||||
pros: ['二级始终可见,少悬停', '保留门户分模块心智', '内容仍全宽'],
|
||||
cons: ['顶栏变高(约 +40px)', '二级过多需横滑/更多'],
|
||||
render: () => `
|
||||
<div class="app">
|
||||
<div class="top tall">
|
||||
<div class="top-row">
|
||||
<div class="brand">山峡</div>
|
||||
<nav class="nav">
|
||||
<a class="on">CRM</a>
|
||||
<a>报销</a>
|
||||
<a>地磅</a>
|
||||
<a>计划</a>
|
||||
<a>系统</a>
|
||||
</nav>
|
||||
<div class="spacer"></div>
|
||||
<div class="avatar"></div>
|
||||
</div>
|
||||
<div class="top-row sub">
|
||||
<nav class="nav">
|
||||
<a class="on">客户列表</a>
|
||||
<a>联系人</a>
|
||||
<a>商机</a>
|
||||
<a>合同</a>
|
||||
<a>报价单</a>
|
||||
<a>产品</a>
|
||||
<a class="more">更多 ▾</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main"><div class="panel"><h3>客户列表</h3><p>菜单全在顶部两行;左侧完全空出。</p><div class="bars"><div class="bar"></div><div class="bar w2"></div><div class="bar w3"></div></div></div></div>
|
||||
<div class="tag">双行顶栏 · 模块 + 二级 Tabs</div>
|
||||
</div>`
|
||||
},
|
||||
{
|
||||
id: 't4-switcher',
|
||||
letter: 'T4',
|
||||
name: '应用切换器 + 模块内横排',
|
||||
desc: '左上角「应用/模块切换器」选 CRM;右侧横排该模块菜单。类似 Notion/飞书工作区切换,适合多系统一体化。',
|
||||
tags: ['多系统门户', '接近现状下拉'],
|
||||
pros: ['一级不占满顶栏', '模块内菜单空间更大', '与现门户心智接近'],
|
||||
cons: ['切换模块需点开切换器', '模块内菜单仍可能挤'],
|
||||
render: () => `
|
||||
<div class="app">
|
||||
<div class="top">
|
||||
<div class="brand">山峡</div>
|
||||
<div class="switcher"><span class="dot"></span> CRM 客户 <span>▾</span></div>
|
||||
<nav class="nav">
|
||||
<a class="on">客户列表</a>
|
||||
<a>联系人</a>
|
||||
<a>商机</a>
|
||||
<a>合同</a>
|
||||
<a>报价</a>
|
||||
<a class="more">更多 ▾</a>
|
||||
</nav>
|
||||
<div class="spacer"></div>
|
||||
<div class="avatar"></div>
|
||||
</div>
|
||||
<div class="main"><div class="panel"><h3>客户列表</h3><p>一级收进切换器,顶栏主区域给二级菜单。</p><div class="bars"><div class="bar"></div><div class="bar w2"></div></div></div></div>
|
||||
<div class="tag">模块切换器 + 二级横排</div>
|
||||
</div>`
|
||||
},
|
||||
{
|
||||
id: 't5-cascade',
|
||||
letter: 'T5',
|
||||
name: '顶栏 + 级联多级菜单',
|
||||
desc: '一级横排;二级、三级用向右展开的级联面板。适合树深、条目多,但不想用 Mega 大面板时。',
|
||||
tags: ['三层级', '传统后台'],
|
||||
pros: ['深层级表达完整', '比 Mega 更省屏', '路径感强'],
|
||||
cons: ['鼠标路径要求高', '移动端难用', '发现性一般'],
|
||||
render: () => `
|
||||
<div class="app">
|
||||
<div class="top">
|
||||
<div class="brand">山峡</div>
|
||||
<nav class="nav">
|
||||
<a>CRM</a>
|
||||
<a class="on">客户 ▾</a>
|
||||
<a>商机</a>
|
||||
<a>系统</a>
|
||||
</nav>
|
||||
<div class="spacer"></div>
|
||||
<div class="avatar"></div>
|
||||
</div>
|
||||
<div class="casc">
|
||||
<div class="col">
|
||||
<div class="on">客户管理 <span>›</span></div>
|
||||
<div>公海池</div>
|
||||
<div>标签管理</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="on">客户列表</div>
|
||||
<div>联系人</div>
|
||||
<div>跟进记录</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main"><div class="panel"><h3>客户列表</h3><p>级联展开二、三级,仍不占左侧。</p><div class="bars"><div class="bar"></div><div class="bar w2"></div></div></div></div>
|
||||
<div class="tag">横排一级 · 级联二三级</div>
|
||||
</div>`
|
||||
},
|
||||
{
|
||||
id: 't6-overflow',
|
||||
letter: 'T6',
|
||||
name: '顶栏 +「更多」溢出折叠',
|
||||
desc: '把高频菜单钉在顶栏,其余收进「更多」。窄屏/菜单多时仍保持单行顶栏不换行。',
|
||||
tags: ['响应式', '高频优先'],
|
||||
pros: ['顶栏永远不挤爆', '可按权限/使用频率排序', '实现可渐进'],
|
||||
cons: ['低频入口发现成本', '「更多」里仍要组织层级'],
|
||||
render: () => `
|
||||
<div class="app">
|
||||
<div class="top">
|
||||
<div class="brand">山峡</div>
|
||||
<nav class="nav">
|
||||
<a class="on">客户</a>
|
||||
<a>商机</a>
|
||||
<a>合同</a>
|
||||
<a class="more">更多 ▾</a>
|
||||
</nav>
|
||||
<div class="spacer"></div>
|
||||
<span class="search">搜索菜单… <span class="kbd">⌘K</span></span>
|
||||
<div class="avatar"></div>
|
||||
</div>
|
||||
<div class="dd" style="left:220px">
|
||||
<div>报价单</div>
|
||||
<div>产品</div>
|
||||
<div>分类</div>
|
||||
<div>单位</div>
|
||||
<div>数据字典</div>
|
||||
</div>
|
||||
<div class="main"><div class="panel"><h3>客户列表</h3><p>可见入口少而稳;其余进「更多」或搜索。</p><div class="bars"><div class="bar"></div><div class="bar w2"></div><div class="bar w3"></div></div></div></div>
|
||||
<div class="tag">钉住高频 · 溢出进更多</div>
|
||||
</div>`
|
||||
},
|
||||
{
|
||||
id: 't7-cmd-hybrid',
|
||||
letter: 'T7',
|
||||
name: '精简顶栏 + 命令面板补全',
|
||||
desc: '顶栏只保留 4~6 个高频入口;完整菜单树靠 ⌘K 命令面板检索跳转。适合重度用户、菜单极多的系统。',
|
||||
tags: ['极简', '键盘效率', '进阶'],
|
||||
pros: ['顶栏最干净', '跨模块直达快', '内容区最大化'],
|
||||
cons: ['新手依赖引导', '不能完全取消可见菜单'],
|
||||
render: () => `
|
||||
<div class="app">
|
||||
<div class="top">
|
||||
<div class="brand">山峡</div>
|
||||
<div class="switcher"><span class="dot"></span> CRM <span>▾</span></div>
|
||||
<span class="pill on">客户</span>
|
||||
<span class="pill">商机</span>
|
||||
<span class="pill">合同</span>
|
||||
<div class="spacer"></div>
|
||||
<span class="kbd">⌘K 跳转</span>
|
||||
<div class="avatar"></div>
|
||||
</div>
|
||||
<div class="cmdk">
|
||||
<input value="合同" readonly />
|
||||
<div class="row on"><span>合同列表</span><span class="kbd">↵</span></div>
|
||||
<div class="row"><span>新建合同</span><span class="kbd">↵</span></div>
|
||||
<div class="row"><span>合同归档</span><span class="kbd">↵</span></div>
|
||||
</div>
|
||||
<div class="main"><div class="panel"><h3>工作区</h3><p>可见菜单很少;完整导航靠搜索命令面板。</p><div class="bars"><div class="bar"></div><div class="bar w2"></div></div></div></div>
|
||||
<div class="tag">可见入口极少 · 命令面板补全</div>
|
||||
</div>`
|
||||
}
|
||||
]
|
||||
|
||||
const picked = new Set()
|
||||
let current = 't3-two-row'
|
||||
|
||||
const tabsEl = document.getElementById('tabs')
|
||||
const metaEl = document.getElementById('meta')
|
||||
const previewEl = document.getElementById('preview')
|
||||
const titleEl = document.getElementById('title')
|
||||
const favEl = document.getElementById('fav')
|
||||
|
||||
function paint() {
|
||||
const p = patterns.find(x => x.id === current)
|
||||
tabsEl.innerHTML = patterns.map(x =>
|
||||
`<button type="button" class="tab ${x.id === current ? 'active' : ''}" data-id="${x.id}">${x.letter} ${x.name.split('(')[0]}</button>`
|
||||
).join('')
|
||||
tabsEl.querySelectorAll('.tab').forEach(btn => {
|
||||
btn.onclick = () => { current = btn.dataset.id; paint() }
|
||||
})
|
||||
|
||||
titleEl.textContent = `预览 ${p.letter} · ${p.name}`
|
||||
previewEl.innerHTML = p.render()
|
||||
metaEl.innerHTML = `
|
||||
<h2>${p.letter}. ${p.name}</h2>
|
||||
<p class="desc">${p.desc}</p>
|
||||
<div class="chips">
|
||||
${p.tags.map(t => `<span class="chip ${p.rec || t.includes('推荐') ? 'good' : ''}">${t}</span>`).join('')}
|
||||
</div>
|
||||
<div class="pc">
|
||||
<div><h4>优点</h4><ul>${p.pros.map(i => `<li>${i}</li>`).join('')}</ul></div>
|
||||
<div><h4>代价</h4><ul>${p.cons.map(i => `<li>${i}</li>`).join('')}</ul></div>
|
||||
</div>
|
||||
<button type="button" class="pick ${picked.has(p.id) ? 'on' : ''}" id="pick"
|
||||
data-choice="${p.id}" onclick="typeof toggleSelect==='function' && toggleSelect(this)">
|
||||
${picked.has(p.id) ? '✓ 已标记候选' : '标记为候选(可多选)'}
|
||||
</button>
|
||||
`
|
||||
document.getElementById('pick').addEventListener('click', () => {
|
||||
if (picked.has(p.id)) picked.delete(p.id)
|
||||
else picked.add(p.id)
|
||||
updateFav()
|
||||
paint()
|
||||
})
|
||||
updateFav()
|
||||
}
|
||||
|
||||
function updateFav() {
|
||||
if (!picked.size) {
|
||||
favEl.textContent = '尚未标记。可多选后回终端说偏好排序。'
|
||||
return
|
||||
}
|
||||
const names = patterns.filter(p => picked.has(p.id)).map(p => p.letter)
|
||||
favEl.innerHTML = `已标记:<strong>${names.join('、')}</strong>。请在终端回复偏好(如 T3 > T1 > T4)。`
|
||||
}
|
||||
|
||||
paint()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,4 @@
|
||||
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh;flex-direction:column;gap:8px">
|
||||
<p class="subtitle">方案 A 已落到 web-admin 代码,请刷新管理端查看。</p>
|
||||
<p class="subtitle">继续在终端反馈即可。</p>
|
||||
</div>
|
||||
@@ -0,0 +1,2 @@
|
||||
{"type":"click","text":"还要改一处","choice":"design-revise","id":null,"timestamp":1785549892937}
|
||||
{"type":"click","text":"改选 B 5列网格","choice":"prefer-grid5","id":null,"timestamp":1785549893699}
|
||||
@@ -0,0 +1 @@
|
||||
{"reason":"owner process exited","timestamp":1785549952006}
|
||||
@@ -0,0 +1 @@
|
||||
69551
|
||||
@@ -0,0 +1,32 @@
|
||||
<div style="display:flex;flex-direction:column;gap:16px;padding:8px 4px 24px">
|
||||
<h2>实现路径对比(请选一条)</h2>
|
||||
<p class="subtitle">权威模型已定:现场 / 司机各自存;归档弹窗独立填最终品牌量并入账。</p>
|
||||
|
||||
<div class="options">
|
||||
<div class="option" data-choice="approach-1" onclick="toggleSelect(this)">
|
||||
<div class="letter">1</div>
|
||||
<div class="content">
|
||||
<h3>推荐:独立明细子表</h3>
|
||||
<p>现场:入/出库明细品牌量表;司机:按计划产品行的回传品牌量表;归档继续用现有 <code>pl_plan_product_brand_qty</code>。三套互不覆盖,展示与审计清晰。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option" data-choice="approach-2" onclick="toggleSelect(this)">
|
||||
<div class="letter">2</div>
|
||||
<div class="content">
|
||||
<h3>JSON 落字段</h3>
|
||||
<p>在入/出库明细、运输单上存 <code>brandQtys</code> JSON。改动少、上线快,但查询/校验/列表展开较弱,后续报表难用。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option" data-choice="approach-3" onclick="toggleSelect(this)">
|
||||
<div class="letter">3</div>
|
||||
<div class="content">
|
||||
<h3>复用归档品牌量表 + source</h3>
|
||||
<p>同一张 <code>pl_plan_product_brand_qty</code> 加来源(site/driver/archive)。表少,但易和归档最终量搅在一起,覆盖风险高。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="note" style="margin-top:8px;padding:12px;background:#fff;border-radius:10px;border:1px solid #e2e8f0;font-size:13px;color:#64748b;line-height:1.55">
|
||||
交互统一为芯片点选填量;无品牌产品只填总量;查看 Tab / 补充弹窗 / 标题栏按钮布局按你已确认的要求改。
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,31 @@
|
||||
<div style="padding:8px 4px 24px">
|
||||
<h2>设计 §1 · 数据与权威模型</h2>
|
||||
<p class="subtitle">独立明细子表;归档最终量单独落库入账</p>
|
||||
|
||||
<div class="section">
|
||||
<h3>三套数量互不覆盖</h3>
|
||||
<ul>
|
||||
<li><strong>现场</strong>:入/出库明细 <code>actual_qty</code> + 新子表品牌量(按 order_item)</li>
|
||||
<li><strong>司机</strong>:按计划产品行存回传总量 + 新子表品牌量;保留旧 <code>feedback_qty</code> 作全单合计兼容</li>
|
||||
<li><strong>归档</strong>:现有 <code>pl_plan_product.actual_qty</code> + <code>pl_plan_product_brand_qty</code>;仅此套触发库存入账</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>新表(示意)</h3>
|
||||
<p><code>pl_io_order_item_brand_qty</code>(inbound/outbound 共用结构或分表二选一,实现时定)</p>
|
||||
<p><code>pl_transport_product_feedback</code> + <code>pl_transport_product_brand_qty</code></p>
|
||||
<p>公共字段遵循工程规范;软删。</p>
|
||||
</div>
|
||||
|
||||
<div class="options">
|
||||
<div class="option" data-choice="s1-ok" onclick="toggleSelect(this)">
|
||||
<div class="letter">✓</div>
|
||||
<div class="content"><h3>本节没问题</h3><p>继续 §2 录入与校验</p></div>
|
||||
</div>
|
||||
<div class="option" data-choice="s1-change" onclick="toggleSelect(this)">
|
||||
<div class="letter">✎</div>
|
||||
<div class="content"><h3>需要修改</h3><p>在终端说明要改哪里</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,43 @@
|
||||
<div style="padding:8px 4px 24px">
|
||||
<h2>设计 §2 · 录入交互与校验</h2>
|
||||
<p class="subtitle">芯片点选填量;无品牌只填总量</p>
|
||||
|
||||
<div class="section">
|
||||
<h3>何时出现品牌区</h3>
|
||||
<ul>
|
||||
<li>计划产品行有 <code>brandMode</code>(any / required)→ 展示品牌芯片 + 数量</li>
|
||||
<li>无品牌概念 → 只填实际/回传总量,不展示品牌区</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>交互(方案 B)</h3>
|
||||
<ul>
|
||||
<li>展示产品可选品牌为芯片;点选启用,再填数量;可多选</li>
|
||||
<li><code>required</code>:仅预选指定品牌,不可改选其他;数量可改</li>
|
||||
<li>已选品牌数量合计自动回写该产品实际数量(只读展示合计)</li>
|
||||
<li>未选中的品牌不落库;数量 ≤ 0 的不落库</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>校验</h3>
|
||||
<ul>
|
||||
<li>有品牌:至少选 1 个品牌且合计 > 0(提交完成态时)</li>
|
||||
<li>各品牌合计 = 该产品实际数量</li>
|
||||
<li>无品牌:只校验总量 ≥ 0(规则与现网一致)</li>
|
||||
<li>归档弹窗沿用现有品牌量校验,与现场/司机互不预填强制覆盖(可选手动参考展示,默认不自动覆盖最终量)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="options">
|
||||
<div class="option" data-choice="s2-ok" onclick="toggleSelect(this)">
|
||||
<div class="letter">✓</div>
|
||||
<div class="content"><h3>本节没问题</h3><p>继续 §3 界面改动范围</p></div>
|
||||
</div>
|
||||
<div class="option" data-choice="s2-change" onclick="toggleSelect(this)">
|
||||
<div class="letter">✎</div>
|
||||
<div class="content"><h3>需要修改</h3><p>在终端说明</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,48 @@
|
||||
<div style="padding:8px 4px 24px">
|
||||
<h2>设计 §3 · 界面改动范围</h2>
|
||||
<p class="subtitle">公开页 / 补充弹窗 / 运输信息 Tab / 标题栏按钮</p>
|
||||
|
||||
<div class="section">
|
||||
<h3>公开页</h3>
|
||||
<ul>
|
||||
<li>现场:<code>PublicIoOrderPage</code> 产品卡增加芯片填量</li>
|
||||
<li>司机:<code>public/pl/transport</code> 改为按产品填量 + 有品牌则芯片区</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>管理端补充弹窗</h3>
|
||||
<ul>
|
||||
<li>司机补充 / 现场(入出库)补充:与公开页同一套录入规则</li>
|
||||
<li>归档弹窗「产品实际数量」区:保持现有按品牌填量(可改为芯片交互以统一,或保留表格;默认统一为芯片)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>查看 · 运输信息 Tab(计划 / 归档 / 危化品证共用 <code>PlPlanViewTabs</code>)</h3>
|
||||
<ul>
|
||||
<li>司机回传:按产品展示回传量 + 品牌摘要(如「君正 16 / 红四方 12」)</li>
|
||||
<li>入/出库回传明细表:增加品牌数量列或摘要列</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>标题栏按钮(归档嵌入且 <code>feedbackEditable</code> 时)</h3>
|
||||
<ul>
|
||||
<li>入/出库回传标题栏:<strong>补充(高亮 primary)</strong> → 复制现场链接</li>
|
||||
<li>司机回传标题栏:补充(高亮)→ <strong>复制司机链接</strong></li>
|
||||
<li>补充按钮从正文底部/操作列挪到标题栏右侧</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="options">
|
||||
<div class="option" data-choice="s3-ok" onclick="toggleSelect(this)">
|
||||
<div class="letter">✓</div>
|
||||
<div class="content"><h3>本节没问题</h3><p>继续写完整设计文档</p></div>
|
||||
</div>
|
||||
<div class="option" data-choice="s3-change" onclick="toggleSelect(this)">
|
||||
<div class="letter">✎</div>
|
||||
<div class="content"><h3>需要修改</h3><p>在终端说明</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,764 @@
|
||||
<!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 {
|
||||
--mint: #059669;
|
||||
--mint-soft: #ecfdf5;
|
||||
--mint-line: #a7f3d0;
|
||||
--slate: #64748b;
|
||||
--ink: #0f172a;
|
||||
--warn: #d97706;
|
||||
--danger: #dc2626;
|
||||
--bg: #f1f5f9;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
background: linear-gradient(165deg, #ecfdf5 0%, #f8fafc 42%, #e2e8f0 100%);
|
||||
color: var(--ink);
|
||||
min-height: 100vh;
|
||||
}
|
||||
.page {
|
||||
max-width: 1180px;
|
||||
margin: 0 auto;
|
||||
padding: 28px 20px 48px;
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.lead {
|
||||
margin: 0 0 20px;
|
||||
color: var(--slate);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.layout-picker {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.layout-opt {
|
||||
border: 2px solid #e2e8f0;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 14px 14px 12px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
}
|
||||
.layout-opt:hover { border-color: var(--mint-line); }
|
||||
.layout-opt.active {
|
||||
border-color: var(--mint);
|
||||
box-shadow: 0 0 0 3px rgb(16 185 129 / 18%);
|
||||
}
|
||||
.layout-opt .tag {
|
||||
display: inline-block;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--mint);
|
||||
background: var(--mint-soft);
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.layout-opt h3 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 15px;
|
||||
}
|
||||
.layout-opt p {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--slate);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.phones {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 28px;
|
||||
align-items: start;
|
||||
}
|
||||
.phone-wrap h2 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 15px;
|
||||
color: var(--slate);
|
||||
font-weight: 600;
|
||||
}
|
||||
.phone {
|
||||
width: 100%;
|
||||
max-width: 390px;
|
||||
margin: 0 auto;
|
||||
background: #0f172a;
|
||||
border-radius: 36px;
|
||||
padding: 12px;
|
||||
box-shadow: 0 24px 48px rgb(15 23 42 / 18%);
|
||||
}
|
||||
.phone-screen {
|
||||
background: var(--bg);
|
||||
border-radius: 28px;
|
||||
overflow: hidden;
|
||||
min-height: 640px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.nav {
|
||||
background: var(--mint);
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
padding: 14px 16px 16px;
|
||||
border-radius: 0 0 14px 14px;
|
||||
}
|
||||
.scroll {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 12px 0 80px;
|
||||
}
|
||||
.card {
|
||||
margin: 0 12px 10px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
.card-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #047857;
|
||||
background: linear-gradient(90deg, #ecfdf5 0%, transparent 72%);
|
||||
}
|
||||
.card-title::before {
|
||||
content: "";
|
||||
width: 3px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
background: var(--mint);
|
||||
}
|
||||
.cell {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 10px 14px;
|
||||
font-size: 14px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
}
|
||||
.cell:first-child { border-top: 0; }
|
||||
.cell .k { color: var(--slate); }
|
||||
.cell .v { font-weight: 600; text-align: right; }
|
||||
.product {
|
||||
margin: 10px 12px;
|
||||
padding: 14px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(180deg, #f0fdf4 0%, #fff 48%);
|
||||
border: 1px solid #d1fae5;
|
||||
}
|
||||
.product-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.product-name { font-size: 15px; font-weight: 700; }
|
||||
.badge {
|
||||
flex-shrink: 0;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: #d1fae5;
|
||||
color: #047857;
|
||||
height: fit-content;
|
||||
}
|
||||
.badge.warn { background: #fef3c7; color: #b45309; }
|
||||
.meta {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.meta-item {
|
||||
background: rgb(255 255 255 / 85%);
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
.meta-item.span { grid-column: 1 / -1; }
|
||||
.meta-label { font-size: 11px; color: var(--slate); margin-bottom: 2px; }
|
||||
.meta-value { font-size: 13px; font-weight: 600; }
|
||||
.meta-value.plan { color: var(--mint); font-size: 16px; }
|
||||
.actual-box {
|
||||
background: #fff;
|
||||
border: 1px solid var(--mint-line);
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
.actual-box__label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.actual-box__label strong { font-size: 13px; }
|
||||
.actual-box__label span { font-size: 11px; color: var(--slate); }
|
||||
.actual-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.actual-row input {
|
||||
flex: 1;
|
||||
border: 0;
|
||||
outline: none;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: var(--ink);
|
||||
width: 0;
|
||||
background: transparent;
|
||||
}
|
||||
.unit { color: var(--slate); font-size: 13px; font-weight: 600; }
|
||||
.brand-block {
|
||||
margin-top: 10px;
|
||||
border-top: 1px dashed #bbf7d0;
|
||||
padding-top: 10px;
|
||||
}
|
||||
.brand-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.brand-head h4 {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #047857;
|
||||
}
|
||||
.brand-sum {
|
||||
font-size: 11px;
|
||||
color: var(--slate);
|
||||
}
|
||||
.brand-sum.bad { color: var(--danger); font-weight: 600; }
|
||||
.brand-sum.ok { color: var(--mint); font-weight: 600; }
|
||||
.brand-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 88px 28px;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.brand-row select,
|
||||
.brand-row input {
|
||||
height: 36px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 8px;
|
||||
padding: 0 8px;
|
||||
font-size: 13px;
|
||||
background: #fff;
|
||||
width: 100%;
|
||||
}
|
||||
.brand-row input { text-align: right; font-weight: 600; }
|
||||
.icon-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: #fee2e2;
|
||||
color: var(--danger);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
}
|
||||
.add-brand {
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
height: 34px;
|
||||
border: 1px dashed var(--mint);
|
||||
border-radius: 8px;
|
||||
background: var(--mint-soft);
|
||||
color: var(--mint);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.chip {
|
||||
border: 1px solid #cbd5e1;
|
||||
background: #fff;
|
||||
border-radius: 999px;
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.chip.on {
|
||||
border-color: var(--mint);
|
||||
background: var(--mint-soft);
|
||||
color: #047857;
|
||||
font-weight: 600;
|
||||
}
|
||||
.chip .q {
|
||||
margin-left: 4px;
|
||||
color: var(--slate);
|
||||
font-weight: 500;
|
||||
}
|
||||
.footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
padding: 12px 16px 16px;
|
||||
background: linear-gradient(180deg, transparent, #f1f5f9 28%);
|
||||
}
|
||||
.submit {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: var(--mint);
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.note {
|
||||
margin-top: 20px;
|
||||
padding: 14px 16px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
font-size: 13px;
|
||||
color: var(--slate);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.note strong { color: var(--ink); }
|
||||
@media (max-width: 900px) {
|
||||
.phones, .layout-picker { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<h1>运输计划回传 · 按品牌填量 Demo</h1>
|
||||
<p class="lead">
|
||||
左侧模拟<strong>现场链接</strong>(入/出库单回执),右侧模拟<strong>司机链接</strong>。
|
||||
有品牌的产品(烧碱)可填 1~N 条「品牌 + 数量」;无品牌产品(盐酸)只填总实际数量。
|
||||
请先选择下方交互方案,再在手机框内试用。
|
||||
</p>
|
||||
|
||||
<div class="layout-picker" id="layoutPicker">
|
||||
<button type="button" class="layout-opt active" data-layout="a" onclick="setLayout('a')">
|
||||
<span class="tag">方案 A · 推荐</span>
|
||||
<h3>下拉选品牌 + 数量行</h3>
|
||||
<p>可增减行;合计自动回写实际数量。与归档弹窗逻辑一致,适合现场快速录入。</p>
|
||||
</button>
|
||||
<button type="button" class="layout-opt" data-layout="b" onclick="setLayout('b')">
|
||||
<span class="tag">方案 B</span>
|
||||
<h3>品牌芯片 + 点选填量</h3>
|
||||
<p>先点选参与品牌,再为每个已选品牌填数量。品牌少时更直观。</p>
|
||||
</button>
|
||||
<button type="button" class="layout-opt" data-layout="c" onclick="setLayout('c')">
|
||||
<span class="tag">方案 C</span>
|
||||
<h3>仅现场填品牌;司机只填总量</h3>
|
||||
<p>司机链接保持现状(一个总实际数量);品牌明细只在现场链接填写。</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="phones">
|
||||
<!-- 现场 -->
|
||||
<div class="phone-wrap">
|
||||
<h2>现场链接 · 入库单回执</h2>
|
||||
<div class="phone">
|
||||
<div class="phone-screen">
|
||||
<div class="nav">入库单 · 现场回执</div>
|
||||
<div class="scroll">
|
||||
<div class="card">
|
||||
<div class="cell"><span class="k">业务类型</span><span class="v">采购入库</span></div>
|
||||
<div class="cell"><span class="k">往来单位</span><span class="v">君正化工</span></div>
|
||||
<div class="cell"><span class="k">车牌</span><span class="v">皖A·12X34</span></div>
|
||||
<div class="cell"><span class="k">计划号</span><span class="v">JH20260731001</span></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">产品实际数量</div>
|
||||
</div>
|
||||
|
||||
<!-- 有品牌:烧碱 -->
|
||||
<div class="product" id="siteBranded">
|
||||
<div class="product-head">
|
||||
<div class="product-name">烧碱</div>
|
||||
<span class="badge warn">不限品牌</span>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<div class="meta-item">
|
||||
<div class="meta-label">规格型号</div>
|
||||
<div class="meta-value">32%</div>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<div class="meta-label">包装规格</div>
|
||||
<div class="meta-value">散装</div>
|
||||
</div>
|
||||
<div class="meta-item span">
|
||||
<div class="meta-label">计划数量</div>
|
||||
<div class="meta-value plan">30 吨</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actual-box">
|
||||
<div class="actual-box__label">
|
||||
<strong>实际数量</strong>
|
||||
<span class="layout-a-only layout-b-only">由品牌明细合计</span>
|
||||
<span class="layout-c-only" style="display:none">请按现场实装填写</span>
|
||||
</div>
|
||||
<div class="actual-row">
|
||||
<input id="siteActualCaustic" type="number" value="28" step="0.001" readonly />
|
||||
<span class="unit">吨</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="brand-block layout-a-only" id="siteBrandRowsA">
|
||||
<div class="brand-head">
|
||||
<h4>按品牌填量</h4>
|
||||
<span class="brand-sum ok" id="siteSumA">合计 28 = 实际数量 ✓</span>
|
||||
</div>
|
||||
<div id="siteRowsA"></div>
|
||||
<button type="button" class="add-brand" onclick="addSiteRowA()">+ 添加品牌</button>
|
||||
</div>
|
||||
|
||||
<div class="brand-block layout-b-only" id="siteBrandRowsB" style="display:none">
|
||||
<div class="brand-head">
|
||||
<h4>选择品牌并填量</h4>
|
||||
<span class="brand-sum ok" id="siteSumB">合计 28 = 实际数量 ✓</span>
|
||||
</div>
|
||||
<div class="chips" id="siteChips"></div>
|
||||
<div id="siteRowsB"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 无品牌:盐酸 -->
|
||||
<div class="product">
|
||||
<div class="product-head">
|
||||
<div class="product-name">盐酸</div>
|
||||
<span class="badge">产品 2/2</span>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<div class="meta-item">
|
||||
<div class="meta-label">规格型号</div>
|
||||
<div class="meta-value">31%</div>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<div class="meta-label">包装规格</div>
|
||||
<div class="meta-value">吨袋</div>
|
||||
</div>
|
||||
<div class="meta-item span">
|
||||
<div class="meta-label">计划数量</div>
|
||||
<div class="meta-value plan">10 吨</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actual-box">
|
||||
<div class="actual-box__label">
|
||||
<strong>实际数量</strong>
|
||||
<span>无品牌产品,直接填写总量</span>
|
||||
</div>
|
||||
<div class="actual-row">
|
||||
<input type="number" value="9.8" step="0.001" />
|
||||
<span class="unit">吨</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<button type="button" class="submit" onclick="alert('Demo:现场回执已提交(含品牌明细)')">提交</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 司机 -->
|
||||
<div class="phone-wrap">
|
||||
<h2>司机链接 · 运输回传</h2>
|
||||
<div class="phone">
|
||||
<div class="phone-screen">
|
||||
<div class="nav">运输计划</div>
|
||||
<div class="scroll">
|
||||
<div class="card">
|
||||
<div class="cell"><span class="k">计划编号</span><span class="v">JH20260731001</span></div>
|
||||
<div class="cell"><span class="k">车牌</span><span class="v">皖A·12X34</span></div>
|
||||
<div class="cell"><span class="k">司机</span><span class="v">张师傅 138****0001</span></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">产品明细</div>
|
||||
<div class="cell"><span class="k">烧碱 32% · 散装</span><span class="v">计划 30 吨</span></div>
|
||||
<div class="cell"><span class="k">盐酸 31% · 吨袋</span><span class="v">计划 10 吨</span></div>
|
||||
</div>
|
||||
|
||||
<div class="card layout-c-only" style="display:none">
|
||||
<div class="card-title">回传数量</div>
|
||||
<div style="padding:12px 14px 14px">
|
||||
<div class="actual-box">
|
||||
<div class="actual-box__label">
|
||||
<strong>实际数量</strong>
|
||||
<span>方案 C:司机只填总量</span>
|
||||
</div>
|
||||
<div class="actual-row">
|
||||
<input type="number" value="37.8" step="0.001" />
|
||||
<span class="unit">吨</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout-a-only layout-b-only" id="driverProducts">
|
||||
<div class="card"><div class="card-title">回传数量(按产品)</div></div>
|
||||
|
||||
<div class="product">
|
||||
<div class="product-head">
|
||||
<div class="product-name">烧碱</div>
|
||||
<span class="badge warn">不限品牌</span>
|
||||
</div>
|
||||
<div class="actual-box">
|
||||
<div class="actual-box__label">
|
||||
<strong>实际数量</strong>
|
||||
<span>由品牌明细合计</span>
|
||||
</div>
|
||||
<div class="actual-row">
|
||||
<input id="driverActualCaustic" type="number" value="28" step="0.001" readonly />
|
||||
<span class="unit">吨</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="brand-block">
|
||||
<div class="brand-head">
|
||||
<h4>按品牌填量</h4>
|
||||
<span class="brand-sum ok" id="driverSum">合计 28 = 实际数量 ✓</span>
|
||||
</div>
|
||||
<div id="driverRows"></div>
|
||||
<button type="button" class="add-brand" onclick="addDriverRow()">+ 添加品牌</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="product">
|
||||
<div class="product-head">
|
||||
<div class="product-name">盐酸</div>
|
||||
<span class="badge">无品牌</span>
|
||||
</div>
|
||||
<div class="actual-box">
|
||||
<div class="actual-box__label">
|
||||
<strong>实际数量</strong>
|
||||
<span>无品牌,直接填总量</span>
|
||||
</div>
|
||||
<div class="actual-row">
|
||||
<input type="number" value="9.8" step="0.001" />
|
||||
<span class="unit">吨</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<button type="button" class="submit" onclick="alert('Demo:司机回执已保存')">保存回执</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
<strong>规则预览:</strong>
|
||||
① 产品有品牌概念(brandMode = any/required)时展示品牌明细;无品牌则隐藏。
|
||||
② 可填 1 个或多个品牌行,数量 > 0 才计入。
|
||||
③ 各品牌数量合计 = 该产品实际数量;不限品牌时可多选品牌,指定品牌时仅允许该品牌一行。
|
||||
④ 现场回传写入后,归档弹窗可预填品牌明细(与现有归档按品牌库存衔接)。
|
||||
<br /><br />
|
||||
点击上方方案卡片选择偏好(会写入选择事件)。本地文件也可直接打开:
|
||||
<code>.superpowers/brainstorm/.../content/site-driver-brand-qty-demo.html</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const BRANDS = [
|
||||
{ id: 'b1', name: '君正' },
|
||||
{ id: 'b2', name: '红四方' },
|
||||
{ id: 'b3', name: '中盐' }
|
||||
]
|
||||
|
||||
let layout = 'a'
|
||||
let siteRows = [
|
||||
{ brandId: 'b1', qty: 16 },
|
||||
{ brandId: 'b2', qty: 12 }
|
||||
]
|
||||
let driverRows = [
|
||||
{ brandId: 'b1', qty: 16 },
|
||||
{ brandId: 'b2', qty: 12 }
|
||||
]
|
||||
let selectedChips = new Set(['b1', 'b2'])
|
||||
|
||||
function setLayout(key) {
|
||||
layout = key
|
||||
document.querySelectorAll('.layout-opt').forEach(el => {
|
||||
el.classList.toggle('active', el.dataset.layout === key)
|
||||
})
|
||||
document.querySelectorAll('.layout-a-only').forEach(el => {
|
||||
el.style.display = key === 'a' || (el.classList.contains('layout-b-only') && key === 'b') ? '' : 'none'
|
||||
})
|
||||
document.querySelectorAll('.layout-b-only').forEach(el => {
|
||||
if (el.classList.contains('layout-a-only')) {
|
||||
el.style.display = (key === 'a' || key === 'b') ? '' : 'none'
|
||||
} else {
|
||||
el.style.display = key === 'b' ? '' : 'none'
|
||||
}
|
||||
})
|
||||
document.querySelectorAll('.layout-c-only').forEach(el => {
|
||||
el.style.display = key === 'c' ? '' : 'none'
|
||||
})
|
||||
// fix dual-class elements
|
||||
document.querySelectorAll('.layout-a-only.layout-b-only').forEach(el => {
|
||||
el.style.display = (key === 'a' || key === 'b') ? '' : 'none'
|
||||
})
|
||||
document.getElementById('siteBrandRowsA').style.display = key === 'a' ? '' : 'none'
|
||||
document.getElementById('siteBrandRowsB').style.display = key === 'b' ? '' : 'none'
|
||||
const actual = document.getElementById('siteActualCaustic')
|
||||
actual.readOnly = key !== 'c'
|
||||
if (key === 'c') actual.value = '28'
|
||||
renderAll()
|
||||
if (typeof toggleSelect === 'function') {
|
||||
const opt = document.querySelector(`.layout-opt[data-layout="${key}"]`)
|
||||
if (opt) toggleSelect(opt)
|
||||
}
|
||||
}
|
||||
|
||||
function sumRows(rows) {
|
||||
return rows.reduce((s, r) => s + (Number(r.qty) || 0), 0)
|
||||
}
|
||||
|
||||
function brandOptionsHtml(selectedId) {
|
||||
return BRANDS.map(b =>
|
||||
`<option value="${b.id}" ${b.id === selectedId ? 'selected' : ''}>${b.name}</option>`
|
||||
).join('')
|
||||
}
|
||||
|
||||
function updateSumLabel(el, total, actual) {
|
||||
const ok = Math.abs(total - actual) < 0.0005
|
||||
el.className = 'brand-sum ' + (ok ? 'ok' : 'bad')
|
||||
el.textContent = ok
|
||||
? `合计 ${total} = 实际数量 ✓`
|
||||
: `合计 ${total} ≠ 实际数量 ${actual}`
|
||||
}
|
||||
|
||||
function renderSiteA() {
|
||||
const box = document.getElementById('siteRowsA')
|
||||
box.innerHTML = siteRows.map((r, i) => `
|
||||
<div class="brand-row">
|
||||
<select onchange="siteRows[${i}].brandId=this.value; renderAll()">
|
||||
${brandOptionsHtml(r.brandId)}
|
||||
</select>
|
||||
<input type="number" step="0.001" min="0" value="${r.qty}"
|
||||
oninput="siteRows[${i}].qty=Number(this.value)||0; syncSiteActual()" />
|
||||
<button type="button" class="icon-btn" onclick="siteRows.splice(${i},1); renderAll()" title="删除">×</button>
|
||||
</div>
|
||||
`).join('')
|
||||
syncSiteActual()
|
||||
}
|
||||
|
||||
function syncSiteActual() {
|
||||
const total = Number(sumRows(siteRows).toFixed(3))
|
||||
document.getElementById('siteActualCaustic').value = total
|
||||
updateSumLabel(document.getElementById('siteSumA'), total, total)
|
||||
updateSumLabel(document.getElementById('siteSumB'), total, total)
|
||||
}
|
||||
|
||||
function addSiteRowA() {
|
||||
const used = new Set(siteRows.map(r => r.brandId))
|
||||
const next = BRANDS.find(b => !used.has(b.id)) || BRANDS[0]
|
||||
siteRows.push({ brandId: next.id, qty: 0 })
|
||||
renderAll()
|
||||
}
|
||||
|
||||
function renderSiteB() {
|
||||
const chips = document.getElementById('siteChips')
|
||||
chips.innerHTML = BRANDS.map(b => {
|
||||
const on = selectedChips.has(b.id)
|
||||
const row = siteRows.find(r => r.brandId === b.id)
|
||||
const q = row ? row.qty : 0
|
||||
return `<button type="button" class="chip ${on ? 'on' : ''}" onclick="toggleChip('${b.id}')">
|
||||
${b.name}${on ? `<span class="q">${q || ''}</span>` : ''}
|
||||
</button>`
|
||||
}).join('')
|
||||
|
||||
const box = document.getElementById('siteRowsB')
|
||||
const active = BRANDS.filter(b => selectedChips.has(b.id))
|
||||
box.innerHTML = active.map(b => {
|
||||
let row = siteRows.find(r => r.brandId === b.id)
|
||||
if (!row) {
|
||||
row = { brandId: b.id, qty: 0 }
|
||||
siteRows.push(row)
|
||||
}
|
||||
const i = siteRows.indexOf(row)
|
||||
return `<div class="brand-row" style="grid-template-columns: 72px 1fr">
|
||||
<div style="font-size:13px;font-weight:600;padding-left:4px">${b.name}</div>
|
||||
<input type="number" step="0.001" min="0" value="${row.qty}"
|
||||
oninput="siteRows[${i}].qty=Number(this.value)||0; syncSiteActual(); renderSiteB()" />
|
||||
</div>`
|
||||
}).join('')
|
||||
siteRows = siteRows.filter(r => selectedChips.has(r.brandId))
|
||||
syncSiteActual()
|
||||
}
|
||||
|
||||
function toggleChip(id) {
|
||||
if (selectedChips.has(id)) selectedChips.delete(id)
|
||||
else selectedChips.add(id)
|
||||
renderAll()
|
||||
}
|
||||
|
||||
function renderDriver() {
|
||||
const box = document.getElementById('driverRows')
|
||||
box.innerHTML = driverRows.map((r, i) => `
|
||||
<div class="brand-row">
|
||||
<select onchange="driverRows[${i}].brandId=this.value; renderAll()">
|
||||
${brandOptionsHtml(r.brandId)}
|
||||
</select>
|
||||
<input type="number" step="0.001" min="0" value="${r.qty}"
|
||||
oninput="driverRows[${i}].qty=Number(this.value)||0; syncDriverActual()" />
|
||||
<button type="button" class="icon-btn" onclick="driverRows.splice(${i},1); renderAll()">×</button>
|
||||
</div>
|
||||
`).join('')
|
||||
syncDriverActual()
|
||||
}
|
||||
|
||||
function syncDriverActual() {
|
||||
const total = Number(sumRows(driverRows).toFixed(3))
|
||||
document.getElementById('driverActualCaustic').value = total
|
||||
updateSumLabel(document.getElementById('driverSum'), total, total)
|
||||
}
|
||||
|
||||
function addDriverRow() {
|
||||
const used = new Set(driverRows.map(r => r.brandId))
|
||||
const next = BRANDS.find(b => !used.has(b.id)) || BRANDS[0]
|
||||
driverRows.push({ brandId: next.id, qty: 0 })
|
||||
renderAll()
|
||||
}
|
||||
|
||||
function renderAll() {
|
||||
renderSiteA()
|
||||
renderSiteB()
|
||||
renderDriver()
|
||||
}
|
||||
|
||||
renderAll()
|
||||
setLayout('a')
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,5 @@
|
||||
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh;flex-direction:column;gap:8px;padding:24px;text-align:center">
|
||||
<p class="subtitle">范围已记:现场链接 + 司机链接都按产品填品牌明细</p>
|
||||
<p class="subtitle">查看 Tab / 归档补充弹窗 / 标题栏按钮布局要求已记录</p>
|
||||
<p class="subtitle">继续在终端确认交互细节…</p>
|
||||
</div>
|
||||
@@ -0,0 +1,4 @@
|
||||
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh;flex-direction:column;gap:8px;padding:24px;text-align:center">
|
||||
<p class="subtitle">设计各节已确认</p>
|
||||
<p class="subtitle">正在写入设计文档,请回到终端审阅…</p>
|
||||
</div>
|
||||
@@ -0,0 +1,4 @@
|
||||
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh;flex-direction:column;gap:8px;padding:24px;text-align:center">
|
||||
<p class="subtitle">实现计划已写完</p>
|
||||
<p class="subtitle">请在终端选择执行方式…</p>
|
||||
</div>
|
||||
@@ -0,0 +1,4 @@
|
||||
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh;flex-direction:column;gap:8px">
|
||||
<p class="subtitle">已选择方案 A:下拉选品牌 + 数量行</p>
|
||||
<p class="subtitle">继续在终端确认范围…</p>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
{"reason":"owner process exited","timestamp":1785444458374}
|
||||
@@ -0,0 +1 @@
|
||||
70608
|
||||
@@ -0,0 +1,557 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>门户大面板宽度 · 一级菜单较少时</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary: #10b981;
|
||||
--primary-dark: #059669;
|
||||
--primary-bg: #ecfdf5;
|
||||
--header: #1ca851;
|
||||
--text: #0f172a;
|
||||
--muted: #64748b;
|
||||
--border: #e2e8f0;
|
||||
--bg: #f1f5f9;
|
||||
--panel-min: 720px;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Inter, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
color: var(--text);
|
||||
background: #f8fafc;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.page { padding: 20px 24px 48px; max-width: 1280px; margin: 0 auto; }
|
||||
h1 { font-size: 22px; margin: 0 0 6px; }
|
||||
.lead { color: var(--muted); font-size: 14px; margin: 0 0 14px; }
|
||||
.note {
|
||||
background: #fffbeb;
|
||||
border: 1px solid #fde68a;
|
||||
color: #92400e;
|
||||
border-radius: 10px;
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.ok {
|
||||
background: var(--primary-bg);
|
||||
border: 1px solid #a7f3d0;
|
||||
color: #065f46;
|
||||
border-radius: 10px;
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px 16px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 14px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
}
|
||||
.controls label {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
font-weight: 600;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.seg {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.seg button {
|
||||
border: 1px solid var(--border);
|
||||
background: #fff;
|
||||
border-radius: 999px;
|
||||
padding: 7px 12px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
color: #334155;
|
||||
}
|
||||
.seg button.active {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
.card {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.card-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: #fafafa;
|
||||
}
|
||||
.card-head h2 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 15px;
|
||||
}
|
||||
.card-head p {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.badge {
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
padding: 4px 8px;
|
||||
border-radius: 999px;
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
}
|
||||
.badge.bad { background: #fee2e2; color: #b91c1c; }
|
||||
.badge.good { background: #d1fae5; color: #047857; }
|
||||
.badge.rec { background: #ecfdf5; color: #065f46; border: 1px solid #a7f3d0; }
|
||||
.frame {
|
||||
height: 360px;
|
||||
background: var(--bg);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.app {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 13px;
|
||||
}
|
||||
.top {
|
||||
position: relative;
|
||||
height: 56px;
|
||||
background: var(--header);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
flex-shrink: 0;
|
||||
color: #fff;
|
||||
z-index: 2;
|
||||
}
|
||||
.brand {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.nav-slot {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
max-width: min(720px, calc(100% - 280px));
|
||||
}
|
||||
.l1 {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 2px;
|
||||
height: 100%;
|
||||
}
|
||||
.l1-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 0 14px;
|
||||
height: 100%;
|
||||
white-space: nowrap;
|
||||
border: none;
|
||||
border-bottom: 4px solid transparent;
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.l1-item.on {
|
||||
font-weight: 600;
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
.l1-item .caret { font-size: 10px; opacity: 0.75; }
|
||||
.end {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
.avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #6ee7b7, #34d399);
|
||||
border: 2px solid rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
.main {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
background: #eef2f7;
|
||||
overflow: hidden;
|
||||
}
|
||||
.page-fake {
|
||||
padding: 20px 24px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
.page-fake .bar {
|
||||
height: 10px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.page-fake .bar.w2 { width: 70%; }
|
||||
.page-fake .bar.w3 { width: 48%; }
|
||||
.mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.25);
|
||||
z-index: 3;
|
||||
}
|
||||
.panel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 4;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-top: none;
|
||||
border-radius: 0 0 10px 10px;
|
||||
box-shadow: 0 14px 32px rgba(15, 23, 42, 0.12);
|
||||
max-height: calc(100% - 8px);
|
||||
overflow: auto;
|
||||
}
|
||||
.panel-body { padding: 12px 16px 16px; }
|
||||
.split {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0 16px;
|
||||
}
|
||||
.pane + .pane {
|
||||
padding-left: 16px;
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 88px 1fr;
|
||||
gap: 8px 12px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
align-items: start;
|
||||
}
|
||||
.row:last-child { border-bottom: 0; }
|
||||
.l2 {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
padding-top: 4px;
|
||||
}
|
||||
.links { display: flex; flex-direction: column; gap: 2px; }
|
||||
.links span {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #0f172a;
|
||||
padding: 3px 0;
|
||||
}
|
||||
.links span.on { color: var(--primary-dark); font-weight: 600; }
|
||||
.measure {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 10px;
|
||||
z-index: 5;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #b45309;
|
||||
background: #fffbeb;
|
||||
border: 1px solid #fde68a;
|
||||
border-radius: 6px;
|
||||
padding: 3px 8px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.measure.ok-m {
|
||||
color: #047857;
|
||||
background: #ecfdf5;
|
||||
border-color: #a7f3d0;
|
||||
}
|
||||
.pick {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.pick button {
|
||||
border: 1px solid var(--primary);
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 10px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.pick button.ghost {
|
||||
background: #fff;
|
||||
color: #334155;
|
||||
border-color: var(--border);
|
||||
}
|
||||
.pick button.alt {
|
||||
background: var(--primary-bg);
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<h1>大面板宽度 Demo:一级菜单较少时</h1>
|
||||
<p class="lead">
|
||||
现状:二级大面板宽度 = 一级菜单区域宽度。权限较少(仅 2 / 4 个一级)时,面板会跟着变窄,两栏二级排不开。
|
||||
</p>
|
||||
<div class="note">
|
||||
先用上方开关模拟「一级数量」,再对比三种宽度策略。点选卡片或底部按钮告诉我偏好。
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div>
|
||||
<label>一级菜单数量</label>
|
||||
<div class="seg" id="countSeg">
|
||||
<button type="button" data-count="2">2 个(窄权限)</button>
|
||||
<button type="button" data-count="4" class="active">4 个</button>
|
||||
<button type="button" data-count="7">7 个(接近满)</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label>高亮模块</label>
|
||||
<div class="seg" id="activeSeg">
|
||||
<button type="button" class="active" data-active="0">第一个</button>
|
||||
<button type="button" data-active="1">第二个</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ok">
|
||||
推荐倾向:<strong>方案 B</strong> — 面板有最小宽度(如 720px),相对一级区域居中;一级很多时仍可随区域变宽,但不超过顶栏中间槽位上限。
|
||||
</div>
|
||||
|
||||
<div class="grid" id="cards"></div>
|
||||
|
||||
<div class="pick">
|
||||
<button type="button" class="alt" data-choice="a">选 A · 跟一级同宽(现状)</button>
|
||||
<button type="button" data-choice="b">选 B · 最小宽度居中(推荐)</button>
|
||||
<button type="button" class="ghost" data-choice="c">选 C · 固定铺满中间槽</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const ALL_L1 = ['CRM', '仓储', '质检', '财务', '系统', '报表', '运营']
|
||||
const PANEL_ROWS_LEFT = [
|
||||
{ l2: '客户管理', links: ['客户列表', '联系人', '公海客户'] },
|
||||
{ l2: '商机报价', links: ['商机列表', '报价单', '价格函'] },
|
||||
{ l2: '合同订单', links: ['合同审批', '订单跟踪'] },
|
||||
{ l2: '拜访计划', links: ['拜访日历', '拜访记录'] },
|
||||
{ l2: '基础资料', links: ['产品目录', '单位管理'] }
|
||||
]
|
||||
const PANEL_ROWS_RIGHT = [
|
||||
{ l2: '审批中心', links: ['待办审批', '已办审批'] },
|
||||
{ l2: '设置', links: ['审批人设置', '业务参数'] }
|
||||
]
|
||||
|
||||
const strategies = [
|
||||
{
|
||||
id: 'a',
|
||||
title: 'A · 跟一级区域同宽(现状)',
|
||||
desc: '面板 left/width 完全等于一级 nav 的 getBoundingClientRect。一级少 → 面板窄。',
|
||||
badge: '现状问题',
|
||||
badgeClass: 'bad',
|
||||
calc(navRect, slotMax) {
|
||||
return { left: navRect.left, width: navRect.width }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
title: 'B · 最小宽度 + 相对一级居中(推荐)',
|
||||
desc: 'width = max(一级宽度, min(720, 槽位上限)),相对一级中心对齐;一级很多时仍可更宽。',
|
||||
badge: '推荐',
|
||||
badgeClass: 'rec',
|
||||
calc(navRect, slotMax, viewportW) {
|
||||
const minW = Math.min(720, slotMax, viewportW - 32)
|
||||
const width = Math.max(navRect.width, minW)
|
||||
const center = navRect.left + navRect.width / 2
|
||||
let left = center - width / 2
|
||||
left = Math.max(16, Math.min(left, viewportW - width - 16))
|
||||
return { left, width }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'c',
|
||||
title: 'C · 始终铺满顶栏中间槽位',
|
||||
desc: '面板宽度固定为中间槽 max-width(约 720),与一级数量无关,始终居中于顶栏。',
|
||||
badge: '固定宽',
|
||||
badgeClass: 'good',
|
||||
calc(navRect, slotMax, viewportW, slotRect) {
|
||||
const width = Math.min(720, slotMax, viewportW - 32)
|
||||
const center = slotRect.left + slotRect.width / 2
|
||||
let left = center - width / 2
|
||||
left = Math.max(16, Math.min(left, viewportW - width - 16))
|
||||
return { left, width }
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
let l1Count = 4
|
||||
let activeIdx = 0
|
||||
|
||||
function rowHtml(row) {
|
||||
return `<div class="row">
|
||||
<div class="l2">${row.l2}</div>
|
||||
<div class="links">${row.links.map((t, i) => `<span class="${i === 0 ? 'on' : ''}">${t}</span>`).join('')}</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
function panelInner() {
|
||||
return `<div class="panel-body">
|
||||
<div class="split">
|
||||
<div class="pane">${PANEL_ROWS_LEFT.map(rowHtml).join('')}</div>
|
||||
<div class="pane">${PANEL_ROWS_RIGHT.map(rowHtml).join('')}</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
function render() {
|
||||
const cards = document.getElementById('cards')
|
||||
cards.innerHTML = strategies.map((s) => `
|
||||
<div class="card" data-choice="${s.id}" onclick="window.__pick('${s.id}')">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<h2>${s.title}</h2>
|
||||
<p>${s.desc}</p>
|
||||
</div>
|
||||
<span class="badge ${s.badgeClass}">${s.badge}</span>
|
||||
</div>
|
||||
<div class="frame" id="frame-${s.id}">
|
||||
<div class="app">
|
||||
<div class="top">
|
||||
<div class="brand">山峡化工</div>
|
||||
<div class="nav-slot" id="slot-${s.id}">
|
||||
<div class="l1" id="nav-${s.id}">
|
||||
${ALL_L1.slice(0, l1Count).map((name, i) => `
|
||||
<button type="button" class="l1-item ${i === activeIdx ? 'on' : ''}">
|
||||
<span>${name}</span><span class="caret">▾</span>
|
||||
</button>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="end"><div class="avatar"></div></div>
|
||||
</div>
|
||||
<div class="main" id="main-${s.id}">
|
||||
<div class="page-fake">
|
||||
<div class="bar"></div>
|
||||
<div class="bar w2"></div>
|
||||
<div class="bar w3"></div>
|
||||
<p>内容区(示意)</p>
|
||||
</div>
|
||||
<div class="mask"></div>
|
||||
<div class="panel" id="panel-${s.id}">${panelInner()}</div>
|
||||
<div class="measure" id="measure-${s.id}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')
|
||||
|
||||
requestAnimationFrame(layoutPanels)
|
||||
}
|
||||
|
||||
function layoutPanels() {
|
||||
strategies.forEach((s) => {
|
||||
const frame = document.getElementById(`frame-${s.id}`)
|
||||
const nav = document.getElementById(`nav-${s.id}`)
|
||||
const slot = document.getElementById(`slot-${s.id}`)
|
||||
const main = document.getElementById(`main-${s.id}`)
|
||||
const panel = document.getElementById(`panel-${s.id}`)
|
||||
const measure = document.getElementById(`measure-${s.id}`)
|
||||
if (!frame || !nav || !slot || !main || !panel) return
|
||||
|
||||
const frameRect = frame.getBoundingClientRect()
|
||||
const navRect = nav.getBoundingClientRect()
|
||||
const slotRect = slot.getBoundingClientRect()
|
||||
const mainRect = main.getBoundingClientRect()
|
||||
const slotMax = Math.min(720, frameRect.width - 280)
|
||||
|
||||
const localNav = {
|
||||
left: navRect.left - mainRect.left,
|
||||
width: navRect.width
|
||||
}
|
||||
const localSlot = {
|
||||
left: slotRect.left - mainRect.left,
|
||||
width: slotRect.width
|
||||
}
|
||||
const viewportW = mainRect.width
|
||||
|
||||
const pos = s.calc(localNav, slotMax, viewportW, localSlot)
|
||||
panel.style.left = `${Math.round(pos.left)}px`
|
||||
panel.style.width = `${Math.round(pos.width)}px`
|
||||
|
||||
const narrow = pos.width < 520
|
||||
measure.textContent = `面板宽 ≈ ${Math.round(pos.width)}px · 一级区 ≈ ${Math.round(navRect.width)}px`
|
||||
measure.className = `measure ${narrow ? '' : 'ok-m'}`
|
||||
})
|
||||
}
|
||||
|
||||
window.__pick = function (id) {
|
||||
const names = { a: 'A 现状同宽', b: 'B 最小宽度居中', c: 'C 固定铺满中间槽' }
|
||||
alert(`已记录偏好:${names[id] || id}\n\n请在对话里回复确认,或继续改参数再看。`)
|
||||
if (typeof toggleSelect === 'function') {
|
||||
const el = document.querySelector(`[data-choice="${id}"]`)
|
||||
if (el) toggleSelect(el)
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('countSeg').addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('button[data-count]')
|
||||
if (!btn) return
|
||||
l1Count = Number(btn.dataset.count)
|
||||
document.querySelectorAll('#countSeg button').forEach((b) => b.classList.toggle('active', b === btn))
|
||||
if (activeIdx >= l1Count) activeIdx = 0
|
||||
render()
|
||||
})
|
||||
|
||||
document.getElementById('activeSeg').addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('button[data-active]')
|
||||
if (!btn) return
|
||||
activeIdx = Math.min(Number(btn.dataset.active), l1Count - 1)
|
||||
document.querySelectorAll('#activeSeg button').forEach((b) => b.classList.toggle('active', b === btn))
|
||||
render()
|
||||
})
|
||||
|
||||
document.querySelector('.pick').addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('button[data-choice]')
|
||||
if (!btn) return
|
||||
window.__pick(btn.dataset.choice)
|
||||
})
|
||||
|
||||
window.addEventListener('resize', layoutPanels)
|
||||
render()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
|
||||
<p class="subtitle">已选方案 B,正在落地实现…</p>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
{"reason":"owner process exited","timestamp":1785574025737}
|
||||
@@ -0,0 +1 @@
|
||||
73671
|
||||
@@ -0,0 +1 @@
|
||||
10706
|
||||
@@ -722,7 +722,8 @@ def register_quote_routes(router):
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
data = get_setting(db, tenant_no=tenant_no)
|
||||
return success_response(data or {"approverIds": [], "status": "1"})
|
||||
# 无配置时默认「启用」,避免设置页首次打开落到「停用」导致保存后无法预填
|
||||
return success_response(data or {"approverIds": [], "status": "0"})
|
||||
except Exception as e:
|
||||
return error_response(500, str(e))
|
||||
|
||||
|
||||
@@ -168,6 +168,7 @@
|
||||
"167": "Community 167",
|
||||
"168": "vehicle_urea.py",
|
||||
"169": "product.py",
|
||||
"170": "test_quote_approver_setting.py",
|
||||
"171": "LocalStorage",
|
||||
"172": "MinIOStorage",
|
||||
"174": "datetime_parse.py",
|
||||
|
||||
@@ -0,0 +1,533 @@
|
||||
{
|
||||
"0": "Community 0",
|
||||
"1": "Community 1",
|
||||
"2": "Community 2",
|
||||
"3": "Community 3",
|
||||
"4": "Community 4",
|
||||
"5": "Community 5",
|
||||
"6": "Community 6",
|
||||
"7": "Community 7",
|
||||
"8": "Community 8",
|
||||
"9": "Community 9",
|
||||
"10": "Community 10",
|
||||
"11": "Community 11",
|
||||
"12": "Community 12",
|
||||
"13": "Community 13",
|
||||
"14": "Community 14",
|
||||
"15": "Community 15",
|
||||
"16": "Community 16",
|
||||
"17": "Community 17",
|
||||
"18": "Community 18",
|
||||
"19": "Community 19",
|
||||
"20": "Community 20",
|
||||
"21": "Community 21",
|
||||
"22": "Community 22",
|
||||
"23": "Community 23",
|
||||
"24": "Community 24",
|
||||
"25": "Community 25",
|
||||
"26": "Community 26",
|
||||
"27": "Community 27",
|
||||
"28": "Community 28",
|
||||
"29": "Community 29",
|
||||
"30": "Community 30",
|
||||
"31": "Community 31",
|
||||
"32": "Community 32",
|
||||
"33": "Community 33",
|
||||
"34": "Community 34",
|
||||
"35": "Community 35",
|
||||
"36": "Community 36",
|
||||
"37": "Community 37",
|
||||
"38": "Community 38",
|
||||
"39": "Community 39",
|
||||
"40": "Community 40",
|
||||
"41": "Community 41",
|
||||
"42": "Community 42",
|
||||
"43": "Community 43",
|
||||
"44": "Community 44",
|
||||
"45": "Community 45",
|
||||
"46": "Community 46",
|
||||
"47": "Community 47",
|
||||
"48": "Community 48",
|
||||
"49": "Community 49",
|
||||
"50": "Community 50",
|
||||
"51": "Community 51",
|
||||
"52": "Community 52",
|
||||
"53": "Community 53",
|
||||
"54": "Community 54",
|
||||
"55": "Community 55",
|
||||
"56": "Community 56",
|
||||
"57": "Community 57",
|
||||
"58": "Community 58",
|
||||
"59": "Community 59",
|
||||
"60": "Community 60",
|
||||
"61": "Community 61",
|
||||
"62": "Community 62",
|
||||
"63": "Community 63",
|
||||
"64": "Community 64",
|
||||
"65": "Community 65",
|
||||
"66": "Community 66",
|
||||
"67": "Community 67",
|
||||
"68": "Community 68",
|
||||
"69": "Community 69",
|
||||
"70": "Community 70",
|
||||
"71": "Community 71",
|
||||
"72": "Community 72",
|
||||
"73": "Community 73",
|
||||
"74": "Community 74",
|
||||
"75": "Community 75",
|
||||
"76": "Community 76",
|
||||
"77": "Community 77",
|
||||
"78": "Community 78",
|
||||
"79": "Community 79",
|
||||
"80": "Community 80",
|
||||
"81": "Community 81",
|
||||
"82": "Community 82",
|
||||
"83": "Community 83",
|
||||
"84": "Community 84",
|
||||
"85": "Community 85",
|
||||
"86": "Community 86",
|
||||
"87": "Community 87",
|
||||
"88": "Any",
|
||||
"89": "pl_ocr_service.py",
|
||||
"90": "Community 90",
|
||||
"91": "pl_vehicle_recharge_service.py",
|
||||
"92": "Community 92",
|
||||
"93": "_invoice_info_by_customer_impl",
|
||||
"94": "response.py",
|
||||
"95": "Community 95",
|
||||
"96": "Community 96",
|
||||
"97": "_ensure_subpack_slip_no",
|
||||
"98": "Community 98",
|
||||
"99": "Community 99",
|
||||
"100": "price_letter_detail",
|
||||
"101": ".to_tenant_user_dict",
|
||||
"102": "dept.py",
|
||||
"103": "Community 103",
|
||||
"104": "apply_crm_create_by_scope",
|
||||
"105": "list_customers",
|
||||
"106": "Community 106",
|
||||
"107": "apply_pl_scope",
|
||||
"108": "Community 108",
|
||||
"109": "Community 109",
|
||||
"110": "Base",
|
||||
"111": "Community 111",
|
||||
"112": "storage.py",
|
||||
"113": "quote_share_log.py",
|
||||
"114": "db_models.py",
|
||||
"115": "site.py",
|
||||
"116": "Community 116",
|
||||
"117": "Community 117",
|
||||
"118": "Community 118",
|
||||
"119": "Community 119",
|
||||
"120": "Community 120",
|
||||
"121": "Community 121",
|
||||
"122": "Community 122",
|
||||
"123": "Community 123",
|
||||
"124": "Community 124",
|
||||
"125": "Community 125",
|
||||
"126": "customer_pool.py",
|
||||
"127": "Community 127",
|
||||
"128": "plan.py",
|
||||
"129": "BaseStorage",
|
||||
"130": "datetime_parse.py",
|
||||
"131": "LocalStorage",
|
||||
"132": "crm_supplier_hazard.py",
|
||||
"133": "Community 133",
|
||||
"134": "Community 134",
|
||||
"135": "post.py",
|
||||
"136": "Community 136",
|
||||
"137": "statistics.py",
|
||||
"138": "Community 138",
|
||||
"139": "Community 139",
|
||||
"140": "Community 140",
|
||||
"141": "clue_statistics.py",
|
||||
"142": "Community 142",
|
||||
"143": "SysEthnic",
|
||||
"144": "SysApiRequestLog",
|
||||
"145": "minio_url.py",
|
||||
"146": "__init__.py",
|
||||
"147": "Community 147",
|
||||
"148": "storage.py",
|
||||
"149": "Community 149",
|
||||
"150": "ContractListQuery",
|
||||
"151": "Community 151",
|
||||
"152": "Community 152",
|
||||
"153": "Community 153",
|
||||
"154": "web_login_rsa_public_key",
|
||||
"155": "Community 155",
|
||||
"156": "get_client_ip",
|
||||
"157": "Community 157",
|
||||
"158": "Community 158",
|
||||
"159": "context.py",
|
||||
"160": "register_local_callback",
|
||||
"161": "backfill_crm_contract_attachment_urls.py",
|
||||
"162": "BaseStorage",
|
||||
"163": "login_crypto.py",
|
||||
"164": "apply_pl_scope",
|
||||
"165": "Community 165",
|
||||
"166": "code:block1 (/iflytek/shanxia2.0/backend)",
|
||||
"167": "Community 167",
|
||||
"168": "vehicle_urea.py",
|
||||
"169": "product.py",
|
||||
"170": "test_quote_approver_setting.py",
|
||||
"171": "LocalStorage",
|
||||
"172": "MinIOStorage",
|
||||
"173": "SysEthnic",
|
||||
"174": "datetime_parse.py",
|
||||
"175": "vehicle_maintenance.py",
|
||||
"176": "_forbid_legacy_invoice_fields",
|
||||
"177": "vehicle_maintenance.py",
|
||||
"179": "__init__.py",
|
||||
"180": "menu.py",
|
||||
"185": "vehicle_urea.py",
|
||||
"186": "transport_address.py",
|
||||
"189": "create",
|
||||
"190": "menu.py",
|
||||
"192": "pool_rule.py",
|
||||
"197": "_coerce_resource_snapshot",
|
||||
"198": "user.py",
|
||||
"202": "statistics.py",
|
||||
"206": "crm_quote_pdf.py",
|
||||
"207": "shanxia2.0 后端骨架(FastAPI)",
|
||||
"211": "leave.py",
|
||||
"216": "io_order.py",
|
||||
"217": "auth.py",
|
||||
"218": "pool_rule.py",
|
||||
"220": "api_crypto_middleware.py",
|
||||
"223": "workflow.py",
|
||||
"224": "supplier_info.py",
|
||||
"226": "get_active",
|
||||
"228": "advance.py",
|
||||
"230": "attachment.py",
|
||||
"232": "transport_address.py",
|
||||
"233": "customer_pool.py",
|
||||
"235": "invoice_info.py",
|
||||
"236": "price_letter.py",
|
||||
"237": "order_status_compat.py",
|
||||
"239": "shanxia2.0 Backend 生产部署说明",
|
||||
"240": "delete",
|
||||
"242": "leave.py",
|
||||
"244": "expense_item.py",
|
||||
"245": "__init__.py",
|
||||
"246": "db_collation.py",
|
||||
"247": "expense_item.py",
|
||||
"249": "__init__.py",
|
||||
"250": "invoice.py",
|
||||
"251": "menu.py",
|
||||
"252": "public_inventory.py",
|
||||
"254": "Activate.ps1",
|
||||
"256": "statistics.py",
|
||||
"259": "一次上传一条发票:多文件挂同一档案,金额为总额,只写一条额度流水。",
|
||||
"262": "reimburse.py",
|
||||
"263": "__init__.py",
|
||||
"264": "__init__.py",
|
||||
"265": "__init__.py",
|
||||
"266": "workflow.py",
|
||||
"269": "__init__.py",
|
||||
"270": "leave.py",
|
||||
"271": "menu.py",
|
||||
"272": "__init__.py",
|
||||
"273": "__init__.py",
|
||||
"274": "__init__.py",
|
||||
"275": "__init__.py",
|
||||
"276": "__init__.py",
|
||||
"278": "__init__.py",
|
||||
"281": "__init__.py",
|
||||
"286": "send_delivery_notice",
|
||||
"287": "__init__.py",
|
||||
"288": "__init__.py",
|
||||
"289": "send_delivery_warning",
|
||||
"292": "send_order_status",
|
||||
"293": "__init__.py",
|
||||
"294": "__init__.py",
|
||||
"295": "__init__.py",
|
||||
"296": "__init__.py",
|
||||
"297": "__init__.py",
|
||||
"298": "__init__.py",
|
||||
"299": "__init__.py",
|
||||
"300": "__init__.py",
|
||||
"301": "__init__.py",
|
||||
"302": "send_payment_success",
|
||||
"303": "send_refund_result",
|
||||
"304": "__init__.py",
|
||||
"305": "__init__.py",
|
||||
"306": "__init__.py",
|
||||
"307": "__init__.py",
|
||||
"308": "__init__.py",
|
||||
"309": "__init__.py",
|
||||
"310": "__init__.py",
|
||||
"311": "__init__.py",
|
||||
"312": "__init__.py",
|
||||
"313": "send_urge_notification",
|
||||
"314": "get",
|
||||
"315": "get_multi_with_filters",
|
||||
"317": "base.py",
|
||||
"318": "clue.py",
|
||||
"320": "customer_pool.py",
|
||||
"321": "customer.py",
|
||||
"323": "invoice_info.py",
|
||||
"326": "price_letter.py",
|
||||
"327": "product.py",
|
||||
"328": "quote.py",
|
||||
"329": "user.py",
|
||||
"330": "base.py",
|
||||
"331": "__tablename__",
|
||||
"332": "__init__.py",
|
||||
"333": "advance.py",
|
||||
"334": "validate_expense_item_id",
|
||||
"335": "attachment.py",
|
||||
"336": "common.py",
|
||||
"337": "validate_parent_id",
|
||||
"338": "__init__.py",
|
||||
"339": "invoice.py",
|
||||
"340": "coerce_applicant_id",
|
||||
"341": "details_must_not_be_empty",
|
||||
"342": "reimburse.py",
|
||||
"343": "validate_advance_id",
|
||||
"344": "validate_expense_item_id",
|
||||
"345": "validate_approver_id",
|
||||
"346": "common.py",
|
||||
"347": "contact.py",
|
||||
"348": "customer_follow.py",
|
||||
"349": "customer.py",
|
||||
"350": "dashboard.py",
|
||||
"351": "__init__.py",
|
||||
"352": "inventory.py",
|
||||
"353": "plans.py",
|
||||
"354": "weighing.py",
|
||||
"355": "__init__.py",
|
||||
"356": "freight.py",
|
||||
"357": "__init__.py",
|
||||
"358": "plan.py",
|
||||
"360": "validate_coordinates",
|
||||
"361": "transport.py",
|
||||
"362": "validate_amount",
|
||||
"363": "validate_amount",
|
||||
"364": "validate_recharge_type",
|
||||
"365": "validate_amount",
|
||||
"366": "validate_apply_amount",
|
||||
"367": "validate_quantity",
|
||||
"368": "approve",
|
||||
"369": "create",
|
||||
"370": "finance_issue_stats",
|
||||
"371": "get_by_id",
|
||||
"372": "get_paged",
|
||||
"373": "global_status_counts",
|
||||
"374": "issue",
|
||||
"375": "_latest_approval_per_node",
|
||||
"376": "_my_apply_base",
|
||||
"377": "my_apply_stats",
|
||||
"378": "offset_amount_map",
|
||||
"379": "pending_approval_stats",
|
||||
"380": "advance.py",
|
||||
"381": "_query_approvals",
|
||||
"382": "reject",
|
||||
"383": "soft_delete",
|
||||
"384": "submit",
|
||||
"385": "_tenant_no",
|
||||
"386": "update",
|
||||
"387": "_workflow_nodes",
|
||||
"389": "create",
|
||||
"390": "get_by_biz",
|
||||
"391": "get_by_id",
|
||||
"392": "attachment.py",
|
||||
"393": "soft_delete",
|
||||
"394": "_tenant_no",
|
||||
"395": "get_by_id",
|
||||
"396": "get_paged",
|
||||
"397": "base.py",
|
||||
"398": "soft_delete",
|
||||
"399": "update",
|
||||
"401": "_build_tree",
|
||||
"402": "create",
|
||||
"403": "get_by_id",
|
||||
"404": "get_item_tree",
|
||||
"405": "expense_item.py",
|
||||
"406": "soft_delete",
|
||||
"407": "_tenant_no",
|
||||
"408": "update",
|
||||
"409": "_count_invoices",
|
||||
"410": "create",
|
||||
"411": "delete",
|
||||
"412": "get_mine_summary",
|
||||
"413": "is_wallet_admin",
|
||||
"414": "add_entry",
|
||||
"415": "assert_can_delete_invoice",
|
||||
"416": "calc_summary",
|
||||
"417": "debit_reimburse",
|
||||
"418": "list_entries",
|
||||
"419": "_lock_summary",
|
||||
"420": "refresh_summary",
|
||||
"421": "release_reimburse",
|
||||
"422": "list_ledgers",
|
||||
"423": "list_wallets",
|
||||
"424": "_lookup_user_name",
|
||||
"425": "out_rows_with_attachments",
|
||||
"426": "invoice.py",
|
||||
"427": "_require_wallet_admin",
|
||||
"428": "_resolve_owner",
|
||||
"429": "_resolve_owner_for_query",
|
||||
"430": "_summary_to_dict",
|
||||
"431": "_tenant_no",
|
||||
"432": "approve",
|
||||
"433": "batch_offset",
|
||||
"434": "build_apply_out_with_details",
|
||||
"435": "build_expense_name_condition",
|
||||
"436": "can_change_applicant",
|
||||
"437": "create_with_details",
|
||||
"438": "finance_payment_stats",
|
||||
"439": "get_by_id",
|
||||
"440": "get_details",
|
||||
"441": "get_details_map_by_reimburse_ids",
|
||||
"442": "get_linked_invoice_ids",
|
||||
"443": "get_linked_invoice_ids_map",
|
||||
"444": "get_linked_invoices_brief",
|
||||
"445": "get_paged",
|
||||
"446": "global_status_counts",
|
||||
"447": "_has_debit_reimburse",
|
||||
"448": "_latest_approval_per_node",
|
||||
"449": "_my_apply_base",
|
||||
"450": "my_apply_stats",
|
||||
"451": "offset",
|
||||
"452": "out_one_with_attachment_counts",
|
||||
"453": "out_rows_with_attachment_counts",
|
||||
"454": "pay",
|
||||
"455": "reimburse.py",
|
||||
"456": "_query_approvals",
|
||||
"457": "reject",
|
||||
"458": "resolve_applicant",
|
||||
"459": "soft_delete",
|
||||
"460": "submit",
|
||||
"461": "sync_reimburse_invoices",
|
||||
"462": "_tenant_no",
|
||||
"463": "update_with_details",
|
||||
"464": "withdraw",
|
||||
"465": "_workflow_nodes",
|
||||
"466": "ensure_by_biz_type",
|
||||
"467": "get_by_biz_type",
|
||||
"468": "get_by_id",
|
||||
"469": "get_paged",
|
||||
"470": "workflow.py",
|
||||
"471": "save_nodes",
|
||||
"472": "soft_delete",
|
||||
"473": "_tenant_no",
|
||||
"474": "update",
|
||||
"487": "_get_variable",
|
||||
"488": "_parse_node_config",
|
||||
"489": "_parse_variables",
|
||||
"490": "customer_pool.py",
|
||||
"491": "__init__.py",
|
||||
"495": "code:bash (cd /path/to/shanxia2.0)",
|
||||
"496": "code:bash (./start-prod.sh status # 查看状态)",
|
||||
"497": "code:bash (curl -s http://127.0.0.1:9111/health)",
|
||||
"498": "code:bash (cd /iflytek/shanxia2.0/backend)",
|
||||
"499": "code:bash (python -m venv venv)",
|
||||
"500": "code:bash (pip install -r requirements.txt)",
|
||||
"501": "code:env (# 数据库配置)",
|
||||
"502": "code:bash (uvicorn app.main:app --reload --port 8000)",
|
||||
"503": "BaseModel",
|
||||
"504": "PlanResourceSnapshotMixin",
|
||||
"510": "advance.py",
|
||||
"511": "attachment.py",
|
||||
"512": "expense_item.py",
|
||||
"514": "workflow.py",
|
||||
"517": "dashboard.py",
|
||||
"518": "__init__.py",
|
||||
"519": "inventory.py",
|
||||
"520": "plans.py",
|
||||
"521": "public_inventory.py",
|
||||
"522": "weighing.py",
|
||||
"523": "__init__.py",
|
||||
"524": "leave.py",
|
||||
"525": "menu.py",
|
||||
"526": "base_data.py",
|
||||
"527": "freight.py",
|
||||
"528": "__init__.py",
|
||||
"529": "inventory.py",
|
||||
"530": "io_order.py",
|
||||
"531": "plan.py",
|
||||
"533": "transport_address.py",
|
||||
"534": "transport.py",
|
||||
"535": "vehicle_maintenance.py",
|
||||
"536": "vehicle_recharge.py",
|
||||
"537": "vehicle_urea.py",
|
||||
"538": "workflow.py",
|
||||
"539": "auth.py",
|
||||
"545": "__init__.py",
|
||||
"548": "storage.py",
|
||||
"551": "base.py",
|
||||
"553": "clue.py",
|
||||
"554": "contact.py",
|
||||
"555": "contract.py",
|
||||
"556": "customer_follow.py",
|
||||
"557": "customer_pool.py",
|
||||
"558": "customer.py",
|
||||
"559": "invoice_info.py",
|
||||
"560": "opportunity.py",
|
||||
"561": "pool_rule.py",
|
||||
"562": "price_letter.py",
|
||||
"563": "product.py",
|
||||
"564": "quote.py",
|
||||
"565": "statistics.py",
|
||||
"566": "supplier_info.py",
|
||||
"569": "__init__.py",
|
||||
"570": "leave.py",
|
||||
"571": "menu.py",
|
||||
"573": "user.py",
|
||||
"574": "workflow.py",
|
||||
"576": "base.py",
|
||||
"577": "__init__.py",
|
||||
"578": "attachment.py",
|
||||
"579": "auth.py",
|
||||
"580": "advance.py",
|
||||
"581": "attachment.py",
|
||||
"582": "common.py",
|
||||
"583": "expense_item.py",
|
||||
"584": "__init__.py",
|
||||
"585": "workflow.py",
|
||||
"586": "common.py",
|
||||
"587": "clue.py",
|
||||
"588": "contact.py",
|
||||
"589": "contract.py",
|
||||
"590": "customer_follow.py",
|
||||
"591": "customer_pool.py",
|
||||
"592": "customer.py",
|
||||
"593": "invoice_info.py",
|
||||
"594": "opportunity.py",
|
||||
"595": "pool_rule.py",
|
||||
"596": "price_letter.py",
|
||||
"597": "product.py",
|
||||
"598": "quote.py",
|
||||
"599": "statistics.py",
|
||||
"600": "supplier_info.py",
|
||||
"601": "dashboard.py",
|
||||
"602": "__init__.py",
|
||||
"603": "inventory.py",
|
||||
"604": "plans.py",
|
||||
"605": "public_inventory.py",
|
||||
"606": "weighing.py",
|
||||
"607": "__init__.py",
|
||||
"608": "leave.py",
|
||||
"609": "menu.py",
|
||||
"610": "base_data.py",
|
||||
"611": "freight.py",
|
||||
"612": "__init__.py",
|
||||
"613": "inventory.py",
|
||||
"614": "io_order.py",
|
||||
"615": "plan.py",
|
||||
"616": "transport_address.py",
|
||||
"617": "transport.py",
|
||||
"618": "vehicle_maintenance.py",
|
||||
"619": "vehicle_recharge.py",
|
||||
"620": "vehicle_urea.py",
|
||||
"621": "user.py",
|
||||
"622": "workflow.py",
|
||||
"623": "advance.py",
|
||||
"624": "attachment.py",
|
||||
"625": "base.py",
|
||||
"626": "expense_item.py",
|
||||
"627": "storage.py",
|
||||
"628": "workflow.py",
|
||||
"636": "customer_pool.py",
|
||||
"638": "__init__.py"
|
||||
}
|
||||
@@ -0,0 +1,771 @@
|
||||
# Graph Report - backend (2026-08-01)
|
||||
|
||||
## Corpus Check
|
||||
- 298 files · ~205,953 words
|
||||
- Verdict: corpus is large enough that graph structure adds value.
|
||||
|
||||
## Summary
|
||||
- 4530 nodes · 15410 edges · 531 communities (514 shown, 17 thin omitted)
|
||||
- Extraction: 97% EXTRACTED · 3% INFERRED · 0% AMBIGUOUS · INFERRED: 411 edges (avg confidence: 0.53)
|
||||
- Token cost: 0 input · 0 output
|
||||
|
||||
## Graph Freshness
|
||||
- Built from commit: `98f33644`
|
||||
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
|
||||
- Run `graphify update .` after code changes (no API cost).
|
||||
|
||||
## Community Hubs (Navigation)
|
||||
- [[_COMMUNITY_Community 0|Community 0]]
|
||||
- [[_COMMUNITY_Community 1|Community 1]]
|
||||
- [[_COMMUNITY_Community 2|Community 2]]
|
||||
- [[_COMMUNITY_Community 3|Community 3]]
|
||||
- [[_COMMUNITY_Community 4|Community 4]]
|
||||
- [[_COMMUNITY_Community 5|Community 5]]
|
||||
- [[_COMMUNITY_Community 6|Community 6]]
|
||||
- [[_COMMUNITY_Community 7|Community 7]]
|
||||
- [[_COMMUNITY_Community 8|Community 8]]
|
||||
- [[_COMMUNITY_Community 11|Community 11]]
|
||||
- [[_COMMUNITY_Community 12|Community 12]]
|
||||
- [[_COMMUNITY_Community 13|Community 13]]
|
||||
- [[_COMMUNITY_Community 14|Community 14]]
|
||||
- [[_COMMUNITY_Community 15|Community 15]]
|
||||
- [[_COMMUNITY_Community 16|Community 16]]
|
||||
- [[_COMMUNITY_Community 18|Community 18]]
|
||||
- [[_COMMUNITY_Community 19|Community 19]]
|
||||
- [[_COMMUNITY_Community 21|Community 21]]
|
||||
- [[_COMMUNITY_Community 22|Community 22]]
|
||||
- [[_COMMUNITY_Community 23|Community 23]]
|
||||
- [[_COMMUNITY_Community 24|Community 24]]
|
||||
- [[_COMMUNITY_Community 25|Community 25]]
|
||||
- [[_COMMUNITY_Community 26|Community 26]]
|
||||
- [[_COMMUNITY_Community 27|Community 27]]
|
||||
- [[_COMMUNITY_Community 28|Community 28]]
|
||||
- [[_COMMUNITY_Community 29|Community 29]]
|
||||
- [[_COMMUNITY_Community 30|Community 30]]
|
||||
- [[_COMMUNITY_Community 33|Community 33]]
|
||||
- [[_COMMUNITY_Community 34|Community 34]]
|
||||
- [[_COMMUNITY_Community 35|Community 35]]
|
||||
- [[_COMMUNITY_Community 36|Community 36]]
|
||||
- [[_COMMUNITY_Community 37|Community 37]]
|
||||
- [[_COMMUNITY_Community 38|Community 38]]
|
||||
- [[_COMMUNITY_Community 39|Community 39]]
|
||||
- [[_COMMUNITY_Community 42|Community 42]]
|
||||
- [[_COMMUNITY_Community 43|Community 43]]
|
||||
- [[_COMMUNITY_Community 44|Community 44]]
|
||||
- [[_COMMUNITY_Community 45|Community 45]]
|
||||
- [[_COMMUNITY_Community 46|Community 46]]
|
||||
- [[_COMMUNITY_Community 48|Community 48]]
|
||||
- [[_COMMUNITY_Community 49|Community 49]]
|
||||
- [[_COMMUNITY_Community 50|Community 50]]
|
||||
- [[_COMMUNITY_Community 51|Community 51]]
|
||||
- [[_COMMUNITY_Community 52|Community 52]]
|
||||
- [[_COMMUNITY_Community 53|Community 53]]
|
||||
- [[_COMMUNITY_Community 54|Community 54]]
|
||||
- [[_COMMUNITY_Community 58|Community 58]]
|
||||
- [[_COMMUNITY_Community 59|Community 59]]
|
||||
- [[_COMMUNITY_Community 60|Community 60]]
|
||||
- [[_COMMUNITY_Community 61|Community 61]]
|
||||
- [[_COMMUNITY_Community 62|Community 62]]
|
||||
- [[_COMMUNITY_Community 63|Community 63]]
|
||||
- [[_COMMUNITY_Community 64|Community 64]]
|
||||
- [[_COMMUNITY_Community 65|Community 65]]
|
||||
- [[_COMMUNITY_Community 66|Community 66]]
|
||||
- [[_COMMUNITY_Community 67|Community 67]]
|
||||
- [[_COMMUNITY_Community 68|Community 68]]
|
||||
- [[_COMMUNITY_Community 69|Community 69]]
|
||||
- [[_COMMUNITY_Community 70|Community 70]]
|
||||
- [[_COMMUNITY_Community 71|Community 71]]
|
||||
- [[_COMMUNITY_Community 72|Community 72]]
|
||||
- [[_COMMUNITY_Community 73|Community 73]]
|
||||
- [[_COMMUNITY_Community 74|Community 74]]
|
||||
- [[_COMMUNITY_Community 77|Community 77]]
|
||||
- [[_COMMUNITY_Community 78|Community 78]]
|
||||
- [[_COMMUNITY_Community 81|Community 81]]
|
||||
- [[_COMMUNITY_Community 83|Community 83]]
|
||||
- [[_COMMUNITY_Community 84|Community 84]]
|
||||
- [[_COMMUNITY_Community 86|Community 86]]
|
||||
- [[_COMMUNITY_Community 87|Community 87]]
|
||||
- [[_COMMUNITY_Any|Any]]
|
||||
- [[_COMMUNITY_pl_ocr_service.py|pl_ocr_service.py]]
|
||||
- [[_COMMUNITY_pl_vehicle_recharge_service.py|pl_vehicle_recharge_service.py]]
|
||||
- [[_COMMUNITY_Community 92|Community 92]]
|
||||
- [[_COMMUNITY__invoice_info_by_customer_impl|_invoice_info_by_customer_impl]]
|
||||
- [[_COMMUNITY_response.py|response.py]]
|
||||
- [[_COMMUNITY_Community 95|Community 95]]
|
||||
- [[_COMMUNITY__ensure_subpack_slip_no|_ensure_subpack_slip_no]]
|
||||
- [[_COMMUNITY_price_letter_detail|price_letter_detail]]
|
||||
- [[_COMMUNITY_.to_tenant_user_dict|.to_tenant_user_dict]]
|
||||
- [[_COMMUNITY_dept.py|dept.py]]
|
||||
- [[_COMMUNITY_apply_crm_create_by_scope|apply_crm_create_by_scope]]
|
||||
- [[_COMMUNITY_list_customers|list_customers]]
|
||||
- [[_COMMUNITY_Community 106|Community 106]]
|
||||
- [[_COMMUNITY_apply_pl_scope|apply_pl_scope]]
|
||||
- [[_COMMUNITY_Community 109|Community 109]]
|
||||
- [[_COMMUNITY_Base|Base]]
|
||||
- [[_COMMUNITY_Community 111|Community 111]]
|
||||
- [[_COMMUNITY_storage.py|storage.py]]
|
||||
- [[_COMMUNITY_quote_share_log.py|quote_share_log.py]]
|
||||
- [[_COMMUNITY_db_models.py|db_models.py]]
|
||||
- [[_COMMUNITY_site.py|site.py]]
|
||||
- [[_COMMUNITY_Community 116|Community 116]]
|
||||
- [[_COMMUNITY_Community 118|Community 118]]
|
||||
- [[_COMMUNITY_Community 119|Community 119]]
|
||||
- [[_COMMUNITY_Community 122|Community 122]]
|
||||
- [[_COMMUNITY_Community 125|Community 125]]
|
||||
- [[_COMMUNITY_customer_pool.py|customer_pool.py]]
|
||||
- [[_COMMUNITY_Community 127|Community 127]]
|
||||
- [[_COMMUNITY_plan.py|plan.py]]
|
||||
- [[_COMMUNITY_BaseStorage|BaseStorage]]
|
||||
- [[_COMMUNITY_datetime_parse.py|datetime_parse.py]]
|
||||
- [[_COMMUNITY_LocalStorage|LocalStorage]]
|
||||
- [[_COMMUNITY_crm_supplier_hazard.py|crm_supplier_hazard.py]]
|
||||
- [[_COMMUNITY_Community 134|Community 134]]
|
||||
- [[_COMMUNITY_post.py|post.py]]
|
||||
- [[_COMMUNITY_Community 136|Community 136]]
|
||||
- [[_COMMUNITY_statistics.py|statistics.py]]
|
||||
- [[_COMMUNITY_Community 138|Community 138]]
|
||||
- [[_COMMUNITY_Community 139|Community 139]]
|
||||
- [[_COMMUNITY_clue_statistics.py|clue_statistics.py]]
|
||||
- [[_COMMUNITY_SysEthnic|SysEthnic]]
|
||||
- [[_COMMUNITY_SysApiRequestLog|SysApiRequestLog]]
|
||||
- [[_COMMUNITY_minio_url.py|minio_url.py]]
|
||||
- [[_COMMUNITY___init__.py|__init__.py]]
|
||||
- [[_COMMUNITY_Community 147|Community 147]]
|
||||
- [[_COMMUNITY_ContractListQuery|ContractListQuery]]
|
||||
- [[_COMMUNITY_Community 151|Community 151]]
|
||||
- [[_COMMUNITY_Community 152|Community 152]]
|
||||
- [[_COMMUNITY_web_login_rsa_public_key|web_login_rsa_public_key]]
|
||||
- [[_COMMUNITY_get_client_ip|get_client_ip]]
|
||||
- [[_COMMUNITY_context.py|context.py]]
|
||||
- [[_COMMUNITY_register_local_callback|register_local_callback]]
|
||||
- [[_COMMUNITY_backfill_crm_contract_attachment_urls.py|backfill_crm_contract_attachment_urls.py]]
|
||||
- [[_COMMUNITY_BaseStorage|BaseStorage]]
|
||||
- [[_COMMUNITY_login_crypto.py|login_crypto.py]]
|
||||
- [[_COMMUNITY_apply_pl_scope|apply_pl_scope]]
|
||||
- [[_COMMUNITY_codeblock1 (iflytekshanxia2.0backend)|code:block1 (/iflytek/shanxia2.0/backend)]]
|
||||
- [[_COMMUNITY_test_quote_approver_setting.py|test_quote_approver_setting.py]]
|
||||
- [[_COMMUNITY_LocalStorage|LocalStorage]]
|
||||
- [[_COMMUNITY_MinIOStorage|MinIOStorage]]
|
||||
- [[_COMMUNITY_SysEthnic|SysEthnic]]
|
||||
- [[_COMMUNITY_datetime_parse.py|datetime_parse.py]]
|
||||
- [[_COMMUNITY___init__.py|__init__.py]]
|
||||
- [[_COMMUNITY_menu.py|menu.py]]
|
||||
- [[_COMMUNITY_crm_quote_pdf.py|crm_quote_pdf.py]]
|
||||
- [[_COMMUNITY_shanxia2.0 后端骨架(FastAPI)|shanxia2.0 后端骨架(FastAPI)]]
|
||||
- [[_COMMUNITY_api_crypto_middleware.py|api_crypto_middleware.py]]
|
||||
- [[_COMMUNITY_order_status_compat.py|order_status_compat.py]]
|
||||
- [[_COMMUNITY_shanxia2.0 Backend 生产部署说明|shanxia2.0 Backend 生产部署说明]]
|
||||
- [[_COMMUNITY_db_collation.py|db_collation.py]]
|
||||
- [[_COMMUNITY_一次上传一条发票:多文件挂同一档案,金额为总额,只写一条额度流水。|一次上传一条发票:多文件挂同一档案,金额为总额,只写一条额度流水。]]
|
||||
- [[_COMMUNITY___init__.py|__init__.py]]
|
||||
- [[_COMMUNITY___init__.py|__init__.py]]
|
||||
- [[_COMMUNITY_codebash (cd pathtoshanxia2.0)|code:bash (cd /path/to/shanxia2.0)]]
|
||||
- [[_COMMUNITY_codebash (.start-prod.sh status 查看状态)|code:bash (./start-prod.sh status # 查看状态)]]
|
||||
- [[_COMMUNITY_codebash (curl -s http127.0.0.19111health)|code:bash (curl -s http://127.0.0.1:9111/health)]]
|
||||
- [[_COMMUNITY_codebash (cd iflytekshanxia2.0backend)|code:bash (cd /iflytek/shanxia2.0/backend)]]
|
||||
- [[_COMMUNITY_codebash (python -m venv venv)|code:bash (python -m venv venv)]]
|
||||
- [[_COMMUNITY_codebash (pip install -r requirements.txt)|code:bash (pip install -r requirements.txt)]]
|
||||
- [[_COMMUNITY_codeenv ( 数据库配置)|code:env (# 数据库配置)]]
|
||||
- [[_COMMUNITY_codebash (uvicorn app.mainapp --reload --port 8000)|code:bash (uvicorn app.main:app --reload --port 8000)]]
|
||||
- [[_COMMUNITY_BaseModel|BaseModel]]
|
||||
- [[_COMMUNITY_PlanResourceSnapshotMixin|PlanResourceSnapshotMixin]]
|
||||
|
||||
## God Nodes (most connected - your core abstractions)
|
||||
1. `BaseModel` - 441 edges
|
||||
2. `get_tenant_no()` - 349 edges
|
||||
3. `DbUserContext` - 254 edges
|
||||
4. `success_response()` - 253 edges
|
||||
5. `error_response()` - 235 edges
|
||||
6. `generate_uuid()` - 193 edges
|
||||
7. `BxUserContext` - 183 edges
|
||||
8. `crm_user_info()` - 137 edges
|
||||
9. `assert_row_deletable_by_creator()` - 83 edges
|
||||
10. `ApiResponse` - 79 edges
|
||||
|
||||
## Surprising Connections (you probably didn't know these)
|
||||
- `BackfillRule` --uses--> `Settings` [INFERRED]
|
||||
scripts/migrate_crm_sys_oss.py → app/core/config.py
|
||||
- `MigrationStats` --uses--> `Settings` [INFERRED]
|
||||
scripts/migrate_bx_data.py → app/core/config.py
|
||||
- `StepStats` --uses--> `Settings` [INFERRED]
|
||||
scripts/migrate_bx_data.py → app/core/config.py
|
||||
- `MigrationStats` --uses--> `Settings` [INFERRED]
|
||||
scripts/migrate_crm_clue.py → app/core/config.py
|
||||
- `StepStats` --uses--> `Settings` [INFERRED]
|
||||
scripts/migrate_crm_clue.py → app/core/config.py
|
||||
|
||||
## Import Cycles
|
||||
- None detected.
|
||||
|
||||
## Communities (531 total, 17 thin omitted)
|
||||
|
||||
### Community 0 - "Community 0"
|
||||
Cohesion: 0.10
|
||||
Nodes (39): _build_public_page(), _build_site_product_read(), _format_file_size(), get_public_site(), _get_site_link_by_token(), _parse_feedback_images(), Session, UploadFile (+31 more)
|
||||
|
||||
### Community 1 - "Community 1"
|
||||
Cohesion: 0.15
|
||||
Nodes (19): ProcessDefCreateBody, ProcessDefUpdateBody, PublishVersionBody, 审批意见:前端传 commentText,兼容 comment。, StartInstanceBody, TaskActionBody, 流程定义版本 wf_process_version, 流程实例 wf_process_instance (+11 more)
|
||||
|
||||
### Community 2 - "Community 2"
|
||||
Cohesion: 0.30
|
||||
Nodes (12): PlCarrier, assert_carrier_eligible_for_transport(), carrier_agreement_expired(), carrier_audit_approved(), carrier_eligible_for_transport(), carrier_qualification_complete(), date, 资质附件均为选填,保留函数签名供兼容调用。 (+4 more)
|
||||
|
||||
### Community 3 - "Community 3"
|
||||
Cohesion: 0.07
|
||||
Nodes (103): attachment_download_url(), attachment_preview_url(), AttachmentDownloadUrlBody, create_dept(), create_dict_data(), create_dict_type(), create_menu(), create_post() (+95 more)
|
||||
|
||||
### Community 4 - "Community 4"
|
||||
Cohesion: 0.12
|
||||
Nodes (61): _apply_list_filters(), assign_opportunities(), batch_update_expect_date(), batch_update_stage(), change_stage(), create_opportunity(), _decimal_value(), _enrich_user_names() (+53 more)
|
||||
|
||||
### Community 5 - "Community 5"
|
||||
Cohesion: 0.20
|
||||
Nodes (24): count_source_rows(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), load_runtime_settings(), load_settings_env(), load_user_mapping() (+16 more)
|
||||
|
||||
### Community 6 - "Community 6"
|
||||
Cohesion: 0.12
|
||||
Nodes (51): _generate_notify_message(), PlPlan, PlPlanProduct, PlPlanTransport, 计划产品明细表 pl_plan_product, 计划-运输关联表 pl_plan_transport, PlanArchiveFreightBlock, PlanArchiveProductItem (+43 more)
|
||||
|
||||
### Community 7 - "Community 7"
|
||||
Cohesion: 0.12
|
||||
Nodes (36): get_password_hash(), Namespace, build_merged_users(), bx_role_labels(), check_minio(), check_mysql(), check_redis(), _clip() (+28 more)
|
||||
|
||||
### Community 8 - "Community 8"
|
||||
Cohesion: 0.10
|
||||
Nodes (51): _build_quote_filters(), calculate_quote_amount(), create_new_version(), create_quote(), _decimal_value(), _enrich_contact_fields(), _enrich_supplier_name(), _ensure_quote_in_scope() (+43 more)
|
||||
|
||||
### Community 11 - "Community 11"
|
||||
Cohesion: 0.15
|
||||
Nodes (40): PlVehicleUreaApply, PlVehicleUreaDisburse, 车辆尿素申请 pl_vehicle_urea_apply, 车辆尿素发放登记 pl_vehicle_urea_disburse, Decimal, VehicleUreaApplyCreate, VehicleUreaApplyRead, VehicleUreaApplyUpdate (+32 more)
|
||||
|
||||
### Community 12 - "Community 12"
|
||||
Cohesion: 0.05
|
||||
Nodes (71): contact_add(), contact_delete(), contact_update(), customer_create(), customer_delete(), customer_update(), customer_name_exists(), BaseModel (+63 more)
|
||||
|
||||
### Community 13 - "Community 13"
|
||||
Cohesion: 0.17
|
||||
Nodes (29): create_instance(), create_process_def(), create_task(), get_all_tasks_for_business(), get_instance(), get_latest_version(), get_nodes_by_version(), get_pending_tasks_by_node() (+21 more)
|
||||
|
||||
### Community 14 - "Community 14"
|
||||
Cohesion: 0.16
|
||||
Nodes (32): check_document_expiry(), document_reminder_stats(), get_inventory_ledger(), get_summary(), list_document_reminders(), list_transactions(), list_warnings(), date (+24 more)
|
||||
|
||||
### Community 15 - "Community 15"
|
||||
Cohesion: 0.11
|
||||
Nodes (16): BxReimburseApply, BxReimburseApproval, BxReimburseDetail, BxReimburseInvoice, BxReimburseOffset, 报销单勾选的发票附件(同一发票可勾选多个附件,各占一条)。, ReimburseApplyOut, ReimburseInvoiceBriefOut (+8 more)
|
||||
|
||||
### Community 16 - "Community 16"
|
||||
Cohesion: 0.09
|
||||
Nodes (60): cancel_apply(), create_apply(), create_record(), delete_applies(), delete_records(), get_apply(), get_record(), list_applies() (+52 more)
|
||||
|
||||
### Community 18 - "Community 18"
|
||||
Cohesion: 0.09
|
||||
Nodes (38): advance_pending_approval_stats(), advance_status_counts(), approve_advance(), approved_by_me(), create_advance(), delete_advance(), _fill_offset_amounts(), get_advance() (+30 more)
|
||||
|
||||
### Community 19 - "Community 19"
|
||||
Cohesion: 0.09
|
||||
Nodes (48): _avatar_url_field(), _build_user_info_payload(), change_current_user_password(), get_current_user_profile(), _jwt_user_payload(), logout(), public_quote_share(), Request (+40 more)
|
||||
|
||||
### Community 21 - "Community 21"
|
||||
Cohesion: 0.09
|
||||
Nodes (58): _alias_row_to_dict(), _assert_brand_deletable(), barcode_exists(), _brand_row_to_dict(), _build_category_tree(), category_code_exists(), _category_row_to_dict(), _collect_category_with_ancestors() (+50 more)
|
||||
|
||||
### Community 22 - "Community 22"
|
||||
Cohesion: 0.08
|
||||
Nodes (28): parse_admin_request_json(), Any, Request, 运营 Web(/web/admin)JSON 请求体解析。 与 ApiCryptoMiddleware 配合:中间件未改写 body 时,在路由内解密 `{, 读取 JSON body;dict 时走 resolve_request_payload(明文 / 密文 / data 包装)。 Returns:, ApiCryptoError, _b64url_decode(), _b64url_encode() (+20 more)
|
||||
|
||||
### Community 23 - "Community 23"
|
||||
Cohesion: 0.11
|
||||
Nodes (8): convert_heic_to_jpeg(), OSSStorage, process_image_content(), QiniuStorage, 文件存储工具模块 支持多种存储方式: 1. 本地存储 (local) - 默认 2. 七牛云存储 (qiniu) - 推荐生产环境使用 3. 阿里云OSS (, 将 HEIC/HEIF 格式转换为 JPEG 格式, 处理图片内容,如果是 HEIC 格式则转换为 JPEG, BytesIO
|
||||
|
||||
### Community 24 - "Community 24"
|
||||
Cohesion: 0.16
|
||||
Nodes (27): CRM 合同子模块路由 — 变更/模板/审批独立页, register_contract_sub_routes(), _change_row_to_dict(), create_contract_change(), execute_change(), export_contract_changes(), export_contract_templates(), _get_change_row() (+19 more)
|
||||
|
||||
### Community 25 - "Community 25"
|
||||
Cohesion: 0.09
|
||||
Nodes (70): _build_driver_url(), get_plan_arrange_info(), 获取计划已安排运输后的司机链接与地磅出入库关联(供查看弹窗复制简述/二维码)。, _apply_plan_logistics_from_payload(), _apply_transport_arrange_fields(), arrange_transport(), _assert_vehicle_carrier_eligible(), _build_arrange_result() (+62 more)
|
||||
|
||||
### Community 26 - "Community 26"
|
||||
Cohesion: 0.15
|
||||
Nodes (32): register_contract_routes(), batch_assign_owner(), batch_submit_contract_approval(), download_contract_pdf(), _ensure_contract_in_scope(), finish_contract(), get_contract(), get_contract_detail() (+24 more)
|
||||
|
||||
### Community 27 - "Community 27"
|
||||
Cohesion: 0.19
|
||||
Nodes (28): _apply_plan_product_update(), _apply_plan_update(), AutoGenerateMonthlyResult, _base_query(), create_plan(), delete_plan(), _generate_plan_no(), get_plan() (+20 more)
|
||||
|
||||
### Community 28 - "Community 28"
|
||||
Cohesion: 0.11
|
||||
Nodes (57): PlFreightSettlement, PlFreightSettlementApproval, PlFreightSettlementTransport, PlRemittanceVoucher, 运费结算表 pl_freight_settlement, 运费结算审批 pl_freight_settlement_approval, 结算单-运输关联表 pl_freight_settlement_transport, 汇款凭证表 pl_remittance_voucher (+49 more)
|
||||
|
||||
### Community 29 - "Community 29"
|
||||
Cohesion: 0.24
|
||||
Nodes (11): _plan_type_to_business_type(), 按运输计划单据类型解析地磅往来单位(不区分 CRM 合同类型)。 - 入库单 inbound:供方 → party_a,其次 supplier_nam, resolve_partner_company(), _normalize_plan_date(), date, Session, 运输计划 · 工厂现场链接(与地磅出入库单解耦), 入库/出库安排运输:按产品生成现场链接(不写 db_inbound_outbound)。 (+3 more)
|
||||
|
||||
### Community 30 - "Community 30"
|
||||
Cohesion: 0.13
|
||||
Nodes (18): menu_badge_stats(), Session, Session, recognize_cert(), RecognizeCertBody, RecognizeCertResult, _auto_fill_create_dept_id(), get_db() (+10 more)
|
||||
|
||||
### Community 33 - "Community 33"
|
||||
Cohesion: 0.20
|
||||
Nodes (15): bind_biz(), delete_attachment(), download_file(), list_by_biz(), _parse_id_list(), preview_attachment(), Session, UploadFile (+7 more)
|
||||
|
||||
### Community 34 - "Community 34"
|
||||
Cohesion: 0.19
|
||||
Nodes (31): assign_customer(), batch_claim_customers(), check_duplicate_customer(), claim_customer(), create_customer(), deal_customer(), _enrich_user_names(), _ensure_customer_in_scope() (+23 more)
|
||||
|
||||
### Community 35 - "Community 35"
|
||||
Cohesion: 0.18
|
||||
Nodes (36): clear_portal_workflow(), _dec(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), load_bx_user_mapping(), load_runtime_settings() (+28 more)
|
||||
|
||||
### Community 36 - "Community 36"
|
||||
Cohesion: 0.20
|
||||
Nodes (22): approve_task(), _assert_wf_instance_in_scope(), _business_type_label(), create_process_def(), _enrich_task_list(), get_business_all_tasks(), _get_business_form_fields(), get_designer_data() (+14 more)
|
||||
|
||||
### Community 37 - "Community 37"
|
||||
Cohesion: 0.13
|
||||
Nodes (29): _actor_ids(), _actor_names(), assert_row_deletable_by_creator(), assert_rows_deletable_by_creator(), can_delete_by_creator(), is_super_admin(), normalize_user_info(), Any (+21 more)
|
||||
|
||||
### Community 38 - "Community 38"
|
||||
Cohesion: 0.12
|
||||
Nodes (28): PlInventory, PlInventoryTransaction, 库存流水表 pl_inventory_transaction, load_brand_qtys_by_plan_product_ids(), persist_plan_product_brand_qtys(), Any, Decimal, Session (+20 more)
|
||||
|
||||
### Community 39 - "Community 39"
|
||||
Cohesion: 0.22
|
||||
Nodes (28): assign_clues(), check_convert(), check_duplicate_clue(), convert_clue(), create_clue(), _enrich_user_names(), _ensure_clue_in_scope(), get_clue() (+20 more)
|
||||
|
||||
### Community 42 - "Community 42"
|
||||
Cohesion: 0.19
|
||||
Nodes (32): _dec(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), load_oss_id_to_url(), load_runtime_settings(), load_settings_env() (+24 more)
|
||||
|
||||
### Community 43 - "Community 43"
|
||||
Cohesion: 0.10
|
||||
Nodes (32): create_invoice(), delete_invoice(), get_mine_wallet_summary(), list_invoice_ledgers(), list_invoice_wallets(), list_invoices(), Request, Session (+24 more)
|
||||
|
||||
### Community 44 - "Community 44"
|
||||
Cohesion: 0.15
|
||||
Nodes (14): _db_pool_keepalive_loop(), _error_json(), lifespan(), Exception, Request, 数据库异常:返回 JSON 并保留 CORS 头,避免浏览器只报跨域错误, 未捕获异常:统一 JSON 500,确保跨域场景下仍有 Access-Control-Allow-Origin, 定期 ping MySQL,避免远程库/NAT 回收空闲 TCP 后首请求长时间卡住。 (+6 more)
|
||||
|
||||
### Community 45 - "Community 45"
|
||||
Cohesion: 0.17
|
||||
Nodes (24): create_personnel(), list_personnel(), list_vehicles(), _personnel_to_detail(), _personnel_to_read(), update_personnel(), PlCert, CertRead (+16 more)
|
||||
|
||||
### Community 46 - "Community 46"
|
||||
Cohesion: 0.28
|
||||
Nodes (17): _address_to_read(), _build_address_query(), create_transport_address(), delete_transport_address(), _get_address(), get_transport_address(), list_all_transport_addresses(), list_transport_addresses() (+9 more)
|
||||
|
||||
### Community 48 - "Community 48"
|
||||
Cohesion: 0.22
|
||||
Nodes (26): _apply_inventory_product_update(), _apply_inventory_update(), _base_query(), create_inventory(), create_link_for_plan(), delete_inventory(), _generate_batch_no(), _get_inventory() (+18 more)
|
||||
|
||||
### Community 49 - "Community 49"
|
||||
Cohesion: 0.07
|
||||
Nodes (35): list_ethnics(), Any, Session, 民族码表 CRUD(全局 sys_ethnic), _row_to_dict(), SoftDeleteModel, BizAttachment, 数据模型(与 docs/sql/shanxia2.0.sql 全库导出基线对齐的核心系统表) (+27 more)
|
||||
|
||||
### Community 50 - "Community 50"
|
||||
Cohesion: 0.11
|
||||
Nodes (37): approve_settlement_legacy(), cancel_settlement(), create_settlement(), delete_settlement(), disburse_settlement(), _generate_settlement_no(), get_settlement(), get_settlement_approval_flow() (+29 more)
|
||||
|
||||
### Community 51 - "Community 51"
|
||||
Cohesion: 0.14
|
||||
Nodes (24): PlTransportProductBrandQty, PlTransportProductFeedback, 运输单按产品回传 pl_transport_product_feedback, 运输单产品回传品牌数量 pl_transport_product_brand_qty, _dict_field(), format_brand_qty_summary(), load_transport_driver_product_feedbacks(), persist_io_item_brand_qtys() (+16 more)
|
||||
|
||||
### Community 52 - "Community 52"
|
||||
Cohesion: 0.18
|
||||
Nodes (29): _dec(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), load_oss_id_to_url(), load_runtime_settings(), load_settings_env() (+21 more)
|
||||
|
||||
### Community 53 - "Community 53"
|
||||
Cohesion: 0.10
|
||||
Nodes (36): 报销模块认证适配:门户 JWT 用户 → bx 业务上下文。, filter_current_cycle_approvals(), 审批轮次过滤 — 驳回后重新提交时,进度接口仅展示当前轮次。, 审批进度仅展示当前轮次;完整历史由 get_approval_history 返回。, 报销模块 Service 基类 — 租户隔离 + UUID + 软删除。, assert_bx_attachment_biz_in_scope(), assert_bx_biz_in_scope(), assert_bx_row_in_scope() (+28 more)
|
||||
|
||||
### Community 54 - "Community 54"
|
||||
Cohesion: 0.26
|
||||
Nodes (23): _get_by_plan(), get_inbound(), get_inbound_by_plan(), _get_order(), get_outbound(), get_outbound_by_plan(), inbound_site_url(), list_inbound() (+15 more)
|
||||
|
||||
### Community 58 - "Community 58"
|
||||
Cohesion: 0.06
|
||||
Nodes (55): operlog_detail(), operlog_list(), Session, 操作日志分页列表(基于 API 请求日志适配 RuoYi OperLog 结构), _user_info(), ApiRequestLogMiddleware, _claims_from_auth(), _format_body_bytes() (+47 more)
|
||||
|
||||
### Community 59 - "Community 59"
|
||||
Cohesion: 0.20
|
||||
Nodes (28): _dec(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), load_oss_id_to_url(), load_runtime_settings(), load_settings_env() (+20 more)
|
||||
|
||||
### Community 60 - "Community 60"
|
||||
Cohesion: 0.09
|
||||
Nodes (37): advance_issue_stats(), batch_offset(), export_advance_issues(), issue_advance(), list_advance_issues(), list_advance_offset_records(), list_pending_payments(), list_processed_payments() (+29 more)
|
||||
|
||||
### Community 61 - "Community 61"
|
||||
Cohesion: 0.19
|
||||
Nodes (23): apply_product_snapshot_to_inventory(), apply_product_snapshot_to_plan(), ProductSnapshot, Session, 地磅模块 — CRM 产品解析与快照写入。, 校验 CRM 产品并返回名称/规格快照。新建单据 product_id 必填。, resolve_crm_product(), _create_db_inventory() (+15 more)
|
||||
|
||||
### Community 62 - "Community 62"
|
||||
Cohesion: 0.21
|
||||
Nodes (22): clear_target_products(), count_target_rows(), _dec(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), load_runtime_settings() (+14 more)
|
||||
|
||||
### Community 63 - "Community 63"
|
||||
Cohesion: 0.17
|
||||
Nodes (30): bx_health(), 报销(bx)API 路由聚合 — REST 风格,协议对齐 bx 原系统。, P0 健康检查:验证 bx 路由已挂载且响应协议为 code=0。, approve_reimburse(), approved_by_me(), _build_details_data(), create_reimburse(), delete_reimburse() (+22 more)
|
||||
|
||||
### Community 64 - "Community 64"
|
||||
Cohesion: 0.03
|
||||
Nodes (187): category_add(), category_delete(), category_tree(), category_update(), clue_detail(), clue_export(), clue_follow_detail(), clue_follow_statistics() (+179 more)
|
||||
|
||||
### Community 65 - "Community 65"
|
||||
Cohesion: 0.10
|
||||
Nodes (58): cancel_apply(), create_apply(), create_process(), delete_applies(), delete_processes(), get_apply(), get_process(), list_applies() (+50 more)
|
||||
|
||||
### Community 66 - "Community 66"
|
||||
Cohesion: 0.14
|
||||
Nodes (25): create_workflow(), delete_workflow(), get_workflow(), get_workflow_by_biz_type(), list_workflows(), Session, save_workflow_nodes(), update_workflow() (+17 more)
|
||||
|
||||
### Community 67 - "Community 67"
|
||||
Cohesion: 0.08
|
||||
Nodes (25): clue_assign(), clue_convert(), clue_create(), clue_delete(), clue_follow_create(), clue_follow_delete(), clue_follow_list(), clue_follow_update() (+17 more)
|
||||
|
||||
### Community 68 - "Community 68"
|
||||
Cohesion: 0.22
|
||||
Nodes (19): register_quote_routes(), 接口菜单权限守卫。super_admin 直接放行。, require_menu_code(), accept_quote(), generate_pdf(), generate_share(), _get_quote_in_scope(), get_template() (+11 more)
|
||||
|
||||
### Community 69 - "Community 69"
|
||||
Cohesion: 0.15
|
||||
Nodes (27): _append_user_scope_conditions(), apply_data_scope(), assert_row_in_data_scope(), _collect_scope_ids(), _get_child_dept_ids(), get_user_dept_id(), Session, 数据权限过滤(V0.3:基于角色 data_scope 的查询级行级过滤) data_scope 值: 1 = 全部数据 2 = 本部门数据 3 (+19 more)
|
||||
|
||||
### Community 70 - "Community 70"
|
||||
Cohesion: 0.39
|
||||
Nodes (10): create_post(), get_post(), list_posts(), post_code_exists(), Any, Session, _row_to_dict(), soft_delete_post() (+2 more)
|
||||
|
||||
### Community 71 - "Community 71"
|
||||
Cohesion: 0.11
|
||||
Nodes (32): CRM 报价单 API 路由 — 挂接到 crm.router, QuoteApproverSettingSaveBody, QuoteAcceptBody, QuoteApprovalApproveBody, QuoteApprovalListBody, QuoteApprovalRefreshNamesBody, QuoteApprovalWithdrawBody, QuoteCreateBody (+24 more)
|
||||
|
||||
### Community 72 - "Community 72"
|
||||
Cohesion: 0.19
|
||||
Nodes (28): apply_biz_backfill(), BackfillRule, BizRef, build_biz_index(), count_target_rows(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows() (+20 more)
|
||||
|
||||
### Community 73 - "Community 73"
|
||||
Cohesion: 0.20
|
||||
Nodes (25): derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), load_oss_id_to_url(), load_runtime_settings(), load_settings_env(), load_user_mapping() (+17 more)
|
||||
|
||||
### Community 74 - "Community 74"
|
||||
Cohesion: 0.35
|
||||
Nodes (11): create_dict_type(), dict_type_code_exists(), get_dict_type(), list_dict_types(), Any, Session, 字典类型 CRUD(sys_dict_type), _row_to_dict() (+3 more)
|
||||
|
||||
### Community 77 - "Community 77"
|
||||
Cohesion: 0.11
|
||||
Nodes (12): get_wechat_pay_client(), get_wechat_pay_v2(), Any, 微信支付工具模块 支持微信支付 V3 API 文档参考: https://pay.weixin.qq.com/docs/v3/apis/index.html, 统一下单接口 (V3) Args: out_trade_no: 商户订单号 total_amo, 申请退款 Args: out_trade_no: 商户订单号 total_amount: 订单, 小程序调起支付的二次签名参数(与公众号 JSAPI 规则一致), 统一下单 (V2 API) Args: out_trade_no: 商户订单号 total_f (+4 more)
|
||||
|
||||
### Community 78 - "Community 78"
|
||||
Cohesion: 0.16
|
||||
Nodes (26): clear_target_categories(), compute_level(), count_target_rows(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), load_runtime_settings() (+18 more)
|
||||
|
||||
### Community 81 - "Community 81"
|
||||
Cohesion: 0.13
|
||||
Nodes (19): API 路由包 — 山峡化工生产一体化门户, create_leave(), delete_leave(), get_leave(), _leave_local_callback(), leave_workflow_callback(), list_leaves(), Session (+11 more)
|
||||
|
||||
### Community 83 - "Community 83"
|
||||
Cohesion: 0.15
|
||||
Nodes (30): _advance_contract_after_approval(), approve_contract(), approve_contract_by_contract_id(), approve_pass(), approve_reject(), approve_transfer(), _build_approval_list_filters(), can_user_approve() (+22 more)
|
||||
|
||||
### Community 84 - "Community 84"
|
||||
Cohesion: 0.23
|
||||
Nodes (23): build_id_maps(), clip_text(), count_rows(), export_mapping(), fetch_rows(), legacy_id_to_str(), main(), map_id() (+15 more)
|
||||
|
||||
### Community 86 - "Community 86"
|
||||
Cohesion: 0.16
|
||||
Nodes (30): _apply_weighing_product_update(), _apply_weighing_update(), _base_query(), create_weighing(), delete_weighing(), _get_weighing(), get_weighing_related(), list_weighing() (+22 more)
|
||||
|
||||
### Community 87 - "Community 87"
|
||||
Cohesion: 0.20
|
||||
Nodes (19): approve_quote(), can_user_approve(), create_approval_records(), delete_pending_approvals(), get_quote_approval(), list_quote_approvals(), Any, Session (+11 more)
|
||||
|
||||
### Community 88 - "Any"
|
||||
Cohesion: 0.18
|
||||
Nodes (31): _dec(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), legacy_parent_id(), load_oss_id_to_url(), load_runtime_settings() (+23 more)
|
||||
|
||||
### Community 89 - "pl_ocr_service.py"
|
||||
Cohesion: 0.19
|
||||
Nodes (22): PlOcrUsageMonth, 阿里云 OCR 月度调用计数 pl_ocr_usage_month(按自然月全局统计), assert_quota_available(), _build_ocr_client(), compress_image_bytes(), current_usage_month(), _first_attachment_url(), get_or_create_usage_row() (+14 more)
|
||||
|
||||
### Community 91 - "pl_vehicle_recharge_service.py"
|
||||
Cohesion: 0.10
|
||||
Nodes (58): _apply_contract_entity_fields(), _build_contract_filters(), _build_customer_address(), _calc_amount_with_tax(), _contract_period_key(), contract_row_to_dict(), create_contract(), create_contract_template() (+50 more)
|
||||
|
||||
### Community 92 - "Community 92"
|
||||
Cohesion: 0.15
|
||||
Nodes (27): 验证密码(哈希非 bcrypt 或损坏时返回 False,避免抛错), verify_password(), change_password(), create_user(), get_user(), get_user_with_relations(), _hash_password(), list_post_ids_for_user() (+19 more)
|
||||
|
||||
### Community 93 - "_invoice_info_by_customer_impl"
|
||||
Cohesion: 0.16
|
||||
Nodes (31): create_contact(), get_contact(), list_contacts_by_customer(), _parse_optional_date(), Any, date, Session, _row_to_dict() (+23 more)
|
||||
|
||||
### Community 94 - "response.py"
|
||||
Cohesion: 0.28
|
||||
Nodes (15): create_clue_follow(), delete_clue_follow(), get_clue_follow(), get_follow_statistics(), list_clue_follows(), Any, Session, 更新线索跟进记录,必要时同步线索 next_time (+7 more)
|
||||
|
||||
### Community 95 - "Community 95"
|
||||
Cohesion: 0.35
|
||||
Nodes (11): create_tenant(), get_tenant(), list_tenants(), Any, Session, _row_to_dict(), soft_delete_tenant(), tenant_no_exists() (+3 more)
|
||||
|
||||
### Community 97 - "_ensure_subpack_slip_no"
|
||||
Cohesion: 0.10
|
||||
Nodes (53): approve_carrier(), bind_vehicle_driver(), create_carrier(), create_driver(), create_vehicle(), delete_carrier(), delete_driver(), delete_personnel() (+45 more)
|
||||
|
||||
### Community 100 - "price_letter_detail"
|
||||
Cohesion: 0.27
|
||||
Nodes (15): Settings, BaseSettings, apply_renumber(), build_mapping(), fetch_products(), format_new_product_no(), load_settings_env(), main() (+7 more)
|
||||
|
||||
### Community 101 - ".to_tenant_user_dict"
|
||||
Cohesion: 0.23
|
||||
Nodes (12): CRM 模块 Excel 导出列定义(字段与字典类型对齐 Java *Vo @ExcelDictFormat)。, _build_workbook(), DictLabelResolver, export_rows_to_xlsx_response(), ExportColumn, Any, Excel 导出工具:集中处理字典 code → label(对齐 Java ExcelDictConvert / ExcelDictPreloadConver, CRM 列表导出统一入口:表头 + 行数据 + 字典 label 转换 + 文件流响应。 (+4 more)
|
||||
|
||||
### Community 102 - "dept.py"
|
||||
Cohesion: 0.33
|
||||
Nodes (13): _build_tree(), _cascade_soft_delete(), _collect_with_ancestors(), create_dept(), get_dept(), list_depts(), list_depts_tree(), Any (+5 more)
|
||||
|
||||
### Community 104 - "apply_crm_create_by_scope"
|
||||
Cohesion: 0.24
|
||||
Nodes (7): camel_to_snake(), CRUDBase, get_model_attr(), HistoryCRUD, Any, Session, ModelType
|
||||
|
||||
### Community 105 - "list_customers"
|
||||
Cohesion: 0.12
|
||||
Nodes (15): pool_rule_add(), pool_rule_delete(), pool_rule_detail(), pool_rule_execute(), pool_rule_list(), pool_rule_update(), pool_rule_update_status(), PoolRuleAddBody (+7 more)
|
||||
|
||||
### Community 106 - "Community 106"
|
||||
Cohesion: 0.17
|
||||
Nodes (16): parse_resource_snapshot(), Any, 运输计划 - 安排运输时车辆/人员/证件资源快照 Schema, 从 ORM 文本列或 dict 解析快照。, SnapshotCertRead, SnapshotPersonnelRow, TransportResourceSnapshot, build_personnel_summary() (+8 more)
|
||||
|
||||
### Community 107 - "apply_pl_scope"
|
||||
Cohesion: 0.20
|
||||
Nodes (19): cancel_apply(), create_apply(), create_disburse(), delete_applies(), delete_disburses(), get_apply(), get_disburse(), list_applies() (+11 more)
|
||||
|
||||
### Community 109 - "Community 109"
|
||||
Cohesion: 0.15
|
||||
Nodes (17): create_expense_item(), delete_expense_item(), get_expense_item(), get_expense_item_tree(), Session, update_expense_item(), BxExpenseItem, _coerce_id() (+9 more)
|
||||
|
||||
### Community 110 - "Base"
|
||||
Cohesion: 0.18
|
||||
Nodes (17): get_web_auth_menu_codes(), 获取当前用户菜单权限标识(sys_menu.perms,经角色继承)。, get_user_menu_codes(), Session, 读取用户授权的菜单权限标识集合(与前端 meta.menuCode / perms 对齐)。, _expand_with_ancestor_menu_ids(), _granted_menu_ids_for_user(), iter_user_menu_perm_codes() (+9 more)
|
||||
|
||||
### Community 111 - "Community 111"
|
||||
Cohesion: 0.28
|
||||
Nodes (14): _assert_editable(), _build_public_page(), _format_file_size(), _get_inventory_by_token(), get_public_inventory(), _parse_feedback_images(), Session, UploadFile (+6 more)
|
||||
|
||||
### Community 112 - "storage.py"
|
||||
Cohesion: 0.22
|
||||
Nodes (17): _apply_list_filters(), create_supplier_info(), get_supplier_info(), list_all_supplier_info(), list_supplier_info(), Any, Session, CRM 供方信息 CRUD — 对照 CrmSupplierInfoServiceImpl.java (+9 more)
|
||||
|
||||
### Community 113 - "quote_share_log.py"
|
||||
Cohesion: 0.29
|
||||
Nodes (10): list_share_logs(), _parse_user_agent(), Any, Session, CRM 报价单分享访问日志 CRUD — 对照 CrmQuoteShareLogServiceImpl.java, 简易 User-Agent 解析(替代 Hutool UserAgentUtil), Java: queryLogsByQuoteId, record_visit() (+2 more)
|
||||
|
||||
### Community 114 - "db_models.py"
|
||||
Cohesion: 0.24
|
||||
Nodes (12): DbSequence, 地磅(db)模块数据模型 — 对齐 docs/sql/db_tables.sql, _allocate_inbound_outbound_id(), _ensure_sequence_row(), max_numeric_inbound_outbound_id(), next_inbound_outbound_id(), Session, 出入库单 id 生成 — 租户内纯数字自增,并发安全。 (+4 more)
|
||||
|
||||
### Community 115 - "site.py"
|
||||
Cohesion: 0.33
|
||||
Nodes (8): allocate_next_biz_no(), _format_biz_no(), _prefix_for(), datetime, Session, 业务协议号自动生成:XBCC / JZCC + YYYYMMDD + 6 位序号, 原子分配下一个业务协议号。 须在 create 事务内调用;使用 MySQL LAST_INSERT_ID 保证并发安全。, BizNoKind
|
||||
|
||||
### Community 116 - "Community 116"
|
||||
Cohesion: 0.19
|
||||
Nodes (26): batch_insert_quote_products(), calculate_subtotal(), _decimal_value(), delete_products_by_quote_id(), _ensure_quote_parent_scope(), _get_field(), get_quote_product(), insert_quote_product() (+18 more)
|
||||
|
||||
### Community 118 - "Community 118"
|
||||
Cohesion: 0.22
|
||||
Nodes (22): batch_insert_contract_products(), calculate_subtotal(), _decimal_value(), delete_price_tiers_by_contract_id(), delete_products_by_contract_id(), _get_field(), insert_contract_product(), _letter_oss_ids_to_json() (+14 more)
|
||||
|
||||
### Community 119 - "Community 119"
|
||||
Cohesion: 0.17
|
||||
Nodes (26): CertRow, _cert_row_complete(), collect_required_cert_errors(), _find_cert_for_required(), get_required_personnel_cert_names(), match_required_cert_name(), _missing_field_labels(), Session (+18 more)
|
||||
|
||||
### Community 122 - "Community 122"
|
||||
Cohesion: 0.22
|
||||
Nodes (15): get_storage(), download_file_bytes(), file_stream(), get_access_url(), get_download_url(), get_preview_url(), normalize_storage_path(), 报销附件存储适配 — 复用门户 app.core.storage,对外保持 bx 协议。 (+7 more)
|
||||
|
||||
### Community 125 - "Community 125"
|
||||
Cohesion: 0.06
|
||||
Nodes (70): _build_page(), _get_by_token(), _models(), Session, 运输计划 · 入库/出库单现场免登录公共 API, PlInboundOrder, PlInboundOrderItem, PlInboundOrderItemBrandQty (+62 more)
|
||||
|
||||
### Community 126 - "customer_pool.py"
|
||||
Cohesion: 0.52
|
||||
Nodes (6): apply_updates(), collect_candidates(), main(), parse_name(), Path, write_csv()
|
||||
|
||||
### Community 127 - "Community 127"
|
||||
Cohesion: 0.38
|
||||
Nodes (14): _cargo_stats(), get_dashboard_stats(), get_inbound_by_cargo(), get_inbound_by_partner_cargo(), get_outbound_by_cargo(), get_outbound_by_partner_cargo(), _partner_cargo_stats(), Session (+6 more)
|
||||
|
||||
### Community 128 - "plan.py"
|
||||
Cohesion: 0.07
|
||||
Nodes (72): archive_plan(), cancel_plan(), cancel_plan_wait_notice(), count_plans_by_contract(), create_from_contract(), create_plan(), create_plan_full(), delete_plan() (+64 more)
|
||||
|
||||
### Community 129 - "BaseStorage"
|
||||
Cohesion: 0.29
|
||||
Nodes (14): PlTransportDbLink, 运输-地磅出入库关联 pl_transport_db_link, _apply_plan_quantity_delta(), 再次勾选地磅安排前:软删本运输单此前生成的、仍可作废的出入库单及关联。 - 不删除地磅月计划(可能被其他业务复用) - 若出入库单已完成的/已, _resolve_link_contributed_qty(), revoke_regeneratable_dibang_for_transport(), _subtract_plan_contribution(), test_apply_plan_quantity_delta_accumulates() (+6 more)
|
||||
|
||||
### Community 130 - "datetime_parse.py"
|
||||
Cohesion: 0.36
|
||||
Nodes (15): create_opportunity_follow(), _ensure_parent_opportunity(), get_opportunity_follow(), list_follows_by_opportunity(), list_opportunity_follows(), _parse_optional_datetime(), Any, datetime (+7 more)
|
||||
|
||||
### Community 131 - "LocalStorage"
|
||||
Cohesion: 0.16
|
||||
Nodes (28): get_personnel(), paste_recognize_commit(), PlPersonnel, PlVehiclePersonnel, 车辆-人员关联表 pl_vehicle_personnel, VehiclePersonnelRow, ConflictAction, PasteCommitBody (+20 more)
|
||||
|
||||
### Community 132 - "crm_supplier_hazard.py"
|
||||
Cohesion: 0.14
|
||||
Nodes (23): build_updates(), derive_source_url(), fetch_source_customers(), legacy_id_to_str(), load_oss_map(), load_runtime_settings(), load_settings_env(), main() (+15 more)
|
||||
|
||||
### Community 134 - "Community 134"
|
||||
Cohesion: 0.25
|
||||
Nodes (18): batch_generate_sample_codes(), batch_issue_sample_codes(), batch_revoke_sample_codes(), create_sample_code(), get_sample_code(), list_sample_codes(), _parse_issued_time(), Any (+10 more)
|
||||
|
||||
### Community 135 - "post.py"
|
||||
Cohesion: 0.24
|
||||
Nodes (12): get_last_month_range(), month_range(), date, Session, 地磅计划按月自动生成 — 迁移自 simple-dibang-xiaoshou/auto-create-plan/auto_create_monthly_pla, 按月自动生成采购计划。 - 默认仅在每月 1 日执行(force=True 时跳过该限制,供手动触发验收) - 遍历所有租户,按上月计划模板生, run_auto_generate_monthly_plans(), default_execution_status() (+4 more)
|
||||
|
||||
### Community 136 - "Community 136"
|
||||
Cohesion: 0.22
|
||||
Nodes (16): create_role(), get_role(), get_role_data_scope(), get_role_menu_ids(), list_roles(), Any, Session, 角色 CRUD(sys_role / sys_role_menu / sys_role_data_scope_detail) (+8 more)
|
||||
|
||||
### Community 137 - "statistics.py"
|
||||
Cohesion: 0.45
|
||||
Nodes (11): _customer_base_query(), get_follow_statistics(), get_industry_statistics(), get_level_statistics(), get_overview_statistics(), get_owner_statistics(), get_source_statistics(), get_trend_statistics() (+3 more)
|
||||
|
||||
### Community 138 - "Community 138"
|
||||
Cohesion: 0.33
|
||||
Nodes (19): check_python(), create_venv(), ensure_dirs(), ensure_valid_venv(), ensure_venv_and_runtime(), install_dependencies(), is_running(), log_error() (+11 more)
|
||||
|
||||
### Community 139 - "Community 139"
|
||||
Cohesion: 0.15
|
||||
Nodes (9): 微信消息通知服务 支持微信小程序模板消息推送 使用方式: from app.core.wechat_message import WechatMes, 发送订单状态通知 Args: openid: 用户openid order_n, 发送支付成功通知 Args: openid: 用户openid order_n, 发送配送通知 Args: openid: 配送员openid task_no:, 发送配送时效预警 Args: openid: 配送员openid task_n, 发送退款结果通知 Args: openid: 用户openid order_n, 发送催单通知给配送员 Args: deliver_openid: 配送员openid, 发送模板消息 Args: openid: 用户openid template_ (+1 more)
|
||||
|
||||
### Community 141 - "clue_statistics.py"
|
||||
Cohesion: 0.58
|
||||
Nodes (9): _apply_create_range(), _base_query(), get_clue_level_statistics(), get_clue_overview_statistics(), get_clue_owner_statistics(), get_clue_source_statistics(), get_clue_trend_statistics(), Any (+1 more)
|
||||
|
||||
### Community 143 - "SysEthnic"
|
||||
Cohesion: 0.33
|
||||
Nodes (11): create_leave(), get_leave(), list_leaves(), Any, Session, _row_to_dict(), soft_delete_leave(), update_leave() (+3 more)
|
||||
|
||||
### Community 144 - "SysApiRequestLog"
|
||||
Cohesion: 0.17
|
||||
Nodes (11): price_letter_add(), price_letter_detail(), price_letter_list(), price_letter_update(), price_letter_void(), PriceLetterCreateBody, PriceLetterDetailBody, PriceLetterListBody (+3 more)
|
||||
|
||||
### Community 145 - "minio_url.py"
|
||||
Cohesion: 0.39
|
||||
Nodes (10): create_site(), get_site(), list_sites(), Any, Session, _row_to_dict(), site_code_exists(), soft_delete_site() (+2 more)
|
||||
|
||||
### Community 146 - "__init__.py"
|
||||
Cohesion: 0.16
|
||||
Nodes (28): _enrich_follow_dict(), get_follow(), list_follows(), list_follows_by_customer(), Any, Session, _row_to_dict(), soft_delete_follow() (+20 more)
|
||||
|
||||
### Community 147 - "Community 147"
|
||||
Cohesion: 0.37
|
||||
Nodes (17): check_env(), check_python(), create_venv(), init_env(), install_dependencies(), log_error(), log_info(), log_success() (+9 more)
|
||||
|
||||
### Community 150 - "ContractListQuery"
|
||||
Cohesion: 0.09
|
||||
Nodes (35): CRM 合同 API 路由 — GET/PUT/DELETE/POST 对齐原 Java CRM, ContractApprovalActionBody, ContractApprovalPassBody, ContractApprovalRejectBody, ContractApprovalTransferBody, ContractBatchAssignBody, ContractBatchSubmitBody, ContractChangeCreateBody (+27 more)
|
||||
|
||||
### Community 152 - "Community 152"
|
||||
Cohesion: 0.12
|
||||
Nodes (16): 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方详情 — Java: CrmSupplierInfoController.getInfo, 新增供方 — Java: CrmSupplierInfoController.add, 修改供方 — Java: CrmSupplierInfoController.edit, 删除供方 — Java: CrmSupplierInfoController.remove, supplier_info_add(), supplier_info_delete(), supplier_info_detail() (+8 more)
|
||||
|
||||
### Community 154 - "web_login_rsa_public_key"
|
||||
Cohesion: 0.08
|
||||
Nodes (44): generate_uuid(), get_current_time(), parse_datetime(), datetime, get_pool_record(), list_pool_records(), Any, Session (+36 more)
|
||||
|
||||
### Community 156 - "get_client_ip"
|
||||
Cohesion: 0.27
|
||||
Nodes (11): customer_dealed(), customer_export(), customer_key(), customer_list(), customer_long_time_no_follow(), customer_mine(), customer_to_follow(), customer_week_new() (+3 more)
|
||||
|
||||
### Community 159 - "context.py"
|
||||
Cohesion: 0.35
|
||||
Nodes (10): format_datetime(), datetime_end_of_day(), fmt_date(), fmt_datetime(), inventory_date_to_api(), is_empty_inventory_date(), _parse_site_feedback_images(), date (+2 more)
|
||||
|
||||
### Community 160 - "register_local_callback"
|
||||
Cohesion: 0.60
|
||||
Nodes (5): product_allowed(), test_dual_requires_both(), test_explosive_only_requires_allow_explosive(), test_plain_always_allowed(), test_toxic_only_requires_allow_toxic()
|
||||
|
||||
### Community 161 - "backfill_crm_contract_attachment_urls.py"
|
||||
Cohesion: 0.33
|
||||
Nodes (10): load_oss_map(), load_runtime_settings(), load_settings_env(), main(), Any, Engine, 返回 (新值, 是否发生了 id→url 替换)。, resolve_token() (+2 more)
|
||||
|
||||
### Community 162 - "BaseStorage"
|
||||
Cohesion: 0.22
|
||||
Nodes (3): BaseStorage, 带下载文件名的访问 URL;默认实现为普通访问地址。, 浏览器内联预览 URL;默认实现为普通访问地址。
|
||||
|
||||
### Community 163 - "login_crypto.py"
|
||||
Cohesion: 0.31
|
||||
Nodes (9): decrypt_login_payload(), ensure_initialized(), extract_login_credentials(), get_public_pem(), Any, 登录凭据 RSA 解密(与前端 JSEncrypt PKCS#1 v1.5 兼容)。, Base64 密文 -> JSON 对象(须含 username、password、ts 毫秒时间戳)。, 从请求体解析用户名、密码。 返回 (username, password, error_message);error_message 非空时表示失败。 (+1 more)
|
||||
|
||||
### Community 164 - "apply_pl_scope"
|
||||
Cohesion: 0.38
|
||||
Nodes (11): apply_pl_scope(), Session, PL 模块数据权限辅助(create_by / create_dept_id,无 owner_field)。, build_menu_badge_counts(), count_document_reminder_expired_pending(), count_freight_approval_pending(), count_freight_disburse_pending(), _count_pending_applies() (+3 more)
|
||||
|
||||
### Community 170 - "test_quote_approver_setting.py"
|
||||
Cohesion: 0.28
|
||||
Nodes (14): _assert_users_exist(), get_setting(), join_approver_ids(), parse_approver_ids(), prefill_approver_ids(), Any, Session, 返回可预填的审批人 ID 列表;不可用则空列表。 (+6 more)
|
||||
|
||||
### Community 172 - "MinIOStorage"
|
||||
Cohesion: 0.32
|
||||
Nodes (3): MinIOStorage, MinIO 预签名下载 URL,携带原始文件名。, MinIO 预签名内联预览 URL(PDF 等 iframe 场景)。
|
||||
|
||||
### Community 173 - "SysEthnic"
|
||||
Cohesion: 0.50
|
||||
Nodes (5): assert_products_allowed_for_supplier(), enrich_products_with_hazard_flags(), Any, Session, 批量回填明细行 isToxic/isExplosive(来自产品主数据,供前端切换主体剔除)。
|
||||
|
||||
### Community 174 - "datetime_parse.py"
|
||||
Cohesion: 0.67
|
||||
Nodes (3): parse_optional_plan_datetime(), datetime, 运输计划相关日期时间解析:前端 date-picker 仅传 YYYY-MM-DD。
|
||||
|
||||
### Community 180 - "menu.py"
|
||||
Cohesion: 0.36
|
||||
Nodes (12): _build_tree(), _cascade_soft_delete(), create_menu(), get_menu(), list_menus(), list_menus_tree(), menu_name_exists(), Any (+4 more)
|
||||
|
||||
### Community 206 - "crm_quote_pdf.py"
|
||||
Cohesion: 0.36
|
||||
Nodes (10): build_quote_html(), _default_value(), _format_date(), _format_discount(), _format_money(), generate_quote_pdf(), _get_pdf_styles(), Any (+2 more)
|
||||
|
||||
### Community 207 - "shanxia2.0 后端骨架(FastAPI)"
|
||||
Cohesion: 0.18
|
||||
Nodes (10): 1. 创建虚拟环境, 2. 安装依赖, 3. 配置环境变量, 4. 启动服务, API 路由分组, shanxia2.0 后端骨架(FastAPI), 开发规范, 快速开始 (+2 more)
|
||||
|
||||
### Community 220 - "api_crypto_middleware.py"
|
||||
Cohesion: 0.13
|
||||
Nodes (22): ApiCryptoMiddleware, _claims_from_auth(), _coerce_post_method_override_to_get(), _get_redis_client(), _is_crypto_api(), _json_error_response(), _mark_nonce_once(), _nonce_key() (+14 more)
|
||||
|
||||
### Community 237 - "order_status_compat.py"
|
||||
Cohesion: 0.36
|
||||
Nodes (7): derive_legacy_status_from_three(), derive_three_status_from_legacy(), Decimal, 阶段A兼容:从旧 status 推导三字段。 约束:旧 status 本身混合了支付/配送/订单生命周期,因此只能尽力推断; 若业务侧已有更精确, 阶段A兼容:从三字段反推旧 status(用于旧前端/旧筛选参数兼容)。, ThreeStatus, _to_decimal()
|
||||
|
||||
### Community 239 - "shanxia2.0 Backend 生产部署说明"
|
||||
Cohesion: 0.25
|
||||
Nodes (7): 1. 上传到服务器, 2. 服务器首次启动, 3. 日常运维, 4. 配置文件, 5. 与 Nginx 配合, 6. 常见问题, shanxia2.0 Backend 生产部署说明
|
||||
|
||||
### Community 246 - "db_collation.py"
|
||||
Cohesion: 0.38
|
||||
Nodes (6): eq_unicode(), like_unicode(), MySQL 字符串比较/模糊匹配时统一排序规则,避免 Illegal mix of collations。, 跨表字符串等值比较(JOIN / WHERE)时统一为 utf8mb4_unicode_ci。, 将列显式设为 utf8mb4_unicode_ci。, unicode_ci()
|
||||
|
||||
## Knowledge Gaps
|
||||
- **30 isolated node(s):** `Config`, `Config`, `Config`, `Config`, `Config` (+25 more)
|
||||
These have ≤1 connection - possible missing edges or undocumented components.
|
||||
- **17 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
||||
|
||||
## Suggested Questions
|
||||
_Questions this graph is uniquely positioned to answer:_
|
||||
|
||||
- **Why does `generate_uuid()` connect `web_login_rsa_public_key` to `plan.py`, `datetime_parse.py`, `LocalStorage`, `Community 4`, `Community 134`, `post.py`, `Community 8`, `Community 136`, `Community 6`, `Community 11`, `Community 7`, `Community 13`, `Community 14`, `SysEthnic`, `Community 15`, `minio_url.py`, `__init__.py`, `Community 19`, `Community 18`, `Community 21`, `Community 16`, `Community 24`, `Community 25`, `Community 27`, `Community 28`, `Community 29`, `Community 34`, `Community 37`, `Community 38`, `Community 39`, `test_quote_approver_setting.py`, `Community 43`, `Community 45`, `Community 46`, `Community 51`, `menu.py`, `Community 53`, `Community 61`, `Community 65`, `Community 66`, `Community 70`, `Community 74`, `Community 83`, `Community 86`, `Community 87`, `pl_ocr_service.py`, `pl_vehicle_recharge_service.py`, `Community 92`, `_invoice_info_by_customer_impl`, `response.py`, `Community 95`, `_ensure_subpack_slip_no`, `dept.py`, `Community 109`, `storage.py`, `quote_share_log.py`, `db_models.py`, `site.py`, `Community 116`, `Community 118`, `Community 125`, `customer_pool.py`?**
|
||||
_High betweenness centrality (0.129) - this node is a cross-community bridge._
|
||||
- **Why does `Settings` connect `price_letter_detail` to `backfill_crm_contract_attachment_urls.py`, `Community 35`, `crm_supplier_hazard.py`, `Community 5`, `Community 7`, `Community 72`, `Community 73`, `Community 42`, `Community 78`, `Community 52`, `Any`, `Community 59`, `api_crypto_middleware.py`, `Community 62`?**
|
||||
_High betweenness centrality (0.110) - this node is a cross-community bridge._
|
||||
- **Why does `BaseModel` connect `Community 12` to `plan.py`, `Community 1`, `Community 3`, `LocalStorage`, `Community 6`, `Community 11`, `Community 14`, `Community 15`, `SysApiRequestLog`, `Community 16`, `Community 18`, `ContractListQuery`, `Community 24`, `Community 25`, `Community 152`, `Community 27`, `get_client_ip`, `Community 30`, `Community 33`, `Community 43`, `Community 45`, `Community 46`, `Community 48`, `Community 49`, `Community 50`, `Community 54`, `Community 58`, `Community 60`, `Community 63`, `Community 64`, `Community 65`, `Community 66`, `Community 67`, `Community 71`, `Community 81`, `Community 86`, `_ensure_subpack_slip_no`, `list_customers`, `Community 106`, `apply_pl_scope`, `Community 109`, `Community 111`, `Community 119`, `Community 125`, `Community 127`?**
|
||||
_High betweenness centrality (0.092) - this node is a cross-community bridge._
|
||||
- **Are the 8 inferred relationships involving `DbUserContext` (e.g. with `InventoryRelatedData` and `AutoGenerateMonthlyResult`) actually correct?**
|
||||
_`DbUserContext` has 8 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **What connects `API 路由包 — 山峡化工生产一体化门户`, `返回运营端角色下拉选项(label=库中角色名,value=Web 角色码)。`, `返回用户下拉选项(label=昵称,value=用户ID,附带 nickName/userNo)。 供申请人/审批人等选择场景使用,返回本租户全部有效` to the rest of the system?**
|
||||
_493 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
- **Should `Community 0` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.10409745293466224 - nodes in this community are weakly interconnected._
|
||||
- **Should `Community 1` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.14919354838709678 - nodes in this community are weakly interconnected._
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -1,16 +1,16 @@
|
||||
# Graph Report - backend (2026-07-31)
|
||||
# Graph Report - backend (2026-08-01)
|
||||
|
||||
## Corpus Check
|
||||
- 294 files · ~205,154 words
|
||||
- 301 files · ~206,587 words
|
||||
- Verdict: corpus is large enough that graph structure adds value.
|
||||
|
||||
## Summary
|
||||
- 4513 nodes · 15360 edges · 527 communities (510 shown, 17 thin omitted)
|
||||
- 4540 nodes · 15450 edges · 528 communities (512 shown, 16 thin omitted)
|
||||
- Extraction: 97% EXTRACTED · 3% INFERRED · 0% AMBIGUOUS · INFERRED: 411 edges (avg confidence: 0.53)
|
||||
- Token cost: 0 input · 0 output
|
||||
|
||||
## Graph Freshness
|
||||
- Built from commit: `5a9abfdd`
|
||||
- Built from commit: `ce75576f`
|
||||
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
|
||||
- Run `graphify update .` after code changes (no API cost).
|
||||
|
||||
@@ -142,6 +142,7 @@
|
||||
- [[_COMMUNITY_BaseStorage|BaseStorage]]
|
||||
- [[_COMMUNITY_login_crypto.py|login_crypto.py]]
|
||||
- [[_COMMUNITY_codeblock1 (iflytekshanxia2.0backend)|code:block1 (/iflytek/shanxia2.0/backend)]]
|
||||
- [[_COMMUNITY_test_quote_approver_setting.py|test_quote_approver_setting.py]]
|
||||
- [[_COMMUNITY_LocalStorage|LocalStorage]]
|
||||
- [[_COMMUNITY_MinIOStorage|MinIOStorage]]
|
||||
- [[_COMMUNITY_datetime_parse.py|datetime_parse.py]]
|
||||
@@ -167,12 +168,12 @@
|
||||
- [[_COMMUNITY_PlanResourceSnapshotMixin|PlanResourceSnapshotMixin]]
|
||||
|
||||
## God Nodes (most connected - your core abstractions)
|
||||
1. `BaseModel` - 440 edges
|
||||
1. `BaseModel` - 442 edges
|
||||
2. `get_tenant_no()` - 349 edges
|
||||
3. `DbUserContext` - 254 edges
|
||||
4. `success_response()` - 253 edges
|
||||
5. `error_response()` - 235 edges
|
||||
6. `generate_uuid()` - 191 edges
|
||||
6. `generate_uuid()` - 195 edges
|
||||
7. `BxUserContext` - 183 edges
|
||||
8. `crm_user_info()` - 137 edges
|
||||
9. `assert_row_deletable_by_creator()` - 83 edges
|
||||
@@ -193,23 +194,23 @@
|
||||
## Import Cycles
|
||||
- None detected.
|
||||
|
||||
## Communities (527 total, 17 thin omitted)
|
||||
## Communities (528 total, 16 thin omitted)
|
||||
|
||||
### Community 0 - "Community 0"
|
||||
Cohesion: 0.09
|
||||
Nodes (42): _build_public_page(), _build_site_product_read(), _format_file_size(), get_public_site(), _get_site_link_by_token(), _parse_feedback_images(), Session, UploadFile (+34 more)
|
||||
Cohesion: 0.18
|
||||
Nodes (20): _build_public_page(), _format_file_size(), _format_plan_date(), get_public_transport(), _get_transport_by_token(), _parse_feedback_images(), Session, UploadFile (+12 more)
|
||||
|
||||
### Community 1 - "Community 1"
|
||||
Cohesion: 0.26
|
||||
Nodes (6): start_instance(), StartInstanceBody, WfNode, Any, Session, WorkflowEngine
|
||||
Cohesion: 0.13
|
||||
Nodes (23): ProcessDefCreateBody, ProcessDefUpdateBody, PublishVersionBody, StartInstanceBody, 流程定义版本 wf_process_version, 流程实例 wf_process_instance, WfNode, WfProcessInstance (+15 more)
|
||||
|
||||
### Community 2 - "Community 2"
|
||||
Cohesion: 0.14
|
||||
Nodes (28): create_carrier(), get_carrier(), update_carrier(), PlCarrier, carrier_to_read(), CarrierBase, CarrierCreate, CarrierQualificationBase (+20 more)
|
||||
Cohesion: 0.27
|
||||
Nodes (11): assert_carrier_eligible_for_transport(), carrier_agreement_expired(), carrier_audit_approved(), carrier_eligible_for_transport(), carrier_qualification_complete(), date, 资质附件均为选填,保留函数签名供兼容调用。, _urls_nonempty() (+3 more)
|
||||
|
||||
### Community 3 - "Community 3"
|
||||
Cohesion: 0.09
|
||||
Nodes (88): attachment_download_url(), attachment_preview_url(), AttachmentDownloadUrlBody, create_dept(), create_dict_data(), create_dict_type(), create_menu(), create_post() (+80 more)
|
||||
Cohesion: 0.08
|
||||
Nodes (100): attachment_download_url(), attachment_preview_url(), AttachmentDownloadUrlBody, create_dept(), create_dict_data(), create_dict_type(), create_menu(), create_post() (+92 more)
|
||||
|
||||
### Community 4 - "Community 4"
|
||||
Cohesion: 0.12
|
||||
@@ -220,88 +221,88 @@ Cohesion: 0.20
|
||||
Nodes (24): count_source_rows(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), load_runtime_settings(), load_settings_env(), load_user_mapping() (+16 more)
|
||||
|
||||
### Community 6 - "Community 6"
|
||||
Cohesion: 0.15
|
||||
Nodes (11): BxAdvanceApply, BxAdvanceApproval, AdvanceService, Any, Session, 同一节点多次审批时,取 approval_time 最新的一条作为当前节点状态。, 返回全部审批动作,按时间正序排列,用于展示完整审批历史。, Any (+3 more)
|
||||
Cohesion: 0.13
|
||||
Nodes (48): PlPlan, PlPlanProduct, 计划产品明细表 pl_plan_product, PlanArchiveFreightBlock, PlanArchiveProductItem, PlanTransportCreateBlock, all_hazard_products_cert_complete(), apply_archive_freight() (+40 more)
|
||||
|
||||
### Community 7 - "Community 7"
|
||||
Cohesion: 0.12
|
||||
Nodes (36): get_password_hash(), Namespace, build_merged_users(), bx_role_labels(), check_minio(), check_mysql(), check_redis(), _clip() (+28 more)
|
||||
|
||||
### Community 8 - "Community 8"
|
||||
Cohesion: 0.14
|
||||
Nodes (45): register_quote_routes(), accept_quote(), calculate_quote_amount(), create_new_version(), create_quote(), _decimal_value(), _enrich_contact_fields(), _enrich_supplier_name() (+37 more)
|
||||
Cohesion: 0.10
|
||||
Nodes (51): create_from_quote(), _quote_products_to_contract_products(), Java: createFromQuote — 返回合同 id, _build_quote_filters(), calculate_quote_amount(), create_quote(), _decimal_value(), _enrich_contact_fields() (+43 more)
|
||||
|
||||
### Community 11 - "Community 11"
|
||||
Cohesion: 0.10
|
||||
Nodes (55): cancel_apply(), create_apply(), create_disburse(), delete_applies(), delete_disburses(), get_apply(), get_disburse(), list_applies() (+47 more)
|
||||
|
||||
### Community 12 - "Community 12"
|
||||
Cohesion: 0.05
|
||||
Nodes (68): contact_update(), customer_create(), customer_update(), customer_name_exists(), BaseModel, AttachmentUploadResponse, ChangePasswordRequest, Config (+60 more)
|
||||
Cohesion: 0.04
|
||||
Nodes (89): contact_add(), contact_delete(), contact_update(), customer_create(), customer_delete(), customer_update(), pool_customer_assign(), pool_customer_claim() (+81 more)
|
||||
|
||||
### Community 13 - "Community 13"
|
||||
Cohesion: 0.16
|
||||
Nodes (31): create_instance(), create_process_def(), create_task(), get_all_tasks_for_business(), get_instance(), get_latest_version(), get_nodes_by_version(), get_pending_tasks_by_node() (+23 more)
|
||||
Nodes (30): create_instance(), create_process_def(), create_task(), get_all_tasks_for_business(), get_instance(), get_latest_version(), get_nodes_by_version(), get_pending_tasks_by_node() (+22 more)
|
||||
|
||||
### Community 14 - "Community 14"
|
||||
Cohesion: 0.16
|
||||
Nodes (32): check_document_expiry(), document_reminder_stats(), get_inventory_ledger(), get_summary(), list_document_reminders(), list_transactions(), list_warnings(), date (+24 more)
|
||||
Cohesion: 0.11
|
||||
Nodes (45): check_document_expiry(), document_reminder_stats(), get_inventory_ledger(), get_summary(), list_document_reminders(), list_transactions(), list_warnings(), date (+37 more)
|
||||
|
||||
### Community 15 - "Community 15"
|
||||
Cohesion: 0.11
|
||||
Nodes (13): BxReimburseApply, BxReimburseApproval, BxReimburseDetail, 报销单勾选的发票附件(同一发票可勾选多个附件,各占一条)。, ReimburseInvoiceBriefOut, ReimburseInvoiceLinkIn, Any, Session (+5 more)
|
||||
Nodes (16): BxReimburseApply, BxReimburseApproval, BxReimburseDetail, BxReimburseInvoice, BxReimburseOffset, 报销单勾选的发票附件(同一发票可勾选多个附件,各占一条)。, ReimburseApplyOut, ReimburseInvoiceBriefOut (+8 more)
|
||||
|
||||
### Community 16 - "Community 16"
|
||||
Cohesion: 0.09
|
||||
Nodes (60): cancel_apply(), create_apply(), create_record(), delete_applies(), delete_records(), get_apply(), get_record(), list_applies() (+52 more)
|
||||
Nodes (64): cancel_apply(), create_apply(), create_record(), delete_applies(), delete_records(), get_apply(), get_record(), list_applies() (+56 more)
|
||||
|
||||
### Community 18 - "Community 18"
|
||||
Cohesion: 0.15
|
||||
Nodes (30): advance_pending_approval_stats(), advance_status_counts(), approve_advance(), approved_by_me(), create_advance(), delete_advance(), _fill_offset_amounts(), get_advance() (+22 more)
|
||||
Cohesion: 0.09
|
||||
Nodes (38): advance_pending_approval_stats(), advance_status_counts(), approve_advance(), approved_by_me(), create_advance(), delete_advance(), _fill_offset_amounts(), get_advance() (+30 more)
|
||||
|
||||
### Community 19 - "Community 19"
|
||||
Cohesion: 0.09
|
||||
Nodes (46): _avatar_url_field(), _build_user_info_payload(), change_current_user_password(), get_current_user_profile(), _jwt_user_payload(), logout(), public_quote_share(), Request (+38 more)
|
||||
|
||||
### Community 21 - "Community 21"
|
||||
Cohesion: 0.06
|
||||
Nodes (82): _alias_row_to_dict(), _assert_brand_deletable(), barcode_exists(), _brand_row_to_dict(), _build_category_tree(), category_code_exists(), _category_row_to_dict(), _collect_category_with_ancestors() (+74 more)
|
||||
Cohesion: 0.08
|
||||
Nodes (63): _alias_row_to_dict(), _assert_brand_deletable(), barcode_exists(), _brand_row_to_dict(), _build_category_tree(), category_code_exists(), _category_row_to_dict(), _collect_category_with_ancestors() (+55 more)
|
||||
|
||||
### Community 22 - "Community 22"
|
||||
Cohesion: 0.08
|
||||
Nodes (28): parse_admin_request_json(), Any, Request, 运营 Web(/web/admin)JSON 请求体解析。 与 ApiCryptoMiddleware 配合:中间件未改写 body 时,在路由内解密 `{, 读取 JSON body;dict 时走 resolve_request_payload(明文 / 密文 / data 包装)。 Returns:, ApiCryptoError, _b64url_decode(), _b64url_encode() (+20 more)
|
||||
|
||||
### Community 23 - "Community 23"
|
||||
Cohesion: 0.15
|
||||
Nodes (6): convert_heic_to_jpeg(), process_image_content(), QiniuStorage, 将 HEIC/HEIF 格式转换为 JPEG 格式, 处理图片内容,如果是 HEIC 格式则转换为 JPEG, BytesIO
|
||||
Cohesion: 0.11
|
||||
Nodes (8): convert_heic_to_jpeg(), OSSStorage, process_image_content(), QiniuStorage, 文件存储工具模块 支持多种存储方式: 1. 本地存储 (local) - 默认 2. 七牛云存储 (qiniu) - 推荐生产环境使用 3. 阿里云OSS (, 将 HEIC/HEIF 格式转换为 JPEG 格式, 处理图片内容,如果是 HEIC 格式则转换为 JPEG, BytesIO
|
||||
|
||||
### Community 24 - "Community 24"
|
||||
Cohesion: 0.10
|
||||
Nodes (19): category_add(), category_delete(), category_tree(), category_update(), product_delete(), product_detail(), product_update_status(), 删除产品(支持单条 body.id 和批量 body.ids) (+11 more)
|
||||
Cohesion: 0.16
|
||||
Nodes (15): ApiRequestLogCRUD, Session, list_ethnics(), Any, Session, 民族码表 CRUD(全局 sys_ethnic), _row_to_dict(), 数据模型(与 docs/sql/shanxia2.0.sql 全库导出基线对齐的核心系统表) (+7 more)
|
||||
|
||||
### Community 25 - "Community 25"
|
||||
Cohesion: 0.09
|
||||
Nodes (71): _build_driver_url(), get_plan_arrange_info(), 获取计划已安排运输后的司机链接与地磅出入库关联(供查看弹窗复制简述/二维码)。, _apply_plan_logistics_from_payload(), _apply_transport_arrange_fields(), arrange_transport(), _assert_vehicle_carrier_eligible(), _build_arrange_result() (+63 more)
|
||||
Cohesion: 0.10
|
||||
Nodes (64): get_plan_arrange_info(), 获取计划已安排运输后的司机链接与地磅出入库关联(供查看弹窗复制简述/二维码)。, _apply_plan_logistics_from_payload(), _apply_transport_arrange_fields(), arrange_transport(), _assert_vehicle_carrier_eligible(), _build_arrange_result(), _build_driver_url() (+56 more)
|
||||
|
||||
### Community 26 - "Community 26"
|
||||
Cohesion: 0.09
|
||||
Nodes (64): register_contract_routes(), _apply_contract_entity_fields(), batch_assign_owner(), batch_submit_contract_approval(), _build_contract_filters(), _build_customer_address(), _calc_amount_with_tax(), _contract_period_key() (+56 more)
|
||||
Nodes (58): register_contract_routes(), delete_pending_approvals(), Java: withdraw — 软删除 pending 审批, batch_assign_owner(), batch_submit_contract_approval(), _contract_period_key(), contract_row_to_dict(), create_from_opportunity() (+50 more)
|
||||
|
||||
### Community 27 - "Community 27"
|
||||
Cohesion: 0.15
|
||||
Nodes (34): _apply_plan_product_update(), _apply_plan_update(), AutoGenerateMonthlyResult, _base_query(), create_plan(), delete_plan(), _generate_plan_no(), get_plan() (+26 more)
|
||||
Cohesion: 0.19
|
||||
Nodes (28): _apply_plan_product_update(), _apply_plan_update(), AutoGenerateMonthlyResult, _base_query(), create_plan(), delete_plan(), _generate_plan_no(), get_plan() (+20 more)
|
||||
|
||||
### Community 28 - "Community 28"
|
||||
Cohesion: 0.09
|
||||
Nodes (66): PlFreightSettlement, PlFreightSettlementApproval, 运费结算表 pl_freight_settlement, 运费结算审批 pl_freight_settlement_approval, FreightSettlementDisburseBody, FreightSettlementSubmitApprovalBody, FreightSettlementUpdate, apply_pl_scope() (+58 more)
|
||||
Cohesion: 0.12
|
||||
Nodes (53): PlFreightSettlement, PlFreightSettlementApproval, 运费结算表 pl_freight_settlement, 运费结算审批 pl_freight_settlement_approval, _advance_after_approval(), _approval_to_dict(), can_user_approve_settlement(), cancel_draft_settlement() (+45 more)
|
||||
|
||||
### Community 29 - "Community 29"
|
||||
Cohesion: 0.09
|
||||
Nodes (69): create_plan_full(), _generate_notify_message(), 创建运输计划(完整表单:合同产品 + 可选预选车辆,不自动安排运输), update_plan(), PlPlan, PlPlanProduct, 计划产品明细表 pl_plan_product, PlanArchiveFreightBlock (+61 more)
|
||||
Cohesion: 0.17
|
||||
Nodes (24): _build_public_page(), _build_site_product_read(), _format_file_size(), get_public_site(), _get_site_link_by_token(), _parse_feedback_images(), Session, UploadFile (+16 more)
|
||||
|
||||
### Community 30 - "Community 30"
|
||||
Cohesion: 0.10
|
||||
Nodes (24): get_menu_list(), 运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,14 个一级菜单), menu_badge_stats(), Session, Session, recognize_cert(), RecognizeCertBody, RecognizeCertResult (+16 more)
|
||||
Nodes (33): menu_badge_stats(), Session, Session, recognize_cert(), RecognizeCertBody, RecognizeCertResult, _address_to_read(), _build_address_query() (+25 more)
|
||||
|
||||
### Community 33 - "Community 33"
|
||||
Cohesion: 0.20
|
||||
@@ -316,128 +317,128 @@ Cohesion: 0.18
|
||||
Nodes (36): clear_portal_workflow(), _dec(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), load_bx_user_mapping(), load_runtime_settings() (+28 more)
|
||||
|
||||
### Community 36 - "Community 36"
|
||||
Cohesion: 0.16
|
||||
Nodes (28): approve_task(), _assert_wf_instance_in_scope(), _business_type_label(), create_process_def(), _enrich_task_list(), get_business_all_tasks(), _get_business_form_fields(), get_designer_data() (+20 more)
|
||||
Cohesion: 0.09
|
||||
Nodes (37): API 路由包 — 山峡化工生产一体化门户, get_menu_list(), 运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,14 个一级菜单), approve_task(), _assert_wf_instance_in_scope(), _business_type_label(), create_process_def(), delete_process_def() (+29 more)
|
||||
|
||||
### Community 37 - "Community 37"
|
||||
Cohesion: 0.24
|
||||
Nodes (18): _actor_ids(), _actor_names(), assert_row_deletable_by_creator(), assert_rows_deletable_by_creator(), can_delete_by_creator(), is_super_admin(), normalize_user_info(), Any (+10 more)
|
||||
|
||||
### Community 38 - "Community 38"
|
||||
Cohesion: 0.14
|
||||
Nodes (25): load_brand_qtys_by_plan_product_ids(), persist_plan_product_brand_qtys(), Any, Decimal, Session, 按计划产品明细 ID 批量加载归档品牌数量(brandId/brandName/qty)。, 软删旧品牌量行后按归档结果插入;无品牌约定则只软删不插入。, validate_brand_qty_rows() (+17 more)
|
||||
Cohesion: 0.13
|
||||
Nodes (28): generate_uuid(), PlInventoryTransaction, 库存流水表 pl_inventory_transaction, load_brand_qtys_by_plan_product_ids(), persist_plan_product_brand_qtys(), Any, Decimal, Session (+20 more)
|
||||
|
||||
### Community 39 - "Community 39"
|
||||
Cohesion: 0.22
|
||||
Nodes (28): assign_clues(), check_convert(), check_duplicate_clue(), convert_clue(), create_clue(), _enrich_user_names(), _ensure_clue_in_scope(), get_clue() (+20 more)
|
||||
|
||||
### Community 42 - "Community 42"
|
||||
Cohesion: 0.19
|
||||
Nodes (32): _dec(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), load_oss_id_to_url(), load_runtime_settings(), load_settings_env() (+24 more)
|
||||
Cohesion: 0.20
|
||||
Nodes (31): _dec(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), load_oss_id_to_url(), load_runtime_settings(), load_settings_env() (+23 more)
|
||||
|
||||
### Community 43 - "Community 43"
|
||||
Cohesion: 0.10
|
||||
Nodes (30): create_invoice(), delete_invoice(), get_mine_wallet_summary(), list_invoice_ledgers(), list_invoice_wallets(), list_invoices(), Request, Session (+22 more)
|
||||
Nodes (32): create_invoice(), delete_invoice(), get_mine_wallet_summary(), list_invoice_ledgers(), list_invoice_wallets(), list_invoices(), Request, Session (+24 more)
|
||||
|
||||
### Community 44 - "Community 44"
|
||||
Cohesion: 0.15
|
||||
Nodes (14): _db_pool_keepalive_loop(), _error_json(), lifespan(), Exception, Request, 数据库异常:返回 JSON 并保留 CORS 头,避免浏览器只报跨域错误, 未捕获异常:统一 JSON 500,确保跨域场景下仍有 Access-Control-Allow-Origin, 定期 ping MySQL,避免远程库/NAT 回收空闲 TCP 后首请求长时间卡住。 (+6 more)
|
||||
|
||||
### Community 45 - "Community 45"
|
||||
Cohesion: 0.13
|
||||
Nodes (27): PlCert, CertRead, 运输计划 - 安排运输时车辆/人员/证件资源快照 Schema, SnapshotCertRead, SnapshotPersonnelRow, TransportResourceSnapshot, certs_to_read(), list_owner_certs() (+19 more)
|
||||
Cohesion: 0.19
|
||||
Nodes (17): ApiRequestLogMiddleware, _claims_from_auth(), _format_body_bytes(), _persist_log(), Any, Request, 统一记录 API 请求/响应出入参的中间件, _redact_obj() (+9 more)
|
||||
|
||||
### Community 46 - "Community 46"
|
||||
Cohesion: 0.26
|
||||
Nodes (18): _address_to_read(), _build_address_query(), create_transport_address(), delete_transport_address(), _get_address(), get_transport_address(), list_all_transport_addresses(), list_transport_addresses() (+10 more)
|
||||
Cohesion: 0.18
|
||||
Nodes (17): get_oper_log_meta(), OperLogMeta, 请求级操作日志元数据(ContextVar), 供异步落库线程使用,避免 ContextVar 跨线程丢失。, set_oper_log_meta(), snapshot_oper_log_meta(), _format_summary(), oper_log() (+9 more)
|
||||
|
||||
### Community 48 - "Community 48"
|
||||
Cohesion: 0.20
|
||||
Nodes (26): _apply_inventory_product_update(), _apply_inventory_update(), _base_query(), create_inventory(), create_link_for_plan(), delete_inventory(), _generate_batch_no(), _get_inventory() (+18 more)
|
||||
Cohesion: 0.18
|
||||
Nodes (27): _apply_inventory_product_update(), _apply_inventory_update(), _base_query(), create_inventory(), create_link_for_plan(), delete_inventory(), _generate_batch_no(), _get_inventory() (+19 more)
|
||||
|
||||
### Community 49 - "Community 49"
|
||||
Cohesion: 0.05
|
||||
Nodes (63): _build_page(), _get_by_token(), _models(), Session, 运输计划 · 入库/出库单现场免登录公共 API, list_ethnics(), Any, Session (+55 more)
|
||||
Cohesion: 0.07
|
||||
Nodes (47): _build_page(), _get_by_token(), _models(), Session, 运输计划 · 入库/出库单现场免登录公共 API, QrCodeResponse, SoftDeleteModel, BizAttachment (+39 more)
|
||||
|
||||
### Community 50 - "Community 50"
|
||||
Cohesion: 0.11
|
||||
Nodes (38): approve_settlement_legacy(), cancel_settlement(), create_settlement(), delete_settlement(), disburse_settlement(), _generate_settlement_no(), get_settlement(), get_settlement_approval_flow() (+30 more)
|
||||
Nodes (37): approve_settlement_legacy(), cancel_settlement(), create_settlement(), delete_settlement(), disburse_settlement(), _generate_settlement_no(), get_settlement(), get_settlement_approval_flow() (+29 more)
|
||||
|
||||
### Community 51 - "Community 51"
|
||||
Cohesion: 0.18
|
||||
Nodes (20): _dict_field(), format_brand_qty_summary(), load_transport_driver_product_feedbacks(), persist_io_item_brand_qtys(), persist_transport_product_feedback(), Any, Decimal, Session (+12 more)
|
||||
Cohesion: 0.14
|
||||
Nodes (24): PlTransportProductBrandQty, PlTransportProductFeedback, 运输单按产品回传 pl_transport_product_feedback, 运输单产品回传品牌数量 pl_transport_product_brand_qty, _dict_field(), format_brand_qty_summary(), load_transport_driver_product_feedbacks(), persist_io_item_brand_qtys() (+16 more)
|
||||
|
||||
### Community 52 - "Community 52"
|
||||
Cohesion: 0.18
|
||||
Nodes (29): _dec(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), load_oss_id_to_url(), load_runtime_settings(), load_settings_env() (+21 more)
|
||||
|
||||
### Community 53 - "Community 53"
|
||||
Cohesion: 0.09
|
||||
Nodes (41): 报销模块认证适配:门户 JWT 用户 → bx 业务上下文。, BxReimburseInvoice, BxReimburseOffset, BxWorkflowNode, 报销(bx)模块数据模型 — 对齐 docs/sql/20260606_bx_tables.sql, filter_current_cycle_approvals(), 审批轮次过滤 — 驳回后重新提交时,进度接口仅展示当前轮次。, 审批进度仅展示当前轮次;完整历史由 get_approval_history 返回。 (+33 more)
|
||||
Cohesion: 0.10
|
||||
Nodes (36): 报销模块认证适配:门户 JWT 用户 → bx 业务上下文。, filter_current_cycle_approvals(), 审批轮次过滤 — 驳回后重新提交时,进度接口仅展示当前轮次。, 审批进度仅展示当前轮次;完整历史由 get_approval_history 返回。, 报销模块 Service 基类 — 租户隔离 + UUID + 软删除。, assert_bx_attachment_biz_in_scope(), assert_bx_biz_in_scope(), assert_bx_row_in_scope() (+28 more)
|
||||
|
||||
### Community 54 - "Community 54"
|
||||
Cohesion: 0.19
|
||||
Nodes (28): _get_by_plan(), get_inbound(), get_inbound_by_plan(), _get_order(), get_outbound(), get_outbound_by_plan(), inbound_site_url(), list_inbound() (+20 more)
|
||||
|
||||
### Community 58 - "Community 58"
|
||||
Cohesion: 0.06
|
||||
Nodes (56): operlog_detail(), operlog_list(), Session, 操作日志分页列表(基于 API 请求日志适配 RuoYi OperLog 结构), _user_info(), ApiRequestLogMiddleware, _claims_from_auth(), _format_body_bytes() (+48 more)
|
||||
Cohesion: 0.17
|
||||
Nodes (20): operlog_detail(), operlog_list(), Session, 操作日志分页列表(基于 API 请求日志适配 RuoYi OperLog 结构), _user_info(), 获取用户所有角色的数据权限配置。 返回: [{"data_scope": str, "details": [SysRoleDataScopeDetai, _resolve_user_role_scopes(), _apply_oper_log_scope() (+12 more)
|
||||
|
||||
### Community 59 - "Community 59"
|
||||
Cohesion: 0.20
|
||||
Nodes (28): _dec(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), load_oss_id_to_url(), load_runtime_settings(), load_settings_env() (+20 more)
|
||||
|
||||
### Community 60 - "Community 60"
|
||||
Cohesion: 0.22
|
||||
Nodes (22): advance_issue_stats(), batch_offset(), export_advance_issues(), issue_advance(), list_advance_issues(), list_advance_offset_records(), list_pending_payments(), list_processed_payments() (+14 more)
|
||||
Cohesion: 0.09
|
||||
Nodes (37): advance_issue_stats(), batch_offset(), export_advance_issues(), issue_advance(), list_advance_issues(), list_advance_offset_records(), list_pending_payments(), list_processed_payments() (+29 more)
|
||||
|
||||
### Community 61 - "Community 61"
|
||||
Cohesion: 0.16
|
||||
Nodes (33): DbInboundOutbound, PlTransportDbLink, 运输-地磅出入库关联 pl_transport_db_link, _apply_plan_quantity_delta(), _create_db_inventory(), _create_db_plan(), DibangInventoryLinkResult, find_monthly_db_plan() (+25 more)
|
||||
Cohesion: 0.18
|
||||
Nodes (25): apply_product_snapshot_to_inventory(), apply_product_snapshot_to_plan(), ProductSnapshot, Session, 地磅模块 — CRM 产品解析与快照写入。, 校验 CRM 产品并返回名称/规格快照。新建单据 product_id 必填。, resolve_crm_product(), _create_db_inventory() (+17 more)
|
||||
|
||||
### Community 62 - "Community 62"
|
||||
Cohesion: 0.21
|
||||
Nodes (22): clear_target_products(), count_target_rows(), _dec(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), load_runtime_settings() (+14 more)
|
||||
|
||||
### Community 63 - "Community 63"
|
||||
Cohesion: 0.09
|
||||
Nodes (46): bx_health(), 报销(bx)API 路由聚合 — REST 风格,协议对齐 bx 原系统。, P0 健康检查:验证 bx 路由已挂载且响应协议为 code=0。, approve_reimburse(), approved_by_me(), _build_details_data(), create_reimburse(), delete_reimburse() (+38 more)
|
||||
Cohesion: 0.17
|
||||
Nodes (30): bx_health(), 报销(bx)API 路由聚合 — REST 风格,协议对齐 bx 原系统。, P0 健康检查:验证 bx 路由已挂载且响应协议为 code=0。, approve_reimburse(), approved_by_me(), _build_details_data(), create_reimburse(), delete_reimburse() (+22 more)
|
||||
|
||||
### Community 64 - "Community 64"
|
||||
Cohesion: 0.04
|
||||
Nodes (182): clue_assign(), clue_check_convert(), clue_check_duplicate(), clue_convert(), clue_create(), clue_delete(), clue_detail(), clue_export() (+174 more)
|
||||
Nodes (184): category_add(), category_delete(), category_tree(), category_update(), clue_assign(), clue_convert(), clue_create(), clue_delete() (+176 more)
|
||||
|
||||
### Community 65 - "Community 65"
|
||||
Cohesion: 0.10
|
||||
Nodes (58): cancel_apply(), create_apply(), create_process(), delete_applies(), delete_processes(), get_apply(), get_process(), list_applies() (+50 more)
|
||||
|
||||
### Community 66 - "Community 66"
|
||||
Cohesion: 0.15
|
||||
Nodes (24): create_workflow(), delete_workflow(), get_workflow(), get_workflow_by_biz_type(), list_workflows(), Session, save_workflow_nodes(), update_workflow() (+16 more)
|
||||
Cohesion: 0.14
|
||||
Nodes (25): create_workflow(), delete_workflow(), get_workflow(), get_workflow_by_biz_type(), list_workflows(), Session, save_workflow_nodes(), update_workflow() (+17 more)
|
||||
|
||||
### Community 67 - "Community 67"
|
||||
Cohesion: 0.21
|
||||
Nodes (20): register_contract_sub_routes(), _change_row_to_dict(), create_contract_change(), _enrich_change_rows(), execute_change(), export_contract_changes(), export_contract_templates(), _get_change_row() (+12 more)
|
||||
Cohesion: 0.29
|
||||
Nodes (16): ensure_crm_customer_linked_row_in_scope(), ensure_crm_owner_row_in_scope(), Session, CRM 数据权限辅助(负责人 owner_user_id + 标准 create_by/create_dept_id)。, 客户子表(开票信息等):先校验关联客户 owner scope,否则校验 create_by。, _clear_default_for_customer(), create_invoice_info(), get_invoice_info() (+8 more)
|
||||
|
||||
### Community 68 - "Community 68"
|
||||
Cohesion: 0.17
|
||||
Nodes (11): pool_customer_assign(), pool_customer_claim(), pool_customer_list(), pool_record_detail(), pool_record_list(), PoolAssignBody, PoolClaimBody, PoolCustomerListBody (+3 more)
|
||||
Cohesion: 0.22
|
||||
Nodes (20): register_quote_routes(), accept_quote(), create_new_version(), generate_pdf(), generate_share(), _get_quote_in_scope(), get_template(), invalid_quote() (+12 more)
|
||||
|
||||
### Community 69 - "Community 69"
|
||||
Cohesion: 0.14
|
||||
Nodes (28): _append_user_scope_conditions(), apply_data_scope(), assert_row_in_data_scope(), _collect_scope_ids(), _get_child_dept_ids(), get_user_dept_id(), Session, 数据权限过滤(V0.3:基于角色 data_scope 的查询级行级过滤) data_scope 值: 1 = 全部数据 2 = 本部门数据 3 (+20 more)
|
||||
Cohesion: 0.15
|
||||
Nodes (23): _append_user_scope_conditions(), apply_data_scope(), _collect_scope_ids(), _get_child_dept_ids(), get_user_dept_id(), Session, 合并多角色数据范围为 dept_ids / user_ids 并集。, 根据用户角色的数据权限过滤查询。 适用于业务单据列表/详情(报销单、客户、运单等)。 以下场景 SHALL NOT 调用本函数(仅租户隔离即 (+15 more)
|
||||
|
||||
### Community 70 - "Community 70"
|
||||
Cohesion: 0.39
|
||||
Nodes (10): create_post(), get_post(), list_posts(), post_code_exists(), Any, Session, _row_to_dict(), soft_delete_post() (+2 more)
|
||||
Cohesion: 0.19
|
||||
Nodes (20): create_post(), get_post(), list_posts(), post_code_exists(), Any, Session, _row_to_dict(), soft_delete_post() (+12 more)
|
||||
|
||||
### Community 71 - "Community 71"
|
||||
Cohesion: 0.12
|
||||
Nodes (31): CRM 报价单 API 路由 — 挂接到 crm.router, QuoteAcceptBody, QuoteApprovalApproveBody, QuoteApprovalListBody, QuoteApprovalRefreshNamesBody, QuoteApprovalWithdrawBody, QuoteCreateBody, QuoteDeleteBody (+23 more)
|
||||
Cohesion: 0.10
|
||||
Nodes (33): CRM 报价单 API 路由 — 挂接到 crm.router, total_pages(), QuoteApproverSettingSaveBody, QuoteAcceptBody, QuoteApprovalApproveBody, QuoteApprovalListBody, QuoteApprovalRefreshNamesBody, QuoteApprovalWithdrawBody (+25 more)
|
||||
|
||||
### Community 72 - "Community 72"
|
||||
Cohesion: 0.17
|
||||
Nodes (30): apply_biz_backfill(), BackfillRule, BizRef, build_biz_index(), count_target_rows(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows() (+22 more)
|
||||
Cohesion: 0.19
|
||||
Nodes (28): apply_biz_backfill(), BackfillRule, BizRef, build_biz_index(), count_target_rows(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows() (+20 more)
|
||||
|
||||
### Community 73 - "Community 73"
|
||||
Cohesion: 0.20
|
||||
@@ -456,12 +457,12 @@ Cohesion: 0.16
|
||||
Nodes (26): clear_target_categories(), compute_level(), count_target_rows(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), load_runtime_settings() (+18 more)
|
||||
|
||||
### Community 81 - "Community 81"
|
||||
Cohesion: 0.13
|
||||
Nodes (21): API 路由包 — 山峡化工生产一体化门户, create_leave(), delete_leave(), get_leave(), _leave_local_callback(), leave_workflow_callback(), list_leaves(), Session (+13 more)
|
||||
Cohesion: 0.38
|
||||
Nodes (10): create_leave(), delete_leave(), get_leave(), _leave_local_callback(), leave_workflow_callback(), list_leaves(), Session, submit_leave() (+2 more)
|
||||
|
||||
### Community 83 - "Community 83"
|
||||
Cohesion: 0.15
|
||||
Nodes (30): _advance_contract_after_approval(), approve_contract(), approve_contract_by_contract_id(), approve_pass(), approve_reject(), approve_transfer(), _build_approval_list_filters(), can_user_approve() (+22 more)
|
||||
Cohesion: 0.09
|
||||
Nodes (52): CRM 合同子模块路由 — 变更/模板/审批独立页, register_contract_sub_routes(), _advance_contract_after_approval(), approve_contract(), approve_contract_by_contract_id(), approve_pass(), approve_reject(), approve_transfer() (+44 more)
|
||||
|
||||
### Community 84 - "Community 84"
|
||||
Cohesion: 0.23
|
||||
@@ -469,7 +470,7 @@ Nodes (23): build_id_maps(), clip_text(), count_rows(), export_mapping(), fetch_
|
||||
|
||||
### Community 86 - "Community 86"
|
||||
Cohesion: 0.14
|
||||
Nodes (37): _apply_weighing_product_update(), _apply_weighing_update(), _base_query(), create_weighing(), delete_weighing(), _get_weighing(), get_weighing_related(), list_weighing() (+29 more)
|
||||
Nodes (36): _apply_weighing_product_update(), _apply_weighing_update(), _base_query(), create_weighing(), delete_weighing(), _get_weighing(), get_weighing_related(), list_weighing() (+28 more)
|
||||
|
||||
### Community 87 - "Community 87"
|
||||
Cohesion: 0.20
|
||||
@@ -477,35 +478,35 @@ Nodes (19): approve_quote(), can_user_approve(), create_approval_records(), dele
|
||||
|
||||
### Community 88 - "Any"
|
||||
Cohesion: 0.18
|
||||
Nodes (31): _dec(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), legacy_parent_id(), load_oss_id_to_url(), load_runtime_settings() (+23 more)
|
||||
Nodes (30): _dec(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), legacy_parent_id(), load_oss_id_to_url(), load_runtime_settings() (+22 more)
|
||||
|
||||
### Community 89 - "pl_ocr_service.py"
|
||||
Cohesion: 0.21
|
||||
Nodes (20): assert_quota_available(), _build_ocr_client(), compress_image_bytes(), current_usage_month(), _first_attachment_url(), get_or_create_usage_row(), increment_usage(), _monthly_limit() (+12 more)
|
||||
Cohesion: 0.19
|
||||
Nodes (22): PlOcrUsageMonth, 阿里云 OCR 月度调用计数 pl_ocr_usage_month(按自然月全局统计), assert_quota_available(), _build_ocr_client(), compress_image_bytes(), current_usage_month(), _first_attachment_url(), get_or_create_usage_row() (+14 more)
|
||||
|
||||
### Community 91 - "pl_vehicle_recharge_service.py"
|
||||
Cohesion: 0.17
|
||||
Nodes (24): create_contract(), create_contract_template(), create_from_opportunity(), create_from_quote(), _decimal_value(), generate_contract_name(), get_contract_template_detail(), _get_field() (+16 more)
|
||||
Cohesion: 0.12
|
||||
Nodes (40): _apply_contract_entity_fields(), _build_contract_filters(), _build_customer_address(), _calc_amount_with_tax(), _change_row_to_dict(), create_contract(), create_contract_change(), create_contract_template() (+32 more)
|
||||
|
||||
### Community 92 - "Community 92"
|
||||
Cohesion: 0.15
|
||||
Nodes (27): 验证密码(哈希非 bcrypt 或损坏时返回 False,避免抛错), verify_password(), change_password(), create_user(), get_user(), get_user_with_relations(), _hash_password(), list_post_ids_for_user() (+19 more)
|
||||
Cohesion: 0.14
|
||||
Nodes (29): 验证密码(哈希非 bcrypt 或损坏时返回 False,避免抛错), verify_password(), change_password(), create_user(), get_user(), get_user_with_relations(), _hash_password(), list_post_ids_for_user() (+21 more)
|
||||
|
||||
### Community 93 - "_invoice_info_by_customer_impl"
|
||||
Cohesion: 0.28
|
||||
Nodes (17): create_contact(), get_contact(), list_contacts(), list_contacts_by_customer(), _parse_optional_date(), Any, date, Session (+9 more)
|
||||
Cohesion: 0.29
|
||||
Nodes (16): create_contact(), get_contact(), list_contacts_by_customer(), _parse_optional_date(), Any, date, Session, _row_to_dict() (+8 more)
|
||||
|
||||
### Community 94 - "response.py"
|
||||
Cohesion: 0.13
|
||||
Nodes (22): _build_quote_filters(), _fill_contract_linked_count(), list_quotes(), _parse_optional_date(), _parse_optional_datetime(), _period_key(), Any, date (+14 more)
|
||||
Cohesion: 0.15
|
||||
Nodes (30): create_clue_follow(), delete_clue_follow(), get_clue_follow(), get_follow_statistics(), list_clue_follows(), Any, Session, 更新线索跟进记录,必要时同步线索 next_time (+22 more)
|
||||
|
||||
### Community 95 - "Community 95"
|
||||
Cohesion: 0.35
|
||||
Nodes (11): create_tenant(), get_tenant(), list_tenants(), Any, Session, _row_to_dict(), soft_delete_tenant(), tenant_no_exists() (+3 more)
|
||||
|
||||
### Community 97 - "_ensure_subpack_slip_no"
|
||||
Cohesion: 0.13
|
||||
Nodes (40): approve_carrier(), bind_vehicle_driver(), create_driver(), create_personnel(), delete_carrier(), delete_driver(), delete_personnel(), delete_vehicle() (+32 more)
|
||||
Cohesion: 0.07
|
||||
Nodes (81): approve_carrier(), bind_vehicle_driver(), create_carrier(), create_driver(), create_personnel(), create_vehicle(), delete_carrier(), delete_driver() (+73 more)
|
||||
|
||||
### Community 100 - "price_letter_detail"
|
||||
Cohesion: 0.27
|
||||
@@ -528,24 +529,24 @@ Cohesion: 0.12
|
||||
Nodes (15): pool_rule_add(), pool_rule_delete(), pool_rule_detail(), pool_rule_execute(), pool_rule_list(), pool_rule_update(), pool_rule_update_status(), PoolRuleAddBody (+7 more)
|
||||
|
||||
### Community 106 - "Community 106"
|
||||
Cohesion: 0.15
|
||||
Nodes (33): create_vehicle(), get_vehicle(), paste_recognize_commit(), update_vehicle(), _vehicle_to_detail(), _vehicle_to_read(), generate_uuid(), PlVehicle (+25 more)
|
||||
Cohesion: 0.17
|
||||
Nodes (16): parse_resource_snapshot(), Any, 运输计划 - 安排运输时车辆/人员/证件资源快照 Schema, 从 ORM 文本列或 dict 解析快照。, SnapshotCertRead, SnapshotPersonnelRow, TransportResourceSnapshot, build_personnel_summary() (+8 more)
|
||||
|
||||
### Community 107 - "apply_pl_scope"
|
||||
Cohesion: 0.31
|
||||
Nodes (15): ensure_crm_create_by_row_in_scope(), create_pool_rule(), delete_pool_rules(), execute_pool_rules(), _execute_single_rule(), get_pool_rule(), list_pool_rules(), Any (+7 more)
|
||||
Cohesion: 0.15
|
||||
Nodes (14): invoice_info_add(), invoice_info_by_customer(), _invoice_info_by_customer_impl(), invoice_info_delete(), invoice_info_list(), invoice_info_set_default(), invoice_info_update(), 按客户ID查询开票信息(与 by-customer 等价) (+6 more)
|
||||
|
||||
### Community 109 - "Community 109"
|
||||
Cohesion: 0.20
|
||||
Nodes (13): create_expense_item(), delete_expense_item(), get_expense_item(), get_expense_item_tree(), Session, update_expense_item(), BxExpenseItem, _coerce_id() (+5 more)
|
||||
Cohesion: 0.15
|
||||
Nodes (17): create_expense_item(), delete_expense_item(), get_expense_item(), get_expense_item_tree(), Session, update_expense_item(), BxExpenseItem, _coerce_id() (+9 more)
|
||||
|
||||
### Community 110 - "Base"
|
||||
Cohesion: 0.16
|
||||
Nodes (19): get_web_auth_menu_codes(), 获取当前用户菜单权限标识(sys_menu.perms,经角色继承)。, get_user_menu_codes(), Session, 读取用户授权的菜单权限标识集合(与前端 meta.menuCode / perms 对齐)。, _expand_with_ancestor_menu_ids(), _granted_menu_ids_for_user(), iter_user_menu_perm_codes() (+11 more)
|
||||
Cohesion: 0.18
|
||||
Nodes (17): get_web_auth_menu_codes(), 获取当前用户菜单权限标识(sys_menu.perms,经角色继承)。, get_user_menu_codes(), Session, 读取用户授权的菜单权限标识集合(与前端 meta.menuCode / perms 对齐)。, _expand_with_ancestor_menu_ids(), _granted_menu_ids_for_user(), iter_user_menu_perm_codes() (+9 more)
|
||||
|
||||
### Community 111 - "Community 111"
|
||||
Cohesion: 0.28
|
||||
Nodes (14): _assert_editable(), _build_public_page(), _format_file_size(), _get_inventory_by_token(), get_public_inventory(), _parse_feedback_images(), Session, UploadFile (+6 more)
|
||||
Cohesion: 0.21
|
||||
Nodes (17): _assert_editable(), _build_public_page(), _format_file_size(), _get_inventory_by_token(), get_public_inventory(), _parse_feedback_images(), Session, UploadFile (+9 more)
|
||||
|
||||
### Community 112 - "storage.py"
|
||||
Cohesion: 0.21
|
||||
@@ -564,52 +565,52 @@ Cohesion: 0.33
|
||||
Nodes (8): allocate_next_biz_no(), _format_biz_no(), _prefix_for(), datetime, Session, 业务协议号自动生成:XBCC / JZCC + YYYYMMDD + 6 位序号, 原子分配下一个业务协议号。 须在 create 事务内调用;使用 MySQL LAST_INSERT_ID 保证并发安全。, BizNoKind
|
||||
|
||||
### Community 116 - "Community 116"
|
||||
Cohesion: 0.19
|
||||
Nodes (26): batch_insert_quote_products(), calculate_subtotal(), _decimal_value(), delete_products_by_quote_id(), _ensure_quote_parent_scope(), _get_field(), get_quote_product(), insert_quote_product() (+18 more)
|
||||
Cohesion: 0.20
|
||||
Nodes (24): batch_insert_quote_products(), calculate_subtotal(), _decimal_value(), delete_products_by_quote_id(), _ensure_quote_parent_scope(), _get_field(), get_quote_product(), insert_quote_product() (+16 more)
|
||||
|
||||
### Community 118 - "Community 118"
|
||||
Cohesion: 0.20
|
||||
Nodes (23): _parse_letter_oss_ids_from_tier(), batch_insert_contract_products(), calculate_subtotal(), _decimal_value(), delete_price_tiers_by_contract_id(), delete_products_by_contract_id(), _get_field(), insert_contract_product() (+15 more)
|
||||
Cohesion: 0.22
|
||||
Nodes (22): batch_insert_contract_products(), calculate_subtotal(), _decimal_value(), delete_price_tiers_by_contract_id(), delete_products_by_contract_id(), _get_field(), insert_contract_product(), _letter_oss_ids_to_json() (+14 more)
|
||||
|
||||
### Community 119 - "Community 119"
|
||||
Cohesion: 0.17
|
||||
Nodes (26): CertRow, _cert_row_complete(), collect_required_cert_errors(), _find_cert_for_required(), get_required_personnel_cert_names(), match_required_cert_name(), _missing_field_labels(), Session (+18 more)
|
||||
|
||||
### Community 122 - "Community 122"
|
||||
Cohesion: 0.19
|
||||
Nodes (16): get_storage(), 文件存储工具模块 支持多种存储方式: 1. 本地存储 (local) - 默认 2. 七牛云存储 (qiniu) - 推荐生产环境使用 3. 阿里云OSS (, download_file_bytes(), file_stream(), get_access_url(), get_download_url(), get_preview_url(), normalize_storage_path() (+8 more)
|
||||
Cohesion: 0.22
|
||||
Nodes (15): get_storage(), download_file_bytes(), file_stream(), get_access_url(), get_download_url(), get_preview_url(), normalize_storage_path(), 报销附件存储适配 — 复用门户 app.core.storage,对外保持 bx 协议。 (+7 more)
|
||||
|
||||
### Community 125 - "Community 125"
|
||||
Cohesion: 0.11
|
||||
Nodes (41): apply_site_feedback(), build_io_order_site_url(), create_io_order_for_transport(), _dibang_qty_by_plan_product(), enrich_io_items_with_brand_feedback(), enrich_io_order_dict(), _format_io_product_summary_line(), get_active_io_order_for_plan() (+33 more)
|
||||
Cohesion: 0.10
|
||||
Nodes (45): 按运输计划单据类型解析地磅往来单位(不区分 CRM 合同类型)。 - 入库单 inbound:供方 → party_a,其次 supplier_nam, resolve_partner_company(), apply_dibang_prefill_to_order(), apply_site_feedback(), build_io_order_site_url(), create_io_order_for_transport(), _dibang_qty_by_plan_product(), enrich_io_items_with_brand_feedback() (+37 more)
|
||||
|
||||
### Community 126 - "customer_pool.py"
|
||||
Cohesion: 0.28
|
||||
Nodes (15): create_clue_follow(), delete_clue_follow(), get_clue_follow(), get_follow_statistics(), list_clue_follows(), Any, Session, 更新线索跟进记录,必要时同步线索 next_time (+7 more)
|
||||
Cohesion: 0.52
|
||||
Nodes (6): apply_updates(), collect_candidates(), main(), parse_name(), Path, write_csv()
|
||||
|
||||
### Community 127 - "Community 127"
|
||||
Cohesion: 0.25
|
||||
Nodes (17): _cargo_stats(), get_dashboard_stats(), get_inbound_by_cargo(), get_inbound_by_partner_cargo(), get_outbound_by_cargo(), get_outbound_by_partner_cargo(), _partner_cargo_stats(), Session (+9 more)
|
||||
|
||||
### Community 128 - "plan.py"
|
||||
Cohesion: 0.07
|
||||
Nodes (62): archive_plan(), cancel_plan(), cancel_plan_wait_notice(), count_plans_by_contract(), create_from_contract(), create_plan(), delete_plan(), _determine_plan_type() (+54 more)
|
||||
Cohesion: 0.06
|
||||
Nodes (73): archive_plan(), _build_driver_url(), cancel_plan(), cancel_plan_wait_notice(), count_plans_by_contract(), create_from_contract(), create_plan(), create_plan_full() (+65 more)
|
||||
|
||||
### Community 129 - "BaseStorage"
|
||||
Cohesion: 0.29
|
||||
Nodes (16): ensure_crm_customer_linked_row_in_scope(), ensure_crm_owner_row_in_scope(), Session, CRM 数据权限辅助(负责人 owner_user_id + 标准 create_by/create_dept_id)。, 客户子表(开票信息等):先校验关联客户 owner scope,否则校验 create_by。, _clear_default_for_customer(), create_invoice_info(), get_invoice_info() (+8 more)
|
||||
Cohesion: 0.31
|
||||
Nodes (15): DbInboundOutbound, PlTransportDbLink, 运输-地磅出入库关联 pl_transport_db_link, _apply_plan_quantity_delta(), 再次勾选地磅安排前:软删本运输单此前生成的、仍可作废的出入库单及关联。 - 不删除地磅月计划(可能被其他业务复用) - 若出入库单已完成的/已, _resolve_link_contributed_qty(), revoke_regeneratable_dibang_for_transport(), _subtract_plan_contribution() (+7 more)
|
||||
|
||||
### Community 130 - "datetime_parse.py"
|
||||
Cohesion: 0.36
|
||||
Nodes (15): create_opportunity_follow(), _ensure_parent_opportunity(), get_opportunity_follow(), list_follows_by_opportunity(), list_opportunity_follows(), _parse_optional_datetime(), Any, datetime (+7 more)
|
||||
|
||||
### Community 131 - "LocalStorage"
|
||||
Cohesion: 0.15
|
||||
Nodes (14): invoice_info_add(), invoice_info_by_customer(), _invoice_info_by_customer_impl(), invoice_info_delete(), invoice_info_list(), invoice_info_set_default(), invoice_info_update(), 按客户ID查询开票信息(与 by-customer 等价) (+6 more)
|
||||
Cohesion: 0.18
|
||||
Nodes (23): paste_recognize_commit(), VehiclePersonnelRow, ConflictAction, PasteCommitBody, PasteCommitResult, PastePersonnelIn, PasteVehicleIn, 运输计划 - 三方粘贴识别提交 Schema (+15 more)
|
||||
|
||||
### Community 132 - "crm_supplier_hazard.py"
|
||||
Cohesion: 0.15
|
||||
Nodes (21): build_updates(), derive_source_url(), fetch_source_customers(), legacy_id_to_str(), load_oss_map(), load_runtime_settings(), load_settings_env(), main() (+13 more)
|
||||
Cohesion: 0.29
|
||||
Nodes (12): build_updates(), derive_source_url(), fetch_source_customers(), legacy_id_to_str(), load_oss_map(), load_runtime_settings(), load_settings_env(), main() (+4 more)
|
||||
|
||||
### Community 134 - "Community 134"
|
||||
Cohesion: 0.25
|
||||
@@ -620,8 +621,8 @@ Cohesion: 0.24
|
||||
Nodes (12): get_last_month_range(), month_range(), date, Session, 地磅计划按月自动生成 — 迁移自 simple-dibang-xiaoshou/auto-create-plan/auto_create_monthly_pla, 按月自动生成采购计划。 - 默认仅在每月 1 日执行(force=True 时跳过该限制,供手动触发验收) - 遍历所有租户,按上月计划模板生, run_auto_generate_monthly_plans(), default_execution_status() (+4 more)
|
||||
|
||||
### Community 136 - "Community 136"
|
||||
Cohesion: 0.22
|
||||
Nodes (16): create_role(), get_role(), get_role_data_scope(), get_role_menu_ids(), list_roles(), Any, Session, 角色 CRUD(sys_role / sys_role_menu / sys_role_data_scope_detail) (+8 more)
|
||||
Cohesion: 0.19
|
||||
Nodes (18): create_role(), get_role(), get_role_data_scope(), get_role_menu_ids(), list_roles(), Any, Session, 角色 CRUD(sys_role / sys_role_menu / sys_role_data_scope_detail) (+10 more)
|
||||
|
||||
### Community 137 - "statistics.py"
|
||||
Cohesion: 0.45
|
||||
@@ -644,12 +645,12 @@ Cohesion: 0.33
|
||||
Nodes (11): create_leave(), get_leave(), list_leaves(), Any, Session, _row_to_dict(), soft_delete_leave(), update_leave() (+3 more)
|
||||
|
||||
### Community 144 - "SysApiRequestLog"
|
||||
Cohesion: 0.27
|
||||
Nodes (11): create_dict_data(), get_dict_data(), list_dict_data(), list_dict_data_options(), Any, Session, 字典数据 CRUD(sys_dict_data), 字典下拉选项:租户主数据,忽略数据范围(user_info 保留兼容调用方,不参与过滤)。 (+3 more)
|
||||
Cohesion: 0.17
|
||||
Nodes (11): price_letter_add(), price_letter_detail(), price_letter_list(), price_letter_update(), price_letter_void(), PriceLetterCreateBody, PriceLetterDetailBody, PriceLetterListBody (+3 more)
|
||||
|
||||
### Community 145 - "minio_url.py"
|
||||
Cohesion: 0.39
|
||||
Nodes (10): create_site(), get_site(), list_sites(), Any, Session, _row_to_dict(), site_code_exists(), soft_delete_site() (+2 more)
|
||||
Cohesion: 0.24
|
||||
Nodes (13): normalize_attachment_csv(), normalize_attachment_csv(), extract_minio_object_key(), join_relative_urls(), normalize_oss_storage_url(), MinIO 附件 URL 规范化(数据迁移共用)。 背景 ---- CRM / 地磅 / 报销与 shanxia2.0 共用同一 MinIO(bucket 默, 业务表 OSS ID 串 → 相对 URL 串(用于客户证照、跟进附件等)。, 去掉预签名 query(``?X-Amz-...``)。 (+5 more)
|
||||
|
||||
### Community 146 - "__init__.py"
|
||||
Cohesion: 0.29
|
||||
@@ -660,24 +661,28 @@ Cohesion: 0.37
|
||||
Nodes (17): check_env(), check_python(), create_venv(), init_env(), install_dependencies(), log_error(), log_info(), log_success() (+9 more)
|
||||
|
||||
### Community 150 - "ContractListQuery"
|
||||
Cohesion: 0.08
|
||||
Nodes (44): CRM 合同 API 路由 — GET/PUT/DELETE/POST 对齐原 Java CRM, CRM 合同子模块路由 — 变更/模板/审批独立页, total_pages(), ContractApprovalActionBody, ContractApprovalExportBody, ContractApprovalListQuery, ContractApprovalPassBody, ContractApprovalRefreshNamesBody (+36 more)
|
||||
Cohesion: 0.10
|
||||
Nodes (32): CRM 合同 API 路由 — GET/PUT/DELETE/POST 对齐原 Java CRM, 接口菜单权限守卫。super_admin 直接放行。, require_menu_code(), ContractApproverSettingSaveBody, ContractApprovalActionBody, ContractBatchAssignBody, ContractBatchSubmitBody, ContractChangeCreateBody (+24 more)
|
||||
|
||||
### Community 152 - "Community 152"
|
||||
Cohesion: 0.09
|
||||
Nodes (21): 分页查询供方列表 — Java: CrmSupplierInfoController.list, 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方详情 — Java: CrmSupplierInfoController.getInfo, 新增供方 — Java: CrmSupplierInfoController.add, 修改供方 — Java: CrmSupplierInfoController.edit, 删除供方 — Java: CrmSupplierInfoController.remove, 导出供方列表 — Java: CrmSupplierInfoController.export, supplier_info_add() (+13 more)
|
||||
Cohesion: 0.12
|
||||
Nodes (16): 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方详情 — Java: CrmSupplierInfoController.getInfo, 新增供方 — Java: CrmSupplierInfoController.add, 修改供方 — Java: CrmSupplierInfoController.edit, 删除供方 — Java: CrmSupplierInfoController.remove, supplier_info_add(), supplier_info_delete(), supplier_info_detail() (+8 more)
|
||||
|
||||
### Community 154 - "web_login_rsa_public_key"
|
||||
Cohesion: 0.13
|
||||
Nodes (19): get_current_time(), parse_datetime(), datetime, get_pool_record(), list_pool_records(), Any, Session, _row_to_dict() (+11 more)
|
||||
Cohesion: 0.10
|
||||
Nodes (27): get_current_time(), parse_datetime(), datetime, get_pool_record(), list_pool_records(), Any, Session, _row_to_dict() (+19 more)
|
||||
|
||||
### Community 156 - "get_client_ip"
|
||||
Cohesion: 0.27
|
||||
Nodes (11): customer_dealed(), customer_export(), customer_key(), customer_list(), customer_long_time_no_follow(), customer_mine(), customer_to_follow(), customer_week_new() (+3 more)
|
||||
Nodes (11): create_dict_data(), get_dict_data(), list_dict_data(), list_dict_data_options(), Any, Session, 字典数据 CRUD(sys_dict_data), 字典下拉选项:租户主数据,忽略数据范围(user_info 保留兼容调用方,不参与过滤)。 (+3 more)
|
||||
|
||||
### Community 159 - "context.py"
|
||||
Cohesion: 0.48
|
||||
Nodes (7): _opp_list_kwargs(), opportunity_export(), opportunity_kanban(), opportunity_list(), opportunity_mine_list(), OpportunityKanbanBody, OpportunityListBody
|
||||
|
||||
### Community 160 - "register_local_callback"
|
||||
Cohesion: 0.15
|
||||
Nodes (16): 数据模型(与 docs/sql/shanxia2.0.sql 全库导出基线对齐的核心系统表), Web 管理端约定角色(由 sys_role.role_name 映射而来,非数据库枚举), 流程实例 wf_process_instance, UserRole, WfProcessInstance, _dispatch_local(), Any, 工作流回调服务 —— 流程状态变更时通知业务系统 (+8 more)
|
||||
Cohesion: 0.60
|
||||
Nodes (5): product_allowed(), test_dual_requires_both(), test_explosive_only_requires_allow_explosive(), test_plain_always_allowed(), test_toxic_only_requires_allow_toxic()
|
||||
|
||||
### Community 161 - "backfill_crm_contract_attachment_urls.py"
|
||||
Cohesion: 0.33
|
||||
@@ -691,6 +696,10 @@ Nodes (3): BaseStorage, 带下载文件名的访问 URL;默认实现为普通
|
||||
Cohesion: 0.31
|
||||
Nodes (9): decrypt_login_payload(), ensure_initialized(), extract_login_credentials(), get_public_pem(), Any, 登录凭据 RSA 解密(与前端 JSEncrypt PKCS#1 v1.5 兼容)。, Base64 密文 -> JSON 对象(须含 username、password、ts 毫秒时间戳)。, 从请求体解析用户名、密码。 返回 (username, password, error_message);error_message 非空时表示失败。 (+1 more)
|
||||
|
||||
### Community 170 - "test_quote_approver_setting.py"
|
||||
Cohesion: 0.20
|
||||
Nodes (21): _assert_users_exist(), get_setting(), Any, Session, _row_to_dict(), save_setting(), _assert_users_exist(), get_setting() (+13 more)
|
||||
|
||||
### Community 172 - "MinIOStorage"
|
||||
Cohesion: 0.32
|
||||
Nodes (3): MinIOStorage, MinIO 预签名下载 URL,携带原始文件名。, MinIO 预签名内联预览 URL(PDF 等 iframe 场景)。
|
||||
@@ -730,22 +739,22 @@ Nodes (6): eq_unicode(), like_unicode(), MySQL 字符串比较/模糊匹配时
|
||||
## Knowledge Gaps
|
||||
- **30 isolated node(s):** `Config`, `Config`, `Config`, `Config`, `Config` (+25 more)
|
||||
These have ≤1 connection - possible missing edges or undocumented components.
|
||||
- **17 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
||||
- **16 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
||||
|
||||
## Suggested Questions
|
||||
_Questions this graph is uniquely positioned to answer:_
|
||||
|
||||
- **Why does `generate_uuid()` connect `Community 106` to `plan.py`, `BaseStorage`, `Community 2`, `datetime_parse.py`, `Community 4`, `Community 134`, `Community 6`, `Community 8`, `Community 136`, `post.py`, `Community 11`, `Community 7`, `Community 13`, `Community 14`, `SysEthnic`, `SysApiRequestLog`, `minio_url.py`, `__init__.py`, `Community 19`, `Community 15`, `Community 21`, `Community 16`, `Community 25`, `Community 26`, `web_login_rsa_public_key`, `Community 27`, `Community 29`, `Community 28`, `Community 34`, `Community 38`, `Community 39`, `Community 43`, `Community 45`, `Community 46`, `Community 51`, `menu.py`, `Community 53`, `Community 61`, `Community 65`, `Community 66`, `Community 67`, `Community 70`, `Community 74`, `Community 83`, `Community 86`, `Community 87`, `pl_ocr_service.py`, `pl_vehicle_recharge_service.py`, `Community 92`, `_invoice_info_by_customer_impl`, `Community 95`, `_ensure_subpack_slip_no`, `dept.py`, `apply_pl_scope`, `storage.py`, `quote_share_log.py`, `db_models.py`, `site.py`, `Community 116`, `Community 118`, `Community 125`, `customer_pool.py`?**
|
||||
_High betweenness centrality (0.130) - this node is a cross-community bridge._
|
||||
- **Why does `BaseModel` connect `Community 12` to `plan.py`, `Community 1`, `Community 2`, `Community 3`, `LocalStorage`, `Community 11`, `Community 14`, `Community 15`, `Community 16`, `Community 18`, `ContractListQuery`, `Community 24`, `Community 25`, `Community 152`, `Community 27`, `get_client_ip`, `Community 28`, `Community 30`, `Community 29`, `Community 33`, `Community 36`, `Community 43`, `Community 45`, `Community 46`, `Community 48`, `Community 49`, `Community 50`, `Community 54`, `Community 58`, `Community 60`, `Community 63`, `Community 64`, `Community 65`, `Community 66`, `Community 68`, `Community 71`, `Community 81`, `Community 86`, `_ensure_subpack_slip_no`, `list_customers`, `Community 106`, `Community 109`, `Community 111`, `Community 119`, `Community 127`?**
|
||||
_High betweenness centrality (0.120) - this node is a cross-community bridge._
|
||||
- **Why does `generate_uuid()` connect `Community 38` to `plan.py`, `datetime_parse.py`, `LocalStorage`, `Community 4`, `Community 134`, `post.py`, `Community 8`, `Community 136`, `Community 6`, `Community 11`, `Community 7`, `Community 13`, `Community 14`, `SysEthnic`, `Community 15`, `Community 16`, `__init__.py`, `Community 19`, `Community 18`, `Community 21`, `Community 25`, `Community 26`, `web_login_rsa_public_key`, `Community 27`, `get_client_ip`, `Community 30`, `Community 28`, `Community 29`, `Community 34`, `Community 39`, `test_quote_approver_setting.py`, `Community 43`, `Community 51`, `menu.py`, `Community 53`, `Community 61`, `Community 65`, `Community 66`, `Community 67`, `Community 68`, `Community 70`, `Community 74`, `Community 83`, `Community 86`, `Community 87`, `pl_ocr_service.py`, `pl_vehicle_recharge_service.py`, `Community 92`, `_invoice_info_by_customer_impl`, `response.py`, `Community 95`, `_ensure_subpack_slip_no`, `dept.py`, `Community 109`, `storage.py`, `quote_share_log.py`, `db_models.py`, `site.py`, `Community 116`, `Community 118`, `Community 125`, `customer_pool.py`?**
|
||||
_High betweenness centrality (0.121) - this node is a cross-community bridge._
|
||||
- **Why does `Settings` connect `price_letter_detail` to `backfill_crm_contract_attachment_urls.py`, `Community 35`, `crm_supplier_hazard.py`, `Community 5`, `Community 7`, `Community 72`, `Community 73`, `Community 42`, `Community 78`, `Community 52`, `Any`, `Community 59`, `api_crypto_middleware.py`, `Community 62`?**
|
||||
_High betweenness centrality (0.113) - this node is a cross-community bridge._
|
||||
_High betweenness centrality (0.111) - this node is a cross-community bridge._
|
||||
- **Why does `BaseModel` connect `Community 12` to `plan.py`, `Community 1`, `Community 3`, `LocalStorage`, `Community 6`, `Community 11`, `Community 14`, `Community 15`, `SysApiRequestLog`, `Community 16`, `Community 18`, `ContractListQuery`, `Community 152`, `Community 25`, `Community 27`, `Community 30`, `context.py`, `Community 33`, `Community 36`, `Community 43`, `Community 48`, `Community 49`, `Community 50`, `Community 54`, `Community 58`, `Community 60`, `Community 63`, `Community 64`, `Community 65`, `Community 66`, `Community 71`, `Community 83`, `Community 86`, `_ensure_subpack_slip_no`, `list_customers`, `Community 106`, `apply_pl_scope`, `Community 109`, `Community 111`, `Community 119`, `Community 127`?**
|
||||
_High betweenness centrality (0.093) - this node is a cross-community bridge._
|
||||
- **Are the 8 inferred relationships involving `DbUserContext` (e.g. with `InventoryRelatedData` and `AutoGenerateMonthlyResult`) actually correct?**
|
||||
_`DbUserContext` has 8 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **What connects `API 路由包 — 山峡化工生产一体化门户`, `返回运营端角色下拉选项(label=库中角色名,value=Web 角色码)。`, `返回用户下拉选项(label=昵称,value=用户ID,附带 nickName/userNo)。 供申请人/审批人等选择场景使用,返回本租户全部有效` to the rest of the system?**
|
||||
_492 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
- **Should `Community 0` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.09343200740055504 - nodes in this community are weakly interconnected._
|
||||
- **Should `Community 2` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.13548387096774195 - nodes in this community are weakly interconnected._
|
||||
_493 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
- **Should `Community 1` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.1251778093883357 - nodes in this community are weakly interconnected._
|
||||
- **Should `Community 3` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.07668413900213551 - nodes in this community are weakly interconnected._
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_py", "label": "contract.py", "file_type": "code", "source_file": "app/crud/crm/contract.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_ensure_contract_in_scope", "label": "_ensure_contract_in_scope()", "file_type": "code", "source_file": "app/crud/crm/contract.py", "source_location": "L48"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/crud/crm/contract.py"}, {"id": "crmcontract", "label": "CrmContract", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/crud/crm/contract.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_get_contract_row_in_scope", "label": "_get_contract_row_in_scope()", "file_type": "code", "source_file": "app/crud/crm/contract.py", "source_location": "L52"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_total_pages", "label": "total_pages()", "file_type": "code", "source_file": "app/crud/crm/contract.py", "source_location": "L69"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_decimal_value", "label": "_decimal_value()", "file_type": "code", "source_file": "app/crud/crm/contract.py", "source_location": "L75"}, {"id": "any", "label": "Any", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/crud/crm/contract.py"}, {"id": "decimal", "label": "Decimal", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/crud/crm/contract.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_parse_optional_date", "label": "_parse_optional_date()", "file_type": "code", "source_file": "app/crud/crm/contract.py", "source_location": "L83"}, {"id": "date", "label": "date", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/crud/crm/contract.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_parse_optional_datetime", "label": "_parse_optional_datetime()", "file_type": "code", "source_file": "app/crud/crm/contract.py", "source_location": "L101"}, {"id": "datetime", "label": "datetime", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/crud/crm/contract.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_get_field", "label": "_get_field()", "file_type": "code", "source_file": "app/crud/crm/contract.py", "source_location": "L117"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_resolve_user_display_name", "label": "_resolve_user_display_name()", "file_type": "code", "source_file": "app/crud/crm/contract.py", "source_location": "L124"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_is_blank", "label": "_is_blank()", "file_type": "code", "source_file": "app/crud/crm/contract.py", "source_location": "L134"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_first_non_blank", "label": "_first_non_blank()", "file_type": "code", "source_file": "app/crud/crm/contract.py", "source_location": "L138"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_get_default_invoice_info", "label": "_get_default_invoice_info()", "file_type": "code", "source_file": "app/crud/crm/contract.py", "source_location": "L146"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_build_customer_address", "label": "_build_customer_address()", "file_type": "code", "source_file": "app/crud/crm/contract.py", "source_location": "L156"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_row_to_dict", "label": "_row_to_dict()", "file_type": "code", "source_file": "app/crud/crm/contract.py", "source_location": "L168"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_contract_row_to_dict", "label": "contract_row_to_dict()", "file_type": "code", "source_file": "app/crud/crm/contract.py", "source_location": "L245"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_build_contract_filters", "label": "_build_contract_filters()", "file_type": "code", "source_file": "app/crud/crm/contract.py", "source_location": "L249"}, {"id": "query", "label": "Query", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/crud/crm/contract.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_list_contracts", "label": "list_contractsLine truncated
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_quote_approver_setting_py", "label": "test_quote_approver_setting.py", "file_type": "code", "source_file": "tests/test_quote_approver_setting.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_quote_approver_setting_test_parse_and_join_roundtrip", "label": "test_parse_and_join_roundtrip()", "file_type": "code", "source_file": "tests/test_quote_approver_setting.py", "source_location": "L8"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_quote_approver_setting_test_prefill_requires_enabled_and_nonempty", "label": "test_prefill_requires_enabled_and_nonempty()", "file_type": "code", "source_file": "tests/test_quote_approver_setting.py", "source_location": "L16"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_quote_approver_setting_test_prefill_filters_against_valid_ids", "label": "test_prefill_filters_against_valid_ids()", "file_type": "code", "source_file": "tests/test_quote_approver_setting.py", "source_location": "L23"}], "edges": [{"source": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_quote_approver_setting_py", "target": "app_crud_crm_quote_approver_setting", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "tests/test_quote_approver_setting.py", "source_location": "L1", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_quote_approver_setting_py", "target": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_quote_approver_setting_test_parse_and_join_roundtrip", "relation": "contains", "confidence": "EXTRACTED", "source_file": "tests/test_quote_approver_setting.py", "source_location": "L8", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_quote_approver_setting_py", "target": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_quote_approver_setting_test_prefill_requires_enabled_and_nonempty", "relation": "contains", "confidence": "EXTRACTED", "source_file": "tests/test_quote_approver_setting.py", "source_location": "L16", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_quote_approver_setting_py", "target": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_quote_approver_setting_test_prefill_filters_against_valid_ids", "relation": "contains", "confidence": "EXTRACTED", "source_file": "tests/test_quote_approver_setting.py", "source_location": "L23", "weight": 1.0}], "raw_calls": [{"caller_nid": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_quote_approver_setting_test_parse_and_join_roundtrip", "callee": "parse_approver_ids", "is_member_call": false, "source_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/tests/test_quote_approver_setting.py", "source_location": "L9", "receiver": null}, {"caller_nid": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_quote_approver_setting_test_parse_and_join_roundtrip", "callee": "parse_approver_ids", "is_member_call": false, "source_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/tests/test_quote_approver_setting.py", "source_location": "L10", "receiver": null}, {"caller_nid": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_quote_approver_setting_test_parse_and_join_roundtrip", "callee": "parse_approver_ids", "is_member_call": false, "source_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/tests/test_quote_approver_setting.py", "source_location": "L11", "receiver": null}, {"caller_nid": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_quote_approver_setting_test_parse_and_join_roundtrip", "callee": "parse_approver_ids", "is_member_call": false, "source_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/tests/test_quote_approver_setting.py", "source_location": "L12", "receiver": null}, {"caller_nid": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_quote_approver_setting_test_parse_and_join_roundtrip", "callee": "join_approver_ids", "is_member_call": false, "source_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/tests/test_quote_approver_setting.py", "source_location": "L13", "receiver": null}, {"caller_nid": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_quote_approver_setting_test_prefill_requires_enabled_and_nonempty", "callee": "prefill_approver_ids", "is_member_call": false, "source_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/tests/test_quote_approver_setting.py", "source_location": "L17", "receiver": null}, {"caller_nid": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_quote_approver_setting_test_prefill_requires_enabled_and_nonempty", "callee": "prefill_approver_ids", "is_member_call": false, "source_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/tests/test_quote_approver_setting.py", "source_location": "L18", "receiver": null}, {"caller_nid": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_quote_approvLine truncated
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_py", "label": "crm_models.py", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmproductcategory", "label": "CrmProductCategory", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L21"}, {"id": "base", "label": "Base", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmproduct", "label": "CrmProduct", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L43"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmproductbrand", "label": "CrmProductBrand", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L78"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmproductalias", "label": "CrmProductAlias", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L98"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_sysoss", "label": "SysOss", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L117"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmcustomer", "label": "CrmCustomer", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L149"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmcontact", "label": "CrmContact", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L232"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crminvoiceinfo", "label": "CrmInvoiceInfo", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L267"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmcustomerfollow", "label": "CrmCustomerFollow", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L295"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmcustomerpool", "label": "CrmCustomerPool", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L326"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmcustomerpoolrule", "label": "CrmCustomerPoolRule", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L352"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmopportunity", "label": "CrmOpportunity", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L375"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmopportunityfollow", "label": "CrmOpportunityFollow", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L418"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmopportunityproduct", "label": "CrmOpportunityProduct", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L449"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmopportunitystagelog", "label": "CrmOpportunityStageLog", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L479"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmcontract", "label": "CrmContract", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L510"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmcontractapproval", "label": "CrmContractApproval", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L593"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmcontractchange", "label": "CrmContractChange", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L624"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmcontracttemplate", "label": "CrmContractTemplate", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L655"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmcontractproduct", "label": "CrmContractProduct", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L689"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmcontractproductpricetier", "label": "CrmContractProductPriceTier", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L730"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmquote", "label": "CrmQuote", "file_type": "code", "source_file": "app/models/crmLine truncated
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_py", "label": "quote_approver_setting.py", "file_type": "code", "source_file": "app/crud/crm/quote_approver_setting.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_parse_approver_ids", "label": "parse_approver_ids()", "file_type": "code", "source_file": "app/crud/crm/quote_approver_setting.py", "source_location": "L6"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_join_approver_ids", "label": "join_approver_ids()", "file_type": "code", "source_file": "app/crud/crm/quote_approver_setting.py", "source_location": "L17"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_prefill_approver_ids", "label": "prefill_approver_ids()", "file_type": "code", "source_file": "app/crud/crm/quote_approver_setting.py", "source_location": "L21"}, {"id": "any", "label": "Any", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/crud/crm/quote_approver_setting.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_rationale_26", "label": "\u8fd4\u56de\u53ef\u9884\u586b\u7684\u5ba1\u6279\u4eba ID \u5217\u8868\uff1b\u4e0d\u53ef\u7528\u5219\u7a7a\u5217\u8868\u3002", "file_type": "rationale", "source_file": "app/crud/crm/quote_approver_setting.py", "source_location": "L26"}], "edges": [{"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_py", "target": "typing", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/crud/crm/quote_approver_setting.py", "source_location": "L3", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_py", "target": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_parse_approver_ids", "relation": "contains", "confidence": "EXTRACTED", "source_file": "app/crud/crm/quote_approver_setting.py", "source_location": "L6", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_py", "target": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_join_approver_ids", "relation": "contains", "confidence": "EXTRACTED", "source_file": "app/crud/crm/quote_approver_setting.py", "source_location": "L17", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_py", "target": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_prefill_approver_ids", "relation": "contains", "confidence": "EXTRACTED", "source_file": "app/crud/crm/quote_approver_setting.py", "source_location": "L21", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_prefill_approver_ids", "target": "any", "relation": "references", "context": "generic_arg", "confidence": "EXTRACTED", "source_file": "app/crud/crm/quote_approver_setting.py", "source_location": "L21", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_rationale_26", "target": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_prefill_approver_ids", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "app/crud/crm/quote_approver_setting.py", "source_location": "L26", "weight": 1.0}], "raw_calls": [{"caller_nid": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_parse_approver_ids", "callee": "split", "is_member_call": true, "source_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/crud/crm/quote_approver_setting.py", "source_location": "L10", "receiver": null}, {"caller_nid": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_parse_approver_ids", "callee": "strip", "is_member_call": true, "source_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/crud/crm/quote_approver_setting.py", "source_location": "L11", "receiver": "part"}, {"caller_nid": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_parse_approver_ids", "callee": "append", "is_member_call": true, "source_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/crud/crm/quote_approver_setting.py", "source_location": "L13", "receiver": "out"}, {"caller_nid": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_join_approver_ids", "callee": "join", "is_member_call": true, "source_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/crud/crm/quote_approver_setting.py", "source_location": "L18", "receiver": null}, {"caller_nid": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_join_apprLine truncated
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "label": "crm_quote_routes.py", "file_type": "code", "source_file": "app/api/crm_quote_routes.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_register_quote_routes", "label": "register_quote_routes()", "file_type": "code", "source_file": "app/api/crm_quote_routes.py", "source_location": "L85"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_rationale_1", "label": "CRM \u62a5\u4ef7\u5355 API \u8def\u7531 \u2014 \u6302\u63a5\u5230 crm.router", "file_type": "rationale", "source_file": "app/api/crm_quote_routes.py", "source_location": "L1"}], "edges": [{"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "datetime", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_quote_routes.py", "source_location": "L4", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "fastapi", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_quote_routes.py", "source_location": "L6", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "sqlalchemy_orm", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_quote_routes.py", "source_location": "L7", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_core_database", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_quote_routes.py", "source_location": "L9", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_core_menu_guard", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_quote_routes.py", "source_location": "L10", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_core_response", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_quote_routes.py", "source_location": "L11", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_core_tenant_context", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_quote_routes.py", "source_location": "L12", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_crud_crm_data_scope_helpers", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_quote_routes.py", "source_location": "L13", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_crud_crm_quote", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_quote_routes.py", "source_location": "L14", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_crud_crm_quote_approver_setting", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_quote_routes.py", "source_location": "L36", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_crud_crm_quote_approval", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_quote_routes.py", "source_location": "L37", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_crud_crm_quote_product", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_quote_routes.py", "source_location": "L43", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_crud_crm_quote_share_log", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_quote_routes.py", "source_location": "L49", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_schemas_crm_quote_approver_setting", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_quote_routes.py", "source_location": "L50", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_schemas_crm_quote", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_Line truncated
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "label": "crm_quote_routes.py", "file_type": "code", "source_file": "app/api/crm_quote_routes.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_register_quote_routes", "label": "register_quote_routes()", "file_type": "code", "source_file": "app/api/crm_quote_routes.py", "source_location": "L85"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_rationale_1", "label": "CRM \u62a5\u4ef7\u5355 API \u8def\u7531 \u2014 \u6302\u63a5\u5230 crm.router", "file_type": "rationale", "source_file": "app/api/crm_quote_routes.py", "source_location": "L1"}], "edges": [{"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "datetime", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_quote_routes.py", "source_location": "L4", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "fastapi", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_quote_routes.py", "source_location": "L6", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "sqlalchemy_orm", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_quote_routes.py", "source_location": "L7", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_core_database", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_quote_routes.py", "source_location": "L9", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_core_menu_guard", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_quote_routes.py", "source_location": "L10", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_core_response", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_quote_routes.py", "source_location": "L11", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_core_tenant_context", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_quote_routes.py", "source_location": "L12", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_crud_crm_data_scope_helpers", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_quote_routes.py", "source_location": "L13", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_crud_crm_quote", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_quote_routes.py", "source_location": "L14", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_crud_crm_quote_approver_setting", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_quote_routes.py", "source_location": "L36", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_crud_crm_quote_approval", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_quote_routes.py", "source_location": "L37", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_crud_crm_quote_product", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_quote_routes.py", "source_location": "L43", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_crud_crm_quote_share_log", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_quote_routes.py", "source_location": "L49", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_schemas_crm_quote_approver_setting", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_quote_routes.py", "source_location": "L50", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_schemas_crm_quote", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_Line truncated
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_py", "label": "quote_approver_setting.py", "file_type": "code", "source_file": "app/crud/crm/quote_approver_setting.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_parse_approver_ids", "label": "parse_approver_ids()", "file_type": "code", "source_file": "app/crud/crm/quote_approver_setting.py", "source_location": "L6"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_join_approver_ids", "label": "join_approver_ids()", "file_type": "code", "source_file": "app/crud/crm/quote_approver_setting.py", "source_location": "L17"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_prefill_approver_ids", "label": "prefill_approver_ids()", "file_type": "code", "source_file": "app/crud/crm/quote_approver_setting.py", "source_location": "L21"}, {"id": "any", "label": "Any", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/crud/crm/quote_approver_setting.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_row_to_dict", "label": "_row_to_dict()", "file_type": "code", "source_file": "app/crud/crm/quote_approver_setting.py", "source_location": "L46"}, {"id": "crmquoteapproversetting", "label": "CrmQuoteApproverSetting", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/crud/crm/quote_approver_setting.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_get_setting", "label": "get_setting()", "file_type": "code", "source_file": "app/crud/crm/quote_approver_setting.py", "source_location": "L56"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/crud/crm/quote_approver_setting.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_assert_users_exist", "label": "_assert_users_exist()", "file_type": "code", "source_file": "app/crud/crm/quote_approver_setting.py", "source_location": "L69"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_save_setting", "label": "save_setting()", "file_type": "code", "source_file": "app/crud/crm/quote_approver_setting.py", "source_location": "L84"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_rationale_26", "label": "\u8fd4\u56de\u53ef\u9884\u586b\u7684\u5ba1\u6279\u4eba ID \u5217\u8868\uff1b\u4e0d\u53ef\u7528\u5219\u7a7a\u5217\u8868\u3002", "file_type": "rationale", "source_file": "app/crud/crm/quote_approver_setting.py", "source_location": "L26"}], "edges": [{"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_py", "target": "typing", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/crud/crm/quote_approver_setting.py", "source_location": "L3", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_py", "target": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_parse_approver_ids", "relation": "contains", "confidence": "EXTRACTED", "source_file": "app/crud/crm/quote_approver_setting.py", "source_location": "L6", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_py", "target": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_join_approver_ids", "relation": "contains", "confidence": "EXTRACTED", "source_file": "app/crud/crm/quote_approver_setting.py", "source_location": "L17", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_py", "target": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_prefill_approver_ids", "relation": "contains", "confidence": "EXTRACTED", "source_file": "app/crud/crm/quote_approver_setting.py", "source_location": "L21", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_prefill_approver_ids", "target": "any", "relation": "references", "context": "generic_arg", "confidence": "EXTRACTED", "source_file": "app/crud/crm/quote_approver_setting.py", "source_location": "L21", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_quote_approver_setting_py", "target": "datetime", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/crud/crm/quote_approver_setting.py", "source_location": "L37", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_Line truncated
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_approver_setting_py", "label": "contract_approver_setting.py", "file_type": "code", "source_file": "app/crud/crm/contract_approver_setting.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_approver_setting_row_to_dict", "label": "_row_to_dict()", "file_type": "code", "source_file": "app/crud/crm/contract_approver_setting.py", "source_location": "L14"}, {"id": "crmcontractapproversetting", "label": "CrmContractApproverSetting", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/crud/crm/contract_approver_setting.py"}, {"id": "any", "label": "Any", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/crud/crm/contract_approver_setting.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_approver_setting_get_setting", "label": "get_setting()", "file_type": "code", "source_file": "app/crud/crm/contract_approver_setting.py", "source_location": "L24"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/crud/crm/contract_approver_setting.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_approver_setting_assert_users_exist", "label": "_assert_users_exist()", "file_type": "code", "source_file": "app/crud/crm/contract_approver_setting.py", "source_location": "L37"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_approver_setting_save_setting", "label": "save_setting()", "file_type": "code", "source_file": "app/crud/crm/contract_approver_setting.py", "source_location": "L52"}], "edges": [{"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_approver_setting_py", "target": "datetime", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/crud/crm/contract_approver_setting.py", "source_location": "L3", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_approver_setting_py", "target": "typing", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/crud/crm/contract_approver_setting.py", "source_location": "L4", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_approver_setting_py", "target": "sqlalchemy_orm", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/crud/crm/contract_approver_setting.py", "source_location": "L6", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_approver_setting_py", "target": "app_core_utils", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/crud/crm/contract_approver_setting.py", "source_location": "L8", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_approver_setting_py", "target": "app_crud_crm_quote_approver_setting", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/crud/crm/contract_approver_setting.py", "source_location": "L9", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_approver_setting_py", "target": "app_models_crm_models", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/crud/crm/contract_approver_setting.py", "source_location": "L10", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_approver_setting_py", "target": "app_models_models", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/crud/crm/contract_approver_setting.py", "source_location": "L11", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_approver_setting_py", "target": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_approver_setting_row_to_dict", "relation": "contains", "confidence": "EXTRACTED", "source_file": "app/crud/crm/contract_approver_setting.py", "source_location": "L14", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_approver_setting_row_to_dict", "target": "crmcontractapproversetting", "relation": "references", "context": "parameter_type", "confidence": "EXTRACTED", "source_file": "app/crud/crm/contract_approver_setting.py", "source_location": "L14", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_crud_crm_contract_approver_setting_row_to_dict", "target": "any", "relation": "references", "context": "generic_arg", "confidence": "EXTRACTED", "source_file": "aLine truncated
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_io_order_py", "label": "io_order.py", "file_type": "code", "source_file": "app/api/pl/io_order.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_io_order_models", "label": "_models()", "file_type": "code", "source_file": "app/api/pl/io_order.py", "source_location": "L34"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_io_order_get_order", "label": "_get_order()", "file_type": "code", "source_file": "app/api/pl/io_order.py", "source_location": "L40"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/pl/io_order.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_io_order_to_read", "label": "_to_read()", "file_type": "code", "source_file": "app/api/pl/io_order.py", "source_location": "L58"}, {"id": "ioorderread", "label": "IoOrderRead", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/pl/io_order.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_io_order_list_orders", "label": "_list_orders()", "file_type": "code", "source_file": "app/api/pl/io_order.py", "source_location": "L64"}, {"id": "dbusercontext", "label": "DbUserContext", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/pl/io_order.py"}, {"id": "ioorderlistresponse", "label": "IoOrderListResponse", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/pl/io_order.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_io_order_patch_order", "label": "_patch_order()", "file_type": "code", "source_file": "app/api/pl/io_order.py", "source_location": "L125"}, {"id": "ioorderpatch", "label": "IoOrderPatch", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/pl/io_order.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_io_order_get_by_plan", "label": "_get_by_plan()", "file_type": "code", "source_file": "app/api/pl/io_order.py", "source_location": "L182"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_io_order_get_inbound_by_plan", "label": "get_inbound_by_plan()", "file_type": "code", "source_file": "app/api/pl/io_order.py", "source_location": "L207"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_io_order_list_inbound", "label": "list_inbound()", "file_type": "code", "source_file": "app/api/pl/io_order.py", "source_location": "L220"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_io_order_get_inbound", "label": "get_inbound()", "file_type": "code", "source_file": "app/api/pl/io_order.py", "source_location": "L252"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_io_order_patch_inbound", "label": "patch_inbound()", "file_type": "code", "source_file": "app/api/pl/io_order.py", "source_location": "L263"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_io_order_inbound_site_url", "label": "inbound_site_url()", "file_type": "code", "source_file": "app/api/pl/io_order.py", "source_location": "L273"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_io_order_get_outbound_by_plan", "label": "get_outbound_by_plan()", "file_type": "code", "source_file": "app/api/pl/io_order.py", "source_location": "L285"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_io_order_list_outbound", "label": "list_outbound()", "file_type": "code", "source_file": "app/api/pl/io_order.py", "source_location": "L298"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_io_order_get_outbound", "label": "get_outbound()", "file_type": "code", "source_file": "app/api/pl/io_order.py", "source_location": "L330"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_io_order_patch_outbound", "label": "patch_outbound()", "file_type": "code", "source_file": "app/api/pl/io_order.py", "source_location": "L341"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_io_order_outbound_site_url", "label": "outbound_site_url()", "file_type": "code", "source_file": "app/api/pl/io_order.py", "source_location": "L351"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_io_order_rationale_1", "label": "\u8fd0\u8f93\u8ba1\u5212 \u00b7 \u5165\u5e93\u5355 / \u51fa\u5e93\u5355\u7ba1\u7406\u7aef API", "file_type": "rationale", "source_file": "app/api/pl/io_order.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_io_order_rationale_183", "label": "\u6309\u8ba1\u5212\u67e5\u627e\u5165\u5e93/\u51fa\u5e93\u5355\uff1b\u672a\u843d\u5355\u6Line truncated
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_py", "label": "crm_models.py", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmproductcategory", "label": "CrmProductCategory", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L21"}, {"id": "base", "label": "Base", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmproduct", "label": "CrmProduct", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L43"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmproductbrand", "label": "CrmProductBrand", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L78"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmproductalias", "label": "CrmProductAlias", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L98"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_sysoss", "label": "SysOss", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L117"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmcustomer", "label": "CrmCustomer", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L149"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmcontact", "label": "CrmContact", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L232"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crminvoiceinfo", "label": "CrmInvoiceInfo", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L267"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmcustomerfollow", "label": "CrmCustomerFollow", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L295"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmcustomerpool", "label": "CrmCustomerPool", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L326"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmcustomerpoolrule", "label": "CrmCustomerPoolRule", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L352"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmopportunity", "label": "CrmOpportunity", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L375"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmopportunityfollow", "label": "CrmOpportunityFollow", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L418"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmopportunityproduct", "label": "CrmOpportunityProduct", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L449"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmopportunitystagelog", "label": "CrmOpportunityStageLog", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L479"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmcontract", "label": "CrmContract", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L510"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmcontractapproversetting", "label": "CrmContractApproverSetting", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L593"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmcontractapproval", "label": "CrmContractApproval", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L612"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmcontractchange", "label": "CrmContractChange", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L643"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmcontracttemplate", "label": "CrmContractTemplate", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L674"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmcontractproduct", "label": "CrmContractProduct", "file_type": "code", "source_file": "app/models/crm_models.py", "source_location": "L708"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_crm_models_crmcontractproductpricetier", "label": "CrmContractProductPriceTier", "file_type": "cLine truncated
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "label": "crm_contract_routes.py", "file_type": "code", "source_file": "app/api/crm_contract_routes.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_register_contract_routes", "label": "register_contract_routes()", "file_type": "code", "source_file": "app/api/crm_contract_routes.py", "source_location": "L78"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_rationale_1", "label": "CRM \u5408\u540c API \u8def\u7531 \u2014 GET/PUT/DELETE/POST \u5bf9\u9f50\u539f Java CRM", "file_type": "rationale", "source_file": "app/api/crm_contract_routes.py", "source_location": "L1"}], "edges": [{"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "datetime", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_contract_routes.py", "source_location": "L4", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "typing", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_contract_routes.py", "source_location": "L5", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "fastapi", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_contract_routes.py", "source_location": "L7", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "sqlalchemy_orm", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_contract_routes.py", "source_location": "L8", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "app_core_database", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_contract_routes.py", "source_location": "L10", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "app_core_menu_guard", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_contract_routes.py", "source_location": "L11", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "app_core_oper_log_decorator", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_contract_routes.py", "source_location": "L12", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "app_core_response", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_contract_routes.py", "source_location": "L13", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "app_core_tenant_context", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_contract_routes.py", "source_location": "L14", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "app_crud_crm_data_scope_helpers", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_contract_routes.py", "source_location": "L15", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "app_crud_crm_contract", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_contract_routes.py", "source_location": "L16", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "app_crud_crm_contract_approval", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_contract_routes.py", "source_location": "L47", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "app_crud_crm_contract_approver_setting", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_contract_routes.py", "source_location": "L51", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "app_crud_crm_contract_approver_setting", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/api/crm_contract_routes.py", "source_location": "L52", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "app_schemas_crLine truncated
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_crm_quote_approver_setting_py", "label": "quote_approver_setting.py", "file_type": "code", "source_file": "app/schemas/crm/quote_approver_setting.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_crm_quote_approver_setting_quoteapproversettingsavebody", "label": "QuoteApproverSettingSaveBody", "file_type": "code", "source_file": "app/schemas/crm/quote_approver_setting.py", "source_location": "L6"}, {"id": "basemodel", "label": "BaseModel", "file_type": "code", "source_file": "", "source_location": ""}], "edges": [{"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_crm_quote_approver_setting_py", "target": "typing", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/schemas/crm/quote_approver_setting.py", "source_location": "L1", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_crm_quote_approver_setting_py", "target": "pydantic", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/schemas/crm/quote_approver_setting.py", "source_location": "L3", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_crm_quote_approver_setting_py", "target": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_crm_quote_approver_setting_quoteapproversettingsavebody", "relation": "contains", "confidence": "EXTRACTED", "source_file": "app/schemas/crm/quote_approver_setting.py", "source_location": "L6", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_crm_quote_approver_setting_quoteapproversettingsavebody", "target": "basemodel", "relation": "inherits", "confidence": "EXTRACTED", "source_file": "app/schemas/crm/quote_approver_setting.py", "source_location": "L6", "weight": 1.0}], "raw_calls": [], "callable_nids": ["users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_crm_quote_approver_setting_quoteapproversettingsavebody"]}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_crm_contract_approver_setting_py", "label": "contract_approver_setting.py", "file_type": "code", "source_file": "app/schemas/crm/contract_approver_setting.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_crm_contract_approver_setting_contractapproversettingsavebody", "label": "ContractApproverSettingSaveBody", "file_type": "code", "source_file": "app/schemas/crm/contract_approver_setting.py", "source_location": "L6"}, {"id": "basemodel", "label": "BaseModel", "file_type": "code", "source_file": "", "source_location": ""}], "edges": [{"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_crm_contract_approver_setting_py", "target": "typing", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/schemas/crm/contract_approver_setting.py", "source_location": "L1", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_crm_contract_approver_setting_py", "target": "pydantic", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/schemas/crm/contract_approver_setting.py", "source_location": "L3", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_crm_contract_approver_setting_py", "target": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_crm_contract_approver_setting_contractapproversettingsavebody", "relation": "contains", "confidence": "EXTRACTED", "source_file": "app/schemas/crm/contract_approver_setting.py", "source_location": "L6", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_crm_contract_approver_setting_contractapproversettingsavebody", "target": "basemodel", "relation": "inherits", "confidence": "EXTRACTED", "source_file": "app/schemas/crm/contract_approver_setting.py", "source_location": "L6", "weight": 1.0}], "raw_calls": [], "callable_nids": ["users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_crm_contract_approver_setting_contractapproversettingsavebody"]}
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"/Users/zlwu/work/00code/shanxia2.0/backend/start-prod.sh":{"size":7357,"mtime_ns":1782639355476060453,"hash":"ca89869446be74a2bf2367b3b28f448580220859a39a84ad9613ac5fe35a8433"},"/Users/zlwu/work/00code/shanxia2.0/backend/start.sh":{"size":9548,"mtime_ns":1780114661446983244,"hash":"4d632863e8a0cd81194b491d83a0218a4157435ae812dec76a6f6197b4d48e57"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/__init__.py":{"size":21,"mtime_ns":1780113982281973984,"hash":"60a04ff08fd97084e83ea809fd13feda6d2848be20ebf309e31fc1dad7523230"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/main.py":{"size":6280,"mtime_ns":1782790852770448643,"hash":"8f9da799977bee7134d87a26ff8434ac5ef8eabb0b7368dd537eee49ad7fd00b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py":{"size":2207,"mtime_ns":1783208067881286091,"hash":"313e6da0e91d743ce891dd283458c79b50b24c4d909dc96ca7b3fd9d2bfa5013"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/auth.py":{"size":7457,"mtime_ns":1782790841500516545,"hash":"34bac56e1c2cb6215f4b5fa2dc32d77d4b4abca51428819eebf4afd269cc51cf"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_crypto.py":{"size":1870,"mtime_ns":1780108930724444498,"hash":"c04b748aea86890a5971f749523c7b17dbad1ddd6f9f7f07a5679d159c984e33"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/login_crypto.py":{"size":3858,"mtime_ns":1780108930725155710,"hash":"b82d51afabf2293a9cb1fb2e5f994c383948e7f47ea95fe674686b4f3f3342b1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/config.py":{"size":6958,"mtime_ns":1784032424448146538,"hash":"c2fb44aedb880733de28afa7f92d2c1c681eb600f821bbeba2bcda3933e5bc37"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/wechat_pay.py":{"size":23585,"mtime_ns":1780108930725768130,"hash":"3d52265ce65d21e7103f94afa1e8547aee0823b5ea6d65c50317436ac81fb4fc"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py":{"size":11971,"mtime_ns":1782701669198208175,"hash":"0b84c32828939e6102bb3647691fa8f04b1f68e0e0e9f28127374cc44e19a11c"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/database.py":{"size":1510,"mtime_ns":1782790847265601586,"hash":"8bc9c8ae62e2be2c6920036582ba448dfcd1711c41cebb62274ff6bc6ee14b0b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/security.py":{"size":728,"mtime_ns":1780108930725470004,"hash":"df0f01115064f7b8de1c81dbe9a6db39360c236610b68c33b3234a51d7f0025b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/request_payload.py":{"size":2842,"mtime_ns":1780108930725350836,"hash":"e9937151530ab1417ef297aef59111977725cf90be2a8fa76c00a0618789aba0"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/__init__.py":{"size":61,"mtime_ns":1780108930724312414,"hash":"3dd1842ee59b0c804176b03b4b8a323bd4a252b7feb1bbc32db64f3a8928570e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/response.py":{"size":1824,"mtime_ns":1780108930725412462,"hash":"f1739352d65ea8287d76e282a59553aa8d63c97e35764c64ac378aa5bd9c347e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/menu_guard.py":{"size":1611,"mtime_ns":1780108930725223002,"hash":"3e26077288c3620a4780038b891a326206b45bffe00aca545bdae30172487235"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/db_collation.py":{"size":548,"mtime_ns":1780108930725025043,"hash":"0c680e25a18244ca839a7de6ed1f4313b33bc8da118d89c463aee1b6af00c6f9"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/context.py":{"size":421,"mtime_ns":1780108930724824458,"hash":"83a10aee67162374993c10a30bf8bb8bd9c4b35cf48f42c832415ccebd91660f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/utils.py":{"size":1498,"mtime_ns":1780108930725607296,"hash":"09323693661c1d4560458b86a954128ccff82bbaf6522da0b44f24da99ddf9ea"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/storage.py":{"size":14141,"mtime_ns":1782855136810943216,"hash":"3ae2c413c10898a8adb3d6773870bafbfbc18c739c10e0e8b4f765b4b5ca6c08"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/order_status_compat.py":{"size":2828,"mtime_ns":1780108930725284669,"hash":"5349e5a0745ee6734c4a995337779307adefc46f453906171988a77de2081540"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/exceptions.py":{"size":4946,"mtime_ns":1780108930725084335,"hash":"79c997d964bca1a4f12f2b92fb1f181fdb9e2b3cf72eae39f4cc164d23794fd7"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/data_scope.py":{"size":8043,"mtime_ns":1782648925694296304,"hash":"626dff86937e5cacec634ecbc61cd53d3763d760223a1d4ec7cd11f64e59a220"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/admin_request_json.py":{"size":2669,"mtime_ns":1780108930724378914,"hash":"4f68b3cf84291e7bddad6ecf4bb4d29ca5e1ec246a9f7f9c4cf5909d0ec1174a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_crypto_middleware.py":{"size":10876,"mtime_ns":1782790001926627098,"hash":"b39f128b6612589ea6ac5babd30752e9585b3af07b82bb1ad53c9e3c0d90816f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/wechat_message.py":{"size":11194,"mtime_ns":1780108930725684255,"hash":"5086070deb14d99d1a69e419b2299ec4Line truncated
|
||||
{"/Users/zlwu/work/00code/shanxia2.0/backend/start-prod.sh":{"size":7357,"mtime_ns":1782639355476060453,"hash":"ca89869446be74a2bf2367b3b28f448580220859a39a84ad9613ac5fe35a8433"},"/Users/zlwu/work/00code/shanxia2.0/backend/start.sh":{"size":9548,"mtime_ns":1780114661446983244,"hash":"4d632863e8a0cd81194b491d83a0218a4157435ae812dec76a6f6197b4d48e57"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/__init__.py":{"size":21,"mtime_ns":1780113982281973984,"hash":"60a04ff08fd97084e83ea809fd13feda6d2848be20ebf309e31fc1dad7523230"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/main.py":{"size":6280,"mtime_ns":1782790852770448643,"hash":"8f9da799977bee7134d87a26ff8434ac5ef8eabb0b7368dd537eee49ad7fd00b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py":{"size":2207,"mtime_ns":1783208067881286091,"hash":"313e6da0e91d743ce891dd283458c79b50b24c4d909dc96ca7b3fd9d2bfa5013"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/auth.py":{"size":7457,"mtime_ns":1782790841500516545,"hash":"34bac56e1c2cb6215f4b5fa2dc32d77d4b4abca51428819eebf4afd269cc51cf"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_crypto.py":{"size":1870,"mtime_ns":1780108930724444498,"hash":"c04b748aea86890a5971f749523c7b17dbad1ddd6f9f7f07a5679d159c984e33"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/login_crypto.py":{"size":3858,"mtime_ns":1780108930725155710,"hash":"b82d51afabf2293a9cb1fb2e5f994c383948e7f47ea95fe674686b4f3f3342b1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/config.py":{"size":6958,"mtime_ns":1784032424448146538,"hash":"c2fb44aedb880733de28afa7f92d2c1c681eb600f821bbeba2bcda3933e5bc37"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/wechat_pay.py":{"size":23585,"mtime_ns":1780108930725768130,"hash":"3d52265ce65d21e7103f94afa1e8547aee0823b5ea6d65c50317436ac81fb4fc"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py":{"size":11971,"mtime_ns":1782701669198208175,"hash":"0b84c32828939e6102bb3647691fa8f04b1f68e0e0e9f28127374cc44e19a11c"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/database.py":{"size":1510,"mtime_ns":1782790847265601586,"hash":"8bc9c8ae62e2be2c6920036582ba448dfcd1711c41cebb62274ff6bc6ee14b0b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/security.py":{"size":728,"mtime_ns":1780108930725470004,"hash":"df0f01115064f7b8de1c81dbe9a6db39360c236610b68c33b3234a51d7f0025b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/request_payload.py":{"size":2842,"mtime_ns":1780108930725350836,"hash":"e9937151530ab1417ef297aef59111977725cf90be2a8fa76c00a0618789aba0"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/__init__.py":{"size":61,"mtime_ns":1780108930724312414,"hash":"3dd1842ee59b0c804176b03b4b8a323bd4a252b7feb1bbc32db64f3a8928570e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/response.py":{"size":1824,"mtime_ns":1780108930725412462,"hash":"f1739352d65ea8287d76e282a59553aa8d63c97e35764c64ac378aa5bd9c347e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/menu_guard.py":{"size":1611,"mtime_ns":1780108930725223002,"hash":"3e26077288c3620a4780038b891a326206b45bffe00aca545bdae30172487235"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/db_collation.py":{"size":548,"mtime_ns":1780108930725025043,"hash":"0c680e25a18244ca839a7de6ed1f4313b33bc8da118d89c463aee1b6af00c6f9"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/context.py":{"size":421,"mtime_ns":1780108930724824458,"hash":"83a10aee67162374993c10a30bf8bb8bd9c4b35cf48f42c832415ccebd91660f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/utils.py":{"size":1498,"mtime_ns":1780108930725607296,"hash":"09323693661c1d4560458b86a954128ccff82bbaf6522da0b44f24da99ddf9ea"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/storage.py":{"size":14141,"mtime_ns":1782855136810943216,"hash":"3ae2c413c10898a8adb3d6773870bafbfbc18c739c10e0e8b4f765b4b5ca6c08"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/order_status_compat.py":{"size":2828,"mtime_ns":1780108930725284669,"hash":"5349e5a0745ee6734c4a995337779307adefc46f453906171988a77de2081540"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/exceptions.py":{"size":4946,"mtime_ns":1780108930725084335,"hash":"79c997d964bca1a4f12f2b92fb1f181fdb9e2b3cf72eae39f4cc164d23794fd7"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/data_scope.py":{"size":8043,"mtime_ns":1782648925694296304,"hash":"626dff86937e5cacec634ecbc61cd53d3763d760223a1d4ec7cd11f64e59a220"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/admin_request_json.py":{"size":2669,"mtime_ns":1780108930724378914,"hash":"4f68b3cf84291e7bddad6ecf4bb4d29ca5e1ec246a9f7f9c4cf5909d0ec1174a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_crypto_middleware.py":{"size":10876,"mtime_ns":1782790001926627098,"hash":"b39f128b6612589ea6ac5babd30752e9585b3af07b82bb1ad53c9e3c0d90816f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/wechat_message.py":{"size":11194,"mtime_ns":1780108930725684255,"hash":"5086070deb14d99d1a69e419b2299ec4Line truncated
|
||||
@@ -63,12 +63,12 @@
|
||||
</div>
|
||||
<div id="legend"></div>
|
||||
</div>
|
||||
<div id="stats">4513 nodes · 15360 edges · 527 communities</div>
|
||||
<div id="stats">4540 nodes · 15450 edges · 528 communities</div>
|
||||
</div>
|
||||
<script>
|
||||
const RAW_NODES = [{"id": "app_init", "label": "__init__.py", "color": {"background": "#59A14F", "border": "#59A14F", "highlight": {"background": "#ffffff", "border": "#59A14F"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "__init__.py", "community": 304, "community_name": "__init__.py", "source_file": "app/__init__.py", "file_type": "code", "degree": 0}, {"id": "app_api_init", "label": "__init__.py", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 10.7, "font": {"size": 0, "color": "#ffffff"}, "title": "__init__.py", "community": 81, "community_name": "Community 81", "source_file": "app/api/__init__.py", "file_type": "code", "degree": 10}, {"id": "app_api_init_rationale_1", "label": "API \u8def\u7531\u5305 \u2014 \u5c71\u5ce1\u5316\u5de5\u751f\u4ea7\u4e00\u4f53\u5316\u95e8\u6237", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "API \u8def\u7531\u5305 \u2014 \u5c71\u5ce1\u5316\u5de5\u751f\u4ea7\u4e00\u4f53\u5316\u95e8\u6237", "community": 81, "community_name": "Community 81", "source_file": "app/api/__init__.py", "file_type": "rationale", "degree": 1}, {"id": "app_api_admin", "label": "admin.py", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 17.2, "font": {"size": 12, "color": "#ffffff"}, "title": "admin.py", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 110}, {"id": "app_api_admin_list_roles_for_select", "label": "list_roles_for_select()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "list_roles_for_select()", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 5}, {"id": "app_api_admin_py_session", "label": "Session", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 13.5, "font": {"size": 0, "color": "#ffffff"}, "title": "Session", "community": 3, "community_name": "Community 3", "source_file": "", "file_type": "code", "degree": 53}, {"id": "app_api_admin_list_users_for_select", "label": "list_users_for_select()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "list_users_for_select()", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 5}, {"id": "app_api_admin_list_roles_for_designer", "label": "list_roles_for_designer()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "list_roles_for_designer()", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 5}, {"id": "app_api_admin_tenantcreatebody", "label": "TenantCreateBody", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.5, "font": {"size": 0, "color": "#ffffff"}, "title": "TenantCreateBody", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 7}, {"id": "app_api_admin_tenantupdatebody", "label": "TenantUpdateBody", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.5, "font": {"size": 0, "color": "#ffffff"}, "title": "TenantUpdateBody", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 7}, {"id": "app_api_admin_list_tenants", "label": "list_tenants()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "list_tenants()", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 3}, {"id": "app_api_admin_create_tenant", "label": "create_tenant()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "create_tenant()", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 5}, {"id": "app_api_admin_update_tenant", "label": "update_tenant()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.3Line truncated
|
||||
const RAW_EDGES = [{"from": "app_api_init", "to": "app_api_admin", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_bx_init", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_crm", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_db_init", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_leave", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_menu", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_monitor", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_public", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_workflow", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init_rationale_1", "to": "app_api_init", "label": "rationale_for", "title": "rationale_for [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_attachment_download_url", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_attachment_preview_url", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_attachmentdownloadurlbody", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_dept", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_dict_data", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_dict_type", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_menu", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_post", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_role_management", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_site", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_tenant", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_user", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_delete_dept", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_delete_dict_data", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_delete_dict_type", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {Line truncated
|
||||
const LEGEND = [{"cid": 0, "color": "#4E79A7", "label": "Community 0", "count": 47}, {"cid": 1, "color": "#F28E2B", "label": "Community 1", "count": 18}, {"cid": 2, "color": "#E15759", "label": "Community 2", "count": 31}, {"cid": 3, "color": "#76B7B2", "label": "Community 3", "count": 89}, {"cid": 4, "color": "#59A14F", "label": "Community 4", "count": 63}, {"cid": 5, "color": "#EDC948", "label": "Community 5", "count": 26}, {"cid": 6, "color": "#B07AA1", "label": "Community 6", "count": 39}, {"cid": 7, "color": "#FF9DA7", "label": "Community 7", "count": 38}, {"cid": 8, "color": "#9C755F", "label": "Community 8", "count": 46}, {"cid": 9, "color": "#BAB0AC", "label": "Community 9", "count": 1}, {"cid": 10, "color": "#4E79A7", "label": "Community 10", "count": 1}, {"cid": 11, "color": "#F28E2B", "label": "Community 11", "count": 61}, {"cid": 12, "color": "#E15759", "label": "Community 12", "count": 82}, {"cid": 13, "color": "#76B7B2", "label": "Community 13", "count": 33}, {"cid": 14, "color": "#59A14F", "label": "Community 14", "count": 35}, {"cid": 15, "color": "#EDC948", "label": "Community 15", "count": 51}, {"cid": 16, "color": "#B07AA1", "label": "Community 16", "count": 66}, {"cid": 17, "color": "#FF9DA7", "label": "Community 17", "count": 1}, {"cid": 18, "color": "#9C755F", "label": "Community 18", "count": 34}, {"cid": 19, "color": "#BAB0AC", "label": "Community 19", "count": 49}, {"cid": 20, "color": "#4E79A7", "label": "Community 20", "count": 1}, {"cid": 21, "color": "#F28E2B", "label": "Community 21", "count": 87}, {"cid": 22, "color": "#E15759", "label": "Community 22", "count": 43}, {"cid": 23, "color": "#76B7B2", "label": "Community 23", "count": 17}, {"cid": 24, "color": "#59A14F", "label": "Community 24", "count": 20}, {"cid": 25, "color": "#EDC948", "label": "Community 25", "count": 75}, {"cid": 26, "color": "#B07AA1", "label": "Community 26", "count": 65}, {"cid": 27, "color": "#FF9DA7", "label": "Community 27", "count": 37}, {"cid": 28, "color": "#9C755F", "label": "Community 28", "count": 68}, {"cid": 29, "color": "#BAB0AC", "label": "Community 29", "count": 71}, {"cid": 30, "color": "#4E79A7", "label": "Community 30", "count": 33}, {"cid": 31, "color": "#F28E2B", "label": "Community 31", "count": 1}, {"cid": 32, "color": "#E15759", "label": "Community 32", "count": 1}, {"cid": 33, "color": "#76B7B2", "label": "Community 33", "count": 24}, {"cid": 34, "color": "#59A14F", "label": "Community 34", "count": 44}, {"cid": 35, "color": "#EDC948", "label": "Community 35", "count": 38}, {"cid": 36, "color": "#B07AA1", "label": "Community 36", "count": 29}, {"cid": 37, "color": "#FF9DA7", "label": "Community 37", "count": 20}, {"cid": 38, "color": "#9C755F", "label": "Community 38", "count": 28}, {"cid": 39, "color": "#BAB0AC", "label": "Community 39", "count": 29}, {"cid": 40, "color": "#4E79A7", "label": "Community 40", "count": 1}, {"cid": 41, "color": "#F28E2B", "label": "Community 41", "count": 1}, {"cid": 42, "color": "#E15759", "label": "Community 42", "count": 34}, {"cid": 43, "color": "#76B7B2", "label": "Community 43", "count": 58}, {"cid": 44, "color": "#59A14F", "label": "Community 44", "count": 18}, {"cid": 45, "color": "#EDC948", "label": "Community 45", "count": 31}, {"cid": 46, "color": "#B07AA1", "label": "Community 46", "count": 21}, {"cid": 47, "color": "#FF9DA7", "label": "Community 47", "count": 1}, {"cid": 48, "color": "#9C755F", "label": "Community 48", "count": 28}, {"cid": 49, "color": "#BAB0AC", "label": "Community 49", "count": 72}, {"cid": 50, "color": "#4E79A7", "label": "Community 50", "count": 43}, {"cid": 51, "color": "#F28E2B", "label": "Community 51", "count": 22}, {"cid": 52, "color": "#E15759", "label": "Community 52", "count": 31}, {"cid": 53, "color": "#76B7B2", "label": "Community 53", "count": 55}, {"cid": 54, "color": "#59A14F", "label": "Community 54", "count": 30}, {"cid": 55, "color": "#EDC948", "label": "Community 55", "count": 1}, {"cid": 56, "color": "#B07AA1", "label": "Community 56", "count": 1}, {"cid": 57, "color": "#FF9DA7", "label": "Community 57", "count": 1}, {"cid": 58, "color": "#9C755F", "label": "Community 58", "count": 68}, {"cid": 59, "color": "#BAB0AC", "label": "Community 59", "count": 30}, {"cid": 60, "color": "#4E79A7", "label": "Community 60", "count": 24}, {"cid": 61, "color": "#F28E2B", "label": "Community 61", "count": 35}, {"cid": 62, "color": "#E15759", "label": "Community 62", "count": 23}, {"cid": 63, "color": "#76B7B2", "label": "Community 63", "count": 60}, {"cid": 64, "color": "#59A14F", "label": "Community 64", "count": 187}, {"cid": 65, "color": "#EDC948", "label": "Community 65", "count": 62}, {"cid": 66, "color": "#B07AA1", "label": "Community 66", "count": 38}, {"cid": 67, "color": "#FF9DA7", "label": "Community 67", "count": 20}, {"cid": 68, "color": "#9C755F", "label": "Community 68", "count": 12}, {"cid": 69, "color": "#BAB0AC", "label": "Community 69", "count": 31}, {"cid": 70, "color": "#4E79A7", "label": "CoLine truncated
|
||||
const RAW_NODES = [{"id": "app_init", "label": "__init__.py", "color": {"background": "#59A14F", "border": "#59A14F", "highlight": {"background": "#ffffff", "border": "#59A14F"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "__init__.py", "community": 304, "community_name": "__init__.py", "source_file": "app/__init__.py", "file_type": "code", "degree": 0}, {"id": "app_api_init", "label": "__init__.py", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.7, "font": {"size": 0, "color": "#ffffff"}, "title": "__init__.py", "community": 36, "community_name": "Community 36", "source_file": "app/api/__init__.py", "file_type": "code", "degree": 10}, {"id": "app_api_init_rationale_1", "label": "API \u8def\u7531\u5305 \u2014 \u5c71\u5ce1\u5316\u5de5\u751f\u4ea7\u4e00\u4f53\u5316\u95e8\u6237", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "API \u8def\u7531\u5305 \u2014 \u5c71\u5ce1\u5316\u5de5\u751f\u4ea7\u4e00\u4f53\u5316\u95e8\u6237", "community": 36, "community_name": "Community 36", "source_file": "app/api/__init__.py", "file_type": "rationale", "degree": 1}, {"id": "app_api_admin", "label": "admin.py", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 17.2, "font": {"size": 12, "color": "#ffffff"}, "title": "admin.py", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 110}, {"id": "app_api_admin_list_roles_for_select", "label": "list_roles_for_select()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "list_roles_for_select()", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 5}, {"id": "app_api_admin_py_session", "label": "Session", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 13.5, "font": {"size": 0, "color": "#ffffff"}, "title": "Session", "community": 3, "community_name": "Community 3", "source_file": "", "file_type": "code", "degree": 53}, {"id": "app_api_admin_list_users_for_select", "label": "list_users_for_select()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "list_users_for_select()", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 5}, {"id": "app_api_admin_list_roles_for_designer", "label": "list_roles_for_designer()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "list_roles_for_designer()", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 5}, {"id": "app_api_admin_tenantcreatebody", "label": "TenantCreateBody", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.5, "font": {"size": 0, "color": "#ffffff"}, "title": "TenantCreateBody", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 7}, {"id": "app_api_admin_tenantupdatebody", "label": "TenantUpdateBody", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.5, "font": {"size": 0, "color": "#ffffff"}, "title": "TenantUpdateBody", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 7}, {"id": "app_api_admin_list_tenants", "label": "list_tenants()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "list_tenants()", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 3}, {"id": "app_api_admin_create_tenant", "label": "create_tenant()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "create_tenant()", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 5}, {"id": "app_api_admin_update_tenant", "label": "update_tenant()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.3Line truncated
|
||||
const RAW_EDGES = [{"from": "app_api_init", "to": "app_api_admin", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_bx_init", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_crm", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_db_init", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_leave", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_menu", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_monitor", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_public", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_workflow", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init_rationale_1", "to": "app_api_init", "label": "rationale_for", "title": "rationale_for [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_attachment_download_url", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_attachment_preview_url", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_attachmentdownloadurlbody", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_dept", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_dict_data", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_dict_type", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_menu", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_post", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_role_management", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_site", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_tenant", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_user", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_delete_dept", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_delete_dict_data", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_delete_dict_type", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {Line truncated
|
||||
const LEGEND = [{"cid": 0, "color": "#4E79A7", "label": "Community 0", "count": 22}, {"cid": 1, "color": "#F28E2B", "label": "Community 1", "count": 38}, {"cid": 2, "color": "#E15759", "label": "Community 2", "count": 13}, {"cid": 3, "color": "#76B7B2", "label": "Community 3", "count": 102}, {"cid": 4, "color": "#59A14F", "label": "Community 4", "count": 63}, {"cid": 5, "color": "#EDC948", "label": "Community 5", "count": 26}, {"cid": 6, "color": "#B07AA1", "label": "Community 6", "count": 49}, {"cid": 7, "color": "#FF9DA7", "label": "Community 7", "count": 38}, {"cid": 8, "color": "#9C755F", "label": "Community 8", "count": 52}, {"cid": 9, "color": "#BAB0AC", "label": "Community 9", "count": 1}, {"cid": 10, "color": "#4E79A7", "label": "Community 10", "count": 1}, {"cid": 11, "color": "#F28E2B", "label": "Community 11", "count": 61}, {"cid": 12, "color": "#E15759", "label": "Community 12", "count": 105}, {"cid": 13, "color": "#76B7B2", "label": "Community 13", "count": 32}, {"cid": 14, "color": "#59A14F", "label": "Community 14", "count": 50}, {"cid": 15, "color": "#EDC948", "label": "Community 15", "count": 55}, {"cid": 16, "color": "#B07AA1", "label": "Community 16", "count": 72}, {"cid": 17, "color": "#FF9DA7", "label": "Community 17", "count": 1}, {"cid": 18, "color": "#9C755F", "label": "Community 18", "count": 65}, {"cid": 19, "color": "#BAB0AC", "label": "Community 19", "count": 49}, {"cid": 20, "color": "#4E79A7", "label": "Community 20", "count": 1}, {"cid": 21, "color": "#F28E2B", "label": "Community 21", "count": 65}, {"cid": 22, "color": "#E15759", "label": "Community 22", "count": 43}, {"cid": 23, "color": "#76B7B2", "label": "Community 23", "count": 25}, {"cid": 24, "color": "#59A14F", "label": "Community 24", "count": 21}, {"cid": 25, "color": "#EDC948", "label": "Community 25", "count": 68}, {"cid": 26, "color": "#B07AA1", "label": "Community 26", "count": 59}, {"cid": 27, "color": "#FF9DA7", "label": "Community 27", "count": 30}, {"cid": 28, "color": "#9C755F", "label": "Community 28", "count": 54}, {"cid": 29, "color": "#BAB0AC", "label": "Community 29", "count": 26}, {"cid": 30, "color": "#4E79A7", "label": "Community 30", "count": 43}, {"cid": 31, "color": "#F28E2B", "label": "Community 31", "count": 1}, {"cid": 32, "color": "#E15759", "label": "Community 32", "count": 1}, {"cid": 33, "color": "#76B7B2", "label": "Community 33", "count": 24}, {"cid": 34, "color": "#59A14F", "label": "Community 34", "count": 44}, {"cid": 35, "color": "#EDC948", "label": "Community 35", "count": 38}, {"cid": 36, "color": "#B07AA1", "label": "Community 36", "count": 41}, {"cid": 37, "color": "#FF9DA7", "label": "Community 37", "count": 20}, {"cid": 38, "color": "#9C755F", "label": "Community 38", "count": 31}, {"cid": 39, "color": "#BAB0AC", "label": "Community 39", "count": 29}, {"cid": 40, "color": "#4E79A7", "label": "Community 40", "count": 1}, {"cid": 41, "color": "#F28E2B", "label": "Community 41", "count": 1}, {"cid": 42, "color": "#E15759", "label": "Community 42", "count": 33}, {"cid": 43, "color": "#76B7B2", "label": "Community 43", "count": 62}, {"cid": 44, "color": "#59A14F", "label": "Community 44", "count": 18}, {"cid": 45, "color": "#EDC948", "label": "Community 45", "count": 20}, {"cid": 46, "color": "#B07AA1", "label": "Community 46", "count": 19}, {"cid": 47, "color": "#FF9DA7", "label": "Community 47", "count": 1}, {"cid": 48, "color": "#9C755F", "label": "Community 48", "count": 30}, {"cid": 49, "color": "#BAB0AC", "label": "Community 49", "count": 55}, {"cid": 50, "color": "#4E79A7", "label": "Community 50", "count": 40}, {"cid": 51, "color": "#F28E2B", "label": "Community 51", "count": 26}, {"cid": 52, "color": "#E15759", "label": "Community 52", "count": 31}, {"cid": 53, "color": "#76B7B2", "label": "Community 53", "count": 47}, {"cid": 54, "color": "#59A14F", "label": "Community 54", "count": 30}, {"cid": 55, "color": "#EDC948", "label": "Community 55", "count": 1}, {"cid": 56, "color": "#B07AA1", "label": "Community 56", "count": 1}, {"cid": 57, "color": "#FF9DA7", "label": "Community 57", "count": 1}, {"cid": 58, "color": "#9C755F", "label": "Community 58", "count": 23}, {"cid": 59, "color": "#BAB0AC", "label": "Community 59", "count": 30}, {"cid": 60, "color": "#4E79A7", "label": "Community 60", "count": 47}, {"cid": 61, "color": "#F28E2B", "label": "Community 61", "count": 27}, {"cid": 62, "color": "#E15759", "label": "Community 62", "count": 23}, {"cid": 63, "color": "#76B7B2", "label": "Community 63", "count": 35}, {"cid": 64, "color": "#59A14F", "label": "Community 64", "count": 190}, {"cid": 65, "color": "#EDC948", "label": "Community 65", "count": 62}, {"cid": 66, "color": "#B07AA1", "label": "Community 66", "count": 40}, {"cid": 67, "color": "#FF9DA7", "label": "Community 67", "count": 18}, {"cid": 68, "color": "#9C755F", "label": "Community 68", "count": 20}, {"cid": 69, "color": "#BAB0AC", "label": "Community 69", "count": 24}, {"cid": 70, "color": "#4E79A7", "label": "Line truncated
|
||||
|
||||
// HTML-escape helper — prevents XSS when injecting graph data into innerHTML
|
||||
function esc(s) {
|
||||
|
||||
+2969
-1731
File diff suppressed because it is too large.
Load diff
+1483
-1273
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,786 @@
|
||||
<!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>
|
||||
@@ -0,0 +1,557 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>门户大面板宽度 · 一级菜单较少时</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary: #10b981;
|
||||
--primary-dark: #059669;
|
||||
--primary-bg: #ecfdf5;
|
||||
--header: #1ca851;
|
||||
--text: #0f172a;
|
||||
--muted: #64748b;
|
||||
--border: #e2e8f0;
|
||||
--bg: #f1f5f9;
|
||||
--panel-min: 720px;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Inter, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
color: var(--text);
|
||||
background: #f8fafc;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.page { padding: 20px 24px 48px; max-width: 1280px; margin: 0 auto; }
|
||||
h1 { font-size: 22px; margin: 0 0 6px; }
|
||||
.lead { color: var(--muted); font-size: 14px; margin: 0 0 14px; }
|
||||
.note {
|
||||
background: #fffbeb;
|
||||
border: 1px solid #fde68a;
|
||||
color: #92400e;
|
||||
border-radius: 10px;
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.ok {
|
||||
background: var(--primary-bg);
|
||||
border: 1px solid #a7f3d0;
|
||||
color: #065f46;
|
||||
border-radius: 10px;
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px 16px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 14px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
}
|
||||
.controls label {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
font-weight: 600;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.seg {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.seg button {
|
||||
border: 1px solid var(--border);
|
||||
background: #fff;
|
||||
border-radius: 999px;
|
||||
padding: 7px 12px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
color: #334155;
|
||||
}
|
||||
.seg button.active {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
.card {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.card-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: #fafafa;
|
||||
}
|
||||
.card-head h2 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 15px;
|
||||
}
|
||||
.card-head p {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.badge {
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
padding: 4px 8px;
|
||||
border-radius: 999px;
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
}
|
||||
.badge.bad { background: #fee2e2; color: #b91c1c; }
|
||||
.badge.good { background: #d1fae5; color: #047857; }
|
||||
.badge.rec { background: #ecfdf5; color: #065f46; border: 1px solid #a7f3d0; }
|
||||
.frame {
|
||||
height: 360px;
|
||||
background: var(--bg);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.app {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 13px;
|
||||
}
|
||||
.top {
|
||||
position: relative;
|
||||
height: 56px;
|
||||
background: var(--header);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
flex-shrink: 0;
|
||||
color: #fff;
|
||||
z-index: 2;
|
||||
}
|
||||
.brand {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.nav-slot {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
max-width: min(720px, calc(100% - 280px));
|
||||
}
|
||||
.l1 {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 2px;
|
||||
height: 100%;
|
||||
}
|
||||
.l1-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 0 14px;
|
||||
height: 100%;
|
||||
white-space: nowrap;
|
||||
border: none;
|
||||
border-bottom: 4px solid transparent;
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.l1-item.on {
|
||||
font-weight: 600;
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
.l1-item .caret { font-size: 10px; opacity: 0.75; }
|
||||
.end {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
.avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #6ee7b7, #34d399);
|
||||
border: 2px solid rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
.main {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
background: #eef2f7;
|
||||
overflow: hidden;
|
||||
}
|
||||
.page-fake {
|
||||
padding: 20px 24px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
.page-fake .bar {
|
||||
height: 10px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.page-fake .bar.w2 { width: 70%; }
|
||||
.page-fake .bar.w3 { width: 48%; }
|
||||
.mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.25);
|
||||
z-index: 3;
|
||||
}
|
||||
.panel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 4;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-top: none;
|
||||
border-radius: 0 0 10px 10px;
|
||||
box-shadow: 0 14px 32px rgba(15, 23, 42, 0.12);
|
||||
max-height: calc(100% - 8px);
|
||||
overflow: auto;
|
||||
}
|
||||
.panel-body { padding: 12px 16px 16px; }
|
||||
.split {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0 16px;
|
||||
}
|
||||
.pane + .pane {
|
||||
padding-left: 16px;
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 88px 1fr;
|
||||
gap: 8px 12px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
align-items: start;
|
||||
}
|
||||
.row:last-child { border-bottom: 0; }
|
||||
.l2 {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
padding-top: 4px;
|
||||
}
|
||||
.links { display: flex; flex-direction: column; gap: 2px; }
|
||||
.links span {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #0f172a;
|
||||
padding: 3px 0;
|
||||
}
|
||||
.links span.on { color: var(--primary-dark); font-weight: 600; }
|
||||
.measure {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 10px;
|
||||
z-index: 5;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #b45309;
|
||||
background: #fffbeb;
|
||||
border: 1px solid #fde68a;
|
||||
border-radius: 6px;
|
||||
padding: 3px 8px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.measure.ok-m {
|
||||
color: #047857;
|
||||
background: #ecfdf5;
|
||||
border-color: #a7f3d0;
|
||||
}
|
||||
.pick {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.pick button {
|
||||
border: 1px solid var(--primary);
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 10px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.pick button.ghost {
|
||||
background: #fff;
|
||||
color: #334155;
|
||||
border-color: var(--border);
|
||||
}
|
||||
.pick button.alt {
|
||||
background: var(--primary-bg);
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<h1>大面板宽度 Demo:一级菜单较少时</h1>
|
||||
<p class="lead">
|
||||
现状:二级大面板宽度 = 一级菜单区域宽度。权限较少(仅 2 / 4 个一级)时,面板会跟着变窄,两栏二级排不开。
|
||||
</p>
|
||||
<div class="note">
|
||||
先用上方开关模拟「一级数量」,再对比三种宽度策略。点选卡片或底部按钮告诉我偏好。
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div>
|
||||
<label>一级菜单数量</label>
|
||||
<div class="seg" id="countSeg">
|
||||
<button type="button" data-count="2">2 个(窄权限)</button>
|
||||
<button type="button" data-count="4" class="active">4 个</button>
|
||||
<button type="button" data-count="7">7 个(接近满)</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label>高亮模块</label>
|
||||
<div class="seg" id="activeSeg">
|
||||
<button type="button" class="active" data-active="0">第一个</button>
|
||||
<button type="button" data-active="1">第二个</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ok">
|
||||
推荐倾向:<strong>方案 B</strong> — 面板有最小宽度(如 720px),相对一级区域居中;一级很多时仍可随区域变宽,但不超过顶栏中间槽位上限。
|
||||
</div>
|
||||
|
||||
<div class="grid" id="cards"></div>
|
||||
|
||||
<div class="pick">
|
||||
<button type="button" class="alt" data-choice="a">选 A · 跟一级同宽(现状)</button>
|
||||
<button type="button" data-choice="b">选 B · 最小宽度居中(推荐)</button>
|
||||
<button type="button" class="ghost" data-choice="c">选 C · 固定铺满中间槽</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const ALL_L1 = ['CRM', '仓储', '质检', '财务', '系统', '报表', '运营']
|
||||
const PANEL_ROWS_LEFT = [
|
||||
{ l2: '客户管理', links: ['客户列表', '联系人', '公海客户'] },
|
||||
{ l2: '商机报价', links: ['商机列表', '报价单', '价格函'] },
|
||||
{ l2: '合同订单', links: ['合同审批', '订单跟踪'] },
|
||||
{ l2: '拜访计划', links: ['拜访日历', '拜访记录'] },
|
||||
{ l2: '基础资料', links: ['产品目录', '单位管理'] }
|
||||
]
|
||||
const PANEL_ROWS_RIGHT = [
|
||||
{ l2: '审批中心', links: ['待办审批', '已办审批'] },
|
||||
{ l2: '设置', links: ['审批人设置', '业务参数'] }
|
||||
]
|
||||
|
||||
const strategies = [
|
||||
{
|
||||
id: 'a',
|
||||
title: 'A · 跟一级区域同宽(现状)',
|
||||
desc: '面板 left/width 完全等于一级 nav 的 getBoundingClientRect。一级少 → 面板窄。',
|
||||
badge: '现状问题',
|
||||
badgeClass: 'bad',
|
||||
calc(navRect, slotMax) {
|
||||
return { left: navRect.left, width: navRect.width }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
title: 'B · 最小宽度 + 相对一级居中(推荐)',
|
||||
desc: 'width = max(一级宽度, min(720, 槽位上限)),相对一级中心对齐;一级很多时仍可更宽。',
|
||||
badge: '推荐',
|
||||
badgeClass: 'rec',
|
||||
calc(navRect, slotMax, viewportW) {
|
||||
const minW = Math.min(720, slotMax, viewportW - 32)
|
||||
const width = Math.max(navRect.width, minW)
|
||||
const center = navRect.left + navRect.width / 2
|
||||
let left = center - width / 2
|
||||
left = Math.max(16, Math.min(left, viewportW - width - 16))
|
||||
return { left, width }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'c',
|
||||
title: 'C · 始终铺满顶栏中间槽位',
|
||||
desc: '面板宽度固定为中间槽 max-width(约 720),与一级数量无关,始终居中于顶栏。',
|
||||
badge: '固定宽',
|
||||
badgeClass: 'good',
|
||||
calc(navRect, slotMax, viewportW, slotRect) {
|
||||
const width = Math.min(720, slotMax, viewportW - 32)
|
||||
const center = slotRect.left + slotRect.width / 2
|
||||
let left = center - width / 2
|
||||
left = Math.max(16, Math.min(left, viewportW - width - 16))
|
||||
return { left, width }
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
let l1Count = 4
|
||||
let activeIdx = 0
|
||||
|
||||
function rowHtml(row) {
|
||||
return `<div class="row">
|
||||
<div class="l2">${row.l2}</div>
|
||||
<div class="links">${row.links.map((t, i) => `<span class="${i === 0 ? 'on' : ''}">${t}</span>`).join('')}</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
function panelInner() {
|
||||
return `<div class="panel-body">
|
||||
<div class="split">
|
||||
<div class="pane">${PANEL_ROWS_LEFT.map(rowHtml).join('')}</div>
|
||||
<div class="pane">${PANEL_ROWS_RIGHT.map(rowHtml).join('')}</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
function render() {
|
||||
const cards = document.getElementById('cards')
|
||||
cards.innerHTML = strategies.map((s) => `
|
||||
<div class="card" data-choice="${s.id}" onclick="window.__pick('${s.id}')">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<h2>${s.title}</h2>
|
||||
<p>${s.desc}</p>
|
||||
</div>
|
||||
<span class="badge ${s.badgeClass}">${s.badge}</span>
|
||||
</div>
|
||||
<div class="frame" id="frame-${s.id}">
|
||||
<div class="app">
|
||||
<div class="top">
|
||||
<div class="brand">山峡化工</div>
|
||||
<div class="nav-slot" id="slot-${s.id}">
|
||||
<div class="l1" id="nav-${s.id}">
|
||||
${ALL_L1.slice(0, l1Count).map((name, i) => `
|
||||
<button type="button" class="l1-item ${i === activeIdx ? 'on' : ''}">
|
||||
<span>${name}</span><span class="caret">▾</span>
|
||||
</button>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="end"><div class="avatar"></div></div>
|
||||
</div>
|
||||
<div class="main" id="main-${s.id}">
|
||||
<div class="page-fake">
|
||||
<div class="bar"></div>
|
||||
<div class="bar w2"></div>
|
||||
<div class="bar w3"></div>
|
||||
<p>内容区(示意)</p>
|
||||
</div>
|
||||
<div class="mask"></div>
|
||||
<div class="panel" id="panel-${s.id}">${panelInner()}</div>
|
||||
<div class="measure" id="measure-${s.id}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')
|
||||
|
||||
requestAnimationFrame(layoutPanels)
|
||||
}
|
||||
|
||||
function layoutPanels() {
|
||||
strategies.forEach((s) => {
|
||||
const frame = document.getElementById(`frame-${s.id}`)
|
||||
const nav = document.getElementById(`nav-${s.id}`)
|
||||
const slot = document.getElementById(`slot-${s.id}`)
|
||||
const main = document.getElementById(`main-${s.id}`)
|
||||
const panel = document.getElementById(`panel-${s.id}`)
|
||||
const measure = document.getElementById(`measure-${s.id}`)
|
||||
if (!frame || !nav || !slot || !main || !panel) return
|
||||
|
||||
const frameRect = frame.getBoundingClientRect()
|
||||
const navRect = nav.getBoundingClientRect()
|
||||
const slotRect = slot.getBoundingClientRect()
|
||||
const mainRect = main.getBoundingClientRect()
|
||||
const slotMax = Math.min(720, frameRect.width - 280)
|
||||
|
||||
const localNav = {
|
||||
left: navRect.left - mainRect.left,
|
||||
width: navRect.width
|
||||
}
|
||||
const localSlot = {
|
||||
left: slotRect.left - mainRect.left,
|
||||
width: slotRect.width
|
||||
}
|
||||
const viewportW = mainRect.width
|
||||
|
||||
const pos = s.calc(localNav, slotMax, viewportW, localSlot)
|
||||
panel.style.left = `${Math.round(pos.left)}px`
|
||||
panel.style.width = `${Math.round(pos.width)}px`
|
||||
|
||||
const narrow = pos.width < 520
|
||||
measure.textContent = `面板宽 ≈ ${Math.round(pos.width)}px · 一级区 ≈ ${Math.round(navRect.width)}px`
|
||||
measure.className = `measure ${narrow ? '' : 'ok-m'}`
|
||||
})
|
||||
}
|
||||
|
||||
window.__pick = function (id) {
|
||||
const names = { a: 'A 现状同宽', b: 'B 最小宽度居中', c: 'C 固定铺满中间槽' }
|
||||
alert(`已记录偏好:${names[id] || id}\n\n请在对话里回复确认,或继续改参数再看。`)
|
||||
if (typeof toggleSelect === 'function') {
|
||||
const el = document.querySelector(`[data-choice="${id}"]`)
|
||||
if (el) toggleSelect(el)
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('countSeg').addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('button[data-count]')
|
||||
if (!btn) return
|
||||
l1Count = Number(btn.dataset.count)
|
||||
document.querySelectorAll('#countSeg button').forEach((b) => b.classList.toggle('active', b === btn))
|
||||
if (activeIdx >= l1Count) activeIdx = 0
|
||||
render()
|
||||
})
|
||||
|
||||
document.getElementById('activeSeg').addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('button[data-active]')
|
||||
if (!btn) return
|
||||
activeIdx = Math.min(Number(btn.dataset.active), l1Count - 1)
|
||||
document.querySelectorAll('#activeSeg button').forEach((b) => b.classList.toggle('active', b === btn))
|
||||
render()
|
||||
})
|
||||
|
||||
document.querySelector('.pick').addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('button[data-choice]')
|
||||
if (!btn) return
|
||||
window.__pick(btn.dataset.choice)
|
||||
})
|
||||
|
||||
window.addEventListener('resize', layoutPanels)
|
||||
render()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
# 门户大面板最小宽度(方案 B)
|
||||
|
||||
> **For agentic workers:** 仅改前端定位;完成后人工在窄权限账号下点开一级验证。
|
||||
|
||||
**Goal:** 一级菜单较少时,二级大面板仍有足够宽度(最小约 720px),相对一级居中。
|
||||
|
||||
**Architecture:** 在 `ArtPortalMegaMenu.syncPanelPosition` 中计算 `max(一级宽, min(720, 视口−32))`,再按一级中心水平居中并钳制边距。
|
||||
|
||||
**Tech Stack:** Vue 3 · `art-portal-mega-menu/index.vue`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 更新面板定位
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/components/core/layouts/art-portal-mega-menu/index.vue`
|
||||
- Modify: `docs/superpowers/specs/2026-08-01-portal-mega-menu-design.md`(已补充规则)
|
||||
|
||||
**Step:** 改写 `syncPanelPosition` 按方案 B 计算 `left` / `width`;`top` 仍为一级底边。
|
||||
|
||||
**Verify:** 一级仅 2/4 项时面板宽 ≈ 720;一级很多时面板随一级变宽;窗口很窄时不超出视口。
|
||||
@@ -0,0 +1,761 @@
|
||||
# 报价单审批人设置 Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 新增租户级「报价单审批人设置」,提交审批弹窗自动预填默认审批人(可改);未配置时行为与现网一致。
|
||||
|
||||
**Architecture:** 新表 `crm_quote_approver_setting` 存有序 `approver_ids` + 启用状态;CRM POST 风格 get/save API;前端设置页维护配置;报价提交弹窗打开时 GET 并预填。`submitQuote` 入参与审批链路不变。
|
||||
|
||||
**Tech Stack:** FastAPI + SQLAlchemy + MySQL;Vue 3 + Element Plus(web-admin);菜单权限 `crm:quote:approverSetting`
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-08-01-quote-approver-setting-design.md`
|
||||
**UI Demo:** `docs/fresh-mint-quote-approver-setting-demo.html`
|
||||
**UI 模式:** 单对象配置表单页(对照 Demo;非 P01 列表)。PR 注明「配置表单 + 提交预填」。
|
||||
|
||||
---
|
||||
|
||||
## File map
|
||||
|
||||
| File | Responsibility |
|
||||
|------|----------------|
|
||||
| `backend/scripts/migrate_20260801_crm_quote_approver_setting.sql` | 建表 + 菜单 |
|
||||
| `docs/sql/shanxia2.0.sql` | 同步 CREATE TABLE / 菜单 INSERT(与 migrate 一致) |
|
||||
| `backend/app/models/crm_models.py` | `CrmQuoteApproverSetting` |
|
||||
| `backend/app/crud/crm/quote_approver_setting.py` | parse/join helpers + get/save upsert |
|
||||
| `backend/app/schemas/crm/quote_approver_setting.py` | Save body |
|
||||
| `backend/app/api/crm_quote_routes.py` | 注册 get/save 路由 |
|
||||
| `backend/tests/test_quote_approver_setting.py` | 纯函数单测 |
|
||||
| `web-admin/src/api/crm/quote/index.ts` + `types.ts` | 前端 API/类型 |
|
||||
| `web-admin/src/views/crm/quote/approver-setting/index.vue` | 设置页 |
|
||||
| `web-admin/src/router/modules/sqxp.ts` | 路由 |
|
||||
| `web-admin/src/views/crm/quote/index.vue` | 提交弹窗预填 |
|
||||
| `web-admin/src/views/crm/quote/components/QuoteFormPanel.vue` | 提交弹窗预填 |
|
||||
| `web-admin/src/views/crm/quote/components/ApproverSelector.vue` | 打开时预填(若仍被调用) |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 纯函数与单测(TDD)
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/crud/crm/quote_approver_setting.py`(先只放 helpers)
|
||||
- Create: `backend/tests/test_quote_approver_setting.py`
|
||||
|
||||
- [ ] **Step 1: 写失败单测**
|
||||
|
||||
```python
|
||||
# backend/tests/test_quote_approver_setting.py
|
||||
from app.crud.crm.quote_approver_setting import (
|
||||
join_approver_ids,
|
||||
parse_approver_ids,
|
||||
prefill_approver_ids,
|
||||
)
|
||||
|
||||
|
||||
def test_parse_and_join_roundtrip():
|
||||
assert parse_approver_ids("a,b,c") == ["a", "b", "c"]
|
||||
assert parse_approver_ids(" a,, b ") == ["a", "b"]
|
||||
assert parse_approver_ids("") == []
|
||||
assert parse_approver_ids(None) == []
|
||||
assert join_approver_ids(["a", "b", ""]) == "a,b"
|
||||
|
||||
|
||||
def test_prefill_requires_enabled_and_nonempty():
|
||||
assert prefill_approver_ids(None) == []
|
||||
assert prefill_approver_ids({"status": "1", "approverIds": ["u1"]}) == []
|
||||
assert prefill_approver_ids({"status": "0", "approverIds": []}) == []
|
||||
assert prefill_approver_ids({"status": "0", "approverIds": ["u1", "u2"]}) == ["u1", "u2"]
|
||||
|
||||
|
||||
def test_prefill_filters_against_valid_ids():
|
||||
setting = {"status": "0", "approverIds": ["u1", "gone", "u2"]}
|
||||
assert prefill_approver_ids(setting, valid_ids={"u1", "u2"}) == ["u1", "u2"]
|
||||
assert prefill_approver_ids(setting, valid_ids=set()) == []
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑测确认失败**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && pytest tests/test_quote_approver_setting.py -v
|
||||
```
|
||||
|
||||
Expected: FAIL(模块/函数不存在)
|
||||
|
||||
- [ ] **Step 3: 实现 helpers**
|
||||
|
||||
```python
|
||||
# backend/app/crud/crm/quote_approver_setting.py
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, Iterable, List, Optional, Set
|
||||
|
||||
|
||||
def parse_approver_ids(raw: Optional[str]) -> List[str]:
|
||||
if not raw:
|
||||
return []
|
||||
out: List[str] = []
|
||||
for part in str(raw).split(","):
|
||||
pid = part.strip()
|
||||
if pid:
|
||||
out.append(pid)
|
||||
return out
|
||||
|
||||
|
||||
def join_approver_ids(ids: Iterable[str]) -> str:
|
||||
return ",".join([str(i).strip() for i in ids if str(i).strip()])
|
||||
|
||||
|
||||
def prefill_approver_ids(
|
||||
setting: Optional[Dict[str, Any]],
|
||||
*,
|
||||
valid_ids: Optional[Set[str]] = None,
|
||||
) -> List[str]:
|
||||
"""返回可预填的审批人 ID 列表;不可用则空列表。"""
|
||||
if not setting:
|
||||
return []
|
||||
if str(setting.get("status") or "") != "0":
|
||||
return []
|
||||
ids = list(setting.get("approverIds") or [])
|
||||
if valid_ids is not None:
|
||||
ids = [i for i in ids if str(i) in valid_ids]
|
||||
return [str(i) for i in ids if str(i).strip()]
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 再跑测确认通过**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && pytest tests/test_quote_approver_setting.py -v
|
||||
```
|
||||
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/app/crud/crm/quote_approver_setting.py backend/tests/test_quote_approver_setting.py
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(crm): add quote approver setting id helpers and unit tests
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: SQL 迁移(表 + 菜单)
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/scripts/migrate_20260801_crm_quote_approver_setting.sql`
|
||||
- Modify: `docs/sql/shanxia2.0.sql`(在 `crm_quote_approval` 表段落后插入 CREATE;在报价单管理菜单附近插入菜单行)
|
||||
|
||||
- [ ] **Step 1: 写 migrate SQL**
|
||||
|
||||
```sql
|
||||
-- backend/scripts/migrate_20260801_crm_quote_approver_setting.sql
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `crm_quote_approver_setting` (
|
||||
`id` varchar(64) NOT NULL COMMENT '主键ID(UUID)',
|
||||
`approver_ids` varchar(1000) NOT NULL DEFAULT '' COMMENT '有序审批人用户ID,逗号分隔',
|
||||
`status` char(1) NOT NULL DEFAULT '0' COMMENT '0启用 1停用',
|
||||
`tenant_no` varchar(64) NOT NULL DEFAULT 'shanxia2.0' COMMENT '租户编号',
|
||||
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
|
||||
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||
`create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
|
||||
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建部门',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_by` varchar(64) DEFAULT NULL COMMENT '更新者',
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`del_flag` smallint(6) NOT NULL DEFAULT '0' COMMENT '删除标志',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant_no` (`tenant_no`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-报价单审批人设置(租户级)';
|
||||
|
||||
-- 说明:MySQL 5.7 无部分唯一索引;租户「有效一条」由 save_setting upsert 保证,不加 (tenant_no, del_flag) 唯一键。
|
||||
|
||||
-- 菜单:报价单审批人设置(CRM 根下,紧挨报价单管理)
|
||||
INSERT INTO `sys_menu` (
|
||||
`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`,
|
||||
`is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`,
|
||||
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
|
||||
) VALUES (
|
||||
'CRM3000000000000000000000000015',
|
||||
'报价单审批人设置',
|
||||
'CRM00000000000000000000000000001',
|
||||
'C',
|
||||
'quote-approver-setting',
|
||||
'/crm/quote/approver-setting/index',
|
||||
'crm:quote:approverSetting',
|
||||
1, '1', '1', 'shanxia2.0', 55,
|
||||
'租户级默认报价审批人',
|
||||
'system', '', NOW(), NULL, NOW(), 0
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`menu_name`=VALUES(`menu_name`),
|
||||
`component`=VALUES(`component`),
|
||||
`perms`=VALUES(`perms`),
|
||||
`path`=VALUES(`path`);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 同步 `docs/sql/shanxia2.0.sql`**
|
||||
|
||||
将相同 `CREATE TABLE` 与 `INSERT INTO sys_menu` 片段并入快照(位置:报价相关表/菜单附近),避免文档与库漂移。
|
||||
|
||||
- [ ] **Step 3: 在本地库执行 migrate**(开发环境)
|
||||
|
||||
```bash
|
||||
# 按本地 .env 连接执行;示例:
|
||||
mysql -h... -u... -p... your_db < backend/scripts/migrate_20260801_crm_quote_approver_setting.sql
|
||||
```
|
||||
|
||||
Expected: 表与菜单创建成功。
|
||||
|
||||
- [ ] **Step 4: 给需要维护配置的角色勾选新菜单**
|
||||
|
||||
在「角色管理」中为管理员角色勾选「报价单审批人设置」(或按环境向 `sys_role_menu` 插入对应 `menu_id`)。`super_admin` 本身放行,可不依赖角色菜单。
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/scripts/migrate_20260801_crm_quote_approver_setting.sql docs/sql/shanxia2.0.sql
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(crm): add quote approver setting table and menu SQL
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Model + CRUD upsert + Schema
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/models/crm_models.py`(在 `CrmQuoteApproval` 前或后新增模型)
|
||||
- Modify: `backend/app/crud/crm/quote_approver_setting.py`(补 get/save)
|
||||
- Create: `backend/app/schemas/crm/quote_approver_setting.py`
|
||||
|
||||
- [ ] **Step 1: 增加 SQLAlchemy 模型**
|
||||
|
||||
```python
|
||||
class CrmQuoteApproverSetting(Base):
|
||||
"""CRM-报价单审批人设置(租户级)"""
|
||||
|
||||
__tablename__ = "crm_quote_approver_setting"
|
||||
|
||||
id = Column(String(64), primary_key=True, comment="主键ID(UUID)")
|
||||
approver_ids = Column(String(1000), nullable=False, default="", comment="有序审批人用户ID,逗号分隔")
|
||||
status = Column(String(1), nullable=False, default="0", comment="0启用 1停用")
|
||||
tenant_no = Column(String(64), nullable=False, default="shanxia2.0", comment="租户编号")
|
||||
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||
remark = Column(String(500), nullable=True, comment="备注")
|
||||
create_by = Column(String(64), nullable=True, comment="创建者")
|
||||
create_dept_id = Column(String(64), nullable=False, default="", comment="创建部门")
|
||||
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||||
update_by = Column(String(64), nullable=True, comment="更新者")
|
||||
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Schema**
|
||||
|
||||
```python
|
||||
# backend/app/schemas/crm/quote_approver_setting.py
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class QuoteApproverSettingSaveBody(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
approverIds: List[str] = Field(default_factory=list)
|
||||
status: str = Field(default="0") # 0启用 1停用
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 实现 get_setting / save_setting**
|
||||
|
||||
在 `quote_approver_setting.py` 追加(要点):
|
||||
|
||||
```python
|
||||
from datetime import datetime
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.utils import generate_uuid
|
||||
from app.models.crm_models import CrmQuoteApproverSetting
|
||||
from app.models.models import SysUser
|
||||
|
||||
|
||||
def _row_to_dict(row: CrmQuoteApproverSetting) -> Dict[str, Any]:
|
||||
return {
|
||||
"id": row.id,
|
||||
"approverIds": parse_approver_ids(row.approver_ids),
|
||||
"status": row.status or "0",
|
||||
"tenantNo": row.tenant_no,
|
||||
"updateTime": row.update_time.isoformat(sep=" ") if row.update_time else None,
|
||||
}
|
||||
|
||||
|
||||
def get_setting(db: Session, *, tenant_no: str) -> Optional[Dict[str, Any]]:
|
||||
row = (
|
||||
db.query(CrmQuoteApproverSetting)
|
||||
.filter(
|
||||
CrmQuoteApproverSetting.tenant_no == tenant_no,
|
||||
CrmQuoteApproverSetting.del_flag == 0,
|
||||
)
|
||||
.order_by(CrmQuoteApproverSetting.update_time.desc())
|
||||
.first()
|
||||
)
|
||||
return _row_to_dict(row) if row else None
|
||||
|
||||
|
||||
def _assert_users_exist(db: Session, *, tenant_no: str, approver_ids: List[str]) -> None:
|
||||
ids = [str(i).strip() for i in approver_ids if str(i).strip()]
|
||||
if not ids:
|
||||
return
|
||||
rows = (
|
||||
db.query(SysUser.id)
|
||||
.filter(SysUser.id.in_(ids), SysUser.del_flag == 0)
|
||||
.all()
|
||||
)
|
||||
found = {str(r[0]) for r in rows}
|
||||
missing = [i for i in ids if i not in found]
|
||||
if missing:
|
||||
raise ValueError(f"审批人无效或不存在:{','.join(missing)}")
|
||||
|
||||
|
||||
def save_setting(
|
||||
db: Session,
|
||||
*,
|
||||
tenant_no: str,
|
||||
approver_ids: List[str],
|
||||
status: str,
|
||||
user_id: str = None,
|
||||
dept_id: str = "",
|
||||
) -> Dict[str, Any]:
|
||||
status_norm = "1" if str(status) == "1" else "0"
|
||||
ids = [str(i).strip() for i in (approver_ids or []) if str(i).strip()]
|
||||
_assert_users_exist(db, tenant_no=tenant_no, approver_ids=ids)
|
||||
|
||||
row = (
|
||||
db.query(CrmQuoteApproverSetting)
|
||||
.filter(
|
||||
CrmQuoteApproverSetting.tenant_no == tenant_no,
|
||||
CrmQuoteApproverSetting.del_flag == 0,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
now = datetime.now()
|
||||
if row:
|
||||
row.approver_ids = join_approver_ids(ids)
|
||||
row.status = status_norm
|
||||
row.update_by = user_id
|
||||
row.update_time = now
|
||||
else:
|
||||
row = CrmQuoteApproverSetting(
|
||||
id=generate_uuid(),
|
||||
approver_ids=join_approver_ids(ids),
|
||||
status=status_norm,
|
||||
tenant_no=tenant_no,
|
||||
create_by=user_id,
|
||||
create_dept_id=dept_id or "",
|
||||
create_time=now,
|
||||
update_by=user_id,
|
||||
update_time=now,
|
||||
del_flag=0,
|
||||
)
|
||||
db.add(row)
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
return _row_to_dict(row)
|
||||
```
|
||||
|
||||
注意:若 `SysUser` 有租户字段且项目惯例按租户过滤用户,在 `_assert_users_exist` 中追加与现有用户查询一致的 `tenant_no` 条件。
|
||||
|
||||
- [ ] **Step 4: 再跑 Task 1 单测**(确保 helpers 未破坏)
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && pytest tests/test_quote_approver_setting.py -v
|
||||
```
|
||||
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/app/models/crm_models.py backend/app/crud/crm/quote_approver_setting.py backend/app/schemas/crm/quote_approver_setting.py
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(crm): implement quote approver setting model and upsert CRUD
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: API 路由
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/api/crm_quote_routes.py`
|
||||
|
||||
- [ ] **Step 1: 在 `register_quote_routes` 末尾注册两个 POST**
|
||||
|
||||
风格对齐现有 `/quote/*`(POST + body),路径:
|
||||
|
||||
- `POST /quote-approver-setting/get` — `Depends(require_admin_or_super_admin)`(业务员可读以预填)
|
||||
- `POST /quote-approver-setting/save` — `Depends(require_menu_code("crm:quote:approverSetting"))`
|
||||
|
||||
```python
|
||||
from app.core.menu_guard import require_admin_or_super_admin, require_menu_code
|
||||
from app.crud.crm.quote_approver_setting import get_setting, save_setting
|
||||
from app.schemas.crm.quote_approver_setting import QuoteApproverSettingSaveBody
|
||||
|
||||
# 在 register_quote_routes 内:
|
||||
|
||||
@router.post("/quote-approver-setting/get")
|
||||
async def quote_approver_setting_get(
|
||||
_user: dict = Depends(require_admin_or_super_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
data = get_setting(db, tenant_no=tenant_no)
|
||||
return success_response(data or {"approverIds": [], "status": "0"})
|
||||
except Exception as e:
|
||||
return error_response(500, str(e))
|
||||
|
||||
|
||||
@router.post("/quote-approver-setting/save")
|
||||
async def quote_approver_setting_save(
|
||||
body: QuoteApproverSettingSaveBody,
|
||||
_user: dict = Depends(require_menu_code("crm:quote:approverSetting")),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
data = save_setting(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
approver_ids=body.approverIds or [],
|
||||
status=body.status or "0",
|
||||
user_id=user_info.get("user_id") or _user.get("user_id"),
|
||||
dept_id=user_info.get("dept_id") or "",
|
||||
)
|
||||
return success_response(data, message="保存成功")
|
||||
except ValueError as e:
|
||||
return error_response(400, str(e))
|
||||
except Exception as e:
|
||||
return error_response(500, str(e))
|
||||
```
|
||||
|
||||
未配置时 GET 返回 `{ approverIds: [], status: "0" }`(表单默认启用;`approverIds` 为空故预填仍为空)。勿用 `status:"1"` 作为空配置哨兵,否则设置页首次打开会落到「停用」。
|
||||
|
||||
- [ ] **Step 2: 手工用已登录 token 冒烟**(Swagger `/docs` 或 curl)
|
||||
|
||||
Expected: get 返回空配置;save 后 get 带回 `approverIds`;无菜单权限用户 save → 403。
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/app/api/crm_quote_routes.py
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(crm): expose quote approver setting get/save APIs
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 前端 API 与类型
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/api/crm/quote/types.ts`
|
||||
- Modify: `web-admin/src/api/crm/quote/index.ts`
|
||||
|
||||
- [ ] **Step 1: 类型**
|
||||
|
||||
```ts
|
||||
export interface QuoteApproverSettingVO {
|
||||
id?: string
|
||||
approverIds: (string | number)[]
|
||||
status: '0' | '1' | string
|
||||
updateTime?: string
|
||||
}
|
||||
|
||||
export interface QuoteApproverSettingForm {
|
||||
approverIds: (string | number)[]
|
||||
status: '0' | '1'
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: API**
|
||||
|
||||
```ts
|
||||
const SETTING_BASE = '/web/crm/quote-approver-setting'
|
||||
|
||||
export async function getQuoteApproverSetting() {
|
||||
const res = await request.post({ url: `${SETTING_BASE}/get`, data: {} })
|
||||
const data = unwrapData(res) || {}
|
||||
return {
|
||||
data: {
|
||||
id: data.id,
|
||||
approverIds: (data.approverIds || []).map(String),
|
||||
status: data.status ?? '1',
|
||||
updateTime: data.updateTime
|
||||
} as QuoteApproverSettingVO
|
||||
}
|
||||
}
|
||||
|
||||
export function saveQuoteApproverSetting(data: QuoteApproverSettingForm) {
|
||||
return request.post({
|
||||
url: `${SETTING_BASE}/save`,
|
||||
data: {
|
||||
approverIds: (data.approverIds || []).map(String),
|
||||
status: data.status
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add web-admin/src/api/crm/quote/types.ts web-admin/src/api/crm/quote/index.ts
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(web-admin): add quote approver setting API client
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 设置页 + 路由
|
||||
|
||||
**Files:**
|
||||
- Create: `web-admin/src/views/crm/quote/approver-setting/index.vue`
|
||||
- Modify: `web-admin/src/router/modules/sqxp.ts`(在 `CrmQuote` 路由后插入)
|
||||
|
||||
- [ ] **Step 1: 路由**
|
||||
|
||||
```ts
|
||||
{
|
||||
id: 'CRM3000000000000000000000000015',
|
||||
name: 'CrmQuoteApproverSetting',
|
||||
path: 'quote-approver-setting',
|
||||
component: '/crm/quote/approver-setting/index',
|
||||
meta: {
|
||||
title: '报价单审批人设置',
|
||||
keepAlive: true,
|
||||
menuCode: 'crm:quote:approverSetting'
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 页面骨架(对齐 Demo / Fresh Mint)**
|
||||
|
||||
要点:
|
||||
|
||||
- `page-container` → `section.panel` → `panel-top` + `h2.title-bar-heading`「报价单审批人设置」
|
||||
- `el-form`:`label-position="top"`,`form-grid-2`
|
||||
- 字段:
|
||||
- 默认审批人:`el-select` multiple(复用报价页 `fetchUserOptions` / 同款 label 格式)
|
||||
- 状态:`el-radio-group` 启用 `0` / 停用 `1`
|
||||
- 底栏:重置 + 保存(`el-icon` Close/RefreshLeft + Check;保存 `type="primary"`)
|
||||
- `onMounted` 调 `getQuoteApproverSetting` 回填;保存调 `saveQuoteApproverSetting`
|
||||
- 审批人允许空数组保存(清空后不再预填)
|
||||
|
||||
参考结构(精简):
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">报价单审批人设置</h2>
|
||||
</div>
|
||||
<div class="panel-body" style="padding: 16px 20px 24px">
|
||||
<el-form ref="formRef" :model="form" label-position="top" class="crm-dialog-form">
|
||||
<div class="form-grid-2">
|
||||
<el-form-item class="form-col-span-2">
|
||||
<template #label>
|
||||
默认审批人<span class="form-label-tip">支持多人,按选择顺序依次审批</span>
|
||||
</template>
|
||||
<el-select v-model="form.approverIds" multiple filterable placeholder="请选择审批人" style="width: 100%">
|
||||
<el-option
|
||||
v-for="user in userList"
|
||||
:key="user.userId"
|
||||
:label="formatApproverOptionLabel(user)"
|
||||
:value="user.userId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio value="0">启用</el-radio>
|
||||
<el-radio value="1">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:16px">
|
||||
<el-button @click="loadSetting"><el-icon><RefreshLeft /></el-icon>重置</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="handleSave">
|
||||
<el-icon><Check /></el-icon>保存设置
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
(`el-radio` 的 `value`/`label` 写法与项目 Element Plus 版本保持一致。)
|
||||
|
||||
- [ ] **Step 3: 本地验证**
|
||||
|
||||
打开 `/#/crm/quote-approver-setting`(需菜单权限)→ 选人保存 → 刷新仍在。
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add web-admin/src/views/crm/quote/approver-setting/index.vue web-admin/src/router/modules/sqxp.ts
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(web-admin): add quote approver setting page and route
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: 提交审批弹窗预填
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/views/crm/quote/index.vue`
|
||||
- Modify: `web-admin/src/views/crm/quote/components/QuoteFormPanel.vue`
|
||||
- Modify: `web-admin/src/views/crm/quote/components/ApproverSelector.vue`(若 `open` 仍被使用)
|
||||
|
||||
- [ ] **Step 1: 抽共用预填逻辑(可内联,避免过度抽象)**
|
||||
|
||||
在打开提交弹窗处(`handleSubmit` / 表单内提交):
|
||||
|
||||
```ts
|
||||
import { getQuoteApproverSetting } from '@/api/crm/quote'
|
||||
// 若后端 helpers 不在前端,前端等价:
|
||||
async function resolvePrefillApproverIds(userList: { userId: string | number }[]) {
|
||||
try {
|
||||
const { data } = await getQuoteApproverSetting()
|
||||
if (!data || String(data.status) !== '0') return [] as string[]
|
||||
const valid = new Set(userList.map((u) => String(u.userId)))
|
||||
return (data.approverIds || []).map(String).filter((id) => valid.has(id))
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: `index.vue` 的 `handleSubmit` / 详情内提交**
|
||||
|
||||
在 `loadUserList()` 之后:
|
||||
|
||||
```ts
|
||||
const prefillIds = await resolvePrefillApproverIds(userList.value)
|
||||
submitForm.value = {
|
||||
id,
|
||||
approverIds: prefillIds,
|
||||
remark: ''
|
||||
}
|
||||
submitPrefillTip.value = prefillIds.length > 0
|
||||
submitDialog.visible = true
|
||||
```
|
||||
|
||||
模板中审批人表单项上方(有预填时):
|
||||
|
||||
```vue
|
||||
<el-alert
|
||||
v-if="submitPrefillTip"
|
||||
type="success"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="已按「报价单审批人设置」预填审批人,可修改后提交"
|
||||
style="margin-bottom: 12px"
|
||||
/>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: `QuoteFormPanel.vue` 同样改造**打开提交对话框的逻辑。
|
||||
|
||||
- [ ] **Step 4: `ApproverSelector.vue`**
|
||||
|
||||
在 `open()` 内 `loadUsers` 后预填 `form.approverIds`(类型与选项 `userId` 一致;若当前是 `number[]`,统一为 `string[]` 更稳妥)。
|
||||
|
||||
- [ ] **Step 5: 手工验收**
|
||||
|
||||
1. 设置页启用并保存张经理→李总
|
||||
2. 报价列表提交审批:弹窗已预填,提示出现
|
||||
3. 改人后提交成功
|
||||
4. 设置停用或清空:弹窗为空,须手选
|
||||
5. 断网/接口失败:弹窗仍可打开,审批人为空
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add web-admin/src/views/crm/quote/index.vue \
|
||||
web-admin/src/views/crm/quote/components/QuoteFormPanel.vue \
|
||||
web-admin/src/views/crm/quote/components/ApproverSelector.vue
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(web-admin): prefill quote submit approvers from tenant setting
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: 收尾
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/superpowers/specs/2026-08-01-quote-approver-setting-design.md`(状态改为「已实现」)
|
||||
- Optional: `docs/web-admin-ui-pattern-index.md` 追加一行业务页对照(报价审批人设置 → 配置表单)
|
||||
- Run: `./scripts/update-graph.sh backend` 与 `./scripts/update-graph.sh frontend`(或 `all`),若本会话改了代码
|
||||
|
||||
- [ ] **Step 1: 更新设计文档状态**
|
||||
|
||||
将 **状态:** 改为 `已实现`。
|
||||
|
||||
- [ ] **Step 2: 回归 pytest + 关键页面**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && pytest tests/test_quote_approver_setting.py -v
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/superpowers/specs/2026-08-01-quote-approver-setting-design.md docs/web-admin-ui-pattern-index.md
|
||||
git commit -m "$(cat <<'EOF'
|
||||
docs: mark quote approver setting design implemented
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Spec coverage checklist
|
||||
|
||||
| Spec 项 | Task |
|
||||
|---------|------|
|
||||
| 租户级设置表 | Task 2–3 |
|
||||
| GET/PUT(实现为 POST get/save) | Task 4 |
|
||||
| 提交可改预填 | Task 7 |
|
||||
| 菜单权限维护 | Task 2 + 4 save 守卫 |
|
||||
| 未配置=手选 | Task 4 空响应 + Task 7 |
|
||||
| 不做默认备注 | 全任务未引入 |
|
||||
| 无效用户过滤/校验 | Task 3 save + Task 7 prefill |
|
||||
| 验收标准 1–5 | Task 7 Step 5 |
|
||||
|
||||
## Self-review notes
|
||||
|
||||
- API 与设计文档的 GET/PUT 字面差异:本计划按 CRM 既有 **POST** 风格落地,语义等价。
|
||||
- 租户单条有效配置靠 `save_setting` upsert,不加部分唯一索引(MySQL 5.7)。
|
||||
- 前端不强制单测文件(与仓库现状一致);后端用 helpers 单测锁预填语义。
|
||||
@@ -0,0 +1,37 @@
|
||||
# 门户顶栏大面板菜单(方案 A)
|
||||
|
||||
## 目标
|
||||
|
||||
将 web-admin 门户布局从「顶栏一级下拉 + 左侧二/三级树」改为「顶栏横排一级 + 点击展开大面板」,取消左侧常驻菜单,内容区全宽。
|
||||
|
||||
## 交互
|
||||
|
||||
1. 顶栏横排展示全部一级模块。
|
||||
2. 点击一级展开大面板;再点同一级或遮罩关闭。
|
||||
3. 面板内左右两栏,二级行左右均分(如 12→6+6);建议每栏可见约 5 行,超出后面板整体纵向滚动。
|
||||
4. 每行:左侧二级标题(不可点)+ 右侧三级文字链(可点)。
|
||||
5. 二级无子节点且自身可导航时:生成与二级同名的虚拟三级,路由继承该二级。
|
||||
6. 点击三级跳转并关闭面板。
|
||||
|
||||
## 范围
|
||||
|
||||
- 仅 `MenuTypeEnum.PORTAL` 布局。
|
||||
- 不改后端菜单数据模型;虚拟三级仅在前端展示层生成。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 方案 B(5 列网格)本期不落地。
|
||||
- 面板内搜索可后续增强。
|
||||
|
||||
## 面板宽度(一级较少时)
|
||||
|
||||
权限导致一级模块仅 2~4 个时,一级横排区域会变窄。大面板宽度不得简单等于一级区域宽度。
|
||||
|
||||
**策略(已确认):最小宽度 + 相对一级居中**
|
||||
|
||||
1. `width = max(一级区域宽度, min(720px, 视口宽度 − 32px))`
|
||||
2. 水平以一级区域中心为基准居中;左右各保留约 16px,避免贴边溢出
|
||||
3. 一级区域已 ≥ 720px 时,面板仍跟随一级区域宽度(与原先「同宽」一致)
|
||||
4. 纵向仍贴在一级栏下方;面板内部左右分栏布局不变
|
||||
|
||||
参考 Demo:`docs/portal-mega-panel-width-demo.html`
|
||||
@@ -65,8 +65,8 @@
|
||||
|
||||
| 方法 | 路径(示意) | 说明 |
|
||||
|------|----------------|------|
|
||||
| GET | `/api/web/admin/crm/quote-approver-setting` | 返回当前租户配置;无记录返回空对象/`null`(前端当未配置) |
|
||||
| PUT | `/api/web/admin/crm/quote-approver-setting` | Body:`{ approverIds: string[], status: '0'\|'1' }`;有则更新、无则创建 |
|
||||
| POST | `/api/web/.../crm/quote-approver-setting/get` | 返回当前租户配置;无记录返回 `{ approverIds: [], status: '0' }`(空列表不预填;默认启用避免设置页首次落到停用) |
|
||||
| POST | `/api/web/.../crm/quote-approver-setting/save` | Body:`{ approverIds: string[], status: '0'\|'1' }`;有则更新、无则创建 |
|
||||
|
||||
约束:
|
||||
|
||||
|
||||
+165
-150
@@ -930,8 +930,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/api/crm/contract/index.ts": {
|
||||
"mtime": 1784432876.0,
|
||||
"ast_hash": "96ea22fdf780e1dbb45ef4ddca1ac752",
|
||||
"mtime": 1785562213.6032443,
|
||||
"ast_hash": "6552d015ed1ceece5a692541a15d267e",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/api/crm/contract/template.ts": {
|
||||
@@ -940,8 +940,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/api/crm/contract/types.ts": {
|
||||
"mtime": 1785418058.3253186,
|
||||
"ast_hash": "2bdd57895c96899c016a5381cc3aadf6",
|
||||
"mtime": 1785562198.7176075,
|
||||
"ast_hash": "96af4081ba44c4dfcf7290bfd0e96c2f",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/api/crm/customer.ts": {
|
||||
@@ -995,13 +995,13 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/api/crm/quote/index.ts": {
|
||||
"mtime": 1782858537.0,
|
||||
"ast_hash": "64a4aeed132b7e5f91e77500cdc72078",
|
||||
"mtime": 1785561129.8765674,
|
||||
"ast_hash": "db60c5fb2bf7b095b34d9c59ce1b23e6",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/api/crm/quote/types.ts": {
|
||||
"mtime": 1785418057.2499275,
|
||||
"ast_hash": "79c22f0c0778870e8d63cd810235025f",
|
||||
"mtime": 1785560664.0574367,
|
||||
"ast_hash": "e2075c731aecd18d9109499499e0e460",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/api/crm/supplierInfo/index.ts": {
|
||||
@@ -1065,8 +1065,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/api/pl/index.ts": {
|
||||
"mtime": 1785453592.784731,
|
||||
"ast_hash": "cd17a6d2b4d1ee6b6306570460406561",
|
||||
"mtime": 1785484576.5006232,
|
||||
"ast_hash": "618f676f635932cb16ea78b4b316aca9",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/api/pl/public-io-order.ts": {
|
||||
@@ -1270,8 +1270,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/components/business/pl-freight-approval-dialog/index.vue": {
|
||||
"mtime": 1783753616.0,
|
||||
"ast_hash": "3fe1efff0d2c38b03c8c54ea019d5178",
|
||||
"mtime": 1785554123.0353835,
|
||||
"ast_hash": "8fd128b63e2a27ac51d1e181844d31c4",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/components/business/pl-freight-disburse-dialog/index.vue": {
|
||||
@@ -1585,8 +1585,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/components/bx/template/ListToolbar.vue": {
|
||||
"mtime": 1782858534.0,
|
||||
"ast_hash": "8184200c37281fa8875a3ab8286d6d1f",
|
||||
"mtime": 1785483961.7167788,
|
||||
"ast_hash": "157a2d009062d996421bf8835c899c59",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/components/common/amount-input/index.vue": {
|
||||
@@ -1820,13 +1820,18 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/components/core/layouts/art-page-content/index.vue": {
|
||||
"mtime": 1782858532.0,
|
||||
"ast_hash": "2fa976632599fa2c6c7dd05533098acf",
|
||||
"mtime": 1785574166.2589664,
|
||||
"ast_hash": "acb2c52fb67a465de38da3cccf949018",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/components/core/layouts/art-portal-header/index.vue": {
|
||||
"mtime": 1784980421.0,
|
||||
"ast_hash": "8e5e59eacc383443e87def622e246c08",
|
||||
"mtime": 1785573262.1831734,
|
||||
"ast_hash": "6ad6a77d5e884aa5e8526aaf7c68a70c",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/components/core/layouts/art-portal-mega-menu/index.vue": {
|
||||
"mtime": 1785573680.8338625,
|
||||
"ast_hash": "555646bf9b64c4a7ff733e3050fbaa2c",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/components/core/layouts/art-portal-mobile-menu/index.vue": {
|
||||
@@ -1950,8 +1955,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/components/core/list-toolbar-date-shortcuts/index.vue": {
|
||||
"mtime": 1785483565.1394918,
|
||||
"ast_hash": "27af14f00670c5d8e3ad33a58edba167",
|
||||
"mtime": 1785484024.3856483,
|
||||
"ast_hash": "854eb23238e83ac6b8a06a427d072cb2",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/components/core/media/art-cutter-img/index.vue": {
|
||||
@@ -2225,8 +2230,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/hooks/core/useLayoutHeight.ts": {
|
||||
"mtime": 1782858536.0,
|
||||
"ast_hash": "77f4c895199ca2a026716644183506ee",
|
||||
"mtime": 1785574161.3632877,
|
||||
"ast_hash": "9d07063081ed7955a0c225dc90d8c3e2",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/hooks/core/useTable.ts": {
|
||||
@@ -2375,8 +2380,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/router/modules/sqxp.ts": {
|
||||
"mtime": 1785038548.642912,
|
||||
"ast_hash": "982e56f3715b56249e52c58b92de1089",
|
||||
"mtime": 1785562198.7176168,
|
||||
"ast_hash": "5f10df50a13a96cdbb8f82214845e327",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/router/routes/asyncRoutes.ts": {
|
||||
@@ -2820,8 +2825,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/utils/router/portal-menu.ts": {
|
||||
"mtime": 1782858530.0,
|
||||
"ast_hash": "cb43a7a4d46a12f342b672d5fcbbed55",
|
||||
"mtime": 1785573820.0049753,
|
||||
"ast_hash": "a757f0d7421b493ad5b7f6a8e30e233e",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/utils/router/sidebar-menu-filter.ts": {
|
||||
@@ -2985,23 +2990,23 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/bx/advance/apply/index.vue": {
|
||||
"mtime": 1785483535.8855953,
|
||||
"ast_hash": "642bc8fcaca8dc9c5df342b1ca430eef",
|
||||
"mtime": 1785576463.2360728,
|
||||
"ast_hash": "462508d9a772f17b3d8cb456695a12fb",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/bx/advance/approve/index.vue": {
|
||||
"mtime": 1785483535.8871527,
|
||||
"ast_hash": "116a725df6fc8b24db7dee414c80de3e",
|
||||
"mtime": 1785576478.511863,
|
||||
"ast_hash": "c0b92572a594f8de091a1336e7023c1a",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/bx/advance/issue/index.vue": {
|
||||
"mtime": 1785483535.8885617,
|
||||
"ast_hash": "bc4fb51e76b82f36b029018fb59e2d4a",
|
||||
"mtime": 1785484060.3834023,
|
||||
"ast_hash": "ca82ee39f3412e04c84b1c167757523a",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/bx/expense-item/index.vue": {
|
||||
"mtime": 1785483535.889635,
|
||||
"ast_hash": "0c0a43378e61b3e57e7a05a818707183",
|
||||
"mtime": 1785484060.3895984,
|
||||
"ast_hash": "76d76013592efe9ade30a3e867febffa",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/bx/invoice-wallet/admin/index.vue": {
|
||||
@@ -3015,18 +3020,18 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/bx/payment/index.vue": {
|
||||
"mtime": 1785483535.8920467,
|
||||
"ast_hash": "4283881d05e9aa3dd4d9273317e20ed1",
|
||||
"mtime": 1785484060.3803341,
|
||||
"ast_hash": "d1171a0901751a08fd58793c7af0e9d1",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/bx/reimburse/apply/index.vue": {
|
||||
"mtime": 1785483535.898319,
|
||||
"ast_hash": "66b6bd332151675ad012cf15ff48d330",
|
||||
"mtime": 1785576281.8421597,
|
||||
"ast_hash": "34959f4d18846d87de0c82822638d267",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/bx/reimburse/approve/index.vue": {
|
||||
"mtime": 1785483535.9008098,
|
||||
"ast_hash": "4c78704a5ea23404a73b7b102125ed63",
|
||||
"mtime": 1785576338.6838882,
|
||||
"ast_hash": "5071e36c9219c711fb58dec19bf790c2",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/bx/view/all/index.vue": {
|
||||
@@ -3040,18 +3045,18 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/bx/view/todo/index.vue": {
|
||||
"mtime": 1785483535.9015062,
|
||||
"ast_hash": "0460e5726f8f702942d1e9c57627c4a5",
|
||||
"mtime": 1785554184.3029728,
|
||||
"ast_hash": "90adfcb301ac359daecd4d4bbd03115a",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/bx/workflow/advance/index.vue": {
|
||||
"mtime": 1785483535.9021401,
|
||||
"ast_hash": "4766897e70ff451e346b220e17e67811",
|
||||
"mtime": 1785484060.3879302,
|
||||
"ast_hash": "f92a441878a7e9e76bb211398ef57f6a",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/bx/workflow/reimburse/index.vue": {
|
||||
"mtime": 1785483535.9033148,
|
||||
"ast_hash": "d6e65274bd2f93f54e836fbc9a7fe2c4",
|
||||
"mtime": 1785484060.3882713,
|
||||
"ast_hash": "b8794c0a2d6a97a8b91a51b14e6b4885",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/crm/clue/assignForm.vue": {
|
||||
@@ -3085,8 +3090,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/crm/clue/index.vue": {
|
||||
"mtime": 1785483535.905028,
|
||||
"ast_hash": "abae2c93a81a4bba18e2060865294095",
|
||||
"mtime": 1785484060.3903744,
|
||||
"ast_hash": "c49f5740e60dbda53241bd0ce8c23670",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/crm/clue/invalidForm.vue": {
|
||||
@@ -3100,18 +3105,18 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/crm/contract/approval/index.vue": {
|
||||
"mtime": 1785476270.7298734,
|
||||
"ast_hash": "82474b45a3eff5d76a72357901c098f2",
|
||||
"mtime": 1785554188.8024724,
|
||||
"ast_hash": "3fbd9fe13fa2abc69ea4063c79dcb7c4",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/crm/contract/change/index.vue": {
|
||||
"mtime": 1785483535.906201,
|
||||
"ast_hash": "ea9b2230531aaaff6af4c27dc50f5640",
|
||||
"mtime": 1785484060.39825,
|
||||
"ast_hash": "50fa69013017bd40cbdacad1f1a27b21",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/crm/contract/components/ApprovalDialog.vue": {
|
||||
"mtime": 1784432883.0,
|
||||
"ast_hash": "9e7271444d197c71909b1a32c29ca9b7",
|
||||
"mtime": 1785575851.7262146,
|
||||
"ast_hash": "03dd18359ab3c2838950bb8455c0f183",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/crm/contract/components/ContractAttachmentPanel.vue": {
|
||||
@@ -3165,13 +3170,13 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/crm/contract/index.vue": {
|
||||
"mtime": 1785483590.9993153,
|
||||
"ast_hash": "89a68c57dbfaf8d1d408ab2f70584b3f",
|
||||
"mtime": 1785575898.0943983,
|
||||
"ast_hash": "10e48bacd13d0f93328f68839a94e209",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/crm/contract/template/index.vue": {
|
||||
"mtime": 1785483535.9146812,
|
||||
"ast_hash": "76db6acb152be4a128a9690e6fc5227c",
|
||||
"mtime": 1785484060.3985548,
|
||||
"ast_hash": "00b523eac8b4fa99bc8366d805adbf33",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/crm/customer/CustomerDetailDialog.vue": {
|
||||
@@ -3220,13 +3225,13 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/crm/customer/index.vue": {
|
||||
"mtime": 1785483535.9185019,
|
||||
"ast_hash": "2c2d683d4d687c4b410fb9a003632ab9",
|
||||
"mtime": 1785484060.4056373,
|
||||
"ast_hash": "2d059e1b1fa1b8eb1d662a211b12b51a",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/crm/customer/pool/RuleIndex.vue": {
|
||||
"mtime": 1785483535.9194925,
|
||||
"ast_hash": "0b26551c3e5fc49a0e3a66d2bee5040f",
|
||||
"mtime": 1785484060.4096186,
|
||||
"ast_hash": "528efbc9627a9e305a834d5a3fa09bdd",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/crm/customer/pool/components/AssignDialog.vue": {
|
||||
@@ -3245,8 +3250,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/crm/customer/pool/index.vue": {
|
||||
"mtime": 1785483535.9205394,
|
||||
"ast_hash": "157ebe1471497148605f2e4363f7ca06",
|
||||
"mtime": 1785484060.4091,
|
||||
"ast_hash": "ff5ab04aeda3a2369b8c9b1cf28337ee",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/crm/opportunity/components/BatchUpdateDateDialog.vue": {
|
||||
@@ -3325,23 +3330,28 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/crm/product-category/index.vue": {
|
||||
"mtime": 1783749495.0,
|
||||
"ast_hash": "884649faca91290b04c9287b0dfaef37",
|
||||
"mtime": 1785483983.372002,
|
||||
"ast_hash": "3af324442041a5f0c147ae61d8f71984",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/crm/product/index.vue": {
|
||||
"mtime": 1785483535.925168,
|
||||
"ast_hash": "1c89afe5b78f15510c5fc6bf40dedffb",
|
||||
"mtime": 1785484060.4052067,
|
||||
"ast_hash": "0feaafc2cce101e3beece19e0ff57671",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/crm/quote/approver-setting/index.vue": {
|
||||
"mtime": 1785561133.7066674,
|
||||
"ast_hash": "721699f8c9f13cfe9e11e4a040dd7030",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/crm/quote/components/ApprovalDialog.vue": {
|
||||
"mtime": 1783753615.0,
|
||||
"ast_hash": "2663d2ded83d77600246f8dd4c939b93",
|
||||
"mtime": 1785554132.7209337,
|
||||
"ast_hash": "8651b3d4814aa74e1dbf387605192722",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/crm/quote/components/ApproverSelector.vue": {
|
||||
"mtime": 1784513207.0,
|
||||
"ast_hash": "e966a94b79ed4774511b2e8fa091cb52",
|
||||
"mtime": 1785561155.732058,
|
||||
"ast_hash": "2f1f262d7f5bb76d5f0b392f1652b86d",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/crm/quote/components/ProductSelector.vue": {
|
||||
@@ -3360,8 +3370,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/crm/quote/components/QuoteFormPanel.vue": {
|
||||
"mtime": 1785418107.430554,
|
||||
"ast_hash": "b2680f0756f645c4acd2602beaa110bd",
|
||||
"mtime": 1785573071.6256256,
|
||||
"ast_hash": "3f303b88dcd12eead55ffba1f2c9ac46",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/crm/quote/components/ShareDialog.vue": {
|
||||
@@ -3395,8 +3405,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/crm/quote/index.vue": {
|
||||
"mtime": 1785483592.9234543,
|
||||
"ast_hash": "01067387bb862a72548237babfcc0dd9",
|
||||
"mtime": 1785575569.4065406,
|
||||
"ast_hash": "ca3484be12868070f830cc5d37f55ab0",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/crm/quote/statistics.vue": {
|
||||
@@ -3470,8 +3480,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/index/index.vue": {
|
||||
"mtime": 1784974043.0,
|
||||
"ast_hash": "c0ac5f6b4e6e8ae8679e2415bba44faa",
|
||||
"mtime": 1785550322.081815,
|
||||
"ast_hash": "daac0b5ab188a0a361fa4615baaf1229",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/index/route-parent.vue": {
|
||||
@@ -3485,18 +3495,18 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/pl/_shared/IoOrderListPage.vue": {
|
||||
"mtime": 1785483535.943148,
|
||||
"ast_hash": "1f1f09c8eb5c367831c9e866d43b93d3",
|
||||
"mtime": 1785546815.3312225,
|
||||
"ast_hash": "c1596dbaa7e56aa6c2132b095c18205f",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/pl/carrier/index.vue": {
|
||||
"mtime": 1785483535.9438162,
|
||||
"ast_hash": "76ef196f4c72393033c85c6aab18bbaf",
|
||||
"mtime": 1785484060.378389,
|
||||
"ast_hash": "048d4ee0a23b918d5f3f76cf68619cd4",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/pl/driver/index.vue": {
|
||||
"mtime": 1785483535.9445777,
|
||||
"ast_hash": "9247a9f1717dfe1c827f2180ba8d3404",
|
||||
"mtime": 1785484060.3675325,
|
||||
"ast_hash": "6b110ad7420e7af993d4c8167e6c5e75",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/pl/freight-approval/index.vue": {
|
||||
@@ -3510,33 +3520,33 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/pl/freight/components/ApprovalDonePanel.vue": {
|
||||
"mtime": 1785483535.9455595,
|
||||
"ast_hash": "878cc9f322235bbc77ba7f1e200345ce",
|
||||
"mtime": 1785484060.3745458,
|
||||
"ast_hash": "3955a09e5a8de3181af74eb85cecc0c3",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/pl/freight/components/ApprovalPendingPanel.vue": {
|
||||
"mtime": 1785483535.946413,
|
||||
"ast_hash": "30a9138e6d9d37b7c564d5282335c79a",
|
||||
"mtime": 1785484060.3717885,
|
||||
"ast_hash": "4799a29a64174ead4568a97c825c06d3",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/pl/freight/components/DisbursePanel.vue": {
|
||||
"mtime": 1785483535.9468424,
|
||||
"ast_hash": "d6959df827b6508c33beec7f9b164c7e",
|
||||
"mtime": 1785484060.372107,
|
||||
"ast_hash": "fc98a489cea7052f878474b526d93dd6",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/pl/freight/components/SettledPanel.vue": {
|
||||
"mtime": 1785483535.9472184,
|
||||
"ast_hash": "fe822f834e9582f2758ba293139e2b9a",
|
||||
"mtime": 1785484060.3727734,
|
||||
"ast_hash": "0f629f81596fc2d3f6743423e91fd842",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/pl/freight/components/SettlementBillPanel.vue": {
|
||||
"mtime": 1785483535.9478219,
|
||||
"ast_hash": "241955e5db053887071651badc2439a3",
|
||||
"mtime": 1785484060.3738606,
|
||||
"ast_hash": "6f86ca59f933801a13801d989b1cbd11",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/pl/freight/components/UnsettledPlanPanel.vue": {
|
||||
"mtime": 1785483535.9482553,
|
||||
"ast_hash": "3b134dfc3a3bdb290d74ea0beda837e3",
|
||||
"mtime": 1785484060.371517,
|
||||
"ast_hash": "78340302e1f2103704e8abf5af8d1e4e",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/pl/freight/freight-status.ts": {
|
||||
@@ -3555,13 +3565,13 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/pl/inventory/document-reminder.vue": {
|
||||
"mtime": 1785483535.9487271,
|
||||
"ast_hash": "a4b46ce2f3b2e6228683ee19f4005e60",
|
||||
"mtime": 1785484060.376436,
|
||||
"ast_hash": "bc9b54cf026c33eefdf8daa852deca52",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/pl/inventory/ledger.vue": {
|
||||
"mtime": 1785483535.9499671,
|
||||
"ast_hash": "9349baac709ec79c6facf2d73f19291d",
|
||||
"mtime": 1785484060.376128,
|
||||
"ast_hash": "12123788d6253c5917f43e9fc653d5c0",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/pl/outbound/index.vue": {
|
||||
@@ -3570,18 +3580,18 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/pl/personnel/index.vue": {
|
||||
"mtime": 1785483535.9510326,
|
||||
"ast_hash": "66c3964517549bd600852fd18039e723",
|
||||
"mtime": 1785484060.3787086,
|
||||
"ast_hash": "4147f6500602567ff0304731d7212080",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/pl/plan-archive/index.vue": {
|
||||
"mtime": 1785483535.9529293,
|
||||
"ast_hash": "43c4e9d9c478aa4040c62143c91f73e3",
|
||||
"mtime": 1785484060.3771799,
|
||||
"ast_hash": "9affbba86635dadf3b88d3675d059807",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/pl/plan-cert/index.vue": {
|
||||
"mtime": 1785483535.953913,
|
||||
"ast_hash": "b56ea190eb80747aa12d3190f3f012a7",
|
||||
"mtime": 1785484025.1107006,
|
||||
"ast_hash": "24553244c8b667db6d12dd0f7adced42",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/pl/plan/arrange.vue": {
|
||||
@@ -3605,53 +3615,53 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/pl/self-vehicle/index.vue": {
|
||||
"mtime": 1785483535.954896,
|
||||
"ast_hash": "8f0a0c477477990fa66ff31ef5fddec0",
|
||||
"mtime": 1785484060.3775532,
|
||||
"ast_hash": "0fae9d03efc6d5c5eeea1182f5b3ba03",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/pl/transport-address/index.vue": {
|
||||
"mtime": 1785483535.9561405,
|
||||
"ast_hash": "7e9a435af5bc095d3f7887797c8492a5",
|
||||
"mtime": 1785484060.369104,
|
||||
"ast_hash": "6d411e4308c984f9964e062c398ab283",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/pl/transport/index.vue": {
|
||||
"mtime": 1785483535.9555125,
|
||||
"ast_hash": "aa69b39f31b0b5602f7b54d32f059ed6",
|
||||
"mtime": 1785484060.366137,
|
||||
"ast_hash": "39352b0c056b21afc3ac04e7e75fbdda",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/pl/vehicle-maintenance-apply/index.vue": {
|
||||
"mtime": 1785483535.9591458,
|
||||
"ast_hash": "a1add271787fd5012c6e5e2e4ec23c9f",
|
||||
"mtime": 1785484060.3696702,
|
||||
"ast_hash": "44b69301a9013b638d5195a47e6e0956",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/pl/vehicle-maintenance-process/index.vue": {
|
||||
"mtime": 1785483535.960391,
|
||||
"ast_hash": "f1f4e9a496d48990f3aeee61ab211328",
|
||||
"mtime": 1785484060.3792765,
|
||||
"ast_hash": "15cdb46461f562a8912800dc56cdaa05",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/pl/vehicle-recharge-apply/index.vue": {
|
||||
"mtime": 1785483535.9612,
|
||||
"ast_hash": "7d27f00d0fbd66d2ca0b41dbc316915c",
|
||||
"mtime": 1785484060.3701077,
|
||||
"ast_hash": "4a0c94ebf39a5b8df21d0c455b2f741d",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/pl/vehicle-recharge-record/index.vue": {
|
||||
"mtime": 1785483535.9624603,
|
||||
"ast_hash": "74d17422d3bf0db02c6a3ab3adfd6ea4",
|
||||
"mtime": 1785484060.3671885,
|
||||
"ast_hash": "6220d779e9310bab18e10f492d50241a",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/pl/vehicle-urea-apply/index.vue": {
|
||||
"mtime": 1785483535.9632154,
|
||||
"ast_hash": "e5f2ca53728905e8f6d0792f21014db7",
|
||||
"mtime": 1785484060.3757749,
|
||||
"ast_hash": "07c9581a1093c830a8fa3c8c45baf840",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/pl/vehicle-urea-disburse/index.vue": {
|
||||
"mtime": 1785483535.9641247,
|
||||
"ast_hash": "a6f3c7b8c371812697fc9ac0fef7e985",
|
||||
"mtime": 1785484060.3752391,
|
||||
"ast_hash": "515c0729c1468c617aeeb79ac5317929",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/pl/vehicle/index.vue": {
|
||||
"mtime": 1785483535.957107,
|
||||
"ast_hash": "193a7a1202071d27b321c94d188ba1c3",
|
||||
"mtime": 1785484060.3667428,
|
||||
"ast_hash": "d2fde8d1efcfe3f281404c6788a83133",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/public/db/inventory/index.vue": {
|
||||
@@ -3685,38 +3695,38 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/system/dept/index.vue": {
|
||||
"mtime": 1785461673.495151,
|
||||
"ast_hash": "815bb5b0b1c034dd7393cecd9ee2c3de",
|
||||
"mtime": 1785483975.8205566,
|
||||
"ast_hash": "d47201d0902fd7eb430f344c6a3655d6",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/system/dict/index.vue": {
|
||||
"mtime": 1785483535.966199,
|
||||
"ast_hash": "e55da3ba6440ba65ff2be859f78b2416",
|
||||
"mtime": 1785484060.420505,
|
||||
"ast_hash": "9f9974a0103e1a897e3f581f063d3cac",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/system/leave/index.vue": {
|
||||
"mtime": 1785483535.9673738,
|
||||
"ast_hash": "50c4e3168dc0b0a3a314dc28fca2205f",
|
||||
"mtime": 1785484060.4208162,
|
||||
"ast_hash": "9279f20bf4a22d57e7ff00002a53152b",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/system/menu/index.vue": {
|
||||
"mtime": 1785461665.3474762,
|
||||
"ast_hash": "3094b652c30c46120be5215f5cc0589a",
|
||||
"mtime": 1785483981.6276739,
|
||||
"ast_hash": "5104151aad91f76bffe609cadad4cad3",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/system/operlog/index.vue": {
|
||||
"mtime": 1785483535.970796,
|
||||
"ast_hash": "6af078dcf6da44d7e22528fce7a3fe3b",
|
||||
"mtime": 1785484060.4136975,
|
||||
"ast_hash": "ac420d9b12de387e47dee582b76d4ac2",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/system/post/index.vue": {
|
||||
"mtime": 1785483535.972269,
|
||||
"ast_hash": "8632267b6e9a1f1fc4d9d149da726e26",
|
||||
"mtime": 1785484060.413259,
|
||||
"ast_hash": "67a5ea5778d3d3e5d431f6e66822b4d1",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/system/role/index.vue": {
|
||||
"mtime": 1785483535.9735475,
|
||||
"ast_hash": "0439c58085d7b498c19e25d50099cb21",
|
||||
"mtime": 1785484060.4129443,
|
||||
"ast_hash": "b84d43f8c366da8f5d6c9a7e7afeec7f",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/system/site/index.vue": {
|
||||
@@ -3725,8 +3735,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/system/tenant/index.vue": {
|
||||
"mtime": 1785483535.976328,
|
||||
"ast_hash": "13e243a63dc4746f328dc771ff61ecce",
|
||||
"mtime": 1785484060.4125135,
|
||||
"ast_hash": "0261e3cf831e5b31a3391a90ea2fe0e0",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/system/user/detail.vue": {
|
||||
@@ -3735,8 +3745,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/system/user/index.vue": {
|
||||
"mtime": 1785483535.9774203,
|
||||
"ast_hash": "a77e7f8225d57086b72cedce5d03496a",
|
||||
"mtime": 1785484060.4144728,
|
||||
"ast_hash": "bf061398bb3bcc85a18f7c6b536ba079",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/system/user/modules/user-dialog.vue": {
|
||||
@@ -3750,8 +3760,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/system/workflow/components/ApprovalDialog.vue": {
|
||||
"mtime": 1782858542.0,
|
||||
"ast_hash": "c9add9d76a1766f53a55a841f6a10d26",
|
||||
"mtime": 1785554129.2843409,
|
||||
"ast_hash": "30bf5da8a4a8f1e00d9ee7baa8f2c8b8",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/system/workflow/components/ProcessStatusTag.vue": {
|
||||
@@ -3770,8 +3780,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/system/workflow/process/index.vue": {
|
||||
"mtime": 1785483535.9780533,
|
||||
"ast_hash": "1a382c91a6f9daa04a9338515b81fd32",
|
||||
"mtime": 1785484060.418166,
|
||||
"ast_hash": "fd97d5ddbd202b8af186d7132b52d160",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/system/workflow/task-completed/index.vue": {
|
||||
@@ -3780,8 +3790,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/system/workflow/task-pending/index.vue": {
|
||||
"mtime": 1785483535.978659,
|
||||
"ast_hash": "dfac6062ed00f7fd70dce5d5d8ac2df2",
|
||||
"mtime": 1785484060.4169238,
|
||||
"ast_hash": "844209c5a7a72faa59fd9da93cb17689",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"tsconfig.json": {
|
||||
@@ -5163,5 +5173,10 @@
|
||||
"mtime": 1780108931.0,
|
||||
"ast_hash": "839d61e0376bb89e1c2bd458ecb2051b",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/views/crm/contract/approver-setting/index.vue": {
|
||||
"mtime": 1785562205.1300278,
|
||||
"ast_hash": "5029bcf7da48be7e164662d243b160c1",
|
||||
"semantic_hash": ""
|
||||
}
|
||||
}
|
||||
@@ -369,7 +369,7 @@
|
||||
"367": "Community 367",
|
||||
"368": "Community 368",
|
||||
"369": "Community 369",
|
||||
"370": "canPreview",
|
||||
"370": "loader.ts",
|
||||
"371": "fetchData",
|
||||
"372": "detail.vue",
|
||||
"373": "Community 373",
|
||||
@@ -377,12 +377,12 @@
|
||||
"375": "Community 375",
|
||||
"376": "index.vue",
|
||||
"377": "Community 377",
|
||||
"378": "Community 378",
|
||||
"378": "index.vue",
|
||||
"379": "Community 379",
|
||||
"380": "Community 380",
|
||||
"381": "Community 381",
|
||||
"382": "Community 382",
|
||||
"383": "index.vue",
|
||||
"383": "ApproverSelector.vue",
|
||||
"384": "Community 384",
|
||||
"385": "Community 385",
|
||||
"386": "navigation.ts",
|
||||
@@ -400,7 +400,7 @@
|
||||
"398": "Community 398",
|
||||
"399": "Community 399",
|
||||
"400": "Community 400",
|
||||
"401": "index.vue",
|
||||
"401": "fetchPriceLetterDetail",
|
||||
"402": "Community 402",
|
||||
"403": "Community 403",
|
||||
"404": "Community 404",
|
||||
@@ -447,29 +447,29 @@
|
||||
"445": "Community 445",
|
||||
"446": "Community 446",
|
||||
"447": "crm-product-display.ts",
|
||||
"448": "loadUserOptions",
|
||||
"448": "theme.min.js",
|
||||
"449": "Community 449",
|
||||
"450": "Community 450",
|
||||
"451": "Community 451",
|
||||
"452": "modal.ts",
|
||||
"453": "SettingActions.vue",
|
||||
"454": "fetchData",
|
||||
"455": "removeProductsNotAllowedByHazard",
|
||||
"452": "handleSearch",
|
||||
"453": "emit",
|
||||
"454": "zh_CN.js",
|
||||
"455": "zh_CN.js",
|
||||
"456": "Community 456",
|
||||
"457": "colors.ts",
|
||||
"457": "index.vue",
|
||||
"458": "Community 458",
|
||||
"459": "Community 459",
|
||||
"460": "Community 460",
|
||||
"461": "handleConfirmApproval",
|
||||
"461": "modal.ts",
|
||||
"462": "Community 462",
|
||||
"463": "db.ts",
|
||||
"464": "canPreview",
|
||||
"463": "error-handle.ts",
|
||||
"464": "zh_CN.js",
|
||||
"465": "index.vue",
|
||||
"466": "SettingDrawer.vue",
|
||||
"466": "v2.4.2.1 - 2025-06-16",
|
||||
"467": "Community 467",
|
||||
"468": "Community 468",
|
||||
"469": "Community 469",
|
||||
"470": "index.vue",
|
||||
"470": "zh_CN.js",
|
||||
"471": "Community 471",
|
||||
"472": "Community 472",
|
||||
"473": "Community 473",
|
||||
@@ -477,13 +477,14 @@
|
||||
"475": "Community 475",
|
||||
"476": "Community 476",
|
||||
"477": "Community 477",
|
||||
"478": "zh_CN.js",
|
||||
"478": "customerPool.ts",
|
||||
"479": "Community 479",
|
||||
"480": "Community 480",
|
||||
"481": "Community 481",
|
||||
"482": "zh_CN.js",
|
||||
"483": "detail.vue",
|
||||
"482": "detail.vue",
|
||||
"483": "emit",
|
||||
"484": "Community 484",
|
||||
"485": "zh_CN.js",
|
||||
"486": "Community 486",
|
||||
"487": "Community 487",
|
||||
"488": "Community 488",
|
||||
@@ -491,6 +492,7 @@
|
||||
"490": "Community 490",
|
||||
"491": "Community 491",
|
||||
"492": "Community 492",
|
||||
"493": "price-letter-template.ts",
|
||||
"494": "Community 494",
|
||||
"496": "Community 496",
|
||||
"498": "Community 498",
|
||||
@@ -630,7 +632,6 @@
|
||||
"670": "Community 670",
|
||||
"671": "Community 671",
|
||||
"672": "Community 672",
|
||||
"674": "Community 674",
|
||||
"678": "Community 678",
|
||||
"680": "Community 680",
|
||||
"681": "Community 681",
|
||||
@@ -877,7 +878,6 @@
|
||||
"1016": "TransferDialog.vue",
|
||||
"1017": "BatchUpdateDateDialog.vue",
|
||||
"1027": "IntoPoolDialog.vue",
|
||||
"1036": "customerPool.ts",
|
||||
"1042": "index.js",
|
||||
"1043": "skin.shadowdom.js",
|
||||
"1044": "content.inline.js",
|
||||
|
||||
@@ -1 +1 @@
|
||||
.
|
||||
/Users/wuzilong/work/00code/shanxia2.0/web-admin
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+5941
-3447
File diff suppressed because it is too large.
Load diff
@@ -283,7 +283,8 @@ export async function getQuoteApproverSetting() {
|
||||
data: {
|
||||
id: data.id,
|
||||
approverIds: (data.approverIds || []).map(String),
|
||||
status: data.status ?? '1',
|
||||
// 无 status 时默认启用;停用须显式为 '1'
|
||||
status: data.status === '1' ? '1' : '0',
|
||||
updateTime: data.updateTime
|
||||
} as QuoteApproverSettingVO
|
||||
}
|
||||
|
||||
@@ -264,11 +264,17 @@ function handleClose() {
|
||||
resetForm()
|
||||
}
|
||||
|
||||
function defaultComment(type: typeof actionType.value) {
|
||||
if (type === 'approve') return '审批通过'
|
||||
if (type === 'reject') return '审批驳回'
|
||||
return ''
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
form.value = {
|
||||
approvalType: 'sequential',
|
||||
approverIds: [],
|
||||
comment: '',
|
||||
comment: defaultComment(actionType.value),
|
||||
remark: ''
|
||||
}
|
||||
formRef.value?.clearValidate()
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RouterView v-if="isRefresh" v-slot="{ Component, route }" :style="contentStyle">
|
||||
<!-- 勿强制 minHeight:calc(100vh-…),否则门户布局会叠出多余纵向滚动条 -->
|
||||
<RouterView v-if="isRefresh" v-slot="{ Component, route }">
|
||||
<!-- 缓存路由动画 -->
|
||||
<Transition :name="showTransitionMask ? '' : actualTransition" mode="out-in" appear>
|
||||
<KeepAlive :max="keepAliveMax" :exclude="keepAliveExclude">
|
||||
@@ -58,7 +59,8 @@
|
||||
defineOptions({ name: 'ArtPageContent' })
|
||||
|
||||
const route = useRoute()
|
||||
const { containerMinHeight } = useAutoLayoutHeight()
|
||||
// 仅同步 --art-full-height;不给业务页强制 minHeight
|
||||
useAutoLayoutHeight()
|
||||
const { pageTransition, containerWidth, refresh, showWorkTab } = storeToRefs(useSettingStore())
|
||||
const { keepAliveExclude } = storeToRefs(useWorktabStore())
|
||||
|
||||
@@ -114,12 +116,6 @@
|
||||
}
|
||||
)
|
||||
|
||||
const contentStyle = computed(
|
||||
(): CSSProperties => ({
|
||||
minHeight: containerMinHeight.value
|
||||
})
|
||||
)
|
||||
|
||||
const reload = () => {
|
||||
isRefresh.value = false
|
||||
nextTick(() => {
|
||||
|
||||
@@ -1,71 +1,47 @@
|
||||
<!-- 一体化门户顶栏 -->
|
||||
<!-- 一体化门户顶栏:一级横排 + 大面板菜单(方案 A) -->
|
||||
<template>
|
||||
<header class="portal-header">
|
||||
<div class="portal-header-start">
|
||||
<div class="portal-header-brand" @click="toHome">
|
||||
<ArtLogo class="portal-header-logo" src="/logo-sx.svg" :size="28" />
|
||||
<p class="portal-header-title">{{ AppConfig.systemInfo.name }}</p>
|
||||
</div>
|
||||
<div class="portal-header-brand" @click="toHome">
|
||||
<ArtLogo class="portal-header-logo" src="/logo-sx.svg" :size="28" />
|
||||
<p class="portal-header-title">{{ AppConfig.systemInfo.name }}</p>
|
||||
</div>
|
||||
|
||||
<div class="portal-header-nav">
|
||||
<div class="portal-header-nav">
|
||||
<ArtPortalMegaMenu />
|
||||
</div>
|
||||
|
||||
<div class="portal-header-end">
|
||||
<div class="portal-header-actions">
|
||||
<ArtIconButton
|
||||
icon="fold"
|
||||
class="portal-header-icon-btn portal-header-fold-btn"
|
||||
:class="{ 'is-menu-collapsed': !menuOpen }"
|
||||
@click="toggleSidebar"
|
||||
v-if="shouldShowFullscreen"
|
||||
icon="fullscreen"
|
||||
:class="[
|
||||
'portal-header-icon-btn max-md:!hidden',
|
||||
!isFullscreen ? 'full-screen-btn' : 'exit-full-screen-btn'
|
||||
]"
|
||||
@click="toggleFullScreen"
|
||||
/>
|
||||
<ArtPortalModuleSelect />
|
||||
<ArtUserMenu />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="portal-header-center" aria-hidden="false">
|
||||
<p v-if="pageTitle" class="portal-header-page-title">{{ pageTitle }}</p>
|
||||
</div>
|
||||
|
||||
<div class="portal-header-actions">
|
||||
<ArtIconButton
|
||||
v-if="shouldShowFullscreen"
|
||||
icon="fullscreen"
|
||||
:class="[
|
||||
'portal-header-icon-btn max-md:!hidden',
|
||||
!isFullscreen ? 'full-screen-btn' : 'exit-full-screen-btn'
|
||||
]"
|
||||
@click="toggleFullScreen"
|
||||
/>
|
||||
<ArtUserMenu />
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useFullscreen } from '@vueuse/core'
|
||||
import AppConfig from '@/config'
|
||||
import { useCommon } from '@/hooks/core/useCommon'
|
||||
import { useHeaderBar } from '@/hooks/core/useHeaderBar'
|
||||
import { useSettingStore } from '@/store/modules/setting'
|
||||
import { formatMenuTitle } from '@/utils/router'
|
||||
import ArtUserMenu from '../art-header-bar/widget/ArtUserMenu.vue'
|
||||
import ArtPortalMegaMenu from '../art-portal-mega-menu/index.vue'
|
||||
|
||||
defineOptions({ name: 'ArtPortalHeader' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { homePath } = useCommon()
|
||||
const { shouldShowFullscreen } = useHeaderBar()
|
||||
const { isFullscreen, toggle: toggleFullscreen } = useFullscreen()
|
||||
const settingStore = useSettingStore()
|
||||
const { menuOpen } = storeToRefs(settingStore)
|
||||
|
||||
const pageTitle = computed(() => {
|
||||
const matched = route.matched
|
||||
if (!matched.length) return ''
|
||||
const current = matched[matched.length - 1]
|
||||
const title = current.meta?.title
|
||||
return title ? formatMenuTitle(String(title)) : ''
|
||||
})
|
||||
|
||||
const toHome = (): void => {
|
||||
router.push(homePath.value)
|
||||
@@ -74,10 +50,6 @@
|
||||
const toggleFullScreen = (): void => {
|
||||
toggleFullscreen()
|
||||
}
|
||||
|
||||
const toggleSidebar = (): void => {
|
||||
settingStore.setMenuOpen(!menuOpen.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -85,42 +57,27 @@
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
/* 左侧无 padding:brand 区与侧栏同宽,右边框才能纵向对齐 */
|
||||
padding: 0 var(--mint-content-padding, 24px) 0 0;
|
||||
user-select: none;
|
||||
background: var(--mint-primary, #1ca851);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.portal-header-start {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.portal-header-brand {
|
||||
box-sizing: border-box;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
/* +1px:与侧栏 #app-sidebar 外边框(内容宽 + 1px border)纵向对齐 */
|
||||
width: calc(var(--app-sidebar-open-width, 270px) + 1px);
|
||||
min-width: calc(var(--app-sidebar-open-width, 270px) + 1px);
|
||||
max-width: calc(var(--app-sidebar-open-width, 270px) + 1px);
|
||||
max-width: none;
|
||||
padding-left: var(--mint-content-padding, 24px);
|
||||
padding-right: 16px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.22);
|
||||
}
|
||||
|
||||
.portal-header-logo {
|
||||
@@ -128,11 +85,8 @@
|
||||
}
|
||||
|
||||
.portal-header-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
@@ -141,34 +95,27 @@
|
||||
}
|
||||
|
||||
.portal-header-nav {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.portal-header-center {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
z-index: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
max-width: min(520px, calc(100% - 420px));
|
||||
pointer-events: none;
|
||||
width: max-content;
|
||||
max-width: min(720px, calc(100% - 520px));
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.portal-header-page-title {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
.portal-header-end {
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.portal-header-actions {
|
||||
@@ -176,21 +123,6 @@
|
||||
flex-shrink: 0;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
:deep(.portal-module-select-trigger) {
|
||||
background: transparent;
|
||||
|
||||
.portal-module-select-trigger__icon,
|
||||
.portal-module-select-trigger__label,
|
||||
.portal-module-select-trigger__arrow {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&:hover:not(.is-disabled) {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.portal-header-icon-btn) {
|
||||
@@ -201,26 +133,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.portal-header-fold-btn :deep(.art-svg-icon) {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.portal-header-fold-btn.is-menu-collapsed :deep(.art-svg-icon) {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
@media only screen and (width <= 800px) {
|
||||
.portal-header-title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.portal-header-center {
|
||||
max-width: min(290px, calc(100% - 290px));
|
||||
}
|
||||
|
||||
.portal-header-page-title {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -0,0 +1,410 @@
|
||||
<!-- 门户顶栏:一级横排 + 方案 A 大面板(左右分行二级/三级文字链) -->
|
||||
<template>
|
||||
<div class="portal-mega-menu" ref="rootRef">
|
||||
<nav class="portal-mega-l1" aria-label="一级模块">
|
||||
<button
|
||||
v-for="item in moduleList"
|
||||
:key="item.path"
|
||||
type="button"
|
||||
class="portal-mega-l1__item"
|
||||
:class="{
|
||||
'is-selected': isL1Selected(item)
|
||||
}"
|
||||
@click="toggleModule(item)"
|
||||
>
|
||||
<span>{{ item.formattedTitle }}</span>
|
||||
<span class="portal-mega-l1__caret">{{ openModulePath === item.path ? '▴' : '▾' }}</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="openModule"
|
||||
class="portal-mega-overlay"
|
||||
@click="closePanel"
|
||||
/>
|
||||
<div
|
||||
v-if="openModule"
|
||||
class="portal-mega-panel"
|
||||
role="dialog"
|
||||
aria-label="子菜单"
|
||||
:style="panelStyle"
|
||||
>
|
||||
<div class="portal-mega-panel__body">
|
||||
<div class="portal-mega-split" :class="{ 'is-single': isSingleColumn }">
|
||||
<div class="portal-mega-pane">
|
||||
<div
|
||||
v-for="row in columns.left"
|
||||
:key="row.l2.path || row.l2.meta?.title"
|
||||
class="portal-mega-row"
|
||||
>
|
||||
<div class="portal-mega-row__l2">{{ formatMenuTitle(row.l2.meta.title) }}</div>
|
||||
<div class="portal-mega-row__l3">
|
||||
<button
|
||||
v-for="link in row.l3Links"
|
||||
:key="link.path || link.meta?.title"
|
||||
type="button"
|
||||
class="portal-mega-link"
|
||||
:class="{ 'is-active': isLinkActive(link) }"
|
||||
@click="handleLinkClick(link)"
|
||||
>
|
||||
{{ formatMenuTitle(link.meta.title) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="!columns.left.length" class="portal-mega-empty">暂无菜单</p>
|
||||
</div>
|
||||
<div v-if="!isSingleColumn" class="portal-mega-pane portal-mega-pane--right">
|
||||
<div
|
||||
v-for="row in columns.right"
|
||||
:key="row.l2.path || row.l2.meta?.title"
|
||||
class="portal-mega-row"
|
||||
>
|
||||
<div class="portal-mega-row__l2">{{ formatMenuTitle(row.l2.meta.title) }}</div>
|
||||
<div class="portal-mega-row__l3">
|
||||
<button
|
||||
v-for="link in row.l3Links"
|
||||
:key="link.path || link.meta?.title"
|
||||
type="button"
|
||||
class="portal-mega-link"
|
||||
:class="{ 'is-active': isLinkActive(link) }"
|
||||
@click="handleLinkClick(link)"
|
||||
>
|
||||
{{ formatMenuTitle(link.meta.title) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onClickOutside } from '@vueuse/core'
|
||||
import { formatMenuTitle } from '@/utils/router'
|
||||
import { handleMenuJump } from '@/utils/navigation'
|
||||
import { useMenuStore } from '@/store/modules/menu'
|
||||
import {
|
||||
buildMegaPanelColumns,
|
||||
filterPortalTopModules,
|
||||
isMegaPanelLinkActive,
|
||||
isPortalModuleActive,
|
||||
type PortalMegaPanelColumns
|
||||
} from '@/utils/router/portal-menu'
|
||||
import type { AppRouteRecord } from '@/types/router'
|
||||
import type { CSSProperties } from 'vue'
|
||||
|
||||
defineOptions({ name: 'ArtPortalMegaMenu' })
|
||||
|
||||
interface PortalModuleItem extends AppRouteRecord {
|
||||
isRouteActive: boolean
|
||||
formattedTitle: string
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const menuStore = useMenuStore()
|
||||
const { menuList } = storeToRefs(menuStore)
|
||||
|
||||
const rootRef = ref<HTMLElement | null>(null)
|
||||
const openModulePath = ref<string | null>(null)
|
||||
const panelStyle = ref<CSSProperties>({})
|
||||
|
||||
const currentRoutePath = computed(() => String(route.meta.activePath || route.path))
|
||||
|
||||
const moduleList = computed<PortalModuleItem[]>(() => {
|
||||
return filterPortalTopModules(menuList.value).map((item) => ({
|
||||
...item,
|
||||
isRouteActive: isPortalModuleActive(item, currentRoutePath.value),
|
||||
formattedTitle: formatMenuTitle(item.meta.title)
|
||||
}))
|
||||
})
|
||||
|
||||
const openModule = computed(() => {
|
||||
if (!openModulePath.value) return undefined
|
||||
return moduleList.value.find((item) => item.path === openModulePath.value)
|
||||
})
|
||||
|
||||
const columns = computed<PortalMegaPanelColumns>(() =>
|
||||
buildMegaPanelColumns(openModule.value)
|
||||
)
|
||||
|
||||
const isSingleColumn = computed(() => columns.value.right.length === 0)
|
||||
|
||||
/** 与顶栏中间槽上限一致;一级较少时保证两栏二级可读 */
|
||||
const PANEL_MIN_WIDTH = 720
|
||||
const PANEL_EDGE_PAD = 16
|
||||
|
||||
const syncPanelPosition = (): void => {
|
||||
const el = rootRef.value
|
||||
if (!el) return
|
||||
const rect = el.getBoundingClientRect()
|
||||
const viewportW = window.innerWidth
|
||||
const minWidth = Math.min(PANEL_MIN_WIDTH, Math.max(0, viewportW - PANEL_EDGE_PAD * 2))
|
||||
const width = Math.max(rect.width, minWidth)
|
||||
const centerX = rect.left + rect.width / 2
|
||||
let left = centerX - width / 2
|
||||
left = Math.max(PANEL_EDGE_PAD, Math.min(left, viewportW - width - PANEL_EDGE_PAD))
|
||||
panelStyle.value = {
|
||||
top: `${rect.bottom}px`,
|
||||
left: `${left}px`,
|
||||
width: `${width}px`
|
||||
}
|
||||
}
|
||||
|
||||
const closePanel = (): void => {
|
||||
openModulePath.value = null
|
||||
}
|
||||
|
||||
const toggleModule = async (item: PortalModuleItem): Promise<void> => {
|
||||
const path = String(item.path || '')
|
||||
if (!path) return
|
||||
openModulePath.value = openModulePath.value === path ? null : path
|
||||
if (openModulePath.value) {
|
||||
await nextTick()
|
||||
syncPanelPosition()
|
||||
}
|
||||
}
|
||||
|
||||
/** 顶栏同时只高亮一个一级:面板展开时跟展开项,否则跟当前路由 */
|
||||
const isL1Selected = (item: PortalModuleItem): boolean => {
|
||||
if (openModulePath.value) {
|
||||
return openModulePath.value === item.path
|
||||
}
|
||||
return item.isRouteActive
|
||||
}
|
||||
|
||||
const isLinkActive = (link: AppRouteRecord): boolean => {
|
||||
return isMegaPanelLinkActive(link, currentRoutePath.value)
|
||||
}
|
||||
|
||||
const handleLinkClick = (link: AppRouteRecord): void => {
|
||||
closePanel()
|
||||
handleMenuJump(link)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => {
|
||||
closePanel()
|
||||
}
|
||||
)
|
||||
|
||||
watch(openModulePath, async (path) => {
|
||||
if (!path) return
|
||||
await nextTick()
|
||||
syncPanelPosition()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', syncPanelPosition)
|
||||
window.addEventListener('scroll', syncPanelPosition, true)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', syncPanelPosition)
|
||||
window.removeEventListener('scroll', syncPanelPosition, true)
|
||||
})
|
||||
|
||||
onClickOutside(rootRef, (event) => {
|
||||
const target = event.target as HTMLElement | null
|
||||
if (target?.closest?.('.portal-mega-panel')) {
|
||||
return
|
||||
}
|
||||
closePanel()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.portal-mega-menu {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.portal-mega-l1 {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 2px;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
max-height: 100%;
|
||||
/* 仅允许横向溢出;若只写 overflow-x:auto,overflow-y 会被算成 auto,易出现竖向滚动条 */
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.portal-mega-l1__item {
|
||||
box-sizing: border-box;
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
max-height: 56px;
|
||||
padding: 0 14px;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 4px solid transparent;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
color 0.15s ease,
|
||||
border-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.portal-mega-l1__caret {
|
||||
font-size: 10px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.portal-mega-overlay {
|
||||
position: fixed;
|
||||
inset: 56px 0 0 0;
|
||||
z-index: 190;
|
||||
background: rgba(15, 23, 42, 0.25);
|
||||
}
|
||||
|
||||
.portal-mega-panel {
|
||||
position: fixed;
|
||||
z-index: 200;
|
||||
box-sizing: border-box;
|
||||
max-height: min(720px, calc(100vh - 72px));
|
||||
overflow: auto;
|
||||
background: #fff;
|
||||
border: 1px solid var(--mint-border, #e2e8f0);
|
||||
border-top: none;
|
||||
border-radius: 0 0 10px 10px;
|
||||
box-shadow: 0 14px 32px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
|
||||
.portal-mega-panel__body {
|
||||
padding: 12px 16px 18px;
|
||||
}
|
||||
|
||||
.portal-mega-split {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0 16px;
|
||||
align-items: stretch;
|
||||
|
||||
&.is-single {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.portal-mega-pane {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.portal-mega-pane--right {
|
||||
padding-left: 16px;
|
||||
border-left: 1px solid var(--mint-border, #e2e8f0);
|
||||
}
|
||||
|
||||
.portal-mega-row {
|
||||
display: grid;
|
||||
grid-template-columns: 110px 1fr;
|
||||
gap: 8px 14px;
|
||||
align-items: start;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.portal-mega-row__l2 {
|
||||
padding-top: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.35;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.portal-mega-row__l3 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
gap: 4px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.portal-mega-link {
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 4px 0;
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1.45;
|
||||
color: #0f172a;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
transition: color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary-dark, #059669);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
font-weight: 600;
|
||||
color: var(--primary-dark, #059669);
|
||||
}
|
||||
}
|
||||
|
||||
.portal-mega-empty {
|
||||
margin: 12px 0;
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
@media only screen and (width <= 900px) {
|
||||
.portal-mega-split {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.portal-mega-pane--right {
|
||||
padding-left: 0;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.portal-mega-row {
|
||||
grid-template-columns: 96px 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,29 +1,24 @@
|
||||
/**
|
||||
* useLayoutHeight - 页面布局高度管理
|
||||
*
|
||||
* 自动计算和管理页面内容区域的高度,确保内容区域能够正确填充剩余空间。
|
||||
* 监听头部元素高度变化,动态调整内容区域高度,避免出现滚动条或布局错乱。
|
||||
*
|
||||
* ## 主要功能
|
||||
*
|
||||
* 1. 动态高度计算 - 根据头部元素高度自动计算内容区域高度
|
||||
* 2. 响应式监听 - 自动监听元素尺寸变化并更新高度
|
||||
* 3. CSS 变量同步 - 自动更新 CSS 变量,方便全局使用
|
||||
* 4. 灵活配置 - 支持自定义间距、CSS 变量名等
|
||||
* 5. 自动查找模式 - 提供通过 ID 自动查找元素的便捷方式
|
||||
* 以实际内容滚动容器 `#app-content` 为高度来源,同步 `--art-full-height`。
|
||||
* 页面已在该容器内滚动,禁止再用 `calc(100vh - 顶栏)` 给业务页强制 minHeight,
|
||||
* 否则门户布局下(顶栏为 `#app-portal-header`)会量不到顶栏,叠出多余纵向滚动条与底部空白。
|
||||
*
|
||||
* @module useLayoutHeight
|
||||
* @author Art Design Pro Team
|
||||
*/
|
||||
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { ref, computed, watch, onMounted, type Ref } from 'vue'
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
|
||||
/**
|
||||
* 页面容器高度配置
|
||||
*/
|
||||
interface LayoutHeightOptions {
|
||||
/** 额外的间距(默认 15px) */
|
||||
/**
|
||||
* @deprecated 已不再用于 100vh 扣减;保留参数以免调用方类型报错
|
||||
*/
|
||||
extraSpacing?: number
|
||||
/** 是否自动更新 CSS 变量(默认 true) */
|
||||
updateCssVar?: boolean
|
||||
@@ -31,26 +26,21 @@ interface LayoutHeightOptions {
|
||||
cssVarName?: string
|
||||
}
|
||||
|
||||
export function useLayoutHeight(options: LayoutHeightOptions = {}) {
|
||||
const { extraSpacing = 15, updateCssVar = true, cssVarName = '--art-full-height' } = options
|
||||
function syncFullHeightCssVar(
|
||||
availableHeight: Ref<number>,
|
||||
contentHeaderHeight: Ref<number>,
|
||||
options: LayoutHeightOptions
|
||||
) {
|
||||
const { updateCssVar = true, cssVarName = '--art-full-height' } = options
|
||||
|
||||
// 元素引用
|
||||
const headerRef = ref<HTMLElement>()
|
||||
const contentHeaderRef = ref<HTMLElement>()
|
||||
|
||||
// 使用 VueUse 自动监听元素尺寸变化
|
||||
const { height: headerHeight } = useElementSize(headerRef)
|
||||
const { height: contentHeaderHeight } = useElementSize(contentHeaderRef)
|
||||
|
||||
// 计算容器最小高度(响应式)
|
||||
const containerMinHeight = computed(() => {
|
||||
const totalHeight = headerHeight.value + contentHeaderHeight.value + extraSpacing
|
||||
return `calc(100vh - ${totalHeight}px)`
|
||||
const fullHeight = computed(() => {
|
||||
const available = Math.max(0, Math.round(availableHeight.value - contentHeaderHeight.value))
|
||||
return available > 0 ? `${available}px` : '100%'
|
||||
})
|
||||
|
||||
if (updateCssVar) {
|
||||
watch(
|
||||
containerMinHeight,
|
||||
fullHeight,
|
||||
(newHeight) => {
|
||||
requestAnimationFrame(() => {
|
||||
document.documentElement.style.setProperty(cssVarName, newHeight)
|
||||
@@ -60,89 +50,63 @@ export function useLayoutHeight(options: LayoutHeightOptions = {}) {
|
||||
)
|
||||
}
|
||||
|
||||
return fullHeight
|
||||
}
|
||||
|
||||
function resolveHeaderEl(): HTMLElement | undefined {
|
||||
if (typeof document === 'undefined') return undefined
|
||||
return (
|
||||
document.getElementById('app-portal-header') ??
|
||||
document.getElementById('app-header') ??
|
||||
undefined
|
||||
)
|
||||
}
|
||||
|
||||
export function useLayoutHeight(options: LayoutHeightOptions = {}) {
|
||||
const appContentRef = ref<HTMLElement>()
|
||||
const headerRef = ref<HTMLElement>()
|
||||
const contentHeaderRef = ref<HTMLElement>()
|
||||
|
||||
const { height: appContentHeight } = useElementSize(appContentRef)
|
||||
const { height: headerHeight } = useElementSize(headerRef)
|
||||
const { height: contentHeaderHeight } = useElementSize(contentHeaderRef)
|
||||
|
||||
/** @deprecated 请勿再用于业务页 minHeight;始终为空字符串 */
|
||||
const containerMinHeight = computed(() => '' as string)
|
||||
|
||||
syncFullHeightCssVar(appContentHeight, contentHeaderHeight, options)
|
||||
|
||||
onMounted(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (typeof document === 'undefined') return
|
||||
appContentRef.value = document.getElementById('app-content') ?? undefined
|
||||
contentHeaderRef.value = document.getElementById('app-content-header') ?? undefined
|
||||
if (!headerRef.value) {
|
||||
headerRef.value = resolveHeaderEl()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
/** 容器最小高度(响应式) */
|
||||
containerMinHeight,
|
||||
/** 头部元素引用 */
|
||||
headerRef,
|
||||
/** 内容头部元素引用 */
|
||||
contentHeaderRef,
|
||||
/** 头部高度(响应式) */
|
||||
headerHeight,
|
||||
/** 内容头部高度(响应式) */
|
||||
contentHeaderHeight
|
||||
contentHeaderHeight,
|
||||
appContentHeight
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 ID 自动查找元素的布局高度管理
|
||||
* 适用于无法直接获取元素引用的场景
|
||||
* 通过 ID 自动查找内容区,同步全高 CSS 变量。
|
||||
* 默认不强制业务页 minHeight(由 ArtPageContent 决定)。
|
||||
*
|
||||
* @param headerIds 头部元素的 ID 数组
|
||||
* @param _headerIds 兼容旧签名,已忽略(改为测量 `#app-content`)
|
||||
* @param options 配置选项
|
||||
*
|
||||
* ```
|
||||
*/
|
||||
export function useAutoLayoutHeight(
|
||||
headerIds: string[] = ['app-header', 'app-content-header'],
|
||||
_headerIds: string[] = ['app-header', 'app-content-header'],
|
||||
options: LayoutHeightOptions = {}
|
||||
) {
|
||||
const { extraSpacing = 15, updateCssVar = true, cssVarName = '--art-full-height' } = options
|
||||
|
||||
// 创建元素引用
|
||||
const headerRef = ref<HTMLElement>()
|
||||
const contentHeaderRef = ref<HTMLElement>()
|
||||
|
||||
// 使用 VueUse 自动监听元素尺寸变化
|
||||
const { height: headerHeight } = useElementSize(headerRef)
|
||||
const { height: contentHeaderHeight } = useElementSize(contentHeaderRef)
|
||||
|
||||
// 计算容器最小高度(响应式)
|
||||
const containerMinHeight = computed(() => {
|
||||
const totalHeight = headerHeight.value + contentHeaderHeight.value + extraSpacing
|
||||
return `calc(100vh - ${totalHeight}px)`
|
||||
})
|
||||
|
||||
if (updateCssVar) {
|
||||
watch(
|
||||
containerMinHeight,
|
||||
(newHeight) => {
|
||||
requestAnimationFrame(() => {
|
||||
document.documentElement.style.setProperty(cssVarName, newHeight)
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
}
|
||||
|
||||
// 在 DOM 挂载后查找元素
|
||||
onMounted(() => {
|
||||
if (typeof document !== 'undefined') {
|
||||
// 使用 nextTick 确保 DOM 完全渲染
|
||||
requestAnimationFrame(() => {
|
||||
const header = document.getElementById(headerIds[0])
|
||||
const contentHeader = document.getElementById(headerIds[1])
|
||||
|
||||
if (header) {
|
||||
headerRef.value = header
|
||||
}
|
||||
if (contentHeader) {
|
||||
contentHeaderRef.value = contentHeader
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
/** 容器最小高度(响应式) */
|
||||
containerMinHeight,
|
||||
/** 头部元素引用 */
|
||||
headerRef,
|
||||
/** 内容头部元素引用 */
|
||||
contentHeaderRef,
|
||||
/** 头部高度(响应式) */
|
||||
headerHeight,
|
||||
/** 内容头部高度(响应式) */
|
||||
contentHeaderHeight
|
||||
}
|
||||
return useLayoutHeight(options)
|
||||
}
|
||||
@@ -1,6 +1,22 @@
|
||||
import type { AppRouteRecord } from '@/types/router'
|
||||
import { getFirstMenuPath, getFirstSecondLevelMenuPath, isIframe } from '@/utils/navigation/route'
|
||||
import { filterSidebarRoutes } from '@/utils/router/sidebar-menu-filter'
|
||||
import {
|
||||
filterSidebarRoutes,
|
||||
isSidebarNavigableRoute
|
||||
} from '@/utils/router/sidebar-menu-filter'
|
||||
|
||||
/** 大面板一行:二级标题 + 三级可点链接 */
|
||||
export interface PortalMegaPanelRow {
|
||||
/** 二级菜单(仅展示标题,不可点) */
|
||||
l2: AppRouteRecord
|
||||
/** 三级链接(含虚拟三级) */
|
||||
l3Links: AppRouteRecord[]
|
||||
}
|
||||
|
||||
export interface PortalMegaPanelColumns {
|
||||
left: PortalMegaPanelRow[]
|
||||
right: PortalMegaPanelRow[]
|
||||
}
|
||||
|
||||
/** 门户顶栏一级模块展示顺序(与路由注册顺序无关) */
|
||||
export const PORTAL_TOP_MODULE_ORDER = [
|
||||
@@ -179,3 +195,88 @@ export function getPortalDefaultPathForRoute(
|
||||
|
||||
return getFirstAccessiblePath(module)
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析二级下的三级可点链接。
|
||||
* 无子节点且二级自身可导航时,生成与二级同名的虚拟三级(路由继承二级)。
|
||||
*/
|
||||
export function resolveMegaPanelL3Links(l2: AppRouteRecord): AppRouteRecord[] {
|
||||
const children = filterSidebarRoutes(l2.children || [])
|
||||
if (!children.length) {
|
||||
if (!isSidebarNavigableRoute(l2)) {
|
||||
return []
|
||||
}
|
||||
return [
|
||||
{
|
||||
...l2,
|
||||
children: undefined,
|
||||
meta: {
|
||||
...l2.meta,
|
||||
title: l2.meta.title
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const links: AppRouteRecord[] = []
|
||||
for (const child of children) {
|
||||
if (isSidebarNavigableRoute(child)) {
|
||||
links.push(child)
|
||||
continue
|
||||
}
|
||||
const grandChildren = filterSidebarRoutes(child.children || [])
|
||||
for (const gc of grandChildren) {
|
||||
if (isSidebarNavigableRoute(gc)) {
|
||||
links.push(gc)
|
||||
}
|
||||
}
|
||||
}
|
||||
return links
|
||||
}
|
||||
|
||||
/** 将一级模块下的二三级整理为大面板左右两栏行数据 */
|
||||
export function buildMegaPanelColumns(
|
||||
l1Module: AppRouteRecord | undefined
|
||||
): PortalMegaPanelColumns {
|
||||
if (!l1Module) {
|
||||
return { left: [], right: [] }
|
||||
}
|
||||
|
||||
const l2List = filterSidebarRoutes(l1Module.children || [])
|
||||
const rows: PortalMegaPanelRow[] = []
|
||||
|
||||
for (const l2 of l2List) {
|
||||
const l3Links = resolveMegaPanelL3Links(l2)
|
||||
if (!l3Links.length) {
|
||||
continue
|
||||
}
|
||||
rows.push({ l2, l3Links })
|
||||
}
|
||||
|
||||
if (!rows.length) {
|
||||
return { left: [], right: [] }
|
||||
}
|
||||
|
||||
// 仅 1 个二级:单列展示,避免右侧空白
|
||||
if (rows.length === 1) {
|
||||
return { left: rows, right: [] }
|
||||
}
|
||||
|
||||
// 左右均分;超出建议可见行数时由面板整体纵向滚动
|
||||
// 例:4 → 2+2;7 → 4+3;10 → 5+5;12 → 6+6(不再 5+7)
|
||||
const leftCount = Math.ceil(rows.length / 2)
|
||||
return {
|
||||
left: rows.slice(0, leftCount),
|
||||
right: rows.slice(leftCount)
|
||||
}
|
||||
}
|
||||
|
||||
/** 判断三级链接是否为当前路由 */
|
||||
export function isMegaPanelLinkActive(item: AppRouteRecord, routePath: string): boolean {
|
||||
const activePath = String(routePath)
|
||||
const itemPath = String(item.meta?.activePath || item.path || '')
|
||||
if (!itemPath) {
|
||||
return false
|
||||
}
|
||||
return activePath === itemPath || activePath.startsWith(`${itemPath}/`)
|
||||
}
|
||||
@@ -684,15 +684,40 @@ async function handleToolbarSubmit() {
|
||||
ElMessage.warning('请先勾选要提交的申请')
|
||||
return
|
||||
}
|
||||
|
||||
const validTargets = selectedRows.value.filter(isDraftEditable)
|
||||
const skippedCount = selectedRows.value.length - validTargets.length
|
||||
|
||||
if (!validTargets.length) {
|
||||
ElMessage.warning('所选申请均不是草稿或已驳回状态,无法提交审批')
|
||||
return
|
||||
}
|
||||
|
||||
if (skippedCount > 0) {
|
||||
ElMessage.warning(
|
||||
`已跳过 ${skippedCount} 条不可提交的申请(仅草稿或已驳回可提交),将继续提交其余 ${validTargets.length} 条`
|
||||
)
|
||||
}
|
||||
|
||||
loadingMap.submit = true
|
||||
try {
|
||||
for (const row of selectedRows.value) {
|
||||
await advanceApi.submit(row.id)
|
||||
const results = await Promise.allSettled(validTargets.map((row) => advanceApi.submit(row.id)))
|
||||
const successCount = results.filter((item) => item.status === 'fulfilled').length
|
||||
const failedCount = results.length - successCount
|
||||
const skipText = skippedCount > 0 ? `,跳过 ${skippedCount} 条` : ''
|
||||
|
||||
if (successCount === 0) {
|
||||
ElMessage.error(`提交失败${skipText}`)
|
||||
return
|
||||
}
|
||||
if (failedCount > 0) {
|
||||
ElMessage.warning(`成功提交 ${successCount} 条,失败 ${failedCount} 条${skipText}`)
|
||||
} else if (successCount === 1 && skippedCount === 0) {
|
||||
ElMessage.success('提交成功')
|
||||
} else {
|
||||
ElMessage.success(`成功提交 ${successCount} 条${skipText}`)
|
||||
}
|
||||
ElMessage.success('提交成功')
|
||||
await refreshList()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '提交失败')
|
||||
} finally {
|
||||
loadingMap.submit = false
|
||||
}
|
||||
|
||||
@@ -420,6 +420,22 @@
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-alert
|
||||
v-if="approveRows.length > 1"
|
||||
type="success"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="batch-action-alert batch-action-alert--success"
|
||||
:title="`将${approveAction === 'approve' ? '审批通过' : '审批驳回'} ${approveRows.length} 条申请(使用同一审批意见)`"
|
||||
/>
|
||||
<el-alert
|
||||
v-if="approveSkippedCount > 0"
|
||||
type="error"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="batch-action-alert batch-action-alert--danger"
|
||||
:title="`已跳过 ${approveSkippedCount} 条不可审批的申请`"
|
||||
/>
|
||||
<el-form label-position="top" class="bx-dialog-form">
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="审批时间">
|
||||
@@ -570,8 +586,32 @@ function formatNow() {
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
}
|
||||
|
||||
const approveSkippedCount = ref(0)
|
||||
|
||||
function isApprovableRow(row?: any) {
|
||||
if (!row) return false
|
||||
return !row.status || row.status === 'pending'
|
||||
}
|
||||
|
||||
function openApproveDialog(rows: any[], action: 'approve' | 'reject') {
|
||||
approveRows.value = rows
|
||||
const validRows = (rows || []).filter(isApprovableRow)
|
||||
const skippedCount = (rows || []).length - validRows.length
|
||||
|
||||
if (!validRows.length) {
|
||||
ElMessage.warning(
|
||||
action === 'approve' ? '所选申请均不可审批通过' : '所选申请均不可驳回'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (skippedCount > 0) {
|
||||
ElMessage.warning(
|
||||
`已跳过 ${skippedCount} 条不可审批的申请,将继续处理其余 ${validRows.length} 条`
|
||||
)
|
||||
}
|
||||
|
||||
approveRows.value = validRows
|
||||
approveSkippedCount.value = skippedCount
|
||||
approveAction.value = action
|
||||
approvalOpinion.value = action === 'approve' ? '审批通过' : '审批驳回'
|
||||
approveTime.value = formatNow()
|
||||
@@ -742,22 +782,61 @@ const pendingSummaryMethod = amountSummary
|
||||
const approvedSummaryMethod = amountSummary
|
||||
|
||||
async function handleConfirmApprove() {
|
||||
if (!approveRows.value.length) {
|
||||
ElMessage.warning('没有可审批的申请')
|
||||
return
|
||||
}
|
||||
|
||||
const key = approveAction.value === 'approve' ? 'approvePass' : 'approveReject'
|
||||
loadingMap[key] = true
|
||||
const apiFn = approveAction.value === 'approve' ? advanceApi.approve : advanceApi.reject
|
||||
const msg = approveAction.value === 'approve' ? '审批通过成功' : '驳回成功'
|
||||
const actionLabel = approveAction.value === 'approve' ? '审批通过' : '审批驳回'
|
||||
|
||||
try {
|
||||
for (const row of approveRows.value) {
|
||||
await apiFn(row.id, { approvalOpinion: approvalOpinion.value })
|
||||
const results = await Promise.allSettled(
|
||||
approveRows.value.map((row) => apiFn(row.id, { approvalOpinion: approvalOpinion.value }))
|
||||
)
|
||||
const successCount = results.filter((item) => item.status === 'fulfilled').length
|
||||
const failedCount = results.length - successCount
|
||||
const skipText = approveSkippedCount.value > 0 ? `,跳过 ${approveSkippedCount.value} 条` : ''
|
||||
|
||||
if (successCount === 0) {
|
||||
ElMessage.error(`${actionLabel}失败${skipText}`)
|
||||
return
|
||||
}
|
||||
ElMessage.success(approveRows.value.length > 1 ? `批量${msg}` : msg)
|
||||
if (failedCount > 0) {
|
||||
ElMessage.warning(`成功${actionLabel} ${successCount} 条,失败 ${failedCount} 条${skipText}`)
|
||||
} else if (successCount === 1 && approveSkippedCount.value === 0) {
|
||||
ElMessage.success(`${actionLabel}成功`)
|
||||
} else {
|
||||
ElMessage.success(`成功${actionLabel} ${successCount} 条${skipText}`)
|
||||
}
|
||||
|
||||
dialogs.approve = false
|
||||
await refreshPending()
|
||||
await refreshMenuBadges(menuStore.menuList)
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '操作失败')
|
||||
} finally {
|
||||
loadingMap[key] = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.batch-action-alert {
|
||||
margin-bottom: 12px;
|
||||
|
||||
&--success {
|
||||
:deep(.el-alert__title),
|
||||
:deep(.el-alert__icon) {
|
||||
color: var(--primary, #10b981);
|
||||
}
|
||||
}
|
||||
|
||||
&--danger {
|
||||
:deep(.el-alert__title),
|
||||
:deep(.el-alert__icon) {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1107,13 +1107,22 @@ async function handleToolbarSubmit() {
|
||||
ElMessage.warning('请先勾选要提交的报销单')
|
||||
return
|
||||
}
|
||||
if (!selectedRows.value.every(isDraftEditable)) {
|
||||
ElMessage.warning('仅草稿或已驳回的报销单可提交')
|
||||
|
||||
const validTargets = selectedRows.value.filter(isDraftEditable)
|
||||
const skippedCount = selectedRows.value.length - validTargets.length
|
||||
|
||||
if (!validTargets.length) {
|
||||
ElMessage.warning('所选报销单均不是草稿或已驳回状态,无法提交审批')
|
||||
return
|
||||
}
|
||||
|
||||
if (skippedCount > 0) {
|
||||
ElMessage.warning(`已跳过 ${skippedCount} 条不可提交的报销单(仅草稿或已驳回可提交),将继续提交其余 ${validTargets.length} 条`)
|
||||
}
|
||||
|
||||
// 按申请人分别校验发票余额(代申请时用申请人额度)
|
||||
const needByApplicant = new Map<string, number>()
|
||||
for (const row of selectedRows.value) {
|
||||
for (const row of validTargets) {
|
||||
const aid = String(row.applicantId || currentUserId())
|
||||
needByApplicant.set(aid, (needByApplicant.get(aid) || 0) + Number(row.amount || 0))
|
||||
}
|
||||
@@ -1126,15 +1135,26 @@ async function handleToolbarSubmit() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
loadingMap.submit = true
|
||||
try {
|
||||
for (const row of selectedRows.value) {
|
||||
await reimburseApi.submit(row.id)
|
||||
const results = await Promise.allSettled(validTargets.map((row) => reimburseApi.submit(row.id)))
|
||||
const successCount = results.filter((item) => item.status === 'fulfilled').length
|
||||
const failedCount = results.length - successCount
|
||||
const skipText = skippedCount > 0 ? `,跳过 ${skippedCount} 条` : ''
|
||||
|
||||
if (successCount === 0) {
|
||||
ElMessage.error(`提交失败${skipText}`)
|
||||
return
|
||||
}
|
||||
if (failedCount > 0) {
|
||||
ElMessage.warning(`成功提交 ${successCount} 条,失败 ${failedCount} 条${skipText}`)
|
||||
} else if (successCount === 1 && skippedCount === 0) {
|
||||
ElMessage.success('提交成功')
|
||||
} else {
|
||||
ElMessage.success(`成功提交 ${successCount} 条${skipText}`)
|
||||
}
|
||||
ElMessage.success('提交成功')
|
||||
await refreshList()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '提交失败')
|
||||
} finally {
|
||||
loadingMap.submit = false
|
||||
}
|
||||
|
||||
@@ -551,6 +551,22 @@
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-alert
|
||||
v-if="approveRows.length > 1"
|
||||
type="success"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="batch-action-alert batch-action-alert--success"
|
||||
:title="`将${approveAction === 'approve' ? '审批通过' : '审批驳回'} ${approveRows.length} 条报销单(使用同一审批意见)`"
|
||||
/>
|
||||
<el-alert
|
||||
v-if="approveSkippedCount > 0"
|
||||
type="error"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="batch-action-alert batch-action-alert--danger"
|
||||
:title="`已跳过 ${approveSkippedCount} 条不可审批的报销单`"
|
||||
/>
|
||||
<el-form label-position="top" class="bx-dialog-form">
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="审批时间">
|
||||
@@ -741,8 +757,35 @@ function formatNow() {
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
}
|
||||
|
||||
const approveSkippedCount = ref(0)
|
||||
|
||||
function isApprovableRow(row?: any) {
|
||||
if (!row) return false
|
||||
// 待审批列表中的单据;若状态已变则不可再审
|
||||
return !row.status || row.status === 'pending'
|
||||
}
|
||||
|
||||
function openApproveDialog(rows: any[], action: 'approve' | 'reject') {
|
||||
approveRows.value = rows
|
||||
const validRows = (rows || []).filter(isApprovableRow)
|
||||
const skippedCount = (rows || []).length - validRows.length
|
||||
|
||||
if (!validRows.length) {
|
||||
ElMessage.warning(
|
||||
action === 'approve'
|
||||
? '所选报销单均不可审批通过'
|
||||
: '所选报销单均不可驳回'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (skippedCount > 0) {
|
||||
ElMessage.warning(
|
||||
`已跳过 ${skippedCount} 条不可审批的报销单,将继续处理其余 ${validRows.length} 条`
|
||||
)
|
||||
}
|
||||
|
||||
approveRows.value = validRows
|
||||
approveSkippedCount.value = skippedCount
|
||||
approveAction.value = action
|
||||
approvalOpinion.value = action === 'approve' ? '审批通过' : '审批驳回'
|
||||
approveTime.value = formatNow()
|
||||
@@ -943,20 +986,39 @@ async function openView(row: any) {
|
||||
}
|
||||
|
||||
async function handleConfirmApprove() {
|
||||
if (!approveRows.value.length) {
|
||||
ElMessage.warning('没有可审批的报销单')
|
||||
return
|
||||
}
|
||||
|
||||
const key = approveAction.value === 'approve' ? 'approvePass' : 'approveReject'
|
||||
loadingMap[key] = true
|
||||
const apiFn = approveAction.value === 'approve' ? reimburseApi.approve : reimburseApi.reject
|
||||
const msg = approveAction.value === 'approve' ? '审批通过成功' : '驳回成功'
|
||||
const actionLabel = approveAction.value === 'approve' ? '审批通过' : '审批驳回'
|
||||
|
||||
try {
|
||||
for (const row of approveRows.value) {
|
||||
await apiFn(row.id, { approvalOpinion: approvalOpinion.value })
|
||||
const results = await Promise.allSettled(
|
||||
approveRows.value.map((row) => apiFn(row.id, { approvalOpinion: approvalOpinion.value }))
|
||||
)
|
||||
const successCount = results.filter((item) => item.status === 'fulfilled').length
|
||||
const failedCount = results.length - successCount
|
||||
const skipText = approveSkippedCount.value > 0 ? `,跳过 ${approveSkippedCount.value} 条` : ''
|
||||
|
||||
if (successCount === 0) {
|
||||
ElMessage.error(`${actionLabel}失败${skipText}`)
|
||||
return
|
||||
}
|
||||
ElMessage.success(approveRows.value.length > 1 ? `批量${msg}` : msg)
|
||||
if (failedCount > 0) {
|
||||
ElMessage.warning(`成功${actionLabel} ${successCount} 条,失败 ${failedCount} 条${skipText}`)
|
||||
} else if (successCount === 1 && approveSkippedCount.value === 0) {
|
||||
ElMessage.success(`${actionLabel}成功`)
|
||||
} else {
|
||||
ElMessage.success(`成功${actionLabel} ${successCount} 条${skipText}`)
|
||||
}
|
||||
|
||||
dialogs.approve = false
|
||||
await refreshPending()
|
||||
await refreshMenuBadges(menuStore.menuList)
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '操作失败')
|
||||
} finally {
|
||||
loadingMap[key] = false
|
||||
}
|
||||
@@ -987,3 +1049,23 @@ function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
|
||||
return sums
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.batch-action-alert {
|
||||
margin-bottom: 12px;
|
||||
|
||||
&--success {
|
||||
:deep(.el-alert__title),
|
||||
:deep(.el-alert__icon) {
|
||||
color: var(--primary, #10b981);
|
||||
}
|
||||
}
|
||||
|
||||
&--danger {
|
||||
:deep(.el-alert__title),
|
||||
:deep(.el-alert__icon) {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -496,7 +496,7 @@ async function handleApprove(row: any) {
|
||||
loadingMap[key] = true
|
||||
try {
|
||||
const api = row.bizType === '备用金' ? advanceApi : reimburseApi
|
||||
await api.approve(row.id, { approvalOpinion: '' })
|
||||
await api.approve(row.id, { approvalOpinion: '审批通过' })
|
||||
ElMessage.success('审批通过')
|
||||
await loadData()
|
||||
await refreshSidebarBadges()
|
||||
@@ -509,7 +509,7 @@ async function handleApprove(row: any) {
|
||||
|
||||
function openRejectDialog(row: any) {
|
||||
currentRow.value = row
|
||||
approveOpinion.value = ''
|
||||
approveOpinion.value = '审批驳回'
|
||||
dialogs.approve = true
|
||||
}
|
||||
|
||||
@@ -518,7 +518,8 @@ async function handleDialogApprove() {
|
||||
loadingMap.approvePass = true
|
||||
try {
|
||||
const api = currentRow.value.bizType === '备用金' ? advanceApi : reimburseApi
|
||||
await api.approve(currentRow.value.id, { approvalOpinion: approveOpinion.value })
|
||||
const opinion = approveOpinion.value.trim() || '审批通过'
|
||||
await api.approve(currentRow.value.id, { approvalOpinion: opinion })
|
||||
ElMessage.success('审批通过')
|
||||
dialogs.approve = false
|
||||
await loadData()
|
||||
@@ -535,7 +536,8 @@ async function handleDialogReject() {
|
||||
loadingMap.approveReject = true
|
||||
try {
|
||||
const api = currentRow.value.bizType === '备用金' ? advanceApi : reimburseApi
|
||||
await api.reject(currentRow.value.id, { approvalOpinion: approveOpinion.value })
|
||||
const opinion = approveOpinion.value.trim() || '审批驳回'
|
||||
await api.reject(currentRow.value.id, { approvalOpinion: opinion })
|
||||
ElMessage.success('已驳回')
|
||||
dialogs.approve = false
|
||||
await loadData()
|
||||
@@ -562,7 +564,7 @@ async function handleBatchPass() {
|
||||
try {
|
||||
for (const row of selectedRows.value) {
|
||||
const api = row.bizType === '备用金' ? advanceApi : reimburseApi
|
||||
await api.approve(row.id, { approvalOpinion: '' })
|
||||
await api.approve(row.id, { approvalOpinion: '审批通过' })
|
||||
}
|
||||
ElMessage.success('批量审批通过成功')
|
||||
await loadData()
|
||||
@@ -583,7 +585,7 @@ async function handleBatchReject() {
|
||||
try {
|
||||
for (const row of selectedRows.value) {
|
||||
const api = row.bizType === '备用金' ? advanceApi : reimburseApi
|
||||
await api.reject(row.id, { approvalOpinion: '' })
|
||||
await api.reject(row.id, { approvalOpinion: '审批驳回' })
|
||||
}
|
||||
ElMessage.success('批量驳回成功')
|
||||
await loadData()
|
||||
|
||||
@@ -271,7 +271,7 @@
|
||||
<script setup lang="ts" name="ContractApproval">
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||
import { watchDebounced } from '@vueuse/core'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
@@ -479,11 +479,29 @@
|
||||
})
|
||||
}
|
||||
|
||||
const DEFAULT_OPINIONS = ['审批通过', '审批驳回'] as const
|
||||
|
||||
const defaultOpinion = (result: string) => (result === 'rejected' ? '审批驳回' : '审批通过')
|
||||
|
||||
watch(
|
||||
() => approvalForm.value.approvalResult,
|
||||
(result) => {
|
||||
if (
|
||||
!approvalForm.value.approvalOpinion.trim() ||
|
||||
DEFAULT_OPINIONS.includes(
|
||||
approvalForm.value.approvalOpinion as (typeof DEFAULT_OPINIONS)[number]
|
||||
)
|
||||
) {
|
||||
approvalForm.value.approvalOpinion = defaultOpinion(result)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const handleApprove = (row: ApprovalVO) => {
|
||||
approvalForm.value = {
|
||||
id: row.id,
|
||||
approvalResult: 'approved',
|
||||
approvalOpinion: ''
|
||||
approvalOpinion: defaultOpinion('approved')
|
||||
}
|
||||
approvalDialog.visible = true
|
||||
}
|
||||
|
||||
@@ -10,6 +10,22 @@
|
||||
modal-class="order-module-dialog-mask"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-alert
|
||||
v-if="isBatchMode && batchCount > 0"
|
||||
type="success"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="batch-action-alert batch-action-alert--success"
|
||||
:title="batchInfoTitle"
|
||||
/>
|
||||
<el-alert
|
||||
v-if="skippedCount > 0"
|
||||
type="error"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="batch-action-alert batch-action-alert--danger"
|
||||
:title="`已跳过 ${skippedCount} 条不可处理的合同`"
|
||||
/>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
@@ -35,14 +51,6 @@
|
||||
<template #label>
|
||||
选择审批人<span class="form-label-tip">支持搜索用户姓名</span>
|
||||
</template>
|
||||
<el-alert
|
||||
v-if="submitPrefillTip"
|
||||
type="success"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="已按「合同审批人设置」预填审批人,可修改后提交"
|
||||
style="margin-bottom: 12px"
|
||||
/>
|
||||
<el-select
|
||||
v-model="form.approverIds"
|
||||
multiple
|
||||
@@ -158,6 +166,7 @@ const dialogVisible = ref(false)
|
||||
const actionType = ref<'submit' | 'approve' | 'reject' | 'withdraw'>('submit')
|
||||
const contractId = ref<string | number>()
|
||||
const contractIds = ref<(string | number)[]>()
|
||||
const skippedCount = ref(0)
|
||||
|
||||
const form = ref({
|
||||
approvalType: 'sequential',
|
||||
@@ -170,9 +179,11 @@ const form = ref({
|
||||
const approverList = ref<ApproverUser[]>([])
|
||||
const submitLoading = ref(false)
|
||||
const userLoading = ref(false)
|
||||
const submitPrefillTip = ref(false)
|
||||
const formRef = ref<ElFormInstance>()
|
||||
|
||||
const isBatchMode = computed(() => !!(contractIds.value && contractIds.value.length > 0))
|
||||
const batchCount = computed(() => contractIds.value?.length || 0)
|
||||
|
||||
const rules = computed(() => {
|
||||
const baseRules: any = {}
|
||||
if (actionType.value === 'submit') {
|
||||
@@ -195,12 +206,25 @@ const dialogTitle = computed(() => {
|
||||
withdraw: '撤回审批'
|
||||
}
|
||||
let title = titleMap[actionType.value]
|
||||
if (contractIds.value && contractIds.value.length > 0) {
|
||||
title += `(${contractIds.value.length}条)`
|
||||
if (isBatchMode.value) {
|
||||
title += `(${batchCount.value}条)`
|
||||
}
|
||||
return title
|
||||
})
|
||||
|
||||
const batchInfoTitle = computed(() => {
|
||||
if (actionType.value === 'submit') {
|
||||
return `将提交 ${batchCount.value} 条草稿合同(使用同一组审批人与说明)`
|
||||
}
|
||||
if (actionType.value === 'approve') {
|
||||
return `将审批通过 ${batchCount.value} 条合同(使用同一审批意见)`
|
||||
}
|
||||
if (actionType.value === 'reject') {
|
||||
return `将审批驳回 ${batchCount.value} 条合同(使用同一审批意见)`
|
||||
}
|
||||
return `将处理 ${batchCount.value} 条合同`
|
||||
})
|
||||
|
||||
const confirmButtonText = computed(() => {
|
||||
const textMap = {
|
||||
submit: '提交',
|
||||
@@ -214,6 +238,7 @@ const confirmButtonText = computed(() => {
|
||||
const open = async (id: string | number, type: 'submit' | 'approve' | 'reject' | 'withdraw' = 'submit') => {
|
||||
contractId.value = id
|
||||
contractIds.value = undefined
|
||||
skippedCount.value = 0
|
||||
actionType.value = type
|
||||
dialogVisible.value = true
|
||||
resetForm()
|
||||
@@ -225,10 +250,12 @@ const open = async (id: string | number, type: 'submit' | 'approve' | 'reject' |
|
||||
|
||||
const openBatch = async (
|
||||
idList: (string | number)[],
|
||||
type: 'submit' | 'approve' | 'reject' | 'withdraw' = 'submit'
|
||||
type: 'submit' | 'approve' | 'reject' | 'withdraw' = 'submit',
|
||||
options?: { skippedCount?: number }
|
||||
) => {
|
||||
contractIds.value = idList
|
||||
contractId.value = undefined
|
||||
skippedCount.value = options?.skippedCount || 0
|
||||
actionType.value = type
|
||||
dialogVisible.value = true
|
||||
resetForm()
|
||||
@@ -238,6 +265,33 @@ const openBatch = async (
|
||||
}
|
||||
}
|
||||
|
||||
const showBatchResultMessage = (options: {
|
||||
actionLabel: string
|
||||
successCount: number
|
||||
failedCount: number
|
||||
skippedCount?: number
|
||||
}) => {
|
||||
const { actionLabel, successCount, failedCount, skippedCount: skip = 0 } = options
|
||||
const skipText = skip > 0 ? `,跳过 ${skip} 条` : ''
|
||||
if (successCount === 0 && failedCount === 0) {
|
||||
$modal.msgWarning(`没有可${actionLabel}的合同${skipText}`)
|
||||
return
|
||||
}
|
||||
if (successCount === 0) {
|
||||
$modal.msgError(`${actionLabel}失败${skipText}`)
|
||||
return
|
||||
}
|
||||
if (failedCount > 0) {
|
||||
$modal.msgWarning(`成功${actionLabel} ${successCount} 条,失败 ${failedCount} 条${skipText}`)
|
||||
return
|
||||
}
|
||||
if (successCount === 1 && skip === 0) {
|
||||
$modal.msgSuccess(`${actionLabel}成功`)
|
||||
return
|
||||
}
|
||||
$modal.msgSuccess(`成功${actionLabel} ${successCount} 条${skipText}`)
|
||||
}
|
||||
|
||||
const loadApproverList = async (nickName?: string) => {
|
||||
userLoading.value = true
|
||||
try {
|
||||
@@ -278,9 +332,7 @@ async function resolvePrefillApproverIds(userList: ApproverUser[]) {
|
||||
}
|
||||
|
||||
async function applyPrefillApprovers() {
|
||||
const prefillIds = await resolvePrefillApproverIds(approverList.value)
|
||||
form.value.approverIds = prefillIds
|
||||
submitPrefillTip.value = prefillIds.length > 0
|
||||
form.value.approverIds = await resolvePrefillApproverIds(approverList.value)
|
||||
}
|
||||
|
||||
const handleSearchUser = (query: string) => {
|
||||
@@ -298,34 +350,73 @@ const getApproverName = (approverId: string | number): string => {
|
||||
|
||||
const handleConfirm = async () => {
|
||||
await formRef.value?.validate()
|
||||
const isBatch = !!(contractIds.value && contractIds.value.length > 0)
|
||||
const isBatch = isBatchMode.value
|
||||
if (!isBatch && (contractId.value == null || contractId.value === '')) {
|
||||
$modal.msgError('合同 ID 缺失,请关闭后重新打开提交审批')
|
||||
$modal.msgError('合同 ID 缺失,请关闭后重新打开')
|
||||
return
|
||||
}
|
||||
submitLoading.value = true
|
||||
try {
|
||||
let shouldClose = true
|
||||
|
||||
if (actionType.value === 'submit') {
|
||||
if (isBatch) {
|
||||
const count = await batchSubmitContract(contractIds.value!, form.value.approverIds, form.value.remark)
|
||||
$modal.msgSuccess(`提交成功,成功 ${count ?? contractIds.value!.length} 条`)
|
||||
showBatchResultMessage({
|
||||
actionLabel: '提交',
|
||||
successCount: count ?? contractIds.value!.length,
|
||||
failedCount: 0,
|
||||
skippedCount: skippedCount.value
|
||||
})
|
||||
} else {
|
||||
await submitContract(contractId.value!, form.value.approverIds, form.value.remark)
|
||||
$modal.msgSuccess('提交成功')
|
||||
}
|
||||
} else if (actionType.value === 'approve') {
|
||||
await approveContract(contractId.value!, form.value.comment)
|
||||
$modal.msgSuccess('审批通过')
|
||||
if (isBatch) {
|
||||
const results = await Promise.allSettled(
|
||||
contractIds.value!.map((id) => approveContract(id, form.value.comment))
|
||||
)
|
||||
const successCount = results.filter((item) => item.status === 'fulfilled').length
|
||||
const failedCount = results.length - successCount
|
||||
showBatchResultMessage({
|
||||
actionLabel: '审批通过',
|
||||
successCount,
|
||||
failedCount,
|
||||
skippedCount: skippedCount.value
|
||||
})
|
||||
shouldClose = successCount > 0
|
||||
} else {
|
||||
await approveContract(contractId.value!, form.value.comment)
|
||||
$modal.msgSuccess('审批通过')
|
||||
}
|
||||
} else if (actionType.value === 'reject') {
|
||||
await rejectContract(contractId.value!, form.value.comment)
|
||||
$modal.msgSuccess('已驳回')
|
||||
if (isBatch) {
|
||||
const results = await Promise.allSettled(
|
||||
contractIds.value!.map((id) => rejectContract(id, form.value.comment))
|
||||
)
|
||||
const successCount = results.filter((item) => item.status === 'fulfilled').length
|
||||
const failedCount = results.length - successCount
|
||||
showBatchResultMessage({
|
||||
actionLabel: '审批驳回',
|
||||
successCount,
|
||||
failedCount,
|
||||
skippedCount: skippedCount.value
|
||||
})
|
||||
shouldClose = successCount > 0
|
||||
} else {
|
||||
await rejectContract(contractId.value!, form.value.comment)
|
||||
$modal.msgSuccess('已驳回')
|
||||
}
|
||||
} else if (actionType.value === 'withdraw') {
|
||||
await withdrawContract(contractId.value!, form.value.reason)
|
||||
$modal.msgSuccess('已撤回')
|
||||
}
|
||||
|
||||
dialogVisible.value = false
|
||||
emit('success')
|
||||
if (shouldClose) {
|
||||
dialogVisible.value = false
|
||||
emit('success')
|
||||
}
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
@@ -350,7 +441,6 @@ const resetForm = () => {
|
||||
remark: '',
|
||||
reason: ''
|
||||
}
|
||||
submitPrefillTip.value = false
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
@@ -361,6 +451,24 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.batch-action-alert {
|
||||
margin-bottom: 12px;
|
||||
|
||||
&--success {
|
||||
:deep(.el-alert__title),
|
||||
:deep(.el-alert__icon) {
|
||||
color: var(--primary, #10b981);
|
||||
}
|
||||
}
|
||||
|
||||
&--danger {
|
||||
:deep(.el-alert__title),
|
||||
:deep(.el-alert__icon) {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-tip-block {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
|
||||
@@ -141,16 +141,16 @@
|
||||
<el-button
|
||||
v-if="activeView === 'submitted'"
|
||||
class="btn-ghost"
|
||||
:disabled="single"
|
||||
@click="handleApprove(contractList[contractList.findIndex((item) => item.id === ids[0])])"
|
||||
:disabled="!canApproveBatch"
|
||||
@click="handleApprove()"
|
||||
>
|
||||
<el-icon><Select /></el-icon>审批通过
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="activeView === 'submitted'"
|
||||
class="btn-ghost"
|
||||
:disabled="single"
|
||||
@click="handleReject(contractList[contractList.findIndex((item) => item.id === ids[0])])"
|
||||
:disabled="!canApproveBatch"
|
||||
@click="handleReject()"
|
||||
>
|
||||
<el-icon><Close /></el-icon>审批驳回
|
||||
</el-button>
|
||||
@@ -1111,12 +1111,13 @@ const handleSubmitBatch = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (draftContracts.length < selectedContracts.length) {
|
||||
$modal.msgWarning(`选中了 ${selectedContracts.length} 条合同,其中 ${draftContracts.length} 条为草稿状态,只有草稿状态的合同可以提交审批`);
|
||||
const skippedCount = selectedContracts.length - draftContracts.length;
|
||||
if (skippedCount > 0) {
|
||||
$modal.msgWarning(`已跳过 ${skippedCount} 条非草稿合同,仅处理草稿状态单据`);
|
||||
}
|
||||
|
||||
const draftIds = draftContracts.map((item) => item.id);
|
||||
approvalDialogRef.value?.openBatch(draftIds, 'submit');
|
||||
approvalDialogRef.value?.openBatch(draftIds, 'submit', { skippedCount });
|
||||
};
|
||||
|
||||
/** 判断是否可以提交审批(至少有一条草稿状态的合同) */
|
||||
@@ -1126,32 +1127,55 @@ const canSubmit = computed(() => {
|
||||
return selectedContracts.some((item) => item.status === 'draft');
|
||||
});
|
||||
|
||||
/** 审批通过 */
|
||||
const handleApprove = (row?: ContractVO) => {
|
||||
if (!row && ids.value.length !== 1) {
|
||||
$modal.msgWarning('请选择一条数据进行审批');
|
||||
/** 判断是否可以审批(至少有一条当前用户可审批的合同) */
|
||||
const canApproveBatch = computed(() => {
|
||||
if (!selectedRows.value.length) return false;
|
||||
return selectedRows.value.some((item) => item.canApprove);
|
||||
});
|
||||
|
||||
/** 筛选可审批合同并打开审批弹窗(支持单条与批量) */
|
||||
const openApprovalForTargets = (
|
||||
row: ContractVO | undefined,
|
||||
type: 'approve' | 'reject'
|
||||
) => {
|
||||
const targets = row ? [row] : selectedRows.value;
|
||||
if (!targets.length) {
|
||||
$modal.msgWarning('请选择要审批的合同');
|
||||
return;
|
||||
}
|
||||
const targetRow = row || contractList.value[contractList.value.findIndex((item) => item.id === ids.value[0])];
|
||||
if (!targetRow) {
|
||||
$modal.msgWarning('未找到选中的数据');
|
||||
|
||||
const validTargets = targets.filter((item) => item.canApprove);
|
||||
const skippedCount = targets.length - validTargets.length;
|
||||
|
||||
if (!validTargets.length) {
|
||||
$modal.msgWarning(
|
||||
targets.length === 1
|
||||
? '您没有该合同的审批权限,或已审批过该合同'
|
||||
: '所选合同均不可审批(无权、已审或不在待审批状态)'
|
||||
);
|
||||
return;
|
||||
}
|
||||
approvalDialogRef.value?.open(targetRow.id, 'approve');
|
||||
|
||||
if (skippedCount > 0) {
|
||||
$modal.msgWarning(`已跳过 ${skippedCount} 条不可审批的合同,仅处理可审批单据`);
|
||||
}
|
||||
|
||||
const validIds = validTargets.map((item) => item.id);
|
||||
if (validIds.length === 1 && skippedCount === 0) {
|
||||
approvalDialogRef.value?.open(validIds[0], type);
|
||||
} else {
|
||||
approvalDialogRef.value?.openBatch(validIds, type, { skippedCount });
|
||||
}
|
||||
};
|
||||
|
||||
/** 审批驳回 */
|
||||
/** 审批通过(支持单条与批量) */
|
||||
const handleApprove = (row?: ContractVO) => {
|
||||
openApprovalForTargets(row, 'approve');
|
||||
};
|
||||
|
||||
/** 审批驳回(支持单条与批量) */
|
||||
const handleReject = (row?: ContractVO) => {
|
||||
if (!row && ids.value.length !== 1) {
|
||||
$modal.msgWarning('请选择一条数据进行审批');
|
||||
return;
|
||||
}
|
||||
const targetRow = row || contractList.value[contractList.value.findIndex((item) => item.id === ids.value[0])];
|
||||
if (!targetRow) {
|
||||
$modal.msgWarning('未找到选中的数据');
|
||||
return;
|
||||
}
|
||||
approvalDialogRef.value?.open(targetRow.id, 'reject');
|
||||
openApprovalForTargets(row, 'reject');
|
||||
};
|
||||
|
||||
/** 上传合同扫描件 */
|
||||
|
||||
@@ -81,11 +81,13 @@
|
||||
try {
|
||||
const res = await fetchUserOptions()
|
||||
const options = (res as any)?.data ?? res ?? []
|
||||
userList.value = (Array.isArray(options) ? options : []).map((item: any) => ({
|
||||
userId: item.value ?? item.userId ?? item.id,
|
||||
nickName: item.nickName ?? item.label ?? item.userName ?? '',
|
||||
userName: item.userNo ?? item.user_no ?? item.userName ?? ''
|
||||
}))
|
||||
userList.value = (Array.isArray(options) ? options : [])
|
||||
.map((item: any) => ({
|
||||
userId: String(item.value ?? item.userId ?? item.id ?? ''),
|
||||
nickName: item.nickName ?? item.label ?? item.userName ?? '',
|
||||
userName: item.userNo ?? item.user_no ?? item.userName ?? ''
|
||||
}))
|
||||
.filter((u) => u.userId)
|
||||
} catch (error) {
|
||||
console.error('加载用户列表失败:', error)
|
||||
userList.value = []
|
||||
|
||||
@@ -123,13 +123,29 @@ watch(dialogVisible, (val) => {
|
||||
emit('update:visible', val)
|
||||
})
|
||||
|
||||
const DEFAULT_OPINIONS = ['审批通过', '审批驳回'] as const
|
||||
|
||||
const form = ref({
|
||||
quoteId: undefined as number | undefined,
|
||||
result: 'approved',
|
||||
opinion: '',
|
||||
opinion: '审批通过',
|
||||
nextApproverUserId: undefined as string | number | undefined
|
||||
})
|
||||
|
||||
const defaultOpinion = (result: string) => (result === 'rejected' ? '审批驳回' : '审批通过')
|
||||
|
||||
watch(
|
||||
() => form.value.result,
|
||||
(result) => {
|
||||
if (
|
||||
!form.value.opinion.trim() ||
|
||||
DEFAULT_OPINIONS.includes(form.value.opinion as (typeof DEFAULT_OPINIONS)[number])
|
||||
) {
|
||||
form.value.opinion = defaultOpinion(result)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const rules = computed<FormRules>(() => {
|
||||
if (props.approvalType === 'approve') {
|
||||
return {
|
||||
@@ -165,7 +181,7 @@ const resetForm = () => {
|
||||
form.value = {
|
||||
quoteId: props.quoteInfo.id as number | undefined,
|
||||
result: 'approved',
|
||||
opinion: '',
|
||||
opinion: defaultOpinion('approved'),
|
||||
nextApproverUserId: undefined
|
||||
}
|
||||
formRef.value?.clearValidate()
|
||||
|
||||
@@ -140,12 +140,14 @@ const loadUsers = async (nickName?: string) => {
|
||||
try {
|
||||
const res = await fetchUserOptions()
|
||||
const options = (res as any)?.data ?? res ?? []
|
||||
const mapped = (Array.isArray(options) ? options : []).map((item: any) => ({
|
||||
userId: item.value ?? item.userId ?? item.id,
|
||||
nickName: item.nickName ?? item.label ?? item.userName ?? '',
|
||||
userName: item.userNo ?? item.user_no ?? item.userName ?? '',
|
||||
deptName: item.deptName ?? ''
|
||||
}))
|
||||
const mapped = (Array.isArray(options) ? options : [])
|
||||
.map((item: any) => ({
|
||||
userId: String(item.value ?? item.userId ?? item.id ?? ''),
|
||||
nickName: item.nickName ?? item.label ?? item.userName ?? '',
|
||||
userName: item.userNo ?? item.user_no ?? item.userName ?? '',
|
||||
deptName: item.deptName ?? ''
|
||||
}))
|
||||
.filter((user) => user.userId)
|
||||
userList.value = nickName
|
||||
? mapped.filter((user) => user.nickName.includes(nickName) || user.userName.includes(nickName))
|
||||
: mapped
|
||||
|
||||
@@ -463,14 +463,6 @@
|
||||
scroll-to-error
|
||||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||
>
|
||||
<el-alert
|
||||
v-if="submitPrefillTip"
|
||||
type="success"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="已按「报价单审批人设置」预填审批人,可修改后提交"
|
||||
style="margin-bottom: 12px"
|
||||
/>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item prop="approverIds" class="form-col-span-2">
|
||||
<template #label>
|
||||
@@ -697,7 +689,6 @@ const submitForm = ref({
|
||||
const submitRules = ref({
|
||||
approverIds: [{ required: true, message: '请选择审批人', trigger: 'change' }]
|
||||
});
|
||||
const submitPrefillTip = ref(false);
|
||||
const submitFormRef = ref<FormInstance>();
|
||||
|
||||
// 用户列表(负责人 + 审批人候选)
|
||||
@@ -1097,11 +1088,11 @@ const loadUserList = async () => {
|
||||
const res = await fetchUserOptions();
|
||||
const options = (res as any)?.data ?? res ?? [];
|
||||
userList.value = (Array.isArray(options) ? options : []).map((item: any) => ({
|
||||
userId: item.value ?? item.userId ?? item.id,
|
||||
userId: String(item.value ?? item.userId ?? item.id ?? ''),
|
||||
nickName: item.nickName ?? item.label ?? item.userName ?? '',
|
||||
userName: item.userNo ?? item.user_no ?? item.userName ?? '',
|
||||
deptName: item.deptName ?? ''
|
||||
}));
|
||||
})).filter((u) => u.userId);
|
||||
} catch (error) {
|
||||
console.error('加载用户列表失败:', error);
|
||||
userList.value = [];
|
||||
@@ -1719,7 +1710,6 @@ const handleSubmit = async () => {
|
||||
approverIds: prefillIds,
|
||||
remark: ''
|
||||
};
|
||||
submitPrefillTip.value = prefillIds.length > 0;
|
||||
|
||||
// 打开审批对话框
|
||||
submitDialog.value.visible = true;
|
||||
|
||||
Loaded 100 of 104 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user