feat(player): PC 端投注/资金/充值/返水列表改为弹窗详情
列表点击与深链统一打开桌面弹窗,并补齐冻结余额文案与审核日志补全。
This commit is contained in:
@@ -1,8 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch, computed, onMounted, onUnmounted } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import api from '../../api';
|
import api from '../../api';
|
||||||
import GoldSpinner from '../GoldSpinner.vue';
|
import GoldSpinner from '../GoldSpinner.vue';
|
||||||
|
import { formatMoney } from '../../utils/localeDisplay';
|
||||||
|
import type { BetHistoryItem } from '../BetHistoryCard.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@@ -15,26 +17,8 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const { t, locale } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
|
|
||||||
type BetDetail = {
|
const detail = ref<BetHistoryItem | null>(null);
|
||||||
betNo: string;
|
const loading = ref(false);
|
||||||
stake: string;
|
|
||||||
potentialReturn?: string;
|
|
||||||
status: string;
|
|
||||||
placedAt: string;
|
|
||||||
legs: Array<{
|
|
||||||
matchTitle?: string;
|
|
||||||
marketLabel?: string;
|
|
||||||
selectionName?: string;
|
|
||||||
odds?: number;
|
|
||||||
resultStatus?: string;
|
|
||||||
score?: { ht: string | null; ft: string | null } | null;
|
|
||||||
[key: string]: any;
|
|
||||||
}>;
|
|
||||||
[key: string]: any;
|
|
||||||
};
|
|
||||||
|
|
||||||
const detail = ref<BetDetail | null>(null);
|
|
||||||
const loading = ref(true);
|
|
||||||
const error = ref(false);
|
const error = ref(false);
|
||||||
|
|
||||||
async function loadDetail(betNo: string) {
|
async function loadDetail(betNo: string) {
|
||||||
@@ -54,10 +38,11 @@ async function loadDetail(betNo: string) {
|
|||||||
watch(
|
watch(
|
||||||
() => [props.visible, props.betNo] as const,
|
() => [props.visible, props.betNo] as const,
|
||||||
([vis, no]) => {
|
([vis, no]) => {
|
||||||
if (vis && no) loadDetail(no);
|
if (vis && no) void loadDetail(no);
|
||||||
if (!vis) {
|
if (!vis) {
|
||||||
detail.value = null;
|
detail.value = null;
|
||||||
error.value = false;
|
error.value = false;
|
||||||
|
loading.value = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
@@ -67,8 +52,21 @@ function close() {
|
|||||||
emit('update:visible', false);
|
emit('update:visible', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onKeydown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Escape' && props.visible) close();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => document.addEventListener('keydown', onKeydown));
|
||||||
|
onUnmounted(() => document.removeEventListener('keydown', onKeydown));
|
||||||
|
|
||||||
function statusClass(status: string) {
|
function statusClass(status: string) {
|
||||||
const map: Record<string, string> = { WON: 'st-won', LOST: 'st-lost', PENDING: 'st-pending', PUSH: 'st-push' };
|
const map: Record<string, string> = {
|
||||||
|
WON: 'st-won',
|
||||||
|
LOST: 'st-lost',
|
||||||
|
PENDING: 'st-pending',
|
||||||
|
PUSH: 'st-push',
|
||||||
|
CANCELLED: 'st-cancelled',
|
||||||
|
};
|
||||||
return map[status?.toUpperCase()] ?? 'st-default';
|
return map[status?.toUpperCase()] ?? 'st-default';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +76,7 @@ function statusLabel(status: string) {
|
|||||||
LOST: t('history.filter_lost'),
|
LOST: t('history.filter_lost'),
|
||||||
PENDING: t('history.filter_pending'),
|
PENDING: t('history.filter_pending'),
|
||||||
PUSH: t('history.filter_push'),
|
PUSH: t('history.filter_push'),
|
||||||
|
CANCELLED: t('common.cancelled'),
|
||||||
};
|
};
|
||||||
return map[status?.toUpperCase()] ?? status;
|
return map[status?.toUpperCase()] ?? status;
|
||||||
}
|
}
|
||||||
@@ -107,16 +106,48 @@ function translateSel(name: string): string {
|
|||||||
}
|
}
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function legResultIcon(status?: string | null) {
|
||||||
|
switch (status?.toUpperCase()) {
|
||||||
|
case 'WON': return '✓';
|
||||||
|
case 'LOST': return '✗';
|
||||||
|
case 'PUSH': return '=';
|
||||||
|
default: return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function legResultClass(status?: string | null) {
|
||||||
|
switch (status?.toUpperCase()) {
|
||||||
|
case 'WON': return 'leg-won';
|
||||||
|
case 'LOST': return 'leg-lost';
|
||||||
|
case 'PUSH': return 'leg-push';
|
||||||
|
default: return 'leg-pending';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayLegs = computed(() => {
|
||||||
|
if (!detail.value) return [];
|
||||||
|
if (detail.value.legs?.length) return detail.value.legs;
|
||||||
|
if (detail.value.isParlay) return [];
|
||||||
|
return [{
|
||||||
|
matchTitle: detail.value.matchTitle,
|
||||||
|
marketLabel: '',
|
||||||
|
selectionName: detail.value.pickLabel,
|
||||||
|
odds: detail.value.totalOdds,
|
||||||
|
resultStatus: detail.value.status,
|
||||||
|
score: detail.value.matchScore ?? null,
|
||||||
|
}];
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<Transition name="modal-fade">
|
<Transition name="modal-fade">
|
||||||
<div v-if="visible" class="modal-overlay" @click.self="close">
|
<div v-if="visible" class="modal-overlay" @click.self="close">
|
||||||
<div class="modal-container" role="dialog" aria-modal="true">
|
<div class="modal-container" role="dialog" aria-modal="true" :aria-label="t('bet.detail_title')">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<span class="modal-title">{{ t('bet.detail_title') || '注单详情' }}</span>
|
<span class="modal-title">{{ t('bet.detail_title') }}</span>
|
||||||
<button type="button" class="modal-close" @click="close">✕</button>
|
<button type="button" class="modal-close" :aria-label="t('support.close')" @click="close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
@@ -126,51 +157,64 @@ function translateSel(name: string): string {
|
|||||||
|
|
||||||
<div v-else-if="error" class="modal-error">
|
<div v-else-if="error" class="modal-error">
|
||||||
<p>{{ t('common.load_failed') }}</p>
|
<p>{{ t('common.load_failed') }}</p>
|
||||||
<button v-if="betNo" type="button" class="retry-btn" @click="loadDetail(betNo)">{{ t('common.retry') }}</button>
|
<button v-if="betNo" type="button" class="retry-btn" @click="loadDetail(betNo)">
|
||||||
|
{{ t('common.retry') }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-else-if="detail">
|
<template v-else-if="detail">
|
||||||
<div class="info-rows">
|
<div class="summary-strip">
|
||||||
<div class="info-row">
|
<div class="summary-main">
|
||||||
<span class="row-label">{{ t('bet.bet_no') || '注单号' }}</span>
|
<span class="bet-no mono">{{ detail.betNo }}</span>
|
||||||
<span class="row-val mono">{{ detail.betNo }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-row">
|
|
||||||
<span class="row-label">{{ t('common.status') || '状态' }}</span>
|
|
||||||
<span class="row-val">
|
|
||||||
<span class="st-badge" :class="statusClass(detail.status)">{{ statusLabel(detail.status) }}</span>
|
<span class="st-badge" :class="statusClass(detail.status)">{{ statusLabel(detail.status) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-metrics">
|
||||||
|
<div class="metric">
|
||||||
|
<span class="metric-label">{{ t('bet.stake') }}</span>
|
||||||
|
<span class="metric-val">{{ formatMoney(detail.stake, locale) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric">
|
||||||
|
<span class="metric-label">{{ t('bet.potential_payout') }}</span>
|
||||||
|
<span class="metric-val accent">{{ formatMoney(detail.potentialReturn, locale) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric">
|
||||||
|
<span class="metric-label">{{ t('bet.placed_at') }}</span>
|
||||||
|
<span class="metric-val muted">
|
||||||
|
{{ detail.placedAt ? new Date(detail.placedAt).toLocaleString() : '-' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
|
||||||
<span class="row-label">{{ t('bet.stake') || '投注额' }}</span>
|
|
||||||
<span class="row-val">{{ detail.stake }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-row">
|
|
||||||
<span class="row-label">{{ t('bet.potential_payout') || '预计派彩' }}</span>
|
|
||||||
<span class="row-val gold">{{ detail.potentialReturn || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-row">
|
|
||||||
<span class="row-label">{{ t('bet.placed_at') || '下注时间' }}</span>
|
|
||||||
<span class="row-val muted">{{ detail.placedAt ? new Date(detail.placedAt).toLocaleString() : '-' }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sel-section">
|
<div class="sel-section">
|
||||||
<div class="sel-section-title">{{ t('bet.selections') || '投注选项' }}</div>
|
<div class="sel-section-title">{{ t('bet.selections') }}</div>
|
||||||
<div v-for="(leg, idx) in detail.legs" :key="idx" class="sel-item">
|
<div v-for="(leg, idx) in displayLegs" :key="idx" class="sel-item">
|
||||||
|
<div class="sel-item-left">
|
||||||
|
<span v-if="displayLegs.length > 1" class="sel-index">{{ idx + 1 }}</span>
|
||||||
<div class="sel-item-main">
|
<div class="sel-item-main">
|
||||||
<span class="sel-item-match">{{ leg.matchTitle || t('bet.match') }}</span>
|
<span class="sel-item-match">{{ leg.matchTitle || t('bet.match') }}</span>
|
||||||
<span class="sel-item-pick">
|
<span class="sel-item-pick">
|
||||||
{{ leg.marketLabel || '' }} · {{ translateSel(leg.selectionName || '') }}
|
<template v-if="leg.marketLabel">{{ leg.marketLabel }} · </template>
|
||||||
|
{{ translateSel(leg.selectionName || '') }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="leg.score?.ft" class="sel-item-score">{{ t('history.ft') }}: {{ leg.score.ft }}</span>
|
<span v-if="leg.score?.ft" class="sel-item-score">{{ t('history.ft') }}: {{ leg.score.ft }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="sel-item-odds">@{{ leg.odds }}</span>
|
</div>
|
||||||
|
<div class="sel-item-right">
|
||||||
|
<span class="sel-item-odds">@{{ leg.odds ?? '-' }}</span>
|
||||||
|
<span
|
||||||
|
v-if="leg.resultStatus"
|
||||||
|
class="leg-result"
|
||||||
|
:class="legResultClass(leg.resultStatus)"
|
||||||
|
>
|
||||||
|
{{ legResultIcon(leg.resultStatus) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-else class="modal-empty">{{ t('common.not_found') || '未找到记录' }}</div>
|
<div v-else class="modal-empty">{{ t('common.not_found') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -187,19 +231,19 @@ function translateSel(name: string): string {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
background: rgba(0, 0, 0, 0.7);
|
background: rgba(0, 0, 0, 0.65);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-container {
|
.modal-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 520px;
|
max-width: 560px;
|
||||||
max-height: 80vh;
|
max-height: 82vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--desktop-sidebar-bg, rgba(20, 20, 20, 0.95));
|
background: var(--bg-card);
|
||||||
border: 1px solid var(--desktop-border, #262626);
|
border: 1px solid var(--border);
|
||||||
border-radius: 4px;
|
border-radius: 10px;
|
||||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,14 +251,14 @@ function translateSel(name: string): string {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 12px 16px;
|
padding: 14px 18px;
|
||||||
border-bottom: 1px solid var(--desktop-border, #262626);
|
border-bottom: 1px solid var(--border);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-title {
|
.modal-title {
|
||||||
font-size: 14px;
|
font-size: 15px;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,26 +272,23 @@ function translateSel(name: string): string {
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-close:hover { color: var(--text); }
|
.modal-close:hover {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-loading {
|
.modal-loading,
|
||||||
display: flex;
|
.modal-error,
|
||||||
justify-content: center;
|
.modal-empty {
|
||||||
align-items: center;
|
|
||||||
padding: 48px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-error {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 48px 20px;
|
padding: 48px 20px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
@@ -255,84 +296,105 @@ function translateSel(name: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.retry-btn {
|
.retry-btn {
|
||||||
padding: 6px 18px;
|
padding: 7px 18px;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
border: 1px solid var(--border-gold-soft, rgba(212,175,55,0.25));
|
border: 1px solid var(--border-active);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--primary-light, #F0D875);
|
color: var(--primary-light);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-empty {
|
.summary-strip {
|
||||||
|
padding: 16px 18px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: rgba(0, 102, 204, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-main {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 10px;
|
||||||
padding: 48px 20px;
|
margin-bottom: 14px;
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Info rows */
|
.bet-no {
|
||||||
.info-rows {
|
|
||||||
border-bottom: 1px solid var(--desktop-border, #262626);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 10px 16px;
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-row:last-child { border-bottom: none; }
|
|
||||||
|
|
||||||
.row-label {
|
|
||||||
width: 90px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-val {
|
|
||||||
flex: 1;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-val.gold { color: var(--primary-light, #F0D875); }
|
.summary-metrics {
|
||||||
.row-val.muted { color: var(--text-muted); font-weight: 500; }
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.mono { font-family: 'SF Mono', 'Consolas', monospace; font-size: 12px; }
|
.metric {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-label {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-val {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--text);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-val.accent {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-val.muted {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mono {
|
||||||
|
font-family: 'SF Mono', 'Consolas', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
.st-badge {
|
.st-badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 2px 10px;
|
padding: 2px 10px;
|
||||||
border-radius: 2px;
|
border-radius: 999px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.st-won { background: rgba(52, 199, 89, 0.12); color: #4cd964; }
|
.st-won { background: rgba(52, 199, 89, 0.12); color: #4cd964; }
|
||||||
.st-lost { background: rgba(255, 69, 58, 0.12); color: #ff453a; }
|
.st-lost { background: rgba(255, 69, 58, 0.12); color: #ff453a; }
|
||||||
.st-pending { background: rgba(212,175,55,0.12); color: var(--primary-light); }
|
.st-pending { background: rgba(0, 102, 204, 0.12); color: var(--primary-light); }
|
||||||
.st-push { background: rgba(100, 100, 100, 0.12); color: #aaa; }
|
.st-push { background: rgba(100, 100, 100, 0.12); color: #aaa; }
|
||||||
|
.st-cancelled { background: rgba(100, 100, 100, 0.1); color: #888; }
|
||||||
.st-default { background: rgba(100, 100, 100, 0.1); color: #888; }
|
.st-default { background: rgba(100, 100, 100, 0.1); color: #888; }
|
||||||
|
|
||||||
/* Selections */
|
.sel-section {
|
||||||
.sel-section { padding: 12px 16px 16px; }
|
padding: 14px 18px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
.sel-section-title {
|
.sel-section-title {
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin-bottom: 8px;
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
margin-bottom: 10px;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
border-bottom: 1px solid var(--desktop-border, #262626);
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sel-item {
|
.sel-item {
|
||||||
@@ -340,11 +402,34 @@ function translateSel(name: string): string {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 10px 0;
|
padding: 12px 0;
|
||||||
border-bottom: 1px solid rgba(255,255,255,0.04);
|
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sel-item:last-child { border-bottom: none; }
|
.sel-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sel-item-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sel-index {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--bg-hover);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-muted);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.sel-item-main {
|
.sel-item-main {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -369,15 +454,36 @@ function translateSel(name: string): string {
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sel-item-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.sel-item-odds {
|
.sel-item-odds {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: var(--primary-light, #F0D875);
|
color: var(--primary);
|
||||||
flex-shrink: 0;
|
|
||||||
font-family: 'SF Mono', 'Consolas', monospace;
|
font-family: 'SF Mono', 'Consolas', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Transition */
|
.leg-result {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leg-won { background: rgba(52, 199, 89, 0.15); color: #4cd964; }
|
||||||
|
.leg-lost { background: rgba(255, 69, 58, 0.15); color: #ff453a; }
|
||||||
|
.leg-push { background: rgba(100, 100, 100, 0.15); color: #aaa; }
|
||||||
|
.leg-pending { background: rgba(0, 102, 204, 0.1); color: var(--primary-light); font-size: 10px; }
|
||||||
|
|
||||||
.modal-fade-enter-active,
|
.modal-fade-enter-active,
|
||||||
.modal-fade-leave-active {
|
.modal-fade-leave-active {
|
||||||
transition: opacity 0.15s ease;
|
transition: opacity 0.15s ease;
|
||||||
@@ -387,4 +493,10 @@ function translateSel(name: string): string {
|
|||||||
.modal-fade-leave-to {
|
.modal-fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.summary-metrics {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, onUnmounted } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { formatMoney, formatMoneyCompact } from '../../utils/localeDisplay';
|
||||||
|
import type { CashbackRecord } from '../../utils/cashback';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean;
|
||||||
|
record: CashbackRecord | null;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:visible': [value: boolean];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t, locale } = useI18n();
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
emit('update:visible', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeydown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Escape' && props.visible) close();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => document.addEventListener('keydown', onKeydown));
|
||||||
|
onUnmounted(() => document.removeEventListener('keydown', onKeydown));
|
||||||
|
|
||||||
|
function formatPeriod(start: string, end: string) {
|
||||||
|
const opts: Intl.DateTimeFormatOptions = { year: 'numeric', 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);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<Transition name="modal-fade">
|
||||||
|
<div v-if="visible" class="modal-overlay" @click.self="close">
|
||||||
|
<div class="modal-container" role="dialog" aria-modal="true" :aria-label="t('cashback.title')">
|
||||||
|
<div class="modal-header">
|
||||||
|
<span class="modal-title">{{ t('cashback.title') }}</span>
|
||||||
|
<button type="button" class="modal-close" :aria-label="t('support.close')" @click="close">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="record" class="modal-body">
|
||||||
|
<div class="amount-hero pos">{{ formatMoney(record.amount, locale) }}</div>
|
||||||
|
<div class="type-label">{{ formatPeriod(record.periodStart, record.periodEnd) }}</div>
|
||||||
|
|
||||||
|
<div class="info-rows">
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="row-label">{{ t('cashback.effective_stake') }}</span>
|
||||||
|
<span class="row-val">{{ formatMoneyCompact(record.effectiveStake, locale) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="row-label">{{ t('cashback.rate') }}</span>
|
||||||
|
<span class="row-val">{{ formatRate(record.rate) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="row-label">{{ t('nav.bet_history') }}</span>
|
||||||
|
<span class="row-val">{{ t('cashback.bet_count', { n: record.betCount }) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="row-label">{{ t('cashback.batch_no') }}</span>
|
||||||
|
<span class="row-val mono muted">{{ record.batchNo }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="row-label">{{ t('wallet.time') }}</span>
|
||||||
|
<span class="row-val muted">{{ formatTime(record.confirmedAt ?? record.createdAt) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="modal-empty">{{ t('common.not_found') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1100;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px;
|
||||||
|
background: rgba(0, 0, 0, 0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 480px;
|
||||||
|
max-height: 82vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 18px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 4px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close:hover {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 48px 20px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-hero {
|
||||||
|
padding: 20px 18px 8px;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 800;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-family: 'SF Mono', 'Consolas', monospace;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-hero.pos {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-label {
|
||||||
|
padding: 0 18px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-rows {
|
||||||
|
padding: 4px 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 11px 18px;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-label {
|
||||||
|
width: 96px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-val {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-val.muted {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mono {
|
||||||
|
font-family: 'SF Mono', 'Consolas', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-fade-enter-active,
|
||||||
|
.modal-fade-leave-active {
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-fade-enter-from,
|
||||||
|
.modal-fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,566 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, onUnmounted } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { formatMoney } from '../../utils/localeDisplay';
|
||||||
|
import type { DepositAuditLog, PlayerDepositOrder } from '../../utils/depositOrderLookup';
|
||||||
|
import {
|
||||||
|
auditActionTone,
|
||||||
|
auditActorSecondary,
|
||||||
|
formatDepositAuditRemark,
|
||||||
|
shouldShowAuditRejectInTimeline,
|
||||||
|
} from '../../utils/depositAuditDisplay';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean;
|
||||||
|
order: PlayerDepositOrder | null;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:visible': [value: boolean];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t, locale } = useI18n();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
type RechargeDetail = PlayerDepositOrder;
|
||||||
|
|
||||||
|
const detail = computed(() => props.order);
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
emit('update:visible', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeydown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Escape' && props.visible) close();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => document.addEventListener('keydown', onKeydown));
|
||||||
|
onUnmounted(() => document.removeEventListener('keydown', onKeydown));
|
||||||
|
|
||||||
|
function statusClass(status: string) {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
APPROVED: 'st-won',
|
||||||
|
REJECTED: 'st-lost',
|
||||||
|
PENDING: 'st-pending',
|
||||||
|
};
|
||||||
|
return map[status?.toUpperCase()] ?? 'st-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');
|
||||||
|
}
|
||||||
|
|
||||||
|
function methodLabel(order: RechargeDetail) {
|
||||||
|
if (order.methodType === 'USDT') return 'USDT';
|
||||||
|
return order.paymentMethodName || order.methodType || '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
function reapply(order: RechargeDetail) {
|
||||||
|
close();
|
||||||
|
const query: Record<string, string> = {
|
||||||
|
orderId: order.id,
|
||||||
|
methodType: order.methodType,
|
||||||
|
amount: order.amount,
|
||||||
|
};
|
||||||
|
if (order.paymentMethodId) query.methodId = order.paymentMethodId;
|
||||||
|
void router.push({ path: '/wallet/recharge', query });
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeText(value: string | null | undefined) {
|
||||||
|
return value?.trim() ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function orderNote(order: RechargeDetail): { label: string; text: string } | null {
|
||||||
|
const rejectReason = normalizeText(order.rejectReason);
|
||||||
|
const remark = normalizeText(order.remark);
|
||||||
|
if (order.status === 'REJECTED') {
|
||||||
|
const text = rejectReason || remark;
|
||||||
|
if (!text) return null;
|
||||||
|
return { label: t('recharge.reject_reason'), text };
|
||||||
|
}
|
||||||
|
if (remark) return { label: t('recharge.remark'), text: remark };
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function auditActionLabel(action: string) {
|
||||||
|
const key = `recharge.audit_${action.toLowerCase()}`;
|
||||||
|
const translated = t(key);
|
||||||
|
return translated !== key ? translated : action;
|
||||||
|
}
|
||||||
|
|
||||||
|
function auditStepClass(action: string) {
|
||||||
|
return `audit-step--${auditActionTone(action)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAuditTime(iso: string) {
|
||||||
|
return new Date(iso).toLocaleString(locale.value, {
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function auditRemarkForTimeline(log: DepositAuditLog, order: RechargeDetail) {
|
||||||
|
if (log.action === 'REJECTED' && !shouldShowAuditRejectInTimeline(log, order.rejectReason)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return formatDepositAuditRemark(log, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
function auditLogsForDisplay(order: RechargeDetail) {
|
||||||
|
return [...(order.auditLogs ?? [])]
|
||||||
|
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||||
|
.map((log) => ({
|
||||||
|
log,
|
||||||
|
actor: auditActorSecondary(log, t),
|
||||||
|
remark: auditRemarkForTimeline(log, order),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function auditNoteDisplayText(remark: { kind: 'note'; text: string }) {
|
||||||
|
if (remark.text === t('wallet.remark_deposit_revoke_generic')) return remark.text;
|
||||||
|
return `${t('recharge.audit_remark_label')}: ${remark.text}`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<Transition name="modal-fade">
|
||||||
|
<div v-if="visible" class="modal-overlay" @click.self="close">
|
||||||
|
<div class="modal-container" role="dialog" aria-modal="true" :aria-label="t('recharge.order_detail')">
|
||||||
|
<div class="modal-header">
|
||||||
|
<span class="modal-title">{{ t('recharge.order_detail') }}</span>
|
||||||
|
<button type="button" class="modal-close" :aria-label="t('support.close')" @click="close">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<template v-if="detail">
|
||||||
|
<div class="summary-strip">
|
||||||
|
<div class="summary-main">
|
||||||
|
<span class="order-no mono">{{ detail.orderNo }}</span>
|
||||||
|
<span class="st-badge" :class="statusClass(detail.status)">{{ statusLabel(detail.status) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="amount-hero">{{ formatMoney(detail.amount, locale) }}</div>
|
||||||
|
<div class="method-label">{{ methodLabel(detail) }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-rows">
|
||||||
|
<div
|
||||||
|
v-if="detail.approvedAmount && detail.approvedAmount !== detail.amount"
|
||||||
|
class="info-row"
|
||||||
|
>
|
||||||
|
<span class="row-label">{{ t('recharge.credited') }}</span>
|
||||||
|
<span class="row-val accent">{{ formatMoney(detail.approvedAmount, locale) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="row-label">{{ t('recharge.apply_time') }}</span>
|
||||||
|
<span class="row-val muted">{{ new Date(detail.createdAt).toLocaleString() }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="detail.reviewedAt" class="info-row">
|
||||||
|
<span class="row-label">{{ t('recharge.review_time') }}</span>
|
||||||
|
<span class="row-val muted">{{ new Date(detail.reviewedAt).toLocaleString() }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="orderNote(detail)" class="info-row info-row--note">
|
||||||
|
<span class="row-label">{{ orderNote(detail)!.label }}</span>
|
||||||
|
<span class="row-val" :class="{ reject: detail.status === 'REJECTED' }">
|
||||||
|
{{ orderNote(detail)!.text }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="detail.auditLogs?.length" class="audit-section">
|
||||||
|
<div class="section-title">{{ t('recharge.audit_title') }}</div>
|
||||||
|
<div
|
||||||
|
v-for="(entry, logIdx) in auditLogsForDisplay(detail)"
|
||||||
|
:key="entry.log.id"
|
||||||
|
class="audit-step"
|
||||||
|
:class="auditStepClass(entry.log.action)"
|
||||||
|
>
|
||||||
|
<div class="audit-step-rail" aria-hidden="true">
|
||||||
|
<span class="audit-dot" />
|
||||||
|
<span
|
||||||
|
v-if="logIdx < auditLogsForDisplay(detail).length - 1"
|
||||||
|
class="audit-line"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="audit-step-body">
|
||||||
|
<div class="audit-step-head">
|
||||||
|
<span class="audit-step-title">{{ auditActionLabel(entry.log.action) }}</span>
|
||||||
|
<time class="audit-step-time">{{ formatAuditTime(entry.log.createdAt) }}</time>
|
||||||
|
</div>
|
||||||
|
<p v-if="entry.actor" class="audit-step-actor">{{ entry.actor }}</p>
|
||||||
|
<p
|
||||||
|
v-if="entry.log.approvedAmount && entry.log.action === 'APPROVED'"
|
||||||
|
class="audit-step-credited"
|
||||||
|
>
|
||||||
|
{{ t('recharge.audit_credited') }} {{ formatMoney(entry.log.approvedAmount, locale) }}
|
||||||
|
</p>
|
||||||
|
<div v-if="entry.remark?.kind === 'reject'" class="audit-step-box audit-step-box--reject">
|
||||||
|
<span class="audit-step-box-label">{{ t('recharge.reject_reason') }}</span>
|
||||||
|
<span class="audit-step-box-text">{{ entry.remark.text }}</span>
|
||||||
|
</div>
|
||||||
|
<p v-else-if="entry.remark?.kind === 'note'" class="audit-step-note">
|
||||||
|
{{ auditNoteDisplayText(entry.remark) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="detail.status === 'REJECTED'" class="modal-actions">
|
||||||
|
<button type="button" class="reapply-btn" @click="reapply(detail)">
|
||||||
|
{{ t('recharge.reapply') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-else class="modal-empty">{{ t('common.not_found') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1100;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px;
|
||||||
|
background: rgba(0, 0, 0, 0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 520px;
|
||||||
|
max-height: 82vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 18px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 4px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close:hover {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-loading,
|
||||||
|
.modal-error,
|
||||||
|
.modal-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 48px 20px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-btn {
|
||||||
|
padding: 7px 18px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--border-active);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--primary-light);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-strip {
|
||||||
|
padding: 16px 18px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: rgba(0, 102, 204, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-no {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-hero {
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: 800;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-family: 'SF Mono', 'Consolas', monospace;
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1.1;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mono {
|
||||||
|
font-family: 'SF Mono', 'Consolas', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st-won { background: rgba(52, 199, 89, 0.12); color: #4cd964; }
|
||||||
|
.st-lost { background: rgba(255, 69, 58, 0.12); color: #ff453a; }
|
||||||
|
.st-pending { background: rgba(0, 102, 204, 0.12); color: var(--primary-light); }
|
||||||
|
.st-default { background: rgba(100, 100, 100, 0.1); color: #888; }
|
||||||
|
|
||||||
|
.info-rows {
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 11px 18px;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row--note {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-label {
|
||||||
|
width: 96px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row--note .row-label {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-val {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text);
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-val.accent {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-val.muted {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-val.reject {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-section {
|
||||||
|
padding: 14px 18px 18px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-step {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-step-rail {
|
||||||
|
flex: 0 0 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #555;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-line {
|
||||||
|
flex: 1;
|
||||||
|
width: 1px;
|
||||||
|
min-height: 12px;
|
||||||
|
margin: 4px 0;
|
||||||
|
background: rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-step-body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-step:last-child .audit-step-body {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-step-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-step-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-step-time {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-step-actor {
|
||||||
|
margin: 3px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-step-credited {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #4cd964;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-step-note {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-step-box {
|
||||||
|
margin-top: 4px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-step-box--reject {
|
||||||
|
border: 1px solid rgba(255, 69, 58, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-step-box-label {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #c07070;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-step-box-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #b08888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-step--submitted .audit-dot { background: var(--primary); }
|
||||||
|
.audit-step--approved .audit-dot { background: #4cd964; }
|
||||||
|
.audit-step--rejected .audit-dot { background: #ff453a; }
|
||||||
|
.audit-step--revoked .audit-dot { background: #888; }
|
||||||
|
.audit-step--reopened .audit-dot { background: #c9a227; }
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
padding: 0 18px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reapply-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border-active);
|
||||||
|
background: rgba(0, 102, 204, 0.08);
|
||||||
|
color: var(--primary-light);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reapply-btn:hover {
|
||||||
|
background: rgba(0, 102, 204, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-fade-enter-active,
|
||||||
|
.modal-fade-leave-active {
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-fade-enter-from,
|
||||||
|
.modal-fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue';
|
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
import api from '../../api';
|
import api from '../../api';
|
||||||
import GoldSpinner from '../GoldSpinner.vue';
|
import GoldSpinner from '../GoldSpinner.vue';
|
||||||
import { formatMoney } from '../../utils/localeDisplay';
|
import { formatMoney } from '../../utils/localeDisplay';
|
||||||
import { txTypeKey, txDisplayType, txSummaryLabel } from '../../utils/walletTx';
|
import { txTypeKey, txDisplayType, txSummaryLabel, isCashbackType } from '../../utils/walletTx';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@@ -16,6 +17,7 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { t, locale } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
type TxDetail = {
|
type TxDetail = {
|
||||||
transactionId: string;
|
transactionId: string;
|
||||||
@@ -29,11 +31,14 @@ type TxDetail = {
|
|||||||
frozenAfter?: string;
|
frozenAfter?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
referenceType?: string | null;
|
referenceType?: string | null;
|
||||||
[key: string]: any;
|
referenceId?: string | null;
|
||||||
|
betNo?: string | null;
|
||||||
|
cashbackBatchNo?: string | null;
|
||||||
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
const detail = ref<TxDetail | null>(null);
|
const detail = ref<TxDetail | null>(null);
|
||||||
const loading = ref(true);
|
const loading = ref(false);
|
||||||
const error = ref(false);
|
const error = ref(false);
|
||||||
|
|
||||||
async function loadDetail(id: string) {
|
async function loadDetail(id: string) {
|
||||||
@@ -53,10 +58,11 @@ async function loadDetail(id: string) {
|
|||||||
watch(
|
watch(
|
||||||
() => [props.visible, props.transactionId] as const,
|
() => [props.visible, props.transactionId] as const,
|
||||||
([vis, id]) => {
|
([vis, id]) => {
|
||||||
if (vis && id) loadDetail(id);
|
if (vis && id) void loadDetail(id);
|
||||||
if (!vis) {
|
if (!vis) {
|
||||||
detail.value = null;
|
detail.value = null;
|
||||||
error.value = false;
|
error.value = false;
|
||||||
|
loading.value = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
@@ -66,6 +72,13 @@ function close() {
|
|||||||
emit('update:visible', false);
|
emit('update:visible', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onKeydown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Escape' && props.visible) close();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => document.addEventListener('keydown', onKeydown));
|
||||||
|
onUnmounted(() => document.removeEventListener('keydown', onKeydown));
|
||||||
|
|
||||||
function txLabel(tx: TxDetail): string {
|
function txLabel(tx: TxDetail): string {
|
||||||
const key = txTypeKey(txDisplayType(tx));
|
const key = txTypeKey(txDisplayType(tx));
|
||||||
if (key) {
|
if (key) {
|
||||||
@@ -74,16 +87,39 @@ function txLabel(tx: TxDetail): string {
|
|||||||
}
|
}
|
||||||
return tx.transactionType;
|
return tx.transactionType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showFrozen = (tx: TxDetail) =>
|
||||||
|
parseFloat(tx.frozenBefore || '0') > 0 || parseFloat(tx.frozenAfter || '0') > 0;
|
||||||
|
|
||||||
|
const betNo = computed(() => detail.value?.betNo?.trim() || null);
|
||||||
|
|
||||||
|
const cashbackBatchNo = computed(() => {
|
||||||
|
const tx = detail.value;
|
||||||
|
if (!tx || !isCashbackType(tx.transactionType)) return null;
|
||||||
|
return tx.cashbackBatchNo ?? tx.referenceId ?? null;
|
||||||
|
});
|
||||||
|
|
||||||
|
function goBetDetail() {
|
||||||
|
if (!betNo.value) return;
|
||||||
|
close();
|
||||||
|
void router.push({ path: '/bets', query: { betNo: betNo.value } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function goCashbackDetail() {
|
||||||
|
if (!cashbackBatchNo.value) return;
|
||||||
|
close();
|
||||||
|
void router.push({ path: '/wallet/cashbacks', query: { batchNo: cashbackBatchNo.value } });
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<Transition name="modal-fade">
|
<Transition name="modal-fade">
|
||||||
<div v-if="visible" class="modal-overlay" @click.self="close">
|
<div v-if="visible" class="modal-overlay" @click.self="close">
|
||||||
<div class="modal-container" role="dialog" aria-modal="true">
|
<div class="modal-container" role="dialog" aria-modal="true" :aria-label="t('wallet.transaction_detail')">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<span class="modal-title">{{ t('wallet.transaction_detail') || '交易详情' }}</span>
|
<span class="modal-title">{{ t('wallet.transaction_detail') }}</span>
|
||||||
<button type="button" class="modal-close" @click="close">✕</button>
|
<button type="button" class="modal-close" :aria-label="t('support.close')" @click="close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
@@ -93,51 +129,59 @@ function txLabel(tx: TxDetail): string {
|
|||||||
|
|
||||||
<div v-else-if="error" class="modal-error">
|
<div v-else-if="error" class="modal-error">
|
||||||
<p>{{ t('common.load_failed') }}</p>
|
<p>{{ t('common.load_failed') }}</p>
|
||||||
<button v-if="transactionId" type="button" class="retry-btn" @click="loadDetail(transactionId)">{{ t('common.retry') }}</button>
|
<button v-if="transactionId" type="button" class="retry-btn" @click="loadDetail(transactionId)">
|
||||||
|
{{ t('common.retry') }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-else-if="detail">
|
<template v-else-if="detail">
|
||||||
|
<div class="amount-hero" :class="parseFloat(detail.amount) >= 0 ? 'pos' : 'neg'">
|
||||||
|
{{ formatMoney(detail.amount, locale) }}
|
||||||
|
</div>
|
||||||
|
<div class="type-label">{{ txLabel(detail) }}</div>
|
||||||
|
|
||||||
<div class="info-rows">
|
<div class="info-rows">
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="row-label">{{ t('wallet.type') || '类型' }}</span>
|
<span class="row-label">{{ t('wallet.note') }}</span>
|
||||||
<span class="row-val">{{ txLabel(detail) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-row">
|
|
||||||
<span class="row-label">{{ t('wallet.amount') || '金额' }}</span>
|
|
||||||
<span class="row-val mono" :class="parseFloat(detail.amount) >= 0 ? 'pos' : 'neg'">{{ formatMoney(detail.amount, locale) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-row">
|
|
||||||
<span class="row-label">{{ t('wallet.note') || '备注' }}</span>
|
|
||||||
<span class="row-val">{{ txSummaryLabel(detail, t) || '-' }}</span>
|
<span class="row-val">{{ txSummaryLabel(detail, t) || '-' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="row-label">{{ t('wallet.balance_before') || '变动前余额' }}</span>
|
<span class="row-label">{{ t('wallet.balance_before') }}</span>
|
||||||
<span class="row-val mono">{{ formatMoney(detail.balanceBefore, locale) }}</span>
|
<span class="row-val mono">{{ formatMoney(detail.balanceBefore, locale) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="row-label">{{ t('wallet.balance_after') || '变动后余额' }}</span>
|
<span class="row-label">{{ t('wallet.balance_after') }}</span>
|
||||||
<span class="row-val mono">{{ formatMoney(detail.balanceAfter, locale) }}</span>
|
<span class="row-val mono">{{ formatMoney(detail.balanceAfter, locale) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="parseFloat(detail.frozenBefore || '0') > 0 || parseFloat(detail.frozenAfter || '0') > 0" class="info-row">
|
<div v-if="showFrozen(detail)" class="info-row">
|
||||||
<span class="row-label">{{ t('wallet.frozen_before') || '冻结前' }}</span>
|
<span class="row-label">{{ t('wallet.frozen_before') }}</span>
|
||||||
<span class="row-val mono">{{ formatMoney(detail.frozenBefore, locale) }}</span>
|
<span class="row-val mono">{{ formatMoney(detail.frozenBefore, locale) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="parseFloat(detail.frozenBefore || '0') > 0 || parseFloat(detail.frozenAfter || '0') > 0" class="info-row">
|
<div v-if="showFrozen(detail)" class="info-row">
|
||||||
<span class="row-label">{{ t('wallet.frozen_after') || '冻结后' }}</span>
|
<span class="row-label">{{ t('wallet.frozen_after') }}</span>
|
||||||
<span class="row-val mono">{{ formatMoney(detail.frozenAfter, locale) }}</span>
|
<span class="row-val mono">{{ formatMoney(detail.frozenAfter, locale) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="row-label">{{ t('wallet.time') || '时间' }}</span>
|
<span class="row-label">{{ t('wallet.time') }}</span>
|
||||||
<span class="row-val muted">{{ new Date(detail.createdAt).toLocaleString() }}</span>
|
<span class="row-val muted">{{ new Date(detail.createdAt).toLocaleString() }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="row-label">{{ t('wallet.transaction_id') || '流水号' }}</span>
|
<span class="row-label">{{ t('wallet.transaction_id') }}</span>
|
||||||
<span class="row-val mono muted">{{ detail.transactionId }}</span>
|
<span class="row-val mono muted">{{ detail.transactionId }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="betNo || cashbackBatchNo" class="link-actions">
|
||||||
|
<button v-if="betNo" type="button" class="link-btn" @click="goBetDetail">
|
||||||
|
{{ t('wallet.detail_bet_link') }} · {{ betNo }}
|
||||||
|
</button>
|
||||||
|
<button v-if="cashbackBatchNo" type="button" class="link-btn" @click="goCashbackDetail">
|
||||||
|
{{ t('wallet.detail_cashback_link') }} · {{ cashbackBatchNo }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-else class="modal-empty">{{ t('common.not_found') || '未找到记录' }}</div>
|
<div v-else class="modal-empty">{{ t('common.not_found') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -154,19 +198,19 @@ function txLabel(tx: TxDetail): string {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
background: rgba(0, 0, 0, 0.7);
|
background: rgba(0, 0, 0, 0.65);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-container {
|
.modal-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 460px;
|
max-width: 480px;
|
||||||
max-height: 80vh;
|
max-height: 82vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--desktop-sidebar-bg, rgba(20, 20, 20, 0.95));
|
background: var(--bg-card);
|
||||||
border: 1px solid var(--desktop-border, #262626);
|
border: 1px solid var(--border);
|
||||||
border-radius: 4px;
|
border-radius: 10px;
|
||||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,14 +218,14 @@ function txLabel(tx: TxDetail): string {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 12px 16px;
|
padding: 14px 18px;
|
||||||
border-bottom: 1px solid var(--desktop-border, #262626);
|
border-bottom: 1px solid var(--border);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-title {
|
.modal-title {
|
||||||
font-size: 14px;
|
font-size: 15px;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,26 +239,24 @@ function txLabel(tx: TxDetail): string {
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-close:hover { color: var(--text); }
|
.modal-close:hover {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0;
|
padding: 0 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-loading {
|
.modal-loading,
|
||||||
display: flex;
|
.modal-error,
|
||||||
justify-content: center;
|
.modal-empty {
|
||||||
align-items: center;
|
|
||||||
padding: 48px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-error {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 48px 20px;
|
padding: 48px 20px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
@@ -222,44 +264,63 @@ function txLabel(tx: TxDetail): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.retry-btn {
|
.retry-btn {
|
||||||
padding: 6px 18px;
|
padding: 7px 18px;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
border: 1px solid var(--border-gold-soft, rgba(212,175,55,0.25));
|
border: 1px solid var(--border-active);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--primary-light, #F0D875);
|
color: var(--primary-light);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-empty {
|
.amount-hero {
|
||||||
display: flex;
|
padding: 20px 18px 8px;
|
||||||
align-items: center;
|
font-size: 32px;
|
||||||
justify-content: center;
|
font-weight: 800;
|
||||||
padding: 48px 20px;
|
font-variant-numeric: tabular-nums;
|
||||||
color: var(--text-muted);
|
font-family: 'SF Mono', 'Consolas', monospace;
|
||||||
font-size: 13px;
|
line-height: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Info rows */
|
.amount-hero.pos {
|
||||||
.info-rows { }
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-hero.neg {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-label {
|
||||||
|
padding: 0 18px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-rows {
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.info-row {
|
.info-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 10px 16px;
|
padding: 11px 18px;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-row:last-child { border-bottom: none; }
|
.info-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
.row-label {
|
.row-label {
|
||||||
width: 100px;
|
width: 96px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-val {
|
.row-val {
|
||||||
@@ -270,13 +331,41 @@ function txLabel(tx: TxDetail): string {
|
|||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-val.pos { color: var(--primary-light, #F0D875); }
|
.row-val.muted {
|
||||||
.row-val.neg { color: var(--danger, #FF453A); }
|
color: var(--text-muted);
|
||||||
.row-val.muted { color: var(--text-muted); font-weight: 500; }
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.mono { font-family: 'SF Mono', 'Consolas', monospace; font-size: 12px; }
|
.mono {
|
||||||
|
font-family: 'SF Mono', 'Consolas', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 18px 16px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 9px 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--border-active);
|
||||||
|
background: rgba(0, 102, 204, 0.06);
|
||||||
|
color: var(--primary-light);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-btn:hover {
|
||||||
|
background: rgba(0, 102, 204, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
/* Transition */
|
|
||||||
.modal-fade-enter-active,
|
.modal-fade-enter-active,
|
||||||
.modal-fade-leave-active {
|
.modal-fade-leave-active {
|
||||||
transition: opacity 0.15s ease;
|
transition: opacity 0.15s ease;
|
||||||
|
|||||||
@@ -305,6 +305,8 @@ export default {
|
|||||||
transaction_detail: 'Transaction details',
|
transaction_detail: 'Transaction details',
|
||||||
balance_before: 'Balance before',
|
balance_before: 'Balance before',
|
||||||
balance_after: 'Balance after',
|
balance_after: 'Balance after',
|
||||||
|
frozen_before: 'Frozen before',
|
||||||
|
frozen_after: 'Frozen after',
|
||||||
transaction_id: 'Transaction ID',
|
transaction_id: 'Transaction ID',
|
||||||
detail_title: 'Wallet details',
|
detail_title: 'Wallet details',
|
||||||
recharge_btn: 'Deposit',
|
recharge_btn: 'Deposit',
|
||||||
|
|||||||
@@ -318,6 +318,8 @@ export default {
|
|||||||
transaction_detail: 'Butiran transaksi',
|
transaction_detail: 'Butiran transaksi',
|
||||||
balance_before: 'Baki sebelum',
|
balance_before: 'Baki sebelum',
|
||||||
balance_after: 'Baki selepas',
|
balance_after: 'Baki selepas',
|
||||||
|
frozen_before: 'Beku sebelum',
|
||||||
|
frozen_after: 'Beku selepas',
|
||||||
transaction_id: 'ID transaksi',
|
transaction_id: 'ID transaksi',
|
||||||
detail_title: 'Butiran bil',
|
detail_title: 'Butiran bil',
|
||||||
recharge_btn: 'Topup',
|
recharge_btn: 'Topup',
|
||||||
|
|||||||
@@ -305,6 +305,8 @@ export default {
|
|||||||
transaction_detail: '交易详情',
|
transaction_detail: '交易详情',
|
||||||
balance_before: '变动前余额',
|
balance_before: '变动前余额',
|
||||||
balance_after: '变动后余额',
|
balance_after: '变动后余额',
|
||||||
|
frozen_before: '变动前冻结',
|
||||||
|
frozen_after: '变动后冻结',
|
||||||
transaction_id: '流水号',
|
transaction_id: '流水号',
|
||||||
detail_title: '账单明细',
|
detail_title: '账单明细',
|
||||||
recharge_btn: '充值',
|
recharge_btn: '充值',
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ const router = createRouter({
|
|||||||
{ path: 'wallet/cashbacks', component: () => import('../views/CashbackRecordsView.vue'), meta: { requiresAuth: true } },
|
{ path: 'wallet/cashbacks', component: () => import('../views/CashbackRecordsView.vue'), meta: { requiresAuth: true } },
|
||||||
{ path: 'wallet/recharge', component: () => import('../views/RechargeView.vue'), meta: { requiresAuth: true } },
|
{ path: 'wallet/recharge', component: () => import('../views/RechargeView.vue'), meta: { requiresAuth: true } },
|
||||||
{ path: 'wallet/recharge/history', component: () => import('../views/RechargeHistoryView.vue'), meta: { requiresAuth: true } },
|
{ path: 'wallet/recharge/history', component: () => import('../views/RechargeHistoryView.vue'), meta: { requiresAuth: true } },
|
||||||
|
{ path: 'wallet/recharge/history/:id', component: () => import('../views/RechargeDetailView.vue'), meta: { requiresAuth: true } },
|
||||||
{ path: 'wallet/transactions/:transactionId', component: () => import('../views/WalletTransactionDetailView.vue'), meta: { requiresAuth: true } },
|
{ path: 'wallet/transactions/:transactionId', component: () => import('../views/WalletTransactionDetailView.vue'), meta: { requiresAuth: true } },
|
||||||
{ path: 'profile', redirect: '/wallet' },
|
{ path: 'profile', redirect: '/wallet' },
|
||||||
{ path: 'profile/cashbacks', redirect: '/wallet/cashbacks' },
|
{ path: 'profile/cashbacks', redirect: '/wallet/cashbacks' },
|
||||||
|
|||||||
@@ -208,10 +208,23 @@
|
|||||||
transition: background 0.15s;
|
transition: background 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.desktop-records-table tbody tr.hover-row:hover {
|
.desktop-records-table tbody tr.clickable-row {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-records-table tbody tr.hover-row:hover,
|
||||||
|
.desktop-records-table tbody tr.clickable-row:hover {
|
||||||
background: rgba(0, 102, 204, 0.06);
|
background: rgba(0, 102, 204, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.desktop-records-table .id-cell {
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: 'SF Mono', 'Consolas', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
.desktop-records-table .num-cell {
|
.desktop-records-table .num-cell {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
|
|||||||
63
apps/player/src/utils/depositOrderLookup.ts
Normal file
63
apps/player/src/utils/depositOrderLookup.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import api from '../api';
|
||||||
|
|
||||||
|
export type DepositAuditLog = {
|
||||||
|
id: string;
|
||||||
|
action: string;
|
||||||
|
actorType: string;
|
||||||
|
statusBefore: string | null;
|
||||||
|
statusAfter: string;
|
||||||
|
amount: string | null;
|
||||||
|
approvedAmount: string | null;
|
||||||
|
remark: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PlayerDepositOrder = {
|
||||||
|
id: string;
|
||||||
|
orderNo: string;
|
||||||
|
paymentMethodId?: string;
|
||||||
|
methodType: string;
|
||||||
|
amount: string;
|
||||||
|
status: string;
|
||||||
|
approvedAmount: string | null;
|
||||||
|
rejectReason: string | null;
|
||||||
|
remark: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
reviewedAt: string | null;
|
||||||
|
paymentMethodName: string | null;
|
||||||
|
bankName?: string | null;
|
||||||
|
auditLogs?: DepositAuditLog[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function fetchDepositOrderAuditLogs(orderId: string): Promise<DepositAuditLog[]> {
|
||||||
|
try {
|
||||||
|
const { data } = await api.get(`/player/deposit-orders/${orderId}/audit-logs`);
|
||||||
|
const items = data.data?.items ?? data.data ?? [];
|
||||||
|
return Array.isArray(items) ? (items as DepositAuditLog[]) : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function enrichDepositOrder(order: PlayerDepositOrder): Promise<PlayerDepositOrder> {
|
||||||
|
if (order.auditLogs?.length) return order;
|
||||||
|
const auditLogs = await fetchDepositOrderAuditLogs(order.id);
|
||||||
|
return auditLogs.length ? { ...order, auditLogs } : order;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function findDepositOrderById(orderId: string): Promise<PlayerDepositOrder | null> {
|
||||||
|
let page = 1;
|
||||||
|
const pageSize = 20;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { data } = await api.get('/player/deposit-orders', { params: { page, pageSize } });
|
||||||
|
const result = data.data ?? { items: [], total: 0, pageSize };
|
||||||
|
const items = (result.items ?? []) as PlayerDepositOrder[];
|
||||||
|
const found = items.find((order) => order.id === orderId);
|
||||||
|
if (found) return enrichDepositOrder(found);
|
||||||
|
|
||||||
|
const total = result.total ?? 0;
|
||||||
|
if (!items.length || page * pageSize >= total) return null;
|
||||||
|
page += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,24 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { watch } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { useViewport } from '../composables/useViewport';
|
import { useViewport } from '../composables/useViewport';
|
||||||
import MobileBetDetailView from './MobileBetDetailView.vue';
|
import MobileBetDetailView from './MobileBetDetailView.vue';
|
||||||
import DesktopBetDetailView from './desktop/DesktopBetDetailView.vue';
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
const { isDesktop } = useViewport();
|
const { isDesktop } = useViewport();
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [isDesktop.value, route.params.betNo] as const,
|
||||||
|
([desktop, betNo]) => {
|
||||||
|
if (desktop && betNo) {
|
||||||
|
router.replace({ path: '/bets', query: { betNo: String(betNo) } });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DesktopBetDetailView v-if="isDesktop" />
|
<MobileBetDetailView v-if="!isDesktop" />
|
||||||
<MobileBetDetailView v-else />
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted } from 'vue';
|
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
import GoldSpinner from '../components/GoldSpinner.vue';
|
import GoldSpinner from '../components/GoldSpinner.vue';
|
||||||
import { usePullToRefresh } from '../composables/usePullToRefresh';
|
import { usePullToRefresh } from '../composables/usePullToRefresh';
|
||||||
import { formatMoney } from '../utils/localeDisplay';
|
import { formatMoney } from '../utils/localeDisplay';
|
||||||
|
import { findDepositOrderById, enrichDepositOrder } from '../utils/depositOrderLookup';
|
||||||
import {
|
import {
|
||||||
auditActionTone,
|
auditActionTone,
|
||||||
auditActorSecondary,
|
auditActorSecondary,
|
||||||
@@ -14,6 +15,7 @@ import {
|
|||||||
} from '../utils/depositAuditDisplay';
|
} from '../utils/depositAuditDisplay';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
const { t, locale } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
|
|
||||||
interface DepositAuditLog {
|
interface DepositAuditLog {
|
||||||
@@ -85,7 +87,7 @@ const { pullDistance, spinning, progress } = usePullToRefresh({
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchOrders(1);
|
void fetchOrders(1).then(() => openDetailFromQuery());
|
||||||
observer = new IntersectionObserver(
|
observer = new IntersectionObserver(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
if (entries[0].isIntersecting && hasMore.value && !loading.value) {
|
if (entries[0].isIntersecting && hasMore.value && !loading.value) {
|
||||||
@@ -97,6 +99,10 @@ onMounted(() => {
|
|||||||
if (sentinel.value) observer.observe(sentinel.value);
|
if (sentinel.value) observer.observe(sentinel.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(() => route.query.orderId, () => {
|
||||||
|
void openDetailFromQuery();
|
||||||
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
observer?.disconnect();
|
observer?.disconnect();
|
||||||
});
|
});
|
||||||
@@ -121,14 +127,24 @@ function goRecharge() {
|
|||||||
router.push('/wallet/recharge');
|
router.push('/wallet/recharge');
|
||||||
}
|
}
|
||||||
|
|
||||||
function openDetail(order: DepositOrder) {
|
async function openDetail(order: DepositOrder) {
|
||||||
selectedOrder.value = order;
|
selectedOrder.value = await enrichDepositOrder(order);
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeDetail() {
|
function closeDetail() {
|
||||||
selectedOrder.value = null;
|
selectedOrder.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openDetailFromQuery() {
|
||||||
|
const orderId = route.query.orderId;
|
||||||
|
if (typeof orderId !== 'string' || !orderId) return;
|
||||||
|
const existing = items.value.find((order) => order.id === orderId);
|
||||||
|
const order = existing ?? await findDepositOrderById(orderId);
|
||||||
|
if (order) openDetail(order);
|
||||||
|
const { orderId: _removed, ...rest } = route.query;
|
||||||
|
void router.replace({ path: route.path, query: rest });
|
||||||
|
}
|
||||||
|
|
||||||
function reapply(order: DepositOrder) {
|
function reapply(order: DepositOrder) {
|
||||||
const query: Record<string, string> = {
|
const query: Record<string, string> = {
|
||||||
orderId: order.id,
|
orderId: order.id,
|
||||||
|
|||||||
21
apps/player/src/views/RechargeDetailView.vue
Normal file
21
apps/player/src/views/RechargeDetailView.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { watch } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.params.id,
|
||||||
|
(id) => {
|
||||||
|
if (!id) return;
|
||||||
|
router.replace({
|
||||||
|
path: '/wallet/recharge/history',
|
||||||
|
query: { orderId: String(id) },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template />
|
||||||
@@ -1,10 +1,24 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { watch } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { useViewport } from '../composables/useViewport';
|
import { useViewport } from '../composables/useViewport';
|
||||||
import MobileWalletTransactionDetailView from './MobileWalletTransactionDetailView.vue';
|
import MobileWalletTransactionDetailView from './MobileWalletTransactionDetailView.vue';
|
||||||
import DesktopWalletTransactionDetailView from './desktop/DesktopWalletTransactionDetailView.vue';
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
const { isDesktop } = useViewport();
|
const { isDesktop } = useViewport();
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [isDesktop.value, route.params.transactionId] as const,
|
||||||
|
([desktop, transactionId]) => {
|
||||||
|
if (desktop && transactionId) {
|
||||||
|
router.replace({ path: '/wallet/detail', query: { transactionId: String(transactionId) } });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DesktopWalletTransactionDetailView v-if="isDesktop" />
|
<MobileWalletTransactionDetailView v-if="!isDesktop" />
|
||||||
<MobileWalletTransactionDetailView v-else />
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,42 +1,25 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, computed, onMounted } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRoute, useRouter, RouterLink } from 'vue-router';
|
import { useRoute, useRouter, RouterLink } from 'vue-router';
|
||||||
import api from '../../api';
|
import api from '../../api';
|
||||||
import GoldSpinner from '../../components/GoldSpinner.vue';
|
import GoldSpinner from '../../components/GoldSpinner.vue';
|
||||||
|
import { formatMoney } from '../../utils/localeDisplay';
|
||||||
|
import type { BetHistoryItem } from '../../components/BetHistoryCard.vue';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
if (window.history.state && window.history.state.back) {
|
if (window.history.state?.back) {
|
||||||
router.back();
|
router.back();
|
||||||
} else {
|
} else {
|
||||||
router.push('/bets');
|
router.push('/bets');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type BetDetail = {
|
const detail = ref<BetHistoryItem | null>(null);
|
||||||
betNo: string;
|
|
||||||
stake: string;
|
|
||||||
potentialPayout?: string;
|
|
||||||
status: string;
|
|
||||||
createdAt: string;
|
|
||||||
selections: Array<{
|
|
||||||
matchName?: string;
|
|
||||||
selectionName?: string;
|
|
||||||
odds?: number;
|
|
||||||
market?: string;
|
|
||||||
homeScore?: number;
|
|
||||||
awayScore?: number;
|
|
||||||
matchStatus?: string;
|
|
||||||
[key: string]: any;
|
|
||||||
}>;
|
|
||||||
[key: string]: any;
|
|
||||||
};
|
|
||||||
|
|
||||||
const detail = ref<BetDetail | null>(null);
|
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const error = ref(false);
|
const error = ref(false);
|
||||||
|
|
||||||
@@ -58,7 +41,13 @@ async function loadDetail() {
|
|||||||
onMounted(loadDetail);
|
onMounted(loadDetail);
|
||||||
|
|
||||||
function statusClass(status: string) {
|
function statusClass(status: string) {
|
||||||
const map: Record<string, string> = { WON: 'status-won', LOST: 'status-lost', PENDING: 'status-pending', PUSH: 'status-push' };
|
const map: Record<string, string> = {
|
||||||
|
WON: 'status-won',
|
||||||
|
LOST: 'status-lost',
|
||||||
|
PENDING: 'status-pending',
|
||||||
|
PUSH: 'status-push',
|
||||||
|
CANCELLED: 'status-cancelled',
|
||||||
|
};
|
||||||
return map[status?.toUpperCase()] ?? 'status-default';
|
return map[status?.toUpperCase()] ?? 'status-default';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,9 +57,67 @@ function statusLabel(status: string) {
|
|||||||
LOST: t('history.filter_lost'),
|
LOST: t('history.filter_lost'),
|
||||||
PENDING: t('history.filter_pending'),
|
PENDING: t('history.filter_pending'),
|
||||||
PUSH: t('history.filter_push'),
|
PUSH: t('history.filter_push'),
|
||||||
|
CANCELLED: t('common.cancelled'),
|
||||||
};
|
};
|
||||||
return map[status?.toUpperCase()] ?? status;
|
return map[status?.toUpperCase()] ?? status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SEL_TRANS: Record<string, Record<string, string>> = {
|
||||||
|
'主胜': { 'en-US': 'Home Win', 'ms-MY': 'Rumah Menang' },
|
||||||
|
'客胜': { 'en-US': 'Away Win', 'ms-MY': 'Tandang Menang' },
|
||||||
|
'和局': { 'en-US': 'Draw', 'ms-MY': 'Seri' },
|
||||||
|
'主': { 'en-US': 'Home', 'ms-MY': 'Rumah' },
|
||||||
|
'客': { 'en-US': 'Away', 'ms-MY': 'Tandang' },
|
||||||
|
'大': { 'en-US': 'Over', 'ms-MY': 'Atas' },
|
||||||
|
'小': { 'en-US': 'Under', 'ms-MY': 'Bawah' },
|
||||||
|
'单': { 'en-US': 'Odd', 'ms-MY': 'Ganjil' },
|
||||||
|
'双': { 'en-US': 'Even', 'ms-MY': 'Genap' },
|
||||||
|
'冠军': { 'en-US': 'Winner', 'ms-MY': 'Juara' },
|
||||||
|
};
|
||||||
|
|
||||||
|
function translateSel(name: string): string {
|
||||||
|
if (locale.value === 'zh-CN') return name;
|
||||||
|
const exact = SEL_TRANS[name];
|
||||||
|
if (exact) return exact[locale.value] ?? exact['en-US'] ?? name;
|
||||||
|
const sp = name.indexOf(' ');
|
||||||
|
if (sp > 0) {
|
||||||
|
const head = name.slice(0, sp);
|
||||||
|
const m = SEL_TRANS[head];
|
||||||
|
if (m) return (m[locale.value] ?? m['en-US'] ?? head) + name.slice(sp);
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function legResultIcon(status?: string | null) {
|
||||||
|
switch (status?.toUpperCase()) {
|
||||||
|
case 'WON': return '✓';
|
||||||
|
case 'LOST': return '✗';
|
||||||
|
case 'PUSH': return '=';
|
||||||
|
default: return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function legResultClass(status?: string | null) {
|
||||||
|
switch (status?.toUpperCase()) {
|
||||||
|
case 'WON': return 'leg-won';
|
||||||
|
case 'LOST': return 'leg-lost';
|
||||||
|
case 'PUSH': return 'leg-push';
|
||||||
|
default: return 'leg-pending';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayLegs = computed(() => {
|
||||||
|
if (!detail.value) return [];
|
||||||
|
if (detail.value.legs?.length) return detail.value.legs;
|
||||||
|
return [{
|
||||||
|
matchTitle: detail.value.matchTitle,
|
||||||
|
marketLabel: '',
|
||||||
|
selectionName: detail.value.pickLabel,
|
||||||
|
odds: detail.value.totalOdds,
|
||||||
|
resultStatus: detail.value.status,
|
||||||
|
score: detail.value.matchScore ?? null,
|
||||||
|
}];
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -78,9 +125,9 @@ function statusLabel(status: string) {
|
|||||||
<main class="account-main">
|
<main class="account-main">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<button type="button" class="back-btn" @click="goBack">
|
<button type="button" class="back-btn" @click="goBack">
|
||||||
‹ {{ t('common.back') || '返回' }}
|
‹ {{ t('common.back') }}
|
||||||
</button>
|
</button>
|
||||||
<h1 class="page-title">{{ t('bet.detail_title') || '注单详情' }}</h1>
|
<h1 class="page-title">{{ t('bet.detail_title') }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="loading" class="loading-state">
|
<div v-if="loading" class="loading-state">
|
||||||
@@ -92,42 +139,69 @@ function statusLabel(status: string) {
|
|||||||
<button type="button" class="retry-btn" @click="loadDetail">{{ t('common.retry') }}</button>
|
<button type="button" class="retry-btn" @click="loadDetail">{{ t('common.retry') }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="detail" class="detail-layout">
|
<div v-else-if="detail" class="detail-grid">
|
||||||
<!-- Header info -->
|
<div class="detail-left">
|
||||||
<div class="detail-card header-card">
|
|
||||||
<div class="bet-no">{{ detail.betNo }}</div>
|
|
||||||
<span class="status-badge" :class="statusClass(detail.status)">{{ statusLabel(detail.status) }}</span>
|
|
||||||
<div class="amount-row">
|
|
||||||
<div class="amount-item">
|
|
||||||
<span class="amount-label">{{ t('bet.stake') || '投注额' }}</span>
|
|
||||||
<span class="amount-val">{{ detail.stake }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="amount-item">
|
|
||||||
<span class="amount-label">{{ t('bet.potential_payout') || '预计派彩' }}</span>
|
|
||||||
<span class="amount-val gold">{{ detail.potentialPayout || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="amount-item">
|
|
||||||
<span class="amount-label">{{ t('bet.placed_at') || '下注时间' }}</span>
|
|
||||||
<span class="amount-val date">{{ new Date(detail.createdAt).toLocaleString() }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Selections -->
|
|
||||||
<div class="detail-card">
|
<div class="detail-card">
|
||||||
<div class="card-title">{{ t('bet.selections') || '投注选项' }}</div>
|
<div class="card-title">{{ t('bet.selections') }}</div>
|
||||||
<div v-for="(sel, idx) in detail.selections" :key="idx" class="sel-row">
|
<div v-for="(leg, idx) in displayLegs" :key="idx" class="sel-row">
|
||||||
<div class="sel-match">{{ sel.matchName || t('bet.match') }}</div>
|
<div class="sel-row-left">
|
||||||
|
<span v-if="displayLegs.length > 1" class="sel-index">{{ idx + 1 }}</span>
|
||||||
|
<div class="sel-info">
|
||||||
|
<div class="sel-match">{{ leg.matchTitle || t('bet.match') }}</div>
|
||||||
<div class="sel-meta">
|
<div class="sel-meta">
|
||||||
<span class="sel-market">{{ sel.market || sel.marketName || '' }}</span>
|
<span v-if="leg.marketLabel" class="sel-market">{{ leg.marketLabel }}</span>
|
||||||
<span class="sel-name">{{ sel.selectionName || '' }}</span>
|
<span class="sel-name">{{ translateSel(leg.selectionName || '') }}</span>
|
||||||
<span class="sel-odds">@{{ sel.odds }}</span>
|
<span v-if="leg.score?.ft" class="sel-score">{{ t('history.ft') }}: {{ leg.score.ft }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sel-row-right">
|
||||||
|
<span class="sel-odds">@{{ leg.odds ?? '-' }}</span>
|
||||||
|
<span
|
||||||
|
v-if="leg.resultStatus"
|
||||||
|
class="leg-result"
|
||||||
|
:class="legResultClass(leg.resultStatus)"
|
||||||
|
>
|
||||||
|
{{ legResultIcon(leg.resultStatus) }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="empty-state">{{ t('common.not_found') || '未找到记录' }}</div>
|
<div class="detail-right">
|
||||||
|
<div class="detail-card summary-card">
|
||||||
|
<div class="card-title">{{ t('history.summary') }}</div>
|
||||||
|
<div class="summary-bet-no">
|
||||||
|
<span class="summary-label">{{ t('bet.bet_no') }}</span>
|
||||||
|
<span class="summary-value mono">{{ detail.betNo }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-status">
|
||||||
|
<span class="status-badge" :class="statusClass(detail.status)">{{ statusLabel(detail.status) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-amounts">
|
||||||
|
<div class="summary-amount-item">
|
||||||
|
<span class="summary-amount-label">{{ t('bet.stake') }}</span>
|
||||||
|
<span class="summary-amount-val">{{ formatMoney(detail.stake, locale) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-amount-item">
|
||||||
|
<span class="summary-amount-label">{{ t('bet.potential_payout') }}</span>
|
||||||
|
<span class="summary-amount-val accent">{{ formatMoney(detail.potentialReturn, locale) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-amount-item">
|
||||||
|
<span class="summary-amount-label">{{ t('bet.placed_at') }}</span>
|
||||||
|
<span class="summary-amount-val date">
|
||||||
|
{{ detail.placedAt ? new Date(detail.placedAt).toLocaleString() : '-' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RouterLink to="/bets" class="back-to-list-btn">{{ t('common.back') }}</RouterLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="empty-state">{{ t('common.not_found') }}</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -142,48 +216,136 @@ function statusLabel(status: string) {
|
|||||||
.page-title { font-size: 18px; font-weight: 800; color: var(--primary-light); margin: 0; }
|
.page-title { font-size: 18px; font-weight: 800; color: var(--primary-light); margin: 0; }
|
||||||
.loading-state { display: flex; justify-content: center; align-items: center; padding: 80px 0; }
|
.loading-state { display: flex; justify-content: center; align-items: center; padding: 80px 0; }
|
||||||
.error-state { display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 80px 20px; color: var(--text-muted); }
|
.error-state { display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 80px 20px; color: var(--text-muted); }
|
||||||
.retry-btn { padding: 8px 24px; border-radius: 6px; border: 1px solid var(--primary); background: transparent; color: var(--primary-light); font-size: 13px; font-weight: 700; cursor: pointer; }
|
.retry-btn { padding: 8px 24px; border-radius: 6px; border: 1px solid var(--border-active); background: transparent; color: var(--primary-light); font-size: 13px; font-weight: 700; cursor: pointer; }
|
||||||
.empty-state { display: flex; align-items: center; justify-content: center; padding: 80px 20px; color: var(--text-muted); font-size: 14px; font-weight: 600; }
|
.empty-state { display: flex; align-items: center; justify-content: center; padding: 80px 20px; color: var(--text-muted); font-size: 14px; font-weight: 600; }
|
||||||
|
|
||||||
.detail-layout { display: flex; flex-direction: column; gap: 16px; max-width: 680px; }
|
.detail-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 340px;
|
||||||
|
gap: 20px;
|
||||||
|
max-width: 1100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-left { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
|
||||||
|
.detail-right { display: flex; flex-direction: column; gap: 16px; }
|
||||||
|
|
||||||
.detail-card {
|
.detail-card {
|
||||||
background: var(--desktop-sidebar-bg);
|
background: var(--bg-card);
|
||||||
border: 1px solid var(--desktop-border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-card { display: flex; flex-direction: column; gap: 12px; }
|
.card-title {
|
||||||
|
font-size: 11px;
|
||||||
.bet-no {
|
|
||||||
font-family: 'SF Mono', 'Consolas', monospace;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
letter-spacing: 0.04em;
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-badge { display: inline-block; padding: 4px 14px; border-radius: 999px; font-size: 12px; font-weight: 700; align-self: flex-start; }
|
.sel-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 0;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sel-row:last-child { border-bottom: none; }
|
||||||
|
|
||||||
|
.sel-row-left { display: flex; align-items: center; gap: 12px; min-width: 0; }
|
||||||
|
|
||||||
|
.sel-index {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--bg-hover);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-muted);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sel-info { min-width: 0; }
|
||||||
|
.sel-match { font-size: 14px; font-weight: 700; color: var(--text); margin-bottom: 4px; }
|
||||||
|
.sel-meta { display: flex; gap: 10px; align-items: center; font-size: 12px; color: var(--text-muted); flex-wrap: wrap; }
|
||||||
|
.sel-name { color: var(--text); font-weight: 700; }
|
||||||
|
.sel-score { color: var(--text-muted); }
|
||||||
|
|
||||||
|
.sel-row-right { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
|
||||||
|
.sel-odds { color: var(--primary); font-weight: 800; font-size: 14px; }
|
||||||
|
|
||||||
|
.leg-result {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leg-won { background: rgba(52, 199, 89, 0.15); color: #4cd964; }
|
||||||
|
.leg-lost { background: rgba(255, 69, 58, 0.15); color: #ff453a; }
|
||||||
|
.leg-push { background: rgba(100, 100, 100, 0.15); color: #aaa; }
|
||||||
|
.leg-pending { background: rgba(0, 102, 204, 0.1); color: var(--primary-light); font-size: 10px; }
|
||||||
|
|
||||||
|
.summary-card { display: flex; flex-direction: column; gap: 16px; }
|
||||||
|
|
||||||
|
.summary-bet-no { display: flex; flex-direction: column; gap: 4px; }
|
||||||
|
.summary-label { font-size: 11px; color: var(--text-muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||||
|
.summary-value { font-size: 14px; font-weight: 700; color: var(--text); }
|
||||||
|
.mono { font-family: 'SF Mono', 'Consolas', monospace; letter-spacing: 0.03em; }
|
||||||
|
|
||||||
|
.summary-status { display: flex; }
|
||||||
|
|
||||||
|
.status-badge { display: inline-block; padding: 4px 14px; border-radius: 999px; font-size: 12px; font-weight: 700; }
|
||||||
.status-won { background: rgba(52, 199, 89, 0.12); color: #4cd964; }
|
.status-won { background: rgba(52, 199, 89, 0.12); color: #4cd964; }
|
||||||
.status-lost { background: rgba(255, 69, 58, 0.12); color: #ff453a; }
|
.status-lost { background: rgba(255, 69, 58, 0.12); color: #ff453a; }
|
||||||
.status-pending { background: rgba(0, 102, 204, 0.12); color: var(--primary-light); }
|
.status-pending { background: rgba(0, 102, 204, 0.12); color: var(--primary-light); }
|
||||||
.status-push { background: rgba(100, 100, 100, 0.12); color: #aaa; }
|
.status-push { background: rgba(100, 100, 100, 0.12); color: #aaa; }
|
||||||
|
.status-cancelled { background: rgba(100, 100, 100, 0.1); color: #888; }
|
||||||
.status-default { background: rgba(100, 100, 100, 0.1); color: #888; }
|
.status-default { background: rgba(100, 100, 100, 0.1); color: #888; }
|
||||||
|
|
||||||
.amount-row { display: flex; gap: 32px; padding-top: 4px; flex-wrap: wrap; }
|
.summary-amounts { display: flex; flex-direction: column; gap: 14px; padding-top: 16px; border-top: 1px solid var(--border); }
|
||||||
.amount-item { display: flex; flex-direction: column; gap: 4px; }
|
.summary-amount-item { display: flex; flex-direction: column; gap: 4px; }
|
||||||
.amount-label { font-size: 11px; color: var(--text-muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; }
|
.summary-amount-label { font-size: 11px; color: var(--text-muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||||
.amount-val { font-size: 18px; font-weight: 800; color: var(--text); font-variant-numeric: tabular-nums; }
|
.summary-amount-val { font-size: 18px; font-weight: 800; color: var(--text); font-variant-numeric: tabular-nums; }
|
||||||
.amount-val.gold { color: var(--primary-light); }
|
.summary-amount-val.accent { color: var(--primary); }
|
||||||
.amount-val.date { font-size: 13px; color: var(--text-muted); font-weight: 600; }
|
.summary-amount-val.date { font-size: 13px; color: var(--text-muted); font-weight: 600; }
|
||||||
|
|
||||||
.card-title { font-size: 11px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 14px; padding-bottom: 10px; border-bottom: 1px solid var(--desktop-border); }
|
.back-to-list-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
.sel-row { padding: 12px 0; border-bottom: 1px solid rgba(0, 0, 0, 0.04); }
|
.back-to-list-btn:hover {
|
||||||
.sel-row:last-child { border-bottom: none; }
|
border-color: var(--border-active);
|
||||||
.sel-match { font-size: 14px; font-weight: 700; color: var(--text); margin-bottom: 4px; }
|
color: var(--primary-light);
|
||||||
.sel-meta { display: flex; gap: 10px; align-items: center; font-size: 12px; color: var(--text-muted); }
|
background: rgba(0, 102, 204, 0.06);
|
||||||
.sel-name { color: var(--text); font-weight: 700; }
|
}
|
||||||
.sel-odds { color: var(--primary-light); font-weight: 800; margin-left: auto; }
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.detail-grid { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,22 +1,48 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, onMounted, onActivated, watch } from 'vue';
|
import { computed, ref, onMounted, onActivated, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import api from '../../api';
|
import api from '../../api';
|
||||||
import GoldSpinner from '../../components/GoldSpinner.vue';
|
import GoldSpinner from '../../components/GoldSpinner.vue';
|
||||||
import Pagination from '../../components/desktop/Pagination.vue';
|
import Pagination from '../../components/desktop/Pagination.vue';
|
||||||
import DesktopWalletPageHeader from '../../components/desktop/DesktopWalletPageHeader.vue';
|
import DesktopWalletPageHeader from '../../components/desktop/DesktopWalletPageHeader.vue';
|
||||||
|
import DesktopCashbackDetailModal from '../../components/desktop/DesktopCashbackDetailModal.vue';
|
||||||
import { formatMoney, formatMoneyCompact } from '../../utils/localeDisplay';
|
import { formatMoney, formatMoneyCompact } from '../../utils/localeDisplay';
|
||||||
import { parseCashbackApiData, type CashbackRecord } from '../../utils/cashback';
|
import { parseCashbackApiData, type CashbackRecord } from '../../utils/cashback';
|
||||||
|
|
||||||
const COL_COUNT = 6;
|
const COL_COUNT = 6;
|
||||||
|
|
||||||
const { t, locale } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const detailModalVisible = ref(false);
|
||||||
|
const detailRecord = ref<CashbackRecord | null>(null);
|
||||||
|
|
||||||
const allItems = ref<CashbackRecord[]>([]);
|
const allItems = ref<CashbackRecord[]>([]);
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const page = ref(1);
|
const page = ref(1);
|
||||||
const pageSize = ref(20);
|
const pageSize = ref(20);
|
||||||
|
|
||||||
|
function openDetail(record: CashbackRecord) {
|
||||||
|
detailRecord.value = record;
|
||||||
|
detailModalVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDetailFromQuery() {
|
||||||
|
const batchNo = route.query.batchNo;
|
||||||
|
if (typeof batchNo !== 'string' || !batchNo) return;
|
||||||
|
const match = allItems.value.find((row) => row.batchNo === batchNo);
|
||||||
|
if (match) {
|
||||||
|
openDetail(match);
|
||||||
|
const { batchNo: _removed, ...rest } = route.query;
|
||||||
|
void router.replace({ path: route.path, query: rest });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => route.query.batchNo, openDetailFromQuery);
|
||||||
|
watch(allItems, openDetailFromQuery);
|
||||||
|
|
||||||
const total = computed(() => allItems.value.length);
|
const total = computed(() => allItems.value.length);
|
||||||
|
|
||||||
const items = computed(() => {
|
const items = computed(() => {
|
||||||
@@ -75,8 +101,14 @@ watch(pageSize, () => {
|
|||||||
page.value = 1;
|
page.value = 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(loadData);
|
onMounted(async () => {
|
||||||
onActivated(loadData);
|
await loadData();
|
||||||
|
openDetailFromQuery();
|
||||||
|
});
|
||||||
|
onActivated(async () => {
|
||||||
|
await loadData();
|
||||||
|
openDetailFromQuery();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -112,7 +144,12 @@ onActivated(loadData);
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<template v-else-if="items.length">
|
<template v-else-if="items.length">
|
||||||
<tr v-for="row in items" :key="row.id" class="hover-row">
|
<tr
|
||||||
|
v-for="row in items"
|
||||||
|
:key="row.id"
|
||||||
|
class="hover-row clickable-row"
|
||||||
|
@click="openDetail(row)"
|
||||||
|
>
|
||||||
<td>{{ formatPeriod(row.periodStart, row.periodEnd) }}</td>
|
<td>{{ formatPeriod(row.periodStart, row.periodEnd) }}</td>
|
||||||
<td>{{ formatMoneyCompact(row.effectiveStake, locale) }}</td>
|
<td>{{ formatMoneyCompact(row.effectiveStake, locale) }}</td>
|
||||||
<td>{{ formatRate(row.rate) }}</td>
|
<td>{{ formatRate(row.rate) }}</td>
|
||||||
@@ -139,6 +176,8 @@ onActivated(loadData);
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<DesktopCashbackDetailModal v-model:visible="detailModalVisible" :record="detailRecord" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,38 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onActivated } from 'vue';
|
import { ref, onMounted, onActivated, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { RouterLink } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import api from '../../api';
|
import api from '../../api';
|
||||||
import GoldSpinner from '../../components/GoldSpinner.vue';
|
import GoldSpinner from '../../components/GoldSpinner.vue';
|
||||||
import Pagination from '../../components/desktop/Pagination.vue';
|
import Pagination from '../../components/desktop/Pagination.vue';
|
||||||
|
import DesktopBetDetailModal from '../../components/desktop/DesktopBetDetailModal.vue';
|
||||||
import { type BetHistoryItem } from '../../components/BetHistoryCard.vue';
|
import { type BetHistoryItem } from '../../components/BetHistoryCard.vue';
|
||||||
import { useOnLocaleChange } from '../../composables/useOnLocaleChange';
|
import { useOnLocaleChange } from '../../composables/useOnLocaleChange';
|
||||||
|
|
||||||
const COL_COUNT = 8;
|
const COL_COUNT = 8;
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const detailModalVisible = ref(false);
|
||||||
|
const detailBetNo = ref<string | null>(null);
|
||||||
|
|
||||||
|
function openDetail(betNo: string) {
|
||||||
|
detailBetNo.value = betNo;
|
||||||
|
detailModalVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDetailFromQuery() {
|
||||||
|
const betNo = route.query.betNo;
|
||||||
|
if (typeof betNo === 'string' && betNo) {
|
||||||
|
openDetail(betNo);
|
||||||
|
const { betNo: _removed, ...rest } = route.query;
|
||||||
|
void router.replace({ path: route.path, query: rest });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => route.query.betNo, openDetailFromQuery);
|
||||||
|
|
||||||
const items = ref<BetHistoryItem[]>([]);
|
const items = ref<BetHistoryItem[]>([]);
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
@@ -60,8 +82,14 @@ useOnLocaleChange(() => {
|
|||||||
void loadPage(1);
|
void loadPage(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
onActivated(() => { void loadPage(page.value); });
|
onActivated(() => {
|
||||||
onMounted(() => { void loadPage(1); });
|
void loadPage(page.value);
|
||||||
|
openDetailFromQuery();
|
||||||
|
});
|
||||||
|
onMounted(() => {
|
||||||
|
void loadPage(1);
|
||||||
|
openDetailFromQuery();
|
||||||
|
});
|
||||||
|
|
||||||
function statusClass(status: string) {
|
function statusClass(status: string) {
|
||||||
const map: Record<string, string> = {
|
const map: Record<string, string> = {
|
||||||
@@ -149,10 +177,13 @@ function statusLabel(status: string) {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<template v-else-if="items.length">
|
<template v-else-if="items.length">
|
||||||
<tr v-for="bet in items" :key="bet.betNo" class="hover-row">
|
<tr
|
||||||
<td>
|
v-for="bet in items"
|
||||||
<RouterLink :to="`/bets/${bet.betNo}`" class="link-cell">{{ bet.betNo }}</RouterLink>
|
:key="bet.betNo"
|
||||||
</td>
|
class="hover-row clickable-row"
|
||||||
|
@click="openDetail(bet.betNo)"
|
||||||
|
>
|
||||||
|
<td class="id-cell">{{ bet.betNo }}</td>
|
||||||
<td>{{ bet.matchTitle || '-' }}</td>
|
<td>{{ bet.matchTitle || '-' }}</td>
|
||||||
<td class="muted-cell">{{ bet.pickLabel || '-' }}</td>
|
<td class="muted-cell">{{ bet.pickLabel || '-' }}</td>
|
||||||
<td class="num-cell">{{ bet.totalOdds ?? '-' }}</td>
|
<td class="num-cell">{{ bet.totalOdds ?? '-' }}</td>
|
||||||
@@ -183,6 +214,8 @@ function statusLabel(status: string) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<DesktopBetDetailModal v-model:visible="detailModalVisible" :bet-no="detailBetNo" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,50 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onActivated } from 'vue';
|
import { ref, onMounted, onActivated, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { RouterLink } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import api from '../../api';
|
import api from '../../api';
|
||||||
import GoldSpinner from '../../components/GoldSpinner.vue';
|
import GoldSpinner from '../../components/GoldSpinner.vue';
|
||||||
import Pagination from '../../components/desktop/Pagination.vue';
|
import Pagination from '../../components/desktop/Pagination.vue';
|
||||||
import DesktopWalletPageHeader from '../../components/desktop/DesktopWalletPageHeader.vue';
|
import DesktopWalletPageHeader from '../../components/desktop/DesktopWalletPageHeader.vue';
|
||||||
|
import DesktopRechargeDetailModal from '../../components/desktop/DesktopRechargeDetailModal.vue';
|
||||||
import { formatMoney } from '../../utils/localeDisplay';
|
import { formatMoney } from '../../utils/localeDisplay';
|
||||||
|
import { enrichDepositOrder, findDepositOrderById, type PlayerDepositOrder } from '../../utils/depositOrderLookup';
|
||||||
|
|
||||||
const COL_COUNT = 5;
|
const COL_COUNT = 5;
|
||||||
|
|
||||||
const { t, locale } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
type RechargeOrder = {
|
const detailModalVisible = ref(false);
|
||||||
id: string;
|
const detailOrder = ref<PlayerDepositOrder | null>(null);
|
||||||
orderNo: string;
|
|
||||||
amount: string;
|
|
||||||
status: string;
|
|
||||||
methodType?: string;
|
|
||||||
bankName?: string | null;
|
|
||||||
paymentMethodName?: string | null;
|
|
||||||
approvedAmount?: string | null;
|
|
||||||
createdAt: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const items = ref<RechargeOrder[]>([]);
|
async function openDetail(order: PlayerDepositOrder) {
|
||||||
|
detailOrder.value = await enrichDepositOrder(order);
|
||||||
|
detailModalVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openDetailFromQuery() {
|
||||||
|
const orderId = route.query.orderId;
|
||||||
|
if (typeof orderId !== 'string' || !orderId) return;
|
||||||
|
|
||||||
|
const existing = items.value.find((order) => order.id === orderId);
|
||||||
|
const order = existing ?? await findDepositOrderById(orderId);
|
||||||
|
if (order) openDetail(order);
|
||||||
|
|
||||||
|
const { orderId: _removed, ...rest } = route.query;
|
||||||
|
void router.replace({ path: route.path, query: rest });
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => route.query.orderId, () => {
|
||||||
|
void openDetailFromQuery();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(detailModalVisible, (visible) => {
|
||||||
|
if (!visible) detailOrder.value = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const items = ref<PlayerDepositOrder[]>([]);
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const page = ref(1);
|
const page = ref(1);
|
||||||
const pageSize = ref(20);
|
const pageSize = ref(20);
|
||||||
@@ -46,10 +66,16 @@ async function loadPage(p: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => loadPage(1));
|
onMounted(async () => {
|
||||||
onActivated(() => loadPage(page.value));
|
await loadPage(1);
|
||||||
|
await openDetailFromQuery();
|
||||||
|
});
|
||||||
|
onActivated(async () => {
|
||||||
|
await loadPage(page.value);
|
||||||
|
await openDetailFromQuery();
|
||||||
|
});
|
||||||
|
|
||||||
function methodLabel(order: RechargeOrder) {
|
function methodLabel(order: PlayerDepositOrder) {
|
||||||
if (order.methodType === 'USDT') return 'USDT';
|
if (order.methodType === 'USDT') return 'USDT';
|
||||||
return order.paymentMethodName || order.bankName || order.methodType || '-';
|
return order.paymentMethodName || order.bankName || order.methodType || '-';
|
||||||
}
|
}
|
||||||
@@ -96,8 +122,13 @@ function statusLabel(status: string) {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<template v-else-if="items.length">
|
<template v-else-if="items.length">
|
||||||
<tr v-for="order in items" :key="order.id" class="hover-row">
|
<tr
|
||||||
<td class="order-no">{{ order.orderNo || order.id }}</td>
|
v-for="order in items"
|
||||||
|
:key="order.id"
|
||||||
|
class="hover-row clickable-row"
|
||||||
|
@click="openDetail(order)"
|
||||||
|
>
|
||||||
|
<td class="id-cell">{{ order.orderNo || order.id }}</td>
|
||||||
<td>{{ methodLabel(order) }}</td>
|
<td>{{ methodLabel(order) }}</td>
|
||||||
<td class="num-cell">{{ formatMoney(order.amount, locale) }}</td>
|
<td class="num-cell">{{ formatMoney(order.amount, locale) }}</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -125,12 +156,7 @@ function statusLabel(status: string) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<DesktopRechargeDetailModal v-model:visible="detailModalVisible" :order="detailOrder" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.order-no {
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--primary-light);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onActivated } from 'vue';
|
import { ref, onMounted, onActivated, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import api from '../../api';
|
import api from '../../api';
|
||||||
import GoldSpinner from '../../components/GoldSpinner.vue';
|
import GoldSpinner from '../../components/GoldSpinner.vue';
|
||||||
import Pagination from '../../components/desktop/Pagination.vue';
|
import Pagination from '../../components/desktop/Pagination.vue';
|
||||||
@@ -10,6 +11,8 @@ import { formatMoney } from '../../utils/localeDisplay';
|
|||||||
import { txTypeKey, txDisplayType, txAmountClass, txSummaryLabel } from '../../utils/walletTx';
|
import { txTypeKey, txDisplayType, txAmountClass, txSummaryLabel } from '../../utils/walletTx';
|
||||||
|
|
||||||
const { t, locale } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const detailModalVisible = ref(false);
|
const detailModalVisible = ref(false);
|
||||||
const detailTxId = ref<string | null>(null);
|
const detailTxId = ref<string | null>(null);
|
||||||
@@ -19,6 +22,17 @@ function openDetail(txId: string) {
|
|||||||
detailModalVisible.value = true;
|
detailModalVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openDetailFromQuery() {
|
||||||
|
const transactionId = route.query.transactionId;
|
||||||
|
if (typeof transactionId === 'string' && transactionId) {
|
||||||
|
openDetail(transactionId);
|
||||||
|
const { transactionId: _removed, ...rest } = route.query;
|
||||||
|
void router.replace({ path: route.path, query: rest });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => route.query.transactionId, openDetailFromQuery);
|
||||||
|
|
||||||
type Transaction = {
|
type Transaction = {
|
||||||
transactionType: string;
|
transactionType: string;
|
||||||
displayType?: string;
|
displayType?: string;
|
||||||
@@ -43,6 +57,10 @@ function txLabel(tx: Transaction) {
|
|||||||
return tx.transactionType;
|
return tx.transactionType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function txSubtitle(tx: Transaction): string {
|
||||||
|
return txSummaryLabel(tx, t);
|
||||||
|
}
|
||||||
|
|
||||||
function amountClass(amount: string) {
|
function amountClass(amount: string) {
|
||||||
return `desktop-records-amount ${txAmountClass(amount)}`;
|
return `desktop-records-amount ${txAmountClass(amount)}`;
|
||||||
}
|
}
|
||||||
@@ -65,8 +83,14 @@ async function fetchData(p = 1) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => fetchData(1));
|
onMounted(() => {
|
||||||
onActivated(() => fetchData(page.value));
|
void fetchData(1);
|
||||||
|
openDetailFromQuery();
|
||||||
|
});
|
||||||
|
onActivated(() => {
|
||||||
|
void fetchData(page.value);
|
||||||
|
openDetailFromQuery();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -94,11 +118,11 @@ onActivated(() => fetchData(page.value));
|
|||||||
<tr
|
<tr
|
||||||
v-for="tx in items"
|
v-for="tx in items"
|
||||||
:key="tx.transactionId"
|
:key="tx.transactionId"
|
||||||
class="hover-row"
|
class="hover-row clickable-row"
|
||||||
@click="openDetail(tx.transactionId)"
|
@click="openDetail(tx.transactionId)"
|
||||||
>
|
>
|
||||||
<td class="type-cell">{{ txLabel(tx) }}</td>
|
<td class="type-cell">{{ txLabel(tx) }}</td>
|
||||||
<td class="muted-cell">{{ txSummaryLabel(tx, t) || '-' }}</td>
|
<td class="muted-cell">{{ txSubtitle(tx) || '-' }}</td>
|
||||||
<td :class="['num-cell', amountClass(tx.amount)]">{{ formatMoney(tx.amount, locale) }}</td>
|
<td :class="['num-cell', amountClass(tx.amount)]">{{ formatMoney(tx.amount, locale) }}</td>
|
||||||
<td class="date-cell">{{ new Date(tx.createdAt).toLocaleString() }}</td>
|
<td class="date-cell">{{ new Date(tx.createdAt).toLocaleString() }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -120,11 +144,7 @@ onActivated(() => fetchData(page.value));
|
|||||||
<div class="desktop-records-empty">{{ t('wallet.no_records') }}</div>
|
<div class="desktop-records-empty">{{ t('wallet.no_records') }}</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<DesktopTxDetailModal
|
<DesktopTxDetailModal v-model:visible="detailModalVisible" :transaction-id="detailTxId" />
|
||||||
:visible="detailModalVisible"
|
|
||||||
:transaction-id="detailTxId"
|
|
||||||
@update:visible="detailModalVisible = $event"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user