feat(closed-loop): complete end-to-end closed loops across miniapp, backend, and admin

这个提交包含在:
xcbyy
2026-10-14 10:13:50 +08:00
父节点 f8e81d2df4
当前提交 51c19d6063
共有 14 个文件被更改,包括 996 次插入 和 134 次删除

查看文件

@@ -154,6 +154,12 @@
<text class="row-label">平台消息与通知</text>
<image class="row-arrow-svg" :src="ICONS.chevronRight" mode="aspectFit" />
</view>
<view class="list-row btn-active" @click="openMyReportsModal">
<image class="row-svg" :src="ICONS.alert" mode="aspectFit" />
<text class="row-label">我的举报与维权反馈</text>
<text class="row-sub-tag" v-if="myReportsList.length > 0">{{ myReportsList.length }}条记录</text>
<image class="row-arrow-svg" :src="ICONS.chevronRight" mode="aspectFit" />
</view>
<view class="list-row btn-active" @click="openFaq">
<image class="row-svg" :src="ICONS.messageSquare" mode="aspectFit" />
<text class="row-label">帮助中心与常见问题</text>
@@ -321,6 +327,50 @@
</view>
</view>
<!-- 我的举报与维权进度反馈弹窗 -->
<view class="profile-edit-modal-mask" v-if="showReportsModal" @click="showReportsModal = 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="showReportsModal = false">
<image class="close-svg" :src="ICONS.close" mode="aspectFit" />
</view>
</view>
<scroll-view scroll-y class="records-modal-scroll" v-if="myReportsList.length > 0">
<view v-for="rpt in myReportsList" :key="rpt.id" class="wd-card-row">
<view class="wd-row-top">
<text class="wd-no">对象:{{ rpt.targetName || rpt.targetId }} · {{ rpt.reason }}</text>
<text
class="wd-badge"
:class="{
pending: rpt.status === '待处理',
success: rpt.status === '已核实处罚',
rejected: rpt.status === '已驳回'
}"
>
{{ rpt.status || '待处理' }}
</text>
</view>
<view class="wd-row-center" v-if="rpt.description">
<text class="wd-ch">举报说明:{{ rpt.description }}</text>
</view>
<view class="wd-row-bot">
<text class="wd-time">{{ rpt.createdAt }}</text>
<text class="wd-reason" v-if="rpt.handleNote || rpt.handleResult">
平台反馈:{{ rpt.handleNote || rpt.handleResult }}
</text>
<text class="wd-time" v-else>平台安全专员正在核实处理中</text>
</view>
</view>
</scroll-view>
<view class="empty-records-box" v-else>
<image class="empty-icon-svg" :src="ICONS.shield" mode="aspectFit" />
<text class="empty-txt">暂无举报与维权记录</text>
</view>
</view>
</view>
<!-- 底部自定义导航栏 (动态自适应发单人4项 / 接单人3项) -->
@@ -340,15 +390,33 @@ 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, getUserWithdrawals, getFavorites, removeFavorite, getDistributionTeam, getDistributionRecords } from '@/common/api.js';
import { getUserProfile, rechargeBalance, withdrawCommission, getUserFinanceRecords, updateUserProfile, uploadImage, getRechargeTiers, getPlatformConfigs, applyExpertCert, getUserWithdrawals, getFavorites, removeFavorite, getDistributionTeam, getDistributionRecords, getMyReports } from '@/common/api.js';
const DEFAULT_GUEST_AVATAR = 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200';
const isLoggedIn = ref(true);
const showLoginModal = ref(false);
// 我的收藏
// 我的收藏与我的举报反馈
const favoriteList = ref([]);
const loadingFavorites = ref(false);
const showReportsModal = ref(false);
const myReportsList = ref([]);
const loadMyReports = async () => {
const uid = user.value.id || uni.getStorageSync('userId') || 1001;
try {
const list = await getMyReports(uid);
if (Array.isArray(list)) {
myReportsList.value = list;
}
} catch (_) {}
};
const openMyReportsModal = async () => {
if (!ensureLoggedIn()) return;
await loadMyReports();
showReportsModal.value = true;
};
// 系统与隐私设置
const privacyHidePhone = ref(uni.getStorageSync('privacy_hide_phone') === '1');
@@ -546,6 +614,16 @@ const loadUserProfile = async (targetId) => {
...profile,
avatar: safeAvatar
};
// 实时同步服务端大神认证审核状态至本地 Storage,确保后台审核通过/驳回立即生效
if (profile.certStatus !== undefined) {
setExpertCertStatus(Number(profile.certStatus));
} else if (profile.isExpert || (profile.role && profile.role.includes('大神'))) {
setExpertCertStatus(CERT_STATUS.APPROVED);
}
uni.setStorageSync('PW_USER_INFO', {
...(uni.getStorageSync('PW_USER_INFO') || {}),
...user.value
});
}
} catch (err) {
console.warn('[User] 加载用户信息异常:', err.message);
@@ -566,6 +644,7 @@ onMounted(() => {
uni.$on('login-success', onLoginSuccess);
loadUserProfile();
loadPlatformConfigs();
loadMyReports();
});
onShow(() => {
@@ -579,6 +658,7 @@ onShow(() => {
privacyHidePhone.value = uni.getStorageSync('privacy_hide_phone') === '1';
if (isLoggedIn.value) {
loadFavorites();
loadMyReports();
}
});
@@ -611,10 +691,17 @@ const handleSwitchTheme = () => {
const toggleRole = () => {
if (!ensureLoggedIn()) return;
if (!isExpertCertified()) {
const curCert = getExpertCertStatus();
const isPending = curCert === CERT_STATUS.PENDING;
const isRejected = curCert === CERT_STATUS.REJECTED;
uni.showModal({
title: '大神实名认证提醒',
content: '当前账号尚未完成大神实名入驻认证,无法切换至接单模式。是否立即申请认证?',
confirmText: '立即认证',
title: isPending ? '入驻资质审核中' : (isRejected ? '入驻审核未通过' : '大神实名认证提醒'),
content: isPending
? '您的大神实名入驻申请正在由平台运营人工核验中,审核通过后即可切换至接单模式。是否查看审核进度?'
: isRejected
? `上次申请未通过:${user.value.rejectReason || '请核对身份与资质信息'}。是否立即修改并重新提交?`
: '当前账号尚未完成大神实名入驻认证,无法切换至接单模式。是否立即申请认证?',
confirmText: isPending ? '查看进度' : (isRejected ? '重新提交' : '立即认证'),
cancelText: '稍后再说',
confirmColor: '#10B981',
success: (res) => {

查看文件

@@ -85,6 +85,14 @@
<button class="btn-poster-action secondary btn-active" @click="copyInviteLink">复制邀请链接</button>
<button class="btn-poster-action primary btn-active" @click="savePosterImage">保存海报至相册</button>
</view>
<view class="bind-inviter-card" style="margin-top: 24rpx; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 20rpx; padding: 20rpx 24rpx; display: flex; align-items: center; justify-content: space-between;">
<text style="font-size: 24rpx; color: #94A3B8;">{{ user.inviterId ? ('已绑定上级邀请人:PW' + user.inviterId) : '填写好友邀请码绑定上下级关系' }}</text>
<view v-if="!user.inviterId" style="display: flex; align-items: center; gap: 12rpx;">
<input v-model="bindCodeInput" placeholder="如 PW2001" placeholder-class="field-placeholder" style="width: 180rpx; height: 56rpx; background: rgba(0,0,0,0.25); border-radius: 12rpx; padding: 0 16rpx; font-size: 24rpx; color: #F8FAFC;" />
<button class="btn-active" style="margin: 0; height: 56rpx; line-height: 56rpx; padding: 0 24rpx; font-size: 24rpx; border-radius: 12rpx; background: #6366F1; color: #FFFFFF;" @click="handleBindInviteCode">绑定</button>
</view>
</view>
</view>
<!-- Tab 2: 下级团队列表 -->
@@ -209,7 +217,8 @@ import {
getUserProfile,
getDistributionTeam,
getDistributionRecords,
withdrawCommission
withdrawCommission,
bindInviteCode
} from '@/common/api.js';
import { ICONS } from '@/common/icons.js';
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
@@ -218,6 +227,7 @@ import { getNavBarInfo } from '@/common/nav.js';
const currentThemeKey = ref(getCurrentThemeKey());
const navInfo = ref(getNavBarInfo());
const isRefreshing = ref(false);
const bindCodeInput = ref('');
const defaultAvatar = 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=200';
@@ -324,6 +334,24 @@ const savePosterImage = () => {
uni.showToast({ title: '海报已保存至相册', icon: 'success' });
};
const handleBindInviteCode = async () => {
const code = bindCodeInput.value.trim();
if (!code) {
uni.showToast({ title: '请输入好友邀请码', icon: 'none' });
return;
}
try {
const uid = user.value.id || 2006;
const res = await bindInviteCode(uid, code);
user.value.inviterId = res.inviterId;
bindCodeInput.value = '';
uni.showToast({ title: `成功绑定上级:${res.inviterName || code}`, icon: 'success' });
loadData();
} catch (err) {
uni.showToast({ title: err.message || '绑定失败', icon: 'none' });
}
};
const handleFillAllCommission = () => {
const comm = Number(user.value.commission || 0);
if (comm <= 0) {