feat(finance): 支持代录指定客服账户,并上线影子总账/对账/关账/快手平台退款
- 人工确认履约:管理员/财务/运营可传 fundAccountId / creditOwnerAdminUserId 指定客服备用金/积分账户;客服仍强制本人账户 - 新增迁移 112:finance_accounts / journal_entries / journal_lines / periods / reconciliations / reconciliation_items,种子 11 个科目 - 影子总账:销售事实按日生成收入/退款/成本凭证,支持手工凭证、试算平衡; 不接管钱包余额;总账同步接入定时任务与退款重算 - 期间关账/反关账:关账先补凭证并校验试算平衡,closed 期间禁止新增凭证 - 平台对账:JSON 账单导入,按 provider + platform_order_id 匹配订单/退款, 标记 matched/amount_mismatch/missing_order/missing_refund/duplicate - 平台退款:快手行业(91)同意退款 API 接入,成功后落 source=platform; 抖音/拼多多返回 501,保留手工退款 - 新增后台「财务 → 总账与对账」页面(总账/期间/对账三个 Tab) - 集成测试扩展到代录、总账平衡、关账阻断、手工凭证、对账匹配 验证:backend check 844(840 pass / 4 skip);frontend check 172 + build; 迁移 112 在 scratch 库全量执行并重跑幂等;本地生产副本已升级到 112, 销售事实同步 56 条凭证,借贷合计 1,885,351,864 = 1,885,351,864。
This commit is contained in:
@@ -0,0 +1,687 @@
|
||||
import { AuditOutlined, PlusOutlined, ReloadOutlined, SyncOutlined } from '@ant-design/icons'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
App,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
DatePicker,
|
||||
Drawer,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Statistic,
|
||||
Tabs,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
import type { TableColumnsType } from 'antd'
|
||||
import { useState } from 'react'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
|
||||
import { AdminRangePicker } from '@/components/admin/AdminDatePicker'
|
||||
import FinanceSubNav from '@/components/admin/FinanceSubNav'
|
||||
import PageHeader from '@/components/admin/PageHeader'
|
||||
import PercentTable from '@/components/common/PercentTable'
|
||||
import dayjs from '@/lib/dayjs'
|
||||
import {
|
||||
closeFinancePeriod,
|
||||
createFinanceManualVoucher,
|
||||
fetchFinanceAccounts,
|
||||
fetchFinanceJournalEntries,
|
||||
fetchFinancePeriods,
|
||||
fetchFinanceReconciliation,
|
||||
fetchFinanceReconciliations,
|
||||
fetchFinanceTrialBalance,
|
||||
importFinanceReconciliation,
|
||||
reopenFinancePeriod,
|
||||
resolveFinanceReconciliation,
|
||||
syncFinanceLedger,
|
||||
} from '@/services/admin'
|
||||
import type {
|
||||
FinanceAccount,
|
||||
FinanceJournalEntry,
|
||||
FinancePeriod,
|
||||
FinanceReconciliation,
|
||||
FinanceReconciliationItem,
|
||||
FinanceTrialBalanceItem,
|
||||
} from '@/types/admin/finance-ledger'
|
||||
import { hasAdminPermission } from '@/utils/admin-auth'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
import { formatMoney } from './panels/shared'
|
||||
|
||||
const PROVIDER_OPTIONS = [
|
||||
{ value: '91kaquan', label: '91 卡券 / 快手' },
|
||||
{ value: 'pdd', label: '拼多多' },
|
||||
{ value: 'douyin_mashida', label: '抖音码速达' },
|
||||
]
|
||||
|
||||
const RECON_STATUS_META: Record<string, { label: string; color: string }> = {
|
||||
imported: { label: '待匹配', color: 'processing' },
|
||||
matched: { label: '全部匹配', color: 'green' },
|
||||
differences: { label: '有差异', color: 'orange' },
|
||||
resolved: { label: '已解决', color: 'default' },
|
||||
}
|
||||
|
||||
const MATCH_STATUS_META: Record<string, { label: string; color: string }> = {
|
||||
matched: { label: '匹配', color: 'green' },
|
||||
amount_mismatch: { label: '金额不一致', color: 'orange' },
|
||||
missing_order: { label: '系统无订单', color: 'red' },
|
||||
missing_refund: { label: '系统无退款', color: 'red' },
|
||||
duplicate: { label: '重复行', color: 'default' },
|
||||
}
|
||||
|
||||
export default function AdminFinanceLedgerPage() {
|
||||
const { message } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
const canConfig = hasAdminPermission('finance.config')
|
||||
const [range, setRange] = useState<[Dayjs, Dayjs]>([dayjs().startOf('month'), dayjs()])
|
||||
const [entryPage, setEntryPage] = useState(1)
|
||||
const [entryPageSize, setEntryPageSize] = useState(20)
|
||||
const [entryDetail, setEntryDetail] = useState<FinanceJournalEntry | null>(null)
|
||||
const [voucherOpen, setVoucherOpen] = useState(false)
|
||||
const [reconImportOpen, setReconImportOpen] = useState(false)
|
||||
const [reconDetailId, setReconDetailId] = useState<number | null>(null)
|
||||
const [reconPage, setReconPage] = useState(1)
|
||||
const [reconPageSize, setReconPageSize] = useState(20)
|
||||
const [voucherForm] = Form.useForm()
|
||||
const [reconForm] = Form.useForm()
|
||||
|
||||
const dateFrom = range[0].format('YYYY-MM-DD')
|
||||
const dateTo = range[1].format('YYYY-MM-DD')
|
||||
|
||||
const accountsQuery = useQuery({
|
||||
queryKey: ['admin-finance-ledger-accounts'],
|
||||
queryFn: () => fetchFinanceAccounts(),
|
||||
})
|
||||
const trialQuery = useQuery({
|
||||
queryKey: ['admin-finance-ledger-trial', dateFrom, dateTo],
|
||||
queryFn: () => fetchFinanceTrialBalance({ dateFrom, dateTo }),
|
||||
})
|
||||
const entriesQuery = useQuery({
|
||||
queryKey: ['admin-finance-ledger-entries', dateFrom, dateTo, entryPage, entryPageSize],
|
||||
queryFn: () =>
|
||||
fetchFinanceJournalEntries({
|
||||
dateFrom,
|
||||
dateTo,
|
||||
page: entryPage,
|
||||
pageSize: entryPageSize,
|
||||
}),
|
||||
})
|
||||
const periodsQuery = useQuery({
|
||||
queryKey: ['admin-finance-periods'],
|
||||
queryFn: () => fetchFinancePeriods(),
|
||||
})
|
||||
const reconciliationsQuery = useQuery({
|
||||
queryKey: ['admin-finance-reconciliations', reconPage, reconPageSize],
|
||||
queryFn: () => fetchFinanceReconciliations({ page: reconPage, pageSize: reconPageSize }),
|
||||
})
|
||||
const reconDetailQuery = useQuery({
|
||||
queryKey: ['admin-finance-reconciliation-detail', reconDetailId],
|
||||
queryFn: () => fetchFinanceReconciliation(Number(reconDetailId)),
|
||||
enabled: Boolean(reconDetailId),
|
||||
})
|
||||
|
||||
const syncMutation = useMutation({
|
||||
mutationFn: () => syncFinanceLedger({ dateFrom, dateTo }),
|
||||
onSuccess: (response) => {
|
||||
message.success(`总账同步完成:新增 ${response.data.created} 条凭证`)
|
||||
void queryClient.invalidateQueries({ queryKey: ['admin-finance-ledger'] })
|
||||
void queryClient.invalidateQueries({ queryKey: ['admin-finance-periods'] })
|
||||
},
|
||||
onError: (error) => message.error(error instanceof Error ? error.message : '同步总账失败'),
|
||||
})
|
||||
|
||||
const voucherMutation = useMutation({
|
||||
mutationFn: () => {
|
||||
const values = voucherForm.getFieldsValue() as {
|
||||
debitAccount: string
|
||||
creditAccount: string
|
||||
amountYuan: number
|
||||
occurredAt?: Dayjs
|
||||
memo?: string
|
||||
}
|
||||
const amountFen = Math.round(Number(values.amountYuan || 0) * 100)
|
||||
return createFinanceManualVoucher({
|
||||
occurredAt: values.occurredAt?.toISOString(),
|
||||
memo: values.memo || '',
|
||||
lines: [
|
||||
{ accountCode: values.debitAccount, debitFen: amountFen, memo: values.memo || '' },
|
||||
{ accountCode: values.creditAccount, creditFen: amountFen, memo: values.memo || '' },
|
||||
],
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
message.success('手工凭证已创建')
|
||||
setVoucherOpen(false)
|
||||
voucherForm.resetFields()
|
||||
void queryClient.invalidateQueries({ queryKey: ['admin-finance-ledger'] })
|
||||
},
|
||||
onError: (error) => message.error(error instanceof Error ? error.message : '创建凭证失败'),
|
||||
})
|
||||
|
||||
const closeMutation = useMutation({
|
||||
mutationFn: (periodKey: string) => closeFinancePeriod(periodKey),
|
||||
onSuccess: () => {
|
||||
message.success('期间已关账')
|
||||
void queryClient.invalidateQueries({ queryKey: ['admin-finance-periods'] })
|
||||
},
|
||||
onError: (error) => message.error(error instanceof Error ? error.message : '关账失败'),
|
||||
})
|
||||
|
||||
const reopenMutation = useMutation({
|
||||
mutationFn: (periodKey: string) => reopenFinancePeriod(periodKey),
|
||||
onSuccess: () => {
|
||||
message.success('期间已重新打开')
|
||||
void queryClient.invalidateQueries({ queryKey: ['admin-finance-periods'] })
|
||||
},
|
||||
onError: (error) => message.error(error instanceof Error ? error.message : '反关账失败'),
|
||||
})
|
||||
|
||||
const importReconMutation = useMutation({
|
||||
mutationFn: () => {
|
||||
const values = reconForm.getFieldsValue() as {
|
||||
provider: string
|
||||
periodRange?: [Dayjs, Dayjs]
|
||||
rowsText: string
|
||||
}
|
||||
const parsed = JSON.parse(values.rowsText || '[]')
|
||||
if (!Array.isArray(parsed) || parsed.length === 0) {
|
||||
throw new Error('请粘贴至少一行 JSON 数组')
|
||||
}
|
||||
const periodRange = values.periodRange || range
|
||||
return importFinanceReconciliation({
|
||||
provider: values.provider,
|
||||
periodFrom: periodRange[0].format('YYYY-MM-DD'),
|
||||
periodTo: periodRange[1].format('YYYY-MM-DD'),
|
||||
rows: parsed,
|
||||
})
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
message.success(
|
||||
`导入完成:${response.data.rows} 行,匹配 ${response.data.matched},差异 ${response.data.mismatched}`,
|
||||
)
|
||||
setReconImportOpen(false)
|
||||
reconForm.resetFields()
|
||||
void queryClient.invalidateQueries({ queryKey: ['admin-finance-reconciliations'] })
|
||||
},
|
||||
onError: (error) => message.error(error instanceof Error ? error.message : '导入账单失败'),
|
||||
})
|
||||
|
||||
const resolveReconMutation = useMutation({
|
||||
mutationFn: (id: number) => resolveFinanceReconciliation(id, '后台确认差异已处理'),
|
||||
onSuccess: () => {
|
||||
message.success('已标记解决')
|
||||
void queryClient.invalidateQueries({ queryKey: ['admin-finance-reconciliations'] })
|
||||
void queryClient.invalidateQueries({ queryKey: ['admin-finance-reconciliation-detail'] })
|
||||
},
|
||||
onError: (error) => message.error(error instanceof Error ? error.message : '标记失败'),
|
||||
})
|
||||
|
||||
const accountOptions = (accountsQuery.data?.data.items || []).map((item: FinanceAccount) => ({
|
||||
value: item.accountCode,
|
||||
label: `${item.accountCode} ${item.accountName}`,
|
||||
}))
|
||||
|
||||
const trialColumns: TableColumnsType<FinanceTrialBalanceItem> = [
|
||||
{ title: '科目', dataIndex: 'accountCode', width: 90 },
|
||||
{ title: '名称', dataIndex: 'accountName', width: 160 },
|
||||
{ title: '类型', dataIndex: 'accountType', width: 90 },
|
||||
{
|
||||
title: '借方',
|
||||
dataIndex: 'debitFen',
|
||||
align: 'right',
|
||||
width: 120,
|
||||
render: (value: number) => formatMoney(value),
|
||||
},
|
||||
{
|
||||
title: '贷方',
|
||||
dataIndex: 'creditFen',
|
||||
align: 'right',
|
||||
width: 120,
|
||||
render: (value: number) => formatMoney(value),
|
||||
},
|
||||
{
|
||||
title: '余额',
|
||||
dataIndex: 'balanceFen',
|
||||
align: 'right',
|
||||
width: 120,
|
||||
render: (value: number) => formatMoney(value),
|
||||
},
|
||||
]
|
||||
|
||||
const entryColumns: TableColumnsType<FinanceJournalEntry> = [
|
||||
{ title: '凭证号', dataIndex: 'entryNo', width: 200 },
|
||||
{
|
||||
title: '日期',
|
||||
dataIndex: 'occurredAt',
|
||||
width: 160,
|
||||
render: (value: string) => formatAdminDateTime(value),
|
||||
},
|
||||
{ title: '事件', dataIndex: 'eventType', width: 140 },
|
||||
{ title: '摘要', dataIndex: 'memo' },
|
||||
{
|
||||
title: '借方合计',
|
||||
dataIndex: 'debitTotalFen',
|
||||
align: 'right',
|
||||
width: 120,
|
||||
render: (value: number) => formatMoney(value),
|
||||
},
|
||||
]
|
||||
|
||||
const periodColumns: TableColumnsType<FinancePeriod> = [
|
||||
{ title: '期间', dataIndex: 'periodKey', width: 100 },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 90,
|
||||
render: (value: string) => (
|
||||
<Tag color={value === 'closed' ? 'default' : 'processing'}>
|
||||
{value === 'closed' ? '已关账' : '开放'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '关账时间',
|
||||
dataIndex: 'closedAt',
|
||||
width: 170,
|
||||
render: (value: string | null) => (value ? formatAdminDateTime(value) : '-'),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 150,
|
||||
render: (_, row) =>
|
||||
canConfig ? (
|
||||
row.status === 'closed' ? (
|
||||
<Button size="small" onClick={() => reopenMutation.mutate(row.periodKey)}>
|
||||
反关账
|
||||
</Button>
|
||||
) : (
|
||||
<Button size="small" type="primary" onClick={() => closeMutation.mutate(row.periodKey)}>
|
||||
关账
|
||||
</Button>
|
||||
)
|
||||
) : null,
|
||||
},
|
||||
]
|
||||
|
||||
const reconColumns: TableColumnsType<FinanceReconciliation> = [
|
||||
{ title: '批次', dataIndex: 'id', width: 80 },
|
||||
{ title: '平台', dataIndex: 'provider', width: 120 },
|
||||
{
|
||||
title: '期间',
|
||||
width: 190,
|
||||
render: (_, row) => `${row.periodFrom} ~ ${row.periodTo}`,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 110,
|
||||
render: (value: string) => {
|
||||
const meta = RECON_STATUS_META[value] || { label: value, color: 'default' }
|
||||
return <Tag color={meta.color}>{meta.label}</Tag>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '系统金额',
|
||||
dataIndex: 'expectedFen',
|
||||
align: 'right',
|
||||
width: 120,
|
||||
render: (value: number) => formatMoney(value),
|
||||
},
|
||||
{
|
||||
title: '账单金额',
|
||||
dataIndex: 'actualFen',
|
||||
align: 'right',
|
||||
width: 120,
|
||||
render: (value: number) => formatMoney(value),
|
||||
},
|
||||
{
|
||||
title: '差异',
|
||||
dataIndex: 'diffFen',
|
||||
align: 'right',
|
||||
width: 120,
|
||||
render: (value: number) => (
|
||||
<Typography.Text type={value === 0 ? 'secondary' : 'danger'}>
|
||||
{formatMoney(value)}
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 140,
|
||||
render: (_, row) => (
|
||||
<Space>
|
||||
<Button size="small" onClick={() => setReconDetailId(row.id)}>
|
||||
明细
|
||||
</Button>
|
||||
{canConfig && row.status !== 'resolved' ? (
|
||||
<Button size="small" onClick={() => resolveReconMutation.mutate(row.id)}>
|
||||
标记解决
|
||||
</Button>
|
||||
) : null}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const reconItemColumns: TableColumnsType<FinanceReconciliationItem> = [
|
||||
{ title: '行', dataIndex: 'lineNo', width: 60 },
|
||||
{ title: '平台单号', dataIndex: 'platformOrderId', width: 190 },
|
||||
{ title: '类型', dataIndex: 'entryType', width: 80 },
|
||||
{
|
||||
title: '账单金额',
|
||||
dataIndex: 'amountFen',
|
||||
align: 'right',
|
||||
width: 110,
|
||||
render: (value: number) => formatMoney(value),
|
||||
},
|
||||
{
|
||||
title: '系统金额',
|
||||
dataIndex: 'expectedFen',
|
||||
align: 'right',
|
||||
width: 110,
|
||||
render: (value: number | null) => (value === null ? '-' : formatMoney(value)),
|
||||
},
|
||||
{
|
||||
title: '结果',
|
||||
dataIndex: 'matchStatus',
|
||||
width: 120,
|
||||
render: (value: string) => {
|
||||
const meta = MATCH_STATUS_META[value] || { label: value, color: 'default' }
|
||||
return <Tag color={meta.color}>{meta.label}</Tag>
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="总账与对账" description="影子总账、期间关账与平台账单对账" />
|
||||
<FinanceSubNav current="/admin/finance/ledger" />
|
||||
<Card bordered={false}>
|
||||
<Space wrap style={{ marginBottom: 16 }}>
|
||||
<AdminRangePicker
|
||||
value={range}
|
||||
onChange={(value) => {
|
||||
if (value?.[0] && value?.[1]) setRange([value[0], value[1]])
|
||||
}}
|
||||
/>
|
||||
{canConfig ? (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SyncOutlined />}
|
||||
loading={syncMutation.isPending}
|
||||
onClick={() => syncMutation.mutate()}
|
||||
>
|
||||
同步销售事实到总账
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => {
|
||||
void trialQuery.refetch()
|
||||
void entriesQuery.refetch()
|
||||
}}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<Tabs
|
||||
items={[
|
||||
{
|
||||
key: 'ledger',
|
||||
label: '总账',
|
||||
children: (
|
||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={8}>
|
||||
<Statistic
|
||||
title="借方合计"
|
||||
value={formatMoney(trialQuery.data?.data.totals.debitFen || 0)}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} md={8}>
|
||||
<Statistic
|
||||
title="贷方合计"
|
||||
value={formatMoney(trialQuery.data?.data.totals.creditFen || 0)}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} md={8}>
|
||||
<Statistic
|
||||
title="借贷平衡"
|
||||
value={trialQuery.data?.data.balanced ? '是' : '否'}
|
||||
valueStyle={{
|
||||
color: trialQuery.data?.data.balanced ? '#3f8600' : '#cf1322',
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Card size="small" title="试算平衡">
|
||||
<PercentTable
|
||||
size="small"
|
||||
rowKey="accountCode"
|
||||
loading={trialQuery.isLoading}
|
||||
dataSource={trialQuery.data?.data.items || []}
|
||||
columns={trialColumns}
|
||||
pagination={false}
|
||||
/>
|
||||
</Card>
|
||||
<Card
|
||||
size="small"
|
||||
title="凭证"
|
||||
extra={
|
||||
canConfig ? (
|
||||
<Button
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setVoucherOpen(true)}
|
||||
>
|
||||
手工凭证
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<PercentTable
|
||||
size="small"
|
||||
rowKey="id"
|
||||
loading={entriesQuery.isLoading}
|
||||
dataSource={entriesQuery.data?.data.items || []}
|
||||
columns={entryColumns}
|
||||
onRow={(row) => ({ onClick: () => setEntryDetail(row) })}
|
||||
pagination={{
|
||||
current: entryPage,
|
||||
pageSize: entryPageSize,
|
||||
total: entriesQuery.data?.data.pagination.total || 0,
|
||||
showSizeChanger: true,
|
||||
onChange: (page, size) => {
|
||||
setEntryPage(page)
|
||||
setEntryPageSize(size)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'periods',
|
||||
label: '期间关账',
|
||||
children: (
|
||||
<Card size="small" title="会计期间">
|
||||
<PercentTable
|
||||
size="small"
|
||||
rowKey="periodKey"
|
||||
loading={periodsQuery.isLoading}
|
||||
dataSource={periodsQuery.data?.data.items || []}
|
||||
columns={periodColumns}
|
||||
pagination={false}
|
||||
/>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'reconciliations',
|
||||
label: '平台对账',
|
||||
children: (
|
||||
<Card
|
||||
size="small"
|
||||
title="对账批次"
|
||||
extra={
|
||||
canConfig ? (
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<AuditOutlined />}
|
||||
onClick={() => setReconImportOpen(true)}
|
||||
>
|
||||
导入账单
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<PercentTable
|
||||
size="small"
|
||||
rowKey="id"
|
||||
loading={reconciliationsQuery.isLoading}
|
||||
dataSource={reconciliationsQuery.data?.data.items || []}
|
||||
columns={reconColumns}
|
||||
pagination={{
|
||||
current: reconPage,
|
||||
pageSize: reconPageSize,
|
||||
total: reconciliationsQuery.data?.data.pagination.total || 0,
|
||||
showSizeChanger: true,
|
||||
onChange: (page, size) => {
|
||||
setReconPage(page)
|
||||
setReconPageSize(size)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Drawer
|
||||
open={Boolean(entryDetail)}
|
||||
title={entryDetail?.entryNo || '凭证明细'}
|
||||
width={640}
|
||||
onClose={() => setEntryDetail(null)}
|
||||
>
|
||||
{entryDetail ? (
|
||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||
<Typography.Text type="secondary">{entryDetail.memo}</Typography.Text>
|
||||
<PercentTable
|
||||
size="small"
|
||||
rowKey="lineNo"
|
||||
pagination={false}
|
||||
dataSource={entryDetail.lines}
|
||||
columns={[
|
||||
{ title: '科目', dataIndex: 'accountName' },
|
||||
{
|
||||
title: '借方',
|
||||
dataIndex: 'debitFen',
|
||||
align: 'right',
|
||||
render: (value: number) => (value ? formatMoney(value) : '-'),
|
||||
},
|
||||
{
|
||||
title: '贷方',
|
||||
dataIndex: 'creditFen',
|
||||
align: 'right',
|
||||
render: (value: number) => (value ? formatMoney(value) : '-'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Space>
|
||||
) : null}
|
||||
</Drawer>
|
||||
|
||||
<Modal
|
||||
open={voucherOpen}
|
||||
title="新增手工凭证"
|
||||
okText="创建"
|
||||
confirmLoading={voucherMutation.isPending}
|
||||
onOk={() => voucherMutation.mutate()}
|
||||
onCancel={() => setVoucherOpen(false)}
|
||||
>
|
||||
<Form form={voucherForm} layout="vertical">
|
||||
<Form.Item name="debitAccount" label="借方科目" rules={[{ required: true }]}>
|
||||
<Select options={accountOptions} showSearch optionFilterProp="label" />
|
||||
</Form.Item>
|
||||
<Form.Item name="creditAccount" label="贷方科目" rules={[{ required: true }]}>
|
||||
<Select options={accountOptions} showSearch optionFilterProp="label" />
|
||||
</Form.Item>
|
||||
<Form.Item name="amountYuan" label="金额(元)" rules={[{ required: true }]}>
|
||||
<InputNumber min={0.01} precision={2} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="occurredAt" label="发生时间">
|
||||
<DatePicker showTime style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="memo" label="摘要">
|
||||
<Input maxLength={200} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={reconImportOpen}
|
||||
title="导入平台账单"
|
||||
width={720}
|
||||
okText="导入并匹配"
|
||||
confirmLoading={importReconMutation.isPending}
|
||||
onOk={() => importReconMutation.mutate()}
|
||||
onCancel={() => setReconImportOpen(false)}
|
||||
>
|
||||
<Form form={reconForm} layout="vertical" initialValues={{ provider: '91kaquan' }}>
|
||||
<Form.Item name="provider" label="平台" rules={[{ required: true }]}>
|
||||
<Select options={PROVIDER_OPTIONS} />
|
||||
</Form.Item>
|
||||
<Form.Item name="periodRange" label="账单期间">
|
||||
<AdminRangePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="rowsText"
|
||||
label="账单明细 JSON"
|
||||
rules={[{ required: true }]}
|
||||
extra='示例:[{"platformOrderId":"...","entryType":"sale","amountFen":100}];refund 行可带 externalRef。'
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={8}
|
||||
placeholder='[{"platformOrderId":"123","entryType":"sale","amountFen":100}]'
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Drawer
|
||||
open={Boolean(reconDetailId)}
|
||||
title={`对账批次 #${reconDetailId || ''}`}
|
||||
width={860}
|
||||
onClose={() => setReconDetailId(null)}
|
||||
>
|
||||
{reconDetailQuery.data ? (
|
||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||
<Typography.Text type="secondary">
|
||||
{reconDetailQuery.data.data.provider} · {reconDetailQuery.data.data.periodFrom} ~{' '}
|
||||
{reconDetailQuery.data.data.periodTo} · 差异{' '}
|
||||
{formatMoney(reconDetailQuery.data.data.diffFen)}
|
||||
</Typography.Text>
|
||||
<PercentTable
|
||||
size="small"
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
dataSource={reconDetailQuery.data.data.items}
|
||||
columns={reconItemColumns}
|
||||
/>
|
||||
</Space>
|
||||
) : null}
|
||||
</Drawer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user