358 lines
10 KiB
Vue
358 lines
10 KiB
Vue
<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>
|