init: 移动游戏项目独立化(便携式 App.vue + 独立 i18n,可嵌入 mainPage)

This commit is contained in:
root
2026-07-23 06:57:03 +00:00
commit a5d4c05392
28 changed files with 2844 additions and 0 deletions

View File

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