This commit is contained in:
@@ -6,11 +6,14 @@ import type {
|
||||
AdminConfigResponse,
|
||||
DashboardOverview,
|
||||
ExecutionLog,
|
||||
GatewayRule,
|
||||
GatewayRuleRequest,
|
||||
LogPage,
|
||||
MessageGateway,
|
||||
Providers,
|
||||
Rule,
|
||||
RuleRequest,
|
||||
AccountDeleteImpact,
|
||||
SiteInfo,
|
||||
Timer,
|
||||
TimerRequest,
|
||||
@@ -66,13 +69,22 @@ export const ruleApi = {
|
||||
remove: (id: number) => api.delete<string>(`/rules/${id}`),
|
||||
}
|
||||
|
||||
export const gatewayRuleApi = {
|
||||
list: (gatewayId?: number) =>
|
||||
api.get<GatewayRule[]>('/gateway-rules/', gatewayId ? { gateway_id: gatewayId } : undefined),
|
||||
create: (body: GatewayRuleRequest) => api.post<GatewayRule>('/gateway-rules/', body),
|
||||
update: (id: number, body: GatewayRuleRequest) => api.put<GatewayRule>(`/gateway-rules/${id}`, body),
|
||||
test: (id: number) => api.post<string>(`/gateway-rules/${id}/test`),
|
||||
remove: (id: number) => api.delete<string>(`/gateway-rules/${id}`),
|
||||
}
|
||||
|
||||
export const accountApi = {
|
||||
list: (type?: string) =>
|
||||
api.get<Account[]>('/accounts/', type ? { type } : undefined),
|
||||
create: (body: AccountRequest) => api.post<Account>('/accounts/', body),
|
||||
update: (id: number, body: AccountRequest) => api.put<Account>(`/accounts/${id}`, body),
|
||||
test: (id: number) => api.post<string>(`/accounts/${id}/test`),
|
||||
checkDelete: (id: number) => api.get<Rule[]>(`/accounts/${id}/check`),
|
||||
checkDelete: (id: number) => api.get<AccountDeleteImpact>(`/accounts/${id}/check`),
|
||||
remove: (id: number) => api.delete<string>(`/accounts/${id}`),
|
||||
}
|
||||
|
||||
@@ -84,7 +96,7 @@ export const logApi = {
|
||||
|
||||
export const gatewayApi = {
|
||||
list: () => api.get<MessageGateway[]>('/gateways/'),
|
||||
create: (name: string) => api.post<MessageGateway>('/gateways/', { name }),
|
||||
create: (name: string, type: string) => api.post<MessageGateway>('/gateways/', { name, type }),
|
||||
remove: (id: number) => api.delete<string>(`/gateways/${id}`),
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,18 @@ export interface Rule {
|
||||
id: number
|
||||
uid: number
|
||||
timer_id: number
|
||||
name: string
|
||||
account_id: number
|
||||
type: string
|
||||
config_json: string
|
||||
enabled: boolean
|
||||
create_at: number
|
||||
}
|
||||
|
||||
/** 消息网关规则:由外部系统投递到网关的消息触发,与定时器规则分开存储 */
|
||||
export interface GatewayRule {
|
||||
id: number
|
||||
uid: number
|
||||
gateway_id: number
|
||||
name: string
|
||||
account_id: number
|
||||
@@ -50,12 +62,27 @@ export interface Rule {
|
||||
create_at: number
|
||||
}
|
||||
|
||||
export interface MessageGateway { id: number; uid: number; name: string; token: string; create_at: number }
|
||||
export interface MessageGateway {
|
||||
id: number
|
||||
uid: number
|
||||
name: string
|
||||
type: 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
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export interface GatewayRuleRequest {
|
||||
name: string
|
||||
gateway_id: number
|
||||
account_id: number
|
||||
type: string
|
||||
config_json: string
|
||||
@@ -94,6 +121,8 @@ export interface MetaField {
|
||||
placeholder?: string
|
||||
help?: string
|
||||
secret?: boolean
|
||||
/** 由消息网关投递的请求内容提供,网关规则页面不需要用户填写 */
|
||||
gateway_message?: boolean
|
||||
default?: unknown
|
||||
}
|
||||
|
||||
@@ -115,9 +144,18 @@ export interface RuleMeta {
|
||||
fields: MetaField[]
|
||||
}
|
||||
|
||||
export interface GatewayMeta {
|
||||
type: string
|
||||
label: string
|
||||
description?: string
|
||||
docs?: string
|
||||
payload_hint?: string
|
||||
}
|
||||
|
||||
export interface Providers {
|
||||
accounts: AccountMeta[]
|
||||
rules: RuleMeta[]
|
||||
gateways: GatewayMeta[]
|
||||
}
|
||||
|
||||
export interface ExecutionLog {
|
||||
@@ -127,7 +165,9 @@ export interface ExecutionLog {
|
||||
rule_name: string
|
||||
rule_type: string
|
||||
timer_id: number
|
||||
trigger: 'timer' | 'manual' | 'remind'
|
||||
gateway_id: number
|
||||
gateway_rule_id: number
|
||||
trigger: 'timer' | 'manual' | 'remind' | 'gateway'
|
||||
success: boolean
|
||||
message: string
|
||||
create_at: number
|
||||
@@ -145,6 +185,8 @@ export interface DashboardOverview {
|
||||
enabled_timers: number
|
||||
triggered_count: number
|
||||
rule_count: number
|
||||
gateway_count: number
|
||||
gateway_rule_count: number
|
||||
account_count: number
|
||||
server_time: number
|
||||
recent_logs: ExecutionLog[]
|
||||
@@ -152,6 +194,12 @@ export interface DashboardOverview {
|
||||
urgent_seconds_left?: number
|
||||
}
|
||||
|
||||
/** 删除账号时会被一并删除的规则 */
|
||||
export interface AccountDeleteImpact {
|
||||
rules: Rule[]
|
||||
gateway_rules: GatewayRule[]
|
||||
}
|
||||
|
||||
export interface SiteInfo {
|
||||
name: string
|
||||
enable_registry: boolean
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Operation,
|
||||
User,
|
||||
Connection,
|
||||
Promotion,
|
||||
Document,
|
||||
Setting,
|
||||
SwitchButton,
|
||||
@@ -40,6 +41,7 @@ const menus = [
|
||||
{ path: '/rules', title: '规则', icon: Operation },
|
||||
{ path: '/accounts', title: '账号', icon: User },
|
||||
{ path: '/gateways', title: '消息网关', icon: Connection },
|
||||
{ path: '/gateway-rules', title: '网关规则', icon: Promotion },
|
||||
{ path: '/logs', title: '执行日志', icon: Document },
|
||||
{ path: '/settings', title: '设置', icon: Setting },
|
||||
]
|
||||
|
||||
@@ -41,6 +41,12 @@ const router = createRouter({
|
||||
meta: { title: '账号' },
|
||||
},
|
||||
{ path: 'gateways', name: 'gateways', component: () => import('@/views/GatewaysView.vue'), meta: { title: '消息网关' } },
|
||||
{
|
||||
path: 'gateway-rules',
|
||||
name: 'gateway-rules',
|
||||
component: () => import('@/views/GatewayRulesView.vue'),
|
||||
meta: { title: '网关规则' },
|
||||
},
|
||||
{
|
||||
path: 'logs',
|
||||
name: 'logs',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { providerApi } from '@/api'
|
||||
import type { AccountMeta, Providers, RuleMeta } from '@/api/types'
|
||||
import type { AccountMeta, GatewayMeta, Providers, RuleMeta } from '@/api/types'
|
||||
|
||||
/** 驱动元数据:账号/规则类型的表单描述,登录后拉取一次全局共享 */
|
||||
export const useMetaStore = defineStore('meta', () => {
|
||||
@@ -16,6 +16,7 @@ export const useMetaStore = defineStore('meta', () => {
|
||||
|
||||
const accountMetas = computed<AccountMeta[]>(() => providers.value?.accounts ?? [])
|
||||
const ruleMetas = computed<RuleMeta[]>(() => providers.value?.rules ?? [])
|
||||
const gatewayMetas = computed<GatewayMeta[]>(() => providers.value?.gateways ?? [])
|
||||
|
||||
function accountMeta(type: string): AccountMeta | undefined {
|
||||
return accountMetas.value.find((m) => m.type === type)
|
||||
@@ -33,14 +34,25 @@ export const useMetaStore = defineStore('meta', () => {
|
||||
return ruleMeta(type)?.label ?? type
|
||||
}
|
||||
|
||||
function gatewayMeta(type: string): GatewayMeta | undefined {
|
||||
return gatewayMetas.value.find((m) => m.type === type)
|
||||
}
|
||||
|
||||
function gatewayLabel(type: string): string {
|
||||
return gatewayMeta(type)?.label ?? type
|
||||
}
|
||||
|
||||
return {
|
||||
providers,
|
||||
accountMetas,
|
||||
ruleMetas,
|
||||
gatewayMetas,
|
||||
ensureLoaded,
|
||||
accountMeta,
|
||||
ruleMeta,
|
||||
gatewayMeta,
|
||||
accountLabel,
|
||||
ruleLabel,
|
||||
gatewayLabel,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -94,9 +94,10 @@ async function test(account: Account) {
|
||||
async function remove(account: Account) {
|
||||
try {
|
||||
const affected = await accountApi.checkDelete(account.id)
|
||||
const affectedRules = [...affected.rules, ...affected.gateway_rules]
|
||||
const warning =
|
||||
affected.length > 0
|
||||
? `删除后,使用该账号的 ${affected.length} 条规则也会被一并删除:${affected
|
||||
affectedRules.length > 0
|
||||
? `删除后,使用该账号的 ${affectedRules.length} 条规则也会被一并删除:${affectedRules
|
||||
.map((r) => r.name)
|
||||
.join('、')}`
|
||||
: '没有规则使用该账号。'
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Timer as TimerIcon,
|
||||
Operation,
|
||||
User,
|
||||
Connection,
|
||||
Warning,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { dashboardApi, timerApi } from '@/api'
|
||||
@@ -73,13 +74,22 @@ const stats = computed(() => [
|
||||
},
|
||||
{
|
||||
label: '规则',
|
||||
sub: '触发时执行的动作',
|
||||
sub: `定时器规则 ${overview.value?.rule_count ?? 0} 条`,
|
||||
value: overview.value?.rule_count,
|
||||
icon: Operation,
|
||||
color: '#6366f1',
|
||||
bg: 'rgb(99 102 241 / 0.12)',
|
||||
to: '/rules',
|
||||
},
|
||||
{
|
||||
label: '消息网关',
|
||||
sub: `网关规则 ${overview.value?.gateway_rule_count ?? 0} 条`,
|
||||
value: overview.value?.gateway_count,
|
||||
icon: Connection,
|
||||
color: '#0ea5e9',
|
||||
bg: 'rgb(14 165 233 / 0.12)',
|
||||
to: '/gateways',
|
||||
},
|
||||
{
|
||||
label: '账号',
|
||||
sub: '第三方凭据',
|
||||
|
||||
@@ -0,0 +1,436 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, VideoPlay } from '@element-plus/icons-vue'
|
||||
import { accountApi, gatewayApi, gatewayRuleApi } from '@/api'
|
||||
import { ApiError } from '@/api/client'
|
||||
import type { Account, GatewayRule, MessageGateway } from '@/api/types'
|
||||
import { useMetaStore } from '@/stores/meta'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import ConfigForm from '@/components/ConfigForm.vue'
|
||||
import { useIsMobile } from '@/composables/useBreakpoint'
|
||||
|
||||
const isMobile = useIsMobile()
|
||||
const route = useRoute()
|
||||
|
||||
const metaStore = useMetaStore()
|
||||
|
||||
const rules = ref<GatewayRule[]>([])
|
||||
const gateways = ref<MessageGateway[]>([])
|
||||
const accounts = ref<Account[]>([])
|
||||
const loading = ref(false)
|
||||
const testingId = ref<number | null>(null)
|
||||
const filterGatewayId = ref<number | undefined>(undefined)
|
||||
|
||||
// ---- 创建 / 编辑对话框 ----
|
||||
const dialogVisible = ref(false)
|
||||
const saving = ref(false)
|
||||
const editingId = ref<number | null>(null)
|
||||
|
||||
const form = ref<{
|
||||
name: string
|
||||
type: string
|
||||
gateway_id: number
|
||||
account_id: number | undefined
|
||||
enabled: boolean
|
||||
config_json: string
|
||||
}>({
|
||||
name: '',
|
||||
type: '',
|
||||
gateway_id: 0,
|
||||
account_id: undefined,
|
||||
enabled: true,
|
||||
config_json: '',
|
||||
})
|
||||
|
||||
const dialogTitle = computed(() => (editingId.value === null ? '创建网关规则' : '编辑网关规则'))
|
||||
const currentMeta = computed(() => metaStore.ruleMeta(form.value.type))
|
||||
|
||||
/** 由投递请求提供的字段,页面上不让用户填 */
|
||||
const gatewayMessageFields = computed(() =>
|
||||
(currentMeta.value?.fields ?? []).filter((f) => f.gateway_message),
|
||||
)
|
||||
|
||||
/** 需要用户填写的字段 */
|
||||
const configFields = computed(() => (currentMeta.value?.fields ?? []).filter((f) => !f.gateway_message))
|
||||
|
||||
/** 当前规则类型可用的账号(按类型过滤) */
|
||||
const availableAccounts = computed(() => {
|
||||
const requiredType = currentMeta.value?.account_type
|
||||
if (!requiredType) return []
|
||||
return accounts.value.filter((a) => a.type === requiredType)
|
||||
})
|
||||
|
||||
const filteredRules = computed(() => {
|
||||
if (!filterGatewayId.value) return rules.value
|
||||
return rules.value.filter((r) => r.gateway_id === filterGatewayId.value)
|
||||
})
|
||||
|
||||
function gatewayName(id: number): string {
|
||||
return gateways.value.find((g) => g.id === id)?.name ?? `#${id}`
|
||||
}
|
||||
|
||||
function accountName(id: number): string {
|
||||
if (!id) return '-'
|
||||
return accounts.value.find((a) => a.id === id)?.name ?? `#${id}`
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [ruleList, gatewayList, accountList] = await Promise.all([
|
||||
gatewayRuleApi.list(),
|
||||
gatewayApi.list(),
|
||||
accountApi.list(),
|
||||
])
|
||||
rules.value = ruleList
|
||||
gateways.value = gatewayList
|
||||
accounts.value = accountList
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof ApiError ? error.message : '加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
if (gateways.value.length === 0) {
|
||||
ElMessage.warning('请先在“消息网关”页面创建一个网关')
|
||||
return
|
||||
}
|
||||
editingId.value = null
|
||||
form.value = {
|
||||
name: '',
|
||||
type: metaStore.ruleMetas[0]?.type ?? '',
|
||||
gateway_id: filterGatewayId.value ?? gateways.value[0]?.id ?? 0,
|
||||
account_id: undefined,
|
||||
enabled: true,
|
||||
config_json: '',
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEdit(rule: GatewayRule) {
|
||||
editingId.value = rule.id
|
||||
form.value = {
|
||||
name: rule.name,
|
||||
type: rule.type,
|
||||
gateway_id: rule.gateway_id,
|
||||
account_id: rule.account_id || undefined,
|
||||
enabled: rule.enabled,
|
||||
config_json: rule.config_json,
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function onTypeChange() {
|
||||
form.value.config_json = ''
|
||||
form.value.account_id = undefined
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!form.value.name) {
|
||||
ElMessage.warning('请填写规则名称')
|
||||
return
|
||||
}
|
||||
if (!form.value.gateway_id) {
|
||||
ElMessage.warning('请选择消息网关')
|
||||
return
|
||||
}
|
||||
if (currentMeta.value?.account_type && !form.value.account_id) {
|
||||
ElMessage.warning(`该规则类型需要关联一个「${metaStore.accountLabel(currentMeta.value.account_type)}」账号`)
|
||||
return
|
||||
}
|
||||
|
||||
const body = {
|
||||
name: form.value.name,
|
||||
type: form.value.type,
|
||||
gateway_id: form.value.gateway_id,
|
||||
account_id: form.value.account_id ?? 0,
|
||||
enabled: form.value.enabled,
|
||||
config_json: form.value.config_json,
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
if (editingId.value === null) {
|
||||
await gatewayRuleApi.create(body)
|
||||
ElMessage.success('网关规则已创建')
|
||||
} else {
|
||||
await gatewayRuleApi.update(editingId.value, body)
|
||||
ElMessage.success('网关规则已更新')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
await refresh()
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof ApiError ? error.message : '保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleEnabled(rule: GatewayRule) {
|
||||
try {
|
||||
await gatewayRuleApi.update(rule.id, {
|
||||
name: rule.name,
|
||||
type: rule.type,
|
||||
gateway_id: rule.gateway_id,
|
||||
account_id: rule.account_id,
|
||||
enabled: rule.enabled,
|
||||
config_json: rule.config_json,
|
||||
})
|
||||
ElMessage.success(rule.enabled ? '已启用' : '已停用')
|
||||
} catch (error) {
|
||||
rule.enabled = !rule.enabled
|
||||
ElMessage.error(error instanceof ApiError ? error.message : '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function test(rule: GatewayRule) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'测试会按规则里保存的内容真实执行一次(发送消息 / 修改仓库等),确定继续吗?',
|
||||
`测试网关规则「${rule.name}」`,
|
||||
{ type: 'warning', confirmButtonText: '执行', cancelButtonText: '取消' },
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
testingId.value = rule.id
|
||||
try {
|
||||
await gatewayRuleApi.test(rule.id)
|
||||
ElMessage.success('执行成功,详情见执行日志')
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof ApiError ? error.message : '执行失败')
|
||||
} finally {
|
||||
testingId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(rule: GatewayRule) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除网关规则「${rule.name}」吗?`, '删除网关规则', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '删除',
|
||||
confirmButtonClass: 'el-button--danger',
|
||||
cancelButtonText: '取消',
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await gatewayRuleApi.remove(rule.id)
|
||||
ElMessage.success('已删除')
|
||||
await refresh()
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof ApiError ? error.message : '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 从「消息网关」页面跳转过来时可以带上要筛选的网关
|
||||
const fromQuery = Number(route.query.gateway_id)
|
||||
if (fromQuery) filterGatewayId.value = fromQuery
|
||||
|
||||
await metaStore.ensureLoaded()
|
||||
await refresh()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-loading="loading">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>网关规则</h2>
|
||||
<div class="muted">外部系统往消息网关投递消息时要执行的动作,只需配置「发给谁」,内容来自请求</div>
|
||||
</div>
|
||||
<div class="header-tools">
|
||||
<el-select
|
||||
v-model="filterGatewayId"
|
||||
placeholder="按消息网关筛选"
|
||||
clearable
|
||||
class="filter-select"
|
||||
>
|
||||
<el-option v-for="g in gateways" :key="g.id" :label="g.name" :value="g.id" />
|
||||
</el-select>
|
||||
<el-button type="primary" :icon="Plus" @click="openCreate">创建网关规则</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-alert class="rule-tip" type="info" :closable="false" show-icon>
|
||||
<template #title>消息内容由投递请求提供</template>
|
||||
投递时把请求里的 <code>message</code> / <code>title</code> 填进规则的消息字段,因此规则里不需要预先写好内容。
|
||||
</el-alert>
|
||||
|
||||
<el-empty v-if="!loading && filteredRules.length === 0" description="还没有网关规则">
|
||||
<el-button type="primary" @click="openCreate">创建网关规则</el-button>
|
||||
</el-empty>
|
||||
|
||||
<el-card v-else class="table-card gb-rise">
|
||||
<el-table :data="filteredRules" :size="isMobile ? 'small' : 'default'">
|
||||
<el-table-column prop="name" label="名称" min-width="110" />
|
||||
<el-table-column label="类型" :width="isMobile ? 120 : 160">
|
||||
<template #default="{ row }">
|
||||
<el-tag>{{ metaStore.ruleLabel(row.type) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="!isMobile" label="消息网关" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" type="info" effect="light">{{ gatewayName(row.gateway_id) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="!isMobile" label="账号" min-width="120">
|
||||
<template #default="{ row }">{{ accountName(row.account_id) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="!isMobile" label="创建时间" width="170">
|
||||
<template #default="{ row }">{{ formatDateTime(row.create_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="启用" :width="isMobile ? 60 : 80">
|
||||
<template #default="{ row }">
|
||||
<el-switch v-model="row.enabled" size="small" @change="toggleEnabled(row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" :width="isMobile ? 200 : 240" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
size="small"
|
||||
:icon="VideoPlay"
|
||||
:loading="testingId === row.id"
|
||||
@click="test(row)"
|
||||
>
|
||||
测试
|
||||
</el-button>
|
||||
<el-button size="small" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" plain @click="remove(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 创建/编辑对话框 -->
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="560px">
|
||||
<el-form :label-width="isMobile ? 'auto' : '110px'" :label-position="isMobile ? 'top' : 'right'">
|
||||
<el-form-item label="规则名称" required>
|
||||
<el-input v-model="form.name" placeholder="给这条规则起个名字" maxlength="64" />
|
||||
</el-form-item>
|
||||
<el-form-item label="消息网关" required>
|
||||
<el-select v-model="form.gateway_id" style="width: 100%" placeholder="选择消息网关">
|
||||
<el-option v-for="g in gateways" :key="g.id" :label="g.name" :value="g.id" />
|
||||
</el-select>
|
||||
<div class="muted">外部系统向这个网关投递消息时触发本规则</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="规则类型" required>
|
||||
<el-select
|
||||
v-model="form.type"
|
||||
:disabled="editingId !== null"
|
||||
style="width: 100%"
|
||||
@change="onTypeChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="m in metaStore.ruleMetas"
|
||||
:key="m.type"
|
||||
:label="m.label"
|
||||
:value="m.type"
|
||||
/>
|
||||
</el-select>
|
||||
<div v-if="currentMeta?.description" class="muted">{{ currentMeta.description }}</div>
|
||||
<div v-if="currentMeta && !gatewayMessageFields.length" class="muted">
|
||||
该规则类型没有消息字段,投递只作为触发,按下方的配置执行
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="currentMeta?.account_type" label="关联账号" required>
|
||||
<el-select
|
||||
v-model="form.account_id"
|
||||
style="width: 100%"
|
||||
:placeholder="availableAccounts.length ? '选择账号' : '没有可用账号,请先到“账号”页面添加'"
|
||||
>
|
||||
<el-option
|
||||
v-for="a in availableAccounts"
|
||||
:key="a.id"
|
||||
:label="a.name"
|
||||
:value="a.id"
|
||||
/>
|
||||
</el-select>
|
||||
<div class="muted">
|
||||
需要「{{ metaStore.accountLabel(currentMeta.account_type) }}」类型的账号
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="gatewayMessageFields.length" label="消息内容">
|
||||
<div class="message-fields">
|
||||
<el-tag
|
||||
v-for="f in gatewayMessageFields"
|
||||
:key="f.key"
|
||||
size="small"
|
||||
type="info"
|
||||
effect="plain"
|
||||
>
|
||||
{{ f.label }}
|
||||
</el-tag>
|
||||
<div class="muted">
|
||||
由投递请求的 <code>title</code> / <code>message</code> 自动填充,无需在此填写
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<ConfigForm v-if="currentMeta" v-model="form.config_json" :fields="configFields" />
|
||||
|
||||
<el-form-item label="启用">
|
||||
<el-switch v-model="form.enabled" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.rule-tip {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.rule-tip code {
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
background: var(--gb-bg);
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.message-fields {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.message-fields .muted {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-tools {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header-tools {
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,18 +1,381 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
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)
|
||||
import { Connection, CopyDocument, Delete, Link, Plus, Setting } from '@element-plus/icons-vue'
|
||||
import { gatewayApi, gatewayRuleApi } from '@/api'
|
||||
import type { GatewayRule, MessageGateway } from '@/api/types'
|
||||
import { useMetaStore } from '@/stores/meta'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
|
||||
const router = useRouter()
|
||||
const metaStore = useMetaStore()
|
||||
|
||||
const gateways = ref<MessageGateway[]>([])
|
||||
const rules = ref<GatewayRule[]>([])
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const name = ref('')
|
||||
const type = ref('')
|
||||
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)
|
||||
|
||||
const currentMeta = computed(() => metaStore.gatewayMeta(type.value))
|
||||
const enabledRuleCount = computed(() => rules.value.filter((r) => r.enabled).length)
|
||||
|
||||
async function refresh() {
|
||||
loading.value = true
|
||||
try {
|
||||
;[gateways.value, rules.value] = await Promise.all([gatewayApi.list(), gatewayRuleApi.list()])
|
||||
} catch {
|
||||
ElMessage.error('加载消息网关失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
name.value = ''
|
||||
type.value = metaStore.gatewayMetas[0]?.type ?? 'webhook'
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function create() {
|
||||
if (!name.value.trim()) {
|
||||
ElMessage.warning('请输入网关名称')
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
await gatewayApi.create(name.value.trim(), type.value)
|
||||
dialogVisible.value = false
|
||||
await refresh()
|
||||
ElMessage.success('消息网关已创建')
|
||||
} catch (e) {
|
||||
ElMessage.error(e instanceof Error ? e.message : '创建失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(item: MessageGateway) {
|
||||
const count = ruleCount(item)
|
||||
const detail = count > 0 ? `关联的 ${count} 条网关规则也会被一并删除。` : ''
|
||||
try {
|
||||
await ElMessageBox.confirm(`删除“${item.name}”?${detail}`, '删除消息网关', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await gatewayApi.remove(item.id)
|
||||
await refresh()
|
||||
ElMessage.success('消息网关已删除')
|
||||
} catch {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function payloadHint(item: MessageGateway) {
|
||||
return metaStore.gatewayMeta(item.type)?.payload_hint ?? '{ "message": "..." }'
|
||||
}
|
||||
|
||||
function manageRules(item: MessageGateway) {
|
||||
router.push({ name: 'gateway-rules', query: { gateway_id: String(item.id) } })
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await metaStore.ensureLoaded()
|
||||
await 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>
|
||||
|
||||
<template>
|
||||
<div v-loading="loading">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>消息网关</h2>
|
||||
<div class="muted">为外部系统生成 Webhook 地址,投递消息即可触发绑定在网关上的规则</div>
|
||||
</div>
|
||||
<el-button type="primary" :icon="Plus" @click="openCreate">新建网关</el-button>
|
||||
</div>
|
||||
|
||||
<div class="gw-stats gb-rise">
|
||||
<div class="gw-stat">
|
||||
<span class="gw-stat-label">网关</span>
|
||||
<span class="gw-stat-value">{{ gateways.length }}</span>
|
||||
</div>
|
||||
<div class="gw-stat">
|
||||
<span class="gw-stat-label">网关规则</span>
|
||||
<span class="gw-stat-value">{{ rules.length }}</span>
|
||||
</div>
|
||||
<div class="gw-stat">
|
||||
<span class="gw-stat-label">已启用规则</span>
|
||||
<span class="gw-stat-value">{{ enabledRuleCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="!loading && gateways.length === 0" description="还没有消息网关">
|
||||
<el-button type="primary" :icon="Plus" @click="openCreate">新建网关</el-button>
|
||||
</el-empty>
|
||||
|
||||
<div v-else class="gateway-grid">
|
||||
<el-card
|
||||
v-for="(item, i) in gateways"
|
||||
:key="item.id"
|
||||
class="gateway-card gb-rise"
|
||||
:style="{ animationDelay: `${i * 0.05}s` }"
|
||||
>
|
||||
<div class="gateway-head">
|
||||
<div class="gateway-ident">
|
||||
<div class="gateway-icon">
|
||||
<el-icon :size="18"><Connection /></el-icon>
|
||||
</div>
|
||||
<div class="gateway-titles">
|
||||
<div class="gateway-name">{{ item.name }}</div>
|
||||
<div class="gateway-sub">
|
||||
<el-tag size="small" type="info" effect="light">
|
||||
{{ metaStore.gatewayLabel(item.type) }}
|
||||
</el-tag>
|
||||
<span>{{ ruleCount(item) }} 条网关规则</span>
|
||||
<span>{{ formatDateTime(item.create_at) }}</span>
|
||||
</div>
|
||||
</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="endpoint-label payload-label">请求示例</div>
|
||||
<pre class="payload"><code>{{ payloadHint(item) }}</code></pre>
|
||||
|
||||
<div class="gateway-actions">
|
||||
<el-button size="small" :icon="Setting" @click="manageRules(item)">管理网关规则</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 新建网关 -->
|
||||
<el-dialog v-model="dialogVisible" title="新建消息网关" width="520px">
|
||||
<el-form label-width="90px">
|
||||
<el-form-item label="网关类型" required>
|
||||
<el-select v-model="type" style="width: 100%">
|
||||
<el-option
|
||||
v-for="m in metaStore.gatewayMetas"
|
||||
:key="m.type"
|
||||
:label="m.label"
|
||||
:value="m.type"
|
||||
/>
|
||||
</el-select>
|
||||
<div v-if="currentMeta?.description" class="muted field-hint">
|
||||
{{ currentMeta.description }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="网关名称" required>
|
||||
<el-input v-model="name" placeholder="例如:生产环境告警" maxlength="64" clearable />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="dialog-hint">
|
||||
<el-icon><Link /></el-icon>
|
||||
<span>创建后到「网关规则」页面,把通知规则关联到这个网关</span>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="create">创建</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.gw-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.gw-stat {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
padding: 14px 18px;
|
||||
border-radius: var(--gb-radius-card);
|
||||
background: var(--gb-card);
|
||||
box-shadow: var(--gb-shadow-card);
|
||||
}
|
||||
|
||||
.gw-stat-label {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.gw-stat-value {
|
||||
margin-left: auto;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--gb-primary-deep);
|
||||
}
|
||||
|
||||
.gateway-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.gateway-card {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.gateway-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: var(--gb-shadow-hover);
|
||||
}
|
||||
|
||||
.gateway-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.gateway-ident {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.gateway-icon {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
flex: none;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 11px;
|
||||
color: var(--gb-primary-deep);
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
.gateway-titles {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.gateway-name {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.gateway-sub {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 6px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.endpoint-label {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.endpoint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
margin-top: 6px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 10px;
|
||||
background: var(--gb-bg);
|
||||
}
|
||||
|
||||
.endpoint code {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.payload-label {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.payload {
|
||||
margin: 6px 0 0;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
background: var(--gb-bg);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.payload code {
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.gateway-actions {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.field-hint {
|
||||
line-height: 1.5;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.dialog-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
background: var(--el-color-primary-light-9);
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.gateway-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -21,6 +21,7 @@ const triggerLabels: Record<string, string> = {
|
||||
timer: '定时触发',
|
||||
manual: '手动测试',
|
||||
remind: '提醒',
|
||||
gateway: '网关投递',
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
|
||||
@@ -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, gatewayApi, ruleApi, timerApi } from '@/api'
|
||||
import { accountApi, ruleApi, timerApi } from '@/api'
|
||||
import { ApiError } from '@/api/client'
|
||||
import type { Account, MessageGateway, Rule, Timer } from '@/api/types'
|
||||
import type { Account, Rule, Timer } from '@/api/types'
|
||||
import { useMetaStore } from '@/stores/meta'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import ConfigForm from '@/components/ConfigForm.vue'
|
||||
@@ -16,7 +16,6 @@ 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)
|
||||
@@ -31,7 +30,6 @@ const form = ref<{
|
||||
name: string
|
||||
type: string
|
||||
timer_id: number
|
||||
gateway_id: number
|
||||
account_id: number | undefined
|
||||
enabled: boolean
|
||||
config_json: string
|
||||
@@ -39,7 +37,6 @@ const form = ref<{
|
||||
name: '',
|
||||
type: '',
|
||||
timer_id: 0,
|
||||
gateway_id: 0,
|
||||
account_id: undefined,
|
||||
enabled: true,
|
||||
config_json: '',
|
||||
@@ -72,16 +69,14 @@ function accountName(id: number): string {
|
||||
async function refresh() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [ruleList, timerList, accountList, gatewayList] = await Promise.all([
|
||||
const [ruleList, timerList, accountList] = 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 {
|
||||
@@ -90,7 +85,7 @@ async function refresh() {
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
if (timers.value.length === 0 && gateways.value.length === 0) {
|
||||
if (timers.value.length === 0) {
|
||||
ElMessage.warning('请先在“定时器”页面创建一个定时器')
|
||||
return
|
||||
}
|
||||
@@ -99,7 +94,6 @@ 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: '',
|
||||
@@ -113,7 +107,6 @@ 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,
|
||||
@@ -140,7 +133,6 @@ 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,
|
||||
@@ -170,7 +162,6 @@ 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,
|
||||
@@ -317,14 +308,11 @@ onMounted(async () => {
|
||||
</el-select>
|
||||
<div v-if="currentMeta?.description" class="muted">{{ currentMeta.description }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="关联定时器/消息网关" required>
|
||||
<div class="source-hint muted">规则只能选择一种触发方式</div>
|
||||
<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
|
||||
|
||||
Reference in New Issue
Block a user