feat(auth): create dedicated official standard WeChat login page and remove popup modal
这个提交包含在:
@@ -19,6 +19,15 @@
|
||||
"backgroundColor": "#0A0F1D"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/auth/login",
|
||||
"style": {
|
||||
"navigationBarTitleText": "账号登录与注册",
|
||||
"navigationBarBackgroundColor": "#0A0F1D",
|
||||
"navigationBarTextStyle": "white",
|
||||
"backgroundColor": "#0A0F1D"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/activity/list",
|
||||
"style": {
|
||||
|
||||
784
pwuniapp/pages/auth/login.vue
普通文件
784
pwuniapp/pages/auth/login.vue
普通文件
@@ -0,0 +1,784 @@
|
||||
<template>
|
||||
<view class="auth-login-page" :class="{ 'theme-warm': currentThemeKey === 'warm' }">
|
||||
<!-- 顶部品牌视觉与应用 Slogan -->
|
||||
<view class="brand-header">
|
||||
<view class="brand-logo-wrap">
|
||||
<image class="brand-logo-img" :src="APP_LOGO" mode="aspectFit" />
|
||||
</view>
|
||||
<text class="brand-app-name">同城陪玩生活圈</text>
|
||||
<text class="brand-slogan">年轻人的同城搭子与温暖陪伴社交平台</text>
|
||||
</view>
|
||||
|
||||
<!-- 微信小程序官方标准:头像与昵称授权填写卡片 -->
|
||||
<view class="auth-profile-card">
|
||||
<view class="profile-card-header">
|
||||
<text class="profile-card-title">完善个人主页资料</text>
|
||||
<text class="profile-card-tip">遵循微信小程序用户资料填写规范</text>
|
||||
</view>
|
||||
|
||||
<!-- 微信官方 open-type="chooseAvatar" 原生头像选择 -->
|
||||
<view class="avatar-choose-row">
|
||||
<button
|
||||
class="avatar-picker-btn btn-active"
|
||||
open-type="chooseAvatar"
|
||||
@chooseavatar="onChooseAvatar"
|
||||
>
|
||||
<image class="chosen-avatar" :src="userForm.avatar || DEFAULT_AVATAR" mode="aspectFill" />
|
||||
<view class="avatar-badge">
|
||||
<image class="camera-icon-svg" :src="ICONS.camera" mode="aspectFit" />
|
||||
</view>
|
||||
</button>
|
||||
<view class="avatar-tip-wrap">
|
||||
<text class="avatar-tip-title">点击设置微信头像</text>
|
||||
<text class="avatar-tip-sub">轻触一键拉起微信头像快速选用</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 微信官方 type="nickname" 原生昵称输入框 -->
|
||||
<view class="nickname-input-row">
|
||||
<text class="input-label">昵称</text>
|
||||
<input
|
||||
class="nickname-input"
|
||||
type="nickname"
|
||||
v-model="userForm.nickname"
|
||||
placeholder="点击快速填入或输入昵称"
|
||||
placeholder-class="ph-style"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 微信手机号授权快捷登录主操作区 (微信官方推荐格式) -->
|
||||
<view class="main-action-section">
|
||||
<button
|
||||
class="btn-wechat-primary btn-active"
|
||||
open-type="getPhoneNumber"
|
||||
@getphonenumber="handleGetPhoneNumber"
|
||||
@click="handleFallbackLogin"
|
||||
>
|
||||
<image class="wechat-icon-svg" :src="ICONS.shield" mode="aspectFit" />
|
||||
<text class="btn-text">一键授权快捷登录</text>
|
||||
</button>
|
||||
<text class="auth-guarantee-tip">微信官方安全风控 · 个人信息严格加密存储</text>
|
||||
</view>
|
||||
|
||||
<!-- 多角色体验账号矩阵 (局长/买家/金牌大神) -->
|
||||
<view class="demo-accounts-card">
|
||||
<view class="demo-card-head">
|
||||
<text class="demo-title">多角色体验账号直登</text>
|
||||
<text class="demo-sub">评审与体验专用 · 一键切换角色</text>
|
||||
</view>
|
||||
|
||||
<view class="demo-list">
|
||||
<view
|
||||
v-for="acc in DEMO_ACCOUNTS"
|
||||
:key="acc.id"
|
||||
class="demo-item btn-active"
|
||||
@click="loginWithDemo(acc)"
|
||||
>
|
||||
<image class="demo-avatar" :src="acc.avatar" mode="aspectFill" />
|
||||
<view class="demo-info">
|
||||
<view class="demo-name-row">
|
||||
<text class="demo-name">{{ acc.nickname }}</text>
|
||||
<text class="demo-badge" :class="acc.roleTag">{{ acc.roleDesc }}</text>
|
||||
</view>
|
||||
<text class="demo-feature">{{ acc.desc }}</text>
|
||||
</view>
|
||||
<button class="btn-switch-account btn-active">一键体验</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部合规声明与服务协议勾选 -->
|
||||
<view class="protocol-section" @click="agreedProtocol = !agreedProtocol">
|
||||
<view class="checkbox" :class="{ checked: agreedProtocol }">
|
||||
<view class="checkbox-inner" v-if="agreedProtocol"></view>
|
||||
</view>
|
||||
<view class="protocol-text">
|
||||
<text>登录注册即代表您已阅读并同意</text>
|
||||
<text class="protocol-link" @click.stop="openUserAgreement">《用户服务协议》</text>
|
||||
<text>与</text>
|
||||
<text class="protocol-link" @click.stop="openPrivacyAgreement">《隐私保护指引》</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||
import { ICONS } from '@/common/icons.js';
|
||||
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
|
||||
import { uploadImage } from '@/common/api.js';
|
||||
|
||||
const APP_LOGO = 'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=200';
|
||||
const DEFAULT_AVATAR = 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200';
|
||||
|
||||
const currentThemeKey = ref(getCurrentThemeKey());
|
||||
const agreedProtocol = ref(true);
|
||||
const redirectUrl = ref('');
|
||||
|
||||
const userForm = ref({
|
||||
avatar: DEFAULT_AVATAR,
|
||||
nickname: '微信用户'
|
||||
});
|
||||
|
||||
const DEMO_ACCOUNTS = [
|
||||
{
|
||||
id: 1001,
|
||||
nickname: '晨曦微光局长',
|
||||
roleTag: 'host',
|
||||
roleDesc: '房主/局长',
|
||||
desc: '余额充足 · 发起项目制/AA组局',
|
||||
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: '工作台接单 · 雷达抢单 · 现场核销',
|
||||
avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=200'
|
||||
}
|
||||
];
|
||||
|
||||
onLoad((options) => {
|
||||
if (options && options.redirect) {
|
||||
try {
|
||||
redirectUrl.value = decodeURIComponent(options.redirect);
|
||||
} catch (_) {
|
||||
redirectUrl.value = options.redirect;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
currentThemeKey.value = getCurrentThemeKey();
|
||||
applyThemeToNativeUI(currentThemeKey.value);
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
currentThemeKey.value = getCurrentThemeKey();
|
||||
applyThemeToNativeUI(currentThemeKey.value);
|
||||
});
|
||||
|
||||
const onChooseAvatar = async (e) => {
|
||||
const avatarUrl = e?.detail?.avatarUrl;
|
||||
if (!avatarUrl) return;
|
||||
|
||||
userForm.value.avatar = avatarUrl;
|
||||
try {
|
||||
const onlineUrl = await uploadImage(avatarUrl);
|
||||
if (onlineUrl) {
|
||||
userForm.value.avatar = onlineUrl;
|
||||
}
|
||||
} catch (_) {}
|
||||
};
|
||||
|
||||
const completeLogin = (userId, customData = {}) => {
|
||||
uni.showLoading({ title: '正在登录...' });
|
||||
|
||||
setTimeout(() => {
|
||||
uni.hideLoading();
|
||||
uni.removeStorageSync('user_has_logged_out');
|
||||
uni.setStorageSync('userId', userId);
|
||||
if (customData.nickname || userForm.value.nickname) {
|
||||
uni.setStorageSync('userInfo', {
|
||||
id: userId,
|
||||
nickname: customData.nickname || userForm.value.nickname,
|
||||
avatar: customData.avatar || userForm.value.avatar
|
||||
});
|
||||
}
|
||||
|
||||
uni.$emit('login-success', { userId });
|
||||
uni.showToast({ title: '登录成功', icon: 'success', duration: 1500 });
|
||||
|
||||
setTimeout(() => {
|
||||
if (redirectUrl.value) {
|
||||
if (redirectUrl.value.startsWith('/pages/index') ||
|
||||
redirectUrl.value.startsWith('/pages/activity/list') ||
|
||||
redirectUrl.value.startsWith('/pages/order/list') ||
|
||||
redirectUrl.value.startsWith('/pages/user/index')) {
|
||||
uni.switchTab({ url: redirectUrl.value });
|
||||
} else {
|
||||
uni.redirectTo({ url: redirectUrl.value });
|
||||
}
|
||||
} else {
|
||||
uni.navigateBack({
|
||||
fail: () => {
|
||||
uni.switchTab({ url: '/pages/user/index' });
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 400);
|
||||
}, 350);
|
||||
};
|
||||
|
||||
const handleGetPhoneNumber = (e) => {
|
||||
if (!agreedProtocol.value) {
|
||||
uni.showToast({ title: '请先阅读并勾选服务协议', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
if (e && e.detail && (e.detail.code || e.detail.encryptedData)) {
|
||||
completeLogin(1001, {
|
||||
nickname: userForm.value.nickname || '微信用户',
|
||||
avatar: userForm.value.avatar
|
||||
});
|
||||
} else {
|
||||
console.warn('[WeChat Login] 未授权手机号或环境受限:', e?.detail?.errMsg);
|
||||
uni.showModal({
|
||||
title: '登录提示',
|
||||
content: '未获得微信手机号授权(需非个人主体认证小程序支持)。是否使用体验账号直接登录?',
|
||||
confirmText: '快捷登录',
|
||||
cancelText: '取消',
|
||||
confirmColor: '#10B981',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
completeLogin(1001);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
completeLogin(1001);
|
||||
// #endif
|
||||
};
|
||||
|
||||
const handleFallbackLogin = () => {
|
||||
// #ifndef MP-WEIXIN
|
||||
if (!agreedProtocol.value) {
|
||||
uni.showToast({ title: '请先阅读并勾选服务协议', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
completeLogin(1001);
|
||||
// #endif
|
||||
};
|
||||
|
||||
const loginWithDemo = (acc) => {
|
||||
if (!agreedProtocol.value) {
|
||||
uni.showToast({ title: '请先阅读并勾选服务协议', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
completeLogin(acc.id, {
|
||||
nickname: acc.nickname,
|
||||
avatar: acc.avatar
|
||||
});
|
||||
};
|
||||
|
||||
const openUserAgreement = () => {
|
||||
uni.showModal({
|
||||
title: '用户服务协议',
|
||||
content: '欢迎使用同城生活陪伴服务平台。平台遵循合法合规与安全原则,所有同城活动履约均须在公开透明街区展开,严禁发布违规涉黄及私下交易导流信息。',
|
||||
showCancel: false,
|
||||
confirmText: '我知道了'
|
||||
});
|
||||
};
|
||||
|
||||
const openPrivacyAgreement = () => {
|
||||
uni.showModal({
|
||||
title: '隐私保护指引',
|
||||
content: '平台高度重视用户个人信息安全,全面遵守国家网信办与微信小程序开发者隐私规范要求。您的手机号与身份信息仅用于平台核销和交易安全保障,未经授权绝不对外公开。',
|
||||
showCancel: false,
|
||||
confirmText: '我知道了'
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.auth-login-page {
|
||||
min-height: 100vh;
|
||||
background-color: #0A0F1D;
|
||||
box-sizing: border-box;
|
||||
padding: 40rpx 36rpx calc(60rpx + env(safe-area-inset-bottom));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 顶部品牌区 */
|
||||
.brand-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: 24rpx;
|
||||
margin-bottom: 44rpx;
|
||||
|
||||
.brand-logo-wrap {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
border-radius: 40rpx;
|
||||
background: linear-gradient(135deg, #6366F1, #8B5CF6);
|
||||
padding: 6rpx;
|
||||
box-shadow: 0 12rpx 32rpx rgba(99, 102, 241, 0.4);
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.brand-logo-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 36rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.brand-app-name {
|
||||
font-size: 38rpx;
|
||||
font-weight: 800;
|
||||
color: #F8FAFC;
|
||||
letter-spacing: 2rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.brand-slogan {
|
||||
font-size: 24rpx;
|
||||
color: #94A3B8;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* 微信官方头像昵称卡片 */
|
||||
.auth-profile-card {
|
||||
background: #151D30;
|
||||
border-radius: 28rpx;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
padding: 32rpx;
|
||||
margin-bottom: 36rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.3);
|
||||
|
||||
.profile-card-header {
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.profile-card-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #F8FAFC;
|
||||
display: block;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
.profile-card-tip {
|
||||
font-size: 20rpx;
|
||||
color: #64748B;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-choose-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding-bottom: 24rpx;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
|
||||
.avatar-picker-btn {
|
||||
width: 110rpx;
|
||||
height: 110rpx;
|
||||
border-radius: 55rpx;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
background: transparent;
|
||||
border: none;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.chosen-avatar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 55rpx;
|
||||
border: 4rpx solid #6366F1;
|
||||
}
|
||||
|
||||
.avatar-badge {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
border-radius: 50%;
|
||||
background: #6366F1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2rpx solid #151D30;
|
||||
|
||||
.camera-icon-svg {
|
||||
width: 22rpx;
|
||||
height: 22rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-tip-wrap {
|
||||
flex: 1;
|
||||
|
||||
.avatar-tip-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #F8FAFC;
|
||||
display: block;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
.avatar-tip-sub {
|
||||
font-size: 20rpx;
|
||||
color: #94A3B8;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nickname-input-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-top: 24rpx;
|
||||
|
||||
.input-label {
|
||||
width: 100rpx;
|
||||
font-size: 26rpx;
|
||||
color: #94A3B8;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nickname-input {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
font-size: 26rpx;
|
||||
color: #F8FAFC;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 14rpx;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
|
||||
.ph-style {
|
||||
color: #64748B;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 主操作按钮区 */
|
||||
.main-action-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.btn-wechat-primary {
|
||||
width: 100%;
|
||||
height: 92rpx;
|
||||
border-radius: 46rpx;
|
||||
background: #07C160;
|
||||
color: #FFFFFF;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(7, 193, 96, 0.35);
|
||||
border: none;
|
||||
margin: 0;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.wechat-icon-svg {
|
||||
width: 38rpx;
|
||||
height: 38rpx;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-guarantee-tip {
|
||||
font-size: 20rpx;
|
||||
color: #64748B;
|
||||
margin-top: 14rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* 体验账号卡片 */
|
||||
.demo-accounts-card {
|
||||
background: #151D30;
|
||||
border-radius: 28rpx;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
padding: 28rpx 28rpx 16rpx;
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.demo-card-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.demo-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #F8FAFC;
|
||||
}
|
||||
.demo-sub {
|
||||
font-size: 20rpx;
|
||||
color: #64748B;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
|
||||
.demo-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
border-radius: 20rpx;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
|
||||
.demo-avatar {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 36rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.demo-info {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.demo-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 4rpx;
|
||||
|
||||
.demo-name {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #F8FAFC;
|
||||
}
|
||||
|
||||
.demo-badge {
|
||||
font-size: 18rpx;
|
||||
padding: 2rpx 10rpx;
|
||||
border-radius: 6rpx;
|
||||
font-weight: 600;
|
||||
|
||||
&.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-feature {
|
||||
font-size: 20rpx;
|
||||
color: #94A3B8;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-switch-account {
|
||||
font-size: 22rpx;
|
||||
color: #6366F1;
|
||||
font-weight: 600;
|
||||
padding: 8rpx 18rpx;
|
||||
border-radius: 12rpx;
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
border: none;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 协议勾选 */
|
||||
.protocol-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
margin-top: auto;
|
||||
padding: 16rpx 0;
|
||||
|
||||
.checkbox {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 8rpx;
|
||||
border: 2px solid #64748B;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.checked {
|
||||
background: #07C160;
|
||||
border-color: #07C160;
|
||||
|
||||
.checkbox-inner {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
background: #FFFFFF;
|
||||
border-radius: 3rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.protocol-text {
|
||||
font-size: 22rpx;
|
||||
color: #94A3B8;
|
||||
line-height: 1.5;
|
||||
|
||||
.protocol-link {
|
||||
color: #07C160;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 浅色暖阳模式适配 */
|
||||
.theme-warm {
|
||||
background-color: #FAF8F5;
|
||||
|
||||
.brand-header {
|
||||
.brand-app-name {
|
||||
color: #1C1917;
|
||||
}
|
||||
.brand-slogan {
|
||||
color: #78716C;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-profile-card {
|
||||
background: #FFFFFF;
|
||||
border-color: #E7E5E4;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||
|
||||
.profile-card-header {
|
||||
.profile-card-title {
|
||||
color: #1C1917;
|
||||
}
|
||||
.profile-card-tip {
|
||||
color: #A8A29E;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-choose-row {
|
||||
border-bottom-color: #F5F5F4;
|
||||
|
||||
.avatar-picker-btn .chosen-avatar {
|
||||
border-color: #EA580C;
|
||||
}
|
||||
.avatar-picker-btn .avatar-badge {
|
||||
background: #EA580C;
|
||||
border-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.avatar-tip-wrap {
|
||||
.avatar-tip-title {
|
||||
color: #1C1917;
|
||||
}
|
||||
.avatar-tip-sub {
|
||||
color: #78716C;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nickname-input-row {
|
||||
.input-label {
|
||||
color: #78716C;
|
||||
}
|
||||
|
||||
.nickname-input {
|
||||
background: #FDFBF7;
|
||||
border-color: #E7E5E4;
|
||||
color: #1C1917;
|
||||
}
|
||||
|
||||
.ph-style {
|
||||
color: #A8A29E;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-action-section {
|
||||
.auth-guarantee-tip {
|
||||
color: #A8A29E;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-accounts-card {
|
||||
background: #FFFFFF;
|
||||
border-color: #E7E5E4;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||
|
||||
.demo-card-head {
|
||||
.demo-title {
|
||||
color: #1C1917;
|
||||
}
|
||||
.demo-sub {
|
||||
color: #A8A29E;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-list .demo-item {
|
||||
background: #FDFBF7;
|
||||
border-color: #E7E5E4;
|
||||
|
||||
.demo-info .demo-name-row .demo-name {
|
||||
color: #1C1917;
|
||||
}
|
||||
|
||||
.demo-info .demo-feature {
|
||||
color: #78716C;
|
||||
}
|
||||
|
||||
.btn-switch-account {
|
||||
background: #FFF7ED;
|
||||
color: #EA580C;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.protocol-section {
|
||||
.checkbox {
|
||||
border-color: #D6D3D1;
|
||||
}
|
||||
|
||||
.protocol-text {
|
||||
color: #78716C;
|
||||
|
||||
.protocol-link {
|
||||
color: #07C160;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -153,49 +153,6 @@
|
||||
<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"
|
||||
open-type="getPhoneNumber"
|
||||
@getphonenumber="handleGetPhoneNumber"
|
||||
@click="handleFallbackLogin"
|
||||
>
|
||||
一键授权快捷登录
|
||||
</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>
|
||||
@@ -377,78 +334,20 @@ const onRoleChanged = (role) => {
|
||||
currentRole.value = role;
|
||||
};
|
||||
|
||||
// 登录/注册已全面重构升级为独立专属页面 /pages/auth/login
|
||||
// 保持向下兼容: switchDemoAccount, login-modal-mask, 一键授权快捷登录
|
||||
const showLoginModal = ref(false);
|
||||
|
||||
const openLoginModal = () => {
|
||||
showLoginModal.value = true;
|
||||
};
|
||||
|
||||
const executeQuickLogin = (targetUid = 1001, phone = '') => {
|
||||
uni.showLoading({ title: '正在登录...' });
|
||||
setTimeout(() => {
|
||||
uni.hideLoading();
|
||||
showLoginModal.value = false;
|
||||
isLoggedIn.value = true;
|
||||
uni.removeStorageSync('user_has_logged_out');
|
||||
uni.setStorageSync('userId', targetUid);
|
||||
loadUserProfile(targetUid);
|
||||
if (phone) {
|
||||
user.value.phone = phone;
|
||||
}
|
||||
uni.showToast({ title: '登录成功', icon: 'success' });
|
||||
}, 400);
|
||||
};
|
||||
|
||||
const handleGetPhoneNumber = (e) => {
|
||||
// #ifdef MP-WEIXIN
|
||||
if (e && e.detail && (e.detail.code || e.detail.encryptedData)) {
|
||||
executeQuickLogin(1001, '138****8888');
|
||||
} else {
|
||||
console.warn('[WeChat Auth] 未获得手机号权限或用户拒绝:', e && e.detail ? e.detail.errMsg : '');
|
||||
uni.showModal({
|
||||
title: '登录提示',
|
||||
content: '未获取到微信手机号授权(需非个人主体认证小程序支持)。是否使用体验账号直接登录?',
|
||||
confirmText: '快捷登录',
|
||||
cancelText: '取消',
|
||||
confirmColor: '#10B981',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
executeQuickLogin(1001);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
executeQuickLogin(1001);
|
||||
// #endif
|
||||
};
|
||||
|
||||
const handleFallbackLogin = () => {
|
||||
// #ifndef MP-WEIXIN
|
||||
executeQuickLogin(1001);
|
||||
// #endif
|
||||
};
|
||||
|
||||
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'
|
||||
uni.navigateTo({
|
||||
url: '/pages/auth/login'
|
||||
});
|
||||
};
|
||||
|
||||
const onLoginSuccess = (payload) => {
|
||||
loadUserProfile(payload?.userId);
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
uni.showModal({
|
||||
title: '确认退出登录?',
|
||||
@@ -523,6 +422,7 @@ onMounted(() => {
|
||||
applyThemeToNativeUI(currentThemeKey.value);
|
||||
uni.$on('theme-change', onThemeChanged);
|
||||
uni.$on('role-change', onRoleChanged);
|
||||
uni.$on('login-success', onLoginSuccess);
|
||||
loadUserProfile();
|
||||
loadPlatformConfigs();
|
||||
});
|
||||
@@ -540,6 +440,7 @@ onShow(() => {
|
||||
onUnmounted(() => {
|
||||
uni.$off('theme-change', onThemeChanged);
|
||||
uni.$off('role-change', onRoleChanged);
|
||||
uni.$off('login-success', onLoginSuccess);
|
||||
});
|
||||
|
||||
const switchThemeTo = (targetKey) => {
|
||||
@@ -1743,150 +1644,6 @@ const navTo = (url) => {
|
||||
}
|
||||
}
|
||||
|
||||
.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);
|
||||
border: none;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
@@ -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(14);
|
||||
expect(config.pages.length).toBe(15);
|
||||
|
||||
for (const pageItem of config.pages) {
|
||||
const vueFilePath = path.join(rootDir, 'pwuniapp', `${pageItem.path}.vue`);
|
||||
@@ -377,20 +377,17 @@ describe('ContentSecurityService & Compliance Architecture (TDD 微信内容安
|
||||
});
|
||||
|
||||
describe('10. 个人中心未登录游客态、退出登录及切换账号门禁验证 (用户体验闭环与安全防范)', () => {
|
||||
it('个人中心页面必须包含退出登录、切换账号与登录弹窗入口', () => {
|
||||
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('/pages/auth/login');
|
||||
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('一键授权快捷登录');
|
||||
expect(content).toContain('open-type="getPhoneNumber"');
|
||||
expect(content).toContain('@getphonenumber="handleGetPhoneNumber"');
|
||||
expect(content).toContain('立即登录 / 注册');
|
||||
});
|
||||
|
||||
it('个人中心核心交互必须包含未登录拦截守护 ensureLoggedIn', () => {
|
||||
@@ -445,6 +442,38 @@ describe('ContentSecurityService & Compliance Architecture (TDD 微信内容安
|
||||
expect(emojiRegex.test(content)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('12. 微信小程序官方标准登录/注册专属独立页面验证 (pages/auth/login.vue)', () => {
|
||||
it('pages.json 必须成功注册 pages/auth/login 且物理文件必须存在', () => {
|
||||
const loginPath = path.join(rootDir, 'pwuniapp', 'pages', 'auth', 'login.vue');
|
||||
expect(fs.existsSync(loginPath)).toBe(true);
|
||||
const pagesJsonPath = path.join(rootDir, 'pwuniapp', 'pages.json');
|
||||
const pagesJson = fs.readFileSync(pagesJsonPath, 'utf-8');
|
||||
expect(pagesJson).toContain('pages/auth/login');
|
||||
expect(pagesJson).toContain('账号登录与注册');
|
||||
});
|
||||
|
||||
it('登录页面必须符合微信官方头像昵称与手机号快捷授权标准', () => {
|
||||
const loginPath = path.join(rootDir, 'pwuniapp', 'pages', 'auth', 'login.vue');
|
||||
const content = fs.readFileSync(loginPath, 'utf-8');
|
||||
expect(content).toContain('open-type="chooseAvatar"');
|
||||
expect(content).toContain('@chooseavatar="onChooseAvatar"');
|
||||
expect(content).toContain('type="nickname"');
|
||||
expect(content).toContain('open-type="getPhoneNumber"');
|
||||
expect(content).toContain('@getphonenumber="handleGetPhoneNumber"');
|
||||
expect(content).toContain('一键授权快捷登录');
|
||||
expect(content).toContain('DEMO_ACCOUNTS');
|
||||
expect(content).toContain('用户服务协议');
|
||||
expect(content).toContain('隐私保护指引');
|
||||
});
|
||||
|
||||
it('登录注册页面代码严禁包含任何系统 Emoji', () => {
|
||||
const loginPath = path.join(rootDir, 'pwuniapp', 'pages', 'auth', 'login.vue');
|
||||
const content = fs.readFileSync(loginPath, 'utf-8');
|
||||
const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u;
|
||||
expect(emojiRegex.test(content)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户