Files
thebet365/apps/player/src/components/BetHistoryCard.vue
Mars d2c0b575b7 feat(player): 统一移动端主题样式并优化多页面视觉一致性
将玩家端组件与核心页面全面切换为统一主题变量,优化卡片、按钮、弹窗、列表和盘口相关模块的视觉表现,并补充多语言文案以匹配新的交互与布局风格。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 15:55:28 +08:00

279 lines
6.9 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 { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { formatMoney } from '../utils/localeDisplay';
import StatusWatermark from './StatusWatermark.vue';
import { matchPhaseLabel, matchPhaseVariant, type MatchPhase } from '../utils/matchPhase';
export interface BetScore {
ht: string | null;
ft: string | null;
}
export interface BetHistoryItem {
betNo: string;
betType: string;
stake: unknown;
totalOdds?: unknown;
potentialReturn: unknown;
actualReturn: unknown;
status: string;
placedAt: string;
isCashbacked?: boolean;
leagueName?: string;
matchTitle: string;
pickLabel: string;
isParlay?: boolean;
legCount?: number;
matchScore?: BetScore | null;
matchPhase?: MatchPhase | null;
legs?: Array<{
marketLabel: string;
selectionName: string;
matchTitle: string;
odds: unknown;
resultStatus?: string | null;
score?: BetScore | null;
}>;
}
const props = defineProps<{ bet: BetHistoryItem }>();
const { t, locale } = useI18n();
const router = useRouter();
const statusKey = computed(() => {
const s = props.bet.status.toUpperCase();
if (s === 'WON' || s === 'WIN') return 'won';
if (s === 'LOST' || s === 'LOSE') return 'lost';
if (s === 'PUSH' || s === 'VOID' || s === 'CANCELLED') return 'push';
return 'pending';
});
const statusLabel = computed(() => t(`history.status_${statusKey.value}`));
const watermarkLabel = computed(() => {
if (statusKey.value === 'pending' && props.bet.matchPhase === 'closed_pending') {
return matchPhaseLabel(t, 'closed_pending');
}
if (statusKey.value === 'pending' && props.bet.matchPhase === 'settled') {
return matchPhaseLabel(t, 'settled');
}
return statusLabel.value;
});
const watermarkVariant = computed(() => {
if (statusKey.value === 'pending' && props.bet.matchPhase === 'closed_pending') return 'closed';
if (statusKey.value === 'pending' && props.bet.matchPhase === 'settled') return 'settled';
return statusKey.value;
});
const placedDate = computed(() =>
new Date(props.bet.placedAt).toLocaleDateString(locale.value, {
month: 'short', day: 'numeric',
}),
);
const title = computed(() => {
if (props.bet.isParlay) {
const n = props.bet.legCount ?? props.bet.legs?.length ?? 0;
return t('history.parlay_title', { n });
}
return props.bet.matchTitle;
});
const subtitle = computed(() => {
if (props.bet.isParlay) return props.bet.leagueName || t('history.parlay_league');
return props.bet.pickLabel || props.bet.leagueName || '';
});
const stakeAmount = computed(() => formatMoney(props.bet.stake, locale.value));
const oddsText = computed(() => {
const o = props.bet.totalOdds;
if (o == null || o === '' || o === 0) return '';
const n = parseFloat(String(o));
return Number.isFinite(n) ? n.toFixed(2) : String(o);
});
const betTypeLabel = computed(() => {
if (props.bet.isParlay) return t('bet.parlay');
return props.bet.betType || '';
});
const returnAmount = computed(() => {
if (statusKey.value === 'won') return formatMoney(props.bet.actualReturn, locale.value);
if (statusKey.value === 'pending') return formatMoney(props.bet.potentialReturn, locale.value);
if (statusKey.value === 'lost') return formatMoney(-parseFloat(String(props.bet.stake ?? 0)), locale.value);
return formatMoney(props.bet.actualReturn ?? props.bet.potentialReturn, locale.value);
});
const returnHighlight = computed(() => statusKey.value === 'won');
const returnPending = computed(() => statusKey.value === 'pending');
const returnLost = computed(() => statusKey.value === 'lost');
function goDetail() {
router.push(`/bets/${props.bet.betNo}`);
}
</script>
<template>
<article class="bet-card" @click="goDetail">
<StatusWatermark :label="watermarkLabel" :variant="watermarkVariant" />
<div class="card-body">
<div class="card-header">
<span v-if="betTypeLabel" class="bet-type-tag">{{ betTypeLabel }}</span>
<span v-if="bet.isCashbacked" class="cashback-tag">{{ t('history.cashbacked') }}</span>
<span class="card-date">{{ placedDate }}</span>
</div>
<span class="title">{{ title }}</span>
<div class="card-detail-row">
<span class="pick-label">{{ subtitle }}</span>
<span v-if="oddsText" class="odds-badge">@{{ oddsText }}</span>
</div>
<div class="card-footer">
<span class="stake-amt">{{ t('history.stake') }} {{ stakeAmount }}</span>
<span class="return-amt" :class="{ highlight: returnHighlight, pending: returnPending, lost: returnLost }">
{{ returnAmount }}
</span>
</div>
</div>
<span class="chevron"></span>
</article>
</template>
<style scoped>
.bet-card {
position: relative;
display: flex;
align-items: center;
gap: 10px;
padding: 14px 16px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 8px;
margin-bottom: 8px;
cursor: pointer;
transition: background 0.15s, box-shadow 0.15s;
-webkit-tap-highlight-color: transparent;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
.bet-card:active {
background: var(--bg-hover);
}
.card-body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.cashback-tag {
font-size: 10px;
font-weight: 700;
color: var(--accent);
background: rgba(244, 162, 97, 0.08);
border: 1px solid rgba(244, 162, 97, 0.2);
border-radius: 4px;
padding: 1px 6px;
}
.bet-type-tag {
font-size: 10px;
font-weight: 700;
color: var(--primary);
background: rgba(244, 162, 97, 0.06);
border: 1px solid rgba(244, 162, 97, 0.15);
border-radius: 4px;
padding: 1px 6px;
letter-spacing: 0.02em;
}
.card-date {
font-size: 10px;
color: var(--text-muted);
font-weight: 600;
flex-shrink: 0;
margin-left: auto;
}
.title {
font-size: 14px;
font-weight: 800;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-detail-row {
display: flex;
align-items: center;
gap: 6px;
}
.pick-label {
font-size: 11px;
color: var(--text-muted);
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
}
.odds-badge {
font-size: 11px;
font-weight: 800;
color: var(--warning);
background: rgba(255, 179, 0, 0.08);
border-radius: 4px;
padding: 1px 6px;
flex-shrink: 0;
}
.card-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-top: 2px;
}
.stake-amt {
font-size: 11px;
color: var(--text-muted);
font-weight: 600;
}
.return-amt {
font-size: 15px;
font-weight: 900;
color: var(--text-muted);
}
.return-amt.highlight { color: var(--success); }
.return-amt.pending { color: var(--warning); }
.return-amt.lost { color: var(--danger); }
.chevron {
flex-shrink: 0;
font-size: 18px;
color: var(--border);
margin-left: 2px;
line-height: 1;
}
</style>