From 6b8ccd5a35881e72e2baebedc09ce95e1a6a73bc Mon Sep 17 00:00:00 2001 From: let5sne Date: Thu, 9 Jul 2026 23:58:09 +0800 Subject: [PATCH] feat(frontend): add review score calibration --- docs/PC管理端功能差异TODO.md | 2 +- frontend/src/views/train/reviews.vue | 27 +++++++++++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/docs/PC管理端功能差异TODO.md b/docs/PC管理端功能差异TODO.md index 33f39f4c..0967000c 100644 --- a/docs/PC管理端功能差异TODO.md +++ b/docs/PC管理端功能差异TODO.md @@ -6,7 +6,7 @@ | 优先级 | 差异项 | 当前状态 | 本次实现 | 验收口径 | |---|---|---|---|---| | P0 | 组织人员 / 岗位 / 项目权限运营页 | 组织同步 API 存在,侧栏未暴露运营视图 | 新增 `组织与权限` 页面,呈现组织人员快照、岗位映射、项目数据范围和同步预检入口 | PC 可查询人员快照,看到身份三轴、权限映射、同步结果边界 | -| P0 | 主管复盘 + 人工校准 + 派发训练闭环 | 后端已有复盘、评分校准、派发接口;PC 只有散落入口 | 新增 `复盘校准` 页面,并由后端基于组织快照做项目/人员筛选 | 主管能从一页完成“看记录 -> 写建议 -> 派训练” | +| P0 | 主管复盘 + 人工校准 + 派发训练闭环 | 已挂 `复盘校准` 页面,接真实复盘、评分校准、学分和派发接口 | 保留一页式处理:看记录、写建议、校准总分、给学分、派训练 | 主管能从一页完成“看记录 -> 写建议 -> 校准评分 -> 派训练” | | P0 | 能力画像 / 等级证据包 / 激励规则 | 后端已有画像和证据包接口;PC 无独立页 | 新增 `成长激励` 页面,并用组织快照选择员工查看画像和派发专项 | HR 能看到初/中/高级证据和学分规则口径 | | P1 | 学模块课程 / 考试后台 | 文档要求阶段二建设;当前无页面 | 暂不实现,保留 TODO | 阶段二专项设计后再建课程、课件、考试、成绩表 | | P1 | 大喇叭文件分发 | 总纲阶段二;当前无页面 | 暂不实现,保留 TODO | 半年会后按身份三轴做摘要、已读未读、补推 | diff --git a/frontend/src/views/train/reviews.vue b/frontend/src/views/train/reviews.vue index 3a3fb6d1..ce9ece57 100644 --- a/frontend/src/views/train/reviews.vue +++ b/frontend/src/views/train/reviews.vue @@ -66,9 +66,7 @@

{{ turn.text }}

@@ -92,6 +90,14 @@ + + + + + + + +
@@ -116,7 +122,7 @@ import { Refresh } from '@element-plus/icons-vue'; import { ElMessage } from 'element-plus'; import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'; -import { listPracticeScenarios, type PracticeScenarioResponse } from '@/api/aihr/practice'; +import { calibratePracticeSession, listPracticeScenarios, type PracticeScenarioResponse } from '@/api/aihr/practice'; import { getPracticeAudioBlobUrl, getOrgSnapshot, @@ -144,6 +150,8 @@ const reviewForm = reactive({ reviewer: '主管', advice: '', incentivePoint: 10, + correctedTotal: 0, + calibrationReason: '', nextScenarioId: '', nextReason: '' }); @@ -155,6 +163,8 @@ type ReviewDialogueTurn = ReviewDetail['dialogue'][number]; const audioKey = (index: number) => `${detail.value?.id || 'review'}-${index}`; const hasAudio = (turn: ReviewDialogueTurn) => Boolean(turn.audioUrl || turn.audioOssId); const audioSource = (turn: ReviewDialogueTurn, index: number) => audioSources.value[audioKey(index)] || (!turn.audioOssId ? turn.audioUrl || '' : ''); +const needsCalibration = () => + Boolean(detail.value?.sessionId) && (reviewForm.correctedTotal !== detail.value.score || Boolean(reviewForm.calibrationReason.trim())); const releaseAudioSources = () => { Object.values(audioSources.value).forEach((url) => URL.revokeObjectURL(url)); @@ -224,6 +234,8 @@ const selectReview = async (row: ReviewRecord) => { detail.value = result.data; reviewForm.advice = result.data.reviewAdvice || ''; reviewForm.incentivePoint = result.data.incentivePoint || 10; + reviewForm.correctedTotal = result.data.score; + reviewForm.calibrationReason = ''; reviewForm.nextScenarioId = ''; reviewForm.nextReason = ''; }; @@ -243,6 +255,13 @@ const submitReview = async () => { if (!detail.value) return; saving.value = true; try { + if (needsCalibration()) { + await calibratePracticeSession(detail.value.sessionId, { + reviewer: reviewForm.reviewer, + correctedTotal: reviewForm.correctedTotal, + reason: reviewForm.calibrationReason + }); + } await markPracticeReviewed(detail.value.id, { reviewer: reviewForm.reviewer, advice: reviewForm.advice,