import { useEffect, useState } from 'react'; import { Button, DatePicker, Descriptions, Drawer, Form, Input, Modal, Select, Space, Table, Tag, Typography, message, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import type { Dayjs } from 'dayjs'; import { STORE_WITHDRAW_STATUS_LABELS, type StoreWithdrawStatus } from '@dukang/shared-types'; import { request, type Paginated } from '../lib/api'; import { ADMIN_OPTIONS_PAGE_SIZE, fmtTime } from '../lib/constants'; import { useAdminList } from '../lib/useAdminList'; type Row = { id: string; withdrawNo: string; amount: number; payoutCount: number; status: StoreWithdrawStatus; appliedAt: string; reviewedAt?: string | null; paidAt?: string | null; paymentRef?: string | null; rejectReason?: string | null; overdue?: boolean; store?: { id: string; name: string; cityName: string; phone?: string }; }; type StoreOption = { id: string; name: string; phone: string }; const STATUS_COLORS: Record = { PENDING_REVIEW: 'orange', REJECTED: 'red', PAID: 'green', }; export default function StoreWithdrawalsPage() { const [form] = Form.useForm(); const [filters, setFilters] = useState>({ status: 'PENDING_REVIEW' }); const [stores, setStores] = useState([]); const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList( '/admin/store-withdrawals', () => { const qs = new URLSearchParams(); if (filters.status) qs.set('status', filters.status); if (filters.storeId) qs.set('storeId', filters.storeId); if (filters.dateFrom) qs.set('dateFrom', filters.dateFrom); if (filters.dateTo) qs.set('dateTo', filters.dateTo); return qs; }, [filters], ); const [detail, setDetail] = useState | null>(null); const [drawerOpen, setDrawerOpen] = useState(false); const [overdueSummary, setOverdueSummary] = useState<{ pendingCount: number; overdueCount: number; } | null>(null); useEffect(() => { void request>(`/admin/stores?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`) .then((res) => setStores(res.items)) .catch(() => {}); void request<{ pendingCount: number; overdueCount: number }>( '/admin/store-withdrawals/overdue-summary', ) .then(setOverdueSummary) .catch(() => {}); }, []); async function openDetail(id: string) { const d = await request>(`/admin/store-withdrawals/${id}`); setDetail(d); setDrawerOpen(true); } function approve(id: string) { let paymentRef = ''; Modal.confirm({ title: '审核通过并标记已结算?', content: ( { paymentRef = e.target.value; }} /> ), okText: '通过并结算', cancelText: '取消', onOk: async () => { await request(`/admin/store-withdrawals/${id}/approve`, { method: 'POST', body: JSON.stringify({ paymentRef: paymentRef.trim() || undefined }), }); message.success('已通过并标记已结算'); setDrawerOpen(false); reload(); void request<{ pendingCount: number; overdueCount: number }>( '/admin/store-withdrawals/overdue-summary', ) .then(setOverdueSummary) .catch(() => {}); }, }); } function reject(id: string) { let reason = ''; Modal.confirm({ title: '驳回提现申请?', content: ( { reason = e.target.value; }} /> ), okText: '确认驳回', okButtonProps: { danger: true }, cancelText: '取消', onOk: async () => { if (!reason.trim()) { message.error('请填写驳回理由'); throw new Error('reason required'); } await request(`/admin/store-withdrawals/${id}/reject`, { method: 'POST', body: JSON.stringify({ reason: reason.trim() }), }); message.success('已驳回'); setDrawerOpen(false); reload(); }, }); } const columns: ColumnsType = [ { title: '提现单号', dataIndex: 'withdrawNo', width: 180, render: (v, row) => ( void openDetail(row.id)}>{v} {row.overdue ? 超时 : null} ), }, { title: '门店', dataIndex: ['store', 'name'], width: 160, render: (_, row) => (
{row.store?.name || '—'}
{row.store?.cityName} {row.store?.phone}
), }, { title: '金额', dataIndex: 'amount', width: 110, render: (v) => `¥${Number(v).toFixed(2)}`, }, { title: '明细笔数', dataIndex: 'payoutCount', width: 90 }, { title: '状态', dataIndex: 'status', width: 100, render: (v: StoreWithdrawStatus) => ( {STORE_WITHDRAW_STATUS_LABELS[v] ?? v} ), }, { title: '申请时间', dataIndex: 'appliedAt', width: 170, render: (v) => fmtTime(v), }, { title: '操作', key: 'actions', width: 180, fixed: 'right', render: (_, row) => ( {row.status === 'PENDING_REVIEW' ? ( <> ) : null} ), }, ]; const detailItems = (detail?.items as Array> | undefined) ?? []; const storeAccount = detail?.storeAccount as | { name?: string; phone?: string; bankAccountName?: string; bankAccountNo?: string; bankBranch?: string; } | undefined; return (
门店提现审 {overdueSummary ? ( 待审 {overdueSummary.pendingCount} 笔 {overdueSummary.overdueCount > 0 ? ( {' '} · 超时未审 {overdueSummary.overdueCount} 笔(FIN-003) ) : null} ) : null}
{ const range = v.range as [Dayjs, Dayjs] | undefined; setFilters({ status: v.status || '', storeId: v.storeId || '', dateFrom: range?.[0]?.format('YYYY-MM-DD') || '', dateTo: range?.[1]?.format('YYYY-MM-DD') || '', }); }} > ({ value: s.id, label: `${s.name} (${s.phone})`, }))} />
(row.overdue ? 'ant-table-row-selected' : '')} pagination={{ current: page, pageSize, total: data?.total ?? 0, showSizeChanger: true, onChange: (p, ps) => { setPage(p); setPageSize(ps); }, }} /> setDrawerOpen(false)} extra={ detail?.status === 'PENDING_REVIEW' ? ( ) : null } > {detail ? ( <> {String(detail.withdrawNo)} {STORE_WITHDRAW_STATUS_LABELS[detail.status as StoreWithdrawStatus] ?? String(detail.status)} {detail.overdue ? 超时 : null} ¥{Number(detail.amount).toFixed(2)} {String(detail.payoutCount)} {fmtTime(String(detail.appliedAt))} {detail.rejectReason ? ( {String(detail.rejectReason)} ) : null} {detail.paymentRef ? ( {String(detail.paymentRef)} ) : null} {storeAccount?.bankAccountName || '—'} {storeAccount?.bankAccountNo || '—'} {storeAccount?.bankBranch || '—'} 关联结算明细
String((r as { id?: string }).id)} pagination={false} dataSource={detailItems} columns={[ { title: '核销单号', render: (_, r) => { const payout = (r as { storePayout?: { redeemRecord?: { redeemNo?: string } } }) .storePayout; return payout?.redeemRecord?.redeemNo || '—'; }, }, { title: '结算额', render: (_, r) => { const payout = (r as { storePayout?: { payoutAmount?: number } }).storePayout; return `¥${Number(payout?.payoutAmount ?? 0).toFixed(2)}`; }, }, ]} /> ) : null} ); }