{
+ return [
+ {
+ title: '提现单',
+ dataIndex: 'withdraw_no',
+ width: 230,
+ fixed: 'left',
+ render: (_, row) => (
+
+ {canView ? (
+
+ ) : (
+ {row.withdraw_no}
+ )}
+ {row.user_nickname || row.user_phone || `UID ${row.user_id}`}
+
+ {formatText(row.user_phone)}
+ {row.distribution_site_name ? ` · ${row.distribution_site_name}` : ''}
+
+
+ ),
+ },
+ {
+ title: '提现金额',
+ key: 'amount',
+ width: 150,
+ align: 'right',
+ render: (_, row) => (
+
+ {formatMoney(row.amount_cent)}
+ 手续费 {formatMoney(row.fee_cent)}
+ 到账 {formatMoney(row.actual_amount_cent)}
+
+ ),
+ },
+ {
+ title: '收款账户',
+ key: 'account',
+ width: 220,
+ render: (_, row) => (
+
+
+ {formatText(row.account_type)} · {formatText(row.account_name)}
+
+ {formatText(row.account_no)}
+
+ {formatText(row.bank_name)}
+ {row.bank_branch ? ` · ${row.bank_branch}` : ''}
+
+
+ ),
+ },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ width: 110,
+ render: value => ,
+ },
+ {
+ title: '申请时间',
+ dataIndex: 'created_at',
+ width: 170,
+ render: value => formatDateTime(value),
+ },
+ {
+ title: '审核 / 打款',
+ key: 'audit',
+ width: 190,
+ render: (_, row) => (
+
+ 审核 {row.reviewed_by_name || '—'}
+ 打款 {row.paid_by_name || '—'}
+ {formatDateTime(row.paid_at || row.reviewed_at)}
+
+ ),
+ },
+ {
+ title: '操作',
+ key: 'actions',
+ width: 96,
+ fixed: 'right',
+ render: (_, row) =>
+ canView ? (
+ } onClick={() => onOpen(row)}>
+ 处理
+
+ ) : (
+ 无权限
+ ),
+ },
+ ]
+}
+
+export function AdminWithdrawalsPage() {
+ const [draft, setDraft] = useState(emptyFilters)
+ const [applied, setApplied] = useState(emptyFilters)
+ const [page, setPage] = useState(1)
+ const [pageSize, setPageSize] = useState(20)
+ const [detailId, setDetailId] = useState(null)
+ const [detailOpen, setDetailOpen] = useState(false)
+ const permissions = useAdminSessionStore(state => state.admin?.permissions)
+ const canView = Boolean(
+ permissions?.includes('*') ||
+ permissions?.includes('withdrawal:list') ||
+ permissions?.includes('withdrawal:detail')
+ )
+
+ const { data, isPending, isFetching, isError, error, refetch } = useQuery({
+ queryKey: ['admin-withdrawals', applied, page, pageSize],
+ queryFn: () => fetchAdminWithdrawals({ ...applied, page, page_size: pageSize }),
+ placeholderData: keepPreviousData,
+ })
+
+ const columns = useMemo(() => buildColumns(row => {
+ setDetailId(row.id)
+ setDetailOpen(true)
+ }, canView), [canView])
+
+ const patchFilter = (patch: Partial) => {
+ setDraft(current => ({ ...current, ...patch }))
+ }
+
+ const applyFilters = () => {
+ setApplied({ ...draft })
+ setPage(1)
+ }
+
+ const resetFilters = () => {
+ setDraft(emptyFilters)
+ setApplied(emptyFilters)
+ setPage(1)
+ }
+
+ const hasActiveFilters = Boolean(draft.status)
+ const errorText = error instanceof Error ? error.message : '请稍后重试或重新登录后台。'
+
+ return (
+
+
+ 共 {data?.total ?? 0} 笔
+ {isFetching && !isPending ? 正在更新 : null}
+
+ }
+ extra={
+ } onClick={() => void refetch()} loading={isFetching}>
+ 刷新数据
+
+ }
+ />
+
+
+
+
+
+
+ {isError ? (
+ void refetch()}>
+ 重试
+
+ }
+ />
+ ) : null}
+
+
+ title={提现申请}
+ extra={
+
+ {data ? `第 ${data.page} 页 · 当前 ${data.items.length} 条` : '加载中'}
+
+ }
+ rowKey="id"
+ columns={columns}
+ dataSource={data?.items ?? []}
+ loading={isFetching}
+ total={data?.total ?? 0}
+ page={data?.page ?? page}
+ pageSize={data?.page_size ?? pageSize}
+ onPageChange={(nextPage, nextPageSize) => {
+ setPage(nextPageSize !== pageSize ? 1 : nextPage)
+ setPageSize(nextPageSize)
+ }}
+ onRowClick={canView ? row => {
+ setDetailId(row.id)
+ setDetailOpen(true)
+ } : undefined}
+ emptyDescription="没有匹配的提现申请"
+ />
+
+ {canView ? (
+ setDetailOpen(false)}
+ onChanged={() => {
+ void refetch()
+ }}
+ />
+ ) : null}
+
+ )
+}
diff --git a/frontend/src/features/admin/api.ts b/frontend/src/features/admin/api.ts
index 4454daa..e360458 100644
--- a/frontend/src/features/admin/api.ts
+++ b/frontend/src/features/admin/api.ts
@@ -13,6 +13,9 @@ import type {
AdminUserItem,
AdminUserPage,
AdminUserQuery,
+ AdminWithdrawal,
+ AdminWithdrawalPage,
+ AdminWithdrawalQuery,
DashboardData,
} from '@/features/admin/types'
@@ -232,3 +235,46 @@ export async function approveAdminRefund(id: number | string) {
export async function rejectAdminRefund(id: number | string, action: 'restore' | 'close') {
await adminClient.post(`/admin/orders/${id}/refund/reject`, { action })
}
+
+export async function fetchAdminWithdrawals(query: AdminWithdrawalQuery) {
+ const { data } = await adminClient.get>>(
+ '/admin/withdrawals',
+ { 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 fetchAdminWithdrawal(id: number | string) {
+ const { data } = await adminClient.get>(
+ `/admin/withdrawals/${id}`
+ )
+ return data.data
+}
+
+export async function reviewAdminWithdrawal(
+ id: number | string,
+ approved: boolean,
+ remark: string
+) {
+ const { data } = await adminClient.post>(
+ `/admin/withdrawals/${id}/review`,
+ { approved, remark }
+ )
+ return data.data
+}
+
+export async function confirmAdminWithdrawalPayment(
+ id: number | string,
+ payload: { payment_proof_url?: string; remark?: string }
+) {
+ const { data } = await adminClient.post>(
+ `/admin/withdrawals/${id}/confirm-payment`,
+ payload
+ )
+ return data.data
+}
diff --git a/frontend/src/features/admin/labels.ts b/frontend/src/features/admin/labels.ts
index d6c344a..b6537ab 100644
--- a/frontend/src/features/admin/labels.ts
+++ b/frontend/src/features/admin/labels.ts
@@ -179,3 +179,13 @@ export const riskStatusMap: Record = {
export const userStatusOptions = toOptions(userStatusMap)
export const realnameStatusOptions = toOptions(realnameStatusMap)
export const riskStatusOptions = toOptions(riskStatusMap)
+
+export const withdrawalStatusMap: Record = {
+ pending: { label: '待审核', tone: 'gold' },
+ processing: { label: '待打款', tone: 'blue' },
+ completed: { label: '已完成', tone: 'success' },
+ rejected: { label: '已驳回', tone: 'red' },
+ cancelled: { label: '已取消', tone: 'default' },
+}
+
+export const withdrawalStatusOptions = toOptions(withdrawalStatusMap)
diff --git a/frontend/src/features/admin/types.ts b/frontend/src/features/admin/types.ts
index af2b500..59ed69e 100644
--- a/frontend/src/features/admin/types.ts
+++ b/frontend/src/features/admin/types.ts
@@ -304,3 +304,50 @@ export interface AdminUserPage {
page: number
page_size: number
}
+
+export interface AdminWithdrawalQuery {
+ page?: number
+ page_size?: number
+ status?: string
+ user_id?: number
+}
+
+export interface AdminWithdrawal {
+ id: number
+ withdraw_no: string
+ user_id: number
+ distribution_site_id: number
+ distribution_site_name: string
+ distribution_site_code: string
+ user_nickname: string
+ user_phone: string
+ amount_cent: number
+ fee_cent: number
+ actual_amount_cent: number
+ payment_account_id?: number | null
+ account_type: string
+ account_name: string
+ account_no: string
+ bank_name: string
+ bank_branch: string
+ certificate_urls: string[]
+ status: string
+ reviewed_by?: number | null
+ reviewed_by_name: string
+ reviewed_at?: string | null
+ review_remark: string
+ paid_by?: number | null
+ paid_by_name: string
+ paid_at?: string | null
+ payment_proof_url: string
+ payment_remark: string
+ created_at: string
+ updated_at: string
+}
+
+export interface AdminWithdrawalPage {
+ items: AdminWithdrawal[]
+ total: number
+ page: number
+ page_size: number
+}
diff --git a/frontend/src/layouts/AdminLayout.tsx b/frontend/src/layouts/AdminLayout.tsx
index 14f8d1c..e74a9ce 100644
--- a/frontend/src/layouts/AdminLayout.tsx
+++ b/frontend/src/layouts/AdminLayout.tsx
@@ -84,7 +84,7 @@ const allNavGroups: NavGroup[] = [
icon: ,
children: [
{ key: '/finance', label: '财务看板', permission: 'finance:view' },
- { key: '/withdrawals', label: '提现审核', permission: 'withdrawal:view' },
+ { key: '/withdrawals', label: '提现审核', permission: 'withdrawal:list' },
],
},
{