发布 0.1.0:通知渠道、告警规则与多项体验修复
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NCollapseTransition, NInput, NSwitch } from 'naive-ui'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
|
||||
import { testNotifyChannel, updateNotifyChannel } from '@/api/settings'
|
||||
import AppInputNumber from '@/components/AppInputNumber.vue'
|
||||
import FormField from '@/components/FormField.vue'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import type { NotifyChannelItem, NotifyChannelType } from '@/types/api'
|
||||
|
||||
const props = defineProps<{ item: NotifyChannelItem }>()
|
||||
const emit = defineEmits<{ saved: [NotifyChannelItem[]] }>()
|
||||
|
||||
const message = useToast()
|
||||
|
||||
/** 渠道展示元数据:图标字符 + 名称 + 一句话说明 + 密文字段文案 */
|
||||
const CHANNEL_META: Record<
|
||||
NotifyChannelType,
|
||||
{ icon: string; name: string; hint: string; secretLabel?: string; secretHint?: string }
|
||||
> = {
|
||||
webhook: {
|
||||
icon: 'W',
|
||||
name: 'Webhook',
|
||||
hint: '通用 HTTP POST,可对接飞书 / 钉钉 / Slack / 企业微信机器人',
|
||||
},
|
||||
ntfy: {
|
||||
icon: 'N',
|
||||
name: 'ntfy',
|
||||
hint: '开源推送服务,官方 ntfy.sh 或自建服务端',
|
||||
secretLabel: 'Access Token',
|
||||
secretHint: '服务端开启鉴权时填写;留空表示沿用已保存值',
|
||||
},
|
||||
bark: {
|
||||
icon: 'B',
|
||||
name: 'Bark',
|
||||
hint: 'iOS 推送,官方 api.day.app 或自建服务端',
|
||||
secretLabel: 'Device Key',
|
||||
secretHint: 'Bark App 内的设备 key;留空表示沿用已保存值',
|
||||
},
|
||||
smtp: {
|
||||
icon: 'M',
|
||||
name: 'SMTP 邮件',
|
||||
hint: '465 端口走隐式 TLS,其余端口自动 STARTTLS',
|
||||
secretLabel: '密码',
|
||||
secretHint: 'SMTP 授权码或密码;留空表示沿用已保存值',
|
||||
},
|
||||
}
|
||||
|
||||
/** Webhook body 模板预设(占位符 {{title}} / {{text}} 发送时替换并做 JSON 转义) */
|
||||
const WEBHOOK_PRESETS = [
|
||||
{ label: 'Slack', tpl: '{"text": "{{title}}\\n{{text}}"}' },
|
||||
{ label: '飞书', tpl: '{"msg_type": "text", "content": {"text": "{{title}}\\n{{text}}"}}' },
|
||||
{ label: '钉钉', tpl: '{"msgtype": "text", "text": {"content": "{{title}}\\n{{text}}"}}' },
|
||||
{ label: '企业微信', tpl: '{"msgtype": "text", "text": {"content": "{{title}}\\n{{text}}"}}' },
|
||||
]
|
||||
|
||||
const meta = computed(() => CHANNEL_META[props.item.type])
|
||||
const expanded = ref(false)
|
||||
const saving = ref(false)
|
||||
const testing = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
enabled: false,
|
||||
url: '',
|
||||
bodyTemplate: '',
|
||||
server: '',
|
||||
topic: '',
|
||||
host: '',
|
||||
port: 587 as number | null,
|
||||
username: '',
|
||||
from: '',
|
||||
to: '',
|
||||
secret: '',
|
||||
})
|
||||
|
||||
// 服务端数据到达/保存后回填;密文字段不回明文,输入框留空表示沿用
|
||||
watch(
|
||||
() => props.item,
|
||||
(it) => {
|
||||
form.enabled = it.enabled
|
||||
form.url = it.url ?? ''
|
||||
form.bodyTemplate = it.bodyTemplate ?? ''
|
||||
form.server = it.server ?? ''
|
||||
form.topic = it.topic ?? ''
|
||||
form.host = it.host ?? ''
|
||||
form.port = it.port || 587
|
||||
form.username = it.username ?? ''
|
||||
form.from = it.from ?? ''
|
||||
form.to = it.to ?? ''
|
||||
form.secret = ''
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const secretPlaceholder = computed(() =>
|
||||
props.item.secretSet
|
||||
? `已配置${props.item.secretTail ? `(尾号 ${props.item.secretTail})` : ''},留空沿用`
|
||||
: '',
|
||||
)
|
||||
|
||||
async function save() {
|
||||
saving.value = true
|
||||
try {
|
||||
const secret = form.secret.trim()
|
||||
const items = await updateNotifyChannel(props.item.type, {
|
||||
enabled: form.enabled,
|
||||
url: form.url.trim(),
|
||||
bodyTemplate: form.bodyTemplate,
|
||||
server: form.server.trim(),
|
||||
topic: form.topic.trim(),
|
||||
host: form.host.trim(),
|
||||
port: form.port ?? 0,
|
||||
username: form.username.trim(),
|
||||
from: form.from.trim(),
|
||||
to: form.to.trim(),
|
||||
// 留空表示沿用已保存的密文字段
|
||||
...(secret ? { secret } : {}),
|
||||
})
|
||||
message.success(`${meta.value.name} 配置已保存`)
|
||||
form.secret = ''
|
||||
emit('saved', items)
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function sendTest() {
|
||||
testing.value = true
|
||||
try {
|
||||
await testNotifyChannel(props.item.type)
|
||||
message.success(`测试消息已发送,请在 ${meta.value.name} 侧查收`)
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '发送失败')
|
||||
} finally {
|
||||
testing.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="border-b border-line-soft last:border-b-0">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full cursor-pointer items-center gap-3 px-4.5 py-3.5 text-left"
|
||||
@click="expanded = !expanded"
|
||||
>
|
||||
<span
|
||||
class="flex h-8.5 w-8.5 flex-none items-center justify-center rounded-full bg-wash text-[15px] font-bold text-ink-2"
|
||||
>
|
||||
{{ meta.icon }}
|
||||
</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-[13px] font-medium">{{ meta.name }}</span>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-full bg-wash px-2 py-px text-[11.5px] text-ink-2"
|
||||
>
|
||||
<span
|
||||
class="h-1.5 w-1.5 rounded-full"
|
||||
:class="item.enabled ? 'bg-ok' : 'bg-ink-3/50'"
|
||||
/>
|
||||
{{ item.enabled ? '已启用' : '未启用' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-0.5 text-xs text-ink-3">{{ meta.hint }}</div>
|
||||
</div>
|
||||
<span class="text-xs text-ink-3">{{ expanded ? '收起' : '配置' }}</span>
|
||||
</button>
|
||||
|
||||
<NCollapseTransition :show="expanded">
|
||||
<div class="px-4.5 pb-3.5">
|
||||
<div class="mb-3 flex items-center justify-between rounded-lg bg-wash px-3 py-2">
|
||||
<span class="text-[12.5px]">启用该渠道</span>
|
||||
<NSwitch v-model:value="form.enabled" size="small" />
|
||||
</div>
|
||||
|
||||
<!-- Webhook -->
|
||||
<template v-if="item.type === 'webhook'">
|
||||
<FormField label="Webhook URL" hint="机器人/集成的完整回调地址,含鉴权参数">
|
||||
<NInput v-model:value="form.url" placeholder="https://open.feishu.cn/open-apis/bot/v2/hook/…" />
|
||||
</FormField>
|
||||
<FormField
|
||||
label="Body 模板"
|
||||
hint="发送时 {{title}} / {{text}} 按 JSON 字符串转义后替换;留空按 Slack 形状发送"
|
||||
>
|
||||
<div class="mb-1.5 flex flex-wrap gap-1.5">
|
||||
<NButton
|
||||
v-for="p in WEBHOOK_PRESETS"
|
||||
:key="p.label"
|
||||
size="tiny"
|
||||
quaternary
|
||||
type="primary"
|
||||
@click="form.bodyTemplate = p.tpl"
|
||||
>
|
||||
{{ p.label }}
|
||||
</NButton>
|
||||
</div>
|
||||
<NInput
|
||||
v-model:value="form.bodyTemplate"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 2, maxRows: 5 }"
|
||||
class="mono"
|
||||
:placeholder="WEBHOOK_PRESETS[0].tpl"
|
||||
/>
|
||||
</FormField>
|
||||
</template>
|
||||
|
||||
<!-- ntfy -->
|
||||
<template v-else-if="item.type === 'ntfy'">
|
||||
<div class="grid grid-cols-2 gap-3 max-md:grid-cols-1">
|
||||
<FormField label="服务端" hint="留空用官方 ntfy.sh">
|
||||
<NInput v-model:value="form.server" placeholder="https://ntfy.sh" />
|
||||
</FormField>
|
||||
<FormField label="Topic" hint="订阅的主题名">
|
||||
<NInput v-model:value="form.topic" placeholder="oci-portal" />
|
||||
</FormField>
|
||||
</div>
|
||||
<FormField :label="meta.secretLabel!" :hint="meta.secretHint">
|
||||
<NInput
|
||||
v-model:value="form.secret"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
:placeholder="secretPlaceholder || 'tk_…(可选)'"
|
||||
/>
|
||||
</FormField>
|
||||
</template>
|
||||
|
||||
<!-- Bark -->
|
||||
<template v-else-if="item.type === 'bark'">
|
||||
<FormField label="服务端" hint="留空用官方 api.day.app">
|
||||
<NInput v-model:value="form.server" placeholder="https://api.day.app" />
|
||||
</FormField>
|
||||
<FormField :label="meta.secretLabel!" :hint="meta.secretHint">
|
||||
<NInput
|
||||
v-model:value="form.secret"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
:placeholder="secretPlaceholder || 'Bark App 内复制'"
|
||||
/>
|
||||
</FormField>
|
||||
</template>
|
||||
|
||||
<!-- SMTP -->
|
||||
<template v-else>
|
||||
<div class="grid grid-cols-2 gap-3 max-md:grid-cols-1">
|
||||
<FormField label="主机">
|
||||
<NInput v-model:value="form.host" placeholder="smtp.example.com" />
|
||||
</FormField>
|
||||
<FormField label="端口" hint="465 隐式 TLS,587 STARTTLS">
|
||||
<AppInputNumber v-model:value="form.port" :min="1" :max="65535" class="!w-full" />
|
||||
</FormField>
|
||||
<FormField label="用户名" hint="留空表示免认证">
|
||||
<NInput v-model:value="form.username" placeholder="user@example.com" />
|
||||
</FormField>
|
||||
<FormField :label="meta.secretLabel!" :hint="meta.secretHint">
|
||||
<NInput
|
||||
v-model:value="form.secret"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
:placeholder="secretPlaceholder || 'SMTP 授权码'"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="发件人">
|
||||
<NInput v-model:value="form.from" placeholder="noreply@example.com" />
|
||||
</FormField>
|
||||
<FormField label="收件人">
|
||||
<NInput v-model:value="form.to" placeholder="me@example.com" />
|
||||
</FormField>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="mt-1 flex items-center gap-2">
|
||||
<NButton size="small" type="primary" :loading="saving" @click="save">保存</NButton>
|
||||
<NButton size="small" :loading="testing" @click="sendTest">发送测试消息</NButton>
|
||||
<span class="text-xs text-ink-3">测试使用已保存的配置,修改后请先保存</span>
|
||||
</div>
|
||||
</div>
|
||||
</NCollapseTransition>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user