Files
order_site/apps/frontend/src/pages/admin/fund/FundManagementView.tsx
T

1812 lines
62 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
CheckCircleOutlined,
PlusOutlined,
ReloadOutlined,
SwapOutlined,
WalletOutlined,
} from '@ant-design/icons'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import {
Alert,
App,
Button,
Card,
Checkbox,
Col,
DatePicker,
Descriptions,
Drawer,
Form,
Input,
InputNumber,
Modal,
Row,
Select,
Space,
Statistic,
Tabs,
Tag,
Typography,
} from 'antd'
import PercentTable from '@/components/common/PercentTable'
import type { TableColumnsType } from 'antd'
import dayjs from 'dayjs'
import type { Dayjs } from 'dayjs'
import { useState } from 'react'
import { PlatformCreditPanel } from './PlatformCreditPanel'
import ImagePreviewList from '@/components/files/ImagePreviewList'
import ImageUpload from '@/components/files/ImageUpload'
import PageHeader from '@/components/admin/PageHeader'
import {
confirmAdminFundReconciliation,
createAdminFundAccount,
createAdminFundExpense,
createAdminFundFunding,
createAdminFundIssue,
createAdminFundReconciliation,
createAdminFundReturn,
ensureAdminFundAgentAccounts,
fetchAdminFundAccountDetail,
fetchAdminFundAccounts,
fetchAdminFundExpenses,
fetchAdminFundOverview,
fetchAdminFundReconciliations,
fetchAdminFundSetupOptions,
updateAdminFundAccount,
} from '@/services/admin'
import type {
AdminFundAccount,
AdminFundEntry,
AdminFundExpense,
AdminFundExpenseCategory,
AdminFundPayeeType,
AdminFundReconciliation,
AdminFundSetupUser,
} from '@/types/admin'
import type { UploadedFile } from '@/types/worker-platform'
import { formatAdminDateTime } from '@/utils/admin-time'
import { buildAdminTablePagination } from '@/utils/admin-pagination'
import {
FUND_ACCOUNT_STATUS_LABELS,
FUND_CATEGORY_LABELS,
FUND_ENTRY_COLORS,
FUND_ENTRY_LABELS,
FUND_PAYEE_LABELS,
FUND_RECONCILIATION_STATUS_COLORS,
FUND_RECONCILIATION_STATUS_LABELS,
formatFundAmount,
fromFen,
toFen,
} from './fund-labels'
type ManagementRole = 'admin' | 'operator' | 'finance'
type IssueFormValues = {
agentAccountId: number
supervisorAccountId?: number
amountYuan: number
occurredAt: Dayjs
evidence: UploadedFile[]
note?: string
}
type FundingFormValues = {
supervisorAccountId: number
amountYuan: number
occurredAt: Dayjs
evidence: UploadedFile[]
note?: string
}
type ExpenseFormValues = {
agentAccountId: number
amountYuan: number
category: AdminFundExpenseCategory
payeeType: AdminFundPayeeType
payeeName: string
payeeAccount?: string
platformName?: string
platformAccount?: string
orderId?: number
paidAt: Dayjs
evidence: UploadedFile[]
note?: string
allowDuplicate?: boolean
}
type ReturnFormValues = {
agentAccountId: number
supervisorAccountId?: number
amountYuan: number
occurredAt: Dayjs
evidence: UploadedFile[]
note?: string
}
type ReconcileFormValues = {
accountId: number
period: [Dayjs, Dayjs]
actualBalanceYuan: number
reason?: string
evidence: UploadedFile[]
}
type ConfirmReconcileFormValues = {
reason?: string
evidence: UploadedFile[]
}
type CreateAccountFormValues = {
accountType: 'supervisor' | 'agent'
ownerAdminUserId: number
responsibleSupervisorAccountId?: number
openingBalanceYuan?: number
}
type BulkAgentsFormValues = {
responsibleSupervisorAccountId: number
openingBalanceYuan?: number
}
export default function FundManagementView({ role }: { role: ManagementRole }) {
const { message } = App.useApp()
const queryClient = useQueryClient()
const isAdmin = role === 'admin'
const canIssue = role === 'admin' || role === 'operator'
const canReconcile = role === 'admin' || role === 'operator' || role === 'finance'
const canExpense = role === 'admin'
const canManageCredits = role === 'admin' || role === 'finance'
const [activeTab, setActiveTab] = useState('overview')
const [submitting, setSubmitting] = useState(false)
const [issueOpen, setIssueOpen] = useState(false)
const [fundingOpen, setFundingOpen] = useState(false)
const [expenseOpen, setExpenseOpen] = useState(false)
const [returnOpen, setReturnOpen] = useState(false)
const [reconcileOpen, setReconcileOpen] = useState(false)
const [confirmReconcileOpen, setConfirmReconcileOpen] = useState(false)
const [createAccountOpen, setCreateAccountOpen] = useState(false)
const [bulkAgentsOpen, setBulkAgentsOpen] = useState(false)
const [detailAccount, setDetailAccount] = useState<AdminFundAccount | null>(null)
const [confirmReconciliation, setConfirmReconciliation] =
useState<AdminFundReconciliation | null>(null)
const [expensePage, setExpensePage] = useState(1)
const [expensePageSize, setExpensePageSize] = useState(20)
const [expenseKeyword, setExpenseKeyword] = useState('')
const [expenseCategory, setExpenseCategory] = useState('')
const [expenseRange, setExpenseRange] = useState<[Dayjs, Dayjs] | null>(null)
const [reconciliationPage, setReconciliationPage] = useState(1)
const [reconciliationPageSize, setReconciliationPageSize] = useState(20)
const [reconciliationStatus, setReconciliationStatus] = useState('')
const [issueForm] = Form.useForm<IssueFormValues>()
const [fundingForm] = Form.useForm<FundingFormValues>()
const [expenseForm] = Form.useForm<ExpenseFormValues>()
const [returnForm] = Form.useForm<ReturnFormValues>()
const [reconcileForm] = Form.useForm<ReconcileFormValues>()
const [confirmReconcileForm] = Form.useForm<ConfirmReconcileFormValues>()
const [createAccountForm] = Form.useForm<CreateAccountFormValues>()
const [bulkAgentsForm] = Form.useForm<BulkAgentsFormValues>()
const createAccountType = Form.useWatch('accountType', createAccountForm) || 'agent'
const overviewQuery = useQuery({
queryKey: ['admin-fund-overview'],
queryFn: fetchAdminFundOverview,
})
const agentsQuery = useQuery({
queryKey: ['admin-fund-accounts', 'agent'],
queryFn: () => fetchAdminFundAccounts({ accountType: 'agent', pageSize: 100 }),
})
const supervisorsQuery = useQuery({
queryKey: ['admin-fund-accounts', 'supervisor'],
queryFn: () => fetchAdminFundAccounts({ accountType: 'supervisor', pageSize: 100 }),
})
const expensesQuery = useQuery({
queryKey: [
'admin-fund-expenses',
expensePage,
expensePageSize,
expenseKeyword,
expenseCategory,
expenseRange?.[0]?.format('YYYY-MM-DD HH:mm:ss') || '',
expenseRange?.[1]?.format('YYYY-MM-DD HH:mm:ss') || '',
],
queryFn: () =>
fetchAdminFundExpenses({
page: expensePage,
pageSize: expensePageSize,
keyword: expenseKeyword || undefined,
category: expenseCategory || undefined,
dateFrom: expenseRange?.[0]?.format('YYYY-MM-DD HH:mm:ss'),
dateTo: expenseRange?.[1]?.format('YYYY-MM-DD HH:mm:ss'),
}),
})
const reconciliationsQuery = useQuery({
queryKey: [
'admin-fund-reconciliations',
reconciliationPage,
reconciliationPageSize,
reconciliationStatus,
],
queryFn: () =>
fetchAdminFundReconciliations({
page: reconciliationPage,
pageSize: reconciliationPageSize,
status: reconciliationStatus || undefined,
}),
})
const setupQuery = useQuery({
queryKey: ['admin-fund-options'],
queryFn: fetchAdminFundSetupOptions,
enabled: isAdmin,
})
const detailQuery = useQuery({
queryKey: ['admin-fund-account-detail', detailAccount?.id],
queryFn: () => fetchAdminFundAccountDetail(detailAccount!.id, { page: 1, pageSize: 100 }),
enabled: Boolean(detailAccount?.id),
})
const overview = overviewQuery.data?.data
const agentAccounts = agentsQuery.data?.data.items || []
const supervisorAccounts = supervisorsQuery.data?.data.items || []
const expenses = expensesQuery.data?.data.items || []
const expensePagination = expensesQuery.data?.data.pagination
const reconciliations = reconciliationsQuery.data?.data.items || []
const reconciliationPagination = reconciliationsQuery.data?.data.pagination
const setup = setupQuery.data?.data
const agentOptions = agentAccounts.map((account) => ({
value: account.id,
label: `${account.ownerName}(余额 ${formatFundAmount(account.balanceFen)})`,
}))
const supervisorOptions = supervisorAccounts.map((account) => ({
value: account.id,
label: `${account.ownerName}(余额 ${formatFundAmount(account.balanceFen)})`,
}))
function refreshAll() {
void queryClient.invalidateQueries({ queryKey: ['admin-fund-overview'] })
void queryClient.invalidateQueries({ queryKey: ['admin-fund-accounts'] })
void queryClient.invalidateQueries({ queryKey: ['admin-fund-expenses'] })
void queryClient.invalidateQueries({ queryKey: ['admin-fund-reconciliations'] })
void queryClient.invalidateQueries({ queryKey: ['admin-fund-account-detail'] })
void queryClient.invalidateQueries({ queryKey: ['admin-fund-options'] })
void queryClient.invalidateQueries({ queryKey: ['admin-fund-my'] })
}
function openIssue(account?: AdminFundAccount) {
issueForm.resetFields()
issueForm.setFieldsValue({
agentAccountId:
account?.accountType === 'agent'
? account.id
: (agentAccounts[0]?.id as number | undefined),
supervisorAccountId:
account?.accountType === 'supervisor'
? account.id
: (supervisorAccounts[0]?.id as number | undefined),
occurredAt: dayjs(),
evidence: [],
})
setIssueOpen(true)
}
function openFunding(account?: AdminFundAccount) {
fundingForm.resetFields()
fundingForm.setFieldsValue({
supervisorAccountId: account?.id || (supervisorAccounts[0]?.id as number | undefined),
occurredAt: dayjs(),
evidence: [],
})
setFundingOpen(true)
}
function openExpense(account?: AdminFundAccount) {
expenseForm.resetFields()
expenseForm.setFieldsValue({
agentAccountId:
account?.accountType === 'agent'
? account.id
: (agentAccounts[0]?.id as number | undefined),
category: 'platform_recharge',
payeeType: 'platform',
paidAt: dayjs(),
evidence: [],
allowDuplicate: false,
})
setExpenseOpen(true)
}
function openReturn(account?: AdminFundAccount) {
returnForm.resetFields()
returnForm.setFieldsValue({
agentAccountId:
account?.accountType === 'agent'
? account.id
: (agentAccounts[0]?.id as number | undefined),
supervisorAccountId:
account?.accountType === 'supervisor'
? account.id
: (supervisorAccounts[0]?.id as number | undefined),
occurredAt: dayjs(),
evidence: [],
})
setReturnOpen(true)
}
function openReconcile(account?: AdminFundAccount) {
reconcileForm.resetFields()
reconcileForm.setFieldsValue({
accountId:
account?.accountType === 'agent'
? account.id
: (agentAccounts[0]?.id as number | undefined),
period: [dayjs().subtract(7, 'day'), dayjs()],
actualBalanceYuan: account ? fromFen(account.balanceFen) : 0,
evidence: [],
})
setReconcileOpen(true)
}
function openConfirmReconcile(reconciliation: AdminFundReconciliation) {
setConfirmReconciliation(reconciliation)
confirmReconcileForm.resetFields()
confirmReconcileForm.setFieldsValue({
reason: reconciliation.reason || '',
evidence: [],
})
setConfirmReconcileOpen(true)
}
async function submitIssue(values: IssueFormValues) {
setSubmitting(true)
try {
await createAdminFundIssue(values.agentAccountId, {
agentAccountId: values.agentAccountId,
supervisorAccountId: values.supervisorAccountId,
amountFen: toFen(values.amountYuan),
occurredAt: values.occurredAt.toISOString(),
evidence: values.evidence || [],
note: values.note || '',
})
message.success('备用金已发放')
setIssueOpen(false)
refreshAll()
} catch (error) {
message.error(error instanceof Error ? error.message : '发放备用金失败')
} finally {
setSubmitting(false)
}
}
async function submitFunding(values: FundingFormValues) {
setSubmitting(true)
try {
await createAdminFundFunding(values.supervisorAccountId, {
supervisorAccountId: values.supervisorAccountId,
amountFen: toFen(values.amountYuan),
occurredAt: values.occurredAt.toISOString(),
evidence: values.evidence || [],
note: values.note || '',
})
message.success('主管资金池已拨付')
setFundingOpen(false)
refreshAll()
} catch (error) {
message.error(error instanceof Error ? error.message : '拨付失败')
} finally {
setSubmitting(false)
}
}
async function submitExpense(values: ExpenseFormValues) {
setSubmitting(true)
try {
await createAdminFundExpense(values.agentAccountId, {
amountFen: toFen(values.amountYuan),
category: values.category,
payeeType: values.payeeType,
payeeName: values.payeeName,
payeeAccount: values.payeeAccount || '',
platformName: values.platformName || '',
platformAccount: values.platformAccount || '',
orderId: values.orderId || null,
paidAt: values.paidAt.toISOString(),
evidence: values.evidence || [],
note: values.note || '',
allowDuplicate: values.allowDuplicate === true,
})
message.success('支出已登记')
setExpenseOpen(false)
refreshAll()
} catch (error) {
message.error(error instanceof Error ? error.message : '登记支出失败')
} finally {
setSubmitting(false)
}
}
async function submitReturn(values: ReturnFormValues) {
setSubmitting(true)
try {
await createAdminFundReturn(values.agentAccountId, {
agentAccountId: values.agentAccountId,
supervisorAccountId: values.supervisorAccountId,
amountFen: toFen(values.amountYuan),
occurredAt: values.occurredAt.toISOString(),
evidence: values.evidence || [],
note: values.note || '',
})
message.success('备用金已缴回')
setReturnOpen(false)
refreshAll()
} catch (error) {
message.error(error instanceof Error ? error.message : '缴回失败')
} finally {
setSubmitting(false)
}
}
async function submitReconcile(values: ReconcileFormValues) {
setSubmitting(true)
try {
await createAdminFundReconciliation({
accountId: values.accountId,
periodStart: values.period[0].toISOString(),
periodEnd: values.period[1].toISOString(),
actualBalanceFen: toFen(values.actualBalanceYuan),
reason: values.reason || '',
evidence: values.evidence || [],
})
message.success('对账已发起')
setReconcileOpen(false)
refreshAll()
} catch (error) {
message.error(error instanceof Error ? error.message : '发起对账失败')
} finally {
setSubmitting(false)
}
}
async function submitConfirmReconcile(values: ConfirmReconcileFormValues) {
if (!confirmReconciliation) return
setSubmitting(true)
try {
await confirmAdminFundReconciliation(confirmReconciliation.id, {
reason: values.reason || '',
evidence: values.evidence || [],
})
message.success('对账已确认')
setConfirmReconcileOpen(false)
setConfirmReconciliation(null)
refreshAll()
} catch (error) {
message.error(error instanceof Error ? error.message : '确认对账失败')
} finally {
setSubmitting(false)
}
}
async function submitCreateAccount(values: CreateAccountFormValues) {
setSubmitting(true)
try {
await createAdminFundAccount({
accountType: values.accountType,
ownerAdminUserId: values.ownerAdminUserId,
responsibleSupervisorAccountId: values.responsibleSupervisorAccountId,
openingBalanceFen: toFen(values.openingBalanceYuan || 0),
})
message.success('备用金账户已创建')
setCreateAccountOpen(false)
refreshAll()
} catch (error) {
message.error(error instanceof Error ? error.message : '创建账户失败')
} finally {
setSubmitting(false)
}
}
async function submitBulkAgents(values: BulkAgentsFormValues) {
setSubmitting(true)
try {
const result = await ensureAdminFundAgentAccounts({
responsibleSupervisorAccountId: values.responsibleSupervisorAccountId,
openingBalanceFen: toFen(values.openingBalanceYuan || 0),
})
message.success(`已创建 ${result.data.createdCount} 个客服账户`)
setBulkAgentsOpen(false)
refreshAll()
} catch (error) {
message.error(error instanceof Error ? error.message : '批量初始化失败')
} finally {
setSubmitting(false)
}
}
async function toggleAccountStatus(account: AdminFundAccount) {
setSubmitting(true)
try {
await updateAdminFundAccount(account.id, {
status: account.status === 'active' ? 'frozen' : 'active',
})
message.success(account.status === 'active' ? '账户已冻结' : '账户已启用')
refreshAll()
} catch (error) {
message.error(error instanceof Error ? error.message : '更新账户状态失败')
} finally {
setSubmitting(false)
}
}
function buildAccountColumns(type: 'agent' | 'supervisor'): TableColumnsType<AdminFundAccount> {
return [
{ title: '账户编号', dataIndex: 'accountNo', width: 150 },
{ title: '姓名', dataIndex: 'ownerName', width: 120 },
...(type === 'agent'
? [
{
title: '责任主管',
dataIndex: 'responsibleSupervisorName',
width: 120,
render: (value: string) => value || '-',
},
]
: []),
{
title: '当前余额',
dataIndex: 'balanceFen',
width: 130,
render: (value: number) => (
<Typography.Text strong>{formatFundAmount(value)}</Typography.Text>
),
},
{
title: '本月流入',
dataIndex: 'monthInFen',
width: 120,
render: (value: number) => formatFundAmount(value),
},
{
title: '本月流出',
dataIndex: 'monthOutFen',
width: 120,
render: (value: number) => formatFundAmount(value),
},
{
title: '最后流水',
dataIndex: 'lastEntryAt',
width: 170,
render: (value: string | null) => (value ? formatAdminDateTime(value) : '-'),
},
{
title: '最后对账',
dataIndex: 'lastReconciledAt',
width: 170,
render: (value: string | null) => (value ? formatAdminDateTime(value) : '-'),
},
{
title: '状态',
dataIndex: 'status',
width: 90,
render: (value: string) => (
<Tag color={value === 'active' ? 'green' : value === 'frozen' ? 'orange' : 'default'}>
{FUND_ACCOUNT_STATUS_LABELS[value] || value}
</Tag>
),
},
{
title: '操作',
key: 'actions',
width: 260,
fixed: 'right',
render: (_value, record) => (
<Space size={4} wrap>
<Button size="small" onClick={() => setDetailAccount(record)}>
详情
</Button>
{isAdmin && type === 'supervisor' ? (
<Button size="small" onClick={() => openFunding(record)}>
拨付
</Button>
) : null}
{canIssue ? (
<Button size="small" onClick={() => openIssue(record)}>
发放
</Button>
) : null}
{type === 'agent' && canIssue ? (
<Button size="small" onClick={() => openReturn(record)}>
缴回
</Button>
) : null}
{type === 'agent' && canReconcile ? (
<Button size="small" onClick={() => openReconcile(record)}>
对账
</Button>
) : null}
{isAdmin ? (
<Button
size="small"
danger={record.status === 'active'}
onClick={() => toggleAccountStatus(record)}
>
{record.status === 'active' ? '冻结' : '启用'}
</Button>
) : null}
</Space>
),
},
]
}
const expenseColumns: TableColumnsType<AdminFundExpense> = [
{
title: '付款时间',
dataIndex: 'paidAt',
width: 170,
render: (value: string) => formatAdminDateTime(value),
},
{ title: '支出单号', dataIndex: 'expenseNo', width: 150 },
{ title: '客服', dataIndex: 'agentName', width: 110 },
{
title: '分类',
dataIndex: 'category',
width: 110,
render: (value: AdminFundExpenseCategory) => FUND_CATEGORY_LABELS[value] || value,
},
{
title: '收款方',
dataIndex: 'payeeName',
width: 180,
render: (_value: string, row) => (
<Space direction="vertical" size={0}>
<span>{row.payeeName}</span>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{FUND_PAYEE_LABELS[row.payeeType]}
{row.payeeAccount ? ` · ${row.payeeAccount}` : ''}
</Typography.Text>
</Space>
),
},
{
title: '平台/账号',
dataIndex: 'platformName',
width: 180,
render: (_value: string, row) =>
row.platformName || row.platformAccount
? `${row.platformName || '-'}${row.platformAccount ? ` / ${row.platformAccount}` : ''}`
: '-',
},
{
title: '关联订单',
dataIndex: 'orderId',
width: 100,
render: (value: number | null) => (value ? `#${value}` : '-'),
},
{
title: '金额',
dataIndex: 'amountFen',
width: 120,
render: (value: number) => (
<Typography.Text type="danger">{formatFundAmount(value)}</Typography.Text>
),
},
{
title: '凭证',
dataIndex: 'evidence',
width: 100,
render: (value: UploadedFile[]) => (
<ImagePreviewList files={value} size={36} maxVisible={2} />
),
},
{ title: '说明', dataIndex: 'note', render: (value: string) => value || '-' },
]
const reconciliationColumns: TableColumnsType<AdminFundReconciliation> = [
{
title: '对账区间',
dataIndex: 'periodStart',
width: 230,
render: (_value: string, row) =>
`${formatAdminDateTime(row.periodStart)} ~ ${formatAdminDateTime(row.periodEnd)}`,
},
{ title: '对账单号', dataIndex: 'reconciliationNo', width: 150 },
{ title: '客服', dataIndex: 'agentName', width: 110 },
{
title: '系统余额',
dataIndex: 'systemBalanceFen',
width: 120,
render: (value: number) => formatFundAmount(value),
},
{
title: '实际余额',
dataIndex: 'actualBalanceFen',
width: 120,
render: (value: number) => formatFundAmount(value),
},
{
title: '差异',
dataIndex: 'differenceFen',
width: 120,
render: (value: number) => (
<Typography.Text type={value === 0 ? 'success' : 'danger'}>
{formatFundAmount(value, true)}
</Typography.Text>
),
},
{
title: '状态',
dataIndex: 'status',
width: 100,
render: (value: AdminFundReconciliation['status']) => (
<Tag color={FUND_RECONCILIATION_STATUS_COLORS[value]}>
{FUND_RECONCILIATION_STATUS_LABELS[value]}
</Tag>
),
},
{
title: '原因',
dataIndex: 'reason',
render: (value: string) => value || '-',
},
{
title: '确认人',
dataIndex: 'confirmedByUsername',
width: 110,
render: (value: string, row) =>
value
? `${value}${row.confirmedAt ? ` · ${formatAdminDateTime(row.confirmedAt)}` : ''}`
: '-',
},
{
title: '操作',
key: 'actions',
width: 100,
fixed: 'right',
render: (_value, row) =>
row.status === 'confirmed' ? (
<Tag color="green">已确认</Tag>
) : (
<Button size="small" type="primary" onClick={() => openConfirmReconcile(row)}>
确认
</Button>
),
},
]
const detailEntries = detailQuery.data?.data.items || []
const detailEntryColumns: TableColumnsType<AdminFundEntry> = [
{
title: '时间',
dataIndex: 'occurredAt',
width: 170,
render: (value: string) => formatAdminDateTime(value),
},
{
title: '类型',
dataIndex: 'entryType',
width: 110,
render: (value: AdminFundEntry['entryType']) => (
<Tag color={FUND_ENTRY_COLORS[value]}>{FUND_ENTRY_LABELS[value]}</Tag>
),
},
{
title: '金额',
dataIndex: 'amountFen',
width: 120,
render: (value: number) => (
<Typography.Text type={value >= 0 ? 'success' : 'danger'}>
{formatFundAmount(value, true)}
</Typography.Text>
),
},
{
title: '余额',
dataIndex: 'balanceAfterFen',
width: 120,
render: (value: number) => formatFundAmount(value),
},
{
title: '对方账户',
dataIndex: 'counterpartyName',
width: 130,
render: (value: string) => value || '-',
},
{
title: '凭证',
dataIndex: 'evidence',
width: 100,
render: (value: UploadedFile[]) => (
<ImagePreviewList files={value} size={36} maxVisible={2} />
),
},
{ title: '说明', dataIndex: 'note', render: (value: string) => value || '-' },
]
const setupSupportUsersWithoutAccount =
setup?.users.filter(
(user) =>
user.role === 'support' &&
!setup.agentAccounts.some((account) => account.ownerAdminUserId === user.id),
) || []
const setupSupervisorUsersWithoutAccount =
setup?.users.filter(
(user) =>
(user.role === 'operator' || user.role === 'admin') &&
!setup.supervisorAccounts.some((account) => account.ownerAdminUserId === user.id),
) || []
function openCreateAccount(prefill?: Partial<CreateAccountFormValues>) {
createAccountForm.resetFields()
createAccountForm.setFieldsValue({
accountType: 'agent',
openingBalanceYuan: 0,
...prefill,
})
setCreateAccountOpen(true)
}
function openBulkAgents() {
bulkAgentsForm.resetFields()
bulkAgentsForm.setFieldsValue({
responsibleSupervisorAccountId: supervisorAccounts[0]?.id,
openingBalanceYuan: 0,
})
setBulkAgentsOpen(true)
}
const createAccountUserOptions =
createAccountType === 'supervisor'
? (setup?.users || []).filter(
(user) =>
(user.role === 'operator' || user.role === 'admin') &&
!setup?.supervisorAccounts.some((account) => account.ownerAdminUserId === user.id),
)
: (setup?.users || []).filter(
(user) =>
user.role === 'support' &&
!setup?.agentAccounts.some((account) => account.ownerAdminUserId === user.id),
)
return (
<section className="page-stack">
<PageHeader
title="钱包管理"
description="统管客服的南航 / 乐玩 / ROCK 积分钱包与备用金现金;可分配积分、发放现金、查看流水与对账。"
extra={
<Button icon={<ReloadOutlined />} loading={overviewQuery.isFetching} onClick={refreshAll}>
刷新
</Button>
}
/>
{overviewQuery.error ? (
<Alert
type="error"
showIcon
message={
overviewQuery.error instanceof Error
? overviewQuery.error.message
: '读取备用金总览失败'
}
/>
) : null}
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
items={[
{
key: 'overview',
label: '钱包总览',
children: (
<Space direction="vertical" size={16} style={{ width: '100%' }}>
<Row gutter={[16, 16]}>
<Col xs={24} sm={12} lg={6}>
<Card>
<Statistic
title="备用金总额"
value={(overview?.totalBalanceFen || 0) / 100}
precision={2}
prefix="¥"
suffix={<WalletOutlined />}
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card>
<Statistic
title="主管池余额"
value={(overview?.supervisorBalanceFen || 0) / 100}
precision={2}
prefix="¥"
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card>
<Statistic
title="客服余额"
value={(overview?.agentBalanceFen || 0) / 100}
precision={2}
prefix="¥"
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card>
<Statistic
title="本月支出"
value={(overview?.monthExpenseFen || 0) / 100}
precision={2}
prefix="¥"
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card>
<Statistic
title="本月发放"
value={(overview?.monthIssuedFen || 0) / 100}
precision={2}
prefix="¥"
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card>
<Statistic
title="本月缴回"
value={(overview?.monthReturnedFen || 0) / 100}
precision={2}
prefix="¥"
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card>
<Statistic
title="待对账"
value={overview?.pendingReconciliationCount || 0}
suffix="笔"
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card>
<Statistic
title="对账差异"
value={overview?.discrepancyCount || 0}
suffix="笔"
valueStyle={{
color: (overview?.discrepancyCount || 0) > 0 ? '#cf1322' : undefined,
}}
/>
</Card>
</Col>
</Row>
<Card title="快捷操作">
<Space wrap>
{canIssue ? (
<Button type="primary" icon={<SwapOutlined />} onClick={() => openIssue()}>
发放备用金
</Button>
) : null}
{canManageCredits ? (
<Button
icon={<WalletOutlined />}
onClick={() => setActiveTab('platform-credits')}
>
分配客服积分
</Button>
) : null}
{canExpense ? (
<Button icon={<PlusOutlined />} onClick={() => openExpense()}>
登记支出
</Button>
) : null}
{canReconcile ? (
<Button
icon={<CheckCircleOutlined />}
onClick={() => setActiveTab('reconciliations')}
>
对账中心
</Button>
) : null}
{isAdmin ? <Button onClick={() => openFunding()}>公司拨付</Button> : null}
</Space>
</Card>
{(overview?.unreconciledAgentCount || 0) > 0 ? (
<Alert
type="warning"
showIcon
message={`有 ${overview?.unreconciledAgentCount} 个客服超过 7 天未对账`}
description="请在“对账”页签发起对账,确认系统余额与实际余额是否一致。"
/>
) : null}
</Space>
),
},
{
key: 'agents',
label: '客服现金钱包',
children: (
<Card
title="客服备用金账户"
extra={
canIssue ? (
<Button type="primary" onClick={() => openIssue()}>
发放备用金
</Button>
) : null
}
>
<PercentTable<AdminFundAccount>
rowKey="id"
size="small"
columns={buildAccountColumns('agent')}
dataSource={agentAccounts}
loading={agentsQuery.isLoading}
pagination={false}
scroll={{ x: 1250 }}
/>
</Card>
),
},
{
key: 'supervisors',
label: '现金资金池',
children: (
<Card
title="主管资金池"
extra={
isAdmin ? (
<Button type="primary" onClick={() => openFunding()}>
公司拨付
</Button>
) : null
}
>
<PercentTable<AdminFundAccount>
rowKey="id"
size="small"
columns={buildAccountColumns('supervisor')}
dataSource={supervisorAccounts}
loading={supervisorsQuery.isLoading}
pagination={false}
scroll={{ x: 1150 }}
/>
</Card>
),
},
{
key: 'platform-credits',
label: '客服积分钱包',
children: <PlatformCreditPanel />,
},
{
key: 'expenses',
label: '现金支出',
children: (
<Card
title="备用金支出"
extra={
canExpense ? (
<Button type="primary" onClick={() => openExpense()}>
登记支出
</Button>
) : null
}
>
<Space wrap style={{ marginBottom: 16 }}>
<Input
allowClear
placeholder="支出单号/收款方/平台/客服/订单"
value={expenseKeyword}
onChange={(event) => {
setExpensePage(1)
setExpenseKeyword(event.target.value)
}}
style={{ width: 260 }}
/>
<Select
allowClear
placeholder="支出分类"
value={expenseCategory || undefined}
onChange={(value) => {
setExpensePage(1)
setExpenseCategory(value || '')
}}
style={{ width: 140 }}
options={Object.entries(FUND_CATEGORY_LABELS).map(([value, label]) => ({
value,
label,
}))}
/>
<DatePicker.RangePicker
showTime
value={expenseRange}
onChange={(value) => {
setExpensePage(1)
setExpenseRange(value as [Dayjs, Dayjs] | null)
}}
/>
</Space>
<PercentTable<AdminFundExpense>
rowKey="id"
size="small"
columns={expenseColumns}
dataSource={expenses}
loading={expensesQuery.isLoading}
pagination={buildAdminTablePagination({
current: expensePage,
pageSize: expensePageSize,
total: expensePagination?.total || 0,
onChange: (page, pageSize) => {
setExpensePage(page || 1)
setExpensePageSize(pageSize || 20)
},
})}
scroll={{ x: 1250 }}
/>
</Card>
),
},
{
key: 'reconciliations',
label: '对账中心',
children: (
<Card
title="现金对账"
extra={
canReconcile ? (
<Button type="primary" onClick={() => openReconcile()}>
发起对账
</Button>
) : null
}
>
<Space wrap style={{ marginBottom: 16 }}>
<Select
allowClear
placeholder="对账状态"
value={reconciliationStatus || undefined}
onChange={(value) => {
setReconciliationPage(1)
setReconciliationStatus(value || '')
}}
style={{ width: 140 }}
options={Object.entries(FUND_RECONCILIATION_STATUS_LABELS).map(
([value, label]) => ({ value, label }),
)}
/>
</Space>
<PercentTable<AdminFundReconciliation>
rowKey="id"
size="small"
columns={reconciliationColumns}
dataSource={reconciliations}
loading={reconciliationsQuery.isLoading}
pagination={buildAdminTablePagination({
current: reconciliationPage,
pageSize: reconciliationPageSize,
total: reconciliationPagination?.total || 0,
onChange: (page, pageSize) => {
setReconciliationPage(page || 1)
setReconciliationPageSize(pageSize || 20)
},
})}
scroll={{ x: 1250 }}
/>
</Card>
),
},
...(isAdmin
? [
{
key: 'setup',
label: '账户初始化',
children: (
<Space direction="vertical" size={16} style={{ width: '100%' }}>
<Card
title="主管资金池"
extra={
<Space>
<Button
onClick={() => openCreateAccount({ accountType: 'supervisor' })}
>
新增主管账户
</Button>
</Space>
}
>
<PercentTable<AdminFundAccount>
rowKey="id"
size="small"
columns={buildAccountColumns('supervisor')}
dataSource={setup?.supervisorAccounts || []}
loading={setupQuery.isLoading}
pagination={false}
scroll={{ x: 1150 }}
/>
{setupSupervisorUsersWithoutAccount.length > 0 ? (
<Alert
style={{ marginTop: 12 }}
type="info"
showIcon
message={`还有 ${setupSupervisorUsersWithoutAccount.length} 个运营/管理员未开通主管资金池`}
description={setupSupervisorUsersWithoutAccount
.map((user) => user.displayName || user.username)
.join('、')}
/>
) : null}
</Card>
<Card
title="客服备用金账户"
extra={
<Space>
<Button onClick={() => openCreateAccount({ accountType: 'agent' })}>
新增客服账户
</Button>
<Button type="primary" onClick={openBulkAgents}>
批量初始化客服
</Button>
</Space>
}
>
<PercentTable<AdminFundAccount>
rowKey="id"
size="small"
columns={buildAccountColumns('agent')}
dataSource={setup?.agentAccounts || []}
loading={setupQuery.isLoading}
pagination={false}
scroll={{ x: 1250 }}
/>
{setupSupportUsersWithoutAccount.length > 0 ? (
<Alert
style={{ marginTop: 12 }}
type="warning"
showIcon
message={`还有 ${setupSupportUsersWithoutAccount.length} 个客服未开通备用金账户`}
description={setupSupportUsersWithoutAccount
.map((user) => user.displayName || user.username)
.join('、')}
/>
) : null}
</Card>
</Space>
),
},
]
: []),
]}
/>
<Modal
title="发放备用金"
open={issueOpen}
onCancel={() => setIssueOpen(false)}
onOk={() => issueForm.submit()}
confirmLoading={submitting}
destroyOnHidden
width={720}
>
<Form<IssueFormValues> form={issueForm} layout="vertical" onFinish={submitIssue}>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="agentAccountId"
label="客服备用金账户"
rules={[{ required: true, message: '请选择客服账户' }]}
>
<Select options={agentOptions} showSearch optionFilterProp="label" />
</Form.Item>
</Col>
{isAdmin ? (
<Col span={12}>
<Form.Item
name="supervisorAccountId"
label="发放主管"
rules={[{ required: true, message: '请选择主管资金池' }]}
>
<Select options={supervisorOptions} showSearch optionFilterProp="label" />
</Form.Item>
</Col>
) : null}
<Col span={12}>
<Form.Item
name="amountYuan"
label="发放金额(元)"
rules={[{ required: true, message: '请输入金额' }]}
>
<InputNumber min={0.01} precision={2} style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="occurredAt"
label="发放时间"
rules={[{ required: true, message: '请选择时间' }]}
>
<DatePicker showTime style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={24}>
<Form.Item name="evidence" label="转账/交接凭证">
<ImageUpload scene="admin_fund_issue" scope="admin" maxCount={4} />
</Form.Item>
</Col>
<Col span={24}>
<Form.Item name="note" label="备注">
<Input.TextArea rows={2} placeholder="发放说明;无凭证时请填写交接说明" />
</Form.Item>
</Col>
</Row>
</Form>
</Modal>
<Modal
title="公司拨付主管资金池"
open={fundingOpen}
onCancel={() => setFundingOpen(false)}
onOk={() => fundingForm.submit()}
confirmLoading={submitting}
destroyOnHidden
>
<Form<FundingFormValues> form={fundingForm} layout="vertical" onFinish={submitFunding}>
<Form.Item
name="supervisorAccountId"
label="主管资金池"
rules={[{ required: true, message: '请选择主管资金池' }]}
>
<Select options={supervisorOptions} showSearch optionFilterProp="label" />
</Form.Item>
<Form.Item
name="amountYuan"
label="拨付金额(元)"
rules={[{ required: true, message: '请输入金额' }]}
>
<InputNumber min={0.01} precision={2} style={{ width: '100%' }} />
</Form.Item>
<Form.Item
name="occurredAt"
label="拨付时间"
rules={[{ required: true, message: '请选择时间' }]}
>
<DatePicker showTime style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="evidence" label="转账/交接凭证">
<ImageUpload scene="admin_fund_funding" scope="admin" maxCount={4} />
</Form.Item>
<Form.Item name="note" label="备注">
<Input.TextArea rows={2} placeholder="公司拨付说明" />
</Form.Item>
</Form>
</Modal>
<Modal
title="登记备用金支出"
open={expenseOpen}
onCancel={() => setExpenseOpen(false)}
onOk={() => expenseForm.submit()}
confirmLoading={submitting}
destroyOnHidden
width={760}
>
<Form<ExpenseFormValues> form={expenseForm} layout="vertical" onFinish={submitExpense}>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="agentAccountId"
label="客服备用金账户"
rules={[{ required: true, message: '请选择客服账户' }]}
>
<Select options={agentOptions} showSearch optionFilterProp="label" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="amountYuan"
label="支出金额(元)"
rules={[{ required: true, message: '请输入金额' }]}
>
<InputNumber min={0.01} precision={2} style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="category"
label="支出分类"
rules={[{ required: true, message: '请选择分类' }]}
>
<Select
options={Object.entries(FUND_CATEGORY_LABELS).map(([value, label]) => ({
value,
label,
}))}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="payeeType"
label="收款方类型"
rules={[{ required: true, message: '请选择收款方类型' }]}
>
<Select
options={Object.entries(FUND_PAYEE_LABELS).map(([value, label]) => ({
value,
label,
}))}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="payeeName"
label="收款方名称"
rules={[{ required: true, message: '请填写收款方' }]}
>
<Input placeholder="平台/供应商/客户名称" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="payeeAccount" label="收款账号">
<Input placeholder="微信号/支付宝/银行卡/平台账号" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="platformName" label="充值/消费平台">
<Input placeholder="例如:91卡券、快手、游戏平台" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="platformAccount" label="平台账号">
<Input placeholder="充值时填写" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="orderId" label="关联订单 ID(可选)">
<InputNumber min={1} precision={0} style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="paidAt"
label="付款时间"
rules={[{ required: true, message: '请选择付款时间' }]}
>
<DatePicker showTime style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="evidence"
label="付款凭证"
rules={[{ required: true, message: '请上传付款截图' }]}
>
<ImageUpload scene="admin_fund_expense" scope="admin" maxCount={6} showPasteHint />
</Form.Item>
</Col>
<Col span={24}>
<Form.Item name="note" label="用途/原因">
<Input.TextArea rows={2} placeholder="例如:给订单充值、赔偿客户、补差价" />
</Form.Item>
</Col>
<Col span={24}>
<Form.Item name="allowDuplicate" valuePropName="checked">
<Checkbox>我已确认这不是重复支出(同一客服、金额、收款方 24 小时内)</Checkbox>
</Form.Item>
</Col>
</Row>
</Form>
</Modal>
<Modal
title="缴回备用金"
open={returnOpen}
onCancel={() => setReturnOpen(false)}
onOk={() => returnForm.submit()}
confirmLoading={submitting}
destroyOnHidden
width={680}
>
<Form<ReturnFormValues> form={returnForm} layout="vertical" onFinish={submitReturn}>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="agentAccountId"
label="客服备用金账户"
rules={[{ required: true, message: '请选择客服账户' }]}
>
<Select options={agentOptions} showSearch optionFilterProp="label" />
</Form.Item>
</Col>
{isAdmin ? (
<Col span={12}>
<Form.Item name="supervisorAccountId" label="收回主管">
<Select
allowClear
options={supervisorOptions}
showSearch
optionFilterProp="label"
placeholder="默认该客服责任主管"
/>
</Form.Item>
</Col>
) : null}
<Col span={12}>
<Form.Item
name="amountYuan"
label="缴回金额(元)"
rules={[{ required: true, message: '请输入金额' }]}
>
<InputNumber min={0.01} precision={2} style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="occurredAt"
label="缴回时间"
rules={[{ required: true, message: '请选择时间' }]}
>
<DatePicker showTime style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={24}>
<Form.Item name="evidence" label="转账/交接凭证">
<ImageUpload scene="admin_fund_return" scope="admin" maxCount={4} />
</Form.Item>
</Col>
<Col span={24}>
<Form.Item name="note" label="备注">
<Input.TextArea rows={2} placeholder="缴回原因或交接说明" />
</Form.Item>
</Col>
</Row>
</Form>
</Modal>
<Modal
title="发起备用金对账"
open={reconcileOpen}
onCancel={() => setReconcileOpen(false)}
onOk={() => reconcileForm.submit()}
confirmLoading={submitting}
destroyOnHidden
width={680}
>
<Form<ReconcileFormValues>
form={reconcileForm}
layout="vertical"
onFinish={submitReconcile}
>
<Form.Item
name="accountId"
label="客服备用金账户"
rules={[{ required: true, message: '请选择账户' }]}
>
<Select options={agentOptions} showSearch optionFilterProp="label" />
</Form.Item>
<Form.Item
name="period"
label="对账区间"
rules={[{ required: true, message: '请选择对账区间' }]}
>
<DatePicker.RangePicker showTime style={{ width: '100%' }} />
</Form.Item>
<Form.Item
name="actualBalanceYuan"
label="实际余额(元)"
rules={[{ required: true, message: '请输入实际余额' }]}
>
<InputNumber min={0} precision={2} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="reason" label="差异原因">
<Input.TextArea rows={2} placeholder="有差异时必填;无差异可留空" />
</Form.Item>
<Form.Item name="evidence" label="盘点/差异凭证">
<ImageUpload scene="admin_fund_reconcile" scope="admin" maxCount={4} />
</Form.Item>
</Form>
</Modal>
<Modal
title="确认对账"
open={confirmReconcileOpen}
onCancel={() => {
setConfirmReconcileOpen(false)
setConfirmReconciliation(null)
}}
onOk={() => confirmReconcileForm.submit()}
confirmLoading={submitting}
destroyOnHidden
>
{confirmReconciliation ? (
<Descriptions column={1} size="small" style={{ marginBottom: 16 }}>
<Descriptions.Item label="客服">{confirmReconciliation.agentName}</Descriptions.Item>
<Descriptions.Item label="系统余额">
{formatFundAmount(confirmReconciliation.systemBalanceFen)}
</Descriptions.Item>
<Descriptions.Item label="实际余额">
{formatFundAmount(confirmReconciliation.actualBalanceFen)}
</Descriptions.Item>
<Descriptions.Item label="差异">
<Typography.Text
type={confirmReconciliation.differenceFen === 0 ? 'success' : 'danger'}
>
{formatFundAmount(confirmReconciliation.differenceFen, true)}
</Typography.Text>
</Descriptions.Item>
</Descriptions>
) : null}
<Form<ConfirmReconcileFormValues>
form={confirmReconcileForm}
layout="vertical"
onFinish={submitConfirmReconcile}
>
<Form.Item name="reason" label="差异原因/确认说明">
<Input.TextArea rows={2} placeholder="有差异时必填,确认后生成调整流水" />
</Form.Item>
<Form.Item name="evidence" label="差异凭证">
<ImageUpload scene="admin_fund_reconcile_confirm" scope="admin" maxCount={4} />
</Form.Item>
</Form>
</Modal>
<Modal
title="新增备用金账户"
open={createAccountOpen}
onCancel={() => setCreateAccountOpen(false)}
onOk={() => createAccountForm.submit()}
confirmLoading={submitting}
destroyOnHidden
>
<Form<CreateAccountFormValues>
form={createAccountForm}
layout="vertical"
onFinish={submitCreateAccount}
>
<Form.Item
name="accountType"
label="账户类型"
rules={[{ required: true, message: '请选择账户类型' }]}
>
<Select
options={[
{ value: 'supervisor', label: '主管资金池' },
{ value: 'agent', label: '客服备用金账户' },
]}
onChange={() => createAccountForm.setFieldsValue({ ownerAdminUserId: undefined })}
/>
</Form.Item>
<Form.Item
name="ownerAdminUserId"
label={createAccountType === 'supervisor' ? '主管账号' : '客服账号'}
rules={[{ required: true, message: '请选择账号' }]}
>
<Select
showSearch
optionFilterProp="label"
options={createAccountUserOptions.map((user: AdminFundSetupUser) => ({
value: user.id,
label: `${user.displayName || user.username}(${user.username} · ${user.role})`,
}))}
/>
</Form.Item>
{createAccountType === 'agent' ? (
<Form.Item
name="responsibleSupervisorAccountId"
label="责任主管"
rules={[{ required: true, message: '请选择责任主管' }]}
>
<Select options={supervisorOptions} showSearch optionFilterProp="label" />
</Form.Item>
) : null}
<Form.Item name="openingBalanceYuan" label="期初余额(元)">
<InputNumber min={0} precision={2} style={{ width: '100%' }} />
</Form.Item>
</Form>
</Modal>
<Modal
title="批量初始化客服备用金账户"
open={bulkAgentsOpen}
onCancel={() => setBulkAgentsOpen(false)}
onOk={() => bulkAgentsForm.submit()}
confirmLoading={submitting}
destroyOnHidden
>
<Alert
type="info"
showIcon
style={{ marginBottom: 16 }}
message="会给所有未开户的 support 角色客服创建账户"
description="已开户的客服不会被覆盖;期初余额由主管确认后填写。"
/>
<Form<BulkAgentsFormValues>
form={bulkAgentsForm}
layout="vertical"
onFinish={submitBulkAgents}
>
<Form.Item
name="responsibleSupervisorAccountId"
label="责任主管"
rules={[{ required: true, message: '请选择责任主管' }]}
>
<Select options={supervisorOptions} showSearch optionFilterProp="label" />
</Form.Item>
<Form.Item name="openingBalanceYuan" label="统一期初余额(元)">
<InputNumber min={0} precision={2} style={{ width: '100%' }} />
</Form.Item>
</Form>
</Modal>
<Drawer
title={detailAccount ? `备用金账户 · ${detailAccount.ownerName}` : '备用金账户'}
open={Boolean(detailAccount)}
onClose={() => setDetailAccount(null)}
width={1000}
destroyOnHidden
>
{detailQuery.data?.data.account ? (
<>
<Descriptions column={2} size="small" bordered>
<Descriptions.Item label="账户编号">
{detailQuery.data.data.account.accountNo}
</Descriptions.Item>
<Descriptions.Item label="类型">
{detailQuery.data.data.account.accountType === 'supervisor'
? '主管资金池'
: '客服备用金'}
</Descriptions.Item>
<Descriptions.Item label="当前余额">
{formatFundAmount(detailQuery.data.data.account.balanceFen)}
</Descriptions.Item>
<Descriptions.Item label="责任主管">
{detailQuery.data.data.account.responsibleSupervisorName || '-'}
</Descriptions.Item>
<Descriptions.Item label="本月流入">
{formatFundAmount(detailQuery.data.data.account.monthInFen)}
</Descriptions.Item>
<Descriptions.Item label="本月流出">
{formatFundAmount(detailQuery.data.data.account.monthOutFen)}
</Descriptions.Item>
<Descriptions.Item label="最后流水">
{detailQuery.data.data.account.lastEntryAt
? formatAdminDateTime(detailQuery.data.data.account.lastEntryAt)
: '-'}
</Descriptions.Item>
<Descriptions.Item label="最后对账">
{detailQuery.data.data.account.lastReconciledAt
? formatAdminDateTime(detailQuery.data.data.account.lastReconciledAt)
: '-'}
</Descriptions.Item>
</Descriptions>
<Card title="资金流水" style={{ marginTop: 16 }}>
<PercentTable<AdminFundEntry>
rowKey="id"
size="small"
columns={detailEntryColumns}
dataSource={detailEntries}
loading={detailQuery.isLoading}
pagination={false}
scroll={{ x: 1000 }}
/>
</Card>
</>
) : null}
</Drawer>
</section>
)
}