Files
SBrainCO/client/src/components/store-expense/DeliveryTab.tsx
T
freedakgmail 42e83696a0 feat: 门店费用分析模块 + 成本分析筛选排序功能
- 新增门店费用分析模块(9个Tab组件 + 后端路由)
  - 门店贡献利润、房租租约风险、外卖佣金分析、人效坪效
  - 固定/变动费用、盈亏平衡分析、亏损门店诊断
  - 关停/续租/改造评估、门店排名
- 门店费用分析9个Tab均支持筛选、排序、正倒序
- 成本分析8个Tab添加筛选、排序、正倒序功能
  - 后端10个端点添加filter/sort/order参数支持
  - 前端8个组件添加UI控件
- 修复门店评估分类逻辑和日期格式化
- 修复盈亏平衡销售额计算(贡献毛利率≤0时设为NULL)
2026-07-29 08:22:09 +08:00

73 lines
4.1 KiB
TypeScript

import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import api from '@/lib/api'
import { CollapsibleSection } from '@/components/CollapsibleSection'
import { DataTable } from '@/components/DataTable'
import { Pagination } from '@/components/Pagination'
import { LoadingSpinner } from '@/components/LoadingSpinner'
import { formatCurrency, formatPercent } from '@/lib/utils'
const PAGE_SIZE = 20
export function DeliveryTab() {
const [page, setPage] = useState(1)
const [sort, setSort] = useState('commission_to_delivery_sales_pct')
const [filter, setFilter] = useState('')
const [order, setOrder] = useState('desc')
const { data, isLoading } = useQuery({ queryKey: ['se/delivery', page, sort, filter, order], queryFn: () => api.get(`/store-expense/delivery-commission?page=${page}&page_size=${PAGE_SIZE}&sort=${sort}&filter=${filter}&order=${order}`) })
if (isLoading) return <LoadingSpinner text="加载外卖佣金分析..." />
const rows = (data as any)?.data || []
const meta = (data as any)?.meta || { page, pageSize: PAGE_SIZE, total: 0 }
const filters = [
{ key: '', label: '全部' },
{ key: 'profitable', label: '盈利门店' },
{ key: 'loss', label: '亏损门店' },
]
const sorts = [
{ key: 'commission_to_delivery_sales_pct', label: '佣金率' },
{ key: 'delivery_sales_share_pct', label: '外卖占比' },
{ key: 'delivery_received', label: '外卖收入' },
{ key: 'received', label: '实收' },
{ key: 'combined_platform_cost_rate_pct', label: '综合成本率' },
]
return (
<div className="space-y-4">
<CollapsibleSection title="外卖佣金分析" subtitle="外卖收入占比、佣金率及综合平台成本率">
<div className="flex gap-2 mb-3 flex-wrap">
{filters.map(f => (
<button key={f.key} onClick={() => { setFilter(f.key); setPage(1) }} className={`px-3 py-1 rounded text-xs ${filter === f.key ? 'bg-primary text-primary-foreground' : 'bg-muted'}`}>{f.label}</button>
))}
<span className="mx-2 text-muted-foreground">|</span>
{sorts.map(s => (
<button key={s.key} onClick={() => { setSort(s.key); setPage(1) }} className={`px-3 py-1 rounded text-xs ${sort === s.key ? 'bg-primary text-primary-foreground' : 'bg-muted'}`}>{s.label}</button>
))}
<span className="mx-2 text-muted-foreground">|</span>
<button onClick={() => { setOrder(order === 'asc' ? 'desc' : 'asc'); setPage(1) }} className="px-3 py-1 rounded text-xs bg-muted">
{order === 'asc' ? '↑ 升序' : '↓ 降序'}
</button>
</div>
<DataTable
columns={[
{ key: 'sales_store_name', label: '门店' },
{ key: 'received', label: '实收', align: 'right', render: (r) => formatCurrency(r.received) },
{ key: 'delivery_received', label: '外卖收入', align: 'right', render: (r) => formatCurrency(r.delivery_received) },
{ key: 'delivery_sales_share_pct', label: '外卖占比', align: 'right', render: (r) => <span className={parseFloat(r.delivery_sales_share_pct) > 40 ? 'text-red-600 font-medium' : parseFloat(r.delivery_sales_share_pct) > 25 ? 'text-orange-600' : ''}>{formatPercent(r.delivery_sales_share_pct)}</span> },
{ key: 'delivery_commission_expense', label: '佣金支出', align: 'right', render: (r) => formatCurrency(r.delivery_commission_expense) },
{ key: 'commission_to_delivery_sales_pct', label: '佣金率', align: 'right', render: (r) => <span className={parseFloat(r.commission_to_delivery_sales_pct) > 25 ? 'text-red-600 font-medium' : ''}>{formatPercent(r.commission_to_delivery_sales_pct)}</span> },
{ key: 'combined_platform_cost_rate_pct', label: '综合平台成本率', align: 'right', render: (r) => formatPercent(r.combined_platform_cost_rate_pct) },
{ key: 'delivery_type', label: '外卖类型' },
{ key: 'suggestion', label: '建议' },
]}
data={rows}
/>
<Pagination page={page} pageSize={meta.pageSize} total={meta.total} onPageChange={setPage} />
</CollapsibleSection>
</div>
)
}