edge-proxy-app/src/views/AlarmManager.vue

358 lines
10 KiB
Vue
Raw Normal View History

2025-11-04 14:52:53 +08:00
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import {
getAlarmConfig, // 返回 string
postAlarmConfig, // 接收 string
postReloadRules,
type AlarmRule, // 导入我们新定义的接口
} from "../api/alarms";
import { Plus, Delete, RefreshLeft } from "@element-plus/icons-vue";
const originalRules = ref<AlarmRule[]>([]); // 从服务器加载的原始数据
const editableRules = ref<AlarmRule[]>([]); // 绑定到表格、可编辑的数据
const isLoading = ref(false);
const isSaving = ref(false);
const isReloading = ref(false);
// 表格中下拉选项
const compareTypes = ["GT", "LT", "EQ"];
const levels = ["INFO", "WARNING", "CRITICAL"];
// --- 1. 数据加载与刷新 ---
const handleRefresh = async (confirm = false) => {
if (confirm) {
try {
await ElMessageBox.confirm(
"刷新将从服务器重新加载配置,并覆盖您在表格中的任何未保存更改。",
"确认刷新?",
{ type: "warning" }
);
} catch (error) {
return; // 用户取消
}
}
isLoading.value = true;
try {
const configString = await getAlarmConfig();
const parsedRules: AlarmRule[] = JSON.parse(configString);
// 填充两份数据
originalRules.value = parsedRules;
// [关键] 必须使用深拷贝
editableRules.value = JSON.parse(JSON.stringify(parsedRules));
ElMessage.success("告警配置已刷新");
} catch (error: any) {
ElMessage.error(`刷新失败: ${error.message}`);
} finally {
isLoading.value = false;
}
};
onMounted(() => {
handleRefresh(false); // 页面加载时自动获取
});
// --- 2. 脏检查 (标红逻辑) ---
const isCellDirty = (index: number, field: keyof AlarmRule) => {
const editableRow = editableRules.value[index];
// 如果是新行,所有字段都算“脏”
if (editableRow._isNew) {
return true;
}
const originalRow = originalRules.value[index];
// 如果原始行不存在 (不应该发生,但作为安全检查) 或值不匹配
if (!originalRow || editableRow[field] !== originalRow[field]) {
return true;
}
return false;
};
// --- 3. CRUD 操作 (增 / 删) ---
const addNewRule = () => {
editableRules.value.unshift({
// 添加一条带默认值的新规则
rule_id: "NEW_RULE_ID",
device_id: "*",
data_point_name: "temperature",
compare_type: "GT",
threshold: 0,
level: "WARNING",
debounce_seconds: 0,
message_template: "New alarm triggered!",
_isNew: true, // 标记为新行
});
ElMessage.success("已添加新规则在顶部,请修改。");
};
const deleteRule = (index: number) => {
// 从可编辑数组中移除
editableRules.value.splice(index, 1);
// 注意:如果这是新行,它就直接消失了。
// 如果这不是新行,下次保存时,它将从配置中被删除。
};
// --- 4. 保存与应用 ---
const handleSaveAndApply = async () => {
isSaving.value = true;
try {
// 步骤 1: 准备要发送的数据
// 我们必须移除前端专用的 '_isNew' 标记
const dataToSend = editableRules.value.map((rule) => {
const { _isNew, ...rest } = rule;
return rest;
});
// 将其序列化回 C++ API 期望的字符串
const jsonString = JSON.stringify(dataToSend, null, 2); // 格式化 JSON
// 步骤 2: 保存 (POST /config)
await postAlarmConfig(jsonString);
ElMessage.success("配置保存成功!");
// 步骤 3: 应用 (POST /reload)
await postReloadRules();
ElMessage.success("配置已在服务器端成功应用!");
// [关键] 保存成功后,更新 "原始" 数据,脏标记消失
originalRules.value = JSON.parse(JSON.stringify(editableRules.value));
} catch (error: any) {
ElMessage.error(`操作失败: ${error.message}`);
} finally {
isSaving.value = false;
}
};
const handleApplyOnly = async () => {
isReloading.value = true;
try {
await postReloadRules();
ElMessage.success("“应用”命令已发送,服务器已重载规则");
} catch (error: any) {
ElMessage.error(`应用失败: ${error.message}`);
} finally {
isReloading.value = false;
}
};
</script>
<template>
<div class="config-manager-page">
<header class="page-header">
<h1>告警配置管理</h1>
<div class="button-group">
<el-button @click="addNewRule" type="success" :icon="Plus">
添加规则
</el-button>
<el-button
@click="handleRefresh(true)"
:loading="isLoading"
:icon="RefreshLeft"
>
刷新
</el-button>
<el-button
@click="handleApplyOnly"
:loading="isReloading"
type="warning"
>
仅应用 (实时重载)
</el-button>
<el-button
type="primary"
@click="handleSaveAndApply"
:loading="isSaving"
>
保存并应用
</el-button>
</div>
</header>
<div class="table-container" v-loading="isLoading">
<ElTable :data="editableRules" stripe border height="calc(100vh - 160px)">
<ElTableColumn label="规则ID (rule_id)" width="200" fixed>
<template #default="scope">
<el-input
v-model="scope.row.rule_id"
:class="{ 'is-dirty': isCellDirty(scope.$index, 'rule_id') }"
/>
</template>
</ElTableColumn>
<ElTableColumn label="设备ID (device_id)" width="200">
<template #default="scope">
<el-input
v-model="scope.row.device_id"
:class="{ 'is-dirty': isCellDirty(scope.$index, 'device_id') }"
/>
</template>
</ElTableColumn>
<ElTableColumn label="数据点 (data_point_name)" width="180">
<template #default="scope">
<el-input
v-model="scope.row.data_point_name"
:class="{
'is-dirty': isCellDirty(scope.$index, 'data_point_name'),
}"
/>
</template>
</ElTableColumn>
<ElTableColumn label="比较 (compare_type)" width="120">
<template #default="scope">
<el-select
v-model="scope.row.compare_type"
:class="{ 'is-dirty': isCellDirty(scope.$index, 'compare_type') }"
>
<el-option
v-for="item in compareTypes"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</template>
</ElTableColumn>
<ElTableColumn label="阈值 (threshold)" width="120">
<template #default="scope">
<el-input-number
v-model="scope.row.threshold"
controls-position="right"
:class="{ 'is-dirty': isCellDirty(scope.$index, 'threshold') }"
/>
</template>
</ElTableColumn>
<ElTableColumn label="级别 (level)" width="120">
<template #default="scope">
<el-select
v-model="scope.row.level"
:class="{ 'is-dirty': isCellDirty(scope.$index, 'level') }"
>
<el-option
v-for="item in levels"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</template>
</ElTableColumn>
<ElTableColumn label="防抖(s) (debounce_seconds)" width="120">
<template #default="scope">
<el-input-number
v-model="scope.row.debounce_seconds"
:min="0"
controls-position="right"
:class="{
'is-dirty': isCellDirty(scope.$index, 'debounce_seconds'),
}"
/>
</template>
</ElTableColumn>
<ElTableColumn label="告警模板 (message_template)" width="400">
<template #default="scope">
<el-input
v-model="scope.row.message_template"
type="textarea"
:rows="2"
:class="{
'is-dirty': isCellDirty(scope.$index, 'message_template'),
}"
/>
</template>
</ElTableColumn>
<ElTableColumn label="解除模板 (clear_message_template)" width="400">
<template #default="scope">
<el-input
v-model="scope.row.clear_message_template"
type="textarea"
:rows="2"
:class="{
'is-dirty': isCellDirty(scope.$index, 'clear_message_template'),
}"
/>
</template>
</ElTableColumn>
<ElTableColumn label="MQTT 主题 (alarm_mqtt_topic)" width="200">
<template #default="scope">
<el-input
v-model="scope.row.alarm_mqtt_topic"
:class="{
'is-dirty': isCellDirty(scope.$index, 'alarm_mqtt_topic'),
}"
/>
</template>
</ElTableColumn>
<ElTableColumn label="操作" width="100" fixed="right">
<template #default="scope">
<el-button
type="danger"
:icon="Delete"
circle
@click="deleteRule(scope.$index)"
/>
</template>
</ElTableColumn>
</ElTable>
</div>
</div>
</template>
<style scoped>
.config-manager-page {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
color: var(--el-text-color-primary);
}
h1 {
font-size: 1.5em;
font-weight: 600;
margin: 0;
}
.table-container {
flex-grow: 1;
overflow: hidden; /* 确保表格在容器内 */
}
/* [关键] "标红" 逻辑 */
:deep(.el-input.is-dirty .el-input__wrapper),
:deep(.el-input-number.is-dirty .el-input__wrapper),
:deep(.el-select.is-dirty .el-select__wrapper) {
/* 使用 Element Plus 的危险色变量 */
box-shadow: 0 0 0 1px var(--el-color-danger) inset !important;
}
:deep(.el-textarea.is-dirty .el-textarea__inner) {
box-shadow: 0 0 0 1px var(--el-color-danger) inset !important;
}
/* 确保表格内的组件占满单元格 */
:deep(.el-table .el-input),
:deep(.el-table .el-input-number),
:deep(.el-table .el-select) {
width: 100%;
}
</style>