feat(game): 添加手续费处理功能并优化提现界面
- 在 API 类型定义中添加 handling_fee 字段支持 - 更新提现组件中的快速金额卡片布局和样式 - 重构 QuickAmountCard 组件以显示金额、钻石和手续费信息 - 在预览区域添加手续费显示行 - 实现手续费计算逻辑和格式化函数 - 更新多语言文件中的手续费相关文本 - 修复提交按钮文本国际化引用路径错误 - 调整桌面和移动端提现界面的宽度和间距参数
This commit is contained in:
@@ -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 ?? '',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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: '固定兑换钻石金额',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user