feat(game): 添加手续费处理功能并优化提现界面

- 在 API 类型定义中添加 handling_fee 字段支持
- 更新提现组件中的快速金额卡片布局和样式
- 重构 QuickAmountCard 组件以显示金额、钻石和手续费信息
- 在预览区域添加手续费显示行
- 实现手续费计算逻辑和格式化函数
- 更新多语言文件中的手续费相关文本
- 修复提交按钮文本国际化引用路径错误
- 调整桌面和移动端提现界面的宽度和间距参数
This commit is contained in:
JiaJun
2026-07-10 17:28:18 +08:00
parent 2d88e4a204
commit 2c1a69dd9a
10 changed files with 148 additions and 36 deletions

View File

@@ -105,6 +105,7 @@ function normalizePayChannel(dto: FinancePayChannelDto) {
currencyCodes: Array.isArray(dto.currency_codes) ? dto.currency_codes : [],
depositBanks: (dto.deposit_banks ?? []).map(normalizeWithdrawBank),
feeNote: dto.fee_note ?? '',
handlingFee: dto.handling_fee ?? '0.00',
methods: (dto.methods ?? []).map(normalizeDepositMethod),
minBank: dto.min_bank ?? '',
minEwallet: dto.min_ewallet ?? '',

View File

@@ -60,7 +60,7 @@ function WithdrawField({
}) {
return (
<div className="flex gap-design-14">
<div className="flex w-design-132 shrink-0 items-center justify-end whitespace-nowrap text-right text-design-16 font-medium uppercase leading-[1.15] tracking-[0.04em] text-[#6FD4DA]">
<div className="flex w-design-160 shrink-0 items-center justify-end text-right text-design-15 font-medium uppercase leading-[1.15] tracking-[0.04em] text-[#6FD4DA]">
<span>{label}</span>
<span className="pl-design-4">:</span>
</div>
@@ -132,13 +132,15 @@ function AmountShell({
}
function QuickAmountCard({
amount,
preview,
amountLabel,
diamondLabel,
feeLabel,
active,
onClick,
}: {
amount: number
preview: string
amountLabel: string
diamondLabel: string
feeLabel: string
active: boolean
onClick: () => void
}) {
@@ -147,7 +149,7 @@ function QuickAmountCard({
type="button"
onClick={onClick}
className={cn(
'group relative flex h-design-76 min-w-0 w-full cursor-pointer flex-col items-start justify-center overflow-hidden rounded-[calc(var(--design-unit)*8)] border px-design-12 text-left transition-[transform,border-color,background-color,box-shadow] duration-150',
'group relative flex h-design-92 min-w-0 w-full cursor-pointer flex-col gap-design-5 items-start justify-center overflow-hidden rounded-[calc(var(--design-unit)*8)] border px-design-12 text-left transition-[transform,border-color,background-color,box-shadow] duration-150',
active
? 'border-[#D18A43] bg-[linear-gradient(180deg,rgba(88,54,28,0.96),rgba(56,33,18,0.92))] shadow-[0_0_calc(var(--design-unit)*14)_rgba(209,138,67,0.22),inset_0_0_calc(var(--design-unit)*12)_rgba(255,217,120,0.08)]'
: 'border-[rgba(103,227,239,0.26)] bg-[linear-gradient(180deg,rgba(11,48,63,0.9),rgba(5,24,35,0.94))] hover:-translate-y-[1px] hover:border-[rgba(170,247,255,0.62)] hover:shadow-[0_0_calc(var(--design-unit)*12)_rgba(88,225,238,0.12)]',
@@ -161,16 +163,19 @@ function QuickAmountCard({
: 'bg-[rgba(122,220,230,0.26)]',
)}
/>
<div className="text-design-24 font-semibold leading-none text-[#FFE229]">
{amount}
<div className="max-w-full truncate pr-design-6 text-design-14 font-semibold tabular-nums leading-none text-[#FFE229]">
{amountLabel}
</div>
<div
className={cn(
'pt-design-6 text-design-12 leading-none tracking-[0.04em]',
'max-w-full truncate pt-design-7 text-[length:calc(var(--design-unit)*12)] leading-none',
active ? 'text-[#FFDFA4]' : 'text-[#63AEB6]',
)}
>
{preview}
{diamondLabel}
</div>
<div className="max-w-full truncate pt-design-5 text-design-10 leading-none text-[#F0B44A]">
{feeLabel}
</div>
</button>
)
@@ -290,12 +295,12 @@ function PreviewRow({
}) {
return (
<div className="flex border-b border-[rgba(89,209,223,0.2)] last:border-b-0">
<div className="flex w-[44%] shrink-0 items-center border-r border-[rgba(89,209,223,0.2)] px-design-14 py-design-20 text-design-16 font-medium uppercase leading-[1.15] text-[#7CE3E8]">
<div className="flex w-[50%] min-w-0 shrink-0 items-center break-words border-r border-[rgba(89,209,223,0.2)] px-design-14 py-design-18 text-design-15 font-medium uppercase leading-[1.2] text-[#7CE3E8]">
{label}
</div>
<div
className={cn(
'flex min-w-0 flex-1 items-center justify-end px-design-14 py-design-20 text-right text-design-16 text-[#E6FFFF]',
'flex min-w-0 flex-1 items-center justify-end break-words px-design-14 py-design-18 text-right text-design-16 text-[#E6FFFF]',
highlight && 'text-design-18 font-semibold text-[#6DFF83]',
)}
>
@@ -500,13 +505,14 @@ function DesktopWithdraw() {
</WithdrawField>
<div className="flex items-start gap-design-14">
<div className="w-design-132 shrink-0" />
<div className="w-design-160 shrink-0" />
<div className="grid min-w-0 flex-1 grid-cols-3 gap-design-10">
{vm.quickAmounts.map((option) => (
<QuickAmountCard
key={option.id}
amount={option.diamonds}
preview={option.preview}
amountLabel={option.amountLabel}
diamondLabel={option.diamondLabel}
feeLabel={option.feeLabel}
active={option.id === activeQuickAmountId}
onClick={() =>
handleQuickAmountSelect(option.id, option.diamonds)
@@ -664,7 +670,7 @@ function DesktopWithdraw() {
<div className="w-px shrink-0 bg-[linear-gradient(180deg,rgba(89,209,223,0)_0%,rgba(89,209,223,0.4)_12%,rgba(89,209,223,0.5)_88%,rgba(89,209,223,0)_100%)]" />
<div className="flex min-h-full min-w-0 w-design-520 shrink-0 flex-col">
<div className="flex min-h-full min-w-0 w-design-640 shrink-0 flex-col">
<div className="flex h-design-44 items-center border-b border-[rgba(89,209,223,0.2)] bg-[linear-gradient(90deg,rgba(18,99,110,0.8),rgba(7,68,79,0.9))] px-design-12 text-design-20 font-semibold uppercase tracking-[0.04em] text-[#9AF5FB]">
{t('gameDesktop.withdraw.preview.title')}
</div>
@@ -684,6 +690,10 @@ function DesktopWithdraw() {
value={vm.selectedCurrencyPreview.convertibleValue}
highlight={true}
/>
<PreviewRow
label={vm.selectedCurrencyPreview.handlingFeeLabel}
value={vm.selectedCurrencyPreview.handlingFeeValue}
/>
<PreviewRow
label={t(
'gameDesktop.withdraw.preview.fixedExchangeDiamondAmount',
@@ -735,7 +745,7 @@ function DesktopWithdraw() {
)}
>
{withdrawSubmitMutation.isPending
? t('commonUi.action.submitting')
? t('commonUi.actions.submitting')
: `${t('gameDesktop.withdraw.confirm')}${t('gameDesktop.withdraw.withdrawal')}`}
</SmartBackground>
</div>

View File

@@ -122,13 +122,15 @@ function AmountShell({
}
function QuickAmountCard({
amount,
preview,
amountLabel,
diamondLabel,
feeLabel,
active,
onClick,
}: {
amount: number
preview: string
amountLabel: string
diamondLabel: string
feeLabel: string
active: boolean
onClick: () => void
}) {
@@ -137,7 +139,7 @@ function QuickAmountCard({
type="button"
onClick={onClick}
className={cn(
'group relative flex h-design-42 min-w-0 w-full cursor-pointer flex-col items-start justify-center overflow-hidden rounded-[calc(var(--design-unit)*6)] border px-design-6 text-left transition-[border-color,background-color,box-shadow,filter] duration-150',
'group relative flex h-design-58 min-w-0 w-full cursor-pointer flex-col items-start justify-center overflow-hidden rounded-[calc(var(--design-unit)*6)] border px-design-6 text-left transition-[border-color,background-color,box-shadow,filter] duration-150',
active
? 'border-[#D18A43] bg-[linear-gradient(180deg,rgba(88,54,28,0.96),rgba(56,33,18,0.92))] shadow-[0_0_calc(var(--design-unit)*10)_rgba(209,138,67,0.2),inset_0_0_calc(var(--design-unit)*10)_rgba(255,217,120,0.08)]'
: 'border-[rgba(103,227,239,0.26)] bg-[linear-gradient(180deg,rgba(11,48,63,0.9),rgba(5,24,35,0.94))] hover:border-[rgba(170,247,255,0.62)] hover:brightness-110',
@@ -151,16 +153,19 @@ function QuickAmountCard({
: 'bg-[rgba(122,220,230,0.26)]',
)}
/>
<div className="max-w-full truncate text-design-13 font-semibold leading-none text-[#FFE229]">
{amount}
<div className="max-w-full truncate text-design-12 font-semibold leading-none text-[#FFE229]">
{amountLabel}
</div>
<div
className={cn(
'max-w-full truncate pt-design-3 text-design-10 leading-none',
'max-w-full truncate pt-design-3 text-[length:calc(var(--design-unit)*10)] leading-none',
active ? 'text-[#FFDFA4]' : 'text-[#63AEB6]',
)}
>
{preview}
{diamondLabel}
</div>
<div className="max-w-full truncate pt-design-2 text-design-10 leading-none text-[#F0B44A]">
{feeLabel}
</div>
</button>
)
@@ -490,8 +495,9 @@ function MobileWithdraw() {
{vm.quickAmounts.map((option) => (
<QuickAmountCard
key={option.id}
amount={option.diamonds}
preview={option.preview}
amountLabel={option.amountLabel}
diamondLabel={option.diamondLabel}
feeLabel={option.feeLabel}
active={option.id === activeQuickAmountId}
onClick={() =>
handleQuickAmountSelect(option.id, option.diamonds)
@@ -654,6 +660,10 @@ function MobileWithdraw() {
value={vm.selectedCurrencyPreview.convertibleValue}
highlight={true}
/>
<PreviewRow
label={vm.selectedCurrencyPreview.handlingFeeLabel}
value={vm.selectedCurrencyPreview.handlingFeeValue}
/>
<PreviewRow
label={t(
'gameDesktop.withdraw.preview.fixedExchangeDiamondAmount',
@@ -707,7 +717,7 @@ function MobileWithdraw() {
)}
>
{withdrawSubmitMutation.isPending
? t('commonUi.action.submitting')
? t('commonUi.actions.submitting')
: `${t('gameDesktop.withdraw.confirm')}`}
</SmartBackground>
</div>

View File

@@ -20,6 +20,42 @@ function formatNumber(locale: string, value: number) {
return new Intl.NumberFormat(locale).format(value)
}
function formatConfiguredAmount(locale: string, value: string) {
const normalizedValue = value.trim()
const numericValue = Number(normalizedValue)
if (!Number.isFinite(numericValue)) {
return normalizedValue || '0'
}
const fractionDigits = Math.min(
6,
normalizedValue.includes('.')
? (normalizedValue.split('.')[1]?.length ?? 0)
: 0,
)
return new Intl.NumberFormat(locale, {
maximumFractionDigits: fractionDigits,
minimumFractionDigits: fractionDigits,
}).format(numericValue)
}
function calculateNetWithdrawalAmount(
grossAmount: number,
handlingFee: string,
) {
const parsedHandlingFee = Number(handlingFee)
if (!Number.isFinite(parsedHandlingFee)) {
return grossAmount
}
const handlingFeeRate = Math.min(100, Math.max(0, parsedHandlingFee)) / 100
return grossAmount * (1 - handlingFeeRate)
}
function getInitialWithdrawAmount(bounds: WithdrawDiamondBounds | null) {
return bounds?.minimumDiamonds ?? 0
}
@@ -209,6 +245,7 @@ export function useWithdrawVm() {
const selectedPaymentChannel =
sortedPayChannels.find((channel) => channel.code === paymentChannelCode) ??
null
const selectedHandlingFee = selectedPaymentChannel?.handlingFee ?? '0.00'
const availablePaymentMethods = useMemo(
() => selectedPaymentChannel?.methods ?? [],
[selectedPaymentChannel],
@@ -223,6 +260,9 @@ export function useWithdrawVm() {
const selectedCurrency =
availableCurrencies.find((item) => item.code === currencyCode) ?? null
const selectedCurrencyCode = selectedCurrency?.code ?? ''
const selectedHandlingFeeValue = selectedPaymentChannel
? `${formatConfiguredAmount(locale, selectedHandlingFee)}%`
: '--'
const selectedRate = selectedCurrency?.withdrawCoinsPerFiatValue ?? 0
const selectedExchangeRate = selectedCurrency
? getCurrencyExchangeRate(config, selectedCurrency.code, selectedRate || 1)
@@ -372,12 +412,21 @@ export function useWithdrawVm() {
}
return getQuickWithdrawAmounts(withdrawDiamondBounds).map((diamonds) => ({
diamonds,
id: `quick-${paymentChannelCode}-${paymentType}-${selectedCurrency.code}-${diamonds}`,
preview: `${selectedCurrency.code} ${formatNumber(
amountLabel: `${selectedCurrency.code} ${formatNumber(
locale,
diamonds * selectedExchangeRate,
calculateNetWithdrawalAmount(
diamonds * selectedExchangeRate,
selectedHandlingFee,
),
)}`,
diamondLabel: t('gameDesktop.withdraw.preview.quickDiamondAmount', {
amount: formatNumber(locale, diamonds),
}),
diamonds,
feeLabel: t('gameDesktop.withdraw.preview.quickHandlingFee', {
fee: selectedHandlingFeeValue,
}),
id: `quick-${paymentChannelCode}-${paymentType}-${selectedCurrency.code}-${diamonds}`,
}))
}, [
locale,
@@ -385,6 +434,9 @@ export function useWithdrawVm() {
paymentType,
selectedCurrency,
selectedExchangeRate,
selectedHandlingFee,
selectedHandlingFeeValue,
t,
withdrawDiamondBounds,
])
@@ -448,11 +500,24 @@ export function useWithdrawVm() {
selectedCurrency && selectedExchangeRate > 0
? `${formatNumber(
locale,
amount * selectedExchangeRate,
calculateNetWithdrawalAmount(
amount * selectedExchangeRate,
selectedHandlingFee,
),
)} ${selectedCurrency.code}`
: '--',
handlingFeeLabel: t('gameDesktop.withdraw.preview.handlingFee'),
handlingFeeValue: selectedHandlingFeeValue,
}
}, [amount, locale, selectedCurrency, selectedExchangeRate, t])
}, [
amount,
locale,
selectedCurrency,
selectedExchangeRate,
selectedHandlingFee,
selectedHandlingFeeValue,
t,
])
return {
amount,

View File

@@ -470,6 +470,9 @@ export default {
},
},
commonUi: {
actions: {
submitting: TEXT_SUBMITTING,
},
dialog: {
close: 'Close alert',
confirm: 'OK',
@@ -839,6 +842,9 @@ export default {
exchangeRate: 'Exchange Rate ({{currency}})',
exchangeRateValue: '{{coins}} {{platformCoinLabel}} = 1 {{currency}}',
convertible: 'Convertible {{currency}}',
handlingFee: 'Handling Fee',
quickDiamondAmount: '{{amount}} diamonds',
quickHandlingFee: 'Fee deducted: {{fee}}',
fixedExchangeDiamondAmount: 'Fixed Exchange Diamond Amount',
},
},

View File

@@ -469,6 +469,9 @@ export default {
},
},
commonUi: {
actions: {
submitting: TEXT_SUBMITTING,
},
dialog: {
close: 'Tutup notifikasi',
confirm: 'OK',
@@ -841,6 +844,9 @@ export default {
exchangeRate: 'Rasio Penukaran ({{currency}})',
exchangeRateValue: '{{coins}} {{platformCoinLabel}} = 1 {{currency}}',
convertible: 'Bisa Ditukar {{currency}}',
handlingFee: 'Biaya Penanganan',
quickDiamondAmount: '{{amount}} diamond',
quickHandlingFee: 'Biaya dipotong: {{fee}}',
fixedExchangeDiamondAmount: 'Jumlah Berlian Tukar Tetap',
},
},

View File

@@ -472,6 +472,9 @@ export default {
},
},
commonUi: {
actions: {
submitting: TEXT_SUBMITTING,
},
dialog: {
close: 'Tutup notifikasi',
confirm: 'OK',
@@ -845,6 +848,9 @@ export default {
exchangeRate: 'Kadar Pertukaran ({{currency}})',
exchangeRateValue: '{{coins}} {{platformCoinLabel}} = 1 {{currency}}',
convertible: 'Boleh Tukar {{currency}}',
handlingFee: 'Yuran Pengendalian',
quickDiamondAmount: '{{amount}} berlian',
quickHandlingFee: 'Yuran ditolak: {{fee}}',
fixedExchangeDiamondAmount: 'Jumlah Berlian Tukaran Tetap',
},
},

View File

@@ -453,6 +453,9 @@ export default {
},
},
commonUi: {
actions: {
submitting: TEXT_SUBMITTING,
},
dialog: {
close: '关闭提示',
confirm: '知道了',
@@ -809,6 +812,9 @@ export default {
exchangeRate: '兑换比例 ({{currency}})',
exchangeRateValue: '{{coins}} {{platformCoinLabel}} = 1 {{currency}}',
convertible: '可兑换{{currency}}',
handlingFee: '手续费',
quickDiamondAmount: '{{amount}} 钻石',
quickHandlingFee: '扣除手续费 {{fee}}',
fixedExchangeDiamondAmount: '固定兑换钻石金额',
},
},

View File

@@ -27,7 +27,7 @@ function DesktopWithdrawTopupModal() {
}
isNormalBg={true}
titleAlign="left"
className={'w-design-1200 h-design-700'}
className={'h-design-700 w-design-1320'}
>
<div className={'w-full h-[96%]'}>
{type === 'withdraw' ? <DesktopWithdraw /> : <DesktopTopup />}

View File

@@ -30,6 +30,7 @@ export interface FinancePayChannelDto {
currency_codes?: string[]
deposit_banks?: FinanceWithdrawBankDto[]
fee_note?: string
handling_fee?: string
methods?: FinanceDepositMethodDto[]
min_bank?: string
min_ewallet?: string
@@ -171,6 +172,7 @@ export interface FinancePayChannel {
currencyCodes: string[]
depositBanks: FinanceWithdrawBank[]
feeNote: string
handlingFee: string
methods: FinanceDepositMethod[]
minBank: string
minEwallet: string