feat(mobile): stage 9 - memories, stats, profile, settings, changelog

- migrate MemoryGridMobile.vue (grid + upload + fullscreen preview + delete)
- migrate StatsPanelMobile.vue (overview + points ranking)
- migrate ProfileMobile.vue (user header + stats + status + entries + logout)
- migrate SettingsMobile.vue (notify/sound toggles + desktop switch + about)
- migrate ChangelogMobile.vue (timeline; data synced with uat PC v0.3.6 + mobile v2.1.0 entry)
- GroupViewMobile: wire memories/stats tabs (replace placeholders)
- router: wire Profile/Settings/Changelog mobile views
- verified: memories/points APIs + Memory type + user store logout/setStatus match uat

build verified: vue-tsc + vite build pass
This commit is contained in:
锦麟 王
2026-06-18 11:25:10 +08:00
parent 13e87110ae
commit 4b54f71902
7 changed files with 677 additions and 4 deletions
@@ -0,0 +1,109 @@
<!-- src/components-mobile/stats/StatsPanelMobile.vue -->
<!-- 手机端统计简化版积分排行 + 群组数据 -->
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { getGroupMemberRanking } from '@/api/points'
import { getGroupGames } from '@/api/games'
import { useGroupStore } from '@/stores/group'
const props = defineProps<{ groupId: string }>()
const groupStore = useGroupStore()
const ranking = ref<{ userId: string; points: number; name?: string }[]>([])
const gameCount = ref(0)
const loading = ref(false)
const group = computed(() => groupStore.currentGroup)
const members = computed(() => groupStore.currentMembers)
onMounted(async () => {
loading.value = true
try {
const [rank, games] = await Promise.all([
getGroupMemberRanking(props.groupId, 20),
getGroupGames(props.groupId, { limit: 1 })
])
ranking.value = rank
gameCount.value = games.total
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
})
// 排名样式
function rankColor(index: number): string {
if (index === 0) return '#f59e0b'
if (index === 1) return '#94a3b8'
if (index === 2) return '#d97706'
return 'var(--gg-text-muted)'
}
</script>
<template>
<div class="stats-mobile">
<div v-if="loading" class="loading-box">
<van-loading size="24px">加载中...</van-loading>
</div>
<template v-else>
<!-- 概览卡片 -->
<div class="overview-card">
<div class="overview-item">
<div class="overview-num">{{ members.length }}</div>
<div class="overview-label">成员</div>
</div>
<div class="overview-item">
<div class="overview-num">{{ gameCount }}</div>
<div class="overview-label">游戏</div>
</div>
<div class="overview-item">
<div class="overview-num">{{ group?.maxMembers || '-' }}</div>
<div class="overview-label">上限</div>
</div>
</div>
<!-- 积分排行 -->
<div class="section">
<div class="section-title">积分排行</div>
<div v-if="ranking.length === 0" class="empty-row">暂无数据</div>
<div class="rank-list">
<div
v-for="(item, idx) in ranking"
:key="item.userId"
class="rank-item"
>
<div class="rank-num" :style="{ color: rankColor(idx) }">{{ idx + 1 }}</div>
<div class="rank-info">
<div class="rank-name">{{ item.name || '玩家' }}</div>
</div>
<div class="rank-points">{{ item.points }}</div>
</div>
</div>
</div>
</template>
</div>
</template>
<style scoped>
.stats-mobile { padding: 12px; }
.loading-box { display: flex; justify-content: center; padding: 40px; }
.overview-card { display: flex; background: var(--gg-bg-card); border-radius: var(--gg-radius-lg); padding: 20px; box-shadow: var(--gg-shadow); margin-bottom: 16px; }
.overview-item { flex: 1; display: flex; flex-direction: column; align-items: center; }
.overview-num { font-size: 24px; font-weight: 700; color: var(--gg-primary); }
.overview-label { font-size: 12px; color: var(--gg-text-muted); margin-top: 4px; }
.section { margin-bottom: 16px; }
.section-title { font-size: 15px; font-weight: 600; color: var(--gg-text); margin-bottom: 10px; }
.empty-row { text-align: center; padding: 20px; color: var(--gg-text-muted); font-size: 13px; }
.rank-list { display: flex; flex-direction: column; gap: 8px; }
.rank-item { display: flex; align-items: center; gap: 12px; background: var(--gg-bg-card); padding: 12px 14px; border-radius: var(--gg-radius-sm); box-shadow: var(--gg-shadow); }
.rank-num { font-size: 18px; font-weight: 700; width: 28px; text-align: center; }
.rank-info { flex: 1; min-width: 0; }
.rank-name { font-size: 14px; color: var(--gg-text); }
.rank-points { font-size: 15px; font-weight: 600; color: var(--gg-primary); }
</style>