Files
goodBaby/web/frontend/src/views/RulesView.vue
T

393 lines
12 KiB
Vue
Raw Normal View History

2026-07-26 22:19:41 +08:00
<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'
2026-07-26 22:19:41 +08:00
import { ApiError } from '@/api/client'
import type { Account, MessageGateway, Rule, Timer } from '@/api/types'
2026-07-26 22:19:41 +08:00
import { useMetaStore } from '@/stores/meta'
import { formatDateTime } from '@/utils/format'
import ConfigForm from '@/components/ConfigForm.vue'
2026-07-26 22:57:52 +08:00
import { useIsMobile } from '@/composables/useBreakpoint'
const isMobile = useIsMobile()
2026-07-26 22:19:41 +08:00
const metaStore = useMetaStore()
const rules = ref<Rule[]>([])
const timers = ref<Timer[]>([])
const gateways = ref<MessageGateway[]>([])
2026-07-26 22:19:41 +08:00
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
2026-07-26 22:19:41 +08:00
account_id: number | undefined
enabled: boolean
config_json: string
}>({
name: '',
type: '',
timer_id: 0,
gateway_id: 0,
2026-07-26 22:19:41 +08:00
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([
2026-07-26 22:19:41 +08:00
ruleApi.list(),
timerApi.list(),
accountApi.list(),
gatewayApi.list(),
2026-07-26 22:19:41 +08:00
])
rules.value = ruleList
timers.value = timerList
accounts.value = accountList
gateways.value = gatewayList
2026-07-26 22:19:41 +08:00
} catch (error) {
ElMessage.error(error instanceof ApiError ? error.message : '加载失败')
} finally {
loading.value = false
}
}
function openCreate() {
if (timers.value.length === 0 && gateways.value.length === 0) {
2026-07-26 22:19:41 +08:00
ElMessage.warning('请先在“定时器”页面创建一个定时器')
return
}
editingId.value = null
form.value = {
name: '',
type: metaStore.ruleMetas[0]?.type ?? '',
timer_id: timers.value[0]?.id ?? 0,
gateway_id: 0,
2026-07-26 22:19:41 +08:00
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,
2026-07-26 22:19:41 +08:00
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,
2026-07-26 22:19:41 +08:00
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,
2026-07-26 22:19:41 +08:00
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
2026-07-26 22:57:52 +08:00
class="filter-select"
2026-07-26 22:19:41 +08:00
>
<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">
2026-07-26 22:57:52 +08:00
<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">
2026-07-26 22:19:41 +08:00
<template #default="{ row }">
<el-tag>{{ metaStore.ruleLabel(row.type) }}</el-tag>
</template>
</el-table-column>
2026-07-26 22:57:52 +08:00
<!-- 窄屏隐藏定时器/账号/创建时间,保留名称、类型与操作 -->
<el-table-column v-if="!isMobile" label="定时器" min-width="120">
2026-07-26 22:19:41 +08:00
<template #default="{ row }">{{ timerName(row.timer_id) }}</template>
</el-table-column>
2026-07-26 22:57:52 +08:00
<el-table-column v-if="!isMobile" label="账号" min-width="120">
2026-07-26 22:19:41 +08:00
<template #default="{ row }">{{ accountName(row.account_id) }}</template>
</el-table-column>
2026-07-26 22:57:52 +08:00
<el-table-column v-if="!isMobile" label="创建时间" width="170">
2026-07-26 22:19:41 +08:00
<template #default="{ row }">{{ formatDateTime(row.create_at) }}</template>
</el-table-column>
2026-07-26 22:57:52 +08:00
<el-table-column label="启用" :width="isMobile ? 60 : 80">
2026-07-26 22:19:41 +08:00
<template #default="{ row }">
<el-switch v-model="row.enabled" size="small" @change="toggleEnabled(row)" />
</template>
</el-table-column>
2026-07-26 22:57:52 +08:00
<el-table-column label="操作" :width="isMobile ? 200 : 240" fixed="right">
2026-07-26 22:19:41 +08:00
<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">
2026-07-26 22:57:52 +08:00
<el-form :label-width="isMobile ? 'auto' : '110px'" :label-position="isMobile ? 'top' : 'right'">
2026-07-26 22:19:41 +08:00
<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="定时器触发">
2026-07-26 22:19:41 +08:00
<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>
2026-07-26 22:19:41 +08:00
</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;
}
2026-07-26 22:57:52 +08:00
.filter-select {
width: 200px;
}
.source-select { width: 100%; margin-top: 8px; }
.source-hint { margin-bottom: 2px; }
2026-07-26 22:57:52 +08:00
@media (max-width: 768px) {
.header-tools {
flex-wrap: wrap;
gap: 10px;
}
.filter-select {
width: 100%;
}
}
2026-07-26 22:19:41 +08:00
</style>