feat: 门店详情增加风险等级和经营象限详细说明+修复后端不存在的视图引用
This commit is contained in:
@@ -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 => (
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user