feat(distribution): 完善分销收号资产双视图、商户提现、自主加价、品牌装修与数据大盘
This commit is contained in:
@@ -0,0 +1,399 @@
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
Typography,
|
||||
Space,
|
||||
Select,
|
||||
Tag,
|
||||
message,
|
||||
Button,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Radio,
|
||||
Statistic,
|
||||
Row,
|
||||
Col,
|
||||
Image,
|
||||
} from 'antd'
|
||||
import {
|
||||
WalletOutlined,
|
||||
PlusOutlined,
|
||||
DollarCircleOutlined,
|
||||
LockOutlined,
|
||||
CheckCircleOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import {
|
||||
fetchSiteWallet,
|
||||
fetchSiteWithdrawals,
|
||||
applySiteWithdrawal,
|
||||
listSites,
|
||||
formatMoney,
|
||||
type SiteWallet,
|
||||
type SiteWithdrawal,
|
||||
type Site,
|
||||
} from '../api'
|
||||
|
||||
const STATUS_CONFIG: Record<string, { label: string; color: string }> = {
|
||||
pending: { label: '待审核', color: 'orange' },
|
||||
processing: { label: '处理中/审核通过', color: 'blue' },
|
||||
completed: { label: '已打款', color: 'success' },
|
||||
rejected: { label: '已拒绝', color: 'error' },
|
||||
}
|
||||
|
||||
export default function Wallet() {
|
||||
const [sites, setSites] = useState<Site[]>([])
|
||||
const [siteID, setSiteID] = useState<number>()
|
||||
const [wallet, setWallet] = useState<SiteWallet | null>(null)
|
||||
const [withdrawals, setWithdrawals] = useState<SiteWithdrawal[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const pageSize = 20
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [modalVisible, setModalVisible] = useState(false)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const loadData = useCallback(async (targetPage: number) => {
|
||||
if (!siteID) return
|
||||
setLoading(true)
|
||||
try {
|
||||
const [walletRes, wdRes] = await Promise.all([
|
||||
fetchSiteWallet(siteID),
|
||||
fetchSiteWithdrawals(siteID, targetPage, pageSize),
|
||||
])
|
||||
setWallet(walletRes)
|
||||
setWithdrawals(wdRes.items)
|
||||
setTotal(wdRes.total)
|
||||
setPage(wdRes.page)
|
||||
} catch {
|
||||
message.error('加载钱包与提现数据失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [siteID])
|
||||
|
||||
useEffect(() => {
|
||||
listSites()
|
||||
.then((rows) => {
|
||||
setSites(rows)
|
||||
if (rows.length > 0) setSiteID(rows[0].id)
|
||||
})
|
||||
.catch(() => message.error('站点加载失败'))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
void loadData(1)
|
||||
}, [loadData])
|
||||
|
||||
async function handleApplyWithdrawal(values: {
|
||||
amount: number
|
||||
account_type: 'alipay' | 'bank'
|
||||
account_name: string
|
||||
account_no: string
|
||||
bank_name?: string
|
||||
bank_branch?: string
|
||||
}) {
|
||||
if (!siteID) return
|
||||
const amountCent = Math.round(values.amount * 100)
|
||||
if (amountCent <= 0) {
|
||||
message.error('提现金额必须大于0')
|
||||
return
|
||||
}
|
||||
if (wallet && amountCent > wallet.available_balance_cent) {
|
||||
message.error('提现金额不能超过当前可提现余额')
|
||||
return
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await applySiteWithdrawal({
|
||||
site_id: siteID,
|
||||
amount_cent: amountCent,
|
||||
account_type: values.account_type,
|
||||
account_name: values.account_name,
|
||||
account_no: values.account_no,
|
||||
bank_name: values.bank_name,
|
||||
bank_branch: values.bank_branch,
|
||||
})
|
||||
message.success('提现申请提交成功,请等待主站财务审核打款')
|
||||
setModalVisible(false)
|
||||
form.resetFields()
|
||||
void loadData(1)
|
||||
} catch (error: unknown) {
|
||||
const respMessage = (error as { response?: { data?: { message?: string } } })?.response?.data?.message
|
||||
message.error(respMessage || '提现申请提交失败')
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const selectedSite = sites.find((s) => s.id === siteID)
|
||||
|
||||
return (
|
||||
<Space direction="vertical" className="page" style={{ width: '100%' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<Typography.Title level={3} style={{ marginBottom: 4 }}>
|
||||
资金管理与商户提现
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
查看分销结算收益到账情况,申请提取收益并实时追踪财务打款流水。
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Space>
|
||||
<Select
|
||||
placeholder="分销网站"
|
||||
style={{ width: 220 }}
|
||||
value={siteID}
|
||||
options={sites.map((site) => ({ label: `${site.site_name} (${site.site_code})`, value: site.id }))}
|
||||
onChange={(value) => setSiteID(value)}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
form.setFieldsValue({ account_type: 'alipay' })
|
||||
setModalVisible(true)
|
||||
}}
|
||||
disabled={!wallet || wallet.available_balance_cent <= 0}
|
||||
>
|
||||
申请提现
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={6}>
|
||||
<Card bordered={false} style={{ background: 'linear-gradient(135deg, #1677ff 0%, #0958d9 100%)', color: '#fff' }}>
|
||||
<Statistic
|
||||
title={<span style={{ color: 'rgba(255,255,255,0.85)' }}>可提现余额</span>}
|
||||
value={((wallet?.available_balance_cent ?? 0) / 100).toFixed(2)}
|
||||
prefix={<WalletOutlined style={{ color: '#fff' }} />}
|
||||
suffix="元"
|
||||
valueStyle={{ color: '#fff', fontSize: 28, fontWeight: 'bold' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card bordered={false} style={{ background: '#fafafa' }}>
|
||||
<Statistic
|
||||
title="冻结中余额 (提现审核中)"
|
||||
value={((wallet?.frozen_balance_cent ?? 0) / 100).toFixed(2)}
|
||||
prefix={<LockOutlined style={{ color: '#faad14' }} />}
|
||||
suffix="元"
|
||||
valueStyle={{ color: '#faad14' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card bordered={false} style={{ background: '#fafafa' }}>
|
||||
<Statistic
|
||||
title="累计已结算收益"
|
||||
value={((wallet?.total_settled_cent ?? 0) / 100).toFixed(2)}
|
||||
prefix={<DollarCircleOutlined style={{ color: '#52c41a' }} />}
|
||||
suffix="元"
|
||||
valueStyle={{ color: '#52c41a' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card bordered={false} style={{ background: '#fafafa' }}>
|
||||
<Statistic
|
||||
title="累计已成功提现"
|
||||
value={((wallet?.total_withdrawn_cent ?? 0) / 100).toFixed(2)}
|
||||
prefix={<CheckCircleOutlined style={{ color: '#13c2c2' }} />}
|
||||
suffix="元"
|
||||
valueStyle={{ color: '#13c2c2' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Card title="提现记录明细">
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
dataSource={withdrawals}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
showTotal: (tot) => `共 ${tot} 条提现记录`,
|
||||
onChange: (p) => void loadData(p),
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
title: '提现单号',
|
||||
dataIndex: 'withdraw_no',
|
||||
width: 170,
|
||||
render: (v: string) => <Typography.Text copyable strong>{v}</Typography.Text>,
|
||||
},
|
||||
{
|
||||
title: '提现金额',
|
||||
dataIndex: 'amount_cent',
|
||||
width: 140,
|
||||
render: (v: number) => (
|
||||
<Typography.Text strong style={{ color: '#cf1322', fontSize: 15 }}>
|
||||
{formatMoney(v)}
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '收款账户',
|
||||
width: 220,
|
||||
render: (_: unknown, row: SiteWithdrawal) => (
|
||||
<div>
|
||||
<Space size={4}>
|
||||
<Tag color={row.account_type === 'alipay' ? 'blue' : 'gold'}>
|
||||
{row.account_type === 'alipay' ? '支付宝' : '银行卡'}
|
||||
</Tag>
|
||||
<Typography.Text strong>{row.account_name}</Typography.Text>
|
||||
</Space>
|
||||
<div style={{ fontSize: 12, color: '#666', marginTop: 2 }}>{row.account_no}</div>
|
||||
{row.bank_name && (
|
||||
<div style={{ fontSize: 11, color: '#999' }}>
|
||||
{row.bank_name} {row.bank_branch}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 130,
|
||||
render: (v: string) => {
|
||||
const conf = STATUS_CONFIG[v] || { label: v, color: 'default' }
|
||||
return <Tag color={conf.color}>{conf.label}</Tag>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '审核 / 付款备注',
|
||||
minWidth: 160,
|
||||
render: (_: unknown, row: SiteWithdrawal) => (
|
||||
<div>
|
||||
{row.review_remark && (
|
||||
<div style={{ fontSize: 12 }}>
|
||||
<span style={{ color: '#888' }}>审核: </span>{row.review_remark}
|
||||
</div>
|
||||
)}
|
||||
{row.payment_remark && (
|
||||
<div style={{ fontSize: 12 }}>
|
||||
<span style={{ color: '#888' }}>打款: </span>{row.payment_remark}
|
||||
</div>
|
||||
)}
|
||||
{row.payment_proof_url && (
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<Image src={row.payment_proof_url} width={40} height={40} style={{ objectFit: 'cover', borderRadius: 4 }} />
|
||||
</div>
|
||||
)}
|
||||
{!row.review_remark && !row.payment_remark && !row.payment_proof_url && '-'}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '申请时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 160,
|
||||
render: (v: string) => <span style={{ fontSize: 12, color: '#666' }}>{v}</span>,
|
||||
},
|
||||
{
|
||||
title: '打款时间',
|
||||
dataIndex: 'paid_at',
|
||||
width: 160,
|
||||
render: (v: string | undefined) => (
|
||||
<span style={{ fontSize: 12, color: v ? '#52c41a' : '#999' }}>{v || '待打款'}</span>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title="申请提现"
|
||||
open={modalVisible}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
footer={null}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleApplyWithdrawal}>
|
||||
<Form.Item label="提现站点">
|
||||
<Input disabled value={`${selectedSite?.site_name ?? ''} (${selectedSite?.site_code ?? ''})`} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={`提现金额 (当前可提现: ${formatMoney(wallet?.available_balance_cent)})`}
|
||||
name="amount"
|
||||
rules={[{ required: true, message: '请输入提现金额' }]}
|
||||
>
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
min={0.01}
|
||||
max={((wallet?.available_balance_cent ?? 0) / 100)}
|
||||
precision={2}
|
||||
placeholder="请输入提现金额(元)"
|
||||
addonAfter={
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
form.setFieldsValue({
|
||||
amount: (wallet?.available_balance_cent ?? 0) / 100,
|
||||
})
|
||||
}}
|
||||
>
|
||||
全部提现
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="收款方式" name="account_type" rules={[{ required: true }]}>
|
||||
<Radio.Group>
|
||||
<Radio value="alipay">支付宝</Radio>
|
||||
<Radio value="bank">银行卡</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="收款人姓名" name="account_name" rules={[{ required: true, message: '请输入收款人姓名' }]}>
|
||||
<Input placeholder="真实姓名" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="收款账号" name="account_no" rules={[{ required: true, message: '请输入收款账号' }]}>
|
||||
<Input placeholder="支付宝账号 / 银行卡号" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prev, cur) => prev.account_type !== cur.account_type}
|
||||
>
|
||||
{({ getFieldValue }) =>
|
||||
getFieldValue('account_type') === 'bank' ? (
|
||||
<>
|
||||
<Form.Item label="开户银行" name="bank_name" rules={[{ required: true, message: '请输入开户行' }]}>
|
||||
<Input placeholder="例如:招商银行" />
|
||||
</Form.Item>
|
||||
<Form.Item label="支行名称" name="bank_branch">
|
||||
<Input placeholder="例如:上海徐家汇支行(可选)" />
|
||||
</Form.Item>
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
|
||||
<Space>
|
||||
<Button onClick={() => setModalVisible(false)}>取消</Button>
|
||||
<Button type="primary" htmlType="submit" loading={submitting}>
|
||||
提交提现申请
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user