refine Semi UI mobile alert rule editing
This commit is contained in:
@@ -309,37 +309,115 @@ function ruleDraft(rule: AlertRule): AlertRuleInput {
|
||||
|
||||
function RulesWorkspace({ rules, metrics }: { rules: AlertRule[]; metrics: MetricDefinition[] }) {
|
||||
const queryClient = useQueryClient();
|
||||
const mobileLayout = useMobileLayout();
|
||||
const [selectedID, setSelectedID] = useState('');
|
||||
const [draft, setDraft] = useState<AlertRuleInput>(emptyRule());
|
||||
const [editorOpen, setEditorOpen] = useState(false);
|
||||
useEffect(() => { if (!selectedID && rules[0]) { setSelectedID(rules[0].id); setDraft(ruleDraft(rules[0])); } }, [rules, selectedID]);
|
||||
const save = useMutation({ mutationFn: api.saveAlertRuleV2, onSuccess: async (rule) => { setSelectedID(rule.id); setDraft(ruleDraft(rule)); await queryClient.invalidateQueries({ queryKey: ['alert-rules-v2'] }); } });
|
||||
const toggle = useMutation({ mutationFn: (rule: AlertRule) => api.setAlertRuleEnabledV2(rule.id, { version: rule.version, enabled: !rule.enabled }), onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ['alert-rules-v2'] }); } });
|
||||
useSideSheetA11y(mobileLayout && editorOpen, '.v2-alert-rule-editor-sidesheet', 'v2-alert-rule-editor', '告警规则编辑', '关闭告警规则编辑');
|
||||
const mutationError = save.error ?? toggle.error;
|
||||
const operators = draft.valueType === 'boolean' ? BOOLEAN_OPERATORS : NUMERIC_OPERATORS;
|
||||
const availableMetrics = metrics.filter((metric) => metric.alertable && metric.valueType === draft.valueType);
|
||||
const catalogLabels = Object.fromEntries(metrics.map((metric) => [metric.key, metric.label]));
|
||||
const setList = (key: 'scopeProtocols' | 'scopeVins' | 'scopeOems' | 'scopeModels' | 'scopeCompanies', value: string) => setDraft({ ...draft, [key]: value.split(',').map((item) => item.trim()).filter(Boolean) });
|
||||
const openNewRule = () => {
|
||||
setSelectedID('__new__');
|
||||
setDraft(emptyRule());
|
||||
if (mobileLayout) setEditorOpen(true);
|
||||
};
|
||||
const openRule = (rule: AlertRule) => {
|
||||
setSelectedID(rule.id);
|
||||
setDraft(ruleDraft(rule));
|
||||
if (mobileLayout) setEditorOpen(true);
|
||||
};
|
||||
const applyTemplate = (template: RuleTemplate) => {
|
||||
setSelectedID('__new__');
|
||||
setDraft({ ...template.draft, scopeProtocols: [...template.draft.scopeProtocols], notificationChannels: [...template.draft.notificationChannels] });
|
||||
};
|
||||
const severityColor = (severity: AlertRule['severity']) => severity === 'critical' ? 'red' : severity === 'major' ? 'orange' : 'amber';
|
||||
return <div className="v2-alert-rules">
|
||||
const toggleButton = draft.version ? <Button
|
||||
htmlType="button"
|
||||
theme="light"
|
||||
disabled={toggle.isPending || save.isPending}
|
||||
onClick={() => {
|
||||
const current = rules.find((item) => item.id === draft.id);
|
||||
if (current) toggle.mutate(current);
|
||||
}}
|
||||
>{toggle.isPending ? '更新中' : draft.enabled ? '停用规则' : '启用规则'}</Button> : null;
|
||||
const editor = <Card className="v2-alert-rule-editor" bodyStyle={{ padding: 0 }}>
|
||||
<form className="v2-alert-rule-editor-form" onSubmit={(event) => { event.preventDefault(); save.mutate(draft); }}>
|
||||
{!mobileLayout ? <WorkspacePanelHeader
|
||||
className="v2-alert-rule-editor-heading"
|
||||
title={draft.version ? '编辑规则' : '新建规则'}
|
||||
description="数值、状态与主数据范围均纳入版本审计"
|
||||
actions={toggleButton}
|
||||
/> : <div className={`v2-alert-rule-sheet-status${draft.version ? '' : ' is-new'}`}>
|
||||
<span>
|
||||
<Tag size="small" color={draft.version ? draft.enabled ? 'green' : 'grey' : 'blue'} type="light">{draft.version ? draft.enabled ? '已启用' : '已停用' : '新规则'}</Tag>
|
||||
<small>{draft.version ? `v${draft.version} · 配置变更写入版本审计` : '保存后生成首个可审计版本'}</small>
|
||||
</span>
|
||||
{toggleButton}
|
||||
</div>}
|
||||
<section className="v2-alert-rule-templates" aria-label="告警规则模板">
|
||||
<div><strong>快速创建</strong><span>从常用业务场景开始,再按车辆、车型或企业收窄范围。</span></div>
|
||||
<nav>{RULE_TEMPLATES.map((template) => {
|
||||
const available = metrics.some((metric) => metric.key === template.draft.metric && metric.alertable);
|
||||
return <Button
|
||||
className="v2-alert-template-card"
|
||||
key={template.id}
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
disabled={!available}
|
||||
onClick={() => applyTemplate(template)}
|
||||
>
|
||||
<span className="v2-alert-template-icon"><IconAlarm /></span>
|
||||
<span className="v2-alert-template-copy"><b>{template.title}</b><small>{available ? template.summary : '当前字段目录尚未启用此指标'}</small></span>
|
||||
<Tag size="small" color={available ? 'blue' : 'grey'} type="light">{available ? (template.draft.scopeProtocols.join(' / ') || '全部协议') : '不可用'}</Tag>
|
||||
</Button>;
|
||||
})}</nav>
|
||||
</section>
|
||||
<div className="v2-rule-form-grid">
|
||||
<label><span>规则名称</span><Input required maxLength={80} value={draft.name} onChange={(value) => setDraft({ ...draft, name: value })} /></label>
|
||||
<label><span>严重程度</span><Select value={draft.severity} onChange={(value) => setDraft({ ...draft, severity: String(value) as AlertRuleInput['severity'] })} optionList={[{ value: 'critical', label: '紧急' }, { value: 'major', label: '重要' }, { value: 'minor', label: '一般' }]} /></label>
|
||||
<label><span>值类型</span><Select value={draft.valueType} onChange={(value) => { const valueType = String(value) as AlertRuleInput['valueType']; const metric = metrics.find((item) => item.alertable && item.valueType === valueType)?.key ?? ''; setDraft({ ...draft, valueType, operator: valueType === 'boolean' ? 'eq' : 'gt', metric }); }} optionList={[{ value: 'numeric', label: '数值' }, { value: 'boolean', label: '布尔' }]} /></label>
|
||||
<label><span>指标</span><Select disabled={!availableMetrics.length} value={draft.metric} onChange={(value) => setDraft({ ...draft, metric: String(value) })} optionList={availableMetrics.map((metric) => ({ value: metric.key, label: `${metric.label}${metric.unit ? ` (${metric.unit})` : ''}` }))} /></label>
|
||||
<label><span>触发比较符</span><Select value={draft.operator} onChange={(value) => { const operator = String(value); setDraft({ ...draft, operator, durationSec: operator === 'changed' ? 0 : draft.durationSec }); }} optionList={operators.map((value) => ({ value, label: operatorLabels[value] }))} /></label>
|
||||
{draft.operator === 'changed' ? <label><span>变化语义</span><Input value="false ↔ true" disabled /></label> : draft.valueType === 'boolean' ? <label><span>目标值</span><Select value={draft.booleanThreshold ? 'true' : 'false'} onChange={(value) => setDraft({ ...draft, booleanThreshold: value === 'true' })} optionList={[{ value: 'true', label: '是' }, { value: 'false', label: '否' }]} /></label> : <label><span>{draft.operator === 'between' || draft.operator === 'outside' ? '区间下限' : '触发阈值'}</span><Input required type="number" step="0.01" placeholder={draft.metric === 'hydrogen_concentration_percent' ? '请按厂家标准填写百分比' : undefined} value={Number.isFinite(draft.threshold) ? String(draft.threshold) : ''} onChange={(value) => setDraft({ ...draft, threshold: value === '' ? Number.NaN : Number(value) })} /></label>}
|
||||
{draft.operator === 'between' || draft.operator === 'outside' ? <label><span>区间上限</span><Input type="number" step="0.1" value={String(draft.thresholdHigh)} onChange={(value) => setDraft({ ...draft, thresholdHigh: Number(value) })} /></label> : null}
|
||||
<label><span>持续时间(秒)</span><Input type="number" min="0" max="86400" disabled={draft.operator === 'changed'} value={String(draft.durationSec)} onChange={(value) => setDraft({ ...draft, durationSec: Number(value) })} /></label>
|
||||
<label><span>恢复比较符</span><Select value={draft.recoveryOperator} onChange={(value) => setDraft({ ...draft, recoveryOperator: String(value) })} optionList={[{ value: '', label: '未配置' }, ...['gt', 'gte', 'lt', 'lte', 'eq', 'neq'].map((value) => ({ value, label: operatorLabels[value] }))]} /></label>
|
||||
<label><span>恢复阈值</span><Input type="number" step="0.1" value={String(draft.recoveryThreshold)} onChange={(value) => setDraft({ ...draft, recoveryThreshold: Number(value) })} /></label>
|
||||
<label><span>重复间隔(秒)</span><Input type="number" min="0" max="604800" value={String(draft.repeatIntervalSec)} onChange={(value) => setDraft({ ...draft, repeatIntervalSec: Number(value) })} /></label>
|
||||
<label className="is-wide"><span>协议范围(逗号分隔;空为全部)</span><Input value={draft.scopeProtocols.join(',')} onChange={(value) => setList('scopeProtocols', value)} /></label>
|
||||
<label className="is-wide"><span>车辆 VIN 范围(逗号分隔;空为全部)</span><Input value={draft.scopeVins.join(',')} onChange={(value) => setList('scopeVins', value)} /></label>
|
||||
<label className="is-wide"><span>厂家范围(逗号分隔;空为全部)</span><Input value={draft.scopeOems.join(',')} onChange={(value) => setList('scopeOems', value)} /></label>
|
||||
<label className="is-wide"><span>车型范围(来自车辆主档;空为全部)</span><Input value={draft.scopeModels.join(',')} onChange={(value) => setList('scopeModels', value)} /></label>
|
||||
<label className="is-wide"><span>企业范围(来自车辆主档;空为全部)</span><Input value={draft.scopeCompanies.join(',')} onChange={(value) => setList('scopeCompanies', value)} /></label>
|
||||
<label className="is-wide"><span>说明</span><TextArea maxCount={500} autosize={{ minRows: 3, maxRows: 7 }} value={draft.description} onChange={(value) => setDraft({ ...draft, description: value })} /></label>
|
||||
</div>
|
||||
<footer><div><b>通知通道</b><span>站内通知已启用;短信、邮件、企微为预留 / 未启用。</span></div>{mutationError ? <em>{mutationError.message}</em> : null}<Button className="v2-primary-button" theme="solid" htmlType="submit" disabled={save.isPending || toggle.isPending}>{save.isPending ? '保存中' : '保存规则'}</Button></footer>
|
||||
</form>
|
||||
</Card>;
|
||||
return <div className={`v2-alert-rules${mobileLayout ? ' is-mobile' : ''}`}>
|
||||
<Card className="v2-alert-rule-list" bodyStyle={{ padding: 0 }}>
|
||||
<WorkspacePanelHeader
|
||||
className="v2-alert-rule-list-heading"
|
||||
title="规则配置"
|
||||
description={rules.length ? `${rules.length} 条可审计规则` : '尚未创建规则'}
|
||||
actions={<Button theme="light" type="primary" icon={<IconPlus />} onClick={() => { setSelectedID('__new__'); setDraft(emptyRule()); }}>新建规则</Button>}
|
||||
actions={<Button theme="light" type="primary" icon={<IconPlus />} aria-haspopup={mobileLayout ? 'dialog' : undefined} aria-expanded={mobileLayout ? editorOpen && selectedID === '__new__' : undefined} onClick={openNewRule}>新建规则</Button>}
|
||||
/>
|
||||
<div className="v2-alert-rule-items" role="region" tabIndex={0} aria-label="告警规则列表,可左右滚动选择">
|
||||
<div className="v2-alert-rule-items" role="region" tabIndex={0} aria-label={mobileLayout ? '告警规则列表' : '告警规则列表,可左右滚动选择'}>
|
||||
{rules.map((rule) => <Button
|
||||
className={`v2-alert-rule-item${selectedID === rule.id ? ' is-selected' : ''}`}
|
||||
key={rule.id}
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
aria-pressed={selectedID === rule.id}
|
||||
onClick={() => { setSelectedID(rule.id); setDraft(ruleDraft(rule)); }}
|
||||
aria-haspopup={mobileLayout ? 'dialog' : undefined}
|
||||
aria-expanded={mobileLayout ? editorOpen && selectedID === rule.id : undefined}
|
||||
onClick={() => openRule(rule)}
|
||||
>
|
||||
<span className={`v2-alert-rule-dot is-${rule.severity}`} aria-hidden="true" />
|
||||
<span className="v2-alert-rule-copy">
|
||||
@@ -351,54 +429,18 @@ function RulesWorkspace({ rules, metrics }: { rules: AlertRule[]; metrics: Metri
|
||||
{!rules.length ? <Empty className="v2-alert-rule-empty" title="暂无告警规则" description="从右侧模板快速创建,或新建一条自定义规则。" /> : null}
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="v2-alert-rule-editor" bodyStyle={{ padding: 0 }}>
|
||||
<form className="v2-alert-rule-editor-form" onSubmit={(event) => { event.preventDefault(); save.mutate(draft); }}>
|
||||
<WorkspacePanelHeader
|
||||
className="v2-alert-rule-editor-heading"
|
||||
title={draft.version ? '编辑规则' : '新建规则'}
|
||||
description="数值、状态与主数据范围均纳入版本审计"
|
||||
actions={draft.version ? <Button htmlType="button" theme="light" disabled={toggle.isPending || save.isPending} onClick={() => { const current = rules.find((item) => item.id === draft.id); if (current) toggle.mutate(current); }}>{toggle.isPending ? '更新中' : draft.enabled ? '停用规则' : '启用规则'}</Button> : null}
|
||||
/>
|
||||
<section className="v2-alert-rule-templates" aria-label="告警规则模板">
|
||||
<div><strong>快速创建</strong><span>从常用业务场景开始,再按车辆、车型或企业收窄范围。</span></div>
|
||||
<nav>{RULE_TEMPLATES.map((template) => {
|
||||
const available = metrics.some((metric) => metric.key === template.draft.metric && metric.alertable);
|
||||
return <Button
|
||||
className="v2-alert-template-card"
|
||||
key={template.id}
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
disabled={!available}
|
||||
onClick={() => applyTemplate(template)}
|
||||
>
|
||||
<span className="v2-alert-template-icon"><IconAlarm /></span>
|
||||
<span className="v2-alert-template-copy"><b>{template.title}</b><small>{available ? template.summary : '当前字段目录尚未启用此指标'}</small></span>
|
||||
<Tag size="small" color={available ? 'blue' : 'grey'} type="light">{available ? (template.draft.scopeProtocols.join(' / ') || '全部协议') : '不可用'}</Tag>
|
||||
</Button>;
|
||||
})}</nav>
|
||||
</section>
|
||||
<div className="v2-rule-form-grid">
|
||||
<label><span>规则名称</span><Input required maxLength={80} value={draft.name} onChange={(value) => setDraft({ ...draft, name: value })} /></label>
|
||||
<label><span>严重程度</span><Select value={draft.severity} onChange={(value) => setDraft({ ...draft, severity: String(value) as AlertRuleInput['severity'] })} optionList={[{ value: 'critical', label: '紧急' }, { value: 'major', label: '重要' }, { value: 'minor', label: '一般' }]} /></label>
|
||||
<label><span>值类型</span><Select value={draft.valueType} onChange={(value) => { const valueType = String(value) as AlertRuleInput['valueType']; const metric = metrics.find((item) => item.alertable && item.valueType === valueType)?.key ?? ''; setDraft({ ...draft, valueType, operator: valueType === 'boolean' ? 'eq' : 'gt', metric }); }} optionList={[{ value: 'numeric', label: '数值' }, { value: 'boolean', label: '布尔' }]} /></label>
|
||||
<label><span>指标</span><Select disabled={!availableMetrics.length} value={draft.metric} onChange={(value) => setDraft({ ...draft, metric: String(value) })} optionList={availableMetrics.map((metric) => ({ value: metric.key, label: `${metric.label}${metric.unit ? ` (${metric.unit})` : ''}` }))} /></label>
|
||||
<label><span>触发比较符</span><Select value={draft.operator} onChange={(value) => { const operator = String(value); setDraft({ ...draft, operator, durationSec: operator === 'changed' ? 0 : draft.durationSec }); }} optionList={operators.map((value) => ({ value, label: operatorLabels[value] }))} /></label>
|
||||
{draft.operator === 'changed' ? <label><span>变化语义</span><Input value="false ↔ true" disabled /></label> : draft.valueType === 'boolean' ? <label><span>目标值</span><Select value={draft.booleanThreshold ? 'true' : 'false'} onChange={(value) => setDraft({ ...draft, booleanThreshold: value === 'true' })} optionList={[{ value: 'true', label: '是' }, { value: 'false', label: '否' }]} /></label> : <label><span>{draft.operator === 'between' || draft.operator === 'outside' ? '区间下限' : '触发阈值'}</span><Input required type="number" step="0.01" placeholder={draft.metric === 'hydrogen_concentration_percent' ? '请按厂家标准填写百分比' : undefined} value={Number.isFinite(draft.threshold) ? String(draft.threshold) : ''} onChange={(value) => setDraft({ ...draft, threshold: value === '' ? Number.NaN : Number(value) })} /></label>}
|
||||
{draft.operator === 'between' || draft.operator === 'outside' ? <label><span>区间上限</span><Input type="number" step="0.1" value={String(draft.thresholdHigh)} onChange={(value) => setDraft({ ...draft, thresholdHigh: Number(value) })} /></label> : null}
|
||||
<label><span>持续时间(秒)</span><Input type="number" min="0" max="86400" disabled={draft.operator === 'changed'} value={String(draft.durationSec)} onChange={(value) => setDraft({ ...draft, durationSec: Number(value) })} /></label>
|
||||
<label><span>恢复比较符</span><Select value={draft.recoveryOperator} onChange={(value) => setDraft({ ...draft, recoveryOperator: String(value) })} optionList={[{ value: '', label: '未配置' }, ...['gt', 'gte', 'lt', 'lte', 'eq', 'neq'].map((value) => ({ value, label: operatorLabels[value] }))]} /></label>
|
||||
<label><span>恢复阈值</span><Input type="number" step="0.1" value={String(draft.recoveryThreshold)} onChange={(value) => setDraft({ ...draft, recoveryThreshold: Number(value) })} /></label>
|
||||
<label><span>重复间隔(秒)</span><Input type="number" min="0" max="604800" value={String(draft.repeatIntervalSec)} onChange={(value) => setDraft({ ...draft, repeatIntervalSec: Number(value) })} /></label>
|
||||
<label className="is-wide"><span>协议范围(逗号分隔;空为全部)</span><Input value={draft.scopeProtocols.join(',')} onChange={(value) => setList('scopeProtocols', value)} /></label>
|
||||
<label className="is-wide"><span>车辆 VIN 范围(逗号分隔;空为全部)</span><Input value={draft.scopeVins.join(',')} onChange={(value) => setList('scopeVins', value)} /></label>
|
||||
<label className="is-wide"><span>厂家范围(逗号分隔;空为全部)</span><Input value={draft.scopeOems.join(',')} onChange={(value) => setList('scopeOems', value)} /></label>
|
||||
<label className="is-wide"><span>车型范围(来自车辆主档;空为全部)</span><Input value={draft.scopeModels.join(',')} onChange={(value) => setList('scopeModels', value)} /></label>
|
||||
<label className="is-wide"><span>企业范围(来自车辆主档;空为全部)</span><Input value={draft.scopeCompanies.join(',')} onChange={(value) => setList('scopeCompanies', value)} /></label>
|
||||
<label className="is-wide"><span>说明</span><TextArea maxCount={500} autosize={{ minRows: 3, maxRows: 7 }} value={draft.description} onChange={(value) => setDraft({ ...draft, description: value })} /></label>
|
||||
</div>
|
||||
<footer><div><b>通知通道</b><span>站内通知已启用;短信、邮件、企微为预留 / 未启用。</span></div>{mutationError ? <em>{mutationError.message}</em> : null}<Button className="v2-primary-button" theme="solid" htmlType="submit" disabled={save.isPending || toggle.isPending}>{save.isPending ? '保存中' : '保存规则'}</Button></footer>
|
||||
</form>
|
||||
</Card>
|
||||
{!mobileLayout ? editor : null}
|
||||
{mobileLayout ? <SideSheet
|
||||
className="v2-alert-rule-editor-sidesheet"
|
||||
visible={editorOpen}
|
||||
aria-label="告警规则编辑"
|
||||
placement="bottom"
|
||||
height="min(92dvh, 820px)"
|
||||
title={<div className="v2-alert-sheet-title"><strong>{draft.version ? '编辑规则' : '新建规则'}</strong><span>{draft.name || '配置触发条件、范围与通知策略'}</span></div>}
|
||||
onCancel={() => setEditorOpen(false)}
|
||||
>
|
||||
{editorOpen ? editor : null}
|
||||
</SideSheet> : null}
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user