718 lines
22 KiB
Vue
718 lines
22 KiB
Vue
<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>
|