Files
goodBaby/web/frontend/src/views/RulesView.vue
T
mei 0d89b7fe8f
Quality check / Web UI (push) Successful in 9m16s
feat(gateway): 新增消息网关及apikey功能
2026-08-30 21:09:26 +08:00

393 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
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 { ApiError } from '@/api/client'
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'
import { useIsMobile } from '@/composables/useBreakpoint'
const isMobile = useIsMobile()
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)
const filterTimerId = 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
timer_id: number
gateway_id: number
account_id: number | undefined
enabled: boolean
config_json: string
}>({
name: '',
type: '',
timer_id: 0,
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 availableAccounts = computed(() => {
const requiredType = currentMeta.value?.account_type
if (!requiredType) return []
return accounts.value.filter((a) => a.type === requiredType)
})
const filteredRules = computed(() => {
if (!filterTimerId.value) return rules.value
return rules.value.filter((r) => r.timer_id === filterTimerId.value)
})
function timerName(id: number): string {
return timers.value.find((t) => t.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, 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 {
loading.value = false
}
}
function openCreate() {
if (timers.value.length === 0 && gateways.value.length === 0) {
ElMessage.warning('请先在“定时器”页面创建一个定时器')
return
}
editingId.value = null
form.value = {
name: '',
type: metaStore.ruleMetas[0]?.type ?? '',
timer_id: timers.value[0]?.id ?? 0,
gateway_id: 0,
account_id: undefined,
enabled: true,
config_json: '',
}
dialogVisible.value = true
}
function openEdit(rule: Rule) {
editingId.value = rule.id
form.value = {
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,
}
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 (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,
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,
}
saving.value = true
try {
if (editingId.value === null) {
await ruleApi.create(body)
ElMessage.success('规则已创建')
} else {
await ruleApi.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: Rule) {
try {
await ruleApi.update(rule.id, {
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,
})
ElMessage.success(rule.enabled ? '已启用' : '已停用')
} catch (error) {
rule.enabled = !rule.enabled
ElMessage.error(error instanceof ApiError ? error.message : '操作失败')
}
}
async function test(rule: Rule) {
try {
await ElMessageBox.confirm(
'测试会真实执行该规则(发送消息 / 修改仓库等),确定继续吗?',
`测试规则「${rule.name}」`,
{ type: 'warning', confirmButtonText: '执行', cancelButtonText: '取消' },
)
} catch {
return
}
testingId.value = rule.id
try {
await ruleApi.test(rule.id)
ElMessage.success('执行成功,详情见执行日志')
} catch (error) {
ElMessage.error(error instanceof ApiError ? error.message : '执行失败')
} finally {
testingId.value = null
}
}
async function remove(rule: Rule) {
try {
await ElMessageBox.confirm(`确定删除规则「${rule.name}」吗?`, '删除规则', {
type: 'warning',
confirmButtonText: '删除',
confirmButtonClass: 'el-button--danger',
cancelButtonText: '取消',
})
} catch {
return
}
try {
await ruleApi.remove(rule.id)
ElMessage.success('已删除')
await refresh()
} catch (error) {
ElMessage.error(error instanceof ApiError ? error.message : '删除失败')
}
}
onMounted(async () => {
await metaStore.ensureLoaded()
await refresh()
})
</script>
<template>
<div v-loading="loading">
<div class="page-header">
<div>
<h2>规则</h2>
<div class="muted">定时器到期时要执行的动作:发送邮件、QQ / 钉钉消息、B 站动态,或公开 GitHub 仓库</div>
</div>
<div class="header-tools">
<el-select
v-model="filterTimerId"
placeholder="按定时器筛选"
clearable
class="filter-select"
>
<el-option v-for="t in timers" :key="t.id" :label="t.name" :value="t.id" />
</el-select>
<el-button type="primary" :icon="Plus" @click="openCreate">创建规则</el-button>
</div>
</div>
<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 }">{{ timerName(row.timer_id) }}</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.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>
</el-form-item>
<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
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>
<ConfigForm
v-if="currentMeta"
v-model="form.config_json"
:fields="currentMeta.fields"
/>
<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>
.tip {
margin-bottom: 16px;
}
.header-tools {
display: flex;
gap: 12px;
}
.filter-select {
width: 200px;
}
.source-select { width: 100%; margin-top: 8px; }
.source-hint { margin-bottom: 2px; }
@media (max-width: 768px) {
.header-tools {
flex-wrap: wrap;
gap: 10px;
}
.filter-select {
width: 100%;
}
}
</style>