风险与内控管理优化: 异常账单服务端分页/筛选/收银员搜索/合计, 收银员TOP15按异常账单数排序, 双击跳转明细, MonthPicker靠右, 侧边栏底部间距
This commit is contained in:
@@ -1,21 +1,17 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import api from '@/lib/api'
|
||||
import { DataTable } from '@/components/DataTable'
|
||||
import { Pagination } from '@/components/Pagination'
|
||||
import { FilterableTable } from '@/components/FilterableTable'
|
||||
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 { useState } from 'react'
|
||||
import { PieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid } from 'recharts'
|
||||
import { MonthPicker } from '@/components/MonthPicker'
|
||||
|
||||
const ABC_COLORS = { 'A-核心': '#22c55e', 'B-成长': '#3b82f6', 'C-长尾': '#ef4444' }
|
||||
const PAGE_SIZE = 15
|
||||
|
||||
export function SKUPage() {
|
||||
const [abcFilter, setAbcFilter] = useState('')
|
||||
const [skuPage, setSkuPage] = useState(1)
|
||||
const [month, setMonth] = useState('2026-04')
|
||||
|
||||
const { data: skuData, isLoading: skuLoading } = useQuery({
|
||||
@@ -28,7 +24,7 @@ export function SKUPage() {
|
||||
queryFn: () => api.get('/sku/category'),
|
||||
})
|
||||
|
||||
const skuRows = ((skuData as any)?.data || []).filter((r: any) => !abcFilter || r.abc_class === abcFilter)
|
||||
const skuRows = ((skuData as any)?.data || [])
|
||||
const catRows = (catData as any)?.data || []
|
||||
|
||||
const abcSummary = (skuData as any)?.data?.reduce((acc: any, r: any) => {
|
||||
@@ -53,8 +49,6 @@ export function SKUPage() {
|
||||
const lowValueSku = ((skuData as any)?.data || []).filter((r: any) => Number(r.received_amount) < 1000).length
|
||||
const singleStoreSku = ((skuData as any)?.data || []).filter((r: any) => Number(r.store_count) <= 1).length
|
||||
|
||||
const pagedSku = useMemo(() => skuRows.slice((skuPage - 1) * PAGE_SIZE, skuPage * PAGE_SIZE), [skuRows, skuPage])
|
||||
|
||||
const pageLoading = skuLoading || catLoading
|
||||
|
||||
if (pageLoading) {
|
||||
@@ -117,40 +111,37 @@ export function SKUPage() {
|
||||
|
||||
{/* SKU明细表 */}
|
||||
<CollapsibleSection title={`SKU明细 (${skuRows.length})`} subtitle="可按ABC分类筛选,点击表头排序">
|
||||
<div className="mb-3 flex gap-2">
|
||||
{['', 'A-核心', 'B-成长', 'C-长尾'].map((f) => (
|
||||
<button
|
||||
key={f || 'all'}
|
||||
onClick={() => { setAbcFilter(f); setSkuPage(1) }}
|
||||
className={`rounded-md border px-3 py-1.5 text-sm ${abcFilter === f ? 'border-primary bg-primary text-primary-foreground' : ''}`}
|
||||
>
|
||||
{f || '全部'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<Pagination page={skuPage} pageSize={PAGE_SIZE} total={skuRows.length} onPageChange={setSkuPage} />
|
||||
<div className="mt-3">
|
||||
<DataTable
|
||||
columns={[
|
||||
{ key: 'dish_name', label: '菜品名称' },
|
||||
{ key: 'category_level1', label: '一级品类' },
|
||||
{ key: 'abc_class', label: 'ABC', render: (r) => (
|
||||
<span className={`rounded px-2 py-0.5 text-xs font-medium ${
|
||||
r.abc_class === 'A-核心' ? 'bg-green-100 text-green-700' :
|
||||
r.abc_class === 'B-成长' ? 'bg-blue-100 text-blue-700' :
|
||||
'bg-red-100 text-red-700'
|
||||
}`}>{r.abc_class}</span>
|
||||
)},
|
||||
{ key: 'sales_quadrant', label: '象限' },
|
||||
{ key: 'store_count', label: '覆盖门店', align: 'center' },
|
||||
{ key: 'bill_count', label: '账单数', align: 'right', render: (r) => formatNumber(r.bill_count) },
|
||||
{ key: 'received_amount', label: '实收', align: 'right', render: (r) => formatCurrency(r.received_amount) },
|
||||
{ key: 'revenue_share_pct', label: '占比', align: 'right', render: (r) => `${Number(r.revenue_share_pct).toFixed(2)}%` },
|
||||
{ key: 'discount_rate_pct', label: '优惠率', align: 'right', render: (r) => `${Number(r.discount_rate_pct).toFixed(1)}%` },
|
||||
]}
|
||||
data={pagedSku}
|
||||
/>
|
||||
</div>
|
||||
<FilterableTable
|
||||
data={skuRows}
|
||||
sortOptions={[
|
||||
{ key: 'received_amount', label: '实收' },
|
||||
{ key: 'bill_count', label: '账单数' },
|
||||
{ key: 'revenue_share_pct', label: '占比' },
|
||||
{ key: 'store_count', label: '覆盖门店' },
|
||||
{ key: 'dish_name', label: '菜品名称' },
|
||||
]}
|
||||
defaultSort="received_amount"
|
||||
defaultOrder="desc"
|
||||
filterKey="abc_class"
|
||||
filterLabel="全部ABC"
|
||||
columns={[
|
||||
{ key: 'dish_name', label: '菜品名称' },
|
||||
{ key: 'category_level1', label: '一级品类' },
|
||||
{ key: 'abc_class', label: 'ABC', render: (r) => (
|
||||
<span className={`rounded px-2 py-0.5 text-xs font-medium ${
|
||||
r.abc_class === 'A-核心' ? 'bg-green-100 text-green-700' :
|
||||
r.abc_class === 'B-成长' ? 'bg-blue-100 text-blue-700' :
|
||||
'bg-red-100 text-red-700'
|
||||
}`}>{r.abc_class}</span>
|
||||
)},
|
||||
{ key: 'sales_quadrant', label: '象限' },
|
||||
{ key: 'store_count', label: '覆盖门店', align: 'center' },
|
||||
{ key: 'bill_count', label: '账单数', align: 'right', render: (r) => formatNumber(r.bill_count) },
|
||||
{ key: 'received_amount', label: '实收', align: 'right', render: (r) => formatCurrency(r.received_amount) },
|
||||
{ key: 'revenue_share_pct', label: '占比', align: 'right', render: (r) => `${Number(r.revenue_share_pct).toFixed(2)}%` },
|
||||
{ key: 'discount_rate_pct', label: '优惠率', align: 'right', render: (r) => `${Number(r.discount_rate_pct).toFixed(1)}%` },
|
||||
]}
|
||||
/>
|
||||
</CollapsibleSection>
|
||||
|
||||
{/* 长尾治理建议 */}
|
||||
|
||||
Reference in New Issue
Block a user