Files
emsfront/src/views/ems/site/sbbh/AddPlan.vue
2026-02-15 16:24:29 +08:00

718 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<el-dialog
v-loading="loading"
width="92%"
:visible.sync="dialogTableVisible"
class="ems-dialog plan-dialog"
title="保护方案"
:close-on-click-modal="false"
:show-close="false"
>
<el-form
v-loading="loading > 0"
ref="addTempForm"
:model="formData"
:rules="rules"
size="medium"
label-width="120px"
class="plan-form"
>
<div class="base-panel">
<div class="panel-title">基础信息</div>
<div class="base-grid">
<el-form-item label="设备保护名称" prop="faultName" class="span-1">
<el-input v-model="formData.faultName" placeholder="请输入" clearable></el-input>
</el-form-item>
<el-form-item label="是否告警" prop="isAlert" class="span-1">
<el-checkbox v-model="formData.isAlert" :true-label="1" :false-label="0">启用告警</el-checkbox>
</el-form-item>
<el-form-item label="告警等级" prop="faultLevel" class="span-1">
<el-radio-group v-model="formData.faultLevel" :disabled="mode === 'edit'">
<el-radio :label="1">等级1</el-radio>
<el-radio :label="2">等级2</el-radio>
<el-radio :label="3">等级3</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="处理方案描述" prop="description" class="span-2">
<el-input
v-model="formData.description"
type="textarea"
:rows="2"
placeholder="请输入"
clearable
></el-input>
</el-form-item>
</div>
</div>
<div class="plan-section">
<div class="section-head">
<div>
<div class="section-title">保护前提</div>
<div class="section-desc">配置触发故障的判定条件和关系</div>
</div>
<div class="section-actions">
<el-button @click.native.prevent="addRow('protectionSettings')" type="primary" size="mini">
新增保护前提
</el-button>
</div>
</div>
<div class="row-card" v-for="(item, index) in protectionSettings" :key="'protectionSettings' + index">
<div class="row-index">{{ index + 1 }}</div>
<div class="row-grid setting-grid">
<div class="field-block field-point">
<div class="field-label">点位</div>
<el-autocomplete
v-model="item.point"
placeholder="请输入点位ID/描述"
clearable
:trigger-on-focus="false"
:debounce="250"
:value-key="'label'"
:fetch-suggestions="(q, c) => querySearchAsync(q, c, index, 'protectionSettings')"
@select="(v) => handleSelect(v, index, 'protectionSettings')"
@blur="() => fillPointMetaByPoint(item.point, index, 'protectionSettings')"
></el-autocomplete>
</div>
<div class="field-block">
<div class="field-label">故障比较符</div>
<el-select v-model="item.faultOperator" placeholder="请选择">
<el-option
v-for="(value, key) in comparisonOperatorOptions"
:key="key + 'faultOperator'"
:label="key"
:value="value"
></el-option>
</el-select>
</div>
<div class="field-block">
<div class="field-label">故障值</div>
<el-input placeholder="请输入故障值" v-model="item.faultValue"></el-input>
</div>
<div class="field-block">
<div class="field-label">释放比较符</div>
<el-select v-model="item.releaseOperator" placeholder="请选择">
<el-option
v-for="(value, key) in comparisonOperatorOptions"
:key="key + 'releaseOperator'"
:label="key"
:value="value"
></el-option>
</el-select>
</div>
<div class="field-block">
<div class="field-label">释放值</div>
<el-input placeholder="请输入释放值" v-model="item.releaseValue"></el-input>
</div>
<div class="field-block">
<div class="field-label">关系</div>
<el-select v-model="item.relationNext" placeholder="请选择">
<el-option
v-for="(value, key) in relationWithPoint"
:key="key + 'relation'"
:label="key"
:value="value"
></el-option>
</el-select>
</div>
</div>
<div class="row-action">
<el-button @click.native.prevent="deleteRow(index, 'protectionSettings')" type="warning" size="mini">
删除
</el-button>
</div>
</div>
<el-empty v-if="protectionSettings.length === 0" description="暂无保护前提请先添加"></el-empty>
</div>
<div class="plan-section">
<div class="section-head">
<div>
<div class="section-title">保护方案</div>
<div class="section-desc">设置触发后的目标写入点位和值</div>
</div>
<div class="section-actions">
<el-button @click.native.prevent="addRow('protectionPlan')" type="primary" size="mini">
新增保护方案
</el-button>
</div>
</div>
<div class="row-card" v-for="(item, index) in protectionPlan" :key="'protectionPlan' + index">
<div class="row-index">{{ index + 1 }}</div>
<div class="row-grid plan-grid">
<div class="field-block field-point">
<div class="field-label">点位</div>
<el-autocomplete
v-model="item.point"
placeholder="请输入点位ID/描述"
clearable
:trigger-on-focus="false"
:debounce="250"
:value-key="'label'"
:fetch-suggestions="(q, c) => querySearchAsync(q, c, index, 'protectionPlan')"
@select="(v) => handleSelect(v, index, 'protectionPlan')"
@blur="() => fillPointMetaByPoint(item.point, index, 'protectionPlan')"
></el-autocomplete>
</div>
<div class="field-block eq-block">
<div class="field-label">比较</div>
<div class="eq-text">=</div>
</div>
<div class="field-block">
<div class="field-label">故障值</div>
<el-input placeholder="请输入故障值" v-model="item.value"></el-input>
</div>
</div>
<div class="row-action">
<el-button @click.native.prevent="deleteRow(index, 'protectionPlan')" type="warning" size="mini">
删除
</el-button>
</div>
</div>
<el-empty v-if="protectionPlan.length === 0" description="暂无保护方案请先添加"></el-empty>
</div>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="closeDialog">取消</el-button>
<el-button type="primary" @click="saveDialog">确定</el-button>
</div>
</el-dialog>
</template>
<script>
import { mapState } from "vuex";
import { validText } from "@/utils/validate";
import { addProtectPlan, getPointMatchList, getProtectPlan, updateProtectPlan } from "@/api/ems/site";
export default {
data() {
const validateText = (rule, value, callback) => {
if (value !== "" && !validText(value)) {
callback(new Error("只能输入中文英文数字和特殊字符!"));
} else {
callback();
}
};
return {
mode: "",
loading: 0,
protectionSettings: [],
protectionPlan: [],
dialogTableVisible: false,
formData: {
id: "",
siteId: "",
faultName: "",
isAlert: 0,
faultLevel: 1,
faultDelaySeconds: "",
releaseDelaySeconds: "",
description: "",
},
rules: {
faultName: [{ required: true, message: "请输入设备保护名称", trigger: "blur" }],
isAlert: [{ required: true, message: "请选择是否告警", trigger: "blur" }],
description: [
{ required: true, message: "请输入设备描述", trigger: "blur" },
{ validator: validateText, trigger: "blur" },
],
},
};
},
computed: {
...mapState({
comparisonOperatorOptions: (state) => state?.ems?.comparisonOperatorOptions || {},
relationWithPoint: (state) => state?.ems?.relationWithPoint || {},
}),
},
methods: {
open(id, siteId) {
const selectedSiteId = siteId || "";
this.formData.siteId = selectedSiteId;
this.dialogTableVisible = true;
if (id) {
this.formData.id = id;
this.mode = "edit";
getProtectPlan(id).then((response) => {
const data = response?.data || {};
this.formData = {
id,
siteId: selectedSiteId || data?.siteId || "",
faultName: data?.faultName || "",
isAlert: data?.isAlert || 0,
faultLevel: data?.faultLevel || 1,
faultDelaySeconds: data?.faultDelaySeconds || "",
releaseDelaySeconds: data?.releaseDelaySeconds || "",
description: data?.description || "",
};
const plan = JSON.parse(data?.protectionPlan || "[]");
const settings = JSON.parse(data?.protectionSettings || "[]");
this.$nextTick(() => {
this.protectionPlan.splice(0, 0, ...plan);
this.protectionSettings.splice(0, 0, ...settings);
});
});
} else {
this.mode = "add";
}
},
addRow(type) {
const item =
type === "protectionSettings"
? {
deviceId: "",
deviceName: "",
deviceCategory: "",
categoryName: "",
point: "",
pointName: "",
faultValue: "",
releaseValue: "",
faultOperator: "",
releaseOperator: "",
relationNext: "",
}
: {
deviceId: "",
deviceName: "",
deviceCategory: "",
categoryName: "",
point: "",
pointName: "",
value: "",
};
this[type].splice(this[type].length, 0, item);
},
deleteRow(index, type) {
this[type].splice(index, 1);
},
querySearchAsync(query, cb, index, type) {
if (!this.formData.siteId) {
this.$message({
type: "warning",
message: "请先选择站点",
});
return cb([]);
}
getPointMatchList({
siteId: this.formData.siteId,
pageNum: 1,
pageSize: 100,
pointId: query || "",
pointDesc: query || "",
})
.then((response) => {
const data = response?.rows || [];
cb(
data.map((item) => {
const pointLabel = item.pointId || item.pointName || item.dataKey || "";
const desc = item.pointDesc || "";
return {
value: pointLabel,
pointName: item.pointName || pointLabel,
pointId: item.pointId || pointLabel,
pointDesc: desc,
deviceId: item.deviceId || "",
deviceName: item.deviceName || "",
deviceCategory: item.deviceCategory || "",
categoryName: item.deviceCategory || "",
label: desc ? `${pointLabel}${desc}` : pointLabel,
};
})
);
})
.catch(() => cb([]));
},
fillPointMetaByPoint(point, index, type) {
if (!this.formData.siteId || !point) {
return;
}
getPointMatchList({
siteId: this.formData.siteId,
pageNum: 1,
pageSize: 1,
pointId: point,
}).then((response) => {
const row = (response?.rows || [])[0];
if (!row) return;
const line = Object.assign({}, this[type][index], {
point: row.pointId || point,
pointName: row.pointName || row.pointId || point,
deviceId: row.deviceId || this[type][index].deviceId,
deviceName: row.deviceName || this[type][index].deviceName,
deviceCategory: row.deviceCategory || this[type][index].deviceCategory,
categoryName: row.deviceCategory || this[type][index].categoryName,
});
this[type].splice(index, 1, line);
});
},
handleSelect(data, index, type) {
const line = Object.assign({}, this[type][index], {
point: data.pointId || data.value,
pointName: data.pointName || data.pointId || data.value,
deviceId: data.deviceId || this[type][index].deviceId,
deviceName: data.deviceName || this[type][index].deviceName,
deviceCategory: data.deviceCategory || this[type][index].deviceCategory,
categoryName: data.categoryName || data.deviceCategory || this[type][index].categoryName,
});
this[type].splice(index, 1, line);
},
saveDialog() {
function getToastMsg(name, type, index) {
return {
protectionSettings: {
deviceId: `请选择保护前提第${index}行的设备`,
deviceCategory: `请选择保护前提第${index}行的设备类型`,
categoryName: `请选择保护前提第${index}行的设备类型`,
point: `请选择保护前提第${index}行的点位`,
pointName: `请选择保护前提第${index}行的点位`,
faultValue: `请输入保护前提第${index}行的故障值`,
releaseValue: `请输入保护前提第${index}行的释放值`,
faultOperator: `请选择保护前提第${index}行的故障值比较关系`,
releaseOperator: `请选择保护前提第${index}行的释放值比较关系`,
relationNext: `请选择保护前提第${index}行与下一个点位的关系`,
},
protectionPlan: {
deviceId: `请选择保护方案第${index}行的设备`,
deviceCategory: `请选择保护方案第${index}行的设备类型`,
categoryName: `请选择保护方案第${index}行的设备类型`,
point: `请选择保护方案第${index}行的点位`,
pointName: `请选择保护方案第${index}行的点位`,
value: `请输入保护方案第${index}行的故障值`,
},
}[type][name];
}
this.$refs.addTempForm.validate((valid) => {
if (!valid) return;
const {
id = "",
siteId = "",
faultName = "",
isAlert = 0,
faultLevel = 1,
faultDelaySeconds = "",
releaseDelaySeconds = "",
description = "",
} = this.formData;
const { protectionSettings, protectionPlan } = this;
let protectionSettingsValidateStatus = true;
let protectionPlanValidateStatus = true;
const settingRequiredFields = ["point", "faultOperator", "faultValue", "releaseOperator", "releaseValue"];
const planRequiredFields = ["point", "value"];
for (let i = 0; i < protectionSettings.length; i++) {
const row = protectionSettings[i] || {};
for (let inner = 0; inner < settingRequiredFields.length; inner++) {
const key = settingRequiredFields[inner];
const value = row[key];
if (![0, "0"].includes(value) && !value) {
this.$message.error(getToastMsg(key, "protectionSettings", i + 1));
protectionSettingsValidateStatus = false;
break;
}
}
if (!protectionSettingsValidateStatus) break;
if (!row.deviceId) {
this.$message.error(`请选择保护前提第${i + 1}行的点位(需从点位列表中选择)`);
protectionSettingsValidateStatus = false;
break;
}
if (protectionSettings[i + 1] && !row.relationNext) {
this.$message.error(getToastMsg("relationNext", "protectionSettings", i + 1));
protectionSettingsValidateStatus = false;
break;
}
}
for (let i = 0; i < protectionPlan.length; i++) {
const row = protectionPlan[i] || {};
for (let inner = 0; inner < planRequiredFields.length; inner++) {
const key = planRequiredFields[inner];
const value = row[key];
if (![0, "0"].includes(value) && !value) {
this.$message.error(getToastMsg(key, "protectionPlan", i + 1));
protectionPlanValidateStatus = false;
break;
}
}
if (!protectionPlanValidateStatus) break;
if (!row.deviceId) {
this.$message.error(`请选择保护方案第${i + 1}行的点位(需从点位列表中选择)`);
protectionPlanValidateStatus = false;
break;
}
}
if (!protectionSettingsValidateStatus || !protectionPlanValidateStatus) return;
const settings = protectionSettings.map((item) => Object.assign({}, item));
const plan = protectionPlan.map((item) => Object.assign({}, item));
this.loading += 1;
const params = {
siteId,
faultName,
isAlert,
faultLevel,
faultDelaySeconds,
releaseDelaySeconds,
description,
protectionSettings: JSON.stringify(settings),
protectionPlan: JSON.stringify(plan),
};
if (this.mode === "add") {
addProtectPlan(params)
.then((response) => {
if (response.code === 200) {
this.$emit("update");
this.closeDialog();
}
})
.finally(() => {
this.loading -= 1;
});
} else {
params.id = id;
updateProtectPlan(params)
.then((response) => {
if (response.code === 200) {
this.$emit("update");
this.closeDialog();
}
})
.finally(() => {
this.loading -= 1;
});
}
});
},
closeDialog() {
this.$emit("clear");
for (let key in this.formData) {
this.formData[key] = key === "isAlert" ? 0 : key === "faultLevel" ? 1 : "";
}
this.$refs.addTempForm.resetFields();
this.$set(this, "protectionSettings", []);
this.$set(this, "protectionPlan", []);
this.dialogTableVisible = false;
},
},
};
</script>
<style scoped lang="scss">
.plan-form {
background: #f5f8ff;
border: 1px solid #e8edf7;
border-radius: 14px;
padding: 16px;
}
.base-panel {
background: #fff;
border: 1px solid #e7ecf6;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
}
.panel-title {
font-size: 15px;
font-weight: 600;
color: #1f2d3d;
margin-bottom: 14px;
}
.base-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px 18px;
.span-1 {
grid-column: span 1;
}
.span-2 {
grid-column: span 2;
}
}
.plan-section {
background: #fff;
border: 1px solid #e7ecf6;
border-radius: 12px;
padding: 16px;
margin-bottom: 14px;
}
.section-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 12px;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: #1f2d3d;
line-height: 1;
margin-bottom: 6px;
}
.section-desc {
color: #7b8999;
font-size: 12px;
}
.section-actions {
display: flex;
align-items: center;
gap: 10px;
}
.row-card {
display: flex;
gap: 12px;
align-items: stretch;
border: 1px solid #e9edf5;
border-radius: 12px;
padding: 12px;
margin-bottom: 12px;
transition: all 0.2s ease;
&:hover {
border-color: #d5e2fb;
box-shadow: 0 4px 14px rgba(30, 70, 140, 0.08);
}
}
.row-index {
width: 30px;
height: 30px;
border-radius: 15px;
background: #eaf1ff;
color: #2d5ee8;
font-size: 13px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.row-grid {
flex: 1;
display: grid;
gap: 10px;
}
.setting-grid {
grid-template-columns: 2fr repeat(5, minmax(120px, 1fr));
}
.plan-grid {
grid-template-columns: 2fr 80px 1fr;
}
.field-block {
.field-label {
color: #66788c;
font-size: 12px;
margin-bottom: 6px;
}
.el-select,
.el-autocomplete,
.el-input {
width: 100%;
}
}
.eq-block {
.eq-text {
height: 36px;
border: 1px solid #dfe5f0;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: #4a5b6f;
font-weight: 600;
background: #f8faff;
}
}
.row-action {
width: 72px;
display: flex;
align-items: flex-start;
justify-content: center;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
}
@media (max-width: 1366px) {
.setting-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.plan-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 992px) {
.base-grid {
grid-template-columns: 1fr;
.span-1,
.span-2 {
grid-column: span 1;
}
}
.section-head {
flex-direction: column;
align-items: stretch;
}
.section-actions {
flex-wrap: wrap;
}
.row-card {
flex-direction: column;
}
.setting-grid,
.plan-grid {
grid-template-columns: 1fr;
}
.row-action {
width: 100%;
justify-content: flex-end;
}
}
</style>