feat: 门店详情增加风险等级和经营象限详细说明+修复后端不存在的视图引用

This commit is contained in:
freedakgmail
2026-07-30 09:16:42 +08:00
parent 3727cf8672
commit e946cdefef
2 changed files with 57 additions and 7 deletions
+54 -2
View File
@@ -87,6 +87,25 @@ export function StoreDetailPage() {
if (!sd) return <div>...</div>
const sc = sd.scorecard
const action = sd.action
const risk = sd.risk
const riskDesc: Record<string, { color: string; desc: string }> = {
'绿色': { color: 'text-green-600 bg-green-50 border-green-200', desc: '各项指标健康,经营状态良好,维持现有运营策略即可。' },
'黄色': { color: 'text-yellow-600 bg-yellow-50 border-yellow-200', desc: '存在单项指标偏差,需关注并针对性改善,避免风险升级。' },
'红色': { color: 'text-red-600 bg-red-50 border-red-200', desc: '多项指标异常,经营风险较高,需立即介入并制定整改计划。' },
}
const quadrantDesc: Record<string, string> = {
'明星门店': '高营收高毛利,是公司的核心利润来源。应总结其成功经验并推广至其他门店。',
'现金牛门店': '高营收但毛利偏低,通过成本优化有较大利润提升空间。',
'潜力门店': '营收偏低但毛利较好,具备增长潜力,需提升客流和营收规模。',
'问题门店': '营收和毛利均偏低,需全面诊断并考虑调整经营策略或选址。',
}
const riskInfo = risk?.risk_level ? riskDesc[risk.risk_level] : null
const quadrant = action?.management_quadrant || ''
const quadrantInfo = quadrantDesc[quadrant]
return (
<div className="space-y-4">
@@ -96,8 +115,8 @@ export function StoreDetailPage() {
<div className="flex items-center gap-3">
<h1 className="text-xl font-bold">{sc.store_name}</h1>
{sd.risk && <Badge type="risk" text={sd.risk.risk_level} />}
{sd.benchmark && <span className="rounded-md bg-muted px-2 py-0.5 text-xs">{sd.benchmark.management_quadrant}</span>}
{risk?.risk_level && <Badge type="risk" text={risk.risk_level} />}
{quadrant && <span className="rounded-md bg-muted px-2 py-0.5 text-xs">{quadrant}</span>}
</div>
{/* 核心指标 */}
@@ -110,6 +129,39 @@ export function StoreDetailPage() {
<MetricCard title="会员占比" value={sc.member_bill_share_pct} format="percent" />
</div>
{/* 风险与经营状态说明 */}
<div className="grid gap-3 md:grid-cols-2">
{riskInfo && (
<div className={`rounded-lg border p-3 ${riskInfo.color}`}>
<div className="flex items-center gap-2">
<span className="font-bold">{risk.risk_level}</span>
{risk.primary_issue && <span className="text-xs opacity-80"> {risk.primary_issue}</span>}
</div>
<p className="mt-1 text-sm opacity-90">{riskInfo.desc}</p>
</div>
)}
{quadrantInfo && (
<div className="rounded-lg border p-3 bg-blue-50 border-blue-200 text-blue-700">
<div className="flex items-center gap-2">
<span className="font-bold">{quadrant}</span>
{action?.scale_tier && <span className="text-xs opacity-80"> {action.scale_tier}</span>}
</div>
<p className="mt-1 text-sm opacity-90">{quadrantInfo}</p>
</div>
)}
</div>
{/* 行动建议 */}
{action?.action_priority && (
<div className="rounded-lg border p-3 bg-muted/50">
<div className="flex flex-wrap items-center gap-3">
<span className="font-bold text-sm">{action.action_priority}</span>
{action.problem_combination && <span className="text-sm text-muted-foreground">{action.problem_combination}</span>}
{action.business_type && <span className="text-xs rounded bg-muted px-2 py-0.5">{action.business_type}</span>}
</div>
</div>
)}
{/* Tab bar */}
<div className="flex gap-1 border-b">
{TABS.map(t => (
+3 -5
View File
@@ -108,13 +108,12 @@ router.get('/stores/quadrant', async (req: AuthRequest, res) => {
router.get('/stores/:code', async (req: AuthRequest, res) => {
try {
const code = req.params.code
const [scorecard, risk, platform, benchmark, action, execution] = await Promise.all([
const [scorecard, risk, platform, benchmark, action] = await Promise.all([
query(`SELECT * FROM analytics.v_store_scorecard WHERE store_code = $1`, [code]),
query(`SELECT * FROM analytics.mv_store_risk_rating WHERE store_code = $1`, [code]),
query(`SELECT * FROM analytics.v_store_platform_economics WHERE store_code = $1`, [code]),
query(`SELECT * FROM analytics.v_store_benchmark WHERE store_code = $1`, [code]),
query(`SELECT * FROM analytics.v_store_action_list WHERE store_code = $1`, [code]),
query(`SELECT * FROM analytics.v_store_execution_priority WHERE store_code = $1`, [code]),
query(`SELECT * FROM analytics.v_store_benchmark_composite WHERE store_code = $1`, [code]),
query(`SELECT * FROM analytics.v_store_action_priority_deep_april WHERE store_code = $1`, [code]),
])
if (scorecard.rows.length === 0) {
@@ -127,7 +126,6 @@ router.get('/stores/:code', async (req: AuthRequest, res) => {
platform: platform.rows[0],
benchmark: benchmark.rows[0],
action: action.rows[0],
execution: execution.rows[0],
})
} catch (err: any) {
sendError(res, err.message)