412 lines
28 KiB
JavaScript
412 lines
28 KiB
JavaScript
// artboard-alarm.jsx — Event rule engine (告警事件 / 运维通知 / 业务事件)
|
||
// "事件规则" — 抽象事件,触发条件 + 通知/动作
|
||
|
||
const EVENT_KINDS = [
|
||
{ id: "all", label: "全部", count: 24, color: "var(--fg-2)", bg: "transparent" },
|
||
{ id: "alarm", label: "告警事件", count: 12, color: "var(--danger)", bg: "var(--danger-soft)", desc: "需立即处理 · P0/P1/P2" },
|
||
{ id: "ops", label: "运维通知", count: 7, color: "var(--warn)", bg: "var(--warn-soft)", desc: "保养/检修/合同到期" },
|
||
{ id: "biz", label: "业务事件", count: 4, color: "var(--info)", bg: "var(--info-soft)", desc: "里程/交付/调度状态" },
|
||
{ id: "auto", label: "自动化", count: 1, color: "var(--accent)", bg: "var(--accent-soft)", desc: "自动派单/路径下发" },
|
||
];
|
||
|
||
const KIND_META = {
|
||
alarm: { label: "告警", color: "var(--danger)", bg: "var(--danger-soft)" },
|
||
ops: { label: "运维", color: "var(--warn)", bg: "var(--warn-soft)" },
|
||
biz: { label: "业务", color: "var(--info)", bg: "var(--info-soft)" },
|
||
auto: { label: "自动化", color: "var(--accent)", bg: "var(--accent-soft)" },
|
||
};
|
||
|
||
// Mock rule library
|
||
const RULES = [
|
||
// 告警 (P0)
|
||
{ n:"H₂压力异常下降", kind:"alarm", c:"P0", on:true, h:"已触发 3 次", cond:"pressure < 35 MPa", actions:["站内","邮件","短信"], a:false },
|
||
{ n:"电堆过温保护", kind:"alarm", c:"P0", on:true, h:"已触发 1 次", cond:"stack.temp > 95℃", actions:["站内","短信","Webhook"], a:false },
|
||
{ n:"剩余氢量不足", kind:"alarm", c:"P0", on:true, h:"已触发 8 次", cond:"H₂ < 15% & 持续 60s", actions:["站内","邮件","路径下发"], a:true },
|
||
{ n:"胎压异常", kind:"alarm", c:"P1", on:true, h:"已触发 12 次", cond:"tire.pressure > 3.0 MPa", actions:["站内","推送"], a:false },
|
||
{ n:"超速预警", kind:"alarm", c:"P1", on:true, h:"已触发 47 次", cond:"speed > limit + 10 km/h", actions:["站内"], a:false },
|
||
{ n:"急加速密集", kind:"alarm", c:"P2", on:true, h:"已触发 18 次", cond:"3 次/分钟 within 5min", actions:["邮件"], a:false },
|
||
{ n:"夜间行驶", kind:"alarm", c:"P2", on:true, h:"已触发 6 次", cond:"22:00–06:00 + 行驶中", actions:["站内"], a:false },
|
||
// 运维通知
|
||
{ n:"总里程到达保养点", kind:"ops", c:"M1", on:true, h:"今日 4 辆", cond:"odometer % 20000 ≈ 0", actions:["站内","工单"], a:false },
|
||
{ n:"保养到期 30 天", kind:"ops", c:"M2", on:true, h:"今日 9 辆", cond:"days_to_maintenance ≤ 30", actions:["邮件","工单"], a:false },
|
||
{ n:"保险即将到期", kind:"ops", c:"M2", on:true, h:"本月 6 辆", cond:"days_to_insurance_end ≤ 60", actions:["邮件"], a:false },
|
||
{ n:"合同到期", kind:"ops", c:"M2", on:true, h:"本月 2 辆", cond:"days_to_contract_end ≤ 90", actions:["邮件","工单"], a:false },
|
||
{ n:"异常静止超 24h", kind:"ops", c:"M3", on:true, h:"今日 2 辆", cond:"idle_duration > 24h", actions:["站内"], a:false },
|
||
{ n:"长时间停留", kind:"ops", c:"M3", on:false, h:"已禁用", cond:"stop_duration > 4h & 非补能站", actions:["站内"], a:false },
|
||
// 业务事件
|
||
{ n:"今日交付完成", kind:"biz", c:"B1", on:true, h:"今日 38 单", cond:"delivery.status = 完成", actions:["站内","Webhook"], a:false },
|
||
{ n:"偏离规划路线", kind:"biz", c:"B2", on:true, h:"已触发 2 次", cond:"distance_from_route > 500m", actions:["站内","邮件"], a:false },
|
||
{ n:"进入禁行区域", kind:"biz", c:"B1", on:true, h:"已触发 0 次", cond:"geofence ∈ 禁行集合", actions:["站内","推送"], a:false },
|
||
{ n:"调度状态变更", kind:"biz", c:"B3", on:true, h:"实时", cond:"dispatch.status changed", actions:["Webhook"], a:false },
|
||
// 自动化
|
||
{ n:"低 SOC 自动派单至最近补能站", kind:"auto", c:"A1", on:true, h:"已执行 5 次", cond:"SOC < 20% & 行驶中", actions:["路径下发","站内"], a:false },
|
||
];
|
||
|
||
const ArtboardAlarm = () => {
|
||
const [activeKind, setActiveKind] = React.useState("all");
|
||
const [activeRule, setActiveRule] = React.useState(RULES.findIndex(r => r.a));
|
||
|
||
const filtered = RULES.filter(r => activeKind === "all" || r.kind === activeKind);
|
||
const rule = RULES[activeRule] || RULES[0];
|
||
|
||
return (
|
||
<div className="app">
|
||
<Sidebar active="alarm"/>
|
||
<div style={{flex:1, display:"flex", flexDirection:"column", minWidth:0, position:"relative", zIndex:1}}>
|
||
<Topbar crumbs={["事件规则", "规则引擎", rule.n]} kpis={[]} showSearch={false}/>
|
||
|
||
{/* Event-kind tabs */}
|
||
<div style={{padding:"10px 16px", borderBottom:"1px solid var(--border-1)", background:"var(--bg-1)", display:"flex", gap:8, alignItems:"center", flexWrap:"wrap"}}>
|
||
<span className="muted" style={{fontSize:11, marginRight:4}}>事件类型</span>
|
||
{EVENT_KINDS.map(k => (
|
||
<span key={k.id}
|
||
onClick={() => setActiveKind(k.id)}
|
||
style={{
|
||
padding:"5px 12px",
|
||
borderRadius:14,
|
||
fontSize:11,
|
||
cursor:"pointer",
|
||
background: activeKind === k.id ? k.bg : "transparent",
|
||
color: activeKind === k.id ? k.color : "var(--fg-2)",
|
||
border: "1px solid " + (activeKind === k.id ? k.color : "var(--border-1)"),
|
||
display:"flex", alignItems:"center", gap:6,
|
||
}}>
|
||
<span>{k.label}</span>
|
||
<span className="mono" style={{fontSize:10, opacity:.8}}>{k.count}</span>
|
||
</span>
|
||
))}
|
||
<div style={{marginLeft:"auto", display:"flex", gap:6}}>
|
||
<button className="btn sm"><Icon name="filter" size={11}/> 筛选</button>
|
||
<button className="btn sm"><Icon name="download" size={11}/> 导出</button>
|
||
<button className="btn primary sm"><Icon name="plus" size={11}/> 新建规则</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{flex:1, display:"grid", gridTemplateColumns:"260px 1fr 320px", minHeight:0}}>
|
||
{/* Rules list */}
|
||
<div style={{borderRight:"1px solid var(--border-1)", background:"var(--bg-1)", display:"flex", flexDirection:"column", minHeight:0}}>
|
||
<div style={{padding:"10px 14px", borderBottom:"1px solid var(--border-1)"}}>
|
||
<div className="between" style={{marginBottom:8}}>
|
||
<span className="eyebrow">规则 · {filtered.length}</span>
|
||
<span className="muted" style={{fontSize:10}}>共 {RULES.length} 条</span>
|
||
</div>
|
||
<div className="search" style={{height:26}}>
|
||
<Icon name="search" size={12}/><input placeholder="搜索规则名 / 字段"/>
|
||
</div>
|
||
</div>
|
||
<div className="scroll" style={{flex:1}}>
|
||
{filtered.map((r,i)=>{
|
||
const realIdx = RULES.indexOf(r);
|
||
const meta = KIND_META[r.kind];
|
||
const isActive = realIdx === activeRule;
|
||
return (
|
||
<div key={i}
|
||
onClick={() => setActiveRule(realIdx)}
|
||
style={{
|
||
padding:"10px 14px",
|
||
borderBottom:"1px solid var(--border-1)",
|
||
borderLeft: isActive ? "2px solid var(--accent)" : "2px solid transparent",
|
||
background: isActive ? "var(--accent-soft)" : "transparent",
|
||
cursor:"pointer"
|
||
}}>
|
||
<div className="between">
|
||
<span className="strong" style={{fontSize:12}}>{r.n}</span>
|
||
<span style={{fontSize:9, padding:"2px 6px", borderRadius:3, color:meta.color, background:meta.bg, fontWeight:500}}>
|
||
{meta.label}·{r.c}
|
||
</span>
|
||
</div>
|
||
<div className="muted mono" style={{fontSize:10, marginTop:4, whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis"}}>
|
||
{r.cond}
|
||
</div>
|
||
<div className="between" style={{marginTop:6}}>
|
||
<span className="muted" style={{fontSize:10}}>{r.h}</span>
|
||
<span style={{width:22, height:12, borderRadius:6, background: r.on?"var(--accent)":"var(--bg-3)", position:"relative", flexShrink:0}}>
|
||
<span style={{position:"absolute", top:1, left: r.on?11:1, width:10, height:10, borderRadius:5, background:"#fff", boxShadow:"0 1px 2px rgba(0,0,0,.2)"}}/>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Rule editor canvas */}
|
||
<RuleEditor rule={rule}/>
|
||
|
||
{/* Right: properties */}
|
||
<RuleProperties rule={rule}/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// ── Rule editor canvas ──────────────────────────────────────
|
||
const RuleEditor = ({ rule }) => {
|
||
const meta = KIND_META[rule.kind];
|
||
const isAlarm = rule.kind === "alarm";
|
||
const isOps = rule.kind === "ops";
|
||
const isBiz = rule.kind === "biz";
|
||
const isAuto = rule.kind === "auto";
|
||
|
||
// Build dynamic conditions per rule
|
||
const conds = (() => {
|
||
if (rule.n.startsWith("胎压")) return [{lbl:"WHEN", v:"tire.pressure", op:">", val:"3.0 MPa"}];
|
||
if (rule.n.startsWith("剩余氢量")) return [{lbl:"WHEN", v:"vehicle.h2.level", op:"<", val:"15 %"}, {lbl:"AND", v:"持续时长", op:"≥", val:"60 秒"}, {lbl:"AND NOT", v:"vehicle.location.poi", op:"=", val:"补能站"}];
|
||
if (rule.n.startsWith("H₂")) return [{lbl:"WHEN", v:"h2.pressure", op:"<", val:"35 MPa"}, {lbl:"AND", v:"vehicle.state", op:"=", val:"行驶中"}];
|
||
if (rule.n.startsWith("电堆")) return [{lbl:"WHEN", v:"fc.stack.temp", op:">", val:"95 ℃"}, {lbl:"AND", v:"持续时长", op:"≥", val:"30 秒"}];
|
||
if (rule.n.startsWith("超速")) return [{lbl:"WHEN", v:"vehicle.speed", op:">", val:"道路限速 + 10 km/h"}];
|
||
if (rule.n.startsWith("急加速")) return [{lbl:"WHEN", v:"急加速次数", op:"≥", val:"3 次"}, {lbl:"WITHIN", v:"时间窗", op:"=", val:"5 分钟"}];
|
||
if (rule.n.startsWith("夜间")) return [{lbl:"WHEN", v:"local_time", op:"∈", val:"[22:00, 06:00]"}, {lbl:"AND", v:"vehicle.state", op:"=", val:"行驶中"}];
|
||
if (rule.n.startsWith("总里程")) return [{lbl:"WHEN", v:"vehicle.odometer", op:"% 20,000 ≈", val:"0 km"}, {lbl:"AND", v:"距离上次保养里程", op:">", val:"19,500 km"}];
|
||
if (rule.n.startsWith("保养")) return [{lbl:"WHEN", v:"距下次保养", op:"≤", val:"30 天"}];
|
||
if (rule.n.startsWith("保险")) return [{lbl:"WHEN", v:"距保险到期", op:"≤", val:"60 天"}];
|
||
if (rule.n.startsWith("合同")) return [{lbl:"WHEN", v:"距合同到期", op:"≤", val:"90 天"}];
|
||
if (rule.n.startsWith("异常静止")) return [{lbl:"WHEN", v:"vehicle.idle_duration", op:">", val:"24 小时"}, {lbl:"AND", v:"asset.status", op:"=", val:"租赁/运营"}];
|
||
if (rule.n.startsWith("长时间停留")) return [{lbl:"WHEN", v:"stop_duration", op:">", val:"4 小时"}, {lbl:"AND NOT", v:"vehicle.location.poi", op:"=", val:"补能站/停车场"}];
|
||
if (rule.n.startsWith("今日交付")) return [{lbl:"WHEN", v:"delivery.status", op:"=", val:"已完成"}];
|
||
if (rule.n.startsWith("偏离")) return [{lbl:"WHEN", v:"distance_from_route", op:">", val:"500 m"}, {lbl:"AND", v:"持续时长", op:"≥", val:"30 秒"}];
|
||
if (rule.n.startsWith("进入禁行")) return [{lbl:"WHEN", v:"vehicle.geofence", op:"∈", val:"禁行围栏集合"}];
|
||
if (rule.n.startsWith("调度")) return [{lbl:"WHEN", v:"dispatch.status", op:"changed", val:"任意 → 任意"}];
|
||
if (rule.n.startsWith("低 SOC")) return [{lbl:"WHEN", v:"SOC", op:"<", val:"20 %"}, {lbl:"AND", v:"vehicle.state", op:"=", val:"行驶中"}, {lbl:"AND", v:"附近补能站", op:"≤", val:"5 km"}];
|
||
return [{lbl:"WHEN", v:"自定义条件", op:"-", val:"-"}];
|
||
})();
|
||
|
||
// Build dynamic actions per rule
|
||
const actionDefs = rule.actions.map(a => {
|
||
if (a === "站内") return {kind:"notif", icon:"inbox", title:"站内消息", who:"业务部门负责人 · 调度组", note:"实时"};
|
||
if (a === "邮件") return {kind:"notif", icon:"mail", title:"邮件", who:"业务负责人 · 安全官 (3人)", note:"含轨迹截图"};
|
||
if (a === "短信") return {kind:"notif", icon:"phone", title:"短信", who:"司机 + 业务负责人", note:"P0 级专用"};
|
||
if (a === "推送") return {kind:"notif", icon:"bell", title:"应用内推送", who:"业务负责人 + 客户联系人", note:"含一键导航"};
|
||
if (a === "Webhook") return {kind:"action",icon:"plug", title:"Webhook", who:"https://erp.lingniu.cn/hook/v1", note:"POST 事件 JSON"};
|
||
if (a === "工单") return {kind:"action",icon:"clipboard",title:"创建工单", who:"维保中心 · 自动指派", note:"24h SLA"};
|
||
if (a === "路径下发") return {kind:"action",icon:"route", title:"路径下发", who:"附近补能站 · TBOX", note:"司机端弹窗确认"};
|
||
return {kind:"notif", icon:"bell", title:a, who:"-", note:""};
|
||
});
|
||
|
||
return (
|
||
<div style={{
|
||
padding:16,
|
||
display:"flex", flexDirection:"column", minHeight:0,
|
||
background:"var(--bg-2)",
|
||
backgroundImage:"radial-gradient(circle, var(--border-1) 1px, transparent 1px)",
|
||
backgroundSize:"18px 18px", backgroundPosition:"-9px -9px",
|
||
}}>
|
||
{/* Header */}
|
||
<div className="between" style={{marginBottom:14}}>
|
||
<div>
|
||
<div className="mid gap-2">
|
||
<span className="strong" style={{fontSize:18, fontWeight:600}}>{rule.n}</span>
|
||
<span style={{fontSize:10, padding:"3px 8px", borderRadius:4, color:meta.color, background:meta.bg, fontWeight:600, border:"1px solid " + meta.color}}>
|
||
{meta.label} · {rule.c}
|
||
</span>
|
||
<span className="chip ok"><span className="dot ok"/> 已启用</span>
|
||
<span className="muted" style={{fontSize:10, marginLeft:6}}>v 1.4 · 2026-04-12</span>
|
||
</div>
|
||
<div className="muted" style={{fontSize:11, marginTop:4}}>
|
||
{isAlarm && "条件命中后立即生成告警事件,按通知渠道发送至业务负责人;P0 级支持一键派单。"}
|
||
{isOps && "条件命中后生成运维通知;如已配置工单动作,将创建保养/检修工单进入维保流程。"}
|
||
{isBiz && "条件命中后生成业务事件;可推送至 Webhook 联动 ERP / TMS / 调度系统。"}
|
||
{isAuto && "条件命中后自动执行动作(路径下发 / 状态变更),司机端弹窗确认后生效。"}
|
||
</div>
|
||
</div>
|
||
<div className="mid gap-2">
|
||
<button className="btn"><Icon name="play" size={12}/> 测试</button>
|
||
<button className="btn"><Icon name="history" size={13}/> 版本</button>
|
||
<button className="btn primary"><Icon name="bookmark" size={13}/> 保存</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Editor — split: WHEN | LOGIC | THEN */}
|
||
<div className="scroll" style={{flex:1, position:"relative", padding:"4px 0"}}>
|
||
<div style={{display:"grid", gridTemplateColumns:"260px 220px 1fr", gap:24, alignItems:"start", minHeight:"100%"}}>
|
||
|
||
{/* WHEN column */}
|
||
<div>
|
||
<div className="eyebrow" style={{marginBottom:8, color:"var(--info)"}}>① 触发条件 · WHEN</div>
|
||
<div style={{display:"flex", flexDirection:"column", gap:10}}>
|
||
{conds.map((c,i) => (
|
||
<div key={i} style={{padding:"10px 12px", background:"var(--bg-1)", border:"1px solid var(--border-2)", borderRadius:8, fontSize:11, boxShadow:"0 1px 2px rgba(0,0,0,.04)", position:"relative"}}>
|
||
<div className="between">
|
||
<span style={{fontSize:9, color:"var(--info)", fontWeight:600, letterSpacing:"0.05em"}}>{c.lbl}</span>
|
||
<Icon name="x" size={10} style={{color:"var(--fg-3)", cursor:"pointer"}}/>
|
||
</div>
|
||
<div className="strong" style={{marginTop:6, fontSize:12}}>{c.v}</div>
|
||
<div className="mid gap-2" style={{marginTop:4, fontSize:11}}>
|
||
<span style={{padding:"2px 6px", background:"var(--bg-2)", borderRadius:3, fontFamily:"var(--font-mono)", border:"1px solid var(--border-1)"}}>{c.op}</span>
|
||
<span className="mono strong" style={{color:"var(--accent)"}}>{c.val}</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
<div style={{padding:"8px 12px", background:"var(--bg-1)", borderRadius:6, border:"1px dashed var(--border-2)", fontSize:11, color:"var(--fg-3)", display:"flex", alignItems:"center", gap:6, justifyContent:"center", cursor:"pointer"}}>
|
||
<Icon name="plus" size={11}/> 添加条件
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* LOGIC center */}
|
||
<div style={{display:"flex", flexDirection:"column", alignItems:"center", paddingTop:32}}>
|
||
<div style={{padding:"16px 14px", background:"var(--accent-soft)", border:"1.5px solid var(--accent)", borderRadius:10, textAlign:"center", boxShadow:"0 0 24px var(--accent-glow)", width:"100%"}}>
|
||
<div style={{fontSize:10, color:"var(--accent)", letterSpacing:"0.1em", marginBottom:6}}>LOGIC GATE</div>
|
||
<div className="strong" style={{fontSize:18, fontFamily:"var(--font-mono)", color:"var(--accent)"}}>
|
||
{conds.length === 1 ? "A" : conds.map((c,i) => (c.lbl === "AND NOT" ? "¬" : "") + String.fromCharCode(65+i)).join(conds.length > 1 ? " ∧ " : "")}
|
||
</div>
|
||
<div className="muted" style={{fontSize:10, marginTop:6}}>{conds.length} 个条件 · 全部满足时触发</div>
|
||
</div>
|
||
<div style={{marginTop:14, padding:"10px 12px", background:"var(--bg-1)", border:"1px solid var(--border-1)", borderRadius:6, fontSize:10, width:"100%"}}>
|
||
<div className="muted" style={{marginBottom:4}}>评估窗口</div>
|
||
<div className="between"><span>采样频率</span><span className="mono strong">10 s · TBOX</span></div>
|
||
<div className="between" style={{marginTop:3}}><span>抑制窗口</span><span className="mono strong">15 min</span></div>
|
||
<div className="between" style={{marginTop:3}}><span>事件 ID</span><span className="mono" style={{color:"var(--accent)"}}>EVT-{rule.kind.toUpperCase()}-{rule.c}</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* THEN column — actions */}
|
||
<div>
|
||
<div className="eyebrow" style={{marginBottom:8, color:"var(--accent)"}}>② 触发动作 · THEN</div>
|
||
<div style={{display:"grid", gridTemplateColumns:"1fr 1fr", gap:10}}>
|
||
{actionDefs.map((a,i) => (
|
||
<div key={i} style={{padding:"10px 12px", background:"var(--bg-1)", border:"1px solid " + (a.kind === "action" ? "var(--accent)" : "var(--border-2)"), borderLeft:"3px solid " + (a.kind === "action" ? "var(--accent)" : "var(--info)"), borderRadius:6, fontSize:11, boxShadow:"0 1px 2px rgba(0,0,0,.04)"}}>
|
||
<div className="between">
|
||
<div className="mid gap-2">
|
||
<Icon name={a.icon} size={12} style={{color: a.kind === "action" ? "var(--accent)" : "var(--info)"}}/>
|
||
<span className="strong" style={{fontSize:11}}>{a.title}</span>
|
||
</div>
|
||
<span style={{fontSize:9, padding:"1px 5px", borderRadius:3, color: a.kind === "action" ? "var(--accent)" : "var(--info)", background: a.kind === "action" ? "var(--accent-soft)" : "var(--info-soft)"}}>
|
||
{a.kind === "action" ? "动作" : "通知"}
|
||
</span>
|
||
</div>
|
||
<div className="muted" style={{fontSize:10, marginTop:4, lineHeight:1.4}}>{a.who}</div>
|
||
<div className="mono" style={{fontSize:9, marginTop:3, color:"var(--fg-3)"}}>{a.note}</div>
|
||
</div>
|
||
))}
|
||
<div style={{padding:"10px 12px", background:"var(--bg-1)", border:"1px dashed var(--border-2)", borderRadius:6, fontSize:11, color:"var(--fg-3)", display:"flex", alignItems:"center", justifyContent:"center", gap:6, cursor:"pointer", minHeight:64}}>
|
||
<Icon name="plus" size={11}/> 添加动作
|
||
</div>
|
||
</div>
|
||
|
||
{/* Block library */}
|
||
<div style={{marginTop:14, padding:"10px 12px", background:"var(--bg-1)", border:"1px solid var(--border-1)", borderRadius:6}}>
|
||
<div className="eyebrow" style={{marginBottom:6}}>组件库 · 拖入条件 / 动作</div>
|
||
<div style={{display:"flex", flexWrap:"wrap", gap:6}}>
|
||
{[
|
||
{ic:"gauge", l:"数值阈值"},
|
||
{ic:"history", l:"持续时间"},
|
||
{ic:"pin", l:"地理围栏"},
|
||
{ic:"timeline", l:"时间窗口"},
|
||
{ic:"branch", l:"逻辑分支"},
|
||
{ic:"speed", l:"速度变化率"},
|
||
{ic:"chart", l:"趋势异常"},
|
||
{ic:"shield", l:"白名单"},
|
||
{ic:"clipboard", l:"创建工单"},
|
||
{ic:"plug", l:"Webhook"},
|
||
{ic:"route", l:"路径下发"},
|
||
{ic:"mail", l:"邮件"},
|
||
].map((b,i)=>(
|
||
<div key={i} className="mid gap-1" style={{padding:"4px 8px", background:"var(--bg-2)", border:"1px solid var(--border-1)", borderRadius:5, fontSize:10, cursor:"grab"}}>
|
||
<Icon name={b.ic} size={11} style={{color:"var(--accent)"}}/> <span>{b.l}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// ── Right-side properties ──────────────────────────────────
|
||
const RuleProperties = ({ rule }) => {
|
||
const meta = KIND_META[rule.kind];
|
||
const isAlarm = rule.kind === "alarm";
|
||
const isOps = rule.kind === "ops";
|
||
|
||
const channels = [
|
||
{ic:"inbox", l:"站内消息中心", on: rule.actions.includes("站内"), who:"业务部门(5) · 调度组(8)"},
|
||
{ic:"mail", l:"邮件", on: rule.actions.includes("邮件"), who:"业务负责人 · 安全官"},
|
||
{ic:"phone", l:"短信", on: rule.actions.includes("短信"), who:"司机 + 业务负责人"},
|
||
{ic:"bell", l:"应用内推送", on: rule.actions.includes("推送"), who:"业务负责人 + 客户联系人"},
|
||
{ic:"plug", l:"Webhook", on: rule.actions.includes("Webhook"), who:"erp.lingniu.cn/hook/v1"},
|
||
{ic:"clipboard",l:"工单", on: rule.actions.includes("工单"), who:"维保中心 · 24h SLA"},
|
||
];
|
||
|
||
return (
|
||
<div style={{borderLeft:"1px solid var(--border-1)", background:"var(--bg-1)", display:"flex", flexDirection:"column", minHeight:0}}>
|
||
<div className="panel-head" style={{borderBottom:"1px solid var(--border-1)"}}>
|
||
<Icon name="sliders" size={13}/><span className="title">规则属性</span>
|
||
</div>
|
||
<div className="scroll" style={{flex:1, padding:14}}>
|
||
<div className="eyebrow" style={{marginBottom:6}}>事件类型</div>
|
||
<div style={{padding:"8px 10px", background:meta.bg, border:"1px solid " + meta.color, borderRadius:5, fontSize:11, marginBottom:14, color:meta.color, fontWeight:600}}>
|
||
{meta.label} · {rule.c}
|
||
</div>
|
||
|
||
<div className="eyebrow" style={{marginBottom:6}}>{isAlarm ? "告警优先级" : isOps ? "运维等级" : "事件等级"}</div>
|
||
<div className="row gap-1" style={{marginBottom:14}}>
|
||
{(isAlarm ? ["P0","P1","P2"] : isOps ? ["M1","M2","M3"] : ["B1","B2","B3"]).map(p => (
|
||
<span key={p}
|
||
className={"chip " + (rule.c === p ? (isAlarm ? "danger" : isOps ? "warn" : "info") : "")}
|
||
style={{flex:1, justifyContent:"center", padding:"4px 0", opacity: rule.c === p ? 1 : 0.4}}>
|
||
{p}
|
||
</span>
|
||
))}
|
||
</div>
|
||
|
||
<div className="eyebrow" style={{marginBottom:6}}>适用车辆</div>
|
||
<div style={{padding:8, background:"var(--bg-2)", border:"1px solid var(--border-1)", borderRadius:5, fontSize:11, marginBottom:14}}>
|
||
<div className="strong">全部车辆 · 512</div>
|
||
<div className="muted" style={{fontSize:10, marginTop:2}}>排除维保中 6 辆 · 排除停运 4 辆</div>
|
||
</div>
|
||
|
||
<div className="eyebrow" style={{marginBottom:8}}>通知 / 动作渠道</div>
|
||
<div className="col gap-2" style={{marginBottom:14}}>
|
||
{channels.map((c,i)=>(
|
||
<div key={i} className="between" style={{padding:"8px 10px", background:"var(--bg-2)", border:"1px solid var(--border-1)", borderRadius:5}}>
|
||
<div className="mid gap-2">
|
||
<Icon name={c.ic} size={12} style={{color: c.on?"var(--accent)":"var(--fg-3)"}}/>
|
||
<div>
|
||
<div className="strong" style={{fontSize:11}}>{c.l}</div>
|
||
<div className="muted" style={{fontSize:10}}>{c.who}</div>
|
||
</div>
|
||
</div>
|
||
<span style={{width:22, height:12, borderRadius:6, background: c.on?"var(--accent)":"var(--bg-3)", position:"relative"}}>
|
||
<span style={{position:"absolute", top:1, left: c.on?11:1, width:10, height:10, borderRadius:5, background:"#fff", boxShadow:"0 1px 2px rgba(0,0,0,.2)"}}/>
|
||
</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div className="eyebrow" style={{marginBottom:6}}>抑制策略</div>
|
||
<div style={{padding:10, background:"var(--bg-2)", border:"1px solid var(--border-1)", borderRadius:5, fontSize:11, marginBottom:14}}>
|
||
<div className="between"><span className="muted">同车去重窗口</span><span className="mono strong">{isOps ? "24 小时" : "15 分钟"}</span></div>
|
||
<div className="between" style={{marginTop:6}}><span className="muted">每日上限</span><span className="mono strong">{isAlarm && rule.c === "P0" ? "无限制" : "20 次/车"}</span></div>
|
||
<div className="between" style={{marginTop:6}}><span className="muted">合并策略</span><span className="mono strong">{isOps ? "按车辆合并" : "不合并"}</span></div>
|
||
</div>
|
||
|
||
<div className="eyebrow" style={{marginBottom:6}}>静音时段</div>
|
||
<div style={{padding:10, background:"var(--bg-2)", border:"1px solid var(--border-1)", borderRadius:5, fontSize:11, marginBottom:14}}>
|
||
<div className="muted" style={{fontSize:10, lineHeight:1.5}}>
|
||
{isAlarm && rule.c === "P0" ? "P0 紧急规则不静音" : isAlarm ? "工作时段:8:00–20:00 推送" : isOps ? "仅工作日发送" : "无静音"}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="eyebrow" style={{marginBottom:6}}>近 7 日触发</div>
|
||
<div style={{padding:10, background:"var(--bg-2)", border:"1px solid var(--border-1)", borderRadius:5}}>
|
||
<div style={{display:"flex", gap:3, height:32, alignItems:"flex-end"}}>
|
||
{[3,7,2,9,12,5,8].map((v,i) => (
|
||
<div key={i} style={{flex:1, height: (v/12)*100 + "%", background: v > 8 ? meta.color : "var(--accent-soft)", borderRadius:1}}/>
|
||
))}
|
||
</div>
|
||
<div className="between muted" style={{marginTop:6, fontSize:10}}>
|
||
<span>4-22</span><span>4-28</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
window.ArtboardAlarm = ArtboardAlarm;
|