feat(wallet): 补齐钱包提现与卖家资金页面
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { keepPreviousData, useQuery } from '@tanstack/react-query'
|
||||
import { Alert, Button, Input, Select, Space, Tag, Typography, type TableColumnsType } from 'antd'
|
||||
import { ReloadOutlined } from '@ant-design/icons'
|
||||
|
||||
import { fetchAdminPayments } 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 type { AdminPaymentQuery, AdminPaymentRow } from '@/features/admin/types'
|
||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||
import { formatDateTime, formatMoney, formatText } from '@/shared/utils/format'
|
||||
|
||||
type Filters = Pick<AdminPaymentQuery, 'order_no' | 'biz_type' | 'status' | 'provider'>
|
||||
|
||||
const emptyFilters: Filters = { order_no: '', biz_type: undefined, status: undefined, provider: undefined }
|
||||
|
||||
const statusOptions = [
|
||||
{ value: 'pending', label: '待支付' },
|
||||
{ value: 'paid', label: '已支付' },
|
||||
{ value: 'failed', label: '支付失败' },
|
||||
{ value: 'closed', label: '已关闭' },
|
||||
]
|
||||
|
||||
const bizTypeOptions = [
|
||||
{ value: 'order_pay', label: '订单支付' },
|
||||
{ value: 'main_recharge', label: '充值' },
|
||||
]
|
||||
|
||||
const providerOptions = [
|
||||
{ value: 'leshua', label: '拉卡拉' },
|
||||
{ value: 'shuncheng', label: '顺诚' },
|
||||
{ value: 'mock', label: '模拟支付' },
|
||||
]
|
||||
|
||||
const statusMeta: Record<string, { label: string; color: string }> = {
|
||||
pending: { label: '待支付', color: 'gold' },
|
||||
paid: { label: '已支付', color: 'green' },
|
||||
failed: { label: '支付失败', color: 'red' },
|
||||
closed: { label: '已关闭', color: 'default' },
|
||||
}
|
||||
|
||||
function statusTag(value?: string | null) {
|
||||
if (!value) return <Tag>未知</Tag>
|
||||
const meta = statusMeta[value] ?? { label: value, color: 'default' }
|
||||
return (
|
||||
<Tag bordered={false} color={meta.color}>
|
||||
{meta.label}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
|
||||
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<AdminPaymentRow> = [
|
||||
{
|
||||
title: '支付单号',
|
||||
key: 'payment',
|
||||
width: 240,
|
||||
fixed: 'left',
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack">
|
||||
<strong>{row.payment_no}</strong>
|
||||
<span className="admin-mono">{formatText(row.order_no)}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '用户',
|
||||
key: 'user',
|
||||
width: 170,
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack">
|
||||
<span>{formatText(row.user_phone)}</span>
|
||||
<span className="admin-mono">UID {row.user_id}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '渠道 / 支付方式',
|
||||
key: 'channel',
|
||||
width: 170,
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack">
|
||||
<span>{formatText(row.provider)}</span>
|
||||
<span className="admin-mono">{formatText(row.pay_way)}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '金额',
|
||||
dataIndex: 'amount_cent',
|
||||
width: 130,
|
||||
align: 'right',
|
||||
render: value => <strong>{formatMoney(value)}</strong>,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 110,
|
||||
render: value => statusTag(value),
|
||||
},
|
||||
{
|
||||
title: '支付时间',
|
||||
dataIndex: 'paid_at',
|
||||
width: 170,
|
||||
render: value => (value ? formatDateTime(value) : '—'),
|
||||
},
|
||||
{
|
||||
title: '失败原因',
|
||||
key: 'error',
|
||||
render: (_, row) =>
|
||||
row.error_message ? (
|
||||
<Typography.Text type="danger">
|
||||
{row.error_code ? `${row.error_code} · ` : ''}
|
||||
{row.error_message}
|
||||
</Typography.Text>
|
||||
) : (
|
||||
<Typography.Text type="secondary">—</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 170,
|
||||
render: value => formatDateTime(value),
|
||||
},
|
||||
]
|
||||
|
||||
export function AdminPaymentsPage() {
|
||||
const [filters, setFilters] = useState<Filters>(emptyFilters)
|
||||
const [applied, setApplied] = useState<Filters>(emptyFilters)
|
||||
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,
|
||||
biz_type: applied.biz_type,
|
||||
status: applied.status,
|
||||
provider: applied.provider,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
}),
|
||||
[applied, page, pageSize]
|
||||
)
|
||||
|
||||
const paymentsQuery = useQuery({
|
||||
queryKey: ['admin-payments', query],
|
||||
queryFn: () => fetchAdminPayments(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">共 {paymentsQuery.data?.total ?? 0} 条</Tag>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={paymentsQuery.isFetching}
|
||||
onClick={() => void paymentsQuery.refetch()}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<FilterBar
|
||||
loading={paymentsQuery.isFetching}
|
||||
onSearch={() => {
|
||||
setPage(1)
|
||||
setApplied(filters)
|
||||
}}
|
||||
onReset={() => {
|
||||
setFilters(emptyFilters)
|
||||
setApplied(emptyFilters)
|
||||
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
|
||||
allowClear
|
||||
value={filters.biz_type}
|
||||
options={bizTypeOptions}
|
||||
placeholder="全部业务"
|
||||
onChange={value => setFilters(current => ({ ...current, biz_type: value }))}
|
||||
/>
|
||||
</FilterField>
|
||||
<FilterField label="状态">
|
||||
<Select
|
||||
allowClear
|
||||
value={filters.status}
|
||||
options={statusOptions}
|
||||
placeholder="全部状态"
|
||||
onChange={value => setFilters(current => ({ ...current, status: value }))}
|
||||
/>
|
||||
</FilterField>
|
||||
<FilterField label="渠道">
|
||||
<Select
|
||||
allowClear
|
||||
value={filters.provider}
|
||||
options={providerOptions}
|
||||
placeholder="全部渠道"
|
||||
onChange={value => setFilters(current => ({ ...current, provider: value }))}
|
||||
/>
|
||||
</FilterField>
|
||||
</FilterBar>
|
||||
|
||||
{paymentsQuery.isError ? (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
className="admin-table-alert"
|
||||
message="支付流水加载失败"
|
||||
description={errorText(paymentsQuery.error)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<DataTable<AdminPaymentRow>
|
||||
columns={columns}
|
||||
dataSource={paymentsQuery.data?.items ?? []}
|
||||
rowKey="id"
|
||||
loading={paymentsQuery.isFetching}
|
||||
total={paymentsQuery.data?.total ?? 0}
|
||||
page={paymentsQuery.data?.page ?? page}
|
||||
pageSize={paymentsQuery.data?.page_size ?? pageSize}
|
||||
onPageChange={(nextPage, nextSize) => {
|
||||
setPage(nextPage)
|
||||
setPageSize(nextSize)
|
||||
}}
|
||||
emptyDescription="还没有支付流水"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
import { useState } from 'react'
|
||||
import { keepPreviousData, useQuery } from '@tanstack/react-query'
|
||||
import { Alert, Button, InputNumber, Select, Space, Tag, type TableColumnsType } from 'antd'
|
||||
import { ReloadOutlined } from '@ant-design/icons'
|
||||
|
||||
import { fetchAdminWalletLedger } 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 type { AdminWalletLedgerQuery, AdminWalletLedgerRow } from '@/features/admin/types'
|
||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||
import { formatDateTime, formatMoney, formatText } from '@/shared/utils/format'
|
||||
|
||||
const bizTypeOptions = [
|
||||
{ value: 'order', label: '订单结算' },
|
||||
{ value: 'order_pay', label: '订单支付' },
|
||||
{ value: 'withdraw_apply', label: '提现申请' },
|
||||
{ value: 'withdraw_complete', label: '提现到账' },
|
||||
{ value: 'admin_pickup', label: '线下提号' },
|
||||
{ value: 'invite_rebate', 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<AdminWalletLedgerRow> = [
|
||||
{
|
||||
title: '流水号',
|
||||
key: 'ledger',
|
||||
width: 240,
|
||||
fixed: 'left',
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack">
|
||||
<strong>{row.ledger_no}</strong>
|
||||
<span className="admin-mono">{formatText(row.order_no)}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '用户',
|
||||
key: 'user',
|
||||
width: 180,
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack">
|
||||
<span>{row.user_nickname || formatText(row.user_phone)}</span>
|
||||
<span className="admin-mono">UID {row.user_id}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '业务',
|
||||
dataIndex: 'biz_type',
|
||||
width: 140,
|
||||
render: value => <Tag bordered={false}>{formatText(value)}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '方向',
|
||||
dataIndex: 'direction',
|
||||
width: 90,
|
||||
render: value => (
|
||||
<Tag bordered={false} color={value === 'income' ? 'green' : 'volcano'}>
|
||||
{value === 'income' ? '入账' : '出账'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '金额',
|
||||
dataIndex: 'amount_cent',
|
||||
width: 130,
|
||||
align: 'right',
|
||||
render: (value: number, row) => (
|
||||
<strong style={{ color: row.direction === 'income' ? 'var(--success)' : 'var(--danger)' }}>
|
||||
{row.direction === 'income' ? '+' : '-'}
|
||||
{formatMoney(Math.abs(value))}
|
||||
</strong>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '变化后余额',
|
||||
dataIndex: 'balance_after_cent',
|
||||
width: 140,
|
||||
align: 'right',
|
||||
render: value => formatMoney(value),
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'remark',
|
||||
ellipsis: true,
|
||||
render: value => formatText(value),
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 170,
|
||||
render: value => formatDateTime(value),
|
||||
},
|
||||
]
|
||||
|
||||
export function AdminWalletLedgerPage() {
|
||||
const [filters, setFilters] = useState<{ user_id?: number; order_id?: number; biz_type?: string }>({})
|
||||
const [applied, setApplied] = useState(filters)
|
||||
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: AdminWalletLedgerQuery = {
|
||||
user_id: applied.user_id,
|
||||
order_id: applied.order_id,
|
||||
biz_type: applied.biz_type,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
}
|
||||
|
||||
const ledgerQuery = useQuery({
|
||||
queryKey: ['admin-wallet-ledger', query],
|
||||
queryFn: () => fetchAdminWalletLedger(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">共 {ledgerQuery.data?.total ?? 0} 条</Tag>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={ledgerQuery.isFetching}
|
||||
onClick={() => void ledgerQuery.refetch()}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<FilterBar
|
||||
loading={ledgerQuery.isFetching}
|
||||
onSearch={() => {
|
||||
setPage(1)
|
||||
setApplied(filters)
|
||||
}}
|
||||
onReset={() => {
|
||||
setFilters({})
|
||||
setApplied({})
|
||||
setPage(1)
|
||||
}}
|
||||
>
|
||||
<FilterField label="用户 ID" width={160}>
|
||||
<InputNumber
|
||||
min={1}
|
||||
style={{ width: '100%' }}
|
||||
value={filters.user_id}
|
||||
onChange={value => setFilters(current => ({ ...current, user_id: value ?? undefined }))}
|
||||
/>
|
||||
</FilterField>
|
||||
<FilterField label="订单 ID" width={160}>
|
||||
<InputNumber
|
||||
min={1}
|
||||
style={{ width: '100%' }}
|
||||
value={filters.order_id}
|
||||
onChange={value => setFilters(current => ({ ...current, order_id: value ?? undefined }))}
|
||||
/>
|
||||
</FilterField>
|
||||
<FilterField label="业务类型">
|
||||
<Select
|
||||
allowClear
|
||||
value={filters.biz_type}
|
||||
options={bizTypeOptions}
|
||||
placeholder="全部业务"
|
||||
onChange={value => setFilters(current => ({ ...current, biz_type: value }))}
|
||||
/>
|
||||
</FilterField>
|
||||
</FilterBar>
|
||||
|
||||
{ledgerQuery.isError ? (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
className="admin-table-alert"
|
||||
message="钱包流水加载失败"
|
||||
description={errorText(ledgerQuery.error)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<DataTable<AdminWalletLedgerRow>
|
||||
columns={columns}
|
||||
dataSource={ledgerQuery.data?.items ?? []}
|
||||
rowKey="id"
|
||||
loading={ledgerQuery.isFetching}
|
||||
total={ledgerQuery.data?.total ?? 0}
|
||||
page={ledgerQuery.data?.page ?? page}
|
||||
pageSize={ledgerQuery.data?.page_size ?? pageSize}
|
||||
onPageChange={(nextPage, nextSize) => {
|
||||
setPage(nextPage)
|
||||
setPageSize(nextSize)
|
||||
}}
|
||||
emptyDescription="还没有钱包流水"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -98,6 +98,10 @@ import type {
|
||||
FinanceDetailPage,
|
||||
FinanceDisbursementPage,
|
||||
FinanceDisbursementQuery,
|
||||
AdminPaymentPage,
|
||||
AdminPaymentQuery,
|
||||
AdminWalletLedgerPage,
|
||||
AdminWalletLedgerQuery,
|
||||
ManualDisbursementPayload,
|
||||
OperatingExpensePage,
|
||||
OperatingExpensePayload,
|
||||
@@ -1365,3 +1369,30 @@ export async function voidOperatingExpense(id: number | string, reason: string)
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function fetchAdminPayments(query: AdminPaymentQuery = {}) {
|
||||
const { data } = await adminClient.get<ApiResponse<Partial<AdminPaymentPage>>>(
|
||||
'/admin/payments',
|
||||
{ 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,
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchAdminWalletLedger(query: AdminWalletLedgerQuery = {}) {
|
||||
const { data } = await adminClient.get<ApiResponse<Partial<AdminWalletLedgerPage>>>(
|
||||
'/admin/wallet/ledger',
|
||||
{ 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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -534,6 +534,81 @@ export interface FinanceDetailPage {
|
||||
page_size: number
|
||||
}
|
||||
|
||||
export interface AdminPaymentRow {
|
||||
id: number
|
||||
payment_no: string
|
||||
order_id: number
|
||||
order_no: string
|
||||
user_id: number
|
||||
payment_config_id: number
|
||||
user_phone: string
|
||||
provider: string
|
||||
merchant_id: string
|
||||
third_order_id: string
|
||||
provider_order_id: string
|
||||
pay_way: string
|
||||
amount_cent: number
|
||||
biz_type: string
|
||||
status: string
|
||||
error_code: string
|
||||
error_message: string
|
||||
paid_at?: string | null
|
||||
notified_at?: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface AdminPaymentPage {
|
||||
items: AdminPaymentRow[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
export interface AdminPaymentQuery {
|
||||
user_id?: number
|
||||
order_id?: number
|
||||
order_no?: string
|
||||
biz_type?: string
|
||||
status?: string
|
||||
provider?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
}
|
||||
|
||||
export interface AdminWalletLedgerRow {
|
||||
id: number
|
||||
ledger_no: string
|
||||
user_id: number
|
||||
user_phone: string
|
||||
user_nickname: string
|
||||
order_id?: number | null
|
||||
order_no: string
|
||||
direction: string
|
||||
amount_cent: number
|
||||
balance_after_cent: number
|
||||
balance_type: string
|
||||
biz_type: string
|
||||
biz_no: string
|
||||
remark: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface AdminWalletLedgerPage {
|
||||
items: AdminWalletLedgerRow[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
export interface AdminWalletLedgerQuery {
|
||||
user_id?: number
|
||||
order_id?: number
|
||||
biz_type?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
}
|
||||
|
||||
export interface AdminAnnouncementQuery {
|
||||
category?: string
|
||||
status?: string
|
||||
|
||||
Reference in New Issue
Block a user