feat: 统一SearchSelect下拉组件 + 修复营销方案数据 + 平台经济性增强

- 创建SearchSelect搜索下拉组件,选项≤8自动隐藏搜索框
- 全站替换原生select为SearchSelect(13处下拉)
- 修复/revenue/store-ranking优惠率字段名不匹配(discount_rate_pct→avg_discount_rate_pct)
- RevenuePage门店营收明细改用FilterableTable支持筛选排序分页
- 修复/marketing/plans按门店分组改为按marketing_plan分组
- 营销方案分析补充消费额、优惠额列和Top5概览卡片
- PlatformPage门店平台经济性增加门店搜索、经济性优列(平台分色)
- 平台成本率算法说明显示在标题下方
This commit is contained in:
freedakgmail
2026-08-01 23:08:34 +08:00
parent d724a565f5
commit ed7beb7e47
20 changed files with 1035 additions and 286 deletions
+26 -38
View File
@@ -5,6 +5,7 @@ import api from '@/lib/api'
import { MetricCard } from '@/components/MetricCard'
import { LoadingSpinner } from '@/components/LoadingSpinner'
import { CollapsibleSection } from '@/components/CollapsibleSection'
import { FilterableTable } from '@/components/FilterableTable'
import { formatNumber, formatCurrency, formatPercent } from '@/lib/utils'
import { BarChart3, TrendingDown, Users, Receipt } from 'lucide-react'
import { MonthPicker } from '@/components/MonthPicker'
@@ -282,44 +283,31 @@ export function RevenuePage() {
{/* 门店营收明细表 */}
<CollapsibleSection title="门店营收明细" subtitle="全部门店营收排名">
<div className="overflow-auto" style={{ maxHeight: 400 }}>
<table className="w-full text-xs">
<thead className="sticky top-0 bg-muted">
<tr>
<th className="text-left p-2"></th>
<th className="text-left p-2"></th>
<th className="text-right p-2"></th>
<th className="text-right p-2"></th>
<th className="text-right p-2"></th>
<th className="text-right p-2"></th>
<th className="text-right p-2"></th>
<th className="text-right p-2"></th>
</tr>
</thead>
<tbody>
{rankRows.map((s: any, i: number) => (
<tr
key={s.store_code}
className="cursor-pointer border-b hover:bg-muted/50"
onClick={() => navigate(`/stores/${s.store_code}`)}
>
<td className="p-2 font-bold">{i + 1}</td>
<td className="p-2 font-medium">{s.store_name}</td>
<td className="text-right p-2">{formatCurrency(s.received)}</td>
<td className="text-right p-2">{formatNumber(s.bill_count)}</td>
<td className="text-right p-2">{formatCurrency(s.avg_bill_value)}</td>
<td className="text-right p-2">
<span className={Number(s.avg_discount_rate_pct) > 25 ? 'text-red-600 font-bold' : Number(s.avg_discount_rate_pct) > 20 ? 'text-yellow-600' : ''}>
{formatPercent(s.avg_discount_rate_pct)}
</span>
</td>
<td className="text-right p-2">{formatNumber(s.guests)}</td>
<td className="text-right p-2">{formatPercent(s.avg_theoretical_margin_pct)}</td>
</tr>
))}
</tbody>
</table>
</div>
<FilterableTable
data={rankRows}
filterKey="store_name"
filterLabel="全部门店"
onRowClick={(r) => r.store_code && navigate(`/stores/${r.store_code}`)}
sortOptions={[
{ key: 'received', label: '实收' },
{ key: 'bill_count', label: '账单数' },
{ key: 'avg_bill_value', label: '客单价' },
{ key: 'avg_discount_rate_pct', label: '优惠率' },
{ key: 'guests', label: '客流量' },
{ key: 'avg_theoretical_margin_pct', label: '毛利率' },
]}
defaultSort="received"
defaultOrder="desc"
columns={[
{ key: 'store_name', label: '门店' },
{ key: 'received', label: '实收', align: 'right', render: (r) => formatCurrency(r.received) },
{ key: 'bill_count', label: '账单数', align: 'right', render: (r) => formatNumber(r.bill_count) },
{ key: 'avg_bill_value', label: '客单价', align: 'right', render: (r) => formatCurrency(r.avg_bill_value) },
{ key: 'avg_discount_rate_pct', label: '优惠率', align: 'right', render: (r) => <span className={Number(r.avg_discount_rate_pct) > 25 ? 'text-red-600 font-bold' : Number(r.avg_discount_rate_pct) > 20 ? 'text-yellow-600' : ''}>{formatPercent(r.avg_discount_rate_pct)}</span> },
{ key: 'guests', label: '客流量', align: 'right', render: (r) => formatNumber(r.guests) },
{ key: 'avg_theoretical_margin_pct', label: '理论毛利率', align: 'right', render: (r) => formatPercent(r.avg_theoretical_margin_pct) },
]}
/>
</CollapsibleSection>
</div>
)