import { useEffect, useState } from 'react'; import { Button, Card, DatePicker, Descriptions, Drawer, Form, Select, Space, Statistic, Table, Tag, Typography, message, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import type { Dayjs } from 'dayjs'; import { request, type Paginated } from '../lib/api'; import { ADMIN_OPTIONS_PAGE_SIZE, fmtTime } from '../lib/constants'; import { downloadExcelCsv } from '../lib/exportExcel'; import { useAdminList } from '../lib/useAdminList'; type Row = { id: string; redeemAmount: number; payoutAmount: number; settlementRate: number; status: string; expectedPayAt: string; paidAt?: string; createdAt: string; store?: { id: string; name: string; cityName: string; phone?: string }; redeemRecord?: { redeemNo: string; amount?: number; createdAt?: string }; }; type StoreOption = { id: string; name: string; phone: string }; const PAYOUT_STATUS_LABELS: Record = { PENDING: '待打款', PAID: '已打款', }; const PAYOUT_STATUS_COLORS: Record = { PENDING: 'orange', PAID: 'green', }; export default function StoreBillsPage() { const [form] = Form.useForm(); const [filters, setFilters] = useState>({}); const [stores, setStores] = useState([]); const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList( '/admin/store-payouts', () => { 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 [exporting, setExporting] = useState(false); useEffect(() => { void request>(`/admin/stores?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`) .then((res) => setStores(res.items)) .catch(() => {}); }, []); async function confirmPayout(id: string) { await request(`/admin/store-payouts/${id}/confirm`, { method: 'POST', body: JSON.stringify({ remark: '财务确认打款' }), }); message.success('已确认打款'); reload(); } async function exportExcel() { setExporting(true); try { 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); const result = await request<{ csv: string; count: number }>( `/admin/store-payouts/export?${qs}`, ); downloadExcelCsv(result.csv, `门店账单_${filters.dateFrom || 'all'}_${filters.dateTo || 'all'}.csv`); message.success(`已导出 ${result.count} 条`); } finally { setExporting(false); } } const summary = data?.summary; const columns: ColumnsType = [ { title: '核销日期', width: 110, render: (_, r) => (r.redeemRecord?.createdAt || r.createdAt || '').toString().slice(0, 10) || '—', }, { title: '门店', dataIndex: ['store', 'name'], width: 140, ellipsis: true }, { title: '登录手机', dataIndex: ['store', 'phone'], width: 120, render: (v) => v || '—' }, { title: '城市', dataIndex: ['store', 'cityName'], width: 90 }, { title: '核销单号', dataIndex: ['redeemRecord', 'redeemNo'], width: 160, ellipsis: true, render: (v) => v || '—', }, { title: '核销面额', dataIndex: 'redeemAmount', width: 100, render: (v) => `¥${v}` }, { title: '核销比例', dataIndex: 'settlementRate', width: 90, render: (v) => (v != null ? `${Math.round(Number(v) * 100)}%` : '—'), }, { title: '应付门店', dataIndex: 'payoutAmount', width: 100, render: (v) => `¥${v}`, }, { title: '打款状态', dataIndex: 'status', width: 100, render: (s) => {PAYOUT_STATUS_LABELS[s] || s}, }, { title: '预计打款(T+1)', dataIndex: 'expectedPayAt', width: 160, render: fmtTime }, { title: '操作', width: 140, fixed: 'right', render: (_, row) => ( {row.status === 'PENDING' && ( )} ), }, ]; return (
门店账单 T+1 结算:按日列出门店核销订单,应付 = 核销面额 × 门店核销比例 {summary && ( )}
{ setFilters({ storeId: v.storeId || '', status: v.status || '', dateFrom: v.dateRange?.[0]?.format('YYYY-MM-DD') || '', dateTo: v.dateRange?.[1]?.format('YYYY-MM-DD') || '', }); setPage(1); }} > ({ value, label }))} />
{ setPage(p); setPageSize(ps); }, }} /> setDrawerOpen(false)}> {detail && ( {String((detail.store as { name?: string })?.name ?? '—')} {String((detail.redeemRecord as { redeemNo?: string })?.redeemNo ?? '—')} ¥{String(detail.redeemAmount)} ¥{String(detail.payoutAmount)} {detail.settlementRate ? `${Math.round(Number(detail.settlementRate) * 100)}%` : '—'} {PAYOUT_STATUS_LABELS[String(detail.status)] || String(detail.status)} {fmtTime(String(detail.expectedPayAt))} {detail.paidAt ? fmtTime(String(detail.paidAt)) : '—'} )} ); }