148 lines
9.1 KiB
TypeScript
148 lines
9.1 KiB
TypeScript
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 } from 'recharts'
|
|
|
|
const PAGE_SIZE = 15
|
|
|
|
export function PlatformPage() {
|
|
const [platformPage, setPlatformPage] = useState(1)
|
|
const [marketingPage, setMarketingPage] = useState(1)
|
|
|
|
const { data: platformData, isLoading: platformLoading } = useQuery({
|
|
queryKey: ['platform/economics'],
|
|
queryFn: () => api.get('/platform/economics'),
|
|
})
|
|
|
|
const { data: marketingData, isLoading: marketingLoading } = useQuery({
|
|
queryKey: ['marketing/plans'],
|
|
queryFn: () => api.get('/marketing/plans'),
|
|
})
|
|
|
|
const platformRows = (platformData as any)?.data || []
|
|
const marketingRows = (marketingData as any)?.data || []
|
|
|
|
const pageLoading = platformLoading || marketingLoading
|
|
|
|
const storesWithPlatform = platformRows.filter((r: any) => Number(r.meituan_received) > 0 || Number(r.taobao_received) > 0 || Number(r.jd_received) > 0)
|
|
|
|
const totalMeituan = storesWithPlatform.reduce((s: number, r: any) => s + Number(r.meituan_received || 0), 0)
|
|
const totalTaobao = storesWithPlatform.reduce((s: number, r: any) => s + Number(r.taobao_received || 0), 0)
|
|
const totalJd = storesWithPlatform.reduce((s: number, r: any) => s + Number(r.jd_received || 0), 0)
|
|
const totalMeituanCost = storesWithPlatform.reduce((s: number, r: any) => s + Number(r.meituan_discount || 0) + Number(r.meituan_commission || 0), 0)
|
|
const totalTaobaoCost = storesWithPlatform.reduce((s: number, r: any) => s + Number(r.taobao_discount || 0) + Number(r.taobao_commission || 0), 0)
|
|
const totalJdCost = storesWithPlatform.reduce((s: number, r: any) => s + Number(r.jd_discount || 0) + Number(r.jd_commission || 0), 0)
|
|
|
|
const totalPlatformReceived = totalMeituan + totalTaobao + totalJd
|
|
const totalPlatformCost = totalMeituanCost + totalTaobaoCost + totalJdCost
|
|
const blendedCostRate = totalPlatformReceived > 0 ? (totalPlatformCost / totalPlatformReceived) * 100 : 0
|
|
|
|
const highCostStores = storesWithPlatform.filter((r: any) => {
|
|
const rates = [Number(r.meituan_cost_rate_pct || 0), Number(r.taobao_cost_rate_pct || 0), Number(r.jd_cost_rate_pct || 0)].filter((v: number) => v > 0)
|
|
return rates.some((v: number) => v >= 38)
|
|
})
|
|
|
|
const platformChartData = [
|
|
{ name: '美团', 实收: totalMeituan, 折扣: storesWithPlatform.reduce((s: number, r: any) => s + Number(r.meituan_discount || 0), 0), 佣金: storesWithPlatform.reduce((s: number, r: any) => s + Number(r.meituan_commission || 0), 0) },
|
|
{ name: '淘宝', 实收: totalTaobao, 折扣: storesWithPlatform.reduce((s: number, r: any) => s + Number(r.taobao_discount || 0), 0), 佣金: storesWithPlatform.reduce((s: number, r: any) => s + Number(r.taobao_commission || 0), 0) },
|
|
{ name: '京东', 实收: totalJd, 折扣: storesWithPlatform.reduce((s: number, r: any) => s + Number(r.jd_discount || 0), 0), 佣金: storesWithPlatform.reduce((s: number, r: any) => s + Number(r.jd_commission || 0), 0) },
|
|
]
|
|
|
|
const pagedPlatform = useMemo(() => storesWithPlatform.slice((platformPage - 1) * PAGE_SIZE, platformPage * PAGE_SIZE), [storesWithPlatform, platformPage])
|
|
const pagedMarketing = useMemo(() => marketingRows.slice((marketingPage - 1) * PAGE_SIZE, marketingPage * PAGE_SIZE), [marketingRows, marketingPage])
|
|
|
|
if (pageLoading) {
|
|
return <LoadingSpinner text="加载平台数据..." />
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h1 className="text-xl font-bold">平台与优惠管理</h1>
|
|
<p className="mt-0.5 text-xs text-muted-foreground">三平台经济性 · 营销方案ROI · 优惠结构 · 2026年4月</p>
|
|
</div>
|
|
|
|
{/* 概览指标 */}
|
|
<CollapsibleSection title="平台概览" subtitle="三平台合并加权成本率">
|
|
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
|
|
<MetricCard title="平台总实收" value={totalPlatformReceived} format="currency" description="美团+淘宝+京东三平台实收合计" />
|
|
<MetricCard title="合并加权成本率" value={blendedCostRate} format="percent" description="(折扣+佣金)÷实收,越高平台让利负担越大" />
|
|
<MetricCard title="高成本门店(≥38%)" value={highCostStores.length} format="number" description="任一平台成本率≥38%的门店数" />
|
|
<MetricCard title="营销方案数" value={marketingRows.length} format="number" description="活跃营销方案数量" />
|
|
</div>
|
|
</CollapsibleSection>
|
|
|
|
{/* 三平台对比图 */}
|
|
<CollapsibleSection title="三平台实收与成本对比" subtitle="实收、折扣、佣金金额对比">
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<BarChart data={platformChartData} margin={{ top: 20, right: 20, bottom: 20, left: 20 }}>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis dataKey="name" tick={{ fontSize: 12 }} />
|
|
<YAxis tickFormatter={(v) => v >= 10000 ? `${(v / 10000).toFixed(0)}万` : v} tick={{ fontSize: 10 }} />
|
|
<Tooltip formatter={(v: any) => formatCurrency(v)} />
|
|
<Legend />
|
|
<Bar dataKey="实收" fill="#3b82f6" />
|
|
<Bar dataKey="折扣" fill="#eab308" />
|
|
<Bar dataKey="佣金" fill="#ef4444" />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</CollapsibleSection>
|
|
|
|
{/* 门店平台经济性 */}
|
|
<CollapsibleSection title={`门店平台经济性 (${storesWithPlatform.length})`} subtitle="各门店三平台实收与成本率">
|
|
<Pagination page={platformPage} pageSize={PAGE_SIZE} total={storesWithPlatform.length} onPageChange={setPlatformPage} />
|
|
<div className="mt-3">
|
|
<DataTable
|
|
columns={[
|
|
{ key: 'store_name', label: '门店' },
|
|
{ key: 'meituan_received', label: '美团实收', align: 'right', render: (r) => formatCurrency(r.meituan_received) },
|
|
{ key: 'meituan_cost_rate_pct', label: '美团成本率', align: 'right', render: (r) => {
|
|
const v = Number(r.meituan_cost_rate_pct || 0)
|
|
return v > 0 ? <span className={v >= 38 ? 'font-medium text-red-600' : v >= 35 ? 'text-yellow-600' : 'text-green-600'}>{v.toFixed(1)}%</span> : <span className="text-muted-foreground">-</span>
|
|
}},
|
|
{ key: 'taobao_received', label: '淘宝实收', align: 'right', render: (r) => formatCurrency(r.taobao_received) },
|
|
{ key: 'taobao_cost_rate_pct', label: '淘宝成本率', align: 'right', render: (r) => {
|
|
const v = Number(r.taobao_cost_rate_pct || 0)
|
|
return v > 0 ? <span className={v >= 38 ? 'font-medium text-red-600' : v >= 35 ? 'text-yellow-600' : 'text-green-600'}>{v.toFixed(1)}%</span> : <span className="text-muted-foreground">-</span>
|
|
}},
|
|
{ key: 'jd_received', label: '京东实收', align: 'right', render: (r) => formatCurrency(r.jd_received) },
|
|
{ key: 'jd_cost_rate_pct', label: '京东成本率', align: 'right', render: (r) => {
|
|
const v = Number(r.jd_cost_rate_pct || 0)
|
|
return v > 0 ? <span className={v >= 38 ? 'font-medium text-red-600' : v >= 35 ? 'text-yellow-600' : 'text-green-600'}>{v.toFixed(1)}%</span> : <span className="text-muted-foreground">-</span>
|
|
}},
|
|
]}
|
|
data={pagedPlatform}
|
|
/>
|
|
</div>
|
|
</CollapsibleSection>
|
|
|
|
{/* 营销方案ROI */}
|
|
<CollapsibleSection title={`营销方案分析 (${marketingRows.length})`} subtitle="各营销方案的订单、实收、优惠率与毛利率">
|
|
<Pagination page={marketingPage} pageSize={PAGE_SIZE} total={marketingRows.length} onPageChange={setMarketingPage} />
|
|
<div className="mt-3">
|
|
<DataTable
|
|
columns={[
|
|
{ key: 'marketing_plan', label: '营销方案' },
|
|
{ key: 'bill_count', label: '账单数', align: 'right', render: (r) => 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) => {
|
|
const v = Number(r.discount_rate_pct || 0)
|
|
return <span className={v >= 25 ? 'font-medium text-red-600' : v >= 15 ? 'text-yellow-600' : 'text-green-600'}>{v.toFixed(1)}%</span>
|
|
}},
|
|
{ key: 'theoretical_margin_pct', label: '理论毛利率', align: 'right', render: (r) => formatPercent(r.theoretical_margin_pct) },
|
|
]}
|
|
data={pagedMarketing}
|
|
/>
|
|
</div>
|
|
</CollapsibleSection>
|
|
</div>
|
|
)
|
|
}
|