import { useEffect, useState } from 'react'; import { Button, Card, DatePicker, Descriptions, Drawer, Form, Modal, 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; billNo: string; billDate: string; redeemCount: number; redeemAmount: number; settlementRate: number; payoutAmount: number; status: string; paidAt?: string | null; store?: { id: string; name: string; cityName: string; phone?: string }; }; type StoreOption = { id: string; name: string; phone: string }; const STATUS_LABELS: Record = { UNPAID: '未打款', PAID: '已打款', }; const STATUS_COLORS: Record = { UNPAID: 'red', 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-bills', () => { 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); const [selectedKeys, setSelectedKeys] = useState([]); const [batchLoading, setBatchLoading] = useState(false); useEffect(() => { void request>(`/admin/stores?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`) .then((res) => setStores(res.items)) .catch(() => {}); }, []); function confirmPay(ids: string[], amountHint?: number) { Modal.confirm({ title: '确认打款?', content: `将确认 ${ids.length} 笔门店对账单${amountHint != null ? `,合计约 ¥${amountHint.toFixed(2)}` : ''}。此操作不可撤销。`, okText: '确认打款', cancelText: '取消', onOk: async () => { if (ids.length === 1) { await request(`/admin/store-bills/${ids[0]}/confirm`, { method: 'POST' }); } else { setBatchLoading(true); try { await request('/admin/store-bills/batch-confirm', { method: 'POST', body: JSON.stringify({ ids }), }); } finally { setBatchLoading(false); } } message.success('已确认打款'); setSelectedKeys([]); reload(); }, }); } async function openDetail(id: string) { const d = await request>(`/admin/store-bills/${id}`); setDetail(d); setDrawerOpen(true); } 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-bills/export?${qs}`); downloadExcelCsv(result.csv, `门店对账单_${filters.dateFrom || 'all'}_${filters.dateTo || 'all'}.csv`); message.success(`已导出 ${result.count} 条`); } finally { setExporting(false); } } const summary = data?.summary; const selectedRows = (data?.items ?? []).filter((r) => selectedKeys.includes(r.id)); const selectedAmount = selectedRows.reduce((s, r) => s + Number(r.payoutAmount), 0); const columns: ColumnsType = [ { title: '账单号', dataIndex: 'billNo', width: 170, ellipsis: true }, { title: '账单日', dataIndex: 'billDate', width: 110, render: (v) => String(v || '').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: 'redeemCount', width: 90 }, { title: '核销金额', dataIndex: 'redeemAmount', width: 110, render: (v) => `¥${Number(v).toFixed(2)}`, }, { title: '结算比例', dataIndex: 'settlementRate', width: 90, render: (v) => `${Math.round(Number(v) * 100)}%`, }, { title: '应付金额', dataIndex: 'payoutAmount', width: 110, render: (v) => `¥${Number(v).toFixed(2)}`, }, { title: '状态', dataIndex: 'status', width: 90, render: (s) => {STATUS_LABELS[s] || s}, }, { title: '操作', width: 160, fixed: 'right', render: (_, row) => ( {row.status === 'UNPAID' && ( )} ), }, ]; return (
门店对账单 按核销日汇总(每日 8:00 自动出账);未打款红色、已打款绿色 {summary && ( )}
{ setFilters({ storeId: v.storeId || '', status: v.status || '', dateFrom: v.range?.[0] ? v.range[0].format('YYYY-MM-DD') : '', dateTo: v.range?.[1] ? v.range[1].format('YYYY-MM-DD') : '', }); setPage(1); }} > ({ value, label }))} />
({ disabled: r.status !== 'UNPAID' }), }} scroll={{ x: 1200 }} pagination={{ current: page, pageSize, total: data?.total ?? 0, showSizeChanger: true, onChange: (p, ps) => { setPage(p); setPageSize(ps); }, }} /> setDrawerOpen(false)} width={520}> {detail && ( <> {String(detail.billNo)} {String(detail.billDate || '').slice(0, 10)} ¥{Number(detail.payoutAmount ?? 0).toFixed(2)} {STATUS_LABELS[String(detail.status)] || String(detail.status)} {detail.paidAt ? fmtTime(String(detail.paidAt)) : '—'} 核销明细
>) || []} columns={[ { title: '核销单号', render: (_, r) => String((r.redeemRecord as { redeemNo?: string })?.redeemNo || '—'), }, { title: '金额', dataIndex: 'payoutAmount', render: (v) => `¥${Number(v).toFixed(2)}`, }, { title: '状态', dataIndex: 'status', render: (s) => (s === 'PAID' ? '已打款' : '未打款'), }, ]} /> )} ); }