hazop插件提交

This commit is contained in:
2026-09-08 10:25:17 +08:00
parent f1a515b70c
commit 6f41d613ba
2 changed files with 140 additions and 50 deletions
+25
View File
@@ -96,3 +96,28 @@ export function hazopRowAddApi(dev_id: number, data: {
data,
});
}
// 编辑偏离分析行
export function hazopRowEditApi(row_id: number, data: {
cause: string,
consequence: string,
l_value: number,
s_value: number,
risk_value:string,
safeguards:string,
suggestions:string
}) {
return request({
url: `/api/hazop/rows/${row_id}`,
method: 'put',
data,
});
}
// 删除偏离分析行
export function hazopRowDelApi(row_id: number) {
return request({
url: `/api/hazop/rows/${row_id}`,
method: 'delete',
});
}
+115 -50
View File
@@ -4,7 +4,6 @@ import { useMessage } from "naive-ui";
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { AiError, aiHazopDeviation } from '@/service/api/ai';
import Ed from "./Ed.vue";
import { matrixConfigApi } from "@/service/api/system";
import {
hazopNodesApi,
@@ -17,7 +16,9 @@ import {
hazopNodeDelApi,
hazopObjDelApi,
hazopDevDelApi,
hazopRowAddApi
hazopRowAddApi,
hazopRowEditApi,
hazopRowDelApi,
} from "@/service/api/plugin";
const themeStore = useThemeStore();
@@ -99,6 +100,38 @@ const nodes = ref<any>([]);
// 表格样式
const TH = "border bg-slate-100 px-2 py-1.5 text-[11px] font-medium text-slate-500";
const TD = "border px-1.5 py-1 align-top";
// 偏离总数
const devTotal = computed(() => {
let total = 0;
nodes.value.forEach((node:any) => {
node.objects.forEach((obj:any) => {
total += obj.deviations.length;
});
});
return total;
});
// 已分析偏离数
const devDone = computed(() => {
let nonEmptyCount = 0;
nodes.value.forEach((node:any) => {
node.objects.forEach((obj:any) => {
obj.deviations.forEach((dev:any) => {
if (dev.rows && dev.rows.length > 0) {
nonEmptyCount++;
}
});
});
});
return nonEmptyCount;
});
// 已完成记录总数
const recTotal = computed(() => {
const totalRows = nodes.value.flatMap((node:any) => node.objects)
.flatMap((obj:any) => obj.deviations)
.filter((d:any) => d.rows && d.rows.length > 0)
.reduce((sum:number, d:any) => sum + d.rows.length, 0);
return totalRows;
});
const HAZOP_AI:any = {
@@ -124,9 +157,6 @@ const lowerOf = (lv: "高" | "中" | "低"): "高" | "中" | "低" => {
const sel = ref({ n: 0, d: 0 });
const analyzing = ref(false);
const devTotal = computed(() => 0);
const devDone = computed(() => 0);
const recTotal = computed(() => 0);
const toRec = (obj: string, dev: string, row: any): any => ({
item: `${dev}${obj}`,
@@ -138,12 +168,6 @@ const toRec = (obj: string, dev: string, row: any): any => ({
source: "HAZOP",
});
const updRow = (ri: number, patch: Partial<any>) => {
const row = currentDev.value.rows[ri];
if (row) Object.assign(row, patch);
};
const clampLS = (v: string) => Math.max(1, Math.min(5, +v || 1));
const runAi = async () => {
if (currentDev.value.rows.length) return message.info("当前偏离已有分析记录,可手动新增行补充");
analyzing.value = true;
@@ -202,6 +226,21 @@ const selectWorker = (n: string) => {
sels.value = sels.value.includes(n) ? sels.value.filter((x) => x !== n) : [...sels.value, n];
};
// 保存
const save = () => {
window.$message?.success("已手动保存,分析记录数已同步至工具卡片(内容修改实时自动保存,退出不丢失)");
};
// AI整理如表
const exportAll = () => {
emit("export", nodes.value.flatMap((n) => n.devs.flatMap((d) => d.rows.map((row) => toRec(n.obj, d.name, row)))), "ai");
}
// 导出 Word
const exportWord = () => {
window.$message?.success('HAZOP 分析报告已导出(Word')
}
// 根据l和s获取风险等级
const riskLevel = (l: number, s: number) => {
const num = l * s;
@@ -216,8 +255,9 @@ const riskLevel = (l: number, s: number) => {
// 新增行
const addRow = async () => {
console.log(l_options.value)
console.log(s_options.value)
if(!currentDev.value){
return window.$message?.warning("请先在左侧选择偏离")
}
loading.value = true;
const {data,error} = await hazopRowAddApi(currentDev.value?.id,{
cause: "",
@@ -229,35 +269,39 @@ const addRow = async () => {
suggestions: "",
})
if(!error){
console.log(data)
// getHazopNodes();
currentDev.value.rows.push(data);
window.$message?.success("新增行成功")
}
loading.value = false;
};
// 编辑行
const editRow = async (row: any) => {
const {error} = await hazopRowEditApi(row.id,{
cause: row.cause,
consequence: row.consequence,
l_value: row.l_value,
s_value: row.s_value,
risk_value: riskLevel(row.l_value, row.s_value)?.name || '',
safeguards: row.safeguards,
suggestions: row.suggestions,
})
}
// 删除行
const delRow = (index: number) => currentDev.value.rows.splice(index, 1);
const delRow = async (index: number, row_id: number) => {
loading.value = true;
const {error} = await hazopRowDelApi(row_id);
if(!error){
currentDev.value.rows.splice(index, 1);
window.$message?.success("删除成功")
}
loading.value = false;
}
// 展开收起
const leftToggle = (id: string) => {
collapsed.value = { ...collapsed.value, [id]: !collapsed.value[id] }
}
// 保存
const save = () => {
window.$message?.success("已手动保存,分析记录数已同步至工具卡片(内容修改实时自动保存,退出不丢失)");
};
// AI整理如表
const exportAll = () => {
emit("export", nodes.value.flatMap((n) => n.devs.flatMap((d) => d.rows.map((row) => toRec(n.obj, d.name, row)))), "ai");
}
// 导出 Word
const exportWord = () => {
window.$message?.success('HAZOP 分析报告已导出(Word')
}
// 打开弹窗
const openModal = (type: string,obj: string, title: string,editName: string = '') => {
if(type==='add'){
@@ -511,6 +555,7 @@ const getHazopNodes = async () => {
treeLoading.value = true;
const {data,error} = await hazopNodesApi(props.currentId);
if(!error){
console.log(data)
nodes.value = data;
}
treeLoading.value = false;
@@ -660,17 +705,17 @@ onMounted(() => {
<thead>
<tr>
<th :class="TH" class="w-16">操作</th>
<th :class="TH">原因</th>
<th :class="TH">后果</th>
<th :class="TH" class="w-8">L</th>
<th :class="TH" class="w-8">S</th>
<th :class="TH" class="w-60">原因</th>
<th :class="TH" class="w-60">后果</th>
<th :class="TH" class="w-6">L</th>
<th :class="TH" class="w-6">S</th>
<th :class="TH" class="w-20">RR</th>
<th :class="TH">安全措施</th>
<th :class="TH">建议措施</th>
<th :class="TH" class="w-60">安全措施</th>
<th :class="TH" class="w-60">建议措施</th>
</tr>
</thead>
<tbody>
<tr v-if="currentDev?.rows?.length === 0">
<tr v-if="!currentDev || currentDev?.rows?.length === 0">
<td colspan="8" class="px-3 py-8 text-center text-xs text-slate-400">
当前偏离暂无分析记录点击右上角AI 分析当前偏离自动生成新增行手动填写
</td>
@@ -681,33 +726,53 @@ onMounted(() => {
<NButton text type="primary" class="mt-1px text-[11px] hover:underline" title="选入风险分析记录表"
@click="message.success('已选入 1 条风险记录,可继续挑选或返回查看记录表')">入表
</NButton>
<NButton text type="error" title="删除行" @click="delRow(Number(ri))">
<Icon icon="ep:delete" class="size-14px" />
</NButton>
<NPopconfirm
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="delRow(Number(ri), row.id)"
>
<template #trigger>
<NButton text type="error" title="删除行">
<Icon icon="ep:delete" class="size-14px" />
</NButton>
</template>
确定删除吗
</NPopconfirm>
</div>
</td>
<td :class="TD">
<Ed :v="row.cause" @update="(x) => updRow(Number(ri), { cause: x })" />
<NInput type="textarea" size="small" v-model:value="row.cause" :autosize="{minRows:1}" class="text-xs" @blur="editRow(row)" />
</td>
<td :class="TD">
<Ed :v="row.consequence" @update="(x) => updRow(Number(ri), { consequence: x })" />
<NInput type="textarea" size="small" v-model:value="row.consequence" :autosize="{minRows:1}" class="text-xs" @blur="editRow(row)" />
</td>
<td :class="TD" class="text-center">
<NInputNumber v-model:value="row.lvalue" size="tiny" :min="l_options[0].key" :max="l_options[l_options.length-1].key" :show-button="false" class="text-center w-10" />
<NInputNumber v-model:value="row.l_value" size="tiny" :min="l_options[0].key" :max="l_options[l_options.length-1].key" :show-button="false" placeholder="" class="text-center w-10" @blur="editRow(row)" />
</td>
<td :class="TD" class="text-center">
<NInputNumber v-model:value="row.svalue" size="tiny" :min="s_options[0].key" :max="l_options[s_options.length-1].key" :show-button="false" class="text-center w-10" />
<NInputNumber v-model:value="row.s_value" size="tiny" :min="s_options[0].key" :max="s_options[s_options.length-1].key" :show-button="false" placeholder="" class="text-center w-10" @blur="editRow(row)" />
</td>
<td :class="TD" class="text-center">
<span class="inline-block rounded px-1.5 py-0.5 text-[11px] font-medium">
{{ row.risk_value }}
</span>
<NTag
v-if="row.l_value>0 && row.s_value>0"
class="text-[11px]"
size="small"
:color="{
color: riskLevel(row.l_value, row.s_value)?.bg_color,
textColor: riskLevel(row.l_value, row.s_value)?.font_color,
borderColor: riskLevel(row.l_value, row.s_value)?.bg_color,
}"
>
{{riskLevel(row.l_value, row.s_value)?.name}}({{row.l_value * row.s_value}})
</NTag>
</td>
<td :class="TD">
<Ed :v="row.safeguards" @update="(x) => updRow(Number(ri), { safeguards: x })" />
<NInput type="textarea" size="small" v-model:value="row.safeguards" :autosize="{minRows:1}" class="text-xs" @blur="editRow(row)" />
</td>
<td :class="TD">
<Ed :v="row.suggestions" @update="(x) => updRow(Number(ri), { suggestions: x })" />
<NInput type="textarea" size="small" v-model:value="row.suggestions" :autosize="{minRows:1}" class="text-xs" @blur="editRow(row)" />
</td>
</tr>
</tbody>