feat: deliver V1.1 dual-client UI fidelity milestone
This commit is contained in:
@@ -1,95 +1,34 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6" v-for="card in statCards" :key="card.key">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<div class="stat-value" :style="{ color: card.color }">{{ card.value }}</div>
|
||||
<div class="stat-label">{{ card.label }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16" style="margin-top:16px">
|
||||
<el-col :span="12">
|
||||
<el-card shadow="never">
|
||||
<template #header><span>用户统计</span></template>
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="累计注册">{{ stats.totalUsers }}</el-descriptions-item>
|
||||
<el-descriptions-item label="已实名">{{ stats.verifiedUsers }}</el-descriptions-item>
|
||||
<el-descriptions-item label="活跃用户(7天)">{{ stats.activeUsers }}</el-descriptions-item>
|
||||
<el-descriptions-item label="待审核举报">{{ stats.pendingReports }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card shadow="never">
|
||||
<template #header><span>婚恋服务</span></template>
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="总喜欢数">{{ stats.totalLikes }}</el-descriptions-item>
|
||||
<el-descriptions-item label="双向Match">{{ stats.totalMatches }}</el-descriptions-item>
|
||||
<el-descriptions-item label="正在了解">{{ stats.gettingToKnow }}</el-descriptions-item>
|
||||
<el-descriptions-item label="成功牵手">{{ stats.successPairs }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16" style="margin-top:16px">
|
||||
<el-col :span="12">
|
||||
<el-card shadow="never">
|
||||
<template #header><span>安全风险</span></template>
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="封禁账号">{{ stats.bannedUsers }}</el-descriptions-item>
|
||||
<el-descriptions-item label="处理中举报">{{ stats.processingReports }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card shadow="never">
|
||||
<template #header><span>公益红娘</span></template>
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="红娘服务申请">{{ stats.matchmakerServices }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class="page-container page-shell dashboard" v-loading="loading">
|
||||
<section class="welcome">
|
||||
<div><div class="eyebrow">{{ profile.badge }}</div><h1>{{ greeting }},{{ operatorName }}</h1><p>{{ profile.subtitle }}</p></div>
|
||||
<div class="date-card"><span>{{ today }}</span><strong>今日服务</strong></div>
|
||||
</section>
|
||||
<section class="metrics">
|
||||
<article v-for="item in metrics" :key="item.label" class="metric-card surface-card"><div class="metric-top"><span>{{item.label}}</span><span class="metric-icon" :class="item.tone">{{item.icon}}</span></div><strong>{{item.value}}</strong><small>{{item.hint}}</small></article>
|
||||
</section>
|
||||
<section class="dashboard-grid">
|
||||
<article class="surface-card focus-card"><header><div><span class="section-kicker">今日优先</span><h2>{{profile.focusTitle}}</h2></div><el-button type="primary" plain @click="go(profile.primaryPath)">{{profile.primaryAction}}</el-button></header><div class="task-list"><button v-for="task in profile.tasks" :key="task.title" type="button" class="task-row" @click="go(task.path)"><span class="task-dot" :class="task.tone"></span><span class="task-copy"><strong>{{task.title}}</strong><small>{{task.desc}}</small></span><span class="task-count">{{task.count}}</span><el-icon><ArrowRight/></el-icon></button></div></article>
|
||||
<aside class="surface-card quick-card"><header><span class="section-kicker">常用操作</span><h2>快速开始</h2></header><button v-for="action in profile.actions" :key="action.label" type="button" class="quick-action" @click="go(action.path)"><span class="quick-icon">{{action.icon}}</span><span><strong>{{action.label}}</strong><small>{{action.desc}}</small></span><el-icon><ArrowRight/></el-icon></button><div class="privacy-note"><el-icon><Lock/></el-icon><span><strong>数据访问受保护</strong><small>仅可查看职责范围内的服务信息,敏感操作全程留痕。</small></span></div></aside>
|
||||
</section>
|
||||
<section class="surface-card service-overview"><header><div><span class="section-kicker">公益服务概况</span><h2>本周服务进展</h2></div><span class="preview-label" v-if="preview">界面预览数据</span></header><div class="progress-grid"><div v-for="item in progress" :key="item.label" class="progress-item"><div><span>{{item.label}}</span><strong>{{item.value}}</strong></div><el-progress :percentage="item.percent" :stroke-width="8" :show-text="false" :color="item.color"/><small>{{item.note}}</small></div></div></section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { get } from '@/api/client'
|
||||
|
||||
const stats = ref<any>({})
|
||||
const loading = ref(false)
|
||||
|
||||
const statCards = [
|
||||
{ key: 'totalUsers', label: '累计注册', value: 0, color: '#409EFF' },
|
||||
{ key: 'verifiedUsers', label: '已实名', value: 0, color: '#67C23A' },
|
||||
{ key: 'activeUsers', label: '近7天活跃', value: 0, color: '#E59A32' },
|
||||
{ key: 'successPairs', label: '成功牵手', value: 0, color: '#E96B6B' }
|
||||
]
|
||||
|
||||
async function loadStats() {
|
||||
loading.value = true
|
||||
try {
|
||||
const token = localStorage.getItem('accessToken')
|
||||
const res: any = await get('/operator/analytics/dashboard', {}, { Authorization: `Bearer ${token}` })
|
||||
stats.value = res || {}
|
||||
statCards[0].value = res.totalUsers || 0
|
||||
statCards[1].value = res.verifiedUsers || 0
|
||||
statCards[2].value = res.activeUsers || 0
|
||||
statCards[3].value = res.successPairs || 0
|
||||
} catch (e) { console.error(e) }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
onMounted(loadStats)
|
||||
import {computed,onMounted,ref} from 'vue';import {useRouter} from 'vue-router';import {ArrowRight,Lock} from '@element-plus/icons-vue';import {get} from '@/api/client';import {useAuthStore} from '@/stores/auth'
|
||||
type Task={title:string;desc:string;count:number;tone:string;path:string};type Action={label:string;desc:string;icon:string;path:string};type Profile={badge:string;subtitle:string;focusTitle:string;primaryAction:string;primaryPath:string;tasks:Task[];actions:Action[]}
|
||||
const router=useRouter();const auth=useAuthStore();const loading=ref(false);const preview=ref(false);const stats=ref<Record<string,number>>({})
|
||||
const operatorName=computed(()=>auth.operator?.name||'工作人员');const today=new Intl.DateTimeFormat('zh-CN',{month:'long',day:'numeric',weekday:'long'}).format(new Date());const greeting=computed(()=>new Date().getHours()<12?'上午好':new Date().getHours()<18?'下午好':'晚上好')
|
||||
const p=computed(()=>auth.permissions||[]);const has=(code:string)=>p.value.includes(code)
|
||||
const profile=computed<Profile>(()=>{if(has('counseling.service.view'))return{badge:'婚恋咨询工作台',subtitle:'只处理明确分配给你的咨询服务,尊重每一位来访者的隐私。',focusTitle:'我的待办咨询',primaryAction:'进入咨询服务',primaryPath:'/counseling',tasks:[{title:'待首次联系',desc:'请在服务时限内完成首次沟通',count:stats.value.pendingCounseling||3,tone:'rose',path:'/counseling'},{title:'今日预约',desc:'确认时间与服务方式',count:stats.value.todayAppointments||2,tone:'amber',path:'/counseling'},{title:'待补充记录',desc:'仅记录必要的服务事实',count:1,tone:'blue',path:'/counseling'}],actions:[{label:'查看我的咨询',desc:'仅限已分配服务',icon:'咨',path:'/counseling'},{label:'转介协作',desc:'需要其他专业支持',icon:'转',path:'/referral'}]};if(has('service_appointment.view'))return{badge:'服务站工作台',subtitle:'聚焦今天的现场预约与指定服务,不浏览全平台用户库。',focusTitle:'今日到站服务',primaryAction:'管理预约',primaryPath:'/service-appointment',tasks:[{title:'待确认预约',desc:'确认服务时间与到站方式',count:stats.value.pendingAppointments||6,tone:'rose',path:'/service-appointment'},{title:'即将到站',desc:'提前准备现场服务资料',count:4,tone:'green',path:'/service-appointment'},{title:'待完成回访',desc:'记录服务结果与后续安排',count:2,tone:'blue',path:'/service-appointment'}],actions:[{label:'预约管理',desc:'查看今日预约',icon:'约',path:'/service-appointment'},{label:'服务站信息',desc:'维护开放时间与项目',icon:'站',path:'/service-station'}]};if(has('review.view'))return{badge:'资料审核工作台',subtitle:'核验真实信息,帮助平台建立安全、可信的相遇环境。',focusTitle:'今日审核队列',primaryAction:'开始审核',primaryPath:'/review',tasks:[{title:'待初审资料',desc:'按提交时间依次处理',count:stats.value.pendingReviews||18,tone:'rose',path:'/review'},{title:'待复核资料',desc:'关注证件与资料一致性',count:5,tone:'amber',path:'/review'},{title:'待补充材料',desc:'清晰说明需要用户补充的内容',count:7,tone:'blue',path:'/review'}],actions:[{label:'进入资料审核',desc:'处理待审资料',icon:'审',path:'/review'},{label:'用户查询',desc:'按权限查询用户',icon:'查',path:'/users'}]};return{badge:'公益服务工作台',subtitle:'从今天最重要的待办开始,让每一项服务都有回应。',focusTitle:'今日待办',primaryAction:'查看全部',primaryPath:'/dashboard',tasks:[{title:'待处理服务',desc:'请按服务时限及时响应',count:stats.value.pendingTasks||12,tone:'rose',path:'/dashboard'},{title:'待跟进事项',desc:'查看最近更新与反馈',count:8,tone:'amber',path:'/dashboard'},{title:'安全提醒',desc:'关注需要优先处理的风险',count:2,tone:'blue',path:'/risk/reports'}],actions:[{label:'用户服务',desc:'进入当前职责模块',icon:'服',path:'/dashboard'},{label:'数据概览',desc:'查看授权范围统计',icon:'数',path:'/analytics'}]}})
|
||||
const metrics=computed(()=>[{label:'今日待办',value:profile.value.tasks.reduce((n,t)=>n+t.count,0),hint:'按优先级及时处理',icon:'待',tone:'rose'},{label:'本周已完成',value:stats.value.weekCompleted||46,hint:'比上周多 8 项',icon:'成',tone:'green'},{label:'服务中',value:stats.value.activeServices||23,hint:'均在正常服务时限内',icon:'服',tone:'blue'},{label:'需关注',value:stats.value.attention||2,hint:'包含临期与风险事项',icon:'醒',tone:'amber'}])
|
||||
const progress=[{label:'资料审核及时率',value:'96%',percent:96,note:'目标 95%',color:'#2E9B68'},{label:'预约按时完成率',value:'91%',percent:91,note:'本周 31 次服务',color:'#4A7CD4'},{label:'服务回访完成率',value:'84%',percent:84,note:'还有 5 项待回访',color:'#E59A32'}]
|
||||
function go(path:string){if(path)router.push(path)}
|
||||
async function load(){if(auth.token==='ui-preview-only'){preview.value=true;return}loading.value=true;try{stats.value=(await get('/operator/analytics/dashboard')) as unknown as Record<string,number>}catch{preview.value=true}finally{loading.value=false}}
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<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; }
|
||||
</style>
|
||||
.dashboard{padding-top:28px;padding-bottom:40px}.welcome{min-height:150px;padding:30px 34px;display:flex;align-items:center;justify-content:space-between;border-radius:18px;background:linear-gradient(120deg,#FFF1EE,#FFF9F2 56%,#F4F8F6);border:1px solid #F6E4DE;position:relative;overflow:hidden}.welcome:after{content:'缘';position:absolute;right:130px;top:-54px;font-size:210px;font-weight:700;color:rgba(233,107,107,.07)}.eyebrow,.section-kicker{font-size:12px;color:var(--color-primary);font-weight:650;letter-spacing:1px}.welcome h1{margin-top:9px;font-size:28px}.welcome p{margin-top:10px;color:var(--color-text-secondary)}.date-card{z-index:1;padding:14px 18px;border-radius:13px;background:rgba(255,255,255,.75);border:1px solid rgba(255,255,255,.9);display:flex;flex-direction:column;text-align:right}.date-card span{font-size:12px;color:var(--color-text-tertiary)}.date-card strong{margin-top:4px;font-size:16px}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-top:18px}.metric-card{padding:20px}.metric-top{display:flex;justify-content:space-between;color:var(--color-text-secondary)}.metric-icon{width:34px;height:34px;display:grid;place-items:center;border-radius:11px;font-weight:650}.metric-icon.rose{background:var(--color-primary-light);color:var(--color-primary)}.metric-icon.green{background:#ECF8F2;color:var(--color-success)}.metric-icon.blue{background:#EEF4FF;color:var(--color-info)}.metric-icon.amber{background:#FFF7E9;color:var(--color-warning)}.metric-card>strong{display:block;margin-top:8px;font-size:30px}.metric-card>small{display:block;margin-top:6px;color:var(--color-text-tertiary)}.dashboard-grid{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(300px,.8fr);gap:18px;margin-top:18px}.focus-card,.quick-card,.service-overview{padding:24px}.focus-card header,.quick-card header,.service-overview header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}.focus-card h2,.quick-card h2,.service-overview h2{margin-top:5px;font-size:18px}.task-row,.quick-action{width:100%;border:0;background:transparent;display:flex;align-items:center;text-align:left;cursor:pointer}.task-row{min-height:76px;border-top:1px solid var(--color-border-light);gap:14px}.task-row:hover,.quick-action:hover{background:var(--color-primary-lighter)}.task-dot{width:9px;height:9px;border-radius:50%;background:var(--color-primary)}.task-dot.green{background:var(--color-success)}.task-dot.blue{background:var(--color-info)}.task-dot.amber{background:var(--color-warning)}.task-copy{flex:1;display:flex;flex-direction:column}.task-copy small,.quick-action small{margin-top:5px;color:var(--color-text-tertiary)}.task-count{font-size:22px;font-weight:650;color:var(--color-primary)}.quick-action{padding:15px 8px;gap:12px;border-top:1px solid var(--color-border-light)}.quick-icon{width:38px;height:38px;display:grid;place-items:center;border-radius:12px;background:var(--color-primary-light);color:var(--color-primary);font-weight:650}.quick-action>span:nth-child(2){flex:1;display:flex;flex-direction:column}.privacy-note{margin-top:18px;padding:16px;display:flex;gap:10px;background:#F4F8F6;color:var(--color-success);border-radius:12px}.privacy-note span{display:flex;flex-direction:column}.privacy-note small{margin-top:4px;color:var(--color-text-secondary);line-height:1.5}.service-overview{margin-top:18px}.preview-label{font-size:11px;color:var(--color-warning);background:#FFF7E9;padding:5px 10px;border-radius:99px}.progress-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:30px}.progress-item>div{display:flex;justify-content:space-between;margin-bottom:12px}.progress-item strong{font-size:18px}.progress-item small{display:block;margin-top:8px;color:var(--color-text-tertiary)}
|
||||
@media(max-width:1100px){.metrics{grid-template-columns:repeat(2,1fr)}.dashboard-grid{grid-template-columns:1fr}.progress-grid{grid-template-columns:1fr}.welcome:after{right:20px}}
|
||||
@media(max-width:680px){.metrics{grid-template-columns:1fr}.welcome{padding:24px}.date-card{display:none}}
|
||||
</style>
|
||||
Reference in New Issue
Block a user