高风险场景查看分析记录,hazop和fmea完成

This commit is contained in:
2026-09-20 16:18:18 +08:00
parent 05b7357ca6
commit 7ea6ef4f2d
8 changed files with 1125 additions and 67 deletions
+224
View File
@@ -0,0 +1,224 @@
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { analysisRecordListApi } from "@/service/api/highRisk";
const themeStore = useThemeStore();
const props = defineProps(['currentId', 'info', 'fmeaRiskMatrixConfig'])
const emit = defineEmits(['close'])
const loading = ref<boolean>(false);
// 当前偏离
const currentDev = ref<any>(null);
// 展开收起
const collapsed = ref<Record<string, boolean>>({})
// 节点列表
const nodes = ref<any>([]);
// 点击偏离
const clickDev = (dev:any) => {
currentDev.value = dev
}
// 根据l和s计算RR风险等级
const riskLevel = (l: number, s: number) => {
const num = l * s;
for (let i = 0; i < props.fmeaRiskMatrixConfig?.risk_grades.length; i++) {
const item = props.fmeaRiskMatrixConfig?.risk_grades[i];
if (num >= item.min && num <= item.max) {
return item; // 返回整条数据
}
}
return null;
}
// 计算RR1风险等级
const countRiskLevel = (row: any) => {
const l = row.l_value;
const s = row.s_value;
const newArr = [...row?.safeguards || [],...row?.suggestions || []];
const result:any = { L: 0, S: 0 };
newArr.forEach(item => {
if (!item.value) return; // 跳过空值
const parts = item.value.split('_');
if (parts.length === 2) {
const type = parts[0].toUpperCase(); // 统一大写
const num = parseInt(parts[1], 10);
if (!isNaN(num) && (type === 'L' || type === 'S')) {
result[type] += num;
}
}
});
const newL = (l - result.L) > 0 ? l - result.L : 1;
const newS = (s - result.S) > 0 ? s - result.S : 1;
return riskLevel(newL, newS);
}
// 展开收起
const leftToggle = (id: string) => {
collapsed.value = { ...collapsed.value, [id]: !collapsed.value[id] }
}
// 导出数据
const exportData = () => {
window.$message?.info('还差接口')
}
// 获取记录
const getList = async () => {
loading.value = true;
const {data,error} = await analysisRecordListApi(props.currentId);
if(!error){
nodes.value = data?.fmea?.nodes || [];
clickDev(nodes.value[0]?.deviations[0] || null)
}
loading.value = false;
}
onMounted(() => {
getList();
})
</script>
<template>
<NSpin :show="loading" size="small">
<div class="flex items-center gap-4 mb-3">
<p v-if="props.info?.change_title" class="text-xs text-slate-500">变更:<span class="text-slate-800">{{ props.info?.change_title }}</span></p>
<p v-if="props.info?.created_at" class="text-xs text-slate-500">分析日期{{ props.info?.created_at?.slice(0,10) }}</p>
<p v-if="props.info?.applicant_name" class="text-xs text-slate-500">责任人{{ props.info?.applicant_name }}</p>
<p class="ml-auto text-xs text-slate-500">分析编制与修改在变更上下文内完成此处只读</p>
</div>
<div
class="flex gap-3"
:style="{
'--theme-color': themeStore.themeColor,
'--success-color': themeStore.otherColor.success
}"
>
<!-- 节点 / 偏离树 -->
<div class="w-240px">
<div class="shrink-0 rounded-md border p-2 scroll">
<p v-if="nodes.length === 0" class="text-center text-xs text-slate-400 py-10">暂无数据</p>
<div v-for="(node, nodeIndex) in nodes" :key="nodeIndex">
<div
class="cursor-pointer flex items-center select-none"
@click="leftToggle(node.id)"
>
<Icon v-if="collapsed[node.id]" icon="akar-icons:chevron-right" class="size-12px" />
<Icon v-else icon="akar-icons:chevron-down" class="size-12px" />
<p class="flex-1 px-2 py-1.5 text-xs font-semibold text-slate-700">步骤{{ Number(nodeIndex) + 1 }}{{ node.node_name }}</p>
</div>
<template v-if="!collapsed[node.id]">
<div v-for="(dev, devIndex) in node.deviations" :key="devIndex"
@click="clickDev(dev)"
class="cursor-pointer flex items-center ml-4 gap-1.5 rounded-md px-2 py-1.5 text-left text-xs transition"
:class="currentDev?.id === dev.id ? 'bg-[var(--theme-color)] text-white' : 'text-slate-600 hover:bg-slate-100'"
>
<span class="flex-1">{{ dev.deviation }}</span>
<span class="rounded-full bg-emerald-100 px-1.5 text-[10px] text-emerald-600">
{{ dev.rows.length }}
</span>
</div>
</template>
</div>
</div>
</div>
<!-- FMEA 记录表 -->
<div class="min-w-0 flex-1 bg-white">
<div class="scroll">
<NTable size="small" class="w-full text-xs">
<thead>
<tr>
<th class="w-60">原因</th>
<th class="w-60">后果</th>
<th class="w-6">L</th>
<th class="w-6">S</th>
<th class="w-20">RR</th>
<th class="w-60">安全措施</th>
<th class="w-60">建议措施</th>
<th class="w-20">RR1</th>
</tr>
</thead>
<tbody>
<tr v-if="!currentDev || currentDev?.rows?.length === 0">
<td colspan="9" class="px-3 py-8 text-center text-xs text-slate-400">
当前暂无分析记录
</td>
</tr>
<tr v-for="(row, ri) in currentDev?.rows" :key="ri">
<td class="align-top">
<p class="text-xs">{{row.cause}}</p>
</td>
<td class="align-top">
<p class="text-xs">{{row.consequence}}</p>
</td>
<td>
<p class="text-xs text-center w-10">{{row.l_value}}</p>
</td>
<td>
<p class="text-xs text-center w-10">{{row.s_value}}</p>
</td>
<td class="text-center">
<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="align-top">
<div v-if="row.safeguards && row.safeguards.length" class="flex flex-col gap-3">
<div v-for="(sItem, si) in row.safeguards" :key="si">
<p class="text-xs">{{Number(si)+1}}{{sItem.name}}</p>
</div>
</div>
</td>
<td class="align-top">
<div v-if="row.suggestions && row.suggestions.length" class="flex flex-col gap-2">
<div v-for="(sItem2, si2) in row.suggestions" :key="si2">
<p class="text-xs">{{Number(si2)+1}}{{sItem2.name}}</p>
</div>
</div>
</td>
<td class="text-center">
<NTag
v-if="row.l_value>0 && row.s_value>0"
class="text-[11px]"
size="small"
:color="{
color: countRiskLevel(row)?.bg_color,
textColor: countRiskLevel(row)?.font_color,
borderColor: countRiskLevel(row)?.bg_color,
}"
>
{{countRiskLevel(row)?.name}}
</NTag>
</td>
</tr>
</tbody>
</NTable>
</div>
</div>
</div>
<div class="flex items-center justify-end gap-2 pt-3">
<NButton size="small" @click="exportData">
<Icon icon="bi:download" class="mr-1 size-12px" />
导出
</NButton>
<NButton type="primary" size="small" @click="emit('close')">关闭</NButton>
</div>
</NSpin>
</template>
<style scoped lang="scss">
.scroll {
height: calc(100vh - 155px);
overflow-y: auto;
}
</style>