/** * 预警日志详情弹窗 * * 点击预警日志行后弹出,展示完整预警信息、指标解释、问题诊断和整改建议, * 并支持处理操作。 */ import { useState } from 'react' import { useMutation, useQueryClient } from '@tanstack/react-query' import { X, AlertTriangle, CheckCircle, EyeOff, Stethoscope, Wrench } from 'lucide-react' import api from '@/lib/api' import { cn } from '@/lib/utils' interface AlertLog { id: number rule_id: number | null rule_name: string store_code: string store_name: string metric_value: number | string threshold: number | string severity: string push_status: string | null handle_status: string handle_comment: string | null triggered_at: string handled_at: string | null } interface AlertLogDetailDialogProps { log: AlertLog onClose: () => void } const SEVERITY_LABELS: Record = { red: '红灯', yellow: '黄灯', green: '绿灯' } const SEVERITY_COLORS: Record = { red: 'bg-red-100 text-red-700 border-red-200', yellow: 'bg-yellow-100 text-yellow-700 border-yellow-200', green: 'bg-green-100 text-green-700 border-green-200', } const STATUS_LABELS: Record = { pending: '待处理', handling: '处理中', resolved: '已解决', ignored: '已忽略' } const STATUS_COLORS: Record = { pending: 'bg-gray-100 text-gray-600 border-gray-200', handling: 'bg-blue-100 text-blue-700 border-blue-200', resolved: 'bg-green-100 text-green-700 border-green-200', ignored: 'bg-gray-100 text-gray-500 border-gray-200', } const PUSH_LABELS: Record = { pending: '待推送', pushed: '已推送', failed: '推送失败', null: '未推送', } /** 按规则名称映射:指标解释、问题诊断、整改建议 */ const RULE_DETAILS: Record = { '营收达成率红灯': { metricLabel: '营收达成率', metricUnit: '%', thresholdDesc: '阈值 80%,低于此值表示门店日营收远低于目标,存在严重经营风险', problemDiagnosis: [ '门店实际营收显著低于目标设定值,达成率不足 80%', '可能原因:客流下降、客单价下滑、促销活动效果不佳、周边竞争加剧', '需排查是目标设定过高还是实际经营下滑', ], remediation: [ '店长复盘当日客流与客单价数据,定位下滑原因', '区域经理 48 小时内到店诊断,提交改善方案', '检查近期是否有竞争对手开业或促销活动影响', '若目标设定不合理(如新店按成熟店标准),申请调整目标基准', '连续 3 天低于 80% 升级为专项整改任务', ], }, '营收达成率黄灯': { metricLabel: '营收达成率', metricUnit: '%', thresholdDesc: '阈值 90%,低于此值表示门店营收未达预期,需关注', problemDiagnosis: [ '门店营收达成率在 80%-90% 之间,未达预期但尚未严重恶化', '可能原因:天气影响、节假日因素、短期客流波动', ], remediation: [ '店长关注每日营收趋势,记录异常原因', '区域经理周检时复盘,判断是否为偶发或趋势性下滑', '若连续一周低于 90%,启动改善行动', ], }, '毛利率异常': { metricLabel: '理论毛利率', metricUnit: '%', thresholdDesc: '阈值 40%,低于此值表示食材成本占比过高,利润空间被压缩', problemDiagnosis: [ '理论毛利率低于 40%,食材成本占营收比超过 60%', '可能原因:菜品定价偏低、食材采购价上涨、BOM 配方不合理、高成本菜品占比过高', ], remediation: [ '菜品利润优化师核查 BOM 配方与实际用量差异', '供应链排查近期食材采购单价是否异常上涨', '菜单工程分析:评估低毛利菜品占比,考虑优化或淘汰', '门店检查是否存在超量出品或标准执行偏差', ], }, '会员渗透率低': { metricLabel: '会员账单占比', metricUnit: '%', thresholdDesc: '阈值 20%,低于此值表示会员消费占比过低,复购潜力未释放', problemDiagnosis: [ '会员账单占比低于 20%,非会员消费为主,顾客粘性不足', '可能原因:会员权益吸引力不够、店员未主动引导注册、周边客群流动性大', ], remediation: [ '会员运营师分析该店会员画像与复购率', '门店加强收银环节会员注册引导,设定周注册目标', '评估会员权益是否需要针对该店客群调整', '推送定向优惠券激活沉默会员', ], }, '客诉率偏高': { metricLabel: '客诉率', metricUnit: '%', thresholdDesc: '阈值 5%,超过此值表示顾客投诉比例过高,严重影响品牌口碑', problemDiagnosis: [ '客诉率超过 5%,顾客投诉频次异常偏高', '可能原因:出品质量不稳定、服务态度差、环境卫生问题、等待时间过长', ], remediation: [ '店长 24 小时内复盘投诉内容,分类归因', '出品问题:核查食材新鲜度与制作标准执行', '服务问题:安排员工服务培训,加强现场管理', '连续 2 周客诉率超 5% 升级为总部督办事项', ], }, '巡检低分': { metricLabel: '食安巡检得分', metricUnit: '分', thresholdDesc: '阈值 90 分,低于此值表示食品安全巡检不达标,存在合规风险', problemDiagnosis: [ '食安巡检得分低于 90 分,食品安全管理存在漏洞', '可能原因:食材储存不规范、加工区域卫生不达标、员工操作不规范', ], remediation: [ '店长立即整改巡检扣分项,48 小时内提交整改报告', '区域经理安排复查,确认整改到位', '对扣分项涉及员工进行专项培训', '连续 2 次低分暂停该店评级资格', ], }, '离职率偏高': { metricLabel: '月度离职率', metricUnit: '%', thresholdDesc: '阈值 15%,超过此值表示人员流失严重,影响门店稳定运营', problemDiagnosis: [ '月度离职率超过 15%,人员流失过快', '可能原因:薪酬竞争力不足、管理风格问题、工作强度过大、晋升通道不清晰', ], remediation: [ '区域经理访谈离职员工,收集真实离职原因', '人力部门评估该店薪酬水平与市场对标', '店长管理风格评估,必要时安排管理培训', '制定留人方案:调薪、排班优化、职业发展规划', ], }, } /** 通用兜底:未匹配到规则名时使用 */ const DEFAULT_DETAIL = { metricLabel: '监控指标', metricUnit: '', thresholdDesc: '指标超出预设阈值范围', problemDiagnosis: ['指标异常触发预警,需进一步排查具体原因'], remediation: ['请相关负责人核查指标数据并制定改善方案'], } function formatTime(s: string | null): string { if (!s) return '-' return s.substring(0, 19).replace('T', ' ') } export function AlertLogDetailDialog({ log, onClose }: AlertLogDetailDialogProps) { const queryClient = useQueryClient() const [comment, setComment] = useState(log.handle_comment || '') const handleMutation = useMutation({ mutationFn: (data: { id: number; handle_status: string; handle_comment?: string }) => api.patch(`/alert/logs/${data.id}`, { handle_status: data.handle_status, handle_comment: data.handle_comment }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['alert/logs'] }) queryClient.invalidateQueries({ queryKey: ['alert/overview'] }) onClose() }, }) const isPending = log.handle_status === 'pending' const detail = RULE_DETAILS[log.rule_name] || DEFAULT_DETAIL const metricValue = Number(log.metric_value || 0) const threshold = Number(log.threshold || 0) const deviation = metricValue - threshold const isLowerBetter = log.rule_name.includes('达成率') || log.rule_name.includes('毛利率') || log.rule_name.includes('渗透率') || log.rule_name.includes('巡检') return (
e.stopPropagation()} > {/* 标题栏 */}

{log.rule_name}

预警 #{log.id} · {log.store_name || '-'}

{/* 状态标签 */}
{SEVERITY_LABELS[log.severity] || log.severity} {STATUS_LABELS[log.handle_status] || log.handle_status} 推送:{PUSH_LABELS[log.push_status || 'null'] || log.push_status || '-'}
{/* 指标对比 */}

指标对比

{detail.metricLabel}(当前)

{metricValue.toFixed(2)}{detail.metricUnit}

预警阈值

{threshold.toFixed(2)}{detail.metricUnit}

偏离值

{deviation > 0 ? '+' : ''}{deviation.toFixed(2)}{detail.metricUnit}

{detail.thresholdDesc}

{/* 基本信息字段 */}

基本信息

门店名称

{log.store_name || '-'}

规则ID

{log.rule_id || '-'}

触发时间

{formatTime(log.triggered_at)}

处理时间

{formatTime(log.handled_at)}

{/* 问题诊断 */}

问题诊断

    {detail.problemDiagnosis.map((d, i) => (
  • {d}
  • ))}
{/* 整改建议 */}

整改建议

    {detail.remediation.map((r, i) => (
  1. {i + 1}. {r}
  2. ))}
{/* 处理备注 */}

处理备注