{
+ return [
+ {
+ title: '订单',
+ dataIndex: 'order_no',
+ width: 200,
+ fixed: 'left',
+ render: (_, row) => (
+
+
+
+ 结算 {formatText(row.settlement_status)}
+
+ ),
+ },
+ {
+ title: '用户',
+ key: 'users',
+ width: 170,
+ render: (_, row) => (
+
+ 租客 {formatText(row.renter_phone)}
+ 号主 {formatText(row.owner_phone)}
+
+ ),
+ },
+ {
+ title: '订单金额',
+ key: 'order_amount',
+ width: 140,
+ align: 'right',
+ render: (_, row) => (
+
+ {formatMoney(row.order_rent_amount_cent)}
+ 押金 {formatMoney(row.order_deposit_amount_cent)}
+
+ ),
+ },
+ {
+ title: '结账',
+ key: 'checkout',
+ width: 160,
+ align: 'right',
+ render: (_, row) => (
+
+ 租金 {formatMoney(row.checkout_rent_amount_cent)}
+ 号主 {formatMoney(row.checkout_owner_income_cent)}
+ 平台费 {formatMoney(row.checkout_platform_fee_cent)}
+
+ ),
+ },
+ {
+ title: '支付 / 退款',
+ key: 'payment',
+ width: 160,
+ align: 'right',
+ render: (_, row) => (
+
+ {formatMoney(row.paid_amount_cent)}
+ 退款 {formatMoney(row.refunded_amount_cent)}
+ 渠道净额 {formatMoney(row.channel_net_amount_cent)}
+
+ ),
+ },
+ {
+ title: '财务状态',
+ dataIndex: 'finance_status',
+ width: 130,
+ render: value => ,
+ },
+ {
+ title: '结算时间',
+ dataIndex: 'settled_at',
+ width: 170,
+ render: value => formatDateTime(value),
+ },
+ {
+ title: '操作',
+ key: 'actions',
+ width: 100,
+ fixed: 'right',
+ render: (_, row) => (
+
+ ),
+ },
+ ]
+}
+
+export function AdminFinancePage() {
+ const navigate = useNavigate()
+ const [range, setRange] = useState<[Dayjs, Dayjs]>([
+ dayjs().subtract(6, 'day'),
+ dayjs(),
+ ])
+ const [keyword, setKeyword] = useState('')
+ const [appliedKeyword, setAppliedKeyword] = useState('')
+ const [dateType, setDateType] = useState('settled')
+ const [page, setPage] = useState(1)
+ const [pageSize, setPageSize] = useState(20)
+
+ const startDate = range[0].format('YYYY-MM-DD')
+ const endDate = range[1].format('YYYY-MM-DD')
+
+ const dashboard = useQuery({
+ queryKey: ['finance-dashboard', startDate, endDate],
+ queryFn: () => fetchFinanceDashboard({ start_date: startDate, end_date: endDate }),
+ staleTime: 60_000,
+ })
+
+ const details = useQuery({
+ queryKey: ['finance-details', startDate, endDate, appliedKeyword, dateType, page, pageSize],
+ queryFn: () =>
+ fetchFinanceDetails({
+ start_date: startDate,
+ end_date: endDate,
+ order_no: appliedKeyword,
+ date_type: dateType,
+ page,
+ page_size: pageSize,
+ }),
+ placeholderData: keepPreviousData,
+ })
+
+ const detailColumns = useMemo(
+ () => buildDetailColumns(orderNo => navigate(`/orders?keyword=${encodeURIComponent(orderNo)}`)),
+ [navigate]
+ )
+
+ const summary = dashboard.data?.summary
+ const disbursement = dashboard.data?.disbursement_summary
+ const pickup = dashboard.data?.pickup_summary
+ const mohong = dashboard.data?.mohong_summary
+ const expense = dashboard.data?.operating_expense_summary
+ const daily = dashboard.data?.daily_items ?? []
+
+ const applyDetailFilters = () => {
+ setAppliedKeyword(keyword.trim())
+ setPage(1)
+ }
+
+ const resetDetailFilters = () => {
+ setKeyword('')
+ setAppliedKeyword('')
+ setPage(1)
+ }
+
+ const errorText =
+ dashboard.error instanceof Error ? dashboard.error.message : '请稍后重试或重新登录后台。'
+
+ return (
+
+
+
+ {startDate} ~ {endDate}
+
+ {dashboard.isFetching ? 正在更新 : null}
+
+ }
+ extra={
+
+ {
+ if (value?.[0] && value[1]) {
+ setRange([value[0], value[1]])
+ setPage(1)
+ }
+ }}
+ />
+ }
+ onClick={() => {
+ void dashboard.refetch()
+ void details.refetch()
+ }}
+ loading={dashboard.isFetching}
+ >
+ 刷新
+
+
+ }
+ />
+
+ {dashboard.isError ? (
+ void dashboard.refetch()}>
+ 重试
+
+ }
+ />
+ ) : null}
+
+
+
+
+
+
+
+ 支付 {summary?.successful_pay_count ?? 0} 笔 · 退款{' '}
+ {summary?.successful_refund_count ?? 0} 笔
+
+
+
+
+
+
+
+
+
+
+ 待退款 {formatMoney(summary?.pending_refund_amount_cent ?? 0)} ·{' '}
+ {summary?.pending_refund_count ?? 0} 笔
+
+
+
+
+
+
+
+
+
+
+ 订单 {formatMoney(summary?.normal_order_profit_amount_cent ?? 0)} · 提号{' '}
+ {formatMoney(summary?.pickup_profit_amount_cent ?? 0)}
+
+
+
+
+
+
+
+
+
+
+ 钱包入账 {formatMoney(summary?.owner_wallet_income_amount_cent ?? 0)}
+
+
+
+
+
+
+
+
+
+
+ 已结算订单 {summary?.settled_order_count ?? 0} 笔
+
+
+
+
+
+
+
+
+
+
+ 结算差异 {formatMoney(summary?.settlement_diff_amount_cent ?? 0)}
+
+
+
+
+
+
+
+
+
+
+ {disbursement?.pending_payment_count ?? 0} 笔 · 已打款{' '}
+ {formatMoney(disbursement?.paid_amount_cent ?? 0)}
+
+
+
+
+
+
+
+
+
+
+ {disbursement?.withdrawal_review_count ?? 0} 笔 · 待打款{' '}
+ {formatMoney(disbursement?.withdrawal_pending_amount_cent ?? 0)}
+
+
+
+
+
+
+
+
+
+ 待结算订单
+ {summary?.pending_settle_order_count ?? 0}
+
+ 待线下结算 {formatMoney(summary?.offline_settlement_pending_amount_cent ?? 0)}
+
+
+
+
+
+ 线下提号
+ {pickup?.completed_count ?? 0}
+
+
+ 利润 {formatMoney(pickup?.profit_amount_cent ?? 0)} · 待结{' '}
+ {formatMoney(pickup?.pending_amount_cent ?? 0)}
+
+
+
+
+
+
+ 撞车商城净额
+ {formatMoney(mohong?.net_amount_cent ?? 0)}
+
+
+ 流水 {formatMoney(mohong?.flow_amount_cent ?? 0)} · 退款{' '}
+ {formatMoney(mohong?.refund_amount_cent ?? 0)}
+
+
+
+
+
+
+ 运营支出
+ {formatMoney(expense?.amount_cent ?? 0)}
+
+ {expense?.count ?? 0} 笔已登记支出
+
+
+
+
+
+ className="admin-finance-daily"
+ title={每日趋势}
+ extra={数据更新于 {formatDateTime(dashboard.data?.generated_at)}}
+ rowKey="date"
+ columns={dailyColumns}
+ dataSource={daily}
+ loading={dashboard.isFetching}
+ emptyDescription="所选区间没有财务数据"
+ />
+
+ {
+ setDateType(value)
+ setPage(1)
+ }}
+ />
+ }
+ >
+
+ setKeyword(event.target.value)}
+ />
+
+
+
+
+ title={财务明细}
+ extra={
+
+ {details.data ? `共 ${details.data.total} 条` : '加载中'}
+
+ }
+ rowKey="order_id"
+ columns={detailColumns}
+ dataSource={details.data?.items ?? []}
+ loading={details.isFetching}
+ total={details.data?.total ?? 0}
+ page={details.data?.page ?? page}
+ pageSize={details.data?.page_size ?? pageSize}
+ onPageChange={(nextPage, nextPageSize) => {
+ setPage(nextPageSize !== pageSize ? 1 : nextPage)
+ setPageSize(nextPageSize)
+ }}
+ emptyDescription="所选区间没有财务明细"
+ />
+
+ )
+}
diff --git a/frontend/src/features/admin/api.ts b/frontend/src/features/admin/api.ts
index 0142fbc..c884885 100644
--- a/frontend/src/features/admin/api.ts
+++ b/frontend/src/features/admin/api.ts
@@ -20,6 +20,8 @@ import type {
AdminWithdrawalPage,
AdminWithdrawalQuery,
DashboardData,
+ FinanceDashboard,
+ FinanceDetailPage,
} from '@/features/admin/types'
export async function fetchAdminCaptcha() {
@@ -311,3 +313,35 @@ export async function arbitrateAdminDispute(
)
return data.data
}
+
+export interface FinanceRangeQuery {
+ start_date?: string
+ end_date?: string
+}
+
+export async function fetchFinanceDashboard(range: FinanceRangeQuery) {
+ const { data } = await adminClient.get>('/admin/finance/dashboard', {
+ params: compactParams(range),
+ })
+ return data.data
+}
+
+export async function fetchFinanceDetails(
+ query: FinanceRangeQuery & {
+ order_no?: string
+ date_type?: 'created' | 'settled' | 'offline_settled'
+ page?: number
+ page_size?: number
+ }
+) {
+ const { data } = await adminClient.get>>(
+ '/admin/finance/details',
+ { params: compactParams(query) }
+ )
+ return {
+ items: data.data.items ?? [],
+ total: data.data.total ?? 0,
+ page: data.data.page ?? query.page ?? 1,
+ page_size: data.data.page_size ?? query.page_size ?? 20,
+ }
+}
diff --git a/frontend/src/features/admin/labels.ts b/frontend/src/features/admin/labels.ts
index b6537ab..3c148ef 100644
--- a/frontend/src/features/admin/labels.ts
+++ b/frontend/src/features/admin/labels.ts
@@ -189,3 +189,11 @@ export const withdrawalStatusMap: Record = {
}
export const withdrawalStatusOptions = toOptions(withdrawalStatusMap)
+
+export const financeStatusMap: Record = {
+ normal: { label: '正常', tone: 'success' },
+ refund_pending: { label: '退款中', tone: 'blue' },
+ refund_failed: { label: '退款失败', tone: 'red' },
+ settlement_diff: { label: '结算差异', tone: 'orange' },
+ offline_settlement_pending: { label: '待线下结算', tone: 'gold' },
+}
diff --git a/frontend/src/features/admin/types.ts b/frontend/src/features/admin/types.ts
index 8573c57..d07054f 100644
--- a/frontend/src/features/admin/types.ts
+++ b/frontend/src/features/admin/types.ts
@@ -404,3 +404,132 @@ export interface AdminDisputePage {
page: number
page_size: number
}
+
+export interface FinanceSummary {
+ total_flow_amount_cent: number
+ total_refund_amount_cent: number
+ pending_refund_amount_cent: number
+ channel_net_amount_cent: number
+ platform_income_amount_cent: number
+ normal_order_profit_amount_cent: number
+ pickup_profit_amount_cent: number
+ owner_should_income_amount_cent: number
+ owner_wallet_income_amount_cent: number
+ owner_effective_settlement_amount_cent: number
+ offline_settlement_amount_cent: number
+ offline_settlement_pending_amount_cent: number
+ settlement_diff_amount_cent: number
+ estimated_income_amount_cent: number
+ successful_pay_count: number
+ successful_refund_count: number
+ pending_refund_count: number
+ settled_order_count: number
+ pending_settle_order_count: number
+ offline_settlement_pending_count: number
+ financial_exception_count: number
+}
+
+export interface FinanceDaily {
+ date: string
+ total_flow_amount_cent: number
+ total_refund_amount_cent: number
+ platform_income_amount_cent: number
+ channel_net_amount_cent: number
+ owner_should_income_amount_cent: number
+ settled_order_count: number
+ paid_order_count: number
+ operating_expense_amount_cent: number
+ disbursement_paid_amount_cent: number
+}
+
+export interface FinancePickupSummary {
+ settled_amount_cent: number
+ profit_amount_cent: number
+ completed_count: number
+ in_progress_count: number
+ pending_amount_cent: number
+ pending_count: number
+}
+
+export interface FinanceMohongSummary {
+ flow_amount_cent: number
+ refund_amount_cent: number
+ net_amount_cent: number
+ paid_order_count: number
+}
+
+export interface FinanceDisbursementSummary {
+ paid_amount_cent: number
+ paid_count: number
+ pending_payment_amount_cent: number
+ pending_payment_count: number
+ withdrawal_amount_cent: number
+ withdrawal_fee_amount_cent: number
+ withdrawal_paid_amount_cent: number
+ withdrawal_paid_count: number
+ withdrawal_pending_amount_cent: number
+ withdrawal_pending_count: number
+ withdrawal_review_amount_cent: number
+ withdrawal_review_count: number
+ manual_paid_amount_cent: number
+ manual_paid_count: number
+}
+
+export interface FinanceOperatingExpenseSummary {
+ amount_cent: number
+ count: number
+}
+
+export interface FinanceDashboard {
+ summary: FinanceSummary
+ daily_items: FinanceDaily[]
+ pickup_summary: FinancePickupSummary
+ mohong_summary: FinanceMohongSummary
+ disbursement_summary: FinanceDisbursementSummary
+ operating_expense_summary: FinanceOperatingExpenseSummary
+ generated_at: string
+}
+
+export interface FinanceDetail {
+ order_id: number
+ order_no: string
+ order_status: string
+ settlement_status: string
+ refund_status: string
+ handoff_mode: string
+ settlement_mode: string
+ offline_settlement_status: string
+ offline_settlement_amount_cent: number
+ renter_id: number
+ renter_phone: string
+ renter_nickname: string
+ owner_id: number
+ owner_phone: string
+ owner_nickname: string
+ order_rent_amount_cent: number
+ order_deposit_amount_cent: number
+ checkout_rent_amount_cent: number
+ checkout_renter_refund_cent: number
+ checkout_owner_income_cent: number
+ checkout_platform_fee_cent: number
+ owner_wallet_income_amount_cent: number
+ owner_effective_settlement_amount_cent: number
+ paid_amount_cent: number
+ refunded_amount_cent: number
+ refunding_amount_cent: number
+ failed_refund_amount_cent: number
+ channel_net_amount_cent: number
+ platform_net_amount_cent: number
+ settlement_diff_amount_cent: number
+ finance_status: string
+ created_at: string
+ settled_at?: string | null
+ offline_settled_at?: string | null
+}
+
+export interface FinanceDetailPage {
+ items: FinanceDetail[]
+ total: number
+ page: number
+ page_size: number
+}
diff --git a/frontend/src/layouts/AdminLayout.tsx b/frontend/src/layouts/AdminLayout.tsx
index e74a9ce..dfe0e6f 100644
--- a/frontend/src/layouts/AdminLayout.tsx
+++ b/frontend/src/layouts/AdminLayout.tsx
@@ -83,7 +83,7 @@ const allNavGroups: NavGroup[] = [
label: '财务',
icon: ,
children: [
- { key: '/finance', label: '财务看板', permission: 'finance:view' },
+ { key: '/finance', label: '财务看板', permission: 'wallet:view' },
{ key: '/withdrawals', label: '提现审核', permission: 'withdrawal:list' },
],
},
diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css
index 6132960..2289c48 100644
--- a/frontend/src/styles/global.css
+++ b/frontend/src/styles/global.css
@@ -1403,6 +1403,15 @@ a {
margin-bottom: 16px;
}
+.admin-finance-mini {
+ margin-top: 16px;
+}
+
+.admin-finance-mini .admin-pending-card > .ant-card-body > span {
+ color: var(--ink-muted);
+ font-size: 11px;
+}
+
.admin-detail {
display: flex;
flex-direction: column;