diff --git a/frontend/public/default-avatar.svg b/frontend/public/default-avatar.svg index 8687dbe..558cc22 100644 --- a/frontend/public/default-avatar.svg +++ b/frontend/public/default-avatar.svg @@ -1,4 +1,11 @@ - - 👤 + + + + + + + + + diff --git a/frontend/public/game-placeholder.svg b/frontend/public/game-placeholder.svg index 64043e1..55490cc 100644 --- a/frontend/public/game-placeholder.svg +++ b/frontend/public/game-placeholder.svg @@ -1,5 +1,17 @@ - - 🎮 - 暂无封面 + + + + + + + + + + + + + + + 暂无封面 diff --git a/frontend/src/assets/mobile.css b/frontend/src/assets/mobile.css index f34789f..1e5e12b 100644 --- a/frontend/src/assets/mobile.css +++ b/frontend/src/assets/mobile.css @@ -51,3 +51,105 @@ .mobile-app * { -webkit-tap-highlight-color: transparent; } + +/* ============ 细节体验优化(全局) ============ */ + +/* 1. 按钮点击反馈:统一按压缩放 */ +.mobile-app .van-button:active { + opacity: 0.85; + transform: scale(0.98); +} +.mobile-app .van-button { + transition: opacity 0.15s, transform 0.15s; +} + +/* 2. 可点击卡片按压反馈(约定:凡 .clickable 或带 @click 的卡片建议加此类) */ +.mobile-app .clickable { + transition: transform 0.12s, box-shadow 0.12s; + cursor: pointer; +} +.mobile-app .clickable:active { + transform: scale(0.98); +} + +/* 3. van-empty 空状态:图标加品牌色淡背景,提升存在感 */ +.mobile-app .van-empty__image { + opacity: 0.85; +} +.mobile-app .van-empty__description { + color: var(--gg-text-muted); + font-size: 13px; +} + +/* 4. 列表项分隔线统一为更柔和的颜色 */ +.mobile-app .van-cell { + border-color: var(--gg-border); +} +.mobile-app .van-cell::after { + border-color: var(--gg-border) !important; +} + +/* 5. 全局卡片阴影统一(覆盖各页面自定义的过深/过浅阴影) */ +.mobile-app .van-popup, +.mobile-app .van-action-sheet { + box-shadow: 0 -4px 24px rgba(15, 23, 42, 0.08); +} + +/* 6. Tab 栏激活态下划线更柔和 */ +.mobile-app .van-tabs__line { + background: var(--gg-gradient); + border-radius: 3px; +} + +/* 7. 输入框聚焦态:边框过渡更顺滑 */ +.mobile-app .van-field { + transition: border-color 0.2s; +} + +/* 8. 骨架屏占位(list 加载时,配合 van-skeleton 或 .skeleton 类使用) */ +.mobile-app .skeleton-block { + background: linear-gradient(90deg, var(--gg-bg-elevated) 25%, var(--gg-bg) 37%, var(--gg-bg-elevated) 63%); + background-size: 400% 100%; + animation: skeleton-loading 1.4s ease infinite; + border-radius: var(--gg-radius-sm); +} +@keyframes skeleton-loading { + 0% { background-position: 100% 50%; } + 100% { background-position: 0 50%; } +} + +/* 9. 滚动容器:隐藏滚动条但保留滚动能力(移动端原生体验) */ +.mobile-app .scroll-hide { + scrollbar-width: none; + -ms-overflow-style: none; +} +.mobile-app .scroll-hide::-webkit-scrollbar { + display: none; +} + +/* 10. 文字溢出省略工具类(列表标题常用) */ +.mobile-app .ellipsis { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.mobile-app .ellipsis-2 { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +/* 11. 页面切换淡入动画(router-view 配合) */ +.mobile-app .mobile-content { + animation: page-fade-in 0.25s ease; +} +@keyframes page-fade-in { + from { opacity: 0; transform: translateY(4px); } + to { opacity: 1; transform: translateY(0); } +} + +/* 12. Toast 在刘海屏顶部不被遮挡 */ +.mobile-app .van-toast { + top: calc(env(safe-area-inset-top, 0px) + 10%); +} diff --git a/frontend/src/components-mobile/MobileIcon.vue b/frontend/src/components-mobile/MobileIcon.vue new file mode 100644 index 0000000..983cf87 --- /dev/null +++ b/frontend/src/components-mobile/MobileIcon.vue @@ -0,0 +1,113 @@ + + + + + + diff --git a/frontend/src/components-mobile/bet/BetListMobile.vue b/frontend/src/components-mobile/bet/BetListMobile.vue index 347ea80..d2a9327 100644 --- a/frontend/src/components-mobile/bet/BetListMobile.vue +++ b/frontend/src/components-mobile/bet/BetListMobile.vue @@ -6,6 +6,7 @@ import { listBets, getBet, getBetOptions, getBetEntries, placeBet, createBet, cl import { useUserStore } from '@/stores/user' import type { Bet, BetOption, BetEntry } from '@/types' import { showSuccessToast, showFailToast, showConfirmDialog } from 'vant' +import MobileIcon from '@/components-mobile/MobileIcon.vue' const props = defineProps<{ groupId: string }>() @@ -224,7 +225,7 @@ function timeAgo(dateStr: string): string { >
{{ opt.content }} - 🏆 胜出 + 胜出
{{ optionEntryCount(opt.id) }} 注 @@ -239,7 +240,7 @@ function timeAgo(dateStr: string): string { :type="myEntry()?.won ? 'success' : 'warning'" wrapable > - {{ myEntry()?.won ? `🎉 你赢了!` : '本次未中奖' }} + {{ myEntry()?.won ? `你赢了!` : '本次未中奖' }}
@@ -385,7 +386,7 @@ function timeAgo(dateStr: string): string { .option-item { background: var(--gg-bg-card); border-radius: var(--gg-radius-sm); padding: 12px 14px; border: 1px solid var(--gg-border); } .option-row { display: flex; align-items: center; justify-content: space-between; } .option-text { font-size: 14px; font-weight: 500; color: var(--gg-text); } -.winner-badge { font-size: 13px; color: var(--gg-warning); font-weight: 600; } +.winner-badge { display: inline-flex; align-items: center; gap: 2px; font-size: 13px; color: var(--gg-warning); font-weight: 600; } .option-stats { display: flex; gap: 16px; margin-top: 8px; font-size: 12px; color: var(--gg-text-muted); } .my-result { margin-top: 16px; } diff --git a/frontend/src/components-mobile/event/EventListMobile.vue b/frontend/src/components-mobile/event/EventListMobile.vue index 317c1c8..fcd9dd5 100644 --- a/frontend/src/components-mobile/event/EventListMobile.vue +++ b/frontend/src/components-mobile/event/EventListMobile.vue @@ -10,6 +10,7 @@ import { EventStatusMap, RSVPTypeMap } from '@/types' import type { Event, RSVPType } from '@/types' import { displayName } from '@/types' import { showSuccessToast, showFailToast, showConfirmDialog } from 'vant' +import MobileIcon from '@/components-mobile/MobileIcon.vue' import CreateEventSheetMobile from './CreateEventSheetMobile.vue' const props = defineProps<{ groupId: string }>() @@ -180,8 +181,8 @@ async function onCreated() {
{{ event.title }}
-
🕐 {{ formatDateTime(event.startTime) }}
-
📍 {{ event.location }}
+
{{ formatDateTime(event.startTime) }}
+
{{ event.location }}
@@ -245,7 +246,7 @@ async function onCreated() {
{{ event.title }}
-
🕐 {{ formatDateTime(event.startTime) }}
+
{{ formatDateTime(event.startTime) }}

{{ event.description }}

@@ -296,8 +297,8 @@ async function onCreated() { .expand-icon { margin-left: auto; color: var(--gg-text-muted); font-size: 14px; } .event-title { font-size: 16px; font-weight: 600; color: var(--gg-text); } -.event-time { font-size: 13px; color: var(--gg-text-secondary); margin-top: 6px; } -.event-loc { font-size: 13px; color: var(--gg-text-secondary); margin-top: 2px; } +.event-time { display: flex; align-items: center; gap: 4px; font-size: 13px; color: var(--gg-text-secondary); margin-top: 6px; } +.event-loc { display: flex; align-items: center; gap: 4px; font-size: 13px; color: var(--gg-text-secondary); margin-top: 2px; } .event-detail { background: var(--gg-bg); diff --git a/frontend/src/components-mobile/game/GameDetailSheetMobile.vue b/frontend/src/components-mobile/game/GameDetailSheetMobile.vue index 339c4e5..5597d2d 100644 --- a/frontend/src/components-mobile/game/GameDetailSheetMobile.vue +++ b/frontend/src/components-mobile/game/GameDetailSheetMobile.vue @@ -13,6 +13,7 @@ import { createTeamSession } from '@/api/sessions' import { useTeamStore } from '@/stores/team' import { pb } from '@/api/pocketbase' import { showSuccessToast, showFailToast, showConfirmDialog } from 'vant' +import MobileIcon from '@/components-mobile/MobileIcon.vue' const props = defineProps<{ show: boolean @@ -239,7 +240,7 @@ function formatDate(dateStr: string): string {
{{ game.platform }} - 🔥 {{ game.popularCount }} 人游玩 + {{ game.popularCount }} 人游玩
@@ -412,6 +413,9 @@ function formatDate(dateStr: string): string { } .popularity { + display: inline-flex; + align-items: center; + gap: 3px; font-size: 13px; color: var(--gg-text-secondary); } diff --git a/frontend/src/components-mobile/group/ActivityFeedMobile.vue b/frontend/src/components-mobile/group/ActivityFeedMobile.vue index 8ad9bfc..5eb24b2 100644 --- a/frontend/src/components-mobile/group/ActivityFeedMobile.vue +++ b/frontend/src/components-mobile/group/ActivityFeedMobile.vue @@ -9,7 +9,8 @@ import { useRouter } from 'vue-router' import { createTeamSession } from '@/api/sessions' import { sendBulkInvitations } from '@/api/invitations' import type { User } from '@/types' -import { showSuccessToast, showFailToast } from 'vant' +import { showSuccessToast, showFailToast, showConfirmDialog } from 'vant' +import MobileIcon from '@/components-mobile/MobileIcon.vue' const props = defineProps<{ groupId: string }>() @@ -107,21 +108,65 @@ function goVoiceRoom() { }) } } + +// 当前会话状态(用于显示招募中/游戏中) +const sessionStatus = computed(() => teamStore.currentSession?.status || 'recruiting') +const isRecruiting = computed(() => sessionStatus.value === 'recruiting') +const isPlaying = computed(() => sessionStatus.value === 'playing') + +// 解散/结束组队(与 PC 端一致:session 存在且处于招募/游戏中时,群组成员可操作) +const ending = ref(false) +async function handleEndSession() { + const s = teamStore.currentSession + if (!s) return + const isRecruit = s.status === 'recruiting' + showConfirmDialog({ + title: isRecruit ? '解散小队' : '结束游戏', + message: isRecruit + ? '确定要解散当前小队吗?' + : '确定要结束当前游戏吗?小队将被解散。' + }).then(async () => { + ending.value = true + try { + await teamStore.finishGame() + showSuccessToast(isRecruit ? '已解散' : '已结束') + } catch (e: any) { + showFailToast(e.message || '操作失败') + } finally { + ending.value = false + } + }).catch(() => {}) +}