feat(mobile): stage 6 - polls + bets

- migrate PollListMobile.vue (list + detail/vote + create + settle)
- migrate BetListMobile.vue (list + detail/place-bet + create + close/settle)
- GroupViewMobile: wire polls/bets tabs (replace placeholders)
- verified: polls API (8 fns) + bets API (8 fns) match uat

build verified: vue-tsc + vite build pass
This commit is contained in:
锦麟 王
2026-06-18 11:11:40 +08:00
parent 0ec868c949
commit 1cc23a0836
3 changed files with 879 additions and 1 deletions
@@ -0,0 +1,410 @@
<!-- src/components-mobile/bet/BetListMobile.vue -->
<!-- 手机端竞猜列表 + 详情 + 下注 + 创建 + 结算 -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { listBets, getBet, getBetOptions, getBetEntries, placeBet, createBet, closeBet, settleBet } from '@/api/bets'
import { useUserStore } from '@/stores/user'
import type { Bet, BetOption, BetEntry } from '@/types'
import { showSuccessToast, showFailToast, showConfirmDialog } from 'vant'
const props = defineProps<{ groupId: string }>()
const userStore = useUserStore()
const bets = ref<Bet[]>([])
const loading = ref(false)
const viewingBetId = ref<string | null>(null)
const currentBet = ref<Bet | null>(null)
const options = ref<BetOption[]>([])
const entries = ref<BetEntry[]>([])
const detailLoading = ref(false)
async function loadBets() {
loading.value = true
try {
bets.value = await listBets(props.groupId)
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
onMounted(async () => {
await loadBets()
})
async function viewBet(betId: string) {
viewingBetId.value = betId
detailLoading.value = true
try {
const [bet, opts, ents] = await Promise.all([
getBet(betId),
getBetOptions(betId),
getBetEntries(betId)
])
currentBet.value = bet
options.value = opts
entries.value = ents
} catch (e) {
showFailToast('加载失败')
} finally {
detailLoading.value = false
}
}
function backToList() {
viewingBetId.value = null
currentBet.value = null
loadBets()
}
function optionEntryCount(optionId: string): number {
return entries.value.filter(e => e.option === optionId).length
}
function optionTotalStake(optionId: string): number {
return entries.value.filter(e => e.option === optionId).reduce((sum, e) => sum + e.stake, 0)
}
const myEntry = () => entries.value.find(e => e.user === userStore.userId)
// 下注
const showBet = ref(false)
const selectedOption = ref<string>('')
const stakeAmount = ref(0)
const placeLoading = ref(false)
function openBetSheet(optionId: string) {
if (!currentBet.value || currentBet.value.status !== 'open') return
if (myEntry()) {
showFailToast('你已经下注了')
return
}
selectedOption.value = optionId
stakeAmount.value = currentBet.value.minStake
showBet.value = true
}
async function handlePlaceBet() {
if (!currentBet.value) return
if (stakeAmount.value < currentBet.value.minStake || stakeAmount.value > currentBet.value.maxStake) {
showFailToast(`下注范围 ${currentBet.value.minStake}-${currentBet.value.maxStake}`)
return
}
placeLoading.value = true
try {
await placeBet(currentBet.value.id, selectedOption.value, stakeAmount.value)
showSuccessToast('下注成功')
showBet.value = false
await viewBet(currentBet.value.id)
} catch (e: any) {
showFailToast(e.message || '下注失败')
} finally {
placeLoading.value = false
}
}
// 创建竞猜
const showCreate = ref(false)
const createForm = ref({
title: '',
description: '',
optionsText: '',
minStake: 1,
maxStake: 100,
deadline: ''
})
const createLoading = ref(false)
async function handleCreate() {
if (!createForm.value.title.trim()) {
showFailToast('请输入标题')
return
}
const opts = createForm.value.optionsText.split('\n').map(s => s.trim()).filter(Boolean)
if (opts.length < 2) {
showFailToast('至少 2 个选项')
return
}
if (!createForm.value.deadline) {
showFailToast('请设置截止时间')
return
}
createLoading.value = true
try {
await createBet({
group: props.groupId,
title: createForm.value.title.trim(),
description: createForm.value.description.trim() || undefined,
options: opts,
minStake: createForm.value.minStake,
maxStake: createForm.value.maxStake,
deadline: createForm.value.deadline
})
showSuccessToast('创建成功')
showCreate.value = false
createForm.value = { title: '', description: '', optionsText: '', minStake: 1, maxStake: 100, deadline: '' }
await loadBets()
} catch (e: any) {
showFailToast(e.message || '创建失败')
} finally {
createLoading.value = false
}
}
// 关闭/结算(创建者)
async function handleClose() {
if (!currentBet.value) return
showConfirmDialog({ title: '关闭竞猜', message: '关闭后将停止下注,确定?' })
.then(async () => {
try {
await closeBet(currentBet.value!.id)
showSuccessToast('已关闭')
await viewBet(currentBet.value!.id)
} catch (e: any) {
showFailToast(e.message || '操作失败')
}
}).catch(() => {})
}
const showSettle = ref(false)
const settleOption = ref('')
async function handleSettle() {
if (!currentBet.value || !settleOption.value) return
try {
await settleBet(currentBet.value.id, settleOption.value)
showSuccessToast('已结算')
showSettle.value = false
await viewBet(currentBet.value.id)
} catch (e: any) {
showFailToast(e.message || '结算失败')
}
}
const isCreator = () => currentBet.value?.creator === userStore.userId
function timeAgo(dateStr: string): string {
const min = Math.floor((Date.now() - new Date(dateStr).getTime()) / 60000)
if (min < 60) return `${min}分钟前`
const hour = Math.floor(min / 60)
if (hour < 24) return `${hour}小时前`
return new Date(dateStr).toLocaleDateString('zh-CN')
}
</script>
<template>
<div class="bet-mobile">
<!-- 详情 -->
<div v-if="viewingBetId && currentBet">
<van-nav-bar :title="currentBet.title" left-arrow @click-left="backToList" />
<div v-if="detailLoading" class="loading-box">
<van-loading size="24px">加载中...</van-loading>
</div>
<div v-else class="detail-content">
<div class="detail-header">
<van-tag :type="currentBet.status === 'open' ? 'success' : currentBet.status === 'closed' ? 'warning' : 'default'">
{{ currentBet.status === 'open' ? '下注中' : currentBet.status === 'closed' ? '待结算' : '已结算' }}
</van-tag>
<span class="detail-meta">范围 {{ currentBet.minStake }}-{{ currentBet.maxStake }} 积分</span>
</div>
<p v-if="currentBet.description" class="detail-desc">{{ currentBet.description }}</p>
<div class="options-list">
<div
v-for="opt in options"
:key="opt.id"
class="option-item"
@click="currentBet.status === 'open' && openBetSheet(opt.id)"
>
<div class="option-row">
<span class="option-text">{{ opt.content }}</span>
<span v-if="currentBet.resultOption === opt.id" class="winner-badge">🏆 胜出</span>
</div>
<div class="option-stats">
<span>{{ optionEntryCount(opt.id) }} </span>
<span>{{ optionTotalStake(opt.id) }} 积分</span>
</div>
</div>
</div>
<!-- 我的结果 -->
<div v-if="currentBet.status === 'settled' && myEntry()" class="my-result">
<van-notice-bar
:type="myEntry()?.won ? 'success' : 'warning'"
wrapable
>
{{ myEntry()?.won ? `🎉 你赢了!` : '本次未中奖' }}
</van-notice-bar>
</div>
<!-- 创建者操作 -->
<div v-if="isCreator()" class="detail-actions">
<van-button v-if="currentBet.status === 'open'" type="warning" block round plain @click="handleClose">
关闭下注
</van-button>
<van-button v-if="currentBet.status === 'closed'" type="primary" block round @click="showSettle = true">
结算
</van-button>
</div>
</div>
</div>
<!-- 列表 -->
<template v-else>
<div class="list-header">
<van-button type="primary" size="small" round icon="plus" @click="showCreate = true">
发起竞猜
</van-button>
</div>
<van-pull-refresh v-model="loading" @refresh="loadBets">
<div v-if="bets.length === 0 && !loading" class="empty">
<van-empty description="暂无竞猜" image-size="100" />
</div>
<div class="bet-list">
<div
v-for="bet in bets"
:key="bet.id"
class="bet-card"
@click="viewBet(bet.id)"
>
<div class="bet-card-header">
<van-tag :type="bet.status === 'open' ? 'success' : bet.status === 'closed' ? 'warning' : 'default'" size="medium">
{{ bet.status === 'open' ? '下注中' : bet.status === 'closed' ? '待结算' : '已结算' }}
</van-tag>
<span class="bet-stake">{{ bet.minStake }}-{{ bet.maxStake }} 积分</span>
</div>
<div class="bet-title">{{ bet.title }}</div>
<div class="bet-card-footer">
<span class="bet-time">{{ timeAgo(bet.created) }}</span>
<van-icon name="arrow" />
</div>
</div>
</div>
</van-pull-refresh>
</template>
<!-- 下注弹层 -->
<van-action-sheet v-model:show="showBet" title="下注">
<div class="bet-sheet-content">
<div class="stake-display">
<span class="stake-num">{{ stakeAmount }}</span>
<span class="stake-unit">积分</span>
</div>
<van-stepper
v-model="stakeAmount"
:min="currentBet?.minStake"
:max="currentBet?.maxStake"
integer
/>
<div class="bet-sheet-actions">
<van-button type="primary" block round :loading="placeLoading" @click="handlePlaceBet">
确认下注
</van-button>
</div>
</div>
</van-action-sheet>
<!-- 创建弹层 -->
<van-popup v-model:show="showCreate" position="bottom" round closeable :style="{ height: '85%' }">
<div class="popup-content">
<div class="popup-title">发起竞猜</div>
<van-cell-group inset>
<van-field v-model="createForm.title" label="标题" placeholder="竞猜主题" required />
<van-field v-model="createForm.description" label="说明" placeholder="可选" />
<van-field
v-model="createForm.optionsText"
type="textarea"
label="选项"
placeholder="每行一个选项"
rows="3"
/>
<van-field name="stepper" label="最低下注">
<template #input><van-stepper v-model="createForm.minStake" min="1" /></template>
</van-field>
<van-field name="stepper" label="最高下注">
<template #input><van-stepper v-model="createForm.maxStake" min="1" /></template>
</van-field>
<van-field v-model="createForm.deadline" label="截止时间" placeholder="如 2026-12-31 23:59" required />
</van-cell-group>
<div class="popup-actions">
<van-button type="primary" block round :loading="createLoading" @click="handleCreate">
创建
</van-button>
</div>
</div>
</van-popup>
<!-- 结算弹层 -->
<van-action-sheet v-model:show="showSettle" title="选择胜出选项">
<div class="settle-content">
<div
v-for="opt in options"
:key="opt.id"
class="settle-option"
:class="{ 'settle-selected': settleOption === opt.id }"
@click="settleOption = opt.id"
>
{{ opt.content }}
</div>
<div class="settle-actions">
<van-button type="primary" block round @click="handleSettle">确认结算</van-button>
</div>
</div>
</van-action-sheet>
</div>
</template>
<style scoped>
.bet-mobile { padding: 12px; }
.loading-box { display: flex; justify-content: center; padding: 40px; }
.empty { padding: 30px 0; }
.list-header { display: flex; justify-content: flex-end; margin-bottom: 10px; }
.bet-list { display: flex; flex-direction: column; gap: 10px; }
.bet-card { background: var(--gg-bg-card); border-radius: var(--gg-radius-md); padding: 14px; box-shadow: var(--gg-shadow); }
.bet-card:active { opacity: 0.85; }
.bet-card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.bet-stake { font-size: 12px; color: var(--gg-text-muted); }
.bet-title { font-size: 16px; font-weight: 600; color: var(--gg-text); }
.bet-card-footer { display: flex; align-items: center; justify-content: space-between; margin-top: 10px; color: var(--gg-text-muted); font-size: 13px; }
.detail-content { padding: 12px; }
.detail-header { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
.detail-meta { font-size: 12px; color: var(--gg-text-muted); }
.detail-desc { font-size: 14px; color: var(--gg-text-secondary); margin: 0 0 16px; line-height: 1.5; }
.options-list { display: flex; flex-direction: column; gap: 10px; }
.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; }
.option-stats { display: flex; gap: 16px; margin-top: 8px; font-size: 12px; color: var(--gg-text-muted); }
.my-result { margin-top: 16px; }
.detail-actions { margin-top: 20px; }
/* 下注弹层 */
.bet-sheet-content { padding: 24px; display: flex; flex-direction: column; align-items: center; gap: 20px; }
.stake-display { display: flex; align-items: baseline; gap: 4px; }
.stake-num { font-size: 40px; font-weight: 700; color: var(--gg-primary); }
.stake-unit { font-size: 14px; color: var(--gg-text-muted); }
.bet-sheet-actions { width: 100%; }
/* 弹层通用 */
.popup-content { padding: 16px 0 24px; display: flex; flex-direction: column; height: 100%; overflow-y: auto; }
.popup-title { text-align: center; font-size: 16px; font-weight: 600; padding: 8px 0 16px; }
.popup-actions { padding: 20px 16px 0; }
.settle-content { padding: 16px; }
.settle-option { padding: 14px; background: var(--gg-bg); border-radius: var(--gg-radius-sm); margin-bottom: 8px; text-align: center; border: 1px solid var(--gg-border); font-size: 14px; }
.settle-selected { background: rgba(5, 150, 105, 0.1); border-color: var(--gg-primary); color: var(--gg-primary); font-weight: 600; }
.settle-actions { margin-top: 16px; }
</style>
@@ -0,0 +1,463 @@
<!-- src/components-mobile/poll/PollListMobile.vue -->
<!-- 手机端投票列表 + 详情 + 创建 -->
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { listPolls, getPoll, getPollOptions, getPollVotes, getUserVote, votePoll, createPoll, settlePoll } from '@/api/polls'
import type { Poll, PollOption, PollVote } from '@/types'
import { showSuccessToast, showFailToast, showConfirmDialog } from 'vant'
const props = defineProps<{ groupId: string }>()
const polls = ref<Poll[]>([])
const loading = ref(false)
const viewingPollId = ref<string | null>(null)
// 详情数据
const currentPoll = ref<Poll | null>(null)
const options = ref<PollOption[]>([])
const votes = ref<PollVote[]>([])
const userVote = ref<PollVote | null>(null)
const detailLoading = ref(false)
let unsub: (() => void) | null = null
async function loadPolls() {
loading.value = true
try {
polls.value = await listPolls(props.groupId)
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
onMounted(async () => {
await loadPolls()
})
onUnmounted(() => {
if (unsub) { (unsub as () => void)() }
})
async function viewPoll(pollId: string) {
viewingPollId.value = pollId
detailLoading.value = true
try {
const [poll, opts, allVotes, myVote] = await Promise.all([
getPoll(pollId),
getPollOptions(pollId),
getPollVotes(pollId),
getUserVote(pollId)
])
currentPoll.value = poll
options.value = opts
votes.value = allVotes
userVote.value = myVote
} catch (e) {
showFailToast('加载失败')
} finally {
detailLoading.value = false
}
}
function backToList() {
viewingPollId.value = null
currentPoll.value = null
loadPolls()
}
// 每个选项的票数
function optionVoteCount(optionId: string): number {
return votes.value.filter(v => v.option === optionId).length
}
function totalVotes(): number {
return votes.value.length || 1
}
async function doVote(optionId: string) {
if (!currentPoll.value || currentPoll.value.status === 'settled') return
if (userVote.value) {
showFailToast('你已经投过票了')
return
}
try {
await votePoll(currentPoll.value.id, optionId)
showSuccessToast('投票成功')
await viewPoll(currentPoll.value.id)
} catch (e: any) {
showFailToast(e.message || '投票失败')
}
}
async function doSettle() {
if (!currentPoll.value) return
showConfirmDialog({
title: '结算投票',
message: '结算后将结束投票,确定吗?'
}).then(async () => {
try {
await settlePoll(currentPoll.value!.id)
showSuccessToast('已结算')
await viewPoll(currentPoll.value!.id)
} catch (e: any) {
showFailToast(e.message || '结算失败')
}
}).catch(() => {})
}
// 创建投票
const showCreate = ref(false)
const createForm = ref({
title: '',
type: 'option' as 'option' | 'rollcall',
anonymous: false,
deadline: '',
optionsText: ''
})
const createLoading = ref(false)
async function handleCreate() {
if (!createForm.value.title.trim()) {
showFailToast('请输入标题')
return
}
const opts = createForm.value.optionsText.split('\n').map(s => s.trim()).filter(Boolean)
if (opts.length < 2) {
showFailToast('至少输入 2 个选项(每行一个)')
return
}
createLoading.value = true
try {
await createPoll({
group: props.groupId,
title: createForm.value.title.trim(),
type: createForm.value.type,
anonymous: createForm.value.anonymous,
deadline: createForm.value.deadline || undefined,
options: opts
})
showSuccessToast('创建成功')
showCreate.value = false
createForm.value = { title: '', type: 'option', anonymous: false, deadline: '', optionsText: '' }
await loadPolls()
} catch (e: any) {
showFailToast(e.message || '创建失败')
} finally {
createLoading.value = false
}
}
function timeAgo(dateStr: string): string {
const min = Math.floor((Date.now() - new Date(dateStr).getTime()) / 60000)
if (min < 60) return `${min}分钟前`
const hour = Math.floor(min / 60)
if (hour < 24) return `${hour}小时前`
return new Date(dateStr).toLocaleDateString('zh-CN')
}
</script>
<template>
<div class="poll-mobile">
<!-- 详情视图 -->
<div v-if="viewingPollId && currentPoll" class="poll-detail">
<van-nav-bar
:title="currentPoll.title"
left-arrow
@click-left="backToList"
/>
<div v-if="detailLoading" class="loading-box">
<van-loading size="24px">加载中...</van-loading>
</div>
<div v-else class="detail-content">
<div class="detail-header">
<van-tag :type="currentPoll.status === 'settled' ? 'default' : 'success'">
{{ currentPoll.status === 'settled' ? '已结算' : '进行中' }}
</van-tag>
<span class="detail-meta">{{ votes.length }} 人参与 · {{ timeAgo(currentPoll.created) }}</span>
</div>
<div class="options-list">
<div
v-for="opt in options"
:key="opt.id"
class="option-item"
:class="{
'option-voted': userVote?.option === opt.id,
'option-disabled': currentPoll.status === 'settled' || !!userVote
}"
@click="doVote(opt.id)"
>
<div class="option-bar">
<div
class="option-bar-fill"
:style="{ width: `${(optionVoteCount(opt.id) / totalVotes()) * 100}%` }"
/>
</div>
<div class="option-content">
<span class="option-text">{{ opt.content }}</span>
<span class="option-count">{{ optionVoteCount(opt.id) }} </span>
</div>
</div>
</div>
<div v-if="currentPoll.status === 'active'" class="detail-actions">
<van-button
type="warning"
block
round
plain
@click="doSettle"
>
结算投票
</van-button>
</div>
</div>
</div>
<!-- 列表视图 -->
<template v-else>
<div class="list-header">
<van-button type="primary" size="small" round icon="plus" @click="showCreate = true">
发起投票
</van-button>
</div>
<van-pull-refresh v-model="loading" @refresh="loadPolls">
<div v-if="polls.length === 0 && !loading" class="empty">
<van-empty description="暂无投票" image-size="100" />
</div>
<div class="poll-list">
<div
v-for="poll in polls"
:key="poll.id"
class="poll-card"
@click="viewPoll(poll.id)"
>
<div class="poll-card-header">
<van-tag :type="poll.status === 'settled' ? 'default' : 'success'" size="medium">
{{ poll.status === 'settled' ? '已结算' : '进行中' }}
</van-tag>
<span class="poll-time">{{ timeAgo(poll.created) }}</span>
</div>
<div class="poll-title">{{ poll.title }}</div>
<div class="poll-card-footer">
<span class="poll-type">{{ poll.type === 'rollcall' ? '点名' : '选项投票' }}</span>
<van-icon name="arrow" />
</div>
</div>
</div>
</van-pull-refresh>
</template>
<!-- 创建弹层 -->
<van-popup
v-model:show="showCreate"
position="bottom"
round
closeable
:style="{ height: '80%' }"
>
<div class="popup-content">
<div class="popup-title">发起投票</div>
<van-cell-group inset>
<van-field v-model="createForm.title" label="标题" placeholder="投票主题" required />
<van-field name="radio" label="类型">
<template #input>
<van-radio-group v-model="createForm.type" direction="horizontal">
<van-radio name="option">选项投票</van-radio>
<van-radio name="rollcall">点名</van-radio>
</van-radio-group>
</template>
</van-field>
<van-field
v-model="createForm.optionsText"
type="textarea"
label="选项"
placeholder="每行输入一个选项"
rows="4"
/>
<van-field
v-model="createForm.deadline"
label="截止时间"
placeholder="留空则不截止"
/>
<van-cell title="匿名投票" center>
<template #right-icon>
<van-switch v-model="createForm.anonymous" size="22px" />
</template>
</van-cell>
</van-cell-group>
<div class="popup-actions">
<van-button type="primary" block round :loading="createLoading" @click="handleCreate">
创建
</van-button>
</div>
</div>
</van-popup>
</div>
</template>
<style scoped>
.poll-mobile {
padding: 12px;
}
.loading-box {
display: flex;
justify-content: center;
padding: 40px;
}
.empty {
padding: 30px 0;
}
.list-header {
display: flex;
justify-content: flex-end;
margin-bottom: 10px;
}
.poll-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.poll-card {
background: var(--gg-bg-card);
border-radius: var(--gg-radius-md);
padding: 14px;
box-shadow: var(--gg-shadow);
}
.poll-card:active {
opacity: 0.85;
}
.poll-card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.poll-time {
font-size: 12px;
color: var(--gg-text-muted);
}
.poll-title {
font-size: 16px;
font-weight: 600;
color: var(--gg-text);
}
.poll-card-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 10px;
color: var(--gg-text-muted);
font-size: 13px;
}
/* 详情 */
.detail-content {
padding: 12px;
}
.detail-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
}
.detail-meta {
font-size: 12px;
color: var(--gg-text-muted);
}
.options-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.option-item {
position: relative;
background: var(--gg-bg-card);
border-radius: var(--gg-radius-sm);
overflow: hidden;
border: 1px solid var(--gg-border);
}
.option-voted {
border-color: var(--gg-primary);
}
.option-bar {
position: absolute;
inset: 0;
}
.option-bar-fill {
height: 100%;
background: rgba(5, 150, 105, 0.08);
transition: width 0.3s;
}
.option-content {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 14px;
}
.option-text {
font-size: 14px;
color: var(--gg-text);
font-weight: 500;
}
.option-count {
font-size: 13px;
color: var(--gg-primary);
font-weight: 600;
}
.option-disabled {
opacity: 0.9;
}
.detail-actions {
margin-top: 20px;
}
/* 弹层 */
.popup-content {
padding: 16px 0 24px;
display: flex;
flex-direction: column;
height: 100%;
overflow-y: auto;
}
.popup-title {
text-align: center;
font-size: 16px;
font-weight: 600;
padding: 8px 0 16px;
}
.popup-actions {
padding: 20px 16px 0;
}
</style>
@@ -8,6 +8,8 @@ import { useGroupStore } from '@/stores/group'
import { pb } from '@/api/pocketbase'
import ActivityFeedMobile from '@/components-mobile/group/ActivityFeedMobile.vue'
import MemberListMobile from '@/components-mobile/group/MemberListMobile.vue'
import PollListMobile from '@/components-mobile/poll/PollListMobile.vue'
import BetListMobile from '@/components-mobile/bet/BetListMobile.vue'
import Placeholder from '@/views-mobile/Placeholder.vue'
import { Wallet, Box, Warning } from '@element-plus/icons-vue'
@@ -123,7 +125,10 @@ function goBlacklist() {
<!-- 阶段 4 已迁移 -->
<ActivityFeedMobile v-if="activeTab === 'activity'" :group-id="groupId" />
<MemberListMobile v-else-if="activeTab === 'members'" :group-id="groupId" />
<!-- 以下 tab 子组件在阶段 6/9 迁移后接入现暂用占位 -->
<!-- 阶段 6 迁移 -->
<PollListMobile v-else-if="activeTab === 'polls'" :group-id="groupId" />
<BetListMobile v-else-if="activeTab === 'bets'" :group-id="groupId" />
<!-- 以下 tab 子组件在阶段 9 迁移后接入现暂用占位 -->
<Placeholder v-else />
</div>
</van-tab>