fix(tongji): 恢复游戏进度并完善周榜
This commit is contained in:
@@ -22,3 +22,5 @@ unpackage/dist
|
|||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
TUICallKit
|
TUICallKit
|
||||||
|
# 本工程的通话源码复用仓库内 ../wx/TUICallKit;保留相对软链接以便干净克隆后可直接构建。
|
||||||
|
!TUICallKit
|
||||||
|
|||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../wx/TUICallKit
|
||||||
@@ -39,6 +39,7 @@ export function useGamePlatform(proxy, ensureLoggedIn) {
|
|||||||
const connected = ref(false)
|
const connected = ref(false)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const syncStatus = ref('idle')
|
const syncStatus = ref('idle')
|
||||||
|
const lastError = ref('')
|
||||||
const leaderboard = ref({ ...EMPTY_BOARD })
|
const leaderboard = ref({ ...EMPTY_BOARD })
|
||||||
const confirmedSessionLearned = ref(0)
|
const confirmedSessionLearned = ref(0)
|
||||||
|
|
||||||
@@ -73,17 +74,19 @@ export function useGamePlatform(proxy, ensureLoggedIn) {
|
|||||||
}
|
}
|
||||||
connected.value = true
|
connected.value = true
|
||||||
syncStatus.value = 'synced'
|
syncStatus.value = 'synced'
|
||||||
|
lastError.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshLeaderboard() {
|
async function refreshLeaderboard() {
|
||||||
const res = await api({
|
const res = await api({
|
||||||
url: '/api/tcm/gameWeeklyLeaderboard',
|
url: '/api/tcm/gameWeeklyLeaderboard',
|
||||||
method: 'GET'
|
method: 'GET',
|
||||||
|
data: { session_key: sessionKey }
|
||||||
})
|
})
|
||||||
if (res?.code !== 1 || !res.data) {
|
if (res?.code !== 1 || !res.data) {
|
||||||
throw new Error(res?.msg || '同行榜加载失败')
|
throw new Error(res?.msg || '同行榜加载失败')
|
||||||
}
|
}
|
||||||
applyLeaderboard(res.data)
|
applyLeaderboard(res.data, sessionKey)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,9 +115,10 @@ export function useGamePlatform(proxy, ensureLoggedIn) {
|
|||||||
await refreshLeaderboard()
|
await refreshLeaderboard()
|
||||||
if (queuedPayloads.length) flushProgress()
|
if (queuedPayloads.length) flushProgress()
|
||||||
return true
|
return true
|
||||||
} catch (_) {
|
} catch (error) {
|
||||||
connected.value = false
|
connected.value = false
|
||||||
syncStatus.value = 'offline'
|
syncStatus.value = 'offline'
|
||||||
|
lastError.value = String(error?.message || '平台连接失败,请点击重试')
|
||||||
return false
|
return false
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
@@ -124,14 +128,21 @@ export function useGamePlatform(proxy, ensureLoggedIn) {
|
|||||||
return connectPromise
|
return connectPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
function beginSession() {
|
function beginSession(existingSessionKey = '') {
|
||||||
if (syncTimer) clearTimeout(syncTimer)
|
if (syncTimer) clearTimeout(syncTimer)
|
||||||
sessionKey = createSessionKey()
|
const restoredKey = String(existingSessionKey || '').trim()
|
||||||
|
sessionKey = /^[A-Za-z0-9_-]{16,64}$/.test(restoredKey)
|
||||||
|
? restoredKey
|
||||||
|
: createSessionKey()
|
||||||
confirmedSessionLearned.value = 0
|
confirmedSessionLearned.value = 0
|
||||||
syncStatus.value = queuedPayloads.length ? 'pending' : (connected.value ? 'synced' : 'offline')
|
syncStatus.value = queuedPayloads.length ? 'pending' : (connected.value ? 'synced' : 'offline')
|
||||||
return sessionKey
|
return sessionKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSessionKey() {
|
||||||
|
return sessionKey
|
||||||
|
}
|
||||||
|
|
||||||
function queueProgress(learnedCount, score, ended = false) {
|
function queueProgress(learnedCount, score, ended = false) {
|
||||||
const nextPayload = {
|
const nextPayload = {
|
||||||
session_key: sessionKey,
|
session_key: sessionKey,
|
||||||
@@ -191,9 +202,10 @@ export function useGamePlatform(proxy, ensureLoggedIn) {
|
|||||||
|| Number(item.ended) > Number(payload.ended)
|
|| Number(item.ended) > Number(payload.ended)
|
||||||
))
|
))
|
||||||
persistPendingPayloads()
|
persistPendingPayloads()
|
||||||
} catch (_) {
|
} catch (error) {
|
||||||
connected.value = false
|
connected.value = false
|
||||||
syncStatus.value = 'offline'
|
syncStatus.value = 'offline'
|
||||||
|
lastError.value = String(error?.message || '成绩暂未保存,联网后会自动重试')
|
||||||
// 队首保留原绝对值,下次连接或打开榜单时安全重试。
|
// 队首保留原绝对值,下次连接或打开榜单时安全重试。
|
||||||
persistPendingPayloads()
|
persistPendingPayloads()
|
||||||
} finally {
|
} finally {
|
||||||
@@ -237,10 +249,12 @@ export function useGamePlatform(proxy, ensureLoggedIn) {
|
|||||||
connected,
|
connected,
|
||||||
loading,
|
loading,
|
||||||
syncStatus,
|
syncStatus,
|
||||||
|
lastError,
|
||||||
leaderboard,
|
leaderboard,
|
||||||
confirmedSessionLearned,
|
confirmedSessionLearned,
|
||||||
connect,
|
connect,
|
||||||
beginSession,
|
beginSession,
|
||||||
|
getSessionKey,
|
||||||
queueProgress,
|
queueProgress,
|
||||||
flushProgress,
|
flushProgress,
|
||||||
syncAndRefresh,
|
syncAndRefresh,
|
||||||
|
|||||||
@@ -337,6 +337,9 @@
|
|||||||
.eg-weekly-list { display: flex; flex-direction: column; gap: 7rpx; margin-top: 15rpx; }
|
.eg-weekly-list { display: flex; flex-direction: column; gap: 7rpx; margin-top: 15rpx; }
|
||||||
.eg-weekly-row { display: flex; min-height: 68rpx; align-items: center; padding: 7rpx 14rpx; border: 2rpx solid transparent; border-radius: 19rpx; color: #345c50; background: rgba(255,255,255,.72); }
|
.eg-weekly-row { display: flex; min-height: 68rpx; align-items: center; padding: 7rpx 14rpx; border: 2rpx solid transparent; border-radius: 19rpx; color: #345c50; background: rgba(255,255,255,.72); }
|
||||||
.eg-weekly-row.is-me { border-color: #f2bd48; color: #174f3e; background: linear-gradient(90deg, #fff2bb, #fff9e3); box-shadow: 0 5rpx 14rpx rgba(153,102,17,.12); transform: scale(1.015); }
|
.eg-weekly-row.is-me { border-color: #f2bd48; color: #174f3e; background: linear-gradient(90deg, #fff2bb, #fff9e3); box-shadow: 0 5rpx 14rpx rgba(153,102,17,.12); transform: scale(1.015); }
|
||||||
|
.eg-weekly-row.is-waiting { color: #8a9b94; border-style: dashed; border-color: #dbe8e2; background: rgba(247,251,249,.72); }
|
||||||
|
.eg-weekly-row.is-waiting .eg-weekly-avatar { color: #6e887e; border-color: #dce9e3; background: #edf4f1; box-shadow: none; }
|
||||||
|
.eg-weekly-row.is-waiting .eg-weekly-count { color: #9baba4; }
|
||||||
.eg-weekly-place { width: 43rpx; color: #6b7d76; font-size: 27rpx; font-weight: 1000; text-align: center; }
|
.eg-weekly-place { width: 43rpx; color: #6b7d76; font-size: 27rpx; font-weight: 1000; text-align: center; }
|
||||||
.eg-weekly-row:nth-child(1) .eg-weekly-place { color: #dd7b17; font-size: 31rpx; }
|
.eg-weekly-row:nth-child(1) .eg-weekly-place { color: #dd7b17; font-size: 31rpx; }
|
||||||
.eg-weekly-avatar { display: flex; width: 52rpx; height: 52rpx; flex: 0 0 52rpx; align-items: center; justify-content: center; margin-left: 6rpx; border: 3rpx solid rgba(255,255,255,.9); border-radius: 50%; color: #fff; background: #5d9b84; box-shadow: 0 3rpx 9rpx rgba(30,78,61,.16); font-size: 24rpx; font-weight: 1000; }
|
.eg-weekly-avatar { display: flex; width: 52rpx; height: 52rpx; flex: 0 0 52rpx; align-items: center; justify-content: center; margin-left: 6rpx; border: 3rpx solid rgba(255,255,255,.9); border-radius: 50%; color: #fff; background: #5d9b84; box-shadow: 0 3rpx 9rpx rgba(30,78,61,.16); font-size: 24rpx; font-weight: 1000; }
|
||||||
@@ -350,6 +353,9 @@
|
|||||||
.eg-weekly-name { flex: 1; margin-left: 13rpx; overflow: hidden; font-size: 27rpx; font-weight: 850; text-overflow: ellipsis; white-space: nowrap; }
|
.eg-weekly-name { flex: 1; margin-left: 13rpx; overflow: hidden; font-size: 27rpx; font-weight: 850; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.eg-weekly-me { margin-right: 8rpx; padding: 3rpx 9rpx; border-radius: 999rpx; color: #fff; background: #dd6b27; font-size: 18rpx; font-weight: 900; }
|
.eg-weekly-me { margin-right: 8rpx; padding: 3rpx 9rpx; border-radius: 999rpx; color: #fff; background: #dd6b27; font-size: 18rpx; font-weight: 900; }
|
||||||
.eg-weekly-count { min-width: 70rpx; color: #315b4d; font-size: 29rpx; font-weight: 1000; text-align: right; }
|
.eg-weekly-count { min-width: 70rpx; color: #315b4d; font-size: 29rpx; font-weight: 1000; text-align: right; }
|
||||||
|
.eg-weekly-connection { display: flex; align-items: center; justify-content: space-between; gap: 12rpx; margin-top: 13rpx; padding: 12rpx 15rpx; border: 2rpx solid #e4d7b2; border-radius: 18rpx; color: #6b6655; background: #fff9e8; font-size: 21rpx; font-weight: 700; }
|
||||||
|
.eg-weekly-connection > text:first-child { flex: 1; }
|
||||||
|
.eg-weekly-retry { flex-shrink: 0; padding: 6rpx 12rpx; border-radius: 999rpx; color: #fff; background: #23765c; font-weight: 900; }
|
||||||
.eg-cheer-card { margin-top: 16rpx; padding: 15rpx 17rpx 14rpx; border: 3rpx solid #f0d38d; border-radius: 23rpx; background: linear-gradient(135deg, #fff9d9, #fff1c2); }
|
.eg-cheer-card { margin-top: 16rpx; padding: 15rpx 17rpx 14rpx; border: 3rpx solid #f0d38d; border-radius: 23rpx; background: linear-gradient(135deg, #fff9d9, #fff1c2); }
|
||||||
.eg-cheer-card.is-family { border-color: #d9c8ef; background: linear-gradient(135deg, #f8f2ff, #efe7ff); }
|
.eg-cheer-card.is-family { border-color: #d9c8ef; background: linear-gradient(135deg, #f8f2ff, #efe7ff); }
|
||||||
.eg-cheer-card.is-bright { border-color: #f4c77b; background: linear-gradient(135deg, #fff8dc, #ffeec7); }
|
.eg-cheer-card.is-bright { border-color: #f4c77b; background: linear-gradient(135deg, #fff8dc, #ffeec7); }
|
||||||
|
|||||||
@@ -224,7 +224,7 @@
|
|||||||
v-for="player in weeklyRankRows"
|
v-for="player in weeklyRankRows"
|
||||||
:key="player.id"
|
:key="player.id"
|
||||||
class="eg-weekly-row"
|
class="eg-weekly-row"
|
||||||
:class="{ 'is-me': player.isMe }"
|
:class="{ 'is-me': player.isMe, 'is-waiting': player.isWaiting }"
|
||||||
>
|
>
|
||||||
<text class="eg-weekly-place">{{ player.rank }}</text>
|
<text class="eg-weekly-place">{{ player.rank }}</text>
|
||||||
<view class="eg-weekly-avatar" :class="`is-tone-${player.tone}`">
|
<view class="eg-weekly-avatar" :class="`is-tone-${player.tone}`">
|
||||||
@@ -237,6 +237,11 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view v-if="!platformConnected" class="eg-weekly-connection" @tap.stop="retryPlatformConnection">
|
||||||
|
<text>{{ platformConnectionHint }}</text>
|
||||||
|
<text class="eg-weekly-retry">点击重试</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view class="eg-cheer-card" :class="`is-${currentCheer.tone}`">
|
<view class="eg-cheer-card" :class="`is-${currentCheer.tone}`">
|
||||||
<view class="eg-cheer-top">
|
<view class="eg-cheer-top">
|
||||||
<text class="eg-cheer-source">{{ currentCheer.source }}</text>
|
<text class="eg-cheer-source">{{ currentCheer.source }}</text>
|
||||||
@@ -246,7 +251,7 @@
|
|||||||
<text v-if="currentCheer.userGenerated" class="eg-cheer-note">亲友自定义内容,不作为医学判断</text>
|
<text v-if="currentCheer.userGenerated" class="eg-cheer-note">亲友自定义内容,不作为医学判断</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="eg-weekly-start" @tap="closeWeeklyRank">开始消除</view>
|
<view class="eg-weekly-start" @tap="closeWeeklyRank">{{ restoredGame ? '继续上次游戏' : '开始消除' }}</view>
|
||||||
<button class="eg-weekly-share" open-type="share">邀请亲友一起认糖</button>
|
<button class="eg-weekly-share" open-type="share">邀请亲友一起认糖</button>
|
||||||
<text class="eg-weekly-footnote">使用平台昵称参加真实周榜,每周一重新分组;分享不会自动绑定家庭关系</text>
|
<text class="eg-weekly-footnote">使用平台昵称参加真实周榜,每周一重新分组;分享不会自动绑定家庭关系</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -300,7 +305,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, getCurrentInstance, ref } from 'vue'
|
import { computed, getCurrentInstance, ref } from 'vue'
|
||||||
import { onLoad, onShareAppMessage, onShareTimeline, onShow, onUnload } from '@dcloudio/uni-app'
|
import { onHide, onLoad, onShareAppMessage, onShareTimeline, onShow, onUnload } from '@dcloudio/uni-app'
|
||||||
import TongjiIcon from '../components/TongjiIcon.vue'
|
import TongjiIcon from '../components/TongjiIcon.vue'
|
||||||
import { useGameSfx } from './composables/useGameSfx.js'
|
import { useGameSfx } from './composables/useGameSfx.js'
|
||||||
import { useGamePlatform } from './composables/useGamePlatform.js'
|
import { useGamePlatform } from './composables/useGamePlatform.js'
|
||||||
@@ -319,6 +324,8 @@ const COMBO_TASK_WAVE = 2
|
|||||||
const COMBO_TASK_TARGET = 2
|
const COMBO_TASK_TARGET = 2
|
||||||
const COMBO_PITY_TURNS = 6
|
const COMBO_PITY_TURNS = 6
|
||||||
const MEGA_COMBO_SCORE = 300
|
const MEGA_COMBO_SCORE = 300
|
||||||
|
const ACTIVE_GAME_KEY = 'tongji_endless_active_game_v1'
|
||||||
|
const ACTIVE_GAME_VERSION = 1
|
||||||
const DANGER_DROP_CHANCE = 0.05 + GAME_DIFFICULTY * 0.01
|
const DANGER_DROP_CHANCE = 0.05 + GAME_DIFFICULTY * 0.01
|
||||||
const COLUMN_GROUP_BIAS = 0.86 - GAME_DIFFICULTY * 0.028
|
const COLUMN_GROUP_BIAS = 0.86 - GAME_DIFFICULTY * 0.028
|
||||||
const CHEER_MESSAGES = [
|
const CHEER_MESSAGES = [
|
||||||
@@ -399,10 +406,12 @@ const {
|
|||||||
connected: platformConnected,
|
connected: platformConnected,
|
||||||
loading: platformLoading,
|
loading: platformLoading,
|
||||||
syncStatus: platformSyncStatus,
|
syncStatus: platformSyncStatus,
|
||||||
|
lastError: platformLastError,
|
||||||
leaderboard: platformLeaderboard,
|
leaderboard: platformLeaderboard,
|
||||||
confirmedSessionLearned,
|
confirmedSessionLearned,
|
||||||
connect: connectPlatform,
|
connect: connectPlatform,
|
||||||
beginSession: beginPlatformSession,
|
beginSession: beginPlatformSession,
|
||||||
|
getSessionKey: getPlatformSessionKey,
|
||||||
queueProgress: queuePlatformProgress,
|
queueProgress: queuePlatformProgress,
|
||||||
flushProgress: flushPlatformProgress,
|
flushProgress: flushPlatformProgress,
|
||||||
syncAndRefresh: syncAndRefreshPlatform,
|
syncAndRefresh: syncAndRefreshPlatform,
|
||||||
@@ -447,6 +456,7 @@ const reshuffleDone = ref(false)
|
|||||||
const reshuffling = ref(false)
|
const reshuffling = ref(false)
|
||||||
const confettiVisible = ref(false)
|
const confettiVisible = ref(false)
|
||||||
const weeklyRankVisible = ref(false)
|
const weeklyRankVisible = ref(false)
|
||||||
|
const restoredGame = ref(false)
|
||||||
const cheerIndex = ref(0)
|
const cheerIndex = ref(0)
|
||||||
const CONFETTI_COLORS = ['#ff3154', '#ffd928', '#28c76f', '#4d8dff', '#a855f7', '#ff7a1a']
|
const CONFETTI_COLORS = ['#ff3154', '#ffd928', '#28c76f', '#4d8dff', '#a855f7', '#ff7a1a']
|
||||||
const confettiPieces = Array.from({ length: 34 }, (_, index) => ({
|
const confettiPieces = Array.from({ length: 34 }, (_, index) => ({
|
||||||
@@ -515,9 +525,24 @@ const weeklyRankRows = computed(() => {
|
|||||||
tone: 1,
|
tone: 1,
|
||||||
isMe: true
|
isMe: true
|
||||||
}]
|
}]
|
||||||
return source
|
const ranked = source
|
||||||
.sort((a, b) => b.count - a.count || Number(b.best_score || 0) - Number(a.best_score || 0))
|
.sort((a, b) => b.count - a.count || Number(b.best_score || 0) - Number(a.best_score || 0))
|
||||||
.map((player, index) => ({ ...player, rank: index + 1 }))
|
.map((player, index) => ({ ...player, rank: index + 1 }))
|
||||||
|
while (ranked.length < 7) {
|
||||||
|
const rank = ranked.length + 1
|
||||||
|
ranked.push({
|
||||||
|
id: `waiting-${rank}`,
|
||||||
|
rank,
|
||||||
|
name: '等待同行加入',
|
||||||
|
avatar: '+',
|
||||||
|
avatarUrl: '',
|
||||||
|
count: '—',
|
||||||
|
tone: rank,
|
||||||
|
isMe: false,
|
||||||
|
isWaiting: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return ranked.slice(0, 7)
|
||||||
})
|
})
|
||||||
const weeklyMyRank = computed(() => weeklyRankRows.value.find(player => player.isMe)?.rank || 1)
|
const weeklyMyRank = computed(() => weeklyRankRows.value.find(player => player.isMe)?.rank || 1)
|
||||||
const weeklyRankRise = computed(() => 0)
|
const weeklyRankRise = computed(() => 0)
|
||||||
@@ -535,13 +560,24 @@ const weeklyRangeLabel = computed(() => {
|
|||||||
return `${formatWeekDate(start)}—${formatWeekDate(end)}`
|
return `${formatWeekDate(start)}—${formatWeekDate(end)}`
|
||||||
})
|
})
|
||||||
const weeklySexLabel = computed(() => platformLeaderboard.value.sex_label || '同行')
|
const weeklySexLabel = computed(() => platformLeaderboard.value.sex_label || '同行')
|
||||||
const weeklyMemberCount = computed(() => Math.max(1, Number(platformLeaderboard.value.member_count || weeklyRankRows.value.length)))
|
const weeklyMemberCount = computed(() => {
|
||||||
|
const reported = Number(platformLeaderboard.value.member_count || 0)
|
||||||
|
const remoteCount = Array.isArray(platformLeaderboard.value.players)
|
||||||
|
? platformLeaderboard.value.players.length
|
||||||
|
: 0
|
||||||
|
return Math.max(1, Math.min(7, reported || remoteCount || 1))
|
||||||
|
})
|
||||||
const weeklyStatusLabel = computed(() => {
|
const weeklyStatusLabel = computed(() => {
|
||||||
if (platformLoading.value) return '连接中'
|
if (platformLoading.value) return '连接中'
|
||||||
if (!platformConnected.value) return '离线记录中'
|
if (!platformConnected.value) return '离线记录中'
|
||||||
if (platformSyncStatus.value === 'pending' || platformSyncStatus.value === 'syncing') return '保存中'
|
if (platformSyncStatus.value === 'pending' || platformSyncStatus.value === 'syncing') return '保存中'
|
||||||
return '真实周榜'
|
return '真实周榜'
|
||||||
})
|
})
|
||||||
|
const platformConnectionHint = computed(() => (
|
||||||
|
platformLoading.value
|
||||||
|
? '正在连接甄养堂账号和本周同行榜'
|
||||||
|
: (platformLastError.value || '当前成绩暂存在本机,联网登录后会自动补传')
|
||||||
|
))
|
||||||
|
|
||||||
onShareAppMessage(() => {
|
onShareAppMessage(() => {
|
||||||
recordPlatformShare()
|
recordPlatformShare()
|
||||||
@@ -592,7 +628,7 @@ onLoad((options = {}) => {
|
|||||||
initNavLayout()
|
initNavLayout()
|
||||||
bestScore.value = Number(uni.getStorageSync('tongji_endless_best') || 0)
|
bestScore.value = Number(uni.getStorageSync('tongji_endless_best') || 0)
|
||||||
incomingInviteCode = String(options.invite_code || '').trim()
|
incomingInviteCode = String(options.invite_code || '').trim()
|
||||||
startGame()
|
if (!restoreActiveGame()) startGame()
|
||||||
weeklyRankVisible.value = true
|
weeklyRankVisible.value = true
|
||||||
connectPlatform(incomingInviteCode)
|
connectPlatform(incomingInviteCode)
|
||||||
})
|
})
|
||||||
@@ -603,7 +639,12 @@ onShow(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onHide(() => {
|
||||||
|
saveActiveGame()
|
||||||
|
})
|
||||||
|
|
||||||
onUnload(() => {
|
onUnload(() => {
|
||||||
|
saveActiveGame()
|
||||||
if (learned.value > 0 || gameOver.value) {
|
if (learned.value > 0 || gameOver.value) {
|
||||||
queuePlatformProgress(learned.value, score.value, gameOver.value)
|
queuePlatformProgress(learned.value, score.value, gameOver.value)
|
||||||
flushPlatformProgress()
|
flushPlatformProgress()
|
||||||
@@ -679,8 +720,117 @@ function cycleCheer() {
|
|||||||
playSfx('select', .68)
|
playSfx('select', .68)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function retryPlatformConnection() {
|
||||||
|
if (platformLoading.value) return
|
||||||
|
const connected = await connectPlatform(incomingInviteCode)
|
||||||
|
if (connected) {
|
||||||
|
await syncAndRefreshPlatform(learned.value, score.value)
|
||||||
|
uni.showToast({ title: '周榜已更新', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showToast({ title: platformConnectionHint.value, icon: 'none' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentWeekStartKey(now = new Date()) {
|
||||||
|
const date = new Date(now)
|
||||||
|
const day = date.getDay() || 7
|
||||||
|
date.setHours(0, 0, 0, 0)
|
||||||
|
date.setDate(date.getDate() - day + 1)
|
||||||
|
const pad = value => String(value).padStart(2, '0')
|
||||||
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearActiveGame() {
|
||||||
|
try { uni.removeStorageSync(ACTIVE_GAME_KEY) } catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveActiveGame() {
|
||||||
|
if (gameOver.value) {
|
||||||
|
clearActiveGame()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (busy.value || board.value.length !== ROWS * COLS) return false
|
||||||
|
const stableBoard = board.value.every(tile => tile && FOODS[tile.key])
|
||||||
|
if (!stableBoard) return false
|
||||||
|
try {
|
||||||
|
uni.setStorageSync(ACTIVE_GAME_KEY, {
|
||||||
|
version: ACTIVE_GAME_VERSION,
|
||||||
|
week_start: currentWeekStartKey(),
|
||||||
|
session_key: getPlatformSessionKey(),
|
||||||
|
confirmed_session_learned: Math.max(0, Number(confirmedSessionLearned.value) || 0),
|
||||||
|
saved_at: Date.now(),
|
||||||
|
theme_index: themeIndex.value,
|
||||||
|
board: board.value.map(tile => ({ id: tile.id, key: tile.key, special: tile.special || '' })),
|
||||||
|
score: score.value,
|
||||||
|
learned: learned.value,
|
||||||
|
combo: combo.value,
|
||||||
|
feast_moves: feastMoves.value,
|
||||||
|
camel_count: camelCount.value,
|
||||||
|
task_index: taskIndex.value,
|
||||||
|
task_progress: taskProgress.value,
|
||||||
|
combo_dry_turns: comboDryTurns.value
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
} catch (_) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreActiveGame() {
|
||||||
|
let saved
|
||||||
|
try { saved = uni.getStorageSync(ACTIVE_GAME_KEY) } catch (_) { return false }
|
||||||
|
const validBoard = Array.isArray(saved?.board)
|
||||||
|
&& saved.board.length === ROWS * COLS
|
||||||
|
&& saved.board.every(tile => tile && FOODS[tile.key])
|
||||||
|
if (saved?.version !== ACTIVE_GAME_VERSION || saved?.week_start !== currentWeekStartKey() || !validBoard) {
|
||||||
|
clearActiveGame()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
themeIndex.value = Math.max(0, Math.min(FOOD_GROUPS.length - 1, Number(saved.theme_index) || 0))
|
||||||
|
board.value = saved.board.map(tile => ({
|
||||||
|
id: Math.max(1, Number(tile.id) || tileId++),
|
||||||
|
key: tile.key,
|
||||||
|
special: ['row', 'col', 'all', 'burst'].includes(tile.special) ? tile.special : ''
|
||||||
|
}))
|
||||||
|
tileId = Math.max(tileId, ...board.value.map(tile => tile.id + 1))
|
||||||
|
score.value = Math.max(0, Number(saved.score) || 0)
|
||||||
|
learned.value = Math.max(0, Number(saved.learned) || 0)
|
||||||
|
combo.value = Math.max(0, Number(saved.combo) || 0)
|
||||||
|
feastMoves.value = Math.max(0, Number(saved.feast_moves) || 0)
|
||||||
|
camelCount.value = Math.max(0, Math.min(MAX_CAMEL_COUNT, Number(saved.camel_count) || 0))
|
||||||
|
taskIndex.value = Math.max(0, Number(saved.task_index) || 0) % 4
|
||||||
|
taskProgress.value = Math.max(0, Number(saved.task_progress) || 0)
|
||||||
|
comboDryTurns.value = Math.max(0, Number(saved.combo_dry_turns) || 0)
|
||||||
|
beginPlatformSession(saved.session_key)
|
||||||
|
confirmedSessionLearned.value = Math.max(0, Number(saved.confirmed_session_learned) || 0)
|
||||||
|
selected.value = -1
|
||||||
|
camelMode.value = false
|
||||||
|
busy.value = false
|
||||||
|
gameOver.value = false
|
||||||
|
taskRewardVisible.value = false
|
||||||
|
clearingIndices.value = []
|
||||||
|
fireClearingIndices.value = []
|
||||||
|
impactText.value = ''
|
||||||
|
camelJackpot.value.visible = false
|
||||||
|
stageVisible.value = false
|
||||||
|
reshuffleVisible.value = false
|
||||||
|
reshuffleDone.value = false
|
||||||
|
reshuffling.value = false
|
||||||
|
confettiVisible.value = false
|
||||||
|
pendingThemeMatchCount = 0
|
||||||
|
taskRewardStatusUntil = 0
|
||||||
|
endedFood.value = foodOf(currentTheme.value.danger)
|
||||||
|
restoredGame.value = true
|
||||||
|
resetDrag()
|
||||||
|
statusText.value = '已恢复上次未完成的游戏和得分'
|
||||||
|
scheduleHint()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
function startGame() {
|
function startGame() {
|
||||||
if (hintTimer) clearTimeout(hintTimer)
|
if (hintTimer) clearTimeout(hintTimer)
|
||||||
|
clearActiveGame()
|
||||||
beginPlatformSession()
|
beginPlatformSession()
|
||||||
themeIndex.value = 0
|
themeIndex.value = 0
|
||||||
board.value = buildPlayableBoard(openingKeysForTheme(currentTheme.value))
|
board.value = buildPlayableBoard(openingKeysForTheme(currentTheme.value))
|
||||||
@@ -708,8 +858,10 @@ function startGame() {
|
|||||||
confettiVisible.value = false
|
confettiVisible.value = false
|
||||||
pendingThemeMatchCount = 0
|
pendingThemeMatchCount = 0
|
||||||
taskRewardStatusUntil = 0
|
taskRewardStatusUntil = 0
|
||||||
|
restoredGame.value = false
|
||||||
resetDrag()
|
resetDrag()
|
||||||
statusText.value = '交换相邻食物,三个相同即可消除'
|
statusText.value = '交换相邻食物,三个相同即可消除'
|
||||||
|
saveActiveGame()
|
||||||
scheduleHint()
|
scheduleHint()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -863,6 +1015,7 @@ async function tapTile(index) {
|
|||||||
finishTurn()
|
finishTurn()
|
||||||
} else {
|
} else {
|
||||||
statusText.value = `已将${foodOf(currentTheme.value.danger).short}移出餐盘,凑齐三个驼乳可换主题`
|
statusText.value = `已将${foodOf(currentTheme.value.danger).short}移出餐盘,凑齐三个驼乳可换主题`
|
||||||
|
saveActiveGame()
|
||||||
if (!checkBoardHealth()) scheduleHint()
|
if (!checkBoardHealth()) scheduleHint()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -928,6 +1081,7 @@ async function trySwap(a, b, soundPlayed = false) {
|
|||||||
endedFood.value = foodOf(highTile.key)
|
endedFood.value = foodOf(highTile.key)
|
||||||
saveBest()
|
saveBest()
|
||||||
gameOver.value = true
|
gameOver.value = true
|
||||||
|
clearActiveGame()
|
||||||
queuePlatformProgress(learned.value, score.value, true)
|
queuePlatformProgress(learned.value, score.value, true)
|
||||||
busy.value = false
|
busy.value = false
|
||||||
return
|
return
|
||||||
@@ -1422,6 +1576,7 @@ function showTaskCamelReward() {
|
|||||||
|
|
||||||
function finishTurn() {
|
function finishTurn() {
|
||||||
saveBest()
|
saveBest()
|
||||||
|
saveActiveGame()
|
||||||
if (checkBoardHealth()) return
|
if (checkBoardHealth()) return
|
||||||
if (stageVisible.value) {
|
if (stageVisible.value) {
|
||||||
scheduleHint()
|
scheduleHint()
|
||||||
@@ -1497,6 +1652,7 @@ async function performReshuffle() {
|
|||||||
reshuffleVisible.value = false
|
reshuffleVisible.value = false
|
||||||
reshuffling.value = false
|
reshuffling.value = false
|
||||||
busy.value = false
|
busy.value = false
|
||||||
|
saveActiveGame()
|
||||||
scheduleHint()
|
scheduleHint()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,10 @@ class TcmController extends BaseApiController
|
|||||||
/** 获取当前登录用户的控糖消消乐每周7人同行榜。 */
|
/** 获取当前登录用户的控糖消消乐每周7人同行榜。 */
|
||||||
public function gameWeeklyLeaderboard()
|
public function gameWeeklyLeaderboard()
|
||||||
{
|
{
|
||||||
$result = GamePlatformLogic::leaderboard((int) $this->userId);
|
$result = GamePlatformLogic::leaderboard(
|
||||||
|
(int) $this->userId,
|
||||||
|
(string) $this->request->get('session_key', '')
|
||||||
|
);
|
||||||
if ($result === false) {
|
if ($result === false) {
|
||||||
return $this->fail(GamePlatformLogic::getError());
|
return $this->fail(GamePlatformLogic::getError());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class GamePlatformLogic
|
|||||||
/**
|
/**
|
||||||
* 获取当前用户所在的真实周榜;首次进入会分配到当周同性别7人组。
|
* 获取当前用户所在的真实周榜;首次进入会分配到当周同性别7人组。
|
||||||
*/
|
*/
|
||||||
public static function leaderboard(int $userId): array|false
|
public static function leaderboard(int $userId, string $sessionKey = ''): array|false
|
||||||
{
|
{
|
||||||
self::$error = '';
|
self::$error = '';
|
||||||
if ($userId <= 0) {
|
if ($userId <= 0) {
|
||||||
@@ -43,7 +43,16 @@ class GamePlatformLogic
|
|||||||
$score = self::ensureWeeklyScore($userId, self::weekStart());
|
$score = self::ensureWeeklyScore($userId, self::weekStart());
|
||||||
$inviteCode = self::ensureShareInvite($userId, self::weekStart());
|
$inviteCode = self::ensureShareInvite($userId, self::weekStart());
|
||||||
Db::commit();
|
Db::commit();
|
||||||
return self::buildLeaderboard((int) $score['group_id'], $userId, $inviteCode);
|
$result = self::buildLeaderboard((int) $score['group_id'], $userId, $inviteCode);
|
||||||
|
$sessionKey = trim($sessionKey);
|
||||||
|
if (preg_match('/^[A-Za-z0-9_-]{16,64}$/', $sessionKey)) {
|
||||||
|
$confirmed = Db::name('tcm_game_session')
|
||||||
|
->where('session_key', $sessionKey)
|
||||||
|
->where('user_id', $userId)
|
||||||
|
->value('learned_count');
|
||||||
|
$result['confirmed_session_learned'] = max(0, (int) $confirmed);
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
Db::rollback();
|
Db::rollback();
|
||||||
self::logException('leaderboard', $e);
|
self::logException('leaderboard', $e);
|
||||||
|
|||||||
Reference in New Issue
Block a user