Files
gamegroup2/frontend/src/views-mobile/HomeMobile.vue
T
锦麟 王 7cd53ac420 feat(mobile): SVG icon system + team disband feature
图标/界面优化(问题1):
- new MobileIcon.vue: 统一 SVG 图标组件(40+ 图标,2px 描边风,currentColor)
- 重画占位图 default-avatar.svg / game-placeholder.svg(去 emoji,纯描边)
- 替换全部 emoji 为 SVG:登录品牌🎮、组队🔊、事件🕐📍、胜出🏆、人气🔥、
  邀请🔗🎮、状态切换🟢🔴 等
- 重做底部 Tab 栏:van-icon 默认 → 自定义 SVG(激活态主色高亮)
- 顶部栏 bell、群组详情快捷入口(账本/资产/黑名单)换 SVG
- mobile.css 全局体验增强:按钮按压反馈、空状态、骨架屏工具类、
  页面淡入动画、Toast 刘海屏适配、滚动条隐藏、省略号工具类

组队解散功能(问题2):
- ActivityFeedMobile 补结束/解散按钮(招募中→解散,游戏中→结束游戏)
- 调用 teamStore.finishGame(),带确认弹窗
- 与 PC 端 TeamSessionPanel 行为一致(session 存在且招募/游戏中时群组成员可操作)

build verified: vue-tsc + vite build pass
2026-06-18 15:15:01 +08:00

487 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- src/views-mobile/HomeMobile.vue -->
<!-- 手机端首页状态 + 当前组队 + 我的群组 + 热门游戏 -->
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useGroupStore } from '@/stores/group'
import { useUserStore } from '@/stores/user'
import { useTeamStore } from '@/stores/team'
import { getPopularGames } from '@/api/games'
import type { Game } from '@/types'
import { UserStatusMap } from '@/types'
import { showSuccessToast } from 'vant'
const router = useRouter()
const groupStore = useGroupStore()
const userStore = useUserStore()
const teamStore = useTeamStore()
const popularGames = ref<Game[]>([])
const loading = ref(false)
const statusText = computed(() => UserStatusMap[userStore.userStatus] || '未知')
const hasNoGroup = computed(() => groupStore.groups.length === 0)
const hasSession = computed(() => !!teamStore.currentSession)
onMounted(async () => {
await loadPopularGames()
})
async function loadPopularGames() {
try {
loading.value = true
popularGames.value = await getPopularGames(10)
} catch (error) {
console.error('加载热门游戏失败:', error)
} finally {
loading.value = false
}
}
function selectGroup(groupId: string) {
groupStore.setCurrentGroup(groupId)
router.push({ name: 'GroupView', params: { id: groupId } })
}
// 状态切换选项
const statusActions = [
{ status: 'idle' as const, label: '空闲', color: 'var(--gg-success)' },
{ status: 'away' as const, label: '离开', color: 'var(--gg-text-muted)' },
{ status: 'working' as const, label: '工作中', color: 'var(--gg-danger)' },
]
const showStatusSheet = ref(false)
async function onStatusSelect(action: { status: any; label: string }) {
showStatusSheet.value = false
try {
await userStore.setStatus(action.status)
showSuccessToast(`已切换为${action.label}`)
} catch (e: any) {
console.error(e)
}
}
function goCreateGroup() {
router.push('/mobile-groups')
}
function goSession() {
const session = teamStore.currentSession
if (session?.voiceRoom) {
router.push({
name: 'VoiceRoom',
params: { groupId: session.sourceGroup, sessionId: session.id }
})
}
}
</script>
<template>
<div class="home-mobile">
<!-- 用户状态卡片 -->
<div class="status-card">
<div class="status-info" @click="showStatusSheet = true">
<img
:src="userStore.user?.avatar || '/default-avatar.svg'"
class="avatar"
alt=""
/>
<div class="status-text">
<div class="user-name">{{ userStore.user?.name || userStore.user?.username }}</div>
<div class="user-status">
<span class="status-dot" :class="`dot-${userStore.userStatus}`" />
<span>{{ statusText }}</span>
<span v-if="userStore.user?.statusNote" class="status-note">
· {{ userStore.user.statusNote }}
</span>
</div>
</div>
<van-icon name="arrow-down" class="status-arrow" />
</div>
<div class="quick-stats">
<div class="stat-item">
<span class="stat-num">{{ userStore.user?.points ?? 0 }}</span>
<span class="stat-label">积分</span>
</div>
<div class="stat-item">
<span class="stat-num">{{ groupStore.groups.length }}</span>
<span class="stat-label">群组</span>
</div>
</div>
</div>
<!-- 当前组队卡片 -->
<div v-if="hasSession" class="session-card" @click="goSession">
<div class="session-header">
<van-icon name="volume-o" class="session-icon" />
<span class="session-title">当前组队</span>
<van-tag type="success" size="medium">{{ teamStore.teamStatus }}</van-tag>
</div>
<div class="session-body">
<div class="session-game">{{ teamStore.currentSession?.gameName }}</div>
<div class="session-members">
{{ teamStore.currentSession?.name }}
· {{ teamStore.currentSession?.members?.length || 0 }}
</div>
</div>
<div class="session-action">
进入语音房 <van-icon name="arrow" />
</div>
</div>
<!-- 我的群组 -->
<div class="section">
<div class="section-header">
<span class="section-title">我的群组</span>
<span v-if="!hasNoGroup" class="section-more" @click="goCreateGroup">全部</span>
</div>
<div v-if="hasNoGroup" class="empty-card">
<van-empty description="还没有群组" image-size="80">
<van-button type="primary" size="small" round @click="goCreateGroup">
创建或加入群组
</van-button>
</van-empty>
</div>
<div v-else class="group-scroll">
<div
v-for="group in groupStore.groups"
:key="group.id"
class="group-card"
@click="selectGroup(group.id)"
>
<div class="group-name">{{ group.name }}</div>
<div class="group-meta">{{ group.members?.length || 0 }} </div>
</div>
</div>
</div>
<!-- 热门游戏 -->
<div class="section">
<div class="section-header">
<span class="section-title">热门游戏</span>
</div>
<div v-if="loading" class="loading-row">
<van-loading size="24px">加载中...</van-loading>
</div>
<div v-else-if="popularGames.length === 0" class="empty-row">
<span class="empty-text">暂无热门游戏</span>
</div>
<div v-else class="games-scroll">
<div
v-for="game in popularGames"
:key="game.id"
class="game-card"
>
<img
:src="game.cover || '/game-placeholder.svg'"
:alt="game.name"
class="game-cover"
/>
<div class="game-name">{{ game.name }}</div>
<van-tag v-if="game.platform" plain size="medium">{{ game.platform }}</van-tag>
</div>
</div>
</div>
<!-- 状态切换 ActionSheet -->
<van-action-sheet v-model:show="showStatusSheet" title="切换状态">
<div class="status-sheet">
<div
v-for="a in statusActions"
:key="a.status"
class="status-sheet-item"
@click="onStatusSelect(a)"
>
<span class="status-sheet-dot" :style="{ background: a.color }" />
<span class="status-sheet-label">{{ a.label }}</span>
<van-icon v-if="userStore.userStatus === a.status" name="success" class="status-sheet-check" />
</div>
</div>
</van-action-sheet>
</div>
</template>
<style scoped>
.home-mobile {
padding: 12px;
display: flex;
flex-direction: column;
gap: 14px;
}
/* 状态卡片 */
.status-card {
background: var(--gg-bg-card);
border-radius: var(--gg-radius-lg);
padding: 16px;
box-shadow: var(--gg-shadow);
}
.status-info {
display: flex;
align-items: center;
gap: 12px;
}
.avatar {
width: 48px;
height: 48px;
border-radius: 50%;
object-fit: cover;
border: 2px solid var(--gg-border);
}
.status-text {
flex: 1;
min-width: 0;
}
.user-name {
font-size: 17px;
font-weight: 600;
color: var(--gg-text);
}
.user-status {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--gg-text-secondary);
margin-top: 4px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.dot-idle { background: var(--gg-success); }
.dot-in_team { background: var(--gg-info); }
.dot-working { background: var(--gg-danger); }
.dot-away { background: var(--gg-text-muted); }
/* 状态切换面板 */
.status-sheet { padding: 8px 0 calc(16px + env(safe-area-inset-bottom, 0px)); }
.status-sheet-item {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 24px;
font-size: 15px;
color: var(--gg-text);
}
.status-sheet-item:active { background: var(--gg-bg-elevated); }
.status-sheet-dot { width: 10px; height: 10px; border-radius: 50%; }
.status-sheet-label { flex: 1; }
.status-sheet-check { color: var(--gg-primary); font-size: 18px; }
.status-note {
color: var(--gg-text-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.status-arrow {
color: var(--gg-text-muted);
font-size: 14px;
}
.quick-stats {
display: flex;
gap: 24px;
margin-top: 14px;
padding-top: 14px;
border-top: 1px solid var(--gg-border);
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
}
.stat-num {
font-size: 20px;
font-weight: 700;
color: var(--gg-primary);
}
.stat-label {
font-size: 12px;
color: var(--gg-text-muted);
margin-top: 2px;
}
/* 组队卡片 */
.session-card {
background: linear-gradient(135deg, var(--gg-primary), var(--gg-accent));
border-radius: var(--gg-radius-lg);
padding: 16px;
color: #fff;
box-shadow: 0 4px 16px rgba(5, 150, 105, 0.25);
}
.session-header {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
opacity: 0.9;
}
.session-icon {
font-size: 16px;
}
.session-body {
margin: 10px 0;
}
.session-game {
font-size: 18px;
font-weight: 700;
}
.session-members {
font-size: 13px;
opacity: 0.85;
margin-top: 4px;
}
.session-action {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 4px;
font-size: 14px;
font-weight: 500;
}
/* 通用 section */
.section {
display: flex;
flex-direction: column;
gap: 10px;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 2px;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: var(--gg-text);
}
.section-more {
font-size: 13px;
color: var(--gg-primary);
}
.empty-card {
background: var(--gg-bg-card);
border-radius: var(--gg-radius-md);
padding: 8px;
}
.loading-row, .empty-row {
display: flex;
justify-content: center;
padding: 24px;
}
.empty-text {
font-size: 13px;
color: var(--gg-text-muted);
}
/* 群组横滑 */
.group-scroll {
display: flex;
gap: 10px;
overflow-x: auto;
padding-bottom: 4px;
scroll-behavior: smooth;
}
.group-scroll::-webkit-scrollbar {
display: none;
}
.group-card {
flex-shrink: 0;
width: 130px;
padding: 14px;
background: var(--gg-bg-card);
border-radius: var(--gg-radius-md);
box-shadow: var(--gg-shadow);
}
.group-card:active {
opacity: 0.85;
}
.group-name {
font-size: 14px;
font-weight: 600;
color: var(--gg-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.group-meta {
font-size: 12px;
color: var(--gg-text-muted);
margin-top: 6px;
}
/* 游戏横滑 */
.games-scroll {
display: flex;
gap: 10px;
overflow-x: auto;
padding-bottom: 4px;
}
.games-scroll::-webkit-scrollbar {
display: none;
}
.game-card {
flex-shrink: 0;
width: 100px;
}
.game-cover {
width: 100px;
height: 130px;
border-radius: var(--gg-radius-md);
object-fit: cover;
background: var(--gg-bg-elevated);
}
.game-name {
font-size: 12px;
color: var(--gg-text);
margin-top: 6px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>