Commit 096259f2 authored by 张俊's avatar 张俊

左侧菜单样式更改

parent dd5821ad
...@@ -8,7 +8,8 @@ ...@@ -8,7 +8,8 @@
v-if="nowParent" v-if="nowParent"
:highlightParentRule="highlightParentRule" :highlightParentRule="highlightParentRule"
width="208px" width="208px"
:list="nowParent" :title="nowParent[0].menuName"
:list="nowParent[0].children"
v-show="navShow" v-show="navShow"
class="con-nav" /> class="con-nav" />
<div class="bg-main view"> <div class="bg-main view">
......
...@@ -94,3 +94,16 @@ const isCurrent = (path) => { ...@@ -94,3 +94,16 @@ const isCurrent = (path) => {
return (props.highlightParentRule && props.highlightParentRule(path)) || false; return (props.highlightParentRule && props.highlightParentRule(path)) || false;
}; };
</script> </script>
<style lang="scss" scoped>
.nav-item {
color: #202531;
margin: 10px 10px 10px 10px;
padding: 6px 10px;
border-radius: 6px;
}
.nav-item:hover {
background-color: #ebeff9;
color: #3759be;
}
</style>
<template> <template>
<div class="bg-nav" :style="{ width: width }"> <div class="bg-nav" :style="{ width: width }">
<div class="bg-nav-title" v-if="title"> <div class="bg-nav-title" v-if="title">
<h3 class="text-clip">{{ title }}</h3> <p class="text-clip"><bg-icon class="menu-icon" icon="#bg-ic-menu"></bg-icon>{{ title }}</p>
</div> </div>
<div class="bg-nav-list bg-scroll"> <div class="bg-nav-list bg-scroll">
<NavList :list="list" :highlight-parent-rule="highlightParentRule" /> <NavList :list="list" :highlight-parent-rule="highlightParentRule" />
...@@ -30,3 +30,52 @@ const props = defineProps({ ...@@ -30,3 +30,52 @@ const props = defineProps({
}, },
}); });
</script> </script>
<style lang="scss" scoped>
.bg-nav {
background-color: #fff;
box-shadow: 4px 0 4px #00076526;
transition: all 0.3s;
height: 100%;
display: flex;
flex-direction: column;
color: #202531;
.bg-nav-title {
padding: 0 0 12px 12px;
font-weight: 600;
line-height: 28px;
.menu-icon {
margin: 0 8px;
}
& > p {
font-size: 16px;
color: #3759be;
}
}
}
</style>
<style lang="scss">
// .bg-nav > .bg-nav-list ul.nav-list > li > .nav-item {
// color: #202531;
// margin: 10px 10px 10px 10px;
// padding: 6px 10px;
// border-radius: 6px;
// }
.bg-nav > .bg-nav-list ul.nav-list > li > .nav-item.current {
background-color: #ebeff9;
color: #3759be;
&::before {
display: none;
}
&::after {
display: none;
}
}
.bg-nav > .bg-nav-list ul.nav-list > li > ul.nav-list {
background-color: #fff;
}
</style>
...@@ -469,40 +469,11 @@ a { ...@@ -469,40 +469,11 @@ a {
> .nav-item { > .nav-item {
position: relative; position: relative;
display: block; display: block;
padding: 10px 10px 10px 24px;
z-index: 9; z-index: 9;
font-size: 14px; font-size: 14px;
line-height: 28px; line-height: 28px;
text-decoration: none; text-decoration: none;
color: #fff;
cursor: pointer; cursor: pointer;
&:hover:not(.nav-more),
&.current {
color: #fff;
&::before {
content: "";
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
background-color: #2a4aa7;
z-index: -1;
}
&::after {
content: "";
width: 3px;
height: 100%;
position: absolute;
top: 0;
left: 0;
background-color: #5a7adc;
z-index: -1;
}
}
}
> ul.nav-list {
background-color: #202531;
} }
} }
} }
......
<template>
<div class="detail_container">
<bg-breadcrumb></bg-breadcrumb>
<div class="main_container">
<div class="add-form-main bg-scroll">
<add-form ref="add_form"></add-form>
</div>
<div class="add-btns">
<el-button size="default" @click="Cancle">取消</el-button>
<el-button type="primary" size="default" @click="SaveSubmit">保存</el-button>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import bgBreadcrumb from "@/components/bg-breadcrumb.vue";
import addForm from "../modules/add-form.vue";
import { Save } from "../modules/interface.js";
import { useRouter, useRoute } from "vue-router";
const router = useRouter();
const route = useRoute();
const { id, class_id } = route.query;
const add_form = ref(null);
const params = {};
const SaveSubmit = async () => {
let res = await add_form.value.Submit();
if (!res) return;
Save(res, { id, class_id }, () => {
Cancle();
});
};
const Cancle = () => {
router.go(-1);
};
</script>
<style lang="scss" scoped>
.detail_container {
width: 100%;
height: calc(100vh - 56px);
padding: 0 24px 16px;
min-height: 100%;
.main_container {
height: calc(100% - 46px);
padding: 0;
.add-form-main {
padding: 24px 24px 16px;
height: calc(100% - 68px);
}
.add-btns {
height: 68px;
display: flex;
align-items: center;
justify-content: flex-end;
border-top: 1px solid #ddd;
padding: 0 24px;
}
}
}
</style>
<template>
<div class="detail_container">
<bg-breadcrumb></bg-breadcrumb>
<div class="main_container bg-scroll">
<gap-title :hasLine="true" title="基本信息"></gap-title>
<div class="info">
<Info :labelData="labelData" :valueData="info">
<template #status="{ item, valueData }">
<span class="status-body">
<span class="status" :class="`status-${valueData.status}`"></span>
<span>{{ STATUS_OBJ[valueData[item.prop]] }}</span>
</span>
</template>
<template #create_time="{ valueData }">
<span>{{ dateStringToDate(valueData.create_time) }}</span>
</template>
<template #update_time="{ valueData }">
<span>{{ dateStringToDate(valueData.update_time) }}</span>
</template>
</Info>
</div>
<gap-title :hasLine="true" title="指标表达式"></gap-title>
<div class="info">
<div class="indicator-expression">
<bg-code-editor v-model="info.indicator_expression" disabled></bg-code-editor>
<!-- <bg-codemirror :disabled="true" v-model="info.indicator_expression"></bg-codemirror> -->
</div>
</div>
<gap-title :hasLine="true" title="预警范围"></gap-title>
<div class="info">
<bg-table border ref="ruletable" :headers="warningScopeHeaders" :rows="warningScopeRows" height="100%">
<template #metric="{ row }">
<span>{{ row.metric_name }}/{{ row.metric_label }}</span>
</template>
<template #is_required="{ row }">
<span>{{ row.is_required ? "" : "" }}</span>
</template>
<template #is_linked="{ row }">
<span>{{ row.is_linked ? "" : "" }}</span>
</template>
</bg-table>
</div>
<gap-title :hasLine="true" title="预警规则类型"></gap-title>
<div class="info">
<Info :labelData="rule_label" :valueData="rule_data">
<template #alert_rule_type="{ valueData }">
<span>
{{ ruleTypeOptions[valueData.alert_rule_type]?.label || "" }}
</span>
</template>
</Info>
</div>
<gap-title :hasLine="true" title="高级配置"> </gap-title>
<div class="info">
<Info :labelData="advanced_label" :valueData="advanced_data"></Info>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onBeforeMount } from "vue";
import { ElMessage } from "element-plus";
import axios from "@/request/http.js";
import { useRoute } from "vue-router";
import gapTitle from "@/components/gap-title.vue";
import bgBreadcrumb from "@/components/bg-breadcrumb.vue";
import Info from "@/components/warn-detail/info.vue";
import { METHODS } from "@/components/manual-distribution/env.js";
import { GetRuleTypeOptions, Empty, dateStringToDate } from "@/components/env.js";
const route = useRoute();
const { id, type_name, target_name } = route.query;
const STATUS_OBJ = {
1: "启用",
2: "禁用",
};
const unitOptions = {
s: "",
m: "分钟",
h: "小时",
};
const labelData = [
[
{
label: "预警分类",
prop: "warning_target",
},
{
label: "预警对象",
prop: "warning_type",
},
],
[
{
label: "预警指标",
prop: "warning_index",
},
{
label: "启用状态",
prop: "status",
},
],
[
{
label: "创建人",
prop: "create_by",
},
{
label: "创建时间",
prop: "create_time",
},
],
[
{
label: "更新时间",
prop: "update_time",
},
],
];
const info = ref({});
const rule_label = [
[
{
prop: "alert_rule_type",
label: "预警规则类型",
},
],
];
const rule_data = ref({});
const ruleTypeOptions = ref({});
const getRuleTypeOptions = async () => {
ruleTypeOptions.value = await GetRuleTypeOptions();
getInfoData();
};
const advanced_label = [
[
{
prop: "duration",
label: "持续时间",
},
],
[
{
prop: "check_period",
label: "检查周期",
},
],
];
const advanced_data = ref({});
const warningScopeHeaders = [
{
prop: "variable_name",
label: "变量名称",
width: 100,
},
{
prop: "metric",
label: "指标标签",
},
{
prop: "chinese_name",
label: "中文名称",
},
{
prop: "is_required",
label: "是否必填",
width: 150,
},
{
prop: "is_linked",
label: "是否联动",
width: 150,
},
];
const warningScopeRows = ref([]);
const getInfoData = () => {
axios
.get("/v1/api/metric_config", {
params: {
id: id,
},
})
.then((res) => {
if (res.data.code == 200) {
let {
metric_name,
is_enabled,
expr,
created_by,
created_at,
updated_at,
alert_range,
duration,
duration_unit,
check_period,
alert_rule_type,
} = res.data.data;
info.value = {
warning_target: type_name,
warning_type: target_name,
warning_index: metric_name,
status: is_enabled,
indicator_expression: expr,
created_by: created_by,
create_time: created_at,
update_time: updated_at,
};
warningScopeRows.value = alert_range || [];
advanced_data.value = {
duration: duration == 0 ? "直接产生预警" : duration + unitOptions[duration_unit],
check_period: check_period + "分钟",
};
rule_data.value = {
alert_rule_type: alert_rule_type,
};
} else {
ElMessage.error(res.data.data);
}
});
};
onBeforeMount(() => {
getRuleTypeOptions();
});
</script>
<style lang="scss" scoped>
.detail_container {
width: 100%;
height: calc(100vh - 56px);
padding: 0 24px;
min-height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
.main_container {
height: 100%;
padding: 24px;
:deep(.gap-title) {
margin-bottom: 16px;
}
.info {
max-width: 1072px;
width: 100%;
padding: 0 8px 0;
&:not(:last-child) {
padding-bottom: 24px;
}
.push-lists {
margin-top: 16px;
}
.indicator-expression {
height: 300px;
width: 100%;
:deep(.vue-ace-editor) {
margin-top: 0;
}
}
}
:deep(.bg-table) {
th > .cell,
td > .cell {
line-height: 20px;
}
}
}
}
.status {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
margin-right: 8px;
$statusObj: (
enabled: #48ad97,
disabled: #9e9e9e,
);
@each $status, $color in $statusObj {
&-#{$status} {
background-color: $color;
}
}
}
.status-body {
display: flex;
align-items: center;
}
</style>
<template>
<div class="detail_container">
<bg-breadcrumb></bg-breadcrumb>
<div class="main_container">
<div class="add-form-main bg-scroll">
<add-form ref="add_form" :row="infoData"></add-form>
</div>
<div class="add-btns">
<el-button size="default" @click="Cancle">取消</el-button>
<el-button type="primary" size="default" @click="SaveSubmit">保存</el-button>
</div>
</div>
</div>
</template>
<script setup>
import { onBeforeMount, ref } from "vue";
import bgBreadcrumb from "@/components/bg-breadcrumb.vue";
import addForm from "../modules/add-form.vue";
import { Save } from "../modules/interface.js";
import axios from "@/request/http.js";
import { ElMessage } from "element-plus";
import { useRouter, useRoute } from "vue-router";
const router = useRouter();
const route = useRoute();
const { id, class_id } = route.query;
const infoData = ref({});
const add_form = ref(null);
const SaveSubmit = async () => {
let res = await add_form.value.Submit();
if (!res) return;
Save(res, { id, class_id }, () => {
Cancle();
});
};
const Cancle = () => {
router.push(`/forewarning/indicator-config?class_id=${class_id}`);
};
const getInfoData = () => {
axios
.get("/v1/api/metric_config", {
params: {
id: id,
},
})
.then((res) => {
if (res.data.code == 200) {
let res_d = res.data.data;
infoData.value = {
name: res_d.metric_name,
indicator_expression: res_d.expr,
rule_type: res_d.alert_rule_type,
time: res_d.duration,
unit: res_d.duration_unit,
inspection_cycle: res_d.check_period,
warningScopeRows:
res_d.alert_range.map((e) => {
return {
key: e.variable_name,
input_indicator_tag: e.metric_label,
indicator_scope: e.metric_name,
indicator_tag: e.metric_label,
cname: e.chinese_name,
is_required: e.is_required ? 1 : 0,
is_linkage: e.is_linked ? 1 : 0,
};
}) || [],
state: res_d.is_enabled,
res: res_d,
};
} else {
ElMessage.error(res.data.data);
}
});
};
onBeforeMount(() => {
getInfoData();
});
</script>
<style lang="scss" scoped>
.detail_container {
width: 100%;
height: calc(100vh - 56px);
padding: 0 24px 16px;
min-height: 100%;
.main_container {
height: calc(100% - 46px);
padding: 0;
.add-form-main {
padding: 24px;
height: calc(100% - 68px);
}
.add-btns {
height: 68px;
display: flex;
align-items: center;
justify-content: flex-end;
border-top: 1px solid #ddd;
padding: 0 24px;
}
}
}
</style>
<template>
<div class="detail_container">
<bg-breadcrumb></bg-breadcrumb>
<div class="detail_container-main">
<Slide v-model="nodeData" />
<div class="main-content">
<bg-filter-group @search="changeSearch" v-model="filter.metric_name" placeholder="请输入指标名称">
<template v-slot:left_action>
<div class="apaas_button">
<el-button
type="primary"
class="add-btn"
@click="addRule"
:disabled="nodeData?.node?.level == 1"
:style="[noType ? 'cursor: not-allowed' : '']">
<bg-icon style="font-size: 12px; color: #fff; margin-right: 8px" icon="#bg-ic-add"></bg-icon>
新增
</el-button>
<el-button
type="default"
@click="batchDelete"
:disabled="nodeData?.node?.level == 1"
:style="[noType ? 'cursor: not-allowed' : '']">
批量删除
</el-button>
<span class="header_info selected-count">
已选择
<span style="color: #202531"> {{ state.selected.length }} </span>
</span>
<span
class="header_info can_click_text clear-selected"
:class="{ 'is-disabled': nodeData?.node?.level == 1 }"
@click="clearSelected">
清空
</span>
</div>
</template>
<template v-slot:filter_group>
<div class="left-filter filter_list">
<div class="filter_item">
<span class="filter_title">启用状态</span>
<el-select v-model="filter.is_enabled" placeholder="请选择" style="width: 300px">
<el-option
v-for="(item, index) in isEnabledOptions"
:key="'isEnabledOptions' + index"
:label="item.name"
:value="item.value">
</el-option>
</el-select>
</div>
<div class="filter_item">
<span class="filter_title">创建时间</span>
<el-date-picker
style="width: 400px"
v-model="filter.time"
type="datetimerange"
value-format="YYYY-MM-DD HH:mm:ss"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间" />
</div>
</div>
<div class="right-action apaas_button">
<el-button type="primary" @click="filterAction"> 查询 </el-button>
<el-button type="default" @click="filterClear"> 重置 </el-button>
</div>
</template>
</bg-filter-group>
<div class="table_container">
<div class="table bg-scroll">
<bg-table
ref="dataTable"
:headers="headers"
:rows="tableRows"
@selectAc="selectRows"
:selectable="selectable"
:isIndex="true"
:select="true"
:stripe="true">
<template v-slot:metric_name="{ row }">
<span class="can_click_text" @click="goDetail(row)">
{{ row.metric_name }}
</span>
</template>
<template #status="{ row }">
<el-switch
v-model="row.status"
active-text="是"
inactive-text="否"
inline-prompt
@change="stateChange(row)">
</el-switch>
<!-- <bg-switch
@click="stateChange(row)"
:labels="['否', '是']"
:values="[2, 1]"
v-model="row.is_enabled"></bg-switch> -->
</template>
<template v-slot:created_at="{ row }">
{{ dateStringToDate(row.created_at) }}
</template>
<template v-slot:action="{ row }">
<bg-table-btns2 :limit="3" :tableData="tableRows">
<bg-table-btn :disabled="row.is_enabled != 2" @click="editRow(row)">编辑</bg-table-btn>
<bg-table-btn :disabled="row.is_enabled != 2" @click="deleteRow(row)">删除</bg-table-btn>
</bg-table-btns2>
</template>
</bg-table>
</div>
<bg-pagination
:page="filter.page"
:size="filter.size"
:total="tableTotal"
@change-page="changePage"
@change-size="changeSize">
</bg-pagination>
</div>
</div>
</div>
<!-- 删除 -->
<el-dialog class="dialog_box" title="删除" v-model="delDialog" width="420px">
<div>确定要删除吗?</div>
<template v-slot:footer>
<div class="apaas_button">
<el-button type="default" @click="delDialog = false">取消</el-button>
<el-button type="primary" @click="delConfirm">确定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import bgBreadcrumb from "@/components/bg-breadcrumb.vue";
import { reactive, ref, onBeforeMount, toRefs, computed, watch, nextTick, watchEffect } from "vue";
import Slide from "./modules/slide.vue";
import { ElMessage } from "element-plus";
import axios from "@/request/http.js";
import { useRouter } from "vue-router";
import { dateStringToDate } from "@/components/env";
const router = useRouter();
const nodeData = ref({});
const noType = computed(() => {
return !nodeData.value?.data?.class_id;
});
const active_node_id = ref("");
watch(
() => nodeData.value,
(n) => {
if (n.node.level == 1) return;
if (n?.data.class_id && active_node_id.value != n?.data.class_id) {
getTableRows();
active_node_id.value = n.data.class_id;
}
},
{ deep: true }
);
const dataTable = ref(null);
const state = reactive({
isEnabledOptions: [
{
name: "全部",
value: "",
},
{
name: "启用",
value: 1,
},
{
name: "禁用",
value: 2,
},
], // 状态
headers: [
{
label: "指标名称",
prop: "metric_name",
width: 200,
},
{
label: "是否启用",
prop: "status",
},
{
label: "创建人",
prop: "created_by",
},
{
label: "创建时间",
prop: "created_at",
width: 160,
},
{
label: "操作",
prop: "action",
width: 136,
fixed: "right",
},
],
filter: {
notice_method: "", // 通知方式
is_enabled: "", // 状态
time: [],
metric_name: "",
page: 1,
page_size: 10,
},
tableRows: [], // 表格数据
selected: [], //选择数据
tableTotal: 0, // 表格数据条数
actionRow: null, // 当前操作的数据
delDialog: false, // 删除弹窗
delType: 0, // 1-单条删除 2-批量删除
});
const changePage = (page) => {
state.filter.page = page;
getTableRows();
}; // 改变页码
const changeSize = (size) => {
state.filter.page_size = size;
changePage(1);
}; // 改变每页条数
const changeSearch = (val) => {
state.filter.metric_name = val;
changePage(1);
};
const filterAction = () => {
changePage(1);
};
const filterClear = () => {
state.filter = {
is_enabled: "", // 状态
time: [],
metric_name: "",
page: 1,
page_size: 10,
};
changePage(1);
};
const selectable = (row, index) => {
return row.is_enabled === 2;
};
const getTableRows = () => {
const [start_time = "", end_time = ""] = state.filter.time || [];
let params = { ...state.filter, class_id: nodeData.value.data.class_id, start_time, end_time };
Reflect.deleteProperty(params, "time");
axios.get("/v1/api/metric_config/list", { params }).then((res) => {
if (res.data.code == 200) {
state.tableRows =
res.data.data?.list.map((e) => {
return {
...e,
status: e.is_enabled == 1,
};
}) || [];
state.tableTotal = res.data.data.total_count;
}
});
};
const goDetail = (row) => {
router.push({
path: "/forewarning/indicator-config/detail",
query: {
id: row.id,
class_id: node.value.data.class_id,
type_name: node.value.node.parent.data.class_name,
target_name: node.value.data.class_name,
},
});
}; // 查看详情
const selectRows = (data) => {
state.selected = data.selection;
};
const deleteRow = (row) => {
state.actionRow = row;
state.delType = 1;
state.delDialog = true;
// console.log("删除");
}; // 删除
const batchDelete = () => {
// console.log("批量删除");
if (noType.value) return;
if (!state.selected || state.selected.length == 0) {
ElMessage.error("请先勾选要删除的数据");
return;
}
state.delType = 2;
state.delDialog = true;
}; // 批量删除
const delConfirm = () => {
let ids = [];
if (state.delType == 1) {
ids.push(state.actionRow.id);
} else {
ids = state.selected.map((e) => {
return e.id;
});
}
// console.log(ids);
state.delDialog = false;
axios
.delete("/v1/api/metric_config", {
data: {
ids,
},
})
.then((res) => {
if (res.data.code == 200) {
ElMessage.success("删除成功");
changePage(1);
clearSelected();
} else {
ElMessage.error(res.data.msg);
}
});
}; // 确定删除
const clearSelected = () => {
if (nodeData.value.node.level == 1) return;
dataTable.value.clearTable();
}; // 清空
const addRule = () => {
// console.log("新增");
if (noType.value) return;
router.push({
path: `/forewarning/indicator-config/add`,
query: {
class_id: nodeData.value.data?.class_id,
},
});
}; // 新增规则
const editRow = (row) => {
// console.log("编辑");
router.push({
path: `/forewarning/indicator-config/edit`,
query: {
id: row.id,
class_id: nodeData.value.data.class_id,
},
});
}; // 编辑
const stateChange = async (row) => {
await nextTick();
const params = {
id: row.id,
is_enabled: row.status ? 1 : 2,
};
axios
.put("/v1/api/metric_config", params)
.then((res) => {
if (res.data.code == 200) {
ElMessage.success(`状态更新成功`);
getTableRows();
} else {
row.status = !row.status;
ElMessage.error(res.data.msg);
}
})
.catch((e) => {
row.status = !row.status;
});
};
const { headers, tableRows, tableTotal, filter, noticeTypes, isEnabledOptions, delDialog } = toRefs(state);
</script>
<style lang="scss" scoped>
.detail_container {
width: 100%;
height: calc(100vh - 56px);
padding: 0 24px;
min-height: 100%;
&-main {
width: 100%;
height: calc(100% - 46px);
display: flex;
align-items: center;
padding-bottom: 16px;
}
.main-content {
height: 100%;
width: calc(100% - 336px);
background-color: #fff;
box-shadow: 0 1px 4px 0 rgba(0, 7, 101, 0.15);
border-radius: 6px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
.filter-group {
.left-filter {
flex: 1;
display: flex;
justify-content: start;
flex-wrap: wrap;
}
.right-action {
width: 144px;
padding-bottom: 16px;
.el-button {
width: 64px;
}
}
}
.selected-count {
color: #404a62;
font-size: 14px;
margin: 0 24px 0 40px;
}
.clear-selected {
color: #3759be;
font-size: 14px;
cursor: pointer;
&.is-disabled {
color: #c0c4cc;
cursor: not-allowed;
}
}
.table_container {
flex: 1;
width: 100%;
padding: 0 16px;
position: relative;
.table {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: calc(100% - 64px);
}
.bg-pagination {
height: 64px;
position: absolute;
right: 0;
bottom: 0;
padding: 16px;
margin-top: 0;
}
}
}
}
.add-btn.is-disabled {
&,
&:hover {
background-image: none;
background-color: rgb(149, 163, 202);
border-color: rgb(149, 163, 202);
}
}
</style>
<template>
<div class="add-form">
<el-form :model="state.form" ref="form_ref" :rules="state.rules" label-width="120px">
<div class="add-form-item">
<el-form-item label="指标名称" prop="name">
<el-input v-model="state.form.name" :disabled="isEdit" placeholder="请输入指标名称"></el-input>
</el-form-item>
<el-form-item label="指标表达式" prop="indicator_expression">
<div class="indicator-expression">
<bg-code-editor v-model="state.form.indicator_expression"></bg-code-editor>
<!-- <bg-codemirror v-model="state.form.indicator_expression"></bg-codemirror> -->
</div>
</el-form-item>
<el-form-item label="预警范围" prop="">
<warningScope
:indicator_expression="state.form.indicator_expression"
ref="table_form_ref"
:warningScopeRows="warningScopeRows" />
</el-form-item>
<el-form-item label="预警规则类型" prop="rule_type">
<el-select style="flex: 1" v-model="state.form.rule_type" placeholder="请选择" filterable>
<el-option v-for="(value, key) in ruleTypeOptions" :key="key" :label="value.label" :value="key">
</el-option>
</el-select>
</el-form-item>
<div class="duration">
<el-form-item label="持续时间" prop="time">
<el-input v-model="state.form.time" placeholder="请输入持续时间" @input="inputNum">
<template #prepend>
<span>当预警持续</span>
</template>
<template #append>
<el-select v-model="state.form.unit" placeholder="请选择">
<el-option v-for="(value, key) in unitOptions" :key="key" :label="value.label" :value="key">
</el-option>
</el-select>
</template>
</el-input>
<span class="duration-append">
<span> 时产生报警 </span>
<el-popover
:width="300"
title=""
content="大于等于0的正整数,等于0时代表直接报警"
trigger="hover"
placement="top-start">
<template #reference>
<bg-icon
style="font-size: 12px; color: #a9b1c7; margin-left: 8px; vertical-align: middle"
icon="#bg-ic-s-circle-tips"></bg-icon>
</template>
</el-popover>
</span>
</el-form-item>
</div>
<el-form-item label="检查周期" prop="inspection_cycle">
<div style="flex: 1; display: flex; align-items: center; gap: 8px">
<el-select style="flex: 1" v-model="state.form.inspection_cycle" placeholder="请选择检查周期">
<el-option v-for="item in inspectionCycleOptions" :key="item" :label="item" :value="item"> </el-option>
</el-select>
<span>分钟</span>
</div>
</el-form-item>
<el-form-item label="是否立即启用">
<el-switch
class="bg-switch-ele"
v-model="state.form.state"
:active-value="1"
:inactive-value="0"
inline-prompt
active-text="是"
inactive-text="否" />
</el-form-item>
</div>
</el-form>
</div>
</template>
<script setup>
import { reactive, ref, computed, watch, onMounted } from "vue";
import gapTitle from "@/components/gap-title.vue";
import ManualDistributionForm from "@/components/manual-distribution/form.vue";
import { MAX_DAY, ONLY_INPUT_NUM } from "@/components/env.js";
import { ElMessage } from "element-plus";
import axios from "@/request/http.js";
import warningScope from "./warning-scope.vue";
import { GetRuleTypeOptions, Empty } from "@/components/env.js";
const props = defineProps({
row: {
type: Object,
default: null,
},
});
// 当前是否是编辑
const isEdit = computed(() => !!props.row);
// 获取旧数据
const warningScopeRows = computed(() => props.row?.warningScopeRows || []);
// 持续时间单位
const unitOptions = {
s: { label: "", max: MAX_DAY * 24 * 3600 },
m: { label: "分钟", max: MAX_DAY * 24 * 60 },
h: { label: "小时", max: MAX_DAY * 24 },
};
const state = reactive({
form: {
name: "",
indicator_expression: "",
rule_type: "",
time: 10,
unit: "s",
inspection_cycle: 1,
state: 1,
},
rules: {
name: [{ required: true, message: "请输入指标名称", trigger: "blur" }],
rule_type: [{ required: true, message: "请选择预警规则类型", trigger: "change" }],
indicator_expression: [{ required: true, message: "请输入指标表达式", trigger: "blur" }],
time: [{ required: true, message: "请输入持续时间", trigger: "blur" }],
},
});
// 预警规则类型下拉
const ruleTypeOptions = ref({});
const getRuleTypeOptions = async () => {
ruleTypeOptions.value = await GetRuleTypeOptions();
};
// 当预警持续输入限制
const inputNum = () => {
state.form.time = state.form.time.replace(/[^\d]/g, "");
let time = +state.form.time;
let { max } = unitOptions[state.form.unit];
if (time > +max) {
state.form.time = max;
ElMessage.closeAll();
ElMessage.error(`最大值:${max}`);
}
};
// 检查周期下拉数据
const inspectionCycleOptions = ref([1, 3, 5, 10, 20]);
// form表单元素
const form_ref = ref(null);
// table的form表单元素
const table_form_ref = ref(null);
// 提交调用
const Submit = async () => {
let form_valid = await new Promise((resolve, reject) => {
form_ref.value.validate((res) => resolve(res));
});
let table_form_valid = await table_form_ref.value.Submit();
if (form_valid && table_form_valid) {
let obj = {
...state.form,
...table_form_ref.value?.form,
};
if (!isEdit.value) {
setTimeout(() => {
state.form.indicator_expression = "";
table_form_ref.value.form_ref.resetFields();
form_ref.value.resetFields();
}, 100);
}
return obj;
}
return;
};
watch(
() => state.form.indicator_expression,
(n) => {
form_ref.value?.validateField(["indicator_expression"]);
}
);
// 监听是否是编辑,是则插入原数据
watch(
() => props.row,
async (n) => {
if (!n) return;
state.form.name = n.name;
state.form.rule_type = n.rule_type;
state.form.inspection_cycle = n.inspection_cycle || 1;
state.form.state = n.state || 1;
state.form.time = n.time || 10;
state.form.unit = n.unit || "s";
state.form.indicator_expression = n.indicator_expression;
},
{ deep: true, immediate: true }
);
// 获取第一级指标范围列表
onMounted(() => {
getRuleTypeOptions();
});
defineExpose({
Submit,
});
</script>
<style lang="scss" scoped>
.add-form {
:deep(.gap-title) {
margin-bottom: 16px;
}
&-item {
max-width: 1080px;
width: 100%;
.indicator-expression {
height: 300px;
width: 100%;
:deep(.vue-ace-editor) {
margin-top: 0;
}
}
.duration {
flex: 1;
display: flex;
align-items: center;
:deep(.el-form-item) {
flex: 1;
.el-input {
margin-right: 8px;
flex: 1;
}
.el-input-group__prepend {
width: 102px;
border-radius: 4px;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.el-input-group__append {
width: 80px;
border-radius: 4px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
// :deep(.el-form-item__content) {
// display: flex;
// align-items: center;
// gap: 8px;
// > .el-input {
// flex: 1;
// width: 80px;
// }
// }
}
.duration-append {
display: flex;
align-items: center;
color: #404a62;
}
:deep(.el-switch__inner) {
.is-hide {
display: none;
}
}
:deep(.el-input-group__append, .el-input-group__prepend) {
border-radius: 4px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
}
</style>
<style lang="scss">
.el-form-item {
min-height: 36px;
label {
height: 36px;
line-height: 36px;
}
.el-input__inner {
min-height: 34px;
}
}
</style>
import { ElMessage } from "element-plus";
import axios from "@/request/http.js";
const setParams = (res, { id, class_id }) => {
let params = {
class_id: +class_id,
metric_name: res.name,
expr: res.indicator_expression,
alert_range:
res.warningScopeRows.map((e) => {
return {
variable_name: e.key,
metric_name: e.indicator_scope,
metric_label: e.indicator_tag,
chinese_name: e.cname,
is_required: e.is_required == 1,
is_linked: e.is_linkage == 1,
};
}) || [],
duration: +res.time,
duration_unit: res.unit,
check_period: res.inspection_cycle,
is_enabled: res.state,
alert_rule_type: res.rule_type,
};
if (id) {
params.id = id;
}
return params;
};
export const Save = (res, p, cb) => {
let params = setParams(res, p);
axios[p.id ? "put" : "post"]("/v1/api/metric_config", params).then((res) => {
if (res.data.code == 200) {
ElMessage.success(`${p.id ? "编辑" : "新增"}成功`);
cb && cb();
} else {
ElMessage.error(res.data.data);
}
});
};
<template>
<div class="main-slide">
<div class="slide-title">预警对象</div>
<div class="slide-search">
<el-input v-model="search" placeholder="请输入关键字" clearable @input="Search" @clear="Search">
<template #prefix>
<bg-icon style="font-size: 12px; color: #404a62" icon="#bg-ic-search"></bg-icon>
</template>
</el-input>
<el-button type="primary" size="default" @click="Add">
<bg-icon style="font-size: 12px; color: #fff" icon="#bg-ic-add"></bg-icon>
</el-button>
</div>
<div class="slide-tree bg-scroll">
<el-tree
ref="treeRef"
default-expand-all
:data="slideTree"
:current-node-key="selectId"
node-key="class_id"
empty-text="暂无数据"
:props="treeProps"
:highlight-current="true"
:expand-on-click-node="false"
:filter-node-method="filterNode"
@node-click="treeNodeChoose">
<template #default="{ node, data }">
<span class="custom-tree-node">
<span>{{ node.label }}</span>
<el-popover popper-class="tree-operation" placement="right-start" :width="120" trigger="click">
<template #reference>
<span class="operation" @click.stop="">
<bg-icon style="font-size: 12px; color: #404a62" icon="#bg-ic-s-more"></bg-icon>
</span>
</template>
<template #default>
<ul class="operation-lists">
<li v-for="(value, key) in operations" :key="key" @click.stop="Operation(key, node, data)">
<el-button link :disabled="value.disabled(node, data)">{{ value.label(node) }}</el-button>
</li>
</ul>
</template>
</el-popover>
</span>
</template>
</el-tree>
</div>
</div>
<el-dialog :close-on-click-modal="false" v-model="addWarnType" width="558px" :before-close="Cancel">
<template #header>
<GapTitle :title="isEditWarnType ? '编辑预警分类' : '新增预警分类'"></GapTitle>
</template>
<div style="padding-top: 20px">
<el-form :model="state.form" ref="add_warn_type_form" :rules="state.rules" label-width="80px">
<el-form-item label="预警分类" prop="name">
<el-input v-model="state.form.name" placeholder="请输入预警分类" maxlength="20" show-word-limit></el-input>
</el-form-item>
</el-form>
</div>
<template #footer>
<el-button size="default" @click="Cancel">取消</el-button>
<el-button type="primary" size="default" @click="Save"> 保存 </el-button>
</template>
</el-dialog>
<el-dialog :close-on-click-modal="false" v-model="addWarnTarget" width="558px" :before-close="CancelAddWarnTarget">
<template #header>
<GapTitle :title="isEditWarnType ? '编辑预警对象' : '新增预警对象'"></GapTitle>
</template>
<div style="padding-top: 20px">
<el-form :model="state.form_target" ref="add_warn_target_form" :rules="state.target_rules" label-width="80px">
<el-form-item label="预警分类" prop="type">
<el-select style="flex: 1" v-model="state.form_target.type" placeholder="请选择预警分类" filterable>
<el-option v-for="item in slideTree" :key="item.class_id" :label="item.class_name" :value="item.class_id">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="预警对象" prop="target">
<el-input
v-model="state.form_target.target"
placeholder="请输入预警对象"
maxlength="20"
show-word-limit></el-input>
</el-form-item>
</el-form>
</div>
<template #footer>
<el-button size="default" @click="CancelAddWarnTarget">取消</el-button>
<el-button type="primary" size="default" @click="SaveAddWarnTarget"> 保存 </el-button>
</template>
</el-dialog>
<el-dialog v-model="delWarnType" width="400px" :before-close="CancelDel">
<template #header>
<GapTitle :title="`删除预警${activeTree.node.level == 1 ? '分类' : '对象'}`"></GapTitle>
</template>
<div style="padding: 20px 0">
是否确定删除 {{ activeTree.own.label }} 这个预警{{ activeTree.node.level == 1 ? "分类" : "对象" }}
</div>
<template #footer>
<el-button size="default" @click="CancelDel">取消</el-button>
<el-button type="primary" size="default" @click="ConfirmDel"> 确定 </el-button>
</template>
</el-dialog>
</template>
<script setup>
import { nextTick, onBeforeMount, reactive, ref } from "vue";
import GapTitle from "@/components/gap-title.vue";
import axios from "@/request/http.js";
import { ElMessage } from "element-plus";
import { useRoute } from "vue-router";
const route = useRoute();
const { class_id } = route.query;
const props = defineProps({
modelValue: {
type: Object,
default: () => ({}),
},
});
const search = ref("");
let slideTree = ref([]);
const treeProps = {
// 组织树获取数据规则
children: "children",
label: "class_name",
};
const treeRef = ref(null);
const selectId = ref("");
const getSlideTree = () => {
axios.get("/v1/api/alert_class/tree").then((res) => {
if (res.data.code == 200) {
slideTree.value = res.data.data;
async function nodeChoose(item) {
await nextTick();
treeRef.value.setCurrentKey(item.class_id);
const node = treeRef.value.getNode(item);
if (node) {
treeNodeChoose(item, node);
}
}
if (class_id) {
let item = null;
slideTree.value.forEach((e) => {
if (e.children && e.children.length > 0) {
e.children.forEach((e) => {
if (e.class_id == class_id) {
item = e;
}
});
}
});
nodeChoose(item);
return;
}
try {
slideTree.value.forEach((e) => {
if (e.children && e.children.length > 0) {
throw e.children[0];
}
});
} catch (item) {
nodeChoose(item);
}
} else {
ElMessage.error(res.data.msg);
}
});
};
const filterNode = (value, data) => {
if (!value) return data;
return data.class_name.includes(value);
};
const Search = async () => {
await nextTick();
treeRef.value.filter(search.value);
};
const Add = () => {
addWarnType.value = true;
};
const emits = defineEmits(["update:modelValue"]);
const treeNodeChoose = (data, node) => {
emits("update:modelValue", { data, node });
};
const getBrotherNodes = (node) => {
let arr = [];
if (node.level == 1) {
arr = slideTree.value;
} else {
arr = node.parent.data.children;
}
return arr;
};
const operations = {
1: {
label: (node) => {
return node.level == 1 ? "编辑预警分类" : "编辑预警对象";
},
disabled: () => {
return false;
},
},
2: {
label: () => "新增预警对象",
disabled: () => {
return false;
},
},
3: {
label: () => "删除",
disabled: () => {
return false;
},
},
4: {
label: () => "上移",
disabled: (node, data) => {
let arr = getBrotherNodes(node);
let id = data.class_id;
let i = arr.findIndex((e) => e.class_id == id);
return 0 == i;
},
},
5: {
label: () => "下移",
disabled: (node, data) => {
let arr = getBrotherNodes(node);
let id = data.class_id;
let i = arr.findIndex((e) => e.class_id == id);
return arr.length - 1 == i;
},
},
};
const addWarnType = ref(false);
const addWarnTarget = ref(false);
const delWarnType = ref(false);
const isEditWarnType = ref(false);
const activeTree = ref({});
const Operation = (key, node, data) => {
activeTree.value = {
node,
own: data,
data: node.level == 1 ? data : node.parent.data,
};
let operation_handled_fn = {
1: async () => {
if (node.level == 1) {
addWarnType.value = true;
await nextTick();
state.form.name = data.class_name;
} else {
addWarnTarget.value = true;
await nextTick();
state.form_target.type = activeTree.value.data.class_id;
state.form_target.target = data.class_name;
}
isEditWarnType.value = true;
},
2: () => {
addWarnTarget.value = true;
isEditWarnType.value = false;
state.form_target.type = activeTree.value.data.class_id;
},
3: () => {
delWarnType.value = true;
},
4: () => {
move(data.class_id, "up");
},
5: () => {
move(data.class_id, "down");
},
};
operation_handled_fn[key]();
};
const move = (class_id, type) => {
const params = {
class_id,
};
axios.put(`/v1/api/alert_class/move/${type}`, params).then((res) => {
if (res.data.code == 200) {
let text = {
up: "上移",
down: "下移",
};
ElMessage.success(`${text[type]}成功`);
getSlideTree();
} else {
ElMessage.error(res.data.msg);
}
});
};
const add_warn_type_form = ref(null);
const add_warn_target_form = ref(null);
const state = reactive({
form: {
name: "",
},
form_target: {
type: "",
target: "",
},
rules: {
name: [{ required: true, message: "请输入预警分类", trigger: "blur" }],
},
target_rules: {
type: [{ required: true, message: "请选择预警分类", trigger: "change" }],
target: [{ required: true, message: "请输入预警对象", trigger: "blur" }],
},
});
const Cancel = async () => {
add_warn_type_form.value.resetFields();
await nextTick();
addWarnType.value = false;
};
const Save = () => {
add_warn_type_form.value.validate((valid) => {
if (valid) {
const params = {
class_name: state.form.name,
parent_id: 0,
};
if (isEditWarnType.value) {
params.class_id = activeTree.value.own.class_id;
}
let method = isEditWarnType.value ? "put" : "post";
axios[method]("/v1/api/alert_class", params).then((res) => {
if (res.data.code == 200) {
ElMessage.success("预警分类新建成功");
Cancel();
getSlideTree();
} else {
ElMessage.error(res.data.msg);
}
});
} else {
return false;
}
});
};
const CancelAddWarnTarget = async () => {
add_warn_target_form.value.resetFields();
await nextTick();
addWarnTarget.value = false;
};
const SaveAddWarnTarget = () => {
add_warn_target_form.value.validate((valid) => {
if (valid) {
const params = {
class_name: state.form_target.target,
parent_id: state.form_target.type,
};
if (isEditWarnType.value) {
params.class_id = activeTree.value.own.class_id;
}
let method = isEditWarnType.value ? "put" : "post";
axios[method]("/v1/api/alert_class", params).then((res) => {
if (res.data.code == 200) {
ElMessage.success(`预警对象${isEditWarnType.value ? "编辑" : "新建"}成功`);
CancelAddWarnTarget();
getSlideTree();
} else {
ElMessage.error(res.data.msg);
}
});
} else {
return false;
}
});
};
const CancelDel = () => {
delWarnType.value = false;
};
const ConfirmDel = () => {
axios
.delete("/v1/api/alert_class", {
data: {
class_id: activeTree.value.own.class_id,
},
})
.then((res) => {
if (res.data.code == 200) {
ElMessage.success("删除成功");
CancelDel();
getSlideTree();
} else {
ElMessage.error(res.data.msg);
}
});
};
onBeforeMount(() => {
getSlideTree();
});
</script>
<style lang="scss" scoped>
.main-slide {
width: 320px;
height: 100%;
background-color: #fff;
box-shadow: 0 1px 4px 0 rgba(0, 7, 101, 0.15);
border-radius: 6px;
margin-right: 16px;
overflow: hidden;
.slide-title {
height: 40px;
background-color: #f7f7f9;
display: flex;
align-items: center;
padding-left: 16px;
}
.slide-search {
padding: 16px;
display: flex;
align-items: center;
:deep(.el-button) {
margin-left: 8px;
}
}
.slide-tree {
height: calc(100% - 108px);
:deep(.el-tree-node__content) {
height: 36px;
}
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
overflow: hidden;
&:hover {
.operation {
visibility: visible;
}
}
.operation {
display: flex;
visibility: hidden;
width: 36px;
height: 36px;
align-items: center;
justify-content: center;
box-shadow: -4px 0 4px 0px rgba(0, 7, 101, 0.15);
}
}
}
}
.add-warn-type-form {
padding-top: 20px;
}
</style>
<style lang="scss">
.tree-operation {
padding: 4px 0 !important;
min-width: 0 !important;
.operation-lists {
li {
line-height: 34px;
cursor: pointer;
transition: all 300ms;
.el-button {
width: 100%;
height: 34px;
justify-content: flex-start;
padding: 0 16px;
}
&:hover {
background-color: #f2f3f7;
.el-button {
&:not(.is-disabled) {
color: #3759be;
}
}
}
}
}
}
</style>
<template>
<el-form :model="state.form.warningScopeRows" ref="form_ref" :rules="tableRules" label-width="0" style="width: 100%">
<el-table :data="state.form.warningScopeRows" stripe border empty-text="请输入表达式,根据表达式$...$自动生成表格">
<el-table-column
v-for="header in warningScopeHeaders"
:prop="header.prop"
:key="header.prop"
:label="header.label"
:width="header.width">
<template #default="{ $index }">
<div v-if="header.prop == 'indicator_tag'">
<el-form-item :prop="`[${$index}].input_indicator_tag`" :rules="tableRules.input_indicator_tag">
<el-input
v-model="state.form.warningScopeRows[$index].input_indicator_tag"
placeholder="请选择指标标签"
ref="inputRef"
@blur="blurInput($index)"
@focus="inputLabel($index, false)"
@input="inputLabel($index, true)"></el-input>
</el-form-item>
</div>
<div v-else-if="header.prop == 'cname'">
<el-form-item :prop="`[${$index}].cname`" :rules="tableRules.cname">
<el-input
style="flex: 1"
v-model="state.form.warningScopeRows[$index].cname"
:maxlength="20"
show-word-limit
placeholder="请输入中文名称">
</el-input>
</el-form-item>
</div>
<div v-else-if="header.prop == 'is_required'">
<el-form-item :prop="`[${$index}].is_required`" :rules="tableRules.is_required">
<el-select v-model="state.form.warningScopeRows[$index].is_required" placeholder="请选择" style="flex: 1">
<el-option v-for="(value, key) in tableSelOptions" :key="key" :label="value" :value="key"> </el-option>
</el-select>
</el-form-item>
</div>
<div v-else-if="header.prop == 'is_linkage'">
<el-form-item :prop="`[${$index}].is_linkage`" :rules="tableRules.is_linkage">
<el-select v-model="state.form.warningScopeRows[$index].is_linkage" placeholder="请选择" style="flex: 1">
<el-option v-for="(value, key) in tableSelOptions" :key="key" :label="value" :value="key"> </el-option>
</el-select>
</el-form-item>
</div>
</template>
</el-table-column>
</el-table>
<el-popover
v-if="inputRef?.length > 0"
:virtual-ref="inputRef?.[active_index]"
:visible="visible"
popper-class="cascader-operation"
placement="bottom-start">
<div class="filter-first-tree-lists" @mousedown.prevent="" v-if="active_row">
<ul class="bg-scroll" ref="first_options_lists" v-show="firstOptions.length > 0">
<li
v-for="data in firstOptions"
:key="data"
:class="{
active: active_row.indicator_scope == data.label,
}">
<div class="first-label-main" @click="chooseTreeFirst(active_index, data)">
<div class="tree-label" v-html="data.label_html"></div>
<div class="tree-icon">
<bg-icon
style="font-size: 15px; color: #404a62"
class="is-loading"
:class="{ 'loading-show': data.isLoading }"
icon="#bg-ic-loading"></bg-icon>
<bg-icon style="font-size: 13px; color: #404a62" icon="#bg-ic-arrow-right"></bg-icon>
</div>
</div>
</li>
</ul>
<ul v-show="firstOptions.length == 0">
<li class="no-data">
{{
active_row.input_indicator_tag
? firstOptionsLoading
? "查询中"
: "未查询到相关指标范围数据"
: "请输入查询"
}}
</li>
</ul>
<ul class="bg-scroll" v-show="lastOptions?.length > 0">
<li
v-for="child in lastOptions"
:key="child"
@click="chooseTreeLast(child, active_index)"
:class="{ active: active_row.indicator_tag == child }">
{{ child }}
</li>
</ul>
<ul v-show="active_row.indicator_scope && lastOptions?.length == 0">
<li class="no-data">
{{ lastOptionsLoading ? "查询中" : "未查询到相关指标标签" }}
</li>
</ul>
</div>
</el-popover>
</el-form>
</template>
<script setup>
import { reactive, watch, nextTick, computed, ref, onMounted } from "vue";
import { ElMessage } from "element-plus";
import axios from "@/request/http.js";
const props = defineProps({
indicator_expression: {
type: String,
default: "",
},
warningScopeRows: {
type: Array,
default: () => [],
},
});
// 是否是必填和是否联动下拉数据
const tableSelOptions = ["", ""];
// 表格表头
const warningScopeHeaders = [
{
prop: "key",
label: "变量名称",
width: 100,
},
{
prop: "indicator_tag",
label: "指标标签",
},
{
prop: "cname",
label: "中文名称",
},
{
prop: "is_required",
label: "是否必填",
width: 150,
},
{
prop: "is_linkage",
label: "是否联动",
width: 150,
},
];
// 表单数据
const state = reactive({
form: {
warningScopeRows: [],
},
});
// 表单验证
const tableRules = {
input_indicator_tag: [{ required: true, message: "请输入", trigger: "blur" }],
cname: [{ required: true, message: "请输入", trigger: "blur" }],
};
// 获取到的第一级指标范围基础数据集合
const dataTreeDefault = ref([]);
// 是否是输入触发
const isInput = ref(false);
const first_options_lists = ref(null);
// 指标输入框元素集合
const inputRef = ref(null);
// 监听父组件传过来的指标表达式
watch(
() => props.indicator_expression,
async (n) => {
await nextTick();
let tableCreateKeys = n?.match(/\$(.+?)\$/g) || [];
let arr = [];
tableCreateKeys.forEach((e) => {
let i = state.form.warningScopeRows.findIndex((el) => el.key == e);
if (i >= 0) {
arr[i] = state.form.warningScopeRows[i];
} else {
arr.push({
key: e,
input_indicator_tag: "",
indicator_scope: "",
indicator_tag: "",
oldQuery: "",
cname: "",
is_required: 1,
is_linkage: 0,
});
}
});
state.form.warningScopeRows = arr;
}
);
// 当前操作input下标
const active_index = ref("");
// 计算:当前操作input所在行数据源
const active_row = computed(() => {
if (active_index.value === "") return null;
return state.form.warningScopeRows[active_index.value];
});
// 下拉是否可见
const visible = ref(false);
// 第一层下拉
const firstOptions = ref([]);
// 第一层下拉加载动画
const firstOptionsLoading = ref(false);
// 最后一层下拉
const lastOptions = ref([]);
// 最后一层的下拉加载动画
const lastOptionsLoading = ref(false);
// 计算:根据聚焦的input 获取当前查询的关键词
const query = computed(() => {
return (index) => {
const { input_indicator_tag, oldQuery } = state.form.warningScopeRows[index];
return inputType.value ? input_indicator_tag : oldQuery;
};
});
// 选择第一级,获取第二级数据
const chooseTreeFirst = (index, data, isInfo = false) => {
lastOptionsLoading.value = true;
let i = "";
// 是否是父组件传过来的默认展示数据
if (!isInfo) {
inputRef.value[index].focus();
i = firstOptions.value.findIndex((e) => e.label == data.label);
firstOptions.value[i].isLoading = true;
state.form.warningScopeRows[index].indicator_tag = "";
state.form.warningScopeRows[index].indicator_scope = data.label;
lastOptions.value = [];
}
const params = {
label_name: data.label,
};
axios
.get("/v1/api/prometheus", { params })
.then((res) => {
if (res.data.code == 200) {
lastOptions.value = res.data.data?.list || [];
} else {
ElMessage.error(res.data.msg);
}
})
.finally(() => {
lastOptionsLoading.value = false;
if (i === "") return;
firstOptions.value[i].isLoading = false;
});
};
// 获取第一级数据
const getDataTreeLevelFirst = () => {
axios.get("/v1/api/prometheus").then((res) => {
if (res.data.code == 200) {
dataTreeDefault.value =
res.data.data.list.map((e) => {
return {
label: e,
isLoading: false,
};
}) || [];
} else {
ElMessage.error(res.data.msg);
}
});
for (let i = 0; i < 1000; i++) {
dataTreeDefault.value.push({
label: "total" + i,
value: "total" + i,
});
}
};
let timer = null;
const inputType = ref(false);
const TreeFilter = async (index) => {
const { input_indicator_tag } = active_row.value;
// 如果没有输入,或者清空输入,则清除已选中的所有东西和所有下拉数据,并返回
if (input_indicator_tag == "") {
lastOptions.value = [];
state.form.warningScopeRows[index].indicator_scope = "";
state.form.warningScopeRows[index].indicator_tag = "";
state.form.warningScopeRows[index].oldQuery = "";
return;
}
let q = query.value(index);
// 提升性能问题使用for循环遍历
for (let i = 0; i < dataTreeDefault.value.length; i++) {
// 不符合直接跳出当次循环
if (!dataTreeDefault.value[i].label.includes(q)) continue;
firstOptions.value.push({
...dataTreeDefault.value[i],
// 高亮显示和输入匹配的项
label_html: dataTreeDefault.value[i].label.replaceAll(q, `<span class='mate-str'>${q}</span>`),
});
}
// 如果第一级下拉没有数据,则隐藏第二级下拉,并清除当前现在的指标范围
if (firstOptions.value.length == 0) {
lastOptions.value = [];
state.form.warningScopeRows[index].indicator_scope = "";
}
// 结束查询中
firstOptionsLoading.value = false;
// 查询后第一级滚动条滚动到顶部
first_options_lists.value.scrollTop = 0;
};
// 输入模糊查询数据
const inputLabel = async (index, type) => {
visible.value = false;
firstOptions.value = [];
await nextTick();
active_index.value = index;
// 记录当前是聚焦还是输入
inputType.value = type;
visible.value = true;
// 第一级显示查询中
firstOptionsLoading.value = true;
// 用户输入节流
if (timer) {
clearTimeout(timer);
timer = null;
}
timer = setTimeout(
() => {
// 过滤下拉数据
TreeFilter(index);
},
// 如果是聚集input的时候不需要节流,输入节流
type ? 500 : 0
);
};
// 失去焦点是隐藏popper
const blurInput = (index) => {
// 判断是聚焦失去焦点还是输入后失去焦点,输入后失去焦点记录输入的内容,聚焦失去焦点还是使用之前的记录
state.form.warningScopeRows[index].oldQuery = query.value(index);
// 隐藏popper
visible.value = false;
};
// 选择范围中的属性
const chooseTreeLast = (last, index) => {
state.form.warningScopeRows[index].input_indicator_tag = last;
state.form.warningScopeRows[index].indicator_tag = last;
isInput.value = false;
inputRef.value[index].blur();
};
// form表单元素
const form_ref = ref(null);
// 提交操作调用
const Submit = async () => {
let form_valid = await new Promise((resolve, reject) => {
form_ref.value.validate((res) => resolve(res));
});
let isFull = true;
try {
state.form.warningScopeRows.forEach((e, i) => {
if (e.indicator_scope == "" || e.indicator_tag == "") {
throw `第${i + 1}条数据的指标没有选择!`;
}
});
} catch (e) {
if (e) {
ElMessage.error(e);
isFull = false;
}
}
return form_valid && isFull;
};
// 监听编辑的原数据,并显示
watch(
() => props.warningScopeRows,
(n) => {
state.form.warningScopeRows =
n?.map((e, i) => {
chooseTreeFirst(i, { label: e.indicator_scope }, true);
return {
key: e.key,
input_indicator_tag: e.indicator_tag,
indicator_scope: e.indicator_scope,
indicator_tag: e.indicator_tag,
oldQuery: e.indicator_scope,
cname: e.cname,
is_required: e.is_required,
is_linkage: e.is_linkage,
};
}) || [];
},
{
deep: true,
immediate: true,
}
);
// 获取第一级指标范围列表
onMounted(() => {
getDataTreeLevelFirst();
});
defineExpose({
Submit,
form: state.form,
form_ref,
});
</script>
<style lang="scss" scoped>
.indictor-tag {
width: 100%;
}
</style>
<style lang="scss">
.cascader-operation {
width: auto !important;
padding: 0 !important;
min-width: 180px !important;
position: relative;
min-height: 204px;
.el-scrollbar {
border-radius: 0;
}
.filter-first-tree-lists {
height: 240px;
display: flex;
min-width: 100%;
content-visibility: auto;
contain-intrinsic-size: 34px;
ul {
min-width: 180px;
height: 100%;
&:not(:first-child) {
border-left: 1px solid #ddd;
}
li {
padding-left: 20px;
line-height: 34px;
// max-width: 400px;
word-break: break-all;
transition: all 300ms;
font-size: 12px;
&:hover {
background-color: #f1f1f1;
}
cursor: pointer;
&.active {
font-weight: bold;
background-color: #f1f1f1;
}
}
}
.first-label-main {
padding-right: 48px;
position: relative;
.tree-label {
width: 100%;
}
.tree-icon {
width: 40px;
height: 100%;
position: absolute;
right: 0;
top: 0;
display: flex;
align-items: center;
svg {
width: 20px;
}
.is-loading {
animation: rotate 3s linear infinite;
opacity: 0;
&.loading-show {
opacity: 1;
}
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
}
}
.no-data {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: gray;
box-sizing: border-box;
padding: 0 20px;
}
}
}
.mate-str {
font-weight: bold;
}
</style>
<template>
<div class="my-warn-detail">
<div class="breadcrumb">
<bg-breadcrumb />
</div>
<div class="content bg-scroll">
<warn-detail :label-data="labelData" :value-data="info" :tabLabels="tabLabels" :tabDatas="tabDatas">
<template #status="{ item, valueData }">
<span class="status-body">
<span class="status" :class="`status-${valueData.status}`"></span>
<span>{{ statusOptions[valueData[item.prop]] }}</span>
</span>
</template>
</warn-detail>
</div>
</div>
</template>
<script setup>
import bgBreadcrumb from "@/components/bg-breadcrumb.vue";
import warnDetail from "@/components/warn-detail/index.vue";
import { useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import axios from "@/request/http.js";
import { ref, onBeforeMount } from "vue";
import { dateStringToDate } from "@/components/env";
const route = useRoute();
const { id } = route.query;
const ruleTypeOptions = ref({});
const statusOptions = {
1: "已恢复",
2: "未恢复",
3: "已关闭",
};
const riskLevels = {
1: "低风险",
2: "一般风险",
3: "较大风险",
4: "重大风险",
};
const labelData = [
[
{
label: "预警点",
prop: "warning_point",
},
{
label: "预警分类",
prop: "warning_type",
},
],
[
{
label: "预警指标",
prop: "warning_index",
},
{
label: "风险等级",
prop: "risk_level",
},
],
[
{
label: "状态",
prop: "status",
},
{
label: "预警阈值",
prop: "warning_threshold",
},
],
[
{
label: "当前报警值",
prop: "current_alarm_value",
},
{
label: "预警时间",
prop: "warning_time",
},
],
];
const info = ref({});
const tabLabels = [
{
label: "推送记录",
prop: "push",
},
{
label: "处置记录",
prop: "dispose",
},
];
const tabDatas = ref({});
const pushType = {
1: "自动推送",
2: "手动推送",
};
const getInfo = () => {
const params = {
id,
};
axios.get("/v1/api/alert", { params }).then((res) => {
if (res.data.code == 200) {
const { data } = res.data;
info.value = {
warning_point: data.alert_point,
warning_type: data.class_parent_name,
warning_index: data.class_name,
risk_level: riskLevels[data.risk_level],
status: data.status,
warning_threshold: `${data.alert_condition.thresholds_min}${
ruleTypeOptions.value[data.alert_rule_type]?.unit || ""
}-${data.alert_condition.thresholds_max}${ruleTypeOptions.value[data.alert_rule_type]?.unit || ""}`,
current_alarm_value: data.current_value + (ruleTypeOptions.value[data.alert_rule_type]?.unit || ""),
warning_time: dateStringToDate(data.alert_time),
};
tabDatas.value = {
push:
data.push_records?.map((e) => {
return {
method: e.notify_method,
person: e.system_account,
push_time: dateStringToDate(e.push_time),
push_type: pushType[e.push_type],
status: e.status,
};
}) || [],
dispose:
data.disposed_list?.map((e) => {
return {
status: e.is_disposed,
feedback: e.disposal_content,
feedback_time: dateStringToDate(e.disposal_time),
feedback_person: e.disposal_user,
};
}) || [],
};
}
});
};
const GetRuleTypeOptions = () => {
const params = {
page: 1,
page_size: 10000000000000,
class: 3,
};
axios.get(`/v1/api/dict`, { params }).then((res) => {
if (res.data.code == 200) {
res.data.data?.forEach((e) => {
let isEmptyOption = e.name == "空";
ruleTypeOptions.value[e.id] = {
label: e.name,
unit: isEmptyOption ? "" : e.unit,
};
});
getInfo();
}
});
};
onBeforeMount(() => {
GetRuleTypeOptions();
});
</script>
<style lang="scss" scoped>
.my-warn-detail {
width: 100%;
height: 100%;
padding: 0 24px 16px;
.breadcrumb {
width: 100%;
height: 46px;
}
.content {
width: 100%;
height: calc(100% - 46px);
background-color: #ffffff;
box-shadow: 0px 1px 4px 0px rgba(0, 7, 101, 0.15);
border-radius: 6px;
padding: 32px;
.status {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
margin-right: 8px;
$statusObj: (
1: #48ad97,
3: #9e9e9e,
2: #3759be,
);
@each $status, $color in $statusObj {
&-#{$status} {
background-color: $color;
}
}
}
.status-body {
display: flex;
align-items: center;
}
}
}
</style>
<template>
<div class="detail_container">
<bg-breadcrumb></bg-breadcrumb>
<div class="main_container">
<bg-filter-group @search="changeSearch" v-model="filter.keyword" placeholder="请输入预警点/分类/指标">
<template v-slot:left_action>
<div class="apaas_button">
<el-button type="primary" @click="batchPush">
<bg-icon style="font-size: 12px; color: #fff; margin-right: 8px" icon="#bg-ic-edit"></bg-icon>
批量推送
</el-button>
<el-button type="default" @click="batchClose">批量关闭</el-button>
<span class="header_info"
>已选择 <span style="color: #202531; font-weight: bold"> {{ state.selected.length }} </span>
</span>
<span class="header_info can_click_text" @click="clearSelected">清空</span>
</div>
</template>
<template v-slot:filter_group>
<div class="left-filter filter_list">
<div class="filter_item">
<span class="filter_title">风险等级</span>
<el-select v-model="filter.risk_level" placeholder="请选择" style="width: 300px">
<el-option
v-for="(value, key, index) in riskLevels"
:key="'riskLevels' + index"
:label="value"
:value="key">
</el-option>
</el-select>
</div>
<div class="filter_item">
<span class="filter_title">状态</span>
<el-select v-model="filter.status" placeholder="请选择" style="width: 300px">
<el-option
v-for="(value, key, index) in statusOptions"
:key="'stateOptions' + index"
:label="value"
:value="key">
</el-option>
</el-select>
</div>
<div class="filter_item">
<span class="filter_title">预警时间</span>
<el-date-picker
style="width: 400px"
v-model="filter.time"
type="datetimerange"
value-format="YYYY-MM-DD HH:mm:ss"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期" />
</div>
</div>
<div class="right-action apaas_button">
<el-button type="primary" @click="filterAction"> 查询 </el-button>
<el-button type="default" @click="filterClear"> 重置 </el-button>
</div>
</template>
</bg-filter-group>
<div class="table_container">
<div class="table bg-scroll">
<bg-table
ref="dataTable"
:headers="headers"
:rows="tableRows"
@selectAc="selectRows"
:selectable="selectable"
:isIndex="true"
:select="true"
:stripe="true">
<template v-slot:alert_point="{ row }">
<span class="can_click_text" @click="goDetail(row)">
{{ row.alert_point }}
</span>
</template>
<template v-slot:risk_level="{ row }">
{{ riskLevels[row.risk_level] }}
</template>
<template v-slot:current_value="{ row }">
{{ row.current_value }}{{ ruleTypeOptions[row.alert_rule_type]?.unit || "" }}
</template>
<template v-slot:warn_threshold="{ row }">
{{ row.alert_condition.thresholds_min }}{{ ruleTypeOptions[row.alert_rule_type]?.unit || "" }}
-
{{ row.alert_condition.thresholds_max }}{{ ruleTypeOptions[row.alert_rule_type]?.unit || "" }}
</template>
<template v-slot:alert_time="{ row }">
{{ dateStringToDate(row.alert_time) }}
</template>
<template v-slot:last_push_time="{ row }">
{{ dateStringToDate(row.last_push_time) }}
</template>
<template #status="{ row }">
<span :class="`circle status-${row.status}`"></span>
{{ statusOptions[row.status] }}
</template>
<template v-slot:action="{ row }">
<bg-table-btns2 :page_size="3" :tableData="tableRows">
<bg-table-btn :disabled="row.status != 2" @click="pushWarning(row)">推送提醒</bg-table-btn>
<bg-table-btn :disabled="row.status != 2" @click="closeWarning(row)">关闭预警</bg-table-btn>
</bg-table-btns2>
</template>
</bg-table>
</div>
<bg-pagination
:page="filter.page"
:size="filter.size"
:total="tableTotal"
@change-page="changePage"
@change-size="changeSize">
</bg-pagination>
</div>
</div>
<!-- 推送提醒 -->
<el-dialog title="推送提醒" v-model="pushDialog" width="780px" :before-close="cancelPushDialog">
<div class="warning_info">
<bg-icon
style="font-size: 12px; color: #a9b1c7; margin-right: 8px; vertical-align: middle"
icon="#bg-ic-s-circle-tips"></bg-icon
>该推送为临时推送,可调整推送人员,仅本次有效!如固定通知人员,则前往【预警规则设置】调整预警内容
</div>
<ManualDistributionForm
ref="manual_distribution_form"
:noElLabel="false"
methodLabel="预警工单推送"
:history="state.history" />
<template v-slot:footer>
<div class="apaas_button">
<el-button type="default" @click="cancelPushDialog">取消</el-button>
<el-button type="primary" @click="pushConfirm">确定</el-button>
</div>
</template>
</el-dialog>
<!-- 关闭提醒 -->
<el-dialog
class="dialog_box"
title="关闭预警"
v-model="closeWarningDialog"
width="400px"
:before-close="cancelCloseWarningDialog">
<el-form ref="closeForm" :model="closeFormData" :rules="closeRules" label-width="80px" class="bg_form">
<el-form-item label="关闭备注" prop="close_notes">
<el-input
v-model="closeFormData.close_notes"
type="textarea"
:autosize="{ minRows: 2 }"
show-word-page_size
maxlength="30"
resize="vertical"
placeholder="请输入内容"></el-input>
</el-form-item>
<el-form-item label="" class="no-el-label" prop="close_remind" style="margin-bottom: 0px">
<el-checkbox v-model="closeFormData.close_remind">
<div>三天内将不再自动推送该告警信息给处置人员,</div>
<div>可手动推送,但告警数据依然会出现</div>
</el-checkbox>
</el-form-item>
</el-form>
<template v-slot:footer>
<div class="apaas_button">
<el-button type="default" @click="cancelCloseWarningDialog">取消</el-button>
<el-button type="primary" @click="confirmClose">确定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { reactive, ref, onBeforeMount, toRefs, computed, watch, nextTick, watchEffect } from "vue";
import { ElMessage } from "element-plus";
import axios from "@/request/http.js";
import { Search } from "@element-plus/icons-vue";
import bgBreadcrumb from "@/components/bg-breadcrumb.vue";
import { useRouter } from "vue-router";
import ManualDistributionForm from "@/components/manual-distribution/form.vue";
import { GetRuleTypeOptions, dateStringToDate } from "@/components/env";
const router = useRouter();
const bgForm = ref(null);
const dataTable = ref(null);
const closeForm = ref(null);
const userTable = ref(null);
const manual_distribution_form = ref(null);
const statusOptions = {
"": "全部",
1: "已恢复",
2: "未恢复",
3: "已关闭",
};
const ruleTypeOptions = ref({});
const state = reactive({
riskLevels: {
"": "全部",
1: "低风险",
2: "一般风险",
3: "较大风险",
4: "重大风险",
}, // 风险等级
headers: [
{
label: "预警点",
prop: "alert_point",
width: 180,
},
{
label: "预警时间",
prop: "alert_time",
width: 160,
},
{
label: "预警分类",
prop: "class_parent_name",
},
{
label: "预警指标",
prop: "class_name",
},
{
label: "风险等级",
prop: "risk_level",
},
{
label: "当前报警值",
prop: "current_value",
},
{
label: "预警阈值",
prop: "warn_threshold",
},
{
label: "通知人数",
prop: "notification_count",
width: 80,
},
{
label: "推送次数",
prop: "push_count",
width: 80,
},
{
label: "最后推送时间",
prop: "last_push_time",
width: 160,
},
{
label: "状态",
prop: "status",
width: 90,
},
{
label: "操作",
prop: "action",
width: 180,
fixed: "right",
},
],
tableRows: [], // 表格数据
selected: [], //选择数据
tableTotal: 0, // 表格数据条数
filter: {
risk_level: "", // 风险等级
status: "", // 状态
time: [],
keyword: "",
page: 1,
page_size: 10,
}, // 表格筛选项
actionRow: null, // 当前操作的数据
closeWarningDialog: false, // 删除弹窗
closeFormData: {
close_notes: "",
close_remind: false,
},
closeRules: {
close_notes: [{ required: true, message: "请输入关闭备注", trigger: "blur" }],
},
history: null,
pushDialog: false,
pushType: 0, // 1-单条推送 2-批量推送
closeType: 0, // 1-单条推送 2-批量推送
});
const userTableFlag = computed(() => {
return !!state.pushDialog;
});
const selectRows = (data) => {
state.selected = data.selection;
};
const clearSelected = () => {
dataTable.value.clearTable();
}; // 清空
const batchClose = () => {
if (!state.selected || state.selected.length == 0) {
ElMessage.error("请先勾选要关闭的预警数据");
return;
}
state.closeType = 2;
state.closeWarningDialog = true;
}; // 批量关闭
const goDetail = (row) => {
router.push(`/forewarning/list/detail?id=${row.id}`);
}; // 查看详情
const changeSearch = (val) => {
state.filter.keyword = val;
changePage(1);
}; // 表格关键字筛选
const filterAction = () => {
changePage(1);
}; // 查询按钮
const filterClear = () => {
state.filter = {
warning_type: "", // 预警类型
warning_target: "", // 预警指标
risk_level: "", // 风险等级
status: "", // 状态
time: [],
keyword: "",
page: 1,
page_size: 10,
};
changePage(1);
}; // 重置筛选项
const selectable = (row, index) => {
return row.status === 2;
};
const getTableRows = () => {
let [start_time = "", end_time = ""] = state.filter.time || [];
let params = {
...state.filter,
start_time,
end_time,
};
Reflect.deleteProperty(params, "time");
axios.get("/v1/api/alert/list", { params }).then((res) => {
if (res.data.code == 200) {
let { list, total_count } = res.data.data;
state.tableRows = list || [];
state.tableTotal = total_count;
} else {
ElMessage.error(res.data.msg);
}
});
}; // 获取表格数据
const changePage = (page) => {
state.filter.page = page;
getTableRows();
}; // 改变页码
const changeSize = (size) => {
state.filter.page_size = size;
changePage(1);
}; // 改变每页条数
const batchPush = () => {
if (!state.selected || state.selected.length == 0) {
ElMessage.error("请先勾选要推送的数据");
return;
}
state.pushType = 2;
state.history = null;
state.pushDialog = true;
}; // 批量推送
const pushWarning = (row) => {
state.actionRow = row;
state.pushType = 1;
state.pushDialog = true;
let alert_rules_id = row.alert_rules_id;
axios.get(`/v1/api/alert_rules/list`, { params: { id: alert_rules_id } }).then((res) => {
if (res.data.code == 200) {
if (!res.data.data.list || res.data.data.list.length == 0) return;
state.history = {
method: res.data.data.list[0].notify_method || [],
lists:
res.data.data.list[0].notify_recipients?.map((e) => {
return {
user_id: e.system_account,
user_name: e.user_name,
phone: e.phone,
};
}) || [],
};
} else {
ElMessage.error(res.data.msg);
}
});
}; // 推送提醒
const cancelPushDialog = () => {
state.pushDialog = false;
}; // 预警推送弹窗取消按钮:重置表单
const pushConfirm = async () => {
const manual_distribution_form_valid = await manual_distribution_form.value.Submit();
if (!manual_distribution_form_valid) return;
const { method, lists } = manual_distribution_form.value.form || {};
let params = {
ids: state.pushType == 1 ? [+state.actionRow.id] : state.selected.map((e) => +e.id),
notify_method: method || [],
notify_recipients: lists?.map((e) => {
return {
system_account: e.user_id,
user_name: e.user_name,
phone: e.phone,
};
}),
};
axios
.put("/v1/api/alert/batch/push", params)
.then((res) => {
if (res.data.code == 200) {
ElMessage.success("推送成功");
} else {
ElMessage.error(res.data.msg);
}
})
.finally((e) => {
cancelPushDialog();
clearSelected();
changePage(1);
});
};
const closeWarning = (row) => {
state.actionRow = row;
state.closeType = 1;
state.closeWarningDialog = true;
console.log("关闭预警");
}; // 关闭预警
const cancelCloseWarningDialog = () => {
closeForm.value.resetFields();
state.closeWarningDialog = false;
}; // 关闭预警弹窗取消按钮:重置表单
const confirmClose = () => {
closeForm.value.validate((valid) => {
if (valid) {
let params = {
ids: state.closeType == 1 ? [+state.actionRow.id] : state.selected.map((e) => +e.id),
close_remark: state.closeFormData.close_notes,
defer_push: state.closeFormData.close_remind ? 1 : 0,
};
axios
.put("/v1/api/alert/batch/close", params)
.then((res) => {
if (res.data.code == 200) {
ElMessage.success("关闭预警操作成功");
} else {
ElMessage.error(res.dta.msg);
}
})
.finally((e) => {
cancelCloseWarningDialog();
clearSelected();
changePage(1);
});
}
});
}; // 关闭预警弹窗确定按钮:提交表单
const getRuleTypeOptions = async () => {
ruleTypeOptions.value = await GetRuleTypeOptions();
getTableRows();
};
onBeforeMount(() => {
getRuleTypeOptions();
});
const {
headers,
tableRows,
tableTotal,
filter,
riskLevels,
stateOptions,
closeWarningDialog,
closeFormData,
closeRules,
pushDialog,
} = toRefs(state);
</script>
<style lang="scss" scoped>
.detail_container {
width: 100%;
height: calc(100vh - 56px);
padding: 0 24px;
min-height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
.main_container {
height: 100%;
.filter-group {
.left-filter {
flex: 1;
display: flex;
justify-content: start;
flex-wrap: wrap;
}
.right-action {
width: 144px;
padding-bottom: 16px;
.el-button {
width: 64px;
}
}
}
.table_container {
height: calc(100% - 70px);
width: 100%;
padding: 0 16px;
.table {
max-height: calc(100% - 64px);
.circle {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 3px;
transform: translateY(-2px);
}
.status {
$statusObj: (
1: #48ad97,
3: #9e9e9e,
2: #3759be,
);
@each $status, $color in $statusObj {
&-#{$status} {
background-color: $color;
}
}
}
}
}
}
.bg_form {
width: 100%;
box-sizing: border-box;
.el-form-item {
margin-bottom: 16px;
:deep().el-form-item__label {
line-height: 36px;
height: 36px;
}
.el-form-item__content {
width: 100%;
.el-textarea {
:deep().el-input__count {
bottom: 6px;
right: 4px;
font-family: Roboto-Regular;
color: #a9b1c7;
}
}
}
}
}
:deep().dialog_box {
.el-dialog__body {
padding-bottom: 0;
}
}
}
.no-el-label {
:deep(.el-form-item__content) {
text-align: left;
margin-left: 10px !important;
.el-checkbox__label {
line-height: 18px;
}
}
}
</style>
<template>
<div class="detail_container">
<bg-breadcrumb></bg-breadcrumb>
<div class="main_container">
<div class="add-form-main bg-scroll">
<add-form ref="add_form"></add-form>
</div>
<div class="add-btns">
<el-button size="default" @click="Cancle">取消</el-button>
<el-button type="primary" size="default" @click="SaveSubmit">保存</el-button>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import { useRouter } from "vue-router";
import bgBreadcrumb from "@/components/bg-breadcrumb.vue";
import addForm from "../modules/add-form.vue";
import { ElMessage } from "element-plus";
import { Save } from "../modules/interface.js";
const router = useRouter();
const Cancle = () => {
router.go(-1);
};
const add_form = ref(null);
const SaveSubmit = async () => {
let { res, cb } = await add_form.value.Submit();
if (!res) return;
Save(res, {}, () => {
Cancle();
cb && cb();
});
};
</script>
<style lang="scss" scoped>
.detail_container {
width: 100%;
height: calc(100vh - 56px);
padding: 0 24px 16px;
min-height: 100%;
.main_container {
height: calc(100% - 46px);
padding: 0;
.add-form-main {
padding: 24px;
height: calc(100% - 68px);
}
.add-btns {
height: 68px;
display: flex;
align-items: center;
justify-content: flex-end;
border-top: 1px solid #ddd;
padding: 0 24px;
}
}
}
</style>
<template>
<div class="detail_container">
<bg-breadcrumb></bg-breadcrumb>
<div class="main_container bg-scroll">
<gap-title :hasLine="true" title="基本信息"></gap-title>
<div class="info">
<Info :labelData="labelData" :valueData="info">
<template #status="{ item, valueData }">
<span class="status-body">
<span class="status" :class="`status-${valueData.status}`"></span>
<span>{{ STATUS_OBJ[valueData[item.prop]] }}</span>
</span>
</template>
</Info>
</div>
<div class="warn-scope" v-if="detection_type == 1 && watning_scope_data_key.length > 0">
<gap-title :hasLine="true" title="预警范围"></gap-title>
<div class="info">
<Info :labelData="warning_scope_label" :valueData="watning_scope_data"> </Info>
</div>
</div>
<div class="warn-scope" v-else>
<gap-title :hasLine="true" title="指标表达式"></gap-title>
<div class="indicator-expression">
<bg-code-editor v-model="indicator_expression" :disabled="true"></bg-code-editor>
<!-- <bg-codemirror :disabled="true" v-model="indicator_expression"></bg-codemirror> -->
</div>
</div>
<gap-title :hasLine="true" title="预警规则"></gap-title>
<div class="info">
<bg-table border ref="ruletable" :headers="ruleHeaders" :rows="ruleRows" height="100%"> </bg-table>
</div>
<gap-title :hasLine="true" title="高级配置"> </gap-title>
<div class="info">
<Info :labelData="advanced_label" :valueData="advanced_data"> </Info>
</div>
<gap-title :hasLine="true" title="预警工单推送"> </gap-title>
<div class="info">
<Info :labelData="ticket_push_label" :valueData="ticket_push_data">
<template #notification_method="{ valueData }">
<span>{{ valueData.notification_method?.map((e) => METHODS[e]).join("") || "" }}</span>
</template>
</Info>
<div class="push-lists">
<bg-table border ref="pushtable" :headers="pushHeaders" :rows="pushRows" height="100%" :isIndex="true">
</bg-table>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onBeforeMount, computed } from "vue";
import { ElMessage } from "element-plus";
import axios from "@/request/http.js";
import { useRoute } from "vue-router";
import gapTitle from "@/components/gap-title.vue";
import bgBreadcrumb from "@/components/bg-breadcrumb.vue";
import Info from "@/components/warn-detail/info.vue";
import { METHODS } from "@/components/manual-distribution/env.js";
import { GetRuleTypeOptions, Empty, WIELESS_SMALL, WIELESS_BIG } from "@/components/env.js";
const route = useRoute();
const { id } = route.query;
const STATUS_OBJ = ["禁用", "启用"];
const ruleTypeOptions = ref({});
const selectRule = {
"=": "等于",
"!=": "不等于",
"=~": "正则匹配",
"!~": "正则不匹配",
};
const riskLevelOptions = {
4: "重大风险",
3: "较大风险",
2: "一般风险",
1: "低风险",
};
const unitOptions = {
s: "",
m: "分钟",
h: "小时",
};
const labelData = [
[
{
label: "预警规则名称",
prop: "warning_rule_name",
},
{
label: "启用状态",
prop: "status",
},
],
[
{
label: "预警分类",
prop: "warning_target",
},
{
label: "预警对象",
prop: "warning_type",
},
],
[
{
label: "预警指标",
prop: "warning_index",
},
{
label: "创建人",
prop: "create_by",
},
],
[
{
label: "创建时间",
prop: "create_time",
},
{
label: "更新时间",
prop: "update_time",
},
],
];
const info = ref({});
const warning_scope_label = ref([]);
const watning_scope_data = ref({});
const watning_scope_data_key = computed(() => {
return Object.keys(watning_scope_data.value);
});
const advanced_label = [
[
{
prop: "duration",
label: "持续时间",
},
],
[
{
prop: "inspection_cycle",
label: "检查周期",
},
],
];
const advanced_data = ref({});
const ticket_push_label = [
[
{
prop: "notification_method",
label: "预警通知方式",
},
],
[
{
prop: "push_num",
label: "消息推送次数",
},
],
[
{
prop: "push_frequency",
label: "消息推送频率",
},
],
];
const ticket_push_data = ref({});
const ruleHeaders = ref([
{
prop: "risk_level",
label: "风险程度",
},
]);
const ruleRows = ref([]);
const pushHeaders = [
{
prop: "system_account",
label: "账号",
},
{
prop: "user_name",
label: "姓名",
},
{
prop: "phone",
label: "联系方式",
},
];
const pushRows = ref([]);
const detection_type = ref(1);
const indicator_expression = ref("");
const getInfoData = () => {
axios
.get("/v1/api/alert_rules", {
params: {
id: id,
},
})
.then((res) => {
if (res.data.code == 200) {
const { data } = res.data;
detection_type.value = data.detection_type;
indicator_expression.value = data.expr;
info.value = {
warning_rule_name: data.metric_name,
status: data.is_enabled,
warning_target: data.class_parent_name,
warning_type: data.class_name,
warning_index: data.metric_config_name,
create_by: data.created_by,
create_time: data.created_at,
update_time: data.updated_at,
};
data.alert_range.forEach((e) => {
warning_scope_label.value.push([
{
prop: e.chinese_name || e.name,
label: e.chinese_name || e.name,
},
]);
watning_scope_data.value[e.chinese_name || e.name] =
e.value == ".*" ? "全部" : `${selectRule[e.compare]} ${e.value}`;
});
let isEmpty = Empty(data.alert_rule_type, ruleTypeOptions.value);
if (!isEmpty) {
ruleHeaders.value = [
{
prop: "warning_threshold",
label: "预警阈值",
},
...ruleHeaders.value,
];
}
let unit = ruleTypeOptions.value[data.alert_rule_type]?.unit || "";
ruleRows.value = data.alert_condition.map((e) => {
let min = e.thresholds_min + unit;
if (e.thresholds_min === undefined) {
if (
data.alert_rule_type &&
ruleTypeOptions.value[data.alert_rule_type] &&
ruleTypeOptions.value[data.alert_rule_type]?.down !== ""
) {
min = ruleTypeOptions.value[data.alert_rule_type].down + unit;
} else {
min = WIELESS_SMALL;
}
}
let max = e.thresholds_max + unit;
if (e.thresholds_max === undefined) {
if (
data.alert_rule_type &&
ruleTypeOptions.value[data.alert_rule_type] &&
ruleTypeOptions.value[data.alert_rule_type].up !== ""
) {
max = ruleTypeOptions.value[data.alert_rule_type].up + unit;
} else {
max = WIELESS_BIG;
}
}
return {
warning_threshold: `${min} - ${max}`,
risk_level: riskLevelOptions[e.risk_level],
};
});
advanced_data.value = {
duration: data.duration == 0 ? "直接产生预警" : data.duration + unitOptions[data.duration_unit],
inspection_cycle: data.check_period + "分钟",
};
ticket_push_data.value = {
notification_method: data.notify_method,
push_num: data.notify_push_count + "",
push_frequency: data.notify_push_frequency + "分钟",
};
pushRows.value = data.notify_recipients;
} else {
ElMessage.error(res.data.data);
}
});
};
const getRuleTypeOptions = async () => {
ruleTypeOptions.value = await GetRuleTypeOptions();
getInfoData();
};
onBeforeMount(() => {
getRuleTypeOptions();
});
</script>
<style lang="scss" scoped>
.detail_container {
width: 100%;
height: calc(100vh - 56px);
padding: 0 24px;
min-height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
.main_container {
height: 100%;
padding: 24px;
:deep(.gap-title) {
margin-bottom: 16px;
}
.warn-scope {
margin-bottom: 24px;
.indicator-expression {
height: 300px;
width: 100%;
:deep(.vue-ace-editor) {
margin-top: 0;
}
}
}
.info {
max-width: 1072px;
width: 100%;
padding: 0 8px 0;
&:not(:last-child) {
padding-bottom: 24px;
}
.push-lists {
margin-top: 16px;
}
}
}
}
.status {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
margin-right: 8px;
$statusObj: (
1: #48ad97,
0: #9e9e9e,
);
@each $status, $color in $statusObj {
&-#{$status} {
background-color: $color;
}
}
}
.status-body {
display: flex;
align-items: center;
}
</style>
<template>
<div class="detail_container">
<bg-breadcrumb></bg-breadcrumb>
<div class="main_container">
<div class="add-form-main bg-scroll">
<add-form ref="add_form" :row="infoData"></add-form>
</div>
<div class="add-btns">
<el-button size="default" @click="Cancle">取消</el-button>
<el-button type="primary" size="default" @click="SaveSubmit">保存</el-button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onBeforeMount } from "vue";
import { useRouter, useRoute } from "vue-router";
import bgBreadcrumb from "@/components/bg-breadcrumb.vue";
import addForm from "../modules/add-form.vue";
import axios from "@/request/http.js";
import { ElMessage } from "element-plus";
import { Save } from "../modules/interface.js";
import { Empty, GetRuleTypeOptions } from "@/components/env.js";
const infoData = ref({});
const ruleTypeOptions = ref({});
const router = useRouter();
const route = useRoute();
const { id } = route.query;
const Cancle = () => {
router.go(-1);
};
const add_form = ref(null);
const SaveSubmit = async () => {
let { res, cb } = await add_form.value.Submit();
if (!res) return;
Save(res, { id }, () => {
Cancle();
});
};
const staticTypeOptions = ref([]);
const findTypeBySecond = (id) => {
return staticTypeOptions.value.find((e) => {
return e.children.find((e_c) => e_c.class_id == id);
});
};
const getInfoData = () => {
axios
.get("/v1/api/alert_rules", {
params: {
id: id,
},
})
.then((res) => {
if (res.data.code == 200) {
const { data } = res.data;
let isEmpty = Empty(data.alert_rule_type, ruleTypeOptions.value);
let type_json = {
1: () => {
let obj = {
key: "static",
type_com_ref: {
warn_type: data.class_id,
warn_indicator: data.metric_config_id,
warn_target: findTypeBySecond(data.class_id)?.class_id || "",
rule_type: data.alert_rule_type,
warning_scpoe_form:
data.alert_range?.map((e) => {
return {
...e,
value: e.value == ".*" ? "" : e.value,
select: e.value == ".*" ? "all" : e.compare,
options: [],
};
}) || [],
},
};
if (isEmpty) {
obj.type_com_ref.risk_level = data.alert_condition[0].risk_level;
} else {
obj.type_com_ref.ruleRows =
data.alert_condition?.map((e) => {
return {
from: e.thresholds_min,
to: e.thresholds_max,
risk_level: e.risk_level,
};
}) || [];
}
return obj;
},
2: () => {
let obj = {
key: "custom",
type_com_ref: {
warn_target: data.class_parent_name,
warn_type: data.class_name,
warn_indicator: data.metric_config_name,
indicator_expression: data.expr,
rule_type: data.alert_rule_type,
},
};
if (isEmpty) {
obj.type_com_ref.risk_level = data.alert_condition[0].risk_level;
} else {
obj.type_com_ref.ruleRows =
data.alert_condition?.map((e) => {
return {
from: e.thresholds_min,
to: e.thresholds_max,
risk_level: e.risk_level,
};
}) || [];
}
return obj;
},
};
let d = type_json[`${data.detection_type}`]();
infoData.value = {
name: data.metric_name,
type_key: d.key,
unit: data.duration_unit,
time: data.duration,
inspection_cycle: data.check_period,
push_num: data.notify_push_count,
push_frequency: data.notify_push_frequency,
enabled: data.is_enabled == 1,
type_com_ref: d.type_com_ref,
manual_distribution_form: {
method: data.notify_method,
lists:
data.notify_recipients?.map((e) => {
return {
user_id: e.system_account,
user_name: e.user_name,
phone: e.phone,
};
}) || [],
},
};
} else {
ElMessage.error(res.data.data);
}
});
};
const getRuleTypeOptions = async (cb) => {
ruleTypeOptions.value = await GetRuleTypeOptions();
getInfoData();
};
const getStaticTypeOptions = () => {
axios.get("/v1/api/alert_class/tree").then(async (res) => {
if (res.data.code == 200) {
staticTypeOptions.value = res.data.data;
getRuleTypeOptions();
} else {
ElMessage.error(res.data.msg);
}
});
};
onBeforeMount(() => {
getStaticTypeOptions();
});
</script>
<style lang="scss" scoped>
.detail_container {
width: 100%;
height: calc(100vh - 56px);
padding: 0 24px 16px;
min-height: 100%;
.main_container {
height: calc(100% - 46px);
padding: 0;
.add-form-main {
padding: 24px;
height: calc(100% - 68px);
}
.add-btns {
height: 68px;
display: flex;
align-items: center;
justify-content: flex-end;
border-top: 1px solid #ddd;
padding: 0 24px;
}
}
}
</style>
<template>
<div class="detail_container">
<bg-breadcrumb></bg-breadcrumb>
<div class="main_container">
<bg-filter-group
@search="changeSearch"
v-model="filter.keyword"
inputWidth="380px"
placeholder="请输入预警规则名称/预警对象/预警分类/预警指标">
<template v-slot:left_action>
<div class="apaas_button">
<el-button type="primary" @click="addRule">
<bg-icon style="font-size: 12px; color: #fff; margin-right: 8px" icon="#bg-ic-add"></bg-icon>
新增
</el-button>
<el-button type="default" @click="batchDelete">批量删除</el-button>
<span class="header_info"
>已选择 <span style="color: #202531; font-weight: bold"> {{ state.selected.length }} </span>
</span>
<span class="header_info can_click_text" @click="clearSelected">清空</span>
</div>
</template>
<template v-slot:filter_group>
<div class="left-filter filter_list">
<div class="filter_item">
<span class="filter_title">通知方式</span>
<el-select v-model="filter.notify_method" placeholder="请选择" style="width: 300px">
<el-option
v-for="(item, index) in noticeTypes"
:key="'noticeTypes' + index"
:label="item.name"
:value="item.value">
</el-option>
</el-select>
</div>
<div class="filter_item">
<span class="filter_title">启用状态</span>
<el-select v-model="filter.is_enabled" placeholder="请选择" style="width: 300px">
<el-option
v-for="(item, index) in stateOptions"
:key="'stateOptions' + index"
:label="item.name"
:value="item.value">
</el-option>
</el-select>
</div>
<div class="filter_item">
<span class="filter_title">时段</span>
<el-date-picker
style="width: 300px"
v-model="filter.time"
type="datetimerange"
value-format="YYYY-MM-DD HH:mm:ss"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期" />
</div>
</div>
<div class="right-action apaas_button">
<el-button type="primary" @click="filterAction"> 查询 </el-button>
<el-button type="default" @click="filterClear"> 重置 </el-button>
</div>
</template>
</bg-filter-group>
<div class="table_container">
<div class="table bg-scroll">
<bg-table
ref="dataTable"
:headers="headers"
:rows="tableRows"
@selectAc="selectRows"
:selectable="selectable"
:isIndex="true"
:select="true"
:stripe="true">
<template v-slot:metric_name="{ row }">
<span class="can_click_text" @click="goDetail(row)">
{{ row.metric_name }}
</span>
</template>
<template v-slot:notify_method="{ row }">
{{ row.notify_method.map((e) => METHODS[e]).join("") }}
</template>
<template v-slot:notify_recipients="{ row }">
{{ row.notify_recipients.length }}
</template>
<template #is_enabled="{ row }">
<bg-switch
@click="stateChange(row)"
:labels="['否', '是']"
:values="[2, 1]"
v-model="row.is_enabled"></bg-switch>
</template>
<template v-slot:created_at="{ row }">
{{ dateStringToDate(row.created_at) }}
</template>
<template v-slot:action="{ row }">
<bg-table-btns2 :limit="3" :tableData="tableRows">
<bg-table-btn :disabled="row.is_enabled != 2" @click="editRow(row)">编辑</bg-table-btn>
<bg-table-btn :disabled="row.is_enabled != 2" @click="deleteRow(row)">删除</bg-table-btn>
</bg-table-btns2>
</template>
</bg-table>
</div>
<bg-pagination
:page="filter.page"
:size="filter.size"
:total="tableTotal"
@change-page="changePage"
@change-size="changeSize">
</bg-pagination>
</div>
</div>
<!-- 删除 -->
<el-dialog class="dialog_box" title="删除" v-model="delDialog" width="420px">
<div>确定要删除吗?</div>
<template v-slot:footer>
<div class="apaas_button">
<el-button type="default" @click="delDialog = false">取消</el-button>
<el-button type="primary" @click="delConfirm">确定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { reactive, ref, onBeforeMount, toRefs, computed, watch, nextTick, watchEffect } from "vue";
import { ElMessage } from "element-plus";
import axios from "@/request/http.js";
import { Search } from "@element-plus/icons-vue";
import bgBreadcrumb from "@/components/bg-breadcrumb.vue";
import { useRouter } from "vue-router";
import { dateStringToDate } from "@/components/env";
const METHODS = {
dingtalk: "钉钉",
sms: "短信",
};
const router = useRouter();
const dataTable = ref(null);
const state = reactive({
noticeTypes: [
{
name: "全部",
value: "",
},
{
name: "钉钉",
value: "dingtalk",
},
{
name: "短信",
value: "sms",
},
],
stateOptions: [
{
name: "全部",
value: "",
},
{
name: "启用",
value: 1,
},
{
name: "停用",
value: 2,
},
], // 状态
headers: [
{
label: "预警规则名称",
prop: "metric_name",
width: 200,
},
{
label: "预警分类",
prop: "class_parent_name",
},
{
label: "预警对象",
prop: "class_name",
},
{
label: "预警指标",
prop: "metric_config_name",
},
{
label: "通知方式",
prop: "notify_method",
},
{
label: "通知人数",
prop: "notify_recipients",
},
{
label: "是否启用",
prop: "is_enabled",
},
{
label: "创建人",
prop: "created_by",
},
{
label: "创建时间",
prop: "created_at",
width: 160,
},
{
label: "操作",
prop: "action",
width: 136,
fixed: "right",
},
],
tableRows: [], // 表格数据
selected: [], //选择数据
tableTotal: 0, // 表格数据条数
filter: {
notify_method: "", // 通知方式
is_enabled: "", // 状态
time: [],
keyword: "",
page: 1,
limit: 10,
}, // 表格筛选项
actionRow: null, // 当前操作的数据
delDialog: false, // 删除弹窗
delType: 0, // 1-单条删除 2-批量删除
});
const selectRows = (data) => {
state.selected = data.selection;
};
const clearSelected = () => {
dataTable.value.clearTable();
}; // 清空
const batchDelete = () => {
console.log("批量删除");
if (!state.selected || state.selected.length == 0) {
ElMessage.error("请先勾选要删除的数据");
return;
}
state.delType = 2;
state.delDialog = true;
}; // 批量删除
const goDetail = (row) => {
console.log("去详情");
router.push({
path: "/forewarning/rule-set/detail",
query: {
id: row.id,
},
});
}; // 查看详情
const changeSearch = (val) => {
state.filter.keyword = val;
changePage(1);
}; // 表格关键字筛选
const filterAction = () => {
changePage(1);
}; // 查询按钮
const filterClear = () => {
state.filter = {
notify_method: "", // 通知方式
is_enabled: "", // 状态
time: "",
keyword: "",
page: 1,
limit: 10,
};
changePage(1);
}; // 重置筛选项
const selectable = (row, index) => {
return row.is_enabled === 2;
};
const getTableRows = () => {
let [start_time = "", end_time = ""] = state.filter.time || [];
let params = {
...state.filter,
start_time,
end_time,
};
Reflect.deleteProperty(params, "time");
axios
.get(`/v1/api/alert_rules/list`, {
params,
})
.then((res) => {
if (res.data.code == 200) {
state.tableRows = res.data.data?.list || [];
state.tableTotal = res.data.data.total_count;
} else {
ElMessage.error(res.data.data);
}
});
}; // 获取表格数据
const changePage = (page) => {
state.filter.page = page;
getTableRows();
}; // 改变页码
const changeSize = (size) => {
state.filter.limit = size;
changePage(1);
}; // 改变每页条数
const stateChange = ({ id, is_enabled }) => {
const params = {
id,
is_enabled,
};
axios.put(`/v1/api/alert_rules/is_enabled`, params).then((res) => {
if (res.data.code == 200) {
ElMessage.success("状态修改成功");
changePage(1);
} else {
ElMessage.error(res.data.msg);
row.state = row.state == 1 ? 2 : 1;
}
});
};
const addRule = () => {
router.push({
path: `/forewarning/rule-set/add`,
});
}; // 新增规则
const editRow = (row) => {
router.push({
path: `/forewarning/rule-set/edit`,
query: {
id: row.id,
},
});
}; // 编辑
const deleteRow = (row) => {
state.actionRow = row;
state.delType = 1;
state.delDialog = true;
}; // 删除
const delConfirm = () => {
let ids = [];
if (state.delType == 1) {
ids.push(state.actionRow.id);
} else {
ids = state.selected.map((e) => {
return e.id;
});
}
axios
.delete("/v1/api/alert_rules", {
data: {
ids,
},
})
.then((res) => {
if (res.data.code == "200") {
ElMessage.success("删除成功");
state.delDialog = false;
clearSelected();
changePage(1);
} else {
ElMessage.error(res.data.msg);
}
});
}; // 确定删除
onBeforeMount(() => {
getTableRows();
});
const { headers, tableRows, tableTotal, filter, noticeTypes, stateOptions, delDialog } = toRefs(state);
</script>
<style lang="scss" scoped>
.detail_container {
width: 100%;
height: calc(100vh - 56px);
padding: 0 24px;
min-height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
.main_container {
height: 100%;
.filter-group {
.left-filter {
flex: 1;
display: flex;
justify-content: start;
flex-wrap: wrap;
}
.right-action {
width: 144px;
padding-bottom: 16px;
.el-button {
width: 64px;
}
}
}
.table_container {
height: calc(100% - 70px);
width: 100%;
padding: 0 16px;
.table {
max-height: calc(100% - 64px);
}
}
}
}
</style>
<template>
<div class="add-form">
<el-form :model="state.form" ref="form_ref" :rules="state.rules" label-width="120px">
<div class="add-form-item">
<el-form-item label="预警规则名称" prop="name">
<el-input v-model="state.form.name" :disabled="isEdit" placeholder="请输入预警规则名称"></el-input>
</el-form-item>
<el-form-item label="检测类型" prop="type">
<el-button-group>
<el-button
v-for="(value, key) in types"
:key="key"
:type="state.form.type_key == key ? 'primary' : ''"
size="small"
@click="changeType(key)"
:disabled="isEdit">
{{ value }}
</el-button>
</el-button-group>
</el-form-item>
</div>
<component
ref="type_com_ref"
:is="typeCom[state.form.type_key]"
:isEdit="isEdit"
:form="typrFormData"
@update-duration="updateDuration"></component>
<gap-title :hasLine="true" title="高级配置"></gap-title>
<div class="add-form-item">
<div class="duration">
<el-form-item label="持续时间" prop="time">
<el-input v-model="state.form.time" placeholder="请输入持续时间" @input="inputNum">
<template #prepend>
<span>当预警持续</span>
</template>
<template #append>
<el-select v-model="state.form.unit" placeholder="请选择">
<el-option v-for="(value, key) in unitOptions" :key="key" :label="value.label" :value="key">
</el-option>
</el-select>
</template>
</el-input>
<span class="duration-append">
<span> 时产生报警 </span>
<el-popover
:width="300"
title=""
content="大于等于0的正整数,等于0时代表直接报警"
trigger="hover"
placement="top-start">
<template #reference>
<bg-icon
style="font-size: 12px; color: #a9b1c7; margin-left: 8px; vertical-align: middle"
icon="#bg-ic-s-circle-tips"></bg-icon>
</template>
</el-popover>
</span>
</el-form-item>
</div>
<el-form-item label="检查周期" prop="inspection_cycle">
<el-select class="cycle-select" style="flex: 1" v-model="state.form.inspection_cycle" placeholder="请选择">
<el-option v-for="item in inspectionCycleOptions" :key="item" :label="item" :value="item"> </el-option>
</el-select>
<div class="cycle-unit">分钟</div>
</el-form-item>
</div>
<gap-title :hasLine="true" title="预警工单推送"></gap-title>
<div class="add-form-item">
<el-form-item class="no-el-label" label="" prop="push_method">
<div style="width: 100%">
<ManualDistributionForm
ref="manual_distribution_form"
class="manual-distribution-form"
labelWidth="120px"
:noElLabel="false"
methodLabel="预警通知方式"
:history="history" />
</div>
</el-form-item>
<el-form-item label="消息推送次数">
<el-input style="flex: 1" v-model="state.form.push_num" placeholder="请输入消息推送次数" clearable>
<template #append></template>
</el-input>
</el-form-item>
<el-form-item label="消息推送频率">
<el-input style="flex: 1" v-model="state.form.push_frequency" placeholder="请输入消息推送频率" clearable>
<template #append>分钟</template>
</el-input>
</el-form-item>
<el-form-item label="是否立即启用" prop="">
<el-switch
v-model="state.form.enabled"
inline-prompt
active-text="是"
inactive-text="否"
:active-value="true"
:inactive-value="false">
</el-switch>
</el-form-item>
</div>
</el-form>
</div>
</template>
<script setup>
import { reactive, ref, shallowReactive, computed, nextTick, watch } from "vue";
import gapTitle from "@/components/gap-title.vue";
import ManualDistributionForm from "@/components/manual-distribution/form.vue";
import Static from "./static.vue";
import Custom from "./custom.vue";
import { MAX_DAY } from "@/components/env.js";
import { ElMessage } from "element-plus";
const props = defineProps({
row: {
type: Object,
default: null,
},
});
const isEdit = computed(() => !!props.row);
const history = computed(() => props.row?.manual_distribution_form || null);
const typrFormData = computed(() => props.row?.type_com_ref || null);
const manual_distribution_form = ref(null);
const typeCom = shallowReactive({
static: Static,
custom: Custom,
});
const unitOptions = {
s: { label: "", max: MAX_DAY * 24 * 3600 },
m: { label: "分钟", max: MAX_DAY * 24 * 60 },
h: { label: "小时", max: MAX_DAY * 24 },
};
const state = reactive({
form: {
name: "",
type_key: "static",
time: 10,
unit: "s",
inspection_cycle: 1,
push_num: 1,
push_frequency: 60,
enabled: true,
},
rules: {
name: [{ required: true, message: "请输入预警规则名称", trigger: "blur" }],
time: [{ required: true, message: "请输入持续时间", trigger: "blur" }],
},
});
const inputNum = () => {
state.form.time = state.form.time.replace(/[^\d]/g, "");
let time = +state.form.time;
let { max } = unitOptions[state.form.unit];
if (time > +max) {
state.form.time = max;
}
};
const updateDuration = (data) => {
const { duration, duration_unit, check_period } = data;
state.form.time = duration;
state.form.unit = duration_unit;
state.form.inspection_cycle = check_period;
};
const types = {
static: "静态阈值",
custom: "自定义",
};
const changeType = async (key) => {
state.form.type_key = key;
form_ref.value.clearValidate();
};
const timeOptions = [10, 20, 60, 120, 180, 300];
const inspectionCycleOptions = ref([1, 3, 5, 10, 20]);
const form_ref = ref(null);
const type_com_ref = ref(null);
const Submit = async () => {
let form_valid = await new Promise((resolve, reject) => {
form_ref.value.validate((res) => resolve(res));
});
let type_com_ref_valid = await type_com_ref.value.Submit();
let manual_distribution_form_valid = await manual_distribution_form.value.Submit();
if (form_valid && type_com_ref_valid && manual_distribution_form_valid) {
let obj = {
...state.form,
type_com_ref: type_com_ref.value?.form || {},
manual_distribution_form: manual_distribution_form.value?.form || {},
};
return {
res: obj,
cb: () => {
if (!isEdit.value) {
setTimeout(() => {
type_com_ref.value.form_ref.resetFields();
manual_distribution_form.value.form_ref.resetFields();
form_ref.value.resetFields();
}, 100);
}
},
};
}
ElMessage.error("有必填项没有填写");
return {};
};
watch(
() => props.row,
(n) => {
if (!n) return;
state.form.name = n.name;
state.form.type_key = n.type_key;
state.form.unit = n.unit;
state.form.time = n.time || 10;
state.form.inspection_cycle = n.inspection_cycle || 1;
state.form.push_num = n.push_num || 1;
state.form.push_frequency = n.push_frequency || 60;
state.form.enabled = n.enabled || false;
},
{ deep: true, immediate: true }
);
defineExpose({
Submit,
});
</script>
<style lang="scss" scoped>
.add-form {
:deep(.gap-title) {
margin-bottom: 16px;
}
&-item {
max-width: 1080px;
width: 100%;
padding-left: 8px;
.indicator-expression {
height: 300px;
width: 100%;
:deep(.vue-ace-editor) {
margin-top: 0;
}
}
.duration {
flex: 1;
display: flex;
align-items: center;
:deep(.el-form-item) {
flex: 1;
.el-input {
margin-right: 8px;
flex: 1;
}
.el-input-group__prepend {
width: 102px;
border-radius: 4px;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.el-input-group__append {
width: 80px;
border-radius: 4px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
// :deep(.el-form-item__content) {
// display: flex;
// align-items: center;
// gap: 8px;
// > .el-input {
// flex: 1;
// width: 80px;
// }
// }
}
:deep(.el-switch__inner) {
.is-hide {
display: none;
}
}
:deep(.el-input-group__append, .el-input-group__prepend) {
border-radius: 4px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.duration-append {
display: flex;
align-items: center;
color: #404a62;
}
.cycle-unit {
width: 60px;
height: 36px;
background-color: #f7f7f9;
border-radius: 0px 4px 4px 0px;
border: solid 1px #dadee7;
text-align: center;
border-left: 0;
}
.cycle-select {
:deep(.el-input__wrapper) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
}
}
.no-el-label {
:deep(.el-form-item__content) {
margin-left: 0 !important;
}
}
.manual-distribution-form {
:deep(.el-form-item) {
&:not(:last-child) {
margin-bottom: 18px;
}
}
}
</style>
<style lang="scss">
.el-form-item {
min-height: 36px;
label {
height: 36px;
line-height: 36px;
}
.el-input__inner {
min-height: 34px;
}
}
</style>
<template>
<div class="custom-form">
<el-form :model="state.form" ref="form_ref" :rules="state.rules" label-width="120px">
<div class="custom-form-item">
<el-form-item label="预警分类" prop="warn_target">
<el-input
v-model="state.form.warn_target"
placeholder="请输入预警分类"
:disabled="isEdit"
:maxlength="20"
show-word-limit
clearable
@change="changeWarnCustomTarget"></el-input>
</el-form-item>
<el-form-item label="预警对象" prop="warn_type">
<el-input
v-model="state.form.warn_type"
placeholder="请输入预警对象"
:disabled="isEdit"
clearable
:maxlength="20"
show-word-limit
@change="changeWarnCustomType"></el-input>
</el-form-item>
<el-form-item label="预警指标" prop="warn_indicator">
<el-input
v-model="state.form.warn_indicator"
placeholder="请输入预警指标"
:disabled="isEdit"
:maxlength="30"
show-word-limit
clearable
@change="changeWarnCustomType"></el-input>
</el-form-item>
</div>
<gap-title :hasLine="true" title="指标表达式"> </gap-title>
<div class="custom-form-item">
<el-form-item label="" prop="indicator_expression">
<div class="indicator-expression">
<bg-code-editor v-model="state.form.indicator_expression"></bg-code-editor>
<!-- <bg-codemirror v-model="state.form.indicator_expression"></bg-codemirror> -->
</div>
</el-form-item>
</div>
<gap-title :hasLine="true" title="预警规则"></gap-title>
<div class="custom-form-item">
<el-form-item label="预警规则类型" prop="rule_type">
<el-select
style="flex: 1"
v-model="state.form.rule_type"
placeholder="请选择"
filterable
@change="changeRuleType">
<el-option v-for="(value, key) in ruleTypeOptions" :key="key" :label="value.label" :value="key">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="风险程度" prop="risk_level" v-if="isEmpty">
<el-select style="flex: 1" v-model="state.form.risk_level" placeholder="请选择风险程度" filterable>
<el-option v-for="item in riskLevels" :key="item.id" :label="item.name" :value="item.id"> </el-option>
</el-select>
</el-form-item>
<el-form-item label="" v-else-if="state.form.ruleRows.length > 0">
<div class="rule-table" style="width: 100%">
<el-form
:model="state.form.ruleRows"
ref="table_form"
:rules="state.tableRules"
label-width="0"
style="width: 100%">
<el-table :data="state.form.ruleRows" stripe border>
<el-table-column
v-for="header in ruleHeaders"
:prop="header.prop"
:key="header.prop"
:label="header.label"
:width="header.width">
<template #default="{ $index }">
<div class="warning-threshold" v-if="header.prop == 'warning_threshold'">
<el-form-item :prop="`[${$index}].from`" :rules="state.tableRules.from($index)" style="flex: 1">
<el-input
style="flex: 1"
v-model="state.form.ruleRows[$index].from"
placeholder="请输入"
@input="inputNum($index, 'from')"
@blur="changeWarningThresholdFrom($index)">
<template #append>{{ unitMap }}</template>
</el-input>
</el-form-item>
<span class="to">-</span>
<el-form-item
label=""
:prop="`${$index}.to`"
:rules="state.tableRules.to($index)"
style="flex: 1">
<el-input
style="flex: 1"
v-model="state.form.ruleRows[$index].to"
placeholder="请输入"
clearable
@input="inputNum($index, 'to')"
@blur="changeWarningThresholdTo($index)">
<template #append>{{ unitMap }}</template>
</el-input>
</el-form-item>
</div>
<div v-else-if="header.prop == 'risk_level'">
<el-form-item label="" :prop="`[${$index}].risk_level`" :rules="state.tableRules.risk_level">
<el-select
style="flex: 1"
v-model="state.form.ruleRows[$index].risk_level"
placeholder="请选择风险程度">
<el-option
v-for="item in riskLevelOptions($index)"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</el-form-item>
</div>
</template>
</el-table-column>
<el-table-column prop="" label="操作" width="150px">
<template #default="{ $index }">
<div class="table-operation">
<el-button
link
type="primary"
@click="createRule($index)"
:disabled="state.form.ruleRows.length >= riskLevels.length">
新增
</el-button>
<span class="line"></span>
<el-button
link
type="primary"
@click="removeRule($index)"
:disabled="state.form.ruleRows.length == 1">
删除
</el-button>
</div>
</template>
</el-table-column>
</el-table>
</el-form>
</div>
</el-form-item>
</div>
</el-form>
</div>
</template>
<script setup>
import { computed, onBeforeMount, reactive, ref } from "vue";
import gapTitle from "@/components/gap-title.vue";
import { ElMessage } from "element-plus";
import { GetRuleTypeOptions, Empty } from "@/components/env.js";
const props = defineProps({
form: {
type: Object,
default: null,
},
isEdit: {
type: Boolean,
default: false,
},
});
var validateWarnIndex = (rule, value, callback) => {
if (value === "") {
if (state.form.type_key == "static") {
callback(new Error("请选择"));
} else {
callback(new Error("请输入"));
}
} else {
callback();
}
};
const validateFrom = (rule, value, callback, index) => {
let thisRow = state.form.ruleRows[index];
// 如果只有一条数据,并且上限下限都没有填写,则提示错误
if (state.form.ruleRows.length == 1 && thisRow.from === "" && thisRow.to == "") {
return callback(new Error("请输入"));
}
// 当前下限不是第一条数据,并且为空,提示错误
if (index > 0 && value === "") {
return callback(new Error("请输入"));
}
callback();
};
const validateTo = (rule, value, callback, index) => {
let thisRow = state.form.ruleRows[index];
// 如果只有一条数据,并且上限下限都没有填写,则提示错误
if (state.form.ruleRows.length == 1 && thisRow.from === "" && thisRow.to == "") {
return callback(new Error("请输入"));
}
// 当前上限不是最后一条数据,并且为空,提示错误
if (index < state.form.ruleRows.length - 1 && value === "") {
return callback(new Error("请输入"));
}
callback();
};
const ruleTypeOptions = ref({});
const state = reactive({
form: {
warn_target: "",
warn_type: "",
warn_indicator: "",
indicator_expression: "",
rule_type: "",
ruleRows: [],
risk_level: "",
},
rules: {
warn_target: [{ required: true, message: "请输入预警分类", trigger: "blur" }],
warn_type: [{ required: true, message: "请输入预警对象", trigger: "blur" }],
rule_type: [{ required: true, message: "请选择预警规则类型", trigger: "change" }],
warn_indicator: [{ validator: validateWarnIndex, trigger: "blur" }],
indicator_expression: [{ required: true, message: "请输入预警指标", trigger: "blur" }],
risk_level: [{ required: true, message: "请选择风险程度", trigger: "change" }],
},
tableRules: {
from: (index) => {
return [{ validator: (rule, value, callback) => validateFrom(rule, value, callback, index), trigger: "blur" }];
},
to: (index) => {
return [{ validator: (rule, value, callback) => validateTo(rule, value, callback, index), trigger: "blur" }];
},
risk_level: [{ required: true, message: "请选择风险程度", trigger: "change" }],
},
});
const isEmpty = computed(() => {
return Empty(state.form.rule_type, ruleTypeOptions.value);
});
const changeWarnCustomTarget = () => {};
const changeWarnCustomType = () => {};
const setLimits = (index) => {
let rows = [...state.form.ruleRows];
rows.splice(index, 1);
return (
rows.map((e) => {
return {
down: +e.from,
up: +e.to,
};
}) || []
);
};
const inputNum = (index, key) => {
if (state.form.ruleRows[index][key] == "") return;
if (state.form.ruleRows[index][key] == "-") return;
state.form.ruleRows[index][key] = `${state.form.ruleRows[index][key]}`
.replace(/[^\-?\d.]/g, "")
.replace(/(\.)+/, "$1")
.replace(/(\.\d+)\.+/g, "$1")
.replace(/(\..*)\./g, "$1");
};
const changeWarningThresholdFrom = (index) => {
let { down, up } = limit.value;
let { from, to } = state.form.ruleRows[index];
if (to != "" && from !== "" && from > +to) {
ElMessage.error(`下限不能大于上限`);
state.form.ruleRows[index].from = "";
return;
} else if (down !== "" && +from < +down) {
ElMessage.error(`下限不能小于${down}`);
state.form.ruleRows[index].from = "";
return;
} else if (up != "" && +from > +up) {
ElMessage.error(`上限不能超过${up}`);
state.form.ruleRows[index].from = "";
return;
}
let rows = setLimits(index);
if (rows.length == 0) return;
let items = rows.filter((e, i) => {
let isPassDown = e.down !== "" ? +e.down <= +from : false;
let isLessUp = e.up !== "" ? +e.up >= +from : false;
let isLessDownAndPassUp = e.down !== "" && e.up !== "" && to !== "" ? +from < +e.down && +to > +e.up : false;
return (isPassDown && isLessUp) || isLessDownAndPassUp;
});
if (items.length > 0) {
ElMessage.error(`该范围已被设置`);
state.form.ruleRows[index].from = "";
}
};
const changeWarningThresholdTo = (index) => {
let { down, up } = limit.value;
let { from, to } = state.form.ruleRows[index];
if (from != "" && from !== "" && from > +to) {
ElMessage.error(`下限不能大于上限`);
state.form.ruleRows[index].to = "";
return;
} else if (down !== "" && +to < +down) {
ElMessage.error(`下限不能小于${down}`);
state.form.ruleRows[index].to = "";
return;
} else if (up != "" && +to > +up) {
ElMessage.error(`上限不能超过${up}`);
state.form.ruleRows[index].to = "";
return;
}
let rows = setLimits(index);
if (rows.length == 0) return;
let items = rows.filter((e, i) => {
let isPassDown = e.down !== "" ? +e.down <= +to : false;
let isLessUp = e.up !== "" ? +e.up >= +to : false;
let isLessDownAndPassUp = e.down !== "" && e.up !== "" && from !== "" ? +from < +e.down && +to > +e.up : false;
return (isPassDown && isLessUp) || isLessDownAndPassUp;
});
if (items.length > 0) {
ElMessage.error(`该范围已被设置`);
state.form.ruleRows[index].to = "";
}
};
const changeRuleType = () => {};
const form_ref = ref(null);
const table_form = ref(null);
const Submit = async () => {
let form_valid = await new Promise((resolve, reject) => {
form_ref.value.validate((res) => resolve(res));
});
if (isEmpty.value) {
return form_valid;
}
let table_form_valid = await new Promise((resolve, reject) => {
table_form.value.validate((res) => resolve(res));
});
return form_valid && table_form_valid;
};
const riskLevels = ref([
{
id: 4,
name: "重大风险",
},
{
id: 3,
name: "较大风险",
},
{
id: 2,
name: "一般风险",
},
{
id: 1,
name: "低风险",
},
]);
const riskLevelOptions = computed(() => {
return (index) => {
let risk_level = state.form.ruleRows[index].risk_level;
let rows = state.form.ruleRows.map((e) => e.risk_level);
if (risk_level) {
let i = rows.findIndex((e) => e == risk_level);
rows.splice(i, 1);
}
return riskLevels.value.filter((e) => !rows.includes(e.id));
};
});
const unitMap = computed(() => {
return ruleTypeOptions.value[state.form.rule_type]?.unit || "";
});
const limit = computed(() => {
return (
ruleTypeOptions.value[state.form.rule_type].limit || {
down: "",
up: "",
}
);
});
const getRuleTypeOptions = async () => {
ruleTypeOptions.value = await GetRuleTypeOptions();
};
const ruleHeaders = [
{
prop: "warning_threshold",
label: "预警阈值",
width: "500px",
},
{
prop: "risk_level",
label: "风险程度",
},
];
const createRule = (index = -1) => {
state.form.ruleRows.splice(index + 1, 0, {
from: "",
to: "",
risk_level: "",
});
};
const removeRule = (index) => {
state.form.ruleRows.splice(index, 1);
};
const info = () => {
if (!props.form) {
createRule();
return;
}
state.form.warn_target = props.form.warn_target;
state.form.warn_type = props.form.warn_type;
state.form.warn_indicator = props.form.warn_indicator;
state.form.indicator_expression = props.form.indicator_expression;
state.form.rule_type = props.form.rule_type;
state.form.risk_level = props.form.risk_level;
state.form.ruleRows =
props.form?.ruleRows?.map((e) => {
return {
from: e.from,
to: e.to,
risk_level: e.risk_level,
};
}) || [];
if (state.form.ruleRows.length == 0) {
createRule();
}
};
const formatForm = computed(() => {
return {
...state.form,
isEmpty: isEmpty.value,
};
});
onBeforeMount(() => {
getRuleTypeOptions();
info();
});
defineExpose({
Submit,
form: formatForm,
form_ref,
});
</script>
<style lang="scss" scoped>
.custom-form {
:deep(.gap-title) {
margin-bottom: 16px;
}
&-item {
max-width: 1080px;
width: 100%;
padding-left: 8px;
.indicator-expression {
height: 300px;
width: 100%;
:deep(.vue-ace-editor) {
margin-top: 0;
}
}
}
}
.warning-threshold {
display: flex;
align-items: center;
:deep(.el-input-group__append, .el-input-group__prepend) {
border-radius: 4px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
.to {
margin: 0 16px;
}
.line {
width: 1px;
height: 14px;
background-color: #c1c7d7;
margin: 0 16px;
display: inline-block;
}
.rule-table {
:deep(.el-table thead th) {
background-color: #f5f6f9;
}
}
</style>
<template>
<div class="gateway-form">
<div class="gateway-form-item">
<el-form
:model="state.form.ruleRows"
ref="form_ref"
:rules="state.tableRules"
label-width="0"
style="width: 100%">
<el-table :data="state.form.ruleRows" stripe border>
<el-table-column
v-for="header in ruleHeaders"
:prop="header.prop"
:key="header.prop"
:label="header.label"
:width="header.width">
<template #default="{ $index }">
<div class="warning-threshold" v-if="header.prop == 'warning_threshold'">
<el-form-item :prop="`[${$index}].from`" :rules="state.tableRules.from($index)" style="flex: 1">
<el-input
style="flex: 1"
v-model="state.form.ruleRows[$index].from"
placeholder="请输入"
@input="inputNum($index, 'from')"
@blur="changeWarningThresholdFrom($index)">
<template v-if="!isEmptyOption" #append>{{
ruleTypeOptions[props.rule_type]?.unit || ""
}}</template>
</el-input>
</el-form-item>
<span class="to">-</span>
<el-form-item label="" :prop="`${$index}.to`" :rules="state.tableRules.to($index)" style="flex: 1">
<el-input
style="flex: 1"
v-model="state.form.ruleRows[$index].to"
placeholder="请输入"
clearable
@input="inputNum($index, 'to')"
@blur="changeWarningThresholdTo($index)">
<template v-if="!isEmptyOption" #append>{{
ruleTypeOptions[props.rule_type]?.unit || ""
}}</template>
</el-input>
</el-form-item>
</div>
<div v-else-if="header.prop == 'risk_level'">
<el-form-item label="" :prop="`[${$index}].risk_level`" :rules="state.tableRules.risk_level">
<el-select style="flex: 1" v-model="state.form.ruleRows[$index].risk_level" placeholder="请选择">
<el-option
v-for="item in riskLevelOptions($index)"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</el-form-item>
</div>
</template>
</el-table-column>
<el-table-column prop="" label="操作" width="150px">
<template #default="{ $index }">
<div class="table-operation">
<el-button
link
type="primary"
@click="createRule($index)"
:disabled="state.form.ruleRows.length >= riskLevels.length">
新增
</el-button>
<span class="line"></span>
<el-button link type="primary" @click="removeRule($index)" :disabled="state.form.ruleRows.length == 1">
删除
</el-button>
</div>
</template>
</el-table-column>
</el-table>
</el-form>
</div>
</div>
</template>
<script setup>
import { computed, nextTick, onBeforeMount, reactive, ref } from "vue";
import gapTitle from "@/components/gap-title.vue";
import { ElMessage } from "element-plus";
import { Empty, GetRuleTypeOptions } from "@/components/env.js";
const props = defineProps({
form: {
type: Object,
default: null,
},
rule_type: {
type: String,
default: "1",
},
});
const ruleTypeOptions = ref({});
const getRuleTypeOptions = async (cb) => {
ruleTypeOptions.value = await GetRuleTypeOptions();
cb && cb();
};
const validateFrom = (rule, value, callback, index) => {
let thisRow = state.form.ruleRows[index];
// 如果只有一条数据,并且上限下限都没有填写,则提示错误
if (state.form.ruleRows.length == 1 && thisRow.from === "" && thisRow.to == "") {
return callback(new Error("请输入"));
}
// 当前下限不是第一条数据,并且为空,提示错误
if (index > 0 && value === "") {
return callback(new Error("请输入"));
}
callback();
};
const validateTo = (rule, value, callback, index) => {
let thisRow = state.form.ruleRows[index];
// 如果只有一条数据,并且上限下限都没有填写,则提示错误
if (state.form.ruleRows.length == 1 && thisRow.from === "" && thisRow.to == "") {
return callback(new Error("请输入"));
}
// 当前上限不是最后一条数据,并且为空,提示错误
if (index < state.form.ruleRows.length - 1 && value === "") {
return callback(new Error("请输入"));
}
callback();
};
const state = reactive({
form: {
ruleRows: [],
},
tableRules: {
from: (index) => {
return [
{
validator: (rule, value, callback) => validateFrom(rule, value, callback, index),
trigger: "blur",
},
];
},
to: (index) => {
return [
{
validator: (rule, value, callback) => validateTo(rule, value, callback, index),
trigger: "blur",
},
];
},
risk_level: [{ required: true, message: "请选择", trigger: "change" }],
},
});
const form_ref = ref(null);
const Submit = async () => {
let form_valid = await new Promise((resolve, reject) => {
form_ref.value.validate((res) => resolve(res));
});
return form_valid;
};
const riskLevels = ref([
{
id: 4,
name: "重大风险",
},
{
id: 3,
name: "较大风险",
},
{
id: 2,
name: "一般风险",
},
{
id: 1,
name: "低风险",
},
]);
const riskLevelOptions = computed(() => {
return (index) => {
let risk_level = state.form.ruleRows[index].risk_level;
let rows = state.form.ruleRows.map((e) => e.risk_level);
if (risk_level) {
let i = rows.findIndex((e) => e == risk_level);
rows.splice(i, 1);
}
return riskLevels.value.filter((e) => !rows.includes(e.id));
};
});
const ruleHeaders = [
{
prop: "warning_threshold",
label: "预警阈值",
width: "500px",
},
{
prop: "risk_level",
label: "风险程度",
},
];
const createRule = (index = -1) => {
state.form.ruleRows.splice(index + 1, 0, {
from: "",
to: "",
risk_level: "",
});
};
const setLimits = (index) => {
let rows = [...state.form.ruleRows];
rows.splice(index, 1);
return (
rows.map((e) => {
return {
down: e.from,
up: e.to,
};
}) || []
);
};
const isEmptyOption = computed(() => {
return Empty(props.rule_type, ruleTypeOptions.value);
});
const limit = computed(() => {
return (
ruleTypeOptions.value[props.rule_type] || {
down: "",
up: "",
}
);
});
const changeWarningThresholdFrom = (index) => {
let { down, up } = limit.value;
let { from, to } = state.form.ruleRows[index];
if (to !== "" && from !== "" && from >= +to) {
ElMessage.error(`下限不能大于上限`);
state.form.ruleRows[index].from = "";
return;
} else if (down !== "" && +from < +down) {
ElMessage.error(`下限不能小于${down}`);
state.form.ruleRows[index].from = "";
return;
} else if (up != "" && +from > +up) {
ElMessage.error(`上限不能超过${up}`);
state.form.ruleRows[index].from = "";
return;
}
let rows = setLimits(index);
if (rows.length == 0) return;
let items = rows.filter((e, i) => {
let isPassDown = e.down !== "" ? +e.down <= +from : false;
let isLessUp = e.up !== "" ? +e.up >= +from : false;
let isLessDownAndPassUp = e.down !== "" && e.up !== "" && to !== "" ? +from < +e.down && +to > +e.up : false;
return (isPassDown && isLessUp) || isLessDownAndPassUp;
});
if (items.length > 0) {
ElMessage.error(`该范围已被设置`);
state.form.ruleRows[index].from = "";
}
};
const inputNum = (index, key) => {
if (state.form.ruleRows[index][key] == "") return;
if (state.form.ruleRows[index][key] == "-") return;
state.form.ruleRows[index][key] = `${state.form.ruleRows[index][key]}`
.replace(/[^\-?\d.]/g, "") //只允许输入负号,数字,小数点
.replace(/(\-)+/, "$1") //过滤连续多个负号
.replace(/(\.)+/, "$1") //过滤连续多个小数点
.replace(/(\.\d+)\./g, "$1") //过滤出现多个小数点
.replace(/(\d)\-/g, "$1"); //过滤处于非开头的负号
};
const changeWarningThresholdTo = (index) => {
let { down, up } = limit.value;
let { from, to } = state.form.ruleRows[index];
if (to !== "" && from !== "" && from >= +to) {
ElMessage.error(`下限不能大于上限`);
state.form.ruleRows[index].to = "";
return;
} else if (down !== "" && +to < +down) {
ElMessage.error(`下限不能小于${down}`);
state.form.ruleRows[index].to = "";
return;
} else if (up != "" && +to > +up) {
ElMessage.error(`上限不能超过${up}`);
state.form.ruleRows[index].to = "";
return;
}
let rows = setLimits(index);
if (rows.length == 0) return;
let items = rows.filter((e, i) => {
let isPassDown = e.down !== "" ? +e.down <= +to : false;
let isLessUp = e.up !== "" ? +e.up >= +to : false;
let isLessDownAndPassUp = e.down !== "" && e.up !== "" && from !== "" ? +from < +e.down && +to > +e.up : false;
return (isPassDown && isLessUp) || isLessDownAndPassUp;
});
if (items.length > 0) {
ElMessage.error(`该范围已被设置`);
state.form.ruleRows[index].to = "";
}
};
const removeRule = (index) => {
state.form.ruleRows.splice(index, 1);
};
const info = () => {
if (!props.form || props.form?.length == 0) {
getRuleTypeOptions();
createRule();
return;
}
getRuleTypeOptions(() => {
state.form.ruleRows =
props.form?.map((e) => {
return {
from: e.from,
to: e.to,
risk_level: e.risk_level,
};
}) || [];
if (state.form.ruleRows.length == 0) {
createRule();
}
});
};
onBeforeMount(() => {
info();
});
defineExpose({
Submit,
form: state.form,
form_ref,
});
</script>
<style lang="scss" scoped>
.gateway-form {
margin-bottom: 16px;
:deep(.gap-title) {
margin-bottom: 16px;
}
&-item {
max-width: 1080px;
width: 100%;
padding-left: 120px;
:deep(.el-table thead th) {
background-color: #f5f6f9;
}
.indicator-expression {
height: 300px;
width: 100%;
:deep(.vue-ace-editor) {
margin-top: 0;
}
}
}
}
:deep(.el-form-item) {
margin-bottom: 0 !important;
}
.warning-threshold {
display: flex;
align-items: center;
:deep(.el-input__wrapper) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
:deep(.el-input-group__append, .el-input-group__prepend) {
border-radius: 4px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
.to {
margin: 0 16px;
}
.line {
width: 1px;
height: 14px;
background-color: #c1c7d7;
margin: 0 16px;
display: inline-block;
}
.rule-table {
:deep(.el-table thead th) {
background-color: #f5f6f9;
}
}
</style>
import { ElMessage } from "element-plus";
import axios from "@/request/http.js";
const setParams = (res, { id }) => {
let isEmpty = res.type_com_ref.isEmpty
let params = {
// 预警规则名称
metric_name: res.name,
// 持续时间
duration: +res.time,
// 持续时间单位
duration_unit: res.unit,
// 检查周期
check_period: +res.inspection_cycle,
// 预警通知方式
notify_method: res.manual_distribution_form.method,
// 预警通知人员列表
notify_recipients: res.manual_distribution_form.lists.map(e => {
return {
system_account: `${e.user_id}`,
user_name: `${e.user_name}`,
phone: `${e.phone}`
}
}),
// 消息推送次数
notify_push_count: +res.push_num,
// 消息推送频率
notify_push_frequency: +res.push_frequency,
// 是否立即启用
is_enabled: res.enabled ? 1 : 2
}
let params_push = {
// 自定义传参
custom: () => {
return {
detection_type: 2,
// 预警分类
class_parent_name: res.type_com_ref.warn_target,
// 预警对象
class_name: res.type_com_ref.warn_type,
// 预警指标
metric_config_name: res.type_com_ref.warn_indicator,
// 指标表达式
expr: res.type_com_ref.indicator_expression || "",
// 预警规则(下拉)
alert_rule_type: res.type_com_ref.rule_type,
// 预警规则对象数组
alert_condition,
alert_range: []
}
},
// 静态阈值传参
static: () => {
return {
detection_type: 1,
// 预警对象
class_id: +res.type_com_ref.warn_type,
metric_config_id: res.type_com_ref.warn_indicator,
// 报警范围(指标)
alert_range: res.type_com_ref.warning_scpoe_form.map(e => {
return {
variable_name: e.variable_name,
metric_name: e.metric_name,
chinese_name: e.chinese_name,
metric_label: e.metric_label,
is_required: e.is_required,
is_linked: e.is_linked,
value: e.select == 'all' ? '.*' : e.value,
compare: e.select == 'all' ? '=~' : e.select
}
}),
// 预警规则(下拉)
alert_rule_type: res.type_com_ref.alert_rule_type,
}
}
}
let alert_condition = []
if (isEmpty) {
alert_condition = [{
thresholds_max: 0,
thresholds_min: 0,
risk_level: +res.type_com_ref.risk_level
}]
} else {
alert_condition = res.type_com_ref.ruleRows.map(e => {
let obj = {
risk_level: +e.risk_level
}
if (e.to !== "") {
obj.thresholds_max = +e.to
}
if (e.from !== "") {
obj.thresholds_min = +e.from
}
return obj
})
}
params = {
...params,
...params_push[res.type_key](),
// 预警规则对象数组
alert_condition,
}
if (id) {
params.id = id
}
return params;
}
export const Save = (res, p, cb) => {
let params = setParams(res, p);
axios[p.id ? 'put' : 'post']('/v1/api/alert_rules', params).then(res => {
if (res.data.code == 200) {
ElMessage.success(`${p.id ? '编辑' : '新增'}成功`)
cb && cb()
} else {
ElMessage.error(res.data.data)
}
})
}
\ No newline at end of file
<template>
<div class="static-form">
<el-form :model="state.form" ref="form_ref" :rules="state.rules" label-width="120px">
<div class="static-form-item">
<el-form-item label="预警对象/分类" prop="warn_type">
<el-cascader
:disabled="props.isEdit"
ref="cascader_ref"
style="flex: 1"
placeholder="请选择预警对象/分类"
:options="staticTypeOptions"
v-model="state.form.warn_type"
@change="changeWarnStaticType"
:props="cascaderProps">
</el-cascader>
</el-form-item>
<el-form-item label="预警指标" prop="warn_indicator">
<el-select
style="flex: 1"
:disabled="!formFormat.warn_type || props.isEdit"
v-model="state.form.warn_indicator"
placeholder="请选择预警指标"
@change="chooseWarnIndicator">
<el-option v-for="item in warningIndexOptions" :key="item.id" :label="item.metric_name" :value="item.id">
</el-option>
</el-select>
</el-form-item>
</div>
<div>
<div v-if="state.form.warning_scpoe_form.length > 0">
<gap-title :hasLine="true" title="预警范围"></gap-title>
<div class="static-form-item">
<div class="warning-scope-main">
<div class="warn-scpoe-form" v-for="(item, index) in state.form.warning_scpoe_form" :key="index">
<el-form-item :label="item.chinese_name">
<el-select class="warn-scpoe-select" v-model="item.select" style="width: 114px">
<el-option v-for="(value, key) in selectRule" :key="key" :label="value" :value="key" />
</el-select>
</el-form-item>
<el-form-item
label=""
class="no-el-label warn-scpoe-input-item"
prop="value"
:rules="[
{
validator: (rule, value, callback) => validateValue(rule, value, callback, item, index),
trigger: showSelect.includes(item.select) ? 'change' : 'blur',
},
]">
<el-input
class="warn-scpoe-input-value"
v-model="state.form.warning_scpoe_form[index].value"
:placeholder="`请输入${item.chinese_name}`"
v-if="!item.is_linked"
:disabled="item.select == 'all'">
</el-input>
<el-select
class="warn-scpoe-input-value"
v-else
v-model="item.value"
:placeholder="`请选择${item.chinese_name}`"
filterable
:loading="item.loading"
remote
:disabled="item.select == 'all'"
:remote-method="(query) => remoteMethod(query, index, item)">
<el-option v-for="item in item.options" :key="item" :label="item" :value="item"> </el-option>
</el-select>
</el-form-item>
</div>
</div>
</div>
</div>
<gap-title :hasLine="true" title="预警规则"></gap-title>
<div class="static-form-item">
<div class="warning-rule-main">
<el-form-item label="风险程度" prop="risk_level" v-if="isEmpty">
<el-select style="flex: 1" v-model="state.form.risk_level" placeholder="请选择风险程度" filterable>
<el-option v-for="item in riskLevelOptions" :key="item.id" :label="item.name" :value="item.id">
</el-option>
</el-select>
</el-form-item>
<Gateway v-else ref="warn_type_com" :form="rule_rows" :rule_type="alert_rule_type" />
</div>
</div>
</div>
</el-form>
</div>
</template>
<script setup>
import { computed, nextTick, onMounted, reactive, ref, shallowReactive, watch } from "vue";
import gapTitle from "@/components/gap-title.vue";
import Gateway from "./gateway.vue";
import { ElMessage } from "element-plus";
import axios from "@/request/http.js";
import { GetRuleTypeOptions, Empty } from "@/components/env.js";
const showSelect = ["=~", "!~"];
const selectRule = ref({
all: "全部",
"=": "等于",
"!=": "不等于",
"=~": "正则匹配",
"!~": "正则不匹配",
});
const riskLevelOptions = ref([
{
id: 4,
name: "重大风险",
},
{
id: 3,
name: "较大风险",
},
{
id: 2,
name: "一般风险",
},
{
id: 1,
name: "低风险",
},
]);
const props = defineProps({
form: {
type: Object,
default: null,
},
isEdit: {
type: Boolean,
default: false,
},
});
const validateValue = (rule, value, callback, item, index) => {
if (!item.is_required || item.select == "all") return callback();
if (item.value == "") {
let msg = showSelect.includes(item.select) ? `请选择${item.chinese_name}` : `请输入${item.chinese_name}`;
return callback(new Error(msg));
}
return callback();
};
const state = reactive({
form: {
warn_type: [],
warn_indicator: "",
risk_level: "",
warning_scpoe_form: [],
},
rules: {
warn_type: [{ type: "array", required: true, message: "请选择预警对象/分类", trigger: "change" }],
warn_indicator: [{ required: true, message: "请选择预警指标", trigger: "change" }],
risk_level: [{ required: true, message: "请选择风险程度", trigger: "change" }],
},
});
const info = () => {
state.form.warn_type =
props.form.warn_target && props.form.warn_type ? [props.form.warn_target, props.form.warn_type] : [];
state.form.warn_indicator = props.form.warn_indicator;
alert_rule_type.value = props.form.rule_type || "1";
let params = {
page: 1,
page_size: 10000000000000,
class_id: props.form.warn_type,
is_enabled: 1,
};
getWarningIndicator(params, () => {
state.form.warning_scpoe_form = props.form?.warning_scpoe_form || [];
});
state.form.risk_level = props.form.risk_level;
};
const ruleTypeOptions = ref({});
const getRuleTypeOptions = async (cb) => {
ruleTypeOptions.value = await GetRuleTypeOptions();
if (props.form) {
info();
}
};
watch(
() => props.form,
(n) => {
if (!n) return;
info();
},
{
deep: true,
}
);
const module_data = ref({});
const alert_rule_type = ref("");
const isEmpty = computed(() => {
return Empty(alert_rule_type.value, ruleTypeOptions.value);
});
const emits = defineEmits(["update-duration"]);
const chooseWarnIndicator = () => {
axios
.get("/v1/api/metric_config", {
params: {
id: state.form.warn_indicator,
},
})
.then((res) => {
if (res.data.code == 200) {
module_data.value = res.data.data;
alert_rule_type.value = module_data.value.alert_rule_type;
state.form.warning_scpoe_form = module_data.value.alert_range.map((e) => {
return {
...e,
value: "",
select: "all",
options: [],
loading: false,
};
});
emits("update-duration", module_data.value);
} else {
ElMessage.error(res.data.data);
}
});
};
const remoteMethod = (query, index, item) => {
state.form.warning_scpoe_form[index].loading = true;
const params = {
metric_name: item.metric_name,
// metric_name: "grpc_client_handled_total",
metric_label: item.metric_label,
// metric_label: "grpc_method",
value: query,
};
axios.get(`/v1/api/prometheus/value`, { params }).then((res) => {
if (res.data.code == 200) {
state.form.warning_scpoe_form[index].options = res.data.data.list || [];
state.form.warning_scpoe_form[index].loading = false;
}
});
};
const warn_type_com_form = computed(() => props.form?.warn_type_com || null);
const cascaderProps = {
value: "class_id",
label: "class_name",
};
const staticTypeOptions = ref([]);
const staticTypeFormat = (res) => {
let arr = res?.map((e) => {
let children = [];
let is_disabled = false;
if (e.children?.length == 0 && e.parent_id == 0) {
children = [];
is_disabled = true;
} else {
children = staticTypeFormat(e.children);
is_disabled = false;
}
return {
...e,
children: children,
disabled: is_disabled,
};
});
return arr;
};
const getStaticTypeOptions = () => {
axios.get("/v1/api/alert_class/tree").then(async (res) => {
if (res.data.code == 200) {
staticTypeOptions.value = staticTypeFormat(res.data.data);
} else {
ElMessage.error(res.data.msg);
}
});
};
const formFormat = computed(() => {
let [warn_target = "", warn_type = ""] = state.form.warn_type || [];
let obj = {
...state.form,
warn_target,
warn_type,
isEmpty: isEmpty.value,
alert_rule_type: module_data.value.alert_rule_type,
};
if (!isEmpty.value) {
obj = {
...obj,
...(warn_type_com.value?.form || {}),
};
}
return obj;
});
const warningIndexOptions = ref({});
const cascader_ref = ref(null);
const changeWarnStaticType = async () => {
let class_id = state.form.warn_type[1];
if (!class_id) return;
let params = { page: 1, page_size: 10000000000000, class_id, is_enabled: 1 };
getWarningIndicator(params);
};
const getWarningIndicator = (params, cb) => {
axios.get("/v1/api/metric_config/list", { params }).then((res) => {
if (res.data.code == 200) {
warningIndexOptions.value = res.data.data?.list || [];
if (!cb) {
state.form.warn_indicator = warningIndexOptions.value[0]?.id || "";
if (state.form.warn_indicator) {
chooseWarnIndicator();
}
} else {
cb();
}
setTimeout(() => {
form_ref.value.clearValidate(["warn_indicator"]);
});
}
});
};
const changeRuleType = () => {};
const form_ref = ref(null);
const warn_type_com = ref(null);
const Submit = async () => {
let form_valid = await new Promise((resolve, reject) => {
form_ref.value.validate((res) => resolve(res));
});
if (isEmpty.value) return form_valid;
let warn_type_com_valid = await warn_type_com.value?.Submit();
return form_valid && warn_type_com_valid;
};
const ruleHeaders = [
{
prop: "warning_threshold",
label: "预警阈值",
width: "500px",
},
{
prop: "risk_level",
label: "风险程度",
},
];
const rule_rows = computed(() => props.form?.ruleRows || []);
onMounted(() => {
getStaticTypeOptions();
getRuleTypeOptions();
});
defineExpose({
Submit,
form: formFormat,
form_ref,
});
</script>
<style lang="scss" scoped>
.static-form {
:deep(.gap-title) {
margin-bottom: 16px;
}
&-item {
max-width: 1080px;
width: 100%;
padding-left: 8px;
.warning-scope-main {
// display: grid;
// grid-template-columns: 1fr 1fr;
// grid-column-gap: 16px;
:deep(.el-input-group__prepend) {
border-radius: 4px;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.warn-scpoe-form {
width: 100%;
display: flex;
align-items: center;
.warn-scpoe-select {
width: 114px !important;
:deep(.el-input__wrapper) {
background-color: #2b4695;
border-radius: 4px;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
.el-input__inner {
color: #fff;
}
}
}
.warn-scpoe-input-item {
flex: 1;
.warn-scpoe-input-value {
flex: 1;
:deep(.el-input__wrapper) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
}
}
}
}
}
.no-el-label {
:deep(.el-form-item__content) {
margin-left: 0 !important;
}
}
</style>
<template>
<div class="my-warn-detail">
<div class="breadcrumb">
<bg-breadcrumb />
</div>
<div class="content bg-scroll">
<warn-detail :label-data="labelData" :value-data="info" :tabLabels="tabLabels" :tabDatas="tabDatas">
<template #status="{ item, valueData }">
<span class="status-body" v-if="valueData.status">
<span class="status" :class="`status-${valueData.status}`"></span>
<span>{{ statusOptions[valueData[item.prop]] }}</span>
</span>
<span v-else>-</span>
</template>
</warn-detail>
</div>
</div>
</template>
<script setup>
import bgBreadcrumb from "@/components/bg-breadcrumb.vue";
import warnDetail from "@/components/warn-detail/index.vue";
import { useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import axios from "@/request/http.js";
import { ref, onBeforeMount } from "vue";
import { dateStringToDate } from "@/components/env";
const route = useRoute();
const { id } = route.query;
const ruleTypeOptions = ref({});
const statusOptions = {
1: "已恢复",
2: "未恢复",
3: "已关闭",
};
const riskLevels = {
1: "低风险",
2: "一般风险",
3: "较大风险",
4: "重大风险",
};
const labelData = [
[
{
label: "预警点",
prop: "warning_point",
},
{
label: "预警分类",
prop: "warning_type",
},
],
[
{
label: "预警指标",
prop: "warning_index",
},
{
label: "风险等级",
prop: "risk_level",
},
],
[
{
label: "状态",
prop: "status",
},
{
label: "预警阈值",
prop: "warning_threshold",
},
],
[
{
label: "当前报警值",
prop: "current_alarm_value",
},
{
label: "预警时间",
prop: "warning_time",
},
],
];
const info = ref({});
const tabLabels = [
{
label: "推送记录",
prop: "push",
},
{
label: "处置记录",
prop: "dispose",
},
];
const tabDatas = ref({});
const pushType = {
1: "自动推送",
2: "手动推送",
};
const getInfo = () => {
const params = {
id,
};
axios.get("/v1/api/work_order/alert", { params }).then((res) => {
if (res.data.code == 200) {
const { data } = res.data;
info.value = {
warning_point: data.alert_point,
warning_type: data.class_parent_name,
warning_index: data.class_name,
risk_level: riskLevels[data.risk_level],
status: data.status,
warning_threshold: `${data.alert_condition.thresholds_min}${
ruleTypeOptions.value[data.alert_rule_type]?.unit || ""
}-${data.alert_condition.thresholds_max}${ruleTypeOptions.value[data.alert_rule_type]?.unit || ""}`,
current_alarm_value: data.current_value + (ruleTypeOptions.value[data.alert_rule_type]?.unit || ""),
warning_time: dateStringToDate(data.alert_time),
};
tabDatas.value = {
push:
data.push_records?.map((e) => {
return {
method: e.notify_method,
person: e.system_account,
push_time: dateStringToDate(e.push_time),
push_type: pushType[e.push_type],
status: e.status,
};
}) || [],
dispose:
data.disposed_list?.map((e) => {
return {
status: e.is_disposed,
feedback: e.disposal_content,
feedback_time: dateStringToDate(e.disposal_time),
feedback_person: e.disposal_user,
};
}) || [],
};
}
});
};
const GetRuleTypeOptions = () => {
const params = {
page: 1,
page_size: 10000000000000,
class: 3,
};
axios.get(`/v1/api/dict`, { params }).then((res) => {
if (res.data.code == 200) {
res.data.data?.forEach((e) => {
let isEmptyOption = e.name == "空";
ruleTypeOptions.value[e.id] = {
label: e.name,
unit: isEmptyOption ? "" : e.unit,
};
});
getInfo();
}
});
};
onBeforeMount(() => {
GetRuleTypeOptions();
});
</script>
<style lang="scss" scoped>
.my-warn-detail {
width: 100%;
height: 100%;
padding: 0 24px 16px;
.breadcrumb {
width: 100%;
height: 46px;
}
.content {
width: 100%;
height: calc(100% - 46px);
background-color: #ffffff;
box-shadow: 0px 1px 4px 0px rgba(0, 7, 101, 0.15);
border-radius: 6px;
padding: 32px;
.status {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
margin-right: 8px;
$statusObj: (
1: #48ad97,
3: #9e9e9e,
2: #3759be,
);
@each $status, $color in $statusObj {
&-#{$status} {
background-color: $color;
}
}
}
.status-body {
display: flex;
align-items: center;
}
}
}
</style>
<template>
<div class="my-warn-ticket">
<div class="breadcrumb">
<bg-breadcrumb />
</div>
<div class="content">
<bg-filter-group @search="changeSearch" v-model="filter.keyword" placeholder="请输入预警点/分类/指标">
<template v-slot:filter_group>
<div class="left-filter filter_list">
<div class="filter_item">
<span class="filter_title">风险等级</span>
<el-select v-model="filter.risk_level" placeholder="请选择" style="width: 300px">
<el-option
v-for="(value, key, index) in riskLevels"
:key="'riskLevels' + index"
:label="value"
:value="key">
</el-option>
</el-select>
</div>
<div class="filter_item">
<span class="filter_title">状态</span>
<el-select v-model="filter.status" placeholder="请选择" style="width: 300px">
<el-option
v-for="(value, key, index) in statusOptions"
:key="'stateOptions' + index"
:label="value"
:value="key">
</el-option>
</el-select>
</div>
<div class="filter_item">
<span class="filter_title">预警时段</span>
<el-date-picker
v-model="filter.time"
style="width: 400px"
type="datetimerange"
value-format="YYYY-MM-DD HH:mm:ss"
range-separator="-"
start-placeholder="开始时间"
end-placeholder="结束时间">
</el-date-picker>
</div>
</div>
<div class="right-action apaas_button">
<el-button type="primary" @click="filterAction"> 查询 </el-button>
<el-button type="default" @click="filterClear"> 重置 </el-button>
</div>
</template>
</bg-filter-group>
<div class="table_container">
<div class="table">
<bg-table ref="listtable" :headers="headers" :rows="rows" height="100%" :isIndex="true" :stripe="true">
<template v-slot:alert_point="{ row }">
<span class="can_click_text" @click="goDetail(row)">
{{ row.alert_point }}
</span>
</template>
<template v-slot:risk_level="{ row }">
{{ riskLevels[row.risk_level] }}
</template>
<template v-slot:current_value="{ row }">
{{ row.current_value }}{{ ruleTypeOptions[row.alert_rule_type]?.unit || "" }}
</template>
<template v-slot:warn_threshold="{ row }">
{{ row.alert_condition.thresholds_min }}{{ ruleTypeOptions[row.alert_rule_type]?.unit || "" }}
-
{{ row.alert_condition.thresholds_max }}{{ ruleTypeOptions[row.alert_rule_type]?.unit || "" }}
</template>
<template v-slot:alert_time="{ row }">
{{ dateStringToDate(row.alert_time) }}
</template>
<template v-slot:last_push_time="{ row }">
{{ dateStringToDate(row.last_push_time) }}
</template>
<template #status="{ row }">
<span :class="`circle status-${row.status}`"></span>
{{ statusOptions[row.status] }}
</template>
<template #operation="{ row }">
<el-button
type="primary"
:disabled="row.status != 2 || row.is_disposed == 1"
link
size="small"
@click="operation(row)">
处置反馈
</el-button>
</template>
</bg-table>
<div class="pagination_box">
<bg-pagination
:page="filter.page"
:size="filter.page_size"
:total="tableTotal"
@change-page="changePage"
@change-size="changeSize">
</bg-pagination>
</div>
</div>
</div>
</div>
<FeedBack v-model:visible="visible" :active_row="active_row" :isWarn="true" />
</div>
</template>
<script setup>
import { reactive, ref, onBeforeMount, toRefs, computed, watch, nextTick, watchEffect } from "vue";
import { ElMessage } from "element-plus";
import axios from "@/request/http.js";
import bgBreadcrumb from "@/components/bg-breadcrumb.vue";
import FeedBack from "../modules/feedback.vue";
import { useRouter } from "vue-router";
import { GetRuleTypeOptions, dateStringToDate } from "@/components/env.js";
const router = useRouter();
const filter = reactive({
risk_level: "", // 风险等级
status: "", // 状态
time: [],
keyword: "",
page: 1,
page_size: 10,
});
const tableTotal = ref(0);
const ruleTypeOptions = ref({});
const filterClear = () => {
filter.warning_index = "";
filter.risk_level = "";
filter.status = "";
filter.time = [];
filter.page_size = 10;
filter.page = 1;
changePage(1);
}; // 重置筛选项
const statusOptions = ["全部", "已恢复", "未恢复", "已关闭"];
// const disposedOptions = ["", "已处置", "已处置"];
const riskLevels = {
"": "全部",
1: "低风险",
2: "一般风险",
3: "较大风险",
4: "重大风险",
};
let headers = reactive([
{
label: "预警点",
prop: "alert_point",
align: "left",
width: 180,
},
{
label: "预警时间",
prop: "alert_time",
align: "left",
width: 160,
},
{
label: "预警分类",
prop: "class_parent_name",
align: "left",
},
{
label: "预警指标",
prop: "class_name",
align: "left",
},
{
label: "风险等级",
prop: "risk_level",
align: "left",
},
{
label: "当前报警值",
prop: "current_value",
align: "left",
},
{
label: "预警阀值",
prop: "warn_threshold",
align: "left",
},
{
label: "推送次数",
prop: "push_count",
align: "left",
},
{
label: "最近推送时间",
prop: "last_push_time",
align: "left",
width: 160,
},
{
label: "状态",
prop: "status",
align: "left",
width: 90,
},
{
label: "操作",
prop: "operation",
align: "left",
width: 80,
fixed: "right",
},
]);
let rows = ref([]);
const changeSize = (size) => {
filter.page_size = size;
changePage(1);
};
const changePage = (page) => {
filter.page = page;
getTableRows();
};
const changeSearch = (val) => {
filter.keyword = val;
changePage(1);
}; // 表格关键字筛选
const filterAction = () => {
changePage(1);
}; // 查询按钮
const getTableRows = async () => {
let [start_time = "", end_time = ""] = filter.time || [];
let params = {
...filter,
start_time,
end_time,
};
Reflect.deleteProperty(params, "time");
axios.get("/v1/api/work_order/alert/list", { params }).then((res) => {
if (res.data.code == 200) {
let { list, total_count } = res.data.data;
rows.value = list || [];
tableTotal.value = total_count;
} else {
ElMessage.error(res.data.msg);
}
});
};
// 处置反馈
const visible = ref(false);
const active_row = ref(null);
const operation = (row) => {
active_row.value = { ...row, url: "/v1/api/work_order/alert/dispose" };
visible.value = true;
};
const goDetail = ({ id }) => {
router.push({
path: "/ticket/my-warn-ticket/detail",
query: {
id,
},
});
};
const getRuleTypeOptions = async () => {
ruleTypeOptions.value = await GetRuleTypeOptions();
getTableRows();
};
onBeforeMount(() => {
getRuleTypeOptions();
});
</script>
<style lang="scss" scoped>
.my-warn-ticket {
width: 100%;
height: 100%;
padding: 0 24px 16px;
.breadcrumb {
width: 100%;
height: 46px;
}
.content {
width: 100%;
height: calc(100% - 46px);
background-color: #ffffff;
box-shadow: 0px 1px 4px 0px rgba(0, 7, 101, 0.15);
border-radius: 6px;
display: flex;
flex-direction: column;
padding: 16px;
:deep(.bg-filter-group) {
padding: 0;
}
.filter-group {
.left-filter {
display: flex;
justify-content: start;
flex-wrap: wrap;
}
.right-action {
white-space: nowrap;
}
}
.table_container {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
.table {
position: absolute;
flex: 1;
width: 100%;
height: calc(100% - 48px);
.href {
font-size: 14px;
color: #3759be;
cursor: pointer;
}
}
}
:deep(.el-button.is-link) {
line-height: 1;
}
.bg-pagination {
bottom: unset;
}
}
}
</style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment