feat(aihr): add scoped case detail view
This commit is contained in:
+8
@@ -6,6 +6,7 @@ import org.dromara.common.core.enums.UserType;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.CurateRequest;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.CurateResponse;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.DetailResponse;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.OrganizeRequest;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.OrganizeResponse;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.RecordResponse;
|
||||
@@ -16,6 +17,7 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
@@ -54,6 +56,12 @@ public class AihrCaseController {
|
||||
return R.ok(caseService.records(limit, currentProjectScopes()));
|
||||
}
|
||||
|
||||
@GetMapping("/records/{caseId}")
|
||||
public R<DetailResponse> detail(@PathVariable String caseId) {
|
||||
DetailResponse detail = caseService.detail(caseId, currentProjectScopes());
|
||||
return detail == null ? R.fail("案例不存在或无权访问") : R.ok(detail);
|
||||
}
|
||||
|
||||
private List<String> currentProjectScopes() {
|
||||
LoginUser loginUser = LoginHelper.getLoginUser();
|
||||
if (loginUser != null && UserType.SYS_USER.getUserType().equals(loginUser.getUserType())) {
|
||||
|
||||
+8
-1
@@ -31,6 +31,13 @@ public final class AihrCaseDto {
|
||||
public record TagResponse(String text, String type) {
|
||||
}
|
||||
|
||||
public record RecordResponse(String title, String type, String status, String time, String owner, String summary) {
|
||||
public record RecordResponse(String caseId, String title, String type, String status, String time, String owner, String summary) {
|
||||
public RecordResponse(String title, String type, String status, String time, String owner, String summary) {
|
||||
this("", title, type, status, time, owner, summary);
|
||||
}
|
||||
}
|
||||
|
||||
public record DetailResponse(String caseId, String fileName, String title, String type, String status,
|
||||
String time, String owner, String transcript, String summary) {
|
||||
}
|
||||
}
|
||||
|
||||
+35
-1
@@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.CurateRequest;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.CurateResponse;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.DetailResponse;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.OrganizeRequest;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.OrganizeResponse;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.RecordResponse;
|
||||
@@ -79,6 +80,7 @@ public class AihrCaseService {
|
||||
List<SummaryResponse> summary = state.summary() == null ? localSummary(state).summary() : state.summary();
|
||||
String title = caseTitle(summary, state.fileName());
|
||||
RecordResponse record = new RecordResponse(
|
||||
state.id(),
|
||||
title,
|
||||
primaryTag(state.tags()),
|
||||
"已入库",
|
||||
@@ -104,7 +106,7 @@ public class AihrCaseService {
|
||||
}
|
||||
args.add(safeLimit);
|
||||
String sql = """
|
||||
SELECT title, type, status, update_time, owner, summary
|
||||
SELECT case_id, title, type, status, update_time, owner, summary
|
||||
FROM aihr_case_record
|
||||
WHERE tenant_id = '000000'
|
||||
""" + projectWhere + """
|
||||
@@ -112,6 +114,7 @@ public class AihrCaseService {
|
||||
LIMIT ?
|
||||
""";
|
||||
return jdbcTemplate.query(sql, (rs, rowNum) -> new RecordResponse(
|
||||
firstNonBlank(rs.getString("case_id"), ""),
|
||||
firstNonBlank(rs.getString("title"), "现场案例"),
|
||||
firstNonBlank(rs.getString("type"), "现场案例"),
|
||||
firstNonBlank(rs.getString("status"), "已入库"),
|
||||
@@ -121,6 +124,37 @@ public class AihrCaseService {
|
||||
), args.toArray());
|
||||
}
|
||||
|
||||
public DetailResponse detail(String caseId, List<String> projectScopes) {
|
||||
String normalizedCaseId = clean(caseId);
|
||||
if (normalizedCaseId.isBlank() || (projectScopes != null && projectScopes.isEmpty())) {
|
||||
return null;
|
||||
}
|
||||
ensureCaseTable();
|
||||
List<Object> args = new ArrayList<>();
|
||||
args.add(normalizedCaseId);
|
||||
String projectWhere = "";
|
||||
if (projectScopes != null) {
|
||||
projectWhere = " AND project_ext_org_id IN (" + placeholders(projectScopes.size()) + ")";
|
||||
args.addAll(projectScopes);
|
||||
}
|
||||
String sql = """
|
||||
SELECT case_id, file_name, title, type, status, update_time, owner, transcript, summary
|
||||
FROM aihr_case_record
|
||||
WHERE tenant_id = '000000' AND case_id = ?
|
||||
""" + projectWhere + " LIMIT 1";
|
||||
return jdbcTemplate.query(sql, (rs, rowNum) -> new DetailResponse(
|
||||
firstNonBlank(rs.getString("case_id"), normalizedCaseId),
|
||||
firstNonBlank(rs.getString("file_name"), "case-audio"),
|
||||
firstNonBlank(rs.getString("title"), "现场案例"),
|
||||
firstNonBlank(rs.getString("type"), "现场案例"),
|
||||
firstNonBlank(rs.getString("status"), "已入库"),
|
||||
rs.getTimestamp("update_time") == null ? "" : rs.getTimestamp("update_time").toLocalDateTime().format(TIME_FORMAT),
|
||||
firstNonBlank(rs.getString("owner"), "培训组"),
|
||||
firstNonBlank(rs.getString("transcript"), ""),
|
||||
firstNonBlank(rs.getString("summary"), "")
|
||||
), args.toArray()).stream().findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public List<String> projectScopesForAppUser(String identity) {
|
||||
String username = clean(identity);
|
||||
if (username.isBlank()) {
|
||||
|
||||
+16
@@ -58,6 +58,19 @@ class AihrCaseServiceTest {
|
||||
assertEquals("persisted-case", jdbcTemplate.lastCaseId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("dev")
|
||||
void detailReturnsPersistedCaseForAuthorizedProject() {
|
||||
PersistentCaseJdbcTemplate jdbcTemplate = new PersistentCaseJdbcTemplate();
|
||||
AihrCaseService service = new AihrCaseService(null, null, new ObjectMapper(), jdbcTemplate);
|
||||
|
||||
var detail = service.detail("persisted-case", List.of("P1"));
|
||||
|
||||
assertEquals("persisted-case", detail.caseId());
|
||||
assertEquals("业主投诉后完成跟进", detail.transcript());
|
||||
assertEquals("P1", jdbcTemplate.lastArgs[1]);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("dev")
|
||||
void appCaseScopeMustMatchAnActiveProject() {
|
||||
@@ -84,10 +97,12 @@ class AihrCaseServiceTest {
|
||||
|
||||
private static final class PersistentCaseJdbcTemplate extends CaseJdbcTemplate {
|
||||
private String lastCaseId;
|
||||
private Object[] lastArgs = new Object[0];
|
||||
|
||||
@Override
|
||||
public <T> List<T> query(String sql, RowMapper<T> rowMapper, Object... args) {
|
||||
this.lastCaseId = String.valueOf(args[0]);
|
||||
this.lastArgs = args;
|
||||
try {
|
||||
ResultSet resultSet = mock(ResultSet.class);
|
||||
when(resultSet.getString("case_id")).thenReturn("persisted-case");
|
||||
@@ -95,6 +110,7 @@ class AihrCaseServiceTest {
|
||||
when(resultSet.getString("project_ext_org_id")).thenReturn("P1");
|
||||
when(resultSet.getString("transcript")).thenReturn("业主投诉后完成跟进");
|
||||
when(resultSet.getString("type")).thenReturn("投诉处理");
|
||||
when(resultSet.getString("title")).thenReturn("业主投诉案例");
|
||||
return List.of(rowMapper.mapRow(resultSet, 0));
|
||||
} catch (SQLException e) {
|
||||
throw new AssertionError(e);
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
| 三角色对练 `/train/practice` | `POST /api/train/practice/start`、`/turn`、`/finish` | 已接入编排 API;数据库启用 chat 模型后,`/turn` 客户回复按人设走真 LLM 生成(seed 剧本作剧情锚点),`/finish` 走单次 temperature=0 结构化评分(4 维分+导师改写+点评);模型未配置或调用失败自动回退 seed,契约不变 |
|
||||
| 正式试点数据导出 | `GET /api/train/practice/export?startDate=YYYY-MM-DD&endDate=YYYY-MM-DD` | 起止日期必填且包含结束日;只统计窗口内能通过唯一手机号或外部 ID 映射到在职组织快照的正式会话,排除重复手机号和身份碰撞。完训定义为每人至少 10 次,校准必须关联同一窗口内正式会话;CSV 同时给出校准命中数、SOP 可用数、满意度响应数/平均分,以及明细级 AI 分、人工校准分、校准人、校准时间、最终采用分、满意度分和意见,避免用四舍五入后的比率反推门禁状态;汇总和明细均携带正式人员及项目口径,不混入历史 seed/开发身份 |
|
||||
| 对练语音 | `POST /api/ai/asr`(multipart 字段 `file`,≤5MB)、`POST /api/ai/tts`(JSON `{text≤300字, voice}`,返回 `{audioUrl}` base64 dataURL)、`GET /api/aihr/mobile/oss/{ossId}` | 走 OpenAI-compatible audio 接口(如硅基流动 SenseVoice/CosyVoice2);模型管理需启用 `category=asr/tts` 配置;移动端优先用浏览器录音,`getUserMedia/MediaRecorder` 不可用或麦克风权限失败时,用 `audio/*` file input 选择/录制音频后继续调同一 ASR 接口;ASR/TTS 未配置或失败返回 fail,前端降级文本;音频下载按员工本人或主管项目范围授权,系统管理端保持后台访问 |
|
||||
| 案例沉淀 `/knowledge/cases` | `POST /api/knowledge/case/upload`、`/organize`、`/curate`、`GET /records` | `/upload` 改为 multipart 真实语音上传并走 ASR;`/organize` 用真实转写调 chat 模型整理案例,未配置模型时按真实 transcript 本地结构化;APP 用户的项目范围从 `aihr_org_snapshot` 登录身份解析,上传、整理、入库和列表均按项目范围校验,未完成正式组织映射时安全拒绝,不接受前端伪造项目范围;管理端系统用户保留全局运营视图 |
|
||||
| 案例沉淀 `/knowledge/cases` | `POST /api/knowledge/case/upload`、`/organize`、`/curate`、`GET /records`、`GET /records/{caseId}` | `/upload` 改为 multipart 真实语音上传并走 ASR;`/organize` 用真实转写调 chat 模型整理案例,未配置模型时按真实 transcript 本地结构化;APP 用户的项目范围从 `aihr_org_snapshot` 登录身份解析,上传、整理、入库、列表和详情均按项目范围校验,未完成正式组织映射时安全拒绝,不接受前端伪造项目范围;移动端可查看持久化转写/摘要详情;管理端系统用户保留全局运营视图;视频样片、学习点和主管点评仍未接入 |
|
||||
| SOP知识库 `/knowledge/sop` | `POST /api/knowledge/search`、`POST /api/knowledge/answer-feedback`、`GET /api/knowledge/position-sop`、`POST /api/knowledge/doc/upload` | 已接入 MySQL Fulltext + Qdrant 混合召回、岗位学习适配摘要、OSS-first 文档上传、txt/md/PDF/Word/Excel/PPT 解析和 embedding 写入,失败回退 seed;搜索返回 `reviewId` 与 `promptVersion`,员工反馈回传并保存该评审批次,SOP 人工评审记录同时保留答案生成提示词版本,管理端可继续复核;`position-sop` 目前只返回一期生活顾问学习导航,不代表正式上岗资格 |
|
||||
| 资料处理 `/knowledge/processing` | `GET /api/knowledge/processing/overview`、`POST /api/knowledge/doc/upload-async`、`GET /api/knowledge/doc/upload-items`、`POST /api/knowledge/doc/upload-items/{id}/retry`、`POST /api/knowledge/doc/import-local-task`、`GET /api/knowledge/doc/import-tasks`、`POST /api/knowledge/doc/import-tasks/{id}/cancel` | 已接入解析任务状态聚合;**批量上传走异步队列**:接口只暂存+入队即秒回,后台 worker(并发 2)逐条解析/归类/向量化;ZIP 在 worker 内安全解压后把支持的子文件继续入同一批次队列,页面按批次轮询进度、失败可单文件重试;服务端目录导入、进度轮询和任务取消保留,失败回退 seed |
|
||||
| 组织人员同步 | `POST /api/aihr/org/sync` | 从开放组织同步系统的 `/api/open/v1/sync/snapshot` 拉取 `company/department/employee` 快照,分页参数使用 `limit`;员工手机号只落 `person_phone` 用于移动端身份映射,不在组织列表响应暴露;岗位识别 `position/job_title/post/job_name/role/title` 等字段。默认 `replaceExisting=true`,写入前必须先用 `{"dryRun":true}`;dry-run 不访问本地快照表、不执行 DDL/写库,返回 `phoneLinked/maskedPhone/suspectText/warnings` 且 `syncedCount=0`。非 dry-run 覆盖写入遇到员工被跳过、手机号不完整、脱敏手机号或疑似乱码时默认拒绝,只有确认 dry-run 结果后显式传 `allowPartialReplace=true` 才允许覆盖;重复 `ext_party_id` 始终拒绝写入,因为数据库唯一键会折叠重复身份;`replaceExisting=false` 不触发不完整快照覆盖闸门,但仍拒绝重复身份。2026-07-10 源接口实测 3474 人仅 1 个可用手机号、3473 个脱敏手机号、8 条疑似乱码,因此未执行覆盖同步,正式试点需上游先开放至少 20 名试点人员手机号 |
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
| 岗位-SOP匹配与上岗资格 gating | 3.1、4.2、TechSpec P1 | 已补 `GET /api/knowledge/position-sop` 和员工学习页状态提示:一期只确认生活顾问内置学习导航;尚无 `sop_applicability`、`onboard_task`、`qualification_gate` 正式数据闭环,不能作为上岗资格判断 | 先接正式岗位/项目与 SOP 适用范围数据,再补培训任务状态;资格 gating 必须由 HR 明确规则和证据来源后实现 |
|
||||
| 认证等级与激励规则可配置 | 4.5、10.2-A、TechSpec M6 | 成长激励页已有初/中/高级证据包和学分口径展示,但规则仍是前端硬编码;原“排行榜”开关无实际业务作用,已移除并明确标记只读;尚无 `cert_rule`/`incentive_rule` 的持久化、版本、角色权限和生效记录 | 先由 HR 确认学分来源、分值、防刷和初/中/高级条件,再设计规则表与管理员 CRUD;在规则契约明确前,不把静态展示称为可配置 |
|
||||
| 智能错题本 | 3.1、4.4、TechSpec P1 | 已补员工本人错题本和主管 `/api/aihr/mobile/practice/team` 内的 `mistakes` 聚合:按当前项目权限统计场景、归因、次数、影响人数、平均分和最近发生时间;员工端可跳转待训练 retry assignment,retry 已完成时改为显示“已完成”,不再重复引导进入已完成任务;不新增独立错题事实表 | 继续用正式组织快照和试点窗口验证主管只看到本项目数据;后续可在真实错题量上来后补趋势/导出,不把本地演示样本当正式指标 |
|
||||
| 案例学习/视频详情 | 3.1、4.6、矩阵第35行 | `AihrCaseService` 已能从 `aihr_case_record` 恢复转写案例,避免后端重启后案例整理/入库失效;但仍没有稳定 media URL、学习点、主管点评和案例详情 API,仓库也没有可用于一期验收的预渲染样片资产 | 先由业务提供样片或正式媒体 URL,并确定案例详情、学习点、主管点评字段,再扩持久化契约和权限;没有真实媒体 URL 时不造播放按钮 |
|
||||
| 案例学习/视频详情 | 3.1、4.6、矩阵第35行 | `AihrCaseService` 已能从 `aihr_case_record` 恢复转写案例,且新增按项目范围校验的 `GET /api/knowledge/case/records/{caseId}`,员工端可查看持久化转写、摘要和状态;但仍没有稳定 media URL、学习点、主管点评和案例视频资产 | 先由业务提供样片或正式媒体 URL,并确定案例详情、学习点、主管点评字段,再扩持久化契约和权限;没有真实媒体 URL 时不造播放按钮 |
|
||||
| 案例库质量与数量 | L1-B、4.6/4.7 | 上传/整理/筛选路径已有;“至少20条脱敏案例”和每周每项目一条需要正式数据证明,当前也没有可用于一期样片验收的真实视频资产 | 先按项目窗口采集并脱敏检查,再补案例统计/学习详情导出;不使用空壳视频或演示 seed 冒充 |
|
||||
| RAG 可用率与知识治理 | 4.7、L1-C、G3 | 搜索、引用、反馈、知识缺口和人工评审入口已有;员工反馈保存同一次搜索的 `reviewId`,SOP 评审记录和搜索响应现在保留答案生成 `promptVersion`;住宅核心流程覆盖率与可用率仍需正式评审数据,不能宣称完成逐问答全链路追溯 | 后续由业务确认模型/检索批次的正式版本口径,再补检索批次与模型版本;当前先用 `reviewId + promptVersion + query + fragment` 做可追溯统计,不伪造正式可用率 |
|
||||
| 方言语音与多模态训练验收 | 4.3.2、4.3.3、L1-C | ASR/TTS、音频 OSS 留痕和文本降级链路已有;但当前没有四川话/粤语等方言供应商配置、样本集和准确率/响应时延证据,也没有把图像/视频训练输入纳入一期验收口径 | 先确定一期方言厂商和可接受指标,用真实短音频做至少一条方言路径回归;图像/视频输入另按场景与合规边界评审,不用通用文本链路冒充完成 |
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
| 2 | uni-app 三端 `https://wygj-mobile-uni.localhost/h5/#/pages/user/today/index`、`/pages/candidate/index/index`、`/pages/supervisor/index/index` | 打开后先手机号登录;员工端从今日页进入问师傅、练习、学习、案例素材和个人页;候选首页按真实状态进入独立 `/interview/index`、`/materials/index`、`/progress/index`、`/study/index`;主管首页进入独立团队画像、指派、复盘列表和复盘详情 | 员工端显示训练评分、历史和画像;候选面试页可真实拉题/答题/评分,资料页显示审核三态,进度页展示办理步骤,预习页从正式知识库返回依据;主管端待复盘列表、复盘详情、专项指派和团队范围均为独立页面且普通员工无主管权限 |
|
||||
| 3 | AI面试 `/recruit/interview` | 生成题目 → 填满参考回答或输入真实回答 → 完成评分 | 配置 chat 模型时真实生成题目和评分;未配置时用本地 Rubric 估分,不返回固定候选人分数;应看到新增面试记录 |
|
||||
| 4 | 三角色对练 `/train/practice` | 开始对练 → 填入参考回复或输入真实回复 → 继续一轮 → 结束并评分 | 已完成闭环、导师改写、新增对练记录;数据库已启用 chat 模型时,客户回复与评分为真实 LLM 生成(回复不再是固定台词,分数随话术变化),未配置时使用本地剧本兜底 |
|
||||
| 5 | 案例沉淀 `/knowledge/cases` | 上传真实语音 → ASR 转写 → AI 整理 → 送审 → 入库 | 已完成语音转写、整理和入库闭环;稳定媒体 URL、视频样片、学习点和案例详情尚未形成,不把“查看样片”作为当前完成证据 |
|
||||
| 5 | 案例沉淀 `/knowledge/cases` | 上传真实语音 → ASR 转写 → AI 整理 → 送审 → 入库 → 查看案例详情 | 已完成语音转写、整理、入库和按项目权限查看持久化转写/摘要详情;稳定媒体 URL、视频样片、学习点和主管点评尚未形成,不把“查看样片”作为当前完成证据 |
|
||||
| 6 | SOP知识库 `/knowledge/sop` | 上传 txt/md/PDF/Word/Excel/PPT 文档或使用 seed 文档 → 检索 → 生成训练题 | 已完成闭环、引用 SOP 原文片段、训练题已生成 |
|
||||
| 7 | 资料处理 `/knowledge/processing` | 打开页面 → 查看解析任务 → 查看处理链路 → 可选点“服务端导入”导入 `.data/import` 下少量样例 | 可看到资料总量、等待/解析中/完成/失败、片段数、向量化状态和事件列表 |
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
| 32 | 案例语音上传 | `79:706` | `mobile-uni/src/pages/user/cases/index.vue`、`frontend/src/views/knowledge/cases.vue` | [x] 已实现 | 已修正 |
|
||||
| 33 | 案例 AI 整理结果 | `79:734` | `frontend/src/views/knowledge/cases.vue` | [x] 已实现 | 已修正 |
|
||||
| 34 | 案例审核/筛选/发布 | `82:2` | `frontend/src/views/knowledge/cases.vue` | [x] 已实现 | 已修正 |
|
||||
| 35 | 案例学习/视频详情 | `80:2` | `mobile-uni/src/pages/user/learning/index.vue`、`cases/index.vue` | 部分实现 | 当前只有案例上传/整理/入库和学习入口;后端没有稳定的案例详情、媒体 URL、学习点/主管点评字段,暂不造视频详情页 |
|
||||
| 35 | 案例学习/视频详情 | `80:2` | `mobile-uni/src/pages/user/learning/index.vue`、`cases/index.vue` | 部分实现 | 已补 `caseId`、按项目权限校验的案例详情 API 和员工端转写/摘要详情查看;仍缺稳定媒体 URL、学习点、主管点评与真实视频样片,暂不造视频详情页 |
|
||||
| 36 | 个人资料库首页 | `86:3` | — | 未实现 | 未修正,阶段二 |
|
||||
| 37 | 个人资料列表/筛选搜索 | `86:58` | — | 未实现 | 未修正,阶段二 |
|
||||
| 38 | 收藏资料 | `86:846` | — | 未实现 | 未修正,阶段二 |
|
||||
|
||||
@@ -77,25 +77,43 @@
|
||||
</button>
|
||||
</view>
|
||||
<view v-if="records.length" class="record-list">
|
||||
<view v-for="record in records" :key="`${record.title}-${record.time}`" class="record-row">
|
||||
<view v-for="record in records" :key="`${record.caseId}-${record.title}-${record.time}`" class="record-row">
|
||||
<view>
|
||||
<text class="record-title">{{ record.title }}</text>
|
||||
<text class="record-desc">{{ record.time }} · {{ record.owner }} · {{ record.type }}</text>
|
||||
<text class="record-summary">{{ record.summary }}</text>
|
||||
</view>
|
||||
<view class="record-actions">
|
||||
<text class="record-status">{{ record.status }}</text>
|
||||
<button class="secondary-button detail-button" :disabled="caseDetailLoading" @click="openCaseDetail(record)">查看详情</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty">暂无入库案例</view>
|
||||
</view>
|
||||
|
||||
<view v-if="caseDetail" class="panel case-detail-panel">
|
||||
<view class="section-head">
|
||||
<view>
|
||||
<text class="panel-title">案例详情</text>
|
||||
<text class="record-desc">{{ caseDetail.time }} · {{ caseDetail.owner }} · {{ caseDetail.status }}</text>
|
||||
</view>
|
||||
<button class="ghost-button refresh-button" @click="caseDetail = null">关闭</button>
|
||||
</view>
|
||||
<text class="detail-label">原始转写</text>
|
||||
<text class="case-content">{{ caseDetail.transcript || '暂无转写内容' }}</text>
|
||||
<text class="detail-label">案例摘要</text>
|
||||
<text class="case-note">{{ caseDetail.summary || '暂无案例摘要' }}</text>
|
||||
<text class="case-note">当前版本提供转写、摘要和状态详情;视频样片、学习点与主管点评待正式媒体和业务字段接入。</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
import type { CaseCurateResponse, CaseOrganizeResponse, CaseRecord, CaseSelectedFile, CaseUploadResponse } from '@/types/api';
|
||||
import { chooseCaseAudio, curateCase, getCaseRecords, organizeCase, uploadCaseAudio } from '@/services/case';
|
||||
import type { CaseCurateResponse, CaseDetailResponse, CaseOrganizeResponse, CaseRecord, CaseSelectedFile, CaseUploadResponse } from '@/types/api';
|
||||
import { chooseCaseAudio, curateCase, getCaseDetail, getCaseRecords, organizeCase, uploadCaseAudio } from '@/services/case';
|
||||
import { isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
||||
import { navigateBackToToday, resetPageScroll } from '@/services/navigation';
|
||||
import { ensureEmployeePosition } from '@/services/position';
|
||||
@@ -106,8 +124,10 @@ const uploadResult = ref<CaseUploadResponse | null>(null);
|
||||
const organizeResult = ref<CaseOrganizeResponse | null>(null);
|
||||
const curateResult = ref<CaseCurateResponse | null>(null);
|
||||
const records = ref<CaseRecord[]>([]);
|
||||
const caseDetail = ref<CaseDetailResponse | null>(null);
|
||||
const status = ref<'idle' | 'uploading' | 'organizing' | 'curating'>('idle');
|
||||
const recordsLoading = ref(false);
|
||||
const caseDetailLoading = ref(false);
|
||||
const message = ref('');
|
||||
const busy = computed(() => status.value !== 'idle');
|
||||
const statusText = computed(() => {
|
||||
@@ -126,6 +146,7 @@ const resetResult = () => {
|
||||
const clearCaseData = () => {
|
||||
selectedFile.value = null;
|
||||
records.value = [];
|
||||
caseDetail.value = null;
|
||||
resetResult();
|
||||
};
|
||||
|
||||
@@ -179,6 +200,19 @@ const refreshRecords = () => {
|
||||
void loadRecords();
|
||||
};
|
||||
|
||||
const openCaseDetail = async (record: CaseRecord) => {
|
||||
if (!record.caseId || caseDetailLoading.value) return;
|
||||
caseDetailLoading.value = true;
|
||||
message.value = '';
|
||||
try {
|
||||
caseDetail.value = await getCaseDetail(record.caseId);
|
||||
} catch (error) {
|
||||
handleRequestError(error, '案例详情加载失败');
|
||||
} finally {
|
||||
caseDetailLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const submitCase = async () => {
|
||||
if (!selectedFile.value || busy.value) return;
|
||||
message.value = '';
|
||||
@@ -310,6 +344,31 @@ onShow(() => {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.record-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.detail-button {
|
||||
min-width: 84px;
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.case-detail-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
color: #1a1a1a;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tag-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
CaseCurateResponse,
|
||||
CaseDetailResponse,
|
||||
CaseOrganizeResponse,
|
||||
CaseRecord,
|
||||
CaseSelectedFile,
|
||||
@@ -78,3 +79,10 @@ export const getCaseRecords = () =>
|
||||
method: 'GET',
|
||||
timeout: 15000
|
||||
});
|
||||
|
||||
export const getCaseDetail = (caseId: string) =>
|
||||
apiRequest<CaseDetailResponse>({
|
||||
url: `/api/knowledge/case/records/${encodeURIComponent(caseId)}`,
|
||||
method: 'GET',
|
||||
timeout: 15000
|
||||
});
|
||||
|
||||
@@ -363,6 +363,7 @@ export interface CaseSummaryItem {
|
||||
}
|
||||
|
||||
export interface CaseRecord {
|
||||
caseId: string;
|
||||
title: string;
|
||||
type: string;
|
||||
status: string;
|
||||
@@ -371,6 +372,18 @@ export interface CaseRecord {
|
||||
summary: string;
|
||||
}
|
||||
|
||||
export interface CaseDetailResponse {
|
||||
caseId: string;
|
||||
fileName: string;
|
||||
title: string;
|
||||
type: string;
|
||||
status: string;
|
||||
time: string;
|
||||
owner: string;
|
||||
transcript: string;
|
||||
summary: string;
|
||||
}
|
||||
|
||||
export interface CaseUploadResponse {
|
||||
caseId: string;
|
||||
fileName: string;
|
||||
|
||||
@@ -316,6 +316,8 @@ contains mobile-uni/src/services/knowledge.ts "/api/knowledge/position-sop"
|
||||
contains mobile-uni/src/services/position.ts "removeStorageSync(roleKey)"
|
||||
contains mobile-uni/src/services/position.ts "/api/aihr/mobile/me"
|
||||
contains mobile-uni/src/services/case.ts "/api/knowledge/case/upload"
|
||||
contains mobile-uni/src/services/case.ts "/api/knowledge/case/records/"
|
||||
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrCaseController.java "/records/{caseId}"
|
||||
contains mobile-uni/src/services/candidate.ts "/api/recruit/interview/start"
|
||||
contains mobile-uni/src/services/candidate.ts "/api/aihr/mobile/candidate/materials"
|
||||
contains mobile-uni/src/pages/candidate/interview/index.vue "startCandidateInterview"
|
||||
|
||||
Reference in New Issue
Block a user