import { useQuery } from '@tanstack/react-query' import api from '@/lib/api' import { DataTable } from '@/components/DataTable' import { Pagination } from '@/components/Pagination' import { LoadingSpinner } from '@/components/LoadingSpinner' import { CollapsibleSection } from '@/components/CollapsibleSection' import { MetricCard } from '@/components/MetricCard' import { formatCurrency, formatNumber, formatPercent } from '@/lib/utils' import { useState, useMemo } from 'react' import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts' import { MonthPicker } from '@/components/MonthPicker' const PAGE_SIZE = 15 export function MemberPage() { const [repeatPage, setRepeatPage] = useState(1) const [month, setMonth] = useState('2026-04') const { data: comparisonData, isLoading: cmpLoading } = useQuery({ queryKey: ['member/comparison'], queryFn: () => api.get('/member/comparison'), }) const { data: repeatData, isLoading: repeatLoading } = useQuery({ queryKey: ['member/repeat', month], queryFn: () => api.get('/member/repeat', { params: { month } }), }) const cmpRows = (comparisonData as any)?.data || [] const repeatRows = (repeatData as any)?.data || [] const pageLoading = cmpLoading || repeatLoading const memberRow = cmpRows.find((r: any) => r.customer_type === '会员') || {} const nonMemberRow = cmpRows.find((r: any) => r.customer_type === '非会员') || {} const totalBills = Number(memberRow.bill_count || 0) + Number(nonMemberRow.bill_count || 0) const memberBillShare = totalBills > 0 ? (Number(memberRow.bill_count || 0) / totalBills * 100) : 0 const totalReceived = Number(memberRow.received || 0) + Number(nonMemberRow.received || 0) const memberRevenueShare = totalReceived > 0 ? (Number(memberRow.received || 0) / totalReceived * 100) : 0 const avgRepeatRate = repeatRows.length > 0 ? repeatRows.reduce((s: number, r: any) => s + Number(r.repeat_rate_pct || 0), 0) / repeatRows.length : 0 const topRepeatStore = repeatRows.length > 0 ? repeatRows.reduce((best: any, r: any) => Number(r.repeat_rate_pct || 0) > Number(best?.repeat_rate_pct || 0) ? r : best, null) : null const lowRepeatCount = repeatRows.filter((r: any) => Number(r.repeat_rate_pct || 0) < 30).length const cmpChartData = cmpRows.map((r: any) => ({ name: r.customer_type, 账单数: Number(r.bill_count || 0), 实收: Number(r.received || 0), 客单价: Number(r.avg_bill_value || 0), })) const pieData = [ { name: '会员账单', value: Number(memberRow.bill_count || 0) }, { name: '非会员账单', value: Number(nonMemberRow.bill_count || 0) }, ] const PIE_COLORS = ['#3b82f6', '#e5e7eb'] const pagedRepeat = useMemo(() => repeatRows.slice((repeatPage - 1) * PAGE_SIZE, repeatPage * PAGE_SIZE), [repeatRows, repeatPage]) if (pageLoading) { return } return (

会员与复购管理

会员对比 · 复购率 · 沉睡分析 · {month}

{/* 概览指标 */}
{/* 会员对比图 */}
v >= 10000 ? `${(v / 10000).toFixed(0)}万` : v} tick={{ fontSize: 10 }} /> name === '实收' ? formatCurrency(v) : name === '客单价' ? formatCurrency(v) : formatNumber(v)} /> `${name}: ${formatNumber(value)}`}> {pieData.map((_, i) => )} formatNumber(v)} />
{/* 会员对比明细 */} formatNumber(r.bill_count) }, { key: 'received', label: '实收', align: 'right', render: (r) => formatCurrency(r.received) }, { key: 'avg_bill_value', label: '客单价', align: 'right', render: (r) => formatCurrency(r.avg_bill_value) }, { key: 'discount_rate_pct', label: '优惠率', align: 'right', render: (r) => formatPercent(r.discount_rate_pct) }, { key: 'theoretical_margin_pct', label: '理论毛利率', align: 'right', render: (r) => formatPercent(r.theoretical_margin_pct) }, ]} data={cmpRows} /> {/* 门店复购率 */}
formatNumber(r.identified_members) }, { key: 'repeat_members', label: '复购会员数', align: 'right', render: (r) => formatNumber(r.repeat_members) }, { key: 'repeat_rate_pct', label: '复购率', align: 'right', render: (r) => { const v = Number(r.repeat_rate_pct || 0) return = 50 ? 'font-medium text-green-600' : v >= 30 ? 'text-yellow-600' : 'font-medium text-red-600'}>{v.toFixed(1)}% }}, { key: 'avg_orders', label: '人均消费次数', align: 'right', render: (r) => Number(r.avg_orders || 0).toFixed(2) }, { key: 'repeat_revenue_share_pct', label: '复购收入占比', align: 'right', render: (r) => `${Number(r.repeat_revenue_share_pct || 0).toFixed(1)}%` }, ]} data={pagedRepeat} />
) }