252 lines
8.4 KiB
JavaScript
252 lines
8.4 KiB
JavaScript
import { ref } from 'vue'
|
|
|
|
const EMPTY_BOARD = {
|
|
week_start: '',
|
|
week_end: '',
|
|
sex_label: '同行',
|
|
group_size: 7,
|
|
member_count: 0,
|
|
players: [],
|
|
me: { count: 0, rank: 1, best_score: 0, distance: 0, is_first: true },
|
|
invite_code: ''
|
|
}
|
|
const PENDING_SYNC_KEY = 'tongji_endless_pending_sync_v1'
|
|
|
|
function createSessionKey() {
|
|
const random = Math.random().toString(36).slice(2, 12)
|
|
return `game_${Date.now().toString(36)}_${random}`
|
|
}
|
|
|
|
function readPendingPayloads() {
|
|
try {
|
|
const stored = uni.getStorageSync(PENDING_SYNC_KEY)
|
|
if (!Array.isArray(stored)) return []
|
|
return stored.filter(item => (
|
|
item
|
|
&& /^[A-Za-z0-9_-]{16,64}$/.test(String(item.session_key || ''))
|
|
&& Number(item.learned_count) >= 0
|
|
)).slice(-8)
|
|
} catch (_) {
|
|
return []
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 小游戏的平台连接层。复用主小程序 token,不在游戏里另建账号。
|
|
* 每次上报的是本局绝对值,后端按 session_key 去重,断网重试也不会重复加分。
|
|
*/
|
|
export function useGamePlatform(proxy, ensureLoggedIn) {
|
|
const connected = ref(false)
|
|
const loading = ref(false)
|
|
const syncStatus = ref('idle')
|
|
const leaderboard = ref({ ...EMPTY_BOARD })
|
|
const confirmedSessionLearned = ref(0)
|
|
|
|
let sessionKey = createSessionKey()
|
|
let syncTimer = null
|
|
let syncing = false
|
|
let queuedPayloads = readPendingPayloads()
|
|
let connectPromise = null
|
|
|
|
function persistPendingPayloads() {
|
|
try { uni.setStorageSync(PENDING_SYNC_KEY, queuedPayloads.slice(-8)) } catch (_) {}
|
|
}
|
|
|
|
async function api(request) {
|
|
if (!proxy?.apiUrl) throw new Error('平台接口未初始化')
|
|
return proxy.apiUrl(request, false)
|
|
}
|
|
|
|
function applyLeaderboard(data, confirmedForSession = '') {
|
|
if (!data || !Array.isArray(data.players)) return
|
|
const inactiveSessionResponse = confirmedForSession && confirmedForSession !== sessionKey
|
|
if (!inactiveSessionResponse) {
|
|
leaderboard.value = {
|
|
...EMPTY_BOARD,
|
|
...data,
|
|
me: { ...EMPTY_BOARD.me, ...(data.me || {}) },
|
|
players: data.players
|
|
}
|
|
}
|
|
if (confirmedForSession === sessionKey && data.confirmed_session_learned != null) {
|
|
confirmedSessionLearned.value = Number(data.confirmed_session_learned) || 0
|
|
}
|
|
connected.value = true
|
|
syncStatus.value = 'synced'
|
|
}
|
|
|
|
async function refreshLeaderboard() {
|
|
const res = await api({
|
|
url: '/api/tcm/gameWeeklyLeaderboard',
|
|
method: 'GET'
|
|
})
|
|
if (res?.code !== 1 || !res.data) {
|
|
throw new Error(res?.msg || '同行榜加载失败')
|
|
}
|
|
applyLeaderboard(res.data)
|
|
return res.data
|
|
}
|
|
|
|
async function acceptShare(inviteCode) {
|
|
const code = String(inviteCode || '').trim().toUpperCase()
|
|
if (!code) return
|
|
try {
|
|
await api({
|
|
url: '/api/tcm/gameAcceptShare',
|
|
method: 'POST',
|
|
data: { invite_code: code }
|
|
})
|
|
} catch (_) {
|
|
// 分享关系是辅助能力,不阻断进入游戏。
|
|
}
|
|
}
|
|
|
|
async function connect(inviteCode = '') {
|
|
if (connectPromise) return connectPromise
|
|
connectPromise = (async () => {
|
|
loading.value = true
|
|
try {
|
|
const loggedIn = await ensureLoggedIn()
|
|
if (!loggedIn) throw new Error('登录失败')
|
|
await acceptShare(inviteCode)
|
|
await refreshLeaderboard()
|
|
if (queuedPayloads.length) flushProgress()
|
|
return true
|
|
} catch (_) {
|
|
connected.value = false
|
|
syncStatus.value = 'offline'
|
|
return false
|
|
} finally {
|
|
loading.value = false
|
|
connectPromise = null
|
|
}
|
|
})()
|
|
return connectPromise
|
|
}
|
|
|
|
function beginSession() {
|
|
if (syncTimer) clearTimeout(syncTimer)
|
|
sessionKey = createSessionKey()
|
|
confirmedSessionLearned.value = 0
|
|
syncStatus.value = queuedPayloads.length ? 'pending' : (connected.value ? 'synced' : 'offline')
|
|
return sessionKey
|
|
}
|
|
|
|
function queueProgress(learnedCount, score, ended = false) {
|
|
const nextPayload = {
|
|
session_key: sessionKey,
|
|
learned_count: Math.max(0, Number(learnedCount) || 0),
|
|
score: Math.max(0, Number(score) || 0),
|
|
ended: ended ? 1 : 0
|
|
}
|
|
const existingIndex = queuedPayloads.findIndex(item => item.session_key === sessionKey)
|
|
if (existingIndex >= 0) {
|
|
const existing = queuedPayloads[existingIndex]
|
|
queuedPayloads[existingIndex] = {
|
|
...nextPayload,
|
|
learned_count: Math.max(existing.learned_count, nextPayload.learned_count),
|
|
score: Math.max(existing.score, nextPayload.score),
|
|
ended: Math.max(existing.ended, nextPayload.ended)
|
|
}
|
|
} else {
|
|
queuedPayloads.push(nextPayload)
|
|
}
|
|
persistPendingPayloads()
|
|
syncStatus.value = 'pending'
|
|
if (syncTimer) clearTimeout(syncTimer)
|
|
if (ended) {
|
|
flushProgress()
|
|
} else {
|
|
syncTimer = setTimeout(flushProgress, 1000)
|
|
}
|
|
}
|
|
|
|
async function flushProgress() {
|
|
if (syncTimer) clearTimeout(syncTimer)
|
|
syncTimer = null
|
|
if (syncing || !queuedPayloads.length) return
|
|
const payload = queuedPayloads[0]
|
|
syncing = true
|
|
syncStatus.value = 'syncing'
|
|
try {
|
|
if (!connected.value) {
|
|
const loggedIn = await ensureLoggedIn()
|
|
if (!loggedIn) throw new Error('未登录')
|
|
}
|
|
const res = await api({
|
|
url: '/api/tcm/gameSubmitProgress',
|
|
method: 'POST',
|
|
data: payload
|
|
})
|
|
if (res?.code !== 1 || !res.data) {
|
|
throw new Error(res?.msg || '成绩保存失败')
|
|
}
|
|
applyLeaderboard(res.data, payload.session_key)
|
|
// 请求发出后玩家可能又完成了消除。只移除已经被本次请求覆盖的进度,
|
|
// 不能按 session_key 整局删除,否则会丢失请求进行期间产生的新进度。
|
|
queuedPayloads = queuedPayloads.filter(item => (
|
|
item.session_key !== payload.session_key
|
|
|| Number(item.learned_count) > Number(payload.learned_count)
|
|
|| Number(item.score) > Number(payload.score)
|
|
|| Number(item.ended) > Number(payload.ended)
|
|
))
|
|
persistPendingPayloads()
|
|
} catch (_) {
|
|
connected.value = false
|
|
syncStatus.value = 'offline'
|
|
// 队首保留原绝对值,下次连接或打开榜单时安全重试。
|
|
persistPendingPayloads()
|
|
} finally {
|
|
syncing = false
|
|
if (queuedPayloads.length && connected.value) {
|
|
setTimeout(flushProgress, 80)
|
|
}
|
|
}
|
|
}
|
|
|
|
async function syncAndRefresh(learnedCount, score) {
|
|
queueProgress(learnedCount, score, false)
|
|
await flushProgress()
|
|
if (!connected.value) {
|
|
await connect()
|
|
if (queuedPayloads.length) await flushProgress()
|
|
} else {
|
|
try { await refreshLeaderboard() } catch (_) {}
|
|
}
|
|
}
|
|
|
|
async function recordShare() {
|
|
try {
|
|
if (!connected.value && !(await connect())) return
|
|
const res = await api({ url: '/api/tcm/gameRecordShare', method: 'POST' })
|
|
if (res?.code === 1 && res.data?.invite_code) {
|
|
leaderboard.value = { ...leaderboard.value, invite_code: res.data.invite_code }
|
|
}
|
|
} catch (_) {
|
|
// 分享本身仍可进行,统计失败不影响用户。
|
|
}
|
|
}
|
|
|
|
function dispose() {
|
|
if (syncTimer) clearTimeout(syncTimer)
|
|
syncTimer = null
|
|
persistPendingPayloads()
|
|
}
|
|
|
|
return {
|
|
connected,
|
|
loading,
|
|
syncStatus,
|
|
leaderboard,
|
|
confirmedSessionLearned,
|
|
connect,
|
|
beginSession,
|
|
queueProgress,
|
|
flushProgress,
|
|
syncAndRefresh,
|
|
refreshLeaderboard,
|
|
recordShare,
|
|
dispose
|
|
}
|
|
}
|