From 66ac69c7a73a984199e99bc5edf51b86a1b01aac Mon Sep 17 00:00:00 2001 From: Mars <3361409208a@gmail.com> Date: Thu, 2 Jul 2026 10:47:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(player):=20=E5=85=85=E5=80=BC=E8=AE=A2?= =?UTF-8?q?=E5=8D=95=E8=AF=A6=E6=83=85=E4=B8=8E=E5=AE=A1=E6=A0=B8=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=EF=BC=8C=E4=BC=98=E5=8C=96=E6=A1=8C=E9=9D=A2=E7=AB=AF?= =?UTF-8?q?=E4=BA=A4=E4=BA=92=E4=B8=8E=E8=AE=B0=E5=BD=95=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增充值详情页及单条订单 API,修复桌面充值提交路径;充值/注单记录编号改为中性色,并包含桌面首页与投注相关 UI 改进。 --- .../applications/player/player.controller.ts | 9 + .../src/domains/deposit/deposit.service.ts | 162 ++++- .../player/src/components/CashBalanceChip.vue | 9 +- .../player/src/components/FloatingMailbox.vue | 218 ++++-- apps/player/src/components/LocaleSwitcher.vue | 8 +- apps/player/src/components/MatchBetCard.vue | 72 +- apps/player/src/components/UserAvatarMenu.vue | 9 +- .../desktop/DesktopOddsBetPopover.vue | 182 +++-- .../match-detail/CorrectScorePanel.vue | 6 +- .../match-detail/MarketSelectionsPanel.vue | 6 +- .../src/composables/useDesktopBetPopover.ts | 68 +- apps/player/src/composables/usePlayerHome.ts | 30 +- apps/player/src/i18n/en-US.ts | 3 + apps/player/src/i18n/ms-MY.ts | 3 + apps/player/src/i18n/zh-CN.ts | 3 + apps/player/src/router/index.ts | 1 + apps/player/src/styles.css | 2 +- apps/player/src/styles/desktop/layout.css | 2 +- apps/player/src/styles/desktop/records.css | 28 +- .../src/views/MobileRechargeDetailView.vue | 343 +++++++++ .../src/views/MobileRechargeHistoryView.vue | 549 +-------------- apps/player/src/views/MobileRechargeView.vue | 2 +- .../MobileWalletTransactionDetailView.vue | 6 +- apps/player/src/views/RechargeDetailView.vue | 12 + .../src/views/desktop/DesktopFootballView.vue | 73 +- .../src/views/desktop/DesktopHomeView.vue | 138 +++- .../views/desktop/DesktopMatchDetailView.vue | 75 +- .../views/desktop/DesktopProfileEditView.vue | 44 +- .../desktop/DesktopRechargeDetailView.vue | 429 ++++++++++++ .../desktop/DesktopRechargeHistoryView.vue | 14 +- .../src/views/desktop/DesktopRechargeView.vue | 650 +++++++++++++----- 31 files changed, 2251 insertions(+), 905 deletions(-) create mode 100644 apps/player/src/views/MobileRechargeDetailView.vue create mode 100644 apps/player/src/views/RechargeDetailView.vue create mode 100644 apps/player/src/views/desktop/DesktopRechargeDetailView.vue diff --git a/apps/api/src/applications/player/player.controller.ts b/apps/api/src/applications/player/player.controller.ts index 5f7c6fd..311fd1f 100644 --- a/apps/api/src/applications/player/player.controller.ts +++ b/apps/api/src/applications/player/player.controller.ts @@ -436,6 +436,15 @@ export class PlayerController { return jsonResponse(result); } + @Get('deposit-orders/:id') + async myDepositOrder( + @CurrentUser('id') userId: bigint, + @Param('id') id: string, + ) { + const order = await this.deposit.getPlayerDepositOrder(userId, BigInt(id)); + return jsonResponse(order); + } + @Get('deposit-orders/:id/audit-logs') async myDepositOrderAuditLogs( @CurrentUser('id') userId: bigint, diff --git a/apps/api/src/domains/deposit/deposit.service.ts b/apps/api/src/domains/deposit/deposit.service.ts index 83cab4a..b928d5a 100644 --- a/apps/api/src/domains/deposit/deposit.service.ts +++ b/apps/api/src/domains/deposit/deposit.service.ts @@ -416,6 +416,85 @@ export class DepositService { return map; } + private formatPlayerDepositOrder( + o: { + id: bigint; + orderNo: string; + paymentMethodId: bigint; + methodType: string; + amount: Decimal; + screenshotUrl: string; + status: string; + approvedAmount: Decimal | null; + rejectReason: string | null; + remark: string | null; + createdAt: Date; + reviewedAt: Date | null; + paymentMethod: { + bankName: string | null; + usdtAddress: string | null; + displayName: string | null; + methodType: string; + } | null; + }, + auditLogs: Array<{ + id: string; + action: string; + actorType: string; + statusBefore: string | null; + statusAfter: string; + amount: string | null; + approvedAmount: string | null; + remark: string | null; + createdAt: string; + }>, + ) { + return { + id: o.id.toString(), + orderNo: o.orderNo, + paymentMethodId: o.paymentMethodId.toString(), + methodType: o.methodType, + amount: o.amount.toString(), + screenshotUrl: o.screenshotUrl, + status: o.status, + approvedAmount: o.approvedAmount?.toString() ?? null, + rejectReason: o.rejectReason, + remark: o.remark, + createdAt: o.createdAt, + reviewedAt: o.reviewedAt, + paymentMethodName: + o.paymentMethod?.displayName ?? o.paymentMethod?.bankName ?? o.paymentMethod?.usdtAddress ?? null, + auditLogs, + }; + } + + async getPlayerDepositOrder(playerId: bigint, orderId: bigint) { + const order = await this.prisma.depositOrder.findFirst({ + where: { id: orderId, playerId }, + include: { + paymentMethod: { + select: { bankName: true, usdtAddress: true, displayName: true, methodType: true }, + }, + }, + }); + if (!order) throw appBadRequest('ORDER_NOT_FOUND'); + + const auditMap = await this.attachPlayerAuditLogs([order]); + const auditLogs = (auditMap.get(order.id.toString()) ?? []).map((log) => ({ + id: log.id, + action: log.action, + actorType: log.actorType, + statusBefore: log.statusBefore, + statusAfter: log.statusAfter, + amount: log.amount, + approvedAmount: log.approvedAmount, + remark: log.remark, + createdAt: log.createdAt, + })); + + return this.formatPlayerDepositOrder(order, auditLogs); + } + async createDepositOrder( playerId: bigint, paymentMethodId: bigint, @@ -522,6 +601,47 @@ export class DepositService { }); } + async getPlayerDepositOrder(playerId: bigint, orderId: bigint) { + const order = await this.prisma.depositOrder.findFirst({ + where: { id: orderId, playerId }, + include: { + paymentMethod: { + select: { bankName: true, usdtAddress: true, displayName: true, methodType: true }, + }, + }, + }); + if (!order) throw appBadRequest('ORDER_NOT_FOUND'); + + const auditMap = await this.attachPlayerAuditLogs([order]); + const o = order; + return { + id: o.id.toString(), + orderNo: o.orderNo, + paymentMethodId: o.paymentMethodId.toString(), + methodType: o.methodType, + amount: o.amount.toString(), + screenshotUrl: o.screenshotUrl, + status: o.status, + approvedAmount: o.approvedAmount?.toString() ?? null, + rejectReason: o.rejectReason, + remark: o.remark, + createdAt: o.createdAt, + reviewedAt: o.reviewedAt, + paymentMethodName: o.paymentMethod?.displayName ?? o.paymentMethod?.bankName ?? o.paymentMethod?.usdtAddress ?? null, + auditLogs: (auditMap.get(o.id.toString()) ?? []).map((log) => ({ + id: log.id, + action: log.action, + actorType: log.actorType, + statusBefore: log.statusBefore, + statusAfter: log.statusAfter, + amount: log.amount, + approvedAmount: log.approvedAmount, + remark: log.remark, + createdAt: log.createdAt, + })), + }; + } + async getPlayerDepositOrders(playerId: bigint, page = 1, pageSize = 20) { const skip = (page - 1) * pageSize; const where = { playerId }; @@ -543,32 +663,22 @@ export class DepositService { const auditMap = await this.attachPlayerAuditLogs(items); return { - items: items.map((o) => ({ - id: o.id.toString(), - orderNo: o.orderNo, - paymentMethodId: o.paymentMethodId.toString(), - methodType: o.methodType, - amount: o.amount.toString(), - screenshotUrl: o.screenshotUrl, - status: o.status, - approvedAmount: o.approvedAmount?.toString() ?? null, - rejectReason: o.rejectReason, - remark: o.remark, - createdAt: o.createdAt, - reviewedAt: o.reviewedAt, - paymentMethodName: o.paymentMethod?.displayName ?? o.paymentMethod?.bankName ?? o.paymentMethod?.usdtAddress ?? null, - auditLogs: (auditMap.get(o.id.toString()) ?? []).map((log) => ({ - id: log.id, - action: log.action, - actorType: log.actorType, - statusBefore: log.statusBefore, - statusAfter: log.statusAfter, - amount: log.amount, - approvedAmount: log.approvedAmount, - remark: log.remark, - createdAt: log.createdAt, - })), - })), + items: items.map((o) => + this.formatPlayerDepositOrder( + o, + (auditMap.get(o.id.toString()) ?? []).map((log) => ({ + id: log.id, + action: log.action, + actorType: log.actorType, + statusBefore: log.statusBefore, + statusAfter: log.statusAfter, + amount: log.amount, + approvedAmount: log.approvedAmount, + remark: log.remark, + createdAt: log.createdAt, + })), + ), + ), total, page, pageSize, diff --git a/apps/player/src/components/CashBalanceChip.vue b/apps/player/src/components/CashBalanceChip.vue index 6a42e6f..925f6ea 100644 --- a/apps/player/src/components/CashBalanceChip.vue +++ b/apps/player/src/components/CashBalanceChip.vue @@ -68,7 +68,12 @@ onUnmounted(() => { - + {{ t('wallet.cash_balance') }} @@ -99,8 +104,6 @@ onUnmounted(() => { {{ t('recharge.title') }} - - diff --git a/apps/player/src/components/FloatingMailbox.vue b/apps/player/src/components/FloatingMailbox.vue index e731dc3..27e3d48 100644 --- a/apps/player/src/components/FloatingMailbox.vue +++ b/apps/player/src/components/FloatingMailbox.vue @@ -19,7 +19,32 @@ const { isDesktop } = useViewport(); const auth = useAuthStore(); const { inboxEnabled, hubTitleKey } = useInboxFeature(); const { unreadCount, messages, listLoaded, refreshUnreadCount, markAllRead, deleteAllMessages } = usePlayerMessages(); -const { isOpen, toggle, close } = useFloatingMailbox(); +const { isOpen, toggle, open, close } = useFloatingMailbox(); +const hovering = ref(false); +let hoverTimer: number | undefined = undefined; + +function onMouseEnter() { + if (hoverTimer !== undefined) { + clearTimeout(hoverTimer); + hoverTimer = undefined; + } + hovering.value = true; +} + +function onMouseLeave() { + hoverTimer = window.setTimeout(() => { + hovering.value = false; + hoverTimer = undefined; + }, 150); +} + +function openTab(tab: HubTab) { + activeTab.value = tab; + selectedMessageId.value = null; + open(); + hovering.value = false; +} + const markingAll = ref(false); const deletingAll = ref(false); @@ -91,7 +116,42 @@ onMounted(() => { - + + + + + + + + + {{ t('messages.tab_inbox') }} + + {{ unreadCount > 99 ? '99+' : unreadCount }} + + + + + + + {{ t('messages.tab_support') }} + + + + { - - - {{ t('messages.tab_inbox') }} - {{ unreadInList }} - - - {{ t('messages.tab_support') }} - + + {{ activeTab === 'messages' ? t('messages.tab_inbox') : t('messages.tab_support') }} + {{ unreadInList }} @@ -168,6 +212,100 @@ onMounted(() => { diff --git a/apps/player/src/components/UserAvatarMenu.vue b/apps/player/src/components/UserAvatarMenu.vue index 8a2aee4..2868a54 100644 --- a/apps/player/src/components/UserAvatarMenu.vue +++ b/apps/player/src/components/UserAvatarMenu.vue @@ -58,7 +58,12 @@ function logout() { - + @@ -69,8 +74,6 @@ function logout() { {{ t('profile.edit') }} {{ t('auth.logout') }} - - diff --git a/apps/player/src/components/desktop/DesktopOddsBetPopover.vue b/apps/player/src/components/desktop/DesktopOddsBetPopover.vue index c933bae..604126e 100644 --- a/apps/player/src/components/desktop/DesktopOddsBetPopover.vue +++ b/apps/player/src/components/desktop/DesktopOddsBetPopover.vue @@ -10,9 +10,10 @@ import { usePlayerProfile } from '../../composables/usePlayerProfile'; import ConfirmDialog from '../ConfirmDialog.vue'; import { buildBetPlaceConfirmMessage } from '../../utils/betPlaceConfirmMessage'; import { useAppToast } from '../../composables/useAppToast'; +import BetSuccessOverlay from '../BetSuccessOverlay.vue'; const { t, locale } = useI18n(); -const { visible, anchorX, anchorY, pendingItem, close } = useDesktopBetPopover(); +const { visible, anchorX, anchorY, pendingItem, placement, close, cancelClose, scheduleClose } = useDesktopBetPopover(); const slip = useBetSlipStore(); const auth = useAuthStore(); const { refreshProfile } = usePlayerProfile(); @@ -25,6 +26,20 @@ const error = ref(''); const showPlaceConfirm = ref(false); const placeConfirmMessage = ref(''); const MIN_STAKE = 5; +const showSuccess = ref(false); +const confirmingItem = ref(null); + +watch(showPlaceConfirm, (val) => { + if (!val) { + confirmingItem.value = null; + } +}); + +function onPopoverMouseLeave() { + if (showPlaceConfirm.value) return; + scheduleClose(200); +} + let outsideClickTimer = 0; @@ -95,6 +110,7 @@ function validatePlaceNow(): boolean { function onPlaceNowClick() { if (!validatePlaceNow()) return; const item = pendingItem.value!; + confirmingItem.value = item; placeConfirmMessage.value = buildBetPlaceConfirmMessage(t, { mode: 'single', items: [item], @@ -107,7 +123,7 @@ function onPlaceNowClick() { showPlaceConfirm.value = true; } -async function executePlaceNow(item = pendingItem.value) { +async function executePlaceNow(item = confirmingItem.value || pendingItem.value) { if (!item) return; loading.value = true; error.value = ''; @@ -122,7 +138,7 @@ async function executePlaceNow(item = pendingItem.value) { await refreshProfile(); showPlaceConfirm.value = false; close(); - showToast(t('bet.place_success')); + showSuccess.value = true; } catch (e: unknown) { error.value = (e as { response?: { data?: { error?: string } } })?.response?.data?.error || @@ -134,7 +150,7 @@ async function executePlaceNow(item = pendingItem.value) { } async function confirmPlaceNow() { - const item = pendingItem.value; + const item = confirmingItem.value; if (!item) return; showPlaceConfirm.value = false; await executePlaceNow(item); @@ -184,11 +200,13 @@ function addToParlayList() { v-if="visible && pendingItem" ref="popRef" class="bet-popover" + :class="`placement-${placement}`" tabindex="-1" :style="{ left: `${anchorX}px`, top: `${anchorY}px` }" @click.stop + @mouseenter="cancelClose" + @mouseleave="onPopoverMouseLeave" > - ✕ {{ pendingItem.matchName }} {{ pendingItem.marketName }} @@ -233,104 +251,146 @@ function addToParlayList() { @confirm="confirmPlaceNow" /> + + diff --git a/apps/player/src/components/match-detail/CorrectScorePanel.vue b/apps/player/src/components/match-detail/CorrectScorePanel.vue index 8a8f457..7ee9447 100644 --- a/apps/player/src/components/match-detail/CorrectScorePanel.vue +++ b/apps/player/src/components/match-detail/CorrectScorePanel.vue @@ -2,6 +2,8 @@ import { computed } from 'vue'; import { useI18n } from 'vue-i18n'; import { groupCorrectScoreSelections, type CsSelection } from '../../utils/correctScoreLayout'; +import { useDesktopBetPopover } from '../../composables/useDesktopBetPopover'; + const props = defineProps<{ marketType: string; @@ -24,6 +26,7 @@ const emit = defineEmits<{ }>(); const { t } = useI18n(); +const { scheduleClose, cancelClose } = useDesktopBetPopover(); const columns = computed(() => groupCorrectScoreSelections( @@ -55,7 +58,7 @@ function formatOdds(odds: string) { {{ t('bet.col_away') }} - + {{ sel.scoreDisplay }} diff --git a/apps/player/src/components/match-detail/MarketSelectionsPanel.vue b/apps/player/src/components/match-detail/MarketSelectionsPanel.vue index 1a6bd4a..d666957 100644 --- a/apps/player/src/components/match-detail/MarketSelectionsPanel.vue +++ b/apps/player/src/components/match-detail/MarketSelectionsPanel.vue @@ -2,6 +2,8 @@ import { computed } from 'vue'; import { useI18n } from 'vue-i18n'; import { resolveSelectionLabel } from '../../utils/selectionLabel'; +import { useDesktopBetPopover } from '../../composables/useDesktopBetPopover'; + const props = defineProps<{ selections: { @@ -23,6 +25,7 @@ const props = defineProps<{ const emit = defineEmits<{ pick: [id: string, event?: MouseEvent] }>(); const { t } = useI18n(); +const { scheduleClose, cancelClose } = useDesktopBetPopover(); function label(sel: (typeof props.selections)[number]) { if (sel.selectionDisplayName?.trim()) return sel.selectionDisplayName; @@ -53,7 +56,7 @@ const panelStyle = computed(() => - + :class="{ selected: isSelected(sel.id), 'odds-btn--locked': locked }" :data-bet-selection-id="sel.id" :disabled="locked" + @mouseenter="onPick(sel.id, $event)" @click="onPick(sel.id, $event)" > {{ label(sel) }} diff --git a/apps/player/src/composables/useDesktopBetPopover.ts b/apps/player/src/composables/useDesktopBetPopover.ts index 982d634..60348fc 100644 --- a/apps/player/src/composables/useDesktopBetPopover.ts +++ b/apps/player/src/composables/useDesktopBetPopover.ts @@ -5,20 +5,45 @@ const visible = ref(false); const anchorX = ref(0); const anchorY = ref(0); const pendingItem = ref(null); +const placement = ref<'top' | 'bottom'>('bottom'); +let closeTimer: number | undefined = undefined; export function useDesktopBetPopover() { function openAt(event: MouseEvent, item: SlipItem) { - const pad = 12; - const popW = 300; - const popH = 320; - let x = event.clientX + pad; - let y = event.clientY + pad; + cancelClose(); + + const el = (event.currentTarget || event.target) as HTMLElement; + const btn = el?.closest?.('button') || el; + const rect = btn?.getBoundingClientRect?.(); + if (!rect) return; + + const popW = 190; + const popH = 170; + const pad = 12; // increased pad to leave space for the arrow pointer + + // Calculate center X of the button + const btnCenterX = rect.left + rect.width / 2; + let x = btnCenterX - popW / 2; + + // Default: place below button (center bottom) + let y = rect.bottom + pad; + placement.value = 'bottom'; + if (typeof window !== 'undefined') { - x = Math.min(x, window.innerWidth - popW - pad); - y = Math.min(y, window.innerHeight - popH - pad); + const spaceBelow = window.innerHeight - rect.bottom; + if (spaceBelow < popH + pad) { + // Not enough space below, place above button (center top) + y = rect.top - popH - pad; + placement.value = 'top'; + } + + // Keep within viewport boundaries + x = Math.max(pad, Math.min(x, window.innerWidth - popW - pad)); + y = Math.max(pad, Math.min(y, window.innerHeight - popH - pad)); } - anchorX.value = Math.max(pad, x); - anchorY.value = Math.max(pad, y); + + anchorX.value = x; + anchorY.value = y; pendingItem.value = item; visible.value = true; } @@ -26,6 +51,28 @@ export function useDesktopBetPopover() { function close() { visible.value = false; pendingItem.value = null; + if (closeTimer !== undefined) { + window.clearTimeout(closeTimer); + closeTimer = undefined; + } + } + + function scheduleClose(delay = 200) { + if (closeTimer !== undefined) { + window.clearTimeout(closeTimer); + } + closeTimer = window.setTimeout(() => { + visible.value = false; + pendingItem.value = null; + closeTimer = undefined; + }, delay); + } + + function cancelClose() { + if (closeTimer !== undefined) { + window.clearTimeout(closeTimer); + closeTimer = undefined; + } } return { @@ -33,7 +80,10 @@ export function useDesktopBetPopover() { anchorX, anchorY, pendingItem, + placement, openAt, close, + scheduleClose, + cancelClose, }; } diff --git a/apps/player/src/composables/usePlayerHome.ts b/apps/player/src/composables/usePlayerHome.ts index 1329afd..b5ec71c 100644 --- a/apps/player/src/composables/usePlayerHome.ts +++ b/apps/player/src/composables/usePlayerHome.ts @@ -113,11 +113,31 @@ function collectAnnouncementLines(data: HomePayload | null): string[] { function collectAnnouncementItems(data: HomePayload | null): PlayerAnnouncementItem[] { if (!data) return []; - const source = - data.announcements && data.announcements.length > 0 - ? data.announcements - : [...(data.ticker ?? []), ...(data.notices ?? [])]; - return source.filter((item) => item.translation?.title || item.translation?.body); + const seen = new Set(); + const merged: PlayerAnnouncementItem[] = []; + + const pushItems = (items: PlayerAnnouncementItem[] | undefined) => { + for (const item of items ?? []) { + if (!item?.id || seen.has(item.id)) continue; + if (!item.translation?.title && !item.translation?.body) continue; + seen.add(item.id); + merged.push(item); + } + }; + + pushItems(data.banners); + pushItems(data.announcements); + pushItems(data.ticker); + pushItems(data.notices); + + merged.sort((a, b) => { + const timeA = a.createdAt ? Date.parse(a.createdAt) : 0; + const timeB = b.createdAt ? Date.parse(b.createdAt) : 0; + if (timeB !== timeA) return timeB - timeA; + return (a.sortOrder ?? 0) - (b.sortOrder ?? 0); + }); + + return merged; } function collectHubContentItems(data: HomePayload | null): PlayerContentItem[] { diff --git a/apps/player/src/i18n/en-US.ts b/apps/player/src/i18n/en-US.ts index 90fdeb5..219ee9f 100644 --- a/apps/player/src/i18n/en-US.ts +++ b/apps/player/src/i18n/en-US.ts @@ -24,6 +24,8 @@ export default { cancelled: 'Cancelled', error: 'Something went wrong', status: 'Status', + copy: 'Copy', + copy_success: 'Copied', }, pagination: { total: '{total} total', @@ -313,6 +315,7 @@ export default { }, recharge: { title: 'Recharge', + scan_to_pay: 'Scan QR code to pay', history: 'History', history_title: 'Recharge History', bank_transfer: 'Bank Transfer', diff --git a/apps/player/src/i18n/ms-MY.ts b/apps/player/src/i18n/ms-MY.ts index 3dd4bfc..e6b023a 100644 --- a/apps/player/src/i18n/ms-MY.ts +++ b/apps/player/src/i18n/ms-MY.ts @@ -24,6 +24,8 @@ export default { cancelled: 'Dibatalkan', error: 'Operasi gagal', status: 'Status', + copy: 'Salin', + copy_success: 'Berjaya disalin', }, pagination: { total: '{total} jumlah', @@ -325,6 +327,7 @@ export default { }, recharge: { title: 'Topup', + scan_to_pay: 'Imbas kod QR untuk bayar', history: 'Sejarah', history_title: 'Sejarah Topup', bank_transfer: 'Pindahan Bank', diff --git a/apps/player/src/i18n/zh-CN.ts b/apps/player/src/i18n/zh-CN.ts index 8a434a2..51f33a3 100644 --- a/apps/player/src/i18n/zh-CN.ts +++ b/apps/player/src/i18n/zh-CN.ts @@ -24,6 +24,8 @@ export default { cancelled: '已取消', error: '操作失败', status: '状态', + copy: '复制', + copy_success: '复制成功', }, pagination: { total: '共 {total} 条', @@ -313,6 +315,7 @@ export default { }, recharge: { title: '充值', + scan_to_pay: '请扫描二维码完成支付', history: '记录', history_title: '充值记录', bank_transfer: '银行转账', diff --git a/apps/player/src/router/index.ts b/apps/player/src/router/index.ts index f1bbbee..ff4855c 100644 --- a/apps/player/src/router/index.ts +++ b/apps/player/src/router/index.ts @@ -42,6 +42,7 @@ const router = createRouter({ { 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/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: 'profile', component: () => import('../views/ProfileView.vue'), meta: { keepAlive: true, requiresAuth: true } }, { path: 'profile/cashbacks', component: () => import('../views/CashbackRecordsView.vue'), meta: { requiresAuth: true } }, diff --git a/apps/player/src/styles.css b/apps/player/src/styles.css index ab83a22..922fd9f 100644 --- a/apps/player/src/styles.css +++ b/apps/player/src/styles.css @@ -223,7 +223,7 @@ body { @media (min-width: 1024px) { body { - background-image: url('./assets/images/pcbg.webp'); + background-image: url('./assets/images/pcbg.png'); background-attachment: fixed; } } diff --git a/apps/player/src/styles/desktop/layout.css b/apps/player/src/styles/desktop/layout.css index 7af9827..6e7f046 100644 --- a/apps/player/src/styles/desktop/layout.css +++ b/apps/player/src/styles/desktop/layout.css @@ -5,7 +5,7 @@ display: flex; flex-direction: column; background-color: #000; - background-image: url('../../assets/images/pcbg.webp'); + background-image: url('../../assets/images/pcbg.png'); background-size: cover; background-position: center; background-attachment: fixed; diff --git a/apps/player/src/styles/desktop/records.css b/apps/player/src/styles/desktop/records.css index c2b98d7..82659c9 100644 --- a/apps/player/src/styles/desktop/records.css +++ b/apps/player/src/styles/desktop/records.css @@ -181,7 +181,7 @@ text-align: left; font-size: 11px; font-weight: 800; - color: var(--primary-light); + color: var(--text-muted); background: #121212; text-transform: uppercase; letter-spacing: 0.06em; @@ -209,7 +209,21 @@ } .desktop-records-table tbody tr.hover-row:hover { - background: rgba(212, 175, 55, 0.06); + background: rgba(255, 255, 255, 0.04); +} + +.desktop-records-table .link-cell { + color: var(--text); + font-weight: 700; + text-decoration: none; + font-family: 'SF Mono', 'Consolas', monospace; + font-size: 12px; + letter-spacing: 0.03em; +} + +.desktop-records-table .link-cell:hover { + color: #fff; + text-decoration: underline; } .desktop-records-table .num-cell { @@ -224,16 +238,6 @@ font-size: 12px; } -.desktop-records-table .link-cell { - color: var(--primary-light); - font-weight: 700; - text-decoration: none; -} - -.desktop-records-table .link-cell:hover { - text-decoration: underline; -} - .desktop-records-pagination { flex-shrink: 0; padding: 12px 16px; diff --git a/apps/player/src/views/MobileRechargeDetailView.vue b/apps/player/src/views/MobileRechargeDetailView.vue new file mode 100644 index 0000000..20a6ed5 --- /dev/null +++ b/apps/player/src/views/MobileRechargeDetailView.vue @@ -0,0 +1,343 @@ + + + + + + ‹ + {{ t('recharge.order_detail') }} + + + + + + + + + + + + {{ t('common.not_found') }} + + + + + + {{ detail.methodType }} + + {{ statusLabel(detail.status) }} + + {{ detail.orderNo }} + {{ formatMoney(detail.amount, locale) }} + + {{ t('recharge.credited') }}: {{ formatMoney(detail.approvedAmount, locale) }} + + {{ detail.paymentMethodName || '-' }} + + {{ t('recharge.apply_time') }} + {{ formatOrderTime(detail.createdAt) }} + + + {{ t('recharge.review_time') }} + {{ formatOrderTime(detail.reviewedAt) }} + + + {{ orderNoteLine(detail) }} + + + + + {{ t('recharge.audit_title') }} + + + + + + + + + {{ auditActionLabel(entry.log.action) }} + {{ formatAuditTime(entry.log.createdAt) }} + + {{ entry.actor }} + + {{ t('recharge.audit_credited') }} {{ formatMoney(entry.log.approvedAmount, locale) }} + + + {{ t('recharge.reject_reason') }} + {{ entry.remark.text }} + + + {{ auditNoteDisplayText(entry.remark) }} + + + + + + + + {{ t('recharge.reapply') }} + + + + + + diff --git a/apps/player/src/views/MobileRechargeHistoryView.vue b/apps/player/src/views/MobileRechargeHistoryView.vue index e035e03..04f26f5 100644 --- a/apps/player/src/views/MobileRechargeHistoryView.vue +++ b/apps/player/src/views/MobileRechargeHistoryView.vue @@ -6,34 +6,15 @@ 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'; import { useDepositNotifications } from '../composables/useDepositNotifications'; const router = useRouter(); const { t, locale } = useI18n(); const { trackPendingOrder, pollOnce } = useDepositNotifications(); -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; @@ -43,7 +24,7 @@ interface DepositOrder { createdAt: string; reviewedAt: string | null; paymentMethodName: string | null; - auditLogs?: DepositAuditLog[]; + auditLogs?: { id: string }[]; } const items = ref([]); @@ -56,8 +37,6 @@ const hasMore = ref(true); const sentinel = ref(null); let observer: IntersectionObserver | null = null; -const selectedOrder = ref(null); - async function fetchOrders(p = 1) { if (loading.value) return; loading.value = true; @@ -128,103 +107,31 @@ function goRecharge() { } function openDetail(order: DepositOrder) { - selectedOrder.value = order; -} - -function closeDetail() { - selectedOrder.value = null; -} - -function reapply(order: DepositOrder) { - const query: Record = { - orderId: order.id, - methodType: order.methodType, - amount: order.amount, - }; - if (order.paymentMethodId) { - query.methodId = order.paymentMethodId; - } - router.push({ path: '/wallet/recharge', query }); + router.push(`/wallet/recharge/history/${order.id}`); } function normalizeText(value: string | null | undefined) { return value?.trim() ?? ''; } -/** Backend sets remark = rejectReason on reject; show one line only. */ -function orderNote(order: DepositOrder): { label: string; text: string } | null { +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 { label: t('recharge.reject_reason'), text }; - } - - if (remark) { - return { label: t('recharge.remark'), text: remark }; + return `${t('recharge.reject_reason')}: ${text}`; } + if (remark) return `${t('recharge.remark')}: ${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 auditNoteLine(text: string) { - return `${t('recharge.audit_remark_label')}: ${text}`; -} - -function auditNoteDisplayText(remark: { kind: 'note'; text: string }) { - if (remark.text === t('wallet.remark_deposit_revoke_generic')) { - return remark.text; - } - return auditNoteLine(remark.text); -} - function auditStepCount(order: DepositOrder) { return order.auditLogs?.length ?? 0; } @@ -268,6 +175,7 @@ function auditStepCount(order: DepositOrder) { {{ statusLabel(order.status) }} + {{ order.orderNo }} {{ formatMoney(order.amount, locale) }} {{ t('recharge.credited') }}: {{ formatMoney(order.approvedAmount, locale) }} @@ -291,20 +199,12 @@ function auditStepCount(order: DepositOrder) { > {{ orderNoteLine(order) }} - - + + {{ t('recharge.audit_summary', { count: auditStepCount(order) }) }} {{ t('recharge.view_detail') }} › - - {{ t('recharge.view_detail') }} › - @@ -319,98 +219,6 @@ function auditStepCount(order: DepositOrder) { {{ t('common.no_more') }} - - - - - ✕ - - {{ t('recharge.audit_title') }} - - - - - {{ selectedOrder.methodType }} - - - {{ statusLabel(selectedOrder.status) }} - - - {{ formatMoney(selectedOrder.amount, locale) }} - - {{ t('recharge.credited') }}: {{ formatMoney(selectedOrder.approvedAmount, locale) }} - - {{ selectedOrder.paymentMethodName || '-' }} - - {{ t('recharge.apply_time') }} - {{ formatOrderTime(selectedOrder.createdAt) }} - - - {{ t('recharge.review_time') }} - {{ formatOrderTime(selectedOrder.reviewedAt) }} - - - {{ orderNoteLine(selectedOrder) }} - - - - - {{ t('recharge.audit_title') }} - - - - - - - - - {{ auditActionLabel(entry.log.action) }} - {{ formatAuditTime(entry.log.createdAt) }} - - {{ entry.actor }} - - {{ t('recharge.audit_credited') }} {{ formatMoney(entry.log.approvedAmount, locale) }} - - - {{ t('recharge.reject_reason') }} - {{ entry.remark.text }} - - - {{ auditNoteDisplayText(entry.remark) }} - - - - - - - - {{ t('recharge.reapply') }} - - - - @@ -435,9 +243,7 @@ function auditStepCount(order: DepositOrder) { cursor: pointer; -webkit-tap-highlight-color: transparent; } -.order-card:active { - opacity: 0.92; -} +.order-card:active { opacity: 0.92; } .order-card::before { content: ''; position: absolute; @@ -453,14 +259,9 @@ function auditStepCount(order: DepositOrder) { background: linear-gradient(90deg, transparent, rgba(245, 108, 108, 0.35), transparent); } .order-card.rejected .order-amount { - background: none; - -webkit-background-clip: unset; - background-clip: unset; color: #bbb; } -.order-card.rejected .info-label { - color: #888; -} +.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; } @@ -469,15 +270,18 @@ function auditStepCount(order: DepositOrder) { .status-pending { color: #e6a23c; } .status-approved { color: #67c23a; } .status-rejected { color: #f56c6c; } -.order-body { } .order-amount { font-size: 22px; font-weight: 900; margin-bottom: 4px; - background: linear-gradient(135deg, #f0d060, #d4a830); - -webkit-background-clip: text; - background-clip: text; - color: transparent; + 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; } @@ -520,39 +324,17 @@ function auditStepCount(order: DepositOrder) { } .card-detail-summary { font-size: 11px; - color: rgba(212, 175, 55, 0.65); + color: var(--text-muted); font-weight: 600; } .card-detail-link { font-size: 11px; - color: var(--primary-light); + color: var(--text-muted); font-weight: 700; white-space: nowrap; } -.card-detail-link-only { - display: block; - width: 100%; - margin-top: 10px; - padding: 0; - border: none; - background: none; - text-align: right; - font-size: 11px; - color: var(--primary-light); - font-weight: 700; - cursor: pointer; -} - -.sentinel { - height: 1px; -} - -.load-more-spinner { - display: flex; - justify-content: center; - padding: 20px 0 8px; -} - +.sentinel { height: 1px; } +.load-more-spinner { display: flex; justify-content: center; padding: 20px 0 8px; } .end-hint { text-align: center; font-size: 12px; @@ -561,291 +343,4 @@ function auditStepCount(order: DepositOrder) { padding: 16px 0 4px; letter-spacing: 0.03em; } - -.detail-overlay { - position: fixed; - inset: 0; - z-index: 200; - background: rgba(0, 0, 0, 0.48); - display: flex; - align-items: flex-end; - justify-content: center; - padding: 0; -} - -.detail-modal { - position: relative; - width: 100%; - max-width: 480px; - max-height: min(88vh, 720px); - overflow-y: auto; - background: #141414; - border: 1px solid #2a2a2a; - border-bottom: none; - border-radius: 14px 14px 0 0; - padding: 16px 16px calc(14px + env(safe-area-inset-bottom, 0px)); - box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.28); -} - -.detail-close { - position: absolute; - top: 12px; - right: 12px; - width: 28px; - height: 28px; - border: none; - border-radius: 50%; - background: transparent; - color: #777; - font-size: 13px; - cursor: pointer; - line-height: 1; -} - -.detail-title { - margin: 0 28px 12px 0; - font-size: 15px; - font-weight: 600; - color: #e8e8e8; -} - -.detail-summary { - margin-bottom: 14px; - padding: 12px; - border-radius: 10px; - background: rgba(255, 255, 255, 0.02); - border: 1px solid #222; -} - -.detail-summary-head { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 10px; -} - -.detail-amount { - font-size: 22px; - font-weight: 700; - margin-bottom: 4px; - color: var(--primary-light, #d4af37); - letter-spacing: -0.02em; -} - -.detail-method-name { - font-size: 12px; - color: #999; - font-weight: 500; - margin-bottom: 8px; -} - -.detail-row { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 12px; - margin-bottom: 6px; -} - -.detail-label { - font-size: 12px; - color: #888; - flex-shrink: 0; -} - -.detail-value { - font-size: 12px; - color: #ccc; - text-align: right; - word-break: break-word; -} - -.detail-summary .order-remark { - margin-top: 8px; - font-size: 11px; - color: #999; - background: transparent; - padding: 6px 0 0; - border-radius: 0; - border: none; - border-top: 1px solid #222; - line-height: 1.45; - word-break: break-word; -} - -.detail-summary .reject-reason { - margin-top: 8px; - font-size: 11px; - color: #b08888; - background: transparent; - padding: 6px 0 0; - border-radius: 0; - border: none; - border-top: 1px solid rgba(245, 108, 108, 0.15); - line-height: 1.45; - word-break: break-word; -} - -.detail-summary .approved-amount { - font-size: 11px; - font-weight: 500; -} - -.detail-audit { - margin-top: 2px; - padding-top: 12px; - border-top: 1px solid #222; -} - -.detail-audit-title { - margin: 0 0 10px; - font-size: 12px; - font-weight: 600; - color: #888; - letter-spacing: 0.02em; -} - -.audit-track { - display: flex; - flex-direction: column; - gap: 0; -} - -.audit-step { - display: flex; - gap: 8px; - min-height: 0; -} - -.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; - line-height: 1.35; -} - -.audit-step-time { - font-size: 10px; - color: #666; - font-variant-numeric: tabular-nums; - white-space: nowrap; - flex-shrink: 0; -} - -.audit-step-actor { - margin: 2px 0 0; - font-size: 11px; - color: #999; - line-height: 1.4; -} - -.audit-step-credited { - margin: 3px 0 0; - font-size: 11px; - font-weight: 500; - color: #7eb87a; - line-height: 1.4; -} - -.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; - line-height: 1.4; - word-break: break-word; -} - -.audit-step-box--reject { - background: transparent; - border: 1px solid rgba(245, 108, 108, 0.22); -} - -.audit-step-box-label { - font-size: 10px; - font-weight: 500; - color: #c07070; - letter-spacing: 0.01em; -} - -.audit-step-box-text { - font-size: 11px; - color: #b08888; -} - -.audit-step--submitted .audit-dot { - background: #c9a227; -} -.audit-step--approved .audit-dot { - background: #5fad5a; -} -.audit-step--rejected .audit-dot { - background: #d06060; -} -.audit-step--revoked .audit-dot { - background: #777; -} -.audit-step--reopened .audit-dot { - background: #c9a227; -} - -.modal-reapply-btn { - width: 100%; - margin-top: 14px; - padding: 11px 16px; - font-size: 13px; - font-weight: 600; - cursor: pointer; -} diff --git a/apps/player/src/views/MobileRechargeView.vue b/apps/player/src/views/MobileRechargeView.vue index 850b9c4..77cead2 100644 --- a/apps/player/src/views/MobileRechargeView.vue +++ b/apps/player/src/views/MobileRechargeView.vue @@ -681,7 +681,7 @@ onMounted(fetchMethods); .success-state { text-align: center; padding: 40px 16px; } .success-icon { font-size: 40px; color: #67c23a; margin-bottom: 10px; } .success-state h3 { margin: 0 0 6px; font-size: 16px; } -.order-no { font-family: monospace; color: var(--primary-light); font-size: 13px; margin: 4px 0; } +.order-no { font-family: monospace; color: var(--text-muted); font-size: 13px; margin: 4px 0; } .success-hint { font-size: 12px; color: var(--text-muted); margin-bottom: 20px; } .btn-primary { background: linear-gradient(135deg, #f0d060, #d4a830); diff --git a/apps/player/src/views/MobileWalletTransactionDetailView.vue b/apps/player/src/views/MobileWalletTransactionDetailView.vue index eb8e194..65b13fe 100644 --- a/apps/player/src/views/MobileWalletTransactionDetailView.vue +++ b/apps/player/src/views/MobileWalletTransactionDetailView.vue @@ -370,9 +370,9 @@ function goCashbackDetail() { margin-top: 12px; padding: 10px 14px; border-radius: 10px; - border: 1px solid var(--border-gold-soft, rgba(212, 175, 55, 0.35)); - background: rgba(212, 175, 55, 0.08); - color: var(--primary-light); + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.04); + color: var(--text); font-size: 13px; font-weight: 700; cursor: pointer; diff --git a/apps/player/src/views/RechargeDetailView.vue b/apps/player/src/views/RechargeDetailView.vue new file mode 100644 index 0000000..92f4b5d --- /dev/null +++ b/apps/player/src/views/RechargeDetailView.vue @@ -0,0 +1,12 @@ + + + + + + diff --git a/apps/player/src/views/desktop/DesktopFootballView.vue b/apps/player/src/views/desktop/DesktopFootballView.vue index 10d6362..e70f05f 100644 --- a/apps/player/src/views/desktop/DesktopFootballView.vue +++ b/apps/player/src/views/desktop/DesktopFootballView.vue @@ -209,13 +209,16 @@ void loadParlayMatches(true); {{ lg.leagueName }} - + + + @@ -350,46 +353,86 @@ void loadParlayMatches(true); } .match-grid { + display: contents; +} + +.match-grid-inner { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 8px; } -.match-grid :deep(.match-card) { +/* --- Card list entrance animation --- */ +.card-list-enter-active { + animation: card-fade-up 0.4s cubic-bezier(0.22, 1, 0.36, 1) both; + animation-delay: calc(var(--i, 0) * 50ms); +} + +.card-list-leave-active { + animation: card-fade-down 0.25s ease both; +} + +.card-list-move { + transition: transform 0.35s ease; +} + +@keyframes card-fade-up { + from { + opacity: 0; + transform: translateY(16px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes card-fade-down { + from { + opacity: 1; + transform: translateY(0); + } + to { + opacity: 0; + transform: translateY(-8px); + } +} + +.match-grid-inner :deep(.match-card) { padding: 8px 6px; gap: 6px; } -.match-grid :deep(.team-name) { +.match-grid-inner :deep(.team-name) { font-size: 13px; } -.match-grid :deep(.team-flag) { +.match-grid-inner :deep(.team-flag) { width: 56px; height: 38px; } -.match-grid :deep(.team-flag.flag-logo) { +.match-grid-inner :deep(.team-flag.flag-logo) { width: 44px; height: 44px; object-fit: contain; } -.match-grid :deep(.kickoff) { +.match-grid-inner :deep(.kickoff) { font-size: 10px; } -.match-grid :deep(.vs) { +.match-grid-inner :deep(.vs) { font-size: 11px; } -.match-grid :deep(.bet-btn) { +.match-grid-inner :deep(.bet-btn) { font-size: 11px; padding: 5px 10px; min-height: 28px; } -.match-grid :deep(.status-tag) { +.match-grid-inner :deep(.status-tag) { font-size: 9px; padding: 2px 6px; } diff --git a/apps/player/src/views/desktop/DesktopHomeView.vue b/apps/player/src/views/desktop/DesktopHomeView.vue index 372d296..b33b5bf 100644 --- a/apps/player/src/views/desktop/DesktopHomeView.vue +++ b/apps/player/src/views/desktop/DesktopHomeView.vue @@ -22,7 +22,7 @@ const { banners, hotMatches, upcomingMatches, loading, load, announcementItems } const slip = useBetSlipStore(); const auth = useAuthStore(); -const { openAt, visible: popoverVisible, pendingItem } = useDesktopBetPopover(); +const { openAt, visible: popoverVisible, pendingItem, cancelClose, scheduleClose } = useDesktopBetPopover(); const activeMarketTypes = ref>({}); @@ -140,6 +140,37 @@ function handleOddsClick(match: PlayerHomeMatch, market: any, selId: string, eve function getSelLabel(sel: any, marketType: string, lineValue?: string | number | null) { return resolveSelectionLabel(t, sel.selectionCode || '', sel.selectionName || '', { lineValue }); } + +function getHoveredSideForMatch(match: PlayerHomeMatch | null) { + if (!match || !popoverVisible.value || !pendingItem.value) return ''; + if (String(pendingItem.value.matchId) !== String(match.id)) return ''; + + const selId = pendingItem.value.selectionId; + // Scan all markets across all tabs to find the selection + for (const market of match.markets ?? []) { + const sel = market.selections?.find((s: any) => s.id === selId); + if (!sel) continue; + const code = (sel.selectionCode || '').toUpperCase(); + if (code === 'HOME') return 'home'; + if (code === 'AWAY') return 'away'; + if (code === 'DRAW') return 'draw'; + if (code === 'OVER') return 'home'; + if (code === 'UNDER') return 'away'; + if (code === 'ODD' || code === 'EVEN') return 'draw'; + // Fallback: use selection index + const selIndex = market.selections?.findIndex((s: any) => s.id === selId) ?? -1; + if (selIndex === -1) continue; + if (market.marketType === 'FT_1X2') { + if (selIndex === 0) return 'home'; + if (selIndex === 1) return 'draw'; + if (selIndex === 2) return 'away'; + } else { + if (selIndex === 0) return 'home'; + if (selIndex === 1) return 'away'; + } + } + return ''; +} @@ -184,7 +215,7 @@ function getSelLabel(sel: any, marketType: string, lineValue?: string | number | {{ formatKickoff(featuredMatch.startTime) }} - + - + - + - + {{ m.leagueName }} {{ formatKickoff(m.startTime) }} - + {{ m.homeTeamName }} @@ -1275,10 +1306,11 @@ function getSelLabel(sel: any, marketType: string, lineValue?: string | number | /* --- 8. 推荐列表 item 左侧金点指示 hover --- */ .side-recommend-item { position: relative; - transition: background 0.15s, transform 0.15s; + transition: background 0.18s, transform 0.18s; } .side-recommend-item:hover { + background: rgba(212, 175, 55, 0.06); transform: translateX(3px); } @@ -1453,4 +1485,96 @@ function getSelLabel(sel: any, marketType: string, lineValue?: string | number | font-size: 10px; color: var(--text-muted); } + +/* --- Sri preview: team highlight on odds selection --- */ +.sri-preview-team { + transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s ease, filter 0.3s ease; +} + +.sri-preview-vs.home .sri-preview-team.home-team { + transform: translateX(0) scale(1.1) !important; + filter: brightness(1.3) saturate(1.2); + opacity: 1 !important; +} +.sri-preview-vs.home .sri-preview-team.away-team { + opacity: 0.35 !important; + transform: translateX(0) scale(0.92) !important; + filter: grayscale(0.6) brightness(0.7); +} + +.sri-preview-vs.away .sri-preview-team.away-team { + transform: translateX(0) scale(1.1) !important; + filter: brightness(1.3) saturate(1.2); + opacity: 1 !important; +} +.sri-preview-vs.away .sri-preview-team.home-team { + opacity: 0.35 !important; + transform: translateX(0) scale(0.92) !important; + filter: grayscale(0.6) brightness(0.7); +} + +.sri-preview-vs.draw .sri-preview-team { + transform: translateX(0) scale(1.05) !important; + filter: brightness(1.15) saturate(1.1); + opacity: 1 !important; +} + +/* --- VS Selection Highlight: scale selected team, no VS animation --- */ +.featured-team, +.hover-team { + transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s ease, filter 0.3s ease; +} + +/* featured match (home) */ +.featured-teams.home .featured-team:first-child { + transform: scale(1.08); + filter: brightness(1.3) saturate(1.2); +} +.featured-teams.home .featured-team:last-child { + opacity: 0.35; + transform: scale(0.94); + filter: grayscale(0.6) brightness(0.7); +} +/* featured match (away) */ +.featured-teams.away .featured-team:last-child { + transform: scale(1.08); + filter: brightness(1.3) saturate(1.2); +} +.featured-teams.away .featured-team:first-child { + opacity: 0.35; + transform: scale(0.94); + filter: grayscale(0.6) brightness(0.7); +} +/* featured match (draw) */ +.featured-teams.draw .featured-team { + transform: scale(1.05); + filter: brightness(1.15) saturate(1.1); +} + +/* hover-versus-layout (sidebar/upcoming) */ +.hover-versus-layout.home .hover-team.home-slide { + transform: scale(1.08); + filter: brightness(1.3) saturate(1.2); + z-index: 2; +} +.hover-versus-layout.home .hover-team.away-slide { + opacity: 0.35 !important; + transform: scale(0.94); + filter: grayscale(0.6) brightness(0.7); +} +.hover-versus-layout.away .hover-team.away-slide { + transform: scale(1.08); + filter: brightness(1.3) saturate(1.2); + z-index: 2; +} +.hover-versus-layout.away .hover-team.home-slide { + opacity: 0.35 !important; + transform: scale(0.94); + filter: grayscale(0.6) brightness(0.7); +} +.hover-versus-layout.draw .hover-team.home-slide, +.hover-versus-layout.draw .hover-team.away-slide { + transform: scale(1.05); + filter: brightness(1.15) saturate(1.1); +} diff --git a/apps/player/src/views/desktop/DesktopMatchDetailView.vue b/apps/player/src/views/desktop/DesktopMatchDetailView.vue index 881e7d4..c10133a 100644 --- a/apps/player/src/views/desktop/DesktopMatchDetailView.vue +++ b/apps/player/src/views/desktop/DesktopMatchDetailView.vue @@ -299,6 +299,37 @@ function toggleSelection(sel: Selection, market: Market, event?: MouseEvent) { if (!item || !event) return; openAt(event, item); } + +function getHoveredSide() { + if (!match.value || !popoverVisible.value || !popoverItem.value) return ''; + if (String(popoverItem.value.matchId) !== String(match.value.id)) return ''; + + const selId = popoverItem.value.selectionId; + // Scan all markets to find the selection by selectionCode + for (const market of allMarkets.value) { + const sel = market.selections?.find((s: any) => s.id === selId); + if (!sel) continue; + const code = (sel.selectionCode || '').toUpperCase(); + if (code === 'HOME') return 'home'; + if (code === 'AWAY') return 'away'; + if (code === 'DRAW') return 'draw'; + if (code === 'OVER') return 'home'; + if (code === 'UNDER') return 'away'; + if (code === 'ODD' || code === 'EVEN') return 'draw'; + // Fallback: use selection index + const selIndex = market.selections?.findIndex((s: any) => s.id === selId) ?? -1; + if (selIndex === -1) continue; + if (market.marketType === 'FT_1X2') { + if (selIndex === 0) return 'home'; + if (selIndex === 1) return 'draw'; + if (selIndex === 2) return 'away'; + } else { + if (selIndex === 0) return 'home'; + if (selIndex === 1) return 'away'; + } + } + return ''; +} @@ -318,7 +349,7 @@ function toggleSelection(sel: Selection, market: Market, event?: MouseEvent) { - + {{ match.homeTeamName }} @@ -503,10 +534,14 @@ function toggleSelection(sel: Selection, market: Market, event?: MouseEvent) { } .match-score-band { - background: linear-gradient(180deg, #181818 0%, #111 100%); - border: 1px solid var(--border); - border-radius: 4px; - padding: 10px 16px; + background: + linear-gradient(135deg, rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.04) 50%, rgba(15, 10, 0, 0.1)), + url('../../assets/images/card-bg.webp') center/cover no-repeat; + border: 1px solid var(--border-gold); + box-shadow: var(--shadow-gold), inset 0 0 20px rgba(212, 175, 55, 0.06); + border-radius: 12px; + padding: 18px 24px; + overflow: hidden; display: flex; align-items: center; justify-content: space-between; @@ -768,4 +803,34 @@ function toggleSelection(sel: Selection, market: Market, event?: MouseEvent) { font-weight: 700; white-space: nowrap; } + +/* --- VS Selection Highlight --- */ +.team-side { + transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s ease, filter 0.3s ease; +} + +.match-score-band.home .team-side.home { + transform: scale(1.06); + filter: brightness(1.3) saturate(1.2); +} +.match-score-band.home .team-side.away { + opacity: 0.35; + transform: scale(0.94); + filter: grayscale(0.6) brightness(0.7); +} + +.match-score-band.away .team-side.away { + transform: scale(1.06); + filter: brightness(1.3) saturate(1.2); +} +.match-score-band.away .team-side.home { + opacity: 0.35; + transform: scale(0.94); + filter: grayscale(0.6) brightness(0.7); +} + +.match-score-band.draw .team-side { + transform: scale(1.04); + filter: brightness(1.15) saturate(1.1); +} diff --git a/apps/player/src/views/desktop/DesktopProfileEditView.vue b/apps/player/src/views/desktop/DesktopProfileEditView.vue index 6694077..6b6246c 100644 --- a/apps/player/src/views/desktop/DesktopProfileEditView.vue +++ b/apps/player/src/views/desktop/DesktopProfileEditView.vue @@ -110,6 +110,13 @@ async function onAvatarSelect(key: string | null) { + + + + + + {{ t('common.back') || '返回' }} + {{ t('profile.edit_title') }} @@ -216,7 +223,42 @@ async function onAvatarSelect(key: string | null) { .desktop-account-page { display: flex; flex-direction: column; min-height: 0; flex: 1; width: 100%; } .account-main { flex: 1; min-width: 0; padding: 24px 28px; overflow-y: auto; } -.page-header { margin-bottom: 24px; } +.page-header { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 24px; +} + +.back-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + border-radius: 6px; + border: 1px solid var(--desktop-border); + background: rgba(255, 255, 255, 0.03); + color: var(--text-muted); + font-size: 13px; + font-weight: 700; + cursor: pointer; + transition: all 0.2s ease; +} + +.back-btn:hover { + background: rgba(255, 255, 255, 0.08); + border-color: var(--border-gold-soft); + color: var(--primary-light); +} + +.back-btn svg { + transition: transform 0.2s ease; +} + +.back-btn:hover svg { + transform: translateX(-2px); +} + .page-title { font-size: 18px; font-weight: 800; color: var(--primary-light); margin: 0; } .edit-layout { diff --git a/apps/player/src/views/desktop/DesktopRechargeDetailView.vue b/apps/player/src/views/desktop/DesktopRechargeDetailView.vue new file mode 100644 index 0000000..3c33c41 --- /dev/null +++ b/apps/player/src/views/desktop/DesktopRechargeDetailView.vue @@ -0,0 +1,429 @@ + + + + + + + + ‹ {{ t('common.back') || '返回' }} + + {{ t('recharge.order_detail') }} + + + + + + + + {{ t('common.load_failed') }} + {{ t('common.retry') }} + + + + + {{ detail.orderNo }} + {{ statusLabel(detail.status) }} + + + {{ t('wallet.amount') }} + {{ formatMoney(detail.amount, locale) }} + + + {{ t('recharge.credited') }} + {{ formatMoney(detail.approvedAmount, locale) }} + + + {{ t('recharge.method') }} + {{ methodLabel(detail) }} + + + {{ t('recharge.apply_time') }} + {{ new Date(detail.createdAt).toLocaleString() }} + + + {{ t('recharge.review_time') }} + {{ new Date(detail.reviewedAt).toLocaleString() }} + + + + {{ orderNoteLine(detail) }} + + + {{ t('recharge.reapply') }} + + + + + {{ t('recharge.audit_title') }} + + + + + + + + + {{ auditActionLabel(entry.log.action) }} + {{ formatAuditTime(entry.log.createdAt) }} + + {{ entry.actor }} + + {{ t('recharge.audit_credited') }} {{ formatMoney(entry.log.approvedAmount, locale) }} + + + {{ t('recharge.reject_reason') }} + {{ entry.remark.text }} + + + {{ auditNoteDisplayText(entry.remark) }} + + + + + + + + {{ t('common.not_found') || '未找到记录' }} + + + + + diff --git a/apps/player/src/views/desktop/DesktopRechargeHistoryView.vue b/apps/player/src/views/desktop/DesktopRechargeHistoryView.vue index 6fef90b..01bc43c 100644 --- a/apps/player/src/views/desktop/DesktopRechargeHistoryView.vue +++ b/apps/player/src/views/desktop/DesktopRechargeHistoryView.vue @@ -20,7 +20,6 @@ type RechargeOrder = { methodType?: string; bankName?: string | null; paymentMethodName?: string | null; - approvedAmount?: string | null; createdAt: string; }; @@ -106,7 +105,11 @@ function statusLabel(status: string) { - {{ order.orderNo || order.id }} + + + {{ order.orderNo || order.id }} + + {{ methodLabel(order) }} {{ formatMoney(order.amount, locale) }} @@ -136,10 +139,3 @@ function statusLabel(status: string) { - - diff --git a/apps/player/src/views/desktop/DesktopRechargeView.vue b/apps/player/src/views/desktop/DesktopRechargeView.vue index 568d804..07b2026 100644 --- a/apps/player/src/views/desktop/DesktopRechargeView.vue +++ b/apps/player/src/views/desktop/DesktopRechargeView.vue @@ -6,11 +6,13 @@ import imageCompression from 'browser-image-compression'; import api from '../../api'; import GoldSpinner from '../../components/GoldSpinner.vue'; import { useDepositNotifications } from '../../composables/useDepositNotifications'; +import { useAppToast } from '../../composables/useAppToast'; const { t } = useI18n(); const router = useRouter(); const route = useRoute(); const { trackPendingOrder } = useDepositNotifications(); +const { showToast } = useAppToast(); const reapplyOrderId = computed(() => { const id = route.query.orderId; @@ -45,6 +47,12 @@ const bankMethods = computed(() => methods.value.filter((m) => m.methodType === const usdtMethods = computed(() => methods.value.filter((m) => m.methodType === 'USDT')); const currentMethods = computed(() => methodType.value === 'BANK' ? bankMethods.value : usdtMethods.value); +const copyText = (text: string) => { + if (!text) return; + navigator.clipboard.writeText(text); + showToast(t('common.copy_success') || '已复制'); +}; + function applyReapplyQuery() { const type = route.query.methodType; if (type === 'BANK' || type === 'USDT') methodType.value = type; @@ -107,8 +115,13 @@ async function submit() { fd.append('paymentMethodId', selectedMethod.value.id); fd.append('amount', amount.value); fd.append('screenshot', screenshotFile.value); - if (isReapply.value) fd.append('originalOrderId', reapplyOrderId.value); - const { data } = await api.post('/player/deposits', fd, { headers: { 'Content-Type': 'multipart/form-data' } }); + + let url = '/player/deposit-orders'; + if (isReapply.value) { + url = `/player/deposit-orders/${reapplyOrderId.value}/reapply`; + } + + const { data } = await api.post(url, fd, { headers: { 'Content-Type': 'multipart/form-data' } }); orderNo.value = data.data?.orderNo ?? ''; if (data.data?.id) trackPendingOrder(String(data.data.id)); success.value = true; @@ -128,6 +141,13 @@ onActivated(fetchMethods); + + + + + + {{ t('common.back') || '返回' }} + {{ t('recharge.title') }} @@ -136,16 +156,30 @@ onActivated(fetchMethods); - - {{ t('recharge.success_title') }} - {{ t('recharge.order_no') }}: {{ orderNo }} - {{ t('recharge.success_hint') }} + + + + + + + {{ t('recharge.success_title') || '充值申请已提交' }} + + + {{ t('recharge.order_no') || '订单号' }} + {{ orderNo }} + + + {{ t('recharge.amount') || '充值金额' }} + ¥ {{ parseFloat(amount).toFixed(2) }} + + + {{ t('recharge.success_hint') || '您的充值申请已提交,客服将尽快为您处理。' }} {{ t('recharge.history_title') }} - + @@ -157,106 +191,139 @@ onActivated(fetchMethods); - - - {{ t('recharge.choose_method') || '选择收款账户' }} - - {{ t('recharge.no_methods') || '暂无可用收款账户' }} - - - {{ m.displayName || m.bankName || 'USDT' }} - {{ m.accountNumber || m.usdtAddress || '' }} - - - - - - - - - {{ t('recharge.bank_name') || '银行名称' }} - {{ selectedMethod.bankName }} + + + {{ t('recharge.choose_method') || '选择收款账户' }} + + {{ t('recharge.no_methods') || '暂无可用收款账户' }} - - {{ t('recharge.account_holder') || '持卡人' }} - {{ selectedMethod.accountHolder }} + + + + + + + {{ m.displayName || m.bankName || 'USDT' }} + + {{ m.accountNumber || m.usdtAddress || '' }} + + + + + + + + + {{ t('recharge.bank_name') || '银行名称' }} + {{ selectedMethod.bankName }} + + {{ t('common.copy') || '复制' }} + + + + {{ t('recharge.account_holder') || '持卡人' }} + {{ selectedMethod.accountHolder }} + + {{ t('common.copy') || '复制' }} + + + + {{ t('recharge.account_number') || '账号' }} + {{ selectedMethod.accountNumber }} + + {{ t('common.copy') || '复制' }} + + - - {{ t('recharge.account_number') || '账号' }} - {{ selectedMethod.accountNumber }} + + + + + + + {{ t('recharge.scan_to_pay') || '请扫描二维码完成支付' }} - - - - - - - - {{ t('recharge.usdt_address') || 'USDT地址' }} - {{ selectedMethod.usdtAddress }} - - - - - {{ t('recharge.amount') || '充值金额' }} - - - - - - {{ t('recharge.screenshot') || '上传凭证截图' }} - - - ✕ + + {{ t('recharge.usdt_address') || 'USDT地址' }} + {{ selectedMethod.usdtAddress }} + + {{ t('common.copy') || '复制' }} + - - - - - - {{ t('recharge.click_to_upload') || '点击上传截图' }} - - - - - - {{ t('recharge.submit') || '提交充值' }} - + + + {{ t('recharge.amount') || '充值金额' }} + + ¥ + + + + + +{{ val }} + + + + + + + {{ t('recharge.screenshot') || '上传凭证截图' }} + + + ✕ + + + + + + + {{ t('recharge.click_to_upload') || '点击上传截图' }} + + + + + + + {{ t('recharge.submit') || '提交充值' }} + + -
{{ entry.actor }}
+ {{ t('recharge.audit_credited') }} {{ formatMoney(entry.log.approvedAmount, locale) }} +
+ {{ auditNoteDisplayText(entry.remark) }} +
- {{ t('recharge.audit_credited') }} {{ formatMoney(entry.log.approvedAmount, locale) }} -
- {{ auditNoteDisplayText(entry.remark) }} -
{{ t('common.load_failed') }}
{{ t('recharge.order_no') }}: {{ orderNo }}
{{ t('recharge.success_hint') }}
{{ t('recharge.success_hint') || '您的充值申请已提交,客服将尽快为您处理。' }}
{{ t('recharge.choose_method') || '选择收款账户' }}
{{ t('recharge.scan_to_pay') || '请扫描二维码完成支付' }}