feat(wallet): 补齐钱包提现与卖家资金页面
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Dialog,
|
||||
ErrorBlock,
|
||||
Form,
|
||||
Input,
|
||||
List,
|
||||
NavBar,
|
||||
Selector,
|
||||
Skeleton,
|
||||
Tag,
|
||||
Toast,
|
||||
} from 'antd-mobile'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import {
|
||||
cancelWithdrawal,
|
||||
createWithdrawal,
|
||||
fetchPaymentAccounts,
|
||||
fetchWalletBalance,
|
||||
fetchWithdrawals,
|
||||
} from '@/features/wallet/api'
|
||||
import { paymentAccountTypeLabel, withdrawalStatusMeta } from '@/features/wallet/labels'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { formatDateTime, formatMoney } from '@/shared/utils/format'
|
||||
|
||||
interface WithdrawFormValues {
|
||||
amount: string
|
||||
}
|
||||
|
||||
function errorText(error: unknown) {
|
||||
const response = (error as { response?: { data?: { message?: string } } })?.response
|
||||
if (response?.data?.message) return response.data.message
|
||||
return error instanceof Error ? error.message : '请求失败,请稍后重试'
|
||||
}
|
||||
|
||||
export function MobileWithdrawalPage() {
|
||||
const navigate = useNavigate()
|
||||
const queryClient = useQueryClient()
|
||||
const [form] = Form.useForm<WithdrawFormValues>()
|
||||
const [accountId, setAccountId] = useState<number>()
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const user = useSessionStore(state => state.user)
|
||||
|
||||
const balanceQuery = useQuery({
|
||||
queryKey: ['wallet-balance'],
|
||||
queryFn: fetchWalletBalance,
|
||||
enabled: Boolean(user),
|
||||
})
|
||||
const accountsQuery = useQuery({
|
||||
queryKey: ['payment-accounts', 1],
|
||||
queryFn: () => fetchPaymentAccounts(1, 50),
|
||||
enabled: Boolean(user),
|
||||
})
|
||||
const withdrawalsQuery = useQuery({
|
||||
queryKey: ['withdrawals', 1, 20],
|
||||
queryFn: () => fetchWithdrawals(1, 20),
|
||||
enabled: Boolean(user),
|
||||
})
|
||||
|
||||
const available = balanceQuery.data?.available_balance_cent ?? 0
|
||||
const accounts = useMemo(() => accountsQuery.data?.items ?? [], [accountsQuery.data])
|
||||
const withdrawals = withdrawalsQuery.data?.items ?? []
|
||||
|
||||
useEffect(() => {
|
||||
if (accountId) return
|
||||
const preferred = accounts.find(item => item.is_default) ?? accounts[0]
|
||||
if (preferred) setAccountId(preferred.id)
|
||||
}, [accounts, accountId])
|
||||
|
||||
const submit = async () => {
|
||||
const values = await form.validateFields()
|
||||
if (!accountId) {
|
||||
Toast.show({ content: '请先选择收款账户' })
|
||||
return
|
||||
}
|
||||
const amountCent = Math.round(Number(values.amount) * 100)
|
||||
if (!Number.isFinite(amountCent) || amountCent <= 0) {
|
||||
Toast.show({ content: '请输入正确的提现金额' })
|
||||
return
|
||||
}
|
||||
if (amountCent > available) {
|
||||
Toast.show({ content: '提现金额不能超过可用余额' })
|
||||
return
|
||||
}
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await createWithdrawal({ payment_account_id: accountId, amount_cent: amountCent })
|
||||
Toast.show({ icon: 'success', content: '提现申请已提交' })
|
||||
form.setFieldValue('amount', '')
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ['withdrawals'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['wallet-balance'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['wallet-ledger'] }),
|
||||
])
|
||||
} catch (error) {
|
||||
Toast.show({ content: errorText(error) })
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const cancel = async (id: number, withdrawNo: string) => {
|
||||
const confirmed = await Dialog.confirm({
|
||||
content: `确认取消提现 ${withdrawNo}?取消后金额退回可用余额。`,
|
||||
confirmText: '取消提现',
|
||||
cancelText: '返回',
|
||||
})
|
||||
if (!confirmed) return
|
||||
try {
|
||||
await cancelWithdrawal(id)
|
||||
Toast.show({ icon: 'success', content: '提现已取消' })
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ['withdrawals'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['wallet-balance'] }),
|
||||
])
|
||||
} catch (error) {
|
||||
Toast.show({ content: errorText(error) })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mobile-wallet">
|
||||
<NavBar onBack={() => navigate('/m/profile')}>提现</NavBar>
|
||||
|
||||
<Card className="mobile-wallet__balance">
|
||||
<div className="mobile-wallet__label">可用余额</div>
|
||||
{balanceQuery.isPending ? (
|
||||
<Skeleton.Title animated />
|
||||
) : (
|
||||
<div className="mobile-wallet__amount">{formatMoney(available)}</div>
|
||||
)}
|
||||
<Button
|
||||
size="mini"
|
||||
color="primary"
|
||||
fill="outline"
|
||||
onClick={() => navigate('/m/profile')}
|
||||
>
|
||||
返回我的
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
{accounts.length === 0 && !accountsQuery.isPending ? (
|
||||
<ErrorBlock
|
||||
status="empty"
|
||||
title="还没有收款账户"
|
||||
description="请先在电脑端「我的钱包 · 收款账户」添加账户后再提现。"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Card title="收款账户">
|
||||
{accountsQuery.isPending ? (
|
||||
<Skeleton.Paragraph lineCount={2} animated />
|
||||
) : (
|
||||
<Selector
|
||||
value={accountId ? [String(accountId)] : []}
|
||||
onChange={values => setAccountId(values[0] ? Number(values[0]) : undefined)}
|
||||
options={accounts.map(account => ({
|
||||
label: `${paymentAccountTypeLabel(account.account_type)} · ${account.account_name}`,
|
||||
description: account.account_no,
|
||||
value: String(account.id),
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card title="提现金额">
|
||||
<Form form={form} layout="horizontal">
|
||||
<Form.Item
|
||||
name="amount"
|
||||
label="金额(元)"
|
||||
rules={[{ required: true, message: '请输入提现金额' }]}
|
||||
>
|
||||
<Input type="number" placeholder="0.00" inputMode="decimal" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<div className="mobile-wallet__hint">
|
||||
可提现 {formatMoney(available)},当前手续费 0%,到账金额以审核结果为准。
|
||||
</div>
|
||||
<Button
|
||||
block
|
||||
color="primary"
|
||||
loading={submitting}
|
||||
onClick={() => void submit()}
|
||||
style={{ marginTop: 12 }}
|
||||
>
|
||||
提交提现申请
|
||||
</Button>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Card title="提现记录">
|
||||
{withdrawalsQuery.isPending ? (
|
||||
<Skeleton.Paragraph lineCount={4} animated />
|
||||
) : withdrawals.length === 0 ? (
|
||||
<ErrorBlock status="empty" title="还没有提现记录" />
|
||||
) : (
|
||||
<List>
|
||||
{withdrawals.map(item => {
|
||||
const meta = withdrawalStatusMeta(item.status)
|
||||
return (
|
||||
<List.Item
|
||||
key={item.id}
|
||||
description={`${formatDateTime(item.created_at)} · 手续费 ${formatMoney(item.fee_cent)}`}
|
||||
extra={
|
||||
<Tag color={meta.color === 'default' ? 'default' : meta.color}>
|
||||
{meta.label}
|
||||
</Tag>
|
||||
}
|
||||
clickable={item.status === 'pending'}
|
||||
onClick={() => {
|
||||
if (item.status === 'pending') void cancel(item.id, item.withdraw_no)
|
||||
}}
|
||||
>
|
||||
<div>{formatMoney(item.amount_cent)}</div>
|
||||
<div className="mobile-wallet__sub">
|
||||
{item.withdraw_no} · 到账 {formatMoney(item.actual_amount_cent)}
|
||||
</div>
|
||||
</List.Item>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user