feat(frontend): add review score calibration
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
| 优先级 | 差异项 | 当前状态 | 本次实现 | 验收口径 |
|
||||
|---|---|---|---|---|
|
||||
| P0 | 组织人员 / 岗位 / 项目权限运营页 | 组织同步 API 存在,侧栏未暴露运营视图 | 新增 `组织与权限` 页面,呈现组织人员快照、岗位映射、项目数据范围和同步预检入口 | PC 可查询人员快照,看到身份三轴、权限映射、同步结果边界 |
|
||||
| P0 | 主管复盘 + 人工校准 + 派发训练闭环 | 后端已有复盘、评分校准、派发接口;PC 只有散落入口 | 新增 `复盘校准` 页面,并由后端基于组织快照做项目/人员筛选 | 主管能从一页完成“看记录 -> 写建议 -> 派训练” |
|
||||
| P0 | 主管复盘 + 人工校准 + 派发训练闭环 | 已挂 `复盘校准` 页面,接真实复盘、评分校准、学分和派发接口 | 保留一页式处理:看记录、写建议、校准总分、给学分、派训练 | 主管能从一页完成“看记录 -> 写建议 -> 校准评分 -> 派训练” |
|
||||
| P0 | 能力画像 / 等级证据包 / 激励规则 | 后端已有画像和证据包接口;PC 无独立页 | 新增 `成长激励` 页面,并用组织快照选择员工查看画像和派发专项 | HR 能看到初/中/高级证据和学分规则口径 |
|
||||
| P1 | 学模块课程 / 考试后台 | 文档要求阶段二建设;当前无页面 | 暂不实现,保留 TODO | 阶段二专项设计后再建课程、课件、考试、成绩表 |
|
||||
| P1 | 大喇叭文件分发 | 总纲阶段二;当前无页面 | 暂不实现,保留 TODO | 半年会后按身份三轴做摘要、已读未读、补推 |
|
||||
|
||||
@@ -66,9 +66,7 @@
|
||||
<p>{{ turn.text }}</p>
|
||||
<div v-if="hasAudio(turn)" class="dialogue-audio-row">
|
||||
<audio v-if="audioSource(turn, index)" controls :src="audioSource(turn, index)" />
|
||||
<el-button v-else size="small" :loading="audioLoading[audioKey(index)]" @click.stop="loadAudio(turn, index)">
|
||||
加载录音
|
||||
</el-button>
|
||||
<el-button v-else size="small" :loading="audioLoading[audioKey(index)]" @click.stop="loadAudio(turn, index)">加载录音</el-button>
|
||||
<span v-if="audioErrors[audioKey(index)]" class="audio-error">{{ audioErrors[audioKey(index)] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -92,6 +90,14 @@
|
||||
<el-form-item label="贡献学分">
|
||||
<el-input-number v-model="reviewForm.incentivePoint" :min="0" :max="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="人工校准总分">
|
||||
<el-input-number v-model="reviewForm.correctedTotal" :min="0" :max="100" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="校准原因">
|
||||
<el-input v-model="reviewForm.calibrationReason" placeholder="例如:AI 对安抚情绪权重偏低" />
|
||||
</el-form-item>
|
||||
<div class="form-grid">
|
||||
<el-form-item label="下次训练">
|
||||
<el-select v-model="reviewForm.nextScenarioId" clearable placeholder="不派发">
|
||||
<el-option v-for="scenario in scenarios" :key="scenario.id" :label="scenario.name" :value="scenario.id" />
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user