feat: 开户备注、账单展示优化与后台代理管理增强

- 新增初始上分备注(日常上分/开户赠金/自定义)及前后台校验与展示

- 优化钱包流水类型与备注显示,区分管理员/代理/玩家上下分

- 修复登录后语言被后端覆盖的问题,登录时同步当前语言到服务端

- 后台代理/玩家表格操作栏重构,充值订单增加备注列

- 前台个人中心、充值、账单与验证码组件体验优化

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-11 17:23:58 +08:00
parent 10485ecfaf
commit 03e72ca9b2
46 changed files with 3721 additions and 1059 deletions

View File

@@ -154,6 +154,12 @@ function copyText(text: string) {
navigator.clipboard?.writeText(text);
}
function formatCardNumber(num: string | null): string {
if (!num) return '—';
const digits = num.replace(/\s/g, '');
return digits.replace(/(\d{4})(?=\d)/g, '$1 ').trim();
}
onMounted(fetchMethods);
</script>
@@ -202,25 +208,43 @@ onMounted(fetchMethods);
</div>
<div v-else class="empty-methods">{{ t('recharge.no_methods') }}</div>
<div v-if="selectedMethod" class="method-info">
<template v-if="selectedMethod.methodType === 'BANK'">
<div class="info-row">
<span class="info-label">{{ t('recharge.bank_name') }}</span>
<span class="info-value">{{ selectedMethod.bankName }}</span>
<div v-if="selectedMethod && selectedMethod.methodType === 'BANK'" class="bank-card-wrap">
<div class="bank-card-face">
<div class="bank-card-shine" aria-hidden="true" />
<div class="bank-card-deco bank-card-deco--1" aria-hidden="true" />
<div class="bank-card-deco bank-card-deco--2" aria-hidden="true" />
<div class="bank-card-top">
<div class="bank-card-chip" aria-hidden="true">
<span /><span /><span />
</div>
<div class="bank-card-bank">{{ selectedMethod.bankName }}</div>
</div>
<div class="info-row">
<span class="info-label">{{ t('recharge.account_holder') }}</span>
<span class="info-value">{{ selectedMethod.accountHolder }}</span>
<div class="bank-card-number-row">
<span class="bank-card-number">{{ formatCardNumber(selectedMethod.accountNumber) }}</span>
<button
type="button"
class="bank-card-copy"
:aria-label="t('recharge.account_number')"
@click="copyText(selectedMethod.accountNumber || '')"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" />
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
</svg>
</button>
</div>
<div class="info-row">
<span class="info-label">{{ t('recharge.account_number') }}</span>
<span class="info-value copyable" @click="copyText(selectedMethod!.accountNumber || '')">
{{ selectedMethod.accountNumber }}
<svg class="copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
<div class="bank-card-bottom">
<div class="bank-card-holder">
<span class="bank-card-holder-label">{{ t('recharge.account_holder') }}</span>
<span class="bank-card-holder-name">{{ selectedMethod.accountHolder }}</span>
</div>
<div class="bank-card-type">DEBIT</div>
</div>
</template>
<template v-else>
</div>
</div>
<div v-else-if="selectedMethod" class="method-info">
<template v-if="selectedMethod.methodType !== 'BANK'">
<div class="info-row">
<span class="info-label">{{ t('recharge.usdt_address') }}</span>
<span class="info-value copyable" @click="copyText(selectedMethod!.usdtAddress || '')">
@@ -317,6 +341,203 @@ onMounted(fetchMethods);
.pill-sub { font-size: 10px; color: var(--text-muted); }
.empty-methods { text-align: center; color: var(--text-muted); padding: 20px; font-size: 12px; }
.bank-card-wrap {
margin-bottom: 16px;
perspective: 800px;
}
.bank-card-face {
position: relative;
aspect-ratio: 2 / 1;
border-radius: 14px;
padding: 14px 18px 12px;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
background:
radial-gradient(ellipse 80% 60% at 100% 0%, rgba(212, 175, 55, 0.22), transparent 55%),
radial-gradient(ellipse 50% 50% at 0% 100%, rgba(212, 175, 55, 0.1), transparent 50%),
linear-gradient(135deg, #2a2218 0%, #1a1610 35%, #12100c 70%, #0a0908 100%);
border: 1px solid rgba(212, 175, 55, 0.35);
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.55),
0 2px 8px rgba(212, 175, 55, 0.12),
inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
.bank-card-shine {
position: absolute;
inset: 0;
background: linear-gradient(
115deg,
transparent 30%,
rgba(255, 255, 255, 0.04) 45%,
rgba(255, 255, 255, 0.08) 50%,
rgba(255, 255, 255, 0.04) 55%,
transparent 70%
);
pointer-events: none;
}
.bank-card-deco {
position: absolute;
border-radius: 50%;
border: 1px solid rgba(212, 175, 55, 0.12);
pointer-events: none;
}
.bank-card-deco--1 {
width: 180px;
height: 180px;
right: -60px;
bottom: -80px;
background: radial-gradient(circle, rgba(212, 175, 55, 0.08), transparent 70%);
}
.bank-card-deco--2 {
width: 100px;
height: 100px;
right: 40px;
bottom: 20px;
border-color: rgba(212, 175, 55, 0.08);
}
.bank-card-top,
.bank-card-number-row,
.bank-card-bottom {
position: relative;
z-index: 1;
}
.bank-card-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.bank-card-chip {
width: 36px;
height: 26px;
border-radius: 6px;
background: linear-gradient(135deg, #e8c96a 0%, #c9a227 40%, #a8841f 100%);
border: 1px solid rgba(255, 255, 255, 0.25);
box-shadow: inset 0 1px 2px rgba(255, 255, 255, 0.3), 0 2px 4px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
justify-content: center;
gap: 4px;
padding: 0 8px;
flex-shrink: 0;
}
.bank-card-chip span {
display: block;
height: 2px;
border-radius: 1px;
background: rgba(0, 0, 0, 0.25);
}
.bank-card-chip span:nth-child(2) { width: 70%; }
.bank-card-chip span:nth-child(3) { width: 50%; }
.bank-card-bank {
font-size: 15px;
font-weight: 800;
color: #fff;
text-align: right;
letter-spacing: 0.5px;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
line-height: 1.3;
max-width: 58%;
word-break: break-all;
}
.bank-card-number-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin: 4px 0 2px;
}
.bank-card-number {
font-family: 'SF Mono', 'Consolas', 'Courier New', monospace;
font-size: clamp(15px, 4.2vw, 19px);
font-weight: 700;
letter-spacing: 0.14em;
color: var(--primary-light);
text-shadow: 0 0 12px rgba(240, 216, 117, 0.25);
word-break: break-all;
line-height: 1.3;
}
.bank-card-copy {
flex-shrink: 0;
width: 30px;
height: 30px;
border-radius: 8px;
border: 1px solid rgba(212, 175, 55, 0.35);
background: rgba(0, 0, 0, 0.25);
color: var(--primary-light);
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.15s, opacity 0.15s;
}
.bank-card-copy svg {
width: 16px;
height: 16px;
}
.bank-card-copy:active {
opacity: 0.65;
background: rgba(212, 175, 55, 0.12);
}
.bank-card-bottom {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 12px;
}
.bank-card-holder {
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
}
.bank-card-holder-label {
font-size: 9px;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.45);
}
.bank-card-holder-name {
font-size: 14px;
font-weight: 700;
color: #fff;
letter-spacing: 0.04em;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
word-break: break-all;
}
.bank-card-type {
flex-shrink: 0;
font-size: 11px;
font-weight: 800;
letter-spacing: 0.18em;
color: rgba(212, 175, 55, 0.55);
font-style: italic;
}
.method-info {
background: rgba(17, 17, 17, 0.9);
border-radius: 8px; padding: 10px 12px;