排行榜扫雷

This commit is contained in:
win 2026-04-20 15:53:49 +08:00
parent 21a174329c
commit eca0561cd9
9 changed files with 506 additions and 35 deletions

3
androidPrivacy.json Normal file
View File

@ -0,0 +1,3 @@
{
"prompt" : "template"
}

View File

@ -435,3 +435,10 @@ export function purchaseGamePass(package_id, count = 1, coupon_ids = []) {
export function bindDouyinID(douyin_id) {
return authRequest({ url: '/api/app/users/douyin/bind', method: 'POST', data: { douyin_id } })
}
/**
* 同步当前用户绑定的抖音订单
*/
export function syncMyDouyinOrders() {
return authRequest({ url: '/api/app/users/douyin/orders/sync', method: 'POST' })
}

View File

@ -121,9 +121,14 @@ export default {
}
})
const nakamaServer = res.nakama_server || 'wss://kdy.1024tool.vip/ws'
const nakamaServer = 'wss://game.1024tool.vip'
const gameBaseUrl = 'https://game.1024tool.vip'
const userInfo = uni.getStorageSync('user_info') || {}
const uid = userInfo.id || userInfo.user_id || ''
const nickname = encodeURIComponent(userInfo.nickname || userInfo.name || '')
const gameUrl = `${gameBaseUrl}/?game_token=${encodeURIComponent(res.game_token)}&nakama_server=${encodeURIComponent(nakamaServer)}&nakama_key=${encodeURIComponent(res.nakama_key)}&game_type=${encodeURIComponent(targetCode)}&uid=${encodeURIComponent(uid)}&nickname=${nickname}`
uni.navigateTo({
url: `/pages-game/game/minesweeper/play?game_token=${encodeURIComponent(res.game_token)}&nakama_server=${encodeURIComponent(nakamaServer)}&nakama_key=${encodeURIComponent(res.nakama_key)}`
url: `/pages-game/game/webview?url=${encodeURIComponent(gameUrl)}`
})
} catch (e) {
uni.showToast({
@ -140,14 +145,15 @@ export default {
const res = await authRequest({
url: '/api/app/games/enter',
method: 'POST',
data: {
game_code: this.gameCode
}
data: { game_code: this.gameCode }
})
const nakamaServer = res.nakama_server || 'wss://kdy.1024tool.vip/ws'
const nakamaServer = 'wss://kdy.1024tool.vip'
const gameBaseUrl = 'http://192.168.31.185:8082'
const userInfo = uni.getStorageSync('user_info') || {}
const uid = userInfo.id || userInfo.user_id || ''
const gameUrl = `${gameBaseUrl}/?game_token=${encodeURIComponent(res.game_token)}&nakama_server=${encodeURIComponent(nakamaServer)}&nakama_key=${encodeURIComponent(res.nakama_key)}&game_type=${encodeURIComponent(this.gameCode)}&uid=${encodeURIComponent(uid)}&scene=room-list`
uni.navigateTo({
url: `/pages-game/game/minesweeper/room-list?game_token=${encodeURIComponent(res.game_token)}&nakama_server=${encodeURIComponent(nakamaServer)}&nakama_key=${encodeURIComponent(res.nakama_key)}`
url: `/pages-game/game/webview?url=${encodeURIComponent(gameUrl)}`
})
} catch (e) {
uni.showToast({ title: '无法获取对战列表', icon: 'none' })

View File

@ -0,0 +1,424 @@
<template>
<view class="page">
<view class="bg-decoration"></view>
<!-- Tab 切换 -->
<view class="tab-bar glass-card">
<view
class="tab-item"
:class="{ active: gameType === 'minesweeper' }"
@tap="switchTab('minesweeper')"
>
<text class="tab-text">付费模式</text>
</view>
<view
class="tab-item"
:class="{ active: gameType === 'minesweeper_free' }"
@tap="switchTab('minesweeper_free')"
>
<text class="tab-text">免费模式</text>
</view>
</view>
<!-- 我的排名 -->
<view class="my-card glass-card" v-if="myRank">
<view class="my-left">
<text class="my-label">我的排名</text>
<text class="my-rank">{{ myRank.rank ? `#${myRank.rank}` : '未上榜' }}</text>
</view>
<view class="my-divider"></view>
<view class="my-stats">
<view class="stat-col">
<text class="stat-val">{{ myRank.wins || 0 }}</text>
<text class="stat-key">胜场</text>
</view>
<view class="stat-col">
<text class="stat-val">{{ myRank.matches_played || 0 }}</text>
<text class="stat-key">总场次</text>
</view>
<view class="stat-col">
<text class="stat-val">{{ formatWinRate(myRank.win_rate) }}</text>
<text class="stat-key">胜率</text>
</view>
</view>
<view class="my-divider"></view>
<view class="my-right">
<text class="my-pts">{{ myRank.total_rank_points || 0 }}</text>
<text class="my-pts-label">积分</text>
</view>
</view>
<!-- 榜单 -->
<scroll-view scroll-y class="list-wrap" @scrolltolower="loadMore">
<view v-if="loading && list.length === 0" class="state-box">
<text class="state-icon"></text>
<text class="state-text">加载中...</text>
</view>
<view v-else-if="!loading && list.length === 0" class="state-box">
<text class="state-icon">🏆</text>
<text class="state-text">暂无数据快来挑战吧</text>
</view>
<view v-else class="list">
<view
v-for="item in list"
:key="item.user_id"
class="rank-row glass-card"
:class="{ 'is-me': item.user_id === myUserId }"
>
<view class="rank-badge">
<text v-if="item.rank === 1" class="medal">🥇</text>
<text v-else-if="item.rank === 2" class="medal">🥈</text>
<text v-else-if="item.rank === 3" class="medal">🥉</text>
<text v-else class="rank-no">{{ item.rank }}</text>
</view>
<image class="avatar" :src="item.avatar || fallback" mode="aspectFill" />
<view class="item-info">
<view class="name-row">
<text class="item-name">{{ item.nickname || '匿名玩家' }}</text>
<text v-if="item.user_id === myUserId" class="me-tag"></text>
</view>
<text class="item-sub">{{ item.wins }} · {{ item.matches_played }} · {{ formatWinRate(item.win_rate) }}胜率</text>
</view>
<view class="pts-box">
<text class="pts-val">{{ item.total_rank_points }}</text>
<text class="pts-unit"></text>
</view>
</view>
<view v-if="loadingMore" class="footer-tip"><text class="footer-txt">加载中...</text></view>
<view v-if="!hasMore && list.length > 0" class="footer-tip"><text class="footer-txt"> 已显示全部 </text></view>
</view>
</scroll-view>
</view>
</template>
<script>
import { authRequest } from '../../../utils/request.js'
export default {
data() {
return {
gameType: 'minesweeper',
list: [],
myRank: null,
myUserId: null,
total: 0,
page: 1,
pageSize: 20,
loading: false,
loadingMore: false,
hasMore: true,
fallback: 'https://via.placeholder.com/80/FF6B00/FFFFFF?text=U',
}
},
onLoad() {
const info = uni.getStorageSync('user_info') || {}
this.myUserId = info.id || info.user_id || null
this.fetchList(true)
},
methods: {
switchTab(type) {
if (this.gameType === type) return
this.gameType = type
this.fetchList(true)
},
async fetchList(reset = false) {
if (reset) {
this.list = []
this.page = 1
this.hasMore = true
this.myRank = null
this.loading = true
} else {
if (!this.hasMore || this.loadingMore) return
this.loadingMore = true
}
try {
const res = await authRequest({
url: '/api/app/games/leaderboard',
method: 'GET',
data: { game_type: this.gameType, page: this.page, page_size: this.pageSize },
})
const items = res.list || []
this.list = reset ? items : [...this.list, ...items]
this.total = res.total || 0
this.myRank = res.me || null
this.hasMore = this.list.length < this.total
if (this.hasMore) this.page++
} catch {
uni.showToast({ title: '加载失败,请重试', icon: 'none' })
} finally {
this.loading = false
this.loadingMore = false
}
},
loadMore() { this.fetchList(false) },
formatWinRate(rate) {
if (rate == null) return '0%'
return `${(rate * 100).toFixed(1)}%`
},
},
}
</script>
<style lang="scss" scoped>
@import '@/uni.scss';
.page {
min-height: 100vh;
background: $bg-page;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
}
/* Tab */
.tab-bar {
position: relative;
z-index: 10;
display: flex;
margin: 24rpx 32rpx 20rpx;
padding: 8rpx;
border-radius: $radius-round;
}
.tab-item {
flex: 1;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: $radius-round;
transition: all $transition-normal $ease-out;
&.active {
background: $gradient-brand;
box-shadow: $shadow-warm;
.tab-text { color: #fff; font-weight: 700; }
}
}
.tab-text {
font-size: $font-md;
color: $text-sub;
font-weight: 600;
}
/* 我的排名 */
.my-card {
position: relative;
z-index: 5;
margin: 0 32rpx 24rpx;
padding: 28rpx 24rpx;
display: flex;
align-items: center;
gap: 0;
}
.my-left {
display: flex;
flex-direction: column;
align-items: center;
min-width: 100rpx;
}
.my-label {
font-size: $font-xs;
color: $text-sub;
margin-bottom: 8rpx;
}
.my-rank {
font-size: 38rpx;
font-weight: 900;
color: $brand-primary;
}
.my-divider {
width: 1px;
height: 60rpx;
background: $border-color-light;
margin: 0 20rpx;
}
.my-stats {
flex: 1;
display: flex;
align-items: center;
justify-content: space-around;
}
.stat-col {
display: flex;
flex-direction: column;
align-items: center;
gap: 4rpx;
}
.stat-val {
font-size: 30rpx;
font-weight: 800;
color: $text-main;
}
.stat-key {
font-size: $font-xs;
color: $text-sub;
}
.my-right {
display: flex;
flex-direction: column;
align-items: center;
min-width: 80rpx;
}
.my-pts {
font-size: 38rpx;
font-weight: 900;
color: $brand-primary;
line-height: 1;
}
.my-pts-label {
font-size: $font-xs;
color: $text-sub;
margin-top: 6rpx;
}
/* 列表 */
.list-wrap {
flex: 1;
padding: 0 32rpx;
}
.state-box {
display: flex;
flex-direction: column;
align-items: center;
padding: 120rpx 0;
gap: 20rpx;
}
.state-icon { font-size: 80rpx; }
.state-text { font-size: $font-md; color: $text-sub; }
.list {
display: flex;
flex-direction: column;
gap: 16rpx;
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
}
.rank-row {
display: flex;
align-items: center;
padding: 20rpx 24rpx;
gap: 20rpx;
&.is-me {
border: 2rpx solid rgba($brand-primary, 0.4);
background: linear-gradient(135deg, rgba($brand-primary, 0.06) 0%, $bg-glass 100%);
}
}
.rank-badge {
width: 52rpx;
height: 52rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.medal { font-size: 44rpx; }
.rank-no {
font-size: $font-md;
font-weight: 800;
color: $text-sub;
}
.avatar {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
background: $bg-secondary;
flex-shrink: 0;
border: 2rpx solid $border-color-light;
}
.item-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.name-row {
display: flex;
align-items: center;
gap: 12rpx;
}
.item-name {
font-size: $font-lg;
font-weight: 700;
color: $text-main;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 220rpx;
}
.me-tag {
font-size: $font-xs;
font-weight: 700;
color: #fff;
background: $gradient-brand;
padding: 2rpx 14rpx;
border-radius: $radius-round;
flex-shrink: 0;
}
.item-sub {
font-size: $font-sm;
color: $text-sub;
}
.pts-box {
display: flex;
align-items: baseline;
gap: 4rpx;
flex-shrink: 0;
}
.pts-val {
font-size: 36rpx;
font-weight: 900;
color: $brand-primary;
}
.pts-unit {
font-size: $font-sm;
color: $text-sub;
}
.footer-tip {
padding: 32rpx 0;
text-align: center;
}
.footer-txt {
font-size: $font-sm;
color: $text-tertiary;
}
</style>

View File

@ -1,7 +1,5 @@
<template>
<view class="container">
<web-view :src="url" @message="onMessage"></web-view>
</view>
<web-view v-if="url" :src="url" @message="onMessage"></web-view>
</template>
<script setup>
@ -21,33 +19,16 @@ onLoad((options) => {
})
function onMessage(e) {
console.log('Message from Game:', e.detail)
const data = e.detail.data || []
// Handle specific messages
data.forEach(msg => {
if (msg.action === 'close') {
uni.navigateBack()
} else if (msg.action === 'playAgain') {
// : token
console.log('PlayAgain: 返回游戏入口页面')
uni.navigateBack({
delta: 1,
success: () => {
// :
uni.$emit('refreshGame')
}
success: () => uni.$emit('refreshGame')
})
} else if (msg.action === 'game_over') {
// Optional: Refresh user balance or state
}
})
}
</script>
<style>
.container {
width: 100%;
height: 100vh;
}
</style>

View File

@ -223,6 +223,12 @@
"disableScroll": true
}
},
{
"path": "game/minesweeper/leaderboard",
"style": {
"navigationBarTitleText": "扫雷战绩榜"
}
},
{
"path": "game/webview",
"style": {

View File

@ -92,9 +92,9 @@
<image class="card-icon-small" src="https://via.placeholder.com/80/9370DB/000000?text=Mine" mode="aspectFit" />
</view>
<view class="game-card-small card-more" @tap="navigateTo('#')">
<text class="card-title-small">更多</text>
<text class="card-subtitle-small">敬请期待</text>
<view class="game-card-small card-more" @tap="openLeaderboard">
<text class="card-title-small">排行榜</text>
<text class="card-subtitle-small">扫雷战绩榜</text>
<image class="card-icon-small" src="https://via.placeholder.com/80/E0E0E0/000000?text=More" mode="aspectFit" />
</view>
</view>
@ -380,6 +380,9 @@ export default {
if(url === '#') return
uni.navigateTo({ url })
},
async openLeaderboard() {
uni.navigateTo({ url: '/pages-game/game/minesweeper/leaderboard' })
},
onNoticeTap() {
const content = this.displayNotices.map(n => n.text).join('\n')
uni.showModal({

View File

@ -178,6 +178,12 @@
</view>
<text class="menu-label">{{ douyinUserId ? '已绑定' : '绑定抖音' }}</text>
</view>
<view class="menu-item" @click="handleSyncDouyinOrders">
<view class="menu-icon-box">
<image class="menu-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0yMSAxMmE5IDkgMCAxIDEtMi42NC02LjM2Ii8+PHBvbHlsaW5lIHBvaW50cz0iMjEgMyAyMSA5IDE1IDkiLz48cGF0aCBkPSJNMyAxMmE5IDkgMCAxIDAgMi42NCA2LjM2Ii8+PHBvbHlsaW5lIHBvaW50cz0iMyAyMSAzIDE1IDkgMTUiLz48L3N2Zz4=" mode="aspectFit"></image>
</view>
<text class="menu-label">{{ douyinSyncing ? '同步中...' : '同步订单' }}</text>
</view>
<!-- #ifdef MP-TOUTIAO -->
<button
@ -494,7 +500,7 @@
<script>
import {
getUserInfo, getUserStats, getPointsBalance, getUserPoints, getUserCoupons, getItemCards,
getUserTasks, getTaskProgress, getInviteRecords, modifyUser, getUserProfile, bindDouyinID, getPublicConfig
getUserTasks, getTaskProgress, getInviteRecords, modifyUser, getUserProfile, bindDouyinID, syncMyDouyinOrders, getPublicConfig
} from '../../api/appUser.js'
// #ifdef MP-TOUTIAO
import customTabBarToutiao from '@/components/app-tab-bar-toutiao.vue'
@ -519,6 +525,7 @@ export default {
mobile: '', //
douyinUserId: '', // ID
douyinSyncing: false,
customerServiceQrCode: '', //
customerServiceId: '0071112x', // IM
pointsBalance: 0,
@ -1029,6 +1036,40 @@ export default {
}
})
},
async handleSyncDouyinOrders() {
if (!this.checkPhoneBound()) return
if (this.douyinSyncing) return
if (!this.douyinUserId) {
uni.showToast({
title: '请先绑定抖音号',
icon: 'none'
})
return
}
this.douyinSyncing = true
try {
uni.showLoading({ title: '同步中...' })
const data = await syncMyDouyinOrders()
await this.loadUserInfo()
uni.hideLoading()
uni.showModal({
title: '同步完成',
content: `抖音号:${data.douyin_user_id || this.douyinUserId}\n抓取订单${data.total_fetched || 0}\n新增订单${data.new_orders || 0}`,
showCancel: false
})
} catch (err) {
uni.hideLoading()
uni.showToast({
title: err.message || '同步失败',
icon: 'none'
})
} finally {
this.douyinSyncing = false
}
},
toMinesweeper() {
if (!this.checkPhoneBound()) return
uni.navigateTo({ url: '/pages-game/game/minesweeper/index' })

View File

@ -1,5 +1,5 @@
const BASE_URL = 'http://127.0.0.1:9991'
// const BASE_URL = 'https://kdy.1024tool.vip'
// const BASE_URL = 'http://127.0.0.1:9991'
const BASE_URL = 'https://kdy.1024tool.vip'
let authModalShown = false
function handleAuthExpired() {