- 入驻申请页/大神工作台:点击开始录音后,按钮显示已录秒数,状态栏显示剩余秒数(每秒刷新) - 录音中按钮转为红橙渐变并呼吸闪烁,状态文字高亮;离开页面/出错自动清理计时器 - 60 秒到点自动 stop 录音,避免超长被服务端拒绝 - voice-intro-consistency 增补读秒护栏(107 套件/710 用例全绿)
1203 行
31 KiB
Vue
1203 行
31 KiB
Vue
<template>
|
||
<view class="expert-apply-page" :class="{ 'theme-warm': currentThemeKey === 'warm' }">
|
||
<!-- 1. 状态一:已通过实名认证 (APPROVED) -->
|
||
<view class="status-card approved-card" v-if="certStatus === CERT_STATUS.APPROVED">
|
||
<view class="status-icon-wrap bg-green">
|
||
<image class="status-icon-svg" :src="ICONS.shield" mode="aspectFit" />
|
||
</view>
|
||
<text class="status-title">已通过大神实名认证</text>
|
||
<text class="status-desc">您已具备平台专属接单特权,可前往工作台开启接单服务与收益结算。</text>
|
||
|
||
<view class="status-info-box">
|
||
<view class="info-row">
|
||
<text class="info-k">认证身份</text>
|
||
<text class="info-v highlight">平台认证金牌陪伴师</text>
|
||
</view>
|
||
<view class="info-row">
|
||
<text class="info-k">权限状态</text>
|
||
<text class="info-v green">接单功能已就绪</text>
|
||
</view>
|
||
</view>
|
||
|
||
<button class="btn-action btn-primary btn-active" @click="navToWorkspace">
|
||
进入接单工作台
|
||
</button>
|
||
<button class="btn-action btn-sub btn-active" @click="navBack">
|
||
返回个人中心
|
||
</button>
|
||
</view>
|
||
|
||
<!-- 2. 状态二:认证审核中 (PENDING) -->
|
||
<view class="status-card pending-card" v-else-if="certStatus === CERT_STATUS.PENDING">
|
||
<view class="status-icon-wrap bg-amber">
|
||
<image class="status-icon-svg" :src="ICONS.clock" mode="aspectFit" />
|
||
</view>
|
||
<text class="status-title">实名认证审核中</text>
|
||
<text class="status-desc">您的入驻申请已成功提交至平台审核中心,运营团队将在 24 小时内完成人工核验。</text>
|
||
|
||
<view class="audit-timeline">
|
||
<view class="timeline-step done">
|
||
<view class="step-dot"></view>
|
||
<text class="step-txt">提交申请</text>
|
||
</view>
|
||
<view class="timeline-line active"></view>
|
||
<view class="timeline-step processing">
|
||
<view class="step-dot"></view>
|
||
<text class="step-txt">官方核验中</text>
|
||
</view>
|
||
<view class="timeline-line"></view>
|
||
<view class="timeline-step">
|
||
<view class="step-dot"></view>
|
||
<text class="step-txt">审核开通</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="status-info-box">
|
||
<view class="info-row">
|
||
<text class="info-k">审核说明</text>
|
||
<text class="info-v">核验通过后系统将自动开通接单模式</text>
|
||
</view>
|
||
<view class="info-row">
|
||
<text class="info-k">服务保障</text>
|
||
<text class="info-v">平台资金托管与 7x24h 履约安全保障</text>
|
||
</view>
|
||
</view>
|
||
|
||
<button class="btn-action btn-primary btn-active" @click="contactService">
|
||
联系官方在线客服
|
||
</button>
|
||
<button class="btn-action btn-sub btn-active" @click="navBack">
|
||
返回个人中心
|
||
</button>
|
||
</view>
|
||
|
||
<!-- 3. 状态三:未认证 / 重新申请 (NOT_APPLIED / REJECTED) -->
|
||
<view class="form-container" v-else>
|
||
<!-- 审核驳回反馈横幅 (当后台驳回时展示具体原因并引导修改重提) -->
|
||
<view class="notice-banner reject-banner" v-if="certStatus === CERT_STATUS.REJECTED">
|
||
<image class="banner-icon-svg" :src="ICONS.alert" mode="aspectFit" />
|
||
<view class="banner-content">
|
||
<text class="banner-title">上次入驻审核未通过,请修改后重新提交</text>
|
||
<text class="banner-desc">驳回原因:{{ rejectReason || '提交的资质或语音未通过人工核验,请核对后重新提交' }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 平台合规与安全保障提示横幅 -->
|
||
<view class="notice-banner">
|
||
<image class="banner-icon-svg" :src="ICONS.shield" mode="aspectFit" />
|
||
<view class="banner-content">
|
||
<text class="banner-title">实名认证与合规入驻指引</text>
|
||
<text class="banner-desc">为构建健康绿色的同城陪伴生态,申请者须提交真实有效的身份信息,平台全面落实微信内容安全与用户隐私保障。</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 入驻表单卡片 -->
|
||
<view class="apply-card">
|
||
<!-- 真实姓名 -->
|
||
<view class="form-item">
|
||
<view class="item-label-row">
|
||
<text class="item-label">真实姓名</text>
|
||
<text class="required-star">*</text>
|
||
</view>
|
||
<input
|
||
class="input-control"
|
||
v-model="applyForm.realName"
|
||
maxlength="20"
|
||
placeholder="请输入与身份证完全一致的姓名"
|
||
placeholder-class="ph-style"
|
||
/>
|
||
</view>
|
||
|
||
<!-- 身份证号码 -->
|
||
<view class="form-item">
|
||
<view class="item-label-row">
|
||
<text class="item-label">身份证号码</text>
|
||
<text class="required-star">*</text>
|
||
</view>
|
||
<input
|
||
class="input-control"
|
||
v-model="applyForm.idCard"
|
||
maxlength="18"
|
||
placeholder="请输入 18 位二代居民身份证号"
|
||
placeholder-class="ph-style"
|
||
/>
|
||
</view>
|
||
|
||
<!-- 主营陪伴品类 -->
|
||
<view class="form-item">
|
||
<view class="item-label-row">
|
||
<text class="item-label">主营陪伴品类</text>
|
||
<text class="required-star">*</text>
|
||
</view>
|
||
<view class="track-grid">
|
||
<view
|
||
v-for="trk in TRACK_OPTIONS"
|
||
:key="trk"
|
||
class="track-item btn-active"
|
||
:class="{ active: applyForm.track === trk }"
|
||
@click="applyForm.track = trk"
|
||
>
|
||
{{ trk }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 期望服务单价 -->
|
||
<view class="form-item">
|
||
<view class="item-label-row">
|
||
<text class="item-label">期望服务单价 (元/小时)</text>
|
||
<text class="required-star">*</text>
|
||
<text class="label-hint">限价 ¥{{ expertPriceMin }} - ¥{{ expertPriceMax }}</text>
|
||
</view>
|
||
<view class="price-input-wrap">
|
||
<text class="price-unit">¥</text>
|
||
<input
|
||
class="price-control"
|
||
type="number"
|
||
v-model="applyForm.hourlyRate"
|
||
:placeholder="`推荐单价 ¥${expertDefaultRate}`"
|
||
placeholder-class="ph-style"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 技能专长标签 -->
|
||
<view class="form-item">
|
||
<view class="item-label-row">
|
||
<text class="item-label">技能专长标签</text>
|
||
<text class="label-hint">(最多任选 6 项,展示在个人接单主页)</text>
|
||
</view>
|
||
<view class="tags-cloud">
|
||
<view
|
||
v-for="tag in candidateTags"
|
||
:key="tag"
|
||
class="tag-pill btn-active"
|
||
:class="{ active: applyForm.tags.includes(tag) }"
|
||
@click="toggleTag(tag)"
|
||
>
|
||
{{ tag }}
|
||
</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" :class="{ recording: voiceRecording }" :disabled="voiceUploading" @click="toggleRecord">
|
||
{{ voiceRecording ? `停止录音 ${voiceSeconds}s` : (voiceRecorded ? '重新录制' : '开始录音') }}
|
||
</button>
|
||
<button v-if="voiceRecorded && !voiceRecording" class="voice-btn btn-active" @click="playRecorded">
|
||
{{ voicePlaying ? '停止播放' : '试听' }}
|
||
</button>
|
||
<text class="voice-status" :class="{ recording: voiceRecording }">{{ voiceStatusText }}</text>
|
||
</view>
|
||
<text class="voice-tip">语音沟通在游戏内或微信进行,请勿在录音中透露联系方式。</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 协议勾选与服务承诺 -->
|
||
<view class="agreement-row" @click="agreed = !agreed">
|
||
<view class="check-box" :class="{ checked: agreed }">
|
||
<view class="check-inner" v-if="agreed"></view>
|
||
</view>
|
||
<view class="agreement-text">
|
||
<text>我已阅读并完全同意</text>
|
||
<text class="link-text" @click.stop="openAgreement">《陪伴师入驻规范与安全履约协议》</text>
|
||
<text>,保证所提交全部材料真实合法。</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 底部固定提交按钮卡片 -->
|
||
<view class="bottom-submit-bar">
|
||
<button
|
||
class="btn-submit btn-active"
|
||
:class="{ disabled: isSubmitting }"
|
||
@click="submitApply"
|
||
>
|
||
{{ isSubmitting ? '正在提交入驻材料...' : '立即提交实名认证申请' }}
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
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, 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);
|
||
const voiceSeconds = ref(0);
|
||
let voiceTempPath = '';
|
||
let recorderManager = null;
|
||
let voiceAudioCtx = null;
|
||
let voiceTimer = null;
|
||
const voiceStatusText = computed(() => {
|
||
if (voiceUploading.value) return '上传中…';
|
||
if (voiceRecording.value) return `录音中 · 剩余 ${Math.max(0, 60 - voiceSeconds.value)}s`;
|
||
if (voiceRecorded.value) return `已录制 ${voiceDuration.value} 秒(提交后自动上传)`;
|
||
return '未录制(可跳过)';
|
||
});
|
||
|
||
const stopVoiceTimer = () => {
|
||
if (voiceTimer) {
|
||
clearInterval(voiceTimer);
|
||
voiceTimer = null;
|
||
}
|
||
};
|
||
|
||
const startVoiceTimer = () => {
|
||
stopVoiceTimer();
|
||
voiceSeconds.value = 0;
|
||
voiceTimer = setInterval(() => {
|
||
voiceSeconds.value += 1;
|
||
if (voiceSeconds.value >= 60) {
|
||
stopVoiceTimer();
|
||
try { recorderManager && recorderManager.stop(); } catch (_) {}
|
||
}
|
||
}, 1000);
|
||
};
|
||
|
||
const ensureRecorder = () => {
|
||
if (recorderManager) return recorderManager;
|
||
recorderManager = uni.getRecorderManager();
|
||
recorderManager.onStop((res) => {
|
||
stopVoiceTimer();
|
||
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(() => {
|
||
stopVoiceTimer();
|
||
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;
|
||
startVoiceTimer();
|
||
};
|
||
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(() => {
|
||
stopVoiceTimer();
|
||
try { if (recorderManager && voiceRecording.value) recorderManager.stop(); } catch (_) {}
|
||
try { if (voiceAudioCtx) { voiceAudioCtx.destroy(); voiceAudioCtx = null; } } catch (_) {}
|
||
});
|
||
|
||
const TRACK_OPTIONS = [
|
||
'同城陪吃',
|
||
'KTV陪唱',
|
||
'同城陪喝',
|
||
'线上电竞',
|
||
'同城桌游',
|
||
'运动健身'
|
||
];
|
||
|
||
const platformConfigs = ref({
|
||
expert_rules: {
|
||
minHourlyRate: 30,
|
||
maxHourlyRate: 500,
|
||
defaultHourlyRate: 88,
|
||
candidateTags: [
|
||
'国服王者', '声优达人', '幽默风趣', '随叫随到', '暖心倾听',
|
||
'百星野王', '金牌探店', '量贩麦霸', '微醺搭子', '绅士礼貌', '活力开朗'
|
||
]
|
||
},
|
||
customer_service: {
|
||
hotline: '400-888-6666',
|
||
wechat: 'peiwan_vip_kefu'
|
||
}
|
||
});
|
||
|
||
const applyForm = ref({
|
||
realName: '',
|
||
idCard: '',
|
||
track: '同城陪吃',
|
||
hourlyRate: 88,
|
||
tags: ['幽默风趣', '随叫随到']
|
||
});
|
||
|
||
const expertPriceMin = computed(() => {
|
||
return Number(platformConfigs.value.expert_rules?.minHourlyRate) || 30;
|
||
});
|
||
const expertPriceMax = computed(() => {
|
||
return Number(platformConfigs.value.expert_rules?.maxHourlyRate) || 500;
|
||
});
|
||
const expertDefaultRate = computed(() => {
|
||
return Number(platformConfigs.value.expert_rules?.defaultHourlyRate) || 88;
|
||
});
|
||
const candidateTags = computed(() => {
|
||
const tags = platformConfigs.value.expert_rules?.candidateTags;
|
||
if (Array.isArray(tags) && tags.length > 0) {
|
||
return tags;
|
||
}
|
||
return [
|
||
'国服王者', '声优达人', '幽默风趣', '随叫随到', '暖心倾听',
|
||
'百星野王', '金牌探店', '量贩麦霸', '微醺搭子', '绅士礼貌', '活力开朗'
|
||
];
|
||
});
|
||
|
||
const loadInitialConfigs = async () => {
|
||
try {
|
||
const res = await getPlatformConfigs();
|
||
if (res && typeof res === 'object') {
|
||
platformConfigs.value = {
|
||
...platformConfigs.value,
|
||
...res
|
||
};
|
||
if (platformConfigs.value.expert_rules?.defaultHourlyRate) {
|
||
applyForm.value.hourlyRate = platformConfigs.value.expert_rules.defaultHourlyRate;
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.warn('[Apply] 加载系统规则降级:', err);
|
||
}
|
||
};
|
||
|
||
const toggleTag = (tag) => {
|
||
const idx = applyForm.value.tags.indexOf(tag);
|
||
if (idx > -1) {
|
||
applyForm.value.tags.splice(idx, 1);
|
||
} else {
|
||
if (applyForm.value.tags.length >= 6) {
|
||
uni.showToast({ title: '最多选择 6 个技能标签', icon: 'none' });
|
||
return;
|
||
}
|
||
applyForm.value.tags.push(tag);
|
||
}
|
||
};
|
||
|
||
const openAgreement = () => {
|
||
uni.showModal({
|
||
title: '陪伴师入驻与安全规范',
|
||
content: '1. 陪伴师在线下履约约局时,须前往合规公开商业街区场所;\n2. 碰面后必须由用户提供 4 位数字核销码进行现场签到核销;\n3. 严禁任何私下脱离平台交易、索要微信或违规低俗行为,平台保留实时追责与仲裁扣除保证金权利。',
|
||
showCancel: false,
|
||
confirmText: '我知道了'
|
||
});
|
||
};
|
||
|
||
const submitApply = async () => {
|
||
if (isSubmitting.value) return;
|
||
|
||
if (!applyForm.value.realName || !applyForm.value.realName.trim()) {
|
||
uni.showToast({ title: '请输入真实姓名', icon: 'none' });
|
||
return;
|
||
}
|
||
if (!applyForm.value.idCard || applyForm.value.idCard.trim().length < 15) {
|
||
uni.showToast({ title: '请输入有效的身份证号码', icon: 'none' });
|
||
return;
|
||
}
|
||
const rate = Number(applyForm.value.hourlyRate) || expertDefaultRate.value;
|
||
if (rate < expertPriceMin.value || rate > expertPriceMax.value) {
|
||
uni.showToast({
|
||
title: `单价需在 ¥${expertPriceMin.value} - ¥${expertPriceMax.value} 之间`,
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
if (!agreed.value) {
|
||
uni.showToast({ title: '请先勾选并同意服务协议', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
isSubmitting.value = true;
|
||
uni.showLoading({ title: '正在提交入驻材料...' });
|
||
// 身份与登录态保持一致:有 token 时由服务端按 token 识别(缺失 userId 传 0)
|
||
const uid = normalizeUserId();
|
||
const profileUid = uid || 1001;
|
||
|
||
try {
|
||
let avatarUrl = '';
|
||
let gender = '男';
|
||
try {
|
||
const userProfile = await getUserProfile(profileUid);
|
||
if (userProfile) {
|
||
avatarUrl = userProfile.avatar || '';
|
||
gender = userProfile.gender === 'female' ? '女' : '男';
|
||
}
|
||
} catch (_) {}
|
||
|
||
const applied = await applyExpertCert({
|
||
userId: uid,
|
||
name: applyForm.value.realName.trim(),
|
||
idCard: applyForm.value.idCard.trim(),
|
||
track: applyForm.value.track,
|
||
hourlyRate: rate,
|
||
tags: applyForm.value.tags,
|
||
avatar: avatarUrl,
|
||
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();
|
||
isSubmitting.value = false;
|
||
|
||
uni.showModal({
|
||
title: '入驻申请提交成功',
|
||
content: '您的入驻资料已送达官方审核中心。平台运营团队将在 24 小时内完成人工核准,核准后将自动激活接单权限。',
|
||
showCancel: false,
|
||
confirmText: '我知道了'
|
||
});
|
||
} catch (err) {
|
||
uni.hideLoading();
|
||
isSubmitting.value = false;
|
||
uni.showToast({
|
||
title: err.message || '提交失败,请检查输入或稍后重试',
|
||
icon: 'none',
|
||
duration: 2500
|
||
});
|
||
}
|
||
};
|
||
|
||
const navToWorkspace = () => {
|
||
uni.navigateTo({ url: '/pages/expert/hall' });
|
||
};
|
||
|
||
const navBack = () => {
|
||
uni.navigateBack({
|
||
fail: () => {
|
||
uni.switchTab({ url: '/pages/user/index' });
|
||
}
|
||
});
|
||
};
|
||
|
||
const contactService = () => {
|
||
const cs = platformConfigs.value.customer_service || {};
|
||
const hotline = cs.hotline || '400-888-6666';
|
||
uni.showActionSheet({
|
||
itemList: [
|
||
`拨打平台客服专线 (${hotline})`,
|
||
'查看平台官方履约指引'
|
||
],
|
||
success: (res) => {
|
||
if (res.tapIndex === 0) {
|
||
uni.makePhoneCall({
|
||
phoneNumber: hotline.replace(/[^\d]/g, ''),
|
||
fail: () => {
|
||
uni.showToast({ title: `客服热线: ${hotline}`, icon: 'none' });
|
||
}
|
||
});
|
||
} else {
|
||
openAgreement();
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
const rejectReason = ref('');
|
||
|
||
const syncRemoteCertStatus = async () => {
|
||
try {
|
||
const cachedUser = uni.getStorageSync('PW_USER_INFO') || {};
|
||
const uid = Number(cachedUser.id) || 1001;
|
||
const profile = await getUserProfile(uid);
|
||
if (profile && profile.certStatus !== undefined) {
|
||
const remoteCert = Number(profile.certStatus);
|
||
setExpertCertStatus(remoteCert);
|
||
certStatus.value = remoteCert;
|
||
rejectReason.value = profile.rejectReason || '';
|
||
uni.setStorageSync('PW_USER_INFO', {
|
||
...cachedUser,
|
||
...profile
|
||
});
|
||
}
|
||
} catch (e) {
|
||
// 保持本地缓存兜底
|
||
}
|
||
};
|
||
|
||
onMounted(() => {
|
||
currentThemeKey.value = getCurrentThemeKey();
|
||
applyThemeToNativeUI(currentThemeKey.value);
|
||
certStatus.value = getExpertCertStatus();
|
||
loadInitialConfigs();
|
||
syncRemoteCertStatus();
|
||
});
|
||
|
||
onShow(() => {
|
||
currentThemeKey.value = getCurrentThemeKey();
|
||
applyThemeToNativeUI(currentThemeKey.value);
|
||
certStatus.value = getExpertCertStatus();
|
||
syncRemoteCertStatus();
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss">
|
||
.expert-apply-page {
|
||
min-height: 100vh;
|
||
background-color: #0A0F1D;
|
||
box-sizing: border-box;
|
||
padding: 28rpx 32rpx calc(140rpx + env(safe-area-inset-bottom));
|
||
}
|
||
|
||
/* 顶部安全提示横幅 */
|
||
.notice-banner {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 16rpx;
|
||
background: rgba(99, 102, 241, 0.1);
|
||
border: 1px solid rgba(99, 102, 241, 0.25);
|
||
border-radius: 20rpx;
|
||
padding: 24rpx;
|
||
margin-bottom: 28rpx;
|
||
|
||
&.reject-banner {
|
||
background: rgba(244, 63, 94, 0.12);
|
||
border-color: rgba(244, 63, 94, 0.35);
|
||
|
||
.banner-title {
|
||
color: #FDA4AF;
|
||
}
|
||
|
||
.banner-desc {
|
||
color: #FECDD3;
|
||
}
|
||
}
|
||
|
||
.banner-icon-svg {
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
flex-shrink: 0;
|
||
margin-top: 4rpx;
|
||
}
|
||
|
||
.banner-content {
|
||
flex: 1;
|
||
|
||
.banner-title {
|
||
font-size: 26rpx;
|
||
font-weight: 700;
|
||
color: #818CF8;
|
||
display: block;
|
||
margin-bottom: 6rpx;
|
||
}
|
||
|
||
.banner-desc {
|
||
font-size: 22rpx;
|
||
color: #94A3B8;
|
||
line-height: 1.5;
|
||
display: block;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 语音自我介绍(可选) */
|
||
.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-btn.recording {
|
||
background: linear-gradient(135deg, #EF4444 0%, #F97316 100%);
|
||
animation: voice-pulse 1.2s ease-in-out infinite;
|
||
}
|
||
@keyframes voice-pulse {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0.72; }
|
||
}
|
||
.voice-status {
|
||
font-size: 22rpx;
|
||
color: var(--text-muted, #94A3B8);
|
||
}
|
||
.voice-status.recording {
|
||
color: #F97316;
|
||
font-weight: 600;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.voice-tip {
|
||
display: block;
|
||
margin-top: 12rpx;
|
||
font-size: 22rpx;
|
||
color: var(--text-muted, #64748B);
|
||
line-height: 1.6;
|
||
}
|
||
|
||
/* 主表单卡片 */
|
||
.apply-card {
|
||
background: #151D30;
|
||
border-radius: 24rpx;
|
||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||
padding: 32rpx;
|
||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.35);
|
||
|
||
.form-item {
|
||
margin-bottom: 32rpx;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.item-label-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
margin-bottom: 16rpx;
|
||
|
||
.item-label {
|
||
font-size: 26rpx;
|
||
font-weight: 600;
|
||
color: #F8FAFC;
|
||
}
|
||
|
||
.required-star {
|
||
font-size: 26rpx;
|
||
color: #EF4444;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.label-hint {
|
||
font-size: 22rpx;
|
||
color: #64748B;
|
||
margin-left: 8rpx;
|
||
}
|
||
}
|
||
|
||
.input-control {
|
||
width: 100%;
|
||
height: 84rpx;
|
||
box-sizing: border-box;
|
||
background: rgba(255, 255, 255, 0.04);
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
border-radius: 16rpx;
|
||
padding: 0 24rpx;
|
||
font-size: 28rpx;
|
||
color: #F8FAFC;
|
||
}
|
||
|
||
.price-input-wrap {
|
||
display: flex;
|
||
align-items: center;
|
||
background: rgba(255, 255, 255, 0.04);
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
border-radius: 16rpx;
|
||
padding: 0 24rpx;
|
||
height: 84rpx;
|
||
|
||
.price-unit {
|
||
font-size: 32rpx;
|
||
font-weight: 700;
|
||
color: #10B981;
|
||
margin-right: 12rpx;
|
||
}
|
||
|
||
.price-control {
|
||
flex: 1;
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #10B981;
|
||
}
|
||
}
|
||
|
||
.ph-style {
|
||
color: #64748B;
|
||
font-size: 26rpx;
|
||
}
|
||
|
||
.track-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 16rpx;
|
||
|
||
.track-item {
|
||
height: 72rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(255, 255, 255, 0.04);
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
border-radius: 16rpx;
|
||
font-size: 24rpx;
|
||
color: #CBD5E1;
|
||
font-weight: 500;
|
||
transition: all 0.2s ease;
|
||
|
||
&.active {
|
||
background: rgba(99, 102, 241, 0.18);
|
||
border-color: #6366F1;
|
||
color: #818CF8;
|
||
font-weight: 700;
|
||
}
|
||
}
|
||
}
|
||
|
||
.tags-cloud {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 14rpx;
|
||
|
||
.tag-pill {
|
||
padding: 10rpx 22rpx;
|
||
border-radius: 28rpx;
|
||
background: rgba(255, 255, 255, 0.04);
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
font-size: 24rpx;
|
||
color: #94A3B8;
|
||
transition: all 0.2s ease;
|
||
|
||
&.active {
|
||
background: rgba(16, 185, 129, 0.18);
|
||
border-color: #10B981;
|
||
color: #10B981;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 协议行 */
|
||
.agreement-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14rpx;
|
||
margin-top: 24rpx;
|
||
padding: 0 8rpx;
|
||
|
||
.check-box {
|
||
width: 36rpx;
|
||
height: 36rpx;
|
||
border-radius: 8rpx;
|
||
border: 2px solid #64748B;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
|
||
&.checked {
|
||
background: #6366F1;
|
||
border-color: #6366F1;
|
||
|
||
.check-inner {
|
||
width: 14rpx;
|
||
height: 14rpx;
|
||
background: #FFFFFF;
|
||
border-radius: 4rpx;
|
||
}
|
||
}
|
||
}
|
||
|
||
.agreement-text {
|
||
font-size: 22rpx;
|
||
color: #94A3B8;
|
||
line-height: 1.5;
|
||
|
||
.link-text {
|
||
color: #818CF8;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 底部提交悬浮条 */
|
||
.bottom-submit-bar {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom));
|
||
background: rgba(21, 29, 48, 0.95);
|
||
backdrop-filter: blur(12px);
|
||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||
max-width: 450px;
|
||
margin: 0 auto;
|
||
box-sizing: border-box;
|
||
z-index: 100;
|
||
|
||
.btn-submit {
|
||
height: 88rpx;
|
||
line-height: 88rpx;
|
||
border-radius: 44rpx;
|
||
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
|
||
color: #FFFFFF;
|
||
font-size: 30rpx;
|
||
font-weight: 700;
|
||
border: none;
|
||
box-shadow: 0 6rpx 20rpx rgba(99, 102, 241, 0.4);
|
||
|
||
&.disabled {
|
||
opacity: 0.6;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 状态结果卡片 (已认证/审核中) */
|
||
.status-card {
|
||
background: #151D30;
|
||
border-radius: 28rpx;
|
||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||
padding: 48rpx 36rpx;
|
||
text-align: center;
|
||
box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.4);
|
||
margin-top: 24rpx;
|
||
|
||
.status-icon-wrap {
|
||
width: 120rpx;
|
||
height: 120rpx;
|
||
border-radius: 60rpx;
|
||
margin: 0 auto 28rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
&.bg-green {
|
||
background: rgba(16, 185, 129, 0.15);
|
||
border: 2px solid rgba(16, 185, 129, 0.35);
|
||
}
|
||
&.bg-amber {
|
||
background: rgba(245, 158, 11, 0.15);
|
||
border: 2px solid rgba(245, 158, 11, 0.35);
|
||
}
|
||
|
||
.status-icon-svg {
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
}
|
||
}
|
||
|
||
.status-title {
|
||
font-size: 34rpx;
|
||
font-weight: 700;
|
||
color: #F8FAFC;
|
||
display: block;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.status-desc {
|
||
font-size: 24rpx;
|
||
color: #94A3B8;
|
||
line-height: 1.6;
|
||
display: block;
|
||
margin-bottom: 36rpx;
|
||
}
|
||
|
||
.audit-timeline {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: 40rpx;
|
||
padding: 0 20rpx;
|
||
|
||
.timeline-step {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
|
||
.step-dot {
|
||
width: 18rpx;
|
||
height: 18rpx;
|
||
border-radius: 50%;
|
||
background: #475569;
|
||
}
|
||
|
||
.step-txt {
|
||
font-size: 20rpx;
|
||
color: #64748B;
|
||
}
|
||
|
||
&.done {
|
||
.step-dot {
|
||
background: #10B981;
|
||
}
|
||
.step-txt {
|
||
color: #10B981;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
&.processing {
|
||
.step-dot {
|
||
background: #F59E0B;
|
||
box-shadow: 0 0 10rpx #F59E0B;
|
||
}
|
||
.step-txt {
|
||
color: #F59E0B;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
}
|
||
|
||
.timeline-line {
|
||
flex: 1;
|
||
height: 4rpx;
|
||
background: #334155;
|
||
margin: 0 12rpx 28rpx;
|
||
|
||
&.active {
|
||
background: #F59E0B;
|
||
}
|
||
}
|
||
}
|
||
|
||
.status-info-box {
|
||
background: rgba(255, 255, 255, 0.03);
|
||
border-radius: 18rpx;
|
||
padding: 24rpx;
|
||
margin-bottom: 40rpx;
|
||
|
||
.info-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font-size: 24rpx;
|
||
margin-bottom: 12rpx;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.info-k {
|
||
color: #94A3B8;
|
||
}
|
||
|
||
.info-v {
|
||
color: #F8FAFC;
|
||
font-weight: 500;
|
||
|
||
&.highlight {
|
||
color: #F59E0B;
|
||
font-weight: 600;
|
||
}
|
||
&.green {
|
||
color: #10B981;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.btn-action {
|
||
height: 84rpx;
|
||
line-height: 84rpx;
|
||
border-radius: 42rpx;
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
margin-bottom: 20rpx;
|
||
|
||
&.btn-primary {
|
||
background: linear-gradient(135deg, #10B981 0%, #059669 100%);
|
||
color: #FFFFFF;
|
||
box-shadow: 0 4rpx 16rpx rgba(16, 185, 129, 0.35);
|
||
border: none;
|
||
}
|
||
|
||
&.btn-sub {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
color: #94A3B8;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 浅色暖阳晨曦模式适配 */
|
||
.theme-warm {
|
||
background-color: #FAF8F5;
|
||
|
||
.notice-banner {
|
||
background: #FFF7ED;
|
||
border-color: #FED7AA;
|
||
|
||
.banner-title {
|
||
color: #EA580C;
|
||
}
|
||
.banner-desc {
|
||
color: #78716C;
|
||
}
|
||
}
|
||
|
||
.apply-card {
|
||
background: #FFFFFF;
|
||
border-color: #E7E5E4;
|
||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||
|
||
.form-item {
|
||
.item-label-row {
|
||
.item-label {
|
||
color: #1C1917;
|
||
}
|
||
.label-hint {
|
||
color: #78716C;
|
||
}
|
||
}
|
||
|
||
.input-control,
|
||
.price-input-wrap {
|
||
background: #FDFBF7;
|
||
border-color: #E7E5E4;
|
||
color: #1C1917;
|
||
}
|
||
|
||
.ph-style {
|
||
color: #A8A29E;
|
||
}
|
||
|
||
.price-input-wrap .price-unit,
|
||
.price-input-wrap .price-control {
|
||
color: #EA580C;
|
||
}
|
||
|
||
.track-grid .track-item {
|
||
background: #FDFBF7;
|
||
border-color: #E7E5E4;
|
||
color: #44403C;
|
||
|
||
&.active {
|
||
background: #FFF7ED;
|
||
border-color: #EA580C;
|
||
color: #EA580C;
|
||
}
|
||
}
|
||
|
||
.tags-cloud .tag-pill {
|
||
background: #FDFBF7;
|
||
border-color: #E7E5E4;
|
||
color: #78716C;
|
||
|
||
&.active {
|
||
background: #FFF7ED;
|
||
border-color: #EA580C;
|
||
color: #EA580C;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.agreement-row {
|
||
.check-box {
|
||
border-color: #D6D3D1;
|
||
|
||
&.checked {
|
||
background: #EA580C;
|
||
border-color: #EA580C;
|
||
}
|
||
}
|
||
|
||
.agreement-text {
|
||
color: #78716C;
|
||
|
||
.link-text {
|
||
color: #EA580C;
|
||
}
|
||
}
|
||
}
|
||
|
||
.bottom-submit-bar {
|
||
background: rgba(255, 255, 255, 0.96);
|
||
border-top-color: #E7E5E4;
|
||
|
||
.btn-submit {
|
||
background: linear-gradient(135deg, #EA580C 0%, #C2410C 100%);
|
||
box-shadow: 0 6rpx 20rpx rgba(234, 88, 12, 0.35);
|
||
}
|
||
}
|
||
|
||
.status-card {
|
||
background: #FFFFFF;
|
||
border-color: #E7E5E4;
|
||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
|
||
|
||
.status-title {
|
||
color: #1C1917;
|
||
}
|
||
.status-desc {
|
||
color: #78716C;
|
||
}
|
||
|
||
.status-info-box {
|
||
background: #FDFBF7;
|
||
|
||
.info-row {
|
||
.info-k {
|
||
color: #78716C;
|
||
}
|
||
.info-v {
|
||
color: #1C1917;
|
||
}
|
||
}
|
||
}
|
||
|
||
.btn-action.btn-sub {
|
||
background: #FDFBF7;
|
||
border-color: #E7E5E4;
|
||
color: #57534E;
|
||
}
|
||
}
|
||
}
|
||
</style>
|