Files
oneos-truck-date-prototype/artboards/alarm.jsx
kkfluous b2d0016a0d
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful
init: 羚牛车辆数据中心原型 + 部署配置
- React 18 + Babel-in-browser SPA 原型,覆盖 8 个画板:
  实时地图 / 车辆详情 / 历史查询 / 轨迹回放 / 事件规则 / 通知中心 / ESG 碳减排 / 移动端
- 设计系统:IBM Plex Sans + JetBrains Mono,亮/暗双主题,羚牛绿 #007143
- 数据模型:12 + 40 辆车,TBOX (T) / JT808+1078 (JT) / 双源 (B)
- 部署:nginx 静态托管,Dockerfile + woodpecker.yml + docker-compose.yml
- 镜像:harbor.lnh2e.com/lingniu-v1/ln-vdc:<branch>-<VERSION>
- 容器端口 80,宿主映射 8112,含 /healthz 探活
2026-04-28 15:12:32 +08:00

412 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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:"电池SOC严重不足", kind:"alarm", c:"P0", on:true, h:"已触发 8 次", cond:"SOC < 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:0006: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("电池SOC")) return [{lbl:"WHEN", v:"vehicle.battery.soc", 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:0020: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;