225 lines
8.2 KiB
Vue
225 lines
8.2 KiB
Vue
<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="material-symbols:download" class="mr-1 size-14px" />
|
||
导出
|
||
</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>
|