新增 DesktopShell 及桌面端首页、赛事、钱包、记录等页面,原 H5 视图迁移至 Mobile* 并由路由 wrapper 按视口切换。补齐右侧投注单栏、赔率快捷浮层、联赛/赛事侧栏、串关与定位能力;桌面下注成功改为全局 Toast,修复侧栏成功遮罩被裁剪与底部汇总黑底。同步悬浮客服、公告卡片、三语 i18n、桌面样式体系,以及 API 赛事与 shared 包相关调整。
184 lines
5.3 KiB
Vue
184 lines
5.3 KiB
Vue
<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>
|