|
|
|
|
@@ -179,6 +179,24 @@
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
<!-- 语音自我介绍(可选):设计文档「在线录制试听」落地 -->
|
|
|
|
|
<view class="form-item">
|
|
|
|
|
<view class="item-label-row">
|
|
|
|
|
<text class="item-label">语音自我介绍</text>
|
|
|
|
|
<text class="label-hint">(可选,≤60 秒;审核与买家试听用)</text>
|
|
|
|
|
</view>
|
|
|
|
|
<view class="voice-row">
|
|
|
|
|
<button class="voice-btn btn-active" :disabled="voiceUploading" @click="toggleRecord">
|
|
|
|
|
{{ voiceRecording ? '停止录音' : (voiceRecorded ? '重新录制' : '开始录音') }}
|
|
|
|
|
</button>
|
|
|
|
|
<button v-if="voiceRecorded" class="voice-btn btn-active" @click="playRecorded">
|
|
|
|
|
{{ voicePlaying ? '停止播放' : '试听' }}
|
|
|
|
|
</button>
|
|
|
|
|
<text class="voice-status">{{ voiceStatusText }}</text>
|
|
|
|
|
</view>
|
|
|
|
|
<text class="voice-tip">语音沟通在游戏内或微信进行,请勿在录音中透露联系方式。</text>
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
<!-- 协议勾选与服务承诺 -->
|
|
|
|
|
@@ -208,18 +226,89 @@
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
import { ref, computed, onMounted } from 'vue';
|
|
|
|
|
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
|
|
|
|
import { onShow } from '@dcloudio/uni-app';
|
|
|
|
|
import { ICONS } from '@/common/icons.js';
|
|
|
|
|
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
|
|
|
|
|
import { getExpertCertStatus, setExpertCertStatus, CERT_STATUS } from '@/common/role.js';
|
|
|
|
|
import { applyExpertCert, getPlatformConfigs, getUserProfile } from '@/common/api.js';
|
|
|
|
|
import { applyExpertCert, getPlatformConfigs, getUserProfile, uploadVoiceIntro, normalizeUserId } from '@/common/api.js';
|
|
|
|
|
|
|
|
|
|
const currentThemeKey = ref(getCurrentThemeKey());
|
|
|
|
|
const certStatus = ref(getExpertCertStatus());
|
|
|
|
|
const isSubmitting = ref(false);
|
|
|
|
|
const agreed = ref(true);
|
|
|
|
|
|
|
|
|
|
// 语音自我介绍(可选,≤60s)
|
|
|
|
|
const voiceRecording = ref(false);
|
|
|
|
|
const voiceRecorded = ref(false);
|
|
|
|
|
const voicePlaying = ref(false);
|
|
|
|
|
const voiceUploading = ref(false);
|
|
|
|
|
const voiceDuration = ref(0);
|
|
|
|
|
let voiceTempPath = '';
|
|
|
|
|
let recorderManager = null;
|
|
|
|
|
let voiceAudioCtx = null;
|
|
|
|
|
const voiceStatusText = computed(() => {
|
|
|
|
|
if (voiceUploading.value) return '上传中…';
|
|
|
|
|
if (voiceRecorded.value) return `已录制 ${voiceDuration.value} 秒(提交后自动上传)`;
|
|
|
|
|
return '未录制(可跳过)';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const ensureRecorder = () => {
|
|
|
|
|
if (recorderManager) return recorderManager;
|
|
|
|
|
recorderManager = uni.getRecorderManager();
|
|
|
|
|
recorderManager.onStop((res) => {
|
|
|
|
|
voiceRecording.value = false;
|
|
|
|
|
voiceTempPath = res.tempFilePath || '';
|
|
|
|
|
voiceDuration.value = Math.max(1, Math.round(Number(res.duration || 0) / 1000) || 1);
|
|
|
|
|
voiceRecorded.value = Boolean(voiceTempPath);
|
|
|
|
|
});
|
|
|
|
|
recorderManager.onError(() => {
|
|
|
|
|
voiceRecording.value = false;
|
|
|
|
|
uni.showToast({ title: '录音失败,请检查麦克风权限', icon: 'none' });
|
|
|
|
|
});
|
|
|
|
|
return recorderManager;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const toggleRecord = () => {
|
|
|
|
|
const rm = ensureRecorder();
|
|
|
|
|
if (voiceRecording.value) {
|
|
|
|
|
rm.stop();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const start = () => {
|
|
|
|
|
rm.start({ duration: 60000, format: 'mp3', sampleRate: 16000, numberOfChannels: 1, encodeBitRate: 48000 });
|
|
|
|
|
voiceRecording.value = true;
|
|
|
|
|
uni.showToast({ title: '开始录音,最长 60 秒', icon: 'none' });
|
|
|
|
|
};
|
|
|
|
|
uni.authorize({
|
|
|
|
|
scope: 'scope.record',
|
|
|
|
|
success: start,
|
|
|
|
|
fail: () => uni.showToast({ title: '请先允许使用麦克风', icon: 'none' })
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const playRecorded = () => {
|
|
|
|
|
if (!voiceTempPath) return;
|
|
|
|
|
if (voicePlaying.value) {
|
|
|
|
|
try { voiceAudioCtx && voiceAudioCtx.stop(); } catch (_) {}
|
|
|
|
|
voicePlaying.value = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!voiceAudioCtx) {
|
|
|
|
|
voiceAudioCtx = uni.createInnerAudioContext();
|
|
|
|
|
voiceAudioCtx.onEnded(() => { voicePlaying.value = false; });
|
|
|
|
|
voiceAudioCtx.onError(() => { voicePlaying.value = false; });
|
|
|
|
|
}
|
|
|
|
|
voiceAudioCtx.src = voiceTempPath;
|
|
|
|
|
voiceAudioCtx.play();
|
|
|
|
|
voicePlaying.value = true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
try { if (recorderManager && voiceRecording.value) recorderManager.stop(); } catch (_) {}
|
|
|
|
|
try { if (voiceAudioCtx) { voiceAudioCtx.destroy(); voiceAudioCtx = null; } } catch (_) {}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const TRACK_OPTIONS = [
|
|
|
|
|
'同城陪吃',
|
|
|
|
|
'KTV陪唱',
|
|
|
|
|
@@ -338,20 +427,22 @@ const submitApply = async () => {
|
|
|
|
|
|
|
|
|
|
isSubmitting.value = true;
|
|
|
|
|
uni.showLoading({ title: '正在提交入驻材料...' });
|
|
|
|
|
const uid = Number(uni.getStorageSync('userId')) || 1001;
|
|
|
|
|
// 身份与登录态保持一致:有 token 时由服务端按 token 识别(缺失 userId 传 0)
|
|
|
|
|
const uid = normalizeUserId();
|
|
|
|
|
const profileUid = uid || 1001;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
let avatarUrl = '';
|
|
|
|
|
let gender = '男';
|
|
|
|
|
try {
|
|
|
|
|
const userProfile = await getUserProfile(uid);
|
|
|
|
|
const userProfile = await getUserProfile(profileUid);
|
|
|
|
|
if (userProfile) {
|
|
|
|
|
avatarUrl = userProfile.avatar || '';
|
|
|
|
|
gender = userProfile.gender === 'female' ? '女' : '男';
|
|
|
|
|
}
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
|
|
|
|
await applyExpertCert({
|
|
|
|
|
const applied = await applyExpertCert({
|
|
|
|
|
userId: uid,
|
|
|
|
|
name: applyForm.value.realName.trim(),
|
|
|
|
|
idCard: applyForm.value.idCard.trim(),
|
|
|
|
|
@@ -362,6 +453,21 @@ const submitApply = async () => {
|
|
|
|
|
gender: gender
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 已录制的语音介绍,在申请成功(拿到大神档案 id)后自动上传;失败不影响申请结果
|
|
|
|
|
if (voiceTempPath && applied && applied.id) {
|
|
|
|
|
voiceUploading.value = true;
|
|
|
|
|
try {
|
|
|
|
|
await uploadVoiceIntro(applied.id, voiceTempPath, voiceDuration.value);
|
|
|
|
|
voiceRecorded.value = false;
|
|
|
|
|
voiceTempPath = '';
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('[Apply] 语音介绍上传失败:', e);
|
|
|
|
|
uni.showToast({ title: '入驻已提交,语音上传失败可在工作台重录', icon: 'none', duration: 2600 });
|
|
|
|
|
} finally {
|
|
|
|
|
voiceUploading.value = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setExpertCertStatus(CERT_STATUS.PENDING);
|
|
|
|
|
certStatus.value = CERT_STATUS.PENDING;
|
|
|
|
|
uni.hideLoading();
|
|
|
|
|
@@ -516,6 +622,35 @@ onShow(() => {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 语音自我介绍(可选) */
|
|
|
|
|
.voice-row {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 16rpx;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
.voice-btn {
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 0 28rpx;
|
|
|
|
|
height: 64rpx;
|
|
|
|
|
line-height: 64rpx;
|
|
|
|
|
border-radius: 32rpx;
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
color: #FFFFFF;
|
|
|
|
|
background: linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%);
|
|
|
|
|
}
|
|
|
|
|
.voice-status {
|
|
|
|
|
font-size: 22rpx;
|
|
|
|
|
color: var(--text-muted, #94A3B8);
|
|
|
|
|
}
|
|
|
|
|
.voice-tip {
|
|
|
|
|
display: block;
|
|
|
|
|
margin-top: 12rpx;
|
|
|
|
|
font-size: 22rpx;
|
|
|
|
|
color: var(--text-muted, #64748B);
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 主表单卡片 */
|
|
|
|
|
.apply-card {
|
|
|
|
|
background: #151D30;
|
|
|
|
|
|