feat(user): add logout, demo account switcher and guest state support

这个提交包含在:
xcbyy
2026-10-10 16:20:55 +08:00
父节点 3a700c42a1
当前提交 593d44bdbd
共有 2 个文件被更改,包括 499 次插入 和 13 次删除

查看文件

@@ -1,18 +1,19 @@
<template>
<view class="user-container" :class="{ 'theme-warm': currentThemeKey === 'warm' }" :style="navStyle">
<!-- 用户头部个人资料卡 -->
<view class="profile-card">
<image class="user-avatar" :src="user.avatar" mode="aspectFill" />
<view class="profile-card" @click="!isLoggedIn && openLoginModal()">
<image class="user-avatar" :src="isLoggedIn && user.avatar ? user.avatar : DEFAULT_GUEST_AVATAR" mode="aspectFill" />
<view class="user-info">
<!-- 顶层水平对齐行:昵称/VIP 与 月亮太阳切换/设置 在同一水平线 -->
<view class="user-top-row">
<view class="name-row">
<text class="user-name">{{ user.nickname }}</text>
<text class="vip-badge">VIP 黄金玩家</text>
<text class="user-name">{{ isLoggedIn ? user.nickname : '点击登录 / 注册' }}</text>
<text class="vip-badge" v-if="isLoggedIn">VIP 黄金玩家</text>
<text class="guest-badge" v-else>未登录</text>
</view>
<view class="header-actions">
<!-- 核心优化:月亮与太阳快捷切换胶囊 (与左侧昵称在同一水平线上) -->
<view class="theme-quick-switch btn-active" @click="toggleThemeDirectly" title="切换界面风格">
<view class="theme-quick-switch btn-active" @click.stop="toggleThemeDirectly" title="切换界面风格">
<view
class="switch-icon-btn moon-btn"
:class="{ active: currentThemeKey === 'cool' }"
@@ -28,12 +29,12 @@
<image class="theme-icon-svg" :src="ICONS.sun" mode="aspectFit" />
</view>
</view>
<view class="edit-btn btn-active" @click="editProfile">
<view class="edit-btn btn-active" v-if="isLoggedIn" @click.stop="editProfile">
<image class="edit-svg" :src="ICONS.settings" mode="aspectFit" />
</view>
</view>
</view>
<text class="user-uid">UID: {{ user.id }}</text>
<text class="user-uid">{{ isLoggedIn ? `UID: ${user.id}` : '登录后享受完整陪伴服务' }}</text>
</view>
</view>
@@ -68,13 +69,13 @@
<view class="asset-grid">
<view class="asset-col">
<text class="asset-label">可用余额 (元)</text>
<text class="asset-val green-val">¥{{ user.balance }}</text>
<text class="asset-val green-val">{{ isLoggedIn ? `¥${user.balance}` : '--' }}</text>
<button class="asset-btn recharge btn-active" @click="handleRecharge">充值</button>
</view>
<view class="asset-divider"></view>
<view class="asset-col">
<text class="asset-label">分销佣金收益 (元)</text>
<text class="asset-val gold-val">¥{{ user.commission }}</text>
<text class="asset-val gold-val">{{ isLoggedIn ? `¥${user.commission}` : '--' }}</text>
<button class="asset-btn withdraw btn-active" @click="handleWithdraw">提现</button>
</view>
</view>
@@ -141,6 +142,55 @@
</view>
</view>
<!-- 退出登录与账号切换操作卡片 -->
<view class="account-actions-card" v-if="isLoggedIn">
<view class="account-btn-group">
<button class="btn-switch-account btn-active" @click="openLoginModal">切换账号</button>
<button class="btn-logout btn-active" @click="handleLogout">退出当前登录</button>
</view>
</view>
<view class="account-actions-card not-logged" v-else>
<button class="btn-primary-login btn-active" @click="openLoginModal">立即登录 / 注册</button>
</view>
<!-- 登录与账号切换快捷弹窗 -->
<view class="login-modal-mask" v-if="showLoginModal" @click="showLoginModal = false">
<view class="login-modal" @click.stop>
<view class="modal-header">
<text class="modal-title">账号登录与快捷切换</text>
<view class="btn-close btn-active" @click="showLoginModal = false">
<image class="close-svg" :src="ICONS.close" mode="aspectFit" />
</view>
</view>
<view class="modal-body">
<button class="btn-wechat-auth btn-active" @click="handleWechatLogin">
微信一键授权快捷登录
</button>
<view class="demo-accounts-wrap">
<text class="demo-tips">支持一键切换指定身份体验账号:</text>
<view
v-for="acc in DEMO_ACCOUNTS"
:key="acc.id"
class="demo-account-item btn-active"
:class="{ current: Number(user.id) === acc.id && isLoggedIn }"
@click="switchDemoAccount(acc)"
>
<image class="demo-avatar" :src="acc.avatar" mode="aspectFill" />
<view class="demo-info">
<view class="demo-title-row">
<text class="demo-name">{{ acc.nickname }}</text>
<text class="demo-tag" :class="acc.roleTag">{{ acc.roleDesc }}</text>
</view>
<text class="demo-desc">UID: {{ acc.id }} · {{ acc.desc }}</text>
</view>
<text class="demo-action-btn">{{ Number(user.id) === acc.id && isLoggedIn ? '当前登录' : '切换至此号' }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 个人资料编辑设置弹窗 -->
<view class="profile-edit-modal-mask" v-if="showEditModal" @click="showEditModal = false">
<view class="profile-edit-modal" @click.stop>
@@ -293,6 +343,37 @@ import { getCurrentRole, setCurrentRole, ROLE_TYPES, toggleRole as toggleUserRol
import { getNavBarInfo } from '@/common/nav.js';
import { getUserProfile, rechargeBalance, withdrawCommission, getUserFinanceRecords, updateUserProfile, uploadImage, getRechargeTiers, getPlatformConfigs } 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 DEMO_ACCOUNTS = [
{
id: 1001,
nickname: '同城局长',
roleTag: 'host',
roleDesc: '房主/局长',
desc: '余额充足 · 组局发单',
avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200'
},
{
id: 2001,
nickname: '超能电竞玩家',
roleTag: 'buyer',
roleDesc: '普通买家',
desc: '搭子报名 · 预约下单',
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=200'
},
{
id: 101,
nickname: '苏苏学姐',
roleTag: 'expert',
roleDesc: '金牌陪伴师',
desc: '接单工作台 · 4位核销',
avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=200'
}
];
const user = ref({
id: 1001,
nickname: '电竞玩家',
@@ -359,9 +440,99 @@ const onRoleChanged = (role) => {
currentRole.value = role;
};
const loadUserProfile = async () => {
const openLoginModal = () => {
showLoginModal.value = true;
};
const handleWechatLogin = () => {
uni.showLoading({ title: '正在授权登录...' });
setTimeout(() => {
uni.hideLoading();
showLoginModal.value = false;
isLoggedIn.value = true;
uni.removeStorageSync('user_has_logged_out');
uni.setStorageSync('userId', 1001);
loadUserProfile(1001);
uni.showToast({ title: '登录成功', icon: 'success' });
}, 400);
};
const switchDemoAccount = async (acc) => {
showLoginModal.value = false;
uni.showLoading({ title: '正在切换账号...' });
isLoggedIn.value = true;
uni.removeStorageSync('user_has_logged_out');
uni.setStorageSync('userId', acc.id);
user.value = {
...user.value,
id: acc.id,
nickname: acc.nickname,
avatar: acc.avatar
};
await loadUserProfile(acc.id);
uni.hideLoading();
uni.showToast({
title: `已切换至【${acc.nickname}】`,
icon: 'success'
});
};
const handleLogout = () => {
uni.showModal({
title: '确认退出登录?',
content: '退出登录后将清除本地会话,切换为未登录游客态。是否确认退出?',
confirmText: '退出登录',
confirmColor: '#EF4444',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
uni.removeStorageSync('userInfo');
uni.removeStorageSync('userId');
uni.setStorageSync('user_has_logged_out', '1');
isLoggedIn.value = false;
user.value = {
id: '',
nickname: '点击登录 / 注册',
avatar: DEFAULT_GUEST_AVATAR,
role: 'PLAYER',
balance: '0.00',
commission: '0.00',
orderCount: 0,
invitedCount: 0,
phone: ''
};
uni.showToast({
title: '已退出登录',
icon: 'none',
duration: 1500
});
}
}
});
};
const loadUserProfile = async (targetId) => {
const hasLoggedOut = uni.getStorageSync('user_has_logged_out');
if (hasLoggedOut && !targetId) {
isLoggedIn.value = false;
user.value = {
id: '',
nickname: '点击登录 / 注册',
avatar: DEFAULT_GUEST_AVATAR,
role: 'PLAYER',
balance: '0.00',
commission: '0.00',
orderCount: 0,
invitedCount: 0,
phone: ''
};
return;
}
isLoggedIn.value = true;
const uid = targetId || uni.getStorageSync('userId') || 1001;
try {
const profile = await getUserProfile(1001);
const profile = await getUserProfile(uid);
if (profile) {
user.value = {
...user.value,
@@ -420,6 +591,7 @@ const handleSwitchTheme = () => {
};
const toggleRole = () => {
if (!ensureLoggedIn()) return;
if (!isExpertCertified()) {
uni.showModal({
title: '大神实名认证提醒',
@@ -468,6 +640,7 @@ const editForm = ref({
});
const editProfile = () => {
if (!ensureLoggedIn()) return;
editForm.value = {
nickname: user.value.nickname || '',
avatar: user.value.avatar || PRESET_AVATARS[0],
@@ -535,9 +708,19 @@ const saveProfile = async () => {
}
};
const ensureLoggedIn = () => {
if (!isLoggedIn.value) {
uni.showToast({ title: '请先登录账号', icon: 'none' });
openLoginModal();
return false;
}
return true;
};
const viewWalletLog = async () => {
if (!ensureLoggedIn()) return;
try {
const list = await getUserFinanceRecords(1001);
const list = await getUserFinanceRecords(user.value.id || 1001);
if (!list || list.length === 0) {
uni.showModal({
title: '账单明细',
@@ -563,6 +746,7 @@ const viewWalletLog = async () => {
};
const handleRecharge = async () => {
if (!ensureLoggedIn()) return;
let tiers = [];
try {
const list = await getRechargeTiers();
@@ -613,6 +797,7 @@ const handleRecharge = async () => {
};
const handleWithdraw = () => {
if (!ensureLoggedIn()) return;
const comm = Number(user.value.commission || 0);
const finRules = platformConfigs.value.finance_rules || {};
const minWithdraw = Number(finRules.minWithdrawAmount) || 50;
@@ -635,7 +820,7 @@ const handleWithdraw = () => {
success: async (res) => {
if (res.confirm) {
try {
const result = await withdrawCommission(1001, comm);
const result = await withdrawCommission(user.value.id || 1001, comm);
if (result && result.commission !== undefined) {
user.value.commission = result.commission;
}
@@ -656,6 +841,7 @@ const handleWithdraw = () => {
};
const showInvitePoster = () => {
if (!ensureLoggedIn()) return;
const finRules = platformConfigs.value.finance_rules || {};
const firstRate = finRules.firstReferralPercent != null ? finRules.firstReferralPercent : 5.0;
const secondRate = finRules.secondReferralPercent != null ? finRules.secondReferralPercent : 2.0;
@@ -751,6 +937,7 @@ const submitExpertCertForm = async () => {
};
const applyExpert = () => {
if (!ensureLoggedIn()) return;
const certStatus = getExpertCertStatus();
if (certStatus === CERT_STATUS.APPROVED) {
uni.showToast({
@@ -872,6 +1059,7 @@ const openAbout = () => {
};
const navTo = (url) => {
if (!ensureLoggedIn()) return;
if (url.startsWith('/pages/activity/list')) {
if (url.includes('tab=')) {
const tab = url.split('tab=')[1].split('&')[0];
@@ -1693,6 +1881,274 @@ const navTo = (url) => {
}
}
.guest-badge {
font-size: 20rpx;
color: #94A3B8;
background: rgba(148, 163, 184, 0.15);
border: 1px solid rgba(148, 163, 184, 0.3);
padding: 4rpx 14rpx;
border-radius: 20rpx;
font-weight: 500;
}
.account-actions-card {
margin-top: 24rpx;
margin-bottom: 30rpx;
.account-btn-group {
display: flex;
gap: 20rpx;
.btn-switch-account {
flex: 1;
height: 84rpx;
line-height: 84rpx;
background: rgba(99, 102, 241, 0.12);
border: 1px solid rgba(99, 102, 241, 0.3);
color: #818CF8;
font-size: 26rpx;
font-weight: 600;
border-radius: 24rpx;
}
.btn-logout {
flex: 1;
height: 84rpx;
line-height: 84rpx;
background: rgba(239, 68, 68, 0.12);
border: 1px solid rgba(239, 68, 68, 0.3);
color: #F87171;
font-size: 26rpx;
font-weight: 600;
border-radius: 24rpx;
}
}
&.not-logged {
.btn-primary-login {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background: linear-gradient(135deg, #6366F1, #8B5CF6);
color: #FFFFFF;
font-size: 28rpx;
font-weight: 700;
border-radius: 24rpx;
box-shadow: 0 4rpx 20rpx rgba(99, 102, 241, 0.35);
}
}
}
.login-modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.75);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 40rpx;
backdrop-filter: blur(6px);
}
.login-modal {
width: 100%;
max-width: 640rpx;
background: #151D30;
border-radius: 32rpx;
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 36rpx;
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.6);
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
.modal-title {
font-size: 30rpx;
font-weight: 700;
color: #F8FAFC;
}
.btn-close .close-svg {
width: 32rpx;
height: 32rpx;
}
}
.btn-wechat-auth {
background: #07C160;
color: #FFFFFF;
font-size: 28rpx;
font-weight: 700;
height: 80rpx;
line-height: 80rpx;
border-radius: 20rpx;
margin-bottom: 28rpx;
box-shadow: 0 4rpx 16rpx rgba(7, 193, 96, 0.35);
}
.demo-accounts-wrap {
.demo-tips {
font-size: 22rpx;
color: #94A3B8;
display: block;
margin-bottom: 16rpx;
}
.demo-account-item {
display: flex;
align-items: center;
gap: 16rpx;
padding: 16rpx;
border-radius: 18rpx;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.06);
margin-bottom: 12rpx;
&.current {
border-color: #6366F1;
background: rgba(99, 102, 241, 0.12);
}
.demo-avatar {
width: 68rpx;
height: 68rpx;
border-radius: 50%;
flex-shrink: 0;
}
.demo-info {
flex: 1;
overflow: hidden;
.demo-title-row {
display: flex;
align-items: center;
gap: 12rpx;
.demo-name {
font-size: 24rpx;
font-weight: 700;
color: #F8FAFC;
}
.demo-tag {
font-size: 18rpx;
padding: 2rpx 8rpx;
border-radius: 6rpx;
&.host {
background: rgba(245, 158, 11, 0.2);
color: #F59E0B;
}
&.buyer {
background: rgba(99, 102, 241, 0.2);
color: #818CF8;
}
&.expert {
background: rgba(16, 185, 129, 0.2);
color: #10B981;
}
}
}
.demo-desc {
font-size: 20rpx;
color: #94A3B8;
margin-top: 4rpx;
display: block;
}
}
.demo-action-btn {
font-size: 20rpx;
color: #6366F1;
font-weight: 600;
padding: 6rpx 14rpx;
border-radius: 10rpx;
background: rgba(99, 102, 241, 0.15);
}
}
}
}
.theme-warm {
.guest-badge {
color: #78716C;
background: #F5F2EC;
border-color: #E7E5E4;
}
.account-actions-card {
.account-btn-group {
.btn-switch-account {
background: rgba(234, 88, 12, 0.08);
border-color: rgba(234, 88, 12, 0.25);
color: #EA580C;
}
.btn-logout {
background: #FEE2E2;
border-color: #FCA5A5;
color: #DC2626;
}
}
&.not-logged {
.btn-primary-login {
background: linear-gradient(135deg, #F97316 0%, #EA580C 100%);
box-shadow: 0 4rpx 20rpx rgba(234, 88, 12, 0.35);
}
}
}
.login-modal {
background: #FFFFFF;
border-color: #F0ECE4;
box-shadow: 0 20rpx 60rpx rgba(28, 25, 23, 0.15);
.modal-header .modal-title {
color: #1C1917;
}
.demo-accounts-wrap {
.demo-tips {
color: #78716C;
}
.demo-account-item {
background: #FAFAF9;
border-color: #E7E5E4;
&.current {
border-color: #EA580C;
background: rgba(234, 88, 12, 0.08);
}
.demo-info {
.demo-title-row .demo-name {
color: #1C1917;
}
.demo-desc {
color: #78716C;
}
}
.demo-action-btn {
color: #EA580C;
background: rgba(234, 88, 12, 0.12);
}
}
}
}
}
@keyframes glowSpin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }

查看文件

@@ -375,5 +375,35 @@ describe('ContentSecurityService & Compliance Architecture (TDD 微信内容安
}
});
});
describe('10. 个人中心未登录游客态、退出登录及切换账号门禁验证 (用户体验闭环与安全防范)', () => {
it('个人中心页面必须包含退出登录、切换账号与登录弹窗入口', () => {
const userIndexPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue');
const content = fs.readFileSync(userIndexPath, 'utf-8');
expect(content).toContain('handleLogout');
expect(content).toContain('switchDemoAccount');
expect(content).toContain('openLoginModal');
expect(content).toContain('isLoggedIn');
expect(content).toContain('account-actions-card');
expect(content).toContain('login-modal-mask');
expect(content).toContain('退出当前登录');
expect(content).toContain('切换账号');
expect(content).toContain('微信一键授权快捷登录');
});
it('个人中心核心交互必须包含未登录拦截守护 ensureLoggedIn', () => {
const userIndexPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue');
const content = fs.readFileSync(userIndexPath, 'utf-8');
expect(content).toContain('const ensureLoggedIn = () =>');
expect(content).toContain('if (!ensureLoggedIn()) return;');
});
it('所有个人中心改造代码严禁包含任何系统 Emoji', () => {
const userIndexPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue');
const text = fs.readFileSync(userIndexPath, 'utf-8');
const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u;
expect(emojiRegex.test(text)).toBe(false);
});
});
});