增加小程序,修改游戏前端

This commit is contained in:
2026-02-04 17:29:51 +08:00
parent de17737ca1
commit 8c677908d7
835 changed files with 283328 additions and 2165 deletions

View File

@@ -0,0 +1,4 @@
{
"usingComponents": {
}
}

View File

@@ -0,0 +1,679 @@
// profile.ts (Copied from previous index.ts)
import { config } from '../../config';
import { remoteConfig } from '../../utils/remoteConfig';
// 获取应用实例
const app = getApp<IAppOption>()
Component({
data: {
motto: 'Hello World',
loginHint: config.loginHint,
userInfo: {
avatarUrl: config.defaultAvatarUrl,
nickName: '',
gender: 0, // 0:未知, 1:男, 2:女
country: '',
province: '',
city: '',
language: ''
},
openid: '',
unionid: '',
phoneNumber: '',
playerid: '',
verificationCode: '',
hasUserInfo: false,
authMode: config.authMode || 'oa', // 'oa' | 'mp'
isTestEnabled: config.testConfig && config.testConfig.enable,
canIUseGetUserProfile: wx.canIUse('getUserProfile'),
canIUseNicknameComp: wx.canIUse('input.type.nickname'),
},
pageLifetimes: {
show() {
// 检查是否有从公众号授权回来的数据
const oaUserInfo = wx.getStorageSync('oa_user_info');
if (oaUserInfo) {
console.log('检测到公众号授权数据:', oaUserInfo);
this.setData({
"userInfo.nickName": oaUserInfo.nickName,
"userInfo.avatarUrl": oaUserInfo.avatarUrl,
"userInfo.gender": oaUserInfo.gender,
"userInfo.country": oaUserInfo.country,
"userInfo.province": oaUserInfo.province,
"userInfo.city": oaUserInfo.city,
"userInfo.language": oaUserInfo.language,
// 如果公众号也返回了 unionid可以更新
unionid: oaUserInfo.unionid || this.data.unionid
});
// 清除缓存,避免重复读取
wx.removeStorageSync('oa_user_info');
// 如果已经有手机号,说明是“先手机后头像”的流程,直接保存并进入
if (this.data.phoneNumber) {
this.saveUserInfoToBackend();
} else {
// 授权回来后,尝试登录游戏服务器获取信息
this.loginToServer();
}
}
}
},
lifetimes: {
attached() {
// 检查本地登录态
this.checkLocalSession();
}
},
methods: {
// 检查本地 Session
checkLocalSession() {
const session = wx.getStorageSync('USER_SESSION');
const now = Date.now();
// 默认 24 小时过期
const expirationHours = config.loginExpirationHours || 24;
const expirationTime = expirationHours * 60 * 60 * 1000;
if (session && session.timestamp && (now - session.timestamp < expirationTime)) {
console.log('恢复本地登录态');
this.setData({
userInfo: session.userInfo,
openid: session.openid,
unionid: session.unionid,
phoneNumber: session.phoneNumber,
playerid: session.playerid,
// 始终保持 hasUserInfo 为 false以显示用户信息和操作按钮
hasUserInfo: false
});
// 恢复后,尝试静默刷新服务端状态(不阻塞 UI
this.loginToServer();
} else {
console.log('本地登录态不存在或已过期');
wx.removeStorageSync('USER_SESSION');
// 页面加载时自动静默登录获取 OpenID但不自动登录游戏服务器等待用户点击登录按钮授权
this.performSilentLogin(false);
}
},
// 弹出设置菜单
onSettingsTap() {
wx.showActionSheet({
itemList: ['注销账号'],
itemColor: '#FF0000', // 红色显示注销
success: (res) => {
if (res.tapIndex === 0) {
this.handleLogout();
}
}
});
},
// 处理注销
handleLogout() {
wx.showModal({
title: '提示',
content: '确定要注销当前账号吗?',
success: (res) => {
if (res.confirm) {
// 1. 清除本地存储
wx.removeStorageSync('USER_SESSION'); // 清除持久化会话
wx.removeStorageSync('oa_user_info'); // 清除可能的 OA 授权信息
// 2. 重置页面数据
this.setData({
userInfo: {
avatarUrl: config.defaultAvatarUrl,
nickName: '',
gender: 0,
country: '',
province: '',
city: '',
language: ''
},
openid: '',
unionid: '',
phoneNumber: '',
playerid: '',
verificationCode: '',
// 重置其他相关状态...
});
wx.showToast({
title: '已注销',
icon: 'success'
});
// 3. 重新执行静默登录以获取基础 OpenID (保持未登录状态)
this.performSilentLogin(false);
}
}
});
},
// 选择头像 (小程序原生)
onChooseAvatar(e: any) {
const { avatarUrl } = e.detail;
this.setData({
"userInfo.avatarUrl": avatarUrl
});
},
// 填写昵称 (小程序原生)
onNicknameChange(e: any) {
const nickName = e.detail.value;
this.setData({
"userInfo.nickName": nickName
});
},
// 跳转到公众号授权页面
goOfficialAccountAuth() {
// 如果是 mp 模式,或者测试模式下配置了 Mock UnionID则跳过公众号授权直接尝试登录
if (config.authMode === 'mp' || (config.testConfig && config.testConfig.enable && config.testConfig.mockUnionId)) {
console.log('跳过公众号授权 (模式: ' + config.authMode + ', 测试: ' + (config.testConfig && config.testConfig.enable) + ')');
this.loginToServer();
return;
}
wx.navigateTo({
url: '/pages/webview/webview'
});
},
// 事件处理函数
bindViewTap() {
wx.navigateTo({
url: '../logs/logs',
})
},
// 静默登录逻辑
performSilentLogin(autoLoginGameServer: boolean = true) {
wx.login({
success: (loginRes) => {
if (loginRes.code) {
console.log('静默登录 code:', loginRes.code);
// 请求本地后端
wx.request({
url: `${config.baseUrl}/api/login`,
method: 'POST',
data: { code: loginRes.code },
success: (res: any) => {
if (res.data.error) {
console.error('静默登录失败:', res.data.error);
return;
}
this.setData({
openid: res.data.openid,
unionid: res.data.unionid || '未获取到UnionID'
});
console.log('OpenID 静默获取成功',res.data);
// 获取到 openid/unionid 后,尝试登录游戏服务器获取玩家信息
if (autoLoginGameServer) {
this.loginToServer();
}
},
fail: (err) => {
console.error('静默登录连接失败:', err);
}
});
}
}
});
},
// 登录游戏服务器
loginToServer() {
// 检查远程配置是否就绪
if (!remoteConfig.isReady()) {
console.log('RemoteConfig not ready, waiting...');
wx.showLoading({ title: '加载配置中...', mask: true });
let hasHandled = false;
remoteConfig.onUpdate(() => {
if (!hasHandled && remoteConfig.isReady()) {
hasHandled = true;
wx.hideLoading();
this.loginToServer();
}
});
return;
}
const { openid, unionid, userInfo } = this.data;
if (!openid) return;
// --- 测试配置注入 ---
let finalUnionId = unionid;
if (config.testConfig && config.testConfig.enable && config.testConfig.mockUnionId) {
console.log('[Test] Using Mock UnionID in loginToServer:', config.testConfig.mockUnionId);
finalUnionId = config.testConfig.mockUnionId;
}
// -------------------
// 获取版本号
const { agentid, gameid, channelid, marketid } = config.remoteConfig;
const version = remoteConfig.getParaValue("game_version", agentid, gameid, channelid, marketid);
console.log("game_version",version);
wx.request({
url: `${config.baseUrl}/api/playerLogin`,
method: 'POST',
data: {
openid,
unionid: finalUnionId,
nickName: userInfo.nickName,
avatarUrl: userInfo.avatarUrl,
gender: userInfo.gender,
province: userInfo.province,
city: userInfo.city,
version: version || 1 // 默认版本号 1
},
success: (res: any) => {
this.handleLoginSuccess(res);
},
fail: (err) => {
console.error('游戏服务器登录失败:', err);
wx.showToast({ title: '连接服务器失败', icon: 'none' });
}
});
},
// 测试登录(带手机号和验证码)
testLoginWithPhone() {
const { openid, unionid, userInfo, phoneNumber, verificationCode } = this.data;
if (!openid) {
wx.showToast({ title: '未获取OpenID', icon: 'none' });
return;
}
if (!phoneNumber) {
wx.showToast({ title: '未获取手机号', icon: 'none' });
return;
}
if (!verificationCode) {
wx.showToast({ title: '未生成随机数', icon: 'none' });
return;
}
// --- 测试配置注入 ---
let finalUnionId = unionid;
if (config.testConfig && config.testConfig.enable && config.testConfig.mockUnionId) {
finalUnionId = config.testConfig.mockUnionId;
}
// -------------------
const { agentid, gameid, channelid, marketid } = config.remoteConfig;
const version = remoteConfig.getParaValue("game_version", agentid, gameid, channelid, marketid);
wx.showLoading({ title: '测试登录中...' });
wx.request({
url: `${config.baseUrl}/api/playerLogin`,
method: 'POST',
// data: {
// openid,
// unionid: finalUnionId,
// nickName: userInfo.nickName,
// avatarUrl: userInfo.avatarUrl,
// gender: userInfo.gender,
// province: userInfo.province,
// city: userInfo.city,
// version: version || 1,
// phoneNumber: phoneNumber,
// verificationCode: verificationCode
// },
data: {
openid:"onJdG10JeHtS0Dbz8FtdVv7aeVB6",
unionid: "oLVKis6bj3_l8qspMybG60KV2GN6",
nickName: "testname",
avatarUrl: "testavatarurl",
gender: 3,
province: "testprovince",
city: "testcity",
version: version || 1,
phoneNumber: phoneNumber,
verificationCode: 11111,
telphoneAuto:true,
playerid:710873
},
success: (res: any) => {
wx.hideLoading();
console.log('测试登录返回:', res.data);
this.handleLoginSuccess(res);
wx.showToast({ title: '测试登录请求已发送', icon: 'none' });
},
fail: (err) => {
wx.hideLoading();
console.error('测试登录失败:', err);
wx.showToast({ title: '测试登录失败', icon: 'none' });
}
});
},
handleLoginSuccess(res: any) {
console.log('游戏服务器登录返回:', res.data);
// res.data 是 wxserver 返回的 { success, message, data }
// res.data.data 是游戏服务器返回的原始包 { rpc, data }
const packet = res.data;
if (packet) {
// 1. 处理异常 RPC (show_message, kick_server)
if (packet.rpc === 'show_message' || packet.rpc === 'kick_server') {
const msg = (packet.data && packet.data.msg) || '未知错误';
wx.showToast({
title: msg,
icon: 'none',
duration: 3000
});
return;
}
// 2. 处理业务错误
if (packet.data && packet.data.error) {
wx.showToast({
title: packet.data.error,
icon: 'none',
duration: 3000
});
return;
}
// 3. 处理登录成功
const gameData = packet.data;
if (gameData && (gameData.playerid || gameData.state === 0)) {
// 兼容不同的字段名
const serverNickName = gameData.nickname || gameData.nickName;
const serverAvatarUrl = gameData.avatar || gameData.headimg || gameData.headimgurl || gameData.avatarUrl;
const serverGender = gameData.sex || gameData.gender;
const newUserInfo = {
...this.data.userInfo,
nickName: serverNickName || this.data.userInfo.nickName,
avatarUrl: serverAvatarUrl || this.data.userInfo.avatarUrl,
gender: serverGender !== undefined ? serverGender : this.data.userInfo.gender
};
this.setData({
userInfo: newUserInfo,
playerid: gameData.playerid || '',
phoneNumber: gameData.tel || ''
});
// 更新本地缓存
wx.setStorageSync('USER_SESSION', {
timestamp: Date.now(),
userInfo: newUserInfo,
openid: this.data.openid,
unionid: this.data.unionid,
phoneNumber: gameData.tel || '',
playerid: gameData.playerid || ''
});
}
}
},
// 获取验证码
getVerificationCode() {
// if (this.data.isCountingDown) return;
const { phoneNumber } = this.data;
if (!phoneNumber) {
wx.showToast({ title: '请先绑定手机号', icon: 'none' });
return;
}
wx.showLoading({ title: '获取中...' });
wx.request({
url: `${config.baseUrl}/api/getPhoneCode`,
method: 'POST',
data: { phonenum: phoneNumber },
success: (res: any) => {
wx.hideLoading();
console.log('验证码返回:', res.data);
const smmcode = res.data && res.data.data && res.data.data.smmcode;
if (smmcode) {
this.setData({ verificationCode: smmcode });
wx.showToast({ title: '获取成功', icon: 'success' });
} else {
wx.showToast({ title: '获取失败', icon: 'none' });
}
},
fail: (err) => {
wx.hideLoading();
console.error('获取验证码请求失败:', err);
wx.showToast({ title: '网络错误', icon: 'none' });
}
});
},
// 复制验证码
copyVerificationCode() {
const { verificationCode } = this.data;
if (!verificationCode) return;
wx.setClipboardData({
data: String(verificationCode),
success: () => {
wx.showToast({ title: '复制成功', icon: 'success' });
}
});
},
getPhoneNumber(e: any) {
// const { avatarUrl, nickName } = this.data.userInfo;
const { openid } = this.data;
// 1. 检查 OpenID 是否已获取
if (!openid) {
wx.showToast({ title: '正在初始化...', icon: 'none' });
// 尝试重新登录
this.performSilentLogin();
return;
}
// 2. 获取手机号
if (e.detail.errMsg === "getPhoneNumber:ok" || e.detail.errMsg.includes("ok")) {
// --- 测试配置Mock 手机号 ---
// 原因:微信小程序获取手机号接口收费。在测试阶段,如果配置了 mockPhoneNumber
// 则直接使用模拟数据,跳过后端请求,从而避免产生费用。
if (config.testConfig && config.testConfig.enable && config.testConfig.mockPhoneNumber) {
console.log('[Test] Using Mock PhoneNumber directly:', config.testConfig.mockPhoneNumber);
this.handlePhoneNumberSuccess(config.testConfig.mockPhoneNumber);
return;
}
// ---------------------------
const code = e.detail.code; // 动态令牌
wx.showLoading({ title: '获取手机号...' });
// 请求本地后端获取手机号
wx.request({
url: `${config.baseUrl}/api/getPhoneNumber`,
method: 'POST',
data: { code: code },
success: (res: any) => {
wx.hideLoading();
console.log('后端返回:', res.data);
if (res.data.phoneNumber) {
this.handlePhoneNumberSuccess(res.data.phoneNumber);
} else {
wx.showToast({ title: '获取失败: ' + (res.data.error || '未知错误'), icon: 'none' });
}
},
fail: (err) => {
wx.hideLoading();
console.error('请求后端接口失败:', err);
wx.showToast({ title: '连接服务器失败', icon: 'none' });
}
});
} else {
console.error(e.detail);
wx.showModal({
title: '获取失败',
content: '用户拒绝授权或账号无权限。',
showCancel: false
})
}
},
// 抽离处理手机号成功的逻辑
handlePhoneNumberSuccess(phoneNumber: string) {
this.setData({
phoneNumber: phoneNumber
});
// 3. 检查是否需要同步头像或获取真实UnionID
// 如果没有昵称、头像是默认的或者没有有效的UnionID自动跳转去同步
const hasValidUnionId = this.data.unionid && this.data.unionid !== '未获取到UnionID';
// 在 mp 模式下,我们不强制检查 unionid因为 chooseAvatar 不会返回 unionid
// 但如果用户需要 unionid可能需要其他方式。这里主要关注头像昵称。
const isInfoMissing = !this.data.userInfo.nickName ||
this.data.userInfo.avatarUrl === config.defaultAvatarUrl ||
(this.data.authMode === 'oa' && !hasValidUnionId);
if (isInfoMissing) {
if (this.data.authMode === 'oa') {
wx.showToast({ title: '正在同步完善信息...', icon: 'none', duration: 1500 });
setTimeout(() => {
this.goOfficialAccountAuth();
}, 1500);
} else {
// mp 模式,提示用户手动填写
wx.showToast({ title: '请完善头像和昵称', icon: 'none' });
}
} else {
// 已经有头像了,直接保存
this.saveUserInfoToBackend();
wx.showToast({ title: '登录成功' });
}
},
saveUserInfoToBackend() {
const { userInfo, openid, unionid, phoneNumber } = this.data;
// --- 测试配置注入 ---
let finalUnionId = unionid;
let finalPhoneNumber = phoneNumber;
if (config.testConfig && config.testConfig.enable) {
if (config.testConfig.mockUnionId) {
console.log('[Test] Using Mock UnionID:', config.testConfig.mockUnionId);
finalUnionId = config.testConfig.mockUnionId;
}
if (config.testConfig.mockPhoneNumber) {
console.log('[Test] Using Mock PhoneNumber:', config.testConfig.mockPhoneNumber);
finalPhoneNumber = config.testConfig.mockPhoneNumber;
}
}
// -------------------
const save = (finalAvatarUrl: string) => {
wx.request({
url: `${config.baseUrl}/api/saveUserInfo`,
method: 'POST',
data: {
...userInfo, // 包含 nickName, gender, country, province, city, language 等所有字段
avatarUrl: finalAvatarUrl, // 覆盖可能为临时路径的 avatarUrl
openid,
unionid: finalUnionId,
phoneNumber: finalPhoneNumber
},
success: (res: any) => {
console.log('用户信息保存成功:', res.data);
const serverData = res.data;
// 如果游戏服务器明确返回 result: -1 (例如手机号已存在),则阻止跳转
// wxserver 返回结构: { success: boolean, message: string, data: { data: { result: number, msg: string } } }
// 兼容直接返回的情况
const gamePacket = serverData.data;
const gameResult = (gamePacket && gamePacket.data && gamePacket.data.result !== undefined) ? gamePacket.data.result : ((gamePacket && gamePacket.result !== undefined) ? gamePacket.result : serverData.result);
if (serverData.success === false || gameResult === -1) {
// 优先显示 serverData.data.message其次是 msg
const errorMsg = serverData.message || (gamePacket && gamePacket.data && gamePacket.data.msg) || (gamePacket && gamePacket.msg) || '操作失败';
wx.showToast({
title: errorMsg,
icon: 'none',
duration: 4000
});
return;
}
// 优先取游戏服务器返回的 msg (在 data.data.msg 中),其次取 wxserver 的 message
const displayMsg = (gamePacket && gamePacket.data && gamePacket.data.msg) || (gamePacket && gamePacket.msg) || serverData.message;
if (displayMsg) {
wx.showToast({
title: displayMsg,
icon: 'none',
duration: 5000
});
}
// 保存到本地缓存
wx.setStorageSync('USER_SESSION', {
timestamp: Date.now(),
userInfo: this.data.userInfo,
openid: this.data.openid,
unionid: this.data.unionid,
phoneNumber: this.data.phoneNumber,
playerid: this.data.playerid
});
},
fail: (err) => {
console.error('保存用户信息失败:', err);
}
});
};
// 检查是否需要上传头像 (如果是临时路径)
// 只有在开启了上传配置,且头像是临时路径时才上传
const shouldUpload = config.enableAvatarUpload &&
userInfo.avatarUrl &&
(userInfo.avatarUrl.startsWith('http://tmp') || userInfo.avatarUrl.startsWith('wxfile://'));
if (shouldUpload) {
wx.showLoading({ title: '上传头像中...' });
wx.uploadFile({
url: `${config.baseUrl}/api/upload`,
filePath: userInfo.avatarUrl,
name: 'file',
formData: {
unionid: finalUnionId || openid // 优先使用 unionid如果没有则使用 openid
},
success: (res) => {
wx.hideLoading();
try {
const data = JSON.parse(res.data);
if (data.url) {
console.log('头像上传成功:', data.url);
save(data.url);
} else {
console.error('头像上传响应异常:', data);
// 降级处理:上传失败也尝试保存,虽然头像链接可能无效
save(userInfo.avatarUrl);
}
} catch (e) {
console.error('解析上传响应失败:', e);
save(userInfo.avatarUrl);
}
},
fail: (err) => {
wx.hideLoading();
console.error('头像上传失败:', err);
save(userInfo.avatarUrl);
}
});
} else {
// 已经是网络链接或默认头像,直接保存
save(userInfo.avatarUrl);
}
},
},
})

View File

@@ -0,0 +1,85 @@
<!--pages/profile/profile.wxml-->
<scroll-view class="scrollarea" scroll-y type="list">
<view class="container">
<!-- 用户信息卡片 -->
<view class="user-card">
<view class="settings-btn" bindtap="onSettingsTap" wx:if="{{playerid}}">
<text class="settings-icon">⚙</text>
</view>
<block wx:if="{{!hasUserInfo}}">
<!-- 头像展示 -->
<view class="avatar-wrapper">
<!-- mp 模式:可点击选择头像 -->
<button wx:if="{{authMode === 'mp'}}" class="avatar-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
<image class="avatar" src="{{userInfo.avatarUrl}}"></image>
</button>
<!-- oa 模式:只读 -->
<image wx:else class="avatar" src="{{userInfo.avatarUrl}}"></image>
</view>
<!-- 昵称展示 -->
<view class="nickname-wrapper">
<!-- mp 模式:输入框 -->
<input wx:if="{{authMode === 'mp'}}" type="nickname" class="nickname-input" placeholder="请输入昵称" value="{{userInfo.nickName}}" bindchange="onNicknameChange"/>
<!-- oa 模式:文本 -->
<text wx:else class="nickname-text">{{userInfo.nickName || '授权获取微信信息'}}</text>
</view>
<!-- 玩家详细信息展示 -->
<view wx:if="{{playerid}}" class="info-grid">
<view class="info-item">
<text class="label">ID</text>
<text class="value">{{playerid}}</text>
</view>
<view class="info-item">
<text class="label">性别</text>
<view class="gender-box {{userInfo.gender == 2 ? 'female' : 'male'}}">
<text class="gender-icon">{{userInfo.gender == 2 ? '♀' : '♂'}}</text>
<text>{{userInfo.gender == 2 ? '女' : '男'}}</text>
</view>
</view>
<view class="info-item">
<text class="label">手机</text>
<text class="value" wx:if="{{phoneNumber}}">{{phoneNumber}}</text>
<text class="value" wx:else style="color: #fa5151; font-weight: bold;">未绑定</text>
</view>
</view>
</block>
</view>
<!-- 操作区域 -->
<view class="action-area">
<!-- 提示文字 -->
<view class="login-hint">{{loginHint}}</view>
<block wx:if="{{!hasUserInfo}}">
<!-- 未登录/未授权状态:显示登录按钮 -->
<block wx:if="{{!playerid}}">
<button class="login-btn" bindtap="goOfficialAccountAuth">微信一键登录</button>
</block>
<!-- 已授权状态:显示后续操作 -->
<block wx:else>
<!-- 手机号按钮 -->
<button wx:if="{{!phoneNumber}}" class="login-btn" open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber">绑定手机号</button>
<block wx:else>
<!-- 验证码区域 -->
<view class="verification-area">
<button class="login-btn" bindtap="getVerificationCode">生成随机数</button>
<view wx:if="{{verificationCode}}" class="code-display">
<text class="code-text">{{verificationCode}}</text>
<view class="copy-btn" bindtap="copyVerificationCode">复制</view>
</view>
</view>
<button wx:if="{{isTestEnabled}}" class="secondary-btn" bindtap="testLoginWithPhone" style="margin-top: 20rpx;">测试登录(带验证码)</button>
</block>
</block>
</block>
</view>
</view>
</scroll-view>

View File

@@ -0,0 +1,203 @@
/**pages/profile/profile.wxss**/
page {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #f6f7f9;
}
.scrollarea {
flex: 1;
}
.container {
padding: 30rpx;
display: flex;
flex-direction: column;
align-items: center;
}
/* User Card Wrapper */
.user-card {
width: 100%;
background-color: #ffffff;
border-radius: 24rpx;
padding: 40rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.03);
margin-bottom: 30rpx;
position: relative;
}
.settings-btn {
position: absolute;
top: 20rpx;
right: 20rpx; /* Move to right */
left: auto;
padding: 20rpx;
}
.settings-icon {
font-size: 40rpx;
color: #333;
}
/* Avatar */
.avatar-wrapper {
margin-bottom: 30rpx;
position: relative;
}
.avatar-btn {
padding: 0;
width: 160rpx !important;
height: 160rpx !important;
border-radius: 50%;
background: none;
border: none;
box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.1);
}
.avatar {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
border: 4rpx solid #fff;
}
/* Nickname */
.nickname-wrapper {
margin-bottom: 20rpx;
width: 100%;
display: flex;
justify-content: center;
}
.nickname-input, .nickname-text {
font-size: 36rpx;
font-weight: bold;
color: #333;
text-align: center;
}
.nickname-input {
border-bottom: 2rpx solid #eee;
padding: 10rpx;
width: 60%;
}
/* Info Grid / Cell List */
.info-grid {
width: 100%;
display: flex;
flex-direction: column;
margin-top: 20rpx;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
.info-item:last-child {
border-bottom: none;
}
.label {
color: #666;
font-size: 28rpx;
}
.value {
color: #333;
font-size: 28rpx;
font-weight: 500;
}
/* Gender Tags */
.gender-box {
display: flex;
align-items: center;
padding: 4rpx 12rpx;
border-radius: 8rpx;
font-size: 24rpx;
}
.gender-box.male { color: #1890ff; background: #e6f7ff; }
.gender-box.female { color: #eb2f96; background: #fff0f6; }
/* Action Buttons */
.action-area {
width: 100%;
}
.login-hint {
color: #999;
font-size: 26rpx; /* 小一点 */
text-align: center;
margin-bottom: 30rpx;
display: block; /* 确保占满一行 */
}
.login-btn, .secondary-btn {
width: 100% !important;
border-radius: 50rpx !important;
font-weight: bold;
padding: 24rpx 0;
font-size: 30rpx;
margin-bottom: 30rpx;
}
.login-btn {
background: linear-gradient(90deg, #07c160, #10ad63);
color: white;
box-shadow: 0 6rpx 16rpx rgba(7, 193, 96, 0.3);
}
.login-btn[disabled] {
background: #a0eac4 !important;
color: #fff !important;
box-shadow: none;
}
.secondary-btn {
background-color: #fff;
color: #666;
border: 1rpx solid #eee;
}
/* Styles for verification area centered */
.verification-area {
width: 100%;
}
.code-display {
background: #eef2f5;
padding: 20rpx;
border-radius: 12rpx;
margin-top: 20rpx;
display: flex;
justify-content: center;
align-items: center;
}
.code-text {
font-size: 40rpx;
font-weight: bold;
color: #333;
margin-right: 20rpx;
letter-spacing: 4rpx;
}
.copy-btn {
font-size: 24rpx;
background: #fff;
padding: 6rpx 16rpx;
border-radius: 20rpx;
color: #1890ff;
border: 1rpx solid #1890ff;
}