feat(aihr): admin dashboard with real aggregates

新增 /api/aihr/dashboard 聚合面试/对练/知识库/组织快照真实数据,
管理端首页从静态卡片改为 API 优先 + fallback。
This commit is contained in:
2026-07-04 16:05:50 +08:00
parent 0539adf612
commit d622c7fb7e
5 changed files with 540 additions and 45 deletions
@@ -0,0 +1,22 @@
package org.dromara.aihr.controller;
import lombok.RequiredArgsConstructor;
import org.dromara.aihr.domain.AihrDashboardDto.OverviewResponse;
import org.dromara.aihr.service.AihrDashboardService;
import org.dromara.common.core.domain.R;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/aihr/dashboard")
public class AihrDashboardController {
private final AihrDashboardService dashboardService;
@GetMapping("/overview")
public R<OverviewResponse> overview() {
return R.ok(dashboardService.overview());
}
}
@@ -0,0 +1,42 @@
package org.dromara.aihr.domain;
import java.util.List;
public final class AihrDashboardDto {
private AihrDashboardDto() {
}
public record OverviewResponse(
String snapshotTime,
List<FeatureStatusResponse> heroEntries,
List<StageResponse> stages,
List<RiskResponse> risks,
List<TrainingRecordResponse> records,
List<PipelineStepResponse> pipeline,
PipelineSummaryResponse pipelineSummary,
List<MetricResponse> metrics
) {
}
public record FeatureStatusResponse(String key, String status, String type, String note) {
}
public record StageResponse(Integer step, String name, String desc, String label, String state, String tagType) {
}
public record RiskResponse(String name, String owner, String status, String type) {
}
public record TrainingRecordResponse(String name, String scene, Integer score, String status, String state, String time) {
}
public record PipelineStepResponse(String name, String count, String state) {
}
public record PipelineSummaryResponse(String todayNew, String pending, String completed) {
}
public record MetricResponse(String label, String value, String target, String type) {
}
}
@@ -0,0 +1,304 @@
package org.dromara.aihr.service;
import lombok.RequiredArgsConstructor;
import org.dromara.aihr.domain.AihrDashboardDto.FeatureStatusResponse;
import org.dromara.aihr.domain.AihrDashboardDto.MetricResponse;
import org.dromara.aihr.domain.AihrDashboardDto.OverviewResponse;
import org.dromara.aihr.domain.AihrDashboardDto.PipelineStepResponse;
import org.dromara.aihr.domain.AihrDashboardDto.PipelineSummaryResponse;
import org.dromara.aihr.domain.AihrDashboardDto.RiskResponse;
import org.dromara.aihr.domain.AihrDashboardDto.StageResponse;
import org.dromara.aihr.domain.AihrDashboardDto.TrainingRecordResponse;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
@Service
@RequiredArgsConstructor
public class AihrDashboardService {
private static final String TENANT_ID = "000000";
private static final DateTimeFormatter SNAPSHOT_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter ROW_TIME = DateTimeFormatter.ofPattern("MM-dd HH:mm");
private final JdbcTemplate jdbcTemplate;
public OverviewResponse overview() {
InterviewStats interview = interviewStats();
PracticeStats practice = practiceStats();
KnowledgeStats knowledge = knowledgeStats();
CaseStats cases = caseStats();
CandidateStats candidate = candidateStats();
OrgStats org = orgStats();
ModelStats models = modelStats();
int completedPaths = bool(interview.total() > 0) + bool(practice.total() > 0) + bool(cases.stored() > 0) + bool(knowledge.fragments() > 0);
return new OverviewResponse(
LocalDateTime.now().format(SNAPSHOT_TIME),
heroEntries(interview, practice, knowledge, cases, models),
stages(interview, practice, knowledge, cases, org),
risks(practice, knowledge, candidate, models),
trainingRecords(),
pipeline(cases),
new PipelineSummaryResponse(cases.todayNew() + " 条", cases.pending() + " 条", cases.stored() + " 条"),
List.of(
new MetricResponse("端到端路径", completedPaths + " 条", "L0 目标 >= 1 条", metricType(completedPaths, 1)),
new MetricResponse("SOP已入库", knowledge.completedDocs() + " 份", "B1 目标 >= 5 流程", metricType(knowledge.completedDocs(), 5)),
new MetricResponse("训练记录", practice.total() + " 条", "育:至少 1 类场景", metricType(practice.total(), 1)),
new MetricResponse("试点人数", org.activePeople() + " 人", "D1 目标 >= 20 人", metricType(org.activePeople(), 20))
)
);
}
private List<FeatureStatusResponse> heroEntries(InterviewStats interview, PracticeStats practice, KnowledgeStats knowledge, CaseStats cases, ModelStats models) {
return List.of(
new FeatureStatusResponse("interview", interview.total() > 0 ? "已有记录" : "可演示", "success", interview.total() + " 条面试结果"),
new FeatureStatusResponse("practice", practice.total() > 0 ? "已有记录" : "待跑通", practice.total() > 0 ? "success" : "warning", practice.total() + " 条训练记录"),
new FeatureStatusResponse("case", cases.stored() > 0 ? "已入库" : models.asrEnabled() ? "可试跑" : "样片兜底", cases.stored() > 0 ? "success" : "warning", cases.stored() + " 条案例入库"),
new FeatureStatusResponse("sop", knowledge.fragments() > 0 ? "可检索" : "待入库", knowledge.fragments() > 0 ? "success" : "warning", knowledge.fragments() + " 个知识片段")
);
}
private List<StageResponse> stages(InterviewStats interview, PracticeStats practice, KnowledgeStats knowledge, CaseStats cases, OrgStats org) {
return List.of(
new StageResponse(1, "选:AI面试", "题目与评估", interview.total() > 0 ? "已留痕" : "可演示", "done", "success"),
new StageResponse(2, "用:岗位适配", "组织快照", org.activePeople() > 0 ? "已接入" : "待同步", org.activePeople() > 0 ? "done" : "idle", org.activePeople() > 0 ? "success" : "info"),
new StageResponse(3, "育:三角色对练", "实战陪练", practice.total() > 0 ? "已留痕" : "待演示", practice.total() > 0 ? "done" : "active", practice.total() > 0 ? "success" : "danger"),
new StageResponse(4, "留:成长追踪", "能力画像", practice.total() > 0 ? "可生成" : "待积累", practice.total() > 0 ? "active" : "idle", practice.total() > 0 ? "warning" : "info"),
new StageResponse(5, "案例沉淀", "知识与复用", cases.stored() > 0 || knowledge.completedDocs() > 0 ? "进行中" : "待入库", cases.stored() > 0 ? "done" : "idle", cases.stored() > 0 ? "success" : "info")
);
}
private InterviewStats interviewStats() {
if (!tableExists("aihr_interview_result")) {
return new InterviewStats(0);
}
return new InterviewStats(count("SELECT COUNT(*) FROM aihr_interview_result WHERE tenant_id = ?", TENANT_ID));
}
private List<RiskResponse> risks(PracticeStats practice, KnowledgeStats knowledge, CandidateStats candidate, ModelStats models) {
return List.of(
new RiskResponse("住宅 SOP 素材", "培训组", knowledge.completedDocs() >= 5 ? "已满足" : knowledge.completedDocs() + "/5", metricType(knowledge.completedDocs(), 5)),
new RiskResponse("方言 ASR", "技术组", models.asrEnabled() ? "已配置" : "未配置", models.asrEnabled() ? "success" : "warning"),
new RiskResponse("候选资料审核", "HR组", candidate.pending() > 0 ? "待审核 " + candidate.pending() : "无待审", candidate.pending() > 0 ? "warning" : "success"),
new RiskResponse("移动训练复盘", "主管", practice.pendingReview() > 0 ? "待复盘 " + practice.pendingReview() : "无待复盘", practice.pendingReview() > 0 ? "warning" : "success")
);
}
private List<PipelineStepResponse> pipeline(CaseStats cases) {
return List.of(
new PipelineStepResponse("语音上传", cases.total() + " 条", state(cases.total())),
new PipelineStepResponse("ASR 转写", cases.transcribed() + " 条", state(cases.transcribed())),
new PipelineStepResponse("AI 整理", cases.organized() + " 条", state(cases.organized())),
new PipelineStepResponse("审核入库", cases.stored() + " 条", state(cases.stored())),
new PipelineStepResponse("样片展示", cases.stored() + " 条", state(cases.stored()))
);
}
private List<TrainingRecordResponse> trainingRecords() {
if (!tableExists("aihr_practice_session")) {
return List.of();
}
try {
return jdbcTemplate.query("""
SELECT trainee_name, scenario_name, total_score, status, finished_time
FROM aihr_practice_session
WHERE tenant_id = ? AND finished_time IS NOT NULL
ORDER BY finished_time DESC, id DESC
LIMIT 5
""", this::trainingRecord, TENANT_ID);
} catch (DataAccessException e) {
return List.of();
}
}
private TrainingRecordResponse trainingRecord(ResultSet rs, int rowNum) throws SQLException {
int score = rs.getInt("total_score");
String status = blankTo(rs.getString("status"), score > 0 ? "已完成" : "待评分");
return new TrainingRecordResponse(
blankTo(rs.getString("trainee_name"), "-"),
blankTo(rs.getString("scenario_name"), "-"),
score,
status,
statusState(status, score),
formatTime(rs.getTimestamp("finished_time"))
);
}
private PracticeStats practiceStats() {
if (!tableExists("aihr_practice_session")) {
return new PracticeStats(0, 0, 0);
}
try {
return jdbcTemplate.queryForObject("""
SELECT COUNT(*) total,
SUM(CASE WHEN status = '待复盘' THEN 1 ELSE 0 END) pending_review,
SUM(CASE WHEN DATE(finished_time) = CURRENT_DATE THEN 1 ELSE 0 END) today
FROM aihr_practice_session
WHERE tenant_id = ? AND finished_time IS NOT NULL
""", (rs, rowNum) -> new PracticeStats(rs.getInt("total"), rs.getInt("pending_review"), rs.getInt("today")), TENANT_ID);
} catch (DataAccessException e) {
return new PracticeStats(0, 0, 0);
}
}
private KnowledgeStats knowledgeStats() {
if (!tableExists("aihr_knowledge_attach") || !tableExists("aihr_knowledge_fragment")) {
return new KnowledgeStats(0, 0, 0);
}
return new KnowledgeStats(
count("SELECT COUNT(*) FROM aihr_knowledge_attach WHERE tenant_id = ?", TENANT_ID),
count("SELECT COUNT(*) FROM aihr_knowledge_attach WHERE tenant_id = ? AND status = 2", TENANT_ID),
count("SELECT COUNT(*) FROM aihr_knowledge_fragment WHERE tenant_id = ?", TENANT_ID)
);
}
private CaseStats caseStats() {
if (!tableExists("aihr_case_record")) {
return new CaseStats(0, 0, 0, 0, 0);
}
try {
return jdbcTemplate.queryForObject("""
SELECT COUNT(*) total,
SUM(CASE WHEN status IN ('已转写', '已整理', '已入库') THEN 1 ELSE 0 END) transcribed,
SUM(CASE WHEN status IN ('已整理', '已入库') THEN 1 ELSE 0 END) organized,
SUM(CASE WHEN status = '已入库' THEN 1 ELSE 0 END) stored,
SUM(CASE WHEN DATE(create_time) = CURRENT_DATE THEN 1 ELSE 0 END) today_new
FROM aihr_case_record
WHERE tenant_id = ?
""", (rs, rowNum) -> new CaseStats(
rs.getInt("total"),
rs.getInt("transcribed"),
rs.getInt("organized"),
rs.getInt("stored"),
rs.getInt("today_new")
), TENANT_ID);
} catch (DataAccessException e) {
return new CaseStats(0, 0, 0, 0, 0);
}
}
private CandidateStats candidateStats() {
if (!tableExists("aihr_candidate_material")) {
return new CandidateStats(0, 0);
}
try {
return jdbcTemplate.queryForObject("""
SELECT COUNT(*) total,
SUM(CASE WHEN status = '待审核' THEN 1 ELSE 0 END) pending
FROM aihr_candidate_material
WHERE tenant_id = ?
""", (rs, rowNum) -> new CandidateStats(rs.getInt("total"), rs.getInt("pending")), TENANT_ID);
} catch (DataAccessException e) {
return new CandidateStats(0, 0);
}
}
private OrgStats orgStats() {
if (!tableExists("aihr_org_snapshot")) {
return new OrgStats(0);
}
return new OrgStats(count("""
SELECT COUNT(*)
FROM aihr_org_snapshot
WHERE tenant_id = ? AND employment_status = 'active'
""", TENANT_ID));
}
private ModelStats modelStats() {
if (!tableExists("aihr_model_config")) {
return new ModelStats(false);
}
return new ModelStats(count("""
SELECT COUNT(*)
FROM aihr_model_config
WHERE tenant_id = ? AND category = 'asr' AND enabled = 1
""", TENANT_ID) > 0);
}
private boolean tableExists(String tableName) {
try {
Integer count = jdbcTemplate.queryForObject("""
SELECT COUNT(*)
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?
""", Integer.class, tableName);
return count != null && count > 0;
} catch (DataAccessException e) {
return false;
}
}
private int count(String sql, Object... args) {
try {
Integer value = jdbcTemplate.queryForObject(sql, Integer.class, args);
return value == null ? 0 : value;
} catch (DataAccessException e) {
return 0;
}
}
private static String statusState(String status, int score) {
if (score > 0 && score < 70) {
return "danger";
}
if (status.contains("待") || status.contains("进行")) {
return "active";
}
return "done";
}
private static String state(int count) {
return count > 0 ? "done" : "todo";
}
private static String metricType(int value, int target) {
if (value >= target) {
return "success";
}
return value > 0 ? "warning" : "danger";
}
private static int bool(boolean value) {
return value ? 1 : 0;
}
private static String formatTime(Timestamp time) {
return time == null ? "" : time.toLocalDateTime().format(ROW_TIME);
}
private static String blankTo(String value, String fallback) {
return value == null || value.isBlank() ? fallback : value;
}
private record PracticeStats(int total, int pendingReview, int today) {
}
private record InterviewStats(int total) {
}
private record KnowledgeStats(int totalDocs, int completedDocs, int fragments) {
}
private record CaseStats(int total, int transcribed, int organized, int stored, int todayNew) {
int pending() {
return Math.max(0, organized - stored);
}
}
private record CandidateStats(int total, int pending) {
}
private record OrgStats(int activePeople) {
}
private record ModelStats(boolean asrEnabled) {
}
}
+74
View File
@@ -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'
});
}
+98 -45
View File
@@ -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>