Compare commits

...

2 Commits

Author SHA1 Message Date
75760d25fd feat: 添加选手详情与比赛记录接口以支持小程序端
- 新增 `/api/ladder/player` 接口,兼容小程序端选手详情查询
- 新增 `/api/match/history` 接口,用于获取选手比赛记录
- 选手详情接口增加 `loseCount` 字段,完善比赛数据统计
- 比赛记录接口提供分页查询,包含对手信息与比赛结果详情
2026-01-30 02:47:41 +08:00
74ed19eee1 feat: 新增选手资料页面并优化界面设计
- 新增选手资料页面,支持查看选手信息及比赛记录
- 添加多个SVG图标替换原有emoji,提升视觉一致性
- 扩展CSS变量系统,增加信息、成功、警告、危险等状态颜色
- 优化多个页面的样式,统一使用CSS变量和现代设计语言
- 修复可选链操作符兼容性问题,改用传统条件判断
- 改进数据加载逻辑,使用Object.assign替代展开运算符
- 调整开发环境配置,使用本地服务器地址
2026-01-30 02:24:03 +08:00
36 changed files with 1621 additions and 822 deletions

View File

@ -81,9 +81,9 @@ App({
sessionKey: wxInfo.sessionKey, sessionKey: wxInfo.sessionKey,
encryptedData, encryptedData,
iv, iv,
nickname: userProfile?.nickName || "", nickname: (userProfile && userProfile.nickName) || "",
avatar: userProfile?.avatarUrl || "", avatar: (userProfile && userProfile.avatarUrl) || "",
gender: userProfile?.gender || 0, gender: (userProfile && userProfile.gender) || 0,
}, },
success: (loginRes) => { success: (loginRes) => {
if (loginRes.data.code === 0) { if (loginRes.data.code === 0) {
@ -93,7 +93,7 @@ App({
// 处理天梯用户信息 // 处理天梯用户信息
if (loginRes.data.data.userInfo.ladderUsers && loginRes.data.data.userInfo.ladderUsers.length > 0) { if (loginRes.data.data.userInfo.ladderUsers && loginRes.data.data.userInfo.ladderUsers.length > 0) {
// 如果有当前门店,优先选择当前门店的天梯用户 // 如果有当前门店,优先选择当前门店的天梯用户
if (this.globalData.currentStore?.storeId) { if (this.globalData.currentStore && this.globalData.currentStore.storeId) {
const currentStoreLadderUser = loginRes.data.data.userInfo.ladderUsers.find( const currentStoreLadderUser = loginRes.data.data.userInfo.ladderUsers.find(
lu => lu.storeId === this.globalData.currentStore.storeId lu => lu.storeId === this.globalData.currentStore.storeId
); );
@ -139,7 +139,7 @@ App({
// 处理天梯用户信息 // 处理天梯用户信息
if (res.data.ladderUsers && res.data.ladderUsers.length > 0) { if (res.data.ladderUsers && res.data.ladderUsers.length > 0) {
// 如果有当前门店,优先选择当前门店的天梯用户 // 如果有当前门店,优先选择当前门店的天梯用户
if (this.globalData.currentStore?.storeId) { if (this.globalData.currentStore && this.globalData.currentStore.storeId) {
const currentStoreLadderUser = res.data.ladderUsers.find( const currentStoreLadderUser = res.data.ladderUsers.find(
lu => lu.storeId === this.globalData.currentStore.storeId lu => lu.storeId === this.globalData.currentStore.storeId
); );
@ -179,11 +179,11 @@ App({
this.globalData.currentStore = res.data; this.globalData.currentStore = res.data;
// 如果当前门店有 ladderUserId获取该门店的天梯用户信息 // 如果当前门店有 ladderUserId获取该门店的天梯用户信息
if (res.data?.ladderUserId) { if (res.data && res.data.ladderUserId) {
this.getLadderUser(res.data.storeId); this.getLadderUser(res.data.storeId);
} else if (res.data?.storeId) { } else if (res.data && res.data.storeId) {
// 如果当前门店没有 ladderUserId但用户信息中有该门店的天梯用户使用它 // 如果当前门店没有 ladderUserId但用户信息中有该门店的天梯用户使用它
if (this.globalData.userInfo?.ladderUsers) { if (this.globalData.userInfo && this.globalData.userInfo.ladderUsers) {
const currentStoreLadderUser = this.globalData.userInfo.ladderUsers.find( const currentStoreLadderUser = this.globalData.userInfo.ladderUsers.find(
lu => lu.storeId === res.data.storeId lu => lu.storeId === res.data.storeId
); );
@ -207,11 +207,11 @@ App({
this.globalData.currentStore = res.data; this.globalData.currentStore = res.data;
// 如果当前门店有 ladderUserId获取该门店的天梯用户信息 // 如果当前门店有 ladderUserId获取该门店的天梯用户信息
if (res.data?.ladderUserId) { if (res.data && res.data.ladderUserId) {
this.getLadderUser(res.data.storeId); this.getLadderUser(res.data.storeId);
} else if (res.data?.storeId) { } else if (res.data && res.data.storeId) {
// 如果当前门店没有 ladderUserId但用户信息中有该门店的天梯用户使用它 // 如果当前门店没有 ladderUserId但用户信息中有该门店的天梯用户使用它
if (this.globalData.userInfo?.ladderUsers) { if (this.globalData.userInfo && this.globalData.userInfo.ladderUsers) {
const currentStoreLadderUser = this.globalData.userInfo.ladderUsers.find( const currentStoreLadderUser = this.globalData.userInfo.ladderUsers.find(
lu => lu.storeId === res.data.storeId lu => lu.storeId === res.data.storeId
); );

View File

@ -1,6 +1,7 @@
{ {
"pages": [ "pages": [
"pages/index/index", "pages/index/index",
"pages/player/index",
"pages/user/index", "pages/user/index",
"pages/match/challenge/index", "pages/match/challenge/index",
"pages/match/challenge-detail/index", "pages/match/challenge-detail/index",

View File

@ -26,6 +26,19 @@ page {
--accent-light: #e6fbf7; --accent-light: #e6fbf7;
--accent-soft: rgba(0, 201, 167, 0.1); --accent-soft: rgba(0, 201, 167, 0.1);
--info: #3b82f6;
--info-soft: rgba(59, 130, 246, 0.12);
--info-text: #1d4ed8;
--success: #16a34a;
--success-soft: rgba(22, 163, 74, 0.12);
--success-text: #166534;
--warning: #ffba08;
--warning-soft: rgba(255, 186, 8, 0.14);
--warning-text: #8a5a00;
--danger: #ef4444;
--danger-soft: rgba(239, 68, 68, 0.12);
--danger-text: #b91c1c;
/* 浅色背景系 */ /* 浅色背景系 */
--bg-page: #f7f8fa; --bg-page: #f7f8fa;
--bg-white: #ffffff; --bg-white: #ffffff;

View File

@ -6,9 +6,9 @@
// 开发环境配置 // 开发环境配置
const devConfig = { const devConfig = {
// API 基础地址(本地开发) // API 基础地址(本地开发)
baseUrl: "https://yingsa-server.ethan.team", baseUrl: "http://127.0.0.1:3000",
// WebSocket 地址(本地开发) // WebSocket 地址(本地开发)
wsUrl: "wss://yingsa-server.ethan.team/ws", wsUrl: "ws://127.0.0.1:3000/ws",
}; };
// 生产环境配置 // 生产环境配置
@ -25,7 +25,10 @@ const prodConfig = {
const getEnv = () => { const getEnv = () => {
try { try {
// 尝试获取微信环境 // 尝试获取微信环境
const envVersion = __wxConfig?.envVersion || "develop"; const envVersion =
typeof __wxConfig !== "undefined" && __wxConfig && __wxConfig.envVersion
? __wxConfig.envVersion
: "develop";
return envVersion === "release" ? "production" : "development"; return envVersion === "release" ? "production" : "development";
} catch (e) { } catch (e) {
return "development"; return "development";
@ -35,14 +38,9 @@ const getEnv = () => {
const env = getEnv(); const env = getEnv();
const config = env === "production" ? prodConfig : devConfig; const config = env === "production" ? prodConfig : devConfig;
module.exports = { module.exports = Object.assign({}, config, {
...config,
env, env,
// 其他配置项
// 上传文件大小限制 (MB)
uploadMaxSize: 5, uploadMaxSize: 5,
// 请求超时时间 (ms)
requestTimeout: 30000, requestTimeout: 30000,
// 版本号
version: "1.0.0", version: "1.0.0",
}; });

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<rect x="14" y="4" width="20" height="40" rx="4" fill="#666666"/>
<rect x="16.5" y="8" width="15" height="28" rx="2" fill="#999999"/>
<circle cx="24" cy="39.5" r="1.6" fill="#666666"/>
</svg>

After

Width:  |  Height:  |  Size: 282 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<path fill="#666666" d="M24 4l16 6v14c0 10.7-6.8 18.8-16 20-9.2-1.2-16-9.3-16-20V10l16-6z"/>
<path fill="#999999" d="M24 10l10 3.8V24c0 7-4.3 12.8-10 14-5.7-1.2-10-7-10-14V13.8L24 10z"/>
</svg>

After

Width:  |  Height:  |  Size: 282 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<circle cx="24" cy="18" r="8" fill="#666666"/>
<path d="M10 44c0-8 6.3-14 14-14s14 6 14 14" fill="#999999"/>
</svg>

After

Width:  |  Height:  |  Size: 204 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<circle cx="18" cy="18" r="7" fill="#666666"/>
<circle cx="32" cy="19" r="6" fill="#777777"/>
<path d="M6 44c0-7.5 5.8-13.5 13-13.5S32 36.5 32 44" fill="#999999"/>
<path d="M24 44c0-6.5 4.8-11.5 11-11.5S46 37.5 46 44" fill="#b0b0b0"/>
</svg>

After

Width:  |  Height:  |  Size: 334 B

View File

@ -1,24 +1,26 @@
const app = getApp() const app = getApp();
const util = require('../../utils/util') const util = require("../../utils/util");
Page({ Page({
data: { data: {
currentStore: null, currentStore: null,
gender: '', gender: "",
list: [], list: [],
loading: false, loading: false,
page: 1, page: 1,
pageSize: 20, pageSize: 20,
hasMore: true hasMore: true,
}, },
onLoad() { onLoad() {
this.initData() this.initData();
}, },
onShow() { onShow() {
const newStore = app.globalData.currentStore const newStore = app.globalData.currentStore;
const oldStoreId = this.data.currentStore?.storeId const oldStoreId = this.data.currentStore
? this.data.currentStore.storeId
: null;
// 检查门店是否切换 // 检查门店是否切换
if (newStore && newStore.storeId !== oldStoreId) { if (newStore && newStore.storeId !== oldStoreId) {
@ -26,32 +28,32 @@ Page({
currentStore: newStore, currentStore: newStore,
page: 1, page: 1,
hasMore: true, hasMore: true,
list: [] list: [],
}) });
this.fetchData() this.fetchData();
} else if (app.globalData.storeChanged) { } else if (app.globalData.storeChanged) {
// 全局标记门店已切换 // 全局标记门店已切换
app.globalData.storeChanged = false app.globalData.storeChanged = false;
this.setData({ this.setData({
currentStore: newStore, currentStore: newStore,
page: 1, page: 1,
hasMore: true, hasMore: true,
list: [] list: [],
}) });
this.fetchData() this.fetchData();
} }
}, },
onPullDownRefresh() { onPullDownRefresh() {
this.setData({ page: 1, hasMore: true }) this.setData({ page: 1, hasMore: true });
this.fetchData().then(() => { this.fetchData().then(() => {
wx.stopPullDownRefresh() wx.stopPullDownRefresh();
}) });
}, },
onReachBottom() { onReachBottom() {
if (this.data.hasMore && !this.data.loading) { if (this.data.hasMore && !this.data.loading) {
this.loadMore() this.loadMore();
} }
}, },
@ -59,66 +61,76 @@ Page({
// 检查是否已登录(有 token // 检查是否已登录(有 token
if (!app.globalData.token) { if (!app.globalData.token) {
// 未登录,跳转到用户页面进行登录 // 未登录,跳转到用户页面进行登录
wx.switchTab({ url: '/pages/user/index' }) wx.switchTab({ url: "/pages/user/index" });
return return;
} }
// 获取当前门店 // 获取当前门店
try { try {
const store = await app.getCurrentStore() const store = await app.getCurrentStore();
this.setData({ currentStore: store }) this.setData({ currentStore: store });
this.fetchData() this.fetchData();
} catch (e) { } catch (e) {
console.error('获取门店失败:', e) console.error("获取门店失败:", e);
// 如果是认证失败,跳转到登录页 // 如果是认证失败,跳转到登录页
if (e.code === 401) { if (e.code === 401) {
wx.switchTab({ url: '/pages/user/index' }) wx.switchTab({ url: "/pages/user/index" });
} }
} }
}, },
async fetchData() { async fetchData() {
if (!this.data.currentStore?.storeId) return if (!this.data.currentStore || !this.data.currentStore.storeId) return;
this.setData({ loading: true }) this.setData({ loading: true });
try { try {
const res = await app.request('/api/ladder/ranking', { const res = await app.request("/api/ladder/ranking", {
store_id: this.data.currentStore.storeId, store_id: this.data.currentStore.storeId,
gender: this.data.gender, gender: this.data.gender,
page: this.data.page, page: this.data.page,
pageSize: this.data.pageSize pageSize: this.data.pageSize,
}) });
const list = res.data.list || [] const list = res.data.list || [];
this.setData({ this.setData({
list: this.data.page === 1 ? list : [...this.data.list, ...list], list: this.data.page === 1 ? list : this.data.list.concat(list),
hasMore: list.length >= this.data.pageSize hasMore: list.length >= this.data.pageSize,
}) });
} catch (e) { } catch (e) {
console.error('获取排名失败:', e) console.error("获取排名失败:", e);
} finally { } finally {
this.setData({ loading: false }) this.setData({ loading: false });
} }
}, },
loadMore() { loadMore() {
this.setData({ page: this.data.page + 1 }) this.setData({ page: this.data.page + 1 });
this.fetchData() this.fetchData();
}, },
setGender(e) { setGender(e) {
const gender = e.currentTarget.dataset.gender const gender = e.currentTarget.dataset.gender;
this.setData({ gender, page: 1, hasMore: true }) this.setData({ gender, page: 1, hasMore: true });
this.fetchData() this.fetchData();
}, },
selectStore() { selectStore() {
wx.navigateTo({ url: '/pages/store/index' }) wx.navigateTo({ url: "/pages/store/index" });
}, },
viewPlayer(e) { viewPlayer(e) {
const id = e.currentTarget.dataset.id const player = e.currentTarget.dataset.player;
wx.navigateTo({ url: `/pages/player/index?id=${id}` }) const id = player && player.id ? player.id : e.currentTarget.dataset.id;
} if (!id) return;
})
wx.navigateTo({
url: `/pages/player/index?id=${id}`,
success: (res) => {
if (res && res.eventChannel && player) {
res.eventChannel.emit("player", player);
}
},
});
},
});

View File

@ -41,14 +41,14 @@
bindtap="setGender" bindtap="setGender"
data-gender="1" data-gender="1"
> >
男子 男子
</view> </view>
<view <view
class="filter-item {{gender === '2' ? 'active' : ''}}" class="filter-item {{gender === '2' ? 'active' : ''}}"
bindtap="setGender" bindtap="setGender"
data-gender="2" data-gender="2"
> >
女子 女子
</view> </view>
</view> </view>
</view> </view>
@ -62,11 +62,11 @@
wx:key="id" wx:key="id"
bindtap="viewPlayer" bindtap="viewPlayer"
data-id="{{item.id}}" data-id="{{item.id}}"
data-player="{{item}}"
> >
<!-- 排名徽章 --> <!-- 排名徽章 -->
<view class="rank-badge {{item.rank === 1 ? 'top1' : item.rank === 2 ? 'top2' : item.rank === 3 ? 'top3' : 'normal'}}"> <view class="rank-badge {{item.rank === 1 ? 'top1' : item.rank === 2 ? 'top2' : item.rank === 3 ? 'top3' : 'normal'}}">
<text wx:if="{{item.rank <= 3}}">{{item.rank === 1 ? '👑' : item.rank === 2 ? '🥈' : '🥉'}}</text> <text>{{item.rank}}</text>
<text wx:else>{{item.rank}}</text>
</view> </view>
<!-- 选手头像 --> <!-- 选手头像 -->

View File

@ -49,7 +49,7 @@
width: 12rpx; width: 12rpx;
height: 12rpx; height: 12rpx;
border-radius: 50%; border-radius: 50%;
background: #ff6b35; background: var(--primary);
box-shadow: 0 0 8rpx rgba(255, 107, 53, 0.4); box-shadow: 0 0 8rpx rgba(255, 107, 53, 0.4);
animation: pulse 2s ease-in-out infinite; animation: pulse 2s ease-in-out infinite;
} }
@ -57,7 +57,7 @@
.store-name { .store-name {
font-size: 30rpx; font-size: 30rpx;
font-weight: 600; font-weight: 600;
color: #333; color: var(--text-primary);
letter-spacing: 0.5rpx; letter-spacing: 0.5rpx;
} }
@ -77,13 +77,13 @@
.change-store-text { .change-store-text {
font-size: 24rpx; font-size: 24rpx;
color: #666; color: var(--text-secondary);
font-weight: 500; font-weight: 500;
} }
.change-store-arrow { .change-store-arrow {
font-size: 22rpx; font-size: 22rpx;
color: #999; color: var(--text-muted);
font-weight: 300; font-weight: 300;
} }
@ -100,7 +100,7 @@
display: block; display: block;
font-size: 52rpx; font-size: 52rpx;
font-weight: 700; font-weight: 700;
color: #1a1a1a; color: var(--text-primary);
margin-bottom: 8rpx; margin-bottom: 8rpx;
letter-spacing: 1rpx; letter-spacing: 1rpx;
} }
@ -108,7 +108,7 @@
.page-subtitle { .page-subtitle {
display: block; display: block;
font-size: 26rpx; font-size: 26rpx;
color: #999; color: var(--text-muted);
font-weight: 400; font-weight: 400;
letter-spacing: 0.5rpx; letter-spacing: 0.5rpx;
} }
@ -149,9 +149,9 @@
} }
.filter-item.active { .filter-item.active {
background: linear-gradient(135deg, #ff6b35 0%, #ff8c42 100%); background: var(--primary-gradient);
color: #fff; color: var(--text-white);
box-shadow: 0 4rpx 16rpx rgba(255, 107, 53, 0.3); box-shadow: var(--shadow-primary);
font-weight: 600; font-weight: 600;
} }
@ -168,12 +168,12 @@
display: flex; display: flex;
align-items: center; align-items: center;
padding: 24rpx; padding: 24rpx;
background: #fff; background: var(--bg-card);
border-radius: 20rpx; border-radius: 20rpx;
margin-bottom: 16rpx; margin-bottom: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08); box-shadow: var(--shadow-card);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 1rpx solid #f0f0f0; border: 1rpx solid var(--border-soft);
} }
.ranking-item:last-child { .ranking-item:last-child {
@ -186,9 +186,9 @@
} }
.ranking-item.top-rank { .ranking-item.top-rank {
background: linear-gradient(135deg, #fff5f0 0%, #fff 100%); background: linear-gradient(135deg, var(--primary-soft) 0%, var(--bg-white) 100%);
border: 2rpx solid rgba(255, 107, 53, 0.2); border: 2rpx solid var(--border-primary);
box-shadow: 0 6rpx 20rpx rgba(255, 107, 53, 0.15); box-shadow: var(--shadow-primary);
} }
/* 排名徽章 */ /* 排名徽章 */
@ -233,8 +233,8 @@
} }
.rank-badge.normal { .rank-badge.normal {
background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%); background: linear-gradient(135deg, var(--bg-soft) 0%, var(--bg-card-hover) 100%);
color: #666; color: var(--text-secondary);
font-weight: 600; font-weight: 600;
} }
@ -244,10 +244,10 @@
height: 80rpx; height: 80rpx;
border-radius: 50%; border-radius: 50%;
margin-right: 20rpx; margin-right: 20rpx;
border: 3rpx solid #fff; border: 3rpx solid var(--bg-white);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1); box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
flex-shrink: 0; flex-shrink: 0;
background: #f5f5f5; background: var(--bg-soft);
} }
/* 选手信息 */ /* 选手信息 */
@ -260,7 +260,7 @@
display: block; display: block;
font-size: 30rpx; font-size: 30rpx;
font-weight: 600; font-weight: 600;
color: #333; color: var(--text-primary);
margin-bottom: 8rpx; margin-bottom: 8rpx;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -292,7 +292,7 @@
.player-stats { .player-stats {
font-size: 24rpx; font-size: 24rpx;
color: #666; color: var(--text-secondary);
font-weight: 500; font-weight: 500;
} }
@ -306,14 +306,14 @@
display: block; display: block;
font-size: 36rpx; font-size: 36rpx;
font-weight: 700; font-weight: 700;
color: #ff6b35; color: var(--primary);
line-height: 1.2; line-height: 1.2;
margin-bottom: 4rpx; margin-bottom: 4rpx;
} }
.power-label { .power-label {
font-size: 22rpx; font-size: 22rpx;
color: #999; color: var(--text-muted);
font-weight: 500; font-weight: 500;
} }

View File

@ -198,8 +198,8 @@ Page({
canConfirmScore = true canConfirmScore = true
console.log('进行中状态:设置确认比分权限(对方已提交,等待确认)', { console.log('进行中状态:设置确认比分权限(对方已提交,等待确认)', {
submitBy: game.submitBy, submitBy: game.submitBy,
challengerId: matchInfo.challenger?.id, challengerId: matchInfo.challenger ? matchInfo.challenger.id : null,
defenderId: matchInfo.defender?.id, defenderId: matchInfo.defender ? matchInfo.defender.id : null,
myRole myRole
}) })
} }
@ -214,11 +214,11 @@ Page({
console.log('尝试通过游戏信息识别角色:', { console.log('尝试通过游戏信息识别角色:', {
player1Id: game.player1Id, player1Id: game.player1Id,
player2Id: game.player2Id, player2Id: game.player2Id,
challengerId: matchInfo.challenger?.id, challengerId: matchInfo.challenger ? matchInfo.challenger.id : null,
defenderId: matchInfo.defender?.id, defenderId: matchInfo.defender ? matchInfo.defender.id : null,
currentUserId: currentUser.id, currentUserId: currentUser.id,
challengerUserId: matchInfo.challenger?.userId, challengerUserId: matchInfo.challenger ? matchInfo.challenger.userId : null,
defenderUserId: matchInfo.defender?.userId defenderUserId: matchInfo.defender ? matchInfo.defender.userId : null
}) })
// 通过比较 challenger/defender 的 idladder_user_id和 player1_id/player2_id 来判断 // 通过比较 challenger/defender 的 idladder_user_id和 player1_id/player2_id 来判断
@ -262,8 +262,8 @@ Page({
// 如果游戏状态为2已提交且对方已提交等待我确认 // 如果游戏状态为2已提交且对方已提交等待我确认
else if (game.status === 2 && game.submitBy) { else if (game.status === 2 && game.submitBy) {
// 判断当前用户是否是提交者 // 判断当前用户是否是提交者
const isSubmitter = (myRole === 'challenger' && game.submitBy == matchInfo.challenger?.id) || const isSubmitter = (myRole === 'challenger' && matchInfo.challenger && game.submitBy == matchInfo.challenger.id) ||
(myRole === 'defender' && game.submitBy == matchInfo.defender?.id) (myRole === 'defender' && matchInfo.defender && game.submitBy == matchInfo.defender.id)
if (!isSubmitter && game.confirmStatus === 0) { if (!isSubmitter && game.confirmStatus === 0) {
canConfirmScore = true canConfirmScore = true
@ -317,10 +317,10 @@ Page({
canAccept, canAccept,
canReject, canReject,
matchInfoStatus: matchInfo.status, matchInfoStatus: matchInfo.status,
defenderUserId: matchInfo.defender?.userId, defenderUserId: matchInfo.defender ? matchInfo.defender.userId : null,
currentUserId: app.globalData.userInfo?.id, currentUserId: app.globalData.userInfo ? app.globalData.userInfo.id : null,
defenderPhone: matchInfo.defender?.phone, defenderPhone: matchInfo.defender ? matchInfo.defender.phone : null,
currentUserPhone: app.globalData.userInfo?.phone currentUserPhone: app.globalData.userInfo ? app.globalData.userInfo.phone : null
}) })
} }
} catch (e) { } catch (e) {
@ -471,7 +471,7 @@ Page({
// 确认比分 // 确认比分
async confirmScore(confirm) { async confirmScore(confirm) {
const game = this.data.matchInfo.games?.[0] const game = this.data.matchInfo.games && this.data.matchInfo.games[0]
if (!game) { if (!game) {
wx.showToast({ title: '比赛信息错误', icon: 'none' }) wx.showToast({ title: '比赛信息错误', icon: 'none' })
return return
@ -503,7 +503,7 @@ Page({
// 确认比分按钮 // 确认比分按钮
confirmScoreBtn() { confirmScoreBtn() {
const game = this.data.matchInfo.games?.[0] const game = this.data.matchInfo.games && this.data.matchInfo.games[0]
if (!game) { if (!game) {
wx.showToast({ title: '比赛信息错误', icon: 'none' }) wx.showToast({ title: '比赛信息错误', icon: 'none' })
return return
@ -521,12 +521,12 @@ Page({
myScore = game.player1Score || 0 myScore = game.player1Score || 0
opponentScore = game.player2Score || 0 opponentScore = game.player2Score || 0
myName = this.data.matchInfo.challenger.realName || '挑战者' myName = this.data.matchInfo.challenger.realName || '挑战者'
opponentName = this.data.matchInfo.defender?.realName || '被挑战者' opponentName = (this.data.matchInfo.defender && this.data.matchInfo.defender.realName) || '被挑战者'
} else if (this.data.matchInfo.challenger && this.data.matchInfo.challenger.id == game.player2Id) { } else if (this.data.matchInfo.challenger && this.data.matchInfo.challenger.id == game.player2Id) {
myScore = game.player2Score || 0 myScore = game.player2Score || 0
opponentScore = game.player1Score || 0 opponentScore = game.player1Score || 0
myName = this.data.matchInfo.challenger.realName || '挑战者' myName = this.data.matchInfo.challenger.realName || '挑战者'
opponentName = this.data.matchInfo.defender?.realName || '被挑战者' opponentName = (this.data.matchInfo.defender && this.data.matchInfo.defender.realName) || '被挑战者'
} else { } else {
// 如果无法确定,使用默认显示 // 如果无法确定,使用默认显示
myScore = game.player1Score || 0 myScore = game.player1Score || 0
@ -538,12 +538,12 @@ Page({
myScore = game.player1Score || 0 myScore = game.player1Score || 0
opponentScore = game.player2Score || 0 opponentScore = game.player2Score || 0
myName = this.data.matchInfo.defender.realName || '被挑战者' myName = this.data.matchInfo.defender.realName || '被挑战者'
opponentName = this.data.matchInfo.challenger?.realName || '挑战者' opponentName = (this.data.matchInfo.challenger && this.data.matchInfo.challenger.realName) || '挑战者'
} else if (this.data.matchInfo.defender && this.data.matchInfo.defender.id == game.player2Id) { } else if (this.data.matchInfo.defender && this.data.matchInfo.defender.id == game.player2Id) {
myScore = game.player2Score || 0 myScore = game.player2Score || 0
opponentScore = game.player1Score || 0 opponentScore = game.player1Score || 0
myName = this.data.matchInfo.defender.realName || '被挑战者' myName = this.data.matchInfo.defender.realName || '被挑战者'
opponentName = this.data.matchInfo.challenger?.realName || '挑战者' opponentName = (this.data.matchInfo.challenger && this.data.matchInfo.challenger.realName) || '挑战者'
} else { } else {
// 如果无法确定,使用默认显示 // 如果无法确定,使用默认显示
myScore = game.player1Score || 0 myScore = game.player1Score || 0

View File

@ -1,6 +1,6 @@
.page-container { .page-container {
min-height: 100vh; min-height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); background: linear-gradient(135deg, var(--bg-page) 0%, var(--primary-soft) 100%);
padding: 20rpx; padding: 20rpx;
} }
@ -9,14 +9,14 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
height: 60vh; height: 60vh;
color: #666; color: var(--text-secondary);
} }
.match-info { .match-info {
background: #fff; background: var(--bg-card);
border-radius: 24rpx; border-radius: var(--radius-lg);
padding: 40rpx; padding: 40rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1); box-shadow: var(--shadow-card);
} }
.match-header { .match-header {
@ -25,13 +25,13 @@
align-items: center; align-items: center;
margin-bottom: 40rpx; margin-bottom: 40rpx;
padding-bottom: 30rpx; padding-bottom: 30rpx;
border-bottom: 2rpx solid #f0f0f0; border-bottom: 2rpx solid var(--border-soft);
} }
.match-title { .match-title {
font-size: 36rpx; font-size: 36rpx;
font-weight: 600; font-weight: 600;
color: #333; color: var(--text-primary);
} }
.match-status { .match-status {
@ -41,23 +41,23 @@
} }
.status-0 { .status-0 {
background: #fff3cd; background: var(--warning-soft);
color: #856404; color: var(--warning-text);
} }
.status-1 { .status-1 {
background: #d1ecf1; background: var(--info-soft);
color: #0c5460; color: var(--info-text);
} }
.status-2 { .status-2 {
background: #d4edda; background: var(--success-soft);
color: #155724; color: var(--success-text);
} }
.status-3 { .status-3 {
background: #f8d7da; background: var(--danger-soft);
color: #721c24; color: var(--danger-text);
} }
.opponent-section { .opponent-section {
@ -70,7 +70,7 @@
.opponent-label { .opponent-label {
font-size: 24rpx; font-size: 24rpx;
color: #999; color: var(--text-muted);
margin-bottom: 20rpx; margin-bottom: 20rpx;
} }
@ -84,7 +84,7 @@
width: 100rpx; width: 100rpx;
height: 100rpx; height: 100rpx;
border-radius: 50%; border-radius: 50%;
border: 4rpx solid #e0e0e0; border: 4rpx solid var(--border-light);
} }
.opponent-details { .opponent-details {
@ -97,12 +97,12 @@
.opponent-name { .opponent-name {
font-size: 32rpx; font-size: 32rpx;
font-weight: 600; font-weight: 600;
color: #333; color: var(--text-primary);
} }
.opponent-level { .opponent-level {
font-size: 24rpx; font-size: 24rpx;
color: #666; color: var(--text-secondary);
} }
.vs-divider { .vs-divider {
@ -110,25 +110,25 @@
margin: 30rpx 0; margin: 30rpx 0;
font-size: 32rpx; font-size: 32rpx;
font-weight: 600; font-weight: 600;
color: #999; color: var(--text-muted);
} }
.match-progress { .match-progress {
margin-bottom: 40rpx; margin-bottom: 40rpx;
padding-top: 30rpx; padding-top: 30rpx;
border-top: 2rpx solid #f0f0f0; border-top: 2rpx solid var(--border-soft);
} }
.progress-title { .progress-title {
font-size: 28rpx; font-size: 28rpx;
font-weight: 600; font-weight: 600;
color: #333; color: var(--text-primary);
margin-bottom: 20rpx; margin-bottom: 20rpx;
} }
.game-item { .game-item {
background: #f8f9fa; background: var(--bg-soft);
border-radius: 16rpx; border-radius: var(--radius-md);
padding: 24rpx; padding: 24rpx;
margin-bottom: 16rpx; margin-bottom: 16rpx;
} }
@ -141,33 +141,33 @@
.score-label { .score-label {
font-size: 24rpx; font-size: 24rpx;
color: #666; color: var(--text-secondary);
} }
.score-value { .score-value {
font-size: 32rpx; font-size: 32rpx;
font-weight: 600; font-weight: 600;
color: #333; color: var(--text-primary);
margin-left: 12rpx; margin-left: 12rpx;
} }
.game-status { .game-status {
font-size: 24rpx; font-size: 24rpx;
color: #999; color: var(--text-muted);
} }
.confirm-tip { .confirm-tip {
margin-top: 16rpx; margin-top: 16rpx;
padding: 16rpx 20rpx; padding: 16rpx 20rpx;
background: linear-gradient(135deg, #fff5f0 0%, #ffe8d6 100%); background: var(--primary-gradient-soft);
border-radius: 12rpx; border-radius: var(--radius-sm);
border-left: 4rpx solid #ff6b35; border-left: 4rpx solid var(--primary);
box-shadow: 0 2rpx 8rpx rgba(255, 107, 53, 0.1); box-shadow: var(--shadow-sm);
} }
.confirm-tip .tip-text { .confirm-tip .tip-text {
font-size: 26rpx; font-size: 26rpx;
color: #d84315; color: var(--primary-dark);
font-weight: 500; font-weight: 500;
} }
@ -181,13 +181,13 @@
margin-top: 40rpx; margin-top: 40rpx;
padding: 30rpx; padding: 30rpx;
text-align: center; text-align: center;
background: #f8f9fa; background: var(--bg-soft);
border-radius: 16rpx; border-radius: var(--radius-md);
} }
.tip-text { .tip-text {
font-size: 28rpx; font-size: 28rpx;
color: #999; color: var(--text-muted);
} }
.action-btn { .action-btn {
@ -203,41 +203,42 @@
} }
.accept-btn { .accept-btn {
background: linear-gradient(135deg, #ff6b35 0%, #ff8c42 100%); background: var(--primary-gradient);
color: #fff; color: #fff;
box-shadow: 0 4rpx 16rpx rgba(255, 107, 53, 0.3); box-shadow: var(--shadow-primary);
} }
.accept-btn:active { .accept-btn:active {
background: linear-gradient(135deg, #e55a2b 0%, #e67e2f 100%); background: var(--primary-dark);
box-shadow: 0 2rpx 8rpx rgba(255, 107, 53, 0.4); box-shadow: var(--shadow-md);
} }
.reject-btn { .reject-btn {
background: #f5f5f5; background: var(--bg-white);
color: #666; color: var(--text-secondary);
border: 2rpx solid var(--border-light);
} }
.submit-btn { .submit-btn {
background: linear-gradient(135deg, #ff6b35 0%, #ff8c42 100%); background: var(--primary-gradient);
color: #fff; color: #fff;
box-shadow: 0 4rpx 16rpx rgba(255, 107, 53, 0.3); box-shadow: var(--shadow-primary);
} }
.submit-btn:active { .submit-btn:active {
background: linear-gradient(135deg, #e55a2b 0%, #e67e2f 100%); background: var(--primary-dark);
box-shadow: 0 2rpx 8rpx rgba(255, 107, 53, 0.4); box-shadow: var(--shadow-md);
} }
.confirm-btn { .confirm-btn {
background: linear-gradient(135deg, #ff6b35 0%, #ff8c42 100%); background: var(--primary-gradient);
color: #fff; color: #fff;
box-shadow: 0 4rpx 16rpx rgba(255, 107, 53, 0.3); box-shadow: var(--shadow-primary);
} }
.confirm-btn:active { .confirm-btn:active {
background: linear-gradient(135deg, #e55a2b 0%, #e67e2f 100%); background: var(--primary-dark);
box-shadow: 0 2rpx 8rpx rgba(255, 107, 53, 0.4); box-shadow: var(--shadow-md);
} }
/* 填写比分弹框 */ /* 填写比分弹框 */
@ -257,10 +258,10 @@
.score-modal-content { .score-modal-content {
width: 600rpx; width: 600rpx;
max-width: 90%; max-width: 90%;
background: #fff; background: var(--bg-card);
border-radius: 24rpx; border-radius: var(--radius-lg);
overflow: hidden; overflow: hidden;
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.15); box-shadow: var(--shadow-lg);
} }
.modal-header { .modal-header {
@ -269,7 +270,7 @@
align-items: center; align-items: center;
padding: 32rpx 40rpx; padding: 32rpx 40rpx;
border-bottom: 2rpx solid rgba(255, 255, 255, 0.2); border-bottom: 2rpx solid rgba(255, 255, 255, 0.2);
background: linear-gradient(135deg, #ff6b35 0%, #ff8c42 100%); background: var(--primary-gradient);
} }
.modal-header .modal-title { .modal-header .modal-title {
@ -311,7 +312,7 @@
.input-label { .input-label {
display: block; display: block;
font-size: 28rpx; font-size: 28rpx;
color: #333; color: var(--text-primary);
font-weight: 500; font-weight: 500;
margin-bottom: 12rpx; margin-bottom: 12rpx;
} }
@ -319,27 +320,27 @@
.score-input { .score-input {
width: 100%; width: 100%;
height: 88rpx; height: 88rpx;
background: #fff; background: var(--bg-white);
border-radius: 12rpx; border-radius: var(--radius-sm);
padding: 0 24rpx; padding: 0 24rpx;
font-size: 32rpx; font-size: 32rpx;
color: #333; color: var(--text-primary);
border: 2rpx solid #e0e0e0; border: 2rpx solid var(--border-light);
box-sizing: border-box; box-sizing: border-box;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.score-input:focus { .score-input:focus {
border-color: #ff6b35; border-color: var(--primary);
background: #fff5f0; background: var(--primary-soft);
} }
.modal-footer { .modal-footer {
display: flex; display: flex;
gap: 20rpx; gap: 20rpx;
padding: 32rpx 40rpx; padding: 32rpx 40rpx;
border-top: 2rpx solid #f0f0f0; border-top: 2rpx solid var(--border-soft);
background: #fafafa; background: var(--bg-card-hover);
} }
.modal-btn { .modal-btn {
@ -353,31 +354,31 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: all 0.3s ease; transition: all 0.3s ease;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1); box-shadow: var(--shadow-sm);
} }
.modal-btn:active { .modal-btn:active {
transform: scale(0.98); transform: scale(0.98);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.15); box-shadow: var(--shadow-md);
} }
.cancel-btn { .cancel-btn {
background: #fff; background: var(--bg-white);
color: #666; color: var(--text-secondary);
border: 2rpx solid #e0e0e0; border: 2rpx solid var(--border-light);
} }
.cancel-btn:active { .cancel-btn:active {
background: #f5f5f5; background: var(--bg-soft);
} }
.modal-btn.submit-btn { .modal-btn.submit-btn {
background: linear-gradient(135deg, #ff6b35 0%, #ff8c42 100%); background: var(--primary-gradient);
color: #fff; color: #fff;
box-shadow: 0 4rpx 16rpx rgba(255, 107, 53, 0.3); box-shadow: var(--shadow-primary);
} }
.modal-btn.submit-btn:active { .modal-btn.submit-btn:active {
background: linear-gradient(135deg, #e55a2b 0%, #e67e2f 100%); background: var(--primary-dark);
box-shadow: 0 2rpx 8rpx rgba(255, 107, 53, 0.4); box-shadow: var(--shadow-md);
} }

View File

@ -1,27 +1,27 @@
const app = getApp() const app = getApp();
Page({ Page({
data: { data: {
userInfo: null, userInfo: null,
ladderUser: null, ladderUser: null,
currentStore: null, currentStore: null,
ongoingMatches: [], // 正在进行中的比赛 ongoingMatches: [], // 正在进行中的比赛
pendingGames: [] // 待确认的比赛 pendingGames: [], // 待确认的比赛
}, },
onLoad() { onLoad() {
this.initData() this.initData();
}, },
onShow() { onShow() {
this.initData() this.initData();
}, },
async onPullDownRefresh() { async onPullDownRefresh() {
try { try {
await this.initData() await this.initData();
} finally { } finally {
wx.stopPullDownRefresh() wx.stopPullDownRefresh();
} }
}, },
@ -29,259 +29,284 @@ Page({
// 检查是否已登录(有 token // 检查是否已登录(有 token
if (!app.globalData.token) { if (!app.globalData.token) {
// 未登录,跳转到用户页面进行登录 // 未登录,跳转到用户页面进行登录
wx.switchTab({ url: '/pages/user/index' }) wx.switchTab({ url: "/pages/user/index" });
return return;
} }
// 每次显示页面时重新获取门店和天梯信息 // 每次显示页面时重新获取门店和天梯信息
try { try {
await app.getCurrentStore() await app.getCurrentStore();
// 如果有门店,获取该门店的天梯信息 // 如果有门店,获取该门店的天梯信息
if (app.globalData.currentStore?.storeId) { if (app.globalData.currentStore && app.globalData.currentStore.storeId) {
await app.getLadderUser(app.globalData.currentStore.storeId) await app.getLadderUser(app.globalData.currentStore.storeId);
} }
} catch (e) { } catch (e) {
console.error('获取门店/天梯信息失败:', e) console.error("获取门店/天梯信息失败:", e);
} }
this.refreshData() this.refreshData();
}, },
refreshData() { refreshData() {
this.setData({ this.setData({
userInfo: app.globalData.userInfo, userInfo: app.globalData.userInfo,
ladderUser: app.globalData.ladderUser, ladderUser: app.globalData.ladderUser,
currentStore: app.globalData.currentStore currentStore: app.globalData.currentStore,
}) });
if (app.globalData.ladderUser) { if (app.globalData.ladderUser) {
this.fetchOngoingMatches() this.fetchOngoingMatches();
this.fetchPendingGames() this.fetchPendingGames();
} }
}, },
// 获取正在进行中的比赛 // 获取正在进行中的比赛
async fetchOngoingMatches() { async fetchOngoingMatches() {
try { try {
const res = await app.request('/api/match/ongoing', { const res = await app.request("/api/match/ongoing", {
store_id: this.data.currentStore?.storeId store_id: this.data.currentStore
}) ? this.data.currentStore.storeId
this.setData({ ongoingMatches: res.data || [] }) : null,
});
this.setData({ ongoingMatches: res.data || [] });
} catch (e) { } catch (e) {
console.error('获取进行中比赛失败:', e) console.error("获取进行中比赛失败:", e);
} }
}, },
// 手动刷新天梯信息 // 手动刷新天梯信息
async refreshLadderInfo() { async refreshLadderInfo() {
wx.showLoading({ title: '刷新中...' }) wx.showLoading({ title: "刷新中..." });
try { try {
// 重新获取门店信息 // 重新获取门店信息
await app.getCurrentStore() await app.getCurrentStore();
// 重新获取天梯信息 // 重新获取天梯信息
if (app.globalData.currentStore?.storeId) { if (app.globalData.currentStore && app.globalData.currentStore.storeId) {
await app.getLadderUser(app.globalData.currentStore.storeId) await app.getLadderUser(app.globalData.currentStore.storeId);
} }
this.refreshData() this.refreshData();
wx.hideLoading() wx.hideLoading();
if (app.globalData.ladderUser) { if (app.globalData.ladderUser) {
wx.showToast({ title: '已加入天梯', icon: 'success' }) wx.showToast({ title: "已加入天梯", icon: "success" });
} else { } else {
wx.showToast({ title: '暂未开通天梯', icon: 'none' }) wx.showToast({ title: "暂未开通天梯", icon: "none" });
} }
} catch (e) { } catch (e) {
wx.hideLoading() wx.hideLoading();
console.error('刷新天梯信息失败:', e) console.error("刷新天梯信息失败:", e);
wx.showToast({ title: '刷新失败', icon: 'none' }) wx.showToast({ title: "刷新失败", icon: "none" });
} }
}, },
async fetchPendingGames() { async fetchPendingGames() {
try { try {
const res = await app.request('/api/match/pending-confirm', { const res = await app.request("/api/match/pending-confirm", {
store_id: this.data.currentStore?.storeId store_id: this.data.currentStore
}) ? this.data.currentStore.storeId
this.setData({ pendingGames: res.data || [] }) : null,
});
this.setData({ pendingGames: res.data || [] });
} catch (e) { } catch (e) {
console.error('获取待确认比赛失败:', e) console.error("获取待确认比赛失败:", e);
} }
}, },
startChallenge() { startChallenge() {
if (!this.data.ladderUser) { if (!this.data.ladderUser) {
wx.showToast({ title: '请先加入天梯系统', icon: 'none' }) wx.showToast({ title: "请先加入天梯系统", icon: "none" });
return return;
}
if (!this.data.currentStore || !this.data.currentStore.storeId) {
wx.showToast({ title: "请先选择门店", icon: "none" });
wx.navigateTo({ url: "/pages/store/index" });
return;
} }
wx.scanCode({ wx.scanCode({
onlyFromCamera: false, onlyFromCamera: false,
scanType: ['qrCode'], scanType: ["qrCode"],
success: async (res) => { success: async (res) => {
const memberCode = res.result const memberCode = res.result;
this.checkAndChallenge(memberCode) this.checkAndChallenge(memberCode);
}, },
fail: (err) => { fail: (err) => {
if (err.errMsg !== 'scanCode:fail cancel') { if (err.errMsg !== "scanCode:fail cancel") {
wx.showToast({ title: '扫码失败', icon: 'none' }) wx.showToast({ title: "扫码失败", icon: "none" });
} }
} },
}) });
}, },
async checkAndChallenge(memberCode) { async checkAndChallenge(memberCode) {
wx.showLoading({ title: '检查中...' }) wx.showLoading({ title: "检查中..." });
try { try {
const res = await app.request(`/api/match/challenge/check/${memberCode}`, { const res = await app.request(
store_id: this.data.currentStore.storeId `/api/match/challenge/check/${memberCode}`,
}) {
store_id: this.data.currentStore.storeId,
},
);
wx.hideLoading() wx.hideLoading();
if (!res.data.canChallenge) { if (!res.data.canChallenge) {
wx.showModal({ wx.showModal({
title: '无法挑战', title: "无法挑战",
content: res.data.reason, content: res.data.reason,
showCancel: false showCancel: false,
}) });
return return;
} }
// 显示确认弹窗 // 显示确认弹窗
const target = res.data.targetUser const target = res.data.targetUser;
wx.showModal({ wx.showModal({
title: '确认挑战', title: "确认挑战",
content: `确定要向 ${target.ladderUser.realName}(Lv${target.ladderUser.level}, 战力${target.ladderUser.powerScore}) 发起挑战吗?`, content: `确定要向 ${target.ladderUser.realName}(Lv${target.ladderUser.level}, 战力${target.ladderUser.powerScore}) 发起挑战吗?`,
success: async (modalRes) => { success: async (modalRes) => {
if (modalRes.confirm) { if (modalRes.confirm) {
await this.createChallenge(memberCode) await this.createChallenge(memberCode);
} }
} },
}) });
} catch (e) { } catch (e) {
wx.hideLoading() wx.hideLoading();
console.error('检查挑战失败:', e) console.error("检查挑战失败:", e);
} }
}, },
async createChallenge(memberCode) { async createChallenge(memberCode) {
wx.showLoading({ title: '发起挑战中...' }) wx.showLoading({ title: "发起挑战中..." });
try { try {
const res = await app.request('/api/match/challenge/create', { const res = await app.request(
store_id: this.data.currentStore.storeId, "/api/match/challenge/create",
target_member_code: memberCode {
}, 'POST') store_id: this.data.currentStore.storeId,
target_member_code: memberCode,
},
"POST",
);
wx.hideLoading() wx.hideLoading();
wx.showToast({ title: '挑战已发起', icon: 'success' }) wx.showToast({ title: "挑战已发起", icon: "success" });
// 跳转到挑战赛详情页面 // 跳转到挑战赛详情页面
if (res.data && res.data.matchId) { if (res.data && res.data.matchId) {
setTimeout(() => { setTimeout(() => {
wx.navigateTo({ wx.navigateTo({
url: `/pages/match/challenge-detail/index?id=${res.data.matchId}` url: `/pages/match/challenge-detail/index?id=${res.data.matchId}`,
}) });
}, 1500) }, 1500);
} }
} catch (e) { } catch (e) {
wx.hideLoading() wx.hideLoading();
console.error('发起挑战失败:', e) console.error("发起挑战失败:", e);
const errorMsg = e.message || e.data?.message || '发起挑战失败' const errorMsg =
wx.showToast({ title: errorMsg, icon: 'none', duration: 2000 }) e.message || (e.data && e.data.message) || "发起挑战失败";
wx.showToast({ title: errorMsg, icon: "none", duration: 2000 });
} }
}, },
joinRankingMatch() { joinRankingMatch() {
if (!this.data.ladderUser) { if (!this.data.ladderUser) {
wx.showToast({ title: '请先加入天梯系统', icon: 'none' }) wx.showToast({ title: "请先加入天梯系统", icon: "none" });
return return;
} }
wx.scanCode({ wx.scanCode({
onlyFromCamera: false, onlyFromCamera: false,
scanType: ['qrCode'], scanType: ["qrCode"],
success: async (res) => { success: async (res) => {
const matchCode = res.result const matchCode = res.result;
wx.showLoading({ title: '加入中...' }) wx.showLoading({ title: "加入中..." });
try { try {
const joinRes = await app.request('/api/match/ranking/join', { const joinRes = await app.request(
match_code: matchCode "/api/match/ranking/join",
}, 'POST') {
match_code: matchCode,
},
"POST",
);
wx.hideLoading() wx.hideLoading();
wx.showToast({ title: '加入成功', icon: 'success' }) wx.showToast({ title: "加入成功", icon: "success" });
// 跳转到排位赛详情 // 跳转到排位赛详情
wx.navigateTo({ wx.navigateTo({
url: `/pages/match/ranking/index?code=${matchCode}` url: `/pages/match/ranking/index?code=${matchCode}`,
}) });
} catch (e) { } catch (e) {
wx.hideLoading() wx.hideLoading();
console.error('加入排位赛失败:', e) console.error("加入排位赛失败:", e);
} }
}, },
fail: (err) => { fail: (err) => {
if (err.errMsg !== 'scanCode:fail cancel') { if (err.errMsg !== "scanCode:fail cancel") {
wx.showToast({ title: '扫码失败', icon: 'none' }) wx.showToast({ title: "扫码失败", icon: "none" });
} }
} },
}) });
}, },
goToStore() { goToStore() {
wx.navigateTo({ url: '/pages/store/index' }) wx.navigateTo({ url: "/pages/store/index" });
}, },
// 跳转到比赛详情 // 跳转到比赛详情
goToMatchDetail(e) { goToMatchDetail(e) {
const match = e.currentTarget.dataset.match const match = e.currentTarget.dataset.match;
if (match.type === 1) { if (match.type === 1) {
// 挑战赛详情 // 挑战赛详情
wx.navigateTo({ wx.navigateTo({
url: `/pages/match/challenge-detail/index?id=${match.id}` url: `/pages/match/challenge-detail/index?id=${match.id}`,
}) });
} else { } else {
// 排位赛详情 // 排位赛详情
wx.navigateTo({ wx.navigateTo({
url: `/pages/match/ranking/index?code=${match.matchCode}` url: `/pages/match/ranking/index?code=${match.matchCode}`,
}) });
} }
}, },
confirmGame(e) { confirmGame(e) {
const game = e.currentTarget.dataset.game const game = e.currentTarget.dataset.game;
wx.showModal({ wx.showModal({
title: '确认比分', title: "确认比分",
content: `确认比分 ${game.myScore} : ${game.opponentScore} 吗?`, content: `确认比分 ${game.myScore} : ${game.opponentScore} 吗?`,
confirmText: '确认', confirmText: "确认",
cancelText: '有争议', cancelText: "有争议",
success: async (res) => { success: async (res) => {
wx.showLoading({ title: '处理中...' }) wx.showLoading({ title: "处理中..." });
try { try {
await app.request('/api/match/challenge/confirm-score', { await app.request(
game_id: game.id, "/api/match/challenge/confirm-score",
confirm: res.confirm {
}, 'POST') game_id: game.id,
confirm: res.confirm,
},
"POST",
);
wx.hideLoading() wx.hideLoading();
wx.showToast({ wx.showToast({
title: res.confirm ? '确认成功' : '已标记争议', title: res.confirm ? "确认成功" : "已标记争议",
icon: 'success' icon: "success",
}) });
this.fetchPendingGames() this.fetchPendingGames();
} catch (e) { } catch (e) {
wx.hideLoading() wx.hideLoading();
console.error('确认比分失败:', e) console.error("确认比分失败:", e);
} }
} },
}) });
} },
}) });

View File

@ -9,20 +9,22 @@
<view class="main-content"> <view class="main-content">
<!-- 页面标题 --> <!-- 页面标题 -->
<view class="page-header"> <view class="page-header">
<text class="page-title">🏸 发起挑战</text> <text class="page-title">发起挑战</text>
<text class="page-subtitle">扫描对手会员码,开启对决</text> <text class="page-subtitle">扫描对手会员码,开启对决</text>
</view> </view>
<!-- 当前门店 --> <!-- 当前门店 -->
<view class="store-bar" wx:if="{{currentStore}}" bindtap="goToStore"> <view class="store-bar" wx:if="{{currentStore}}" bindtap="goToStore">
<text class="store-icon">📍</text> <image class="store-icon" src="/images/icon-store.svg" mode="aspectFit"></image>
<text class="store-name">{{currentStore.storeName}}</text> <text class="store-name">{{currentStore.storeName}}</text>
<text class="store-arrow"></text> <text class="store-arrow"></text>
</view> </view>
<!-- 未登录或非天梯用户提示 --> <!-- 未登录或非天梯用户提示 -->
<view class="notice-card animate-fadeInUp" wx:if="{{!ladderUser}}"> <view class="notice-card animate-fadeInUp" wx:if="{{!ladderUser}}">
<view class="notice-icon">🏸</view> <view class="notice-icon">
<image class="notice-icon-img" src="/images/icon-challenge.svg" mode="aspectFit"></image>
</view>
<view class="notice-content"> <view class="notice-content">
<text class="notice-title">暂未开通天梯</text> <text class="notice-title">暂未开通天梯</text>
<text class="notice-desc">请联系门店工作人员加入天梯系统</text> <text class="notice-desc">请联系门店工作人员加入天梯系统</text>
@ -67,7 +69,7 @@
<view class="scan-grid animate-fadeInUp" style="animation-delay: 0.1s"> <view class="scan-grid animate-fadeInUp" style="animation-delay: 0.1s">
<view class="scan-card challenge" bindtap="startChallenge"> <view class="scan-card challenge" bindtap="startChallenge">
<view class="scan-icon-wrapper"> <view class="scan-icon-wrapper">
<text class="scan-icon">⚔️</text> <image class="scan-icon-img" src="/images/icon-challenge.svg" mode="aspectFit"></image>
</view> </view>
<text class="scan-title">挑战赛</text> <text class="scan-title">挑战赛</text>
<text class="scan-desc">1v1 对决</text> <text class="scan-desc">1v1 对决</text>
@ -76,7 +78,7 @@
<view class="scan-card ranking" bindtap="joinRankingMatch"> <view class="scan-card ranking" bindtap="joinRankingMatch">
<view class="scan-icon-wrapper"> <view class="scan-icon-wrapper">
<text class="scan-icon">🏆</text> <image class="scan-icon-img" src="/images/icon-ranking.svg" mode="aspectFit"></image>
</view> </view>
<text class="scan-title">排位赛</text> <text class="scan-title">排位赛</text>
<text class="scan-desc">多人竞技</text> <text class="scan-desc">多人竞技</text>
@ -88,7 +90,7 @@
<view class="ongoing-card animate-fadeInUp" style="animation-delay: 0.12s" wx:if="{{ongoingMatches.length > 0}}"> <view class="ongoing-card animate-fadeInUp" style="animation-delay: 0.12s" wx:if="{{ongoingMatches.length > 0}}">
<view class="ongoing-header"> <view class="ongoing-header">
<view class="ongoing-header-left"> <view class="ongoing-header-left">
<text class="ongoing-icon">🔥</text> <image class="ongoing-icon-img" src="/images/icon-history.svg" mode="aspectFit"></image>
<text class="ongoing-title">进行中的比赛</text> <text class="ongoing-title">进行中的比赛</text>
</view> </view>
<view class="ongoing-count">{{ongoingMatches.length}}</view> <view class="ongoing-count">{{ongoingMatches.length}}</view>
@ -134,9 +136,9 @@
<text class="current-name">{{item.opponent.realName}}</text> <text class="current-name">{{item.opponent.realName}}</text>
</view> </view>
<view class="my-status {{item.myStatus}}"> <view class="my-status {{item.myStatus}}">
<text wx:if="{{item.myStatus === 'waiting'}}">等待中</text> <text wx:if="{{item.myStatus === 'waiting'}}">等待中</text>
<text wx:elif="{{item.myStatus === 'playing'}}">🎾 比赛中</text> <text wx:elif="{{item.myStatus === 'playing'}}">比赛中</text>
<text wx:else>已完成</text> <text wx:else>已完成</text>
</view> </view>
</view> </view>
</block> </block>
@ -152,7 +154,7 @@
<!-- 待确认比赛 --> <!-- 待确认比赛 -->
<view class="pending-card animate-fadeInUp" style="animation-delay: 0.15s" wx:if="{{pendingGames.length > 0}}"> <view class="pending-card animate-fadeInUp" style="animation-delay: 0.15s" wx:if="{{pendingGames.length > 0}}">
<view class="pending-header"> <view class="pending-header">
<text class="pending-title">📋 待确认比分</text> <text class="pending-title">待确认比分</text>
<view class="pending-count">{{pendingGames.length}}</view> <view class="pending-count">{{pendingGames.length}}</view>
</view> </view>
<view class="pending-list"> <view class="pending-list">
@ -171,7 +173,9 @@
<!-- 战力值规则 --> <!-- 战力值规则 -->
<view class="rules-card animate-fadeInUp" style="animation-delay: 0.2s"> <view class="rules-card animate-fadeInUp" style="animation-delay: 0.2s">
<view class="rules-header"> <view class="rules-header">
<view class="rules-icon">📖</view> <view class="rules-icon">
<image class="rules-icon-img" src="/images/icon-info.svg" mode="aspectFit"></image>
</view>
<text class="rules-title">战力值规则</text> <text class="rules-title">战力值规则</text>
</view> </view>
<view class="rules-grid"> <view class="rules-grid">
@ -197,7 +201,9 @@
</view> </view>
</view> </view>
<view class="rule-item"> <view class="rule-item">
<view class="rule-icon shield">🛡</view> <view class="rule-icon shield">
<image class="rule-icon-img" src="/images/icon-shield.svg" mode="aspectFit"></image>
</view>
<view class="rule-text"> <view class="rule-text">
<text class="rule-label">新手保护</text> <text class="rule-label">新手保护</text>
<text class="rule-value">输分减半</text> <text class="rule-value">输分减半</text>
@ -205,7 +211,7 @@
</view> </view>
</view> </view>
<view class="rules-note"> <view class="rules-note">
💡 同一对手30天内仅限挑战1次 提示:同一对手30天内仅限挑战1次
</view> </view>
</view> </view>
</view> </view>

View File

@ -4,7 +4,12 @@
.page-container { .page-container {
min-height: 100vh; min-height: 100vh;
background: linear-gradient(180deg, #FEF7F3 0%, #FAFAFA 30%, #F5F5F5 100%); background: linear-gradient(
180deg,
var(--primary-soft) 0%,
var(--bg-page) 30%,
var(--bg-soft) 100%
);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
@ -42,7 +47,9 @@
} }
@keyframes spin { @keyframes spin {
to { transform: translateX(-50%) rotate(360deg); } to {
transform: translateX(-50%) rotate(360deg);
}
} }
/* 主要内容 */ /* 主要内容 */
@ -63,7 +70,7 @@
display: block; display: block;
font-size: 52rpx; font-size: 52rpx;
font-weight: 700; font-weight: 700;
color: #1a1a1a; color: var(--text-primary);
margin-bottom: 12rpx; margin-bottom: 12rpx;
letter-spacing: 1rpx; letter-spacing: 1rpx;
} }
@ -71,7 +78,7 @@
.page-subtitle { .page-subtitle {
display: block; display: block;
font-size: 26rpx; font-size: 26rpx;
color: #999; color: var(--text-muted);
font-weight: 400; font-weight: 400;
letter-spacing: 0.5rpx; letter-spacing: 0.5rpx;
} }
@ -88,11 +95,12 @@
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04); box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
margin: 0 auto 24rpx; margin: 0 auto 24rpx;
width: fit-content; width: fit-content;
border: 1rpx solid rgba(0, 0, 0, 0.04); border: 1rpx solid var(--border-soft);
} }
.store-icon { .store-icon {
font-size: 28rpx; width: 28rpx;
height: 28rpx;
} }
.store-name { .store-name {
@ -117,8 +125,12 @@
align-items: center; align-items: center;
gap: 20rpx; gap: 20rpx;
padding: 28rpx 24rpx; padding: 28rpx 24rpx;
background: linear-gradient(135deg, #FFF9F5 0%, #FFFFFF 100%); background: linear-gradient(
border: 2rpx solid #FFE8D5; 135deg,
var(--primary-soft) 0%,
var(--bg-white) 100%
);
border: 2rpx solid var(--border-primary);
border-radius: 24rpx; border-radius: 24rpx;
margin-bottom: 24rpx; margin-bottom: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(255, 152, 0, 0.1); box-shadow: 0 4rpx 16rpx rgba(255, 152, 0, 0.1);
@ -127,12 +139,16 @@
.notice-icon { .notice-icon {
width: 80rpx; width: 80rpx;
height: 80rpx; height: 80rpx;
background: linear-gradient(135deg, #FFF3E0, #FFE0B2); background: var(--primary-gradient-soft);
border-radius: 20rpx; border-radius: 20rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 40rpx; }
.notice-icon-img {
width: 44rpx;
height: 44rpx;
} }
.notice-content { .notice-content {
@ -143,21 +159,21 @@
display: block; display: block;
font-size: 30rpx; font-size: 30rpx;
font-weight: 700; font-weight: 700;
color: #E65100; color: var(--primary-dark);
margin-bottom: 6rpx; margin-bottom: 6rpx;
} }
.notice-desc { .notice-desc {
display: block; display: block;
font-size: 24rpx; font-size: 24rpx;
color: #F57C00; color: var(--primary);
} }
.notice-action { .notice-action {
padding: 16rpx 28rpx; padding: 16rpx 28rpx;
background: linear-gradient(135deg, #FF8A65, #FF6B35); background: var(--primary-gradient);
border-radius: 50rpx; border-radius: 50rpx;
box-shadow: 0 4rpx 12rpx rgba(255, 107, 53, 0.3); box-shadow: var(--shadow-primary);
} }
.notice-action:active { .notice-action:active {
@ -174,13 +190,17 @@
用户信息卡片 - 全新设计 用户信息卡片 - 全新设计
========================================== */ ========================================== */
.user-card { .user-card {
background: linear-gradient(135deg, #FFFFFF 0%, #FAFAFA 100%); background: linear-gradient(
135deg,
var(--bg-white) 0%,
var(--bg-card-hover) 100%
);
border-radius: 28rpx; border-radius: 28rpx;
padding: 0; padding: 0;
margin-bottom: 24rpx; margin-bottom: 24rpx;
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.08); box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.08);
overflow: hidden; overflow: hidden;
border: 1rpx solid rgba(255, 107, 53, 0.1); border: 1rpx solid var(--border-primary);
} }
.user-card-inner { .user-card-inner {
@ -192,13 +212,13 @@
} }
.user-card-inner::before { .user-card-inner::before {
content: ''; content: "";
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
height: 6rpx; height: 6rpx;
background: linear-gradient(90deg, #FF8A65, #FF6B35, #FFB74D); background: var(--primary-gradient);
} }
.user-avatar-box { .user-avatar-box {
@ -209,10 +229,10 @@
} }
.user-avatar-box::before { .user-avatar-box::before {
content: ''; content: "";
position: absolute; position: absolute;
inset: -6rpx; inset: -6rpx;
background: linear-gradient(135deg, #FF8A65, #FFB74D); background: var(--primary-gradient);
border-radius: 50%; border-radius: 50%;
z-index: 0; z-index: 0;
} }
@ -253,11 +273,26 @@
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1); box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
} }
.user-level.lv1 { background: linear-gradient(135deg, #81C784, #66BB6A); color: #fff; } .user-level.lv1 {
.user-level.lv2 { background: linear-gradient(135deg, #64B5F6, #42A5F5); color: #fff; } background: linear-gradient(135deg, #81c784, #66bb6a);
.user-level.lv3 { background: linear-gradient(135deg, #FFB74D, #FFA726); color: #fff; } color: #fff;
.user-level.lv4 { background: linear-gradient(135deg, #F06292, #EC407A); color: #fff; } }
.user-level.lv5 { background: linear-gradient(135deg, #BA68C8, #AB47BC); color: #fff; } .user-level.lv2 {
background: linear-gradient(135deg, #64b5f6, #42a5f5);
color: #fff;
}
.user-level.lv3 {
background: linear-gradient(135deg, #ffb74d, #ffa726);
color: #fff;
}
.user-level.lv4 {
background: linear-gradient(135deg, #f06292, #ec407a);
color: #fff;
}
.user-level.lv5 {
background: linear-gradient(135deg, #ba68c8, #ab47bc);
color: #fff;
}
.user-stats-row { .user-stats-row {
display: flex; display: flex;
@ -278,7 +313,7 @@
} }
.mini-stat-value.win { .mini-stat-value.win {
color: #00C853; color: #00c853;
} }
.mini-stat-label { .mini-stat-label {
@ -318,7 +353,7 @@
} }
.scan-card::before { .scan-card::before {
content: ''; content: "";
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@ -329,11 +364,19 @@
} }
.scan-card.challenge::before { .scan-card.challenge::before {
background: linear-gradient(180deg, rgba(255, 107, 53, 0.08) 0%, transparent 50%); background: linear-gradient(
180deg,
rgba(255, 107, 53, 0.08) 0%,
transparent 50%
);
} }
.scan-card.ranking::before { .scan-card.ranking::before {
background: linear-gradient(180deg, rgba(255, 193, 7, 0.1) 0%, transparent 50%); background: linear-gradient(
180deg,
rgba(255, 193, 7, 0.1) 0%,
transparent 50%
);
} }
.scan-card:active { .scan-card:active {
@ -357,15 +400,16 @@
} }
.scan-card.challenge .scan-icon-wrapper { .scan-card.challenge .scan-icon-wrapper {
background: linear-gradient(135deg, #FFE8DD, #FFCCBC); background: linear-gradient(135deg, #ffe8dd, #ffccbc);
} }
.scan-card.ranking .scan-icon-wrapper { .scan-card.ranking .scan-icon-wrapper {
background: linear-gradient(135deg, #FFF8E1, #FFE082); background: linear-gradient(135deg, #fff8e1, #ffe082);
} }
.scan-icon { .scan-icon-img {
font-size: 52rpx; width: 56rpx;
height: 56rpx;
} }
.scan-title { .scan-title {
@ -387,7 +431,7 @@
.scan-badge { .scan-badge {
display: inline-block; display: inline-block;
padding: 8rpx 20rpx; padding: 8rpx 20rpx;
background: linear-gradient(135deg, #FF8A65, #FF6B35); background: linear-gradient(135deg, #ff8a65, #ff6b35);
color: #fff; color: #fff;
font-size: 22rpx; font-size: 22rpx;
font-weight: 700; font-weight: 700;
@ -396,8 +440,8 @@
} }
.scan-badge.accent { .scan-badge.accent {
background: linear-gradient(135deg, #FFD54F, #FFB300); background: linear-gradient(135deg, #ffd54f, #ffb300);
color: #5D4037; color: #5d4037;
box-shadow: 0 4rpx 12rpx rgba(255, 179, 0, 0.3); box-shadow: 0 4rpx 12rpx rgba(255, 179, 0, 0.3);
} }
@ -418,7 +462,7 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 20rpx 24rpx; padding: 20rpx 24rpx;
background: linear-gradient(90deg, #FFF3E0, #FFFFFF); background: linear-gradient(90deg, #fff3e0, #ffffff);
border-bottom: 1rpx solid rgba(255, 152, 0, 0.1); border-bottom: 1rpx solid rgba(255, 152, 0, 0.1);
} }
@ -428,8 +472,10 @@
gap: 10rpx; gap: 10rpx;
} }
.ongoing-icon { .ongoing-icon-img {
font-size: 28rpx; width: 28rpx;
height: 28rpx;
opacity: 0.9;
} }
.ongoing-title { .ongoing-title {
@ -442,7 +488,7 @@
min-width: 40rpx; min-width: 40rpx;
height: 40rpx; height: 40rpx;
padding: 0 14rpx; padding: 0 14rpx;
background: linear-gradient(135deg, #FF5722, #FF8A65); background: linear-gradient(135deg, #ff5722, #ff8a65);
color: #fff; color: #fff;
font-size: 24rpx; font-size: 24rpx;
font-weight: 700; font-weight: 700;
@ -458,7 +504,7 @@
} }
.ongoing-item { .ongoing-item {
background: linear-gradient(135deg, #FAFAFA, #F5F5F5); background: linear-gradient(135deg, #fafafa, #f5f5f5);
border-radius: 20rpx; border-radius: 20rpx;
margin-bottom: 16rpx; margin-bottom: 16rpx;
overflow: hidden; overflow: hidden;
@ -498,13 +544,13 @@
} }
.match-type-tag.challenge { .match-type-tag.challenge {
background: linear-gradient(135deg, #FFE8DD, #FFCCBC); background: linear-gradient(135deg, #ffe8dd, #ffccbc);
color: #E65100; color: #e65100;
} }
.match-type-tag.ranking { .match-type-tag.ranking {
background: linear-gradient(135deg, #FFF8E1, #FFE082); background: linear-gradient(135deg, #fff8e1, #ffe082);
color: #F57C00; color: #f57c00;
} }
.match-status-tag { .match-status-tag {
@ -515,19 +561,24 @@
} }
.match-status-tag.waiting { .match-status-tag.waiting {
background: #E3F2FD; background: #e3f2fd;
color: #1565C0; color: #1565c0;
} }
.match-status-tag.playing { .match-status-tag.playing {
background: #E8F5E9; background: #e8f5e9;
color: #2E7D32; color: #2e7d32;
animation: pulse 2s infinite; animation: pulse 2s infinite;
} }
@keyframes pulse { @keyframes pulse {
0%, 100% { opacity: 1; } 0%,
50% { opacity: 0.7; } 100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
} }
.ongoing-item-body { .ongoing-item-body {
@ -545,7 +596,7 @@
width: 80rpx; width: 80rpx;
height: 80rpx; height: 80rpx;
border-radius: 50%; border-radius: 50%;
border: 3rpx solid #FFE0B2; border: 3rpx solid #ffe0b2;
} }
.opponent-detail { .opponent-detail {
@ -567,7 +618,7 @@
.opponent-level { .opponent-level {
padding: 4rpx 12rpx; padding: 4rpx 12rpx;
background: linear-gradient(135deg, #FFB74D, #FFA726); background: linear-gradient(135deg, #ffb74d, #ffa726);
color: #fff; color: #fff;
font-size: 20rpx; font-size: 20rpx;
font-weight: 600; font-weight: 600;
@ -601,8 +652,8 @@
.ranking-stage { .ranking-stage {
padding: 4rpx 12rpx; padding: 4rpx 12rpx;
background: #E3F2FD; background: #e3f2fd;
color: #1565C0; color: #1565c0;
border-radius: 6rpx; border-radius: 6rpx;
font-weight: 600; font-weight: 600;
} }
@ -633,11 +684,11 @@
} }
.my-status.waiting { .my-status.waiting {
color: #1565C0; color: #1565c0;
} }
.my-status.playing { .my-status.playing {
color: #2E7D32; color: #2e7d32;
} }
.my-status.finished { .my-status.finished {
@ -655,7 +706,7 @@
.match-weight { .match-weight {
padding: 4rpx 12rpx; padding: 4rpx 12rpx;
background: linear-gradient(135deg, #FF8A65, #FF6B35); background: linear-gradient(135deg, #ff8a65, #ff6b35);
color: #fff; color: #fff;
font-size: 20rpx; font-size: 20rpx;
font-weight: 600; font-weight: 600;
@ -685,7 +736,7 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 20rpx 24rpx; padding: 20rpx 24rpx;
background: linear-gradient(90deg, #FFF5F2, #FFFFFF); background: linear-gradient(90deg, #fff5f2, #ffffff);
border-bottom: 1rpx solid rgba(255, 107, 53, 0.1); border-bottom: 1rpx solid rgba(255, 107, 53, 0.1);
} }
@ -699,7 +750,7 @@
min-width: 40rpx; min-width: 40rpx;
height: 40rpx; height: 40rpx;
padding: 0 14rpx; padding: 0 14rpx;
background: linear-gradient(135deg, #FF8A65, #FF6B35); background: linear-gradient(135deg, #ff8a65, #ff6b35);
color: #fff; color: #fff;
font-size: 24rpx; font-size: 24rpx;
font-weight: 700; font-weight: 700;
@ -718,7 +769,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
padding: 18rpx 20rpx; padding: 18rpx 20rpx;
background: linear-gradient(135deg, #FAFAFA, #F5F5F5); background: linear-gradient(135deg, #fafafa, #f5f5f5);
border-radius: 16rpx; border-radius: 16rpx;
margin-bottom: 12rpx; margin-bottom: 12rpx;
transition: all 0.2s; transition: all 0.2s;
@ -729,7 +780,7 @@
} }
.pending-item:active { .pending-item:active {
background: linear-gradient(135deg, #F5F5F5, #EEEEEE); background: linear-gradient(135deg, #f5f5f5, #eeeeee);
} }
.game-info { .game-info {
@ -741,7 +792,7 @@
.vs-tag { .vs-tag {
padding: 6rpx 14rpx; padding: 6rpx 14rpx;
background: linear-gradient(135deg, #FF8A65, #FF6B35); background: linear-gradient(135deg, #ff8a65, #ff6b35);
color: #fff; color: #fff;
font-size: 20rpx; font-size: 20rpx;
font-weight: 800; font-weight: 800;
@ -759,12 +810,12 @@
font-weight: 800; font-weight: 800;
color: var(--text-primary); color: var(--text-primary);
padding: 0 20rpx; padding: 0 20rpx;
font-family: 'SF Mono', 'Monaco', monospace; font-family: "SF Mono", "Monaco", monospace;
} }
.confirm-btn { .confirm-btn {
padding: 14rpx 28rpx; padding: 14rpx 28rpx;
background: linear-gradient(135deg, #00C853, #00E676); background: linear-gradient(135deg, #00c853, #00e676);
color: #fff; color: #fff;
font-size: 24rpx; font-size: 24rpx;
font-weight: 700; font-weight: 700;
@ -798,12 +849,16 @@
.rules-icon { .rules-icon {
width: 48rpx; width: 48rpx;
height: 48rpx; height: 48rpx;
background: linear-gradient(135deg, #FFF3E0, #FFE0B2); background: linear-gradient(135deg, #fff3e0, #ffe0b2);
border-radius: 14rpx; border-radius: 14rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 26rpx; }
.rules-icon-img {
width: 28rpx;
height: 28rpx;
} }
.rules-title { .rules-title {
@ -823,7 +878,7 @@
align-items: center; align-items: center;
gap: 14rpx; gap: 14rpx;
padding: 18rpx; padding: 18rpx;
background: linear-gradient(135deg, #FAFAFA, #F5F5F5); background: linear-gradient(135deg, #fafafa, #f5f5f5);
border-radius: 18rpx; border-radius: 18rpx;
transition: all 0.2s; transition: all 0.2s;
} }
@ -844,24 +899,29 @@
flex-shrink: 0; flex-shrink: 0;
} }
.rule-icon-img {
width: 28rpx;
height: 28rpx;
}
.rule-icon.win { .rule-icon.win {
background: linear-gradient(135deg, #E8F5E9, #C8E6C9); background: linear-gradient(135deg, #e8f5e9, #c8e6c9);
color: #2E7D32; color: #2e7d32;
} }
.rule-icon.lose { .rule-icon.lose {
background: linear-gradient(135deg, #FFEBEE, #FFCDD2); background: linear-gradient(135deg, #ffebee, #ffcdd2);
color: #C62828; color: #c62828;
} }
.rule-icon.bonus { .rule-icon.bonus {
background: linear-gradient(135deg, #FFF8E1, #FFECB3); background: linear-gradient(135deg, #fff8e1, #ffecb3);
color: #F57C00; color: #f57c00;
} }
.rule-icon.shield { .rule-icon.shield {
background: linear-gradient(135deg, #E3F2FD, #BBDEFB); background: linear-gradient(135deg, #e3f2fd, #bbdefb);
color: #1565C0; color: #1565c0;
} }
.rule-text { .rule-text {
@ -884,21 +944,21 @@
} }
.rule-value.positive { .rule-value.positive {
color: #2E7D32; color: #2e7d32;
} }
.rule-value.negative { .rule-value.negative {
color: #C62828; color: #c62828;
} }
.rules-note { .rules-note {
margin-top: 18rpx; margin-top: 18rpx;
padding: 18rpx; padding: 18rpx;
background: linear-gradient(135deg, #FFF8E1, #FFFDE7); background: linear-gradient(135deg, #fff8e1, #fffde7);
border-radius: 14rpx; border-radius: 14rpx;
text-align: center; text-align: center;
font-size: 24rpx; font-size: 24rpx;
color: #F57C00; color: #f57c00;
font-weight: 600; font-weight: 600;
border: 1rpx solid rgba(255, 152, 0, 0.15); border: 1rpx solid rgba(255, 152, 0, 0.15);
} }

View File

@ -1,5 +1,5 @@
const app = getApp() const app = getApp();
const util = require('../../../utils/util') const util = require("../../../utils/util");
Page({ Page({
data: { data: {
@ -7,89 +7,89 @@ Page({
loading: false, loading: false,
page: 1, page: 1,
pageSize: 20, pageSize: 20,
hasMore: true hasMore: true,
}, },
onLoad() { onLoad() {
this.fetchMatches() this.fetchMatches();
}, },
onShow() { onShow() {
// 门店切换后刷新数据 // 门店切换后刷新数据
if (app.globalData.storeChanged) { if (app.globalData.storeChanged) {
app.globalData.storeChanged = false app.globalData.storeChanged = false;
this.setData({ page: 1, hasMore: true, matches: [] }) this.setData({ page: 1, hasMore: true, matches: [] });
this.fetchMatches() this.fetchMatches();
} }
}, },
onPullDownRefresh() { onPullDownRefresh() {
this.setData({ page: 1, hasMore: true }) this.setData({ page: 1, hasMore: true });
this.fetchMatches().then(() => { this.fetchMatches().then(() => {
wx.stopPullDownRefresh() wx.stopPullDownRefresh();
}) });
}, },
onReachBottom() { onReachBottom() {
if (this.data.hasMore && !this.data.loading) { if (this.data.hasMore && !this.data.loading) {
this.loadMore() this.loadMore();
} }
}, },
async fetchMatches() { async fetchMatches() {
const currentStore = app.globalData.currentStore const currentStore = app.globalData.currentStore;
if (!currentStore?.storeId) { if (!currentStore || !currentStore.storeId) {
return return;
} }
this.setData({ loading: true }) this.setData({ loading: true });
try { try {
const res = await app.request('/api/match/my-matches', { const res = await app.request("/api/match/my-matches", {
store_id: currentStore.storeId, store_id: currentStore.storeId,
page: this.data.page, page: this.data.page,
pageSize: this.data.pageSize pageSize: this.data.pageSize,
}) });
const matches = (res.data.list || []).map(match => { const matches = (res.data.list || []).map((match) => {
// 确保 powerChange 是数字类型,移除可能存在的加号和其他非数字字符 // 确保 powerChange 是数字类型,移除可能存在的加号和其他非数字字符
let powerChange = match.powerChange let powerChange = match.powerChange;
if (powerChange != null && powerChange !== undefined) { if (powerChange != null && powerChange !== undefined) {
// 如果是字符串,移除所有加号、空格等非数字字符(保留负号) // 如果是字符串,移除所有加号、空格等非数字字符(保留负号)
if (typeof powerChange === 'string') { if (typeof powerChange === "string") {
// 保留负号,移除所有加号和其他字符 // 保留负号,移除所有加号和其他字符
const cleaned = powerChange.replace(/\+/g, '').trim() const cleaned = powerChange.replace(/\+/g, "").trim();
powerChange = parseFloat(cleaned) || 0 powerChange = parseFloat(cleaned) || 0;
} }
// 确保是数字类型 // 确保是数字类型
powerChange = Number(powerChange) powerChange = Number(powerChange);
// 如果是 NaN设为 0 // 如果是 NaN设为 0
if (isNaN(powerChange)) { if (isNaN(powerChange)) {
powerChange = 0 powerChange = 0;
} }
} else { } else {
powerChange = 0 powerChange = 0;
} }
return { return Object.assign({}, match, {
...match,
powerChange: powerChange, powerChange: powerChange,
confirmedAt: util.formatDate(match.confirmedAt) confirmedAt: util.formatDate(match.confirmedAt),
} });
}) });
this.setData({ this.setData({
matches: this.data.page === 1 ? matches : [...this.data.matches, ...matches], matches:
hasMore: matches.length >= this.data.pageSize this.data.page === 1 ? matches : this.data.matches.concat(matches),
}) hasMore: matches.length >= this.data.pageSize,
});
} catch (e) { } catch (e) {
console.error('获取比赛记录失败:', e) console.error("获取比赛记录失败:", e);
} finally { } finally {
this.setData({ loading: false }) this.setData({ loading: false });
} }
}, },
loadMore() { loadMore() {
this.setData({ page: this.data.page + 1 }) this.setData({ page: this.data.page + 1 });
this.fetchMatches() this.fetchMatches();
} },
}) });

View File

@ -4,7 +4,7 @@
.container { .container {
min-height: 100vh; min-height: 100vh;
background: #f5f5f5; background: var(--bg-page);
padding: 20rpx; padding: 20rpx;
} }
@ -15,10 +15,10 @@
} }
.match-item { .match-item {
background: #fff; background: var(--bg-card);
border-radius: 20rpx; border-radius: 20rpx;
overflow: hidden; overflow: hidden;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08); box-shadow: var(--shadow-card);
transition: all 0.3s ease; transition: all 0.3s ease;
} }
@ -32,7 +32,7 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 24rpx 28rpx; padding: 24rpx 28rpx;
background: linear-gradient(135deg, #ff6b35 0%, #ff8c42 100%); background: var(--primary-gradient);
border-bottom: 2rpx solid rgba(255, 255, 255, 0.2); border-bottom: 2rpx solid rgba(255, 255, 255, 0.2);
} }
@ -74,13 +74,13 @@
} }
.result.win { .result.win {
background: linear-gradient(135deg, #4caf50 0%, #66bb6a 100%); background: linear-gradient(135deg, var(--success) 0%, #34d399 100%);
color: #fff; color: #fff;
box-shadow: 0 4rpx 12rpx rgba(76, 175, 80, 0.3); box-shadow: 0 4rpx 12rpx rgba(76, 175, 80, 0.3);
} }
.result.lose { .result.lose {
background: linear-gradient(135deg, #f44336 0%, #ef5350 100%); background: linear-gradient(135deg, var(--danger) 0%, #f87171 100%);
color: #fff; color: #fff;
box-shadow: 0 4rpx 12rpx rgba(244, 67, 54, 0.3); box-shadow: 0 4rpx 12rpx rgba(244, 67, 54, 0.3);
} }
@ -94,14 +94,14 @@
.opponent { .opponent {
font-size: 28rpx; font-size: 28rpx;
color: #666; color: var(--text-secondary);
font-weight: 500; font-weight: 500;
} }
.score { .score {
font-size: 40rpx; font-size: 40rpx;
font-weight: 700; font-weight: 700;
color: #333; color: var(--text-primary);
letter-spacing: 4rpx; letter-spacing: 4rpx;
} }
@ -126,13 +126,13 @@
.match-footer { .match-footer {
padding: 20rpx 28rpx; padding: 20rpx 28rpx;
background: #fafafa; background: var(--bg-card-hover);
border-top: 1rpx solid #f0f0f0; border-top: 1rpx solid var(--border-soft);
} }
.match-time { .match-time {
font-size: 24rpx; font-size: 24rpx;
color: #999; color: var(--text-muted);
} }
.empty-state { .empty-state {

View File

@ -9,7 +9,9 @@
<view class="main-content"> <view class="main-content">
<!-- 比赛头部信息 --> <!-- 比赛头部信息 -->
<view class="match-header animate-fadeInUp"> <view class="match-header animate-fadeInUp">
<view class="match-badge">🏆</view> <view class="match-badge">
<image class="match-badge-img" src="/images/icon-ranking.svg" mode="aspectFit"></image>
</view>
<view class="match-title">{{match.name || '排位赛'}}</view> <view class="match-title">{{match.name || '排位赛'}}</view>
<view class="match-status status-{{match.status}}"> <view class="match-status status-{{match.status}}">
<text class="status-dot"></text> <text class="status-dot"></text>
@ -42,15 +44,15 @@
<!-- 我的状态 --> <!-- 我的状态 -->
<view class="my-status-card animate-fadeInUp" style="animation-delay: 0.1s" wx:if="{{myPlayer}}"> <view class="my-status-card animate-fadeInUp" style="animation-delay: 0.1s" wx:if="{{myPlayer}}">
<view class="card-header"> <view class="card-header">
<text class="card-icon">👤</text> <image class="card-icon" src="/images/icon-user.svg" mode="aspectFit"></image>
<text class="card-title">我的状态</text> <text class="card-title">我的状态</text>
</view> </view>
<view class="status-content"> <view class="status-content">
<view class="status-main"> <view class="status-main">
<view class="status-badge {{myPlayer.status}}"> <view class="status-badge {{myPlayer.status}}">
<text wx:if="{{myPlayer.status === 'playing'}}">🎾 比赛中</text> <text wx:if="{{myPlayer.status === 'playing'}}">比赛中</text>
<text wx:elif="{{myPlayer.status === 'finished'}}">已完成</text> <text wx:elif="{{myPlayer.status === 'finished'}}">已完成</text>
<text wx:else>等待匹配</text> <text wx:else>等待匹配</text>
</view> </view>
<view class="win-lose-stats"> <view class="win-lose-stats">
<view class="stat win"> <view class="stat win">
@ -86,7 +88,7 @@
<!-- 参赛选手 --> <!-- 参赛选手 -->
<view class="players-card animate-fadeInUp" style="animation-delay: 0.15s"> <view class="players-card animate-fadeInUp" style="animation-delay: 0.15s">
<view class="card-header"> <view class="card-header">
<text class="card-icon">👥</text> <image class="card-icon" src="/images/icon-users.svg" mode="aspectFit"></image>
<text class="card-title">参赛选手</text> <text class="card-title">参赛选手</text>
<text class="player-count">{{match.players.length || 0}}人</text> <text class="player-count">{{match.players.length || 0}}人</text>
</view> </view>
@ -112,7 +114,7 @@
</view> </view>
<view class="empty-players" wx:else> <view class="empty-players" wx:else>
<text class="empty-icon">🏸</text> <image class="empty-icon" src="/images/empty-match.svg" mode="aspectFit"></image>
<text class="empty-text">暂无参赛选手</text> <text class="empty-text">暂无参赛选手</text>
</view> </view>
</view> </view>

View File

@ -4,7 +4,12 @@
.page-container { .page-container {
min-height: 100vh; min-height: 100vh;
background: linear-gradient(180deg, #FEF7F3 0%, #FAFAFA 30%, #F5F5F5 100%); background: linear-gradient(
180deg,
var(--primary-soft) 0%,
var(--bg-page) 30%,
var(--bg-soft) 100%
);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
@ -42,7 +47,9 @@
} }
@keyframes spin { @keyframes spin {
to { transform: translateX(-50%) rotate(360deg); } to {
transform: translateX(-50%) rotate(360deg);
}
} }
/* 主要内容 */ /* 主要内容 */
@ -61,15 +68,22 @@
} }
.match-badge { .match-badge {
font-size: 64rpx; width: 72rpx;
height: 72rpx;
margin: 0 auto;
margin-bottom: 16rpx; margin-bottom: 16rpx;
} }
.match-badge-img {
width: 72rpx;
height: 72rpx;
}
.match-title { .match-title {
display: block; display: block;
font-size: 48rpx; font-size: 48rpx;
font-weight: 700; font-weight: 700;
color: #1a1a1a; color: var(--text-primary);
margin-bottom: 16rpx; margin-bottom: 16rpx;
letter-spacing: 1rpx; letter-spacing: 1rpx;
} }
@ -83,14 +97,14 @@
border-radius: 50rpx; border-radius: 50rpx;
font-size: 26rpx; font-size: 26rpx;
font-weight: 600; font-weight: 600;
color: #666; color: var(--text-secondary);
} }
.status-dot { .status-dot {
width: 12rpx; width: 12rpx;
height: 12rpx; height: 12rpx;
border-radius: 50%; border-radius: 50%;
background: #ff6b35; background: var(--primary);
} }
.match-status.status-1 .status-dot { .match-status.status-1 .status-dot {
@ -98,17 +112,24 @@
} }
@keyframes pulse { @keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); } 0%,
50% { opacity: 0.5; transform: scale(1.2); } 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(1.2);
}
} }
/* 信息卡片 */ /* 信息卡片 */
.info-card { .info-card {
background: #fff; background: var(--bg-card);
border-radius: 28rpx; border-radius: 28rpx;
padding: 28rpx; padding: 28rpx;
margin-bottom: 20rpx; margin-bottom: 20rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.06); box-shadow: var(--shadow-card);
} }
.info-grid { .info-grid {
@ -131,7 +152,7 @@
} }
.info-value.accent { .info-value.accent {
color: #FF9800; color: var(--warning);
} }
.info-label { .info-label {
@ -149,10 +170,22 @@
margin-bottom: 8rpx; margin-bottom: 8rpx;
} }
.stage-tag.stage-0 { background: #E3F2FD; color: #1565C0; } .stage-tag.stage-0 {
.stage-tag.stage-1 { background: #E8F5E9; color: #2E7D32; } background: #e3f2fd;
.stage-tag.stage-2 { background: #FFF3E0; color: #E65100; } color: #1565c0;
.stage-tag.stage-3 { background: #ECEFF1; color: #546E7A; } }
.stage-tag.stage-1 {
background: #e8f5e9;
color: #2e7d32;
}
.stage-tag.stage-2 {
background: #fff3e0;
color: #e65100;
}
.stage-tag.stage-3 {
background: #eceff1;
color: #546e7a;
}
/* 我的状态卡片 */ /* 我的状态卡片 */
.my-status-card { .my-status-card {
@ -168,12 +201,13 @@
align-items: center; align-items: center;
gap: 12rpx; gap: 12rpx;
padding: 20rpx 24rpx; padding: 20rpx 24rpx;
background: linear-gradient(90deg, #FFF8E1, #FFFFFF); background: linear-gradient(90deg, #fff8e1, #ffffff);
border-bottom: 1rpx solid rgba(255, 152, 0, 0.1); border-bottom: 1rpx solid rgba(255, 152, 0, 0.1);
} }
.card-icon { .card-icon {
font-size: 28rpx; width: 28rpx;
height: 28rpx;
} }
.card-title { .card-title {
@ -209,18 +243,18 @@
} }
.status-badge.waiting { .status-badge.waiting {
background: linear-gradient(135deg, #E3F2FD, #BBDEFB); background: linear-gradient(135deg, #e3f2fd, #bbdefb);
color: #1565C0; color: #1565c0;
} }
.status-badge.playing { .status-badge.playing {
background: linear-gradient(135deg, #E8F5E9, #C8E6C9); background: linear-gradient(135deg, #e8f5e9, #c8e6c9);
color: #2E7D32; color: #2e7d32;
} }
.status-badge.finished { .status-badge.finished {
background: linear-gradient(135deg, #ECEFF1, #CFD8DC); background: linear-gradient(135deg, #eceff1, #cfd8dc);
color: #546E7A; color: #546e7a;
} }
.win-lose-stats { .win-lose-stats {
@ -240,11 +274,11 @@
} }
.stat.win .stat-num { .stat.win .stat-num {
color: #2E7D32; color: #2e7d32;
} }
.stat.lose .stat-num { .stat.lose .stat-num {
color: #C62828; color: #c62828;
} }
.stat-text { .stat-text {
@ -265,10 +299,15 @@
.game-divider::before, .game-divider::before,
.game-divider::after { .game-divider::after {
content: ''; content: "";
flex: 1; flex: 1;
height: 1rpx; height: 1rpx;
background: linear-gradient(90deg, transparent, rgba(0,0,0,0.08), transparent); background: linear-gradient(
90deg,
transparent,
rgba(0, 0, 0, 0.08),
transparent
);
} }
.divider-text { .divider-text {
@ -282,7 +321,7 @@
align-items: center; align-items: center;
gap: 16rpx; gap: 16rpx;
padding: 18rpx; padding: 18rpx;
background: linear-gradient(135deg, #FFF8E1, #FFFDE7); background: linear-gradient(135deg, #fff8e1, #fffde7);
border-radius: 16rpx; border-radius: 16rpx;
border: 2rpx solid rgba(255, 152, 0, 0.15); border: 2rpx solid rgba(255, 152, 0, 0.15);
} }
@ -291,7 +330,7 @@
width: 80rpx; width: 80rpx;
height: 80rpx; height: 80rpx;
border-radius: 50%; border-radius: 50%;
border: 3rpx solid #FFE082; border: 3rpx solid #ffe082;
} }
.opponent-info { .opponent-info {
@ -319,11 +358,26 @@
font-weight: 600; font-weight: 600;
} }
.level-tag.lv1 { background: linear-gradient(135deg, #81C784, #66BB6A); color: #fff; } .level-tag.lv1 {
.level-tag.lv2 { background: linear-gradient(135deg, #64B5F6, #42A5F5); color: #fff; } background: linear-gradient(135deg, #81c784, #66bb6a);
.level-tag.lv3 { background: linear-gradient(135deg, #FFB74D, #FFA726); color: #fff; } color: #fff;
.level-tag.lv4 { background: linear-gradient(135deg, #F06292, #EC407A); color: #fff; } }
.level-tag.lv5 { background: linear-gradient(135deg, #BA68C8, #AB47BC); color: #fff; } .level-tag.lv2 {
background: linear-gradient(135deg, #64b5f6, #42a5f5);
color: #fff;
}
.level-tag.lv3 {
background: linear-gradient(135deg, #ffb74d, #ffa726);
color: #fff;
}
.level-tag.lv4 {
background: linear-gradient(135deg, #f06292, #ec407a);
color: #fff;
}
.level-tag.lv5 {
background: linear-gradient(135deg, #ba68c8, #ab47bc);
color: #fff;
}
.opponent-power { .opponent-power {
font-size: 24rpx; font-size: 24rpx;
@ -347,7 +401,7 @@
align-items: center; align-items: center;
gap: 14rpx; gap: 14rpx;
padding: 16rpx 18rpx; padding: 16rpx 18rpx;
background: linear-gradient(135deg, #FAFAFA, #F5F5F5); background: linear-gradient(135deg, #fafafa, #f5f5f5);
border-radius: 16rpx; border-radius: 16rpx;
margin-bottom: 12rpx; margin-bottom: 12rpx;
transition: all 0.2s; transition: all 0.2s;
@ -358,7 +412,7 @@
} }
.player-item.is-me { .player-item.is-me {
background: linear-gradient(135deg, #FFF8E1, #FFFDE7); background: linear-gradient(135deg, #fff8e1, #fffde7);
border: 2rpx solid rgba(255, 152, 0, 0.2); border: 2rpx solid rgba(255, 152, 0, 0.2);
} }
@ -380,17 +434,17 @@
} }
.player-rank.rank-1 { .player-rank.rank-1 {
background: linear-gradient(135deg, #FFD54F, #FFB300); background: linear-gradient(135deg, #ffd54f, #ffb300);
color: #fff; color: #fff;
} }
.player-rank.rank-2 { .player-rank.rank-2 {
background: linear-gradient(135deg, #E0E0E0, #BDBDBD); background: linear-gradient(135deg, #e0e0e0, #bdbdbd);
color: #fff; color: #fff;
} }
.player-rank.rank-3 { .player-rank.rank-3 {
background: linear-gradient(135deg, #FFCC80, #FF9800); background: linear-gradient(135deg, #ffcc80, #ff9800);
color: #fff; color: #fff;
} }
@ -418,7 +472,7 @@
.player-me { .player-me {
padding: 2rpx 10rpx; padding: 2rpx 10rpx;
background: linear-gradient(135deg, #FF8A65, #FF6B35); background: linear-gradient(135deg, #ff8a65, #ff6b35);
color: #fff; color: #fff;
font-size: 18rpx; font-size: 18rpx;
font-weight: 600; font-weight: 600;
@ -432,12 +486,12 @@
} }
.record-win { .record-win {
color: #2E7D32; color: #2e7d32;
font-weight: 600; font-weight: 600;
} }
.record-lose { .record-lose {
color: #C62828; color: #c62828;
font-weight: 600; font-weight: 600;
} }
@ -445,20 +499,20 @@
width: 12rpx; width: 12rpx;
height: 12rpx; height: 12rpx;
border-radius: 50%; border-radius: 50%;
background: #BDBDBD; background: #bdbdbd;
} }
.player-status-dot.waiting { .player-status-dot.waiting {
background: #1565C0; background: #1565c0;
} }
.player-status-dot.playing { .player-status-dot.playing {
background: #2E7D32; background: #2e7d32;
animation: pulse 1.5s infinite; animation: pulse 1.5s infinite;
} }
.player-status-dot.finished { .player-status-dot.finished {
background: #9E9E9E; background: #9e9e9e;
} }
/* 空状态 */ /* 空状态 */
@ -470,7 +524,8 @@
} }
.empty-icon { .empty-icon {
font-size: 80rpx; width: 160rpx;
height: 160rpx;
margin-bottom: 16rpx; margin-bottom: 16rpx;
opacity: 0.5; opacity: 0.5;
} }
@ -495,4 +550,3 @@
.animate-fadeInUp { .animate-fadeInUp {
animation: fadeInUp 0.5s ease-out forwards; animation: fadeInUp 0.5s ease-out forwards;
} }

View File

@ -0,0 +1,68 @@
const app = getApp()
Page({
data: {
playerId: null,
player: null,
matches: [],
loadingMatches: false
},
onLoad(options) {
const playerId = options && options.id ? String(options.id) : null
this.setData({ playerId })
const eventChannel = this.getOpenerEventChannel ? this.getOpenerEventChannel() : null
if (eventChannel) {
eventChannel.on('player', (player) => {
if (player) this.setData({ player })
})
}
this.refresh()
},
async onPullDownRefresh() {
try {
await this.refresh()
} finally {
wx.stopPullDownRefresh()
}
},
async refresh() {
await Promise.all([this.fetchPlayer(), this.fetchMatches()])
},
async fetchPlayer() {
if (!this.data.playerId) return
try {
const res = await app.request('/api/ladder/player', { id: this.data.playerId })
if (res && res.data) this.setData({ player: res.data })
} catch (e) {
}
},
async fetchMatches() {
if (!this.data.playerId) return
this.setData({ loadingMatches: true })
try {
const res = await app.request('/api/match/history', { player_id: this.data.playerId })
const list = Array.isArray(res && res.data) ? res.data : (res && res.data && res.data.list) || []
const matches = list.map((item) => {
return Object.assign({}, item, {
timeText: item.timeText || item.createTime || item.matchTime || '',
resultClass: item.resultClass || (item.result === 'win' ? 'win' : item.result === 'lose' ? 'lose' : ''),
resultText:
item.resultText ||
(item.result === 'win' ? '胜' : item.result === 'lose' ? '负' : item.resultName || '')
})
})
this.setData({ matches })
} catch (e) {
this.setData({ matches: [] })
} finally {
this.setData({ loadingMatches: false })
}
}
})

View File

@ -0,0 +1,5 @@
{
"navigationBarTitleText": "选手资料",
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark"
}

View File

@ -0,0 +1,58 @@
<view class="page-container">
<view class="card profile-card" wx:if="{{player}}">
<view class="profile-top">
<image class="avatar" src="{{player.avatar || '/images/avatar-default.svg'}}" mode="aspectFill"></image>
<view class="profile-main">
<view class="name-row">
<text class="name">{{player.realName || player.nickname || '未命名'}}</text>
<view class="level-pill">Lv{{player.level || 1}}</view>
</view>
<view class="meta-row">
<text class="meta">战力 {{player.powerScore || 0}}</text>
<text class="meta-divider">·</text>
<text class="meta">胜率 {{player.winRate || 0}}%</text>
</view>
</view>
</view>
<view class="profile-stats">
<view class="stat">
<text class="stat-value">{{player.matchCount || 0}}</text>
<text class="stat-label">总场次</text>
</view>
<view class="stat">
<text class="stat-value">{{player.winCount || 0}}</text>
<text class="stat-label">胜场</text>
</view>
<view class="stat">
<text class="stat-value">{{player.loseCount || 0}}</text>
<text class="stat-label">负场</text>
</view>
</view>
</view>
<view class="card section-card">
<view class="section-header">
<text class="section-title">近期比赛</text>
<text class="section-sub" wx:if="{{loadingMatches}}">加载中</text>
</view>
<view class="match-list" wx:if="{{matches.length > 0}}">
<view class="match-item" wx:for="{{matches}}" wx:key="id">
<view class="match-row">
<text class="match-name">{{item.name || item.typeName || '比赛'}}</text>
<text class="match-time">{{item.timeText || item.createTime || ''}}</text>
</view>
<view class="match-row">
<text class="match-desc">{{item.desc || ''}}</text>
<text class="match-result {{item.resultClass || ''}}">{{item.resultText || ''}}</text>
</view>
</view>
</view>
<view class="empty-state" wx:else>
<image class="empty-icon" src="/images/empty-records.svg" mode="aspectFit"></image>
<text class="empty-title">暂无比赛记录</text>
<text class="empty-desc">完成比赛后会在这里展示</text>
</view>
</view>
</view>

View File

@ -0,0 +1,209 @@
.page-container {
min-height: 100vh;
background: var(--bg-page);
padding: 24rpx;
}
.profile-card {
padding: 28rpx;
}
.profile-top {
display: flex;
gap: 20rpx;
align-items: center;
}
.avatar {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
border: 4rpx solid var(--bg-white);
box-shadow: var(--shadow-sm);
background: var(--bg-soft);
flex-shrink: 0;
}
.profile-main {
flex: 1;
overflow: hidden;
}
.name-row {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 10rpx;
}
.name {
font-size: 36rpx;
font-weight: 700;
color: var(--text-primary);
max-width: 360rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.level-pill {
padding: 6rpx 14rpx;
border-radius: var(--radius-full);
font-size: 22rpx;
font-weight: 700;
background: var(--primary-soft);
color: var(--primary-dark);
border: 1rpx solid var(--border-primary);
}
.meta-row {
display: flex;
align-items: center;
gap: 10rpx;
color: var(--text-muted);
font-size: 24rpx;
}
.meta-divider {
opacity: 0.8;
}
.profile-stats {
margin-top: 24rpx;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12rpx;
padding-top: 24rpx;
border-top: 1rpx solid var(--border-soft);
}
.stat {
text-align: center;
background: var(--bg-soft);
border-radius: var(--radius-md);
padding: 18rpx 12rpx;
}
.stat-value {
display: block;
font-size: 32rpx;
font-weight: 800;
color: var(--text-primary);
line-height: 1.2;
margin-bottom: 6rpx;
}
.stat-label {
display: block;
font-size: 22rpx;
color: var(--text-muted);
}
.section-card {
padding: 28rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 18rpx;
}
.section-title {
font-size: 30rpx;
font-weight: 700;
color: var(--text-primary);
}
.section-sub {
font-size: 24rpx;
color: var(--text-muted);
}
.match-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.match-item {
padding: 18rpx 20rpx;
border-radius: var(--radius-md);
background: var(--bg-white);
border: 1rpx solid var(--border-soft);
}
.match-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16rpx;
}
.match-row + .match-row {
margin-top: 8rpx;
}
.match-name {
font-size: 26rpx;
color: var(--text-primary);
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.match-time {
font-size: 22rpx;
color: var(--text-muted);
flex-shrink: 0;
}
.match-desc {
font-size: 24rpx;
color: var(--text-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.match-result {
font-size: 24rpx;
font-weight: 700;
color: var(--text-muted);
flex-shrink: 0;
}
.match-result.win {
color: var(--success-text);
}
.match-result.lose {
color: var(--danger-text);
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 60rpx 24rpx 20rpx;
}
.empty-icon {
width: 160rpx;
height: 160rpx;
opacity: 0.75;
margin-bottom: 18rpx;
}
.empty-title {
font-size: 28rpx;
color: var(--text-secondary);
margin-bottom: 8rpx;
}
.empty-desc {
font-size: 24rpx;
color: var(--text-muted);
}

View File

@ -1,4 +1,4 @@
const app = getApp() const app = getApp();
Page({ Page({
data: { data: {
@ -10,150 +10,156 @@ Page({
pageSize: 20, pageSize: 20,
hasMore: true, hasMore: true,
showProductModal: false, showProductModal: false,
currentProduct: null currentProduct: null,
}, },
onLoad() { onLoad() {
this.initData() this.initData();
}, },
onShow() { onShow() {
this.setData({ this.setData({
userInfo: app.globalData.userInfo, userInfo: app.globalData.userInfo,
currentStore: app.globalData.currentStore currentStore: app.globalData.currentStore,
}) });
// 门店切换后刷新商品 // 门店切换后刷新商品
if (app.globalData.storeChanged) { if (app.globalData.storeChanged) {
app.globalData.storeChanged = false app.globalData.storeChanged = false;
this.setData({ page: 1, hasMore: true, products: [] }) this.setData({ page: 1, hasMore: true, products: [] });
this.fetchProducts() this.fetchProducts();
} }
}, },
onPullDownRefresh() { onPullDownRefresh() {
this.setData({ page: 1, hasMore: true }) this.setData({ page: 1, hasMore: true });
this.fetchProducts().then(() => { this.fetchProducts().then(() => {
wx.stopPullDownRefresh() wx.stopPullDownRefresh();
}) });
}, },
onReachBottom() { onReachBottom() {
if (this.data.hasMore && !this.data.loading) { if (this.data.hasMore && !this.data.loading) {
this.loadMore() this.loadMore();
} }
}, },
async initData() { async initData() {
if (!app.globalData.token) { if (!app.globalData.token) {
try { try {
await app.login() await app.login();
} catch (e) { } catch (e) {
console.error('登录失败:', e) console.error("登录失败:", e);
} }
} }
this.setData({ this.setData({
userInfo: app.globalData.userInfo, userInfo: app.globalData.userInfo,
currentStore: app.globalData.currentStore currentStore: app.globalData.currentStore,
}) });
this.fetchProducts() this.fetchProducts();
}, },
async fetchProducts() { async fetchProducts() {
this.setData({ loading: true }) this.setData({ loading: true });
try { try {
const params = { const params = {
page: this.data.page, page: this.data.page,
pageSize: this.data.pageSize pageSize: this.data.pageSize,
} };
// 根据当前门店筛选商品 // 根据当前门店筛选商品
if (this.data.currentStore?.storeId) { if (this.data.currentStore && this.data.currentStore.storeId) {
params.store_id = this.data.currentStore.storeId params.store_id = this.data.currentStore.storeId;
} }
const res = await app.request('/api/points/products', params) const res = await app.request("/api/points/products", params);
const products = res.data.list || [] const products = res.data.list || [];
this.setData({ this.setData({
products: this.data.page === 1 ? products : [...this.data.products, ...products], products:
hasMore: products.length >= this.data.pageSize this.data.page === 1 ? products : this.data.products.concat(products),
}) hasMore: products.length >= this.data.pageSize,
});
} catch (e) { } catch (e) {
console.error('获取商品列表失败:', e) console.error("获取商品列表失败:", e);
} finally { } finally {
this.setData({ loading: false }) this.setData({ loading: false });
} }
}, },
loadMore() { loadMore() {
this.setData({ page: this.data.page + 1 }) this.setData({ page: this.data.page + 1 });
this.fetchProducts() this.fetchProducts();
}, },
viewProduct(e) { viewProduct(e) {
const product = e.currentTarget.dataset.product const product = e.currentTarget.dataset.product;
this.setData({ this.setData({
currentProduct: product, currentProduct: product,
showProductModal: true showProductModal: true,
}) });
}, },
closeProductModal() { closeProductModal() {
this.setData({ showProductModal: false }) this.setData({ showProductModal: false });
}, },
async exchangeProduct() { async exchangeProduct() {
const product = this.data.currentProduct const product = this.data.currentProduct;
wx.showModal({ wx.showModal({
title: '确认兑换', title: "确认兑换",
content: `确定使用 ${product.pointsRequired} 积分兑换「${product.name}」吗?\n请到 ${product.storeName} 领取`, content: `确定使用 ${product.pointsRequired} 积分兑换「${product.name}」吗?\n请到 ${product.storeName} 领取`,
success: async (res) => { success: async (res) => {
if (!res.confirm) return if (!res.confirm) return;
wx.showLoading({ title: '兑换中...' }) wx.showLoading({ title: "兑换中..." });
try { try {
const exchangeRes = await app.request('/api/points/exchange', { const exchangeRes = await app.request(
product_id: product.id "/api/points/exchange",
}, 'POST') {
product_id: product.id,
},
"POST",
);
wx.hideLoading() wx.hideLoading();
// 更新用户积分 // 更新用户积分
const newPoints = this.data.userInfo.totalPoints - product.pointsRequired const newPoints =
app.globalData.userInfo.totalPoints = newPoints this.data.userInfo.totalPoints - product.pointsRequired;
app.globalData.userInfo.totalPoints = newPoints;
this.setData({ this.setData({
'userInfo.totalPoints': newPoints, "userInfo.totalPoints": newPoints,
showProductModal: false showProductModal: false,
}) });
wx.showModal({ wx.showModal({
title: '兑换成功', title: "兑换成功",
content: `请到 ${product.storeName} 出示兑换码领取\n兑换码: ${exchangeRes.data.exchangeCode}`, content: `请到 ${product.storeName} 出示兑换码领取\n兑换码: ${exchangeRes.data.exchangeCode}`,
showCancel: false, showCancel: false,
success: () => { success: () => {
wx.navigateTo({ url: '/pages/points/order/index' }) wx.navigateTo({ url: "/pages/points/order/index" });
} },
}) });
this.fetchProducts() this.fetchProducts();
} catch (e) { } catch (e) {
wx.hideLoading() wx.hideLoading();
console.error('兑换失败:', e) console.error("兑换失败:", e);
} }
} },
}) });
}, },
goToRecords() { goToRecords() {
wx.navigateTo({ url: '/pages/points/records/index' }) wx.navigateTo({ url: "/pages/points/records/index" });
}, },
goToOrders() { goToOrders() {
wx.navigateTo({ url: '/pages/points/order/index' }) wx.navigateTo({ url: "/pages/points/order/index" });
} },
}) });

View File

@ -44,13 +44,14 @@ Page({
const res = await app.request('/api/points/orders', params) const res = await app.request('/api/points/orders', params)
const orders = (res.data.list || []).map(order => ({ const orders = (res.data.list || []).map(order =>
...order, Object.assign({}, order, {
createdAt: util.formatDate(order.createdAt) createdAt: util.formatDate(order.createdAt)
})) })
)
this.setData({ this.setData({
orders: this.data.page === 1 ? orders : [...this.data.orders, ...orders], orders: this.data.page === 1 ? orders : this.data.orders.concat(orders),
hasMore: orders.length >= this.data.pageSize hasMore: orders.length >= this.data.pageSize
}) })
} catch (e) { } catch (e) {
@ -81,11 +82,10 @@ Page({
wx.hideLoading() wx.hideLoading()
const orderData = { const orderData = Object.assign({}, res.data, {
...res.data,
createdAt: util.formatDate(res.data.createdAt), createdAt: util.formatDate(res.data.createdAt),
qrcodeImage: '' qrcodeImage: ''
} })
this.setData({ this.setData({
currentOrder: orderData, currentOrder: orderData,

View File

@ -45,13 +45,14 @@ Page({
pageSize: this.data.pageSize pageSize: this.data.pageSize
}) })
const records = (res.data.list || []).map(record => ({ const records = (res.data.list || []).map(record =>
...record, Object.assign({}, record, {
createdAt: util.formatDate(record.createdAt) createdAt: util.formatDate(record.createdAt)
})) })
)
this.setData({ this.setData({
records: this.data.page === 1 ? records : [...this.data.records, ...records], records: this.data.page === 1 ? records : this.data.records.concat(records),
hasMore: records.length >= this.data.pageSize hasMore: records.length >= this.data.pageSize
}) })
} catch (e) { } catch (e) {

View File

@ -47,7 +47,7 @@ module.exports = { formatDistance: formatDistance };
<view class="store-section"> <view class="store-section">
<view class="section-header"> <view class="section-header">
<view class="section-title"> <view class="section-title">
<text class="section-icon">📍</text> <image class="section-icon" src="/images/icon-store.svg" mode="aspectFit"></image>
<text class="section-text">附近门店</text> <text class="section-text">附近门店</text>
</view> </view>
<text class="section-count">共 {{stores.length}} 家</text> <text class="section-count">共 {{stores.length}} 家</text>
@ -70,7 +70,7 @@ module.exports = { formatDistance: formatDistance };
<text class="store-item-address">{{item.address}}</text> <text class="store-item-address">{{item.address}}</text>
<view class="store-item-meta"> <view class="store-item-meta">
<text class="store-distance" wx:if="{{item.distance}}">{{util.formatDistance(item.distance)}}</text> <text class="store-distance" wx:if="{{item.distance}}">{{util.formatDistance(item.distance)}}</text>
<text class="store-users">{{item.sportType === 1 ? '🏸 羽毛球' : '🎾 网球'}}</text> <text class="store-users">{{item.sportType === 1 ? '羽毛球' : '网球'}}</text>
</view> </view>
</view> </view>

View File

@ -171,7 +171,8 @@
} }
.section-icon { .section-icon {
font-size: 28rpx; width: 28rpx;
height: 28rpx;
} }
.section-text { .section-text {

View File

@ -1,4 +1,4 @@
const app = getApp() const app = getApp();
Page({ Page({
data: { data: {
@ -6,41 +6,41 @@ Page({
ladderUser: null, ladderUser: null,
currentStore: null, currentStore: null,
showQrcode: false, showQrcode: false,
qrcodeImage: '', qrcodeImage: "",
qrcodeLoading: false, qrcodeLoading: false,
// 完善资料弹框 // 完善资料弹框
showProfileModal: false, showProfileModal: false,
profileForm: { profileForm: {
avatar: '', avatar: "",
nickname: '' nickname: "",
}, },
isEditMode: false // true: 编辑模式false: 完善模式(登录时) isEditMode: false, // true: 编辑模式false: 完善模式(登录时)
}, },
onLoad() { onLoad() {
this.initData() this.initData();
}, },
onShow() { onShow() {
// 检查门店是否切换 // 检查门店是否切换
if (app.globalData.storeChanged) { if (app.globalData.storeChanged) {
app.globalData.storeChanged = false app.globalData.storeChanged = false;
this.refreshData() this.refreshData();
} else { } else {
// 同步最新数据 // 同步最新数据
this.setData({ this.setData({
userInfo: app.globalData.userInfo, userInfo: app.globalData.userInfo,
ladderUser: app.globalData.ladderUser, ladderUser: app.globalData.ladderUser,
currentStore: app.globalData.currentStore currentStore: app.globalData.currentStore,
}) });
} }
}, },
async onPullDownRefresh() { async onPullDownRefresh() {
try { try {
await this.refreshData() await this.refreshData();
} finally { } finally {
wx.stopPullDownRefresh() wx.stopPullDownRefresh();
} }
}, },
@ -48,79 +48,84 @@ Page({
// 先进行微信登录获取openid // 先进行微信登录获取openid
if (!app.globalData.wxLoginInfo) { if (!app.globalData.wxLoginInfo) {
try { try {
await app.wxLogin() await app.wxLogin();
} catch (e) { } catch (e) {
console.error('微信登录失败:', e) console.error("微信登录失败:", e);
} }
} }
if (app.globalData.token) { if (app.globalData.token) {
await this.refreshData() await this.refreshData();
} }
}, },
async refreshData() { async refreshData() {
if (!app.globalData.token) return if (!app.globalData.token) return;
try { try {
await app.getUserInfo() await app.getUserInfo();
// 如果当前门店有 ladderUserId确保获取该门店的天梯用户信息 // 如果当前门店有 ladderUserId确保获取该门店的天梯用户信息
if (app.globalData.currentStore?.storeId && !app.globalData.ladderUser) { if (
app.globalData.currentStore &&
app.globalData.currentStore.storeId &&
!app.globalData.ladderUser
) {
try { try {
await app.getLadderUser(app.globalData.currentStore.storeId) await app.getLadderUser(app.globalData.currentStore.storeId);
} catch (e) { } catch (e) {
console.error('获取天梯用户信息失败:', e) console.error("获取天梯用户信息失败:", e);
} }
} }
this.setData({ this.setData({
userInfo: app.globalData.userInfo, userInfo: app.globalData.userInfo,
ladderUser: app.globalData.ladderUser, ladderUser: app.globalData.ladderUser,
currentStore: app.globalData.currentStore currentStore: app.globalData.currentStore,
}) });
} catch (e) { } catch (e) {
console.error('获取用户信息失败:', e) console.error("获取用户信息失败:", e);
} }
}, },
// 获取手机号授权 // 获取手机号授权
async onGetPhoneNumber(e) { async onGetPhoneNumber(e) {
if (e.detail.errMsg !== 'getPhoneNumber:ok') { if (e.detail.errMsg !== "getPhoneNumber:ok") {
wx.showToast({ title: '需要授权手机号才能登录', icon: 'none' }) wx.showToast({ title: "需要授权手机号才能登录", icon: "none" });
return return;
} }
wx.showLoading({ title: '登录中...' }) wx.showLoading({ title: "登录中..." });
try { try {
// 如果没有微信登录信息,先登录 // 如果没有微信登录信息,先登录
if (!app.globalData.wxLoginInfo) { if (!app.globalData.wxLoginInfo) {
await app.wxLogin() await app.wxLogin();
} }
// 手机号登录(先不传头像昵称) // 手机号登录(先不传头像昵称)
await app.phoneLogin(e.detail.encryptedData, e.detail.iv, null) await app.phoneLogin(e.detail.encryptedData, e.detail.iv, null);
// 获取门店信息 // 获取门店信息
await app.getCurrentStore() await app.getCurrentStore();
const userInfo = app.globalData.userInfo const userInfo = app.globalData.userInfo;
this.setData({ this.setData({
userInfo: userInfo, userInfo: userInfo,
ladderUser: app.globalData.ladderUser, ladderUser: app.globalData.ladderUser,
currentStore: app.globalData.currentStore currentStore: app.globalData.currentStore,
}) });
wx.hideLoading() wx.hideLoading();
// 检查是否需要完善资料(没有头像或昵称为默认值) // 检查是否需要完善资料(没有头像或昵称为默认值)
const needProfile = !userInfo.avatar || const needProfile =
userInfo.avatar === '' || !userInfo.avatar ||
!userInfo.nickname || userInfo.avatar === "" ||
userInfo.nickname === '新用户' || !userInfo.nickname ||
userInfo.nickname === '' userInfo.nickname === "新用户" ||
userInfo.nickname === "";
if (needProfile) { if (needProfile) {
// 弹出完善资料弹框 // 弹出完善资料弹框
@ -128,94 +133,101 @@ Page({
showProfileModal: true, showProfileModal: true,
isEditMode: false, isEditMode: false,
profileForm: { profileForm: {
avatar: userInfo.avatar || '/images/avatar-default.svg', avatar: userInfo.avatar || "/images/avatar-default.svg",
nickname: userInfo.nickname === '新用户' ? '' : (userInfo.nickname || '') nickname:
} userInfo.nickname === "新用户" ? "" : userInfo.nickname || "",
}) },
wx.showToast({ title: '登录成功,请完善资料', icon: 'none' }) });
wx.showToast({ title: "登录成功,请完善资料", icon: "none" });
} else { } else {
wx.showToast({ title: '登录成功', icon: 'success' }) wx.showToast({ title: "登录成功", icon: "success" });
} }
} catch (e) { } catch (e) {
wx.hideLoading() wx.hideLoading();
console.error('登录失败:', e) console.error("登录失败:", e);
wx.showToast({ title: e.message || '登录失败', icon: 'none' }) wx.showToast({ title: e.message || "登录失败", icon: "none" });
} }
}, },
// 点击头像,打开编辑资料弹框 // 点击头像,打开编辑资料弹框
onTapAvatar() { onTapAvatar() {
if (!this.data.userInfo?.phone) return if (!this.data.userInfo || !this.data.userInfo.phone) return;
this.setData({ this.setData({
showProfileModal: true, showProfileModal: true,
isEditMode: true, isEditMode: true,
profileForm: { profileForm: {
avatar: this.data.userInfo.avatar || '/images/avatar-default.svg', avatar: this.data.userInfo.avatar || "/images/avatar-default.svg",
nickname: this.data.userInfo.nickname || '' nickname: this.data.userInfo.nickname || "",
} },
}) });
}, },
// 选择头像新APIbutton open-type="chooseAvatar" // 选择头像新APIbutton open-type="chooseAvatar"
onChooseAvatarNew(e) { onChooseAvatarNew(e) {
const avatarUrl = e.detail.avatarUrl const avatarUrl = e.detail.avatarUrl;
this.setData({ this.setData({
'profileForm.avatar': avatarUrl "profileForm.avatar": avatarUrl,
}) });
}, },
// 输入昵称 // 输入昵称
onNicknameInput(e) { onNicknameInput(e) {
this.setData({ this.setData({
'profileForm.nickname': e.detail.value "profileForm.nickname": e.detail.value,
}) });
}, },
// 确认保存资料 // 确认保存资料
async saveProfile() { async saveProfile() {
const { avatar, nickname } = this.data.profileForm const { avatar, nickname } = this.data.profileForm;
if (!nickname || nickname.trim() === '') { if (!nickname || nickname.trim() === "") {
wx.showToast({ title: '请输入昵称', icon: 'none' }) wx.showToast({ title: "请输入昵称", icon: "none" });
return return;
} }
wx.showLoading({ title: '保存中...' }) wx.showLoading({ title: "保存中..." });
try { try {
// 如果选择了新头像,先上传 // 如果选择了新头像,先上传
let avatarUrl = avatar let avatarUrl = avatar;
if (avatar && (avatar.startsWith('wxfile://') || avatar.startsWith('http://tmp'))) { if (
avatarUrl = await this.uploadAvatar(avatar) avatar &&
(avatar.startsWith("wxfile://") || avatar.startsWith("http://tmp"))
) {
avatarUrl = await this.uploadAvatar(avatar);
} }
// 调用更新资料接口 // 调用更新资料接口
const res = await app.request('/api/user/profile', { const res = await app.request(
nickname: nickname.trim(), "/api/user/profile",
avatar: avatarUrl {
}, 'PUT') nickname: nickname.trim(),
avatar: avatarUrl,
},
"PUT",
);
// 更新本地数据服务端已返回完整URL // 更新本地数据服务端已返回完整URL
const userInfo = { const userInfo = Object.assign({}, this.data.userInfo, {
...this.data.userInfo, nickname: (res.data && res.data.nickname) || nickname.trim(),
nickname: res.data?.nickname || nickname.trim(), avatar: (res.data && res.data.avatar) || avatarUrl,
avatar: res.data?.avatar || avatarUrl });
} app.globalData.userInfo = userInfo;
app.globalData.userInfo = userInfo
this.setData({ this.setData({
userInfo: userInfo, userInfo: userInfo,
showProfileModal: false, showProfileModal: false,
profileForm: { avatar: '', nickname: '' } profileForm: { avatar: "", nickname: "" },
}) });
wx.hideLoading() wx.hideLoading();
wx.showToast({ title: '保存成功', icon: 'success' }) wx.showToast({ title: "保存成功", icon: "success" });
} catch (e) { } catch (e) {
wx.hideLoading() wx.hideLoading();
console.error('保存资料失败:', e) console.error("保存资料失败:", e);
wx.showToast({ title: e.message || '保存失败', icon: 'none' }) wx.showToast({ title: e.message || "保存失败", icon: "none" });
} }
}, },
@ -225,29 +237,29 @@ Page({
wx.uploadFile({ wx.uploadFile({
url: `${app.globalData.baseUrl}/api/upload/avatar`, url: `${app.globalData.baseUrl}/api/upload/avatar`,
filePath: filePath, filePath: filePath,
name: 'file', name: "file",
header: { header: {
'Authorization': `Bearer ${app.globalData.token}` Authorization: `Bearer ${app.globalData.token}`,
}, },
success: (res) => { success: (res) => {
try { try {
const data = JSON.parse(res.data) const data = JSON.parse(res.data);
if (data.code === 0 && data.data?.url) { if (data.code === 0 && data.data && data.data.url) {
resolve(data.data.url) resolve(data.data.url);
} else { } else {
console.error('上传头像失败:', data) console.error("上传头像失败:", data);
resolve(filePath) resolve(filePath);
} }
} catch (e) { } catch (e) {
resolve(filePath) resolve(filePath);
} }
}, },
fail: (err) => { fail: (err) => {
console.error('上传头像失败:', err) console.error("上传头像失败:", err);
resolve(filePath) resolve(filePath);
} },
}) });
}) });
}, },
// 关闭资料弹框 // 关闭资料弹框
@ -255,63 +267,63 @@ Page({
// 如果是完善模式,提示用户 // 如果是完善模式,提示用户
if (!this.data.isEditMode) { if (!this.data.isEditMode) {
wx.showModal({ wx.showModal({
title: '提示', title: "提示",
content: '完善资料后可以让好友更容易找到你,确定跳过?', content: "完善资料后可以让好友更容易找到你,确定跳过?",
confirmText: '跳过', confirmText: "跳过",
cancelText: '继续完善', cancelText: "继续完善",
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
this.setData({ showProfileModal: false }) this.setData({ showProfileModal: false });
} }
} },
}) });
} else { } else {
this.setData({ showProfileModal: false }) this.setData({ showProfileModal: false });
} }
}, },
async showMemberCode() { async showMemberCode() {
if (!this.data.userInfo?.memberCode) return if (!this.data.userInfo || !this.data.userInfo.memberCode) return;
this.setData({ this.setData({
showQrcode: true, showQrcode: true,
qrcodeLoading: true qrcodeLoading: true,
}) });
try { try {
// 调用接口获取二维码 // 调用接口获取二维码
const res = await app.request('/api/user/qrcode') const res = await app.request("/api/user/qrcode");
if (res.data && res.data.qrcode) { if (res.data && res.data.qrcode) {
this.setData({ this.setData({
qrcodeImage: res.data.qrcode, qrcodeImage: res.data.qrcode,
qrcodeLoading: false qrcodeLoading: false,
}) });
} }
} catch (e) { } catch (e) {
console.error('获取二维码失败:', e) console.error("获取二维码失败:", e);
this.setData({ qrcodeLoading: false }) this.setData({ qrcodeLoading: false });
wx.showToast({ title: '获取二维码失败', icon: 'none' }) wx.showToast({ title: "获取二维码失败", icon: "none" });
} }
}, },
hideQrcode() { hideQrcode() {
this.setData({ this.setData({
showQrcode: false, showQrcode: false,
qrcodeImage: '' qrcodeImage: "",
}) });
}, },
goTo(e) { goTo(e) {
const url = e.currentTarget.dataset.url const url = e.currentTarget.dataset.url;
if (!app.globalData.token) { if (!app.globalData.token) {
wx.showToast({ title: '请先登录', icon: 'none' }) wx.showToast({ title: "请先登录", icon: "none" });
return return;
} }
wx.navigateTo({ url }) wx.navigateTo({ url });
}, },
// 阻止事件冒泡 // 阻止事件冒泡
preventBubble() { preventBubble() {
// 空函数,仅用于阻止事件冒泡 // 空函数,仅用于阻止事件冒泡
} },
}) });

View File

@ -67,7 +67,7 @@
<text class="login-subtitle">授权手机号,加入英飒俱乐部</text> <text class="login-subtitle">授权手机号,加入英飒俱乐部</text>
</view> </view>
<button class="login-btn-primary" open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber"> <button class="login-btn-primary" open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber">
<text class="btn-icon">📱</text> <image class="btn-icon" src="/images/icon-phone.svg" mode="aspectFit"></image>
<text class="btn-text">手机号快捷登录</text> <text class="btn-text">手机号快捷登录</text>
</button> </button>
</view> </view>
@ -117,7 +117,9 @@
</view> </view>
<!-- 未加入天梯提示 --> <!-- 未加入天梯提示 -->
<view class="notice-card animate-fadeInUp" style="animation-delay: 0.2s" wx:elif="{{userInfo && userInfo.phone}}"> <view class="notice-card animate-fadeInUp" style="animation-delay: 0.2s" wx:elif="{{userInfo && userInfo.phone}}">
<view class="notice-icon">🏸</view> <view class="notice-icon">
<image class="notice-icon-img" src="/images/icon-challenge.svg" mode="aspectFit"></image>
</view>
<view class="notice-content"> <view class="notice-content">
<text class="notice-title">尚未加入天梯系统</text> <text class="notice-title">尚未加入天梯系统</text>
<text class="notice-desc">请联系门店工作人员,开启你的天梯之旅</text> <text class="notice-desc">请联系门店工作人员,开启你的天梯之旅</text>
@ -183,7 +185,7 @@
</view> </view>
<view class="qrcode-tips"> <view class="qrcode-tips">
<view class="tip-item"> <view class="tip-item">
<text class="tip-icon">📱</text> <image class="tip-icon" src="/images/icon-qrcode.svg" mode="aspectFit"></image>
<text class="tip-text">请出示给对方扫描发起挑战</text> <text class="tip-text">请出示给对方扫描发起挑战</text>
</view> </view>
</view> </view>
@ -199,7 +201,7 @@
<view class="profile-modal-body"> <view class="profile-modal-body">
<!-- 提示信息 --> <!-- 提示信息 -->
<view class="profile-tips" wx:if="{{!isEditMode}}"> <view class="profile-tips" wx:if="{{!isEditMode}}">
<text class="tips-icon">💡</text> <image class="tips-icon" src="/images/icon-info.svg" mode="aspectFit"></image>
<text class="tips-text">完善资料后,好友可以更容易找到你</text> <text class="tips-text">完善资料后,好友可以更容易找到你</text>
</view> </view>
<!-- 头像选择 --> <!-- 头像选择 -->
@ -208,7 +210,7 @@
<button class="avatar-choose-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatarNew"> <button class="avatar-choose-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatarNew">
<image class="profile-avatar-preview" src="{{profileForm.avatar || '/images/avatar-default.svg'}}" mode="aspectFill"></image> <image class="profile-avatar-preview" src="{{profileForm.avatar || '/images/avatar-default.svg'}}" mode="aspectFill"></image>
<view class="avatar-choose-badge"> <view class="avatar-choose-badge">
<text class="choose-icon">📷</text> <image class="choose-icon" src="/images/icon-scan.svg" mode="aspectFit"></image>
</view> </view>
</button> </button>
<text class="avatar-tip">点击更换头像</text> <text class="avatar-tip">点击更换头像</text>

View File

@ -153,7 +153,7 @@
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background: linear-gradient(135deg, #FFF8F5 0%, #FFFFFF 50%, #FFF5F0 100%); background: linear-gradient(135deg, #fff8f5 0%, #ffffff 50%, #fff5f0 100%);
} }
.member-card-content { .member-card-content {
@ -233,7 +233,7 @@
.scan-hint { .scan-hint {
position: relative; position: relative;
padding: 12rpx 24rpx; padding: 12rpx 24rpx;
background: linear-gradient(90deg, var(--primary-soft), #FFF8F5); background: linear-gradient(90deg, var(--primary-soft), #fff8f5);
text-align: center; text-align: center;
font-size: 22rpx; font-size: 22rpx;
color: var(--primary); color: var(--primary);
@ -321,8 +321,10 @@
} }
.login-btn-primary .btn-icon { .login-btn-primary .btn-icon {
font-size: 32rpx; width: 32rpx;
height: 32rpx;
flex-shrink: 0; flex-shrink: 0;
display: block;
} }
.login-btn-primary .btn-text { .login-btn-primary .btn-text {
@ -360,7 +362,7 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 20rpx 24rpx; padding: 20rpx 24rpx;
background: linear-gradient(90deg, #FFF8F5, var(--bg-white)); background: linear-gradient(90deg, #fff8f5, var(--bg-white));
border-bottom: 1rpx solid var(--border-soft); border-bottom: 1rpx solid var(--border-soft);
} }
@ -397,11 +399,21 @@
color: #fff; color: #fff;
} }
.stat-icon.lv1 { background: linear-gradient(135deg, #4CAF50, #8BC34A); } .stat-icon.lv1 {
.stat-icon.lv2 { background: linear-gradient(135deg, #2196F3, #03A9F4); } background: linear-gradient(135deg, #4caf50, #8bc34a);
.stat-icon.lv3 { background: linear-gradient(135deg, #FF9800, #FFC107); } }
.stat-icon.lv4 { background: linear-gradient(135deg, #E91E63, #FF5722); } .stat-icon.lv2 {
.stat-icon.lv5 { background: linear-gradient(135deg, #9C27B0, #673AB7); } background: linear-gradient(135deg, #2196f3, #03a9f4);
}
.stat-icon.lv3 {
background: linear-gradient(135deg, #ff9800, #ffc107);
}
.stat-icon.lv4 {
background: linear-gradient(135deg, #e91e63, #ff5722);
}
.stat-icon.lv5 {
background: linear-gradient(135deg, #9c27b0, #673ab7);
}
.stat-name { .stat-name {
font-size: 30rpx; font-size: 30rpx;
@ -447,12 +459,17 @@
.ladder-record { .ladder-record {
display: flex; display: flex;
justify-content: space-around; justify-content: space-between;
padding-top: 20rpx; padding-top: 20rpx;
border-top: 1rpx solid var(--border-soft); border-top: 1rpx solid var(--border-soft);
} }
.record-item { .record-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center; text-align: center;
} }
@ -486,14 +503,27 @@
gap: 16rpx; gap: 16rpx;
margin: 0 24rpx 20rpx; margin: 0 24rpx 20rpx;
padding: 24rpx; padding: 24rpx;
background: #FFFBF5; background: #fffbf5;
border: 1rpx solid #FFE8D5; border: 1rpx solid #ffe8d5;
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
animation: fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1) 0.2s backwards; animation: fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1) 0.2s backwards;
} }
.notice-icon { .notice-icon {
font-size: 36rpx; width: 72rpx;
height: 72rpx;
background: var(--primary-gradient-soft);
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.notice-icon-img {
width: 40rpx;
height: 40rpx;
display: block;
} }
.notice-content { .notice-content {
@ -504,14 +534,14 @@
display: block; display: block;
font-size: 26rpx; font-size: 26rpx;
font-weight: 600; font-weight: 600;
color: #B7791F; color: #b7791f;
margin-bottom: 6rpx; margin-bottom: 6rpx;
} }
.notice-desc { .notice-desc {
display: block; display: block;
font-size: 22rpx; font-size: 22rpx;
color: #C68A42; color: #c68a42;
line-height: 1.5; line-height: 1.5;
} }
@ -556,19 +586,19 @@
} }
.menu-icon.history { .menu-icon.history {
background: linear-gradient(135deg, #E3F2FD, #BBDEFB); background: linear-gradient(135deg, #e3f2fd, #bbdefb);
} }
.menu-icon.points { .menu-icon.points {
background: linear-gradient(135deg, #FFF8E1, #FFECB3); background: linear-gradient(135deg, #fff8e1, #ffecb3);
} }
.menu-icon.order { .menu-icon.order {
background: linear-gradient(135deg, #E8F5E9, #C8E6C9); background: linear-gradient(135deg, #e8f5e9, #c8e6c9);
} }
.menu-icon.store { .menu-icon.store {
background: linear-gradient(135deg, #FFF3E0, #FFE0B2); background: linear-gradient(135deg, #fff3e0, #ffe0b2);
} }
.menu-icon image { .menu-icon image {
@ -627,7 +657,7 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 24rpx 28rpx; padding: 24rpx 28rpx;
background: linear-gradient(90deg, #FFF8F5, var(--bg-white)); background: linear-gradient(90deg, #fff8f5, var(--bg-white));
border-bottom: 1rpx solid var(--border-soft); border-bottom: 1rpx solid var(--border-soft);
} }
@ -667,7 +697,7 @@
.qrcode-border { .qrcode-border {
width: 320rpx; width: 320rpx;
height: 320rpx; height: 320rpx;
background: #FFFFFF; background: #ffffff;
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
padding: 16rpx; padding: 16rpx;
box-shadow: var(--shadow-md); box-shadow: var(--shadow-md);
@ -723,10 +753,30 @@
border-style: solid; border-style: solid;
} }
.corner.tl { top: 0; left: 0; border-width: 6rpx 0 0 6rpx; border-radius: 10rpx 0 0 0; } .corner.tl {
.corner.tr { top: 0; right: 0; border-width: 6rpx 6rpx 0 0; border-radius: 0 10rpx 0 0; } top: 0;
.corner.bl { bottom: 0; left: 0; border-width: 0 0 6rpx 6rpx; border-radius: 0 0 0 10rpx; } left: 0;
.corner.br { bottom: 0; right: 0; border-width: 0 6rpx 6rpx 0; border-radius: 0 0 10rpx 0; } border-width: 6rpx 0 0 6rpx;
border-radius: 10rpx 0 0 0;
}
.corner.tr {
top: 0;
right: 0;
border-width: 6rpx 6rpx 0 0;
border-radius: 0 10rpx 0 0;
}
.corner.bl {
bottom: 0;
left: 0;
border-width: 0 0 6rpx 6rpx;
border-radius: 0 0 0 10rpx;
}
.corner.br {
bottom: 0;
right: 0;
border-width: 0 6rpx 6rpx 0;
border-radius: 0 0 10rpx 0;
}
.qrcode-info { .qrcode-info {
text-align: center; text-align: center;
@ -762,7 +812,9 @@
} }
.tip-icon { .tip-icon {
font-size: 24rpx; width: 24rpx;
height: 24rpx;
display: block;
} }
.tip-text { .tip-text {
@ -837,7 +889,7 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 28rpx 32rpx; padding: 28rpx 32rpx;
background: linear-gradient(90deg, #FFF8F5, var(--bg-white)); background: linear-gradient(90deg, #fff8f5, var(--bg-white));
border-bottom: 1rpx solid var(--border-soft); border-bottom: 1rpx solid var(--border-soft);
} }
@ -874,19 +926,21 @@
align-items: center; align-items: center;
gap: 12rpx; gap: 12rpx;
padding: 20rpx 24rpx; padding: 20rpx 24rpx;
background: #FFFBF5; background: #fffbf5;
border: 1rpx solid #FFE8D5; border: 1rpx solid #ffe8d5;
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
margin-bottom: 32rpx; margin-bottom: 32rpx;
} }
.tips-icon { .tips-icon {
font-size: 32rpx; width: 32rpx;
height: 32rpx;
display: block;
} }
.tips-text { .tips-text {
font-size: 24rpx; font-size: 24rpx;
color: #B7791F; color: #b7791f;
line-height: 1.4; line-height: 1.4;
} }
@ -948,8 +1002,9 @@
} }
.avatar-choose-badge .choose-icon { .avatar-choose-badge .choose-icon {
font-size: 24rpx; width: 24rpx;
line-height: 1; height: 24rpx;
display: block;
} }
.avatar-tip { .avatar-tip {

View File

@ -1,28 +1,46 @@
const { LadderUser, User, Store } = require('../models'); const { LadderUser, User, Store } = require("../models");
const { LADDER_LEVEL_NAMES, LADDER_LEVEL_DESC, POWER_CALC } = require('../config/constants'); const {
const { success, error, getPagination, pageResult } = require('../utils/helper'); LADDER_LEVEL_NAMES,
const { Op } = require('sequelize'); LADDER_LEVEL_DESC,
const sequelize = require('../config/database'); POWER_CALC,
} = require("../config/constants");
const {
success,
error,
getPagination,
pageResult,
} = require("../utils/helper");
const { Op } = require("sequelize");
const sequelize = require("../config/database");
class LadderController { class LadderController {
// 获取天梯排名 // 获取天梯排名
async getRanking(req, res) { async getRanking(req, res) {
try { try {
const { store_id, gender, level, page = 1, pageSize = 50, is_display } = req.query; const {
store_id,
gender,
level,
page = 1,
pageSize = 50,
is_display,
} = req.query;
const { limit, offset } = getPagination(page, pageSize); const { limit, offset } = getPagination(page, pageSize);
if (!store_id) { if (!store_id) {
return res.status(400).json(error('缺少门店ID', 400)); return res.status(400).json(error("缺少门店ID", 400));
} }
const where = { const where = {
store_id, store_id,
status: 1 status: 1,
}; };
// 如果不是大屏显示,则需要满足每月最低参赛场次限制 // 如果不是大屏显示,则需要满足每月最低参赛场次限制
if (!is_display) { if (!is_display) {
where.monthly_match_count = { [Op.gte]: POWER_CALC.MIN_MONTHLY_MATCHES }; where.monthly_match_count = {
[Op.gte]: POWER_CALC.MIN_MONTHLY_MATCHES,
};
} }
if (gender) { if (gender) {
@ -35,11 +53,11 @@ class LadderController {
const { rows, count } = await LadderUser.findAndCountAll({ const { rows, count } = await LadderUser.findAndCountAll({
where, where,
include: [ include: [
{ model: User, as: 'user', attributes: ['nickname', 'avatar'] } { model: User, as: "user", attributes: ["nickname", "avatar"] },
], ],
order: [['power_score', 'DESC']], order: [["power_score", "DESC"]],
limit, limit,
offset offset,
}); });
// 添加排名 // 添加排名
@ -57,13 +75,16 @@ class LadderController {
powerScore: lu.power_score, powerScore: lu.power_score,
matchCount: lu.match_count, matchCount: lu.match_count,
winCount: lu.win_count, winCount: lu.win_count,
winRate: lu.match_count > 0 ? Math.round(lu.win_count / lu.match_count * 100) : 0 winRate:
lu.match_count > 0
? Math.round((lu.win_count / lu.match_count) * 100)
: 0,
})); }));
res.json(pageResult(list, count, page, pageSize)); res.json(pageResult(list, count, page, pageSize));
} catch (err) { } catch (err) {
console.error('获取排名失败:', err); console.error("获取排名失败:", err);
res.status(500).json(error('获取失败')); res.status(500).json(error("获取失败"));
} }
} }
@ -74,13 +95,17 @@ class LadderController {
const ladderUser = await LadderUser.findByPk(id, { const ladderUser = await LadderUser.findByPk(id, {
include: [ include: [
{ model: User, as: 'user', attributes: ['nickname', 'avatar', 'member_code'] }, {
{ model: Store, as: 'store', attributes: ['id', 'name'] } model: User,
] as: "user",
attributes: ["nickname", "avatar", "member_code"],
},
{ model: Store, as: "store", attributes: ["id", "name"] },
],
}); });
if (!ladderUser || ladderUser.status !== 1) { if (!ladderUser || ladderUser.status !== 1) {
return res.status(404).json(error('用户不存在', 404)); return res.status(404).json(error("用户不存在", 404));
} }
// 计算排名 // 计算排名
@ -90,62 +115,138 @@ class LadderController {
gender: ladderUser.gender, gender: ladderUser.gender,
status: 1, status: 1,
power_score: { [Op.gt]: ladderUser.power_score }, power_score: { [Op.gt]: ladderUser.power_score },
monthly_match_count: { [Op.gte]: POWER_CALC.MIN_MONTHLY_MATCHES } monthly_match_count: { [Op.gte]: POWER_CALC.MIN_MONTHLY_MATCHES },
} },
}); });
res.json(success({ res.json(
id: ladderUser.id, success({
userId: ladderUser.user_id, id: ladderUser.id,
realName: ladderUser.real_name, userId: ladderUser.user_id,
nickname: ladderUser.user?.nickname, realName: ladderUser.real_name,
avatar: ladderUser.user?.avatar, nickname: ladderUser.user?.nickname,
memberCode: ladderUser.user?.member_code, avatar: ladderUser.user?.avatar,
gender: ladderUser.gender, memberCode: ladderUser.user?.member_code,
level: ladderUser.level, gender: ladderUser.gender,
levelName: LADDER_LEVEL_NAMES[ladderUser.level], level: ladderUser.level,
levelDesc: LADDER_LEVEL_DESC[ladderUser.level], levelName: LADDER_LEVEL_NAMES[ladderUser.level],
powerScore: ladderUser.power_score, levelDesc: LADDER_LEVEL_DESC[ladderUser.level],
matchCount: ladderUser.match_count, powerScore: ladderUser.power_score,
winCount: ladderUser.win_count, matchCount: ladderUser.match_count,
monthlyMatchCount: ladderUser.monthly_match_count, winCount: ladderUser.win_count,
winRate: ladderUser.match_count > 0 ? Math.round(ladderUser.win_count / ladderUser.match_count * 100) : 0, monthlyMatchCount: ladderUser.monthly_match_count,
rank: higherCount + 1, winRate:
storeId: ladderUser.store_id, ladderUser.match_count > 0
storeName: ladderUser.store?.name, ? Math.round(
lastMatchTime: ladderUser.last_match_time (ladderUser.win_count / ladderUser.match_count) * 100,
})); )
: 0,
rank: higherCount + 1,
storeId: ladderUser.store_id,
storeName: ladderUser.store?.name,
lastMatchTime: ladderUser.last_match_time,
}),
);
} catch (err) { } catch (err) {
console.error('获取用户详情失败:', err); console.error("获取用户详情失败:", err);
res.status(500).json(error('获取失败')); res.status(500).json(error("获取失败"));
}
}
// 选手详情(兼容小程序端:/api/ladder/player?id=xxx
async getPlayerDetail(req, res) {
try {
const id = req.query && req.query.id ? String(req.query.id) : null;
if (!id) {
return res.status(400).json(error("缺少选手ID", 400));
}
const ladderUser = await LadderUser.findByPk(id, {
include: [
{
model: User,
as: "user",
attributes: ["nickname", "avatar", "member_code"],
},
{ model: Store, as: "store", attributes: ["id", "name"] },
],
});
if (!ladderUser || ladderUser.status !== 1) {
return res.status(404).json(error("用户不存在", 404));
}
const higherCount = await LadderUser.count({
where: {
store_id: ladderUser.store_id,
gender: ladderUser.gender,
status: 1,
power_score: { [Op.gt]: ladderUser.power_score },
monthly_match_count: { [Op.gte]: POWER_CALC.MIN_MONTHLY_MATCHES },
},
});
const matchCount = ladderUser.match_count || 0;
const winCount = ladderUser.win_count || 0;
res.json(
success({
id: ladderUser.id,
userId: ladderUser.user_id,
realName: ladderUser.real_name,
nickname: ladderUser.user && ladderUser.user.nickname,
avatar: ladderUser.user && ladderUser.user.avatar,
memberCode: ladderUser.user && ladderUser.user.member_code,
gender: ladderUser.gender,
level: ladderUser.level,
levelName: LADDER_LEVEL_NAMES[ladderUser.level],
levelDesc: LADDER_LEVEL_DESC[ladderUser.level],
powerScore: ladderUser.power_score,
matchCount: matchCount,
winCount: winCount,
loseCount: Math.max(matchCount - winCount, 0),
monthlyMatchCount: ladderUser.monthly_match_count,
winRate:
matchCount > 0 ? Math.round((winCount / matchCount) * 100) : 0,
rank: higherCount + 1,
storeId: ladderUser.store_id,
storeName: ladderUser.store && ladderUser.store.name,
lastMatchTime: ladderUser.last_match_time,
}),
);
} catch (err) {
console.error("获取用户详情失败:", err);
res.status(500).json(error("获取失败"));
} }
} }
// 获取等级说明 // 获取等级说明
async getLevelInfo(req, res) { async getLevelInfo(req, res) {
try { try {
const levels = Object.keys(LADDER_LEVEL_NAMES).map(level => ({ const levels = Object.keys(LADDER_LEVEL_NAMES).map((level) => ({
level: parseInt(level), level: parseInt(level),
name: LADDER_LEVEL_NAMES[level], name: LADDER_LEVEL_NAMES[level],
description: LADDER_LEVEL_DESC[level] description: LADDER_LEVEL_DESC[level],
})); }));
res.json(success({ res.json(
levels, success({
powerCalcRules: { levels,
baseWin: POWER_CALC.BASE_WIN, powerCalcRules: {
baseLose: POWER_CALC.BASE_LOSE, baseWin: POWER_CALC.BASE_WIN,
underdogThreshold: POWER_CALC.UNDERDOG_THRESHOLD, baseLose: POWER_CALC.BASE_LOSE,
underdogRate: POWER_CALC.UNDERDOG_RATE, underdogThreshold: POWER_CALC.UNDERDOG_THRESHOLD,
maxChange: POWER_CALC.MAX_CHANGE, underdogRate: POWER_CALC.UNDERDOG_RATE,
newbieProtection: POWER_CALC.NEWBIE_PROTECTION, maxChange: POWER_CALC.MAX_CHANGE,
minMonthlyMatches: POWER_CALC.MIN_MONTHLY_MATCHES, newbieProtection: POWER_CALC.NEWBIE_PROTECTION,
challengeCooldown: POWER_CALC.CHALLENGE_COOLDOWN minMonthlyMatches: POWER_CALC.MIN_MONTHLY_MATCHES,
} challengeCooldown: POWER_CALC.CHALLENGE_COOLDOWN,
})); },
}),
);
} catch (err) { } catch (err) {
console.error('获取等级信息失败:', err); console.error("获取等级信息失败:", err);
res.status(500).json(error('获取失败')); res.status(500).json(error("获取失败"));
} }
} }
} }

View File

@ -756,6 +756,90 @@ class MatchController {
} }
} }
// 获取选手比赛记录(用于选手详情页)
async getPlayerHistory(req, res) {
try {
const { player_id, page = 1, pageSize = 20 } = req.query;
const { limit, offset } = getPagination(page, pageSize);
if (!player_id) {
return res.status(400).json(error('缺少选手ID', 400));
}
const player = await LadderUser.findByPk(player_id);
if (!player || player.status !== 1) {
return res.json(pageResult([], 0, page, pageSize));
}
const { rows, count } = await MatchGame.findAndCountAll({
where: {
[Op.or]: [
{ player1_id: player.id },
{ player2_id: player.id }
],
confirm_status: CONFIRM_STATUS.CONFIRMED
},
include: [
{ model: Match, as: 'match', attributes: ['id', 'name', 'type', 'weight'] }
],
order: [['confirmed_at', 'DESC']],
limit,
offset
});
const opponentIds = [];
rows.forEach((g) => {
if (g.player1_id === player.id) opponentIds.push(g.player2_id);
else opponentIds.push(g.player1_id);
});
const uniqueOpponentIds = Array.from(new Set(opponentIds));
const opponents = await LadderUser.findAll({
where: { id: { [Op.in]: uniqueOpponentIds } },
include: [{ model: User, as: 'user', attributes: ['nickname', 'avatar'] }]
});
const opponentMap = new Map(opponents.map((o) => [String(o.id), o]));
const list = rows.map((game) => {
const isPlayer1 = game.player1_id === player.id;
const opponentId = isPlayer1 ? game.player2_id : game.player1_id;
const opponent = opponentMap.get(String(opponentId));
const myScore = isPlayer1 ? game.player1_score : game.player2_score;
const opponentScore = isPlayer1 ? game.player2_score : game.player1_score;
const isWin = game.winner_id === player.id;
const typeName = game.match && game.match.type === MATCH_TYPES.CHALLENGE ? '挑战赛' : '排位赛';
return {
id: game.id,
matchId: game.match_id,
name: (game.match && game.match.name) || typeName,
type: game.match && game.match.type,
typeName,
createTime: game.confirmed_at,
desc: opponent ? `vs ${opponent.real_name} ${myScore}:${opponentScore}` : `${myScore}:${opponentScore}`,
result: isWin ? 'win' : 'lose',
resultName: isWin ? '胜' : '负',
powerChange: isWin ? game.winner_power_change : game.loser_power_change,
opponent: opponent
? {
id: opponent.id,
realName: opponent.real_name,
nickname: opponent.user && opponent.user.nickname,
avatar: opponent.user && opponent.user.avatar,
level: opponent.level,
powerScore: opponent.power_score
}
: null
};
});
res.json(pageResult(list, count, page, pageSize));
} catch (err) {
console.error('获取选手比赛记录失败:', err);
res.status(500).json(error('获取失败'));
}
}
// 获取正在进行中的比赛 // 获取正在进行中的比赛
async getOngoingMatches(req, res) { async getOngoingMatches(req, res) {
try { try {

View File

@ -9,6 +9,9 @@ router.get('/ranking', ladderController.getRanking);
// 获取天梯用户详情 // 获取天梯用户详情
router.get('/user/:id', ladderController.getUserDetail); router.get('/user/:id', ladderController.getUserDetail);
// 选手详情(兼容小程序端:/api/ladder/player?id=xxx
router.get('/player', ladderController.getPlayerDetail);
// 获取等级说明 // 获取等级说明
router.get('/levels', ladderController.getLevelInfo); router.get('/levels', ladderController.getLevelInfo);

View File

@ -39,6 +39,9 @@ router.post('/ranking/confirm-score', authUser, matchController.confirmRankingSc
// 获取正在进行中的比赛 // 获取正在进行中的比赛
router.get('/ongoing', authUser, matchController.getOngoingMatches); router.get('/ongoing', authUser, matchController.getOngoingMatches);
// 获取选手比赛记录(用于选手详情页)
router.get('/history', authUser, matchController.getPlayerHistory);
// 获取我的比赛记录 // 获取我的比赛记录
router.get('/my-matches', authUser, matchController.getMyMatches); router.get('/my-matches', authUser, matchController.getMyMatches);