管理端:设计系统 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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user