目录结构调整
This commit is contained in:
6
codes/minipro/calculation/.gitignore
vendored
Normal file
6
codes/minipro/calculation/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
project.private.config.json
|
||||
.DS_Store
|
||||
dist/
|
||||
server/node_modules/
|
||||
miniprogram/node_modules/
|
||||
18
codes/minipro/calculation/miniprogram/app.json
Normal file
18
codes/minipro/calculation/miniprogram/app.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"pages": [
|
||||
"pages/index/index",
|
||||
"pages/profile/profile",
|
||||
"pages/calculator/calculator",
|
||||
"pages/unit-converter/unit-converter",
|
||||
"pages/random-draw/random-draw",
|
||||
"pages/logs/logs",
|
||||
"pages/webview/webview"
|
||||
],
|
||||
"window": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "道棋百宝箱",
|
||||
"navigationBarBackgroundColor": "#ffffff"
|
||||
},
|
||||
"style": "v2",
|
||||
"componentFramework": "glass-easel"
|
||||
}
|
||||
79
codes/minipro/calculation/miniprogram/app.ts
Normal file
79
codes/minipro/calculation/miniprogram/app.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
// app.ts
|
||||
import { config } from './config';
|
||||
import { remoteConfig } from './utils/remoteConfig';
|
||||
|
||||
App<IAppOption>({
|
||||
globalData: {},
|
||||
onLaunch() {
|
||||
// 1. 动态拉取后端地址配置
|
||||
this.initRemoteConfig();
|
||||
|
||||
// 展示本地存储能力
|
||||
const logs = wx.getStorageSync('logs') || []
|
||||
logs.unshift(Date.now())
|
||||
wx.setStorageSync('logs', logs)
|
||||
|
||||
// 登录
|
||||
wx.login({
|
||||
success: res => {
|
||||
console.log(res.code)
|
||||
// 发送 res.code 到后台换取 openId, sessionKey, unionId
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
// 初始化远程配置
|
||||
initRemoteConfig() {
|
||||
// 每次启动时清除本地缓存的 BaseUrl,确保强制使用最新的远程配置
|
||||
config.resetBaseUrl();
|
||||
|
||||
const { agentid, gameid, channelid, marketid, paraname, errorHint } = config.remoteConfig;
|
||||
|
||||
// 显示全屏 Loading,mask=true 阻止用户操作
|
||||
wx.showLoading({ title: '正在初始化...', mask: true });
|
||||
|
||||
// 监听配置更新
|
||||
remoteConfig.onUpdate(() => {
|
||||
const gameServerHttp = remoteConfig.getParaValue(paraname, agentid, gameid, channelid, marketid);
|
||||
|
||||
if (gameServerHttp) {
|
||||
console.log('远程配置获取成功:', gameServerHttp);
|
||||
// 确保 URL 包含协议头
|
||||
const finalUrl = gameServerHttp.startsWith('https') ? gameServerHttp : `https://${gameServerHttp}`;
|
||||
// 更新全局配置
|
||||
config.updateBaseUrl(finalUrl);
|
||||
|
||||
// 获取成功,隐藏 Loading,允许用户操作
|
||||
wx.hideLoading();
|
||||
} else {
|
||||
console.warn(`远程配置中未找到 ${paraname}`);
|
||||
// 如果是首次加载且失败,才显示错误提示
|
||||
// 检查当前是否有可用 baseUrl,如果没有则报错
|
||||
if (!config.baseUrl) {
|
||||
this.handleConfigError(errorHint);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 开始获取
|
||||
remoteConfig.start();
|
||||
},
|
||||
|
||||
// 处理配置获取失败:弹出模态框并阻止操作
|
||||
handleConfigError(msg: string) {
|
||||
wx.hideLoading();
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: msg,
|
||||
showCancel: false, // 不显示取消按钮
|
||||
confirmText: '重试',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 用户点击重试,重新发起请求
|
||||
wx.showLoading({ title: '正在初始化...', mask: true });
|
||||
remoteConfig.fetchConfig();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
10
codes/minipro/calculation/miniprogram/app.wxss
Normal file
10
codes/minipro/calculation/miniprogram/app.wxss
Normal file
@@ -0,0 +1,10 @@
|
||||
/**app.wxss**/
|
||||
.container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 200rpx 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
62
codes/minipro/calculation/miniprogram/config.ts
Normal file
62
codes/minipro/calculation/miniprogram/config.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
export const config = {
|
||||
// 动态获取后端服务器地址
|
||||
get baseUrl() {
|
||||
// 仅使用本地缓存的动态域名 (由远程配置更新写入)
|
||||
// 首次启动时为空,需等待 app.ts 中的 fetchRemoteConfig 完成
|
||||
return wx.getStorageSync('API_BASE_URL_OVERRIDE') || '';
|
||||
},
|
||||
|
||||
// 更新 BaseUrl 的方法 (可在业务代码中调用,例如从配置中心获取新域名后调用)
|
||||
updateBaseUrl(url: string) {
|
||||
if (url && url.startsWith('http')) {
|
||||
wx.setStorageSync('API_BASE_URL_OVERRIDE', url);
|
||||
}
|
||||
},
|
||||
|
||||
// 重置 BaseUrl 为默认配置
|
||||
resetBaseUrl() {
|
||||
wx.removeStorageSync('API_BASE_URL_OVERRIDE');
|
||||
},
|
||||
|
||||
// 默认头像链接
|
||||
defaultAvatarUrl: 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0',
|
||||
|
||||
// 认证模式: 'oa' (公众号授权,获取真实头像昵称+UnionID) | 'mp' (小程序原生,使用头像昵称填写能力)
|
||||
authMode: 'oa',
|
||||
|
||||
// 登录页提示文字
|
||||
loginHint: '为了获取更好的用户体验,登录后使用',
|
||||
|
||||
// 登录态过期时间(小时)
|
||||
loginExpirationHours: 24,
|
||||
|
||||
// 验证码倒计时时长(秒)
|
||||
smsCountdown: 30,
|
||||
|
||||
// 是否开启头像上传 (true: 上传临时头像到服务器; false: 直接使用临时路径或默认头像)
|
||||
enableAvatarUpload: false,
|
||||
|
||||
// 远程配置参数
|
||||
remoteConfig: {
|
||||
// 方式一:直接使用 Gitee/GitHub 的 Raw 文件地址 (推荐,最简单稳定)
|
||||
// 示例:https://gitee.com/用户名/仓库名/raw/分支名/config.json
|
||||
configUrl: 'https://gitee.com/daoqijuyou/config/raw/master/update_jsonv2.txt',
|
||||
|
||||
// 核心参数配置
|
||||
paraname: 'minipro_api_url', // 在远程配置中查找的服务器地址 Key
|
||||
errorHint: '无法连接到服务器,请检查网络设置或稍后重试', // 获取配置失败时的提示信息
|
||||
|
||||
agentid: "veRa0qrBf0df2K1G4de2tgfmVxB2jxpv",
|
||||
gameid: "G2hw0ubng0zcoI0r4mx3H2yr4GejidwO",
|
||||
channelid: "FtJf073aa0d6rI1xD8J1Y42fINTm0ziK",
|
||||
marketid: 3
|
||||
},
|
||||
|
||||
// 测试配置
|
||||
testConfig: {
|
||||
enable: false, // 是否开启测试模式
|
||||
mockUnionId: '', // 自定义 UnionID,为空则使用真实值
|
||||
mockPhoneNumber: '', // 自定义手机号,为空则使用真实值
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "计算器"
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
}
|
||||
}
|
||||
73
codes/minipro/calculation/miniprogram/pages/index/index.ts
Normal file
73
codes/minipro/calculation/miniprogram/pages/index/index.ts
Normal 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'
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
69
codes/minipro/calculation/miniprogram/pages/index/index.wxml
Normal file
69
codes/minipro/calculation/miniprogram/pages/index/index.wxml
Normal 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>
|
||||
192
codes/minipro/calculation/miniprogram/pages/index/index.wxss
Normal file
192
codes/minipro/calculation/miniprogram/pages/index/index.wxss
Normal 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;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
}
|
||||
}
|
||||
21
codes/minipro/calculation/miniprogram/pages/logs/logs.ts
Normal file
21
codes/minipro/calculation/miniprogram/pages/logs/logs.ts
Normal 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
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -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>
|
||||
16
codes/minipro/calculation/miniprogram/pages/logs/logs.wxss
Normal file
16
codes/minipro/calculation/miniprogram/pages/logs/logs.wxss
Normal 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);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
}
|
||||
}
|
||||
679
codes/minipro/calculation/miniprogram/pages/profile/profile.ts
Normal file
679
codes/minipro/calculation/miniprogram/pages/profile/profile.ts
Normal 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);
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -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>
|
||||
203
codes/minipro/calculation/miniprogram/pages/profile/profile.wxss
Normal file
203
codes/minipro/calculation/miniprogram/pages/profile/profile.wxss
Normal 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;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationBarTitleText": "谁去拿外卖"
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationBarTitleText": "单位换算"
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "微信授权"
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
<web-view src="{{url}}" bindmessage="onMessage"></web-view>
|
||||
316
codes/minipro/calculation/miniprogram/utils/remoteConfig.ts
Normal file
316
codes/minipro/calculation/miniprogram/utils/remoteConfig.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
import { config } from '../config';
|
||||
|
||||
class RemoteConfig {
|
||||
data: any;
|
||||
url: string;
|
||||
timer: number | null;
|
||||
updateCallbacks: Function[];
|
||||
|
||||
constructor() {
|
||||
this.data = null;
|
||||
this.url = "";
|
||||
this.timer = null;
|
||||
this.updateCallbacks = [];
|
||||
|
||||
// 优先使用直接配置的完整 URL (如 Gitee/GitHub Raw URL)
|
||||
if (config.remoteConfig.configUrl && config.remoteConfig.configUrl.startsWith('http')) {
|
||||
this.url = config.remoteConfig.configUrl;
|
||||
console.log('RemoteConfig: Using direct configUrl:', this.url);
|
||||
} else {
|
||||
console.warn('RemoteConfig: Missing configUrl.');
|
||||
}
|
||||
}
|
||||
|
||||
isReady(): boolean {
|
||||
return this.data !== null;
|
||||
}
|
||||
|
||||
onUpdate(callback: Function) {
|
||||
if (typeof callback === 'function') {
|
||||
this.updateCallbacks.push(callback);
|
||||
}
|
||||
}
|
||||
|
||||
triggerUpdate() {
|
||||
this.updateCallbacks.forEach(cb => {
|
||||
try {
|
||||
cb();
|
||||
} catch (e) {
|
||||
console.error('Error in remote config update callback:', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
start() {
|
||||
this.fetchConfig();
|
||||
// 小程序端通常不需要高频轮询,或者可以在 onShow 中触发
|
||||
// const interval = 30000;
|
||||
// this.timer = setInterval(() => {
|
||||
// this.fetchConfig();
|
||||
// }, interval);
|
||||
}
|
||||
|
||||
fetchConfig() {
|
||||
if (!this.url) return;
|
||||
|
||||
console.log('Fetching remote config from:', this.url);
|
||||
|
||||
wx.request({
|
||||
url: this.url,
|
||||
data: { t: Date.now() },
|
||||
success: (res: any) => {
|
||||
if (res.statusCode === 200) {
|
||||
this.parseData(res.data);
|
||||
} else {
|
||||
console.error('Remote config fetch failed with status:', res.statusCode);
|
||||
// 获取失败,触发更新以便上层处理错误(此时 data 可能为 null)
|
||||
this.triggerUpdate();
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('Error fetching remote config:', err);
|
||||
// 网络错误,触发更新以便上层处理错误
|
||||
this.triggerUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
parseData(data: any) {
|
||||
let parsedData = null;
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
parsedData = JSON.parse(data.trim());
|
||||
} catch (e) {
|
||||
console.error('Failed to parse config JSON:', e);
|
||||
}
|
||||
} else {
|
||||
parsedData = data;
|
||||
}
|
||||
|
||||
if (parsedData) {
|
||||
this.data = parsedData;
|
||||
this.triggerUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
getParaValue(paraname: string, agentid: string, gameid: string, channelid: string, marketid: number) {
|
||||
let paravalue = null;
|
||||
|
||||
if (!this.data) {
|
||||
return paravalue;
|
||||
}
|
||||
|
||||
// 1. Root level
|
||||
if (this.data[paraname]) {
|
||||
paravalue = this.data[paraname];
|
||||
}
|
||||
|
||||
// Helper to find item in list and update paravalue
|
||||
const findAndCheck = (list: any[], key: string, value: any) => {
|
||||
if (!list || !Array.isArray(list)) return null;
|
||||
for (const item of list) {
|
||||
if (item[key] == value) {
|
||||
if (item[paraname]) {
|
||||
paravalue = item[paraname];
|
||||
}
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 2. Agent level
|
||||
const agent = findAndCheck(this.data.agentlist, 'agentid', agentid);
|
||||
if (!agent) return paravalue;
|
||||
|
||||
// 3. Game level
|
||||
const game = findAndCheck(agent.gamelist, 'gameid', gameid);
|
||||
if (!game) return paravalue;
|
||||
|
||||
// 4. Channel level
|
||||
const channel = findAndCheck(game.channellist, 'channelid', channelid);
|
||||
if (!channel) return paravalue;
|
||||
|
||||
// 5. Market level
|
||||
const market = findAndCheck(channel.marketlist, 'marketid', marketid);
|
||||
|
||||
return paravalue;
|
||||
}
|
||||
|
||||
// 获取游戏列表_下载页面
|
||||
getGameListDownHtml(agentid: string, channelid: string) {
|
||||
const gamelist: any[] = [];
|
||||
if (!this.data || !this.data.agentlist) {
|
||||
return gamelist;
|
||||
}
|
||||
|
||||
for (const agent of this.data.agentlist) {
|
||||
if (agent.agentid == agentid) {
|
||||
if (agent.gamelist) {
|
||||
for (const game of agent.gamelist) {
|
||||
const o_game: any = {
|
||||
name: game.gamename,
|
||||
image: game.game_down_image,
|
||||
state: game.game_down_state,
|
||||
memo: game.game_down_memo
|
||||
};
|
||||
|
||||
if (game.channellist) {
|
||||
for (const channel of game.channellist) {
|
||||
if (channel.channelid == channelid) {
|
||||
const _ios_marketid = channel.ios_defdownload_marketid;
|
||||
const _and_marketid = channel.and_defdownload_marketid;
|
||||
|
||||
if (channel.marketlist) {
|
||||
for (const market of channel.marketlist) {
|
||||
if (market.marketid == _ios_marketid) {
|
||||
o_game.ios_down = market.app_download;
|
||||
o_game.ios_size = market.app_size;
|
||||
o_game.ios_marketid = _ios_marketid;
|
||||
}
|
||||
if (market.marketid == _and_marketid) {
|
||||
o_game.android_down = market.app_download;
|
||||
o_game.android_size = market.app_size;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (o_game.ios_down && o_game.android_down) {
|
||||
gamelist.push(o_game);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return gamelist;
|
||||
}
|
||||
|
||||
// 获取游戏列表_游戏大厅
|
||||
getGameListGameHall(agentid: string, channelid: string, marketid: number) {
|
||||
const gamelist: any[] = [];
|
||||
if (!this.data || !this.data.agentlist) {
|
||||
return gamelist;
|
||||
}
|
||||
|
||||
for (const agent of this.data.agentlist) {
|
||||
if (agent.agentid == agentid) {
|
||||
if (agent.gamelist) {
|
||||
for (const game of agent.gamelist) {
|
||||
if (game.gameid != "G2hw0ubng0zcoI0r4mx3H2yr4GejidwO") {
|
||||
const o_game: any = {
|
||||
gameid: game.gameid,
|
||||
gamename: game.gamename,
|
||||
gamedir: game.game_hall_dir,
|
||||
gameimage: game.game_hall_image,
|
||||
gameversion: game.game_version,
|
||||
gamezip: game.game_zip,
|
||||
zipsize: game.game_size
|
||||
};
|
||||
|
||||
if (game.channellist) {
|
||||
for (const channel of game.channellist) {
|
||||
if (channel.channelid == channelid) {
|
||||
if (channel.game_version > 0) {
|
||||
o_game.gameversion = channel.game_version;
|
||||
}
|
||||
if (channel.game_zip) {
|
||||
o_game.gamezip = channel.game_zip;
|
||||
}
|
||||
if (channel.game_size) {
|
||||
o_game.zipsize = channel.game_size;
|
||||
}
|
||||
|
||||
if (channel.marketlist) {
|
||||
for (const market of channel.marketlist) {
|
||||
if (market.marketid == marketid) {
|
||||
if (market.game_version > 0) {
|
||||
o_game.gameversion = market.game_version;
|
||||
}
|
||||
if (market.game_zip) {
|
||||
o_game.gamezip = market.game_zip;
|
||||
}
|
||||
if (market.game_size) {
|
||||
o_game.zipsize = market.game_size;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
gamelist.push(o_game);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return gamelist;
|
||||
}
|
||||
|
||||
// 获取代理城市列表
|
||||
getAgentList(agentid: string, channelid: string) {
|
||||
const agentlist: any[] = [];
|
||||
if (!this.data || !this.data.agentlist) {
|
||||
return agentlist;
|
||||
}
|
||||
|
||||
for (const agent of this.data.agentlist) {
|
||||
if (agent.agentid == agentid || agent.relagentid == agentid) {
|
||||
const o_agent = {
|
||||
agentid: agent.agentid,
|
||||
name: agent.agentname,
|
||||
channelid: (agent.gamelist && agent.gamelist[0] && agent.gamelist[0].channellist && agent.gamelist[0].channellist[0]) ? agent.gamelist[0].channellist[0].channelid : ''
|
||||
};
|
||||
agentlist.push(o_agent);
|
||||
}
|
||||
}
|
||||
return agentlist;
|
||||
}
|
||||
|
||||
// 获取子游戏服务器列表
|
||||
getGameServerList(agentid: string) {
|
||||
const iplist: string[] = [];
|
||||
const paraname = "game_server_http";
|
||||
|
||||
const doPushToIpList = (ip: string) => {
|
||||
if (ip && !iplist.includes(ip)) {
|
||||
iplist.push(ip);
|
||||
}
|
||||
};
|
||||
|
||||
if (!this.data || !this.data.agentlist) {
|
||||
return iplist;
|
||||
}
|
||||
|
||||
const agent = this.data.agentlist.find((a: any) => a.agentid === agentid);
|
||||
if (!agent) {
|
||||
return iplist;
|
||||
}
|
||||
|
||||
if (this.data[paraname]) {
|
||||
doPushToIpList(this.data[paraname]);
|
||||
}
|
||||
if (agent[paraname]) {
|
||||
doPushToIpList(agent[paraname]);
|
||||
}
|
||||
if (agent.gamelist) {
|
||||
for (const game of agent.gamelist) {
|
||||
if (game[paraname]) {
|
||||
doPushToIpList(game[paraname]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return iplist;
|
||||
}
|
||||
}
|
||||
|
||||
export const remoteConfig = new RemoteConfig();
|
||||
19
codes/minipro/calculation/miniprogram/utils/util.ts
Normal file
19
codes/minipro/calculation/miniprogram/utils/util.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export const formatTime = (date: Date) => {
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
const hour = date.getHours()
|
||||
const minute = date.getMinutes()
|
||||
const second = date.getSeconds()
|
||||
|
||||
return (
|
||||
[year, month, day].map(formatNumber).join('/') +
|
||||
' ' +
|
||||
[hour, minute, second].map(formatNumber).join(':')
|
||||
)
|
||||
}
|
||||
|
||||
const formatNumber = (n: number) => {
|
||||
const s = n.toString()
|
||||
return s[1] ? s : '0' + s
|
||||
}
|
||||
15
codes/minipro/calculation/package.json
Normal file
15
codes/minipro/calculation/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "miniprogram-ts-quickstart",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "",
|
||||
"dependencies": {
|
||||
},
|
||||
"devDependencies": {
|
||||
"miniprogram-api-typings": "^2.8.3-1"
|
||||
}
|
||||
}
|
||||
51
codes/minipro/calculation/project.config.json
Normal file
51
codes/minipro/calculation/project.config.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"description": "项目配置文件",
|
||||
"miniprogramRoot": "miniprogram/",
|
||||
"compileType": "miniprogram",
|
||||
"setting": {
|
||||
"useCompilerPlugins": [
|
||||
"typescript"
|
||||
],
|
||||
"urlCheck": false,
|
||||
"babelSetting": {
|
||||
"ignore": [],
|
||||
"disablePlugins": [],
|
||||
"outputPath": ""
|
||||
},
|
||||
"coverView": false,
|
||||
"postcss": false,
|
||||
"minified": false,
|
||||
"enhance": false,
|
||||
"showShadowRootInWxmlPanel": false,
|
||||
"packNpmRelationList": [],
|
||||
"ignoreUploadUnusedFiles": true,
|
||||
"compileHotReLoad": false,
|
||||
"skylineRenderEnable": true,
|
||||
"es6": false,
|
||||
"compileWorklet": false,
|
||||
"uglifyFileName": false,
|
||||
"uploadWithSourceMap": true,
|
||||
"packNpmManually": false,
|
||||
"minifyWXSS": true,
|
||||
"minifyWXML": true,
|
||||
"localPlugins": false,
|
||||
"condition": false,
|
||||
"swc": false,
|
||||
"disableSWC": true,
|
||||
"disableUseStrict": false
|
||||
},
|
||||
"simulatorType": "wechat",
|
||||
"simulatorPluginLibVersion": {},
|
||||
"condition": {},
|
||||
"srcMiniprogramRoot": "miniprogram/",
|
||||
"editorSetting": {
|
||||
"tabIndent": "insertSpaces",
|
||||
"tabSize": 2
|
||||
},
|
||||
"libVersion": "3.12.1",
|
||||
"packOptions": {
|
||||
"ignore": [],
|
||||
"include": []
|
||||
},
|
||||
"appid": "wx51ab9a04fac56760"
|
||||
}
|
||||
30
codes/minipro/calculation/tsconfig.json
Normal file
30
codes/minipro/calculation/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strictNullChecks": true,
|
||||
"noImplicitAny": true,
|
||||
"module": "CommonJS",
|
||||
"target": "ES2020",
|
||||
"allowJs": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true,
|
||||
"noImplicitThis": true,
|
||||
"noImplicitReturns": true,
|
||||
"alwaysStrict": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"strict": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"lib": ["ES2020"],
|
||||
"typeRoots": [
|
||||
"./typings"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
39
codes/minipro/wxserver/config/index.js
Normal file
39
codes/minipro/wxserver/config/index.js
Normal file
@@ -0,0 +1,39 @@
|
||||
module.exports = {
|
||||
// 服务端口
|
||||
port: 3000,
|
||||
|
||||
// 是否开启日志输出
|
||||
enableLog: false,
|
||||
|
||||
// 是否开启头像上传
|
||||
enableAvatarUpload: false,
|
||||
|
||||
// 小程序配置
|
||||
miniProgram: {
|
||||
appId: 'wx51ab9a04fac56760',
|
||||
appSecret: 'd326aaf93eb4d106e35592667ef022f5'
|
||||
},
|
||||
|
||||
// 公众号配置 (用于获取永久头像)
|
||||
officialAccount: {
|
||||
appId: 'wx7a1c6f324182bc83',
|
||||
appSecret: 'a90ba94e3a2dca8d09656dcc364e1df0', // 请在此处填入您的公众号 Secret
|
||||
|
||||
// 远程配置 Key:用于动态获取网页授权域名
|
||||
// 在远程配置 json 中配置此 key 对应的值为您的测试/正式域名
|
||||
redirectDomainKey: 'minipro_api_url'
|
||||
},
|
||||
|
||||
// 远程配置
|
||||
remoteConfig: {
|
||||
// 方式一:直接使用 Gitee/GitHub 的 Raw 文件地址 (推荐,最简单稳定)
|
||||
// 示例:https://gitee.com/用户名/仓库名/raw/分支名/config.json
|
||||
configUrl: 'https://gitee.com/daoqijuyou/config/raw/master/update_jsonv2.txt',
|
||||
|
||||
interval: 30 * 1000, // 更新间隔,单位毫秒
|
||||
agentid: "veRa0qrBf0df2K1G4de2tgfmVxB2jxpv",
|
||||
gameid: "G2hw0ubng0zcoI0r4mx3H2yr4GejidwO",
|
||||
channelid: "FtJf073aa0d6rI1xD8J1Y42fINTm0ziK",
|
||||
marketid: 3
|
||||
}
|
||||
};
|
||||
1
codes/minipro/wxserver/debug/geturl.txt
Normal file
1
codes/minipro/wxserver/debug/geturl.txt
Normal file
@@ -0,0 +1 @@
|
||||
http://8.139.255.236:56073?data=%7B%22app%22%3A%22youle%22%2C%22route%22%3A%22agent%22%2C%22rpc%22%3A%22bind_player_wechat%22%2C%22data%22%3A%7B%22agentid%22%3A%22veRa0qrBf0df2K1G4de2tgfmVxB2jxpv%22%2C%22channelid%22%3A%22FtJf073aa0d6rI1xD8J1Y42fINTm0ziK%22%2C%22marketid%22%3A3%2C%22gameid%22%3A%22G2hw0ubng0zcoI0r4mx3H2yr4GejidwO%22%2C%22openid%22%3A%22onJdG10JeHtS0Dbz8FtdVv7aeVBY%22%2C%22unionid%22%3A%22oLVKis6bj3_l8qspMybG60KV2GN5%22%2C%22nickname%22%3A%22%E5%85%AB%E4%B9%9D444%22%2C%22avatar%22%3A%22http%3A%2F%2F6bae8d8b.r29.cpolar.top%2Fuploads%2F1766995380449_yl4r842it.jpg%22%2C%22sex%22%3A0%2C%22province%22%3A%22%22%2C%22city%22%3A%22%22%2C%22telphone%22%3A%2215797777777%22%7D%7D
|
||||
510
codes/minipro/wxserver/index.js
Normal file
510
codes/minipro/wxserver/index.js
Normal file
@@ -0,0 +1,510 @@
|
||||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const cors = require('cors');
|
||||
const axios = require('axios');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const multer = require('multer');
|
||||
// const { S3Client } = require('@aws-sdk/client-s3'); // Removed S3
|
||||
// const { Upload } = require('@aws-sdk/lib-storage'); // Removed S3
|
||||
const config = require('./config/index');
|
||||
const remoteConfig = require('./services/remoteConfig');
|
||||
const logger = require('./utils/logger');
|
||||
|
||||
const app = express();
|
||||
const port = config.port;
|
||||
|
||||
app.use(cors());
|
||||
app.use(bodyParser.json());
|
||||
|
||||
// 请求日志中间件
|
||||
app.use((req, res, next) => {
|
||||
// 忽略静态资源请求日志,避免刷屏
|
||||
if (!req.url.startsWith('/uploads') && !req.url.startsWith('/public')) {
|
||||
logger.info('HTTP', `${req.method} ${req.url}`);
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// 配置静态文件服务,用于微信域名校验文件
|
||||
// 请将下载的 MP_verify_xxx.txt 文件放入 server/public 目录
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
// 配置上传文件的静态服务
|
||||
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
|
||||
|
||||
// ==================================================================
|
||||
// 公众号配置区域 (用于获取永久头像)
|
||||
// ==================================================================
|
||||
// 已移至 config.js
|
||||
|
||||
// Step 1: 小程序跳转到这里,服务器重定向到微信授权页
|
||||
app.get('/auth/oa/login', (req, res) => {
|
||||
// 1. 尝试从远程配置获取动态域名
|
||||
const { agentid, gameid, channelid, marketid } = config.remoteConfig;
|
||||
const key = config.officialAccount.redirectDomainKey || 'auth_redirect_domain';
|
||||
|
||||
let currentDomain = remoteConfig.getParaValue(key, agentid, gameid, channelid, marketid);
|
||||
|
||||
if (!currentDomain) {
|
||||
return res.status(500).send('Configuration Error: No redirect domain available in remote config.');
|
||||
}
|
||||
|
||||
// 3. 格式化域名 (确保有 https且无末尾斜杠)
|
||||
currentDomain = currentDomain.trim().replace(/\/$/, '');
|
||||
if (!currentDomain.startsWith('http')) {
|
||||
currentDomain = `https://${currentDomain}`;
|
||||
}
|
||||
|
||||
// 这里的 redirect_uri 必须是 encodeURIComponent 后的完整 URL
|
||||
const redirectUri = encodeURIComponent(`${currentDomain}/auth/oa/callback`);
|
||||
const scope = 'snsapi_userinfo'; // 获取头像必须用 userinfo 作用域
|
||||
const url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${config.officialAccount.appId}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}&state=STATE#wechat_redirect`;
|
||||
|
||||
// console.log('Redirecting to WeChat Auth:', url); // 打印跳转链接,方便调试
|
||||
logger.info('Auth', 'Redirecting to WeChat Auth:', url);
|
||||
res.redirect(url);
|
||||
});
|
||||
|
||||
// Step 2: 微信回调,获取 code -> access_token -> userinfo -> 返回 HTML 给 web-view
|
||||
app.get('/auth/oa/callback', async (req, res) => {
|
||||
const code = req.query.code;
|
||||
// console.log('WeChat Callback Code:', code); // 打印回调 Code
|
||||
logger.info('Auth', 'WeChat Callback Code:', code);
|
||||
|
||||
if (!code) return res.send('Auth failed, no code');
|
||||
|
||||
try {
|
||||
// 1. 获取 access_token
|
||||
const tokenUrl = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${config.officialAccount.appId}&secret=${config.officialAccount.appSecret}&code=${code}&grant_type=authorization_code`;
|
||||
// console.log('Requesting Token URL:', tokenUrl); // 打印 Token 请求链接 (注意:生产环境不要打印 Secret)
|
||||
logger.debug('Auth', 'Requesting Token URL (Secret Hidden)');
|
||||
|
||||
const tokenRes = await axios.get(tokenUrl);
|
||||
// console.log('Token Response:', tokenRes.data); // 打印 Token 响应结果
|
||||
logger.debug('Auth', 'Token Response:', tokenRes.data);
|
||||
|
||||
if (tokenRes.data.errcode) {
|
||||
return res.send('Token Error: ' + JSON.stringify(tokenRes.data));
|
||||
}
|
||||
|
||||
const { access_token, openid } = tokenRes.data;
|
||||
|
||||
// 2. 获取用户信息 (包含永久头像 headimgurl)
|
||||
const infoUrl = `https://api.weixin.qq.com/sns/userinfo?access_token=${access_token}&openid=${openid}&lang=zh_CN`;
|
||||
const infoRes = await axios.get(infoUrl);
|
||||
// console.log('User Info Response:', infoRes.data); // 打印用户信息响应
|
||||
logger.success('Auth', 'User Info Retrieved:', infoRes.data.nickname);
|
||||
|
||||
const userInfo = infoRes.data; // { headimgurl, nickname, unionid, ... }
|
||||
|
||||
// 3. 返回一个 HTML,利用 JSSDK 把数据传回小程序
|
||||
// 注意:wx.miniProgram.postMessage 只有在页面后退、销毁、分享时才会触发小程序的 bindmessage
|
||||
// 所以这里我们发送数据后,立即调用 navigateBack
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>正在同步...</title>
|
||||
<script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h3 style="text-align:center; margin-top: 50px;">正在同步微信头像...</h3>
|
||||
<script>
|
||||
function sendAndBack() {
|
||||
// 发送消息给小程序
|
||||
wx.miniProgram.postMessage({
|
||||
data: ${JSON.stringify(userInfo)}
|
||||
});
|
||||
// 跳回小程序上一页
|
||||
wx.miniProgram.navigateBack();
|
||||
}
|
||||
|
||||
if (window.WeixinJSBridge) {
|
||||
sendAndBack();
|
||||
} else {
|
||||
document.addEventListener('WeixinJSBridgeReady', sendAndBack, false);
|
||||
}
|
||||
// 保底策略
|
||||
setTimeout(sendAndBack, 1000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
res.send(html);
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.send('Auth error: ' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取 AccessToken (简单实现,实际生产环境需要缓存 AccessToken)
|
||||
async function getAccessToken() {
|
||||
const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${config.miniProgram.appId}&secret=${config.miniProgram.appSecret}`;
|
||||
try {
|
||||
const response = await axios.get(url);
|
||||
if (response.data.errcode) {
|
||||
throw new Error(`获取 AccessToken 失败: ${response.data.errmsg}`);
|
||||
}
|
||||
return response.data.access_token;
|
||||
} catch (error) {
|
||||
console.error('Get AccessToken Error:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 登录接口:换取 OpenID 和 UnionID
|
||||
app.post('/api/login', async (req, res) => {
|
||||
const { code } = req.body;
|
||||
if (!code) {
|
||||
return res.status(400).json({ error: 'Code is required' });
|
||||
}
|
||||
|
||||
// 如果用户没有配置 AppID,返回模拟数据
|
||||
if (config.miniProgram.appId === 'YOUR_APP_ID') {
|
||||
// console.log('未配置 AppID,返回模拟登录数据');
|
||||
return res.json({
|
||||
openid: 'mock_openid_' + Date.now(),
|
||||
unionid: 'mock_unionid_' + Date.now(),
|
||||
session_key: 'mock_session_key'
|
||||
});
|
||||
}
|
||||
|
||||
const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${config.miniProgram.appId}&secret=${config.miniProgram.appSecret}&js_code=${code}&grant_type=authorization_code`;
|
||||
|
||||
try {
|
||||
const response = await axios.get(url);
|
||||
if (response.data.errcode) {
|
||||
return res.status(500).json({ error: response.data.errmsg });
|
||||
}
|
||||
// 返回 openid, unionid (如果有), session_key
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 2. 获取手机号接口
|
||||
app.post('/api/getPhoneNumber', async (req, res) => {
|
||||
const { code } = req.body;
|
||||
if (!code) {
|
||||
return res.status(400).json({ error: 'Code is required' });
|
||||
}
|
||||
|
||||
// 如果用户没有配置 AppID,返回模拟数据
|
||||
if (config.miniProgram.appId === 'YOUR_APP_ID') {
|
||||
// console.log('未配置 AppID,返回模拟手机号');
|
||||
return res.json({
|
||||
phoneNumber: '13800138000',
|
||||
purePhoneNumber: '13800138000',
|
||||
countryCode: '86',
|
||||
watermark: { timestamp: Date.now(), appid: config.miniProgram.appId }
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const accessToken = await getAccessToken();
|
||||
const url = `https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=${accessToken}`;
|
||||
|
||||
const response = await axios.post(url, { code });
|
||||
|
||||
if (response.data.errcode === 0) {
|
||||
// 成功获取
|
||||
res.json(response.data.phone_info);
|
||||
} else {
|
||||
res.status(500).json({ error: response.data.errmsg, errcode: response.data.errcode });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Get Phone Number Error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 缓存 game_server_http
|
||||
let cachedGameServerHttp = null;
|
||||
|
||||
// 监听远程配置更新
|
||||
remoteConfig.onUpdate(() => {
|
||||
const { agentid, gameid, channelid, marketid } = config.remoteConfig;
|
||||
const newValue = remoteConfig.getParaValue('game_server_http', agentid, gameid, channelid, marketid);
|
||||
|
||||
if (newValue) {
|
||||
cachedGameServerHttp = newValue;
|
||||
// logger.info('Config', 'Updated cached game_server_http:', cachedGameServerHttp);
|
||||
} else {
|
||||
logger.warn('Config', 'game_server_http not found in new config');
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 文件上传接口 (本地存储)
|
||||
// 确保存储目录存在
|
||||
const uploadDir = path.join(__dirname, 'uploads');
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: function (req, file, cb) {
|
||||
cb(null, uploadDir)
|
||||
},
|
||||
filename: function (req, file, cb) {
|
||||
const fileExtension = path.extname(file.originalname) || '.jpg';
|
||||
const fileName = `${Date.now()}_${Math.random().toString(36).substr(2, 9)}${fileExtension}`;
|
||||
cb(null, fileName)
|
||||
}
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage: storage,
|
||||
fileFilter: (req, file, cb) => {
|
||||
if (config.enableAvatarUpload === false) {
|
||||
// 如果配置关闭了上传,拒绝文件
|
||||
return cb(null, false);
|
||||
}
|
||||
cb(null, true);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/upload', upload.single('file'), (req, res) => {
|
||||
if (config.enableAvatarUpload === false) {
|
||||
return res.status(403).json({ error: 'Avatar upload is disabled' });
|
||||
}
|
||||
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'No file uploaded' });
|
||||
}
|
||||
|
||||
try {
|
||||
// 构造访问链接
|
||||
const protocol = req.protocol;
|
||||
const host = req.get('host');
|
||||
const fileUrl = `${protocol}://${host}/uploads/${req.file.filename}`;
|
||||
|
||||
logger.success('Upload', 'File uploaded successfully:', fileUrl);
|
||||
res.json({ url: fileUrl });
|
||||
} catch (error) {
|
||||
logger.error('Upload', 'Upload Error:', error);
|
||||
res.status(500).json({ error: 'Upload failed: ' + error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 4. 提交用户信息接口 (真实保存到游戏服务器)
|
||||
app.post('/api/saveUserInfo', async (req, res) => {
|
||||
const userInfo = req.body;
|
||||
logger.divider('Save User Info');
|
||||
logger.info('API', 'Received User Info:', userInfo.nickName);
|
||||
|
||||
try {
|
||||
// 1. 使用缓存的 game_server_http
|
||||
if (!cachedGameServerHttp) {
|
||||
// 如果缓存为空(例如启动时首次获取尚未完成),尝试手动获取一次
|
||||
const { agentid, gameid, channelid, marketid } = config.remoteConfig;
|
||||
cachedGameServerHttp = remoteConfig.getParaValue('game_server_http', agentid, gameid, channelid, marketid);
|
||||
}
|
||||
|
||||
if (!cachedGameServerHttp) {
|
||||
logger.error('Config', 'game_server_http not found');
|
||||
return res.status(500).json({ error: 'Configuration error: game_server_http not found' });
|
||||
}
|
||||
|
||||
// 确保 URL 格式正确
|
||||
const targetUrl = cachedGameServerHttp.startsWith('http') ? cachedGameServerHttp : `http://${cachedGameServerHttp}`;
|
||||
|
||||
// 2. 构造请求数据
|
||||
const { agentid, gameid, channelid, marketid } = config.remoteConfig;
|
||||
const payload = {
|
||||
"app": "youle",
|
||||
"route": "agent",
|
||||
"rpc": "bind_player_wechat",
|
||||
"data": {
|
||||
"agentid": agentid,
|
||||
"channelid": channelid,
|
||||
"marketid": marketid,
|
||||
"gameid": gameid,
|
||||
"openid": userInfo.openid,
|
||||
"unionid": userInfo.unionid,
|
||||
"nickname": userInfo.nickName,
|
||||
"avatar": userInfo.avatarUrl,
|
||||
"sex": userInfo.gender,
|
||||
"province": userInfo.province,
|
||||
"city": userInfo.city,
|
||||
"telphone": userInfo.phoneNumber
|
||||
}
|
||||
};
|
||||
|
||||
// --- Debug: 生成可粘贴到浏览器的 GET 链接 ---
|
||||
// try {
|
||||
// const debugQuery = `data=${encodeURIComponent(JSON.stringify(payload))}`;
|
||||
// const debugUrl = `${targetUrl}?${debugQuery}`;
|
||||
// console.log('\n[Debug] Request URL for Browser:');
|
||||
// console.log(debugUrl);
|
||||
// console.log('');
|
||||
// } catch (e) {
|
||||
// console.error('[Debug] Failed to generate debug URL:', e);
|
||||
// }
|
||||
// -------------------------------------------
|
||||
|
||||
// 3. 发送请求
|
||||
const bodyData = 'data=' + JSON.stringify(payload);
|
||||
|
||||
logger.info('GameServer', 'Sending bind_player_wechat request...');
|
||||
logger.debug('GameServer', 'Payload:', payload);
|
||||
|
||||
const response = await axios.post(targetUrl, bodyData, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
});
|
||||
logger.info('GameServer', 'Response received');
|
||||
logger.debug('GameServer', 'Response Data:', response.data);
|
||||
|
||||
const gameResult = response.data?.data?.result;
|
||||
const isSuccess = gameResult !== -1;
|
||||
res.json({ success: isSuccess, message: response.data?.data?.msg, data: response.data });
|
||||
|
||||
} catch (error) {
|
||||
logger.error('API', 'Save User Info Error:', error.message);
|
||||
res.status(500).json({ success: false, message: '用户信息保存失败: ' + error.message, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 5. 玩家登录接口
|
||||
app.post('/api/playerLogin', async (req, res) => {
|
||||
const userInfo = req.body;
|
||||
logger.divider('Player Login');
|
||||
logger.info('API', 'Received Login Request:', userInfo.nickName);
|
||||
|
||||
try {
|
||||
if (!cachedGameServerHttp) {
|
||||
const { agentid, gameid, channelid, marketid } = config.remoteConfig;
|
||||
cachedGameServerHttp = remoteConfig.getParaValue('game_server_http', agentid, gameid, channelid, marketid);
|
||||
}
|
||||
|
||||
if (!cachedGameServerHttp) {
|
||||
return res.status(500).json({ error: 'Configuration error: game_server_http not found' });
|
||||
}
|
||||
|
||||
const targetUrl = cachedGameServerHttp.startsWith('http') ? cachedGameServerHttp : `http://${cachedGameServerHttp}`;
|
||||
const { agentid, gameid, channelid, marketid } = config.remoteConfig;
|
||||
|
||||
const payload = {
|
||||
"app": "youle",
|
||||
"route": "agent",
|
||||
"rpc": "player_login",
|
||||
"data": {
|
||||
"agentid": agentid,
|
||||
"channelid": channelid,
|
||||
"marketid": marketid,
|
||||
"gameid": gameid,
|
||||
"openid": userInfo.openid,
|
||||
"unionid": userInfo.unionid,
|
||||
"nickname": userInfo.nickName,
|
||||
"avatar": userInfo.avatarUrl,
|
||||
"sex": userInfo.gender,
|
||||
"province": userInfo.province,
|
||||
"city": userInfo.city,
|
||||
"version": userInfo.version,
|
||||
"telphone": userInfo.phoneNumber,
|
||||
"smmcode": userInfo.verificationCode,
|
||||
"telphoneAuto": userInfo.telphoneAuto,
|
||||
"playerid": userInfo.playerid,
|
||||
}
|
||||
};
|
||||
|
||||
// --- Debug: 生成可粘贴到浏览器的 GET 链接 ---
|
||||
// try {
|
||||
// const debugQuery = `data=${encodeURIComponent(JSON.stringify(payload))}`;
|
||||
// const debugUrl = `${targetUrl}?${debugQuery}`;
|
||||
// console.log('\n[Debug] Request URL for Browser:');
|
||||
// console.log(debugUrl);
|
||||
// console.log('');
|
||||
// } catch (e) {
|
||||
// console.error('[Debug] Failed to generate debug URL:', e);
|
||||
// }
|
||||
// -------------------------------------------
|
||||
|
||||
const bodyData = 'data=' + JSON.stringify(payload);
|
||||
logger.info('GameServer', 'Sending player_login request...');
|
||||
logger.debug('GameServer', 'Payload:', payload);
|
||||
|
||||
const response = await axios.post(targetUrl, bodyData, {
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||
});
|
||||
logger.info('GameServer', 'Login Response received');
|
||||
logger.debug('GameServer', 'Response Data:', response.data);
|
||||
|
||||
res.json(response.data);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('API', 'Player Login Error:', error.message);
|
||||
res.status(500).json({ error: '登录失败: ' + error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 6. 获取验证码接口
|
||||
app.post('/api/getPhoneCode', async (req, res) => {
|
||||
const { phonenum } = req.body;
|
||||
logger.divider('Get Phone Code');
|
||||
logger.info('API', 'Received GetCode Request:', phonenum);
|
||||
|
||||
try {
|
||||
if (!cachedGameServerHttp) {
|
||||
const { agentid, gameid, channelid, marketid } = config.remoteConfig;
|
||||
cachedGameServerHttp = remoteConfig.getParaValue('game_server_http', agentid, gameid, channelid, marketid);
|
||||
}
|
||||
|
||||
if (!cachedGameServerHttp) {
|
||||
return res.status(500).json({ error: 'Configuration error: game_server_http not found' });
|
||||
}
|
||||
|
||||
const targetUrl = cachedGameServerHttp.startsWith('http') ? cachedGameServerHttp : `http://${cachedGameServerHttp}`;
|
||||
const { agentid } = config.remoteConfig;
|
||||
|
||||
const payload = {
|
||||
"app": "youle",
|
||||
"route": "agent",
|
||||
"rpc": "get_phone_code_wechat",
|
||||
"data": {
|
||||
"agentid": agentid,
|
||||
"phonenum": phonenum
|
||||
}
|
||||
};
|
||||
|
||||
// --- Debug: 生成可粘贴到浏览器的 GET 链接 ---
|
||||
// try {
|
||||
// const debugQuery = `data=${encodeURIComponent(JSON.stringify(payload))}`;
|
||||
// const debugUrl = `${targetUrl}?${debugQuery}`;
|
||||
// console.log('\n[Debug] Request URL for Browser:');
|
||||
// console.log(debugUrl);
|
||||
// console.log('');
|
||||
// } catch (e) {
|
||||
// console.error('[Debug] Failed to generate debug URL:', e);
|
||||
// }
|
||||
// -------------------------------------------
|
||||
|
||||
const bodyData = 'data=' + JSON.stringify(payload);
|
||||
logger.info('GameServer', 'Sending get_phone_code_wechat request...');
|
||||
|
||||
const response = await axios.post(targetUrl, bodyData, {
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||
});
|
||||
logger.info('GameServer', 'GetCode Response received');
|
||||
logger.debug('GameServer', 'Response Data:', response.data);
|
||||
|
||||
res.json(response.data);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('API', 'Get Phone Code Error:', error.message);
|
||||
res.status(500).json({ error: '获取随机数失败: ' + error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 启动远程配置更新
|
||||
remoteConfig.start();
|
||||
|
||||
app.listen(port, () => {
|
||||
logger.divider();
|
||||
logger.success('System', `Server running at http://localhost:${port}`);
|
||||
logger.info('System', 'Press Ctrl+C to stop');
|
||||
});
|
||||
2655
codes/minipro/wxserver/package-lock.json
generated
Normal file
2655
codes/minipro/wxserver/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
codes/minipro/wxserver/package.json
Normal file
18
codes/minipro/wxserver/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "calculation-server",
|
||||
"version": "1.0.0",
|
||||
"description": "Backend for WeChat Mini Program Calculator",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.958.0",
|
||||
"@aws-sdk/lib-storage": "^3.958.0",
|
||||
"axios": "^1.6.0",
|
||||
"body-parser": "^1.20.2",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
"multer": "^2.0.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
cYmJAn4NiUsc7oAH
|
||||
317
codes/minipro/wxserver/services/remoteConfig.js
Normal file
317
codes/minipro/wxserver/services/remoteConfig.js
Normal file
@@ -0,0 +1,317 @@
|
||||
const axios = require('axios');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const config = require('../config/index');
|
||||
|
||||
class RemoteConfig {
|
||||
constructor() {
|
||||
this.data = null;
|
||||
|
||||
// 使用 Gitee/GitHub Raw URL
|
||||
if (config.remoteConfig.configUrl && config.remoteConfig.configUrl.startsWith('http')) {
|
||||
this.url = config.remoteConfig.configUrl;
|
||||
//console.log('RemoteConfig: Using direct configUrl:', this.url);
|
||||
} else {
|
||||
this.url = "";
|
||||
console.warn('RemoteConfig: Missing configUrl.');
|
||||
}
|
||||
|
||||
this.timer = null;
|
||||
this.localPath = path.join(__dirname, '../config/update_jsonv2.txt');
|
||||
this.updateCallbacks = [];
|
||||
}
|
||||
|
||||
onUpdate(callback) {
|
||||
if (typeof callback === 'function') {
|
||||
this.updateCallbacks.push(callback);
|
||||
}
|
||||
}
|
||||
|
||||
triggerUpdate() {
|
||||
this.updateCallbacks.forEach(cb => {
|
||||
try {
|
||||
cb();
|
||||
} catch (e) {
|
||||
console.error('Error in remote config update callback:', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
start() {
|
||||
this.fetchConfig();
|
||||
// 定时更新
|
||||
const interval = config.remoteConfig.interval || 30000;
|
||||
this.timer = setInterval(() => {
|
||||
this.fetchConfig();
|
||||
}, interval);
|
||||
}
|
||||
|
||||
async fetchConfig() {
|
||||
try {
|
||||
// 1. 尝试从远程 URL 获取配置
|
||||
// console.log('Fetching remote config from:', this.url);
|
||||
const response = await axios.get(this.url + "?" + Date.now(), { timeout: 5000 }); // 添加超时控制
|
||||
|
||||
if (response.data) {
|
||||
this.parseData(response.data);
|
||||
// console.log('Remote config updated from URL.');
|
||||
|
||||
// 可选:更新成功后,可以把最新的配置写入本地文件作为缓存
|
||||
// fs.writeFileSync(this.localPath, typeof response.data === 'string' ? response.data : JSON.stringify(response.data, null, 4));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching remote config:', error.message);
|
||||
|
||||
// 2. 远程获取失败,降级读取本地文件
|
||||
if (fs.existsSync(this.localPath)) {
|
||||
try {
|
||||
const fileContent = fs.readFileSync(this.localPath, 'utf-8');
|
||||
this.parseData(fileContent);
|
||||
//console.log('Fallback: Loaded config from local file:', this.localPath);
|
||||
} catch(e) {
|
||||
console.error('Fallback failed:', e);
|
||||
}
|
||||
} else {
|
||||
console.warn('No local config file found for fallback.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parseData(data) {
|
||||
let parsedData = null;
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
parsedData = JSON.parse(data.trim());
|
||||
} catch (e) {
|
||||
console.error('Failed to parse config JSON:', e);
|
||||
}
|
||||
} else {
|
||||
parsedData = data;
|
||||
}
|
||||
|
||||
if (parsedData) {
|
||||
this.data = parsedData;
|
||||
this.triggerUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
getParaValue(paraname, agentid, gameid, channelid, marketid) {
|
||||
let paravalue = null;
|
||||
|
||||
if (!this.data) {
|
||||
return paravalue;
|
||||
}
|
||||
|
||||
// 1. Root level
|
||||
if (this.data[paraname]) {
|
||||
paravalue = this.data[paraname];
|
||||
}
|
||||
|
||||
// Helper to find item in list and update paravalue
|
||||
const findAndCheck = (list, key, value) => {
|
||||
if (!list || !Array.isArray(list)) return null;
|
||||
for (const item of list) {
|
||||
if (item[key] == value) {
|
||||
if (item[paraname]) {
|
||||
paravalue = item[paraname];
|
||||
}
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 2. Agent level
|
||||
const agent = findAndCheck(this.data.agentlist, 'agentid', agentid);
|
||||
if (!agent) return paravalue;
|
||||
|
||||
// 3. Game level
|
||||
const game = findAndCheck(agent.gamelist, 'gameid', gameid);
|
||||
if (!game) return paravalue;
|
||||
|
||||
// 4. Channel level
|
||||
const channel = findAndCheck(game.channellist, 'channelid', channelid);
|
||||
if (!channel) return paravalue;
|
||||
|
||||
// 5. Market level
|
||||
const market = findAndCheck(channel.marketlist, 'marketid', marketid);
|
||||
|
||||
return paravalue;
|
||||
}
|
||||
|
||||
// 获取游戏列表_下载页面
|
||||
getGameListDownHtml(agentid, channelid) {
|
||||
const gamelist = [];
|
||||
if (!this.data || !this.data.agentlist) {
|
||||
return gamelist;
|
||||
}
|
||||
|
||||
for (const agent of this.data.agentlist) {
|
||||
if (agent.agentid == agentid) {
|
||||
if (agent.gamelist) {
|
||||
for (const game of agent.gamelist) {
|
||||
const o_game = {
|
||||
name: game.gamename,
|
||||
image: game.game_down_image,
|
||||
state: game.game_down_state,
|
||||
memo: game.game_down_memo
|
||||
};
|
||||
|
||||
if (game.channellist) {
|
||||
for (const channel of game.channellist) {
|
||||
if (channel.channelid == channelid) {
|
||||
const _ios_marketid = channel.ios_defdownload_marketid;
|
||||
const _and_marketid = channel.and_defdownload_marketid;
|
||||
|
||||
if (channel.marketlist) {
|
||||
for (const market of channel.marketlist) {
|
||||
if (market.marketid == _ios_marketid) {
|
||||
o_game.ios_down = market.app_download;
|
||||
o_game.ios_size = market.app_size;
|
||||
o_game.ios_marketid = _ios_marketid;
|
||||
}
|
||||
if (market.marketid == _and_marketid) {
|
||||
o_game.android_down = market.app_download;
|
||||
o_game.android_size = market.app_size;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (o_game.ios_down && o_game.android_down) {
|
||||
gamelist.push(o_game);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return gamelist;
|
||||
}
|
||||
|
||||
// 获取游戏列表_游戏大厅
|
||||
getGameListGameHall(agentid, channelid, marketid) {
|
||||
const gamelist = [];
|
||||
if (!this.data || !this.data.agentlist) {
|
||||
return gamelist;
|
||||
}
|
||||
|
||||
for (const agent of this.data.agentlist) {
|
||||
if (agent.agentid == agentid) {
|
||||
if (agent.gamelist) {
|
||||
for (const game of agent.gamelist) {
|
||||
if (game.gameid != "G2hw0ubng0zcoI0r4mx3H2yr4GejidwO") {
|
||||
const o_game = {
|
||||
gameid: game.gameid,
|
||||
gamename: game.gamename,
|
||||
gamedir: game.game_hall_dir,
|
||||
gameimage: game.game_hall_image,
|
||||
gameversion: game.game_version,
|
||||
gamezip: game.game_zip,
|
||||
zipsize: game.game_size
|
||||
};
|
||||
|
||||
if (game.channellist) {
|
||||
for (const channel of game.channellist) {
|
||||
if (channel.channelid == channelid) {
|
||||
if (channel.game_version > 0) {
|
||||
o_game.gameversion = channel.game_version;
|
||||
}
|
||||
if (channel.game_zip) {
|
||||
o_game.gamezip = channel.game_zip;
|
||||
}
|
||||
if (channel.game_size) {
|
||||
o_game.zipsize = channel.game_size;
|
||||
}
|
||||
|
||||
if (channel.marketlist) {
|
||||
for (const market of channel.marketlist) {
|
||||
if (market.marketid == marketid) {
|
||||
if (market.game_version > 0) {
|
||||
o_game.gameversion = market.game_version;
|
||||
}
|
||||
if (market.game_zip) {
|
||||
o_game.gamezip = market.game_zip;
|
||||
}
|
||||
if (market.game_size) {
|
||||
o_game.zipsize = market.game_size;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
gamelist.push(o_game);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return gamelist;
|
||||
}
|
||||
|
||||
// 获取代理城市列表
|
||||
getAgentList(agentid, channelid) {
|
||||
const agentlist = [];
|
||||
if (!this.data || !this.data.agentlist) {
|
||||
return agentlist;
|
||||
}
|
||||
|
||||
for (const agent of this.data.agentlist) {
|
||||
if (agent.agentid == agentid || agent.relagentid == agentid) {
|
||||
const o_agent = {
|
||||
agentid: agent.agentid,
|
||||
name: agent.agentname,
|
||||
// 注意:这里假设 gamelist 和 channellist 存在且非空,参考原逻辑
|
||||
channelid: (agent.gamelist && agent.gamelist[0] && agent.gamelist[0].channellist && agent.gamelist[0].channellist[0]) ? agent.gamelist[0].channellist[0].channelid : ''
|
||||
};
|
||||
agentlist.push(o_agent);
|
||||
}
|
||||
}
|
||||
return agentlist;
|
||||
}
|
||||
|
||||
// 获取子游戏服务器列表
|
||||
getGameServerList(agentid) {
|
||||
const iplist = [];
|
||||
const paraname = "game_server_http";
|
||||
|
||||
const doPushToIpList = (ip) => {
|
||||
if (ip && !iplist.includes(ip)) {
|
||||
iplist.push(ip);
|
||||
}
|
||||
};
|
||||
|
||||
if (!this.data || !this.data.agentlist) {
|
||||
return iplist;
|
||||
}
|
||||
|
||||
const agent = this.data.agentlist.find(a => a.agentid === agentid);
|
||||
if (!agent) {
|
||||
return iplist;
|
||||
}
|
||||
|
||||
if (this.data[paraname]) {
|
||||
doPushToIpList(this.data[paraname]);
|
||||
}
|
||||
if (agent[paraname]) {
|
||||
doPushToIpList(agent[paraname]);
|
||||
}
|
||||
if (agent.gamelist) {
|
||||
for (const game of agent.gamelist) {
|
||||
if (game[paraname]) {
|
||||
doPushToIpList(game[paraname]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return iplist;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new RemoteConfig();
|
||||
1
codes/minipro/wxserver/sql/player.txt
Normal file
1
codes/minipro/wxserver/sql/player.txt
Normal file
@@ -0,0 +1 @@
|
||||
"idx" "play_agentid" "play_playerid" "play_channelid" "play_openid" "play_unionid" "play_nickname" "play_avatar" "play_sex" "play_province" "play_city" "play_roomcard" "play_bean" "play_regtime" "play_lasttime" "play_logindate" "play_usecard" "play_taskaward" "play_type" "play_score" "play_a_country" "play_a_province" "play_a_city" "play_a_citycode" "play_a_district" "play_a_street" "play_a_address" "play_longitude" "play_latitude" "play_invitecode" "play_inviteid" "play_state" "play_advanced" "play_shortcode" "play_roomcodes" "play_desone" "play_destwo" "play_whitelist" "play_limit" "play_notice" "play_bankpower" "play_bank" "play_bankpwd" "play_tel" "play_wechat" "play_marketid" "play_phoneinfo" "play_sign"
|
||||
7
codes/minipro/wxserver/start_server.bat
Normal file
7
codes/minipro/wxserver/start_server.bat
Normal file
@@ -0,0 +1,7 @@
|
||||
@echo off
|
||||
title BaibaoxiangServer
|
||||
cd /d "%~dp0"
|
||||
echo Starting Baibaoxiang Server...
|
||||
echo Server will run on port 3000 (or configured port)
|
||||
node index.js
|
||||
pause
|
||||
12
codes/minipro/wxserver/start_server_colored.bat
Normal file
12
codes/minipro/wxserver/start_server_colored.bat
Normal file
@@ -0,0 +1,12 @@
|
||||
@echo off
|
||||
title WeChat Server - Running
|
||||
color 0A
|
||||
cls
|
||||
|
||||
:: 切换到脚本所在目录,确保 node 能找到 index.js
|
||||
cd /d "%~dp0"
|
||||
|
||||
echo Starting WeChat Server...
|
||||
echo.
|
||||
node index.js
|
||||
pause
|
||||
21
codes/minipro/wxserver/stop_server.bat
Normal file
21
codes/minipro/wxserver/stop_server.bat
Normal file
@@ -0,0 +1,21 @@
|
||||
@echo off
|
||||
setlocal
|
||||
set PORT=3000
|
||||
echo Looking for process on port %PORT%...
|
||||
|
||||
:: 查找占用端口 3000 的进程 PID
|
||||
set PID=
|
||||
for /f "tokens=5" %%a in ('netstat -aon ^| findstr ":%PORT% " ^| findstr "LISTENING"') do (
|
||||
set PID=%%a
|
||||
)
|
||||
|
||||
if defined PID (
|
||||
echo Found process with PID: %PID%
|
||||
echo Killing process...
|
||||
taskkill /F /PID %PID%
|
||||
echo Server stopped successfully.
|
||||
) else (
|
||||
echo No server found running on port %PORT%.
|
||||
)
|
||||
|
||||
pause
|
||||
101
codes/minipro/wxserver/utils/logger.js
Normal file
101
codes/minipro/wxserver/utils/logger.js
Normal file
@@ -0,0 +1,101 @@
|
||||
const util = require('util');
|
||||
const config = require('../config');
|
||||
|
||||
// ANSI 颜色代码
|
||||
const colors = {
|
||||
reset: "\x1b[0m",
|
||||
bright: "\x1b[1m",
|
||||
dim: "\x1b[2m",
|
||||
underscore: "\x1b[4m",
|
||||
blink: "\x1b[5m",
|
||||
reverse: "\x1b[7m",
|
||||
hidden: "\x1b[8m",
|
||||
|
||||
fg: {
|
||||
black: "\x1b[30m",
|
||||
red: "\x1b[31m",
|
||||
green: "\x1b[32m",
|
||||
yellow: "\x1b[33m",
|
||||
blue: "\x1b[34m",
|
||||
magenta: "\x1b[35m",
|
||||
cyan: "\x1b[36m",
|
||||
white: "\x1b[37m",
|
||||
gray: "\x1b[90m",
|
||||
},
|
||||
bg: {
|
||||
black: "\x1b[40m",
|
||||
red: "\x1b[41m",
|
||||
green: "\x1b[42m",
|
||||
yellow: "\x1b[43m",
|
||||
blue: "\x1b[44m",
|
||||
magenta: "\x1b[45m",
|
||||
cyan: "\x1b[46m",
|
||||
white: "\x1b[47m",
|
||||
}
|
||||
};
|
||||
|
||||
function formatTime() {
|
||||
const now = new Date();
|
||||
const timeStr = now.toLocaleTimeString('zh-CN', { hour12: false });
|
||||
const ms = String(now.getMilliseconds()).padStart(3, '0');
|
||||
return `${timeStr}.${ms}`;
|
||||
}
|
||||
|
||||
const logger = {
|
||||
// 普通信息 - 青色标签
|
||||
info: (category, message, ...args) => {
|
||||
if (!config.enableLog) return;
|
||||
const time = `${colors.fg.gray}[${formatTime()}]${colors.reset}`;
|
||||
const tag = `${colors.fg.cyan}[${category}]${colors.reset}`;
|
||||
console.log(`${time} ${tag} ${message}`, ...args);
|
||||
},
|
||||
|
||||
// 成功信息 - 绿色标签
|
||||
success: (category, message, ...args) => {
|
||||
if (!config.enableLog) return;
|
||||
const time = `${colors.fg.gray}[${formatTime()}]${colors.reset}`;
|
||||
const tag = `${colors.fg.green}[${category}]${colors.reset}`;
|
||||
console.log(`${time} ${tag} ${message}`, ...args);
|
||||
},
|
||||
|
||||
// 警告信息 - 黄色标签
|
||||
warn: (category, message, ...args) => {
|
||||
if (!config.enableLog) return;
|
||||
const time = `${colors.fg.gray}[${formatTime()}]${colors.reset}`;
|
||||
const tag = `${colors.fg.yellow}[${category}]${colors.reset}`;
|
||||
console.warn(`${time} ${tag} ${message}`, ...args);
|
||||
},
|
||||
|
||||
// 错误信息 - 红色标签
|
||||
error: (category, message, ...args) => {
|
||||
if (!config.enableLog) return;
|
||||
const time = `${colors.fg.gray}[${formatTime()}]${colors.reset}`;
|
||||
const tag = `${colors.fg.red}[${category}]${colors.reset}`;
|
||||
console.error(`${time} ${tag} ${message}`, ...args);
|
||||
},
|
||||
|
||||
// 调试信息 - 洋红色标签 (对象会自动展开)
|
||||
debug: (category, message, ...args) => {
|
||||
if (!config.enableLog) return;
|
||||
const time = `${colors.fg.gray}[${formatTime()}]${colors.reset}`;
|
||||
const tag = `${colors.fg.magenta}[${category}]${colors.reset}`;
|
||||
// 使用 util.inspect 格式化对象,使其带有颜色且深度无限
|
||||
const formattedArgs = args.map(arg =>
|
||||
(typeof arg === 'object' && arg !== null)
|
||||
? '\n' + util.inspect(arg, { colors: true, depth: null, breakLength: 80 })
|
||||
: arg
|
||||
);
|
||||
console.log(`${time} ${tag} ${message}`, ...formattedArgs);
|
||||
},
|
||||
|
||||
// 分割线 - 用于区分请求
|
||||
divider: (title) => {
|
||||
if (!config.enableLog) return;
|
||||
console.log(`${colors.fg.gray}----------------------------------------------------------------${colors.reset}`);
|
||||
if (title) {
|
||||
console.log(`${colors.fg.white}${colors.bright}👉 ${title}${colors.reset}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = logger;
|
||||
Reference in New Issue
Block a user