feat(expert): create dedicated expert apply page and navigate from user center

这个提交包含在:
xcbyy
2026-10-10 16:57:48 +08:00
父节点 37b52c1efe
当前提交 d7460f898d
共有 4 个文件被更改,包括 1037 次插入 和 242 次删除

查看文件

@@ -56,6 +56,15 @@
"backgroundColor": "#0A0F1D"
}
},
{
"path": "pages/expert/apply",
"style": {
"navigationBarTitleText": "大神入驻认证",
"navigationBarBackgroundColor": "#0A0F1D",
"navigationBarTextStyle": "white",
"backgroundColor": "#0A0F1D"
}
},
{
"path": "pages/order/create",
"style": {

查看文件

@@ -0,0 +1,983 @@
<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">
<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>
<!-- 协议勾选与服务承诺 -->
<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 } 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';
const currentThemeKey = ref(getCurrentThemeKey());
const certStatus = ref(getExpertCertStatus());
const isSubmitting = ref(false);
const agreed = ref(true);
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: '正在提交入驻材料...' });
const uid = Number(uni.getStorageSync('userId')) || 1001;
try {
let avatarUrl = '';
let gender = '男';
try {
const userProfile = await getUserProfile(uid);
if (userProfile) {
avatarUrl = userProfile.avatar || '';
gender = userProfile.gender === 'female' ? '女' : '男';
}
} catch (_) {}
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
});
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();
}
}
});
};
onMounted(() => {
currentThemeKey.value = getCurrentThemeKey();
applyThemeToNativeUI(currentThemeKey.value);
certStatus.value = getExpertCertStatus();
loadInitialConfigs();
});
onShow(() => {
currentThemeKey.value = getCurrentThemeKey();
applyThemeToNativeUI(currentThemeKey.value);
certStatus.value = getExpertCertStatus();
});
</script>
<style lang="scss" scoped>
.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;
.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;
}
}
}
/* 主表单卡片 */
.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>

查看文件

@@ -261,74 +261,6 @@
</view>
</view>
<!-- 大神实名认证与资质入驻申请弹窗 (任务四:真实资料与后台审核) -->
<view class="profile-edit-modal-mask" v-if="showExpertCertModal" @click="showExpertCertModal = false">
<view class="profile-edit-modal expert-cert-modal" @click.stop>
<view class="modal-header">
<text class="modal-title">陪伴师 / 大神入驻实名申请</text>
<view class="btn-close btn-active" @click="showExpertCertModal = false">
<image class="close-svg" :src="ICONS.close" mode="aspectFit" />
</view>
</view>
<view class="modal-body">
<view class="cert-alert">
<text class="cert-alert-text">提交真实有效资料,平台将在 24 小时内由运营团队人工核验后开通接单工作台。</text>
</view>
<view class="edit-field">
<text class="field-label">真实姓名</text>
<input class="field-input" v-model="expertCertForm.realName" placeholder="如:张三 (须与身份证一致)" placeholder-class="field-placeholder" />
</view>
<view class="edit-field">
<text class="field-label">身份证号码</text>
<input class="field-input" v-model="expertCertForm.idCard" maxlength="18" placeholder="18位二代身份证号" placeholder-class="field-placeholder" />
</view>
<view class="edit-field">
<text class="field-label">主营陪伴品类</text>
<view class="track-chips">
<view
v-for="trk in ['同城陪吃', 'KTV陪唱', '同城陪喝', '线上电竞', '同城桌游']"
:key="trk"
class="track-chip btn-active"
:class="{ active: expertCertForm.track === trk }"
@click="expertCertForm.track = trk"
>
{{ trk }}
</view>
</view>
</view>
<view class="edit-field">
<text class="field-label">期望服务单价 (元/小时,平台限价: ¥{{ expertPriceMin }} - ¥{{ expertPriceMax }})</text>
<input class="field-input price-num-input" type="number" v-model="expertCertForm.hourlyRate" :placeholder="`推荐单价 ¥${expertDefaultRate}`" placeholder-class="field-placeholder" />
</view>
<view class="edit-field" v-if="expertCandidateTags.length > 0">
<text class="field-label">技能专长标签 (最多选6项)</text>
<view class="tags-chips">
<view
v-for="tg in expertCandidateTags"
:key="tg"
class="tag-pill btn-active"
:class="{ active: expertCertForm.tags.includes(tg) }"
@click="toggleCertTag(tg)"
>
{{ tg }}
</view>
</view>
</view>
</view>
<view class="modal-footer">
<button class="btn-modal btn-cancel btn-active" @click="showExpertCertModal = false">取消</button>
<button class="btn-modal btn-save btn-active" @click="submitExpertCertForm">提交审核</button>
</view>
</view>
</view>
<!-- 底部自定义导航栏 (动态自适应发单人4项 / 接单人3项) -->
<AppTabBar current="user" />
@@ -346,7 +278,7 @@ import { ICONS } from '@/common/icons.js';
import { getCurrentThemeKey, setTheme, THEME_KEYS, applyThemeToNativeUI } from '@/common/theme.js';
import { getCurrentRole, setCurrentRole, ROLE_TYPES, toggleRole as toggleUserRole, isExpertCertified, getExpertCertStatus, setExpertCertStatus, CERT_STATUS } from '@/common/role.js';
import { getNavBarInfo } from '@/common/nav.js';
import { getUserProfile, rechargeBalance, withdrawCommission, getUserFinanceRecords, updateUserProfile, uploadImage, getRechargeTiers, getPlatformConfigs } from '@/common/api.js';
import { getUserProfile, rechargeBalance, withdrawCommission, getUserFinanceRecords, updateUserProfile, uploadImage, getRechargeTiers, getPlatformConfigs, applyExpertCert } from '@/common/api.js';
const DEFAULT_GUEST_AVATAR = 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200';
const isLoggedIn = ref(true);
@@ -895,115 +827,14 @@ const showInvitePoster = () => {
});
};
// 大神实名入驻认证已全面升级重构为独立专享页面 /pages/expert/apply
const showExpertCertModal = ref(false);
const expertCertForm = ref({
realName: '',
idCard: '',
track: '同城陪吃',
hourlyRate: 88,
tags: []
});
const expertPriceMin = computed(() => {
return platformConfigs.value.expert_rules?.minHourlyRate || 30;
});
const expertPriceMax = computed(() => {
return platformConfigs.value.expert_rules?.maxHourlyRate || 500;
});
const expertDefaultRate = computed(() => {
return platformConfigs.value.expert_rules?.defaultHourlyRate || 88;
});
const expertCandidateTags = computed(() => {
return platformConfigs.value.expert_rules?.candidateTags || [
'国服王者', '声优达人', '幽默风趣', '随叫随到', '暖心倾听',
'百星野王', '金牌探店', '量贩麦霸', '微醺搭子'
];
});
const toggleCertTag = (tag) => {
const idx = expertCertForm.value.tags.indexOf(tag);
if (idx > -1) {
expertCertForm.value.tags.splice(idx, 1);
} else {
if (expertCertForm.value.tags.length >= 6) {
uni.showToast({ title: '最多选择6个技能标签', icon: 'none' });
return;
}
expertCertForm.value.tags.push(tag);
}
};
const submitExpertCertForm = async () => {
if (!expertCertForm.value.realName || !expertCertForm.value.realName.trim()) {
uni.showToast({ title: '请输入真实姓名', icon: 'none' });
return;
}
if (!expertCertForm.value.idCard || expertCertForm.value.idCard.length < 15) {
uni.showToast({ title: '请输入有效身份证号', icon: 'none' });
return;
}
const rate = Number(expertCertForm.value.hourlyRate) || expertDefaultRate.value;
if (rate < expertPriceMin.value || rate > expertPriceMax.value) {
uni.showToast({ title: `单价需在 ¥${expertPriceMin.value} - ¥${expertPriceMax.value} 之间`, icon: 'none' });
return;
}
uni.showLoading({ title: '正在提交认证...' });
try {
await applyExpertCert({
userId: user.value.id || 1001,
name: expertCertForm.value.realName.trim(),
idCard: expertCertForm.value.idCard.trim(),
track: expertCertForm.value.track,
hourlyRate: rate,
tags: expertCertForm.value.tags,
avatar: user.value.avatar,
gender: user.value.gender === 'male' ? '男' : '女'
});
setExpertCertStatus(CERT_STATUS.PENDING);
showExpertCertModal.value = false;
uni.hideLoading();
uni.showModal({
title: '申请已提交',
content: '您的入驻申请已成功提交至平台审核中心,运营团队核准后即可自动开通接单工作台。',
showCancel: false,
confirmText: '我知道了'
});
} catch (e) {
uni.hideLoading();
uni.showToast({ title: e.message || '提交申请失败', icon: 'none' });
}
};
const applyExpert = () => {
if (!ensureLoggedIn()) return;
const certStatus = getExpertCertStatus();
if (certStatus === CERT_STATUS.APPROVED) {
uni.showToast({
title: '您已通过大神实名认证,接单权限正常',
icon: 'none'
});
return;
}
if (certStatus === CERT_STATUS.PENDING) {
uni.showModal({
title: '认证审核中',
content: '您的入驻申请正在平台运营端审核中,请耐心等待核准。',
showCancel: false,
confirmText: '我知道了'
});
return;
}
expertCertForm.value = {
realName: '',
idCard: '',
track: '同城陪吃',
hourlyRate: expertDefaultRate.value,
tags: []
};
showExpertCertModal.value = true;
uni.navigateTo({
url: '/pages/expert/apply'
});
};
const contactService = () => {
@@ -1854,73 +1685,6 @@ const navTo = (url) => {
}
}
.expert-cert-modal {
max-height: 85vh;
overflow-y: auto;
.cert-alert {
background: rgba(99, 102, 241, 0.08);
border: 1px solid rgba(99, 102, 241, 0.2);
border-radius: 12rpx;
padding: 16rpx 20rpx;
margin-bottom: 24rpx;
.cert-alert-text {
font-size: 24rpx;
color: #6366F1;
line-height: 1.5;
}
}
.track-chips, .tags-chips {
display: flex;
flex-wrap: wrap;
gap: 14rpx;
margin-top: 10rpx;
.track-chip, .tag-pill {
font-size: 24rpx;
padding: 12rpx 24rpx;
border-radius: 30rpx;
background: var(--bg-card, #1E293B);
color: #94A3B8;
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.2s ease;
&.active {
background: #6366F1;
color: #FFFFFF;
border-color: #6366F1;
box-shadow: 0 4rpx 12rpx rgba(99, 102, 241, 0.35);
}
}
}
}
.theme-warm .expert-cert-modal {
.cert-alert {
background: #FFF7ED;
border-color: #FDBA74;
.cert-alert-text {
color: #EA580C;
}
}
.track-chips .track-chip, .tags-chips .tag-pill {
background: #FDFBF7;
color: #78716C;
border-color: #E7E2D8;
&.active {
background: linear-gradient(135deg, #EA580C, #F97316);
color: #FFFFFF;
border-color: #EA580C;
box-shadow: 0 4rpx 14rpx rgba(234, 88, 12, 0.35);
}
}
}
.guest-badge {
font-size: 20rpx;
color: #94A3B8;

查看文件

@@ -130,7 +130,7 @@ describe('ContentSecurityService & Compliance Architecture (TDD 微信内容安
const raw = fs.readFileSync(pagesJsonPath, 'utf-8');
const config = JSON.parse(raw);
expect(Array.isArray(config.pages)).toBe(true);
expect(config.pages.length).toBe(13);
expect(config.pages.length).toBe(14);
for (const pageItem of config.pages) {
const vueFilePath = path.join(rootDir, 'pwuniapp', `${pageItem.path}.vue`);
@@ -407,5 +407,44 @@ describe('ContentSecurityService & Compliance Architecture (TDD 微信内容安
expect(emojiRegex.test(text)).toBe(false);
});
});
describe('11. 大神实名认证专属独立页面与多态流程验证 (pages/expert/apply.vue)', () => {
it('pages.json 必须成功注册 pages/expert/apply 且物理文件必须存在', () => {
const applyPath = path.join(rootDir, 'pwuniapp', 'pages', 'expert', 'apply.vue');
expect(fs.existsSync(applyPath)).toBe(true);
const pagesJsonPath = path.join(rootDir, 'pwuniapp', 'pages.json');
const pagesJson = fs.readFileSync(pagesJsonPath, 'utf-8');
expect(pagesJson).toContain('pages/expert/apply');
expect(pagesJson).toContain('大神入驻认证');
});
it('大神入驻申请独立页面必须包含完整表单、协议声明与提交接口', () => {
const applyPath = path.join(rootDir, 'pwuniapp', 'pages', 'expert', 'apply.vue');
const content = fs.readFileSync(applyPath, 'utf-8');
expect(content).toContain('applyForm');
expect(content).toContain('applyExpertCert');
expect(content).toContain('TRACK_OPTIONS');
expect(content).toContain('expertPriceMin');
expect(content).toContain('expertPriceMax');
expect(content).toContain('candidateTags');
expect(content).toContain('submitApply');
expect(content).toContain('实名认证审核中');
expect(content).toContain('已通过大神实名认证');
});
it('个人中心的大神入驻入口必须路由跳转至 /pages/expert/apply', () => {
const userIndexPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue');
const content = fs.readFileSync(userIndexPath, 'utf-8');
expect(content).toContain('/pages/expert/apply');
});
it('新页面与改造代码严禁包含任何系统 Emoji', () => {
const applyPath = path.join(rootDir, 'pwuniapp', 'pages', 'expert', 'apply.vue');
const content = fs.readFileSync(applyPath, 'utf-8');
const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u;
expect(emojiRegex.test(content)).toBe(false);
});
});
});