feat(player): 新增桌面端 UI 与响应式双端路由架构

- 将原移动端页面重命名为 Mobile*,新增 22 个 Desktop* 视图与 DesktopShell 布局体系
- 路由入口改为 Wrapper 视图,通过 useViewport(≥1024px)自动切换移动/桌面端
- 新增投注单面板、盘口弹层、联赛侧栏、数据表格等桌面组件及独立样式令牌
- 扩展 betSlip store、串关筛选、冠军盘、Toast/悬浮信箱等交互与三语 i18n
- 公告/消息 Hub 拆分移动与桌面实现;API 投注/钱包/充值记录支持 pageSize 分页
This commit is contained in:
2026-06-29 18:01:39 +08:00
parent 9c5e8d6f5c
commit 8a1ba0fd95
116 changed files with 22249 additions and 8693 deletions

View File

@@ -0,0 +1,145 @@
<script setup lang="ts">
import { ref, onMounted, onActivated } from 'vue';
import { useI18n } from 'vue-i18n';
import { RouterLink } from 'vue-router';
import api from '../../api';
import GoldSpinner from '../../components/GoldSpinner.vue';
import Pagination from '../../components/desktop/Pagination.vue';
import DesktopWalletSubNav from '../../components/desktop/DesktopWalletSubNav.vue';
import { formatMoney } from '../../utils/localeDisplay';
const COL_COUNT = 5;
const { t, locale } = useI18n();
type RechargeOrder = {
id: string;
orderNo: string;
amount: string;
status: string;
methodType?: string;
bankName?: string | null;
paymentMethodName?: string | null;
approvedAmount?: string | null;
createdAt: string;
};
const items = ref<RechargeOrder[]>([]);
const loading = ref(true);
const page = ref(1);
const pageSize = ref(20);
const total = ref(0);
async function loadPage(p: number) {
loading.value = true;
try {
const { data } = await api.get('/player/deposit-orders', { params: { page: p, pageSize: pageSize.value } });
const result = data.data ?? { items: [], total: 0, pageSize: pageSize.value };
total.value = result.total ?? 0;
items.value = result.items ?? [];
page.value = p;
} catch {
if (p === 1) items.value = [];
total.value = 0;
} finally {
loading.value = false;
}
}
onMounted(() => loadPage(1));
onActivated(() => loadPage(page.value));
function methodLabel(order: RechargeOrder) {
if (order.methodType === 'USDT') return 'USDT';
return order.paymentMethodName || order.bankName || order.methodType || '-';
}
function statusClass(status: string) {
const map: Record<string, string> = {
APPROVED: 'status-won',
REJECTED: 'status-lost',
PENDING: 'status-pending',
};
return map[status?.toUpperCase()] ?? 'status-default';
}
function statusLabel(status: string) {
const s = status?.toUpperCase();
if (s === 'APPROVED') return t('recharge.status_approved');
if (s === 'REJECTED') return t('recharge.status_rejected');
return t('recharge.status_pending');
}
</script>
<template>
<div class="desktop-records-page">
<header class="desktop-records-header">
<div class="desktop-records-header-main">
<DesktopWalletSubNav />
</div>
<div class="desktop-records-actions">
<RouterLink to="/wallet/recharge" class="desktop-records-action-btn">
{{ t('wallet.recharge_btn') }}
</RouterLink>
</div>
</header>
<section class="desktop-records-panel">
<div class="desktop-records-table-wrap">
<table class="desktop-records-table">
<thead>
<tr>
<th>{{ t('recharge.order_no') }}</th>
<th>{{ t('recharge.method') }}</th>
<th class="num-th">{{ t('wallet.amount') }}</th>
<th>{{ t('common.status') }}</th>
<th>{{ t('wallet.time') }}</th>
</tr>
</thead>
<tbody>
<tr v-if="loading" class="state-row">
<td :colspan="COL_COUNT">
<div class="desktop-records-loading-cell">
<GoldSpinner :size="36" />
</div>
</td>
</tr>
<template v-else-if="items.length">
<tr v-for="order in items" :key="order.id" class="hover-row">
<td class="order-no">{{ order.orderNo || order.id }}</td>
<td>{{ methodLabel(order) }}</td>
<td class="num-cell">{{ formatMoney(order.amount, locale) }}</td>
<td>
<span class="desktop-records-status" :class="statusClass(order.status)">
{{ statusLabel(order.status) }}
</span>
</td>
<td class="date-cell">{{ new Date(order.createdAt).toLocaleString() }}</td>
</tr>
</template>
<tr v-else class="state-row">
<td :colspan="COL_COUNT" class="desktop-records-table-empty">
{{ t('recharge.no_orders') }}
</td>
</tr>
</tbody>
</table>
</div>
<div v-if="!loading && total > 0" class="desktop-records-pagination">
<Pagination
v-model="page"
v-model:pageSize="pageSize"
:total="total"
@change="loadPage"
/>
</div>
</section>
</div>
</template>
<style scoped>
.order-no {
font-weight: 700;
color: var(--primary-light);
}
</style>