fix(frontend): play review audio in calibration page
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import request from '@/utils/request';
|
||||
import axios from 'axios';
|
||||
import request, { globalHeaders } from '@/utils/request';
|
||||
|
||||
type ApiResult<T> = {
|
||||
code: number;
|
||||
@@ -24,7 +25,7 @@ export type ReviewDetail = ReviewRecord & {
|
||||
reviewAdvice: string;
|
||||
incentivePoint: number;
|
||||
scoreItems: Array<{ label: string; value: number | string; note: string }>;
|
||||
dialogue: Array<{ role: string; label: string; text: string; audioUrl?: string }>;
|
||||
dialogue: Array<{ role: string; label: string; text: string; audioUrl?: string; audioOssId?: number | string }>;
|
||||
annotations: Array<{ turnIndex: number; sourceText: string; issue: string; rewrite: string; sopPoint: string }>;
|
||||
};
|
||||
|
||||
@@ -175,6 +176,16 @@ export function getPracticeReview(id: number): Promise<ApiResult<ReviewDetail>>
|
||||
});
|
||||
}
|
||||
|
||||
export async function getPracticeAudioBlobUrl(ossId: number | string): Promise<string> {
|
||||
const response = await axios({
|
||||
method: 'get',
|
||||
url: `${import.meta.env.VITE_APP_BASE_API}/api/aihr/mobile/oss/${ossId}`,
|
||||
responseType: 'blob',
|
||||
headers: globalHeaders()
|
||||
});
|
||||
return URL.createObjectURL(response.data);
|
||||
}
|
||||
|
||||
export function markPracticeReviewed(id: number, data: ReviewAdviceRequest): Promise<ApiResult<null>> {
|
||||
return request({
|
||||
url: `/api/aihr/mobile/practice/reviews/${id}/reviewed`,
|
||||
|
||||
@@ -59,8 +59,18 @@
|
||||
|
||||
<div class="dialogue-list">
|
||||
<div v-for="(turn, index) in detail.dialogue" :key="`${turn.role}-${index}`" class="dialogue-item">
|
||||
<strong>{{ turn.label }}</strong>
|
||||
<div class="dialogue-head">
|
||||
<strong>{{ turn.label }}</strong>
|
||||
<el-tag v-if="hasAudio(turn)" size="small" type="info">有录音</el-tag>
|
||||
</div>
|
||||
<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>
|
||||
<span v-if="audioErrors[audioKey(index)]" class="audio-error">{{ audioErrors[audioKey(index)] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -105,9 +115,10 @@
|
||||
<script setup name="TrainReviews" lang="ts">
|
||||
import { Refresh } from '@element-plus/icons-vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { listPracticeScenarios, type PracticeScenarioResponse } from '@/api/aihr/practice';
|
||||
import {
|
||||
getPracticeAudioBlobUrl,
|
||||
getOrgSnapshot,
|
||||
getPracticeReview,
|
||||
listPracticeReviews,
|
||||
@@ -125,6 +136,9 @@ const scenarios = ref<PracticeScenarioResponse[]>([]);
|
||||
const orgSnapshot = ref<OrgSnapshotResponse | null>(null);
|
||||
const selectedProjectCode = ref('');
|
||||
const selectedExtPartyId = ref('');
|
||||
const audioSources = ref<Record<string, string>>({});
|
||||
const audioLoading = ref<Record<string, boolean>>({});
|
||||
const audioErrors = ref<Record<string, string>>({});
|
||||
|
||||
const reviewForm = reactive({
|
||||
reviewer: '主管',
|
||||
@@ -136,6 +150,33 @@ const reviewForm = reactive({
|
||||
|
||||
const orgPeople = computed(() => orgSnapshot.value?.rows || []);
|
||||
const visibleReviews = computed(() => reviews.value);
|
||||
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 releaseAudioSources = () => {
|
||||
Object.values(audioSources.value).forEach((url) => URL.revokeObjectURL(url));
|
||||
audioSources.value = {};
|
||||
audioLoading.value = {};
|
||||
audioErrors.value = {};
|
||||
};
|
||||
|
||||
const loadAudio = async (turn: ReviewDialogueTurn, index: number) => {
|
||||
const key = audioKey(index);
|
||||
if (audioSource(turn, index) || !turn.audioOssId || audioLoading.value[key]) return;
|
||||
audioLoading.value = { ...audioLoading.value, [key]: true };
|
||||
audioErrors.value = { ...audioErrors.value, [key]: '' };
|
||||
try {
|
||||
const url = await getPracticeAudioBlobUrl(turn.audioOssId);
|
||||
audioSources.value = { ...audioSources.value, [key]: url };
|
||||
} catch {
|
||||
audioErrors.value = { ...audioErrors.value, [key]: '录音加载失败' };
|
||||
} finally {
|
||||
audioLoading.value = { ...audioLoading.value, [key]: false };
|
||||
}
|
||||
};
|
||||
|
||||
const loadScenarios = async () => {
|
||||
try {
|
||||
@@ -178,6 +219,7 @@ const loadReviews = async () => {
|
||||
};
|
||||
|
||||
const selectReview = async (row: ReviewRecord) => {
|
||||
releaseAudioSources();
|
||||
const result = await getPracticeReview(row.id);
|
||||
detail.value = result.data;
|
||||
reviewForm.advice = result.data.reviewAdvice || '';
|
||||
@@ -190,6 +232,7 @@ const selectFirstVisibleReview = async () => {
|
||||
if (visibleReviews.value.some((item) => item.id === detail.value?.id)) return;
|
||||
const first = visibleReviews.value[0];
|
||||
if (!first) {
|
||||
releaseAudioSources();
|
||||
detail.value = null;
|
||||
return;
|
||||
}
|
||||
@@ -208,6 +251,7 @@ const submitReview = async () => {
|
||||
nextReason: reviewForm.nextReason
|
||||
});
|
||||
ElMessage.success('已完成复盘处理');
|
||||
releaseAudioSources();
|
||||
detail.value = null;
|
||||
await loadReviews();
|
||||
} catch (error) {
|
||||
@@ -232,6 +276,8 @@ onMounted(() => {
|
||||
loadOrgSnapshot();
|
||||
loadReviews();
|
||||
});
|
||||
|
||||
onBeforeUnmount(releaseAudioSources);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -338,6 +384,26 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.dialogue-head,
|
||||
.dialogue-audio-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dialogue-audio-row {
|
||||
margin-top: 10px;
|
||||
|
||||
audio {
|
||||
width: min(360px, 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.audio-error {
|
||||
color: #d92d20;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.annotation-list {
|
||||
margin-top: 18px;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user