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:
@@ -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>
|
||||
Reference in New Issue
Block a user