feat(theme-4): fix dark backgrounds, quick bet, inbox spacing, customer service theme
This commit is contained in:
343
apps/player/src/views/MobileRechargeDetailView.vue
Normal file
343
apps/player/src/views/MobileRechargeDetailView.vue
Normal file
@@ -0,0 +1,343 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRoute, 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 {
|
||||
auditActionTone,
|
||||
auditActorSecondary,
|
||||
formatDepositAuditRemark,
|
||||
shouldShowAuditRejectInTimeline,
|
||||
} from '../utils/depositAuditDisplay';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
interface DepositAuditLog {
|
||||
id: string;
|
||||
action: string;
|
||||
actorType: string;
|
||||
statusBefore: string | null;
|
||||
statusAfter: string;
|
||||
amount: string | null;
|
||||
approvedAmount: string | null;
|
||||
remark: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface DepositOrder {
|
||||
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;
|
||||
auditLogs?: DepositAuditLog[];
|
||||
}
|
||||
|
||||
const detail = ref<DepositOrder | null>(null);
|
||||
const loading = ref(true);
|
||||
const notFound = ref(false);
|
||||
|
||||
async function loadDetail() {
|
||||
loading.value = true;
|
||||
notFound.value = false;
|
||||
try {
|
||||
const { data } = await api.get(`/player/deposit-orders/${route.params.id}`);
|
||||
if (!data.data) {
|
||||
notFound.value = true;
|
||||
return;
|
||||
}
|
||||
detail.value = data.data;
|
||||
} catch {
|
||||
notFound.value = true;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadDetail);
|
||||
|
||||
const { pullDistance, spinning, progress } = usePullToRefresh({
|
||||
onRefresh: loadDetail,
|
||||
});
|
||||
|
||||
function goBack() {
|
||||
router.push('/wallet/recharge/history');
|
||||
}
|
||||
|
||||
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 reapply(order: DepositOrder) {
|
||||
const query: Record<string, string> = {
|
||||
orderId: order.id,
|
||||
methodType: order.methodType,
|
||||
amount: order.amount,
|
||||
};
|
||||
if (order.paymentMethodId) query.methodId = order.paymentMethodId;
|
||||
router.push({ path: '/wallet/recharge', query });
|
||||
}
|
||||
|
||||
function normalizeText(value: string | null | undefined) {
|
||||
return value?.trim() ?? '';
|
||||
}
|
||||
|
||||
function orderNote(order: DepositOrder): { 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 orderNoteLine(order: DepositOrder) {
|
||||
const note = orderNote(order);
|
||||
return note ? `${note.label}: ${note.text}` : null;
|
||||
}
|
||||
|
||||
function auditActionLabel(action: string) {
|
||||
const key = `recharge.audit_${action.toLowerCase()}` as const;
|
||||
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(undefined, {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
function formatOrderTime(iso: string) {
|
||||
return new Date(iso).toLocaleString();
|
||||
}
|
||||
|
||||
function auditRemarkForTimeline(log: DepositAuditLog, order: DepositOrder) {
|
||||
if (log.action === 'REJECTED' && !shouldShowAuditRejectInTimeline(log, order.rejectReason)) {
|
||||
return null;
|
||||
}
|
||||
return formatDepositAuditRemark(log, t);
|
||||
}
|
||||
|
||||
function auditLogsForDisplay(order: DepositOrder) {
|
||||
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>
|
||||
<div class="recharge-detail-page">
|
||||
<div class="page-header">
|
||||
<button class="back-btn" @click="goBack">‹</button>
|
||||
<h2>{{ t('recharge.order_detail') }}</h2>
|
||||
<span class="header-spacer" />
|
||||
</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="loading" class="state">
|
||||
<GoldSpinner :size="36" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="notFound || !detail" class="empty">{{ t('common.not_found') }}</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="summary-card">
|
||||
<div class="summary-head">
|
||||
<span class="method-badge" :class="detail.methodType === 'BANK' ? 'bank' : 'usdt'">
|
||||
{{ detail.methodType }}
|
||||
</span>
|
||||
<span :class="['status-badge', statusClass(detail.status)]">{{ statusLabel(detail.status) }}</span>
|
||||
</div>
|
||||
<div class="order-no">{{ detail.orderNo }}</div>
|
||||
<div class="order-amount">{{ formatMoney(detail.amount, locale) }}</div>
|
||||
<div
|
||||
v-if="detail.approvedAmount && detail.approvedAmount !== detail.amount"
|
||||
class="approved-amount"
|
||||
>
|
||||
{{ t('recharge.credited') }}: {{ formatMoney(detail.approvedAmount, locale) }}
|
||||
</div>
|
||||
<div class="method-name">{{ detail.paymentMethodName || '-' }}</div>
|
||||
<div class="time-row">
|
||||
<span class="time-label">{{ t('recharge.apply_time') }}</span>
|
||||
<span class="time-value">{{ formatOrderTime(detail.createdAt) }}</span>
|
||||
</div>
|
||||
<div v-if="detail.reviewedAt" class="time-row">
|
||||
<span class="time-label">{{ t('recharge.review_time') }}</span>
|
||||
<span class="time-value">{{ formatOrderTime(detail.reviewedAt) }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="orderNoteLine(detail)"
|
||||
:class="detail.status === 'REJECTED' ? 'reject-reason' : 'order-remark'"
|
||||
>
|
||||
{{ orderNoteLine(detail) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="detail.auditLogs?.length" class="audit-card">
|
||||
<h3 class="audit-title">{{ t('recharge.audit_title') }}</h3>
|
||||
<div class="audit-track">
|
||||
<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>
|
||||
|
||||
<button
|
||||
v-if="detail.status === 'REJECTED'"
|
||||
type="button"
|
||||
class="reapply-btn btn-gold-outline"
|
||||
@click="reapply(detail)"
|
||||
>
|
||||
{{ t('recharge.reapply') }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.recharge-detail-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; flex: 1; text-align: center; }
|
||||
.back-btn { background: none; border: none; color: var(--primary-light); font-size: 24px; cursor: pointer; padding: 0 8px; }
|
||||
.header-spacer { width: 40px; }
|
||||
.state, .empty { display: flex; justify-content: center; padding: 48px 16px; color: #666; font-weight: 600; }
|
||||
.pull-indicator { display: flex; align-items: center; justify-content: center; overflow: hidden; transition: height 0.15s ease; }
|
||||
|
||||
.summary-card,
|
||||
.audit-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;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.summary-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
||||
.method-badge { font-size: 10px; font-weight: 700; padding: 2px 8px; border-radius: 4px; }
|
||||
.method-badge.bank { background: rgba(212,175,55,0.15); color: var(--primary-light); }
|
||||
.method-badge.usdt { background: rgba(38,161,123,0.15); color: #26a17b; }
|
||||
.status-badge { font-size: 11px; font-weight: 700; padding: 2px 10px; border-radius: 999px; }
|
||||
.status-approved { background: rgba(52,199,89,0.15); color: #4cd964; }
|
||||
.status-rejected { background: rgba(255,69,58,0.15); color: #ff453a; }
|
||||
.status-pending { background: rgba(212,175,55,0.15); color: var(--primary-light); }
|
||||
.order-no { font-size: 11px; color: #888; font-family: monospace; margin-bottom: 4px; }
|
||||
.order-amount { font-size: 24px; font-weight: 800; color: var(--text); margin-bottom: 4px; }
|
||||
.approved-amount { font-size: 12px; color: #7eb87a; margin-bottom: 8px; }
|
||||
.method-name { font-size: 12px; color: #888; margin-bottom: 8px; }
|
||||
.time-row { display: flex; justify-content: space-between; font-size: 12px; margin-bottom: 4px; }
|
||||
.time-label { color: #666; }
|
||||
.time-value { color: #aaa; }
|
||||
.reject-reason { margin-top: 8px; padding: 8px 10px; border-radius: 8px; border: 1px solid rgba(245,108,108,0.22); font-size: 12px; color: #f56c6c; line-height: 1.45; word-break: break-word; }
|
||||
.order-remark { margin-top: 8px; font-size: 12px; color: #888; line-height: 1.45; }
|
||||
|
||||
.audit-title { margin: 0 0 12px; font-size: 14px; font-weight: 700; color: #ccc; }
|
||||
.audit-track { display: flex; flex-direction: column; }
|
||||
.audit-step { display: flex; gap: 8px; }
|
||||
.audit-step-rail { flex: 0 0 10px; display: flex; flex-direction: column; align-items: center; padding-top: 4px; }
|
||||
.audit-dot { width: 5px; height: 5px; border-radius: 50%; background: #555; flex-shrink: 0; }
|
||||
.audit-line { flex: 1; width: 1px; min-height: 10px; margin: 3px 0; background: #2a2a2a; }
|
||||
.audit-step-body { flex: 1; min-width: 0; padding-bottom: 12px; }
|
||||
.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: 12px; font-weight: 600; color: #ccc; }
|
||||
.audit-step-time { font-size: 10px; color: #666; white-space: nowrap; }
|
||||
.audit-step-actor { margin: 2px 0 0; font-size: 11px; color: #999; }
|
||||
.audit-step-credited { margin: 3px 0 0; font-size: 11px; font-weight: 500; color: #7eb87a; }
|
||||
.audit-step-note { margin: 4px 0 0; font-size: 11px; color: #888; line-height: 1.45; word-break: break-word; }
|
||||
.audit-step-box { margin-top: 4px; padding: 6px 8px; border-radius: 6px; display: flex; flex-direction: column; gap: 1px; word-break: break-word; }
|
||||
.audit-step-box--reject { border: 1px solid rgba(245,108,108,0.22); }
|
||||
.audit-step-box-label { font-size: 10px; font-weight: 500; color: #c07070; }
|
||||
.audit-step-box-text { font-size: 11px; color: #b08888; }
|
||||
.audit-step--submitted .audit-dot { background: #c9a227; }
|
||||
.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; }
|
||||
|
||||
.reapply-btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user