This commit is contained in:
2026-02-16 13:41:41 +08:00
parent 41a3ab45b3
commit c7c1b416ee
17 changed files with 2821 additions and 375 deletions

View File

@ -1,7 +1,7 @@
<template>
<el-dialog
v-loading="loading"
width="92%"
width="42%"
:visible.sync="dialogTableVisible"
class="ems-dialog plan-dialog"
title="保护方案"
@ -18,7 +18,7 @@
class="plan-form"
>
<div class="base-panel">
<div class="panel-title">基础信息</div>
<div class="card-legend">基础信息</div>
<div class="base-grid">
<el-form-item label="设备保护名称" prop="faultName" class="span-1">
<el-input v-model="formData.faultName" placeholder="请输入" clearable></el-input>
@ -26,7 +26,7 @@
<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-form-item label="告警等级" prop="faultLevel" class="span-2 level-row">
<el-radio-group v-model="formData.faultLevel" :disabled="mode === 'edit'">
<el-radio :label="1">等级1</el-radio>
<el-radio :label="2">等级2</el-radio>
@ -46,34 +46,43 @@
</div>
<div class="plan-section">
<div class="card-legend">故障保护</div>
<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 @click.native.prevent="addRow('faultProtectionSettings')" type="primary" size="mini">
新增故障保护
</el-button>
</div>
</div>
<div class="row-card" v-for="(item, index) in protectionSettings" :key="'protectionSettings' + index">
<div class="row-card" v-for="(item, index) in faultProtectionSettings" :key="'faultProtectionSettings' + index">
<div class="row-index">{{ index + 1 }}</div>
<div class="row-grid setting-grid">
<div class="row-grid trigger-grid">
<div class="field-block field-point">
<div class="field-label">点位</div>
<el-autocomplete
<el-select
v-model="item.point"
placeholder="请输入点位ID/描述"
filterable
remote
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>
reserve-keyword
placeholder="支持关键字搜索,展开可查看点位列表"
:no-data-text="'暂无匹配点位'"
:loading="item.pointLoading"
@change="(value) => handlePointChange(index, 'faultProtectionSettings', value)"
@visible-change="(visible) => handlePointDropdownVisible(index, 'faultProtectionSettings', visible)"
:remote-method="(query) => remotePointSearch(index, 'faultProtectionSettings', query)"
>
<el-option
v-for="pointItem in item.pointOptions"
:key="'fault-point-' + index + '-' + pointItem.value"
:label="pointItem.label"
:value="pointItem.value"
/>
</el-select>
</div>
<div class="field-block">
@ -93,6 +102,68 @@
<el-input placeholder="请输入故障值" v-model="item.faultValue"></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, 'faultProtectionSettings')" type="warning" size="mini">
删除
</el-button>
</div>
</div>
<el-empty v-if="faultProtectionSettings.length === 0" description="暂无故障保护请先添加"></el-empty>
</div>
<div class="plan-section">
<div class="card-legend">释放保护</div>
<div class="section-head">
<div>
<div class="section-desc">配置故障释放的判定条件和关系</div>
</div>
<div class="section-actions">
<el-button @click.native.prevent="addRow('releaseProtectionSettings')" type="primary" size="mini">
新增释放保护
</el-button>
</div>
</div>
<div class="row-card" v-for="(item, index) in releaseProtectionSettings" :key="'releaseProtectionSettings' + index">
<div class="row-index">{{ index + 1 }}</div>
<div class="row-grid trigger-grid">
<div class="field-block field-point">
<div class="field-label">点位</div>
<el-select
v-model="item.point"
filterable
remote
clearable
reserve-keyword
placeholder="支持关键字搜索展开可查看点位列表"
:no-data-text="'暂无匹配点位'"
:loading="item.pointLoading"
@change="(value) => handlePointChange(index, 'releaseProtectionSettings', value)"
@visible-change="(visible) => handlePointDropdownVisible(index, 'releaseProtectionSettings', visible)"
:remote-method="(query) => remotePointSearch(index, 'releaseProtectionSettings', query)"
>
<el-option
v-for="pointItem in item.pointOptions"
:key="'release-point-' + index + '-' + pointItem.value"
:label="pointItem.label"
:value="pointItem.value"
/>
</el-select>
</div>
<div class="field-block">
<div class="field-label">释放比较符</div>
<el-select v-model="item.releaseOperator" placeholder="请选择">
@ -115,7 +186,7 @@
<el-select v-model="item.relationNext" placeholder="请选择">
<el-option
v-for="(value, key) in relationWithPoint"
:key="key + 'relation'"
:key="key + 'releaseRelation'"
:label="key"
:value="value"
></el-option>
@ -123,24 +194,24 @@
</div>
</div>
<div class="row-action">
<el-button @click.native.prevent="deleteRow(index, 'protectionSettings')" type="warning" size="mini">
<el-button @click.native.prevent="deleteRow(index, 'releaseProtectionSettings')" type="warning" size="mini">
删除
</el-button>
</div>
</div>
<el-empty v-if="protectionSettings.length === 0" description="暂无保护前提请先添加"></el-empty>
<el-empty v-if="releaseProtectionSettings.length === 0" description="暂无释放保护请先添加"></el-empty>
</div>
<div class="plan-section">
<div class="card-legend">执行保护</div>
<div class="section-head">
<div>
<div class="section-title">保护方案</div>
<div class="section-desc">设置触发后的目标写入点位和值</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>
@ -148,29 +219,26 @@
<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 class="field-label">执行动作</div>
<el-select
v-model="item.action"
clearable
placeholder="请选择执行动作"
@change="(value) => handleProtectionActionChange(index, value)"
>
<el-option
v-for="opt in protectionCapabilityOptions"
:key="'plan-action-' + index + '-' + opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</div>
<div class="field-block" v-if="isDerateAction(item.action)">
<div class="field-label">降功率比例(%)</div>
<el-input placeholder="请输入0-100" v-model="item.value"></el-input>
</div>
</div>
<div class="row-action">
@ -180,7 +248,7 @@
</div>
</div>
<el-empty v-if="protectionPlan.length === 0" description="暂无保护方案请先添加"></el-empty>
<el-empty v-if="protectionPlan.length === 0" description="暂无执行保护请先添加"></el-empty>
</div>
</el-form>
@ -208,8 +276,19 @@ export default {
return {
mode: "",
loading: 0,
protectionSettings: [],
faultProtectionSettings: [],
releaseProtectionSettings: [],
protectionPlan: [],
protectionCapabilityOptions: [
{ label: "降功率", value: "derate", requireValue: true },
{ label: "关机/停机/切断", value: "shutdown", requireValue: false },
{ label: "禁止充电", value: "forbid_charge", requireValue: false },
{ label: "允许放电", value: "allow_discharge", requireValue: false },
{ label: "禁止放电", value: "forbid_discharge", requireValue: false },
{ label: "允许充电", value: "allow_charge", requireValue: false },
{ label: "禁止充放电", value: "forbid_charge_discharge", requireValue: false },
{ label: "待机", value: "standby", requireValue: false },
],
dialogTableVisible: false,
formData: {
id: "",
@ -257,140 +336,313 @@ export default {
releaseDelaySeconds: data?.releaseDelaySeconds || "",
description: data?.description || "",
};
const plan = JSON.parse(data?.protectionPlan || "[]");
const settings = JSON.parse(data?.protectionSettings || "[]");
const planRaw = JSON.parse(data?.protectionPlan || "[]");
const plan = Array.isArray(planRaw) ? planRaw : planRaw ? [planRaw] : [];
const settingsRaw = JSON.parse(data?.protectionSettings || "[]");
const settings =
Array.isArray(settingsRaw)
? {
faultSettings: settingsRaw.map((item) => Object.assign({}, item)),
releaseSettings: settingsRaw.map((item) => Object.assign({}, item)),
}
: {
faultSettings: Array.isArray(settingsRaw?.faultSettings) ? settingsRaw.faultSettings : [],
releaseSettings: Array.isArray(settingsRaw?.releaseSettings) ? settingsRaw.releaseSettings : [],
};
this.$nextTick(() => {
this.protectionPlan.splice(0, 0, ...plan);
this.protectionSettings.splice(0, 0, ...settings);
this.protectionPlan.splice(0, 0, ...plan.map((item) => this.normalizeProtectionPlanItem(item)));
this.faultProtectionSettings.splice(0, 0, ...settings.faultSettings.map((item) => this.enhancePointRow(item)));
this.releaseProtectionSettings.splice(
0,
0,
...settings.releaseSettings.map((item) => this.enhancePointRow(item))
);
});
});
} else {
this.mode = "add";
}
},
formatPointLabel(item = {}) {
const pointId = item?.pointId || "";
const pointName = item?.pointName || "";
const pointDesc = item?.pointDesc || "";
return `${pointId || "-"}-${pointName || "-"}(${pointDesc || "-"})`;
},
normalizePointOptions(data = []) {
return (data || []).map((item) => {
const pointId = item?.pointId || item?.point || item?.value || "";
const pointName = item?.pointName || pointId || "";
const pointDesc = item?.pointDesc || "";
return {
value: pointId,
label: this.formatPointLabel({ pointId, pointName, pointDesc }),
pointId,
pointName,
pointDesc,
deviceId: item?.deviceId || "",
deviceName: item?.deviceName || "",
deviceCategory: item?.deviceCategory || "",
categoryName: item?.categoryName || item?.deviceCategory || "",
};
});
},
enhancePointRow(row = {}) {
const nextRow = Object.assign(
{
deviceId: "",
deviceName: "",
deviceCategory: "",
categoryName: "",
point: "",
pointName: "",
pointOptions: [],
pointOptionsCache: {},
pointRequestId: 0,
pointLoading: false,
},
row || {}
);
const selectedPointId = String(nextRow.point || "").trim();
if (selectedPointId) {
const hasSelected = (nextRow.pointOptions || []).some((item) => item?.value === selectedPointId);
if (!hasSelected) {
const selectedOption = this.normalizePointOptions([
{
pointId: selectedPointId,
pointName: nextRow.pointName || selectedPointId,
pointDesc: "",
deviceId: nextRow.deviceId || "",
deviceName: nextRow.deviceName || "",
deviceCategory: nextRow.deviceCategory || "",
categoryName: nextRow.categoryName || "",
},
])[0];
nextRow.pointOptions = [...(nextRow.pointOptions || []), selectedOption];
}
}
return nextRow;
},
getRow(type, index) {
return (this[type] || [])[index];
},
setRow(type, index, row) {
this[type].splice(index, 1, row);
},
addRow(type) {
const item =
type === "protectionSettings"
? {
deviceId: "",
deviceName: "",
deviceCategory: "",
categoryName: "",
point: "",
pointName: "",
faultValue: "",
releaseValue: "",
faultOperator: "",
releaseOperator: "",
relationNext: "",
}
: {
deviceId: "",
deviceName: "",
deviceCategory: "",
categoryName: "",
point: "",
pointName: "",
value: "",
};
type === "protectionPlan"
? this.normalizeProtectionPlanItem({})
: this.enhancePointRow(this.createProtectionSettingItem(type));
this[type].splice(this[type].length, 0, item);
},
getCapabilityLabel(action) {
const target = (this.protectionCapabilityOptions || []).find((item) => item.value === action);
return target?.label || "";
},
isDerateAction(action) {
return action === "derate";
},
inferProtectionAction(raw = {}) {
const marker = `${raw?.actionName || ""} ${raw?.action || ""} ${raw?.pointName || ""} ${raw?.point || ""}`.toLowerCase();
const mapping = [
{ value: "forbid_charge_discharge", markers: ["禁止充放电", "forbid_charge_discharge", "disable_charge_discharge"] },
{ value: "forbid_charge", markers: ["禁止充电", "forbid_charge", "disable_charge"] },
{ value: "allow_discharge", markers: ["允许放电", "allow_discharge"] },
{ value: "forbid_discharge", markers: ["禁止放电", "forbid_discharge", "disable_discharge"] },
{ value: "allow_charge", markers: ["允许充电", "allow_charge"] },
{ value: "shutdown", markers: ["关机", "停机", "切断", "shutdown", "stop"] },
{ value: "standby", markers: ["待机", "standby"] },
{ value: "derate", markers: ["降功率", "derate", "power_limit", "powerlimit"] },
];
for (let i = 0; i < mapping.length; i++) {
const item = mapping[i];
if (item.markers.some((mk) => marker.includes(mk))) {
return item.value;
}
}
return "";
},
normalizeDerateValue(rawValue) {
if (rawValue === null || rawValue === undefined || rawValue === "") return "";
const num = Number(rawValue);
if (Number.isNaN(num)) return rawValue;
if (num > 0 && num <= 1) {
return String((num * 100).toFixed(2)).replace(/\.?0+$/, "");
}
return String(rawValue);
},
normalizeProtectionPlanItem(raw = {}) {
const action = raw?.action || this.inferProtectionAction(raw);
const label = raw?.actionName || this.getCapabilityLabel(action);
const value = this.isDerateAction(action) ? this.normalizeDerateValue(raw?.value) : "";
return {
action,
actionName: label,
point: raw?.point || action || "",
pointName: raw?.pointName || label || "",
value,
};
},
handleProtectionActionChange(index, action) {
const row = this.getRow("protectionPlan", index);
if (!row) return;
const label = this.getCapabilityLabel(action);
this.setRow(
"protectionPlan",
index,
Object.assign({}, row, {
action: action || "",
actionName: label || "",
point: action || "",
pointName: label || "",
value: this.isDerateAction(action) ? row.value : "",
})
);
},
createProtectionSettingItem(type) {
const isFault = type === "faultProtectionSettings";
return this.enhancePointRow({
deviceId: "",
deviceName: "",
deviceCategory: "",
categoryName: "",
point: "",
pointName: "",
faultValue: isFault ? "" : null,
releaseValue: isFault ? null : "",
faultOperator: isFault ? "" : null,
releaseOperator: isFault ? null : "",
relationNext: "",
});
},
deleteRow(index, type) {
this[type].splice(index, 1);
},
querySearchAsync(query, cb, index, type) {
if (!this.formData.siteId) {
this.$message({
type: "warning",
message: "请先选择站点",
});
return cb([]);
setPointOptions(index, type, data = []) {
const row = this.getRow(type, index);
if (!row) return;
const normalized = this.normalizePointOptions(data);
const selectedPoint = row.point
? this.normalizePointOptions([
{
pointId: row.point,
pointName: row.pointName || row.point,
pointDesc: "",
deviceId: row.deviceId || "",
deviceName: row.deviceName || "",
deviceCategory: row.deviceCategory || "",
categoryName: row.categoryName || "",
},
])
: [];
const merged = [...normalized, ...(row.pointOptions || []), ...selectedPoint];
const optionMap = {};
const uniqueOptions = merged.filter((item) => {
if (!item?.value || optionMap[item.value]) return false;
optionMap[item.value] = true;
return true;
});
this.setRow(type, index, Object.assign({}, row, { pointOptions: uniqueOptions }));
},
fetchPointOptions(index, type, query = "", { force = false } = {}) {
const row = this.getRow(type, index);
if (!row || !this.formData.siteId) return Promise.resolve([]);
const normalizedQuery = String(query || "").trim();
const cacheKey = `${this.formData.siteId}_${normalizedQuery}`;
if (!force && row.pointOptionsCache?.[cacheKey]) {
this.setPointOptions(index, type, row.pointOptionsCache[cacheKey]);
return Promise.resolve(row.pointOptionsCache[cacheKey]);
}
getPointMatchList({
const requestId = Number(row.pointRequestId || 0) + 1;
this.setRow(type, index, Object.assign({}, row, { pointRequestId: requestId, pointLoading: true }));
return getPointMatchList({
siteId: this.formData.siteId,
pageNum: 1,
pageSize: 100,
pointId: query || "",
pointDesc: query || "",
pointId: normalizedQuery,
pointDesc: normalizedQuery,
})
.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,
};
})
);
const latestRow = this.getRow(type, index);
if (!latestRow || latestRow.pointRequestId !== requestId) return [];
const result = response?.rows || [];
const cache = Object.assign({}, latestRow.pointOptionsCache || {}, { [cacheKey]: result });
this.setRow(type, index, Object.assign({}, latestRow, { pointOptionsCache: cache }));
this.setPointOptions(index, type, result);
return result;
})
.catch(() => cb([]));
.finally(() => {
const latestRow = this.getRow(type, index);
if (!latestRow || latestRow.pointRequestId !== requestId) return;
this.setRow(type, index, Object.assign({}, latestRow, { pointLoading: false }));
});
},
fillPointMetaByPoint(point, index, type) {
if (!this.formData.siteId || !point) {
remotePointSearch(index, type, query) {
this.fetchPointOptions(index, type, query);
},
handlePointDropdownVisible(index, type, visible) {
if (!visible) return;
this.fetchPointOptions(index, type, "");
},
handlePointChange(index, type, value) {
const row = this.getRow(type, index);
if (!row) return;
if (!value) {
this.setRow(
type,
index,
Object.assign({}, row, {
point: "",
pointName: "",
deviceId: "",
deviceName: "",
deviceCategory: "",
categoryName: "",
})
);
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);
const selectedOption = (row.pointOptions || []).find((item) => item?.value === value) || {};
this.setRow(
type,
index,
Object.assign({}, row, {
point: selectedOption.pointId || selectedOption.value || value,
pointName: selectedOption.pointName || selectedOption.pointId || selectedOption.value || value,
deviceId: selectedOption.deviceId || "",
deviceName: selectedOption.deviceName || "",
deviceCategory: selectedOption.deviceCategory || "",
categoryName: selectedOption.categoryName || selectedOption.deviceCategory || "",
})
);
},
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}行与下一个点位的关系`,
faultProtectionSettings: {
deviceId: `请选择故障保护第${index}行的设备`,
deviceCategory: `请选择故障保护第${index}行的设备类型`,
categoryName: `请选择故障保护第${index}行的设备类型`,
point: `请选择故障保护第${index}行的点位`,
pointName: `请选择故障保护第${index}行的点位`,
faultValue: `请输入故障保护第${index}行的故障值`,
faultOperator: `请选择故障保护第${index}行的故障值比较关系`,
relationNext: `请选择故障保护第${index}行与下一个点位的关系`,
},
releaseProtectionSettings: {
deviceId: `请选择释放保护第${index}行的设备`,
deviceCategory: `请选择释放保护第${index}行的设备类型`,
categoryName: `请选择释放保护第${index}行的设备类型`,
point: `请选择释放保护第${index}行的点位`,
pointName: `请选择释放保护第${index}行的点位`,
releaseValue: `请输入释放保护第${index}行的释放值`,
releaseOperator: `请选择释放保护第${index}行的释放值比较关系`,
relationNext: `请选择释放保护第${index}行与下一个点位的关系`,
},
protectionPlan: {
deviceId: `请选择保护方案第${index}行的设备`,
deviceCategory: `请选择保护方案第${index}行的设备类型`,
categoryName: `请选择保护方案第${index}行的设备类型`,
point: `请选择保护方案第${index}行的点位`,
pointName: `请选择保护方案第${index}行的点位`,
value: `请输入保护方案第${index}行的故障值`,
action: `请选择执行保护第${index}行的动作`,
value: `请输入执行保护第${index}行的降功率比例(0-100)`,
},
}[type][name];
}
@ -407,32 +659,58 @@ export default {
releaseDelaySeconds = "",
description = "",
} = this.formData;
const { protectionSettings, protectionPlan } = this;
let protectionSettingsValidateStatus = true;
const { faultProtectionSettings, releaseProtectionSettings, protectionPlan } = this;
let faultSettingsValidateStatus = true;
let releaseSettingsValidateStatus = true;
let protectionPlanValidateStatus = true;
const settingRequiredFields = ["point", "faultOperator", "faultValue", "releaseOperator", "releaseValue"];
const planRequiredFields = ["point", "value"];
const faultSettingRequiredFields = ["point", "faultOperator", "faultValue"];
const releaseSettingRequiredFields = ["point", "releaseOperator", "releaseValue"];
const planRequiredFields = ["action"];
for (let i = 0; i < protectionSettings.length; i++) {
const row = protectionSettings[i] || {};
for (let inner = 0; inner < settingRequiredFields.length; inner++) {
const key = settingRequiredFields[inner];
for (let i = 0; i < faultProtectionSettings.length; i++) {
const row = faultProtectionSettings[i] || {};
for (let inner = 0; inner < faultSettingRequiredFields.length; inner++) {
const key = faultSettingRequiredFields[inner];
const value = row[key];
if (![0, "0"].includes(value) && !value) {
this.$message.error(getToastMsg(key, "protectionSettings", i + 1));
protectionSettingsValidateStatus = false;
this.$message.error(getToastMsg(key, "faultProtectionSettings", i + 1));
faultSettingsValidateStatus = false;
break;
}
}
if (!protectionSettingsValidateStatus) break;
if (!faultSettingsValidateStatus) break;
if (!row.deviceId) {
this.$message.error(`请选择保护前提第${i + 1}行的点位(需从点位列表中选择)`);
protectionSettingsValidateStatus = false;
this.$message.error(`请选择故障保护第${i + 1}行的点位(需从点位列表中选择)`);
faultSettingsValidateStatus = false;
break;
}
if (protectionSettings[i + 1] && !row.relationNext) {
this.$message.error(getToastMsg("relationNext", "protectionSettings", i + 1));
protectionSettingsValidateStatus = false;
if (faultProtectionSettings[i + 1] && !row.relationNext) {
this.$message.error(getToastMsg("relationNext", "faultProtectionSettings", i + 1));
faultSettingsValidateStatus = false;
break;
}
}
for (let i = 0; i < releaseProtectionSettings.length; i++) {
const row = releaseProtectionSettings[i] || {};
for (let inner = 0; inner < releaseSettingRequiredFields.length; inner++) {
const key = releaseSettingRequiredFields[inner];
const value = row[key];
if (![0, "0"].includes(value) && !value) {
this.$message.error(getToastMsg(key, "releaseProtectionSettings", i + 1));
releaseSettingsValidateStatus = false;
break;
}
}
if (!releaseSettingsValidateStatus) break;
if (!row.deviceId) {
this.$message.error(`请选择释放保护第${i + 1}行的点位(需从点位列表中选择)`);
releaseSettingsValidateStatus = false;
break;
}
if (releaseProtectionSettings[i + 1] && !row.relationNext) {
this.$message.error(getToastMsg("relationNext", "releaseProtectionSettings", i + 1));
releaseSettingsValidateStatus = false;
break;
}
}
@ -449,17 +727,35 @@ export default {
}
}
if (!protectionPlanValidateStatus) break;
if (!row.deviceId) {
this.$message.error(`请选择保护方案第${i + 1}行的点位(需从点位列表中选择)`);
if (this.isDerateAction(row.action)) {
const derate = Number(row.value);
if (Number.isNaN(derate) || derate < 0 || derate > 100) {
this.$message.error(getToastMsg("value", "protectionPlan", i + 1));
protectionPlanValidateStatus = false;
break;
}
}
if (!row.pointName) {
this.$message.error(getToastMsg("action", "protectionPlan", i + 1));
protectionPlanValidateStatus = false;
break;
}
}
if (!protectionSettingsValidateStatus || !protectionPlanValidateStatus) return;
if (!faultSettingsValidateStatus || !releaseSettingsValidateStatus || !protectionPlanValidateStatus) return;
const settings = protectionSettings.map((item) => Object.assign({}, item));
const plan = protectionPlan.map((item) => Object.assign({}, item));
const settings = {
faultSettings: faultProtectionSettings.map((item) => Object.assign({}, item)),
releaseSettings: releaseProtectionSettings.map((item) => Object.assign({}, item)),
};
const plan = protectionPlan.map((item) =>
Object.assign({}, item, {
actionName: this.getCapabilityLabel(item.action),
point: item.action || "",
pointName: this.getCapabilityLabel(item.action) || "",
value: this.isDerateAction(item.action) ? item.value : null,
})
);
this.loading += 1;
const params = {
@ -506,7 +802,8 @@ export default {
this.formData[key] = key === "isAlert" ? 0 : key === "faultLevel" ? 1 : "";
}
this.$refs.addTempForm.resetFields();
this.$set(this, "protectionSettings", []);
this.$set(this, "faultProtectionSettings", []);
this.$set(this, "releaseProtectionSettings", []);
this.$set(this, "protectionPlan", []);
this.dialogTableVisible = false;
},
@ -516,25 +813,28 @@ export default {
<style scoped lang="scss">
.plan-form {
background: #f5f8ff;
border: 1px solid #e8edf7;
border-radius: 14px;
padding: 16px;
padding: 16px 0;
}
.base-panel {
position: relative;
background: #fff;
border: 1px solid #e7ecf6;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
margin: 14px 0 16px;
}
.panel-title {
.card-legend {
position: absolute;
top: 0;
left: 14px;
transform: translateY(-50%);
padding: 0 10px;
background: #fff;
font-size: 15px;
font-weight: 600;
color: #1f2d3d;
margin-bottom: 14px;
}
.base-grid {
@ -551,12 +851,34 @@ export default {
}
}
.level-row {
::v-deep .el-form-item__content {
min-height: 36px;
display: flex;
align-items: center;
}
::v-deep .el-radio-group {
display: flex;
flex-wrap: nowrap;
align-items: center;
height: 36px;
}
::v-deep .el-radio {
display: inline-flex;
align-items: center;
margin-right: 16px;
}
}
.plan-section {
position: relative;
background: #fff;
border: 1px solid #e7ecf6;
border-radius: 12px;
padding: 16px;
margin-bottom: 14px;
margin: 14px 0;
}
.section-head {
@ -567,14 +889,6 @@ export default {
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;
@ -622,8 +936,8 @@ export default {
gap: 10px;
}
.setting-grid {
grid-template-columns: 2fr repeat(5, minmax(120px, 1fr));
.trigger-grid {
grid-template-columns: 2fr repeat(3, minmax(120px, 1fr));
}
.plan-grid {
@ -672,7 +986,7 @@ export default {
}
@media (max-width: 1366px) {
.setting-grid {
.trigger-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
@ -704,7 +1018,7 @@ export default {
flex-direction: column;
}
.setting-grid,
.trigger-grid,
.plan-grid {
grid-template-columns: 1fr;
}

View File

@ -31,13 +31,13 @@
<template slot-scope="scope">{{ scope.row.isAlert === 1 ? '是' : '否' }}</template>
</el-table-column>
<el-table-column prop="description" label="处理方案描述" min-width="180" show-overflow-tooltip />
<el-table-column prop="protectionSettings" label="保护前提" min-width="360" show-overflow-tooltip>
<el-table-column prop="protectionSettings" label="故障/释放保护" min-width="360" show-overflow-tooltip>
<template slot-scope="scope">
<div class="rich-lines" v-html="handleProtectionSettings(scope.row.protectionSettings)"></div>
</template>
</el-table-column>
<el-table-column prop="faultDelaySeconds" label="前提延时(s)" width="110" />
<el-table-column prop="protectionPlan" label="保护方案" min-width="260" show-overflow-tooltip>
<el-table-column prop="protectionPlan" label="执行保护" min-width="260" show-overflow-tooltip>
<template slot-scope="scope">
<div class="rich-lines" v-html="handleProtectionPlan(scope.row.protectionPlan)"></div>
</template>
@ -96,37 +96,76 @@ export default {
this.getData();
},
handleProtectionSettings(data) {
if (!data || !JSON.parse(data)) return;
const arr = JSON.parse(data);
const str = arr.map((item, index) => {
if (!data) return;
let parsed = null;
try {
parsed = JSON.parse(data);
} catch (e) {
return "";
}
const faultSettings = Array.isArray(parsed) ? parsed : parsed?.faultSettings || [];
const releaseSettings = Array.isArray(parsed) ? parsed : parsed?.releaseSettings || [];
const buildLine = (item, index, total, key, value, relationKey) => {
const {
categoryName = "",
deviceId = "",
point = "",
faultOperator = "",
faultValue = "",
releaseOperator = "",
releaseValue = "",
[key]: operator = "",
[value]: val = "",
relationNext = "",
} = item;
return `<div>${index + 1}、 <span>${categoryName ? categoryName + "-" : ""}${
deviceId ? deviceId + "-" : ""
}${point || ""}</span> <span>故障:${faultOperator || ""}${faultValue || ""}</span> <span>释放:${
releaseOperator || ""
}${releaseValue || ""}</span> ${
arr[index + 1] ? "<span>关系:" + (relationNext || "") + "</span>" : ""
}${point || ""}</span> <span>${relationKey}:${operator || ""}${val || ""}</span> ${
total[index + 1] ? "<span>关系:" + (relationNext || "") + "</span>" : ""
}</div>`;
});
return str.join("");
};
const faultStr = faultSettings.map((item, index) =>
buildLine(item, index, faultSettings, "faultOperator", "faultValue", "故障")
);
const releaseStr = releaseSettings.map((item, index) =>
buildLine(item, index, releaseSettings, "releaseOperator", "releaseValue", "释放")
);
const groups = [];
if (faultStr.length) {
groups.push(`<div><strong>故障保护</strong></div>${faultStr.join("")}`);
}
if (releaseStr.length) {
groups.push(`<div><strong>释放保护</strong></div>${releaseStr.join("")}`);
}
return groups.join("");
},
handleProtectionPlan(data) {
if (!data || !JSON.parse(data)) return;
const arr = JSON.parse(data);
if (!data) return;
let arr = [];
try {
const parsed = JSON.parse(data);
arr = Array.isArray(parsed) ? parsed : parsed ? [parsed] : [];
} catch (e) {
return "";
}
const actionLabelMap = {
derate: "降功率",
shutdown: "关机/停机/切断",
forbid_charge: "禁止充电",
allow_discharge: "允许放电",
forbid_discharge: "禁止放电",
allow_charge: "允许充电",
forbid_charge_discharge: "禁止充放电",
standby: "待机",
};
const str = arr.map((item, index) => {
const { categoryName = "", deviceId = "", point = "", value = "" } = item;
return `<div>${index + 1}、 <span>${categoryName ? categoryName + "-" : ""}${
deviceId ? deviceId + "-" : ""
}${point || ""}</span> <span>故障:=${value || ""}</span> </div>`;
const action = item?.action || "";
const point = item?.point || "";
const pointName = item?.pointName || "";
const actionName = item?.actionName || actionLabelMap[action] || pointName || point || "未配置";
const value = item?.value;
if ((action === "derate" || actionName.includes("降功率")) && value !== null && value !== undefined && value !== "") {
return `<div>${index + 1}、 <span>动作:${actionName}</span> <span>比例:${value}%</span></div>`;
}
return `<div>${index + 1}、 <span>动作:${actionName}</span></div>`;
});
return str.join("");
},