feat(player): 充值订单详情与审核记录,优化桌面端交互与记录列表样式
新增充值详情页及单条订单 API,修复桌面充值提交路径;充值/注单记录编号改为中性色,并包含桌面首页与投注相关 UI 改进。
This commit is contained in:
@@ -436,6 +436,15 @@ export class PlayerController {
|
|||||||
return jsonResponse(result);
|
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')
|
@Get('deposit-orders/:id/audit-logs')
|
||||||
async myDepositOrderAuditLogs(
|
async myDepositOrderAuditLogs(
|
||||||
@CurrentUser('id') userId: bigint,
|
@CurrentUser('id') userId: bigint,
|
||||||
|
|||||||
@@ -416,6 +416,85 @@ export class DepositService {
|
|||||||
return map;
|
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(
|
async createDepositOrder(
|
||||||
playerId: bigint,
|
playerId: bigint,
|
||||||
paymentMethodId: 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) {
|
async getPlayerDepositOrders(playerId: bigint, page = 1, pageSize = 20) {
|
||||||
const skip = (page - 1) * pageSize;
|
const skip = (page - 1) * pageSize;
|
||||||
const where = { playerId };
|
const where = { playerId };
|
||||||
@@ -543,32 +663,22 @@ export class DepositService {
|
|||||||
const auditMap = await this.attachPlayerAuditLogs(items);
|
const auditMap = await this.attachPlayerAuditLogs(items);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: items.map((o) => ({
|
items: items.map((o) =>
|
||||||
id: o.id.toString(),
|
this.formatPlayerDepositOrder(
|
||||||
orderNo: o.orderNo,
|
o,
|
||||||
paymentMethodId: o.paymentMethodId.toString(),
|
(auditMap.get(o.id.toString()) ?? []).map((log) => ({
|
||||||
methodType: o.methodType,
|
id: log.id,
|
||||||
amount: o.amount.toString(),
|
action: log.action,
|
||||||
screenshotUrl: o.screenshotUrl,
|
actorType: log.actorType,
|
||||||
status: o.status,
|
statusBefore: log.statusBefore,
|
||||||
approvedAmount: o.approvedAmount?.toString() ?? null,
|
statusAfter: log.statusAfter,
|
||||||
rejectReason: o.rejectReason,
|
amount: log.amount,
|
||||||
remark: o.remark,
|
approvedAmount: log.approvedAmount,
|
||||||
createdAt: o.createdAt,
|
remark: log.remark,
|
||||||
reviewedAt: o.reviewedAt,
|
createdAt: log.createdAt,
|
||||||
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,
|
|
||||||
})),
|
|
||||||
})),
|
|
||||||
total,
|
total,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
|
|||||||
@@ -68,7 +68,12 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="root" class="cash-chip-wrap">
|
<div
|
||||||
|
ref="root"
|
||||||
|
class="cash-chip-wrap"
|
||||||
|
@mouseenter="open = true; refreshProfile()"
|
||||||
|
@mouseleave="open = false"
|
||||||
|
>
|
||||||
<button type="button" class="cash-chip" @click="toggle">
|
<button type="button" class="cash-chip" @click="toggle">
|
||||||
<span class="chip-body">
|
<span class="chip-body">
|
||||||
<span class="chip-label">{{ t('wallet.cash_balance') }}</span>
|
<span class="chip-label">{{ t('wallet.cash_balance') }}</span>
|
||||||
@@ -99,8 +104,6 @@ onUnmounted(() => {
|
|||||||
{{ t('recharge.title') }}
|
{{ t('recharge.title') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="open" class="backdrop" @click="close" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,32 @@ const { isDesktop } = useViewport();
|
|||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const { inboxEnabled, hubTitleKey } = useInboxFeature();
|
const { inboxEnabled, hubTitleKey } = useInboxFeature();
|
||||||
const { unreadCount, messages, listLoaded, refreshUnreadCount, markAllRead, deleteAllMessages } = usePlayerMessages();
|
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 markingAll = ref(false);
|
||||||
const deletingAll = ref(false);
|
const deletingAll = ref(false);
|
||||||
@@ -91,7 +116,42 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="floating-mailbox" :class="{ 'is-betting-desktop': isBettingDesktop }">
|
<div
|
||||||
|
class="floating-mailbox"
|
||||||
|
:class="{ 'is-betting-desktop': isBettingDesktop }"
|
||||||
|
@mouseenter="onMouseEnter"
|
||||||
|
@mouseleave="onMouseLeave"
|
||||||
|
>
|
||||||
|
<!-- Hover Menu -->
|
||||||
|
<Transition name="fade-slide-mini">
|
||||||
|
<div v-if="isDesktop && hovering && !isOpen" class="hover-menu">
|
||||||
|
<button
|
||||||
|
v-if="inboxEnabled"
|
||||||
|
type="button"
|
||||||
|
class="menu-item"
|
||||||
|
@click="openTab('messages')"
|
||||||
|
>
|
||||||
|
<svg class="menu-icon" viewBox="0 0 24 24" width="16" height="16">
|
||||||
|
<path fill="currentColor" d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/>
|
||||||
|
</svg>
|
||||||
|
<span>{{ t('messages.tab_inbox') }}</span>
|
||||||
|
<span v-if="unreadCount > 0" class="menu-badge">
|
||||||
|
{{ unreadCount > 99 ? '99+' : unreadCount }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="menu-item"
|
||||||
|
@click="openTab('support')"
|
||||||
|
>
|
||||||
|
<svg class="menu-icon" viewBox="0 0 24 24" width="16" height="16">
|
||||||
|
<path fill="currentColor" d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zM6 9h12v2H6V9zm8 5H6v-2h8v2zm4-6H6V6h12v2z"/>
|
||||||
|
</svg>
|
||||||
|
<span>{{ t('messages.tab_support') }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
<!-- Floating Button -->
|
<!-- Floating Button -->
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -121,25 +181,9 @@ onMounted(() => {
|
|||||||
<Transition name="fade-slide">
|
<Transition name="fade-slide">
|
||||||
<div v-if="isOpen" class="popup-panel">
|
<div v-if="isOpen" class="popup-panel">
|
||||||
<div class="popup-header">
|
<div class="popup-header">
|
||||||
<div class="tabs">
|
<div class="popup-title">
|
||||||
<button
|
<span>{{ activeTab === 'messages' ? t('messages.tab_inbox') : t('messages.tab_support') }}</span>
|
||||||
v-if="inboxEnabled"
|
<span v-if="activeTab === 'messages' && unreadInList > 0" class="tab-badge">{{ unreadInList }}</span>
|
||||||
type="button"
|
|
||||||
class="tab-btn"
|
|
||||||
:class="{ active: activeTab === 'messages' }"
|
|
||||||
@click="switchTab('messages')"
|
|
||||||
>
|
|
||||||
{{ t('messages.tab_inbox') }}
|
|
||||||
<span v-if="unreadInList > 0" class="tab-badge">{{ unreadInList }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="tab-btn"
|
|
||||||
:class="{ active: activeTab === 'support' }"
|
|
||||||
@click="switchTab('support')"
|
|
||||||
>
|
|
||||||
{{ t('messages.tab_support') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
@@ -168,6 +212,100 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.hover-menu {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 64px;
|
||||||
|
right: 0;
|
||||||
|
background: rgba(22, 22, 22, 0.95);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(212, 175, 55, 0.25);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
|
||||||
|
width: 120px;
|
||||||
|
z-index: 1001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-menu::after,
|
||||||
|
.hover-menu::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-style: solid;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-menu::after {
|
||||||
|
bottom: -6px;
|
||||||
|
right: 22px;
|
||||||
|
border-width: 6px 6px 0 6px;
|
||||||
|
border-color: rgba(22, 22, 22, 0.95) transparent transparent transparent;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-menu::before {
|
||||||
|
bottom: -7px;
|
||||||
|
right: 21px;
|
||||||
|
border-width: 7px 7px 0 7px;
|
||||||
|
border-color: rgba(212, 175, 55, 0.25) transparent transparent transparent;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
color: var(--primary-light);
|
||||||
|
background: rgba(212, 175, 55, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
color: var(--primary-light);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-badge {
|
||||||
|
margin-left: auto;
|
||||||
|
background: var(--danger, #ff4d4f);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 8px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-slide-mini-enter-active,
|
||||||
|
.fade-slide-mini-leave-active {
|
||||||
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transform-origin: bottom right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-slide-mini-enter-from,
|
||||||
|
.fade-slide-mini-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95) translateY(10px);
|
||||||
|
}
|
||||||
|
|
||||||
.floating-mailbox {
|
.floating-mailbox {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 32px;
|
bottom: 32px;
|
||||||
@@ -249,45 +387,23 @@ onMounted(() => {
|
|||||||
height: 48px;
|
height: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
.popup-title {
|
||||||
display: flex;
|
color: var(--primary-light);
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-btn {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
border-bottom: 2px solid transparent;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
padding: 0 16px;
|
padding: 0 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-btn:hover {
|
.popup-title .tab-badge {
|
||||||
color: #fff;
|
background: var(--primary);
|
||||||
}
|
color: #141414;
|
||||||
|
|
||||||
.tab-btn.active {
|
|
||||||
color: var(--primary-light);
|
|
||||||
border-bottom-color: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-badge {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
color: #fff;
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
font-weight: 700;
|
||||||
|
|
||||||
.tab-btn.active .tab-badge {
|
|
||||||
background: var(--primary);
|
|
||||||
color: #141414;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-actions {
|
.header-actions {
|
||||||
|
|||||||
@@ -45,7 +45,13 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="root" class="locale-switch" :class="{ compact, open }">
|
<div
|
||||||
|
ref="root"
|
||||||
|
class="locale-switch"
|
||||||
|
:class="{ compact, open }"
|
||||||
|
@mouseenter="open = true"
|
||||||
|
@mouseleave="open = false"
|
||||||
|
>
|
||||||
<button type="button" class="locale-trigger" :aria-expanded="open" aria-haspopup="listbox" @click.stop="toggle">
|
<button type="button" class="locale-trigger" :aria-expanded="open" aria-haspopup="listbox" @click.stop="toggle">
|
||||||
<LocaleFlag :locale="locale" :size="compact ? 16 : 18" />
|
<LocaleFlag :locale="locale" :size="compact ? 16 : 18" />
|
||||||
</button> <ul v-show="open" class="locale-menu" role="listbox" :aria-label="compact ? 'Language' : undefined">
|
</button> <ul v-show="open" class="locale-menu" role="listbox" :aria-label="compact ? 'Language' : undefined">
|
||||||
|
|||||||
@@ -82,7 +82,40 @@ const liveScoreText = computed(() => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const slip = useBetSlipStore();
|
const slip = useBetSlipStore();
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const { openAt } = useDesktopBetPopover();
|
const { openAt, visible: popoverVisible, pendingItem, cancelClose, scheduleClose } = useDesktopBetPopover();
|
||||||
|
|
||||||
|
function getHoveredSide() {
|
||||||
|
if (!popoverVisible.value || !pendingItem.value) return '';
|
||||||
|
if (String(pendingItem.value.matchId) !== String(props.match.id)) return '';
|
||||||
|
|
||||||
|
const selId = pendingItem.value.selectionId;
|
||||||
|
// Scan all markets to find the selection and get its code
|
||||||
|
for (const market of props.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';
|
||||||
|
// Over/Under: over = home side, under = away side (for visual cue)
|
||||||
|
if (code === 'OVER') return 'home';
|
||||||
|
if (code === 'UNDER') return 'away';
|
||||||
|
// Odd/Even: highlight both
|
||||||
|
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 '';
|
||||||
|
}
|
||||||
|
|
||||||
const activeMarketType = ref<string>('');
|
const activeMarketType = ref<string>('');
|
||||||
|
|
||||||
@@ -173,7 +206,7 @@ function goMatchDetail() {
|
|||||||
<span v-else-if="phase === 'settled'" class="status-tag status-tag--settled">{{ phaseLabel }}</span>
|
<span v-else-if="phase === 'settled'" class="status-tag status-tag--settled">{{ phaseLabel }}</span>
|
||||||
<span v-else class="status-tag status-tag--pending">{{ phaseLabel }}</span>
|
<span v-else class="status-tag status-tag--pending">{{ phaseLabel }}</span>
|
||||||
|
|
||||||
<div class="teams-row">
|
<div class="teams-row" :class="getHoveredSide()">
|
||||||
<div class="team">
|
<div class="team">
|
||||||
<span class="team-name">{{ match.homeTeamName }}</span>
|
<span class="team-name">{{ match.homeTeamName }}</span>
|
||||||
<img
|
<img
|
||||||
@@ -217,7 +250,7 @@ function goMatchDetail() {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Morphing Quick Bet panel (Desktop Only, disabled when noQuickBet=true) -->
|
<!-- Morphing Quick Bet panel (Desktop Only, disabled when noQuickBet=true) -->
|
||||||
<div v-if="!noQuickBet" class="card-quick-bet" @click.stop>
|
<div v-if="!noQuickBet" class="card-quick-bet" @click.stop @mouseenter="cancelClose" @mouseleave="scheduleClose()">
|
||||||
<div class="quick-bet-tabs" v-if="getAvailableMarketsForMatch(match).length">
|
<div class="quick-bet-tabs" v-if="getAvailableMarketsForMatch(match).length">
|
||||||
<button
|
<button
|
||||||
v-for="m in getAvailableMarketsForMatch(match)"
|
v-for="m in getAvailableMarketsForMatch(match)"
|
||||||
@@ -379,7 +412,7 @@ function goMatchDetail() {
|
|||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.9);
|
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.9);
|
||||||
transition: font-size 0.25s ease;
|
transition: font-size 0.25s ease, transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), text-shadow 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-tag {
|
.status-tag {
|
||||||
@@ -593,4 +626,35 @@ function goMatchDetail() {
|
|||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- VS Selection Highlight: scale selected team, no VS animation --- */
|
||||||
|
.teams-row .team {
|
||||||
|
transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s ease, filter 0.3s ease;
|
||||||
|
}
|
||||||
|
.teams-row.home .team:first-child {
|
||||||
|
transform: scale(1.08);
|
||||||
|
filter: brightness(1.3) saturate(1.2);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.teams-row.home .team:last-child {
|
||||||
|
opacity: 0.35 !important;
|
||||||
|
transform: scale(0.94);
|
||||||
|
filter: grayscale(0.6) brightness(0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.teams-row.away .team:last-child {
|
||||||
|
transform: scale(1.08);
|
||||||
|
filter: brightness(1.3) saturate(1.2);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.teams-row.away .team:first-child {
|
||||||
|
opacity: 0.35 !important;
|
||||||
|
transform: scale(0.94);
|
||||||
|
filter: grayscale(0.6) brightness(0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.teams-row.draw .team {
|
||||||
|
transform: scale(1.05);
|
||||||
|
filter: brightness(1.15) saturate(1.1);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -58,7 +58,12 @@ function logout() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="root" class="avatar-wrap">
|
<div
|
||||||
|
ref="root"
|
||||||
|
class="avatar-wrap"
|
||||||
|
@mouseenter="open = true"
|
||||||
|
@mouseleave="open = false"
|
||||||
|
>
|
||||||
<button type="button" class="avatar-btn" :aria-expanded="open" @click="toggle">
|
<button type="button" class="avatar-btn" :aria-expanded="open" @click="toggle">
|
||||||
<img v-if="displayAvatarUrl" :src="displayAvatarUrl" alt="" class="avatar-img" />
|
<img v-if="displayAvatarUrl" :src="displayAvatarUrl" alt="" class="avatar-img" />
|
||||||
</button>
|
</button>
|
||||||
@@ -69,8 +74,6 @@ function logout() {
|
|||||||
<button type="button" class="menu-item" @click="goEdit">{{ t('profile.edit') }}</button>
|
<button type="button" class="menu-item" @click="goEdit">{{ t('profile.edit') }}</button>
|
||||||
<button type="button" class="menu-item danger" @click="logout">{{ t('auth.logout') }}</button>
|
<button type="button" class="menu-item danger" @click="logout">{{ t('auth.logout') }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="open" class="backdrop" @click="close" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import { usePlayerProfile } from '../../composables/usePlayerProfile';
|
|||||||
import ConfirmDialog from '../ConfirmDialog.vue';
|
import ConfirmDialog from '../ConfirmDialog.vue';
|
||||||
import { buildBetPlaceConfirmMessage } from '../../utils/betPlaceConfirmMessage';
|
import { buildBetPlaceConfirmMessage } from '../../utils/betPlaceConfirmMessage';
|
||||||
import { useAppToast } from '../../composables/useAppToast';
|
import { useAppToast } from '../../composables/useAppToast';
|
||||||
|
import BetSuccessOverlay from '../BetSuccessOverlay.vue';
|
||||||
|
|
||||||
const { t, locale } = useI18n();
|
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 slip = useBetSlipStore();
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const { refreshProfile } = usePlayerProfile();
|
const { refreshProfile } = usePlayerProfile();
|
||||||
@@ -25,6 +26,20 @@ const error = ref('');
|
|||||||
const showPlaceConfirm = ref(false);
|
const showPlaceConfirm = ref(false);
|
||||||
const placeConfirmMessage = ref('');
|
const placeConfirmMessage = ref('');
|
||||||
const MIN_STAKE = 5;
|
const MIN_STAKE = 5;
|
||||||
|
const showSuccess = ref(false);
|
||||||
|
const confirmingItem = ref<any>(null);
|
||||||
|
|
||||||
|
watch(showPlaceConfirm, (val) => {
|
||||||
|
if (!val) {
|
||||||
|
confirmingItem.value = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function onPopoverMouseLeave() {
|
||||||
|
if (showPlaceConfirm.value) return;
|
||||||
|
scheduleClose(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
let outsideClickTimer = 0;
|
let outsideClickTimer = 0;
|
||||||
|
|
||||||
@@ -95,6 +110,7 @@ function validatePlaceNow(): boolean {
|
|||||||
function onPlaceNowClick() {
|
function onPlaceNowClick() {
|
||||||
if (!validatePlaceNow()) return;
|
if (!validatePlaceNow()) return;
|
||||||
const item = pendingItem.value!;
|
const item = pendingItem.value!;
|
||||||
|
confirmingItem.value = item;
|
||||||
placeConfirmMessage.value = buildBetPlaceConfirmMessage(t, {
|
placeConfirmMessage.value = buildBetPlaceConfirmMessage(t, {
|
||||||
mode: 'single',
|
mode: 'single',
|
||||||
items: [item],
|
items: [item],
|
||||||
@@ -107,7 +123,7 @@ function onPlaceNowClick() {
|
|||||||
showPlaceConfirm.value = true;
|
showPlaceConfirm.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executePlaceNow(item = pendingItem.value) {
|
async function executePlaceNow(item = confirmingItem.value || pendingItem.value) {
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
@@ -122,7 +138,7 @@ async function executePlaceNow(item = pendingItem.value) {
|
|||||||
await refreshProfile();
|
await refreshProfile();
|
||||||
showPlaceConfirm.value = false;
|
showPlaceConfirm.value = false;
|
||||||
close();
|
close();
|
||||||
showToast(t('bet.place_success'));
|
showSuccess.value = true;
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
error.value =
|
error.value =
|
||||||
(e as { response?: { data?: { error?: string } } })?.response?.data?.error ||
|
(e as { response?: { data?: { error?: string } } })?.response?.data?.error ||
|
||||||
@@ -134,7 +150,7 @@ async function executePlaceNow(item = pendingItem.value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function confirmPlaceNow() {
|
async function confirmPlaceNow() {
|
||||||
const item = pendingItem.value;
|
const item = confirmingItem.value;
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
showPlaceConfirm.value = false;
|
showPlaceConfirm.value = false;
|
||||||
await executePlaceNow(item);
|
await executePlaceNow(item);
|
||||||
@@ -184,11 +200,13 @@ function addToParlayList() {
|
|||||||
v-if="visible && pendingItem"
|
v-if="visible && pendingItem"
|
||||||
ref="popRef"
|
ref="popRef"
|
||||||
class="bet-popover"
|
class="bet-popover"
|
||||||
|
:class="`placement-${placement}`"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
:style="{ left: `${anchorX}px`, top: `${anchorY}px` }"
|
:style="{ left: `${anchorX}px`, top: `${anchorY}px` }"
|
||||||
@click.stop
|
@click.stop
|
||||||
|
@mouseenter="cancelClose"
|
||||||
|
@mouseleave="onPopoverMouseLeave"
|
||||||
>
|
>
|
||||||
<button type="button" class="pop-close" aria-label="Close" @click="close">✕</button>
|
|
||||||
|
|
||||||
<div class="pop-match">{{ pendingItem.matchName }}</div>
|
<div class="pop-match">{{ pendingItem.matchName }}</div>
|
||||||
<div class="pop-market">{{ pendingItem.marketName }}</div>
|
<div class="pop-market">{{ pendingItem.marketName }}</div>
|
||||||
@@ -233,104 +251,146 @@ function addToParlayList() {
|
|||||||
@confirm="confirmPlaceNow"
|
@confirm="confirmPlaceNow"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<BetSuccessOverlay :show="showSuccess" @done="showSuccess = false" />
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.bet-popover {
|
.bet-popover {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
width: 280px;
|
width: 190px;
|
||||||
padding: 12px 28px 12px 12px;
|
padding: 8px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid var(--border-gold-soft);
|
border: 1px solid rgba(212, 175, 55, 0.2);
|
||||||
background: rgba(18, 18, 18, 0.98);
|
background: rgba(22, 22, 22, 0.9);
|
||||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.55);
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.55);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pop-close {
|
.bet-popover::after,
|
||||||
|
.bet-popover::before {
|
||||||
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 6px;
|
width: 0;
|
||||||
right: 6px;
|
height: 0;
|
||||||
background: none;
|
border-style: solid;
|
||||||
border: none;
|
pointer-events: none;
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 2px 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pop-close:hover {
|
.bet-popover.placement-bottom::after {
|
||||||
color: #fff;
|
top: -6px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
border-width: 0 6px 6px 6px;
|
||||||
|
border-color: transparent transparent rgba(22, 22, 22, 0.9) transparent;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bet-popover.placement-bottom::before {
|
||||||
|
top: -7px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
border-width: 0 7px 7px 7px;
|
||||||
|
border-color: transparent transparent rgba(212, 175, 55, 0.2) transparent;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bet-popover.placement-top::after {
|
||||||
|
bottom: -6px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
border-width: 6px 6px 0 6px;
|
||||||
|
border-color: rgba(22, 22, 22, 0.9) transparent transparent transparent;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bet-popover.placement-top::before {
|
||||||
|
bottom: -7px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
border-width: 7px 7px 0 7px;
|
||||||
|
border-color: rgba(212, 175, 55, 0.2) transparent transparent transparent;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pop-match {
|
.pop-match {
|
||||||
font-size: 11px;
|
font-size: 9px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
line-height: 1.35;
|
line-height: 1.3;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 1px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pop-market {
|
.pop-market {
|
||||||
font-size: 10px;
|
font-size: 8px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin-bottom: 6px;
|
margin-bottom: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pop-pick-row {
|
.pop-pick-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
padding: 6px 8px;
|
padding: 4px 6px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 6px;
|
||||||
border-radius: 4px;
|
border-radius: 3px;
|
||||||
background: rgba(212, 175, 55, 0.06);
|
background: rgba(212, 175, 55, 0.04);
|
||||||
border: 1px solid var(--border-gold-soft);
|
border: 1px solid rgba(212, 175, 55, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pick {
|
.pick {
|
||||||
font-size: 12px;
|
font-size: 10px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.odds {
|
.odds {
|
||||||
font-size: 12px;
|
font-size: 10px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: var(--primary-light);
|
color: var(--primary-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stake-label {
|
.stake-label {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 10px;
|
font-size: 8px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin-bottom: 4px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stake-input {
|
.stake-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 6px 8px;
|
padding: 4px 6px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 5px;
|
||||||
border-radius: 4px;
|
border-radius: 3px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
background: #0d0d0d;
|
background: #080808;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 12px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stake-input:focus {
|
.stake-input:focus {
|
||||||
border-color: var(--border-gold-soft);
|
border-color: rgba(212, 175, 55, 0.3);
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.est-row {
|
.est-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
font-size: 10px;
|
font-size: 8px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin-bottom: 10px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.est-val {
|
.est-val {
|
||||||
@@ -339,20 +399,20 @@ function addToParlayList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pop-error {
|
.pop-error {
|
||||||
font-size: 10px;
|
font-size: 8px;
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
margin: 0 0 8px;
|
margin: 0 0 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pop-actions {
|
.pop-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 6px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pop-actions-row {
|
.pop-actions-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 6px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pop-actions-row .btn-outline {
|
.pop-actions-row .btn-outline {
|
||||||
@@ -363,18 +423,19 @@ function addToParlayList() {
|
|||||||
|
|
||||||
.btn-primary-gold {
|
.btn-primary-gold {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 8px;
|
padding: 5px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 3px;
|
||||||
background: linear-gradient(180deg, #f0d875 0%, #d4af37 100%);
|
background: linear-gradient(180deg, #f0d875 0%, #d4af37 100%);
|
||||||
color: #3d2800;
|
color: #3d2800;
|
||||||
font-size: 12px;
|
font-size: 10px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary-gold:hover:not(:disabled) {
|
.btn-primary-gold:hover:not(:disabled) {
|
||||||
opacity: 0.92;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary-gold:disabled {
|
.btn-primary-gold:disabled {
|
||||||
@@ -384,17 +445,18 @@ function addToParlayList() {
|
|||||||
|
|
||||||
.btn-outline {
|
.btn-outline {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 7px;
|
padding: 4px;
|
||||||
border-radius: 4px;
|
border-radius: 3px;
|
||||||
border: 1px solid var(--border-gold-soft);
|
border: 1px solid rgba(212, 175, 55, 0.25);
|
||||||
background: rgba(212, 175, 55, 0.05);
|
background: rgba(212, 175, 55, 0.03);
|
||||||
color: var(--primary-light);
|
color: var(--primary-light);
|
||||||
font-size: 11px;
|
font-size: 9px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-outline:hover {
|
.btn-outline:hover {
|
||||||
background: rgba(212, 175, 55, 0.12);
|
background: rgba(212, 175, 55, 0.08);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { groupCorrectScoreSelections, type CsSelection } from '../../utils/correctScoreLayout';
|
import { groupCorrectScoreSelections, type CsSelection } from '../../utils/correctScoreLayout';
|
||||||
|
import { useDesktopBetPopover } from '../../composables/useDesktopBetPopover';
|
||||||
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
marketType: string;
|
marketType: string;
|
||||||
@@ -24,6 +26,7 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const { scheduleClose, cancelClose } = useDesktopBetPopover();
|
||||||
|
|
||||||
const columns = computed(() =>
|
const columns = computed(() =>
|
||||||
groupCorrectScoreSelections(
|
groupCorrectScoreSelections(
|
||||||
@@ -55,7 +58,7 @@ function formatOdds(odds: string) {
|
|||||||
<span>{{ t('bet.col_away') }}</span>
|
<span>{{ t('bet.col_away') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="cols-grid">
|
<div class="cols-grid" @mouseenter="cancelClose" @mouseleave="scheduleClose()">
|
||||||
<div v-for="colKey in ['home', 'draw', 'away'] as const" :key="colKey" class="col">
|
<div v-for="colKey in ['home', 'draw', 'away'] as const" :key="colKey" class="col">
|
||||||
<button
|
<button
|
||||||
v-for="sel in columns[colKey]"
|
v-for="sel in columns[colKey]"
|
||||||
@@ -65,6 +68,7 @@ function formatOdds(odds: string) {
|
|||||||
:class="{ selected: isSelected(sel.id), 'score-card--locked': locked }"
|
:class="{ selected: isSelected(sel.id), 'score-card--locked': locked }"
|
||||||
:data-bet-selection-id="sel.id"
|
:data-bet-selection-id="sel.id"
|
||||||
:disabled="locked"
|
:disabled="locked"
|
||||||
|
@mouseenter="onPick(sel, $event)"
|
||||||
@click="onPick(sel, $event)"
|
@click="onPick(sel, $event)"
|
||||||
>
|
>
|
||||||
<span class="score-line">{{ sel.scoreDisplay }}</span>
|
<span class="score-line">{{ sel.scoreDisplay }}</span>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { resolveSelectionLabel } from '../../utils/selectionLabel';
|
import { resolveSelectionLabel } from '../../utils/selectionLabel';
|
||||||
|
import { useDesktopBetPopover } from '../../composables/useDesktopBetPopover';
|
||||||
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
selections: {
|
selections: {
|
||||||
@@ -23,6 +25,7 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const emit = defineEmits<{ pick: [id: string, event?: MouseEvent] }>();
|
const emit = defineEmits<{ pick: [id: string, event?: MouseEvent] }>();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const { scheduleClose, cancelClose } = useDesktopBetPopover();
|
||||||
|
|
||||||
function label(sel: (typeof props.selections)[number]) {
|
function label(sel: (typeof props.selections)[number]) {
|
||||||
if (sel.selectionDisplayName?.trim()) return sel.selectionDisplayName;
|
if (sel.selectionDisplayName?.trim()) return sel.selectionDisplayName;
|
||||||
@@ -53,7 +56,7 @@ const panelStyle = computed(() =>
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="wrap" :class="{ compact, horizontal, desktopCard, locked }">
|
<div class="wrap" :class="{ compact, horizontal, desktopCard, locked }" @mouseenter="cancelClose" @mouseleave="scheduleClose()">
|
||||||
<div
|
<div
|
||||||
class="panel"
|
class="panel"
|
||||||
:class="{ horizontal, 'desktop-card': desktopCard }"
|
:class="{ horizontal, 'desktop-card': desktopCard }"
|
||||||
@@ -67,6 +70,7 @@ const panelStyle = computed(() =>
|
|||||||
:class="{ selected: isSelected(sel.id), 'odds-btn--locked': locked }"
|
:class="{ selected: isSelected(sel.id), 'odds-btn--locked': locked }"
|
||||||
:data-bet-selection-id="sel.id"
|
:data-bet-selection-id="sel.id"
|
||||||
:disabled="locked"
|
:disabled="locked"
|
||||||
|
@mouseenter="onPick(sel.id, $event)"
|
||||||
@click="onPick(sel.id, $event)"
|
@click="onPick(sel.id, $event)"
|
||||||
>
|
>
|
||||||
<span class="label">{{ label(sel) }}</span>
|
<span class="label">{{ label(sel) }}</span>
|
||||||
|
|||||||
@@ -5,20 +5,45 @@ const visible = ref(false);
|
|||||||
const anchorX = ref(0);
|
const anchorX = ref(0);
|
||||||
const anchorY = ref(0);
|
const anchorY = ref(0);
|
||||||
const pendingItem = ref<SlipItem | null>(null);
|
const pendingItem = ref<SlipItem | null>(null);
|
||||||
|
const placement = ref<'top' | 'bottom'>('bottom');
|
||||||
|
let closeTimer: number | undefined = undefined;
|
||||||
|
|
||||||
export function useDesktopBetPopover() {
|
export function useDesktopBetPopover() {
|
||||||
function openAt(event: MouseEvent, item: SlipItem) {
|
function openAt(event: MouseEvent, item: SlipItem) {
|
||||||
const pad = 12;
|
cancelClose();
|
||||||
const popW = 300;
|
|
||||||
const popH = 320;
|
const el = (event.currentTarget || event.target) as HTMLElement;
|
||||||
let x = event.clientX + pad;
|
const btn = el?.closest?.('button') || el;
|
||||||
let y = event.clientY + pad;
|
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') {
|
if (typeof window !== 'undefined') {
|
||||||
x = Math.min(x, window.innerWidth - popW - pad);
|
const spaceBelow = window.innerHeight - rect.bottom;
|
||||||
y = Math.min(y, window.innerHeight - popH - pad);
|
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;
|
pendingItem.value = item;
|
||||||
visible.value = true;
|
visible.value = true;
|
||||||
}
|
}
|
||||||
@@ -26,6 +51,28 @@ export function useDesktopBetPopover() {
|
|||||||
function close() {
|
function close() {
|
||||||
visible.value = false;
|
visible.value = false;
|
||||||
pendingItem.value = null;
|
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 {
|
return {
|
||||||
@@ -33,7 +80,10 @@ export function useDesktopBetPopover() {
|
|||||||
anchorX,
|
anchorX,
|
||||||
anchorY,
|
anchorY,
|
||||||
pendingItem,
|
pendingItem,
|
||||||
|
placement,
|
||||||
openAt,
|
openAt,
|
||||||
close,
|
close,
|
||||||
|
scheduleClose,
|
||||||
|
cancelClose,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,11 +113,31 @@ function collectAnnouncementLines(data: HomePayload | null): string[] {
|
|||||||
|
|
||||||
function collectAnnouncementItems(data: HomePayload | null): PlayerAnnouncementItem[] {
|
function collectAnnouncementItems(data: HomePayload | null): PlayerAnnouncementItem[] {
|
||||||
if (!data) return [];
|
if (!data) return [];
|
||||||
const source =
|
const seen = new Set<string>();
|
||||||
data.announcements && data.announcements.length > 0
|
const merged: PlayerAnnouncementItem[] = [];
|
||||||
? data.announcements
|
|
||||||
: [...(data.ticker ?? []), ...(data.notices ?? [])];
|
const pushItems = (items: PlayerAnnouncementItem[] | undefined) => {
|
||||||
return source.filter((item) => item.translation?.title || item.translation?.body);
|
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[] {
|
function collectHubContentItems(data: HomePayload | null): PlayerContentItem[] {
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ export default {
|
|||||||
cancelled: 'Cancelled',
|
cancelled: 'Cancelled',
|
||||||
error: 'Something went wrong',
|
error: 'Something went wrong',
|
||||||
status: 'Status',
|
status: 'Status',
|
||||||
|
copy: 'Copy',
|
||||||
|
copy_success: 'Copied',
|
||||||
},
|
},
|
||||||
pagination: {
|
pagination: {
|
||||||
total: '{total} total',
|
total: '{total} total',
|
||||||
@@ -313,6 +315,7 @@ export default {
|
|||||||
},
|
},
|
||||||
recharge: {
|
recharge: {
|
||||||
title: 'Recharge',
|
title: 'Recharge',
|
||||||
|
scan_to_pay: 'Scan QR code to pay',
|
||||||
history: 'History',
|
history: 'History',
|
||||||
history_title: 'Recharge History',
|
history_title: 'Recharge History',
|
||||||
bank_transfer: 'Bank Transfer',
|
bank_transfer: 'Bank Transfer',
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ export default {
|
|||||||
cancelled: 'Dibatalkan',
|
cancelled: 'Dibatalkan',
|
||||||
error: 'Operasi gagal',
|
error: 'Operasi gagal',
|
||||||
status: 'Status',
|
status: 'Status',
|
||||||
|
copy: 'Salin',
|
||||||
|
copy_success: 'Berjaya disalin',
|
||||||
},
|
},
|
||||||
pagination: {
|
pagination: {
|
||||||
total: '{total} jumlah',
|
total: '{total} jumlah',
|
||||||
@@ -325,6 +327,7 @@ export default {
|
|||||||
},
|
},
|
||||||
recharge: {
|
recharge: {
|
||||||
title: 'Topup',
|
title: 'Topup',
|
||||||
|
scan_to_pay: 'Imbas kod QR untuk bayar',
|
||||||
history: 'Sejarah',
|
history: 'Sejarah',
|
||||||
history_title: 'Sejarah Topup',
|
history_title: 'Sejarah Topup',
|
||||||
bank_transfer: 'Pindahan Bank',
|
bank_transfer: 'Pindahan Bank',
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ export default {
|
|||||||
cancelled: '已取消',
|
cancelled: '已取消',
|
||||||
error: '操作失败',
|
error: '操作失败',
|
||||||
status: '状态',
|
status: '状态',
|
||||||
|
copy: '复制',
|
||||||
|
copy_success: '复制成功',
|
||||||
},
|
},
|
||||||
pagination: {
|
pagination: {
|
||||||
total: '共 {total} 条',
|
total: '共 {total} 条',
|
||||||
@@ -313,6 +315,7 @@ export default {
|
|||||||
},
|
},
|
||||||
recharge: {
|
recharge: {
|
||||||
title: '充值',
|
title: '充值',
|
||||||
|
scan_to_pay: '请扫描二维码完成支付',
|
||||||
history: '记录',
|
history: '记录',
|
||||||
history_title: '充值记录',
|
history_title: '充值记录',
|
||||||
bank_transfer: '银行转账',
|
bank_transfer: '银行转账',
|
||||||
|
|||||||
@@ -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', component: () => import('../views/ProfileView.vue'), meta: { keepAlive: true, 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 } },
|
{ path: 'profile/cashbacks', component: () => import('../views/CashbackRecordsView.vue'), meta: { requiresAuth: true } },
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ body {
|
|||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
body {
|
body {
|
||||||
background-image: url('./assets/images/pcbg.webp');
|
background-image: url('./assets/images/pcbg.png');
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: #000;
|
background-color: #000;
|
||||||
background-image: url('../../assets/images/pcbg.webp');
|
background-image: url('../../assets/images/pcbg.png');
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
|
|||||||
@@ -181,7 +181,7 @@
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: var(--primary-light);
|
color: var(--text-muted);
|
||||||
background: #121212;
|
background: #121212;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.06em;
|
||||||
@@ -209,7 +209,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.desktop-records-table tbody tr.hover-row:hover {
|
.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 {
|
.desktop-records-table .num-cell {
|
||||||
@@ -224,16 +238,6 @@
|
|||||||
font-size: 12px;
|
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 {
|
.desktop-records-pagination {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
|
|||||||
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>
|
||||||
@@ -6,34 +6,15 @@ 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 {
|
|
||||||
auditActionTone,
|
|
||||||
auditActorSecondary,
|
|
||||||
formatDepositAuditRemark,
|
|
||||||
shouldShowAuditRejectInTimeline,
|
|
||||||
} from '../utils/depositAuditDisplay';
|
|
||||||
import { useDepositNotifications } from '../composables/useDepositNotifications';
|
import { useDepositNotifications } from '../composables/useDepositNotifications';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t, locale } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
const { trackPendingOrder, pollOnce } = useDepositNotifications();
|
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 {
|
interface DepositOrder {
|
||||||
id: string;
|
id: string;
|
||||||
orderNo: string;
|
orderNo: string;
|
||||||
paymentMethodId?: string;
|
|
||||||
methodType: string;
|
methodType: string;
|
||||||
amount: string;
|
amount: string;
|
||||||
status: string;
|
status: string;
|
||||||
@@ -43,7 +24,7 @@ interface DepositOrder {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
reviewedAt: string | null;
|
reviewedAt: string | null;
|
||||||
paymentMethodName: string | null;
|
paymentMethodName: string | null;
|
||||||
auditLogs?: DepositAuditLog[];
|
auditLogs?: { id: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = ref<DepositOrder[]>([]);
|
const items = ref<DepositOrder[]>([]);
|
||||||
@@ -56,8 +37,6 @@ const hasMore = ref(true);
|
|||||||
const sentinel = ref<HTMLElement | null>(null);
|
const sentinel = ref<HTMLElement | null>(null);
|
||||||
let observer: IntersectionObserver | null = null;
|
let observer: IntersectionObserver | null = null;
|
||||||
|
|
||||||
const selectedOrder = ref<DepositOrder | null>(null);
|
|
||||||
|
|
||||||
async function fetchOrders(p = 1) {
|
async function fetchOrders(p = 1) {
|
||||||
if (loading.value) return;
|
if (loading.value) return;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
@@ -128,103 +107,31 @@ function goRecharge() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openDetail(order: DepositOrder) {
|
function openDetail(order: DepositOrder) {
|
||||||
selectedOrder.value = order;
|
router.push(`/wallet/recharge/history/${order.id}`);
|
||||||
}
|
|
||||||
|
|
||||||
function closeDetail() {
|
|
||||||
selectedOrder.value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
function normalizeText(value: string | null | undefined) {
|
||||||
return value?.trim() ?? '';
|
return value?.trim() ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Backend sets remark = rejectReason on reject; show one line only. */
|
function orderNoteLine(order: DepositOrder) {
|
||||||
function orderNote(order: DepositOrder): { label: string; text: string } | null {
|
|
||||||
const rejectReason = normalizeText(order.rejectReason);
|
const rejectReason = normalizeText(order.rejectReason);
|
||||||
const remark = normalizeText(order.remark);
|
const remark = normalizeText(order.remark);
|
||||||
|
|
||||||
if (order.status === 'REJECTED') {
|
if (order.status === 'REJECTED') {
|
||||||
const text = rejectReason || remark;
|
const text = rejectReason || remark;
|
||||||
if (!text) return null;
|
if (!text) return null;
|
||||||
return { label: t('recharge.reject_reason'), text };
|
return `${t('recharge.reject_reason')}: ${text}`;
|
||||||
}
|
|
||||||
|
|
||||||
if (remark) {
|
|
||||||
return { label: t('recharge.remark'), text: remark };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (remark) return `${t('recharge.remark')}: ${remark}`;
|
||||||
return null;
|
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) {
|
function formatOrderTime(iso: string) {
|
||||||
return new Date(iso).toLocaleString();
|
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) {
|
function auditStepCount(order: DepositOrder) {
|
||||||
return order.auditLogs?.length ?? 0;
|
return order.auditLogs?.length ?? 0;
|
||||||
}
|
}
|
||||||
@@ -268,6 +175,7 @@ function auditStepCount(order: DepositOrder) {
|
|||||||
<span :class="['status-badge', statusClass(order.status)]">{{ statusLabel(order.status) }}</span>
|
<span :class="['status-badge', statusClass(order.status)]">{{ statusLabel(order.status) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="order-body">
|
<div class="order-body">
|
||||||
|
<div class="order-no">{{ order.orderNo }}</div>
|
||||||
<div class="order-amount">{{ formatMoney(order.amount, locale) }}</div>
|
<div class="order-amount">{{ formatMoney(order.amount, locale) }}</div>
|
||||||
<div v-if="order.approvedAmount && order.approvedAmount !== order.amount" class="approved-amount">
|
<div v-if="order.approvedAmount && order.approvedAmount !== order.amount" class="approved-amount">
|
||||||
{{ t('recharge.credited') }}: {{ formatMoney(order.approvedAmount, locale) }}
|
{{ t('recharge.credited') }}: {{ formatMoney(order.approvedAmount, locale) }}
|
||||||
@@ -291,20 +199,12 @@ function auditStepCount(order: DepositOrder) {
|
|||||||
>
|
>
|
||||||
{{ orderNoteLine(order) }}
|
{{ orderNoteLine(order) }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="auditStepCount(order)" class="card-detail-hint">
|
<div class="card-detail-hint">
|
||||||
<span class="card-detail-summary">
|
<span v-if="auditStepCount(order)" class="card-detail-summary">
|
||||||
{{ t('recharge.audit_summary', { count: auditStepCount(order) }) }}
|
{{ t('recharge.audit_summary', { count: auditStepCount(order) }) }}
|
||||||
</span>
|
</span>
|
||||||
<span class="card-detail-link">{{ t('recharge.view_detail') }} ›</span>
|
<span class="card-detail-link">{{ t('recharge.view_detail') }} ›</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
v-else
|
|
||||||
type="button"
|
|
||||||
class="card-detail-link-only"
|
|
||||||
@click.stop="openDetail(order)"
|
|
||||||
>
|
|
||||||
{{ t('recharge.view_detail') }} ›
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -319,98 +219,6 @@ function auditStepCount(order: DepositOrder) {
|
|||||||
{{ t('common.no_more') }}
|
{{ t('common.no_more') }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<Teleport to="body">
|
|
||||||
<div v-if="selectedOrder" class="detail-overlay" @click.self="closeDetail">
|
|
||||||
<div class="detail-modal" role="dialog" aria-modal="true" :aria-label="t('recharge.audit_title')">
|
|
||||||
<button type="button" class="detail-close" :aria-label="t('common.close')" @click="closeDetail">✕</button>
|
|
||||||
|
|
||||||
<h3 class="detail-title">{{ t('recharge.audit_title') }}</h3>
|
|
||||||
|
|
||||||
<div class="detail-summary">
|
|
||||||
<div class="detail-summary-head">
|
|
||||||
<span class="method-badge" :class="selectedOrder.methodType === 'BANK' ? 'bank' : 'usdt'">
|
|
||||||
{{ selectedOrder.methodType }}
|
|
||||||
</span>
|
|
||||||
<span :class="['status-badge', statusClass(selectedOrder.status)]">
|
|
||||||
{{ statusLabel(selectedOrder.status) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-amount">{{ formatMoney(selectedOrder.amount, locale) }}</div>
|
|
||||||
<div
|
|
||||||
v-if="selectedOrder.approvedAmount && selectedOrder.approvedAmount !== selectedOrder.amount"
|
|
||||||
class="approved-amount"
|
|
||||||
>
|
|
||||||
{{ t('recharge.credited') }}: {{ formatMoney(selectedOrder.approvedAmount, locale) }}
|
|
||||||
</div>
|
|
||||||
<div class="detail-method-name">{{ selectedOrder.paymentMethodName || '-' }}</div>
|
|
||||||
<div class="detail-row">
|
|
||||||
<span class="detail-label">{{ t('recharge.apply_time') }}</span>
|
|
||||||
<span class="detail-value">{{ formatOrderTime(selectedOrder.createdAt) }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="selectedOrder.reviewedAt" class="detail-row">
|
|
||||||
<span class="detail-label">{{ t('recharge.review_time') }}</span>
|
|
||||||
<span class="detail-value">{{ formatOrderTime(selectedOrder.reviewedAt) }}</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="orderNoteLine(selectedOrder)"
|
|
||||||
:class="selectedOrder.status === 'REJECTED' ? 'reject-reason' : 'order-remark'"
|
|
||||||
>
|
|
||||||
{{ orderNoteLine(selectedOrder) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="selectedOrder.auditLogs?.length" class="detail-audit">
|
|
||||||
<h4 class="detail-audit-title">{{ t('recharge.audit_title') }}</h4>
|
|
||||||
<div class="audit-track">
|
|
||||||
<div
|
|
||||||
v-for="(entry, logIdx) in auditLogsForDisplay(selectedOrder)"
|
|
||||||
: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(selectedOrder).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="selectedOrder.status === 'REJECTED'"
|
|
||||||
type="button"
|
|
||||||
class="modal-reapply-btn btn-gold-outline"
|
|
||||||
@click.stop="reapply(selectedOrder)"
|
|
||||||
>
|
|
||||||
{{ t('recharge.reapply') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Teleport>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -435,9 +243,7 @@ function auditStepCount(order: DepositOrder) {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
.order-card:active {
|
.order-card:active { opacity: 0.92; }
|
||||||
opacity: 0.92;
|
|
||||||
}
|
|
||||||
.order-card::before {
|
.order-card::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -453,14 +259,9 @@ function auditStepCount(order: DepositOrder) {
|
|||||||
background: linear-gradient(90deg, transparent, rgba(245, 108, 108, 0.35), transparent);
|
background: linear-gradient(90deg, transparent, rgba(245, 108, 108, 0.35), transparent);
|
||||||
}
|
}
|
||||||
.order-card.rejected .order-amount {
|
.order-card.rejected .order-amount {
|
||||||
background: none;
|
|
||||||
-webkit-background-clip: unset;
|
|
||||||
background-clip: unset;
|
|
||||||
color: #bbb;
|
color: #bbb;
|
||||||
}
|
}
|
||||||
.order-card.rejected .info-label {
|
.order-card.rejected .info-label { color: #888; }
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
.order-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
|
.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 { 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.bank { background: rgba(30, 58, 95, 0.6); color: #66b1ff; }
|
||||||
@@ -469,15 +270,18 @@ function auditStepCount(order: DepositOrder) {
|
|||||||
.status-pending { color: #e6a23c; }
|
.status-pending { color: #e6a23c; }
|
||||||
.status-approved { color: #67c23a; }
|
.status-approved { color: #67c23a; }
|
||||||
.status-rejected { color: #f56c6c; }
|
.status-rejected { color: #f56c6c; }
|
||||||
.order-body { }
|
|
||||||
.order-amount {
|
.order-amount {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
background: linear-gradient(135deg, #f0d060, #d4a830);
|
color: var(--text);
|
||||||
-webkit-background-clip: text;
|
}
|
||||||
background-clip: text;
|
.order-no {
|
||||||
color: transparent;
|
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; }
|
.approved-amount { font-size: 12px; color: #67c23a; margin-bottom: 6px; font-weight: 600; }
|
||||||
.order-info-row { margin-bottom: 8px; }
|
.order-info-row { margin-bottom: 8px; }
|
||||||
@@ -520,39 +324,17 @@ function auditStepCount(order: DepositOrder) {
|
|||||||
}
|
}
|
||||||
.card-detail-summary {
|
.card-detail-summary {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: rgba(212, 175, 55, 0.65);
|
color: var(--text-muted);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.card-detail-link {
|
.card-detail-link {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--primary-light);
|
color: var(--text-muted);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.card-detail-link-only {
|
.sentinel { height: 1px; }
|
||||||
display: block;
|
.load-more-spinner { display: flex; justify-content: center; padding: 20px 0 8px; }
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.end-hint {
|
.end-hint {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -561,291 +343,4 @@ function auditStepCount(order: DepositOrder) {
|
|||||||
padding: 16px 0 4px;
|
padding: 16px 0 4px;
|
||||||
letter-spacing: 0.03em;
|
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;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -681,7 +681,7 @@ onMounted(fetchMethods);
|
|||||||
.success-state { text-align: center; padding: 40px 16px; }
|
.success-state { text-align: center; padding: 40px 16px; }
|
||||||
.success-icon { font-size: 40px; color: #67c23a; margin-bottom: 10px; }
|
.success-icon { font-size: 40px; color: #67c23a; margin-bottom: 10px; }
|
||||||
.success-state h3 { margin: 0 0 6px; font-size: 16px; }
|
.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; }
|
.success-hint { font-size: 12px; color: var(--text-muted); margin-bottom: 20px; }
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: linear-gradient(135deg, #f0d060, #d4a830);
|
background: linear-gradient(135deg, #f0d060, #d4a830);
|
||||||
|
|||||||
@@ -370,9 +370,9 @@ function goCashbackDetail() {
|
|||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid var(--border-gold-soft, rgba(212, 175, 55, 0.35));
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
background: rgba(212, 175, 55, 0.08);
|
background: rgba(255, 255, 255, 0.04);
|
||||||
color: var(--primary-light);
|
color: var(--text);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
12
apps/player/src/views/RechargeDetailView.vue
Normal file
12
apps/player/src/views/RechargeDetailView.vue
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useViewport } from '../composables/useViewport';
|
||||||
|
import MobileRechargeDetailView from './MobileRechargeDetailView.vue';
|
||||||
|
import DesktopRechargeDetailView from './desktop/DesktopRechargeDetailView.vue';
|
||||||
|
|
||||||
|
const { isDesktop } = useViewport();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DesktopRechargeDetailView v-if="isDesktop" />
|
||||||
|
<MobileRechargeDetailView v-else />
|
||||||
|
</template>
|
||||||
@@ -209,13 +209,16 @@ void loadParlayMatches(true);
|
|||||||
<section v-for="lg in leagueGroups" :key="lg.leagueName" class="league-section">
|
<section v-for="lg in leagueGroups" :key="lg.leagueName" class="league-section">
|
||||||
<h2 class="league-title">{{ lg.leagueName }}</h2>
|
<h2 class="league-title">{{ lg.leagueName }}</h2>
|
||||||
<div class="match-grid">
|
<div class="match-grid">
|
||||||
<MatchBetCard
|
<TransitionGroup name="card-list" appear tag="div" class="match-grid-inner">
|
||||||
v-for="m in lg.matches"
|
<MatchBetCard
|
||||||
:key="m.id"
|
v-for="(m, i) in lg.matches"
|
||||||
class="match-card-dense"
|
:key="m.id"
|
||||||
:match="m"
|
class="match-card-dense"
|
||||||
@bet="goMatch"
|
:style="{ '--i': i }"
|
||||||
/>
|
:match="m"
|
||||||
|
@bet="goMatch"
|
||||||
|
/>
|
||||||
|
</TransitionGroup>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
@@ -350,46 +353,86 @@ void loadParlayMatches(true);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.match-grid {
|
.match-grid {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-grid-inner {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||||
gap: 8px;
|
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;
|
padding: 8px 6px;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.match-grid :deep(.team-name) {
|
.match-grid-inner :deep(.team-name) {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.match-grid :deep(.team-flag) {
|
.match-grid-inner :deep(.team-flag) {
|
||||||
width: 56px;
|
width: 56px;
|
||||||
height: 38px;
|
height: 38px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.match-grid :deep(.team-flag.flag-logo) {
|
.match-grid-inner :deep(.team-flag.flag-logo) {
|
||||||
width: 44px;
|
width: 44px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.match-grid :deep(.kickoff) {
|
.match-grid-inner :deep(.kickoff) {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.match-grid :deep(.vs) {
|
.match-grid-inner :deep(.vs) {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.match-grid :deep(.bet-btn) {
|
.match-grid-inner :deep(.bet-btn) {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.match-grid :deep(.status-tag) {
|
.match-grid-inner :deep(.status-tag) {
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const { banners, hotMatches, upcomingMatches, loading, load, announcementItems }
|
|||||||
|
|
||||||
const slip = useBetSlipStore();
|
const slip = useBetSlipStore();
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const { openAt, visible: popoverVisible, pendingItem } = useDesktopBetPopover();
|
const { openAt, visible: popoverVisible, pendingItem, cancelClose, scheduleClose } = useDesktopBetPopover();
|
||||||
|
|
||||||
const activeMarketTypes = ref<Record<string, string>>({});
|
const activeMarketTypes = ref<Record<string, string>>({});
|
||||||
|
|
||||||
@@ -140,6 +140,37 @@ function handleOddsClick(match: PlayerHomeMatch, market: any, selId: string, eve
|
|||||||
function getSelLabel(sel: any, marketType: string, lineValue?: string | number | null) {
|
function getSelLabel(sel: any, marketType: string, lineValue?: string | number | null) {
|
||||||
return resolveSelectionLabel(t, sel.selectionCode || '', sel.selectionName || '', { lineValue });
|
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 '';
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -184,7 +215,7 @@ function getSelLabel(sel: any, marketType: string, lineValue?: string | number |
|
|||||||
<span class="featured-time">{{ formatKickoff(featuredMatch.startTime) }}</span>
|
<span class="featured-time">{{ formatKickoff(featuredMatch.startTime) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="featured-teams" @click="goMatch(featuredMatch.id)">
|
<div class="featured-teams" :class="getHoveredSideForMatch(featuredMatch)" @click="goMatch(featuredMatch.id)">
|
||||||
<div class="featured-team">
|
<div class="featured-team">
|
||||||
<TeamEmblem
|
<TeamEmblem
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -271,7 +302,7 @@ function getSelLabel(sel: any, marketType: string, lineValue?: string | number |
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hover versus layout (visible on hover) -->
|
<!-- Hover versus layout (visible on hover) -->
|
||||||
<div class="hover-versus-layout">
|
<div class="hover-versus-layout" :class="getHoveredSideForMatch(match)">
|
||||||
<div class="hover-team home-slide">
|
<div class="hover-team home-slide">
|
||||||
<TeamEmblem
|
<TeamEmblem
|
||||||
size="md"
|
size="md"
|
||||||
@@ -379,7 +410,7 @@ function getSelLabel(sel: any, marketType: string, lineValue?: string | number |
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hover versus layout (visible on hover) -->
|
<!-- Hover versus layout (visible on hover) -->
|
||||||
<div class="hover-versus-layout">
|
<div class="hover-versus-layout" :class="getHoveredSideForMatch(match)">
|
||||||
<div class="hover-team home-slide">
|
<div class="hover-team home-slide">
|
||||||
<TeamEmblem
|
<TeamEmblem
|
||||||
size="md"
|
size="md"
|
||||||
@@ -466,12 +497,12 @@ function getSelLabel(sel: any, marketType: string, lineValue?: string | number |
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 悬浮预览卡 -->
|
<!-- 悬浮预览卡 -->
|
||||||
<div class="sri-preview-card" @click.stop>
|
<div class="sri-preview-card" @click.stop @mouseenter="cancelClose" @mouseleave="scheduleClose()">
|
||||||
<div class="sri-preview-top">
|
<div class="sri-preview-top">
|
||||||
<span class="sri-preview-league">{{ m.leagueName }}</span>
|
<span class="sri-preview-league">{{ m.leagueName }}</span>
|
||||||
<span class="sri-preview-time">{{ formatKickoff(m.startTime) }}</span>
|
<span class="sri-preview-time">{{ formatKickoff(m.startTime) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="sri-preview-vs">
|
<div class="sri-preview-vs" :class="getHoveredSideForMatch(m)">
|
||||||
<div class="sri-preview-team home-team">
|
<div class="sri-preview-team home-team">
|
||||||
<TeamEmblem size="md" :team-code="m.homeTeamCode" :team-name="m.homeTeamName" :logo-url="m.homeTeamLogoUrl" />
|
<TeamEmblem size="md" :team-code="m.homeTeamCode" :team-name="m.homeTeamName" :logo-url="m.homeTeamLogoUrl" />
|
||||||
<span>{{ m.homeTeamName }}</span>
|
<span>{{ m.homeTeamName }}</span>
|
||||||
@@ -1275,10 +1306,11 @@ function getSelLabel(sel: any, marketType: string, lineValue?: string | number |
|
|||||||
/* --- 8. 推荐列表 item 左侧金点指示 hover --- */
|
/* --- 8. 推荐列表 item 左侧金点指示 hover --- */
|
||||||
.side-recommend-item {
|
.side-recommend-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: background 0.15s, transform 0.15s;
|
transition: background 0.18s, transform 0.18s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-recommend-item:hover {
|
.side-recommend-item:hover {
|
||||||
|
background: rgba(212, 175, 55, 0.06);
|
||||||
transform: translateX(3px);
|
transform: translateX(3px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1453,4 +1485,96 @@ function getSelLabel(sel: any, marketType: string, lineValue?: string | number |
|
|||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: var(--text-muted);
|
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);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -299,6 +299,37 @@ function toggleSelection(sel: Selection, market: Market, event?: MouseEvent) {
|
|||||||
if (!item || !event) return;
|
if (!item || !event) return;
|
||||||
openAt(event, item);
|
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 '';
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -318,7 +349,7 @@ function toggleSelection(sel: Selection, market: Market, event?: MouseEvent) {
|
|||||||
|
|
||||||
<template v-else-if="match">
|
<template v-else-if="match">
|
||||||
<!-- High Density Score Band -->
|
<!-- High Density Score Band -->
|
||||||
<div class="match-score-band" :class="{ 'phase-settled': matchPhase === 'settled' }">
|
<div class="match-score-band" :class="[{ 'phase-settled': matchPhase === 'settled' }, getHoveredSide()]">
|
||||||
<div class="team-side home">
|
<div class="team-side home">
|
||||||
<TeamEmblem size="xl" :team-code="match.homeTeamCode" :team-name="match.homeTeamName" :logo-url="match.homeTeamLogoUrl" />
|
<TeamEmblem size="xl" :team-code="match.homeTeamCode" :team-name="match.homeTeamName" :logo-url="match.homeTeamLogoUrl" />
|
||||||
<span class="name">{{ match.homeTeamName }}</span>
|
<span class="name">{{ match.homeTeamName }}</span>
|
||||||
@@ -503,10 +534,14 @@ function toggleSelection(sel: Selection, market: Market, event?: MouseEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.match-score-band {
|
.match-score-band {
|
||||||
background: linear-gradient(180deg, #181818 0%, #111 100%);
|
background:
|
||||||
border: 1px solid var(--border);
|
linear-gradient(135deg, rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.04) 50%, rgba(15, 10, 0, 0.1)),
|
||||||
border-radius: 4px;
|
url('../../assets/images/card-bg.webp') center/cover no-repeat;
|
||||||
padding: 10px 16px;
|
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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -768,4 +803,34 @@ function toggleSelection(sel: Selection, market: Market, event?: MouseEvent) {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
white-space: nowrap;
|
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);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -110,6 +110,13 @@ async function onAvatarSelect(key: string | null) {
|
|||||||
<main class="account-main">
|
<main class="account-main">
|
||||||
<div class="edit-layout">
|
<div class="edit-layout">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
|
<button type="button" class="back-btn" @click="router.back()">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<line x1="19" y1="12" x2="5" y2="12"></line>
|
||||||
|
<polyline points="12 19 5 12 12 5"></polyline>
|
||||||
|
</svg>
|
||||||
|
<span>{{ t('common.back') || '返回' }}</span>
|
||||||
|
</button>
|
||||||
<h1 class="page-title">{{ t('profile.edit_title') }}</h1>
|
<h1 class="page-title">{{ t('profile.edit_title') }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -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%; }
|
.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; }
|
.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; }
|
.page-title { font-size: 18px; font-weight: 800; color: var(--primary-light); margin: 0; }
|
||||||
|
|
||||||
.edit-layout {
|
.edit-layout {
|
||||||
|
|||||||
429
apps/player/src/views/desktop/DesktopRechargeDetailView.vue
Normal file
429
apps/player/src/views/desktop/DesktopRechargeDetailView.vue
Normal file
@@ -0,0 +1,429 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import api from '../../api';
|
||||||
|
import GoldSpinner from '../../components/GoldSpinner.vue';
|
||||||
|
import { formatMoney } from '../../utils/localeDisplay';
|
||||||
|
import {
|
||||||
|
auditActionTone,
|
||||||
|
auditActorSecondary,
|
||||||
|
formatDepositAuditRemark,
|
||||||
|
shouldShowAuditRejectInTimeline,
|
||||||
|
} from '../../utils/depositAuditDisplay';
|
||||||
|
|
||||||
|
const { t, locale } = useI18n();
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
interface DepositAuditLog {
|
||||||
|
id: string;
|
||||||
|
action: string;
|
||||||
|
actorType: string;
|
||||||
|
statusBefore: string | null;
|
||||||
|
statusAfter: string;
|
||||||
|
amount: string | null;
|
||||||
|
approvedAmount: string | null;
|
||||||
|
remark: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RechargeDetail = {
|
||||||
|
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<RechargeDetail | null>(null);
|
||||||
|
const loading = ref(true);
|
||||||
|
const error = ref(false);
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
if (window.history.state && window.history.state.back) {
|
||||||
|
router.back();
|
||||||
|
} else {
|
||||||
|
router.push('/wallet/recharge/history');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDetail() {
|
||||||
|
const id = route.params.id as string;
|
||||||
|
if (!id) return;
|
||||||
|
loading.value = true;
|
||||||
|
error.value = false;
|
||||||
|
try {
|
||||||
|
const { data } = await api.get(`/player/deposit-orders/${id}`);
|
||||||
|
detail.value = data.data ?? null;
|
||||||
|
} catch {
|
||||||
|
error.value = true;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadDetail);
|
||||||
|
|
||||||
|
function statusClass(status: string) {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
APPROVED: 'status-won',
|
||||||
|
REJECTED: 'status-lost',
|
||||||
|
PENDING: 'status-pending',
|
||||||
|
};
|
||||||
|
return map[status?.toUpperCase()] ?? 'status-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) {
|
||||||
|
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: 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 };
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function orderNoteLine(order: RechargeDetail) {
|
||||||
|
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 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>
|
||||||
|
<div class="desktop-account-page">
|
||||||
|
<main class="account-main">
|
||||||
|
<div class="page-header">
|
||||||
|
<button type="button" class="back-btn" @click="goBack">
|
||||||
|
‹ {{ t('common.back') || '返回' }}
|
||||||
|
</button>
|
||||||
|
<h1 class="page-title">{{ t('recharge.order_detail') }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading" class="loading-state">
|
||||||
|
<GoldSpinner :size="36" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="error" class="error-state">
|
||||||
|
<p>{{ t('common.load_failed') }}</p>
|
||||||
|
<button type="button" class="retry-btn" @click="loadDetail">{{ t('common.retry') }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="detail" class="detail-layout">
|
||||||
|
<div class="detail-card header-card">
|
||||||
|
<div class="order-no">{{ detail.orderNo }}</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('wallet.amount') }}</span>
|
||||||
|
<span class="amount-val">{{ formatMoney(detail.amount, locale) }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="detail.approvedAmount && detail.approvedAmount !== detail.amount"
|
||||||
|
class="amount-item"
|
||||||
|
>
|
||||||
|
<span class="amount-label">{{ t('recharge.credited') }}</span>
|
||||||
|
<span class="amount-val">{{ formatMoney(detail.approvedAmount, locale) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="amount-item">
|
||||||
|
<span class="amount-label">{{ t('recharge.method') }}</span>
|
||||||
|
<span class="amount-val method">{{ methodLabel(detail) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="amount-item">
|
||||||
|
<span class="amount-label">{{ t('recharge.apply_time') }}</span>
|
||||||
|
<span class="amount-val date">{{ new Date(detail.createdAt).toLocaleString() }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="detail.reviewedAt" class="amount-item">
|
||||||
|
<span class="amount-label">{{ t('recharge.review_time') }}</span>
|
||||||
|
<span class="amount-val date">{{ new Date(detail.reviewedAt).toLocaleString() }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="orderNoteLine(detail)"
|
||||||
|
:class="detail.status === 'REJECTED' ? 'reject-reason' : 'order-remark'"
|
||||||
|
>
|
||||||
|
{{ orderNoteLine(detail) }}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="detail.status === 'REJECTED'"
|
||||||
|
type="button"
|
||||||
|
class="reapply-btn"
|
||||||
|
@click="reapply(detail)"
|
||||||
|
>
|
||||||
|
{{ t('recharge.reapply') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="detail.auditLogs?.length" class="detail-card">
|
||||||
|
<div class="card-title">{{ t('recharge.audit_title') }}</div>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="empty-state">{{ t('common.not_found') || '未找到记录' }}</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.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 { display: flex; align-items: center; gap: 16px; margin-bottom: 24px; }
|
||||||
|
.back-btn { background: none; border: none; color: var(--text-muted); font-size: 14px; font-weight: 700; cursor: pointer; padding: 0; }
|
||||||
|
.back-btn:hover { color: var(--text); }
|
||||||
|
.page-title { font-size: 18px; font-weight: 800; color: var(--text); margin: 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); }
|
||||||
|
.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; }
|
||||||
|
.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-card {
|
||||||
|
background: var(--desktop-sidebar-bg);
|
||||||
|
border: 1px solid var(--desktop-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-card { display: flex; flex-direction: column; gap: 12px; }
|
||||||
|
|
||||||
|
.order-no {
|
||||||
|
font-family: 'SF Mono', 'Consolas', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-muted);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge { display: inline-block; padding: 4px 14px; border-radius: 999px; font-size: 12px; font-weight: 700; align-self: flex-start; }
|
||||||
|
.status-won { background: rgba(52,199,89,0.12); color: #4cd964; }
|
||||||
|
.status-lost { background: rgba(255,69,58,0.12); color: #ff453a; }
|
||||||
|
.status-pending { background: rgba(212,175,55,0.12); color: var(--primary-light); }
|
||||||
|
.status-default { background: rgba(100,100,100,0.1); color: #888; }
|
||||||
|
|
||||||
|
.amount-row { display: flex; gap: 32px; padding-top: 4px; flex-wrap: wrap; }
|
||||||
|
.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; }
|
||||||
|
.amount-val { font-size: 18px; font-weight: 800; color: var(--text); font-variant-numeric: tabular-nums; }
|
||||||
|
.amount-val.method { font-size: 14px; font-weight: 700; }
|
||||||
|
.amount-val.date { font-size: 13px; color: var(--text-muted); font-weight: 600; }
|
||||||
|
|
||||||
|
.reject-reason {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255, 69, 58, 0.22);
|
||||||
|
font-size: 13px;
|
||||||
|
color: #ff8a82;
|
||||||
|
line-height: 1.45;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-remark {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reapply-btn {
|
||||||
|
align-self: flex-start;
|
||||||
|
padding: 9px 24px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reapply-btn:hover { opacity: 0.88; }
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-track { display: flex; flex-direction: column; }
|
||||||
|
|
||||||
|
.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(255, 255, 255, 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; flex-shrink: 0; }
|
||||||
|
.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: #7eb87a; }
|
||||||
|
.audit-step-note { margin: 4px 0 0; font-size: 12px; color: var(--text-muted); line-height: 1.45; word-break: break-word; }
|
||||||
|
|
||||||
|
.audit-step-box {
|
||||||
|
margin-top: 4px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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; }
|
||||||
|
</style>
|
||||||
@@ -20,7 +20,6 @@ type RechargeOrder = {
|
|||||||
methodType?: string;
|
methodType?: string;
|
||||||
bankName?: string | null;
|
bankName?: string | null;
|
||||||
paymentMethodName?: string | null;
|
paymentMethodName?: string | null;
|
||||||
approvedAmount?: string | null;
|
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -106,7 +105,11 @@ function statusLabel(status: string) {
|
|||||||
</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 v-for="order in items" :key="order.id" class="hover-row">
|
||||||
<td class="order-no">{{ order.orderNo || order.id }}</td>
|
<td>
|
||||||
|
<RouterLink :to="`/wallet/recharge/history/${order.id}`" class="link-cell">
|
||||||
|
{{ order.orderNo || order.id }}
|
||||||
|
</RouterLink>
|
||||||
|
</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>
|
||||||
@@ -136,10 +139,3 @@ function statusLabel(status: string) {
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.order-no {
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--primary-light);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import imageCompression from 'browser-image-compression';
|
|||||||
import api from '../../api';
|
import api from '../../api';
|
||||||
import GoldSpinner from '../../components/GoldSpinner.vue';
|
import GoldSpinner from '../../components/GoldSpinner.vue';
|
||||||
import { useDepositNotifications } from '../../composables/useDepositNotifications';
|
import { useDepositNotifications } from '../../composables/useDepositNotifications';
|
||||||
|
import { useAppToast } from '../../composables/useAppToast';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { trackPendingOrder } = useDepositNotifications();
|
const { trackPendingOrder } = useDepositNotifications();
|
||||||
|
const { showToast } = useAppToast();
|
||||||
|
|
||||||
const reapplyOrderId = computed(() => {
|
const reapplyOrderId = computed(() => {
|
||||||
const id = route.query.orderId;
|
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 usdtMethods = computed(() => methods.value.filter((m) => m.methodType === 'USDT'));
|
||||||
const currentMethods = computed(() => methodType.value === 'BANK' ? bankMethods.value : usdtMethods.value);
|
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() {
|
function applyReapplyQuery() {
|
||||||
const type = route.query.methodType;
|
const type = route.query.methodType;
|
||||||
if (type === 'BANK' || type === 'USDT') methodType.value = type;
|
if (type === 'BANK' || type === 'USDT') methodType.value = type;
|
||||||
@@ -107,8 +115,13 @@ async function submit() {
|
|||||||
fd.append('paymentMethodId', selectedMethod.value.id);
|
fd.append('paymentMethodId', selectedMethod.value.id);
|
||||||
fd.append('amount', amount.value);
|
fd.append('amount', amount.value);
|
||||||
fd.append('screenshot', screenshotFile.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 ?? '';
|
orderNo.value = data.data?.orderNo ?? '';
|
||||||
if (data.data?.id) trackPendingOrder(String(data.data.id));
|
if (data.data?.id) trackPendingOrder(String(data.data.id));
|
||||||
success.value = true;
|
success.value = true;
|
||||||
@@ -128,6 +141,13 @@ onActivated(fetchMethods);
|
|||||||
<main class="account-main">
|
<main class="account-main">
|
||||||
<div class="recharge-layout">
|
<div class="recharge-layout">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
|
<button type="button" class="back-btn" @click="router.back()">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<line x1="19" y1="12" x2="5" y2="12"></line>
|
||||||
|
<polyline points="12 19 5 12 12 5"></polyline>
|
||||||
|
</svg>
|
||||||
|
<span>{{ t('common.back') || '返回' }}</span>
|
||||||
|
</button>
|
||||||
<h1 class="page-title">{{ t('recharge.title') }}</h1>
|
<h1 class="page-title">{{ t('recharge.title') }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -136,16 +156,30 @@ onActivated(fetchMethods);
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="success" class="success-card">
|
<div v-else-if="success" class="success-card">
|
||||||
<svg width="52" height="52" viewBox="0 0 24 24" fill="none" stroke="var(--primary)" stroke-width="1.6"><circle cx="12" cy="12" r="9"/><path d="m8 12 3 3 5-5"/></svg>
|
<div class="success-icon-wrap">
|
||||||
<h2>{{ t('recharge.success_title') }}</h2>
|
<svg class="check-svg" viewBox="0 0 52 52" xmlns="http://www.w3.org/2000/svg">
|
||||||
<p class="order-no">{{ t('recharge.order_no') }}: {{ orderNo }}</p>
|
<circle class="check-circle" cx="26" cy="26" r="24" fill="none" stroke="#D4AF37" stroke-width="3" />
|
||||||
<p class="success-hint">{{ t('recharge.success_hint') }}</p>
|
<path class="check-mark" d="M14 27l7 7 16-16" fill="none" stroke="#D4AF37" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2>{{ t('recharge.success_title') || '充值申请已提交' }}</h2>
|
||||||
|
<div class="success-details">
|
||||||
|
<div class="success-detail-row">
|
||||||
|
<span class="s-label">{{ t('recharge.order_no') || '订单号' }}</span>
|
||||||
|
<span class="s-value mono muted">{{ orderNo }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="success-detail-row">
|
||||||
|
<span class="s-label">{{ t('recharge.amount') || '充值金额' }}</span>
|
||||||
|
<span class="s-value gold">¥ {{ parseFloat(amount).toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="success-hint">{{ t('recharge.success_hint') || '您的充值申请已提交,客服将尽快为您处理。' }}</p>
|
||||||
<button type="button" class="action-btn" @click="router.push('/wallet/recharge/history')">
|
<button type="button" class="action-btn" @click="router.push('/wallet/recharge/history')">
|
||||||
{{ t('recharge.history_title') }}
|
{{ t('recharge.history_title') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-else>
|
<div v-else class="recharge-box">
|
||||||
<!-- Method type tabs -->
|
<!-- Method type tabs -->
|
||||||
<div class="type-tabs">
|
<div class="type-tabs">
|
||||||
<button type="button" class="type-tab" :class="{ active: methodType === 'BANK' }" @click="switchType('BANK')">
|
<button type="button" class="type-tab" :class="{ active: methodType === 'BANK' }" @click="switchType('BANK')">
|
||||||
@@ -157,106 +191,139 @@ onActivated(fetchMethods);
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="recharge-cols">
|
<div class="recharge-cols">
|
||||||
<!-- Method list -->
|
<!-- Method list -->
|
||||||
<div class="method-list">
|
<div class="method-list">
|
||||||
<p class="section-label">{{ t('recharge.choose_method') || '选择收款账户' }}</p>
|
<p class="section-label">{{ t('recharge.choose_method') || '选择收款账户' }}</p>
|
||||||
<div v-if="currentMethods.length === 0" class="no-methods">
|
<div v-if="currentMethods.length === 0" class="no-methods">
|
||||||
{{ t('recharge.no_methods') || '暂无可用收款账户' }}
|
{{ t('recharge.no_methods') || '暂无可用收款账户' }}
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
v-for="m in currentMethods"
|
|
||||||
:key="m.id"
|
|
||||||
type="button"
|
|
||||||
class="method-card"
|
|
||||||
:class="{ selected: selectedMethod?.id === m.id }"
|
|
||||||
@click="selectMethod(m)"
|
|
||||||
>
|
|
||||||
<span class="method-name">{{ m.displayName || m.bankName || 'USDT' }}</span>
|
|
||||||
<span class="method-account">{{ m.accountNumber || m.usdtAddress || '' }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Form -->
|
|
||||||
<div v-if="selectedMethod" class="recharge-form">
|
|
||||||
<!-- Bank details -->
|
|
||||||
<div v-if="methodType === 'BANK'" class="bank-detail-card">
|
|
||||||
<div class="detail-row">
|
|
||||||
<span class="detail-label">{{ t('recharge.bank_name') || '银行名称' }}</span>
|
|
||||||
<span class="detail-val">{{ selectedMethod.bankName }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-row">
|
<button
|
||||||
<span class="detail-label">{{ t('recharge.account_holder') || '持卡人' }}</span>
|
v-for="m in currentMethods"
|
||||||
<span class="detail-val">{{ selectedMethod.accountHolder }}</span>
|
:key="m.id"
|
||||||
|
type="button"
|
||||||
|
class="method-card"
|
||||||
|
:class="{ selected: selectedMethod?.id === m.id }"
|
||||||
|
@click="selectMethod(m)"
|
||||||
|
>
|
||||||
|
<div class="method-card-header">
|
||||||
|
<div class="method-icon-wrap">
|
||||||
|
<svg v-if="m.methodType === 'BANK'" viewBox="0 0 24 24" class="method-svg-icon"><path fill="currentColor" d="M22 6H2c-1.1 0-1.99.9-1.99 2L0 18c0 1.1.9 2 2 2h20c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 12H2v-6h20v6zm0-8H2V8h20v2z"/></svg>
|
||||||
|
<svg v-else viewBox="0 0 24 24" class="method-svg-icon"><path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1.5 13H10.5v-1H9.06c-.53 0-.96-.43-.96-.96V10.5c0-.53.43-.96.96-.96h3.88c.53 0 .96.43.96.96v2.54c0 .53-.43.96-.96.96H10.5v1h3v-1.5z"/></svg>
|
||||||
|
</div>
|
||||||
|
<span class="method-name">{{ m.displayName || m.bankName || 'USDT' }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="method-account">{{ m.accountNumber || m.usdtAddress || '' }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form -->
|
||||||
|
<div v-if="selectedMethod" class="recharge-form">
|
||||||
|
<!-- Bank details -->
|
||||||
|
<div v-if="methodType === 'BANK'" class="bank-detail-card">
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label">{{ t('recharge.bank_name') || '银行名称' }}</span>
|
||||||
|
<span class="detail-val">{{ selectedMethod.bankName }}</span>
|
||||||
|
<button type="button" class="copy-btn" @click="copyText(selectedMethod.bankName || '')">
|
||||||
|
{{ t('common.copy') || '复制' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label">{{ t('recharge.account_holder') || '持卡人' }}</span>
|
||||||
|
<span class="detail-val">{{ selectedMethod.accountHolder }}</span>
|
||||||
|
<button type="button" class="copy-btn" @click="copyText(selectedMethod.accountHolder || '')">
|
||||||
|
{{ t('common.copy') || '复制' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label">{{ t('recharge.account_number') || '账号' }}</span>
|
||||||
|
<span class="detail-val mono">{{ selectedMethod.accountNumber }}</span>
|
||||||
|
<button type="button" class="copy-btn" @click="copyText(selectedMethod.accountNumber || '')">
|
||||||
|
{{ t('common.copy') || '复制' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-row">
|
|
||||||
<span class="detail-label">{{ t('recharge.account_number') || '账号' }}</span>
|
<!-- USDT QR -->
|
||||||
<span class="detail-val mono">{{ selectedMethod.accountNumber }}</span>
|
<div v-if="methodType === 'USDT' && selectedMethod.qrCodeUrl" class="qr-section">
|
||||||
|
<div class="qr-wrap">
|
||||||
|
<img :src="selectedMethod.qrCodeUrl" alt="USDT QR" class="qr-img" />
|
||||||
|
</div>
|
||||||
|
<p class="qr-hint">{{ t('recharge.scan_to_pay') || '请扫描二维码完成支付' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div v-if="methodType === 'USDT'" class="detail-row">
|
||||||
|
<span class="detail-label">{{ t('recharge.usdt_address') || 'USDT地址' }}</span>
|
||||||
<!-- USDT QR -->
|
<span class="detail-val mono break-all">{{ selectedMethod.usdtAddress }}</span>
|
||||||
<div v-if="methodType === 'USDT' && selectedMethod.qrCodeUrl" class="qr-wrap">
|
<button type="button" class="copy-btn" @click="copyText(selectedMethod.usdtAddress || '')">
|
||||||
<img :src="selectedMethod.qrCodeUrl" alt="USDT QR" class="qr-img" />
|
{{ t('common.copy') || '复制' }}
|
||||||
</div>
|
</button>
|
||||||
<div v-if="methodType === 'USDT'" class="detail-row">
|
|
||||||
<span class="detail-label">{{ t('recharge.usdt_address') || 'USDT地址' }}</span>
|
|
||||||
<span class="detail-val mono break-all">{{ selectedMethod.usdtAddress }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Amount input -->
|
|
||||||
<div class="field">
|
|
||||||
<label class="field-label">{{ t('recharge.amount') || '充值金额' }}</label>
|
|
||||||
<input
|
|
||||||
v-model="amount"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
class="field-input"
|
|
||||||
:placeholder="t('recharge.amount_placeholder') || '请输入金额'"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Screenshot upload -->
|
|
||||||
<div class="field">
|
|
||||||
<label class="field-label">{{ t('recharge.screenshot') || '上传凭证截图' }}</label>
|
|
||||||
<div v-if="screenshotPreview" class="preview-wrap">
|
|
||||||
<img :src="screenshotPreview" alt="preview" class="preview-img" />
|
|
||||||
<button type="button" class="clear-btn" @click="clearFile">✕</button>
|
|
||||||
</div>
|
</div>
|
||||||
<label v-else class="upload-zone">
|
|
||||||
<input type="file" accept="image/*" class="hidden-input" @change="handleFileChange" />
|
|
||||||
<GoldSpinner v-if="compressing" :size="24" />
|
|
||||||
<template v-else>
|
|
||||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.5"><path d="M4 17v1a2 2 0 002 2h12a2 2 0 002-2v-1M12 3v12M8 7l4-4 4 4"/></svg>
|
|
||||||
<span class="upload-hint">{{ t('recharge.click_to_upload') || '点击上传截图' }}</span>
|
|
||||||
</template>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<!-- Amount input -->
|
||||||
type="button"
|
<div class="field">
|
||||||
class="submit-btn"
|
<label class="field-label">{{ t('recharge.amount') || '充值金额' }}</label>
|
||||||
:disabled="submitting || !amount || !screenshotFile"
|
<div class="amount-input-wrap">
|
||||||
@click="submit"
|
<span class="currency-symbol">¥</span>
|
||||||
>
|
<input
|
||||||
<GoldSpinner v-if="submitting" :size="18" />
|
v-model="amount"
|
||||||
<span v-else>{{ t('recharge.submit') || '提交充值' }}</span>
|
type="number"
|
||||||
</button>
|
min="0"
|
||||||
|
class="field-input"
|
||||||
|
:placeholder="t('recharge.amount_placeholder') || '请输入金额'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="amount-presets">
|
||||||
|
<button
|
||||||
|
v-for="val in [100, 500, 1000, 5000, 10000]"
|
||||||
|
:key="val"
|
||||||
|
type="button"
|
||||||
|
class="preset-btn"
|
||||||
|
@click="amount = String(val)"
|
||||||
|
>
|
||||||
|
+{{ val }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Screenshot upload -->
|
||||||
|
<div class="field">
|
||||||
|
<label class="field-label">{{ t('recharge.screenshot') || '上传凭证截图' }}</label>
|
||||||
|
<div v-if="screenshotPreview" class="preview-wrap">
|
||||||
|
<img :src="screenshotPreview" alt="preview" class="preview-img" />
|
||||||
|
<button type="button" class="clear-btn" @click="clearFile">✕</button>
|
||||||
|
</div>
|
||||||
|
<label v-else class="upload-zone">
|
||||||
|
<input type="file" accept="image/*" class="hidden-input" @change="handleFileChange" />
|
||||||
|
<GoldSpinner v-if="compressing" :size="24" />
|
||||||
|
<template v-else>
|
||||||
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.5"><path d="M4 17v1a2 2 0 002 2h12a2 2 0 002-2v-1M12 3v12M8 7l4-4 4 4"/></svg>
|
||||||
|
<span class="upload-hint">{{ t('recharge.click_to_upload') || '点击上传截图' }}</span>
|
||||||
|
</template>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="submit-btn"
|
||||||
|
:disabled="submitting || !amount || !screenshotFile"
|
||||||
|
@click="submit"
|
||||||
|
>
|
||||||
|
<GoldSpinner v-if="submitting" :size="18" />
|
||||||
|
<span v-else>{{ t('recharge.submit') || '提交充值' }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.desktop-account-page { display: flex; flex-direction: column; min-height: 0; flex: 1; width: 100%; }
|
.desktop-account-page {
|
||||||
|
|
||||||
.recharge-layout {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
max-width: 800px;
|
min-height: 0;
|
||||||
margin: 0 auto;
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,12 +334,52 @@ onActivated(fetchMethods);
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.recharge-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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 {
|
.page-title {
|
||||||
font-size: 18px;
|
font-size: 20px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: var(--primary-light);
|
color: var(--primary-light);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -286,40 +393,94 @@ onActivated(fetchMethods);
|
|||||||
padding: 80px 0;
|
padding: 80px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Success Card */
|
||||||
.success-card {
|
.success-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 14px;
|
gap: 16px;
|
||||||
padding: 60px 20px;
|
padding: 48px 24px;
|
||||||
|
background: rgba(25, 25, 25, 0.65);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid rgba(212, 175, 55, 0.12);
|
||||||
|
border-radius: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
max-width: 480px;
|
||||||
|
margin: 40px auto;
|
||||||
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.success-card h2 {
|
.success-icon-wrap {
|
||||||
font-size: 20px;
|
width: 72px;
|
||||||
font-weight: 800;
|
height: 72px;
|
||||||
color: var(--primary-light);
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-no {
|
.check-svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-circle {
|
||||||
|
stroke-dasharray: 151;
|
||||||
|
stroke-dashoffset: 151;
|
||||||
|
animation: circle-draw 0.5s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-mark {
|
||||||
|
stroke-dasharray: 42;
|
||||||
|
stroke-dashoffset: 42;
|
||||||
|
animation: check-draw 0.35s 0.35s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-details {
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-detail-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-label {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin: 0;
|
}
|
||||||
|
|
||||||
|
.s-value {
|
||||||
|
color: var(--text);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.success-hint {
|
.s-value.muted {
|
||||||
font-size: 13px;
|
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-value.gold {
|
||||||
|
color: var(--primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
line-height: 1.5;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn {
|
.action-btn {
|
||||||
padding: 9px 24px;
|
padding: 10px 28px;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
background: var(--primary);
|
background: linear-gradient(180deg, #f0d875 0%, #d4af37 100%);
|
||||||
color: #111;
|
color: #3d2800;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -327,41 +488,66 @@ onActivated(fetchMethods);
|
|||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
box-shadow: 0 4px 12px rgba(212, 175, 55, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn:hover { opacity: 0.88; }
|
.action-btn:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
box-shadow: 0 4px 16px rgba(212, 175, 55, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Recharge Box Card */
|
||||||
|
.recharge-box {
|
||||||
|
background: rgba(25, 25, 25, 0.65);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid rgba(212, 175, 55, 0.12);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Segment Tabs */
|
||||||
.type-tabs {
|
.type-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
background: rgba(0, 0, 0, 0.35);
|
||||||
margin-bottom: 20px;
|
border-radius: 8px;
|
||||||
|
padding: 4px;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
max-width: 320px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.type-tab {
|
.type-tab {
|
||||||
padding: 6px 20px;
|
flex: 1;
|
||||||
|
padding: 8px 16px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: 1px solid var(--desktop-border);
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.25s ease;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-tab:hover {
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.type-tab.active {
|
.type-tab.active {
|
||||||
border-color: var(--border-gold-soft);
|
color: #3d2800;
|
||||||
color: var(--primary-light);
|
background: linear-gradient(180deg, #f0d875 0%, #d4af37 100%);
|
||||||
background: rgba(212,175,55,0.08);
|
box-shadow: 0 2px 8px rgba(212, 175, 55, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Columns */
|
||||||
.recharge-cols {
|
.recharge-cols {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 240px 1fr;
|
grid-template-columns: 240px 1fr;
|
||||||
gap: 24px;
|
gap: 28px;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-label {
|
.section-label {
|
||||||
@@ -370,7 +556,7 @@ onActivated(fetchMethods);
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.06em;
|
||||||
margin: 0 0 10px;
|
margin: 0 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-methods {
|
.no-methods {
|
||||||
@@ -387,35 +573,71 @@ onActivated(fetchMethods);
|
|||||||
.method-card {
|
.method-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 12px 16px;
|
gap: 8px;
|
||||||
|
padding: 14px 16px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid var(--desktop-border);
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
background: var(--bg-card);
|
background: rgba(255, 255, 255, 0.02);
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
transition: all 0.2s;
|
transition: all 0.25s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.method-card:hover { border-color: rgba(255, 255, 255, 0.15); }
|
.method-card:hover {
|
||||||
|
border-color: rgba(212, 175, 55, 0.3);
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
.method-card.selected {
|
.method-card.selected {
|
||||||
border-color: var(--border-gold-soft);
|
border-color: var(--border-gold-soft);
|
||||||
background: rgba(212,175,55,0.06);
|
background: rgba(212, 175, 55, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-icon-wrap {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(212, 175, 55, 0.1);
|
||||||
|
color: var(--primary-light);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-svg-icon {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-card.selected .method-icon-wrap {
|
||||||
|
background: var(--primary);
|
||||||
|
color: #3d2800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.method-name {
|
.method-name {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: var(--text);
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.method-account {
|
.method-account {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Form & Detail Card */
|
||||||
.recharge-form {
|
.recharge-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -423,57 +645,96 @@ onActivated(fetchMethods);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bank-detail-card {
|
.bank-detail-card {
|
||||||
background: var(--bg-card);
|
background: rgba(0, 0, 0, 0.2);
|
||||||
backdrop-filter: blur(12px);
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 16px 18px;
|
padding: 16px 18px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-row {
|
.detail-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-label {
|
.detail-label {
|
||||||
width: 80px;
|
width: 72px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
padding-top: 1px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-val {
|
.detail-val {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text);
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mono { font-family: 'SF Mono', 'Consolas', monospace; letter-spacing: 0.04em; }
|
.copy-btn {
|
||||||
.break-all { word-break: break-all; }
|
background: rgba(212, 175, 55, 0.1);
|
||||||
|
border: 1px solid rgba(212, 175, 55, 0.25);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--primary-light);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 2px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn:hover {
|
||||||
|
background: rgba(212, 175, 55, 0.2);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mono {
|
||||||
|
font-family: 'SF Mono', 'Consolas', monospace;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.break-all {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* USDT QR */
|
||||||
|
.qr-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.qr-wrap {
|
.qr-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 12px 0;
|
padding: 8px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.qr-img {
|
.qr-img {
|
||||||
width: 140px;
|
width: 130px;
|
||||||
height: 140px;
|
height: 130px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
border-radius: 8px;
|
display: block;
|
||||||
border: 1px solid var(--desktop-border);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Inputs & Zones */
|
||||||
.field {
|
.field {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -488,11 +749,33 @@ onActivated(fetchMethods);
|
|||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.06em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.amount-input-wrap {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.currency-symbol {
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
color: var(--primary-light);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 800;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-input-wrap .field-input {
|
||||||
|
padding-left: 28px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
.field-input {
|
.field-input {
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: 1px solid var(--desktop-border);
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
background: #111;
|
background: rgba(0, 0, 0, 0.25);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -501,7 +784,33 @@ onActivated(fetchMethods);
|
|||||||
max-width: 320px;
|
max-width: 320px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-input:focus { border-color: var(--border-gold-soft); }
|
.field-input:focus {
|
||||||
|
border-color: var(--border-gold-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-presets {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-btn {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 4px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-btn:hover {
|
||||||
|
border-color: rgba(212, 175, 55, 0.3);
|
||||||
|
color: var(--primary-light);
|
||||||
|
background: rgba(212, 175, 55, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
.preview-wrap {
|
.preview-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -512,7 +821,7 @@ onActivated(fetchMethods);
|
|||||||
max-width: 180px;
|
max-width: 180px;
|
||||||
max-height: 180px;
|
max-height: 180px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid var(--desktop-border);
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -542,15 +851,21 @@ onActivated(fetchMethods);
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
width: 180px;
|
width: 180px;
|
||||||
height: 120px;
|
height: 120px;
|
||||||
border: 2px dashed var(--desktop-border);
|
border: 1.5px dashed rgba(212, 175, 55, 0.3);
|
||||||
|
background: rgba(255, 255, 255, 0.01);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: border-color 0.2s;
|
transition: all 0.25s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-zone:hover { border-color: var(--border-gold-soft); }
|
.upload-zone:hover {
|
||||||
|
border-color: var(--primary);
|
||||||
|
background: rgba(212, 175, 55, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
.hidden-input { display: none; }
|
.hidden-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.upload-hint {
|
.upload-hint {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -563,18 +878,35 @@ onActivated(fetchMethods);
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 11px 32px;
|
padding: 12px 36px;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
background: var(--primary);
|
background: linear-gradient(180deg, #f0d875 0%, #d4af37 100%);
|
||||||
color: #111;
|
color: #3d2800;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: opacity 0.2s;
|
transition: all 0.25s ease;
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
|
box-shadow: 0 4px 12px rgba(212, 175, 55, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.submit-btn:hover:not(:disabled) { opacity: 0.88; }
|
.submit-btn:hover:not(:disabled) {
|
||||||
.submit-btn:disabled { opacity: 0.45; cursor: default; }
|
opacity: 0.9;
|
||||||
|
box-shadow: 0 4px 16px rgba(212, 175, 55, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: default;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes circle-draw {
|
||||||
|
to { stroke-dashoffset: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes check-draw {
|
||||||
|
to { stroke-dashoffset: 0; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user