Files
thebet365/apps/player/src/views/MobileRechargeHistoryView.vue
Mars 66ac69c7a7 feat(player): 充值订单详情与审核记录,优化桌面端交互与记录列表样式
新增充值详情页及单条订单 API,修复桌面充值提交路径;充值/注单记录编号改为中性色,并包含桌面首页与投注相关 UI 改进。
2026-07-02 10:47:46 +08:00

347 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import api from '../api';
import GoldSpinner from '../components/GoldSpinner.vue';
import { usePullToRefresh } from '../composables/usePullToRefresh';
import { formatMoney } from '../utils/localeDisplay';
import { useDepositNotifications } from '../composables/useDepositNotifications';
const router = useRouter();
const { t, locale } = useI18n();
const { trackPendingOrder, pollOnce } = useDepositNotifications();
interface DepositOrder {
id: string;
orderNo: string;
methodType: string;
amount: string;
status: string;
approvedAmount: string | null;
rejectReason: string | null;
remark: string | null;
createdAt: string;
reviewedAt: string | null;
paymentMethodName: string | null;
auditLogs?: { id: string }[];
}
const items = ref<DepositOrder[]>([]);
const loading = ref(false);
const initialLoading = ref(true);
const page = ref(1);
const total = ref(0);
const hasMore = ref(true);
const sentinel = ref<HTMLElement | null>(null);
let observer: IntersectionObserver | null = null;
async function fetchOrders(p = 1) {
if (loading.value) return;
loading.value = true;
try {
const { data } = await api.get('/player/deposit-orders', { params: { page: p } });
const result = data.data ?? { items: [], total: 0, pageSize: 20 };
const newItems = result.items ?? [];
if (p === 1) {
items.value = newItems;
for (const order of newItems) {
if (order.status === 'PENDING') trackPendingOrder(order.id);
}
void pollOnce();
} else {
items.value = [...items.value, ...newItems];
}
total.value = result.total ?? 0;
const pageSize = result.pageSize ?? 20;
hasMore.value = newItems.length >= pageSize && items.value.length < total.value;
page.value = p;
} catch { /* */ } finally {
loading.value = false;
initialLoading.value = false;
}
}
const { pullDistance, spinning, progress } = usePullToRefresh({
onRefresh: async () => { await fetchOrders(1); },
});
onMounted(() => {
fetchOrders(1);
observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasMore.value && !loading.value) {
fetchOrders(page.value + 1);
}
},
{ rootMargin: '200px' },
);
if (sentinel.value) observer.observe(sentinel.value);
});
onUnmounted(() => {
observer?.disconnect();
});
function statusClass(s: string) {
if (s === 'APPROVED') return 'status-approved';
if (s === 'REJECTED') return 'status-rejected';
return 'status-pending';
}
function statusLabel(s: string) {
if (s === 'APPROVED') return t('recharge.status_approved');
if (s === 'REJECTED') return t('recharge.status_rejected');
return t('recharge.status_pending');
}
function goBack() {
router.push('/wallet');
}
function goRecharge() {
router.push('/wallet/recharge');
}
function openDetail(order: DepositOrder) {
router.push(`/wallet/recharge/history/${order.id}`);
}
function normalizeText(value: string | null | undefined) {
return value?.trim() ?? '';
}
function orderNoteLine(order: DepositOrder) {
const rejectReason = normalizeText(order.rejectReason);
const remark = normalizeText(order.remark);
if (order.status === 'REJECTED') {
const text = rejectReason || remark;
if (!text) return null;
return `${t('recharge.reject_reason')}: ${text}`;
}
if (remark) return `${t('recharge.remark')}: ${remark}`;
return null;
}
function formatOrderTime(iso: string) {
return new Date(iso).toLocaleString();
}
function auditStepCount(order: DepositOrder) {
return order.auditLogs?.length ?? 0;
}
</script>
<template>
<div class="recharge-history-page">
<div class="page-header">
<button class="back-btn" @click="goBack"></button>
<h2>{{ t('recharge.history_title') }}</h2>
<button class="recharge-btn" @click="goRecharge">+ {{ t('recharge.title') }}</button>
</div>
<div
class="pull-indicator"
:style="{ height: `${pullDistance}px`, opacity: Math.min(pullDistance / 48, 1) }"
>
<GoldSpinner v-if="spinning" :size="28" :progress="progress" :active="spinning" />
</div>
<div v-if="initialLoading && loading" class="state">
<GoldSpinner :size="36" />
</div>
<template v-else>
<div v-if="!items.length" class="empty">{{ t('recharge.no_orders') }}</div>
<div v-else class="order-list">
<div
v-for="order in items"
:key="order.id"
class="order-card"
:class="{ rejected: order.status === 'REJECTED' }"
role="button"
tabindex="0"
@click="openDetail(order)"
@keydown.enter="openDetail(order)"
>
<div class="order-header">
<span class="method-badge" :class="order.methodType === 'BANK' ? 'bank' : 'usdt'">{{ order.methodType }}</span>
<span :class="['status-badge', statusClass(order.status)]">{{ statusLabel(order.status) }}</span>
</div>
<div class="order-body">
<div class="order-no">{{ order.orderNo }}</div>
<div class="order-amount">{{ formatMoney(order.amount, locale) }}</div>
<div v-if="order.approvedAmount && order.approvedAmount !== order.amount" class="approved-amount">
{{ t('recharge.credited') }}: {{ formatMoney(order.approvedAmount, locale) }}
</div>
<div class="order-info-row">
<span class="info-label">{{ order.paymentMethodName || '-' }}</span>
</div>
<div class="order-times">
<div class="time-row">
<span class="time-label">{{ t('recharge.apply_time') }}</span>
<span class="time-value">{{ formatOrderTime(order.createdAt) }}</span>
</div>
<div v-if="order.reviewedAt" class="time-row">
<span class="time-label">{{ t('recharge.review_time') }}</span>
<span class="time-value">{{ formatOrderTime(order.reviewedAt) }}</span>
</div>
</div>
<div
v-if="orderNoteLine(order)"
:class="order.status === 'REJECTED' ? 'reject-reason' : 'order-remark'"
>
{{ orderNoteLine(order) }}
</div>
<div class="card-detail-hint">
<span v-if="auditStepCount(order)" class="card-detail-summary">
{{ t('recharge.audit_summary', { count: auditStepCount(order) }) }}
</span>
<span class="card-detail-link">{{ t('recharge.view_detail') }} </span>
</div>
</div>
</div>
</div>
<div ref="sentinel" class="sentinel" />
<div v-if="loading && items.length > 0" class="load-more-spinner">
<GoldSpinner :size="24" />
</div>
<div v-else-if="!hasMore && items.length > 0" class="end-hint">
{{ t('common.no_more') }}
</div>
</template>
</div>
</template>
<style scoped>
.recharge-history-page { padding: 0 0 24px; }
.page-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 0; }
.page-header h2 { margin: 0; font-size: 17px; font-weight: 700; }
.back-btn { background: none; border: none; color: var(--primary-light); font-size: 24px; cursor: pointer; padding: 0 8px; }
.recharge-btn { background: none; border: none; color: var(--primary-light); font-size: 13px; cursor: pointer; font-weight: 600; }
.state { display: flex; justify-content: center; padding: 48px; }
.empty { text-align: center; color: #666; padding: 48px 16px; font-weight: 600; }
.pull-indicator { display: flex; align-items: center; justify-content: center; overflow: hidden; transition: height 0.15s ease; }
.order-list { display: flex; flex-direction: column; gap: 12px; }
.order-card {
background: linear-gradient(135deg, #1a1810 0%, #1f1b0e 40%, #16140c 100%);
border: 1px solid rgba(212, 175, 55, 0.2);
border-radius: 12px;
padding: 16px;
position: relative;
overflow: hidden;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.order-card:active { opacity: 0.92; }
.order-card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(212, 175, 55, 0.6), transparent);
}
.order-card.rejected {
background: #141414;
border-color: rgba(245, 108, 108, 0.22);
}
.order-card.rejected::before {
background: linear-gradient(90deg, transparent, rgba(245, 108, 108, 0.35), transparent);
}
.order-card.rejected .order-amount {
color: #bbb;
}
.order-card.rejected .info-label { color: #888; }
.order-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.method-badge { padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 700; }
.method-badge.bank { background: rgba(30, 58, 95, 0.6); color: #66b1ff; }
.method-badge.usdt { background: rgba(26, 58, 42, 0.6); color: #67c23a; }
.status-badge { font-size: 12px; font-weight: 700; }
.status-pending { color: #e6a23c; }
.status-approved { color: #67c23a; }
.status-rejected { color: #f56c6c; }
.order-amount {
font-size: 22px;
font-weight: 900;
margin-bottom: 4px;
color: var(--text);
}
.order-no {
font-size: 11px;
color: var(--text-muted);
font-family: ui-monospace, monospace;
margin-bottom: 6px;
letter-spacing: 0.02em;
}
.approved-amount { font-size: 12px; color: #67c23a; margin-bottom: 6px; font-weight: 600; }
.order-info-row { margin-bottom: 8px; }
.info-label { font-size: 13px; color: rgba(212, 175, 55, 0.7); font-weight: 600; }
.order-times { display: flex; flex-direction: column; gap: 4px; margin-bottom: 6px; }
.time-row { display: flex; justify-content: space-between; align-items: center; }
.time-label { font-size: 11px; color: #888; }
.time-value { font-size: 11px; color: #aaa; font-variant-numeric: tabular-nums; }
.order-remark {
font-size: 12px;
color: rgba(212, 175, 55, 0.8);
background: rgba(212, 175, 55, 0.06);
padding: 6px 10px;
border-radius: 6px;
margin-top: 6px;
border-left: 2px solid rgba(212, 175, 55, 0.3);
line-height: 1.45;
word-break: break-word;
}
.reject-reason {
margin-top: 8px;
font-size: 12px;
color: #c07070;
background: transparent;
padding: 6px 0 0;
border-radius: 0;
border: none;
border-top: 1px solid rgba(245, 108, 108, 0.18);
line-height: 1.45;
word-break: break-word;
}
.card-detail-hint {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid rgba(212, 175, 55, 0.12);
}
.card-detail-summary {
font-size: 11px;
color: var(--text-muted);
font-weight: 600;
}
.card-detail-link {
font-size: 11px;
color: var(--text-muted);
font-weight: 700;
white-space: nowrap;
}
.sentinel { height: 1px; }
.load-more-spinner { display: flex; justify-content: center; padding: 20px 0 8px; }
.end-hint {
text-align: center;
font-size: 12px;
color: #555;
font-weight: 600;
padding: 16px 0 4px;
letter-spacing: 0.03em;
}
</style>