- 添加项目基础结构:Cargo.toml、.gitignore、设备UID和密钥文件 - 实现前端Vue3项目结构:路由、登录页面、设备管理页面 - 添加核心协议定义(crates/protocol):设备状态、资产、USB事件等 - 实现客户端监控模块:系统状态收集、资产收集 - 实现服务端基础API和插件系统 - 添加数据库迁移脚本:设备管理、资产跟踪、告警系统等 - 实现前端设备状态展示和基本交互 - 添加使用时长统计和水印功能插件
217 lines
8.6 KiB
Vue
217 lines
8.6 KiB
Vue
<template>
|
|
<div class="alerts-page">
|
|
<el-tabs v-model="activeTab">
|
|
<el-tab-pane label="告警记录" name="records">
|
|
<div class="toolbar">
|
|
<el-select v-model="severityFilter" placeholder="严重程度" clearable style="width: 140px" @change="fetchRecords">
|
|
<el-option label="Critical" value="critical" />
|
|
<el-option label="High" value="high" />
|
|
<el-option label="Medium" value="medium" />
|
|
<el-option label="Low" value="low" />
|
|
</el-select>
|
|
<el-select v-model="handledFilter" placeholder="处理状态" clearable style="width: 140px" @change="fetchRecords">
|
|
<el-option label="待处理" value="false" />
|
|
<el-option label="已处理" value="true" />
|
|
</el-select>
|
|
</div>
|
|
<el-table :data="records" v-loading="recLoading" stripe size="small">
|
|
<el-table-column label="严重程度" width="100">
|
|
<template #default="{ row }">
|
|
<el-tag :type="severityTag(row.severity)" size="small">{{ row.severity }}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="alert_type" label="告警类型" width="130" />
|
|
<el-table-column prop="detail" label="详情" min-width="250" show-overflow-tooltip />
|
|
<el-table-column prop="device_uid" label="终端" width="150" show-overflow-tooltip />
|
|
<el-table-column prop="triggered_at" label="触发时间" width="170" />
|
|
<el-table-column label="状态" width="80">
|
|
<template #default="{ row }">
|
|
<el-tag :type="row.handled ? 'success' : 'warning'" size="small">
|
|
{{ row.handled ? '已处理' : '待处理' }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="100" fixed="right">
|
|
<template #default="{ row }">
|
|
<el-button v-if="!row.handled" link type="primary" size="small" @click="handleRecord(row.id)">处理</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</el-tab-pane>
|
|
|
|
<el-tab-pane label="告警规则" name="rules">
|
|
<div class="toolbar">
|
|
<el-button type="primary" @click="showRuleDialog()">新建规则</el-button>
|
|
</div>
|
|
<el-table :data="rules" v-loading="ruleLoading" stripe size="small">
|
|
<el-table-column prop="name" label="规则名称" width="180" />
|
|
<el-table-column prop="rule_type" label="规则类型" width="140" />
|
|
<el-table-column prop="severity" label="严重程度" width="100">
|
|
<template #default="{ row }">
|
|
<el-tag :type="severityTag(row.severity)" size="small">{{ row.severity }}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="condition" label="条件" min-width="200" show-overflow-tooltip />
|
|
<el-table-column prop="enabled" label="启用" width="80">
|
|
<template #default="{ row }">
|
|
<el-switch :model-value="row.enabled" @change="toggleRule(row)" size="small" />
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="140" fixed="right">
|
|
<template #default="{ row }">
|
|
<el-button link type="primary" size="small" @click="showRuleDialog(row)">编辑</el-button>
|
|
<el-button link type="danger" size="small" @click="deleteRule(row.id)">删除</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</el-tab-pane>
|
|
</el-tabs>
|
|
|
|
<el-dialog v-model="ruleDialogVisible" :title="editingRule ? '编辑规则' : '新建规则'" width="500px">
|
|
<el-form :model="ruleForm" label-width="100px">
|
|
<el-form-item label="规则名称">
|
|
<el-input v-model="ruleForm.name" />
|
|
</el-form-item>
|
|
<el-form-item label="规则类型">
|
|
<el-select v-model="ruleForm.rule_type" style="width: 100%">
|
|
<el-option label="CPU过高" value="cpu_high" />
|
|
<el-option label="内存过高" value="memory_high" />
|
|
<el-option label="未授权USB" value="usb_unauth" />
|
|
<el-option label="终端离线" value="device_offline" />
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="条件">
|
|
<el-input v-model="ruleForm.condition" type="textarea" :rows="2" placeholder='{"threshold":90,"duration_secs":300}' />
|
|
</el-form-item>
|
|
<el-form-item label="严重程度">
|
|
<el-select v-model="ruleForm.severity" style="width: 100%">
|
|
<el-option label="Critical" value="critical" />
|
|
<el-option label="High" value="high" />
|
|
<el-option label="Medium" value="medium" />
|
|
<el-option label="Low" value="low" />
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="通知邮箱">
|
|
<el-input v-model="ruleForm.notify_email" placeholder="可选" />
|
|
</el-form-item>
|
|
<el-form-item label="Webhook">
|
|
<el-input v-model="ruleForm.notify_webhook" placeholder="可选" />
|
|
</el-form-item>
|
|
</el-form>
|
|
<template #footer>
|
|
<el-button @click="ruleDialogVisible = false">取消</el-button>
|
|
<el-button type="primary" @click="saveRule">保存</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, reactive, onMounted } from 'vue'
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
import { api } from '@/lib/api'
|
|
|
|
const activeTab = ref('records')
|
|
|
|
// Records
|
|
const records = ref<any[]>([])
|
|
const recLoading = ref(false)
|
|
const severityFilter = ref('')
|
|
const handledFilter = ref('')
|
|
|
|
async function fetchRecords() {
|
|
recLoading.value = true
|
|
try {
|
|
const params = new URLSearchParams()
|
|
if (severityFilter.value) params.set('severity', severityFilter.value)
|
|
if (handledFilter.value) params.set('handled', handledFilter.value)
|
|
const data = await api.get<any>(`/api/alerts/records?${params}`)
|
|
records.value = data.records || []
|
|
} catch { /* api.ts handles 401 */ } finally { recLoading.value = false }
|
|
}
|
|
|
|
async function handleRecord(id: number) {
|
|
try {
|
|
await api.put(`/api/alerts/records/${id}/handle`)
|
|
ElMessage.success('已标记处理')
|
|
fetchRecords()
|
|
} catch (e: any) { ElMessage.error(e.message || '操作失败') }
|
|
}
|
|
|
|
// Rules
|
|
const rules = ref<any[]>([])
|
|
const ruleLoading = ref(false)
|
|
const ruleDialogVisible = ref(false)
|
|
const editingRule = ref<any>(null)
|
|
const ruleForm = reactive({ name: '', rule_type: 'cpu_high', condition: '{"threshold":90}', severity: 'high', notify_email: '', notify_webhook: '' })
|
|
|
|
async function fetchRules() {
|
|
ruleLoading.value = true
|
|
try {
|
|
const data = await api.get<any>('/api/alerts/rules')
|
|
rules.value = data.rules || []
|
|
} catch { /* api.ts handles 401 */ } finally { ruleLoading.value = false }
|
|
}
|
|
|
|
function showRuleDialog(row?: any) {
|
|
if (row) {
|
|
editingRule.value = row
|
|
ruleForm.name = row.name
|
|
ruleForm.rule_type = row.rule_type
|
|
ruleForm.condition = row.condition
|
|
ruleForm.severity = row.severity
|
|
ruleForm.notify_email = row.notify_email || ''
|
|
ruleForm.notify_webhook = row.notify_webhook || ''
|
|
} else {
|
|
editingRule.value = null
|
|
Object.assign(ruleForm, { name: '', rule_type: 'cpu_high', condition: '{"threshold":90}', severity: 'high', notify_email: '', notify_webhook: '' })
|
|
}
|
|
ruleDialogVisible.value = true
|
|
}
|
|
|
|
async function saveRule() {
|
|
try {
|
|
if (editingRule.value) {
|
|
await api.put(`/api/alerts/rules/${editingRule.value.id}`, ruleForm)
|
|
ElMessage.success('规则已更新')
|
|
} else {
|
|
await api.post('/api/alerts/rules', ruleForm)
|
|
ElMessage.success('规则已创建')
|
|
}
|
|
ruleDialogVisible.value = false
|
|
fetchRules()
|
|
} catch (e: any) { ElMessage.error(e.message || '操作失败') }
|
|
}
|
|
|
|
async function toggleRule(row: any) {
|
|
try {
|
|
await api.put(`/api/alerts/rules/${row.id}`, { enabled: !row.enabled ? 1 : 0 })
|
|
fetchRules()
|
|
} catch { /* ignore */ }
|
|
}
|
|
|
|
async function deleteRule(id: number) {
|
|
await ElMessageBox.confirm('确定删除该规则?', '确认', { type: 'warning' })
|
|
try {
|
|
await api.delete(`/api/alerts/rules/${id}`)
|
|
ElMessage.success('规则已删除')
|
|
fetchRules()
|
|
} catch (e: any) { ElMessage.error(e.message || '删除失败') }
|
|
}
|
|
|
|
function severityTag(s: string) {
|
|
const map: Record<string, string> = { critical: 'danger', high: 'warning', medium: '', low: 'info' }
|
|
return map[s] || 'info'
|
|
}
|
|
|
|
onMounted(() => {
|
|
fetchRecords()
|
|
fetchRules()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.alerts-page { padding: 20px; }
|
|
.toolbar { display: flex; gap: 12px; margin-bottom: 16px; }
|
|
</style>
|