import { useEffect, useMemo, useState } from 'react'; import { Download, Search } from 'lucide-react'; import { adminApi, type AdminChannel, type DailyProfitReport, type EnterpriseApplication, type ProfitReportSummary, type TenantOption } from '@/api/adminApi'; import { Breadcrumb, Button, DateRangeInput, MoneyText, Pagination, Select, Tag, type DateRangeValue } from '@/components/ui'; import { formatCents } from '@/utils/currency'; import { formatDateTime } from '@/utils/dateTime'; const pageSize = 20; export function AdminProfitReportsPage() { const [rows, setRows] = useState([]); const [tenants, setTenants] = useState([]); const [applications, setApplications] = useState([]); const [channels, setChannels] = useState([]); const [dateRange, setDateRange] = useState(defaultDateRange()); const [dimensionType, setDimensionType] = useState<'application' | 'channel'>('application'); const [tenantId, setTenantId] = useState(''); const [applicationId, setApplicationId] = useState(''); const [channelId, setChannelId] = useState(''); const [page, setPage] = useState(1); const [total, setTotal] = useState(0); const [summary, setSummary] = useState(emptyProfitSummary); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [exporting, setExporting] = useState(false); useEffect(() => { Promise.all([adminApi.listTenants(), adminApi.listEnterpriseApplications(), adminApi.listChannels()]) .then(([nextTenants, nextApplications, nextChannels]) => { setTenants(nextTenants); setApplications(nextApplications); setChannels(nextChannels); }) .catch(() => { setTenants([]); setApplications([]); setChannels([]); }); }, []); useEffect(() => { void loadData(); }, [page, dateRange.start, dateRange.end, dimensionType, tenantId, applicationId, channelId]); async function loadData() { setLoading(true); setError(''); try { const response = await adminApi.listProfitReports({ dateFrom: dateRange.start, dateTo: dateRange.end, dimensionType, tenantId: tenantId || undefined, applicationId: applicationId || undefined, channelId: channelId || undefined, page, pageSize }); setRows(response.items); setTotal(response.total); setSummary(response.summary); } catch (failure) { setRows([]); setTotal(0); setSummary(emptyProfitSummary); setError(failure instanceof Error ? failure.message : '利润报表加载失败'); } finally { setLoading(false); } } async function exportData() { setExporting(true); setError(''); try { downloadBlob(await adminApi.exportProfitReports({ dateFrom: dateRange.start, dateTo: dateRange.end, dimensionType, tenantId: tenantId || undefined, applicationId: applicationId || undefined, channelId: channelId || undefined }), '利润报表.csv'); } catch (failure) { setError(failure instanceof Error ? failure.message : '利润报表导出失败'); } finally { setExporting(false); } } const availableApplications = useMemo(() => applications.filter((application) => !tenantId || application.tenantId === tenantId), [applications, tenantId]); const totalPages = Math.max(1, Math.ceil(total / pageSize)); return (

利润报表

T+1 生成 · 每日重算 T-4~T-1
{ setDateRange(value); setPage(1); }} value={dateRange} /> { setTenantId(event.target.value); setApplicationId(''); setPage(1); }} options={[{ label: '全部企业', value: '' }, ...tenants.map((tenant) => ({ label: tenant.name, value: tenant.id }))]} value={tenantId} /> : { setApplicationId(event.target.value); setPage(1); }} options={[{ label: '全部应用', value: '' }, ...availableApplications.map((application) => ({ label: application.name, value: application.id }))]} value={applicationId} /> :
}
筛选结果汇总统计全部匹配数据,不受当前分页影响
{[ ['提交合计', summary.submittedUnits.toLocaleString('zh-CN')], ['发送合计', summary.sentUnits.toLocaleString('zh-CN')], ['未知合计', summary.unknownUnits.toLocaleString('zh-CN')], ['成功合计', summary.successUnits.toLocaleString('zh-CN')], ['失败合计', summary.failedUnits.toLocaleString('zh-CN')], ['收入合计', `¥${formatCents(summary.revenueCents)}`], ['成本合计', `¥${formatCents(summary.costCents)}`], ['利润合计', `¥${formatCents(summary.profitCents)}`], ['综合利润率', `${(summary.profitRateBps / 100).toFixed(2)}%`], ].map(([label, value]) =>
{label}{label.includes('合计') && ['收入合计', '成本合计', '利润合计'].includes(label) ? {value} : value}
)}
{error ? : loading ? : rows.length === 0 ? : rows.map((row) => )}
发送日期{dimensionType === 'application' ? '企业 / 企业应用' : '通道'}提交发送未知成功失败收入成本利润利润率生成时间
{error}
正在加载真实利润数据...
暂无已生成的利润报表
{row.reportDate.slice(0, 10)}{row.dimensionName}{row.tenantName ?
{row.tenantName}
: null}
{row.submittedUnits.toLocaleString('zh-CN')}{row.sentUnits.toLocaleString('zh-CN')}{row.unknownUnits.toLocaleString('zh-CN')}{row.successUnits.toLocaleString('zh-CN')}{row.failedUnits.toLocaleString('zh-CN')}¥{formatCents(row.revenueCents)}¥{formatCents(row.costCents)}¥{formatCents(row.profitCents)}{(row.profitRateBps / 100).toFixed(2)}%{formatDateTime(row.generatedAt)}
setPage((value) => Math.max(1, value - 1))} onNext={() => setPage((value) => Math.min(totalPages, value + 1))} previousDisabled={page <= 1} nextDisabled={page >= totalPages} />
); } const emptyProfitSummary: ProfitReportSummary = { submittedUnits: 0, sentUnits: 0, unknownUnits: 0, successUnits: 0, failedUnits: 0, revenueCents: 0, costCents: 0, profitCents: 0, profitRateBps: 0 }; function defaultDateRange(): DateRangeValue { const end = new Date(); end.setDate(end.getDate() - 1); const start = new Date(end); start.setDate(start.getDate() - 29); return { start: localDate(start), end: localDate(end) }; } function localDate(value: Date) { return `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}`; } function downloadBlob(blob: Blob, fileName: string) { const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = fileName; anchor.click(); URL.revokeObjectURL(url); }