feat(finance): 添加财务记录状态显示功能

- 在 API 类型定义中添加订单状态字段及其枚举值
- 更新桌面端财务记录表格界面,增加状态列并调整列宽布局
- 实现移动端财务记录表格的状态列显示和样式适配
- 添加财务记录状态的颜色标识和标签显示逻辑
- 在多语言文件中添加状态相关的翻译内容
- 修复充值组件中的金额字段引用错误
- 重构提现功能中的汇率计算逻辑,提高准确性
This commit is contained in:
JiaJun
2026-07-07 16:29:05 +08:00
parent 8519a36ab7
commit 8721a47a61
12 changed files with 135 additions and 23 deletions

View File

@@ -266,6 +266,7 @@ function normalizeFinanceOrderItem(dto: FinanceOrderItemDto) {
amount: String(dto.amount ?? ''), amount: String(dto.amount ?? ''),
bonusAmount: String(dto.bonus_amount ?? ''), bonusAmount: String(dto.bonus_amount ?? ''),
orderNo: dto.order_no, orderNo: dto.order_no,
status: String(dto.status ?? ''),
} }
} }

View File

@@ -334,7 +334,7 @@ function DesktopTopup() {
{tier.title} {tier.title}
</div> </div>
<div className="pt-design-7 text-design-24 font-semibold leading-none text-[#FFE229]"> <div className="pt-design-7 text-design-24 font-semibold leading-none text-[#FFE229]">
{formatNumber(tier.payAmount)} {formatNumber(tier.amount)}
</div> </div>
</div> </div>
<div className="shrink-0 rounded-full border border-[rgba(121,219,229,0.24)] bg-[rgba(10,39,52,0.72)] px-design-6 py-[2px] text-design-10 leading-none text-[#7CDDE7]"> <div className="shrink-0 rounded-full border border-[rgba(121,219,229,0.24)] bg-[rgba(10,39,52,0.72)] px-design-6 py-[2px] text-design-10 leading-none text-[#7CDDE7]">

View File

@@ -334,7 +334,7 @@ function MobileTopup() {
{tier.title} {tier.title}
</div> </div>
<div className="pt-design-6 text-design-20 font-semibold leading-none text-[#FFE229]"> <div className="pt-design-6 text-design-20 font-semibold leading-none text-[#FFE229]">
{formatNumber(tier.payAmount)} {formatNumber(tier.amount)}
</div> </div>
</div> </div>
<div className="shrink-0 rounded-full border border-[rgba(121,219,229,0.24)] bg-[rgba(10,39,52,0.72)] px-design-5 py-[2px] text-design-8 leading-none text-[#7CDDE7]"> <div className="shrink-0 rounded-full border border-[rgba(121,219,229,0.24)] bg-[rgba(10,39,52,0.72)] px-design-5 py-[2px] text-design-8 leading-none text-[#7CDDE7]">

View File

@@ -31,6 +31,31 @@ function formatFinanceAmount(value: string, locale: string) {
}).format(numberValue) }).format(numberValue)
} }
function getFinanceStatusTone(status: string) {
if (status === 'approved') {
return 'approved'
}
if (status === 'rejected' || status === 'failed') {
return 'rejected'
}
return 'pending'
}
function getFinanceStatusLabel(status: string, t: (key: string) => string) {
if (
status === 'pending_review' ||
status === 'approved' ||
status === 'rejected' ||
status === 'failed'
) {
return t(`game.modals.userInfo.financeRecords.statuses.${status}`)
}
return '--'
}
export function useFinanceRecordsVm({ enabled }: { enabled: boolean }) { export function useFinanceRecordsVm({ enabled }: { enabled: boolean }) {
const { i18n, t } = useTranslation() const { i18n, t } = useTranslation()
const locale = i18n.resolvedLanguage ?? i18n.language ?? 'en-US' const locale = i18n.resolvedLanguage ?? i18n.language ?? 'en-US'
@@ -80,9 +105,11 @@ export function useFinanceRecordsVm({ enabled }: { enabled: boolean }) {
bonusAmountLabel: formatFinanceAmount(item.bonusAmount, locale), bonusAmountLabel: formatFinanceAmount(item.bonusAmount, locale),
id: item.orderNo || `${page.pagination.page}-${index}`, id: item.orderNo || `${page.pagination.page}-${index}`,
orderNoLabel: item.orderNo || '--', orderNoLabel: item.orderNo || '--',
statusLabel: getFinanceStatusLabel(item.status, t),
statusTone: getFinanceStatusTone(item.status),
})), })),
), ),
[locale, query.data?.pages], [locale, query.data?.pages, t],
) )
const selectRecordType = useCallback((type: FinanceRecordType) => { const selectRecordType = useCallback((type: FinanceRecordType) => {
@@ -103,6 +130,7 @@ export function useFinanceRecordsVm({ enabled }: { enabled: boolean }) {
amount: t('game.modals.userInfo.financeRecords.amount'), amount: t('game.modals.userInfo.financeRecords.amount'),
bonusAmount: t('game.modals.userInfo.financeRecords.bonusAmount'), bonusAmount: t('game.modals.userInfo.financeRecords.bonusAmount'),
orderNo: t('game.modals.userInfo.financeRecords.orderNo'), orderNo: t('game.modals.userInfo.financeRecords.orderNo'),
status: t('game.modals.userInfo.financeRecords.status'),
}, },
isError: query.isError, isError: query.isError,
isFetchingNextPage: query.isFetchingNextPage, isFetchingNextPage: query.isFetchingNextPage,

View File

@@ -16,7 +16,7 @@ function formatNumber(locale: string, value: number) {
} }
function getInitialWithdrawAmount( function getInitialWithdrawAmount(
selectedRate: number, selectedExchangeRate: number,
maxWithdrawAmount: number, maxWithdrawAmount: number,
) { ) {
if (maxWithdrawAmount <= 0) { if (maxWithdrawAmount <= 0) {
@@ -25,10 +25,22 @@ function getInitialWithdrawAmount(
return Math.min( return Math.min(
maxWithdrawAmount, maxWithdrawAmount,
Math.max(1, Math.round(selectedRate * QUICK_FIAT_AMOUNTS[0])), Math.max(1, Math.round(QUICK_FIAT_AMOUNTS[0] / selectedExchangeRate)),
) )
} }
function getCurrencyExchangeRate(
config: DepositWithdrawConfig,
currencyCode: string,
fallbackRate: number,
) {
const configuredRate = config.rates.find(
(rate) => rate.currency === currencyCode,
)?.diamondsPerFiatUnitValue
return configuredRate && configuredRate > 0 ? configuredRate : fallbackRate
}
function getActiveCurrencyCode( function getActiveCurrencyCode(
currencies: DepositWithdrawConfig['currencies'], currencies: DepositWithdrawConfig['currencies'],
selectedCurrencyCode: string, selectedCurrencyCode: string,
@@ -108,9 +120,11 @@ export function useWithdrawVm() {
currencyCode, currencyCode,
) )
const selectedRate = selectedCurrency.withdrawCoinsPerFiatValue || 1 const selectedRate = selectedCurrency.withdrawCoinsPerFiatValue || 1
const selectedExchangeRate = const selectedExchangeRate = getCurrencyExchangeRate(
config.rates.find((rate) => rate.currency === selectedCurrency.code) config,
?.diamondsPerFiatUnitValue || selectedRate selectedCurrency.code,
selectedRate,
)
const sortedPayChannels = useMemo( const sortedPayChannels = useMemo(
() => () =>
[ [
@@ -200,11 +214,13 @@ export function useWithdrawVm() {
}, [bankCode, sortedBanks]) }, [bankCode, sortedBanks])
useEffect(() => { useEffect(() => {
if (!hasInitializedAmount && selectedRate > 0) { if (!hasInitializedAmount && selectedExchangeRate > 0) {
setAmount(getInitialWithdrawAmount(selectedRate, maxWithdrawAmount)) setAmount(
getInitialWithdrawAmount(selectedExchangeRate, maxWithdrawAmount),
)
setHasInitializedAmount(true) setHasInitializedAmount(true)
} }
}, [hasInitializedAmount, maxWithdrawAmount, selectedRate, setAmount]) }, [hasInitializedAmount, maxWithdrawAmount, selectedExchangeRate, setAmount])
useEffect(() => { useEffect(() => {
if (amount > maxWithdrawAmount) { if (amount > maxWithdrawAmount) {
@@ -216,12 +232,12 @@ export function useWithdrawVm() {
return QUICK_FIAT_AMOUNTS.map((fiatAmount) => ({ return QUICK_FIAT_AMOUNTS.map((fiatAmount) => ({
diamonds: Math.min( diamonds: Math.min(
maxWithdrawAmount, maxWithdrawAmount,
Math.max(1, Math.round(selectedRate * fiatAmount)), Math.max(1, Math.round(fiatAmount / selectedExchangeRate)),
), ),
id: `quick-${selectedCurrency.code}-${fiatAmount}`, id: `quick-${selectedCurrency.code}-${fiatAmount}`,
preview: `${selectedCurrency.code} ${formatNumber(locale, fiatAmount)}`, preview: `${selectedCurrency.code} ${formatNumber(locale, fiatAmount)}`,
})) }))
}, [locale, maxWithdrawAmount, selectedCurrency.code, selectedRate]) }, [locale, maxWithdrawAmount, selectedCurrency.code, selectedExchangeRate])
const resetForm = useCallback(() => { const resetForm = useCallback(() => {
const nextCurrencyCode = config.currencies[0]?.code ?? DEFAULT_CURRENCY_CODE const nextCurrencyCode = config.currencies[0]?.code ?? DEFAULT_CURRENCY_CODE
@@ -229,9 +245,15 @@ export function useWithdrawVm() {
config.currencies, config.currencies,
nextCurrencyCode, nextCurrencyCode,
) )
const nextRate = nextCurrency.withdrawCoinsPerFiatValue || 1 const nextExchangeRate = getCurrencyExchangeRate(
config,
nextCurrency.code,
nextCurrency.withdrawCoinsPerFiatValue || 1,
)
setAmountState(getInitialWithdrawAmount(nextRate, maxWithdrawAmount)) setAmountState(
getInitialWithdrawAmount(nextExchangeRate, maxWithdrawAmount),
)
setHasInitializedAmount(true) setHasInitializedAmount(true)
setCurrencyCode(nextCurrencyCode) setCurrencyCode(nextCurrencyCode)
setPaymentChannelCode(sortedPayChannels[0]?.code ?? '') setPaymentChannelCode(sortedPayChannels[0]?.code ?? '')
@@ -240,7 +262,7 @@ export function useWithdrawVm() {
setBankAccount('') setBankAccount('')
setReceiverEmail('') setReceiverEmail('')
setReceiverPhone('') setReceiverPhone('')
}, [config.currencies, maxWithdrawAmount, sortedPayChannels]) }, [config, maxWithdrawAmount, sortedPayChannels])
const selectedCurrencyPreview = useMemo( const selectedCurrencyPreview = useMemo(
() => ({ () => ({
@@ -255,7 +277,7 @@ export function useWithdrawVm() {
}), }),
convertibleValue: `${formatNumber( convertibleValue: `${formatNumber(
locale, locale,
selectedRate > 0 ? amount / selectedRate : 0, selectedExchangeRate > 0 ? amount * selectedExchangeRate : 0,
)} ${selectedCurrency.code}`, )} ${selectedCurrency.code}`,
}), }),
[ [
@@ -264,7 +286,6 @@ export function useWithdrawVm() {
selectedCurrency.code, selectedCurrency.code,
selectedCurrency.label, selectedCurrency.label,
selectedExchangeRate, selectedExchangeRate,
selectedRate,
t, t,
], ],
) )

View File

@@ -270,6 +270,13 @@ export default {
orderNo: 'Order No.', orderNo: 'Order No.',
amount: 'Amount', amount: 'Amount',
bonusAmount: 'Bonus Amount', bonusAmount: 'Bonus Amount',
status: 'Status',
statuses: {
pending_review: 'Pending Review',
approved: 'Approved',
rejected: 'Rejected',
failed: 'Failed',
},
loading: 'Loading records...', loading: 'Loading records...',
loadFailed: 'Failed to load records. Please try again later.', loadFailed: 'Failed to load records. Please try again later.',
empty: 'No records yet', empty: 'No records yet',

View File

@@ -269,6 +269,13 @@ export default {
orderNo: 'No. Pesanan', orderNo: 'No. Pesanan',
amount: 'Jumlah', amount: 'Jumlah',
bonusAmount: 'Jumlah Bonus', bonusAmount: 'Jumlah Bonus',
status: 'Status',
statuses: {
pending_review: 'Menunggu Tinjauan',
approved: 'Disetujui',
rejected: 'Ditolak',
failed: 'Gagal',
},
loading: 'Memuat riwayat...', loading: 'Memuat riwayat...',
loadFailed: 'Gagal memuat riwayat. Silakan coba lagi nanti.', loadFailed: 'Gagal memuat riwayat. Silakan coba lagi nanti.',
empty: 'Belum ada riwayat', empty: 'Belum ada riwayat',

View File

@@ -272,6 +272,13 @@ export default {
orderNo: 'No. Pesanan', orderNo: 'No. Pesanan',
amount: 'Jumlah', amount: 'Jumlah',
bonusAmount: 'Jumlah Bonus', bonusAmount: 'Jumlah Bonus',
status: 'Status',
statuses: {
pending_review: 'Menunggu Semakan',
approved: 'Diluluskan',
rejected: 'Ditolak',
failed: 'Gagal',
},
loading: 'Memuatkan rekod...', loading: 'Memuatkan rekod...',
loadFailed: 'Gagal memuatkan rekod. Sila cuba lagi kemudian.', loadFailed: 'Gagal memuatkan rekod. Sila cuba lagi kemudian.',
empty: 'Belum ada rekod', empty: 'Belum ada rekod',

View File

@@ -264,6 +264,13 @@ export default {
orderNo: '订单号', orderNo: '订单号',
amount: '金额', amount: '金额',
bonusAmount: '赠送金额', bonusAmount: '赠送金额',
status: '状态',
statuses: {
pending_review: '待审核',
approved: '通过',
rejected: '拒绝',
failed: '失败',
},
loading: '记录加载中...', loading: '记录加载中...',
loadFailed: '记录加载失败,请稍后重试', loadFailed: '记录加载失败,请稍后重试',
empty: '暂无记录', empty: '暂无记录',

View File

@@ -94,12 +94,13 @@ function DesktopFinanceRecordsTab({ enabled }: { enabled: boolean }) {
<div className={'min-h-0 flex-1 rounded-md'}> <div className={'min-h-0 flex-1 rounded-md'}>
<div <div
className={ className={
'grid grid-cols-[minmax(0,1.55fr)_minmax(0,0.9fr)_minmax(0,0.9fr)] gap-design-10 rounded-md border border-[#2B8CA3]/35 bg-[#031B24]/75 px-design-16 py-design-12 text-design-16 text-[#7ECAD1]' 'grid grid-cols-[minmax(0,1.45fr)_minmax(0,0.78fr)_minmax(0,0.78fr)_minmax(0,0.72fr)] gap-design-10 rounded-md border border-[#2B8CA3]/35 bg-[#031B24]/75 px-design-16 py-design-12 text-design-16 text-[#7ECAD1]'
} }
> >
<div>{vm.headers.orderNo}</div> <div>{vm.headers.orderNo}</div>
<div>{vm.headers.amount}</div> <div>{vm.headers.amount}</div>
<div>{vm.headers.bonusAmount}</div> <div>{vm.headers.bonusAmount}</div>
<div>{vm.headers.status}</div>
</div> </div>
<div <div
@@ -138,7 +139,7 @@ function DesktopFinanceRecordsTab({ enabled }: { enabled: boolean }) {
{item ? ( {item ? (
<motion.div <motion.div
className={ className={
'grid h-[calc(var(--design-unit)*62)] grid-cols-[minmax(0,1.55fr)_minmax(0,0.9fr)_minmax(0,0.9fr)] items-center gap-design-10 rounded-md bg-[#0A4252] px-design-16 py-design-14 text-design-18 text-[#C4F2F7] shadow-[inset_0_0_calc(var(--design-unit)*10)_rgba(108,205,207,0.05)]' 'grid h-[calc(var(--design-unit)*62)] grid-cols-[minmax(0,1.45fr)_minmax(0,0.78fr)_minmax(0,0.78fr)_minmax(0,0.72fr)] items-center gap-design-10 rounded-md bg-[#0A4252] px-design-16 py-design-14 text-design-18 text-[#C4F2F7] shadow-[inset_0_0_calc(var(--design-unit)*10)_rgba(108,205,207,0.05)]'
} }
initial={{ opacity: 0, y: 6 }} initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
@@ -156,6 +157,20 @@ function DesktopFinanceRecordsTab({ enabled }: { enabled: boolean }) {
<div className={'truncate text-[#7CFFCF]'}> <div className={'truncate text-[#7CFFCF]'}>
{item.bonusAmountLabel} {item.bonusAmountLabel}
</div> </div>
<div>
<span
className={cn(
'inline-flex max-w-full items-center rounded-full border px-design-7 py-design-3 text-design-13 font-medium',
item.statusTone === 'approved'
? 'border-[#5BEA99]/35 bg-[#0F593A]/55 text-[#7CFFCF]'
: item.statusTone === 'rejected'
? 'border-[#F47C7C]/35 bg-[#5D1E24]/55 text-[#FF9C9C]'
: 'border-[#F0B44A]/35 bg-[#5D441E]/55 text-[#FFD583]',
)}
>
<span className="truncate">{item.statusLabel}</span>
</span>
</div>
</motion.div> </motion.div>
) : ( ) : (
<DataLoadingIndicator <DataLoadingIndicator

View File

@@ -98,11 +98,12 @@ function MobileFinanceRecordsTab({ enabled }: { enabled: boolean }) {
</div> </div>
<div className="min-h-0 flex-1 overflow-x-auto rounded-md"> <div className="min-h-0 flex-1 overflow-x-auto rounded-md">
<div className="min-w-design-330"> <div className="min-w-design-410">
<div className="grid grid-cols-[minmax(0,1.55fr)_minmax(0,0.9fr)_minmax(0,0.9fr)] gap-design-6 rounded-md border border-[#2B8CA3]/35 bg-[#031B24]/75 px-design-10 py-design-8 text-design-12 text-[#7ECAD1]"> <div className="grid grid-cols-[minmax(0,1.45fr)_minmax(0,0.75fr)_minmax(0,0.75fr)_minmax(0,0.72fr)] gap-design-6 rounded-md border border-[#2B8CA3]/35 bg-[#031B24]/75 px-design-10 py-design-8 text-design-12 text-[#7ECAD1]">
<div>{vm.headers.orderNo}</div> <div>{vm.headers.orderNo}</div>
<div>{vm.headers.amount}</div> <div>{vm.headers.amount}</div>
<div>{vm.headers.bonusAmount}</div> <div>{vm.headers.bonusAmount}</div>
<div>{vm.headers.status}</div>
</div> </div>
<div <div
@@ -138,7 +139,7 @@ function MobileFinanceRecordsTab({ enabled }: { enabled: boolean }) {
> >
{item ? ( {item ? (
<motion.div <motion.div
className="grid h-design-45 grid-cols-[minmax(0,1.55fr)_minmax(0,0.9fr)_minmax(0,0.9fr)] items-center gap-design-6 rounded-md bg-[#0A4252] px-design-10 py-design-8 text-design-12 text-[#C4F2F7] shadow-[inset_0_0_calc(var(--design-unit)*10)_rgba(108,205,207,0.05)]" className="grid h-design-45 grid-cols-[minmax(0,1.45fr)_minmax(0,0.75fr)_minmax(0,0.75fr)_minmax(0,0.72fr)] items-center gap-design-6 rounded-md bg-[#0A4252] px-design-10 py-design-8 text-design-12 text-[#C4F2F7] shadow-[inset_0_0_calc(var(--design-unit)*10)_rgba(108,205,207,0.05)]"
initial={{ opacity: 0, y: 6 }} initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ transition={{
@@ -158,6 +159,22 @@ function MobileFinanceRecordsTab({ enabled }: { enabled: boolean }) {
<div className="truncate text-[#7CFFCF]"> <div className="truncate text-[#7CFFCF]">
{item.bonusAmountLabel} {item.bonusAmountLabel}
</div> </div>
<div>
<span
className={cn(
'inline-flex max-w-full items-center rounded-full border px-design-5 py-design-2 text-design-9 font-medium',
item.statusTone === 'approved'
? 'border-[#5BEA99]/35 bg-[#0F593A]/55 text-[#7CFFCF]'
: item.statusTone === 'rejected'
? 'border-[#F47C7C]/35 bg-[#5D1E24]/55 text-[#FF9C9C]'
: 'border-[#F0B44A]/35 bg-[#5D441E]/55 text-[#FFD583]',
)}
>
<span className="truncate">
{item.statusLabel}
</span>
</span>
</div>
</motion.div> </motion.div>
) : ( ) : (
<DataLoadingIndicator <DataLoadingIndicator

View File

@@ -286,6 +286,7 @@ export interface FinanceOrderItemDto {
amount: number | string amount: number | string
bonus_amount: number | string bonus_amount: number | string
order_no: string order_no: string
status?: 'pending_review' | 'approved' | 'rejected' | 'failed' | (string & {})
} }
export interface FinanceOrderListDto { export interface FinanceOrderListDto {
@@ -297,6 +298,7 @@ export interface FinanceOrderItem {
amount: string amount: string
bonusAmount: string bonusAmount: string
orderNo: string orderNo: string
status: string
} }
export interface FinanceOrderList { export interface FinanceOrderList {