Files
thebet365/apps/player/src/views/WalletTransactionDetailView.vue
Mars 4a93fcfce0 feat(player): 全站 UI 主题重构与布局优化 — 从暗金奢华风格迁移至 Pinnacle 风格蓝白专业主题
一、全局主题重构(52 文件)
  - 设计体系从深色/金色奢华调性全面迁移至 Pinnacle 风格的清爽专业调性
  - 主色:深海蓝 #003D6B,辅色:橙色 #F8971F / #E8870A(赔率强调色)
  - 背景从纯黑/深灰改为白色/浅灰 (#F5F7FA, #E8EDF2)
  - 更新 CSS 变量设计令牌体系:--primary, --border, --text-muted, --bg-body, --bg-hover, --radius-sm 等
  - styles.css 全量重写,所有组件 scoped CSS 同步适配

二、紧凑布局优化
  - 首页:BannerCarousel 轮播高度缩减 (clamp 148→120px),MatchBetCard 内边距/间距/字号全面收紧
  - 首页:赛事卡片 padding 14px→10px,队旗尺寸 72×48→56×38,VS 区域缩小
  - 个人中心:钱包横幅宽高比 2/1→1.75/1,设置单元格 min-height 48→42px
  - 钱包页:交易行 padding/字号收紧,金额字号 15→14px
  - MainLayout:顶栏高度 50→48px,底部导航间距微调

三、对比度与可读性增强
  - 赛事详情/投注页背景从透明加深至 #E8EDF2
  - 所有卡片边框从 #E5E7EB 加深至 #CBD5E1,阴影强度提升
  - 盘口选择面板背景 #F5F7FA→#EDF0F4,增加 border-top 分隔线
  - 赔率颜色从 #F8971F 加深至 #E8870A,选中态 border-width 加粗至 2px
  - 标签字号增大并加粗 (font-weight: 600),颜色从 #6B7280 加深至 #4B5563

四、首页快捷入口 & 个人中心网格布局
  - 首页新增 4 宫格快捷入口(投注/充值/账单/活动),彩色图标区分功能
  - 个人中心重构为「4 宫格快捷操作 + 列表」结构:钱包/充值/返水/记录
  - 移除原个人中心金色入口列表项,精简设置列表为修改资料/投注规则/语言

五、顶部导航栏重设计
  - Logo 从 img 标签替换为内联 SVG 纯文字:THE(灰) + BET(深蓝) + 365(橙)
  - 客服按钮改为纯图标圆形按钮 (32×32px),增加竖线分隔符
  - Chip 高度 34→32px,间距 8→6px
  - 底部导航激活态指示条加粗至 2.5px,左右缩进从 22% 调至 18%

六、SVG Favicon
  - 新增纯路径 SVG favicon(无 <text> 元素,确保浏览器 favicon 沙箱兼容)
  - 深蓝圆角方块 + 白色 "B" 字母路径 + 橙色圆点 + 白色 "365" 数字路径
  - index.html 更新 favicon 引用为 /favicon.svg

🤖 Generated with [Qoder][https://qoder.com]
2026-06-16 12:24:41 +08:00

387 lines
9.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import api from '../api';
import { formatMoney } from '../utils/localeDisplay';
import { txTypeKey, isCashbackType, txDisplayType, txSummaryLabel, txRemarkLabel, isDepositReversalType } from '../utils/walletTx';
import GoldSpinner from '../components/GoldSpinner.vue';
import { usePullToRefresh } from '../composables/usePullToRefresh';
type TransactionDetail = {
transactionId: string;
transactionType: string;
displayType?: string;
summary?: string | null;
summaryKind?: 'opening_bonus' | null;
amount: string;
balanceBefore: string;
balanceAfter: string;
frozenBefore: string;
frozenAfter: string;
referenceType: string | null;
referenceId: string | null;
remark: string | null;
createdAt: string;
betNo: string | null;
cashbackBatchNo?: string | null;
};
const route = useRoute();
const router = useRouter();
const { t, locale } = useI18n();
const tx = ref<TransactionDetail | null>(null);
const loading = ref(true);
const notFound = ref(false);
async function loadTransaction() {
loading.value = true;
try {
const { data } = await api.get(`/player/wallet/transactions/${route.params.transactionId}`);
if (!data.data) {
notFound.value = true;
return;
}
tx.value = data.data;
} catch {
notFound.value = true;
} finally {
loading.value = false;
}
}
onMounted(loadTransaction);
const { pullDistance, spinning, progress } = usePullToRefresh({
onRefresh: loadTransaction,
});
const pullIndicatorStyle = () => ({
height: `${pullDistance.value}px`,
opacity: Math.min(pullDistance.value / 48, 1),
});
function txLabel(type: string): string {
const key = txTypeKey(type);
if (key) {
const translated = t(key);
if (translated !== key) return translated;
}
return type;
}
const displayTypeLabel = computed(() => {
if (!tx.value) return '';
return txLabel(txDisplayType(tx.value));
});
const summaryText = computed(() => {
if (!tx.value) return '';
return txSummaryLabel(tx.value, t);
});
const amountClass = computed(() => {
if (!tx.value) return '';
return parseFloat(tx.value.amount) >= 0 ? 'pos' : 'neg';
});
const formattedTime = computed(() => {
if (!tx.value) return '';
return new Date(tx.value.createdAt).toLocaleString(locale.value);
});
const frozenChanged = computed(() => {
if (!tx.value) return false;
return tx.value.frozenBefore !== tx.value.frozenAfter;
});
const isCashbackTx = computed(
() => !!tx.value && isCashbackType(tx.value.transactionType),
);
const cashbackBatchNo = computed(() => {
if (!isCashbackTx.value) return null;
return tx.value?.cashbackBatchNo ?? tx.value?.referenceId ?? null;
});
const referenceLabel = computed(() => {
if (isCashbackTx.value) return t('wallet.ref_cashback');
if (!tx.value?.referenceType) return '';
if (isDepositReversalType(tx.value.transactionType)) return t('wallet.ref_deposit');
const rt = tx.value.referenceType.toUpperCase();
if (rt === 'BET') return t('wallet.ref_bet');
if (rt === 'DEPOSIT') return t('wallet.ref_deposit');
if (rt === 'WITHDRAW') return t('wallet.ref_withdraw');
return tx.value.referenceType;
});
const remarkText = computed(() => {
if (!tx.value) return '';
return txRemarkLabel(tx.value, t);
});
function goBetDetail() {
if (!tx.value?.betNo) return;
router.push(`/bets/${tx.value.betNo}`);
}
function goCashbackDetail() {
if (!cashbackBatchNo.value) return;
router.push({ path: '/wallet/cashbacks', query: { batchNo: cashbackBatchNo.value } });
}
</script>
<template>
<div class="detail-page">
<div
class="pull-indicator"
:style="pullIndicatorStyle()"
>
<GoldSpinner v-if="spinning" :size="28" :progress="progress" :active="spinning" />
</div>
<div class="top-bar">
<button class="back-btn" type="button" @click="router.back()"> {{ t('history.back') }}</button>
</div>
<div v-if="loading" class="state">
<GoldSpinner :size="36" />
</div>
<div v-else-if="notFound || !tx" class="state">{{ t('wallet.detail_not_found') }}</div>
<template v-else>
<div class="hero" :class="amountClass">
<span class="hero-type">{{ displayTypeLabel }}</span>
<span v-if="summaryText" class="hero-summary">{{ summaryText }}</span>
<span class="hero-amount">{{ formatMoney(tx.amount, locale) }}</span>
<span class="hero-time">{{ formattedTime }}</span>
</div>
<section class="section">
<div class="section-title">{{ t('wallet.detail_summary') }}</div>
<div class="summary-rows">
<div class="sum-row">
<span>{{ t('wallet.detail_amount') }}</span>
<span :class="amountClass">{{ formatMoney(tx.amount, locale) }}</span>
</div>
<div class="sum-row">
<span>{{ t('wallet.detail_balance_before') }}</span>
<span>{{ formatMoney(tx.balanceBefore, locale) }}</span>
</div>
<div class="sum-row">
<span>{{ t('wallet.detail_balance_after') }}</span>
<span>{{ formatMoney(tx.balanceAfter, locale) }}</span>
</div>
<template v-if="frozenChanged">
<div class="sum-row">
<span>{{ t('wallet.detail_frozen_before') }}</span>
<span>{{ formatMoney(tx.frozenBefore, locale) }}</span>
</div>
<div class="sum-row">
<span>{{ t('wallet.detail_frozen_after') }}</span>
<span>{{ formatMoney(tx.frozenAfter, locale) }}</span>
</div>
</template>
</div>
</section>
<section v-if="tx.referenceType || remarkText" class="section">
<div class="section-title">{{ t('wallet.detail_reference') }}</div>
<div class="summary-rows">
<div v-if="tx.referenceType" class="sum-row">
<span>{{ t('wallet.detail_reference_type') }}</span>
<span>{{ referenceLabel }}</span>
</div>
<div v-if="tx.referenceId && !tx.betNo" class="sum-row">
<span>{{ t('wallet.detail_reference_id') }}</span>
<span class="mono">{{ tx.referenceId }}</span>
</div>
<div v-if="remarkText" class="sum-row">
<span>{{ t('wallet.detail_remark') }}</span>
<span class="remark">{{ remarkText }}</span>
</div>
</div>
<button v-if="tx.betNo" type="button" class="bet-link" @click="goBetDetail">
{{ t('wallet.detail_bet_link') }} · {{ tx.betNo }}
</button>
<button v-if="cashbackBatchNo" type="button" class="bet-link" @click="goCashbackDetail">
{{ t('wallet.detail_cashback_link') }} · {{ cashbackBatchNo }}
</button>
</section>
<section class="section">
<div class="section-title">{{ t('wallet.detail_tx_id') }}</div>
<div class="tx-id">{{ tx.transactionId }}</div>
</section>
</template>
</div>
</template>
<style scoped>
.detail-page {
padding-bottom: 32px;
}
.pull-indicator {
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
transition: height 0.15s ease;
}
.top-bar {
margin-bottom: 4px;
}
.back-btn {
background: none;
border: none;
color: #003D6B;
font-size: 15px;
font-weight: 700;
padding: 4px 0 8px;
cursor: pointer;
display: flex;
align-items: center;
gap: 2px;
}
.state {
text-align: center;
padding: 60px 20px;
color: #6B7280;
font-size: 14px;
font-weight: 600;
}
.hero {
border-radius: 14px;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
margin-bottom: 12px;
text-align: center;
background: #FFFFFF;
border: 1px solid #E5E7EB;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.hero.pos {
border-color: rgba(0, 61, 107, 0.2);
background: linear-gradient(135deg, rgba(0, 61, 107, 0.04), #FFFFFF);
}
.hero.neg {
border-color: rgba(220, 38, 38, 0.2);
background: linear-gradient(135deg, rgba(220, 38, 38, 0.04), #FFFFFF);
}
.hero-type {
font-size: 12px;
font-weight: 800;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #6B7280;
}
.hero-summary {
font-size: 13px;
font-weight: 600;
color: #6B7280;
max-width: 100%;
word-break: break-all;
}
.hero-amount {
font-size: 34px;
font-weight: 900;
line-height: 1.1;
}
.hero.pos .hero-amount { color: #003D6B; }
.hero.neg .hero-amount { color: #DC2626; }
.hero-time {
font-size: 11px;
color: #6B7280;
font-weight: 600;
}
.section {
background: #FFFFFF;
border: 1px solid #E5E7EB;
border-radius: 8px;
padding: 14px 16px;
margin-bottom: 10px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.section-title {
font-size: 12px;
font-weight: 800;
color: #6B7280;
letter-spacing: 0.04em;
margin-bottom: 10px;
}
.summary-rows {
display: flex;
flex-direction: column;
gap: 10px;
}
.sum-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
font-size: 13px;
color: #6B7280;
}
.sum-row span:last-child {
color: #1A1A2E;
font-weight: 700;
text-align: right;
word-break: break-all;
}
.pos { color: #003D6B !important; }
.neg { color: #DC2626 !important; }
.mono {
font-family: ui-monospace, monospace;
font-size: 12px;
}
.remark {
max-width: 60%;
line-height: 1.4;
}
.bet-link {
width: 100%;
margin-top: 12px;
padding: 10px 14px;
border-radius: 10px;
border: 1px solid #003D6B;
background: rgba(0, 61, 107, 0.04);
color: #003D6B;
font-size: 13px;
font-weight: 700;
cursor: pointer;
}
.tx-id {
font-family: ui-monospace, monospace;
font-size: 12px;
color: #1A1A2E;
word-break: break-all;
line-height: 1.5;
}
</style>