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> = {
|
type ApiResult<T> = {
|
||||||
code: number;
|
code: number;
|
||||||
@@ -24,7 +25,7 @@ export type ReviewDetail = ReviewRecord & {
|
|||||||
reviewAdvice: string;
|
reviewAdvice: string;
|
||||||
incentivePoint: number;
|
incentivePoint: number;
|
||||||
scoreItems: Array<{ label: string; value: number | string; note: string }>;
|
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 }>;
|
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>> {
|
export function markPracticeReviewed(id: number, data: ReviewAdviceRequest): Promise<ApiResult<null>> {
|
||||||
return request({
|
return request({
|
||||||
url: `/api/aihr/mobile/practice/reviews/${id}/reviewed`,
|
url: `/api/aihr/mobile/practice/reviews/${id}/reviewed`,
|
||||||
|
|||||||
@@ -59,8 +59,18 @@
|
|||||||
|
|
||||||
<div class="dialogue-list">
|
<div class="dialogue-list">
|
||||||
<div v-for="(turn, index) in detail.dialogue" :key="`${turn.role}-${index}`" class="dialogue-item">
|
<div v-for="(turn, index) in detail.dialogue" :key="`${turn.role}-${index}`" class="dialogue-item">
|
||||||
|
<div class="dialogue-head">
|
||||||
<strong>{{ turn.label }}</strong>
|
<strong>{{ turn.label }}</strong>
|
||||||
|
<el-tag v-if="hasAudio(turn)" size="small" type="info">有录音</el-tag>
|
||||||
|
</div>
|
||||||
<p>{{ turn.text }}</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -105,9 +115,10 @@
|
|||||||
<script setup name="TrainReviews" lang="ts">
|
<script setup name="TrainReviews" lang="ts">
|
||||||
import { Refresh } from '@element-plus/icons-vue';
|
import { Refresh } from '@element-plus/icons-vue';
|
||||||
import { ElMessage } from 'element-plus';
|
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 { listPracticeScenarios, type PracticeScenarioResponse } from '@/api/aihr/practice';
|
||||||
import {
|
import {
|
||||||
|
getPracticeAudioBlobUrl,
|
||||||
getOrgSnapshot,
|
getOrgSnapshot,
|
||||||
getPracticeReview,
|
getPracticeReview,
|
||||||
listPracticeReviews,
|
listPracticeReviews,
|
||||||
@@ -125,6 +136,9 @@ const scenarios = ref<PracticeScenarioResponse[]>([]);
|
|||||||
const orgSnapshot = ref<OrgSnapshotResponse | null>(null);
|
const orgSnapshot = ref<OrgSnapshotResponse | null>(null);
|
||||||
const selectedProjectCode = ref('');
|
const selectedProjectCode = ref('');
|
||||||
const selectedExtPartyId = 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({
|
const reviewForm = reactive({
|
||||||
reviewer: '主管',
|
reviewer: '主管',
|
||||||
@@ -136,6 +150,33 @@ const reviewForm = reactive({
|
|||||||
|
|
||||||
const orgPeople = computed(() => orgSnapshot.value?.rows || []);
|
const orgPeople = computed(() => orgSnapshot.value?.rows || []);
|
||||||
const visibleReviews = computed(() => reviews.value);
|
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 () => {
|
const loadScenarios = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -178,6 +219,7 @@ const loadReviews = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const selectReview = async (row: ReviewRecord) => {
|
const selectReview = async (row: ReviewRecord) => {
|
||||||
|
releaseAudioSources();
|
||||||
const result = await getPracticeReview(row.id);
|
const result = await getPracticeReview(row.id);
|
||||||
detail.value = result.data;
|
detail.value = result.data;
|
||||||
reviewForm.advice = result.data.reviewAdvice || '';
|
reviewForm.advice = result.data.reviewAdvice || '';
|
||||||
@@ -190,6 +232,7 @@ const selectFirstVisibleReview = async () => {
|
|||||||
if (visibleReviews.value.some((item) => item.id === detail.value?.id)) return;
|
if (visibleReviews.value.some((item) => item.id === detail.value?.id)) return;
|
||||||
const first = visibleReviews.value[0];
|
const first = visibleReviews.value[0];
|
||||||
if (!first) {
|
if (!first) {
|
||||||
|
releaseAudioSources();
|
||||||
detail.value = null;
|
detail.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -208,6 +251,7 @@ const submitReview = async () => {
|
|||||||
nextReason: reviewForm.nextReason
|
nextReason: reviewForm.nextReason
|
||||||
});
|
});
|
||||||
ElMessage.success('已完成复盘处理');
|
ElMessage.success('已完成复盘处理');
|
||||||
|
releaseAudioSources();
|
||||||
detail.value = null;
|
detail.value = null;
|
||||||
await loadReviews();
|
await loadReviews();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -232,6 +276,8 @@ onMounted(() => {
|
|||||||
loadOrgSnapshot();
|
loadOrgSnapshot();
|
||||||
loadReviews();
|
loadReviews();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(releaseAudioSources);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<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 {
|
.annotation-list {
|
||||||
margin-top: 18px;
|
margin-top: 18px;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user