docs(requirements): 需求裁决对照表 v2、功能模块清单与 M1 设计产物入库(移除被 v2 取代的旧清单)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
8668e58c30
commit
e568ebde61
45
docs/design/echarts.min.js
vendored
Normal file
45
docs/design/echarts.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
docs/design/视觉方向预览-全览.png
Normal file
BIN
docs/design/视觉方向预览-全览.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
976
docs/design/视觉方向预览.html
Normal file
976
docs/design/视觉方向预览.html
Normal file
@ -0,0 +1,976 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>衍水工资系统 · 视觉方向预览</title>
|
||||
<style>
|
||||
/* ============================================================
|
||||
设计体系 v3(质感版):中性灰白 · 靛蓝点缀 · 微质感
|
||||
质感来源(而非装饰):
|
||||
1) 表面微渐变 —— 1–2% 明度差,暗示受光方向
|
||||
2) 半透明描边 —— 边界在不同底色上自然融合
|
||||
3) 分层阴影 —— 近距实影 + 远距弥散影叠加
|
||||
4) 内高光 / 内阴影 —— 浮起与凹陷的材质暗示
|
||||
5) 极微噪点 —— 去除纯色的「塑料感」
|
||||
6) 精确排版 —— 字距、等宽数字、行高的一致节律
|
||||
============================================================ */
|
||||
:root {
|
||||
--ys-indigo: #2e4ba8;
|
||||
--ys-indigo-hi: #3a58bc; /* 渐变亮端 */
|
||||
--ys-indigo-deep: #243c8b;
|
||||
--ys-indigo-wash: #edf1fa;
|
||||
--ys-paper: #ffffff;
|
||||
--ys-mist: #f4f5f8; /* 页面底 */
|
||||
--ys-aside-1: #f2f4f8; /* 侧栏渐变起 */
|
||||
--ys-aside-2: #ecedf3; /* 侧栏渐变止 */
|
||||
--ys-ink: #1c2333;
|
||||
--ys-ink-2: #5b6577;
|
||||
--ys-ink-3: #9aa3b2;
|
||||
--ys-line: rgba(28, 35, 51, 0.09); /* 半透明描边 */
|
||||
--ys-line-soft: rgba(28, 35, 51, 0.065);
|
||||
|
||||
--ys-ok: #1e9e6a; --ys-ok-wash-1: #ecf8f2; --ys-ok-wash-2: #e4f3ec;
|
||||
--ys-warn: #c98a1b; --ys-warn-wash-1: #fbf4e4; --ys-warn-wash-2: #f8efd9;
|
||||
--ys-danger: #c94f4f; --ys-danger-wash: #fbeded;
|
||||
|
||||
--ys-radius: 10px;
|
||||
--ys-radius-sm: 7px;
|
||||
--ys-font: 'PingFang SC', 'Microsoft YaHei', 'Noto Sans SC', -apple-system,
|
||||
'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
|
||||
--ys-ease: cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||
|
||||
/* 分层阴影:近距定义轮廓,远距弥散氛围 */
|
||||
--ys-shadow-card: 0 1px 2px rgba(28, 35, 51, 0.04), 0 3px 10px rgba(28, 35, 51, 0.03);
|
||||
--ys-shadow-raise: 0 1px 2px rgba(28, 35, 51, 0.05), 0 6px 18px rgba(28, 35, 51, 0.07);
|
||||
|
||||
/* 极微噪点:去除大色块的塑料感(内联 SVG,无外部依赖) */
|
||||
--ys-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
|
||||
|
||||
/* 品牌字:思源黑体(现代几何黑体;实施时自托管 4 字子集,内网可用) */
|
||||
--ys-font-brand: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: var(--ys-font);
|
||||
color: var(--ys-ink);
|
||||
background-color: var(--ys-mist);
|
||||
/* 点阵网格:22px 间距的 1px 微点 —— 给背景「秩序与密度」,不再空旷 */
|
||||
background-image: radial-gradient(rgba(28, 35, 51, 0.07) 1px, transparent 1px);
|
||||
background-size: 24px 24px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
/* 噪点覆盖层:全页 2.2% 强度,肉眼几乎无感,但色块不再是「纯色塑料」 */
|
||||
body::after {
|
||||
content: ''; position: fixed; inset: 0; z-index: 2147483647;
|
||||
pointer-events: none; opacity: 0.022; background-image: var(--ys-noise);
|
||||
}
|
||||
.num { font-variant-numeric: tabular-nums; }
|
||||
|
||||
.wrap { max-width: 1180px; margin: 0 auto; padding: 48px 32px 96px; }
|
||||
.section { margin-top: 64px; }
|
||||
.section > h2 {
|
||||
font-size: 13px; font-weight: 500; letter-spacing: 0.08em;
|
||||
color: var(--ys-ink-3); text-transform: uppercase;
|
||||
display: flex; align-items: center; gap: 10px; margin-bottom: 20px;
|
||||
}
|
||||
.section > h2::after { content: ''; height: 1px; flex: 1; background: var(--ys-line); }
|
||||
|
||||
/* 签名元素:水线 */
|
||||
.waterline {
|
||||
width: 56px; height: 2px; border-radius: 1px;
|
||||
background: linear-gradient(90deg, var(--ys-indigo), rgba(46, 75, 168, 0));
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
|
||||
/* ---------- 封面 ---------- */
|
||||
.cover { padding: 24px 0 8px; }
|
||||
.cover .brand {
|
||||
font-family: var(--ys-font-brand);
|
||||
font-size: 15px; font-weight: 500; letter-spacing: 0.32em; color: var(--ys-ink-2);
|
||||
}
|
||||
.cover h1 { font-size: 30px; font-weight: 600; margin-top: 18px; letter-spacing: -0.01em; }
|
||||
.cover .sub { color: var(--ys-ink-2); margin-top: 10px; font-size: 15px; }
|
||||
.cover .tags { display: flex; gap: 8px; margin-top: 22px; flex-wrap: wrap; }
|
||||
.tag {
|
||||
font-size: 12px; color: var(--ys-ink-2);
|
||||
background: linear-gradient(180deg, #ffffff, #fbfcfe);
|
||||
border: 1px solid var(--ys-line); border-radius: 999px; padding: 4px 12px;
|
||||
box-shadow: 0 1px 2px rgba(28, 35, 51, 0.03);
|
||||
}
|
||||
|
||||
/* ---------- 色板 ---------- */
|
||||
.swatches { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }
|
||||
.swatch {
|
||||
background: linear-gradient(180deg, #ffffff, #fcfcfe);
|
||||
border: 1px solid var(--ys-line); border-radius: var(--ys-radius);
|
||||
overflow: hidden; box-shadow: var(--ys-shadow-card);
|
||||
}
|
||||
.swatch .chip { height: 76px; }
|
||||
.swatch .meta { padding: 10px 14px 12px; }
|
||||
.swatch .name { font-size: 13px; font-weight: 600; }
|
||||
.swatch .hex { font-size: 12px; color: var(--ys-ink-2); margin-top: 2px; }
|
||||
.swatch .use { font-size: 12px; color: var(--ys-ink-3); margin-top: 6px; line-height: 1.5; }
|
||||
|
||||
/* ---------- 字体 ---------- */
|
||||
.type-sheet {
|
||||
background: linear-gradient(180deg, #ffffff, #fcfcfe);
|
||||
border: 1px solid var(--ys-line); border-radius: var(--ys-radius);
|
||||
padding: 26px 28px; box-shadow: var(--ys-shadow-card);
|
||||
}
|
||||
.type-row { display: flex; align-items: baseline; gap: 24px; padding: 12px 0; border-bottom: 1px dashed var(--ys-line-soft); }
|
||||
.type-row:last-child { border-bottom: 0; }
|
||||
.type-row .label { width: 150px; flex: none; font-size: 12px; color: var(--ys-ink-3); }
|
||||
.t-page-title { font-size: 18px; font-weight: 600; letter-spacing: -0.01em; }
|
||||
.t-card-title { font-size: 15px; font-weight: 600; }
|
||||
.t-body { font-size: 14px; }
|
||||
.t-th { font-size: 12px; font-weight: 500; letter-spacing: 0.04em; color: var(--ys-ink-2); }
|
||||
.t-amount { font-size: 15px; font-weight: 600; font-variant-numeric: tabular-nums; }
|
||||
|
||||
.amount-demo {
|
||||
margin-top: 18px;
|
||||
background: linear-gradient(180deg, #ffffff, #fcfcfe);
|
||||
border: 1px solid var(--ys-line); border-radius: var(--ys-radius);
|
||||
padding: 18px 28px; box-shadow: var(--ys-shadow-card);
|
||||
}
|
||||
.amount-demo table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
|
||||
.amount-demo th {
|
||||
text-align: right; font-size: 12px; font-weight: 500; letter-spacing: 0.04em;
|
||||
color: var(--ys-ink-2); padding: 6px 0; border-bottom: 1px solid var(--ys-line);
|
||||
}
|
||||
.amount-demo th:first-child { text-align: left; }
|
||||
.amount-demo td { text-align: right; padding: 9px 0; border-bottom: 1px solid var(--ys-line-soft); font-size: 13px; }
|
||||
.amount-demo td:first-child { text-align: left; color: var(--ys-ink-2); }
|
||||
.amount-demo tr:last-child td { border-bottom: 0; font-weight: 600; font-size: 15px; }
|
||||
|
||||
/* 品牌字候选(思源黑体已装于本机;实施时自托管子集) */
|
||||
.brand-candidates { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin-top: 14px; }
|
||||
.bc {
|
||||
background: linear-gradient(180deg, #ffffff, #fcfcfe);
|
||||
border: 1px solid var(--ys-line); border-radius: var(--ys-radius);
|
||||
box-shadow: var(--ys-shadow-card); padding: 20px 24px; text-align: center;
|
||||
}
|
||||
.bc-cap { font-size: 11px; color: var(--ys-ink-3); margin-top: 12px; }
|
||||
.bc-rec { color: var(--ys-indigo); font-weight: 500; margin-left: 4px; }
|
||||
|
||||
/* ---------- 组件 ---------- */
|
||||
.comp-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
|
||||
.comp-card {
|
||||
background: linear-gradient(180deg, #ffffff, #fcfcfe);
|
||||
border: 1px solid var(--ys-line); border-radius: var(--ys-radius);
|
||||
padding: 22px 24px; box-shadow: var(--ys-shadow-card);
|
||||
}
|
||||
.comp-card h3 { font-size: 13px; font-weight: 600; color: var(--ys-ink-2); margin-bottom: 16px; letter-spacing: 0.02em; }
|
||||
.row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
||||
|
||||
.btn {
|
||||
height: 34px; padding: 0 16px; border-radius: var(--ys-radius-sm);
|
||||
font-size: 13px; font-family: inherit; cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
transition: all 150ms var(--ys-ease);
|
||||
}
|
||||
/* 主按钮:微渐变 + 内高光 + 静置浅影 —— hover 抬升 1px、影加深 */
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background: linear-gradient(180deg, var(--ys-indigo-hi), var(--ys-indigo));
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 1px 2px rgba(46, 75, 168, 0.22);
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(180deg, #31509f, var(--ys-indigo-deep));
|
||||
transform: translateY(-1px);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 4px 12px rgba(46, 75, 168, 0.3);
|
||||
}
|
||||
.btn-primary:active { transform: translateY(0); box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.18); }
|
||||
.btn-default {
|
||||
background: linear-gradient(180deg, #ffffff, #fbfcfe); color: var(--ys-ink);
|
||||
border-color: var(--ys-line); box-shadow: 0 1px 2px rgba(28, 35, 51, 0.03);
|
||||
}
|
||||
.btn-default:hover { color: var(--ys-indigo); border-color: rgba(46, 75, 168, 0.45); }
|
||||
.btn-danger { background: linear-gradient(180deg, #ffffff, #fbfcfe); color: var(--ys-danger); border-color: rgba(201, 79, 79, 0.28); }
|
||||
.btn-danger:hover { border-color: rgba(201, 79, 79, 0.6); }
|
||||
.btn-text { background: none; color: var(--ys-indigo); padding: 0 6px; height: auto; }
|
||||
.btn-text:hover { color: var(--ys-indigo-deep); }
|
||||
.btn-ghost-hover { pointer-events: none; } /* 预览:静态展示 hover 态 */
|
||||
|
||||
.input {
|
||||
height: 34px; padding: 0 12px; border-radius: var(--ys-radius-sm);
|
||||
border: 1px solid rgba(28, 35, 51, 0.13); background: #fff;
|
||||
font-size: 13px; font-family: inherit; color: var(--ys-ink); width: 190px;
|
||||
box-shadow: inset 0 1px 2px rgba(28, 35, 51, 0.035); /* 微凹陷:输入区的材质暗示 */
|
||||
transition: border-color 150ms var(--ys-ease), box-shadow 150ms var(--ys-ease);
|
||||
}
|
||||
.input::placeholder { color: var(--ys-ink-3); }
|
||||
.input:focus, .input.focused {
|
||||
outline: none; border-color: var(--ys-indigo);
|
||||
box-shadow: inset 0 1px 2px rgba(28, 35, 51, 0.03), 0 0 0 3px rgba(46, 75, 168, 0.13);
|
||||
}
|
||||
|
||||
.pill { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; padding: 2px 10px; border-radius: 999px; white-space: nowrap; }
|
||||
.pill::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
||||
.pill-ok { color: var(--ys-ok); background: linear-gradient(180deg, var(--ys-ok-wash-1), var(--ys-ok-wash-2)); }
|
||||
.pill-off { color: var(--ys-ink-2); background: linear-gradient(180deg, #f4f5f8, #eff1f5); }
|
||||
.pill-warn { color: var(--ys-warn); background: linear-gradient(180deg, var(--ys-warn-wash-1), var(--ys-warn-wash-2)); }
|
||||
|
||||
.pager { display: flex; gap: 6px; align-items: center; font-size: 13px; color: var(--ys-ink-2); }
|
||||
.pager .pg {
|
||||
min-width: 30px; height: 30px; display: inline-flex; align-items: center; justify-content: center;
|
||||
border-radius: var(--ys-radius-sm); border: 1px solid var(--ys-line);
|
||||
background: linear-gradient(180deg, #ffffff, #fbfcfe); font-variant-numeric: tabular-nums;
|
||||
box-shadow: 0 1px 2px rgba(28, 35, 51, 0.03);
|
||||
}
|
||||
.pager .pg.cur {
|
||||
background: linear-gradient(180deg, var(--ys-indigo-hi), var(--ys-indigo));
|
||||
border-color: transparent; color: #fff;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 1px 2px rgba(46, 75, 168, 0.22);
|
||||
}
|
||||
|
||||
/* ---------- 应用界面 ---------- */
|
||||
.app-shot {
|
||||
border: 1px solid var(--ys-line); border-radius: 14px; overflow: hidden;
|
||||
background: var(--ys-mist); box-shadow: 0 12px 40px rgba(28, 35, 51, 0.1);
|
||||
}
|
||||
.app { display: grid; grid-template-columns: 220px 1fr; min-height: 620px; }
|
||||
.aside {
|
||||
background: linear-gradient(180deg, var(--ys-aside-1), var(--ys-aside-2));
|
||||
border-right: 1px solid var(--ys-line);
|
||||
padding: 20px 14px; display: flex; flex-direction: column;
|
||||
}
|
||||
.aside .logo { font-family: var(--ys-font-brand); font-size: 15px; font-weight: 500; letter-spacing: 0.3em; color: var(--ys-ink); padding: 0 10px; }
|
||||
.aside .logo-sub { font-size: 11px; color: var(--ys-ink-3); letter-spacing: 0.06em; padding: 0 10px; margin-top: 4px; }
|
||||
.menu-group { margin-top: 26px; }
|
||||
.menu-label { font-size: 11px; letter-spacing: 0.1em; color: var(--ys-ink-3); padding: 0 10px; margin-bottom: 8px; }
|
||||
.menu-item {
|
||||
display: flex; align-items: center; gap: 10px; padding: 9px 10px; border-radius: var(--ys-radius-sm);
|
||||
font-size: 13px; color: var(--ys-ink-2); position: relative; margin-bottom: 2px;
|
||||
transition: all 150ms var(--ys-ease);
|
||||
}
|
||||
.menu-item:hover { color: var(--ys-ink); background: rgba(255, 255, 255, 0.55); }
|
||||
/* 当前项:白底「浮起」——微渐变 + 内高光 + 分层阴影 */
|
||||
.menu-item.active {
|
||||
color: var(--ys-indigo); font-weight: 500;
|
||||
background: linear-gradient(180deg, #ffffff, #fbfcfe);
|
||||
box-shadow: inset 0 1px 0 #fff, 0 1px 2px rgba(28, 35, 51, 0.06), 0 4px 10px rgba(28, 35, 51, 0.06);
|
||||
}
|
||||
.menu-item.active::before {
|
||||
content: ''; position: absolute; left: -14px; top: 8px; bottom: 8px; width: 2px;
|
||||
border-radius: 1px; background: var(--ys-indigo);
|
||||
}
|
||||
.menu-item svg { width: 16px; height: 16px; flex: none; opacity: 0.7; }
|
||||
.menu-item.active svg { opacity: 1; }
|
||||
.aside-foot { margin-top: auto; padding: 0 10px 2px; }
|
||||
.aside-foot-text { font-size: 11px; color: var(--ys-ink-3); letter-spacing: 0.06em; }
|
||||
|
||||
.main { display: flex; flex-direction: column; }
|
||||
.topbar {
|
||||
height: 52px;
|
||||
background: linear-gradient(180deg, #ffffff, #fdfdfe);
|
||||
border-bottom: 1px solid var(--ys-line);
|
||||
box-shadow: 0 1px 0 rgba(28, 35, 51, 0.015);
|
||||
display: flex; align-items: center; justify-content: space-between; padding: 0 22px;
|
||||
}
|
||||
.crumb { font-size: 13px; color: var(--ys-ink-3); }
|
||||
.crumb b { color: var(--ys-ink); font-weight: 500; }
|
||||
.topbar .who { display: flex; align-items: center; gap: 12px; font-size: 13px; color: var(--ys-ink-2); }
|
||||
.topbar .who .logout { color: var(--ys-ink-3); }
|
||||
.topbar .who .avatar {
|
||||
width: 26px; height: 26px; border-radius: 50%;
|
||||
background: linear-gradient(180deg, var(--ys-indigo-wash), #e4eaf7);
|
||||
box-shadow: inset 0 1px 0 #fff, 0 1px 2px rgba(28, 35, 51, 0.06);
|
||||
color: var(--ys-indigo); font-size: 12px;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 22px; flex: 1;
|
||||
/* 顶部受光:白光 + 靛蓝微光叠加,内容区有「空气感」 */
|
||||
background:
|
||||
radial-gradient(1100px 320px at 50% -110px, rgba(46, 75, 168, 0.055), rgba(46, 75, 168, 0) 65%),
|
||||
radial-gradient(900px 220px at 50% -80px, rgba(255, 255, 255, 0.85), rgba(255, 255, 255, 0) 70%);
|
||||
}
|
||||
.page-head { display: flex; align-items: flex-end; justify-content: space-between; margin-bottom: 18px; }
|
||||
.page-head h1 { font-size: 18px; font-weight: 600; letter-spacing: -0.01em; }
|
||||
.page-head .desc { font-size: 12px; color: var(--ys-ink-3); margin-top: 4px; }
|
||||
|
||||
.filter {
|
||||
background: linear-gradient(180deg, #ffffff, #fcfcfe);
|
||||
border: 1px solid var(--ys-line); border-radius: var(--ys-radius);
|
||||
padding: 14px 16px; display: flex; gap: 10px; align-items: center; margin-bottom: 14px;
|
||||
box-shadow: var(--ys-shadow-card);
|
||||
}
|
||||
.filter .spacer { flex: 1; }
|
||||
|
||||
.card {
|
||||
background: linear-gradient(180deg, #ffffff, #fcfcfe);
|
||||
border: 1px solid var(--ys-line); border-radius: var(--ys-radius);
|
||||
box-shadow: var(--ys-shadow-card); overflow: hidden;
|
||||
}
|
||||
table.grid { width: 100%; border-collapse: collapse; }
|
||||
table.grid th {
|
||||
text-align: left; font-size: 12px; font-weight: 500; letter-spacing: 0.04em;
|
||||
color: var(--ys-ink-2); background: linear-gradient(180deg, #fafbfc, #f5f6f9);
|
||||
padding: 10px 16px; border-bottom: 1px solid var(--ys-line);
|
||||
}
|
||||
table.grid td { padding: 12px 16px; border-bottom: 1px solid var(--ys-line-soft); font-size: 13px; }
|
||||
table.grid tr:last-child td { border-bottom: 0; }
|
||||
table.grid tbody tr { transition: background 120ms var(--ys-ease); }
|
||||
table.grid tbody tr:hover { background: #f7f9fd; }
|
||||
.cell-code { font-family: 'SFMono-Regular', Consolas, monospace; font-size: 12px; color: var(--ys-ink-2); }
|
||||
.cell-muted { color: var(--ys-ink-2); }
|
||||
.cell-actions a { color: var(--ys-indigo); font-size: 13px; margin-right: 12px; cursor: pointer; }
|
||||
.cell-actions a.muted { color: var(--ys-ink-3); }
|
||||
.card-foot { padding: 12px 16px; display: flex; justify-content: flex-end; border-top: 1px solid var(--ys-line); background: linear-gradient(180deg, #fdfdfe, #fbfcfe); }
|
||||
|
||||
/* ---------- 全局规范表 ---------- */
|
||||
.spec-table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
|
||||
.spec-table td { padding: 7px 8px; border-bottom: 1px solid var(--ys-line-soft); color: var(--ys-ink-2); vertical-align: top; line-height: 1.6; }
|
||||
.spec-table tr:last-child td { border-bottom: 0; }
|
||||
.spec-table td:first-child { color: var(--ys-ink); font-weight: 500; white-space: nowrap; }
|
||||
.spec-table td.num { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
|
||||
.spec-table td.bad { color: var(--ys-ink-3); text-decoration: line-through; text-decoration-color: rgba(201, 79, 79, 0.35); }
|
||||
.spec-table td.good { color: var(--ys-ink); }
|
||||
|
||||
/* ---------- 首页仪表盘(Bento Grid) ---------- */
|
||||
.bento { display: grid; grid-template-columns: repeat(4, 1fr); grid-auto-rows: 102px; gap: 14px; }
|
||||
.bento-card {
|
||||
background: linear-gradient(180deg, #ffffff, #fcfcfe);
|
||||
border: 1px solid var(--ys-line); border-radius: var(--ys-radius);
|
||||
box-shadow: var(--ys-shadow-card);
|
||||
padding: 17px 20px; display: flex; flex-direction: column;
|
||||
/* 微动效:入场交错上浮 + hover 抬升 2px(prefers-reduced-motion 下全部关闭) */
|
||||
animation: rise 320ms var(--ys-ease) backwards;
|
||||
transition: transform 180ms var(--ys-ease), box-shadow 180ms var(--ys-ease);
|
||||
}
|
||||
.bento-card:hover { transform: translateY(-2px); box-shadow: var(--ys-shadow-raise); }
|
||||
.b-head { display: flex; align-items: center; justify-content: space-between; }
|
||||
.b-icon { width: 15px; height: 15px; color: var(--ys-ink-3); opacity: 0.8; }
|
||||
.bento-card:nth-child(1) { animation-delay: 0ms; }
|
||||
.bento-card:nth-child(2) { animation-delay: 45ms; }
|
||||
.bento-card:nth-child(3) { animation-delay: 90ms; }
|
||||
.bento-card:nth-child(4) { animation-delay: 135ms; }
|
||||
.bento-card:nth-child(5) { animation-delay: 180ms; }
|
||||
.bento-card:nth-child(6) { animation-delay: 225ms; }
|
||||
.bento-card:nth-child(7) { animation-delay: 270ms; }
|
||||
.bento-card:nth-child(8) { animation-delay: 315ms; }
|
||||
@keyframes rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.bento-card, .login-card { animation: none; transition: none; }
|
||||
}
|
||||
.b-2x2 { grid-column: span 2; grid-row: span 2; }
|
||||
.b-2x1 { grid-column: span 2; }
|
||||
.b-eyebrow { font-size: 12px; font-weight: 500; letter-spacing: 0.04em; color: var(--ys-ink-2); }
|
||||
.b-big { font-size: 30px; font-weight: 600; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; line-height: 1.15; margin-top: auto; }
|
||||
.b-big small { font-size: 13px; font-weight: 500; color: var(--ys-ink-3); margin-left: 4px; letter-spacing: 0; }
|
||||
.b-num { font-size: 24px; font-weight: 600; letter-spacing: -0.01em; font-variant-numeric: tabular-nums; line-height: 1.2; margin-top: auto; }
|
||||
.b-num small { font-size: 12px; font-weight: 400; color: var(--ys-ink-3); margin-left: 3px; }
|
||||
.b-sub { font-size: 11px; color: var(--ys-ink-3); margin-top: 3px; }
|
||||
.b-up { color: var(--ys-ok); font-weight: 500; }
|
||||
.b-todo { display: flex; align-items: baseline; gap: 8px; font-size: 13px; color: var(--ys-ink-2); }
|
||||
.b-todo .cnt { color: var(--ys-indigo); font-weight: 600; font-variant-numeric: tabular-nums; }
|
||||
|
||||
/* ---------- 登录页 ---------- */
|
||||
.login-shot { border: 1px solid var(--ys-line); border-radius: 14px; overflow: hidden; box-shadow: 0 12px 40px rgba(28, 35, 51, 0.1); }
|
||||
.login { display: grid; grid-template-columns: 460px 1fr; min-height: 520px; }
|
||||
.login-brand {
|
||||
background: linear-gradient(168deg, #2c4aa6 0%, #223a85 55%, #17275c 100%);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12);
|
||||
padding: 46px 44px; display: flex; flex-direction: column; justify-content: space-between;
|
||||
position: relative; overflow: hidden;
|
||||
}
|
||||
.login-brand .brandmark { font-family: var(--ys-font-brand); font-size: 19px; font-weight: 500; letter-spacing: 0.36em; color: #fff; }
|
||||
.login-brand .slogan { font-size: 15px; color: rgba(255, 255, 255, 0.82); margin-top: 14px; line-height: 1.9; }
|
||||
.login-brand .foot { font-size: 12px; color: rgba(255, 255, 255, 0.42); letter-spacing: 0.08em; }
|
||||
.ripple { position: absolute; inset: auto 0 0 0; height: 220px; pointer-events: none; }
|
||||
.ripple i { display: block; height: 1px; margin-top: 26px; }
|
||||
.ripple i:nth-child(1) { background: linear-gradient(90deg, rgba(255,255,255,0.36), rgba(255,255,255,0.02)); }
|
||||
.ripple i:nth-child(2) { margin-top: 22px; width: 92%; background: linear-gradient(90deg, rgba(255,255,255,0.30), rgba(255,255,255,0.02)); }
|
||||
.ripple i:nth-child(3) { margin-top: 18px; width: 84%; background: linear-gradient(90deg, rgba(255,255,255,0.25), rgba(255,255,255,0.02)); }
|
||||
.ripple i:nth-child(4) { margin-top: 15px; width: 74%; background: linear-gradient(90deg, rgba(255,255,255,0.20), rgba(255,255,255,0.02)); }
|
||||
.ripple i:nth-child(5) { margin-top: 12px; width: 62%; background: linear-gradient(90deg, rgba(255,255,255,0.15), rgba(255,255,255,0.02)); }
|
||||
.ripple i:nth-child(6) { margin-top: 10px; width: 48%; background: linear-gradient(90deg, rgba(255,255,255,0.11), rgba(255,255,255,0.02)); }
|
||||
.login-form {
|
||||
background: linear-gradient(180deg, #ffffff, #fdfdff);
|
||||
box-shadow: inset 0 1px 0 #fff;
|
||||
padding: 46px 56px; display: flex; flex-direction: column; justify-content: center;
|
||||
}
|
||||
.login-form h2 { font-size: 18px; font-weight: 600; letter-spacing: -0.01em; }
|
||||
.login-form .hint { font-size: 13px; color: var(--ys-ink-3); margin: 6px 0 26px; }
|
||||
.field { margin-bottom: 16px; }
|
||||
.field label { display: block; font-size: 13px; color: var(--ys-ink-2); margin-bottom: 6px; }
|
||||
.field .input { width: 100%; height: 38px; }
|
||||
.captcha-row { display: flex; gap: 10px; }
|
||||
.captcha-row .input { flex: 1; }
|
||||
.captcha-box {
|
||||
width: 104px; height: 38px; border: 1px solid var(--ys-line); border-radius: var(--ys-radius-sm);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-family: 'SFMono-Regular', Consolas, monospace; font-size: 17px; letter-spacing: 0.28em;
|
||||
color: var(--ys-indigo); background: linear-gradient(180deg, #f7f8fb, #f1f3f8);
|
||||
box-shadow: inset 0 1px 2px rgba(28, 35, 51, 0.04); user-select: none;
|
||||
}
|
||||
.login-btn { width: 100%; height: 38px; margin-top: 6px; font-size: 14px; }
|
||||
|
||||
/* 方案 B:居中卡片 */
|
||||
.login-center {
|
||||
position: relative; overflow: hidden;
|
||||
min-height: 480px; display: flex; align-items: center; justify-content: center;
|
||||
background-color: #f2f4f8;
|
||||
background-image: radial-gradient(rgba(28, 35, 51, 0.07) 1px, transparent 1px);
|
||||
background-size: 24px 24px;
|
||||
padding: 40px;
|
||||
}
|
||||
/* 顶部光晕:靛蓝的「天光」,柔和地洒在登录卡上 */
|
||||
.login-center::before {
|
||||
content: ''; position: absolute; top: -160px; left: 50%; transform: translateX(-50%);
|
||||
width: 860px; height: 460px; pointer-events: none;
|
||||
background: radial-gradient(closest-side, rgba(46, 75, 168, 0.08), rgba(46, 75, 168, 0));
|
||||
}
|
||||
/* 背景涟漪:同心圆水波(「衍水」的品牌装饰,右上入画) */
|
||||
.ripple-bg { position: absolute; top: -170px; right: -130px; width: 520px; height: 520px; pointer-events: none; }
|
||||
.ripple-bg i {
|
||||
position: absolute; inset: 0; border-radius: 50%;
|
||||
border: 1px solid rgba(46, 75, 168, 0.09);
|
||||
}
|
||||
.ripple-bg i:nth-child(2) { inset: 65px; border-color: rgba(46, 75, 168, 0.07); }
|
||||
.ripple-bg i:nth-child(3) { inset: 130px; border-color: rgba(46, 75, 168, 0.055); }
|
||||
.ripple-bg i:nth-child(4) { inset: 195px; border: none; background: radial-gradient(circle, rgba(46, 75, 168, 0.05), rgba(46, 75, 168, 0.015)); }
|
||||
.login-card {
|
||||
position: relative; overflow: hidden;
|
||||
animation: rise 320ms var(--ys-ease) backwards;
|
||||
width: 400px;
|
||||
background: linear-gradient(180deg, #ffffff, #fcfcfe);
|
||||
border: 1px solid var(--ys-line); border-radius: 14px;
|
||||
box-shadow: 0 2px 4px rgba(28, 35, 51, 0.04), 0 16px 40px rgba(28, 35, 51, 0.1);
|
||||
padding: 30px 36px 32px;
|
||||
}
|
||||
.lc-brand { text-align: center; }
|
||||
.lc-brand-name { font-family: var(--ys-font-brand); font-size: 17px; font-weight: 500; letter-spacing: 0.34em; color: var(--ys-ink); }
|
||||
.lc-slogan { font-size: 12.5px; color: var(--ys-ink-3); margin-top: 8px; letter-spacing: 0.02em; }
|
||||
.lc-form-head { font-size: 15px; font-weight: 600; margin-top: 26px; }
|
||||
.lc-form-hint { font-size: 12px; color: var(--ys-ink-3); margin: 4px 0 18px; }
|
||||
.lc-foot { text-align: center; margin-top: 16px; font-size: 11px; color: var(--ys-ink-3); letter-spacing: 0.08em; }
|
||||
.login-card .input { width: 100%; height: 38px; }
|
||||
|
||||
.note { margin-top: 18px; font-size: 13px; color: var(--ys-ink-2); line-height: 1.9; }
|
||||
.note b { color: var(--ys-ink); font-weight: 600; }
|
||||
.states { display: flex; gap: 14px; margin-top: 12px; }
|
||||
.state { text-align: center; }
|
||||
.state .cap { font-size: 11px; color: var(--ys-ink-3); margin-top: 6px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
<!-- ============ 封面 ============ -->
|
||||
<header class="cover" id="sec-cover">
|
||||
<div class="brand">衍水集团</div>
|
||||
<h1>营销人员工资管理系统 · 视觉方向</h1>
|
||||
<div class="sub">企业级扁平 · 轻质感 · Bento 布局 · 品牌色点缀 · 微动效 · 精致文案</div>
|
||||
<div class="waterline"></div>
|
||||
<div class="tags">
|
||||
<span class="tag">表面微渐变(受光)</span>
|
||||
<span class="tag">半透明描边</span>
|
||||
<span class="tag">分层阴影</span>
|
||||
<span class="tag">内高光 / 内阴影</span>
|
||||
<span class="tag">2% 噪点</span>
|
||||
<span class="tag">等宽数字</span>
|
||||
<span class="tag">120–180ms 轻动效</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ============ 色板 ============ -->
|
||||
<section class="section" id="sec-palette">
|
||||
<h2>01 · 色板与材质</h2>
|
||||
<div class="swatches">
|
||||
<div class="swatch">
|
||||
<div class="chip" style="background:linear-gradient(180deg,#3a58bc,#2e4ba8)"></div>
|
||||
<div class="meta"><div class="name">靛蓝</div><div class="hex num">#2E4BA8 ↑#3A58BC</div>
|
||||
<div class="use">品牌色。按钮/链接/焦点/当前菜单,配微渐变与内高光</div></div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="chip" style="background:linear-gradient(180deg,#f2f4f8,#ecedf3)"></div>
|
||||
<div class="meta"><div class="name">侧栏灰</div><div class="hex num">#F2F4F8 → #ECEDF3</div>
|
||||
<div class="use">侧栏微渐变(自上而下微沉)</div></div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="chip" style="background:#f4f5f8"></div>
|
||||
<div class="meta"><div class="name">雾灰</div><div class="hex num">#F4F5F8</div>
|
||||
<div class="use">页面底</div></div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="chip" style="background:linear-gradient(180deg,#ffffff,#fcfcfe);border-bottom:1px solid #e3e6ec"></div>
|
||||
<div class="meta"><div class="name">纸白</div><div class="hex num">#FFFFFF → #FCFCFE</div>
|
||||
<div class="use">卡片/表格:顶白底微灰的「受光」渐变</div></div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="chip" style="background:#e4e7ec"></div>
|
||||
<div class="meta"><div class="name">界线</div><div class="hex num">rgba(28,35,51,.09)</div>
|
||||
<div class="use">1px 半透明描边:在不同底色上自然融合</div></div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="chip" style="background:#1c2333"></div>
|
||||
<div class="meta"><div class="name">墨色</div><div class="hex num">#1C2333</div>
|
||||
<div class="use">主文字(冷调近黑)</div></div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="chip" style="background:#5b6577"></div>
|
||||
<div class="meta"><div class="name">次墨</div><div class="hex num">#5B6577</div>
|
||||
<div class="use">次级文字、表头、说明</div></div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="chip" style="background:linear-gradient(90deg,#1e9e6a,#c98a1b,#c94f4f)"></div>
|
||||
<div class="meta"><div class="name">语义色</div><div class="hex num">#1E9E6A · #C98A1B · #C94F4F</div>
|
||||
<div class="use">成功/警示/危险,浅底微渐变 + 深字</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ 字体与数字 ============ -->
|
||||
<section class="section" id="sec-type">
|
||||
<h2>02 · 字体与数字</h2>
|
||||
<div class="type-sheet">
|
||||
<div class="type-row"><span class="label">页面标题</span><span class="t-page-title">岗位角色</span><span class="label" style="width:auto">18px / 600 · 字距 −0.01em</span></div>
|
||||
<div class="type-row"><span class="label">卡片标题</span><span class="t-card-title">计提参数 · 业务员个人户</span><span class="label" style="width:auto">15px / 600</span></div>
|
||||
<div class="type-row"><span class="label">正文 / 表格</span><span class="t-body">回款天数 = 发货日首日起算,按自然日累计</span><span class="label" style="width:auto">14px / 400 · 13px / 400</span></div>
|
||||
<div class="type-row"><span class="label">表头 / 标签</span><span class="t-th">岗位编码 公式模板 生效日期</span><span class="label" style="width:auto">12px / 500 · 字距 +0.04em</span></div>
|
||||
<div class="type-row"><span class="label">品牌字</span><span style="font-weight:600;letter-spacing:.35em;font-size:15px">衍水集团</span><span class="label" style="width:auto">无衬线 600 · 大字距 · 仅品牌落款一处</span></div>
|
||||
</div>
|
||||
<div class="amount-demo">
|
||||
<table>
|
||||
<thead><tr><th>回款日期</th><th>回款金额(元)</th><th>回款天数</th><th>计提比例</th><th>绩效金额(元)</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>2026-01-10</td><td>386,200.00</td><td>27</td><td>9.7‰</td><td>14,062.31</td></tr>
|
||||
<tr><td>2026-02-18</td><td>1,240,000.00</td><td>361</td><td>4‰</td><td>19,840.00</td></tr>
|
||||
<tr><td>合计</td><td>1,626,200.00</td><td>—</td><td>—</td><td>33,902.31</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="note">金额与比例一律<b>等宽数字(tabular-nums)右对齐</b>,合计行加粗放大半档——列对列、位对位,扫一眼就能核对。</div>
|
||||
</div>
|
||||
<div class="brand-candidates">
|
||||
<div class="bc">
|
||||
<div style="font-family:'Noto Sans SC',sans-serif;font-weight:500;letter-spacing:.32em;font-size:16px">衍水集团</div>
|
||||
<div class="bc-cap">A · 思源黑体 500 · 0.32em <span class="bc-rec">当前应用</span></div>
|
||||
</div>
|
||||
<div class="bc">
|
||||
<div style="font-family:'Noto Sans SC',sans-serif;font-weight:300;letter-spacing:.42em;font-size:16px">衍水集团</div>
|
||||
<div class="bc-cap">B · 思源黑体 300 细体 · 0.42em</div>
|
||||
</div>
|
||||
<div class="bc">
|
||||
<div style="font-family:'Microsoft YaHei',sans-serif;font-weight:600;letter-spacing:.25em;font-size:16px">衍水集团</div>
|
||||
<div class="bc-cap">C · 微软雅黑 600 · 0.25em</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ 组件 ============ -->
|
||||
<section class="section" id="sec-comp">
|
||||
<h2>03 · 组件基调</h2>
|
||||
<div class="comp-grid">
|
||||
<div class="comp-card">
|
||||
<h3>按钮 · 三态</h3>
|
||||
<div class="states">
|
||||
<div class="state"><button class="btn btn-primary btn-ghost-hover">新增岗位</button><div class="cap">静置</div></div>
|
||||
<div class="state"><button class="btn btn-primary" style="background:linear-gradient(180deg,#31509f,#243c8b);transform:translateY(-1px);box-shadow:inset 0 1px 0 rgba(255,255,255,.18),0 4px 12px rgba(46,75,168,.3)">新增岗位</button><div class="cap">悬停:抬升 1px · 影加深</div></div>
|
||||
<div class="state"><button class="btn btn-primary" style="box-shadow:inset 0 1px 2px rgba(0,0,0,.18)">新增岗位</button><div class="cap">按下:回落 · 内阴影</div></div>
|
||||
</div>
|
||||
<div class="note">微渐变 + 内高光给按钮「实体感」;次级按钮一律白底细边框,无阴影污染。</div>
|
||||
</div>
|
||||
<div class="comp-card">
|
||||
<h3>输入框 · 状态</h3>
|
||||
<div class="row">
|
||||
<input class="input" placeholder="搜索岗位编码或名称" />
|
||||
<input class="input focused" value="4/360" />
|
||||
</div>
|
||||
<div class="row" style="margin-top:14px">
|
||||
<span class="pill pill-ok">启用</span>
|
||||
<span class="pill pill-off">禁用</span>
|
||||
<span class="pill pill-warn">待生效</span>
|
||||
</div>
|
||||
<div class="note">输入框带微内阴影(凹陷暗示);聚焦 = 靛蓝描边 + 3px 柔光外环。</div>
|
||||
</div>
|
||||
<div class="comp-card">
|
||||
<h3>分页</h3>
|
||||
<div class="pager">
|
||||
<span class="pg">‹</span><span class="pg cur">1</span><span class="pg">2</span><span class="pg">3</span><span class="pg">›</span>
|
||||
<span style="margin-left:10px">共 28 条 · 每页 10 条</span>
|
||||
</div>
|
||||
<div class="note" style="margin-top:14px">当前页与主按钮同材质:微渐变 + 内高光。</div>
|
||||
</div>
|
||||
<div class="comp-card">
|
||||
<h3>动效原则</h3>
|
||||
<div class="note">
|
||||
过渡 120–180ms,<b>只做颜色、阴影与 1px 内的位移</b>;弹层淡入 + 4px 上移;
|
||||
数字与表格不加动画;尊重 <b>prefers-reduced-motion</b>。
|
||||
动效的目标是「顺滑可信」,不是表演。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ 应用界面 ============ -->
|
||||
<section class="section" id="sec-app">
|
||||
<h2>04 · 应用界面(布局与列表页)</h2>
|
||||
<div class="app-shot">
|
||||
<div class="app">
|
||||
<aside class="aside">
|
||||
<div class="logo">衍水集团</div>
|
||||
<div class="logo-sub">营销人员工资管理系统</div>
|
||||
<div class="waterline" style="width:22px;margin:10px 10px 0;"></div>
|
||||
<div class="menu-group">
|
||||
<div class="menu-item">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"><path d="M2.5 7 8 2.5 13.5 7v6.5H10V9.5H6v4H2.5Z"/></svg>
|
||||
首页
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-group">
|
||||
<div class="menu-label">系统管理</div>
|
||||
<div class="menu-item">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><circle cx="8" cy="5.5" r="2.5"/><path d="M3 13.5c0-2.5 2.2-4 5-4s5 1.5 5 4"/></svg>
|
||||
用户管理
|
||||
</div>
|
||||
<div class="menu-item">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"><path d="M8 2 13 4v4c0 3-2 5-5 6-3-1-5-3-5-6V4Z"/></svg>
|
||||
角色管理
|
||||
</div>
|
||||
<div class="menu-item active">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><rect x="2.5" y="3.5" width="11" height="9" rx="1.5"/><circle cx="6" cy="7.5" r="1.2"/><path d="M4.5 11c.3-1 1-1.5 1.5-1.5s1.2.5 1.5 1.5M9.5 6.5h2M9.5 9h2"/></svg>
|
||||
岗位角色
|
||||
</div>
|
||||
<div class="menu-item">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M3 5h10M3 11h10"/><circle cx="6" cy="5" r="1.6"/><circle cx="10" cy="11" r="1.6"/></svg>
|
||||
计提参数
|
||||
</div>
|
||||
<div class="menu-item">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M4 4.5h8M4 8h8M4 11.5h5"/></svg>
|
||||
操作日志
|
||||
</div>
|
||||
</div>
|
||||
<div class="aside-foot">
|
||||
<div class="waterline" style="width:22px;margin:0 0 8px;"></div>
|
||||
<div class="aside-foot-text">内部系统 · v0.1</div>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="main">
|
||||
<div class="topbar">
|
||||
<div class="crumb">系统管理 <span>/</span> <b>岗位角色</b></div>
|
||||
<div class="who"><span class="avatar">管</span><span>管理员</span><span class="logout">退出登录</span></div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>岗位角色</h1>
|
||||
<div class="desc">决定人员适用的计提公式模板与参数;新增岗位无需改代码</div>
|
||||
<div class="waterline"></div>
|
||||
</div>
|
||||
<button class="btn btn-primary">新增岗位</button>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<input class="input" placeholder="岗位编码或名称" />
|
||||
<input class="input" placeholder="状态:全部" style="width:130px" />
|
||||
<button class="btn btn-primary">查询</button>
|
||||
<button class="btn btn-default">重置</button>
|
||||
<span class="spacer"></span>
|
||||
</div>
|
||||
<div class="card">
|
||||
<table class="grid">
|
||||
<thead>
|
||||
<tr><th>岗位编码</th><th style="width:110px">岗位名称</th><th>公式模板</th><th>说明</th><th style="width:90px">状态</th><th style="width:150px">操作</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="cell-code">salesperson</td><td>营销员</td>
|
||||
<td class="cell-muted">业务员个人户 · 天数线性递减</td>
|
||||
<td class="cell-muted">回款天数线性递减(360 天=6‰,361 天起 4‰)</td>
|
||||
<td><span class="pill pill-ok">启用</span></td>
|
||||
<td class="cell-actions"><a>编辑</a><a class="muted" title="内置岗位不可删除">删除</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="cell-code">sales_manager</td><td>营销经理</td>
|
||||
<td class="cell-muted">营销经理 · 团队与全公司分成</td>
|
||||
<td class="cell-muted">本团队 12.5% + 全公司 5%</td>
|
||||
<td><span class="pill pill-ok">启用</span></td>
|
||||
<td class="cell-actions"><a>编辑</a><a class="muted" title="内置岗位不可删除">删除</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="cell-code">info_officer</td><td>信息员</td>
|
||||
<td class="cell-muted">信息员 · 销售额固定比例</td>
|
||||
<td class="cell-muted">本人销售额 × 1‰</td>
|
||||
<td><span class="pill pill-ok">启用</span></td>
|
||||
<td class="cell-actions"><a>编辑</a><a class="muted" title="内置岗位不可删除">删除</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="cell-code">senior_sales</td><td>高级业务员</td>
|
||||
<td class="cell-muted">业务员个人户 · 天数线性递减</td>
|
||||
<td class="cell-muted">自定义岗位(新增岗位无需改代码)</td>
|
||||
<td><span class="pill pill-off">禁用</span></td>
|
||||
<td class="cell-actions"><a>编辑</a><a>删除</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="card-foot">
|
||||
<div class="pager">
|
||||
<span class="pg">‹</span><span class="pg cur">1</span><span class="pg">›</span>
|
||||
<span style="margin-left:10px">共 4 条</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="note">
|
||||
质感的三层材料:<b>侧栏</b>灰渐变(沉)→ <b>内容底</b>雾灰 + 顶部受光 → <b>卡片</b>纸白微渐变 + 半透明描边 + 分层阴影。
|
||||
当前项白底浮起,白色被灰托住,不再扎眼。
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ 首页仪表盘 ============ -->
|
||||
<section class="section" id="sec-bento">
|
||||
<h2>05 · 首页仪表盘(Bento Grid)</h2>
|
||||
<div class="app-shot">
|
||||
<div class="app">
|
||||
<aside class="aside">
|
||||
<div class="logo">衍水集团</div>
|
||||
<div class="logo-sub">营销人员工资管理系统</div>
|
||||
<div class="waterline" style="width:22px;margin:10px 10px 0;"></div>
|
||||
<div class="menu-group">
|
||||
<div class="menu-item active">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"><path d="M2.5 7 8 2.5 13.5 7v6.5H10V9.5H6v4H2.5Z"/></svg>
|
||||
首页
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-group">
|
||||
<div class="menu-label">系统管理</div>
|
||||
<div class="menu-item">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><circle cx="8" cy="5.5" r="2.5"/><path d="M3 13.5c0-2.5 2.2-4 5-4s5 1.5 5 4"/></svg>
|
||||
用户管理
|
||||
</div>
|
||||
<div class="menu-item">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"><path d="M8 2 13 4v4c0 3-2 5-5 6-3-1-5-3-5-6V4Z"/></svg>
|
||||
角色管理
|
||||
</div>
|
||||
<div class="menu-item">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><rect x="2.5" y="3.5" width="11" height="9" rx="1.5"/><circle cx="6" cy="7.5" r="1.2"/><path d="M4.5 11c.3-1 1-1.5 1.5-1.5s1.2.5 1.5 1.5M9.5 6.5h2M9.5 9h2"/></svg>
|
||||
岗位角色
|
||||
</div>
|
||||
<div class="menu-item">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M3 5h10M3 11h10"/><circle cx="6" cy="5" r="1.6"/><circle cx="10" cy="11" r="1.6"/></svg>
|
||||
计提参数
|
||||
</div>
|
||||
<div class="menu-item">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M4 4.5h8M4 8h8M4 11.5h5"/></svg>
|
||||
操作日志
|
||||
</div>
|
||||
</div>
|
||||
<div class="aside-foot">
|
||||
<div class="waterline" style="width:22px;margin:0 0 8px;"></div>
|
||||
<div class="aside-foot-text">内部系统 · v0.1</div>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="main">
|
||||
<div class="topbar">
|
||||
<div class="crumb"><b>仪表盘</b></div>
|
||||
<div class="who"><span class="avatar">管</span><span>管理员</span><span class="logout">退出登录</span></div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>仪表盘</h1>
|
||||
<div class="desc">数据截至今日 06:00 · 12 笔回款待核算</div>
|
||||
<div class="waterline"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bento">
|
||||
<div class="bento-card b-2x2">
|
||||
<div class="b-eyebrow">本月绩效总额</div>
|
||||
<div class="b-big" style="color:var(--ys-indigo)">268,430.16<small>元</small></div>
|
||||
<div class="b-sub">较上月 <span class="b-up">+12.4%</span> · 已核销 86 笔</div>
|
||||
<div id="chartPerf" style="width:100%;height:40px;margin-top:12px"></div>
|
||||
</div>
|
||||
<div class="bento-card b-2x2">
|
||||
<div class="b-eyebrow">近 6 个月回款</div>
|
||||
<div class="b-sub" style="margin-top:2px">单位:万元</div>
|
||||
<div id="chartRev" style="width:100%;height:78px;margin-top:auto"></div>
|
||||
</div>
|
||||
<div class="bento-card b-2x1">
|
||||
<div class="b-eyebrow">待办</div>
|
||||
<div style="margin-top:auto;display:flex;flex-direction:column;gap:4px">
|
||||
<div class="b-todo"><span class="cnt">12 笔</span>回款待核算</div>
|
||||
<div class="b-todo"><span class="cnt">3 笔</span>业务归属待确认</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bento-card">
|
||||
<div class="b-head"><div class="b-eyebrow">营销人员</div>
|
||||
<svg class="b-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><circle cx="8" cy="5.5" r="2.5"/><path d="M3 13.5c0-2.5 2.2-4 5-4s5 1.5 5 4"/></svg>
|
||||
</div>
|
||||
<div class="b-num">36<small>人</small></div>
|
||||
</div>
|
||||
<div class="bento-card">
|
||||
<div class="b-head"><div class="b-eyebrow">岗位角色</div>
|
||||
<svg class="b-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><rect x="2.5" y="3.5" width="11" height="9" rx="1.5"/><circle cx="6" cy="7.5" r="1.2"/><path d="M4.5 11c.3-1 1-1.5 1.5-1.5s1.2.5 1.5 1.5M9.5 6.5h2M9.5 9h2"/></svg>
|
||||
</div>
|
||||
<div class="b-num">3<small>个</small></div>
|
||||
</div>
|
||||
<div class="bento-card b-2x1">
|
||||
<div class="b-eyebrow">常用操作</div>
|
||||
<div class="row" style="margin-top:auto">
|
||||
<button class="btn btn-primary" style="height:30px;font-size:12px;padding:0 13px">录入回款</button>
|
||||
<button class="btn btn-default" style="height:30px;font-size:12px;padding:0 13px">计提参数</button>
|
||||
<button class="btn btn-default" style="height:30px;font-size:12px;padding:0 13px">岗位角色</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bento-card">
|
||||
<div class="b-head"><div class="b-eyebrow">计提参数</div>
|
||||
<svg class="b-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M3 5h10M3 11h10"/><circle cx="6" cy="5" r="1.6"/><circle cx="10" cy="11" r="1.6"/></svg>
|
||||
</div>
|
||||
<div class="b-num">16<small>项</small></div>
|
||||
<div class="b-sub">版本 P20260101-00</div>
|
||||
</div>
|
||||
<div class="bento-card">
|
||||
<div class="b-head"><div class="b-eyebrow">T+ 同步</div>
|
||||
<svg class="b-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M13 8a5 5 0 1 1-1.5-3.5"/><path d="M13 2.5V5h-2.5"/></svg>
|
||||
</div>
|
||||
<div class="b-num" style="color:var(--ys-ok)">正常</div>
|
||||
<div class="b-sub">每周一 06:00</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="note">
|
||||
<b>Bento Grid</b>:大小不一的卡片按重要性排布——绩效总额与回款趋势占 2×2 主角位,数字型指标 1×1 紧凑排列。
|
||||
卡片入场为 <b>45ms 交错上浮(320ms)</b>,悬停抬升 2px、阴影加深——打开预览即可看到微动效。
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ 登录页 ============ -->
|
||||
<section class="section" id="sec-login">
|
||||
<h2>06 · 登录页(重做)</h2>
|
||||
<div class="login-shot">
|
||||
<div class="login-center">
|
||||
<div class="ripple-bg"><i></i><i></i><i></i><i></i></div>
|
||||
<div style="position:relative">
|
||||
<div class="login-card">
|
||||
<div class="lc-brand">
|
||||
<div class="lc-brand-name">衍水集团</div>
|
||||
<div class="lc-slogan">把每一笔回款,算成一份踏实。</div>
|
||||
<div class="waterline" style="width:40px;margin:14px auto 0"></div>
|
||||
</div>
|
||||
<div class="lc-form-head" style="margin-top:28px">欢迎回来</div>
|
||||
<div class="lc-form-hint">请输入账号密码进入系统</div>
|
||||
<div class="field">
|
||||
<label>用户名</label>
|
||||
<input class="input" placeholder="请输入用户名" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>密码</label>
|
||||
<input class="input" type="password" placeholder="请输入密码" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>验证码</label>
|
||||
<div class="captcha-row">
|
||||
<input class="input" placeholder="请输入验证码" />
|
||||
<div class="captcha-box">7K3P</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary login-btn">登 录</button>
|
||||
</div>
|
||||
<div class="lc-foot">© 衍水集团 · 内部系统</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="note">
|
||||
背景三层:<b>点阵网格</b>(秩序)+ <b>顶部靛蓝天光</b>(氛围)+ <b>右上涟漪同心圆</b>(「衍水」的品牌装饰);
|
||||
卡片内品牌区承担门面——品牌字、标语、水线;表单以「欢迎回来」承接。装饰与文案都来自「衍水」本身。
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ 签名元素 ============ -->
|
||||
<section class="section" id="sec-sign">
|
||||
<h2>07 · 签名元素:水线</h2>
|
||||
<div class="comp-card">
|
||||
<div style="font-size:18px;font-weight:600;letter-spacing:-0.01em">计提参数</div>
|
||||
<div class="waterline" style="width:56px"></div>
|
||||
<div class="note" style="max-width:720px">
|
||||
页面标题下的<b>渐隐靛蓝细线(56px,左实右虚)</b>——「衍水」之水的提纯,全站唯一的装饰性元素,
|
||||
只在每个页面标题处出现一次。它与登录页的水纹层理同源:<b>品牌连续,而非推倒重来</b>。
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ 微动效与精致文案(全局规范) ============ -->
|
||||
<section class="section" id="sec-motion">
|
||||
<h2>08 · 微动效与精致文案(全局规范)</h2>
|
||||
<div class="comp-grid">
|
||||
<div class="comp-card">
|
||||
<h3>微动效 · 全站统一</h3>
|
||||
<table class="spec-table">
|
||||
<tr><td>页面 / 卡片入场</td><td>淡入 + 上浮 6px,45ms 交错</td><td class="num">320ms</td></tr>
|
||||
<tr><td>按钮悬停 / 按下</td><td>抬升 1px、影加深 / 回落内阴影</td><td class="num">150ms</td></tr>
|
||||
<tr><td>卡片悬停</td><td>抬升 2px、阴影加深</td><td class="num">180ms</td></tr>
|
||||
<tr><td>输入聚焦</td><td>靛蓝描边 + 3px 柔光外环</td><td class="num">150ms</td></tr>
|
||||
<tr><td>弹层 / 抽屉</td><td>淡入 + 上移 4px</td><td class="num">160ms</td></tr>
|
||||
<tr><td>表格行</td><td>底色渐显(无位移)</td><td class="num">120ms</td></tr>
|
||||
<tr><td>数据刷新</td><td>数字不做滚动动画(核对场景,动即干扰)</td><td class="num">—</td></tr>
|
||||
<tr><td>无障碍</td><td>prefers-reduced-motion 时全部关闭</td><td class="num">—</td></tr>
|
||||
</table>
|
||||
<div class="note">目标不是「有动画」,而是<b>顺滑可信</b>:每次操作都有轻微、及时、可预期的反馈。</div>
|
||||
</div>
|
||||
<div class="comp-card">
|
||||
<h3>精致文案 · 全站统一</h3>
|
||||
<table class="spec-table">
|
||||
<tr><td>按钮</td><td class="bad">提交</td><td class="good">保存变更 · 录入回款</td></tr>
|
||||
<tr><td>空态</td><td class="bad">暂无数据</td><td class="good">还没有岗位角色,点「新增岗位」创建第一个</td></tr>
|
||||
<tr><td>错误</td><td class="bad">操作失败</td><td class="good">岗位编码已存在,请换一个</td></tr>
|
||||
<tr><td>危险确认</td><td class="bad">确定删除吗?</td><td class="good">删除后无法恢复,该岗位下的人员需重新归属</td></tr>
|
||||
<tr><td>成功提示</td><td class="bad">操作成功</td><td class="good">岗位已创建 · 参数 11-01 起生效</td></tr>
|
||||
<tr><td>页面描述</td><td class="bad">本页面用于管理</td><td class="good">决定人员适用的计提公式模板与参数;新增岗位无需改代码</td></tr>
|
||||
</table>
|
||||
<div class="note">说清楚<b>会发生什么</b>;动词开头、句子短;不道歉、不卖萌;<b>空态是邀请,错误是路标</b>。</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
<script src="echarts.min.js"></script>
|
||||
<script>
|
||||
/* 仪表盘图表:ECharts 定制主题(圆角柱 / 无网格线 / 浅灰轴标签)——实施时前端引入 echarts 依赖 */
|
||||
const chartFont = "'PingFang SC','Microsoft YaHei','Noto Sans SC',sans-serif";
|
||||
const rev = echarts.init(document.getElementById('chartRev'), null, { renderer: 'svg' });
|
||||
rev.setOption({
|
||||
grid: { left: 0, right: 0, top: 6, bottom: 20, containLabel: true },
|
||||
xAxis: {
|
||||
type: 'category', data: ['5月', '6月', '7月', '8月', '9月', '10月'],
|
||||
axisLine: { show: false }, axisTick: { show: false },
|
||||
axisLabel: { color: '#9aa3b2', fontSize: 9, margin: 10, fontFamily: chartFont }
|
||||
},
|
||||
yAxis: { type: 'value', show: false },
|
||||
series: [{
|
||||
type: 'bar', barWidth: 22, data: [34, 42, 30, 48, 38, 56],
|
||||
itemStyle: {
|
||||
borderRadius: [3, 3, 0, 0],
|
||||
color: (p) => p.dataIndex === 5
|
||||
? new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: '#3a58bc' }, { offset: 1, color: '#2e4ba8' }])
|
||||
: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: '#d5def3' }, { offset: 1, color: '#e8edf8' }])
|
||||
}
|
||||
}]
|
||||
});
|
||||
const perf = echarts.init(document.getElementById('chartPerf'), null, { renderer: 'svg' });
|
||||
perf.setOption({
|
||||
grid: { left: 0, right: 0, top: 6, bottom: 2 },
|
||||
xAxis: { type: 'category', show: false, boundaryGap: false, data: [1, 2, 3, 4, 5, 6, 7] },
|
||||
yAxis: { type: 'value', show: false, min: 0 },
|
||||
series: [{
|
||||
type: 'line', data: [8, 13, 10, 19, 23, 29, 33], smooth: 0.4, symbol: 'none',
|
||||
lineStyle: { color: '#2e4ba8', width: 1.6 },
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(46,75,168,.10)' }, { offset: 1, color: 'rgba(46,75,168,0)' }
|
||||
])
|
||||
}
|
||||
}]
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
268
docs/superpowers/plans/2026-10-12-M1-系统设置与参数体系.md
Normal file
268
docs/superpowers/plans/2026-10-12-M1-系统设置与参数体系.md
Normal file
@ -0,0 +1,268 @@
|
||||
# M1 开发计划:系统设置与参数体系(#28)
|
||||
|
||||
## Context
|
||||
|
||||
《剩余开发计划》已定稿(`docs/superpowers/plans/2026-10-12-剩余开发计划.md`),M1 是首个工作块。本文档即 M1 的正式开发计划(2026-10-12 批准):涵盖设计(四表/参数/模板/接口)、任务分解(T1–T8 步骤级)、执行安排与验收标准;开工后按第二章逐任务执行。
|
||||
|
||||
**为什么先做 M1**:岗位角色被基础数据的人员管理引用(外键上游);参数快照与公式模板是绩效核算的输入契约。先立地基可避免后续模块加表、加外键、改表单的连锁返工;计算预览还可作为业务对账工具先行验收公式口径。
|
||||
|
||||
**#28 需求原文要点**:① 人员岗位角色(营销员/营销经理/信息员等)支持增删,新增岗位时在系统设置中绑定计提公式与参数,**无需改代码**;② 全部计提比例、系数、阈值、分成比例、起付比例统一在系统设置维护,**支持版本管理与留痕**。
|
||||
|
||||
## 现状(开工前已核查)
|
||||
|
||||
| 项 | 状态 |
|
||||
| --- | --- |
|
||||
| 分支 | `feat/system-params`(基于 `feature/system-management` HEAD;后者领先 main 5 提交、未合并——是否先合并 main 由用户决定,不阻塞开发) |
|
||||
| 容器 | Docker Desktop 位于 `D:\work\app\Docker\Docker Desktop.exe`(非默认路径) |
|
||||
| 工作区 | docs/ 下有用户侧整理改动(业务清单删除、v2 新增、superpowers 目录),与 M1 无关,不动 |
|
||||
| 基线 | 301 用例全绿(系统管理交付时);开工先复跑确认 |
|
||||
|
||||
## 范围
|
||||
|
||||
**做**:四张新表与种子(V3 迁移双轨);岗位角色 CRUD + 模板绑定;参数目录/版本化存储/按日期取值/变更留痕/对比/导出;公式纯函数与模板注册表;计算预览;前端两个页面(岗位角色、计提参数)。
|
||||
|
||||
**不做**:核算引擎与台账(M3);基础数据与业务数据(M2);参数 Redis 缓存(一期不做,保留键位);规则引擎(既有决策)。
|
||||
|
||||
---
|
||||
|
||||
## 一、技术设计
|
||||
|
||||
### 1.1 数据模型(四张新表,V3 迁移)
|
||||
|
||||
**`business_roles` 业务岗位角色表**(与系统登录角色 `roles` 严格区分,表注释写明)
|
||||
|
||||
| 列 | 类型 | 约束/说明 |
|
||||
| --- | --- | --- |
|
||||
| id | BIGINT UNSIGNED | PK AUTO_INCREMENT |
|
||||
| role_code | VARCHAR(32) | NOT NULL,uk;创建后不可改 |
|
||||
| role_name | VARCHAR(64) | NOT NULL |
|
||||
| calc_template | VARCHAR(32) | NOT NULL,公式模板编码(以代码注册表为准) |
|
||||
| description | VARCHAR(255) | NULL |
|
||||
| sort_order | INT | NOT NULL DEFAULT 0 |
|
||||
| status | TINYINT | NOT NULL DEFAULT 1(1 启用 0 禁用) |
|
||||
| is_builtin | TINYINT | NOT NULL DEFAULT 0(1 内置禁删) |
|
||||
| create_time / update_time | DATETIME | TimestampMixin(update 用 `text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP")`) |
|
||||
|
||||
**`param_definitions` 参数定义表**:`param_code` VARCHAR(64) uk;`param_name` VARCHAR(64);`param_group` VARCHAR(32);`value_type` VARCHAR(16)(DECIMAL/INT/BOOL/ENUM/TEXT);`unit` VARCHAR(16)(‰/%/元/天);`dimension_type` VARCHAR(32) DEFAULT 'NONE'(NONE/PRODUCT_CATEGORY/BUSINESS_ROLE);`options_text` VARCHAR(255)(枚举竖线分隔);`default_value`/`value_min`/`value_max` VARCHAR(64)(Fraction 可解析字符串);`description` VARCHAR(255);`sort_order` INT;`status` TINYINT;TimestampMixin。
|
||||
|
||||
**`param_change_batches` 参数变更批次表**:`version_no` VARCHAR(32) uk(`P{YYYYMMDD}-{当日序号}`);`effective_date` DATE NOT NULL idx;`change_type` TINYINT(1 立即 2 指定日期);`remark` VARCHAR(255);`operator_id` FK→users ON DELETE SET NULL + `operator_name` VARCHAR(64) 冗余固化;`item_count` INT DEFAULT 0;仅 `create_time`(append-only,不继承 TimestampMixin)。
|
||||
|
||||
**`param_values` 参数版本条目表(append-only)**:`param_id` FK→param_definitions RESTRICT idx;`dimension_value` VARCHAR(32) NOT NULL DEFAULT `''`(用空串不用 NULL);`effective_date` DATE NOT NULL idx;`value_text` VARCHAR(64) NOT NULL;`old_value_text` VARCHAR(64);`batch_id` FK→param_change_batches RESTRICT idx;仅 `create_time`。热路径索引 `idx_param_values_param_id_dimension_value_effective_date`。已生效条目禁改禁删;未生效批次可整批撤回。
|
||||
|
||||
**种子**(迁移 + init-dev 双写):3 个内置岗位(`salesperson 营销员`/TPL_SALESPERSON_DECAY、`sales_manager 营销经理`/TPL_MANAGER_SHARE、`info_officer 信息员`/TPL_INFO_SALES_FIXED)+ 16 项参数定义 + 版本条目 + 一条初始批次(`P20260101-00`)。
|
||||
|
||||
### 1.2 参数清单(16 项)
|
||||
|
||||
| param_code | 名称 | 类型/单位 | 维度 | 默认值 | 生效日期 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| salesperson.rate.start | 业务员起始计提比例 | DECIMAL ‰ | BUSINESS_ROLE | 10 | 2026-02-17 |
|
||||
| salesperson.rate.slope | 业务员日递减斜率 | DECIMAL ‰/天 | BUSINESS_ROLE | **4/360** | 2026-02-17 |
|
||||
| salesperson.rate.floor | 业务员计提下限 | DECIMAL ‰ | BUSINESS_ROLE | 4 | 2026-02-17 |
|
||||
| salesperson.days.boundary | 线性递减分界天数 | INT 天 | BUSINESS_ROLE | 360 | 2026-02-17 |
|
||||
| manager.share.team | 经理本团队分成 | DECIMAL % | BUSINESS_ROLE | 12.5 | 2026-08-13 |
|
||||
| manager.share.company | 经理全公司分成 | DECIMAL % | BUSINESS_ROLE | 5 | 2026-08-13 |
|
||||
| enterprise.rate | 企业户计提比例 | DECIMAL ‰ | PRODUCT_CATEGORY | **2(02-17 起)/ 3(08-13 起)** | 双行 |
|
||||
| info.rate | 信息员计提比例 | DECIMAL ‰ | BUSINESS_ROLE | 1 | 2026-08-13 |
|
||||
| prepay.rate | 预付款计提比例 | DECIMAL ‰ | NONE | 10 | 2026-01-01 |
|
||||
| barter.discount | 以物抵顶折算比例 | DECIMAL % | NONE | 80 | 2026-01-01 |
|
||||
| payment.start.ratio | 支付起付比例 | DECIMAL % | NONE | 60 | 2026-01-01 |
|
||||
| payment.weight.mode | 支付权重应用方式 | ENUM FULL\|BALANCE | NONE | FULL | 2026-01-01 |
|
||||
| product.threshold | 折算毛利润额阈值 | DECIMAL % | PRODUCT_CATEGORY | 市政水利 30 / 轨交 20 | 2026-02-17 |
|
||||
| salary.basic.mode | 月工资基本工资口径 | ENUM INCLUDE\|DEDUCT\|EXCLUDE | BUSINESS_ROLE | 业务员 INCLUDE / 经理 DEDUCT | 2026-01-01 |
|
||||
| probation.annual.threshold | 试用期年累计结算阈值 | DECIMAL 元 | NONE | 18000 | 2026-01-01 |
|
||||
| probation.salary.basic | 试用期基本工资标准 | DECIMAL 元 | NONE | 3000 | 2026-01-01 |
|
||||
|
||||
**不设参数**(作废条款落地):经理个人户 8‰ 起始/斜率;协助 25% 分成;含运费口径开关。
|
||||
|
||||
**值校验规则**(`validate_and_normalize`):DECIMAL 用 `Fraction(text)` 解析(拒绝 `1/0`、非数字),并按 `value_min`/`value_max` 校验;INT 必须整数;BOOL 仅 0/1;ENUM 必须在 `options_text` 内;规范化去首尾空白、分数式原样存储。
|
||||
|
||||
### 1.3 公式模板与消费契约
|
||||
|
||||
**模板注册表**(`app/services/calc_templates.py`,代码内有限集合,一期不引入规则引擎的落地方式):
|
||||
|
||||
| 模板编码 | 形态 | 声明参数 |
|
||||
| --- | --- | --- |
|
||||
| TPL_SALESPERSON_DECAY | 分段:≤分界 = 起始 − 天数×斜率;> 分界 = 下限 | salesperson.rate.* / days.boundary |
|
||||
| TPL_MANAGER_SHARE | 聚合:∑本团队×12.5% + ∑全公司×5% | manager.share.* |
|
||||
| TPL_ENTERPRISE_FIXED | 乘法:折算毛利润额 × 比例 | enterprise.rate |
|
||||
| TPL_INFO_SALES_FIXED | 乘法:本人销售额 × 比例 | info.rate |
|
||||
| TPL_PREPAY_FIXED | 乘法:回款额 × 比例 | prepay.rate |
|
||||
|
||||
修饰器(不绑角色,参数驱动):以物抵顶 ×80%、支付权重 R×R、起付 60%、试用期缓发。**绑定模型**:`business_roles.calc_template` → 模板;角色级覆盖经 `dimension_type=BUSINESS_ROLE` 条目;新增岗位 = 新增行 + 选模板(零代码)。
|
||||
|
||||
**消费接口契约**(M3 核算与预览共用):
|
||||
|
||||
```python
|
||||
# app/services/param_service.py
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ParamSnapshot:
|
||||
biz_date: date
|
||||
def get(self, code, *, role_code=None, product_category=None) -> str: ... # 覆盖链:角色 > 产品类别 > 默认
|
||||
def fraction(self, code, **dims) -> Fraction: ...
|
||||
def integer(self, code, **dims) -> int: ...
|
||||
def boolean(self, code, **dims) -> bool: ...
|
||||
def text(self, code, **dims) -> str: ...
|
||||
|
||||
def resolve_snapshot(session, biz_date: date) -> ParamSnapshot
|
||||
def submit_change(session, payload, *, operator, meta=None) -> ParamChangeBatchOut
|
||||
def compare(session, *, from_date: date, to_date: date) -> list[ParamDiffOut]
|
||||
|
||||
# app/services/formula.py(纯函数,禁 float)
|
||||
def payment_days(ship_date, receipt_date) -> int # 发货日首日=1
|
||||
def salesperson_rate(days, *, start, slope, floor, boundary_days) -> Fraction
|
||||
def profit_rate(profit: Decimal, revenue: Decimal) -> Fraction # 不含运费;revenue=0 防除零
|
||||
def converted_profit(revenue, rate, threshold) -> Decimal # 化简式:销售额 ×(毛利润率−阈值)
|
||||
def performance_amount(base, rate) -> Decimal # 乘法方向(对照表 #3)
|
||||
def apply_barter(amount, discount) -> Decimal
|
||||
def payment_weight(r: int) -> Fraction # r×r
|
||||
def start_payment_ok(received, revenue, ratio) -> bool
|
||||
```
|
||||
|
||||
### 1.4 API 设计(/api/v1/system/*,AdminUser)
|
||||
|
||||
**岗位角色**:`GET/POST /business-roles`、`GET/PUT/DELETE /business-roles/{id}`、`PUT /business-roles/{id}/status`、`GET /business-roles/meta/templates`(模板下拉,含声明参数说明)。
|
||||
|
||||
**计提参数**:`GET /params`(目录+当前快照,支持 `asOf`)、`GET /params/{param_code}/history`、`POST /params/changes`(提交变更批次)、`DELETE /params/changes/{batch_id}`(撤回未生效)、`GET /params/changes`(记录分页)、`GET /params/changes/{batch_id}`(明细)、`GET /params/compare`(两日期对比)、`GET /params/export`(Excel,`StreamingResponse` 非统一信封)、`POST /params/preview`(计算预览)。
|
||||
|
||||
**关键示例 1:提交变更**
|
||||
|
||||
```json
|
||||
// POST /api/v1/system/params/changes
|
||||
{
|
||||
"changeType": 2,
|
||||
"effectiveDate": "2026-11-01",
|
||||
"remark": "四季度系数调整",
|
||||
"items": [
|
||||
{ "paramCode": "enterprise.rate", "dimensionValue": "", "value": "3" },
|
||||
{ "paramCode": "product.threshold", "dimensionValue": "rail_transit", "value": "22" }
|
||||
]
|
||||
}
|
||||
// data:
|
||||
{ "batchId": 12, "versionNo": "P20261012-01", "effectiveDate": "2026-11-01",
|
||||
"changeType": 2, "itemCount": 2, "operatorName": "admin", "createTime": "..." }
|
||||
```
|
||||
|
||||
**关键示例 2:计算预览**(示意值已验算)
|
||||
|
||||
```json
|
||||
// POST /api/v1/system/params/preview
|
||||
{ "businessRole": "salesperson", "customerType": "personal",
|
||||
"productCategory": "municipal_water", "shipDate": "2025-12-15",
|
||||
"receiptDate": "2026-01-10", "revenue": "100000", "profit": "45000",
|
||||
"receivedAmount": "60000", "barter": false, "probation": false }
|
||||
// data:
|
||||
{ "paymentDays": 27, "rate": "97/10000", "rateText": "9.7‰(10‰ − 27 天 × 4/360‰)",
|
||||
"profitRate": "9/20", "convertedProfit": "15000", "performanceAmount": "145.5",
|
||||
"startPaymentOk": true, "paymentWeightR": 60, "paymentWeight": "9/25",
|
||||
"payableAmount": "52.38", "paramVersion": "P20260101-00", "paramEffectiveDate": "2026-02-17" }
|
||||
```
|
||||
|
||||
### 1.5 前端设计
|
||||
|
||||
**岗位角色页**(`views/system/business-roles/index.vue`):列表(复用 `usePagedList`/`AppPagination`/`StatusTag`)+ 新增/编辑弹窗(模板下拉,选中后展示模板声明的参数清单与当前值)+ 启停/删除(内置岗位按钮禁用并 title 提示原因)。路由 meta `{ title: '岗位角色', group: '系统管理', requiresAdmin: true, order: 5 }`。
|
||||
|
||||
**计提参数页**(`views/system/params/index.vue`):按 `param_group` 分组展示(名称/当前值+单位/生效日期/口径说明);编辑后进入「待提交」区,顶部选生效方式(立即/指定日期)+ 备注;「提交变更」二次确认(列出每项 旧值→新值);版本管理抽屉(变更记录分页 + 批次明细 + 两日期对比 + Excel 导出下载);预览抽屉(表单 → 结果卡,注明「斜率精确值 4/360」)。路由 meta order 6。导出接口走 axios 实例取 blob(非统一信封,api 文件内注明)。
|
||||
|
||||
---
|
||||
|
||||
## 二、任务分解(T1–T8)
|
||||
|
||||
任务依赖:T1 → T2 → T3 → (T4 ∥ T5) → (T6 ∥ T7) → T8。每任务遵循「写测试 → 确认失败 → 实现 → 确认通过 → commit」。
|
||||
|
||||
### T1 迁移与模型(四表 + 种子)
|
||||
|
||||
**文件**:`app/models/business_role.py`、`app/models/param.py`(创建);`app/models/__init__.py`(改);`alembic/versions/xxxx_*.py`(autogenerate+种子);`db/migration/V3__add_business_roles_and_params.sql`、`db/init/init-dev.sql`(改)、`db/queries/计提参数.sql`(创建)。
|
||||
|
||||
- [ ] 写两个模型文件(参照 `app/models/role.py` 写法;`ParamChangeBatch`/`ParamValue` 不继承 TimestampMixin)
|
||||
- [ ] 注册进 `models/__init__.py`(漏注册会让 autogenerate 生成删表迁移)
|
||||
- [ ] `uv run alembic revision --autogenerate -m "新增岗位角色与计提参数四表"` → 人工检查命名/注释/ON UPDATE → 补种子(3 岗位/16 定义/版本条目/初始批次)
|
||||
- [ ] `uv run alembic upgrade head` → `uv run alembic upgrade <V2_rev>:head --sql > db/migration/V3__*.sql`
|
||||
- [ ] 同步 `db/init/init-dev.sql`(建表 + 种子 + alembic_version 更新)
|
||||
- [ ] 写 `db/queries/计提参数.sql`(时点化解析与对比查询留档)
|
||||
- [ ] 验证:`uv run pytest tests/integration/test_schema_sync.py tests/unit/test_models.py -q` 全绿
|
||||
- [ ] Commit:`feat(system): 新增岗位角色与计提参数四表及 V3 迁移`
|
||||
|
||||
### T2 参数注册表 + 公式纯函数
|
||||
|
||||
**文件**:`app/services/param_registry.py`、`calc_templates.py`、`formula.py`(创建);`app/core/constants.py`(改);`tests/unit/test_formula.py`、`test_param_registry.py`。
|
||||
|
||||
- [ ] 写失败测试(关键断言):`salesperson_rate(360,...) == Fraction(6,1000)`;`361 天 == Fraction(4,1000)`;`第 1 天 == Fraction(10) - Fraction(1,90)`;`payment_days(2025-12-15, 2026-01-10) == 27`;同日回款 == 1;`converted_profit(0,...) == 0`(防除零);负绩效不归零;`payment_weight(60) == Fraction(36,100)`;`parse_fraction("4/360") == Fraction(1,90)`;注册表 16 项且模板声明参数均存在
|
||||
- [ ] 运行确认失败 → 实现三个文件 → 运行 `uv run pytest tests/unit -q` 全绿
|
||||
- [ ] Commit:`feat(system): 计提公式模板注册表与精确分数计算函数`
|
||||
|
||||
### T3 参数仓储 + 取值服务(时点化解析)
|
||||
|
||||
**文件**:`app/repositories/param_repository.py`、`app/services/param_service.py`(解析部分);`tests/unit/test_param_snapshot.py`。
|
||||
|
||||
- [ ] 写失败测试:覆盖链(角色 > 产品类别 > 默认);未定义参数按注册表默认值兜底;同日多次变更取 id 最大
|
||||
- [ ] 实现 `resolve_values(session, biz_date)`(一条 SQL 取全量快照)+ `ParamSnapshot`/`resolve_snapshot` → `uv run pytest tests/unit -q` 全绿
|
||||
- [ ] Commit:`feat(system): 计提参数按业务日期时点化取值服务`
|
||||
|
||||
### T4 岗位角色 CRUD 后端(含审计与保护规则)
|
||||
|
||||
**文件**:`app/repositories/business_role_repository.py`、`app/schemas/business_role.py`、`app/services/business_role_service.py`、`app/api/v1/system/business_roles.py`(创建);`app/api/router.py`、`app/core/constants.py`(改)。整体参照 `roles` 模块模式。
|
||||
|
||||
- [ ] 实现(保护规则:内置禁删禁停、code 不可改、模板必须存在于注册表、人员引用计数钩子预留;写操作包 `audit_service.audit()`)
|
||||
- [ ] 冒烟:新增自定义岗位 ✓、删除内置 → 403、非法模板 → 400
|
||||
- [ ] Commit:`feat(system): 业务岗位角色管理接口与保护规则`
|
||||
|
||||
### T5 参数变更/历史/对比/导出/预览后端
|
||||
|
||||
**文件**:`app/schemas/param.py`、`app/api/v1/system/params.py`(创建);`param_service.py` 扩展、`app/api/router.py`、`constants.py`(改);`pyproject.toml`(`uv add openpyxl`)。
|
||||
|
||||
- [ ] 实现 9 个端点(提交/撤回/历史/分页/明细/对比/导出/预览/目录快照);撤回已生效批次 → 400;导出列含变更人/时间/项/旧值/新值/生效日期
|
||||
- [ ] 冒烟链路:提交(指定未来日期)→ 当前快照不变 → 立即生效提交 → 快照变化 → 撤回未生效 → 回退 → 预览 360/361 → 导出可打开
|
||||
- [ ] Commit:`feat(system): 计提参数版本管理、变更留痕与计算预览接口`
|
||||
|
||||
### T6 前端岗位角色页
|
||||
|
||||
**文件**:`src/api/businessRoles.ts`、`src/views/system/business-roles/index.vue`(创建);`router/index.ts`、`utils/constants.ts`(改)。
|
||||
|
||||
- [ ] 实现页面与路由 → `pnpm type-check && pnpm build` 通过
|
||||
- [ ] Commit:`feat(system): 前端岗位角色管理页面`
|
||||
|
||||
### T7 前端参数配置页(版本管理 + 预览)
|
||||
|
||||
**文件**:`src/api/params.ts`、`src/views/system/params/index.vue`(创建);`router/index.ts`、`utils/constants.ts`(改)。
|
||||
|
||||
- [ ] 实现页面(分组表/生效方式/二次确认/版本抽屉/预览抽屉)→ `pnpm type-check` 通过
|
||||
- [ ] e2e 链路:改参数 → 变更记录出现 → 预览值变化 → 导出可下载
|
||||
- [ ] Commit:`feat(system): 前端计提参数配置与版本管理页面`
|
||||
|
||||
### T8 模块测试统一编写与文档
|
||||
|
||||
**文件**:`tests/unit/test_param_service.py`、`tests/integration/test_business_roles.py`、`test_param_management.py`(创建);`backend/tests/README.md`、`frontend/README.md`、`backend/README.md`、`frontend/e2e/*`(改)。
|
||||
|
||||
- [ ] 集成用例:参数精度全链路(注册表→种子→解析→预览一致);未来生效不泄漏;撤回回退;同日多次变更;审计留痕;权限 403;岗位保护规则
|
||||
- [ ] 全量回归 `uv run pytest -q`(301 基线 + 新增全绿);e2e;三份 README 更新
|
||||
- [ ] Commit:`test(system): 参数体系模块用例与测试记录`
|
||||
|
||||
---
|
||||
|
||||
## 三、执行安排
|
||||
|
||||
- **分支**:`feat/system-params`(已创建);`feature/system-management` 是否先合并 main 由用户决定,不阻塞
|
||||
- **环境**:启动 `D:\work\app\Docker\Docker Desktop.exe` → `docker compose -f docker/docker-compose.dev.yml up -d`;T2 纯函数开发不依赖容器
|
||||
- **方式**:原生执行(当前会话逐任务推进);每任务后跑验证命令;T8 全量回归;收尾一轮 code review
|
||||
- **注意**:Windows uvicorn `--reload` 偶发卡死——验证 reload 以「新 worker 的 startup complete」为准;开放问题 7 项(见下)按默认处理,不阻塞
|
||||
|
||||
## 四、验收标准(M1 完成的定义)
|
||||
|
||||
1. 管理员可新增岗位并绑定公式模板(无需改代码);内置岗位禁删禁停
|
||||
2. 任一参数变更产生含变更人/时间/项/旧值/新值/生效日期的记录(批次 + 操作日志双保险)
|
||||
3. 任意业务日期可解析出当时快照(08-12 企业户=2‰、08-13=3‰ 回溯正确)
|
||||
4. 计算预览精确复现:360 天=6‰、361 天=4‰、12-15→01-10=27 天
|
||||
5. `uv run pytest -q` 301 基线 + 新增全绿;`pnpm type-check && pnpm build` 通过;e2e 链路通过
|
||||
6. `db/migration/V3__*.sql` 与 `db/init/init-dev.sql` 双轨同步(`test_schema_sync.py` 把关)
|
||||
|
||||
## 五、风险与开放问题
|
||||
|
||||
**风险(已定对策)**:参数缓存一期不做(保留 `ys:param.ver:*` 键位,未来按批次版本号精确失效);同日多次变更 UI 明示「按最新一条生效」;`4/360` 展示注明精确值;模板扩展摩擦一期接受(受控下拉);预览出参与需规 2.4.1 明细列同名(减少 M3 映射);openpyxl 在 T5 增补依赖。
|
||||
|
||||
**开放问题(不阻塞,按默认处理)**:① 第 1 天 10‰ vs 9.9889‰ → 按公式实现并参数化;② 企业户 2‰ 历史种子 → 已双种子;③ BALANCE 模式语义 → M3 前确认;④ `probation.salary.basic` 归属 → M2 时复核;⑤ 参数查询权限 → 暂 AdminUser;⑥ 导出列集合 → 以需规 2.3.1(四) 为准;⑦ 经理个人户业绩进入 12.5%+5% 的衔接 → **M3 开工前必须确认**。
|
||||
|
||||
## 六、验证方式
|
||||
|
||||
1. 按 T1–T8 的验证节点逐任务执行(见第二章各任务);T8 后全量回归 + e2e;
|
||||
2. 验收标准 6 条逐条核对(见第四章);
|
||||
3. 与 `ai-aux/AI_CONTEXT.md` 需求裁决节交叉核对口径无冲突。
|
||||
607
docs/superpowers/plans/2026-10-12-剩余开发计划.md
Normal file
607
docs/superpowers/plans/2026-10-12-剩余开发计划.md
Normal file
@ -0,0 +1,607 @@
|
||||
# 剩余开发计划(路线图 + 首工作块实施计划)
|
||||
|
||||
> **面向 AI 代理的工作者:** 首工作块(M1)实施时使用 subagent-driven-development(推荐)或 executing-plans 逐任务实现本计划第二章;步骤使用复选框(`- [ ]`)语法跟踪进度。第一章路线图供全局导航。
|
||||
|
||||
**目标:** 在需求裁决全部落定(2026-10-12)后,为剩余 5 大模块与 T+ 同步给出推进路线;并交付首个工作块「系统设置与参数体系(#28)」的可执行实施计划(任务级,含测试与验证)。
|
||||
|
||||
**架构:** 沿用现有六层(api → services → repositories → models,schemas 承载请求/响应,core 横切);新增「公式模板注册表(代码内有限集合)+ 参数按生效日期时点化存储 + 计算预览」作为核算模块的输入契约,不引入规则引擎。
|
||||
|
||||
**技术栈:** 后端 Python 3.12 + FastAPI + SQLAlchemy 2.0(同步)+ MySQL 8.0 + Redis 7;前端 Vue 3.5 + TS 5.9 + Vite 8 + Element Plus 2.14 + Pinia 4。
|
||||
|
||||
**规格:** `ai-aux/AI_CONTEXT.md`「需求裁决」节(权威口径);`docs/问题与解决方案对照表_v2.xlsx`(28 项裁决);`docs/2026.9.8修改方案.docx`(公式与生效日期)。需规(`docs/衍水绩效管理系统功能需规.docx`)中 4 类作废条款不再引用:26 天示例、含运费公式、协助 25%、经理个人户 8‰。
|
||||
|
||||
## 全局约束
|
||||
|
||||
- 表:小写蛇形复数、列小写蛇形、每列中文 COMMENT、主键 `id`、`uk_`/`idx_`/`fk_` 前缀;金额类 DECIMAL;时间列用 `TimestampMixin`(`ON UPDATE CURRENT_TIMESTAMP` 须 `text()` 显式写);append-only 表不继承 TimestampMixin(与 `login_logs` 先例一致)
|
||||
- 迁移双轨:Alembic 生成迁移 → 导出 SQL 到 `db/migration/V{n}__*.sql`(按序编号)→ 同步 `db/init/init-dev.sql`;`tests/integration/test_schema_sync.py` 拦截漂移
|
||||
- 六层红线:api 只参数绑定(写 `def` 非 `async def`)→ services 唯一事务边界 → repositories 只 `flush()`;统一响应 `{code,message,data}`,分页 `pageNum/pageSize` → `list/total/pageNum/pageSize`
|
||||
- 写操作接入 `audit_service.audit()`;审计脱敏在格式化后执行(顺序不可倒)
|
||||
- 金额全程 `Decimal`/`Fraction`,**禁 float**;`4/360` 按精确分数存储(`"4/360"`,禁 `0.011111`)
|
||||
- Redis 键前缀 `ys:`,TTL 按业务语义;禁 `KEYS *` / `FLUSHALL` / `FLUSHDB`
|
||||
- 前端:TS 锁 `~5.9.3`(不可 7.x);复用 `usePagedList`/`useConfirmAction`/`AppPagination`/`StatusTag`;视觉沿用 `--ys-*` token;新页面必须写 `meta.title`
|
||||
- 测试:业务模块整体功能完成时统一编写用例;记录写入 `backend/tests/README.md` / `frontend/README.md`
|
||||
- 提交:`feat(scope): 中文说明`;不直推 main/develop
|
||||
|
||||
## 审查重点(Review Focus)
|
||||
|
||||
以下输入/失败模式最可能伤到使用者,对应测试已钉进第二章各任务:
|
||||
|
||||
1. 畸形参数值(`"1/0"` 除零分数、非数字、超取值域)→ 保存必须 400 拒绝且不落库(T2/T5)
|
||||
2. 指定未来生效的变更泄漏进当前快照 → 快照解析必须不含(T3/T5)
|
||||
3. 撤回未生效批次后解析未回退 → 必须回退到撤回前值(T5)
|
||||
4. 回款天数边界:同日回款 = 1 天;12-15 → 01-10 = 27 天;跨年连续(T2)
|
||||
5. 计提比例边界:360 天 = 6‰、361 天 = 4‰、第 1 天 = 10 − 4/360 ≈ 9.9889‰(精确分数断言)(T2)
|
||||
6. 毛利润额 = 0 → 折算额 0、绩效 0 不除零;负绩效按公式正常计算不归零(T2)
|
||||
|
||||
---
|
||||
|
||||
## 第一章 模块级路线图
|
||||
|
||||
### 1.1 推荐顺序与依赖
|
||||
|
||||
```text
|
||||
① 系统设置与参数体系(#28) ← 地基:岗位角色 + 参数 + 公式模板
|
||||
② 基础数据管理 ← 人员引用 ① 岗位角色(外键上游)
|
||||
③ 业务数据管理 ← 引用 ② 人员/产品/客户
|
||||
④ 绩效工资核算 ← 消费 ① 参数快照(按回款到账日)+ ②③ 数据
|
||||
⑤ 报表统计(∥ ⑥ T+ 同步) ← ⑤ 只读消费 ③④;⑥ 写入 ②③ 目标表,受 IT 字段映射制约
|
||||
```
|
||||
|
||||
### 1.2 顺序论证(要点)
|
||||
|
||||
**① 先于基础数据:** 外键方向在上游(人员→岗位,先建避免迁移返工);参数取值契约是 ④ 的输入面,先定型让后续模块按契约编码;计算预览(需规 2.3.1 四)首块即可交给业务对账「360 天=6‰、361 天=4‰」,验收前移;参数清单提炼本身 = 28 项裁决逐条复核。阈值参数归 ① 有明文依据(#28「全部阈值统一在系统设置维护」)。
|
||||
|
||||
**⑥ 放最后:** 字段映射未定(对照表 #26 ⏳IT),放前部会把外部不确定性引入关键路径;目标表由 ②③ 定稿后同步才有意义;一期可手工录入闭环。**但预留前移**:②③ 表设计必须含外部唯一键列(`source_no`/`t_plus_id`)、同步批次时间列,并约定「同步不覆盖人工字段(指导价、归属、岗位)」。
|
||||
|
||||
**⑤ 可后置:** 纯消费端无下游,口径依赖真实核算数据校准;唯一约束是 ④ 设计台账时对照需规 2.4.1 明细列(合同编号/发货日/回款日/回款天数/产品类别/客户类型/人员岗位/计提比例/支付权重R/绩效金额)一次性建全。
|
||||
|
||||
### 1.3 各块范围与验收标准
|
||||
|
||||
| 块 | 范围界定 | 验收标准(一句话) |
|
||||
| --- | --- | --- |
|
||||
| ① 系统设置与参数体系 | 岗位角色 CRUD + 模板绑定;参数目录/版本化存储/按日期取值/变更留痕/对比/导出;公式纯函数;计算预览 | 管理员可新增岗位并绑定模板;任一参数变更产生含变更人/时间/项/旧值/新值/生效日期的记录;任意业务日期可解析当时快照;预览精确复现 360 天=6‰、361 天=4‰;301 基线不回归 |
|
||||
| ② 基础数据管理 | 人员(岗位/工资标准/团队/辐射区域/拓展计划)、产品类型与目录、业务(客户)与归属变更 | 三类数据可维护并留痕;人员岗位引用 ① 岗位表;表含 T+ 预留列;客户分类(个人户/经理个人户/企业户)与归属修改不回溯历史 |
|
||||
| ③ 业务数据管理 | 业务明细+指导价、发货(批次/运费/发货日)、回款(批次号/到账日/以物抵顶/手动指定发货批次)、毛利润(不含运费)、客户评级计算器 | 一笔业务可多产品多批次发货、多笔回款;回款天数按「发货日首日=1」自动算;折算走精确分数;评级只出等级不影响金额 |
|
||||
| ④ 绩效工资核算 | 核算引擎(按回款到账日取参数)、单笔台账+核销防重、经理 12.5%+5%、试用期/年累计 18000、起付 60% 与 R×R、预付款/以物抵顶、月工资(分岗口径)、重算授权留痕 | 任一笔回款可复算到分(零误差);同笔回款不重复核算;08-13 前后企业户 2‰/3‰ 回溯正确;月工资表可生成 |
|
||||
| ⑤ 报表统计 | 个人绩效明细、部门汇总、业务统计、回款区间统计、Excel 导出与打印 | 四表口径与台账一致且可下钻单笔;导出精度与页面一致 |
|
||||
| ⑥ T+ 数据同步 | 五类数据抓取/清洗/入库、唯一键去重、失败重试、同步日志、周定时+手动触发 | 手动同步幂等(重跑不重复入库);失败有日志与重试;不覆盖人工字段 |
|
||||
|
||||
### 1.4 里程碑(交付批次)
|
||||
|
||||
| 批次 | 内容 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| **M1** | ① 系统设置与参数体系 | 独立可交付;预览工具可作业务对账入口先行验收 |
|
||||
| **M2** | ② + ③ | 合并交付:一次表结构评审把 T+ 预留列与报表字段清单同时定稿 |
|
||||
| **M3** | ④ 绩效工资核算 | 核心钱袋子;测试重点在精度与参数回溯 |
|
||||
| **M4** | ⑤ 报表 + ⑥ T+ 同步(并行) | ⑥ 受 IT 制约,谁先就绪谁先交 |
|
||||
|
||||
### 1.5 需求依据对照
|
||||
|
||||
| 块 | 依据 | 关联作废条款(不引用) |
|
||||
| --- | --- | --- |
|
||||
| ① | 需规 2.3.1(四)/2.5.2/2.1.2;对照表 #28/#5/#8/#20–#24 | 经理个人户 8‰(不设参数);含运费口径;协助 25%(不设参数) |
|
||||
| ② | 需规 2.1.1–2.1.3;对照表 #9/#19/#26 | 信息部负责人条款(信息员独立角色) |
|
||||
| ③ | 需规 2.2.1–2.2.5;对照表 #1/#2/#12/#13/#14/#18/#25 | 26 天示例;含运费公式(2.2.4/2.3.3 整段) |
|
||||
| ④ | 需规 2.3.1–2.3.3;对照表 #3/#4/#6/#7/#11/#15/#16/#17/#20–#24 | 协助 25%;经理 8‰;26 天示例;含运费公式 |
|
||||
| ⑤ | 需规 2.4.1–2.4.3;对照表 #22(明细展示 R) | — |
|
||||
| ⑥ | 需规各「同步 T+」条;对照表 #10/#26/#27 | — |
|
||||
|
||||
---
|
||||
|
||||
## 第二章 首工作块「系统设置与参数体系(#28)」实施计划
|
||||
|
||||
### 2.1 关键设计取舍(已定)
|
||||
|
||||
1. **参数存储 = 参数定义 + 逐项版本条目(时点化 append-only)+ 变更批次表承载「版本」**;否决全量快照(改一项复制全量、单参数历史需遍历)与单行 JSON(无回溯、无字段校验);
|
||||
2. 数值一律以 **Fraction 可解析字符串**存储(`"4/360"`,禁 `0.011111`),解析走 `Fraction(text)` 不经 float;
|
||||
3. **不设日期唯一键**:同日多次修正是合法留痕,解析按 `effective_date DESC, id DESC` 取最新;
|
||||
4. **一期不做参数 Redis 缓存**(算钱系统缓存失效即错钱;20 行级查询成本可忽略),保留 `ys:param:ver:*` 键位待实测后启用;
|
||||
5. 维度覆盖链:**角色维度 > 产品类别维度 > 默认行**;`dimension_value` 用空串不用 NULL(MySQL 组合索引对 NULL 不去重);
|
||||
6. append-only 表(`param_values`/`param_change_batches`)**不继承 TimestampMixin**(无 update_time,与 `login_logs` 先例一致)。
|
||||
|
||||
### 2.2 表设计(V3 迁移,四张新表)
|
||||
|
||||
**`business_roles` 业务岗位角色表**(与系统登录角色 `roles` 严格区分,表注释写明;这是 #28「人员岗位角色」的落点)
|
||||
|
||||
| 列 | 类型 | 约束/说明 |
|
||||
| --- | --- | --- |
|
||||
| id | BIGINT UNSIGNED | PK AUTO_INCREMENT |
|
||||
| role_code | VARCHAR(32) | NOT NULL,uk;创建后不可改(同 roles 保护) |
|
||||
| role_name | VARCHAR(64) | NOT NULL |
|
||||
| calc_template | VARCHAR(32) | NOT NULL,计提公式模板编码(以代码注册表为准) |
|
||||
| description | VARCHAR(255) | NULL |
|
||||
| sort_order | INT | NOT NULL DEFAULT 0 |
|
||||
| status | TINYINT | NOT NULL DEFAULT 1(1 启用 0 禁用) |
|
||||
| is_builtin | TINYINT | NOT NULL DEFAULT 0(1 内置禁删) |
|
||||
| create_time / update_time | DATETIME | TimestampMixin |
|
||||
|
||||
种子(迁移 + init-dev 双写):`salesperson 营销员`(TPL_SALESPERSON_DECAY)、`sales_manager 营销经理`(TPL_MANAGER_SHARE)、`info_officer 信息员`(TPL_INFO_SALES_FIXED),均内置。
|
||||
|
||||
**`param_definitions` 计提参数定义表**:`param_code` VARCHAR(64) uk;`param_name` VARCHAR(64);`param_group` VARCHAR(32);`value_type` VARCHAR(16)(DECIMAL/INT/BOOL/ENUM/TEXT);`unit` VARCHAR(16)(‰/%/元/天,可 NULL);`dimension_type` VARCHAR(32) NOT NULL DEFAULT 'NONE'(NONE/PRODUCT_CATEGORY/BUSINESS_ROLE);`options_text` VARCHAR(255)(枚举竖线分隔);`default_value` / `value_min` / `value_max` VARCHAR(64)(Fraction 可解析字符串);`description` VARCHAR(255);`sort_order` INT;`status` TINYINT;TimestampMixin。
|
||||
|
||||
**`param_change_batches` 参数变更批次表**:`version_no` VARCHAR(32) uk(如 `P20261012-01`);`effective_date` DATE NOT NULL idx;`change_type` TINYINT(1 立即 2 指定日期);`remark` VARCHAR(255);`operator_id` BIGINT UNSIGNED FK→users ON DELETE SET NULL + `operator_name` VARCHAR(64) 冗余固化;`item_count` INT NOT NULL DEFAULT 0;仅 `create_time`。
|
||||
|
||||
**`param_values` 参数版本条目表(append-only)**:`param_id` FK→param_definitions RESTRICT idx;`dimension_value` VARCHAR(32) NOT NULL DEFAULT `''`;`effective_date` DATE NOT NULL idx;`value_text` VARCHAR(64) NOT NULL;`old_value_text` VARCHAR(64) NULL;`batch_id` FK→param_change_batches RESTRICT idx;仅 `create_time`。索引 `idx_param_values_param_id_dimension_value_effective_date`(解析热路径)。已生效条目禁改禁删;未生效批次可整批撤回(删批次+条目)。解析 SQL 留存 `db/queries/计提参数.sql`。
|
||||
|
||||
### 2.3 参数清单(16 项,迁移种子)
|
||||
|
||||
| param_code | 名称 | 类型/单位 | 维度 | 默认值 | 依据 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| salesperson.rate.start | 业务员起始计提比例 | DECIMAL ‰ | BUSINESS_ROLE | 10 | 需规 2.3.1(一)1 |
|
||||
| salesperson.rate.slope | 业务员日递减斜率 | DECIMAL ‰/天 | BUSINESS_ROLE | **4/360** | 对照表 #4 |
|
||||
| salesperson.rate.floor | 业务员计提下限 | DECIMAL ‰ | BUSINESS_ROLE | 4 | 对照表 #4 |
|
||||
| salesperson.days.boundary | 线性递减分界天数 | INT 天 | BUSINESS_ROLE | 360 | 对照表 #4 |
|
||||
| manager.share.team | 经理本团队分成 | DECIMAL % | BUSINESS_ROLE | 12.5 | #6/#15/#16 |
|
||||
| manager.share.company | 经理全公司分成 | DECIMAL % | BUSINESS_ROLE | 5 | 同上 |
|
||||
| enterprise.rate | 企业户计提比例 | DECIMAL ‰ | PRODUCT_CATEGORY | 3 | #20 |
|
||||
| info.rate | 信息员计提比例 | DECIMAL ‰ | BUSINESS_ROLE | 1 | #9 |
|
||||
| prepay.rate | 预付款计提比例 | DECIMAL ‰ | NONE | 10 | #23 |
|
||||
| barter.discount | 以物抵顶折算比例 | DECIMAL % | NONE | 80 | #24 |
|
||||
| payment.start.ratio | 支付起付比例 | DECIMAL % | NONE | 60 | #21 |
|
||||
| payment.weight.mode | 支付权重应用方式 | ENUM FULL\|BALANCE | NONE | FULL | #22 |
|
||||
| product.threshold | 折算毛利润额阈值 | DECIMAL % | PRODUCT_CATEGORY | 市政水利 30 / 轨交 20 | #12、需规 2.1.2 |
|
||||
| salary.basic.mode | 月工资基本工资口径 | ENUM INCLUDE\|DEDUCT\|EXCLUDE | BUSINESS_ROLE | 业务员 INCLUDE / 经理 DEDUCT | #8 |
|
||||
| probation.annual.threshold | 试用期年累计结算阈值 | DECIMAL 元 | NONE | 18000 | #17 |
|
||||
| probation.salary.basic | 试用期基本工资标准 | DECIMAL 元 | NONE | 3000 | 需规(归人员字段则撤,见开放问题) |
|
||||
|
||||
**不设参数**(作废条款的落地):经理个人户 8‰ 起始/斜率;协助 25% 分成;含运费口径开关。
|
||||
|
||||
**种子生效日期**(迁移与 init-dev 双写):`product.threshold`/`salesperson.*` = 2026-02-17;`enterprise.rate` = **2026-02-17 起 2‰ + 2026-08-13 起 3‰ 两行**;`manager.share.*`/`info.rate` = 2026-08-13;支付环节/预付款/以物抵顶/月工资口径 = 2026-01-01。
|
||||
|
||||
### 2.4 公式模板与角色绑定
|
||||
|
||||
**模板注册表**(`app/services/calc_templates.py`,代码内有限集合):
|
||||
|
||||
| 模板编码 | 名称 | 形态 | 声明参数 |
|
||||
| --- | --- | --- | --- |
|
||||
| TPL_SALESPERSON_DECAY | 业务员个人户·天数线性递减 | 分段:≤分界 = 起始 − 天数×斜率;> 分界 = 下限 | salesperson.rate.* / days.boundary |
|
||||
| TPL_MANAGER_SHARE | 经理·团队与全公司分成 | 聚合:∑本团队×12.5% + ∑全公司×5% | manager.share.* |
|
||||
| TPL_ENTERPRISE_FIXED | 企业户·固定计提 | 乘法:折算毛利润额 × 比例 | enterprise.rate |
|
||||
| TPL_INFO_SALES_FIXED | 信息员·销售额固定比例 | 乘法:本人销售额 × 比例 | info.rate |
|
||||
| TPL_PREPAY_FIXED | 预付款·固定计提 | 乘法:回款额 × 比例 | prepay.rate |
|
||||
|
||||
修饰器(不绑角色,参数驱动):以物抵顶 ×80%、支付权重 R×R、起付 60%、试用期缓发。
|
||||
|
||||
**绑定模型**:`business_roles.calc_template` → 模板;角色级覆盖经 `dimension_type=BUSINESS_ROLE` 条目。新增岗位 = 新增行 + 选模板(零代码,满足 #28 ①)。
|
||||
|
||||
**消费接口契约**(④ 与预览共用,函数签名级):
|
||||
|
||||
```python
|
||||
# app/services/param_service.py
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ParamSnapshot:
|
||||
biz_date: date
|
||||
def get(self, code, *, role_code=None, product_category=None) -> str: ... # 覆盖链解析后原始值
|
||||
def fraction(self, code, **dims) -> Fraction: ...
|
||||
def integer(self, code, **dims) -> int: ...
|
||||
def boolean(self, code, **dims) -> bool: ...
|
||||
def text(self, code, **dims) -> str: ...
|
||||
|
||||
def resolve_snapshot(session, biz_date: date) -> ParamSnapshot
|
||||
def submit_change(session, payload, *, operator, meta=None) -> ParamChangeBatchOut
|
||||
def compare(session, *, from_date: date, to_date: date) -> list[ParamDiffOut]
|
||||
|
||||
# app/services/formula.py(纯函数,预览与 ④ 共用;禁 float)
|
||||
def payment_days(ship_date, receipt_date) -> int # 发货日首日=1
|
||||
def salesperson_rate(days, *, start, slope, floor, boundary_days) -> Fraction
|
||||
def profit_rate(profit: Decimal, revenue: Decimal) -> Fraction # 不含运费;revenue=0 防除零
|
||||
def converted_profit(revenue, rate, threshold) -> Decimal # 化简式:销售额 ×(毛利润率−阈值)
|
||||
def performance_amount(base, rate) -> Decimal # 乘法方向(对照表 #3)
|
||||
def apply_barter(amount, discount) -> Decimal
|
||||
def payment_weight(r: int) -> Fraction # r×r
|
||||
def start_payment_ok(received, revenue, ratio) -> bool
|
||||
```
|
||||
|
||||
### 2.5 文件清单
|
||||
|
||||
#### 后端
|
||||
|
||||
| 层 | 文件 | 内容 |
|
||||
| --- | --- | --- |
|
||||
| models | `app/models/business_role.py`、`app/models/param.py`、`app/models/__init__.py`(改) | 四张表;**必须注册进 `__init__`**(漏注册会让 autogenerate 生成删表迁移) |
|
||||
| repositories | `app/repositories/business_role_repository.py`、`param_repository.py` | 只 flush;`resolve_values(session, biz_date)` 一条 SQL 取全量快照;批次撤回用 `for_update` 锁行 |
|
||||
| schemas | `app/schemas/business_role.py`、`param.py` | Out/CreateIn/UpdateIn/StatusIn/Query、TemplateOptionOut;ParamDefinitionOut/SnapshotOut/ChangeIn/Out/BatchOut/BatchDetailOut/DiffOut/PreviewIn/Out |
|
||||
| services | `app/services/param_registry.py`、`calc_templates.py`、`formula.py`、`business_role_service.py`、`param_service.py` | 参数目录注册表(种子唯一来源 + 运行期校验);模板注册表;纯函数;岗位 CRUD+保护规则;参数提交/历史/对比/预览/导出 |
|
||||
| api | `app/api/v1/system/business_roles.py`、`params.py`、`app/api/router.py`(改) | 挂载 `/api/v1/system/business-roles`、`/api/v1/system/params`(AdminUser) |
|
||||
| core | `app/core/constants.py`(改) | `MODULE_BUSINESS_ROLE`/`MODULE_PARAM`、内置岗位编码、产品类别码 |
|
||||
| 迁移 | `alembic/versions/xxxx_*.py`、`db/migration/V3__add_business_roles_and_params.sql`、`db/init/init-dev.sql`(改)、`db/queries/计提参数.sql` | 四表 + 种子;双轨同步 |
|
||||
| 依赖 | `backend/pyproject.toml`(改) | 增补 `openpyxl`(参数导出用;`uv add openpyxl`) |
|
||||
|
||||
**接口集合**:business-roles 列表/新增/详情/编辑/删除/启停 + `meta/templates` 模板下拉;params 目录快照(`asOf`)、单参数历史、变更批次提交/撤回/分页/明细、双日期对比、Excel 导出(`StreamingResponse`,非统一信封)、计算预览。写操作全走 `audit_service.audit()`;变更批次表与操作日志互为双保险。
|
||||
|
||||
#### 前端
|
||||
|
||||
| 类型 | 文件 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| api | `src/api/businessRoles.ts`、`src/api/params.ts` | params 的导出接口直接用 axios 实例(响应为 blob,非统一信封,文件内注明例外) |
|
||||
| 页面 | `src/views/system/business-roles/index.vue` | 列表+表单+启停+删除(保护提示)+模板选择(展示模板声明参数) |
|
||||
| 页面 | `src/views/system/params/index.vue` | 按 param_group 分组;生效方式(立即/指定日期)+备注;保存二次确认;版本管理抽屉(记录/明细/对比/导出);预览抽屉 |
|
||||
| 路由 | `src/router/index.ts`(改) | `{ title: '岗位角色', group: '系统管理', requiresAdmin: true, order: 5 }`、`{ title: '计提参数', …, order: 6 }` |
|
||||
| 常量 | `src/utils/constants.ts`(改) | MODULE_OPTIONS 增补;参数分组/单位展示映射 |
|
||||
| 复用 | `usePagedList`/`useConfirmAction`/`AppPagination`/`StatusTag`/`--ys-*` | 不新增组件 |
|
||||
|
||||
### 2.6 任务序列(步骤级)
|
||||
|
||||
任务依赖:T1 → T2 → T3 → (T4 ∥ T5) → (T6 ∥ T7) → T8。
|
||||
|
||||
---
|
||||
|
||||
#### 任务 T1:迁移与模型(四张新表 + 种子)
|
||||
|
||||
**文件:**
|
||||
|
||||
- 创建:`backend/app/models/business_role.py`、`backend/app/models/param.py`
|
||||
- 修改:`backend/app/models/__init__.py`
|
||||
- 创建:`backend/alembic/versions/xxxx_add_business_roles_and_params.py`(autogenerate 后人工补种子)
|
||||
- 创建:`db/migration/V3__add_business_roles_and_params.sql`
|
||||
- 修改:`db/init/init-dev.sql`
|
||||
- 创建:`db/queries/计提参数.sql`
|
||||
|
||||
- [ ] **步骤 1:写两个模型文件**
|
||||
|
||||
按 2.2 字段设计实现 `BusinessRole`、`ParamDefinition`、`ParamChangeBatch`、`ParamValue`。写法参照 `backend/app/models/role.py`(`Mapped`/`mapped_column`、`mysql_table_args("中文表注释")`、`BIGINT_UNSIGNED`、`TINYINT_TYPE`);`ParamChangeBatch`/`ParamValue` 不继承 `TimestampMixin`,仅手写 `create_time`(`server_default=text("CURRENT_TIMESTAMP")`);外键目标类型必须与 `users.id` 一致(`BIGINT_UNSIGNED`)。
|
||||
|
||||
- [ ] **步骤 2:注册进 `models/__init__.py`**
|
||||
|
||||
四个类加入导入与 `__all__`(漏注册会让 autogenerate 把新表当成待删除)。
|
||||
|
||||
- [ ] **步骤 3:生成迁移并补种子**
|
||||
|
||||
```bash
|
||||
cd backend && uv run alembic revision --autogenerate -m "新增岗位角色与计提参数四表"
|
||||
```
|
||||
|
||||
人工检查:表/列中文 COMMENT、`uk_`/`idx_`/`fk_` 命名、`ON UPDATE CURRENT_TIMESTAMP` 子句;然后按 2.2/2.3 用 `op.bulk_insert` 或 `op.execute` 补种子:3 个内置岗位、16 项参数定义、版本条目(`enterprise.rate` 两行:2026-02-17=2、2026-08-13=3;其余按 2.3 生效日期表;维度行展开——`product.threshold` 两行市政/轨交、`salary.basic.mode` 两行业务员/经理)。种子条目的 `batch_id` 需要一条「初始版本」批次(`version_no` 如 `P20260101-00`、`remark` 初始版本)。
|
||||
|
||||
- [ ] **步骤 4:应用迁移并导出 SQL**
|
||||
|
||||
```bash
|
||||
cd backend && uv run alembic upgrade head
|
||||
uv run alembic upgrade <V2_revision>:head --sql > ../db/migration/V3__add_business_roles_and_params.sql
|
||||
```
|
||||
|
||||
(V2 revision 从 `alembic/versions/` 中操作日志迁移文件读取。)
|
||||
|
||||
- [ ] **步骤 5:同步 `db/init/init-dev.sql`**
|
||||
|
||||
追加四表 DDL(与迁移产出一致)+ 种子 INSERT + 更新末尾 `alembic_version` 为 V3 revision id。
|
||||
|
||||
- [ ] **步骤 6:写 `db/queries/计提参数.sql`**
|
||||
|
||||
留存解析查询(时点化取值:`WHERE effective_date <= :biz_date ORDER BY effective_date DESC, id DESC` 取每组最新)与对比查询。
|
||||
|
||||
- [ ] **步骤 7:验证**
|
||||
|
||||
```bash
|
||||
cd backend && uv run pytest tests/integration/test_schema_sync.py tests/unit/test_models.py -q
|
||||
```
|
||||
|
||||
预期:全部 PASS(schema 双轨无漂移、新表规范断言通过)。
|
||||
|
||||
- [ ] **步骤 8:Commit**
|
||||
|
||||
```bash
|
||||
git add backend/app/models backend/alembic db/init/init-dev.sql db/migration db/queries
|
||||
git commit -m "feat(system): 新增岗位角色与计提参数四表及 V3 迁移"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 任务 T2:参数注册表 + 公式纯函数
|
||||
|
||||
**文件:**
|
||||
|
||||
- 创建:`backend/app/services/param_registry.py`、`backend/app/services/calc_templates.py`、`backend/app/services/formula.py`
|
||||
- 修改:`backend/app/core/constants.py`(内置岗位编码、产品类别码常量)
|
||||
- 测试:`backend/tests/unit/test_formula.py`、`backend/tests/unit/test_param_registry.py`
|
||||
|
||||
- [ ] **步骤 1:编写失败的测试(公式,算钱核心)**
|
||||
|
||||
代表性用例(全部精确分数断言,禁近似):
|
||||
|
||||
```python
|
||||
def test_业务员360天_比例恰为6‰():
|
||||
rate = salesperson_rate(360, start=Fraction(10), slope=Fraction(4, 360),
|
||||
floor=Fraction(4), boundary_days=360)
|
||||
assert rate == Fraction(6, 1000)
|
||||
|
||||
def test_业务员361天_固定下限4‰_分界线():
|
||||
rate = salesperson_rate(361, start=Fraction(10), slope=Fraction(4, 360),
|
||||
floor=Fraction(4), boundary_days=360)
|
||||
assert rate == Fraction(4, 1000)
|
||||
|
||||
def test_业务员第1天_按公式为899比90000_精确分数():
|
||||
rate = salesperson_rate(1, start=Fraction(10), slope=Fraction(4, 360),
|
||||
floor=Fraction(4), boundary_days=360)
|
||||
assert rate == Fraction(10) - Fraction(1, 90) # 899/90 ‰
|
||||
|
||||
def test_回款天数_发货日首日为第1天_12月15日到1月10日为27天():
|
||||
assert payment_days(date(2025, 12, 15), date(2026, 1, 10)) == 27
|
||||
|
||||
def test_回款天数_同日回款为1天():
|
||||
d = date(2026, 1, 1)
|
||||
assert payment_days(d, d) == 1
|
||||
|
||||
def test_毛利润额为零_折算额与绩效均为零_不除零():
|
||||
assert converted_profit(Decimal("0"), Fraction(0), Fraction(3, 10)) == Decimal("0")
|
||||
|
||||
def test_毛利润率低于阈值_折算为负_绩效负值不归零():
|
||||
# 销售额 100、毛利润率 10%、阈值 30% → 折算 -20
|
||||
assert converted_profit(Decimal("100"), Fraction(1, 10), Fraction(3, 10)) == Decimal("-20")
|
||||
assert performance_amount(Decimal("-20"), Fraction(6, 1000)) == Decimal("-0.12")
|
||||
|
||||
def test_支付权重_R为60时权重0点36():
|
||||
assert payment_weight(60) == Fraction(36, 100)
|
||||
|
||||
def test_注册表_16项_编码唯一_必填字段非空():
|
||||
assert len(PARAM_REGISTRY) == 16
|
||||
assert len({s.code for s in PARAM_REGISTRY.values()}) == 16
|
||||
# 每项 name/group/value_type 非空
|
||||
|
||||
def test_解析分数_4除以360_按Fraction不丢精度():
|
||||
assert parse_fraction("4/360") == Fraction(1, 90)
|
||||
|
||||
def test_模板注册表_五个模板_声明参数均在注册表内():
|
||||
# TEMPLATES 的 params 引用必须全部存在于 PARAM_REGISTRY(跨表一致性)
|
||||
```
|
||||
|
||||
- [ ] **步骤 2:运行确认失败**
|
||||
|
||||
```bash
|
||||
cd backend && uv run pytest tests/unit/test_formula.py tests/unit/test_param_registry.py -q
|
||||
```
|
||||
|
||||
预期:FAIL(`ModuleNotFoundError` / `NameError`)。
|
||||
|
||||
- [ ] **步骤 3:实现三个 service 文件**
|
||||
|
||||
`formula.py`:8 个纯函数(签名见 2.4 契约)。要点:`salesperson_rate` 中 `days <= boundary_days` 用线性式、否则取 `floor`;`converted_profit` 用化简式 `revenue × (rate − threshold)`;`performance_amount` 为乘法;`payment_days` 为 `(receipt_date - ship_date).days + 1`。
|
||||
|
||||
`param_registry.py`:`ParamSpec` 冻结 dataclass + `PARAM_REGISTRY`(16 项,值与 2.3 种子一致)+ `get_spec`(未注册编码抛 500 级异常,属代码 bug)+ `parse_fraction`(`Fraction(text)`,捕获 `ZeroDivisionError`/`ValueError` → 400)+ `validate_and_normalize`(类型/取值域/枚举校验,返回规范串)。
|
||||
|
||||
`calc_templates.py`:`TemplateSpec` + `TEMPLATES`(5 个模板,见 2.4)+ `get_template`/`required_params`。
|
||||
|
||||
- [ ] **步骤 4:运行确认通过**
|
||||
|
||||
```bash
|
||||
cd backend && uv run pytest tests/unit -q
|
||||
```
|
||||
|
||||
预期:全部 PASS(含既有单元基线)。
|
||||
|
||||
- [ ] **步骤 5:Commit**
|
||||
|
||||
```bash
|
||||
git add backend/app/services/formula.py backend/app/services/param_registry.py backend/app/services/calc_templates.py backend/app/core/constants.py backend/tests/unit/test_formula.py backend/tests/unit/test_param_registry.py
|
||||
git commit -m "feat(system): 计提公式模板注册表与精确分数计算函数"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 任务 T3:参数仓储 + 取值服务(时点化解析)
|
||||
|
||||
**文件:**
|
||||
|
||||
- 创建:`backend/app/repositories/param_repository.py`
|
||||
- 创建:`backend/app/services/param_service.py`(先实现 `ParamSnapshot`/`resolve_snapshot` 与覆盖链选择)
|
||||
- 测试:`backend/tests/unit/test_param_snapshot.py`(覆盖链纯逻辑;真实 DB 解析在 T8 统一集成测试)
|
||||
|
||||
- [ ] **步骤 1:编写失败的测试(覆盖链与兜底)**
|
||||
|
||||
```python
|
||||
def test_覆盖链_角色维度优先于产品类别与默认():
|
||||
# rows: (code, dimension) 含 ('enterprise.rate', ''), ('enterprise.rate', 'municipal_water')
|
||||
# 取 role_code=None, product_category='municipal_water' → 返回产品类别行
|
||||
|
||||
def test_未定义参数_按注册表默认值兜底():
|
||||
# 某参数无任何版本条目 → snapshot.fraction(code) == 注册表 default_value 的 Fraction
|
||||
|
||||
def test_同日多次变更_取id最大的一条():
|
||||
# 同日两行 → 取 id 大者
|
||||
```
|
||||
|
||||
- [ ] **步骤 2:运行确认失败**(`uv run pytest tests/unit/test_param_snapshot.py -q`,预期 FAIL)
|
||||
|
||||
- [ ] **步骤 3:实现**
|
||||
|
||||
`param_repository.py`:`resolve_values(session, biz_date)` 一条 SQL 取全量快照(按 `(param_id, dimension_value)` 分组取 `effective_date DESC, id DESC` 首行;可用窗口函数或子查询,SQL 留存 `db/queries/计提参数.sql`);批次 CRUD 与 `for_update` 加锁函数(签名参照 2.5 与设计报告)。
|
||||
|
||||
`param_service.py`:`ParamSnapshot`(frozen dataclass,持有解析后 `dict[(code, dim)] → value` 与 `biz_date`)+ `resolve_snapshot(session, biz_date)`(读定义 + 读条目 + 合并注册表默认值);`get/fraction/integer/boolean/text` 五类取值(覆盖链:role_code 维度 > product_category 维度 > 默认行;`fraction` 走 `parse_fraction`)。
|
||||
|
||||
- [ ] **步骤 4:运行确认通过**(`uv run pytest tests/unit -q`,预期 PASS)
|
||||
|
||||
- [ ] **步骤 5:Commit**
|
||||
|
||||
```bash
|
||||
git add backend/app/repositories/param_repository.py backend/app/services/param_service.py backend/tests/unit/test_param_snapshot.py
|
||||
git commit -m "feat(system): 计提参数按业务日期时点化取值服务"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 任务 T4:岗位角色 CRUD 后端(含审计与保护规则)
|
||||
|
||||
**文件:**
|
||||
|
||||
- 创建:`backend/app/repositories/business_role_repository.py`、`backend/app/schemas/business_role.py`、`backend/app/services/business_role_service.py`、`backend/app/api/v1/system/business_roles.py`
|
||||
- 修改:`backend/app/api/router.py`、`backend/app/core/constants.py`(MODULE_BUSINESS_ROLE)
|
||||
|
||||
- [ ] **步骤 1:实现四层文件**
|
||||
|
||||
整体参照 `roles` 模块(`role_repository.py`/`role_service.py`/`api/v1/system/roles.py`)的写法与保护规则模式。接口:列表(分页+关键字+状态)、新增、详情、编辑、删除、启停、`GET /meta/templates` 模板下拉。保护规则:`is_builtin` 禁删禁停;`role_code` 创建后不可改;`calc_template` 必须存在于 `TEMPLATES`(否则 400);删除前检查人员引用(② 落地前以引用计数钩子预留,接入后生效);写操作全部包 `audit_service.audit()`。
|
||||
|
||||
- [ ] **步骤 2:冒烟验证(容器 + 后端在跑)**
|
||||
|
||||
```bash
|
||||
cd backend && uv run uvicorn app.main:app --reload --port 8000
|
||||
# 另一终端:curl 登录取 token 后依次打 7 个端点;重点验证:
|
||||
# - 新增自定义岗位(绑 TPL_ENTERPRISE_FIXED)→ 201/200
|
||||
# - 删除内置 salesperson → 403;删除自定义岗位 → 成功
|
||||
# - calc_template 传不存在的编码 → 400
|
||||
```
|
||||
|
||||
- [ ] **步骤 3:Commit**
|
||||
|
||||
```bash
|
||||
git add backend/app/repositories/business_role_repository.py backend/app/schemas/business_role.py backend/app/services/business_role_service.py backend/app/api/v1/system/business_roles.py backend/app/api/router.py backend/app/core/constants.py
|
||||
git commit -m "feat(system): 业务岗位角色管理接口与保护规则"
|
||||
```
|
||||
|
||||
(模块级正式用例按项目约定在 T8 统一编写。)
|
||||
|
||||
---
|
||||
|
||||
#### 任务 T5:参数变更/历史/对比/导出/预览后端
|
||||
|
||||
**文件:**
|
||||
|
||||
- 创建:`backend/app/schemas/param.py`、`backend/app/api/v1/system/params.py`
|
||||
- 修改:`backend/app/services/param_service.py`(提交/撤回/历史/对比/预览/导出)、`backend/app/api/router.py`、`backend/app/core/constants.py`(MODULE_PARAM)、`backend/pyproject.toml`(`uv add openpyxl`)
|
||||
|
||||
- [ ] **步骤 1:加依赖**:`cd backend && uv add openpyxl`(参数导出用)
|
||||
|
||||
- [ ] **步骤 2:实现 schemas 与 service 扩展**
|
||||
|
||||
`submit_change`:事务内校验全部 items(`validate_and_normalize`)→ 建批次(`version_no` 规则 `P{YYYYMMDD}-{当日序号}`)→ 逐项写 `param_values`(`old_value_text` 取当前生效值)→ `item_count` → commit。`withdraw_batch`:已生效(`effective_date <= today`)禁撤(400);`for_update` 锁批次行后删条目与批次。`compare`:两日期各取快照后逐项 diff。`preview`:入参(产品类别/客户类型/岗位/发货日/回款日/回款金额/销售额/毛利润额/是否以物抵顶/是否试用期)→ 输出(回款天数/计提比例/折算毛利润额/单笔绩效/起付判定/R×R 结果/所用参数生效版本)。`export_changes`:openpyxl 流式写变更记录(列:变更人/时间/参数/旧值/新值/生效日期/批次号)。
|
||||
|
||||
- [ ] **步骤 3:实现 api 层**
|
||||
|
||||
9 个端点(目录快照/历史/提交/撤回/批次分页/批次明细/对比/导出/预览);写操作包审计;导出返回 `StreamingResponse`(非统一信封,前端 api 层注明)。
|
||||
|
||||
- [ ] **步骤 4:冒烟验证**
|
||||
|
||||
```bash
|
||||
# 提交一次变更(3 项参数,指定未来日期)→ 当前快照不变
|
||||
# 到该日期(或提交立即生效)→ 快照变化、历史出现记录
|
||||
# 撤回未生效批次 → 快照回退;撤回已生效批次 → 400
|
||||
# 预览:360 天/361 天边界、12-15→01-10=27 天精确复现
|
||||
# 导出 → xlsx 可打开且列齐全
|
||||
```
|
||||
|
||||
- [ ] **步骤 5:Commit**
|
||||
|
||||
```bash
|
||||
git add backend/app/schemas/param.py backend/app/services/param_service.py backend/app/api/v1/system/params.py backend/app/api/router.py backend/app/core/constants.py backend/pyproject.toml backend/uv.lock
|
||||
git commit -m "feat(system): 计提参数版本管理、变更留痕与计算预览接口"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 任务 T6:前端岗位角色页
|
||||
|
||||
**文件:**
|
||||
|
||||
- 创建:`frontend/src/api/businessRoles.ts`、`frontend/src/views/system/business-roles/index.vue`
|
||||
- 修改:`frontend/src/router/index.ts`、`frontend/src/utils/constants.ts`
|
||||
|
||||
- [ ] **步骤 1:api 封装 + 页面**
|
||||
|
||||
页面整体复用用户管理页模式(`usePagedList` + `useConfirmAction` + `AppPagination` + `StatusTag`);表单含模板下拉(选中后展示模板声明的参数清单与当前值);内置岗位删除/停用按钮禁用并给 title 提示;路由 meta `{ title: '岗位角色', group: '系统管理', requiresAdmin: true, order: 5 }`。
|
||||
|
||||
- [ ] **步骤 2:验证**
|
||||
|
||||
```bash
|
||||
cd frontend && pnpm type-check && pnpm build
|
||||
```
|
||||
|
||||
预期:类型检查与构建通过。
|
||||
|
||||
- [ ] **步骤 3:Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/api/businessRoles.ts frontend/src/views/system/business-roles frontend/src/router/index.ts frontend/src/utils/constants.ts
|
||||
git commit -m "feat(system): 前端岗位角色管理页面"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 任务 T7:前端参数配置页(含版本管理抽屉与预览)
|
||||
|
||||
**文件:**
|
||||
|
||||
- 创建:`frontend/src/api/params.ts`、`frontend/src/views/system/params/index.vue`
|
||||
- 修改:`frontend/src/router/index.ts`、`frontend/src/utils/constants.ts`
|
||||
|
||||
- [ ] **步骤 1:api 封装**(导出接口用 axios 实例取 blob,注明非统一信封例外)
|
||||
|
||||
- [ ] **步骤 2:页面实现**
|
||||
|
||||
按 `param_group` 分组展示参数(名称/当前值+单位/生效日期/口径说明);顶部「生效方式(立即/指定日期)+ 生效日期 + 备注」;保存前二次确认(列出改动项与旧值→新值);版本管理抽屉(变更记录分页 + 批次明细 + 两日期对比 + 导出下载);预览抽屉(表单 → 结果卡,注明「斜率精确值 4/360」);路由 meta order 6。
|
||||
|
||||
- [ ] **步骤 3:验证**
|
||||
|
||||
```bash
|
||||
cd frontend && pnpm type-check
|
||||
# e2e(需容器+前后端在跑):改一个参数 → 变更记录出现 → 预览值变化 → 导出可下载
|
||||
```
|
||||
|
||||
- [ ] **步骤 4:Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/api/params.ts frontend/src/views/system/params frontend/src/router/index.ts frontend/src/utils/constants.ts
|
||||
git commit -m "feat(system): 前端计提参数配置与版本管理页面"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 任务 T8:模块测试统一编写与文档更新
|
||||
|
||||
**文件:**
|
||||
|
||||
- 创建:`backend/tests/unit/test_param_service.py`(可 fakeredis 化的部分)、`backend/tests/integration/test_business_roles.py`、`backend/tests/integration/test_param_management.py`
|
||||
- 修改:`backend/tests/README.md`、`frontend/README.md`、`backend/README.md`(接口清单)、`frontend/e2e/system_management.py` 或新增 `frontend/e2e/param_management.py`
|
||||
|
||||
- [ ] **步骤 1:编写集成用例**(覆盖审查重点 1/2/3)
|
||||
|
||||
参数精度专项(Fraction/1/90 全链路:注册表 → 种子 → 解析 → 预览一致);未来生效不泄漏;撤回回退;同日多次变更取最新;审计留痕(成功与失败);权限(普通用户 403);岗位保护规则(内置禁删、模板校验)。
|
||||
|
||||
- [ ] **步骤 2:全量回归**
|
||||
|
||||
```bash
|
||||
cd backend && uv run pytest -q
|
||||
```
|
||||
|
||||
预期:301 基线 + 新增用例全绿。
|
||||
|
||||
- [ ] **步骤 3:e2e 与文档**
|
||||
|
||||
前端 e2e 步骤(菜单出现两个新页、改参数→记录→预览链路);更新三份 README(用例清单/运行记录/发现缺陷/待补用例、接口清单)。
|
||||
|
||||
- [ ] **步骤 4:Commit**
|
||||
|
||||
```bash
|
||||
git add backend/tests frontend/e2e backend/README.md frontend/README.md
|
||||
git commit -m "test(system): 参数体系模块用例与测试记录"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.7 风险与开放问题
|
||||
|
||||
#### 风险(已定对策)
|
||||
|
||||
1. 参数缓存:一期不做(见 2.1-4);若未来启用必须按批次版本号精确失效,禁通配删除;
|
||||
2. 同日多次变更:UI 明示「同日多次变更按最新一条生效」;
|
||||
3. `4/360` 展示:存储分数、展示格式化(预览页注明「精确值 4/360,非截断 0.011111」);
|
||||
4. 模板扩展摩擦:新公式形态仍需加模板(一期接受,与「不引入规则引擎」一致);岗位表单把模板做成带说明的受控下拉;
|
||||
5. 报表字段前移:预览出参尽量与需规 2.4.1 明细列同名,减少 ④ 映射工作;
|
||||
6. 参数导出用 openpyxl:已在 T5 增补依赖(AI_CONTEXT 已认可方向)。
|
||||
|
||||
#### 开放问题(不阻塞首块开工,逐项有默认处理)
|
||||
|
||||
| # | 问题 | 默认处理 |
|
||||
| --- | --- | --- |
|
||||
| 1 | 第 1 天计提:需规图示「第 1 天 10‰」vs 公式 `10 − 4/360 ≈ 9.9889‰` | 按公式实现(上限为参考);起始值参数化,业务如需 10‰ 可在系统设置调整 |
|
||||
| 2 | 企业户 02-17 前历史(2‰ 是否回溯) | 已按双种子(02-17=2‰、08-13=3‰)实现;如无需回溯可撤首行 |
|
||||
| 3 | `payment.weight.mode` BALANCE 模式语义(余额口径) | 首块只存枚举;语义在 ④ 开工前确认 |
|
||||
| 4 | `probation.salary.basic`(3000 元)是参数还是人员工资标准字段 | 暂列参数;② 开工时如归人员字段则移除 |
|
||||
| 5 | 参数查询权限(普通用户可否只读快照) | 建议「查看可、修改仅管理员」,实现为 AdminUser 时可放宽——待确认 |
|
||||
| 6 | 导出格式与列集合(是否含批次说明/操作人) | 以需规 2.3.1(四) 字段为准,导出前与业务对一次列 |
|
||||
| 7 | 经理个人户业绩如何进入 12.5%+5%(裁决 #7 的具体衔接:该业绩是否计入「本人带领业务员」基数) | ④ 开工前必须确认(影响台账归属字段设计) |
|
||||
|
||||
## 验证方式
|
||||
|
||||
1. 计划内容与 `ai-aux/AI_CONTEXT.md` 需求裁决节逐条核对(口径无冲突、无遗漏作废条款);
|
||||
2. 计划中的命令与项目现有命令体系一致(uv / pnpm / docker compose);
|
||||
3. 首块每个任务有独立可测交付物;测试要点符合「模块整体完成时统一编写」约定(纯函数与 core 级随实现即时测);
|
||||
4. 执行阶段验证:T1 后 `test_schema_sync.py` 通过;T2/T3 后纯单元全绿;T5 后冒烟链路完整;T8 后全量 301+ 用例与 e2e 全绿。
|
||||
Binary file not shown.
Binary file not shown.
BIN
docs/功能模块清单.xlsx
Normal file
BIN
docs/功能模块清单.xlsx
Normal file
Binary file not shown.
BIN
docs/问题与解决方案对照表_v2.xlsx
Normal file
BIN
docs/问题与解决方案对照表_v2.xlsx
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user