@@ -90,6 +90,8 @@ export default function NotificationsPage() {
9090 const [ errThreshold , setErrThreshold ] = useState ( "1" ) ;
9191 const [ errRetry , setErrRetry ] = useState ( "30" ) ;
9292 const [ rulesSaving , setRulesSaving ] = useState ( false ) ;
93+ // 每类告警的推送渠道绑定(空 = 所有启用渠道);选择即落库,与开关一致
94+ const [ channelsFor , setChannelsFor ] = useState < Record < string , string [ ] > > ( { } ) ;
9395
9496 // 每日日报表单
9597 const [ drEnabled , setDrEnabled ] = useState ( false ) ;
@@ -119,6 +121,7 @@ export default function NotificationsPage() {
119121 setRenotify ( String ( r ?. renotifyHours ?? 24 ) ) ;
120122 setErrThreshold ( String ( r ?. errorThreshold ?? 1 ) ) ;
121123 setErrRetry ( String ( r ?. errorRetrySec ?? 30 ) ) ;
124+ setChannelsFor ( r ?. channelsFor || { } ) ;
122125 } ;
123126
124127 const syncDrForm = ( s : any ) => {
@@ -148,11 +151,13 @@ export default function NotificationsPage() {
148151 } , [ ] ) ;
149152
150153 // 重新拉取渠道数据(对照 v1 loadNotifications)
154+ // 同步渠道绑定(删除渠道时服务端会清理其中的死 id),但不动阈值输入表单
151155 const reloadChannels = async ( ) => {
152156 const n = await api ( "/api/notifications" ) ;
153157 setChannels ( n . channels ) ;
154158 setRules ( n . rules ) ;
155159 setChannelTypes ( n . channelTypes ) ;
160+ setChannelsFor ( n . rules ?. channelsFor || { } ) ;
156161 } ;
157162
158163 // ---- 渠道操作 ---------------------------------------------------------------
@@ -288,6 +293,47 @@ export default function NotificationsPage() {
288293 }
289294 } ;
290295
296+ // 每类告警的渠道绑定:选择即落库;失败时回滚为服务端状态
297+ const saveChannelsFor = async ( key : string , ids : string [ ] ) => {
298+ setChannelsFor ( ( cf ) => ( { ...cf , [ key ] : ids } ) ) ;
299+ try {
300+ const r = await api ( "/api/notifications/rules" , {
301+ method : "PUT" ,
302+ body : { channelsFor : { [ key ] : ids } } ,
303+ } ) ;
304+ setRules ( r . rules ) ;
305+ setChannelsFor ( r . rules ?. channelsFor || { } ) ;
306+ } catch ( e : any ) {
307+ message . error ( e . message ) ;
308+ setChannelsFor ( rules ?. channelsFor || { } ) ;
309+ }
310+ } ;
311+
312+ // 告警规则行右侧:渠道多选(空 = 全部启用渠道)+ 开关
313+ const alertChannelOptions = channels . map ( ( c : any ) => ( {
314+ value : c . id ,
315+ label : c . enabled === false ? `${ c . name } (已停用)` : c . name ,
316+ } ) ) ;
317+ // 普通渲染函数而非内嵌组件:避免每次渲染产生新组件类型导致 Select 重挂、
318+ // 多选下拉每选一项就被关闭
319+ const renderAlertControls = ( evKey : string , ruleKey : string ) => (
320+ < div style = { { display : "flex" , gap : 8 , alignItems : "center" , flexWrap : "wrap" , justifyContent : "flex-end" } } >
321+ < Select
322+ mode = "multiple"
323+ allowClear
324+ style = { { minWidth : 200 , maxWidth : 320 } }
325+ placeholder = "全部启用渠道"
326+ maxTagCount = "responsive"
327+ optionFilterProp = "label"
328+ value = { channelsFor [ evKey ] || [ ] }
329+ onChange = { ( ids ) => saveChannelsFor ( evKey , ids as string [ ] ) }
330+ options = { alertChannelOptions }
331+ disabled = { ! channels . length }
332+ />
333+ < Switch checked = { ! ! rules [ ruleKey ] } onChange = { ( ) => toggleRule ( ruleKey ) } />
334+ </ div >
335+ ) ;
336+
291337 // 切换单位时把输入值换算过去(两个单位间必然是互换,对照 v1 rule-etaUnit onchange)
292338 const onEtaUnitChange = ( u : "days" | "hours" ) => {
293339 const v = Number ( etaVal ) ;
@@ -395,7 +441,7 @@ export default function NotificationsPage() {
395441 title = { < span style = { { whiteSpace : "nowrap" , flexShrink : 0 } } > 通知渠道</ span > }
396442 extra = {
397443 < Text type = "secondary" style = { { fontSize : 12 , whiteSpace : "normal" , textAlign : "right" } } >
398- 告警将同时推送到所有启用的渠道
444+ 在下方「告警规则」中可为每类告警单独选择推送渠道
399445 </ Text >
400446 }
401447 >
@@ -451,20 +497,23 @@ export default function NotificationsPage() {
451497
452498 { /* 告警规则 */ }
453499 < ProCard title = "告警规则" style = { { marginTop : 16 } } >
500+ < Text type = "secondary" style = { { fontSize : 12 , display : "block" , marginBottom : 4 } } >
501+ 每类告警可单独选择推送渠道;不选 = 发送到所有启用的渠道
502+ </ Text >
454503 < SetRow title = "余额偏低" desc = "剩余余额低于阈值时通知" >
455- < Switch checked = { ! ! r . onLow } onChange = { ( ) => toggleRule ( "onLow" ) } />
504+ { renderAlertControls ( "low" , "onLow" ) }
456505 </ SetRow >
457506 < SetRow title = "余额耗尽" desc = "剩余余额归零时通知" >
458- < Switch checked = { ! ! r . onExhaust } onChange = { ( ) => toggleRule ( "onExhaust" ) } />
507+ { renderAlertControls ( "exhaust" , "onExhaust" ) }
459508 </ SetRow >
460509 < SetRow title = "查询失败" desc = "接口查询出错时通知(令牌失效、站点宕机等)" >
461- < Switch checked = { ! ! r . onError } onChange = { ( ) => toggleRule ( "onError" ) } />
510+ { renderAlertControls ( "error" , "onError" ) }
462511 </ SetRow >
463512 < SetRow title = "恢复正常" desc = "从异常状态恢复后通知" >
464- < Switch checked = { ! ! r . onRecover } onChange = { ( ) => toggleRule ( "onRecover" ) } />
513+ { renderAlertControls ( "recover" , "onRecover" ) }
465514 </ SetRow >
466515 < SetRow title = "耗尽预警" desc = "按消耗速度预计即将耗尽时通知" >
467- < Switch checked = { ! ! r . onEta } onChange = { ( ) => toggleRule ( "onEta" ) } />
516+ { renderAlertControls ( "eta" , "onEta" ) }
468517 </ SetRow >
469518 < SetRow title = "耗尽预警阈值" desc = "预计在该时间内耗尽则触发「耗尽预警」,可按天或小时设置" >
470519 < div style = { { display : "flex" , gap : 8 } } >
@@ -564,7 +613,7 @@ export default function NotificationsPage() {
564613 }
565614 destroyOnHidden
566615 >
567- < Text type = "secondary" > 告警触发时将推送到所有启用的渠道 。</ Text >
616+ < Text type = "secondary" > 默认接收所有告警;可在「告警规则」中按告警类型指定渠道 。</ Text >
568617 < div style = { { marginTop : 16 , display : "flex" , flexDirection : "column" , gap : 12 } } >
569618 < div >
570619 < div style = { { marginBottom : 4 } } > 名称</ div >
0 commit comments