Compare commits

..

No commits in common. "75760d25fdf34661948c229b5f107a5effdfdc53" and "e0713c3fd8dc05300753ef52f68a44e7a7a09049" have entirely different histories.

36 changed files with 822 additions and 1621 deletions

View File

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

View File

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

View File

@ -26,19 +26,6 @@ page {
--accent-light: #e6fbf7;
--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-white: #ffffff;

View File

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

View File

@ -1,5 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 282 B

View File

@ -1,4 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 282 B

View File

@ -1,4 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 204 B

View File

@ -1,6 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 334 B

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,68 +0,0 @@
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

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

View File

@ -1,58 +0,0 @@
<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

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

View File

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

View File

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

View File

@ -47,7 +47,7 @@ module.exports = { formatDistance: formatDistance };
<view class="store-section">
<view class="section-header">
<view class="section-title">
<image class="section-icon" src="/images/icon-store.svg" mode="aspectFit"></image>
<text class="section-icon">📍</text>
<text class="section-text">附近门店</text>
</view>
<text class="section-count">共 {{stores.length}} 家</text>
@ -70,7 +70,7 @@ module.exports = { formatDistance: formatDistance };
<text class="store-item-address">{{item.address}}</text>
<view class="store-item-meta">
<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 File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -756,90 +756,6 @@ 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) {
try {

View File

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

View File

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