feat(player): theme-3 移动端/PC 双端拆分与桌面投注工作台

新增 DesktopShell 及桌面端首页、赛事、钱包、记录等页面,原 H5 视图迁移至 Mobile* 并由路由 wrapper 按视口切换。补齐右侧投注单栏、赔率快捷浮层、联赛/赛事侧栏、串关与定位能力;桌面下注成功改为全局 Toast,修复侧栏成功遮罩被裁剪与底部汇总黑底。同步悬浮客服、公告卡片、三语 i18n、桌面样式体系,以及 API 赛事与 shared 包相关调整。
This commit is contained in:
mars
2026-07-07 17:07:38 +08:00
parent fee34feee8
commit e7e2b77906
125 changed files with 26393 additions and 8687 deletions

View File

@@ -0,0 +1,183 @@
<script setup lang="ts">
import { computed, ref, onMounted, onActivated, watch } from 'vue';
import { useI18n } from 'vue-i18n';
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, formatMoneyCompact } from '../../utils/localeDisplay';
import { parseCashbackApiData, type CashbackRecord } from '../../utils/cashback';
const COL_COUNT = 6;
const { t, locale } = useI18n();
const allItems = ref<CashbackRecord[]>([]);
const loading = ref(true);
const page = ref(1);
const pageSize = ref(20);
const total = computed(() => allItems.value.length);
const items = computed(() => {
const start = (page.value - 1) * pageSize.value;
return allItems.value.slice(start, start + pageSize.value);
});
const totalAmount = computed(() =>
allItems.value.reduce((sum, row) => sum + Math.abs(parseFloat(row.amount) || 0), 0),
);
function formatPeriod(start: string, end: string) {
const opts: Intl.DateTimeFormatOptions = { month: '2-digit', day: '2-digit' };
const s = new Date(start).toLocaleDateString(locale.value, opts);
const e = new Date(end).toLocaleDateString(locale.value, opts);
return `${s} ${e}`;
}
function formatRate(rate: string) {
const n = parseFloat(rate);
if (!Number.isFinite(n)) return rate;
return `${(n * 100).toFixed(2)}%`;
}
function formatTime(value: string | null) {
if (!value) return '—';
return new Date(value).toLocaleString(locale.value, {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false,
});
}
async function loadData() {
loading.value = true;
try {
const { data } = await api.get('/player/cashbacks');
allItems.value = parseCashbackApiData(data.data);
const maxPage = Math.max(1, Math.ceil(allItems.value.length / pageSize.value));
if (page.value > maxPage) page.value = maxPage;
} catch {
allItems.value = [];
page.value = 1;
} finally {
loading.value = false;
}
}
function onPageChange(p: number) {
page.value = p;
}
watch(pageSize, () => {
page.value = 1;
});
onMounted(loadData);
onActivated(loadData);
</script>
<template>
<div class="desktop-records-page">
<header class="desktop-records-header">
<div class="desktop-records-header-main">
<DesktopWalletSubNav />
</div>
<div v-if="!loading && allItems.length" class="desktop-records-actions summary-chip">
<span class="summary-label">{{ t('cashback.total_received') }}</span>
<span class="summary-value">{{ formatMoney(totalAmount, locale) }}</span>
</div>
</header>
<section class="desktop-records-panel">
<div class="desktop-records-table-wrap">
<table class="desktop-records-table">
<thead>
<tr>
<th>{{ t('cashback.period') }}</th>
<th>{{ t('cashback.effective_stake') }}</th>
<th>{{ t('cashback.rate') }}</th>
<th class="num-th">{{ t('cashback.amount') }}</th>
<th>{{ t('cashback.batch_no') }}</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="row in items" :key="row.id" class="hover-row">
<td>{{ formatPeriod(row.periodStart, row.periodEnd) }}</td>
<td>{{ formatMoneyCompact(row.effectiveStake, locale) }}</td>
<td>{{ formatRate(row.rate) }}</td>
<td class="num-cell desktop-records-amount pos">{{ formatMoney(row.amount, locale) }}</td>
<td class="batch-cell">{{ row.batchNo }}</td>
<td class="date-cell">{{ formatTime(row.confirmedAt ?? row.createdAt) }}</td>
</tr>
</template>
<tr v-else class="state-row">
<td :colspan="COL_COUNT" class="desktop-records-table-empty">
<div>{{ t('cashback.empty') }}</div>
<p class="empty-hint">{{ t('cashback.empty_hint') }}</p>
</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="onPageChange"
/>
</div>
</section>
</div>
</template>
<style scoped>
.summary-chip {
display: inline-flex;
align-items: baseline;
gap: 8px;
padding: 8px 14px;
border: 1px solid rgba(244, 162, 97, 0.22);
border-radius: 8px;
background: rgba(244, 162, 97, 0.08);
}
.summary-label {
font-size: 12px;
color: var(--text-muted);
font-weight: 700;
}
.summary-value {
font-size: 16px;
font-weight: 800;
color: var(--primary-light);
font-variant-numeric: tabular-nums;
}
.batch-cell {
font-family: 'SF Mono', Consolas, monospace;
font-size: 12px;
color: var(--text-muted);
}
.empty-hint {
margin: 8px 0 0;
font-size: 13px;
font-weight: 500;
line-height: 1.6;
color: var(--text-muted);
}
</style>