目录结构调整

This commit is contained in:
2026-02-04 23:47:45 +08:00
parent 6938c911c3
commit 6b22238c6e
8780 changed files with 15333 additions and 574 deletions

View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "计算器"
}

View File

@@ -0,0 +1,126 @@
// pages/calculator/calculator.ts
Page({
data: {
displayValue: '0',
history: '',
operator: null as string | null,
firstOperand: null as number | null,
waitingForSecondOperand: false,
},
onLoad() {
},
onDigit(e: any) {
const digit = e.currentTarget.dataset.digit;
const { displayValue, waitingForSecondOperand } = this.data;
if (waitingForSecondOperand) {
this.setData({
displayValue: digit,
waitingForSecondOperand: false
});
} else {
this.setData({
displayValue: displayValue === '0' ? digit : displayValue + digit
});
}
},
onDot() {
const { displayValue, waitingForSecondOperand } = this.data;
if (waitingForSecondOperand) {
this.setData({
displayValue: '0.',
waitingForSecondOperand: false
});
} else if (displayValue.indexOf('.') === -1) {
this.setData({
displayValue: displayValue + '.'
});
}
},
onClear() {
this.setData({
displayValue: '0',
history: '',
operator: null,
firstOperand: null,
waitingForSecondOperand: false
});
},
onDelete() {
const { displayValue } = this.data;
this.setData({
displayValue: displayValue.length > 1 ? displayValue.slice(0, -1) : '0'
});
},
onOperator(e: any) {
const nextOperator = e.currentTarget.dataset.op;
const { displayValue, operator, firstOperand } = this.data;
const inputValue = parseFloat(displayValue);
if (operator && this.data.waitingForSecondOperand) {
this.setData({
operator: nextOperator,
history: `${firstOperand} ${nextOperator}`
});
return;
}
let newFirstOperand = firstOperand;
if (firstOperand == null) {
newFirstOperand = inputValue;
} else if (operator) {
const result = this.performCalculation(operator, firstOperand, inputValue);
newFirstOperand = result;
this.setData({
displayValue: String(result),
});
}
this.setData({
firstOperand: newFirstOperand,
waitingForSecondOperand: true,
operator: nextOperator,
history: `${newFirstOperand} ${nextOperator}`
});
},
onEqual() {
const { displayValue, operator, firstOperand } = this.data;
const inputValue = parseFloat(displayValue);
if (operator && firstOperand != null) {
const result = this.performCalculation(operator, firstOperand, inputValue);
this.setData({
displayValue: String(result),
firstOperand: null,
operator: null,
waitingForSecondOperand: true,
history: ''
});
}
},
performCalculation(operator: string, firstOperand: number, secondOperand: number) {
switch (operator) {
case '+':
return firstOperand + secondOperand;
case '-':
return firstOperand - secondOperand;
case '*':
return firstOperand * secondOperand;
case '/':
return firstOperand / secondOperand;
case '%':
return firstOperand % secondOperand;
default:
return secondOperand;
}
}
});

View File

@@ -0,0 +1,32 @@
<!--pages/calculator/calculator.wxml-->
<view class="calculator">
<view class="screen">
<view class="history">{{history}}</view>
<view class="result">{{displayValue}}</view>
</view>
<view class="keypad">
<view class="btn operator" bindtap="onClear">C</view>
<view class="btn operator" bindtap="onDelete">DEL</view>
<view class="btn operator" bindtap="onOperator" data-op="%">%</view>
<view class="btn operator" bindtap="onOperator" data-op="/">÷</view>
<view class="btn" bindtap="onDigit" data-digit="7">7</view>
<view class="btn" bindtap="onDigit" data-digit="8">8</view>
<view class="btn" bindtap="onDigit" data-digit="9">9</view>
<view class="btn operator" bindtap="onOperator" data-op="*">×</view>
<view class="btn" bindtap="onDigit" data-digit="4">4</view>
<view class="btn" bindtap="onDigit" data-digit="5">5</view>
<view class="btn" bindtap="onDigit" data-digit="6">6</view>
<view class="btn operator" bindtap="onOperator" data-op="-">-</view>
<view class="btn" bindtap="onDigit" data-digit="1">1</view>
<view class="btn" bindtap="onDigit" data-digit="2">2</view>
<view class="btn" bindtap="onDigit" data-digit="3">3</view>
<view class="btn operator" bindtap="onOperator" data-op="+">+</view>
<view class="btn zero" bindtap="onDigit" data-digit="0">0</view>
<view class="btn" bindtap="onDot">.</view>
<view class="btn equal" bindtap="onEqual">=</view>
</view>
</view>

View File

@@ -0,0 +1,74 @@
/* pages/calculator/calculator.wxss */
page {
height: 100%;
background-color: #f5f5f5;
}
.calculator {
display: flex;
flex-direction: column;
height: 100%;
}
.screen {
flex: 1;
background-color: #333;
color: white;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: flex-end;
padding: 20rpx;
word-break: break-all;
}
.history {
font-size: 40rpx;
color: #aaa;
margin-bottom: 10rpx;
}
.result {
font-size: 80rpx;
font-weight: bold;
}
.keypad {
background-color: #fff;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-gap: 1px;
background-color: #ccc; /* Gap color */
}
.btn {
background-color: #fff;
height: 150rpx;
display: flex;
justify-content: center;
align-items: center;
font-size: 40rpx;
active-color: #eee;
}
.btn:active {
background-color: #eee;
}
.operator {
color: #ff9500;
font-weight: bold;
}
.equal {
background-color: #ff9500;
color: white;
}
.equal:active {
background-color: #e08900;
}
.zero {
grid-column: span 2;
}

View File

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

View File

@@ -0,0 +1,73 @@
// index.ts
import { config } from '../../config';
const app = getApp<IAppOption>()
Component({
data: {
userInfo: {
avatarUrl: config.defaultAvatarUrl,
nickName: '未登录'
},
hasUserInfo: false,
greeting: ''
},
pageLifetimes: {
show() {
this.updateGreeting();
// 每次显示页面时,重新获取最新的用户信息
const session = wx.getStorageSync('USER_SESSION');
if (session && session.userInfo) {
this.setData({
userInfo: session.userInfo,
hasUserInfo: true
});
} else {
this.setData({
userInfo: {
avatarUrl: config.defaultAvatarUrl,
nickName: '点击登录'
},
hasUserInfo: false
});
}
}
},
methods: {
updateGreeting() {
const hour = new Date().getHours();
let greeting = '';
if (hour < 5) greeting = '夜深了,注意休息';
else if (hour < 9) greeting = '早上好,新的一天';
else if (hour < 12) greeting = '上午好';
else if (hour < 14) greeting = '中午好';
else if (hour < 19) greeting = '下午好';
else greeting = '晚上好';
this.setData({ greeting });
},
goToProfile() {
wx.navigateTo({
url: '/pages/profile/profile'
});
},
goToCalculator() {
wx.navigateTo({
url: '/pages/calculator/calculator'
});
},
goToUnitConverter() {
wx.navigateTo({
url: '/pages/unit-converter/unit-converter'
});
},
goToRandomDraw() {
wx.navigateTo({
url: '/pages/random-draw/random-draw'
});
}
}
})

View File

@@ -0,0 +1,69 @@
<!--index.wxml-->
<scroll-view class="scrollarea" scroll-y type="list">
<view class="container">
<!-- 顶部背景区 -->
<view class="header-bg">
<view class="header-content" bindtap="goToProfile">
<view class="text-area">
<text class="greeting">{{greeting}}</text>
<text class="nickname">{{userInfo.nickName}}</text>
</view>
<view class="avatar-wrapper">
<image class="avatar" src="{{userInfo.avatarUrl}}"></image>
</view>
</view>
</view>
<!-- 主要内容区(向上浮动覆盖背景) -->
<view class="main-content">
<view class="section-title">工具箱</view>
<!-- 功能网格 -->
<view class="grid-container">
<!-- 计算器 -->
<view class="grid-item" bindtap="goToCalculator" hover-class="grid-item-hover">
<view class="icon-box bg-gradient-blue">
<text class="icon">🧮</text>
</view>
<view class="text-box">
<text class="label">计算器</text>
<text class="sub-label">日常计算</text>
</view>
</view>
<!-- 单位换算 -->
<view class="grid-item" bindtap="goToUnitConverter" hover-class="grid-item-hover">
<view class="icon-box bg-gradient-green">
<text class="icon">⚖️</text>
</view>
<view class="text-box">
<text class="label">单位换算</text>
<text class="sub-label">长度/重量...</text>
</view>
</view>
<!-- 谁去拿外卖 -->
<view class="grid-item" bindtap="goToRandomDraw" hover-class="grid-item-hover">
<view class="icon-box bg-gradient-purple">
<text class="icon">🎲</text>
</view>
<view class="text-box">
<text class="label">谁去拿外卖</text>
<text class="sub-label">随机抽取幸运儿</text>
</view>
</view>
</view>
<!-- 推广/banner区域 -->
<view class="banner" bindtap="goToProfile" wx:if="{{!hasUserInfo}}">
<view class="banner-text">
<text class="banner-title">点击登录 / 完善信息</text>
<text class="banner-desc">绑定手机号,体验更多云端功能</text>
</view>
<view class="banner-icon">📝</view>
</view>
</view>
</view>
</scroll-view>

View File

@@ -0,0 +1,192 @@
/**index.wxss**/
page {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #f6f7f9;
}
.scrollarea {
flex: 1;
}
.container {
padding: 0;
display: flex;
flex-direction: column;
}
/* 顶部背景区 */
.header-bg {
width: 100%;
height: 380rpx;
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
border-bottom-left-radius: 40rpx;
border-bottom-right-radius: 40rpx;
padding: 40rpx;
box-sizing: border-box;
display: flex;
justify-content: center;
/* 加上一点阴影 */
box-shadow: 0 10rpx 20rpx rgba(79, 172, 254, 0.2);
}
.header-content {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center; /* 垂直居中 */
margin-top: 20rpx;
height: 140rpx; /* 固定高度确保对齐 */
}
.text-area {
display: flex;
flex-direction: column;
justify-content: center;
}
.greeting {
color: rgba(255, 255, 255, 0.9);
font-size: 28rpx;
margin-bottom: 10rpx;
}
.nickname {
color: #ffffff;
font-size: 48rpx;
font-weight: bold;
letter-spacing: 2rpx;
}
.avatar-wrapper {
padding: 6rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
}
.avatar {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background-color: #fff;
border: 4rpx solid #ffffff;
}
/* 主要内容区 */
.main-content {
flex: 1;
width: 100%;
padding: 0 30rpx;
box-sizing: border-box;
margin-top: -80rpx; /* 向上浮动覆盖 Header */
z-index: 10;
padding-bottom: 40rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-left: 10rpx;
margin-bottom: 20rpx;
display: none; /* 卡片式设计不需要标题也行,或保留 */
}
/* 一行两列或三列的卡片 */
.grid-container {
display: flex;
flex-direction: column; /* 垂直排列的卡片列表看起来更像菜单 */
gap: 24rpx;
}
/* 单个卡片样式 */
.grid-item {
background-color: #ffffff;
border-radius: 24rpx;
padding: 30rpx;
display: flex;
align-items: center;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.04);
transition: all 0.2s ease;
}
.grid-item-hover {
transform: scale(0.98);
background-color: #fafafa;
}
.icon-box {
width: 100rpx;
height: 100rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 30rpx;
}
.icon {
font-size: 50rpx;
}
.bg-gradient-blue {
background: linear-gradient(135deg, #e0c3fc 0%, #8ec5fc 100%);
}
.bg-gradient-green {
background: linear-gradient(135deg, #d299c2 0%, #fef9d7 100%);
}
/* 替换为柔和的莫兰迪色或渐变 */
.bg-gradient-blue { background: #e3f2fd; color: #2196f3; }
.bg-gradient-green { background: #e8f5e9; color: #4caf50; }
.bg-gradient-orange { background: #fff3e0; color: #ff9800; }
.bg-gradient-purple { background: #f3e5f5; color: #9c27b0; }
.text-box {
display: flex;
flex-direction: column;
}
.label {
font-size: 32rpx;
color: #333;
font-weight: 600;
margin-bottom: 6rpx;
}
.sub-label {
font-size: 24rpx;
color: #999;
}
/* Banner */
.banner {
margin-top: 40rpx;
background: linear-gradient(to right, #4facfe, #00f2fe);
border-radius: 24rpx;
padding: 30rpx 40rpx;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 8rpx 20rpx rgba(79, 172, 254, 0.3);
}
.banner-text {
display: flex;
flex-direction: column;
}
.banner-title {
color: #fff;
font-weight: bold;
font-size: 30rpx;
margin-bottom: 8rpx;
}
.banner-desc {
color: rgba(255,255,255,0.8);
font-size: 24rpx;
}
.banner-icon {
font-size: 40rpx;
}

View File

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

View File

@@ -0,0 +1,21 @@
// logs.ts
// const util = require('../../utils/util.js')
import { formatTime } from '../../utils/util'
Component({
data: {
logs: [],
},
lifetimes: {
attached() {
this.setData({
logs: (wx.getStorageSync('logs') || []).map((log: string) => {
return {
date: formatTime(new Date(log)),
timeStamp: log
}
}),
})
}
},
})

View File

@@ -0,0 +1,6 @@
<!--logs.wxml-->
<scroll-view class="scrollarea" scroll-y type="list">
<block wx:for="{{logs}}" wx:key="timeStamp" wx:for-item="log">
<view class="log-item">{{index + 1}}. {{log.date}}</view>
</block>
</scroll-view>

View File

@@ -0,0 +1,16 @@
page {
height: 100vh;
display: flex;
flex-direction: column;
}
.scrollarea {
flex: 1;
overflow-y: hidden;
}
.log-item {
margin-top: 20rpx;
text-align: center;
}
.log-item:last-child {
padding-bottom: env(safe-area-inset-bottom);
}

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;
}

View File

@@ -0,0 +1,4 @@
{
"usingComponents": {},
"navigationBarTitleText": "谁去拿外卖"
}

View File

@@ -0,0 +1,84 @@
Page({
data: {
names: [] as string[],
newName: '',
result: '',
isRolling: false
},
onLoad() {
// Initialize with some default placeholder data optionally, or keep empty
// this.setData({ names: ['张三', '李四', '王五'] });
},
onInput(e: any) {
this.setData({ newName: e.detail.value });
},
addName() {
const name = this.data.newName.trim();
if (!name) {
wx.showToast({ title: '请输入名字', icon: 'none' });
return;
}
// Check duplication
if (this.data.names.includes(name)) {
wx.showToast({ title: '名字已存在', icon: 'none' });
return;
}
const names = [...this.data.names, name];
this.setData({ names, newName: '' });
},
removeName(e: any) {
if (this.data.isRolling) return;
const index = e.currentTarget.dataset.index;
const names = [...this.data.names];
names.splice(index, 1);
this.setData({ names });
// If deleted the current result, clear result
if (this.data.names.indexOf(this.data.result) === -1) {
// actually result is a string copy, but if logic requires reset:
// this.setData({ result: '' });
}
},
startDraw() {
if (this.data.isRolling) return;
const names = this.data.names;
if (names.length < 2) {
wx.showToast({ title: '至少需要两个人才能抽取哦', icon: 'none' });
return;
}
this.setData({ isRolling: true, result: '' });
let count = 0;
// Speed up first then slow down? Or simple uniform interval.
// Let's do a simple one first.
let baseInterval = 50;
let totalRolls = 30;
const roll = () => {
const randomIndex = Math.floor(Math.random() * names.length);
this.setData({ result: names[randomIndex] });
count++;
if (count < totalRolls) {
// dynamic interval could be fun, but keeping it simple for now
setTimeout(roll, baseInterval + (count * 5)); // slowing down
} else {
this.setData({ isRolling: false });
wx.vibrateShort({ type: 'heavy' });
}
};
roll();
}
});

View File

@@ -0,0 +1,39 @@
<view class="container">
<view class="header-tip">
<view class="title">🎲 天选打工人</view>
<text class="subtitle">输入名字,看看今天谁去拿外卖</text>
</view>
<view class="input-area">
<input class="input" placeholder="输入名字 (如: 小明)" value="{{newName}}" bindinput="onInput" bindconfirm="addName" />
<view class="btn-add" bindtap="addName">添加</view>
</view>
<view class="list-container">
<view class="list-header">候选名单 ({{names.length}})</view>
<scroll-view scroll-y class="list-area">
<view class="name-list">
<block wx:for="{{names}}" wx:key="*this">
<view class="name-item color-{{index % 5}}">
<text class="name-text">{{item}}</text>
<view class="btn-delete" bindtap="removeName" data-index="{{index}}">×</view>
</view>
</block>
<view wx:if="{{names.length === 0}}" class="empty-tip">
<text class="empty-icon">🍃</text>
<text>还没有候选人,快去添加吧~</text>
</view>
</view>
</scroll-view>
</view>
<view class="result-area">
<view class="result-box {{isRolling ? 'rolling' : ''}}">
<text class="result-label">🎉 天选之人</text>
<text class="result-text">{{result || '?'}}</text>
</view>
<button class="btn-start" hover-class="btn-start-hover" bindtap="startDraw" disabled="{{isRolling || names.length < 2}}">
{{isRolling ? '🤞 随机抽取中...' : '🚀 开始抽取'}}
</button>
</view>
</view>

View File

@@ -0,0 +1,235 @@
page {
background: linear-gradient(180deg, #fce4ec 0%, #f3e5f5 100%);
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
.container {
display: flex;
flex-direction: column;
height: 100vh;
padding: 20px;
box-sizing: border-box;
}
.header-tip {
text-align: center;
margin-bottom: 24px;
}
.title {
font-size: 24px;
font-weight: bold;
color: #880e4f;
margin-bottom: 4px;
}
.subtitle {
font-size: 14px;
color: #ad1457;
opacity: 0.7;
}
.input-area {
display: flex;
margin-bottom: 20px;
background: #fff;
border-radius: 50px;
padding: 6px;
box-shadow: 0 4px 12px rgba(173, 20, 87, 0.1);
border: 2px solid rgba(255,255,255,0.5);
}
.input {
flex: 1;
height: 44px;
padding: 0 20px;
font-size: 16px;
color: #333;
}
.btn-add {
min-width: 80px;
height: 44px;
line-height: 44px;
text-align: center;
background: linear-gradient(135deg, #ec407a, #d81b60);
color: #fff;
border-radius: 40px;
font-size: 15px;
font-weight: bold;
box-shadow: 0 4px 8px rgba(216, 27, 96, 0.3);
transition: opacity 0.2s;
}
.btn-add:active {
opacity: 0.8;
}
.list-container {
flex: 1;
display: flex;
flex-direction: column;
background: rgba(255, 255, 255, 0.6);
border-radius: 20px;
padding: 15px 15px 5px 15px; /* bottom padding smaller because scrollview */
margin-bottom: 20px;
backdrop-filter: blur(10px);
min-height: 0; /* Important for flex child to scroll */
}
.list-header {
font-size: 14px;
color: #888;
margin-bottom: 10px;
padding-left: 5px;
}
.list-area {
flex: 1;
overflow: hidden;
}
.name-list {
display: flex;
flex-wrap: wrap;
align-content: flex-start; /* 防止换行后拉伸 */
gap: 10px;
padding-bottom: 10px;
}
.name-item {
padding: 8px 16px 8px 12px;
border-radius: 20px;
font-size: 14px;
display: flex;
align-items: center;
position: relative;
font-weight: 500;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
animation: popIn 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes popIn {
from { transform: scale(0.8); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
/* 伪随机颜色,根据 .color-0 到 .color-4 */
.color-0 { background: #e3f2fd; color: #1565c0; }
.color-1 { background: #e8f5e9; color: #2e7d32; }
.color-2 { background: #fff3e0; color: #ef6c00; }
.color-3 { background: #f3e5f5; color: #7b1fa2; }
.color-4 { background: #ffebee; color: #c62828; }
.name-text {
margin-right: 18px; /* 给关闭按钮留空间 */
}
.btn-delete {
position: absolute;
right: 6px;
top: 50%;
transform: translateY(-50%);
width: 20px;
height: 20px;
line-height: 18px;
text-align: center;
border-radius: 50%;
background: rgba(0,0,0,0.08);
font-size: 16px;
font-weight: normal;
}
.btn-delete:active {
background: rgba(0,0,0,0.2);
}
.color-0 .btn-delete { color: #1565c0; }
.color-1 .btn-delete { color: #2e7d32; }
.color-2 .btn-delete { color: #ef6c00; }
.color-3 .btn-delete { color: #7b1fa2; }
.color-4 .btn-delete { color: #c62828; }
.empty-tip {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
color: #999;
padding-top: 40px;
}
.empty-icon {
font-size: 40px;
margin-bottom: 10px;
opacity: 0.5;
}
.result-area {
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 20px;
}
.result-box {
width: 100%;
background: #fff;
border-radius: 20px;
padding: 20px;
text-align: center;
margin-bottom: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
justify-content: center;
min-height: 120px;
transition: transform 0.1s;
}
.result-box.rolling {
transform: scale(0.98);
background: #fff8e1;
}
.result-label {
font-size: 13px;
color: #999;
margin-bottom: 5px;
text-transform: uppercase;
letter-spacing: 1px;
}
.result-text {
font-size: 32px;
font-weight: 800;
color: #333;
/* Prevent long names from overflowing */
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.btn-start {
width: 100% !important;
background: linear-gradient(135deg, #7b1fa2 0%, #4a148c 100%);
color: #fff;
border-radius: 50px;
padding: 14px 0;
font-size: 18px;
font-weight: bold;
box-shadow: 0 8px 20px rgba(74, 20, 140, 0.4);
transition: all 0.2s;
}
.btn-start-hover {
transform: translateY(2px);
box-shadow: 0 4px 10px rgba(74, 20, 140, 0.3);
}
.btn-start[disabled] {
opacity: 0.6;
background: #9e9e9e;
box-shadow: none;
transform: none;
}

View File

@@ -0,0 +1,4 @@
{
"usingComponents": {},
"navigationBarTitleText": "单位换算"
}

View File

@@ -0,0 +1,171 @@
Page({
data: {
categories: ['长度', '面积', '体积', '重量', '温度'],
categoryIndex: 0,
units: {
'长度': [
{ name: '米', factor: 1 },
{ name: '千米', factor: 1000 },
{ name: '分米', factor: 0.1 },
{ name: '厘米', factor: 0.01 },
{ name: '毫米', factor: 0.001 },
{ name: '微米', factor: 0.000001 },
{ name: '纳米', factor: 0.000000001 },
{ name: '英里', factor: 1609.344 },
{ name: '海里', factor: 1852 },
{ name: '码', factor: 0.9144 },
{ name: '英尺', factor: 0.3048 },
{ name: '英寸', factor: 0.0254 }
],
'面积': [
{ name: '平方米', factor: 1 },
{ name: '平方千米', factor: 1000000 },
{ name: '公顷', factor: 10000 },
{ name: '公亩', factor: 100 },
{ name: '平方英里', factor: 2589988.11 },
{ name: '英亩', factor: 4046.8564 },
{ name: '平方码', factor: 0.836127 },
{ name: '平方英尺', factor: 0.092903 },
{ name: '平方英寸', factor: 0.000645 }
],
'体积': [
{ name: '立方米', factor: 1 },
{ name: '立方分米', factor: 0.001 },
{ name: '立方厘米', factor: 0.000001 },
{ name: '升', factor: 0.001 },
{ name: '分升', factor: 0.0001 },
{ name: '毫升', factor: 0.000001 },
{ name: '立方英尺', factor: 0.028317 },
{ name: '立方英寸', factor: 0.000016 },
{ name: '立方码', factor: 0.764555 }
],
'重量': [
{ name: '千克', factor: 1 },
{ name: '克', factor: 0.001 },
{ name: '毫克', factor: 0.000001 },
{ name: '吨', factor: 1000 },
{ name: '磅', factor: 0.453592 },
{ name: '盎司', factor: 0.02835 },
{ name: '克拉', factor: 0.0002 }
],
'温度': [
{ name: '摄氏度', type: 'C' },
{ name: '华氏度', type: 'F' },
{ name: '开氏度', type: 'K' }
]
},
currentUnits: [] as any[],
fromIndex: 0,
toIndex: 1,
inputValue: '',
outputValue: ''
},
onLoad() {
this.updateCurrentUnits();
},
updateCurrentUnits() {
const category = this.data.categories[this.data.categoryIndex];
// @ts-ignore
const units = this.data.units[category];
this.setData({
currentUnits: units,
fromIndex: 0,
toIndex: 1,
inputValue: '',
outputValue: ''
});
},
bindCategoryChange(e: any) {
this.setData({
categoryIndex: e.detail.value
});
this.updateCurrentUnits();
},
bindFromUnitChange(e: any) {
this.setData({
fromIndex: e.detail.value
});
this.calculate();
},
bindToUnitChange(e: any) {
this.setData({
toIndex: e.detail.value
});
this.calculate();
},
bindInput(e: any) {
this.setData({
inputValue: e.detail.value
});
this.calculate();
},
calculate() {
const val = parseFloat(this.data.inputValue);
if (isNaN(val)) {
this.setData({ outputValue: '' });
return;
}
const category = this.data.categories[this.data.categoryIndex];
if (category === '温度') {
this.calculateTemperature(val);
} else {
this.calculateStandard(val);
}
},
calculateStandard(val: number) {
const fromUnit = this.data.currentUnits[this.data.fromIndex];
const toUnit = this.data.currentUnits[this.data.toIndex];
// Convert to base unit then to target unit
const baseVal = val * fromUnit.factor;
const result = baseVal / toUnit.factor;
this.setData({
outputValue: this.formatResult(result)
});
},
calculateTemperature(val: number) {
const fromUnit = this.data.currentUnits[this.data.fromIndex];
const toUnit = this.data.currentUnits[this.data.toIndex];
let celsius = val;
// Convert to Celsius first
if (fromUnit.type === 'F') {
celsius = (val - 32) * 5 / 9;
} else if (fromUnit.type === 'K') {
celsius = val - 273.15;
}
// Convert from Celsius to target
let result = celsius;
if (toUnit.type === 'F') {
result = celsius * 9 / 5 + 32;
} else if (toUnit.type === 'K') {
result = celsius + 273.15;
}
this.setData({
outputValue: this.formatResult(result)
});
},
formatResult(val: number): string {
if (Math.abs(val) < 0.000001 || Math.abs(val) > 10000000) {
return val.toExponential(4);
}
return parseFloat(val.toPrecision(6)).toString();
}
});

View File

@@ -0,0 +1,49 @@
<view class="container">
<!-- 顶部类型选择 -->
<view class="header-section">
<picker bindchange="bindCategoryChange" value="{{categoryIndex}}" range="{{categories}}">
<view class="category-picker">
<text class="label">当前换算</text>
<text class="value">{{categories[categoryIndex]}}</text>
<text class="arrow">▼</text>
</view>
</picker>
</view>
<view class="converter-card">
<!-- 输入区域 -->
<view class="conversion-row input-row">
<view class="row-label">输入</view>
<view class="row-content">
<input class="value-input" type="digit" placeholder="0" bindinput="bindInput" value="{{inputValue}}" />
<picker class="unit-selector" bindchange="bindFromUnitChange" value="{{fromIndex}}" range="{{currentUnits}}" range-key="name">
<view class="unit-text">
{{currentUnits[fromIndex].name}} <text class="unit-arrow">▼</text>
</view>
</picker>
</view>
</view>
<!-- 分割线/转换图标 -->
<view class="divider">
<view class="icon-transfer">⇅</view>
</view>
<!-- 输出区域 -->
<view class="conversion-row output-row">
<view class="row-label">结果</view>
<view class="row-content">
<view class="value-display">{{outputValue || '0'}}</view>
<picker class="unit-selector" bindchange="bindToUnitChange" value="{{toIndex}}" range="{{currentUnits}}" range-key="name">
<view class="unit-text">
{{currentUnits[toIndex].name}} <text class="unit-arrow">▼</text>
</view>
</picker>
</view>
</view>
</view>
<view class="tips">
点击单位或数字进行修改
</view>
</view>

View File

@@ -0,0 +1,151 @@
page {
background-color: #f7f8fa;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
.container {
padding: 20px;
max-width: 600px;
margin: 0 auto;
}
/* 顶部选择区 */
.header-section {
margin-bottom: 24px;
display: flex;
justify-content: center;
}
.category-picker {
background: #ffffff;
padding: 10px 24px;
border-radius: 50px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
display: flex;
align-items: center;
gap: 10px;
color: #333;
}
.category-picker .label {
font-size: 14px;
color: #888;
}
.category-picker .value {
font-size: 18px;
font-weight: 600;
color: #007aff;
}
.category-picker .arrow {
font-size: 12px;
color: #ccc;
}
/* 核心转换卡片 */
.converter-card {
background: #ffffff;
border-radius: 20px;
padding: 10px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06);
display: flex;
flex-direction: column;
}
.conversion-row {
padding: 20px;
position: relative;
}
.input-row {
background-color: #fff;
border-radius: 16px 16px 4px 4px;
}
.output-row {
background-color: #f9fbfd; /* 稍微不同的背景色区分输入输出 */
border-radius: 4px 4px 16px 16px;
}
.row-label {
font-size: 12px;
color: #999;
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 1px;
}
.row-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.value-input, .value-display {
flex: 1;
font-size: 32px;
font-weight: 500;
color: #333;
height: 48px;
line-height: 48px;
min-width: 0; /* 防止flex子项溢出 */
}
.value-display {
color: #007aff; /* 结果颜色高亮 */
}
/* 单位选择器 */
.unit-selector {
margin-left: 15px;
}
.unit-text {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
background-color: #f0f2f5;
border-radius: 12px;
font-size: 16px;
color: #444;
font-weight: 500;
transition: background-color 0.2s;
}
.unit-text:active {
background-color: #e1e4e8;
}
.unit-arrow {
font-size: 10px;
color: #888;
}
/* 分割线和图标 */
.divider {
height: 1px;
background-color: #eee;
margin: 0 20px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.icon-transfer {
position: absolute;
background: #fff;
color: #ccc;
font-size: 20px;
padding: 0 10px;
top: -14px;
}
.tips {
text-align: center;
margin-top: 30px;
font-size: 13px;
color: #ccc;
}

View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "微信授权"
}

View File

@@ -0,0 +1,51 @@
import { config } from '../../config';
Page({
data: {
url: ''
},
onLoad(options: any) {
// 这里的 URL 必须是您后端服务器的地址
// 注意本地调试时web-view 访问 localhost 需要在开发者工具勾选“不校验域名”
// 但微信 OAuth 回调必须是公网域名,所以这里填 localhost 只能走到跳转微信那一步,回调会失败
// 除非您配置了 host 映射或者使用了内网穿透
const serverUrl = `${config.baseUrl}/auth/oa/login`;
this.setData({ url: serverUrl });
},
onMessage(e: any) {
console.log('WebView message:', e.detail);
const data = e.detail.data;
if (data && data.length > 0) {
const lastData = data[data.length - 1];
// 保存到本地存储,供 index 页面 onShow 读取并触发后续逻辑
// 保存所有可用的微信用户信息
wx.setStorageSync('oa_user_info', {
nickName: lastData.nickname,
avatarUrl: lastData.headimgurl,
unionid: lastData.unionid,
gender: lastData.sex, // 1男 2女 0未知
country: lastData.country,
province: lastData.province,
city: lastData.city,
language: lastData.language
});
// 同时更新上一页数据,确保 UI 即时刷新
const pages = getCurrentPages();
const prevPage = pages[pages.length - 2]; // 上一个页面 (index)
if (prevPage) {
prevPage.setData({
"userInfo.avatarUrl": lastData.headimgurl,
"userInfo.nickName": lastData.nickname,
"userInfo.gender": lastData.sex,
"userInfo.country": lastData.country,
"userInfo.province": lastData.province,
"userInfo.city": lastData.city,
"userInfo.language": lastData.language,
"unionid": lastData.unionid || prevPage.data.unionid
});
}
}
}
})

View File

@@ -0,0 +1 @@
<web-view src="{{url}}" bindmessage="onMessage"></web-view>