feat(expert): full closure of expert revenue trends, multichannel withdrawals, service manage, review reply and post manage

这个提交包含在:
xcbyy
2026-10-13 09:13:06 +08:00
父节点 a004584109
当前提交 e867488d0d
共有 11 个文件被更改,包括 2565 次插入 和 103 次删除

查看文件

@@ -38,8 +38,8 @@
<image class="edit-svg" :src="ICONS.settings" mode="aspectFit" />
</view>
</view>
</view>
<text class="user-uid" @click.stop="handleAvatarOrNameClick">{{ isLoggedIn ? `UID: ${user.id}` : '登录后享受完整陪伴服务' }}</text>
<text class="user-signature-text" v-if="isLoggedIn && user.signature" @click.stop="handleAvatarOrNameClick">{{ user.signature }}</text>
</view>
</view>
@@ -81,7 +81,10 @@
<view class="asset-col">
<text class="asset-label">分销佣金收益 (元)</text>
<text class="asset-val gold-val">{{ isLoggedIn ? `¥${user.commission}` : '--' }}</text>
<button class="asset-btn withdraw btn-active" @click="handleWithdraw">提现</button>
<view class="asset-btn-group">
<button class="asset-btn withdraw btn-active" @click="openWithdrawModal">提现</button>
<button class="asset-btn records btn-active" @click="openWithdrawRecordsModal">记录</button>
</view>
</view>
</view>
</view>
@@ -222,6 +225,34 @@
placeholder-class="field-placeholder"
/>
</view>
<!-- 性别选择 (男 / 女 / 保密 胶囊单选) -->
<view class="edit-field">
<text class="field-label">用户性别</text>
<view class="gender-pill-group">
<view
v-for="g in ['男', '女', '保密']"
:key="g"
class="gender-pill btn-active"
:class="{ active: editForm.gender === g }"
@click="editForm.gender = g"
>
{{ g }}
</view>
</view>
</view>
<!-- 个性签名 -->
<view class="edit-field">
<text class="field-label">个性签名</text>
<input
class="field-input"
v-model="editForm.signature"
maxlength="60"
placeholder="分享您的常玩段位或搭子宣言(如:常驻朝阳,王者荣耀星耀)"
placeholder-class="field-placeholder"
/>
</view>
</view>
<view class="modal-footer">
@@ -231,6 +262,167 @@
</view>
</view>
<!-- 多渠道提现申请弹窗 -->
<view class="profile-edit-modal-mask" v-if="showWithdrawModal" @click="showWithdrawModal = false">
<view class="profile-edit-modal withdraw-modal-box" @click.stop>
<view class="modal-header">
<text class="modal-title">佣金收益提现申请</text>
<view class="btn-close btn-active" @click="showWithdrawModal = false">
<image class="close-svg" :src="ICONS.close" mode="aspectFit" />
</view>
</view>
<view class="modal-body">
<!-- 提现渠道切换 -->
<view class="channel-tabs-row">
<view
class="channel-pill btn-active"
:class="{ active: withdrawForm.channel === 'WECHAT' }"
@click="withdrawForm.channel = 'WECHAT'"
>
微信零钱
</view>
<view
class="channel-pill btn-active"
:class="{ active: withdrawForm.channel === 'ALIPAY' }"
@click="withdrawForm.channel = 'ALIPAY'"
>
支付宝
</view>
<view
class="channel-pill btn-active"
:class="{ active: withdrawForm.channel === 'BANK' }"
@click="withdrawForm.channel = 'BANK'"
>
银行卡
</view>
</view>
<!-- 提现金额输入 -->
<view class="edit-field">
<view class="field-title-row">
<text class="field-label">提现金额 (元)</text>
<text class="field-tip">可提现 ¥{{ user.commission || '0.00' }}</text>
</view>
<view class="amount-input-wrap">
<text class="currency-symbol">¥</text>
<input
class="field-input amount-val-input"
type="digit"
v-model="withdrawForm.amount"
:placeholder="`最低 ¥${minWithdrawAmount} 起提`"
placeholder-class="field-placeholder"
/>
<text class="btn-all-in btn-active" @click="withdrawForm.amount = String(user.commission || '0')">全部</text>
</view>
</view>
<!-- 渠道补充信息 -->
<view v-if="withdrawForm.channel === 'WECHAT'" class="channel-hint-card">
<text class="hint-text">资金将通过微信企业付款直达当前绑定微信号零钱钱包,预计 1-24 小时内到账。</text>
</view>
<view v-if="withdrawForm.channel === 'ALIPAY'">
<view class="edit-field">
<text class="field-label">支付宝账号</text>
<input
class="field-input"
v-model="withdrawForm.alipayAccount"
placeholder="请输入支付宝登录账号/手机号/邮箱"
placeholder-class="field-placeholder"
/>
</view>
<view class="edit-field">
<text class="field-label">收款人真实姓名</text>
<input
class="field-input"
v-model="withdrawForm.realName"
placeholder="需与支付宝实名认证姓名一致"
placeholder-class="field-placeholder"
/>
</view>
</view>
<view v-if="withdrawForm.channel === 'BANK'">
<view class="edit-field">
<text class="field-label">银行卡卡号</text>
<input
class="field-input"
type="number"
v-model="withdrawForm.bankCard"
placeholder="请输入储蓄卡卡号"
placeholder-class="field-placeholder"
/>
</view>
<view class="edit-field">
<text class="field-label">开户银行名称</text>
<input
class="field-input"
v-model="withdrawForm.bankName"
placeholder="例如:中国工商银行 / 招商银行"
placeholder-class="field-placeholder"
/>
</view>
<view class="edit-field">
<text class="field-label">持卡人姓名</text>
<input
class="field-input"
v-model="withdrawForm.realName"
placeholder="需与银行卡开户姓名一致"
placeholder-class="field-placeholder"
/>
</view>
</view>
</view>
<view class="modal-footer">
<button class="btn-modal btn-cancel btn-active" @click="showWithdrawModal = false">取消</button>
<button class="btn-modal btn-save btn-active" @click="submitWithdraw">确认提现</button>
</view>
</view>
</view>
<!-- 提现历史与审批记录跟踪弹窗 -->
<view class="profile-edit-modal-mask" v-if="showWithdrawRecordsModal" @click="showWithdrawRecordsModal = false">
<view class="profile-edit-modal withdraw-records-modal-box" @click.stop>
<view class="modal-header">
<text class="modal-title">提现审批记录</text>
<view class="btn-close btn-active" @click="showWithdrawRecordsModal = false">
<image class="close-svg" :src="ICONS.close" mode="aspectFit" />
</view>
</view>
<scroll-view scroll-y class="records-modal-scroll" v-if="userWithdrawalsList.length > 0">
<view v-for="rec in userWithdrawalsList" :key="rec.id" class="wd-card-row">
<view class="wd-row-top">
<text class="wd-no">{{ rec.withdrawNo }}</text>
<text
class="wd-badge"
:class="{
pending: rec.status === '待审核',
success: rec.status === '已打款',
rejected: rec.status === '已驳回'
}"
>
{{ rec.status }}
</text>
</view>
<view class="wd-row-center">
<text class="wd-val">¥{{ rec.amount }}</text>
<text class="wd-ch">{{ rec.channel }}</text>
</view>
<view class="wd-row-bot">
<text class="wd-time">{{ rec.applyTime }}</text>
<text class="wd-reason" v-if="rec.reason">驳回理由:{{ rec.reason }}</text>
</view>
</view>
</scroll-view>
<view class="empty-records-box" v-else>
<text class="empty-txt">暂无提现申请记录</text>
</view>
</view>
</view>
<!-- 底部自定义导航栏 (动态自适应发单人4项 / 接单人3项) -->
<AppTabBar current="user" />
@@ -248,7 +440,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, applyExpertCert } from '@/common/api.js';
import { getUserProfile, rechargeBalance, withdrawCommission, getUserFinanceRecords, updateUserProfile, uploadImage, getRechargeTiers, getPlatformConfigs, applyExpertCert, getUserWithdrawals } from '@/common/api.js';
const DEFAULT_GUEST_AVATAR = 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200';
const isLoggedIn = ref(true);
@@ -291,7 +483,8 @@ const user = ref({
commission: '0.00',
orderCount: 0,
invitedCount: 0,
phone: '138****8888'
phone: '138****8888',
signature: '同城常驻朝阳 · 寻找靠谱搭子'
});
const currentRole = ref(getCurrentRole());
const currentThemeKey = ref(getCurrentThemeKey());
@@ -520,7 +713,9 @@ const PRESET_AVATARS = [
const editForm = ref({
nickname: '',
avatar: '',
phone: ''
phone: '',
gender: '保密',
signature: ''
});
const editProfile = () => {
@@ -528,7 +723,9 @@ const editProfile = () => {
editForm.value = {
nickname: user.value.nickname || '',
avatar: user.value.avatar || PRESET_AVATARS[0],
phone: user.value.phone || ''
phone: user.value.phone || '',
gender: user.value.gender || '保密',
signature: user.value.signature || ''
};
showEditModal.value = true;
};
@@ -549,13 +746,13 @@ const onWechatChooseAvatar = async (e) => {
uni.showLoading({ title: '正在同步微信头像...' });
try {
const onlineUrl = await uploadImage(avatarUrl);
editForm.value.avatar = onlineUrl || avatarUrl;
editForm.value.avatar = onlineUrl;
uni.hideLoading();
uni.showToast({ title: '微信头像获取成功', icon: 'success' });
} catch (err) {
uni.hideLoading();
editForm.value.avatar = avatarUrl;
uni.showToast({ title: '已选取微信头像', icon: 'none' });
// 上传失败时保留原头像,切勿将本地临时路径 http://tmp/... 赋值给 avatar,避免触发渲染层 CORS 拦截
uni.showToast({ title: err.message || '微信头像上传失败,请重试', icon: 'none' });
}
};
@@ -586,12 +783,24 @@ const saveProfile = async () => {
uni.showToast({ title: '昵称不能为空', icon: 'none' });
return;
}
const currentAvatar = editForm.value.avatar || '';
if (
currentAvatar.startsWith('http://tmp/') ||
currentAvatar.startsWith('http://usr/') ||
currentAvatar.startsWith('wxfile://') ||
currentAvatar.startsWith('blob:')
) {
uni.showToast({ title: '头像正在同步或上传异常,请重新选取', icon: 'none' });
return;
}
uni.showLoading({ title: '正在保存资料...' });
try {
const updated = await updateUserProfile(user.value.id || 1001, {
nickname: editForm.value.nickname.trim(),
avatar: editForm.value.avatar,
phone: editForm.value.phone
phone: editForm.value.phone,
gender: editForm.value.gender || '保密',
signature: editForm.value.signature ? editForm.value.signature.trim() : ''
});
uni.hideLoading();
if (updated) {
@@ -603,6 +812,8 @@ const saveProfile = async () => {
user.value.nickname = editForm.value.nickname.trim();
user.value.avatar = editForm.value.avatar;
user.value.phone = editForm.value.phone;
user.value.gender = editForm.value.gender || '保密';
user.value.signature = editForm.value.signature ? editForm.value.signature.trim() : '';
}
showEditModal.value = false;
uni.showToast({
@@ -706,48 +917,132 @@ const handleRecharge = async () => {
});
};
const handleWithdraw = () => {
const showWithdrawModal = ref(false);
const showWithdrawRecordsModal = ref(false);
const userWithdrawalsList = ref([]);
const withdrawForm = ref({
channel: 'WECHAT',
amount: '',
alipayAccount: '',
bankCard: '',
bankName: '',
realName: ''
});
const minWithdrawAmount = computed(() => {
const finRules = platformConfigs.value.finance_rules || {};
return Number(finRules.minWithdrawAmount) || 50;
});
const openWithdrawModal = () => {
if (!ensureLoggedIn()) return;
const comm = Number(user.value.commission || 0);
const finRules = platformConfigs.value.finance_rules || {};
const minWithdraw = Number(finRules.minWithdrawAmount) || 50;
const withdrawDesc = finRules.withdrawDesc || '提现将自动发起微信企业付款打款到微信零钱。';
if (comm <= 0) {
uni.showToast({ title: '暂无可用提现佣金', icon: 'none' });
uni.showToast({ title: '暂无可提现佣金', icon: 'none' });
return;
}
if (comm < minWithdraw) {
uni.showToast({ title: `提现金额须满 ¥${minWithdraw} 元起提`, icon: 'none' });
withdrawForm.value = {
channel: 'WECHAT',
amount: String(comm),
alipayAccount: '',
bankCard: '',
bankName: '',
realName: user.value.nickname || ''
};
showWithdrawModal.value = true;
};
const openWithdrawRecordsModal = async () => {
if (!ensureLoggedIn()) return;
try {
const list = await getUserWithdrawals(user.value.id || 1001);
userWithdrawalsList.value = Array.isArray(list) ? list : [];
} catch (_) {
userWithdrawalsList.value = [];
}
showWithdrawRecordsModal.value = true;
};
const submitWithdraw = async () => {
const comm = Number(user.value.commission || 0);
const amount = Number(withdrawForm.value.amount || 0);
const minLimit = minWithdrawAmount.value;
if (isNaN(amount) || amount <= 0) {
uni.showToast({ title: '请输入有效的提现金额', icon: 'none' });
return;
}
if (amount < minLimit) {
uni.showToast({ title: `提现金额须满 ¥${minLimit} 起提`, icon: 'none' });
return;
}
if (amount > comm) {
uni.showToast({ title: '提现金额超出可提余额', icon: 'none' });
return;
}
if (amount > 20000) {
uni.showToast({ title: '单笔提现最高 ¥20000', icon: 'none' });
return;
}
uni.showModal({
title: '佣金提现申请',
content: `当前可提现佣金为 ¥${user.value.commission} (最低满¥${minWithdraw}起提)。\n${withdrawDesc}`,
confirmText: '确认提现',
confirmColor: '#10B981',
success: async (res) => {
if (res.confirm) {
try {
const result = await withdrawCommission(user.value.id || 1001, comm);
if (result && result.commission !== undefined) {
user.value.commission = result.commission;
}
uni.showToast({
title: '提现申请已提交,等待到账',
icon: 'success',
duration: 2000
});
} catch (err) {
uni.showToast({
title: err.message || '提现失败,请稍后重试',
icon: 'none'
});
}
}
// 渠道账号校验
if (withdrawForm.value.channel === 'ALIPAY') {
if (!withdrawForm.value.alipayAccount.trim()) {
uni.showToast({ title: '请输入支付宝账号', icon: 'none' });
return;
}
});
if (!withdrawForm.value.realName.trim()) {
uni.showToast({ title: '请输入真实姓名', icon: 'none' });
return;
}
} else if (withdrawForm.value.channel === 'BANK') {
if (!withdrawForm.value.bankCard.trim()) {
uni.showToast({ title: '请输入银行卡号', icon: 'none' });
return;
}
if (!withdrawForm.value.bankName.trim()) {
uni.showToast({ title: '请输入开户行全称', icon: 'none' });
return;
}
if (!withdrawForm.value.realName.trim()) {
uni.showToast({ title: '请输入持卡人姓名', icon: 'none' });
return;
}
}
try {
uni.showLoading({ title: '正在提交申请...' });
const payload = {
channel: withdrawForm.value.channel,
alipayAccount: withdrawForm.value.alipayAccount.trim(),
bankCard: withdrawForm.value.bankCard.trim(),
bankName: withdrawForm.value.bankName.trim(),
realName: withdrawForm.value.realName.trim()
};
const result = await withdrawCommission(user.value.id || 1001, amount, payload);
uni.hideLoading();
if (result && result.commission !== undefined) {
user.value.commission = result.commission;
} else {
user.value.commission = Math.max(0, comm - amount).toFixed(2);
}
showWithdrawModal.value = false;
uni.showToast({
title: '提现申请已提交',
icon: 'success',
duration: 2000
});
} catch (err) {
uni.hideLoading();
uni.showToast({
title: err.message || '提现申请失败',
icon: 'none'
});
}
};
const handleWithdraw = () => {
openWithdrawModal();
};
const showInvitePoster = () => {
@@ -1204,9 +1499,15 @@ const navTo = (url) => {
}
}
.asset-btn-group {
display: flex;
align-items: center;
gap: 12rpx;
}
.asset-btn {
font-size: 20rpx;
padding: 0 24rpx;
padding: 0 20rpx;
height: 48rpx;
line-height: 46rpx;
border-radius: 24rpx;
@@ -1222,6 +1523,11 @@ const navTo = (url) => {
color: #F59E0B;
border: 1px solid rgba(245, 158, 11, 0.3);
}
&.records {
background: rgba(99, 102, 241, 0.15);
color: #818CF8;
border: 1px solid rgba(99, 102, 241, 0.3);
}
}
}
@@ -1829,6 +2135,241 @@ const navTo = (url) => {
}
}
}
.withdraw-modal-box {
max-width: 640rpx;
.channel-tabs-row {
display: flex;
background: #0F172A;
border-radius: 16rpx;
padding: 6rpx;
margin-bottom: 24rpx;
gap: 8rpx;
.channel-pill {
flex: 1;
text-align: center;
padding: 14rpx 0;
font-size: 24rpx;
font-weight: 600;
color: #94A3B8;
border-radius: 12rpx;
transition: all 0.2s ease;
&.active {
background: linear-gradient(135deg, #10B981, #059669);
color: #FFFFFF;
box-shadow: 0 4rpx 12rpx rgba(16, 185, 129, 0.35);
}
}
}
.field-title-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
.field-tip {
font-size: 20rpx;
color: #94A3B8;
}
}
.amount-input-wrap {
display: flex;
align-items: center;
background: #0F172A;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16rpx;
padding: 0 20rpx;
height: 84rpx;
.currency-symbol {
font-size: 32rpx;
font-weight: 700;
color: #10B981;
margin-right: 12rpx;
}
.amount-val-input {
flex: 1;
font-size: 32rpx;
font-weight: 700;
color: #F8FAFC;
}
.btn-all-in {
font-size: 22rpx;
font-weight: 600;
color: #10B981;
padding: 8rpx 16rpx;
background: rgba(16, 185, 129, 0.12);
border-radius: 10rpx;
}
}
.channel-hint-card {
background: rgba(16, 185, 129, 0.08);
border: 1px dashed rgba(16, 185, 129, 0.3);
border-radius: 14rpx;
padding: 16rpx;
margin-top: 16rpx;
.hint-text {
font-size: 20rpx;
color: #34D399;
line-height: 1.5;
}
}
}
.withdraw-records-modal-box {
max-width: 660rpx;
max-height: 80vh;
.records-modal-scroll {
max-height: 560rpx;
padding-right: 8rpx;
.wd-card-row {
background: #0F172A;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16rpx;
padding: 18rpx 20rpx;
margin-bottom: 16rpx;
.wd-row-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10rpx;
.wd-no {
font-size: 20rpx;
color: #64748B;
font-family: monospace;
}
.wd-badge {
font-size: 20rpx;
padding: 2rpx 12rpx;
border-radius: 8rpx;
font-weight: 600;
&.pending {
background: rgba(245, 158, 11, 0.15);
color: #F59E0B;
}
&.success {
background: rgba(16, 185, 129, 0.15);
color: #10B981;
}
&.rejected {
background: rgba(239, 68, 68, 0.15);
color: #EF4444;
}
}
}
.wd-row-center {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 8rpx;
.wd-val {
font-size: 32rpx;
font-weight: 700;
color: #F8FAFC;
}
.wd-ch {
font-size: 22rpx;
color: #94A3B8;
}
}
.wd-row-bot {
display: flex;
justify-content: space-between;
align-items: center;
.wd-time {
font-size: 18rpx;
color: #64748B;
}
.wd-reason {
font-size: 18rpx;
color: #EF4444;
}
}
}
}
.empty-records-box {
padding: 60rpx 0;
text-align: center;
.empty-txt {
font-size: 24rpx;
color: #64748B;
}
}
}
.user-signature-text {
font-size: 22rpx;
color: #94A3B8;
margin-top: 8rpx;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 480rpx;
}
.gender-pill-group {
display: flex;
gap: 16rpx;
margin-top: 10rpx;
.gender-pill {
flex: 1;
text-align: center;
padding: 16rpx 0;
font-size: 26rpx;
color: #94A3B8;
background-color: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 14rpx;
transition: all 0.2s ease;
&.active {
color: #FFFFFF;
background: linear-gradient(135deg, rgba(99, 102, 241, 0.3) 0%, rgba(139, 92, 246, 0.4) 100%);
border-color: #6366F1;
font-weight: 600;
}
}
}
.theme-warm {
.user-signature-text {
color: #64748B;
}
.gender-pill {
background-color: #F1F5F9;
border-color: #E2E8F0;
color: #475569;
&.active {
color: #FFFFFF;
background: linear-gradient(135deg, #F97316 0%, #FB923C 100%);
border-color: #F97316;
}
}
}
@keyframes glowSpin {