diff --git a/client/src/components/AgentDetailDialog.tsx b/client/src/components/AgentDetailDialog.tsx new file mode 100644 index 0000000..923f008 --- /dev/null +++ b/client/src/components/AgentDetailDialog.tsx @@ -0,0 +1,239 @@ +/** + * AI 员工详情弹窗 + * + * 点击 AgentCard 后弹出,展示完整能力描述、数据来源 API、今日交付明细列表, + * 并提供跳转到对应业务页面的按钮。 + */ +import { useQuery } from '@tanstack/react-query' +import { useNavigate } from 'react-router-dom' +import { X, ExternalLink, Bot } from 'lucide-react' +import api from '@/lib/api' +import { cn, formatCurrency, formatNumber } from '@/lib/utils' +import { LoadingSpinner } from '@/components/LoadingSpinner' +import type { AgentConfig } from '@/data/agents' + +interface AgentDetailDialogProps { + agent: AgentConfig + deliveries: number | null + onClose: () => void +} + +const AUTONOMY_LABELS: Record = { + L1: 'L1 人工审批', + L2: 'L2 受控执行', + L3: 'L3 全自动', +} + +const AUTONOMY_COLORS: Record = { + L1: 'bg-red-50 text-red-600 border border-red-200', + L2: 'bg-amber-50 text-amber-600 border border-amber-200', + L3: 'bg-green-50 text-green-600 border border-green-200', +} + +const STATUS_DOT: Record = { + '运行中': 'bg-green-500', + '待审批': 'bg-amber-500', + '受控': 'bg-blue-500', +} + +/** 后端统计字段名 → 中文标签映射(用于对象型 API 响应的统计卡片展示) */ +const FIELD_LABELS: Record = { + // 门店/成本总览 + total_stores: '门店总数', + profitable_stores: '盈利门店', + loss_stores: '亏损门店', + no_expense_stores: '缺费用数据门店', + total_bills: '账单总数', + total_received: '实收合计', + total_consumption: '消费合计', + total_discount: '优惠合计', + total_expense: '费用合计', + total_wage: '人工合计', + total_rent: '房租合计', + total_utility: '水电合计', + total_dorm: '宿舍合计', + total_commission: '佣金合计', + total_card_fee: '刷卡手续费', + total_repair: '维修保洁', + total_food_cost: '食材成本', + total_theoretical_cost: '理论成本', + total_contribution: '贡献利润', + total_theoretical_contribution: '理论贡献', + theoretical_net_profit: '理论净利润', + actual_net_profit: '实际净利润', + total_area: '总面积', + overall_expense_rate_pct: '费用率%', + overall_contribution_rate_pct: '贡献率%', + theoretical_net_margin_pct: '理论净利率%', + actual_net_margin_pct: '实际净利率%', + theoretical_food_cost_rate_pct: '理论食材成本率%', + actual_food_cost_rate_pct: '实际食材成本率%', + overall_wage_rate_pct: '人工费率%', + overall_rent_rate_pct: '房租费率%', + overall_utility_rate_pct: '水电费率%', + // 成本分析 + red_count: '红色严重超耗', + orange_count: '橙色明显超耗', + green_count: '绿色基本正常', + gray_count: '灰色口径异常', + total_variance: '差异合计', + // 告警总览 + total_alerts: '告警总数', + red_alerts: '红色告警', + yellow_alerts: '黄色告警', + pending_alerts: '待处理告警', + resolved_alerts: '已处理告警', + today_alerts: '今日告警', +} + +/** 数值字段格式化 */ +function formatCell(value: any): string { + if (value === null || value === undefined || value === '') return '-' + const num = Number(value) + if (!isNaN(num) && typeof value !== 'string' && value !== null) { + return formatNumber(num) + } + return String(value) +} + +export function AgentDetailDialog({ agent, deliveries, onClose }: AgentDetailDialogProps) { + const navigate = useNavigate() + + // 获取今日交付明细原始数据 + const { data: rawData, isLoading } = useQuery({ + queryKey: ['agent-delivery-detail', agent.code], + queryFn: () => { + // /overview/profit-opportunity 需要 month 参数(与 AgentsPage chiefQuery 一致) + const params = agent.deliveryApi === '/overview/profit-opportunity' ? { month: '2026-04' } : undefined + return api.get(agent.deliveryApi || '', { params }) + }, + enabled: !!agent.deliveryApi, + staleTime: 5 * 60 * 1000, + }) + + // 从响应中提取明细数据 + const root = rawData as any + const dataField = root?.data + // 支持 items 嵌套(如 profit-opportunity 返回 { items: [...] }) + const items: any[] = Array.isArray(dataField) ? dataField : Array.isArray(dataField?.items) ? dataField.items : [] + const stats: Record | null = !Array.isArray(dataField) && !dataField?.items && dataField && typeof dataField === 'object' ? dataField : null + + const top10 = items.slice(0, 10) + + return ( +
+
e.stopPropagation()} + > + {/* 标题栏 */} +
+
+
+ +
+
+
+ {agent.domain} + + {agent.status} +
+

{agent.name}

+

{agent.roleTarget}

+
+
+ +
+ + {/* 基本信息标签 */} +
+ + {AUTONOMY_LABELS[agent.autonomyLevel]} + + + 今日交付 {deliveries ?? '…'} 项 + +
+ + {/* 能力描述 */} +
+

对应 SBrainCO 能力

+
+

{agent.capability}

+
+
+ + {/* 数据来源 API */} + {agent.deliveryApi && ( +
+

数据来源

+
+

API: GET {agent.deliveryApi}

+

提取字段: {agent.deliveryField}

+
+
+ )} + + {/* 今日交付明细 */} + {agent.deliveryApi && ( +
+

今日交付明细{items.length > 10 ? `(前 10 条 / 共 ${items.length} 条)` : stats ? '' : `(共 ${items.length} 条)`}

+ {isLoading ? ( + + ) : top10.length > 0 && agent.deliveryColumns ? ( +
+ + + + {agent.deliveryColumns.map((col) => ( + + ))} + + + + {top10.map((row, i) => ( + + {agent.deliveryColumns!.map((col) => ( + + ))} + + ))} + +
{col.label}
{formatCell(row[col.field])}
+
+ ) : stats ? ( +
+ {Object.entries(stats).slice(0, 12).map(([key, val]) => ( +
+

{FIELD_LABELS[key] || key}

+

{formatCell(val)}

+
+ ))} +
+ ) : ( +

暂无明细数据

+ )} +
+ )} + + {/* 底部操作 */} + {agent.businessPage && ( +
+ +
+ )} +
+
+ ) +} diff --git a/client/src/components/AlertLogDetailDialog.tsx b/client/src/components/AlertLogDetailDialog.tsx new file mode 100644 index 0000000..8885d20 --- /dev/null +++ b/client/src/components/AlertLogDetailDialog.tsx @@ -0,0 +1,369 @@ +/** + * 预警日志详情弹窗 + * + * 点击预警日志行后弹出,展示完整预警信息、指标解释、问题诊断和整改建议, + * 并支持处理操作。 + */ +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. + ))} +
+
+
+ + {/* 处理备注 */} +
+

处理备注

+