129 lines
3.3 KiB
Vue
129 lines
3.3 KiB
Vue
<script setup>
|
||
import { ref, onMounted } from 'vue'
|
||
import { useI18n } from 'vue-i18n'
|
||
import { leaderboard } from '../config.js'
|
||
|
||
const { t } = useI18n({ useScope: 'global' })
|
||
|
||
const rows = ref([])
|
||
const state = ref('loading') // loading | ok | error
|
||
const errorMsg = ref('')
|
||
|
||
// 复用 449 现有后端:POST endpoint {num} 取分页 JSON
|
||
async function load() {
|
||
state.value = 'loading'
|
||
try {
|
||
const all = []
|
||
for (let page = 0; page < leaderboard.pages; page++) {
|
||
const res = await fetch(leaderboard.endpoint, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
body: `num=${page * leaderboard.perPage}`
|
||
})
|
||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||
const data = await res.json()
|
||
// data 形如 { "1": ["name", score], ... }
|
||
Object.keys(data).forEach((k) => {
|
||
const entry = data[k]
|
||
all.push({ rank: all.length + 1, name: entry[0], score: entry[1] })
|
||
})
|
||
}
|
||
rows.value = all
|
||
state.value = all.length ? 'ok' : 'error'
|
||
if (!all.length) errorMsg.value = t('mobileGame.leaderboard.noScores')
|
||
} catch (e) {
|
||
state.value = 'error'
|
||
errorMsg.value = t('mobileGame.leaderboard.error')
|
||
}
|
||
}
|
||
|
||
onMounted(load)
|
||
</script>
|
||
|
||
<template>
|
||
<div class="leaderboard">
|
||
<div v-if="state === 'loading'" class="leaderboard__state">
|
||
{{ t('common.loading') }}
|
||
</div>
|
||
|
||
<div v-else-if="state === 'error'" class="leaderboard__state leaderboard__state--error">
|
||
{{ errorMsg }}
|
||
</div>
|
||
|
||
<table v-else class="leaderboard__table">
|
||
<thead>
|
||
<tr>
|
||
<th>{{ t('mobileGame.leaderboard.colRank') }}</th>
|
||
<th>{{ t('mobileGame.leaderboard.colName') }}</th>
|
||
<th>{{ t('mobileGame.leaderboard.colScore') }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="r in rows" :key="r.rank" :class="{ 'is-top': r.rank <= 3 }">
|
||
<td class="leaderboard__rank">{{ r.rank }}</td>
|
||
<td class="leaderboard__name">{{ r.name }}</td>
|
||
<td class="leaderboard__score">{{ r.score }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.leaderboard {
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
overflow: hidden;
|
||
background: var(--bg);
|
||
}
|
||
.leaderboard__state {
|
||
padding: var(--space-xl);
|
||
text-align: center;
|
||
color: var(--text-secondary);
|
||
}
|
||
.leaderboard__state--error {
|
||
color: var(--text-tertiary);
|
||
font-size: 0.9rem;
|
||
}
|
||
.leaderboard__table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 0.92rem;
|
||
}
|
||
.leaderboard__table th {
|
||
text-align: left;
|
||
padding: var(--space-sm) var(--space-md);
|
||
background: var(--surface);
|
||
border-bottom: 1px solid var(--border);
|
||
font-weight: 600;
|
||
color: var(--text-secondary);
|
||
font-size: 0.82rem;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.04em;
|
||
}
|
||
.leaderboard__table td {
|
||
padding: var(--space-sm) var(--space-md);
|
||
border-bottom: 1px solid var(--border);
|
||
color: var(--text-primary);
|
||
}
|
||
.leaderboard__table tbody tr:last-child td {
|
||
border-bottom: none;
|
||
}
|
||
.leaderboard__table tbody tr:hover {
|
||
background: var(--surface);
|
||
}
|
||
.leaderboard__rank {
|
||
width: 50px;
|
||
font-family: var(--font-mono);
|
||
color: var(--text-tertiary);
|
||
}
|
||
tr.is-top .leaderboard__rank {
|
||
color: var(--accent);
|
||
font-weight: 600;
|
||
}
|
||
.leaderboard__score {
|
||
font-family: var(--font-mono);
|
||
text-align: right;
|
||
}
|
||
</style>
|