feat(aihr): admin dashboard with real aggregates
新增 /api/aihr/dashboard 聚合面试/对练/知识库/组织快照真实数据, 管理端首页从静态卡片改为 API 优先 + fallback。
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
type ApiResult<T> = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: T;
|
||||
};
|
||||
|
||||
export type DashboardHeroEntry = {
|
||||
key: string;
|
||||
status: string;
|
||||
type: string;
|
||||
note: string;
|
||||
};
|
||||
|
||||
export type DashboardStage = {
|
||||
step: number;
|
||||
name: string;
|
||||
desc: string;
|
||||
label: string;
|
||||
state: string;
|
||||
tagType: string;
|
||||
};
|
||||
|
||||
export type DashboardRisk = {
|
||||
name: string;
|
||||
owner: string;
|
||||
status: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
export type DashboardTrainingRecord = {
|
||||
name: string;
|
||||
scene: string;
|
||||
score: number;
|
||||
status: string;
|
||||
state: string;
|
||||
time: string;
|
||||
};
|
||||
|
||||
export type DashboardPipelineStep = {
|
||||
name: string;
|
||||
count: string;
|
||||
state: string;
|
||||
};
|
||||
|
||||
export type DashboardMetric = {
|
||||
label: string;
|
||||
value: string;
|
||||
target: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
export type DashboardOverview = {
|
||||
snapshotTime: string;
|
||||
heroEntries: DashboardHeroEntry[];
|
||||
stages: DashboardStage[];
|
||||
risks: DashboardRisk[];
|
||||
records: DashboardTrainingRecord[];
|
||||
pipeline: DashboardPipelineStep[];
|
||||
pipelineSummary: {
|
||||
todayNew: string;
|
||||
pending: string;
|
||||
completed: string;
|
||||
};
|
||||
metrics: DashboardMetric[];
|
||||
};
|
||||
|
||||
export function getDashboardOverview(): Promise<ApiResult<DashboardOverview>> {
|
||||
return request({
|
||||
url: '/api/aihr/dashboard/overview',
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="demo-home">
|
||||
<section class="toolbar-meta">
|
||||
<span class="snapshot-time">数据截至:2026-07-02 10:00:00</span>
|
||||
<el-button text :icon="Refresh">刷新</el-button>
|
||||
<span class="snapshot-time">数据截至:{{ displayDashboard.snapshotTime || '暂无实时数据' }}</span>
|
||||
<el-button text :icon="Refresh" :loading="dashboardLoading" @click="loadDashboard">刷新</el-button>
|
||||
</section>
|
||||
|
||||
<section class="hero-grid">
|
||||
@@ -66,7 +66,7 @@
|
||||
<div class="panel-title compact">
|
||||
<h2>最近训练记录</h2>
|
||||
</div>
|
||||
<el-table :data="records" size="small" class="record-table">
|
||||
<el-table :data="records" size="small" class="record-table" empty-text="暂无训练记录">
|
||||
<el-table-column prop="name" label="人员" min-width="92" />
|
||||
<el-table-column prop="scene" label="场景" min-width="118" />
|
||||
<el-table-column prop="score" label="得分" width="68" />
|
||||
@@ -98,9 +98,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="pipeline-summary">
|
||||
<span>今日新增:12 条</span>
|
||||
<span>待审核:2 条</span>
|
||||
<span>已入库:128 条</span>
|
||||
<span>今日新增:{{ pipelineSummary.todayNew }}</span>
|
||||
<span>待审核:{{ pipelineSummary.pending }}</span>
|
||||
<span>已入库:{{ pipelineSummary.completed }}</span>
|
||||
</div>
|
||||
<div class="panel-link">
|
||||
进入案例沉淀 <el-icon><ArrowRight /></el-icon>
|
||||
@@ -151,12 +151,52 @@ import {
|
||||
UserFilled,
|
||||
VideoCamera
|
||||
} from '@element-plus/icons-vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { getDashboardOverview, type DashboardMetric, type DashboardOverview, type DashboardRisk } from '@/api/aihr/dashboard';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const heroEntries = [
|
||||
const emptyDashboard: DashboardOverview = {
|
||||
snapshotTime: '',
|
||||
heroEntries: [],
|
||||
stages: [
|
||||
{ step: 1, name: '选:AI面试', desc: '题目与评估', label: '待接入', state: 'idle', tagType: 'info' },
|
||||
{ step: 2, name: '用:岗位适配', desc: '组织快照', label: '待同步', state: 'idle', tagType: 'info' },
|
||||
{ step: 3, name: '育:三角色对练', desc: '实战陪练', label: '待演示', state: 'active', tagType: 'danger' },
|
||||
{ step: 4, name: '留:成长追踪', desc: '能力画像', label: '待积累', state: 'idle', tagType: 'info' },
|
||||
{ step: 5, name: '案例沉淀', desc: '知识与复用', label: '待入库', state: 'idle', tagType: 'info' }
|
||||
],
|
||||
risks: [
|
||||
{ name: '住宅 SOP 素材', owner: '培训组', status: '0/5', type: 'danger' },
|
||||
{ name: '方言 ASR', owner: '技术组', status: '未配置', type: 'warning' },
|
||||
{ name: '候选资料审核', owner: 'HR组', status: '无待审', type: 'success' },
|
||||
{ name: '移动训练复盘', owner: '主管', status: '无待复盘', type: 'success' }
|
||||
],
|
||||
records: [],
|
||||
pipeline: [
|
||||
{ name: '语音上传', count: '0 条', state: 'todo' },
|
||||
{ name: 'ASR 转写', count: '0 条', state: 'todo' },
|
||||
{ name: 'AI 整理', count: '0 条', state: 'todo' },
|
||||
{ name: '审核入库', count: '0 条', state: 'todo' },
|
||||
{ name: '样片展示', count: '0 条', state: 'todo' }
|
||||
],
|
||||
pipelineSummary: { todayNew: '0 条', pending: '0 条', completed: '0 条' },
|
||||
metrics: [
|
||||
{ label: '端到端路径', value: '0 条', target: 'L0 目标 >= 1 条', type: 'danger' },
|
||||
{ label: 'SOP已入库', value: '0 份', target: 'B1 目标 >= 5 流程', type: 'danger' },
|
||||
{ label: '训练记录', value: '0 条', target: '育:至少 1 类场景', type: 'danger' },
|
||||
{ label: '试点人数', value: '0 人', target: 'D1 目标 >= 20 人', type: 'danger' }
|
||||
]
|
||||
};
|
||||
|
||||
const dashboard = ref<DashboardOverview | null>(null);
|
||||
const dashboardLoading = ref(false);
|
||||
const displayDashboard = computed(() => dashboard.value ?? emptyDashboard);
|
||||
|
||||
const baseHeroEntries = [
|
||||
{
|
||||
key: 'interview',
|
||||
title: 'AI面试出题',
|
||||
desc: '基于岗位画像与 JD,一键生成面试题目',
|
||||
status: '可演示',
|
||||
@@ -165,14 +205,16 @@ const heroEntries = [
|
||||
path: '/recruit/interview'
|
||||
},
|
||||
{
|
||||
key: 'practice',
|
||||
title: '三角色对练',
|
||||
desc: '简历生成 + 面试对象 + 结果评估闭环',
|
||||
status: '可演示',
|
||||
type: 'success',
|
||||
status: '待跑通',
|
||||
type: 'warning',
|
||||
icon: UserFilled,
|
||||
path: '/train/practice'
|
||||
},
|
||||
{
|
||||
key: 'case',
|
||||
title: '案例语音整理',
|
||||
desc: '语音上传,AI 整理为结构化案例',
|
||||
status: '样片兜底',
|
||||
@@ -181,57 +223,68 @@ const heroEntries = [
|
||||
path: '/knowledge/cases'
|
||||
},
|
||||
{
|
||||
key: 'sop',
|
||||
title: 'SOP知识库',
|
||||
desc: '知识库检索 + RAG问答,支持员工赋能',
|
||||
status: '可演示',
|
||||
type: 'success',
|
||||
status: '待入库',
|
||||
type: 'warning',
|
||||
icon: Reading,
|
||||
path: '/knowledge/sop'
|
||||
}
|
||||
] as const;
|
||||
];
|
||||
|
||||
const goFeature = (path?: string) => {
|
||||
if (!path) return;
|
||||
router.push(path);
|
||||
};
|
||||
|
||||
const stages = [
|
||||
{ step: 1, name: '选:AI面试', desc: '题目与评估', label: '已完成', state: 'done', tagType: 'success' },
|
||||
{ step: 2, name: '用:岗位适配', desc: '与录用评估', label: '已完成', state: 'done', tagType: 'success' },
|
||||
{ step: 3, name: '育:三角色对练', desc: '实战陪练', label: '进行中', state: 'active', tagType: 'danger' },
|
||||
{ step: 4, name: '留:成长追踪', desc: '与能力提升', label: '待启动', state: 'idle', tagType: 'info' },
|
||||
{ step: 5, name: '案例沉淀', desc: '知识与复用', label: '待启动', state: 'idle', tagType: 'info' }
|
||||
] as const;
|
||||
const heroEntries = computed(() => {
|
||||
const statusMap = new Map(displayDashboard.value.heroEntries.map((item) => [item.key, item]));
|
||||
return baseHeroEntries.map((item) => ({ ...item, ...(statusMap.get(item.key) ?? {}) }));
|
||||
});
|
||||
|
||||
const risks = [
|
||||
{ name: '住宅 SOP 素材', owner: '培训组', status: '待确认', type: 'danger', icon: Tickets },
|
||||
{ name: '方言 ASR', owner: '技术组', status: '样片兜底', type: 'warning', icon: Headset },
|
||||
{ name: 'PII 出境评估', owner: '合规组', status: '评估中', type: 'warning', icon: Finished },
|
||||
{ name: '录屏兜底方案', owner: '产品组', status: '已准备', type: 'primary', icon: VideoCamera }
|
||||
] as const;
|
||||
const stages = computed(() => displayDashboard.value.stages);
|
||||
const records = computed(() => displayDashboard.value.records);
|
||||
const pipelineSummary = computed(() => displayDashboard.value.pipelineSummary);
|
||||
|
||||
const records = [
|
||||
{ name: '张三', scene: '客服应对投诉', score: 86, status: '已完成', state: 'done', time: '07-02 09:32' },
|
||||
{ name: '李四', scene: '催缴沟通技巧', score: 78, status: '已完成', state: 'done', time: '07-02 09:12' },
|
||||
{ name: '王五', scene: '突发事件处理', score: 72, status: '进行中', state: 'active', time: '07-02 08:47' },
|
||||
{ name: '赵六', scene: '业主沟通谈判', score: 90, status: '已完成', state: 'done', time: '07-02 08:15' },
|
||||
{ name: '孙七', scene: '服务礼仪规范', score: 65, status: '未达标', state: 'danger', time: '07-01 18:33' }
|
||||
];
|
||||
const pipelineIcons = [FolderChecked, Histogram, Cpu, Tickets, VideoCamera];
|
||||
const pipeline = computed(() =>
|
||||
displayDashboard.value.pipeline.map((step, index) => ({
|
||||
...step,
|
||||
icon: pipelineIcons[index] ?? FolderChecked
|
||||
}))
|
||||
);
|
||||
|
||||
const pipeline = [
|
||||
{ name: '语音上传', count: '12 条', icon: FolderChecked },
|
||||
{ name: 'ASR 转写', count: '12 条', icon: Histogram },
|
||||
{ name: 'AI 整理', count: '10 条', icon: Cpu },
|
||||
{ name: '审核入库', count: '8 条', icon: Tickets },
|
||||
{ name: '样片展示', count: '5 条', icon: VideoCamera }
|
||||
] as const;
|
||||
const risks = computed(() => displayDashboard.value.risks.map((risk) => ({ ...risk, icon: riskIcon(risk) })));
|
||||
const metrics = computed(() => displayDashboard.value.metrics.map((metric) => ({ ...metric, icon: metricIcon(metric) })));
|
||||
|
||||
const metrics = [
|
||||
{ label: '端到端路径', value: '1 条', target: '可完整演示', icon: Switch },
|
||||
{ label: 'RAG 可用率', value: '95%', target: '目标 > 90%', icon: Operation },
|
||||
{ label: '评分一致率', value: '92%', target: '目标 > 85%', icon: TrendCharts },
|
||||
{ label: '试点人数', value: '120 人', target: '目标 > 100 人', icon: DataLine }
|
||||
] as const;
|
||||
const riskIcon = (risk: DashboardRisk) => {
|
||||
if (risk.name.includes('SOP')) return Tickets;
|
||||
if (risk.name.includes('ASR')) return Headset;
|
||||
if (risk.name.includes('审核')) return Finished;
|
||||
return VideoCamera;
|
||||
};
|
||||
|
||||
const metricIcon = (metric: DashboardMetric) => {
|
||||
if (metric.label.includes('路径')) return Switch;
|
||||
if (metric.label.includes('SOP')) return Operation;
|
||||
if (metric.label.includes('训练')) return TrendCharts;
|
||||
return DataLine;
|
||||
};
|
||||
|
||||
const loadDashboard = async () => {
|
||||
dashboardLoading.value = true;
|
||||
try {
|
||||
const { data } = await getDashboardOverview();
|
||||
dashboard.value = data;
|
||||
} catch {
|
||||
dashboard.value = null;
|
||||
} finally {
|
||||
dashboardLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(loadDashboard);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
Reference in New Issue
Block a user