Files
hfb_sys/distribution-admin/src/pages/Wallet.tsx
T
yml2213andCursor 768902f2f7 feat(distribution): 补齐退款加价回退、本站自卖与开通默认规则
退款时压缩卖方加价,人工/自动批次仅完结单可入账,号主收入进主站钱包;仲裁后重算释放并支持作废未入账 DS。开通时四种模式按默认生效,对外口径合并为 35/40/25,不再单独展示通道费。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 18:13:46 +08:00

464 lines
15 KiB
TypeScript

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,
HourglassOutlined,
} from '@ant-design/icons'
import {
fetchSiteWallet,
fetchSiteWithdrawals,
applySiteWithdrawal,
listSites,
formatMoney,
getMe,
type SiteWallet,
type SiteWithdrawal,
type Site,
} from '../api'
import { useMember } from '../MemberContext'
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 member = useMember()
const [sites, setSites] = useState<Site[]>([])
const [siteID, setSiteID] = useState<number | undefined>(member?.site_id)
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(() => {
async function init() {
let currentMember = member
if (!currentMember) {
try {
currentMember = await getMe()
} catch {
// ignore
}
}
const activeSiteID = currentMember?.site_id
if (activeSiteID) {
setSiteID(activeSiteID)
}
if (currentMember?.is_superadmin) {
try {
const rows = await listSites()
setSites(rows)
if (!activeSiteID && rows.length > 0) {
setSiteID(rows[0].id)
}
} catch {
// ignore
}
}
}
void init()
}, [member])
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">
查看分销结算收益到账情况,申请提取收益并实时追踪财务打款流水。
{wallet?.settlement_hold_days != null
? ` 当前站点冷却期 ${wallet.settlement_hold_days} 天。`
: ''}
</Typography.Text>
</div>
<Space>
{member?.is_superadmin && sites.length > 1 && (
<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 flex="1">
<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 flex="1">
<Card bordered={false} style={{ background: '#fafafa' }}>
<Statistic
title="待释放收益 (冷却期内)"
value={((wallet?.pending_release_cent ?? 0) / 100).toFixed(2)}
prefix={<HourglassOutlined style={{ color: '#1677ff' }} />}
suffix="元"
valueStyle={{ color: '#1677ff' }}
/>
</Card>
</Col>
<Col flex="1">
<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 flex="1">
<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 flex="1">
<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),
}}
scroll={{ x: 1100 }}
columns={[
{
title: '提现单号',
dataIndex: 'withdraw_no',
width: 200,
render: (v: string) => (
<div style={{ whiteSpace: 'nowrap' }}>
<Typography.Text
copyable={{ text: v, tooltips: ['复制单号', '已复制'] }}
strong
style={{
fontFamily: 'monospace',
fontSize: 13,
whiteSpace: 'nowrap',
display: 'inline-flex',
alignItems: 'center',
}}
>
{v}
</Typography.Text>
</div>
),
},
{
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: '审核 / 付款备注',
width: 180,
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', whiteSpace: 'nowrap' }}>
{v ? new Date(v).toLocaleString('zh-CN', { hour12: false }) : '-'}
</span>
),
},
{
title: '打款时间',
dataIndex: 'paid_at',
width: 160,
render: (v: string | undefined) => (
<span style={{ fontSize: 12, color: v ? '#52c41a' : '#999', whiteSpace: 'nowrap' }}>
{v ? new Date(v).toLocaleString('zh-CN', { hour12: false }) : '待打款'}
</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>
)
}