feat(admin): 新增后台财务看板与财务明细
This commit is contained in:
+2
-2
@@ -194,9 +194,9 @@ dist/assets/AdminLayout-*.js 36.87 kB │ gzip 13.02 kB
|
||||
## 5. 已知限制
|
||||
|
||||
1. 移动端订单列表/详情与消息中心已接入真实接口;下单、支付、交接、归还、结账等操作仍待迁移。
|
||||
2. 后台已完成仪表盘、订单列表/详情、退款审核、提现审核、商品列表/详情与审核、用户列表/详情与人工操作、申诉仲裁,其余页面仍为统一占位。
|
||||
2. 后台已完成仪表盘、订单列表/详情、退款审核、提现审核、商品列表/详情与审核、用户列表/详情与人工操作、申诉仲裁、财务看板,其余页面仍为统一占位。
|
||||
3. 分销后台已完成主题、Logo、登录页与 Shell 轻改,页面结构未重写。
|
||||
4. 后台菜单权限码按后端逐个核对:订单 `order:list` / `order:view` / `order:close`,商品 `listing:view` / `listing:approve` / `listing:reject` / `listing:offline`,用户 `user:view` / `user:freeze` / `user:unfreeze` / `user:deposit_free` / `user:growth_points` / `user:manual_realname` / `user:revoke_realname` / `user:wallet_adjust`,提现 `withdrawal:list` / `withdrawal:detail` / `withdrawal:review` / `withdrawal:pay`,申诉 `dispute:view` / `dispute:arbitrate`;其余页面仍需核对。
|
||||
4. 后台菜单权限码按后端逐个核对:订单 `order:list` / `order:view` / `order:close`,商品 `listing:view` / `listing:approve` / `listing:reject` / `listing:offline`,用户 `user:view` / `user:freeze` / `user:unfreeze` / `user:deposit_free` / `user:growth_points` / `user:manual_realname` / `user:revoke_realname` / `user:wallet_adjust`,提现 `withdrawal:list` / `withdrawal:detail` / `withdrawal:review` / `withdrawal:pay`,申诉 `dispute:view` / `dispute:arbitrate`,财务 `wallet:view`;其余页面仍需核对。
|
||||
5. SVG 中文尚未转曲。
|
||||
6. 入口包已降至 495 kB(gzip 161 kB);AntD 共享基础设施仍在入口,后续可拆独立入口。
|
||||
7. 移动端、订单、分销轻改与首屏优化均已提交;商品管理与审核在本批次提交。
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# Phase 2 验收报告(后台财务看板)
|
||||
|
||||
> 日期:2026-10-30
|
||||
> 项目:解忧商行(hfb_jysh)
|
||||
> 范围:`/console-jy8k3m/finance`
|
||||
> 结论:**本切片通过,财务总览、每日趋势与财务明细已接真实接口**
|
||||
|
||||
---
|
||||
|
||||
## 1. 接口与权限
|
||||
|
||||
| 能力 | 接口 | 权限码 |
|
||||
| --- | --- | --- |
|
||||
| 财务总览 | `GET /api/admin/finance/dashboard` | `wallet:view` |
|
||||
| 财务明细 | `GET /api/admin/finance/details` | `wallet:view` |
|
||||
|
||||
日期参数:`start_date` / `end_date`(`YYYY-MM-DD`)。前端菜单权限已从推断值 `finance:view` 修正为 `wallet:view`。
|
||||
|
||||
## 2. 页面结构
|
||||
|
||||
`frontend/src/features/admin/AdminFinancePage.tsx`
|
||||
|
||||
- 顶部日期区间:默认近 7 天,可切换任意区间;总览与明细共用区间
|
||||
- 主 KPI:总流水、总退款、平台收入、号主应得、渠道净额、资金异常、待打款、提现待审
|
||||
- 次级摘要:待结算订单、线下提号、撞车商城净额、运营支出
|
||||
- 每日趋势:流水、退款、渠道净额、平台收入、号主应得、结算/支付订单数、运营支出、已打款
|
||||
- 财务明细:订单号、双方、订单金额、结账金额、支付/退款/渠道净额、财务状态、结算时间
|
||||
- 明细支持订单号精确查询、日期类型(下单/结算/线下结算)、分页
|
||||
- 点击订单号跳转订单管理并带上 `keyword` 查询
|
||||
|
||||
## 3. 财务状态
|
||||
|
||||
| 值 | 文案 |
|
||||
| --- | --- |
|
||||
| `normal` | 正常 |
|
||||
| `refund_pending` | 退款中 |
|
||||
| `refund_failed` | 退款失败 |
|
||||
| `settlement_diff` | 结算差异 |
|
||||
| `offline_settlement_pending` | 待线下结算 |
|
||||
|
||||
## 4. 验证结果
|
||||
|
||||
```text
|
||||
npx tsc --noEmit 通过
|
||||
npm run build 通过(入口 496.68 kB,gzip 161.26 kB)
|
||||
GET /api/admin/finance/dashboard?start_date=2026-10-24&end_date=2026-10-30 ok,返回 summary/daily/各摘要
|
||||
GET /api/admin/finance/details?...&page=1&page_size=5 ok,{"items":[],"total":0}
|
||||
GET /console-jy8k3m/finance 200
|
||||
```
|
||||
|
||||
开发库没有订单流水,总览与明细均为 0;接口字段与 DTO 已核对。
|
||||
|
||||
## 5. 已知限制
|
||||
|
||||
1. 未接线下出款、人工出款、运营支出登记与作废接口(需要 `finance:manual_disbursement` / `finance:operating_expense`)。
|
||||
2. 未做图表可视化,当前以 KPI + 表格呈现。
|
||||
3. 财务明细不支持按财务状态筛选(后端未提供该参数)。
|
||||
4. 未做导出与对账单下载。
|
||||
|
||||
## 6. 下一步建议
|
||||
|
||||
1. 后台公告管理与系统配置。
|
||||
2. 后台审计日志。
|
||||
3. 线下出款/运营支出登记。
|
||||
4. 移动端下单、支付与交接/归还/结账。
|
||||
+1
-1
@@ -395,7 +395,7 @@ hfb_jysh/
|
||||
|
||||
### Phase 4:管理后台核心(4–5 周)
|
||||
|
||||
> 状态:**进行中(2026-10-30)**。已完成仪表盘、订单列表/详情、退款审核、提现审核、商品列表/详情与审核、用户列表/详情与人工操作、申诉仲裁;财务看板、系统配置与审计日志待迁移。
|
||||
> 状态:**进行中(2026-10-30)**。已完成仪表盘、订单、退款、提现、商品、用户、申诉与财务看板;线下出款/运营支出、系统配置与审计日志待迁移。
|
||||
|
||||
- 后台登录、工作台、订单列表、订单详情。
|
||||
- 商品列表、商品详情、审核。
|
||||
|
||||
@@ -93,6 +93,11 @@ const AdminDisputesPage = lazy(() =>
|
||||
default: module.AdminDisputesPage,
|
||||
}))
|
||||
)
|
||||
const AdminFinancePage = lazy(() =>
|
||||
import('@/features/admin/AdminFinancePage').then(module => ({
|
||||
default: module.AdminFinancePage,
|
||||
}))
|
||||
)
|
||||
const AdminLayout = lazy(() =>
|
||||
import('@/layouts/AdminLayout').then(module => ({ default: module.AdminLayout }))
|
||||
)
|
||||
@@ -101,7 +106,6 @@ const AdminSectionPlaceholder = lazy(() =>
|
||||
)
|
||||
|
||||
const adminSections: Array<{ path: string; title: string; description: string }> = [
|
||||
{ path: 'finance', title: '财务看板', description: '查看收入、退款、结算与资金趋势。' },
|
||||
{ path: 'distribution/sites', title: '分销站点', description: '管理分站、规则、结算与站点钱包。' },
|
||||
{ path: 'announcements', title: '公告管理', description: '维护主站与分站公告。' },
|
||||
{ path: 'system-configs', title: '系统配置', description: '维护首页、发布、订单与支付配置。' },
|
||||
@@ -144,6 +148,7 @@ export function AppRouter() {
|
||||
<Route path="users" element={<AdminUsersPage />} />
|
||||
<Route path="withdrawals" element={<AdminWithdrawalsPage />} />
|
||||
<Route path="disputes" element={<AdminDisputesPage />} />
|
||||
<Route path="finance" element={<AdminFinancePage />} />
|
||||
{adminSections.map(section => (
|
||||
<Route
|
||||
key={section.path}
|
||||
|
||||
@@ -0,0 +1,534 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { keepPreviousData, useQuery } from '@tanstack/react-query'
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
DatePicker,
|
||||
Input,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Statistic,
|
||||
Tag,
|
||||
Typography,
|
||||
type TableColumnsType,
|
||||
} from 'antd'
|
||||
import {
|
||||
AccountBookOutlined,
|
||||
BankOutlined,
|
||||
ReloadOutlined,
|
||||
RiseOutlined,
|
||||
WalletOutlined,
|
||||
WarningOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import dayjs, { type Dayjs } from 'dayjs'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import { fetchFinanceDashboard, 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 { StatusTag } from '@/features/admin/components/StatusTag'
|
||||
import { financeStatusMap, orderStatusMap, readStatusMeta } from '@/features/admin/labels'
|
||||
import type { FinanceDaily, FinanceDetail } from '@/features/admin/types'
|
||||
import { formatDateTime, formatMoney, formatText } from '@/shared/utils/format'
|
||||
|
||||
type DateType = 'settled' | 'created' | 'offline_settled'
|
||||
|
||||
const dateTypeOptions = [
|
||||
{ value: 'settled', label: '按结算时间' },
|
||||
{ value: 'created', label: '按下单时间' },
|
||||
{ value: 'offline_settled', label: '按线下结算时间' },
|
||||
]
|
||||
|
||||
const dailyColumns: TableColumnsType<FinanceDaily> = [
|
||||
{ title: '日期', dataIndex: 'date', width: 120, fixed: 'left' },
|
||||
{
|
||||
title: '流水',
|
||||
dataIndex: 'total_flow_amount_cent',
|
||||
width: 130,
|
||||
align: 'right',
|
||||
render: value => formatMoney(value),
|
||||
},
|
||||
{
|
||||
title: '退款',
|
||||
dataIndex: 'total_refund_amount_cent',
|
||||
width: 120,
|
||||
align: 'right',
|
||||
render: value => formatMoney(value),
|
||||
},
|
||||
{
|
||||
title: '渠道净额',
|
||||
dataIndex: 'channel_net_amount_cent',
|
||||
width: 130,
|
||||
align: 'right',
|
||||
render: value => formatMoney(value),
|
||||
},
|
||||
{
|
||||
title: '平台收入',
|
||||
dataIndex: 'platform_income_amount_cent',
|
||||
width: 130,
|
||||
align: 'right',
|
||||
render: value => <span className="admin-money">{formatMoney(value)}</span>,
|
||||
},
|
||||
{
|
||||
title: '号主应得',
|
||||
dataIndex: 'owner_should_income_amount_cent',
|
||||
width: 130,
|
||||
align: 'right',
|
||||
render: value => formatMoney(value),
|
||||
},
|
||||
{
|
||||
title: '已结算订单',
|
||||
dataIndex: 'settled_order_count',
|
||||
width: 110,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
title: '支付订单',
|
||||
dataIndex: 'paid_order_count',
|
||||
width: 100,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
title: '运营支出',
|
||||
dataIndex: 'operating_expense_amount_cent',
|
||||
width: 130,
|
||||
align: 'right',
|
||||
render: value => formatMoney(value),
|
||||
},
|
||||
{
|
||||
title: '已打款',
|
||||
dataIndex: 'disbursement_paid_amount_cent',
|
||||
width: 130,
|
||||
align: 'right',
|
||||
render: value => formatMoney(value),
|
||||
},
|
||||
]
|
||||
|
||||
function buildDetailColumns(onOpenOrder: (orderNo: string) => void): TableColumnsType<FinanceDetail> {
|
||||
return [
|
||||
{
|
||||
title: '订单',
|
||||
dataIndex: 'order_no',
|
||||
width: 200,
|
||||
fixed: 'left',
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack">
|
||||
<button type="button" className="admin-order-link" onClick={() => onOpenOrder(row.order_no)}>
|
||||
{row.order_no}
|
||||
</button>
|
||||
<StatusTag meta={readStatusMeta(orderStatusMap, row.order_status)} />
|
||||
<span>结算 {formatText(row.settlement_status)}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '用户',
|
||||
key: 'users',
|
||||
width: 170,
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack">
|
||||
<span>租客 {formatText(row.renter_phone)}</span>
|
||||
<span>号主 {formatText(row.owner_phone)}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '订单金额',
|
||||
key: 'order_amount',
|
||||
width: 140,
|
||||
align: 'right',
|
||||
render: (_, row) => (
|
||||
<div className="admin-amount">
|
||||
<strong>{formatMoney(row.order_rent_amount_cent)}</strong>
|
||||
<span>押金 {formatMoney(row.order_deposit_amount_cent)}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '结账',
|
||||
key: 'checkout',
|
||||
width: 160,
|
||||
align: 'right',
|
||||
render: (_, row) => (
|
||||
<div className="admin-amount">
|
||||
<span>租金 {formatMoney(row.checkout_rent_amount_cent)}</span>
|
||||
<span>号主 {formatMoney(row.checkout_owner_income_cent)}</span>
|
||||
<span>平台费 {formatMoney(row.checkout_platform_fee_cent)}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '支付 / 退款',
|
||||
key: 'payment',
|
||||
width: 160,
|
||||
align: 'right',
|
||||
render: (_, row) => (
|
||||
<div className="admin-amount">
|
||||
<strong>{formatMoney(row.paid_amount_cent)}</strong>
|
||||
<span>退款 {formatMoney(row.refunded_amount_cent)}</span>
|
||||
<span>渠道净额 {formatMoney(row.channel_net_amount_cent)}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '财务状态',
|
||||
dataIndex: 'finance_status',
|
||||
width: 130,
|
||||
render: value => <StatusTag meta={readStatusMeta(financeStatusMap, value)} />,
|
||||
},
|
||||
{
|
||||
title: '结算时间',
|
||||
dataIndex: 'settled_at',
|
||||
width: 170,
|
||||
render: value => formatDateTime(value),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 100,
|
||||
fixed: 'right',
|
||||
render: (_, row) => (
|
||||
<Button type="link" size="small" onClick={() => onOpenOrder(row.order_no)}>
|
||||
查看订单
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
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<DateType>('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 (
|
||||
<div className="admin-page">
|
||||
<AdminPageHeader
|
||||
eyebrow="JIEYOU OPS · 财务"
|
||||
title="财务看板"
|
||||
description="按时间区间核对流水、退款、平台收入、号主结算与出款压力。"
|
||||
meta={
|
||||
<Space size={8} wrap>
|
||||
<Tag color="geekblue">
|
||||
{startDate} ~ {endDate}
|
||||
</Tag>
|
||||
{dashboard.isFetching ? <Tag color="processing">正在更新</Tag> : null}
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Space wrap>
|
||||
<DatePicker.RangePicker
|
||||
value={range}
|
||||
allowClear={false}
|
||||
onChange={value => {
|
||||
if (value?.[0] && value[1]) {
|
||||
setRange([value[0], value[1]])
|
||||
setPage(1)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => {
|
||||
void dashboard.refetch()
|
||||
void details.refetch()
|
||||
}}
|
||||
loading={dashboard.isFetching}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
|
||||
{dashboard.isError ? (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
className="admin-table-alert"
|
||||
message="财务数据没有加载出来"
|
||||
description={errorText}
|
||||
action={
|
||||
<Button size="small" onClick={() => void dashboard.refetch()}>
|
||||
重试
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={12} xl={6}>
|
||||
<Card className="admin-kpi-card">
|
||||
<div className="admin-kpi-card__icon">
|
||||
<AccountBookOutlined />
|
||||
</div>
|
||||
<Statistic title="总流水" value={formatMoney(summary?.total_flow_amount_cent ?? 0)} />
|
||||
<span className="admin-kpi-card__foot">
|
||||
支付 {summary?.successful_pay_count ?? 0} 笔 · 退款{' '}
|
||||
{summary?.successful_refund_count ?? 0} 笔
|
||||
</span>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} xl={6}>
|
||||
<Card className="admin-kpi-card">
|
||||
<div className="admin-kpi-card__icon">
|
||||
<WalletOutlined />
|
||||
</div>
|
||||
<Statistic title="总退款" value={formatMoney(summary?.total_refund_amount_cent ?? 0)} />
|
||||
<span className="admin-kpi-card__foot">
|
||||
待退款 {formatMoney(summary?.pending_refund_amount_cent ?? 0)} ·{' '}
|
||||
{summary?.pending_refund_count ?? 0} 笔
|
||||
</span>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} xl={6}>
|
||||
<Card className="admin-kpi-card admin-kpi-card--accent">
|
||||
<div className="admin-kpi-card__icon">
|
||||
<RiseOutlined />
|
||||
</div>
|
||||
<Statistic
|
||||
title="平台收入"
|
||||
value={formatMoney(summary?.platform_income_amount_cent ?? 0)}
|
||||
/>
|
||||
<span className="admin-kpi-card__foot">
|
||||
订单 {formatMoney(summary?.normal_order_profit_amount_cent ?? 0)} · 提号{' '}
|
||||
{formatMoney(summary?.pickup_profit_amount_cent ?? 0)}
|
||||
</span>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} xl={6}>
|
||||
<Card className="admin-kpi-card">
|
||||
<div className="admin-kpi-card__icon">
|
||||
<BankOutlined />
|
||||
</div>
|
||||
<Statistic
|
||||
title="号主应得"
|
||||
value={formatMoney(summary?.owner_should_income_amount_cent ?? 0)}
|
||||
/>
|
||||
<span className="admin-kpi-card__foot">
|
||||
钱包入账 {formatMoney(summary?.owner_wallet_income_amount_cent ?? 0)}
|
||||
</span>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} xl={6}>
|
||||
<Card className="admin-kpi-card">
|
||||
<div className="admin-kpi-card__icon">
|
||||
<WalletOutlined />
|
||||
</div>
|
||||
<Statistic
|
||||
title="渠道净额"
|
||||
value={formatMoney(summary?.channel_net_amount_cent ?? 0)}
|
||||
/>
|
||||
<span className="admin-kpi-card__foot">
|
||||
已结算订单 {summary?.settled_order_count ?? 0} 笔
|
||||
</span>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} xl={6}>
|
||||
<Card className="admin-kpi-card">
|
||||
<div className="admin-kpi-card__icon">
|
||||
<WarningOutlined />
|
||||
</div>
|
||||
<Statistic title="资金异常" value={summary?.financial_exception_count ?? 0} />
|
||||
<span className="admin-kpi-card__foot">
|
||||
结算差异 {formatMoney(summary?.settlement_diff_amount_cent ?? 0)}
|
||||
</span>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} xl={6}>
|
||||
<Card className="admin-kpi-card">
|
||||
<div className="admin-kpi-card__icon">
|
||||
<BankOutlined />
|
||||
</div>
|
||||
<Statistic
|
||||
title="待打款"
|
||||
value={formatMoney(disbursement?.pending_payment_amount_cent ?? 0)}
|
||||
/>
|
||||
<span className="admin-kpi-card__foot">
|
||||
{disbursement?.pending_payment_count ?? 0} 笔 · 已打款{' '}
|
||||
{formatMoney(disbursement?.paid_amount_cent ?? 0)}
|
||||
</span>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} xl={6}>
|
||||
<Card className="admin-kpi-card">
|
||||
<div className="admin-kpi-card__icon">
|
||||
<WarningOutlined />
|
||||
</div>
|
||||
<Statistic
|
||||
title="提现待审"
|
||||
value={formatMoney(disbursement?.withdrawal_review_amount_cent ?? 0)}
|
||||
/>
|
||||
<span className="admin-kpi-card__foot">
|
||||
{disbursement?.withdrawal_review_count ?? 0} 笔 · 待打款{' '}
|
||||
{formatMoney(disbursement?.withdrawal_pending_amount_cent ?? 0)}
|
||||
</span>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 16]} className="admin-finance-mini">
|
||||
<Col xs={24} sm={12} xl={6}>
|
||||
<Card className="admin-pending-card">
|
||||
<div>
|
||||
<span className="admin-pending-card__label">待结算订单</span>
|
||||
<strong>{summary?.pending_settle_order_count ?? 0}</strong>
|
||||
</div>
|
||||
<span>待线下结算 {formatMoney(summary?.offline_settlement_pending_amount_cent ?? 0)}</span>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} xl={6}>
|
||||
<Card className="admin-pending-card">
|
||||
<div>
|
||||
<span className="admin-pending-card__label">线下提号</span>
|
||||
<strong>{pickup?.completed_count ?? 0}</strong>
|
||||
</div>
|
||||
<span>
|
||||
利润 {formatMoney(pickup?.profit_amount_cent ?? 0)} · 待结{' '}
|
||||
{formatMoney(pickup?.pending_amount_cent ?? 0)}
|
||||
</span>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} xl={6}>
|
||||
<Card className="admin-pending-card">
|
||||
<div>
|
||||
<span className="admin-pending-card__label">撞车商城净额</span>
|
||||
<strong>{formatMoney(mohong?.net_amount_cent ?? 0)}</strong>
|
||||
</div>
|
||||
<span>
|
||||
流水 {formatMoney(mohong?.flow_amount_cent ?? 0)} · 退款{' '}
|
||||
{formatMoney(mohong?.refund_amount_cent ?? 0)}
|
||||
</span>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} xl={6}>
|
||||
<Card className="admin-pending-card">
|
||||
<div>
|
||||
<span className="admin-pending-card__label">运营支出</span>
|
||||
<strong>{formatMoney(expense?.amount_cent ?? 0)}</strong>
|
||||
</div>
|
||||
<span>{expense?.count ?? 0} 笔已登记支出</span>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<DataTable<FinanceDaily>
|
||||
className="admin-finance-daily"
|
||||
title={<span>每日趋势</span>}
|
||||
extra={<Typography.Text type="secondary">数据更新于 {formatDateTime(dashboard.data?.generated_at)}</Typography.Text>}
|
||||
rowKey="date"
|
||||
columns={dailyColumns}
|
||||
dataSource={daily}
|
||||
loading={dashboard.isFetching}
|
||||
emptyDescription="所选区间没有财务数据"
|
||||
/>
|
||||
|
||||
<FilterBar
|
||||
onSearch={applyDetailFilters}
|
||||
onReset={appliedKeyword ? resetDetailFilters : undefined}
|
||||
loading={details.isFetching}
|
||||
extra={
|
||||
<Select
|
||||
value={dateType}
|
||||
style={{ width: 160 }}
|
||||
options={dateTypeOptions}
|
||||
onChange={value => {
|
||||
setDateType(value)
|
||||
setPage(1)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<FilterField label="订单号" width={280}>
|
||||
<Input
|
||||
allowClear
|
||||
value={keyword}
|
||||
placeholder="按订单号精确查询"
|
||||
onChange={event => setKeyword(event.target.value)}
|
||||
/>
|
||||
</FilterField>
|
||||
</FilterBar>
|
||||
|
||||
<DataTable<FinanceDetail>
|
||||
title={<span>财务明细</span>}
|
||||
extra={
|
||||
<Typography.Text type="secondary">
|
||||
{details.data ? `共 ${details.data.total} 条` : '加载中'}
|
||||
</Typography.Text>
|
||||
}
|
||||
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="所选区间没有财务明细"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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<ApiResponse<FinanceDashboard>>('/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<ApiResponse<Partial<FinanceDetailPage>>>(
|
||||
'/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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,3 +189,11 @@ export const withdrawalStatusMap: Record<string, StatusMeta> = {
|
||||
}
|
||||
|
||||
export const withdrawalStatusOptions = toOptions(withdrawalStatusMap)
|
||||
|
||||
export const financeStatusMap: Record<string, StatusMeta> = {
|
||||
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' },
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ const allNavGroups: NavGroup[] = [
|
||||
label: '财务',
|
||||
icon: <AccountBookOutlined />,
|
||||
children: [
|
||||
{ key: '/finance', label: '财务看板', permission: 'finance:view' },
|
||||
{ key: '/finance', label: '财务看板', permission: 'wallet:view' },
|
||||
{ key: '/withdrawals', label: '提现审核', permission: 'withdrawal:list' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user