Files
hfb_jysh/frontend/src/features/admin/AdminFinanceDetailsPage.tsx
T

281 lines
8.6 KiB
TypeScript

import { useMemo, useState } from 'react'
import { keepPreviousData, useQuery } from '@tanstack/react-query'
import { Alert, Button, DatePicker, Input, Select, Space, Tag, Typography, type TableColumnsType } from 'antd'
import { ReloadOutlined } from '@ant-design/icons'
import { type Dayjs } from 'dayjs'
import { fetchFinanceDetails } from '@/features/admin/api'
import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader'
import { DataTable } from '@/features/admin/components/DataTable'
import { FilterBar, FilterField } from '@/features/admin/components/FilterBar'
import { orderStatusMap } from '@/features/admin/labels'
import type { FinanceDetail } from '@/features/admin/types'
import { useAdminSessionStore } from '@/stores/adminSession'
import { formatDateTime, formatMoney, formatText } from '@/shared/utils/format'
interface Filters {
order_no: string
date_type: 'created' | 'settled' | 'offline_settled'
range: [Dayjs, Dayjs] | null
}
const emptyFilters: Filters = { order_no: '', date_type: 'settled', range: null }
const settlementStatusOptions = [
{ value: 'pending', label: '待结算' },
{ value: 'settled', label: '已结算' },
{ value: 'frozen', label: '冻结中' },
]
function errorText(error: unknown) {
const response = (error as { response?: { data?: { message?: string } } })?.response
if (response?.data?.message) return response.data.message
return error instanceof Error ? error.message : '请求失败,请稍后重试'
}
const columns: TableColumnsType<FinanceDetail> = [
{
title: '订单',
key: 'order',
width: 230,
fixed: 'left',
render: (_, row) => (
<div className="admin-stack">
<strong>{row.order_no}</strong>
<Space size={4} wrap>
<Tag bordered={false}>{orderStatusMap[row.order_status]?.label ?? row.order_status}</Tag>
<Tag bordered={false} color={row.finance_status === 'settled' ? 'green' : 'gold'}>
{row.finance_status === 'settled' ? '已结算' : '待结算'}
</Tag>
</Space>
</div>
),
},
{
title: '租客',
key: 'renter',
width: 160,
render: (_, row) => (
<div className="admin-stack">
<span>{formatText(row.renter_nickname)}</span>
<span className="admin-mono">{formatText(row.renter_phone)}</span>
</div>
),
},
{
title: '号主',
key: 'owner',
width: 160,
render: (_, row) => (
<div className="admin-stack">
<span>{formatText(row.owner_nickname)}</span>
<span className="admin-mono">{formatText(row.owner_phone)}</span>
</div>
),
},
{
title: '实付 / 退款',
key: 'amount',
width: 160,
align: 'right',
render: (_, row) => (
<div className="admin-stack">
<span>{formatMoney(row.paid_amount_cent)}</span>
<Typography.Text type="secondary">
退款 {formatMoney(row.refunded_amount_cent)}
</Typography.Text>
</div>
),
},
{
title: '号主结算',
key: 'owner_settle',
width: 150,
align: 'right',
render: (_, row) => (
<div className="admin-stack">
<span>{formatMoney(row.owner_effective_settlement_amount_cent)}</span>
<Typography.Text type="secondary">
钱包 {formatMoney(row.owner_wallet_income_amount_cent)}
</Typography.Text>
</div>
),
},
{
title: '平台净额',
dataIndex: 'platform_net_amount_cent',
width: 130,
align: 'right',
render: value => formatMoney(value),
},
{
title: '结算模式',
key: 'mode',
width: 150,
render: (_, row) => (
<div className="admin-stack">
<Tag bordered={false}>{formatText(row.settlement_mode)}</Tag>
<span className="admin-mono">{formatText(row.offline_settlement_status)}</span>
</div>
),
},
{
title: '创建时间',
dataIndex: 'created_at',
width: 170,
render: value => formatDateTime(value),
},
{
title: '结算时间',
dataIndex: 'settled_at',
width: 170,
render: value => (value ? formatDateTime(value) : '—'),
},
]
export function AdminFinanceDetailsPage() {
const [filters, setFilters] = useState<Filters>(emptyFilters)
const [applied, setApplied] = useState<Filters>(emptyFilters)
const [settlementStatus, setSettlementStatus] = useState<string>()
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
const permissions = useAdminSessionStore(state => state.admin?.permissions)
const canView = Boolean(permissions?.includes('*') || permissions?.includes('wallet:view'))
const query = useMemo(
() => ({
order_no: applied.order_no.trim() || undefined,
date_type: applied.date_type,
settlement_status: settlementStatus,
start_date: applied.range?.[0]?.format('YYYY-MM-DD'),
end_date: applied.range?.[1]?.format('YYYY-MM-DD'),
page,
page_size: pageSize,
}),
[applied, settlementStatus, page, pageSize]
)
const detailsQuery = useQuery({
queryKey: ['admin-finance-details', query],
queryFn: () => fetchFinanceDetails(query),
enabled: canView,
placeholderData: keepPreviousData,
})
if (!canView) {
return (
<div className="admin-page">
<AdminPageHeader title="财务明细" eyebrow="JIEYOU OPS · 财务" />
<Alert type="warning" showIcon message="当前账号没有资金查看权限" />
</div>
)
}
return (
<div className="admin-page">
<AdminPageHeader
eyebrow="JIEYOU OPS · 财务"
title="财务明细"
description="按订单维度核对实付、退款、号主结算与平台净额。"
meta={
<Space size={8} wrap>
<Tag color="geekblue">共 {detailsQuery.data?.total ?? 0} 单</Tag>
</Space>
}
extra={
<Button
icon={<ReloadOutlined />}
loading={detailsQuery.isFetching}
onClick={() => void detailsQuery.refetch()}
>
刷新
</Button>
}
/>
<FilterBar
loading={detailsQuery.isFetching}
onSearch={() => {
setPage(1)
setApplied(filters)
}}
onReset={() => {
setFilters(emptyFilters)
setApplied(emptyFilters)
setSettlementStatus(undefined)
setPage(1)
}}
>
<FilterField label="订单号">
<Input
allowClear
value={filters.order_no}
placeholder="完整订单号"
onChange={event =>
setFilters(current => ({ ...current, order_no: event.target.value }))
}
/>
</FilterField>
<FilterField label="日期口径">
<Select
value={filters.date_type}
options={[
{ value: 'settled', label: '按结算时间' },
{ value: 'created', label: '按下单时间' },
{ value: 'offline_settled', label: '按线下结算时间' },
]}
onChange={value => setFilters(current => ({ ...current, date_type: value }))}
/>
</FilterField>
<FilterField label="日期区间" width={280}>
<DatePicker.RangePicker
style={{ width: '100%' }}
value={filters.range}
allowEmpty={[true, true]}
onChange={value =>
setFilters(current => ({
...current,
range: value && value[0] && value[1] ? [value[0], value[1]] : null,
}))
}
/>
</FilterField>
<FilterField label="结算状态">
<Select
allowClear
value={settlementStatus}
options={settlementStatusOptions}
placeholder="全部状态"
onChange={value => setSettlementStatus(value)}
/>
</FilterField>
</FilterBar>
{detailsQuery.isError ? (
<Alert
type="error"
showIcon
className="admin-table-alert"
message="财务明细加载失败"
description={errorText(detailsQuery.error)}
/>
) : null}
<DataTable<FinanceDetail>
columns={columns}
dataSource={detailsQuery.data?.items ?? []}
rowKey="order_id"
loading={detailsQuery.isFetching}
total={detailsQuery.data?.total ?? 0}
page={detailsQuery.data?.page ?? page}
pageSize={detailsQuery.data?.page_size ?? pageSize}
onPageChange={(nextPage, nextSize) => {
setPage(nextPage)
setPageSize(nextSize)
}}
emptyDescription="所选区间还没有财务明细"
/>
</div>
)
}