feat: 全部页面添加 MonthPicker 月份切换
新增 MonthPicker 的页面(6个): - CostPage: 成本与库存管理 - SKUPage: 商品SKU管理 - PlatformPage: 平台与优惠管理 - MemberPage: 会员与复购管理 - SiteSelectionPage: 门店选址分析 - DataQualityPage: 数据质量看板 容器+子组件添加 MonthPicker(2个页面, 11个子组件): - StoreExpensePage: 门店营业费用分析(10个子组件全部传month) - CostAnalysisPage/StoreTab: 门店成本tab 跳过(后端不支持month参数): - RiskPage: 风险管理(全量视图, 不按月过滤) - TimePage: 时间段分析(全量视图, 不按月过滤)
This commit is contained in:
@@ -8,6 +8,7 @@ import { Pagination } from '@/components/Pagination'
|
||||
import { formatCurrency, formatPercent, formatNumber } from '@/lib/utils'
|
||||
import { useState } from 'react'
|
||||
import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from 'recharts'
|
||||
import { MonthPicker } from '@/components/MonthPicker'
|
||||
|
||||
const PAGE_SIZE = 15
|
||||
const LEVEL_COLORS: Record<string, string> = {
|
||||
@@ -22,9 +23,10 @@ export function StoreTab() {
|
||||
const [sort, setSort] = useState('food_cost_variance')
|
||||
const [order, setOrder] = useState('desc')
|
||||
const [filter, setFilter] = useState('')
|
||||
const [month, setMonth] = useState('2026-04')
|
||||
|
||||
const { data: ov, isLoading: l1 } = useQuery({ queryKey: ['ca/store-overview'], queryFn: () => api.get('/cost-analysis/store-overview') })
|
||||
const { data: ranking, isLoading: l2 } = useQuery({ queryKey: ['ca/store-ranking', page, sort, order, filter], queryFn: () => api.get(`/cost-analysis/store-ranking?page=${page}&page_size=${PAGE_SIZE}&sort=${sort}&order=${order}&filter=${filter}`) })
|
||||
const { data: ov, isLoading: l1 } = useQuery({ queryKey: ['ca/store-overview', month], queryFn: () => api.get('/cost-analysis/store-overview', { params: { month } }) })
|
||||
const { data: ranking, isLoading: l2 } = useQuery({ queryKey: ['ca/store-ranking', month, page, sort, order, filter], queryFn: () => api.get(`/cost-analysis/store-ranking?page=${page}&page_size=${PAGE_SIZE}&sort=${sort}&order=${order}&filter=${filter}&month=${month}`) })
|
||||
|
||||
if (l1 || l2) return <LoadingSpinner text="加载门店成本数据..." />
|
||||
|
||||
@@ -48,6 +50,9 @@ export function StoreTab() {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-end">
|
||||
<MonthPicker month={month} onChange={setMonth} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-3">
|
||||
<MetricCard title="门店总数" value={ovData.total_stores} format="number" />
|
||||
<MetricCard title="红色-严重超耗" value={ovData.red_count} format="number" />
|
||||
|
||||
@@ -17,12 +17,12 @@ const SAFETY_COLORS: Record<string, string> = {
|
||||
'安全边际充足': 'text-green-600',
|
||||
}
|
||||
|
||||
export function BreakEvenTab() {
|
||||
export function BreakEvenTab({ month }: { month: string }) {
|
||||
const [page, setPage] = useState(1)
|
||||
const [sort, setSort] = useState('safety_margin_pct')
|
||||
const [filter, setFilter] = useState('')
|
||||
const [order, setOrder] = useState('asc')
|
||||
const { data, isLoading } = useQuery({ queryKey: ['se/break-even', page, sort, filter, order], queryFn: () => api.get(`/store-expense/break-even?page=${page}&page_size=${PAGE_SIZE}&sort=${sort}&filter=${filter}&order=${order}`) })
|
||||
const { data, isLoading } = useQuery({ queryKey: ['se/break-even', month, page, sort, filter, order], queryFn: () => api.get(`/store-expense/break-even?page=${page}&page_size=${PAGE_SIZE}&sort=${sort}&filter=${filter}&order=${order}&month=${month}`) })
|
||||
|
||||
if (isLoading) return <LoadingSpinner text="加载盈亏平衡分析..." />
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@ import { formatCurrency, formatPercent } from '@/lib/utils'
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
export function ContributionTab() {
|
||||
export function ContributionTab({ month }: { month: string }) {
|
||||
const [page, setPage] = useState(1)
|
||||
const [sort, setSort] = useState('actual_store_contribution_rate_pct')
|
||||
const [filter, setFilter] = useState('')
|
||||
const [order, setOrder] = useState('asc')
|
||||
const { data, isLoading } = useQuery({ queryKey: ['se/contribution', page, sort, filter, order], queryFn: () => api.get(`/store-expense/store-contribution?page=${page}&page_size=${PAGE_SIZE}&sort=${sort}&filter=${filter}&order=${order}`) })
|
||||
const { data, isLoading } = useQuery({ queryKey: ['se/contribution', month, page, sort, filter, order], queryFn: () => api.get(`/store-expense/store-contribution?page=${page}&page_size=${PAGE_SIZE}&sort=${sort}&filter=${filter}&order=${order}&month=${month}`) })
|
||||
|
||||
if (isLoading) return <LoadingSpinner text="加载门店贡献利润..." />
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@ import { formatCurrency, formatPercent } from '@/lib/utils'
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
export function DeliveryTab() {
|
||||
export function DeliveryTab({ month }: { month: string }) {
|
||||
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}`) })
|
||||
const { data, isLoading } = useQuery({ queryKey: ['se/delivery', month, page, sort, filter, order], queryFn: () => api.get(`/store-expense/delivery-commission?page=${page}&page_size=${PAGE_SIZE}&sort=${sort}&filter=${filter}&order=${order}&month=${month}`) })
|
||||
|
||||
if (isLoading) return <LoadingSpinner text="加载外卖佣金分析..." />
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@ import { formatCurrency, formatPercent, formatNumber } from '@/lib/utils'
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
export function EfficiencyTab() {
|
||||
export function EfficiencyTab({ month }: { month: string }) {
|
||||
const [page, setPage] = useState(1)
|
||||
const [sort, setSort] = useState('received_per_sqm')
|
||||
const [filter, setFilter] = useState('')
|
||||
const [order, setOrder] = useState('desc')
|
||||
const { data, isLoading } = useQuery({ queryKey: ['se/efficiency', page, sort, filter, order], queryFn: () => api.get(`/store-expense/efficiency?page=${page}&page_size=${PAGE_SIZE}&sort=${sort}&filter=${filter}&order=${order}`) })
|
||||
const { data, isLoading } = useQuery({ queryKey: ['se/efficiency', month, page, sort, filter, order], queryFn: () => api.get(`/store-expense/efficiency?page=${page}&page_size=${PAGE_SIZE}&sort=${sort}&filter=${filter}&order=${order}&month=${month}`) })
|
||||
|
||||
if (isLoading) return <LoadingSpinner text="加载人效坪效数据..." />
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContaine
|
||||
|
||||
const PIE_COLORS = ['#3b82f6', '#ef4444', '#22c55e', '#f97316', '#eab308', '#8b5cf6', '#06b6d4', '#ec4899', '#84cc16', '#a3a3a3', '#f43f5e', '#6366f1', '#14b8a6', '#facc15', '#7c3aed', '#10b981', '#fb923c']
|
||||
|
||||
export function ExpenseOverviewTab() {
|
||||
const { data: overview, isLoading: l1 } = useQuery({ queryKey: ['se/overview'], queryFn: () => api.get('/store-expense/overview') })
|
||||
const { data: structure, isLoading: l2 } = useQuery({ queryKey: ['se/structure'], queryFn: () => api.get('/store-expense/expense-structure') })
|
||||
export function ExpenseOverviewTab({ month }: { month: string }) {
|
||||
const { data: overview, isLoading: l1 } = useQuery({ queryKey: ['se/overview', month], queryFn: () => api.get('/store-expense/overview', { params: { month } }) })
|
||||
const { data: structure, isLoading: l2 } = useQuery({ queryKey: ['se/structure', month], queryFn: () => api.get('/store-expense/expense-structure', { params: { month } }) })
|
||||
|
||||
if (l1 || l2) return <LoadingSpinner text="加载费用总览..." />
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@ import { formatCurrency, formatPercent } from '@/lib/utils'
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
export function FixedVariableTab() {
|
||||
export function FixedVariableTab({ month }: { month: string }) {
|
||||
const [page, setPage] = useState(1)
|
||||
const [sort, setSort] = useState('fixed_rate_pct')
|
||||
const [filter, setFilter] = useState('')
|
||||
const [order, setOrder] = useState('desc')
|
||||
const { data, isLoading } = useQuery({ queryKey: ['se/fixed-variable', page, sort, filter, order], queryFn: () => api.get(`/store-expense/fixed-variable?page=${page}&page_size=${PAGE_SIZE}&sort=${sort}&filter=${filter}&order=${order}`) })
|
||||
const { data, isLoading } = useQuery({ queryKey: ['se/fixed-variable', month, page, sort, filter, order], queryFn: () => api.get(`/store-expense/fixed-variable?page=${page}&page_size=${PAGE_SIZE}&sort=${sort}&filter=${filter}&order=${order}&month=${month}`) })
|
||||
|
||||
if (isLoading) return <LoadingSpinner text="加载固定/变动费用拆分..." />
|
||||
|
||||
|
||||
@@ -185,13 +185,13 @@ function StoreDetailModal({ store, onClose }: { store: StoreDetail | null, onClo
|
||||
)
|
||||
}
|
||||
|
||||
export function LossDiagnosisTab() {
|
||||
export function LossDiagnosisTab({ month }: { month: string }) {
|
||||
const [page, setPage] = useState(1)
|
||||
const [sort, setSort] = useState('actual_store_contribution')
|
||||
const [filter, setFilter] = useState('')
|
||||
const [order, setOrder] = useState('asc')
|
||||
const [selectedStore, setSelectedStore] = useState<StoreDetail | null>(null)
|
||||
const { data, isLoading } = useQuery({ queryKey: ['se/loss-diagnosis', page, sort, filter, order], queryFn: () => api.get(`/store-expense/loss-diagnosis?page=${page}&page_size=${PAGE_SIZE}&sort=${sort}&filter=${filter}&order=${order}`) })
|
||||
const { data, isLoading } = useQuery({ queryKey: ['se/loss-diagnosis', month, page, sort, filter, order], queryFn: () => api.get(`/store-expense/loss-diagnosis?page=${page}&page_size=${PAGE_SIZE}&sort=${sort}&filter=${filter}&order=${order}&month=${month}`) })
|
||||
|
||||
if (isLoading) return <LoadingSpinner text="加载亏损门店诊断..." />
|
||||
|
||||
|
||||
@@ -17,13 +17,13 @@ const RISK_COLORS: Record<string, string> = {
|
||||
'正常': '',
|
||||
}
|
||||
|
||||
export function RentRiskTab() {
|
||||
export function RentRiskTab({ month }: { month: string }) {
|
||||
const [page, setPage] = useState(1)
|
||||
const [sort, setSort] = useState('rent_rate_pct')
|
||||
const [filter, setFilter] = useState('')
|
||||
const [riskOnly, setRiskOnly] = useState(false)
|
||||
const [order, setOrder] = useState('desc')
|
||||
const { data, isLoading } = useQuery({ queryKey: ['se/rent', page, sort, filter, riskOnly, order], queryFn: () => api.get(`/store-expense/rent-risk?page=${page}&page_size=${PAGE_SIZE}&sort=${sort}&filter=${filter}&risk_only=${riskOnly}&order=${order}`) })
|
||||
const { data, isLoading } = useQuery({ queryKey: ['se/rent', month, page, sort, filter, riskOnly, order], queryFn: () => api.get(`/store-expense/rent-risk?page=${page}&page_size=${PAGE_SIZE}&sort=${sort}&filter=${filter}&risk_only=${riskOnly}&order=${order}&month=${month}`) })
|
||||
|
||||
if (isLoading) return <LoadingSpinner text="加载房租及租约风险..." />
|
||||
|
||||
|
||||
@@ -107,13 +107,13 @@ function EvalDetailModal({ store, onClose }: { store: EvalStoreDetail | null, on
|
||||
)
|
||||
}
|
||||
|
||||
export function StoreEvaluationTab() {
|
||||
export function StoreEvaluationTab({ month }: { month: string }) {
|
||||
const [page, setPage] = useState(1)
|
||||
const [sort, setSort] = useState('actual_store_contribution_rate_pct')
|
||||
const [filter, setFilter] = useState('')
|
||||
const [order, setOrder] = useState('asc')
|
||||
const [selectedStore, setSelectedStore] = useState<EvalStoreDetail | null>(null)
|
||||
const { data, isLoading } = useQuery({ queryKey: ['se/evaluation', page, sort, filter, order], queryFn: () => api.get(`/store-expense/store-evaluation?page=${page}&page_size=${PAGE_SIZE}&sort=${sort}&filter=${filter}&order=${order}`) })
|
||||
const { data, isLoading } = useQuery({ queryKey: ['se/evaluation', month, page, sort, filter, order], queryFn: () => api.get(`/store-expense/store-evaluation?page=${page}&page_size=${PAGE_SIZE}&sort=${sort}&filter=${filter}&order=${order}&month=${month}`) })
|
||||
|
||||
if (isLoading) return <LoadingSpinner text="加载门店评估..." />
|
||||
|
||||
|
||||
@@ -9,13 +9,13 @@ import { formatCurrency, formatPercent, formatNumber } from '@/lib/utils'
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
export function StoreRankingTab() {
|
||||
export function StoreRankingTab({ month }: { month: string }) {
|
||||
const [page, setPage] = useState(1)
|
||||
const [sort, setSort] = useState('operating_expense_rate_pct')
|
||||
const [filter, setFilter] = useState('')
|
||||
const [order, setOrder] = useState('desc')
|
||||
|
||||
const { data, isLoading } = useQuery({ queryKey: ['se/ranking', page, sort, filter, order], queryFn: () => api.get(`/store-expense/store-ranking?page=${page}&page_size=${PAGE_SIZE}&sort=${sort}&filter=${filter}&order=${order}`) })
|
||||
const { data, isLoading } = useQuery({ queryKey: ['se/ranking', month, page, sort, filter, order], queryFn: () => api.get(`/store-expense/store-ranking?page=${page}&page_size=${PAGE_SIZE}&sort=${sort}&filter=${filter}&order=${order}&month=${month}`) })
|
||||
|
||||
if (isLoading) return <LoadingSpinner text="加载门店费用率排名..." />
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Badge } from '@/components/Badge'
|
||||
import { formatCurrency, formatNumber, formatPercent } from '@/lib/utils'
|
||||
import { useState, useMemo } from 'react'
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ScatterChart, Scatter } from 'recharts'
|
||||
import { MonthPicker } from '@/components/MonthPicker'
|
||||
|
||||
const PAGE_SIZE = 15
|
||||
|
||||
@@ -16,20 +17,21 @@ export function CostPage() {
|
||||
const [costPage, setCostPage] = useState(1)
|
||||
const [invPage, setInvPage] = useState(1)
|
||||
const [catPage, setCatPage] = useState(1)
|
||||
const [month, setMonth] = useState('2026-04')
|
||||
|
||||
const { data: costData, isLoading: costLoading } = useQuery({
|
||||
queryKey: ['cost/comparison'],
|
||||
queryFn: () => api.get('/cost/comparison'),
|
||||
queryKey: ['cost/comparison', month],
|
||||
queryFn: () => api.get('/cost/comparison', { params: { month } }),
|
||||
})
|
||||
|
||||
const { data: invData, isLoading: invLoading } = useQuery({
|
||||
queryKey: ['cost/inventory'],
|
||||
queryFn: () => api.get('/cost/inventory'),
|
||||
queryKey: ['cost/inventory', month],
|
||||
queryFn: () => api.get('/cost/inventory', { params: { month } }),
|
||||
})
|
||||
|
||||
const { data: catData, isLoading: catLoading } = useQuery({
|
||||
queryKey: ['cost/category-benchmark'],
|
||||
queryFn: () => api.get('/cost/category-benchmark'),
|
||||
queryKey: ['cost/category-benchmark', month],
|
||||
queryFn: () => api.get('/cost/category-benchmark', { params: { month } }),
|
||||
})
|
||||
|
||||
const costRows = (costData as any)?.data || []
|
||||
@@ -67,9 +69,12 @@ export function CostPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">成本与库存管理</h1>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">理论vs实际成本 · 原料分类差异 · 库存效率 · 2026年4月</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">成本与库存管理</h1>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">理论vs实际成本 · 原料分类差异 · 库存效率 · {month}</p>
|
||||
</div>
|
||||
<MonthPicker month={month} onChange={setMonth} />
|
||||
</div>
|
||||
|
||||
{/* 概览指标 */}
|
||||
|
||||
@@ -4,11 +4,14 @@ import { LoadingSpinner } from '@/components/LoadingSpinner'
|
||||
import { CollapsibleSection } from '@/components/CollapsibleSection'
|
||||
import { MetricCard } from '@/components/MetricCard'
|
||||
import { formatNumber, formatCurrency } from '@/lib/utils'
|
||||
import { useState } from 'react'
|
||||
import { MonthPicker } from '@/components/MonthPicker'
|
||||
|
||||
export function DataQualityPage() {
|
||||
const [month, setMonth] = useState('2026-04')
|
||||
const { data: qualityData, isLoading } = useQuery({
|
||||
queryKey: ['data-quality'],
|
||||
queryFn: () => api.get('/data-quality'),
|
||||
queryKey: ['data-quality', month],
|
||||
queryFn: () => api.get('/data-quality', { params: { month } }),
|
||||
})
|
||||
|
||||
const dq = (qualityData as any)?.data || {}
|
||||
@@ -45,9 +48,12 @@ export function DataQualityPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">数据质量看板</h1>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">账单与菜品明细数据质量检查 · {dq.min_date?.substring(0, 10)} ~ {dq.max_date?.substring(0, 10)}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">数据质量看板</h1>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">账单与菜品明细数据质量检查 · {dq.min_date?.substring(0, 10)} ~ {dq.max_date?.substring(0, 10)}</p>
|
||||
</div>
|
||||
<MonthPicker month={month} onChange={setMonth} />
|
||||
</div>
|
||||
|
||||
{/* 质量状态总览 */}
|
||||
|
||||
@@ -8,11 +8,13 @@ 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'],
|
||||
@@ -20,8 +22,8 @@ export function MemberPage() {
|
||||
})
|
||||
|
||||
const { data: repeatData, isLoading: repeatLoading } = useQuery({
|
||||
queryKey: ['member/repeat'],
|
||||
queryFn: () => api.get('/member/repeat'),
|
||||
queryKey: ['member/repeat', month],
|
||||
queryFn: () => api.get('/member/repeat', { params: { month } }),
|
||||
})
|
||||
|
||||
const cmpRows = (comparisonData as any)?.data || []
|
||||
@@ -66,9 +68,12 @@ export function MemberPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">会员与复购管理</h1>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">会员对比 · 复购率 · 沉睡分析 · 2026年4月</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">会员与复购管理</h1>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">会员对比 · 复购率 · 沉睡分析 · {month}</p>
|
||||
</div>
|
||||
<MonthPicker month={month} onChange={setMonth} />
|
||||
</div>
|
||||
|
||||
{/* 概览指标 */}
|
||||
|
||||
@@ -8,16 +8,18 @@ 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'
|
||||
import { MonthPicker } from '@/components/MonthPicker'
|
||||
|
||||
const PAGE_SIZE = 15
|
||||
|
||||
export function PlatformPage() {
|
||||
const [platformPage, setPlatformPage] = useState(1)
|
||||
const [marketingPage, setMarketingPage] = useState(1)
|
||||
const [month, setMonth] = useState('2026-04')
|
||||
|
||||
const { data: platformData, isLoading: platformLoading } = useQuery({
|
||||
queryKey: ['platform/economics'],
|
||||
queryFn: () => api.get('/platform/economics'),
|
||||
queryKey: ['platform/economics', month],
|
||||
queryFn: () => api.get('/platform/economics', { params: { month } }),
|
||||
})
|
||||
|
||||
const { data: marketingData, isLoading: marketingLoading } = useQuery({
|
||||
@@ -63,9 +65,12 @@ export function PlatformPage() {
|
||||
|
||||
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 className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">平台与优惠管理</h1>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">三平台经济性 · 营销方案ROI · 优惠结构 · {month}</p>
|
||||
</div>
|
||||
<MonthPicker month={month} onChange={setMonth} />
|
||||
</div>
|
||||
|
||||
{/* 概览指标 */}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { MetricCard } from '@/components/MetricCard'
|
||||
import { formatCurrency, formatNumber, formatPercent } from '@/lib/utils'
|
||||
import { useState, useMemo } 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
|
||||
@@ -15,10 +16,11 @@ 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({
|
||||
queryKey: ['sku/abc'],
|
||||
queryFn: () => api.get('/sku/abc'),
|
||||
queryKey: ['sku/abc', month],
|
||||
queryFn: () => api.get('/sku/abc', { params: { month } }),
|
||||
})
|
||||
|
||||
const { data: catData, isLoading: catLoading } = useQuery({
|
||||
@@ -61,9 +63,12 @@ export function SKUPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">商品SKU管理</h1>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">SKU ABC分析 · 长尾治理 · 品类结构 · 2026年4月</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">商品SKU管理</h1>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">SKU ABC分析 · 长尾治理 · 品类结构 · {month}</p>
|
||||
</div>
|
||||
<MonthPicker month={month} onChange={setMonth} />
|
||||
</div>
|
||||
|
||||
{/* 概览指标 */}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { formatCurrency, formatNumber, formatPercent } from '@/lib/utils'
|
||||
import { useState, useMemo } from 'react'
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ScatterChart, Scatter, ZAxis, ReferenceLine, Cell } from 'recharts'
|
||||
import { StoreMap } from '@/components/StoreMap'
|
||||
import { MonthPicker } from '@/components/MonthPicker'
|
||||
|
||||
const PAGE_SIZE = 15
|
||||
const SCENE_COLORS: Record<string, string> = {
|
||||
@@ -33,30 +34,31 @@ export function SiteSelectionPage() {
|
||||
const [replPage, setReplPage] = useState(1)
|
||||
const [overlapPage, setOverlapPage] = useState(1)
|
||||
const [profilePage, setProfilePage] = useState(1)
|
||||
const [month, setMonth] = useState('2026-04')
|
||||
|
||||
const { data: profileData, isLoading: profileLoading } = useQuery({
|
||||
queryKey: ['site-selection/profile'],
|
||||
queryFn: () => api.get('/site-selection/profile'),
|
||||
queryKey: ['site-selection/profile', month],
|
||||
queryFn: () => api.get('/site-selection/profile', { params: { month } }),
|
||||
})
|
||||
|
||||
const { data: segData, isLoading: segLoading } = useQuery({
|
||||
queryKey: ['site-selection/segment-benchmark'],
|
||||
queryFn: () => api.get('/site-selection/segment-benchmark'),
|
||||
queryKey: ['site-selection/segment-benchmark', month],
|
||||
queryFn: () => api.get('/site-selection/segment-benchmark', { params: { month } }),
|
||||
})
|
||||
|
||||
const { data: replData, isLoading: replLoading } = useQuery({
|
||||
queryKey: ['site-selection/replication'],
|
||||
queryFn: () => api.get('/site-selection/replication'),
|
||||
queryKey: ['site-selection/replication', month],
|
||||
queryFn: () => api.get('/site-selection/replication', { params: { month } }),
|
||||
})
|
||||
|
||||
const { data: overlapData, isLoading: overlapLoading } = useQuery({
|
||||
queryKey: ['site-selection/overlap-risk'],
|
||||
queryFn: () => api.get('/site-selection/overlap-risk'),
|
||||
queryKey: ['site-selection/overlap-risk', month],
|
||||
queryFn: () => api.get('/site-selection/overlap-risk', { params: { month } }),
|
||||
})
|
||||
|
||||
const { data: districtData, isLoading: districtLoading } = useQuery({
|
||||
queryKey: ['site-selection/district-benchmark'],
|
||||
queryFn: () => api.get('/site-selection/district-benchmark'),
|
||||
queryKey: ['site-selection/district-benchmark', month],
|
||||
queryFn: () => api.get('/site-selection/district-benchmark', { params: { month } }),
|
||||
})
|
||||
|
||||
const profileRows = ((profileData as any)?.data || []).filter((r: any) => !sceneFilter || r.site_scene === sceneFilter)
|
||||
@@ -90,9 +92,12 @@ export function SiteSelectionPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">门店选址分析</h1>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">基于全部现有数据的选址决策支持 · 2026年4月 · 91家经营门店</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">门店选址分析</h1>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">基于现有数据的选址决策支持 · {month} · {totalStores}家经营门店</p>
|
||||
</div>
|
||||
<MonthPicker month={month} onChange={setMonth} />
|
||||
</div>
|
||||
|
||||
{/* 门店分布地图(左) + 指标+面积坪效分布(右) 左右并排 */}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { Tabs } from '@/components/Tabs'
|
||||
import { MonthPicker } from '@/components/MonthPicker'
|
||||
import { ExpenseOverviewTab } from '@/components/store-expense/ExpenseOverviewTab'
|
||||
import { StoreRankingTab } from '@/components/store-expense/StoreRankingTab'
|
||||
import { ContributionTab } from '@/components/store-expense/ContributionTab'
|
||||
@@ -26,27 +27,31 @@ const TABS = [
|
||||
|
||||
export function StoreExpensePage() {
|
||||
const [activeTab, setActiveTab] = useState('overview')
|
||||
const [month, setMonth] = useState('2026-04')
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">门店营业费用分析</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">2026年4月 · 89家经营门店 · 费用合计 ¥31,811,801</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">门店营业费用分析</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">{month} · 费用结构与贡献分析</p>
|
||||
</div>
|
||||
<MonthPicker month={month} onChange={setMonth} />
|
||||
</div>
|
||||
|
||||
<Tabs tabs={TABS} active={activeTab} onChange={setActiveTab} />
|
||||
|
||||
<div>
|
||||
{activeTab === 'overview' && <ExpenseOverviewTab />}
|
||||
{activeTab === 'ranking' && <StoreRankingTab />}
|
||||
{activeTab === 'contribution' && <ContributionTab />}
|
||||
{activeTab === 'rent' && <RentRiskTab />}
|
||||
{activeTab === 'delivery' && <DeliveryTab />}
|
||||
{activeTab === 'efficiency' && <EfficiencyTab />}
|
||||
{activeTab === 'fixed-variable' && <FixedVariableTab />}
|
||||
{activeTab === 'break-even' && <BreakEvenTab />}
|
||||
{activeTab === 'loss-diagnosis' && <LossDiagnosisTab />}
|
||||
{activeTab === 'evaluation' && <StoreEvaluationTab />}
|
||||
{activeTab === 'overview' && <ExpenseOverviewTab month={month} />}
|
||||
{activeTab === 'ranking' && <StoreRankingTab month={month} />}
|
||||
{activeTab === 'contribution' && <ContributionTab month={month} />}
|
||||
{activeTab === 'rent' && <RentRiskTab month={month} />}
|
||||
{activeTab === 'delivery' && <DeliveryTab month={month} />}
|
||||
{activeTab === 'efficiency' && <EfficiencyTab month={month} />}
|
||||
{activeTab === 'fixed-variable' && <FixedVariableTab month={month} />}
|
||||
{activeTab === 'break-even' && <BreakEvenTab month={month} />}
|
||||
{activeTab === 'loss-diagnosis' && <LossDiagnosisTab month={month} />}
|
||||
{activeTab === 'evaluation' && <StoreEvaluationTab month={month} />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user