webadmin增加财务模块

This commit is contained in:
2026-07-14 15:16:41 +08:00
parent 450dc44d0e
commit d61a43cc8c
15 changed files with 1352 additions and 132 deletions
+110 -17
View File
@@ -1,8 +1,24 @@
import { useEffect, useState } from 'react';
import { Button, Descriptions, Drawer, Form, Select, Space, Table, Tag, Typography, message } from 'antd';
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 = {
@@ -15,7 +31,7 @@ type Row = {
paidAt?: string;
createdAt: string;
store?: { id: string; name: string; cityName: string; phone?: string };
redeemRecord?: { redeemNo: string; amount?: number };
redeemRecord?: { redeemNo: string; amount?: number; createdAt?: string };
};
type StoreOption = { id: string; name: string; phone: string };
@@ -40,12 +56,15 @@ export default function StoreBillsPage() {
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<Record<string, unknown> | null>(null);
const [drawerOpen, setDrawerOpen] = useState(false);
const [exporting, setExporting] = useState(false);
useEffect(() => {
void request<Paginated<StoreOption>>(`/admin/stores?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`)
@@ -62,7 +81,33 @@ export default function StoreBillsPage() {
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<Row> = [
{
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 },
@@ -75,10 +120,16 @@ export default function StoreBillsPage() {
},
{ title: '核销面额', dataIndex: 'redeemAmount', width: 100, render: (v) => `¥${v}` },
{
title: '到账金额',
title: '核销比例',
dataIndex: 'settlementRate',
width: 90,
render: (v) => (v != null ? `${Math.round(Number(v) * 100)}%` : '—'),
},
{
title: '应付门店',
dataIndex: 'payoutAmount',
width: 100,
render: (v, row) => `¥${v}${row.settlementRate ? ` (${Math.round(row.settlementRate * 100)}%)` : ''}`,
render: (v) => `¥${v}`,
},
{
title: '打款状态',
@@ -86,8 +137,7 @@ export default function StoreBillsPage() {
width: 100,
render: (s) => <Tag color={PAYOUT_STATUS_COLORS[s] || 'default'}>{PAYOUT_STATUS_LABELS[s] || s}</Tag>,
},
{ title: '预计打款', dataIndex: 'expectedPayAt', width: 160, render: fmtTime },
{ title: '实际打款', dataIndex: 'paidAt', width: 160, render: (v) => (v ? fmtTime(String(v)) : '—') },
{ title: '预计打款(T+1)', dataIndex: 'expectedPayAt', width: 160, render: fmtTime },
{
title: '操作',
width: 140,
@@ -117,17 +167,39 @@ export default function StoreBillsPage() {
return (
<div>
<Space direction="vertical" size={0} style={{ marginBottom: 16 }}>
<Typography.Title level={4} style={{ margin: 0 }}></Typography.Title>
<Typography.Title level={4} style={{ margin: 0 }}>
</Typography.Title>
<Typography.Text type="secondary">
T+1
T+1 = ×
</Typography.Text>
</Space>
{summary && (
<Card size="small" style={{ marginBottom: 16 }}>
<Space size="large" wrap>
<Statistic title="明细笔数" value={summary.count} />
<Statistic title="核销面额合计" value={summary.redeemAmount ?? 0} prefix="¥" precision={2} />
<Statistic title="应付门店合计" value={summary.payoutAmount ?? summary.totalAmount} prefix="¥" precision={2} />
</Space>
</Card>
)}
<Form
form={form}
layout="inline"
style={{ marginBottom: 16 }}
onFinish={(v) => {
setFilters(v);
onFinish={(v: {
storeId?: string;
status?: string;
dateRange?: [Dayjs, Dayjs];
}) => {
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);
}}
>
@@ -152,20 +224,39 @@ export default function StoreBillsPage() {
options={Object.entries(PAYOUT_STATUS_LABELS).map(([value, label]) => ({ value, label }))}
/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit"></Button>
<Form.Item name="dateRange" label="核销日期">
<DatePicker.RangePicker />
</Form.Item>
<Form.Item>
<Button onClick={() => { form.resetFields(); setFilters({}); setPage(1); }}></Button>
<Button type="primary" htmlType="submit">
</Button>
</Form.Item>
<Form.Item>
<Button
onClick={() => {
form.resetFields();
setFilters({});
setPage(1);
}}
>
</Button>
</Form.Item>
<Form.Item>
<Button loading={exporting} onClick={() => void exportExcel()}>
Excel
</Button>
</Form.Item>
</Form>
<Table
rowKey="id"
className="admin-table-nowrap"
loading={loading}
columns={columns}
dataSource={data?.items ?? []}
scroll={{ x: 1200 }}
scroll={{ x: 1300 }}
pagination={{
current: page,
pageSize,
@@ -177,15 +268,18 @@ export default function StoreBillsPage() {
},
}}
/>
<Drawer title="账单详情" width={520} open={drawerOpen} onClose={() => setDrawerOpen(false)}>
{detail && (
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="门店">{String((detail.store as { name?: string })?.name ?? '—')}</Descriptions.Item>
<Descriptions.Item label="门店">
{String((detail.store as { name?: string })?.name ?? '—')}
</Descriptions.Item>
<Descriptions.Item label="核销单号">
{String((detail.redeemRecord as { redeemNo?: string })?.redeemNo ?? '—')}
</Descriptions.Item>
<Descriptions.Item label="核销面额">¥{String(detail.redeemAmount)}</Descriptions.Item>
<Descriptions.Item label="到账金额">¥{String(detail.payoutAmount)}</Descriptions.Item>
<Descriptions.Item label="应付金额">¥{String(detail.payoutAmount)}</Descriptions.Item>
<Descriptions.Item label="结算比例">
{detail.settlementRate ? `${Math.round(Number(detail.settlementRate) * 100)}%` : '—'}
</Descriptions.Item>
@@ -196,7 +290,6 @@ export default function StoreBillsPage() {
<Descriptions.Item label="实际打款">
{detail.paidAt ? fmtTime(String(detail.paidAt)) : '—'}
</Descriptions.Item>
<Descriptions.Item label="创建时间">{fmtTime(String(detail.createdAt))}</Descriptions.Item>
</Descriptions>
)}
</Drawer>