feat(voice): 入驻页录音 + 后台审核真实试听

- 入驻申请页新增「语音自我介绍(可选,≤60s)」录音/试听,申请成功拿到大神档案 id 后自动上传,失败不影响入驻结果

- 管理端审核「真人语音包核听」由 3 秒假播放改为按 voiceIntroUrl 真实播放,未上传显示灰色标记

- 清理小程序审核敏感词(语音房/连麦/开麦),入驻页身份改用 normalizeUserId 与 token 保持一致

- 新增 voice-intro-consistency.spec.ts(107 套件/709 用例全绿)
这个提交包含在:
xcbyy
2026-10-21 11:12:42 +08:00
父节点 7a1e32688c
当前提交 ae62f2b805
共有 6 个文件被更改,包括 228 次插入 和 18 次删除

查看文件

@@ -64,16 +64,18 @@
<template #cell="{ record }">
<a-space>
<a-button
v-if="record.voiceIntroUrl"
size="mini"
type="outline"
shape="round"
@click="playVoice(record.id)"
@click="playVoice(record)"
>
<template #icon>
<icon-sound />
</template>
{{ playingId === record.id ? '播放中 16s...' : '试听语音包' }}
{{ playingId === record.id ? '播放中…' : '试听语音包' }}
</a-button>
<a-tag v-else color="gray">未上传</a-tag>
</a-space>
</template>
</a-table-column>
@@ -161,7 +163,7 @@
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
import { ref, computed, onMounted, onUnmounted } from 'vue';
import axios from 'axios';
import { Message } from '@arco-design/web-vue';
@@ -214,15 +216,39 @@
}
};
const playVoice = (id: number) => {
playingId.value = id;
setTimeout(() => {
if (playingId.value === id) {
playingId.value = null;
}
}, 3000);
let voiceAudio: HTMLAudioElement | null = null;
const playVoice = (record: any) => {
const url = String(record?.voiceIntroUrl || '');
if (!url) {
Message.info('该申请人未上传语音介绍');
return;
}
// 再次点击同一条 = 停止
if (playingId.value === record.id) {
try { voiceAudio?.pause(); } catch (_) {}
playingId.value = null;
return;
}
try { voiceAudio?.pause(); } catch (_) {}
voiceAudio = new Audio(url);
voiceAudio.onended = () => { playingId.value = null; };
voiceAudio.onerror = () => {
playingId.value = null;
Message.error('语音播放失败,请检查文件是否存在');
};
voiceAudio.play().catch(() => {
playingId.value = null;
Message.error('浏览器阻止了自动播放,请重试');
});
playingId.value = record.id;
};
onUnmounted(() => {
try { voiceAudio?.pause(); } catch (_) {}
voiceAudio = null;
playingId.value = null;
});
const handleAudit = async (id: number, action: 'approve' | 'reject' | 'revoke', reason?: string) => {
try {
await axios.post(`/api/companions/${id}/audit`, {