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
This commit is contained in:
@@ -1,4 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||
<rect fill="#e8f5e9" width="64" height="64" rx="32"/>
|
||||
<text x="32" y="40" text-anchor="middle" fill="#059669" font-size="28">👤</text>
|
||||
<defs>
|
||||
<linearGradient id="avBg" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#ecfdf5"/>
|
||||
<stop offset="100%" stop-color="#d1fae5"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect fill="url(#avBg)" width="64" height="64" rx="32"/>
|
||||
<circle cx="32" cy="25" r="10" fill="none" stroke="#059669" stroke-width="2.5"/>
|
||||
<path d="M14 54c0-9 8-15 18-15s18 6 18 15" fill="none" stroke="#059669" stroke-width="2.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 232 B After Width: | Height: | Size: 547 B |
@@ -1,5 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="260" viewBox="0 0 200 260">
|
||||
<rect fill="#e8f5e9" width="200" height="260"/>
|
||||
<text x="100" y="120" text-anchor="middle" fill="#059669" font-size="48">🎮</text>
|
||||
<text x="100" y="155" text-anchor="middle" fill="#6b7280" font-size="14">暂无封面</text>
|
||||
<defs>
|
||||
<linearGradient id="gBg" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#f0fdf4"/>
|
||||
<stop offset="100%" stop-color="#dcfce7"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect fill="url(#gBg)" width="200" height="260"/>
|
||||
<!-- 手柄主体 -->
|
||||
<rect x="44" y="108" width="112" height="58" rx="20" fill="none" stroke="#059669" stroke-width="3.5"/>
|
||||
<!-- 左侧十字方向 -->
|
||||
<path d="M70 137v-10M65 132h10" stroke="#059669" stroke-width="3.5" stroke-linecap="round"/>
|
||||
<!-- 右侧按钮 -->
|
||||
<circle cx="125" cy="128" r="3.2" fill="#10b981"/>
|
||||
<circle cx="138" cy="142" r="3.2" fill="#10b981"/>
|
||||
<text x="100" y="200" text-anchor="middle" fill="#059669" font-size="13" font-family="system-ui,sans-serif" font-weight="500">暂无封面</text>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 327 B After Width: | Height: | Size: 874 B |
@@ -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%);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
<!--
|
||||
MobileIcon.vue
|
||||
手机端统一 SVG 图标组件 —— 线性描边风格,2px 线宽,24×24 viewBox
|
||||
颜色继承 currentColor,通过 color/style/class 控制
|
||||
用法:<MobileIcon name="game" /> <MobileIcon name="bell" :size="22" color="var(--gg-primary)" />
|
||||
-->
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
name: string
|
||||
size?: number | string
|
||||
color?: string
|
||||
}>(), {
|
||||
size: 20,
|
||||
color: 'currentColor'
|
||||
})
|
||||
|
||||
// 所有图标的 path 数据(线性描边,stroke 风格统一)
|
||||
// 每个 name 对应一组 SVG 内部元素(path/circle 等)
|
||||
const ICONS: Record<string, string> = {
|
||||
// ---- 品牌/导航 ----
|
||||
logo: '<path d="M6 8h12a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-6a2 2 0 0 1 2-2Z"/><path d="M7 13h2"/><path d="M15 13h2"/><path d="M9 16h6"/>',
|
||||
home: '<path d="M3 10.5 12 3l9 7.5"/><path d="M5 9.5V20a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V9.5"/><path d="M9.5 21v-6h5v6"/>',
|
||||
users: '<circle cx="9" cy="8" r="3"/><path d="M3.5 20a5.5 5.5 0 0 1 11 0"/><path d="M16 6.5a3 3 0 0 1 0 5.8"/><path d="M17 14.5a5.5 5.5 0 0 1 3.5 5.1"/>',
|
||||
game: '<rect x="2.5" y="7" width="19" height="10" rx="3"/><path d="M7 11v3"/><path d="M5.5 12.5h3"/><circle cx="16" cy="11.5" r="0.8" fill="currentColor" stroke="none"/><circle cx="18.5" cy="14" r="0.8" fill="currentColor" stroke="none"/>',
|
||||
bell: '<path d="M6 9a6 6 0 0 1 12 0c0 4 1.2 5.5 2 6.5H4c.8-1 2-2.5 2-6.5Z"/><path d="M10 19a2 2 0 0 0 4 0"/>',
|
||||
user: '<circle cx="12" cy="8" r="3.5"/><path d="M5 20a7 7 0 0 1 14 0"/>',
|
||||
|
||||
// ---- 功能/操作 ----
|
||||
plus: '<path d="M12 5v14"/><path d="M5 12h14"/>',
|
||||
search: '<circle cx="11" cy="11" r="6.5"/><path d="m20 20-3.6-3.6"/>',
|
||||
edit: '<path d="M4 20h4L19 9l-4-4L4 16v4Z"/><path d="m14 6 4 4"/>',
|
||||
trash: '<path d="M4 7h16"/><path d="M9 7V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/><path d="M6 7l1 13a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1l1-13"/><path d="M10 11v6"/><path d="M14 11v6"/>',
|
||||
close: '<path d="M6 6l12 12"/><path d="M18 6 6 18"/>',
|
||||
check: '<path d="m5 12.5 4.5 4.5L19 7"/>',
|
||||
arrowDown: '<path d="M12 5v14"/><path d="m6 13 6 6 6-6"/>',
|
||||
arrowLeft: '<path d="M19 12H5"/><path d="m11 6-6 6 6 6"/>',
|
||||
arrowRight: '<path d="M5 12h14"/><path d="m13 6 6 6-6 6"/>',
|
||||
arrowUp: '<path d="M12 19V5"/><path d="m6 11 6-6 6 6"/>',
|
||||
refresh: '<path d="M4 12a8 8 0 0 1 13.5-5.8L20 8"/><path d="M20 4v4h-4"/><path d="M20 12a8 8 0 0 1-13.5 5.8L4 16"/><path d="M4 20v-4h4"/>',
|
||||
more: '<circle cx="5" cy="12" r="1.4" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="1.4" fill="currentColor" stroke="none"/><circle cx="19" cy="12" r="1.4" fill="currentColor" stroke="none"/>',
|
||||
|
||||
// ---- 状态/反馈 ----
|
||||
star: '<path d="m12 3 2.7 5.7 6.3.9-4.6 4.4 1.1 6.3L12 17.3 6.5 20.3l1.1-6.3L3 9.6l6.3-.9L12 3Z"/>',
|
||||
starFilled: '<path d="m12 3 2.7 5.7 6.3.9-4.6 4.4 1.1 6.3L12 17.3 6.5 20.3l1.1-6.3L3 9.6l6.3-.9L12 3Z" fill="currentColor" stroke="none"/>',
|
||||
fire: '<path d="M12 3c1 3-1 4-1 6 0 1 1 2 2 2s2-1 2-3c2 2 3 4 3 7a6 6 0 0 1-12 0c0-3 2-5 3-6 1 1 1 2 2 2 1 0 1-2 1-5 0-1 0-2 0-3Z"/>',
|
||||
bookmark: '<path d="M6 4h12v16l-6-4-6 4V4Z"/>',
|
||||
warning: '<path d="M12 4 2.5 20h19L12 4Z"/><path d="M12 10v4"/><circle cx="12" cy="17" r="0.8" fill="currentColor" stroke="none"/>',
|
||||
info: '<circle cx="12" cy="12" r="9"/><path d="M12 11v5"/><circle cx="12" cy="7.5" r="0.8" fill="currentColor" stroke="none"/>',
|
||||
|
||||
// ---- 时间/地点/类型 ----
|
||||
clock: '<circle cx="12" cy="12" r="8.5"/><path d="M12 7v5l3.5 2"/>',
|
||||
location: '<path d="M12 21s7-6 7-11a7 7 0 0 0-14 0c0 5 7 11 7 11Z"/><circle cx="12" cy="10" r="2.5"/>',
|
||||
calendar: '<rect x="4" y="5" width="16" height="16" rx="2"/><path d="M4 9h16"/><path d="M8 3v4"/><path d="M16 3v4"/>',
|
||||
trophy: '<path d="M8 4h8v4a4 4 0 0 1-8 0V4Z"/><path d="M8 5H5v2a3 3 0 0 0 3 3"/><path d="M16 5h3v2a3 3 0 0 1-3 3"/><path d="M12 12v4"/><path d="M9 20h6"/><path d="M10 16h4l.5 4h-5l.5-4Z"/>',
|
||||
tag: '<path d="M3 5h7l9 9-7 7-9-9V5Z"/><circle cx="7.5" cy="9.5" r="1.2" fill="currentColor" stroke="none"/>',
|
||||
chart: '<path d="M4 20V4"/><path d="M4 20h16"/><path d="M8 16v-4"/><path d="M13 16V8"/><path d="M18 16v-7"/>',
|
||||
|
||||
// ---- 组队/语音 ----
|
||||
volume: '<path d="M4 9v6h4l5 4V5L8 9H4Z"/><path d="M16 9a3 3 0 0 1 0 6"/><path d="M18.5 7a6 6 0 0 1 0 10"/>',
|
||||
volumeOff: '<path d="M4 9v6h4l5 4V5L8 9H4Z"/><path d="m16 9 5 6"/><path d="m21 9-5 6"/>',
|
||||
speaker: '<path d="M4 9v6h4l5 4V5L8 9H4Z"/><path d="M16 8a5 5 0 0 1 0 8"/><path d="M18.5 5.5a9 9 0 0 1 0 13"/>',
|
||||
mic: '<rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5 11a7 7 0 0 0 14 0"/><path d="M12 18v3"/>',
|
||||
micOff: '<path d="M9 5a3 3 0 0 1 6 0v4"/><path d="M9 9v2a3 3 0 0 0 5 2"/><path d="M5 11a7 7 0 0 0 11 4"/><path d="M12 18v3"/><path d="m4 4 16 16"/>',
|
||||
|
||||
// ---- 群组/资产相关 ----
|
||||
wallet: '<rect x="3" y="6" width="18" height="13" rx="2"/><path d="M3 9h18"/><circle cx="16.5" cy="13" r="1.2" fill="currentColor" stroke="none"/>',
|
||||
gift: '<rect x="4" y="8" width="16" height="12" rx="1"/><path d="M4 12h16"/><path d="M12 8v12"/><path d="M12 8C12 5 10 4 8.5 4S6 6 8 8"/><path d="M12 8c0-3 2-4 3.5-4S18 6 16 8"/>',
|
||||
photo: '<rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="9" cy="11" r="2"/><path d="m3 17 5-4 4 3 3-2 6 4"/>',
|
||||
link: '<path d="M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1.5 1.5"/><path d="M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1.5-1.5"/>',
|
||||
download: '<path d="M12 4v11"/><path d="m7 11 5 5 5-5"/><path d="M5 20h14"/>',
|
||||
upload: '<path d="M12 16V5"/><path d="m7 9 5-5 5 5"/><path d="M5 20h14"/>',
|
||||
music: '<circle cx="6" cy="18" r="2.5"/><circle cx="18" cy="16" r="2.5"/><path d="M8.5 18V5l12-2v13"/>',
|
||||
video: '<rect x="3" y="6" width="13" height="12" rx="2"/><path d="m16 10 5-3v10l-5-3"/>',
|
||||
document: '<path d="M6 3h8l4 4v14H6V3Z"/><path d="M14 3v4h4"/><path d="M9 13h6"/><path d="M9 17h6"/>',
|
||||
setting: '<circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3M5 5l2 2M17 17l2 2M19 5l-2 2M7 17l-2 2"/>',
|
||||
logout: '<path d="M10 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h4"/><path d="M16 16l4-4-4-4"/><path d="M20 12H9"/>',
|
||||
|
||||
// ---- 状态点 ----
|
||||
dotIdle: '<circle cx="12" cy="12" r="5" fill="currentColor" stroke="none"/>',
|
||||
}
|
||||
|
||||
const innerSvg = computed(() => ICONS[props.name] || ICONS.info)
|
||||
|
||||
const wrapperStyle = computed(() => ({
|
||||
width: typeof props.size === 'number' ? `${props.size}px` : props.size,
|
||||
height: typeof props.size === 'number' ? `${props.size}px` : props.size,
|
||||
color: props.color
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
:style="wrapperStyle"
|
||||
class="mobile-icon"
|
||||
v-html="innerSvg"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mobile-icon {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -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 {
|
||||
>
|
||||
<div class="option-row">
|
||||
<span class="option-text">{{ opt.content }}</span>
|
||||
<span v-if="currentBet.resultOption === opt.id" class="winner-badge">🏆 胜出</span>
|
||||
<span v-if="currentBet.resultOption === opt.id" class="winner-badge"><MobileIcon name="trophy" :size="13" color="var(--gg-warning)" /> 胜出</span>
|
||||
</div>
|
||||
<div class="option-stats">
|
||||
<span>{{ optionEntryCount(opt.id) }} 注</span>
|
||||
@@ -239,7 +240,7 @@ function timeAgo(dateStr: string): string {
|
||||
:type="myEntry()?.won ? 'success' : 'warning'"
|
||||
wrapable
|
||||
>
|
||||
{{ myEntry()?.won ? `🎉 你赢了!` : '本次未中奖' }}
|
||||
{{ myEntry()?.won ? `你赢了!` : '本次未中奖' }}
|
||||
</van-notice-bar>
|
||||
</div>
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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() {
|
||||
<van-icon :name="expandedId === event.id ? 'arrow-up' : 'arrow-down'" class="expand-icon" />
|
||||
</div>
|
||||
<div class="event-title">{{ event.title }}</div>
|
||||
<div class="event-time">🕐 {{ formatDateTime(event.startTime) }}</div>
|
||||
<div v-if="event.location" class="event-loc">📍 {{ event.location }}</div>
|
||||
<div class="event-time"><MobileIcon name="clock" :size="13" /> {{ formatDateTime(event.startTime) }}</div>
|
||||
<div v-if="event.location" class="event-loc"><MobileIcon name="location" :size="13" /> {{ event.location }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 展开详情 -->
|
||||
@@ -245,7 +246,7 @@ async function onCreated() {
|
||||
<van-icon :name="expandedId === event.id ? 'arrow-up' : 'arrow-down'" class="expand-icon" />
|
||||
</div>
|
||||
<div class="event-title">{{ event.title }}</div>
|
||||
<div class="event-time">🕐 {{ formatDateTime(event.startTime) }}</div>
|
||||
<div class="event-time"><MobileIcon name="clock" :size="13" /> {{ formatDateTime(event.startTime) }}</div>
|
||||
</div>
|
||||
<div v-if="expandedId === event.id" class="event-detail">
|
||||
<p v-if="event.description" class="detail-desc">{{ event.description }}</p>
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
<div class="detail-meta">
|
||||
<van-tag v-if="game.platform" type="primary" size="medium">{{ game.platform }}</van-tag>
|
||||
<span class="popularity">🔥 {{ game.popularCount }} 人游玩</span>
|
||||
<span class="popularity"><MobileIcon name="fire" :size="13" color="var(--gg-warning)" /> {{ game.popularCount }} 人游玩</span>
|
||||
</div>
|
||||
|
||||
<div v-if="game.tags?.length" class="detail-tags">
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(() => {})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="activity-feed">
|
||||
<!-- 当前组队卡片 -->
|
||||
<div v-if="hasSession && sessionInThisGroup" class="current-team-card" @click="goVoiceRoom">
|
||||
<div class="team-header">
|
||||
<van-icon name="volume-o" />
|
||||
<span class="team-label">进行中</span>
|
||||
<div v-if="hasSession && sessionInThisGroup" class="current-team-card">
|
||||
<div class="team-card-body" @click="goVoiceRoom">
|
||||
<div class="team-header">
|
||||
<MobileIcon name="volume" :size="16" color="#fff" />
|
||||
<span class="team-label">{{ isPlaying ? '游戏中' : '招募中' }}</span>
|
||||
</div>
|
||||
<div class="team-game">{{ teamStore.currentSession?.gameName }}</div>
|
||||
<div class="team-meta">
|
||||
{{ teamStore.currentSession?.name }} · {{ teamStore.currentSession?.members?.length || 0 }} 人
|
||||
</div>
|
||||
<div class="team-go">
|
||||
<MobileIcon name="arrowRight" :size="14" color="#fff" />
|
||||
进入语音房
|
||||
</div>
|
||||
</div>
|
||||
<div class="team-game">{{ teamStore.currentSession?.gameName }}</div>
|
||||
<div class="team-meta">
|
||||
{{ teamStore.currentSession?.name }} · {{ teamStore.currentSession?.members?.length || 0 }} 人
|
||||
</div>
|
||||
<div class="team-go">进入语音房 →</div>
|
||||
<!-- 解散/结束按钮(招募中或游戏中时显示) -->
|
||||
<button
|
||||
v-if="isRecruiting || isPlaying"
|
||||
class="team-end-btn"
|
||||
:loading="ending"
|
||||
@click.stop="handleEndSession"
|
||||
>
|
||||
<MobileIcon name="close" :size="14" color="#fff" />
|
||||
{{ isRecruiting ? '解散' : '结束' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 快速组队按钮 -->
|
||||
@@ -130,10 +175,10 @@ function goVoiceRoom() {
|
||||
type="primary"
|
||||
block
|
||||
round
|
||||
icon="plus"
|
||||
@click="showCreateTeam = true"
|
||||
>
|
||||
发起组队
|
||||
<MobileIcon name="plus" :size="16" color="#fff" />
|
||||
<span style="margin-left: 4px;">发起组队</span>
|
||||
</van-button>
|
||||
|
||||
<!-- 成员状态分组 -->
|
||||
@@ -226,6 +271,11 @@ function goVoiceRoom() {
|
||||
padding: 16px;
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 16px rgba(5, 150, 105, 0.25);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.team-card-body:active {
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.team-header {
|
||||
@@ -249,12 +299,38 @@ function goVoiceRoom() {
|
||||
}
|
||||
|
||||
.team-go {
|
||||
text-align: right;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
margin-top: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 解散/结束按钮 */
|
||||
.team-end-btn {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 14px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
padding: 5px 10px;
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
border: 1px solid rgba(255, 255, 255, 0.35);
|
||||
border-radius: 14px;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.team-end-btn:active {
|
||||
background: rgba(239, 68, 68, 0.5);
|
||||
border-color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.members-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import MobileIcon from '@/components-mobile/MobileIcon.vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useGroupStore } from '@/stores/group'
|
||||
import { useTeamStore } from '@/stores/team'
|
||||
@@ -113,7 +114,7 @@ function goNotifications() {
|
||||
>
|
||||
<template #right>
|
||||
<van-badge :content="unreadCount > 0 ? unreadCount : ''" :show-zero="false">
|
||||
<van-icon name="bell" size="22" @click="goNotifications" />
|
||||
<MobileIcon name="bell" :size="22" @click="goNotifications" />
|
||||
</van-badge>
|
||||
</template>
|
||||
</van-nav-bar>
|
||||
@@ -125,11 +126,36 @@ function goNotifications() {
|
||||
|
||||
<!-- 底部 Tab -->
|
||||
<van-tabbar v-model="activeTab" @change="onTabChange" placeholder fixed>
|
||||
<van-tabbar-item name="home" icon="wap-home-o">首页</van-tabbar-item>
|
||||
<van-tabbar-item name="groups" icon="friends-o">群组</van-tabbar-item>
|
||||
<van-tabbar-item name="games" icon="game-o">游戏</van-tabbar-item>
|
||||
<van-tabbar-item name="notifications" icon="bell">通知</van-tabbar-item>
|
||||
<van-tabbar-item name="profile" icon="user-o">我的</van-tabbar-item>
|
||||
<van-tabbar-item name="home">
|
||||
<template #icon="{ active }">
|
||||
<MobileIcon name="home" :size="22" :color="active ? 'var(--gg-primary)' : 'var(--gg-text-muted)'" />
|
||||
</template>
|
||||
首页
|
||||
</van-tabbar-item>
|
||||
<van-tabbar-item name="groups">
|
||||
<template #icon="{ active }">
|
||||
<MobileIcon name="users" :size="22" :color="active ? 'var(--gg-primary)' : 'var(--gg-text-muted)'" />
|
||||
</template>
|
||||
群组
|
||||
</van-tabbar-item>
|
||||
<van-tabbar-item name="games">
|
||||
<template #icon="{ active }">
|
||||
<MobileIcon name="game" :size="22" :color="active ? 'var(--gg-primary)' : 'var(--gg-text-muted)'" />
|
||||
</template>
|
||||
游戏
|
||||
</van-tabbar-item>
|
||||
<van-tabbar-item name="notifications">
|
||||
<template #icon="{ active }">
|
||||
<MobileIcon name="bell" :size="22" :color="active ? 'var(--gg-primary)' : 'var(--gg-text-muted)'" />
|
||||
</template>
|
||||
通知
|
||||
</van-tabbar-item>
|
||||
<van-tabbar-item name="profile">
|
||||
<template #icon="{ active }">
|
||||
<MobileIcon name="user" :size="22" :color="active ? 'var(--gg-primary)' : 'var(--gg-text-muted)'" />
|
||||
</template>
|
||||
我的
|
||||
</van-tabbar-item>
|
||||
</van-tabbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -14,8 +14,8 @@ import MemoryGridMobile from '@/components-mobile/memory/MemoryGridMobile.vue'
|
||||
import StatsPanelMobile from '@/components-mobile/stats/StatsPanelMobile.vue'
|
||||
import BulletinListMobile from '@/components-mobile/bulletin/BulletinListMobile.vue'
|
||||
import EventListMobile from '@/components-mobile/event/EventListMobile.vue'
|
||||
import MobileIcon from '@/components-mobile/MobileIcon.vue'
|
||||
import Placeholder from '@/views-mobile/Placeholder.vue'
|
||||
import { Wallet, Box, Warning } from '@element-plus/icons-vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -103,15 +103,15 @@ function goBlacklist() {
|
||||
<!-- 快捷入口 -->
|
||||
<div class="quick-entry">
|
||||
<div class="entry-item" @click="goLedger">
|
||||
<el-icon><Wallet /></el-icon>
|
||||
<MobileIcon name="wallet" :size="20" color="var(--gg-primary)" />
|
||||
<span>账本</span>
|
||||
</div>
|
||||
<div class="entry-item" @click="goAssets">
|
||||
<el-icon><Box /></el-icon>
|
||||
<MobileIcon name="gift" :size="20" color="var(--gg-primary)" />
|
||||
<span>资产</span>
|
||||
</div>
|
||||
<div class="entry-item" @click="goBlacklist">
|
||||
<el-icon><Warning /></el-icon>
|
||||
<MobileIcon name="warning" :size="20" color="var(--gg-primary)" />
|
||||
<span>黑名单</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -228,8 +228,7 @@ function goBlacklist() {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.entry-item .el-icon {
|
||||
font-size: 20px;
|
||||
.entry-item .mobile-icon {
|
||||
color: var(--gg-primary);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,11 +44,11 @@ function selectGroup(groupId: string) {
|
||||
router.push({ name: 'GroupView', params: { id: groupId } })
|
||||
}
|
||||
|
||||
// 一键切换状态(空闲/离开/工作中循环)
|
||||
// 状态切换选项
|
||||
const statusActions = [
|
||||
{ status: 'idle' as const, label: '空闲', icon: '🟢' },
|
||||
{ status: 'away' as const, label: '离开', icon: '⚫' },
|
||||
{ status: 'working' as const, label: '工作中', icon: '🔴' },
|
||||
{ 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)
|
||||
@@ -191,12 +191,20 @@ function goSession() {
|
||||
</div>
|
||||
|
||||
<!-- 状态切换 ActionSheet -->
|
||||
<van-action-sheet
|
||||
v-model:show="showStatusSheet"
|
||||
title="切换状态"
|
||||
:actions="statusActions.map(a => ({ name: `${a.icon} ${a.label}`, status: a.status, label: a.label }))"
|
||||
@select="onStatusSelect"
|
||||
/>
|
||||
<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>
|
||||
|
||||
@@ -261,6 +269,21 @@ function goSession() {
|
||||
.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;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useGroupStore } from '@/stores/group'
|
||||
import { pb, isAuthenticated } from '@/api/pocketbase'
|
||||
import type { Group } from '@/types'
|
||||
import { showSuccessToast, showFailToast } from 'vant'
|
||||
import MobileIcon from '@/components-mobile/MobileIcon.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -75,7 +76,7 @@ async function handleJoin() {
|
||||
<div class="join-page">
|
||||
<div class="join-card">
|
||||
<div class="join-header">
|
||||
<div class="header-icon">🔗</div>
|
||||
<div class="header-icon"><MobileIcon name="link" :size="40" color="var(--gg-primary)" /></div>
|
||||
<h1>群组邀请</h1>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useGroupStore } from '@/stores/group'
|
||||
import { joinTeamSession, getTeamSession } from '@/api/sessions'
|
||||
import type { TeamSession } from '@/types'
|
||||
import { showSuccessToast, showFailToast } from 'vant'
|
||||
import MobileIcon from '@/components-mobile/MobileIcon.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -78,7 +79,7 @@ async function handleJoin() {
|
||||
<div class="join-page">
|
||||
<div class="join-card">
|
||||
<div class="join-header">
|
||||
<div class="header-icon">🎮</div>
|
||||
<div class="header-icon"><MobileIcon name="game" :size="40" color="var(--gg-primary)" /></div>
|
||||
<h1>组队邀请</h1>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useRouter, useRoute } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { pb } from '@/api/pocketbase'
|
||||
import { showFailToast } from 'vant'
|
||||
import MobileIcon from '@/components-mobile/MobileIcon.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -52,7 +53,7 @@ async function handleLogin() {
|
||||
<template>
|
||||
<div class="login-mobile">
|
||||
<div class="brand-area">
|
||||
<div class="brand-icon">🎮</div>
|
||||
<div class="brand-icon"><MobileIcon name="logo" :size="56" color="var(--gg-primary)" /></div>
|
||||
<h1 class="brand-title">Game Group</h1>
|
||||
<p class="brand-subtitle">组队开黑,一起嗨</p>
|
||||
</div>
|
||||
|
||||
@@ -19,9 +19,9 @@ const statusText = computed(() => UserStatusMap[userStore.userStatus] || '未知
|
||||
|
||||
const showStatusSheet = ref(false)
|
||||
const statusActions = [
|
||||
{ status: 'idle' as UserStatus, name: '🟢 空闲' },
|
||||
{ status: 'away' as UserStatus, name: '⚫ 离开' },
|
||||
{ status: 'working' as UserStatus, name: '🔴 工作中' },
|
||||
{ status: 'idle' as UserStatus, label: '空闲', color: 'var(--gg-success)' },
|
||||
{ status: 'away' as UserStatus, label: '离开', color: 'var(--gg-text-muted)' },
|
||||
{ status: 'working' as UserStatus, label: '工作中', color: 'var(--gg-danger)' },
|
||||
]
|
||||
|
||||
async function onStatusSelect(action: any) {
|
||||
@@ -110,12 +110,20 @@ function handleLogout() {
|
||||
</div>
|
||||
|
||||
<!-- 状态选择 -->
|
||||
<van-action-sheet
|
||||
v-model:show="showStatusSheet"
|
||||
title="切换状态"
|
||||
:actions="statusActions"
|
||||
@select="onStatusSelect"
|
||||
/>
|
||||
<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>
|
||||
|
||||
@@ -134,4 +142,12 @@ function handleLogout() {
|
||||
.stat-label { font-size: 12px; color: var(--gg-text-muted); margin-top: 2px; }
|
||||
|
||||
.logout-area { padding: 8px 28px; }
|
||||
|
||||
/* 状态切换面板 */
|
||||
.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; }
|
||||
</style>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ref, computed } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { showFailToast, showSuccessToast } from 'vant'
|
||||
import MobileIcon from '@/components-mobile/MobileIcon.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -62,7 +63,7 @@ async function handleRegister() {
|
||||
|
||||
<div class="form-area">
|
||||
<div class="intro">
|
||||
<div class="intro-icon">🎮</div>
|
||||
<div class="intro-icon"><MobileIcon name="logo" :size="48" color="var(--gg-primary)" /></div>
|
||||
<h2 class="intro-title">创建账号</h2>
|
||||
<p class="intro-desc">输入昵称开始你的组队之旅</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user