feat(aihr): admin dashboard with real aggregates
新增 /api/aihr/dashboard 聚合面试/对练/知识库/组织快照真实数据, 管理端首页从静态卡片改为 API 优先 + fallback。
This commit is contained in:
+22
@@ -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());
|
||||
}
|
||||
}
|
||||
+42
@@ -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) {
|
||||
}
|
||||
}
|
||||
+304
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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