@@ -58,8 +58,12 @@
< a-form-item label = "密送" field = "email_bcc" >
< a-form-item label = "密送" field = "email_bcc" >
< a-input-tag v-model = "form.email_bcc" placeholder="输入密送邮箱后回车,可选" allow -clear / >
< a-input-tag v-model = "form.email_bcc" placeholder="输入密送邮箱后回车,可选" allow -clear / >
< / a-form-item >
< / a-form-item >
< a-form-item label = "启用 TLS " field = "email_u se_tls " >
< a-form-item label = "连接安全方式 " field = "email_security_mode " >
< a-switch v-model = "form.email_u se_tls" / >
< a-select v-model = "form.email_security_mode" placeholder="请选择连接安全方式" @change="handleEmailSecurityModeChange" >
< a -option value = "none" > 普通 SMTP < / a-option >
< a-option value = "starttls" > STARTTLS < / a-option >
< a-option value = "tls" > TLS < / a-option >
< / a-select >
< / a-form-item >
< / a-form-item >
< / template >
< / template >
@@ -98,20 +102,66 @@
< template v-if = "form.type === 'in_app'" >
< template v-if = "form.type === 'in_app'" >
< a -form -item label = "站内消息目标" field = "in_app_targets" >
< a -form -item label = "站内消息目标" field = "in_app_targets" >
< a-input-tag v-model = "form.in_app_targets" placeholder="输入目标后回车,如 user:42" allow -clear / >
< a-select
v-model = "form.in_app_targets"
placeholder = "请选择接收用户"
multiple
allow -clear
allow -search
:filter-option = "false"
:loading = "userLoading"
@search ="handleUserSearch"
>
< a-option v-for = "user in userOptions" :key="user.id" :value="`user:${user.id}`" >
{{ user.name | | user.username | | ` 用户 $ { user.id } ` }} ( {{ user.username | | user.id }} )
< / a -option >
< / a-select >
< / a-form-item >
< a-form-item label = "同告警聚合窗口(秒)" field = "in_app_aggregation_window" >
< a-input-number v-model = "form.in_app_aggregation_window" :min="10" :max="3600" style="width: 100%" / >
< / a-form-item >
< / a-form-item >
< / template >
< / template >
< a-form-item label = "消息模板" field= "message_template" >
< a-form-item field = "message_template" >
< template # label >
< span class = "template-label" >
< span > 消息模板 < / span >
< a-popover position = "right" trigger = "hover" >
< icon-question-circle class = "template-help-icon" / >
< template # content >
< div class = "template-help-panel" >
< div class = "template-help-header" >
< strong > 可用变量 < / strong >
< span > 变量区分大小写 < / span >
< / div >
< table class = "template-help-table" >
< thead >
< tr >
< th > 变量 < / th >
< th > 说明 < / th >
< / tr >
< / thead >
< tbody >
< tr v-for = "item in messageTemplateVariables" :key="item.variable" >
< td > < code > { { item . variable } } < / code > < / td >
< td > { { item . description } } < / td >
< / tr >
< / tbody >
< / table >
< / div >
< / template >
< / a-popover >
< / span >
< / template >
< a-textarea
< a-textarea
v-model = "form.message_template"
v-model = "form.message_template"
placeholder = "请输入消息模板,支持变量:{{.RuleName}}、{{.Summary}}、{{.FiredAt}}、{{.Value}}、{{.Threshold}}等 "
placeholder = "请输入消息模板"
: auto -size = " { minRows : 3 , maxRows : 6 } "
: auto -size = " { minRows : 3 , maxRows : 6 } "
:max-length = "1000"
:max-length = "1000"
/ >
/ >
< / a-form-item >
< / a-form-item >
< a-row :gutter= "16" >
< a-row v-if = "form.type !== 'in_app'" :gutter= "16">
< a -col :span = "12" >
< a -col :span = "12" >
< a-form-item label = "限流次数" field = "rate_limit" >
< a-form-item label = "限流次数" field = "rate_limit" >
< a-input-number v-model = "form.rate_limit" placeholder="0表示不限制" :min="0" :max="10000" style="width: 100%" / >
< a-input-number v-model = "form.rate_limit" placeholder="0表示不限制" :min="0" :max="10000" style="width: 100%" / >
@@ -169,7 +219,7 @@
< / a-space >
< / a-space >
< / a-form-item >
< / a-form-item >
< a-row :gutter= "16" >
< a-row v-if = "form.type !== 'in_app'" :gutter= "16">
< a -col :span = "12" >
< a -col :span = "12" >
< a-form-item label = "重试次数" field = "retry_times" >
< a-form-item label = "重试次数" field = "retry_times" >
< a-input-number v-model = "form.retry_times" placeholder="发送失败重试次数" :min="0" :max="10" style="width: 100%" / >
< a-input-number v-model = "form.retry_times" placeholder="发送失败重试次数" :min="0" :max="10" style="width: 100%" / >
@@ -199,8 +249,11 @@
< script lang = "ts" setup >
< script lang = "ts" setup >
import { ref , watch , computed , onMounted } from 'vue'
import { ref , watch , computed , onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import { Message } from '@arco-design/web-vue'
import { IconQuestionCircle } from '@arco-design/web-vue/es/icon'
import { createNoticeChannel , updateNoticeChannel } from '@/api/ops/noticeChannel'
import { createNoticeChannel , updateNoticeChannel } from '@/api/ops/noticeChannel'
import { fetchAlertLevelOptions } from '@/api/ops/alertLevel'
import { fetchAlertLevelOptions } from '@/api/ops/alertLevel'
import { fetchUserList } from '@/api/ops/rbac2'
import type { UserItem } from '@/api/ops/rbac2'
interface NoticeChannel {
interface NoticeChannel {
id ? : number
id ? : number
@@ -224,7 +277,7 @@ interface NoticeChannel {
email _to ? : string [ ] | string
email _to ? : string [ ] | string
email _cc ? : string [ ] | string
email _cc ? : string [ ] | string
email _bcc ? : string [ ] | string
email _bcc ? : string [ ] | string
email _u se_tls ? : boolean
email _security _mode ? : 'none' | 'starttls' | 'tls'
sms _provider ? : string
sms _provider ? : string
sms _access _key _id ? : string
sms _access _key _id ? : string
sms _access _key _secret ? : string
sms _access _key _secret ? : string
@@ -232,6 +285,7 @@ interface NoticeChannel {
sms _template _code ? : string
sms _template _code ? : string
sms _phones ? : string [ ] | string
sms _phones ? : string [ ] | string
in _app _targets ? : string [ ] | string
in _app _targets ? : string [ ] | string
in _app _aggregation _window ? : number
}
}
interface Props {
interface Props {
@@ -239,6 +293,21 @@ interface Props {
channel : NoticeChannel | null
channel : NoticeChannel | null
}
}
const messageTemplateVariables = [
{ variable : '{{.AlertName}}' , description : '告警名称' } ,
{ variable : '{{.RuleName}}' , description : '规则名称' } ,
{ variable : '{{.Severity}}' , description : '告警级别' } ,
{ variable : '{{.Status}}' , description : '告警状态' } ,
{ variable : '{{.Summary}}' , description : '告警摘要' } ,
{ variable : '{{.Description}}' , description : '详细描述' } ,
{ variable : '{{.Value}}' , description : '当前值' } ,
{ variable : '{{.Threshold}}' , description : '阈值' } ,
{ variable : '{{.FiredAt}} / {{.StartsAt}}' , description : '触发时间' } ,
{ variable : '{{.ResolvedAt}}' , description : '恢复时间' } ,
{ variable : '{{.Duration}}' , description : '持续秒数' } ,
{ variable : '{{.Fingerprint}}' , description : '告警指纹' } ,
] as const
interface Emits {
interface Emits {
( e : 'update:visible' , value : boolean ) : void
( e : 'update:visible' , value : boolean ) : void
( e : 'success' ) : void
( e : 'success' ) : void
@@ -271,14 +340,14 @@ const form = ref({
enabled : true ,
enabled : true ,
description : '' ,
description : '' ,
email _smtp _host : '' ,
email _smtp _host : '' ,
email _smtp _port : 2 5,
email _smtp _port : 587 ,
email _username : '' ,
email _username : '' ,
email _password : '' ,
email _password : '' ,
email _from : '' ,
email _from : '' ,
email _to : [ ] as string [ ] ,
email _to : [ ] as string [ ] ,
email _cc : [ ] as string [ ] ,
email _cc : [ ] as string [ ] ,
email _bcc : [ ] as string [ ] ,
email _bcc : [ ] as string [ ] ,
email _u se_tls : false ,
email _security _mode : 'starttls' as 'none' | 'starttls' | 'tls' ,
sms _provider : '' ,
sms _provider : '' ,
sms _access _key _id : '' ,
sms _access _key _id : '' ,
sms _access _key _secret : '' ,
sms _access _key _secret : '' ,
@@ -286,6 +355,7 @@ const form = ref({
sms _template _code : '' ,
sms _template _code : '' ,
sms _phones : [ ] as string [ ] ,
sms _phones : [ ] as string [ ] ,
in _app _targets : [ ] as string [ ] ,
in _app _targets : [ ] as string [ ] ,
in _app _aggregation _window : 60 ,
} )
} )
// 是否为编辑模式
// 是否为编辑模式
@@ -293,6 +363,8 @@ const isEdit = computed(() => !!props.channel?.id)
const severityLoading = ref ( false )
const severityLoading = ref ( false )
const baseSeverityLevels = ref < any [ ] > ( [ ] )
const baseSeverityLevels = ref < any [ ] > ( [ ] )
const userLoading = ref ( false )
const userOptions = ref < UserItem [ ] > ( [ ] )
// 获取告警级别列表(支持 keyword 搜索)
// 获取告警级别列表(支持 keyword 搜索)
const fetchSeverityLevels = async ( keyword ? : string ) => {
const fetchSeverityLevels = async ( keyword ? : string ) => {
@@ -342,6 +414,55 @@ const handleSeveritySearch = (keyword: string) => {
} , 300 )
} , 300 )
}
}
const loadUserOptions = async ( keyword = '' ) => {
userLoading . value = true
try {
const response = await fetchUserList ( {
page : 1 ,
size : 50 ,
keyword : keyword || undefined ,
status : 1 ,
} )
const raw = response . details ? ? response . data
const list = Array . isArray ( raw ) ? raw : Array . isArray ( raw ? . data ) ? raw . data : [ ]
const selectedIDs = new Set (
form . value . in _app _targets
. map ( ( target ) => Number ( target . replace ( /^user:/ , '' ) ) )
. filter ( ( id ) => Number . isInteger ( id ) && id > 0 )
)
const merged = new Map < number , UserItem > ( )
selectedIDs . forEach ( ( id ) => merged . set ( id , { id , name : ` 用户 ${ id } ` } ) )
list . forEach ( ( user ) => {
const id = Number ( user . id )
if ( Number . isInteger ( id ) && id > 0 ) merged . set ( id , { ... user , id } )
} )
userOptions . value = Array . from ( merged . values ( ) )
} catch ( error ) {
userOptions . value = [ ]
console . error ( '获取用户列表失败:' , error )
} finally {
userLoading . value = false
}
}
let userSearchTimer : ReturnType < typeof setTimeout > | null = null
const handleUserSearch = ( keyword : string ) => {
if ( userSearchTimer ) clearTimeout ( userSearchTimer )
userSearchTimer = setTimeout ( ( ) => loadUserOptions ( keyword ) , 300 )
}
const handleEmailSecurityModeChange = ( mode : string ) => {
if ( mode === 'tls' ) {
form . value . email _smtp _port = 465
return
}
if ( mode === 'starttls' ) {
form . value . email _smtp _port = 587
return
}
form . value . email _smtp _port = 25
}
// 处理静默时间段变化
// 处理静默时间段变化
const handleQuietHoursChange = ( ) => {
const handleQuietHoursChange = ( ) => {
if ( quietHoursEnabled . value ) {
if ( quietHoursEnabled . value ) {
@@ -378,14 +499,14 @@ watch(
enabled : props . channel . enabled !== undefined ? props . channel . enabled : true ,
enabled : props . channel . enabled !== undefined ? props . channel . enabled : true ,
description : props . channel . description || '' ,
description : props . channel . description || '' ,
email _smtp _host : props . channel . email _smtp _host || '' ,
email _smtp _host : props . channel . email _smtp _host || '' ,
email _smtp _port : props . channel . email _smtp _port ? ? 2 5,
email _smtp _port : props . channel . email _smtp _port ? ? 587 ,
email _username : props . channel . email _username || '' ,
email _username : props . channel . email _username || '' ,
email _password : props . channel . email _password || '' ,
email _password : props . channel . email _password || '' ,
email _from : props . channel . email _from || '' ,
email _from : props . channel . email _from || '' ,
email _to : parseStringArray ( props . channel . email _to ) ,
email _to : parseStringArray ( props . channel . email _to ) ,
email _cc : parseStringArray ( props . channel . email _cc ) ,
email _cc : parseStringArray ( props . channel . email _cc ) ,
email _bcc : parseStringArray ( props . channel . email _bcc ) ,
email _bcc : parseStringArray ( props . channel . email _bcc ) ,
email _u se_tls : props . channel . email _u se_tls || fa lse ,
email _security _mode : props . channel . email _security _mode || 'startt ls' ,
sms _provider : props . channel . sms _provider || '' ,
sms _provider : props . channel . sms _provider || '' ,
sms _access _key _id : props . channel . sms _access _key _id || '' ,
sms _access _key _id : props . channel . sms _access _key _id || '' ,
sms _access _key _secret : props . channel . sms _access _key _secret || '' ,
sms _access _key _secret : props . channel . sms _access _key _secret || '' ,
@@ -393,6 +514,7 @@ watch(
sms _template _code : props . channel . sms _template _code || '' ,
sms _template _code : props . channel . sms _template _code || '' ,
sms _phones : parseStringArray ( props . channel . sms _phones ) ,
sms _phones : parseStringArray ( props . channel . sms _phones ) ,
in _app _targets : parseStringArray ( props . channel . in _app _targets ) ,
in _app _targets : parseStringArray ( props . channel . in _app _targets ) ,
in _app _aggregation _window : props . channel . in _app _aggregation _window ? ? 60 ,
}
}
// 解析静默时间段
// 解析静默时间段
@@ -424,14 +546,14 @@ watch(
enabled : true ,
enabled : true ,
description : '' ,
description : '' ,
email _smtp _host : '' ,
email _smtp _host : '' ,
email _smtp _port : 2 5,
email _smtp _port : 587 ,
email _username : '' ,
email _username : '' ,
email _password : '' ,
email _password : '' ,
email _from : '' ,
email _from : '' ,
email _to : [ ] ,
email _to : [ ] ,
email _cc : [ ] ,
email _cc : [ ] ,
email _bcc : [ ] ,
email _bcc : [ ] ,
email _u se_tls : fa lse ,
email _security _mode : 'startt ls' ,
sms _provider : '' ,
sms _provider : '' ,
sms _access _key _id : '' ,
sms _access _key _id : '' ,
sms _access _key _secret : '' ,
sms _access _key _secret : '' ,
@@ -439,6 +561,7 @@ watch(
sms _template _code : '' ,
sms _template _code : '' ,
sms _phones : [ ] ,
sms _phones : [ ] ,
in _app _targets : [ ] ,
in _app _targets : [ ] ,
in _app _aggregation _window : 60 ,
}
}
quietHoursEnabled . value = false
quietHoursEnabled . value = false
quietHoursStart . value = '22:00'
quietHoursStart . value = '22:00'
@@ -467,15 +590,17 @@ const handleOk = async () => {
type : form . value . type ,
type : form . value . type ,
config : form . value . config ,
config : form . value . config ,
message _template : form . value . message _template ,
message _template : form . value . message _template ,
rate _limit : form . value . rate _limit ,
rate _limit _window : form . value . rate _limit _window ,
severity _filter : Array . isArray ( form . value . severity _filter ) ? form . value . severity _filter . join ( ',' ) : form . value . severity _filter ,
severity _filter : Array . isArray ( form . value . severity _filter ) ? form . value . severity _filter . join ( ',' ) : form . value . severity _filter ,
quiet _hours : quietHoursPayload ,
quiet _hours : quietHoursPayload ,
retry _times : form . value . retry _times ,
retry _interval : form . value . retry _interval ,
enabled : form . value . enabled ,
enabled : form . value . enabled ,
description : form . value . description ,
description : form . value . description ,
}
}
if ( form . value . type !== 'in_app' ) {
data . rate _limit = form . value . rate _limit
data . rate _limit _window = form . value . rate _limit _window
data . retry _times = form . value . retry _times
data . retry _interval = form . value . retry _interval
}
appendChannelPayload ( data )
appendChannelPayload ( data )
let res
let res
@@ -523,7 +648,14 @@ const isMaskedSecret = (value?: string) => {
const validateChannelConfig = ( ) => {
const validateChannelConfig = ( ) => {
if ( form . value . type === 'email' ) {
if ( form . value . type === 'email' ) {
if ( ! form . value . email _smtp _host || ! form . value . email _smtp _port || ! form . value . email _username || ! form . value . email _from || ! form . value . email _to . length ) {
if (
! form . value . email _smtp _host ||
! form . value . email _smtp _port ||
! form . value . email _username ||
! form . value . email _from ||
! form . value . email _to . length ||
! form . value . email _security _mode
) {
return '请补齐邮件渠道必填字段'
return '请补齐邮件渠道必填字段'
}
}
if ( ! form . value . email _password ) return '请输入 SMTP 密码/授权码'
if ( ! form . value . email _password ) return '请输入 SMTP 密码/授权码'
@@ -540,8 +672,11 @@ const validateChannelConfig = () => {
}
}
if ( ! form . value . sms _access _key _secret ) return '请输入 AccessKey Secret'
if ( ! form . value . sms _access _key _secret ) return '请输入 AccessKey Secret'
}
}
if ( form . value . type === 'in_app' && ! form . value . in _app _targets . length ) {
if ( form . value . type === 'in_app' ) {
return '请至少配置一个站内消息目标'
if ( ! form . value . in _app _targets . length ) return '请至少配置一个站内消息目标'
if ( form . value . in _app _aggregation _window < 10 || form . value . in _app _aggregation _window > 3600 ) {
return '同告警聚合窗口须在 10~ 3600 秒之间'
}
}
}
return ''
return ''
}
}
@@ -558,7 +693,7 @@ const appendChannelPayload = (data: any) => {
data . email _to = form . value . email _to
data . email _to = form . value . email _to
data . email _cc = form . value . email _cc
data . email _cc = form . value . email _cc
data . email _bcc = form . value . email _bcc
data . email _bcc = form . value . email _bcc
data . email _u se_tls = form . value . email _u se_tls
data . email _security _mode = form . value . email _security _mode
}
}
if ( form . value . type === 'sms' ) {
if ( form . value . type === 'sms' ) {
data . sms _provider = form . value . sms _provider
data . sms _provider = form . value . sms _provider
@@ -572,6 +707,7 @@ const appendChannelPayload = (data: any) => {
}
}
if ( form . value . type === 'in_app' ) {
if ( form . value . type === 'in_app' ) {
data . in _app _targets = form . value . in _app _targets
data . in _app _targets = form . value . in _app _targets
data . in _app _aggregation _window = form . value . in _app _aggregation _window
}
}
}
}
@@ -589,6 +725,13 @@ const handleVisibleChange = (visible: boolean) => {
onMounted ( ( ) => {
onMounted ( ( ) => {
fetchSeverityLevels ( )
fetchSeverityLevels ( )
} )
} )
watch (
( ) => form . value . type ,
( type ) => {
if ( type === 'in_app' ) loadUserOptions ( )
}
)
< / script >
< / script >
< script lang = "ts" >
< script lang = "ts" >
@@ -597,4 +740,77 @@ export default {
}
}
< / script >
< / script >
< style scoped lang = "less" > < / style >
< style scoped lang = "less" >
. template - label {
display : inline - flex ;
gap : 6 px ;
align - items : center ;
}
. template - help - icon {
font - size : 14 px ;
color : var ( -- color - text - 3 ) ;
cursor : help ;
& : hover {
color : rgb ( var ( -- primary - 6 ) ) ;
}
}
. template - help - panel {
width : 460 px ;
}
. template - help - header {
display : flex ;
align - items : center ;
justify - content : space - between ;
margin - bottom : 10 px ;
span {
font - size : 12 px ;
color : var ( -- color - text - 3 ) ;
}
}
. template - help - table {
width : 100 % ;
overflow : hidden ;
border : 1 px solid var ( -- color - border - 2 ) ;
border - spacing : 0 ;
border - collapse : separate ;
border - radius : 4 px ;
th ,
td {
padding : 6 px 12 px ;
text - align : left ;
border - bottom : 1 px solid var ( -- color - border - 2 ) ;
}
th {
font - weight : 500 ;
color : var ( -- color - text - 2 ) ;
background : var ( -- color - fill - 2 ) ;
}
tbody tr : nth - child ( even ) {
background : var ( -- color - fill - 1 ) ;
}
tbody tr : last - child td {
border - bottom : 0 ;
}
th : first - child ,
td : first - child {
width : 68 % ;
border - right : 1 px solid var ( -- color - border - 2 ) ;
}
code {
font - family : Consolas , 'Courier New' , monospace ;
color : rgb ( var ( -- primary - 6 ) ) ;
}
}
< / style >