积分充值全流程落地:申请审核入账、凭证上传压缩转webp、管理员调账收权
- 新增积分充值申请与审核:商户提交充值申请(凭证图片必填,1元=100积分), 管理员审核通过后事务内自动入账,幂等键防重复 - 新增通用图片上传组件 ImageUploader:canvas 压缩转 webp、预览、删除, 后端按魔数校验图片格式并持久化到 data/uploads - 管理员钱包调整改为按商户维度:移除商户侧调账接口,新增管理员 平台商户积分调整与钱包流水查看 - 低余额 Webhook 告警配置持久化,充值入账/调账后自动检查推送 - 平台商户操作列移除进入/成员,积分充值页接入真实接口并修复金额单位换算
This commit is contained in:
@@ -0,0 +1,663 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Descriptions,
|
||||
Form,
|
||||
Image,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
message,
|
||||
} from 'antd'
|
||||
import {
|
||||
BellOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
EyeOutlined,
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import { PageHeader } from '../components/PageHeader'
|
||||
import ImageUploader from '../components/ImageUploader'
|
||||
import { merchantApi, platformApi } from '../api'
|
||||
import { useAuth } from '../store/auth'
|
||||
import type { LowBalanceAlertConfig, PageResult, RechargeApplication, WalletAccount } from '../types'
|
||||
import { formatDateTime } from '../utils/time'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
export default function MerchantRecharge() {
|
||||
const { isAdmin } = useAuth()
|
||||
const [wallet, setWallet] = useState<WalletAccount | null>(null)
|
||||
const [applications, setApplications] = useState<PageResult<RechargeApplication>>({ list: [], total: 0, page: 1, size: 10 })
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [alertForm] = Form.useForm()
|
||||
const [filterForm] = Form.useForm()
|
||||
const [createForm] = Form.useForm()
|
||||
const [reviewForm] = Form.useForm()
|
||||
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
const [createSubmitting, setCreateSubmitting] = useState(false)
|
||||
const [detailItem, setDetailItem] = useState<RechargeApplication | null>(null)
|
||||
const [reviewItem, setReviewItem] = useState<RechargeApplication | null>(null)
|
||||
const [reviewSubmitting, setReviewSubmitting] = useState(false)
|
||||
|
||||
const [filterParams, setFilterParams] = useState<{ no?: string; status?: string }>({})
|
||||
|
||||
const currentPointsBalance = wallet?.available_balance
|
||||
|
||||
const filteredList = useMemo(() => {
|
||||
if (!filterParams.no) {
|
||||
return applications.list
|
||||
}
|
||||
return applications.list.filter((item) => item.application_no.includes(filterParams.no || ''))
|
||||
}, [applications.list, filterParams.no])
|
||||
|
||||
const loadApplications = useCallback(async (page = applications.page, size = applications.size, params = filterParams) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = isAdmin
|
||||
? await platformApi.rechargeApplications({ page, size, status: params.status || undefined })
|
||||
: await merchantApi.rechargeApplications({ page, size, status: params.status || undefined })
|
||||
setApplications(result)
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '加载失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [applications.page, applications.size, filterParams, isAdmin])
|
||||
|
||||
const loadAlertConfig = useCallback(async () => {
|
||||
try {
|
||||
const data = await merchantApi.alertConfig()
|
||||
alertForm.setFieldsValue(data)
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '告警配置加载失败')
|
||||
}
|
||||
}, [alertForm])
|
||||
|
||||
const loadWallet = useCallback(async () => {
|
||||
try {
|
||||
setWallet(await merchantApi.wallet())
|
||||
} catch {
|
||||
// 钱包不可用时静默降级
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
loadApplications()
|
||||
loadAlertConfig()
|
||||
loadWallet()
|
||||
}, [loadAlertConfig, loadApplications, loadWallet])
|
||||
|
||||
const handleSaveAlertConfig = async () => {
|
||||
try {
|
||||
const values = await alertForm.validateFields()
|
||||
const newConfig: LowBalanceAlertConfig = {
|
||||
enabled: values.enabled,
|
||||
threshold_points: values.threshold_points,
|
||||
webhook_url: values.webhook_url,
|
||||
}
|
||||
await merchantApi.saveAlertConfig(newConfig)
|
||||
message.success('低余额告警设置已更新')
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
message.error(e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleFilterSubmit = (values: { no?: string; status?: string }) => {
|
||||
const params = {
|
||||
no: values.no?.trim() || undefined,
|
||||
status: values.status || undefined,
|
||||
}
|
||||
setFilterParams(params)
|
||||
loadApplications(1, applications.size, params)
|
||||
}
|
||||
|
||||
const handleFilterReset = () => {
|
||||
filterForm.resetFields()
|
||||
setFilterParams({})
|
||||
loadApplications(1, applications.size, {})
|
||||
}
|
||||
|
||||
const handleCreateSubmit = async () => {
|
||||
try {
|
||||
const values = await createForm.validateFields()
|
||||
// 表单单位为元,后端 amount_cny 以分为单位(1元 = 100分 = 100积分)
|
||||
const amountCny = Math.round(Number(values.amount_cny || 0) * 100)
|
||||
|
||||
const vouchers: string[] = values.vouchers || []
|
||||
if (vouchers.length === 0) {
|
||||
message.error('请上传打款凭证截图(必填)')
|
||||
return
|
||||
}
|
||||
|
||||
setCreateSubmitting(true)
|
||||
await merchantApi.createRechargeApplication({
|
||||
amount_cny: amountCny,
|
||||
vouchers,
|
||||
note: values.note,
|
||||
})
|
||||
message.success('充值购买申请已提交,等待审核入账!')
|
||||
setCreateOpen(false)
|
||||
createForm.resetFields()
|
||||
loadApplications()
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
message.error(e.message)
|
||||
}
|
||||
} finally {
|
||||
setCreateSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleReviewSubmit = async (approved: boolean) => {
|
||||
if (!reviewItem) return
|
||||
try {
|
||||
const values = await reviewForm.validateFields()
|
||||
setReviewSubmitting(true)
|
||||
await platformApi.reviewRechargeApplication(reviewItem.id, {
|
||||
approved,
|
||||
review_note: values.review_note || undefined,
|
||||
})
|
||||
message.success(approved ? '已通过该笔充值申请,积分入账成功!' : '申请已驳回')
|
||||
setReviewItem(null)
|
||||
reviewForm.resetFields()
|
||||
loadApplications()
|
||||
loadWallet()
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '审核失败')
|
||||
} finally {
|
||||
setReviewSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const columns: ColumnsType<RechargeApplication> = [
|
||||
{
|
||||
title: '申请单号',
|
||||
dataIndex: 'application_no',
|
||||
width: 210,
|
||||
render: (v) => (
|
||||
<Text code copyable={{ tooltips: false }} style={{ fontWeight: 600 }}>
|
||||
{v}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
...(isAdmin ? [{
|
||||
title: '商户',
|
||||
dataIndex: ['merchant', 'name'],
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
render: (_: unknown, record: RechargeApplication) => record.merchant?.name || `商户#${record.merchant_id}`,
|
||||
} as ColumnsType<RechargeApplication>[number]] : []),
|
||||
{
|
||||
title: '充值金额 (元)',
|
||||
dataIndex: 'amount_cny',
|
||||
width: 140,
|
||||
render: (v) => <span style={{ fontWeight: 700, color: '#0f172a' }}>¥{(Number(v) / 100).toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</span>,
|
||||
},
|
||||
{
|
||||
title: '折算积分',
|
||||
dataIndex: 'points_amount',
|
||||
width: 150,
|
||||
render: (v) => (
|
||||
<span style={{ fontWeight: 700, color: '#16a34a', fontSize: 13.5 }}>
|
||||
+{Number(v).toLocaleString('zh-CN')} 积分
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '凭证截图',
|
||||
dataIndex: 'vouchers',
|
||||
width: 120,
|
||||
render: (vouchers: string[]) => {
|
||||
if (!vouchers || vouchers.length === 0) {
|
||||
return <span style={{ color: '#94a3b8', fontSize: 12 }}>无凭证</span>
|
||||
}
|
||||
return (
|
||||
<Image.PreviewGroup>
|
||||
<Space size={4}>
|
||||
{vouchers.map((img, idx) => (
|
||||
<Image
|
||||
key={idx}
|
||||
src={img}
|
||||
width={36}
|
||||
height={36}
|
||||
style={{ objectFit: 'cover', borderRadius: 4, border: '1px solid #e2e8f0' }}
|
||||
/>
|
||||
))}
|
||||
</Space>
|
||||
</Image.PreviewGroup>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 110,
|
||||
render: (v) => {
|
||||
if (v === 'approved') {
|
||||
return (
|
||||
<span className="status-tag status-tag--green">
|
||||
<span className="status-dot"></span>
|
||||
已通过
|
||||
</span>
|
||||
)
|
||||
}
|
||||
if (v === 'rejected') {
|
||||
return (
|
||||
<span className="status-tag status-tag--red">
|
||||
<span className="status-dot"></span>
|
||||
已驳回
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<span className="status-tag status-tag--blue">
|
||||
<span className="status-dot"></span>
|
||||
待审核
|
||||
</span>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '提交时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 170,
|
||||
render: formatDateTime,
|
||||
},
|
||||
{
|
||||
title: '审核时间',
|
||||
dataIndex: 'reviewed_at',
|
||||
width: 170,
|
||||
render: (v) => (v ? formatDateTime(v) : '-'),
|
||||
},
|
||||
{
|
||||
title: '备注说明',
|
||||
dataIndex: 'note',
|
||||
width: 160,
|
||||
ellipsis: { showTitle: false },
|
||||
render: (v) => <Text ellipsis title={v}>{v || '-'}</Text>,
|
||||
},
|
||||
{
|
||||
title: '审核说明',
|
||||
dataIndex: 'review_note',
|
||||
width: 180,
|
||||
ellipsis: { showTitle: false },
|
||||
render: (v) => <Text ellipsis title={v} style={{ color: '#475569' }}>{v || '-'}</Text>,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 150,
|
||||
fixed: 'right',
|
||||
render: (_, record) => (
|
||||
<Space size={0}>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => setDetailItem(record)}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
{isAdmin && record.status === 'pending' && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setReviewItem(record)
|
||||
reviewForm.resetFields()
|
||||
reviewForm.setFieldsValue({ review_note: '审核通过,资金真实到账并注入积分' })
|
||||
}}
|
||||
>
|
||||
审核
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title="积分购买与充值"
|
||||
subtitle="提交人民币额度充值申请(需上传打款凭证)、查看入账记录及配置低余额 Webhook 自动化告警"
|
||||
breadcrumbs={[{ title: '积分管理' }, { title: '积分充值' }]}
|
||||
extra={
|
||||
<Button icon={<ReloadOutlined />} onClick={() => loadApplications()}>
|
||||
刷新
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
||||
{/* 告警与通知配置面板 */}
|
||||
<Card
|
||||
size="small"
|
||||
style={{ background: '#ffffff', border: '1px solid #e2e8f0', borderRadius: 10 }}
|
||||
title={
|
||||
<Space size={8}>
|
||||
<BellOutlined style={{ color: '#2563eb', fontSize: 16 }} />
|
||||
<span style={{ fontWeight: 700, color: '#0f172a' }}>余额告警与 Webhook 通知设置</span>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
form={alertForm}
|
||||
layout="vertical"
|
||||
style={{ padding: '4px 8px 0' }}
|
||||
>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 24, alignItems: 'flex-start' }}>
|
||||
<div style={{ flex: '0 0 200px' }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>当前积分余额</Text>
|
||||
<div style={{ fontSize: 22, fontWeight: 800, color: '#16a34a', marginTop: 2 }}>
|
||||
{currentPointsBalance === undefined ? '-' : currentPointsBalance.toLocaleString('zh-CN')} <span style={{ fontSize: 13, color: '#64748b', fontWeight: 500 }}>积分</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
name="enabled"
|
||||
label="启用告警"
|
||||
valuePropName="checked"
|
||||
style={{ marginBottom: 12 }}
|
||||
>
|
||||
<Switch checkedChildren="开" unCheckedChildren="关" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="threshold_points"
|
||||
label="低于多少积分提醒"
|
||||
rules={[{ required: true, message: '请填写预警阈值' }]}
|
||||
style={{ marginBottom: 12, minWidth: 220 }}
|
||||
>
|
||||
<InputNumber<number>
|
||||
min={0}
|
||||
step={100000}
|
||||
addonAfter="积分"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="webhook_url"
|
||||
label="告警 Webhook (钉钉/飞书机器人)"
|
||||
rules={[{ required: true, message: '请填写 Webhook URL' }]}
|
||||
style={{ marginBottom: 12, flex: 1, minWidth: 320 }}
|
||||
>
|
||||
<Input placeholder="https://oapi.dingtalk.com/robot/send?access_token=..." allowClear />
|
||||
</Form.Item>
|
||||
|
||||
<div style={{ alignSelf: 'flex-end', marginBottom: 12 }}>
|
||||
<Button type="primary" onClick={handleSaveAlertConfig}>
|
||||
保存设置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
提示:告警机器人安全设置需包含关键词「余额」。当额度低于阈值时(充值入账/调账后触发),平台将以 JSON POST 发送通知至上述 URL。
|
||||
</Typography.Text>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
{/* 申请记录表格 Card */}
|
||||
<Card
|
||||
size="small"
|
||||
style={{ background: '#ffffff', border: '1px solid #e2e8f0', borderRadius: 10 }}
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<span style={{ fontWeight: 700, color: '#0f172a' }}>
|
||||
{isAdmin ? '全部商户充值申请记录' : '积分购买申请记录'}
|
||||
</span>
|
||||
{!isAdmin && (
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => {
|
||||
createForm.resetFields()
|
||||
setCreateOpen(true)
|
||||
}}>
|
||||
提交充值申请
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{/* 筛选表单 */}
|
||||
<div style={{ background: '#f8fafc', padding: '12px 16px', borderRadius: 8, marginBottom: 16, border: '1px solid #e2e8f0' }}>
|
||||
<Form form={filterForm} layout="inline" onFinish={handleFilterSubmit}>
|
||||
<Form.Item name="no" label="单号">
|
||||
<Input placeholder="充值单号" allowClear style={{ width: 200 }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="status" label="状态">
|
||||
<Select placeholder="全部状态" allowClear style={{ width: 120 }}>
|
||||
<Select.Option value="pending">待审核</Select.Option>
|
||||
<Select.Option value="approved">已通过</Select.Option>
|
||||
<Select.Option value="rejected">已驳回</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button onClick={handleFilterReset}>重置</Button>
|
||||
<Button type="primary" htmlType="submit">查询</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={filteredList}
|
||||
scroll={{ x: isAdmin ? 1500 : 1400 }}
|
||||
pagination={{
|
||||
current: applications.page,
|
||||
pageSize: applications.size,
|
||||
total: applications.total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条申请记录`,
|
||||
onChange: (page, size) => loadApplications(page, size),
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</Space>
|
||||
|
||||
{/* 新增充值申请 Modal */}
|
||||
<Modal
|
||||
title="提交积分购买申请"
|
||||
open={createOpen}
|
||||
onOk={handleCreateSubmit}
|
||||
onCancel={() => setCreateOpen(false)}
|
||||
destroyOnClose
|
||||
width={620}
|
||||
okText="提交审核"
|
||||
confirmLoading={createSubmitting}
|
||||
>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="充值说明"
|
||||
description="请填写实际充值金额(人民币),上传打款凭证截图(必填)。财务审核通过后,系统将按 1元 = 100积分 自动折算并实时注入商户钱包。"
|
||||
style={{ marginBottom: 20 }}
|
||||
/>
|
||||
|
||||
<Form form={createForm} layout="vertical">
|
||||
<Form.Item
|
||||
name="amount_cny"
|
||||
label="充值金额 (人民币 元)"
|
||||
rules={[
|
||||
{ required: true, message: '请输入充值金额' },
|
||||
{ type: 'number', min: 10, message: '最小充值金额为 10 元' },
|
||||
{
|
||||
validator: (_, value) => (value == null || Number.isInteger(value))
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error('请输入整数金额(元)')),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
min={10}
|
||||
step={100}
|
||||
precision={0}
|
||||
addonBefore="¥"
|
||||
addonAfter="元"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="请输入填写转账金额(整数元)"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item shouldUpdate={(prev, cur) => prev.amount_cny !== cur.amount_cny}>
|
||||
{() => {
|
||||
const amount = Number(createForm.getFieldValue('amount_cny') || 0)
|
||||
const calculatedPoints = Math.round(amount * 100)
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: '#f0fdf4',
|
||||
border: '1px solid #bbf7d0',
|
||||
borderRadius: 6,
|
||||
padding: '8px 14px',
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>预计折算入账积分:</Text>
|
||||
<span style={{ fontWeight: 800, color: '#16a34a', fontSize: 16, marginLeft: 6 }}>
|
||||
+{calculatedPoints.toLocaleString('zh-CN')} 积分
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="vouchers"
|
||||
label="打款凭证截图 (必填,最多 5 张)"
|
||||
rules={[{ required: true, message: '请上传打款凭证截图' }]}
|
||||
>
|
||||
<ImageUploader maxCount={5} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="note" label="备注 (可选)" rules={[{ max: 200 }]}>
|
||||
<Input.TextArea rows={3} placeholder="可填写打款银行账号末四位、流水号或备注说明" maxLength={200} showCount />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 查看详情 Modal */}
|
||||
<Modal
|
||||
title="充值申请详情"
|
||||
open={!!detailItem}
|
||||
onCancel={() => setDetailItem(null)}
|
||||
footer={<Button type="primary" onClick={() => setDetailItem(null)}>关闭</Button>}
|
||||
width={600}
|
||||
>
|
||||
{detailItem && (
|
||||
<Descriptions size="small" bordered column={1} style={{ marginTop: 16 }}>
|
||||
<Descriptions.Item label="申请单号">
|
||||
<Text code copyable>{detailItem.application_no}</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="商户名称">{detailItem.merchant?.name || `商户#${detailItem.merchant_id}`}</Descriptions.Item>
|
||||
<Descriptions.Item label="充值人民币">¥{(detailItem.amount_cny / 100).toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</Descriptions.Item>
|
||||
<Descriptions.Item label="折算积分">
|
||||
<span style={{ fontWeight: 700, color: '#16a34a' }}>+{detailItem.points_amount.toLocaleString('zh-CN')} 积分</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="申请状态">
|
||||
{detailItem.status === 'approved' ? (
|
||||
<Tag color="green">已通过并入账</Tag>
|
||||
) : detailItem.status === 'rejected' ? (
|
||||
<Tag color="red">已驳回</Tag>
|
||||
) : (
|
||||
<Tag color="blue">待财务审核</Tag>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="提交时间">{formatDateTime(detailItem.created_at)}</Descriptions.Item>
|
||||
<Descriptions.Item label="审核时间">{detailItem.reviewed_at ? formatDateTime(detailItem.reviewed_at) : '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="申请备注">{detailItem.note || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="审核说明">{detailItem.review_note || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="凭证截图">
|
||||
{detailItem.vouchers && detailItem.vouchers.length > 0 ? (
|
||||
<Image.PreviewGroup>
|
||||
<Space size={8} wrap>
|
||||
{detailItem.vouchers.map((img, idx) => (
|
||||
<Image key={idx} src={img} width={72} height={72} style={{ borderRadius: 6, objectFit: 'cover' }} />
|
||||
))}
|
||||
</Space>
|
||||
</Image.PreviewGroup>
|
||||
) : (
|
||||
<Text type="secondary">无凭证截图</Text>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* 管理员审核 Modal */}
|
||||
<Modal
|
||||
title="充值申请审核"
|
||||
open={!!reviewItem}
|
||||
onCancel={() => setReviewItem(null)}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={() => setReviewItem(null)}>取消</Button>,
|
||||
<Popconfirm
|
||||
key="reject"
|
||||
title="确认驳回该笔申请?"
|
||||
onConfirm={() => handleReviewSubmit(false)}
|
||||
>
|
||||
<Button danger icon={<CloseCircleOutlined />} disabled={reviewSubmitting}>驳回申请</Button>
|
||||
</Popconfirm>,
|
||||
<Button
|
||||
key="approve"
|
||||
type="primary"
|
||||
icon={<CheckCircleOutlined />}
|
||||
loading={reviewSubmitting}
|
||||
onClick={() => handleReviewSubmit(true)}
|
||||
>
|
||||
通过并自动入账
|
||||
</Button>,
|
||||
]}
|
||||
width={560}
|
||||
>
|
||||
{reviewItem && (
|
||||
<Form form={reviewForm} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Descriptions size="small" bordered column={2} style={{ marginBottom: 16 }}>
|
||||
<Descriptions.Item label="充值单号">{reviewItem.application_no}</Descriptions.Item>
|
||||
<Descriptions.Item label="人民币金额">¥{(reviewItem.amount_cny / 100).toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</Descriptions.Item>
|
||||
<Descriptions.Item label="充值积分" span={2}>
|
||||
<span style={{ fontWeight: 800, color: '#16a34a' }}>+{reviewItem.points_amount.toLocaleString('zh-CN')} 积分</span>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Form.Item label="凭证截图" style={{ marginBottom: 16 }}>
|
||||
{reviewItem.vouchers && reviewItem.vouchers.length > 0 ? (
|
||||
<Image.PreviewGroup>
|
||||
<Space size={8} wrap>
|
||||
{reviewItem.vouchers.map((img, idx) => (
|
||||
<Image key={idx} src={img} width={72} height={72} style={{ borderRadius: 6, objectFit: 'cover' }} />
|
||||
))}
|
||||
</Space>
|
||||
</Image.PreviewGroup>
|
||||
) : (
|
||||
<Text type="secondary">无凭证截图</Text>
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="review_note" label="审核说明 / 对账备注">
|
||||
<Input.TextArea rows={3} placeholder="请填写财务对账流水或审核说明" maxLength={200} showCount />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user