This commit is contained in:
@@ -7,6 +7,7 @@ import type {
|
||||
DashboardOverview,
|
||||
ExecutionLog,
|
||||
LogPage,
|
||||
MessageGateway,
|
||||
Providers,
|
||||
Rule,
|
||||
RuleRequest,
|
||||
@@ -81,6 +82,12 @@ export const logApi = {
|
||||
clear: () => api.delete<string>('/logs/'),
|
||||
}
|
||||
|
||||
export const gatewayApi = {
|
||||
list: () => api.get<MessageGateway[]>('/gateways/'),
|
||||
create: (name: string) => api.post<MessageGateway>('/gateways/', { name }),
|
||||
remove: (id: number) => api.delete<string>(`/gateways/${id}`),
|
||||
}
|
||||
|
||||
export const adminApi = {
|
||||
getConfig: () => api.get<AdminConfigResponse>('/admin/config'),
|
||||
updateConfig: (body: AdminConfig) => api.put<AdminConfig>('/admin/config', body),
|
||||
|
||||
@@ -10,7 +10,8 @@ export interface UserInfo {
|
||||
create_at: number
|
||||
username: string
|
||||
is_admin: boolean
|
||||
dingtalk_config: string | null
|
||||
dingtalk_config: string | null
|
||||
api_key: string
|
||||
}
|
||||
|
||||
export interface Timer {
|
||||
@@ -40,6 +41,7 @@ export interface Rule {
|
||||
id: number
|
||||
uid: number
|
||||
timer_id: number
|
||||
gateway_id: number
|
||||
name: string
|
||||
account_id: number
|
||||
type: string
|
||||
@@ -48,9 +50,12 @@ export interface Rule {
|
||||
create_at: number
|
||||
}
|
||||
|
||||
export interface MessageGateway { id: number; uid: number; name: string; token: string; create_at: number }
|
||||
|
||||
export interface RuleRequest {
|
||||
name: string
|
||||
timer_id: number
|
||||
gateway_id?: number
|
||||
account_id: number
|
||||
type: string
|
||||
config_json: string
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Timer as TimerIcon,
|
||||
Operation,
|
||||
User,
|
||||
Connection,
|
||||
Document,
|
||||
Setting,
|
||||
SwitchButton,
|
||||
@@ -38,6 +39,7 @@ const menus = [
|
||||
{ path: '/timers', title: '定时器', icon: TimerIcon },
|
||||
{ path: '/rules', title: '规则', icon: Operation },
|
||||
{ path: '/accounts', title: '账号', icon: User },
|
||||
{ path: '/gateways', title: '消息网关', icon: Connection },
|
||||
{ path: '/logs', title: '执行日志', icon: Document },
|
||||
{ path: '/settings', title: '设置', icon: Setting },
|
||||
]
|
||||
|
||||
@@ -40,6 +40,7 @@ const router = createRouter({
|
||||
component: () => import('@/views/AccountsView.vue'),
|
||||
meta: { title: '账号' },
|
||||
},
|
||||
{ path: 'gateways', name: 'gateways', component: () => import('@/views/GatewaysView.vue'), meta: { title: '消息网关' } },
|
||||
{
|
||||
path: 'logs',
|
||||
name: 'logs',
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { CopyDocument, Delete, Link, Plus } from '@element-plus/icons-vue'
|
||||
import { gatewayApi, ruleApi } from '@/api'
|
||||
import type { MessageGateway, Rule } from '@/api/types'
|
||||
const gateways = ref<MessageGateway[]>([]), rules = ref<Rule[]>([]), name = ref(''), loading = ref(false), saving = ref(false)
|
||||
const origin = window.location.origin
|
||||
async function refresh() { loading.value = true; try { ;[gateways.value, rules.value] = await Promise.all([gatewayApi.list(), ruleApi.list()]) } catch { ElMessage.error('加载消息网关失败') } finally { loading.value = false } }
|
||||
async function create() { if (!name.value.trim()) { ElMessage.warning('请输入网关名称'); return }; saving.value = true; try { await gatewayApi.create(name.value.trim()); name.value = ''; await refresh(); ElMessage.success('消息网关已创建') } catch (e) { ElMessage.error(e instanceof Error ? e.message : '创建失败') } finally { saving.value = false } }
|
||||
async function remove(item: MessageGateway) { try { await ElMessageBox.confirm(`删除“${item.name}”?关联规则将无法继续通过此网关触发。`, '删除消息网关', { type: 'warning' }); await gatewayApi.remove(item.id); await refresh() } catch {} }
|
||||
async function copy(url: string) { await navigator.clipboard?.writeText(url); ElMessage.success('Webhook 地址已复制') }
|
||||
function endpoint(item: MessageGateway) { return `${origin}/api/v1/gateways/${item.token}/webhook` }
|
||||
function ruleCount(item: MessageGateway) { return rules.value.filter(r => r.gateway_id === item.id).length }
|
||||
onMounted(refresh)
|
||||
</script>
|
||||
<template><div class="gateway-page" v-loading="loading"><div class="page-header"><div><h2>消息网关</h2><div class="muted">创建入口,让外部系统通过 Webhook 触发通知规则</div></div><el-button type="primary" :icon="Plus">新建网关</el-button></div><el-card class="create-card"><div class="create-copy"><div class="create-icon"><el-icon :size="20"><Link /></el-icon></div><div><strong>新建 Webhook 入口</strong><div class="muted">创建后,在规则编辑页将通知规则关联到此网关</div></div></div><el-form class="create-form" @submit.prevent="create"><el-input v-model="name" size="large" placeholder="例如:生产环境告警" maxlength="64" clearable /><el-button type="primary" size="large" :loading="saving" @click="create">创建网关</el-button></el-form></el-card><div v-if="gateways.length" class="gateway-grid"><el-card v-for="item in gateways" :key="item.id" class="gateway-card"><div class="gateway-top"><div><div class="gateway-name">{{ item.name }}</div><div class="gateway-meta">{{ ruleCount(item) }} 条关联通知规则</div></div><el-button circle text type="danger" :icon="Delete" title="删除网关" @click="remove(item)" /></div><div class="endpoint-label">Webhook 地址</div><div class="endpoint"><code>{{ endpoint(item) }}</code><el-button text :icon="CopyDocument" title="复制地址" @click="copy(endpoint(item))" /></div><div class="payload-hint">POST JSON:<code>{ "message": "...", "title": "..." }</code></div></el-card></div><el-empty v-else-if="!loading" description="还没有消息网关" /></div></template>
|
||||
<style scoped>.create-card{margin-bottom:20px}.create-copy{display:flex;align-items:center;gap:12px;margin-bottom:16px}.create-copy strong{font-size:15px}.create-icon{width:40px;height:40px;display:grid;place-items:center;border-radius:10px;color:var(--gb-primary-deep);background:var(--el-color-primary-light-9)}.create-form{display:flex;gap:10px;max-width:620px}.create-form .el-input{flex:1}.gateway-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:16px}.gateway-card{min-width:0}.gateway-top{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:22px}.gateway-name{font-size:17px;font-weight:700}.gateway-meta,.endpoint-label,.payload-hint{color:var(--el-text-color-secondary);font-size:12px}.endpoint-label{margin-bottom:6px}.endpoint{display:flex;align-items:center;gap:6px;min-width:0;padding:10px 12px;border-radius:8px;background:var(--gb-bg)}.endpoint code{flex:1;min-width:0;overflow-wrap:anywhere;color:var(--el-text-color-primary);font-size:12px}.payload-hint{margin-top:12px}.payload-hint code{color:var(--el-text-color-primary)}@media(max-width:600px){.create-form{flex-direction:column}.gateway-grid{grid-template-columns:1fr}}</style>
|
||||
@@ -2,9 +2,9 @@
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, VideoPlay } from '@element-plus/icons-vue'
|
||||
import { accountApi, ruleApi, timerApi } from '@/api'
|
||||
import { accountApi, gatewayApi, ruleApi, timerApi } from '@/api'
|
||||
import { ApiError } from '@/api/client'
|
||||
import type { Account, Rule, Timer } from '@/api/types'
|
||||
import type { Account, MessageGateway, Rule, Timer } from '@/api/types'
|
||||
import { useMetaStore } from '@/stores/meta'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import ConfigForm from '@/components/ConfigForm.vue'
|
||||
@@ -16,6 +16,7 @@ const metaStore = useMetaStore()
|
||||
|
||||
const rules = ref<Rule[]>([])
|
||||
const timers = ref<Timer[]>([])
|
||||
const gateways = ref<MessageGateway[]>([])
|
||||
const accounts = ref<Account[]>([])
|
||||
const loading = ref(false)
|
||||
const testingId = ref<number | null>(null)
|
||||
@@ -30,6 +31,7 @@ const form = ref<{
|
||||
name: string
|
||||
type: string
|
||||
timer_id: number
|
||||
gateway_id: number
|
||||
account_id: number | undefined
|
||||
enabled: boolean
|
||||
config_json: string
|
||||
@@ -37,6 +39,7 @@ const form = ref<{
|
||||
name: '',
|
||||
type: '',
|
||||
timer_id: 0,
|
||||
gateway_id: 0,
|
||||
account_id: undefined,
|
||||
enabled: true,
|
||||
config_json: '',
|
||||
@@ -69,14 +72,16 @@ function accountName(id: number): string {
|
||||
async function refresh() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [ruleList, timerList, accountList] = await Promise.all([
|
||||
const [ruleList, timerList, accountList, gatewayList] = await Promise.all([
|
||||
ruleApi.list(),
|
||||
timerApi.list(),
|
||||
accountApi.list(),
|
||||
gatewayApi.list(),
|
||||
])
|
||||
rules.value = ruleList
|
||||
timers.value = timerList
|
||||
accounts.value = accountList
|
||||
gateways.value = gatewayList
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof ApiError ? error.message : '加载失败')
|
||||
} finally {
|
||||
@@ -85,7 +90,7 @@ async function refresh() {
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
if (timers.value.length === 0) {
|
||||
if (timers.value.length === 0 && gateways.value.length === 0) {
|
||||
ElMessage.warning('请先在“定时器”页面创建一个定时器')
|
||||
return
|
||||
}
|
||||
@@ -94,6 +99,7 @@ function openCreate() {
|
||||
name: '',
|
||||
type: metaStore.ruleMetas[0]?.type ?? '',
|
||||
timer_id: timers.value[0]?.id ?? 0,
|
||||
gateway_id: 0,
|
||||
account_id: undefined,
|
||||
enabled: true,
|
||||
config_json: '',
|
||||
@@ -107,6 +113,7 @@ function openEdit(rule: Rule) {
|
||||
name: rule.name,
|
||||
type: rule.type,
|
||||
timer_id: rule.timer_id,
|
||||
gateway_id: rule.gateway_id || 0,
|
||||
account_id: rule.account_id || undefined,
|
||||
enabled: rule.enabled,
|
||||
config_json: rule.config_json,
|
||||
@@ -133,6 +140,7 @@ async function save() {
|
||||
name: form.value.name,
|
||||
type: form.value.type,
|
||||
timer_id: form.value.timer_id,
|
||||
gateway_id: form.value.gateway_id,
|
||||
account_id: form.value.account_id ?? 0,
|
||||
enabled: form.value.enabled,
|
||||
config_json: form.value.config_json,
|
||||
@@ -162,6 +170,7 @@ async function toggleEnabled(rule: Rule) {
|
||||
name: rule.name,
|
||||
type: rule.type,
|
||||
timer_id: rule.timer_id,
|
||||
gateway_id: rule.gateway_id,
|
||||
account_id: rule.account_id,
|
||||
enabled: rule.enabled,
|
||||
config_json: rule.config_json,
|
||||
@@ -308,10 +317,14 @@ onMounted(async () => {
|
||||
</el-select>
|
||||
<div v-if="currentMeta?.description" class="muted">{{ currentMeta.description }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="关联定时器" required>
|
||||
<el-select v-model="form.timer_id" style="width: 100%">
|
||||
<el-form-item label="关联定时器/消息网关" required>
|
||||
<div class="source-hint muted">规则只能选择一种触发方式</div>
|
||||
<el-select v-model="form.timer_id" class="source-select" clearable placeholder="定时器触发">
|
||||
<el-option v-for="t in timers" :key="t.id" :label="t.name" :value="t.id" />
|
||||
</el-select>
|
||||
<el-select v-model="form.gateway_id" clearable placeholder="消息网关(可选)" style="width: 100%; margin-top: 8px" @change="form.gateway_id && (form.timer_id = 0)">
|
||||
<el-option v-for="g in gateways" :key="g.id" :label="g.name" :value="g.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="currentMeta?.account_type" label="关联账号" required>
|
||||
<el-select
|
||||
@@ -363,6 +376,9 @@ onMounted(async () => {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.source-select { width: 100%; margin-top: 8px; }
|
||||
.source-hint { margin-bottom: 2px; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header-tools {
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -14,6 +14,13 @@ const userStore = useUserStore()
|
||||
|
||||
const isAdmin = computed(() => userStore.user?.is_admin === true)
|
||||
|
||||
async function copyApiKey() {
|
||||
const key = userStore.user?.api_key
|
||||
if (!key) return
|
||||
await navigator.clipboard?.writeText(key)
|
||||
ElMessage.success('API Key 已复制')
|
||||
}
|
||||
|
||||
// ---- 系统配置(仅管理员) ----
|
||||
const adminLoading = ref(false)
|
||||
const adminSaving = ref(false)
|
||||
@@ -166,6 +173,13 @@ onMounted(async () => {
|
||||
<el-descriptions-item label="注册时间">
|
||||
{{ formatDateTime(userStore.user?.create_at ?? 0) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="API Key">
|
||||
<el-input :model-value="userStore.user?.api_key ?? ''" readonly style="max-width: 380px">
|
||||
<template #append>
|
||||
<el-button title="复制 API Key" @click="copyApiKey">复制</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user