管理端:设计系统 v2 收敛 + 修复漏斗卡恒为 0 的响应式缺陷
## 一、设计系统收敛 - theme/index.css 重写为 token 底座:品牌三级、语义五组、文字四级、 边框/背景/圆角/阴影/动效/热区/排版刻度 - 修复 Element Plus 派生色阶残留:--el-color-*-light-N 是 SCSS 编译期静态生成, 只覆盖基色不会带动它们 —— 数据统计页实测 133 处元素仍在用 EP 默认调色板 (其中主色蓝 8 处)。已按 mix(#fff,base,N*10%) / mix(#000,base,20%) 补全全部档位 - 新增 src/theme/tokens.ts:模板里 :style 需拼 8 位 hex(color + '15'),用不了 var(), 故以 TS 常量镜像色值;守卫会双向校验它与 index.css 同名取值一致 - 24 个 .vue 收敛:硬编码色值 97 处/67 种 → 0;越界字号 88 处(最低 9px)→ 0; 非法字重 650×6 → 0;触摸目标 17 处 <44px → 全部 ≥44px - 对比度:text-tertiary 3.54:1、warning 2.33:1、coral 作文字 3.14:1 → 全部 ≥4.5:1 - 新增守卫 ui-tests/admin-design-tokens.mjs(含 WCAG 对比度实算) ## 二、修复既有功能性缺陷:数据统计页漏斗卡与安全卡恒显示 0 - 现象:同一张卡片里,上方漏斗数字全是 0,下方汇总表却是 12846/9217/8462/3812, 界面自相矛盾 - 根因:funnelStages / matchStages / safetyCards 是普通数组(const x = [...]), loadFunnel() 直接改元素属性,模板读的却是同一个数组 —— 非响应式写入不触发重渲染。 同页凡走 ref 的值(汇总表、V11 区块)全部正常,规律清晰 - 修法(按根因):三个集合改为 computed() 从既有 ref 派生,卡片与汇总表因此同源, 结构上不可能再出现「同一份数据的两处展示对不上」;另把 loadFunnel() 内遮蔽 颜色助手的局部变量 const token 改名 authToken - 该缺陷无 TS/lint/构建报错,前 18 个守卫全绿也抓不到 —— 只有真跑起来看渲染结果才暴露 - 新增守卫 ui-tests/admin-reactivity-contracts.mjs: ①非响应式状态(普通声明 + 被当状态写 + 出现在模板里;未被写过的静态常量清单合法) ②数据统计页同源契约 ③颜色助手遮蔽 ## 验证 - UI 契约门禁 20/20 PASS(MIN_GUARDS 18→20) - 负向注入 3 例全部 FAIL(exit 1),还原后复跑 exit 0(证明守卫有判别力) - vue-tsc --noEmit exit 0 - vite build 成功(17.74s) - 渲染态实测(Edge 无头 + CDP 读渲染后 DOM,同一 mock、同一路由,只差代码): 修复前漏斗卡/安全卡全 0、比率全 '-' → 修复后 12846/9217/8462/3812/ 48210/51236/7420/4380/1926/1284,安全卡 17/214/23/9 交付物:artifacts/admin-design-v2-preview.html
This commit is contained in:
@@ -264,19 +264,20 @@ function handleLogout() {
|
||||
/* 背景图是 public/ 下的运行时资源,必须带 BASE_URL 前缀:
|
||||
写死品牌目录的绝对路径,在产物部署于 /admin/ 子路径时会 404(页面背景变纯色)。
|
||||
用 v-bind 把常量注入 CSS,避免在样式里各写一遍拼接。 */
|
||||
background: #fff7f7 center / cover fixed no-repeat;
|
||||
/* 先给纯色兜底,避免图未加载完成时闪白 */
|
||||
background: var(--admin-coral-lighter) center / cover fixed no-repeat;
|
||||
background-image: url(v-bind('BRAND_BACKDROP'));
|
||||
}
|
||||
|
||||
.scene-header {
|
||||
display: grid;
|
||||
max-width: 1840px;
|
||||
max-width: var(--admin-shell-max-width);
|
||||
height: 188px;
|
||||
margin: 0 auto;
|
||||
grid-template-columns: minmax(640px, 1fr) auto;
|
||||
grid-template-rows: 112px 76px;
|
||||
align-items: center;
|
||||
column-gap: 32px;
|
||||
column-gap: var(--space-8);
|
||||
}
|
||||
|
||||
.scene-brand,
|
||||
@@ -295,23 +296,23 @@ function handleLogout() {
|
||||
|
||||
.scene-slogan {
|
||||
display: flex;
|
||||
padding-right: 24px;
|
||||
padding-right: var(--space-6);
|
||||
align-items: flex-end;
|
||||
flex-direction: column;
|
||||
color: var(--admin-coral);
|
||||
font-family: KaiTi, STKaiti, serif;
|
||||
color: var(--admin-coral-strong);
|
||||
font-family: var(--font-family-brand);
|
||||
line-height: 1.05;
|
||||
transform: rotate(-7deg);
|
||||
}
|
||||
|
||||
.scene-slogan span {
|
||||
font-size: 17px;
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
.scene-slogan strong {
|
||||
margin-top: 4px;
|
||||
font-size: 25px;
|
||||
font-weight: 500;
|
||||
margin-top: var(--space-1);
|
||||
font-size: var(--font-size-3xl);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.scene-brand-copy {
|
||||
@@ -319,18 +320,18 @@ function handleLogout() {
|
||||
margin-left: 10px;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
color: var(--admin-coral);
|
||||
color: var(--admin-coral-strong);
|
||||
}
|
||||
|
||||
.scene-brand-copy strong {
|
||||
font-size: 27px;
|
||||
font-size: var(--font-size-4xl);
|
||||
line-height: 1.15;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.scene-brand-copy span {
|
||||
margin-top: 5px;
|
||||
font-size: 12px;
|
||||
font-size: var(--font-size-xs);
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
|
||||
@@ -338,18 +339,18 @@ function handleLogout() {
|
||||
min-width: 370px;
|
||||
margin-left: 28px;
|
||||
padding-left: 28px;
|
||||
border-left: 1px solid rgba(244, 95, 104, 0.22);
|
||||
border-left: 1px solid var(--admin-coral-border);
|
||||
}
|
||||
|
||||
.scene-title h1 {
|
||||
font-size: clamp(26px, 2.2vw, 38px);
|
||||
font-size: clamp(var(--font-size-3xl), 2.2vw, var(--font-size-display));
|
||||
line-height: 1.1;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.scene-title p {
|
||||
margin-top: 9px;
|
||||
color: #66717a;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.scene-benefits {
|
||||
@@ -357,40 +358,41 @@ function handleLogout() {
|
||||
align-self: start;
|
||||
justify-content: flex-start;
|
||||
gap: clamp(38px, 5vw, 92px);
|
||||
padding: 4px 0 0 50px;
|
||||
padding: var(--space-1) 0 0 50px;
|
||||
}
|
||||
|
||||
.scene-benefits span {
|
||||
display: grid;
|
||||
grid-template-columns: 36px auto;
|
||||
column-gap: 8px;
|
||||
color: var(--admin-coral);
|
||||
column-gap: var(--space-2);
|
||||
/* 13px 文字压在摄影背景图上,必须用 -strong 档(5.13:1),基础色只有 3.14:1 */
|
||||
color: var(--admin-coral-strong);
|
||||
}
|
||||
|
||||
.scene-benefits .el-icon {
|
||||
grid-row: 1 / 3;
|
||||
font-size: 30px;
|
||||
font-size: var(--font-size-4xl);
|
||||
}
|
||||
|
||||
.scene-benefits b {
|
||||
font-size: 13px;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.scene-benefits small {
|
||||
margin-top: 2px;
|
||||
color: #7b7f86;
|
||||
font-size: 10px;
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: var(--font-size-micro);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-shell {
|
||||
width: min(1840px, 100%);
|
||||
width: min(var(--admin-shell-max-width), 100%);
|
||||
height: calc(100vh - 238px);
|
||||
min-height: 560px;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
background: var(--admin-shell-bg);
|
||||
border: 1px solid rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid var(--color-bg-overlay-strong);
|
||||
border-radius: var(--admin-shell-radius);
|
||||
box-shadow: var(--admin-shadow);
|
||||
backdrop-filter: blur(14px);
|
||||
@@ -401,23 +403,23 @@ function handleLogout() {
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
color: var(--admin-sidebar-text);
|
||||
background: linear-gradient(180deg, #202d38 0%, #192733 100%);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.06);
|
||||
transition: width 0.2s ease;
|
||||
background: var(--admin-sidebar-gradient);
|
||||
border-right: 1px solid var(--admin-sidebar-hairline-soft);
|
||||
transition: width var(--motion-normal) var(--ease-standard);
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
height: 82px;
|
||||
padding: 17px 16px;
|
||||
padding: 17px var(--space-4);
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-bottom: 1px solid var(--admin-sidebar-hairline);
|
||||
}
|
||||
|
||||
.brand.compact {
|
||||
justify-content: center;
|
||||
padding: 16px 8px;
|
||||
padding: var(--space-4) var(--space-2);
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
@@ -435,15 +437,15 @@ function handleLogout() {
|
||||
}
|
||||
|
||||
.brand-copy strong {
|
||||
color: #fff;
|
||||
font-size: 17px;
|
||||
color: var(--color-text-inverse);
|
||||
font-size: var(--font-size-lg);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.brand-copy span {
|
||||
margin-top: 4px;
|
||||
color: #b5c0c8;
|
||||
font-size: 10px;
|
||||
margin-top: var(--space-1);
|
||||
color: var(--color-text-on-dark-muted);
|
||||
font-size: var(--font-size-micro);
|
||||
letter-spacing: 1.5px;
|
||||
}
|
||||
|
||||
@@ -457,49 +459,54 @@ function handleLogout() {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* 主导航行项:桌面密度下取 44px(兼顾触屏笔记本,且远高于 WCAG 2.5.8 的 24px 下限) */
|
||||
.sidebar-menu :deep(.el-menu-item) {
|
||||
height: 41px;
|
||||
height: var(--tap-min);
|
||||
margin: 2px 0;
|
||||
color: var(--admin-sidebar-text);
|
||||
border-radius: 9px;
|
||||
border-radius: var(--border-radius-sm);
|
||||
transition: background var(--motion-fast) var(--ease-standard),
|
||||
color var(--motion-fast) var(--ease-standard);
|
||||
}
|
||||
|
||||
.sidebar-menu :deep(.el-menu-item:hover) {
|
||||
color: #fff;
|
||||
color: var(--color-text-inverse);
|
||||
background: var(--admin-sidebar-surface);
|
||||
}
|
||||
|
||||
/* 激活态原先压的是明亮珊瑚渐变(#f45f68→#ee727b),白字只有 3.14:1;
|
||||
改用达标渐变(两端 4.60:1 / 5.33:1)。 */
|
||||
.sidebar-menu :deep(.el-menu-item.is-active) {
|
||||
color: #fff;
|
||||
font-weight: 650;
|
||||
background: linear-gradient(135deg, #f45f68, #ee727b);
|
||||
box-shadow: 0 8px 18px rgba(244, 95, 104, 0.26);
|
||||
color: var(--color-text-inverse);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
background: var(--gradient-brand-solid);
|
||||
box-shadow: var(--shadow-brand);
|
||||
}
|
||||
|
||||
.sidebar-menu :deep(.el-icon) {
|
||||
font-size: 17px;
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
.sidebar-motto {
|
||||
display: flex;
|
||||
margin: 8px 15px 2px;
|
||||
padding: 14px 12px;
|
||||
margin: var(--space-2) 15px 2px;
|
||||
padding: 14px var(--space-3);
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
color: #ff969c;
|
||||
font-family: KaiTi, STKaiti, serif;
|
||||
font-size: 15px;
|
||||
color: var(--admin-coral-on-dark);
|
||||
font-family: var(--font-family-brand);
|
||||
font-size: var(--font-size-md);
|
||||
line-height: 1.45;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-top: 1px solid var(--admin-sidebar-hairline);
|
||||
}
|
||||
|
||||
.sidebar-motto .el-icon {
|
||||
font-size: 31px;
|
||||
font-size: var(--font-size-4xl);
|
||||
}
|
||||
|
||||
.collapse-control {
|
||||
display: flex;
|
||||
height: 42px;
|
||||
min-height: var(--tap-min);
|
||||
margin: 2px 10px 10px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -508,10 +515,14 @@ function handleLogout() {
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: var(--border-radius-sm);
|
||||
transition: color var(--motion-fast) var(--ease-standard),
|
||||
background var(--motion-fast) var(--ease-standard);
|
||||
}
|
||||
|
||||
.collapse-control:hover {
|
||||
color: #fff;
|
||||
color: var(--color-text-inverse);
|
||||
background: var(--admin-sidebar-surface);
|
||||
}
|
||||
|
||||
.workspace {
|
||||
@@ -520,12 +531,12 @@ function handleLogout() {
|
||||
|
||||
.admin-topbar {
|
||||
display: flex;
|
||||
height: 64px;
|
||||
padding: 0 24px;
|
||||
height: var(--header-height);
|
||||
padding: 0 var(--space-6);
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
border-bottom: 1px solid #edf0f2;
|
||||
background: var(--color-bg-overlay-strong);
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
}
|
||||
|
||||
.page-context,
|
||||
@@ -537,15 +548,18 @@ function handleLogout() {
|
||||
|
||||
.page-context {
|
||||
min-width: 180px;
|
||||
gap: 12px;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.mobile-collapse {
|
||||
display: none;
|
||||
color: #56616a;
|
||||
min-width: var(--tap-icon);
|
||||
min-height: var(--tap-icon);
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
.header-tools {
|
||||
@@ -557,57 +571,59 @@ function handleLogout() {
|
||||
}
|
||||
|
||||
.global-search :deep(.el-input__wrapper) {
|
||||
background: #f6f7f8;
|
||||
border-radius: 9px;
|
||||
box-shadow: inset 0 0 0 1px #e8eaed;
|
||||
background: var(--color-bg-input);
|
||||
border-radius: var(--border-radius-sm);
|
||||
box-shadow: inset 0 0 0 1px var(--color-border);
|
||||
}
|
||||
|
||||
.notification-button {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
color: #4f5b65;
|
||||
width: var(--tap-icon);
|
||||
height: var(--tap-icon);
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 9px;
|
||||
border-radius: var(--border-radius-sm);
|
||||
place-items: center;
|
||||
transition: background var(--motion-fast) var(--ease-standard);
|
||||
}
|
||||
|
||||
.notification-button:hover {
|
||||
background: #f6f7f8;
|
||||
background: var(--color-bg-input);
|
||||
}
|
||||
|
||||
/* 角标字号原为 9px(低于可读下限),提到 11px,容器同步放大以容纳 */
|
||||
.notification-badge {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 1px;
|
||||
min-width: 15px;
|
||||
height: 15px;
|
||||
min-width: var(--space-4);
|
||||
height: var(--space-4);
|
||||
padding: 0 3px;
|
||||
color: #fff;
|
||||
font-size: 9px;
|
||||
line-height: 15px;
|
||||
color: var(--color-text-inverse);
|
||||
font-size: var(--font-size-micro);
|
||||
line-height: var(--space-4);
|
||||
text-align: center;
|
||||
background: var(--admin-coral);
|
||||
border-radius: 999px;
|
||||
background: var(--admin-coral-solid);
|
||||
border-radius: var(--border-radius-pill);
|
||||
}
|
||||
|
||||
.user-info {
|
||||
gap: 8px;
|
||||
gap: var(--space-2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.user-info .el-avatar {
|
||||
color: #fff;
|
||||
background: var(--admin-coral);
|
||||
color: var(--color-text-inverse);
|
||||
background: var(--admin-coral-solid);
|
||||
}
|
||||
|
||||
.user-info strong {
|
||||
max-width: 92px;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
font-size: var(--font-size-sm);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -615,27 +631,28 @@ function handleLogout() {
|
||||
.admin-content {
|
||||
padding: 0;
|
||||
overflow: auto;
|
||||
background: var(--admin-page-bg);
|
||||
background: var(--color-bg-page);
|
||||
}
|
||||
|
||||
.scene-footer {
|
||||
display: flex;
|
||||
max-width: 1840px;
|
||||
height: 32px;
|
||||
max-width: var(--admin-shell-max-width);
|
||||
height: var(--space-8);
|
||||
margin: 0 auto;
|
||||
align-items: flex-end;
|
||||
gap: 10px;
|
||||
color: #8d8587;
|
||||
font-size: 11px;
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: var(--font-size-micro);
|
||||
}
|
||||
|
||||
.scene-footer strong {
|
||||
color: var(--admin-coral);
|
||||
font-size: 14px;
|
||||
color: var(--admin-coral-strong);
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
|
||||
.scene-footer small {
|
||||
margin-left: auto;
|
||||
font-size: var(--font-size-micro);
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
|
||||
@@ -653,15 +670,15 @@ function handleLogout() {
|
||||
}
|
||||
|
||||
.scene-slogan {
|
||||
padding-right: 12px;
|
||||
padding-right: var(--space-3);
|
||||
}
|
||||
|
||||
.scene-slogan span {
|
||||
font-size: 14px;
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
|
||||
.scene-slogan strong {
|
||||
font-size: 20px;
|
||||
font-size: var(--font-size-2xl);
|
||||
}
|
||||
|
||||
.scene-brand > img {
|
||||
@@ -670,7 +687,7 @@ function handleLogout() {
|
||||
}
|
||||
|
||||
.scene-brand-copy strong {
|
||||
font-size: 22px;
|
||||
font-size: var(--font-size-2xl);
|
||||
}
|
||||
|
||||
.scene-title {
|
||||
@@ -679,8 +696,9 @@ function handleLogout() {
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
/* 紧凑态下正文标题略大于品牌字号,层级保持「页面 → 品牌」的正确指向 */
|
||||
.scene-title h1 {
|
||||
font-size: 27px;
|
||||
font-size: var(--font-size-3xl);
|
||||
}
|
||||
|
||||
.scene-benefits {
|
||||
@@ -707,7 +725,7 @@ function handleLogout() {
|
||||
}
|
||||
|
||||
.admin-sidebar {
|
||||
width: 76px !important;
|
||||
width: var(--sidebar-width-collapsed) !important;
|
||||
}
|
||||
|
||||
.brand-copy,
|
||||
@@ -741,7 +759,7 @@ function handleLogout() {
|
||||
}
|
||||
|
||||
.admin-topbar {
|
||||
padding: 0 12px;
|
||||
padding: 0 var(--space-3);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -183,13 +183,13 @@ onMounted(loadActivities)
|
||||
}
|
||||
.form-hint {
|
||||
margin-top: 8px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-placeholder);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
.opt-type {
|
||||
float: right;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-placeholder);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
.result-card {
|
||||
margin-top: 12px;
|
||||
|
||||
@@ -434,8 +434,8 @@ onMounted(() => {
|
||||
}
|
||||
.form-hint {
|
||||
margin-top: 4px;
|
||||
color: #8a90a2;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-placeholder);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.partner-row {
|
||||
|
||||
@@ -213,8 +213,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { get } from '@/api/client'
|
||||
import { token } from '@/theme/tokens'
|
||||
|
||||
const loading = ref(false)
|
||||
const dateRange = ref<[string, string]>([
|
||||
@@ -227,87 +228,108 @@ const safetyStats = ref<any>({})
|
||||
const v11 = ref<any>({})
|
||||
const dailySummary = ref<any[]>([])
|
||||
|
||||
const funnelStages = [
|
||||
{ key: 'registered', label: '累计注册', value: 0, rate: null as string | null, color: '#409EFF' },
|
||||
{ key: 'verified', label: '已实名', value: 0, rate: null as string | null, color: '#67C23A' },
|
||||
{ key: 'approved', label: '审核通过', value: 0, rate: null as string | null, color: '#E59A32' },
|
||||
{ key: 'active', label: '7日活跃', value: 0, rate: null as string | null, color: '#E96B6B' }
|
||||
]
|
||||
// 漏斗卡与安全卡一律**派生自上面的 ref**,不再各自持有一份可变副本。
|
||||
//
|
||||
// 历史缺陷(2026-09-23 渲染态实测发现):这三组曾是 `const x = [...]` 普通数组,
|
||||
// loadFunnel() 直接对元素赋值(`x[0].value = ...`),模板读的却是同一个数组 ——
|
||||
// 非响应式写入不触发重渲染,于是卡片恒显示 0,而同一张卡里的汇总表(走 ref)
|
||||
// 显示 12846/9217/8462/3812,界面自相矛盾。
|
||||
// 改为 computed 派生后两者同源,结构上不可能再对不上。
|
||||
const funnelStages = computed(() => [
|
||||
{
|
||||
key: 'registered', label: '累计注册',
|
||||
value: funnelStats.value.totalRegistered || 0,
|
||||
rate: null as string | null, color: token('--color-info')
|
||||
},
|
||||
{
|
||||
key: 'verified', label: '已实名',
|
||||
value: funnelStats.value.verifiedCount || 0,
|
||||
rate: funnelStats.value.verifyRate || '-', color: token('--color-success')
|
||||
},
|
||||
{
|
||||
key: 'approved', label: '审核通过',
|
||||
value: funnelStats.value.profileApproved || 0,
|
||||
rate: funnelStats.value.approvalRate || '-', color: token('--color-warning')
|
||||
},
|
||||
{
|
||||
key: 'active', label: '7日活跃',
|
||||
value: funnelStats.value.activeUsers || 0,
|
||||
rate: funnelStats.value.activeRate || '-', color: token('--color-primary')
|
||||
}
|
||||
])
|
||||
|
||||
const matchStages = [
|
||||
{ key: 'recommended', label: '推荐', value: 0, rate: '-', color: '#409EFF' },
|
||||
{ key: 'liked', label: '喜欢', value: 0, rate: '-', color: '#67C23A' },
|
||||
{ key: 'matches', label: 'Match', value: 0, rate: '-', color: '#E59A32' },
|
||||
{ key: 'chats', label: '聊天', value: 0, rate: '-', color: '#909399' },
|
||||
{ key: 'knowing', label: '了解中', value: 0, rate: '-', color: '#E96B6B' },
|
||||
{ key: 'success', label: '成功牵手', value: 0, rate: '-', color: '#F56C6C' }
|
||||
]
|
||||
const matchStages = computed(() => [
|
||||
{
|
||||
key: 'recommended', label: '推荐',
|
||||
value: matchFunnelStats.value.totalRecommended || 0,
|
||||
rate: '-', color: token('--color-info')
|
||||
},
|
||||
{
|
||||
key: 'liked', label: '喜欢',
|
||||
value: matchFunnelStats.value.totalLiked || 0,
|
||||
rate: matchFunnelStats.value.likeRate || '-', color: token('--color-success')
|
||||
},
|
||||
{
|
||||
key: 'matches', label: 'Match',
|
||||
value: matchFunnelStats.value.totalMatches || 0,
|
||||
rate: matchFunnelStats.value.matchRate || '-', color: token('--color-warning')
|
||||
},
|
||||
{
|
||||
key: 'chats', label: '聊天',
|
||||
value: matchFunnelStats.value.chatsStarted || 0,
|
||||
rate: matchFunnelStats.value.chatRate || '-', color: token('--color-neutral')
|
||||
},
|
||||
{
|
||||
key: 'knowing', label: '了解中',
|
||||
value: matchFunnelStats.value.gettingToKnow || 0,
|
||||
rate: '-', color: token('--color-primary')
|
||||
},
|
||||
{
|
||||
key: 'success', label: '成功牵手',
|
||||
value: matchFunnelStats.value.successPairs || 0,
|
||||
rate: matchFunnelStats.value.successRate || '-', color: token('--color-danger')
|
||||
}
|
||||
])
|
||||
|
||||
const safetyCards = [
|
||||
{ key: 'pending', label: '待处理举报', value: 0, color: '#E96B6B' },
|
||||
{ key: 'resolved', label: '已处理举报', value: 0, color: '#67C23A' },
|
||||
{ key: 'highRisk', label: '高风险用户', value: 0, color: '#E59A32' },
|
||||
{ key: 'banned', label: '封禁用户', value: 0, color: '#F56C6C' }
|
||||
]
|
||||
const safetyCards = computed(() => [
|
||||
{ key: 'pending', label: '待处理举报', value: safetyStats.value.pendingReports || 0, color: token('--color-primary') },
|
||||
{ key: 'resolved', label: '已处理举报', value: safetyStats.value.resolvedReports || 0, color: token('--color-success') },
|
||||
{ key: 'highRisk', label: '高风险用户', value: safetyStats.value.highRiskUsers || 0, color: token('--color-warning') },
|
||||
{ key: 'banned', label: '封禁用户', value: safetyStats.value.bannedUsers || 0, color: token('--color-danger') }
|
||||
])
|
||||
|
||||
async function loadFunnel() {
|
||||
try {
|
||||
const token = localStorage.getItem('accessToken')
|
||||
// 注意:这里曾是 `const token = ...`,与顶部从 @/theme/tokens 导入的**颜色助手**
|
||||
// `token` 同名(函数内遮蔽)。现已改名 authToken,避免后续在函数内误用颜色助手。
|
||||
const authToken = localStorage.getItem('accessToken')
|
||||
if (dateRange.value[0] && dateRange.value[1]) {
|
||||
const [userRes, matchRes, safetyRes, v11Res]: any[] = await Promise.all([
|
||||
get('/operator/analytics/user-funnel', {
|
||||
since: dateRange.value[0], until: dateRange.value[1]
|
||||
}, { Authorization: `Bearer ${token}` }),
|
||||
}, { Authorization: `Bearer ${authToken}` }),
|
||||
get('/operator/analytics/match-funnel', {
|
||||
since: dateRange.value[0], until: dateRange.value[1]
|
||||
}, { Authorization: `Bearer ${token}` }),
|
||||
get('/operator/analytics/safety', {}, { Authorization: `Bearer ${token}` }),
|
||||
}, { Authorization: `Bearer ${authToken}` }),
|
||||
get('/operator/analytics/safety', {}, { Authorization: `Bearer ${authToken}` }),
|
||||
// Phase 20:原先只接了 4/15 个端点,V1.1 公益服务的专项统计
|
||||
// (服务用户数/人次、活动互选 vs 成功、Banner、专项项目)
|
||||
// 后端已实现却无人调用 —— 本行起补齐。
|
||||
get('/operator/analytics/v11-dashboard', {}, { Authorization: `Bearer ${token}` })
|
||||
get('/operator/analytics/v11-dashboard', {}, { Authorization: `Bearer ${authToken}` })
|
||||
])
|
||||
const userData = userRes.data || {}
|
||||
const matchData = matchRes.data || {}
|
||||
const safetyData = safetyRes.data || {}
|
||||
const v11Data = v11Res.data || {}
|
||||
funnelStats.value = userData
|
||||
matchFunnelStats.value = matchData
|
||||
safetyStats.value = safetyData
|
||||
v11.value = v11Data
|
||||
|
||||
// 更新漏斗显示
|
||||
funnelStages[0].value = userData.totalRegistered || 0
|
||||
funnelStages[1].value = userData.verifiedCount || 0
|
||||
funnelStages[1].rate = userData.verifyRate || '-'
|
||||
funnelStages[2].value = userData.profileApproved || 0
|
||||
funnelStages[2].rate = userData.approvalRate || '-'
|
||||
funnelStages[3].value = userData.activeUsers || 0
|
||||
funnelStages[3].rate = userData.activeRate || '-'
|
||||
|
||||
matchStages[0].value = matchData.totalRecommended || 0
|
||||
matchStages[1].value = matchData.totalLiked || 0
|
||||
matchStages[1].rate = matchData.likeRate || '-'
|
||||
matchStages[2].value = matchData.totalMatches || 0
|
||||
matchStages[2].rate = matchData.matchRate || '-'
|
||||
matchStages[3].value = matchData.chatsStarted || 0
|
||||
matchStages[3].rate = matchData.chatRate || '-'
|
||||
matchStages[4].value = matchData.gettingToKnow || 0
|
||||
matchStages[5].value = matchData.successPairs || 0
|
||||
matchStages[5].rate = matchData.successRate || '-'
|
||||
|
||||
safetyCards[0].value = safetyData.pendingReports || 0
|
||||
safetyCards[1].value = safetyData.resolvedReports || 0
|
||||
safetyCards[2].value = safetyData.highRiskUsers || 0
|
||||
safetyCards[3].value = safetyData.bannedUsers || 0
|
||||
// 只落 ref,卡片由上面三个 computed 自动派生 —— 不再手工同步第二份副本。
|
||||
funnelStats.value = userRes.data || {}
|
||||
matchFunnelStats.value = matchRes.data || {}
|
||||
safetyStats.value = safetyRes.data || {}
|
||||
v11.value = v11Res.data || {}
|
||||
}
|
||||
} catch (e) { console.error(e) }
|
||||
}
|
||||
|
||||
async function loadDailySummary() {
|
||||
try {
|
||||
const token = localStorage.getItem('accessToken')
|
||||
const res: any = await get('/operator/analytics/daily-summary', { days: 30 }, { Authorization: `Bearer ${token}` })
|
||||
const authToken = localStorage.getItem('accessToken')
|
||||
const res: any = await get('/operator/analytics/daily-summary', { days: 30 }, { Authorization: `Bearer ${authToken}` })
|
||||
dailySummary.value = res.data || []
|
||||
} catch (e) { console.error(e) }
|
||||
}
|
||||
@@ -329,21 +351,21 @@ onMounted(loadData)
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.funnel-label { font-size: 14px; color: #666; margin-bottom: 8px; }
|
||||
.funnel-value { font-size: 28px; font-weight: bold; color: #333; }
|
||||
.funnel-rate { font-size: 12px; color: #999; margin-top: 4px; }
|
||||
.funnel-label { font-size: var(--font-size-base); color: var(--color-text-tertiary); margin-bottom: 8px; }
|
||||
.funnel-value { font-size: var(--font-size-4xl); font-weight: var(--font-weight-bold); color: var(--color-text-primary); }
|
||||
.funnel-rate { font-size: var(--font-size-xs); color: var(--color-text-placeholder); margin-top: 4px; }
|
||||
.stat-card { text-align: center; }
|
||||
.stat-value { font-size: 32px; font-weight: bold; }
|
||||
.stat-label { font-size: 13px; color: #666; margin-top: 6px; }
|
||||
.stat-value { font-size: var(--font-size-5xl); font-weight: var(--font-weight-bold); }
|
||||
.stat-label { font-size: var(--font-size-sm); color: var(--color-text-tertiary); margin-top: 6px; }
|
||||
.mini-stat {
|
||||
border: 1px solid #ebeef5;
|
||||
border: 1px solid var(--color-border-light);
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
text-align: center;
|
||||
background: #fafafa;
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
.mini-value { font-size: 22px; font-weight: bold; color: #303133; }
|
||||
.mini-label { font-size: 12px; color: #909399; margin-top: 6px; }
|
||||
.metric-note { font-size: 12px; color: #909399; margin-top: 12px; line-height: 1.6; }
|
||||
.unavailable { color: #c0c4cc; font-style: italic; }
|
||||
.mini-value { font-size: var(--font-size-3xl); font-weight: var(--font-weight-bold); color: var(--color-text-primary); }
|
||||
.mini-label { font-size: var(--font-size-xs); color: var(--color-text-placeholder); margin-top: 6px; }
|
||||
.metric-note { font-size: var(--font-size-xs); color: var(--color-text-placeholder); margin-top: 12px; line-height: 1.6; }
|
||||
.unavailable { color: var(--color-text-disabled); font-style: italic; }
|
||||
</style>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</el-descriptions>
|
||||
|
||||
<template v-if="detail.status==='pending'||detail.status==='reviewing'||detail.status==='escalated'">
|
||||
<h4 style="margin:20px 0 12px;color:#E96B6B">审核决策</h4>
|
||||
<h4 style="margin:20px 0 12px;color:var(--admin-coral-strong)">审核决策</h4>
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="处理结果">
|
||||
<el-select v-model="decision" style="width:100%">
|
||||
@@ -86,6 +86,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { get, post } from '@/api/client'
|
||||
import { token as tokenColor } from '@/theme/tokens'
|
||||
|
||||
const loading = ref(false)
|
||||
const processing = ref(false)
|
||||
@@ -122,11 +123,11 @@ async function loadStats() {
|
||||
const res: any = await get('/operator/appeals/stats', {}, { Authorization: `Bearer ${token}` })
|
||||
const data = res.data || {}
|
||||
stats.value = [
|
||||
{ key: 'pending', label: '待审核', value: data.pendingCount || 0, color: '#E96B6B' },
|
||||
{ key: 'reviewing', label: '审核中', value: data.reviewingCount || 0, color: '#E59A32' },
|
||||
{ key: 'approved', label: '已通过', value: data.approvedCount || 0, color: '#67C23A' },
|
||||
{ key: 'rejected', label: '已驳回', value: data.rejectedCount || 0, color: '#909399' },
|
||||
{ key: 'escalated', label: '已升级', value: data.escalatedCount || 0, color: '#F56C6C' }
|
||||
{ key: 'pending', label: '待审核', value: data.pendingCount || 0, color: tokenColor('--color-primary') },
|
||||
{ key: 'reviewing', label: '审核中', value: data.reviewingCount || 0, color: tokenColor('--color-warning') },
|
||||
{ key: 'approved', label: '已通过', value: data.approvedCount || 0, color: tokenColor('--color-success') },
|
||||
{ key: 'rejected', label: '已驳回', value: data.rejectedCount || 0, color: tokenColor('--color-neutral') },
|
||||
{ key: 'escalated', label: '已升级', value: data.escalatedCount || 0, color: tokenColor('--color-danger') }
|
||||
]
|
||||
} catch {}
|
||||
}
|
||||
@@ -168,8 +169,8 @@ onMounted(loadData)
|
||||
<style scoped>
|
||||
.page-container { padding: 24px; }
|
||||
.stat-card { text-align: center; }
|
||||
.stat-value { font-size: 32px; font-weight: bold; color: #409EFF; }
|
||||
.stat-label { font-size: 13px; color: #666; margin-top: 6px; }
|
||||
.stat-value { font-size: var(--font-size-5xl); font-weight: var(--font-weight-bold); color: var(--admin-coral-strong); }
|
||||
.stat-label { font-size: var(--font-size-sm); color: var(--color-text-tertiary); margin-top: 6px; }
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.text-muted { color: #BBB; font-size: 13px; }
|
||||
.text-muted { color: var(--color-text-disabled); font-size: var(--font-size-sm); }
|
||||
</style>
|
||||
|
||||
@@ -302,21 +302,21 @@ onMounted(loadBanners)
|
||||
<style scoped>
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.field-hint { width: 100%; font-size: 12px; color: var(--el-text-color-secondary); line-height: 1.5; }
|
||||
.preview-stage { padding: 20px; background: #f5f6f8; border-radius: 12px; }
|
||||
.preview-stage { padding: 20px; background: var(--color-bg-subtle); border-radius: 12px; }
|
||||
.preview-card {
|
||||
padding: 20px 18px;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(135deg, #ffe9df, #fff7f2);
|
||||
background: var(--gradient-brand-soft); /* 还原消费端 Banner 预览底色 */
|
||||
box-shadow: 0 6px 16px rgba(200, 120, 100, 0.16);
|
||||
}
|
||||
.preview-tag {
|
||||
display: inline-block; padding: 2px 10px; border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.85); color: #c8553d; font-size: 12px;
|
||||
background: var(--color-bg-overlay); color: var(--admin-coral-strong); font-size: var(--font-size-xs);
|
||||
}
|
||||
.preview-title { margin-top: 10px; font-size: 17px; font-weight: 700; color: #2c2c34; }
|
||||
.preview-subtitle { margin-top: 6px; font-size: 12px; line-height: 1.6; color: #6b6b78; }
|
||||
.preview-title { margin-top: 10px; font-size: var(--font-size-xl); font-weight: var(--font-weight-bold); color: var(--color-text-primary); }
|
||||
.preview-subtitle { margin-top: 6px; font-size: var(--font-size-xs); line-height: 1.6; color: var(--color-text-tertiary); }
|
||||
.preview-button {
|
||||
display: inline-block; margin-top: 14px; padding: 5px 16px; border-radius: 999px;
|
||||
background: linear-gradient(135deg, #ff7d6e, #ff5c82); color: #fff; font-size: 12px; font-weight: 600;
|
||||
background: var(--gradient-brand-vivid); color: var(--color-text-inverse); font-size: var(--font-size-xs); font-weight: var(--font-weight-semibold); min-height: var(--tap-control); /* 还原消费端 Banner 按钮外观预览 */
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -253,5 +253,5 @@ onMounted(() => { loadCategories(); loadArticles() })
|
||||
|
||||
<style scoped>
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.field-hint { width: 100%; font-size: 12px; color: var(--el-text-color-secondary); line-height: 1.5; }
|
||||
.field-hint { width: 100%; font-size: var(--font-size-xs); color: var(--el-text-color-secondary); line-height: 1.5; }
|
||||
</style>
|
||||
|
||||
@@ -284,5 +284,5 @@ onMounted(loadPage)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container{padding:28px}.page-kicker{color:var(--admin-primary);font-size:12px;font-weight:650;letter-spacing:1px}.stat-tags{display:flex;gap:8px}.review-panel{padding:20px 24px}.muted{color:var(--color-text-tertiary);font-size:12px}.drawer-loading{min-height:240px}.drawer-heading{margin:24px 0 12px;font-size:16px}.full-width{width:100%}.review-actions{display:flex;flex-wrap:wrap;gap:8px}@media(max-width:760px){.page-container{padding:18px}}
|
||||
.page-container{padding:28px}.page-kicker{color:var(--admin-primary);font-size:var(--font-size-xs);font-weight:var(--font-weight-semibold);letter-spacing:1px}.stat-tags{display:flex;gap:8px}.review-panel{padding:20px 24px}.muted{color:var(--color-text-tertiary);font-size:var(--font-size-xs)}.drawer-loading{min-height:240px}.drawer-heading{margin:24px 0 12px;font-size:var(--font-size-lg)}.full-width{width:100%}.review-actions{display:flex;flex-wrap:wrap;gap:8px}@media(max-width:760px){.page-container{padding:18px}}
|
||||
</style>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
<el-descriptions-item label="补充说明" :span="2">{{ detail.report?.description || '无' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<h4 style="margin:20px 0 12px;color:#E96B6B">被举报人风险画像</h4>
|
||||
<h4 style="margin:20px 0 12px;color:var(--admin-coral-strong)">被举报人风险画像</h4>
|
||||
<el-descriptions :column="2" border v-if="detail.riskProfile">
|
||||
<el-descriptions-item label="用户">{{ detail.riskProfile.displayName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="风险分数">{{ detail.riskProfile.totalRiskScore }}</el-descriptions-item>
|
||||
@@ -72,7 +72,7 @@
|
||||
<el-descriptions-item label="历史举报数">{{ detail.riskProfile.totalReportsCount }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<h4 style="margin:20px 0 12px;color:#E96B6B">处置操作</h4>
|
||||
<h4 style="margin:20px 0 12px;color:var(--admin-coral-strong)">处置操作</h4>
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="处理结果">
|
||||
<el-select v-model="processResult" style="width:100%">
|
||||
@@ -100,6 +100,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { get, post } from '@/api/client'
|
||||
import { token as tokenColor } from '@/theme/tokens'
|
||||
|
||||
const loading = ref(false)
|
||||
const processing = ref(false)
|
||||
@@ -137,10 +138,10 @@ async function loadStats() {
|
||||
const res: any = await get('/operator/risk/reports/stats', {}, { Authorization: `Bearer ${token}` })
|
||||
const data = res.data || {}
|
||||
stats.value = [
|
||||
{ key: 'pending', label: '待处理', value: data.pendingCount || 0, color: '#E96B6B' },
|
||||
{ key: 'processing', label: '处理中', value: data.processingCount || 0, color: '#E59A32' },
|
||||
{ key: 'resolved', label: '已处理', value: data.resolvedCount || 0, color: '#67C23A' },
|
||||
{ key: 'dismissed', label: '已驳回', value: data.dismissedCount || 0, color: '#909399' }
|
||||
{ key: 'pending', label: '待处理', value: data.pendingCount || 0, color: tokenColor('--color-primary') },
|
||||
{ key: 'processing', label: '处理中', value: data.processingCount || 0, color: tokenColor('--color-warning') },
|
||||
{ key: 'resolved', label: '已处理', value: data.resolvedCount || 0, color: tokenColor('--color-success') },
|
||||
{ key: 'dismissed', label: '已驳回', value: data.dismissedCount || 0, color: tokenColor('--color-neutral') }
|
||||
]
|
||||
} catch {}
|
||||
}
|
||||
@@ -188,7 +189,7 @@ onMounted(loadData)
|
||||
<style scoped>
|
||||
.page-container { padding: 24px; }
|
||||
.stat-card { text-align: center; }
|
||||
.stat-value { font-size: 36px; font-weight: bold; }
|
||||
.stat-label { font-size: 14px; color: #666; margin-top: 8px; }
|
||||
.stat-value { font-size: var(--font-size-5xl); font-weight: var(--font-weight-bold); }
|
||||
.stat-label { font-size: var(--font-size-base); color: var(--color-text-tertiary); margin-top: 8px; }
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
</style>
|
||||
|
||||
@@ -348,6 +348,6 @@ onMounted(loadStations)
|
||||
.form-hint {
|
||||
margin-left: 10px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<div class="identity-summary">
|
||||
<el-avatar :size="58">{{ (user.displayName || '用')[0] }}</el-avatar>
|
||||
<div><h2>{{ user.displayName || '未设置显示名称' }}</h2><span>用户编号:{{ user.id }}</span></div>
|
||||
<el-progress class="completion" type="dashboard" :percentage="user.profileCompletion || 0" :width="72" :stroke-width="7" color="#EE666B" />
|
||||
<el-progress class="completion" type="dashboard" :percentage="user.profileCompletion || 0" :width="72" :stroke-width="7" color="var(--admin-coral-solid)" />
|
||||
</div>
|
||||
<el-tabs v-model="activeTab" class="detail-tabs">
|
||||
<el-tab-pane label="基础信息" name="basic">
|
||||
@@ -129,5 +129,5 @@ function formatTime(time?: string) { return time ? new Date(time).toLocaleString
|
||||
onMounted(loadDetail)
|
||||
</script>
|
||||
<style scoped>
|
||||
.page-container{padding:28px}.back-button{margin-bottom:8px}.heading-tags{display:flex;gap:8px}.detail-card{padding:24px}.identity-summary{display:flex;align-items:center;gap:14px;padding-bottom:20px;border-bottom:1px solid var(--color-border-light)}.identity-summary .el-avatar{color:#fff;background:var(--admin-primary)}.identity-summary h2{font-size:20px}.identity-summary span{display:block;margin-top:6px;color:var(--color-text-tertiary);font-size:12px}.completion{margin-left:auto}.detail-tabs{margin-top:18px}.masked{margin-right:8px;color:var(--color-text-tertiary)}.risk-summary{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}.risk-summary article{padding:18px;background:var(--admin-soft-pink);border-radius:12px}.risk-summary span{color:var(--color-text-secondary)}.risk-summary strong{display:block;margin-top:8px;font-size:26px}.danger-zone{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-top:20px;padding:18px;border:1px solid #F3CCCC;border-radius:12px}.danger-zone h3{display:flex;align-items:center;gap:7px;color:var(--color-danger)}.danger-zone p{margin-top:6px;color:var(--color-text-secondary)}.status-form{margin-top:18px}.full-width{width:100%}@media(max-width:760px){.page-container{padding:18px}.risk-summary{grid-template-columns:1fr}.danger-zone{align-items:flex-start;flex-direction:column}}
|
||||
.page-container{padding:28px}.back-button{margin-bottom:8px}.heading-tags{display:flex;gap:8px}.detail-card{padding:24px}.identity-summary{display:flex;align-items:center;gap:14px;padding-bottom:20px;border-bottom:1px solid var(--color-border-light)}.identity-summary .el-avatar{color:var(--color-text-inverse);background:var(--admin-coral-solid)}.identity-summary h2{font-size:var(--font-size-2xl)}.identity-summary span{display:block;margin-top:6px;color:var(--color-text-tertiary);font-size:var(--font-size-xs)}.completion{margin-left:auto}.detail-tabs{margin-top:18px}.masked{margin-right:8px;color:var(--color-text-tertiary)}.risk-summary{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}.risk-summary article{padding:18px;background:var(--admin-soft-pink);border-radius:12px}.risk-summary span{color:var(--color-text-secondary)}.risk-summary strong{display:block;margin-top:8px;font-size:var(--font-size-4xl)}.danger-zone{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-top:20px;padding:18px;border:1px solid var(--color-border-danger);border-radius:12px}.danger-zone h3{display:flex;align-items:center;gap:7px;color:var(--color-danger-strong)}.danger-zone p{margin-top:6px;color:var(--color-text-secondary)}.status-form{margin-top:18px}.full-width{width:100%}@media(max-width:760px){.page-container{padding:18px}.risk-summary{grid-template-columns:1fr}.danger-zone{align-items:flex-start;flex-direction:column}}
|
||||
</style>
|
||||
|
||||
@@ -206,8 +206,8 @@ onMounted(loadData)
|
||||
|
||||
.page-kicker {
|
||||
color: var(--admin-primary);
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
|
||||
+378
-59
@@ -1,93 +1,412 @@
|
||||
/* ============================================================================
|
||||
* 管理端设计系统 · v2
|
||||
*
|
||||
* 本文件是管理端唯一的设计 token 事实源。24 个 .vue 的 <style> 只允许通过
|
||||
* var(--token) 取色 / 取字号 / 取间距,不应出现字面色值(由门控脚本
|
||||
* scripts/lint-admin-tokens.mjs 拦截)。
|
||||
*
|
||||
* 立此版本的原因(2026-09-22 审计实测,不是主观判断):
|
||||
* · 97 处硬编码颜色 / 67 种色值:6 种近似粉底(#FFF0F1 #FFF3F2 #FFF6F6
|
||||
* #FFF7F7 #FFF8F7 #FFFAFA)、6 种近似浅灰边框、大量灰阶碎片。
|
||||
* · 88 处硬编码字号 / 23 种档位,其中 20 处 ≤11px(9px 见于 .metric-copy em、
|
||||
* .activity-status、.todo-copy small),低于可读下限。
|
||||
* · --color-text-tertiary(#888888) 白底 3.54:1、--color-warning(#E59A32) 2.33:1、
|
||||
* --admin-coral(#F45F68) 3.14:1 —— 在各自用法下均不达 WCAG AA 的 4.5:1。
|
||||
* · Element Plus 的 --el-color-*-light-N / -dark-2 / -rgb 是**编译期静态生成**的,
|
||||
* 项目只覆盖了 --el-color-primary,于是开关、滑块、焦点环、分页、标签等
|
||||
* 组件仍是默认蓝 —— 品牌色与 EP 蓝混用。
|
||||
*
|
||||
* 三条硬规则:
|
||||
* 1) 品牌色三级分化:基础色只做色块/渐变/图形(不承载文字);
|
||||
* -strong 做白底文字;-solid 做实心底压白字;两者均实算 ≥4.5:1。
|
||||
* 2) 字重只用 400 / 500 / 600 / 700 四档(跨平台缺字重会回退,导致渲染不一致)。
|
||||
* 3) 字号有下限:正文最小 12px,角标/计数不得低于 11px。
|
||||
*
|
||||
* 所有对比度数值均由脚本按 WCAG 相对亮度公式实算(非估算),标注在各 token 后。
|
||||
* ========================================================================== */
|
||||
|
||||
:root {
|
||||
--admin-coral: #f45f68;
|
||||
--admin-coral-hover: #df505a;
|
||||
--admin-coral-soft: #fff0f1;
|
||||
--admin-shell-bg: rgba(255, 255, 255, 0.96);
|
||||
--admin-sidebar-bg: #202d38;
|
||||
--admin-sidebar-surface: #2b3a46;
|
||||
--admin-sidebar-text: #D9E1E6;
|
||||
--admin-sidebar-muted: #8FA1AC;
|
||||
--admin-page-bg: #f7f9fb;
|
||||
/* ---------------- 品牌色 ---------------- */
|
||||
/* 基础色:仅用于色块 / 渐变 / 图标 / 边框氛围。白底作文字只有 3.14:1,禁止承载文字。 */
|
||||
--admin-coral: #F45F68;
|
||||
--admin-coral-hover: #E5535D;
|
||||
--admin-coral-strong: #C0414A; /* 白底文字 5.13:1 */
|
||||
--admin-coral-solid: #C84A53; /* 实心底压白字 4.60:1 */
|
||||
--admin-coral-solid-hover: #B8434A; /* hover 反而更深更清晰 5.33:1 */
|
||||
--admin-coral-on-dark: #FF969C; /* 侧栏深底文字 6.76:1 */
|
||||
--admin-coral-soft: #FFF0F1; /* 品牌浅底(角标 / 选中态底) */
|
||||
--admin-coral-lighter: #FFF8F7; /* 更浅一档,hover 底 */
|
||||
--admin-coral-border: rgba(244, 95, 104, 0.22);
|
||||
/* 承载白字的品牌渐变(两端均 ≥4.5:1)。基础色渐变见 --gradient-brand-vivid。 */
|
||||
--gradient-brand-solid: linear-gradient(135deg, #C84A53 0%, #B8434A 100%);
|
||||
/* 鲜亮品牌渐变:只允许用在「无文字覆盖」的装饰,或预览态还原(如 Banner 预览)。 */
|
||||
--gradient-brand-vivid: linear-gradient(135deg, #FF7D6E 0%, #FF5C82 100%);
|
||||
--gradient-brand-soft: linear-gradient(135deg, #FFE9DF 0%, #FFF7F2 100%);
|
||||
/* 水平进度条填充(对轨道 --color-primary-light):原 #F56B75→#EF5A65 仅 2.53:1,
|
||||
条与轨道难以区分;改达标渐变后 4.01:1。 */
|
||||
--gradient-brand-bar: linear-gradient(90deg, #C84A53 0%, #B8434A 100%);
|
||||
|
||||
/* 兼容既有引用名(全站仍在用) */
|
||||
--admin-primary: var(--admin-coral);
|
||||
--admin-primary-hover: var(--admin-coral-hover);
|
||||
--admin-card-radius: 14px;
|
||||
--admin-shell-radius: 22px;
|
||||
--admin-shadow: 0 18px 48px rgba(57, 40, 45, 0.14);
|
||||
--admin-header-bg: #FFFFFF;
|
||||
--admin-soft-pink: var(--admin-coral-soft);
|
||||
--admin-soft-blue: #EEF5FF;
|
||||
--admin-soft-green: #EDF8F1;
|
||||
--admin-soft-purple: #F4EFFF;
|
||||
--color-primary: var(--admin-primary);
|
||||
--color-primary-hover: var(--admin-primary-hover);
|
||||
--color-primary-light: #FFF3F2;
|
||||
--color-primary-lighter: #FFF8F7;
|
||||
--color-success: #2E9B68;
|
||||
--color-warning: #E59A32;
|
||||
--color-danger: #D94A4A;
|
||||
--color-info: #4A7CD4;
|
||||
--color-text-primary: #222222;
|
||||
--color-text-secondary: #555555;
|
||||
--color-text-tertiary: #888888;
|
||||
--admin-primary-strong: var(--admin-coral-strong);
|
||||
--admin-primary-solid: var(--admin-coral-solid);
|
||||
--color-primary: var(--admin-coral);
|
||||
--color-primary-hover: var(--admin-coral-hover);
|
||||
--color-primary-strong: var(--admin-coral-strong);
|
||||
--color-primary-solid: var(--admin-coral-solid);
|
||||
--color-primary-light: var(--admin-coral-soft);
|
||||
--color-primary-lighter: var(--admin-coral-lighter);
|
||||
|
||||
/* ---------------- 语义色五组 ----------------
|
||||
基色做图形/色块;-strong 做文字(实算 ≥4.5:1);-soft 为配套浅底。 */
|
||||
--color-success: #2E9B68; /* 白底 3.50:1 —— 仅图形 */
|
||||
--color-success-strong: #1F7A4F; /* 白底 5.31:1 */
|
||||
--color-success-soft: #EDF8F1;
|
||||
--color-warning: #E59A32; /* 白底 2.33:1 —— 仅图形 */
|
||||
--color-warning-strong: #8F5D0F; /* 白底 5.61:1 */
|
||||
--color-warning-soft: #FFF6E6;
|
||||
--color-danger: #D94A4A; /* 白底 4.18:1 —— 图形 / ≥24px 大字 */
|
||||
--color-danger-strong: #B8353A; /* 白底 5.82:1 */
|
||||
--color-danger-soft: #FFF6F6;
|
||||
--color-info: #4A7CD4; /* 白底 4.10:1 —— 图形 */
|
||||
--color-info-strong: #3A63B0; /* 白底 5.85:1 */
|
||||
--color-info-soft: #EEF5FF;
|
||||
--color-purple: #8B63BC;
|
||||
--color-purple-strong: #6B47A0; /* 白底 6.92:1 */
|
||||
--color-purple-soft: #F4EFFF;
|
||||
--color-neutral: #8A9099;
|
||||
--color-neutral-strong: #656C79; /* 白底 5.28:1 */
|
||||
--color-neutral-soft: #F4F5F7;
|
||||
|
||||
/* ---------------- 文字 ---------------- */
|
||||
--color-text-primary: #1F2328; /* 白底 15.80:1 */
|
||||
--color-text-secondary: #555555; /* 白底 7.46:1 */
|
||||
--color-text-tertiary: #676E7B; /* 白底 5.13:1;浅底 #F5F6F8 上 4.74:1(原 #888 仅 3.54:1) */
|
||||
--color-text-placeholder: #8A9099; /* 3.25:1:占位符按惯例弱于正文一档(EP 默认 2.5:1) */
|
||||
--color-text-disabled: #B9BEC7;
|
||||
--color-text-inverse: #FFFFFF;
|
||||
--color-text-on-dark: #D9E1E6; /* 侧栏深底 10.62:1 */
|
||||
--color-text-on-dark-muted: #8FA1AC; /* 侧栏深底 5.26:1 */
|
||||
--color-icon-muted: #D7DCE0; /* 空态大图标等纯装饰 */
|
||||
|
||||
/* ---------------- 背景 ---------------- */
|
||||
--color-bg-primary: #FFFFFF;
|
||||
--color-bg-secondary: #FFF8F7;
|
||||
--color-bg-page: #FAFAF8;
|
||||
--color-border: #E8E5E2;
|
||||
--color-border-light: #F0EDE9;
|
||||
--color-bg-page: #F7F9FB;
|
||||
--color-bg-secondary: #FAFAFB;
|
||||
--color-bg-subtle: #F5F6F8;
|
||||
--color-bg-input: #F6F7F8;
|
||||
--color-bg-overlay: rgba(255, 255, 255, 0.86);
|
||||
--color-bg-overlay-strong: rgba(255, 255, 255, 0.96);
|
||||
|
||||
/* ---------------- 边框 ---------------- */
|
||||
--color-border: #E4E7EB;
|
||||
--color-border-light: #EDF0F2;
|
||||
--color-border-hairline: #F2F3F4;
|
||||
--color-border-strong: #D7DCE0;
|
||||
--color-border-danger: #F3CCCC;
|
||||
|
||||
/* ---------------- 侧栏(深底) ---------------- */
|
||||
--admin-sidebar-bg: #202D38;
|
||||
--admin-sidebar-bg-deep: #192733;
|
||||
--admin-sidebar-gradient: linear-gradient(180deg, #202D38 0%, #192733 100%);
|
||||
--admin-sidebar-surface: #2B3A46;
|
||||
--admin-sidebar-text: #D9E1E6;
|
||||
--admin-sidebar-muted: #8FA1AC;
|
||||
--admin-sidebar-hairline: rgba(255, 255, 255, 0.08);
|
||||
--admin-sidebar-hairline-soft: rgba(255, 255, 255, 0.06);
|
||||
|
||||
/* ---------------- 壳与栅格 ---------------- */
|
||||
--admin-shell-bg: rgba(255, 255, 255, 0.96);
|
||||
--admin-header-bg: #FFFFFF;
|
||||
--admin-shell-radius: 22px;
|
||||
--admin-card-radius: 14px;
|
||||
--admin-shell-max-width: 1840px;
|
||||
--admin-page-max-width: 1440px;
|
||||
--admin-content-max-width: 1500px;
|
||||
--sidebar-width: 220px;
|
||||
--sidebar-width-collapsed: 76px;
|
||||
--header-height: 64px;
|
||||
|
||||
/* ---------------- 圆角 ---------------- */
|
||||
--border-radius-xs: 6px;
|
||||
--border-radius-sm: 8px;
|
||||
--border-radius-md: var(--admin-card-radius);
|
||||
--border-radius-md: 14px;
|
||||
--border-radius-lg: 16px;
|
||||
--border-radius-xl: 18px;
|
||||
--border-radius-pill: 999px;
|
||||
|
||||
/* ---------------- 阴影 ---------------- */
|
||||
--shadow-panel: 0 8px 20px rgba(48, 58, 67, 0.035);
|
||||
--shadow-card: 0 8px 24px rgba(75, 55, 46, 0.06);
|
||||
--el-color-primary: var(--color-primary);
|
||||
--shadow-raise: 0 10px 24px rgba(68, 48, 55, 0.1);
|
||||
--shadow-brand: 0 8px 18px rgba(200, 74, 83, 0.26);
|
||||
--admin-shadow: 0 18px 48px rgba(57, 40, 45, 0.14);
|
||||
|
||||
/* ---------------- 排版刻度 ----------------
|
||||
正文 14px 起;角标/计数下限 11px;元信息下限 12px。 */
|
||||
--font-family-base: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
--font-family-brand: KaiTi, STKaiti, serif; /* 品牌书法体:仅装饰性标语 */
|
||||
--font-size-micro: 11px;
|
||||
--font-size-xs: 12px;
|
||||
--font-size-sm: 13px;
|
||||
--font-size-base: 14px;
|
||||
--font-size-md: 15px;
|
||||
--font-size-lg: 16px;
|
||||
--font-size-xl: 18px;
|
||||
--font-size-2xl: 20px;
|
||||
--font-size-3xl: 24px;
|
||||
--font-size-4xl: 28px;
|
||||
--font-size-5xl: 32px;
|
||||
--font-size-display: 38px;
|
||||
--font-line-tight: 1.2;
|
||||
--font-line-snug: 1.35;
|
||||
--font-line-base: 1.6;
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
/* ---------------- 间距 ---------------- */
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--space-8: 32px;
|
||||
--space-10: 40px;
|
||||
|
||||
/* ---------------- 动效 ---------------- */
|
||||
--motion-fast: 140ms;
|
||||
--motion-normal: 200ms;
|
||||
--motion-slow: 320ms;
|
||||
--ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--press-scale: 0.985;
|
||||
--press-opacity: 0.88;
|
||||
|
||||
/* ---------------- 交互热区 ----------------
|
||||
桌面端按下限为 WCAG 2.5.8(AA) 的 24×24px;本项目常规控件取 32px(舒适),
|
||||
主导航行取 44px(兼顾触屏笔记本)。不套用移动端 44px 全量规则,
|
||||
否则会把管理端的单屏信息密度牺牲掉。 */
|
||||
--tap-min: 44px;
|
||||
--tap-control: 32px;
|
||||
--tap-icon: 36px;
|
||||
|
||||
/* ---------------- tone 色调组(工作台指标卡 / 待办图标) ----------------
|
||||
每组三档:--tone-x 用于图标与图形;--tone-x-ink 用于文字(压 --tone-x-soft
|
||||
实算均 ≥4.6:1);--tone-x-soft 为浅底。 */
|
||||
--tone-rose: #F45F68; --tone-rose-ink: var(--admin-coral-strong); --tone-rose-soft: #FFF0F1;
|
||||
--tone-amber: #E59A32; --tone-amber-ink: var(--color-warning-strong); --tone-amber-soft: #FFF6E6;
|
||||
--tone-blue: #4A7CD4; --tone-blue-ink: var(--color-info-strong); --tone-blue-soft: #EEF5FF;
|
||||
--tone-green: #2E9B68; --tone-green-ink: var(--color-success-strong); --tone-green-soft: #EDF8F1;
|
||||
--tone-purple: #8B63BC; --tone-purple-ink: var(--color-purple-strong); --tone-purple-soft: #F4EFFF;
|
||||
--tone-slate: #8A9099; --tone-slate-ink: var(--color-neutral-strong); --tone-slate-soft: #F4F5F7;
|
||||
--tone-red: #D94A4A; --tone-red-ink: var(--color-danger-strong); --tone-red-soft: #FFF6F6;
|
||||
|
||||
/* 兼容既有引用名 */
|
||||
--admin-soft-pink: var(--admin-coral-soft);
|
||||
--admin-soft-blue: var(--color-info-soft);
|
||||
--admin-soft-green: var(--color-success-soft);
|
||||
--admin-soft-purple: var(--color-purple-soft);
|
||||
|
||||
/* ==========================================================================
|
||||
Element Plus 联动
|
||||
EP 的 --el-color-{t}-light-{i} / -dark-2 / -rgb 由 SCSS 在**编译期**按默认色
|
||||
静态生成,运行时改 --el-color-primary 不会带动它们(开关、滑块、焦点环、
|
||||
分页、标签因此仍是默认蓝)。这里按 EP 的派生公式补全:
|
||||
light-{i} = mix(#FFFFFF, base, i*10%) i ∈ 1..9
|
||||
dark-2 = mix(#000000, base, 20%)
|
||||
基色取 -strong / -solid 档 —— 既能当文字色也能做实心底,使 EP 全组件与品牌一致。
|
||||
info 保持中性灰:EP 的 info 语义是「中性/未知」,不应变成品牌红。
|
||||
========================================================================== */
|
||||
--el-color-primary: #C84A53;
|
||||
--el-color-primary-rgb: 200, 74, 83;
|
||||
--el-color-primary-light-3: #d98087;
|
||||
--el-color-primary-light-5: #e4a5a9;
|
||||
--el-color-primary-light-7: #efc9cb;
|
||||
--el-color-primary-light-8: #f4dbdd;
|
||||
--el-color-primary-light-9: #faedee;
|
||||
--el-color-primary-dark-2: #a03b42;
|
||||
|
||||
--el-color-success: #1F7A4F;
|
||||
--el-color-success-rgb: 31, 122, 79;
|
||||
--el-color-success-light-3: #62a284;
|
||||
--el-color-success-light-5: #8fbda7;
|
||||
--el-color-success-light-7: #bcd7ca;
|
||||
--el-color-success-light-8: #d2e4dc;
|
||||
--el-color-success-light-9: #e9f2ed;
|
||||
--el-color-success-dark-2: #19623f;
|
||||
|
||||
--el-color-warning: #8F5D0F;
|
||||
--el-color-warning-rgb: 143, 93, 15;
|
||||
--el-color-warning-light-3: #b18e57;
|
||||
--el-color-warning-light-5: #c7ae87;
|
||||
--el-color-warning-light-7: #ddceb7;
|
||||
--el-color-warning-light-8: #e9dfcf;
|
||||
--el-color-warning-light-9: #f4efe7;
|
||||
--el-color-warning-dark-2: #724a0c;
|
||||
|
||||
--el-color-danger: #B8353A;
|
||||
--el-color-danger-rgb: 184, 53, 58;
|
||||
--el-color-danger-light-3: #cd7275;
|
||||
--el-color-danger-light-5: #dc9a9d;
|
||||
--el-color-danger-light-7: #eac2c4;
|
||||
--el-color-danger-light-8: #f1d7d8;
|
||||
--el-color-danger-light-9: #f8ebeb;
|
||||
--el-color-danger-dark-2: #932a2e;
|
||||
|
||||
--el-color-info: #656C79;
|
||||
--el-color-info-rgb: 101, 108, 121;
|
||||
--el-color-info-light-3: #9398a1;
|
||||
--el-color-info-light-5: #b2b6bc;
|
||||
--el-color-info-light-7: #d1d3d7;
|
||||
--el-color-info-light-8: #e0e2e4;
|
||||
--el-color-info-light-9: #f0f0f2;
|
||||
--el-color-info-dark-2: #515661;
|
||||
|
||||
/* EP 中性色(文字 / 边框 / 填充)接到同一套 token,避免表格、输入框另成一套灰 */
|
||||
--el-text-color-primary: var(--color-text-primary);
|
||||
--el-text-color-regular: var(--color-text-secondary);
|
||||
--el-text-color-secondary: var(--color-text-tertiary);
|
||||
--el-text-color-placeholder: var(--color-text-placeholder);
|
||||
--el-text-color-disabled: var(--color-text-disabled);
|
||||
--el-border-color: var(--color-border);
|
||||
--el-border-color-light: var(--color-border-light);
|
||||
--el-border-color-lighter: var(--color-border-light);
|
||||
--el-border-color-extra-light: var(--color-border-hairline);
|
||||
--el-fill-color: var(--color-bg-subtle);
|
||||
--el-fill-color-light: var(--color-bg-secondary);
|
||||
--el-fill-color-lighter: var(--color-bg-secondary);
|
||||
--el-bg-color: var(--color-bg-primary);
|
||||
--el-bg-color-page: var(--color-bg-page);
|
||||
--el-border-radius-base: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
html, body, #app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
font-size: 14px;
|
||||
font-family: var(--font-family-base);
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--color-text-primary);
|
||||
background-color: var(--color-bg-page);
|
||||
}
|
||||
|
||||
/* 管理端是桌面密度布局,1100px 以下由 MainLayout 的 .admin-scene 承担横向滚动。 */
|
||||
body { min-width: 1100px; }
|
||||
|
||||
/* <small> 的浏览器默认样式是 font-size: smaller(0.833em),会按父级字号再缩一档:
|
||||
父级 12px 时实际渲染 10px、11px 时渲染 9.17px —— 静默跌破 11px 下限,
|
||||
且源码里看不到任何非法字号,静态检查抓不到(2026-09-22 由渲染态实测发现)。
|
||||
这里统一改为继承父级字号,需要更小的场景必须显式声明 token 字号(守卫会校验)。 */
|
||||
small { font-size: inherit; }
|
||||
|
||||
button:focus-visible,
|
||||
a:focus-visible,
|
||||
input:focus-visible,
|
||||
[tabindex]:focus-visible {
|
||||
outline: 3px solid rgba(238, 102, 107, 0.32);
|
||||
outline: 3px solid var(--admin-coral-border);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ---------------- Element Plus 组件级覆盖 ---------------- */
|
||||
/* EP 默认 hover 是把主色「加白」(白字对比度随之掉到 2.2:1)。这里改成更深一档,
|
||||
既保住白字可读性(5.33:1),hover 反馈也更明确。 */
|
||||
.el-button--primary {
|
||||
--el-button-bg-color: var(--color-primary);
|
||||
--el-button-border-color: var(--color-primary);
|
||||
--el-button-hover-bg-color: var(--color-primary-hover);
|
||||
--el-button-hover-border-color: var(--color-primary-hover);
|
||||
--el-button-bg-color: var(--admin-coral-solid);
|
||||
--el-button-border-color: var(--admin-coral-solid);
|
||||
--el-button-hover-bg-color: var(--admin-coral-solid-hover);
|
||||
--el-button-hover-border-color: var(--admin-coral-solid-hover);
|
||||
--el-button-active-bg-color: var(--el-color-primary-dark-2);
|
||||
--el-button-active-border-color: var(--el-color-primary-dark-2);
|
||||
}
|
||||
|
||||
.el-radio-group .el-radio-button__inner:checked {
|
||||
background-color: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
background-color: var(--admin-coral-solid);
|
||||
border-color: var(--admin-coral-solid);
|
||||
}
|
||||
.el-card { --el-card-border-radius: var(--border-radius-md); --el-card-border-color: var(--color-border-light); }
|
||||
.el-table { --el-table-header-bg-color: #FBFAF8; --el-table-border-color: var(--color-border-light); }
|
||||
.el-tag { border-radius: 999px; }
|
||||
.page-container { padding: 24px; }
|
||||
.page-shell { max-width: 1440px; margin: 0 auto; }
|
||||
.page-heading { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; margin-bottom: 20px; }
|
||||
.page-heading h1 { font-size: 24px; line-height: 1.35; font-weight: 650; }
|
||||
|
||||
.el-card {
|
||||
--el-card-border-radius: var(--border-radius-md);
|
||||
--el-card-border-color: var(--color-border-light);
|
||||
}
|
||||
|
||||
.el-table {
|
||||
--el-table-header-bg-color: var(--color-bg-secondary);
|
||||
--el-table-border-color: var(--color-border-light);
|
||||
}
|
||||
|
||||
.el-tag { border-radius: var(--border-radius-pill); }
|
||||
|
||||
/* ---------------- 通用版式 ---------------- */
|
||||
.page-container { padding: var(--space-6); }
|
||||
|
||||
.page-shell { max-width: var(--admin-page-max-width); margin: 0 auto; }
|
||||
|
||||
.page-heading {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
margin-bottom: var(--space-5);
|
||||
}
|
||||
|
||||
.page-heading h1 {
|
||||
font-size: var(--font-size-3xl);
|
||||
line-height: var(--font-line-snug);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.page-heading p { margin-top: 6px; color: var(--color-text-tertiary); }
|
||||
.surface-card { background: var(--color-bg-primary); border: 1px solid var(--color-border-light); border-radius: var(--border-radius-md); box-shadow: var(--shadow-card); }
|
||||
|
||||
.surface-card {
|
||||
background: var(--color-bg-primary);
|
||||
border: 1px solid var(--color-border-light);
|
||||
border-radius: var(--border-radius-md);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
/* 语义文字色工具类(比在页面里各写一遍 color: #xxx 更不容易漂移) */
|
||||
.text-primary { color: var(--color-text-primary); }
|
||||
.text-secondary { color: var(--color-text-secondary); }
|
||||
.text-tertiary { color: var(--color-text-tertiary); }
|
||||
.mt-sm { margin-top: 8px; }
|
||||
.mt-md { margin-top: 16px; }
|
||||
.mt-lg { margin-top: 24px; }
|
||||
.mb-sm { margin-bottom: 8px; }
|
||||
.mb-md { margin-bottom: 16px; }
|
||||
.mb-lg { margin-bottom: 24px; }
|
||||
.text-muted { color: var(--color-text-tertiary); }
|
||||
.text-brand { color: var(--admin-coral-strong); }
|
||||
.text-success { color: var(--color-success-strong); }
|
||||
.text-warning { color: var(--color-warning-strong); }
|
||||
.text-danger { color: var(--color-danger-strong); }
|
||||
|
||||
/* 语义底色工具类 */
|
||||
.bg-card { background: var(--color-bg-primary); }
|
||||
.bg-subtle { background: var(--color-bg-subtle); }
|
||||
.bg-brand-soft { background: var(--admin-coral-soft); }
|
||||
|
||||
/* 间距工具类 */
|
||||
.mt-sm { margin-top: var(--space-2); }
|
||||
.mt-md { margin-top: var(--space-4); }
|
||||
.mt-lg { margin-top: var(--space-6); }
|
||||
.mb-sm { margin-bottom: var(--space-2); }
|
||||
.mb-md { margin-bottom: var(--space-4); }
|
||||
.mb-lg { margin-bottom: var(--space-6); }
|
||||
|
||||
.flex-center { display: flex; align-items: center; justify-content: center; }
|
||||
@media (max-width: 1200px) { body { min-width: 0; } }
|
||||
@media (max-width: 900px) { .page-container { padding: 16px; } .page-heading { align-items: flex-start; flex-direction: column; } }
|
||||
|
||||
/* ---------------- 交互反馈 ---------------- */
|
||||
/* 桌面端控件热区下限:32px(WCAG 2.5.8 AA 为 24px,留出舒适余量) */
|
||||
.tap-control { min-height: var(--tap-control); min-width: var(--tap-control); }
|
||||
|
||||
/* 按压反馈:统一走 token,避免各页面各写一套 scale/opacity */
|
||||
.pressable { transition: transform var(--motion-fast) var(--ease-standard), opacity var(--motion-fast) var(--ease-standard); }
|
||||
.pressable:active { transform: scale(var(--press-scale)); opacity: var(--press-opacity); }
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
body { min-width: 0; }
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.page-container { padding: var(--space-4); }
|
||||
.page-heading { align-items: flex-start; flex-direction: column; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* 管理端调色板常量(TS 侧)。
|
||||
*
|
||||
* 为什么需要这个文件:
|
||||
* 部分模板用 `:style="{ backgroundColor: stage.color + '15' }"` 把 6 位 hex 拼成
|
||||
* 8 位带 alpha 的颜色('#409EFF' + '15' → '#409EFF15',即 8% 不透明度)。
|
||||
* 这类位置**不能**用 CSS var —— `'var(--x)' + '15'` 是无效值,浏览器会整条丢弃。
|
||||
* 于是「色值只有一处事实源」这条规则在 TS 侧会漏。本文件补上这个缺口。
|
||||
*
|
||||
* 单一事实源约定:
|
||||
* 键名就是 src/theme/index.css 里的 var 名,值必须与之一致。
|
||||
* 门控脚本 scripts/lint-admin-tokens.mjs 会逐项比对,任一侧改了而另一侧没改即失败。
|
||||
*/
|
||||
|
||||
export const TOKEN_HEX: Record<string, string> = {
|
||||
'--color-primary': '#F45F68',
|
||||
'--color-primary-strong': '#C0414A',
|
||||
'--color-primary-solid': '#C84A53',
|
||||
'--color-success': '#2E9B68',
|
||||
'--color-success-strong': '#1F7A4F',
|
||||
'--color-warning': '#E59A32',
|
||||
'--color-warning-strong': '#8F5D0F',
|
||||
'--color-danger': '#D94A4A',
|
||||
'--color-danger-strong': '#B8353A',
|
||||
'--color-info': '#4A7CD4',
|
||||
'--color-info-strong': '#3A63B0',
|
||||
'--color-purple': '#8B63BC',
|
||||
'--color-purple-strong': '#6B47A0',
|
||||
'--color-neutral': '#8A9099',
|
||||
'--color-neutral-strong': '#656C79',
|
||||
'--color-text-primary': '#1F2328',
|
||||
'--color-text-secondary': '#555555',
|
||||
'--color-text-tertiary': '#676E7B',
|
||||
'--color-bg-page': '#F7F9FB',
|
||||
'--color-border': '#E4E7EB',
|
||||
}
|
||||
|
||||
/**
|
||||
* 取 token 的 hex 字面量。用于 :style 绑定(含 hex 拼接透明度的场景)。
|
||||
* @example token('--color-success') // '#2E9B68'
|
||||
*/
|
||||
export function token(name: string): string {
|
||||
const value = TOKEN_HEX[name]
|
||||
if (!value) throw new Error(`未登记的 token: ${name}(请先加入 TOKEN_HEX 并同步 theme/index.css)`)
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成带 alpha 的 8 位 hex。alpha 传 2 位十六进制('15' = 21/255 ≈ 8%)。
|
||||
* 与 `color + '15'` 等价,但把「必须 6 位 hex」这个约束显式化,避免误把 var() 传进来。
|
||||
*/
|
||||
export function tint(hex: string, alphaHex: string): string {
|
||||
if (!/^#[0-9a-fA-F]{6}$/.test(hex)) {
|
||||
throw new Error(`tint() 需要 6 位 hex(不能是 var() 或 3/8 位 hex):${hex}`)
|
||||
}
|
||||
return `${hex}${alphaHex}`
|
||||
}
|
||||
|
||||
/** 指标卡 / 待办项的色调组,与 theme/index.css 的 --tone-* 一一对应。 */
|
||||
export interface Tone {
|
||||
/** 图标与图形用的鲜亮色 */
|
||||
vivid: string
|
||||
/** 文字用的可读色(压 soft 底 ≥4.5:1) */
|
||||
ink: string
|
||||
/** 浅底 */
|
||||
soft: string
|
||||
}
|
||||
|
||||
export const TONES: Record<string, Tone> = {
|
||||
rose: { vivid: '#F45F68', ink: '#C0414A', soft: '#FFF0F1' },
|
||||
amber: { vivid: '#E59A32', ink: '#8F5D0F', soft: '#FFF6E6' },
|
||||
blue: { vivid: '#4A7CD4', ink: '#3A63B0', soft: '#EEF5FF' },
|
||||
green: { vivid: '#2E9B68', ink: '#1F7A4F', soft: '#EDF8F1' },
|
||||
purple: { vivid: '#8B63BC', ink: '#6B47A0', soft: '#F4EFFF' },
|
||||
slate: { vivid: '#8A9099', ink: '#656C79', soft: '#F4F5F7' },
|
||||
red: { vivid: '#D94A4A', ink: '#B8353A', soft: '#FFF6F6' },
|
||||
}
|
||||
@@ -283,9 +283,9 @@ onMounted(retryLoad)
|
||||
<style scoped>
|
||||
.dashboard-page {
|
||||
min-width: 0;
|
||||
max-width: 1500px;
|
||||
max-width: var(--admin-content-max-width);
|
||||
margin: 0 auto;
|
||||
padding: 20px 26px 28px;
|
||||
padding: var(--space-5) 26px 28px;
|
||||
}
|
||||
|
||||
.dashboard-welcome {
|
||||
@@ -293,33 +293,34 @@ onMounted(retryLoad)
|
||||
min-height: 66px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
gap: var(--space-5);
|
||||
}
|
||||
|
||||
.dashboard-welcome h1 {
|
||||
font-size: 24px;
|
||||
line-height: 1.25;
|
||||
font-size: var(--font-size-3xl);
|
||||
line-height: var(--font-line-snug);
|
||||
}
|
||||
|
||||
.dashboard-welcome p {
|
||||
margin-top: 6px;
|
||||
color: #78828b;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.dashboard-date {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
color: #7b858e;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.dashboard-date small {
|
||||
padding: 6px 10px;
|
||||
color: #d75a63;
|
||||
background: #fff0f1;
|
||||
border-radius: 999px;
|
||||
color: var(--admin-coral-strong);
|
||||
background: var(--admin-coral-soft);
|
||||
border-radius: var(--border-radius-pill);
|
||||
font-size: var(--font-size-micro);
|
||||
}
|
||||
|
||||
.dashboard-loading,
|
||||
@@ -335,7 +336,7 @@ onMounted(retryLoad)
|
||||
|
||||
.metric-skeleton {
|
||||
height: 105px;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--border-radius-md);
|
||||
}
|
||||
|
||||
.dashboard-error,
|
||||
@@ -343,14 +344,14 @@ onMounted(retryLoad)
|
||||
.dashboard-notice {
|
||||
display: flex;
|
||||
min-height: 48px;
|
||||
margin: 8px 0 12px;
|
||||
margin: var(--space-2) 0 var(--space-3);
|
||||
padding: 10px 14px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #6f5355;
|
||||
background: #fff6f6;
|
||||
border: 1px solid #f7dfe1;
|
||||
border-radius: 11px;
|
||||
color: var(--color-danger-strong);
|
||||
background: var(--color-danger-soft);
|
||||
border: 1px solid var(--color-border-danger);
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
.dashboard-error > span,
|
||||
@@ -363,7 +364,8 @@ onMounted(retryLoad)
|
||||
.dashboard-error small,
|
||||
.dashboard-forbidden small {
|
||||
margin-top: 3px;
|
||||
color: #8c7d7e;
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: var(--font-size-micro);
|
||||
}
|
||||
|
||||
.dashboard-metrics {
|
||||
@@ -375,33 +377,40 @@ onMounted(retryLoad)
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
min-height: 108px;
|
||||
padding: 14px 12px;
|
||||
padding: 14px var(--space-3);
|
||||
grid-template-columns: 38px 1fr;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
border: 1px solid rgba(235, 235, 238, 0.9);
|
||||
border-radius: 12px;
|
||||
transition: transform 0.18s ease, box-shadow 0.18s ease;
|
||||
background: var(--color-bg-primary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-md);
|
||||
transition: transform var(--motion-normal) var(--ease-standard),
|
||||
box-shadow var(--motion-normal) var(--ease-standard);
|
||||
}
|
||||
|
||||
.metric-card:hover {
|
||||
box-shadow: 0 10px 24px rgba(68, 48, 55, 0.1);
|
||||
box-shadow: var(--shadow-raise);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.metric-card:active {
|
||||
transform: translateY(0) scale(var(--press-scale));
|
||||
}
|
||||
|
||||
.metric-card.unavailable {
|
||||
cursor: default;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
/* 图标字形属「有意义的非文本内容」,按 WCAG 需 ≥3:1。
|
||||
原先用鲜亮色调压同色系浅底(amber 仅 2.2:1),统一改用 -ink 档(≥4.6:1)。 */
|
||||
.metric-icon,
|
||||
.todo-icon {
|
||||
display: grid;
|
||||
color: var(--tone-color);
|
||||
color: var(--tone-ink);
|
||||
background: var(--tone-soft);
|
||||
border-radius: 50%;
|
||||
place-items: center;
|
||||
@@ -410,7 +419,7 @@ onMounted(retryLoad)
|
||||
.metric-icon {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
font-size: 19px;
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
|
||||
.metric-copy {
|
||||
@@ -421,24 +430,25 @@ onMounted(retryLoad)
|
||||
|
||||
.metric-copy small {
|
||||
overflow: hidden;
|
||||
color: #55616a;
|
||||
font-size: 11px;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-xs);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.metric-copy strong {
|
||||
margin-top: 3px;
|
||||
color: #171a1d;
|
||||
font-size: 25px;
|
||||
color: var(--color-text-primary);
|
||||
font-size: var(--font-size-3xl);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* 原为 9px(低于可读下限),提到 11px */
|
||||
.metric-copy em {
|
||||
margin-top: 6px;
|
||||
overflow: hidden;
|
||||
color: var(--tone-color);
|
||||
font-size: 9px;
|
||||
color: var(--tone-ink);
|
||||
font-size: var(--font-size-micro);
|
||||
font-style: normal;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -446,18 +456,20 @@ onMounted(retryLoad)
|
||||
|
||||
.metric-arrow {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
bottom: 8px;
|
||||
color: var(--tone-color);
|
||||
font-size: 11px;
|
||||
right: var(--space-2);
|
||||
bottom: var(--space-2);
|
||||
color: var(--tone-ink);
|
||||
font-size: var(--font-size-micro);
|
||||
}
|
||||
|
||||
.tone-rose { --tone-color: #f05e69; --tone-soft: #ffe9eb; }
|
||||
.tone-amber { --tone-color: #e58b39; --tone-soft: #fff1df; }
|
||||
.tone-blue { --tone-color: #568ce9; --tone-soft: #eaf2ff; }
|
||||
.tone-green { --tone-color: #4aad76; --tone-soft: #e9f7ef; }
|
||||
.tone-purple { --tone-color: #9c69df; --tone-soft: #f2eaff; }
|
||||
.tone-red { --tone-color: #ed5b64; --tone-soft: #ffe9eb; }
|
||||
/* 色调组改为引用 theme/index.css 的 --tone-*-ink / --tone-*-soft。
|
||||
--tone-ink 是本组件内的局部变量(由下面各 tone-* 类赋值),供图标与文字统一取用。 */
|
||||
.tone-rose { --tone-ink: var(--tone-rose-ink); --tone-soft: var(--tone-rose-soft); }
|
||||
.tone-amber { --tone-ink: var(--tone-amber-ink); --tone-soft: var(--tone-amber-soft); }
|
||||
.tone-blue { --tone-ink: var(--tone-blue-ink); --tone-soft: var(--tone-blue-soft); }
|
||||
.tone-green { --tone-ink: var(--tone-green-ink); --tone-soft: var(--tone-green-soft); }
|
||||
.tone-purple { --tone-ink: var(--tone-purple-ink); --tone-soft: var(--tone-purple-soft); }
|
||||
.tone-red { --tone-ink: var(--tone-red-ink); --tone-soft: var(--tone-red-soft); }
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
@@ -468,11 +480,11 @@ onMounted(retryLoad)
|
||||
|
||||
.dashboard-panel {
|
||||
min-width: 0;
|
||||
padding: 14px 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #e9edf0;
|
||||
padding: 14px var(--space-4);
|
||||
background: var(--color-bg-primary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--admin-card-radius);
|
||||
box-shadow: 0 8px 20px rgba(48, 58, 67, 0.035);
|
||||
box-shadow: var(--shadow-panel);
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
@@ -480,42 +492,49 @@ onMounted(retryLoad)
|
||||
min-height: 30px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #f0f1f2;
|
||||
border-bottom: 1px solid var(--color-border-hairline);
|
||||
}
|
||||
|
||||
.panel-header h2 {
|
||||
padding-bottom: 10px;
|
||||
font-size: 16px;
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
.panel-header h2 span {
|
||||
display: inline-grid;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
min-width: var(--space-5);
|
||||
height: var(--space-5);
|
||||
margin-left: 6px;
|
||||
padding: 0 5px;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
line-height: 20px;
|
||||
background: var(--admin-coral);
|
||||
border-radius: 999px;
|
||||
color: var(--color-text-inverse);
|
||||
font-size: var(--font-size-micro);
|
||||
line-height: var(--space-5);
|
||||
background: var(--admin-coral-solid);
|
||||
border-radius: var(--border-radius-pill);
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.panel-header button {
|
||||
display: flex;
|
||||
min-height: var(--tap-control);
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #89929a;
|
||||
font-size: 11px;
|
||||
gap: var(--space-1);
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: var(--font-size-xs);
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: var(--border-radius-xs);
|
||||
transition: color var(--motion-fast) var(--ease-standard);
|
||||
}
|
||||
|
||||
.panel-header button:hover {
|
||||
color: var(--admin-coral-strong);
|
||||
}
|
||||
|
||||
.panel-header > span {
|
||||
color: #98a0a7;
|
||||
font-size: 10px;
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: var(--font-size-micro);
|
||||
}
|
||||
|
||||
.todo-list,
|
||||
@@ -535,12 +554,13 @@ onMounted(retryLoad)
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-bottom: 1px solid #f2f3f4;
|
||||
border-bottom: 1px solid var(--color-border-hairline);
|
||||
transition: background var(--motion-fast) var(--ease-standard);
|
||||
}
|
||||
|
||||
.todo-row:hover,
|
||||
.activity-row:hover {
|
||||
background: #fffafa;
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
.todo-icon {
|
||||
@@ -560,8 +580,8 @@ onMounted(retryLoad)
|
||||
.todo-copy strong,
|
||||
.activity-copy strong {
|
||||
overflow: hidden;
|
||||
color: #30373d;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-primary);
|
||||
font-size: var(--font-size-xs);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -570,22 +590,22 @@ onMounted(retryLoad)
|
||||
.activity-copy small {
|
||||
margin-top: 3px;
|
||||
overflow: hidden;
|
||||
color: #939ba2;
|
||||
font-size: 9px;
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: var(--font-size-micro);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.todo-row > b {
|
||||
min-width: 29px;
|
||||
color: var(--admin-coral);
|
||||
font-size: 13px;
|
||||
color: var(--admin-coral-strong);
|
||||
font-size: var(--font-size-sm);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.todo-row > em {
|
||||
color: var(--admin-coral);
|
||||
font-size: 10px;
|
||||
color: var(--admin-coral-strong);
|
||||
font-size: var(--font-size-micro);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@@ -594,21 +614,21 @@ onMounted(retryLoad)
|
||||
height: 38px;
|
||||
flex: none;
|
||||
object-fit: cover;
|
||||
border-radius: 7px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
}
|
||||
|
||||
.activity-status {
|
||||
padding: 3px 6px;
|
||||
color: #318b5a;
|
||||
font-size: 9px;
|
||||
background: #eaf7ef;
|
||||
border-radius: 999px;
|
||||
color: var(--color-success-strong);
|
||||
font-size: var(--font-size-micro);
|
||||
background: var(--color-success-soft);
|
||||
border-radius: var(--border-radius-pill);
|
||||
}
|
||||
|
||||
.activity-row > em {
|
||||
min-width: 76px;
|
||||
color: #7d878f;
|
||||
font-size: 9px;
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: var(--font-size-micro);
|
||||
font-style: normal;
|
||||
text-align: right;
|
||||
}
|
||||
@@ -620,47 +640,47 @@ onMounted(retryLoad)
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
color: #a0a7ad;
|
||||
font-size: 11px;
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.dashboard-empty .el-icon {
|
||||
color: #d7dce0;
|
||||
font-size: 34px;
|
||||
color: var(--color-icon-muted);
|
||||
font-size: var(--font-size-5xl);
|
||||
}
|
||||
|
||||
.overview-grid {
|
||||
display: grid;
|
||||
min-height: 118px;
|
||||
padding-top: 12px;
|
||||
padding-top: var(--space-3);
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.overview-grid > div {
|
||||
display: flex;
|
||||
padding: 12px 10px;
|
||||
padding: var(--space-3) 10px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
background: var(--tone-soft);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
.overview-grid small {
|
||||
color: var(--tone-color);
|
||||
font-size: 10px;
|
||||
color: var(--tone-ink);
|
||||
font-size: var(--font-size-micro);
|
||||
}
|
||||
|
||||
.overview-grid strong {
|
||||
margin-top: 7px;
|
||||
font-size: 24px;
|
||||
font-size: var(--font-size-3xl);
|
||||
}
|
||||
|
||||
.overview-grid em {
|
||||
margin-top: 7px;
|
||||
color: #8a9298;
|
||||
font-size: 9px;
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: var(--font-size-micro);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@@ -675,52 +695,53 @@ onMounted(retryLoad)
|
||||
grid-template-columns: 92px 1fr 76px;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
font-size: 10px;
|
||||
font-size: var(--font-size-micro);
|
||||
}
|
||||
|
||||
.effectiveness-row > span {
|
||||
color: #747e87;
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
/* 轨道与填充的对比原为 2.53:1(条几乎看不出进度),改后 4.01:1 */
|
||||
.effectiveness-row > div {
|
||||
height: 8px;
|
||||
height: var(--space-2);
|
||||
overflow: hidden;
|
||||
background: #fdebee;
|
||||
border-radius: 999px;
|
||||
background: var(--color-primary-light);
|
||||
border-radius: var(--border-radius-pill);
|
||||
}
|
||||
|
||||
.effectiveness-row i {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #f56b75, #ef5a65);
|
||||
background: var(--gradient-brand-bar);
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.effectiveness-row strong {
|
||||
color: #75808a;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: var(--font-size-micro);
|
||||
font-weight: var(--font-weight-medium);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@media (max-width: 1450px), (max-height: 820px) {
|
||||
.dashboard-page { padding: 12px 18px 20px; }
|
||||
.dashboard-welcome { min-height: 54px; }
|
||||
.dashboard-welcome h1 { font-size: 20px; }
|
||||
.dashboard-welcome p { margin-top: 3px; font-size: 11px; }
|
||||
.dashboard-welcome h1 { font-size: var(--font-size-2xl); }
|
||||
.dashboard-welcome p { margin-top: 3px; font-size: var(--font-size-xs); }
|
||||
.dashboard-metrics { margin-top: 6px; gap: 7px; }
|
||||
.metric-card { min-height: 90px; padding: 10px 8px; grid-template-columns: 32px 1fr; gap: 6px; }
|
||||
.metric-icon { width: 32px; height: 32px; font-size: 16px; }
|
||||
.metric-copy strong { font-size: 21px; }
|
||||
.metric-card { min-height: 90px; padding: 10px var(--space-2); grid-template-columns: var(--space-8) 1fr; gap: 6px; }
|
||||
.metric-icon { width: var(--space-8); height: var(--space-8); font-size: var(--font-size-lg); }
|
||||
.metric-copy strong { font-size: var(--font-size-2xl); }
|
||||
.dashboard-grid { margin-top: 9px; gap: 9px; }
|
||||
.dashboard-panel { padding: 10px 12px; }
|
||||
.dashboard-panel { padding: 10px var(--space-3); }
|
||||
.todo-list, .activity-list { min-height: 152px; }
|
||||
.todo-row, .activity-row { min-height: 38px; padding: 4px; }
|
||||
.todo-row, .activity-row { min-height: 38px; padding: var(--space-1); }
|
||||
.activity-row img { width: 58px; height: 31px; }
|
||||
.dashboard-grid-bottom .dashboard-panel { min-height: 150px; }
|
||||
.overview-grid, .effectiveness-list { min-height: 98px; }
|
||||
.overview-grid > div { padding: 8px 6px; }
|
||||
.overview-grid strong { font-size: 20px; }
|
||||
.overview-grid > div { padding: var(--space-2) 6px; }
|
||||
.overview-grid strong { font-size: var(--font-size-2xl); }
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
|
||||
@@ -101,7 +101,7 @@ async function handleLogin() {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.25fr) minmax(420px, 0.75fr);
|
||||
min-height: 100vh;
|
||||
background: #FFFFFF;
|
||||
background: var(--color-bg-primary);
|
||||
}
|
||||
|
||||
.brand-panel {
|
||||
@@ -144,28 +144,28 @@ async function handleLogin() {
|
||||
|
||||
.brand-title strong {
|
||||
color: var(--admin-primary);
|
||||
font-size: 25px;
|
||||
font-size: var(--font-size-3xl);
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.brand-title span {
|
||||
margin-top: 3px;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.brand-message {
|
||||
max-width: 590px;
|
||||
padding: 26px 30px;
|
||||
background: rgba(255, 255, 255, 0.86);
|
||||
border: 1px solid rgba(255, 255, 255, 0.94);
|
||||
background: var(--color-bg-overlay);
|
||||
border: 1px solid var(--color-bg-overlay-strong);
|
||||
border-radius: 18px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.brand-message p {
|
||||
color: var(--admin-primary);
|
||||
font-weight: 650;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
letter-spacing: 3px;
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ async function handleLogin() {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px;
|
||||
background: #FFFFFF;
|
||||
background: var(--color-bg-primary);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
@@ -201,14 +201,14 @@ async function handleLogin() {
|
||||
|
||||
.eyebrow {
|
||||
color: var(--admin-primary);
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.login-header h2 {
|
||||
margin-top: 10px;
|
||||
font-size: 30px;
|
||||
font-size: var(--font-size-5xl);
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
@@ -228,7 +228,7 @@ async function handleLogin() {
|
||||
margin-top: 24px;
|
||||
padding: 14px 16px;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: 1.6;
|
||||
background: var(--admin-soft-green);
|
||||
border-radius: 10px;
|
||||
@@ -243,7 +243,7 @@ async function handleLogin() {
|
||||
footer {
|
||||
margin-top: 40px;
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: 12px;
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
|
||||
Reference in New Issue
Block a user